Skip to content
Open
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
15 changes: 14 additions & 1 deletion .github/instructions/testing.instructions.md
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,8 @@ to verify specific call expectations.

## Lock Files in Tests

Use `env.WriteLock(t, name, lock)` to create lock files on the test filesystem:
For unit tests using `testutils.NewTestEnv(t)`, use `env.WriteLock(t, name, lock)`
to create lock files on the in-memory test filesystem:

```go
lock := lockfile.New()
Expand All @@ -122,6 +123,18 @@ lock.ManualBump = 1
env.WriteLock(t, "curl", lock)
```

For scenario project fixtures, use `projecttest.AddLock(...)` when the lock
should be serialized with the dynamic project, or `projecttest.WriteLock(...)`
when a test updates lock files between git commits:

```go
projecttest.NewDynamicTestProject(
projecttest.AddLock("curl", projecttest.WithLockInputFingerprint("sha256:v1")),
)

projecttest.WriteLock(t, projectDir, "curl", projecttest.WithLockInputFingerprint("sha256:v2"))
```

## Mocking External Commands

`CmdFactory.RunHandler` and `RunAndGetOutputHandler` intercept ALL external
Expand Down
1 change: 1 addition & 0 deletions scenario/component_build_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ func TestBuildingLocalComponent(t *testing.T) {
spec := projecttest.NewSpec(projecttest.WithBuildArch(projecttest.NoArch))
project := projecttest.NewDynamicTestProject(
projecttest.AddSpec(spec),
projecttest.AddLock(spec.GetName(), projecttest.WithLockInputFingerprint("sha256:"+spec.GetName()+"-v1")),
projecttest.UseTestDefaultConfigs(),
projecttest.WithGitRepo(),
)
Expand Down
60 changes: 24 additions & 36 deletions scenario/component_changed_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ package scenario_tests

import (
"encoding/json"
"fmt"
"os"
"os/exec"
"path/filepath"
Expand Down Expand Up @@ -88,6 +87,13 @@ func writeFileInDir(t *testing.T, dir, relPath, content string) {
require.NoError(t, os.WriteFile(absPath, []byte(content), fileperms.PublicFile))
}

// writeLockInDir writes a simple fingerprint-only lock file for scenario tests.
func writeLockInDir(t *testing.T, dir, componentName, inputFingerprint string) {
t.Helper()

projecttest.WriteLock(t, dir, componentName, projecttest.WithLockInputFingerprint(inputFingerprint))
}

// TestComponentChanged_E2E exercises the full `azldev component changed` command
// with a real git repository, verifying JSON output for multi-component change
// detection.
Expand Down Expand Up @@ -157,20 +163,16 @@ func TestComponentChanged_E2E(t *testing.T) {
gitInDir(t, projectDir, "config", "user.email", "test@test.com")
gitInDir(t, projectDir, "config", "user.name", "Test")

lockV1Curl := fmt.Sprintf("version = 1\ninput-fingerprint = %q\n", "sha256:curl-v1")
lockV1Bash := fmt.Sprintf("version = 1\ninput-fingerprint = %q\n", "sha256:bash-v1")

writeFileInDir(t, projectDir, "locks/curl.lock", lockV1Curl)
writeFileInDir(t, projectDir, "locks/bash.lock", lockV1Bash)
writeLockInDir(t, projectDir, "curl", "sha256:curl-v1")
writeLockInDir(t, projectDir, "bash", "sha256:bash-v1")

gitInDir(t, projectDir, "add", ".")
gitInDir(t, projectDir, "-c", "commit.gpgsign=false", "commit", "-m", "initial")

fromRef := gitInDir(t, projectDir, "rev-parse", "HEAD")

// Second commit: change curl's lock, leave bash unchanged.
lockV2Curl := fmt.Sprintf("version = 1\ninput-fingerprint = %q\n", "sha256:curl-v2")
writeFileInDir(t, projectDir, "locks/curl.lock", lockV2Curl)
writeLockInDir(t, projectDir, "curl", "sha256:curl-v2")

gitInDir(t, projectDir, "add", "locks/curl.lock")
gitInDir(t, projectDir, "-c", "commit.gpgsign=false", "commit", "-m", "update curl")
Expand Down Expand Up @@ -243,8 +245,7 @@ func TestComponentChanged_SameRef(t *testing.T) {
gitInDir(t, projectDir, "config", "user.email", "test@test.com")
gitInDir(t, projectDir, "config", "user.name", "Test")

lockContent := fmt.Sprintf("version = 1\ninput-fingerprint = %q\n", "sha256:v1")
writeFileInDir(t, projectDir, "locks/curl.lock", lockContent)
writeLockInDir(t, projectDir, "curl", "sha256:v1")

gitInDir(t, projectDir, "add", ".")
gitInDir(t, projectDir, "-c", "commit.gpgsign=false", "commit", "-m", "initial")
Expand Down Expand Up @@ -387,26 +388,23 @@ func TestComponentChanged_SourcesChange(t *testing.T) {
)

// Commit 1: initial lock + rendered sources.
writeFileInDir(t, projectDir, "locks/curl.lock",
fmt.Sprintf("version = 1\ninput-fingerprint = %q\n", "sha256:v1"))
writeLockInDir(t, projectDir, "curl", "sha256:v1")
writeFileInDir(t, projectDir, "specs/c/curl/sources",
"SHA512 (curl-8.0.tar.gz) = aaa111")
gitInDir(t, projectDir, "add", ".")
gitInDir(t, projectDir, "-c", "commit.gpgsign=false", "commit", "-m", "initial")
ref1 := gitInDir(t, projectDir, "rev-parse", "HEAD")

// Commit 2: change lock fingerprint AND sources (new tarball).
writeFileInDir(t, projectDir, "locks/curl.lock",
fmt.Sprintf("version = 1\ninput-fingerprint = %q\n", "sha256:v2"))
writeLockInDir(t, projectDir, "curl", "sha256:v2")
writeFileInDir(t, projectDir, "specs/c/curl/sources",
"SHA512 (curl-8.1.tar.gz) = bbb222")
gitInDir(t, projectDir, "add", ".")
gitInDir(t, projectDir, "-c", "commit.gpgsign=false", "commit", "-m", "update sources")
ref2 := gitInDir(t, projectDir, "rev-parse", "HEAD")

// Commit 3: change lock fingerprint only (config tweak, same tarball).
writeFileInDir(t, projectDir, "locks/curl.lock",
fmt.Sprintf("version = 1\ninput-fingerprint = %q\n", "sha256:v3"))
writeLockInDir(t, projectDir, "curl", "sha256:v3")
gitInDir(t, projectDir, "add", ".")
gitInDir(t, projectDir, "-c", "commit.gpgsign=false", "commit", "-m", "config change")

Expand Down Expand Up @@ -460,15 +458,13 @@ func TestComponentChanged_InvertedRefs(t *testing.T) {
)

// Commit 1: v1 lock.
writeFileInDir(t, projectDir, "locks/curl.lock",
fmt.Sprintf("version = 1\ninput-fingerprint = %q\n", "sha256:old"))
writeLockInDir(t, projectDir, "curl", "sha256:old")
gitInDir(t, projectDir, "add", ".")
gitInDir(t, projectDir, "-c", "commit.gpgsign=false", "commit", "-m", "v1")
oldRef := gitInDir(t, projectDir, "rev-parse", "HEAD")

// Commit 2: v2 lock.
writeFileInDir(t, projectDir, "locks/curl.lock",
fmt.Sprintf("version = 1\ninput-fingerprint = %q\n", "sha256:new"))
writeLockInDir(t, projectDir, "curl", "sha256:new")
gitInDir(t, projectDir, "add", ".")
gitInDir(t, projectDir, "-c", "commit.gpgsign=false", "commit", "-m", "v2")
newRef := gitInDir(t, projectDir, "rev-parse", "HEAD")
Expand Down Expand Up @@ -526,8 +522,7 @@ func TestComponentChanged_NewComponent(t *testing.T) {
fromRef := gitInDir(t, projectDir, "rev-parse", "HEAD")

// Commit 2: add curl lock.
writeFileInDir(t, projectDir, "locks/curl.lock",
fmt.Sprintf("version = 1\ninput-fingerprint = %q\n", "sha256:first"))
writeLockInDir(t, projectDir, "curl", "sha256:first")
gitInDir(t, projectDir, "add", ".")
gitInDir(t, projectDir, "-c", "commit.gpgsign=false", "commit", "-m", "add curl")

Expand Down Expand Up @@ -571,10 +566,8 @@ func TestComponentChanged_DeletedComponent(t *testing.T) {
)

// Commit 1: lock files for curl (in config) and oldpkg (NOT in config).
writeFileInDir(t, projectDir, "locks/curl.lock",
fmt.Sprintf("version = 1\ninput-fingerprint = %q\n", "sha256:curl-v1"))
writeFileInDir(t, projectDir, "locks/oldpkg.lock",
fmt.Sprintf("version = 1\ninput-fingerprint = %q\n", "sha256:oldpkg-v1"))
writeLockInDir(t, projectDir, "curl", "sha256:curl-v1")
writeLockInDir(t, projectDir, "oldpkg", "sha256:oldpkg-v1")
gitInDir(t, projectDir, "add", ".")
gitInDir(t, projectDir, "-c", "commit.gpgsign=false", "commit", "-m", "initial")
fromRef := gitInDir(t, projectDir, "rev-parse", "HEAD")
Expand Down Expand Up @@ -643,21 +636,17 @@ func TestComponentChanged_JSONContract(t *testing.T) {
)

// Commit 1: curl has lock + sources, bash has lock, oldpkg has lock (not in config).
writeFileInDir(t, projectDir, "locks/curl.lock",
fmt.Sprintf("version = 1\ninput-fingerprint = %q\n", "sha256:curl-v1"))
writeFileInDir(t, projectDir, "locks/bash.lock",
fmt.Sprintf("version = 1\ninput-fingerprint = %q\n", "sha256:bash-v1"))
writeFileInDir(t, projectDir, "locks/oldpkg.lock",
fmt.Sprintf("version = 1\ninput-fingerprint = %q\n", "sha256:old-v1"))
writeLockInDir(t, projectDir, "curl", "sha256:curl-v1")
writeLockInDir(t, projectDir, "bash", "sha256:bash-v1")
writeLockInDir(t, projectDir, "oldpkg", "sha256:old-v1")
writeFileInDir(t, projectDir, "specs/c/curl/sources",
"SHA512 (curl-1.0.tar.gz) = aaa")
gitInDir(t, projectDir, "add", ".")
gitInDir(t, projectDir, "-c", "commit.gpgsign=false", "commit", "-m", "initial")
fromRef := gitInDir(t, projectDir, "rev-parse", "HEAD")

// Commit 2: curl fingerprint + sources changed, bash unchanged, oldpkg removed.
writeFileInDir(t, projectDir, "locks/curl.lock",
fmt.Sprintf("version = 1\ninput-fingerprint = %q\n", "sha256:curl-v2"))
writeLockInDir(t, projectDir, "curl", "sha256:curl-v2")
writeFileInDir(t, projectDir, "specs/c/curl/sources",
"SHA512 (curl-2.0.tar.gz) = bbb")
gitInDir(t, projectDir, "rm", "locks/oldpkg.lock")
Expand Down Expand Up @@ -762,8 +751,7 @@ func TestComponentChanged_IntegrityViolation(t *testing.T) {
)

// Commit 1: lock + matching rendered sources.
writeFileInDir(t, projectDir, "locks/curl.lock",
fmt.Sprintf("version = 1\ninput-fingerprint = %q\n", "sha256:v1"))
writeLockInDir(t, projectDir, "curl", "sha256:v1")
writeFileInDir(t, projectDir, "specs/c/curl/sources",
"SHA512 (curl-8.0.tar.gz) = aaa111")
gitInDir(t, projectDir, "add", ".")
Expand Down
1 change: 1 addition & 0 deletions scenario/component_query_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ func TestQueryingAComponent(t *testing.T) {
// Create a simple project with the spec, using test default configs for distro and mock configurations.
project := projecttest.NewDynamicTestProject(
projecttest.AddSpec(spec),
projecttest.AddLock(spec.GetName(), projecttest.WithLockInputFingerprint("sha256:"+spec.GetName()+"-v1")),
projecttest.UseTestDefaultConfigs(),
)

Expand Down
24 changes: 16 additions & 8 deletions scenario/component_render_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,10 @@ func localComponentConfig(name string, overlays ...projectconfig.ComponentOverla
}
}

func localComponentLock(name string) projecttest.DynamicTestProjectOption {
return projecttest.AddLock(name, projecttest.WithLockInputFingerprint("sha256:"+name+"-v1"))
}

func TestRenderSimpleLocalSpec(t *testing.T) {
t.Parallel()

Expand All @@ -49,6 +53,7 @@ func TestRenderSimpleLocalSpec(t *testing.T) {
project := projecttest.NewDynamicTestProject(
projecttest.AddSpec(spec),
projecttest.AddComponent(localComponentConfig("test-render")),
localComponentLock("test-render"),
projecttest.UseTestDefaultConfigs(),
projecttest.WithGitRepo(),
)
Expand Down Expand Up @@ -99,6 +104,7 @@ func TestRenderWithConfiguredOutputDir(t *testing.T) {
project := projecttest.NewDynamicTestProject(
projecttest.AddSpec(spec),
projecttest.AddComponent(localComponentConfig("config-test")),
localComponentLock("config-test"),
projecttest.UseTestDefaultConfigs(),
projecttest.WithGitRepo(),
// Set rendered-specs-dir in project config instead of using -o.
Expand Down Expand Up @@ -149,6 +155,7 @@ func TestRenderWithOverlayApplied(t *testing.T) {
Value: "test-overlay-dep",
},
)),
localComponentLock("test-overlay"),
projecttest.UseTestDefaultConfigs(),
projecttest.WithGitRepo(),
)
Expand Down Expand Up @@ -205,6 +212,7 @@ func TestRenderWithPatchSidecar(t *testing.T) {
Source: "patches/fix-stuff.patch",
},
)),
localComponentLock("test-patch"),
projecttest.AddFile("patches/fix-stuff.patch", patchContent),
projecttest.UseTestDefaultConfigs(),
projecttest.WithGitRepo(),
Expand Down Expand Up @@ -251,6 +259,7 @@ func TestRenderStaleCleanup(t *testing.T) {
project := projecttest.NewDynamicTestProject(
projecttest.AddSpec(spec),
projecttest.AddComponent(localComponentConfig("keep-me")),
localComponentLock("keep-me"),
projecttest.UseTestDefaultConfigs(),
projecttest.WithGitRepo(),
projecttest.AddFile("SPECS/s/stale-component/RENDER_FAILED", "Rendering failed.\n"),
Expand Down Expand Up @@ -297,6 +306,7 @@ func TestRenderRefusesOverwriteWithoutForce(t *testing.T) {
project := projecttest.NewDynamicTestProject(
projecttest.AddSpec(spec),
projecttest.AddComponent(localComponentConfig("no-clobber")),
localComponentLock("no-clobber"),
projecttest.UseTestDefaultConfigs(),
projecttest.WithGitRepo(),
projecttest.AddFile("SPECS/n/no-clobber/existing-file.txt", "do not delete me\n"),
Expand Down Expand Up @@ -371,6 +381,7 @@ License: MIT

project := projecttest.NewDynamicTestProject(
projecttest.AddComponent(localComponentConfig("golang-example")),
localComponentLock("golang-example"),
// Write the custom spec content directly via AddFile since AddSpec's
// TestSpec renderer doesn't support %gometa.
projecttest.AddFile("specs/golang-example/golang-example.spec", goSpecContent),
Expand Down Expand Up @@ -438,6 +449,8 @@ func TestRenderMultipleComponentsParallel(t *testing.T) {
projecttest.AddSpec(specB),
projecttest.AddComponent(localComponentConfig("comp-alpha")),
projecttest.AddComponent(localComponentConfig("comp-beta")),
localComponentLock("comp-alpha"),
localComponentLock("comp-beta"),
projecttest.UseTestDefaultConfigs(),
projecttest.WithGitRepo(),
)
Expand Down Expand Up @@ -502,9 +515,11 @@ func TestRenderBrokenSpecWithGoodSpec(t *testing.T) {
project := projecttest.NewDynamicTestProject(
projecttest.AddSpec(goodSpec),
projecttest.AddComponent(localComponentConfig("good-pkg")),
localComponentLock("good-pkg"),
// Add a broken spec as a raw file — not valid RPM spec syntax.
projecttest.AddFile("specs/broken-pkg/broken-pkg.spec", "this is not a valid spec file\n"),
projecttest.AddComponent(localComponentConfig("broken-pkg")),
localComponentLock("broken-pkg"),
projecttest.UseTestDefaultConfigs(),
projecttest.WithGitRepo(),
)
Expand Down Expand Up @@ -569,13 +584,6 @@ func TestRenderLocalSpecWithSyntheticHistory(t *testing.T) {
projecttest.WithBuildArch(projecttest.NoArch),
)

// Pre-baked lock file with a stale fingerprint. The overlay in the
// component config changes the runtime fingerprint, so dirty detection
// will fire and add a synthetic commit.
const lockFileContent = `version = 1
input-fingerprint = "pre-baked-for-test"
`

project := projecttest.NewDynamicTestProject(
projecttest.AddSpec(spec),
projecttest.AddComponent(localComponentConfig("synth-local",
Expand All @@ -587,7 +595,7 @@ input-fingerprint = "pre-baked-for-test"
},
)),
projecttest.UseTestDefaultConfigs(),
projecttest.AddFile("locks/synth-local.lock", lockFileContent),
projecttest.AddLock("synth-local", projecttest.WithLockInputFingerprint("pre-baked-for-test")),
projecttest.WithGitRepo(),
)

Expand Down
37 changes: 35 additions & 2 deletions scenario/internal/projecttest/dynamictestproject.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import (
"testing"

"github.com/brunoga/deep"
"github.com/microsoft/azure-linux-dev-tools/internal/lockfile"
"github.com/microsoft/azure-linux-dev-tools/internal/projectconfig"
"github.com/microsoft/azure-linux-dev-tools/internal/projectgen"
"github.com/microsoft/azure-linux-dev-tools/internal/utils/fileperms"
Expand All @@ -29,6 +30,9 @@ type dynamicTestProject struct {
// Maps relative file path to file contents (as bytes).
otherFiles map[string][]byte

// Maps component name to lock file content.
locks map[string]*lockfile.ComponentLock

// initGitRepo causes [Serialize] to initialize a git repo in the project directory
// and commit all files. Required for commands that use synthetic history (e.g., render).
initGitRepo bool
Expand All @@ -41,6 +45,7 @@ func NewDynamicTestProject(options ...DynamicTestProjectOption) *dynamicTestProj
project := &dynamicTestProject{
configFile: projectgen.GenerateBasicConfig("test-project"),
otherFiles: make(map[string][]byte),
locks: make(map[string]*lockfile.ComponentLock),
}

// Make sure we have an empty component map so we can easily add to it later.
Expand Down Expand Up @@ -78,6 +83,11 @@ func (p *dynamicTestProject) Serialize(t *testing.T, projectDir string) {
require.NoError(t, os.WriteFile(destFilePath, fileContent, fileperms.PublicFile))
}

// Write out lock files before git initialization so [WithGitRepo] commits them.
for componentName, componentLock := range p.locks {
writeComponentLock(t, projectDir, componentName, componentLock)
}

// Initialize a git repo if requested.
if p.initGitRepo {
initProjectGitRepo(t, projectDir)
Expand Down Expand Up @@ -123,6 +133,23 @@ func (p *dynamicTestProject) addComponent(componentConfig *projectconfig.Compone
p.configFile.Components[componentConfig.Name] = deep.MustCopy(*componentConfig)
}

func (p *dynamicTestProject) addLock(componentName string, componentLock *lockfile.ComponentLock) {
lockRelPath := lockRelativePath(componentName)
if _, exists := p.otherFiles[lockRelPath]; exists {
panic(fmt.Sprintf("AddLock: lock file for component %#q already exists via AddFile", componentName))
}

cp := deep.MustCopy(*componentLock)
p.locks[componentName] = &cp
}

// AddLock adds (or updates) a lock file for the component in the project.
func AddLock(componentName string, options ...LockOption) DynamicTestProjectOption {
return func(p *dynamicTestProject) {
p.addLock(componentName, NewLock(options...))
}
}

// AddFile adds an arbitrary file to the project at the specified relative path.
// The path must be relative and must not escape the project directory.
func AddFile(relativePath, content string) DynamicTestProjectOption {
Expand All @@ -132,8 +159,14 @@ func AddFile(relativePath, content string) DynamicTestProjectOption {
panic(fmt.Sprintf("AddFile: path %#q is invalid or escapes the project directory", relativePath))
}

return func(p *dynamicTestProject) {
p.otherFiles[cleaned] = []byte(content)
return func(project *dynamicTestProject) {
for componentName := range project.locks {
if cleaned == lockRelativePath(componentName) {
panic(fmt.Sprintf("AddFile: path %#q already exists via AddLock", relativePath))
}
}

project.otherFiles[cleaned] = []byte(content)
}
}

Expand Down
Loading
Loading