Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

global: make gremlins work on packages #120

Merged
merged 9 commits into from
Aug 10, 2022
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions cmd/gremlins_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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")
}
Expand Down Expand Up @@ -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")
}
Expand Down
16 changes: 9 additions & 7 deletions cmd/unleash.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import (
"context"
"fmt"
"os"
"path/filepath"
"strings"
"sync"

Expand All @@ -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"
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down
10 changes: 6 additions & 4 deletions cmd/unleash_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import (
"context"
"errors"
"fmt"
"path/filepath"
"strings"
"testing"

Expand All @@ -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")
}
Expand Down Expand Up @@ -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)
Expand Down
1 change: 1 addition & 0 deletions configuration/configuration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,7 @@ func TestGeneratesMutantTypeEnabledKey(t *testing.T) {
}

func TestViperSynchronisedAccess(t *testing.T) {
t.Parallel()
testCases := []struct {
value any
name string
Expand Down
1 change: 1 addition & 0 deletions configuration/mutantenables_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import (
)

func TestMutantDefaultStatus(t *testing.T) {
t.Parallel()
testCases := []struct {
mutantType mutant.Type
expected bool
Expand Down
6 changes: 2 additions & 4 deletions docs/docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
91 changes: 91 additions & 0 deletions internal/gomodule/gomodule.go
Original file line number Diff line number Diff line change
@@ -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 ""
}
83 changes: 83 additions & 0 deletions internal/gomodule/gomodule_test.go
Original file line number Diff line number Diff line change
@@ -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")
}
})
}
Loading