mirror of
https://github.com/ossf/scorecard.git
synced 2024-09-11 08:55:27 +03:00
✨ scdiff: add basic compare functionality (#3363)
* Add unmarshall func. Signed-off-by: Spencer Schrock <sschrock@google.com> * try to parse the details too. Signed-off-by: Spencer Schrock <sschrock@google.com> * Compare skeleton. Signed-off-by: Spencer Schrock <sschrock@google.com> * add basic comparison func. Signed-off-by: Spencer Schrock <sschrock@google.com> * make normalize exported. Signed-off-by: Spencer Schrock <sschrock@google.com> * split compare to separate func. Signed-off-by: Spencer Schrock <sschrock@google.com> * Add experimental diff output. Signed-off-by: Spencer Schrock <sschrock@google.com> * clarify expected format. Signed-off-by: Spencer Schrock <sschrock@google.com> * Handle multiple repo results in files. Signed-off-by: Spencer Schrock <sschrock@google.com> * add tests for compare. Signed-off-by: Spencer Schrock <sschrock@google.com> * clean up result loading logic. Signed-off-by: Spencer Schrock <sschrock@google.com> * add doc comments for advancescanners. Signed-off-by: Spencer Schrock <sschrock@google.com> * clarify file error string. Signed-off-by: Spencer Schrock <sschrock@google.com> * add high level instructions for the command. Signed-off-by: Spencer Schrock <sschrock@google.com> --------- Signed-off-by: Spencer Schrock <sschrock@google.com>
This commit is contained in:
parent
9a844abbba
commit
b0a96fe9e3
123
cmd/internal/scdiff/app/compare.go
Normal file
123
cmd/internal/scdiff/app/compare.go
Normal file
@ -0,0 +1,123 @@
|
||||
// Copyright 2023 OpenSSF Scorecard Authors
|
||||
//
|
||||
// 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.
|
||||
|
||||
package app
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/ossf/scorecard/v4/cmd/internal/scdiff/app/compare"
|
||||
"github.com/ossf/scorecard/v4/cmd/internal/scdiff/app/format"
|
||||
"github.com/ossf/scorecard/v4/pkg"
|
||||
)
|
||||
|
||||
//nolint:gochecknoinits // common for cobra apps
|
||||
func init() {
|
||||
rootCmd.AddCommand(compareCmd)
|
||||
}
|
||||
|
||||
var (
|
||||
errMissingInputFiles = errors.New("must provide at least two files from scdiff generate")
|
||||
errResultsDiffer = errors.New("results differ")
|
||||
errNumResults = errors.New("number of results being compared differ")
|
||||
|
||||
compareCmd = &cobra.Command{
|
||||
Use: "compare [flags] FILE1 FILE2",
|
||||
Short: "Compare Scorecard results",
|
||||
Long: `Compare Scorecard results`,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
if len(args) < 2 {
|
||||
return errMissingInputFiles
|
||||
}
|
||||
f1, err := os.Open(args[0])
|
||||
if err != nil {
|
||||
return fmt.Errorf("opening %q: %w", args[0], err)
|
||||
}
|
||||
defer f1.Close()
|
||||
f2, err := os.Open(args[1])
|
||||
if err != nil {
|
||||
return fmt.Errorf("opening %q: %w", args[1], err)
|
||||
}
|
||||
defer f2.Close()
|
||||
cmd.SilenceUsage = true // disables printing Usage
|
||||
cmd.SilenceErrors = true // disables the "Error: <err>" message
|
||||
return compareReaders(f1, f2, os.Stderr)
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
func compareReaders(x, y io.Reader, output io.Writer) error {
|
||||
// results are currently newline delimited
|
||||
xs := bufio.NewScanner(x)
|
||||
ys := bufio.NewScanner(y)
|
||||
for {
|
||||
if shouldContinue, err := advanceScanners(xs, ys); err != nil {
|
||||
return err
|
||||
} else if !shouldContinue {
|
||||
break
|
||||
}
|
||||
xResult, yResult, err := loadResults(xs, ys)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !compare.Results(&xResult, &yResult) {
|
||||
// go-cmp says its not production ready. Is this a valid usage?
|
||||
// it certainly helps with readability.
|
||||
fmt.Fprintf(output, "%s\n", cmp.Diff(xResult, yResult))
|
||||
return errResultsDiffer
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func loadResults(x, y *bufio.Scanner) (pkg.ScorecardResult, pkg.ScorecardResult, error) {
|
||||
xResult, err := pkg.ExperimentalFromJSON2(strings.NewReader(x.Text()))
|
||||
if err != nil {
|
||||
return pkg.ScorecardResult{}, pkg.ScorecardResult{}, fmt.Errorf("parsing first result: %w", err)
|
||||
}
|
||||
yResult, err := pkg.ExperimentalFromJSON2(strings.NewReader(y.Text()))
|
||||
if err != nil {
|
||||
return pkg.ScorecardResult{}, pkg.ScorecardResult{}, fmt.Errorf("parsing second result: %w", err)
|
||||
}
|
||||
format.Normalize(&xResult)
|
||||
format.Normalize(&yResult)
|
||||
return xResult, yResult, nil
|
||||
}
|
||||
|
||||
// advanceScanners is intended to expand the normal `for scanner.Scan()` semantics to two scanners,
|
||||
// it keeps the scanners in sync, and determines if iteration should continue.
|
||||
//
|
||||
// Iteration should continue until any scanner reaches EOF, or any scanner encounters a non-EOF error.
|
||||
func advanceScanners(x, y *bufio.Scanner) (shouldContinue bool, err error) {
|
||||
xContinue := x.Scan()
|
||||
yContinue := y.Scan()
|
||||
if err := x.Err(); err != nil {
|
||||
return false, fmt.Errorf("reading results: %w", err)
|
||||
}
|
||||
if err := y.Err(); err != nil {
|
||||
return false, fmt.Errorf("reading results: %w", err)
|
||||
}
|
||||
if xContinue != yContinue {
|
||||
return false, errNumResults
|
||||
}
|
||||
return xContinue, nil
|
||||
}
|
70
cmd/internal/scdiff/app/compare/compare.go
Normal file
70
cmd/internal/scdiff/app/compare/compare.go
Normal file
@ -0,0 +1,70 @@
|
||||
// Copyright 2023 OpenSSF Scorecard Authors
|
||||
//
|
||||
// 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.
|
||||
|
||||
package compare
|
||||
|
||||
import "github.com/ossf/scorecard/v4/pkg"
|
||||
|
||||
// results should be normalized before comparison.
|
||||
func Results(r1, r2 *pkg.ScorecardResult) bool {
|
||||
if r1 == nil && r2 == nil {
|
||||
return true
|
||||
}
|
||||
|
||||
if (r1 != nil) != (r2 != nil) {
|
||||
return false
|
||||
}
|
||||
|
||||
// intentionally not comparing CommitSHA
|
||||
if r1.Repo.Name != r2.Repo.Name {
|
||||
return false
|
||||
}
|
||||
|
||||
if !compareChecks(r1, r2) {
|
||||
return false
|
||||
}
|
||||
|
||||
// not comparing findings, as we're JSON first for now
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
func compareChecks(r1, r2 *pkg.ScorecardResult) bool {
|
||||
if len(r1.Checks) != len(r2.Checks) {
|
||||
return false
|
||||
}
|
||||
|
||||
for i := 0; i < len(r1.Checks); i++ {
|
||||
if r1.Checks[i].Name != r2.Checks[i].Name {
|
||||
return false
|
||||
}
|
||||
if r1.Checks[i].Score != r2.Checks[i].Score {
|
||||
return false
|
||||
}
|
||||
if r1.Checks[i].Reason != r2.Checks[i].Reason {
|
||||
return false
|
||||
}
|
||||
if len(r1.Checks[i].Details) != len(r2.Checks[i].Details) {
|
||||
return false
|
||||
}
|
||||
for j := 0; j < len(r1.Checks[i].Details); j++ {
|
||||
if r1.Checks[i].Details[j].Type != r2.Checks[i].Details[j].Type {
|
||||
return false
|
||||
}
|
||||
// TODO compare detail specifics?
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
222
cmd/internal/scdiff/app/compare/compare_test.go
Normal file
222
cmd/internal/scdiff/app/compare/compare_test.go
Normal file
@ -0,0 +1,222 @@
|
||||
// Copyright 2023 OpenSSF Scorecard Authors
|
||||
//
|
||||
// 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.
|
||||
|
||||
package compare
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/ossf/scorecard/v4/checker"
|
||||
"github.com/ossf/scorecard/v4/pkg"
|
||||
)
|
||||
|
||||
func TestResults(t *testing.T) {
|
||||
//nolint:govet // field alignment
|
||||
tests := []struct {
|
||||
name string
|
||||
a, b *pkg.ScorecardResult
|
||||
wantEqual bool
|
||||
}{
|
||||
{
|
||||
name: "both nil",
|
||||
a: nil,
|
||||
b: nil,
|
||||
wantEqual: true,
|
||||
},
|
||||
{
|
||||
name: "one nil",
|
||||
a: nil,
|
||||
b: &pkg.ScorecardResult{},
|
||||
wantEqual: false,
|
||||
},
|
||||
{
|
||||
name: "different repo name",
|
||||
a: &pkg.ScorecardResult{
|
||||
Repo: pkg.RepoInfo{
|
||||
Name: "a",
|
||||
},
|
||||
},
|
||||
b: &pkg.ScorecardResult{
|
||||
Repo: pkg.RepoInfo{
|
||||
Name: "b",
|
||||
},
|
||||
},
|
||||
wantEqual: false,
|
||||
},
|
||||
{
|
||||
name: "unequal amount of checks",
|
||||
a: &pkg.ScorecardResult{
|
||||
Checks: []checker.CheckResult{
|
||||
{
|
||||
Name: "a1",
|
||||
},
|
||||
{
|
||||
Name: "a2",
|
||||
},
|
||||
},
|
||||
},
|
||||
b: &pkg.ScorecardResult{
|
||||
Checks: []checker.CheckResult{
|
||||
{
|
||||
Name: "b",
|
||||
},
|
||||
},
|
||||
},
|
||||
wantEqual: false,
|
||||
},
|
||||
{
|
||||
name: "different check name",
|
||||
a: &pkg.ScorecardResult{
|
||||
Checks: []checker.CheckResult{
|
||||
{
|
||||
Name: "a",
|
||||
},
|
||||
},
|
||||
},
|
||||
b: &pkg.ScorecardResult{
|
||||
Checks: []checker.CheckResult{
|
||||
{
|
||||
Name: "b",
|
||||
},
|
||||
},
|
||||
},
|
||||
wantEqual: false,
|
||||
},
|
||||
{
|
||||
name: "different check score",
|
||||
a: &pkg.ScorecardResult{
|
||||
Checks: []checker.CheckResult{
|
||||
{
|
||||
Score: 1,
|
||||
},
|
||||
},
|
||||
},
|
||||
b: &pkg.ScorecardResult{
|
||||
Checks: []checker.CheckResult{
|
||||
{
|
||||
Score: 2,
|
||||
},
|
||||
},
|
||||
},
|
||||
wantEqual: false,
|
||||
},
|
||||
{
|
||||
name: "different check reason",
|
||||
a: &pkg.ScorecardResult{
|
||||
Checks: []checker.CheckResult{
|
||||
{
|
||||
Reason: "a",
|
||||
},
|
||||
},
|
||||
},
|
||||
b: &pkg.ScorecardResult{
|
||||
Checks: []checker.CheckResult{
|
||||
{
|
||||
Reason: "b",
|
||||
},
|
||||
},
|
||||
},
|
||||
wantEqual: false,
|
||||
},
|
||||
{
|
||||
name: "unequal number of details",
|
||||
a: &pkg.ScorecardResult{
|
||||
Checks: []checker.CheckResult{
|
||||
{
|
||||
Details: []checker.CheckDetail{},
|
||||
},
|
||||
},
|
||||
},
|
||||
b: &pkg.ScorecardResult{
|
||||
Checks: []checker.CheckResult{
|
||||
{
|
||||
Details: []checker.CheckDetail{
|
||||
{
|
||||
Type: checker.DetailWarn,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
wantEqual: false,
|
||||
},
|
||||
{
|
||||
name: "details have differnet levels",
|
||||
a: &pkg.ScorecardResult{
|
||||
Checks: []checker.CheckResult{
|
||||
{
|
||||
Details: []checker.CheckDetail{
|
||||
{
|
||||
Type: checker.DetailInfo,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
b: &pkg.ScorecardResult{
|
||||
Checks: []checker.CheckResult{
|
||||
{
|
||||
Details: []checker.CheckDetail{
|
||||
{
|
||||
Type: checker.DetailWarn,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
wantEqual: false,
|
||||
},
|
||||
{
|
||||
name: "equal results",
|
||||
a: &pkg.ScorecardResult{
|
||||
Repo: pkg.RepoInfo{
|
||||
Name: "foo",
|
||||
},
|
||||
Checks: []checker.CheckResult{
|
||||
{
|
||||
Name: "bar",
|
||||
Details: []checker.CheckDetail{
|
||||
{
|
||||
Type: checker.DetailWarn,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
b: &pkg.ScorecardResult{
|
||||
Repo: pkg.RepoInfo{
|
||||
Name: "foo",
|
||||
},
|
||||
Checks: []checker.CheckResult{
|
||||
{
|
||||
Name: "bar",
|
||||
Details: []checker.CheckDetail{
|
||||
{
|
||||
Type: checker.DetailWarn,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
wantEqual: true,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if got := Results(tt.a, tt.b); got != tt.wantEqual {
|
||||
t.Errorf("Results() = %v, want %v", got, tt.wantEqual)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
136
cmd/internal/scdiff/app/compare_test.go
Normal file
136
cmd/internal/scdiff/app/compare_test.go
Normal file
@ -0,0 +1,136 @@
|
||||
// Copyright 2023 OpenSSF Scorecard Authors
|
||||
//
|
||||
// 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.
|
||||
|
||||
package app
|
||||
|
||||
import (
|
||||
"io"
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
//nolint:lll // results are long
|
||||
func Test_compare(t *testing.T) {
|
||||
t.Parallel()
|
||||
//nolint:govet // struct alignment
|
||||
tests := []struct {
|
||||
name string
|
||||
x string
|
||||
y string
|
||||
match bool
|
||||
wantErrSubstr string
|
||||
}{
|
||||
{
|
||||
name: "results match",
|
||||
x: `{"date":"2023-08-11T10:22:43-07:00","repo":{"name":"github.com/foo/bar","commit":"f0840f7158c8044af2bd9b8aa661d7942b1f29d2"},"scorecard":{"version":"","commit":"unknown"},"score":10.0,"checks":[{"details":null,"score":10,"reason":"no vulnerabilities detected","name":"Vulnerabilities","documentation":{"url":"https://github.com/ossf/scorecard/blob/main/docs/checks.md#vulnerabilities","short":"Determines if the project has open, known unfixed vulnerabilities."}}],"metadata":null}
|
||||
`,
|
||||
y: `{"date":"2023-08-11T10:22:43-07:00","repo":{"name":"github.com/foo/bar","commit":"f0840f7158c8044af2bd9b8aa661d7942b1f29d2"},"scorecard":{"version":"","commit":"unknown"},"score":10.0,"checks":[{"details":null,"score":10,"reason":"no vulnerabilities detected","name":"Vulnerabilities","documentation":{"url":"https://github.com/ossf/scorecard/blob/main/docs/checks.md#vulnerabilities","short":"Determines if the project has open, known unfixed vulnerabilities."}}],"metadata":null}
|
||||
`,
|
||||
match: true,
|
||||
},
|
||||
{
|
||||
name: "unequal number of results",
|
||||
x: `{"date":"2023-08-11T10:22:43-07:00","repo":{"name":"github.com/foo/bar","commit":"f0840f7158c8044af2bd9b8aa661d7942b1f29d2"},"scorecard":{"version":"","commit":"unknown"},"score":10.0,"checks":[{"details":null,"score":10,"reason":"no vulnerabilities detected","name":"Vulnerabilities","documentation":{"url":"https://github.com/ossf/scorecard/blob/main/docs/checks.md#vulnerabilities","short":"Determines if the project has open, known unfixed vulnerabilities."}}],"metadata":null}
|
||||
{"date":"2023-08-11T10:22:43-07:00","repo":{"name":"github.com/foo/bar","commit":"f0840f7158c8044af2bd9b8aa661d7942b1f29d2"},"scorecard":{"version":"","commit":"unknown"},"score":10.0,"checks":[{"details":null,"score":10,"reason":"no vulnerabilities detected","name":"Vulnerabilities","documentation":{"url":"https://github.com/ossf/scorecard/blob/main/docs/checks.md#vulnerabilities","short":"Determines if the project has open, known unfixed vulnerabilities."}}],"metadata":null}
|
||||
`,
|
||||
y: `{"date":"2023-08-11T10:22:43-07:00","repo":{"name":"github.com/foo/bar","commit":"f0840f7158c8044af2bd9b8aa661d7942b1f29d2"},"scorecard":{"version":"","commit":"unknown"},"score":10.0,"checks":[{"details":null,"score":10,"reason":"no vulnerabilities detected","name":"Vulnerabilities","documentation":{"url":"https://github.com/ossf/scorecard/blob/main/docs/checks.md#vulnerabilities","short":"Determines if the project has open, known unfixed vulnerabilities."}}],"metadata":null}
|
||||
`,
|
||||
wantErrSubstr: "number of results",
|
||||
},
|
||||
{
|
||||
name: "results differ",
|
||||
x: `{"date":"2023-08-11T10:22:43-07:00","repo":{"name":"github.com/foo/bar","commit":"f0840f7158c8044af2bd9b8aa661d7942b1f29d2"},"scorecard":{"version":"","commit":"unknown"},"score":10.0,"checks":[{"details":null,"score":10,"reason":"no vulnerabilities detected","name":"Vulnerabilities","documentation":{"url":"https://github.com/ossf/scorecard/blob/main/docs/checks.md#vulnerabilities","short":"Determines if the project has open, known unfixed vulnerabilities."}}],"metadata":null}
|
||||
`,
|
||||
y: `{"date":"2023-08-11T10:22:43-07:00","repo":{"name":"github.com/foo/bar","commit":"f0840f7158c8044af2bd9b8aa661d7942b1f29d2"},"scorecard":{"version":"","commit":"unknown"},"score":7.0,"checks":[{"details":null,"score":7,"reason":"3 existing vulnerabilities detected","name":"Vulnerabilities","documentation":{"url":"https://github.com/ossf/scorecard/blob/main/docs/checks.md#vulnerabilities","short":"Determines if the project has open, known unfixed vulnerabilities."}}],"metadata":null}
|
||||
`,
|
||||
wantErrSubstr: "results differ",
|
||||
},
|
||||
{
|
||||
name: "x results fail to parse",
|
||||
x: `not a scorecard result
|
||||
`,
|
||||
y: `{"date":"2023-08-11T10:22:43-07:00","repo":{"name":"github.com/foo/bar","commit":"f0840f7158c8044af2bd9b8aa661d7942b1f29d2"},"scorecard":{"version":"","commit":"unknown"},"score":7.0,"checks":[{"details":null,"score":7,"reason":"3 existing vulnerabilities detected","name":"Vulnerabilities","documentation":{"url":"https://github.com/ossf/scorecard/blob/main/docs/checks.md#vulnerabilities","short":"Determines if the project has open, known unfixed vulnerabilities."}}],"metadata":null}
|
||||
`,
|
||||
wantErrSubstr: "parsing first",
|
||||
},
|
||||
{
|
||||
name: "y results fail to parse",
|
||||
x: `{"date":"2023-08-11T10:22:43-07:00","repo":{"name":"github.com/foo/bar","commit":"f0840f7158c8044af2bd9b8aa661d7942b1f29d2"},"scorecard":{"version":"","commit":"unknown"},"score":7.0,"checks":[{"details":null,"score":7,"reason":"3 existing vulnerabilities detected","name":"Vulnerabilities","documentation":{"url":"https://github.com/ossf/scorecard/blob/main/docs/checks.md#vulnerabilities","short":"Determines if the project has open, known unfixed vulnerabilities."}}],"metadata":null}
|
||||
`,
|
||||
y: `not a scorecard result
|
||||
`,
|
||||
wantErrSubstr: "parsing second",
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
tt := tt
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
x := strings.NewReader(tt.x)
|
||||
y := strings.NewReader(tt.y)
|
||||
err := compareReaders(x, y, os.Stderr)
|
||||
if (err != nil) == tt.match {
|
||||
t.Errorf("wanted match: %t, but got err: %v", tt.match, err)
|
||||
}
|
||||
if !tt.match && !strings.Contains(err.Error(), tt.wantErrSubstr) {
|
||||
t.Errorf("wanted err: %v, got err: %v", tt.wantErrSubstr, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
type alwaysErrorReader struct{}
|
||||
|
||||
func (a alwaysErrorReader) Read(b []byte) (n int, err error) {
|
||||
return 0, io.ErrClosedPipe
|
||||
}
|
||||
|
||||
//nolint:lll // results are long
|
||||
func Test_compare_reader_err(t *testing.T) {
|
||||
t.Parallel()
|
||||
//nolint:govet // struct alignment
|
||||
tests := []struct {
|
||||
name string
|
||||
x io.Reader
|
||||
y io.Reader
|
||||
}{
|
||||
{
|
||||
name: "error in y reader",
|
||||
x: strings.NewReader(`{"date":"2023-08-11T10:22:43-07:00","repo":{"name":"github.com/foo/bar","commit":"f0840f7158c8044af2bd9b8aa661d7942b1f29d2"},"scorecard":{"version":"","commit":"unknown"},"score":10.0,"checks":[{"details":null,"score":10,"reason":"no vulnerabilities detected","name":"Vulnerabilities","documentation":{"url":"https://github.com/ossf/scorecard/blob/main/docs/checks.md#vulnerabilities","short":"Determines if the project has open, known unfixed vulnerabilities."}}],"metadata":null}
|
||||
`),
|
||||
y: alwaysErrorReader{},
|
||||
},
|
||||
{
|
||||
name: "error in x reader",
|
||||
x: alwaysErrorReader{},
|
||||
y: strings.NewReader(`{"date":"2023-08-11T10:22:43-07:00","repo":{"name":"github.com/foo/bar","commit":"f0840f7158c8044af2bd9b8aa661d7942b1f29d2"},"scorecard":{"version":"","commit":"unknown"},"score":10.0,"checks":[{"details":null,"score":10,"reason":"no vulnerabilities detected","name":"Vulnerabilities","documentation":{"url":"https://github.com/ossf/scorecard/blob/main/docs/checks.md#vulnerabilities","short":"Determines if the project has open, known unfixed vulnerabilities."}}],"metadata":null}
|
||||
`),
|
||||
},
|
||||
{
|
||||
name: "error in both readesr",
|
||||
x: alwaysErrorReader{},
|
||||
y: alwaysErrorReader{},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
tt := tt
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
if err := compareReaders(tt.x, tt.y, os.Stderr); err == nil { // if NO error
|
||||
t.Errorf("wanted error, got none")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
@ -26,7 +26,7 @@ import (
|
||||
|
||||
const logLevel = log.DefaultLevel
|
||||
|
||||
func normalize(r *pkg.ScorecardResult) {
|
||||
func Normalize(r *pkg.ScorecardResult) {
|
||||
if r == nil {
|
||||
return
|
||||
}
|
||||
@ -55,6 +55,6 @@ func JSON(r *pkg.ScorecardResult, w io.Writer) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
normalize(r)
|
||||
Normalize(r)
|
||||
return r.AsJSON2(details, logLevel, docs, w)
|
||||
}
|
||||
|
@ -167,8 +167,8 @@ func TestJSON(t *testing.T) {
|
||||
|
||||
func Test_normalize_nil_safe(t *testing.T) {
|
||||
var x, y *pkg.ScorecardResult
|
||||
normalize(x)
|
||||
normalize(y)
|
||||
Normalize(x)
|
||||
Normalize(y)
|
||||
if !cmp.Equal(x, y) {
|
||||
t.Errorf("normalized results differ: %v", cmp.Diff(x, y))
|
||||
}
|
||||
|
@ -12,6 +12,20 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
/*
|
||||
Scdiff is a tool to create and diff goldens when analyzing results.
|
||||
These results can be from different points in time, or generated by differnet versions of Scorecard.
|
||||
|
||||
Intended usage:
|
||||
|
||||
1. Use scdiff generate on a list of repositories to produce the golden file.
|
||||
|
||||
2. Similarly, evaluate the same list using the proposed changed.
|
||||
|
||||
3. Run scdiff compare to compare the two files.
|
||||
|
||||
4. If changes are intentional and acceptable, run scdiff accept to merge the changes to the golden.
|
||||
*/
|
||||
package main
|
||||
|
||||
import "github.com/ossf/scorecard/v4/cmd/internal/scdiff/app"
|
||||
|
@ -110,3 +110,28 @@ func typeToString(cd checker.DetailType) string {
|
||||
return "Debug"
|
||||
}
|
||||
}
|
||||
|
||||
func stringToDetailType(s string) checker.DetailType {
|
||||
switch s {
|
||||
case "Debug":
|
||||
return checker.DetailDebug
|
||||
case "Info":
|
||||
return checker.DetailInfo
|
||||
case "Warn":
|
||||
return checker.DetailWarn
|
||||
}
|
||||
return checker.DetailType(-1) // Uhhh todo err, at least for now return an unofficial DetailUnknown
|
||||
}
|
||||
|
||||
func stringToDetail(s string) checker.CheckDetail {
|
||||
parts := strings.SplitN(s, ":", 3)
|
||||
if len(parts) < 2 {
|
||||
return checker.CheckDetail{} // err?
|
||||
}
|
||||
return checker.CheckDetail{
|
||||
Type: stringToDetailType(parts[0]),
|
||||
Msg: checker.LogMessage{
|
||||
Text: parts[1],
|
||||
},
|
||||
}
|
||||
}
|
||||
|
44
pkg/json.go
44
pkg/json.go
@ -20,6 +20,7 @@ import (
|
||||
"io"
|
||||
"time"
|
||||
|
||||
"github.com/ossf/scorecard/v4/checker"
|
||||
docs "github.com/ossf/scorecard/v4/docs/checks"
|
||||
sce "github.com/ossf/scorecard/v4/errors"
|
||||
"github.com/ossf/scorecard/v4/log"
|
||||
@ -175,6 +176,49 @@ func (r *ScorecardResult) AsJSON2(showDetails bool,
|
||||
return nil
|
||||
}
|
||||
|
||||
// This function is experimental. Do not depend on it, it may be removed at any point.
|
||||
func ExperimentalFromJSON2(r io.Reader) (ScorecardResult, error) {
|
||||
var jsr JSONScorecardResultV2
|
||||
decoder := json.NewDecoder(r)
|
||||
if err := decoder.Decode(&jsr); err != nil {
|
||||
return ScorecardResult{}, fmt.Errorf("decode json: %w", err)
|
||||
}
|
||||
|
||||
date, err := time.Parse(time.RFC3339, jsr.Date)
|
||||
if err != nil {
|
||||
return ScorecardResult{}, fmt.Errorf("parse scorecard analysis time: %w", err)
|
||||
}
|
||||
|
||||
sr := ScorecardResult{
|
||||
Repo: RepoInfo{
|
||||
Name: jsr.Repo.Name,
|
||||
CommitSHA: jsr.Repo.Commit,
|
||||
},
|
||||
Scorecard: ScorecardInfo{
|
||||
Version: jsr.Scorecard.Version,
|
||||
CommitSHA: jsr.Scorecard.Commit,
|
||||
},
|
||||
Date: date,
|
||||
Metadata: jsr.Metadata,
|
||||
Checks: make([]checker.CheckResult, 0, len(jsr.Checks)),
|
||||
}
|
||||
|
||||
for _, check := range jsr.Checks {
|
||||
cr := checker.CheckResult{
|
||||
Name: check.Name,
|
||||
Score: check.Score,
|
||||
Reason: check.Reason,
|
||||
}
|
||||
cr.Details = make([]checker.CheckDetail, 0, len(check.Details))
|
||||
for _, detail := range check.Details {
|
||||
cr.Details = append(cr.Details, stringToDetail(detail))
|
||||
}
|
||||
sr.Checks = append(sr.Checks, cr)
|
||||
}
|
||||
|
||||
return sr, nil
|
||||
}
|
||||
|
||||
func (r *ScorecardResult) AsFJSON(showDetails bool,
|
||||
logLevel log.Level, checkDocs docs.Doc, writer io.Writer,
|
||||
) error {
|
||||
|
Loading…
Reference in New Issue
Block a user