Synchronize delivery test fixtures and record the concurrency audit

This commit is contained in:
2026-10-11 00:07:25 +00:00
parent 6176971307
commit 059a79525a
17 changed files with 615 additions and 328 deletions

View File

@@ -37,7 +37,7 @@ func TestBuildAndPrebuiltDeliveryUseSameDirectoryBase(t *testing.T) {
} }
} }
writeRepositoryFile(t, repository, ".gitignore", "dist/\n") writeRepositoryFile(t, repository, ".gitignore", "dist/\n")
state := &deliveryServerState{tag: tag} state := &deliveryServerState{data: deliveryServerData{tag: tag}}
server := newDeliveryServer(t, state) server := newDeliveryServer(t, state)
defer server.Close() defer server.Close()
settings := cli.Settings{Product: "Example Product", ModuleDir: tt.module, settings := cli.Settings{Product: "Example Product", ModuleDir: tt.module,
@@ -47,7 +47,7 @@ func TestBuildAndPrebuiltDeliveryUseSameDirectoryBase(t *testing.T) {
writeDeliveryProfile(t, repository, settings) writeDeliveryProfile(t, repository, settings)
cliGitRun(t, repository, "add", ".") cliGitRun(t, repository, "add", ".")
cliGitRun(t, repository, "commit", "-m", "Prepare module source and output exclusion") cliGitRun(t, repository, "commit", "-m", "Prepare module source and output exclusion")
state.commit = cliGitOutput(t, repository, "rev-parse", "HEAD") state.update(func(state *deliveryServerData) { state.commit = cliGitOutput(t, repository, "rev-parse", "HEAD") })
cliGitRun(t, repository, "tag", tag) cliGitRun(t, repository, "tag", tag)
output, resolved := "dist", filepath.Join(moduleDirectory, "dist") output, resolved := "dist", filepath.Join(moduleDirectory, "dist")
if tt.absolute { if tt.absolute {
@@ -73,12 +73,12 @@ func TestBuildAndPrebuiltDeliveryUseSameDirectoryBase(t *testing.T) {
args := []string{"deliver", "--repo", invocationRepository, "--assets", output, "v1.2.3"} args := []string{"deliver", "--repo", invocationRepository, "--assets", output, "v1.2.3"}
stdout.Reset() stdout.Reset()
stderr.Reset() stderr.Reset()
if status := run(context.Background(), args, &stdout, &stderr); status != 0 || state.releaseCreates != 1 || state.assetUploads != 2 { if status := run(context.Background(), args, &stdout, &stderr); status != 0 || state.snapshot().releaseCreates != 1 || state.snapshot().assetUploads != 2 {
t.Fatalf("delivery status=%d, creates=%d, uploads=%d, stderr=%s", status, state.releaseCreates, state.assetUploads, stderr.String()) t.Fatalf("delivery status=%d, creates=%d, uploads=%d, stderr=%s", status, state.snapshot().releaseCreates, state.snapshot().assetUploads, stderr.String())
} }
if tt.alias { if tt.alias {
if status := run(context.Background(), []string{"deliver", "--repo", repository, "--assets", output, "v1.2.3"}, &stdout, &stderr); status != 0 || state.assetUploads != 2 { if status := run(context.Background(), []string{"deliver", "--repo", repository, "--assets", output, "v1.2.3"}, &stdout, &stderr); status != 0 || state.snapshot().assetUploads != 2 {
t.Fatalf("physical-path retry status=%d, uploads=%d, stderr=%s", status, state.assetUploads, stderr.String()) t.Fatalf("physical-path retry status=%d, uploads=%d, stderr=%s", status, state.snapshot().assetUploads, stderr.String())
} }
} }
if tt.module != "" { if tt.module != "" {
@@ -86,17 +86,17 @@ func TestBuildAndPrebuiltDeliveryUseSameDirectoryBase(t *testing.T) {
wrongModule := settings wrongModule := settings
wrongModule.ModuleDir = "" wrongModule.ModuleDir = ""
invocation := cli.Invocation{Repository: repository, AssetsDir: resolved, Version: "v1.2.3"} invocation := cli.Invocation{Repository: repository, AssetsDir: resolved, Version: "v1.2.3"}
requestsBefore := state.apiRequests requestsBefore := state.snapshot().apiRequests
if status := deliverWithSettings(context.Background(), invocation, wrongModule, &stdout, &stderr); status != 1 || state.apiRequests != requestsBefore { if status := deliverWithSettings(context.Background(), invocation, wrongModule, &stdout, &stderr); status != 1 || state.snapshot().apiRequests != requestsBefore {
t.Fatalf("wrong module status=%d, requests=%d->%d, stderr=%s", status, requestsBefore, state.apiRequests, stderr.String()) t.Fatalf("wrong module status=%d, requests=%d->%d, stderr=%s", status, requestsBefore, state.snapshot().apiRequests, stderr.String())
} }
} }
if err := os.WriteFile(receipt.Artifacts[0].Path, []byte("changed"), 0o600); err != nil { if err := os.WriteFile(receipt.Artifacts[0].Path, []byte("changed"), 0o600); err != nil {
t.Fatal(err) t.Fatal(err)
} }
requestsBefore := state.apiRequests requestsBefore := state.snapshot().apiRequests
if status := run(context.Background(), args, &stdout, &stderr); status != 1 || state.apiRequests != requestsBefore { if status := run(context.Background(), args, &stdout, &stderr); status != 1 || state.snapshot().apiRequests != requestsBefore {
t.Fatalf("changed content status=%d, requests=%d->%d, stderr=%s", status, requestsBefore, state.apiRequests, stderr.String()) t.Fatalf("changed content status=%d, requests=%d->%d, stderr=%s", status, requestsBefore, state.snapshot().apiRequests, stderr.String())
} }
}) })
} }

View File

@@ -36,15 +36,15 @@ func TestPageOnlyDeliveryUsesCommittedModuleInSparseCheckout(t *testing.T) {
writeRepositoryFile(t, repository, module+"/docs/releases/v1.2.3.md", "# Example Product v1.2.3\n\nNested release note.\n") writeRepositoryFile(t, repository, module+"/docs/releases/v1.2.3.md", "# Example Product v1.2.3\n\nNested release note.\n")
cliGitRun(t, repository, "add", ".") cliGitRun(t, repository, "add", ".")
cliGitRun(t, repository, "commit", "-m", "Add sparse release module") cliGitRun(t, repository, "commit", "-m", "Add sparse release module")
state := &deliveryServerState{tag: module + "/v1.2.3"} state := &deliveryServerState{data: deliveryServerData{tag: module + "/v1.2.3"}}
server := newDeliveryServer(t, state) server := newDeliveryServer(t, state)
defer server.Close() defer server.Close()
writeDeliveryProfile(t, repository, cli.Settings{Product: "Example Product", ModuleDir: module, writeDeliveryProfile(t, repository, cli.Settings{Product: "Example Product", ModuleDir: module,
Delivery: &cli.DeliverySettings{BaseURL: server.URL, Owner: "owner", Repository: "example"}, Delivery: &cli.DeliverySettings{BaseURL: server.URL, Owner: "owner", Repository: "example"},
}) })
state.commit = cliGitOutput(t, repository, "rev-parse", "HEAD") state.update(func(state *deliveryServerData) { state.commit = cliGitOutput(t, repository, "rev-parse", "HEAD") })
cliGitRun(t, repository, "tag", state.tag) cliGitRun(t, repository, "tag", state.snapshot().tag)
cliGitRun(t, repository, "checkout", "--detach", state.tag) cliGitRun(t, repository, "checkout", "--detach", state.snapshot().tag)
cliGitRun(t, repository, "sparse-checkout", "set", "--cone", "docs") cliGitRun(t, repository, "sparse-checkout", "set", "--cone", "docs")
if _, err := os.Stat(filepath.Join(repository, filepath.FromSlash(module))); !errors.Is(err, os.ErrNotExist) { if _, err := os.Stat(filepath.Join(repository, filepath.FromSlash(module))); !errors.Is(err, os.ErrNotExist) {
t.Fatalf("module fixture is materialized: %v", err) t.Fatalf("module fixture is materialized: %v", err)
@@ -61,11 +61,11 @@ func TestPageOnlyDeliveryUsesCommittedModuleInSparseCheckout(t *testing.T) {
t.Setenv("PATH", gitOnly) t.Setenv("PATH", gitOnly)
for _, args := range [][]string{ for _, args := range [][]string{
{"deliver", "--repo", repository, "v1.2.3"}, {"deliver", "--repo", repository, "v1.2.3"},
{"ci-deliver", "--repo", repository, "--prepared", "--expected-module", module, "--commit", state.commit, state.tag}, {"ci-deliver", "--repo", repository, "--prepared", "--expected-module", module, "--commit", state.snapshot().commit, state.snapshot().tag},
} { } {
var stdout, stderr bytes.Buffer var stdout, stderr bytes.Buffer
if status := run(context.Background(), args, &stdout, &stderr); status != 0 || state.releaseCreates != 1 || state.assetUploads != 0 { if status := run(context.Background(), args, &stdout, &stderr); status != 0 || state.snapshot().releaseCreates != 1 || state.snapshot().assetUploads != 0 {
t.Fatalf("%s sparse delivery status=%d creates=%d uploads=%d stderr=%s", args[0], status, state.releaseCreates, state.assetUploads, stderr.String()) t.Fatalf("%s sparse delivery status=%d creates=%d uploads=%d stderr=%s", args[0], status, state.snapshot().releaseCreates, state.snapshot().assetUploads, stderr.String())
} }
} }
} }

View File

@@ -34,11 +34,11 @@ func TestMainModuleWithoutDomainSupportsCheckBuildAndPageDelivery(t *testing.T)
} }
settings.Build = nil settings.Build = nil
writeDeliveryProfile(t, repository, settings) writeDeliveryProfile(t, repository, settings)
state.commit = cliGitOutput(t, repository, "rev-parse", "HEAD") state.update(func(state *deliveryServerData) { state.commit = cliGitOutput(t, repository, "rev-parse", "HEAD") })
cliGitRun(t, repository, "tag", "v1.2.3") cliGitRun(t, repository, "tag", "v1.2.3")
t.Setenv("RELEASEKIT_GITEA_TOKEN", "fixture-token") t.Setenv("RELEASEKIT_GITEA_TOKEN", "fixture-token")
var stdout, stderr bytes.Buffer var stdout, stderr bytes.Buffer
if status := run(context.Background(), []string{"deliver", "--repo", repository, "v1.2.3"}, &stdout, &stderr); status != 0 || state.releaseCreates != 1 || state.assetUploads != 0 { if status := run(context.Background(), []string{"deliver", "--repo", repository, "v1.2.3"}, &stdout, &stderr); status != 0 || state.snapshot().releaseCreates != 1 || state.snapshot().assetUploads != 0 {
t.Fatalf("page delivery status=%d creates=%d uploads=%d stderr=%s", status, state.releaseCreates, state.assetUploads, stderr.String()) t.Fatalf("page delivery status=%d creates=%d uploads=%d stderr=%s", status, state.snapshot().releaseCreates, state.snapshot().assetUploads, stderr.String())
} }
} }

View File

@@ -15,6 +15,7 @@ import (
"path/filepath" "path/filepath"
"runtime" "runtime"
"strings" "strings"
"sync"
"testing" "testing"
releasekit "gitea.maximumdirect.net/eric/ReleaseKit" releasekit "gitea.maximumdirect.net/eric/ReleaseKit"
@@ -641,7 +642,7 @@ func TestDeliverPageOnlyCreatesAndReusesRelease(t *testing.T) {
Delivery: &cli.DeliverySettings{BaseURL: server.URL, Owner: "owner", Repository: "example"}, Delivery: &cli.DeliverySettings{BaseURL: server.URL, Owner: "owner", Repository: "example"},
} }
writeDeliveryProfile(t, repository, settings) writeDeliveryProfile(t, repository, settings)
state.commit = cliGitOutput(t, repository, "rev-parse", "HEAD") state.update(func(state *deliveryServerData) { state.commit = cliGitOutput(t, repository, "rev-parse", "HEAD") })
cliGitRun(t, repository, "tag", "v1.2.3") // Historical lightweight tags are valid delivery inputs. cliGitRun(t, repository, "tag", "v1.2.3") // Historical lightweight tags are valid delivery inputs.
t.Setenv("RELEASEKIT_GITEA_TOKEN", "fixture-token") t.Setenv("RELEASEKIT_GITEA_TOKEN", "fixture-token")
for attempt := 0; attempt < 2; attempt++ { for attempt := 0; attempt < 2; attempt++ {
@@ -654,11 +655,11 @@ func TestDeliverPageOnlyCreatesAndReusesRelease(t *testing.T) {
t.Fatalf("delivery output = %q", stdout.String()) t.Fatalf("delivery output = %q", stdout.String())
} }
} }
if state.releaseCreates != 1 || state.assetUploads != 0 || state.assetListReads != 0 { if state.snapshot().releaseCreates != 1 || state.snapshot().assetUploads != 0 || state.snapshot().assetListReads != 0 {
t.Fatalf("page-only calls = creates:%d asset uploads:%d asset reads:%d", state.releaseCreates, state.assetUploads, state.assetListReads) t.Fatalf("page-only calls = creates:%d asset uploads:%d asset reads:%d", state.snapshot().releaseCreates, state.snapshot().assetUploads, state.snapshot().assetListReads)
} }
if state.noteBody != "# Example Product v1.2.3\n\nRelease note.\n" { if state.snapshot().noteBody != "# Example Product v1.2.3\n\nRelease note.\n" {
t.Fatalf("hosted note body = %q", state.noteBody) t.Fatalf("hosted note body = %q", state.snapshot().noteBody)
} }
} }
@@ -675,7 +676,7 @@ func TestDeliverBuildsTaggedArtifactsAndReusesAssets(t *testing.T) {
Delivery: &cli.DeliverySettings{BaseURL: server.URL, Owner: "owner", Repository: "example"}, Delivery: &cli.DeliverySettings{BaseURL: server.URL, Owner: "owner", Repository: "example"},
} }
writeDeliveryProfile(t, repository, settings) writeDeliveryProfile(t, repository, settings)
state.commit = cliGitOutput(t, repository, "rev-parse", "HEAD") state.update(func(state *deliveryServerData) { state.commit = cliGitOutput(t, repository, "rev-parse", "HEAD") })
cliGitRun(t, repository, "tag", "v1.2.3") cliGitRun(t, repository, "tag", "v1.2.3")
cliGitRun(t, repository, "checkout", "--detach", "v1.2.3") cliGitRun(t, repository, "checkout", "--detach", "v1.2.3")
t.Setenv("RELEASEKIT_GITEA_TOKEN", "fixture-token") t.Setenv("RELEASEKIT_GITEA_TOKEN", "fixture-token")
@@ -689,14 +690,15 @@ func TestDeliverBuildsTaggedArtifactsAndReusesAssets(t *testing.T) {
t.Fatalf("second delivery did not report matching assets: %q", stdout.String()) t.Fatalf("second delivery did not report matching assets: %q", stdout.String())
} }
} }
if state.releaseCreates != 1 || state.assetUploads != 2 { observed := state.snapshot()
t.Fatalf("delivery calls = creates:%d asset uploads:%d", state.releaseCreates, state.assetUploads) if observed.releaseCreates != 1 || observed.assetUploads != 2 {
t.Fatalf("delivery calls = creates:%d asset uploads:%d", observed.releaseCreates, observed.assetUploads)
} }
if _, ok := state.assets["helper-v1.2.3-linux-amd64"]; !ok { if _, ok := observed.assets["helper-v1.2.3-linux-amd64"]; !ok {
t.Fatalf("binary was not uploaded: %#v", state.assets) t.Fatalf("binary was not uploaded: %#v", observed.assets)
} }
if _, ok := state.assets["SHA256SUMS"]; !ok { if _, ok := observed.assets["SHA256SUMS"]; !ok {
t.Fatalf("checksum manifest was not uploaded: %#v", state.assets) t.Fatalf("checksum manifest was not uploaded: %#v", observed.assets)
} }
if entries, err := os.ReadDir(temporaryRoot); err != nil || len(entries) != 0 { if entries, err := os.ReadDir(temporaryRoot); err != nil || len(entries) != 0 {
t.Fatalf("facade-owned build output remains after delivery: entries=%v error=%v", entries, err) t.Fatalf("facade-owned build output remains after delivery: entries=%v error=%v", entries, err)
@@ -717,7 +719,7 @@ func TestCIDeliverLoadsTaggedProfileAndSkipsUnavailablePageOnlyDependencies(t *t
Delivery: &cli.DeliverySettings{BaseURL: server.URL, Owner: "owner", Repository: "example"}, Delivery: &cli.DeliverySettings{BaseURL: server.URL, Owner: "owner", Repository: "example"},
}) })
expectedCommit := pushTaggedCLIRepository(t, repository) expectedCommit := pushTaggedCLIRepository(t, repository)
state.commit = expectedCommit state.update(func(state *deliveryServerData) { state.commit = expectedCommit })
writeDeliveryProfileAt(t, repository, "profiles/ci-delivery.json", cli.Settings{Product: "Later Branch Profile"}) writeDeliveryProfileAt(t, repository, "profiles/ci-delivery.json", cli.Settings{Product: "Later Branch Profile"})
cliGitRun(t, repository, "checkout", "--detach", expectedCommit) cliGitRun(t, repository, "checkout", "--detach", expectedCommit)
cliGitRun(t, repository, "remote", "remove", "origin") cliGitRun(t, repository, "remote", "remove", "origin")
@@ -730,8 +732,8 @@ func TestCIDeliverLoadsTaggedProfileAndSkipsUnavailablePageOnlyDependencies(t *t
if status := run(context.Background(), args, &stdout, &stderr); status != 0 { if status := run(context.Background(), args, &stdout, &stderr); status != 0 {
t.Fatalf("run() status=%d stdout=%q stderr=%q", status, stdout.String(), stderr.String()) t.Fatalf("run() status=%d stdout=%q stderr=%q", status, stdout.String(), stderr.String())
} }
if state.releaseCreates != 1 || state.apiRequests == 0 { if state.snapshot().releaseCreates != 1 || state.snapshot().apiRequests == 0 {
t.Fatalf("page delivery did not reach the server: creates=%d requests=%d", state.releaseCreates, state.apiRequests) t.Fatalf("page delivery did not reach the server: creates=%d requests=%d", state.snapshot().releaseCreates, state.snapshot().apiRequests)
} }
if got := cliGitOutput(t, repository, "rev-parse", "HEAD"); got != expectedCommit { if got := cliGitOutput(t, repository, "rev-parse", "HEAD"); got != expectedCommit {
t.Fatalf("prepared HEAD = %s, want %s", got, expectedCommit) t.Fatalf("prepared HEAD = %s, want %s", got, expectedCommit)
@@ -752,7 +754,7 @@ func TestCIDeliverRejectsWrongCommitBeforeGoOrHostedRequests(t *testing.T) {
Delivery: &cli.DeliverySettings{BaseURL: server.URL, Owner: "owner", Repository: "example"}, Delivery: &cli.DeliverySettings{BaseURL: server.URL, Owner: "owner", Repository: "example"},
}) })
expectedCommit := pushTaggedCLIRepository(t, repository) expectedCommit := pushTaggedCLIRepository(t, repository)
state.commit = expectedCommit state.update(func(state *deliveryServerData) { state.commit = expectedCommit })
shimDir := t.TempDir() shimDir := t.TempDir()
markerPath := filepath.Join(shimDir, "go-invoked") markerPath := filepath.Join(shimDir, "go-invoked")
@@ -775,8 +777,8 @@ func TestCIDeliverRejectsWrongCommitBeforeGoOrHostedRequests(t *testing.T) {
if _, err := os.Stat(markerPath); !os.IsNotExist(err) { if _, err := os.Stat(markerPath); !os.IsNotExist(err) {
t.Fatalf("Go was invoked before commit agreement: %v", err) t.Fatalf("Go was invoked before commit agreement: %v", err)
} }
if state.apiRequests != 0 { if state.snapshot().apiRequests != 0 {
t.Fatalf("hosted API requests = %d, want 0", state.apiRequests) t.Fatalf("hosted API requests = %d, want 0", state.snapshot().apiRequests)
} }
} }
@@ -803,7 +805,7 @@ func TestCIDeliverRejectsExpectedModuleMismatchBeforeDependencyPreparation(t *te
Delivery: &cli.DeliverySettings{BaseURL: server.URL, Owner: "owner", Repository: "example"}, Delivery: &cli.DeliverySettings{BaseURL: server.URL, Owner: "owner", Repository: "example"},
}) })
expectedCommit := pushTaggedCLIRepository(t, repository) expectedCommit := pushTaggedCLIRepository(t, repository)
state.commit = expectedCommit state.update(func(state *deliveryServerData) { state.commit = expectedCommit })
shimDir := t.TempDir() shimDir := t.TempDir()
markerPath := filepath.Join(shimDir, "go-invoked") markerPath := filepath.Join(shimDir, "go-invoked")
goShim := "#!/bin/sh\n: > \"$RELEASEKIT_GO_MARKER\"\nexit 99\n" goShim := "#!/bin/sh\n: > \"$RELEASEKIT_GO_MARKER\"\nexit 99\n"
@@ -824,8 +826,8 @@ func TestCIDeliverRejectsExpectedModuleMismatchBeforeDependencyPreparation(t *te
if _, err := os.Stat(markerPath); !os.IsNotExist(err) { if _, err := os.Stat(markerPath); !os.IsNotExist(err) {
t.Fatalf("Go was invoked before profile agreement: %v", err) t.Fatalf("Go was invoked before profile agreement: %v", err)
} }
if state.apiRequests != 0 { if state.snapshot().apiRequests != 0 {
t.Fatalf("hosted API requests = %d, want 0", state.apiRequests) t.Fatalf("hosted API requests = %d, want 0", state.snapshot().apiRequests)
} }
} }
@@ -840,18 +842,18 @@ func TestCIDeliverPreparesDependenciesAndBuildsConfiguredTargets(t *testing.T) {
Delivery: &cli.DeliverySettings{BaseURL: server.URL, Owner: "owner", Repository: "example"}, Delivery: &cli.DeliverySettings{BaseURL: server.URL, Owner: "owner", Repository: "example"},
}) })
expectedCommit := pushTaggedCLIRepository(t, repository) expectedCommit := pushTaggedCLIRepository(t, repository)
state.commit = expectedCommit state.update(func(state *deliveryServerData) { state.commit = expectedCommit })
t.Setenv("RELEASEKIT_GITEA_TOKEN", "fixture-token") t.Setenv("RELEASEKIT_GITEA_TOKEN", "fixture-token")
var stdout, stderr bytes.Buffer var stdout, stderr bytes.Buffer
args := []string{"ci-deliver", "--repo", repository, "--commit", expectedCommit, "v1.2.3"} args := []string{"ci-deliver", "--repo", repository, "--commit", expectedCommit, "v1.2.3"}
if status := run(context.Background(), args, &stdout, &stderr); status != 0 { if status := run(context.Background(), args, &stdout, &stderr); status != 0 {
t.Fatalf("run() status=%d stdout=%q stderr=%q", status, stdout.String(), stderr.String()) t.Fatalf("run() status=%d stdout=%q stderr=%q", status, stdout.String(), stderr.String())
} }
if state.releaseCreates != 1 || state.assetUploads != 2 { if state.snapshot().releaseCreates != 1 || state.snapshot().assetUploads != 2 {
t.Fatalf("delivery counts = creates:%d uploads:%d", state.releaseCreates, state.assetUploads) t.Fatalf("delivery counts = creates:%d uploads:%d", state.snapshot().releaseCreates, state.snapshot().assetUploads)
} }
if _, ok := state.assets["helper-v1.2.3-"+runtime.GOOS+"-"+runtime.GOARCH]; !ok { if _, ok := state.snapshot().assets["helper-v1.2.3-"+runtime.GOOS+"-"+runtime.GOARCH]; !ok {
t.Fatalf("binary not uploaded: %#v", state.assets) t.Fatalf("binary not uploaded: %#v", state.snapshot().assets)
} }
} }
@@ -866,7 +868,7 @@ func TestCIDeliverValidatesCredentialsBeforePreparingDependencies(t *testing.T)
Delivery: &cli.DeliverySettings{BaseURL: server.URL, Owner: "owner", Repository: "example"}, Delivery: &cli.DeliverySettings{BaseURL: server.URL, Owner: "owner", Repository: "example"},
}) })
expectedCommit := pushTaggedCLIRepository(t, repository) expectedCommit := pushTaggedCLIRepository(t, repository)
state.commit = expectedCommit state.update(func(state *deliveryServerData) { state.commit = expectedCommit })
shimDir := t.TempDir() shimDir := t.TempDir()
markerPath := filepath.Join(shimDir, "go-invoked") markerPath := filepath.Join(shimDir, "go-invoked")
@@ -888,8 +890,8 @@ func TestCIDeliverValidatesCredentialsBeforePreparingDependencies(t *testing.T)
if _, err := os.Stat(markerPath); !os.IsNotExist(err) { if _, err := os.Stat(markerPath); !os.IsNotExist(err) {
t.Fatalf("Go was invoked before credential validation: %v", err) t.Fatalf("Go was invoked before credential validation: %v", err)
} }
if state.apiRequests != 0 { if state.snapshot().apiRequests != 0 {
t.Fatalf("hosted API requests = %d, want 0", state.apiRequests) t.Fatalf("hosted API requests = %d, want 0", state.snapshot().apiRequests)
} }
} }
@@ -904,7 +906,7 @@ func TestDeliverRejectsInvalidTaggedSourceBeforeManagedBuild(t *testing.T) {
Delivery: &cli.DeliverySettings{BaseURL: server.URL, Owner: "owner", Repository: "example"}, Delivery: &cli.DeliverySettings{BaseURL: server.URL, Owner: "owner", Repository: "example"},
} }
writeDeliveryProfile(t, repository, settings) writeDeliveryProfile(t, repository, settings)
state.commit = cliGitOutput(t, repository, "rev-parse", "HEAD") state.update(func(state *deliveryServerData) { state.commit = cliGitOutput(t, repository, "rev-parse", "HEAD") })
cliGitRun(t, repository, "tag", "v1.2.3") cliGitRun(t, repository, "tag", "v1.2.3")
if err := os.WriteFile(filepath.Join(repository, "uncommitted.txt"), []byte("dirty"), 0600); err != nil { if err := os.WriteFile(filepath.Join(repository, "uncommitted.txt"), []byte("dirty"), 0600); err != nil {
t.Fatal(err) t.Fatal(err)
@@ -912,8 +914,8 @@ func TestDeliverRejectsInvalidTaggedSourceBeforeManagedBuild(t *testing.T) {
t.Setenv("RELEASEKIT_GITEA_TOKEN", "fixture-token") t.Setenv("RELEASEKIT_GITEA_TOKEN", "fixture-token")
var stdout, stderr bytes.Buffer var stdout, stderr bytes.Buffer
status := run(context.Background(), []string{"deliver", "--repo", repository, "v1.2.3"}, &stdout, &stderr) status := run(context.Background(), []string{"deliver", "--repo", repository, "v1.2.3"}, &stdout, &stderr)
if status != 1 || state.apiRequests != 0 || strings.Contains(stderr.String(), "building ") { if status != 1 || state.snapshot().apiRequests != 0 || strings.Contains(stderr.String(), "building ") {
t.Fatalf("invalid-source delivery status=%d requests=%d stdout=%q stderr=%q", status, state.apiRequests, stdout.String(), stderr.String()) t.Fatalf("invalid-source delivery status=%d requests=%d stdout=%q stderr=%q", status, state.snapshot().apiRequests, stdout.String(), stderr.String())
} }
} }
@@ -958,16 +960,18 @@ func TestDeliverFailureReportsRecoveryForPartialConflictAndUnknownStates(t *test
Build: &build, Build: &build,
Delivery: &cli.DeliverySettings{BaseURL: server.URL, Owner: "owner", Repository: "example"}, Delivery: &cli.DeliverySettings{BaseURL: server.URL, Owner: "owner", Repository: "example"},
}) })
state.commit = cliGitOutput(t, repository, "rev-parse", "HEAD") state.update(func(state *deliveryServerData) { state.commit = cliGitOutput(t, repository, "rev-parse", "HEAD") })
cliGitRun(t, repository, "tag", "v1.2.3") cliGitRun(t, repository, "tag", "v1.2.3")
assetsDir, _ := writeDeliveryReceipt(t, repository, build) assetsDir, _ := writeDeliveryReceipt(t, repository, build)
switch test.name { switch test.name {
case "partial": case "partial":
state.failAssetName = "helper-v1.2.3-linux-amd64" state.update(func(state *deliveryServerData) { state.failAssetName = "helper-v1.2.3-linux-amd64" })
case "conflict": case "conflict":
state.assets["helper-v1.2.3-linux-amd64"] = []byte("different hosted bytes") state.update(func(state *deliveryServerData) {
state.assets["helper-v1.2.3-linux-amd64"] = []byte("different hosted bytes")
})
case "unknown": case "unknown":
state.ambiguousAssetName = "helper-v1.2.3-linux-amd64" state.update(func(state *deliveryServerData) { state.ambiguousAssetName = "helper-v1.2.3-linux-amd64" })
} }
t.Setenv("RELEASEKIT_GITEA_TOKEN", "fixture-token") t.Setenv("RELEASEKIT_GITEA_TOKEN", "fixture-token")
var stdout, stderr bytes.Buffer var stdout, stderr bytes.Buffer
@@ -995,17 +999,17 @@ func TestDeliverFailureReportsRecoveryForPartialConflictAndUnknownStates(t *test
func TestDeliverRejectsWrongHostedTagBeforeReleaseWrite(t *testing.T) { func TestDeliverRejectsWrongHostedTagBeforeReleaseWrite(t *testing.T) {
repository := newCLIRepository(t) repository := newCLIRepository(t)
state := &deliveryServerState{remoteCommit: strings.Repeat("0", 40)} state := &deliveryServerState{data: deliveryServerData{remoteCommit: strings.Repeat("0", 40)}}
server := newDeliveryServer(t, state) server := newDeliveryServer(t, state)
defer server.Close() defer server.Close()
writeDeliveryProfile(t, repository, cli.Settings{Product: "Example Product", Delivery: &cli.DeliverySettings{BaseURL: server.URL, Owner: "owner", Repository: "example"}}) writeDeliveryProfile(t, repository, cli.Settings{Product: "Example Product", Delivery: &cli.DeliverySettings{BaseURL: server.URL, Owner: "owner", Repository: "example"}})
state.commit = cliGitOutput(t, repository, "rev-parse", "HEAD") state.update(func(state *deliveryServerData) { state.commit = cliGitOutput(t, repository, "rev-parse", "HEAD") })
cliGitRun(t, repository, "tag", "v1.2.3") cliGitRun(t, repository, "tag", "v1.2.3")
t.Setenv("RELEASEKIT_GITEA_TOKEN", "fixture-token") t.Setenv("RELEASEKIT_GITEA_TOKEN", "fixture-token")
var stdout, stderr bytes.Buffer var stdout, stderr bytes.Buffer
status := run(context.Background(), []string{"deliver", "--repo", repository, "v1.2.3"}, &stdout, &stderr) status := run(context.Background(), []string{"deliver", "--repo", repository, "v1.2.3"}, &stdout, &stderr)
if status != 1 || state.releaseCreates != 0 || state.assetUploads != 0 { if status != 1 || state.snapshot().releaseCreates != 0 || state.snapshot().assetUploads != 0 {
t.Fatalf("wrong hosted tag status=%d creates=%d uploads=%d stdout=%q stderr=%q", status, state.releaseCreates, state.assetUploads, stdout.String(), stderr.String()) t.Fatalf("wrong hosted tag status=%d creates=%d uploads=%d stdout=%q stderr=%q", status, state.snapshot().releaseCreates, state.snapshot().assetUploads, stdout.String(), stderr.String())
} }
} }
@@ -1015,13 +1019,13 @@ func TestDeliverRequiresCredentialBeforeRemoteRequests(t *testing.T) {
server := newDeliveryServer(t, state) server := newDeliveryServer(t, state)
defer server.Close() defer server.Close()
writeDeliveryProfile(t, repository, cli.Settings{Product: "Example Product", Delivery: &cli.DeliverySettings{BaseURL: server.URL, Owner: "owner", Repository: "example", TokenEnv: "RELEASEKIT_TEST_DELIVERY_TOKEN"}}) writeDeliveryProfile(t, repository, cli.Settings{Product: "Example Product", Delivery: &cli.DeliverySettings{BaseURL: server.URL, Owner: "owner", Repository: "example", TokenEnv: "RELEASEKIT_TEST_DELIVERY_TOKEN"}})
state.commit = cliGitOutput(t, repository, "rev-parse", "HEAD") state.update(func(state *deliveryServerData) { state.commit = cliGitOutput(t, repository, "rev-parse", "HEAD") })
cliGitRun(t, repository, "tag", "v1.2.3") cliGitRun(t, repository, "tag", "v1.2.3")
t.Setenv("RELEASEKIT_TEST_DELIVERY_TOKEN", "") t.Setenv("RELEASEKIT_TEST_DELIVERY_TOKEN", "")
var stdout, stderr bytes.Buffer var stdout, stderr bytes.Buffer
status := run(context.Background(), []string{"deliver", "--repo", repository, "v1.2.3"}, &stdout, &stderr) status := run(context.Background(), []string{"deliver", "--repo", repository, "v1.2.3"}, &stdout, &stderr)
if status != 2 || state.apiRequests != 0 || !strings.Contains(stderr.String(), "RELEASEKIT_TEST_DELIVERY_TOKEN") { if status != 2 || state.snapshot().apiRequests != 0 || !strings.Contains(stderr.String(), "RELEASEKIT_TEST_DELIVERY_TOKEN") {
t.Fatalf("missing credential status=%d requests=%d stdout=%q stderr=%q", status, state.apiRequests, stdout.String(), stderr.String()) t.Fatalf("missing credential status=%d requests=%d stdout=%q stderr=%q", status, state.snapshot().apiRequests, stdout.String(), stderr.String())
} }
} }
@@ -1031,7 +1035,7 @@ func TestDeliverRejectsCheckoutThatDiffersFromLocalTag(t *testing.T) {
server := newDeliveryServer(t, state) server := newDeliveryServer(t, state)
defer server.Close() defer server.Close()
writeDeliveryProfile(t, repository, cli.Settings{Product: "Example Product", Delivery: &cli.DeliverySettings{BaseURL: server.URL, Owner: "owner", Repository: "example"}}) writeDeliveryProfile(t, repository, cli.Settings{Product: "Example Product", Delivery: &cli.DeliverySettings{BaseURL: server.URL, Owner: "owner", Repository: "example"}})
state.commit = cliGitOutput(t, repository, "rev-parse", "HEAD") state.update(func(state *deliveryServerData) { state.commit = cliGitOutput(t, repository, "rev-parse", "HEAD") })
cliGitRun(t, repository, "tag", "v1.2.3") cliGitRun(t, repository, "tag", "v1.2.3")
if err := os.WriteFile(filepath.Join(repository, "later.txt"), []byte("later commit"), 0o600); err != nil { if err := os.WriteFile(filepath.Join(repository, "later.txt"), []byte("later commit"), 0o600); err != nil {
t.Fatal(err) t.Fatal(err)
@@ -1041,11 +1045,11 @@ func TestDeliverRejectsCheckoutThatDiffersFromLocalTag(t *testing.T) {
t.Setenv("RELEASEKIT_GITEA_TOKEN", "fixture-token") t.Setenv("RELEASEKIT_GITEA_TOKEN", "fixture-token")
var stdout, stderr bytes.Buffer var stdout, stderr bytes.Buffer
status := run(context.Background(), []string{"deliver", "--repo", repository, "v1.2.3"}, &stdout, &stderr) status := run(context.Background(), []string{"deliver", "--repo", repository, "v1.2.3"}, &stdout, &stderr)
if status != 1 || state.apiRequests != 0 || state.releaseCreates != 0 { if status != 1 || state.snapshot().apiRequests != 0 || state.snapshot().releaseCreates != 0 {
t.Fatalf("mismatched checkout status=%d requests=%d creates=%d stdout=%q stderr=%q", status, state.apiRequests, state.releaseCreates, stdout.String(), stderr.String()) t.Fatalf("mismatched checkout status=%d requests=%d creates=%d stdout=%q stderr=%q", status, state.snapshot().apiRequests, state.snapshot().releaseCreates, stdout.String(), stderr.String())
} }
if got := cliGitOutput(t, repository, "rev-parse", "refs/tags/v1.2.3"); got != state.commit { if got := cliGitOutput(t, repository, "rev-parse", "refs/tags/v1.2.3"); got != state.snapshot().commit {
t.Fatalf("delivery moved tag from %s to %s", state.commit, got) t.Fatalf("delivery moved tag from %s to %s", state.snapshot().commit, got)
} }
} }
@@ -1059,7 +1063,7 @@ func TestDeliverRejectsTamperedAssetsAndStaleReceiptBeforeRemoteWrites(t *testin
current := cli.BuildSettings{MainPackage: ".", BinaryName: "helper", Targets: []string{"linux/amd64"}} current := cli.BuildSettings{MainPackage: ".", BinaryName: "helper", Targets: []string{"linux/amd64"}}
settings := cli.Settings{Product: "Example Product", Build: &current, Delivery: &cli.DeliverySettings{BaseURL: server.URL, Owner: "owner", Repository: "example"}} settings := cli.Settings{Product: "Example Product", Build: &current, Delivery: &cli.DeliverySettings{BaseURL: server.URL, Owner: "owner", Repository: "example"}}
writeDeliveryProfile(t, repository, settings) writeDeliveryProfile(t, repository, settings)
state.commit = cliGitOutput(t, repository, "rev-parse", "HEAD") state.update(func(state *deliveryServerData) { state.commit = cliGitOutput(t, repository, "rev-parse", "HEAD") })
cliGitRun(t, repository, "tag", "v1.2.3") cliGitRun(t, repository, "tag", "v1.2.3")
receiptSettings := current receiptSettings := current
if mode == "stale settings" { if mode == "stale settings" {
@@ -1085,16 +1089,16 @@ func TestDeliverRejectsTamperedAssetsAndStaleReceiptBeforeRemoteWrites(t *testin
var stdout, stderr bytes.Buffer var stdout, stderr bytes.Buffer
status := run(context.Background(), []string{"deliver", "--repo", repository, "--assets", assetsDir, "v1.2.3"}, &stdout, &stderr) status := run(context.Background(), []string{"deliver", "--repo", repository, "--assets", assetsDir, "v1.2.3"}, &stdout, &stderr)
if mode == "valid receipt" { if mode == "valid receipt" {
if status != 0 || state.releaseCreates != 1 || state.assetUploads != 2 { if status != 0 || state.snapshot().releaseCreates != 1 || state.snapshot().assetUploads != 2 {
t.Fatalf("valid receipt status=%d creates=%d uploads=%d stdout=%q stderr=%q", status, state.releaseCreates, state.assetUploads, stdout.String(), stderr.String()) t.Fatalf("valid receipt status=%d creates=%d uploads=%d stdout=%q stderr=%q", status, state.snapshot().releaseCreates, state.snapshot().assetUploads, stdout.String(), stderr.String())
} }
if _, err := os.Stat(artifactPath); err != nil { if _, err := os.Stat(artifactPath); err != nil {
t.Fatalf("prebuilt artifact was not retained: %v", err) t.Fatalf("prebuilt artifact was not retained: %v", err)
} }
return return
} }
if status != 1 || state.apiRequests != 0 || state.releaseCreates != 0 || state.assetUploads != 0 { if status != 1 || state.snapshot().apiRequests != 0 || state.snapshot().releaseCreates != 0 || state.snapshot().assetUploads != 0 {
t.Fatalf("%s receipt status=%d requests=%d stdout=%q stderr=%q", mode, status, state.apiRequests, stdout.String(), stderr.String()) t.Fatalf("%s receipt status=%d requests=%d stdout=%q stderr=%q", mode, status, state.snapshot().apiRequests, stdout.String(), stderr.String())
} }
if mode == "tampered" || mode == "missing artifact" { if mode == "tampered" || mode == "missing artifact" {
if strings.Contains(stderr.String(), "load verified build receipt") { if strings.Contains(stderr.String(), "load verified build receipt") {
@@ -1136,6 +1140,30 @@ func writeDeliveryReceipt(t *testing.T, repository string, settings cli.BuildSet
} }
type deliveryServerState struct { type deliveryServerState struct {
mu sync.Mutex
data deliveryServerData
}
func (state *deliveryServerState) update(change func(*deliveryServerData)) {
state.mu.Lock()
defer state.mu.Unlock()
change(&state.data)
}
func (state *deliveryServerState) snapshot() deliveryServerData {
state.mu.Lock()
defer state.mu.Unlock()
result := state.data
result.assets = make(map[string][]byte, len(state.data.assets))
for key, value := range state.data.assets {
result.assets[key] = append([]byte(nil), value...)
}
return result
}
type deliveryServerData struct {
tag string tag string
malformedCreate bool malformedCreate bool
failAssetListing bool failAssetListing bool
@@ -1156,36 +1184,38 @@ type deliveryServerState struct {
func newDeliveryServer(t *testing.T, state *deliveryServerState) *httptest.Server { func newDeliveryServer(t *testing.T, state *deliveryServerState) *httptest.Server {
t.Helper() t.Helper()
if state.tag == "" { data := &state.data
state.tag = "v1.2.3" if data.tag == "" {
data.tag = "v1.2.3"
} }
if state.assets == nil { if data.assets == nil {
state.assets = make(map[string][]byte) data.assets = make(map[string][]byte)
} }
serverURL := ""
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
state.mu.Lock()
defer state.mu.Unlock()
if strings.HasPrefix(r.URL.Path, "/api/") { if strings.HasPrefix(r.URL.Path, "/api/") {
state.apiRequests++ data.apiRequests++
if r.Header.Get("Authorization") != "token fixture-token" { if r.Header.Get("Authorization") != "token fixture-token" {
t.Errorf("authorization = %q", r.Header.Get("Authorization")) t.Errorf("authorization = %q", r.Header.Get("Authorization"))
} }
} }
switch r.URL.Path { switch r.URL.Path {
case "/api/v1/repos/owner/example/tags/" + state.tag: case "/api/v1/repos/owner/example/tags/" + data.tag:
if state.commit == "" { if data.commit == "" {
t.Error("tag source commit was not initialized") t.Error("tag source commit was not initialized")
} }
commit := state.remoteCommit commit := data.remoteCommit
if commit == "" { if commit == "" {
commit = state.commit commit = data.commit
} }
_, _ = fmt.Fprintf(w, `{"name":%q,"commit":{"sha":%q}}`, state.tag, commit) _, _ = fmt.Fprintf(w, `{"name":%q,"commit":{"sha":%q}}`, data.tag, commit)
case "/api/v1/repos/owner/example/releases/tags/" + state.tag: case "/api/v1/repos/owner/example/releases/tags/" + data.tag:
if !state.pageExists { if !data.pageExists {
http.NotFound(w, r) http.NotFound(w, r)
return return
} }
writeDeliveryRelease(w, serverURL, state) writeDeliveryRelease(w, serverURL(r), data)
case "/api/v1/repos/owner/example/releases": case "/api/v1/repos/owner/example/releases":
if r.Method != http.MethodPost { if r.Method != http.MethodPost {
t.Errorf("release request method = %s", r.Method) t.Errorf("release request method = %s", r.Method)
@@ -1201,26 +1231,26 @@ func newDeliveryServer(t *testing.T, state *deliveryServerState) *httptest.Serve
if err := json.NewDecoder(r.Body).Decode(&payload); err != nil { if err := json.NewDecoder(r.Body).Decode(&payload); err != nil {
t.Errorf("decode create payload: %v", err) t.Errorf("decode create payload: %v", err)
} }
if payload.Tag != state.tag || payload.Target != state.commit { if payload.Tag != data.tag || payload.Target != data.commit {
t.Errorf("release source payload = %#v", payload) t.Errorf("release source payload = %#v", payload)
} }
state.pageExists, state.releaseCreates, state.noteBody = true, state.releaseCreates+1, payload.Body data.pageExists, data.releaseCreates, data.noteBody = true, data.releaseCreates+1, payload.Body
if state.malformedCreate { if data.malformedCreate {
w.WriteHeader(http.StatusCreated) w.WriteHeader(http.StatusCreated)
_ = json.NewEncoder(w).Encode(map[string]any{"id": 10, "tag_name": state.tag, "name": "inconsistent response", "body": payload.Body}) _ = json.NewEncoder(w).Encode(map[string]any{"id": 10, "tag_name": data.tag, "name": "inconsistent response", "body": payload.Body})
return return
} }
writeDeliveryRelease(w, serverURL, state) writeDeliveryRelease(w, serverURL(r), data)
case "/api/v1/repos/owner/example/releases/10/assets": case "/api/v1/repos/owner/example/releases/10/assets":
if r.Method == http.MethodGet { if r.Method == http.MethodGet {
state.assetListReads++ data.assetListReads++
if state.failAssetListing { if data.failAssetListing {
http.Error(w, "unavailable", 503) http.Error(w, "unavailable", 503)
return return
} }
list := make([]map[string]any, 0, len(state.assets)) list := make([]map[string]any, 0, len(data.assets))
for name, content := range state.assets { for name, content := range data.assets {
list = append(list, map[string]any{"id": len(list) + 1, "name": name, "size": len(content), "browser_download_url": serverURL + "/files/" + name}) list = append(list, map[string]any{"id": len(list) + 1, "name": name, "size": len(content), "browser_download_url": serverURL(r) + "/files/" + name})
} }
_ = json.NewEncoder(w).Encode(list) _ = json.NewEncoder(w).Encode(list)
return return
@@ -1242,28 +1272,28 @@ func newDeliveryServer(t *testing.T, state *deliveryServerState) *httptest.Serve
if err != nil { if err != nil {
t.Errorf("read asset bytes: %v", err) t.Errorf("read asset bytes: %v", err)
} }
if _, exists := state.assets[name]; exists { if _, exists := data.assets[name]; exists {
t.Errorf("asset %q uploaded more than once", name) t.Errorf("asset %q uploaded more than once", name)
} }
if name == state.failAssetName { if name == data.failAssetName {
http.Error(w, "upload failed", http.StatusBadRequest) http.Error(w, "upload failed", http.StatusBadRequest)
return return
} }
if name == state.ambiguousAssetName { if name == data.ambiguousAssetName {
http.Error(w, "upload outcome ambiguous", http.StatusInternalServerError) http.Error(w, "upload outcome ambiguous", http.StatusInternalServerError)
return return
} }
state.assets[name] = content data.assets[name] = content
state.assetUploads++ data.assetUploads++
_ = json.NewEncoder(w).Encode(map[string]any{"id": state.assetUploads, "name": name, "size": len(content)}) _ = json.NewEncoder(w).Encode(map[string]any{"id": data.assetUploads, "name": name, "size": len(content)})
default: default:
if strings.HasPrefix(r.URL.Path, "/files/") { if strings.HasPrefix(r.URL.Path, "/files/") {
if state.failAssetDownloads { if data.failAssetDownloads {
http.Error(w, "unavailable", 503) http.Error(w, "unavailable", 503)
return return
} }
name := strings.TrimPrefix(r.URL.Path, "/files/") name := strings.TrimPrefix(r.URL.Path, "/files/")
content, ok := state.assets[name] content, ok := data.assets[name]
if !ok { if !ok {
http.NotFound(w, r) http.NotFound(w, r)
return return
@@ -1276,17 +1306,17 @@ func newDeliveryServer(t *testing.T, state *deliveryServerState) *httptest.Serve
} }
}) })
server := httptest.NewServer(handler) server := httptest.NewServer(handler)
serverURL = server.URL
return server return server
} }
func writeDeliveryRelease(w http.ResponseWriter, serverURL string, state *deliveryServerState) { // writeDeliveryRelease reads data while the request handler holds state.mu.
func writeDeliveryRelease(w http.ResponseWriter, baseURL string, data *deliveryServerData) {
w.Header().Set("Content-Type", "application/json") w.Header().Set("Content-Type", "application/json")
product := state.product product := data.product
if product == "" { if product == "" {
product = "Example Product" product = "Example Product"
} }
_ = json.NewEncoder(w).Encode(map[string]any{"id": 10, "html_url": serverURL + "/releases/10", "tag_name": state.tag, "target_commitish": state.commit, "name": product + " v1.2.3", "body": state.noteBody, "draft": false, "prerelease": false}) _ = json.NewEncoder(w).Encode(map[string]any{"id": 10, "html_url": baseURL + "/releases/10", "tag_name": data.tag, "target_commitish": data.commit, "name": product + " v1.2.3", "body": data.noteBody, "draft": false, "prerelease": false})
} }
func writeDeliveryProfile(t *testing.T, repository string, settings cli.Settings) { func writeDeliveryProfile(t *testing.T, repository string, settings cli.Settings) {
@@ -1417,7 +1447,7 @@ func TestDeliverRejectsReceiptBuiltFromDirtySourceAfterCleanup(t *testing.T) {
defer server.Close() defer server.Close()
settings := cli.Settings{Product: "Example Product", Build: &cli.BuildSettings{MainPackage: ".", BinaryName: "helper", Targets: []string{"linux/amd64"}}, Delivery: &cli.DeliverySettings{BaseURL: server.URL, Owner: "owner", Repository: "example"}} settings := cli.Settings{Product: "Example Product", Build: &cli.BuildSettings{MainPackage: ".", BinaryName: "helper", Targets: []string{"linux/amd64"}}, Delivery: &cli.DeliverySettings{BaseURL: server.URL, Owner: "owner", Repository: "example"}}
writeDeliveryProfile(t, repository, settings) writeDeliveryProfile(t, repository, settings)
state.commit = cliGitOutput(t, repository, "rev-parse", "HEAD") state.update(func(state *deliveryServerData) { state.commit = cliGitOutput(t, repository, "rev-parse", "HEAD") })
cliGitRun(t, repository, "tag", "v1.2.3") cliGitRun(t, repository, "tag", "v1.2.3")
mainPath := filepath.Join(repository, "main.go") mainPath := filepath.Join(repository, "main.go")
original, err := os.ReadFile(mainPath) original, err := os.ReadFile(mainPath)
@@ -1444,26 +1474,26 @@ func TestDeliverRejectsReceiptBuiltFromDirtySourceAfterCleanup(t *testing.T) {
stdout.Reset() stdout.Reset()
stderr.Reset() stderr.Reset()
status := run(context.Background(), []string{"deliver", "--repo", repository, "--assets", assets, "v1.2.3"}, &stdout, &stderr) status := run(context.Background(), []string{"deliver", "--repo", repository, "--assets", assets, "v1.2.3"}, &stdout, &stderr)
if status != 1 || state.apiRequests != 0 || !strings.Contains(stderr.String(), "build source") { if status != 1 || state.snapshot().apiRequests != 0 || !strings.Contains(stderr.String(), "build source") {
t.Fatalf("status=%d requests=%d %s", status, state.apiRequests, stderr.String()) t.Fatalf("status=%d requests=%d %s", status, state.snapshot().apiRequests, stderr.String())
} }
} }
func TestRecoveredPageCreatePreservesPartialDeliveryOnInspectionFailure(t *testing.T) { func TestRecoveredPageCreatePreservesPartialDeliveryOnInspectionFailure(t *testing.T) {
repository := newCLIRepository(t) repository := newCLIRepository(t)
state := &deliveryServerState{malformedCreate: true, failAssetListing: true} state := &deliveryServerState{data: deliveryServerData{malformedCreate: true, failAssetListing: true}}
server := newDeliveryServer(t, state) server := newDeliveryServer(t, state)
defer server.Close() defer server.Close()
build := cli.BuildSettings{MainPackage: ".", BinaryName: "helper", Targets: []string{"linux/amd64"}} build := cli.BuildSettings{MainPackage: ".", BinaryName: "helper", Targets: []string{"linux/amd64"}}
writeDeliveryProfile(t, repository, cli.Settings{Product: "Example Product", Build: &build, Delivery: &cli.DeliverySettings{BaseURL: server.URL, Owner: "owner", Repository: "example"}}) writeDeliveryProfile(t, repository, cli.Settings{Product: "Example Product", Build: &build, Delivery: &cli.DeliverySettings{BaseURL: server.URL, Owner: "owner", Repository: "example"}})
state.commit = cliGitOutput(t, repository, "rev-parse", "HEAD") state.update(func(state *deliveryServerData) { state.commit = cliGitOutput(t, repository, "rev-parse", "HEAD") })
cliGitRun(t, repository, "tag", "v1.2.3") cliGitRun(t, repository, "tag", "v1.2.3")
assets, _ := writeDeliveryReceipt(t, repository, build) assets, _ := writeDeliveryReceipt(t, repository, build)
t.Setenv("RELEASEKIT_GITEA_TOKEN", "fixture-token") t.Setenv("RELEASEKIT_GITEA_TOKEN", "fixture-token")
var stdout, stderr bytes.Buffer var stdout, stderr bytes.Buffer
status := run(context.Background(), []string{"deliver", "--repo", repository, "--assets", assets, "v1.2.3"}, &stdout, &stderr) status := run(context.Background(), []string{"deliver", "--repo", repository, "--assets", assets, "v1.2.3"}, &stdout, &stderr)
if status != 1 || !strings.Contains(stderr.String(), "state=partial") || state.releaseCreates != 1 || state.assetListReads != 1 || state.assetUploads != 0 { if status != 1 || !strings.Contains(stderr.String(), "state=partial") || state.snapshot().releaseCreates != 1 || state.snapshot().assetListReads != 1 || state.snapshot().assetUploads != 0 {
t.Fatalf("status=%d state=%#v %s", status, state, stderr.String()) t.Fatalf("status=%d state=%#v %s", status, state.snapshot(), stderr.String())
} }
} }
@@ -1471,12 +1501,12 @@ func TestUnreadableAssetsDoNotClaimAnUnknownWrite(t *testing.T) {
for _, existingPage := range []bool{false, true} { for _, existingPage := range []bool{false, true} {
t.Run(fmt.Sprintf("existing-page-%t", existingPage), func(t *testing.T) { t.Run(fmt.Sprintf("existing-page-%t", existingPage), func(t *testing.T) {
repository := newCLIRepository(t) repository := newCLIRepository(t)
state := &deliveryServerState{pageExists: existingPage, noteBody: "# Example Product v1.2.3\n\nRelease note.\n", failAssetDownloads: true, assets: map[string][]byte{"helper-v1.2.3-linux-amd64": []byte("fixture artifact")}} state := &deliveryServerState{data: deliveryServerData{pageExists: existingPage, noteBody: "# Example Product v1.2.3\n\nRelease note.\n", failAssetDownloads: true, assets: map[string][]byte{"helper-v1.2.3-linux-amd64": []byte("fixture artifact")}}}
server := newDeliveryServer(t, state) server := newDeliveryServer(t, state)
defer server.Close() defer server.Close()
build := cli.BuildSettings{MainPackage: ".", BinaryName: "helper", Targets: []string{"linux/amd64"}} build := cli.BuildSettings{MainPackage: ".", BinaryName: "helper", Targets: []string{"linux/amd64"}}
writeDeliveryProfile(t, repository, cli.Settings{Product: "Example Product", Build: &build, Delivery: &cli.DeliverySettings{BaseURL: server.URL, Owner: "owner", Repository: "example"}}) writeDeliveryProfile(t, repository, cli.Settings{Product: "Example Product", Build: &build, Delivery: &cli.DeliverySettings{BaseURL: server.URL, Owner: "owner", Repository: "example"}})
state.commit = cliGitOutput(t, repository, "rev-parse", "HEAD") state.update(func(state *deliveryServerData) { state.commit = cliGitOutput(t, repository, "rev-parse", "HEAD") })
cliGitRun(t, repository, "tag", "v1.2.3") cliGitRun(t, repository, "tag", "v1.2.3")
assets, _ := writeDeliveryReceipt(t, repository, build) assets, _ := writeDeliveryReceipt(t, repository, build)
t.Setenv("RELEASEKIT_GITEA_TOKEN", "fixture-token") t.Setenv("RELEASEKIT_GITEA_TOKEN", "fixture-token")
@@ -1488,8 +1518,8 @@ func TestUnreadableAssetsDoNotClaimAnUnknownWrite(t *testing.T) {
expectedState = "not_attempted" expectedState = "not_attempted"
expectedCreates = 0 expectedCreates = 0
} }
if status != 1 || !strings.Contains(stderr.String(), "state="+expectedState) || state.assetUploads != 0 || state.releaseCreates != expectedCreates { if status != 1 || !strings.Contains(stderr.String(), "state="+expectedState) || state.snapshot().assetUploads != 0 || state.snapshot().releaseCreates != expectedCreates {
t.Fatalf("status=%d creates=%d uploads=%d %s", status, state.releaseCreates, state.assetUploads, stderr.String()) t.Fatalf("status=%d creates=%d uploads=%d %s", status, state.snapshot().releaseCreates, state.snapshot().assetUploads, stderr.String())
} }
if existingPage && !strings.Contains(stderr.String(), "recovery: fix the local or preflight failure") { if existingPage && !strings.Contains(stderr.String(), "recovery: fix the local or preflight failure") {
t.Fatalf("not-attempted recovery guidance missing: %s", stderr.String()) t.Fatalf("not-attempted recovery guidance missing: %s", stderr.String())
@@ -1532,7 +1562,7 @@ func TestTemporaryDeliveryBuildDoesNotRequireWritableRepositoryParent(t *testing
server := newDeliveryServer(t, state) server := newDeliveryServer(t, state)
defer server.Close() defer server.Close()
writeDeliveryProfile(t, repository, cli.Settings{Product: "Example Product", Build: &cli.BuildSettings{MainPackage: ".", BinaryName: "helper", Targets: []string{"linux/amd64"}}, Delivery: &cli.DeliverySettings{BaseURL: server.URL, Owner: "owner", Repository: "example"}}) writeDeliveryProfile(t, repository, cli.Settings{Product: "Example Product", Build: &cli.BuildSettings{MainPackage: ".", BinaryName: "helper", Targets: []string{"linux/amd64"}}, Delivery: &cli.DeliverySettings{BaseURL: server.URL, Owner: "owner", Repository: "example"}})
state.commit = cliGitOutput(t, repository, "rev-parse", "HEAD") state.update(func(state *deliveryServerData) { state.commit = cliGitOutput(t, repository, "rev-parse", "HEAD") })
cliGitRun(t, repository, "tag", "v1.2.3") cliGitRun(t, repository, "tag", "v1.2.3")
parent := t.TempDir() parent := t.TempDir()
moved := filepath.Join(parent, "repo") moved := filepath.Join(parent, "repo")

View File

@@ -12,6 +12,7 @@ import (
"os/exec" "os/exec"
"path/filepath" "path/filepath"
"strings" "strings"
"sync"
"testing" "testing"
"gitea.maximumdirect.net/eric/ReleaseKit/cmd/releasekit/internal/cli" "gitea.maximumdirect.net/eric/ReleaseKit/cmd/releasekit/internal/cli"
@@ -43,18 +44,18 @@ func TestMultiModuleWoodpeckerDelivery(t *testing.T) {
repository := newWorkflowRepository(t, example) repository := newWorkflowRepository(t, example)
writeMappedProfile(t, repository, "releasekit-ckconfig.json", cli.Settings{ writeMappedProfile(t, repository, "releasekit-ckconfig.json", cli.Settings{
Product: "Example Consumer Integration", ModuleDir: "integrations/ckconfig", Product: "Example Consumer Integration", ModuleDir: "integrations/ckconfig",
Delivery: workflowDeliverySettings(delivery.URL), Delivery: workflowDeliverySettings(delivery.snapshot().URL),
}) })
writeRepositoryFile(t, repository, "integrations/ckconfig/docs/releases/v1.2.3.md", "# Example Consumer Integration v1.2.3\n\nTagged integration note.\n") writeRepositoryFile(t, repository, "integrations/ckconfig/docs/releases/v1.2.3.md", "# Example Consumer Integration v1.2.3\n\nTagged integration note.\n")
writeRepositoryFile(t, repository, "integrations/ckconfig/go.mod", workflowModuleFile("example.com/multi-module-consumer/integrations/ckconfig", "v0.2.197")) writeRepositoryFile(t, repository, "integrations/ckconfig/go.mod", workflowModuleFile("example.com/multi-module-consumer/integrations/ckconfig", "v0.2.197"))
commit := tagAndPushWorkflowRepository(t, repository, tag) commit := tagAndPushWorkflowRepository(t, repository, tag)
delivery.commit = commit delivery.update(func(delivery *mappedDeliveryData) { delivery.commit = commit })
env := multiModuleWorkflowEnvironment(t, repository, commit, tag, toolBinary, proxy) env := multiModuleWorkflowEnvironment(t, repository, commit, tag, toolBinary, proxy)
if output := runWoodpeckerBlock(t, command, repository, env); !strings.Contains(output, "state complete") || !strings.Contains(output, "tool-version=v0.2.197") { if output := runWoodpeckerBlock(t, command, repository, env); !strings.Contains(output, "state complete") || !strings.Contains(output, "tool-version=v0.2.197") {
t.Fatalf("tagged page delivery output = %q", output) t.Fatalf("tagged page delivery output = %q", output)
} }
if delivery.creates != 1 || delivery.requests == 0 || delivery.tag != tag { if delivery.snapshot().creates != 1 || delivery.snapshot().requests == 0 || delivery.snapshot().tag != tag {
t.Fatalf("page delivery fixture = %#v", delivery) t.Fatalf("page delivery fixture = %#v", delivery.snapshot())
} }
cliGitRun(t, repository, "checkout", "main") cliGitRun(t, repository, "checkout", "main")
@@ -68,8 +69,8 @@ func TestMultiModuleWoodpeckerDelivery(t *testing.T) {
if !strings.Contains(output, "tool-version=v0.2.197") || strings.Contains(output, "tool-version=v0.2.198") { if !strings.Contains(output, "tool-version=v0.2.197") || strings.Contains(output, "tool-version=v0.2.198") {
t.Fatalf("tag retry did not use its committed tool version: %q", output) t.Fatalf("tag retry did not use its committed tool version: %q", output)
} }
if delivery.creates != 1 || delivery.tag != tag { if delivery.snapshot().creates != 1 || delivery.snapshot().tag != tag {
t.Fatalf("tag retry did not reuse the tagged mapping: %#v", delivery) t.Fatalf("tag retry did not reuse the tagged mapping: %#v", delivery.snapshot())
} }
}) })
@@ -80,25 +81,26 @@ func TestMultiModuleWoodpeckerDelivery(t *testing.T) {
writeMappedProfile(t, repository, "releasekit-core-v2.json", cli.Settings{ writeMappedProfile(t, repository, "releasekit-core-v2.json", cli.Settings{
Product: "Example Consumer Core v2", ModuleDir: "integrations/core/v2", Product: "Example Consumer Core v2", ModuleDir: "integrations/core/v2",
Build: &cli.BuildSettings{MainPackage: ".", BinaryName: "core", Targets: []string{"linux/amd64"}}, Build: &cli.BuildSettings{MainPackage: ".", BinaryName: "core", Targets: []string{"linux/amd64"}},
Delivery: workflowDeliverySettings(delivery.URL), Delivery: workflowDeliverySettings(delivery.snapshot().URL),
}) })
writeRepositoryFile(t, repository, "integrations/core/v2/docs/releases/v2.1.0.md", "# Example Consumer Core v2 v2.1.0\n\nTagged core note.\n") writeRepositoryFile(t, repository, "integrations/core/v2/docs/releases/v2.1.0.md", "# Example Consumer Core v2 v2.1.0\n\nTagged core note.\n")
prepareWorkflowModuleSum(t, repository, "integrations/core/v2", proxy) prepareWorkflowModuleSum(t, repository, "integrations/core/v2", proxy)
commit := tagAndPushWorkflowRepository(t, repository, tag) commit := tagAndPushWorkflowRepository(t, repository, tag)
delivery.commit = commit delivery.update(func(delivery *mappedDeliveryData) { delivery.commit = commit })
env := multiModuleWorkflowEnvironment(t, repository, commit, tag, toolBinary, proxy) env := multiModuleWorkflowEnvironment(t, repository, commit, tag, toolBinary, proxy)
output := runWoodpeckerBlock(t, command, repository, env) output := runWoodpeckerBlock(t, command, repository, env)
if !strings.Contains(output, "tool-version=v0.2.197") || !strings.Contains(output, "state complete") { if !strings.Contains(output, "tool-version=v0.2.197") || !strings.Contains(output, "state complete") {
t.Fatalf("binary delivery output = %q", output) t.Fatalf("binary delivery output = %q", output)
} }
if delivery.creates != 1 || delivery.uploads != 2 || delivery.tag != tag { observed := delivery.snapshot()
t.Fatalf("binary delivery fixture = %#v", delivery) if observed.creates != 1 || observed.uploads != 2 || observed.tag != tag {
t.Fatalf("binary delivery fixture = %#v", observed)
} }
if _, ok := delivery.assets["core-v2.1.0-linux-amd64"]; !ok { if _, ok := observed.assets["core-v2.1.0-linux-amd64"]; !ok {
t.Fatalf("binary asset missing: %#v", delivery.assets) t.Fatalf("binary asset missing: %#v", observed.assets)
} }
if _, ok := delivery.assets["SHA256SUMS"]; !ok { if _, ok := observed.assets["SHA256SUMS"]; !ok {
t.Fatalf("checksum asset missing: %#v", delivery.assets) t.Fatalf("checksum asset missing: %#v", observed.assets)
} }
}) })
@@ -109,21 +111,21 @@ func TestMultiModuleWoodpeckerDelivery(t *testing.T) {
writeRepositoryFile(t, repository, "release-modules.tsv", strings.ReplaceAll(readWorkflowFile(t, repository, "release-modules.tsv"), "integrations/core/v2\treleasekit-core-v2.json", "integrations/core\treleasekit-core-v2.json")) writeRepositoryFile(t, repository, "release-modules.tsv", strings.ReplaceAll(readWorkflowFile(t, repository, "release-modules.tsv"), "integrations/core/v2\treleasekit-core-v2.json", "integrations/core\treleasekit-core-v2.json"))
writeMappedProfile(t, repository, "releasekit-core-v2.json", cli.Settings{ writeMappedProfile(t, repository, "releasekit-core-v2.json", cli.Settings{
Product: "Example Consumer Core v2", ModuleDir: "integrations/core", Product: "Example Consumer Core v2", ModuleDir: "integrations/core",
Delivery: workflowDeliverySettings(delivery.URL), Delivery: workflowDeliverySettings(delivery.snapshot().URL),
}) })
writeRepositoryFile(t, repository, "integrations/core/go.mod", workflowModuleFile("example.com/multi-module-consumer/integrations/core/v2", "v0.2.197")) writeRepositoryFile(t, repository, "integrations/core/go.mod", workflowModuleFile("example.com/multi-module-consumer/integrations/core/v2", "v0.2.197"))
writeRepositoryFile(t, repository, "integrations/core/main.go", readWorkflowFile(t, example, "integrations/core/v2/main.go")) writeRepositoryFile(t, repository, "integrations/core/main.go", readWorkflowFile(t, example, "integrations/core/v2/main.go"))
writeRepositoryFile(t, repository, "integrations/core/docs/releases/v2.1.0.md", "# Example Consumer Core v2 v2.1.0\n\nTagged core note.\n") writeRepositoryFile(t, repository, "integrations/core/docs/releases/v2.1.0.md", "# Example Consumer Core v2 v2.1.0\n\nTagged core note.\n")
prepareWorkflowModuleSum(t, repository, "integrations/core", proxy) prepareWorkflowModuleSum(t, repository, "integrations/core", proxy)
commit := tagAndPushWorkflowRepository(t, repository, tag) commit := tagAndPushWorkflowRepository(t, repository, tag)
delivery.commit = commit delivery.update(func(delivery *mappedDeliveryData) { delivery.commit = commit })
env := multiModuleWorkflowEnvironment(t, repository, commit, tag, toolBinary, proxy) env := multiModuleWorkflowEnvironment(t, repository, commit, tag, toolBinary, proxy)
output := runWoodpeckerBlock(t, command, repository, env) output := runWoodpeckerBlock(t, command, repository, env)
if !strings.Contains(output, "tool-version=v0.2.197") || !strings.Contains(output, "state complete") { if !strings.Contains(output, "tool-version=v0.2.197") || !strings.Contains(output, "state complete") {
t.Fatalf("virtual-major delivery output = %q", output) t.Fatalf("virtual-major delivery output = %q", output)
} }
if delivery.creates != 1 || delivery.tag != tag { if delivery.snapshot().creates != 1 || delivery.snapshot().tag != tag {
t.Fatalf("virtual-major delivery fixture = %#v", delivery) t.Fatalf("virtual-major delivery fixture = %#v", delivery.snapshot())
} }
}) })
@@ -138,24 +140,48 @@ func TestMultiModuleWoodpeckerDelivery(t *testing.T) {
writeRepositoryFile(t, repository, "release-modules.tsv", mapping) writeRepositoryFile(t, repository, "release-modules.tsv", mapping)
writeMappedProfile(t, repository, "releasekit-core-v2.json", cli.Settings{ writeMappedProfile(t, repository, "releasekit-core-v2.json", cli.Settings{
Product: "Example Consumer Core v2", ModuleDir: "integrations/core/v2", Product: "Example Consumer Core v2", ModuleDir: "integrations/core/v2",
Delivery: workflowDeliverySettings(delivery.URL), Delivery: workflowDeliverySettings(delivery.snapshot().URL),
}) })
writeRepositoryFile(t, repository, "integrations/core/v2/docs/releases/v2.1.0.md", "# Example Consumer Core v2 v2.1.0\n\nMismatched mapped note.\n") writeRepositoryFile(t, repository, "integrations/core/v2/docs/releases/v2.1.0.md", "# Example Consumer Core v2 v2.1.0\n\nMismatched mapped note.\n")
commit := tagAndPushWorkflowRepository(t, repository, tag) commit := tagAndPushWorkflowRepository(t, repository, tag)
delivery.commit = commit delivery.update(func(delivery *mappedDeliveryData) { delivery.commit = commit })
env := multiModuleWorkflowEnvironment(t, repository, commit, tag, toolBinary, proxy) env := multiModuleWorkflowEnvironment(t, repository, commit, tag, toolBinary, proxy)
if output, err := executeWoodpeckerBlock(command, repository, env); err == nil { if output, err := executeWoodpeckerBlock(command, repository, env); err == nil {
t.Fatalf("inconsistent mapping succeeded: %s", output) t.Fatalf("inconsistent mapping succeeded: %s", output)
} else if !strings.Contains(output, tag) || !strings.Contains(output, "integrations/core/v2.1.0") { } else if !strings.Contains(output, tag) || !strings.Contains(output, "integrations/core/v2.1.0") {
t.Fatalf("inconsistent mapping did not fail the exact tag comparison: %s", output) t.Fatalf("inconsistent mapping did not fail the exact tag comparison: %s", output)
} }
if delivery.requests != 0 { if delivery.snapshot().requests != 0 {
t.Fatalf("exact event tag mismatch reached hosted delivery: %#v", delivery) t.Fatalf("exact event tag mismatch reached hosted delivery: %#v", delivery.snapshot())
} }
}) })
} }
type mappedDeliveryFixture struct { type mappedDeliveryFixture struct {
mu sync.Mutex
data mappedDeliveryData
}
func (state *mappedDeliveryFixture) update(change func(*mappedDeliveryData)) {
state.mu.Lock()
defer state.mu.Unlock()
change(&state.data)
}
func (state *mappedDeliveryFixture) snapshot() mappedDeliveryData {
state.mu.Lock()
defer state.mu.Unlock()
result := state.data
result.assets = make(map[string][]byte, len(state.data.assets))
for key, value := range state.data.assets {
result.assets[key] = append([]byte(nil), value...)
}
return result
}
type mappedDeliveryData struct {
URL string URL string
tag string tag string
commit string commit string
@@ -170,9 +196,12 @@ type mappedDeliveryFixture struct {
func newMappedDeliveryFixture(t *testing.T, tag string) *mappedDeliveryFixture { func newMappedDeliveryFixture(t *testing.T, tag string) *mappedDeliveryFixture {
t.Helper() t.Helper()
state := &mappedDeliveryFixture{tag: tag, assets: make(map[string][]byte)} state := &mappedDeliveryFixture{data: mappedDeliveryData{tag: tag, assets: make(map[string][]byte)}}
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { data := &state.data
state.requests++ server := httptest.NewUnstartedServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
state.mu.Lock()
defer state.mu.Unlock()
data.requests++
if r.Header.Get("Authorization") != "token fixture-token" { if r.Header.Get("Authorization") != "token fixture-token" {
t.Errorf("delivery authorization = %q", r.Header.Get("Authorization")) t.Errorf("delivery authorization = %q", r.Header.Get("Authorization"))
} }
@@ -180,13 +209,13 @@ func newMappedDeliveryFixture(t *testing.T, tag string) *mappedDeliveryFixture {
escapedTag := url.PathEscape(tag) escapedTag := url.PathEscape(tag)
switch r.URL.EscapedPath() { switch r.URL.EscapedPath() {
case apiRoot + "tags/" + escapedTag: case apiRoot + "tags/" + escapedTag:
_ = json.NewEncoder(w).Encode(map[string]any{"name": tag, "commit": map[string]string{"sha": state.commit}}) _ = json.NewEncoder(w).Encode(map[string]any{"name": tag, "commit": map[string]string{"sha": data.commit}})
case apiRoot + "releases/tags/" + escapedTag: case apiRoot + "releases/tags/" + escapedTag:
if !state.page { if !data.page {
http.NotFound(w, r) http.NotFound(w, r)
return return
} }
_ = json.NewEncoder(w).Encode(map[string]any{"id": 10, "html_url": serverURL(r) + "/releases/10", "tag_name": tag, "target_commitish": state.commit, "name": state.name, "body": state.body, "draft": false, "prerelease": false}) _ = json.NewEncoder(w).Encode(map[string]any{"id": 10, "html_url": serverURL(r) + "/releases/10", "tag_name": tag, "target_commitish": data.commit, "name": data.name, "body": data.body, "draft": false, "prerelease": false})
case apiRoot + "releases": case apiRoot + "releases":
var payload struct { var payload struct {
Tag string `json:"tag_name"` Tag string `json:"tag_name"`
@@ -197,18 +226,18 @@ func newMappedDeliveryFixture(t *testing.T, tag string) *mappedDeliveryFixture {
if err := json.NewDecoder(r.Body).Decode(&payload); err != nil { if err := json.NewDecoder(r.Body).Decode(&payload); err != nil {
t.Errorf("decode release create: %v", err) t.Errorf("decode release create: %v", err)
} }
if payload.Tag != tag || payload.Commit != state.commit { if payload.Tag != tag || payload.Commit != data.commit {
t.Errorf("release payload = %#v", payload) t.Errorf("release payload = %#v", payload)
} }
state.page = true data.page = true
state.name = payload.Name data.name = payload.Name
state.body = payload.Body data.body = payload.Body
state.creates++ data.creates++
_ = json.NewEncoder(w).Encode(map[string]any{"id": 10, "html_url": serverURL(r) + "/releases/10", "tag_name": tag, "target_commitish": state.commit, "name": payload.Name, "body": payload.Body, "draft": false, "prerelease": false}) _ = json.NewEncoder(w).Encode(map[string]any{"id": 10, "html_url": serverURL(r) + "/releases/10", "tag_name": tag, "target_commitish": data.commit, "name": payload.Name, "body": payload.Body, "draft": false, "prerelease": false})
case apiRoot + "releases/10/assets": case apiRoot + "releases/10/assets":
if r.Method == http.MethodGet { if r.Method == http.MethodGet {
listing := make([]map[string]any, 0, len(state.assets)) listing := make([]map[string]any, 0, len(data.assets))
for name, content := range state.assets { for name, content := range data.assets {
listing = append(listing, map[string]any{"id": len(listing) + 1, "name": name, "size": len(content), "browser_download_url": serverURL(r) + "/files/" + name}) listing = append(listing, map[string]any{"id": len(listing) + 1, "name": name, "size": len(content), "browser_download_url": serverURL(r) + "/files/" + name})
} }
_ = json.NewEncoder(w).Encode(listing) _ = json.NewEncoder(w).Encode(listing)
@@ -231,13 +260,13 @@ func newMappedDeliveryFixture(t *testing.T, tag string) *mappedDeliveryFixture {
if err != nil { if err != nil {
t.Errorf("read asset bytes: %v", err) t.Errorf("read asset bytes: %v", err)
} }
state.assets[name] = content data.assets[name] = content
state.uploads++ data.uploads++
_ = json.NewEncoder(w).Encode(map[string]any{"id": state.uploads, "name": name, "size": len(content)}) _ = json.NewEncoder(w).Encode(map[string]any{"id": data.uploads, "name": name, "size": len(content)})
default: default:
if strings.HasPrefix(r.URL.Path, "/files/") { if strings.HasPrefix(r.URL.Path, "/files/") {
name := strings.TrimPrefix(r.URL.Path, "/files/") name := strings.TrimPrefix(r.URL.Path, "/files/")
if content, ok := state.assets[name]; ok { if content, ok := data.assets[name]; ok {
_, _ = w.Write(content) _, _ = w.Write(content)
return return
} }
@@ -248,8 +277,9 @@ func newMappedDeliveryFixture(t *testing.T, tag string) *mappedDeliveryFixture {
http.NotFound(w, r) http.NotFound(w, r)
} }
})) }))
data.URL = "http://" + server.Listener.Addr().String()
server.Start()
t.Cleanup(server.Close) t.Cleanup(server.Close)
state.URL = server.URL
return state return state
} }

View File

@@ -50,14 +50,14 @@ func TestReleaseKitSourceDeliveryWorkflowFromTaggedCheckout(t *testing.T) {
cliGitRun(t, repository, "add", "docs/releases/"+version+".md") cliGitRun(t, repository, "add", "docs/releases/"+version+".md")
cliGitRun(t, repository, "commit", "-m", "prepare source-only delivery") cliGitRun(t, repository, "commit", "-m", "prepare source-only delivery")
state := &deliveryServerState{product: "ReleaseKit"} state := &deliveryServerState{data: deliveryServerData{product: "ReleaseKit"}}
server := newDeliveryServer(t, state) server := newDeliveryServer(t, state)
defer server.Close() defer server.Close()
writeDeliveryProfile(t, repository, cli.Settings{ writeDeliveryProfile(t, repository, cli.Settings{
Product: "ReleaseKit", Product: "ReleaseKit",
Delivery: &cli.DeliverySettings{BaseURL: server.URL, Owner: "owner", Repository: "example"}, Delivery: &cli.DeliverySettings{BaseURL: server.URL, Owner: "owner", Repository: "example"},
}) })
state.commit = cliGitOutput(t, repository, "rev-parse", "HEAD") state.update(func(state *deliveryServerData) { state.commit = cliGitOutput(t, repository, "rev-parse", "HEAD") })
cliGitRun(t, repository, "tag", "-a", version, "-m", "ReleaseKit v1.2.3") cliGitRun(t, repository, "tag", "-a", version, "-m", "ReleaseKit v1.2.3")
remote := filepath.Join(t.TempDir(), "origin.git") remote := filepath.Join(t.TempDir(), "origin.git")
@@ -78,7 +78,7 @@ func TestReleaseKitSourceDeliveryWorkflowFromTaggedCheckout(t *testing.T) {
} }
env := map[string]string{ env := map[string]string{
"CI_WORKSPACE": repository, "CI_WORKSPACE": repository,
"CI_COMMIT_SHA": state.commit, "CI_COMMIT_SHA": state.snapshot().commit,
"CI_COMMIT_TAG": version, "CI_COMMIT_TAG": version,
"RELEASEKIT_GITEA_TOKEN": "fixture-token", "RELEASEKIT_GITEA_TOKEN": "fixture-token",
"GOTOOLCHAIN": "local", "GOTOOLCHAIN": "local",
@@ -87,14 +87,14 @@ func TestReleaseKitSourceDeliveryWorkflowFromTaggedCheckout(t *testing.T) {
} }
command := strings.ReplaceAll(block, "$${", "${") command := strings.ReplaceAll(block, "$${", "${")
state.remoteCommit = strings.Repeat("f", 40) state.update(func(state *deliveryServerData) { state.remoteCommit = strings.Repeat("f", 40) })
if output, err := executeWoodpeckerBlock(command, repository, env); err == nil { if output, err := executeWoodpeckerBlock(command, repository, env); err == nil {
t.Fatalf("delivery with a mismatched hosted tag succeeded: %s", output) t.Fatalf("delivery with a mismatched hosted tag succeeded: %s", output)
} }
if state.releaseCreates != 0 || cliGitOutput(t, repository, "rev-parse", "refs/tags/"+version+"^{commit}") != state.commit { if state.snapshot().releaseCreates != 0 || cliGitOutput(t, repository, "rev-parse", "refs/tags/"+version+"^{commit}") != state.snapshot().commit {
t.Fatal("delivery failure changed hosted release state or source tag") t.Fatal("delivery failure changed hosted release state or source tag")
} }
state.remoteCommit = "" state.update(func(state *deliveryServerData) { state.remoteCommit = "" })
for attempt := 0; attempt < 2; attempt++ { for attempt := 0; attempt < 2; attempt++ {
output := runWoodpeckerBlock(t, command, repository, env) output := runWoodpeckerBlock(t, command, repository, env)
@@ -102,14 +102,14 @@ func TestReleaseKitSourceDeliveryWorkflowFromTaggedCheckout(t *testing.T) {
t.Fatalf("delivery attempt %d output = %q", attempt+1, output) t.Fatalf("delivery attempt %d output = %q", attempt+1, output)
} }
} }
if state.releaseCreates != 1 || state.assetUploads != 0 || state.assetListReads != 0 { if state.snapshot().releaseCreates != 1 || state.snapshot().assetUploads != 0 || state.snapshot().assetListReads != 0 {
t.Fatalf("page-only workflow calls = creates:%d uploads:%d asset reads:%d", state.releaseCreates, state.assetUploads, state.assetListReads) t.Fatalf("page-only workflow calls = creates:%d uploads:%d asset reads:%d", state.snapshot().releaseCreates, state.snapshot().assetUploads, state.snapshot().assetListReads)
} }
if state.noteBody != note { if state.snapshot().noteBody != note {
t.Fatalf("hosted note body = %q, want complete note %q", state.noteBody, note) t.Fatalf("hosted note body = %q, want complete note %q", state.snapshot().noteBody, note)
} }
if got := cliGitOutput(t, repository, "rev-parse", "refs/tags/"+version+"^{commit}"); got != state.commit { if got := cliGitOutput(t, repository, "rev-parse", "refs/tags/"+version+"^{commit}"); got != state.snapshot().commit {
t.Fatalf("source tag moved after delivery: %s, want %s", got, state.commit) t.Fatalf("source tag moved after delivery: %s, want %s", got, state.snapshot().commit)
} }
if status := cliGitOutput(t, repository, "status", "--porcelain"); status != "" { if status := cliGitOutput(t, repository, "status", "--porcelain"); status != "" {
t.Fatalf("tagged checkout became dirty after delivery: %s", status) t.Fatalf("tagged checkout became dirty after delivery: %s", status)

View File

@@ -42,7 +42,7 @@ func TestWoodpeckerTagDeliveryTemplate(t *testing.T) {
Delivery: &cli.DeliverySettings{BaseURL: server.URL, Owner: "owner", Repository: "example"}, Delivery: &cli.DeliverySettings{BaseURL: server.URL, Owner: "owner", Repository: "example"},
}) })
commit := pushTaggedCLIRepository(t, repository) commit := pushTaggedCLIRepository(t, repository)
state.commit = commit state.update(func(state *deliveryServerData) { state.commit = commit })
goDir, goLog := installGoBootstrapShim(t) goDir, goLog := installGoBootstrapShim(t)
env := templateEnvironment(repository, commit, goDir, goLog) env := templateEnvironment(repository, commit, goDir, goLog)
@@ -51,8 +51,8 @@ func TestWoodpeckerTagDeliveryTemplate(t *testing.T) {
if !strings.Contains(output, "state complete") { if !strings.Contains(output, "state complete") {
t.Fatalf("delivery output=%q", output) t.Fatalf("delivery output=%q", output)
} }
if state.releaseCreates != 1 || state.assetUploads != 0 { if state.snapshot().releaseCreates != 1 || state.snapshot().assetUploads != 0 {
t.Fatalf("source-only calls = creates:%d uploads:%d", state.releaseCreates, state.assetUploads) t.Fatalf("source-only calls = creates:%d uploads:%d", state.snapshot().releaseCreates, state.snapshot().assetUploads)
} }
assertModuleBootstrap(t, repository, goLog, moduleBefore) assertModuleBootstrap(t, repository, goLog, moduleBefore)
}) })
@@ -73,7 +73,7 @@ func TestWoodpeckerTagDeliveryTemplate(t *testing.T) {
Delivery: &cli.DeliverySettings{BaseURL: server.URL, Owner: "owner", Repository: "example"}, Delivery: &cli.DeliverySettings{BaseURL: server.URL, Owner: "owner", Repository: "example"},
}) })
commit := pushTaggedCLIRepository(t, repository) commit := pushTaggedCLIRepository(t, repository)
state.commit = commit state.update(func(state *deliveryServerData) { state.commit = commit })
goDir, goLog := installGoBootstrapShim(t) goDir, goLog := installGoBootstrapShim(t)
env := templateEnvironment(repository, commit, goDir, goLog) env := templateEnvironment(repository, commit, goDir, goLog)
@@ -88,14 +88,14 @@ func TestWoodpeckerTagDeliveryTemplate(t *testing.T) {
t.Fatalf("second delivery did not reuse matching assets: %q", output) t.Fatalf("second delivery did not reuse matching assets: %q", output)
} }
} }
if state.releaseCreates != 1 || state.assetUploads != 2 { if state.snapshot().releaseCreates != 1 || state.snapshot().assetUploads != 2 {
t.Fatalf("binary calls = creates:%d uploads:%d", state.releaseCreates, state.assetUploads) t.Fatalf("binary calls = creates:%d uploads:%d", state.snapshot().releaseCreates, state.snapshot().assetUploads)
} }
if _, ok := state.assets["helper-v1.2.3-"+runtime.GOOS+"-"+runtime.GOARCH]; !ok { if _, ok := state.snapshot().assets["helper-v1.2.3-"+runtime.GOOS+"-"+runtime.GOARCH]; !ok {
t.Fatalf("binary asset was not delivered: %#v", state.assets) t.Fatalf("binary asset was not delivered: %#v", state.snapshot().assets)
} }
if _, ok := state.assets["SHA256SUMS"]; !ok { if _, ok := state.snapshot().assets["SHA256SUMS"]; !ok {
t.Fatalf("checksum manifest was not delivered: %#v", state.assets) t.Fatalf("checksum manifest was not delivered: %#v", state.snapshot().assets)
} }
assertModuleBootstrap(t, repository, goLog, moduleBefore, sumBefore) assertModuleBootstrap(t, repository, goLog, moduleBefore, sumBefore)
}) })

View File

@@ -24,9 +24,10 @@ func TestDeliverReadsLocalBinaryOnceForVerificationAndOnceForUpload(t *testing.T
} }
state := &deliveryBenchmarkServer{commit: build.CandidateCommit, assets: expected} state := &deliveryBenchmarkServer{commit: build.CandidateCommit, assets: expected}
state.reset(false) state.reset(false)
server := httptest.NewServer(http.HandlerFunc(state.serveHTTP)) server := httptest.NewUnstartedServer(http.HandlerFunc(state.serveHTTP))
state.baseURL = "http://" + server.Listener.Addr().String()
server.Start()
defer server.Close() defer server.Close()
state.baseURL = server.URL
result, err := releasekit.Deliver(t.Context(), releasekit.DeliveryRequest{ result, err := releasekit.Deliver(t.Context(), releasekit.DeliveryRequest{
RepositoryDir: repository, Product: "Sample App", Version: build.Version, RepositoryDir: repository, Product: "Sample App", Version: build.Version,
Destination: facadeDeliveryDestination(server.URL), Prebuilt: &build, Destination: facadeDeliveryDestination(server.URL), Prebuilt: &build,

View File

@@ -13,6 +13,7 @@ import (
"path/filepath" "path/filepath"
"runtime" "runtime"
"strings" "strings"
"sync"
"testing" "testing"
releasekit "gitea.maximumdirect.net/eric/ReleaseKit" releasekit "gitea.maximumdirect.net/eric/ReleaseKit"
@@ -41,11 +42,12 @@ func TestDeliverManagedBuildUploadsArtifactsAndCleansTemporaryOutput(t *testing.
if err != nil { if err != nil {
t.Fatalf("Deliver() error = %v; result = %#v", err, result) t.Fatalf("Deliver() error = %v; result = %#v", err, result)
} }
if result.State != releasekit.DeliveryComplete || result.CandidateCommit != remote.commit { observed := remote.snapshot()
if result.State != releasekit.DeliveryComplete || result.CandidateCommit != observed.commit {
t.Fatalf("delivery result = %#v", result) t.Fatalf("delivery result = %#v", result)
} }
if len(result.Uploaded) != 2 || len(remote.assets) != 2 { if len(result.Uploaded) != 2 || len(observed.assets) != 2 {
t.Fatalf("uploaded = %#v; hosted assets = %v", result.Uploaded, mapKeys(remote.assets)) t.Fatalf("uploaded = %#v; hosted assets = %v", result.Uploaded, mapKeys(observed.assets))
} }
if !strings.Contains(progress.String(), "checking fixture version output") { if !strings.Contains(progress.String(), "checking fixture version output") {
t.Fatalf("build progress = %q", progress.String()) t.Fatalf("build progress = %q", progress.String())
@@ -79,7 +81,8 @@ func TestDeliverBinaryConsumerWithBuildinfoAndSelectedTargets(t *testing.T) {
if err != nil { if err != nil {
t.Fatalf("Deliver() error = %v; result = %#v", err, result) t.Fatalf("Deliver() error = %v; result = %#v", err, result)
} }
if result.State != releasekit.DeliveryComplete || result.CandidateCommit != remote.commit { observed := remote.snapshot()
if result.State != releasekit.DeliveryComplete || result.CandidateCommit != observed.commit {
t.Fatalf("delivery result = %#v", result) t.Fatalf("delivery result = %#v", result)
} }
wantAssets := []string{ wantAssets := []string{
@@ -87,12 +90,12 @@ func TestDeliverBinaryConsumerWithBuildinfoAndSelectedTargets(t *testing.T) {
"fixture-v1.2.3-linux-arm64", "fixture-v1.2.3-linux-arm64",
"SHA256SUMS", "SHA256SUMS",
} }
if len(result.Uploaded) != len(wantAssets) || len(remote.assets) != len(wantAssets) { if len(result.Uploaded) != len(wantAssets) || len(observed.assets) != len(wantAssets) {
t.Fatalf("uploaded = %#v; hosted assets = %v", result.Uploaded, mapKeys(remote.assets)) t.Fatalf("uploaded = %#v; hosted assets = %v", result.Uploaded, mapKeys(observed.assets))
} }
for _, name := range wantAssets { for _, name := range wantAssets {
if _, ok := remote.assets[name]; !ok { if _, ok := observed.assets[name]; !ok {
t.Errorf("hosted assets omit %q: %v", name, mapKeys(remote.assets)) t.Errorf("hosted assets omit %q: %v", name, mapKeys(observed.assets))
} }
} }
if !strings.Contains(progress.String(), "checking fixture version output") { if !strings.Contains(progress.String(), "checking fixture version output") {
@@ -174,8 +177,8 @@ func TestDeliverRejectsSameSizePrebuiltTamperingBeforeHostedWrites(t *testing.T)
if err == nil || !strings.Contains(err.Error(), "verify build assets") || result.State != releasekit.DeliveryNotAttempted { if err == nil || !strings.Contains(err.Error(), "verify build assets") || result.State != releasekit.DeliveryNotAttempted {
t.Fatalf("Deliver() = %#v, %v; want local asset verification failure", result, err) t.Fatalf("Deliver() = %#v, %v; want local asset verification failure", result, err)
} }
if remote.requests != 0 { if remote.snapshot().requests != 0 {
t.Fatalf("hosted requests = %d, want none before local verification", remote.requests) t.Fatalf("hosted requests = %d, want none before local verification", remote.snapshot().requests)
} }
} }
@@ -217,8 +220,8 @@ func TestDeliverPageOnlySkipsBuildAndHostedAssets(t *testing.T) {
if err != nil || result.State != releasekit.DeliveryComplete { if err != nil || result.State != releasekit.DeliveryComplete {
t.Fatalf("Deliver() = %#v, %v", result, err) t.Fatalf("Deliver() = %#v, %v", result, err)
} }
if len(remote.assets) != 0 || len(result.Uploaded) != 0 || len(result.Pending) != 0 { if len(remote.snapshot().assets) != 0 || len(result.Uploaded) != 0 || len(result.Pending) != 0 {
t.Fatalf("page-only delivery included assets: result=%#v assets=%v", result, mapKeys(remote.assets)) t.Fatalf("page-only delivery included assets: result=%#v assets=%v", result, mapKeys(remote.snapshot().assets))
} }
if progress.Len() != 0 { if progress.Len() != 0 {
t.Fatalf("page-only delivery emitted build progress: %q", progress.String()) t.Fatalf("page-only delivery emitted build progress: %q", progress.String())
@@ -242,8 +245,8 @@ func TestDeliverUsesCommittedModuleIdentityForSeparatePages(t *testing.T) {
ExpectedTag: "integrations/wrong/v1.2.3", ExpectedTag: "integrations/wrong/v1.2.3",
Destination: facadeDeliveryDestination(server.URL), Destination: facadeDeliveryDestination(server.URL),
}) })
if err == nil || wrongTag.Tag != "v1.2.3" || remote.requests != 0 { if err == nil || wrongTag.Tag != "v1.2.3" || remote.snapshot().requests != 0 {
t.Fatalf("delivery with inconsistent expected tag = %#v, %v; hosted requests %d", wrongTag, err, remote.requests) t.Fatalf("delivery with inconsistent expected tag = %#v, %v; hosted requests %d", wrongTag, err, remote.snapshot().requests)
} }
rootResult, err := releasekit.Deliver(context.Background(), releasekit.DeliveryRequest{ rootResult, err := releasekit.Deliver(context.Background(), releasekit.DeliveryRequest{
@@ -258,8 +261,10 @@ func TestDeliverUsesCommittedModuleIdentityForSeparatePages(t *testing.T) {
} }
nestedTag := "integrations/ckconfig/v1.2.3" nestedTag := "integrations/ckconfig/v1.2.3"
remote.tag = nestedTag remote.update(func(remote *facadeDeliveryData) {
remote.noteBody = "# Fixture v1.2.3\n\nNested delivery fixture.\n" remote.tag = nestedTag
remote.noteBody = "# Fixture v1.2.3\n\nNested delivery fixture.\n"
})
nestedResult, err := releasekit.Deliver(context.Background(), releasekit.DeliveryRequest{ nestedResult, err := releasekit.Deliver(context.Background(), releasekit.DeliveryRequest{
RepositoryDir: repository, RepositoryDir: repository,
ModuleDir: "integrations/ckconfig", ModuleDir: "integrations/ckconfig",
@@ -271,8 +276,8 @@ func TestDeliverUsesCommittedModuleIdentityForSeparatePages(t *testing.T) {
if err != nil || nestedResult.State != releasekit.DeliveryComplete || nestedResult.Tag != nestedTag || nestedResult.ModulePath != "example.com/ckconfig" { if err != nil || nestedResult.State != releasekit.DeliveryComplete || nestedResult.Tag != nestedTag || nestedResult.ModulePath != "example.com/ckconfig" {
t.Fatalf("nested delivery = %#v, %v", nestedResult, err) t.Fatalf("nested delivery = %#v, %v", nestedResult, err)
} }
if len(remote.pages) != 2 || !remote.pages["v1.2.3"] || !remote.pages[nestedTag] { if len(remote.snapshot().pages) != 2 || !remote.snapshot().pages["v1.2.3"] || !remote.snapshot().pages[nestedTag] {
t.Fatalf("created hosted pages = %#v", remote.pages) t.Fatalf("created hosted pages = %#v", remote.snapshot().pages)
} }
} }
@@ -293,21 +298,21 @@ func TestDeliverRejectsPrebuiltArtifactsForAnotherModuleBeforeHostedRequests(t *
} }
server, remote := newFacadeDeliveryServer(t, repository) server, remote := newFacadeDeliveryServer(t, repository)
defer server.Close() defer server.Close()
remote.tag = "integrations/ckconfig/v1.2.3" remote.update(func(remote *facadeDeliveryData) { remote.tag = "integrations/ckconfig/v1.2.3" })
result, err := releasekit.Deliver(context.Background(), releasekit.DeliveryRequest{ result, err := releasekit.Deliver(context.Background(), releasekit.DeliveryRequest{
RepositoryDir: repository, RepositoryDir: repository,
ModuleDir: "integrations/ckconfig", ModuleDir: "integrations/ckconfig",
Product: "Fixture", Product: "Fixture",
Version: "v1.2.3", Version: "v1.2.3",
ExpectedTag: remote.tag, ExpectedTag: remote.snapshot().tag,
Destination: facadeDeliveryDestination(server.URL), Destination: facadeDeliveryDestination(server.URL),
Prebuilt: &prebuilt, Prebuilt: &prebuilt,
}) })
if err == nil || !strings.Contains(err.Error(), "selected tagged module") || result.Tag != remote.tag { if err == nil || !strings.Contains(err.Error(), "selected tagged module") || result.Tag != remote.snapshot().tag {
t.Fatalf("nested delivery with root artifacts = %#v, %v", result, err) t.Fatalf("nested delivery with root artifacts = %#v, %v", result, err)
} }
if remote.requests != 0 { if remote.snapshot().requests != 0 {
t.Fatalf("hosted requests before prebuilt identity rejection = %d", remote.requests) t.Fatalf("hosted requests before prebuilt identity rejection = %d", remote.snapshot().requests)
} }
} }
@@ -351,14 +356,14 @@ func TestDeliverRejectsDirtyTaggedSourceBeforeBuildOrHostedWrites(t *testing.T)
}, },
Output: &progress, Output: &progress,
}) })
if err == nil || result.State != releasekit.DeliveryNotAttempted || result.CandidateCommit != remote.commit { if err == nil || result.State != releasekit.DeliveryNotAttempted || result.CandidateCommit != remote.snapshot().commit {
t.Fatalf("Deliver() = %#v, %v; want preflight failure for known commit", result, err) t.Fatalf("Deliver() = %#v, %v; want preflight failure for known commit", result, err)
} }
if progress.Len() != 0 { if progress.Len() != 0 {
t.Fatalf("build started before source eligibility passed: %q", progress.String()) t.Fatalf("build started before source eligibility passed: %q", progress.String())
} }
if remote.requests != 0 { if remote.snapshot().requests != 0 {
t.Fatalf("hosted requests = %d, want none", remote.requests) t.Fatalf("hosted requests = %d, want none", remote.snapshot().requests)
} }
} }
@@ -392,8 +397,8 @@ func TestDeliverRejectsCheckoutAwayFromTagBeforeBuildOrHostedWrites(t *testing.T
if progress.Len() != 0 { if progress.Len() != 0 {
t.Fatalf("build started before tag/HEAD agreement: %q", progress.String()) t.Fatalf("build started before tag/HEAD agreement: %q", progress.String())
} }
if remote.requests != 0 { if remote.snapshot().requests != 0 {
t.Fatalf("hosted requests = %d, want none", remote.requests) t.Fatalf("hosted requests = %d, want none", remote.snapshot().requests)
} }
} }
@@ -417,14 +422,14 @@ func TestDeliverRejectsInvalidCommittedNoteBeforeBuildOrHostedWrites(t *testing.
}, },
Output: &progress, Output: &progress,
}) })
if err == nil || result.State != releasekit.DeliveryNotAttempted || result.CandidateCommit != remote.commit { if err == nil || result.State != releasekit.DeliveryNotAttempted || result.CandidateCommit != remote.snapshot().commit {
t.Fatalf("Deliver() = %#v, %v; want committed-note preflight failure", result, err) t.Fatalf("Deliver() = %#v, %v; want committed-note preflight failure", result, err)
} }
if progress.Len() != 0 { if progress.Len() != 0 {
t.Fatalf("build started before committed-note validation: %q", progress.String()) t.Fatalf("build started before committed-note validation: %q", progress.String())
} }
if remote.requests != 0 { if remote.snapshot().requests != 0 {
t.Fatalf("hosted requests = %d, want none", remote.requests) t.Fatalf("hosted requests = %d, want none", remote.snapshot().requests)
} }
} }
@@ -458,8 +463,8 @@ func TestDeliverManagedBuildRejectsCheckoutMutationBeforeHostedWrites(t *testing
if err == nil || !strings.Contains(err.Error(), "build source") || result.State != releasekit.DeliveryNotAttempted { if err == nil || !strings.Contains(err.Error(), "build source") || result.State != releasekit.DeliveryNotAttempted {
t.Fatalf("Deliver() = %#v, %v; want build-source eligibility failure", result, err) t.Fatalf("Deliver() = %#v, %v; want build-source eligibility failure", result, err)
} }
if remote.requests != 0 { if remote.snapshot().requests != 0 {
t.Fatalf("hosted requests = %d, want none", remote.requests) t.Fatalf("hosted requests = %d, want none", remote.snapshot().requests)
} }
if entries, err := os.ReadDir(temporaryRoot); err != nil || len(entries) != 0 { if entries, err := os.ReadDir(temporaryRoot); err != nil || len(entries) != 0 {
t.Fatalf("ineligible managed build output remains: entries=%v error=%v", entries, err) t.Fatalf("ineligible managed build output remains: entries=%v error=%v", entries, err)
@@ -484,11 +489,11 @@ func TestDeliverManagedBuildFailureCleansTemporaryOutputBeforeHostedWrites(t *te
Targets: []releasekit.Target{{OS: runtime.GOOS, Arch: runtime.GOARCH}}, Targets: []releasekit.Target{{OS: runtime.GOOS, Arch: runtime.GOARCH}},
}, },
}) })
if err == nil || result.State != releasekit.DeliveryNotAttempted || result.CandidateCommit != remote.commit { if err == nil || result.State != releasekit.DeliveryNotAttempted || result.CandidateCommit != remote.snapshot().commit {
t.Fatalf("Deliver() = %#v, %v; want managed build failure before hosted writes", result, err) t.Fatalf("Deliver() = %#v, %v; want managed build failure before hosted writes", result, err)
} }
if remote.requests != 0 { if remote.snapshot().requests != 0 {
t.Fatalf("hosted requests = %d, want none", remote.requests) t.Fatalf("hosted requests = %d, want none", remote.snapshot().requests)
} }
if entries, err := os.ReadDir(temporaryRoot); err != nil || len(entries) != 0 { if entries, err := os.ReadDir(temporaryRoot); err != nil || len(entries) != 0 {
t.Fatalf("failed managed build output remains: entries=%v error=%v", entries, err) t.Fatalf("failed managed build output remains: entries=%v error=%v", entries, err)
@@ -520,8 +525,8 @@ func TestDeliverManagedBuildCancellationCleansTemporaryOutput(t *testing.T) {
if !errors.Is(err, context.Canceled) || result.State != releasekit.DeliveryNotAttempted { if !errors.Is(err, context.Canceled) || result.State != releasekit.DeliveryNotAttempted {
t.Fatalf("Deliver() = %#v, %v; want cancellation before hosted writes", result, err) t.Fatalf("Deliver() = %#v, %v; want cancellation before hosted writes", result, err)
} }
if remote.requests != 0 { if remote.snapshot().requests != 0 {
t.Fatalf("hosted requests = %d, want none", remote.requests) t.Fatalf("hosted requests = %d, want none", remote.snapshot().requests)
} }
if entries, err := os.ReadDir(temporaryRoot); err != nil || len(entries) != 0 { if entries, err := os.ReadDir(temporaryRoot); err != nil || len(entries) != 0 {
t.Fatalf("canceled managed build output remains: entries=%v error=%v", entries, err) t.Fatalf("canceled managed build output remains: entries=%v error=%v", entries, err)
@@ -544,8 +549,8 @@ func TestDeliverRejectsBothBuildSourcesBeforeHostedWrites(t *testing.T) {
if err == nil || result.State != releasekit.DeliveryNotAttempted || !strings.Contains(err.Error(), "both") { if err == nil || result.State != releasekit.DeliveryNotAttempted || !strings.Contains(err.Error(), "both") {
t.Fatalf("Deliver() = %#v, %v; want invalid build-source combination", result, err) t.Fatalf("Deliver() = %#v, %v; want invalid build-source combination", result, err)
} }
if remote.requests != 0 { if remote.snapshot().requests != 0 {
t.Fatalf("hosted requests = %d, want none", remote.requests) t.Fatalf("hosted requests = %d, want none", remote.snapshot().requests)
} }
} }
@@ -582,6 +587,35 @@ func (writer deliveryCancelWriter) Write(content []byte) (int, error) {
} }
type facadeDeliveryServer struct { type facadeDeliveryServer struct {
mu sync.Mutex
data facadeDeliveryData
}
func (state *facadeDeliveryServer) update(change func(*facadeDeliveryData)) {
state.mu.Lock()
defer state.mu.Unlock()
change(&state.data)
}
func (state *facadeDeliveryServer) snapshot() facadeDeliveryData {
state.mu.Lock()
defer state.mu.Unlock()
result := state.data
result.assets = make(map[string][]byte, len(state.data.assets))
for key, value := range state.data.assets {
result.assets[key] = append([]byte(nil), value...)
}
result.pages = make(map[string]bool, len(state.data.pages))
for key, value := range state.data.pages {
result.pages[key] = value
}
return result
}
type facadeDeliveryData struct {
t *testing.T t *testing.T
commit string commit string
tag string tag string
@@ -595,24 +629,28 @@ type facadeDeliveryServer struct {
func newFacadeDeliveryServer(t *testing.T, repository string) (*httptest.Server, *facadeDeliveryServer) { func newFacadeDeliveryServer(t *testing.T, repository string) (*httptest.Server, *facadeDeliveryServer) {
t.Helper() t.Helper()
state := &facadeDeliveryServer{t: t, commit: buildGit(t, repository, "rev-parse", "HEAD"), tag: "v1.2.3", version: "v1.2.3", noteBody: "# Fixture v1.2.3\n\nDelivery fixture.\n", pages: make(map[string]bool), assets: make(map[string][]byte)} state := &facadeDeliveryServer{data: facadeDeliveryData{t: t, commit: buildGit(t, repository, "rev-parse", "HEAD"), tag: "v1.2.3", version: "v1.2.3", noteBody: "# Fixture v1.2.3\n\nDelivery fixture.\n", pages: make(map[string]bool), assets: make(map[string][]byte)}}
server := httptest.NewServer(http.HandlerFunc(state.serveHTTP)) server := httptest.NewUnstartedServer(http.HandlerFunc(state.serveHTTP))
state.url = server.URL state.data.url = "http://" + server.Listener.Addr().String()
server.Start()
return server, state return server, state
} }
func (state *facadeDeliveryServer) serveHTTP(w http.ResponseWriter, r *http.Request) { func (state *facadeDeliveryServer) serveHTTP(w http.ResponseWriter, r *http.Request) {
state.mu.Lock()
defer state.mu.Unlock()
data := &state.data
if strings.HasPrefix(r.URL.Path, "/api/") { if strings.HasPrefix(r.URL.Path, "/api/") {
state.requests++ data.requests++
if r.Header.Get("Authorization") != "token fixture-token" { if r.Header.Get("Authorization") != "token fixture-token" {
state.t.Errorf("authorization = %q", r.Header.Get("Authorization")) data.t.Errorf("authorization = %q", r.Header.Get("Authorization"))
} }
} }
switch r.URL.EscapedPath() { switch r.URL.EscapedPath() {
case "/api/v1/repos/owner/example/tags/" + url.PathEscape(state.tag): case "/api/v1/repos/owner/example/tags/" + url.PathEscape(data.tag):
_, _ = fmt.Fprintf(w, `{"name":%q,"commit":{"sha":%q}}`, state.tag, state.commit) _, _ = fmt.Fprintf(w, `{"name":%q,"commit":{"sha":%q}}`, data.tag, data.commit)
case "/api/v1/repos/owner/example/releases/tags/" + url.PathEscape(state.tag): case "/api/v1/repos/owner/example/releases/tags/" + url.PathEscape(data.tag):
if !state.pages[state.tag] { if !data.pages[data.tag] {
http.NotFound(w, r) http.NotFound(w, r)
return return
} }
@@ -625,20 +663,20 @@ func (state *facadeDeliveryServer) serveHTTP(w http.ResponseWriter, r *http.Requ
Body string `json:"body"` Body string `json:"body"`
} }
if err := json.NewDecoder(r.Body).Decode(&payload); err != nil { if err := json.NewDecoder(r.Body).Decode(&payload); err != nil {
state.t.Errorf("decode release payload: %v", err) data.t.Errorf("decode release payload: %v", err)
http.Error(w, "invalid payload", http.StatusBadRequest) http.Error(w, "invalid payload", http.StatusBadRequest)
return return
} }
if payload.Tag != state.tag || payload.Target != state.commit || payload.Name != "Fixture "+state.version { if payload.Tag != data.tag || payload.Target != data.commit || payload.Name != "Fixture "+data.version {
state.t.Errorf("release payload = %#v", payload) data.t.Errorf("release payload = %#v", payload)
} }
state.pages[state.tag] = true data.pages[data.tag] = true
state.writeRelease(w) state.writeRelease(w)
case "/api/v1/repos/owner/example/releases/10/assets": case "/api/v1/repos/owner/example/releases/10/assets":
if r.Method == http.MethodGet { if r.Method == http.MethodGet {
list := make([]map[string]any, 0, len(state.assets)) list := make([]map[string]any, 0, len(data.assets))
for name, content := range state.assets { for name, content := range data.assets {
list = append(list, map[string]any{"id": len(list) + 1, "name": name, "size": len(content), "browser_download_url": state.url + "/files/" + name}) list = append(list, map[string]any{"id": len(list) + 1, "name": name, "size": len(content), "browser_download_url": data.url + "/files/" + name})
} }
_ = json.NewEncoder(w).Encode(list) _ = json.NewEncoder(w).Encode(list)
return return
@@ -646,22 +684,22 @@ func (state *facadeDeliveryServer) serveHTTP(w http.ResponseWriter, r *http.Requ
name := r.URL.Query().Get("name") name := r.URL.Query().Get("name")
file, _, err := r.FormFile("attachment") file, _, err := r.FormFile("attachment")
if err != nil { if err != nil {
state.t.Errorf("read upload %q: %v", name, err) data.t.Errorf("read upload %q: %v", name, err)
http.Error(w, "invalid multipart upload", http.StatusBadRequest) http.Error(w, "invalid multipart upload", http.StatusBadRequest)
return return
} }
content, err := io.ReadAll(file) content, err := io.ReadAll(file)
_ = file.Close() _ = file.Close()
if err != nil { if err != nil {
state.t.Errorf("read upload %q: %v", name, err) data.t.Errorf("read upload %q: %v", name, err)
} }
state.assets[name] = content data.assets[name] = content
w.WriteHeader(http.StatusCreated) w.WriteHeader(http.StatusCreated)
_ = json.NewEncoder(w).Encode(map[string]any{"id": len(state.assets), "name": name, "size": len(content)}) _ = json.NewEncoder(w).Encode(map[string]any{"id": len(data.assets), "name": name, "size": len(content)})
default: default:
if strings.HasPrefix(r.URL.Path, "/files/") { if strings.HasPrefix(r.URL.Path, "/files/") {
name := strings.TrimPrefix(r.URL.Path, "/files/") name := strings.TrimPrefix(r.URL.Path, "/files/")
content, ok := state.assets[name] content, ok := data.assets[name]
if !ok { if !ok {
http.NotFound(w, r) http.NotFound(w, r)
return return
@@ -669,17 +707,19 @@ func (state *facadeDeliveryServer) serveHTTP(w http.ResponseWriter, r *http.Requ
_, _ = w.Write(content) _, _ = w.Write(content)
return return
} }
state.t.Errorf("unexpected delivery request %s %s", r.Method, r.URL) data.t.Errorf("unexpected delivery request %s %s", r.Method, r.URL)
http.NotFound(w, r) http.NotFound(w, r)
} }
} }
func (state *facadeDeliveryServer) writeRelease(w http.ResponseWriter) { func (state *facadeDeliveryServer) writeRelease(w http.ResponseWriter) {
// The request handler holds state.mu.
data := &state.data
w.Header().Set("Content-Type", "application/json") w.Header().Set("Content-Type", "application/json")
_ = json.NewEncoder(w).Encode(map[string]any{ _ = json.NewEncoder(w).Encode(map[string]any{
"id": 10, "html_url": state.url + "/releases/10", "tag_name": state.tag, "id": 10, "html_url": data.url + "/releases/10", "tag_name": data.tag,
"target_commitish": state.commit, "name": "Fixture " + state.version, "target_commitish": data.commit, "name": "Fixture " + data.version,
"body": state.noteBody, "draft": false, "prerelease": false, "body": data.noteBody, "draft": false, "prerelease": false,
}) })
} }

152
docs/roadmap/audit.md Normal file
View File

@@ -0,0 +1,152 @@
# Targeted concurrency audit
Status: complete; all five finding groups corrected and verified.
## Scope and baseline
Reviewed ReleaseKit at `6176971`, including the
[PromptKit report](promptkit-bug-report.md) and
[fixture synchronization roadmap](race.md). The audit covered shared-state
ownership in HTTP fixtures, background goroutines, cancellation callbacks,
subprocess-driven tests, output capture, and production operation callbacks.
Code discovery used the codebase graph, with source inspection and filesystem
searches where the graph omitted recent files. Reviewed current source and
fixture consumers, including assertion and diagnostic reads. The full offline
race-enabled baseline used Go `go1.26.8 linux/amd64`:
```sh
GOTOOLCHAIN=local GOWORK=off GOPROXY=off GOSUMDB=off \
go test -race -mod=readonly -count=1 ./...
```
The recorded baseline failed in
`TestReleaseKitSourceDeliveryWorkflowFromTaggedCheckout`. It reported four race
diagnostics involving three fields: `commit`, `remoteCommit`, and
`releaseCreates`. Other packages passed. An earlier redirected invocation
returned unsuccessfully without diagnostics and supplied no usable baseline;
the complete results above came from a direct invocation.
The additional findings below were established by source inspection. Their
absence from this baseline's diagnostics reflects its exercised schedules.
## Findings and corrections
### 1. CLI delivery fixtures share mutable state without synchronization
Status: corrected; original reported defect and related fixture accesses.
The HTTP handlers in [main_test.go](../../cmd/releasekit/main_test.go) read
configuration and update page state, counters, note content, and assets while
tests configure and inspect that state. The
[mapped workflow fixture](../../cmd/releasekit/multi_module_woodpecker_test.go)
has the same ownership pattern. Subprocess completion supplies no shared-state
synchronization with the parent process's HTTP goroutines.
**Correction:** Each fixture owns a mutex and a separate plain state value.
Handlers serialize state access; test configuration uses locked updates, and
assertions and diagnostics use locked snapshots. Snapshots copy asset maps and
byte slices so inspection retains independent values. All consumers use that
boundary, including source-only, binary, sparse-checkout, alias, failure,
ordinary Woodpecker, and mapped-module tests. Response helpers run under the
handler's lock. The CLI fixture derives response URLs from the request; mapped
fixture configuration is initialized before its server starts.
### 2. The public delivery fixture has the same unsynchronized ownership
Status: corrected; additional finding.
The fixture in [delivery_test.go](../../delivery_test.go) shares request counts,
tags, notes, release-page maps, and asset maps between handlers and test code.
The module-page test also changes the selected tag and note between deliveries.
Its consumers include [repository resolution](../../repository_test.go) and
[partial-clone inspection](../../source_inspection_test.go) tests.
**Correction:** Added a fixture-local mutex, locked updates, and independent
snapshots, including copied page and asset maps. Tag and note changes occur in
one locked update. The server URL is initialized before serving requests.
Applied the same startup ordering to the existing synchronized benchmark server
and its [artifact-read regression test](../../delivery_reads_linux_test.go).
Binary assertion groups reuse their captured snapshot to avoid repeated asset
copies.
### 3. Gitea adapter fixtures expose unsynchronized counters and flags
Status: corrected; additional finding.
[Release fixtures](../../internal/gitea/release_test.go) share creation and
lookup counts, creation flags, and cancellation observations between handlers
and assertions. Lost-acknowledgment scenarios also carry state across separate
HTTP connections. The
[external attachment fixture](../../internal/gitea/assets_test.go) shares its
download counter with the test goroutine.
**Correction:** Used standard-library atomic counters and flags for these
independent scalar observations. Handler updates and assertion/diagnostic reads
use their atomic methods. Existing exact counts, conflict handling, recovery,
and external-origin credential assertions remain in place.
### 4. HTTP assertions invoke fatal methods from request goroutines
Status: corrected; additional finding.
Gitea release and asset handlers, including their assertion helpers, used
`testing.T.Fatal` and `Fatalf`. Those methods terminate the calling goroutine
and belong to the test goroutine. In a handler, they can abandon request
processing and produce misleading transport failures.
**Correction:** Handler assertions report through `Error` or `Errorf` and
return from the affected handler or helper. Fatal assertions remain in the test
goroutine. The fixtures still mark every violated assertion as a test failure.
### 5. A release fixture omits server lifetime cleanup
Status: corrected; additional finding.
`newReleaseServer` returned an active HTTP server without registering cleanup,
and several callers omitted closure. This left listeners and serving goroutines
alive after their owning test finished.
**Correction:** The helper registers `server.Close` with `t.Cleanup`, giving
server shutdown one owner for all its consumers.
## Production and other reviewed paths
The audit found no confirmed production concurrency defect in these paths:
| Boundary | Ownership and synchronization evidence |
| --- | --- |
| [Git blob reader](../../internal/git/blob_reader.go) | A mutex serializes reads, close, and terminal state. A cancellation callback closes process pipes, and the read operation joins a started callback before continuing. Process diagnostics are inspected after child wait. |
| [Process adapter](../../internal/process/process.go) | Process handles are initialized before publication; cancellation uses a context. `sync.Once` protects child wait and its result. Ordinary command completion joins output-copy workers before returning captures. |
| [Go output capture](../../internal/gotool/gotool.go) | One mutex protects combined output, forwarding, and writer errors. Capture ownership transfers after the process finishes writing. |
| [Gitea upload](../../internal/gitea/upload.go) | The multipart worker owns the writer and file reader. An error channel transfers completion; request failure closes the pipe and joins the worker. |
| [Runtime metadata](../../internal/runtimeinfo/version.go) | The injected value is set at link time; fallback initialization uses `sync.OnceValue`. Existing concurrent accessor coverage uses atomics, channels, and a wait group. |
| [Go settings owner](../../internal/gotool/owner.go), candidate validation, and publication callbacks | Mutable owners and validation caches belong to one operation. Validation, product callbacks, and subsequent builds execute sequentially. Internal mutable owners remain behind the facade. |
| Background test workers | Reviewed build/process/dependency cancellation, Git-reader lifecycle, publication watchers, and destination-resolution workers. Results transfer through channels; the publication watcher has an explicit completion join. |
| Other HTTP fixtures | Immutable asset payloads remain read-only. The upload fixture already locks files and observations and returns copied POST names. The module-proxy fixture uses an atomic request counter. |
| Process environment | Tests changing environment settings run sequentially within their package; subprocess overlays belong to individual invocations. |
These conclusions apply to the reviewed ownership paths. Race-enabled execution
and inspection supply evidence for this audit, rather than a guarantee about
every possible caller or schedule. Production files and public API contracts
were unchanged by the corrections.
## Verification and completion
The affected source-delivery, ordinary Woodpecker, and mapped-module workflows
passed with the race detector after fixture synchronization. The Gitea release
and asset checks also passed during focused verification. Existing assertions
continue to cover tag mismatch rejection, source preservation, complete notes,
idempotent delivery, asset counts, conflicts, and recovery.
**Completed:** The final full offline race-enabled suite passed across all
packages with Go 1.26.8, including the original failing workflow and every
fixture consumer. The compiled public API/dependency check passed. The corrected
handlers, configuration updates, snapshots, diagnostics, counters, and server
startup/cleanup were reviewed against their ownership boundaries. Local
documentation links and fragments, Go formatting, and diff whitespace passed.
The linked roadmap and bug report record the completed remediation.
The audit and corrections are local development work. Future publication follows
the existing [release procedure](../release.md), with the published v0.4.0
contents preserved.

View File

@@ -1,6 +1,8 @@
# PromptKit Bug Report: Race In Source-Delivery Test Fixture # PromptKit Bug Report: Race In Source-Delivery Test Fixture
Status: reported for upstream investigation; no implementation changes made. Status: remediated in current source; focused and full offline race-enabled
verification passed. See the [concurrency audit](audit.md) for corrections and
additional findings. The original report below records the reviewed release.
## Reviewed Release And Environment ## Reviewed Release And Environment

View File

@@ -1,6 +1,9 @@
# Roadmap: synchronize delivery test fixtures # Roadmap: synchronize delivery test fixtures
Status: planned; implementation has not started. Status: complete; fixture synchronization and race-enabled verification passed.
The [concurrency audit](audit.md) records the wider review, additional findings,
corrections, and final verification evidence.
## Objective and evidence ## Objective and evidence
@@ -101,3 +104,10 @@ This verification addresses the reported defect. Future release validation
continues to follow the [release procedure](../release.md). The correction is continues to follow the [release procedure](../release.md). The correction is
suitable for a subsequent patch release; preparation and publication are separate suitable for a subsequent patch release; preparation and publication are separate
work under that procedure, preserving the published v0.4.0 contents. work under that procedure, preserving the published v0.4.0 contents.
**Completed:** Both delivery fixtures and all their consumers use mutex-protected
updates and independent snapshots. Existing mismatch, note, asset, retry, and
source-preservation assertions remain in place. Focused workflow checks and the
full offline race-enabled suite passed with Go 1.26.8; the compiled API/dependency
check passed. The audit also corrected public delivery fixture ownership,
Gitea scalar observations, HTTP assertion handling, and server cleanup.

View File

@@ -47,7 +47,8 @@ func TestInspectAssetsAcceptsMatchingContentAndPreservesUnrelatedFiles(t *testin
case "/files/notes": case "/files/notes":
_, _ = w.Write([]byte("unrelated")) _, _ = w.Write([]byte("unrelated"))
default: default:
t.Fatalf("unexpected path %s", r.URL.Path) t.Errorf("unexpected path %s", r.URL.Path)
return
} }
})) }))
defer server.Close() defer server.Close()
@@ -107,7 +108,8 @@ func TestInspectAssetsAcceptsFullManifestWhenBinariesAreMissing(t *testing.T) {
return return
} }
if r.URL.Path != "/files/manifest" { if r.URL.Path != "/files/manifest" {
t.Fatalf("unexpected path %s", r.URL.Path) t.Errorf("unexpected path %s", r.URL.Path)
return
} }
_, _ = w.Write(manifest.Content) _, _ = w.Write(manifest.Content)
})) }))
@@ -237,7 +239,8 @@ func TestInspectAssetsFollowsSameOriginRedirectWithAuthorization(t *testing.T) {
_, _ = w.Write(content) _, _ = w.Write(content)
return return
} }
t.Fatalf("unexpected path %s", r.URL.Path) t.Errorf("unexpected path %s", r.URL.Path)
return
})) }))
defer server.Close() defer server.Close()
inspection, err := testClient(server).InspectAssets(context.Background(), testDestination(server.URL), 10, []ExpectedAsset{expected}) inspection, err := testClient(server).InspectAssets(context.Background(), testDestination(server.URL), 10, []ExpectedAsset{expected})
@@ -253,9 +256,9 @@ func TestInspectAssetsVerifiesExternalContentWithoutGiteaToken(t *testing.T) {
for _, mode := range []string{"direct", "redirect", "conflicting"} { for _, mode := range []string{"direct", "redirect", "conflicting"} {
t.Run(mode, func(t *testing.T) { t.Run(mode, func(t *testing.T) {
expected := assetFor("app.bin", []byte("matching bytes")) expected := assetFor("app.bin", []byte("matching bytes"))
externalGets := 0 var externalGets atomic.Int64
external := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { external := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
externalGets++ externalGets.Add(1)
if r.Header.Get("Authorization") != "" { if r.Header.Get("Authorization") != "" {
t.Error("Gitea token reached external host") t.Error("Gitea token reached external host")
} }
@@ -287,8 +290,8 @@ func TestInspectAssetsVerifiesExternalContentWithoutGiteaToken(t *testing.T) {
if mode == "conflicting" { if mode == "conflicting" {
want = AssetConflicting want = AssetConflicting
} }
if inspection.Assets[0].State != want || externalGets != 1 { if inspection.Assets[0].State != want || externalGets.Load() != 1 {
t.Fatalf("inspection=%#v downloads=%d", inspection, externalGets) t.Fatalf("inspection=%#v downloads=%d", inspection, externalGets.Load())
} }
}) })
} }

View File

@@ -8,6 +8,7 @@ import (
"net/http" "net/http"
"net/http/httptest" "net/http/httptest"
"strings" "strings"
"sync/atomic"
"testing" "testing"
) )
@@ -40,19 +41,23 @@ func TestEnsureReleaseEscapesSlashContainingTag(t *testing.T) {
http.NotFound(w, r) http.NotFound(w, r)
case "/api/v1/repos/owner/project/releases": case "/api/v1/repos/owner/project/releases":
assertRequest(t, r, http.MethodPost, r.URL.Path) assertRequest(t, r, http.MethodPost, r.URL.Path)
state.posts++ state.posts.Add(1)
var payload createReleaseRequest var payload createReleaseRequest
if err := json.NewDecoder(r.Body).Decode(&payload); err != nil { if err := json.NewDecoder(r.Body).Decode(&payload); err != nil {
t.Fatal(err) t.Error(err)
return
} }
if payload.TagName != tag || payload.Name != "Example v1.2.3" || payload.Body != testNote { if payload.TagName != tag || payload.Name != "Example v1.2.3" || payload.Body != testNote {
t.Fatalf("create payload = %#v", payload) t.Errorf("create payload = %#v", payload)
return
} }
if err := json.NewEncoder(w).Encode(releaseResponse{ID: 7, HTMLURL: "https://example.test/release", TagName: tag, TargetCommitish: testCommit, Name: payload.Name, Body: payload.Body}); err != nil { if err := json.NewEncoder(w).Encode(releaseResponse{ID: 7, HTMLURL: "https://example.test/release", TagName: tag, TargetCommitish: testCommit, Name: payload.Name, Body: payload.Body}); err != nil {
t.Fatal(err) t.Error(err)
return
} }
default: default:
t.Fatalf("unexpected escaped request %s %s", r.Method, r.URL.EscapedPath()) t.Errorf("unexpected escaped request %s %s", r.Method, r.URL.EscapedPath())
return
} }
})) }))
defer server.Close() defer server.Close()
@@ -60,8 +65,8 @@ func TestEnsureReleaseEscapesSlashContainingTag(t *testing.T) {
request := testRequest(server.URL) request := testRequest(server.URL)
request.Tag = tag request.Tag = tag
result, err := testClient(server).EnsureRelease(context.Background(), request) result, err := testClient(server).EnsureRelease(context.Background(), request)
if err != nil || !result.Created || state.posts != 1 { if err != nil || !result.Created || state.posts.Load() != 1 {
t.Fatalf("nested tag release = %#v, posts %d, error %v", result, state.posts, err) t.Fatalf("nested tag release = %#v, posts %d, error %v", result, state.posts.Load(), err)
} }
} }
@@ -82,7 +87,7 @@ func TestEnsureReleaseConflictDoesNotEditExistingPage(t *testing.T) {
if failureKind(t, err) != FailureConflict { if failureKind(t, err) != FailureConflict {
t.Fatalf("error = %v", err) t.Fatalf("error = %v", err)
} }
if got := state.posts; got != 0 { if got := state.posts.Load(); got != 0 {
t.Fatalf("release creates = %d, want 0", got) t.Fatalf("release creates = %d, want 0", got)
} }
} }
@@ -103,68 +108,68 @@ func TestEnsureReleaseUnauthorizedIsStructuredAndHidesToken(t *testing.T) {
} }
func TestEnsureReleaseMalformedTagResponseStopsBeforeCreate(t *testing.T) { func TestEnsureReleaseMalformedTagResponseStopsBeforeCreate(t *testing.T) {
posts := 0 var posts atomic.Int64
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method == http.MethodPost { if r.Method == http.MethodPost {
posts++ posts.Add(1)
} }
assertRequest(t, r, http.MethodGet, "/api/v1/repos/owner/project/tags/v1.2.3") assertRequest(t, r, http.MethodGet, "/api/v1/repos/owner/project/tags/v1.2.3")
_, _ = w.Write([]byte("{")) _, _ = w.Write([]byte("{"))
})) }))
defer server.Close() defer server.Close()
_, err := testClient(server).EnsureRelease(context.Background(), testRequest(server.URL)) _, err := testClient(server).EnsureRelease(context.Background(), testRequest(server.URL))
if failureKind(t, err) != FailureMalformed || posts != 0 { if failureKind(t, err) != FailureMalformed || posts.Load() != 0 {
t.Fatalf("error = %v, posts = %d", err, posts) t.Fatalf("error = %v, posts = %d", err, posts.Load())
} }
} }
func TestEnsureReleaseWrongHostedTagStopsBeforeCreate(t *testing.T) { func TestEnsureReleaseWrongHostedTagStopsBeforeCreate(t *testing.T) {
posts := 0 var posts atomic.Int64
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
assertRequest(t, r, http.MethodGet, "/api/v1/repos/owner/project/tags/v1.2.3") assertRequest(t, r, http.MethodGet, "/api/v1/repos/owner/project/tags/v1.2.3")
if r.Method == http.MethodPost { if r.Method == http.MethodPost {
posts++ posts.Add(1)
} }
_, _ = fmt.Fprintf(w, `{"commit":{"sha":"%040d"}}`, 7) _, _ = fmt.Fprintf(w, `{"commit":{"sha":"%040d"}}`, 7)
})) }))
defer server.Close() defer server.Close()
_, err := testClient(server).EnsureRelease(context.Background(), testRequest(server.URL)) _, err := testClient(server).EnsureRelease(context.Background(), testRequest(server.URL))
if failureKind(t, err) != FailureConflict || posts != 0 { if failureKind(t, err) != FailureConflict || posts.Load() != 0 {
t.Fatalf("error = %v, posts = %d", err, posts) t.Fatalf("error = %v, posts = %d", err, posts.Load())
} }
} }
func TestEnsureReleaseMissingHostedTagStopsBeforeCreate(t *testing.T) { func TestEnsureReleaseMissingHostedTagStopsBeforeCreate(t *testing.T) {
posts := 0 var posts atomic.Int64
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
assertRequest(t, r, http.MethodGet, "/api/v1/repos/owner/project/tags/v1.2.3") assertRequest(t, r, http.MethodGet, "/api/v1/repos/owner/project/tags/v1.2.3")
if r.Method == http.MethodPost { if r.Method == http.MethodPost {
posts++ posts.Add(1)
} }
http.NotFound(w, r) http.NotFound(w, r)
})) }))
defer server.Close() defer server.Close()
_, err := testClient(server).EnsureRelease(context.Background(), testRequest(server.URL)) _, err := testClient(server).EnsureRelease(context.Background(), testRequest(server.URL))
if failureKind(t, err) != FailureNotFound || posts != 0 { if failureKind(t, err) != FailureNotFound || posts.Load() != 0 {
t.Fatalf("error = %v, posts = %d", err, posts) t.Fatalf("error = %v, posts = %d", err, posts.Load())
} }
} }
func TestEnsureReleaseHonorsContextCancellation(t *testing.T) { func TestEnsureReleaseHonorsContextCancellation(t *testing.T) {
called := false var called atomic.Bool
server := httptest.NewServer(http.HandlerFunc(func(http.ResponseWriter, *http.Request) { called = true })) server := httptest.NewServer(http.HandlerFunc(func(http.ResponseWriter, *http.Request) { called.Store(true) }))
defer server.Close() defer server.Close()
ctx, cancel := context.WithCancel(context.Background()) ctx, cancel := context.WithCancel(context.Background())
cancel() cancel()
_, err := testClient(server).EnsureRelease(ctx, testRequest(server.URL)) _, err := testClient(server).EnsureRelease(ctx, testRequest(server.URL))
if !errors.Is(err, context.Canceled) || called { if !errors.Is(err, context.Canceled) || called.Load() {
t.Fatalf("error = %v, server called = %t", err, called) t.Fatalf("error = %v, server called = %t", err, called.Load())
} }
} }
func TestEnsureReleaseResolvesLostCreateAcknowledgmentWithOneLookup(t *testing.T) { func TestEnsureReleaseResolvesLostCreateAcknowledgmentWithOneLookup(t *testing.T) {
created := false var created atomic.Bool
lookups := 0 var lookups atomic.Int64
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
assertAuth(t, r) assertAuth(t, r)
switch r.URL.Path { switch r.URL.Path {
@@ -173,8 +178,8 @@ func TestEnsureReleaseResolvesLostCreateAcknowledgmentWithOneLookup(t *testing.T
writeTag(t, w, testCommit) writeTag(t, w, testCommit)
case "/api/v1/repos/owner/project/releases/tags/v1.2.3": case "/api/v1/repos/owner/project/releases/tags/v1.2.3":
assertRequest(t, r, http.MethodGet, r.URL.Path) assertRequest(t, r, http.MethodGet, r.URL.Path)
lookups++ lookups.Add(1)
if !created { if !created.Load() {
http.NotFound(w, r) http.NotFound(w, r)
return return
} }
@@ -183,23 +188,28 @@ func TestEnsureReleaseResolvesLostCreateAcknowledgmentWithOneLookup(t *testing.T
assertRequest(t, r, http.MethodPost, r.URL.Path) assertRequest(t, r, http.MethodPost, r.URL.Path)
var payload createReleaseRequest var payload createReleaseRequest
if err := json.NewDecoder(r.Body).Decode(&payload); err != nil { if err := json.NewDecoder(r.Body).Decode(&payload); err != nil {
t.Fatal(err) t.Error(err)
return
} }
if payload.TagName != "v1.2.3" || payload.TargetCommitish != testCommit || payload.Name != "Example v1.2.3" || payload.Body != testNote || payload.Draft || payload.Prerelease { if payload.TagName != "v1.2.3" || payload.TargetCommitish != testCommit || payload.Name != "Example v1.2.3" || payload.Body != testNote || payload.Draft || payload.Prerelease {
t.Fatalf("create payload = %#v", payload) t.Errorf("create payload = %#v", payload)
return
} }
created = true created.Store(true)
hijacker, ok := w.(http.Hijacker) hijacker, ok := w.(http.Hijacker)
if !ok { if !ok {
t.Fatal("HTTP writer does not support hijacking") t.Error("HTTP writer does not support hijacking")
return
} }
connection, _, err := hijacker.Hijack() connection, _, err := hijacker.Hijack()
if err != nil { if err != nil {
t.Fatal(err) t.Error(err)
return
} }
_ = connection.Close() _ = connection.Close()
default: default:
t.Fatalf("unexpected request %s %s", r.Method, r.URL.Path) t.Errorf("unexpected request %s %s", r.Method, r.URL.Path)
return
} }
})) }))
defer server.Close() defer server.Close()
@@ -207,13 +217,13 @@ func TestEnsureReleaseResolvesLostCreateAcknowledgmentWithOneLookup(t *testing.T
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
if result.Created || lookups != 2 { if result.Created || lookups.Load() != 2 {
t.Fatalf("result = %#v, release lookups = %d", result, lookups) t.Fatalf("result = %#v, release lookups = %d", result, lookups.Load())
} }
} }
func TestEnsureReleaseResolvesCreateConflictWithOneLookup(t *testing.T) { func TestEnsureReleaseResolvesCreateConflictWithOneLookup(t *testing.T) {
lookups := 0 var lookups atomic.Int64
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
assertAuth(t, r) assertAuth(t, r)
switch r.URL.Path { switch r.URL.Path {
@@ -222,8 +232,8 @@ func TestEnsureReleaseResolvesCreateConflictWithOneLookup(t *testing.T) {
writeTag(t, w, testCommit) writeTag(t, w, testCommit)
case "/api/v1/repos/owner/project/releases/tags/v1.2.3": case "/api/v1/repos/owner/project/releases/tags/v1.2.3":
assertRequest(t, r, http.MethodGet, r.URL.Path) assertRequest(t, r, http.MethodGet, r.URL.Path)
lookups++ lookups.Add(1)
if lookups == 1 { if lookups.Load() == 1 {
http.NotFound(w, r) http.NotFound(w, r)
return return
} }
@@ -232,7 +242,8 @@ func TestEnsureReleaseResolvesCreateConflictWithOneLookup(t *testing.T) {
assertRequest(t, r, http.MethodPost, r.URL.Path) assertRequest(t, r, http.MethodPost, r.URL.Path)
w.WriteHeader(http.StatusConflict) w.WriteHeader(http.StatusConflict)
default: default:
t.Fatalf("unexpected request %s %s", r.Method, r.URL.Path) t.Errorf("unexpected request %s %s", r.Method, r.URL.Path)
return
} }
})) }))
defer server.Close() defer server.Close()
@@ -240,13 +251,13 @@ func TestEnsureReleaseResolvesCreateConflictWithOneLookup(t *testing.T) {
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
if result.Created || result.Release.Title != "Created by another request" || lookups != 2 { if result.Created || result.Release.Title != "Created by another request" || lookups.Load() != 2 {
t.Fatalf("result = %#v, release lookups = %d", result, lookups) t.Fatalf("result = %#v, release lookups = %d", result, lookups.Load())
} }
} }
func TestEnsureReleaseReportsUnknownWhenAmbiguousCreateCannotBeInspected(t *testing.T) { func TestEnsureReleaseReportsUnknownWhenAmbiguousCreateCannotBeInspected(t *testing.T) {
lookups := 0 var lookups atomic.Int64
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
assertAuth(t, r) assertAuth(t, r)
switch r.URL.Path { switch r.URL.Path {
@@ -255,8 +266,8 @@ func TestEnsureReleaseReportsUnknownWhenAmbiguousCreateCannotBeInspected(t *test
writeTag(t, w, testCommit) writeTag(t, w, testCommit)
case "/api/v1/repos/owner/project/releases/tags/v1.2.3": case "/api/v1/repos/owner/project/releases/tags/v1.2.3":
assertRequest(t, r, http.MethodGet, r.URL.Path) assertRequest(t, r, http.MethodGet, r.URL.Path)
lookups++ lookups.Add(1)
if lookups == 1 { if lookups.Load() == 1 {
http.NotFound(w, r) http.NotFound(w, r)
return return
} }
@@ -265,13 +276,14 @@ func TestEnsureReleaseReportsUnknownWhenAmbiguousCreateCannotBeInspected(t *test
assertRequest(t, r, http.MethodPost, r.URL.Path) assertRequest(t, r, http.MethodPost, r.URL.Path)
w.WriteHeader(http.StatusInternalServerError) w.WriteHeader(http.StatusInternalServerError)
default: default:
t.Fatalf("unexpected request %s %s", r.Method, r.URL.Path) t.Errorf("unexpected request %s %s", r.Method, r.URL.Path)
return
} }
})) }))
defer server.Close() defer server.Close()
_, err := testClient(server).EnsureRelease(context.Background(), testRequest(server.URL)) _, err := testClient(server).EnsureRelease(context.Background(), testRequest(server.URL))
if failureKind(t, err) != FailureUnknown || lookups != 2 { if failureKind(t, err) != FailureUnknown || lookups.Load() != 2 {
t.Fatalf("error = %v, release lookups = %d", err, lookups) t.Fatalf("error = %v, release lookups = %d", err, lookups.Load())
} }
} }
@@ -282,7 +294,7 @@ type releaseFixture struct {
pre bool pre bool
} }
type fixtureState struct{ posts int } type fixtureState struct{ posts atomic.Int64 }
func newReleaseServer(t *testing.T, existing releaseFixture, createMissing bool) (*httptest.Server, *fixtureState) { func newReleaseServer(t *testing.T, existing releaseFixture, createMissing bool) (*httptest.Server, *fixtureState) {
t.Helper() t.Helper()
@@ -302,13 +314,15 @@ func newReleaseServer(t *testing.T, existing releaseFixture, createMissing bool)
writeRelease(t, w, existing) writeRelease(t, w, existing)
case "/api/v1/repos/owner/project/releases": case "/api/v1/repos/owner/project/releases":
assertRequest(t, r, http.MethodPost, r.URL.Path) assertRequest(t, r, http.MethodPost, r.URL.Path)
state.posts++ state.posts.Add(1)
var payload createReleaseRequest var payload createReleaseRequest
if err := json.NewDecoder(r.Body).Decode(&payload); err != nil { if err := json.NewDecoder(r.Body).Decode(&payload); err != nil {
t.Fatal(err) t.Error(err)
return
} }
if payload.TagName != "v1.2.3" || payload.TargetCommitish != testCommit || payload.Name != "Example v1.2.3" || payload.Body != testNote || payload.Draft || payload.Prerelease { if payload.TagName != "v1.2.3" || payload.TargetCommitish != testCommit || payload.Name != "Example v1.2.3" || payload.Body != testNote || payload.Draft || payload.Prerelease {
t.Fatalf("create payload = %#v", payload) t.Errorf("create payload = %#v", payload)
return
} }
if !createMissing { if !createMissing {
w.WriteHeader(http.StatusConflict) w.WriteHeader(http.StatusConflict)
@@ -316,10 +330,12 @@ func newReleaseServer(t *testing.T, existing releaseFixture, createMissing bool)
} }
writeRelease(t, w, releaseFixture{title: payload.Name}) writeRelease(t, w, releaseFixture{title: payload.Name})
default: default:
t.Fatalf("unexpected request %s %s", r.Method, r.URL.Path) t.Errorf("unexpected request %s %s", r.Method, r.URL.Path)
return
} }
}) })
server := httptest.NewServer(handler) server := httptest.NewServer(handler)
t.Cleanup(server.Close)
return server, state return server, state
} }
@@ -346,7 +362,8 @@ func (fixture releaseFixture) bodyOrDefault() string {
func assertRequest(t *testing.T, request *http.Request, method, path string) { func assertRequest(t *testing.T, request *http.Request, method, path string) {
t.Helper() t.Helper()
if request.Method != method || request.URL.Path != path { if request.Method != method || request.URL.Path != path {
t.Fatalf("request = %s %s, want %s %s", request.Method, request.URL.Path, method, path) t.Errorf("request = %s %s, want %s %s", request.Method, request.URL.Path, method, path)
return
} }
assertAuth(t, request) assertAuth(t, request)
} }
@@ -354,7 +371,8 @@ func assertRequest(t *testing.T, request *http.Request, method, path string) {
func assertAuth(t *testing.T, request *http.Request) { func assertAuth(t *testing.T, request *http.Request) {
t.Helper() t.Helper()
if got := request.Header.Get("Authorization"); got != "token "+testToken { if got := request.Header.Get("Authorization"); got != "token "+testToken {
t.Fatalf("Authorization = %q", got) t.Errorf("Authorization = %q", got)
return
} }
} }
@@ -382,14 +400,14 @@ func contains(value, substring string) bool {
func TestEnsureReleaseReconcilesInconsistentSuccessfulCreate(t *testing.T) { func TestEnsureReleaseReconcilesInconsistentSuccessfulCreate(t *testing.T) {
for _, mode := range []string{"matching", "conflicting", "unavailable"} { for _, mode := range []string{"matching", "conflicting", "unavailable"} {
t.Run(mode, func(t *testing.T) { t.Run(mode, func(t *testing.T) {
posts, lookups := 0, 0 var posts, lookups atomic.Int64
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch { switch {
case strings.Contains(r.URL.Path, "/tags/") && !strings.Contains(r.URL.Path, "/releases/"): case strings.Contains(r.URL.Path, "/tags/") && !strings.Contains(r.URL.Path, "/releases/"):
writeTag(t, w, testCommit) writeTag(t, w, testCommit)
case strings.Contains(r.URL.Path, "/releases/tags/"): case strings.Contains(r.URL.Path, "/releases/tags/"):
lookups++ lookups.Add(1)
if posts == 0 { if posts.Load() == 0 {
http.NotFound(w, r) http.NotFound(w, r)
return return
} }
@@ -403,7 +421,7 @@ func TestEnsureReleaseReconcilesInconsistentSuccessfulCreate(t *testing.T) {
} }
writeRelease(t, w, releaseFixture{title: "Example v1.2.3", body: body}) writeRelease(t, w, releaseFixture{title: "Example v1.2.3", body: body})
case r.Method == http.MethodPost: case r.Method == http.MethodPost:
posts++ posts.Add(1)
w.WriteHeader(http.StatusCreated) w.WriteHeader(http.StatusCreated)
json.NewEncoder(w).Encode(releaseResponse{ID: 10, TagName: "v1.2.3", Name: "inconsistent title", Body: testNote}) json.NewEncoder(w).Encode(releaseResponse{ID: 10, TagName: "v1.2.3", Name: "inconsistent title", Body: testNote})
default: default:
@@ -412,8 +430,8 @@ func TestEnsureReleaseReconcilesInconsistentSuccessfulCreate(t *testing.T) {
})) }))
defer server.Close() defer server.Close()
result, err := testClient(server).EnsureRelease(context.Background(), testRequest(server.URL)) result, err := testClient(server).EnsureRelease(context.Background(), testRequest(server.URL))
if posts != 1 || lookups != 2 { if posts.Load() != 1 || lookups.Load() != 2 {
t.Fatalf("posts=%d lookups=%d", posts, lookups) t.Fatalf("posts=%d lookups=%d", posts.Load(), lookups.Load())
} }
switch mode { switch mode {
case "matching": case "matching":

View File

@@ -47,8 +47,9 @@ type deliveryBenchmarkIdentity struct {
func BenchmarkPerformanceFacadeDeliver(b *testing.B) { func BenchmarkPerformanceFacadeDeliver(b *testing.B) {
repository, build, expected := newDeliveryBenchmarkFixture(b) repository, build, expected := newDeliveryBenchmarkFixture(b)
state := &deliveryBenchmarkServer{commit: gitText(b, repository, "rev-parse", "HEAD"), assets: expected} state := &deliveryBenchmarkServer{commit: gitText(b, repository, "rev-parse", "HEAD"), assets: expected}
server := httptest.NewServer(http.HandlerFunc(state.serveHTTP)) server := httptest.NewUnstartedServer(http.HandlerFunc(state.serveHTTP))
state.baseURL = server.URL state.baseURL = "http://" + server.Listener.Addr().String()
server.Start()
b.Cleanup(server.Close) b.Cleanup(server.Close)
request := releasekit.DeliveryRequest{ request := releasekit.DeliveryRequest{
RepositoryDir: repository, RepositoryDir: repository,

View File

@@ -40,7 +40,7 @@ func TestModuleSelectionFollowsSymlinkBeforeParentAcrossOperations(t *testing.T)
buildGit(t, repository, "tag", wantTag) buildGit(t, repository, "tag", wantTag)
server, remote := newFacadeDeliveryServer(t, repository) server, remote := newFacadeDeliveryServer(t, repository)
defer server.Close() defer server.Close()
remote.tag, remote.noteBody = wantTag, note remote.update(func(remote *facadeDeliveryData) { remote.tag, remote.noteBody = wantTag, note })
delivered, err := releasekit.Deliver(t.Context(), releasekit.DeliveryRequest{ delivered, err := releasekit.Deliver(t.Context(), releasekit.DeliveryRequest{
RepositoryDir: repository, ModuleDir: "link/..", Product: "Fixture", Version: version, RepositoryDir: repository, ModuleDir: "link/..", Product: "Fixture", Version: version,
ExpectedTag: wantTag, Destination: facadeDeliveryDestination(server.URL), ExpectedTag: wantTag, Destination: facadeDeliveryDestination(server.URL),

View File

@@ -40,8 +40,8 @@ func TestSourceOperationsRejectUnavailableNoteWithoutTransport(t *testing.T) {
if checked.Commit != commit || checked.Tag != "v1.2.3" || delivered.CandidateCommit != commit || delivered.Tag != checked.Tag || delivered.State != releasekit.DeliveryNotAttempted { if checked.Commit != commit || checked.Tag != "v1.2.3" || delivered.CandidateCommit != commit || delivered.Tag != checked.Tag || delivered.State != releasekit.DeliveryNotAttempted {
t.Fatalf("missing-note evidence: check=%#v, delivery=%#v", checked, delivered) t.Fatalf("missing-note evidence: check=%#v, delivery=%#v", checked, delivered)
} }
if remote.requests != 0 { if remote.snapshot().requests != 0 {
t.Fatalf("missing note caused %d hosted requests", remote.requests) t.Fatalf("missing note caused %d hosted requests", remote.snapshot().requests)
} }
assertInspectionStayedLocal(t, repository, attempts, before) assertInspectionStayedLocal(t, repository, attempts, before)
} }