Files
syft/cmd/syft/internal/options/writer_test.go
T
Alex Goodman 6c9bb732dd ci: run the full unit suite on windows (#5370)
* ci: run the full unit suite on windows via make unit

Drops the curated `unit:windows` subset; the windows job now bootstraps like linux and runs `make unit`. Exceptions get added as failures show up.

Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>

* ci: skip fixture refresh for windows unit tests

binny has no windows release, so the bootstrap action can't install the tools the fixture refresh needs.

Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>

* test: skip linux container fixtures when docker can't run them

Windows CI runners only run windows containers, so image fixtures go through `testutils.SkipWithoutLinuxContainers`. No behavior change on linux/mac.

Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>

* fix: close leaked file handles and posix squashfs paths on windows

- output writers are now closed on early returns in `scan`/`convert` (open handles block cleanup on windows)
- `newSBOMMultiWriter` used `path.Dir` on host paths, so output subdirectories were never created on windows
- squashfs walk paths were built with `filepath.Join`
- skip a few posix-only test cases (dir perms, xdg fallback, go-rpmdb ndb handle leak upstream)

Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>

* fix(golang): resolve go source module dir natively on windows

- `parseGoModFile` passed the resolver's posix (`/c/...`) path to the go toolchain, so go source resolution silently found nothing on windows
- skip container-built fixtures (go archs, java builds) and golden image tarballs on windows; stereoscope's layer cache filenames contain `:`, which windows rejects

Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>

* fix: keep posix paths posix on windows

- `ChrootContext.ToChrootPath` nested volumes (`D:\cwd\C:\root`) when the scan root was on another drive, so the prefix was never trimmed
- bitnami, python wheel/egg, and alpm parsers used `filepath` on posix location paths
- alpm backup-file lookups silently missed on windows

Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>

* ci: pull the test fixture cache for windows unit tests

Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>

* fix: don't map virtual file tree paths through the host on windows

`FiletreeResolver` without a chroot (snaps) ran request paths through `filepath.Abs`, rooting them on the current volume (`/payload.txt` -> `/d/payload.txt`), so snap scans on windows found nothing.

Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>

* fix: match tar entry globs as posix on windows

- `matchesAnyGlob` used `doublestar.PathMatch` (host separator) on tar entry names, so tar-wrapped java archives silently produced nothing on windows
- test helpers now treat binary snippet keys and `FromFile` locations as posix

Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>

* incorporate windows path fix

Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>

* incorporate go-rpmdb close fix

Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>

* close squashfs fixture writers before finalizing

- windows dir listings can report a zero size for files still open, so the manifest was packed empty

(also adds a temporary windows diagnostic for the binary cataloger)

Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>

* temp: surface windows binary cataloger diagnostics

Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>

* fix: keep directory symlink targets posix on windows

- the directory indexer joined symlink targets with native separators, so links in the posix file tree pointed nowhere and globs on link names found nothing
- binary fixture logical keys used native separators, so nested snippets were picked as the fixture dir on windows

Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>

* keep addSymlinkToIndex under the funlen limit

Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>

* drop windows skips covered by the stereoscope and go-rpmdb bumps

Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>

---------

Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>
2026-10-05 10:41:03 -04:00

297 lines
5.8 KiB
Go

package options
import (
"io"
"path/filepath"
"strings"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/anchore/go-homedir"
"github.com/anchore/syft/syft/sbom"
)
func Test_MakeSBOMWriter(t *testing.T) {
tests := []struct {
name string
outputs []string
wantErr assert.ErrorAssertionFunc
}{
{
name: "go case",
outputs: []string{"json"},
wantErr: assert.NoError,
},
{
name: "multiple",
outputs: []string{"table", "json"},
wantErr: assert.NoError,
},
{
name: "unknown format",
outputs: []string{"unknown"},
wantErr: func(t assert.TestingT, err error, bla ...any) bool {
return assert.ErrorContains(t, err, `unsupported output format "unknown", supported formats are:`)
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
opt := DefaultOutput()
require.NoError(t, opt.Format.PostLoad())
encoders, err := opt.Encoders()
require.NoError(t, err)
_, err = makeSBOMWriter(tt.outputs, "", encoders)
tt.wantErr(t, err)
})
}
}
func dummyFormat(name string) sbom.FormatEncoder {
return dummyEncoder{name: name}
}
var _ sbom.FormatEncoder = (*dummyEncoder)(nil)
type dummyEncoder struct {
name string
}
func (d dummyEncoder) ID() sbom.FormatID {
return sbom.FormatID(d.name)
}
func (d dummyEncoder) Aliases() []string {
return nil
}
func (d dummyEncoder) Version() string {
return sbom.AnyVersion
}
func (d dummyEncoder) Encode(writer io.Writer, s sbom.SBOM) error {
return nil
}
func Test_newSBOMMultiWriter(t *testing.T) {
type writerConfig struct {
format string
file string
}
tmp := t.TempDir()
testName := func(options []sbomWriterDescription, err bool) string {
var out []string
for _, opt := range options {
out = append(out, string(opt.Format.ID())+"="+opt.Path)
}
errs := ""
if err {
errs = "(err)"
}
return strings.Join(out, ", ") + errs
}
tests := []struct {
outputs []sbomWriterDescription
err bool
expected []writerConfig
}{
{
outputs: []sbomWriterDescription{},
err: true,
},
{
outputs: []sbomWriterDescription{
{
Format: dummyFormat("table"),
Path: "",
},
},
expected: []writerConfig{
{
format: "table",
},
},
},
{
outputs: []sbomWriterDescription{
{
Format: dummyFormat("json"),
},
},
expected: []writerConfig{
{
format: "json",
},
},
},
{
outputs: []sbomWriterDescription{
{
Format: dummyFormat("json"),
Path: "test-2.json",
},
},
expected: []writerConfig{
{
format: "json",
file: "test-2.json",
},
},
},
{
outputs: []sbomWriterDescription{
{
Format: dummyFormat("json"),
Path: "test-3/1.json",
},
{
Format: dummyFormat("spdx-json"),
Path: "test-3/2.json",
},
},
expected: []writerConfig{
{
format: "json",
file: "test-3/1.json",
},
{
format: "spdx-json",
file: "test-3/2.json",
},
},
},
{
outputs: []sbomWriterDescription{
{
Format: dummyFormat("text"),
},
{
Format: dummyFormat("spdx-json"),
Path: "test-4.json",
},
},
expected: []writerConfig{
{
format: "text",
},
{
format: "spdx-json",
file: "test-4.json",
},
},
},
}
for _, test := range tests {
t.Run(testName(test.outputs, test.err), func(t *testing.T) {
outputs := test.outputs
for i := range outputs {
if outputs[i].Path != "" {
outputs[i].Path = tmp + outputs[i].Path
}
}
mw, err := newSBOMMultiWriter(outputs...)
if test.err {
assert.Error(t, err)
return
}
require.NoError(t, err)
t.Cleanup(func() { _ = mw.Close() })
assert.Len(t, mw.writers, len(test.expected))
for i, e := range test.expected {
switch w := mw.writers[i].(type) {
case *sbomStreamWriter:
assert.Equal(t, string(w.format.ID()), e.format)
assert.NotNil(t, w.out)
if e.file != "" {
assert.FileExists(t, tmp+e.file)
}
case *sbomPublisher:
assert.Equal(t, string(w.format.ID()), e.format)
default:
t.Fatalf("unknown writer type: %T", w)
}
}
})
}
}
func Test_newSBOMWriterDescription(t *testing.T) {
h, err := homedir.Dir()
require.NoError(t, err)
tests := []struct {
name string
path string
expected string
}{
{
name: "expand home dir",
path: "~/place.txt",
expected: filepath.Join(h, "place.txt"),
},
{
name: "passthrough other paths",
path: "/other/place.txt",
expected: "/other/place.txt",
},
{
name: "no path",
path: "",
expected: "",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
o := newSBOMWriterDescription(dummyFormat("table"), tt.path)
assert.Equal(t, tt.expected, o.Path)
})
}
}
func Test_formatVersionOptions(t *testing.T) {
tests := []struct {
name string
nameVersionPairs []string
want string
}{
{
name: "gocase",
nameVersionPairs: []string{
"cyclonedx-json@1.2", "cyclonedx-json@1.3", "cyclonedx-json@1.4", "cyclonedx-json@1.5",
"cyclonedx-xml@1.0", "cyclonedx-xml@1.1", "cyclonedx-xml@1.2", "cyclonedx-xml@1.3",
"cyclonedx-xml@1.4", "cyclonedx-xml@1.5", "github-json", "spdx-json@2.2", "spdx-json@2.3",
"spdx-tag-value@2.1", "spdx-tag-value@2.2", "spdx-tag-value@2.3", "syft-json@11.0.0",
"syft-table", "syft-text", "template",
},
want: `
Available formats:
- cyclonedx-json @ 1.2, 1.3, 1.4, 1.5
- cyclonedx-xml @ 1.0, 1.1, 1.2, 1.3, 1.4, 1.5
- github-json
- spdx-json @ 2.2, 2.3
- spdx-tag-value @ 2.1, 2.2, 2.3
- syft-json
- syft-table
- syft-text
- template`,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
assert.Equal(t, tt.want, formatVersionOptions(tt.nameVersionPairs))
})
}
}