diff --git a/coverage/coverage.go b/coverage/coverage.go index a8de86bb..858633dd 100644 --- a/coverage/coverage.go +++ b/coverage/coverage.go @@ -24,18 +24,6 @@ import ( "os/exec" ) -// Block holds the start and end coordinates of a section of a source file -// covered by tests. -type Block struct { - StartLine int - StartCol int - EndLine int - EndCol int -} - -// Profile is implemented as a map holding a slice of Block per each filename. -type Profile map[string][]Block - // Coverage is responsible for executing a Go test with coverage via the Run() method, // then parsing the result coverage report file. type Coverage struct { diff --git a/coverage/profile.go b/coverage/profile.go new file mode 100644 index 00000000..ca3a40d8 --- /dev/null +++ b/coverage/profile.go @@ -0,0 +1,48 @@ +/* + * 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 coverage + +import ( + "go/token" +) + +// Block holds the start and end coordinates of a section of a source file +// covered by tests. +type Block struct { + StartLine int + StartCol int + EndLine int + EndCol int +} + +// Profile is implemented as a map holding a slice of Block per each filename. +type Profile map[string][]Block + +func (p Profile) IsCovered(pos token.Position) bool { + block, ok := p[pos.Filename] + if !ok { + return false + } + for _, b := range block { + if pos.Line >= b.StartLine && pos.Line < b.EndLine { + if pos.Column >= b.StartCol && pos.Column < b.EndCol { + return true + } + } + } + return false +} diff --git a/coverage/profile_test.go b/coverage/profile_test.go new file mode 100644 index 00000000..4304c904 --- /dev/null +++ b/coverage/profile_test.go @@ -0,0 +1,106 @@ +/* + * 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 coverage_test + +import ( + "github.com/k3rn31/gremlins/coverage" + "go/token" + "testing" +) + +func TestIsCovered(t *testing.T) { + testCases := []struct { + name string + proFilename string + proStartL int + proEndL int + proStartC int + proEndC int + + posFilename string + posL int + posC int + + expected bool + }{ + { + name: "returns true when position is covered", + proFilename: "test", + proStartL: 10, + proEndL: 11, + proStartC: 10, + proEndC: 11, + posFilename: "test", + posL: 10, + posC: 10, + expected: true, + }, + { + name: "returns false when position is not covered", + proFilename: "test", + proStartL: 10, + proEndL: 11, + proStartC: 10, + proEndC: 11, + posFilename: "test", + posL: 11, + posC: 10, + expected: false, + }, + { + name: "returns false when filename is not found", + proFilename: "test_pro", + proStartL: 10, + proEndL: 5, + proStartC: 10, + proEndC: 6, + posFilename: "test_pos", + posL: 10, + posC: 6, + expected: false, + }, + } + + for _, tc := range testCases { + tc := tc + t.Run(tc.name, func(t *testing.T) { + profile := coverage.Profile{ + tc.proFilename: { + { + StartLine: tc.proStartL, + StartCol: tc.proStartC, + EndLine: tc.proEndL, + EndCol: tc.proEndC, + }, + }, + } + + position := token.Position{ + Filename: tc.posFilename, + Offset: 100, + Line: tc.posL, + Column: tc.posC, + } + + got := profile.IsCovered(position) + + if got != tc.expected { + t.Errorf("expected coverage to be %v, got %v", tc.expected, got) + } + }) + } +} diff --git a/mutator/mutator.go b/mutator/mutator.go new file mode 100644 index 00000000..799721eb --- /dev/null +++ b/mutator/mutator.go @@ -0,0 +1,88 @@ +/* + * 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 mutator + +import ( + "github.com/k3rn31/gremlins/coverage" + "go/ast" + "go/parser" + "go/token" +) + +type Mutant struct { + MutantType MutantType + TokenType token.Token + Position token.Position + Covered bool +} + +type Mutator struct { + covProfile coverage.Profile +} + +func New(p coverage.Profile) *Mutator { + return &Mutator{covProfile: p} +} + +func (m Mutator) RunWithFileName(fileName string) []Mutant { + var result []Mutant + set := token.NewFileSet() + file, _ := parser.ParseFile(set, fileName, nil, parser.ParseComments) + ast.Inspect(file, func(node ast.Node) bool { + switch node := node.(type) { + case *ast.BinaryExpr: + r, ok := inspectBinaryExpr(set, node) + if !ok { + return false + } + r.Covered = m.covProfile.IsCovered(r.Position) + result = append(result, r) + } + return true + }) + return result +} + +func inspectBinaryExpr(set *token.FileSet, be *ast.BinaryExpr) (Mutant, bool) { + switch be.Op { + case token.GTR: + return Mutant{ + MutantType: CONDITIONAL_BOUNDARY, + TokenType: token.GTR, + Position: set.Position(be.OpPos), + }, true + case token.LSS: + return Mutant{ + MutantType: CONDITIONAL_BOUNDARY, + TokenType: token.LSS, + Position: set.Position(be.OpPos), + }, true + case token.LEQ: + return Mutant{ + MutantType: CONDITIONAL_BOUNDARY, + TokenType: token.LEQ, + Position: set.Position(be.OpPos), + }, true + case token.GEQ: + return Mutant{ + MutantType: CONDITIONAL_BOUNDARY, + TokenType: token.GEQ, + Position: set.Position(be.OpPos), + }, true + } + return Mutant{}, false +} diff --git a/mutator/mutator_test.go b/mutator/mutator_test.go new file mode 100644 index 00000000..f58b4e83 --- /dev/null +++ b/mutator/mutator_test.go @@ -0,0 +1,171 @@ +/* + * 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 mutator_test + +import ( + "github.com/google/go-cmp/cmp" + "github.com/k3rn31/gremlins/coverage" + "github.com/k3rn31/gremlins/mutator" + "go/token" + "testing" +) + +func TestMutations(t *testing.T) { + testCases := []struct { + name string + fileName string + mutantType mutator.MutantType + tokenType token.Token + position token.Position + covProfile coverage.Profile + covered bool + }{ + { + name: "it recognizes CONDITIONAL_BOUNDARY with GTR", + mutantType: mutator.CONDITIONAL_BOUNDARY, + tokenType: token.GTR, + position: token.Position{ + Filename: "testdata/fixtures/conditional_boundary_gtr_go", + Offset: 65, + Line: 6, + Column: 8, + }, + covProfile: coverage.Profile{ + "testdata/fixtures/conditional_boundary_gtr_go": { + { + StartLine: 6, + StartCol: 8, + EndLine: 6, + EndCol: 8, + }, + }, + }, + covered: false, + }, + { + name: "it recognizes CONDITIONAL_BOUNDARY with LSS", + mutantType: mutator.CONDITIONAL_BOUNDARY, + tokenType: token.LSS, + position: token.Position{ + Filename: "testdata/fixtures/conditional_boundary_lss_go", + Offset: 65, + Line: 6, + Column: 8, + }, + covProfile: coverage.Profile{ + "testdata/fixtures/conditional_boundary_gtr_go": { + { + StartLine: 8, + StartCol: 8, + EndLine: 9, + EndCol: 8, + }, + }, + }, + covered: false, + }, + { + name: "it recognizes CONDITIONAL_BOUNDARY with LEQ", + mutantType: mutator.CONDITIONAL_BOUNDARY, + tokenType: token.LEQ, + position: token.Position{ + Filename: "testdata/fixtures/conditional_boundary_leq_go", + Offset: 65, + Line: 6, + Column: 8, + }, + covProfile: coverage.Profile{ + "testdata/fixtures/conditional_boundary_leq_go": { + { + StartLine: 8, + StartCol: 8, + EndLine: 9, + EndCol: 8, + }, + }, + }, + covered: false, + }, + { + name: "it recognizes CONDITIONAL_BOUNDARY with GEQ", + mutantType: mutator.CONDITIONAL_BOUNDARY, + tokenType: token.GEQ, + position: token.Position{ + Filename: "testdata/fixtures/conditional_boundary_geq_go", + Offset: 65, + Line: 6, + Column: 8, + }, + covProfile: coverage.Profile{ + "testdata/fixtures/conditional_boundary_geq_go": { + { + StartLine: 8, + StartCol: 8, + EndLine: 9, + EndCol: 8, + }, + }, + }, + covered: false, + }, + { + name: "it skips invalid CONDITIONAL_BOUNDARY", + mutantType: mutator.CONDITIONAL_BOUNDARY, + tokenType: token.ILLEGAL, + position: token.Position{ + Filename: "testdata/fixtures/conditional_boundary_illegal_go", + Offset: 65, + Line: 6, + Column: 8, + }, + covProfile: coverage.Profile{ + "testdata/fixtures/conditional_boundary_gtr_go": { + { + StartLine: 8, + StartCol: 8, + EndLine: 9, + EndCol: 8, + }, + }, + }, + }, + } + for _, tc := range testCases { + tc := tc + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + want := []mutator.Mutant{ + { + MutantType: tc.mutantType, + TokenType: tc.tokenType, + Position: tc.position, + Covered: tc.covered, + }, + } + if tc.tokenType == token.ILLEGAL { + want = nil + } + + mut := mutator.New(tc.covProfile) + got := mut.RunWithFileName(tc.position.Filename) + + if !cmp.Equal(got, want) { + t.Errorf(cmp.Diff(got, want)) + } + }) + } +} diff --git a/mutator/testdata/fixtures/conditional_boundary_geq_go b/mutator/testdata/fixtures/conditional_boundary_geq_go new file mode 100644 index 00000000..25819fdc --- /dev/null +++ b/mutator/testdata/fixtures/conditional_boundary_geq_go @@ -0,0 +1,9 @@ +package main +import "fmt" +func main() { + a := 1 + b := 2 + if a >= 2 { + // Do something + } +} diff --git a/mutator/testdata/fixtures/conditional_boundary_gtr_go b/mutator/testdata/fixtures/conditional_boundary_gtr_go new file mode 100644 index 00000000..39379f16 --- /dev/null +++ b/mutator/testdata/fixtures/conditional_boundary_gtr_go @@ -0,0 +1,9 @@ +package main +import "fmt" +func main() { + a := 1 + b := 2 + if a > 2 { + // Do something + } +} diff --git a/mutator/testdata/fixtures/conditional_boundary_illegal_go b/mutator/testdata/fixtures/conditional_boundary_illegal_go new file mode 100644 index 00000000..09e8b32c --- /dev/null +++ b/mutator/testdata/fixtures/conditional_boundary_illegal_go @@ -0,0 +1,9 @@ +package main +import "fmt" +func main() { + a := 1 + b := 2 + if a << 2 { + // ILLEGAL + } +} diff --git a/mutator/testdata/fixtures/conditional_boundary_leq_go b/mutator/testdata/fixtures/conditional_boundary_leq_go new file mode 100644 index 00000000..774a0317 --- /dev/null +++ b/mutator/testdata/fixtures/conditional_boundary_leq_go @@ -0,0 +1,9 @@ +package main +import "fmt" +func main() { + a := 1 + b := 2 + if a <= 2 { + // Do something + } +} diff --git a/mutator/testdata/fixtures/conditional_boundary_lss_go b/mutator/testdata/fixtures/conditional_boundary_lss_go new file mode 100644 index 00000000..90a962a3 --- /dev/null +++ b/mutator/testdata/fixtures/conditional_boundary_lss_go @@ -0,0 +1,9 @@ +package main +import "fmt" +func main() { + a := 1 + b := 2 + if a < 2 { + // Do something + } +} diff --git a/mutator/types.go b/mutator/types.go new file mode 100644 index 00000000..ec18c4c8 --- /dev/null +++ b/mutator/types.go @@ -0,0 +1,23 @@ +/* + * 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 mutator + +type MutantType int + +const ( + CONDITIONAL_BOUNDARY MutantType = iota +)