286 lines
8 KiB
Go
286 lines
8 KiB
Go
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()
|
|
}
|
|
}
|
|
|
|
// TestSyncSummary verifies a run reports what it changed: the first run
|
|
// fetches, a repeat run finds everything unchanged, and a removed upstream
|
|
// file is counted as pruned.
|
|
func TestSyncSummary(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,
|
|
URLs: []string{srv.URL + "/el9"},
|
|
Destination: dest,
|
|
Workers: 2,
|
|
Prune: true,
|
|
}
|
|
|
|
// The first run populates the destination, so everything it fetched is a
|
|
// change.
|
|
summary, err := Sync(context.Background(), opts)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if summary.Repositories != 1 || summary.Failed != 0 {
|
|
t.Errorf("repositories = %d, failed = %d, want 1 and 0", summary.Repositories, summary.Failed)
|
|
}
|
|
if summary.Fetched == 0 {
|
|
t.Error("first run reported nothing fetched")
|
|
}
|
|
if summary.FetchedBytes == 0 {
|
|
t.Error("first run reported no bytes fetched")
|
|
}
|
|
if !summary.Changed() {
|
|
t.Error("first run did not report a change")
|
|
}
|
|
|
|
// Repeating the run transfers nothing, which is what a hook keys off to
|
|
// stay idle.
|
|
summary, err = Sync(context.Background(), opts)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if summary.Fetched != 0 || summary.Pruned != 0 {
|
|
t.Errorf("repeat run fetched %d and pruned %d, want 0 and 0", summary.Fetched, summary.Pruned)
|
|
}
|
|
if summary.Unchanged == 0 {
|
|
t.Error("repeat run reported nothing unchanged")
|
|
}
|
|
if summary.Changed() {
|
|
t.Error("repeat run reported a change")
|
|
}
|
|
|
|
// A local file the upstream never served is pruned, which counts as a
|
|
// change even though nothing was fetched.
|
|
stale := filepath.Join(dest, "el9", "Packages", "stale.rpm")
|
|
if err := os.WriteFile(stale, []byte("junk"), 0644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
summary, err = Sync(context.Background(), opts)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if summary.Pruned != 1 {
|
|
t.Errorf("pruned = %d, want 1", summary.Pruned)
|
|
}
|
|
if !summary.Changed() {
|
|
t.Error("pruning run did not report a change")
|
|
}
|
|
}
|
|
|
|
// TestSyncSummaryDryRun verifies a dry run reports its planned work without
|
|
// ever claiming the mirror changed.
|
|
func TestSyncSummaryDryRun(t *testing.T) {
|
|
www := t.TempDir()
|
|
testrepos.BuildRPMRepo(t, filepath.Join(www, "el9"))
|
|
srv := testrepos.ServeDir(t, www)
|
|
|
|
opts := &Options{
|
|
Type: RepoRPM,
|
|
URLs: []string{srv.URL + "/el9"},
|
|
Destination: t.TempDir(),
|
|
Workers: 2,
|
|
DryRun: true,
|
|
}
|
|
summary, err := Sync(context.Background(), opts)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if summary.Planned == 0 {
|
|
t.Error("dry run planned nothing")
|
|
}
|
|
if summary.PlannedBytes == 0 {
|
|
t.Error("dry run planned no bytes")
|
|
}
|
|
// A dry run still fetches metadata to plan against, but none of it
|
|
// reaches the destination, so the run must never read as a change.
|
|
if summary.Changed() {
|
|
t.Error("dry run reported a change")
|
|
}
|
|
}
|
|
|
|
// TestSyncSummaryFailure verifies a run that fails still reports the totals
|
|
// it accumulated, so a caller can tell a failed run apart from an idle one.
|
|
func TestSyncSummaryFailure(t *testing.T) {
|
|
www := t.TempDir()
|
|
srv := testrepos.ServeDir(t, www)
|
|
|
|
opts := &Options{
|
|
Type: RepoRPM,
|
|
URLs: []string{srv.URL + "/missing"},
|
|
Destination: t.TempDir(),
|
|
Workers: 2,
|
|
}
|
|
summary, err := Sync(context.Background(), opts)
|
|
if err == nil {
|
|
t.Fatal("expected a failure syncing a repository that does not exist")
|
|
}
|
|
if summary == nil {
|
|
t.Fatal("no summary returned for a failed run")
|
|
}
|
|
if summary.Repositories != 1 || summary.Failed != 1 {
|
|
t.Errorf("repositories = %d, failed = %d, want 1 and 1", summary.Repositories, summary.Failed)
|
|
}
|
|
if summary.Changed() {
|
|
t.Error("a run that transferred nothing reported a change")
|
|
}
|
|
}
|