From 9fa59c5f54be29ca0e2188b3a1245d16bda51b0a Mon Sep 17 00:00:00 2001 From: Davide Petilli Date: Tue, 9 Aug 2022 11:06:58 +0200 Subject: [PATCH 1/6] global: make gremlins work on packages --- cmd/gremlins_test.go | 6 +- cmd/unleash.go | 16 ++-- cmd/unleash_test.go | 10 ++- configuration/configuration_test.go | 1 + configuration/mutantenables_test.go | 1 + docs/docs/index.md | 6 +- internal/gomodule/gomodule.go | 91 +++++++++++++++++++ internal/gomodule/gomodule_test.go | 83 ++++++++++++++++++ pkg/coverage/coverage.go | 48 +++------- pkg/coverage/coverage_test.go | 82 ++++++------------ pkg/coverage/testdata/valid/coverage | 2 +- pkg/mutator/internal/node_test.go | 2 - pkg/mutator/internal/tokenmutant_test.go | 1 - pkg/mutator/mutator.go | 28 ++++-- pkg/mutator/mutator_test.go | 106 ++++++++++++++--------- pkg/mutator/workdir/workdir_test.go | 5 -- 16 files changed, 318 insertions(+), 170 deletions(-) create mode 100644 internal/gomodule/gomodule.go create mode 100644 internal/gomodule/gomodule_test.go diff --git a/cmd/gremlins_test.go b/cmd/gremlins_test.go index 077b5be7..2759b516 100644 --- a/cmd/gremlins_test.go +++ b/cmd/gremlins_test.go @@ -24,7 +24,7 @@ import ( func TestGremlins(t *testing.T) { const boolType = "bool" - c, err := newRootCmd(context.TODO(), "1.2.3") + c, err := newRootCmd(context.Background(), "1.2.3") if err != nil { t.Fatal("newRootCmd should not fail") } @@ -60,14 +60,14 @@ func TestGremlins(t *testing.T) { func TestExecute(t *testing.T) { t.Run("should not fail", func(t *testing.T) { - err := Execute(context.TODO(), "1.2.3") + err := Execute(context.Background(), "1.2.3") if err != nil { t.Errorf("execute should not fail") } }) t.Run("should fail if version is not set", func(t *testing.T) { - err := Execute(context.TODO(), "") + err := Execute(context.Background(), "") if err == nil { t.Errorf("expected failure") } diff --git a/cmd/unleash.go b/cmd/unleash.go index 32196ab7..ad1a12c6 100644 --- a/cmd/unleash.go +++ b/cmd/unleash.go @@ -20,6 +20,7 @@ import ( "context" "fmt" "os" + "path/filepath" "strings" "sync" @@ -28,6 +29,7 @@ import ( "github.com/go-gremlins/gremlins/cmd/internal/flags" "github.com/go-gremlins/gremlins/configuration" + "github.com/go-gremlins/gremlins/internal/gomodule" "github.com/go-gremlins/gremlins/pkg/coverage" "github.com/go-gremlins/gremlins/pkg/log" "github.com/go-gremlins/gremlins/pkg/mutant" @@ -139,19 +141,20 @@ func cleanUp(wd, rd string) { } func run(ctx context.Context, workDir, currPath string) (report.Results, error) { - c, err := coverage.New(workDir, currPath) + mod, err := gomodule.Init(currPath) if err != nil { - return report.Results{}, fmt.Errorf("failed to gather coverage in %q: %w", currPath, err) + return report.Results{}, fmt.Errorf("%q is not in a Go module: %w", currPath, err) } + c := coverage.New(workDir, mod) p, err := c.Run() if err != nil { return report.Results{}, fmt.Errorf("failed to gather coverage: %w", err) } - d := workdir.NewDealer(workDir, currPath) + d := workdir.NewDealer(workDir, mod.Root) - mut := mutator.New(os.DirFS(currPath), p, d) + mut := mutator.New(mod, p, d) results := mut.Run(ctx) return results, nil @@ -162,16 +165,15 @@ func changePath(args []string, chdir func(dir string) error, getwd func() (strin if err != nil { return "", "", err } - cp := "." + cp, _ := os.Getwd() if len(args) > 0 { - cp = args[0] + cp, _ = filepath.Abs(args[0]) } if cp != "." { err = chdir(cp) if err != nil { return "", "", err } - cp = "." } return cp, rd, nil diff --git a/cmd/unleash_test.go b/cmd/unleash_test.go index fd066304..4e4639bc 100644 --- a/cmd/unleash_test.go +++ b/cmd/unleash_test.go @@ -20,6 +20,7 @@ import ( "context" "errors" "fmt" + "path/filepath" "strings" "testing" @@ -28,7 +29,7 @@ import ( ) func TestUnleash(t *testing.T) { - c, err := newUnleashCmd(context.TODO()) + c, err := newUnleashCmd(context.Background()) if err != nil { t.Fatal("newUnleashCmd should no fail") } @@ -132,11 +133,12 @@ func TestChangePath(t *testing.T) { p, wd, _ := changePath(args, chdir, getwd) - if calledDir != wantCalledDir { + wantAbs, _ := filepath.Abs(wantCalledDir) + if calledDir != wantAbs { t.Errorf("expected %q, got %q", wantCalledDir, calledDir) } - if p != "." { - t.Errorf("expected '.', got %q", p) + if p != wantAbs { + t.Errorf("expected %q, got %q", wantAbs, p) } if wd != "test/dir" { t.Errorf("expected 'test/dir', got %s", wd) diff --git a/configuration/configuration_test.go b/configuration/configuration_test.go index cad1f815..0969b4bc 100644 --- a/configuration/configuration_test.go +++ b/configuration/configuration_test.go @@ -174,6 +174,7 @@ func TestGeneratesMutantTypeEnabledKey(t *testing.T) { } func TestViperSynchronisedAccess(t *testing.T) { + t.Parallel() testCases := []struct { value any name string diff --git a/configuration/mutantenables_test.go b/configuration/mutantenables_test.go index f34576b8..2366dab8 100644 --- a/configuration/mutantenables_test.go +++ b/configuration/mutantenables_test.go @@ -24,6 +24,7 @@ import ( ) func TestMutantDefaultStatus(t *testing.T) { + t.Parallel() testCases := []struct { mutantType mutant.Type expected bool diff --git a/docs/docs/index.md b/docs/docs/index.md index 743f32b0..4a7d6394 100644 --- a/docs/docs/index.md +++ b/docs/docs/index.md @@ -39,10 +39,8 @@ catch their damage? There are some limitations on how Gremlins works right now, but rest assured we'll try to make things better. -- Gremlins can be run only from the root of a Go module and will run all the test suite; this is a problem if the tests - are especially slow. -- For each mutation, Gremlins will run all the test suite; it would be better to only run the test cases that actually - cover the mutation. +- For each mutation, Gremlins will run all the test suite in the package; it would be better to only run the test cases + that actually cover the mutation. - Gremlins doesn't support custom test commands; if you have to do anything different from `go test [-tags t1 t2] ./...` to run your test suite, most probably it will not work with Gremlins. - There is no way to implement custom mutations. diff --git a/internal/gomodule/gomodule.go b/internal/gomodule/gomodule.go new file mode 100644 index 00000000..4952743f --- /dev/null +++ b/internal/gomodule/gomodule.go @@ -0,0 +1,91 @@ +/* + * Copyright 2022 The Gremlins 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 gomodule + +import ( + "bufio" + "bytes" + "fmt" + "os" + "path/filepath" +) + +// GoModule represents the current execution context in Gremlins. +// +// Name is the module name of the Go module being tested by Gremlins. +// Root is the root folder of the Go module. +// PkgDir is the folder in which Gremlins is running. +type GoModule struct { + Name string + Root string + PkgDir string +} + +// Init initializes the current module. It finds the module name and the root +// of the module, then returns a GoModule struct. +func Init(path string) (GoModule, error) { + if path == "" { + return GoModule{}, fmt.Errorf("path is not set") + } + mod, root, err := getMod(path) + if err != nil { + return GoModule{}, err + } + path, _ = filepath.Rel(root, path) + + return GoModule{ + Name: mod, + Root: root, + PkgDir: path, + }, nil +} + +func getMod(path string) (string, string, error) { + root := findModuleRoot(path) + file, err := os.Open(root + "/go.mod") + defer func(file *os.File) { + _ = file.Close() + }(file) + if err != nil { + return "", "", err + } + r := bufio.NewReader(file) + line, _, err := r.ReadLine() + if err != nil { + return "", "", err + } + packageName := bytes.TrimPrefix(line, []byte("module ")) + + return string(packageName), root, nil +} + +func findModuleRoot(path string) string { + // Inspired by how Go itself finds the module root. + path = filepath.Clean(path) + for { + if fi, err := os.Stat(filepath.Join(path, "go.mod")); err == nil && !fi.IsDir() { + return path + } + d := filepath.Dir(path) + if d == path { + break + } + path = d + } + + return "" +} diff --git a/internal/gomodule/gomodule_test.go b/internal/gomodule/gomodule_test.go new file mode 100644 index 00000000..c7d2ddab --- /dev/null +++ b/internal/gomodule/gomodule_test.go @@ -0,0 +1,83 @@ +/* + * Copyright 2022 The Gremlins 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 gomodule_test + +import ( + "os" + "path/filepath" + "testing" + + "github.com/go-gremlins/gremlins/internal/gomodule" +) + +func TestDetectsModule(t *testing.T) { + t.Run("does not return error if it can retrieve module", func(t *testing.T) { + const modName = "example.com" + rootDir := t.TempDir() + pkgDir := "pkgDir" + absPkgDir := filepath.Join(rootDir, pkgDir) + _ = os.MkdirAll(absPkgDir, 0600) + goMod := filepath.Join(rootDir, "go.mod") + err := os.WriteFile(goMod, []byte("module "+modName), 0600) + if err != nil { + t.Fatal(err) + } + + mod, err := gomodule.Init(absPkgDir) + if err != nil { + t.Fatal(err) + } + + if mod.Name != modName { + t.Errorf("expected Go module to be %q, got %q", modName, mod.Name) + } + if mod.Root != rootDir { + t.Errorf("expected Go root to be %q, got %q", rootDir, mod.Root) + } + if mod.PkgDir != pkgDir { + t.Errorf("expected Go package dir to be %q, got %q", pkgDir, mod.PkgDir) + } + }) + + t.Run("returns error if go.mod is invalid", func(t *testing.T) { + path := t.TempDir() + goMod := path + "/go.mod" + err := os.WriteFile(goMod, []byte(""), 0600) + if err != nil { + t.Fatal(err) + } + + _, err = gomodule.Init(path) + if err == nil { + t.Errorf("expected an error") + } + }) + + t.Run("returns error if it cannot find module", func(t *testing.T) { + _, err := gomodule.Init(t.TempDir()) + if err == nil { + t.Errorf("expected an error") + } + }) + + t.Run("returns error if path is empty", func(t *testing.T) { + _, err := gomodule.Init("") + if err == nil { + t.Errorf("expected an error") + } + }) +} diff --git a/pkg/coverage/coverage.go b/pkg/coverage/coverage.go index f4be1987..b268d811 100644 --- a/pkg/coverage/coverage.go +++ b/pkg/coverage/coverage.go @@ -17,18 +17,18 @@ package coverage import ( - "bufio" - "bytes" "fmt" "io" "os" "os/exec" + "path/filepath" "strings" "time" "golang.org/x/tools/cover" "github.com/go-gremlins/gremlins/configuration" + "github.com/go-gremlins/gremlins/internal/gomodule" "github.com/go-gremlins/gremlins/pkg/log" ) @@ -36,7 +36,6 @@ import ( // it took to generate the coverage report. type Result struct { Profile Profile - Module string Elapsed time.Duration } @@ -47,7 +46,7 @@ type Coverage struct { workDir string path string fileName string - mod string + mod gomodule.GoModule buildTags string } @@ -59,42 +58,18 @@ type execContext = func(name string, args ...string) *exec.Cmd // New instantiates a Coverage element using exec.Command as execContext, // actually running the command on the OS. -func New(workdir, path string, opts ...Option) (*Coverage, error) { - mod, err := getMod(path) - if err != nil { - return &Coverage{}, err - } - - return NewWithCmdAndPackage(exec.Command, mod, workdir, path, opts...), nil +func New(workdir string, mod gomodule.GoModule, opts ...Option) *Coverage { + return NewWithCmd(exec.Command, workdir, mod, opts...) } -func getMod(path string) (string, error) { - file, err := os.Open(path + "/go.mod") - defer func(file *os.File) { - _ = file.Close() - }(file) - if err != nil { - return "", err - } - r := bufio.NewReader(file) - line, _, err := r.ReadLine() - if err != nil { - return "", err - } - packageName := bytes.TrimPrefix(line, []byte("module ")) - - return string(packageName), nil -} - -// NewWithCmdAndPackage instantiates a Coverage element given a custom execContext. -func NewWithCmdAndPackage(cmdContext execContext, mod, workdir, path string, opts ...Option) *Coverage { +// NewWithCmd instantiates a Coverage element given a custom execContext. +func NewWithCmd(cmdContext execContext, workdir string, mod gomodule.GoModule, opts ...Option) *Coverage { buildTags := configuration.Get[string](configuration.UnleashTagsKey) - path = strings.TrimSuffix(path, "/") c := &Coverage{ cmdContext: cmdContext, workDir: workdir, - path: path + "/...", + path: "./...", fileName: "coverage", mod: mod, buildTags: buildTags, @@ -126,7 +101,7 @@ func (c *Coverage) Run() (Result, error) { return Result{}, fmt.Errorf("an error occurred while generating coverage profile: %w", err) } - return Result{Module: c.mod, Profile: profile, Elapsed: elapsed}, nil + return Result{Profile: profile, Elapsed: elapsed}, nil } func (c *Coverage) getProfile() (Profile, error) { @@ -200,5 +175,8 @@ func (c *Coverage) parse(data io.Reader) (Profile, error) { } func (c *Coverage) removeModuleFromPath(p *cover.Profile) string { - return strings.ReplaceAll(p.FileName, c.mod+"/", "") + path := strings.ReplaceAll(p.FileName, c.mod.Name+"/", "") + path, _ = filepath.Rel(c.mod.PkgDir, path) + + return path } diff --git a/pkg/coverage/coverage_test.go b/pkg/coverage/coverage_test.go index 64fb5f1a..8292540d 100644 --- a/pkg/coverage/coverage_test.go +++ b/pkg/coverage/coverage_test.go @@ -27,6 +27,7 @@ import ( "github.com/spf13/viper" "github.com/go-gremlins/gremlins/configuration" + "github.com/go-gremlins/gremlins/internal/gomodule" "github.com/go-gremlins/gremlins/pkg/coverage" ) @@ -45,11 +46,11 @@ func TestCoverageRun(t *testing.T) { wantFilename := "coverage" wantFilePath := wantWorkdir + "/" + wantFilename holder := &commandHolder{} - cov := coverage.NewWithCmdAndPackage( - fakeExecCommandSuccess(holder), - "example.com", - wantWorkdir, - ".") + mod := gomodule.GoModule{ + Name: "example.com", + PkgDir: ".", + } + cov := coverage.NewWithCmd(fakeExecCommandSuccess(holder), wantWorkdir, mod) _, _ = cov.Run() @@ -71,15 +72,20 @@ func TestCoverageRun(t *testing.T) { } func TestCoverageRunFails(t *testing.T) { + mod := gomodule.GoModule{ + Name: "example.com", + PkgDir: "./...", + } + t.Run("failure of: go mod download", func(t *testing.T) { - cov := coverage.NewWithCmdAndPackage(fakeExecCommandFailure(0), "example.com", "workdir", "./...") + cov := coverage.NewWithCmd(fakeExecCommandFailure(0), "workdir", mod) if _, err := cov.Run(); err == nil { t.Error("expected run to report an error") } }) t.Run("failure of: go test", func(t *testing.T) { - cov := coverage.NewWithCmdAndPackage(fakeExecCommandFailure(1), "example.com", "workdir", "./...") + cov := coverage.NewWithCmd(fakeExecCommandFailure(1), "workdir", mod) if _, err := cov.Run(); err == nil { t.Error("expected run to report an error") } @@ -87,11 +93,14 @@ func TestCoverageRunFails(t *testing.T) { } func TestCoverageParsesOutput(t *testing.T) { - t.Parallel() module := "example.com" - cov := coverage.NewWithCmdAndPackage(fakeExecCommandSuccess(nil), module, "testdata/valid", "./...") + mod := gomodule.GoModule{ + Name: module, + PkgDir: "path", + } + cov := coverage.NewWithCmd(fakeExecCommandSuccess(nil), "testdata/valid", mod) profile := coverage.Profile{ - "path/file1.go": { + "file1.go": { { StartLine: 47, StartCol: 2, @@ -99,7 +108,7 @@ func TestCoverageParsesOutput(t *testing.T) { EndCol: 16, }, }, - "path2/file2.go": { + "file2.go": { { StartLine: 52, StartCol: 2, @@ -109,7 +118,6 @@ func TestCoverageParsesOutput(t *testing.T) { }, } want := coverage.Result{ - Module: module, Profile: profile, } @@ -121,57 +129,17 @@ func TestCoverageParsesOutput(t *testing.T) { if !cmp.Equal(got.Profile, want.Profile) { t.Error(cmp.Diff(got, want)) } - if !cmp.Equal(got.Module, want.Module) { - t.Error(cmp.Diff(got, want)) - } if got.Elapsed == 0 { t.Errorf("expected elapsed time to be greater than 0") } } -func TestCoverageNew(t *testing.T) { - t.Run("does not return error if it can retrieve module", func(t *testing.T) { - t.Parallel() - path := t.TempDir() - goMod := path + "/go.mod" - err := os.WriteFile(goMod, []byte("module example.com"), 0600) - if err != nil { - t.Fatal(err) - } - - _, err = coverage.New(t.TempDir(), path) - if err != nil { - t.Fatal(err) - } - }) - - t.Run("returns error if go.mod is invalid", func(t *testing.T) { - t.Parallel() - path := t.TempDir() - goMod := path + "/go.mod" - err := os.WriteFile(goMod, []byte(""), 0600) - if err != nil { - t.Fatal(err) - } - - _, err = coverage.New(t.TempDir(), path) - if err == nil { - t.Errorf("expected an error") - } - }) - - t.Run("returns error if it cannot find module", func(t *testing.T) { - t.Parallel() - _, err := coverage.New(t.TempDir(), t.TempDir()) - if err == nil { - t.Errorf("expected an error") - } - }) -} - func TestParseOutputFail(t *testing.T) { - t.Parallel() - cov := coverage.NewWithCmdAndPackage(fakeExecCommandSuccess(nil), "example.com", "testdata/invalid", "./...") + mod := gomodule.GoModule{ + Name: "example.com", + PkgDir: "./...", + } + cov := coverage.NewWithCmd(fakeExecCommandSuccess(nil), "testdata/invalid", mod) if _, err := cov.Run(); err == nil { t.Errorf("espected an error") diff --git a/pkg/coverage/testdata/valid/coverage b/pkg/coverage/testdata/valid/coverage index fcd0947d..ff82da2a 100644 --- a/pkg/coverage/testdata/valid/coverage +++ b/pkg/coverage/testdata/valid/coverage @@ -1,4 +1,4 @@ mode: set example.com/path/file1.go:47.2,48.16 2 1 example.com/path/file1.go:48.4,49.20 2 0 -example.com/path2/file2.go:52.2,53.16 2 1 \ No newline at end of file +example.com/path/file2.go:52.2,53.16 2 1 \ No newline at end of file diff --git a/pkg/mutator/internal/node_test.go b/pkg/mutator/internal/node_test.go index e8f619a5..70ebe561 100644 --- a/pkg/mutator/internal/node_test.go +++ b/pkg/mutator/internal/node_test.go @@ -78,8 +78,6 @@ func TestNewTokenNode(t *testing.T) { for _, tc := range testCases { tc := tc t.Run(tc.name, func(t *testing.T) { - t.Parallel() - tn, ok := internal.NewTokenNode(tc.node) if ok != tc.supported { t.Fatal("expected to receive a token") diff --git a/pkg/mutator/internal/tokenmutant_test.go b/pkg/mutator/internal/tokenmutant_test.go index c8764c62..9e4f4345 100644 --- a/pkg/mutator/internal/tokenmutant_test.go +++ b/pkg/mutator/internal/tokenmutant_test.go @@ -30,7 +30,6 @@ import ( ) func TestMutantApplyAndRollback(t *testing.T) { - t.Parallel() want := []string{ "package main\n\nfunc main() {\n\ta := 1 - 2\n\tb := 1 - 2\n}\n", "package main\n\nfunc main() {\n\ta := 1 + 2\n\tb := 1 + 2\n}\n", diff --git a/pkg/mutator/mutator.go b/pkg/mutator/mutator.go index 053a8f21..dc823c1a 100644 --- a/pkg/mutator/mutator.go +++ b/pkg/mutator/mutator.go @@ -30,6 +30,7 @@ import ( "time" "github.com/go-gremlins/gremlins/configuration" + "github.com/go-gremlins/gremlins/internal/gomodule" "github.com/go-gremlins/gremlins/pkg/coverage" "github.com/go-gremlins/gremlins/pkg/log" "github.com/go-gremlins/gremlins/pkg/mutant" @@ -43,6 +44,7 @@ import ( // It traverses the AST of the project, finds which TokenMutant can be applied and // performs the actual mutation testing. type Mutator struct { + module gomodule.GoModule fs fs.FS wdManager workdir.Dealer covProfile coverage.Profile @@ -51,7 +53,6 @@ type Mutator struct { rollback func(m mutant.Mutant) error mutantStream chan mutant.Mutant buildTags string - module string testExecutionTime time.Duration dryRun bool } @@ -74,16 +75,17 @@ type Option func(m Mutator) Mutator // The apply and rollback functions are wrappers around the TokenMutant apply and // rollback. These can be overridden with nop functions in tests. Not an // ideal setup. In the future we can think of a better way to handle this. -func New(fs fs.FS, r coverage.Result, manager workdir.Dealer, opts ...Option) Mutator { +func New(mod gomodule.GoModule, r coverage.Result, manager workdir.Dealer, opts ...Option) Mutator { + dirFS := os.DirFS(filepath.Join(mod.Root, mod.PkgDir)) buildTags := configuration.Get[string](configuration.UnleashTagsKey) dryRun := configuration.Get[bool](configuration.UnleashDryRunKey) mut := Mutator{ - module: r.Module, + module: mod, wdManager: manager, covProfile: r.Profile, testExecutionTime: r.Elapsed * timeoutCoefficient, - fs: fs, + fs: dirFS, execContext: exec.CommandContext, apply: func(m mutant.Mutant) error { return m.Apply() @@ -121,6 +123,15 @@ func WithApplyAndRollback(a, r func(m mutant.Mutant) error) Option { } } +// WithDirFs overrides the fs.FS of the module (mainly used for testing purposes). +func WithDirFs(dirFS fs.FS) Option { + return func(m Mutator) Mutator { + m.fs = dirFS + + return m + } +} + // Run executes the mutation testing. // // It walks the fs.FS provided and checks every .go file which is not a test. @@ -146,7 +157,7 @@ func (mu *Mutator) Run(ctx context.Context) report.Results { start := time.Now() res := mu.executeTests(ctx) res.Elapsed = time.Since(start) - res.Module = mu.module + res.Module = mu.module.Name return res } @@ -203,7 +214,7 @@ func (mu *Mutator) executeTests(ctx context.Context) report.Results { log.Infoln("Executing mutation testing on covered mutants...") } currDir, _ := os.Getwd() - wDir, cl, err := mu.wdManager.Get() + rootWd, cl, err := mu.wdManager.Get() if err != nil { panic("error, this is temporary") } @@ -212,14 +223,15 @@ func (mu *Mutator) executeTests(ctx context.Context) report.Results { cl() }(currDir) - _ = os.Chdir(wDir) + workingDir := filepath.Join(rootWd, mu.module.PkgDir) + _ = os.Chdir(workingDir) for mut := range mu.mutantStream { ok := checkDone(ctx) if !ok { return results(mutants) } - mut.SetWorkdir(wDir) + mut.SetWorkdir(workingDir) if mut.Status() == mutant.NotCovered || mu.dryRun { mutants = append(mutants, mut) report.Mutant(mut) diff --git a/pkg/mutator/mutator_test.go b/pkg/mutator/mutator_test.go index 2cb3e947..94813f41 100644 --- a/pkg/mutator/mutator_test.go +++ b/pkg/mutator/mutator_test.go @@ -24,6 +24,7 @@ import ( "io" "os" "os/exec" + "path/filepath" "strings" "sync" "testing" @@ -33,11 +34,14 @@ import ( "github.com/google/go-cmp/cmp" "github.com/go-gremlins/gremlins/configuration" + "github.com/go-gremlins/gremlins/internal/gomodule" "github.com/go-gremlins/gremlins/pkg/coverage" "github.com/go-gremlins/gremlins/pkg/mutant" "github.com/go-gremlins/gremlins/pkg/mutator" ) +var viperMutex sync.RWMutex + func init() { viperMutex.Lock() viperReset() @@ -45,8 +49,6 @@ func init() { const defaultFixture = "testdata/fixtures/gtr_go" -var viperMutex = sync.Mutex{} - func viperSet(set map[string]any) { viperMutex.Lock() for k, v := range set { @@ -69,23 +71,27 @@ func coveredPosition(fixture string) coverage.Result { fn := filenameFromFixture(fixture) p := coverage.Profile{fn: {{StartLine: 6, EndLine: 7, StartCol: 8, EndCol: 9}}} - return coverage.Result{Module: expectedModule, Profile: p, Elapsed: expectedTimeout} + return coverage.Result{Profile: p, Elapsed: expectedTimeout} } func notCoveredPosition(fixture string) coverage.Result { fn := filenameFromFixture(fixture) p := coverage.Profile{fn: {{StartLine: 9, EndLine: 9, StartCol: 8, EndCol: 9}}} - return coverage.Result{Module: expectedModule, Profile: p, Elapsed: expectedTimeout} + return coverage.Result{Profile: p, Elapsed: expectedTimeout} } -type dealerStub struct{} +type dealerStub struct { + t *testing.T +} -func (dealerStub) Get() (string, func(), error) { - return ".", func() {}, nil +func (d dealerStub) Get() (string, func(), error) { + return d.t.TempDir(), func() {}, nil } func TestMutations(t *testing.T) { + t.Parallel() + testCases := []struct { name string fixture string @@ -261,15 +267,16 @@ func TestMutations(t *testing.T) { }, } for _, tc := range testCases { - tCase := tc - t.Run(tCase.name, func(t *testing.T) { + tc := tc + t.Run(tc.name, func(t *testing.T) { t.Parallel() - mapFS, c := loadFixture(tCase.fixture) - defer c() - viperSet(map[string]any{configuration.UnleashDryRunKey: true}) defer viperReset() - mut := mutator.New(mapFS, tCase.covResult, dealerStub{}) + + mapFS, mod, c := loadFixture(tc.fixture) + defer c() + + mut := mutator.New(mod, tc.covResult, dealerStub{t: t}, mutator.WithDirFs(mapFS)) res := mut.Run(context.Background()) got := res.Mutants @@ -277,7 +284,7 @@ func TestMutations(t *testing.T) { t.Errorf("expected module to be %q, got %q", expectedModule, res.Module) } - if tCase.token == token.ILLEGAL { + if tc.token == token.ILLEGAL { if len(got) != 0 { t.Errorf("expected no mutator found") } @@ -286,7 +293,7 @@ func TestMutations(t *testing.T) { } for _, g := range got { - if g.Type() == tCase.mutantType && g.Status() == tCase.mutStatus && g.Pos() > 0 { + if g.Type() == tc.mutantType && g.Status() == tc.mutStatus && g.Pos() > 0 { // PASS return } @@ -299,10 +306,11 @@ func TestMutations(t *testing.T) { } func TestMutantSkipDisabled(t *testing.T) { + t.Parallel() for _, mt := range mutant.MutantTypes { t.Run(mt.String(), func(t *testing.T) { t.Parallel() - mapFS, c := loadFixture(defaultFixture) + mapFS, mod, c := loadFixture(defaultFixture) defer c() viperSet(map[string]any{ @@ -311,8 +319,8 @@ func TestMutantSkipDisabled(t *testing.T) { ) defer viperReset() - mut := mutator.New(mapFS, coveredPosition(defaultFixture), dealerStub{}, - mutator.WithExecContext(fakeExecCommandSuccess)) + mut := mutator.New(mod, coveredPosition(defaultFixture), dealerStub{t: t}, + mutator.WithExecContext(fakeExecCommandSuccess), mutator.WithDirFs(mapFS)) res := mut.Run(context.Background()) got := res.Mutants @@ -334,9 +342,14 @@ func TestSkipTestAndNonGoFiles(t *testing.T) { "file_test.go": {Data: file}, "folder1/file": {Data: file}, } + mod := gomodule.GoModule{ + Name: "example.com", + Root: ".", + PkgDir: ".", + } viperSet(map[string]any{configuration.UnleashDryRunKey: true}) defer viperReset() - mut := mutator.New(sys, coverage.Result{}, dealerStub{}) + mut := mutator.New(mod, coverage.Result{}, dealerStub{t: t}, mutator.WithDirFs(sys)) res := mut.Run(context.Background()) if got := res.Mutants; len(got) != 0 { @@ -354,13 +367,14 @@ type execContext = func(ctx context.Context, name string, args ...string) *exec. func TestMutatorRun(t *testing.T) { t.Parallel() - mapFS, c := loadFixture(defaultFixture) + mapFS, mod, c := loadFixture(defaultFixture) defer c() viperSet(map[string]any{configuration.UnleashTagsKey: "tag1 tag2"}) defer viperReset() holder := &commandHolder{} - mut := mutator.New(mapFS, coveredPosition(defaultFixture), dealerStub{}, + mut := mutator.New(mod, coveredPosition(defaultFixture), dealerStub{t: t}, + mutator.WithDirFs(mapFS), mutator.WithExecContext(fakeExecCommandSuccessWithHolder(holder)), mutator.WithApplyAndRollback( func(m mutant.Mutant) error { @@ -432,14 +446,16 @@ func TestMutatorTestExecution(t *testing.T) { }, } for _, tc := range testCases { - tCase := tc - t.Run(tCase.name, func(t *testing.T) { - t.Parallel() - mapFS, c := loadFixture(tCase.fixture) + tc := tc + t.Run(tc.name, func(t *testing.T) { + viperSet(map[string]any{configuration.UnleashDryRunKey: false}) + defer viperReset() + mapFS, mod, c := loadFixture(tc.fixture) defer c() - mut := mutator.New(mapFS, tCase.covResult, dealerStub{}, - mutator.WithExecContext(tCase.testResult), + mut := mutator.New(mod, tc.covResult, dealerStub{t: t}, + mutator.WithDirFs(mapFS), + mutator.WithExecContext(tc.testResult), mutator.WithApplyAndRollback( func(m mutant.Mutant) error { return nil @@ -453,10 +469,10 @@ func TestMutatorTestExecution(t *testing.T) { if len(got) < 1 { t.Fatal("no mutants received") } - if got[0].Status() != tCase.wantMutStatus { - t.Errorf("expected mutation to be %v, but got: %v", tCase.wantMutStatus, got[0].Status()) + if got[0].Status() != tc.wantMutStatus { + t.Errorf("expected mutation to be %v, but got: %v", tc.wantMutStatus, got[0].Status()) } - if tCase.wantMutStatus != mutant.NotCovered && res.Elapsed <= 0 { + if tc.wantMutStatus != mutant.NotCovered && res.Elapsed <= 0 { t.Errorf("expected elapsed time to be greater than zero, got %s", res.Elapsed) } }) @@ -465,11 +481,11 @@ func TestMutatorTestExecution(t *testing.T) { func TestApplyAndRollbackError(t *testing.T) { t.Run("apply fails", func(t *testing.T) { - t.Parallel() - mapFS, c := loadFixture(defaultFixture) + mapFS, mod, c := loadFixture(defaultFixture) defer c() - mut := mutator.New(mapFS, coveredPosition(defaultFixture), dealerStub{}, + mut := mutator.New(mod, coveredPosition(defaultFixture), dealerStub{t: t}, + mutator.WithDirFs(mapFS), mutator.WithExecContext(fakeExecCommandSuccess), mutator.WithApplyAndRollback( func(m mutant.Mutant) error { @@ -487,11 +503,11 @@ func TestApplyAndRollbackError(t *testing.T) { }) t.Run("rollback fails", func(t *testing.T) { - t.Parallel() - mapFS, c := loadFixture(defaultFixture) + mapFS, mod, c := loadFixture(defaultFixture) defer c() - mut := mutator.New(mapFS, coveredPosition(defaultFixture), dealerStub{}, + mut := mutator.New(mod, coveredPosition(defaultFixture), dealerStub{t: t}, + mutator.WithDirFs(mapFS), mutator.WithExecContext(fakeExecCommandSuccess), mutator.WithApplyAndRollback( func(m mutant.Mutant) error { @@ -510,11 +526,11 @@ func TestApplyAndRollbackError(t *testing.T) { } func TestStopsOnCancel(t *testing.T) { - t.Parallel() - mapFS, c := loadFixture(defaultFixture) + mapFS, mod, c := loadFixture(defaultFixture) defer c() - mut := mutator.New(mapFS, coveredPosition(defaultFixture), dealerStub{}, + mut := mutator.New(mod, coveredPosition(defaultFixture), dealerStub{t: t}, + mutator.WithDirFs(mapFS), mutator.WithExecContext(fakeExecCommandSuccess), mutator.WithApplyAndRollback( func(m mutant.Mutant) error { @@ -533,7 +549,7 @@ func TestStopsOnCancel(t *testing.T) { } } -func loadFixture(fixture string) (fstest.MapFS, func()) { +func loadFixture(fixture string) (fstest.MapFS, gomodule.GoModule, func()) { f, _ := os.Open(fixture) src, _ := io.ReadAll(f) filename := filenameFromFixture(fixture) @@ -541,9 +557,13 @@ func loadFixture(fixture string) (fstest.MapFS, func()) { filename: {Data: src}, } - return mapFS, func() { - _ = f.Close() - } + return mapFS, gomodule.GoModule{ + Name: "example.com", + Root: filepath.Dir(filename), + PkgDir: filepath.Dir(filename), + }, func() { + _ = f.Close() + } } func TestCoverageProcessSuccess(_ *testing.T) { diff --git a/pkg/mutator/workdir/workdir_test.go b/pkg/mutator/workdir/workdir_test.go index 7bb92e64..379d6171 100644 --- a/pkg/mutator/workdir/workdir_test.go +++ b/pkg/mutator/workdir/workdir_test.go @@ -31,7 +31,6 @@ import ( ) func TestLinkFolder(t *testing.T) { - t.Parallel() srcDir := t.TempDir() populateSrcDir(t, srcDir, 3) dstDir := t.TempDir() @@ -82,7 +81,6 @@ func TestLinkFolder(t *testing.T) { } func TestCopyFolder(t *testing.T) { - t.Parallel() srcDir := t.TempDir() populateSrcDir(t, srcDir, 3) dstDir := t.TempDir() @@ -140,7 +138,6 @@ func TestCopyFolder(t *testing.T) { func TestCDealerErrors(t *testing.T) { t.Run("dstDir is not a path", func(t *testing.T) { - t.Parallel() srcDir := "not a dir" dstDir := t.TempDir() @@ -153,7 +150,6 @@ func TestCDealerErrors(t *testing.T) { }) t.Run("srcDir is not readable", func(t *testing.T) { - t.Parallel() srcDir := t.TempDir() err := os.Chmod(srcDir, 0000) clean := os.Chmod @@ -179,7 +175,6 @@ func TestCDealerErrors(t *testing.T) { }) t.Run("dstDir is not writeable", func(t *testing.T) { - t.Parallel() srcDir := t.TempDir() dstDir := t.TempDir() err := os.Chmod(dstDir, 0000) From ad841d136fe9e7071bd37876f933fc192a0b5151 Mon Sep 17 00:00:00 2001 From: Davide Petilli Date: Tue, 9 Aug 2022 14:47:02 +0200 Subject: [PATCH 2/6] config: load config from module root Also fix the configuration loading order. --- configuration/configuration.go | 31 ++++++++++++++++- configuration/configuration_test.go | 48 ++++++++++++++++++++++++--- configuration/testdata/config1/go.mod | 1 + docs/docs/usage/configuration.md | 7 ++-- 4 files changed, 78 insertions(+), 9 deletions(-) create mode 100644 configuration/testdata/config1/go.mod diff --git a/configuration/configuration.go b/configuration/configuration.go index d46ba118..0269d4cc 100644 --- a/configuration/configuration.go +++ b/configuration/configuration.go @@ -110,11 +110,12 @@ func arePathsNotSet(cPaths []string) bool { func defaultConfigPaths() []string { result := make([]string, 0, 4) - result = append(result, ".") + // First global config if runtime.GOOS != windowsOs { result = append(result, "/etc/gremlins") } + // Then $XDG_CONFIG_HOME xchLocation, _ := homedir.Expand("~/.config") if x := os.Getenv(xdgConfigHomeKey); x != "" { xchLocation = x @@ -122,15 +123,43 @@ func defaultConfigPaths() []string { xchLocation = filepath.Join(xchLocation, "gremlins", "gremlins") result = append(result, xchLocation) + // Then $HOME homeLocation, err := homedir.Expand("~/.gremlins") if err != nil { return result } result = append(result, homeLocation) + // Then the Go module root + if root := findModuleRoot(); root != "" { + result = append(result, root) + } + + // Finally the current folder + result = append(result, ".") + return result } +func findModuleRoot() string { + // This function is duplicated from internal/gomodule. We should find a way + // to use here gomodule. The problem is the point of initialization, because + // configuration is initialised before gomodule. + path, _ := os.Getwd() + for { + if fi, err := os.Stat(filepath.Join(path, "go.mod")); err == nil && !fi.IsDir() { + return path + } + d := filepath.Dir(path) + if d == path { + break + } + path = d + } + + return "" +} + var mutex sync.RWMutex // Set offers synchronised access to Viper. diff --git a/configuration/configuration_test.go b/configuration/configuration_test.go index 0969b4bc..63de46cd 100644 --- a/configuration/configuration_test.go +++ b/configuration/configuration_test.go @@ -17,6 +17,7 @@ package configuration import ( + "os" "path/filepath" "runtime" "testing" @@ -34,7 +35,6 @@ type envEntry struct { } func TestConfiguration(t *testing.T) { - testCases := []struct { wantedConfig map[string]interface{} name string @@ -130,12 +130,31 @@ func TestConfigPaths(t *testing.T) { home, _ := homedir.Dir() t.Run("it lookups in default locations", func(t *testing.T) { + oldDir, _ := os.Getwd() + _ = os.Chdir("testdata/config1") + defer func(dir string) { + _ = os.Chdir(dir) + }(oldDir) + var want []string - want = append(want, ".") + + // First global if runtime.GOOS != "windows" { want = append(want, "/etc/gremlins") } - want = append(want, filepath.Join(home, ".config", "gremlins", "gremlins"), filepath.Join(home, ".gremlins")) + + // Then $XDG_CONFIG_HOME and $HOME + want = append(want, + filepath.Join(home, ".config", "gremlins", "gremlins"), + filepath.Join(home, ".gremlins"), + ) + + // Then module root + moduleRoot, _ := os.Getwd() + want = append(want, moduleRoot) + + // Last current folder + want = append(want, ".") got := defaultConfigPaths() @@ -145,14 +164,33 @@ func TestConfigPaths(t *testing.T) { }) t.Run("when XDG_CONFIG_HOME is set, it lookups in that locations", func(t *testing.T) { + oldDir, _ := os.Getwd() + _ = os.Chdir("testdata/config1") + defer func(dir string) { + _ = os.Chdir(dir) + }(oldDir) + customPath := filepath.Join("my", "custom", "path") t.Setenv("XDG_CONFIG_HOME", customPath) + var want []string - want = append(want, ".") + + // First global if runtime.GOOS != "windows" { want = append(want, "/etc/gremlins") } - want = append(want, filepath.Join(customPath, "gremlins", "gremlins"), filepath.Join(home, ".gremlins")) + + // Then $XDG_CONFIG_HOME and $HOME + want = append(want, + filepath.Join(customPath, "gremlins", "gremlins"), + filepath.Join(home, ".gremlins")) + + // Then Go module root + moduleRoot, _ := os.Getwd() + want = append(want, moduleRoot) + + // Last the current directory + want = append(want, ".") got := defaultConfigPaths() diff --git a/configuration/testdata/config1/go.mod b/configuration/testdata/config1/go.mod new file mode 100644 index 00000000..5b780143 --- /dev/null +++ b/configuration/testdata/config1/go.mod @@ -0,0 +1 @@ +module example.com \ No newline at end of file diff --git a/docs/docs/usage/configuration.md b/docs/docs/usage/configuration.md index cdc1cc9b..e0713b26 100644 --- a/docs/docs/usage/configuration.md +++ b/docs/docs/usage/configuration.md @@ -20,9 +20,10 @@ Gremlins can be configured with a configuration file. The configuration file can be placed in (in order of precedence) 1. `./.gremlins.yaml` (the current directory) -2. `/etc/gremlins/gremlins.yaml` -3. `$XDG_CONFIG_HOME/gremlins/gremlins.yaml` -4. `$HOME/.gremlins.yaml` +2. The module root +3. `/etc/gremlins/.gremlins.yaml` +4. `$XDG_CONFIG_HOME/gremlins/.gremlins.yaml` +5. `$HOME/.gremlins.yaml` !!! hint `XDG_CONFIG_HOME` is usually `~/.config`. From be2926e16bef98ac027b16f1c87ae6cb91660023 Mon Sep 17 00:00:00 2001 From: Davide Petilli Date: Tue, 9 Aug 2022 15:30:26 +0200 Subject: [PATCH 3/6] set new gremlins version --- .github/workflows/gremlins.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/gremlins.yml b/.github/workflows/gremlins.yml index c625f240..daf0bd9f 100644 --- a/.github/workflows/gremlins.yml +++ b/.github/workflows/gremlins.yml @@ -19,6 +19,6 @@ jobs: cache-dependency-path: go.sum # While we wait for a Gremlins Action to be made, let's install Gremlins directly. - name: Install Gremlins - run: go install github.com/go-gremlins/gremlins/cmd/gremlins@v0.2.0 + run: go install github.com/go-gremlins/gremlins/cmd/gremlins@v0.2.1 - name: Run Gremlins run: gremlins unleash --silent From 34b89467faf4f76f23968079b1b5861b4db82232 Mon Sep 17 00:00:00 2001 From: Davide Petilli Date: Tue, 9 Aug 2022 16:19:47 +0200 Subject: [PATCH 4/6] possibly fix windows random failure --- configuration/configuration_test.go | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/configuration/configuration_test.go b/configuration/configuration_test.go index 63de46cd..2b4fcc3f 100644 --- a/configuration/configuration_test.go +++ b/configuration/configuration_test.go @@ -220,27 +220,27 @@ func TestViperSynchronisedAccess(t *testing.T) { }{ { name: "bool", - key: "bool.key", + key: "tvsa.bool.key", value: true, }, { name: "int", - key: "int.key", + key: "tvsa.int.key", value: 10, }, { name: "float64", - key: "float64.key", + key: "tvsa.float64.key", value: float64(10), }, { name: "string", - key: "string.key", + key: "tvsa.string.key", value: "test string", }, { name: "char", - key: "char.key", + key: "tvsa.char.key", value: 'a', }, } @@ -248,7 +248,6 @@ func TestViperSynchronisedAccess(t *testing.T) { tc := tc t.Run(tc.name, func(t *testing.T) { t.Parallel() - defer Reset() Set(tc.key, tc.value) @@ -257,7 +256,6 @@ func TestViperSynchronisedAccess(t *testing.T) { if !cmp.Equal(got, tc.value) { t.Errorf("expected %v, got %v", tc.value, got) } - }) } } From cb29618ee9299de11cd1a32fabf46233a42ee97b Mon Sep 17 00:00:00 2001 From: Davide Petilli Date: Tue, 9 Aug 2022 16:26:07 +0200 Subject: [PATCH 5/6] possibly fix random failure --- pkg/mutator/mutator_test.go | 1 - 1 file changed, 1 deletion(-) diff --git a/pkg/mutator/mutator_test.go b/pkg/mutator/mutator_test.go index 94813f41..32361ec0 100644 --- a/pkg/mutator/mutator_test.go +++ b/pkg/mutator/mutator_test.go @@ -366,7 +366,6 @@ type commandHolder struct { type execContext = func(ctx context.Context, name string, args ...string) *exec.Cmd func TestMutatorRun(t *testing.T) { - t.Parallel() mapFS, mod, c := loadFixture(defaultFixture) defer c() From 4e27c9888da9fb8ca6478db22862454bef2ee439 Mon Sep 17 00:00:00 2001 From: Davide Petilli Date: Tue, 9 Aug 2022 18:32:17 +0200 Subject: [PATCH 6/6] expand tests --- configuration/configuration_test.go | 46 +++++++++++++++++++++++++++-- 1 file changed, 44 insertions(+), 2 deletions(-) diff --git a/configuration/configuration_test.go b/configuration/configuration_test.go index 2b4fcc3f..0587121f 100644 --- a/configuration/configuration_test.go +++ b/configuration/configuration_test.go @@ -139,7 +139,7 @@ func TestConfigPaths(t *testing.T) { var want []string // First global - if runtime.GOOS != "windows" { + if runtime.GOOS != windowsOs { want = append(want, "/etc/gremlins") } @@ -163,6 +163,36 @@ func TestConfigPaths(t *testing.T) { } }) + t.Run("no module root if not in go module", func(t *testing.T) { + oldDir, _ := os.Getwd() + _ = os.Chdir(t.TempDir()) + defer func(dir string) { + _ = os.Chdir(dir) + }(oldDir) + + var want []string + + // First global + if runtime.GOOS != windowsOs { + want = append(want, "/etc/gremlins") + } + + // Then $XDG_CONFIG_HOME and $HOME + want = append(want, + filepath.Join(home, ".config", "gremlins", "gremlins"), + filepath.Join(home, ".gremlins"), + ) + + // Last current folder + want = append(want, ".") + + got := defaultConfigPaths() + + if !cmp.Equal(got, want) { + t.Errorf(cmp.Diff(got, want)) + } + }) + t.Run("when XDG_CONFIG_HOME is set, it lookups in that locations", func(t *testing.T) { oldDir, _ := os.Getwd() _ = os.Chdir("testdata/config1") @@ -176,7 +206,7 @@ func TestConfigPaths(t *testing.T) { var want []string // First global - if runtime.GOOS != "windows" { + if runtime.GOOS != windowsOs { want = append(want, "/etc/gremlins") } @@ -259,3 +289,15 @@ func TestViperSynchronisedAccess(t *testing.T) { }) } } + +func TestReset(t *testing.T) { + Set("test.key", true) + + Reset() + + got := Get[bool]("test.key") + + if got != false { + t.Errorf("expected config to be reset") + } +}