// SPDX-License-Identifier: GPL-3.0-or-later package model import ( "errors" "os" "path/filepath" "strings" "testing" "krino/internal/engine" ) // mainConf opens krino.conf over a sandbox whose main file holds text. func mainConf(t *testing.T, main string) (*MainConf, *engine.Engine, string) { t.Helper() e, h := sandboxDir(t, "(path \"~/dl\")\n(rule \"all\" (move \"Out\"))\n", map[string]string{"a.pdf": "one"}) if main != "" { if err := os.WriteFile(e.MainFile, []byte(main), 0o644); err != nil { t.Fatal(err) } reloaded, diags := engine.Load(e.MainFile) if len(diags) > 0 { t.Fatalf("the fixture does not load: %v", diags) } e = reloaded } m, err := OpenMain(e) if err != nil { t.Fatal(err) } return m, e, h } // TestMainSettingsReadAndWrite: a default is read as written, changed in // place, and taken out again, with everything else in the file untouched. func TestMainSettingsReadAndWrite(t *testing.T) { main := ";; the main file\n(include \"dl\")\n\n(defaults\n (min-age 2m) ; leave fresh files\n (on-conflict suffix))\n" m, _, _ := mainConf(t, main) if args, set, err := m.Setting("min-age"); err != nil || !set || args != "2m" { t.Errorf("min-age = %q, %v, %v", args, set, err) } if _, set, _ := m.Setting("fold"); set { t.Error("fold reads as set") } if err := m.SetSetting("min-age", "1d"); err != nil { t.Fatal(err) } if !strings.Contains(m.Text, "(min-age 1d)") || strings.Contains(m.Text, "(min-age 2m)") { t.Errorf("min-age not rewritten:\n%s", m.Text) } for _, keep := range []string{";; the main file", "(include \"dl\")", "; leave fresh files", "(on-conflict suffix)"} { if !strings.Contains(m.Text, keep) { t.Errorf("writing one setting lost %q:\n%s", keep, m.Text) } } // A setting the file does not have goes inside (defaults ...). if err := m.SetSetting("fold", "no"); err != nil { t.Fatal(err) } if !strings.Contains(m.Text, "(fold no)") { t.Errorf("fold not written:\n%s", m.Text) } if strings.Index(m.Text, "(fold no)") < strings.Index(m.Text, "(defaults") { t.Errorf("fold landed outside the defaults:\n%s", m.Text) } if diags := m.Check(); len(diags) > 0 { t.Fatalf("the file no longer loads: %v", diags) } // And out again. if err := m.SetSetting("min-age", ""); err != nil { t.Fatal(err) } if strings.Contains(m.Text, "min-age") { t.Errorf("min-age still there:\n%s", m.Text) } if diags := m.Check(); len(diags) > 0 { t.Errorf("the file no longer loads: %v", diags) } } // TestMainSettingsWithoutADefaultsForm: a file with no (defaults ...) gets // one, and it loads. func TestMainSettingsWithoutADefaultsForm(t *testing.T) { m, _, _ := mainConf(t, "(include \"dl\")\n") if err := m.SetSetting("recursive", "yes"); err != nil { t.Fatal(err) } if !strings.Contains(m.Text, "(defaults") || !strings.Contains(m.Text, "(recursive yes)") { t.Errorf("no defaults form was written:\n%s", m.Text) } if diags := m.Check(); len(diags) > 0 { t.Errorf("the file no longer loads: %v", diags) } } // TestMainLogIsTopLevel: the log path is not a default; it is written at // the top level, where krino.conf(5) puts it. func TestMainLogIsTopLevel(t *testing.T) { m, _, h := mainConf(t, "(include \"dl\")\n") if err := m.SetSetting("log", "\"~/tmp/krino.log\""); err != nil { t.Fatal(err) } if strings.Contains(m.Text, "(defaults") { t.Errorf("the log went into the defaults:\n%s", m.Text) } if diags := m.Check(); len(diags) > 0 { t.Fatalf("the file no longer loads: %v", diags) } if err := m.Save(); err != nil { t.Fatal(err) } e, diags := engine.Load(filepath.Join(h, ".config", "krino", "krino.conf")) if len(diags) > 0 { t.Fatal(diags) } if !strings.HasSuffix(e.Config.LogFile(), "tmp/krino.log") { t.Errorf("the saved log path is not in effect: %s", e.Config.LogFile()) } } // TestMainSaveGuards: krino.conf is saved by the same rules as a directory // file - never broken, never over someone else's edit, and the previous // text is kept. func TestMainSaveGuards(t *testing.T) { main := "(include \"dl\")\n" m, _, _ := mainConf(t, main) m.Text = "(include \"dl\")\n(defaults (min-age nonsense))\n" if err := m.Save(); err == nil { t.Error("a file that will not load was saved") } if on, _ := os.ReadFile(m.File); string(on) != main { t.Errorf("the refused save wrote anyway: %q", on) } m.Text = "(include \"dl\")\n(defaults (min-age 1d))\n" if err := m.Save(); err != nil { t.Fatal(err) } if on, _ := os.ReadFile(m.File + ".bak"); string(on) != main { t.Errorf("backup = %q, want the previous text", on) } // Someone else edits it, and the next save refuses. if err := os.WriteFile(m.File, []byte("(include \"dl\")\n;; theirs\n"), 0o644); err != nil { t.Fatal(err) } m.Text = "(include \"dl\")\n;; mine\n" if err := m.Save(); !errors.Is(err, ErrChangedOnDisk) { t.Errorf("Save = %v, want ErrChangedOnDisk", err) } } // TestMainSettingRefusesAnUnknownHead: only the settings krino.conf(5) // documents can be written this way. func TestMainSettingRefusesAnUnknownHead(t *testing.T) { m, _, _ := mainConf(t, "(include \"dl\")\n") if err := m.SetSetting("nonsense", "1"); err == nil { t.Error("an unknown setting was accepted") } if _, _, err := m.Setting("nonsense"); err == nil { t.Error("an unknown setting was read") } }