diff --git a/internal/civisibility/constants/test_tags.go b/internal/civisibility/constants/test_tags.go index 443f635c8a..ccd4fa4635 100644 --- a/internal/civisibility/constants/test_tags.go +++ b/internal/civisibility/constants/test_tags.go @@ -46,6 +46,10 @@ const ( // This constant is used to tag traces with the line number in the source file where the test starts. TestSourceStartLine = "test.source.start" + // TestSourceEndLine indicates the line of the source file where the test ends. + // This constant is used to tag traces with the line number in the source file where the test ends. + TestSourceEndLine = "test.source.end" + // TestCodeOwners indicates the test code owners. // This constant is used to tag traces with the code owners responsible for the test. TestCodeOwners = "test.codeowners" diff --git a/internal/civisibility/integrations/manual_api_ddtest.go b/internal/civisibility/integrations/manual_api_ddtest.go index cdb01f4d4b..834929fa3d 100644 --- a/internal/civisibility/integrations/manual_api_ddtest.go +++ b/internal/civisibility/integrations/manual_api_ddtest.go @@ -8,6 +8,9 @@ package integrations import ( "context" "fmt" + "go/ast" + "go/parser" + "go/token" "runtime" "strings" "time" @@ -134,11 +137,58 @@ func (t *tslvTest) SetTestFunc(fn *runtime.Func) { return } - file, line := fn.FileLine(fn.Entry()) - file = utils.GetRelativePathFromCITagsSourceRoot(file) + // let's get the file path and the start line of the function + absolutePath, startLine := fn.FileLine(fn.Entry()) + file := utils.GetRelativePathFromCITagsSourceRoot(absolutePath) t.SetTag(constants.TestSourceFile, file) - t.SetTag(constants.TestSourceStartLine, line) + t.SetTag(constants.TestSourceStartLine, startLine) + + // now, let's try to get the end line of the function using ast + // parse the entire file where the function is defined to create an abstract syntax tree (AST) + // if we can't parse the file (source code is not available) we silently bail out + fset := token.NewFileSet() + fileNode, err := parser.ParseFile(fset, absolutePath, nil, parser.AllErrors) + if err == nil { + // get the function name without the package name + fullName := fn.Name() + firstDot := strings.LastIndex(fullName, ".") + 1 + name := fullName[firstDot:] + + // variable to store the ending line of the function + var endLine int + // traverse the AST to find the function declaration for the target function + ast.Inspect(fileNode, func(n ast.Node) bool { + // check if the current node is a function declaration + if funcDecl, ok := n.(*ast.FuncDecl); ok { + // if the function name matches the target function name + if funcDecl.Name.Name == name { + // get the line number of the end of the function body + endLine = fset.Position(funcDecl.Body.End()).Line + // stop further inspection since we have found the target function + return false + } + } + // check if the current node is a function literal (FuncLit) + if funcLit, ok := n.(*ast.FuncLit); ok { + // get the line number of the start of the function literal + funcStartLine := fset.Position(funcLit.Body.Pos()).Line + // if the start line matches the known start line, record the end line + if funcStartLine == startLine { + endLine = fset.Position(funcLit.Body.End()).Line + return false // stop further inspection since we have found the function + } + } + // continue inspecting other nodes + return true + }) + + // if we found an endLine we check is greater than the calculated startLine + if endLine > startLine { + t.SetTag(constants.TestSourceEndLine, endLine) + } + } + // get the codeowner of the function codeOwners := utils.GetCodeOwners() if codeOwners != nil { match, found := codeOwners.Match("/" + file) diff --git a/internal/civisibility/integrations/manual_api_mocktracer_test.go b/internal/civisibility/integrations/manual_api_mocktracer_test.go index afb6d321e4..ba27d37249 100644 --- a/internal/civisibility/integrations/manual_api_mocktracer_test.go +++ b/internal/civisibility/integrations/manual_api_mocktracer_test.go @@ -248,6 +248,39 @@ func Test(t *testing.T) { test.Close(ResultStatusSkip) } +func TestWithInnerFunc(t *testing.T) { + mockTracer.Reset() + assert := assert.New(t) + + now := time.Now() + session, module, suite, test := createDDTest(now) + defer func() { + session.Close(0) + module.Close() + suite.Close() + }() + test.SetError(errors.New("we keep the last error")) + test.SetErrorInfo("my-type", "my-message", "my-stack") + func() { + pc, _, _, _ := runtime.Caller(0) + test.SetTestFunc(runtime.FuncForPC(pc)) + }() + + assert.NotNil(test.Context()) + assert.Equal("my-test", test.Name()) + assert.Equal(now, test.StartTime()) + assert.Equal(suite, test.Suite()) + + test.Close(ResultStatusPass) + + finishedSpans := mockTracer.FinishedSpans() + assert.Equal(1, len(finishedSpans)) + testAssertions(assert, now, finishedSpans[0]) + + //no-op call + test.Close(ResultStatusSkip) +} + func testAssertions(assert *assert.Assertions, now time.Time, testSpan mocktracer.Span) { assert.Equal(now, testSpan.StartTime()) assert.Equal("my-module-framework.test", testSpan.OperationName()) @@ -272,6 +305,12 @@ func testAssertions(assert *assert.Assertions, now time.Time, testSpan mocktrace assert.Contains(spanTags, constants.TestModuleIDTag) assert.Contains(spanTags, constants.TestSuiteIDTag) assert.Contains(spanTags, constants.TestSourceFile) + + // make sure we have both start and end line assert.Contains(spanTags, constants.TestSourceStartLine) + assert.Contains(spanTags, constants.TestSourceEndLine) + // make sure the startLine < endLine + assert.Less(spanTags[constants.TestSourceStartLine].(int), spanTags[constants.TestSourceEndLine].(int)) + commonAssertions(assert, testSpan) }