1use crate::set::{PackageLookup, PackageSet, PackageSetError};
5
6pub struct OverlayPackageSet {
7 pub base: Box<dyn PackageSet>,
8 pub top: Box<dyn PackageSet>,
9 pub label: String,
10}
11
12impl OverlayPackageSet {
13 pub fn new(base: Box<dyn PackageSet>, top: Box<dyn PackageSet>) -> Self {
14 let lbl = format!("{} <- {}", base.label(), top.label());
15 Self {
16 base,
17 top,
18 label: lbl,
19 }
20 }
21}
22
23impl PackageSet for OverlayPackageSet {
24 fn get(&self, name: &str) -> Result<PackageLookup, PackageSetError> {
25 match self.top.get(name)? {
26 Some(d) => Ok(Some(d)),
27 None => self.base.get(name),
28 }
29 }
30
31 fn names(&self) -> Vec<String> {
32 let mut all = self.base.names();
33 for n in self.top.names() {
34 if !all.contains(&n) {
35 all.push(n);
36 }
37 }
38 all.sort();
39 all
40 }
41
42 fn label(&self) -> &str {
43 &self.label
44 }
45}
46
47#[cfg(test)]
48mod tests {
49 use super::*;
50 use crate::NixpkgsBridge;
51
52 struct Erroring {
55 reason: String,
56 }
57 impl PackageSet for Erroring {
58 fn get(&self, _name: &str) -> Result<PackageLookup, PackageSetError> {
59 Err(PackageSetError::Backend(self.reason.clone()))
60 }
61 fn names(&self) -> Vec<String> {
62 vec![]
63 }
64 fn label(&self) -> &str {
65 &self.reason
66 }
67 }
68
69 struct Empty {
74 label: String,
75 }
76 impl PackageSet for Empty {
77 fn get(&self, _name: &str) -> Result<PackageLookup, PackageSetError> {
78 Ok(None)
79 }
80 fn names(&self) -> Vec<String> {
81 vec![]
82 }
83 fn label(&self) -> &str {
84 &self.label
85 }
86 }
87
88 #[test]
89 fn top_wins_on_overlap_base_fills_rest() {
90 let base = NixpkgsBridge::new()
91 .with_names(vec!["hello".into(), "bash".into()])
92 .with_label("base");
93 let top = NixpkgsBridge::new()
94 .with_pkg_set("import ./my-nixpkgs.nix {}")
95 .with_names(vec!["bash".into(), "zsh".into()])
96 .with_label("top");
97 let overlay = OverlayPackageSet::new(Box::new(base), Box::new(top));
98
99 let bash = overlay.get("bash").unwrap().unwrap();
100 assert_eq!(
102 bash.bridge.unwrap().pkg_set.as_deref(),
103 Some("import ./my-nixpkgs.nix {}")
104 );
105
106 let hello = overlay.get("hello").unwrap().unwrap();
107 assert!(hello.bridge.unwrap().pkg_set.is_none());
109
110 let mut ns = overlay.names();
111 ns.sort();
112 assert_eq!(ns, vec!["bash", "hello", "zsh"]);
113 }
114
115 #[test]
116 fn label_composition_is_base_arrow_top() {
117 let base = NixpkgsBridge::new().with_label("stable").with_names(vec![]);
121 let top = NixpkgsBridge::new().with_label("nightly").with_names(vec![]);
122 let overlay = OverlayPackageSet::new(Box::new(base), Box::new(top));
123 assert_eq!(overlay.label(), "stable <- nightly");
124 }
125
126 #[test]
127 fn empty_overlay_yields_no_names_and_none_lookup() {
128 let base = Empty { label: "b".into() };
131 let top = Empty { label: "t".into() };
132 let overlay = OverlayPackageSet::new(Box::new(base), Box::new(top));
133 assert!(overlay.get("anything").unwrap().is_none());
134 assert!(overlay.names().is_empty());
135 }
136
137 #[test]
138 fn names_are_returned_sorted() {
139 let base = NixpkgsBridge::new()
143 .with_names(vec!["zulu".into(), "alpha".into()])
144 .with_label("base");
145 let top = NixpkgsBridge::new()
146 .with_names(vec!["mike".into(), "charlie".into()])
147 .with_label("top");
148 let overlay = OverlayPackageSet::new(Box::new(base), Box::new(top));
149 let ns = overlay.names();
150 assert_eq!(ns, vec!["alpha", "charlie", "mike", "zulu"]);
151 }
152
153 #[test]
154 fn duplicate_names_across_sides_deduped() {
155 let base = NixpkgsBridge::new()
158 .with_names(vec!["bash".into(), "hello".into()])
159 .with_label("base");
160 let top = NixpkgsBridge::new()
161 .with_names(vec!["bash".into(), "zsh".into()])
162 .with_label("top");
163 let overlay = OverlayPackageSet::new(Box::new(base), Box::new(top));
164 let ns = overlay.names();
165 let bash_count = ns.iter().filter(|n| *n == "bash").count();
166 assert_eq!(bash_count, 1);
167 assert_eq!(ns.len(), 3);
168 }
169
170 #[test]
171 fn top_error_propagates() {
172 let base = NixpkgsBridge::new()
175 .with_names(vec!["curl".into()])
176 .with_label("base");
177 let top = Erroring {
178 reason: "top exploded".into(),
179 };
180 let overlay = OverlayPackageSet::new(Box::new(base), Box::new(top));
181 let err = overlay.get("curl").unwrap_err();
182 assert_eq!(err.to_string(), "backend: top exploded");
183 }
184
185 #[test]
186 fn base_error_propagates_when_top_returns_none() {
187 let base = Erroring {
191 reason: "base exploded".into(),
192 };
193 let top = NixpkgsBridge::new()
194 .with_names(vec!["never".into()])
195 .with_label("top");
196 let overlay = OverlayPackageSet::new(Box::new(base), Box::new(top));
197 let err = overlay.get("not-in-top").unwrap_err();
198 assert_eq!(err.to_string(), "backend: base exploded");
199 }
200
201 #[test]
202 fn only_base_resolves_when_top_universe_empty() {
203 let base = NixpkgsBridge::new()
206 .with_names(vec!["hello".into()])
207 .with_label("base");
208 let top = NixpkgsBridge::new()
209 .with_names(vec!["nothing".into()])
210 .with_label("top");
211 let overlay = OverlayPackageSet::new(Box::new(base), Box::new(top));
212 let hello = overlay.get("hello").unwrap().unwrap();
213 assert_eq!(hello.name, "hello");
214 assert!(hello.bridge.unwrap().pkg_set.is_none());
216 }
217}