Enable race testing in validation
This commit is contained in:
24
check.go
24
check.go
@@ -6,6 +6,7 @@ import (
|
||||
|
||||
internalbuild "gitea.maximumdirect.net/eric/ReleaseKit/internal/build"
|
||||
"gitea.maximumdirect.net/eric/ReleaseKit/internal/candidate"
|
||||
"gitea.maximumdirect.net/eric/ReleaseKit/internal/validation"
|
||||
)
|
||||
|
||||
// ErrInvalidVersion reports a version that is not stable vMAJOR.MINOR.PATCH.
|
||||
@@ -27,18 +28,34 @@ type ReleaseRequest struct {
|
||||
Product string
|
||||
Version string
|
||||
Remote string
|
||||
// AdditionalChecks optionally runs product checks after the standard suite.
|
||||
// Validation selects optional checks run against the captured candidate.
|
||||
Validation ValidationSpec
|
||||
// AdditionalChecks optionally runs product checks after shared validation.
|
||||
AdditionalChecks func(context.Context, Candidate) error
|
||||
Build *BuildSpec // optional selected-target build after validation.
|
||||
Output io.Writer
|
||||
}
|
||||
|
||||
// ValidationSpec selects optional shared validation checks. Its zero value
|
||||
// preserves the current validation behavior.
|
||||
type ValidationSpec struct {
|
||||
Go GoValidationSpec
|
||||
}
|
||||
|
||||
// GoValidationSpec selects checks performed by the Go toolchain.
|
||||
type GoValidationSpec struct {
|
||||
// Race runs the one standard Go test suite with the race detector enabled.
|
||||
Race bool
|
||||
}
|
||||
|
||||
// Check validates one local, committed release candidate. It requires a clean
|
||||
// worktree and index on main, a committed note matching Product and Version,
|
||||
// and an unused local tag. It always runs the offline Go test suite with the
|
||||
// available compatible local Go toolchain and Go host target, then calls
|
||||
// AdditionalChecks when supplied. If Build selects targets, Check builds them
|
||||
// after validation in temporary storage outside the worktree, then rechecks
|
||||
// AdditionalChecks when supplied. Validation.Go.Race runs that one suite with
|
||||
// the race detector; the zero value preserves the ordinary native suite. If
|
||||
// Build selects targets, Check builds them after validation in temporary
|
||||
// storage outside the worktree, then rechecks
|
||||
// branch, HEAD, worktree, index, and local tag state. Success establishes local
|
||||
// candidate evidence only; it does not establish remote availability.
|
||||
func Check(ctx context.Context, request ReleaseRequest) (Candidate, error) {
|
||||
@@ -58,6 +75,7 @@ func candidateRequest(request ReleaseRequest) candidate.Request {
|
||||
RepositoryDir: request.RepositoryDir,
|
||||
Product: request.Product,
|
||||
Version: request.Version,
|
||||
Validation: validation.Settings{Race: request.Validation.Go.Race},
|
||||
AdditionalChecks: candidateChecks(request.AdditionalChecks),
|
||||
AfterValidate: internalbuild.CandidateCallback(internalBuildSpec(request.Build), request.Output),
|
||||
Output: request.Output,
|
||||
|
||||
@@ -112,14 +112,25 @@ type CheckCommand struct {
|
||||
Env map[string]string `json:"env,omitempty"`
|
||||
}
|
||||
|
||||
// Settings contains the product, remote, and check commands read from a
|
||||
// releasekit.json profile.
|
||||
// Settings contains the product, remote, shared validation selections, and
|
||||
// check commands read from a releasekit.json profile.
|
||||
type Settings struct {
|
||||
Product string `json:"product"`
|
||||
Remote string `json:"remote,omitempty"`
|
||||
Checks []CheckCommand `json:"checks,omitempty"`
|
||||
Build *BuildSettings `json:"build,omitempty"`
|
||||
Delivery *DeliverySettings `json:"delivery,omitempty"`
|
||||
Product string `json:"product"`
|
||||
Remote string `json:"remote,omitempty"`
|
||||
Checks []CheckCommand `json:"checks,omitempty"`
|
||||
Validation *ValidationSettings `json:"validation,omitempty"`
|
||||
Build *BuildSettings `json:"build,omitempty"`
|
||||
Delivery *DeliverySettings `json:"delivery,omitempty"`
|
||||
}
|
||||
|
||||
// ValidationSettings selects shared candidate checks in a release profile.
|
||||
type ValidationSettings struct {
|
||||
Go GoValidationSettings `json:"go,omitempty"`
|
||||
}
|
||||
|
||||
// GoValidationSettings selects checks performed by the Go toolchain.
|
||||
type GoValidationSettings struct {
|
||||
Race bool `json:"race,omitempty"`
|
||||
}
|
||||
|
||||
// DeliverySettings selects the hosted Gitea release destination.
|
||||
|
||||
@@ -141,6 +141,22 @@ func TestLoadSettingsDefaultsRemoteAndConfigPath(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadSettingsReadsRaceValidation(t *testing.T) {
|
||||
repoDir := t.TempDir()
|
||||
profilePath := filepath.Join(repoDir, "releasekit.json")
|
||||
if err := os.WriteFile(profilePath, []byte(`{"product":"Example","validation":{"go":{"race":true}}}`), 0o600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
settings, err := LoadSettings(repoDir, "")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if settings.Validation == nil || !settings.Validation.Go.Race {
|
||||
t.Fatalf("validation settings = %#v, want Go race enabled", settings.Validation)
|
||||
}
|
||||
}
|
||||
|
||||
func TestExampleProfileLoads(t *testing.T) {
|
||||
repoDir, err := filepath.Abs(filepath.Join("..", "..", "..", ".."))
|
||||
if err != nil {
|
||||
|
||||
@@ -139,6 +139,7 @@ func candidateRequest(invocation cli.Invocation, settings cli.Settings, output i
|
||||
Product: settings.Product,
|
||||
Version: invocation.Version,
|
||||
Remote: settings.Remote,
|
||||
Validation: validationSpec(settings.Validation),
|
||||
Build: configuredBuildSpec(settings.Build),
|
||||
Output: output,
|
||||
}
|
||||
@@ -154,6 +155,15 @@ func candidateRequest(invocation cli.Invocation, settings cli.Settings, output i
|
||||
return request
|
||||
}
|
||||
|
||||
func validationSpec(settings *cli.ValidationSettings) releasekit.ValidationSpec {
|
||||
if settings == nil {
|
||||
return releasekit.ValidationSpec{}
|
||||
}
|
||||
return releasekit.ValidationSpec{
|
||||
Go: releasekit.GoValidationSpec{Race: settings.Go.Race},
|
||||
}
|
||||
}
|
||||
|
||||
func candidateFailureText(err error) string {
|
||||
return err.Error()
|
||||
}
|
||||
|
||||
@@ -49,6 +49,7 @@ func TestUnknownCommand(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestCheckCommandBindsConfiguredChecksToCapturedCandidate(t *testing.T) {
|
||||
raceSelected := racePrerequisitesAvailable()
|
||||
repository := newCLIRepository(t)
|
||||
profilePath := filepath.Join(t.TempDir(), "profile with spaces.json")
|
||||
orderPath := filepath.Join(t.TempDir(), "check-order")
|
||||
@@ -73,6 +74,9 @@ func TestCheckCommandBindsConfiguredChecksToCapturedCandidate(t *testing.T) {
|
||||
Remote: "origin",
|
||||
Checks: []cli.CheckCommand{check("first"), check("second")},
|
||||
}
|
||||
if raceSelected {
|
||||
settings.Validation = &cli.ValidationSettings{Go: cli.GoValidationSettings{Race: true}}
|
||||
}
|
||||
contents, err := json.Marshal(settings)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
@@ -82,6 +86,11 @@ func TestCheckCommandBindsConfiguredChecksToCapturedCandidate(t *testing.T) {
|
||||
}
|
||||
|
||||
var stdout, stderr bytes.Buffer
|
||||
raceMarker := ""
|
||||
if raceSelected {
|
||||
raceMarker = filepath.Join(t.TempDir(), "race-enabled")
|
||||
t.Setenv("RELEASEKIT_RACE_MARKER", raceMarker)
|
||||
}
|
||||
args := []string{"check", "--repo", repository, "--config", profilePath, "v1.2.3"}
|
||||
if status := run(context.Background(), args, &stdout, &stderr); status != 0 {
|
||||
t.Fatalf("run() status = %d; stdout=%q stderr=%q", status, stdout.String(), stderr.String())
|
||||
@@ -96,6 +105,23 @@ func TestCheckCommandBindsConfiguredChecksToCapturedCandidate(t *testing.T) {
|
||||
if want := fmt.Sprintf("local candidate v1.2.3 at %s is valid\n", commit); stdout.String() != want {
|
||||
t.Fatalf("stdout = %q, want %q", stdout.String(), want)
|
||||
}
|
||||
if raceSelected {
|
||||
if _, err := os.Stat(raceMarker); err != nil {
|
||||
t.Fatalf("race-enabled profile did not run: %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestCandidateRequestMapsRaceValidation(t *testing.T) {
|
||||
request := candidateRequest(cli.Invocation{Repository: "repository", Version: "v1.2.3"}, cli.Settings{
|
||||
Product: "Example Product",
|
||||
Validation: &cli.ValidationSettings{
|
||||
Go: cli.GoValidationSettings{Race: true},
|
||||
},
|
||||
}, nil)
|
||||
if !request.Validation.Go.Race {
|
||||
t.Fatalf("candidate request validation = %#v, want Go race enabled", request.Validation)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCandidateCommandsPresentValidationDiagnosticsOnce(t *testing.T) {
|
||||
@@ -165,6 +191,7 @@ func TestCandidateFailurePresentationRetainsCallbackContext(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestPublishCommandPublishesConfiguredCandidate(t *testing.T) {
|
||||
raceSelected := racePrerequisitesAvailable()
|
||||
repository := newCLIRepository(t)
|
||||
writeRepositoryFile(t, repository, "docs/releases/v1.2.3.md", "# Example Product v1.2.3\n\n## Compatibility\n\nNo action required.\n")
|
||||
writeRepositoryFile(t, repository, "scripts/check-release-note.sh", `#!/bin/sh
|
||||
@@ -189,6 +216,9 @@ printf '%s %s\n' "$version" "$candidate" >> "$RELEASEKIT_COMPATIBILITY_LOG"
|
||||
Env: map[string]string{"RELEASEKIT_COMPATIBILITY_LOG": checkLog},
|
||||
}},
|
||||
}
|
||||
if raceSelected {
|
||||
settings.Validation = &cli.ValidationSettings{Go: cli.GoValidationSettings{Race: true}}
|
||||
}
|
||||
contents, err := json.Marshal(settings)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
@@ -204,6 +234,11 @@ printf '%s %s\n' "$version" "$candidate" >> "$RELEASEKIT_COMPATIBILITY_LOG"
|
||||
cliGitRun(t, repository, "push", "--set-upstream", "origin", "main")
|
||||
|
||||
var stdout, stderr bytes.Buffer
|
||||
raceMarker := ""
|
||||
if raceSelected {
|
||||
raceMarker = filepath.Join(t.TempDir(), "race-enabled")
|
||||
t.Setenv("RELEASEKIT_RACE_MARKER", raceMarker)
|
||||
}
|
||||
args := []string{"publish", "--repo", repository, "v1.2.3"}
|
||||
if status := run(context.Background(), args, &stdout, &stderr); status != 0 {
|
||||
t.Fatalf("run() status = %d; stdout=%q stderr=%q", status, stdout.String(), stderr.String())
|
||||
@@ -226,6 +261,11 @@ printf '%s %s\n' "$version" "$candidate" >> "$RELEASEKIT_COMPATIBILITY_LOG"
|
||||
if got, want := strings.Count(string(checkResult), "v1.2.3 "+commit), 1; got != want {
|
||||
t.Fatalf("product compatibility check ran %d times, want %d", got, want)
|
||||
}
|
||||
if raceSelected {
|
||||
if _, err := os.Stat(raceMarker); err != nil {
|
||||
t.Fatalf("race-enabled profile did not run: %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildCommandWritesValidatedReceiptForConfiguredArtifacts(t *testing.T) {
|
||||
@@ -343,12 +383,52 @@ func newCLIRepository(t *testing.T) string {
|
||||
}
|
||||
write("go.mod", "module example.com/cli-candidate\n\ngo 1.25.0\n")
|
||||
write("candidate_test.go", "package candidate\nimport \"testing\"\nfunc TestFixture(t *testing.T) {}\n")
|
||||
write("race_candidate_test.go", `//go:build race
|
||||
|
||||
package candidate
|
||||
|
||||
import (
|
||||
"os"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestRaceBuildSelection(t *testing.T) {
|
||||
marker := os.Getenv("RELEASEKIT_RACE_MARKER")
|
||||
if marker == "" {
|
||||
return
|
||||
}
|
||||
if err := os.WriteFile(marker, []byte("race enabled"), 0o600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
`)
|
||||
write("docs/releases/v1.2.3.md", "# Example Product v1.2.3\n\nRelease note.\n")
|
||||
cliGitRun(t, repository, "add", "--all")
|
||||
cliGitRun(t, repository, "commit", "-m", "candidate")
|
||||
return repository
|
||||
}
|
||||
|
||||
func racePrerequisitesAvailable() bool {
|
||||
supported := runtime.GOOS == "darwin" && (runtime.GOARCH == "amd64" || runtime.GOARCH == "arm64") ||
|
||||
(runtime.GOOS == "freebsd" || runtime.GOOS == "linux") && (runtime.GOARCH == "amd64" || runtime.GOARCH == "arm64" || runtime.GOARCH == "ppc64le") ||
|
||||
runtime.GOOS == "windows" && runtime.GOARCH == "amd64"
|
||||
if !supported {
|
||||
return false
|
||||
}
|
||||
cgo, err := exec.Command("go", "env", "CGO_ENABLED").Output()
|
||||
if err != nil || strings.TrimSpace(string(cgo)) != "1" {
|
||||
return false
|
||||
}
|
||||
cc, err := exec.Command("go", "env", "CC").Output()
|
||||
if err != nil || len(strings.Fields(string(cc))) == 0 {
|
||||
return false
|
||||
}
|
||||
if _, err := exec.LookPath(strings.Fields(string(cc))[0]); err != nil {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func TestDeliverPageOnlyCreatesAndReusesRelease(t *testing.T) {
|
||||
repository := newCLIRepository(t)
|
||||
state := &deliveryServerState{}
|
||||
|
||||
17
docs/cli.md
17
docs/cli.md
@@ -23,10 +23,10 @@ go run ./cmd/releasekit check [--repo DIR] [--config FILE] VERSION
|
||||
`VERSION` must use stable `vMAJOR.MINOR.PATCH` syntax. Options must precede the
|
||||
version. The repository defaults to the current directory, and the profile
|
||||
defaults to `<repository>/releasekit.json`. A successful command prints the
|
||||
captured local commit and version. It checks the committed release note, local
|
||||
Git state, runs the standard offline Go test suite, and then runs any configured
|
||||
additional checks in profile order. It does not inspect remote availability or
|
||||
publish a tag.
|
||||
captured local commit and version. It checks the committed release note and
|
||||
local Git state, runs the standard offline Go test suite (with `-race` when
|
||||
`validation.go.race` is true), and then runs any configured additional checks
|
||||
in profile order. It does not inspect remote availability or publish a tag.
|
||||
|
||||
`check` writes errors and diagnostics to standard error. It returns status 0
|
||||
after local validation, 1 after a candidate validation failure, and 2 for usage,
|
||||
@@ -40,10 +40,11 @@ Publish a source tag:
|
||||
go run ./cmd/releasekit publish [--repo DIR] [--config FILE] VERSION
|
||||
```
|
||||
|
||||
`publish` reads the same product, remote, and additional-check settings as
|
||||
`check`. It checks the authoritative remote and validates the candidate: the
|
||||
standard offline Go test suite runs first, followed by configured checks in
|
||||
profile order. It then creates an annotated local tag and pushes only its exact
|
||||
`publish` reads the same product, remote, validation, and additional-check
|
||||
settings as `check`. It checks the authoritative remote and validates the
|
||||
candidate: the single standard offline Go test suite runs first, with `-race`
|
||||
when `validation.go.race` is true, followed by configured checks in profile
|
||||
order. It then creates an annotated local tag and pushes only its exact
|
||||
tag object. It prints the confirmed version, candidate commit, and tag object
|
||||
on success. Validation
|
||||
progress and failed publication state, including the captured publication
|
||||
|
||||
@@ -11,6 +11,7 @@ The current profile fields are:
|
||||
| `product` | Yes | Product name used by release operations. |
|
||||
| `remote` | No | Selected Git remote; defaults to `origin`. |
|
||||
| `checks` | No | Ordered additional child commands, each with an argument vector and optional environment map. |
|
||||
| `validation` | No | Optional shared checks selected by ReleaseKit. |
|
||||
| `build` | No | Optional main package, binary name, and selected `GOOS/GOARCH` target strings. |
|
||||
| `delivery` | No | Optional Gitea `base_url`, `owner`, `repository`, and `token_env` settings for tagged-source delivery. |
|
||||
|
||||
@@ -25,6 +26,17 @@ Unknown fields are rejected so a setting cannot be silently ignored. The
|
||||
target requires the package and binary name; each target is written as one
|
||||
`GOOS/GOARCH` pair. An omitted or empty target list means no binary work.
|
||||
|
||||
The optional `validation` object selects shared checks implemented by ReleaseKit.
|
||||
`validation.go.race` enables the race detector for the single standard Go test
|
||||
suite. When omitted or false, the ordinary native suite runs. For example:
|
||||
|
||||
```json
|
||||
{
|
||||
"product": "Example",
|
||||
"validation": {"go": {"race": true}}
|
||||
}
|
||||
```
|
||||
|
||||
The `delivery` object contains `base_url`, `owner`, `repository`, and optional
|
||||
`token_env`. Its values are required when running `deliver`, but credentials are
|
||||
not required for `check`, `publish`, or `build`. `token_env` names the
|
||||
@@ -32,10 +44,11 @@ environment variable containing the Gitea token; it defaults to
|
||||
`RELEASEKIT_GITEA_TOKEN`. The CLI reads this variable only for `deliver` and
|
||||
passes its value explicitly to the library operation.
|
||||
|
||||
The `check` and `publish` commands always run
|
||||
`go test -mod=readonly -count=1 ./...` with Go module and toolchain network
|
||||
access disabled. They then run each configured `checks` command once, in order,
|
||||
as an additional product check. Selected-target builds run after both. Do not
|
||||
The `check` and `publish` commands run one
|
||||
`go test -mod=readonly -count=1 ./...` suite with Go module and toolchain network
|
||||
access disabled, adding `-race` when `validation.go.race` is true. They then run
|
||||
each configured `checks` command once, in order, as an additional product
|
||||
check. Selected-target builds run after the suite and product checks. Do not
|
||||
repeat the standard suite in `checks`; doing so runs it twice. The `build`
|
||||
command uses the selected build settings and writes binaries to `dist` unless
|
||||
`--output` selects another path.
|
||||
|
||||
@@ -8,6 +8,9 @@ result, err := releasekit.Check(ctx, releasekit.ReleaseRequest{
|
||||
RepositoryDir: "/work/project",
|
||||
Product: "Example",
|
||||
Version: "v1.2.3",
|
||||
Validation: releasekit.ValidationSpec{
|
||||
Go: releasekit.GoValidationSpec{Race: true},
|
||||
},
|
||||
AdditionalChecks: func(ctx context.Context, candidate releasekit.Candidate) error {
|
||||
return runProjectChecks(ctx, candidate.RepositoryDir)
|
||||
},
|
||||
@@ -24,14 +27,17 @@ version must be stable `vMAJOR.MINOR.PATCH`. The committed
|
||||
product name and exact version as complete title elements. No local tag with
|
||||
that version may already exist.
|
||||
|
||||
ReleaseKit always runs `go test -mod=readonly -count=1 ./...` with the available
|
||||
compatible local Go toolchain, then calls `AdditionalChecks` once when supplied.
|
||||
It does not download a different toolchain automatically. The standard suite
|
||||
uses the Go host target, disables workspace and module network access, and
|
||||
ignores ambient `GOFLAGS`, experiment, and CPU feature overrides. Effective
|
||||
cache and native cgo/C-tool settings are preserved, including values from Go's
|
||||
persistent environment file. The check revalidates the branch, commit, and
|
||||
cleanliness after validation.
|
||||
ReleaseKit runs one `go test -mod=readonly -count=1 ./...` suite with the
|
||||
available compatible local Go toolchain, then calls `AdditionalChecks` once
|
||||
when supplied. Set `Validation.Go.Race` to run that suite with `-race`; the zero
|
||||
value keeps the ordinary native suite. ReleaseKit does not download a different
|
||||
toolchain automatically. If the race prerequisite is unavailable, the `go-test`
|
||||
check fails rather than being skipped. The suite uses the Go host target,
|
||||
disables workspace and module network access, and ignores ambient `GOFLAGS`,
|
||||
experiment, and CPU feature overrides. Effective cache and native cgo/C-tool
|
||||
settings are preserved, including values from Go's persistent environment
|
||||
file. The check revalidates the branch, commit, and cleanliness after
|
||||
validation.
|
||||
|
||||
A successful check is evidence about the local candidate only. It does not
|
||||
check whether a remote branch or tag is available and does not create a tag.
|
||||
|
||||
@@ -8,6 +8,9 @@ result, err := releasekit.Publish(ctx, releasekit.ReleaseRequest{
|
||||
RepositoryDir: "/work/project",
|
||||
Product: "Example",
|
||||
Version: "v1.2.3",
|
||||
Validation: releasekit.ValidationSpec{
|
||||
Go: releasekit.GoValidationSpec{Race: true},
|
||||
},
|
||||
AdditionalChecks: func(ctx context.Context, candidate releasekit.Candidate) error {
|
||||
return runProjectChecks(ctx, candidate.RepositoryDir)
|
||||
},
|
||||
@@ -20,12 +23,15 @@ fmt.Println(result.Version, result.CandidateCommit, result.TagObject)
|
||||
|
||||
Publication captures a clean candidate on `main`, validates its committed
|
||||
release note, and checks the selected remote before running candidate checks.
|
||||
It always runs `go test -mod=readonly -count=1 ./...` with the available
|
||||
compatible local Go toolchain. It then calls `AdditionalChecks` once when
|
||||
supplied. The standard suite cannot be replaced; move product-specific behavior
|
||||
into `AdditionalChecks` and remove repeated standard-suite commands from custom
|
||||
checks. A failed suite or additional check prevents tag creation, and wrapped
|
||||
errors preserve their causes.
|
||||
It runs one `go test -mod=readonly -count=1 ./...` suite with the available
|
||||
compatible local Go toolchain. Set `Validation.Go.Race` to run that suite with
|
||||
`-race`; its zero value keeps the ordinary native suite. It then calls
|
||||
`AdditionalChecks` once when supplied. The standard suite cannot be replaced;
|
||||
move product-specific behavior into `AdditionalChecks` and remove repeated
|
||||
standard-suite commands from custom checks. A failed suite or additional check
|
||||
prevents tag creation, and wrapped errors preserve their causes.
|
||||
If race prerequisites are unavailable, the selected suite fails as `go-test`
|
||||
instead of silently running without the race detector.
|
||||
|
||||
If `Build` selects targets, ReleaseKit verifies them after the standard suite
|
||||
and additional checks, before creating the tag. The operation then rechecks the
|
||||
|
||||
@@ -46,7 +46,7 @@ these implemented packages:
|
||||
| `internal/tempdir` | Disposable system temporary storage outside source worktrees. |
|
||||
| `internal/process` | Cancellable child-process execution with per-command environment overlays and removals. |
|
||||
| `internal/preparation` | Coordination for expected-commit tag checkout and Go module dependency preparation. |
|
||||
| `internal/validation` | Shared default Go validation, structured check failures, and lazy committed-file access for captured candidates; `internal/candidate` retains capture, callback sequencing, and local-state rechecks. |
|
||||
| `internal/validation` | Shared Go validation with optional race selection, structured check failures, and lazy committed-file access for captured candidates; `internal/candidate` retains capture, callback sequencing, and local-state rechecks. |
|
||||
| `internal/publish` | Publication guards, one validation run, annotated tag creation, push, and recovery state. |
|
||||
| `internal/runtimeinfo` | Private version lookup used by the runtime facade. |
|
||||
| `internal/versioncontract` | Shared stable-version rules and linker-symbol identity. |
|
||||
@@ -78,7 +78,7 @@ not a separate release matrix:
|
||||
| Capability | Implementation owner | Existing evidence |
|
||||
| --- | --- | --- |
|
||||
| Runtime version lookup and injected application version | `buildinfo`, `internal/runtimeinfo`, `internal/versioncontract`, `internal/build` | `TestVersionInConsumerExecutable`, `TestVersion` |
|
||||
| Candidate capture, shared validation, public diagnostics, additional checks, and rechecks | `releasekit.Check`, `releasekit.Publish`, `internal/candidate`, `internal/validation`, `cmd/releasekit/internal/cli` | `TestCheckCapturesCandidateAndCallsAdditionalChecksOnce`, `TestCheckReturnsPublicValidationError`, `TestPublishPushesOnlyTheCapturedAnnotatedTag`, `TestPublishFailedStandardSuiteSkipsAdditionalChecksAndTagCreation`, `TestPublishAdditionalCheckFailurePreservesCauseAndPreventsTagCreation`, `TestCheckCommandBindsConfiguredChecksToCapturedCandidate`, `TestPublishCommandPublishesConfiguredCandidate`, `TestCandidateCommandsPresentValidationDiagnosticsOnce` |
|
||||
| Candidate capture, shared validation, public diagnostics, additional checks, and rechecks | `releasekit.Check`, `releasekit.Publish`, `internal/candidate`, `internal/validation`, `cmd/releasekit/internal/cli` | `TestRunUsesOneSuiteWithSelectedRaceSetting`, `TestRunReportsRacePrerequisiteFailureWithoutFallback`, `TestRunExecutesRaceEnabledSuiteOnHost`, `TestCandidateRequestMapsRaceValidation`, `TestLoadSettingsReadsRaceValidation`, `TestCheckUsesSelectedRaceSuiteBeforeAdditionalChecks`, `TestCheckCapturesCandidateAndCallsAdditionalChecksOnce`, `TestCheckReturnsPublicValidationError`, `TestPublishPushesOnlyTheCapturedAnnotatedTag`, `TestPublishFailedStandardSuiteSkipsAdditionalChecksAndTagCreation`, `TestPublishAdditionalCheckFailurePreservesCauseAndPreventsTagCreation`, `TestCheckCommandBindsConfiguredChecksToCapturedCandidate`, `TestPublishCommandPublishesConfiguredCandidate`, `TestCandidateCommandsPresentValidationDiagnosticsOnce` |
|
||||
| Shared Go settings for validation and builds | `internal/gotool`, `internal/process`, `internal/candidate`, `internal/build` | `TestDefaultValidationDoesNotInheritGOFLAGSRunFilter`, `TestNativeAndStaticCommandsUseOwnedBaseline`, `TestPersistentSettingsPreserveCachesAndNativeCTools`, `TestBuildIgnoresAmbientAndPersistentArchitectureTuning` |
|
||||
| Authoritative endpoint and exact annotated tag publication | `releasekit.Publish`, `internal/publish`, `internal/git` | `TestPublishPushesOnlyTheCapturedAnnotatedTag`, `TestResolveUsesPushURLAndQueriesWithoutMutation` |
|
||||
| Publication partial and uncertain recovery | `internal/publish`, `internal/git` | `TestPublishRejectedPushKeepsLocalTagAndReportsNotPublished`, `TestPublishLostAcknowledgmentConfirmsExactRemoteObject`, `TestPublishLostAcknowledgmentWithChangedEndpointRemainsUnknown`, `TestPublishUnavailableReconciliationRemainsUnknownAndPreservesPushError` |
|
||||
|
||||
@@ -8,10 +8,11 @@ post-validation build callback and local-state recheck. `internal/publish`
|
||||
adds its endpoint guards, tag creation, push, and recovery around that same
|
||||
candidate path.
|
||||
|
||||
The current executor runs the default offline Go suite. Its request carries the
|
||||
captured repository directory and commit, output writer, and internal
|
||||
settings. `Request.ReadCandidateTree` lets a selected file check request
|
||||
candidate contents; the default Go-only path does not enumerate the tree.
|
||||
The executor runs one offline Go suite, optionally with the race detector when
|
||||
selected. Its request carries the captured repository directory and commit,
|
||||
output writer, and internal settings. `Request.ReadCandidateTree` lets a
|
||||
selected file check request candidate contents; the Go-only path does not
|
||||
enumerate the tree.
|
||||
`CandidateTree` gets metadata and bytes from Git objects, reads blob bytes
|
||||
lazily, and resolves destinations without consulting the worktree or
|
||||
neighboring files.
|
||||
|
||||
@@ -226,6 +226,11 @@ imports only lightweight `buildinfo`. Stages 12–14 implement this agreed appro
|
||||
|
||||
## Stage 1: Introduce the specification with race selection
|
||||
|
||||
**Completion:** Complete. `ReleaseRequest.Validation.Go.Race` and profile
|
||||
`validation.go.race` select `-race` for the single native Go suite. Both
|
||||
candidate operations share the selection, and race failures retain the `go-test`
|
||||
identity without falling back to an ordinary suite.
|
||||
|
||||
**Outcome:** The first public/profile selection changes the one existing suite.
|
||||
|
||||
1. Add `ReleaseRequest.Validation`, `ValidationSpec.Go`, and
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# Validation API and profile model
|
||||
|
||||
Status: Shared validation foundation implemented; optional v0.2.0 capabilities
|
||||
and their public/profile settings remain future work.
|
||||
Status: Shared validation foundation and Go race selection implemented;
|
||||
remaining v0.2.0 capabilities and their settings are future work.
|
||||
|
||||
This roadmap describes the shared model that enables the optional validation
|
||||
capabilities in the [v0.2.0 roadmap](v0.2.0.md). It owns their common API,
|
||||
@@ -18,9 +18,9 @@ Selected executable builds follow validation. The CLI maps profile `checks`
|
||||
commands to the additional-check callback.
|
||||
|
||||
This provides a common procedure and a product-specific extension point. The
|
||||
request and profile currently have no structured selection for shared optional
|
||||
validation. Race testing, full vet, package compilation, formatting, Markdown,
|
||||
module-distribution, and file-size checks need a coherent home as they are added.
|
||||
request and profile now have a structured selection for race testing; full vet,
|
||||
package compilation, formatting, Markdown, module-distribution, and file-size
|
||||
checks will use the same model as their capabilities are added.
|
||||
|
||||
The model should let consumers declare their choices once while ReleaseKit owns
|
||||
execution and coordination. This follows [ADR 0002](../adr/0002-routine-releases.md):
|
||||
@@ -124,11 +124,10 @@ failure-stopping behavior of the candidate workflow.
|
||||
|
||||
## Implementation
|
||||
|
||||
The [implementation plan](implementation.md) records the completed foundation:
|
||||
shared execution, internal diagnostics, captured candidate-tree inspection and
|
||||
destination resolution, facade diagnostics, and consistent CLI presentation.
|
||||
The public `ReleaseRequest.Validation` field and profile `validation` object are
|
||||
not yet available; each setting will be introduced with its first working
|
||||
The [implementation plan](implementation.md) records the completed foundation
|
||||
and current rollout. The facade exposes `ReleaseRequest.Validation.Go.Race`, and
|
||||
profiles accept `validation.go.race`; both select `-race` for the one base Go
|
||||
suite. Each remaining setting will be introduced with its first working
|
||||
capability. Individual checkers, module-selected CI tooling, and the publication
|
||||
endpoint expectation remain future work in the [v0.2.0 roadmap](v0.2.0.md).
|
||||
|
||||
|
||||
@@ -29,6 +29,7 @@ type Request struct {
|
||||
RepositoryDir string
|
||||
Product string
|
||||
Version string
|
||||
Validation validation.Settings
|
||||
AdditionalChecks func(context.Context, Candidate) error
|
||||
AfterValidate func(context.Context, Candidate) error
|
||||
Output io.Writer
|
||||
@@ -95,13 +96,14 @@ func Capture(ctx context.Context, request Request) (Candidate, error) {
|
||||
return Candidate{RepositoryDir: repositoryDir, Commit: state.commit, Version: request.Version, Note: string(note)}, nil
|
||||
}
|
||||
|
||||
// Validate runs the standard offline Go suite, followed by AdditionalChecks
|
||||
// Validate runs the selected offline Go suite, followed by AdditionalChecks
|
||||
// when supplied, for the captured candidate.
|
||||
func Validate(ctx context.Context, request Request, captured Candidate) error {
|
||||
if err := validation.Run(ctx, validation.Request{
|
||||
RepositoryDir: captured.RepositoryDir,
|
||||
Commit: captured.Commit,
|
||||
Output: request.Output,
|
||||
Settings: request.Validation,
|
||||
}); err != nil {
|
||||
return fmt.Errorf("validate candidate %s: %w", captured.Commit, err)
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
|
||||
"gitea.maximumdirect.net/eric/ReleaseKit/internal/candidate"
|
||||
"gitea.maximumdirect.net/eric/ReleaseKit/internal/git"
|
||||
"gitea.maximumdirect.net/eric/ReleaseKit/internal/validation"
|
||||
)
|
||||
|
||||
type State string
|
||||
@@ -40,6 +41,7 @@ type Request struct {
|
||||
Product string
|
||||
Version string
|
||||
Remote string
|
||||
Validation validation.Settings
|
||||
AdditionalChecks func(context.Context, candidate.Candidate) error
|
||||
AfterValidate func(context.Context, candidate.Candidate) error
|
||||
Output io.Writer
|
||||
@@ -55,7 +57,8 @@ type Result struct {
|
||||
}
|
||||
|
||||
// Publish validates one captured candidate and pushes only its annotated tag
|
||||
// object. It runs the standard Go suite, then AdditionalChecks when supplied.
|
||||
// object. It runs one standard Go suite, with the selected settings, then
|
||||
// AdditionalChecks when supplied.
|
||||
// A failed push receives one bounded authoritative tag query. Exact
|
||||
// object equality confirms publication; absence and conflict are classified,
|
||||
// while an unreadable result remains Unknown and preserves the push error.
|
||||
@@ -69,6 +72,7 @@ func Publish(ctx context.Context, request Request) (Result, error) {
|
||||
RepositoryDir: request.RepositoryDir,
|
||||
Product: request.Product,
|
||||
Version: request.Version,
|
||||
Validation: request.Validation,
|
||||
AdditionalChecks: request.AdditionalChecks,
|
||||
Output: request.Output,
|
||||
}
|
||||
|
||||
188
internal/validation/race_test.go
Normal file
188
internal/validation/race_test.go
Normal file
@@ -0,0 +1,188 @@
|
||||
package validation
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestRunUsesOneSuiteWithSelectedRaceSetting(t *testing.T) {
|
||||
for _, test := range []struct {
|
||||
name string
|
||||
race bool
|
||||
}{
|
||||
{name: "default"},
|
||||
{name: "race", race: true},
|
||||
} {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
logPath := installGoShim(t, false)
|
||||
err := Run(context.Background(), Request{
|
||||
RepositoryDir: t.TempDir(),
|
||||
Settings: Settings{Race: test.race},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("Run() error = %v", err)
|
||||
}
|
||||
|
||||
commands := readGoShimLog(t, logPath)
|
||||
var tests []string
|
||||
for _, command := range commands {
|
||||
if strings.HasPrefix(command, "test ") {
|
||||
tests = append(tests, command)
|
||||
}
|
||||
}
|
||||
if len(tests) != 1 {
|
||||
t.Fatalf("Go test invocations = %q, want exactly one", tests)
|
||||
}
|
||||
command := " " + tests[0] + " "
|
||||
for _, argument := range []string{"test", "-mod=readonly", "-count=1", "./..."} {
|
||||
if !strings.Contains(command, " "+argument+" ") {
|
||||
t.Errorf("Go test command %q does not contain %q", tests[0], argument)
|
||||
}
|
||||
}
|
||||
if got := strings.Contains(command, " -race "); got != test.race {
|
||||
t.Errorf("Go test command %q has race=%t, want %t", tests[0], got, test.race)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunReportsRacePrerequisiteFailureWithoutFallback(t *testing.T) {
|
||||
logPath := installGoShim(t, true)
|
||||
err := Run(context.Background(), Request{
|
||||
RepositoryDir: t.TempDir(),
|
||||
Settings: Settings{Race: true},
|
||||
})
|
||||
var failure *Failure
|
||||
if !errors.As(err, &failure) || failure.Check != GoTest {
|
||||
t.Fatalf("Run() error = %v, want a go-test validation failure", err)
|
||||
}
|
||||
var exitError *exec.ExitError
|
||||
if !errors.As(err, &exitError) {
|
||||
t.Fatalf("Run() error = %v, want the command failure preserved", err)
|
||||
}
|
||||
commands := readGoShimLog(t, logPath)
|
||||
var tests []string
|
||||
for _, command := range commands {
|
||||
if strings.HasPrefix(command, "test ") {
|
||||
tests = append(tests, command)
|
||||
}
|
||||
}
|
||||
if len(tests) != 1 || !strings.Contains(" "+tests[0]+" ", " -race ") {
|
||||
t.Fatalf("Go test invocations = %q, want one race-enabled invocation and no fallback", tests)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunExecutesRaceEnabledSuiteOnHost(t *testing.T) {
|
||||
if !raceSupported(runtime.GOOS, runtime.GOARCH) {
|
||||
t.Skipf("Go race detector is not supported on %s/%s", runtime.GOOS, runtime.GOARCH)
|
||||
}
|
||||
cgo, err := exec.Command("go", "env", "CGO_ENABLED").Output()
|
||||
if err != nil || strings.TrimSpace(string(cgo)) != "1" {
|
||||
t.Skip("Go race detector requires CGO_ENABLED=1")
|
||||
}
|
||||
cc, err := exec.Command("go", "env", "CC").Output()
|
||||
if err != nil || len(strings.Fields(string(cc))) == 0 {
|
||||
t.Skip("Go race detector requires a C compiler")
|
||||
}
|
||||
if _, err := exec.LookPath(strings.Fields(string(cc))[0]); err != nil {
|
||||
t.Skip("Go race detector C compiler is unavailable")
|
||||
}
|
||||
module := t.TempDir()
|
||||
if err := os.WriteFile(filepath.Join(module, "go.mod"), []byte("module example.com/racefixture\n\ngo 1.25.0\n"), 0o600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := os.WriteFile(filepath.Join(module, "race_test.go"), []byte(`package racefixture
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestRaceFixture(t *testing.T) {}
|
||||
`), 0o600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := Run(context.Background(), Request{RepositoryDir: module, Settings: Settings{Race: true}}); err != nil {
|
||||
t.Fatalf("race-enabled Go suite failed: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func installGoShim(t *testing.T, failRace bool) string {
|
||||
t.Helper()
|
||||
if runtime.GOOS == "windows" {
|
||||
t.Skip("the controlled Go command fixture uses a POSIX shell")
|
||||
}
|
||||
directory := t.TempDir()
|
||||
logPath := filepath.Join(directory, "go-commands")
|
||||
settings := map[string]string{
|
||||
"GOHOSTOS": runtime.GOOS,
|
||||
"GOHOSTARCH": runtime.GOARCH,
|
||||
"CC": "", "CXX": "", "CGO_CFLAGS": "", "CGO_CPPFLAGS": "",
|
||||
"CGO_CXXFLAGS": "", "CGO_ENABLED": "1", "CGO_LDFLAGS": "",
|
||||
"GOCACHE": "", "GOCACHEPROG": "", "GOMODCACHE": "", "GOPATH": "",
|
||||
"GOROOT": "", "GOTMPDIR": "", "PKG_CONFIG": "",
|
||||
}
|
||||
encodedSettings, err := json.Marshal(settings)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
failBlock := ""
|
||||
if failRace {
|
||||
failBlock = `
|
||||
for argument in "$@"; do
|
||||
if [ "$argument" = "-race" ]; then
|
||||
echo "race detector prerequisite unavailable" >&2
|
||||
exit 2
|
||||
fi
|
||||
done
|
||||
`
|
||||
}
|
||||
script := fmt.Sprintf(`#!/bin/sh
|
||||
set -eu
|
||||
printf '%%s\n' "$*" >> "$RELEASEKIT_GO_LOG"
|
||||
if [ "${1-}" = "env" ]; then
|
||||
printf '%%s\n' '%s'
|
||||
exit 0
|
||||
fi
|
||||
%s
|
||||
exit 0
|
||||
`, encodedSettings, failBlock)
|
||||
shimPath := filepath.Join(directory, "go")
|
||||
if err := os.WriteFile(shimPath, []byte(script), 0o700); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
path := directory
|
||||
if current := os.Getenv("PATH"); current != "" {
|
||||
path += string(os.PathListSeparator) + current
|
||||
}
|
||||
t.Setenv("PATH", path)
|
||||
t.Setenv("RELEASEKIT_GO_LOG", logPath)
|
||||
return logPath
|
||||
}
|
||||
|
||||
func readGoShimLog(t *testing.T, path string) []string {
|
||||
t.Helper()
|
||||
contents, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return strings.Split(strings.TrimSpace(string(contents)), "\n")
|
||||
}
|
||||
|
||||
func raceSupported(goos, goarch string) bool {
|
||||
switch goos {
|
||||
case "darwin":
|
||||
return goarch == "amd64" || goarch == "arm64"
|
||||
case "freebsd", "linux":
|
||||
return goarch == "amd64" || goarch == "arm64" || goarch == "ppc64le"
|
||||
case "windows":
|
||||
return goarch == "amd64"
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
@@ -8,9 +8,10 @@ import (
|
||||
"gitea.maximumdirect.net/eric/ReleaseKit/internal/gotool"
|
||||
)
|
||||
|
||||
// Settings selects shared validation behavior. Its zero value runs the current
|
||||
// default Go suite.
|
||||
type Settings struct{}
|
||||
// Settings selects shared validation behavior.
|
||||
type Settings struct {
|
||||
Race bool
|
||||
}
|
||||
|
||||
// Request identifies the captured source and output used by shared validation.
|
||||
type Request struct {
|
||||
@@ -32,8 +33,12 @@ func Run(ctx context.Context, request Request) error {
|
||||
if err != nil {
|
||||
return &Failure{Check: GoTest, Cause: err}
|
||||
}
|
||||
_, err = tool.Run(ctx, request.RepositoryDir, gotool.Native, gotool.Target{}, request.Output,
|
||||
"test", "-mod=readonly", "-count=1", "./...")
|
||||
args := []string{"test", "-mod=readonly", "-count=1"}
|
||||
if request.Settings.Race {
|
||||
args = append(args, "-race")
|
||||
}
|
||||
args = append(args, "./...")
|
||||
_, err = tool.Run(ctx, request.RepositoryDir, gotool.Native, gotool.Target{}, request.Output, args...)
|
||||
if err != nil {
|
||||
return &Failure{Check: GoTest, Cause: err}
|
||||
}
|
||||
|
||||
@@ -48,8 +48,9 @@ type PublicationResult struct {
|
||||
State PublicationState // publication outcome for this invocation
|
||||
}
|
||||
|
||||
// Publish always runs the offline Go test suite for the captured candidate,
|
||||
// then calls AdditionalChecks when supplied. It optionally builds selected
|
||||
// Publish always runs one offline Go test suite for the captured candidate;
|
||||
// Validation.Go.Race enables the race detector for that suite. It then calls
|
||||
// AdditionalChecks when supplied. It optionally builds selected
|
||||
// targets in temporary storage outside the worktree and pushes the exact
|
||||
// annotated tag object to the selected remote. It returns a result with errors: preflight
|
||||
// failures are not_attempted, failed authoritative guards are conflict, and
|
||||
@@ -65,6 +66,7 @@ func Publish(ctx context.Context, request ReleaseRequest) (PublicationResult, er
|
||||
Product: candidateInput.Product,
|
||||
Version: candidateInput.Version,
|
||||
Remote: request.Remote,
|
||||
Validation: candidateInput.Validation,
|
||||
AdditionalChecks: candidateInput.AdditionalChecks,
|
||||
AfterValidate: candidateInput.AfterValidate,
|
||||
Output: candidateInput.Output,
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
@@ -93,6 +94,35 @@ func TestPublishPushesOnlyTheCapturedAnnotatedTag(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestCheckUsesSelectedRaceSuiteBeforeAdditionalChecks(t *testing.T) {
|
||||
requireRacePrerequisites(t)
|
||||
repo := newPublishFixture(t)
|
||||
raceMarker := filepath.Join(t.TempDir(), "race-enabled")
|
||||
t.Setenv("RELEASEKIT_RACE_MARKER", raceMarker)
|
||||
checks := 0
|
||||
_, err := releasekit.Check(context.Background(), releasekit.ReleaseRequest{
|
||||
RepositoryDir: repo.work,
|
||||
Product: "Sample App",
|
||||
Version: "v1.2.3",
|
||||
Validation: releasekit.ValidationSpec{
|
||||
Go: releasekit.GoValidationSpec{Race: true},
|
||||
},
|
||||
AdditionalChecks: func(context.Context, releasekit.Candidate) error {
|
||||
checks++
|
||||
return nil
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("Check() error = %v", err)
|
||||
}
|
||||
if checks != 1 {
|
||||
t.Fatalf("additional checks ran %d times, want once", checks)
|
||||
}
|
||||
if _, err := os.Stat(raceMarker); err != nil {
|
||||
t.Fatalf("race-enabled fixture did not run: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPublishFailedStandardSuiteSkipsAdditionalChecksAndTagCreation(t *testing.T) {
|
||||
repo := newPublishFixture(t)
|
||||
t.Setenv("RELEASEKIT_FAIL_FIXTURE_SUITE", "1")
|
||||
@@ -133,6 +163,27 @@ func TestPublishFailedStandardSuiteSkipsAdditionalChecksAndTagCreation(t *testin
|
||||
}
|
||||
}
|
||||
|
||||
func requireRacePrerequisites(t *testing.T) {
|
||||
t.Helper()
|
||||
supported := runtime.GOOS == "darwin" && (runtime.GOARCH == "amd64" || runtime.GOARCH == "arm64") ||
|
||||
(runtime.GOOS == "freebsd" || runtime.GOOS == "linux") && (runtime.GOARCH == "amd64" || runtime.GOARCH == "arm64" || runtime.GOARCH == "ppc64le") ||
|
||||
runtime.GOOS == "windows" && runtime.GOARCH == "amd64"
|
||||
if !supported {
|
||||
t.Skipf("Go race detector is not supported on %s/%s", runtime.GOOS, runtime.GOARCH)
|
||||
}
|
||||
cgo, err := exec.Command("go", "env", "CGO_ENABLED").Output()
|
||||
if err != nil || strings.TrimSpace(string(cgo)) != "1" {
|
||||
t.Skip("Go race detector requires CGO_ENABLED=1")
|
||||
}
|
||||
cc, err := exec.Command("go", "env", "CC").Output()
|
||||
if err != nil || len(strings.Fields(string(cc))) == 0 {
|
||||
t.Skip("Go race detector requires a C compiler")
|
||||
}
|
||||
if _, err := exec.LookPath(strings.Fields(string(cc))[0]); err != nil {
|
||||
t.Skip("Go race detector C compiler is unavailable")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCheckReturnsPublicValidationError(t *testing.T) {
|
||||
repo := newPublishFixture(t)
|
||||
t.Setenv("RELEASEKIT_FAIL_FIXTURE_SUITE", "1")
|
||||
@@ -627,6 +678,25 @@ func TestFixture(t *testing.T) {
|
||||
t.Fatal("fixture standard suite failure")
|
||||
}
|
||||
}
|
||||
`)
|
||||
writeFile(t, work, "race_fixture_test.go", `//go:build race
|
||||
|
||||
package fixture
|
||||
|
||||
import (
|
||||
"os"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestRaceBuildSelection(t *testing.T) {
|
||||
marker := os.Getenv("RELEASEKIT_RACE_MARKER")
|
||||
if marker == "" {
|
||||
return
|
||||
}
|
||||
if err := os.WriteFile(marker, []byte("race enabled"), 0o600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
`)
|
||||
writeFile(t, work, "docs/releases/v1.2.3.md", releaseNote)
|
||||
writeFile(t, work, "source.txt", "candidate")
|
||||
|
||||
12
validation_test.go
Normal file
12
validation_test.go
Normal file
@@ -0,0 +1,12 @@
|
||||
package releasekit
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestCandidateRequestMapsRaceValidation(t *testing.T) {
|
||||
request := candidateRequest(ReleaseRequest{
|
||||
Validation: ValidationSpec{Go: GoValidationSpec{Race: true}},
|
||||
})
|
||||
if !request.Validation.Race {
|
||||
t.Fatalf("candidate request validation = %#v, want race enabled", request.Validation)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user