From 305b4d0a08c1879a541c6f0f8f9f6ca2956c9421 Mon Sep 17 00:00:00 2001 From: Eric Sorenson Date: Wed, 4 Mar 2026 10:55:10 -0800 Subject: [PATCH 1/3] Add simple cli to validate expressions This adds a simple cli to the package called `spdx-validate`. It takes a string or an input file containing spdx expressions and runs the `ValidateLicenses` function on them, presenting errors if they fail validation. (Disclosure: Coded with Claude Opus 4.6) --- README.md | 47 +++++++ cmd/spdx-validate/main.go | 127 +++++++++++++++++ cmd/spdx-validate/main_test.go | 248 +++++++++++++++++++++++++++++++++ go.mod | 7 +- go.sum | 9 ++ 5 files changed, 437 insertions(+), 1 deletion(-) create mode 100644 cmd/spdx-validate/main.go create mode 100644 cmd/spdx-validate/main_test.go diff --git a/README.md b/README.md index 68a9e27..4b3c726 100644 --- a/README.md +++ b/README.md @@ -17,6 +17,53 @@ go get github.com/github/go-spdx@latest - [spdxexp](https://pkg.go.dev/github.com/github/go-spdx/spdxexp) - Expression package validates licenses and determines if a license expression is satisfied by a list of licenses. Validity of a license is determined by the SPDX license list. +## CLI: spdx-validate + +`spdx-validate` is a command-line tool that validates SPDX license expressions. + +### Building + +```sh +go build -o spdx-validate ./cmd/spdx-validate/ +``` + +### Usage + +**Validate a single expression on stdin:** + +```sh +echo "MIT" | ./spdx-validate +echo "Apache-2.0 OR MIT" | ./spdx-validate +``` + +The tool exits with code 0 if the expression is valid, or code 1 (with an error message on stderr) if it is invalid. + +```sh +$ echo "BOGUS" | ./spdx-validate +invalid SPDX expression: "BOGUS" +$ echo $? +1 +``` + +**Validate a file of expressions with `-f`/`--file`:** + +```sh +./spdx-validate -f licenses.txt +``` + +The file should contain one SPDX expression per line. Blank lines are skipped. The tool reports each invalid expression to stderr, prints a summary, and exits with code 0 if all pass or code 1 if any fail. + +```sh +$ cat licenses.txt +MIT +NOT-A-LICENSE +Apache-2.0 + +$ ./spdx-validate -f licenses.txt +line 2: invalid SPDX expression: "NOT-A-LICENSE" +1 of 3 expressions failed validation +``` + ## Public API _NOTE: The public API is initially limited to the Satisfies and ValidateLicenses functions. If diff --git a/cmd/spdx-validate/main.go b/cmd/spdx-validate/main.go new file mode 100644 index 0000000..49201ce --- /dev/null +++ b/cmd/spdx-validate/main.go @@ -0,0 +1,127 @@ +package main + +import ( + "bufio" + "fmt" + "io" + "os" + "strings" + + "github.com/github/go-spdx/v2/spdxexp" + "github.com/spf13/cobra" +) + +var filePath string + +var rootCmd = &cobra.Command{ + Use: "spdx-validate", + Short: "Validate SPDX license expressions", + Long: `spdx-validate reads SPDX license expressions and validates them. + +By default it reads a single expression from stdin. Use -f/--file to read +a newline-separated list of expressions from a file. + +Exits 0 if all expressions are valid, or 1 if any are invalid. + +Examples: + echo "MIT" | spdx-validate + echo "Apache-2.0 OR MIT" | spdx-validate + spdx-validate -f licenses.txt`, + RunE: func(cmd *cobra.Command, args []string) error { + if filePath != "" { + f, err := os.Open(filePath) + if err != nil { + return fmt.Errorf("unable to open file: %w", err) + } + defer f.Close() + ok, err := validateExpressions(f, os.Stderr) + if err != nil { + return err + } + if !ok { + os.Exit(1) + } + return nil + } + ok, err := validateSingleExpression(os.Stdin, os.Stderr) + if err != nil { + return err + } + if !ok { + os.Exit(1) + } + return nil + }, + SilenceUsage: true, + SilenceErrors: true, +} + +func init() { + rootCmd.Flags().StringVarP(&filePath, "file", "f", "", "path to a newline-separated file of SPDX expressions") +} + +// validateSingleExpression reads one line from r, validates it as an SPDX +// expression, and writes an error message to w if invalid. Returns (true, nil) +// when valid, (false, nil) when invalid, or (false, err) on read errors. +func validateSingleExpression(r io.Reader, w io.Writer) (bool, error) { + scanner := bufio.NewScanner(r) + if !scanner.Scan() { + return false, fmt.Errorf("no input provided") + } + input := strings.TrimSpace(scanner.Text()) + if input == "" { + return false, fmt.Errorf("empty input") + } + + valid, _ := spdxexp.ValidateLicenses([]string{input}) + if !valid { + fmt.Fprintf(w, "invalid SPDX expression: %q\n", input) + return false, nil + } + return true, nil +} + +// validateExpressions reads newline-separated SPDX expressions from r, +// validates each one, and writes error messages to w for any that are invalid. +// Returns (true, nil) when all are valid, (false, nil) when any are invalid, or +// (false, err) on read errors or when no expressions are found. +func validateExpressions(r io.Reader, w io.Writer) (bool, error) { + scanner := bufio.NewScanner(r) + lineNum := 0 + failures := 0 + + for scanner.Scan() { + lineNum++ + line := strings.TrimSpace(scanner.Text()) + if line == "" { + continue + } + valid, _ := spdxexp.ValidateLicenses([]string{line}) + if !valid { + failures++ + fmt.Fprintf(w, "line %d: invalid SPDX expression: %q\n", lineNum, line) + } + } + + if err := scanner.Err(); err != nil { + return false, fmt.Errorf("error reading file: %w", err) + } + + if lineNum == 0 || (lineNum > 0 && failures == lineNum) { + return false, fmt.Errorf("no valid expressions found") + } + + if failures > 0 { + fmt.Fprintf(w, "%d of %d expressions failed validation\n", failures, lineNum) + return false, nil + } + + return true, nil +} + +func main() { + if err := rootCmd.Execute(); err != nil { + fmt.Fprintln(os.Stderr, err) + os.Exit(1) + } +} diff --git a/cmd/spdx-validate/main_test.go b/cmd/spdx-validate/main_test.go new file mode 100644 index 0000000..ccfd642 --- /dev/null +++ b/cmd/spdx-validate/main_test.go @@ -0,0 +1,248 @@ +package main + +import ( + "bytes" + "os" + "path/filepath" + "strings" + "testing" +) + +// --- Tests for validateSingleExpression (stdin path) --- + +func TestValidateSingleExpression_Valid(t *testing.T) { + tests := []string{ + "MIT", + "Apache-2.0", + "BSD-3-Clause", + "Apache-2.0 OR MIT", + "MIT AND ISC", + "GPL-3.0-only WITH Classpath-exception-2.0", + } + for _, expr := range tests { + t.Run(expr, func(t *testing.T) { + r := strings.NewReader(expr + "\n") + var w bytes.Buffer + ok, err := validateSingleExpression(r, &w) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if !ok { + t.Errorf("expected valid, got invalid; stderr: %s", w.String()) + } + if w.Len() != 0 { + t.Errorf("expected no stderr output, got: %s", w.String()) + } + }) + } +} + +func TestValidateSingleExpression_Invalid(t *testing.T) { + tests := []string{ + "BOGUS-LICENSE", + "NOT-A-REAL-ID", + "MIT ANDOR Apache-2.0", + } + for _, expr := range tests { + t.Run(expr, func(t *testing.T) { + r := strings.NewReader(expr + "\n") + var w bytes.Buffer + ok, err := validateSingleExpression(r, &w) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if ok { + t.Error("expected invalid, got valid") + } + if !strings.Contains(w.String(), "invalid SPDX expression") { + t.Errorf("expected error message in output, got: %s", w.String()) + } + if !strings.Contains(w.String(), expr) { + t.Errorf("expected expression %q in output, got: %s", expr, w.String()) + } + }) + } +} + +func TestValidateSingleExpression_EmptyInput(t *testing.T) { + r := strings.NewReader("") + var w bytes.Buffer + ok, err := validateSingleExpression(r, &w) + if err == nil { + t.Fatal("expected error for empty input, got nil") + } + if ok { + t.Error("expected ok=false for empty input") + } + if !strings.Contains(err.Error(), "no input provided") { + t.Errorf("expected 'no input provided' error, got: %v", err) + } +} + +func TestValidateSingleExpression_BlankLine(t *testing.T) { + r := strings.NewReader(" \n") + var w bytes.Buffer + ok, err := validateSingleExpression(r, &w) + if err == nil { + t.Fatal("expected error for blank input, got nil") + } + if ok { + t.Error("expected ok=false for blank input") + } + if !strings.Contains(err.Error(), "empty input") { + t.Errorf("expected 'empty input' error, got: %v", err) + } +} + +// --- Tests for validateExpressions (file path) --- + +func TestValidateExpressions_AllValid(t *testing.T) { + input := "MIT\nApache-2.0\nBSD-3-Clause OR MIT\n" + r := strings.NewReader(input) + var w bytes.Buffer + ok, err := validateExpressions(r, &w) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if !ok { + t.Errorf("expected all valid, got invalid; stderr: %s", w.String()) + } + if w.Len() != 0 { + t.Errorf("expected no stderr output, got: %s", w.String()) + } +} + +func TestValidateExpressions_SomeInvalid(t *testing.T) { + input := "MIT\nNOT-A-LICENSE\nApache-2.0\nALSO-BOGUS\n" + r := strings.NewReader(input) + var w bytes.Buffer + ok, err := validateExpressions(r, &w) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if ok { + t.Error("expected invalid result, got valid") + } + output := w.String() + if !strings.Contains(output, `"NOT-A-LICENSE"`) { + t.Errorf("expected NOT-A-LICENSE in output, got: %s", output) + } + if !strings.Contains(output, `"ALSO-BOGUS"`) { + t.Errorf("expected ALSO-BOGUS in output, got: %s", output) + } + if !strings.Contains(output, "line 2:") { + t.Errorf("expected 'line 2:' in output, got: %s", output) + } + if !strings.Contains(output, "line 4:") { + t.Errorf("expected 'line 4:' in output, got: %s", output) + } + if !strings.Contains(output, "2 of 4 expressions failed") { + t.Errorf("expected summary in output, got: %s", output) + } +} + +func TestValidateExpressions_AllInvalid(t *testing.T) { + input := "BOGUS-1\nBOGUS-2\n" + r := strings.NewReader(input) + var w bytes.Buffer + ok, err := validateExpressions(r, &w) + if err == nil { + t.Fatal("expected error when all expressions are invalid, got nil") + } + if ok { + t.Error("expected ok=false") + } + if !strings.Contains(err.Error(), "no valid expressions found") { + t.Errorf("expected 'no valid expressions found' error, got: %v", err) + } +} + +func TestValidateExpressions_EmptyFile(t *testing.T) { + r := strings.NewReader("") + var w bytes.Buffer + ok, err := validateExpressions(r, &w) + if err == nil { + t.Fatal("expected error for empty file, got nil") + } + if ok { + t.Error("expected ok=false for empty file") + } + if !strings.Contains(err.Error(), "no valid expressions found") { + t.Errorf("expected 'no valid expressions found' error, got: %v", err) + } +} + +func TestValidateExpressions_SkipsBlankLines(t *testing.T) { + input := "\nMIT\n\n\nApache-2.0\n\n" + r := strings.NewReader(input) + var w bytes.Buffer + ok, err := validateExpressions(r, &w) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if !ok { + t.Errorf("expected all valid, got invalid; stderr: %s", w.String()) + } +} + +// --- Integration test using a temp file --- + +func TestValidateExpressions_FromTempFile(t *testing.T) { + dir := t.TempDir() + path := filepath.Join(dir, "licenses.txt") + + content := "MIT\nApache-2.0\nBSD-2-Clause\n" + if err := os.WriteFile(path, []byte(content), 0644); err != nil { + t.Fatalf("failed to write temp file: %v", err) + } + + f, err := os.Open(path) + if err != nil { + t.Fatalf("failed to open temp file: %v", err) + } + defer f.Close() + + var w bytes.Buffer + ok, err := validateExpressions(f, &w) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if !ok { + t.Errorf("expected all valid from file, got invalid; stderr: %s", w.String()) + } +} + +func TestValidateExpressions_FromTempFileWithFailures(t *testing.T) { + dir := t.TempDir() + path := filepath.Join(dir, "licenses.txt") + + content := "MIT\nINVALID-1\nApache-2.0\nINVALID-2\nBSD-2-Clause\n" + if err := os.WriteFile(path, []byte(content), 0644); err != nil { + t.Fatalf("failed to write temp file: %v", err) + } + + f, err := os.Open(path) + if err != nil { + t.Fatalf("failed to open temp file: %v", err) + } + defer f.Close() + + var w bytes.Buffer + ok, err := validateExpressions(f, &w) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if ok { + t.Error("expected invalid result from file with bad entries") + } + output := w.String() + if !strings.Contains(output, `"INVALID-1"`) { + t.Errorf("expected INVALID-1 in output, got: %s", output) + } + if !strings.Contains(output, `"INVALID-2"`) { + t.Errorf("expected INVALID-2 in output, got: %s", output) + } + if !strings.Contains(output, "2 of 5 expressions failed") { + t.Errorf("expected summary in output, got: %s", output) + } +} diff --git a/go.mod b/go.mod index 408aa69..620ed3b 100644 --- a/go.mod +++ b/go.mod @@ -4,10 +4,15 @@ retract v2.3.0 // Compatibility issues with go 1.22 go 1.24 -require github.com/stretchr/testify v1.8.1 +require ( + github.com/spf13/cobra v1.10.2 + github.com/stretchr/testify v1.8.1 +) require ( github.com/davecgh/go-spew v1.1.1 // indirect + github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/spf13/pflag v1.0.9 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 2ec90f7..b0af8ab 100644 --- a/go.sum +++ b/go.sum @@ -1,8 +1,16 @@ +github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= +github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU= +github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4= +github.com/spf13/pflag v1.0.9 h1:9exaQaMOCwffKiiiYk6/BndUBv+iRViNW+4lEMi0PvY= +github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= @@ -10,6 +18,7 @@ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/ github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= From f678c1e579e6a28587758948ccabb960ed2bec2f Mon Sep 17 00:00:00 2001 From: Eric Sorenson Date: Wed, 4 Mar 2026 11:02:01 -0800 Subject: [PATCH 2/3] Fix file permissions failing gosec tests --- cmd/spdx-validate/main_test.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cmd/spdx-validate/main_test.go b/cmd/spdx-validate/main_test.go index ccfd642..c56cfa3 100644 --- a/cmd/spdx-validate/main_test.go +++ b/cmd/spdx-validate/main_test.go @@ -192,7 +192,7 @@ func TestValidateExpressions_FromTempFile(t *testing.T) { path := filepath.Join(dir, "licenses.txt") content := "MIT\nApache-2.0\nBSD-2-Clause\n" - if err := os.WriteFile(path, []byte(content), 0644); err != nil { + if err := os.WriteFile(path, []byte(content), 0600); err != nil { t.Fatalf("failed to write temp file: %v", err) } @@ -217,7 +217,7 @@ func TestValidateExpressions_FromTempFileWithFailures(t *testing.T) { path := filepath.Join(dir, "licenses.txt") content := "MIT\nINVALID-1\nApache-2.0\nINVALID-2\nBSD-2-Clause\n" - if err := os.WriteFile(path, []byte(content), 0644); err != nil { + if err := os.WriteFile(path, []byte(content), 0600); err != nil { t.Fatalf("failed to write temp file: %v", err) } From fd8cb55499724a7954172f3c89923ce29dd50637 Mon Sep 17 00:00:00 2001 From: Eric Sorenson Date: Thu, 5 Mar 2026 15:13:51 -0800 Subject: [PATCH 3/3] Treat stdin as just another input file The previous split code path was left over from my initial attempts. now stdin is treated like a newline-separated file rather than being special-cased, so the tool can accept multiple lines from a shell pipeline or a file. --- README.md | 19 +++++++------- cmd/spdx-validate/main.go | 44 ++++++-------------------------- cmd/spdx-validate/main_test.go | 46 ++++++---------------------------- 3 files changed, 26 insertions(+), 83 deletions(-) diff --git a/README.md b/README.md index 4b3c726..f7cd346 100644 --- a/README.md +++ b/README.md @@ -29,29 +29,30 @@ go build -o spdx-validate ./cmd/spdx-validate/ ### Usage -**Validate a single expression on stdin:** +**Validate expressions on stdin:** + +Stdin is read as a stream of newline-separated expressions: ```sh echo "MIT" | ./spdx-validate -echo "Apache-2.0 OR MIT" | ./spdx-validate +printf "MIT\nApache-2.0\nBSD-3-Clause\n" | ./spdx-validate ``` -The tool exits with code 0 if the expression is valid, or code 1 (with an error message on stderr) if it is invalid. +Exits with code 0 if all expressions are valid, or code 1 (with error messages on stderr) if any are invalid. ```sh -$ echo "BOGUS" | ./spdx-validate -invalid SPDX expression: "BOGUS" -$ echo $? -1 +$ printf "MIT\nBOGUS\nApache-2.0\n" | ./spdx-validate +line 2: invalid SPDX expression: "BOGUS" +1 of 3 expressions failed validation ``` -**Validate a file of expressions with `-f`/`--file`:** +**Validate from a file with `-f`/`--file`:** ```sh ./spdx-validate -f licenses.txt ``` -The file should contain one SPDX expression per line. Blank lines are skipped. The tool reports each invalid expression to stderr, prints a summary, and exits with code 0 if all pass or code 1 if any fail. +The file should contain one SPDX expression per line. Blank lines are skipped. ```sh $ cat licenses.txt diff --git a/cmd/spdx-validate/main.go b/cmd/spdx-validate/main.go index 49201ce..d31bf2d 100644 --- a/cmd/spdx-validate/main.go +++ b/cmd/spdx-validate/main.go @@ -16,34 +16,27 @@ var filePath string var rootCmd = &cobra.Command{ Use: "spdx-validate", Short: "Validate SPDX license expressions", - Long: `spdx-validate reads SPDX license expressions and validates them. + Long: `spdx-validate reads newline-separated SPDX license expressions and validates them. -By default it reads a single expression from stdin. Use -f/--file to read -a newline-separated list of expressions from a file. - -Exits 0 if all expressions are valid, or 1 if any are invalid. +It reads from stdin by default, or from a file specified with -f/--file. +Blank lines are skipped. Exits 0 if all expressions are valid, or 1 if any +are invalid. Examples: echo "MIT" | spdx-validate - echo "Apache-2.0 OR MIT" | spdx-validate + printf "MIT\nApache-2.0\n" | spdx-validate spdx-validate -f licenses.txt`, RunE: func(cmd *cobra.Command, args []string) error { + var r io.Reader = os.Stdin if filePath != "" { f, err := os.Open(filePath) if err != nil { return fmt.Errorf("unable to open file: %w", err) } defer f.Close() - ok, err := validateExpressions(f, os.Stderr) - if err != nil { - return err - } - if !ok { - os.Exit(1) - } - return nil + r = f } - ok, err := validateSingleExpression(os.Stdin, os.Stderr) + ok, err := validateExpressions(r, os.Stderr) if err != nil { return err } @@ -60,27 +53,6 @@ func init() { rootCmd.Flags().StringVarP(&filePath, "file", "f", "", "path to a newline-separated file of SPDX expressions") } -// validateSingleExpression reads one line from r, validates it as an SPDX -// expression, and writes an error message to w if invalid. Returns (true, nil) -// when valid, (false, nil) when invalid, or (false, err) on read errors. -func validateSingleExpression(r io.Reader, w io.Writer) (bool, error) { - scanner := bufio.NewScanner(r) - if !scanner.Scan() { - return false, fmt.Errorf("no input provided") - } - input := strings.TrimSpace(scanner.Text()) - if input == "" { - return false, fmt.Errorf("empty input") - } - - valid, _ := spdxexp.ValidateLicenses([]string{input}) - if !valid { - fmt.Fprintf(w, "invalid SPDX expression: %q\n", input) - return false, nil - } - return true, nil -} - // validateExpressions reads newline-separated SPDX expressions from r, // validates each one, and writes error messages to w for any that are invalid. // Returns (true, nil) when all are valid, (false, nil) when any are invalid, or diff --git a/cmd/spdx-validate/main_test.go b/cmd/spdx-validate/main_test.go index c56cfa3..9fe7388 100644 --- a/cmd/spdx-validate/main_test.go +++ b/cmd/spdx-validate/main_test.go @@ -8,9 +8,9 @@ import ( "testing" ) -// --- Tests for validateSingleExpression (stdin path) --- +// --- Tests for single expression on stdin --- -func TestValidateSingleExpression_Valid(t *testing.T) { +func TestValidateExpressions_SingleValid(t *testing.T) { tests := []string{ "MIT", "Apache-2.0", @@ -23,7 +23,7 @@ func TestValidateSingleExpression_Valid(t *testing.T) { t.Run(expr, func(t *testing.T) { r := strings.NewReader(expr + "\n") var w bytes.Buffer - ok, err := validateSingleExpression(r, &w) + ok, err := validateExpressions(r, &w) if err != nil { t.Fatalf("unexpected error: %v", err) } @@ -37,7 +37,7 @@ func TestValidateSingleExpression_Valid(t *testing.T) { } } -func TestValidateSingleExpression_Invalid(t *testing.T) { +func TestValidateExpressions_SingleInvalid(t *testing.T) { tests := []string{ "BOGUS-LICENSE", "NOT-A-REAL-ID", @@ -47,9 +47,9 @@ func TestValidateSingleExpression_Invalid(t *testing.T) { t.Run(expr, func(t *testing.T) { r := strings.NewReader(expr + "\n") var w bytes.Buffer - ok, err := validateSingleExpression(r, &w) - if err != nil { - t.Fatalf("unexpected error: %v", err) + ok, err := validateExpressions(r, &w) + if err == nil { + t.Fatal("expected error for single invalid expression, got nil") } if ok { t.Error("expected invalid, got valid") @@ -64,37 +64,7 @@ func TestValidateSingleExpression_Invalid(t *testing.T) { } } -func TestValidateSingleExpression_EmptyInput(t *testing.T) { - r := strings.NewReader("") - var w bytes.Buffer - ok, err := validateSingleExpression(r, &w) - if err == nil { - t.Fatal("expected error for empty input, got nil") - } - if ok { - t.Error("expected ok=false for empty input") - } - if !strings.Contains(err.Error(), "no input provided") { - t.Errorf("expected 'no input provided' error, got: %v", err) - } -} - -func TestValidateSingleExpression_BlankLine(t *testing.T) { - r := strings.NewReader(" \n") - var w bytes.Buffer - ok, err := validateSingleExpression(r, &w) - if err == nil { - t.Fatal("expected error for blank input, got nil") - } - if ok { - t.Error("expected ok=false for blank input") - } - if !strings.Contains(err.Error(), "empty input") { - t.Errorf("expected 'empty input' error, got: %v", err) - } -} - -// --- Tests for validateExpressions (file path) --- +// --- Tests for multiple expressions --- func TestValidateExpressions_AllValid(t *testing.T) { input := "MIT\nApache-2.0\nBSD-3-Clause OR MIT\n"