From 42f3f75837c3f2d7b7e6bae5c92ad2423d634730 Mon Sep 17 00:00:00 2001 From: Justin Schneck Date: Sun, 23 Nov 2025 18:50:27 -0500 Subject: [PATCH] allow passing multiple --input flags in priority order. Multiple input flags will be searched from first flag to last flag. --- src/commands/stone/create.rs | 135 +++++++++++++------ src/commands/stone/provision.rs | 111 ++++++++++------ src/commands/stone/validate.rs | 73 +++++------ tests/commands/stone/create/mod.rs | 187 +++++++++++++++++++++++++++ tests/commands/stone/validate/mod.rs | 157 ++++++++++++++++++++++ 5 files changed, 551 insertions(+), 112 deletions(-) diff --git a/src/commands/stone/create.rs b/src/commands/stone/create.rs index 279d305..674740d 100644 --- a/src/commands/stone/create.rs +++ b/src/commands/stone/create.rs @@ -19,14 +19,14 @@ pub struct CreateArgs { #[arg(long = "os-release", value_name = "PATH")] pub os_release: PathBuf, - /// Path to the input directory + /// Path to the input directory (can be specified multiple times for search priority) #[arg( short = 'i', long = "input-dir", value_name = "DIR", default_value = "." )] - pub input_dir: PathBuf, + pub input_dirs: Vec, /// Path to the output directory #[arg( @@ -47,17 +47,28 @@ impl CreateArgs { create_command( &self.manifest, &self.os_release, - &self.input_dir, + &self.input_dirs, &self.output_dir, self.verbose, ) } } +/// Helper function to find a file in multiple input directories, searching in order +fn find_file_in_dirs(filename: &str, input_dirs: &[PathBuf]) -> Option { + for dir in input_dirs { + let candidate = dir.join(filename); + if candidate.exists() { + return Some(candidate); + } + } + None +} + pub fn create_command( manifest_path: &Path, os_release_path: &Path, - input_dir: &Path, + input_dirs: &[PathBuf], output_dir: &PathBuf, verbose: bool, ) -> Result<(), String> { @@ -99,12 +110,20 @@ pub fn create_command( if let Some(build_args) = &device.build_args && let Some(template) = build_args.fwup_template() { - let input_path = input_dir.join(template); - let output_path = output_dir.join(template); - if let Err(e) = copy_file(&input_path, &output_path, verbose) { - errors.push(format!( - "Failed to copy fwup template '{template}' for device '{device_name}': {e}" - )); + match find_file_in_dirs(template, input_dirs) { + Some(input_path) => { + let output_path = output_dir.join(template); + if let Err(e) = copy_file(&input_path, &output_path, verbose) { + errors.push(format!( + "Failed to copy fwup template '{template}' for device '{device_name}': {e}" + )); + } + } + None => { + errors.push(format!( + "fwup template '{template}' for device '{device_name}' not found in any input directory" + )); + } } } @@ -114,7 +133,7 @@ pub fn create_command( device_name, image_name, image, - input_dir, + input_dirs, output_dir, verbose, ) { @@ -127,25 +146,42 @@ pub fn create_command( // Copy the provision file if specified in runtime if let Some(provision_file) = &manifest.runtime.provision { - let provision_input_path = input_dir.join(provision_file); - let provision_output_path = output_dir.join(provision_file); - if let Err(e) = copy_file(&provision_input_path, &provision_output_path, verbose) { - errors.push(format!( - "Failed to copy provision file '{provision_file}': {e}" - )); + match find_file_in_dirs(provision_file, input_dirs) { + Some(provision_input_path) => { + let provision_output_path = output_dir.join(provision_file); + if let Err(e) = copy_file(&provision_input_path, &provision_output_path, verbose) { + errors.push(format!( + "Failed to copy provision file '{provision_file}': {e}" + )); + } + } + None => { + errors.push(format!( + "Provision file '{provision_file}' not found in any input directory" + )); + } } } // Copy provision profile scripts if let Some(provision) = &manifest.provision { for (profile_name, profile) in &provision.profiles { - let script_input_path = input_dir.join(&profile.script); - let script_output_path = output_dir.join(&profile.script); - if let Err(e) = copy_file(&script_input_path, &script_output_path, verbose) { - errors.push(format!( - "Failed to copy provision profile script '{}' for profile '{profile_name}': {e}", - profile.script - )); + match find_file_in_dirs(&profile.script, input_dirs) { + Some(script_input_path) => { + let script_output_path = output_dir.join(&profile.script); + if let Err(e) = copy_file(&script_input_path, &script_output_path, verbose) { + errors.push(format!( + "Failed to copy provision profile script '{}' for profile '{profile_name}': {e}", + profile.script + )); + } + } + None => { + errors.push(format!( + "Failed to copy provision profile script '{}' for profile '{profile_name}': not found in any input directory", + profile.script + )); + } } } } @@ -185,7 +221,7 @@ fn process_image( _device_name: &str, image_name: &str, image: &crate::manifest::Image, - input_dir: &Path, + input_dirs: &[PathBuf], output_dir: &Path, verbose: bool, ) -> Result<(), String> { @@ -195,12 +231,20 @@ fn process_image( if let Some(build_args) = image.build_args() && let Some(template) = build_args.fwup_template() { - let input_path = input_dir.join(template); - let output_path = output_dir.join(template); - if let Err(e) = copy_file(&input_path, &output_path, verbose) { - return Err(format!( - "Failed to copy fwup template '{template}' for image '{image_name}': {e}" - )); + match find_file_in_dirs(template, input_dirs) { + Some(input_path) => { + let output_path = output_dir.join(template); + if let Err(e) = copy_file(&input_path, &output_path, verbose) { + return Err(format!( + "Failed to copy fwup template '{template}' for image '{image_name}': {e}" + )); + } + } + None => { + return Err(format!( + "fwup template '{template}' for image '{image_name}' not found in any input directory" + )); + } } } @@ -208,7 +252,7 @@ fn process_image( let files = image.files(); if !files.is_empty() { for file_entry in files { - if let Err(e) = process_file_entry(file_entry, input_dir, output_dir, verbose) { + if let Err(e) = process_file_entry(file_entry, input_dirs, output_dir, verbose) { return Err(format!( "Failed to process file in image '{image_name}': {e}" )); @@ -221,9 +265,15 @@ fn process_image( match image { crate::manifest::Image::String(filename) => { // This is an input file that should be copied - let input_path = input_dir.join(filename); - let output_path = output_dir.join(filename); - copy_file(&input_path, &output_path, verbose) + match find_file_in_dirs(filename, input_dirs) { + Some(input_path) => { + let output_path = output_dir.join(filename); + copy_file(&input_path, &output_path, verbose) + } + None => Err(format!( + "Image file '{filename}' for image '{image_name}' not found in any input directory" + )), + } } crate::manifest::Image::Object { out, .. } => { // This is an output file that will be generated during provision @@ -240,16 +290,21 @@ fn process_image( fn process_file_entry( file_entry: &crate::manifest::FileEntry, - input_dir: &Path, + input_dirs: &[PathBuf], output_dir: &Path, verbose: bool, ) -> Result<(), String> { let input_filename = file_entry.input_filename(); - let input_path = input_dir.join(input_filename); - let output_path = output_dir.join(input_filename); - - copy_file(&input_path, &output_path, verbose) + match find_file_in_dirs(input_filename, input_dirs) { + Some(input_path) => { + let output_path = output_dir.join(input_filename); + copy_file(&input_path, &output_path, verbose) + } + None => Err(format!( + "File '{input_filename}' not found in any input directory" + )), + } } fn copy_file(input_path: &Path, output_path: &Path, verbose: bool) -> Result<(), String> { diff --git a/src/commands/stone/provision.rs b/src/commands/stone/provision.rs index 1e5b222..6ea7dd8 100644 --- a/src/commands/stone/provision.rs +++ b/src/commands/stone/provision.rs @@ -11,14 +11,14 @@ use std::process::Command; #[derive(Args, Debug)] pub struct ProvisionArgs { - /// Path to the input directory containing manifest.json + /// Path to the input directory containing manifest.json (can be specified multiple times for search priority) #[arg( short = 'i', long = "input-dir", value_name = "DIR", default_value = "." )] - pub input_dir: PathBuf, + pub input_dirs: Vec, /// Enable verbose output #[arg(short = 'v', long = "verbose")] @@ -27,12 +27,28 @@ pub struct ProvisionArgs { impl ProvisionArgs { pub fn execute(&self) -> Result<(), String> { - provision_command(&self.input_dir, self.verbose) + provision_command(&self.input_dirs, self.verbose) } } -pub fn provision_command(input_dir: &Path, verbose: bool) -> Result<(), String> { - // Find manifest.json in the input directory +/// Helper function to find a file in multiple input directories, searching in order +fn find_file_in_dirs(filename: &str, input_dirs: &[PathBuf]) -> Option { + for dir in input_dirs { + let candidate = dir.join(filename); + if candidate.exists() { + return Some(candidate); + } + } + None +} + +pub fn provision_command(input_dirs: &[PathBuf], verbose: bool) -> Result<(), String> { + // Use the first input directory as the primary directory + let input_dir = input_dirs + .first() + .ok_or_else(|| "At least one input directory must be specified".to_string())?; + + // Find manifest.json in the first input directory let manifest_path = input_dir.join("manifest.json"); if !manifest_path.exists() { return Err(format!( @@ -70,7 +86,7 @@ pub fn provision_command(input_dir: &Path, verbose: bool) -> Result<(), String> device_name, image_name, image, - input_dir, + input_dirs, &build_dir, verbose, )?; @@ -83,7 +99,7 @@ pub fn provision_command(input_dir: &Path, verbose: bool) -> Result<(), String> device, build_args, &manifest, - input_dir, + input_dirs, &build_dir, verbose, )?; @@ -91,7 +107,7 @@ pub fn provision_command(input_dir: &Path, verbose: bool) -> Result<(), String> } // Execute provision script using profile-based approach - execute_provision_with_profile(&manifest, input_dir, &build_dir, verbose)?; + execute_provision_with_profile(&manifest, input_dirs, &build_dir, verbose)?; log_success("Provision completed."); Ok(()) @@ -102,7 +118,7 @@ fn build_storage_device( device: &crate::manifest::StorageDevice, build_args: &BuildArgs, manifest: &Manifest, - input_dir: &Path, + input_dirs: &[PathBuf], build_dir: &Path, verbose: bool, ) -> Result<(), String> { @@ -117,7 +133,7 @@ fn build_storage_device( device, template, manifest, - input_dir, + input_dirs, build_dir, verbose, )?; @@ -134,7 +150,7 @@ fn build_image( device_name: &str, image_name: &str, image: &Image, - input_dir: &Path, + input_dirs: &[PathBuf], build_dir: &Path, verbose: bool, ) -> Result<(), String> { @@ -166,12 +182,12 @@ fn build_image( files, size, size_unit, - input_dir, + input_dirs, build_dir, verbose, }), BuildArgs::Fwup { template } => { - build_fwup_image(image_name, image, template, input_dir, build_dir, verbose) + build_fwup_image(image_name, image, template, input_dirs, build_dir, verbose) } }, Image::Object { @@ -195,7 +211,7 @@ struct FatImageParams<'a> { files: &'a [FileEntry], size: &'a i64, size_unit: &'a str, - input_dir: &'a Path, + input_dirs: &'a [PathBuf], build_dir: &'a Path, verbose: bool, } @@ -230,10 +246,16 @@ fn build_fat_image(params: FatImageParams) -> Result<(), String> { let output_path = params.build_dir.join(params.out); + // Use the first input directory as the base path for FAT image + let base_path = params + .input_dirs + .first() + .ok_or_else(|| "At least one input directory must be specified".to_string())?; + // Create FAT image options let options = fat::FatImageOptions::new() .with_manifest_path(&temp_manifest_path) - .with_base_path(params.input_dir) + .with_base_path(base_path) .with_output_path(&output_path) .with_size_mebibytes(size_mb) .with_fat_type(fat_type) @@ -255,7 +277,7 @@ fn build_fwup_image( image_name: &str, image: &Image, template: &str, - input_dir: &Path, + input_dirs: &[PathBuf], build_dir: &Path, verbose: bool, ) -> Result<(), String> { @@ -264,7 +286,8 @@ fn build_fwup_image( "Building fwup image '{image_name}' -> '{out}' using template '{template}'." )); - let template_path = input_dir.join(template); + let template_path = find_file_in_dirs(template, input_dirs) + .ok_or_else(|| format!("fwup template '{template}' not found in any input directory"))?; let output_path = build_dir.join(out); let mut cmd = Command::new("fwup"); @@ -366,15 +389,17 @@ fn build_fwup_with_env_vars( device: &crate::manifest::StorageDevice, template: &str, manifest: &Manifest, - input_dir: &Path, + input_dirs: &[PathBuf], build_dir: &Path, verbose: bool, ) -> Result<(), String> { - let template_path = input_dir.join(template); + let template_path = find_file_in_dirs(template, input_dirs) + .ok_or_else(|| format!("fwup template '{template}' not found in any input directory"))?; let output_path = build_dir.join(&device.out); // Calculate environment variables from manifest - let env_vars = calculate_avocado_env_vars(device_name, device, manifest, input_dir, build_dir)?; + let env_vars = + calculate_avocado_env_vars(device_name, device, manifest, input_dirs, build_dir)?; let mut cmd = Command::new("fwup"); cmd.arg("-c") @@ -439,13 +464,18 @@ fn calculate_avocado_env_vars( _device_name: &str, device: &crate::manifest::StorageDevice, manifest: &Manifest, - input_dir: &Path, + input_dirs: &[PathBuf], build_dir: &Path, ) -> Result, String> { let mut env_vars = HashMap::new(); // No longer setting AVOCADO_SDK_RUNTIME_DIR - image paths are now absolute + // Use the first input directory as the primary directory + let input_dir = input_dirs + .first() + .ok_or_else(|| "At least one input directory must be specified".to_string())?; + // Meta Data - read from os-release file and manifest let (os_version, os_codename, os_description, os_author) = read_os_release_info(input_dir)?; env_vars.insert("AVOCADO_OS_VERSION".to_string(), os_version); @@ -485,8 +515,13 @@ fn calculate_avocado_env_vars( // Determine the full path based on image type let image_path = match image { Image::String(filename) => { - // Input files are in the input directory - input_dir.join(filename).to_string_lossy().to_string() + // Input files - search in input directories + find_file_in_dirs(filename, input_dirs) + .ok_or_else(|| { + format!("Image file '{filename}' not found in any input directory") + })? + .to_string_lossy() + .to_string() } Image::Object { out, @@ -501,8 +536,11 @@ fn calculate_avocado_env_vars( build_args: None, .. } => { - // Object files without build_args are input files in the input directory - input_dir.join(out).to_string_lossy().to_string() + // Object files without build_args are input files - search in input directories + find_file_in_dirs(out, input_dirs) + .ok_or_else(|| format!("Image file '{out}' not found in any input directory"))? + .to_string_lossy() + .to_string() } }; @@ -653,7 +691,7 @@ fn read_os_release_info(input_dir: &Path) -> Result<(String, String, String, Str fn execute_provision_with_profile( manifest: &Manifest, - input_dir: &Path, + input_dirs: &[PathBuf], build_dir: &Path, verbose: bool, ) -> Result<(), String> { @@ -664,7 +702,7 @@ fn execute_provision_with_profile( log_info("Using legacy provision script from runtime.provision."); return execute_provision_script( provision_file, - input_dir, + input_dirs, build_dir, verbose, &HashMap::new(), @@ -718,7 +756,7 @@ fn execute_provision_with_profile( // Execute the provision script execute_provision_script( &profile.script, - input_dir, + input_dirs, build_dir, verbose, &expanded_envs, @@ -727,12 +765,19 @@ fn execute_provision_with_profile( fn execute_provision_script( provision_file: &str, - input_dir: &Path, + input_dirs: &[PathBuf], build_dir: &Path, verbose: bool, additional_envs: &HashMap, ) -> Result<(), String> { - let provision_path = input_dir.join(provision_file); + // Use the first input directory as the primary directory + let input_dir = input_dirs + .first() + .ok_or_else(|| "At least one input directory must be specified".to_string())?; + + let provision_path = find_file_in_dirs(provision_file, input_dirs).ok_or_else(|| { + format!("[ERROR] Provision file '{provision_file}' not found in any input directory.") + })?; if verbose { log_debug(&format!( @@ -741,12 +786,6 @@ fn execute_provision_script( )); } - if !provision_path.exists() { - return Err(format!( - "[ERROR] Provision file '{provision_file}' not found in input directory." - )); - } - log_info(&format!( "Executing provision script '{}'.", provision_path.display() diff --git a/src/commands/stone/validate.rs b/src/commands/stone/validate.rs index 95865bf..af529d5 100644 --- a/src/commands/stone/validate.rs +++ b/src/commands/stone/validate.rs @@ -15,23 +15,34 @@ pub struct ValidateArgs { )] pub manifest: PathBuf, - /// Path to the input directory + /// Path to the input directory (can be specified multiple times for search priority) #[arg( short = 'i', long = "input-dir", value_name = "DIR", default_value = "." )] - pub input_dir: PathBuf, + pub input_dirs: Vec, } impl ValidateArgs { pub fn execute(&self) -> Result<(), String> { - validate_command(&self.manifest, &self.input_dir) + validate_command(&self.manifest, &self.input_dirs) } } -pub fn validate_command(manifest_path: &Path, input_dir: &Path) -> Result<(), String> { +/// Helper function to find a file in multiple input directories, searching in order +fn find_file_in_dirs(filename: &str, input_dirs: &[PathBuf]) -> Option { + for dir in input_dirs { + let candidate = dir.join(filename); + if candidate.exists() { + return Some(candidate); + } + } + None +} + +pub fn validate_command(manifest_path: &Path, input_dirs: &[PathBuf]) -> Result<(), String> { // Check if manifest file exists if !manifest_path.exists() { return Err(format!( @@ -48,22 +59,20 @@ pub fn validate_command(manifest_path: &Path, input_dir: &Path) -> Result<(), St let mut missing_provision_files = Vec::new(); // Check if provision file exists if specified in runtime - if let Some(provision_file) = &manifest.runtime.provision { - let provision_path = input_dir.join(provision_file); - if !provision_path.exists() { - missing_files.push(( - "runtime".to_string(), - "provision".to_string(), - provision_file.clone(), - )); - } + if let Some(provision_file) = &manifest.runtime.provision + && find_file_in_dirs(provision_file, input_dirs).is_none() + { + missing_files.push(( + "runtime".to_string(), + "provision".to_string(), + provision_file.clone(), + )); } // Check provision profiles and their scripts if let Some(provision) = &manifest.provision { for (profile_name, profile) in &provision.profiles { - let script_path = input_dir.join(&profile.script); - if !script_path.exists() { + if find_file_in_dirs(&profile.script, input_dirs).is_none() { missing_provision_files .push((format!("Profile '{profile_name}'"), profile.script.clone())); } @@ -94,22 +103,18 @@ pub fn validate_command(manifest_path: &Path, input_dir: &Path) -> Result<(), St // Check fwup template file if device has fwup build args if let Some(build_args) = &device.build_args && let Some(template) = build_args.fwup_template() + && find_file_in_dirs(template, input_dirs).is_none() { - let template_path = input_dir.join(template); - if !template_path.exists() { - missing_device_files.push((device_name.clone(), template.to_string())); - } + missing_device_files.push((device_name.clone(), template.to_string())); } // Process each image in the device for (image_name, image) in &device.images { // Check if this is a string-type image (direct file reference) - if let crate::manifest::Image::String(filename) = image { - let file_path = input_dir.join(filename); - - if !file_path.exists() { - missing_files.push((device_name.clone(), image_name.clone(), filename.clone())); - } + if let crate::manifest::Image::String(filename) = image + && find_file_in_dirs(filename, input_dirs).is_none() + { + missing_files.push((device_name.clone(), image_name.clone(), filename.clone())); } // For fwup builds, check if template file exists @@ -117,15 +122,13 @@ pub fn validate_command(manifest_path: &Path, input_dir: &Path) -> Result<(), St && build_type == "fwup" && let Some(build_args) = image.build_args() && let Some(template) = build_args.fwup_template() + && find_file_in_dirs(template, input_dirs).is_none() { - let template_path = input_dir.join(template); - if !template_path.exists() { - missing_files.push(( - device_name.clone(), - image_name.clone(), - template.to_string(), - )); - } + missing_files.push(( + device_name.clone(), + image_name.clone(), + template.to_string(), + )); } // Validate build_args for different build types @@ -161,9 +164,7 @@ pub fn validate_command(manifest_path: &Path, input_dir: &Path) -> Result<(), St }; for file_entry in files { - let file_path = input_dir.join(file_entry.input_filename()); - - if !file_path.exists() { + if find_file_in_dirs(file_entry.input_filename(), input_dirs).is_none() { missing_files.push(( device_name.clone(), image_name.clone(), diff --git a/tests/commands/stone/create/mod.rs b/tests/commands/stone/create/mod.rs index 09fcd22..9eb59bd 100644 --- a/tests/commands/stone/create/mod.rs +++ b/tests/commands/stone/create/mod.rs @@ -407,3 +407,190 @@ fn test_create_with_missing_provision_profile_script() { "Failed to copy provision profile script 'missing_script.sh' for profile 'development'", )); } + +#[test] +fn test_create_with_multiple_input_dirs_priority() { + use std::fs; + + let temp_dir = TempDir::new().unwrap(); + let input_path1 = temp_dir.path().join("input1"); + let input_path2 = temp_dir.path().join("input2"); + let output_path = temp_dir.path().join("output"); + + fs::create_dir_all(&input_path1).unwrap(); + fs::create_dir_all(&input_path2).unwrap(); + fs::create_dir_all(&output_path).unwrap(); + + // Create a manifest that references test.img + let manifest_content = r#"{ + "runtime": { + "platform": "test-platform", + "architecture": "noarch" + }, + "storage_devices": { + "test_device": { + "out": "test.img", + "devpath": "/dev/test", + "images": { + "simple_image": "test.img" + }, + "partitions": [] + } + } + }"#; + + let manifest_path = input_path1.join("manifest.json"); + fs::write(&manifest_path, manifest_content).unwrap(); + fs::write(input_path1.join("os-release"), "NAME=Test\nVERSION_ID=1.0").unwrap(); + + // Create test.img with different content in both directories + // input1 has higher priority (specified first) + fs::write(input_path1.join("test.img"), "content from input1").unwrap(); + fs::write(input_path2.join("test.img"), "content from input2").unwrap(); + + Command::cargo_bin("stone") + .unwrap() + .args([ + "create", + "--manifest-path", + &manifest_path.to_string_lossy(), + "--os-release", + &input_path1.join("os-release").to_string_lossy(), + "--output-dir", + &output_path.to_string_lossy(), + "--input-dir", + &input_path1.to_string_lossy(), + "--input-dir", + &input_path2.to_string_lossy(), + ]) + .assert() + .success(); + + // Verify that the file from input1 (higher priority) was used + let output_content = fs::read_to_string(output_path.join("test.img")).unwrap(); + assert_eq!( + output_content, "content from input1", + "Should use file from first input directory (higher priority)" + ); +} + +#[test] +fn test_create_with_multiple_input_dirs_fallback() { + use std::fs; + + let temp_dir = TempDir::new().unwrap(); + let input_path1 = temp_dir.path().join("input1"); + let input_path2 = temp_dir.path().join("input2"); + let output_path = temp_dir.path().join("output"); + + fs::create_dir_all(&input_path1).unwrap(); + fs::create_dir_all(&input_path2).unwrap(); + fs::create_dir_all(&output_path).unwrap(); + + // Create a manifest that references test.img + let manifest_content = r#"{ + "runtime": { + "platform": "test-platform", + "architecture": "noarch" + }, + "storage_devices": { + "test_device": { + "out": "test.img", + "devpath": "/dev/test", + "images": { + "simple_image": "test.img" + }, + "partitions": [] + } + } + }"#; + + let manifest_path = input_path1.join("manifest.json"); + fs::write(&manifest_path, manifest_content).unwrap(); + fs::write(input_path1.join("os-release"), "NAME=Test\nVERSION_ID=1.0").unwrap(); + + // Only create test.img in input2 (second priority) + fs::write(input_path2.join("test.img"), "content from input2").unwrap(); + + Command::cargo_bin("stone") + .unwrap() + .args([ + "create", + "--manifest-path", + &manifest_path.to_string_lossy(), + "--os-release", + &input_path1.join("os-release").to_string_lossy(), + "--output-dir", + &output_path.to_string_lossy(), + "--input-dir", + &input_path1.to_string_lossy(), + "--input-dir", + &input_path2.to_string_lossy(), + ]) + .assert() + .success(); + + // Verify that the file from input2 was used as fallback + let output_content = fs::read_to_string(output_path.join("test.img")).unwrap(); + assert_eq!( + output_content, "content from input2", + "Should fall back to second input directory when file not in first" + ); +} + +#[test] +fn test_create_with_multiple_input_dirs_not_found() { + use std::fs; + + let temp_dir = TempDir::new().unwrap(); + let input_path1 = temp_dir.path().join("input1"); + let input_path2 = temp_dir.path().join("input2"); + let output_path = temp_dir.path().join("output"); + + fs::create_dir_all(&input_path1).unwrap(); + fs::create_dir_all(&input_path2).unwrap(); + fs::create_dir_all(&output_path).unwrap(); + + // Create a manifest that references test.img + let manifest_content = r#"{ + "runtime": { + "platform": "test-platform", + "architecture": "noarch" + }, + "storage_devices": { + "test_device": { + "out": "test.img", + "devpath": "/dev/test", + "images": { + "simple_image": "test.img" + }, + "partitions": [] + } + } + }"#; + + let manifest_path = input_path1.join("manifest.json"); + fs::write(&manifest_path, manifest_content).unwrap(); + fs::write(input_path1.join("os-release"), "NAME=Test\nVERSION_ID=1.0").unwrap(); + + // Don't create test.img in either directory + + Command::cargo_bin("stone") + .unwrap() + .args([ + "create", + "--manifest-path", + &manifest_path.to_string_lossy(), + "--os-release", + &input_path1.join("os-release").to_string_lossy(), + "--output-dir", + &output_path.to_string_lossy(), + "--input-dir", + &input_path1.to_string_lossy(), + "--input-dir", + &input_path2.to_string_lossy(), + ]) + .assert() + .failure() + .stdout(str::contains("not found in any input directory")); +} diff --git a/tests/commands/stone/validate/mod.rs b/tests/commands/stone/validate/mod.rs index d60ac80..508f22e 100644 --- a/tests/commands/stone/validate/mod.rs +++ b/tests/commands/stone/validate/mod.rs @@ -478,3 +478,160 @@ fn test_validate_missing_provision_default_script() { .failure() .stdout(contains("missing_default.sh")); } + +#[test] +fn test_validate_with_multiple_input_dirs_priority() { + use std::fs; + use tempfile::TempDir; + + let temp_dir = TempDir::new().unwrap(); + let input_path1 = temp_dir.path().join("input1"); + let input_path2 = temp_dir.path().join("input2"); + + fs::create_dir_all(&input_path1).unwrap(); + fs::create_dir_all(&input_path2).unwrap(); + + // Create a manifest that references test.img + let manifest_content = r#"{ + "runtime": { + "platform": "test-platform", + "architecture": "noarch" + }, + "storage_devices": { + "test_device": { + "out": "test.img", + "devpath": "/dev/test", + "images": { + "simple_image": "test.img" + }, + "partitions": [] + } + } + }"#; + + let manifest_path = input_path1.join("manifest.json"); + fs::write(&manifest_path, manifest_content).unwrap(); + + // Create test.img in both directories + fs::write(input_path1.join("test.img"), "content from input1").unwrap(); + fs::write(input_path2.join("test.img"), "content from input2").unwrap(); + + // Validation should succeed because file exists (in first directory) + Command::cargo_bin("stone") + .unwrap() + .args([ + "validate", + "--manifest-path", + &manifest_path.to_string_lossy(), + "--input-dir", + &input_path1.to_string_lossy(), + "--input-dir", + &input_path2.to_string_lossy(), + ]) + .assert() + .success(); +} + +#[test] +fn test_validate_with_multiple_input_dirs_fallback() { + use std::fs; + use tempfile::TempDir; + + let temp_dir = TempDir::new().unwrap(); + let input_path1 = temp_dir.path().join("input1"); + let input_path2 = temp_dir.path().join("input2"); + + fs::create_dir_all(&input_path1).unwrap(); + fs::create_dir_all(&input_path2).unwrap(); + + // Create a manifest that references test.img + let manifest_content = r#"{ + "runtime": { + "platform": "test-platform", + "architecture": "noarch" + }, + "storage_devices": { + "test_device": { + "out": "test.img", + "devpath": "/dev/test", + "images": { + "simple_image": "test.img" + }, + "partitions": [] + } + } + }"#; + + let manifest_path = input_path1.join("manifest.json"); + fs::write(&manifest_path, manifest_content).unwrap(); + + // Only create test.img in input2 (second priority) + fs::write(input_path2.join("test.img"), "content from input2").unwrap(); + + // Validation should succeed because file exists in second directory + Command::cargo_bin("stone") + .unwrap() + .args([ + "validate", + "--manifest-path", + &manifest_path.to_string_lossy(), + "--input-dir", + &input_path1.to_string_lossy(), + "--input-dir", + &input_path2.to_string_lossy(), + ]) + .assert() + .success(); +} + +#[test] +fn test_validate_with_multiple_input_dirs_not_found() { + use std::fs; + use tempfile::TempDir; + + let temp_dir = TempDir::new().unwrap(); + let input_path1 = temp_dir.path().join("input1"); + let input_path2 = temp_dir.path().join("input2"); + + fs::create_dir_all(&input_path1).unwrap(); + fs::create_dir_all(&input_path2).unwrap(); + + // Create a manifest that references test.img + let manifest_content = r#"{ + "runtime": { + "platform": "test-platform", + "architecture": "noarch" + }, + "storage_devices": { + "test_device": { + "out": "test.img", + "devpath": "/dev/test", + "images": { + "simple_image": "test.img" + }, + "partitions": [] + } + } + }"#; + + let manifest_path = input_path1.join("manifest.json"); + fs::write(&manifest_path, manifest_content).unwrap(); + + // Don't create test.img in either directory + + // Validation should fail because file doesn't exist in any directory + Command::cargo_bin("stone") + .unwrap() + .args([ + "validate", + "--manifest-path", + &manifest_path.to_string_lossy(), + "--input-dir", + &input_path1.to_string_lossy(), + "--input-dir", + &input_path2.to_string_lossy(), + ]) + .assert() + .failure() + .stdout(contains("test.img")); +}