From a38ff5815c415be5c7d608512cdd3d6c548035ed Mon Sep 17 00:00:00 2001 From: Tim Burks Date: Thu, 30 Mar 2023 12:18:14 -0700 Subject: [PATCH] Update 'compute lint' to use linter plugins. (#1125) --- cmd/registry/cmd/compute/lint/lint.go | 88 ++++---- cmd/registry/cmd/compute/lint/lint_test.go | 117 ++++++++++ cmd/registry/conformance/conformance-task.go | 75 +------ cmd/registry/conformance/linter.go | 87 ++++++++ .../plugins/registry-lint-api-linter/main.go | 5 + .../plugins/registry-lint-spectral/main.go | 30 ++- .../plugins/registry-lint-test/main.go | 80 +++++++ .../plugins/registry-lint-test/main_test.go | 209 ++++++++++++++++++ 8 files changed, 565 insertions(+), 126 deletions(-) create mode 100644 cmd/registry/plugins/registry-lint-test/main.go create mode 100644 cmd/registry/plugins/registry-lint-test/main_test.go diff --git a/cmd/registry/cmd/compute/lint/lint.go b/cmd/registry/cmd/compute/lint/lint.go index 73289923b..7610d6f45 100644 --- a/cmd/registry/cmd/compute/lint/lint.go +++ b/cmd/registry/cmd/compute/lint/lint.go @@ -16,12 +16,14 @@ package lint import ( "context" + "errors" "fmt" + "os" + "os/exec" + "github.com/apigee/registry/cmd/registry/conformance" "github.com/apigee/registry/cmd/registry/tasks" - "github.com/apigee/registry/pkg/application/style" "github.com/apigee/registry/pkg/connection" - "github.com/apigee/registry/pkg/log" "github.com/apigee/registry/pkg/mime" "github.com/apigee/registry/pkg/names" "github.com/apigee/registry/pkg/visitor" @@ -32,47 +34,57 @@ import ( ) func Command() *cobra.Command { - var linter string cmd := &cobra.Command{ Use: "lint SPEC", Short: "Compute lint results for API specs", Args: cobra.ExactArgs(1), - Run: func(cmd *cobra.Command, args []string) { + RunE: func(cmd *cobra.Command, args []string) error { ctx := cmd.Context() c, err := connection.ActiveConfig() if err != nil { - log.FromContext(ctx).WithError(err).Fatal("Failed to get config") + return err } args[0] = c.FQName(args[0]) + linter, err := cmd.Flags().GetString("linter") + if err != nil { + return fmt.Errorf("failed to get linter from flags: %s", err) + } + if linter == "" { + return errors.New("--linter argument cannot be empty") + } + if _, err = exec.LookPath(fmt.Sprintf("registry-lint-%s", linter)); err != nil { + return err + } + filter, err := cmd.Flags().GetString("filter") if err != nil { - log.FromContext(ctx).WithError(err).Fatal("Failed to get filter from flags") + return fmt.Errorf("failed to get filter from flags: %s", err) } dryRun, err := cmd.Flags().GetBool("dry-run") if err != nil { - log.FromContext(ctx).WithError(err).Fatal("Failed to get fry-run from flags") + return fmt.Errorf("failed to get dry-run from flags: %s", err) } client, err := connection.NewRegistryClientWithSettings(ctx, c) if err != nil { - log.FromContext(ctx).WithError(err).Fatal("Failed to get client") + return err } // Initialize task queue. jobs, err := cmd.Flags().GetInt("jobs") if err != nil { - log.FromContext(ctx).WithError(err).Fatal("Failed to get jobs from flags") + return fmt.Errorf("failed to get jobs from flags: %s", err) } taskQueue, wait := tasks.WorkerPoolIgnoreError(ctx, jobs) defer wait() spec, err := names.ParseSpec(args[0]) if err != nil { - log.FromContext(ctx).WithError(err).Fatal("Failed parse") + return fmt.Errorf("invalid spec pattern %s", args[0]) } // Iterate through a collection of specs and evaluate each. - err = visitor.ListSpecs(ctx, client, spec, filter, false, func(ctx context.Context, spec *rpc.ApiSpec) error { + return visitor.ListSpecs(ctx, client, spec, filter, false, func(ctx context.Context, spec *rpc.ApiSpec) error { taskQueue <- &computeLintTask{ client: client, spec: spec, @@ -81,13 +93,14 @@ func Command() *cobra.Command { } return nil }) - if err != nil { - log.FromContext(ctx).WithError(err).Fatal("Failed to list specs") - } }, } - cmd.Flags().StringVar(&linter, "linter", "", "the linter to use (aip|spectral|gnostic)") + cmd.Flags().String("linter", "", "the linter to use") + _ = cmd.MarkFlagRequired("linter") + cmd.Flags().String("filter", "", "Filter selected resources") + cmd.Flags().Bool("dry-run", false, "if set, computation results will only be printed and will not stored in the registry") + cmd.Flags().Int("jobs", 10, "Number of actions to perform concurrently") return cmd } @@ -107,50 +120,27 @@ func lintRelation(linter string) string { } func (task *computeLintTask) Run(ctx context.Context) error { - spec := task.spec - err := visitor.FetchSpecContents(ctx, task.client, spec) + root, err := conformance.WriteSpecForLinting(ctx, task.client, task.spec) + if root != "" { + defer os.RemoveAll(root) + } if err != nil { return err } - var relation string - var lint *style.Lint - if mime.IsOpenAPIv2(spec.GetMimeType()) || mime.IsOpenAPIv3(spec.GetMimeType()) { - // the default openapi linter is gnostic - if task.linter == "" { - task.linter = "gnostic" - } - relation = lintRelation(task.linter) - log.Debugf(ctx, "Computing %s/artifacts/%s", spec.Name, relation) - lint, err = NewLintFromOpenAPI(spec.Name, spec.Contents, task.linter) - if err != nil { - return fmt.Errorf("error processing OpenAPI: %s (%s)", spec.Name, err.Error()) - } - } else if mime.IsDiscovery(spec.GetMimeType()) { - return fmt.Errorf("unsupported Discovery document: %s", spec.Name) - } else if mime.IsProto(spec.GetMimeType()) && mime.IsZipArchive(spec.GetMimeType()) { - // the default proto linter is the aip linter - if task.linter == "" { - task.linter = "aip" - } - relation = lintRelation(task.linter) - log.Debugf(ctx, "Computing %s/artifacts/%s", spec.Name, relation) - lint, err = NewLintFromZippedProtos(spec.Name, spec.Contents) - if err != nil { - return fmt.Errorf("error processing protos: %s (%s)", spec.Name, err.Error()) - } - } else { - return fmt.Errorf("we don't know how to lint %s", spec.Name) + linterMetadata := conformance.SimpleLinterMetadata(task.linter) + response, err := conformance.RunLinter(ctx, root, linterMetadata) + if err != nil { + return err } - + lint := response.Lint if task.dryRun { fmt.Println(protojson.Format((lint))) return nil } - - subject := spec.GetName() + subject := task.spec.GetName() messageData, _ := proto.Marshal(lint) artifact := &rpc.Artifact{ - Name: subject + "/artifacts/" + relation, + Name: subject + "/artifacts/" + lintRelation(task.linter), MimeType: mime.MimeTypeForMessageType("google.cloud.apigeeregistry.v1.style.Lint"), Contents: messageData, } diff --git a/cmd/registry/cmd/compute/lint/lint_test.go b/cmd/registry/cmd/compute/lint/lint_test.go index 1396147ce..6525a99ef 100644 --- a/cmd/registry/cmd/compute/lint/lint_test.go +++ b/cmd/registry/cmd/compute/lint/lint_test.go @@ -15,10 +15,18 @@ package lint import ( + "context" "testing" + "github.com/apigee/registry/cmd/registry/cmd/apply" + "github.com/apigee/registry/pkg/application/style" + "github.com/apigee/registry/pkg/connection" "github.com/apigee/registry/pkg/connection/grpctest" + "github.com/apigee/registry/pkg/names" + "github.com/apigee/registry/pkg/visitor" + "github.com/apigee/registry/rpc" "github.com/apigee/registry/server/registry" + "google.golang.org/protobuf/proto" ) // TestMain will set up a local RegistryServer and grpc.Server for all @@ -30,7 +38,116 @@ func TestMain(m *testing.M) { func TestLint(t *testing.T) { command := Command() + command.SilenceErrors = true + command.SilenceUsage = true if err := command.Execute(); err == nil { t.Fatalf("Execute() with no args succeeded and should have failed") } } + +func TestInvalidComputeLint(t *testing.T) { + tests := []struct { + desc string + args []string + }{ + { + desc: "no-linter-specified", + args: []string{"spec"}, + }, + { + desc: "missing-linter-specified", + args: []string{"spec", "--linter", "nonexistent"}, + }, + { + desc: "empty-linter-specified", + args: []string{"spec", "--linter", ""}, + }, + } + for _, test := range tests { + t.Run(test.desc, func(t *testing.T) { + command := Command() + command.SilenceErrors = true + command.SilenceUsage = true + command.SetArgs(test.args) + if err := command.Execute(); err == nil { + t.Fatalf("Execute() with no args succeeded and should have failed") + } + }) + } +} + +func TestComputeLint(t *testing.T) { + project := names.Project{ProjectID: "lint-test"} + ctx := context.Background() + registryClient, _ := grpctest.SetupRegistry(ctx, t, project.ProjectID, nil) + + config, err := connection.ActiveConfig() + if err != nil { + t.Fatalf("Setup: Failed to get registry configuration: %s", err) + } + config.Project = project.ProjectID + connection.SetConfig(config) + + applyCmd := apply.Command() + applyCmd.SetArgs([]string{"-f", "../complexity/testdata/apigeeregistry", "-R"}) + if err := applyCmd.Execute(); err != nil { + t.Fatalf("Failed to apply test API") + } + + t.Run("protos", func(t *testing.T) { + specName := project.Api("apigeeregistry").Version("v1").Spec("protos") + lintCmd := Command() + lintCmd.SetArgs([]string{specName.String(), "--linter", "test"}) + if err := lintCmd.Execute(); err != nil { + t.Fatalf("Compute lint failed: %s", err) + } + artifactName := specName.Artifact("lint-test") + if err = visitor.GetArtifact(ctx, registryClient, artifactName, true, func(ctx context.Context, message *rpc.Artifact) error { + var lint style.Lint + if err = proto.Unmarshal(message.Contents, &lint); err != nil { + return err + } + return nil + }); err != nil { + t.Fatalf("Error getting artifact: %s", err) + } + }) + + t.Run("openapi", func(t *testing.T) { + specName := project.Api("apigeeregistry").Version("v1").Spec("openapi") + lintCmd := Command() + lintCmd.SetArgs([]string{specName.String(), "--linter", "test"}) + if err := lintCmd.Execute(); err != nil { + t.Fatalf("Compute lint failed: %s", err) + } + artifactName := specName.Artifact("lint-test") + if err = visitor.GetArtifact(ctx, registryClient, artifactName, true, func(ctx context.Context, message *rpc.Artifact) error { + var lint style.Lint + if err = proto.Unmarshal(message.Contents, &lint); err != nil { + return err + } + return nil + }); err != nil { + t.Fatalf("Error getting artifact: %s", err) + } + }) + + t.Run("discovery", func(t *testing.T) { + specName := project.Api("apigeeregistry").Version("v1").Spec("discovery") + lintCmd := Command() + lintCmd.SetArgs([]string{specName.String(), "--linter", "test"}) + if err := lintCmd.Execute(); err != nil { + t.Fatalf("Compute lint failed: %s", err) + } + artifactName := specName.Artifact("lint-test") + if err = visitor.GetArtifact(ctx, registryClient, artifactName, true, func(ctx context.Context, message *rpc.Artifact) error { + var lint style.Lint + if err = proto.Unmarshal(message.Contents, &lint); err != nil { + return err + } + return nil + }); err != nil { + t.Fatalf("Error getting artifact: %s", err) + } + }) +} diff --git a/cmd/registry/conformance/conformance-task.go b/cmd/registry/conformance/conformance-task.go index bf9df1dc9..737f757a1 100644 --- a/cmd/registry/conformance/conformance-task.go +++ b/cmd/registry/conformance/conformance-task.go @@ -15,15 +15,11 @@ package conformance import ( - "bytes" "context" "fmt" "os" - "os/exec" "path/filepath" - "time" - "github.com/apigee/registry/cmd/registry/compress" "github.com/apigee/registry/pkg/application/style" "github.com/apigee/registry/pkg/connection" "github.com/apigee/registry/pkg/log" @@ -94,33 +90,15 @@ func (task *ComputeConformanceTask) String() string { func (task *ComputeConformanceTask) Run(ctx context.Context) error { log.Debugf(ctx, "Computing conformance report %s/artifacts/%s", task.Spec.GetName(), conformanceReportId(task.StyleguideId)) - err := visitor.FetchSpecContents(ctx, task.Client, task.Spec) - if err != nil { - return err - } - // Put the spec in a temporary directory. - root, err := os.MkdirTemp("", "registry-spec-") - if err != nil { - return err - } - filename := task.Spec.GetFilename() - if filename == "" { - return fmt.Errorf("%s does not specify a filename", task.Spec.GetName()) - } - name := filepath.Base(filename) - defer os.RemoveAll(root) - - if mime.IsZipArchive(task.Spec.GetMimeType()) { - _, err = compress.UnzipArchiveToPath(task.Spec.GetContents(), root) - } else { - // Write the file to the temporary directory. - err = os.WriteFile(filepath.Join(root, name), task.Spec.GetContents(), 0644) + root, err := WriteSpecForLinting(ctx, task.Client, task.Spec) + if root != "" { + defer os.RemoveAll(root) } if err != nil { return err } - // Get project + // Get project ID from spec name spec, err := names.ParseSpecRevision(task.Spec.GetName()) if err != nil { return err @@ -130,7 +108,7 @@ func (task *ComputeConformanceTask) Run(ctx context.Context) error { conformanceReport := initializeConformanceReport(task.Spec.GetName(), task.StyleguideId, spec.ProjectID) guidelineReportsMap := make(map[string]int) for _, metadata := range task.LintersMetadata { - linterResponse, err := task.invokeLinter(ctx, root, metadata) + linterResponse, err := RunLinter(ctx, root, metadata) // If a linter returned an error, we shouldn't stop linting completely across all linters and // discard the conformance report for this spec. We should log but still continue, because there // may still be useful information from other linters that we may be discarding. @@ -149,49 +127,6 @@ func (task *ComputeConformanceTask) Run(ctx context.Context) error { return task.storeConformanceReport(ctx, conformanceReport) } -func (task *ComputeConformanceTask) invokeLinter( - ctx context.Context, - specDirectory string, - metadata *linterMetadata) (*style.LinterResponse, error) { - // Formulate the request. - requestBytes, err := proto.Marshal(&style.LinterRequest{ - SpecDirectory: specDirectory, - RuleIds: metadata.rules, - }) - if err != nil { - return nil, fmt.Errorf("failed marshaling linterRequest, Error: %s ", err) - } - - executableName := getLinterBinaryName(metadata.name) - cmd := exec.Command(executableName) - cmd.Stdin = bytes.NewReader(requestBytes) - cmd.Stderr = os.Stderr - - pluginStartTime := time.Now() - // Run the linter. - output, err := cmd.Output() - if err != nil { - return nil, fmt.Errorf("running the plugin %s return error: %s", executableName, err) - } - - pluginElapsedTime := time.Since(pluginStartTime) - log.Debugf(ctx, "Plugin %s ran in time %s", executableName, pluginElapsedTime) - - // Unmarshal the output bytes into a response object. If there's a failure, log and continue. - linterResponse := &style.LinterResponse{} - err = proto.Unmarshal(output, linterResponse) - if err != nil { - return nil, fmt.Errorf("failed unmarshalling LinterResponse (plugins must write log messages to stderr, not stdout): %s", err) - } - - // Check if there were any errors in the plugin. - if len(linterResponse.GetErrors()) > 0 { - return nil, fmt.Errorf("plugin %s encountered errors: %v", executableName, linterResponse.GetErrors()) - } - - return linterResponse, nil -} - func (task *ComputeConformanceTask) computeConformanceReport( ctx context.Context, conformanceReport *style.ConformanceReport, diff --git a/cmd/registry/conformance/linter.go b/cmd/registry/conformance/linter.go index ff1ca5d69..e7cb31aa7 100644 --- a/cmd/registry/conformance/linter.go +++ b/cmd/registry/conformance/linter.go @@ -15,9 +15,22 @@ package conformance import ( + "bytes" + "context" "fmt" + "os" + "os/exec" + "path/filepath" + "time" + "github.com/apigee/registry/cmd/registry/compress" "github.com/apigee/registry/pkg/application/style" + "github.com/apigee/registry/pkg/connection" + "github.com/apigee/registry/pkg/log" + "github.com/apigee/registry/pkg/mime" + "github.com/apigee/registry/pkg/visitor" + "github.com/apigee/registry/rpc" + "google.golang.org/protobuf/proto" ) type ruleMetadata struct { @@ -35,6 +48,10 @@ func getLinterBinaryName(linterName string) string { return "registry-lint-" + linterName } +func SimpleLinterMetadata(linter string) *linterMetadata { + return &linterMetadata{name: linter} +} + func GenerateLinterMetadata(styleguide *style.StyleGuide) (map[string]*linterMetadata, error) { linterNameToMetadata := make(map[string]*linterMetadata) @@ -79,3 +96,73 @@ func GenerateLinterMetadata(styleguide *style.StyleGuide) (map[string]*linterMet } return linterNameToMetadata, nil } + +func WriteSpecForLinting(ctx context.Context, client connection.RegistryClient, spec *rpc.ApiSpec) (string, error) { + err := visitor.FetchSpecContents(ctx, client, spec) + if err != nil { + return "", err + } + // Put the spec in a temporary directory. + root, err := os.MkdirTemp("", "registry-spec-") + if err != nil { + return "", err + } + filename := spec.GetFilename() + if filename == "" { + return root, fmt.Errorf("%s does not specify a filename", spec.GetName()) + } + name := filepath.Base(filename) + + if mime.IsZipArchive(spec.GetMimeType()) { + _, err = compress.UnzipArchiveToPath(spec.GetContents(), root) + } else { + // Write the file to the temporary directory. + err = os.WriteFile(filepath.Join(root, name), spec.GetContents(), 0644) + } + if err != nil { + return root, err + } + return root, nil +} + +func RunLinter(ctx context.Context, + specDirectory string, + metadata *linterMetadata) (*style.LinterResponse, error) { + // Formulate the request. + requestBytes, err := proto.Marshal(&style.LinterRequest{ + SpecDirectory: specDirectory, + RuleIds: metadata.rules, + }) + if err != nil { + return nil, fmt.Errorf("failed marshaling linterRequest, Error: %s ", err) + } + + executableName := getLinterBinaryName(metadata.name) + cmd := exec.Command(executableName) + cmd.Stdin = bytes.NewReader(requestBytes) + cmd.Stderr = os.Stderr + + pluginStartTime := time.Now() + // Run the linter. + output, err := cmd.Output() + if err != nil { + return nil, fmt.Errorf("running the plugin %s return error: %s", executableName, err) + } + + pluginElapsedTime := time.Since(pluginStartTime) + log.Debugf(ctx, "Plugin %s ran in time %s", executableName, pluginElapsedTime) + + // Unmarshal the output bytes into a response object. If there's a failure, log and continue. + linterResponse := &style.LinterResponse{} + err = proto.Unmarshal(output, linterResponse) + if err != nil { + return nil, fmt.Errorf("failed unmarshalling LinterResponse (plugins must write log messages to stderr, not stdout): %s", err) + } + + // Check if there were any errors in the plugin. + if len(linterResponse.GetErrors()) > 0 { + return nil, fmt.Errorf("plugin %s encountered errors: %v", executableName, linterResponse.GetErrors()) + } + + return linterResponse, nil +} diff --git a/cmd/registry/plugins/registry-lint-api-linter/main.go b/cmd/registry/plugins/registry-lint-api-linter/main.go index 9c862c17b..a6a383f68 100644 --- a/cmd/registry/plugins/registry-lint-api-linter/main.go +++ b/cmd/registry/plugins/registry-lint-api-linter/main.go @@ -111,6 +111,11 @@ func (linter *apiLinterRunner) filterProblems( problems []*style.LintProblem, rules []string, ) []*style.LintProblem { + // If no rules were specified, return the list without filtering. + if len(rules) == 0 { + return problems + } + // Construct a set of all the problems enabled for this mimetype // so we have efficient lookup. enabledProblems := make(map[string]bool) diff --git a/cmd/registry/plugins/registry-lint-spectral/main.go b/cmd/registry/plugins/registry-lint-spectral/main.go index 7d0d35bee..99487b904 100644 --- a/cmd/registry/plugins/registry-lint-spectral/main.go +++ b/cmd/registry/plugins/registry-lint-spectral/main.go @@ -16,6 +16,7 @@ package main import ( "encoding/json" + "log" "os" "os/exec" "path/filepath" @@ -125,24 +126,26 @@ func (linter *spectralLinterRunner) createConfigurationFile(root string, ruleIds // Create the spectral configuration. configuration := spectralConfiguration{} configuration.Rules = make(map[string]bool) - configuration.Extends = [][]string{{"spectral:oas", "off"}, {"spectral:asyncapi", "off"}} + if len(ruleIds) == 0 { + // if no rules were specified, use the default rules. + configuration.Extends = [][]string{{"spectral:oas", "all"}, {"spectral:asyncapi", "all"}} + } else { + configuration.Extends = [][]string{{"spectral:oas", "off"}, {"spectral:asyncapi", "off"}} + } for _, ruleName := range ruleIds { configuration.Rules[ruleName] = true } - // Marshal the configuration into a file. file, err := json.MarshalIndent(configuration, "", " ") if err != nil { return "", err } - // Write the configuration to the temporary directory. configPath := filepath.Join(root, "spectral.json") err = os.WriteFile(configPath, file, 0644) if err != nil { return "", err } - return configPath, nil } @@ -189,9 +192,22 @@ func runSpectralLinter(specPath, configPath string) ([]*spectralLintResult, erro "--output", outputPath, ) - // Ignore errors from Spectral because Spectral returns an - // error result when APIs have errors. - _ = cmd.Run() + output, err := cmd.CombinedOutput() + if err != nil { + switch v := err.(type) { + case *exec.ExitError: + code := v.ExitCode() + if code == 1 { + // This just means there were linter errors + } else { + log.Printf("linter error %T (%s)", err, specPath) + log.Printf("%s", string(output)) + } + default: + log.Printf("linter error %T (%s)", err, specPath) + log.Printf("%s", string(output)) + } + } // Read and parse the spectral output. b, err := os.ReadFile(outputPath) diff --git a/cmd/registry/plugins/registry-lint-test/main.go b/cmd/registry/plugins/registry-lint-test/main.go new file mode 100644 index 000000000..7db73459d --- /dev/null +++ b/cmd/registry/plugins/registry-lint-test/main.go @@ -0,0 +1,80 @@ +// 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. + +package main + +import ( + "fmt" + "io/fs" + "os" + "path/filepath" + "strings" + + "github.com/apigee/registry/cmd/registry/plugins/linter" + "github.com/apigee/registry/pkg/application/style" +) + +type testLinterRunner struct{} + +func (*testLinterRunner) Run(req *style.LinterRequest) (*style.LinterResponse, error) { + lintFiles := make([]*style.LintFile, 0) + err := filepath.WalkDir(req.SpecDirectory, + func(p string, entry fs.DirEntry, err error) error { + if err != nil { + return err + } else if entry.IsDir() { + return nil // Do nothing for the directory, but still walk its contents. + } + bytes, err := os.ReadFile(p) + if err != nil { + return err + } + lines := strings.Split(string(bytes), "\n") + filePath := strings.TrimPrefix(p, req.SpecDirectory+"/") + lintFile := &style.LintFile{ + FilePath: filePath, + Problems: []*style.LintProblem{{ + RuleId: "size", + Message: fmt.Sprintf("%d", len(bytes)), + RuleDocUri: "https://github.com/apigee/registry", + Suggestion: fmt.Sprintf("This is the size of %s.", filePath), + Location: &style.LintLocation{ + StartPosition: &style.LintPosition{ + LineNumber: 1, + ColumnNumber: 1, + }, + EndPosition: &style.LintPosition{ + LineNumber: int32(len(lines) - 1), + ColumnNumber: int32(len(lines[len(lines)-1]) + 1), + }, + }, + }}, + } + lintFiles = append(lintFiles, lintFile) + return nil + }) + if err != nil { + return nil, fs.ErrClosed + } + return &style.LinterResponse{ + Lint: &style.Lint{ + Name: "registry-lint-test", + Files: lintFiles, + }, + }, nil +} + +func main() { + linter.Main(&testLinterRunner{}) +} diff --git a/cmd/registry/plugins/registry-lint-test/main_test.go b/cmd/registry/plugins/registry-lint-test/main_test.go new file mode 100644 index 000000000..0320da8ea --- /dev/null +++ b/cmd/registry/plugins/registry-lint-test/main_test.go @@ -0,0 +1,209 @@ +// 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. + +package main + +import ( + "os" + "path/filepath" + "testing" + + "github.com/apigee/registry/pkg/application/style" + "github.com/stretchr/testify/assert" +) + +func TestTestLinter(t *testing.T) { + var err error + specDirectory := t.TempDir() + err = os.WriteFile(filepath.Join(specDirectory, "openapi.yaml"), []byte(petstore), 0666) + if err != nil { + t.Fatal("Failed to create test file") + } + err = os.WriteFile(filepath.Join(specDirectory, "petstore.yaml"), []byte(petstore), 0666) + if err != nil { + t.Fatal("Failed to create test file") + } + request := &style.LinterRequest{ + SpecDirectory: specDirectory, + } + expectedResponse := &style.LinterResponse{ + Lint: &style.Lint{ + Name: "registry-lint-test", + Files: []*style.LintFile{ + { + FilePath: "openapi.yaml", + Problems: []*style.LintProblem{ + { + Message: "2618", + RuleId: "size", + RuleDocUri: "https://github.com/apigee/registry", + Suggestion: "This is the size of openapi.yaml.", + Location: &style.LintLocation{ + StartPosition: &style.LintPosition{ + LineNumber: 1, + ColumnNumber: 1, + }, + EndPosition: &style.LintPosition{ + LineNumber: 113, + ColumnNumber: 1, + }, + }, + }, + }, + }, + { + FilePath: "petstore.yaml", + Problems: []*style.LintProblem{ + { + Message: "2618", + RuleId: "size", + RuleDocUri: "https://github.com/apigee/registry", + Suggestion: "This is the size of petstore.yaml.", + Location: &style.LintLocation{ + StartPosition: &style.LintPosition{ + LineNumber: 1, + ColumnNumber: 1, + }, + EndPosition: &style.LintPosition{ + LineNumber: 113, + ColumnNumber: 1, + }, + }, + }, + }, + }, + }, + }, + } + response, err := (&testLinterRunner{}).Run(request) + if err != nil { + t.Fatalf("Linter failed with error %s", err) + } + assert.EqualValues(t, expectedResponse, response) +} + +const petstore = `openapi: "3.0.0" +info: + version: 1.0.0 + title: Swagger Petstore + license: + name: MIT +servers: + - url: http://petstore.swagger.io/v1 +paths: + /pets: + get: + summary: List all pets + operationId: listPets + tags: + - pets + parameters: + - name: limit + in: query + description: How many items to return at one time (max 100) + required: false + schema: + type: integer + maximum: 100 + format: int32 + responses: + '200': + description: A paged array of pets + headers: + x-next: + description: A link to the next page of responses + schema: + type: string + content: + application/json: + schema: + $ref: "#/components/schemas/Pets" + default: + description: unexpected error + content: + application/json: + schema: + $ref: "#/components/schemas/Error" + post: + summary: Create a pet + operationId: createPets + tags: + - pets + responses: + '201': + description: Null response + default: + description: unexpected error + content: + application/json: + schema: + $ref: "#/components/schemas/Error" + /pets/{petId}: + get: + summary: Info for a specific pet + operationId: showPetById + tags: + - pets + parameters: + - name: petId + in: path + required: true + description: The id of the pet to retrieve + schema: + type: string + responses: + '200': + description: Expected response to a valid request + content: + application/json: + schema: + $ref: "#/components/schemas/Pet" + default: + description: unexpected error + content: + application/json: + schema: + $ref: "#/components/schemas/Error" +components: + schemas: + Pet: + type: object + required: + - id + - name + properties: + id: + type: integer + format: int64 + name: + type: string + tag: + type: string + Pets: + type: array + maxItems: 100 + items: + $ref: "#/components/schemas/Pet" + Error: + type: object + required: + - code + - message + properties: + code: + type: integer + format: int32 + message: + type: string +`