2 Commits
Author SHA1 Message Date
sneak 114c31777f Delete the oldest report files to stay under the size cap (closes #54)
check / check (push) Successful in 1m49s
When a report would take the report files past DATA_DIR_MAX_BYTES,
reportbuf now deletes the oldest report files until it fits, and does
the same at start when files left by an earlier run are already past
it. A file joins the files that may be deleted only once it is
completely written, so a file still being written is never deleted. A
report is refused with 507 only when the reports waiting to be written
fill the cap on their own, and then no file is deleted. The reports of
a failed write stop counting, and the part of its file written is
removed. A file whose deletion fails keeps counting; one already
deleted by hand counts as freed.

Model: opus-5-5
2026-10-03 15:02:49 +00:00
clawbot aa35625d47 Go tests run with -race and -cover under Go's own timeout (closes #88)
check / check (push) Successful in 2m30s
backend/script/test runs go test -timeout 30s -race -cover and, if
that fails, runs it again with -v and fails. The root script/test drops
its one 30-second timeout around both halves: from a cold Go build
cache, compiling the tests with -race used it all up. Each half keeps
its own limit. The race detector needs a C compiler: the Dockerfile
builder stage gains gcc and musl-dev, and script/bootstrap installs gcc,
with the C library headers on apt and apk, when gcc is missing; make
build still sets CGO_ENABLED=0. New tests: the health check's answer, a
valid report's answer, a report file's exact lines, and the flush at the
10 MiB threshold. The handlers TestImport stub is gone.

Model: opus-5-5
2026-10-03 16:26:17 +02:00
12 changed files with 367 additions and 58 deletions
+4 -1
View File
@@ -20,7 +20,10 @@ RUN make lint
# golang:1.25-alpine (2026-02-27) # golang:1.25-alpine (2026-02-27)
FROM golang:1.25-alpine@sha256:f6751d823c26342f9506c03797d2527668d095b0a15f1862cddb4d927a7a4ced AS builder FROM golang:1.25-alpine@sha256:f6751d823c26342f9506c03797d2527668d095b0a15f1862cddb4d927a7a4ced AS builder
RUN apk add --no-cache git make # gcc and musl-dev are for make test: its race detector needs cgo, which
# Go turns on by itself once a C compiler is present. make build still
# sets CGO_ENABLED=0, so the binary stays static.
RUN apk add --no-cache gcc git make musl-dev
WORKDIR /src WORKDIR /src
+7 -5
View File
@@ -39,14 +39,16 @@ halves, so the root `make check` fails if either one is broken. We provide:
- `script/bootstrap` — install all dependencies (the pinned node via nvm unless - `script/bootstrap` — install all dependencies (the pinned node via nvm unless
one new enough for the frontend's dependencies is installed, yarn via one new enough for the frontend's dependencies is installed, yarn via
corepack, `yarn install --frozen-lockfile`, the pinned Go unless one at least corepack, `yarn install --frozen-lockfile`, the pinned Go unless one at least
as new as `backend/go.mod` asks for is installed, and the Go modules), linking as new as `backend/go.mod` asks for is installed, the Go modules, and gcc with
what it installs itself into `~/.local/bin`, which has to be on `PATH`. It the C library headers unless gcc is installed, for the race detector in
installs no Go linter and not Docker: `make lint` runs the linter in Docker `make test`), linking what it installs itself into `~/.local/bin`, which has
to be on `PATH`. It installs no Go linter and not Docker: `make lint` runs the
linter in Docker
- `script/setup` — make a fresh clone ready for development: bootstrap plus the - `script/setup` — make a fresh clone ready for development: bootstrap plus the
git pre-commit hook git pre-commit hook
- `script/projectname` — print the project name (used for the Docker image tag) - `script/projectname` — print the project name (used for the Docker image tag)
- `script/test` — run `script/frontend-test`, then the backend's Go tests, both - `script/test` — run `script/frontend-test`, then the backend's Go tests, each
within one 30-second timeout under its own 30-second timeout
- `script/lint` — run `script/frontend-lint`, then golangci-lint in Docker, by - `script/lint` — run `script/frontend-lint`, then golangci-lint in Docker, by
building the lint stage of `Dockerfile` without the cache building the lint stage of `Dockerfile` without the cache
- `script/fmt` — format all files (writes): prettier, then gofmt over `backend/` - `script/fmt` — format all files (writes): prettier, then gofmt over `backend/`
+11
View File
@@ -32,6 +32,17 @@ latest run passes.
write stop counting, and the part of its file written is removed. A file that write stop counting, and the part of its file written is removed. A file that
cannot be deleted still counts until the next start; one already deleted by cannot be deleted still counts until the next start; one already deleted by
hand counts as freed hand counts as freed
- 2026-10-03: the Go tests run with the race detector and coverage (issue #88):
`backend/script/test` runs `go test -timeout 30s -race -cover ./...` and, if
that fails, runs it again with `-v` and fails. Go's `-timeout` bounds the
tests, not their compile; the root `script/test` no longer puts one 30-second
timeout around both halves, which a cold Go build cache could use up on
compiling alone. The race detector needs a C compiler: the builder stage of
`Dockerfile` has gcc and musl-dev, and `script/bootstrap` installs gcc, with
the C library headers on apt and apk, when gcc is missing; the binary is still
built with `CGO_ENABLED=0`. New tests cover the health check's answer, a valid
report's answer, a report file's exact contents, and the flush when the buffer
reaches 10 MiB; the handlers' `TestImport` stub is gone
- 2026-10-03: each target check times out after 80% of the refresh interval - 2026-10-03: each target check times out after 80% of the refresh interval
(issue #78), 24 seconds at 30 seconds, where it was capped at 3 seconds. A (issue #78), 24 seconds at 30 seconds, where it was capped at 3 seconds. A
round started early, after an interval change or when the recovery probe finds round started early, after an interval change or when the recovery probe finds
+9 -5
View File
@@ -35,7 +35,10 @@ pattern as the repo root: the targets in `backend/Makefile` are thin shims over
stamped in. The version is `VERSION` from the environment; stamped in. The version is `VERSION` from the environment;
when that is unset or empty, it falls back to `git describe` inside a git when that is unset or empty, it falls back to `git describe` inside a git
checkout, then to `dev` checkout, then to `dev`
- `script/test` — run the Go tests under a 30-second timeout - `script/test` — run the Go tests with the race detector and coverage. Go's
`-timeout 30s` bounds the tests, not their compile. If they fail, they run
again with `-v` for the details, and the script fails. The race detector needs
a C compiler
- `script/lint` — check `.golangci.yml` against its pinned sha256, then run - `script/lint` — check `.golangci.yml` against its pinned sha256, then run
golangci-lint. It runs inside the golangci-lint image of the lint stage of golangci-lint. It runs inside the golangci-lint image of the lint stage of
the root `Dockerfile`; from a checkout, run `make lint` at the repo root, the root `Dockerfile`; from a checkout, run `make lint` at the repo root,
@@ -125,10 +128,11 @@ Reports are written as `reports-<timestamp>-<number>.jsonl.zst` files in
`DATA_DIR`. The timestamp is in UTC to the millisecond, so the names sort by `DATA_DIR`. The timestamp is in UTC to the millisecond, so the names sort by
time. The number starts at 1 when the server starts and goes up by one for each time. The number starts at 1 when the server starts and goes up by one for each
file the server starts to write, so two files written in the same millisecond file the server starts to write, so two files written in the same millisecond
still get different names. A failed write uses up its number, leaving a gap in still get different names. A failed write uses up its number and leaves a gap in
the numbers if the file could not be created and otherwise a file under that the numbers: its file, if it was created, is removed. The file stays, counted
number that may be incomplete. Each file contains one JSON object per line, toward `DATA_DIR_MAX_BYTES` from the next start, only if removing it fails too.
compressed with zstd. Files are created with `O_EXCL` to prevent overwrites. Each file contains one JSON object per line, compressed with zstd. Files are
created with `O_EXCL` to prevent overwrites.
### Report limits ### Report limits
@@ -1,13 +0,0 @@
package handlers_test
import (
"testing"
_ "sneak.berlin/go/netwatch/internal/handlers"
)
func TestImport(t *testing.T) {
t.Parallel()
// Compilation check — verifies the package parses
// and all imports resolve.
}
@@ -0,0 +1,120 @@
package handlers_test
import (
"encoding/json"
"maps"
"net/http"
"net/http/httptest"
"slices"
"testing"
"time"
"sneak.berlin/go/netwatch/internal/globals"
"sneak.berlin/go/netwatch/internal/handlers"
"sneak.berlin/go/netwatch/internal/healthcheck"
"sneak.berlin/go/netwatch/internal/logger"
"go.uber.org/fx/fxtest"
)
// newStartedHandlers builds Handlers with a real health check for the
// server named in g, and starts them, which records the time the
// uptime counts from.
func newStartedHandlers(t *testing.T, g *globals.Globals) *handlers.Handlers {
t.Helper()
lc := fxtest.NewLifecycle(t)
log, err := logger.New(lc, logger.Params{Globals: g})
if err != nil {
t.Fatalf("logger: %v", err)
}
hc, err := healthcheck.New(lc,
healthcheck.Params{Globals: g, Logger: log})
if err != nil {
t.Fatalf("health check: %v", err)
}
h, err := handlers.New(lc,
handlers.Params{Globals: g, Healthcheck: hc, Logger: log})
if err != nil {
t.Fatalf("handlers: %v", err)
}
lc.RequireStart()
t.Cleanup(lc.RequireStop)
return h
}
// TestHandleHealthCheck checks the health check's answer: 200, a JSON
// content type, and a JSON object with exactly the fields of
// healthcheck.Response, carrying this server's name and version and
// an uptime counted from its start.
func TestHandleHealthCheck(t *testing.T) {
t.Parallel()
g := &globals.Globals{Appname: "netwatch-server", Version: "v1.2.3"}
h := newStartedHandlers(t, g)
rec := httptest.NewRecorder()
req := httptest.NewRequestWithContext(t.Context(),
http.MethodGet, "/.well-known/healthcheck", http.NoBody)
h.HandleHealthCheck().ServeHTTP(rec, req)
if rec.Code != http.StatusOK {
t.Fatalf("status = %d, want %d", rec.Code, http.StatusOK)
}
contentType := rec.Header().Get("Content-Type")
if contentType != "application/json; charset=utf-8" {
t.Errorf("Content-Type = %q, want %q",
contentType, "application/json; charset=utf-8")
}
var body map[string]any
err := json.Unmarshal(rec.Body.Bytes(), &body)
if err != nil {
t.Fatalf("body not a JSON object: %v (%q)", err, rec.Body.String())
}
fields := []string{
"appname", "now", "status", "uptimeHuman", "uptimeSeconds", "version",
}
if got := slices.Sorted(maps.Keys(body)); !slices.Equal(got, fields) {
t.Fatalf("fields = %v, want %v", got, fields)
}
for field, want := range map[string]string{
"appname": g.Appname, "status": "ok", "version": g.Version,
} {
if body[field] != want {
t.Errorf("%s = %v, want %q", field, body[field], want)
}
}
now, _ := body["now"].(string)
at, err := time.Parse(time.RFC3339Nano, now)
if err != nil || time.Since(at).Abs() > time.Minute {
t.Errorf("now = %q, want the current time in RFC 3339 (%v)", now, err)
}
// Started just now, so the uptime is well under a minute.
human, _ := body["uptimeHuman"].(string)
uptime, err := time.ParseDuration(human)
if err != nil || uptime > time.Minute {
t.Errorf("uptimeHuman = %q, want a duration under a minute (%v)",
human, err)
}
seconds, ok := body["uptimeSeconds"].(float64)
if !ok || seconds < 0 || seconds > time.Minute.Seconds() {
t.Errorf("uptimeSeconds = %v, want a number of seconds under a minute",
body["uptimeSeconds"])
}
}
+57
View File
@@ -46,6 +46,63 @@ func decodeStatus(t *testing.T, body []byte) string {
return resp.Status return resp.Status
} }
// TestHandleReportAcceptsValidReports checks the answer to a valid
// report, one with no hosts and one shaped as the frontend sends them:
// 200 and {"status":"ok"} as JSON.
func TestHandleReportAcceptsValidReports(t *testing.T) {
t.Parallel()
tests := []struct {
name string
body string
}{
{
name: "no hosts",
body: `{"clientId":"c1","geo":null,"hosts":[],` +
`"timestamp":"2026-10-03T12:00:00.000Z"}`,
},
{
name: "a host with a latency and an error sample",
body: `{"clientId":"c1","geo":null,"hosts":[{` +
`"name":"Example","url":"https://example.com/",` +
`"status":"error","history":[` +
`{"t":1790000000000,"latency":42,"error":null},` +
`{"t":1790000003000,"latency":null,"error":"timeout"}]}],` +
`"timestamp":"2026-10-03T12:00:00.000Z"}`,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
h := newTestHandlers(stubAppender{}, io.Discard)
rec := httptest.NewRecorder()
req := httptest.NewRequestWithContext(t.Context(),
http.MethodPost, "/api/v1/reports",
strings.NewReader(tt.body),
)
h.HandleReport().ServeHTTP(rec, req)
if rec.Code != http.StatusOK {
t.Fatalf("status = %d, want %d", rec.Code, http.StatusOK)
}
contentType := rec.Header().Get("Content-Type")
if contentType != "application/json; charset=utf-8" {
t.Errorf("Content-Type = %q, want %q",
contentType, "application/json; charset=utf-8")
}
if got := rec.Body.String(); got != "{\"status\":\"ok\"}\n" {
t.Errorf("body = %q, want %q", got, "{\"status\":\"ok\"}\n")
}
})
}
}
func TestHandleReportStorageFailureIsNon2xx(t *testing.T) { func TestHandleReportStorageFailureIsNon2xx(t *testing.T) {
t.Parallel() t.Parallel()
@@ -5,6 +5,10 @@ import (
"time" "time"
) )
// FlushSizeThreshold exposes the buffer size at which Append starts
// writing a report file to the external tests.
const FlushSizeThreshold = flushSizeThreshold
// Flush writes the buffered reports to a file now, as the periodic // Flush writes the buffered reports to a file now, as the periodic
// flush does, so tests need not wait a minute for it. // flush does, so tests need not wait a minute for it.
func (b *Buffer) Flush() error { func (b *Buffer) Flush() error {
+129 -26
View File
@@ -424,15 +424,18 @@ func TestFileDeletedByHandFreesRoom(t *testing.T) {
} }
} }
// TestFileBeingWrittenIsNeverDeleted holds a write open, its file // TestFileBeingWrittenIsNeverDeleted holds the write of one report file
// created but not complete, while a report needs room. The file may // open while a second write completes, then sends a report that needs
// not be deleted to make it, so the report is refused. Once the write // room. Deleting either file would make it, and the one being written is
// is complete, the file is deleted when room is needed. // the older, but only the complete one may be deleted. Once the first
// write is complete, its file is deleted when room is needed.
func TestFileBeingWrittenIsNeverDeleted(t *testing.T) { func TestFileBeingWrittenIsNeverDeleted(t *testing.T) {
report := map[string]string{"id": "writing"} report := map[string]string{"id": "writing"}
t.Setenv("DATA_DIR", t.TempDir()) t.Setenv("DATA_DIR", t.TempDir())
t.Setenv("DATA_DIR_MAX_BYTES", strconv.Itoa(lineBytes(t, report))) // Room for two reports waiting to be written, but not for two
// beside a report file.
t.Setenv("DATA_DIR_MAX_BYTES", strconv.Itoa(2*lineBytes(t, report)))
buf := startBuffer(t) buf := startBuffer(t)
@@ -447,7 +450,7 @@ func TestFileBeingWrittenIsNeverDeleted(t *testing.T) {
err := buf.Append(report) err := buf.Append(report)
if err != nil { if err != nil {
t.Fatalf("report that fills the cap exactly: %v", err) t.Fatalf("first report: %v", err)
} }
flushed := make(chan error) flushed := make(chan error)
@@ -456,32 +459,47 @@ func TestFileBeingWrittenIsNeverDeleted(t *testing.T) {
writing := <-created writing := <-created
// Errorf, not Fatalf, until the write is released, so that a // Only the first write is held; the second goes through, and
// so do the writes after it, the final one at stop included.
var complete string
buf.OnFileCreated(func(f *os.File) { complete = f.Name() })
// Errorf, not Fatalf, until the first write is released, so that a
// failure here does not leave it held. // failure here does not leave it held.
err = buf.Append(report) err = buf.Append(report)
if !errors.Is(err, reportbuf.ErrFull) { if err != nil {
t.Errorf("report while the first was being written: "+ t.Errorf("second report: %v", err)
"error = %v, want ErrFull", err) }
err = buf.Flush()
if err != nil {
t.Errorf("flush of the second report: %v", err)
}
err = buf.Append(report)
if err != nil {
t.Errorf("report that needs room: %v", err)
} }
if !exists(t, writing) { if !exists(t, writing) {
t.Error("report file deleted while it was being written") t.Error("report file deleted while it was being written")
} }
if exists(t, complete) {
t.Error("complete report file kept, though room was needed")
}
close(release) close(release)
err = <-flushed err = <-flushed
if err != nil { if err != nil {
t.Fatalf("flush: %v", err) t.Fatalf("flush of the first report: %v", err)
} }
// Writes from here on, the final one at stop included, go
// through unheld.
buf.OnFileCreated(func(*os.File) {})
err = buf.Append(report) err = buf.Append(report)
if err != nil { if err != nil {
t.Fatalf("report after the write was complete: %v", err) t.Fatalf("report after the first write was complete: %v", err)
} }
if exists(t, writing) { if exists(t, writing) {
@@ -604,7 +622,11 @@ func TestTwoFlushesInOneMillisecond(t *testing.T) {
} }
} }
files := readReportFiles(t, dir) files, err := readReportFiles(dir)
if err != nil {
t.Fatalf("read report files: %v", err)
}
if len(files) != flushes { if len(files) != flushes {
t.Fatalf("%d report files after %d flushes", len(files), flushes) t.Fatalf("%d report files after %d flushes", len(files), flushes)
} }
@@ -617,6 +639,88 @@ func TestTwoFlushesInOneMillisecond(t *testing.T) {
} }
} }
// TestReportFileHoldsTheLinesAppended flushes three reports and reads
// their file back: it must decompress to exactly their JSON lines, in
// the order they were appended.
func TestReportFileHoldsTheLinesAppended(t *testing.T) {
dir := t.TempDir()
t.Setenv("DATA_DIR", dir)
buf := startBuffer(t)
for _, id := range []int{1, 2, 3} {
err := buf.Append(map[string]int{"id": id})
if err != nil {
t.Fatalf("append report %d: %v", id, err)
}
}
err := buf.Flush()
if err != nil {
t.Fatalf("flush: %v", err)
}
files, err := readReportFiles(dir)
if err != nil {
t.Fatalf("read report files: %v", err)
}
want := `{"id":1}` + "\n" + `{"id":2}` + "\n" + `{"id":3}` + "\n"
if len(files) != 1 || files[0] != want {
t.Fatalf("report files = %q, want one holding %q", files, want)
}
}
// TestFlushAtSizeThreshold appends reports until the buffer holds
// FlushSizeThreshold bytes. The append that gets it there must write
// them all to one report file, with no call to Flush and the periodic
// flush a minute away, and no earlier append may write one.
func TestFlushAtSizeThreshold(t *testing.T) {
dir := t.TempDir()
t.Setenv("DATA_DIR", dir)
buf := startBuffer(t)
// Large reports, so the threshold takes a few hundred appends.
pad := strings.Repeat("a", 64<<10)
var appended strings.Builder
for id := 0; appended.Len() < reportbuf.FlushSizeThreshold; id++ {
report := map[string]any{"id": id, "pad": pad}
err := buf.Append(report)
if err != nil {
t.Fatalf("append report %d: %v", id, err)
}
line, err := json.Marshal(report)
if err != nil {
t.Fatalf("marshal report %d: %v", id, err)
}
appended.Write(line)
appended.WriteByte('\n')
}
// Append writes the file in the background, so wait for it.
deadline := time.Now().Add(10 * time.Second)
for {
files, err := readReportFiles(dir)
if err == nil && len(files) == 1 && files[0] == appended.String() {
return
}
if time.Now().After(deadline) {
t.Fatalf("%d report files (error: %v), want one holding the "+
"%d bytes appended", len(files), err, appended.Len())
}
time.Sleep(10 * time.Millisecond)
}
}
// reportFilesBytes returns the total size of the report files in dir. // reportFilesBytes returns the total size of the report files in dir.
func reportFilesBytes(t *testing.T, dir string) int64 { func reportFilesBytes(t *testing.T, dir string) int64 {
t.Helper() t.Helper()
@@ -641,20 +745,19 @@ func reportFilesBytes(t *testing.T, dir string) int64 {
} }
// readReportFiles returns the decompressed contents of each report // readReportFiles returns the decompressed contents of each report
// file in dir. // file in dir. A file still being written does not decompress, so it
func readReportFiles(t *testing.T, dir string) []string { // gives an error.
t.Helper() func readReportFiles(dir string) ([]string, error) {
files := os.DirFS(dir) files := os.DirFS(dir)
names, err := fs.Glob(files, "reports-*.jsonl.zst") names, err := fs.Glob(files, "reports-*.jsonl.zst")
if err != nil { if err != nil {
t.Fatalf("list report files: %v", err) return nil, fmt.Errorf("list report files: %w", err)
} }
dec, err := zstd.NewReader(nil) dec, err := zstd.NewReader(nil)
if err != nil { if err != nil {
t.Fatalf("create zstd decoder: %v", err) return nil, fmt.Errorf("create zstd decoder: %w", err)
} }
defer dec.Close() defer dec.Close()
@@ -663,18 +766,18 @@ func readReportFiles(t *testing.T, dir string) []string {
for _, name := range names { for _, name := range names {
compressed, readErr := fs.ReadFile(files, name) compressed, readErr := fs.ReadFile(files, name)
if readErr != nil { if readErr != nil {
t.Fatalf("read %s: %v", name, readErr) return nil, fmt.Errorf("read %s: %w", name, readErr)
} }
data, decErr := dec.DecodeAll(compressed, nil) data, decErr := dec.DecodeAll(compressed, nil)
if decErr != nil { if decErr != nil {
t.Fatalf("decompress %s: %v", name, decErr) return nil, fmt.Errorf("decompress %s: %w", name, decErr)
} }
contents = append(contents, string(data)) contents = append(contents, string(data))
} }
return contents return contents, nil
} }
// reportFilePath returns the path in dir of a report file named as // reportFilePath returns the path in dir of a report file named as
+10 -2
View File
@@ -1,12 +1,20 @@
#!/bin/sh #!/bin/sh
# script/test: run the backend test suite. # script/test: run the backend test suite with the race detector and
# coverage. Go's own -timeout bounds the tests and not their compile,
# so a cold build cache cannot fail it. The race detector needs cgo,
# and so a C compiler. If the tests fail, they run again with -v for
# the details, and the script fails even if that run passes.
set -eu set -eu
ROOT="$(cd "$(dirname "$0")/.." && pwd -P)" ROOT="$(cd "$(dirname "$0")/.." && pwd -P)"
main() { main() {
cd "$ROOT" cd "$ROOT"
timeout 30 go test ./... go test -timeout 30s -race -cover ./... || {
echo "--- Rerunning with -v for details ---"
go test -timeout 30s -race -v ./...
exit 1
}
} }
main "$@" main "$@"
+10 -3
View File
@@ -64,7 +64,8 @@ detect_pkgmgr() {
fi fi
} }
# pkg_install <nix-attr> <apt-pkg> <brew-formula> <apk-pkg> # pkg_install <nix-attr> <apt-pkgs> <brew-formula> <apk-pkgs>: the apt
# and apk arguments may each list several packages, separated by spaces.
pkg_install() { pkg_install() {
detect_pkgmgr detect_pkgmgr
case "$PKGMGR" in case "$PKGMGR" in
@@ -74,10 +75,10 @@ pkg_install() {
$SUDO env DEBIAN_FRONTEND=noninteractive apt-get update $SUDO env DEBIAN_FRONTEND=noninteractive apt-get update
APT_UPDATED=1 APT_UPDATED=1
fi fi
$SUDO env DEBIAN_FRONTEND=noninteractive apt-get install -y "$2" $SUDO env DEBIAN_FRONTEND=noninteractive apt-get install -y $2
;; ;;
brew) brew install "$3" ;; brew) brew install "$3" ;;
apk) apk add --no-cache "$4" ;; apk) apk add --no-cache $4 ;;
esac esac
} }
@@ -253,6 +254,12 @@ main() {
if missing make; then pkg_install gnumake make make make; fi if missing make; then pkg_install gnumake make make make; fi
if missing git; then pkg_install git git git git; fi if missing git; then pkg_install git git git git; fi
# The race detector in make test needs cgo, which Go turns on only
# when it finds its C compiler, gcc on Linux. apt and apk ship the C
# library headers apart from gcc.
if missing gcc; then
pkg_install gcc "gcc libc6-dev" gcc "gcc musl-dev"
fi
ensure_node ensure_node
ensure_yarn ensure_yarn
+6 -3
View File
@@ -1,14 +1,17 @@
#!/bin/sh #!/bin/sh
# script/test: run the test suite for the whole repo: the frontend at # script/test: run the test suite for the whole repo: the frontend at
# the repo root, then the Go backend in backend/. Both halves together # the repo root, then the Go backend in backend/. Each half has its own
# get 30 seconds; each also keeps its own limit for the Dockerfiles. # 30-second limit, and there is none around both: from a cold Go build
# cache, compiling the backend's tests with the race detector can take
# 30 seconds on its own, and Go's -timeout leaves the compile out.
set -eu set -eu
ROOT="$(cd "$(dirname "$0")/.." && pwd -P)" ROOT="$(cd "$(dirname "$0")/.." && pwd -P)"
main() { main() {
cd "$ROOT" cd "$ROOT"
timeout 30 sh -c 'script/frontend-test && backend/script/test' script/frontend-test
backend/script/test
} }
main "$@" main "$@"