From f8f17653e66bd2cd90c6a2cf7970afee54c643dc Mon Sep 17 00:00:00 2001 From: Paul Buetow Date: Mon, 27 Apr 2026 08:25:32 +0300 Subject: fix: seed rand source for --theme random to avoid deterministic output --- cmd/snonux/main.go | 11 ++++++++--- cmd/snonux/main_test.go | 42 ++++++++++++++++++++++++++++++++++++------ 2 files changed, 44 insertions(+), 9 deletions(-) diff --git a/cmd/snonux/main.go b/cmd/snonux/main.go index 42a377f..a494854 100644 --- a/cmd/snonux/main.go +++ b/cmd/snonux/main.go @@ -17,6 +17,7 @@ import ( "os" "path/filepath" "strings" + "time" "codeberg.org/snonux/snonux/internal/config" "codeberg.org/snonux/snonux/internal/generator" @@ -34,7 +35,7 @@ const ( ) func main() { - cfg, mode, err := parseFlags(os.Args[1:]) + cfg, mode, err := parseFlags(os.Args[1:], rand.New(rand.NewSource(time.Now().UnixNano()))) if err != nil { log.Fatalf("error: %v", err) } @@ -64,7 +65,8 @@ var errParseFlags = errors.New("flag parse error") // parseFlags reads CLI flags and returns a validated Config. // Special theme value "random" picks a theme at random from the registry. -func parseFlags(args []string) (*config.Config, cliMode, error) { +// The rng parameter must be non-nil; it is used for theme selection. +func parseFlags(args []string, rng *rand.Rand) (*config.Config, cliMode, error) { cfg := &config.Config{} fs := flag.NewFlagSet("snonux", flag.ContinueOnError) fs.SetOutput(io.Discard) @@ -94,8 +96,11 @@ func parseFlags(args []string) (*config.Config, cliMode, error) { // Resolve the special "random" value before any further validation. if cfg.Theme == "random" { + if rng == nil { + return nil, modeRun, fmt.Errorf("theme %q requires a seeded rng", cfg.Theme) + } themes := generator.ListThemes() - cfg.Theme = themes[rand.Intn(len(themes))] + cfg.Theme = themes[rng.Intn(len(themes))] log.Printf("random theme selected: %s", cfg.Theme) } diff --git a/cmd/snonux/main_test.go b/cmd/snonux/main_test.go index 7f05c8b..a3ff292 100644 --- a/cmd/snonux/main_test.go +++ b/cmd/snonux/main_test.go @@ -2,6 +2,7 @@ package main import ( "errors" + "math/rand" "os" "path/filepath" "strings" @@ -61,7 +62,7 @@ func TestEnsureDir_createsAndRejectsFile(t *testing.T) { func TestParseFlags_version(t *testing.T) { t.Parallel() - _, mode, err := parseFlags([]string{"-version"}) + _, mode, err := parseFlags([]string{"-version"}, rand.New(rand.NewSource(1))) if err != nil { t.Fatal(err) } @@ -73,7 +74,7 @@ func TestParseFlags_version(t *testing.T) { func TestParseFlags_listThemes(t *testing.T) { t.Parallel() - _, mode, err := parseFlags([]string{"-list-themes"}) + _, mode, err := parseFlags([]string{"-list-themes"}, rand.New(rand.NewSource(1))) if err != nil { t.Fatal(err) } @@ -92,7 +93,7 @@ func TestParseFlags_run(t *testing.T) { "-output", out, "-theme", "neon", "-base-url", "https://t.test", - }) + }, rand.New(rand.NewSource(1))) if err != nil { t.Fatal(err) } @@ -114,7 +115,7 @@ func TestParseFlags_sync(t *testing.T) { "-output", out, "-theme", "neon", "-sync", - }) + }, rand.New(rand.NewSource(1))) if err != nil { t.Fatal(err) } @@ -131,7 +132,7 @@ func TestParseFlags_randomTheme(t *testing.T) { in := t.TempDir() out := t.TempDir() - cfg, _, err := parseFlags([]string{"-input", in, "-output", out, "-theme", "random"}) + cfg, _, err := parseFlags([]string{"-input", in, "-output", out, "-theme", "random"}, rand.New(rand.NewSource(42))) if err != nil { t.Fatal(err) } @@ -144,10 +145,39 @@ func TestParseFlags_randomTheme(t *testing.T) { } } +// TestParseFlags_randomTheme_deterministic verifies that when a seeded +// *rand.Rand is passed in, the "random" theme resolve predictably. +// This ensures the rng parameter is actually used. +func TestParseFlags_randomTheme_deterministic(t *testing.T) { + t.Parallel() + + in := t.TempDir() + out := t.TempDir() + + // NewSource(1) results in a deterministic first draw across multiple calls. + src := rand.NewSource(1) + rng := rand.New(src) + + cfg, _, err := parseFlags([]string{"-input", in, "-output", out, "-theme", "random"}, rng) + if err != nil { + t.Fatal(err) + } + + // Reset the source and re-invoke parseFlags to confirm the same theme + // is selected again (determinism). + cfg2, _, err := parseFlags([]string{"-input", in, "-output", out, "-theme", "random"}, rand.New(rand.NewSource(1))) + if err != nil { + t.Fatal(err) + } + if cfg.Theme != cfg2.Theme { + t.Fatalf("expected deterministic theme %q, got %q", cfg.Theme, cfg2.Theme) + } +} + func TestParseFlags_unknownFlag(t *testing.T) { t.Parallel() - _, _, err := parseFlags([]string{"-not-a-real-flag"}) + _, _, err := parseFlags([]string{"-not-a-real-flag"}, rand.New(rand.NewSource(1))) if err == nil { t.Fatal("expected error") } -- cgit v1.2.3