use std::collections::HashMap;
use std::fs;
use std::path::{Path, PathBuf};
use crate::artifact::{Artifact, ArtifactKind};
use crate::hashing::sha256_file;
#[derive(Debug, Clone)]
pub struct TestBinary {
pub crate_name: String,
pub target: String,
pub bytes: Vec<u8>,
pub include_checksum: bool,
pub include_signature: bool,
}
#[derive(Debug, Clone, Default)]
pub struct TestArtifactSet {
binaries: Vec<TestBinary>,
}
impl TestArtifactSet {
pub fn new() -> Self {
Self::default()
}
pub fn binary(mut self, b: TestBinary) -> Self {
self.binaries.push(b);
self
}
pub fn linux_amd64(self, crate_name: &str) -> Self {
self.binary(TestBinary {
crate_name: crate_name.into(),
target: "x86_64-unknown-linux-gnu".into(),
bytes: b"#!/bin/sh\necho hi\n".to_vec(),
include_checksum: true,
include_signature: false,
})
}
pub fn windows_amd64_zip(self, crate_name: &str) -> Self {
let mut zip_bytes: Vec<u8> = b"PK\x03\x04".to_vec();
zip_bytes.extend_from_slice(&[0u8; 30]);
self.binary(TestBinary {
crate_name: crate_name.into(),
target: "x86_64-pc-windows-msvc".into(),
bytes: zip_bytes,
include_checksum: true,
include_signature: false,
})
}
pub fn write_to(&self, dir: &Path) -> Vec<Artifact> {
let mut out = Vec::with_capacity(self.binaries.len() * 3);
for b in &self.binaries {
let (filename, kind, format) = filename_kind_format(b);
let path = dir.join(&filename);
if let Some(parent) = path.parent() {
fs::create_dir_all(parent)
.unwrap_or_else(|e| panic!("create_dir_all {} failed: {e}", parent.display()));
}
fs::write(&path, &b.bytes)
.unwrap_or_else(|e| panic!("write {} failed: {e}", path.display()));
let digest = sha256_file(&path)
.unwrap_or_else(|e| panic!("sha256_file({}) failed: {e}", path.display()));
let mut metadata: HashMap<String, String> = HashMap::new();
metadata.insert("sha256".to_string(), digest.clone());
if let Some(fmt) = format {
metadata.insert("format".to_string(), fmt.to_string());
}
metadata.insert("url".to_string(), synthetic_url(&filename));
out.push(Artifact {
kind,
path: path.clone(),
name: filename.clone(),
target: Some(b.target.clone()),
crate_name: b.crate_name.clone(),
metadata,
size: None,
});
if b.include_checksum {
let sum_path = sidecar_path(&path, ".sha256");
let line = format!("{digest} {filename}\n");
fs::write(&sum_path, line.as_bytes())
.unwrap_or_else(|e| panic!("write {} failed: {e}", sum_path.display()));
out.push(Artifact {
kind: ArtifactKind::Checksum,
path: sum_path.clone(),
name: sum_path
.file_name()
.and_then(|s| s.to_str())
.unwrap_or("")
.to_string(),
target: Some(b.target.clone()),
crate_name: b.crate_name.clone(),
metadata: HashMap::new(),
size: None,
});
}
if b.include_signature {
let sig_path = sidecar_path(&path, ".sig");
fs::write(
&sig_path,
b"-----BEGIN SIGNATURE-----\nfake\n-----END SIGNATURE-----\n",
)
.unwrap_or_else(|e| panic!("write {} failed: {e}", sig_path.display()));
out.push(Artifact {
kind: ArtifactKind::Signature,
path: sig_path.clone(),
name: sig_path
.file_name()
.and_then(|s| s.to_str())
.unwrap_or("")
.to_string(),
target: Some(b.target.clone()),
crate_name: b.crate_name.clone(),
metadata: HashMap::new(),
size: None,
});
}
}
out
}
}
fn filename_kind_format(b: &TestBinary) -> (String, ArtifactKind, Option<&'static str>) {
if b.bytes.starts_with(b"PK\x03\x04") {
(
format!("{}_{}.zip", b.crate_name, b.target),
ArtifactKind::Archive,
Some("zip"),
)
} else {
(
format!("{}_{}", b.crate_name, b.target),
ArtifactKind::Binary,
None,
)
}
}
fn sidecar_path(base: &Path, suffix: &str) -> PathBuf {
let mut name = base
.file_name()
.and_then(|s| s.to_str())
.unwrap_or("artifact")
.to_string();
name.push_str(suffix);
base.with_file_name(name)
}
fn synthetic_url(filename: &str) -> String {
format!("https://example.test/dl/{filename}")
}
#[cfg(test)]
mod self_tests {
use super::*;
use tempfile::TempDir;
#[test]
fn write_to_materialises_files_at_expected_paths() {
let tmp = TempDir::new().unwrap();
let artifacts = TestArtifactSet::new()
.linux_amd64("demo")
.write_to(tmp.path());
assert_eq!(artifacts.len(), 2, "binary + .sha256 expected");
let binary = &artifacts[0];
let checksum = &artifacts[1];
assert_eq!(binary.kind, ArtifactKind::Binary);
assert_eq!(checksum.kind, ArtifactKind::Checksum);
assert!(
binary.path.exists(),
"binary file should exist: {:?}",
binary.path
);
assert!(
checksum.path.exists(),
"sha256 file should exist: {:?}",
checksum.path
);
assert_eq!(binary.name, "demo_x86_64-unknown-linux-gnu");
assert_eq!(checksum.name, "demo_x86_64-unknown-linux-gnu.sha256");
}
#[test]
fn sha256_metadata_matches_on_disk_file() {
let tmp = TempDir::new().unwrap();
let artifacts = TestArtifactSet::new()
.linux_amd64("demo")
.write_to(tmp.path());
let binary = &artifacts[0];
let recomputed = sha256_file(&binary.path).expect("sha256_file");
assert_eq!(
binary.metadata.get("sha256").map(String::as_str),
Some(recomputed.as_str()),
"metadata sha256 must match on-disk file"
);
}
#[test]
fn linux_and_windows_zip_produce_expected_kinds_and_targets() {
let tmp = TempDir::new().unwrap();
let artifacts = TestArtifactSet::new()
.linux_amd64("demo")
.windows_amd64_zip("demo")
.write_to(tmp.path());
assert_eq!(artifacts.len(), 4);
let linux = artifacts
.iter()
.find(|a| {
a.target.as_deref() == Some("x86_64-unknown-linux-gnu")
&& a.kind == ArtifactKind::Binary
})
.expect("linux binary present");
let windows = artifacts
.iter()
.find(|a| {
a.target.as_deref() == Some("x86_64-pc-windows-msvc")
&& a.kind == ArtifactKind::Archive
})
.expect("windows archive present");
assert_eq!(linux.kind, ArtifactKind::Binary);
assert_eq!(
linux.metadata.get("format"),
None,
"plain binary has no format"
);
assert!(linux.metadata.contains_key("url"));
assert_eq!(windows.kind, ArtifactKind::Archive);
assert_eq!(
windows.metadata.get("format").map(String::as_str),
Some("zip"),
"windows zip should have format=zip"
);
assert!(windows.name.ends_with(".zip"));
}
}