Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a668e8aab8 | ||
|
|
337b319542 |
+2
-2
@@ -1,6 +1,6 @@
|
||||
# Lint stage — fast feedback on formatting and lint issues
|
||||
# golangci/golangci-lint:v2.12.2 (Debian-based), 2026-08-07
|
||||
FROM golangci/golangci-lint:v2.12.2@sha256:5cceeef04e53efe1470638d4b4b4f5ceefd574955ab3941b2d9a68a8c9ad5240 AS lint
|
||||
# golangci/golangci-lint:v2.12.2, 2026-08-07
|
||||
FROM golangci/golangci-lint@sha256:5cceeef04e53efe1470638d4b4b4f5ceefd574955ab3941b2d9a68a8c9ad5240 AS lint
|
||||
WORKDIR /src
|
||||
COPY go.mod go.sum ./
|
||||
RUN go mod download
|
||||
|
||||
+2
-2
@@ -9,8 +9,8 @@
|
||||
# stage of the main Dockerfile because script/lint must not depend on
|
||||
# the rest of that build; the two FROM lines are kept identical by
|
||||
# script/verify-lint-image-pin, run as a gate below.
|
||||
# golangci/golangci-lint:v2.12.2 (Debian-based), 2026-08-07
|
||||
FROM golangci/golangci-lint:v2.12.2@sha256:5cceeef04e53efe1470638d4b4b4f5ceefd574955ab3941b2d9a68a8c9ad5240
|
||||
# golangci/golangci-lint:v2.12.2, 2026-08-07
|
||||
FROM golangci/golangci-lint@sha256:5cceeef04e53efe1470638d4b4b4f5ceefd574955ab3941b2d9a68a8c9ad5240
|
||||
|
||||
WORKDIR /src
|
||||
|
||||
|
||||
@@ -29,6 +29,24 @@
|
||||
|
||||
# Completed Steps
|
||||
|
||||
- test the `-x` filesystem-boundary rejection branch by calling
|
||||
`subdirJob` directly: reject across a boundary, bypass when the root
|
||||
device is unknown, stat-error warning, and default crossing
|
||||
(2026-09-21, branch `next`, closes
|
||||
https://git.eeqj.de/sneak/sfdupes/issues/17)
|
||||
|
||||
- fix the lint-image pin comments and `FROM` form in `Dockerfile` and
|
||||
`Dockerfile.lint` (2026-08-10, branch `next`, closes
|
||||
https://git.eeqj.de/sneak/sfdupes/issues/25): dropped the false
|
||||
`(Debian-based)` parenthetical (v2.12.1 was Debian too) and the
|
||||
redundant tag, so both pins are the policy `# image:vX.Y.Z,
|
||||
YYYY-MM-DD` comment over a bare `FROM image@sha256:...`. Digest
|
||||
unchanged. `script/verify-lint-image-pin` parses those `FROM` lines
|
||||
and still matches the tagless form; its advice line lost the now
|
||||
meaningless "tag and digest". With no tag in either reference, a
|
||||
tag-only disagreement no longer exists — a one-sided tag is caught as
|
||||
a plain mismatch.
|
||||
|
||||
- run all linting in Docker via `Dockerfile.lint` and `script/lint`
|
||||
(2026-08-10, branch `next`, closes
|
||||
https://git.eeqj.de/sneak/sfdupes/issues/46): per the owner ruling, the
|
||||
|
||||
+148
@@ -6,7 +6,9 @@ import (
|
||||
"crypto/sha256"
|
||||
"database/sql"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
@@ -324,6 +326,152 @@ func TestDeviceOfInfo(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// dirEntryFor returns the fs.DirEntry for name within dir, obtained via
|
||||
// the same os.ReadDir the walk uses, so it carries a real Info().
|
||||
func dirEntryFor(t *testing.T, dir, name string) fs.DirEntry {
|
||||
t.Helper()
|
||||
|
||||
entries, err := os.ReadDir(dir)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
for _, e := range entries {
|
||||
if e.Name() == name {
|
||||
return e
|
||||
}
|
||||
}
|
||||
|
||||
t.Fatalf("entry %q not found in %q", name, dir)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// callSubdirJob runs subdirJob against e under parent, collecting any
|
||||
// warning events it emits (subdirJob emits at most one).
|
||||
func callSubdirJob(t *testing.T, p string, e fs.DirEntry,
|
||||
parent dirJob, oneFS bool,
|
||||
) (dirJob, bool, []walkEvent) {
|
||||
t.Helper()
|
||||
|
||||
events := make(chan walkEvent, 1)
|
||||
job, ok := subdirJob(t.Context(), p, e, parent, oneFS, events)
|
||||
close(events)
|
||||
|
||||
var evs []walkEvent
|
||||
for ev := range events {
|
||||
evs = append(evs, ev)
|
||||
}
|
||||
|
||||
return job, ok, evs
|
||||
}
|
||||
|
||||
// TestSubdirJobOneFilesystem exercises the -x boundary check in
|
||||
// subdirJob directly, so no second real filesystem is needed. The
|
||||
// subdirectory's real device is compared against a fabricated operand
|
||||
// device.
|
||||
func TestSubdirJobOneFilesystem(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
dir := t.TempDir()
|
||||
|
||||
sub := filepath.Join(dir, "sub")
|
||||
|
||||
err := os.Mkdir(sub, 0o750)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
info, err := os.Lstat(sub)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
dev, ok := deviceOfInfo(info)
|
||||
if !ok {
|
||||
t.Skip("platform exposes no device id")
|
||||
}
|
||||
|
||||
// A device the subdirectory is not on, standing in for an operand
|
||||
// rooted on a different filesystem.
|
||||
otherDev := dev + 1
|
||||
e := dirEntryFor(t, dir, "sub")
|
||||
|
||||
cases := []struct {
|
||||
name string
|
||||
oneFS bool
|
||||
parent dirJob
|
||||
wantOK bool
|
||||
}{
|
||||
// -x on, subdirectory on a different device than its operand:
|
||||
// descent is refused.
|
||||
{"reject across boundary", true,
|
||||
dirJob{rootDev: otherDev, rootDevOK: true}, false},
|
||||
// -x on but the operand's own device is unknown: the boundary
|
||||
// check is bypassed and descent proceeds.
|
||||
{"bypass when root device unknown", true,
|
||||
dirJob{rootDev: otherDev, rootDevOK: false}, true},
|
||||
// Default (no -x): boundaries are crossed even onto a different
|
||||
// device.
|
||||
{"cross by default", false,
|
||||
dirJob{rootDev: otherDev, rootDevOK: true}, true},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
job, ok, evs := callSubdirJob(t, sub, e, tc.parent, tc.oneFS)
|
||||
if ok != tc.wantOK {
|
||||
t.Fatalf("accepted = %v, want %v", ok, tc.wantOK)
|
||||
}
|
||||
|
||||
if len(evs) != 0 {
|
||||
t.Fatalf("unexpected events: %+v", evs)
|
||||
}
|
||||
|
||||
if ok && job.path != sub {
|
||||
t.Fatalf("job.path = %q, want %q", job.path, sub)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// errInfoUnavailable is returned by errDirEntry.Info().
|
||||
var errInfoUnavailable = errors.New("info unavailable")
|
||||
|
||||
// errDirEntry is a directory entry whose Info() always fails, driving
|
||||
// subdirJob's stat-error branch deterministically.
|
||||
type errDirEntry struct{ name string }
|
||||
|
||||
func (e errDirEntry) Name() string { return e.name }
|
||||
func (errDirEntry) IsDir() bool { return true }
|
||||
func (errDirEntry) Type() fs.FileMode {
|
||||
return fs.ModeDir
|
||||
}
|
||||
|
||||
func (errDirEntry) Info() (fs.FileInfo, error) {
|
||||
return nil, errInfoUnavailable
|
||||
}
|
||||
|
||||
// TestSubdirJobStatError asserts that when a subdirectory's Info()
|
||||
// fails under -x, subdirJob warns and refuses descent.
|
||||
func TestSubdirJobStatError(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
p := "/does/not/matter/sub"
|
||||
|
||||
_, ok, evs := callSubdirJob(t, p, errDirEntry{name: "sub"},
|
||||
dirJob{rootDev: 1, rootDevOK: true}, true)
|
||||
if ok {
|
||||
t.Fatal("descent accepted after stat error, want refused")
|
||||
}
|
||||
|
||||
if len(evs) != 1 || !evs[0].fail || !strings.Contains(evs[0].warn, p) {
|
||||
t.Fatalf("want one warning naming the path, got %+v", evs)
|
||||
}
|
||||
}
|
||||
|
||||
// buildSmokeTree recreates the README smoke-test filesystem layout
|
||||
// with deterministic content and returns the tree root.
|
||||
func buildSmokeTree(t *testing.T) string {
|
||||
|
||||
@@ -71,9 +71,9 @@ main() {
|
||||
"the two pins disagree:" >&2
|
||||
echo "verify-lint-image-pin: $LINT_DOCKERFILE: $lint_ref" >&2
|
||||
echo "verify-lint-image-pin: $MAIN_DOCKERFILE: $main_ref" >&2
|
||||
echo "verify-lint-image-pin: bump both FROM lines together, tag and" \
|
||||
"digest, so script/lint and the Dockerfile lint stage keep" \
|
||||
"running the same linter" >&2
|
||||
echo "verify-lint-image-pin: bump both FROM lines together so" \
|
||||
"script/lint and the Dockerfile lint stage keep running the" \
|
||||
"same linter" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
|
||||
Reference in New Issue
Block a user