Repository: google/osv-scanner
Pull request #558 (2023-09-27 00:29:50)

test: use `cmp.Diff` for comparing output
This makes it easier to determine what is wrong in CLI tests by showing an actual diff; while in most cases this is an improvement, I have found it's sometimes useful to have the raw output printed so I've included an env variable to allow switching easily with `cmp.Diff` being the default.

In order to reduce the diff noise when a test does fail, this also switches to replacing occurrences of the current working directory in the _actual_ output with `<rootdir>`; this also means that the output will be what should be in the test cases, rather than the absolute path that people would have to replace with `<rootdir>`.

While this could be used throughout the whole test suite, I've just applied this to the CLI tests for now because I think they've got the most to gain whereas it'd be a lot of tedious work to switch to using it everywhere; it should be easy to switch to using it in other places over time.

I have also confirmed that `dedent` correctly handles both spaces and tabs - you can mix and match them without issue (and in fact a few of the tests are using spaces instead of tabs).

codecov-commenter: ## [Codecov](https://app.codecov.io/gh/google/osv-scanner/pull/558?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=google) Report
> Merging [#558](https://app.codecov.io/gh/google/osv-scanner/pull/558?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=google) (0486230) into [main](https://app.codecov.io/gh/google/osv-scanner/commit/5862bfd4c6b7a0b77609fd7b5f07701f3dafb92c?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=google) (5862bfd) will **not change** coverage.
> The diff coverage is `n/a`.

```diff
@@           Coverage Diff           @@
##             main     #558   +/-   ##
=======================================
  Coverage   78.45%   78.45%           
=======================================
  Files          77       77           
  Lines        5273     5273           
=======================================
  Hits         4137     4137           
  Misses        971      971           
  Partials      165      165           
```




another-rex: I think the dedent assumes you're tab sizing of 2 (it replaces tab with 2 spaces), but I think we often have a tab sizing of 4, which is where we run into the issue

Patch:
diff --git a/.github/workflows/lint.yaml b/.github/workflows/checks.yml
similarity index 71%
rename from .github/workflows/lint.yaml
rename to .github/workflows/checks.yml
index a616a49..fed714b 100644
--- a/.github/workflows/lint.yaml
+++ b/.github/workflows/checks.yml
@@ -12,7 +12,7 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-name: lint
+name: Checks
 
 on:
   push:
@@ -43,3 +43,19 @@ jobs:
           check-latest: true
       - name: Run lint action
         uses: ./.github/workflows/lint-action
+  test:
+    name: Run unit tests
+    runs-on: ubuntu-latest
+    steps:
+      - name: Check out code
+        uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 # v4.1.0
+        with:
+          persist-credentials: false
+          fetch-depth: 0
+      - name: Set up Go
+        uses: actions/setup-go@93397bea11091df50f3d7e59dc26a7711a8bcfbe # v4.1.0
+        with:
+          go-version: '1.19'
+          check-latest: true
+      - name: Run test action
+        uses: ./.github/workflows/test-action
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index d9226d0..90a9c25 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -34,9 +34,12 @@ Please review the documentation [README](docs/README.md) for more information ab
 
 ### Prerequisites
 Install:
-1. [Go](https://go.dev/) 1.18+, use `go version` to check.
+1. [Go](https://go.dev/) 1.19+, use `go version` to check.
 2. [GoReleaser](https://goreleaser.com/) (Optional, only if you want reproducible builds).
-3. [golangci-lint](https://golangci-lint.run/) (Optional, only if you want to run the linters locally)
+
+> **Note**
+>
+> The scripts within `/scripts` expect to be run from the root of the repository
 
 ### Building
 
@@ -44,7 +47,7 @@ Install:
 
 Run the following in the project directory:
 ```shell
-$ go build ./cmd/osv-scanner/
+./scripts/build.sh
 ```
 Produces `osv-scanner` binary in the project directory.
 
@@ -52,7 +55,7 @@ Produces `osv-scanner` binary in the project directory.
 
 Run the following in the project directory:
 ```shell
-$ goreleaser build --rm-dist --single-target --snapshot
+./scripts/build_snapshot.sh
 ```
 
 See GoReleaser [documentation](https://goreleaser.com/cmd/goreleaser_build/) for build options.
@@ -64,20 +67,20 @@ using the same Go version as the one used during the actual release (see gorelea
 
 To run tests:
 ```shell
-./run_tests.sh
+./scripts/run_tests.sh
 ```
 
-You can generate an HTML coverage report afterward by running:
+You can generate an HTML coverage report by running:
 
-```
-go tool cover -html=coverage.out -o coverage.html
+```shell
+./scripts/generate_coverage_report.sh
 ```
 
 ### Linting
 To lint your code, run
 
 ```shell
-./run_lints.sh
+./scripts/run_lints.sh
 ```
 
 ## Contributing documentation
diff --git a/pkg/models/constants.go b/pkg/models/constants.go
index 246fa5e..7c392fe 100644
--- a/pkg/models/constants.go
+++ b/pkg/models/constants.go
@@ -23,8 +23,39 @@ const (
 	EcosystemRockyLinux    Ecosystem = "Rocky Linux"
 	EcosystemAlmaLinux     Ecosystem = "AlmaLinux"
 	EcosystemBitnami       Ecosystem = "Bitnami"
+	EcosystemPhotonOS      Ecosystem = "Photon OS"
+	EcosystemCRAN          Ecosystem = "CRAN"
+	EcosystemBioconductor  Ecosystem = "Bioconductor"
+	EcosystemSwiftURL      Ecosystem = "SwiftURL"
 )
 
+var Ecosystems = []Ecosystem{
+	EcosystemGo,
+	EcosystemNPM,
+	EcosystemOSSFuzz,
+	EcosystemPyPI,
+	EcosystemRubyGems,
+	EcosystemCratesIO,
+	EcosystemPackagist,
+	EcosystemMaven,
+	EcosystemNuGet,
+	EcosystemLinux,
+	EcosystemDebian,
+	EcosystemAlpine,
+	EcosystemHex,
+	EcosystemAndroid,
+	EcosystemGitHubActions,
+	EcosystemPub,
+	EcosystemConanCenter,
+	EcosystemRockyLinux,
+	EcosystemAlmaLinux,
+	EcosystemBitnami,
+	EcosystemPhotonOS,
+	EcosystemCRAN,
+	EcosystemBioconductor,
+	EcosystemSwiftURL,
+}
+
 type SeverityType string
 
 const (
diff --git a/scripts/build.sh b/scripts/build.sh
new file mode 100755
index 0000000..9b482d2
--- /dev/null
+++ b/scripts/build.sh
@@ -0,0 +1,5 @@
+#!/usr/bin/env bash
+
+set -e
+
+go build ./cmd/osv-scanner/
diff --git a/scripts/build_snapshot.sh b/scripts/build_snapshot.sh
new file mode 100755
index 0000000..dd5d0bd
--- /dev/null
+++ b/scripts/build_snapshot.sh
@@ -0,0 +1,5 @@
+#!/usr/bin/env bash
+
+set -e
+
+goreleaser build --rm-dist --single-target --snapshot
diff --git a/scripts/generate_coverage_report.sh b/scripts/generate_coverage_report.sh
new file mode 100755
index 0000000..be736c3
--- /dev/null
+++ b/scripts/generate_coverage_report.sh
@@ -0,0 +1,7 @@
+#!/usr/bin/env bash
+
+set -e
+
+./scripts/run_tests.sh
+
+go tool cover -html=coverage.out -o coverage.html


Test patch:
diff --git a/.github/workflows/test-action/action.yml b/.github/workflows/test-action/action.yml
index 36d5e12..a66778c 100644
--- a/.github/workflows/test-action/action.yml
+++ b/.github/workflows/test-action/action.yml
@@ -20,6 +20,6 @@ runs:
   steps:
     - name: Run go test
       shell: bash
-      run: ./run_tests.sh
+      run: ./scripts/run_tests.sh
     - name: Upload coverage to Codecov
       uses: codecov/codecov-action@eaaf4bedf32dbdc6b720b63067d99c4d77d6047d # v3.1.4
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
deleted file mode 100644
index de4a180..0000000
--- a/.github/workflows/test.yml
+++ /dev/null
@@ -1,45 +0,0 @@
-# Copyright 2023 Google LLC
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-#      http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-
-name: test
-
-on:
-  push:
-    branches: [ main ]
-  pull_request:
-    # The branches below must be a subset of the branches above
-    branches: [ main ]
-  merge_group:
-    branches: [ main ]
-
-permissions:
-  contents: read # to fetch code (actions/checkout)
-
-jobs:
-  lint:
-    name: Run unit tests
-    runs-on: ubuntu-latest
-    steps:
-      - name: Check out code
-        uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 # v4.1.0
-        with:
-          persist-credentials: false
-          fetch-depth: 0
-      - name: Set up Go
-        uses: actions/setup-go@93397bea11091df50f3d7e59dc26a7711a8bcfbe # v4.1.0
-        with:
-          go-version: '1.19'
-          check-latest: true
-      - name: Run test action
-        uses: ./.github/workflows/test-action
diff --git a/cmd/osv-scanner/main_test.go b/cmd/osv-scanner/main_test.go
index f12f275..1890b2d 100644
--- a/cmd/osv-scanner/main_test.go
+++ b/cmd/osv-scanner/main_test.go
@@ -11,6 +11,7 @@ import (
 	"testing"
 
 	"github.com/go-git/go-git/v5"
+	"github.com/google/go-cmp/cmp"
 	"github.com/google/osv-scanner/internal/version"
 )
 
@@ -89,6 +90,34 @@ type cliTestCase struct {
 	wantStderr   string
 }
 
+func expectAreEqual(t *testing.T, subject, actual, expect string) {
+	t.Helper()
+
+	actual = dedent(t, actual)
+	expect = dedent(t, expect)
+
+	if !areEqual(t, actual, expect) {
+		if os.Getenv("TEST_NO_DIFF") == "true" {
+			t.Errorf("\nactual %s does not match expected:\n got:\n%s\n\n want:\n%s", subject, actual, expect)
+		} else {
+			t.Errorf("\nactual %s does not match expected:\n%s", subject, cmp.Diff(expect, actual))
+		}
+	}
+}
+
+// normalizeRootDirectory attempts to replace references to the current working
+// directory with "<rootdir>", in order to reduce the noise of the cmp diff
+func normalizeRootDirectory(t *testing.T, str string) string {
+	t.Helper()
+
+	cwd, err := os.Getwd()
+	if err != nil {
+		t.Errorf("could not get cwd (%v) - results and diff might be inaccurate!", err)
+	}
+
+	return strings.ReplaceAll(str, cwd, "<rootdir>")
+}
+
 func testCli(t *testing.T, tc cliTestCase) {
 	t.Helper()
 
@@ -101,17 +130,15 @@ func testCli(t *testing.T, tc cliTestCase) {
 	stdout := stdoutBuffer.String()
 	stderr := stderrBuffer.String()
 
+	stdout = normalizeRootDirectory(t, stdout)
+	stderr = normalizeRootDirectory(t, stderr)
+
 	if ec != tc.wantExitCode {
 		t.Errorf("cli exited with code %d, not %d", ec, tc.wantExitCode)
 	}
 
-	if !areEqual(t, dedent(t, stdout), dedent(t, tc.wantStdout)) {
-		t.Errorf("stdout\n got:\n%s\n\n want:\n%s", dedent(t, stdout), dedent(t, tc.wantStdout))
-	}
-
-	if !areEqual(t, dedent(t, stderr), dedent(t, tc.wantStderr)) {
-		t.Errorf("stderr\n got:\n%s\n\n want:\n%s", dedent(t, stderr), dedent(t, tc.wantStderr))
-	}
+	expectAreEqual(t, "stdout output", stdout, tc.wantStdout)
+	expectAreEqual(t, "stderr output", stderr, tc.wantStderr)
 }
 
 func TestRun(t *testing.T) {
@@ -145,7 +172,7 @@ func TestRun(t *testing.T) {
 			wantExitCode: 0,
 			wantStdout: `
 				Scanning dir ./fixtures/locks-many/composer.lock
-				Scanned %%/fixtures/locks-many/composer.lock file and found 1 package
+				Scanned <rootdir>/fixtures/locks-many/composer.lock file and found 1 package
 				No vulnerabilities found
 			`,
 			wantStderr: "",
@@ -157,8 +184,8 @@ func TestRun(t *testing.T) {
 			wantExitCode: 1,
 			wantStdout: `
 				Scanning dir ./fixtures/sbom-insecure/
-				Scanned %%/fixtures/sbom-insecure/alpine.cdx.xml as CycloneDX SBOM and found 15 packages
-				Scanned %%/fixtures/sbom-insecure/postgres-stretch.cdx.xml as CycloneDX SBOM and found 136 packages
+				Scanned <rootdir>/fixtures/sbom-insecure/alpine.cdx.xml as CycloneDX SBOM and found 15 packages
+				Scanned <rootdir>/fixtures/sbom-insecure/postgres-stretch.cdx.xml as CycloneDX SBOM and found 136 packages
 				+-------------------------------------+------+-----------+--------------------------------+------------------------------------+-------------------------------------------------+
 				| OSV URL                             | CVSS | ECOSYSTEM | PACKAGE                        | VERSION                            | SOURCE                                          |
 				+-------------------------------------+------+-----------+--------------------------------+------------------------------------+-------------------------------------------------+
@@ -198,12 +225,12 @@ func TestRun(t *testing.T) {
 			wantExitCode: 0,
 			wantStdout: `
 				Scanning dir ./fixtures/locks-many
-				Scanned %%/fixtures/locks-many/Gemfile.lock file and found 1 package
-				Scanned %%/fixtures/locks-many/alpine.cdx.xml as CycloneDX SBOM and found 15 packages
-				Scanned %%/fixtures/locks-many/composer.lock file and found 1 package
-				Scanned %%/fixtures/locks-many/package-lock.json file and found 1 package
-				Scanned %%/fixtures/locks-many/yarn.lock file and found 1 package
-				Loaded filter from: %%/fixtures/locks-many/osv-scanner.toml
+				Scanned <rootdir>/fixtures/locks-many/Gemfile.lock file and found 1 package
+				Scanned <rootdir>/fixtures/locks-many/alpine.cdx.xml as CycloneDX SBOM and found 15 packages
+				Scanned <rootdir>/fixtures/locks-many/composer.lock file and found 1 package
+				Scanned <rootdir>/fixtures/locks-many/package-lock.json file and found 1 package
+				Scanned <rootdir>/fixtures/locks-many/yarn.lock file and found 1 package
+				Loaded filter from: <rootdir>/fixtures/locks-many/osv-scanner.toml
 				GHSA-whgm-jr23-g3j9 has been filtered out because: Test manifest file
 				Filtered 1 vulnerability from output
 				No vulnerabilities found
@@ -217,11 +244,11 @@ func TestRun(t *testing.T) {
 			wantExitCode: 127,
 			wantStdout: `
 				Scanning dir ./fixtures/locks-many-with-invalid
-				Scanned %%/fixtures/locks-many-with-invalid/Gemfile.lock file and found 1 package
-				Scanned %%/fixtures/locks-many-with-invalid/yarn.lock file and found 1 package
+				Scanned <rootdir>/fixtures/locks-many-with-invalid/Gemfile.lock file and found 1 package
+				Scanned <rootdir>/fixtures/locks-many-with-invalid/yarn.lock file and found 1 package
 			`,
 			wantStderr: `
-				Attempted to scan lockfile but failed: %%/fixtures/locks-many-with-invalid/composer.lock
+				Attempted to scan lockfile but failed: <rootdir>/fixtures/locks-many-with-invalid/composer.lock
 			`,
 		},
 		// only the files in the given directories are checked by default (no recursion)
@@ -231,7 +258,7 @@ func TestRun(t *testing.T) {
 			wantExitCode: 0,
 			wantStdout: `
 				Scanning dir ./fixtures/locks-one-with-nested
-				Scanned %%/fixtures/locks-one-with-nested/yarn.lock file and found 1 package
+				Scanned <rootdir>/fixtures/locks-one-with-nested/yarn.lock file and found 1 package
 				No vulnerabilities found
 			`,
 			wantStderr: "",
@@ -243,8 +270,8 @@ func TestRun(t *testing.T) {
 			wantExitCode: 0,
 			wantStdout: `
 				Scanning dir ./fixtures/locks-one-with-nested
-				Scanned %%/fixtures/locks-one-with-nested/nested/composer.lock file and found 1 package
-				Scanned %%/fixtures/locks-one-with-nested/yarn.lock file and found 1 package
+				Scanned <rootdir>/fixtures/locks-one-with-nested/nested/composer.lock file and found 1 package
+				Scanned <rootdir>/fixtures/locks-one-with-nested/yarn.lock file and found 1 package
 				No vulnerabilities found
 			`,
 			wantStderr: "",
@@ -256,8 +283,8 @@ func TestRun(t *testing.T) {
 			wantExitCode: 0,
 			wantStdout: `
 				Scanning dir ./fixtures/locks-gitignore
-				Scanned %%/fixtures/locks-gitignore/Gemfile.lock file and found 1 package
-				Scanned %%/fixtures/locks-gitignore/subdir/yarn.lock file and found 1 package
+				Scanned <rootdir>/fixtures/locks-gitignore/Gemfile.lock file and found 1 package
+				Scanned <rootdir>/fixtures/locks-gitignore/subdir/yarn.lock file and found 1 package
 				No vulnerabilities found
 			`,
 			wantStderr: "",
@@ -269,14 +296,14 @@ func TestRun(t *testing.T) {
 			wantExitCode: 0,
 			wantStdout: `
 				Scanning dir ./fixtures/locks-gitignore
-				Scanned %%/fixtures/locks-gitignore/Gemfile.lock file and found 1 package
-				Scanned %%/fixtures/locks-gitignore/composer.lock file and found 1 package
-				Scanned %%/fixtures/locks-gitignore/ignored/Gemfile.lock file and found 1 package
-				Scanned %%/fixtures/locks-gitignore/ignored/yarn.lock file and found 1 package
-				Scanned %%/fixtures/locks-gitignore/subdir/Gemfile.lock file and found 1 package
-				Scanned %%/fixtures/locks-gitignore/subdir/composer.lock file and found 1 package
-				Scanned %%/fixtures/locks-gitignore/subdir/yarn.lock file and found 1 package
-				Scanned %%/fixtures/locks-gitignore/yarn.lock file and found 1 package
+				Scanned <rootdir>/fixtures/locks-gitignore/Gemfile.lock file and found 1 package
+				Scanned <rootdir>/fixtures/locks-gitignore/composer.lock file and found 1 package
+				Scanned <rootdir>/fixtures/locks-gitignore/ignored/Gemfile.lock file and found 1 package
+				Scanned <rootdir>/fixtures/locks-gitignore/ignored/yarn.lock file and found 1 package
+				Scanned <rootdir>/fixtures/locks-gitignore/subdir/Gemfile.lock file and found 1 package
+				Scanned <rootdir>/fixtures/locks-gitignore/subdir/composer.lock file and found 1 package
+				Scanned <rootdir>/fixtures/locks-gitignore/subdir/yarn.lock file and found 1 package
+        Scanned <rootdir>/fixtures/locks-gitignore/yarn.lock file and found 1 package
 				No vulnerabilities found
 			`,
 			wantStderr: "",
@@ -293,7 +320,7 @@ func TestRun(t *testing.T) {
 			`,
 			wantStderr: `
 				Scanning dir ./fixtures/locks-many/composer.lock
-				Scanned %%/fixtures/locks-many/composer.lock file and found 1 package
+				Scanned <rootdir>/fixtures/locks-many/composer.lock file and found 1 package
 			`,
 		},
 		{
@@ -307,7 +334,7 @@ func TestRun(t *testing.T) {
 			`,
 			wantStderr: `
 				Scanning dir ./fixtures/locks-many/composer.lock
-				Scanned %%/fixtures/locks-many/composer.lock file and found 1 package
+				Scanned <rootdir>/fixtures/locks-many/composer.lock file and found 1 package
 			`,
 		},
 		// output format: sarif
@@ -336,7 +363,7 @@ func TestRun(t *testing.T) {
 			`, version.OSVVersion),
 			wantStderr: `
 				Scanning dir ./fixtures/locks-many/composer.lock
-				Scanned %%/fixtures/locks-many/composer.lock file and found 1 package
+				Scanned <rootdir>/fixtures/locks-many/composer.lock file and found 1 package
 			`,
 		},
 		{
@@ -368,8 +395,8 @@ func TestRun(t *testing.T) {
                             "GHSA-whgm-jr23-g3j9"
                           ],
                           "help": {
-                            "text": "\n**Your dependency is vulnerable to [CVE-2021-23424](https://osv.dev/vulnerability/CVE-2021-23424)**.\n\n\n\u003e ## [GHSA-whgm-jr23-g3j9](https://osv.dev/vulnerability/GHSA-whgm-jr23-g3j9)\n\u003e \n\u003e This affects all versions of package ansi-html. If an attacker provides a malicious string, it will get stuck processing the input for an extremely long time.\n\u003e \n\n\n---\n\n### Affected Packages\n| Source | Package Name | Package Version |\n| --- | --- | --- |\n| lockfile:/%%%%/fixtures/locks-many/package-lock.json | ansi-html | 0.0.1 |\n\n",
-                            "markdown": "\n**Your dependency is vulnerable to [CVE-2021-23424](https://osv.dev/vulnerability/CVE-2021-23424)**.\n\n\n\u003e ## [GHSA-whgm-jr23-g3j9](https://osv.dev/vulnerability/GHSA-whgm-jr23-g3j9)\n\u003e \n\u003e This affects all versions of package ansi-html. If an attacker provides a malicious string, it will get stuck processing the input for an extremely long time.\n\u003e \n\n\n---\n\n### Affected Packages\n| Source | Package Name | Package Version |\n| --- | --- | --- |\n| lockfile:/%%%%/fixtures/locks-many/package-lock.json | ansi-html | 0.0.1 |\n\n"
+                            "text": "\n**Your dependency is vulnerable to [CVE-2021-23424](https://osv.dev/vulnerability/CVE-2021-23424)**.\n\n\n\u003e ## [GHSA-whgm-jr23-g3j9](https://osv.dev/vulnerability/GHSA-whgm-jr23-g3j9)\n\u003e \n\u003e This affects all versions of package ansi-html. If an attacker provides a malicious string, it will get stuck processing the input for an extremely long time.\n\u003e \n\n\n---\n\n### Affected Packages\n| Source | Package Name | Package Version |\n| --- | --- | --- |\n| lockfile:<rootdir>/fixtures/locks-many/package-lock.json | ansi-html | 0.0.1 |\n\n",
+                            "markdown": "\n**Your dependency is vulnerable to [CVE-2021-23424](https://osv.dev/vulnerability/CVE-2021-23424)**.\n\n\n\u003e ## [GHSA-whgm-jr23-g3j9](https://osv.dev/vulnerability/GHSA-whgm-jr23-g3j9)\n\u003e \n\u003e This affects all versions of package ansi-html. If an attacker provides a malicious string, it will get stuck processing the input for an extremely long time.\n\u003e \n\n\n---\n\n### Affected Packages\n| Source | Package Name | Package Version |\n| --- | --- | --- |\n| lockfile:<rootdir>/fixtures/locks-many/package-lock.json | ansi-html | 0.0.1 |\n\n"
                           }
                         }
                       ],
@@ -379,7 +406,7 @@ func TestRun(t *testing.T) {
                   "artifacts": [
                     {
                       "location": {
-                        "uri": "file://%%%%/fixtures/locks-many/package-lock.json"
+                        "uri": "file://<rootdir>/fixtures/locks-many/package-lock.json"
                       },
                       "length": -1
                     }
@@ -396,7 +423,7 @@ func TestRun(t *testing.T) {
                         {
                           "physicalLocation": {
                             "artifactLocation": {
-                              "uri": "file://%%%%/fixtures/locks-many/package-lock.json"
+                              "uri": "file://<rootdir>/fixtures/locks-many/package-lock.json"
                             }
                           }
                         }
@@ -409,7 +436,7 @@ func TestRun(t *testing.T) {
 			`, version.OSVVersion),
 			wantStderr: `
 				Scanning dir ./fixtures/locks-many/package-lock.json
-				Scanned %%/fixtures/locks-many/package-lock.json file and found 1 package
+				Scanned <rootdir>/fixtures/locks-many/package-lock.json file and found 1 package
 			`,
 		},
 		// output format: gh-annotations
@@ -420,7 +447,7 @@ func TestRun(t *testing.T) {
 			wantStdout:   ``,
 			wantStderr: `
 				Scanning dir ./fixtures/locks-many/composer.lock
-				Scanned %%/fixtures/locks-many/composer.lock file and found 1 package
+				Scanned <rootdir>/fixtures/locks-many/composer.lock file and found 1 package
 			`,
 		},
 		{
@@ -430,7 +457,7 @@ func TestRun(t *testing.T) {
 			wantStdout:   ``,
 			wantStderr: `
 				Scanning dir ./fixtures/locks-many/package-lock.json
-				Scanned %%/fixtures/locks-many/package-lock.json file and found 1 package
+				Scanned <rootdir>/fixtures/locks-many/package-lock.json file and found 1 package
 				::error file=fixtures/locks-many/package-lock.json::fixtures/locks-many/package-lock.json%0A+-----------+-------------------------------------+------+-----------------+---------------+%0A| PACKAGE   | VULNERABILITY ID                    | CVSS | CURRENT VERSION | FIXED VERSION |%0A+-----------+-------------------------------------+------+-----------------+---------------+%0A| ansi-html | https://osv.dev/GHSA-whgm-jr23-g3j9 | 7.5  | 0.0.1           | 0.0.8         |%0A+-----------+-------------------------------------+------+-----------------+---------------+
 			`,
 		},
@@ -441,7 +468,7 @@ func TestRun(t *testing.T) {
 			wantExitCode: 0,
 			wantStdout: `
 				Scanning dir ./fixtures/locks-many/composer.lock
-				Scanned %%/fixtures/locks-many/composer.lock file and found 1 package
+				Scanned <rootdir>/fixtures/locks-many/composer.lock file and found 1 package
 				No vulnerabilities found
 			`,
 			wantStderr: "",
@@ -491,7 +518,7 @@ func TestRun_LockfileWithExplicitParseAs(t *testing.T) {
 			},
 			wantExitCode: 0,
 			wantStdout: `
-				Scanned %%/fixtures/locks-many/composer.lock file and found 1 package
+				Scanned <rootdir>/fixtures/locks-many/composer.lock file and found 1 package
 				No vulnerabilities found
 			`,
 			wantStderr: "",
@@ -507,7 +534,7 @@ func TestRun_LockfileWithExplicitParseAs(t *testing.T) {
 			wantExitCode: 127,
 			wantStdout:   "",
 			wantStderr: `
-				open %%/path/to/my:file: no such file or directory
+				open <rootdir>/path/to/my:file: no such file or directory
 			`,
 		},
 		{
@@ -520,7 +547,7 @@ func TestRun_LockfileWithExplicitParseAs(t *testing.T) {
 			wantExitCode: 127,
 			wantStdout:   "",
 			wantStderr: `
-				open %%/path/to/my:project/package-lock.json: no such file or directory
+				open <rootdir>/path/to/my:project/package-lock.json: no such file or directory
 			`,
 		},
 		// when an explicit parse-as is given, it's applied to that file
@@ -534,9 +561,9 @@ func TestRun_LockfileWithExplicitParseAs(t *testing.T) {
 			},
 			wantExitCode: 1,
 			wantStdout: `
-				Scanned %%/fixtures/locks-insecure/my-package-lock.json file as a package-lock.json and found 1 package
+				Scanned <rootdir>/fixtures/locks-insecure/my-package-lock.json file as a package-lock.json and found 1 package
 				Scanning dir ./fixtures/locks-insecure
-				Scanned %%/fixtures/locks-insecure/composer.lock file and found 0 packages
+				Scanned <rootdir>/fixtures/locks-insecure/composer.lock file and found 0 packages
 				+-------------------------------------+------+-----------+-----------+---------+----------------------------------------------+
 				| OSV URL                             | CVSS | ECOSYSTEM | PACKAGE   | VERSION | SOURCE                                       |
 				+-------------------------------------+------+-----------+-----------+---------+----------------------------------------------+
@@ -556,10 +583,10 @@ func TestRun_LockfileWithExplicitParseAs(t *testing.T) {
 			},
 			wantExitCode: 1,
 			wantStdout: `
-				Scanned %%/fixtures/locks-insecure/my-package-lock.json file as a package-lock.json and found 1 package
-				Scanned %%/fixtures/locks-insecure/my-yarn.lock file as a yarn.lock and found 1 package
+				Scanned <rootdir>/fixtures/locks-insecure/my-package-lock.json file as a package-lock.json and found 1 package
+				Scanned <rootdir>/fixtures/locks-insecure/my-yarn.lock file as a yarn.lock and found 1 package
 				Scanning dir ./fixtures/locks-insecure
-				Scanned %%/fixtures/locks-insecure/composer.lock file and found 0 packages
+				Scanned <rootdir>/fixtures/locks-insecure/composer.lock file and found 0 packages
 				+-------------------------------------+------+-----------+-----------+---------+----------------------------------------------+
 				| OSV URL                             | CVSS | ECOSYSTEM | PACKAGE   | VERSION | SOURCE                                       |
 				+-------------------------------------+------+-----------+-----------+---------+----------------------------------------------+
@@ -579,10 +606,10 @@ func TestRun_LockfileWithExplicitParseAs(t *testing.T) {
 			},
 			wantExitCode: 1,
 			wantStdout: `
-				Scanned %%/fixtures/locks-insecure/my-yarn.lock file as a yarn.lock and found 1 package
-				Scanned %%/fixtures/locks-insecure/my-package-lock.json file as a package-lock.json and found 1 package
+				Scanned <rootdir>/fixtures/locks-insecure/my-yarn.lock file as a yarn.lock and found 1 package
+				Scanned <rootdir>/fixtures/locks-insecure/my-package-lock.json file as a package-lock.json and found 1 package
 				Scanning dir ./fixtures/locks-insecure
-				Scanned %%/fixtures/locks-insecure/composer.lock file and found 0 packages
+				Scanned <rootdir>/fixtures/locks-insecure/composer.lock file and found 0 packages
 				+-------------------------------------+------+-----------+-----------+---------+----------------------------------------------+
 				| OSV URL                             | CVSS | ECOSYSTEM | PACKAGE   | VERSION | SOURCE                                       |
 				+-------------------------------------+------+-----------+-----------+---------+----------------------------------------------+
@@ -605,7 +632,7 @@ func TestRun_LockfileWithExplicitParseAs(t *testing.T) {
 			wantExitCode: 127,
 			wantStdout:   "",
 			wantStderr: `
-				(extracting as Cargo.lock) could not extract from %%/fixtures/locks-insecure/my-package-lock.json: toml: line 1: expected '.' or '=', but got '{' instead
+				(extracting as Cargo.lock) could not extract from <rootdir>/fixtures/locks-insecure/my-package-lock.json: toml: line 1: expected '.' or '=', but got '{' instead
 			`,
 		},
 		// parse-as takes priority, even if it's wrong
@@ -619,7 +646,7 @@ func TestRun_LockfileWithExplicitParseAs(t *testing.T) {
 			wantExitCode: 127,
 			wantStdout:   "",
 			wantStderr: `
-				(extracting as package-lock.json) could not extract from %%/fixtures/locks-many/yarn.lock: invalid character '#' looking for beginning of value
+				(extracting as package-lock.json) could not extract from <rootdir>/fixtures/locks-many/yarn.lock: invalid character '#' looking for beginning of value
 			`,
 		},
 		// "apk-installed" is supported
@@ -632,7 +659,7 @@ func TestRun_LockfileWithExplicitParseAs(t *testing.T) {
 			},
 			wantExitCode: 0,
 			wantStdout: `
-				Scanned %%/fixtures/locks-many/installed file as a apk-installed and found 1 package
+				Scanned <rootdir>/fixtures/locks-many/installed file as a apk-installed and found 1 package
 				No vulnerabilities found
 			`,
 			wantStderr: "",
@@ -647,7 +674,7 @@ func TestRun_LockfileWithExplicitParseAs(t *testing.T) {
 			},
 			wantExitCode: 0,
 			wantStdout: `
-				Scanned %%/fixtures/locks-many/status file as a dpkg-status and found 1 package
+				Scanned <rootdir>/fixtures/locks-many/status file as a dpkg-status and found 1 package
 				No vulnerabilities found
 			`,
 			wantStderr: "",
@@ -674,7 +701,7 @@ func TestRun_LocalDatabases(t *testing.T) {
 			wantExitCode: 0,
 			wantStdout: `
 				Scanning dir ./fixtures/locks-many/composer.lock
-				Scanned %%/fixtures/locks-many/composer.lock file and found 1 package
+				Scanned <rootdir>/fixtures/locks-many/composer.lock file and found 1 package
 				Loaded Packagist local db from %%/osv-scanner/Packagist/all.zip
 				No vulnerabilities found
 			`,
@@ -687,7 +714,7 @@ func TestRun_LocalDatabases(t *testing.T) {
 			wantExitCode: 1,
 			wantStdout: `
 				Scanning dir ./fixtures/sbom-insecure/postgres-stretch.cdx.xml
-				Scanned %%/fixtures/sbom-insecure/postgres-stretch.cdx.xml as CycloneDX SBOM and found 136 packages
+				Scanned <rootdir>/fixtures/sbom-insecure/postgres-stretch.cdx.xml as CycloneDX SBOM and found 136 packages
 				Loaded Debian local db from %%/osv-scanner/Debian/all.zip
 				Loaded Go local db from %%/osv-scanner/Go/all.zip
 				Loaded OSS-Fuzz local db from %%/osv-scanner/OSS-Fuzz/all.zip
@@ -723,16 +750,16 @@ func TestRun_LocalDatabases(t *testing.T) {
 			wantExitCode: 0,
 			wantStdout: `
 				Scanning dir ./fixtures/locks-many
-				Scanned %%/fixtures/locks-many/Gemfile.lock file and found 1 package
-				Scanned %%/fixtures/locks-many/alpine.cdx.xml as CycloneDX SBOM and found 15 packages
-				Scanned %%/fixtures/locks-many/composer.lock file and found 1 package
-				Scanned %%/fixtures/locks-many/package-lock.json file and found 1 package
-				Scanned %%/fixtures/locks-many/yarn.lock file and found 1 package
+				Scanned <rootdir>/fixtures/locks-many/Gemfile.lock file and found 1 package
+				Scanned <rootdir>/fixtures/locks-many/alpine.cdx.xml as CycloneDX SBOM and found 15 packages
+				Scanned <rootdir>/fixtures/locks-many/composer.lock file and found 1 package
+				Scanned <rootdir>/fixtures/locks-many/package-lock.json file and found 1 package
+				Scanned <rootdir>/fixtures/locks-many/yarn.lock file and found 1 package
 				Loaded RubyGems local db from %%/osv-scanner/RubyGems/all.zip
 				Loaded Alpine local db from %%/osv-scanner/Alpine/all.zip
 				Loaded Packagist local db from %%/osv-scanner/Packagist/all.zip
 				Loaded npm local db from %%/osv-scanner/npm/all.zip
-				Loaded filter from: %%/fixtures/locks-many/osv-scanner.toml
+				Loaded filter from: <rootdir>/fixtures/locks-many/osv-scanner.toml
 				GHSA-whgm-jr23-g3j9 has been filtered out because: Test manifest file
 				Filtered 1 vulnerability from output
 				No vulnerabilities found
@@ -746,13 +773,13 @@ func TestRun_LocalDatabases(t *testing.T) {
 			wantExitCode: 127,
 			wantStdout: `
 				Scanning dir ./fixtures/locks-many-with-invalid
-				Scanned %%/fixtures/locks-many-with-invalid/Gemfile.lock file and found 1 package
-				Scanned %%/fixtures/locks-many-with-invalid/yarn.lock file and found 1 package
+				Scanned <rootdir>/fixtures/locks-many-with-invalid/Gemfile.lock file and found 1 package
+				Scanned <rootdir>/fixtures/locks-many-with-invalid/yarn.lock file and found 1 package
 				Loaded RubyGems local db from %%/osv-scanner/RubyGems/all.zip
 				Loaded npm local db from %%/osv-scanner/npm/all.zip
 			`,
 			wantStderr: `
-				Attempted to scan lockfile but failed: %%/fixtures/locks-many-with-invalid/composer.lock
+				Attempted to scan lockfile but failed: <rootdir>/fixtures/locks-many-with-invalid/composer.lock
 			`,
 		},
 		// only the files in the given directories are checked by default (no recursion)
@@ -762,7 +789,7 @@ func TestRun_LocalDatabases(t *testing.T) {
 			wantExitCode: 0,
 			wantStdout: `
 				Scanning dir ./fixtures/locks-one-with-nested
-				Scanned %%/fixtures/locks-one-with-nested/yarn.lock file and found 1 package
+				Scanned <rootdir>/fixtures/locks-one-with-nested/yarn.lock file and found 1 package
 				Loaded npm local db from %%/osv-scanner/npm/all.zip
 				No vulnerabilities found
 			`,
@@ -775,8 +802,8 @@ func TestRun_LocalDatabases(t *testing.T) {
 			wantExitCode: 0,
 			wantStdout: `
 				Scanning dir ./fixtures/locks-one-with-nested
-				Scanned %%/fixtures/locks-one-with-nested/nested/composer.lock file and found 1 package
-				Scanned %%/fixtures/locks-one-with-nested/yarn.lock file and found 1 package
+				Scanned <rootdir>/fixtures/locks-one-with-nested/nested/composer.lock file and found 1 package
+				Scanned <rootdir>/fixtures/locks-one-with-nested/yarn.lock file and found 1 package
 				Loaded Packagist local db from %%/osv-scanner/Packagist/all.zip
 				Loaded npm local db from %%/osv-scanner/npm/all.zip
 				No vulnerabilities found
@@ -790,8 +817,8 @@ func TestRun_LocalDatabases(t *testing.T) {
 			wantExitCode: 0,
 			wantStdout: `
 				Scanning dir ./fixtures/locks-gitignore
-				Scanned %%/fixtures/locks-gitignore/Gemfile.lock file and found 1 package
-				Scanned %%/fixtures/locks-gitignore/subdir/yarn.lock file and found 1 package
+				Scanned <rootdir>/fixtures/locks-gitignore/Gemfile.lock file and found 1 package
+				Scanned <rootdir>/fixtures/locks-gitignore/subdir/yarn.lock file and found 1 package
 				Loaded RubyGems local db from %%/osv-scanner/RubyGems/all.zip
 				Loaded npm local db from %%/osv-scanner/npm/all.zip
 				No vulnerabilities found
@@ -805,14 +832,14 @@ func TestRun_LocalDatabases(t *testing.T) {
 			wantExitCode: 0,
 			wantStdout: `
 				Scanning dir ./fixtures/locks-gitignore
-				Scanned %%/fixtures/locks-gitignore/Gemfile.lock file and found 1 package
-				Scanned %%/fixtures/locks-gitignore/composer.lock file and found 1 package
-				Scanned %%/fixtures/locks-gitignore/ignored/Gemfile.lock file and found 1 package
-				Scanned %%/fixtures/locks-gitignore/ignored/yarn.lock file and found 1 package
-				Scanned %%/fixtures/locks-gitignore/subdir/Gemfile.lock file and found 1 package
-				Scanned %%/fixtures/locks-gitignore/subdir/composer.lock file and found 1 package
-				Scanned %%/fixtures/locks-gitignore/subdir/yarn.lock file and found 1 package
-				Scanned %%/fixtures/locks-gitignore/yarn.lock file and found 1 package
+				Scanned <rootdir>/fixtures/locks-gitignore/Gemfile.lock file and found 1 package
+				Scanned <rootdir>/fixtures/locks-gitignore/composer.lock file and found 1 package
+				Scanned <rootdir>/fixtures/locks-gitignore/ignored/Gemfile.lock file and found 1 package
+				Scanned <rootdir>/fixtures/locks-gitignore/ignored/yarn.lock file and found 1 package
+				Scanned <rootdir>/fixtures/locks-gitignore/subdir/Gemfile.lock file and found 1 package
+				Scanned <rootdir>/fixtures/locks-gitignore/subdir/composer.lock file and found 1 package
+				Scanned <rootdir>/fixtures/locks-gitignore/subdir/yarn.lock file and found 1 package
+				Scanned <rootdir>/fixtures/locks-gitignore/yarn.lock file and found 1 package
 				Loaded RubyGems local db from %%/osv-scanner/RubyGems/all.zip
 				Loaded Packagist local db from %%/osv-scanner/Packagist/all.zip
 				Loaded npm local db from %%/osv-scanner/npm/all.zip
@@ -832,7 +859,7 @@ func TestRun_LocalDatabases(t *testing.T) {
 			`,
 			wantStderr: `
 				Scanning dir ./fixtures/locks-many/composer.lock
-				Scanned %%/fixtures/locks-many/composer.lock file and found 1 package
+				Scanned <rootdir>/fixtures/locks-many/composer.lock file and found 1 package
 				Loaded Packagist local db from %%/osv-scanner/Packagist/all.zip
 			`,
 		},
@@ -847,7 +874,7 @@ func TestRun_LocalDatabases(t *testing.T) {
 			`,
 			wantStderr: `
 				Scanning dir ./fixtures/locks-many/composer.lock
-				Scanned %%/fixtures/locks-many/composer.lock file and found 1 package
+				Scanned <rootdir>/fixtures/locks-many/composer.lock file and found 1 package
 				Loaded Packagist local db from %%/osv-scanner/Packagist/all.zip
 			`,
 		},
@@ -858,7 +885,7 @@ func TestRun_LocalDatabases(t *testing.T) {
 			wantExitCode: 0,
 			wantStdout: `
 				Scanning dir ./fixtures/locks-many/composer.lock
-				Scanned %%/fixtures/locks-many/composer.lock file and found 1 package
+				Scanned <rootdir>/fixtures/locks-many/composer.lock file and found 1 package
 				Loaded Packagist local db from %%/osv-scanner/Packagist/all.zip
 				No vulnerabilities found
 			`,
diff --git a/internal/local/zip_test.go b/internal/local/zip_test.go
index e8bba96..d39385e 100644
--- a/internal/local/zip_test.go
+++ b/internal/local/zip_test.go
@@ -38,7 +38,7 @@ func expectDBToHaveOSVs(
 	db interface {
 		Vulnerabilities(includeWithdrawn bool) []models.Vulnerability
 	},
-	actual []models.Vulnerability,
+	expect []models.Vulnerability,
 ) {
 	t.Helper()
 
@@ -47,12 +47,12 @@ func expectDBToHaveOSVs(
 	sort.Slice(vulns, func(i, j int) bool {
 		return vulns[i].ID < vulns[j].ID
 	})
-	sort.Slice(actual, func(i, j int) bool {
-		return actual[i].ID < actual[j].ID
+	sort.Slice(expect, func(i, j int) bool {
+		return expect[i].ID < expect[j].ID
 	})
 
-	if !reflect.DeepEqual(vulns, actual) {
-		t.Errorf("db is missing some vulnerabilities: %v vs %v", vulns, actual)
+	if !reflect.DeepEqual(expect, vulns) {
+		t.Errorf("db is missing some vulnerabilities: %v vs %v", expect, vulns)
 	}
 }