package cli import ( "fmt" "strings" "testing" ) const version = `$VER:ddt-ocr,1.0.1,2025-11-23,celestino.amoroso@gmail.com:$` type GlobalData struct { config string log []string printOcr bool saveClips bool trace bool page []int cliVars map[string]string inputName string workDir string attempts int sources []string dest string report string } func TestVersion(t *testing.T) { var cli CliParser var gd GlobalData if err := initCli(&cli, &gd); err != nil { t.Error(err) return } if v, err := cli.GetVersionSection("all"); err == nil { if v != version[5:len(version)-2] { t.Errorf("Version string does not match expected.\nGot:\n%q\nExpected:\n%q", v, version) } } else { t.Error(err) } } func TestVersionNumber(t *testing.T) { var cli CliParser var gd GlobalData if err := initCli(&cli, &gd); err != nil { t.Error(err) return } secValues := []string{"ddt-ocr", "1.0.1", "2025-11-23", "celestino.amoroso@gmail.com"} for i, sec := range []string{"program", "version", "date", "email"} { if v, err := cli.GetVersionSection(sec); err == nil { if v != secValues[i] { t.Errorf("Version section %q does not match expected value.\nGot:\n%q\nExpected:\n%q", sec, v, version) } } else { t.Error(err) } } } func TestUsage(t *testing.T) { const expectedUsage = `NAME ddt-ocr - cli-test USAGE: ddt-ocr [] ... [] where: Source image files Output destination file Optional report file -o, --print-ocr Print the OCR output to stderr -s, --save-clip Save the image clips as PNG files (alias: save-clips) -t, --trace Enable trace mode for detailed logging -p, --page(s) ["," ...] Process only the specified pages (comma-separated list) -c, --config Alternate configuration file -l, --log(s) ["," ...] Logging options (comma-separated list) -V, --var(s) ["," ...] Define one or more comma separated variables for the actions context (multiple allowed) -n, --input-name Input file name when source comes from stdin -d, --work-dir Work directory --attempts Attempts for retrying failed operations (default: "1") ` var cli CliParser var gd GlobalData if err := initCli(&cli, &gd); err != nil { t.Error(err) return } usage := cli.Usage() if usage != expectedUsage { t.Errorf("Usage output does not match expected.\nGot:\n%s\nExpected:\n%s", usage, expectedUsage) } } func TestParser(t *testing.T) { const expectedOutput = `Option: print-ocr, Type: bool, Value: true Option: save-clip, Type: bool, Value: false Option: trace, Type: bool, Value: true Option: page, Type: num-array, Value: [17 18] Option: config, Type: string, Value: devel-config.yaml Option: log, Type: string-array, Value: [all] Option: var, Type: map-string, Value: map[deploy_env:devel] Option: input-name, Type: string, Value: my-scan.pdf Option: work-dir, Type: string, Value: Option: attempts, Type: num, Value: 1 Option: help, Type: n/a, Value: Option: version, Type: n/a, Value: ` var cli CliParser var gd GlobalData var sb strings.Builder if err := initCli(&cli, &gd); err != nil { t.Error(err) return } tracer := NewSimpleOptionTracer(&sb) if err := cli.Parse(); err == nil { cli.TraceOptions(tracer) if sb.String() != expectedOutput { t.Errorf("Parsed options do not match expected.\nGot:\n%q\nExpected:\n%q", sb.String(), expectedOutput) } } else { t.Error(err) } } func initCli(cli *CliParser, gd *GlobalData) (err error) { args := []string{ "ddt-ocr", "--log", "all", "-t", "--config", "devel-config.yaml", "--var", "deploy_env=devel", "--print-ocr", "--pages", "17,18", "--input-name=my-scan.pdf", "scan1.pdf", "scan2.pdf", "result.txt", "report.txt", } cli.Init(args, version, "cli-test") err = gd.addOptions(cli) return } func TestOptErrorUnknownOption(t *testing.T) { const unknownOption = "--logging" var expectedErr = errUnknownOption(unknownOption) var cli CliParser var gd GlobalData if err := initCliUnknownOption(&cli, &gd, unknownOption); err == nil { if err = cli.Parse(); err != nil { if err.Error() != expectedErr.Error() { t.Errorf("Invalid error message.\nGot:\n%v\nExpected:\n%v", err, expectedErr) } } else { t.Errorf("Expected error for unknown option %q, but got none", unknownOption) } } else { t.Error(err) return } } func initCliUnknownOption(cli *CliParser, gd *GlobalData, option string) (err error) { args := []string{ "ddt-ocr", option, "scan1.pdf", "scan2.pdf", "result.txt", "report.txt", } cli.Init(args, version, "cli-test") err = gd.addOptions(cli) return } func TestOptErrorMissingOptionValue(t *testing.T) { const missingValueOption = "--page" var expectedErr = fmt.Errorf(`invalid value scan1.pdf (string) for option "page" (num-array)`) var cli CliParser var gd GlobalData if err := initCliMissingOptionValue(&cli, &gd, missingValueOption); err == nil { if err = cli.Parse(); err != nil { if err.Error() != expectedErr.Error() { t.Errorf("Invalid error message.\nGot:\n%v\nExpected:\n%v", err, expectedErr) } } else { t.Errorf("Expected error for unknown option %q, but got none", missingValueOption) } } else { t.Error(err) return } } func initCliMissingOptionValue(cli *CliParser, gd *GlobalData, option string) (err error) { args := []string{ "ddt-ocr", option, "scan1.pdf", "scan2.pdf", "result.txt", "report.txt", } cli.Init(args, version, "cli-test") err = gd.addOptions(cli) return } func TestOptErrorInvalidOptionValue(t *testing.T) { const missingInvalidValueOption = "--page" var expectedErr = errInvalidOptionValue("page", "some", "num-array") var cli CliParser var gd GlobalData if err := initCliInvalidOptionValue(&cli, &gd, missingInvalidValueOption); err == nil { if err = cli.Parse(); err != nil { if err.Error() != expectedErr.Error() { t.Errorf("Invalid error message.\nGot:\n%v\nExpected:\n%v", err, expectedErr) } } else { t.Errorf("Expected error for unknown option %q, but got none", missingInvalidValueOption) } } else { t.Error(err) return } } func initCliInvalidOptionValue(cli *CliParser, gd *GlobalData, option string) (err error) { args := []string{ "ddt-ocr", option, "some", "scan1.pdf", "scan2.pdf", "result.txt", "report.txt", } cli.Init(args, version, "cli-test") err = gd.addOptions(cli) return } func TestArgErrorMissingRequired(t *testing.T) { const missingRequiredArg = "--page" var expectedErr = fmt.Errorf(`missing required arg `) var cli CliParser var gd GlobalData if err := initCliMissingRequiredArg(&cli, &gd); err == nil { if err = cli.Parse(); err != nil { if err.Error() != expectedErr.Error() { t.Errorf("Invalid error message.\nGot:\n%v\nExpected:\n%v", err, expectedErr) } } else { t.Errorf("Expected error for unknown option %q, but got none", missingRequiredArg) } } else { t.Error(err) return } } func initCliMissingRequiredArg(cli *CliParser, gd *GlobalData) (err error) { args := []string{ "ddt-ocr", } cli.Init(args, version, "cli-test") err = gd.addOptions(cli) return } func TestArgErrorReapet(t *testing.T) { var expectedErr = fmt.Errorf(`repeat property already set for arg `) var cli CliParser var gd GlobalData if err := initCliReapetArg(&cli, &gd); err != nil { if err.Error() != expectedErr.Error() { t.Errorf("Invalid error message.\nGot:\n%v\nExpected:\n%v", err, expectedErr) } } else { t.Errorf("Expected error, but got none") } } func initCliReapetArg(cli *CliParser, gd *GlobalData) (err error) { args := []string{ "ddt-ocr", "scan1.pdf", "scan2.pdf", "result.txt", "report.txt", } cli.Init(args, version, "cli-test") if err = gd.addOptions(cli); err == nil { defer func() { if r := recover(); r != nil { err = r.(error) } }() cli.AddStringArrayArg("other", true, nil, "other args") cli.AddStringArrayArg("other2", true, nil, "other args 2") } return } func TestOptHidden(t *testing.T) { const expectedUsage = `NAME ddt-ocr - cli-test USAGE: ddt-ocr [] ... [] where: Source image files Output destination file Optional report file -o, --print-ocr Print the OCR output to stderr -t, --trace Enable trace mode for detailed logging -p, --page(s) ["," ...] Process only the specified pages (comma-separated list) -c, --config Alternate configuration file -l, --log(s) ["," ...] Logging options (comma-separated list) -V, --var(s) ["," ...] Define one or more comma separated variables for the actions context (multiple allowed) -n, --input-name Input file name when source comes from stdin -d, --work-dir Work directory --attempts Attempts for retrying failed operations (default: "1") ` var cli CliParser var gd GlobalData if err := initCliHiddenOpt(&cli, &gd); err != nil { t.Error(err) return } usage := cli.Usage() if usage != expectedUsage { t.Errorf("Usage output does not match expected.\nGot:\n%s\nExpected:\n%s", usage, expectedUsage) } } func initCliHiddenOpt(cli *CliParser, gd *GlobalData) (err error) { if err = initCli(cli, gd); err == nil { if ref := cli.GetOption("save-clip"); ref != nil { ref.SetHidden(true) } else { err = fmt.Errorf("option save-clip not found") } } return } func (gd *GlobalData) addOptions(cli *CliParser) (err error) { // Always recover from panic to return error before adding options defer func() { if r := recover(); r != nil { err = r.(error) } }() // Define options cli.AddBoolOpt("print-ocr", "o", &gd.printOcr, "Print the OCR output to stderr") cli.AddBoolOpt("save-clip", "s", &gd.saveClips, "Save the image clips as PNG files", "save-clips") cli.AddBoolOpt("trace", "t", &gd.trace, "Enable trace mode for detailed logging") cli.AddIntArrayOpt("page", "p", &gd.page, gd.page, "Process only the specified pages (comma-separated list)") cli.AddFileOpt("config", "c", &gd.config, gd.config, "Alternate configuration file") cli.AddStringArrayOpt("log", "l", &gd.log, gd.log, "Logging options (comma-separated list)") cli.AddStringMapOpt("var", "V", &gd.cliVars, nil, "Define one or more comma separated variables for the actions context (multiple allowed)") cli.AddStringOpt("input-name", "n", &gd.inputName, "", "Input file name when source comes from stdin") cli.AddDirOpt("work-dir", "d", &gd.workDir, "", "Work directory") if ref := cli.AddIntOpt("attempts", "", &gd.attempts, 1, "Attempts for retrying failed operations"); ref != nil { ref.AddSpecialValue("many", func(manager OptManager, cliValue string, currentValue any) (any, error) { return 1000, nil }) ref.AddSpecialValue("few", func(manager OptManager, cliValue string, currentValue any) (any, error) { manager.SetOptionValue("page", "1") return nil, nil }) } // Define arguments cli.AddStringArrayArg("image-sources", true, &gd.sources, "Source image files") cli.AddStringArg("dest", true, &gd.dest, "Output destination file") cli.AddStringArg("report", false, &gd.report, "Optional report file") return }