Pass environment-derived values into the resolvers their tests steer (#806) - #810
Conversation
In Rust 2024 `set_var`/`remove_var` are unsafe, and the shared mutex these tests took before each mutation does not discharge that contract: it serialises only the code that takes the lock, while the tokio runtime, the HTTP client, the env-filtered subscriber and the libc resolver all read the environment from threads that hold nothing. Every resolver a test steered now takes the value as a parameter, with one thin wrapper left at the composition root doing the read. The precedence, the fallbacks and the variables consulted are unchanged; what moves is where they are read. The `.env` load keeps its single production `set_var`, with the decision of which pairs to apply split off into a pure function the tests can drive. That removes the last user of `compose_project::test_env`, of `openbao_auth`'s `ENV_LOCK`, of `init::steps::test_support`'s lock and of the three anonymous `OnceLock<Mutex<()>>` guards, all of which are deleted — clippy fails on a dead static, so they cannot be left for a later tidy-up. `reinit`'s scope-check test no longer moves the whole process into a tempdir either: the working directory is process-global exactly as the environment is, and the `.` it was reproducing is a property of `compose_file_dir`, not of where the process happens to stand. Closes #806
`resolver_reads_the_dotenv_beside_the_given_compose_file` went through `ComposeIdentity::resolve`, which is the composition root that reads `COMPOSE_PROJECT_NAME`. Dropping its `ScopedEnv` left the assertion steered by whatever the invoking shell exported: with the variable set the resolver returns the override and the test fails, which is exactly the ambient dependence the conversion was meant to remove — the E2E harness exports that variable for a whole scenario. `resolve` gets the same override-taking sibling `resolve_for_dir` already has, and the test passes `None` in. Part of #806
`resolve_db_admin_dsn` no longer takes a compose directory, so the ca.json the no-source test wrote could not be reached by any code path the test exercises. The file proved nothing and its comment claimed the opposite; what the signature now rules out is stated instead. Also rewords the `dotenv_pairs_to_apply` summary, which read as a typo. Part of #806
Mutating the production side showed three of the new parameters were threaded but never steered anything: `status_openbao_url_with_env`, `init_args_for_reinit`'s `openbao_host_port_env`, and every field of `HostPortEnv` bar `postgres`. Each could be replaced with `None` and the whole suite still passed, which is the failure mode this work is supposed to avoid — a resolver that no longer reads the environment and no test that proves the caller supplies it instead. Each new assertion now fails when the value does not reach its resolver. Part of #806
|
[Reviewer Round 1] No blocking findings. The change follows the issue’s intended shape: the value-steered paths now have parameterized forms ( I also confirmed the scoped test environment mutations and the associated lock/RAII helpers are gone, |
|
[Review Verdict Round 1: APPROVED] |
Suggested squash commitTitle Body |
Summary
Test code in this crate mutated the process environment to steer production functions that read it, serialised behind shared mutexes. In Rust 2024
set_var/remove_varareunsafe, and that lock does not discharge the contract: it serialises only the code that takes the lock, while the tokio runtime, the HTTP client, the env-filtered subscriber and the libc resolver all read the environment from threads that hold nothing.Every resolver a test steered now takes the value as a parameter, leaving one thin wrapper at the composition root that reads the environment. The variables consulted, their precedence and their fallbacks are unchanged; what moves is where they are read.
POSTGRES_*—resolve_db_dsn_for_initandresolve_db_dsnreceive the six values;init::steps::orchestratorreads them straight afterload_dotenv_into_envpromotes the compose.env, preserving the precedence the builders rely on.COMPOSE_PROJECT_NAME—compose_project_name_overridetakes the override, and bothComposeIdentity::resolveandComposeIdentity::resolve_for_dirgain an override-taking sibling, so the compose-file resolver test is no longer steered by whatever the invoking shell exported (the E2E harness exports that variable for a whole scenario).OPENBAO_ROOT_TOKEN—resolve_root_tokentakes the token.*_HOST_PORT—host_port::resolve_host_port,db::resolve_postgres_host_port, the OpenBao URL resolver androtate::db::resolve_db_admin_dsntake the port.resolve_db_admin_dsntookcompose_dironly to derive that port, so the port replaces it; theca.jsonfixture that existed to prove no fallthrough went with the directory, since the signature is now what rules it out.load_dotenv_into_env— split into a puredotenv_pairs_to_apply, which decides which pairs apply (including theCOMPOSE_PROJECT_NAMEexclusion), and the thin wrapper that keeps the single productionset_varwith its// SAFETY:comment unchanged.Deleted as their last users went:
compose_project::test_env(ENV_LOCK,env_lock,ScopedEnv,ComposeProjectEnv),openbao_auth'sENV_LOCK/env_lock,init::steps::test_support'sENV_LOCK/env_lock, and the three anonymousOnceLock<Mutex<()>>guards inhost_port.rs,openbao_url.rsanddb.rs. The rest ofinit::steps::test_support(default_init_args,test_messages,test_cert_pem) and all ofrotate::test_supportandverify.rs'sENV_LOCKare untouched — they belong to other issues in this effort.reinit's scope-check test no longer callsset_current_dir. The working directory is process-global exactly as the environment is, and the.it was reproducing is a property ofcompose_file_dir, so the test passes the directory in.Three parameters ended up threaded but steering nothing —
status_openbao_url_with_env,init_args_for_reinit'sopenbao_host_port_env, and every field ofHostPortEnvbarpostgrescould each be replaced withNonewith the whole suite still green. Each now has an assertion that fails when the value does not reach its resolver.No new
unsafeblock, no replacement lock, no--test-threads=1.Closes #806
Test plan
cargo test --bin bootrootpasses (980 tests) — the one that actually runs thecommands::*testscargo test --libpasses (400 tests)grep -rn "set_var\|remove_var"over the ten scope-table files returns nothingsrc/commands/dotenv.rsreturns only the production callgrep -rn "env_lock\|ENV_LOCK\|ScopedEnv\|ComposeProjectEnv\|set_current_dir"over all eleven files returns nothinggrep -rn "env_lock" src/commands/initreturns only imports and call sites resolving torotate::test_supportrotate::test_supportandverify.rs'sENV_LOCKstill present and compilinginit::steps::test_supportstill exportsdefault_init_args,test_messagesandtest_cert_pemto its six importerscargo clippy --all-targets -- -D warningscleancargo fmt -- --check --config group_imports=StdExternalCratecleanrun-extendedfrome2e-extended.ymldispatched at the head commit; still running