blob: ee0eb0ec99a7b7012f945774febfb6832efa5d87 [file]
// Copyright The OpenTelemetry Authors
// SPDX-License-Identifier: Apache-2.0
package main
import (
"bytes"
"go/ast"
"go/parser"
"go/token"
"os"
"path/filepath"
"strings"
"testing"
)
func TestNewMigrationIgnoresUnexportedDeclarations(t *testing.T) {
t.Parallel()
root := t.TempDir()
goMod := filepath.Join(root, "go.mod")
if err := os.WriteFile(
goMod,
[]byte("module example.com/semconvtest\n\ngo 1.25.0\n"),
0o600,
); err != nil {
t.Fatalf("WriteFile(%q) error = %v", goMod, err)
}
prev := filepath.Join(root, "v1.40.0")
cur := filepath.Join(root, "v1.41.0")
writeGoFile(t, prev, "semconv", `package semconv
func ErrorType(err error) {}
func DeploymentEnvironmentName(val string) {}
`)
writeGoFile(t, cur, "semconv", `package semconv
func ErrorType(err error) {}
func errorType(err error) {}
`)
m, err := newMigration(cur, prev)
if err != nil {
t.Fatalf("newMigration() error = %v", err)
}
if len(m.Renames) != 0 {
t.Fatalf("newMigration() renames = %#v, want none", m.Renames)
}
wantRemoval := []string{"DeploymentEnvironmentName"}
if len(m.Removals) != len(wantRemoval) || m.Removals[0] != wantRemoval[0] {
t.Fatalf("newMigration() removals = %#v, want %#v", m.Removals, wantRemoval)
}
}
func TestMigrationPreservesNotes(t *testing.T) {
t.Parallel()
root := t.TempDir()
prev := filepath.Join(root, "v1.43.0")
cur := filepath.Join(root, "v1.44.0")
writeGoFile(t, prev, "semconv", "package semconv\n")
writeGoFile(t, cur, "semconv", "package semconv\n")
const notes = "## Metric API changes\n\nUse deltas with Add.\n"
path := filepath.Join(cur, "MIGRATION_NOTES.md.inc")
if err := os.WriteFile(path, []byte(notes), 0o600); err != nil {
t.Fatal(err)
}
for range 2 {
m, err := newMigration(cur, prev)
if err != nil {
t.Fatal(err)
}
if err := render("templates/MIGRATION.md.tmpl", cur, m); err != nil {
t.Fatal(err)
}
data, err := os.ReadFile(filepath.Join(cur, "MIGRATION.md"))
if err != nil {
t.Fatal(err)
}
if strings.Count(string(data), notes) != 1 {
t.Fatalf("migration does not preserve notes exactly once:\n%s", data)
}
}
}
// TestNoInvalidObservableHistogramTypes asserts that the semconv code
// generator never emits types containing "HistogramObservable" or
// "ObservableObservable", which would indicate the histogram-exclusion
// rule in metric.go.j2 was accidentally removed or bypassed.
func TestNoInvalidObservableHistogramTypes(t *testing.T) {
t.Parallel()
semconvDir := filepath.Join("..", "..", "..", "semconv")
forbidden := [][]byte{
[]byte("HistogramObservable"),
[]byte("ObservableObservable"),
}
err := filepath.WalkDir(semconvDir, func(path string, d os.DirEntry, err error) error {
if err != nil {
return err
}
if d.IsDir() ||
filepath.Base(path) != "metric.go" ||
!strings.HasSuffix(filepath.Base(filepath.Dir(path)), "conv") {
return nil
}
data, err := os.ReadFile(path)
if err != nil {
return err
}
for _, term := range forbidden {
if bytes.Contains(data, term) {
t.Errorf("%s: contains forbidden substring %q", path, term)
}
}
return nil
})
if err != nil {
t.Fatalf("WalkDir(%q) error = %v", semconvDir, err)
}
}
// TestGeneratedMetricHelpersReturnAfterEmptyAttrs checks every retained helper
// affected by the missing return in semconv v1.32.0 through v1.34.0.
func TestGeneratedMetricHelpersReturnAfterEmptyAttrs(t *testing.T) {
t.Parallel()
packages := map[string][]string{
"containerconv": {"Uptime"},
"goconv": {"ScheduleDuration"},
"k8sconv": {"NodeCPUUsage", "NodeMemoryUsage", "NodeUptime", "PodCPUUsage", "PodMemoryUsage", "PodUptime"},
"processconv": {"Uptime"},
"rpcconv": {"ClientDuration", "ClientRequestSize", "ClientRequestsPerRPC", "ClientResponseSize", "ClientResponsesPerRPC", "ServerDuration", "ServerRequestSize", "ServerRequestsPerRPC", "ServerResponseSize", "ServerResponsesPerRPC"},
"systemconv": {"Uptime"},
}
versions := []string{"v1.32.0", "v1.33.0", "v1.34.0"}
fset := token.NewFileSet()
for _, version := range versions {
for pkg, functions := range packages {
path := filepath.Join("..", "..", "..", "semconv", version, pkg, "metric.go")
file, err := parser.ParseFile(fset, path, nil, 0)
if err != nil {
t.Fatalf("ParseFile(%q) error = %v", path, err)
}
for _, function := range functions {
matches := 0
ast.Inspect(file, func(node ast.Node) bool {
fn, ok := node.(*ast.FuncDecl)
if !ok || fn.Recv == nil || fn.Name.Name != "Record" {
return true
}
if len(fn.Recv.List) != 1 || len(fn.Recv.List[0].Names) != 1 || fn.Recv.List[0].Names[0].Name != "m" {
return true
}
receiver, ok := fn.Recv.List[0].Type.(*ast.Ident)
if !ok || receiver.Name != function {
return true
}
fastPaths := 0
ast.Inspect(fn.Body, func(node ast.Node) bool {
ifStmt, ok := node.(*ast.IfStmt)
if !ok {
return true
}
condition, ok := ifStmt.Cond.(*ast.BinaryExpr)
if !ok || condition.Op != token.EQL {
return true
}
call, ok := condition.X.(*ast.CallExpr)
zero, zeroOK := condition.Y.(*ast.BasicLit)
if !ok || !zeroOK || zero.Kind != token.INT || zero.Value != "0" || len(call.Args) != 1 {
return true
}
fn, fnOK := call.Fun.(*ast.Ident)
arg, argOK := call.Args[0].(*ast.Ident)
if !fnOK || !argOK || fn.Name != "len" || arg.Name != "attrs" {
return true
}
if len(ifStmt.Body.List) > 0 {
if _, ok := ifStmt.Body.List[len(ifStmt.Body.List)-1].(*ast.ReturnStmt); ok {
fastPaths++
}
}
return true
})
if fastPaths == 1 {
matches++
}
return false
})
if matches != 1 {
t.Errorf("%s: %s.Record empty-attrs fast paths = %d, want 1", path, function, matches)
}
}
}
}
}
func writeGoFile(t *testing.T, dir, _, src string) {
t.Helper()
if err := os.MkdirAll(dir, 0o755); err != nil {
t.Fatalf("MkdirAll(%q) error = %v", dir, err)
}
modPath := filepath.Join(dir, "go.mod")
if err := os.WriteFile(modPath, []byte("module example.com/semconvtest\n\ngo 1.25.0\n"), 0o644); err != nil {
t.Fatalf("WriteFile(%q) error = %v", modPath, err)
}
path := filepath.Join(dir, "decls.go")
if err := os.WriteFile(path, []byte(src), 0o644); err != nil {
t.Fatalf("WriteFile(%q) error = %v", path, err)
}
}