Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add feature to collect function/statement/branch coverage to test command #343

Open
wants to merge 4 commits into
base: main
Choose a base branch
from

Conversation

nodaguti
Copy link
Contributor

@nodaguti nodaguti commented Jul 28, 2024

This PR implements a new feature to collect test coverage data while running tests with -c / --coverage option enabled and report them in the output (either in human-friendly way or in JSON).

Approach

There are roughly two ways to implement coverage measurement: 1) collecting coverage in the interpreter side (like V8 JavaScript code coverage), and 2) collecting coverage in the userland side (like Istanbul test coverage)

For option 1, adding support for code coverage to our interpreter may result in tight coupling between the interpreter and coverage measurement, which I think would not a great idea especially when considering a fact that falco uses the interpreter not only in tester but also in simulator and console features.

For option 2, since we already have a special function to mutate an existing table in tester mode, testing.table_set, tables can be used to store coverage data.

This PR adds a new process called "instrumentation" to the preparation phase of testing: traversing an AST tree to find subroutines, statements, and branches to initialise coverage tables, and injecting testing.table_set statements to record executions of each subroutine, statement, or branch.

These injected statements will be evaluated along with original VCL code and produce coverage information for each test case.

After running all tests, each coverage information will be merged so that falco can calculate overall coverage percentages.

Screenshot

Screenshot 2024-07-21 at 4 08 02 PM

Related Issues

@nodaguti
Copy link
Contributor Author

@ysugimoto
I'm sorry for the PR being relatively large, but I look forward to hearing your impressions. Thank you!

Copy link
Owner

@ysugimoto ysugimoto left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This PR seems to be conceptual and looks very cool approach.

I put some request changes for adjusting current implementation, and this will be needed more working so could you change the base branch to the develop?
I've just created develop branch from current main branch, let's do more working on it.

@@ -62,6 +62,7 @@ type SimulatorConfig struct {
type TestConfig struct {
Timeout int `cli:"t,timeout" yaml:"timeout"`
Filter string `cli:"f,filter" default:"*.test.vcl"`
Coverage bool `cli:"c,coverage" yaml:"coverage" default:"false"`
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry for the our circumstance, could the coverage option be a --converage - only long option?
This is because we'd like to use -c short option for the other usage like config path definition.

@@ -65,6 +65,7 @@ All configurations of configuration files and CLI arguments are described follow
| simulator.edge_dictionary.[name] | Object | - | - | Local edge dictionary name |
| testing | Object | null | - | Testing configuration object |
| testing.timeout | Integer | 10 | -t, --timeout | Set timeout to stop testing |
| testing.coverage | Boolean | false | -c, --coverage | Collect test coverage information and report it in the output |
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry for the our circumstance, could the coverage option be a --converage - only long option?
This is because we'd like to use -c short option for the other usage like config path definition.

Comment on lines +16 to +32
func getFunctionId(s ast.SubroutineDeclaration) string {
return s.GetMeta().Token.File + "_" + s.Name.Value
}

func getStatementId(stmt ast.Statement) string {
t := stmt.GetMeta().Token
l := strconv.Itoa(t.Line)
p := strconv.Itoa(t.Position)
return t.File + "_stmt_l" + l + "_p" + p
}

func getExpressionId(exp ast.Expression) string {
t := exp.GetMeta().Token
l := strconv.Itoa(t.Line)
p := strconv.Itoa(t.Position)
return t.File + "_exp_l" + l + "_p" + p
}
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The parser assigns unique id for each node so the id would be useful for recognizing function, statement, and expression. What do you think about it?
ref: https://github.com/ysugimoto/falco/blob/main/ast/ast.go#L79-L87

Comment on lines +35 to +52
i.ctx.Tables[FUNCTION_COVERAGE] = &ast.TableDeclaration{
Meta: &ast.Meta{Token: token.Null},
Name: &ast.Ident{Value: FUNCTION_COVERAGE},
ValueType: &ast.Ident{Value: "STRING"},
Properties: []*ast.TableProperty{},
}
i.ctx.Tables[STATEMENT_COVERAGE] = &ast.TableDeclaration{
Meta: &ast.Meta{Token: token.Null},
Name: &ast.Ident{Value: STATEMENT_COVERAGE},
ValueType: &ast.Ident{Value: "STRING"},
Properties: []*ast.TableProperty{},
}
i.ctx.Tables[BRANCH_COVERAGE] = &ast.TableDeclaration{
Meta: &ast.Meta{Token: token.Null},
Name: &ast.Ident{Value: BRANCH_COVERAGE},
ValueType: &ast.Ident{Value: "STRING"},
Properties: []*ast.TableProperty{},
}
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i.ctx.Tables holds the actual table declarations that are defined in a VCL so rarely it may conflict hence we should avoid it.
We could declare another property that dedicated to hold the converage state in the context.Context struct here instead. And, the data type could be arbitrary so you don't need to define as a table declaration. I think it's good enough to declare like:

type CoverageInfo struct {
  Functions map[string]ast.Node
  Statements map[string]ast.Node
  Branches map[string]ast.Node
}

The map value should be ast.Node interface because it will be useful for generating converage report against the actual VCL.

Comment on lines +71 to +73
[]ast.Statement{
createMarkAsCovered(FUNCTION_COVERAGE, id),
},
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This seems like an instanbul approach, interesting and very nice!

Comment on lines +401 to +429
type Coverage struct {
Function CoverageTable
Statement CoverageTable
Branch CoverageTable
}

type CoverageTable map[string]bool

func (i *Interpreter) GetCoverage() Coverage {
if i.ctx.Tables[FUNCTION_COVERAGE] == nil || i.ctx.Tables[STATEMENT_COVERAGE] == nil || i.ctx.Tables[BRANCH_COVERAGE] == nil {
return Coverage{}
}

return Coverage{
Function: convertToCoverageTable(i.ctx.Tables[FUNCTION_COVERAGE]),
Statement: convertToCoverageTable(i.ctx.Tables[STATEMENT_COVERAGE]),
Branch: convertToCoverageTable(i.ctx.Tables[BRANCH_COVERAGE]),
}
}

func convertToCoverageTable(decl *ast.TableDeclaration) CoverageTable {
table := make(CoverageTable)

for _, prop := range decl.Properties {
table[prop.Key.Value] = prop.Value.(*ast.String).Value == "true"
}

return table
}
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Probably you can declare this types in the interpreter's context package.

Meta: &ast.Meta{Token: token.Null},
Function: &ast.Ident{
Meta: &ast.Meta{
Token: token.Token{Type: token.STRING, Literal: "testing.table_set"},
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I suggest that you can create new testing function for marking converage like coverage.mark() but it's okay for this PR. we can do it later!

Comment on lines +230 to +259
instrumented: `
sub func1 {
testing.table_set(falco_coverage_function, "main_func1", "true");
testing.table_set(falco_coverage_statement, "main_stmt_l3_p3", "true");
if (req.http.Foo == "1") {
testing.table_set(falco_coverage_branch, "main_exp_l3_p13_true", "true");
} else {
testing.table_set(falco_coverage_branch, "main_exp_l3_p13_false", "true");
}
error 600 if(req.http.Foo == "1", "1", "2");
testing.table_set(falco_coverage_statement, "main_stmt_l4_p3", "true");
if (req.http.Foo == "1") {
testing.table_set(falco_coverage_branch, "main_exp_l4_p26_true", "true");
} else {
testing.table_set(falco_coverage_branch, "main_exp_l4_p26_false", "true");
}
header.set(req, "bar", if(req.http.Foo == "1", "1", "2"));
testing.table_set(falco_coverage_statement, "main_stmt_l5_p3", "true");
if (req.http.Foo == "1") {
testing.table_set(falco_coverage_branch, "main_exp_l5_p22_true", "true");
} else {
testing.table_set(falco_coverage_branch, "main_exp_l5_p22_false", "true");
if (req.http.Foo == "2") {
testing.table_set(falco_coverage_branch, "main_exp_l5_p60_true", "true");
} else {
testing.table_set(falco_coverage_branch, "main_exp_l5_p60_false", "true");
}
}
set req.http.Bar = if(req.http.Foo == "1", "1", "bar_" + if(req.http.Foo == "2", "2", "3"));
}
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks tricky a bit but very interesting, awesome!

icontext "github.com/ysugimoto/falco/interpreter/context"
"github.com/ysugimoto/falco/interpreter/value"
)

const testBackendResponseBody = "falco_test_response"

func (i *Interpreter) TestProcessInit(r *http.Request) error {
func (i *Interpreter) TestProcessInit(r *http.Request, c *config.TestConfig) error {
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Passing a bool argument like enableCoverage bool seems to be enough.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants