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
10 changes: 10 additions & 0 deletions docs/testing.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,16 @@ There are two main areas:
- **Tool tests** (`tool/`). Cover the compile-time instrumentation pipeline: AST rewriting, import resolution, trampoline generation, package loading, and setup logic. Golden-file tests in `tool/internal/instrument/` snapshot expected output and can be updated with `make test-unit/update-golden`.
- **Package tests** (`pkg/`). Cover the runtime instrumentation hooks and semantic convention helpers. Each hook package has tests that verify span creation, context propagation, error recording, and the enable/disable mechanism via `OTEL_GO_ENABLED_INSTRUMENTATIONS` / `OTEL_GO_DISABLED_INSTRUMENTATIONS`.

### Golden-test helper packages

A golden testcase directory under `tool/internal/instrument/testdata/golden/<name>/` may contain a `helpers/` subdirectory with one or more Go packages. The test harness automatically discovers each subdirectory, compiles it into a `.a` archive, and registers it in the `importcfg` so the instrumented source can import it at compile time.

Use this convention when a testcase exercises call rules that reference wrapper functions from an external (non-stdlib) package via the `imports:` field. To add a new helper:

1. Create `helpers/<pkgname>/<pkgname>.go` with the wrapper code (package name must match the directory name).
2. Reference the full import path in `rules.yml` under `imports:`, the path is `<root-module>/tool/internal/instrument/testdata/golden/<testname>/helpers/<pkgname>`.
3. Create a placeholder for the golden file and run `make test-unit/update-golden` to regenerate the `.golden` snapshot.

## Integration Tests

> [!IMPORTANT]
Expand Down
61 changes: 56 additions & 5 deletions tool/internal/instrument/instrument_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
package instrument

import (
"bytes"
"context"
"encoding/json"
"fmt"
Expand All @@ -34,6 +35,12 @@ import (
"gotest.tools/v3/golden"
)

// helperPkg holds a compiled helper package for use in golden tests.
type helperPkg struct {
importPath string
archive string
}

const (
testdataDir = "testdata"
goldenDir = "golden"
Expand Down Expand Up @@ -78,7 +85,10 @@ func runTest(t *testing.T, testName string) {
ruleSet := loadRulesYAML(t, testName, sourceFile)
writeMatchedJSON(ruleSet)

args := compileArgs(tempDir, sourceFile)
testcaseDir := filepath.Join(testdataDir, goldenDir, testName)
helpers := buildTestcaseHelpers(ctx, t, testcaseDir)

args := compileArgs(tempDir, sourceFile, helpers)
err := Toolexec(ctx, args)

if testName == invalidReceiver {
Expand Down Expand Up @@ -157,12 +167,12 @@ func writeMatchedJSON(ruleSet *rule.InstRuleSet) {
util.WriteFile(matchedFile, string(matchedJSON))
}

func compileArgs(tempDir, sourceFile string) []string {
func compileArgs(tempDir, sourceFile string, helpers []helperPkg) []string {
output, _ := exec.Command("go", "env", "GOTOOLDIR").Output()

// Create importcfg file for the test
importCfgPath := filepath.Join(tempDir, "importcfg")
createImportCfg(importCfgPath)
createImportCfg(importCfgPath, helpers)

return []string{
filepath.Join(strings.TrimSpace(string(output)), "compile"),
Expand All @@ -176,8 +186,9 @@ func compileArgs(tempDir, sourceFile string) []string {
}
}

// createImportCfg creates a basic importcfg file with standard library packages.
func createImportCfg(path string) {
// createImportCfg creates an importcfg file with standard library packages
// and any additional helper packages built for the testcase.
func createImportCfg(path string, helpers []helperPkg) {
// Get standard library package locations
// We'll use go list to populate common packages
ctx := context.Background()
Expand Down Expand Up @@ -207,6 +218,11 @@ func createImportCfg(path string) {
}
}

// Register testcase-local helper packages
for _, h := range helpers {
cfg.PackageFile[h.importPath] = h.archive
}

// Write the importcfg file
f, err := os.Create(path)
if err != nil {
Expand All @@ -219,6 +235,41 @@ func createImportCfg(path string) {
}
}

// buildTestcaseHelpers discovers Go helper packages under <testcaseDir>/helpers/,
// compiles each one via "go list -export -json" and returns the resulting
// (importPath, archivePath) pairs so they can be added to the importcfg.
func buildTestcaseHelpers(ctx context.Context, t *testing.T, testcaseDir string) []helperPkg {
helpersDir := filepath.Join(testcaseDir, "helpers")
entries, readErr := os.ReadDir(helpersDir)
if os.IsNotExist(readErr) {
return nil
}
require.NoError(t, readErr, "reading helpers dir %s", helpersDir)

var out []helperPkg
for _, e := range entries {
if !e.IsDir() {
continue
}
pkgPath := "./" + filepath.ToSlash(filepath.Join(helpersDir, e.Name()))

var stderr bytes.Buffer
cmd := exec.CommandContext(ctx, "go", "list", "-export", "-json", pkgPath)
cmd.Stderr = &stderr
listOut, listErr := cmd.Output()
require.NoError(t, listErr, "go list -export -json %s: %s", pkgPath, stderr.String())

var info struct {
ImportPath string `json:"ImportPath"`
Export string `json:"Export"`
}
require.NoError(t, json.Unmarshal(listOut, &info))

out = append(out, helperPkg{importPath: info.ImportPath, archive: info.Export})
}
return out
}

func verifyGoldenFiles(t *testing.T, tempDir, testName string) {
entries, _ := os.ReadDir(filepath.Join(testdataDir, goldenDir, testName))
for _, entry := range entries {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
// Copyright The OpenTelemetry Authors
// SPDX-License-Identifier: Apache-2.0

package main

import (
"unsafe"

"github.com/open-telemetry/opentelemetry-go-compile-instrumentation/tool/internal/instrument/testdata/golden/all-rule-external-wrapper/helpers/wrapper"
)

type T struct{ ExternalTag wrapper.Tag }

var GlobalVar interface{} = wrapper.WrapValue("original")

func RawFunc() {
wrapper.Log("raw instrumented")
println("raw func body")
}

//otelc:external-log
func DirectiveFunc() {
wrapper.Log("DirectiveFunc")
println("directive func body")
}

func CallSizeof() {
x := 42
size := wrapper.Wrapper(unsafe.Sizeof(x))
_ = size
}

func main() {
y := "hello"
_ = wrapper.Wrapper(unsafe.Sizeof(y))
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
// Copyright The OpenTelemetry Authors
// SPDX-License-Identifier: Apache-2.0

package wrapper

// Tag is a named type used to verify that struct rules can inject external types as fields.
type Tag struct{ Value string }

// Wrapper wraps a uintptr value, used by call rules.
func Wrapper(size uintptr) uintptr {
return size
}

// Log is a no-op helper used by raw and directive rules to verify external import injection.
func Log(msg string) {}

// WrapValue wraps an interface{} value, used by decl rules.
func WrapValue(v interface{}) interface{} {
return v
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
wrap_sizeof_external:
target: main
function_call: unsafe.Sizeof
replace: "wrapper.Wrapper({{ . }})"
imports:
wrapper: "github.com/open-telemetry/opentelemetry-go-compile-instrumentation/tool/internal/instrument/testdata/golden/all-rule-external-wrapper/helpers/wrapper"

inject_raw_external:
target: main
func: RawFunc
raw: 'wrapper.Log("raw instrumented")'
imports:
wrapper: "github.com/open-telemetry/opentelemetry-go-compile-instrumentation/tool/internal/instrument/testdata/golden/all-rule-external-wrapper/helpers/wrapper"

add_external_field:
target: main
struct: T
new_field:
- name: ExternalTag
type: wrapper.Tag
imports:
wrapper: "github.com/open-telemetry/opentelemetry-go-compile-instrumentation/tool/internal/instrument/testdata/golden/all-rule-external-wrapper/helpers/wrapper"

wrap_global_var_external:
target: main
kind: var
identifier: GlobalVar
wrap: "wrapper.WrapValue({{ . }})"
imports:
wrapper: "github.com/open-telemetry/opentelemetry-go-compile-instrumentation/tool/internal/instrument/testdata/golden/all-rule-external-wrapper/helpers/wrapper"

log_directive_external:
target: main
directive: "otelc:external-log"
template: 'wrapper.Log("{{FuncName}}")'
imports:
wrapper: "github.com/open-telemetry/opentelemetry-go-compile-instrumentation/tool/internal/instrument/testdata/golden/all-rule-external-wrapper/helpers/wrapper"
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
// Copyright The OpenTelemetry Authors
// SPDX-License-Identifier: Apache-2.0

package main

import "unsafe"

type T struct{}

var GlobalVar interface{} = "original"

func RawFunc() {
println("raw func body")
}

//otelc:external-log
func DirectiveFunc() {
println("directive func body")
}

func CallSizeof() {
x := 42
size := unsafe.Sizeof(x)
_ = size
}

func main() {
y := "hello"
_ = unsafe.Sizeof(y)
}
Loading