diff --git a/README.md b/README.md index 30e34e9..3cedd97 100644 --- a/README.md +++ b/README.md @@ -19,6 +19,8 @@ The detailed result of the checks are described in a [JSON schema](https://grafa - `license` - checks whether there is a suitable OSS license - `git` - checks if the directory is git workdir - `versions` - checks for semantic versioning git tags + - `build` - checks if k6 can be built with the extension + - `smoke` - checks if the smoke test script exists and runs successfully ## Install @@ -58,7 +60,11 @@ Details ✔ git found git worktree ✔ versions - found `6` versions, the latest is `v0.3.0` + found `13` versions, the latest is `v1.0.0` +✔ build + can be built with the latest k6 version +✔ smoke + `smoke.test.ts` successfully run with k6 ``` @@ -99,14 +105,24 @@ Details "passed": true }, { - "details": "found `6` versions, the latest is `v0.3.0`", + "details": "found `13` versions, the latest is `v1.0.0`", "id": "versions", "passed": true + }, + { + "details": "can be built with the latest k6 version", + "id": "build", + "passed": true + }, + { + "details": "`smoke.test.ts` successfully run with k6", + "id": "smoke", + "passed": true } ], "grade": "A", "level": 100, - "timestamp": 1724833956 + "timestamp": 1731058317 } ``` diff --git a/checker_module.go b/checker_module.go index f913f95..f1799a1 100644 --- a/checker_module.go +++ b/checker_module.go @@ -2,14 +2,18 @@ package k6lint import ( "context" + "fmt" "os" + "os/exec" "path/filepath" + "regexp" "golang.org/x/mod/modfile" ) type moduleChecker struct { file *modfile.File + exe string } func newModuleChecker() *moduleChecker { @@ -32,12 +36,9 @@ func (mc *moduleChecker) hasGoModule(_ context.Context, dir string) *checkResult return checkPassed("found `%s` as go module", mc.file.Module.Mod.String()) } -func (mc *moduleChecker) hasNoReplace(ctx context.Context, dir string) *checkResult { +func (mc *moduleChecker) hasNoReplace(_ context.Context, _ string) *checkResult { if mc.file == nil { - res := mc.hasGoModule(ctx, dir) - if !res.passed { - return res - } + return checkFailed("missing go.mod") } if len(mc.file.Replace) != 0 { @@ -46,3 +47,52 @@ func (mc *moduleChecker) hasNoReplace(ctx context.Context, dir string) *checkRes return checkPassed("no `replace` directive in the `go.mod` file") } + +func (mc *moduleChecker) canBuild(ctx context.Context, dir string) *checkResult { + if mc.file == nil { + return checkFailed("missing go.mod") + } + + exe, err := build(ctx, mc.file.Module.Mod.Path, dir) + if err != nil { + return checkError(err) + } + + mc.exe = exe + + return checkPassed("can be built with the latest k6 version") +} + +var reSmoke = regexp.MustCompile(`(?i)^smoke(\.test)?\.(?:js|ts)`) + +//nolint:forbidigo +func (mc *moduleChecker) smoke(ctx context.Context, dir string) *checkResult { + if mc.exe == "" { + return checkFailed("can't build") + } + + filename, sortname, err := findFile(reSmoke, + dir, + filepath.Join(dir, "test"), + filepath.Join(dir, "tests"), + filepath.Join(dir, "examples"), + ) + if err != nil { + return checkError(err) + } + + if len(sortname) == 0 { + return checkFailed("no smoke test file found") + } + + cmd := exec.CommandContext(ctx, mc.exe, "run", "--no-usage-report", "--no-summary", "--quiet", filename) //nolint:gosec + + out, err := cmd.CombinedOutput() + if err != nil { + fmt.Fprintln(os.Stderr, string(out)) + + return checkError(err) + } + + return checkPassed("`%s` successfully run with k6", sortname) +} diff --git a/checker_readme.go b/checker_readme.go index 0167596..0fbb1fa 100644 --- a/checker_readme.go +++ b/checker_readme.go @@ -2,7 +2,6 @@ package k6lint import ( "context" - "os" "regexp" ) @@ -11,15 +10,13 @@ var reREADME = regexp.MustCompile( ) func checkerReadme(_ context.Context, dir string) *checkResult { - entries, err := os.ReadDir(dir) //nolint:forbidigo + _, name, err := findFile(reREADME, dir) if err != nil { - return checkFailed("") + return checkError(err) } - for _, entry := range entries { - if reREADME.Match([]byte(entry.Name())) { - return checkPassed("found `%s` as README file", entry.Name()) - } + if len(name) > 0 { + return checkPassed("found `%s` as README file", name) } return checkFailed("no README file found") diff --git a/checker_util.go b/checker_util.go new file mode 100644 index 0000000..3737582 --- /dev/null +++ b/checker_util.go @@ -0,0 +1,102 @@ +package k6lint + +import ( + "bytes" + "context" + "fmt" + "io" + "log/slog" + "os" + "path/filepath" + "regexp" + "runtime" + + "github.com/grafana/k6foundry" +) + +//nolint:forbidigo +func build(ctx context.Context, module string, dir string) (filename string, result error) { + exe, err := os.CreateTemp("", "k6-*.exe") + if err != nil { + return "", err + } + + if err = os.Chmod(exe.Name(), 0o700); err != nil { //nolint:gosec + return "", err + } + + var out bytes.Buffer + + defer func() { + if result != nil { + _, _ = io.Copy(os.Stderr, &out) + fmt.Fprintln(os.Stderr) + } + }() + + builder, err := k6foundry.NewNativeBuilder( + ctx, + k6foundry.NativeBuilderOpts{ + Logger: slog.New(slog.NewTextHandler(&out, &slog.HandlerOptions{Level: slog.LevelError})), + Stdout: &out, + Stderr: &out, + GoOpts: k6foundry.GoOpts{ + CopyGoEnv: true, + Env: map[string]string{"GOWORK": "off"}, + }, + }, + ) + if err != nil { + result = err + return "", result + } + + _, result = builder.Build( + ctx, + k6foundry.NewPlatform(runtime.GOOS, runtime.GOARCH), + "latest", + []k6foundry.Module{{Path: module, ReplacePath: dir}}, + nil, + exe, + ) + + if result != nil { + return "", result + } + + if err = exe.Close(); err != nil { + return "", err + } + + return exe.Name(), nil +} + +//nolint:forbidigo +func findFile(rex *regexp.Regexp, dirs ...string) (string, string, error) { + for idx, dir := range dirs { + entries, err := os.ReadDir(dir) + if err != nil { + if idx == 0 { + return "", "", err + } + + continue + } + + script := "" + + for _, entry := range entries { + if rex.Match([]byte(entry.Name())) { + script = entry.Name() + + break + } + } + + if len(script) > 0 { + return filepath.Join(dir, script), script, nil + } + } + + return "", "", nil +} diff --git a/checks.go b/checks.go index 93230b5..ce30e82 100644 --- a/checks.go +++ b/checks.go @@ -39,10 +39,12 @@ func checkDefinitions() []checkDefinition { {id: CheckerModule, score: 2, fn: modCheck.hasGoModule}, {id: CheckerReplace, score: 2, fn: modCheck.hasNoReplace}, {id: CheckerReadme, score: 5, fn: checkerReadme}, - {id: CheckerExamples, score: 5, fn: checkerExamples}, + {id: CheckerExamples, score: 2, fn: checkerExamples}, {id: CheckerLicense, score: 5, fn: checkerLicense}, {id: CheckerGit, score: 1, fn: gitCheck.isWorkDir}, {id: CheckerVersions, score: 5, fn: gitCheck.hasVersions}, + {id: CheckerBuild, score: 5, fn: modCheck.canBuild}, + {id: CheckerSmoke, score: 2, fn: modCheck.smoke}, } return defs diff --git a/compliance_gen.go b/compliance_gen.go index f531caf..59376eb 100644 --- a/compliance_gen.go +++ b/compliance_gen.go @@ -24,12 +24,14 @@ type Check struct { type Checker string +const CheckerBuild Checker = "build" const CheckerExamples Checker = "examples" const CheckerGit Checker = "git" const CheckerLicense Checker = "license" const CheckerModule Checker = "module" const CheckerReadme Checker = "readme" const CheckerReplace Checker = "replace" +const CheckerSmoke Checker = "smoke" const CheckerVersions Checker = "versions" // The result of the extension's k6 compliance checks. diff --git a/docs/compliance.schema.json b/docs/compliance.schema.json index d75a263..5b58029 100644 --- a/docs/compliance.schema.json +++ b/docs/compliance.schema.json @@ -81,7 +81,9 @@ "examples", "license", "git", - "versions" + "versions", + "build", + "smoke" ] } } diff --git a/docs/compliance.schema.yaml b/docs/compliance.schema.yaml index 7fa7267..1c63adc 100644 --- a/docs/compliance.schema.yaml +++ b/docs/compliance.schema.yaml @@ -77,3 +77,5 @@ $defs: - license - git - versions + - build + - smoke diff --git a/docs/example.json b/docs/example.json index 33e6069..3cfa35c 100644 --- a/docs/example.json +++ b/docs/example.json @@ -31,12 +31,22 @@ "passed": true }, { - "details": "found `6` versions, the latest is `v0.3.0`", + "details": "found `13` versions, the latest is `v1.0.0`", "id": "versions", "passed": true + }, + { + "details": "can be built with the latest k6 version", + "id": "build", + "passed": true + }, + { + "details": "`smoke.test.ts` successfully run with k6", + "id": "smoke", + "passed": true } ], "grade": "A", "level": 100, - "timestamp": 1724833956 + "timestamp": 1731058317 } diff --git a/docs/example.txt b/docs/example.txt index fbfac37..cfc1003 100644 --- a/docs/example.txt +++ b/docs/example.txt @@ -17,5 +17,9 @@ Details ✔ git found git worktree ✔ versions - found `6` versions, the latest is `v0.3.0` + found `13` versions, the latest is `v1.0.0` +✔ build + can be built with the latest k6 version +✔ smoke + `smoke.test.ts` successfully run with k6 diff --git a/go.mod b/go.mod index 77ec43f..3ec9c58 100644 --- a/go.mod +++ b/go.mod @@ -8,6 +8,7 @@ require ( github.com/go-enry/go-license-detector/v4 v4.3.1 github.com/go-git/go-git/v5 v5.12.0 github.com/grafana/clireadme v0.1.0 + github.com/grafana/k6foundry v0.3.0 github.com/mattn/go-colorable v0.1.13 github.com/spf13/cobra v1.8.1 golang.org/x/mod v0.21.0 diff --git a/go.sum b/go.sum index 71bdfec..056776a 100644 --- a/go.sum +++ b/go.sum @@ -56,6 +56,8 @@ github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/grafana/clireadme v0.1.0 h1:KYEYSnYdSzmHf3bufaK6fQZ5j4dzvM/T+G6Ba+qNnAM= github.com/grafana/clireadme v0.1.0/go.mod h1:Wy4KIG2ZBGMYAYyF9l7qAy+yoJVasqk/txsRgoRI3gc= +github.com/grafana/k6foundry v0.3.0 h1:C+6dPbsOv7Uq4hEhBFNuYqmTdE9jQ0VqhXqBDtMkVTE= +github.com/grafana/k6foundry v0.3.0/go.mod h1:/NtBSQQgXup5SVbfInl0Q8zKVx08xgvXIZ0xncqexEs= github.com/hhatto/gorst v0.0.0-20181029133204-ca9f730cac5b h1:Jdu2tbAxkRouSILp2EbposIb8h4gO+2QuZEn3d9sKAc= github.com/hhatto/gorst v0.0.0-20181029133204-ca9f730cac5b/go.mod h1:HmaZGXHdSwQh1jnUlBGN2BeEYOHACLVGzYOXCbsLvxY= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= diff --git a/releases/v0.2.0.md b/releases/v0.2.0.md new file mode 100644 index 0000000..db701d3 --- /dev/null +++ b/releases/v0.2.0.md @@ -0,0 +1,21 @@ +k6lint `v0.2.0` is here 🎉! + +This version adds two new checkers to the linter: +- **build checker** +- **smoke checker** + +## build checker + +The check is successful if the extension can be built with the latest k6 release. + +## smoke checker + +The check is successful if there is a smoke test script and it runs successfully with the k6 built with the extension. + +Obviously, a prerequisite for a successful run is that the build checker runs successfully, otherwise k6 cannot be built with the extension. + +The smoke test script file is searched for in the root of the repository and in the `test`,`tests`,`examples` directories. The name of the smoke test script is one of the following: + - `smoke.js` + - `smoke.ts` + - `smoke.test.js` + - `smoke.test.ts`