Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
158 changes: 158 additions & 0 deletions analyzer_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package gobce
import (
"os"
"path/filepath"
"strconv"
"strings"
"testing"
)
Expand Down Expand Up @@ -135,3 +136,160 @@ func score(v int) int {
t.Fatalf("expected uncovered branches from resolvable source file")
}
}

func TestAnalyzeIfWithoutElseTerminatingBody(t *testing.T) {
tests := []struct {
name string
ifBodyCount int
fallthroughCount int
wantUncoveredTruePath bool
wantUncoveredImplicitFalse bool
}{
{
name: "only_true_path_covered",
ifBodyCount: 1,
fallthroughCount: 0,
wantUncoveredImplicitFalse: true,
},
{
name: "only_false_path_covered",
ifBodyCount: 0,
fallthroughCount: 1,
wantUncoveredTruePath: true,
},
{
name: "both_paths_covered",
ifBodyCount: 1,
fallthroughCount: 1,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
tmp := t.TempDir()
srcPath := filepath.Join(tmp, "sample.go")
src := `package sample

func score(v int) int {
if v > 10 {
return 1
}
return 2
}
`
if err := os.WriteFile(srcPath, []byte(src), 0o644); err != nil {
t.Fatalf("write source: %v", err)
}

coverPath := filepath.Join(tmp, "coverage.out")
coverage := strings.Join([]string{
"mode: set",
srcPath + ":3.23,4.13 1 1",
srcPath + ":4.13,6.3 1 " + strconv.Itoa(tt.ifBodyCount),
srcPath + ":6.3,7.10 1 " + strconv.Itoa(tt.fallthroughCount),
}, "\n")
if err := os.WriteFile(coverPath, []byte(coverage), 0o644); err != nil {
t.Fatalf("write coverage: %v", err)
}

result, err := Analyze(AnalyzeInput{CoverProfilePath: coverPath})
if err != nil {
t.Fatalf("analyze: %v", err)
}

gotUncoveredTruePath := hasUncoveredBranchKind(result, "if_true_path")
if gotUncoveredTruePath != tt.wantUncoveredTruePath {
t.Fatalf("if_true_path uncovered: got %v, want %v", gotUncoveredTruePath, tt.wantUncoveredTruePath)
}

gotUncoveredImplicitFalse := hasUncoveredBranchKind(result, "if_implicit_false_path")
if gotUncoveredImplicitFalse != tt.wantUncoveredImplicitFalse {
t.Fatalf("if_implicit_false_path uncovered: got %v, want %v", gotUncoveredImplicitFalse, tt.wantUncoveredImplicitFalse)
}
})
}
}

func TestAnalyzeIfWithoutElseNonTerminatingBody(t *testing.T) {
tmp := t.TempDir()
srcPath := filepath.Join(tmp, "sample.go")
src := `package sample

func score(v int) int {
score := 0
if v > 10 {
score = 1
}
return score
}
`
if err := os.WriteFile(srcPath, []byte(src), 0o644); err != nil {
t.Fatalf("write source: %v", err)
}

coverPath := filepath.Join(tmp, "coverage.out")
coverage := strings.Join([]string{
"mode: set",
srcPath + ":3.23,5.13 2 1",
srcPath + ":5.13,7.3 1 0",
srcPath + ":7.3,8.14 1 1",
}, "\n")
if err := os.WriteFile(coverPath, []byte(coverage), 0o644); err != nil {
t.Fatalf("write coverage: %v", err)
}

result, err := Analyze(AnalyzeInput{CoverProfilePath: coverPath})
if err != nil {
t.Fatalf("analyze: %v", err)
}

if hasUncoveredBranchKind(result, "if_implicit_false_path") {
t.Fatalf("did not expect if_implicit_false_path for non-terminating if body")
}
}

func TestAnalyzeIfWithoutElsePanicBody(t *testing.T) {
tmp := t.TempDir()
srcPath := filepath.Join(tmp, "sample.go")
src := `package sample

func score(v int) int {
if v > 10 {
panic("too high")
}
return 2
}
`
if err := os.WriteFile(srcPath, []byte(src), 0o644); err != nil {
t.Fatalf("write source: %v", err)
}

coverPath := filepath.Join(tmp, "coverage.out")
coverage := strings.Join([]string{
"mode: set",
srcPath + ":3.23,4.13 1 1",
srcPath + ":4.13,6.3 1 1",
srcPath + ":6.3,7.10 1 0",
}, "\n")
if err := os.WriteFile(coverPath, []byte(coverage), 0o644); err != nil {
t.Fatalf("write coverage: %v", err)
}

result, err := Analyze(AnalyzeInput{CoverProfilePath: coverPath})
if err != nil {
t.Fatalf("analyze: %v", err)
}

if !hasUncoveredBranchKind(result, "if_implicit_false_path") {
t.Fatalf("expected if_implicit_false_path in uncovered branches")
}
}

func hasUncoveredBranchKind(result Result, kind string) bool {
for _, b := range result.UncoveredBranches {
if b.Kind == kind {
return true
}
}
return false
}
53 changes: 50 additions & 3 deletions internal/analyzer/branches.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ func collectFileBranchCandidates(filePath string, blocks []coverageBlock) ([]bra
FilePath: filePath,
Line: fset.Position(n.Body.Lbrace).Line,
Kind: "if_true_path",
Covered: spanCovered(blocks, fset, n.Body.Pos(), n.Body.End()),
Covered: blockStmtCovered(blocks, fset, n.Body),
})

if n.Else != nil {
Expand All @@ -54,14 +54,21 @@ func collectFileBranchCandidates(filePath string, blocks []coverageBlock) ([]bra
Kind: "if_false_path",
Covered: spanCovered(blocks, fset, n.Else.Pos(), n.Else.End()),
})
} else if ifBodyTerminates(n.Body) {
candidates = append(candidates, branchCandidate{
FilePath: filePath,
Line: fset.Position(n.If).Line,
Kind: "if_implicit_false_path",
Covered: fallthroughCovered(blocks, fset, n.Body),
})
}

case *ast.SwitchStmt:
candidates = append(candidates, collectCaseClauseCandidates(filePath, fset, blocks, n.Body.List, "switch_case_path", "switch_default_path")...)
case *ast.TypeSwitchStmt:
candidates = append(candidates, collectCaseClauseCandidates(filePath, fset, blocks, n.Body.List, "type_switch_case_path", "type_switch_default_path")...)
case *ast.ForStmt:
bodyCovered := spanCovered(blocks, fset, n.Body.Pos(), n.Body.End())
bodyCovered := blockStmtCovered(blocks, fset, n.Body)
stmtCovered := spanCovered(blocks, fset, n.Pos(), n.Body.Lbrace)
candidates = append(candidates, branchCandidate{
FilePath: filePath,
Expand All @@ -76,7 +83,7 @@ func collectFileBranchCandidates(filePath string, blocks []coverageBlock) ([]bra
Covered: stmtCovered && !bodyCovered,
})
case *ast.RangeStmt:
bodyCovered := spanCovered(blocks, fset, n.Body.Pos(), n.Body.End())
bodyCovered := blockStmtCovered(blocks, fset, n.Body)
stmtCovered := spanCovered(blocks, fset, n.Pos(), n.Body.Lbrace)
candidates = append(candidates, branchCandidate{
FilePath: filePath,
Expand Down Expand Up @@ -118,6 +125,46 @@ func collectCaseClauseCandidates(filePath string, fset *token.FileSet, blocks []
return candidates
}

func ifBodyTerminates(body *ast.BlockStmt) bool {
if len(body.List) == 0 {
return false
}

last := body.List[len(body.List)-1]
switch s := last.(type) {
case *ast.ReturnStmt:
return true
case *ast.BranchStmt:
return true
case *ast.ExprStmt:
call, ok := s.X.(*ast.CallExpr)
if !ok {
return false
}
ident, ok := call.Fun.(*ast.Ident)
return ok && ident.Name == "panic"
default:
return false
}
}

func fallthroughCovered(blocks []coverageBlock, fset *token.FileSet, body *ast.BlockStmt) bool {
endPos := fset.Position(body.End())
for _, b := range blocks {
if b.Count <= 0 {
continue
}
if b.StartLine == endPos.Line && b.StartCol == endPos.Column {
return true
}
}
return false
}

func blockStmtCovered(blocks []coverageBlock, fset *token.FileSet, body *ast.BlockStmt) bool {
return spanCovered(blocks, fset, body.Lbrace+1, body.End())
}

func spanCovered(blocks []coverageBlock, fset *token.FileSet, start token.Pos, end token.Pos) bool {
startPos := fset.Position(start)
endPos := fset.Position(end)
Expand Down
Loading