Skip to content

Commit

Permalink
global: make gremlins work on packages
Browse files Browse the repository at this point in the history
  • Loading branch information
k3rn31 committed Aug 9, 2022
1 parent c7e1938 commit 9fa59c5
Show file tree
Hide file tree
Showing 16 changed files with 318 additions and 170 deletions.
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

0 comments on commit 9fa59c5

Please sign in to comment.