diff --git a/docs/testing.md b/docs/testing.md index 3b4cc74f..6563273b 100644 --- a/docs/testing.md +++ b/docs/testing.md @@ -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//` 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//.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 `/tool/internal/instrument/testdata/golden//helpers/`. +3. Create a placeholder for the golden file and run `make test-unit/update-golden` to regenerate the `.golden` snapshot. + ## Integration Tests > [!IMPORTANT] diff --git a/tool/internal/instrument/instrument_test.go b/tool/internal/instrument/instrument_test.go index d057da1f..e392b08e 100644 --- a/tool/internal/instrument/instrument_test.go +++ b/tool/internal/instrument/instrument_test.go @@ -15,6 +15,7 @@ package instrument import ( + "bytes" "context" "encoding/json" "fmt" @@ -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" @@ -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 { @@ -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"), @@ -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() @@ -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 { @@ -219,6 +235,41 @@ func createImportCfg(path string) { } } +// buildTestcaseHelpers discovers Go helper packages under /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 { diff --git a/tool/internal/instrument/testdata/golden/all-rule-external-wrapper/all-rule-external-wrapper.main.go.golden b/tool/internal/instrument/testdata/golden/all-rule-external-wrapper/all-rule-external-wrapper.main.go.golden new file mode 100644 index 00000000..db5ac31c --- /dev/null +++ b/tool/internal/instrument/testdata/golden/all-rule-external-wrapper/all-rule-external-wrapper.main.go.golden @@ -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)) +} diff --git a/tool/internal/instrument/testdata/golden/all-rule-external-wrapper/helpers/wrapper/wrapper.go b/tool/internal/instrument/testdata/golden/all-rule-external-wrapper/helpers/wrapper/wrapper.go new file mode 100644 index 00000000..f97c4870 --- /dev/null +++ b/tool/internal/instrument/testdata/golden/all-rule-external-wrapper/helpers/wrapper/wrapper.go @@ -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 +} diff --git a/tool/internal/instrument/testdata/golden/all-rule-external-wrapper/rules.yml b/tool/internal/instrument/testdata/golden/all-rule-external-wrapper/rules.yml new file mode 100644 index 00000000..f84a8e32 --- /dev/null +++ b/tool/internal/instrument/testdata/golden/all-rule-external-wrapper/rules.yml @@ -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" diff --git a/tool/internal/instrument/testdata/golden/all-rule-external-wrapper/source.go b/tool/internal/instrument/testdata/golden/all-rule-external-wrapper/source.go new file mode 100644 index 00000000..537e0315 --- /dev/null +++ b/tool/internal/instrument/testdata/golden/all-rule-external-wrapper/source.go @@ -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) +}