Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 6 additions & 4 deletions .github/workflows/build.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@ jobs:
run: |
pwd
ls -ahl
${{ matrix.test-binary }} --run-tests
${{ matrix.test-binary }} run-tests

- name: Cache JUCE example plugin binaries
id: cache-plugins
Expand All @@ -127,9 +127,9 @@ jobs:
- name: Validate JUCE Plugin examples (VST3)
shell: bash
run: |
# Paths must be single quoted for bash not to escape the Windows backslash character \ used in absolute paths
${{ env.APP_DIR }}/${{ matrix.test-binary }} --strictness-level 10 --validate '${{ env.PLUGIN_CACHE_PATH }}/DSPModulePluginDemo_artefacts/Release/VST3/DSPModulePluginDemo.vst3'
${{ env.APP_DIR }}/${{ matrix.test-binary }} --strictness-level 10 --validate '${{ env.PLUGIN_CACHE_PATH }}/MultiOutSynthPlugin_artefacts/Release/VST3/MultiOutSynthPlugin.vst3'
# Paths must be single quoted for bash not to escape the Windows backslash character \ used in absolute paths
${{ env.APP_DIR }}/${{ matrix.test-binary }} validate --strictness-level 10 '${{ env.PLUGIN_CACHE_PATH }}/DSPModulePluginDemo_artefacts/Release/VST3/DSPModulePluginDemo.vst3'
${{ env.APP_DIR }}/${{ matrix.test-binary }} validate --strictness-level 10 '${{ env.PLUGIN_CACHE_PATH }}/MultiOutSynthPlugin_artefacts/Release/VST3/MultiOutSynthPlugin.vst3'

- name: Validate JUCE Plugin examples (AU)
shell: bash
Expand All @@ -141,6 +141,8 @@ jobs:
mkdir -p ~/Library/Audio/Plug-Ins/Components/
cp -R ${{ env.PLUGIN_CACHE_PATH }}/DSPModulePluginDemo_artefacts/Release/AU/DSPModulePluginDemo.component ~/Library/Audio/Plug-Ins/Components/
killall -9 AudioComponentRegistrar # kick the AU registrar
# Intentionally uses the deprecated "--validate" flat flag to keep the
# backwards-compatible alias covered (this step is continue-on-error).
${{ env.APP_DIR }}/${{ matrix.test-binary }} --strictness-level 10 --validate ~/Library/Audio/Plug-Ins/Components/DSPModulePluginDemo.component

- name: Codesign (macOS)
Expand Down
2 changes: 2 additions & 0 deletions CHANGELIST.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
# pluginval Change List

### 2.0.0
- Restructured the command line into subcommands: `pluginval validate [options] <plugin>` (the default), `pluginval run-tests` and `pluginval strictness-help [level]`. The plugin path is now a positional argument of `validate`
- **Deprecated:** the flat flags `--validate <plugin>`, `--run-tests` and `--strictness-help [level]` still work as aliases (with a one-line notice) but will be removed in a future version. `pluginval <plugin>` remains a silent shorthand for `validate`
- Replaced the hand-rolled command-line parser with CLI11 and a single JSON-based settings pipeline
- Added `--config <file.json>` to load settings from JSON (repeatable; later files win per key)
- Settings precedence is now (lowest to highest): defaults, environment variables, `--config`, command-line options
Expand Down
46 changes: 42 additions & 4 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -193,6 +193,38 @@ VST2_SDK_DIR=/path/to/vst2sdk cmake -B Builds/Debug .

### CLI Settings Pipeline

#### Subcommand dispatch layer

The command line is structured into subcommands, peeled off by a thin verb
dispatcher in front of the settings pipeline (it does **not** use CLI11-native
subcommands, so the env/config/CLI layering below is untouched):

- `pluginval validate [options] <plugin>` — the default; `<plugin>` is a
positional argument. `pluginval <plugin>` and `pluginval [options] <plugin>`
(no verb) also resolve to validate.
- `pluginval run-tests` — runs the internal unit tests.
- `pluginval strictness-help [level]` — lists tests at a strictness level.

`settings_parser::dispatch()` (in `SettingsParser.cpp`) takes the `tokenise()`d
command line and returns a `DispatchResult { command, validateTokens,
deprecatedAlias, strictnessLevel }`. For `validate` it strips the verb and hands
`validateTokens` to `parseTokens` unchanged. `CommandLine.cpp`'s
`performCommandLine()` switches on `command` and emits a one-line stderr notice
when `deprecatedAlias` is set.

The old flat flags (`--validate <plugin>`, `--run-tests`, `--strictness-help`)
are kept as **deprecated aliases** that route to the same commands with
`deprecatedAlias = true` (the bare-path shorthand and the internal child handoff
stay silent). `preprocess()` is now `tokenise()` + `insertImplicitValidate()`.
The child process is launched with the explicit verb:
`validate --config-base64 <b64> <path>`.

Note: `--config` is parsed manually (it is stripped from the tokens fed to the
CLI11 pass) so its greedy CLI11 vector parsing can't swallow the positional
plugin path; it stays registered only so it appears in `--help`.

#### Settings layering

Command-line parsing centres on one plain settings struct (`PluginvalSettings`)
that CLI11 binds to directly. A single instance is filled by successive layers,
**lowest to highest precedence: defaults → environment → `--config` → CLI**
Expand Down Expand Up @@ -360,11 +392,17 @@ ut.logVerboseMessage("Detail message"); // Only with --verbose flag

Basic usage:
```bash
./pluginval --strictness-level 5 /path/to/plugin.vst3
./pluginval validate --strictness-level 5 /path/to/plugin.vst3
```

Key options:
- `--validate [path]` - Validate plugin at path
Commands:
- `validate [options] <plugin>` - Validate the plugin at the given path/AU id (the default; `./pluginval <plugin>` also works)
- `run-tests` - Run the internal unit tests
- `strictness-help [level]` - List the tests that run at a strictness level

The flat flags `--validate <plugin>`, `--run-tests` and `--strictness-help [level]` are deprecated aliases.

Key options (for `validate`):
- `--config [file.json]` - Load a full settings set from JSON (overridden by env vars and CLI options)
- `--strictness-level [1-10]` - Test thoroughness (default: 5)
- `--skip-gui-tests` - Skip GUI tests (for headless CI)
Expand Down Expand Up @@ -434,7 +472,7 @@ Debug unit tests run automatically in debug builds:

Run internal tests via CLI:
```bash
./pluginval --run-tests
./pluginval run-tests
```

## Release Process
Expand Down
3 changes: 2 additions & 1 deletion CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -250,8 +250,9 @@ else()
# TODO: This doesn't populate the executable in clion
add_custom_target(${CMAKE_PROJECT_NAME}_pluginval_cli
COMMAND $<TARGET_FILE:pluginval>
--validate ${artefact}
validate
--strictness-level 10
${artefact}
DEPENDS pluginval ${PLUGINVAL_TARGET}
COMMENT "Run pluginval CLI with strict validation")
endif()
33 changes: 21 additions & 12 deletions Source/CommandLine.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -203,38 +203,47 @@ juce::StringArray createCommandLine (juce::String fileOrID, PluginTests::Options
}

//==============================================================================
static void warnDeprecated (const juce::String& oldForm, const juce::String& newForm)
{
std::cerr << "!!! WARNING: " << oldForm << " is deprecated; use '" << newForm
<< "' instead. It will be removed in a future version." << std::endl;
}

void performCommandLine (CommandLineValidator& validator, const juce::String& commandLine)
{
hideDockIcon();

auto& app = *juce::JUCEApplication::getInstance();
const auto tokens = settings_parser::preprocess (commandLine);
const auto routed = settings_parser::dispatch (settings_parser::tokenise (commandLine));

if (tokens.contains ("--run-tests"))
if (routed.command == settings_parser::Command::runTests)
{
if (routed.deprecatedAlias)
warnDeprecated ("--run-tests", "pluginval run-tests");

runUnitTests();
app.quit();
return;
}

if (tokens.contains ("--strictness-help"))
if (routed.command == settings_parser::Command::strictnessHelp)
{
int level = 5;

if (const auto idx = tokens.indexOf ("--strictness-help"); idx >= 0 && idx + 1 < tokens.size())
if (const auto next = tokens[idx + 1]; ! next.startsWith ("-"))
level = next.getIntValue();
if (routed.deprecatedAlias)
warnDeprecated ("--strictness-help", "pluginval strictness-help");

printStrictnessHelp (level);
printStrictnessHelp (routed.strictnessLevel);
app.quit();
return;
}

// Otherwise this is a validation run (explicit or implicit --validate).
// CLI11 handles --help/--version and parse errors.
// Otherwise this is a validation run (positional plugin, or explicit/implicit
// --validate). CLI11 handles --help/--version and parse errors.
if (routed.deprecatedAlias)
warnDeprecated ("--validate", "pluginval validate <plugin>");

try
{
const auto result = settings_parser::parseTokens (tokens);
const auto result = settings_parser::parseTokens (routed.validateTokens);

if (result.handled)
{
Expand Down
106 changes: 106 additions & 0 deletions Source/CommandLineTests.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -331,6 +331,112 @@ struct CommandLineTests : public juce::UnitTest
expectEquals (r.exitCode, 1);
}
}

beginTest ("Subcommand: validate with a positional plugin path");
{
const auto currentDir = juce::File::getCurrentWorkingDirectory();

// Plugin path as a positional after the verb.
expectEquals (juce::String (parse ("validate MyPlugin.vst3").validatePath),
currentDir.getChildFile ("MyPlugin.vst3").getFullPathName());

// Options before the positional plugin path.
const auto s = parse ("validate --strictness-level 8 MyPlugin.vst3");
expectEquals (s.strictnessLevel, 8);
expectEquals (juce::String (s.validatePath), currentDir.getChildFile ("MyPlugin.vst3").getFullPathName());

// A bare AU component id positional is left untouched.
expectEquals (juce::String (parse ("validate MyPluginID").validatePath), juce::String ("MyPluginID"));
}

beginTest ("Subcommand: validate with --config before the positional plugin");
{
juce::TemporaryFile configFile (".json");
configFile.getFile().replaceWithText (R"({ "strictnessLevel": 2 })");

const auto currentDir = juce::File::getCurrentWorkingDirectory();
const auto cmd = "validate --config " + configFile.getFile().getFullPathName().quoted() + " MyPlugin.vst3";

const auto s = parse (cmd);
expectEquals (s.strictnessLevel, 2); // --config didn't swallow the plugin path
expectEquals (juce::String (s.validatePath), currentDir.getChildFile ("MyPlugin.vst3").getFullPathName());
}

beginTest ("Subcommand dispatch routing (new verbs do not warn)");
{
using settings_parser::Command;

{
const auto d = settings_parser::dispatch (settings_parser::tokenise ("run-tests"));
expect (d.command == Command::runTests);
expect (! d.deprecatedAlias);
}
{
const auto d = settings_parser::dispatch (settings_parser::tokenise ("strictness-help 7"));
expect (d.command == Command::strictnessHelp);
expectEquals (d.strictnessLevel, 7);
expect (! d.deprecatedAlias);
}
{
const auto d = settings_parser::dispatch (settings_parser::tokenise ("strictness-help"));
expect (d.command == Command::strictnessHelp);
expectEquals (d.strictnessLevel, 5); // default level
}
{
const auto d = settings_parser::dispatch (settings_parser::tokenise ("validate MyPlugin.vst3"));
expect (d.command == Command::validate);
expect (! d.deprecatedAlias);
}
}

beginTest ("Deprecated flat flags still route, flagged as deprecated");
{
using settings_parser::Command;

{
const auto d = settings_parser::dispatch (settings_parser::tokenise ("--run-tests"));
expect (d.command == Command::runTests);
expect (d.deprecatedAlias);
}
{
const auto d = settings_parser::dispatch (settings_parser::tokenise ("--strictness-help 9"));
expect (d.command == Command::strictnessHelp);
expectEquals (d.strictnessLevel, 9);
expect (d.deprecatedAlias);
}
{
const auto d = settings_parser::dispatch (settings_parser::tokenise ("--validate x"));
expect (d.command == Command::validate);
expect (d.deprecatedAlias);
}
}

beginTest ("Bare-path shorthand and child handoff do not warn");
{
juce::TemporaryFile temp ("path_to_file.vst3");
expect (temp.getFile().create());

// Bare plugin path -> validate, no deprecation warning.
const auto bare = settings_parser::dispatch (settings_parser::tokenise (temp.getFile().getFullPathName()));
expect (bare.command == settings_parser::Command::validate);
expect (! bare.deprecatedAlias);

// The internal child handoff uses the explicit verb -> no warning.
PluginTests::Options opts;
juce::StringArray childArgs (createCommandLine ("/some/MyPlugin.vst3", opts));
childArgs.remove (0); // drop the executable path
const auto child = settings_parser::dispatch (childArgs);
expect (child.command == settings_parser::Command::validate);
expect (! child.deprecatedAlias);
}

beginTest ("Should perform command line recognises subcommands");
{
expect (shouldPerformCommandLine ("run-tests"));
expect (shouldPerformCommandLine ("strictness-help"));
expect (shouldPerformCommandLine ("validate MyPlugin.vst3"));
expect (shouldPerformCommandLine ("validate MyPluginID"));
}
}
};

Expand Down
Loading
Loading