Skip to content

Commit a3c9069

Browse files
drowaudioclaude
andcommitted
Revise precedence to defaults < env < config < CLI
Reworks the layering so a --config file (deliberate, per-invocation) ranks above ambient environment variables, and makes --config repeatable. - Environment layer: env-var names are now derived from the registered CLI11 options (--strictness-level -> STRICTNESS_LEVEL) instead of CLI11's ->envname(), so there is no hand-maintained env table and env support is automatic for every option. A synthetic "--name=value" argv is built from the environment and parsed by CLI11, reusing all its coercion (comma lists, enum transformer, hex-seed callback). Flags accept --flag=1/0. - --config: repeatable; each JSON file is merge_patch-ed in command-line order (later files win per key). Applied between the env and CLI layers, so it beats env and loses to explicit CLI options. (Inline JSON dropped for now to avoid command-line re-tokenisation hazards; file paths only.) - The env layer reads through an injectable EnvProvider again, so the unit tests no longer mutate the real process environment (also removes the Windows _putenv_s fragility). - Tests updated for the new precedence + a repeatable-config test. - Help footer, docs and CLAUDE.md updated. Verified end-to-end: env(6) -> 6; env(6)+config(2) -> 2; +CLI(9) -> 9. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 54dd719 commit a3c9069

5 files changed

Lines changed: 300 additions & 175 deletions

File tree

CLAUDE.md

Lines changed: 20 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -194,24 +194,32 @@ VST2_SDK_DIR=/path/to/vst2sdk cmake -B Builds/Debug .
194194
### CLI Settings Pipeline
195195

196196
Command-line parsing centres on one plain settings struct (`PluginvalSettings`)
197-
that CLI11 binds to directly. The flow (in `SettingsParser`):
197+
that CLI11 binds to directly. A single instance is filled by successive layers,
198+
**lowest to highest precedence: defaults → environment → `--config` → CLI**
199+
(in `SettingsParser::parseTokens`):
198200

199201
1. **preprocess** the raw command line — rewrite the deprecated `strictnessLevel`,
200202
strip the macOS `-NSDocumentRevisionsDebugMode YES` flag, and insert an
201203
implicit `--validate` when the last argument is a bare plugin path.
202-
2. If `--config <file.json>` is present, **seed** the struct from that JSON first.
203-
3. **CLI11** binds each `add_option`/`add_flag` straight to a `PluginvalSettings`
204-
member, with the environment variable on the same line via `->envname()`.
205-
Because CLI11 only overwrites a member when its flag/env was actually provided,
206-
precedence is **defaults < config < env < CLI** (CLI wins) with no manual
207-
layering. Comma lists use `->delimiter(',')`, the enum uses a
208-
`CheckedTransformer`, and the hex/int seed is a small callback.
209-
4. `PluginvalSettings::toPluginTestOptions()` converts to the JUCE-flavoured
210-
`PluginTests::Options` at the boundary.
204+
2. **Environment layer.** Env-var names are *derived* from the registered
205+
options (`--strictness-level``STRICTNESS_LEVEL`), so there is no separate
206+
env table. A synthetic `--name=value` argv is built from the environment and
207+
parsed by CLI11, reusing all its coercion.
208+
3. **`--config` layer.** Repeatable; each JSON file is `merge_patch`-ed in
209+
command-line order (later files win per key). Beats the environment.
210+
4. **CLI layer.** The real arguments are parsed last and beat everything;
211+
CLI11 only overwrites a member when its option was actually provided.
212+
213+
`configureApp()` registers every option (bound to the struct) and is used for
214+
both the env pass and the CLI pass. Comma lists use `->delimiter(',')`, the enum
215+
uses a `CheckedTransformer`, and the hex/int seed is a small callback.
216+
`PluginvalSettings::toPluginTestOptions()` converts to the JUCE-flavoured
217+
`PluginTests::Options` at the boundary.
211218

212219
Adding a new option is three edits: a struct member, an entry in the nlohmann
213-
macro list, and one `add_option(...)` line. `SettingsSerializer` handles JSON
214-
load/save plus the two remaining conversions (hex seed, disabled-tests file).
220+
macro list, and one `add_option(...)` line — its environment variable then works
221+
automatically. `SettingsSerializer` handles JSON load/save plus the two
222+
remaining conversions (hex seed, disabled-tests file).
215223

216224
The child validation process receives a fully-resolved, **authoritative**
217225
settings set via a base64-encoded JSON argument (`--config-base64`), avoiding

Source/CommandLineTests.cpp

Lines changed: 63 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
1313
==============================================================================*/
1414

15-
#include <cstdlib>
15+
#include <map>
1616

1717
struct CommandLineTests : public juce::UnitTest
1818
{
@@ -21,46 +21,32 @@ struct CommandLineTests : public juce::UnitTest
2121
{
2222
}
2323

24-
static constexpr const char* knownEnvVars[] = {
25-
"STRICTNESS_LEVEL", "RANDOM_SEED", "TIMEOUT_MS", "VERBOSE", "REPEAT",
26-
"RANDOMISE", "SKIP_GUI_TESTS", "DATA_FILE", "OUTPUT_DIR", "OUTPUT_FILENAME",
27-
"SAMPLE_RATES", "BLOCK_SIZES", "RTCHECK"
28-
};
29-
30-
static void setEnv (const char* name, const char* value)
24+
static settings_parser::EnvProvider emptyEnv()
3125
{
32-
#if JUCE_WINDOWS
33-
_putenv_s (name, value);
34-
#else
35-
::setenv (name, value, 1);
36-
#endif
26+
return [] (const juce::String&) { return juce::String(); };
3727
}
3828

39-
static void unsetEnv (const char* name)
29+
static settings_parser::EnvProvider envFrom (std::map<juce::String, juce::String> vars)
4030
{
41-
#if JUCE_WINDOWS
42-
_putenv_s (name, "");
43-
#else
44-
::unsetenv (name);
45-
#endif
31+
return [vars = std::move (vars)] (const juce::String& name)
32+
{
33+
const auto it = vars.find (name);
34+
return it != vars.end() ? it->second : juce::String();
35+
};
4636
}
4737

48-
static void clearKnownEnv()
38+
static PluginvalSettings parse (const juce::String& cmd, settings_parser::EnvProvider env)
4939
{
50-
for (auto* n : knownEnvVars)
51-
unsetEnv (n);
40+
return settings_parser::parse (cmd, env);
5241
}
5342

5443
static PluginvalSettings parse (const juce::String& cmd)
5544
{
56-
return settings_parser::parse (cmd);
45+
return settings_parser::parse (cmd, emptyEnv());
5746
}
5847

5948
void runTest() override
6049
{
61-
// Start from a clean environment so host env vars don't affect the deterministic tests.
62-
clearKnownEnv();
63-
6450
beginTest ("Command line defaults");
6551
{
6652
const auto opts = parse ("").toPluginTestOptions();
@@ -203,79 +189,93 @@ struct CommandLineTests : public juce::UnitTest
203189

204190
beginTest ("Environment variables");
205191
{
206-
setEnv ("STRICTNESS_LEVEL", "7");
207-
setEnv ("RANDOM_SEED", "1234");
208-
setEnv ("TIMEOUT_MS", "20000");
209-
setEnv ("VERBOSE", "1");
210-
setEnv ("REPEAT", "11");
211-
setEnv ("RANDOMISE", "1");
212-
setEnv ("SKIP_GUI_TESTS", "1");
213-
setEnv ("DATA_FILE", "/path/to/file");
214-
setEnv ("OUTPUT_DIR", "/path/to/dir");
215-
setEnv ("SAMPLE_RATES", "22050,44100");
216-
setEnv ("BLOCK_SIZES", "32,64");
217-
setEnv ("RTCHECK", "relaxed");
218-
219-
const auto opts = parse ("--validate x").toPluginTestOptions();
220-
221-
clearKnownEnv();
222-
192+
const auto env = envFrom ({
193+
{ "STRICTNESS_LEVEL", "7" },
194+
{ "RANDOM_SEED", "1234" },
195+
{ "TIMEOUT_MS", "20000" },
196+
{ "VERBOSE", "1" },
197+
{ "REPEAT", "11" },
198+
{ "RANDOMISE", "1" },
199+
{ "SKIP_GUI_TESTS", "1" },
200+
{ "DATA_FILE", "/path/to/file" },
201+
{ "OUTPUT_DIR", "/path/to/dir" },
202+
{ "SAMPLE_RATES", "22050,44100" },
203+
{ "BLOCK_SIZES", "32,64" },
204+
{ "RTCHECK", "relaxed" },
205+
});
206+
207+
const auto settings = parse ("--validate x", env);
208+
const auto opts = settings.toPluginTestOptions();
223209
expectEquals (opts.strictnessLevel, 7);
224210
expectEquals (opts.randomSeed, (juce::int64) 1234);
225211
expectEquals (opts.timeoutMs, (juce::int64) 20000);
226212
expect (opts.verbose);
227213
expectEquals (opts.numRepeats, 11);
228214
expect (opts.randomiseTestOrder);
229215
expect (opts.withGUI == false);
216+
expectEquals (juce::String (settings.dataFile), juce::String ("/path/to/file"));
230217
expect (opts.sampleRates == std::vector<double> ({ 22050.0, 44100.0 }));
231218
expect (opts.blockSizes == std::vector<int> ({ 32, 64 }));
232219
expect (opts.realtimeCheck == RealtimeCheck::relaxed);
233220
}
234221

235222
beginTest ("Command line overrides environment variables");
236223
{
237-
setEnv ("STRICTNESS_LEVEL", "3");
238-
const auto envOnly = parse ("--validate x").strictnessLevel;
239-
const auto cliWins = parse ("--strictness-level 9 --validate x").strictnessLevel;
240-
clearKnownEnv();
241-
242-
expectEquals (envOnly, 3); // env only
243-
expectEquals (cliWins, 9); // CLI wins
224+
const auto env = envFrom ({ { "STRICTNESS_LEVEL", "3" } });
225+
expectEquals (parse ("--validate x", env).strictnessLevel, 3); // env only
226+
expectEquals (parse ("--strictness-level 9 --validate x", env).strictnessLevel, 9); // CLI wins
244227
}
245228

246-
beginTest ("Config file and precedence (CLI > env > config > defaults)");
229+
beginTest ("Precedence: CLI > --config > env > defaults");
247230
{
248231
juce::TemporaryFile configFile (".json");
249-
configFile.getFile().replaceWithText (R"({ "strictnessLevel": 2, "timeoutMs": 12345, "numRepeats": 4 })");
232+
configFile.getFile().replaceWithText (R"({ "strictnessLevel": 2, "timeoutMs": 12345 })");
250233
const auto cfg = "--config " + configFile.getFile().getFullPathName().quoted();
251234

235+
const auto env6 = envFrom ({ { "STRICTNESS_LEVEL", "6" } });
236+
237+
// env beats defaults
238+
expectEquals (parse ("--validate x", env6).strictnessLevel, 6);
239+
252240
// config alone
253241
{
254242
const auto s = parse (cfg + " --validate x");
255243
expectEquals (s.strictnessLevel, 2);
256244
expectEquals ((juce::int64) s.timeoutMs, (juce::int64) 12345);
257-
expectEquals (s.numRepeats, 4);
258245
}
259246

260-
// env overrides config
247+
// config beats env
261248
{
262-
setEnv ("STRICTNESS_LEVEL", "6");
263-
const auto s = parse (cfg + " --validate x");
264-
clearKnownEnv();
265-
expectEquals (s.strictnessLevel, 6); // env beats config
266-
expectEquals ((juce::int64) s.timeoutMs, (juce::int64) 12345); // still from config
249+
const auto s = parse (cfg + " --validate x", env6);
250+
expectEquals (s.strictnessLevel, 2); // config wins over env
251+
expectEquals ((juce::int64) s.timeoutMs, (juce::int64) 12345);
267252
}
268253

269-
// CLI overrides env and config
254+
// CLI beats config and env
270255
{
271-
setEnv ("STRICTNESS_LEVEL", "6");
272-
const auto s = parse (cfg + " --strictness-level 9 --validate x");
273-
clearKnownEnv();
256+
const auto s = parse (cfg + " --strictness-level 9 --validate x", env6);
274257
expectEquals (s.strictnessLevel, 9);
275258
expectEquals ((juce::int64) s.timeoutMs, (juce::int64) 12345);
276259
}
277260
}
278261

262+
beginTest ("Repeatable --config merges per key, last wins");
263+
{
264+
juce::TemporaryFile baseFile (".json");
265+
baseFile.getFile().replaceWithText (R"({ "strictnessLevel": 2, "timeoutMs": 11111 })");
266+
267+
juce::TemporaryFile overrideFile (".json");
268+
overrideFile.getFile().replaceWithText (R"({ "strictnessLevel": 8 })");
269+
270+
const auto cmd = "--config " + baseFile.getFile().getFullPathName().quoted()
271+
+ " --config " + overrideFile.getFile().getFullPathName().quoted()
272+
+ " --validate x";
273+
274+
const auto s = parse (cmd);
275+
expectEquals (s.strictnessLevel, 8); // overridden by the second file
276+
expectEquals ((juce::int64) s.timeoutMs, (juce::int64) 11111); // untouched, kept from the first
277+
}
278+
279279
beginTest ("Child-process command line round-trip");
280280
{
281281
PluginTests::Options opts;

0 commit comments

Comments
 (0)