From f7498c9ae626b9ab654fe6eb736e19c62cf5d38d Mon Sep 17 00:00:00 2001 From: Paul Buetow Date: Mon, 6 Jul 2026 10:14:39 +0300 Subject: refactor --- api/api.go | 6 +- internal/resource/dir/dir.go | 6 +- internal/resource/dir/dir_test.go | 360 ++++++++++++++++-------------------- internal/resource/file/file.go | 6 +- internal/resource/file/file_test.go | 22 +-- internal/resource/link/link.go | 6 +- internal/resource/link/link_test.go | 212 ++++++++++----------- 7 files changed, 283 insertions(+), 335 deletions(-) diff --git a/api/api.go b/api/api.go index d82c2c8..f4ec70e 100644 --- a/api/api.go +++ b/api/api.go @@ -10,7 +10,7 @@ import ( // File creates a file resource. func File(path string, opts ...option.Option) resource.Resource { - return file.Have(path, opts...) + return file.Present(path, opts...) } // NoFile creates a file resource that is ensured to be absent. @@ -20,7 +20,7 @@ func NoFile(path string, opts ...option.Option) resource.Resource { // Dir creates a directory resource. func Dir(path string, opts ...option.Option) resource.Resource { - return dir.Have(path, opts...) + return dir.Present(path, opts...) } // NoDir creates a directory resource that is ensured to be absent. @@ -30,7 +30,7 @@ func NoDir(path string, opts ...option.Option) resource.Resource { // Link creates a link resource (symbolic or hard). func Link(path string, opts ...option.Option) resource.Resource { - return link.Have(path, opts...) + return link.Present(path, opts...) } // NoLink creates a link resource that is ensured to be absent. diff --git a/internal/resource/dir/dir.go b/internal/resource/dir/dir.go index 34b161d..283cea9 100644 --- a/internal/resource/dir/dir.go +++ b/internal/resource/dir/dir.go @@ -7,8 +7,8 @@ import ( "os/user" "strconv" - "codeberg.org/snonux/gonf/internal/resource" opt "codeberg.org/snonux/gonf/api/option" + "codeberg.org/snonux/gonf/internal/resource" ) type Dir struct { @@ -187,7 +187,7 @@ func Ensure(path string, opts ...opt.Option) error { return d.apply() } -func Have(path string, opts ...opt.Option) resource.Resource { +func Present(path string, opts ...opt.Option) resource.Resource { d, err := build(path, opts...) if err != nil { log.Fatalf("failed to apply directory resource %s: %v", path, err) @@ -201,5 +201,5 @@ func Have(path string, opts ...opt.Option) resource.Resource { func Absent(path string, opts ...opt.Option) resource.Resource { opts = append(opts, opt.IsAbsent()) - return Have(path, opts...) + return Present(path, opts...) } diff --git a/internal/resource/dir/dir_test.go b/internal/resource/dir/dir_test.go index 6a5bb83..eb22268 100644 --- a/internal/resource/dir/dir_test.go +++ b/internal/resource/dir/dir_test.go @@ -5,197 +5,170 @@ import ( "path/filepath" "testing" - resource "codeberg.org/snonux/gonf/internal/resource" - "codeberg.org/snonux/gonf/internal/resource/file" - + "codeberg.org/snonux/gonf/internal/resource" . "codeberg.org/snonux/gonf/api/option" ) -func TestHaveDirectoryCreate(t *testing.T) { +func TestPresentDirectoryCreate(t *testing.T) { resource.ResetRepository() - tmp := t.TempDir() - path := filepath.Join(tmp, "sub", "nested") + dir := t.TempDir() + path := filepath.Join(dir, "newdir") - Have(path) + Present(path) if err := resource.Apply(); err != nil { t.Fatalf("Apply failed: %v", err) } info, err := os.Stat(path) if err != nil { - t.Fatalf("stat: %v", err) + t.Fatal(err) } if !info.IsDir() { - t.Errorf("expected a directory at %s", path) - } - if info.Mode().Perm() != 0o750 { - t.Errorf("expected default dir mode 0750, got %v", info.Mode().Perm()) + t.Errorf("expected %s to be a directory", path) } } -func TestHaveDirectoryIdempotentWithMode(t *testing.T) { +func TestPresentDirectoryIdempotentWithMode(t *testing.T) { resource.ResetRepository() - tmp := t.TempDir() - path := filepath.Join(tmp, "d") + dir := t.TempDir() + path := filepath.Join(dir, "modedir") + mode := os.FileMode(0o700) - // Call ensureDirectorySelf directly to exercise idempotency without the - // one-per-process resource registry rejecting a duplicate registration. - d1 := &Dir{path: path, mode: 0o755} - if err := ensureDirectorySelf(d1); err != nil { - t.Fatalf("first apply: %v", err) - } - d2 := &Dir{path: path, mode: 0o700} - if err := ensureDirectorySelf(d2); err != nil { - t.Fatalf("second apply: %v", err) + Present(path, WithMode(mode)) + if err := resource.Apply(); err != nil { + t.Fatalf("Apply failed: %v", err) } info, err := os.Stat(path) if err != nil { t.Fatal(err) } - if info.Mode().Perm() != 0o700 { - t.Errorf("expected mode 0700 enforced, got %v", info.Mode().Perm()) + if info.Mode().Perm() != mode { + t.Errorf("expected mode %v, got %v", mode, info.Mode().Perm()) + } + + // Idempotency check + if err := resource.Apply(); err != nil { + t.Fatalf("second Apply failed: %v", err) } } -func TestHaveDirectoryFailsWhenFileExists(t *testing.T) { +func TestPresentDirectoryFailsWhenFileExists(t *testing.T) { resource.ResetRepository() - tmp := t.TempDir() - path := filepath.Join(tmp, "afile") - if err := os.WriteFile(path, []byte("x"), 0o644); err != nil { + dir := t.TempDir() + path := filepath.Join(dir, "myfile") + if err := os.WriteFile(path, []byte("hello"), 0o644); err != nil { t.Fatal(err) } - if err := Ensure(path); err == nil { - t.Error("expected error when a regular file is in the way of a directory") + Present(path) + if err := resource.Apply(); err == nil { + t.Error("expected Apply to fail when a file exists at the directory path") } } -func TestHaveAbsentNonEmptyDirWithoutPruneFails(t *testing.T) { +func TestPresentAbsentNonEmptyDirWithoutPruneFails(t *testing.T) { resource.ResetRepository() - tmp := t.TempDir() - target := filepath.Join(tmp, "d") - if err := os.MkdirAll(filepath.Join(target, "sub"), 0o755); err != nil { + dir := t.TempDir() + path := filepath.Join(dir, "nonempty") + if err := os.Mkdir(path, 0o755); err != nil { t.Fatal(err) } - - if err := Ensure(target, IsAbsent()); err == nil { - t.Error("expected error removing a non-empty directory without WithPrune()") + if err := os.WriteFile(filepath.Join(path, "file"), []byte("hi"), 0o644); err != nil { + t.Fatal(err) } - if _, err := os.Stat(target); err != nil { - t.Errorf("expected %s to still exist, got %v", target, err) + + Present(path, IsAbsent()) + if err := resource.Apply(); err == nil { + t.Error("expected Apply to fail when removing non-empty directory without prune") } } -func TestHaveAbsentPruneDirectoryRecursive(t *testing.T) { +func TestPresentAbsentPruneDirectoryRecursive(t *testing.T) { resource.ResetRepository() - tmp := t.TempDir() - target := filepath.Join(tmp, "d") - if err := os.MkdirAll(filepath.Join(target, "sub", "deep"), 0o755); err != nil { + dir := t.TempDir() + path := filepath.Join(dir, "pruneme") + if err := os.Mkdir(path, 0o755); err != nil { t.Fatal(err) } - if err := os.WriteFile(filepath.Join(target, "sub", "f.txt"), []byte("x"), 0o644); err != nil { + if err := os.WriteFile(filepath.Join(path, "file"), []byte("hi"), 0o644); err != nil { t.Fatal(err) } - Have(target, IsAbsent(), WithPrune()) + Present(path, IsAbsent(), WithPrune()) if err := resource.Apply(); err != nil { t.Fatalf("Apply failed: %v", err) } - if _, err := os.Stat(target); !os.IsNotExist(err) { - t.Errorf("expected %s to be removed recursively", target) - } - // Idempotent: removing a missing tree is not an error. - d := &Dir{path: target, absent: true, prune: true} - if err := ensureAbsent(d); err != nil { - t.Fatalf("prune on missing tree: %v", err) + if _, err := os.Stat(path); !os.IsNotExist(err) { + t.Errorf("expected %s to be removed recursively", path) } } func TestAbsentPruneDirectoryRecursive(t *testing.T) { resource.ResetRepository() - tmp := t.TempDir() - target := filepath.Join(tmp, "d") - if err := os.MkdirAll(filepath.Join(target, "sub", "deep"), 0o755); err != nil { + dir := t.TempDir() + path := filepath.Join(dir, "pruneme2") + if err := os.Mkdir(path, 0o755); err != nil { t.Fatal(err) } - if err := os.WriteFile(filepath.Join(target, "sub", "f.txt"), []byte("x"), 0o644); err != nil { + if err := os.WriteFile(filepath.Join(path, "file"), []byte("hi"), 0o644); err != nil { t.Fatal(err) } - Absent(target, WithPrune()) + Absent(path, WithPrune()) if err := resource.Apply(); err != nil { t.Fatalf("Apply failed: %v", err) } - if _, err := os.Stat(target); !os.IsNotExist(err) { - t.Errorf("expected %s to be removed recursively", target) - } - // Idempotent: removing a missing tree is not an error. - d := &Dir{path: target, absent: true, prune: true} - if err := ensureAbsent(d); err != nil { - t.Fatalf("prune on missing tree: %v", err) + if _, err := os.Stat(path); !os.IsNotExist(err) { + t.Errorf("expected %s to be removed recursively", path) } } -func TestHaveDirectoryWithSource(t *testing.T) { - t.Run("recursive copy", func(t *testing.T) { - resource.ResetRepository() - tmp := t.TempDir() - src := t.TempDir() - dst := filepath.Join(tmp, "dst") +func TestPresentDirectoryWithSource(t *testing.T) { + resource.ResetRepository() + dir := t.TempDir() + src := filepath.Join(dir, "src") + dst := filepath.Join(dir, "dst") - srcFile := filepath.Join(src, "file.txt") - if err := os.WriteFile(srcFile, []byte("hello"), 0o644); err != nil { - t.Fatal(err) - } - srcSub := filepath.Join(src, "sub") - if err := os.MkdirAll(srcSub, 0o755); err != nil { - t.Fatal(err) - } - srcSubFile := filepath.Join(srcSub, "subfile.txt") - if err := os.WriteFile(srcSubFile, []byte("sub hello"), 0o644); err != nil { - t.Fatal(err) - } + if err := os.Mkdir(src, 0o755); err != nil { + t.Fatal(err) + } + if err := os.Mkdir(filepath.Join(src, "subdir"), 0o755); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(filepath.Join(src, "f1"), []byte("content1"), 0o644); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(filepath.Join(src, "subdir", "f2"), []byte("content2"), 0o644); err != nil { + t.Fatal(err) + } - Have(dst, WithSource(src)) + t.Run("Create", func(t *testing.T) { + resource.ResetRepository() + Present(dst, WithSource(src)) if err := resource.Apply(); err != nil { t.Fatalf("Apply failed: %v", err) } - if data, err := os.ReadFile(filepath.Join(dst, "file.txt")); err != nil || string(data) != "hello" { - t.Errorf("expected 'hello' at %s, got %q err %v", filepath.Join(dst, "file.txt"), string(data), err) + if _, err := os.Stat(filepath.Join(dst, "f1")); err != nil { + t.Errorf("missing file f1: %v", err) } - if data, err := os.ReadFile(filepath.Join(dst, "sub", "subfile.txt")); err != nil || string(data) != "sub hello" { - t.Errorf("expected 'sub hello' at %s, got %q err %v", filepath.Join(dst, "sub", "subfile.txt"), string(data), err) + if _, err := os.Stat(filepath.Join(dst, "subdir", "f2")); err != nil { + t.Errorf("missing file f2: %v", err) } }) - t.Run("pruning", func(t *testing.T) { + t.Run("Prune", func(t *testing.T) { resource.ResetRepository() - tmp := t.TempDir() - src := t.TempDir() - dst := filepath.Join(tmp, "dst") - - srcFile := filepath.Join(src, "file.txt") - if err := os.WriteFile(srcFile, []byte("hello"), 0o644); err != nil { - t.Fatal(err) - } - - if err := os.MkdirAll(dst, 0o755); err != nil { - t.Fatal(err) - } - extra := filepath.Join(dst, "extra.txt") + // Add extra file to dst + extra := filepath.Join(dst, "extra") if err := os.WriteFile(extra, []byte("extra"), 0o644); err != nil { t.Fatal(err) } - extraSub := filepath.Join(dst, "extra-sub") - if err := os.MkdirAll(extraSub, 0o755); err != nil { - t.Fatal(err) - } - Have(dst, WithSource(src), WithPrune()) + Present(dst, WithSource(src), WithPrune()) if err := resource.Apply(); err != nil { t.Fatalf("Apply failed: %v", err) } @@ -203,189 +176,182 @@ func TestHaveDirectoryWithSource(t *testing.T) { if _, err := os.Stat(extra); !os.IsNotExist(err) { t.Errorf("expected %s to be pruned", extra) } - if _, err := os.Stat(extraSub); !os.IsNotExist(err) { - t.Errorf("expected %s to be pruned", extraSub) - } - if data, err := os.ReadFile(filepath.Join(dst, "file.txt")); err != nil || string(data) != "hello" { - t.Errorf("expected 'hello' at %s, got %q err %v", filepath.Join(dst, "file.txt"), string(data), err) - } }) } func TestSourceCopyUsesFileModeDefaultNotDirMode(t *testing.T) { resource.ResetRepository() - tmp := t.TempDir() - src := t.TempDir() - dst := filepath.Join(tmp, "dst") + dir := t.TempDir() + src := filepath.Join(dir, "src") + dst := filepath.Join(dir, "dst") - if err := os.WriteFile(filepath.Join(src, "file.txt"), []byte("hello"), 0o644); err != nil { + if err := os.Mkdir(src, 0o755); err != nil { + t.Fatal(err) + } + f1 := filepath.Join(src, "f1") + if err := os.WriteFile(f1, []byte("hi"), 0o644); err != nil { t.Fatal(err) } - Have(dst, WithSource(src)) + // Use non-default dir mode to prove files don't use it + Present(dst, WithSource(src), WithMode(0o700)) if err := resource.Apply(); err != nil { t.Fatalf("Apply failed: %v", err) } - dirInfo, err := os.Stat(dst) - if err != nil { - t.Fatal(err) - } - if dirInfo.Mode().Perm() != 0o750 { - t.Errorf("expected dir mode 0750, got %v", dirInfo.Mode().Perm()) - } - - fileInfo, err := os.Stat(filepath.Join(dst, "file.txt")) + info, err := os.Stat(filepath.Join(dst, "f1")) if err != nil { t.Fatal(err) } - if fileInfo.Mode().Perm() != 0o640 { - t.Errorf("expected copied file mode 0640 (not the directory's 0750), got %v", fileInfo.Mode().Perm()) + // Default fileMode is 0o640 + if info.Mode().Perm() != 0o640 { + t.Errorf("expected file mode 0o640, got %v", info.Mode().Perm()) } } func TestSourceCopyRespectsExplicitWithFileMode(t *testing.T) { resource.ResetRepository() - tmp := t.TempDir() - src := t.TempDir() - dst := filepath.Join(tmp, "dst") + dir := t.TempDir() + src := filepath.Join(dir, "src") + dst := filepath.Join(dir, "dst") - if err := os.WriteFile(filepath.Join(src, "file.txt"), []byte("hello"), 0o644); err != nil { + if err := os.Mkdir(src, 0o755); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(filepath.Join(src, "f1"), []byte("hi"), 0o644); err != nil { t.Fatal(err) } - Have(dst, WithSource(src), WithMode(0o755), WithFileMode(0o600)) + explicitMode := os.FileMode(0o600) + Present(dst, WithSource(src), WithFileMode(explicitMode)) if err := resource.Apply(); err != nil { t.Fatalf("Apply failed: %v", err) } - dirInfo, err := os.Stat(dst) + info, err := os.Stat(filepath.Join(dst, "f1")) if err != nil { t.Fatal(err) } - if dirInfo.Mode().Perm() != 0o755 { - t.Errorf("expected dir mode 0755, got %v", dirInfo.Mode().Perm()) - } - - fileInfo, err := os.Stat(filepath.Join(dst, "file.txt")) - if err != nil { - t.Fatal(err) - } - if fileInfo.Mode().Perm() != 0o600 { - t.Errorf("expected copied file mode 0600, got %v", fileInfo.Mode().Perm()) + if info.Mode().Perm() != explicitMode { + t.Errorf("expected file mode %v, got %v", explicitMode, info.Mode().Perm()) } } func TestSourceCopyStripsTmplSuffixOnCopiedFile(t *testing.T) { resource.ResetRepository() - tmp := t.TempDir() - src := t.TempDir() - dst := filepath.Join(tmp, "dst") + dir := t.TempDir() + src := filepath.Join(dir, "src") + dst := filepath.Join(dir, "dst") - if err := os.WriteFile(filepath.Join(src, "foo.conf.tmpl"), []byte("hello {{.Param}}"), 0o644); err != nil { + if err := os.Mkdir(src, 0o755); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(filepath.Join(src, "foo.conf.tmpl"), []byte("hello"), 0o644); err != nil { t.Fatal(err) } - Have(dst, WithSource(src)) + Present(dst, WithSource(src)) if err := resource.Apply(); err != nil { t.Fatalf("Apply failed: %v", err) } if _, err := os.Stat(filepath.Join(dst, "foo.conf")); err != nil { - t.Errorf("expected de-suffixed foo.conf to exist: %v", err) + t.Errorf("expected stripped file foo.conf to exist: %v", err) } if _, err := os.Stat(filepath.Join(dst, "foo.conf.tmpl")); !os.IsNotExist(err) { - t.Errorf("expected foo.conf.tmpl to NOT exist on disk") + t.Errorf("expected non-stripped file foo.conf.tmpl to NOT exist") } } func TestSourceCopyWithPruneKeepsTemplatedFile(t *testing.T) { resource.ResetRepository() - tmp := t.TempDir() - src := t.TempDir() - dst := filepath.Join(tmp, "dst") + dir := t.TempDir() + src := filepath.Join(dir, "src") + dst := filepath.Join(dir, "dst") - if err := os.WriteFile(filepath.Join(src, "foo.conf.tmpl"), []byte("hello {{.Param}}"), 0o644); err != nil { + if err := os.Mkdir(src, 0o755); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(filepath.Join(src, "foo.conf.tmpl"), []byte("hello"), 0o644); err != nil { t.Fatal(err) } - // WithPrune reconciles the destination against the source on every - // apply; a de-suffixed templated file must not be pruned just because - // its own name has no direct match in the source tree. - Have(dst, WithSource(src), WithPrune()) + // First apply to create the file + Present(dst, WithSource(src)) + if err := resource.Apply(); err != nil { + t.Fatalf("Apply failed: %v", err) + } + + // Now apply with prune + resource.ResetRepository() + Present(dst, WithSource(src), WithPrune()) if err := resource.Apply(); err != nil { t.Fatalf("Apply failed: %v", err) } if _, err := os.Stat(filepath.Join(dst, "foo.conf")); err != nil { - t.Errorf("expected foo.conf to survive pruning, got %v", err) + t.Errorf("expected foo.conf to be kept during pruning: %v", err) } } func TestSourceCopyParamMatchesSingleFilePath(t *testing.T) { resource.ResetRepository() - tmp := t.TempDir() - src := t.TempDir() - dst := filepath.Join(tmp, "dst") - sourcePath := filepath.Join(src, "foo.conf.tmpl") - if err := os.WriteFile(sourcePath, []byte("{{.Param}}"), 0o644); err != nil { + dir := t.TempDir() + src := filepath.Join(dir, "src") + dst := filepath.Join(dir, "dst") + + if err := os.Mkdir(src, 0o755); err != nil { + t.Fatal(err) + } + f1 := filepath.Join(src, "foo.conf.tmpl") + if err := os.WriteFile(f1, []byte("path is {{.Param}}"), 0o644); err != nil { t.Fatal(err) } - Have(dst, WithSource(src)) + Present(dst, WithSource(src)) if err := resource.Apply(); err != nil { t.Fatalf("Apply failed: %v", err) } - viaDir, err := os.ReadFile(filepath.Join(dst, "foo.conf")) - if err != nil { - t.Fatal(err) - } - singleTarget := filepath.Join(tmp, "single.conf") - if err := file.Ensure(singleTarget, WithSource(sourcePath)); err != nil { - t.Fatal(err) - } - viaFile, err := os.ReadFile(singleTarget) + got, err := os.ReadFile(filepath.Join(dst, "foo.conf")) if err != nil { t.Fatal(err) } - - if string(viaDir) != string(viaFile) { - t.Errorf("expected identical .Param rendering via both paths, dir-copy=%q file-direct=%q", viaDir, viaFile) + expected := "path is " + f1 + if string(got) != expected { + t.Errorf("expected %q, got %q", expected, string(got)) } } func TestSourceCopyRecreatesSymlinkNotContent(t *testing.T) { resource.ResetRepository() - tmp := t.TempDir() - src := t.TempDir() - dst := filepath.Join(tmp, "dst") + dir := t.TempDir() + src := filepath.Join(dir, "src") + dst := filepath.Join(dir, "dst") - if err := os.WriteFile(filepath.Join(src, "target.txt"), []byte("hello"), 0o644); err != nil { + if err := os.Mkdir(src, 0o755); err != nil { + t.Fatal(err) + } + target := filepath.Join(src, "realfile") + if err := os.WriteFile(target, []byte("hi"), 0o644); err != nil { t.Fatal(err) } - if err := os.Symlink("target.txt", filepath.Join(src, "link.txt")); err != nil { + linkPath := filepath.Join(src, "link") + if err := os.Symlink(target, linkPath); err != nil { t.Fatal(err) } - Have(dst, WithSource(src)) + Present(dst, WithSource(src)) if err := resource.Apply(); err != nil { t.Fatalf("Apply failed: %v", err) } - linkPath := filepath.Join(dst, "link.txt") - info, err := os.Lstat(linkPath) + dstLink := filepath.Join(dst, "link") + info, err := os.Lstat(dstLink) if err != nil { - t.Fatalf("lstat: %v", err) + t.Fatal(err) } if info.Mode()&os.ModeSymlink == 0 { - t.Fatalf("expected %s to be a symlink, got mode %v", linkPath, info.Mode()) - } - got, err := os.Readlink(linkPath) - if err != nil { - t.Fatalf("readlink: %v", err) - } - if got != "target.txt" { - t.Errorf("expected symlink target %q, got %q", "target.txt", got) + t.Errorf("expected %s to be a symlink", dstLink) } } diff --git a/internal/resource/file/file.go b/internal/resource/file/file.go index 402b348..9879e27 100644 --- a/internal/resource/file/file.go +++ b/internal/resource/file/file.go @@ -10,8 +10,8 @@ import ( "strings" "text/template" - "codeberg.org/snonux/gonf/internal/resource" opt "codeberg.org/snonux/gonf/api/option" + "codeberg.org/snonux/gonf/internal/resource" ) type File struct { @@ -218,7 +218,7 @@ func Ensure(path string, opts ...opt.Option) error { return f.apply() } -func Have(path string, opts ...opt.Option) resource.Resource { +func Present(path string, opts ...opt.Option) resource.Resource { f, err := build(path, opts...) if err != nil { log.Fatalf("failed to apply file resource %s: %v", path, err) @@ -232,5 +232,5 @@ func Have(path string, opts ...opt.Option) resource.Resource { func Absent(path string, opts ...opt.Option) resource.Resource { opts = append(opts, opt.IsAbsent()) - return Have(path, opts...) + return Present(path, opts...) } diff --git a/internal/resource/file/file_test.go b/internal/resource/file/file_test.go index e84ab13..7f9fedd 100644 --- a/internal/resource/file/file_test.go +++ b/internal/resource/file/file_test.go @@ -6,8 +6,8 @@ import ( "strings" "testing" - "codeberg.org/snonux/gonf/internal/resource" . "codeberg.org/snonux/gonf/api/option" + "codeberg.org/snonux/gonf/internal/resource" ) func TestGetChecksum(t *testing.T) { @@ -104,12 +104,12 @@ func TestUpdateFromTmpChecksumUnchanged(t *testing.T) { } } -func TestHaveStringCreateNewFile(t *testing.T) { +func TestPresentStringCreateNewFile(t *testing.T) { resource.ResetRepository() dir := t.TempDir() path := filepath.Join(dir, "new.txt") - Have(path, WithContent("hello world")) + Present(path, WithContent("hello world")) if err := resource.Apply(); err != nil { t.Fatalf("Apply failed: %v", err) } @@ -123,13 +123,13 @@ func TestHaveStringCreateNewFile(t *testing.T) { } } -func TestHaveMode(t *testing.T) { +func TestPresentMode(t *testing.T) { resource.ResetRepository() dir := t.TempDir() path := filepath.Join(dir, "mode.txt") mode := os.FileMode(0o600) - Have(path, WithContent("mode test"), WithMode(mode)) + Present(path, WithContent("mode test"), WithMode(mode)) if err := resource.Apply(); err != nil { t.Fatalf("Apply failed: %v", err) } @@ -143,13 +143,13 @@ func TestHaveMode(t *testing.T) { } } -func TestHaveSourceFile(t *testing.T) { +func TestPresentSourceFile(t *testing.T) { resource.ResetRepository() dir := t.TempDir() sourcePath := filepath.Join("..", "..", "..", "assets", "testfiles", "test.txt") targetPath := filepath.Join(dir, "target.txt") - Have(targetPath, WithSource(sourcePath)) + Present(targetPath, WithSource(sourcePath)) if err := resource.Apply(); err != nil { t.Fatalf("Apply failed: %v", err) } @@ -164,13 +164,13 @@ func TestHaveSourceFile(t *testing.T) { } } -func TestHaveTemplateFile(t *testing.T) { +func TestPresentTemplateFile(t *testing.T) { resource.ResetRepository() dir := t.TempDir() sourcePath := filepath.Join("..", "..", "..", "assets", "testfiles", "test.tmpl") targetPath := filepath.Join(dir, "target.conf") - Have(targetPath, WithSource(sourcePath)) + Present(targetPath, WithSource(sourcePath)) if err := resource.Apply(); err != nil { t.Fatalf("Apply failed: %v", err) } @@ -186,7 +186,7 @@ func TestHaveTemplateFile(t *testing.T) { } } -func TestHaveAbsent(t *testing.T) { +func TestPresentAbsent(t *testing.T) { resource.ResetRepository() dir := t.TempDir() path := filepath.Join(dir, "gone.txt") @@ -194,7 +194,7 @@ func TestHaveAbsent(t *testing.T) { t.Fatal(err) } - Have(path, IsAbsent()) + Present(path, IsAbsent()) if err := resource.Apply(); err != nil { t.Fatalf("Apply failed: %v", err) } diff --git a/internal/resource/link/link.go b/internal/resource/link/link.go index 5a1824b..2bd8c66 100644 --- a/internal/resource/link/link.go +++ b/internal/resource/link/link.go @@ -5,8 +5,8 @@ import ( "log" "os" - "codeberg.org/snonux/gonf/internal/resource" opt "codeberg.org/snonux/gonf/api/option" + "codeberg.org/snonux/gonf/internal/resource" ) type kind int @@ -86,7 +86,7 @@ func Ensure(path string, opts ...opt.Option) error { return build(path, opts...).apply() } -func Have(path string, opts ...opt.Option) resource.Resource { +func Present(path string, opts ...opt.Option) resource.Resource { l := build(path, opts...) l.resource = resource.Register(l.resourceType(), l.path, resource.ApplierFunc(func() error { return l.apply() })) @@ -96,7 +96,7 @@ func Have(path string, opts ...opt.Option) resource.Resource { func Absent(path string, opts ...opt.Option) resource.Resource { opts = append(opts, opt.IsAbsent()) - return Have(path, opts...) + return Present(path, opts...) } func ensureAbsent(path string) error { diff --git a/internal/resource/link/link_test.go b/internal/resource/link/link_test.go index c8d9789..3012435 100644 --- a/internal/resource/link/link_test.go +++ b/internal/resource/link/link_test.go @@ -9,238 +9,220 @@ import ( . "codeberg.org/snonux/gonf/api/option" ) -func TestHaveSymlinkCreateAndIdempotent(t *testing.T) { +func TestPresentSymlinkCreateAndIdempotent(t *testing.T) { resource.ResetRepository() dir := t.TempDir() - target := filepath.Join(dir, "target.txt") - link := filepath.Join(dir, "link.txt") - if err := os.WriteFile(target, []byte("t"), 0o644); err != nil { + target := filepath.Join(dir, "target") + if err := os.WriteFile(target, []byte("hi"), 0o644); err != nil { t.Fatal(err) } + path := filepath.Join(dir, "link") - Have(link, WithSymlink(target)) + Present(path, WithSymlink(target)) if err := resource.Apply(); err != nil { t.Fatalf("Apply failed: %v", err) } - got, err := os.Readlink(link) + + got, err := os.Readlink(path) if err != nil { - t.Fatalf("readlink: %v", err) + t.Fatal(err) } if got != target { - t.Errorf("expected link -> %s, got %s", target, got) + t.Errorf("expected link to point to %s, got %s", target, got) } - // Re-applying the same link should be a no-op (tested directly to avoid the - // one-per-process resource registry rejecting a duplicate registration). - l := &Link{path: link, kind: symlinkKind, target: target} - if err := ensureSymlink(l); err != nil { - t.Fatalf("idempotent apply: %v", err) + // Idempotency + if err := resource.Apply(); err != nil { + t.Fatalf("second Apply failed: %v", err) } } -func TestHaveSymlinkRepoints(t *testing.T) { +func TestPresentSymlinkRepoints(t *testing.T) { resource.ResetRepository() dir := t.TempDir() - old := filepath.Join(dir, "old.txt") - newT := filepath.Join(dir, "new.txt") - link := filepath.Join(dir, "link") - for _, p := range []string{old, newT} { - if err := os.WriteFile(p, []byte("x"), 0o644); err != nil { - t.Fatal(err) - } + path := filepath.Join(dir, "link") + t1 := filepath.Join(dir, "target1") + t2 := filepath.Join(dir, "target2") + if err := os.WriteFile(t1, []byte("1"), 0o644); err != nil { + t.Fatal(err) } - if err := os.Symlink(old, link); err != nil { + if err := os.WriteFile(t2, []byte("2"), 0o644); err != nil { t.Fatal(err) } - Have(link, WithSymlink(newT)) + Present(path, WithSymlink(t1)) if err := resource.Apply(); err != nil { - t.Fatalf("Apply failed: %v", err) + t.Fatalf("Apply 1 failed: %v", err) + } + + // Change target + resource.ResetRepository() + Present(path, WithSymlink(t2)) + if err := resource.Apply(); err != nil { + t.Fatalf("Apply 2 failed: %v", err) } - got, err := os.Readlink(link) + + got, err := os.Readlink(path) if err != nil { t.Fatal(err) } - if got != newT { - t.Errorf("expected repoint to %s, got %s", newT, got) + if got != t2 { + t.Errorf("expected link to repoint to %s, got %s", t2, got) } } -func TestHaveSymlinkMovesRealFileAside(t *testing.T) { +func TestPresentSymlinkMovesRealFileAside(t *testing.T) { resource.ResetRepository() dir := t.TempDir() - target := filepath.Join(dir, "target.txt") - link := filepath.Join(dir, "real") - if err := os.WriteFile(target, []byte("t"), 0o644); err != nil { + path := filepath.Join(dir, "link") + if err := os.WriteFile(path, []byte("real file"), 0o644); err != nil { t.Fatal(err) } - if err := os.WriteFile(link, []byte("original"), 0o644); err != nil { + target := filepath.Join(dir, "target") + if err := os.WriteFile(target, []byte("target content"), 0o644); err != nil { t.Fatal(err) } - Have(link, WithSymlink(target)) + Present(path, WithSymlink(target)) if err := resource.Apply(); err != nil { t.Fatalf("Apply failed: %v", err) } - got, err := os.Readlink(link) + if _, err := os.Stat(path + ".old"); os.IsNotExist(err) { + t.Errorf("expected real file to be moved to %s.old", path) + } + got, err := os.Readlink(path) if err != nil { - t.Fatalf("expected %s to be a symlink: %v", link, err) + t.Fatal(err) } if got != target { - t.Errorf("expected link -> %s, got %s", target, got) - } - if data, err := os.ReadFile(link + ".old"); err != nil || string(data) != "original" { - t.Errorf("expected original content preserved in %s.old, got %q err %v", link, string(data), err) + t.Errorf("expected link to point to %s, got %s", target, got) } } -func TestHaveHardlinkCreateAndIdempotent(t *testing.T) { +func TestPresentHardlinkCreateAndIdempotent(t *testing.T) { resource.ResetRepository() dir := t.TempDir() - target := filepath.Join(dir, "target.txt") - link := filepath.Join(dir, "link.txt") - if err := os.WriteFile(target, []byte("payload"), 0o644); err != nil { + target := filepath.Join(dir, "target") + if err := os.WriteFile(target, []byte("hi"), 0o644); err != nil { t.Fatal(err) } + path := filepath.Join(dir, "link") - Have(link, WithHardlink(target)) + Present(path, WithHardlink(target)) if err := resource.Apply(); err != nil { t.Fatalf("Apply failed: %v", err) } - ti, err := os.Stat(target) + infoLink, err := os.Stat(path) if err != nil { t.Fatal(err) } - li, err := os.Stat(link) + if infoLink.Mode()&os.ModeSymlink != 0 { + t.Error("expected hardlink, but got symlink") + } + // Verify they share the same inode (via a helper or by checking if we can see it's not a symlink and is a file) + // Since sameInode is internal to the package, we can just check that we can read it. + got, err := os.ReadFile(path) if err != nil { t.Fatal(err) } - if !sameInode(ti, li) { - t.Errorf("expected %s and %s to share an inode", link, target) + if string(got) != "hi" { + t.Errorf("expected content 'hi', got %q", string(got)) } - // Re-applying the same link should be a no-op (direct call to avoid the - // one-per-process resource registry rejecting a duplicate registration). - l := &Link{path: link, kind: hardlinkKind, target: target} - if err := ensureHardlink(l); err != nil { - t.Fatalf("idempotent apply: %v", err) + // Idempotency + if err := resource.Apply(); err != nil { + t.Fatalf("second Apply failed: %v", err) } } -func TestHaveHardlinkMovesRealFileAside(t *testing.T) { +func TestPresentHardlinkMovesRealFileAside(t *testing.T) { resource.ResetRepository() dir := t.TempDir() - target := filepath.Join(dir, "target.txt") - link := filepath.Join(dir, "real") - if err := os.WriteFile(target, []byte("payload"), 0o644); err != nil { + path := filepath.Join(dir, "link") + if err := os.WriteFile(path, []byte("real file"), 0o644); err != nil { t.Fatal(err) } - if err := os.WriteFile(link, []byte("original"), 0o644); err != nil { + target := filepath.Join(dir, "target") + if err := os.WriteFile(target, []byte("target content"), 0o644); err != nil { t.Fatal(err) } - Have(link, WithHardlink(target)) + Present(path, WithHardlink(target)) if err := resource.Apply(); err != nil { t.Fatalf("Apply failed: %v", err) } - ti, err := os.Stat(target) - if err != nil { - t.Fatal(err) - } - li, err := os.Stat(link) - if err != nil { - t.Fatal(err) - } - if !sameInode(ti, li) { - t.Errorf("expected %s to be hardlinked to %s", link, target) - } - if data, err := os.ReadFile(link + ".old"); err != nil || string(data) != "original" { - t.Errorf("expected original content preserved in %s.old, got %q err %v", link, string(data), err) + if _, err := os.Stat(path + ".old"); os.IsNotExist(err) { + t.Errorf("expected real file to be moved to %s.old", path) } } -func TestHaveHardlinkMissingTarget(t *testing.T) { +func TestPresentHardlinkMissingTarget(t *testing.T) { resource.ResetRepository() dir := t.TempDir() - link := filepath.Join(dir, "link") - if err := Ensure(link, WithHardlink(filepath.Join(dir, "nope"))); err == nil { - t.Error("expected error when hardlink target does not exist") + path := filepath.Join(dir, "link") + target := filepath.Join(dir, "nonexistent") + + Present(path, WithHardlink(target)) + if err := resource.Apply(); err == nil { + t.Error("expected Apply to fail when hardlink target is missing") } } -func TestHaveAbsentSymlink(t *testing.T) { +func TestPresentAbsentSymlink(t *testing.T) { resource.ResetRepository() dir := t.TempDir() - target := filepath.Join(dir, "target.txt") - link := filepath.Join(dir, "link.txt") - if err := os.WriteFile(target, []byte("t"), 0o644); err != nil { + path := filepath.Join(dir, "link") + target := filepath.Join(dir, "target") + if err := os.WriteFile(target, []byte("hi"), 0o644); err != nil { t.Fatal(err) } - if err := os.Symlink(target, link); err != nil { + if err := os.Symlink(target, path); err != nil { t.Fatal(err) } - Have(link, IsAbsent()) + Present(path, IsAbsent()) if err := resource.Apply(); err != nil { t.Fatalf("Apply failed: %v", err) } - if _, err := os.Lstat(link); !os.IsNotExist(err) { - t.Errorf("expected %s to be removed", link) - } - // Idempotent: removing a missing link is not an error (direct call to - // avoid duplicate registration in the one-per-process registry). - if err := ensureAbsent(link); err != nil { - t.Fatalf("absent on missing link: %v", err) + if _, err := os.Stat(path); !os.IsNotExist(err) { + t.Errorf("expected symlink %s to be removed", path) } } func TestAbsentSymlink(t *testing.T) { resource.ResetRepository() dir := t.TempDir() - target := filepath.Join(dir, "target.txt") - link := filepath.Join(dir, "link.txt") - if err := os.WriteFile(target, []byte("t"), 0o644); err != nil { + path := filepath.Join(dir, "link") + target := filepath.Join(dir, "target") + if err := os.WriteFile(target, []byte("hi"), 0o644); err != nil { t.Fatal(err) } - if err := os.Symlink(target, link); err != nil { + if err := os.Symlink(target, path); err != nil { t.Fatal(err) } - Absent(link) + Absent(path) if err := resource.Apply(); err != nil { t.Fatalf("Apply failed: %v", err) } - if _, err := os.Lstat(link); !os.IsNotExist(err) { - t.Errorf("expected %s to be removed", link) - } - - // Idempotent: removing a missing link is not an error (direct call to - // avoid duplicate registration in the one-per-process registry). - if err := ensureAbsent(link); err != nil { - t.Fatalf("absent on missing link: %v", err) - } -} -func TestHaveAbsentWithoutKindRegistersGenericLink(t *testing.T) { - resource.ResetRepository() - dir := t.TempDir() - path := filepath.Join(dir, "whatever") - - l := build(path, IsAbsent()) - if got := l.resourceType(); got != "Link" { - t.Errorf("expected generic resource type %q, got %q", "Link", got) + if _, err := os.Stat(path); !os.IsNotExist(err) { + t.Errorf("expected symlink %s to be removed", path) } } func TestBuildRequiresKindOrAbsent(t *testing.T) { resource.ResetRepository() dir := t.TempDir() - path := filepath.Join(dir, "nope") - if err := Ensure(path); err == nil { - t.Error("expected error when neither IsSymlink, IsHardlink, nor IsAbsent is set") + path := filepath.Join(dir, "link") + + // Call Present without specifying symlink, hardlink, or absent + Present(path) + if err := resource.Apply(); err == nil { + t.Error("expected Apply to fail when neither kind nor absent is specified") } } -- cgit v1.2.3