feat(job-array-one-based-index): parse submit index expressions (task 6.1)
ParseSubmitIndexExpr accepts only name[1-N] and name[1] (leading zeros allowed) and otherwise reports the lowest-numbered broken rule with its specific message: interactive arrays (2.1), invalid name or malformed expression (2.2), lists/steps (2.3), %N (2.4), start 0 (2.5), reversed (2.6), start above 1 (2.7), above the limit (2.8). Oversized numbers saturate for the bound checks while reversal compares the digit strings, so the rule order holds for any input. Not yet wired into CreateJob (task 6.2). Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
3a7e3890e2
commit
0318ac5f44
3 changed files with 359 additions and 1 deletions
153
src/frontend/apiserver/controllers/jobarray_test.go
Normal file
153
src/frontend/apiserver/controllers/jobarray_test.go
Normal file
|
|
@ -0,0 +1,153 @@
|
|||
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())
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue