Skip to content
quarry Artifact [9971f436e5]
Login

Artifact [9971f436e5]

Artifact 9971f436e5d5ad5ce9847e8f70c74ad1c3a8f61bc078b3a32881dedf14b4933b:


//go:build !js && !wasip1

package obsduck

import (
	"path/filepath"
	"testing"
	"time"

	"craftdesign.group/quarry/internal/obs"
)

func openSink(t *testing.T) *Sink {
	t.Helper()
	s, err := Open(filepath.Join(t.TempDir(), "ops.duckdb"), 0)
	if err != nil {
		t.Skipf("duckdb unavailable: %v", err)
	}
	t.Cleanup(func() { s.Close() })
	return s
}

func rows(t *testing.T, s *Sink, table string) int {
	t.Helper()
	var n int
	if err := s.db.QueryRow(`SELECT count(*) FROM ` + table).Scan(&n); err != nil {
		t.Fatal(err)
	}
	return n
}

// Nothing pruned the store, so it grew without bound — on a busy forge
// the thing watching for trouble becomes the trouble. The window is the
// operator's to set, including setting it to for ever.
func TestPruneDropsOnlyWhatIsOlderThanTheWindow(t *testing.T) {
	s := openSink(t)
	now := time.Now()
	s.Render(obs.Render{At: now.Add(-30 * 24 * time.Hour), Path: "GET /old"})
	s.Render(obs.Render{At: now.Add(-time.Hour), Path: "GET /new"})
	s.Subprocess(obs.Subprocess{At: now.Add(-30 * 24 * time.Hour), Verb: "ls"})
	s.Subprocess(obs.Subprocess{At: now.Add(-time.Hour), Verb: "ls"})
	s.Flush()
	if got := rows(t, s, "render"); got != 2 {
		t.Fatalf("before prune: %d renders, want 2", got)
	}

	n, err := s.Prune(14 * 24 * time.Hour)
	if err != nil {
		t.Fatal(err)
	}
	if n != 2 {
		t.Errorf("pruned %d rows, want 2 (one render, one subprocess)", n)
	}
	if got := rows(t, s, "render"); got != 1 {
		t.Errorf("after prune: %d renders, want 1", got)
	}
	if got := rows(t, s, "subprocess"); got != 1 {
		t.Errorf("after prune: %d subprocesses, want 1", got)
	}
	var path string
	if err := s.db.QueryRow(`SELECT path FROM render`).Scan(&path); err != nil {
		t.Fatal(err)
	}
	if path != "GET /new" {
		t.Errorf("the surviving render is %q — prune took the wrong end", path)
	}
}

// Keeping everything has to stay available, and has to be free: an
// operator who switched retention off should not be paying for a DELETE
// that can never match.
func TestPruneKeepsEverythingWhenTheWindowIsZero(t *testing.T) {
	s := openSink(t)
	s.Render(obs.Render{At: time.Now().Add(-365 * 24 * time.Hour), Path: "GET /ancient"})
	s.Flush()
	n, err := s.Prune(0)
	if err != nil {
		t.Fatal(err)
	}
	if n != 0 {
		t.Errorf("pruned %d rows with retention off", n)
	}
	if got := rows(t, s, "render"); got != 1 {
		t.Errorf("%d renders survived retention-off, want 1", got)
	}
}

// Prune flushes first: rows still buffered are the newest and would
// survive anyway, but the count handed to an operator must not be a
// guess about what was on disk at the time.
func TestPruneFlushesBeforeCounting(t *testing.T) {
	s := openSink(t)
	s.Render(obs.Render{At: time.Now(), Path: "GET /buffered"})
	if got := rows(t, s, "render"); got != 0 {
		t.Fatalf("precondition: %d rows already written, wanted them still buffered", got)
	}
	if _, err := s.Prune(time.Hour); err != nil {
		t.Fatal(err)
	}
	if got := rows(t, s, "render"); got != 1 {
		t.Errorf("%d renders on disk after prune, want the buffered one flushed", got)
	}
}