Apply module scope to file validation checks
This commit is contained in:
2
check.go
2
check.go
@@ -87,7 +87,7 @@ type FileSizeValidationSpec struct {
|
||||
// LimitBytes is the maximum decimal byte count. Nil selects 1,000,000;
|
||||
// zero is a zero-byte limit, and negative values are invalid.
|
||||
LimitBytes *int64
|
||||
// Exclusions are literal repository-relative file or directory paths.
|
||||
// Exclusions are literal module-relative file or directory paths.
|
||||
// A directory exclusion includes committed descendants.
|
||||
Exclusions []string
|
||||
}
|
||||
|
||||
@@ -95,9 +95,10 @@ go.work.sum paths as `workspace-files`. Formatting failures are reported as
|
||||
`go-format` with affected paths and preserve parser or source-inspection causes.
|
||||
|
||||
When `validation.module_distribution.local_replacements` is true, the command
|
||||
parses committed `go.mod` declarations, including nested and vendor modules,
|
||||
after workspace-file inspection. Filesystem replacements use the
|
||||
`local-replacements` identity and include the declaration path, source line,
|
||||
parses committed `go.mod` declarations in the selected module after
|
||||
workspace-file inspection. Vendored contents remain in scope, while
|
||||
independently released nested modules are excluded. Filesystem replacements use
|
||||
the `local-replacements` identity and include the declaration path, source line,
|
||||
and target; versioned replacements are permitted. Committed module symlinks are
|
||||
resolved within the candidate, regardless of `GOWORK`.
|
||||
When `validation.file_size` is a non-null object, the command checks each
|
||||
|
||||
@@ -69,12 +69,13 @@ Anchors keep Unicode letters and numbers, underscores, and hyphens; spaces
|
||||
become hyphens, and repeated headings receive numeric suffixes.
|
||||
|
||||
When `validation.module_distribution.workspace_files` is true, it reports
|
||||
committed blob entries whose basename is `go.work` or `go.work.sum`, including
|
||||
nested paths and symlinks. It checks candidate distribution contents regardless
|
||||
committed `go.work` and `go.work.sum` files in the selected module inputs,
|
||||
including nested paths and symlinks. It checks the captured candidate regardless
|
||||
of `GOWORK`; it does not select or change the workspace used by Go commands,
|
||||
and it does not alter existing vendor behavior. Omitted or false disables it.
|
||||
When `validation.module_distribution.local_replacements` is true, it parses all
|
||||
committed `go.mod` declarations, including vendor and nested modules, and reports
|
||||
and vendored contents remain in scope. Omitted or false disables it. When
|
||||
`validation.module_distribution.local_replacements` is true, it parses the
|
||||
selected module's committed `go.mod` declarations. Vendored contents remain in
|
||||
scope, while independently released nested modules are excluded. It reports
|
||||
`replace` directives whose replacement has no version. These filesystem
|
||||
replacements include relative and absolute paths; versioned module replacements
|
||||
are permitted. Committed `go.mod` symlinks are resolved within the candidate,
|
||||
@@ -109,8 +110,8 @@ check; `{}` enables it with a default of 1,000,000 bytes. A missing or null
|
||||
`limit_bytes` selects that default, zero is a valid limit, and negative limits
|
||||
are invalid.
|
||||
|
||||
`validation.file_size.exclusions` contains literal repository-relative file or
|
||||
directory paths. Directory exclusions include committed descendants, using path
|
||||
`validation.file_size.exclusions` contains literal paths relative to the
|
||||
selected module. Directory exclusions include committed descendants, using path
|
||||
boundaries (`assets` does not exclude `assets-old`). Ordinary relative path
|
||||
normalization applies; metacharacters are literal and unmatched paths have no
|
||||
effect. The check measures regular and symlink blobs, including small binaries;
|
||||
|
||||
@@ -43,22 +43,24 @@ ignores symlink blobs and worktree changes, and reports unformatted paths as
|
||||
after tests, selected vet, and package compilation, before `AdditionalChecks`.
|
||||
|
||||
Set `Validation.ModuleDistribution.WorkspaceFiles` to reject committed
|
||||
`go.work` and `go.work.sum` blobs, including nested and symlink entries. It
|
||||
examines the captured candidate even when `GOWORK=off`, ignores uncommitted
|
||||
files, and does not change Go test workspace selection or vendor behavior. The
|
||||
zero value disables the check; failures use `workspace-files`.
|
||||
`go.work` and `go.work.sum` files in the selected module inputs, including
|
||||
nested and symlink entries. It examines the captured candidate even when
|
||||
`GOWORK=off`, ignores uncommitted files, and does not change Go test workspace
|
||||
selection; vendored contents remain in scope. The zero value disables the check;
|
||||
failures use `workspace-files`.
|
||||
Set `Validation.ModuleDistribution.LocalReplacements` independently to parse
|
||||
committed `go.mod` declarations throughout the candidate, including nested and
|
||||
vendor modules. It reports filesystem `replace` directives with their source
|
||||
path, line, and target as `local-replacements`; versioned replacements are
|
||||
the selected module's committed `go.mod` declarations. Vendored contents remain
|
||||
in scope, while independently released nested modules are excluded. It reports
|
||||
filesystem `replace` directives with their source path, line, and target as
|
||||
`local-replacements`; versioned replacements are
|
||||
permitted. Committed module symlinks are resolved within the candidate. Parse
|
||||
and inspection errors retain their causes, and the rule applies even when
|
||||
`GOWORK=off`. Its zero value disables the check.
|
||||
Set `Validation.FileSize` to check each committed blob against a byte limit.
|
||||
Nil disables the check; an empty object enables the default of 1,000,000
|
||||
decimal bytes. `LimitBytes` is strict greater-than, accepts zero, and rejects
|
||||
negative values. `Exclusions` contains literal repository-relative files or
|
||||
directories; excluded directories include descendants. Symlink blobs are
|
||||
negative values. `Exclusions` contains literal files or directories relative to
|
||||
the selected module; excluded directories include descendants. Symlink blobs are
|
||||
measured by link-text bytes, and gitlinks are omitted. The check applies to the
|
||||
captured candidate and reports one oversized file with its measurement as
|
||||
`file-size`; it does not measure historical or aggregate size.
|
||||
|
||||
@@ -62,9 +62,10 @@ are ignored, and Go workspace selection and vendor behavior are unchanged. Its
|
||||
zero value disables the check. Findings use `workspace-files` and stop
|
||||
publication before product checks or tag creation.
|
||||
Set `Validation.ModuleDistribution.LocalReplacements` independently to parse
|
||||
committed `go.mod` declarations across the candidate, including nested and
|
||||
vendor modules. Filesystem `replace` directives are reported with their source
|
||||
path, line, and target as `local-replacements`; versioned replacements are
|
||||
selected module's committed `go.mod` declarations. Vendored contents remain in
|
||||
scope, while independently released nested modules are excluded. Filesystem
|
||||
`replace` directives are reported with their source path, line, and target as
|
||||
`local-replacements`; versioned replacements are
|
||||
permitted. Committed module symlinks are resolved within the candidate, and
|
||||
parse or inspection errors retain their causes. This check is independent of
|
||||
`GOWORK`; its zero value disables it, and a failure stops publication before
|
||||
@@ -72,9 +73,9 @@ product checks or tag creation.
|
||||
Set `Validation.FileSize` to check each committed blob against a byte limit.
|
||||
Nil disables the check; an empty object enables the default of 1,000,000
|
||||
decimal bytes. `LimitBytes` uses strict greater-than, accepts zero, and rejects
|
||||
negative values. `Exclusions` contains literal repository-relative files or
|
||||
directories, including committed descendants of excluded directories. Symlink
|
||||
blob size is its link text; gitlinks are omitted. This check measures only the
|
||||
negative values. `Exclusions` contains literal files or directories relative to
|
||||
the selected module, including committed descendants of excluded directories.
|
||||
Symlink blob size is its link text; gitlinks are omitted. This check measures only the
|
||||
captured candidate and reports one oversized file with its measurement as
|
||||
`file-size`; it stops publication before product checks or tag creation.
|
||||
|
||||
|
||||
@@ -332,6 +332,15 @@ at different paths keep distinct relative-link findings. Confirm selection
|
||||
errors with content checks disabled and preserve cancellation/cleanup regressions.
|
||||
Run affected validation and facade tests.
|
||||
|
||||
**Completed:** Formatting, workspace, local-replacement, file-size, and Markdown
|
||||
checks now iterate the selected committed inputs. Targeted reads still resolve
|
||||
against the full repository tree. File-size exclusions are translated from the
|
||||
selected module to repository-relative paths, and shared-file failures preserve
|
||||
their public check, path, and cause. Updated file-size guidance and verified
|
||||
in-scope and out-of-scope findings, shared inputs, cross-module anchors, reader
|
||||
cleanup, facade conversion, `go test ./internal/validation .`, and
|
||||
`go run ./internal/apicheck`.
|
||||
|
||||
## Stage 6: Execute Go validation in the module with reusable local settings
|
||||
|
||||
**Outcome:** One selected standard suite runs in the module, and later composite
|
||||
|
||||
@@ -12,9 +12,8 @@ func checkFileSize(tree *CandidateTree, settings *FileSizeSettings) error {
|
||||
if settings.LimitBytes != nil {
|
||||
limit = *settings.LimitBytes
|
||||
}
|
||||
exclusions := normalizedExclusions(settings.Exclusions)
|
||||
for _, entry := range tree.Entries() {
|
||||
if entry.Type != "blob" || !entry.HasSize || excludedByPath(entry.Path, exclusions) {
|
||||
for _, entry := range tree.inputEntries {
|
||||
if entry.Type != "blob" || !entry.HasSize || excludedByPath(entry.Path, settings.Exclusions) {
|
||||
continue
|
||||
}
|
||||
if entry.Size > limit {
|
||||
@@ -31,6 +30,23 @@ func checkFileSize(tree *CandidateTree, settings *FileSizeSettings) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func fileSizeSettingsForModule(settings *FileSizeSettings, modulePath string) *FileSizeSettings {
|
||||
if settings == nil {
|
||||
return nil
|
||||
}
|
||||
selected := *settings
|
||||
moduleRelative := make([]string, len(settings.Exclusions))
|
||||
for index, exclusion := range settings.Exclusions {
|
||||
if modulePath == "." || path.IsAbs(exclusion) || exclusion == "" {
|
||||
moduleRelative[index] = exclusion
|
||||
continue
|
||||
}
|
||||
moduleRelative[index] = modulePath + "/" + exclusion
|
||||
}
|
||||
selected.Exclusions = normalizedExclusions(moduleRelative)
|
||||
return &selected
|
||||
}
|
||||
|
||||
func normalizedExclusions(exclusions []string) []string {
|
||||
result := make([]string, 0, len(exclusions))
|
||||
for _, exclusion := range exclusions {
|
||||
|
||||
@@ -29,9 +29,10 @@ func TestCheckFileSizeUsesStrictLimitsAndDefault(t *testing.T) {
|
||||
}
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
tree := &CandidateTree{entries: []git.TreeEntry{{
|
||||
entries := []git.TreeEntry{{
|
||||
Path: "file.bin", Type: "blob", Mode: "100644", Size: test.size, HasSize: true,
|
||||
}}}
|
||||
}}
|
||||
tree := &CandidateTree{entries: entries, inputEntries: entries}
|
||||
err := checkFileSize(tree, &FileSizeSettings{LimitBytes: test.limit})
|
||||
if !test.wantError {
|
||||
if err != nil {
|
||||
@@ -104,10 +105,11 @@ func TestCheckFileSizeAppliesLiteralPathExclusions(t *testing.T) {
|
||||
}
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
err := checkFileSize(&CandidateTree{entries: test.entries}, &FileSizeSettings{
|
||||
settings := fileSizeSettingsForModule(&FileSizeSettings{
|
||||
LimitBytes: int64Pointer(10),
|
||||
Exclusions: test.exclusions,
|
||||
})
|
||||
}, ".")
|
||||
err := checkFileSize(&CandidateTree{entries: test.entries, inputEntries: test.entries}, settings)
|
||||
failure, ok := err.(*Failure)
|
||||
if !ok || failure.Check != FileSize || len(failure.Findings) != 1 || failure.Findings[0].Path != test.wantPath {
|
||||
t.Fatalf("checkFileSize() error = %v, want only %q to exceed the limit", err, test.wantPath)
|
||||
|
||||
@@ -89,6 +89,7 @@ func TestCheckGoFormatPreservesParserAndInspectionCauses(t *testing.T) {
|
||||
tree := &CandidateTree{
|
||||
repositoryDir: t.TempDir(),
|
||||
entries: []git.TreeEntry{{Path: "missing.go", ObjectID: strings.Repeat("0", 40), Mode: "100644", Type: "blob"}},
|
||||
inputEntries: []git.TreeEntry{{Path: "missing.go", ObjectID: strings.Repeat("0", 40), Mode: "100644", Type: "blob"}},
|
||||
byPath: map[string]int{"missing.go": 0},
|
||||
}
|
||||
cleanupCandidateTree(t, tree)
|
||||
|
||||
@@ -13,7 +13,7 @@ import (
|
||||
func checkLocalReplacements(ctx context.Context, tree *CandidateTree) error {
|
||||
var findings []Finding
|
||||
replacementsByObjectID := make(map[string][]moduleReplacementSummary)
|
||||
for _, entry := range tree.Entries() {
|
||||
for _, entry := range tree.inputEntries {
|
||||
if err := ctx.Err(); err != nil {
|
||||
return &Failure{Check: LocalReplacements, Findings: findings, Cause: err}
|
||||
}
|
||||
|
||||
@@ -29,9 +29,7 @@ func TestCheckLocalReplacementsParsesCommittedModuleDeclarations(t *testing.T) {
|
||||
t.Fatalf("checkLocalReplacements error = %v, want local-replacements failure", err)
|
||||
}
|
||||
want := map[string]Finding{
|
||||
"go.mod": {Path: "go.mod", Line: 3, Target: "../local module", Directive: "replace"},
|
||||
"nested/go.mod:4": {Path: "nested/go.mod", Line: 4, Target: "../../local", Directive: "replace"},
|
||||
"nested/go.mod:5": {Path: "nested/go.mod", Line: 5, Target: absoluteTarget, Directive: "replace"},
|
||||
"go.mod": {Path: "go.mod", Line: 3, Target: "../local module", Directive: "replace"},
|
||||
}
|
||||
if len(failure.Findings) != len(want) {
|
||||
t.Fatalf("findings = %#v, want %d filesystem replacements", failure.Findings, len(want))
|
||||
@@ -46,6 +44,27 @@ func TestCheckLocalReplacementsParsesCommittedModuleDeclarations(t *testing.T) {
|
||||
t.Errorf("finding = %#v, want %q", finding, key)
|
||||
}
|
||||
}
|
||||
|
||||
nestedTree, err := ReadCandidateTreeForScope(context.Background(), repository, commit, TreeScope{ModuleDir: filepath.Join(repository, "nested")})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
cleanupCandidateTree(t, nestedTree)
|
||||
err = checkLocalReplacements(context.Background(), nestedTree)
|
||||
if !errors.As(err, &failure) || failure.Check != LocalReplacements || len(failure.Findings) != 2 {
|
||||
t.Fatalf("nested replacement findings = %#v, want the selected module's two replacements", err)
|
||||
}
|
||||
for _, finding := range failure.Findings {
|
||||
wantLine := 4
|
||||
wantTarget := "../../local"
|
||||
if finding.Line == 5 {
|
||||
wantLine = 5
|
||||
wantTarget = absoluteTarget
|
||||
}
|
||||
if finding.Path != "nested/go.mod" || finding.Line != wantLine || finding.Target != wantTarget || finding.Directive != "replace" {
|
||||
t.Errorf("nested finding = %#v, want line %d target %q", finding, wantLine, wantTarget)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestCheckLocalReplacementsFollowsCommittedModuleSymlinks(t *testing.T) {
|
||||
@@ -58,7 +77,7 @@ func TestCheckLocalReplacementsFollowsCommittedModuleSymlinks(t *testing.T) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
commit := commitCandidateTree(t, repository)
|
||||
tree, err := ReadCandidateTree(context.Background(), repository, commit)
|
||||
tree, err := ReadCandidateTreeForScope(context.Background(), repository, commit, TreeScope{ModuleDir: filepath.Join(repository, "nested")})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -91,7 +110,10 @@ func TestCheckLocalReplacementsSharesSummariesAcrossModuleSymlinks(t *testing.T)
|
||||
}
|
||||
}
|
||||
commit := commitCandidateTree(t, repository)
|
||||
tree, err := ReadCandidateTree(context.Background(), repository, commit)
|
||||
tree, err := ReadCandidateTreeForScope(context.Background(), repository, commit, TreeScope{
|
||||
ModuleDir: repository,
|
||||
SharedFiles: []string{"first/go.mod", "second/go.mod"},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@ func checkMarkdownLinks(ctx context.Context, tree *CandidateTree, enforceCheckou
|
||||
}
|
||||
cache := newMarkdownCache(tree)
|
||||
var findings []Finding
|
||||
for _, entry := range tree.Entries() {
|
||||
for _, entry := range tree.inputEntries {
|
||||
if err := ctx.Err(); err != nil {
|
||||
return &Failure{Check: MarkdownLinks, Findings: findings, Cause: err}
|
||||
}
|
||||
|
||||
125
internal/validation/scope_checks_test.go
Normal file
125
internal/validation/scope_checks_test.go
Normal file
@@ -0,0 +1,125 @@
|
||||
package validation
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestRunFileChecksUseSelectedInputsAndFullReferenceLookup(t *testing.T) {
|
||||
repository := newTreeRepository(t)
|
||||
writeTreeFile(t, repository, "go.mod", []byte("module example.com/root\n\ngo 1.25.0\nreplace example.com/rootdep => ./missing\n"))
|
||||
writeTreeFile(t, repository, "go.work", []byte("go 1.25.0\n"))
|
||||
writeTreeFile(t, repository, "outside.go", []byte("package outside\nfunc broken( ){}\n"))
|
||||
writeTreeFile(t, repository, "outside.bin", []byte(strings.Repeat("x", 160)))
|
||||
writeTreeFile(t, repository, "nested/go.mod", []byte("module example.com/nested\n\ngo 1.25.0\n"))
|
||||
writeTreeFile(t, repository, "nested/source.go", []byte("package nested\n\nfunc ready() {}\n"))
|
||||
writeTreeFile(t, repository, "nested/ignored.bin", []byte(strings.Repeat("x", 160)))
|
||||
writeTreeFile(t, repository, "nested/docs/guide.md", []byte("# Guide\n\n[details](../../sibling/docs/target.md#details)\n"))
|
||||
writeTreeFile(t, repository, "nested/child/go.mod", []byte("module example.com/nested/child\n"))
|
||||
writeTreeFile(t, repository, "nested/child/broken.go", []byte("package child\nfunc broken( ){}\n"))
|
||||
writeTreeFile(t, repository, "nested/child/go.work", []byte("go 1.25.0\n"))
|
||||
writeTreeFile(t, repository, "sibling/go.mod", []byte("module example.com/sibling\n\nreplace example.com/siblingdep => ./missing\n"))
|
||||
writeTreeFile(t, repository, "sibling/go.work", []byte("go 1.25.0\n"))
|
||||
writeTreeFile(t, repository, "sibling/broken.go", []byte("package sibling\nfunc broken( ){}\n"))
|
||||
writeTreeFile(t, repository, "sibling/outside.bin", []byte(strings.Repeat("x", 160)))
|
||||
writeTreeFile(t, repository, "sibling/docs/target.md", []byte("# Details\n\n[broken](missing.md)\n"))
|
||||
writeTreeFile(t, repository, "shared/broken.go", []byte("package shared\nfunc broken( ){}\n"))
|
||||
commit := commitCandidateTree(t, repository)
|
||||
installGoShim(t, "", false)
|
||||
moduleDir := filepath.Join(repository, "nested")
|
||||
settings := Settings{
|
||||
GoFormat: true,
|
||||
ModuleDistribution: ModuleDistributionSettings{
|
||||
WorkspaceFiles: true,
|
||||
LocalReplacements: true,
|
||||
},
|
||||
FileSize: &FileSizeSettings{LimitBytes: int64Pointer(100), Exclusions: []string{"ignored.bin"}},
|
||||
Markdown: &MarkdownSettings{EnforceCheckoutBoundary: true},
|
||||
}
|
||||
request := Request{RepositoryDir: repository, ModuleDir: moduleDir, Commit: commit, Settings: settings}
|
||||
if err := Run(context.Background(), request); err != nil {
|
||||
t.Fatalf("selected module checks = %v, want out-of-scope violations ignored and cross-module anchor resolved", err)
|
||||
}
|
||||
|
||||
request.Settings.FileSize.Exclusions = nil
|
||||
err := Run(context.Background(), request)
|
||||
assertScopedFailure(t, err, FileSize, "nested/ignored.bin")
|
||||
|
||||
writeTreeFile(t, repository, "nested/broken.go", []byte("package nested\nfunc broken( ){}\n"))
|
||||
request.Commit = commitCandidateTree(t, repository)
|
||||
request.Settings = Settings{GoFormat: true}
|
||||
assertScopedFailure(t, Run(context.Background(), request), GoFormat, "nested/broken.go")
|
||||
|
||||
writeTreeFile(t, repository, "nested/go.work", []byte("go 1.25.0\n"))
|
||||
request.Commit = commitCandidateTree(t, repository)
|
||||
request.Settings = Settings{ModuleDistribution: ModuleDistributionSettings{WorkspaceFiles: true}}
|
||||
assertScopedFailure(t, Run(context.Background(), request), WorkspaceFiles, "nested/go.work")
|
||||
|
||||
writeTreeFile(t, repository, "nested/go.mod", []byte("module example.com/nested\n\ngo 1.25.0\nreplace example.com/nesteddep => ./missing\n"))
|
||||
request.Commit = commitCandidateTree(t, repository)
|
||||
request.Settings = Settings{ModuleDistribution: ModuleDistributionSettings{LocalReplacements: true}}
|
||||
assertScopedFailure(t, Run(context.Background(), request), LocalReplacements, "nested/go.mod")
|
||||
|
||||
writeTreeFile(t, repository, "nested/docs/guide.md", []byte("# Guide\n\n[details](../../sibling/docs/target.md#details)\n\n[missing](absent.md)\n"))
|
||||
request.Commit = commitCandidateTree(t, repository)
|
||||
request.Settings = Settings{Markdown: &MarkdownSettings{}}
|
||||
assertScopedFailure(t, Run(context.Background(), request), MarkdownLinks, "nested/docs/guide.md")
|
||||
|
||||
request.Settings = Settings{GoFormat: true, SharedFiles: []string{"shared/broken.go"}}
|
||||
assertScopedFailure(t, Run(context.Background(), request), GoFormat, "shared/broken.go")
|
||||
}
|
||||
|
||||
func assertScopedFailure(t *testing.T, err error, check Check, path string) {
|
||||
t.Helper()
|
||||
failure, ok := err.(*Failure)
|
||||
if !ok || failure.Check != check {
|
||||
t.Fatalf("validation error = %v, want %s failure", err, check)
|
||||
}
|
||||
for _, finding := range failure.Findings {
|
||||
if finding.Path == path {
|
||||
return
|
||||
}
|
||||
}
|
||||
t.Fatalf("%s findings = %#v, want path %q", check, failure.Findings, path)
|
||||
}
|
||||
|
||||
func TestFileSizeExclusionsAreModuleRelative(t *testing.T) {
|
||||
repository := newTreeRepository(t)
|
||||
writeTreeFile(t, repository, "nested/go.mod", []byte("module example.com/nested\n"))
|
||||
writeTreeFile(t, repository, "nested/dist/output.bin", []byte(strings.Repeat("x", 20)))
|
||||
writeTreeFile(t, repository, "nested/dist-old/output.bin", []byte(strings.Repeat("x", 20)))
|
||||
commit := commitCandidateTree(t, repository)
|
||||
tree, err := ReadCandidateTreeForScope(context.Background(), repository, commit, TreeScope{ModuleDir: filepath.Join(repository, "nested")})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
cleanupCandidateTree(t, tree)
|
||||
settings := fileSizeSettingsForModule(&FileSizeSettings{LimitBytes: int64Pointer(10), Exclusions: []string{"./dist/"}}, tree.modulePath)
|
||||
err = checkFileSize(tree, settings)
|
||||
var failure *Failure
|
||||
if !errors.As(err, &failure) || failure.Check != FileSize || len(failure.Findings) != 1 || failure.Findings[0].Path != "nested/dist-old/output.bin" {
|
||||
t.Fatalf("module-relative file-size exclusion error = %v, want only nested/dist-old/output.bin", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSharedFileSelectionFailureCarriesCauseAndPresentation(t *testing.T) {
|
||||
repository := newTreeRepository(t)
|
||||
writeTreeFile(t, repository, "go.mod", []byte("module example.com/root\n"))
|
||||
commit := commitCandidateTree(t, repository)
|
||||
installGoShim(t, "", false)
|
||||
err := Run(context.Background(), Request{
|
||||
RepositoryDir: repository,
|
||||
Commit: commit,
|
||||
Settings: Settings{SharedFiles: []string{"missing.txt"}},
|
||||
})
|
||||
var failure *Failure
|
||||
if !errors.As(err, &failure) || failure.Check != SharedFiles || failure.Cause == nil || len(failure.Findings) != 1 || failure.Findings[0].Path != "missing.txt" {
|
||||
t.Fatalf("shared-file selection error = %#v, want path, cause, and shared-files identity", err)
|
||||
}
|
||||
if !strings.Contains(err.Error(), "shared-files") || !strings.Contains(err.Error(), "missing.txt") {
|
||||
t.Fatalf("shared-file error text = %q, want existing check/path presentation", err)
|
||||
}
|
||||
}
|
||||
@@ -16,6 +16,7 @@ import (
|
||||
type CandidateTree struct {
|
||||
context context.Context
|
||||
repositoryDir string
|
||||
modulePath string
|
||||
entries []git.TreeEntry
|
||||
inputEntries []git.TreeEntry
|
||||
byPath map[string]int
|
||||
@@ -53,9 +54,14 @@ func ReadCandidateTreeForScope(ctx context.Context, repositoryDir, commit string
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
modulePath, err := repositoryModulePath(repositoryDir, scope.ModuleDir)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
tree := &CandidateTree{
|
||||
context: ctx,
|
||||
repositoryDir: repositoryDir,
|
||||
modulePath: modulePath,
|
||||
entries: entries,
|
||||
byPath: make(map[string]int, len(entries)),
|
||||
directories: make(map[string]struct{}),
|
||||
@@ -68,7 +74,7 @@ func ReadCandidateTreeForScope(ctx context.Context, repositoryDir, commit string
|
||||
tree.directories[directory] = struct{}{}
|
||||
}
|
||||
}
|
||||
tree.inputEntries, err = selectInputEntries(repositoryDir, entries, tree.byPath, scope)
|
||||
tree.inputEntries, err = selectInputEntries(entries, tree.byPath, modulePath, scope.SharedFiles)
|
||||
if err != nil {
|
||||
_ = tree.Close()
|
||||
return nil, err
|
||||
@@ -88,34 +94,7 @@ func (tree *CandidateTree) InputEntries() []git.TreeEntry {
|
||||
return append([]git.TreeEntry(nil), tree.inputEntries...)
|
||||
}
|
||||
|
||||
func selectInputEntries(repositoryDir string, entries []git.TreeEntry, byPath map[string]int, scope TreeScope) ([]git.TreeEntry, error) {
|
||||
repositoryAbs, err := filepath.Abs(repositoryDir)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("resolve candidate repository directory: %w", err)
|
||||
}
|
||||
moduleDir := scope.ModuleDir
|
||||
if moduleDir == "" {
|
||||
moduleDir = repositoryAbs
|
||||
}
|
||||
if !filepath.IsAbs(moduleDir) {
|
||||
return nil, fmt.Errorf("selected module directory must be absolute: %q", moduleDir)
|
||||
}
|
||||
moduleAbs, err := filepath.Abs(moduleDir)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("resolve selected module directory: %w", err)
|
||||
}
|
||||
relative, err := filepath.Rel(repositoryAbs, moduleAbs)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("make selected module relative to repository: %w", err)
|
||||
}
|
||||
if relative == ".." || strings.HasPrefix(relative, ".."+string(filepath.Separator)) || filepath.IsAbs(relative) {
|
||||
return nil, fmt.Errorf("selected module directory %q is outside repository %q", moduleDir, repositoryDir)
|
||||
}
|
||||
modulePath := filepath.ToSlash(relative)
|
||||
if relative == "." {
|
||||
modulePath = "."
|
||||
}
|
||||
|
||||
func selectInputEntries(entries []git.TreeEntry, byPath map[string]int, modulePath string, sharedFiles []string) ([]git.TreeEntry, error) {
|
||||
boundaries := make([]string, 0)
|
||||
for _, entry := range entries {
|
||||
moduleRelative, ok := relativeToModule(entry.Path, modulePath)
|
||||
@@ -145,7 +124,6 @@ func selectInputEntries(repositoryDir string, entries []git.TreeEntry, byPath ma
|
||||
}
|
||||
}
|
||||
|
||||
sharedFiles := scope.SharedFiles
|
||||
usingDefaults := sharedFiles == nil
|
||||
if usingDefaults {
|
||||
sharedFiles = []string{"COPYRIGHT", "LICENSE", "docs/oss/attribution.md"}
|
||||
@@ -153,14 +131,16 @@ func selectInputEntries(repositoryDir string, entries []git.TreeEntry, byPath ma
|
||||
for _, requested := range sharedFiles {
|
||||
name, err := normalizeSharedFile(requested)
|
||||
if err != nil {
|
||||
return nil, &Failure{Check: SharedFiles, Findings: []Finding{{Path: requested, Message: err.Error()}}}
|
||||
cause := fmt.Errorf("invalid shared file path %q: %w", requested, err)
|
||||
return nil, &Failure{Check: SharedFiles, Findings: []Finding{{Path: requested, Message: cause.Error()}}, Cause: cause}
|
||||
}
|
||||
index, exists := byPath[name]
|
||||
if !exists || entries[index].Type != "blob" {
|
||||
if usingDefaults {
|
||||
continue
|
||||
}
|
||||
return nil, &Failure{Check: SharedFiles, Findings: []Finding{{Path: name, Message: "selected shared file is not present in the committed tree"}}}
|
||||
cause := fmt.Errorf("selected shared file %q is not present in the committed tree", name)
|
||||
return nil, &Failure{Check: SharedFiles, Findings: []Finding{{Path: name, Message: "selected shared file is not present in the committed tree"}}, Cause: cause}
|
||||
}
|
||||
selected[name] = entries[index]
|
||||
}
|
||||
@@ -177,6 +157,35 @@ func selectInputEntries(repositoryDir string, entries []git.TreeEntry, byPath ma
|
||||
return inputs, nil
|
||||
}
|
||||
|
||||
func repositoryModulePath(repositoryDir, moduleDir string) (string, error) {
|
||||
repositoryAbs, err := filepath.Abs(repositoryDir)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("resolve candidate repository directory: %w", err)
|
||||
}
|
||||
if moduleDir == "" {
|
||||
moduleDir = repositoryAbs
|
||||
}
|
||||
if !filepath.IsAbs(moduleDir) {
|
||||
return "", fmt.Errorf("selected module directory must be absolute: %q", moduleDir)
|
||||
}
|
||||
moduleAbs, err := filepath.Abs(moduleDir)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("resolve selected module directory: %w", err)
|
||||
}
|
||||
relative, err := filepath.Rel(repositoryAbs, moduleAbs)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("make selected module relative to repository: %w", err)
|
||||
}
|
||||
if relative == ".." || strings.HasPrefix(relative, ".."+string(filepath.Separator)) || filepath.IsAbs(relative) {
|
||||
return "", fmt.Errorf("selected module directory %q is outside repository %q", moduleDir, repositoryDir)
|
||||
}
|
||||
modulePath := filepath.ToSlash(relative)
|
||||
if relative == "." {
|
||||
modulePath = "."
|
||||
}
|
||||
return modulePath, nil
|
||||
}
|
||||
|
||||
func relativeToModule(repositoryPath, modulePath string) (string, bool) {
|
||||
if modulePath == "." {
|
||||
return repositoryPath, true
|
||||
|
||||
@@ -148,6 +148,7 @@ func Run(ctx context.Context, request Request) (result error) {
|
||||
return &Failure{Check: check, Cause: err}
|
||||
}
|
||||
lastContentCheck := Check("")
|
||||
fileSizeSettings := fileSizeSettingsForModule(request.Settings.FileSize, tree.modulePath)
|
||||
defer func() {
|
||||
if closeErr := tree.Close(); closeErr != nil && result == nil && lastContentCheck != "" {
|
||||
result = &Failure{
|
||||
@@ -180,7 +181,7 @@ func Run(ctx context.Context, request Request) (result error) {
|
||||
}
|
||||
}
|
||||
if request.Settings.FileSize != nil {
|
||||
if err := checkFileSize(tree, request.Settings.FileSize); err != nil {
|
||||
if err := checkFileSize(tree, fileSizeSettings); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
@@ -199,7 +200,7 @@ func Run(ctx context.Context, request Request) (result error) {
|
||||
|
||||
func checkWorkspaceFiles(tree *CandidateTree) error {
|
||||
var findings []Finding
|
||||
for _, entry := range tree.Entries() {
|
||||
for _, entry := range tree.inputEntries {
|
||||
if entry.Type != "blob" {
|
||||
continue
|
||||
}
|
||||
@@ -220,7 +221,7 @@ func checkWorkspaceFiles(tree *CandidateTree) error {
|
||||
func checkGoFormat(ctx context.Context, tree *CandidateTree) error {
|
||||
var findings []Finding
|
||||
formattedByObjectID := make(map[string]bool)
|
||||
for _, entry := range tree.Entries() {
|
||||
for _, entry := range tree.inputEntries {
|
||||
if entry.Type != "blob" || (entry.Mode != "100644" && entry.Mode != "100755") || !strings.HasSuffix(entry.Path, ".go") {
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -9,13 +9,14 @@ import (
|
||||
)
|
||||
|
||||
func TestCheckWorkspaceFilesReportsCommittedBlobNames(t *testing.T) {
|
||||
tree := &CandidateTree{entries: []git.TreeEntry{
|
||||
entries := []git.TreeEntry{
|
||||
{Path: "go.work", Type: "blob", Mode: "100644"},
|
||||
{Path: "nested/go.work.sum", Type: "blob", Mode: "100644"},
|
||||
{Path: "vendor/nested/link/go.work", Type: "blob", Mode: "120000"},
|
||||
{Path: "nested/not-go.work.backup", Type: "blob", Mode: "100644"},
|
||||
{Path: "submodule/go.work", Type: "commit", Mode: "160000"},
|
||||
}}
|
||||
}
|
||||
tree := &CandidateTree{entries: entries, inputEntries: entries}
|
||||
err := checkWorkspaceFiles(tree)
|
||||
failure, ok := err.(*Failure)
|
||||
if !ok || failure.Check != WorkspaceFiles {
|
||||
@@ -43,10 +44,11 @@ func TestCheckWorkspaceFilesReportsCommittedBlobNames(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestCheckWorkspaceFilesAllowsOtherNames(t *testing.T) {
|
||||
tree := &CandidateTree{entries: []git.TreeEntry{
|
||||
entries := []git.TreeEntry{
|
||||
{Path: "go.mod", Type: "blob", Mode: "100644"},
|
||||
{Path: "examples/go.work.example", Type: "blob", Mode: "100644"},
|
||||
}}
|
||||
}
|
||||
tree := &CandidateTree{entries: entries, inputEntries: entries}
|
||||
if err := checkWorkspaceFiles(tree); err != nil {
|
||||
t.Fatalf("checkWorkspaceFiles error = %v, want no findings", err)
|
||||
}
|
||||
|
||||
37
validation_scope_test.go
Normal file
37
validation_scope_test.go
Normal file
@@ -0,0 +1,37 @@
|
||||
package releasekit
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"gitea.maximumdirect.net/eric/ReleaseKit/internal/validation"
|
||||
)
|
||||
|
||||
func TestSharedFileFailureKeepsPublicIdentityPathAndCause(t *testing.T) {
|
||||
cause := errors.New("selected file is missing")
|
||||
err := convertValidationError(&validation.Failure{
|
||||
Check: validation.SharedFiles,
|
||||
Findings: []validation.Finding{{
|
||||
Path: "shared/NOTICE.md",
|
||||
Message: "selected shared file is not present in the committed tree",
|
||||
}},
|
||||
Cause: cause,
|
||||
})
|
||||
var failure *ValidationError
|
||||
if !errors.As(err, &failure) {
|
||||
t.Fatalf("converted error = %T, want *ValidationError", err)
|
||||
}
|
||||
if failure.Check != ValidationCheck("shared-files") {
|
||||
t.Fatalf("validation check = %q, want shared-files", failure.Check)
|
||||
}
|
||||
if len(failure.Findings) != 1 || failure.Findings[0].Path != "shared/NOTICE.md" {
|
||||
t.Fatalf("validation findings = %#v, want selected shared path", failure.Findings)
|
||||
}
|
||||
if !errors.Is(err, cause) {
|
||||
t.Fatalf("converted error %v does not preserve its cause", err)
|
||||
}
|
||||
if !strings.Contains(err.Error(), "shared-files") || !strings.Contains(err.Error(), "shared/NOTICE.md") {
|
||||
t.Fatalf("validation presentation = %q, want check and path", err)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user