Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6092c62223 | ||
|
|
648c225919 |
@@ -26,12 +26,21 @@ Run `make frontend-viewport-test` in CI as its own step; it is not part of
|
||||
failed writes are written in the order they arrived. Two writes that failed at
|
||||
the same time put their reports back in the wrong order when the older failed
|
||||
first. One write runs at a time, and each takes the waiting reports only when
|
||||
it starts, after a failed write has put its own back. The write that 10 MiB
|
||||
waiting starts waits for one under way, and only one such write waits at a
|
||||
time. A unit test holds a write open, starts a second at 10 MiB, fails both
|
||||
and checks the order in the next file. Files written since the start are
|
||||
deleted to make room in the order they were written, after those found at
|
||||
start
|
||||
it starts, after a failed write has put its own back. The write `Append`
|
||||
starts when 10 MiB are waiting waits for any write under way, and `Append`
|
||||
starts no other while it waits. Files written since the start are deleted to
|
||||
make room in the order they were written, after those found at start. A unit
|
||||
test holds a write open, starts a second at 10 MiB, fails both and checks the
|
||||
order in the next file. Others check that `Append` starts no second write at
|
||||
10 MiB while one waits, that it starts one again the next time 10 MiB are
|
||||
waiting, and that the first file written since the start is the first deleted
|
||||
- 2026-10-19: the three unit tests of a check whose result is discarded (its
|
||||
round given up, the user paused, or the first round) also check that the
|
||||
host's history is still empty, its status still `pending`, and nothing was
|
||||
written to the console
|
||||
([#151](https://git.eeqj.de/sneak/netwatch/issues/151)). They checked only the
|
||||
row, so a change that recorded the result and skipped only the row would have
|
||||
kept them passing
|
||||
- 2026-10-18: when the backend runs on its own, a `DATA_DIR` that cannot be
|
||||
created or read, or where no file can be created, stops the start with an
|
||||
error naming `DATA_DIR` and its value, as the other settings' errors do
|
||||
|
||||
@@ -581,6 +581,62 @@ func TestWrittenFilesDeletedToMakeRoom(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// TestWrittenFilesDeletedOldestFirst writes three report files, then
|
||||
// appends a report that fits only once one of them is deleted. The
|
||||
// first one written must be the one deleted.
|
||||
func TestWrittenFilesDeletedOldestFirst(t *testing.T) {
|
||||
const maxBytes = 1000
|
||||
|
||||
report := map[string]string{"id": "written"}
|
||||
|
||||
t.Setenv("DATA_DIR", t.TempDir())
|
||||
t.Setenv("DATA_DIR_MAX_BYTES", strconv.Itoa(maxBytes))
|
||||
|
||||
buf := startBuffer(t)
|
||||
|
||||
var written []string
|
||||
|
||||
buf.OnFileCreated(func(f *os.File) { written = append(written, f.Name()) })
|
||||
|
||||
for range 3 {
|
||||
err := buf.Append(report)
|
||||
if err != nil {
|
||||
t.Fatalf("append report: %v", err)
|
||||
}
|
||||
|
||||
err = buf.Flush()
|
||||
if err != nil {
|
||||
t.Fatalf("flush: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// The three files hold the same report, so they are the same size.
|
||||
info, err := os.Stat(written[0])
|
||||
if err != nil {
|
||||
t.Fatalf("stat %s: %v", written[0], err)
|
||||
}
|
||||
|
||||
// A report that fits beside two of the files, so deleting one makes
|
||||
// exactly the room it needs.
|
||||
pad := maxBytes - 2*int(info.Size()) -
|
||||
lineBytes(t, map[string]string{"id": ""})
|
||||
|
||||
err = buf.Append(map[string]string{"id": strings.Repeat("a", pad)})
|
||||
if err != nil {
|
||||
t.Fatalf("report that needs room: %v", err)
|
||||
}
|
||||
|
||||
if exists(t, written[0]) {
|
||||
t.Fatal("first report file written kept when room was needed")
|
||||
}
|
||||
|
||||
for _, path := range written[1:] {
|
||||
if !exists(t, path) {
|
||||
t.Fatalf("%s deleted before the first report file written", path)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestFailedDeletionStillCounts makes deleting the oldest report file
|
||||
// fail. It is still there, so it still takes room, and the next oldest
|
||||
// is deleted in its place.
|
||||
@@ -1223,6 +1279,106 @@ func TestFlushAtSizeThreshold(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// TestEachSizeThresholdStartsAWrite reaches FlushSizeThreshold twice,
|
||||
// with no call to Flush and the periodic flush a minute away. Append
|
||||
// must start a write each time, so that the reports end up in two
|
||||
// report files. The test runs in a synctest bubble, so that
|
||||
// synctest.Wait returns once each write is done.
|
||||
func TestEachSizeThresholdStartsAWrite(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
t.Setenv("DATA_DIR", dir)
|
||||
|
||||
synctest.Test(t, func(t *testing.T) {
|
||||
buf := startBuffer(t)
|
||||
|
||||
var appended string
|
||||
|
||||
for range 2 {
|
||||
appended = appendToThreshold(t, buf)
|
||||
|
||||
synctest.Wait()
|
||||
}
|
||||
|
||||
files, err := readReportFiles(dir)
|
||||
if err != nil {
|
||||
t.Fatalf("read report files: %v", err)
|
||||
}
|
||||
|
||||
if !slices.Equal(files, []string{appended, appended}) {
|
||||
t.Fatalf("%d report files, want two, each holding the %d "+
|
||||
"bytes appended to reach the threshold",
|
||||
len(files), len(appended))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// TestAppendStartsOneWaitingWrite holds a write open, then appends two
|
||||
// reports of FlushSizeThreshold bytes each. Append starts a write at
|
||||
// the first, which waits for the held one, and none at the second,
|
||||
// since that write is still waiting. Every write fails, so a write
|
||||
// started at the second report would find the reports back in the
|
||||
// buffer and try them again: once the held write is released, two
|
||||
// writes must have been started, the held one and one more.
|
||||
func TestAppendStartsOneWaitingWrite(t *testing.T) {
|
||||
t.Setenv("DATA_DIR", t.TempDir())
|
||||
|
||||
synctest.Test(t, func(t *testing.T) {
|
||||
buf := startBuffer(t)
|
||||
|
||||
var writes atomic.Int64
|
||||
|
||||
release := make(chan struct{})
|
||||
|
||||
// Each write is held until release is closed, then fails, as its
|
||||
// file is closed under it.
|
||||
buf.OnFileCreated(func(f *os.File) {
|
||||
writes.Add(1)
|
||||
|
||||
<-release
|
||||
|
||||
_ = f.Close()
|
||||
})
|
||||
|
||||
err := buf.Append(map[string]string{"id": "one-waiting"})
|
||||
if err != nil {
|
||||
t.Fatalf("report of the held write: %v", err)
|
||||
}
|
||||
|
||||
go func() { _ = buf.Flush() }()
|
||||
|
||||
// Wait returns once the flush's write is held.
|
||||
synctest.Wait()
|
||||
|
||||
big := map[string]string{
|
||||
"id": strings.Repeat("a", reportbuf.FlushSizeThreshold),
|
||||
}
|
||||
|
||||
// Errorf, not Fatalf, until the held write is released, so that a
|
||||
// failure here does not leave it held.
|
||||
for range 2 {
|
||||
err = buf.Append(big)
|
||||
if err != nil {
|
||||
t.Errorf("report of FlushSizeThreshold bytes: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
close(release)
|
||||
|
||||
// Wait returns once every write started is done.
|
||||
synctest.Wait()
|
||||
|
||||
started := writes.Load()
|
||||
|
||||
// Writes from here on, the final one at stop included, succeed.
|
||||
buf.OnFileCreated(func(*os.File) {})
|
||||
|
||||
if started != 2 {
|
||||
t.Fatalf("%d writes started, want 2: the held one and one "+
|
||||
"at the threshold", started)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// appendToThreshold appends reports to buf until it holds
|
||||
// FlushSizeThreshold bytes, so that the last append starts writing them
|
||||
// to a report file, and returns the lines appended.
|
||||
|
||||
+22
-9
@@ -83,6 +83,11 @@ function statusText(state, host) {
|
||||
return elements[`.status-text[data-host="${index}"]`]?.innerHTML;
|
||||
}
|
||||
|
||||
// What this test wrote to the browser console with console[method].
|
||||
function consoleLines(method) {
|
||||
return console[method].mock.calls.map((call) => call.arguments[0]);
|
||||
}
|
||||
|
||||
// Mocks the clock for test t, so that a check lasting seconds takes no real
|
||||
// time, and replaces fetch with targets that each answer after
|
||||
// answerAfter(url) milliseconds of that clock, or never when that is
|
||||
@@ -202,9 +207,10 @@ test("at a 30000ms interval, a target answering after 1000ms shows in its row wh
|
||||
});
|
||||
|
||||
// In the next three tests, the answering target's check is still waiting
|
||||
// when something happens after which its result must not show.
|
||||
// when something happens after which its result must not be recorded or
|
||||
// show in its row, and the check must write nothing to the console.
|
||||
|
||||
test("at a 30000ms interval, a check still waiting when its round is given up does not show in its row", async (t) => {
|
||||
test("at a 30000ms interval, a check still waiting when its round is given up is not recorded and does not show in its row", async (t) => {
|
||||
CONFIG.updateInterval = 30000;
|
||||
const state = new AppState([
|
||||
{ name: "Answering", url: "https://answering.test" },
|
||||
@@ -220,9 +226,13 @@ test("at a 30000ms interval, a check still waiting when its round is given up do
|
||||
roundChecks.abort();
|
||||
assert.notEqual(await settled(round), "still waiting");
|
||||
assert.equal(latencyFigure(state, answering), undefined);
|
||||
assert.deepEqual(answering.history, []);
|
||||
assert.equal(answering.status, "pending");
|
||||
assert.deepEqual(consoleLines("error"), []);
|
||||
assert.deepEqual(consoleLines("info"), []);
|
||||
});
|
||||
|
||||
test("at a 30000ms interval, a check still waiting when the user pauses does not show in its row", async (t) => {
|
||||
test("at a 30000ms interval, a check still waiting when the user pauses is not recorded and does not show in its row", async (t) => {
|
||||
CONFIG.updateInterval = 30000;
|
||||
const state = new AppState([
|
||||
{ name: "Answering", url: "https://answering.test" },
|
||||
@@ -237,9 +247,13 @@ test("at a 30000ms interval, a check still waiting when the user pauses does not
|
||||
t.mock.timers.tick(500);
|
||||
assert.equal(await settled(round), "still waiting");
|
||||
assert.equal(latencyFigure(state, answering), undefined);
|
||||
assert.deepEqual(answering.history, []);
|
||||
assert.equal(answering.status, "pending");
|
||||
assert.deepEqual(consoleLines("error"), []);
|
||||
assert.deepEqual(consoleLines("info"), []);
|
||||
});
|
||||
|
||||
test("at a 30000ms interval, a check in the first round does not show in its row", async (t) => {
|
||||
test("at a 30000ms interval, a check in the first round is not recorded and does not show in its row", async (t) => {
|
||||
CONFIG.updateInterval = 30000;
|
||||
const state = new AppState([
|
||||
{ name: "Answering", url: "https://answering.test" },
|
||||
@@ -251,6 +265,10 @@ test("at a 30000ms interval, a check in the first round does not show in its row
|
||||
t.mock.timers.tick(1000);
|
||||
assert.equal(await settled(round), "still waiting");
|
||||
assert.equal(latencyFigure(state, answering), undefined);
|
||||
assert.deepEqual(answering.history, []);
|
||||
assert.equal(answering.status, "pending");
|
||||
assert.deepEqual(consoleLines("error"), []);
|
||||
assert.deepEqual(consoleLines("info"), []);
|
||||
});
|
||||
|
||||
test("at a 30000ms interval, after the user pauses and resumes during a round, no row reads paused once its last check ends", async (t) => {
|
||||
@@ -300,11 +318,6 @@ function stateWithOneTarget() {
|
||||
return { state, target: state.local[0] };
|
||||
}
|
||||
|
||||
// What this test wrote to the browser console with console[method].
|
||||
function consoleLines(method) {
|
||||
return console[method].mock.calls.map((call) => call.arguments[0]);
|
||||
}
|
||||
|
||||
test("a check that fails with a network error writes one console line with the target, the time, the error and how long the request took", async (t) => {
|
||||
const { state, target } = stateWithOneTarget();
|
||||
mockTargets(t, () => 23, target.url);
|
||||
|
||||
Reference in New Issue
Block a user