Skip to content

Commit

Permalink
Code coverage support (#154)
Browse files Browse the repository at this point in the history
This PR adds code coverage support to the goawk command
with the -cover* options. See details in cover.md.
  • Loading branch information
xonixx committed Oct 21, 2022
1 parent f2c16f0 commit 7bcfd23
Show file tree
Hide file tree
Showing 30 changed files with 1,113 additions and 50 deletions.
59 changes: 59 additions & 0 deletions cover.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
# Code coverage for GoAWK

GoAWK implements code coverage functionality, similar to the one built in the Golang itself (https://go.dev/blog/cover).

![screenshot](cover.png)

## How to use

To generate coverage report:
```
goawk -f a.awk -coverprofile c.out -covermode set
```
This generates `c.out` file with coverage profile data for the `a.awk` program execution.

Now it's time to visualize the coverage report `c.out` by rendering it to HTML.

Please note. The code coverage functionality of GoAWK relies on Golang toolset.
So to generate the HTML coverage report (like on the screenshot above) use the command below. This opens a web browser displaying annotated source code:
```
go tool cover -html=c.out
```

Write out an HTML file instead of launching a web browser:
```
go tool cover -html=c.out -o coverage.html
```

Please note. At the moment it's not possible to generate coverage report for AWK functions, similar to what is possible for Go:
```
go tool cover -func=c.out # this will not work :(
```
This won't work because the `go tool cover` functionality for `-func` is only capable of processing the Go code. Luckily, the `-html` report option is generic enough to process any source code!

If you don't provide the `coverprofile` argument for `goawk` but you do provide `-covermode`, it just prints to stdout the coverage-annotated source code (this might be useful for debug purposes):
```
goawk -f a.awk -covermode set
```


## CLI options for coverage

- `-coverprofile <file>`
- sets the cover report filename to put collected coverage profile data.
If this option is omitted, but the `-covermode` is set - outputs the annotated awk source to stdout.
- `-covermode <mode>`
- `mode` can be one of:
- `set` - did each statement run?
- `count` - how many times did each statement run? (Produces heat maps report).
- `-coverappend`
- if this option is provided the profile data will append to existing, otherwise the profile file will be first truncated.

## Future work

- Add a way to support per-function coverage report similar to `go tool cover -func=c.out`
- More complete handling for coverage cases for `if/else`. That is not only we want to check if `if` body was visited but also we want to make sure the case where `if` body was *not* visited is also covered. In other words we want to make sure that both variants of `if` condition is covered.

## Feedback

Please [open an issue](https://github.com/benhoyt/goawk/issues) if you have bug reports or feature requests for GoAWK's code coverage support.
Binary file added cover.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
102 changes: 88 additions & 14 deletions goawk.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,10 @@ import (
"strings"
"unicode/utf8"

"github.com/benhoyt/goawk/internal/compiler"
"github.com/benhoyt/goawk/internal/cover"
"github.com/benhoyt/goawk/internal/parseutil"
"github.com/benhoyt/goawk/internal/resolver"
"github.com/benhoyt/goawk/interp"
"github.com/benhoyt/goawk/lexer"
"github.com/benhoyt/goawk/parser"
Expand All @@ -63,11 +66,14 @@ Additional GoAWK features:
-version show GoAWK version and exit
GoAWK debugging arguments:
-cpuprofile file write CPU profile to file
-coverappend append to coverage profile instead of overwriting
-covermode mode set coverage mode: set, count (default "set")
-coverprofile fn write coverage profile to file
-cpuprofile fn write CPU profile to file
-d print parsed syntax tree to stdout and exit
-da print VM assembly instructions to stdout and exit
-dt print variable type information to stdout and exit
-memprofile file write memory profile to file
-memprofile fn write memory profile to file
`
)

Expand All @@ -78,15 +84,18 @@ func main() {
var progFiles []string
var vars []string
fieldSep := " "
cpuprofile := ""
cpuProfile := ""
debug := false
debugAsm := false
debugTypes := false
memprofile := ""
memProfile := ""
inputMode := ""
outputMode := ""
header := false
noArgVars := false
coverMode := cover.ModeUnspecified
coverProfile := ""
coverAppend := false

var i int
argsLoop:
Expand All @@ -102,6 +111,20 @@ argsLoop:
}

switch arg {
case "-covermode":
if i+1 >= len(os.Args) {
errorExitf("flag needs an argument: -covermode")
}
i++
coverMode = coverModeFromString(os.Args[i])
case "-coverprofile":
if i+1 >= len(os.Args) {
errorExitf("flag needs an argument: -coverprofile")
}
i++
coverProfile = os.Args[i]
case "-coverappend":
coverAppend = true
case "-E":
if i+1 >= len(os.Args) {
errorExitf("flag needs an argument: -E")
Expand Down Expand Up @@ -134,7 +157,7 @@ argsLoop:
errorExitf("flag needs an argument: -cpuprofile")
}
i++
cpuprofile = os.Args[i]
cpuProfile = os.Args[i]
case "-d":
debug = true
case "-da":
Expand All @@ -157,7 +180,7 @@ argsLoop:
errorExitf("flag needs an argument: -memprofile")
}
i++
memprofile = os.Args[i]
memProfile = os.Args[i]
case "-o":
if i+1 >= len(os.Args) {
errorExitf("flag needs an argument: -o")
Expand Down Expand Up @@ -185,14 +208,21 @@ argsLoop:
case strings.HasPrefix(arg, "-v"):
vars = append(vars, arg[2:])
case strings.HasPrefix(arg, "-cpuprofile="):
cpuprofile = arg[12:]
cpuProfile = arg[len("-cpuprofile="):]
case strings.HasPrefix(arg, "-memprofile="):
memprofile = arg[12:]
memProfile = arg[len("-memprofile="):]
case strings.HasPrefix(arg, "-covermode="):
coverMode = coverModeFromString(arg[len("-covermode="):])
case strings.HasPrefix(arg, "-coverprofile="):
coverProfile = arg[len("-coverprofile="):]
default:
errorExitf("flag provided but not defined: %s", arg)
}
}
}
if coverProfile != "" && coverMode == cover.ModeUnspecified {
coverMode = cover.ModeSet
}

// Any remaining args are program and input files
args := os.Args[i:]
Expand Down Expand Up @@ -248,6 +278,29 @@ argsLoop:
errorExitf("%s", err)
}

coverage := cover.New(coverMode, coverAppend, fileReader)

if coverMode != cover.ModeUnspecified {
astProgram := &prog.ResolvedProgram.Program
coverage.Annotate(astProgram)

// re-resolve annotated program
prog.ResolvedProgram = *resolver.Resolve(astProgram, &resolver.Config{
DebugTypes: parserConfig.DebugTypes,
DebugWriter: parserConfig.DebugWriter})

// re-compile it
prog.Compiled, err = compiler.Compile(&prog.ResolvedProgram)
if err != nil {
errorExitf("%s", err)
}

if coverProfile == "" {
fmt.Fprintln(os.Stdout, prog)
os.Exit(0)
}
}

if debug {
fmt.Fprintln(os.Stdout, prog)
}
Expand Down Expand Up @@ -303,8 +356,8 @@ argsLoop:
config.Vars = append(config.Vars, name, value)
}

if cpuprofile != "" {
f, err := os.Create(cpuprofile)
if cpuProfile != "" {
f, err := os.Create(cpuProfile)
if err != nil {
errorExitf("could not create CPU profile: %v", err)
}
Expand All @@ -314,16 +367,25 @@ argsLoop:
}

// Run the program!
status, err := interp.ExecProgram(prog, config)
interpreter, err := interp.New(prog)
status, err := interpreter.Execute(config)

if err != nil {
errorExit(err)
}

if cpuprofile != "" {
if coverProfile != "" {
err := coverage.WriteProfile(coverProfile, interpreter.Array(cover.ArrayName))
if err != nil {
errorExitf("unable to write coverage profile: %v", err)
}
}

if cpuProfile != "" {
pprof.StopCPUProfile()
}
if memprofile != "" {
f, err := os.Create(memprofile)
if memProfile != "" {
f, err := os.Create(memProfile)
if err != nil {
errorExitf("could not create memory profile: %v", err)
}
Expand All @@ -337,6 +399,18 @@ argsLoop:
os.Exit(status)
}

func coverModeFromString(mode string) cover.Mode {
switch mode {
case "set":
return cover.ModeSet
case "count":
return cover.ModeCount
default:
errorExitf("-covermode can only be one of: set, count")
return cover.ModeUnspecified
}
}

// Show source line and position of error, for example:
//
// BEGIN { x*; }
Expand Down
Loading

0 comments on commit 7bcfd23

Please sign in to comment.