Skip to content

Commit

Permalink
Merge branch 'feature/#2' into develop
Browse files Browse the repository at this point in the history
  • Loading branch information
k3rn31 committed Jun 25, 2022
2 parents ac0fbfd + 8ca45ec commit 9b1538e
Show file tree
Hide file tree
Showing 13 changed files with 607 additions and 46 deletions.
55 changes: 34 additions & 21 deletions coverage/coverage.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,54 +17,62 @@
package coverage

import (
"bufio"
"bytes"
"fmt"
"golang.org/x/tools/cover"
"io"
"os"
"os/exec"
"strings"
)

// 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 {
cmdContext execContext
workDir string
fileName string
mod string
}

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 string) *Coverage {
return NewWithCmd(exec.Command, workdir)
return NewWithCmdAndPackage(exec.Command, getMod(), workdir)
}

func getMod() string {
file, err := os.Open("go.mod")
if err != nil {
fmt.Printf("not in a go module root folder: %v\n", err)
os.Exit(-1)
}
r := bufio.NewReader(file)
line, _, err := r.ReadLine()
if err != nil {
fmt.Printf("not in a go module root folder %v\n\n", err)
os.Exit(-1)
}
packageName := bytes.TrimPrefix(line, []byte("module "))
return string(packageName)
}

// NewWithCmd instantiates a Coverage element given a custom execContext.
func NewWithCmd(cmdContext execContext, workdir string) *Coverage {
// NewWithCmdAndPackage instantiates a Coverage element given a custom execContext.
func NewWithCmdAndPackage(cmdContext execContext, mod, workdir string) *Coverage {
return &Coverage{
cmdContext: cmdContext,
workDir: workdir,
fileName: "coverage",
mod: mod,
}
}

// Run executes the coverage command and parses the results, returning a *Profile
// object.
func (c Coverage) Run() (*Profile, error) {
func (c Coverage) Run() (Profile, error) {
err := c.execute()
if err != nil {
return nil, err
Expand All @@ -77,12 +85,12 @@ func (c Coverage) Run() (*Profile, error) {
return profile, nil
}

func (c Coverage) getProfile(err error) (*Profile, error) {
func (c Coverage) getProfile(err error) (Profile, error) {
cf, err := os.Open(c.filePath())
if err != nil {
return nil, err
}
profile, err := parse(cf)
profile, err := c.parse(cf)
if err != nil {
return nil, err
}
Expand All @@ -103,7 +111,7 @@ func (c Coverage) execute() error {
return nil
}

func parse(data io.Reader) (*Profile, error) {
func (c Coverage) parse(data io.Reader) (Profile, error) {
profiles, err := cover.ParseProfilesFromReader(data)
if err != nil {
return nil, err
Expand All @@ -117,8 +125,13 @@ func parse(data io.Reader) (*Profile, error) {
EndLine: b.EndLine,
EndCol: b.EndCol,
}
status[p.FileName] = append(status[p.FileName], block)
fn := removeModuleFromPath(p, c)
status[fn] = append(status[fn], block)
}
}
return &status, nil
return status, nil
}

func removeModuleFromPath(p *cover.Profile, c Coverage) string {
return strings.ReplaceAll(p.FileName, c.mod+"/", "")
}
40 changes: 15 additions & 25 deletions coverage/coverage_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,12 @@
package coverage_test

import (
"fmt"
"github.com/google/go-cmp/cmp"
"github.com/k3rn31/gremlins/coverage"
"os"
"os/exec"
"strings"
"testing"
)

Expand All @@ -34,34 +36,22 @@ func TestCoverageRun(t *testing.T) {
wantWorkdir := "workdir"
wantFilename := "coverage"
wantFilePath := wantWorkdir + "/" + wantFilename
got := &commandHolder{}
cov := coverage.NewWithCmd(fakeExecCommandSuccess(got), wantWorkdir)
holder := &commandHolder{}
cov := coverage.NewWithCmdAndPackage(fakeExecCommandSuccess(holder), "example.com", wantWorkdir)

_, _ = cov.Run()

if !cmp.Equal(got.command, "go") {
t.Errorf("expected commandHolder to be 'go', got %q", got.command)
}
if !cmp.Equal(got.args[0], "test") {
t.Errorf("expected 'test', got %q", got.args[0])
}
if !cmp.Equal(got.args[1], "-cover") {
t.Errorf("expected '-cover', got %q", got.args[0])
}
if !cmp.Equal(got.args[2], "-coverprofile") {
t.Errorf("expected '-coverprofile', got %q", got.args[0])
}
if !cmp.Equal(got.args[3], wantFilePath) {
t.Errorf("expected %q, got %q", wantFilePath, got.args[3])
}
if !cmp.Equal(got.args[4], "./...") {
t.Errorf("expected %q, got %q", "./...", got.args[4])
want := fmt.Sprintf("go test -cover -coverprofile %v ./...", wantFilePath)
got := fmt.Sprintf("go %v", strings.Join(holder.args, " "))

if !cmp.Equal(got, want) {
t.Errorf(cmp.Diff(got, want))
}
}

func TestCoverageRunFails(t *testing.T) {
t.Parallel()
cov := coverage.NewWithCmd(fakeExecCommandFailure, "workdir")
cov := coverage.NewWithCmdAndPackage(fakeExecCommandFailure, "example.com", "workdir")
_, err := cov.Run()
if err == nil {
t.Error("expected run to report an error")
Expand All @@ -70,9 +60,9 @@ func TestCoverageRunFails(t *testing.T) {

func TestCoverageParsesOutput(t *testing.T) {
t.Parallel()
cov := coverage.NewWithCmd(fakeExecCommandSuccess(nil), "testdata/valid")
want := &coverage.Profile{
"example.com/path/file1.go": {
cov := coverage.NewWithCmdAndPackage(fakeExecCommandSuccess(nil), "example.com", "testdata/valid")
want := coverage.Profile{
"path/file1.go": {
{
StartLine: 47,
StartCol: 2,
Expand All @@ -86,7 +76,7 @@ func TestCoverageParsesOutput(t *testing.T) {
EndCol: 20,
},
},
"example.com/path2/file2.go": {
"path2/file2.go": {
{
StartLine: 52,
StartCol: 2,
Expand All @@ -108,7 +98,7 @@ func TestCoverageParsesOutput(t *testing.T) {

func TestParseOutputFail(t *testing.T) {
t.Parallel()
cov := coverage.NewWithCmd(fakeExecCommandSuccess(nil), "testdata/invalid")
cov := coverage.NewWithCmdAndPackage(fakeExecCommandSuccess(nil), "example.com", "testdata/invalid")

_, err := cov.Run()
if err == nil {
Expand Down
48 changes: 48 additions & 0 deletions coverage/profile.go
Original file line number Diff line number Diff line change
@@ -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
}
118 changes: 118 additions & 0 deletions coverage/profile_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
/*
* 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 line and column are covered",
proFilename: "test",
proStartL: 10,
proEndL: 10,
proStartC: 10,
proEndC: 10,
posFilename: "test",
posL: 10,
posC: 10,
expected: true,
},
{
name: "returns false when line is not covered and column is covered",
proFilename: "test",
proStartL: 10,
proEndL: 10,
proStartC: 10,
proEndC: 10,
posFilename: "test",
posL: 11,
posC: 10,
expected: false,
},
{
name: "returns false when line is covered and column not covered",
proFilename: "test",
proStartL: 10,
proEndL: 10,
proStartC: 10,
proEndC: 10,
posFilename: "test",
posL: 10,
posC: 11,
expected: false,
},
{
name: "returns false when filename is not found",
proFilename: "test_pro",
proStartL: 10,
proEndL: 10,
proStartC: 10,
proEndC: 10,
posFilename: "test_pos",
posL: 10,
posC: 10,
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)
}
})
}
}
Loading

0 comments on commit 9b1538e

Please sign in to comment.