Skip to content

Commit

Permalink
feat: add only diff run flag (#215)
Browse files Browse the repository at this point in the history
* feat: add only diff run flag

* fix: lint issues

* docs: add diff flag

* docs: fix github action path

* fix: engine.New too many params

Codeclimate issue

---------

Co-authored-by: Nikita Rusin <[email protected]>
Co-authored-by: Davide Petilli <[email protected]>
  • Loading branch information
3 people authored Dec 19, 2023
1 parent bd792f3 commit 84488fa
Show file tree
Hide file tree
Showing 19 changed files with 508 additions and 30 deletions.
2 changes: 2 additions & 0 deletions cmd/internal/flags/flags_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,7 @@ func TestSet(t *testing.T) {

cmd := &cobra.Command{}

// #nosec G601 - We are in tests, we don't care
err := Set(cmd, &tc.flag)
if (tc.expectError && err == nil) || (!tc.expectError && err != nil) {
t.Fatal("error not expected")
Expand All @@ -132,6 +133,7 @@ func TestSet(t *testing.T) {
}

tc.flag.Name += "_persistent"
// #nosec G601 - We are in tests, we don't care
err = SetPersistent(cmd, &tc.flag)
if (tc.expectError && err == nil) || (!tc.expectError && err != nil) {
t.Fatal("error not expected")
Expand Down
15 changes: 14 additions & 1 deletion cmd/unleash.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import (
"github.com/spf13/pflag"

"github.com/go-gremlins/gremlins/internal/coverage"
"github.com/go-gremlins/gremlins/internal/diff"
"github.com/go-gremlins/gremlins/internal/engine"
"github.com/go-gremlins/gremlins/internal/engine/workdir"
"github.com/go-gremlins/gremlins/internal/log"
Expand All @@ -46,6 +47,7 @@ type unleashCmd struct {
const (
commandName = "unleash"

paramDiff = "diff"
paramBuildTags = "tags"
paramCoverPackages = "coverpkg"
paramDryRun = "dry-run"
Expand Down Expand Up @@ -154,6 +156,11 @@ func cleanUp(wd string) {
}

func run(ctx context.Context, mod gomodule.GoModule, workDir string) (report.Results, error) {
fDiff, err := diff.New()
if err != nil {
return report.Results{}, err
}

c := coverage.New(workDir, mod)

cProfile, err := c.Run()
Expand All @@ -166,7 +173,12 @@ func run(ctx context.Context, mod gomodule.GoModule, workDir string) (report.Res

jDealer := engine.NewExecutorDealer(mod, wdDealer, cProfile.Elapsed)

mut := engine.New(mod, cProfile, jDealer)
codeData := engine.CodeData{
Cov: cProfile.Profile,
Diff: fDiff,
}

mut := engine.New(mod, codeData, jDealer)
results := mut.Run(ctx)

return results, nil
Expand All @@ -188,6 +200,7 @@ func setFlagsOnCmd(cmd *cobra.Command) error {
{Name: paramDryRun, CfgKey: configuration.UnleashDryRunKey, Shorthand: "d", DefaultV: false, Usage: "find mutations but do not executes tests"},
{Name: paramBuildTags, CfgKey: configuration.UnleashTagsKey, Shorthand: "t", DefaultV: "", Usage: "a comma-separated list of build tags"},
{Name: paramCoverPackages, CfgKey: configuration.UnleashCoverPkgKey, DefaultV: "", Usage: "a comma-separated list of package patterns"},
{Name: paramDiff, CfgKey: configuration.UnleashDiffRef, Shorthand: "D", DefaultV: "", Usage: "diff branch or commit"},
{Name: paramOutput, CfgKey: configuration.UnleashOutputKey, Shorthand: "o", DefaultV: "", Usage: "set the output file for machine readable results"},
{Name: paramIntegrationMode, CfgKey: configuration.UnleashIntegrationMode, Shorthand: "i", DefaultV: false, Usage: "makes Gremlins run the complete test suite for each mutation"},
{Name: paramThresholdEfficacy, CfgKey: configuration.UnleashThresholdEfficacyKey, DefaultV: float64(0), Usage: "threshold for code-efficacy percent"},
Expand Down
6 changes: 6 additions & 0 deletions cmd/unleash_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,12 @@ func TestUnleash(t *testing.T) {
flagType: "string",
defValue: "",
},
{
name: "diff",
shorthand: "D",
flagType: "string",
defValue: "",
},
{
name: "dry-run",
shorthand: "d",
Expand Down
1 change: 1 addition & 0 deletions docs/docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ catch their damage?

- Discovers mutant candidates and tests them
- Only tests mutants covered by tests
- Can test mutants only in PR changes
- Supports five mutant types
- Yaml-based configuration
- Can run as quality gate on CI
2 changes: 1 addition & 1 deletion docs/docs/usage/ci/github-action.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ jobs:
gremlins:
- uses: actions/checkout@v3
- uses: actions/setup-go@v3
- uses: actions/gremlins-action@v1
- uses: go-gremlins/gremlins-action@v1
with:
version: latest
args: --tags="tag1,tag2"
Expand Down
29 changes: 29 additions & 0 deletions docs/docs/usage/commands/unleash/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,35 @@ The default is for each test to analyze only the package being tested.
gremlins unleash --coverpkg "./internal/...,./pkg/..."
```

### Diff

:material-flag: `--diff` · :material-sign-direction: Default: empty

Run tests only for mutants inside code changes between current state and git reference (branch or commit).
The default is each mutant covered by tests.

#### Branch merge base

```shell
gremlins unleash --diff "origin/main"
```

#### Commit

```shell
gremlins unleash --diff "b62af323"
```

#### PR

```shell
gremlins unleash --diff "origin/$GITHUB_BASE_REF"
```

Use `actions/checkout@v4` with `fetch-depth: 0` to fetch all history.

#### Using

### Dry run

:material-flag:`--dry-run`/`-d` · :material-sign-direction: Default: false
Expand Down
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ go 1.19

require (
github.com/MakeNowJust/heredoc v1.0.0
github.com/bluekeyes/go-gitdiff v0.7.1
github.com/fatih/color v1.14.1
github.com/google/go-cmp v0.5.9
github.com/hako/durafmt v0.0.0-20210608085754-5c1018a4e16b
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,8 @@ github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
github.com/MakeNowJust/heredoc v1.0.0 h1:cXCdzVdstXyiTqTvfqk9SDHpKNjxuom+DOlyEeQ4pzQ=
github.com/MakeNowJust/heredoc v1.0.0/go.mod h1:mG5amYoWBHf8vpLOuehzbGGw0EHxpZZ6lCpQ4fNJ8LE=
github.com/bluekeyes/go-gitdiff v0.7.1 h1:graP4ElLRshr8ecu0UtqfNTCHrtSyZd3DABQm/DWesQ=
github.com/bluekeyes/go-gitdiff v0.7.1/go.mod h1:QpfYYO1E0fTVHVZAZKiRjtSGY9823iCdvGXBcEzHGbM=
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
Expand Down
1 change: 1 addition & 0 deletions internal/configuration/configuration.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ const (
UnleashTestCPUKey = "unleash.test-cpu"
UnleashTimeoutCoefficientKey = "unleash.timeout-coefficient"
UnleashIntegrationMode = "unleash.integration"
UnleashDiffRef = "unleash.diff"
UnleashThresholdEfficacyKey = "unleash.threshold.efficacy"
UnleashThresholdMCoverageKey = "unleash.threshold.mutant-coverage"
)
Expand Down
63 changes: 63 additions & 0 deletions internal/diff/diff.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
package diff

import (
"go/token"

"github.com/bluekeyes/go-gitdiff/gitdiff"
)

type FileName string

type Change struct {
StartLine int
EndLine int
}

type Diff map[FileName][]Change

func newDiff(files []*gitdiff.File) Diff {
result := map[FileName][]Change{}

for _, file := range files {
name, changes := newChanges(file)

result[name] = changes
}

return result
}

func newChanges(file *gitdiff.File) (FileName, []Change) {
var changes []Change

for _, fragment := range file.TextFragments {
if fragment.LinesAdded == 0 {
continue
}

startLine := int(fragment.NewPosition + fragment.LeadingContext)

changes = append(changes, Change{
StartLine: startLine,
EndLine: startLine + int(fragment.LinesAdded-1),
})
}

return FileName(file.NewName), changes
}

func (d Diff) IsChanged(pos token.Position) bool {
if len(d) == 0 {
return true
}

fileDiff := d[FileName(pos.Filename)]

for _, change := range fileDiff {
if pos.Line >= change.StartLine && pos.Line <= change.EndLine {
return true
}
}

return false
}
162 changes: 162 additions & 0 deletions internal/diff/diff_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
package diff

import (
"go/token"
"reflect"
"testing"

"github.com/bluekeyes/go-gitdiff/gitdiff"
)

func TestDiff_IsChanged(t *testing.T) {
tests := []struct {
name string
d Diff
pos token.Position
want bool
}{
{
name: "must be changed on nil Diff",
d: nil,
pos: token.Position{},
want: true,
},
{
name: "must be changed on empty Diff",
d: map[FileName][]Change{},
pos: token.Position{},
want: true,
},
{
name: "must be changed if in range",
d: map[FileName][]Change{
"test": {{StartLine: 21, EndLine: 21}},
},
pos: token.Position{Filename: "test", Line: 21},
want: true,
},
{
name: "must be unchanged if outside range",
d: map[FileName][]Change{
"test": {{StartLine: 21, EndLine: 21}},
},
pos: token.Position{Filename: "test", Line: 22},
want: false,
},
{
name: "must be unchanged if no such file",
d: map[FileName][]Change{
"test": {{StartLine: 21, EndLine: 21}},
},
pos: token.Position{Filename: "test1", Line: 21},
want: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := tt.d.IsChanged(tt.pos)
if got != tt.want {
t.Errorf("IsChanged() = %v, want %v", got, tt.want)
}
})
}
}

func Test_newDiff(t *testing.T) {
fragments := []*gitdiff.TextFragment{fragment(21, 1)}

files := []*gitdiff.File{
{
NewName: "test1",
TextFragments: fragments,
},
{
NewName: "test2",
TextFragments: fragments,
},
}

expected := Diff{
"test1": {{StartLine: 25, EndLine: 25}},
"test2": {{StartLine: 25, EndLine: 25}},
}

result := newDiff(files)
if !reflect.DeepEqual(result, expected) {
t.Log("want", expected)
t.Log("got", result)
t.Fatalf("unexpected newDiff result")
}
}

func Test_newChanges(t *testing.T) {
fragments := []*gitdiff.TextFragment{
fragment(0, 1),
fragment(10, 0),
fragment(21, 2),
fragment(44, 4),
fragment(231, 201),
}
file := &gitdiff.File{
NewName: "test",
TextFragments: fragments,
}

expect := []Change{
{StartLine: 4, EndLine: 4},
{StartLine: 25, EndLine: 26},
{StartLine: 48, EndLine: 51},
{StartLine: 235, EndLine: 435},
}

name, changes := newChanges(file)

if name != "test" {
t.Fatalf("name %s unexpected", name)
}
if !reflect.DeepEqual(changes, expect) {
t.Log("want", expect)
t.Log("got", changes)
t.Fatalf("unexpected newChanges result")
}
}

func fragment(startLine int, adds int, del ...int) *gitdiff.TextFragment {
const contexts = 4

dels := adds
if len(del) > 0 {
dels = del[0]
}

var lines []gitdiff.Line

lines = append(lines, opLines(gitdiff.OpContext, contexts)...)
lines = append(lines, opLines(gitdiff.OpDelete, dels)...)
lines = append(lines, opLines(gitdiff.OpAdd, adds)...)
lines = append(lines, opLines(gitdiff.OpContext, contexts)...)

line := int64(startLine)
added := int64(adds)
deleted := int64(dels)

return &gitdiff.TextFragment{
OldLines: line - 1,
NewPosition: line,
LinesAdded: added,
LinesDeleted: deleted,
LeadingContext: contexts,
TrailingContext: contexts,
Lines: lines,
}
}

func opLines(op gitdiff.LineOp, count int) []gitdiff.Line {
result := make([]gitdiff.Line, count)

for i := 0; i < count; i++ {
result[i] = gitdiff.Line{Op: op, Line: "test"}
}

return result
}
Loading

0 comments on commit 84488fa

Please sign in to comment.