Skip to content

Commit 278c11b

Browse files
committed
Acceptance testing: Initial commit
1 parent d2d5956 commit 278c11b

40 files changed

Lines changed: 2501 additions & 35 deletions

.github/workflows/acceptance.yml

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
name: Acceptance tests
2+
3+
# Builds the in-repo dogfood plugins and runs the acceptance (golden-file)
4+
# self-tests via CTest. This is independent of Tracktion's main private build
5+
# pipeline; it exists so the `pluginval test` feature has a public, reproducible
6+
# regression check across the three desktop platforms.
7+
8+
on:
9+
push:
10+
branches: [ master, develop, v2 ]
11+
pull_request:
12+
workflow_dispatch:
13+
14+
concurrency:
15+
group: acceptance-${{ github.ref }}
16+
cancel-in-progress: true
17+
18+
jobs:
19+
acceptance:
20+
name: ${{ matrix.os }}
21+
runs-on: ${{ matrix.os }}
22+
strategy:
23+
fail-fast: false
24+
matrix:
25+
os: [ ubuntu-latest, macos-latest, windows-latest ]
26+
27+
env:
28+
# Cache CPM's downloads (JUCE etc.) so reruns don't re-fetch them.
29+
CPM_SOURCE_CACHE: ${{ github.workspace }}/.cpm-cache
30+
31+
steps:
32+
- uses: actions/checkout@v4
33+
34+
- name: Cache CPM sources
35+
uses: actions/cache@v4
36+
with:
37+
path: ${{ github.workspace }}/.cpm-cache
38+
key: cpm-${{ matrix.os }}-${{ hashFiles('CMakeLists.txt', 'cmake/CPM.cmake') }}
39+
restore-keys: cpm-${{ matrix.os }}-
40+
41+
- name: Install Linux dependencies
42+
if: runner.os == 'Linux'
43+
run: |
44+
sudo apt-get update
45+
sudo apt-get install -y \
46+
libasound2-dev libx11-dev libxext-dev libxinerama-dev libxrandr-dev \
47+
libxcursor-dev libxcomposite-dev libfreetype6-dev libfontconfig1-dev \
48+
libgl1-mesa-dev libcurl4-openssl-dev ninja-build xvfb
49+
50+
# The acceptance tests host the dogfood plugins via JUCE's own VST3 hosting,
51+
# so the embedded VST3 validator (and its heavy SDK build) isn't needed here.
52+
- name: Configure
53+
run: >
54+
cmake -B build
55+
-DCMAKE_BUILD_TYPE=Release
56+
-DPLUGINVAL_BUILD_TEST_PLUGINS=ON
57+
-DPLUGINVAL_VST3_VALIDATOR=OFF
58+
59+
- name: Build
60+
run: cmake --build build --config Release
61+
62+
- name: Run acceptance tests (Linux)
63+
if: runner.os == 'Linux'
64+
working-directory: build
65+
run: xvfb-run --auto-servernum ctest -C Release --output-on-failure -R pluginval.acceptance
66+
67+
- name: Run acceptance tests (macOS / Windows)
68+
if: runner.os != 'Linux'
69+
working-directory: build
70+
run: ctest -C Release --output-on-failure -R pluginval.acceptance

CLAUDE.md

Lines changed: 53 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,7 @@ Replace `<run_id>` with the ID from step 2.
7575

7676
```
7777
pluginval/
78-
├── Source/ # Main application source code
78+
├── source/ # Main application source code
7979
│ ├── Main.cpp # Application entry point
8080
│ ├── MainComponent.cpp/h # GUI main window component
8181
│ ├── Validator.cpp/h # Core validation orchestration
@@ -93,6 +93,11 @@ pluginval/
9393
│ ├── vst3validator/ # Embedded VST3 validator integration
9494
│ │ ├── VST3ValidatorRunner.h
9595
│ │ └── VST3ValidatorRunner.cpp
96+
│ ├── acceptance/ # `pluginval test` golden-file subsystem (parallel to validate)
97+
│ │ ├── TestConfig.cpp/h # snake_case JSON config struct (independent of PluginvalSettings)
98+
│ │ ├── AcceptanceTest.cpp/h # plugin load + state + input + render; record-or-compare orchestrator
99+
│ │ ├── ReferenceComparator.cpp/h # Comparator interface + registry + SampleComparator
100+
│ │ └── TestReporter.cpp/h # text + JSON result, exit code
96101
│ └── tests/ # Individual test implementations
97102
│ ├── BasicTests.cpp # Core plugin tests (info, state, audio)
98103
│ ├── BusTests.cpp # Audio bus configuration tests
@@ -108,6 +113,9 @@ pluginval/
108113
├── tests/
109114
│ ├── AddPluginvalTests.cmake # CMake module for CTest integration
110115
│ ├── test_plugins/ # Test plugin files
116+
│ │ ├── tone_generator/ # Deterministic dogfood generator (PLUGINVAL_BUILD_TEST_PLUGINS)
117+
│ │ └── gain/ # Deterministic dogfood gain effect (for the input.audio path)
118+
│ ├── acceptance/ # Acceptance self-tests: configs (*.json.in), inputs/, checked-in refs/ WAVs
111119
│ ├── mac_tests/ # macOS-specific tests
112120
│ └── windows_tests.bat # Windows test scripts
113121
├── docs/ # Documentation
@@ -150,6 +158,7 @@ cmake --build Builds/Debug --config Debug
150158
| `WITH_ADDRESS_SANITIZER` | Enable AddressSanitizer | OFF |
151159
| `WITH_THREAD_SANITIZER` | Enable ThreadSanitizer | OFF |
152160
| `VST2_SDK_DIR` | Path to VST2 SDK (env var) | - |
161+
| `PLUGINVAL_BUILD_TEST_PLUGINS` | Build the in-repo dogfood plugins + acceptance CTest self-tests | OFF |
153162

154163
### Enabling VST2 Support
155164

@@ -258,6 +267,46 @@ settings set via a base64-encoded JSON argument (`--config-base64`), avoiding
258267
per-flag re-serialisation and command-line quoting hazards. `--help`/`--version`
259268
are handled by CLI11 (auto usage + a footer with the env-var/commands notes).
260269

270+
### Acceptance Testing (`pluginval test`)
271+
272+
A **parallel subsystem** to validate, in `source/acceptance/`. It answers "does
273+
this plugin produce the expected output for a known input + state?" — a
274+
deterministic *render + golden-file comparison*, not a unit-test pass/fail. Full
275+
spec: `tests/acceptance/Acceptance testing design.md`; end-user guide:
276+
`docs/Acceptance testing.md`.
277+
278+
- **CLI**: `pluginval test <config.json>`. A new `Command::test` is recognised
279+
by `settings_parser::dispatch()` (captures the positional config path into
280+
`DispatchResult::testConfigPath`), `isCommandLine()` and `getFooterText()`;
281+
`CommandLine.cpp`'s `performCommandLine()` has a `Command::test` branch that
282+
runs the acceptance runner **synchronously on the message thread** and quits.
283+
- **Config**: `acceptance::TestConfig` (`TestConfig.cpp/h`) — std-typed struct,
284+
**snake_case JSON keys** mapped via explicit `to_json`/`from_json` (members
285+
stay camelCase). It is **independent** of `PluginvalSettings` / the `--config`
286+
layering: the test config is a positional argument loaded standalone.
287+
- **Flow** (`AcceptanceTest.cpp`): load plugin → apply `state.file` then
288+
`state.parameters` (normalised, matched by index / case-insensitive name or
289+
paramID) → feed `input.audio`/`input.midi` or silence → render a fixed
290+
duration block-by-block (reusing the `AudioProcessingTest` shape + the
291+
VST3-safe helpers in `TestUtilities.h`). If no reference exists it **records**
292+
one (32-bit float WAV + `<name>.wav.json` sidecar manifest); otherwise it
293+
**compares** and writes a diff WAV on failure. Exit `0`/`1`.
294+
- **Comparators** (`ReferenceComparator.cpp/h`): pluggable `Comparator` +
295+
`createComparator(name)` registry. v1 ships only `sample` (per-sample abs-diff
296+
tolerance, default one 16-bit LSB = `1/32768`; `0` = bit-exact). Adding
297+
`spectrum`/`crosscorr`/etc. is one registry entry, no config/runner changes.
298+
- **Dogfood + self-tests**: two minimal deterministic `juce_add_plugin` targets
299+
behind `PLUGINVAL_BUILD_TEST_PLUGINS``tests/test_plugins/tone_generator/`
300+
(closed-form sine/square generator, phase resets on `prepareToPlay`) and
301+
`tests/test_plugins/gain/` (a gain effect, used to dogfood the `input.audio`
302+
path: it gains a checked-in full-height sine and is compared bit-exact).
303+
`tests/acceptance/` holds checked-in configs (`*.json.in`, the plugin paths +
304+
input dir substituted at configure time), `inputs/` and reference WAVs, run via
305+
CTest (`pluginval.acceptance.*`: sine-440, square-220, square-state, gain-half).
306+
Phase 2 items (config-array multiplexing,
307+
automation, playhead, extra comparators, child-process isolation) are notes
308+
only.
309+
261310
### Test Framework
262311

263312
Tests are self-registering. To find all tests, look for static instances:
@@ -298,12 +347,12 @@ The VST3 validator (Steinberg's vstvalidator) is embedded into pluginval when bu
298347
1. The VST3 SDK is fetched via CPM during CMake configure
299348
2. The SDK's own `validator` target is built as a separate executable
300349
3. A CMake script (`cmake/GenerateBinaryHeader.cmake`) converts the compiled binary into a C byte array header
301-
4. `VST3ValidatorRunner` (`Source/vst3validator/`) extracts the embedded binary to a temp file on first use
350+
4. `VST3ValidatorRunner` (`source/vst3validator/`) extracts the embedded binary to a temp file on first use
302351
5. When the `VST3validator` test runs, it spawns the extracted validator as a subprocess
303352
304353
**Key files:**
305354
- `cmake/GenerateBinaryHeader.cmake` — binary-to-C-header conversion script
306-
- `Source/vst3validator/VST3ValidatorRunner.h/cpp` — extracts embedded binary, returns `juce::File`
355+
- `source/vst3validator/VST3ValidatorRunner.h/cpp` — extracts embedded binary, returns `juce::File`
307356
308357
**Disabling embedded validator:**
309358
```bash
@@ -486,7 +535,7 @@ Run internal tests via CLI:
486535
## Common Tasks for AI Assistants
487536

488537
### Finding Where Tests Are Defined
489-
- All test classes are in `Source/tests/*.cpp`
538+
- All test classes are in `source/tests/*.cpp`
490539
- Search for `static.*Test.*Test;` to find registrations
491540
- Each test subclasses `PluginTest`
492541

CMakeLists.txt

Lines changed: 49 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,10 @@ endif()
2828
option(WITH_ADDRESS_SANITIZER "Enable Address Sanitizer" OFF)
2929
option(WITH_THREAD_SANITIZER "Enable Thread Sanitizer" OFF)
3030

31+
# Builds the in-repo dogfood plugins (e.g. the tone generator) used by the
32+
# acceptance self-tests. Off for normal release builds.
33+
option(PLUGINVAL_BUILD_TEST_PLUGINS "Build the in-repo test plugins used by the acceptance self-tests" OFF)
34+
3135
message(STATUS "Sanitizers: ASan=${WITH_ADDRESS_SANITIZER} TSan=${WITH_THREAD_SANITIZER}")
3236
if (WITH_ADDRESS_SANITIZER)
3337
if (MSVC)
@@ -105,7 +109,7 @@ endif()
105109
juce_add_gui_app(pluginval
106110
BUNDLE_ID com.Tracktion.pluginval
107111
COMPANY_NAME Tracktion
108-
ICON_BIG "${CMAKE_CURRENT_SOURCE_DIR}/Source/binarydata/icon.png"
112+
ICON_BIG "${CMAKE_CURRENT_SOURCE_DIR}/source/binarydata/icon.png"
109113
HARDENED_RUNTIME_ENABLED TRUE
110114
HARDENED_RUNTIME_OPTIONS com.apple.security.cs.allow-unsigned-executable-memory com.apple.security.cs.disable-library-validation com.apple.security.get-task-allow)
111115

@@ -118,42 +122,50 @@ set_target_properties(pluginval PROPERTIES
118122
CXX_VISIBILITY_PRESET hidden)
119123

120124
set(SourceFiles
121-
Source/CommandLine.h
122-
Source/CrashHandler.h
123-
Source/MainComponent.h
124-
Source/PluginTests.h
125-
Source/PluginvalSettings.h
126-
Source/SettingsParser.h
127-
Source/SettingsSerializer.h
128-
Source/TestUtilities.h
129-
Source/Validator.h
130-
Source/CommandLine.cpp
131-
Source/CrashHandler.cpp
132-
Source/Main.cpp
133-
Source/MainComponent.cpp
134-
Source/PluginTests.cpp
135-
Source/SettingsParser.cpp
136-
Source/SettingsSerializer.cpp
137-
Source/tests/BasicTests.cpp
138-
Source/tests/LocaleTest.cpp
139-
Source/tests/BusTests.cpp
140-
Source/tests/EditorTests.cpp
141-
Source/tests/ExtremeTests.cpp
142-
Source/tests/ParameterFuzzTests.cpp
143-
Source/TestUtilities.cpp
144-
Source/Validator.cpp)
125+
source/CommandLine.h
126+
source/CrashHandler.h
127+
source/MainComponent.h
128+
source/PluginTests.h
129+
source/PluginvalSettings.h
130+
source/SettingsParser.h
131+
source/SettingsSerializer.h
132+
source/TestUtilities.h
133+
source/Validator.h
134+
source/acceptance/TestConfig.h
135+
source/acceptance/TestConfig.cpp
136+
source/acceptance/AcceptanceTest.h
137+
source/acceptance/AcceptanceTest.cpp
138+
source/acceptance/ReferenceComparator.h
139+
source/acceptance/ReferenceComparator.cpp
140+
source/acceptance/TestReporter.h
141+
source/acceptance/TestReporter.cpp
142+
source/CommandLine.cpp
143+
source/CrashHandler.cpp
144+
source/Main.cpp
145+
source/MainComponent.cpp
146+
source/PluginTests.cpp
147+
source/SettingsParser.cpp
148+
source/SettingsSerializer.cpp
149+
source/tests/BasicTests.cpp
150+
source/tests/LocaleTest.cpp
151+
source/tests/BusTests.cpp
152+
source/tests/EditorTests.cpp
153+
source/tests/ExtremeTests.cpp
154+
source/tests/ParameterFuzzTests.cpp
155+
source/TestUtilities.cpp
156+
source/Validator.cpp)
145157

146158
target_sources(pluginval PRIVATE ${SourceFiles})
147-
source_group(TREE ${CMAKE_CURRENT_SOURCE_DIR}/Source PREFIX Source FILES ${SourceFiles})
159+
source_group(TREE ${CMAKE_CURRENT_SOURCE_DIR}/source PREFIX Source FILES ${SourceFiles})
148160

149161
# Add VST3 validator runner sources to pluginval (extracts embedded binary at runtime)
150162
if(PLUGINVAL_VST3_VALIDATOR)
151163
set(VST3ValidatorFiles
152-
Source/vst3validator/VST3ValidatorRunner.h
153-
Source/vst3validator/VST3ValidatorRunner.cpp
164+
source/vst3validator/VST3ValidatorRunner.h
165+
source/vst3validator/VST3ValidatorRunner.cpp
154166
)
155167
target_sources(pluginval PRIVATE ${VST3ValidatorFiles})
156-
source_group("Source/vst3validator" FILES ${VST3ValidatorFiles})
168+
source_group("source/vst3validator" FILES ${VST3ValidatorFiles})
157169
endif()
158170

159171
if (DEFINED ENV{VST2_SDK_DIR})
@@ -256,3 +268,11 @@ else()
256268
DEPENDS pluginval ${PLUGINVAL_TARGET}
257269
COMMENT "Run pluginval CLI with strict validation")
258270
endif()
271+
272+
# In-repo dogfood plugins + acceptance self-tests.
273+
if (PLUGINVAL_BUILD_TEST_PLUGINS)
274+
enable_testing()
275+
add_subdirectory(tests/test_plugins/tone_generator)
276+
add_subdirectory(tests/test_plugins/gain)
277+
add_subdirectory(tests/acceptance)
278+
endif()

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,7 @@ This means you can check the exit code on your various CI and mark builds a fail
102102
- [Testing plugins with pluginval](<docs/Testing plugins with pluginval.md>)
103103
- [Debugging a failed validation](<docs/Debugging a failed validation.md>)
104104
- [Adding pluginval to CI](<docs/Adding pluginval to CI.md>)
105+
- [Acceptance testing](<docs/Acceptance testing.md>)
105106

106107
### Contributing
107108
If you would like to contribute to the project please do! It's very simple to add tests, simply:

docs/Acceptance testing.md

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
# Acceptance testing
2+
3+
Acceptance testing answers a different question from normal validation. Instead of
4+
"does this plugin conform to the host API and behave safely?" it asks **"does this
5+
plugin still produce the output I expect for a known input and state?"** — a
6+
deterministic *render + golden-file comparison*. It is ideal for catching
7+
accidental DSP changes (a wrong coefficient, a refactor that shifts the output) in
8+
your own plugins from CI.
9+
10+
It is driven by a small JSON config and a dedicated command:
11+
12+
```
13+
pluginval test myPlugin-default.json
14+
```
15+
16+
- The **first** run renders the plugin and, finding no reference, **records** one
17+
(a `.wav` plus a `.wav.json` manifest) next to the config and reports success.
18+
- **Subsequent** runs render again and **compare** against that reference,
19+
exiting `0` on a match and `1` on a mismatch (writing a diff `.wav` to help you
20+
see what changed).
21+
22+
So the workflow is: write a config, run once to record the reference, **commit the
23+
config and the reference**, then let CI run the same command on every change.
24+
25+
### A minimal config
26+
27+
```json
28+
{
29+
"name": "myReverb-default",
30+
"plugin": "/path/to/MyReverb.vst3",
31+
"input": { "audio": "inputs/drums.wav" },
32+
"reference": "refs/myReverb-default.wav",
33+
"state": { "parameters": { "Mix": 0.5, "Decay": 0.8 } },
34+
"sample_rate": 48000,
35+
"block_size": 512,
36+
"render_duration": 2.0,
37+
"comparison": { "sample": 1e-6 }
38+
}
39+
```
40+
41+
JSON keys are `snake_case`. The most useful fields:
42+
43+
| Field | Notes |
44+
|---|---|
45+
| `plugin` | Path to the plugin (or an AU identifier). **Required.** |
46+
| `input.audio` / `input.midi` | Input files to feed it. Omit both for silence (e.g. instruments driven only by MIDI, or generators). |
47+
| `state.parameters` | A map of parameter name (or index) → **normalised** value (`0``1`), applied before rendering. |
48+
| `state.file` | A binary `getStateInformation` blob to restore first (e.g. a captured preset). Applied before `state.parameters`. |
49+
| `reference` | The golden `.wav`. Defaults to `<name>.wav` next to the config. |
50+
| `render_duration` | Seconds to render. If omitted, the input audio's length is used. |
51+
| `comparison` | How to compare. `{ "sample": <tolerance> }` is a per-sample absolute-difference tolerance (`0` = bit-exact); the default is one 16-bit LSB. |
52+
53+
### Determinism matters
54+
55+
Acceptance testing only works for output that is reproducible from a fixed input
56+
and state. A plugin with free-running randomness can't be golden-tested reliably.
57+
For the same reason, references are only safely **portable across platforms** when
58+
you allow a tolerance — exact per-sample matches rarely survive different CPUs and
59+
floating-point libraries. If a reference recorded on one OS fails on another, raise
60+
the `sample` tolerance (or use a more tolerant comparison method as they are added).
61+
62+
### Running from CI
63+
64+
`pluginval test` is just a command that returns an exit code, so any CI system can
65+
run it the same way it runs your other checks:
66+
67+
```bash
68+
pluginval test tests/acceptance/myReverb-default.json
69+
```
70+
71+
A non-zero exit fails the build. Commit the config and its reference `.wav`
72+
alongside your project so every run compares against the same golden file.
73+
74+
For the complete schema, the comparator design and the planned roadmap, see the
75+
[design document](<../tests/acceptance/Acceptance testing design.md>).

docs/Command line options.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,9 @@ COMMANDS:
1111
"pluginval [options] <plugin>" also work.
1212
run-tests Run the internal unit tests.
1313
strictness-help [level] List all tests that run at the given strictness level.
14+
test <config.json> Run a deterministic acceptance (golden-file) test
15+
from a config. Records a reference on first run,
16+
compares against it afterwards (exit 0/1).
1417

1518
The flat flags --validate <plugin>, --run-tests and --strictness-help [level]
1619
are deprecated aliases for the commands above and will be removed in a future

0 commit comments

Comments
 (0)