vole
Clone
Read only, no credentials:
https://rodent.software/vole.git
Write over SSH with a registered key:
git clone ssh://git@rodent.software:2222/vole.git
A free and open-source forge that lives in your Git repo.
| 1 | package main |
| 2 | |
| 3 | import ( |
| 4 | "testing" |
| 5 | "time" |
| 6 | |
| 7 | "rodent.software/vole/metadata" |
| 8 | ) |
| 9 | |
| 10 | func issueForTest() *metadata.Issue { |
| 11 | return &metadata.Issue{ |
| 12 | ID: "01M5JR2JK29KKMAA1T9MDNTPBP", |
| 13 | State: "open", |
| 14 | Author: "keenan", |
| 15 | Title: "A title", |
| 16 | Labels: []string{"bug", "ui"}, |
| 17 | Created: time.Unix(1700000000, 0).UTC(), |
| 18 | } |
| 19 | } |
| 20 | |
| 21 | func issueSpecs() []flagSpec { |
| 22 | return []flagSpec{ |
| 23 | {long: "title", short: "t"}, |
| 24 | {long: "message", short: "m"}, |
| 25 | {long: "file", short: "F"}, |
| 26 | {long: "label", short: "l"}, |
| 27 | {long: "no-push", boolean: true}, |
| 28 | } |
| 29 | } |
| 30 | |
| 31 | func TestFlagForms(t *testing.T) { |
| 32 | for name, args := range map[string][]string{ |
| 33 | "separate long": {"--title", "hello"}, |
| 34 | "stuck long": {"--title=hello"}, |
| 35 | "separate short": {"-t", "hello"}, |
| 36 | "stuck short": {"-thello"}, |
| 37 | } { |
| 38 | f, err := parseFlags(args, issueSpecs()...) |
| 39 | if err != nil { |
| 40 | t.Fatalf("%s: %v", name, err) |
| 41 | } |
| 42 | if got := f.one("title"); got != "hello" { |
| 43 | t.Fatalf("%s: title = %q", name, got) |
| 44 | } |
| 45 | } |
| 46 | } |
| 47 | |
| 48 | func TestRepeatableFlag(t *testing.T) { |
| 49 | f, err := parseFlags([]string{"-l", "bug", "--label", "ui", "-lperf"}, issueSpecs()...) |
| 50 | if err != nil { |
| 51 | t.Fatal(err) |
| 52 | } |
| 53 | got := f.all("label") |
| 54 | if len(got) != 3 || got[0] != "bug" || got[1] != "ui" || got[2] != "perf" { |
| 55 | t.Fatalf("labels = %v", got) |
| 56 | } |
| 57 | } |
| 58 | |
| 59 | func TestBooleanFlag(t *testing.T) { |
| 60 | f, err := parseFlags([]string{"--no-push"}, issueSpecs()...) |
| 61 | if err != nil { |
| 62 | t.Fatal(err) |
| 63 | } |
| 64 | if !f.has("no-push") { |
| 65 | t.Fatal("--no-push not set") |
| 66 | } |
| 67 | if _, err := parseFlags([]string{"--no-push=yes"}, issueSpecs()...); err == nil { |
| 68 | t.Fatal("a boolean flag accepted a value") |
| 69 | } |
| 70 | } |
| 71 | |
| 72 | // TestOptionsAnywhere: git accepts options after arguments, so Vole does too. |
| 73 | // Anything else is a local dialect, and `git commit -m msg path` is the shape |
| 74 | // people already type. |
| 75 | func TestOptionsAnywhere(t *testing.T) { |
| 76 | f, err := parseFlags([]string{"01M5", "-m", "text"}, issueSpecs()...) |
| 77 | if err != nil { |
| 78 | t.Fatalf("option after argument was refused: %v", err) |
| 79 | } |
| 80 | if f.one("message") != "text" || len(f.rest) != 1 || f.rest[0] != "01M5" { |
| 81 | t.Fatalf("rest = %v, message = %q", f.rest, f.one("message")) |
| 82 | } |
| 83 | |
| 84 | f, err = parseFlags([]string{"-m", "text", "01M5"}, issueSpecs()...) |
| 85 | if err != nil { |
| 86 | t.Fatalf("options first failed: %v", err) |
| 87 | } |
| 88 | if len(f.rest) != 1 || f.rest[0] != "01M5" { |
| 89 | t.Fatalf("rest = %v", f.rest) |
| 90 | } |
| 91 | } |
| 92 | |
| 93 | func TestEndOfOptions(t *testing.T) { |
| 94 | for _, term := range []string{"--", "--end-of-options"} { |
| 95 | f, err := parseFlags([]string{term, "--title", "-m"}, issueSpecs()...) |
| 96 | if err != nil { |
| 97 | t.Fatalf("%s: %v", term, err) |
| 98 | } |
| 99 | if len(f.rest) != 2 || f.rest[0] != "--title" || f.rest[1] != "-m" { |
| 100 | t.Fatalf("%s: rest = %v", term, f.rest) |
| 101 | } |
| 102 | if f.one("title") != "" { |
| 103 | t.Fatal("option after the terminator was parsed") |
| 104 | } |
| 105 | } |
| 106 | } |
| 107 | |
| 108 | func TestUsageErrors(t *testing.T) { |
| 109 | for name, args := range map[string][]string{ |
| 110 | "unknown long": {"--nope"}, |
| 111 | "unknown short": {"-Z"}, |
| 112 | "missing value": {"--title"}, |
| 113 | "equals on a short opt": {"-l=perf"}, |
| 114 | } { |
| 115 | _, err := parseFlags(args, issueSpecs()...) |
| 116 | if err == nil { |
| 117 | t.Fatalf("%s: expected an error", name) |
| 118 | } |
| 119 | if !isUsageErr(err) { |
| 120 | t.Fatalf("%s: error is %T, want usageErr", name, err) |
| 121 | } |
| 122 | } |
| 123 | } |
| 124 | |
| 125 | // TestShortOptionsBundle: `-am msg` is `-a -m msg`, as it is in git. The |
| 126 | // option that takes a value consumes the rest of the cluster, so `-mm` still |
| 127 | // means `-m m`. |
| 128 | func TestShortOptionsBundle(t *testing.T) { |
| 129 | specs := []flagSpec{ |
| 130 | {long: "message", short: "m"}, |
| 131 | {long: "quiet", short: "q", boolean: true}, |
| 132 | {long: "all", short: "a", boolean: true}, |
| 133 | } |
| 134 | f, err := parseFlags([]string{"-qamhello"}, specs...) |
| 135 | if err != nil { |
| 136 | t.Fatalf("-qamhello: %v", err) |
| 137 | } |
| 138 | if !f.has("quiet") || !f.has("all") { |
| 139 | t.Fatalf("bundle did not set booleans: %+v", f.bools) |
| 140 | } |
| 141 | if f.one("message") != "hello" { |
| 142 | t.Fatalf("message = %q, want \"hello\"", f.one("message")) |
| 143 | } |
| 144 | |
| 145 | f, err = parseFlags([]string{"-mm"}, specs...) |
| 146 | if err != nil { |
| 147 | t.Fatalf("-mm should be the stuck form: %v", err) |
| 148 | } |
| 149 | if f.one("message") != "m" { |
| 150 | t.Fatalf("message = %q, want \"m\"", f.one("message")) |
| 151 | } |
| 152 | |
| 153 | if _, err := parseFlags([]string{"-qZ"}, specs...); err == nil || !isUsageErr(err) { |
| 154 | t.Fatalf("unknown option inside a cluster = %v, want a usage error", err) |
| 155 | } |
| 156 | } |
| 157 | |
| 158 | // TestConfigFlagSetsPath: the parser stores flags generically, so --config has to |
| 159 | // be copied to cfgPath explicitly. Missing that silently discards an operator's |
| 160 | // chosen instance and falls back to discovery. |
| 161 | func TestConfigFlagSetsPath(t *testing.T) { |
| 162 | for _, args := range [][]string{ |
| 163 | {"--config", "/tmp/x.toml"}, |
| 164 | {"-c", "/tmp/x.toml"}, |
| 165 | {"--config=/tmp/x.toml"}, |
| 166 | } { |
| 167 | f, err := parseFlags(args, configFlag()) |
| 168 | if err != nil { |
| 169 | t.Fatal(err) |
| 170 | } |
| 171 | if f.cfgPath != "/tmp/x.toml" { |
| 172 | t.Fatalf("%v: cfgPath = %q", args, f.cfgPath) |
| 173 | } |
| 174 | } |
| 175 | f, err := parseFlags(nil, configFlag()) |
| 176 | if err != nil { |
| 177 | t.Fatal(err) |
| 178 | } |
| 179 | if f.cfgPath != "" { |
| 180 | t.Fatalf("default cfgPath = %q, want empty (resolved later)", f.cfgPath) |
| 181 | } |
| 182 | } |
| 183 | |
| 184 | func TestHelpFlag(t *testing.T) { |
| 185 | for _, a := range []string{"-h", "--help"} { |
| 186 | f, err := parseFlags([]string{a}, issueSpecs()...) |
| 187 | if err != nil { |
| 188 | t.Fatal(err) |
| 189 | } |
| 190 | if !f.help { |
| 191 | t.Fatalf("%s did not set help", a) |
| 192 | } |
| 193 | } |
| 194 | } |
| 195 | |
| 196 | func TestMessageAndFileAreExclusive(t *testing.T) { |
| 197 | f, err := parseFlags([]string{"-m", "text", "-F", "file"}, issueSpecs()...) |
| 198 | if err != nil { |
| 199 | t.Fatal(err) |
| 200 | } |
| 201 | if _, err := readBody(f); err == nil { |
| 202 | t.Fatal("-m with -F was accepted") |
| 203 | } else if !isUsageErr(err) { |
| 204 | t.Fatalf("not a usage error: %v", err) |
| 205 | } |
| 206 | } |
| 207 | |
| 208 | // TestRepeatedMessagesBecomeParagraphs: git concatenates repeated -m values |
| 209 | // with a blank line, and silently dropping every one but the last is data loss. |
| 210 | func TestRepeatedMessagesBecomeParagraphs(t *testing.T) { |
| 211 | f, err := parseFlags([]string{"-m", "one", "--message", "two"}, issueSpecs()...) |
| 212 | if err != nil { |
| 213 | t.Fatal(err) |
| 214 | } |
| 215 | body, err := readBody(f) |
| 216 | if err != nil { |
| 217 | t.Fatal(err) |
| 218 | } |
| 219 | if body != "one\n\ntwo\n" { |
| 220 | t.Fatalf("body = %q, want %q", body, "one\n\ntwo\n") |
| 221 | } |
| 222 | } |
| 223 | |
| 224 | func TestFormatIssue(t *testing.T) { |
| 225 | iss := issueForTest() |
| 226 | // The numbering is derived from history, so the formatting test supplies it the |
| 227 | // same way the command does. |
| 228 | nums := metadata.RankAdditions([][]string{{"issues/" + iss.ID + "/issue.md"}}) |
| 229 | out := formatIssue("%(number) %(shortid) %(state) %(labels) %(title)", iss, nums) |
| 230 | want := "#1 01M5JR2J open bug,ui A title" |
| 231 | if out != want { |
| 232 | t.Fatalf("format = %q, want %q", out, want) |
| 233 | } |
| 234 | // A branch with no numbering must say so rather than print a number nobody |
| 235 | // assigned. |
| 236 | if out := formatIssue("%(number)", iss, nil); out != "#?" { |
| 237 | t.Fatalf("unnumbered format = %q, want %q", out, "#?") |
| 238 | } |
| 239 | } |
| 240 | |
| 241 | // TestFlagsBelongToTheSubcommand: options are parsed per group, but a flag that |
| 242 | // belongs to a sibling subcommand must still be refused. Silent acceptance |
| 243 | // reads as success, which is worse than a usage error. |
| 244 | func TestFlagsBelongToTheSubcommand(t *testing.T) { |
| 245 | f, err := parseFlags([]string{"--title", "x", "--no-push"}, issueSpecs()...) |
| 246 | if err != nil { |
| 247 | t.Fatal(err) |
| 248 | } |
| 249 | if err := f.checkAllowed("vole issue close", "remote", "no-push"); err == nil || !isUsageErr(err) { |
| 250 | t.Fatalf("close with --title = %v, want a usage error", err) |
| 251 | } |
| 252 | if err := f.checkAllowed("vole issue new", "title", "message", "file", "label", "no-push"); err != nil { |
| 253 | t.Fatalf("new with its own flags: %v", err) |
| 254 | } |
| 255 | } |
| 256 | |
| 257 | // TestAdminGlobalsFollowGitOptionForms: the admin namespace has its own small |
| 258 | // parser because it must find --remote before it knows the resource. It should |
| 259 | // accept the same short forms as the main parser, and `--` before the resource |
| 260 | // must not become the resource name. |
| 261 | func TestAdminGlobalsFollowGitOptionForms(t *testing.T) { |
| 262 | for _, args := range [][]string{ |
| 263 | {"--remote", "origin", "account", "list"}, |
| 264 | {"--remote=origin", "account", "list"}, |
| 265 | {"-r", "origin", "account", "list"}, |
| 266 | {"-rorigin", "account", "list"}, |
| 267 | {"--", "account", "list"}, |
| 268 | } { |
| 269 | remote, _, _, rest, err := splitAdminGlobals(args) |
| 270 | if err != nil { |
| 271 | t.Fatalf("%v: %v", args, err) |
| 272 | } |
| 273 | if args[0] == "--" { |
| 274 | if remote != "" || len(rest) != 2 || rest[0] != "account" { |
| 275 | t.Fatalf("%v: remote = %q, rest = %v", args, remote, rest) |
| 276 | } |
| 277 | continue |
| 278 | } |
| 279 | if remote != "origin" || len(rest) != 2 || rest[0] != "account" { |
| 280 | t.Fatalf("%v: remote = %q, rest = %v", args, remote, rest) |
| 281 | } |
| 282 | } |
| 283 | if _, _, _, _, err := splitAdminGlobals([]string{"-r=origin", "account", "list"}); err == nil || !isUsageErr(err) { |
| 284 | t.Fatalf("-r=origin = %v, want a usage error", err) |
| 285 | } |
| 286 | } |
| 287 |