package mirror import ( "context" "encoding/json" "io/fs" "os" "path/filepath" "testing" "time" "github.com/grmrgecko/repo-sync/fetch" "github.com/grmrgecko/repo-sync/internal/testrepos" ) // countFiles returns the number of regular files under root. func countFiles(t *testing.T, root string) int { t.Helper() count := 0 err := filepath.WalkDir(root, func(name string, d fs.DirEntry, err error) error { if err != nil { return err } if !d.IsDir() { count++ } return nil }) if err != nil && !os.IsNotExist(err) { t.Fatal(err) } return count } // TestDryRun verifies a dry run leaves the destination unchanged: no // packages downloaded, no metadata published, and no files pruned. func TestDryRun(t *testing.T) { www := t.TempDir() testrepos.BuildRPMRepo(t, filepath.Join(www, "el9")) srv := testrepos.ServeDir(t, www) // A dry run against an empty destination writes no files at all. dest := t.TempDir() opts := &Options{Type: RepoRPM, Destination: dest, Workers: 2, DryRun: true, Prune: true} if err := syncOne(context.Background(), srv.URL+"/el9", opts); err != nil { t.Fatal(err) } if n := countFiles(t, dest); n != 0 { t.Errorf("dry run left %d files in the destination", n) } // A real run works, and a later dry run with pruning keeps stale files. opts.DryRun = false if err := syncOne(context.Background(), srv.URL+"/el9", opts); err != nil { t.Fatal(err) } stale := filepath.Join(dest, "el9", "Packages", "stale.rpm") if err := os.WriteFile(stale, []byte("junk"), 0644); err != nil { t.Fatal(err) } opts.DryRun = true if err := syncOne(context.Background(), srv.URL+"/el9", opts); err != nil { t.Fatal(err) } if _, err := os.Stat(stale); err != nil { t.Error("dry run pruned a file:", err) } } // TestPruneGrace verifies stale files wait out the grace period across // runs before being removed. func TestPruneGrace(t *testing.T) { www := t.TempDir() testrepos.BuildRPMRepo(t, filepath.Join(www, "el9")) srv := testrepos.ServeDir(t, www) dest := t.TempDir() opts := &Options{Type: RepoRPM, Destination: dest, Workers: 2, Prune: true, PruneGrace: time.Hour} if err := syncOne(context.Background(), srv.URL+"/el9", opts); err != nil { t.Fatal(err) } local := filepath.Join(dest, "el9") stale := filepath.Join(local, "Packages", "stale.rpm") if err := os.WriteFile(stale, []byte("junk"), 0644); err != nil { t.Fatal(err) } // The first run only starts the grace period. if err := syncOne(context.Background(), srv.URL+"/el9", opts); err != nil { t.Fatal(err) } if _, err := os.Stat(stale); err != nil { t.Fatal("file pruned before its grace period expired:", err) } statePath := filepath.Join(local, fetch.PruneStateName) if _, err := os.Stat(statePath); err != nil { t.Fatal("prune state not recorded:", err) } // A second run within the grace period still keeps the file. if err := syncOne(context.Background(), srv.URL+"/el9", opts); err != nil { t.Fatal(err) } if _, err := os.Stat(stale); err != nil { t.Fatal("file pruned within its grace period:", err) } // Age the recorded timestamp past the grace period; the next run // prunes the file and clears the bookkeeping. data, err := os.ReadFile(statePath) if err != nil { t.Fatal(err) } state := map[string]time.Time{} if err := json.Unmarshal(data, &state); err != nil { t.Fatal(err) } for rel := range state { state[rel] = time.Now().Add(-2 * time.Hour) } data, err = json.Marshal(state) if err != nil { t.Fatal(err) } if err := os.WriteFile(statePath, data, 0644); err != nil { t.Fatal(err) } if err := syncOne(context.Background(), srv.URL+"/el9", opts); err != nil { t.Fatal(err) } if _, err := os.Stat(stale); err == nil { t.Error("stale file survived past its grace period") } if _, err := os.Stat(statePath); err == nil { t.Error("prune state lingered after all grace periods resolved") } } // TestLockDestination verifies a held destination lock blocks a second run // and is released on close. func TestLockDestination(t *testing.T) { dest := t.TempDir() lock, err := lockDestination(dest) if err != nil { t.Fatal(err) } if _, err := lockDestination(dest); err == nil { t.Error("second lock unexpectedly succeeded") } lock.Close() lock, err = lockDestination(dest) if err != nil { t.Error("lock not released on close:", err) } else { lock.Close() } }