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
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,9 @@ go run github.com/keyskey/gobce/cmd/gobce@latest analyze --coverprofile coverage
jq -e '.estimatedBranchCoverage >= 70' gobce.json
```

`uncoveredBranches` is nested by `importPath` and file `path`. To flatten all branch entries with `jq`, for example:
`jq '[.uncoveredBranches.packages[] | .files[] | .branches[]]' gobce.json`

For stable CI behavior, prefer pinned versions (`@vX.Y.Z`) over `@latest`.

## Versioning Policy
Expand Down
31 changes: 20 additions & 11 deletions analyze.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,20 +10,29 @@ func Analyze(input AnalyzeInput) (Result, error) {
return Result{}, err
}

uncovered := make([]UncoveredBranch, 0, len(analysisResult.UncoveredBranches))
for _, b := range analysisResult.UncoveredBranches {
uncovered = append(uncovered, UncoveredBranch{
File: b.File,
Line: b.Line,
Kind: b.Kind,
Recommendation: b.Recommendation,
})
}

return Result{
Language: analysisResult.Language,
StatementCoverage: analysisResult.StatementCoverage,
EstimatedBranchCoverage: analysisResult.EstimatedBranchCoverage,
UncoveredBranches: uncovered,
UncoveredBranches: convertUncoveredRollup(analysisResult.UncoveredBranches),
}, nil
}

func convertUncoveredRollup(in analyzer.UncoveredBranchesReport) UncoveredBranchesReport {
pkgs := make([]UncoveredPackage, len(in.Packages))
for i, p := range in.Packages {
files := make([]UncoveredFile, len(p.Files))
for j, f := range p.Files {
br := make([]UncoveredBranch, len(f.Branches))
for k, b := range f.Branches {
br[k] = UncoveredBranch{
Line: b.Line,
Kind: b.Kind,
}
}
files[j] = UncoveredFile{Path: f.Path, Branches: br}
}
pkgs[i] = UncoveredPackage{ImportPath: p.ImportPath, Files: files}
}
return UncoveredBranchesReport{Packages: pkgs}
}
42 changes: 21 additions & 21 deletions analyzer_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,18 +44,11 @@ func score(v int) int {
if result.Language != "go" {
t.Fatalf("language: got %q", result.Language)
}
if len(result.UncoveredBranches) == 0 {
if totalUncoveredBranches(result) == 0 {
t.Fatalf("expected uncovered branches")
}

var hasIfFalse bool
for _, b := range result.UncoveredBranches {
if b.Kind == "if_false_path" {
hasIfFalse = true
break
}
}
if !hasIfFalse {
if !hasUncoveredBranchKind(result, "if_false_path") {
t.Fatalf("expected if_false_path in uncovered branches")
}
}
Expand Down Expand Up @@ -86,14 +79,7 @@ func score(v int) int { if v > 10 { return 1 } else { return 2 } }
t.Fatalf("analyze: %v", err)
}

var hasIfFalse bool
for _, b := range result.UncoveredBranches {
if b.Kind == "if_false_path" {
hasIfFalse = true
break
}
}
if !hasIfFalse {
if !hasUncoveredBranchKind(result, "if_false_path") {
t.Fatalf("expected if_false_path in uncovered branches")
}
}
Expand Down Expand Up @@ -132,7 +118,7 @@ func score(v int) int {
t.Fatalf("analyze should not fail on unresolved source path: %v", err)
}

if len(result.UncoveredBranches) == 0 {
if totalUncoveredBranches(result) == 0 {
t.Fatalf("expected uncovered branches from resolvable source file")
}
}
Expand Down Expand Up @@ -325,10 +311,24 @@ func score(v int) int {
}
}

func totalUncoveredBranches(r Result) int {
n := 0
for _, p := range r.UncoveredBranches.Packages {
for _, f := range p.Files {
n += len(f.Branches)
}
}
return n
}

func hasUncoveredBranchKind(result Result, kind string) bool {
for _, b := range result.UncoveredBranches {
if b.Kind == kind {
return true
for _, p := range result.UncoveredBranches.Packages {
for _, f := range p.Files {
for _, b := range f.Branches {
if b.Kind == kind {
return true
}
}
}
}
return false
Expand Down
27 changes: 19 additions & 8 deletions docs/gobce.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,13 +46,24 @@ go run ./cmd/gobce analyze \
"language": "go",
"statementCoverage": 82.1,
"estimatedBranchCoverage": 68.4,
"uncoveredBranches": [
{
"file": "internal/order/validator.go",
"line": 42,
"kind": "if_false_path"
}
]
"uncoveredBranches": {
"packages": [
{
"importPath": "github.com/example/app/internal/order",
"files": [
{
"path": "internal/order/validator.go",
"branches": [
{
"line": 42,
"kind": "if_false_path"
}
]
}
]
}
]
}
}
```

Expand Down Expand Up @@ -88,7 +99,7 @@ generated-code filtering improvements
4. coverage block と branch candidate span を対応付ける。
5. 各 branch side が実行されたかを推定する。
6. estimated C1 percentage を計算する。
7. file, line, kind, recommendation 付きの uncovered branch finding を出す。
7. import path とファイルごとにまとめた uncovered branch finding(line, kind)を出す。
```

## 重要な制約
Expand Down
30 changes: 20 additions & 10 deletions docs/how-gobce-works.md
Original file line number Diff line number Diff line change
Expand Up @@ -93,13 +93,24 @@ sample.go:6.8,8.3 ... count=0 (false 側の範囲)

```json
{
"uncoveredBranches": [
{
"file": "sample.go",
"line": 6,
"kind": "if_false_path"
}
]
"uncoveredBranches": {
"packages": [
{
"importPath": "example.com/m",
"files": [
{
"path": "sample.go",
"branches": [
{
"line": 6,
"kind": "if_false_path"
}
]
}
]
}
]
}
}
```

Expand All @@ -115,8 +126,7 @@ sample.go:6.8,8.3 ... count=0 (false 側の範囲)
`gobce` が分岐候補に対して covered/uncovered を推定して計算した割合。

- `uncoveredBranches`
テスト実行時にまだ実行されていないと推定された分岐の一覧。
どのファイル・どの行・どの kind かが入ります。
テスト実行時にまだ実行されていないと推定された分岐の一覧。`go.mod` の module path が分かる場合は `importPath` とモジュール相対の `path` に階層化し、各ファイルの `branches` に行番号と `kind` が入ります。

## 「推定」になる理由

Expand All @@ -135,7 +145,7 @@ A. 直線的な処理のコードはテスト実行時に実行されていて

### Q. まず何を見ればいい?

A. `uncoveredBranches` から見れば OK です。
A. `uncoveredBranches.packages` 以下を辿れば OK です。
分岐の未到達ポイントが具体的に出るので、テスト追加の優先順位を付けやすくなります。

## 開発者向けの読み進め順
Expand Down
17 changes: 10 additions & 7 deletions internal/analyzer/analyze.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,11 @@ func Analyze(input Input) (Result, error) {
return Result{}, errors.New("coverprofile path is required")
}

blocks, err := parseCoverProfile(input.CoverProfilePath)
parsed, err := parseCoverProfile(input.CoverProfilePath)
if err != nil {
return Result{}, err
}
blocks := parsed.Blocks

statementCoverage := computeStatementCoverage(blocks)
candidates, err := collectBranchCandidates(blocks)
Expand All @@ -19,20 +20,22 @@ func Analyze(input Input) (Result, error) {
}

covered := 0
uncovered := make([]UncoveredBranch, 0)
sources := make([]uncoveredSource, 0)
for _, c := range candidates {
if c.Covered {
covered++
continue
}

uncovered = append(uncovered, UncoveredBranch{
File: c.FilePath,
Line: c.Line,
Kind: c.Kind,
sources = append(sources, uncoveredSource{
file: c.FilePath,
line: c.Line,
kind: c.Kind,
})
}

rollup := groupUncoveredSources(sources, parsed.ModuleRoot, parsed.ModulePath)

estimated := 100.0
if len(candidates) > 0 {
estimated = percent(float64(covered), float64(len(candidates)))
Expand All @@ -42,6 +45,6 @@ func Analyze(input Input) (Result, error) {
Language: "go",
StatementCoverage: statementCoverage,
EstimatedBranchCoverage: estimated,
UncoveredBranches: uncovered,
UncoveredBranches: rollup,
}, nil
}
30 changes: 22 additions & 8 deletions internal/analyzer/coverprofile.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,17 @@ import (
"strings"
)

func parseCoverProfile(path string) ([]coverageBlock, error) {
// ParsedCoverProfile holds normalized coverage blocks and module context used for path grouping.
type ParsedCoverProfile struct {
Blocks []coverageBlock
ModuleRoot string
ModulePath string
}

func parseCoverProfile(path string) (ParsedCoverProfile, error) {
f, err := os.Open(path)
if err != nil {
return nil, fmt.Errorf("open coverprofile: %w", err)
return ParsedCoverProfile{}, fmt.Errorf("open coverprofile: %w", err)
}
defer f.Close()

Expand All @@ -32,24 +39,31 @@ func parseCoverProfile(path string) ([]coverageBlock, error) {

b, err := parseCoverProfileLine(line)
if err != nil {
return nil, fmt.Errorf("parse coverprofile line %d: %w", lineNo, err)
return ParsedCoverProfile{}, fmt.Errorf("parse coverprofile line %d: %w", lineNo, err)
}
blocks = append(blocks, b)
}

if err := scanner.Err(); err != nil {
return nil, fmt.Errorf("scan coverprofile: %w", err)
return ParsedCoverProfile{}, fmt.Errorf("scan coverprofile: %w", err)
}

if len(blocks) == 0 {
return nil, errors.New("coverprofile contains no coverage blocks")
return ParsedCoverProfile{}, errors.New("coverprofile contains no coverage blocks")
}

normalizedBlocks, err := normalizeBlockPaths(blocks, path)
coverDir := filepath.Dir(path)
moduleRoot, modulePath := discoverModuleContext(coverDir)

normalizedBlocks, err := normalizeBlockPaths(blocks, path, moduleRoot, modulePath)
if err != nil {
return nil, err
return ParsedCoverProfile{}, err
}
return normalizedBlocks, nil
return ParsedCoverProfile{
Blocks: normalizedBlocks,
ModuleRoot: moduleRoot,
ModulePath: modulePath,
}, nil
}

func parseCoverProfileLine(line string) (coverageBlock, error) {
Expand Down
3 changes: 1 addition & 2 deletions internal/analyzer/path_resolver.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,8 @@ import (
"strings"
)

func normalizeBlockPaths(blocks []coverageBlock, coverProfilePath string) ([]coverageBlock, error) {
func normalizeBlockPaths(blocks []coverageBlock, coverProfilePath string, moduleRoot string, modulePath string) ([]coverageBlock, error) {
coverDir := filepath.Dir(coverProfilePath)
moduleRoot, modulePath := discoverModuleContext(coverDir)

normalized := make([]coverageBlock, 0, len(blocks))
for _, b := range blocks {
Expand Down
Loading
Loading