use core::str; use std::{ collections::HashMap, fmt::Display, fs::File, io::{BufRead, BufReader}, path, str::FromStr, }; use anyhow::{anyhow, Context, Result}; use chrono::{DateTime, Local, Utc}; use clap::Parser; use log::{debug, error, info, trace, warn}; use regex::Regex; use semver::Version; /// Parse master jathub logfile for PS Board QAQC and write out to CSV. #[derive(Parser, Debug)] #[command(version, about, long_about = None)] struct Args { /// Master log file. master_log: path::PathBuf, #[command(flatten)] verbose: clap_verbosity_flag::Verbosity, } /// Layer #[derive(Debug, PartialEq, Eq, Hash, Clone)] enum PositionLayer { A, B, } impl Display for PositionLayer { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { let c = match self { PositionLayer::A => "A", PositionLayer::B => "B", }; write!(f, "{}", c) } } impl FromStr for PositionLayer { type Err = anyhow::Error; fn from_str(s: &str) -> std::result::Result { match s { "A" => Ok(Self::A), "B" => Ok(Self::B), _ => Err(anyhow!("Invalid value")), } } } /// Where PS Board is placed while QAQC. /// TODO: name #[derive(Debug, PartialEq, Eq, Hash, Clone)] struct Position { major: PositionLayer, minor: u8, patch: u8, } impl Display for Position { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "{}-{}-{}", self.major, self.minor, self.patch) } } impl FromStr for Position { type Err = anyhow::Error; fn from_str(s: &str) -> std::result::Result { let mut count = 0; let mut major_pre = None; let mut minor_pre = None; let mut patch_pre = None; for part in s.split('-') { count += 1; match count { 1 => major_pre = Some(part.parse()?), 2 => minor_pre = Some(part.parse()?), 3 => patch_pre = Some(part.parse()?), _ => (), } } if count > 3 { return Err(anyhow!("Invalid Position format")); } Ok(Position { major: major_pre.context("No major")?, minor: minor_pre.context("No minor")?, patch: patch_pre.context("No patch")?, }) } } /// PSB ID printed on the tape on the board. #[derive(Debug, PartialEq)] struct PsbId { id: u8, } impl PsbId { /// Without validation. pub fn new(id: u8) -> Self { PsbId { id } } } impl FromStr for PsbId { type Err = anyhow::Error; fn from_str(s: &str) -> std::result::Result { let num: u8 = s.parse()?; // TODO: add validation warn!("No validation implemented"); Ok(PsbId::new(num)) } } /// QAQC results for each boards extracted from master log. /// TODO: specify each filed types.(maybe multi values) #[derive(Debug)] struct BoardResult { id: PsbId, qspif: bool, qspip: bool, recov: bool, clock: bool, regac: bool, asdtp: bool, done: bool, } /// Full result for a single QAQC run from a log file on JATHub master. #[derive(Debug)] struct MasterLogResult { version: Version, datetime: DateTime, shifter: String, board_results: HashMap, } /// Get version of shift script. fn extract_version(line: &str) -> Result { Ok(line .split_whitespace() .nth(2) .context("Invalid log format(version)")? .parse()?) } /// Get shifters from shifter line fn extract_shifter_line(line: &str) -> Result { let re = Regex::new(r"^Shifters: (.+)$").unwrap(); let caps = re.captures(line).unwrap(); trace!("Regex {:?}", caps); Ok(caps.get(1).unwrap().as_str().to_owned()) } /// Get position and PSB ID pair from a master log line. fn extract_position_id(line: &str) -> Result<(Position, PsbId)> { let re = Regex::new(r"Position / assigned-ID : (.+) / (.+)").unwrap(); let caps = re.captures(line).context("No capture")?; let pos = Position::from_str(caps.get(1).unwrap().into())?; let id = PsbId::from_str(caps.get(2).unwrap().into())?; Ok((pos, id)) } impl MasterLogResult { /// Parse log file on master jathub. fn parse_file(file: impl BufRead) -> Result { let mut lines = file.lines(); let version = { let line = lines.next().context("Invalid log format(no versions)")??; extract_version(&line)? }; let _sep = lines.next().context("Invalid log format(separator)")??; let datetime: DateTime = { let line = lines .next() .context("Invalid log format(no datetime line)")??; DateTime::parse_from_str(&line, "Date: %+") .context("Invalid datetime format (must be ISO 8601)")? .to_utc() }; let shifter = { let line = lines .next() .context("Invalid log format(no shifter line)")??; extract_shifter_line(&line)? }; let _sep = lines.next().context("Invalid log format")?; if !lines .next() .context("Invalid log format")?? .starts_with("PBS Assignment:") { return Err(anyhow!("Invalid log format")); } let mut assignments = HashMap::new(); for i in 0..18 { let line = lines.next().context("Unexpected EOF")??; if line.starts_with("=========") { debug!("End of assignments"); break; } let (pos, id) = extract_position_id(&line)?; match assignments.insert(pos.clone(), id) { None => (), Some(old_id) => { return Err(anyhow!( "Value already exists on row {}: {:?} => {:?}", i, pos, old_id )); } }; } trace!("Read all PBS assignments"); info!("{:?}", assignments); todo!() } } fn main() -> Result<()> { let args = Args::parse(); env_logger::Builder::new() .filter_level(args.verbose.log_level_filter()) .init(); debug!("Args: {:?}", args); println!("Hello, world!"); let file = File::open(args.master_log)?; let mut reader = BufReader::new(file); let result = MasterLogResult::parse_file(reader)?; Ok(()) } #[cfg(test)] mod test { use std::str::FromStr; use chrono::{DateTime, Utc}; use crate::{extract_position_id, extract_shifter_line, Position, PositionLayer, PsbId}; #[test] fn parse_position() { assert_eq!( Position::from_str("A-1-4").unwrap(), Position { major: PositionLayer::A, minor: 1, patch: 4 } ); assert_eq!( Position::from_str("A-0-9").unwrap(), Position { major: PositionLayer::A, minor: 0, patch: 9 } ); assert_ne!( Position::from_str("A-1-4").unwrap(), Position { major: PositionLayer::A, minor: 0, patch: 2 } ); } #[test] fn parse_datetime() { assert!(DateTime::parse_from_str("2024-06-20T08:42:01+0000", "%+").is_ok()); assert!(DateTime::parse_from_str("Date: 2024-06-20T08:42:01+0000", "Date: %+").is_ok()); } #[test] fn parse_shifter_line() { assert_eq!(extract_shifter_line("Shifters: foo").unwrap(), "foo"); assert_eq!( extract_shifter_line("Shifters: foo bar").unwrap(), "foo bar" ); } #[test] fn parse_pos_id_line() { assert_eq!( extract_position_id("Position / assigned-ID : A-0-0 / 000004").unwrap(), ( Position { major: PositionLayer::A, minor: 0, patch: 0, }, PsbId::new(4) ) ); assert_eq!( extract_position_id("Position / assigned-ID : A-1-7 / 000108").unwrap(), ( Position { major: PositionLayer::A, minor: 1, patch: 7, }, PsbId::new(108) ) ); assert_ne!( extract_position_id("Position / assigned-ID : A-1-7 / 000108").unwrap(), ( Position { major: PositionLayer::A, minor: 0, patch: 7, }, PsbId::new(106) ) ); } }