Integration tests #
Run from the repository root, using only Go and its standard library:
go run ./test
go run ./test single-file pin-exact
go run ./test 'types-*'
go run ./test -list
go run ./test -binary ./unpm single-file
make integration runs the same runner; make integration CASES=single-file
selects a case. make test runs both the existing unit tests and integration
cases. CI runs both suites.
Case layout #
Each directory containing case.json is a case. Adding a case requires no Go
code. For example:
test/single-file/
├── case.json
├── unpm.json
├── server/
│ └── mylib.js
├── expected/
│ ├── stdout.txt
│ ├── stderr.txt
│ └── vendor/
│ ├── {{host}}/
│ │ └── mylib.js
│ ├── importmap.js
│ ├── importmap.json
│ └── jsconfig.json
└── actual/ # ignored; replaced on each run
The runner builds unpm once, copies unpm.json and the contents of an optional
input/ directory to a fresh temporary working directory, serves server/ over
local HTTP, and runs the binary there. Other files at the case root are not
copied into the working directory.
For pinning, cleanup, check, and why, place the starting vendor tree in
input/vendor/. Other starting files belong under input/ too: for example,
input/custom.json becomes custom.json in the working directory. The usual
unpm.json stays at the case root; defining it both there and in input/ is an
error. Cases that do not need a configuration file can omit it.
The selected output paths and captured stdout/stderr are copied to actual/.
The runner compares the entire expected/ and actual/ trees, including file
contents, missing files, extra files, and directories. An absent expected output
directory asserts that the command must not create it. Empty stdout/stderr files
assert that the corresponding stream is empty. Files are compared byte for byte;
JSON formatting, whitespace, and final newlines matter.
Cases use --silent to suppress progress and success messages while retaining
plain-text warnings, errors, and why results. Arguments are passed exactly as
written in case.json; the runner does not insert flags. Console output is
compared directly, without stripping spinner frames or ANSI escape sequences.
Both successful and failed runs leave actual/ available for inspection. The
temporary working directory and binary are removed when the runner exits. The
runner never updates expected/. Edit expectations deliberately and review their
Git diff, or inspect a failure with:
git diff --no-index test/single-file/expected test/single-file/actual
case.json #
A minimal case is:
{
"args": ["vendor", "--silent"]
}
Supported settings:
| Key | Default | Meaning |
|---|---|---|
args |
Required | Arguments passed directly to unpm, without a shell. [] tests invoking it without arguments. |
exit_code |
0 |
Expected process exit code. |
outputs |
["vendor"] |
Relative files or directories to copy into actual/ and compare. Use [] for console-only cases. |
timeout |
"10s" |
Maximum command duration, as a Go duration. A timeout always fails the case. |
responses |
{} |
HTTP response overrides, keyed by exact request path including its query string. |
Unknown settings are errors. Output paths must be relative, non-overlapping, and
inside the working directory. Include every output location whose presence or
absence matters; for example, an --out assets/deps case can specify
["assets", "vendor"] to also catch accidental writes to the default directory.
HTTP fixtures #
Requests normally read the matching file under server/, ignoring the query
string for the filename. Missing files return 404. JavaScript is served as
text/javascript, .json and .map as application/json, and other files as
text/plain. No directory listings or automatic directory redirects are used.
Each response override accepts status (default 200), headers, and file
(a relative path under server/). For example:
{
"args": ["vendor"],
"responses": {
"/pkg": {
"status": 302,
"headers": {"Location": "/pkg/index.js"}
},
"/pkg/index.js": {
"headers": {"X-Typescript-Types": "./index.d.ts"}
},
"/shim": {
"file": "shim.js",
"headers": {"X-ESM-Path": "/pkg/index.js"}
}
}
}
An override applies only to its exact request URI. This allows a redirect from
/file.js to /file.js?target=es2022 while serving the latter normally. A file
override also lets a URL such as /pkg have a body while files beneath /pkg/
exist on disk. Non-200 responses have an empty body unless file is set.
Use local fixture URLs for downloads; the runner does not block external network access. Existing cases need no internet access.
Variable output #
The runner expands two literal placeholders in input filenames and contents, command arguments, server file contents, and response header values:
{{host}}: the local HTTP server's127.0.0.1:portaddress. Write URLs ashttp://{{host}}/mylib.jsand expected vendor paths asvendor/{{host}}/mylib.js.{{work}}: the absolute temporary working directory, for cases testing absolute configuration paths.
Those exact values are replaced with placeholders in captured output paths and
contents, so actual/ can be compared directly with expected/. Nothing else
is normalized: diagnostics are not sorted, JSON is not reformatted, and terminal
bytes are retained in console output and file contents.
The string-literal-import case records an existing scanner limitation: text
that looks like an import inside a string triggers a download. Since required
download failures are fatal, this case expects an error and no output directory.