diff --git a/fpga_arch_viewer/src/error_report.rs b/fpga_arch_viewer/src/error_report.rs new file mode 100644 index 0000000..6d27989 --- /dev/null +++ b/fpga_arch_viewer/src/error_report.rs @@ -0,0 +1,157 @@ +use fpga_arch_parser::FPGAArchParseError; +use std::io::{BufRead, BufReader}; + +fn get_file_line(file_path: &std::path::Path, line_num: u64) -> Option { + let file = std::fs::File::open(file_path).ok()?; + let reader = BufReader::new(file); + let target = line_num.saturating_sub(1) as usize; + reader.lines().nth(target).and_then(Result::ok) +} + +fn format_context_line(line: &str, column: u64) -> String { + let column = column as usize; + let mut result = format!(" {}\n", line); + let offset = column.saturating_sub(1).min(line.len()); + result.push_str(&format!(" {}{}", " ".repeat(offset), "^")); + result +} + +pub(crate) fn format_parse_error( + error: &FPGAArchParseError, + file_path: Option<&std::path::Path>, +) -> String { + match error { + FPGAArchParseError::ArchFileOpenError(msg) => { + format!("Failed to open architecture file:\n{}", msg) + } + FPGAArchParseError::MissingRequiredTag(tag) => { + format!("Missing required XML tag: {}", tag) + } + FPGAArchParseError::MissingRequiredAttribute(attr, pos) => { + let mut msg = format!( + "Missing required attribute '{}' at line {}, column {}", + attr, + pos.row + 1, + pos.column + 1 + ); + if let Some(path) = file_path + && let Some(line) = get_file_line(path, pos.row + 1) + { + msg.push_str("\n\n"); + msg.push_str(&format_context_line(&line, pos.column + 1)); + } + msg + } + FPGAArchParseError::InvalidTag(tag, pos) => { + let mut msg = format!( + "Invalid or unexpected tag '{}' at line {}, column {}", + tag, + pos.row + 1, + pos.column + 1 + ); + if let Some(path) = file_path + && let Some(line) = get_file_line(path, pos.row + 1) + { + msg.push_str("\n\n"); + msg.push_str(&format_context_line(&line, pos.column + 1)); + } + msg + } + FPGAArchParseError::XMLParseError(msg_text, pos) => { + let mut msg = format!( + "XML parsing error at line {}, column {}:\n{}", + pos.row + 1, + pos.column + 1, + msg_text + ); + if let Some(path) = file_path + && let Some(line) = get_file_line(path, pos.row + 1) + { + msg.push_str("\n\n"); + msg.push_str(&format_context_line(&line, pos.column + 1)); + } + msg + } + FPGAArchParseError::UnknownAttribute(attr, pos) => { + let mut msg = format!( + "Unknown attribute '{}' at line {}, column {}", + attr, + pos.row + 1, + pos.column + 1 + ); + if let Some(path) = file_path + && let Some(line) = get_file_line(path, pos.row + 1) + { + msg.push_str("\n\n"); + msg.push_str(&format_context_line(&line, pos.column + 1)); + } + msg + } + FPGAArchParseError::DuplicateTag(tag, pos) => { + let mut msg = format!( + "Duplicate tag '{}' at line {}, column {}", + tag, + pos.row + 1, + pos.column + 1 + ); + if let Some(path) = file_path + && let Some(line) = get_file_line(path, pos.row + 1) + { + msg.push_str("\n\n"); + msg.push_str(&format_context_line(&line, pos.column + 1)); + } + msg + } + FPGAArchParseError::DuplicateAttribute(attr, pos) => { + let mut msg = format!( + "Duplicate attribute '{}' at line {}, column {}", + attr, + pos.row + 1, + pos.column + 1 + ); + if let Some(path) = file_path + && let Some(line) = get_file_line(path, pos.row + 1) + { + msg.push_str("\n\n"); + msg.push_str(&format_context_line(&line, pos.column + 1)); + } + msg + } + FPGAArchParseError::UnexpectedEndTag(tag, pos) => { + let mut msg = format!( + "Unexpected end tag '' at line {}, column {}", + tag, + pos.row + 1, + pos.column + 1 + ); + if let Some(path) = file_path + && let Some(line) = get_file_line(path, pos.row + 1) + { + msg.push_str("\n\n"); + msg.push_str(&format_context_line(&line, pos.column + 1)); + } + msg + } + FPGAArchParseError::AttributeParseError(msg_text, pos) => { + let mut msg = format!( + "Failed to parse attribute at line {}, column {}:\n{}", + pos.row + 1, + pos.column + 1, + msg_text + ); + if let Some(path) = file_path + && let Some(line) = get_file_line(path, pos.row + 1) + { + msg.push_str("\n\n"); + msg.push_str(&format_context_line(&line, pos.column + 1)); + } + msg + } + FPGAArchParseError::UnexpectedEndOfDocument(msg) => { + format!("Unexpected end of document:\n{}", msg) + } + FPGAArchParseError::PinParsingError(msg) => { + format!("Pin parsing error:\n{}", msg) + } + } +} diff --git a/fpga_arch_viewer/src/main.rs b/fpga_arch_viewer/src/main.rs index fb1bf49..29aefa1 100644 --- a/fpga_arch_viewer/src/main.rs +++ b/fpga_arch_viewer/src/main.rs @@ -8,6 +8,7 @@ mod common_ui; mod complex_block_view; mod crr_sb_view; mod crr_view; +mod error_report; mod grid; mod grid_renderer; mod grid_view; @@ -27,6 +28,46 @@ fn main() -> Result<(), eframe::Error> { // Log to stderr (if you run with `RUST_LOG=debug`). env_logger::init(); + let args: Vec = std::env::args().collect(); + + if let Some(unknown) = args + .iter() + .skip(1) + .find(|a| a.starts_with('-') && *a != "--parse-only") + { + eprintln!("error: unknown argument: {unknown}"); + std::process::exit(1); + } + + let parse_only = args.iter().any(|a| a == "--parse-only"); + let initial_file = args + .iter() + .skip(1) + .find(|a| !a.starts_with('-')) + .map(std::path::PathBuf::from); + + // --parse-only: parse the architecture file and report errors without opening the GUI. + if parse_only { + let Some(file_path) = initial_file.as_deref() else { + eprintln!("error: --parse-only requires a file path"); + std::process::exit(1); + }; + match fpga_arch_parser::parse(file_path) { + Ok(_) => { + println!("Successfully parsed: {}", file_path.display()); + return Ok(()); + } + Err(e) => { + eprintln!( + "Parse error in {}:\n{}", + file_path.display(), + error_report::format_parse_error(&e, Some(file_path)) + ); + std::process::exit(1); + } + } + } + // Load the icon data. let icon_data = eframe::icon_data::from_png_bytes(&include_bytes!("../assets/icon-256.png")[..]); @@ -41,7 +82,7 @@ fn main() -> Result<(), eframe::Error> { eframe::run_native( "FPGA Architecture Visualizer", options, - Box::new(|_cc| Ok(Box::new(viewer::FpgaViewer::new()))), + Box::new(|_cc| Ok(Box::new(viewer::FpgaViewer::new(initial_file)))), ) } @@ -70,7 +111,7 @@ fn main() { .start( canvas, web_options, - Box::new(|_cc| Ok(Box::new(viewer::FpgaViewer::new()))), + Box::new(|_cc| Ok(Box::new(viewer::FpgaViewer::new(None)))), ) .await; diff --git a/fpga_arch_viewer/src/viewer.rs b/fpga_arch_viewer/src/viewer.rs index e137eb3..b639b58 100644 --- a/fpga_arch_viewer/src/viewer.rs +++ b/fpga_arch_viewer/src/viewer.rs @@ -1,7 +1,8 @@ use eframe::egui; -use fpga_arch_parser::{FPGAArch, FPGAArchParseError}; +use fpga_arch_parser::FPGAArch; use log::{info, warn}; -use std::io::{BufRead, BufReader}; + +use crate::error_report::format_parse_error; #[cfg(target_arch = "wasm32")] use rfd::AsyncFileDialog; @@ -90,161 +91,9 @@ pub struct FpgaViewer { pending_file_dialog: Option>>, } -fn get_file_line(file_path: &std::path::Path, line_num: u64) -> Option { - let file = std::fs::File::open(file_path).ok()?; - let reader = BufReader::new(file); - let target = line_num.saturating_sub(1) as usize; - reader.lines().nth(target).and_then(Result::ok) -} - -fn format_context_line(line: &str, column: u64) -> String { - let column = column as usize; - let mut result = format!(" {}\n", line); - let offset = column.saturating_sub(1).min(line.len()); - result.push_str(&format!(" {}{}", " ".repeat(offset), "^")); - result -} - -fn format_parse_error(error: &FPGAArchParseError, file_path: Option<&std::path::Path>) -> String { - match error { - FPGAArchParseError::ArchFileOpenError(msg) => { - format!("Failed to open architecture file:\n{}", msg) - } - FPGAArchParseError::MissingRequiredTag(tag) => { - format!("Missing required XML tag: {}", tag) - } - FPGAArchParseError::MissingRequiredAttribute(attr, pos) => { - let mut msg = format!( - "Missing required attribute '{}' at line {}, column {}", - attr, - pos.row + 1, - pos.column + 1 - ); - if let Some(path) = file_path - && let Some(line) = get_file_line(path, pos.row + 1) - { - msg.push_str("\n\n"); - msg.push_str(&format_context_line(&line, pos.column + 1)); - } - msg - } - FPGAArchParseError::InvalidTag(tag, pos) => { - let mut msg = format!( - "Invalid or unexpected tag '{}' at line {}, column {}", - tag, - pos.row + 1, - pos.column + 1 - ); - if let Some(path) = file_path - && let Some(line) = get_file_line(path, pos.row + 1) - { - msg.push_str("\n\n"); - msg.push_str(&format_context_line(&line, pos.column + 1)); - } - msg - } - FPGAArchParseError::XMLParseError(msg_text, pos) => { - let mut msg = format!( - "XML parsing error at line {}, column {}:\n{}", - pos.row + 1, - pos.column + 1, - msg_text - ); - if let Some(path) = file_path - && let Some(line) = get_file_line(path, pos.row + 1) - { - msg.push_str("\n\n"); - msg.push_str(&format_context_line(&line, pos.column + 1)); - } - msg - } - FPGAArchParseError::UnknownAttribute(attr, pos) => { - let mut msg = format!( - "Unknown attribute '{}' at line {}, column {}", - attr, - pos.row + 1, - pos.column + 1 - ); - if let Some(path) = file_path - && let Some(line) = get_file_line(path, pos.row + 1) - { - msg.push_str("\n\n"); - msg.push_str(&format_context_line(&line, pos.column + 1)); - } - msg - } - FPGAArchParseError::DuplicateTag(tag, pos) => { - let mut msg = format!( - "Duplicate tag '{}' at line {}, column {}", - tag, - pos.row + 1, - pos.column + 1 - ); - if let Some(path) = file_path - && let Some(line) = get_file_line(path, pos.row + 1) - { - msg.push_str("\n\n"); - msg.push_str(&format_context_line(&line, pos.column + 1)); - } - msg - } - FPGAArchParseError::DuplicateAttribute(attr, pos) => { - let mut msg = format!( - "Duplicate attribute '{}' at line {}, column {}", - attr, - pos.row + 1, - pos.column + 1 - ); - if let Some(path) = file_path - && let Some(line) = get_file_line(path, pos.row + 1) - { - msg.push_str("\n\n"); - msg.push_str(&format_context_line(&line, pos.column + 1)); - } - msg - } - FPGAArchParseError::UnexpectedEndTag(tag, pos) => { - let mut msg = format!( - "Unexpected end tag '' at line {}, column {}", - tag, - pos.row + 1, - pos.column + 1 - ); - if let Some(path) = file_path - && let Some(line) = get_file_line(path, pos.row + 1) - { - msg.push_str("\n\n"); - msg.push_str(&format_context_line(&line, pos.column + 1)); - } - msg - } - FPGAArchParseError::AttributeParseError(msg_text, pos) => { - let mut msg = format!( - "Failed to parse attribute at line {}, column {}:\n{}", - pos.row + 1, - pos.column + 1, - msg_text - ); - if let Some(path) = file_path - && let Some(line) = get_file_line(path, pos.row + 1) - { - msg.push_str("\n\n"); - msg.push_str(&format_context_line(&line, pos.column + 1)); - } - msg - } - FPGAArchParseError::UnexpectedEndOfDocument(msg) => { - format!("Unexpected end of document:\n{}", msg) - } - FPGAArchParseError::PinParsingError(msg) => { - format!("Pin parsing error:\n{}", msg) - } - } -} - impl FpgaViewer { - pub fn new() -> Self { - Self { + pub fn new(initial_file: Option) -> Self { + let mut viewer = Self { architecture: None, viewer_ctx: ViewerContext { show_about: false, @@ -270,7 +119,11 @@ impl FpgaViewer { fps: 0.0, #[cfg(not(target_arch = "wasm32"))] pending_file_dialog: None, + }; + if let Some(path) = initial_file { + viewer.load_architecture_file(path); } + viewer } fn loaded_arch_filename(&self) -> Option { diff --git a/fpga_arch_viewer/tests/fixtures/invalid_arch.xml b/fpga_arch_viewer/tests/fixtures/invalid_arch.xml new file mode 100644 index 0000000..37b1930 --- /dev/null +++ b/fpga_arch_viewer/tests/fixtures/invalid_arch.xml @@ -0,0 +1,3 @@ + + + diff --git a/fpga_arch_viewer/tests/parse_only.rs b/fpga_arch_viewer/tests/parse_only.rs new file mode 100644 index 0000000..bc8707f --- /dev/null +++ b/fpga_arch_viewer/tests/parse_only.rs @@ -0,0 +1,91 @@ +use std::process::{Command, Stdio}; + +fn binary() -> &'static str { + env!("CARGO_BIN_EXE_fpga_arch_viewer") +} + +fn valid_arch() -> &'static str { + concat!( + env!("CARGO_MANIFEST_DIR"), + "/../fpga_arch_parser/tests/k4_N4_90nm.xml" + ) +} + +fn invalid_arch() -> &'static str { + concat!( + env!("CARGO_MANIFEST_DIR"), + "/tests/fixtures/invalid_arch.xml" + ) +} + +#[test] +fn parse_only_valid_exits_zero() { + let status = Command::new(binary()) + .args([valid_arch(), "--parse-only"]) + .stderr(Stdio::null()) + .status() + .unwrap(); + assert!(status.success()); +} + +#[test] +fn parse_only_valid_prints_success() { + let output = Command::new(binary()) + .args([valid_arch(), "--parse-only"]) + .output() + .unwrap(); + assert!(output.status.success()); + let stdout = String::from_utf8_lossy(&output.stdout); + assert!(stdout.contains("Successfully parsed")); +} + +#[test] +fn parse_only_invalid_exits_nonzero() { + let status = Command::new(binary()) + .args([invalid_arch(), "--parse-only"]) + .stderr(Stdio::null()) + .status() + .unwrap(); + assert!(!status.success()); +} + +#[test] +fn parse_only_invalid_reports_error_to_stderr() { + let output = Command::new(binary()) + .args([invalid_arch(), "--parse-only"]) + .output() + .unwrap(); + assert!(!output.status.success()); + let stderr = String::from_utf8_lossy(&output.stderr); + assert!(stderr.contains("Parse error")); +} + +#[test] +fn parse_only_missing_file_exits_nonzero() { + let status = Command::new(binary()) + .args(["/nonexistent/path/arch.xml", "--parse-only"]) + .stderr(Stdio::null()) + .status() + .unwrap(); + assert!(!status.success()); +} + +#[test] +fn parse_only_flag_without_path_exits_nonzero() { + let status = Command::new(binary()) + .arg("--parse-only") + .stderr(Stdio::null()) + .status() + .unwrap(); + assert!(!status.success()); +} + +#[test] +fn unknown_flag_exits_nonzero() { + let status = Command::new(binary()) + .arg("--parse_only") + .stderr(Stdio::null()) + .status() + .unwrap(); + assert!(!status.success()); +}