Skip to content

Commit f257fb4

Browse files
authored
Add --junit-xml and --json-results report output to validate and test (#192)
* Added --junit-xml and --json-results report output to validate and test Both commands can now write machine-readable reports through a shared report_writer. A placeholder error report is written before a run and replaced on completion, and a timeout writes a timed-out report, so a report always agrees with the exit code. A report that can't be written fails the run. * Report output: Renamed TestCase::Status to avoid the X11 Status macro on Linux
1 parent 790ba5e commit f257fb4

20 files changed

Lines changed: 836 additions & 28 deletions

‎CHANGELIST.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
### 2.0.0
44
- Updated JUCE to 9.0.3
55
- Fixed the crash handler so a backtrace is shown again when a plugin crashes, and made it async-signal-safe [#144]
6+
- Added `--junit-xml <file>` and `--json-results <file>` to both `pluginval validate` and `pluginval test` to write machine-readable reports for CI (also settable as `JUNIT_XML` / `JSON_RESULTS`). A report that can't be written fails the run. See `docs/Adding pluginval to CI.md`
67
- Added acceptance testing: `pluginval test <config.json>` renders a plugin with a known state, input (audio/MIDI) and optional playhead, then compares the output against a checked-in reference WAV, exiting `0` on a match and `1` on a mismatch (with a diff WAV). Use `--record-missing` to record new references. See `docs/Acceptance testing.md`
78
- 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`
89
- **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`

‎CLAUDE.md‎

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,7 @@ pluginval/
8484
│ ├── PluginvalSettings.h # Unified settings struct + JSON mapping + toPluginTestOptions()
8585
│ ├── SettingsParser.cpp/h # CLI/env/config -> merged JSON -> settings; child-process handoff
8686
│ ├── SettingsSerializer.cpp/h # JSON load/save + value coercions (comma lists, hex seed)
87+
│ ├── ReportWriter.cpp/h # JUnit XML + JSON report files (shared by validate and test)
8788
│ ├── CrashHandler.cpp/h # Crash reporting utilities
8889
│ ├── TestUtilities.cpp/h # Helper functions for tests
8990
│ ├── RTCheck.h # Real-time safety checking macros
@@ -98,7 +99,7 @@ pluginval/
9899
│ │ ├── TestConfig.cpp/h # snake_case JSON config struct (independent of PluginvalSettings)
99100
│ │ ├── AcceptanceTest.cpp/h # plugin load + state + input + render; record-or-compare orchestrator
100101
│ │ ├── ReferenceComparator.cpp/h # Comparator interface + registry + SampleComparator
101-
│ │ └── TestReporter.cpp/h # text + JSON result, exit code
102+
│ │ └── TestReporter.cpp/h # text + JSON result, exit code, conversion to a report_writer::Report
102103
│ └── tests/ # Individual test implementations
103104
│ ├── BasicTests.cpp # Core plugin tests (info, state, audio)
104105
│ ├── BusTests.cpp # Audio bus configuration tests
@@ -316,6 +317,36 @@ spec: `tests/acceptance/Acceptance testing design.md`; end-user guide:
316317
multiplexing, automation, extra comparators, child-process isolation) are notes
317318
only.
318319

320+
### Report Output (`--junit-xml` / `--json-results`)
321+
322+
Both `validate` and `test` can write machine-readable report files through one
323+
shared writer, `report_writer` (`source/ReportWriter.cpp/h`). Stdout and the text
324+
log are unaffected.
325+
326+
- **Model**: a neutral `report_writer::Report` (command, target, properties and a
327+
list of `TestCase`s with a `passed` / `failed` / `error` status, messages,
328+
duration and optional JSON `details`). `toJUnitXml()` builds a single
329+
`<testsuite>` with `juce::XmlElement`; `toJson()` emits snake_case keys with a
330+
`summary` block. `write()` skips unset files and creates parent directories; if it fails, the
331+
run fails (exit `1`).
332+
- **Two adapters**: `toReport()` in `Validator.cpp` converts the
333+
`juce::UnitTestRunner::TestResult` array collected in `runTests()` (one case per
334+
`beginTest`); `acceptance::reporter::toReport()` converts an acceptance
335+
`TestResult` (its existing JSON goes in `details`).
336+
- **Options**: for `validate` they are ordinary settings (`junitXml` /
337+
`jsonResults` in `PluginvalSettings`, resolved to `juce::File`s on
338+
`PluginTests::Options`), so env vars (`JUNIT_XML` / `JSON_RESULTS`), `--config`
339+
and the child-process handoff all work. For `test` they are parsed in
340+
`settings_parser::dispatch()` into `DispatchResult` and carried on
341+
`acceptance::RunOptions`.
342+
- **Incomplete runs**: before a run starts, `makeIncomplete()` writes a
343+
placeholder report with a single `error` case; the real report overwrites it on
344+
completion, so a crash leaves an error report rather than a stale or missing
345+
one. On a timeout the thread that ends the process writes a "timed out" error
346+
report as its last act (`PluginsUnitTestRunner`'s `onTimeout` for validate, the
347+
acceptance `Watchdog` for test), so the report always agrees with the exit code
348+
even if the tests finish in the moment before the process is terminated.
349+
319350
### Test Framework
320351

321352
Tests are self-registering. To find all tests, look for static instances:
@@ -467,6 +498,8 @@ Key options (for `validate`):
467498
- `--timeout-ms [ms]` - Test timeout (default: 30000, -1 for none)
468499
- `--verbose` - Enable verbose logging
469500
- `--output-dir [dir]` - Directory for log files
501+
- `--junit-xml [file]` - Write a JUnit XML report (also accepted by `test`)
502+
- `--json-results [file]` - Write a JSON report (also accepted by `test`)
470503
- `--sample-rates [list]` - Comma-separated sample rates
471504
- `--block-sizes [list]` - Comma-separated block sizes
472505
- `--rtcheck [disabled|enabled|relaxed]` - Real-time safety checking

‎CMakeLists.txt‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,7 @@ set(SourceFiles
134134
source/MainComponent.h
135135
source/PluginTests.h
136136
source/PluginvalSettings.h
137+
source/ReportWriter.h
137138
source/SettingsParser.h
138139
source/SettingsSerializer.h
139140
source/TestUtilities.h
@@ -151,6 +152,7 @@ set(SourceFiles
151152
source/Main.cpp
152153
source/MainComponent.cpp
153154
source/PluginTests.cpp
155+
source/ReportWriter.cpp
154156
source/SettingsParser.cpp
155157
source/SettingsSerializer.cpp
156158
source/tests/BasicTests.cpp

‎docs/Acceptance testing.md‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,13 @@ Any other option is an error.
8989
`RECORD_MISSING=1`.
9090
- `--timeout-ms <ms>` - fail (exit `1`) if the run takes longer than this
9191
(default `30000`, `-1` to never time out). Also settable as `TIMEOUT_MS`.
92+
- `--junit-xml <file>` - write a JUnit XML report, for CI systems that display
93+
test results. Also settable as `JUNIT_XML`.
94+
- `--json-results <file>` - write a JSON report (the same result that is printed
95+
to stdout, wrapped in a summary). Also settable as `JSON_RESULTS`.
96+
97+
See [Adding pluginval to CI](Adding%20pluginval%20to%20CI.md#machine-readable-reports)
98+
for the report formats.
9299

93100
The render runs on a background thread, as a host's audio thread would, leaving
94101
the message thread free for the plugin.

‎docs/Adding pluginval to CI.md‎

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,3 +41,62 @@ $ ./pluginval validate --output-dir "./bin" "<path_to_plugin>" || exit 1
4141
```sh
4242
> choco install pluginval
4343
```
44+
45+
### Machine-readable reports
46+
Both `pluginval validate` and `pluginval test` can write their results to a file, as well as printing them:
47+
48+
- `--junit-xml <file>` writes a JUnit XML report, which most CI systems (GitHub Actions, GitLab, Jenkins, Azure Pipelines) can display as per-test results
49+
- `--json-results <file>` writes the same results as JSON, for scripts and dashboards
50+
51+
Relative paths are resolved against the working directory and any missing directories are created. Both options can be used together, and can also be set as the `JUNIT_XML` and `JSON_RESULTS` environment variables. The text output and log files are unchanged. If a report can't be written (e.g. the path isn't writable) the run fails with exit code `1`.
52+
53+
```sh
54+
$ ./pluginval validate --strictness-level 5 --junit-xml "./reports/MyPlugin.xml" --json-results "./reports/MyPlugin.json" "<path_to_plugin>"
55+
```
56+
57+
Each test pluginval runs becomes one `<testcase>`, with a `<failure>` holding the failure messages if it failed:
58+
59+
```xml
60+
<testsuites name="pluginval" tests="20" failures="1" errors="0" time="12.345">
61+
<testsuite name="pluginval validate: MyPlugin.vst3" tests="20" failures="1" errors="0" time="12.345" skipped="0" timestamp="...">
62+
<testcase classname="pluginval.validate" name="Open plugin (cold)" time="0.150"/>
63+
<testcase classname="pluginval.validate" name="Plugin state" time="0.020">
64+
<failure message="...">...</failure>
65+
</testcase>
66+
</testsuite>
67+
</testsuites>
68+
```
69+
70+
The JSON report has the same content:
71+
72+
```json
73+
{
74+
"pluginval_version": "2.0.0",
75+
"command": "validate",
76+
"target": "/path/to/MyPlugin.vst3",
77+
"start_time": "...",
78+
"duration_ms": 12345,
79+
"properties": { "plugin_name": "MyPlugin", "strictness_level": "5" },
80+
"summary": { "tests": 20, "passed": 19, "failed": 1, "errors": 0 },
81+
"tests": [
82+
{ "name": "Open plugin (cold)", "status": "passed", "duration_ms": 150, "messages": [] }
83+
]
84+
}
85+
```
86+
87+
`status` is `passed`, `failed` or `error`. For `pluginval test`, each test also has a `details` object holding the comparison results.
88+
89+
If pluginval doesn't finish (the plugin crashes it, or the run times out), the report contains a single `error` entry saying so, so a report left over from an earlier run is never mistaken for a pass.
90+
91+
##### GitHub Actions
92+
```yaml
93+
- name: Validate plugin
94+
run: ./pluginval validate --strictness-level 5 --skip-gui-tests --junit-xml reports/pluginval.xml ./build/MyPlugin.vst3
95+
96+
- name: Upload pluginval report
97+
if: always()
98+
uses: actions/upload-artifact@v4
99+
with:
100+
name: pluginval-report
101+
path: reports/
102+
```

‎docs/Command line options.md‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ COMMANDS:
1818
pass instead.
1919
--timeout-ms <ms> Fail if the run takes longer
2020
(default 30000, -1 = never).
21+
--junit-xml <file> Write a JUnit XML report.
22+
--json-results <file> Write a JSON report.
2123
Any other option is an error.
2224

2325
The flat flags --validate <plugin>, --run-tests and --strictness-help [level]
@@ -46,6 +48,8 @@ OPTIONS (for the validate command):
4648
--output-dir TEXT Directory in which to write the log files.
4749
--output-filename TEXT
4850
Filename to write logs into.
51+
--junit-xml TEXT File to write a JUnit XML report to.
52+
--json-results TEXT File to write a JSON report to.
4953
--disabled-tests TEXT
5054
Comma-separated test names, or a path to a file listing them.
5155
--random-seed TEXT Random seed (hex 0x.. or int) for replicable test runs.

‎source/CommandLine.cpp‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -232,6 +232,14 @@ void performCommandLine (CommandLineValidator& validator, const juce::String& co
232232
options.recordMissing = routed.recordMissing;
233233
options.timeoutMs = routed.timeoutMs;
234234

235+
const auto toReportFile = [] (const juce::String& path)
236+
{
237+
return path.isEmpty() ? juce::File() : juce::File::getCurrentWorkingDirectory().getChildFile (path);
238+
};
239+
240+
options.junitXmlFile = toReportFile (routed.junitXmlPath);
241+
options.jsonResultsFile = toReportFile (routed.jsonResultsPath);
242+
235243
// Runs async so will quit itself when done
236244
validator.runAcceptanceTest (juce::File::getCurrentWorkingDirectory().getChildFile (routed.testConfigPath), options);
237245
return;

0 commit comments

Comments
 (0)