inshellah/test/test_inshellah.ml
2026-03-27 15:02:16 +11:00

492 lines
16 KiB
OCaml

open Inshellah.Parser
open Inshellah.Manpage
open Inshellah.Nushell
let failures = ref 0
let passes = ref 0
let check name condition =
if condition then begin
incr passes;
Printf.printf " PASS: %s\n" name
end else begin
incr failures;
Printf.printf " FAIL: %s\n" name
end
let parse txt =
match parse_help txt with
| Ok r -> r
| Error msg -> failwith (Printf.sprintf "parse_help failed: %s" msg)
(* --- Help parser tests --- *)
let test_gnu_basic () =
Printf.printf "\n== GNU basic flags ==\n";
let r = parse " -a, --all do not ignore entries starting with .\n" in
check "one entry" (List.length r.entries = 1);
let e = List.hd r.entries in
check "both switch" (e.switch = Both ('a', "all"));
check "no param" (e.param = None);
check "desc" (String.length e.desc > 0)
let test_gnu_eq_param () =
Printf.printf "\n== GNU = param ==\n";
let r = parse " --block-size=SIZE scale sizes by SIZE\n" in
check "one entry" (List.length r.entries = 1);
let e = List.hd r.entries in
check "long switch" (e.switch = Long "block-size");
check "mandatory param" (e.param = Some (Mandatory "SIZE"))
let test_gnu_opt_param () =
Printf.printf "\n== GNU optional param ==\n";
let r = parse " --color[=WHEN] color the output WHEN\n" in
check "one entry" (List.length r.entries = 1);
let e = List.hd r.entries in
check "long switch" (e.switch = Long "color");
check "optional param" (e.param = Some (Optional "WHEN"))
let test_underscore_param () =
Printf.printf "\n== Underscore in param (TIME_STYLE) ==\n";
let r = parse " --time-style=TIME_STYLE time/date format\n" in
check "one entry" (List.length r.entries = 1);
let e = List.hd r.entries in
check "param with underscore" (e.param = Some (Mandatory "TIME_STYLE"))
let test_short_only () =
Printf.printf "\n== Short-only flag ==\n";
let r = parse " -v verbose output\n" in
check "one entry" (List.length r.entries = 1);
check "short switch" ((List.hd r.entries).switch = Short 'v')
let test_long_only () =
Printf.printf "\n== Long-only flag ==\n";
let r = parse " --help display help\n" in
check "one entry" (List.length r.entries = 1);
check "long switch" ((List.hd r.entries).switch = Long "help")
let test_multiline_desc () =
Printf.printf "\n== Multi-line description ==\n";
let r = parse {| --block-size=SIZE with -l, scale sizes by SIZE when printing them;
e.g., '--block-size=M'; see SIZE format below
|} in
check "one entry" (List.length r.entries = 1);
let e = List.hd r.entries in
check "desc includes continuation" (String.length e.desc > 50)
let test_multiple_entries () =
Printf.printf "\n== Multiple entries ==\n";
let r = parse {| -a, --all do not ignore entries starting with .
-A, --almost-all do not list implied . and ..
--author with -l, print the author of each file
|} in
check "three entries" (List.length r.entries = 3)
let test_clap_short_sections () =
Printf.printf "\n== Clap short with section headers ==\n";
let r = parse {|INPUT OPTIONS:
-e, --regexp=PATTERN A pattern to search for.
-f, --file=PATTERNFILE Search for patterns from the given file.
SEARCH OPTIONS:
-s, --case-sensitive Search case sensitively.
|} in
check "three entries" (List.length r.entries = 3);
let e = List.hd r.entries in
check "first is regexp" (e.switch = Both ('e', "regexp"));
check "first has param" (e.param = Some (Mandatory "PATTERN"))
let test_clap_long_style () =
Printf.printf "\n== Clap long style (desc below flag) ==\n";
let r = parse {| -H, --hidden
Include hidden directories and files.
--no-ignore
Do not respect ignore files.
|} in
check "two entries" (List.length r.entries = 2);
let e = List.hd r.entries in
check "hidden switch" (e.switch = Both ('H', "hidden"));
check "desc below" (String.length e.desc > 0)
let test_clap_long_angle_param () =
Printf.printf "\n== Clap long angle bracket param ==\n";
let r = parse {| --nonprintable-notation <notation>
Set notation for non-printable characters.
|} in
check "one entry" (List.length r.entries = 1);
let e = List.hd r.entries in
check "long switch" (e.switch = Long "nonprintable-notation");
check "angle param" (e.param = Some (Mandatory "notation"))
let test_space_upper_param () =
Printf.printf "\n== Space-separated ALL_CAPS param ==\n";
let r = parse " -f, --foo FOO foo help\n" in
check "one entry" (List.length r.entries = 1);
let e = List.hd r.entries in
check "switch" (e.switch = Both ('f', "foo"));
check "space param" (e.param = Some (Mandatory "FOO"))
let test_go_cobra_flags () =
Printf.printf "\n== Go/Cobra flags ==\n";
let r = parse {|Flags:
-D, --debug Enable debug mode
-H, --host string Daemon socket to connect to
-v, --version Print version information
|} in
check "three flag entries" (List.length r.entries = 3);
(* Check the host flag has a type param *)
let host = List.nth r.entries 1 in
check "host switch" (host.switch = Both ('H', "host"));
check "host type param" (host.param = Some (Mandatory "string"))
let test_go_cobra_subcommands () =
Printf.printf "\n== Go/Cobra subcommands ==\n";
let r = parse {|Common Commands:
run Create and run a new container from an image
exec Execute a command in a running container
build Build an image from a Dockerfile
|} in
check "has subcommands" (List.length r.subcommands > 0)
let test_busybox_tab () =
Printf.printf "\n== Busybox tab-indented ==\n";
let r = parse "\t-1\tOne column output\n\t-a\tInclude names starting with .\n" in
check "two entries" (List.length r.entries = 2);
check "first is -1" ((List.hd r.entries).switch = Short '1')
let test_no_debug_prints () =
Printf.printf "\n== No debug side effects ==\n";
(* The old parser had print_endline at module load time.
If we got here without "opt param is running" on stdout, we're good. *)
check "no debug prints" true
(* --- Manpage parser tests --- *)
let test_manpage_tp_style () =
Printf.printf "\n== Manpage .TP style ==\n";
let groff = {|.SH OPTIONS
.TP
\fB\-a\fR, \fB\-\-all\fR
do not ignore entries starting with .
.TP
\fB\-A\fR, \fB\-\-almost\-all\fR
do not list implied . and ..
.TP
\fB\-\-block\-size\fR=\fISIZE\fR
with \fB\-l\fR, scale sizes by SIZE
.SH AUTHOR
Written by someone.
|} in
let result = parse_manpage_string groff in
check "three entries" (List.length result.entries = 3);
if List.length result.entries >= 1 then begin
let e = List.hd result.entries in
check "first is -a/--all" (e.switch = Both ('a', "all"));
check "first desc" (String.length e.desc > 0)
end;
if List.length result.entries >= 3 then begin
let e = List.nth result.entries 2 in
check "block-size switch" (e.switch = Long "block-size");
check "block-size param" (e.param = Some (Mandatory "SIZE"))
end
let test_manpage_ip_style () =
Printf.printf "\n== Manpage .IP style ==\n";
let groff = {|.SH OPTIONS
.IP "\fB\-k\fR, \fB\-\-insecure\fR"
Allow insecure connections.
.IP "\fB\-o\fR, \fB\-\-output\fR \fIfile\fR"
Write output to file.
.SH SEE ALSO
|} in
let result = parse_manpage_string groff in
check "two entries" (List.length result.entries = 2);
if List.length result.entries >= 1 then begin
let e = List.hd result.entries in
check "first is -k/--insecure" (e.switch = Both ('k', "insecure"))
end
let test_manpage_groff_stripping () =
Printf.printf "\n== Groff escape stripping ==\n";
let s = strip_groff_escapes {|\fB\-\-color\fR[=\fIWHEN\fR]|} in
check "font escapes removed" (not (String.contains s 'f' && String.contains s 'B'));
check "dashes converted" (String.contains s '-');
let s2 = strip_groff_escapes {|\(aqhello\(aq|} in
check "aq -> quote" (String.contains s2 '\'')
let test_manpage_empty_options () =
Printf.printf "\n== Manpage with no OPTIONS section ==\n";
let groff = {|.SH NAME
foo \- does stuff
.SH DESCRIPTION
Does stuff.
|} in
let result = parse_manpage_string groff in
check "no entries" (List.length result.entries = 0)
let test_slash_switch_separator () =
Printf.printf "\n== Slash switch separator (--long / -s) ==\n";
let r = parse " --verbose / -v Increase verbosity\n" in
check "one entry" (List.length r.entries = 1);
let e = List.hd r.entries in
check "both switch" (e.switch = Both ('v', "verbose"));
check "no param" (e.param = None);
check "desc" (e.desc = "Increase verbosity")
let test_manpage_nix3_style () =
Printf.printf "\n== Manpage nix3 style ==\n";
let groff = {|.SH Options
.SS Logging-related options
.IP "\(bu" 3
.UR #opt-verbose
\f(CR--verbose\fR
.UE
/ \f(CR-v\fR
.IP
Increase the logging verbosity level.
.IP "\(bu" 3
.UR #opt-quiet
\f(CR--quiet\fR
.UE
.IP
Decrease the logging verbosity level.
.SH SEE ALSO
|} in
let result = parse_manpage_string groff in
check "two entries" (List.length result.entries = 2);
if List.length result.entries >= 1 then begin
let e = List.hd result.entries in
check "verbose is Both" (e.switch = Both ('v', "verbose"));
check "verbose desc" (String.length e.desc > 0)
end;
if List.length result.entries >= 2 then begin
let e = List.nth result.entries 1 in
check "quiet is Long" (e.switch = Long "quiet");
check "quiet desc" (String.length e.desc > 0)
end
let test_manpage_nix3_with_params () =
Printf.printf "\n== Manpage nix3 with params ==\n";
let groff = {|.SH Options
.IP "\(bu" 3
.UR #opt-arg
\f(CR--arg\fR
.UE
\fIname\fR \fIexpr\fR
.IP
Pass the value as the argument name to Nix functions.
.IP "\(bu" 3
.UR #opt-include
\f(CR--include\fR
.UE
/ \f(CR-I\fR \fIpath\fR
.IP
Add path to search path entries.
.IP
This option may be given multiple times.
.SH SEE ALSO
|} in
let result = parse_manpage_string groff in
check "two entries" (List.length result.entries = 2);
if List.length result.entries >= 1 then begin
let e = List.hd result.entries in
check "arg is Long" (e.switch = Long "arg");
check "arg has param" (e.param <> None)
end;
if List.length result.entries >= 2 then begin
let e = List.nth result.entries 1 in
check "include is Both" (e.switch = Both ('I', "include"));
check "include has path param" (e.param = Some (Mandatory "path"))
end
let test_synopsis_subcommand () =
Printf.printf "\n== SYNOPSIS subcommand detection ==\n";
let groff = {|.SH "SYNOPSIS"
.sp
.nf
\fBgit\fR \fBcommit\fR [\fB\-a\fR | \fB\-\-interactive\fR]
.fi
.SH "DESCRIPTION"
|} in
let cmd = extract_synopsis_command groff in
check "detected git commit" (cmd = Some "git commit")
let test_synopsis_standalone () =
Printf.printf "\n== SYNOPSIS standalone command ==\n";
let groff = {|.SH Synopsis
.LP
\f(CRnix-build\fR [\fIpaths\fR]
.SH Description
|} in
let cmd = extract_synopsis_command groff in
check "detected nix-build" (cmd = Some "nix-build")
let test_synopsis_nix3 () =
Printf.printf "\n== SYNOPSIS nix3 subcommand ==\n";
let groff = {|.SH Synopsis
.LP
\f(CRnix run\fR [\fIoption\fR] \fIinstallable\fR
.SH Description
|} in
let cmd = extract_synopsis_command groff in
check "detected nix run" (cmd = Some "nix run")
(* --- Nushell generation tests --- *)
let contains s sub =
try
let _ = Str.search_forward (Str.regexp_string sub) s 0 in true
with Not_found -> false
let test_nushell_basic () =
Printf.printf "\n== Nushell basic extern ==\n";
let r = parse " -a, --all do not ignore entries starting with .\n" in
let nu = generate_extern "ls" r in
check "has extern" (contains nu "export extern \"ls\"");
check "has --all(-a)" (contains nu "--all(-a)");
check "has comment" (contains nu "# do not ignore")
let test_nushell_param_types () =
Printf.printf "\n== Nushell param type mapping ==\n";
let r = parse {| -w, --width=COLS set output width
--block-size=SIZE scale sizes
-o, --output FILE output file
|} in
let nu = generate_extern "ls" r in
check "COLS -> int" (contains nu "--width(-w): int");
check "SIZE -> string" (contains nu "--block-size: string");
check "FILE -> path" (contains nu "--output(-o): path")
let test_nushell_subcommands () =
Printf.printf "\n== Nushell subcommands ==\n";
let r = parse {|Common Commands:
run Create and run a new container
exec Execute a command
Flags:
-D, --debug Enable debug mode
|} in
let nu = generate_extern "docker" r in
check "has main extern" (contains nu "export extern \"docker\"");
check "has --debug" (contains nu "--debug(-D)");
check "has run subcommand" (contains nu "export extern \"docker run\"");
check "has exec subcommand" (contains nu "export extern \"docker exec\"")
let test_nushell_from_manpage () =
Printf.printf "\n== Nushell from manpage ==\n";
let groff = {|.SH OPTIONS
.TP
\fB\-a\fR, \fB\-\-all\fR
do not ignore entries starting with .
.TP
\fB\-\-block\-size\fR=\fISIZE\fR
scale sizes by SIZE
.SH AUTHOR
|} in
let result = parse_manpage_string groff in
let nu = generate_extern "ls" result in
check "has extern" (contains nu "export extern \"ls\"");
check "has --all(-a)" (contains nu "--all(-a)");
check "has --block-size" (contains nu "--block-size: string")
let test_nushell_module () =
Printf.printf "\n== Nushell module wrapper ==\n";
let r = parse " -v, --verbose verbose output\n" in
let nu = generate_module "myapp" r in
check "has module" (contains nu "module myapp-completions");
check "has extern inside" (contains nu "export extern \"myapp\"");
check "has flag" (contains nu "--verbose(-v)")
let test_dedup_entries () =
Printf.printf "\n== Deduplication ==\n";
let r = parse {| -v, --verbose verbose output
--verbose verbose mode
-v be verbose
|} in
let nu = generate_extern "test" r in
(* Count occurrences of --verbose *)
let count =
let re = Str.regexp_string "--verbose" in
let n = ref 0 in
let i = ref 0 in
(try while true do
let _ = Str.search_forward re nu !i in
incr n; i := Str.match_end ()
done with Not_found -> ());
!n
in
check "verbose appears once" (count = 1);
check "best version kept (Both)" (contains nu "--verbose(-v)")
let test_dedup_manpage () =
Printf.printf "\n== Dedup from manpage ==\n";
let groff = {|.SH OPTIONS
.TP
\fB\-v\fR, \fB\-\-verbose\fR
Be verbose.
.SH DESCRIPTION
Use \fB\-v\fR for verbose output.
Use \fB\-\-verbose\fR to see more.
|} in
let result = parse_manpage_string groff in
let nu = generate_extern "test" result in
check "has --verbose(-v)" (contains nu "--verbose(-v)");
(* Should not have standalone -v or duplicate --verbose *)
let lines = String.split_on_char '\n' nu in
let verbose_lines = List.filter (fun l -> contains l "verbose") lines in
check "only one verbose line" (List.length verbose_lines = 1)
let test_font_boundary_spacing () =
Printf.printf "\n== Font boundary spacing ==\n";
(* \fB--max-results\fR\fIcount\fR should become "--max-results count" *)
let s = strip_groff_escapes {|\fB\-\-max\-results\fR\fIcount\fR|} in
check "has space before param" (contains s "--max-results count");
(* \fB--color\fR[=\fIWHEN\fR] should NOT insert space before = *)
let s2 = strip_groff_escapes {|\fB\-\-color\fR[=\fIWHEN\fR]|} in
check "no space before =" (contains s2 "--color[=WHEN]")
let () =
Printf.printf "Running help parser tests...\n";
test_gnu_basic ();
test_gnu_eq_param ();
test_gnu_opt_param ();
test_underscore_param ();
test_short_only ();
test_long_only ();
test_multiline_desc ();
test_multiple_entries ();
test_clap_short_sections ();
test_clap_long_style ();
test_clap_long_angle_param ();
test_space_upper_param ();
test_go_cobra_flags ();
test_go_cobra_subcommands ();
test_busybox_tab ();
test_no_debug_prints ();
Printf.printf "\nRunning manpage parser tests...\n";
test_manpage_tp_style ();
test_manpage_ip_style ();
test_manpage_groff_stripping ();
test_manpage_empty_options ();
test_slash_switch_separator ();
test_manpage_nix3_style ();
test_manpage_nix3_with_params ();
test_synopsis_subcommand ();
test_synopsis_standalone ();
test_synopsis_nix3 ();
Printf.printf "\nRunning nushell generation tests...\n";
test_nushell_basic ();
test_nushell_param_types ();
test_nushell_subcommands ();
test_nushell_from_manpage ();
test_nushell_module ();
Printf.printf "\nRunning dedup and font tests...\n";
test_dedup_entries ();
test_dedup_manpage ();
test_font_boundary_spacing ();
Printf.printf "\n=== Results: %d passed, %d failed ===\n" !passes !failures;
if !failures > 0 then exit 1