Check committed Go formatting
This commit is contained in:
8
check.go
8
check.go
@@ -49,6 +49,9 @@ type ReleaseRequest struct {
|
|||||||
// preserves the current validation behavior.
|
// preserves the current validation behavior.
|
||||||
type ValidationSpec struct {
|
type ValidationSpec struct {
|
||||||
Go GoValidationSpec
|
Go GoValidationSpec
|
||||||
|
// GoFormat checks the formatting of committed regular Go source files. It
|
||||||
|
// does not modify repository contents and is disabled when false.
|
||||||
|
GoFormat bool
|
||||||
}
|
}
|
||||||
|
|
||||||
// GoValidationSpec selects checks performed by the Go toolchain.
|
// GoValidationSpec selects checks performed by the Go toolchain.
|
||||||
@@ -74,7 +77,9 @@ type GoValidationSpec struct {
|
|||||||
// of test-time vet, and Validation.Go.CompileTargets compiles all packages for
|
// of test-time vet, and Validation.Go.CompileTargets compiles all packages for
|
||||||
// each selected GOOS/GOARCH pair after tests and vet with cgo disabled. Package
|
// each selected GOOS/GOARCH pair after tests and vet with cgo disabled. Package
|
||||||
// compilation produces no artifacts and does not execute target binaries.
|
// compilation produces no artifacts and does not execute target binaries.
|
||||||
// Their zero values preserve the ordinary suite. If
|
// Validation.GoFormat checks regular committed Go source after those selected
|
||||||
|
// Go checks and before AdditionalChecks. Its zero value disables formatting.
|
||||||
|
// The zero values of all selections preserve the ordinary suite. If
|
||||||
// Build selects targets, Check builds them after validation in temporary
|
// Build selects targets, Check builds them after validation in temporary
|
||||||
// storage outside the worktree, then rechecks
|
// storage outside the worktree, then rechecks
|
||||||
// branch, HEAD, worktree, index, and local tag state. Success establishes local
|
// branch, HEAD, worktree, index, and local tag state. Success establishes local
|
||||||
@@ -100,6 +105,7 @@ func candidateRequest(request ReleaseRequest) candidate.Request {
|
|||||||
Race: request.Validation.Go.Race,
|
Race: request.Validation.Go.Race,
|
||||||
FullVet: request.Validation.Go.FullVet,
|
FullVet: request.Validation.Go.FullVet,
|
||||||
CompileTargets: make([]validation.Target, len(request.Validation.Go.CompileTargets)),
|
CompileTargets: make([]validation.Target, len(request.Validation.Go.CompileTargets)),
|
||||||
|
GoFormat: request.Validation.GoFormat,
|
||||||
},
|
},
|
||||||
AdditionalChecks: candidateChecks(request.AdditionalChecks),
|
AdditionalChecks: candidateChecks(request.AdditionalChecks),
|
||||||
AfterValidate: internalbuild.CandidateCallback(internalBuildSpec(request.Build), request.Output),
|
AfterValidate: internalbuild.CandidateCallback(internalBuildSpec(request.Build), request.Output),
|
||||||
|
|||||||
@@ -127,6 +127,9 @@ type Settings struct {
|
|||||||
// ValidationSettings selects shared candidate checks in a release profile.
|
// ValidationSettings selects shared candidate checks in a release profile.
|
||||||
type ValidationSettings struct {
|
type ValidationSettings struct {
|
||||||
Go GoValidationSettings `json:"go,omitempty"`
|
Go GoValidationSettings `json:"go,omitempty"`
|
||||||
|
// GoFormat checks the formatting of committed regular Go source files.
|
||||||
|
// Omitted or false disables the check.
|
||||||
|
GoFormat bool `json:"go_format,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// GoValidationSettings selects checks performed by the Go toolchain.
|
// GoValidationSettings selects checks performed by the Go toolchain.
|
||||||
|
|||||||
@@ -144,7 +144,7 @@ func TestLoadSettingsDefaultsRemoteAndConfigPath(t *testing.T) {
|
|||||||
func TestLoadSettingsReadsGoValidation(t *testing.T) {
|
func TestLoadSettingsReadsGoValidation(t *testing.T) {
|
||||||
repoDir := t.TempDir()
|
repoDir := t.TempDir()
|
||||||
profilePath := filepath.Join(repoDir, "releasekit.json")
|
profilePath := filepath.Join(repoDir, "releasekit.json")
|
||||||
if err := os.WriteFile(profilePath, []byte(`{"product":"Example","expected_push_url":"ssh://git.example/owner/repo.git","validation":{"go":{"race":true,"full_vet":true,"compile_targets":["linux/amd64","linux/arm64","darwin/arm64"]}}}`), 0o600); err != nil {
|
if err := os.WriteFile(profilePath, []byte(`{"product":"Example","expected_push_url":"ssh://git.example/owner/repo.git","validation":{"go_format":true,"go":{"race":true,"full_vet":true,"compile_targets":["linux/amd64","linux/arm64","darwin/arm64"]}}}`), 0o600); err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -152,8 +152,8 @@ func TestLoadSettingsReadsGoValidation(t *testing.T) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
if settings.Validation == nil || !settings.Validation.Go.Race || !settings.Validation.Go.FullVet {
|
if settings.Validation == nil || !settings.Validation.GoFormat || !settings.Validation.Go.Race || !settings.Validation.Go.FullVet {
|
||||||
t.Fatalf("validation settings = %#v, want Go race and full vet enabled", settings.Validation)
|
t.Fatalf("validation settings = %#v, want Go formatting, race, and full vet enabled", settings.Validation)
|
||||||
}
|
}
|
||||||
if got, want := strings.Join(settings.Validation.Go.CompileTargets, ","), "linux/amd64,linux/arm64,darwin/arm64"; got != want {
|
if got, want := strings.Join(settings.Validation.Go.CompileTargets, ","), "linux/amd64,linux/arm64,darwin/arm64"; got != want {
|
||||||
t.Fatalf("compile targets = %q, want %q", got, want)
|
t.Fatalf("compile targets = %q, want %q", got, want)
|
||||||
|
|||||||
@@ -161,6 +161,7 @@ func validationSpec(settings *cli.ValidationSettings) releasekit.ValidationSpec
|
|||||||
return releasekit.ValidationSpec{}
|
return releasekit.ValidationSpec{}
|
||||||
}
|
}
|
||||||
result := releasekit.ValidationSpec{
|
result := releasekit.ValidationSpec{
|
||||||
|
GoFormat: settings.GoFormat,
|
||||||
Go: releasekit.GoValidationSpec{
|
Go: releasekit.GoValidationSpec{
|
||||||
Race: settings.Go.Race,
|
Race: settings.Go.Race,
|
||||||
FullVet: settings.Go.FullVet,
|
FullVet: settings.Go.FullVet,
|
||||||
|
|||||||
@@ -117,12 +117,16 @@ func TestCandidateRequestMapsGoValidation(t *testing.T) {
|
|||||||
Product: "Example Product",
|
Product: "Example Product",
|
||||||
ExpectedPushURL: "publication-only value",
|
ExpectedPushURL: "publication-only value",
|
||||||
Validation: &cli.ValidationSettings{
|
Validation: &cli.ValidationSettings{
|
||||||
Go: cli.GoValidationSettings{Race: true, FullVet: true, CompileTargets: []string{"linux/amd64", "darwin/arm64"}},
|
GoFormat: true,
|
||||||
|
Go: cli.GoValidationSettings{Race: true, FullVet: true, CompileTargets: []string{"linux/amd64", "darwin/arm64"}},
|
||||||
},
|
},
|
||||||
}, nil)
|
}, nil)
|
||||||
if !request.Validation.Go.Race || !request.Validation.Go.FullVet {
|
if !request.Validation.Go.Race || !request.Validation.Go.FullVet {
|
||||||
t.Fatalf("candidate request validation = %#v, want Go race and full vet enabled", request.Validation)
|
t.Fatalf("candidate request validation = %#v, want Go race and full vet enabled", request.Validation)
|
||||||
}
|
}
|
||||||
|
if !request.Validation.GoFormat {
|
||||||
|
t.Fatalf("candidate request GoFormat = false, want true")
|
||||||
|
}
|
||||||
if request.ExpectedPushURL != "" {
|
if request.ExpectedPushURL != "" {
|
||||||
t.Fatalf("candidate request carried publication-only expected push URL %q", request.ExpectedPushURL)
|
t.Fatalf("candidate request carried publication-only expected push URL %q", request.ExpectedPushURL)
|
||||||
}
|
}
|
||||||
|
|||||||
17
docs/cli.md
17
docs/cli.md
@@ -27,6 +27,7 @@ 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
|
local Git state, runs the standard offline Go test suite (with `-race` when
|
||||||
`validation.go.race` is true), and runs a separate `go vet -mod=readonly ./...`
|
`validation.go.race` is true), and runs a separate `go vet -mod=readonly ./...`
|
||||||
pass after successful tests when `validation.go.full_vet` is true. It then runs
|
pass after successful tests when `validation.go.full_vet` is true. It then runs
|
||||||
|
selected package compilation and committed Go formatting, followed by
|
||||||
configured additional checks in profile order. It does not inspect remote
|
configured additional checks in profile order. It does not inspect remote
|
||||||
availability or publish a tag.
|
availability or publish a tag.
|
||||||
|
|
||||||
@@ -34,7 +35,11 @@ When `validation.go.compile_targets` is selected, `check` compiles all packages
|
|||||||
for each configured `GOOS/GOARCH` pair after tests and full vet, using static
|
for each configured `GOOS/GOARCH` pair after tests and full vet, using static
|
||||||
mode with cgo disabled. This check produces no artifacts and does not execute
|
mode with cgo disabled. This check produces no artifacts and does not execute
|
||||||
target binaries. Compilation failures use the `package-compile` identity and
|
target binaries. Compilation failures use the `package-compile` identity and
|
||||||
show the selected target. Any artifact build selected by the separate `build`
|
show the selected target. When `validation.go_format` is true, `check` examines
|
||||||
|
regular committed Go files with the Go formatter after tests, vet, and package
|
||||||
|
compilation. It includes vendor and nested-module source, ignores the worktree
|
||||||
|
and symlink blobs, and reports unformatted paths as `go-format` without
|
||||||
|
modifying files. Any artifact build selected by the separate `build`
|
||||||
setting runs after shared validation and configured checks.
|
setting runs after shared validation and configured checks.
|
||||||
|
|
||||||
`check` writes errors and diagnostics to standard error. It returns status 0
|
`check` writes errors and diagnostics to standard error. It returns status 0
|
||||||
@@ -55,8 +60,9 @@ settings as `check`. It checks the authoritative remote and validates the
|
|||||||
candidate: the single standard offline Go test suite runs first, with `-race`
|
candidate: the single standard offline Go test suite runs first, with `-race`
|
||||||
when `validation.go.race` is true. When `validation.go.full_vet` is true,
|
when `validation.go.race` is true. When `validation.go.full_vet` is true,
|
||||||
test-time vet is disabled and `go vet -mod=readonly ./...` runs after successful
|
test-time vet is disabled and `go vet -mod=readonly ./...` runs after successful
|
||||||
tests. Configured checks then run in profile order. It then creates an annotated
|
tests. Selected package compilation and committed Go formatting follow, then
|
||||||
local tag and pushes only its exact tag object. It prints the confirmed version,
|
configured checks run 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
|
candidate commit, and tag object
|
||||||
on success. Validation
|
on success. Validation
|
||||||
progress and failed publication state, including the captured publication
|
progress and failed publication state, including the captured publication
|
||||||
@@ -69,7 +75,10 @@ Shared validation failures use the same check and finding presentation as
|
|||||||
When `validation.go.compile_targets` is selected, package compilation runs
|
When `validation.go.compile_targets` is selected, package compilation runs
|
||||||
after tests and full vet and before configured checks. Each failure is reported
|
after tests and full vet and before configured checks. Each failure is reported
|
||||||
as `package-compile` with its selected target; this does not create build
|
as `package-compile` with its selected target; this does not create build
|
||||||
artifacts or execute target binaries.
|
artifacts or execute target binaries. When `validation.go_format` is true,
|
||||||
|
committed Go formatting runs after package compilation and before configured
|
||||||
|
checks. A failure is reported as `go-format` with affected repository paths;
|
||||||
|
parser and source-inspection causes are retained.
|
||||||
|
|
||||||
The optional profile field `expected_push_url` applies only to `publish`. A
|
The optional profile field `expected_push_url` applies only to `publish`. A
|
||||||
nonempty value must exactly match the effective push URL resolved by Git;
|
nonempty value must exactly match the effective push URL resolved by Git;
|
||||||
|
|||||||
@@ -39,16 +39,25 @@ This source compatibility check does not name or produce binaries, run a target
|
|||||||
executable, or create an artifact manifest. Omitted or empty `compile_targets`
|
executable, or create an artifact manifest. Omitted or empty `compile_targets`
|
||||||
selects no package-compilation pass. When `full_vet` is omitted or false, the
|
selects no package-compilation pass. When `full_vet` is omitted or false, the
|
||||||
test command keeps its existing vet behavior. These settings are independent.
|
test command keeps its existing vet behavior. These settings are independent.
|
||||||
|
`validation.go_format` checks regular committed Go blobs with the Go formatter
|
||||||
|
used to build ReleaseKit. It includes vendor and nested-module source, ignores
|
||||||
|
symlink blobs and the worktree, and reports every selected path whose bytes
|
||||||
|
change under formatting. It never writes formatted bytes back. Omitted or false
|
||||||
|
disables the check. It runs after tests, selected full vet, and selected package
|
||||||
|
compilation, before configured product checks.
|
||||||
For example:
|
For example:
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"product": "Example",
|
"product": "Example",
|
||||||
"validation": {"go": {
|
"validation": {
|
||||||
"race": true,
|
"go": {
|
||||||
"full_vet": true,
|
"race": true,
|
||||||
"compile_targets": ["linux/amd64", "linux/arm64"]
|
"full_vet": true,
|
||||||
}}
|
"compile_targets": ["linux/amd64", "linux/arm64"]
|
||||||
|
},
|
||||||
|
"go_format": true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -73,7 +82,9 @@ access disabled, adding `-race` when `validation.go.race` is true. When
|
|||||||
run native `go vet -mod=readonly ./...` after successful tests. They then run
|
run native `go vet -mod=readonly ./...` after successful tests. They then run
|
||||||
`go build -mod=readonly -o <os.DevNull> ./...` in static mode for each
|
`go build -mod=readonly -o <os.DevNull> ./...` in static mode for each
|
||||||
`validation.go.compile_targets` entry, in profile order. Compile failures use
|
`validation.go.compile_targets` entry, in profile order. Compile failures use
|
||||||
the `package-compile` identity and include the selected target. Configured
|
the `package-compile` identity and include the selected target. When selected,
|
||||||
|
`validation.go_format` checks regular committed Go source with go/format after
|
||||||
|
package compilation; unformatted paths use the `go-format` identity. Configured
|
||||||
`checks` commands run afterward, once each in order, as additional product
|
`checks` commands run afterward, once each in order, as additional product
|
||||||
checks. Artifact builds selected by the separate `build` setting run after
|
checks. Artifact builds selected by the separate `build` setting run after
|
||||||
shared validation and product checks.
|
shared validation and product checks.
|
||||||
|
|||||||
@@ -36,6 +36,11 @@ complete target pair after tests and vet. Package compilation uses
|
|||||||
`CGO_ENABLED=0`, writes no artifact, and does not execute a target binary; its
|
`CGO_ENABLED=0`, writes no artifact, and does not execute a target binary; its
|
||||||
failures use the `package-compile` identity and include the target. The zero
|
failures use the `package-compile` identity and include the target. The zero
|
||||||
values retain the ordinary native test behavior and select no compilation.
|
values retain the ordinary native test behavior and select no compilation.
|
||||||
|
Set `Validation.GoFormat` to check committed regular Go files using the Go
|
||||||
|
formatter built into ReleaseKit. It includes vendor and nested-module source,
|
||||||
|
ignores symlink blobs and worktree changes, and reports unformatted paths as
|
||||||
|
`go-format` without rewriting files. Its zero value disables the check; it runs
|
||||||
|
after tests, selected vet, and package compilation, before `AdditionalChecks`.
|
||||||
ReleaseKit calls `AdditionalChecks` once after shared validation when supplied.
|
ReleaseKit calls `AdditionalChecks` once after shared validation when supplied.
|
||||||
It does not download a different toolchain automatically. If race prerequisites
|
It does not download a different toolchain automatically. If race prerequisites
|
||||||
are unavailable, the `go-test` check fails rather than being skipped; a selected
|
are unavailable, the `go-test` check fails rather than being skipped; a selected
|
||||||
|
|||||||
@@ -48,6 +48,13 @@ pair after tests and vet. This uses `CGO_ENABLED=0`, produces no artifact, and
|
|||||||
does not execute target binaries. A failure is reported as `package-compile`
|
does not execute target binaries. A failure is reported as `package-compile`
|
||||||
with the selected target and prevents tag creation.
|
with the selected target and prevents tag creation.
|
||||||
|
|
||||||
|
Set `Validation.GoFormat` to check committed regular Go source using the Go
|
||||||
|
formatter built into ReleaseKit. It includes vendor and nested modules, ignores
|
||||||
|
worktree changes and symlink blobs, and reports unformatted paths as
|
||||||
|
`go-format` without changing files. The default is disabled. The check follows
|
||||||
|
tests, selected vet, and package compilation, and a failure stops publication
|
||||||
|
before product checks or tag creation.
|
||||||
|
|
||||||
If `Build` selects targets, ReleaseKit verifies them after the standard suite
|
If `Build` selects targets, ReleaseKit verifies them after the standard suite
|
||||||
and additional checks, before creating the tag. The operation then rechecks the
|
and additional checks, before creating the tag. The operation then rechecks the
|
||||||
candidate and remote guards, creates an annotated tag for the captured commit,
|
candidate and remote guards, creates an annotated tag for the captured commit,
|
||||||
|
|||||||
@@ -18,9 +18,13 @@ executing a target binary. Test, vet, and package-compilation failures retain
|
|||||||
their `go-test`, `go-vet`, and `package-compile` identities, selected target
|
their `go-test`, `go-vet`, and `package-compile` identities, selected target
|
||||||
where applicable, and underlying causes. Its
|
where applicable, and underlying causes. Its
|
||||||
request carries the captured repository directory and commit, output writer,
|
request carries the captured repository directory and commit, output writer,
|
||||||
and internal settings. `Request.ReadCandidateTree` lets a
|
and internal settings. When selected, the Go-format check loads the candidate
|
||||||
selected file check request candidate contents; the Go-only path does not
|
tree once, reads regular Go blobs from the captured commit, and compares them
|
||||||
enumerate the tree.
|
with go/format output. It includes vendor and nested-module files, skips
|
||||||
|
symlink blobs, reports all unformatted paths, and stops with a contextual cause
|
||||||
|
on parse or inspection errors. It runs before product checks and build callbacks.
|
||||||
|
`Request.ReadCandidateTree` lets selected file checks request candidate
|
||||||
|
contents; the Go-only path does not enumerate the tree.
|
||||||
`CandidateTree` gets metadata and bytes from Git objects, reads blob bytes
|
`CandidateTree` gets metadata and bytes from Git objects, reads blob bytes
|
||||||
lazily, and resolves destinations without consulting the worktree or
|
lazily, and resolves destinations without consulting the worktree or
|
||||||
neighboring files.
|
neighboring files.
|
||||||
|
|||||||
@@ -318,6 +318,11 @@ Existing endpoint-change and uncertain-push tests keep their ownership.
|
|||||||
|
|
||||||
## Stage 5: Check committed Go formatting
|
## Stage 5: Check committed Go formatting
|
||||||
|
|
||||||
|
**Completion:** Complete. `ValidationSpec.GoFormat` and profile
|
||||||
|
`validation.go_format` run `go/format` on regular Go blobs from the captured
|
||||||
|
commit. Findings identify unformatted paths; parser and inspection failures
|
||||||
|
retain their causes, and the check leaves repository contents unchanged.
|
||||||
|
|
||||||
**Outcome:** A declaration replaces tracked-file formatting loops.
|
**Outcome:** A declaration replaces tracked-file formatting loops.
|
||||||
|
|
||||||
1. Add `GoFormat` and `validation.go_format` with working executor integration.
|
1. Add `GoFormat` and `validation.go_format` with working executor integration.
|
||||||
|
|||||||
@@ -78,6 +78,36 @@ func TestCheckCapturesCandidateAndCallsAdditionalChecksOnce(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestCheckStopsBeforeProductAndBuildCallbacksWhenGoFormatFails(t *testing.T) {
|
||||||
|
repository := newRepository(t, "# Example Product v1.2.3\n\nRelease note.\n")
|
||||||
|
writeTestFile(t, repository, "unformatted.go", "package candidate\nfunc Value( ) int{return 1}\n")
|
||||||
|
gitRun(t, repository, "add", "unformatted.go")
|
||||||
|
gitRun(t, repository, "commit", "-m", "add unformatted Go source")
|
||||||
|
productCalls := 0
|
||||||
|
buildCalls := 0
|
||||||
|
_, err := Check(context.Background(), Request{
|
||||||
|
RepositoryDir: repository,
|
||||||
|
Product: "Example Product",
|
||||||
|
Version: testVersion,
|
||||||
|
Validation: validation.Settings{GoFormat: true},
|
||||||
|
AdditionalChecks: func(context.Context, Candidate) error {
|
||||||
|
productCalls++
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
AfterValidate: func(context.Context, Candidate) error {
|
||||||
|
buildCalls++
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
})
|
||||||
|
var failure *validation.Failure
|
||||||
|
if !errors.As(err, &failure) || failure.Check != validation.GoFormat {
|
||||||
|
t.Fatalf("Check error = %v, want go-format failure", err)
|
||||||
|
}
|
||||||
|
if productCalls != 0 || buildCalls != 0 {
|
||||||
|
t.Fatalf("product callback calls = %d, build callback calls = %d; want neither after formatting failure", productCalls, buildCalls)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestCheckRunsDefaultOfflineGoValidation(t *testing.T) {
|
func TestCheckRunsDefaultOfflineGoValidation(t *testing.T) {
|
||||||
repository := newRepository(t, "# Example Product v1.2.3\n\nRelease note.\n")
|
repository := newRepository(t, "# Example Product v1.2.3\n\nRelease note.\n")
|
||||||
var output bytes.Buffer
|
var output bytes.Buffer
|
||||||
|
|||||||
@@ -15,6 +15,8 @@ const (
|
|||||||
GoVet Check = "go-vet"
|
GoVet Check = "go-vet"
|
||||||
// PackageCompile identifies selected-target package compilation.
|
// PackageCompile identifies selected-target package compilation.
|
||||||
PackageCompile Check = "package-compile"
|
PackageCompile Check = "package-compile"
|
||||||
|
// GoFormat identifies committed Go source formatting.
|
||||||
|
GoFormat Check = "go-format"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Finding describes a validation observation at an optional source location.
|
// Finding describes a validation observation at an optional source location.
|
||||||
|
|||||||
133
internal/validation/format_test.go
Normal file
133
internal/validation/format_test.go
Normal file
@@ -0,0 +1,133 @@
|
|||||||
|
package validation
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"gitea.maximumdirect.net/eric/ReleaseKit/internal/git"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestCheckGoFormatUsesCapturedRegularGoBlobs(t *testing.T) {
|
||||||
|
repository := newTreeRepository(t)
|
||||||
|
writeTreeFile(t, repository, "main.go", []byte("package main\n\nfunc main() {}\n"))
|
||||||
|
writeTreeFile(t, repository, "vendor/example/format.go", []byte("package example\nfunc Value( ){return 1}\n"))
|
||||||
|
writeTreeFile(t, repository, "nested/module/odd name.go", []byte("package nested\nfunc Value( ){return 2}\n"))
|
||||||
|
writeTreeFile(t, repository, "notes.txt", []byte("not Go source\n"))
|
||||||
|
if err := os.Symlink("main.go", filepath.Join(repository, "link.go")); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
gitTree(t, repository, "add", ".")
|
||||||
|
gitTree(t, repository, "commit", "-m", "capture Go files")
|
||||||
|
commit := strings.TrimSpace(gitTree(t, repository, "rev-parse", "HEAD"))
|
||||||
|
|
||||||
|
// Worktree edits must not affect the formatting result for the captured commit.
|
||||||
|
writeTreeFile(t, repository, "main.go", []byte("package main\nfunc main( ){}\n"))
|
||||||
|
writeTreeFile(t, repository, "vendor/example/format.go", []byte("package example\n\nfunc Value() { return 1 }\n"))
|
||||||
|
worktreeMain, err := os.ReadFile(filepath.Join(repository, "main.go"))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
tree, err := ReadCandidateTree(context.Background(), repository, commit)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
err = checkGoFormat(context.Background(), tree)
|
||||||
|
var failure *Failure
|
||||||
|
if !errors.As(err, &failure) || failure.Check != GoFormat {
|
||||||
|
t.Fatalf("checkGoFormat error = %v, want go-format failure", err)
|
||||||
|
}
|
||||||
|
wantPaths := []string{"nested/module/odd name.go", "vendor/example/format.go"}
|
||||||
|
if len(failure.Findings) != len(wantPaths) {
|
||||||
|
t.Fatalf("format findings = %#v, want paths %q", failure.Findings, wantPaths)
|
||||||
|
}
|
||||||
|
for index, path := range wantPaths {
|
||||||
|
if failure.Findings[index].Path != path {
|
||||||
|
t.Errorf("format finding %d path = %q, want %q", index, failure.Findings[index].Path, path)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
worktreeMainAfter, err := os.ReadFile(filepath.Join(repository, "main.go"))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if string(worktreeMainAfter) != string(worktreeMain) {
|
||||||
|
t.Fatalf("format check changed worktree source: got %q, want %q", worktreeMainAfter, worktreeMain)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCheckGoFormatPreservesParserAndInspectionCauses(t *testing.T) {
|
||||||
|
t.Run("parser", func(t *testing.T) {
|
||||||
|
repository := newTreeRepository(t)
|
||||||
|
writeTreeFile(t, repository, "broken.go", []byte("package broken\nfunc ( {\n"))
|
||||||
|
gitTree(t, repository, "add", ".")
|
||||||
|
gitTree(t, repository, "commit", "-m", "capture invalid Go source")
|
||||||
|
commit := strings.TrimSpace(gitTree(t, repository, "rev-parse", "HEAD"))
|
||||||
|
tree, err := ReadCandidateTree(context.Background(), repository, commit)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
err = checkGoFormat(context.Background(), tree)
|
||||||
|
var failure *Failure
|
||||||
|
if !errors.As(err, &failure) || failure.Check != GoFormat || failure.Cause == nil {
|
||||||
|
t.Fatalf("parser failure = %#v, want go-format failure with cause", err)
|
||||||
|
}
|
||||||
|
if len(failure.Findings) != 1 || failure.Findings[0].Path != "broken.go" {
|
||||||
|
t.Fatalf("parser findings = %#v, want broken.go", failure.Findings)
|
||||||
|
}
|
||||||
|
if !strings.Contains(failure.Cause.Error(), "broken.go") {
|
||||||
|
t.Fatalf("parser cause = %v, want committed filename context", failure.Cause)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("inspection", func(t *testing.T) {
|
||||||
|
tree := &CandidateTree{
|
||||||
|
repositoryDir: t.TempDir(),
|
||||||
|
entries: []git.TreeEntry{{Path: "missing.go", ObjectID: strings.Repeat("0", 40), Mode: "100644", Type: "blob"}},
|
||||||
|
byPath: map[string]int{"missing.go": 0},
|
||||||
|
blobs: make(map[string][]byte),
|
||||||
|
}
|
||||||
|
err := checkGoFormat(context.Background(), tree)
|
||||||
|
var failure *Failure
|
||||||
|
if !errors.As(err, &failure) || failure.Check != GoFormat || failure.Cause == nil {
|
||||||
|
t.Fatalf("inspection failure = %#v, want go-format failure with cause", err)
|
||||||
|
}
|
||||||
|
if len(failure.Findings) != 1 || failure.Findings[0].Path != "missing.go" {
|
||||||
|
t.Fatalf("inspection findings = %#v, want missing.go", failure.Findings)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRunGoFormatIsOptionalAndStopsAfterFinding(t *testing.T) {
|
||||||
|
repository := newTreeRepository(t)
|
||||||
|
writeTreeFile(t, repository, "unformatted.go", []byte("package fixture\nfunc Value( ){return 1}\n"))
|
||||||
|
gitTree(t, repository, "add", ".")
|
||||||
|
gitTree(t, repository, "commit", "-m", "capture unformatted Go source")
|
||||||
|
commit := strings.TrimSpace(gitTree(t, repository, "rev-parse", "HEAD"))
|
||||||
|
|
||||||
|
logPath := installGoShim(t, "", false)
|
||||||
|
if err := Run(context.Background(), Request{RepositoryDir: repository, Commit: commit}); err != nil {
|
||||||
|
t.Fatalf("default validation = %v, want success with formatting disabled", err)
|
||||||
|
}
|
||||||
|
if got := strings.Join(readGoShimLog(t, logPath), "\n"); !strings.Contains(got, "test -mod=readonly -count=1 ./...") {
|
||||||
|
t.Fatalf("default Go commands = %q, want standard suite", got)
|
||||||
|
}
|
||||||
|
|
||||||
|
logPath = installGoShim(t, "", false)
|
||||||
|
err := Run(context.Background(), Request{
|
||||||
|
RepositoryDir: repository,
|
||||||
|
Commit: commit,
|
||||||
|
Settings: Settings{GoFormat: true},
|
||||||
|
})
|
||||||
|
var failure *Failure
|
||||||
|
if !errors.As(err, &failure) || failure.Check != GoFormat {
|
||||||
|
t.Fatalf("selected formatting error = %v, want go-format failure", err)
|
||||||
|
}
|
||||||
|
commands := strings.Join(readGoShimLog(t, logPath), "\n")
|
||||||
|
if strings.Contains(commands, "build ") || strings.Contains(commands, "vet ") {
|
||||||
|
t.Fatalf("later Go commands ran after formatting failure: %q", commands)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,8 +2,10 @@
|
|||||||
package validation
|
package validation
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"go/format"
|
||||||
"io"
|
"io"
|
||||||
"os"
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
@@ -16,6 +18,7 @@ type Settings struct {
|
|||||||
Race bool
|
Race bool
|
||||||
FullVet bool
|
FullVet bool
|
||||||
CompileTargets []Target
|
CompileTargets []Target
|
||||||
|
GoFormat bool
|
||||||
}
|
}
|
||||||
|
|
||||||
// Target selects one package-compilation target.
|
// Target selects one package-compilation target.
|
||||||
@@ -88,5 +91,55 @@ func Run(ctx context.Context, request Request) error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if request.Settings.GoFormat {
|
||||||
|
tree, err := request.ReadCandidateTree(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return &Failure{Check: GoFormat, Cause: err}
|
||||||
|
}
|
||||||
|
if err := checkGoFormat(ctx, tree); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func checkGoFormat(ctx context.Context, tree *CandidateTree) error {
|
||||||
|
var findings []Finding
|
||||||
|
for _, entry := range tree.Entries() {
|
||||||
|
if entry.Type != "blob" || (entry.Mode != "100644" && entry.Mode != "100755") || !strings.HasSuffix(entry.Path, ".go") {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if err := ctx.Err(); err != nil {
|
||||||
|
return &Failure{Check: GoFormat, Cause: err}
|
||||||
|
}
|
||||||
|
contents, err := tree.ReadFile(ctx, entry.Path)
|
||||||
|
if err != nil {
|
||||||
|
return &Failure{
|
||||||
|
Check: GoFormat,
|
||||||
|
Findings: []Finding{{
|
||||||
|
Path: entry.Path,
|
||||||
|
Message: "unable to inspect committed Go source",
|
||||||
|
}},
|
||||||
|
Cause: fmt.Errorf("inspect committed Go file %q: %w", entry.Path, err),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
formatted, err := format.Source(contents)
|
||||||
|
if err != nil {
|
||||||
|
return &Failure{
|
||||||
|
Check: GoFormat,
|
||||||
|
Findings: []Finding{{
|
||||||
|
Path: entry.Path,
|
||||||
|
Message: "committed Go source cannot be formatted",
|
||||||
|
}},
|
||||||
|
Cause: fmt.Errorf("format committed Go file %q: %w", entry.Path, err),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !bytes.Equal(contents, formatted) {
|
||||||
|
findings = append(findings, Finding{Path: entry.Path, Message: "file is not gofmt-formatted"})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(findings) != 0 {
|
||||||
|
return &Failure{Check: GoFormat, Findings: findings}
|
||||||
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,7 +8,8 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
// ValidationCheck identifies a shared validation check that failed.
|
// ValidationCheck identifies a shared validation check that failed.
|
||||||
// Current checks include "go-test", "go-vet", and "package-compile".
|
// Current checks include "go-test", "go-vet", "package-compile", and
|
||||||
|
// "go-format".
|
||||||
type ValidationCheck string
|
type ValidationCheck string
|
||||||
|
|
||||||
// ValidationFinding describes a failed check observation at an optional
|
// ValidationFinding describes a failed check observation at an optional
|
||||||
|
|||||||
@@ -12,10 +12,10 @@ func TestCandidateRequestMapsGoValidation(t *testing.T) {
|
|||||||
Validation: ValidationSpec{Go: GoValidationSpec{
|
Validation: ValidationSpec{Go: GoValidationSpec{
|
||||||
Race: true, FullVet: true,
|
Race: true, FullVet: true,
|
||||||
CompileTargets: []Target{{OS: "linux", Arch: "amd64"}, {OS: "darwin", Arch: "arm64"}},
|
CompileTargets: []Target{{OS: "linux", Arch: "amd64"}, {OS: "darwin", Arch: "arm64"}},
|
||||||
}},
|
}, GoFormat: true},
|
||||||
})
|
})
|
||||||
if !request.Validation.Race || !request.Validation.FullVet {
|
if !request.Validation.Race || !request.Validation.FullVet || !request.Validation.GoFormat {
|
||||||
t.Fatalf("candidate request validation = %#v, want race and full vet enabled", request.Validation)
|
t.Fatalf("candidate request validation = %#v, want race, full vet, and formatting enabled", request.Validation)
|
||||||
}
|
}
|
||||||
want := []validation.Target{{OS: "linux", Arch: "amd64"}, {OS: "darwin", Arch: "arm64"}}
|
want := []validation.Target{{OS: "linux", Arch: "amd64"}, {OS: "darwin", Arch: "arm64"}}
|
||||||
if len(request.Validation.CompileTargets) != len(want) {
|
if len(request.Validation.CompileTargets) != len(want) {
|
||||||
@@ -39,3 +39,25 @@ func TestValidationErrorPreservesGoVetIdentity(t *testing.T) {
|
|||||||
t.Fatalf("converted error = %v, want cause preserved", got)
|
t.Fatalf("converted error = %v, want cause preserved", got)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestValidationErrorPreservesGoFormatIdentityAndFindings(t *testing.T) {
|
||||||
|
cause := errors.New("format parser failed")
|
||||||
|
got := convertValidationError(&validation.Failure{
|
||||||
|
Check: validation.GoFormat,
|
||||||
|
Findings: []validation.Finding{{
|
||||||
|
Path: "vendor/example/bad.go",
|
||||||
|
Message: "committed Go source cannot be formatted",
|
||||||
|
}},
|
||||||
|
Cause: cause,
|
||||||
|
})
|
||||||
|
var validationError *ValidationError
|
||||||
|
if !errors.As(got, &validationError) || validationError.Check != "go-format" {
|
||||||
|
t.Fatalf("converted error = %#v, want go-format validation identity", got)
|
||||||
|
}
|
||||||
|
if len(validationError.Findings) != 1 || validationError.Findings[0].Path != "vendor/example/bad.go" {
|
||||||
|
t.Fatalf("converted findings = %#v, want vendor path", validationError.Findings)
|
||||||
|
}
|
||||||
|
if !errors.Is(got, cause) {
|
||||||
|
t.Fatalf("converted error = %v, want parser cause preserved", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user