From e2e7d6abed1fcdf4e0d0075f1b4d856aa18e643b Mon Sep 17 00:00:00 2001 From: Jordan Noone Date: Sat, 5 Sep 2026 20:36:35 -0700 Subject: [PATCH 1/4] Fix KCL volume unit parsing --- src/cmd_kcl.rs | 62 ++++++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 58 insertions(+), 4 deletions(-) diff --git a/src/cmd_kcl.rs b/src/cmd_kcl.rs index 5189c6c3..d9832488 100644 --- a/src/cmd_kcl.rs +++ b/src/cmd_kcl.rs @@ -20,6 +20,21 @@ mod camera_angles; /// Send a heartbeat every N seconds. pub const HEARTBEATS: u64 = 3; +fn modeling_volume_unit(unit: &kt::UnitVolume) -> kcmc::units::UnitVolume { + match unit { + kt::UnitVolume::Mm3 => kcmc::units::UnitVolume::CubicMillimeters, + kt::UnitVolume::Cm3 => kcmc::units::UnitVolume::CubicCentimeters, + kt::UnitVolume::Ft3 => kcmc::units::UnitVolume::CubicFeet, + kt::UnitVolume::In3 => kcmc::units::UnitVolume::CubicInches, + kt::UnitVolume::M3 => kcmc::units::UnitVolume::CubicMeters, + kt::UnitVolume::Yd3 => kcmc::units::UnitVolume::CubicYards, + kt::UnitVolume::Usfloz => kcmc::units::UnitVolume::FluidOunces, + kt::UnitVolume::Usgal => kcmc::units::UnitVolume::Gallons, + kt::UnitVolume::L => kcmc::units::UnitVolume::Liters, + kt::UnitVolume::Ml => kcmc::units::UnitVolume::Milliliters, + } +} + pub(crate) fn with_heartbeats(mut settings: kcl_lib::ExecutorSettings) -> kcl_lib::ExecutorSettings { if settings.heartbeats.is_none() { settings.heartbeats = Some(HEARTBEATS); @@ -992,7 +1007,7 @@ pub struct CmdKclAnalyze { /// What units do you want volumes shown in? #[clap(long = "volume-output-unit", value_enum, default_value = "m3")] - pub volume_output_unit: kcmc::units::UnitVolume, + pub volume_output_unit: kt::UnitVolume, /// What units do you want masses shown in? #[clap(long = "mass-output-unit", value_enum, default_value = "kg")] @@ -1037,7 +1052,11 @@ impl crate::cmd::Command for CmdKclAnalyze { &filepath.display().to_string(), &code, vec![ - kcmc::ModelingCmd::Volume(kcmc::Volume::builder().output_unit(self.volume_output_unit).build()), + kcmc::ModelingCmd::Volume( + kcmc::Volume::builder() + .output_unit(modeling_volume_unit(&self.volume_output_unit)) + .build(), + ), kcmc::ModelingCmd::Mass( kcmc::Mass::builder() .material_density(self.material_density.into()) @@ -1160,7 +1179,7 @@ pub struct CmdKclVolume { /// Output unit. #[clap(long = "output-unit", short = 'u', value_enum)] - pub output_unit: kcmc::units::UnitVolume, + pub output_unit: kt::UnitVolume, /// If true, print a link to this request's tracing data. #[clap(long, default_value = "false")] @@ -1219,7 +1238,7 @@ impl crate::cmd::Command for CmdKclVolume { kittycad_modeling_cmds::ModelingCmd::Volume( kittycad_modeling_cmds::Volume::builder() .entity_ids(vec![]) // get whole model - .output_unit(self.output_unit) + .output_unit(modeling_volume_unit(&self.output_unit)) .build(), ), executor_settings, @@ -1997,8 +2016,43 @@ fn combine_quadrants( #[cfg(test)] mod tests { + use clap::{ValueEnum, error::ErrorKind}; + use super::*; + const POSSIBLE_VOLUME_UNITS: &str = "[possible values: mm3, cm3, ft3, in3, m3, yd3, usfloz, usgal, l, ml]"; + + #[test] + fn file_and_kcl_volume_accept_mm3() { + let file = + crate::cmd_file::CmdFileVolume::try_parse_from(["volume", "part.step", "--output-unit", "mm3"]).unwrap(); + let kcl = CmdKclVolume::try_parse_from(["volume", "part.kcl", "--output-unit", "mm3"]).unwrap(); + + assert_eq!(file.output_unit, kt::UnitVolume::Mm3); + assert_eq!(kcl.output_unit, kt::UnitVolume::Mm3); + } + + #[test] + fn kcl_volume_invalid_units_list_possible_values() { + let volume_error = CmdKclVolume::try_parse_from(["volume", "part.kcl", "--output-unit", "bogus"]).unwrap_err(); + let analyze_error = + CmdKclAnalyze::try_parse_from(["analyze", "part.kcl", "--volume-output-unit", "bogus"]).unwrap_err(); + + for error in [volume_error, analyze_error] { + assert_eq!(error.kind(), ErrorKind::InvalidValue); + assert!(error.to_string().contains(POSSIBLE_VOLUME_UNITS)); + } + } + + #[test] + fn api_volume_units_map_to_the_same_modeling_spelling() { + for unit in kt::UnitVolume::value_variants() { + let cli_spelling = unit.to_possible_value().unwrap().get_name().to_owned(); + + assert_eq!(modeling_volume_unit(unit).to_string(), cli_spelling); + } + } + #[test] fn with_heartbeats_adds_cli_default() { let settings = with_heartbeats(kcl_lib::ExecutorSettings::default()); From afb4c099bc5416939bd05aedbc73b91afdac1158 Mon Sep 17 00:00:00 2001 From: Jordan Noone Date: Sun, 6 Sep 2026 20:35:06 -0700 Subject: [PATCH 2/4] Parse KCL units through modeling-command Clap support --- Cargo.lock | 42 +++++++++++++++++------------------ Cargo.toml | 8 ++++++- src/cmd_kcl.rs | 60 ++++++++++++++++++++++++++++---------------------- 3 files changed, 61 insertions(+), 49 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index af4fbec6..e57345b8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -500,7 +500,7 @@ version = "3.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6dee98b0db6a962de883bf5d20362dee4d7ca0d12fe39a7c6c73c844e1cd7c1f" dependencies = [ - "darling 0.23.0", + "darling 0.20.11", "ident_case", "prettyplease", "proc-macro2", @@ -801,7 +801,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "117725a109d387c937a1533ce01b450cbde6b88abceea8473c4d7a85853cda3c" dependencies = [ "lazy_static", - "windows-sys 0.59.0", + "windows-sys 0.48.0", ] [[package]] @@ -1257,7 +1257,7 @@ dependencies = [ "libc", "option-ext", "redox_users", - "windows-sys 0.61.1", + "windows-sys 0.60.2", ] [[package]] @@ -1444,7 +1444,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "976dd42dc7e85965fe702eb8164f21f450704bdde31faefd6471dba214cb594e" dependencies = [ "libc", - "windows-sys 0.59.0", + "windows-sys 0.52.0", ] [[package]] @@ -2706,7 +2706,7 @@ checksum = "e04d7f318608d35d4b61ddd75cbdaee86b023ebe2bd5a66ee0915f0bf93095a9" dependencies = [ "hermit-abi", "libc", - "windows-sys 0.59.0", + "windows-sys 0.52.0", ] [[package]] @@ -3013,13 +3013,13 @@ dependencies = [ [[package]] name = "kittycad-modeling-cmds" -version = "0.2.224" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85eaa800375ae686cca00b3d916d3712108411de18665bff360e0ec7947b21b6" +version = "0.2.228" +source = "git+https://github.com/KittyCAD/modeling-api?rev=52ecdb59580435964776ad458e2036b73a4ef7d7#52ecdb59580435964776ad458e2036b73a4ef7d7" dependencies = [ "anyhow", "bon", "chrono", + "clap", "data-encoding", "enum-iterator", "enum-iterator-derive", @@ -3046,24 +3046,22 @@ dependencies = [ [[package]] name = "kittycad-modeling-cmds-macros" version = "0.1.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eb4ba190f74f6d32f4607ecac4bdebd4a935d4943b8ca8369305e6e7a59b5976" +source = "git+https://github.com/KittyCAD/modeling-api?rev=52ecdb59580435964776ad458e2036b73a4ef7d7#52ecdb59580435964776ad458e2036b73a4ef7d7" dependencies = [ "kittycad-modeling-cmds-macros-impl", "proc-macro2", "quote", - "syn 2.0.119", + "syn 3.0.4", ] [[package]] name = "kittycad-modeling-cmds-macros-impl" version = "0.1.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "743b6533d1848de67e3455a28614a2b7896807ab0d3358ccd3ea1863de3c4e6b" +source = "git+https://github.com/KittyCAD/modeling-api?rev=52ecdb59580435964776ad458e2036b73a4ef7d7#52ecdb59580435964776ad458e2036b73a4ef7d7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.119", + "syn 3.0.4", ] [[package]] @@ -3529,7 +3527,7 @@ version = "0.50.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" dependencies = [ - "windows-sys 0.61.1", + "windows-sys 0.60.2", ] [[package]] @@ -4271,7 +4269,7 @@ dependencies = [ "once_cell", "socket2 0.5.9", "tracing", - "windows-sys 0.59.0", + "windows-sys 0.52.0", ] [[package]] @@ -4916,7 +4914,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys 0.4.15", - "windows-sys 0.59.0", + "windows-sys 0.52.0", ] [[package]] @@ -4929,7 +4927,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys 0.12.1", - "windows-sys 0.61.1", + "windows-sys 0.52.0", ] [[package]] @@ -5880,7 +5878,7 @@ dependencies = [ "getrandom 0.4.1", "once_cell", "rustix 1.1.4", - "windows-sys 0.61.1", + "windows-sys 0.52.0", ] [[package]] @@ -5889,7 +5887,7 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2111ef44dae28680ae9752bb89409e7310ca33a8c621ebe7b106cf5c928b3ac0" dependencies = [ - "windows-sys 0.61.1", + "windows-sys 0.60.2", ] [[package]] @@ -5908,7 +5906,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "230a1b821ccbd75b185820a1f1ff7b14d21da1e442e22c0863ea5f08771a8874" dependencies = [ "rustix 1.1.4", - "windows-sys 0.61.1", + "windows-sys 0.60.2", ] [[package]] @@ -6896,7 +6894,7 @@ version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" dependencies = [ - "windows-sys 0.59.0", + "windows-sys 0.48.0", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 711357ad..8eee7b92 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -49,7 +49,8 @@ kittycad = { version = "0.5", features = [ "requests", "retry", ] } -kittycad-modeling-cmds = { version = "0.2.224", features = [ +kittycad-modeling-cmds = { version = "0.2.228", features = [ + "clap", "exec-kcl", "websocket", "tabled", @@ -129,3 +130,8 @@ debug = 0 incremental = true # Set this to 1 or 2 to get more useful backtraces in debugger. debug = 0 + +# Use the same modeling-command types in the CLI and its KCL dependencies. +# modeling-api#1369 plus the companion fix preserving existing unit spellings. +[patch.crates-io] +kittycad-modeling-cmds = { git = "https://github.com/KittyCAD/modeling-api", rev = "52ecdb59580435964776ad458e2036b73a4ef7d7" } diff --git a/src/cmd_kcl.rs b/src/cmd_kcl.rs index d9832488..b37797f8 100644 --- a/src/cmd_kcl.rs +++ b/src/cmd_kcl.rs @@ -20,21 +20,6 @@ mod camera_angles; /// Send a heartbeat every N seconds. pub const HEARTBEATS: u64 = 3; -fn modeling_volume_unit(unit: &kt::UnitVolume) -> kcmc::units::UnitVolume { - match unit { - kt::UnitVolume::Mm3 => kcmc::units::UnitVolume::CubicMillimeters, - kt::UnitVolume::Cm3 => kcmc::units::UnitVolume::CubicCentimeters, - kt::UnitVolume::Ft3 => kcmc::units::UnitVolume::CubicFeet, - kt::UnitVolume::In3 => kcmc::units::UnitVolume::CubicInches, - kt::UnitVolume::M3 => kcmc::units::UnitVolume::CubicMeters, - kt::UnitVolume::Yd3 => kcmc::units::UnitVolume::CubicYards, - kt::UnitVolume::Usfloz => kcmc::units::UnitVolume::FluidOunces, - kt::UnitVolume::Usgal => kcmc::units::UnitVolume::Gallons, - kt::UnitVolume::L => kcmc::units::UnitVolume::Liters, - kt::UnitVolume::Ml => kcmc::units::UnitVolume::Milliliters, - } -} - pub(crate) fn with_heartbeats(mut settings: kcl_lib::ExecutorSettings) -> kcl_lib::ExecutorSettings { if settings.heartbeats.is_none() { settings.heartbeats = Some(HEARTBEATS); @@ -1007,7 +992,7 @@ pub struct CmdKclAnalyze { /// What units do you want volumes shown in? #[clap(long = "volume-output-unit", value_enum, default_value = "m3")] - pub volume_output_unit: kt::UnitVolume, + pub volume_output_unit: kcmc::units::UnitVolume, /// What units do you want masses shown in? #[clap(long = "mass-output-unit", value_enum, default_value = "kg")] @@ -1052,11 +1037,7 @@ impl crate::cmd::Command for CmdKclAnalyze { &filepath.display().to_string(), &code, vec![ - kcmc::ModelingCmd::Volume( - kcmc::Volume::builder() - .output_unit(modeling_volume_unit(&self.volume_output_unit)) - .build(), - ), + kcmc::ModelingCmd::Volume(kcmc::Volume::builder().output_unit(self.volume_output_unit).build()), kcmc::ModelingCmd::Mass( kcmc::Mass::builder() .material_density(self.material_density.into()) @@ -1179,7 +1160,7 @@ pub struct CmdKclVolume { /// Output unit. #[clap(long = "output-unit", short = 'u', value_enum)] - pub output_unit: kt::UnitVolume, + pub output_unit: kcmc::units::UnitVolume, /// If true, print a link to this request's tracing data. #[clap(long, default_value = "false")] @@ -1238,7 +1219,7 @@ impl crate::cmd::Command for CmdKclVolume { kittycad_modeling_cmds::ModelingCmd::Volume( kittycad_modeling_cmds::Volume::builder() .entity_ids(vec![]) // get whole model - .output_unit(modeling_volume_unit(&self.output_unit)) + .output_unit(self.output_unit) .build(), ), executor_settings, @@ -2029,7 +2010,7 @@ mod tests { let kcl = CmdKclVolume::try_parse_from(["volume", "part.kcl", "--output-unit", "mm3"]).unwrap(); assert_eq!(file.output_unit, kt::UnitVolume::Mm3); - assert_eq!(kcl.output_unit, kt::UnitVolume::Mm3); + assert_eq!(kcl.output_unit, kcmc::units::UnitVolume::CubicMillimeters); } #[test] @@ -2045,11 +2026,38 @@ mod tests { } #[test] - fn api_volume_units_map_to_the_same_modeling_spelling() { + fn file_and_kcl_volume_accept_the_same_unit_spellings() { for unit in kt::UnitVolume::value_variants() { let cli_spelling = unit.to_possible_value().unwrap().get_name().to_owned(); - assert_eq!(modeling_volume_unit(unit).to_string(), cli_spelling); + let volume = CmdKclVolume::try_parse_from(["volume", "part.kcl", "--output-unit", &cli_spelling]).unwrap(); + let analyze = + CmdKclAnalyze::try_parse_from(["analyze", "part.kcl", "--volume-output-unit", &cli_spelling]).unwrap(); + + assert_eq!(volume.output_unit.to_string(), cli_spelling); + assert_eq!(analyze.volume_output_unit, volume.output_unit); + } + } + + #[test] + fn kcl_analyze_accepts_default_units_and_density_aliases() { + let analyze = CmdKclAnalyze::try_parse_from(["analyze", "part.kcl"]).unwrap(); + assert_eq!(analyze.volume_output_unit, kcmc::units::UnitVolume::CubicMeters); + assert_eq!(analyze.mass_output_unit, kcmc::units::UnitMass::Kilograms); + assert_eq!( + analyze.density_output_unit, + kcmc::units::UnitDensity::KilogramsPerCubicMeter + ); + assert_eq!(analyze.surface_area_output_unit, kcmc::units::UnitArea::SquareMeters); + assert_eq!(analyze.center_of_mass_output_unit, kcmc::units::UnitLength::Meters); + + for spelling in ["lbft3", "lb:ft3", "lb-ft3", "kgm3", "kg:m3", "kg-m3"] { + let analyze = + CmdKclAnalyze::try_parse_from(["analyze", "part.kcl", "--material-density-unit", spelling]).unwrap(); + assert_eq!( + analyze.material_density_unit, + ::from_str(spelling).unwrap() + ); } } From 3468bf3f8a7b2599b2c2f5651a055f56a96b2dff Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 7 Sep 2026 03:37:26 +0000 Subject: [PATCH 3/4] [dependabot skip] Automatically fix Nix hashes --- flake.nix | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/flake.nix b/flake.nix index 0452a0e1..2b2a6d04 100644 --- a/flake.nix +++ b/flake.nix @@ -76,7 +76,7 @@ version = cargoToml.package.version; src = ./.; - cargoHash = "sha256-fMIRNF7xU+HIGPbzZUtsXUed/M66HKAQYPRHyadbCEU="; + cargoHash = "sha256-S9FTL+zb0NyeAwot50BBTAKA7Gb3fTSQpFKnPt60e94="; doCheck = false; nativeBuildInputs = [pkgs.pkg-config]; From 284572b188cfdb10fc37749da4bfb7ae3b5866a4 Mon Sep 17 00:00:00 2001 From: Jordan Noone Date: Sun, 6 Sep 2026 22:45:31 -0700 Subject: [PATCH 4/4] Cover all KCL measurement unit flags --- Cargo.toml | 3 +- src/cmd_kcl.rs | 121 ++++++++++++++++++++++++++++++++++++++++--------- 2 files changed, 102 insertions(+), 22 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 8eee7b92..4c7cf43e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -132,6 +132,7 @@ incremental = true debug = 0 # Use the same modeling-command types in the CLI and its KCL dependencies. -# modeling-api#1369 plus the companion fix preserving existing unit spellings. +# v0.2.229 omits the unit spellings and volume Clap support fixed by modeling-api#1370. +# Remove this patch once a crates.io release includes those fixes. [patch.crates-io] kittycad-modeling-cmds = { git = "https://github.com/KittyCAD/modeling-api", rev = "52ecdb59580435964776ad458e2036b73a4ef7d7" } diff --git a/src/cmd_kcl.rs b/src/cmd_kcl.rs index b37797f8..06b70c39 100644 --- a/src/cmd_kcl.rs +++ b/src/cmd_kcl.rs @@ -2001,8 +2001,6 @@ mod tests { use super::*; - const POSSIBLE_VOLUME_UNITS: &str = "[possible values: mm3, cm3, ft3, in3, m3, yd3, usfloz, usgal, l, ml]"; - #[test] fn file_and_kcl_volume_accept_mm3() { let file = @@ -2013,18 +2011,6 @@ mod tests { assert_eq!(kcl.output_unit, kcmc::units::UnitVolume::CubicMillimeters); } - #[test] - fn kcl_volume_invalid_units_list_possible_values() { - let volume_error = CmdKclVolume::try_parse_from(["volume", "part.kcl", "--output-unit", "bogus"]).unwrap_err(); - let analyze_error = - CmdKclAnalyze::try_parse_from(["analyze", "part.kcl", "--volume-output-unit", "bogus"]).unwrap_err(); - - for error in [volume_error, analyze_error] { - assert_eq!(error.kind(), ErrorKind::InvalidValue); - assert!(error.to_string().contains(POSSIBLE_VOLUME_UNITS)); - } - } - #[test] fn file_and_kcl_volume_accept_the_same_unit_spellings() { for unit in kt::UnitVolume::value_variants() { @@ -2039,8 +2025,71 @@ mod tests { } } + // Exercise the actual command parsers: enabling ValueEnum must preserve the + // public abbreviations and list them when a user supplies an invalid unit. + fn assert_kcl_unit_choices(command: &str, flag: &str, units: &[&str]) { + let mut args = vec!["kcl", command, "part.kcl"]; + let required = match command { + "mass" => vec![ + ("--material-density", "1"), + ("--material-density-unit", "kg:m3"), + ("--output-unit", "kg"), + ], + "density" => vec![ + ("--material-mass", "1"), + ("--material-mass-unit", "kg"), + ("--output-unit", "kg:m3"), + ], + _ => vec![], + }; + for (required_flag, value) in required { + if required_flag != flag { + args.extend([required_flag, value]); + } + } + args.push(flag); + + for unit in units { + let parsed = CmdKcl::try_parse_from(args.iter().copied().chain([*unit])); + assert!(parsed.is_ok(), "{command} {flag} {unit}: {parsed:?}"); + } + + let error = CmdKcl::try_parse_from(args.iter().copied().chain(["bogus"])).unwrap_err(); + assert_eq!(error.kind(), ErrorKind::InvalidValue, "{command} {flag}: {error}"); + let choices = format!("[possible values: {}]", units.join(", ")); + assert!(error.to_string().contains(&choices), "{command} {flag}: {error}"); + } + #[test] - fn kcl_analyze_accepts_default_units_and_density_aliases() { + fn all_kcl_measurement_flags_preserve_unit_choices() { + let length = ["cm", "ft", "in", "m", "mm", "yd"]; + let area = ["cm2", "dm2", "ft2", "in2", "km2", "m2", "mm2", "yd2"]; + let mass = ["g", "kg", "lb"]; + let density = ["lb:ft3", "kg:m3"]; + let volume = ["mm3", "cm3", "ft3", "in3", "m3", "yd3", "usfloz", "usgal", "l", "ml"]; + let cases: &[(&str, &str, &[&str])] = &[ + ("volume", "--output-unit", &volume), + ("mass", "--output-unit", &mass), + ("mass", "--material-density-unit", &density), + ("density", "--output-unit", &density), + ("density", "--material-mass-unit", &mass), + ("surface-area", "--output-unit", &area), + ("center-of-mass", "--output-unit", &length), + ("bounding-box", "--output-unit", &length), + ("analyze", "--volume-output-unit", &volume), + ("analyze", "--mass-output-unit", &mass), + ("analyze", "--density-output-unit", &density), + ("analyze", "--material-density-unit", &density), + ("analyze", "--surface-area-output-unit", &area), + ("analyze", "--center-of-mass-output-unit", &length), + ]; + for (command, flag, units) in cases { + assert_kcl_unit_choices(command, flag, units); + } + } + + #[test] + fn kcl_measurements_preserve_defaults_and_density_aliases() { let analyze = CmdKclAnalyze::try_parse_from(["analyze", "part.kcl"]).unwrap(); assert_eq!(analyze.volume_output_unit, kcmc::units::UnitVolume::CubicMeters); assert_eq!(analyze.mass_output_unit, kcmc::units::UnitMass::Kilograms); @@ -2052,12 +2101,42 @@ mod tests { assert_eq!(analyze.center_of_mass_output_unit, kcmc::units::UnitLength::Meters); for spelling in ["lbft3", "lb:ft3", "lb-ft3", "kgm3", "kg:m3", "kg-m3"] { - let analyze = - CmdKclAnalyze::try_parse_from(["analyze", "part.kcl", "--material-density-unit", spelling]).unwrap(); - assert_eq!( - analyze.material_density_unit, - ::from_str(spelling).unwrap() - ); + let analyze = CmdKclAnalyze::try_parse_from([ + "analyze", + "part.kcl", + "--material-density-unit", + spelling, + "--density-output-unit", + spelling, + ]) + .unwrap(); + let mass = CmdKclMass::try_parse_from([ + "mass", + "part.kcl", + "--material-density", + "1", + "--material-density-unit", + spelling, + "--output-unit", + "kg", + ]) + .unwrap(); + let density = CmdKclDensity::try_parse_from([ + "density", + "part.kcl", + "--material-mass", + "1", + "--material-mass-unit", + "kg", + "--output-unit", + spelling, + ]) + .unwrap(); + let expected = ::from_str(spelling).unwrap(); + assert_eq!(analyze.material_density_unit, expected); + assert_eq!(analyze.density_output_unit, expected); + assert_eq!(mass.material_density_unit, expected); + assert_eq!(density.output_unit, expected); } }