aboutsummaryrefslogtreecommitdiff
path: root/internal/journal/journal_test.go
diff options
context:
space:
mode:
authorLukasz Kasprzak <lukas@labunix.xyz>2026-09-12 20:14:47 +0200
committerLukasz Kasprzak <lukas@labunix.xyz>2026-09-12 20:14:47 +0200
commit3f8679be9373ee7508d512dfdfc1dda0839c7f90 (patch)
treeec02eb075f6c4e90f21baa2fe674e86a2f7f6a62 /internal/journal/journal_test.go
parent24a84671ace373ae331fa83a1ff484990f4dff0e (diff)
downloadkrino-3f8679be9373ee7508d512dfdfc1dda0839c7f90.tar.gz
krino-3f8679be9373ee7508d512dfdfc1dda0839c7f90.zip
krino: acting — trash, journal, apply, lock, review, undo
Diffstat (limited to 'internal/journal/journal_test.go')
-rw-r--r--internal/journal/journal_test.go150
1 files changed, 150 insertions, 0 deletions
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)
+ }
+}