Plugin testing guide
Plugin authors write and test Lua plugins entirely in Lua — no Go toolchain required. Tests are co-located with the plugin under init_test.lua and run via the inspector-sbomgen plugin test command.
For general plugin authoring, see the Plugin developer guide. For the complete function catalog (including the testing.* API), see the Plugin API reference.
-- init_test.lua (next to init.lua) function test_discovers_curl_version() local result = testing.scan_directory("_testdata/include/curl") testing.assert_equals(1, #result.findings) testing.assert_equals("libcurl", result.findings[1].name) testing.assert_equals("8.14.1", result.findings[1].version) end
# Run every test under a plugin directory inspector-sbomgen plugin test --path ./my-plugins # Verbose output — show each test name and result inspector-sbomgen plugin test --path ./my-plugins -v
Quick Start
1. Create a test file
Place init_test.lua next to your plugin's init.lua:
my-plugin/ ├── discovery/cross-platform/extra-ecosystems/curl/ │ ├── init.lua │ ├── init_test.lua │ └── _testdata/ │ └── include/curl/curlver.h
2. Write test functions
Any global function starting with test_ is discovered and executed:
function test_finds_libcurl() local result = testing.scan_directory("_testdata/include/curl") testing.assert_equals(1, #result.findings) testing.assert_equals("libcurl", result.findings[1].name) end function test_no_findings_for_empty_dir() local result = testing.scan_directory("_testdata/empty") testing.assert_equals(0, #result.findings) end
3. Run tests
inspector-sbomgen plugin test --path ./my-plugin
Directory Layout
Plugin structure
Test files and test data are co-located with the plugin:
my-plugins/ ├── discovery/ │ └── cross-platform/ │ └── extra-ecosystems/ │ └── curl/ │ ├── init.lua # plugin source │ ├── init_test.lua # test file │ └── _testdata/ # test fixtures │ ├── include/curl/curlver.h │ └── binaries/unix/curl ├── collection/ │ └── cross-platform/ │ └── extra-ecosystems/ │ └── curl-installation/ │ ├── init.lua │ └── init_test.lua
Test file naming
-
Default:
init_test.luanext to the plugin'sinit.lua -
Multiple test files per plugin: any file matching
*_test.luais discovered -
Examples:
init_test.lua,parsing_test.lua,discovery_test.lua
Test data: _testdata/
Test data lives in _testdata/ next to the plugin. The leading underscore is a convention that keeps fixtures visually separate from plugin source; the plugin test command does not descend into _testdata/ when searching for *_test.lua files, so fixtures are never mistaken for test files.
Test files reference fixtures with relative paths:
local result = testing.scan_directory("_testdata/include/curl")
Paths are resolved relative to the directory containing the test file.
The testing.* API
Scan Functions
Each scan function creates an artifact, runs the plugin's discovery → collection pipeline, and returns findings. The test author never manually creates artifacts, event buses, or registries.
-- Scan a directory of test fixtures (most common) local result = testing.scan_directory("_testdata/curl") -- Alias for scan_directory (archives use the same backend) local result = testing.scan_archive("_testdata/app.tar.gz") -- Scan as a localhost artifact local result = testing.scan_localhost("_testdata/curl") -- Scan a compiled binary local result = testing.scan_binary("_testdata/binaries/curl") -- Scan a mounted volume local result = testing.scan_volume("_testdata/volume-root") -- Scan a container image tarball local result = testing.scan_container("_testdata/images/alpine.tar")
Each scan function:
-
Creates a fresh artifact for each call (no state leaks between calls)
-
Loads only the current plugin's discovery + collection pair
-
Returns a result table
Result Table
result.findings -- array of finding tables result.findings[1].name -- package name (string) result.findings[1].version -- package version (string) result.findings[1].purl -- package URL (string) result.findings[1].component_type -- component type (string) result.findings[1].properties -- table<string, string> result.findings[1].children -- array of nested finding tables (same shape)
Assertions
-- Equality testing.assert_equals(expected, actual, message?) testing.assert_not_equals(expected, actual, message?) -- Truthiness testing.assert_true(value, message?) testing.assert_false(value, message?) -- Nil checks testing.assert_nil(value, message?) testing.assert_not_nil(value, message?) -- String testing.assert_contains(haystack, needle, message?) testing.assert_matches(string, pattern, message?) -- Tables testing.assert_length(table, expected_length, message?) -- Control flow testing.fail(message) -- immediately fail the current test testing.skip(message) -- skip the current test (not a failure)
Standard sbomgen.* API
The full sbomgen.* API (file I/O, regex, system info, logging, etc.) is available in test files, same as in production plugins. However, sbomgen.* functions that require an artifact (e.g., sbomgen.read_file()) only work inside a testing.scan_* callback — they are not available at the top level of a test function.
Running Tests
# Run all tests under a plugin directory inspector-sbomgen plugin test --path ./my-plugins # Filter by regex pattern inspector-sbomgen plugin test --path ./my-plugins --run curl # Verbose output (show each test name and result) inspector-sbomgen plugin test --path ./my-plugins -v # Stop on the first failing test inspector-sbomgen plugin test --path ./my-plugins --fail-fast
The --path flag accepts a plugin root directory (containing discovery/ and/or collection/) or a single ecosystem directory (auto-detected). The command exits non-zero if any test fails.
Output format
With -v, each test prints a === RUN line and a result line (--- PASS, --- FAIL, or --- SKIP). Without -v, only failing tests print. A summary line is printed at the end:
=== RUN curl/discovery/init_test/test_discovers_libcurl_header --- PASS: curl/discovery/init_test/test_discovers_libcurl_header (0.04s) === RUN curl/discovery/init_test/test_discovers_curl_binary_unix --- PASS: curl/discovery/init_test/test_discovers_curl_binary_unix (0.04s) === RUN curl/discovery/init_test/test_no_findings_for_unrelated_files --- PASS: curl/discovery/init_test/test_no_findings_for_unrelated_files (0.04s) ok 3 tests passed
Testing Plugin Helpers
The test file is loaded into the same Lua VM as the plugin's init.lua. Global functions defined in the plugin are callable from tests. To test helper functions, expose them as globals or in a module table:
-- init.lua M = {} function M.parse_version(raw) return string.match(raw, "(%d+%.%d+%.%d+)") end function collect(file_path) local ver = M.parse_version(...) -- ... end
-- init_test.lua function test_parse_version_extracts_semver() testing.assert_equals("1.2.3", M.parse_version("curl/1.2.3")) end function test_parse_version_returns_nil_for_garbage() testing.assert_nil(M.parse_version("not-a-version")) end
Functions declared local in init.lua are not visible to the test file. This is standard Lua scoping.
Behavior and Invariants
Shared VM, shared state
Test functions within a single file share a Lua VM. A global variable set in one test_* function is visible to subsequent functions. If two functions define the same global, the second overwrites the first. Each test should be self-contained and not depend on state from other tests.
Non-deterministic execution order
Test functions are discovered by iterating Lua's global table, which uses hash-based ordering. Tests are not guaranteed to run in the order they are defined. Do not write tests that depend on execution order.
Fresh artifact per scan call
Each call to testing.scan_directory() (or any scan function) creates a completely new artifact. There is no state carried between scan calls within a test or across tests.
Plugin loading
Plugins are loaded once per test run, not once per test file. The test runner loads all plugins from the provided filesystem, then matches each test file to its corresponding plugin VM by phase, platform, category, and ecosystem.
Assertion behavior
When an assertion fails, the failure is recorded but the test function continues executing — subsequent assertions and statements still run. If more than one assertion fails in the same test, the most recent failure is the message reported in the summary; earlier failures are overwritten. To stop a test on its first failure, return from the function after the failing assertion (or use testing.fail() inside a conditional).
Limitations
-
localfunctions are not testable. Only global functions frominit.luaare visible to the test file. Expose helpers via a module table if they need testing. -
No
sbomgen.*file I/O outside scan calls. Functions likesbomgen.read_file()require an artifact context, which only exists insidetesting.scan_*calls. -
No lifecycle hooks. There is no
before_each,after_each,setup, orteardown. Each test function manages its own state. -
No test timeout. A test function that loops forever will hang the runner.
-
No coverage reporting. There is no way to measure which lines of
init.luawere exercised. -
No benchmarks. The test framework does not support performance benchmarks.
Developer Responsibilities
When writing a new Lua plugin
-
Create
init_test.luanext to yourinit.lua -
Create
_testdata/with minimal fixtures that exercise your plugin's logic -
Write
test_*functions covering: successful detection, version extraction, edge cases, and no-match scenarios -
Run tests locally before submitting
Test data guidelines
-
Keep fixtures minimal — use the smallest file that exercises the behavior
-
Avoid committing large binaries to
_testdata/when a small text fixture would suffice -
Each plugin's
_testdata/should be self-contained — no references to files outside the plugin directory