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