diff options
| author | Paul Buetow <paul@buetow.org> | 2026-04-27 08:53:26 +0300 |
|---|---|---|
| committer | Paul Buetow <paul@buetow.org> | 2026-04-27 08:53:26 +0300 |
| commit | c6e0b5cc48dedb52477cb0060e6ebc8ca4f088f2 (patch) | |
| tree | c8dfff1c65648e342a5908e8df3e660173cd3d6a | |
| parent | dee27d8f2805c9e409853462d35f9103e1a8c53e (diff) | |
refactor(snonux): separate CLI parsing from I/O and theme logic
Split the overloaded parseFlags into focused helpers:
- parseFlags now does *only* CLI flag parsing.
- resolvePaths handles ~ expansion.
- validateDirs performs filesystem creation/checks.
- resolveTheme selects the random theme using rng.
This removes the SRP violation in parseFlags and makes each
function independently testable. Tests updated accordingly.
Fixes: z8
| -rw-r--r-- | cmd/snonux/main.go | 51 | ||||
| -rw-r--r-- | cmd/snonux/main_test.go | 115 | ||||
| -rw-r--r-- | cmd/snonux/resolve.go | 51 |
3 files changed, 149 insertions, 68 deletions
diff --git a/cmd/snonux/main.go b/cmd/snonux/main.go index a494854..75e386e 100644 --- a/cmd/snonux/main.go +++ b/cmd/snonux/main.go @@ -35,7 +35,7 @@ const ( ) func main() { - cfg, mode, err := parseFlags(os.Args[1:], rand.New(rand.NewSource(time.Now().UnixNano()))) + cfg, mode, err := parseFlags(os.Args[1:]) if err != nil { log.Fatalf("error: %v", err) } @@ -49,6 +49,18 @@ func main() { return } + if err := resolveTheme(cfg, rand.New(rand.NewSource(time.Now().UnixNano()))); err != nil { + log.Fatalf("error: %v", err) + } + + if err := resolvePaths(cfg); err != nil { + log.Fatalf("error: %v", err) + } + + if err := validateDirs(cfg); err != nil { + log.Fatalf("error: %v", err) + } + if err := run(cfg); err != nil { log.Fatalf("error: %v", err) } @@ -63,10 +75,9 @@ func main() { // errParseFlags is returned when flag parsing fails (e.g. unknown flag). 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. -// The rng parameter must be non-nil; it is used for theme selection. -func parseFlags(args []string, rng *rand.Rand) (*config.Config, cliMode, error) { +// parseFlags reads CLI flags and returns a Config without touching the filesystem or generators. +// Callers should invoke resolveTheme, resolvePaths, and validateDirs separately. +func parseFlags(args []string) (*config.Config, cliMode, error) { cfg := &config.Config{} fs := flag.NewFlagSet("snonux", flag.ContinueOnError) fs.SetOutput(io.Discard) @@ -94,36 +105,6 @@ func parseFlags(args []string, rng *rand.Rand) (*config.Config, cliMode, error) return nil, modeListThemes, nil } - // 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[rng.Intn(len(themes))] - log.Printf("random theme selected: %s", cfg.Theme) - } - - var err error - - cfg.InputDir, err = expandHome(cfg.InputDir) - if err != nil { - return nil, modeRun, fmt.Errorf("input dir: %w", err) - } - - cfg.OutputDir, err = expandHome(cfg.OutputDir) - if err != nil { - return nil, modeRun, fmt.Errorf("output dir: %w", err) - } - - if err := ensureDir(cfg.InputDir); err != nil { - return nil, modeRun, fmt.Errorf("input dir: %w", err) - } - - if err := ensureDir(cfg.OutputDir); err != nil { - return nil, modeRun, fmt.Errorf("output dir: %w", err) - } - return cfg, modeRun, nil } diff --git a/cmd/snonux/main_test.go b/cmd/snonux/main_test.go index a3ff292..a4fe22f 100644 --- a/cmd/snonux/main_test.go +++ b/cmd/snonux/main_test.go @@ -62,7 +62,7 @@ func TestEnsureDir_createsAndRejectsFile(t *testing.T) { func TestParseFlags_version(t *testing.T) { t.Parallel() - _, mode, err := parseFlags([]string{"-version"}, rand.New(rand.NewSource(1))) + _, mode, err := parseFlags([]string{"-version"}) if err != nil { t.Fatal(err) } @@ -74,7 +74,7 @@ func TestParseFlags_version(t *testing.T) { func TestParseFlags_listThemes(t *testing.T) { t.Parallel() - _, mode, err := parseFlags([]string{"-list-themes"}, rand.New(rand.NewSource(1))) + _, mode, err := parseFlags([]string{"-list-themes"}) if err != nil { t.Fatal(err) } @@ -86,14 +86,12 @@ func TestParseFlags_listThemes(t *testing.T) { func TestParseFlags_run(t *testing.T) { t.Parallel() - in := t.TempDir() - out := t.TempDir() cfg, mode, err := parseFlags([]string{ - "-input", in, - "-output", out, + "-input", "/tmp/in", + "-output", "/tmp/out", "-theme", "neon", "-base-url", "https://t.test", - }, rand.New(rand.NewSource(1))) + }) if err != nil { t.Fatal(err) } @@ -108,14 +106,12 @@ func TestParseFlags_run(t *testing.T) { func TestParseFlags_sync(t *testing.T) { t.Parallel() - in := t.TempDir() - out := t.TempDir() cfg, mode, err := parseFlags([]string{ - "-input", in, - "-output", out, + "-input", "./in", + "-output", "./out", "-theme", "neon", "-sync", - }, rand.New(rand.NewSource(1))) + }) if err != nil { t.Fatal(err) } @@ -127,15 +123,49 @@ func TestParseFlags_sync(t *testing.T) { } } -func TestParseFlags_randomTheme(t *testing.T) { +func TestResolvePaths(t *testing.T) { t.Parallel() - in := t.TempDir() - out := t.TempDir() - cfg, _, err := parseFlags([]string{"-input", in, "-output", out, "-theme", "random"}, rand.New(rand.NewSource(42))) + cfg := &config.Config{ + InputDir: filepath.Join("~", "snonux-test-in"), + OutputDir: filepath.Join("~", "snonux-test-out"), + } + + if err := resolvePaths(cfg); err != nil { + t.Fatal(err) + } + + home, err := os.UserHomeDir() if err != nil { t.Fatal(err) } + if cfg.InputDir != filepath.Join(home, "snonux-test-in") { + t.Fatalf("input dir: got %q", cfg.InputDir) + } + if cfg.OutputDir != filepath.Join(home, "snonux-test-out") { + t.Fatalf("output dir: got %q", cfg.OutputDir) + } +} + +func TestResolveTheme_fixed(t *testing.T) { + t.Parallel() + + cfg := &config.Config{Theme: "neon"} + if err := resolveTheme(cfg, rand.New(rand.NewSource(1))); err != nil { + t.Fatal(err) + } + if cfg.Theme != "neon" { + t.Fatalf("expected fixed theme, got %q", cfg.Theme) + } +} + +func TestResolveTheme_random(t *testing.T) { + t.Parallel() + + cfg := &config.Config{Theme: "random"} + if err := resolveTheme(cfg, rand.New(rand.NewSource(42))); err != nil { + t.Fatal(err) + } names := map[string]bool{} for _, n := range generator.ListThemes() { names[n] = true @@ -145,39 +175,58 @@ func TestParseFlags_randomTheme(t *testing.T) { } } -// TestParseFlags_randomTheme_deterministic verifies that when a seeded +// TestResolveTheme_random_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) { +func TestResolveTheme_random_deterministic(t *testing.T) { t.Parallel() - in := t.TempDir() - out := t.TempDir() + cfg1 := &config.Config{Theme: "random"} + if err := resolveTheme(cfg1, rand.New(rand.NewSource(1))); err != nil { + t.Fatal(err) + } - // NewSource(1) results in a deterministic first draw across multiple calls. - src := rand.NewSource(1) - rng := rand.New(src) + cfg2 := &config.Config{Theme: "random"} + if err := resolveTheme(cfg2, rand.New(rand.NewSource(1))); err != nil { + t.Fatal(err) + } + if cfg1.Theme != cfg2.Theme { + t.Fatalf("expected deterministic theme %q, got %q", cfg1.Theme, cfg2.Theme) + } +} - cfg, _, err := parseFlags([]string{"-input", in, "-output", out, "-theme", "random"}, rng) - if err != nil { +func TestResolveTheme_nilRng(t *testing.T) { + t.Parallel() + + cfg := &config.Config{Theme: "random"} + if err := resolveTheme(cfg, nil); err == nil { + t.Fatal("expected error for nil rng") + } +} + +func TestValidateDirs(t *testing.T) { + t.Parallel() + + in := t.TempDir() + out := t.TempDir() + cfg := &config.Config{InputDir: in, OutputDir: out} + if err := validateDirs(cfg); 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 { + badFile := filepath.Join(t.TempDir(), "notadir") + if err := os.WriteFile(badFile, []byte("x"), 0o644); err != nil { t.Fatal(err) } - if cfg.Theme != cfg2.Theme { - t.Fatalf("expected deterministic theme %q, got %q", cfg.Theme, cfg2.Theme) + cfg = &config.Config{InputDir: badFile, OutputDir: out} + if err := validateDirs(cfg); err == nil { + t.Fatal("expected error when input is file") } } func TestParseFlags_unknownFlag(t *testing.T) { t.Parallel() - _, _, err := parseFlags([]string{"-not-a-real-flag"}, rand.New(rand.NewSource(1))) + _, _, err := parseFlags([]string{"-not-a-real-flag"}) if err == nil { t.Fatal("expected error") } diff --git a/cmd/snonux/resolve.go b/cmd/snonux/resolve.go new file mode 100644 index 0000000..df16ffa --- /dev/null +++ b/cmd/snonux/resolve.go @@ -0,0 +1,51 @@ +package main + +import ( + "fmt" + "math/rand" + + "codeberg.org/snonux/snonux/internal/config" + "codeberg.org/snonux/snonux/internal/generator" +) + +// resolvePaths expands home directories in cfg.InputDir and cfg.OutputDir. +func resolvePaths(cfg *config.Config) error { + var err error + + cfg.InputDir, err = expandHome(cfg.InputDir) + if err != nil { + return fmt.Errorf("input dir: %w", err) + } + + cfg.OutputDir, err = expandHome(cfg.OutputDir) + if err != nil { + return fmt.Errorf("output dir: %w", err) + } + + return nil +} + +// validateDirs ensures cfg.InputDir and cfg.OutputDir exist as directories. +func validateDirs(cfg *config.Config) error { + if err := ensureDir(cfg.InputDir); err != nil { + return fmt.Errorf("input dir: %w", err) + } + if err := ensureDir(cfg.OutputDir); err != nil { + return fmt.Errorf("output dir: %w", err) + } + return nil +} + +// resolveTheme resolves the special "random" theme value by picking a registered +// theme using rng. The rng parameter must be non-nil. +func resolveTheme(cfg *config.Config, rng *rand.Rand) error { + if cfg.Theme != "random" { + return nil + } + if rng == nil { + return fmt.Errorf("theme %q requires a seeded rng", cfg.Theme) + } + themes := generator.ListThemes() + cfg.Theme = themes[rng.Intn(len(themes))] + return nil +} |
