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
54 changes: 54 additions & 0 deletions tool/internal/setup/sync.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,14 @@ package setup

import (
"context"
"fmt"
goversion "go/version"
"os"
"path/filepath"
"strings"

"golang.org/x/mod/modfile"
"golang.org/x/mod/semver"

"github.com/open-telemetry/opentelemetry-go-compile-instrumentation/tool/ex"
"github.com/open-telemetry/opentelemetry-go-compile-instrumentation/tool/internal/rule"
Expand Down Expand Up @@ -70,6 +73,50 @@ func addReplace(modfile *modfile.File, replace *replaceDirective) (bool, error)
return false, nil
}

// versionSnapshot records go directive and direct dep versions before tidy.
type versionSnapshot struct {
goVersion string
deps map[string]string
}

func snapshotVersion(mf *modfile.File) versionSnapshot {
snap := versionSnapshot{
deps: make(map[string]string),
}
if mf.Go != nil {
snap.goVersion = mf.Go.Version
}
for _, req := range mf.Require {
if !req.Indirect {
snap.deps[req.Mod.Path] = req.Mod.Version
}
}
return snap
}

func (sp *SetupPhase) warnVersion(goModPath string, before versionSnapshot) error {
after, err := parseGoMod(goModPath)
if err != nil {
return ex.Wrapf(err, "unable to check for version bumps after go mod tidy")
}

// Go directives use Go toolchain syntax ("1.21"), not module semver.
if after.Go != nil && before.goVersion != "" {
if goversion.Compare("go"+after.Go.Version, "go"+before.goVersion) > 0 {
sp.Warn(fmt.Sprintf("Bumped go version (%s -> %s)", before.goVersion, after.Go.Version))
Comment thread
gyanranjanpanda marked this conversation as resolved.
}
}

for _, req := range after.Require {
if oldVer, tracked := before.deps[req.Mod.Path]; tracked {
if semver.Compare(req.Mod.Version, oldVer) > 0 {
sp.Warn(fmt.Sprintf("Bumped dependency %s (%s -> %s)", req.Mod.Path, oldVer, req.Mod.Version))
Comment thread
gyanranjanpanda marked this conversation as resolved.
}
}
}
return nil
}

func (sp *SetupPhase) syncDeps(ctx context.Context, matched []*rule.InstRuleSet, moduleDir string) error {
rules := make([]*rule.InstFuncRule, 0, len(matched))
for _, m := range matched {
Expand All @@ -89,6 +136,8 @@ func (sp *SetupPhase) syncDeps(ctx context.Context, matched []*rule.InstRuleSet,
if err != nil {
return err
}

before := snapshotVersion(modfile)
replaces := make([]*replaceDirective, 0)
for _, m := range rules {
util.Assert(strings.HasPrefix(m.Path, util.OtelcRoot), "sanity check")
Expand Down Expand Up @@ -148,6 +197,11 @@ func (sp *SetupPhase) syncDeps(ctx context.Context, matched []*rule.InstRuleSet,
if err != nil {
return ex.Wrapf(err, "running go mod tidy in %s", moduleDir)
}
// Compare after tidy because MVS may raise existing consumer versions.
err = sp.warnVersion(goModFile, before)
if err != nil {
return err
}
sp.keepForDebug(goModFile)
}
return nil
Expand Down
178 changes: 178 additions & 0 deletions tool/internal/setup/sync_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
package setup

import (
"bytes"
"log/slog"
"os"
"path/filepath"
Expand Down Expand Up @@ -225,3 +226,180 @@ go 1.21
// At minimum, the pkg replace should be added
assert.Contains(t, string(content), "replace")
}

func warnCapture() (*SetupPhase, *bytes.Buffer) {
var buf bytes.Buffer
handler := slog.NewTextHandler(&buf, &slog.HandlerOptions{Level: slog.LevelWarn})
return &SetupPhase{logger: slog.New(handler)}, &buf
}

func TestSnapshotVersion(t *testing.T) {
content := `module example.com/app

go 1.22.0

require (
go.opentelemetry.io/otel v1.38.0
github.com/example/lib v0.9.0
)

require (
github.com/indirect/dep v0.5.0 // indirect
)
`
mf, err := modfile.Parse("go.mod", []byte(content), nil)
require.NoError(t, err)

snap := snapshotVersion(mf)

assert.Equal(t, "1.22.0", snap.goVersion)
assert.Equal(t, "v1.38.0", snap.deps["go.opentelemetry.io/otel"])
assert.Equal(t, "v0.9.0", snap.deps["github.com/example/lib"])

// indirect deps must not leak into the snapshot
_, tracked := snap.deps["github.com/indirect/dep"]
assert.False(t, tracked)
}

func TestSnapshotVersion_MinimalGoMod(t *testing.T) {
content := `module example.com/tiny

go 1.21
`
mf, err := modfile.Parse("go.mod", []byte(content), nil)
require.NoError(t, err)

snap := snapshotVersion(mf)
assert.Equal(t, "1.21", snap.goVersion)
assert.Empty(t, snap.deps)
}

func TestWarnVersion_GoVersionRaised(t *testing.T) {
tests := []struct {
name string
goVersion string
}{
{
name: "patch version",
goVersion: "1.22.0",
},
{
name: "language version",
goVersion: "1.21",
},
}

for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
tempDir := t.TempDir()
gomodPath := filepath.Join(tempDir, "go.mod")
afterContent := `module example.com/app

go 1.25.0

require (
go.opentelemetry.io/otel v1.38.0
)
`
require.NoError(t, os.WriteFile(gomodPath, []byte(afterContent), 0o644))

sp, buf := warnCapture()
before := versionSnapshot{
goVersion: test.goVersion,
deps: map[string]string{
"go.opentelemetry.io/otel": "v1.38.0",
},
}

require.NoError(t, sp.warnVersion(gomodPath, before))

logged := buf.String()
assert.Contains(t, logged, "Bumped go version")
assert.Contains(t, logged, test.goVersion+" -> 1.25.0")
})
}
}
Comment thread
gyanranjanpanda marked this conversation as resolved.

func TestWarnVersion_DepVersionRaised(t *testing.T) {
tempDir := t.TempDir()
gomodPath := filepath.Join(tempDir, "go.mod")
afterContent := `module example.com/app

go 1.22.0

require (
go.opentelemetry.io/otel v1.43.0
)
`
require.NoError(t, os.WriteFile(gomodPath, []byte(afterContent), 0o644))

sp, buf := warnCapture()
before := versionSnapshot{
goVersion: "1.22.0",
deps: map[string]string{
"go.opentelemetry.io/otel": "v1.38.0",
},
}

require.NoError(t, sp.warnVersion(gomodPath, before))

logged := buf.String()
assert.Contains(t, logged, "Bumped dependency go.opentelemetry.io/otel")
assert.Contains(t, logged, "v1.38.0 -> v1.43.0")
}

func TestWarnVersion_NoChange(t *testing.T) {
tempDir := t.TempDir()
gomodPath := filepath.Join(tempDir, "go.mod")
content := `module example.com/app

go 1.22.0

require (
go.opentelemetry.io/otel v1.38.0
)
`
require.NoError(t, os.WriteFile(gomodPath, []byte(content), 0o644))

sp, buf := warnCapture()
before := versionSnapshot{
goVersion: "1.22.0",
deps: map[string]string{
"go.opentelemetry.io/otel": "v1.38.0",
},
}

require.NoError(t, sp.warnVersion(gomodPath, before))

assert.Empty(t, buf.String())
}

func TestWarnVersion_MissingFile(t *testing.T) {
sp, _ := warnCapture()
before := versionSnapshot{goVersion: "1.22.0", deps: map[string]string{}}

err := sp.warnVersion("/nonexistent/go.mod", before)

require.Error(t, err)
assert.Contains(t, err.Error(), "unable to check for version bumps")
}

func TestWarnVersion_EmptyGoVersion(t *testing.T) {
tempDir := t.TempDir()
gomodPath := filepath.Join(tempDir, "go.mod")
afterContent := `module example.com/app

go 1.25.0
`
require.NoError(t, os.WriteFile(gomodPath, []byte(afterContent), 0o644))

sp, buf := warnCapture()
before := versionSnapshot{
goVersion: "",
deps: map[string]string{},
}

require.NoError(t, sp.warnVersion(gomodPath, before))

assert.Empty(t, buf.String())
}
Loading