summaryrefslogtreecommitdiff
path: root/cmd
diff options
context:
space:
mode:
authorPaul Buetow <paul@buetow.org>2026-04-10 10:43:51 +0300
committerPaul Buetow <paul@buetow.org>2026-04-10 10:43:51 +0300
commitfc28cc5196900f0c8ae446269294da42d5488c95 (patch)
tree57313f0d9bd17e722150be53fe230a38a37dfddb /cmd
parenta2cf4a2d5b59fb6e445f8b3f5bfbdace42b6a5bf (diff)
Release v0.1.3v0.1.3
Testable CLI flags; version package under internal/version; broad tests for atom, generator, post, processor, and cmd—overall coverage ~85%. Made-with: Cursor
Diffstat (limited to 'cmd')
-rw-r--r--cmd/snonux/main.go67
-rw-r--r--cmd/snonux/main_test.go162
2 files changed, 209 insertions, 20 deletions
diff --git a/cmd/snonux/main.go b/cmd/snonux/main.go
index 6b6d0bb..1d00646 100644
--- a/cmd/snonux/main.go
+++ b/cmd/snonux/main.go
@@ -8,54 +8,81 @@
package main
import (
+ "errors"
"flag"
"fmt"
+ "io"
"log"
"math/rand"
"os"
"path/filepath"
"strings"
- "codeberg.org/snonux/snonux/internal"
"codeberg.org/snonux/snonux/internal/config"
"codeberg.org/snonux/snonux/internal/generator"
"codeberg.org/snonux/snonux/internal/processor"
+ "codeberg.org/snonux/snonux/internal/version"
+)
+
+// cliMode tells main whether to run the pipeline or print and exit.
+type cliMode int
+
+const (
+ modeRun cliMode = iota
+ modeVersion
+ modeListThemes
)
func main() {
- cfg, err := parseFlags()
+ cfg, mode, err := parseFlags(os.Args[1:])
if err != nil {
log.Fatalf("error: %v", err)
}
+ switch mode {
+ case modeVersion:
+ fmt.Println(version.Version)
+ return
+ case modeListThemes:
+ fmt.Println(strings.Join(generator.ListThemes(), "\n"))
+ return
+ }
+
if err := run(cfg); err != nil {
log.Fatalf("error: %v", err)
}
}
+// 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.
-func parseFlags() (*config.Config, error) {
+func parseFlags(args []string) (*config.Config, cliMode, error) {
cfg := &config.Config{}
+ fs := flag.NewFlagSet("snonux", flag.ContinueOnError)
+ fs.SetOutput(io.Discard)
+
var showVersion bool
- flag.BoolVar(&showVersion, "version", false, "print version and exit (-version, --version)")
- flag.BoolVar(&showVersion, "v", false, "print version and exit (shorthand for -version)")
- listThemes := flag.Bool("list-themes", false, "print all available theme names and exit")
+ fs.BoolVar(&showVersion, "version", false, "print version and exit (-version, --version)")
+ fs.BoolVar(&showVersion, "v", false, "print version and exit (shorthand for -version)")
+ listThemes := fs.Bool("list-themes", false, "print all available theme names and exit")
+
+ fs.StringVar(&cfg.InputDir, "input", "./inbox", "directory containing new source files to process")
+ fs.StringVar(&cfg.OutputDir, "output", "~/git/snonux.foo/dist", "root directory for generated static site output")
+ fs.StringVar(&cfg.BaseURL, "base-url", "https://snonux.foo", "canonical base URL used in Atom feed links")
+ fs.StringVar(&cfg.Theme, "theme", "random", "visual theme name, or \"random\" to pick one at random")
- flag.StringVar(&cfg.InputDir, "input", "./inbox", "directory containing new source files to process")
- flag.StringVar(&cfg.OutputDir, "output", "~/git/snonux.foo/dist", "root directory for generated static site output")
- flag.StringVar(&cfg.BaseURL, "base-url", "https://snonux.foo", "canonical base URL used in Atom feed links")
- flag.StringVar(&cfg.Theme, "theme", "random", "visual theme name, or \"random\" to pick one at random")
- flag.Parse()
+ if err := fs.Parse(args); err != nil {
+ return nil, modeRun, fmt.Errorf("%w: %w", errParseFlags, err)
+ }
if showVersion {
- fmt.Println(version.Version)
- os.Exit(0)
+ return nil, modeVersion, nil
}
if *listThemes {
- fmt.Println(strings.Join(generator.ListThemes(), "\n"))
- os.Exit(0)
+ return nil, modeListThemes, nil
}
// Resolve the special "random" value before any further validation.
@@ -69,23 +96,23 @@ func parseFlags() (*config.Config, error) {
cfg.InputDir, err = expandHome(cfg.InputDir)
if err != nil {
- return nil, fmt.Errorf("input dir: %w", err)
+ return nil, modeRun, fmt.Errorf("input dir: %w", err)
}
cfg.OutputDir, err = expandHome(cfg.OutputDir)
if err != nil {
- return nil, fmt.Errorf("output dir: %w", err)
+ return nil, modeRun, fmt.Errorf("output dir: %w", err)
}
if err := ensureDir(cfg.InputDir); err != nil {
- return nil, fmt.Errorf("input dir: %w", err)
+ return nil, modeRun, fmt.Errorf("input dir: %w", err)
}
if err := ensureDir(cfg.OutputDir); err != nil {
- return nil, fmt.Errorf("output dir: %w", err)
+ return nil, modeRun, fmt.Errorf("output dir: %w", err)
}
- return cfg, nil
+ return cfg, modeRun, nil
}
// expandHome replaces a leading ~ with the current user's home directory.
diff --git a/cmd/snonux/main_test.go b/cmd/snonux/main_test.go
new file mode 100644
index 0000000..212efc8
--- /dev/null
+++ b/cmd/snonux/main_test.go
@@ -0,0 +1,162 @@
+package main
+
+import (
+ "errors"
+ "os"
+ "path/filepath"
+ "strings"
+ "testing"
+
+ "codeberg.org/snonux/snonux/internal/config"
+ "codeberg.org/snonux/snonux/internal/generator"
+)
+
+func TestExpandHome(t *testing.T) {
+ t.Parallel()
+
+ home, err := os.UserHomeDir()
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ got, err := expandHome(filepath.Join("~", "snonux-test-sub"))
+ if err != nil {
+ t.Fatal(err)
+ }
+ want := filepath.Join(home, "snonux-test-sub")
+ if got != want {
+ t.Fatalf("got %q; want %q", got, want)
+ }
+
+ got, err = expandHome("/no/tilde")
+ if err != nil || got != "/no/tilde" {
+ t.Fatalf("got %q err %v", got, err)
+ }
+}
+
+func TestEnsureDir_createsAndRejectsFile(t *testing.T) {
+ t.Parallel()
+
+ dir := t.TempDir()
+ sub := filepath.Join(dir, "newdir")
+ if err := ensureDir(sub); err != nil {
+ t.Fatal(err)
+ }
+ if st, err := os.Stat(sub); err != nil || !st.IsDir() {
+ t.Fatal("not a dir")
+ }
+ if err := ensureDir(sub); err != nil {
+ t.Fatal(err)
+ }
+
+ filePath := filepath.Join(dir, "file")
+ if err := os.WriteFile(filePath, []byte("x"), 0o644); err != nil {
+ t.Fatal(err)
+ }
+ if err := ensureDir(filePath); err == nil {
+ t.Fatal("expected error when path is file")
+ }
+}
+
+func TestParseFlags_version(t *testing.T) {
+ t.Parallel()
+
+ _, mode, err := parseFlags([]string{"-version"})
+ if err != nil {
+ t.Fatal(err)
+ }
+ if mode != modeVersion {
+ t.Fatalf("mode %v", mode)
+ }
+}
+
+func TestParseFlags_listThemes(t *testing.T) {
+ t.Parallel()
+
+ _, mode, err := parseFlags([]string{"-list-themes"})
+ if err != nil {
+ t.Fatal(err)
+ }
+ if mode != modeListThemes {
+ t.Fatalf("mode %v", mode)
+ }
+}
+
+func TestParseFlags_run(t *testing.T) {
+ t.Parallel()
+
+ in := t.TempDir()
+ out := t.TempDir()
+ cfg, mode, err := parseFlags([]string{
+ "-input", in,
+ "-output", out,
+ "-theme", "neon",
+ "-base-url", "https://t.test",
+ })
+ if err != nil {
+ t.Fatal(err)
+ }
+ if mode != modeRun {
+ t.Fatalf("mode %v", mode)
+ }
+ if cfg.Theme != "neon" || cfg.BaseURL != "https://t.test" {
+ t.Fatalf("cfg %+v", cfg)
+ }
+}
+
+func TestParseFlags_randomTheme(t *testing.T) {
+ t.Parallel()
+
+ in := t.TempDir()
+ out := t.TempDir()
+ cfg, _, err := parseFlags([]string{"-input", in, "-output", out, "-theme", "random"})
+ if err != nil {
+ t.Fatal(err)
+ }
+ names := map[string]bool{}
+ for _, n := range generator.ListThemes() {
+ names[n] = true
+ }
+ if !names[cfg.Theme] {
+ t.Fatalf("unexpected theme %q", cfg.Theme)
+ }
+}
+
+func TestParseFlags_unknownFlag(t *testing.T) {
+ t.Parallel()
+
+ _, _, err := parseFlags([]string{"-not-a-real-flag"})
+ if err == nil {
+ t.Fatal("expected error")
+ }
+ if !errors.Is(err, errParseFlags) {
+ t.Fatalf("got %v", err)
+ }
+}
+
+func TestRun_pipeline(t *testing.T) {
+ t.Parallel()
+
+ in := t.TempDir()
+ out := t.TempDir()
+ if err := os.WriteFile(filepath.Join(in, "a.txt"), []byte("x"), 0o644); err != nil {
+ t.Fatal(err)
+ }
+
+ cfg := &config.Config{
+ InputDir: in,
+ OutputDir: out,
+ BaseURL: "https://pipe.test",
+ Theme: "neon",
+ }
+ if err := run(cfg); err != nil {
+ t.Fatal(err)
+ }
+ data, err := os.ReadFile(filepath.Join(out, "index.html"))
+ if err != nil {
+ t.Fatal(err)
+ }
+ if !strings.Contains(string(data), "snonux") {
+ t.Fatal("expected generated html")
+ }
+}