1use std::collections::HashMap;
2use std::path::PathBuf;
3
4use serde::Serialize;
5
6#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize)]
7#[serde(rename_all = "snake_case")]
8#[non_exhaustive]
9pub enum ArtifactKind {
10 Binary,
12 UploadableBinary,
15 UniversalBinary,
16 Library,
17 Header,
18 CArchive,
19 CShared,
20 Wasm,
21
22 Archive,
24 SourceArchive,
25 Makeself,
26
27 LinuxPackage,
29 Snap,
30 PublishableSnapcraft,
31 Flatpak,
32 SourceRpm,
33
34 DiskImage,
36 Installer,
37 MacOsPackage,
38
39 DockerImage,
41 DockerImageV2,
42 PublishableDockerImage,
43 DockerManifest,
44 DockerDigest,
45
46 BrewFormula,
48 BrewCask,
49 Nixpkg,
50 ScoopManifest,
51 PublishableChocolatey,
52 WingetInstaller,
53 WingetDefaultLocale,
54 WingetVersion,
55 PkgBuild,
56 SrcInfo,
57 SourcePkgBuild,
58 SourceSrcInfo,
59 KrewPluginManifest,
60
61 Checksum,
63 Signature,
64 Certificate,
65 Sbom,
66 Metadata,
67 UploadableFile,
68}
69
70impl std::fmt::Display for ArtifactKind {
71 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
72 f.write_str(self.as_str())
73 }
74}
75
76impl ArtifactKind {
77 pub fn as_str(&self) -> &'static str {
79 match self {
80 ArtifactKind::Binary => "binary",
81 ArtifactKind::UploadableBinary => "uploadable_binary",
82 ArtifactKind::UniversalBinary => "universal_binary",
83 ArtifactKind::Library => "library",
84 ArtifactKind::Header => "header",
85 ArtifactKind::CArchive => "c_archive",
86 ArtifactKind::CShared => "c_shared",
87 ArtifactKind::Wasm => "wasm",
88 ArtifactKind::Archive => "archive",
89 ArtifactKind::SourceArchive => "source_archive",
90 ArtifactKind::Makeself => "makeself",
91 ArtifactKind::LinuxPackage => "linux_package",
92 ArtifactKind::Snap => "snap",
93 ArtifactKind::PublishableSnapcraft => "publishable_snapcraft",
94 ArtifactKind::Flatpak => "flatpak",
95 ArtifactKind::SourceRpm => "source_rpm",
96 ArtifactKind::DiskImage => "disk_image",
97 ArtifactKind::Installer => "installer",
98 ArtifactKind::MacOsPackage => "macos_package",
99 ArtifactKind::DockerImage => "docker_image",
100 ArtifactKind::DockerImageV2 => "docker_image_v2",
101 ArtifactKind::PublishableDockerImage => "publishable_docker_image",
102 ArtifactKind::DockerManifest => "docker_manifest",
103 ArtifactKind::DockerDigest => "docker_digest",
104 ArtifactKind::BrewFormula => "brew_formula",
105 ArtifactKind::BrewCask => "brew_cask",
106 ArtifactKind::Nixpkg => "nixpkg",
107 ArtifactKind::ScoopManifest => "scoop_manifest",
108 ArtifactKind::PublishableChocolatey => "publishable_chocolatey",
109 ArtifactKind::WingetInstaller => "winget_installer",
110 ArtifactKind::WingetDefaultLocale => "winget_default_locale",
111 ArtifactKind::WingetVersion => "winget_version",
112 ArtifactKind::PkgBuild => "pkg_build",
113 ArtifactKind::SrcInfo => "src_info",
114 ArtifactKind::SourcePkgBuild => "source_pkg_build",
115 ArtifactKind::SourceSrcInfo => "source_src_info",
116 ArtifactKind::KrewPluginManifest => "krew_plugin_manifest",
117 ArtifactKind::Checksum => "checksum",
118 ArtifactKind::Signature => "signature",
119 ArtifactKind::Certificate => "certificate",
120 ArtifactKind::Sbom => "sbom",
121 ArtifactKind::Metadata => "metadata",
122 ArtifactKind::UploadableFile => "uploadable_file",
123 }
124 }
125
126 pub fn parse(s: &str) -> Option<Self> {
128 match s {
129 "binary" => Some(ArtifactKind::Binary),
130 "uploadable_binary" => Some(ArtifactKind::UploadableBinary),
131 "universal_binary" => Some(ArtifactKind::UniversalBinary),
132 "library" => Some(ArtifactKind::Library),
133 "header" => Some(ArtifactKind::Header),
134 "c_archive" => Some(ArtifactKind::CArchive),
135 "c_shared" => Some(ArtifactKind::CShared),
136 "wasm" => Some(ArtifactKind::Wasm),
137 "archive" => Some(ArtifactKind::Archive),
138 "source_archive" => Some(ArtifactKind::SourceArchive),
139 "makeself" => Some(ArtifactKind::Makeself),
140 "linux_package" => Some(ArtifactKind::LinuxPackage),
141 "snap" => Some(ArtifactKind::Snap),
142 "publishable_snapcraft" => Some(ArtifactKind::PublishableSnapcraft),
143 "flatpak" => Some(ArtifactKind::Flatpak),
144 "source_rpm" => Some(ArtifactKind::SourceRpm),
145 "disk_image" => Some(ArtifactKind::DiskImage),
146 "installer" => Some(ArtifactKind::Installer),
147 "macos_package" => Some(ArtifactKind::MacOsPackage),
148 "docker_image" => Some(ArtifactKind::DockerImage),
149 "docker_image_v2" => Some(ArtifactKind::DockerImageV2),
150 "publishable_docker_image" => Some(ArtifactKind::PublishableDockerImage),
151 "docker_manifest" => Some(ArtifactKind::DockerManifest),
152 "docker_digest" => Some(ArtifactKind::DockerDigest),
153 "brew_formula" => Some(ArtifactKind::BrewFormula),
154 "brew_cask" => Some(ArtifactKind::BrewCask),
155 "nixpkg" => Some(ArtifactKind::Nixpkg),
156 "scoop_manifest" => Some(ArtifactKind::ScoopManifest),
157 "publishable_chocolatey" => Some(ArtifactKind::PublishableChocolatey),
158 "winget_installer" => Some(ArtifactKind::WingetInstaller),
159 "winget_default_locale" => Some(ArtifactKind::WingetDefaultLocale),
160 "winget_version" => Some(ArtifactKind::WingetVersion),
161 "pkg_build" => Some(ArtifactKind::PkgBuild),
162 "src_info" => Some(ArtifactKind::SrcInfo),
163 "source_pkg_build" => Some(ArtifactKind::SourcePkgBuild),
164 "source_src_info" => Some(ArtifactKind::SourceSrcInfo),
165 "krew_plugin_manifest" => Some(ArtifactKind::KrewPluginManifest),
166 "checksum" => Some(ArtifactKind::Checksum),
167 "signature" => Some(ArtifactKind::Signature),
168 "certificate" => Some(ArtifactKind::Certificate),
169 "sbom" => Some(ArtifactKind::Sbom),
170 "metadata" => Some(ArtifactKind::Metadata),
171 "uploadable_file" => Some(ArtifactKind::UploadableFile),
172 _ => None,
173 }
174 }
175}
176
177#[derive(Debug, Clone, Serialize)]
178pub struct Artifact {
179 pub kind: ArtifactKind,
180 pub path: PathBuf,
181 pub name: String,
183 pub target: Option<String>,
184 pub crate_name: String,
185 #[serde(serialize_with = "serialize_metadata_sorted")]
186 pub metadata: HashMap<String, String>,
187 #[serde(default, skip_serializing_if = "Option::is_none")]
189 pub size: Option<u64>,
190}
191
192const METADATA_HASH_KEYS: &[&str] = &[
203 "Checksum", "sha256", "sha512", "sha384", "sha224", "sha1", "md5", "blake2b", "blake3", "crc32",
204];
205
206fn serialize_metadata_sorted<S>(map: &HashMap<String, String>, ser: S) -> Result<S::Ok, S::Error>
210where
211 S: serde::Serializer,
212{
213 use serde::ser::SerializeMap as _;
214 let sorted: std::collections::BTreeMap<&String, &String> = map
215 .iter()
216 .filter(|(k, _)| !METADATA_HASH_KEYS.contains(&k.as_str()))
217 .collect();
218 let mut m = ser.serialize_map(Some(sorted.len()))?;
219 for (k, v) in sorted {
220 m.serialize_entry(k, v)?;
221 }
222 m.end()
223}
224
225impl Artifact {
226 pub fn name(&self) -> &str {
228 &self.name
229 }
230
231 pub fn goos(&self) -> Option<String> {
233 self.target.as_ref().map(|t| crate::target::map_target(t).0)
234 }
235
236 pub fn goarch(&self) -> Option<String> {
238 self.target.as_ref().map(|t| crate::target::map_target(t).1)
239 }
240
241 pub fn only_replacing_unibins(&self) -> bool {
246 self.metadata.get("replaces").is_none_or(|v| v != "false")
247 }
248
249 pub fn extra_binaries(&self) -> Vec<String> {
251 self.metadata
252 .get("extra_binaries")
253 .map(|v| {
254 v.split(',')
255 .filter(|s| !s.is_empty())
256 .map(|s| s.to_string())
257 .collect()
258 })
259 .unwrap_or_default()
260 }
261
262 pub fn extra_binary(&self) -> Option<String> {
264 self.metadata.get("binary").cloned()
265 }
266
267 pub fn ext(&self) -> String {
277 if let Some(ext) = self.metadata.get("ext")
278 && !ext.is_empty()
279 {
280 return ext.clone();
281 }
282 crate::template::extract_artifact_ext(&self.name).to_string()
283 }
284}
285
286#[derive(Debug, Default)]
287pub struct ArtifactRegistry {
288 artifacts: Vec<Artifact>,
289}
290
291impl ArtifactRegistry {
292 pub fn new() -> Self {
293 Self::default()
294 }
295
296 pub fn add(&mut self, mut artifact: Artifact) {
297 let name = if artifact.name.is_empty() {
299 let derived = artifact
300 .path
301 .file_name()
302 .and_then(|n| n.to_str())
303 .unwrap_or("artifact")
304 .trim()
305 .to_string();
306 artifact.name = derived.clone();
307 derived
308 } else {
309 artifact.name.clone()
310 };
311
312 if should_relativize_path(artifact.kind)
332 && artifact.path.is_absolute()
333 && let Ok(cwd) = std::env::current_dir()
334 && cwd.parent().is_some()
335 && let Ok(rel) = artifact.path.strip_prefix(&cwd)
336 {
337 artifact.path = rel.to_path_buf();
338 }
339
340 let path_str = crate::util::normalize_path_separators(&artifact.path.to_string_lossy());
342 artifact.path = PathBuf::from(path_str);
343
344 if is_uploadable(artifact.kind)
346 && let Some(existing) = self
347 .artifacts
348 .iter()
349 .find(|a| is_uploadable(a.kind) && a.name == name)
350 {
351 tracing::warn!(
354 artifact = %name,
355 existing = %existing.path.display(),
356 new = %artifact.path.display(),
357 "artifact already registered; upload may fail with duplicate error",
358 );
359 }
360
361 self.artifacts.push(artifact);
362 }
363
364 pub fn dedupe_targetless_duplicates(&mut self) {
379 let mut seen: std::collections::HashSet<PathBuf> = std::collections::HashSet::new();
380 self.artifacts.retain(|a| {
381 if a.target.is_some() {
382 return true;
383 }
384 seen.insert(a.path.clone())
385 });
386 }
387
388 pub fn by_kind(&self, kind: ArtifactKind) -> Vec<&Artifact> {
389 self.artifacts.iter().filter(|a| a.kind == kind).collect()
390 }
391
392 pub fn by_kind_and_crate(&self, kind: ArtifactKind, crate_name: &str) -> Vec<&Artifact> {
393 self.artifacts
394 .iter()
395 .filter(|a| a.kind == kind && a.crate_name == crate_name)
396 .collect()
397 }
398
399 pub fn by_kinds_and_crate(&self, kinds: &[ArtifactKind], crate_name: &str) -> Vec<&Artifact> {
400 self.artifacts
401 .iter()
402 .filter(|a| kinds.contains(&a.kind) && a.crate_name == crate_name)
403 .collect()
404 }
405
406 pub fn binary_like_dedup(&self) -> Vec<&Artifact> {
416 let uploadable_paths: std::collections::HashSet<&std::path::Path> = self
417 .artifacts
418 .iter()
419 .filter(|a| a.kind == ArtifactKind::UploadableBinary)
420 .map(|a| a.path.as_path())
421 .collect();
422 self.artifacts
423 .iter()
424 .filter(|a| {
425 matches!(
426 a.kind,
427 ArtifactKind::Binary
428 | ArtifactKind::UploadableBinary
429 | ArtifactKind::UniversalBinary
430 )
431 })
432 .filter(|a| {
433 a.kind == ArtifactKind::UploadableBinary
434 || !uploadable_paths.contains(a.path.as_path())
435 })
436 .collect()
437 }
438
439 pub fn all(&self) -> &[Artifact] {
440 &self.artifacts
441 }
442
443 pub fn all_mut(&mut self) -> &mut [Artifact] {
444 &mut self.artifacts
445 }
446
447 pub fn filter<F: Fn(&Artifact) -> bool>(&self, predicate: F) -> Vec<&Artifact> {
449 self.artifacts.iter().filter(|a| predicate(a)).collect()
450 }
451
452 pub fn remove_by_paths(&mut self, paths: &[std::path::PathBuf]) {
454 self.artifacts.retain(|a| !paths.contains(&a.path));
455 }
456
457 pub fn to_artifacts_json(&self) -> anyhow::Result<serde_json::Value> {
472 let mut sorted: Vec<&Artifact> = self.artifacts.iter().collect();
473 sorted.sort_by(|a, b| {
474 (
475 a.kind.as_str(),
476 a.target.as_deref().unwrap_or(""),
477 a.crate_name.as_str(),
478 a.name.as_str(),
479 a.path.as_path(),
480 )
481 .cmp(&(
482 b.kind.as_str(),
483 b.target.as_deref().unwrap_or(""),
484 b.crate_name.as_str(),
485 b.name.as_str(),
486 b.path.as_path(),
487 ))
488 });
489 let mut val = serde_json::to_value(&sorted)?;
490 if let Some(arr) = val.as_array_mut() {
492 for entry in arr {
493 if let Some(path) = entry
494 .get("path")
495 .and_then(|p| p.as_str())
496 .map(crate::util::normalize_path_separators)
497 {
498 entry["path"] = serde_json::Value::String(path);
499 }
500 }
501 }
502 Ok(val)
503 }
504}
505
506pub fn size_reportable_kinds() -> &'static [ArtifactKind] {
508 &[
509 ArtifactKind::Archive,
511 ArtifactKind::SourceArchive,
512 ArtifactKind::UploadableFile,
513 ArtifactKind::Makeself,
514 ArtifactKind::LinuxPackage,
515 ArtifactKind::Flatpak,
516 ArtifactKind::SourceRpm,
517 ArtifactKind::Sbom,
518 ArtifactKind::Checksum,
519 ArtifactKind::Signature,
520 ArtifactKind::Certificate,
521 ArtifactKind::DiskImage,
522 ArtifactKind::Installer,
523 ArtifactKind::MacOsPackage,
524 ArtifactKind::Snap,
525 ArtifactKind::PublishableSnapcraft,
526 ArtifactKind::Binary,
528 ArtifactKind::UploadableBinary,
529 ArtifactKind::UniversalBinary,
530 ArtifactKind::Library,
531 ArtifactKind::Header,
532 ArtifactKind::CArchive,
533 ArtifactKind::CShared,
534 ArtifactKind::Wasm,
535 ]
536}
537
538pub fn uploadable_kinds() -> &'static [ArtifactKind] {
541 &[
542 ArtifactKind::Archive,
543 ArtifactKind::UploadableBinary,
544 ArtifactKind::SourceArchive,
545 ArtifactKind::UploadableFile,
546 ArtifactKind::Makeself,
547 ArtifactKind::LinuxPackage,
548 ArtifactKind::PublishableSnapcraft,
549 ArtifactKind::Flatpak,
550 ArtifactKind::SourceRpm,
551 ArtifactKind::Sbom,
552 ArtifactKind::Checksum,
553 ArtifactKind::Signature,
554 ArtifactKind::Certificate,
555 ArtifactKind::DiskImage,
556 ArtifactKind::Installer,
557 ArtifactKind::MacOsPackage,
558 ]
559}
560
561pub fn release_uploadable_kinds() -> &'static [ArtifactKind] {
575 &[
576 ArtifactKind::Archive,
577 ArtifactKind::UploadableBinary,
578 ArtifactKind::UploadableFile,
579 ArtifactKind::SourceArchive,
580 ArtifactKind::Makeself,
581 ArtifactKind::LinuxPackage,
582 ArtifactKind::Flatpak,
583 ArtifactKind::SourceRpm,
584 ArtifactKind::Installer,
585 ArtifactKind::DiskImage,
586 ArtifactKind::MacOsPackage,
587 ArtifactKind::Sbom,
588 ArtifactKind::Checksum,
589 ArtifactKind::Signature,
590 ArtifactKind::Certificate,
591 ]
592}
593
594fn is_uploadable(kind: ArtifactKind) -> bool {
596 uploadable_kinds().contains(&kind)
597}
598
599fn should_relativize_path(kind: ArtifactKind) -> bool {
607 !matches!(
608 kind,
609 ArtifactKind::DockerImage
610 | ArtifactKind::DockerImageV2
611 | ArtifactKind::PublishableDockerImage
612 | ArtifactKind::DockerManifest
613 | ArtifactKind::DockerDigest
614 )
615}
616
617pub fn is_binary_sign_output(artifact: &Artifact) -> bool {
622 artifact
623 .metadata
624 .get("binary_sign")
625 .is_some_and(|v| v == "true")
626}
627
628pub fn matches_id_filter(artifact: &Artifact, ids: Option<&[String]>) -> bool {
640 let Some(id_list) = ids else { return true };
641 if id_list.is_empty() {
642 return true;
643 }
644 if matches!(
645 artifact.kind,
646 ArtifactKind::Checksum
647 | ArtifactKind::SourceArchive
648 | ArtifactKind::UploadableFile
649 | ArtifactKind::Metadata
650 ) {
651 return true;
652 }
653 let artifact_id = artifact
654 .metadata
655 .get("id")
656 .map(|s| s.as_str())
657 .unwrap_or("");
658 id_list.iter().any(|id| id == artifact_id)
659}
660
661pub fn format_size(bytes: u64) -> String {
663 const KB: f64 = 1024.0;
664 const MB: f64 = KB * 1024.0;
665 const GB: f64 = MB * 1024.0;
666
667 let b = bytes as f64;
668 if b >= GB {
669 format!("{:.1} GB", b / GB)
670 } else if b >= MB {
671 format!("{:.1} MB", b / MB)
672 } else if b >= KB {
673 format!("{:.1} KB", b / KB)
674 } else {
675 format!("{} B", bytes)
676 }
677}
678
679pub fn print_size_report(registry: &mut ArtifactRegistry, log: &crate::log::StageLogger) {
685 let reportable = size_reportable_kinds();
686 let mut entries: Vec<(String, u64)> = Vec::new();
687 let mut total: u64 = 0;
688
689 for artifact in registry.all_mut() {
690 if !reportable.contains(&artifact.kind) {
691 continue;
692 }
693 if let Ok(meta) = std::fs::metadata(&artifact.path) {
694 let size = meta.len();
695 artifact.size = Some(size);
696 let name = artifact
697 .path
698 .file_name()
699 .map(|n| n.to_string_lossy().to_string())
700 .unwrap_or_else(|| artifact.path.display().to_string());
701 entries.push((name, size));
702 total += size;
703 }
704 }
705
706 if entries.is_empty() {
707 return;
708 }
709
710 let max_name_len = entries.iter().map(|(n, _)| n.len()).max().unwrap_or(0);
711
712 log.status("");
713 log.status("Artifact Sizes:");
714 for (name, size) in &entries {
715 log.status(&format!(
716 " {:<width$} {}",
717 name,
718 format_size(*size),
719 width = max_name_len
720 ));
721 }
722 log.status(&format!(
723 " {:<width$} {}",
724 "Total:",
725 format_size(total),
726 width = max_name_len
727 ));
728}
729
730#[cfg(test)]
731mod tests {
732 use super::*;
733 use std::path::PathBuf;
734
735 #[test]
736 fn test_add_and_query_artifacts() {
737 let mut registry = ArtifactRegistry::new();
738 registry.add(Artifact {
739 kind: ArtifactKind::Binary,
740 name: String::new(),
741 path: PathBuf::from("dist/cfgd"),
742 target: Some("x86_64-unknown-linux-gnu".to_string()),
743 crate_name: "cfgd".to_string(),
744 metadata: Default::default(),
745 size: None,
746 });
747 registry.add(Artifact {
748 kind: ArtifactKind::Archive,
749 name: String::new(),
750 path: PathBuf::from("dist/cfgd.tar.gz"),
751 target: Some("x86_64-unknown-linux-gnu".to_string()),
752 crate_name: "cfgd".to_string(),
753 metadata: Default::default(),
754 size: None,
755 });
756
757 let binaries = registry.by_kind(ArtifactKind::Binary);
758 assert_eq!(binaries.len(), 1);
759
760 let archives = registry.by_kind_and_crate(ArtifactKind::Archive, "cfgd");
761 assert_eq!(archives.len(), 1);
762 }
763
764 #[test]
765 fn test_empty_query() {
766 let registry = ArtifactRegistry::new();
767 assert!(registry.by_kind(ArtifactKind::Binary).is_empty());
768 }
769
770 #[test]
777 fn dedupe_targetless_duplicates_collapses_cross_shard_dups() {
778 let mut registry = ArtifactRegistry::new();
779 for _ in 0..3 {
781 registry.add(Artifact {
782 kind: ArtifactKind::SourceArchive,
783 name: "anodizer-0.3.0-source.tar.gz".to_string(),
784 path: PathBuf::from("dist/anodizer-0.3.0-source.tar.gz"),
785 target: None,
786 crate_name: "anodizer".to_string(),
787 metadata: HashMap::new(),
788 size: None,
789 });
790 }
791 for triple in &["x86_64-unknown-linux-gnu", "aarch64-unknown-linux-gnu"] {
797 registry.add(Artifact {
798 kind: ArtifactKind::Archive,
799 name: format!("anodizer-0.3.0-{}.tar.gz", triple),
800 path: PathBuf::from(format!("dist/anodizer-0.3.0-{}.tar.gz", triple)),
801 target: Some((*triple).to_string()),
802 crate_name: "anodizer".to_string(),
803 metadata: HashMap::new(),
804 size: None,
805 });
806 }
807
808 registry.dedupe_targetless_duplicates();
809
810 let sources: Vec<_> = registry.by_kind(ArtifactKind::SourceArchive);
812 assert_eq!(
813 sources.len(),
814 1,
815 "cross-shard target-None duplicates must collapse to 1 entry"
816 );
817 assert_eq!(registry.by_kind(ArtifactKind::Archive).len(), 2);
819 }
820
821 #[test]
825 fn dedupe_targetless_duplicates_leaves_per_target_duplicates_intact() {
826 let mut registry = ArtifactRegistry::new();
827 for _ in 0..3 {
828 registry.add(Artifact {
829 kind: ArtifactKind::Archive,
830 name: "anodizer-x86_64.tar.gz".to_string(),
831 path: PathBuf::from("dist/anodizer-x86_64.tar.gz"),
832 target: Some("x86_64-unknown-linux-gnu".to_string()),
833 crate_name: "anodizer".to_string(),
834 metadata: HashMap::new(),
835 size: None,
836 });
837 }
838
839 registry.dedupe_targetless_duplicates();
840
841 assert_eq!(
842 registry.by_kind(ArtifactKind::Archive).len(),
843 3,
844 "per-target duplicates must remain so detect_duplicate_artifact_paths can flag them"
845 );
846 }
847
848 #[test]
849 fn test_by_kinds_and_crate() {
850 let mut registry = ArtifactRegistry::new();
851 registry.add(Artifact {
852 kind: ArtifactKind::Binary,
853 name: "bin".to_string(),
854 path: PathBuf::from("bin"),
855 target: None,
856 crate_name: "app".to_string(),
857 metadata: HashMap::new(),
858 size: None,
859 });
860 registry.add(Artifact {
861 kind: ArtifactKind::UniversalBinary,
862 name: "ubin".to_string(),
863 path: PathBuf::from("ubin"),
864 target: None,
865 crate_name: "app".to_string(),
866 metadata: HashMap::new(),
867 size: None,
868 });
869 registry.add(Artifact {
870 kind: ArtifactKind::Header,
871 name: "hdr".to_string(),
872 path: PathBuf::from("hdr"),
873 target: None,
874 crate_name: "other".to_string(),
875 metadata: HashMap::new(),
876 size: None,
877 });
878
879 let results = registry.by_kinds_and_crate(
880 &[ArtifactKind::Binary, ArtifactKind::UniversalBinary],
881 "app",
882 );
883 assert_eq!(results.len(), 2);
884
885 let results = registry.by_kinds_and_crate(&[ArtifactKind::Header], "app");
887 assert_eq!(results.len(), 0);
888 }
889
890 #[test]
891 fn test_to_artifacts_json_empty() {
892 let registry = ArtifactRegistry::new();
893 let json = registry.to_artifacts_json().unwrap();
894 assert!(json.is_array());
895 assert_eq!(json.as_array().unwrap().len(), 0);
896 }
897
898 #[test]
899 fn test_to_artifacts_json_with_artifacts() {
900 let mut registry = ArtifactRegistry::new();
901 let mut meta = HashMap::new();
902 meta.insert("format".to_string(), "tar.gz".to_string());
903 registry.add(Artifact {
904 kind: ArtifactKind::Archive,
905 name: String::new(),
906 path: PathBuf::from("dist/myapp-1.0.0-linux-amd64.tar.gz"),
907 target: Some("x86_64-unknown-linux-gnu".to_string()),
908 crate_name: "myapp".to_string(),
909 metadata: meta,
910 size: None,
911 });
912 registry.add(Artifact {
913 kind: ArtifactKind::Checksum,
914 name: String::new(),
915 path: PathBuf::from("dist/myapp_1.0.0_checksums.txt"),
916 target: None,
917 crate_name: "myapp".to_string(),
918 metadata: Default::default(),
919 size: None,
920 });
921
922 let json = registry.to_artifacts_json().unwrap();
923 let arr = json.as_array().unwrap();
924 assert_eq!(arr.len(), 2);
925
926 let first = &arr[0];
928 assert_eq!(first["kind"], "archive");
929 assert_eq!(first["path"], "dist/myapp-1.0.0-linux-amd64.tar.gz");
930 assert_eq!(first["target"], "x86_64-unknown-linux-gnu");
931 assert_eq!(first["crate_name"], "myapp");
932 assert_eq!(first["metadata"]["format"], "tar.gz");
933
934 let second = &arr[1];
936 assert_eq!(second["kind"], "checksum");
937 assert!(second["target"].is_null());
938 }
939
940 #[test]
950 #[serial_test::serial(cwd)]
951 fn to_artifacts_json_strips_absolute_worktree_prefix() {
952 let cwd_guard = tempfile::tempdir().unwrap();
953 let original_cwd = std::env::current_dir().unwrap();
954 std::env::set_current_dir(cwd_guard.path()).unwrap();
955 let canonical_cwd = std::env::current_dir().unwrap();
958 let abs = canonical_cwd
959 .join("dist")
960 .join("anodize-1.0.0-linux-amd64.tar.gz");
961
962 let mut registry = ArtifactRegistry::new();
963 registry.add(Artifact {
964 kind: ArtifactKind::Archive,
965 name: String::new(),
966 path: abs,
967 target: Some("x86_64-unknown-linux-gnu".to_string()),
968 crate_name: "anodize".to_string(),
969 metadata: Default::default(),
970 size: None,
971 });
972
973 let json = registry.to_artifacts_json().unwrap();
974 let arr = json.as_array().unwrap();
975 assert_eq!(
976 arr[0]["path"], "dist/anodize-1.0.0-linux-amd64.tar.gz",
977 "absolute worktree prefix must be stripped at add() time so two \
978 determinism-harness runs at different worktree paths produce \
979 byte-identical artifacts.json"
980 );
981
982 std::env::set_current_dir(original_cwd).unwrap();
983 }
984
985 #[test]
997 fn to_artifacts_json_output_is_order_insensitive() {
998 let mut reg_a = ArtifactRegistry::new();
1000 reg_a.add(Artifact {
1001 kind: ArtifactKind::Archive,
1002 name: String::new(),
1003 path: PathBuf::from("dist/anodize-1.0.0-linux-arm64.tar.gz"),
1004 target: Some("aarch64-unknown-linux-gnu".to_string()),
1005 crate_name: "anodize".to_string(),
1006 metadata: Default::default(),
1007 size: Some(15_000_000),
1008 });
1009 reg_a.add(Artifact {
1010 kind: ArtifactKind::Archive,
1011 name: String::new(),
1012 path: PathBuf::from("dist/anodize-1.0.0-linux-amd64.tar.gz"),
1013 target: Some("x86_64-unknown-linux-gnu".to_string()),
1014 crate_name: "anodize".to_string(),
1015 metadata: Default::default(),
1016 size: Some(18_000_000),
1017 });
1018
1019 let mut reg_b = ArtifactRegistry::new();
1021 reg_b.add(Artifact {
1022 kind: ArtifactKind::Archive,
1023 name: String::new(),
1024 path: PathBuf::from("dist/anodize-1.0.0-linux-amd64.tar.gz"),
1025 target: Some("x86_64-unknown-linux-gnu".to_string()),
1026 crate_name: "anodize".to_string(),
1027 metadata: Default::default(),
1028 size: Some(18_000_000),
1029 });
1030 reg_b.add(Artifact {
1031 kind: ArtifactKind::Archive,
1032 name: String::new(),
1033 path: PathBuf::from("dist/anodize-1.0.0-linux-arm64.tar.gz"),
1034 target: Some("aarch64-unknown-linux-gnu".to_string()),
1035 crate_name: "anodize".to_string(),
1036 metadata: Default::default(),
1037 size: Some(15_000_000),
1038 });
1039
1040 let json_a = serde_json::to_string_pretty(®_a.to_artifacts_json().unwrap()).unwrap();
1041 let json_b = serde_json::to_string_pretty(®_b.to_artifacts_json().unwrap()).unwrap();
1042
1043 assert_eq!(
1044 json_a, json_b,
1045 "two registries with the same artifacts in different insertion \
1046 orders must produce byte-identical artifacts.json — otherwise \
1047 the determinism harness will surface per-run drift in dist/"
1048 );
1049 }
1050
1051 #[test]
1057 #[serial_test::serial(cwd)]
1058 fn to_artifacts_json_preserves_docker_image_refs() {
1059 let mut registry = ArtifactRegistry::new();
1060 registry.add(Artifact {
1061 kind: ArtifactKind::DockerImage,
1062 name: "myorg/myimage:v1.2.3".to_string(),
1063 path: PathBuf::from("/myorg/myimage:v1.2.3"),
1064 target: None,
1065 crate_name: "myapp".to_string(),
1066 metadata: Default::default(),
1067 size: None,
1068 });
1069
1070 let json = registry.to_artifacts_json().unwrap();
1071 let arr = json.as_array().unwrap();
1072 assert_eq!(
1073 arr[0]["path"], "/myorg/myimage:v1.2.3",
1074 "docker image refs are pass-through and must not be relativized"
1075 );
1076 }
1077
1078 #[test]
1079 fn to_artifacts_json_drops_content_hash_keys() {
1080 let mut metadata = HashMap::new();
1081 metadata.insert("format".into(), "deb".into());
1082 metadata.insert("id".into(), "default".into());
1083 metadata.insert("Checksum".into(), "sha256:abc".into());
1087 metadata.insert("sha256".into(), "abc".into());
1088 metadata.insert("blake3".into(), "xyz".into());
1089
1090 let mut registry = ArtifactRegistry::new();
1091 registry.add(Artifact {
1092 kind: ArtifactKind::LinuxPackage,
1093 name: "pkg.deb".to_string(),
1094 path: PathBuf::from("dist/pkg.deb"),
1095 target: Some("x86_64-unknown-linux-gnu".to_string()),
1096 crate_name: "myapp".to_string(),
1097 metadata,
1098 size: None,
1099 });
1100
1101 let json = registry.to_artifacts_json().unwrap();
1102 let meta = &json.as_array().unwrap()[0]["metadata"];
1103 assert_eq!(meta["format"], "deb");
1104 assert_eq!(meta["id"], "default");
1105 assert!(
1106 meta.get("Checksum").is_none(),
1107 "Checksum (content-hash) must be filtered from artifacts.json: {meta:?}"
1108 );
1109 assert!(meta.get("sha256").is_none(), "sha256 must be filtered");
1110 assert!(meta.get("blake3").is_none(), "blake3 must be filtered");
1111 }
1112
1113 #[test]
1114 fn test_metadata_json_is_valid_json_string() {
1115 let mut registry = ArtifactRegistry::new();
1116 registry.add(Artifact {
1117 kind: ArtifactKind::Binary,
1118 name: String::new(),
1119 path: PathBuf::from("dist/myapp"),
1120 target: Some("x86_64-unknown-linux-gnu".to_string()),
1121 crate_name: "myapp".to_string(),
1122 metadata: Default::default(),
1123 size: None,
1124 });
1125
1126 let json = registry.to_artifacts_json().unwrap();
1127 let serialized = serde_json::to_string_pretty(&json).unwrap();
1128 let parsed: serde_json::Value = serde_json::from_str(&serialized).unwrap();
1130 assert_eq!(parsed, json);
1131 }
1132
1133 #[test]
1134 fn test_format_size_bytes() {
1135 assert_eq!(format_size(0), "0 B");
1136 assert_eq!(format_size(512), "512 B");
1137 assert_eq!(format_size(1023), "1023 B");
1138 }
1139
1140 #[test]
1141 fn test_format_size_kilobytes() {
1142 assert_eq!(format_size(1024), "1.0 KB");
1143 assert_eq!(format_size(1536), "1.5 KB");
1144 assert_eq!(format_size(10240), "10.0 KB");
1145 }
1146
1147 #[test]
1148 fn test_format_size_megabytes() {
1149 assert_eq!(format_size(1048576), "1.0 MB");
1150 assert_eq!(format_size(4404019), "4.2 MB");
1151 }
1152
1153 #[test]
1154 fn test_format_size_gigabytes() {
1155 assert_eq!(format_size(1073741824), "1.0 GB");
1156 assert_eq!(format_size(2147483648), "2.0 GB");
1157 }
1158
1159 #[test]
1160 fn test_artifact_kind_serializes_to_snake_case() {
1161 let json = serde_json::to_value(ArtifactKind::DockerImage).unwrap();
1162 assert_eq!(json, "docker_image");
1163 let json = serde_json::to_value(ArtifactKind::LinuxPackage).unwrap();
1164 assert_eq!(json, "linux_package");
1165 let json = serde_json::to_value(ArtifactKind::Binary).unwrap();
1166 assert_eq!(json, "binary");
1167 }
1168
1169 #[test]
1170 fn test_artifact_kind_new_variants_serialize() {
1171 assert_eq!(
1172 serde_json::to_value(ArtifactKind::UploadableBinary).unwrap(),
1173 "uploadable_binary"
1174 );
1175 assert_eq!(
1176 serde_json::to_value(ArtifactKind::UniversalBinary).unwrap(),
1177 "universal_binary"
1178 );
1179 assert_eq!(
1180 serde_json::to_value(ArtifactKind::Header).unwrap(),
1181 "header"
1182 );
1183 assert_eq!(
1184 serde_json::to_value(ArtifactKind::CArchive).unwrap(),
1185 "c_archive"
1186 );
1187 assert_eq!(
1188 serde_json::to_value(ArtifactKind::CShared).unwrap(),
1189 "c_shared"
1190 );
1191 assert_eq!(
1192 serde_json::to_value(ArtifactKind::Makeself).unwrap(),
1193 "makeself"
1194 );
1195 assert_eq!(
1196 serde_json::to_value(ArtifactKind::DockerImageV2).unwrap(),
1197 "docker_image_v2"
1198 );
1199 assert_eq!(
1200 serde_json::to_value(ArtifactKind::PublishableDockerImage).unwrap(),
1201 "publishable_docker_image"
1202 );
1203 assert_eq!(
1204 serde_json::to_value(ArtifactKind::PublishableSnapcraft).unwrap(),
1205 "publishable_snapcraft"
1206 );
1207 assert_eq!(
1208 serde_json::to_value(ArtifactKind::SourceRpm).unwrap(),
1209 "source_rpm"
1210 );
1211 assert_eq!(
1212 serde_json::to_value(ArtifactKind::BrewFormula).unwrap(),
1213 "brew_formula"
1214 );
1215 assert_eq!(
1216 serde_json::to_value(ArtifactKind::BrewCask).unwrap(),
1217 "brew_cask"
1218 );
1219 assert_eq!(
1220 serde_json::to_value(ArtifactKind::Nixpkg).unwrap(),
1221 "nixpkg"
1222 );
1223 assert_eq!(
1224 serde_json::to_value(ArtifactKind::ScoopManifest).unwrap(),
1225 "scoop_manifest"
1226 );
1227 assert_eq!(
1228 serde_json::to_value(ArtifactKind::PublishableChocolatey).unwrap(),
1229 "publishable_chocolatey"
1230 );
1231 assert_eq!(
1232 serde_json::to_value(ArtifactKind::WingetInstaller).unwrap(),
1233 "winget_installer"
1234 );
1235 assert_eq!(
1236 serde_json::to_value(ArtifactKind::WingetDefaultLocale).unwrap(),
1237 "winget_default_locale"
1238 );
1239 assert_eq!(
1240 serde_json::to_value(ArtifactKind::WingetVersion).unwrap(),
1241 "winget_version"
1242 );
1243 assert_eq!(
1244 serde_json::to_value(ArtifactKind::PkgBuild).unwrap(),
1245 "pkg_build"
1246 );
1247 assert_eq!(
1248 serde_json::to_value(ArtifactKind::SrcInfo).unwrap(),
1249 "src_info"
1250 );
1251 assert_eq!(
1252 serde_json::to_value(ArtifactKind::SourcePkgBuild).unwrap(),
1253 "source_pkg_build"
1254 );
1255 assert_eq!(
1256 serde_json::to_value(ArtifactKind::SourceSrcInfo).unwrap(),
1257 "source_src_info"
1258 );
1259 assert_eq!(
1260 serde_json::to_value(ArtifactKind::KrewPluginManifest).unwrap(),
1261 "krew_plugin_manifest"
1262 );
1263 assert_eq!(
1264 serde_json::to_value(ArtifactKind::UploadableFile).unwrap(),
1265 "uploadable_file"
1266 );
1267 }
1268
1269 #[test]
1270 fn test_artifact_kind_library_and_wasm() {
1271 let json = serde_json::to_value(ArtifactKind::Library).unwrap();
1272 assert_eq!(json, "library");
1273 let json = serde_json::to_value(ArtifactKind::Wasm).unwrap();
1274 assert_eq!(json, "wasm");
1275 }
1276
1277 #[test]
1278 fn test_artifact_kind_as_str_library_wasm() {
1279 assert_eq!(ArtifactKind::Library.as_str(), "library");
1280 assert_eq!(ArtifactKind::Wasm.as_str(), "wasm");
1281 }
1282
1283 #[test]
1284 fn test_artifact_kind_parse_roundtrip_all_variants() {
1285 let all_variants = [
1286 ArtifactKind::Binary,
1287 ArtifactKind::UploadableBinary,
1288 ArtifactKind::UniversalBinary,
1289 ArtifactKind::Library,
1290 ArtifactKind::Header,
1291 ArtifactKind::CArchive,
1292 ArtifactKind::CShared,
1293 ArtifactKind::Wasm,
1294 ArtifactKind::Archive,
1295 ArtifactKind::SourceArchive,
1296 ArtifactKind::Makeself,
1297 ArtifactKind::LinuxPackage,
1298 ArtifactKind::Snap,
1299 ArtifactKind::PublishableSnapcraft,
1300 ArtifactKind::Flatpak,
1301 ArtifactKind::SourceRpm,
1302 ArtifactKind::DiskImage,
1303 ArtifactKind::Installer,
1304 ArtifactKind::MacOsPackage,
1305 ArtifactKind::DockerImage,
1306 ArtifactKind::DockerImageV2,
1307 ArtifactKind::PublishableDockerImage,
1308 ArtifactKind::DockerManifest,
1309 ArtifactKind::BrewFormula,
1310 ArtifactKind::BrewCask,
1311 ArtifactKind::Nixpkg,
1312 ArtifactKind::ScoopManifest,
1313 ArtifactKind::PublishableChocolatey,
1314 ArtifactKind::WingetInstaller,
1315 ArtifactKind::WingetDefaultLocale,
1316 ArtifactKind::WingetVersion,
1317 ArtifactKind::PkgBuild,
1318 ArtifactKind::SrcInfo,
1319 ArtifactKind::SourcePkgBuild,
1320 ArtifactKind::SourceSrcInfo,
1321 ArtifactKind::KrewPluginManifest,
1322 ArtifactKind::Checksum,
1323 ArtifactKind::Signature,
1324 ArtifactKind::Certificate,
1325 ArtifactKind::Sbom,
1326 ArtifactKind::Metadata,
1327 ArtifactKind::UploadableFile,
1328 ];
1329 for variant in &all_variants {
1330 let s = variant.as_str();
1331 let parsed =
1332 ArtifactKind::parse(s).unwrap_or_else(|| panic!("parse({:?}) returned None", s));
1333 assert_eq!(*variant, parsed, "roundtrip failed for {:?}", s);
1334 }
1335 assert_eq!(all_variants.len(), 42, "update test when adding variants");
1336 }
1337
1338 #[test]
1339 fn test_query_by_library_and_wasm_kinds() {
1340 let mut registry = ArtifactRegistry::new();
1341 registry.add(Artifact {
1342 kind: ArtifactKind::Library,
1343 name: String::new(),
1344 path: PathBuf::from("target/libmylib.so"),
1345 target: Some("x86_64-unknown-linux-gnu".to_string()),
1346 crate_name: "mylib".to_string(),
1347 metadata: Default::default(),
1348 size: None,
1349 });
1350 registry.add(Artifact {
1351 kind: ArtifactKind::Wasm,
1352 name: String::new(),
1353 path: PathBuf::from("target/mylib.wasm"),
1354 target: Some("wasm32-unknown-unknown".to_string()),
1355 crate_name: "mylib".to_string(),
1356 metadata: Default::default(),
1357 size: None,
1358 });
1359
1360 assert_eq!(registry.by_kind(ArtifactKind::Library).len(), 1);
1361 assert_eq!(registry.by_kind(ArtifactKind::Wasm).len(), 1);
1362 assert_eq!(
1363 registry
1364 .by_kind_and_crate(ArtifactKind::Wasm, "mylib")
1365 .len(),
1366 1
1367 );
1368 }
1369
1370 #[test]
1371 fn test_size_reportable_kinds_includes_releasable_and_binaries() {
1372 let kinds = size_reportable_kinds();
1373 assert!(kinds.contains(&ArtifactKind::Archive));
1375 assert!(kinds.contains(&ArtifactKind::SourceArchive));
1376 assert!(kinds.contains(&ArtifactKind::UploadableFile));
1377 assert!(kinds.contains(&ArtifactKind::Makeself));
1378 assert!(kinds.contains(&ArtifactKind::LinuxPackage));
1379 assert!(kinds.contains(&ArtifactKind::Flatpak));
1380 assert!(kinds.contains(&ArtifactKind::SourceRpm));
1381 assert!(kinds.contains(&ArtifactKind::Sbom));
1382 assert!(kinds.contains(&ArtifactKind::Checksum));
1383 assert!(kinds.contains(&ArtifactKind::Signature));
1384 assert!(kinds.contains(&ArtifactKind::Certificate));
1385 assert!(kinds.contains(&ArtifactKind::DiskImage));
1386 assert!(kinds.contains(&ArtifactKind::Installer));
1387 assert!(kinds.contains(&ArtifactKind::MacOsPackage));
1388 assert!(kinds.contains(&ArtifactKind::Snap));
1389 assert!(kinds.contains(&ArtifactKind::Binary));
1391 assert!(kinds.contains(&ArtifactKind::UniversalBinary));
1392 assert!(kinds.contains(&ArtifactKind::Library));
1393 assert!(kinds.contains(&ArtifactKind::Header));
1394 assert!(kinds.contains(&ArtifactKind::CArchive));
1395 assert!(kinds.contains(&ArtifactKind::CShared));
1396 assert!(kinds.contains(&ArtifactKind::Wasm));
1397 }
1398
1399 #[test]
1400 fn test_size_reportable_kinds_excludes_non_releasable() {
1401 let kinds = size_reportable_kinds();
1402 assert!(!kinds.contains(&ArtifactKind::DockerImage));
1403 assert!(!kinds.contains(&ArtifactKind::DockerManifest));
1404 assert!(!kinds.contains(&ArtifactKind::Metadata));
1405 assert!(!kinds.contains(&ArtifactKind::BrewFormula));
1406 assert!(!kinds.contains(&ArtifactKind::ScoopManifest));
1407 }
1408
1409 #[test]
1410 fn test_print_size_report_filters_and_stores_size() {
1411 use std::io::Write;
1412
1413 let dir = std::env::temp_dir().join("anodizer_test_size_report");
1414 let _ = std::fs::remove_dir_all(&dir);
1415 std::fs::create_dir_all(&dir).unwrap();
1416
1417 let archive_path = dir.join("app.tar.gz");
1419 let mut f = std::fs::File::create(&archive_path).unwrap();
1420 f.write_all(&[0u8; 2048]).unwrap();
1421
1422 let binary_path = dir.join("app");
1423 let mut f = std::fs::File::create(&binary_path).unwrap();
1424 f.write_all(&[0u8; 4096]).unwrap();
1425
1426 let docker_path = dir.join("docker-image");
1427 let mut f = std::fs::File::create(&docker_path).unwrap();
1428 f.write_all(&[0u8; 8192]).unwrap();
1429
1430 let mut registry = ArtifactRegistry::new();
1431 registry.add(Artifact {
1432 kind: ArtifactKind::Archive,
1433 name: String::new(),
1434 path: archive_path.clone(),
1435 target: None,
1436 crate_name: "app".to_string(),
1437 metadata: Default::default(),
1438 size: None,
1439 });
1440 registry.add(Artifact {
1441 kind: ArtifactKind::Binary,
1442 name: String::new(),
1443 path: binary_path.clone(),
1444 target: None,
1445 crate_name: "app".to_string(),
1446 metadata: Default::default(),
1447 size: None,
1448 });
1449 registry.add(Artifact {
1451 kind: ArtifactKind::DockerImage,
1452 name: String::new(),
1453 path: docker_path.clone(),
1454 target: None,
1455 crate_name: "app".to_string(),
1456 metadata: Default::default(),
1457 size: None,
1458 });
1459
1460 let log = crate::log::StageLogger::new("test", crate::log::Verbosity::Normal);
1461 print_size_report(&mut registry, &log);
1462
1463 let archive = ®istry.all()[0];
1465 assert_eq!(archive.kind, ArtifactKind::Archive);
1466 assert_eq!(archive.size, Some(2048));
1467
1468 let binary = ®istry.all()[1];
1469 assert_eq!(binary.kind, ArtifactKind::Binary);
1470 assert_eq!(binary.size, Some(4096));
1471
1472 let docker = ®istry.all()[2];
1474 assert_eq!(docker.kind, ArtifactKind::DockerImage);
1475 assert_eq!(docker.size, None);
1476
1477 let _ = std::fs::remove_dir_all(&dir);
1478 }
1479
1480 #[test]
1481 fn test_size_field_defaults_to_none() {
1482 let registry = ArtifactRegistry::new();
1483 let mut reg = ArtifactRegistry::new();
1485 reg.add(Artifact {
1486 kind: ArtifactKind::Binary,
1487 name: String::new(),
1488 path: PathBuf::from("/nonexistent/binary"),
1489 target: None,
1490 crate_name: "test".to_string(),
1491 metadata: Default::default(),
1492 size: None,
1493 });
1494 assert_eq!(reg.all()[0].size, None);
1495 drop(registry);
1496 }
1497
1498 #[test]
1499 fn test_size_field_not_serialized_when_none() {
1500 let mut registry = ArtifactRegistry::new();
1501 registry.add(Artifact {
1502 kind: ArtifactKind::Binary,
1503 name: String::new(),
1504 path: PathBuf::from("dist/myapp"),
1505 target: None,
1506 crate_name: "myapp".to_string(),
1507 metadata: Default::default(),
1508 size: None,
1509 });
1510 let json = registry.to_artifacts_json().unwrap();
1511 let first = &json.as_array().unwrap()[0];
1512 assert!(first.get("size").is_none());
1514 }
1515
1516 #[test]
1517 fn test_size_field_serialized_when_some() {
1518 let mut registry = ArtifactRegistry::new();
1519 registry.add(Artifact {
1520 kind: ArtifactKind::Binary,
1521 name: String::new(),
1522 path: PathBuf::from("dist/myapp"),
1523 target: None,
1524 crate_name: "myapp".to_string(),
1525 metadata: Default::default(),
1526 size: Some(12345),
1527 });
1528 let json = registry.to_artifacts_json().unwrap();
1529 let first = &json.as_array().unwrap()[0];
1530 assert_eq!(first["size"], 12345);
1531 }
1532
1533 #[test]
1534 fn release_uploadable_kinds_matches_canonical_set() {
1535 let kinds = release_uploadable_kinds();
1545 let expected = [
1546 ArtifactKind::Archive,
1547 ArtifactKind::UploadableBinary,
1548 ArtifactKind::UploadableFile,
1549 ArtifactKind::SourceArchive,
1550 ArtifactKind::Makeself,
1551 ArtifactKind::LinuxPackage,
1552 ArtifactKind::Flatpak,
1553 ArtifactKind::SourceRpm,
1554 ArtifactKind::Installer,
1555 ArtifactKind::DiskImage,
1556 ArtifactKind::MacOsPackage,
1557 ArtifactKind::Sbom,
1558 ArtifactKind::Checksum,
1559 ArtifactKind::Signature,
1560 ArtifactKind::Certificate,
1561 ];
1562 assert_eq!(kinds, &expected);
1563 }
1564
1565 #[test]
1566 fn artifact_ext_prefers_metadata_when_present() {
1567 let mut metadata = HashMap::new();
1573 metadata.insert("ext".to_string(), ".src.rpm".to_string());
1574 let art = Artifact {
1575 kind: ArtifactKind::SourceRpm,
1576 name: "myapp-1.0.0-1.fc42.src.rpm".to_string(),
1577 path: PathBuf::from("dist/myapp-1.0.0-1.fc42.src.rpm"),
1578 target: None,
1579 crate_name: "myapp".to_string(),
1580 metadata,
1581 size: None,
1582 };
1583 assert_eq!(art.ext(), ".src.rpm");
1584 }
1585
1586 #[test]
1587 fn artifact_ext_falls_back_to_filename_when_metadata_missing() {
1588 let art = Artifact {
1589 kind: ArtifactKind::Archive,
1590 name: "myapp-1.0.0-linux-amd64.tar.gz".to_string(),
1591 path: PathBuf::from("dist/myapp-1.0.0-linux-amd64.tar.gz"),
1592 target: Some("x86_64-unknown-linux-gnu".to_string()),
1593 crate_name: "myapp".to_string(),
1594 metadata: HashMap::new(),
1595 size: None,
1596 };
1597 assert_eq!(art.ext(), ".tar.gz");
1598 }
1599
1600 #[test]
1601 fn artifact_ext_falls_back_when_metadata_ext_is_empty() {
1602 let mut metadata = HashMap::new();
1603 metadata.insert("ext".to_string(), String::new());
1604 let art = Artifact {
1605 kind: ArtifactKind::Archive,
1606 name: "myapp.zip".to_string(),
1607 path: PathBuf::from("dist/myapp.zip"),
1608 target: None,
1609 crate_name: "myapp".to_string(),
1610 metadata,
1611 size: None,
1612 };
1613 assert_eq!(art.ext(), ".zip");
1614 }
1615
1616 #[test]
1617 fn release_uploadable_kinds_excludes_snap_store_and_raw_build_outputs() {
1618 let kinds = release_uploadable_kinds();
1625 for excluded in [
1626 ArtifactKind::Snap,
1627 ArtifactKind::PublishableSnapcraft,
1628 ArtifactKind::Binary,
1629 ArtifactKind::UniversalBinary,
1630 ] {
1631 assert!(
1632 !kinds.contains(&excluded),
1633 "{:?} must not be in release_uploadable_kinds()",
1634 excluded
1635 );
1636 }
1637 }
1638}