Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
269 changes: 185 additions & 84 deletions crates/firmware/src/artifact_resolution.rs
Original file line number Diff line number Diff line change
Expand Up @@ -95,99 +95,175 @@ pub fn resolve_files_firmware_artifact(

#[cfg(test)]
mod tests {
use carbide_test_support::Outcome::*;
use carbide_test_support::{Case, check_cases};
use model::firmware::FirmwareFileArtifact;

use super::*;

#[test]
fn resolve_files_firmware_artifact_uses_url_as_remote_source() {
let firmware_cache_directory = Path::new("/mnt/persistence/fw/download-cache");
let firmware = firmware_with_files(vec![FirmwareFileArtifact {
filename: None,
url: Some("https://firmware.example.invalid/path/fw.bin".to_string()),
sha256: "abc123".to_string(),
}]);

let artifact = resolve_files_firmware_artifact(firmware_cache_directory, &firmware, 0)
.unwrap()
.unwrap();

assert!(artifact.local_path.starts_with(firmware_cache_directory));
assert_eq!(artifact.local_path.file_name().unwrap(), "fw.bin");
assert_eq!(
artifact.source,
ResolvedFirmwareArtifactSource::Remote {
url: "https://firmware.example.invalid/path/fw.bin".to_string(),
sha256: "abc123".to_string(),
}
);
}

#[test]
fn resolve_files_firmware_artifact_uses_filename_as_local_source() {
let firmware = firmware_with_files(vec![FirmwareFileArtifact {
filename: Some("/opt/carbide/firmware/fw.bin".to_string()),
url: None,
sha256: "abc123".to_string(),
}]);

let artifact = resolve_files_firmware_artifact(Path::new("/cache"), &firmware, 0)
.unwrap()
.unwrap();

assert_eq!(
artifact.local_path,
PathBuf::from("/opt/carbide/firmware/fw.bin")
);
assert_eq!(artifact.source, ResolvedFirmwareArtifactSource::Local);
}
const CACHE_DIRECTORY: &str = "/mnt/persistence/fw/download-cache";

#[test]
fn resolve_files_firmware_artifact_gives_url_precedence_over_filename() {
let firmware_cache_directory = Path::new("/mnt/persistence/fw/download-cache");
let firmware = firmware_with_files(vec![FirmwareFileArtifact {
filename: Some("/opt/carbide/firmware/local.bin".to_string()),
url: Some("https://firmware.example.invalid/remote.bin".to_string()),
sha256: "abc123".to_string(),
}]);

let artifact = resolve_files_firmware_artifact(firmware_cache_directory, &firmware, 0)
.unwrap()
.unwrap();

assert!(artifact.local_path.starts_with(firmware_cache_directory));
assert_eq!(artifact.local_path.file_name().unwrap(), "remote.bin");
assert_eq!(
artifact.source,
ResolvedFirmwareArtifactSource::Remote {
url: "https://firmware.example.invalid/remote.bin".to_string(),
sha256: "abc123".to_string(),
}
);
struct ResolutionInput {
files: Vec<FirmwareFileArtifact>,
pos: u32,
}

#[test]
fn resolve_files_firmware_artifact_returns_none_without_files() {
let firmware = FirmwareEntry::standard("1.0");

let artifact = resolve_files_firmware_artifact(Path::new("/cache"), &firmware, 0).unwrap();

assert_eq!(artifact, None);
}
fn resolve_files_firmware_artifact_cases() {
let remote_url = "https://firmware.example.invalid/path/fw.bin";
let second_url = "https://firmware.example.invalid/second.bin";

#[test]
fn resolve_files_firmware_artifact_rejects_url_without_filename() {
let firmware = firmware_with_files(vec![FirmwareFileArtifact {
filename: None,
url: Some("https://firmware.example.invalid/".to_string()),
sha256: "abc123".to_string(),
}]);

let error = resolve_files_firmware_artifact(Path::new("/cache"), &firmware, 0)
.unwrap_err()
.to_string();

assert!(error.contains("URL does not include a filename"));
check_cases(
[
Case {
scenario: "no files",
input: ResolutionInput {
files: Vec::new(),
pos: 0,
},
expect: Yields(None),
},
Case {
scenario: "URL only",
input: ResolutionInput {
files: vec![file(None, Some(remote_url), "abc123")],
pos: 0,
},
expect: Yields(Some(remote_artifact(remote_url, "abc123"))),
},
Case {
scenario: "filename only",
input: ResolutionInput {
files: vec![file(
Some("/opt/carbide/firmware/fw.bin"),
None,
"abc123",
)],
pos: 0,
},
expect: Yields(Some(local_artifact("/opt/carbide/firmware/fw.bin"))),
},
Case {
scenario: "URL takes precedence over filename",
input: ResolutionInput {
files: vec![file(
Some("/opt/carbide/firmware/local.bin"),
Some(remote_url),
"abc123",
)],
pos: 0,
},
expect: Yields(Some(remote_artifact(remote_url, "abc123"))),
},
Case {
scenario: "requested index selects matching artifact",
input: ResolutionInput {
files: vec![
file(
Some("/opt/carbide/firmware/first.bin"),
Some("https://firmware.example.invalid/first.bin"),
"first-sha",
),
file(
Some("/opt/carbide/firmware/second.bin"),
Some(second_url),
"second-sha",
),
],
pos: 1,
},
expect: Yields(Some(remote_artifact(second_url, "second-sha"))),
},
Case {
scenario: "requested index is out of range",
input: ResolutionInput {
files: vec![file(
Some("/opt/carbide/firmware/fw.bin"),
None,
"abc123",
)],
pos: 1,
},
expect: FailsWith(
"firmware version 1.0 has no files[] artifact at index 1".to_string(),
),
},
Case {
scenario: "URL has no filename",
input: ResolutionInput {
files: vec![file(
None,
Some("https://firmware.example.invalid/"),
"abc123",
)],
pos: 0,
},
expect: FailsWith(
"firmware version 1.0 files[] artifact at index 0 URL does not include a filename"
.to_string(),
),
},
Case {
scenario: "filename and URL are missing",
input: ResolutionInput {
files: vec![file(None, None, "abc123")],
pos: 0,
},
expect: FailsWith(
"firmware version 1.0 files[] artifact at index 0 has no filename or URL"
.to_string(),
),
},
Case {
scenario: "surrounding URL whitespace is trimmed",
input: ResolutionInput {
files: vec![file(
None,
Some(" https://firmware.example.invalid/trimmed.bin \n"),
"abc123",
)],
pos: 0,
},
expect: Yields(Some(remote_artifact(
"https://firmware.example.invalid/trimmed.bin",
"abc123",
))),
},
Case {
scenario: "surrounding filename whitespace is trimmed",
input: ResolutionInput {
files: vec![file(
Some(" \t/opt/carbide/firmware/trimmed.bin "),
None,
"abc123",
)],
pos: 0,
},
expect: Yields(Some(local_artifact(
"/opt/carbide/firmware/trimmed.bin",
))),
},
Case {
scenario: "blank filename and URL are missing",
input: ResolutionInput {
files: vec![file(Some(" \t "), Some(" \n "), "abc123")],
pos: 0,
},
expect: FailsWith(
"firmware version 1.0 files[] artifact at index 0 has no filename or URL"
.to_string(),
),
},
],
|ResolutionInput { files, pos }| {
resolve_files_firmware_artifact(
Path::new(CACHE_DIRECTORY),
&firmware_with_files(files),
pos,
)
.map_err(|error| error.to_string())
},
);
}

fn firmware_with_files(files: Vec<FirmwareFileArtifact>) -> FirmwareEntry {
Expand All @@ -197,4 +273,29 @@ mod tests {
..FirmwareEntry::default()
}
}

fn file(filename: Option<&str>, url: Option<&str>, sha256: &str) -> FirmwareFileArtifact {
FirmwareFileArtifact {
filename: filename.map(str::to_string),
url: url.map(str::to_string),
sha256: sha256.to_string(),
}
}

fn remote_artifact(url: &str, sha256: &str) -> ResolvedFirmwareArtifact {
ResolvedFirmwareArtifact {
local_path: firmware_cache_filename(Path::new(CACHE_DIRECTORY), url).unwrap(),
source: ResolvedFirmwareArtifactSource::Remote {
url: url.to_string(),
sha256: sha256.to_string(),
},
}
}

fn local_artifact(path: &str) -> ResolvedFirmwareArtifact {
ResolvedFirmwareArtifact {
local_path: PathBuf::from(path),
source: ResolvedFirmwareArtifactSource::Local,
}
}
}
Loading
Loading