diff --git a/.cargo/config.toml b/.cargo/config.toml index 89430923..89c6fc25 100644 --- a/.cargo/config.toml +++ b/.cargo/config.toml @@ -9,5 +9,8 @@ # crt-static reference: See https://rust-lang.github.io/rfcs/1721-crt-static.html # dumpbin verification: https://github.com/sensmetry/sysand/pull/91#issuecomment-4081523020 # +# Increase the stack size to 8MiB, since Windows default 1 MiB causes a stack +# overflow on almost any CLI operation. Likely related to +# https://github.com/clap-rs/clap/issues/5134 [target.'cfg(windows)'] -rustflags = ["-Ctarget-feature=+crt-static"] +rustflags = ["-Ctarget-feature=+crt-static", "-Clink-args=/STACK:8388608"] diff --git a/Cargo.lock b/Cargo.lock index 1bd00283..ee1e4bf3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3968,6 +3968,7 @@ version = "1.1.2+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "81f3d15e84cbcd896376e6730314d59fb5a87f31e4b038454184435cd57defee" dependencies = [ + "foldhash 0.2.0", "indexmap", "serde_core", "serde_spanned", diff --git a/bindings/java/src/lib.rs b/bindings/java/src/lib.rs index 836bb9af..8ba13052 100644 --- a/bindings/java/src/lib.rs +++ b/bindings/java/src/lib.rs @@ -473,18 +473,15 @@ fn handle_build_error(env: &mut JNIEnv<'_>, error: KParBuildError format!("Workspace read error: {}", error), ); } - KParBuildError::PathUsage(usage) => { - env.throw_exception( - ExceptionKind::SysandException, - format!( - "project includes a path usage `{usage}`,\n\ - which is unlikely to be available on other computers at the same path" - ), - ); + KParBuildError::PathUsage(_) => { + env.throw_exception(ExceptionKind::SysandException, error.to_string()); } KParBuildError::WorkspaceMetamodelConflict { .. } => { env.throw_exception(ExceptionKind::SysandException, error.to_string()); } + KParBuildError::MissingIndexSymbol(_, _) => { + env.throw_exception(ExceptionKind::InvalidValue, error.to_string()) + } } } @@ -530,7 +527,7 @@ pub extern "system" fn Java_com_sensmetry_sysand_Sysand_buildProject<'local>( &project, &output_path, compression, - true, + false, false, ); match command_result { diff --git a/bindings/py/src/lib.rs b/bindings/py/src/lib.rs index babb1b1f..f48c4e63 100644 --- a/bindings/py/src/lib.rs +++ b/bindings/py/src/lib.rs @@ -225,7 +225,7 @@ fn do_build_py( None => KparCompressionMethod::default(), }; - do_build_kpar(&project, &output_path, compression, true, false) + do_build_kpar(&project, &output_path, compression, false, false) .map(|_| ()) .map_err(|err| match err { KParBuildError::ProjectRead(_) => PyRuntimeError::new_err(err.to_string()), @@ -244,6 +244,7 @@ fn do_build_py( KParBuildError::WorkspaceMetamodelConflict { .. } => { PyValueError::new_err(err.to_string()) } + KParBuildError::MissingIndexSymbol(_, _) => PyValueError::new_err(err.to_string()), }) } diff --git a/core/Cargo.toml b/core/Cargo.toml index 4b51676d..9eb87e2d 100644 --- a/core/Cargo.toml +++ b/core/Cargo.toml @@ -54,7 +54,7 @@ serde_json = { version = "1.0.149", default-features = false, features = ["prese sysand-macros = { path = "../macros"} spdx = "0.13.4" thiserror = { version = "2.0.18", default-features = false } -toml = "1.0.6" +toml = { version = "1.0.6", features = ["fast_hash"] } typed-path = { version = "0.12.3", default-features = false } walkdir = "2.5.0" # unicode-normalization = { version = "0.1.24", default-features = false } diff --git a/core/scripts/run_tests.sh b/core/scripts/run_tests.sh index 9837efd9..46dfb183 100755 --- a/core/scripts/run_tests.sh +++ b/core/scripts/run_tests.sh @@ -12,5 +12,6 @@ PACKAGE_DIR=$(dirname "$SCRIPT_DIR") cd "$PACKAGE_DIR" cargo test --features filesystem,networking,alltests $@ -cargo test --features js $@ -cargo test --features python $@ +# Currently these features don't enable any tests +# cargo test --features js $@ +# cargo test --features python $@ diff --git a/core/src/commands/build.rs b/core/src/commands/build.rs index 611dd64d..2aa0d75e 100644 --- a/core/src/commands/build.rs +++ b/core/src/commands/build.rs @@ -4,9 +4,11 @@ use camino::{Utf8Path, Utf8PathBuf}; use thiserror::Error; +use std::collections::HashSet; + use crate::{ env::utils::{CloneError, ErrorBound}, - include::IncludeError, + include::{IncludeError, do_include, extract_symbols}, model::InterchangeProjectValidationError, project::{ ProjectRead, @@ -164,6 +166,8 @@ pub enum KParBuildError { project_metamodel: String, project_path: String, }, + #[error("file `{0}` is missing symbol `{1}` found in index")] + MissingIndexSymbol(Box, String), } impl From for KParBuildError { @@ -243,18 +247,20 @@ pub fn default_kpar_file_name( )) } +/// `update_index_checksum` controls whether to parse symbols from current +/// file to update index and also update file checksum pub fn do_build_kpar, Pr: ProjectRead>( project: &Pr, path: P, compression: KparCompressionMethod, - canonicalise: bool, + update_index_checksum: bool, allow_path_usage: bool, ) -> Result> { do_build_kpar_inner( project, path, compression, - canonicalise, + update_index_checksum, allow_path_usage, None, ) @@ -264,12 +270,10 @@ fn do_build_kpar_inner, Pr: ProjectRead>( project: &Pr, path: P, compression: KparCompressionMethod, - canonicalise: bool, + update_index_checksum: bool, allow_path_usage: bool, workspace_metamodel: Option<&str>, ) -> Result> { - use crate::project::local_src::LocalSrcProject; - let building = "Building"; let header = crate::style::get_style_config().header; log::info!("{header}{building:>12}{header:#} kpar `{}`", path.as_ref()); @@ -334,11 +338,31 @@ fn do_build_kpar_inner, Pr: ProjectRead>( } } - if canonicalise { - for path in meta.validate()?.source_paths(true) { - use crate::include::do_include; - - do_include(&mut local_project, &path, true, true, None)?; + let meta = meta.validate()?; + // Check whether index symbols are up to date + if update_index_checksum { + for p in meta.source_paths(true) { + do_include(&mut local_project, p, true, true, None)? + } + } else { + for p in meta.source_paths(true) { + let new_symbols = extract_symbols(&mut local_project, &p, None)?; + let new_symbols: HashSet = new_symbols.into_iter().collect(); + let old_symbols = meta.file_index_symbols(&p); + if let Some(only_in_old) = old_symbols.difference(&new_symbols).next() { + return Err(KParBuildError::MissingIndexSymbol( + p.as_str().into(), + only_in_old.clone(), + )); + } + for only_in_new in new_symbols.difference(&old_symbols) { + // TODO: figure out a way to only print suggestions when running the CLI + log::warn!( + "index is missing symbol `{only_in_new}` found in file `{p}`;\n\ + if this is not intentional, include the file again to update its\n\ + exported symbols, or pass `--update-meta` to do so for all files" + ); + } } } @@ -395,7 +419,7 @@ pub fn do_build_workspace_kpars>( workspace: &Workspace, path: P, compression: KparCompressionMethod, - canonicalise: bool, + update_index_checksum: bool, allow_path_usage: bool, ) -> Result, KParBuildError> { let ws_metamodel = workspace.metamodel().map(|iri| iri.as_str()); @@ -414,7 +438,7 @@ pub fn do_build_workspace_kpars>( &project, &output_path, compression, - canonicalise, + update_index_checksum, allow_path_usage, ws_metamodel, )?; diff --git a/core/src/commands/include.rs b/core/src/commands/include.rs index ca208181..61addf13 100644 --- a/core/src/commands/include.rs +++ b/core/src/commands/include.rs @@ -52,29 +52,29 @@ pub fn do_include>( log::info!("{header}{including:>12}{header:#} file `{}`", path.as_ref()); if index_symbols { - match force_format.or_else(|| Language::guess_from_path(&path)) { - Some(Language::SysML) => { - let new_symbols = crate::symbols::top_level_sysml( - project.read_source(&path).map_err(IncludeError::Project)?, - ) - .map_err(|e| IncludeError::Extract(path.as_ref().as_str().into(), e))?; - - project.replace_index_for_file(new_symbols.into_iter(), &path, true)?; - } - Some(Language::KerML) => { - let new_symbols = crate::symbols::top_level_kerml( - project.read_source(&path).map_err(IncludeError::Project)?, - ) - .map_err(|e| IncludeError::Extract(path.as_ref().as_str().into(), e))?; - - project.replace_index_for_file(new_symbols.into_iter(), &path, true)?; - } - _ => { - return Err(IncludeError::UnknownFormat(path.as_ref().as_str().into())); - } - } + let new_symbols = extract_symbols(project, &path, force_format)?; + project.replace_index_for_file(new_symbols.into_iter(), &path, true)?; } project.include_source(&path, compute_checksum, true)?; Ok(()) } + +/// Extract top level symbols from file at `path` belonging to `project` +pub fn extract_symbols>( + project: &mut Pr, + path: &P, + force_format: Option, +) -> Result, IncludeError> { + match force_format.or_else(|| Language::guess_from_path(path)) { + Some(Language::SysML) => crate::symbols::top_level_sysml( + project.read_source(path).map_err(IncludeError::Project)?, + ) + .map_err(|e| IncludeError::Extract(path.as_ref().as_str().into(), e)), + Some(Language::KerML) => crate::symbols::top_level_kerml( + project.read_source(path).map_err(IncludeError::Project)?, + ) + .map_err(|e| IncludeError::Extract(path.as_ref().as_str().into(), e)), + _ => Err(IncludeError::UnknownFormat(path.as_ref().as_str().into())), + } +} diff --git a/core/src/model.rs b/core/src/model.rs index 1b48db89..2a79fefc 100644 --- a/core/src/model.rs +++ b/core/src/model.rs @@ -505,6 +505,22 @@ pub enum InterchangeProjectValidationError { }, } +impl InterchangeProjectMetadata { + /// Get symbols recorded in `index` for file at `path` + pub fn file_index_symbols>(&self, path: P) -> HashSet { + self.index + .iter() + .filter_map(|(k, v)| { + if v == path.as_ref() { + Some(k.clone()) + } else { + None + } + }) + .collect() + } +} + impl Default for InterchangeProjectMetadataRaw { fn default() -> Self { InterchangeProjectMetadataRaw { diff --git a/docs/src/commands/build.md b/docs/src/commands/build.md index 0eb285a4..88833361 100644 --- a/docs/src/commands/build.md +++ b/docs/src/commands/build.md @@ -1,7 +1,7 @@ # `sysand build` -Build a KerML Project Archive (KPAR). If executed in a workspace outside of a -project, builds all projects in the workspace. +Build a KerML Project Archive (KPAR). If executed in a workspace +outside of a project, builds all projects in the workspace. ## Usage @@ -13,8 +13,8 @@ sysand build [OPTIONS] [PATH] Creates a KPAR file from the current project. -Current project is determined as in [sysand print-root](root.md) and -if none is found uses the current directory instead. +Current project is determined as in [`sysand print-root`](root.md) and +if none is found, defaults to current directory. If a `README.md` file exist at the project root, it is included in the `.kpar` archive. @@ -57,4 +57,8 @@ warning; the build still succeeds. computers, as `file://` URL always contains an absolute path. For multiple related projects, consider using a workspace instead +- `-u`, `--update-meta`: Update project metadata before building. This + includes updating project symbol index and adding/updating source + file checksums. + {{#include ./partials/global_opts.md}} diff --git a/sysand/Cargo.toml b/sysand/Cargo.toml index 45272ed6..d0e6a595 100644 --- a/sysand/Cargo.toml +++ b/sysand/Cargo.toml @@ -32,7 +32,7 @@ env_logger = "0.11.9" log = { version = "0.4.29", default-features = false } sysand-core = { path = "../core", features = ["std", "filesystem", "networking"] } thiserror = "2.0.18" -toml = { version = "1.0.6", default-features = false } +toml = { version = "1.0.6", features = ["fast_hash"] } semver = "1.0.27" serde_json = { version = "1.0.149", default-features = false } spdx = "0.13.4" diff --git a/sysand/src/cli.rs b/sysand/src/cli.rs index 9af02749..6e2c3b3b 100644 --- a/sysand/src/cli.rs +++ b/sysand/src/cli.rs @@ -183,6 +183,11 @@ pub enum Command { /// For multiple related projects, consider using a workspace instead #[arg(long, short, default_value_t = false, verbatim_doc_comment)] allow_path_usage: bool, + /// Update project metadata to be included in the build artifacts. This + /// includes updating project symbol index and adding/updating source file + /// checksums. Original project(s) will not be affected + #[arg(long, short, default_value_t = false, verbatim_doc_comment)] + update_meta: bool, }, /// Publish a KPAR to a sysand package index Publish { diff --git a/sysand/src/commands/build.rs b/sysand/src/commands/build.rs index 12c525d9..d786be53 100644 --- a/sysand/src/commands/build.rs +++ b/sysand/src/commands/build.rs @@ -13,9 +13,16 @@ pub fn command_build_for_project>( path: P, compression: KparCompressionMethod, current_project: LocalSrcProject, + update_index_checksum: bool, allow_path_usage: bool, ) -> Result<()> { - match do_build_kpar(¤t_project, &path, compression, true, allow_path_usage) { + match do_build_kpar( + ¤t_project, + &path, + compression, + update_index_checksum, + allow_path_usage, + ) { Ok(_) => Ok(()), Err(err) => match err { KParBuildError::PathUsage(_) => bail!( @@ -31,6 +38,7 @@ pub fn command_build_for_workspace>( path: P, compression: KparCompressionMethod, workspace: Workspace, + update_index_checksum: bool, allow_path_usage: bool, ) -> Result<()> { log::warn!( @@ -39,7 +47,13 @@ pub fn command_build_for_workspace>( releases. For the status of this feature, see\n\ https://github.com/sensmetry/sysand/issues/101." ); - do_build_workspace_kpars(&workspace, &path, compression, true, allow_path_usage)?; + do_build_workspace_kpars( + &workspace, + &path, + compression, + update_index_checksum, + allow_path_usage, + )?; Ok(()) } diff --git a/sysand/src/lib.rs b/sysand/src/lib.rs index 8ba0eb0f..32fa42dc 100644 --- a/sysand/src/lib.rs +++ b/sysand/src/lib.rs @@ -667,6 +667,7 @@ pub fn run_cli(args: cli::Args) -> Result<()> { Command::Build { path, compression, + update_meta, allow_path_usage, } => { if let Some(current_project) = ctx.current_project { @@ -690,6 +691,7 @@ pub fn run_cli(args: cli::Args) -> Result<()> { path, compression.into(), current_project, + update_meta, allow_path_usage, ) } else { @@ -708,6 +710,7 @@ pub fn run_cli(args: cli::Args) -> Result<()> { output_dir, compression.into(), current_workspace, + update_meta, allow_path_usage, ) } diff --git a/sysand/tests/cli_build.rs b/sysand/tests/cli_build.rs index 6b19e4fc..f3da9a21 100644 --- a/sysand/tests/cli_build.rs +++ b/sysand/tests/cli_build.rs @@ -2,16 +2,21 @@ // SPDX-FileCopyrightText: © 2025 Sysand contributors use assert_cmd::prelude::*; +use camino::Utf8PathBuf; use clap::ValueEnum; use predicates::prelude::*; -use std::io::{Read, Write}; +use serde_json::json; +use std::{ + fs, + io::{Read, Write}, +}; use sysand::cli::KparCompressionMethodCli; use sysand_core::{ - model::{InterchangeProjectChecksumRaw, KerMlChecksumAlg}, - project::{ - ProjectRead, - local_kpar::{KparInnerPath, LocalKParProject}, + model::{ + InterchangeProjectChecksumRaw, InterchangeProjectInfoRaw, InterchangeProjectMetadataRaw, + KerMlChecksumAlg, }, + project::{ProjectRead, local_kpar::LocalKParProjectRaw}, }; // pub due to https://github.com/rust-lang/rust/issues/46379 @@ -30,6 +35,10 @@ fn project_build() -> Result<(), Box> { let out = run_sysand_in(&cwd, ["include", "--no-index-symbols", "test.sysml"], None)?; out.assert().success(); + let orig_info_path = cwd.join(".project.json"); + let orig_meta_path = cwd.join(".meta.json"); + let orig_info_contents = fs::read_to_string(&orig_info_path)?; + let orig_meta_contents = fs::read_to_string(&orig_meta_path)?; let out = run_sysand_in(&cwd, ["build", "./test_build.kpar"], None)?; @@ -46,19 +55,33 @@ fn project_build() -> Result<(), Box> { .stdout(predicate::str::contains("Name: test_build")) .stdout(predicate::str::contains("Version: 1.2.3")); - let kpar_project = LocalKParProject::new( - cwd.join("test_build.kpar"), - KparInnerPath::Guess, - None, - None, - ); + let kpar_project = LocalKParProjectRaw::new_guess_root(cwd.join("test_build.kpar"))?; - let (Some(_), Some(meta)) = kpar_project.get_project()? else { + let (Some(info), Some(meta)) = kpar_project.get_project()? else { panic!("failed to get built project info/meta"); }; - // Ensure things get canonicalised during build + // Ensure the build artifact matches original project + let original_meta: InterchangeProjectMetadataRaw = serde_json::from_str(&orig_meta_contents)?; + let original_info: InterchangeProjectInfoRaw = serde_json::from_str(&orig_info_contents)?; + assert_eq!(original_info, info); + assert_eq!(original_meta, meta); + + // Ensure no changes were made to the original project + let new_info_contents = fs::read_to_string(&orig_info_path)?; + let new_meta_contents = fs::read_to_string(&orig_meta_path)?; + assert_eq!(orig_info_contents, new_info_contents); + assert_eq!(orig_meta_contents, new_meta_contents); + // Now canonicalize meta during build + let out = run_sysand_in(&cwd, ["build", "--update-meta", "./test_build2.kpar"], None)?; + out.assert().success(); + let kpar_project = LocalKParProjectRaw::new_guess_root(cwd.join("test_build2.kpar"))?; + + let (Some(_), Some(meta)) = kpar_project.get_project()? else { + panic!("failed to get built project info/meta"); + }; + // File hash should be updated assert_eq!(meta.checksum.as_ref().unwrap().len(), 1); assert_eq!( meta.checksum.as_ref().unwrap().get("test.sysml").unwrap(), @@ -71,6 +94,93 @@ fn project_build() -> Result<(), Box> { assert_eq!(meta.index.len(), 1); assert_eq!(meta.index.get("P").unwrap(), "test.sysml"); + // Ensure no changes were made to the original project + let new_info_contents = fs::read_to_string(&orig_info_path)?; + let new_meta_contents = fs::read_to_string(&orig_meta_path)?; + assert_eq!(orig_info_contents, new_info_contents); + assert_eq!(orig_meta_contents, new_meta_contents); + + Ok(()) +} + +#[test] +fn project_build_errors_when_index_symbol_is_missing_from_file() +-> Result<(), Box> { + let (_temp_dir, cwd, out) = run_sysand( + [ + "init", + "--version", + "1.2.3", + "--name", + "test_build_missing_index_symbol", + ], + None, + )?; + out.assert().success(); + + std::fs::write(cwd.join("test.sysml"), b"package Present;\n")?; + let out = run_sysand_in( + &cwd, + [ + "include", + "--compute-checksum", + "--no-index-symbols", + "test.sysml", + ], + None, + )?; + out.assert().success(); + + let meta_path = cwd.join(".meta.json"); + let mut meta: serde_json::Value = serde_json::from_str(&fs::read_to_string(&meta_path)?)?; + meta["index"] = json!({ + "Missing": "test.sysml" + }); + std::fs::write(&meta_path, serde_json::to_string_pretty(&meta)?)?; + + let out = run_sysand_in(&cwd, ["build", "./test_build.kpar"], None)?; + out.assert().failure().stderr(predicate::str::contains( + "file `test.sysml` is missing symbol `Missing` found in index", + )); + assert!(!cwd.join("test_build.kpar").exists()); + + Ok(()) +} + +#[test] +fn project_build_warns_when_file_symbol_is_missing_from_index() +-> Result<(), Box> { + let (_temp_dir, cwd, out) = run_sysand( + [ + "init", + "--version", + "1.2.3", + "--name", + "test_build_missing_index_entry", + ], + None, + )?; + out.assert().success(); + + std::fs::write(cwd.join("test.sysml"), b"package Present;\n")?; + let out = run_sysand_in( + &cwd, + [ + "include", + "--compute-checksum", + "--no-index-symbols", + "test.sysml", + ], + None, + )?; + out.assert().success(); + + let out = run_sysand_in(&cwd, ["build", "./test_build.kpar"], None)?; + out.assert().success().stderr(predicate::str::contains( + "index is missing symbol `Present` found in file `test.sysml`", + )); + assert!(cwd.join("test_build.kpar").exists()); + Ok(()) } @@ -122,12 +232,7 @@ fn project_build_path_usage() -> Result<(), Box> { .stdout(predicate::str::contains("Version: 1.2.3")) .stdout(predicate::str::contains(file_url_from_path(&cwd2))); - let kpar_project = LocalKParProject::new( - cwd1.join("test_build.kpar"), - KparInnerPath::Guess, - None, - None, - ); + let kpar_project = LocalKParProjectRaw::new_guess_root(cwd1.join("test_build.kpar"))?; let (Some(_), Some(_)) = kpar_project.get_project()? else { panic!("failed to get built project info/meta"); @@ -136,11 +241,36 @@ fn project_build_path_usage() -> Result<(), Box> { Ok(()) } +struct WProject { + name: String, + info_path: Utf8PathBuf, + meta_path: Utf8PathBuf, + orig_info_contents: String, + orig_meta_contents: String, +} +impl WProject { + fn new(cwd: Utf8PathBuf) -> Result> { + let name = cwd.file_name().unwrap().to_owned(); + let info_path = cwd.join(".project.json"); + let meta_path = cwd.join(".meta.json"); + let orig_info_contents = fs::read_to_string(&info_path)?; + let orig_meta_contents = fs::read_to_string(&meta_path)?; + Ok(Self { + name, + info_path, + meta_path, + orig_info_contents, + orig_meta_contents, + }) + } +} + #[test] fn workspace_build() -> Result<(), Box> { let (_temp_dir, cwd) = new_temp_cwd()?; let project_group_cwd = cwd.join("subgroup"); std::fs::create_dir(&project_group_cwd)?; + let project1_cwd = project_group_cwd.join("project1"); let project2_cwd = project_group_cwd.join("project2"); let project3_cwd = cwd.join("project3"); @@ -148,19 +278,25 @@ fn workspace_build() -> Result<(), Box> { // Create .workspace.json file std::fs::write( cwd.join(".workspace.json"), - br#"{"projects": [ - {"path": "subgroup/project1", "iris": ["urn:kpar:project1"]}, - {"path": "subgroup/project2", "iris": ["urn:kpar:project2"]}, - {"path": "project3", "iris": ["urn:kpar:project3"]} - ]}"#, + json!({"projects": [ + {"path": "subgroup/project1", "iris": ["urn:kpar:project1"]}, + {"path": "subgroup/project2", "iris": ["urn:kpar:project2"]}, + {"path": "project3", "iris": ["urn:kpar:project3"]} + ]}) + .to_string(), )?; for project_cwd in [&project1_cwd, &project2_cwd, &project3_cwd] { std::fs::create_dir(project_cwd)?; - let project_name = project_cwd.file_name().unwrap(); let out = run_sysand_in( project_cwd, - ["init", "--version", "1.2.3", "--name", project_name], + [ + "init", + "--version", + "1.2.3", + "--name", + project_cwd.file_name().unwrap(), + ], None, )?; out.assert().success(); @@ -174,13 +310,19 @@ fn workspace_build() -> Result<(), Box> { out.assert().success(); } + let projects = [ + WProject::new(project1_cwd)?, + WProject::new(project2_cwd)?, + WProject::new(project3_cwd)?, + ]; + let out = run_sysand_in(&cwd, ["build"], None)?; out.assert().success(); - for project_name in ["project1", "project2", "project3"] { + for project in &projects { let kpar_path = cwd .join("output") - .join(format!("{}-1.2.3.kpar", project_name)); + .join(format!("{}-1.2.3.kpar", project.name)); assert!( kpar_path.is_file(), "kpar file does not exist: {}", @@ -191,29 +333,77 @@ fn workspace_build() -> Result<(), Box> { out.assert() .success() - .stdout(predicate::str::contains(format!("Name: {}", project_name))) + .stdout(predicate::str::contains(format!("Name: {}", project.name))) .stdout(predicate::str::contains("Version: 1.2.3")); - let kpar_project = LocalKParProject::new(kpar_path, KparInnerPath::Guess, None, None); + let kpar_project = LocalKParProjectRaw::new_guess_root(kpar_path)?; - let (Some(_), Some(meta)) = kpar_project.get_project()? else { + let (Some(info), Some(meta)) = kpar_project.get_project()? else { panic!("failed to get built project info/meta"); }; - // Ensure things get canonicalised during build + // Ensure the build artifact matches original project + let original_meta: InterchangeProjectMetadataRaw = + serde_json::from_str(&project.orig_meta_contents)?; + let original_info: InterchangeProjectInfoRaw = + serde_json::from_str(&project.orig_info_contents)?; + assert_eq!(original_info, info); + assert_eq!(original_meta, meta); + + // Ensure no changes were made to the original project + let new_info_contents = fs::read_to_string(&project.info_path)?; + let new_meta_contents = fs::read_to_string(&project.meta_path)?; + assert_eq!(project.orig_info_contents, new_info_contents); + assert_eq!(project.orig_meta_contents, new_meta_contents); + } + + // Now canonicalize meta during build. Previous artifacts should be overwritten + let out = run_sysand_in(&cwd, ["build", "--update-meta"], None)?; + out.assert().success(); + + for project in &projects { + println!("W9: {}", project.name); + let kpar_path = cwd + .join("output") + .join(format!("{}-1.2.3.kpar", project.name)); + assert!( + kpar_path.is_file(), + "kpar file does not exist: {}", + kpar_path + ); + + let out = run_sysand_in(&cwd, ["info", "--path", kpar_path.as_str()], None)?; + out.assert() + .success() + .stdout(predicate::str::contains(format!("Name: {}", project.name))) + .stdout(predicate::str::contains("Version: 1.2.3")); + + let kpar_project = LocalKParProjectRaw::new_guess_root(kpar_path)?; + + let (Some(_), Some(meta)) = kpar_project.get_project()? else { + panic!("failed to get built project info/meta"); + }; + + // File hash should be updated assert_eq!(meta.checksum.as_ref().unwrap().len(), 1); assert_eq!( meta.checksum.as_ref().unwrap().get("test.sysml").unwrap(), &InterchangeProjectChecksumRaw { value: "b4ee9d8a3ffb51787bd30ab1a74c2333565fd2b8be1334e827c5937f44d54dd8" .to_string(), - algorithm: KerMlChecksumAlg::Sha256.into(), + algorithm: KerMlChecksumAlg::Sha256.into() } ); assert_eq!(meta.index.len(), 1); assert_eq!(meta.index.get("P").unwrap(), "test.sysml"); + + // Ensure no changes were made to the original project + let new_info_contents = fs::read_to_string(&project.info_path)?; + let new_meta_contents = fs::read_to_string(&project.meta_path)?; + assert_eq!(project.orig_info_contents, new_info_contents); + assert_eq!(project.orig_meta_contents, new_meta_contents); } Ok(()) @@ -263,7 +453,7 @@ fn workspace_build_with_metamodel() -> Result<(), Box> { kpar_path ); - let kpar_project = LocalKParProject::new(kpar_path, KparInnerPath::Guess, None, None); + let kpar_project = LocalKParProjectRaw::new_guess_root(kpar_path)?; let (Some(_), Some(meta)) = kpar_project.get_project()? else { panic!("failed to get built project info/meta"); }; @@ -321,7 +511,7 @@ fn workspace_build_with_unknown_metamodel() -> Result<(), Box Result<(), Box Result<(), Box) -> Result<(), Box) -> Result<(), Box