View a markdown version of this page

Plugin testing guide - Amazon Inspector

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.lua next to the plugin's init.lua

  • Multiple test files per plugin: any file matching *_test.lua is 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

  • local functions are not testable. Only global functions from init.lua are visible to the test file. Expose helpers via a module table if they need testing.

  • No sbomgen.* file I/O outside scan calls. Functions like sbomgen.read_file() require an artifact context, which only exists inside testing.scan_* calls.

  • No lifecycle hooks. There is no before_each, after_each, setup, or teardown. 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.lua were exercised.

  • No benchmarks. The test framework does not support performance benchmarks.

Developer Responsibilities

When writing a new Lua plugin

  • Create init_test.lua next to your init.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