diff --git a/README.md b/README.md index a743132..f4c47f7 100644 --- a/README.md +++ b/README.md @@ -41,9 +41,9 @@ Legend: | Common (base) | 6 | 4 | 67% | | Common editor/document | 23 | 12 | 52% | | Project manager | 5 | 3 | 60% | -| Board editor (PCB) | 22 | 18 | 82% | +| Board editor (PCB) | 22 | 19 | 86% | | Schematic editor (dedicated proto commands) | 0 | 0 | n/a | -| **Total** | **56** | **37** | **66%** | +| **Total** | **56** | **38** | **68%** | ### Common (base) @@ -112,7 +112,7 @@ Legend: | `RefillZones` | Not yet | - | | `GetPadShapeAsPolygon` | Implemented | `KiCadClient::get_pad_shape_as_polygon_raw`, `KiCadClient::get_pad_shape_as_polygon` | | `CheckPadstackPresenceOnLayers` | Implemented | `KiCadClient::check_padstack_presence_on_layers_raw`, `KiCadClient::check_padstack_presence_on_layers` | -| `InjectDrcError` | Not yet | - | +| `InjectDrcError` | Implemented | `KiCadClient::inject_drc_error_raw`, `KiCadClient::inject_drc_error` | | `GetVisibleLayers` | Implemented | `KiCadClient::get_visible_layers` | | `SetVisibleLayers` | Implemented | `KiCadClient::set_visible_layers` | | `GetActiveLayer` | Implemented | `KiCadClient::get_active_layer` | diff --git a/docs/TEST_CLI.md b/docs/TEST_CLI.md index f9d67e2..a3bbb3b 100644 --- a/docs/TEST_CLI.md +++ b/docs/TEST_CLI.md @@ -261,6 +261,12 @@ Set editor appearance: cargo run --bin kicad-ipc-cli -- set-appearance --inactive-layer-display hidden --net-color-display all --board-flip normal --ratsnest-display all-layers ``` +Inject DRC marker: + +```bash +cargo run --bin kicad-ipc-cli -- inject-drc-error --severity error --message "API marker test" --x-nm 1000000 --y-nm 1000000 +``` + Show typed netclass map: ```bash diff --git a/src/client.rs b/src/client.rs index f161327..639a6dd 100644 --- a/src/client.rs +++ b/src/client.rs @@ -9,7 +9,7 @@ use crate::model::board::{ ArcStartMidEndNm, BoardEditorAppearanceSettings, BoardEnabledLayers, BoardFlipMode, BoardLayerClass, BoardLayerGraphicsDefault, BoardLayerInfo, BoardNet, BoardOriginKind, BoardStackup, BoardStackupDielectricProperties, BoardStackupLayer, BoardStackupLayerType, - ColorRgba, GraphicsDefaults, InactiveLayerDisplayMode, NetClassBoardSettings, + ColorRgba, DrcSeverity, GraphicsDefaults, InactiveLayerDisplayMode, NetClassBoardSettings, NetClassForNetEntry, NetClassInfo, NetClassType, NetColorDisplayMode, PadNetEntry, PadShapeAsPolygonEntry, PadstackPresenceEntry, PadstackPresenceState, PcbArc, PcbBoardGraphicShape, PcbBoardText, PcbBoardTextBox, PcbDimension, PcbField, PcbFootprint, @@ -65,6 +65,7 @@ const CMD_GET_NETCLASS_FOR_NETS: &str = "kiapi.board.commands.GetNetClassForNets const CMD_GET_PAD_SHAPE_AS_POLYGON: &str = "kiapi.board.commands.GetPadShapeAsPolygon"; const CMD_CHECK_PADSTACK_PRESENCE_ON_LAYERS: &str = "kiapi.board.commands.CheckPadstackPresenceOnLayers"; +const CMD_INJECT_DRC_ERROR: &str = "kiapi.board.commands.InjectDrcError"; const CMD_GET_SELECTION: &str = "kiapi.common.commands.GetSelection"; const CMD_BEGIN_COMMIT: &str = "kiapi.common.commands.BeginCommit"; const CMD_END_COMMIT: &str = "kiapi.common.commands.EndCommit"; @@ -95,6 +96,7 @@ const RES_BOARD_EDITOR_APPEARANCE_SETTINGS: &str = const RES_NETCLASS_FOR_NETS_RESPONSE: &str = "kiapi.board.commands.NetClassForNetsResponse"; const RES_PAD_SHAPE_AS_POLYGON_RESPONSE: &str = "kiapi.board.commands.PadShapeAsPolygonResponse"; const RES_PADSTACK_PRESENCE_RESPONSE: &str = "kiapi.board.commands.PadstackPresenceResponse"; +const RES_INJECT_DRC_ERROR_RESPONSE: &str = "kiapi.board.commands.InjectDrcErrorResponse"; const RES_VECTOR2: &str = "kiapi.common.types.Vector2"; const RES_SELECTION_RESPONSE: &str = "kiapi.common.commands.SelectionResponse"; const RES_BEGIN_COMMIT_RESPONSE: &str = "kiapi.common.commands.BeginCommitResponse"; @@ -1047,6 +1049,46 @@ impl KiCadClient { Ok(entries) } + pub async fn inject_drc_error_raw( + &self, + severity: DrcSeverity, + message: impl Into, + position: Option, + item_ids: Vec, + ) -> Result { + let board = self.current_board_document_proto().await?; + let command = board_commands::InjectDrcError { + board: Some(board), + severity: drc_severity_to_proto(severity), + message: message.into(), + position: position.map(vector2_nm_to_proto), + items: item_ids + .into_iter() + .map(|value| common_types::Kiid { value }) + .collect(), + }; + + let response = self + .send_command(envelope::pack_any(&command, CMD_INJECT_DRC_ERROR)) + .await?; + response_payload_as_any(response, RES_INJECT_DRC_ERROR_RESPONSE) + } + + pub async fn inject_drc_error( + &self, + severity: DrcSeverity, + message: impl Into, + position: Option, + item_ids: Vec, + ) -> Result, KiCadError> { + let payload = self + .inject_drc_error_raw(severity, message, position, item_ids) + .await?; + let response: board_commands::InjectDrcErrorResponse = + decode_any(&payload, RES_INJECT_DRC_ERROR_RESPONSE)?; + Ok(response.marker.map(|marker| marker.value)) + } + pub async fn get_board_stackup_raw(&self) -> Result { let command = board_commands::GetBoardStackup { board: Some(self.current_board_document_proto().await?), @@ -1674,6 +1716,19 @@ fn board_origin_kind_to_proto(kind: BoardOriginKind) -> i32 { } } +fn drc_severity_to_proto(value: DrcSeverity) -> i32 { + match value { + DrcSeverity::Warning => board_commands::DrcSeverity::DrsWarning as i32, + DrcSeverity::Error => board_commands::DrcSeverity::DrsError as i32, + DrcSeverity::Exclusion => board_commands::DrcSeverity::DrsExclusion as i32, + DrcSeverity::Ignore => board_commands::DrcSeverity::DrsIgnore as i32, + DrcSeverity::Info => board_commands::DrcSeverity::DrsInfo as i32, + DrcSeverity::Action => board_commands::DrcSeverity::DrsAction as i32, + DrcSeverity::Debug => board_commands::DrcSeverity::DrsDebug as i32, + DrcSeverity::Undefined => board_commands::DrcSeverity::DrsUndefined as i32, + } +} + fn commit_action_to_proto(action: CommitAction) -> i32 { match action { CommitAction::Commit => common_commands::CommitAction::CmaCommit as i32, @@ -2854,12 +2909,12 @@ fn default_client_name() -> String { mod tests { use super::{ any_to_pretty_debug, board_editor_appearance_settings_to_proto, commit_action_to_proto, - ensure_item_request_ok, layer_to_model, map_commit_session, map_hit_test_result, - map_item_bounding_boxes, map_polygon_with_holes, model_document_to_proto, - normalize_socket_uri, pad_netlist_from_footprint_items, response_payload_as_any, - select_single_board_document, select_single_project_path, selection_item_detail, - summarize_item_details, summarize_selection, text_horizontal_alignment_to_proto, - text_spec_to_proto, PCB_OBJECT_TYPES, + drc_severity_to_proto, ensure_item_request_ok, layer_to_model, map_commit_session, + map_hit_test_result, map_item_bounding_boxes, map_polygon_with_holes, + model_document_to_proto, normalize_socket_uri, pad_netlist_from_footprint_items, + response_payload_as_any, select_single_board_document, select_single_project_path, + selection_item_detail, summarize_item_details, summarize_selection, + text_horizontal_alignment_to_proto, text_spec_to_proto, PCB_OBJECT_TYPES, }; use crate::error::KiCadError; use crate::model::common::{ @@ -3027,6 +3082,18 @@ mod tests { ); } + #[test] + fn drc_severity_to_proto_maps_known_variants() { + assert_eq!( + drc_severity_to_proto(crate::model::board::DrcSeverity::Warning), + crate::proto::kiapi::board::commands::DrcSeverity::DrsWarning as i32 + ); + assert_eq!( + drc_severity_to_proto(crate::model::board::DrcSeverity::Error), + crate::proto::kiapi::board::commands::DrcSeverity::DrsError as i32 + ); + } + #[test] fn board_editor_appearance_settings_to_proto_maps_known_variants() { let proto = board_editor_appearance_settings_to_proto( diff --git a/src/lib.rs b/src/lib.rs index 23bf66e..b922904 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -24,7 +24,7 @@ pub use crate::model::board::{ ArcStartMidEndNm, BoardEditorAppearanceSettings, BoardEnabledLayers, BoardFlipMode, BoardLayerClass, BoardLayerGraphicsDefault, BoardLayerInfo, BoardNet, BoardOriginKind, BoardStackup, BoardStackupDielectricProperties, BoardStackupLayer, BoardStackupLayerType, - ColorRgba, GraphicsDefaults, InactiveLayerDisplayMode, NetClassBoardSettings, + ColorRgba, DrcSeverity, GraphicsDefaults, InactiveLayerDisplayMode, NetClassBoardSettings, NetClassForNetEntry, NetClassInfo, NetClassType, NetColorDisplayMode, PadNetEntry, PadShapeAsPolygonEntry, PadstackPresenceEntry, PadstackPresenceState, PcbArc, PcbBoardGraphicShape, PcbBoardText, PcbBoardTextBox, PcbDimension, PcbField, PcbFootprint, diff --git a/src/model/board.rs b/src/model/board.rs index ca3f97d..c289065 100644 --- a/src/model/board.rs +++ b/src/model/board.rs @@ -224,6 +224,54 @@ pub enum RatsnestDisplayMode { Unknown(i32), } +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub enum DrcSeverity { + Warning, + Error, + Exclusion, + Ignore, + Info, + Action, + Debug, + Undefined, +} + +impl std::fmt::Display for DrcSeverity { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let value = match self { + Self::Warning => "warning", + Self::Error => "error", + Self::Exclusion => "exclusion", + Self::Ignore => "ignore", + Self::Info => "info", + Self::Action => "action", + Self::Debug => "debug", + Self::Undefined => "undefined", + }; + write!(f, "{value}") + } +} + +impl FromStr for DrcSeverity { + type Err = String; + + fn from_str(value: &str) -> Result { + match value { + "warning" => Ok(Self::Warning), + "error" => Ok(Self::Error), + "exclusion" => Ok(Self::Exclusion), + "ignore" => Ok(Self::Ignore), + "info" => Ok(Self::Info), + "action" => Ok(Self::Action), + "debug" => Ok(Self::Debug), + "undefined" => Ok(Self::Undefined), + _ => Err(format!( + "unknown drc severity `{value}`; expected warning, error, exclusion, ignore, info, action, debug, or undefined" + )), + } + } +} + #[derive(Clone, Debug, Eq, PartialEq)] pub struct BoardEditorAppearanceSettings { pub inactive_layer_display: InactiveLayerDisplayMode, @@ -424,7 +472,7 @@ pub enum PcbItem { mod tests { use std::str::FromStr; - use super::BoardOriginKind; + use super::{BoardOriginKind, DrcSeverity}; #[test] fn board_origin_kind_parses_known_values() { @@ -443,4 +491,22 @@ mod tests { let result = BoardOriginKind::from_str("other"); assert!(result.is_err()); } + + #[test] + fn drc_severity_parses_known_values() { + assert_eq!( + DrcSeverity::from_str("warning").expect("warning should parse"), + DrcSeverity::Warning + ); + assert_eq!( + DrcSeverity::from_str("error").expect("error should parse"), + DrcSeverity::Error + ); + } + + #[test] + fn drc_severity_rejects_unknown_values() { + let result = DrcSeverity::from_str("fatal"); + assert!(result.is_err()); + } } diff --git a/test-scripts/kicad-ipc-cli.rs b/test-scripts/kicad-ipc-cli.rs index 3ae7225..bf97cb1 100644 --- a/test-scripts/kicad-ipc-cli.rs +++ b/test-scripts/kicad-ipc-cli.rs @@ -7,9 +7,9 @@ use std::time::Duration; use kicad_ipc::{ BoardFlipMode, BoardOriginKind, ClientBuilder, CommitAction, CommitSession, DocumentType, - EditorFrameType, InactiveLayerDisplayMode, KiCadClient, KiCadError, NetColorDisplayMode, - PadstackPresenceState, PcbObjectTypeCode, RatsnestDisplayMode, TextObjectSpec, - TextShapeGeometry, TextSpec, Vector2Nm, + DrcSeverity, EditorFrameType, InactiveLayerDisplayMode, KiCadClient, KiCadError, + NetColorDisplayMode, PadstackPresenceState, PcbObjectTypeCode, RatsnestDisplayMode, + TextObjectSpec, TextShapeGeometry, TextSpec, Vector2Nm, }; const REPORT_MAX_PAD_NET_ROWS: usize = 2_000; @@ -68,6 +68,13 @@ enum Command { x_nm: i64, y_nm: i64, }, + InjectDrcError { + severity: DrcSeverity, + message: String, + x_nm: Option, + y_nm: Option, + item_ids: Vec, + }, RefreshEditor { frame: EditorFrameType, }, @@ -371,6 +378,25 @@ async fn run() -> Result<(), KiCadError> { .await?; println!("set_origin_kind={} x_nm={} y_nm={}", kind, x_nm, y_nm); } + Command::InjectDrcError { + severity, + message, + x_nm, + y_nm, + item_ids, + } => { + let position = match (x_nm, y_nm) { + (Some(x_nm), Some(y_nm)) => Some(Vector2Nm { x_nm, y_nm }), + _ => None, + }; + let marker = client + .inject_drc_error(severity, message, position, item_ids) + .await?; + println!( + "drc_marker_id={}", + marker.unwrap_or_else(|| "-".to_string()) + ); + } Command::RefreshEditor { frame } => { client.refresh_editor(frame).await?; println!("refresh_editor=ok frame={}", frame); @@ -1005,6 +1031,79 @@ fn parse_args_from(mut args: Vec) -> Result<(CliConfig, Command), KiCadE })?, } } + "inject-drc-error" => { + let mut severity = DrcSeverity::Error; + let mut message = None; + let mut x_nm = None; + let mut y_nm = None; + let mut item_ids = Vec::new(); + let mut i = 1; + while i < args.len() { + match args[i].as_str() { + "--severity" => { + let value = args.get(i + 1).ok_or_else(|| KiCadError::Config { + reason: "missing value for inject-drc-error --severity".to_string(), + })?; + severity = parse_drc_severity(value) + .map_err(|err| KiCadError::Config { reason: err })?; + i += 2; + } + "--message" => { + let value = args.get(i + 1).ok_or_else(|| KiCadError::Config { + reason: "missing value for inject-drc-error --message".to_string(), + })?; + message = Some(value.clone()); + i += 2; + } + "--x-nm" => { + let value = args.get(i + 1).ok_or_else(|| KiCadError::Config { + reason: "missing value for inject-drc-error --x-nm".to_string(), + })?; + x_nm = Some(value.parse::().map_err(|err| KiCadError::Config { + reason: format!("invalid inject-drc-error --x-nm `{value}`: {err}"), + })?); + i += 2; + } + "--y-nm" => { + let value = args.get(i + 1).ok_or_else(|| KiCadError::Config { + reason: "missing value for inject-drc-error --y-nm".to_string(), + })?; + y_nm = Some(value.parse::().map_err(|err| KiCadError::Config { + reason: format!("invalid inject-drc-error --y-nm `{value}`: {err}"), + })?); + i += 2; + } + "--item-id" => { + let value = args.get(i + 1).ok_or_else(|| KiCadError::Config { + reason: "missing value for inject-drc-error --item-id".to_string(), + })?; + item_ids.push(value.clone()); + i += 2; + } + _ => { + i += 1; + } + } + } + + if (x_nm.is_some() && y_nm.is_none()) || (x_nm.is_none() && y_nm.is_some()) { + return Err(KiCadError::Config { + reason: + "inject-drc-error requires both --x-nm and --y-nm when providing a position" + .to_string(), + }); + } + + Command::InjectDrcError { + severity, + message: message.ok_or_else(|| KiCadError::Config { + reason: "inject-drc-error requires `--message `".to_string(), + })?, + x_nm, + y_nm, + item_ids, + } + } "refresh-editor" => { let mut frame = EditorFrameType::PcbEditor; let mut i = 1; @@ -1488,6 +1587,22 @@ fn parse_ratsnest_display_mode(value: &str) -> Result Result { + match value { + "warning" => Ok(DrcSeverity::Warning), + "error" => Ok(DrcSeverity::Error), + "exclusion" => Ok(DrcSeverity::Exclusion), + "ignore" => Ok(DrcSeverity::Ignore), + "info" => Ok(DrcSeverity::Info), + "action" => Ok(DrcSeverity::Action), + "debug" => Ok(DrcSeverity::Debug), + "undefined" => Ok(DrcSeverity::Undefined), + _ => Err(format!( + "unknown drc severity `{value}`; expected warning, error, exclusion, ignore, info, action, debug, or undefined" + )), + } +} + fn default_config() -> CliConfig { CliConfig { socket: None, @@ -1499,7 +1614,7 @@ fn default_config() -> CliConfig { fn print_help() { println!( - "kicad-ipc-cli\n\nUSAGE:\n cargo run --bin kicad-ipc-cli -- [--socket URI] [--token TOKEN] [--client-name NAME] [--timeout-ms N] [command options]\n\nCOMMANDS:\n ping Check IPC connectivity\n version Fetch KiCad version\n open-docs [--type ] List open docs (default type: pcb)\n project-path Get current project path from open PCB docs\n board-open Exit non-zero if no PCB doc is open\n net-classes List project netclass definitions\n text-variables List text variables for current board document\n expand-text-variables Expand variables in provided text values\n Options: --text (repeatable)\n text-extents Measure text bounding box\n Options: --text \n text-as-shapes Convert text to rendered shapes\n Options: --text (repeatable)\n nets List board nets (requires one open PCB)\n netlist-pads Emit pad-level netlist data (with footprint context)\n items-by-id --id ... Show parsed details for specific item IDs\n item-bbox --id ... Show bounding boxes for item IDs\n hit-test --id --x-nm --y-nm [--tolerance-nm ]\n Hit-test one item at a point\n types-pcb List PCB KiCad object type IDs from proto enum\n items-raw --type-id ... Dump raw Any payloads for requested item type IDs\n items-raw-all-pcb [--debug] Dump all PCB item payloads across all PCB object types\n pad-shape-polygon --pad-id ... --layer-id [--debug]\n Dump pad polygons on a target layer\n padstack-presence --item-id ... --layer-id ... [--debug]\n Check padstack shape presence matrix across layers\n title-block Show title block fields\n board-as-string Dump board as KiCad s-expression text\n selection-as-string Dump current selection as KiCad s-expression text\n stackup Show typed board stackup\n graphics-defaults Show typed graphics defaults\n appearance Show typed editor appearance settings\n set-appearance --inactive-layer-display \n --net-color-display \n --board-flip \n --ratsnest-display \n Set editor appearance settings\n netclass Show typed netclass map for current board nets\n proto-coverage-board-read Print board-read command coverage vs proto\n board-read-report [--out P] Write markdown board reconstruction report\n enabled-layers List enabled board layers\n set-enabled-layers --copper-layer-count [--layer-id ...]\n Set enabled board layer set\n active-layer Show active board layer\n set-active-layer --layer-id \n Set active board layer\n visible-layers Show currently visible board layers\n set-visible-layers --layer-id ...\n Set visible board layers\n board-origin [--type ] Show board origin (`grid` default, or `drill`)\n set-board-origin --type --x-nm --y-nm \n Set board origin (`grid` or `drill`)\n refresh-editor [--frame ] Refresh a specific editor frame (default: pcb)\n begin-commit Start staged commit and print commit ID\n end-commit --id [--action ] [--message ]\n End staged commit with commit/drop action\n selection-summary Show current selection item type counts\n selection-details Show parsed details for selected items\n selection-raw Show raw Any payload bytes for selected items\n smoke ping + version + board-open summary\n help Show help\n\nTYPES:\n schematic | symbol | pcb | footprint | drawing-sheet | project\n" + "kicad-ipc-cli\n\nUSAGE:\n cargo run --bin kicad-ipc-cli -- [--socket URI] [--token TOKEN] [--client-name NAME] [--timeout-ms N] [command options]\n\nCOMMANDS:\n ping Check IPC connectivity\n version Fetch KiCad version\n open-docs [--type ] List open docs (default type: pcb)\n project-path Get current project path from open PCB docs\n board-open Exit non-zero if no PCB doc is open\n net-classes List project netclass definitions\n text-variables List text variables for current board document\n expand-text-variables Expand variables in provided text values\n Options: --text (repeatable)\n text-extents Measure text bounding box\n Options: --text \n text-as-shapes Convert text to rendered shapes\n Options: --text (repeatable)\n nets List board nets (requires one open PCB)\n netlist-pads Emit pad-level netlist data (with footprint context)\n items-by-id --id ... Show parsed details for specific item IDs\n item-bbox --id ... Show bounding boxes for item IDs\n hit-test --id --x-nm --y-nm [--tolerance-nm ]\n Hit-test one item at a point\n types-pcb List PCB KiCad object type IDs from proto enum\n items-raw --type-id ... Dump raw Any payloads for requested item type IDs\n items-raw-all-pcb [--debug] Dump all PCB item payloads across all PCB object types\n pad-shape-polygon --pad-id ... --layer-id [--debug]\n Dump pad polygons on a target layer\n padstack-presence --item-id ... --layer-id ... [--debug]\n Check padstack shape presence matrix across layers\n title-block Show title block fields\n board-as-string Dump board as KiCad s-expression text\n selection-as-string Dump current selection as KiCad s-expression text\n stackup Show typed board stackup\n graphics-defaults Show typed graphics defaults\n appearance Show typed editor appearance settings\n set-appearance --inactive-layer-display \n --net-color-display \n --board-flip \n --ratsnest-display \n Set editor appearance settings\n inject-drc-error --severity --message [--x-nm --y-nm ] [--item-id ...]\n Inject a DRC marker (severity: warning|error|exclusion|ignore|info|action|debug|undefined)\n netclass Show typed netclass map for current board nets\n proto-coverage-board-read Print board-read command coverage vs proto\n board-read-report [--out P] Write markdown board reconstruction report\n enabled-layers List enabled board layers\n set-enabled-layers --copper-layer-count [--layer-id ...]\n Set enabled board layer set\n active-layer Show active board layer\n set-active-layer --layer-id \n Set active board layer\n visible-layers Show currently visible board layers\n set-visible-layers --layer-id ...\n Set visible board layers\n board-origin [--type ] Show board origin (`grid` default, or `drill`)\n set-board-origin --type --x-nm --y-nm \n Set board origin (`grid` or `drill`)\n refresh-editor [--frame ] Refresh a specific editor frame (default: pcb)\n begin-commit Start staged commit and print commit ID\n end-commit --id [--action ] [--message ]\n End staged commit with commit/drop action\n selection-summary Show current selection item type counts\n selection-details Show parsed details for selected items\n selection-raw Show raw Any payload bytes for selected items\n smoke ping + version + board-open summary\n help Show help\n\nTYPES:\n schematic | symbol | pcb | footprint | drawing-sheet | project\n" ); } @@ -2053,7 +2168,7 @@ fn hex_char(value: u8) -> char { mod tests { use super::{parse_args_from, Command}; use kicad_ipc::{ - BoardFlipMode, BoardOriginKind, CommitAction, InactiveLayerDisplayMode, + BoardFlipMode, BoardOriginKind, CommitAction, DrcSeverity, InactiveLayerDisplayMode, NetColorDisplayMode, RatsnestDisplayMode, }; @@ -2224,4 +2339,37 @@ mod tests { other => panic!("unexpected command variant: {other:?}"), } } + + #[test] + fn parse_args_parses_inject_drc_error() { + let (_, command) = parse_args_from(vec![ + "inject-drc-error".to_string(), + "--severity".to_string(), + "warning".to_string(), + "--message".to_string(), + "marker".to_string(), + "--x-nm".to_string(), + "100".to_string(), + "--y-nm".to_string(), + "200".to_string(), + ]) + .expect("inject-drc-error args should parse"); + + match command { + Command::InjectDrcError { + severity, + message, + x_nm, + y_nm, + item_ids, + } => { + assert_eq!(severity, DrcSeverity::Warning); + assert_eq!(message, "marker"); + assert_eq!(x_nm, Some(100)); + assert_eq!(y_nm, Some(200)); + assert!(item_ids.is_empty()); + } + other => panic!("unexpected command variant: {other:?}"), + } + } }