// getopttest — exercises lib/getopt. Run with // `out/bin/ww run lib/getopt/getopttest.ww`. // // Every @test enumerates parallel `[N]T` arrays of inputs and // expectations, then iterates one body across them. Parallel arrays // (rather than `[N]struct{...}`) sidestep the cstage cgen's chained // `arr[i].field` store gap (task #6). // // Reads of [[command.optsptr]] go through `&cmd.optsptr[i]` rather // than `cmd.optsptr[i].field` directly: the cstage cgen emits a bare // MOVQ for a pointer-indexed struct-field read, so packed i32 fields // (flag rune at offset 0) silently pick up the next 4 bytes (str.ptr // low half) in the upper 32 bits. The chained-N_DOT-through-*struct // path uses MOVL and avoids the over-read. // // Guards on a possibly-nil [[command.argsptr]] are split into nested // `if`s rather than `argslen > 0 && !streq(argsptr[0], ...)`. The // flattened `&&` form is now legal — both cgen stages short-circuit // per task #15 — but the nested-if shape was the original workaround // and is preserved here as a regression marker. package getopt_test; import getopt; import io; import memio; import strings; fn streq(a: str, b: str) bool = { if (a.len != b.len) { return false; }; let i: i32 = 0; for (i < a.len) { if (a[i] != b[i]) { return false; }; i += 1; }; return true; }; // ---- flagcluster: -Fahs files.txt → 4 flags + 1 arg -------------------- @test fn flagcluster() void = { let helps: [5]getopt.help; getopt.cmdhelp(&helps[0], "list files"); getopt.flaghelp(&helps[1], 'F': rune, "do F"); getopt.flaghelp(&helps[2], 'a': rune, "do a"); getopt.flaghelp(&helps[3], 'h': rune, "do h"); getopt.flaghelp(&helps[4], 's': rune, "do s"); let argv: [3]str; argv[0] = "ls"; argv[1] = "-Fahs"; argv[2] = "files.txt"; let cmd: getopt.command; let r: (void | getopt.error) = getopt.tryparse(&cmd, argv[0:3], helps[0:5]); match (r) { case void => {}; case let e: getopt.error => abort(); }; assert(!(cmd.optslen != 4)); // (wantflag, wantval) parallel arrays, indexed by option order. let wantf: [4]u8; let wantv: [4]str; wantf[0] = 'F': u8; wantv[0] = ""; wantf[1] = 'a': u8; wantv[1] = ""; wantf[2] = 'h': u8; wantv[2] = ""; wantf[3] = 's': u8; wantv[3] = ""; let i: i32 = 0; for (i < 4) { let p: *getopt.option = &cmd.optsptr[i]; assert(!((p.flag: u8) != wantf[i])); assert(!(!streq(p.value, wantv[i]))); i += 1; }; assert(!(cmd.argslen != 1)); assert(!(!streq(cmd.argsptr[0], "files.txt"))); getopt.finish(&cmd); }; // ---- paramflag: glued + separated arguments ---------------------------- @test fn paramflag() void = { let helps: [3]getopt.help; getopt.cmdhelp(&helps[0], "edit"); getopt.paramhelp(&helps[1], 'e': rune, "script", "script"); getopt.paramhelp(&helps[2], 'f': rune, "file", "script file"); let argv: [5]str; argv[0] = "sed"; argv[1] = "-e"; argv[2] = "s/foo/bar/"; argv[3] = "-f/tmp/x.sed"; argv[4] = "-"; let cmd: getopt.command; let r: (void | getopt.error) = getopt.tryparse(&cmd, argv[0:5], helps[0:3]); match (r) { case void => {}; case let e: getopt.error => abort(); }; assert(!(cmd.optslen != 2)); let wantf: [2]u8; let wantv: [2]str; wantf[0] = 'e': u8; wantv[0] = "s/foo/bar/"; wantf[1] = 'f': u8; wantv[1] = "/tmp/x.sed"; let i: i32 = 0; for (i < 2) { let p: *getopt.option = &cmd.optsptr[i]; assert(!((p.flag: u8) != wantf[i])); assert(!(!streq(p.value, wantv[i]))); i += 1; }; assert(!(cmd.argslen != 1)); assert(!(!streq(cmd.argsptr[0], "-"))); getopt.finish(&cmd); }; // ---- separator: -- ends option processing ------------------------------ // // `--` in three positions: after a flag, immediately after argv[0], // and at the tail with nothing trailing. Flat-packed argv per // errortable's pattern; rows expect distinct (optslen, argslen, // args[0]) outcomes. @test fn separator() void = { let helps: [2]getopt.help; getopt.cmdhelp(&helps[0], "list"); getopt.flaghelp(&helps[1], 'F': rune, "do F"); // row 0: ["ls", "-F", "--", "-name"] → 1 opt 'F', 1 arg "-name" // row 1: ["ls", "--", "-F"] → 0 opts, 1 arg "-F" // row 2: ["ls", "--"] → 0 opts, 0 args let srcs: [9]str; srcs[0]="ls"; srcs[1]="-F"; srcs[2]="--"; srcs[3]="-name"; srcs[4]="ls"; srcs[5]="--"; srcs[6]="-F"; srcs[7]="ls"; srcs[8]="--"; let argo: [3]i32; let argn: [3]i32; let wantopts: [3]i32; let wantargs: [3]i32; let wantarg0: [3]str; argo[0]=0; argn[0]=4; wantopts[0]=1; wantargs[0]=1; wantarg0[0]="-name"; argo[1]=4; argn[1]=3; wantopts[1]=0; wantargs[1]=1; wantarg0[1]="-F"; argo[2]=7; argn[2]=2; wantopts[2]=0; wantargs[2]=0; wantarg0[2]=""; let i: i32 = 0; for (i < 3) { let argv: []str; argv.ptr = &srcs[argo[i]]; argv.len = argn[i]; argv.cap = argn[i]; let cmd: getopt.command; let r: (void | getopt.error) = getopt.tryparse(&cmd, argv, helps[0:2]); match (r) { case void => {}; case let e: getopt.error => abort(); }; assert(!(cmd.optslen != wantopts[i])); assert(!(cmd.argslen != wantargs[i])); if (cmd.argslen > 0) { assert(!(!streq(cmd.argsptr[0], wantarg0[i]))); }; getopt.finish(&cmd); i += 1; }; }; // ---- repeatedflag: -vvv → 3 v opts ------------------------------------- @test fn repeatedflag() void = { let helps: [2]getopt.help; getopt.cmdhelp(&helps[0], "verbose count"); getopt.flaghelp(&helps[1], 'v': rune, "verbosity"); let argv: [2]str; argv[0] = "x"; argv[1] = "-vvv"; let cmd: getopt.command; let r: (void | getopt.error) = getopt.tryparse(&cmd, argv[0:2], helps[0:2]); match (r) { case void => {}; case let e: getopt.error => abort(); }; assert(!(cmd.optslen != 3)); let i: i32 = 0; for (i < 3) { let p: *getopt.option = &cmd.optsptr[i]; assert(!((p.flag: u8) != ('v': u8))); assert(!(p.value.len != 0)); i += 1; }; assert(!(cmd.argslen != 0)); getopt.finish(&cmd); }; // ---- baredash: "-" alone is a positional -------------------------------- // // Bare `-` in three positions: trailing after a flag, leading (which // halts option scanning), and followed by what looks like a flag // (also halts — first non-flag wins). @test fn baredash() void = { let helps: [2]getopt.help; getopt.cmdhelp(&helps[0], "cat"); getopt.flaghelp(&helps[1], 'v': rune, "verbose"); // row 0: ["cat", "-v", "-"] → 1 opt 'v', 1 arg "-" // row 1: ["cat", "-"] → 0 opts, 1 arg "-" // row 2: ["cat", "-", "-v"] → 0 opts, 2 args "-","-v" let srcs: [8]str; srcs[0]="cat"; srcs[1]="-v"; srcs[2]="-"; srcs[3]="cat"; srcs[4]="-"; srcs[5]="cat"; srcs[6]="-"; srcs[7]="-v"; let argo: [3]i32; let argn: [3]i32; let wantopts: [3]i32; let wantargs: [3]i32; argo[0]=0; argn[0]=3; wantopts[0]=1; wantargs[0]=1; argo[1]=3; argn[1]=2; wantopts[1]=0; wantargs[1]=1; argo[2]=5; argn[2]=3; wantopts[2]=0; wantargs[2]=2; let i: i32 = 0; for (i < 3) { let argv: []str; argv.ptr = &srcs[argo[i]]; argv.len = argn[i]; argv.cap = argn[i]; let cmd: getopt.command; let r: (void | getopt.error) = getopt.tryparse(&cmd, argv, helps[0:2]); match (r) { case void => {}; case let e: getopt.error => abort(); }; assert(!(cmd.optslen != wantopts[i])); assert(!(cmd.argslen != wantargs[i])); // First positional is always the bare "-" in these rows. if (cmd.argslen > 0) { assert(!(!streq(cmd.argsptr[0], "-"))); }; getopt.finish(&cmd); i += 1; }; }; // ---- errortable: unknownopt + requiresarg variants --------------------- // // Parallel arrays per row: argv flat-packed into `srcs`, with `argo` // the offset and `argn` the count for each row. @test fn errortable() void = { let helps: [3]getopt.help; getopt.cmdhelp(&helps[0], "prog"); getopt.flaghelp(&helps[1], 'v': rune, "verbose"); getopt.paramhelp(&helps[2], 'e': rune, "expr", "expression"); // Flat-packed argv for 3 cases: // row 0: ["prog", "-x"] → UNKNOWNOPT 'x' // row 1: ["prog", "-e"] → REQUIRESARG 'e' // row 2: ["prog", "-v", "-e"] → REQUIRESARG 'e' (cluster ends mid-param) let srcs: [7]str; srcs[0] = "prog"; srcs[1] = "-x"; srcs[2] = "prog"; srcs[3] = "-e"; srcs[4] = "prog"; srcs[5] = "-v"; srcs[6] = "-e"; let argo: [3]i32; let argn: [3]i32; let wantk: [3]i32; let wantf: [3]u8; argo[0] = 0; argn[0] = 2; wantk[0] = 1; wantf[0] = 'x': u8; // UNKNOWNOPT=1 argo[1] = 2; argn[1] = 2; wantk[1] = 0; wantf[1] = 'e': u8; // REQUIRESARG=0 argo[2] = 4; argn[2] = 3; wantk[2] = 0; wantf[2] = 'e': u8; let i: i32 = 0; for (i < 3) { let argv: []str; argv.ptr = &srcs[argo[i]]; argv.len = argn[i]; argv.cap = argn[i]; let cmd: getopt.command; let r: (void | getopt.error) = getopt.tryparse(&cmd, argv, helps[0:3]); match (r) { case void => abort(); case let e: getopt.error => { assert(!((e.kind: i32) != wantk[i])); assert(!((e.flag: u8) != wantf[i])); assert(!(!streq(e.name, "prog"))); }; }; getopt.finish(&cmd); i += 1; }; }; // ---- strerrortext: render both error kinds ----------------------------- // // Parallel rows over (kind, flag, name) → expected message. `kind` // rides as i32 because the errorkind enum doesn't yet index an // array element directly. @test fn strerrortext() void = { let kinds: [2]i32; let flags: [2]u8; let names: [2]str; let wants: [2]str; kinds[0]=1; flags[0]='x': u8; names[0]="prog"; wants[0]="prog: unrecognized option: -x"; kinds[1]=0; flags[1]='e': u8; names[1]="sed"; wants[1]="sed: option -e requires an argument"; let i: i32 = 0; for (i < 2) { let e: getopt.error; if (kinds[i] == 0) { e.kind = getopt.errorkind.REQUIRESARG; }; if (kinds[i] == 1) { e.kind = getopt.errorkind.UNKNOWNOPT; }; e.flag = flags[i]: rune; e.name = names[i]; let s: str = getopt.strerror(&e); assert(!(!streq(s, wants[i]))); i += 1; }; }; // ---- nooptionsbare: argv with only program name + positionals ---------- // // Positional-only argv at three lengths: two positionals, one // positional, and the program-name-only edge. @test fn nooptionsbare() void = { let helps: [2]getopt.help; getopt.cmdhelp(&helps[0], "echo"); getopt.flaghelp(&helps[1], 'n': rune, "no newline"); // row 0: ["echo", "hello", "world"] → 2 args, args[0]="hello" // row 1: ["echo", "a"] → 1 arg, args[0]="a" // row 2: ["echo"] → 0 args let srcs: [6]str; srcs[0]="echo"; srcs[1]="hello"; srcs[2]="world"; srcs[3]="echo"; srcs[4]="a"; srcs[5]="echo"; let argo: [3]i32; let argn: [3]i32; let wantargs: [3]i32; let wantarg0: [3]str; argo[0]=0; argn[0]=3; wantargs[0]=2; wantarg0[0]="hello"; argo[1]=3; argn[1]=2; wantargs[1]=1; wantarg0[1]="a"; argo[2]=5; argn[2]=1; wantargs[2]=0; wantarg0[2]=""; let i: i32 = 0; for (i < 3) { let argv: []str; argv.ptr = &srcs[argo[i]]; argv.len = argn[i]; argv.cap = argn[i]; let cmd: getopt.command; let r: (void | getopt.error) = getopt.tryparse(&cmd, argv, helps[0:2]); match (r) { case void => {}; case let e: getopt.error => abort(); }; assert(!(cmd.optslen != 0)); assert(!(cmd.argslen != wantargs[i])); if (cmd.argslen > 0) { assert(!(!streq(cmd.argsptr[0], wantarg0[i]))); }; getopt.finish(&cmd); i += 1; }; }; // ---- printusage_cases: table of width-measure paths -------------------- // // ref/hare/getopt/getopts.ha:202-265. // Row 0: short line (≤72) — auto-[-h] + FLAG cluster, no wrap. // Row 1: long line (>72) — PARAM slots each prefixed with \n\t. // // Help entries are packed into a flat [7]getopt.help array; hoff[i] / // hcnt[i] mark each row's slice. Parallel [2]str arrays hold name and // expected output per row. @test fn printusage_cases() void = { let helps: [7]getopt.help; // Row 0: CMD + FLAG 'F' + FLAG 'a' getopt.cmdhelp(&helps[0], "list files"); getopt.flaghelp(&helps[1], 'F': rune, "do F"); getopt.flaghelp(&helps[2], 'a': rune, "do a"); // Row 1: CMD + 3 PARAMs (no-indent measurement 77 > 72) getopt.cmdhelp(&helps[3], "process files"); getopt.paramhelp(&helps[4], 'e': rune, "expression", "run expr"); getopt.paramhelp(&helps[5], 'o': rune, "output-file", "output"); getopt.paramhelp(&helps[6], 'i': rune, "input-path", "input"); let names: [2]str; let wants: [2]str; let hoff: [2]i32; let hcnt: [2]i32; names[0] = "ls"; wants[0] = "Usage: ls [-hFa]\n"; hoff[0] = 0; hcnt[0] = 3; names[1] = "myprogram"; wants[1] = "Usage: myprogram [-h]\n\t [-e ]\n\t [-o ]\n\t [-i ]\n"; hoff[1] = 3; hcnt[1] = 4; let buf: [512]u8; let i: i32 = 0; for (i < 2) { let ms: memio.stream = memio.fixed(buf[0:512]); let s: io.stream = &ms.vt; let h: io.handle = s: io.handle; let hs: []getopt.help; hs.ptr = &helps[hoff[i]]; hs.len = hcnt[i]; hs.cap = hcnt[i]; match (getopt.printusage(h, names[i], hs)) { case void => {}; case let e: io.error => abort(); }; let got: str = memio.string(&ms); assert(!(!streq(got, wants[i]))); i += 1; }; }; // ---- printhelp_cases: table of help-output paths ---------------------- // // ref/hare/getopt/getopts.ha:281-314. // Row 0: empty help slice → early return, no output. // Row 1: full help (CMD+FLAG+PARAM+CMD) → summary + usage + option list. // // help[0..4] holds the full-case entries; the empty-case uses a 0-len // slice from the same array (hcnt[0]=0). @test fn printhelp_cases() void = { let helps: [4]getopt.help; getopt.cmdhelp(&helps[0], "concatenate files"); getopt.flaghelp(&helps[1], 'n': rune, "number lines"); getopt.paramhelp(&helps[2], 'o': rune, "output", "output file"); getopt.cmdhelp(&helps[3], "files..."); let names: [2]str; let wants: [2]str; let hcnt: [2]i32; names[0] = "prog"; wants[0] = ""; hcnt[0] = 0; names[1] = "cat"; wants[1] = "cat: concatenate files\n\nUsage: cat [-hn] [-o ] files...\n\n-h: print this help text\n-n: number lines\n-o : output file\n"; hcnt[1] = 4; let buf: [512]u8; let i: i32 = 0; for (i < 2) { let ms: memio.stream = memio.fixed(buf[0:512]); let s: io.stream = &ms.vt; let h: io.handle = s: io.handle; match (getopt.printhelp(h, names[i], helps[0:hcnt[i]])) { case void => {}; case let e: io.error => abort(); }; let got: str = memio.string(&ms); assert(!(!streq(got, wants[i]))); i += 1; }; }; export fn main() i32 = { flagcluster(); paramflag(); separator(); repeatedflag(); baredash(); errortable(); strerrortext(); nooptionsbare(); printusage_cases(); printhelp_cases(); return 0; };