From 3f8679be9373ee7508d512dfdfc1dda0839c7f90 Mon Sep 17 00:00:00 2001 From: Lukasz Kasprzak Date: Sat, 12 Sep 2026 20:14:47 +0200 Subject: krino: acting — trash, journal, apply, lock, review, undo MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- internal/journal/journal_test.go | 150 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 150 insertions(+) create mode 100644 internal/journal/journal_test.go (limited to 'internal/journal/journal_test.go') diff --git a/internal/journal/journal_test.go b/internal/journal/journal_test.go new file mode 100644 index 0000000..a95bb05 --- /dev/null +++ b/internal/journal/journal_test.go @@ -0,0 +1,150 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +package journal + +import ( + "os" + "path/filepath" + "strings" + "testing" + "time" +) + +func TestNewRunID(t *testing.T) { + at := time.Date(2026, 9, 11, 10, 2, 3, 0, time.UTC) + id := NewRunID(at) + if !strings.HasPrefix(id, "20260911T100203-") || len(id) != len("20260911T100203-")+4 { + t.Fatalf("run id = %q", id) + } + if NewRunID(at) == id { + t.Error("two run ids for the same instant collided; the suffix is not random") + } +} + +// TestRoundTripsAwkwardNames is the point of the escaping: a file name with a +// tab, a newline, a backslash, a control byte or invalid UTF-8 must come back +// byte-for-byte, and must not break the line or column structure awk sees. +func TestRoundTripsAwkwardNames(t *testing.T) { + names := []string{ + "plain.pdf", + "with\ttab.pdf", + "with\nnewline.pdf", + "back\\slash.pdf", + "bell\a.pdf", + "invalid\xff\xfeutf8.pdf", + "zażółć gęślą jaźń.pdf", + } + path := filepath.Join(t.TempDir(), "state", "krino.log") + w, err := Open(path) + if err != nil { + t.Fatal(err) + } + at := time.Date(2026, 9, 11, 10, 2, 3, 0, time.FixedZone("CEST", 2*3600)) + if err := w.Append(Entry{Time: at, Run: "R", Action: "run-start", Status: "ok"}); err != nil { + t.Fatal(err) + } + for i, n := range names { + e := Entry{Time: at, Run: "R", Dir: "dl", File: n, Step: i + 1, + Action: "move", Status: "ok", Rule: "acme", Src: "/a/" + n, Dst: "/b/" + n, + Size: int64(i), ModTime: at, Detail: ""} + if err := w.Append(e); err != nil { + t.Fatal(err) + } + } + if err := w.Close(); err != nil { + t.Fatal(err) + } + + raw, err := os.ReadFile(path) + if err != nil { + t.Fatal(err) + } + if got := strings.Count(string(raw), "\n"); got != len(names)+1 { + t.Errorf("file has %d lines, want %d: a field broke the line structure", got, len(names)+1) + } + for _, line := range strings.Split(strings.TrimRight(string(raw), "\n"), "\n") { + if n := strings.Count(line, "\t"); n != 12 { + t.Errorf("line has %d tabs, want 12 (13 columns): %q", n, line) + } + } + + got, err := Entries(path, "R") + if err != nil { + t.Fatal(err) + } + if len(got) != len(names)+1 { + t.Fatalf("read %d entries, want %d", len(got), len(names)+1) + } + for i, n := range names { + if got[i+1].File != n { + t.Errorf("entry %d: File = %q, want %q", i, got[i+1].File, n) + } + if got[i+1].Src != "/a/"+n || got[i+1].Dst != "/b/"+n { + t.Errorf("entry %d: paths did not round-trip: %q %q", i, got[i+1].Src, got[i+1].Dst) + } + if !got[i+1].Time.Equal(at) { + t.Errorf("entry %d: Time = %v, want %v", i, got[i+1].Time, at) + } + } +} + +// TestAppendWritesRFC3339WithOffsetAndKeepsNanoseconds is item 13, promoted +// to before-commit by the plan 4 final review: nothing anywhere pinned the +// journal's on-disk time format - RFC3339Nano appears in no test file, and +// every timestamp assertion round-trips through krino's own Writer and +// Entries, so a change to something no other tool could parse would pass +// silently. The journal is the only record undo has. This reads the RAW +// bytes of a written line - not Entries, which would launder the format +// through krino's own parser - and asserts column 1 parses as RFC 3339 with +// a real numeric offset (not just "Z"), and that a time carrying +// nanoseconds keeps them. +func TestAppendWritesRFC3339WithOffsetAndKeepsNanoseconds(t *testing.T) { + path := filepath.Join(t.TempDir(), "state", "krino.log") + w, err := Open(path) + if err != nil { + t.Fatal(err) + } + at := time.Date(2026, 9, 11, 10, 2, 3, 123456789, time.FixedZone("", 2*3600)) + if err := w.Append(Entry{Time: at, Run: "R", Action: "run-start", Status: "ok"}); err != nil { + t.Fatal(err) + } + if err := w.Close(); err != nil { + t.Fatal(err) + } + + raw, err := os.ReadFile(path) + if err != nil { + t.Fatal(err) + } + line := strings.TrimSuffix(string(raw), "\n") + col1 := strings.SplitN(line, "\t", 2)[0] + + parsed, err := time.Parse(time.RFC3339, col1) + if err != nil { + t.Fatalf("column 1 %q does not parse as RFC 3339: %v", col1, err) + } + if _, offset := parsed.Zone(); offset != 2*3600 { + t.Errorf("offset = %ds, want %ds: the written column must carry a real offset, not just a bare local time", offset, 2*3600) + } + if parsed.Nanosecond() != 123456789 { + t.Errorf("nanoseconds = %d, want 123456789: a nanosecond-precision time must not be truncated on the wire", parsed.Nanosecond()) + } +} + +func TestAppendIsAppendOnly(t *testing.T) { + path := filepath.Join(t.TempDir(), "krino.log") + for i := 0; i < 2; i++ { + w, err := Open(path) + if err != nil { + t.Fatal(err) + } + if err := w.Append(Entry{Time: time.Now(), Run: "R", Action: "run-start", Status: "ok"}); err != nil { + t.Fatal(err) + } + w.Close() + } + raw, _ := os.ReadFile(path) + if got := strings.Count(string(raw), "\n"); got != 2 { + t.Errorf("%d lines after two Open/Append/Close cycles, want 2: the second Open truncated", got) + } +} -- cgit v1.3