summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorPaul Buetow <paul@buetow.org>2026-04-27 08:25:32 +0300
committerPaul Buetow <paul@buetow.org>2026-04-27 08:25:32 +0300
commitf8f17653e66bd2cd90c6a2cf7970afee54c643dc (patch)
tree148458c7989693cb0cca76c36c2be11486c0f971
parent734c7fbd89241133499a88674d5cf62de2ca1469 (diff)
fix: seed rand source for --theme random to avoid deterministic output
-rw-r--r--cmd/snonux/main.go11
-rw-r--r--cmd/snonux/main_test.go42
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")
}