Script 'mail_helper' called by obssrc Hello community, here is the log from the commit of package grant for openSUSE:Factory checked in at 2026-02-11 18:48:47 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Comparing /work/SRC/openSUSE:Factory/grant (Old) and /work/SRC/openSUSE:Factory/.grant.new.1670 (New) ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Package is "grant" Wed Feb 11 18:48:47 2026 rev:19 rq:1332378 version:0.6.0 Changes: -------- --- /work/SRC/openSUSE:Factory/grant/grant.changes 2026-02-04 21:08:26.922508456 +0100 +++ /work/SRC/openSUSE:Factory/.grant.new.1670/grant.changes 2026-02-11 18:50:04.144455293 +0100 @@ -1,0 +2,25 @@ +Wed Feb 11 05:55:01 UTC 2026 - Johannes Kastl <[email protected]> + +- Update to version 0.6.0: + * Added Features + - bug: Support new opensource.org license URLs [#208] + * Bug Fixes + - skip dir symlinks in grant license search [#399 @spiffcs] + - bug: "[0090] ERROR unable to classify license: unable to + read" on symlinkx [#70] + - bug: [0000] ERROR unable to determine case for -: unable to + determine SBOM or licenses for stdin: sbom format not + recognized [#215 #397 @spiffcs] + * Dependencies + - chore(deps): update anchore dependencies (#403) + - chore(deps): bump github.com/go-git/go-git/v5 from 5.16.4 to + 5.16.5 (#402) + - chore(deps): bump zizmorcore/zizmor-action from 0.4.1 to + 0.5.0 (#400) + - chore(deps): update tools to latest versions (#395) + - chore(deps): bump actions/cache in /.github/actions/bootstrap + (#396) + - chore(deps): bump docker/login-action from 3.6.0 to 3.7.0 + (#394) + +------------------------------------------------------------------- Old: ---- grant-0.5.8.obscpio New: ---- grant-0.6.0.obscpio ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Other differences: ------------------ ++++++ grant.spec ++++++ --- /var/tmp/diff_new_pack.kBNHfG/_old 2026-02-11 18:50:06.148539454 +0100 +++ /var/tmp/diff_new_pack.kBNHfG/_new 2026-02-11 18:50:06.152539622 +0100 @@ -17,7 +17,7 @@ Name: grant -Version: 0.5.8 +Version: 0.6.0 Release: 0 Summary: Search an SBOM for licenses and the packages they belong to License: Apache-2.0 ++++++ _service ++++++ --- /var/tmp/diff_new_pack.kBNHfG/_old 2026-02-11 18:50:06.240543317 +0100 +++ /var/tmp/diff_new_pack.kBNHfG/_new 2026-02-11 18:50:06.272544661 +0100 @@ -3,7 +3,7 @@ <param name="url">https://github.com/anchore/grant</param> <param name="scm">git</param> <param name="exclude">.git</param> - <param name="revision">v0.5.8</param> + <param name="revision">v0.6.0</param> <param name="versionformat">@PARENT_TAG@</param> <param name="versionrewrite-pattern">v(.*)</param> <param name="changesgenerate">enable</param> ++++++ _servicedata ++++++ --- /var/tmp/diff_new_pack.kBNHfG/_old 2026-02-11 18:50:06.340547516 +0100 +++ /var/tmp/diff_new_pack.kBNHfG/_new 2026-02-11 18:50:06.348547853 +0100 @@ -1,6 +1,6 @@ <servicedata> <service name="tar_scm"> <param name="url">https://github.com/anchore/grant</param> - <param name="changesrevision">351740eac7b055feceee7e874647288660dbefba</param></service></servicedata> + <param name="changesrevision">f071e8d6aa0f126561e75637e46a90106b5301af</param></service></servicedata> (No newline at EOF) ++++++ grant-0.5.8.obscpio -> grant-0.6.0.obscpio ++++++ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/grant-0.5.8/.binny.yaml new/grant-0.6.0/.binny.yaml --- old/grant-0.5.8/.binny.yaml 2026-02-03 19:29:04.000000000 +0100 +++ new/grant-0.6.0/.binny.yaml 2026-02-10 18:55:25.000000000 +0100 @@ -15,7 +15,7 @@ - name: binny version: - want: v0.11.1 + want: v0.11.2 method: github-release with: repo: anchore/binny @@ -93,7 +93,7 @@ - name: syft version: - want: v1.41.1 + want: v1.41.2 method: github-release with: repo: anchore/syft diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/grant-0.5.8/cmd/grant/cli/command/check.go new/grant-0.6.0/cmd/grant/cli/command/check.go --- old/grant-0.5.8/cmd/grant/cli/command/check.go 2026-02-03 19:29:04.000000000 +0100 +++ new/grant-0.6.0/cmd/grant/cli/command/check.go 2026-02-10 18:55:25.000000000 +0100 @@ -11,6 +11,7 @@ "github.com/anchore/grant/cmd/grant/cli/internal" "github.com/anchore/grant/grant" + "github.com/anchore/grant/internal/input" ) type checkFlags struct { @@ -43,7 +44,7 @@ Exit codes: - 0: All targets are compliant - 1: One or more targets are non-compliant or an error occurred`, - Args: cobra.MinimumNArgs(1), + Args: cobra.ArbitraryArgs, RunE: runCheck, } @@ -56,33 +57,55 @@ return cmd } +// parseCheckArgumentsWithStdin resolves targets, defaulting to stdin when no args are provided and stdin is available. +func parseCheckArgumentsWithStdin(args []string) ([]string, error) { + hasStdin, err := input.IsStdinPipeOrRedirect() + if err != nil { + return nil, err + } + + if len(args) == 0 { + if hasStdin { + return []string{"-"}, nil + } + return nil, fmt.Errorf("no target specified and no input available on stdin") + } + + return args, nil +} + // runCheck executes the check command func runCheck(cmd *cobra.Command, args []string) error { + targets, err := parseCheckArgumentsWithStdin(args) + if err != nil { + return err + } + globalConfig := GetGlobalConfig(cmd) flags := parseCheckFlags(cmd) // Check if input is grant JSON from stdin - if len(args) > 0 { - if grantResult, isGrantJSON := isGrantJSONInput(args[0]); isGrantJSON { + if len(targets) > 0 { + if grantResult, isGrantJSON := isGrantJSONInput(targets[0]); isGrantJSON { // Handle grant JSON input directly result := handleGrantJSONInput(grantResult, []string{}) // No license filters for check return handleCheckOutput(result, globalConfig, flags) } } - realtimeUI := setupRealtimeUI(globalConfig, args) + realtimeUI := setupRealtimeUI(globalConfig, targets) orchestrator, err := setupOrchestrator(globalConfig, flags.DisableFileSearch) if err != nil { return err } defer orchestrator.Close() - result, err := performCheck(orchestrator, globalConfig, args) + result, err := performCheck(orchestrator, globalConfig, targets) if err != nil { return err } - updateUIWithResults(realtimeUI, result, args) + updateUIWithResults(realtimeUI, result, targets) return handleCheckOutput(result, globalConfig, flags) } diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/grant-0.5.8/go.mod new/grant-0.6.0/go.mod --- old/grant-0.5.8/go.mod 2026-02-03 19:29:04.000000000 +0100 +++ new/grant-0.6.0/go.mod 2026-02-10 18:55:25.000000000 +0100 @@ -8,8 +8,8 @@ github.com/anchore/clio v0.0.0-20250319180342-2cfe4b0cb716 github.com/anchore/go-collections v0.0.0-20251016125210-a3c352120e8c github.com/anchore/go-logger v0.0.0-20250318195838-07ae343dd722 - github.com/anchore/stereoscope v0.1.19 - github.com/anchore/syft v1.41.2 + github.com/anchore/stereoscope v0.1.20 + github.com/anchore/syft v1.42.0 github.com/github/go-spdx/v2 v2.3.6 github.com/google/licenseclassifier/v2 v2.0.0 github.com/gookit/color v1.6.0 @@ -90,7 +90,7 @@ github.com/bgentry/go-netrc v0.0.0-20140422174119-9fd32a8b3d3d // indirect github.com/bitnami/go-version v0.0.0-20250131085805-b1f57a8634ef // indirect github.com/blakesmith/ar v0.0.0-20190502131153-809d4375e1fb // indirect - github.com/bmatcuk/doublestar/v4 v4.9.2 // indirect + github.com/bmatcuk/doublestar/v4 v4.10.0 // indirect github.com/bodgit/plumbing v1.3.0 // indirect github.com/bodgit/sevenzip v1.6.1 // indirect github.com/bodgit/windows v1.0.1 // indirect @@ -123,7 +123,7 @@ github.com/deitch/magic v0.0.0-20230404182410-1ff89d7342da // indirect github.com/diskfs/go-diskfs v1.7.0 // indirect github.com/distribution/reference v0.6.0 // indirect - github.com/docker/cli v29.1.5+incompatible // indirect + github.com/docker/cli v29.2.0+incompatible // indirect github.com/docker/distribution v2.8.3+incompatible // indirect github.com/docker/docker v28.5.2+incompatible // indirect github.com/docker/docker-credential-helpers v0.9.4 // indirect @@ -143,7 +143,7 @@ github.com/gabriel-vasile/mimetype v1.4.12 // indirect github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect github.com/go-git/go-billy/v5 v5.7.0 // indirect - github.com/go-git/go-git/v5 v5.16.4 // indirect + github.com/go-git/go-git/v5 v5.16.5 // indirect github.com/go-jose/go-jose/v4 v4.1.2 // indirect github.com/go-logr/logr v1.4.3 // indirect github.com/go-logr/stdr v1.2.2 // indirect diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/grant-0.5.8/go.sum new/grant-0.6.0/go.sum --- old/grant-0.5.8/go.sum 2026-02-03 19:29:04.000000000 +0100 +++ new/grant-0.6.0/go.sum 2026-02-10 18:55:25.000000000 +0100 @@ -148,10 +148,10 @@ github.com/anchore/go-version v1.2.2-0.20200701162849-18adb9c92b9b/go.mod h1:Bkc+JYWjMCF8OyZ340IMSIi2Ebf3uwByOk6ho4wne1E= github.com/anchore/packageurl-go v0.1.1-0.20250220190351-d62adb6e1115 h1:ZyRCmiEjnoGJZ1+Ah0ZZ/mKKqNhGcUZBl0s7PTTDzvY= github.com/anchore/packageurl-go v0.1.1-0.20250220190351-d62adb6e1115/go.mod h1:KoYIv7tdP5+CC9VGkeZV4/vGCKsY55VvoG+5dadg4YI= -github.com/anchore/stereoscope v0.1.19 h1:1G5LVmRN1Sz6qNezpVAEeN7QfWwCE9zw9TJK1ZGnkvw= -github.com/anchore/stereoscope v0.1.19/go.mod h1:+laNHlk05xA2YqgEzq8mxkFzclL3NRdeNIsiQQVeZZ4= -github.com/anchore/syft v1.41.2 h1:mC2l3P8dUvBdz+97ZNcKD410s8vGFGFXdZa+neaQEb8= -github.com/anchore/syft v1.41.2/go.mod h1:j8SaTiPQzSxElS0MWw3ML2m2EK4av/7Vm4q8WpwUmYw= +github.com/anchore/stereoscope v0.1.20 h1:32720yZ/YtvzF5tvsoRL/ibdAJzOdIaR444fDXW4arQ= +github.com/anchore/stereoscope v0.1.20/go.mod h1:6Ef0xQAuN2Ito7eV9A9pYjD1x/0cX5fy56MwgEGyrB4= +github.com/anchore/syft v1.42.0 h1:UCoqHhPdmLxCrJmq0qxQxyIs0rf9sqKlG9p9OVs7yO4= +github.com/anchore/syft v1.42.0/go.mod h1:rlZUlW45y8IOR0YO4EivNHXnI51mDCAxmYj9fBW0gmI= github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883/go.mod h1:rCTlJbsFo29Kk6CurOXKm700vrz8f0KW0JNfpkRJY/8= github.com/andybalholm/brotli v1.2.0 h1:ukwgCxwYrmACq68yiUqwIWnGY0cTPox/M94sVwToPjQ= github.com/andybalholm/brotli v1.2.0/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY= @@ -225,8 +225,8 @@ github.com/bitnami/go-version v0.0.0-20250131085805-b1f57a8634ef/go.mod h1:9iglf1GG4oNRJ39bZ5AZrjgAFD2RwQbXw6Qf7Cs47wo= github.com/blakesmith/ar v0.0.0-20190502131153-809d4375e1fb h1:m935MPodAbYS46DG4pJSv7WO+VECIWUQ7OJYSoTrMh4= github.com/blakesmith/ar v0.0.0-20190502131153-809d4375e1fb/go.mod h1:PkYb9DJNAwrSvRx5DYA+gUcOIgTGVMNkfSCbZM8cWpI= -github.com/bmatcuk/doublestar/v4 v4.9.2 h1:b0mc6WyRSYLjzofB2v/0cuDUZ+MqoGyH3r0dVij35GI= -github.com/bmatcuk/doublestar/v4 v4.9.2/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc= +github.com/bmatcuk/doublestar/v4 v4.10.0 h1:zU9WiOla1YA122oLM6i4EXvGW62DvKZVxIe6TYWexEs= +github.com/bmatcuk/doublestar/v4 v4.10.0/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc= github.com/bodgit/plumbing v1.3.0 h1:pf9Itz1JOQgn7vEOE7v7nlEfBykYqvUYioC61TwWCFU= github.com/bodgit/plumbing v1.3.0/go.mod h1:JOTb4XiRu5xfnmdnDJo6GmSbSbtSyufrsyZFByMtKEs= github.com/bodgit/sevenzip v1.6.1 h1:kikg2pUMYC9ljU7W9SaqHXhym5HyKm8/M/jd31fYan4= @@ -335,8 +335,8 @@ github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E= github.com/djherbis/times v1.6.0 h1:w2ctJ92J8fBvWPxugmXIv7Nz7Q3iDMKNx9v5ocVH20c= github.com/djherbis/times v1.6.0/go.mod h1:gOHeRAz2h+VJNZ5Gmc/o7iD9k4wW7NMVqieYCY99oc0= -github.com/docker/cli v29.1.5+incompatible h1:GckbANUt3j+lsnQ6eCcQd70mNSOismSHWt8vk2AX8ao= -github.com/docker/cli v29.1.5+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= +github.com/docker/cli v29.2.0+incompatible h1:9oBd9+YM7rxjZLfyMGxjraKBKE4/nVyvVfN4qNl9XRM= +github.com/docker/cli v29.2.0+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= github.com/docker/distribution v2.8.3+incompatible h1:AtKxIZ36LoNK51+Z6RpzLpddBirtxJnzDrHLEKxTAYk= github.com/docker/distribution v2.8.3+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w= github.com/docker/docker v28.5.2+incompatible h1:DBX0Y0zAjZbSrm1uzOkdr1onVghKaftjlSWt4AFexzM= @@ -416,8 +416,8 @@ github.com/go-git/go-billy/v5 v5.7.0/go.mod h1:/1IUejTKH8xipsAcdfcSAlUlo2J7lkYV8GTKxAT/L3E= github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399 h1:eMje31YglSBqCdIqdhKBW8lokaMrL3uTkpGYlE2OOT4= github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399/go.mod h1:1OCfN199q1Jm3HZlxleg+Dw/mwps2Wbk9frAWm+4FII= -github.com/go-git/go-git/v5 v5.16.4 h1:7ajIEZHZJULcyJebDLo99bGgS0jRrOxzZG4uCk2Yb2Y= -github.com/go-git/go-git/v5 v5.16.4/go.mod h1:4Ge4alE/5gPs30F2H1esi2gPd69R0C39lolkucHBOp8= +github.com/go-git/go-git/v5 v5.16.5 h1:mdkuqblwr57kVfXri5TTH+nMFLNUxIj9Z7F5ykFbw5s= +github.com/go-git/go-git/v5 v5.16.5/go.mod h1:QOMLpNf1qxuSY4StA/ArOdfFR2TrKEjJiye2kel2m+M= github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/grant-0.5.8/grant/case.go new/grant-0.6.0/grant/case.go --- old/grant-0.5.8/grant/case.go 2026-02-03 19:29:04.000000000 +0100 +++ new/grant-0.6.0/grant/case.go 2026-02-10 18:55:25.000000000 +0100 @@ -390,6 +390,19 @@ "temp": true, } +// skipSymlink reports whether d is a symlink that should be skipped. +// This includes symlinks to directories and broken symlinks whose targets +// no longer exist. Symlinks to regular files are not skipped. +// WalkDir uses lstat semantics, so symlinks appear as non-directory entries +// even when they point to directories. +func skipSymlink(path string, d os.DirEntry) bool { + if d.Type()&os.ModeSymlink == 0 { + return false + } + fi, err := os.Stat(path) + return err != nil || fi.IsDir() +} + // searchLicenseFiles searches for license files recursively in the given directory func (ch *CaseHandler) searchLicenseFiles(root string) ([]License, error) { patterns := licensepatterns.Patterns @@ -402,6 +415,8 @@ return nil // Continue walking even if there's an error with a specific directory } + // this returns false for a symlink to a directory + // current implementations of walk dir never descends into a symlinked dir if d.IsDir() { dirName := d.Name() if skipDirectories[dirName] { @@ -410,6 +425,16 @@ return nil } + // TODO: grant's local license analysis (outside of the syft SBOM) + // does not follow/resolve directory symlinks. This should be improved. + // For now we need to skip these since WalkDir uses lstat semantics. + // If we do not skip symlinked dirs then they would get passed to the license classifier. + // This can cause errors as seen in: https://github.com/anchore/grant/issues/70 + // Regular file symlink targets are allowed and work as expected + if skipSymlink(path, d) { + return nil + } + // skip if we've already processed this file if visited[path] { return nil diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/grant-0.5.8/grant/case_test.go new/grant-0.6.0/grant/case_test.go --- old/grant-0.5.8/grant/case_test.go 2026-02-03 19:29:04.000000000 +0100 +++ new/grant-0.6.0/grant/case_test.go 2026-02-10 18:55:25.000000000 +0100 @@ -205,3 +205,61 @@ // Just verify that licenses were found - the specific type detection depends on the classifier assert.True(t, len(result2.Licenses) > 0, "Should detect licenses when DisableFileSearch is false") } + +func TestSearchLicenseFiles_Symlinks(t *testing.T) { + tests := []struct { + name string + setup func(t *testing.T, dir string) + minCount int + }{ + { + name: "skips symlink to directory", + setup: func(t *testing.T, dir string) { + // Mimics snap package layout (e.g. libncursesw6 -> libtinfo6/) + subDir := filepath.Join(dir, "libtinfo6") + require.NoError(t, os.MkdirAll(subDir, 0755)) + require.NoError(t, os.WriteFile(filepath.Join(subDir, "copyright"), []byte("some copyright info"), 0644)) + require.NoError(t, os.Symlink(subDir, filepath.Join(dir, "libncursesw6"))) + + mitLicense := readTestLicense(t, "mit-license.txt") + require.NoError(t, os.WriteFile(filepath.Join(dir, "LICENSE"), []byte(mitLicense), 0644)) + }, + minCount: 1, + }, + { + name: "follows symlink to file", + setup: func(t *testing.T, dir string) { + mitLicense := readTestLicense(t, "mit-license.txt") + realFile := filepath.Join(dir, "actual-license.txt") + require.NoError(t, os.WriteFile(realFile, []byte(mitLicense), 0644)) + require.NoError(t, os.Symlink(realFile, filepath.Join(dir, "LICENSE"))) + }, + minCount: 1, + }, + { + name: "skips broken symlink", + setup: func(t *testing.T, dir string) { + require.NoError(t, os.Symlink("/nonexistent/target", filepath.Join(dir, "LICENSE"))) + + mitLicense := readTestLicense(t, "mit-license.txt") + require.NoError(t, os.WriteFile(filepath.Join(dir, "COPYING"), []byte(mitLicense), 0644)) + }, + minCount: 1, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + dir := t.TempDir() + tt.setup(t, dir) + + ch, err := NewCaseHandler() + require.NoError(t, err) + defer ch.Close() + + licenses, err := ch.searchLicenseFiles(dir) + require.NoError(t, err) + assert.GreaterOrEqual(t, len(licenses), tt.minCount, "searchLicenseFiles(%s) returned %d licenses, want >= %d", dir, len(licenses), tt.minCount) + }) + } +} diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/grant-0.5.8/tests/cli/check_test.go new/grant-0.6.0/tests/cli/check_test.go --- old/grant-0.5.8/tests/cli/check_test.go 2026-02-03 19:29:04.000000000 +0100 +++ new/grant-0.6.0/tests/cli/check_test.go 2026-02-10 18:55:25.000000000 +0100 @@ -37,3 +37,84 @@ }) } } + +func Test_CheckCmdStdin(t *testing.T) { + tests := []struct { + name string + args []string + stdin string + wantErr bool + wantInOutput []string + wantAbsent []string + }{ + { + name: "no args and no stdin shows helpful error", + args: []string{"check"}, + wantErr: true, + wantInOutput: []string{ + "no target specified and no input available on stdin", + }, + }, + { + name: "piped stdin with no args reads from stdin", + args: []string{"check"}, + stdin: `{"bomFormat":"CycloneDX","specVersion":"1.4","components":[]}`, + wantErr: false, + wantAbsent: []string{ + "no target specified", + "requires at least 1 arg", + }, + }, + { + name: "explicit args are used even when stdin is available", + args: []string{"check", "../../grant/testdata/mit-license.txt"}, + stdin: "this should be ignored", + wantErr: false, + wantAbsent: []string{ + "no target specified", + }, + }, + { + name: "explicit dash reads from stdin", + args: []string{"check", "-"}, + stdin: `{"bomFormat":"CycloneDX","specVersion":"1.4","components":[]}`, + wantErr: false, + wantAbsent: []string{ + "no target specified", + "requires at least 1 arg", + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + cmd := exec.Command(grantTmpPath, tt.args...) + if tt.stdin != "" { + cmd.Stdin = strings.NewReader(tt.stdin) + } + output, err := cmd.CombinedOutput() + got := string(output) + + if tt.wantErr { + if err == nil { + t.Fatalf("grant %s: got success, want error", strings.Join(tt.args, " ")) + } + } else { + // Allow exit status 1 (policy violations) but not other errors + if err != nil && !strings.Contains(err.Error(), "exit status 1") { + t.Fatalf("grant %s: got unexpected error: %v\noutput: %s", strings.Join(tt.args, " "), err, got) + } + } + + for _, want := range tt.wantInOutput { + if !strings.Contains(got, want) { + t.Errorf("grant %s: output does not contain %q\ngot: %s", strings.Join(tt.args, " "), want, got) + } + } + for _, absent := range tt.wantAbsent { + if strings.Contains(got, absent) { + t.Errorf("grant %s: output unexpectedly contains %q\ngot: %s", strings.Join(tt.args, " "), absent, got) + } + } + }) + } +} ++++++ grant.obsinfo ++++++ --- /var/tmp/diff_new_pack.kBNHfG/_old 2026-02-11 18:50:06.808567171 +0100 +++ /var/tmp/diff_new_pack.kBNHfG/_new 2026-02-11 18:50:06.816567507 +0100 @@ -1,5 +1,5 @@ name: grant -version: 0.5.8 -mtime: 1770143344 -commit: 351740eac7b055feceee7e874647288660dbefba +version: 0.6.0 +mtime: 1770746125 +commit: f071e8d6aa0f126561e75637e46a90106b5301af ++++++ vendor.tar.gz ++++++ /work/SRC/openSUSE:Factory/grant/vendor.tar.gz /work/SRC/openSUSE:Factory/.grant.new.1670/vendor.tar.gz differ: char 134, line 1
