summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorPaul Buetow <paul@buetow.org>2026-04-14 10:05:43 +0300
committerPaul Buetow <paul@buetow.org>2026-04-14 10:05:43 +0300
commit806ff16a0ad70ae2883a666bdc4158ba20ca4d4f (patch)
tree69be449899e449e50a591138f46a32d53207b4c6
parent4d9004ab31a6064af741fbb73758bd8844964c6f (diff)
test(x2): codify CLI backward-compatibility contract
- Add internal/cli tests for stable entry points: -version/--version, file-based report (-stats-dir), import/query, test subcommand, -daemon/--daemon validation, and subcommand recognition. - Assert RegisterReportFlags keeps required flag names and defaults (CLI/HTTP). - Run integration import/export against a temp SQLite file instead of fixtures/test_import.db to avoid flaky readonly errors under parallel tests. Made-with: Cursor
-rw-r--r--internal/cli/cli_test.go149
-rw-r--r--internal/goprecords/integration_test_runner.go16
-rw-r--r--internal/goprecords/report_test.go37
3 files changed, 199 insertions, 3 deletions
diff --git a/internal/cli/cli_test.go b/internal/cli/cli_test.go
new file mode 100644
index 0000000..9fa20e5
--- /dev/null
+++ b/internal/cli/cli_test.go
@@ -0,0 +1,149 @@
+package cli
+
+import (
+ "bytes"
+ "io"
+ "os"
+ "path/filepath"
+ "strings"
+ "testing"
+
+ "codeberg.org/snonux/goprecords/internal/version"
+)
+
+func moduleRoot(t *testing.T) string {
+ t.Helper()
+ dir, err := os.Getwd()
+ if err != nil {
+ t.Fatal(err)
+ }
+ for {
+ if _, err := os.Stat(filepath.Join(dir, "go.mod")); err == nil {
+ return dir
+ }
+ parent := filepath.Dir(dir)
+ if parent == dir {
+ t.Fatal("go.mod not found from test working directory")
+ }
+ dir = parent
+ }
+}
+
+func captureStdout(t *testing.T, fn func()) string {
+ t.Helper()
+ old := os.Stdout
+ r, w, err := os.Pipe()
+ if err != nil {
+ t.Fatal(err)
+ }
+ os.Stdout = w
+ fn()
+ if err := w.Close(); err != nil {
+ os.Stdout = old
+ t.Fatal(err)
+ }
+ os.Stdout = old
+ var buf bytes.Buffer
+ if _, err := io.Copy(&buf, r); err != nil {
+ t.Fatal(err)
+ }
+ if err := r.Close(); err != nil {
+ t.Fatal(err)
+ }
+ return buf.String()
+}
+
+func TestStableVersionFlags(t *testing.T) {
+ for _, arg := range []string{"-version", "--version"} {
+ arg := arg
+ t.Run(arg, func(t *testing.T) {
+ out := captureStdout(t, func() {
+ if err := Execute([]string{arg}); err != nil {
+ t.Fatalf("Execute: %v", err)
+ }
+ })
+ if !strings.Contains(out, version.Version) {
+ t.Fatalf("stdout %q should contain version %q", out, version.Version)
+ }
+ })
+ }
+}
+
+func TestStableReportFromFilesRequiresStatsDir(t *testing.T) {
+ err := Execute(nil)
+ if err == nil {
+ t.Fatal("expected error")
+ }
+ if !strings.Contains(err.Error(), "stats-dir") {
+ t.Fatalf("expected stats-dir in error, got %v", err)
+ }
+ err = Execute([]string{})
+ if err == nil {
+ t.Fatal("expected error")
+ }
+ if !strings.Contains(err.Error(), "stats-dir") {
+ t.Fatalf("expected stats-dir in error, got %v", err)
+ }
+}
+
+func TestStableReportFromFilesWithFixtures(t *testing.T) {
+ root := moduleRoot(t)
+ fixtures := filepath.Join(root, "fixtures")
+ out := captureStdout(t, func() {
+ if err := Execute([]string{"-stats-dir", fixtures}); err != nil {
+ t.Fatal(err)
+ }
+ })
+ if !strings.Contains(out, "earth") {
+ t.Fatalf("report output should mention fixture host earth; got len=%d", len(out))
+ }
+}
+
+func TestStableImportAndQuery(t *testing.T) {
+ root := moduleRoot(t)
+ fixtures := filepath.Join(root, "fixtures")
+ db := filepath.Join(t.TempDir(), "compat.db")
+ if err := Execute([]string{"import", "-stats-dir", fixtures, "-db", db}); err != nil {
+ t.Fatalf("import: %v", err)
+ }
+ out := captureStdout(t, func() {
+ if err := Execute([]string{"query", "-db", db, "-limit", "3"}); err != nil {
+ t.Fatal(err)
+ }
+ })
+ if len(strings.TrimSpace(out)) == 0 {
+ t.Fatal("expected non-empty query output")
+ }
+}
+
+func TestStableIntegrationTestSubcommand(t *testing.T) {
+ root := moduleRoot(t)
+ t.Chdir(root)
+ if err := Execute([]string{"test"}); err != nil {
+ t.Fatal(err)
+ }
+}
+
+func TestStableDaemonRequiresStatsDir(t *testing.T) {
+ t.Setenv("GOPRECORDS_STATS_DIR", "")
+ for _, arg := range []string{"-daemon", "--daemon"} {
+ arg := arg
+ t.Run(arg, func(t *testing.T) {
+ err := Execute([]string{arg})
+ if err == nil {
+ t.Fatal("expected error")
+ }
+ if !strings.Contains(err.Error(), "stats-dir") {
+ t.Fatalf("expected stats-dir in error, got %v", err)
+ }
+ })
+ }
+}
+
+func TestStableSubcommandsStillRecognized(t *testing.T) {
+ for _, sub := range []string{"import", "query", "test"} {
+ if err := Execute([]string{sub}); err == nil {
+ t.Fatalf("subcommand %q should fail without required args/env, not succeed silently", sub)
+ }
+ }
+}
diff --git a/internal/goprecords/integration_test_runner.go b/internal/goprecords/integration_test_runner.go
index ff4a8c3..276714d 100644
--- a/internal/goprecords/integration_test_runner.go
+++ b/internal/goprecords/integration_test_runner.go
@@ -82,17 +82,27 @@ func testStatsOrder() int {
}
func testImportExport(ctx context.Context, aggregates *Aggregates, fixturesDir string) int {
- tmpDB := fixturesDir + "/test_import.db"
- os.Remove(tmpDB)
+ f, err := os.CreateTemp("", "goprecords-import-*.db")
+ if err != nil {
+ fmt.Printf("FAIL: temp db path: %v\n", err)
+ return 1
+ }
+ tmpDB := f.Name()
+ if err := f.Close(); err != nil {
+ fmt.Printf("FAIL: close temp db placeholder: %v\n", err)
+ _ = os.Remove(tmpDB)
+ return 1
+ }
failed := 0
db, err := OpenDB(ctx, tmpDB)
if err != nil {
+ _ = os.Remove(tmpDB)
fmt.Printf("FAIL: open tmp db: %v\n", err)
return 1
}
defer func() {
db.Close()
- os.Remove(tmpDB)
+ _ = os.Remove(tmpDB)
}()
CreateSchema(ctx, db)
if err := ImportFromDir(ctx, db, fixturesDir); err != nil {
diff --git a/internal/goprecords/report_test.go b/internal/goprecords/report_test.go
index d6b884d..9715c12 100644
--- a/internal/goprecords/report_test.go
+++ b/internal/goprecords/report_test.go
@@ -2,6 +2,7 @@ package goprecords
import (
"bytes"
+ "flag"
"net/url"
"strings"
"testing"
@@ -361,6 +362,42 @@ func TestParseReportQuery(t *testing.T) {
}
}
+func TestRegisterReportFlagsStableNames(t *testing.T) {
+ fs := flag.NewFlagSet("x", flag.ContinueOnError)
+ RegisterReportFlags(fs)
+ found := map[string]bool{}
+ fs.VisitAll(func(f *flag.Flag) {
+ found[f.Name] = true
+ })
+ required := []string{
+ "category", "metric", "limit", "output-format",
+ "all", "include-kernel", "stats-order",
+ }
+ for _, name := range required {
+ if !found[name] {
+ t.Errorf("missing required report flag %q (CLI and HTTP report contract)", name)
+ }
+ }
+}
+
+func TestRegisterReportFlagsDefaultsUnchanged(t *testing.T) {
+ fs := flag.NewFlagSet("x", flag.ContinueOnError)
+ rf := RegisterReportFlags(fs)
+ if err := fs.Parse(nil); err != nil {
+ t.Fatal(err)
+ }
+ cfg, err := rf.Parse()
+ if err != nil {
+ t.Fatal(err)
+ }
+ if cfg.Category != CategoryHost || cfg.Metric != MetricUptime || cfg.Limit != 20 {
+ t.Fatalf("defaults: %+v", cfg)
+ }
+ if cfg.OutputFormat != FormatPlaintext || cfg.All || cfg.IncludeKernel || cfg.StatsOrder != "" {
+ t.Fatalf("defaults: %+v", cfg)
+ }
+}
+
func hostName(i int) string {
switch i {
case 0: