Skip to content

Commit

Permalink
decouple
Browse files Browse the repository at this point in the history
  • Loading branch information
notJoon committed Nov 29, 2024
1 parent abdd0b3 commit 88128a4
Show file tree
Hide file tree
Showing 13 changed files with 1,386 additions and 1,827 deletions.
93 changes: 1 addition & 92 deletions gnovm/cmd/gno/test.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,13 +26,6 @@ type testCfg struct {
updateGoldenTests bool
printRuntimeMetrics bool
printEvents bool

// coverage flags
coverage bool
viewFile string
showHits bool
output string
htmlOutput string
}

func newTestCmd(io commands.IO) *commands.Command {
Expand Down Expand Up @@ -144,43 +137,6 @@ func (c *testCfg) RegisterFlags(fs *flag.FlagSet) {
"print runtime metrics (gas, memory, cpu cycles)",
)

// test coverage flags

fs.BoolVar(
&c.coverage,
"cover",
false,
"enable coverage analysis",
)

fs.BoolVar(
&c.showHits,
"show-hits",
false,
"show number of times each line was executed",
)

fs.StringVar(
&c.viewFile,
"view",
"",
"view coverage for a specific file",
)

fs.StringVar(
&c.output,
"out",
"",
"save coverage data as JSON to specified file",
)

fs.StringVar(
&c.htmlOutput,
"html",
"",
"output coverage report in HTML format",
)

fs.BoolVar(
&c.printEvents,
"print-events",
Expand Down Expand Up @@ -239,29 +195,18 @@ func execTest(cfg *testCfg, args []string, io commands.IO) error {
io.ErrPrintfln("? %s \t[no test files]", pkg.Dir)
continue
}

coverageData := gno.NewCoverageData(cfg.rootDir)
if cfg.coverage {
coverageData.Enable()
} else {
coverageData.Disable()
}

// Determine gnoPkgPath by reading gno.mod
var gnoPkgPath string
modfile, err := gnomod.ParseAt(pkg.Dir)
if err == nil {
// TODO: use pkgPathFromRootDir?
gnoPkgPath = modfile.Module.Mod.Path
coverageData.PkgPath = gnoPkgPath
} else {
gnoPkgPath = pkgPathFromRootDir(pkg.Dir, cfg.rootDir)
if gnoPkgPath == "" {
// unable to read pkgPath from gno.mod, generate a random realm path
io.ErrPrintfln("--- WARNING: unable to read package path from gno.mod or gno root directory; try creating a gno.mod file")
gnoPkgPath = gno.RealmPathPrefix + strings.ToLower(random.RandStr(8))
}
coverageData.PkgPath = pkgPath
}

memPkg := gno.ReadMemPackage(pkg.Dir, gnoPkgPath)
Expand All @@ -271,12 +216,6 @@ func execTest(cfg *testCfg, args []string, io commands.IO) error {
err = test.Test(memPkg, pkg.Dir, opts)
})

m := tests.TestMachine(testStore, stdout, gnoPkgPath)
if coverageData.IsEnabled() {
m.Coverage = coverageData
m.Coverage.CurrentPackage = memPkg.Path
}

duration := time.Since(startedAt)
dstr := fmtDuration(duration)

Expand All @@ -297,37 +236,7 @@ func execTest(cfg *testCfg, args []string, io commands.IO) error {
return fmt.Errorf("FAIL: %d build errors, %d test errors", buildErrCount, testErrCount)
}

if cfg.coverage {
// TODO: consider cache
if cfg.viewFile != "" {
err := coverageData.ViewFiles(cfg.viewFile, cfg.showHits, io)
if err != nil {
return fmt.Errorf("failed to view file coverage: %w", err)
}
return nil // prevent printing out coverage report
}

if cfg.output != "" {
err := coverageData.SaveJSON(cfg.output)
if err != nil {
return fmt.Errorf("failed to save coverage data: %w", err)
}
io.Println("coverage data saved to", cfg.output)
return nil
}

if cfg.htmlOutput != "" {
err := coverageData.SaveHTML(cfg.htmlOutput)
if err != nil {
return fmt.Errorf("failed to save coverage data: %w", err)
}
io.Println("coverage report saved to", cfg.htmlOutput)
return nil
}
coverageData.Report(io)
}

return errs
return nil
}

// attempts to determine the full gno pkg path by analyzing the directory.
Expand Down
108 changes: 108 additions & 0 deletions gnovm/pkg/coverage/analyze.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
package coverage

import (
"go/ast"
"go/parser"
"go/token"
)

// detectExecutableLines analyzes the given source code content and returns a map
// of line numbers to boolean values indicating whether each line is executable.
func DetectExecutableLines(content string) (map[int]bool, error) {
fset := token.NewFileSet()
node, err := parser.ParseFile(fset, "", content, parser.ParseComments)
if err != nil {
return nil, err
}

executableLines := make(map[int]bool)

ast.Inspect(node, func(n ast.Node) bool {
if n == nil {
return true
}

if isExecutableLine(n) {
line := fset.Position(n.Pos()).Line
executableLines[line] = true
}

return true
})

return executableLines, nil
}

// countCodeLines counts the number of executable lines in the given source code content.
func CountCodeLines(content string) int {
lines, err := DetectExecutableLines(content)
if err != nil {
return 0
}

Check warning on line 41 in gnovm/pkg/coverage/analyze.go

View check run for this annotation

Codecov / codecov/patch

gnovm/pkg/coverage/analyze.go#L37-L41

Added lines #L37 - L41 were not covered by tests

return len(lines)

Check warning on line 43 in gnovm/pkg/coverage/analyze.go

View check run for this annotation

Codecov / codecov/patch

gnovm/pkg/coverage/analyze.go#L43

Added line #L43 was not covered by tests
}

// isExecutableLine determines whether a given AST node represents an
// executable line of code for the purpose of code coverage measurement.
//
// It returns true for statement nodes that typically contain executable code,
// such as assignments, expressions, return statements, and control flow statements.
//
// It returns false for nodes that represent non-executable lines, such as
// declarations, blocks, and function definitions.
func isExecutableLine(node ast.Node) bool {
switch n := node.(type) {
case *ast.AssignStmt, *ast.ExprStmt, *ast.ReturnStmt, *ast.BranchStmt,
*ast.IncDecStmt, *ast.GoStmt, *ast.DeferStmt, *ast.SendStmt:
return true
case *ast.IfStmt, *ast.ForStmt, *ast.RangeStmt, *ast.SwitchStmt,
*ast.TypeSwitchStmt, *ast.SelectStmt:
return true
case *ast.CaseClause:
// Even if a `case` condition (e.g., `case 1:`) in a `switch` statement is executed,
// the condition itself is not included in the coverage; coverage only recorded for the
// code block inside the corresponding `case` clause.
return false
case *ast.LabeledStmt:
return isExecutableLine(n.Stmt)

Check warning on line 68 in gnovm/pkg/coverage/analyze.go

View check run for this annotation

Codecov / codecov/patch

gnovm/pkg/coverage/analyze.go#L62-L68

Added lines #L62 - L68 were not covered by tests
case *ast.FuncDecl:
return false
case *ast.BlockStmt:
return false
case *ast.DeclStmt:
// check inner declarations in the DeclStmt (e.g. `var a, b = 1, 2`)
// if there is a value initialization, then the line is executable
genDecl, ok := n.Decl.(*ast.GenDecl)
if ok && (genDecl.Tok == token.VAR || genDecl.Tok == token.CONST) {
for _, spec := range genDecl.Specs {
valueSpec, ok := spec.(*ast.ValueSpec)
if ok && len(valueSpec.Values) > 0 {
return true
}

Check warning on line 82 in gnovm/pkg/coverage/analyze.go

View check run for this annotation

Codecov / codecov/patch

gnovm/pkg/coverage/analyze.go#L73-L82

Added lines #L73 - L82 were not covered by tests
}
}
return false

Check warning on line 85 in gnovm/pkg/coverage/analyze.go

View check run for this annotation

Codecov / codecov/patch

gnovm/pkg/coverage/analyze.go#L85

Added line #L85 was not covered by tests
case *ast.ImportSpec, *ast.TypeSpec, *ast.ValueSpec:
return false
case *ast.InterfaceType:
return false

Check warning on line 89 in gnovm/pkg/coverage/analyze.go

View check run for this annotation

Codecov / codecov/patch

gnovm/pkg/coverage/analyze.go#L88-L89

Added lines #L88 - L89 were not covered by tests
case *ast.GenDecl:
switch n.Tok {
case token.VAR, token.CONST:
for _, spec := range n.Specs {
valueSpec, ok := spec.(*ast.ValueSpec)
if ok && len(valueSpec.Values) > 0 {
return true
}

Check warning on line 97 in gnovm/pkg/coverage/analyze.go

View check run for this annotation

Codecov / codecov/patch

gnovm/pkg/coverage/analyze.go#L96-L97

Added lines #L96 - L97 were not covered by tests
}
return false
case token.TYPE, token.IMPORT:
return false
default:
return true

Check warning on line 103 in gnovm/pkg/coverage/analyze.go

View check run for this annotation

Codecov / codecov/patch

gnovm/pkg/coverage/analyze.go#L102-L103

Added lines #L102 - L103 were not covered by tests
}
default:
return false
}
}
90 changes: 90 additions & 0 deletions gnovm/pkg/coverage/analyze_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
package coverage

import (
"testing"

"github.com/stretchr/testify/assert"
)

func TestDetectExecutableLines(t *testing.T) {
t.Parallel()
tests := []struct {
name string
content string
want map[int]bool
wantErr bool
}{
{
name: "Simple function",
content: `
package main
func main() {
x := 5
if x > 3 {
println("Greater")
}
}`,
want: map[int]bool{
5: true, // x := 5
6: true, // if x > 3
7: true, // println("Greater")
},
wantErr: false,
},
{
name: "Function with loop",
content: `
package main
func loopFunction() {
for i := 0; i < 5; i++ {
if i%2 == 0 {
continue
}
println(i)
}
}`,
want: map[int]bool{
5: true, // for i := 0; i < 5; i++
6: true, // if i%2 == 0
7: true, // continue
9: true, // println(i)
},
wantErr: false,
},
{
name: "Only declarations",
content: `
package main
import "fmt"
var x int
type MyStruct struct {
field int
}`,
want: map[int]bool{},
wantErr: false,
},
{
name: "Invalid gno code",
content: `
This is not valid Go code
It should result in an error`,
want: nil,
wantErr: true,
},
}

for _, tt := range tests {
tt := tt
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
got, err := DetectExecutableLines(tt.content)
assert.Equal(t, tt.wantErr, err != nil)
assert.Equal(t, tt.want, got)
})
}
}
Loading

0 comments on commit 88128a4

Please sign in to comment.