Skip to main content

usage_config/
env.rs

1//! The environment as a layer.
2//!
3//! The one layer every CLI in the fleet has, and the one every CLI writes again: mise reads 33 of
4//! its settings through `parse_env` functions, hk's generator emits a match arm per variable, fnox
5//! has about 48 `FNOX_*` variables that live outside its registry entirely. All of them are doing
6//! the same thing — look up the names a setting declares, take the first one that is set — and the
7//! registry already knows those names.
8//!
9//! The environment is *injected* rather than read at the point of use, so a test does not have to
10//! touch the process to describe one, and two tests can describe different ones at the same time.
11//! `EnvLayer::from_process` is the one place `std::env` is read.
12
13use std::collections::BTreeMap;
14use std::ffi::OsStr;
15
16use crate::layer::{Layer, LayerCtx, LayerError, LayerOutput, Warning, WarningKind};
17use crate::registry::PropId;
18use crate::source::{Origin, SourceKind};
19
20/// Settings read from environment variables.
21pub struct EnvLayer {
22    /// Keyed by the comparable form of each name, holding the name as it was actually set and its
23    /// value. Both are kept because the comparison and the report want different things: one needs
24    /// the names to match, the other needs to print what the user typed.
25    vars: BTreeMap<String, (String, String)>,
26}
27
28impl EnvLayer {
29    /// The variables this process was started with.
30    ///
31    /// `std::env::vars` *panics* on a name or value that is not UTF-8, which would take the CLI down
32    /// before it resolved anything — over a variable that in all likelihood belongs to something
33    /// else entirely. Read as OS strings and skipped when they will not convert: a setting whose
34    /// variable holds bytes this cannot read has no value it could have been given anyway.
35    pub fn from_process() -> Self {
36        Self::new(std::env::vars_os().filter_map(|(name, value)| readable(&name, &value)))
37    }
38
39    /// A named environment, for a test or for a CLI that has its own idea of one.
40    pub fn new(vars: impl IntoIterator<Item = (String, String)>) -> Self {
41        Self {
42            vars: vars
43                .into_iter()
44                .map(|(name, value)| (normalize(&name), (name, value)))
45                .collect(),
46        }
47    }
48
49    /// What this layer would read for `name`, if anything.
50    pub fn get(&self, name: &str) -> Option<&str> {
51        self.vars
52            .get(&normalize(name))
53            .map(|(_, value)| value.as_str())
54    }
55
56    /// The name as it is really set, which is what a report should print.
57    fn set_as(&self, name: &str) -> Option<&str> {
58        self.vars
59            .get(&normalize(name))
60            .map(|(set_as, _)| set_as.as_str())
61    }
62}
63
64/// One variable, if it is text at all.
65///
66/// A name or value that is not UTF-8 is skipped. A setting whose variable holds bytes that cannot be
67/// read has no value it could have been given, and the alternative is what `std::env::vars` does:
68/// panic, taking the CLI down before it resolves anything, over a variable that in all likelihood
69/// belongs to something else entirely.
70fn readable(name: &OsStr, value: &OsStr) -> Option<(String, String)> {
71    Some((name.to_str()?.to_string(), value.to_str()?.to_string()))
72}
73
74/// A variable's name in the form this layer compares.
75///
76/// Windows environment variable names are case-insensitive — `std::env::var("PATH")` finds `Path` —
77/// so a lookup that is case-sensitive there would miss a variable the user has plainly set, and
78/// would do it only on Windows, which is the worst place for a difference like this to live.
79/// Everywhere else the name is the name.
80fn normalize(name: &str) -> String {
81    if cfg!(windows) {
82        name.to_uppercase()
83    } else {
84        name.to_string()
85    }
86}
87
88impl Layer for EnvLayer {
89    fn source(&self) -> SourceKind {
90        SourceKind::ENV
91    }
92
93    fn load(&self, ctx: &LayerCtx) -> Result<LayerOutput, LayerError> {
94        let registry = ctx.registry();
95        let mut out = LayerOutput::new();
96
97        // Everything the environment has to say, gathered by the setting it will end up in — which
98        // is what an old name shares with the name that replaced it.
99        let mut found: BTreeMap<PropId, Vec<(PropId, &str, &str, bool)>> = BTreeMap::new();
100        for id in registry.ids() {
101            let meta = registry.get(id);
102            // The *declared* order, because a setting's variables are listed highest first: mise's
103            // `MISE_JOBS` beside an older `MISE_JOB`. First one set wins and the rest are not looked
104            // at, which is what makes an alias an alias rather than a second setting.
105            for (name, deprecated) in meta
106                .envs
107                .iter()
108                .map(|name| (*name, false))
109                .chain(meta.deprecated_envs.iter().map(|name| (*name, true)))
110            {
111                let Some(raw) = self.get(name) else {
112                    continue;
113                };
114                // The name the *user* set, not the setting's canonical one — an explanation that said
115                // "set by the environment" would send them looking through all of them — and their
116                // spelling of it, which on Windows need not be the declared one.
117                let set_as = self.set_as(name).unwrap_or(name);
118                let target = meta
119                    .renamed_to
120                    .and_then(|to| registry.lookup(to))
121                    .map_or(id, |found| found.id);
122                found
123                    .entry(target)
124                    .or_default()
125                    .push((id, set_as, raw, deprecated));
126                break;
127            }
128        }
129
130        for (target, mut candidates) in found {
131            // One entry per setting, chosen here rather than left to the merge. Pushing every one of
132            // them and relying on the last writer only works for `replace`: a `union` list took the
133            // items from a deprecated variable *as well*, and an emptied one cleared the default
134            // before the current name was merged.
135            //
136            // Every current environment name wins over every deprecated alias, including across a
137            // renamed setting that folds into this target. Within the same class, the target's own
138            // name wins; among old setting names, registry order is stable and the layer reports
139            // what it did.
140            candidates.sort_by_key(|(id, _, _, deprecated_env)| (*deprecated_env, *id != target));
141            let mut read_by: Option<&str> = None;
142            for (id, set_as, raw, deprecated_env) in candidates {
143                let origin = Origin::new(SourceKind::ENV, set_as);
144                if let Some(first) = read_by {
145                    out.warn(
146                        Warning::at(
147                            format!(
148                                "{set_as} was not read: {first} also sets {}",
149                                registry.get(target).key
150                            ),
151                            origin,
152                        )
153                        .of(WarningKind::NotRead),
154                    );
155                    continue;
156                }
157                match ctx.entry(id, raw, origin) {
158                    // Only a value that *reads* speaks for its setting: a typo in one name would
159                    // otherwise discard a perfectly good value in another.
160                    Ok(entry) => {
161                        out.push(entry);
162                        read_by = Some(set_as);
163                        if deprecated_env {
164                            let target_meta = registry.get(target);
165                            out.warn(
166                                Warning::at(
167                                    format!(
168                                        "{set_as} is deprecated; use {} for {}",
169                                        target_meta
170                                            .envs
171                                            .first()
172                                            .copied()
173                                            .unwrap_or(target_meta.key),
174                                        target_meta.key
175                                    ),
176                                    Origin::new(SourceKind::ENV, set_as),
177                                )
178                                .of(WarningKind::Deprecated),
179                            );
180                        }
181                    }
182                    // And a value of the wrong type costs that variable and nothing else. Refusing to
183                    // start because one variable in a shell profile is a typo would be worse than the
184                    // typo.
185                    Err(warning) => out.warn(warning),
186                }
187            }
188        }
189        Ok(out)
190    }
191}
192
193#[cfg(test)]
194mod tests {
195    use super::*;
196    use crate::registry::{PropMeta, Registry, Scope};
197    use crate::resolve::{resolve, Layers};
198    use crate::ty::{Parser, Ty};
199    use crate::value::{Const, Value};
200
201    static PROPS: &[PropMeta] = &[
202        PropMeta {
203            default: Some(Const::Int(4)),
204            // Highest first, which is what makes the second one an alias.
205            envs: &["HK_JOBS", "HK_JOB"],
206            deprecated_envs: &["HK_JOBS_OLD"],
207            ..PropMeta::new("jobs", Ty::Uint)
208        },
209        PropMeta {
210            parse: Some(Parser::ListByComma),
211            envs: &["HK_EXCLUDE"],
212            ..PropMeta::new("exclude", Ty::List(&Ty::String))
213        },
214        PropMeta {
215            scope: Scope::Global,
216            envs: &["HK_TRUSTED"],
217            ..PropMeta::new("trusted", Ty::Bool)
218        },
219        // No variables at all: plenty of settings are file-only.
220        PropMeta::new("stash", Ty::String),
221        PropMeta {
222            envs: &["HK_CONCURRENCY"],
223            deprecated_envs: &["HK_CONCURRENCY_OLD"],
224            deprecated: Some("Use jobs instead."),
225            renamed_to: Some("jobs"),
226            ..PropMeta::new("concurrency", Ty::Uint)
227        },
228        // A `union` list and an old name for it: this is the pair that "last writer wins" could not
229        // settle, since a union takes from *every* contributor rather than the last.
230        PropMeta {
231            merge: crate::registry::Merge::Union,
232            parse: Some(Parser::ListByComma),
233            envs: &["HK_SKIP"],
234            ..PropMeta::new("skip", Ty::List(&Ty::String))
235        },
236        PropMeta {
237            merge: crate::registry::Merge::Union,
238            parse: Some(Parser::ListByComma),
239            envs: &["HK_SKIP_STEPS"],
240            deprecated: Some("Use skip instead."),
241            renamed_to: Some("skip"),
242            ..PropMeta::new("skip_steps", Ty::List(&Ty::String))
243        },
244        // A second old name for the same setting, which is what a registry looks like after two
245        // renames. Sorts after `concurrency`, so it is the one that used to win by accident.
246        PropMeta {
247            envs: &["HK_THREADS"],
248            deprecated: Some("Use jobs instead."),
249            renamed_to: Some("jobs"),
250            ..PropMeta::new("threads", Ty::Uint)
251        },
252    ];
253    const REGISTRY: Registry = Registry::new(PROPS);
254
255    fn env(vars: &[(&str, &str)]) -> EnvLayer {
256        EnvLayer::new(
257            vars.iter()
258                .map(|(k, v)| ((*k).to_string(), (*v).to_string())),
259        )
260    }
261
262    #[test]
263    fn a_setting_is_read_from_the_variables_it_declares() {
264        let layer = env(&[
265            ("HK_JOBS", "8"),
266            ("HK_EXCLUDE", "target,dist"),
267            ("PATH", "/bin"),
268        ]);
269        let resolved = resolve(REGISTRY, Layers::new().then(&layer)).expect("resolves");
270
271        assert_eq!(resolved.get_key("jobs"), Some(&Value::Int(8)));
272        assert_eq!(
273            resolved.get_key("exclude"),
274            Some(&Value::List(vec![
275                Value::from("target"),
276                Value::from("dist")
277            ]))
278        );
279        // A variable no setting declares is not this layer's business, and certainly not a warning:
280        // the environment of a running process has hundreds of them in it.
281        assert!(resolved.warnings.is_empty(), "{:?}", resolved.warnings);
282        // The name the user set is what an explanation names.
283        assert_eq!(
284            resolved.origin_key("jobs").map(|o| o.describe()),
285            Some("HK_JOBS")
286        );
287    }
288
289    #[test]
290    fn the_first_name_a_setting_declares_is_the_one_that_wins() {
291        // Both set, which happens while a rename is being lived through. The declared order is the
292        // answer, and the loser is not read at all — an alias is a second name for one setting, not
293        // a second setting.
294        let layer = env(&[("HK_JOB", "2"), ("HK_JOBS", "8")]);
295        let resolved = resolve(REGISTRY, Layers::new().then(&layer)).expect("resolves");
296        assert_eq!(resolved.get_key("jobs"), Some(&Value::Int(8)));
297        assert_eq!(
298            resolved.contributors_key("jobs").len(),
299            2,
300            "the default and one variable, not both variables: {:?}",
301            resolved.contributors_key("jobs")
302        );
303
304        // And with only the older one set, it is read.
305        let layer = env(&[("HK_JOB", "2")]);
306        let resolved = resolve(REGISTRY, Layers::new().then(&layer)).expect("resolves");
307        assert_eq!(resolved.get_key("jobs"), Some(&Value::Int(2)));
308        assert_eq!(
309            resolved.origin_key("jobs").map(|o| o.describe()),
310            Some("HK_JOB"),
311            "named as the user set it"
312        );
313    }
314
315    #[test]
316    fn a_deprecated_environment_alias_is_read_last_and_warned_about() {
317        let resolved =
318            resolve(REGISTRY, Layers::new().then(&env(&[("HK_JOBS_OLD", "3")]))).expect("resolves");
319        assert_eq!(resolved.get_key("jobs"), Some(&Value::Int(3)));
320        let warnings = crate::explain::warnings(&resolved);
321        assert_eq!(warnings.len(), 1, "{warnings:?}");
322        assert!(
323            warnings[0].contains("HK_JOBS_OLD is deprecated"),
324            "{warnings:?}"
325        );
326        assert!(warnings[0].contains("use HK_JOBS"), "{warnings:?}");
327
328        let resolved = resolve(
329            REGISTRY,
330            Layers::new().then(&env(&[("HK_JOBS", "8"), ("HK_JOBS_OLD", "3")])),
331        )
332        .expect("resolves");
333        assert_eq!(resolved.get_key("jobs"), Some(&Value::Int(8)));
334        assert!(resolved.warnings.is_empty(), "{:?}", resolved.warnings);
335    }
336
337    #[test]
338    fn a_renamed_settings_deprecated_environment_alias_names_the_replacement() {
339        let resolved = resolve(
340            REGISTRY,
341            Layers::new().then(&env(&[("HK_CONCURRENCY_OLD", "6")])),
342        )
343        .expect("resolves");
344        assert_eq!(resolved.get_key("jobs"), Some(&Value::Int(6)));
345        let warnings = crate::explain::warnings(&resolved);
346        assert!(
347            warnings.iter().any(|warning| warning
348                .starts_with("HK_CONCURRENCY_OLD is deprecated; use HK_JOBS for jobs")),
349            "{warnings:?}"
350        );
351        assert!(
352            !warnings
353                .iter()
354                .any(|warning| warning.contains("use HK_CONCURRENCY for concurrency")),
355            "the deprecated alias should direct the user to the folded replacement: {warnings:?}"
356        );
357    }
358
359    #[test]
360    fn a_current_renamed_variable_beats_a_deprecated_target_alias() {
361        let resolved = resolve(
362            REGISTRY,
363            Layers::new().then(&env(&[("HK_JOBS_OLD", "3"), ("HK_CONCURRENCY", "6")])),
364        )
365        .expect("resolves");
366        assert_eq!(resolved.get_key("jobs"), Some(&Value::Int(6)));
367        assert_eq!(
368            resolved.origin_key("jobs").map(|origin| origin.describe()),
369            Some("HK_CONCURRENCY")
370        );
371        let warnings = crate::explain::warnings(&resolved);
372        assert!(
373            warnings
374                .iter()
375                .any(|warning| warning.starts_with("HK_JOBS_OLD was not read: HK_CONCURRENCY")),
376            "{warnings:?}"
377        );
378        assert!(
379            !warnings
380                .iter()
381                .any(|warning| warning.contains("HK_JOBS_OLD is deprecated")),
382            "an unused deprecated alias should not produce an actionable warning: {warnings:?}"
383        );
384    }
385
386    #[test]
387    fn a_variable_that_is_not_set_leaves_the_default_alone() {
388        let layer = env(&[]);
389        let resolved = resolve(REGISTRY, Layers::new().then(&layer)).expect("resolves");
390        assert_eq!(resolved.get_key("jobs"), Some(&Value::Int(4)));
391        assert_eq!(resolved.get_key("exclude"), None);
392        assert!(resolved.warnings.is_empty());
393    }
394
395    #[test]
396    fn a_value_of_the_wrong_type_costs_its_own_variable_and_nothing_else() {
397        // A typo in a shell profile. Refusing to start would be worse than the typo, and every
398        // other setting is perfectly readable.
399        let layer = env(&[("HK_JOBS", "lots"), ("HK_EXCLUDE", "target")]);
400        let resolved = resolve(REGISTRY, Layers::new().then(&layer)).expect("resolves");
401        assert_eq!(
402            resolved.get_key("jobs"),
403            Some(&Value::Int(4)),
404            "the default"
405        );
406        assert_eq!(
407            resolved.get_key("exclude"),
408            Some(&Value::List(vec![Value::from("target")]))
409        );
410        let warnings = crate::explain::warnings(&resolved);
411        assert_eq!(warnings.len(), 1, "{warnings:?}");
412        assert!(warnings[0].contains("HK_JOBS"), "{warnings:?}");
413        assert!(warnings[0].contains("but has `lots`"), "{warnings:?}");
414    }
415
416    #[test]
417    fn an_environment_variable_may_set_a_global_scoped_setting() {
418        // The point of `scope="global"` is that a *checkout* cannot set it. The environment is the
419        // user's own, so it can.
420        let layer = env(&[("HK_TRUSTED", "yes")]);
421        let resolved = resolve(REGISTRY, Layers::new().then(&layer)).expect("resolves");
422        assert_eq!(resolved.get_key("trusted"), Some(&Value::Bool(true)));
423        assert!(resolved.warnings.is_empty(), "{:?}", resolved.warnings);
424    }
425
426    #[test]
427    fn an_old_name_in_the_environment_is_folded_and_reported() {
428        // A variable for a setting that has been renamed. The value still applies — an upgrade must
429        // not silently change what a machine's environment means — and something is said about it.
430        let layer = env(&[("HK_CONCURRENCY", "6")]);
431        let resolved = resolve(REGISTRY, Layers::new().then(&layer)).expect("resolves");
432        assert_eq!(resolved.get_key("jobs"), Some(&Value::Int(6)));
433        // Two things worth saying, and the merge says both: why not to use the old name, and where
434        // the value ended up. Both name the variable the user set rather than the setting.
435        let warnings = crate::explain::warnings(&resolved);
436        assert_eq!(warnings.len(), 2, "{warnings:?}");
437        assert!(
438            warnings[0].starts_with("concurrency is deprecated: Use jobs instead."),
439            "{warnings:?}"
440        );
441        assert!(
442            warnings[1].starts_with("concurrency was read as jobs"),
443            "{warnings:?}"
444        );
445        assert!(
446            warnings.iter().all(|w| w.contains("HK_CONCURRENCY")),
447            "{warnings:?}"
448        );
449    }
450
451    #[test]
452    fn a_setting_own_variable_beats_the_one_it_replaced() {
453        // Both set, which is exactly what living through a rename looks like. Pushed and left to the
454        // merge, which of them won came down to which key sorted later — and for a `union` setting
455        // both applied. The setting's own name is chosen here, and the old one is reported.
456        let layer = env(&[("HK_CONCURRENCY", "6"), ("HK_JOBS", "8")]);
457        let resolved = resolve(REGISTRY, Layers::new().then(&layer)).expect("resolves");
458        assert_eq!(resolved.get_key("jobs"), Some(&Value::Int(8)));
459        assert_eq!(
460            resolved.origin_key("jobs").map(|o| o.describe()),
461            Some("HK_JOBS"),
462            "the name that is not deprecated"
463        );
464        // One warning, and it is the one to act on: the old variable does nothing at all. Its
465        // deprecation notice is about a value that was *used*, and none was — if the user removes
466        // `HK_JOBS` believing the old name still works, the notice arrives then, which is when it
467        // says something they need.
468        let warnings = crate::explain::warnings(&resolved);
469        assert_eq!(
470            warnings,
471            vec!["HK_CONCURRENCY was not read: HK_JOBS also sets jobs (HK_CONCURRENCY)"]
472        );
473    }
474
475    #[test]
476    fn one_of_two_old_names_is_chosen_and_the_other_is_reported() {
477        // Two deprecated names for one setting, both set, and the current name not set at all.
478        // Nothing declares which old name should win, so the answer is the first in registry order —
479        // and taking the last one silently made it a matter of which key sorted later.
480        let layer = env(&[("HK_CONCURRENCY", "6"), ("HK_THREADS", "9")]);
481        let resolved = resolve(REGISTRY, Layers::new().then(&layer)).expect("resolves");
482        assert_eq!(resolved.get_key("jobs"), Some(&Value::Int(6)));
483        let warnings = crate::explain::warnings(&resolved);
484        assert!(
485            warnings
486                .iter()
487                .any(|w| w.starts_with("HK_THREADS was not read: HK_CONCURRENCY also sets jobs")),
488            "{warnings:?}"
489        );
490        // Three kinds at once, which is what living through a rename actually produces: the name
491        // that was passed over, and — for the one that *was* read — that it is deprecated and what
492        // it was read as. A variable passed over is its own sort of thing: nothing is wrong with the
493        // value, and a `--strict` mode that stops for a bad one should not stop for this.
494        assert_eq!(
495            resolved.warnings.iter().map(|w| w.kind).collect::<Vec<_>>(),
496            vec![
497                WarningKind::NotRead,
498                WarningKind::Deprecated,
499                WarningKind::Renamed
500            ]
501        );
502
503        // And the current name still beats both of them.
504        let layer = env(&[
505            ("HK_CONCURRENCY", "6"),
506            ("HK_THREADS", "9"),
507            ("HK_JOBS", "8"),
508        ]);
509        let resolved = resolve(REGISTRY, Layers::new().then(&layer)).expect("resolves");
510        assert_eq!(resolved.get_key("jobs"), Some(&Value::Int(8)));
511    }
512
513    #[test]
514    fn an_old_name_that_does_not_read_does_not_speak_for_the_setting() {
515        // The rule this layer already had — a bad value costs its own variable and nothing else —
516        // applied to the rule this layer just gained. Recorded on presence rather than on reading,
517        // a typo in the first old name discarded a good value in the second, and the warning said
518        // the failed variable had set the setting.
519        let layer = env(&[("HK_CONCURRENCY", "lots"), ("HK_THREADS", "9")]);
520        let resolved = resolve(REGISTRY, Layers::new().then(&layer)).expect("resolves");
521        assert_eq!(resolved.get_key("jobs"), Some(&Value::Int(9)));
522
523        let warnings = crate::explain::warnings(&resolved);
524        assert!(
525            warnings
526                .iter()
527                .any(|w| w.contains("HK_CONCURRENCY") && w.contains("but has `lots`")),
528            "{warnings:?}"
529        );
530        assert!(
531            !warnings.iter().any(|w| w.contains("was not read")),
532            "nothing was passed over: {warnings:?}"
533        );
534    }
535
536    #[test]
537    fn an_old_name_does_not_contribute_to_a_union_beside_the_new_one() {
538        // The case last-writer-wins could not settle. A `union` takes from every contributor, so a
539        // deprecated variable's items ended up in the list *as well* as the current one's — and an
540        // emptied old variable cleared the declared default on its way past.
541        let layer = env(&[("HK_SKIP_STEPS", "lint"), ("HK_SKIP", "test")]);
542        let resolved = resolve(REGISTRY, Layers::new().then(&layer)).expect("resolves");
543        assert_eq!(
544            resolved.get_key("skip"),
545            Some(&Value::List(vec![Value::from("test")])),
546            "only the name that is not deprecated"
547        );
548        let warnings = crate::explain::warnings(&resolved);
549        assert_eq!(
550            warnings,
551            vec!["HK_SKIP_STEPS was not read: HK_SKIP also sets skip (HK_SKIP_STEPS)"]
552        );
553
554        // With only the old name set it is read, because then it is the only thing that can be.
555        let layer = env(&[("HK_SKIP_STEPS", "lint")]);
556        let resolved = resolve(REGISTRY, Layers::new().then(&layer)).expect("resolves");
557        assert_eq!(
558            resolved.get_key("skip"),
559            Some(&Value::List(vec![Value::from("lint")]))
560        );
561    }
562
563    #[test]
564    fn a_variable_that_is_not_text_is_skipped_rather_than_fatal() {
565        // `std::env::vars` panics on a name or value that is not UTF-8, which would take a CLI down
566        // before it resolved anything — over a variable that probably belongs to something else.
567        // Nothing here can construct one portably, so this is the property that matters: reading the
568        // process environment does not panic, whatever is in it.
569        let layer = EnvLayer::from_process();
570        let resolved = resolve(REGISTRY, Layers::new().then(&layer)).expect("resolves");
571        // Whatever the machine's environment holds, a setting nothing declares a variable for is
572        // untouched by it.
573        assert_eq!(resolved.get_key("stash"), None);
574    }
575
576    #[cfg(unix)]
577    #[test]
578    fn bytes_that_are_not_text_are_not_a_variable() {
579        use std::os::unix::ffi::OsStrExt;
580        let ok = readable(OsStr::new("HK_JOBS"), OsStr::new("8"));
581        assert_eq!(ok, Some(("HK_JOBS".to_string(), "8".to_string())));
582        // Either half being unreadable is enough to skip it, and neither is a reason to stop.
583        let bad_value = readable(OsStr::new("HK_JOBS"), OsStr::from_bytes(&[0xff, 0xfe]));
584        assert_eq!(bad_value, None);
585        let bad_name = readable(OsStr::from_bytes(&[0xff, 0xfe]), OsStr::new("8"));
586        assert_eq!(bad_name, None);
587    }
588
589    #[test]
590    fn the_environment_is_described_rather_than_reached_for() {
591        // Injection, which is what lets these tests exist at all: two of them describing different
592        // environments at once, and none of them touching the process.
593        let layer = env(&[("HK_JOBS", "8")]);
594        assert_eq!(layer.get("HK_JOBS"), Some("8"));
595        assert_eq!(layer.get("HK_NOTHING"), None);
596        // And the process is still readable, for the CLI that wants it.
597        let _ = EnvLayer::from_process();
598    }
599
600    #[cfg(windows)]
601    #[test]
602    fn a_name_that_windows_spells_differently_is_still_the_same_name() {
603        // `std::env::var("HK_JOBS")` finds `Hk_Jobs` on Windows, so a case-sensitive lookup would
604        // miss a variable the user has plainly set — and only there.
605        let layer = env(&[("Hk_Jobs", "8")]);
606        let resolved = resolve(REGISTRY, Layers::new().then(&layer)).expect("resolves");
607        assert_eq!(resolved.get_key("jobs"), Some(&Value::Int(8)));
608        // And reported as the user spelled it, not as the spec declares it: this is the only
609        // platform where those can differ, so it is the only place the difference can be asserted.
610        assert_eq!(
611            resolved.origin_key("jobs").map(|o| o.describe()),
612            Some("Hk_Jobs")
613        );
614    }
615}