package controllers import ( "strings" "testing" ) // TestParseSubmitIndexExpr covers requirements 1.5 and 2.1-2.9 for the // submit-time index expression parser (design: ApiSubmitExpr). func TestParseSubmitIndexExpr(t *testing.T) { longName := strings.Repeat("a", 4094) tooLongName := strings.Repeat("a", 4095) type want struct { arrayName string n int rule string // "" means success msgHas []string // substrings the error message must contain } cases := []struct { name string input string interactive bool limit int want want }{ // --- success (1.1, 1.2) --- {"range 1-N", "arr[1-3]", false, 1000, want{arrayName: "arr", n: 3}}, {"single 1", "arr[1]", false, 1000, want{arrayName: "arr", n: 1}}, {"range 1-1", "arr[1-1]", false, 1000, want{arrayName: "arr", n: 1}}, {"name charset", "a.B_c-9[1-2]", false, 1000, want{arrayName: "a.B_c-9", n: 2}}, {"name 4094 chars", longName + "[1-2]", false, 1000, want{arrayName: longName, n: 2}}, // --- leading zeros (1.5) --- {"leading zeros range", "arr[001-0003]", false, 1000, want{arrayName: "arr", n: 3}}, {"leading zeros single", "arr[01]", false, 1000, want{arrayName: "arr", n: 1}}, {"leading zeros limit", "arr[1-0001000]", false, 1000, want{arrayName: "arr", n: 1000}}, {"leading zeros zero", "arr[00-3]", false, 1000, want{rule: "2.5"}}, // --- limit edge (2.8) --- {"limit exact", "arr[1-1000]", false, 1000, want{arrayName: "arr", n: 1000}}, {"limit plus one", "arr[1-1001]", false, 1000, want{rule: "2.8", msgHas: []string{"1000"}}}, {"limit 32767 exact", "arr[1-32767]", false, 32767, want{arrayName: "arr", n: 32767}}, {"limit 32767 plus one", "arr[1-32768]", false, 32767, want{rule: "2.8", msgHas: []string{"32767"}}}, {"limit huge", "arr[1-99999999999999999999]", false, 1000, want{rule: "2.8", msgHas: []string{"1000"}}}, {"limit custom", "arr[1-6]", false, 5, want{rule: "2.8", msgHas: []string{"5"}}}, // --- 2.1 interactive --- {"interactive array", "arr[1-3]", true, 1000, want{rule: "2.1", msgHas: []string{"interactive jobs cannot be arrays"}}}, {"interactive malformed", "arr[", true, 1000, want{rule: "2.1", msgHas: []string{"interactive jobs cannot be arrays"}}}, {"interactive bad name", "a b]", true, 1000, want{rule: "2.1"}}, // --- 2.2 invalid name --- {"empty name", "[1-3]", false, 1000, want{rule: "2.2", msgHas: []string{"'[1-3]'", "array name is invalid"}}}, {"bad char in name", "a*b[1-3]", false, 1000, want{rule: "2.2", msgHas: []string{"'a*b[1-3]'", "array name is invalid"}}}, {"space in name", "a b[1-3]", false, 1000, want{rule: "2.2", msgHas: []string{"array name is invalid"}}}, {"leading space", " arr[1-3]", false, 1000, want{rule: "2.2", msgHas: []string{"array name is invalid"}}}, {"name too long", tooLongName + "[1-2]", false, 1000, want{rule: "2.2", msgHas: []string{"array name is invalid"}}}, {"only close bracket bad name", "a*b]", false, 1000, want{rule: "2.2", msgHas: []string{"array name is invalid"}}}, {"bad name beats list", "a/b[1,2]", false, 1000, want{rule: "2.2", msgHas: []string{"array name is invalid"}}}, // --- 2.2 malformed --- {"only close bracket", "arr]", false, 1000, want{rule: "2.2", msgHas: []string{"'arr]'", "array name is invalid"}}}, {"unterminated", "arr[1-3", false, 1000, want{rule: "2.2", msgHas: []string{"'arr[1-3'", "malformed"}}}, {"empty brackets", "arr[]", false, 1000, want{rule: "2.2", msgHas: []string{"malformed"}}}, {"trailing text", "arr[1-3]x", false, 1000, want{rule: "2.2", msgHas: []string{"malformed"}}}, {"trailing space", "arr[1-3] ", false, 1000, want{rule: "2.2", msgHas: []string{"malformed"}}}, {"inner space", "arr[1 -3]", false, 1000, want{rule: "2.2", msgHas: []string{"malformed"}}}, {"sign", "arr[+1-3]", false, 1000, want{rule: "2.2", msgHas: []string{"malformed"}}}, {"negative", "arr[-1]", false, 1000, want{rule: "2.2", msgHas: []string{"malformed"}}}, {"empty item", "arr[1,,2]", false, 1000, want{rule: "2.2", msgHas: []string{"malformed"}}}, {"trailing comma", "arr[1,]", false, 1000, want{rule: "2.2", msgHas: []string{"malformed"}}}, {"missing end", "arr[1-]", false, 1000, want{rule: "2.2", msgHas: []string{"malformed"}}}, {"missing step", "arr[1-3:]", false, 1000, want{rule: "2.2", msgHas: []string{"malformed"}}}, {"step without range", "arr[1:2]", false, 1000, want{rule: "2.2", msgHas: []string{"malformed"}}}, {"pct without number", "arr[1-3]%", false, 1000, want{rule: "2.2", msgHas: []string{"malformed"}}}, {"pct trailing", "arr[1-3]%2x", false, 1000, want{rule: "2.2", msgHas: []string{"malformed"}}}, {"two brackets", "arr[1][2]", false, 1000, want{rule: "2.2", msgHas: []string{"malformed"}}}, {"nested bracket", "arr[[1]]", false, 1000, want{rule: "2.2", msgHas: []string{"malformed"}}}, {"malformed beats list", "arr[1,2", false, 1000, want{rule: "2.2", msgHas: []string{"malformed"}}}, {"malformed beats zero", "arr[0", false, 1000, want{rule: "2.2", msgHas: []string{"malformed"}}}, // --- 2.3 lists / steps --- {"list", "arr[1,2]", false, 1000, want{rule: "2.3", msgHas: []string{"only a single range name[1-N] is supported"}}}, {"list of ranges", "arr[1-3,5-6]", false, 1000, want{rule: "2.3", msgHas: []string{"only a single range name[1-N] is supported"}}}, {"step", "arr[1-10:2]", false, 1000, want{rule: "2.3", msgHas: []string{"only a single range name[1-N] is supported"}}}, {"precedence 0-0:2", "arr[0-0:2]", false, 1000, want{rule: "2.3"}}, {"list beats pct", "arr[1,2]%3", false, 1000, want{rule: "2.3"}}, // --- 2.4 running limit --- {"pct", "arr[1-3]%2", false, 1000, want{rule: "2.4", msgHas: []string{"running limits are not supported"}}}, {"pct beats zero", "arr[0-3]%2", false, 1000, want{rule: "2.4"}}, // --- 2.5 start 0 --- {"start zero", "arr[0-3]", false, 1000, want{rule: "2.5", msgHas: []string{"element numbers start at 1"}}}, {"single zero", "arr[0]", false, 1000, want{rule: "2.5"}}, {"zero beats reversed", "arr[0-0]", false, 1000, want{rule: "2.5"}}, {"precedence 0-huge", "arr[0-99999999999999999999]", false, 1000, want{rule: "2.5"}}, // --- 2.6 reversed --- {"reversed", "arr[3-1]", false, 1000, want{rule: "2.6", msgHas: []string{"reversed"}}}, {"precedence huge-5", "arr[99999999999999999999-5]", false, 1000, want{rule: "2.6"}}, {"reversed beats limit", "arr[5000-2000]", false, 1000, want{rule: "2.6"}}, // --- 2.7 start > 1 --- {"single 5", "arr[5]", false, 1000, want{rule: "2.7", msgHas: []string{"only name[1-N] and name[1] are currently supported"}}}, {"start above 1", "arr[1001-2000]", false, 1000, want{rule: "2.7", msgHas: []string{"only name[1-N] and name[1] are currently supported"}}}, {"start 2", "arr[2-3]", false, 1000, want{rule: "2.7"}}, {"precedence huge single", "arr[99999999999999999999]", false, 1000, want{rule: "2.7"}}, {"huge range equal", "arr[99999999999999999999-99999999999999999999]", false, 1000, want{rule: "2.7"}}, {"huge reversed two oversized", "arr[99999999999999999999-99999999999999999998]", false, 1000, want{rule: "2.6"}}, {"huge reversed just above 2^40", "arr[1099511627777-1099511627776]", false, 1000, want{rule: "2.6"}}, {"huge ascending two oversized", "arr[99999999999999999998-99999999999999999999]", false, 1000, want{rule: "2.7"}}, {"huge equal after leading zeros", "arr[0099999999999999999999-99999999999999999999]", false, 1000, want{rule: "2.7"}}, } for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { arrayName, n, isArray, err := ParseSubmitIndexExpr(tc.input, tc.interactive, tc.limit) if tc.want.rule == "" { if err != nil { t.Fatalf("ParseSubmitIndexExpr(%q) unexpected error: rule %s: %s", tc.input, err.Rule, err.Msg) } if !isArray || arrayName != tc.want.arrayName || n != tc.want.n { t.Fatalf("ParseSubmitIndexExpr(%q) = (%q, %d, %v), want (%q, %d, true)", tc.input, arrayName, n, isArray, tc.want.arrayName, tc.want.n) } return } if err == nil { t.Fatalf("ParseSubmitIndexExpr(%q) = (%q, %d, %v, nil), want rule %s", tc.input, arrayName, n, isArray, tc.want.rule) } if err.Rule != tc.want.rule { t.Fatalf("ParseSubmitIndexExpr(%q) rule = %s (%s), want %s", tc.input, err.Rule, err.Msg, tc.want.rule) } if isArray || n != 0 || arrayName != "" { t.Fatalf("ParseSubmitIndexExpr(%q) returned (%q, %d, %v) alongside an error", tc.input, arrayName, n, isArray) } if err.Error() != err.Msg { t.Fatalf("Error() = %q, want Msg %q", err.Error(), err.Msg) } for _, s := range tc.want.msgHas { if !strings.Contains(err.Msg, s) { t.Fatalf("ParseSubmitIndexExpr(%q) message %q does not contain %q", tc.input, err.Msg, s) } } }) } } // TestParseSubmitIndexExprErrorAsError checks a non-nil *ArrayExprError // satisfies the error interface (used by preCheckJobRequest / errors.As). func TestParseSubmitIndexExprErrorAsError(t *testing.T) { _, _, _, ae := ParseSubmitIndexExpr("arr[0]", false, 1000) if ae == nil { t.Fatal("expected an error for arr[0]") } var e error = ae if !strings.Contains(e.Error(), "element numbers start at 1") { t.Fatalf("Error() = %q", e.Error()) } }