Compare commits
4
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
114ba0a29f | ||
|
|
64eb5cbd40 | ||
|
|
45eac1f6f8 | ||
|
|
b91e92b070 |
@@ -16,6 +16,7 @@ linters:
|
|||||||
- depguard # Dependency allow/block lists
|
- depguard # Dependency allow/block lists
|
||||||
- godot # Requires comments to end with periods
|
- godot # Requires comments to end with periods
|
||||||
- wsl # Deprecated, replaced by wsl_v5
|
- wsl # Deprecated, replaced by wsl_v5
|
||||||
|
- gomodguard # Deprecated, replaced by gomodguard_v2
|
||||||
- wrapcheck # Too verbose for internal packages
|
- wrapcheck # Too verbose for internal packages
|
||||||
- varnamelen # Short names like db, id are idiomatic Go
|
- varnamelen # Short names like db, id are idiomatic Go
|
||||||
settings:
|
settings:
|
||||||
|
|||||||
+7
-2
@@ -14,7 +14,9 @@ RUN touch mfer/mf.pb.go
|
|||||||
# Go half of fmt-check only: this image has no node, so no prettier. The
|
# Go half of fmt-check only: this image has no node, so no prettier. The
|
||||||
# markdown half runs in the mdfmt stage below.
|
# markdown half runs in the mdfmt stage below.
|
||||||
RUN make fmt-check-go
|
RUN make fmt-check-go
|
||||||
RUN make lint
|
# The linter directly, not `make lint`: script/lint builds this stage, and
|
||||||
|
# there is no docker inside this build.
|
||||||
|
RUN golangci-lint run --config .golangci.yml ./...
|
||||||
|
|
||||||
# Markdown/JSON format stage — prettier needs node, which the Go images
|
# Markdown/JSON format stage — prettier needs node, which the Go images
|
||||||
# do not have. node:22.17.0-bookworm-slim (2026-08-09); ships node
|
# do not have. node:22.17.0-bookworm-slim (2026-08-09); ships node
|
||||||
@@ -67,7 +69,10 @@ RUN version="${VERSION:-$(git describe --tags --always)}"; \
|
|||||||
exit 1; \
|
exit 1; \
|
||||||
fi; \
|
fi; \
|
||||||
cd cmd/mfer && \
|
cd cmd/mfer && \
|
||||||
go build -tags urfave_cli_no_docs -ldflags "-X main.Gitrev=$version" -o /mfer .
|
CGO_ENABLED=0 go build -tags urfave_cli_no_docs -ldflags "-X main.Gitrev=$version" -o /mfer .
|
||||||
|
|
||||||
|
# Fail unless /mfer is statically linked: scratch has no C library to run it.
|
||||||
|
RUN ldd /mfer 2>&1 | grep -q 'not a dynamic executable'
|
||||||
|
|
||||||
FROM scratch
|
FROM scratch
|
||||||
COPY --from=builder /mfer /mfer
|
COPY --from=builder /mfer /mfer
|
||||||
|
|||||||
@@ -58,9 +58,6 @@ fmt-check-md:
|
|||||||
hooks:
|
hooks:
|
||||||
@script/install-precommit
|
@script/install-precommit
|
||||||
|
|
||||||
devprereqs:
|
|
||||||
which golangci-lint || go install -v github.com/golangci/golangci-lint/v2/cmd/golangci-lint@v2.12.2
|
|
||||||
|
|
||||||
mfer/mf.pb.go: mfer/mf.proto
|
mfer/mf.pb.go: mfer/mf.proto
|
||||||
cd mfer && go generate .
|
cd mfer && go generate .
|
||||||
|
|
||||||
|
|||||||
@@ -65,9 +65,9 @@ standard: normalized scripts in `script/` are the entrypoints for the
|
|||||||
development workflow, and the Makefile targets are thin shims that call them. We
|
development workflow, and the Makefile targets are thin shims that call them. We
|
||||||
provide:
|
provide:
|
||||||
|
|
||||||
- `script/bootstrap` — install all dependencies (Go, golangci-lint, Go module
|
- `script/bootstrap` — install all dependencies (Go, Go module download, and
|
||||||
download, and node/yarn plus the prettier version pinned in
|
node/yarn plus the prettier version pinned in `package.json`/`yarn.lock`),
|
||||||
`package.json`/`yarn.lock`), idempotently
|
idempotently; golangci-lint is not installed, it runs only in Docker
|
||||||
- `script/setup` — make a fresh clone ready for development: runs
|
- `script/setup` — make a fresh clone ready for development: runs
|
||||||
`script/bootstrap`, then `script/install-precommit`
|
`script/bootstrap`, then `script/install-precommit`
|
||||||
- `script/projectname` — output the project name (`mfer`); used by other scripts
|
- `script/projectname` — output the project name (`mfer`); used by other scripts
|
||||||
@@ -77,9 +77,11 @@ provide:
|
|||||||
- `script/fuzz` — fuzz the manifest parser for one minute; run by hand
|
- `script/fuzz` — fuzz the manifest parser for one minute; run by hand
|
||||||
(`make fuzz`), never by CI, while `script/test` runs its committed seed corpus
|
(`make fuzz`), never by CI, while `script/test` runs its committed seed corpus
|
||||||
as ordinary tests
|
as ordinary tests
|
||||||
- `script/lint` — run `golangci-lint` and verify `gofmt` cleanliness
|
- `script/lint` — run `golangci-lint` in Docker: builds only the `lint` stage of
|
||||||
- `script/fmt` — format all code and docs (writes): `gofumpt`,
|
the `Dockerfile` (the Go format check, then the linter), uncached so it runs
|
||||||
`golangci-lint run --fix`, and `script/prettier --write`
|
every time, then removes the image
|
||||||
|
- `script/fmt` — format all code and docs (writes): `gofumpt` and
|
||||||
|
`script/prettier --write`
|
||||||
- `script/prettier` — run prettier over the repository's canonical file set
|
- `script/prettier` — run prettier over the repository's canonical file set
|
||||||
(Markdown and JSON, minus `.prettierignore`) in the given mode, `--write` or
|
(Markdown and JSON, minus `.prettierignore`) in the given mode, `--write` or
|
||||||
`--check`; the single definition of that file set, so `script/fmt` and
|
`--check`; the single definition of that file set, so `script/fmt` and
|
||||||
@@ -105,11 +107,10 @@ yet. Primary development happens on a privately-run Gitea instance at
|
|||||||
[tracked there](https://git.eeqj.de/sneak/mfer/issues).
|
[tracked there](https://git.eeqj.de/sneak/mfer/issues).
|
||||||
|
|
||||||
Changes must always be formatted with a standard `go fmt`, syntactically valid,
|
Changes must always be formatted with a standard `go fmt`, syntactically valid,
|
||||||
and must pass the linting defined in the repository (presently only the
|
and must pass the linting defined in the repository's `.golangci.yml`, which
|
||||||
`golangci-lint` defaults), which can be run with a `make lint`. The `main`
|
`make lint` runs in Docker. The `main` branch is protected and all changes must
|
||||||
branch is protected and all changes must be made via
|
be made via [pull requests](https://git.eeqj.de/sneak/mfer/pulls) and pass CI to
|
||||||
[pull requests](https://git.eeqj.de/sneak/mfer/pulls) and pass CI to be merged.
|
be merged. Any changes submitted to this project must also be
|
||||||
Any changes submitted to this project must also be
|
|
||||||
[WTFPL-licensed](https://wtfpl.net) to be considered.
|
[WTFPL-licensed](https://wtfpl.net) to be considered.
|
||||||
|
|
||||||
See [`REPO_POLICIES.md`](REPO_POLICIES.md) for detailed coding standards,
|
See [`REPO_POLICIES.md`](REPO_POLICIES.md) for detailed coding standards,
|
||||||
|
|||||||
+34
-18
@@ -10,6 +10,7 @@ import (
|
|||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
"syscall"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -425,18 +426,31 @@ func TestGPGTimeoutKillsGPG(t *testing.T) {
|
|||||||
assert.Contains(t, err.Error(), "gpg sign failed: gpg timed out")
|
assert.Contains(t, err.Error(), "gpg sign failed: gpg timed out")
|
||||||
}
|
}
|
||||||
|
|
||||||
// TestGPGTimeoutWhenChildHoldsOutput uses a fake gpg that runs sleep as a
|
// TestGPGCancelWhenChildHoldsOutput uses a fake gpg that runs sleep as a
|
||||||
// child instead of exec-ing it, the way a wrapper script around the real
|
// child instead of exec-ing it, the way a wrapper script around the real
|
||||||
// gpg might. Killing the fake gpg leaves sleep holding its stdout and
|
// gpg might. Killing the fake gpg leaves sleep holding its stdout and
|
||||||
// stderr open; the call must still return shortly after the deadline
|
// stderr open; the call must still return once ctx ends instead of waiting
|
||||||
// instead of waiting for sleep to exit. The fake gpg writes the process ID
|
// for sleep to exit. The fake gpg writes the process ID of sleep to a named
|
||||||
// of sleep to a file so that the test can kill it before returning.
|
// pipe; the test ends ctx only after reading it, so sleep is running by
|
||||||
func TestGPGTimeoutWhenChildHoldsOutput(t *testing.T) {
|
// then, and kills sleep before returning.
|
||||||
pidFile := filepath.Join(t.TempDir(), "sleep.pid")
|
func TestGPGCancelWhenChildHoldsOutput(t *testing.T) {
|
||||||
|
pidPipe := filepath.Join(t.TempDir(), "sleep.pid")
|
||||||
|
require.NoError(t, syscall.Mkfifo(pidPipe, 0o600))
|
||||||
|
// sleep outlasts the 10 s wait below, so a call that waits for it fails.
|
||||||
t.Setenv("PATH", fakeGPGPath(t,
|
t.Setenv("PATH", fakeGPGPath(t,
|
||||||
"#!/bin/sh\nsleep 3 &\necho $! >'"+pidFile+"'\nwait\n"))
|
"#!/bin/sh\nsleep 60 &\necho $! >'"+pidPipe+"'\nwait\n"))
|
||||||
t.Cleanup(func() {
|
|
||||||
pid, err := os.ReadFile(pidFile) //nolint:gosec // G304: path inside t.TempDir()
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
signErr := make(chan error, 1)
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
_, err := gpgSign(ctx, []byte("data"), GPGKeyID("any"))
|
||||||
|
signErr <- err
|
||||||
|
}()
|
||||||
|
|
||||||
|
pid, err := os.ReadFile(pidPipe) //nolint:gosec // G304: path inside t.TempDir()
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
n, err := strconv.Atoi(strings.TrimSpace(string(pid)))
|
n, err := strconv.Atoi(strings.TrimSpace(string(pid)))
|
||||||
@@ -444,17 +458,19 @@ func TestGPGTimeoutWhenChildHoldsOutput(t *testing.T) {
|
|||||||
|
|
||||||
sleep, err := os.FindProcess(n)
|
sleep, err := os.FindProcess(n)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.NoError(t, sleep.Kill())
|
t.Cleanup(func() { require.NoError(t, sleep.Kill()) })
|
||||||
})
|
|
||||||
|
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond)
|
cancel()
|
||||||
defer cancel()
|
|
||||||
|
|
||||||
start := time.Now()
|
// The call should return about gpgWaitDelay (one second) after the
|
||||||
_, err := gpgSign(ctx, []byte("data"), GPGKeyID("any"))
|
// cancel. 10 s is far above that and well under the 30 s test timeout,
|
||||||
require.ErrorIs(t, err, context.DeadlineExceeded)
|
// which would abort the whole package before the cleanup kills sleep.
|
||||||
assert.Less(t, time.Since(start), 3*time.Second,
|
select {
|
||||||
"the call waited for the child holding gpg's output to exit")
|
case err := <-signErr:
|
||||||
|
require.ErrorIs(t, err, context.Canceled)
|
||||||
|
case <-time.After(10 * time.Second):
|
||||||
|
t.Fatal("the call waited for the child holding gpg's output to exit")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TestBuildPassesContextToSigning checks that a caller can cancel the gpg
|
// TestBuildPassesContextToSigning checks that a caller can cancel the gpg
|
||||||
|
|||||||
+2
-29
@@ -304,46 +304,19 @@ func TestScannerEnumerateFS(t *testing.T) {
|
|||||||
func TestSendEnumerateStatusNonBlocking(t *testing.T) {
|
func TestSendEnumerateStatusNonBlocking(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
// Channel with no buffer - send should not block
|
// Nobody receives, so a blocking send would hang the test into its timeout.
|
||||||
ch := make(chan EnumerateStatus)
|
ch := make(chan EnumerateStatus)
|
||||||
|
|
||||||
// This should not block
|
|
||||||
done := make(chan bool)
|
|
||||||
|
|
||||||
go func() {
|
|
||||||
sendEnumerateStatus(ch, EnumerateStatus{FilesFound: 1})
|
sendEnumerateStatus(ch, EnumerateStatus{FilesFound: 1})
|
||||||
|
|
||||||
done <- true
|
|
||||||
}()
|
|
||||||
|
|
||||||
select {
|
|
||||||
case <-done:
|
|
||||||
// Success - did not block
|
|
||||||
case <-time.After(100 * time.Millisecond):
|
|
||||||
t.Fatal("sendEnumerateStatus blocked on full channel")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestSendScanStatusNonBlocking(t *testing.T) {
|
func TestSendScanStatusNonBlocking(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
// Channel with no buffer - send should not block
|
// Nobody receives, so a blocking send would hang the test into its timeout.
|
||||||
ch := make(chan ScanStatus)
|
ch := make(chan ScanStatus)
|
||||||
|
|
||||||
done := make(chan bool)
|
|
||||||
|
|
||||||
go func() {
|
|
||||||
sendScanStatus(ch, ScanStatus{ScannedFiles: 1})
|
sendScanStatus(ch, ScanStatus{ScannedFiles: 1})
|
||||||
|
|
||||||
done <- true
|
|
||||||
}()
|
|
||||||
|
|
||||||
select {
|
|
||||||
case <-done:
|
|
||||||
// Success - did not block
|
|
||||||
case <-time.After(100 * time.Millisecond):
|
|
||||||
t.Fatal("sendScanStatus blocked on full channel")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestSendStatusNilChannel(t *testing.T) {
|
func TestSendStatusNilChannel(t *testing.T) {
|
||||||
|
|||||||
+1
-6
@@ -140,12 +140,7 @@ main() {
|
|||||||
|
|
||||||
# ---- Go repos ----
|
# ---- Go repos ----
|
||||||
if missing go; then pkg_install go golang go go; fi
|
if missing go; then pkg_install go golang go go; fi
|
||||||
# golangci-lint: packaged in nix, brew, and apk. On apt there is no
|
# No golangci-lint: script/lint runs it in Docker only.
|
||||||
# package: download a specific release archive from GitHub and
|
|
||||||
# verify its hash (verify_sha256), never curl | sh.
|
|
||||||
if missing golangci-lint; then
|
|
||||||
pkg_install golangci-lint golangci-lint golangci-lint golangci-lint
|
|
||||||
fi
|
|
||||||
go mod download
|
go mod download
|
||||||
|
|
||||||
# ---- Python repos ----
|
# ---- Python repos ----
|
||||||
|
|||||||
@@ -19,7 +19,6 @@ main() {
|
|||||||
cd "$ROOT"
|
cd "$ROOT"
|
||||||
ensure_pb
|
ensure_pb
|
||||||
gofumpt -l -w mfer internal cmd
|
gofumpt -l -w mfer internal cmd
|
||||||
golangci-lint run --fix
|
|
||||||
# Markdown and JSON, over the same file set script/fmt-check verifies.
|
# Markdown and JSON, over the same file set script/fmt-check verifies.
|
||||||
"$SCRIPT_DIR/prettier" --write
|
"$SCRIPT_DIR/prettier" --write
|
||||||
}
|
}
|
||||||
|
|||||||
+11
-8
@@ -1,17 +1,20 @@
|
|||||||
#!/bin/sh
|
#!/bin/sh
|
||||||
# script/lint: run the linter.
|
# script/lint: run golangci-lint, in Docker only. Builds the lint stage of
|
||||||
|
# the Dockerfile, whose build runs the linter, so a successful build is a
|
||||||
|
# clean lint. --no-cache because a cached build runs no linter. The image
|
||||||
|
# is removed afterwards, whatever the outcome.
|
||||||
set -eu
|
set -eu
|
||||||
|
|
||||||
ROOT="$(cd "$(dirname "$0")/.." && pwd -P)"
|
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd -P)"
|
||||||
|
ROOT="$(cd "$SCRIPT_DIR/.." && pwd -P)"
|
||||||
|
|
||||||
main() {
|
main() {
|
||||||
cd "$ROOT"
|
cd "$ROOT"
|
||||||
golangci-lint run
|
# Tagged per run, so concurrent runs never remove each other's image.
|
||||||
if [ -n "$(gofmt -l .)" ]; then
|
image="$("$SCRIPT_DIR/projectname")-lint:$$"
|
||||||
echo "gofmt: files need formatting:" >&2
|
# A failed build leaves no image, so there is nothing to remove then.
|
||||||
gofmt -l . >&2
|
trap 'docker image rm "$image" >/dev/null 2>&1 || true' EXIT INT TERM
|
||||||
exit 1
|
docker build --no-cache --target lint -t "$image" .
|
||||||
fi
|
|
||||||
}
|
}
|
||||||
|
|
||||||
main "$@"
|
main "$@"
|
||||||
|
|||||||
Reference in New Issue
Block a user