feat(job-array-one-based-index): REST identity and name selection parsers (task 7.1)
ParseIdentitySelection (strict written form or base-10 decimal key, optional [k] or [a-b]) and ParseNameSelection (name part may be empty, no whitespace or brackets) apply the shared selection grammar and check order (5.16 syntax, 5.14 width, 5.17 marked decimal without [k], 5.8 alignment only with [...], 5.7 element bound, 5.5 reversed), reporting only the first failure with a rule tag and a message quoting the selection. Runs the shared selection-cases table plus REST-only rows. Not yet wired into the handlers. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
c30e5f1d82
commit
2a8490bc92
3 changed files with 557 additions and 1 deletions
|
|
@ -139,7 +139,7 @@
|
|||
- _Depends: 5.2, 5.4, 6.1_
|
||||
|
||||
- [ ] 7. API server: element selection in queries and control
|
||||
- [ ] 7.1 (P) Parse REST identity and name selections
|
||||
- [x] 7.1 (P) Parse REST identity and name selections
|
||||
- Parse identity selections (strict written form or base-10 decimal) and name selections (name part may be empty, no whitespace or brackets) under the shared grammar and check order, including the marked-decimal-without-index rejection
|
||||
- Return a rule tag and a message that names the selection as written
|
||||
- Done when the selection unit tests pass for the shared conformance table plus decimal-key cases (marked decimal without index, `1e3`, `-1`) and the name rows
|
||||
|
|
@ -246,3 +246,4 @@
|
|||
- 5.1: Go helpers: `ParseHumanJobIDStrict(s) (uint64, error)` returns sentinel `ErrJobIDSyntax` / `ErrJobIDWidth` (callers trim and split `[...]` first; no whitespace accepted); `MakeElementID(key uint64, k int) uint64`; `IsArrayCapableKey(key)`; `GenerateArrayJobID(c, a, n)` (non-cgo stub returns errNoCGO).
|
||||
- 5.4: handler fixture in `controllers/jobs_handler_test.go` (`newHandlerFixture`, `seedJob`/`seedOrdinaryJob`/`seedArrayElements`/`seedQueue`, `submit`, `controlByID`, `controlByFilter`, `dispatchedIDs`, `submissions`); submit bodies must be client-style JSON maps (marshalled `models.JobSubmitRequest` breaks on zero CustomTime). Handlers are registered without `RequirePermission`; in-handler RBAC/queue/status checks still run.
|
||||
- 6.1: the design's "saturate oversized numbers" rule alone loses the order of two oversized numbers; `ParseSubmitIndexExpr` compares leading-zero-stripped digit strings for 2.6. Use the same approach for the selection parsers' reversed-range check (7.1, 8.1) if they saturate. Rule tags are strings "2.1".."2.8" on `*ArrayExprError`.
|
||||
- 7.1: `ParseNameSelection` on a value without `[`/`]` returns `SelBare` with `Name` = the raw untrimmed value (`Raw` is trimmed); 7.2 must match bare names on `Name` and must not treat a bare result as validated. Rule tags on `*SelError` are "5.16", "5.14", "5.17", "5.8", "5.7", "5.5"; decimal keys above uint64 are 5.14.
|
||||
|
|
|
|||
220
src/frontend/apiserver/controllers/jobsel.go
Normal file
220
src/frontend/apiserver/controllers/jobsel.go
Normal file
|
|
@ -0,0 +1,220 @@
|
|||
package controllers
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
"unicode"
|
||||
|
||||
"apiserver/backend"
|
||||
)
|
||||
|
||||
// REST element selections (job-array-one-based-index, design "ApiSel and
|
||||
// ApiMatch"). ParseIdentitySelection and ParseNameSelection are pure parsers:
|
||||
// they depend only on the backend ID helpers and the standard library.
|
||||
|
||||
// SelForm is the shape of a parsed selection.
|
||||
type SelForm int
|
||||
|
||||
const (
|
||||
SelBare SelForm = iota // no "[...]"
|
||||
SelSingle // "[k]" (Lo == Hi == k)
|
||||
SelRange // "[a-b]" (a == b allowed)
|
||||
)
|
||||
|
||||
// Selection is one parsed identity or name selection.
|
||||
type Selection struct {
|
||||
Raw string // as the user wrote it (trimmed)
|
||||
Name string // name selections only (job_name / -J)
|
||||
Key uint64 // identity selections: exact ID (bare) or array ID
|
||||
Form SelForm
|
||||
Lo, Hi int // element numbers, Lo == Hi for SelSingle
|
||||
}
|
||||
|
||||
// SelError is a rejected selection. Rule is the requirement it breaks
|
||||
// ("5.16", "5.14", "5.17", "5.8", "5.7", "5.5"); Msg names the selection as
|
||||
// written (trimmed).
|
||||
type SelError struct {
|
||||
Rule string
|
||||
Msg string
|
||||
}
|
||||
|
||||
func (e *SelError) Error() string { return e.Msg }
|
||||
|
||||
// maxElementNumber is the largest element number a selection may name (5.7):
|
||||
// the 15-bit array-index field.
|
||||
const maxElementNumber = 32767
|
||||
|
||||
func selErr(rule, format string, args ...any) (Selection, *SelError) {
|
||||
return Selection{}, &SelError{Rule: rule, Msg: fmt.Sprintf(format, args...)}
|
||||
}
|
||||
|
||||
// selIndex is the element part of a selection.
|
||||
type selIndex struct {
|
||||
present bool
|
||||
isRange bool
|
||||
lo, hi int // saturated at exprNumSaturate
|
||||
loDigits, hiDigits string // leading zeros stripped
|
||||
}
|
||||
|
||||
// splitSelection splits a trimmed selection into the part before the
|
||||
// element part and the element part. ok is false when the text breaks the
|
||||
// 5.16 grammar of the element part: a ']' before any '[', an element part that
|
||||
// is not "[num]" or "[num-num]" (num = one or more decimal digits), or any
|
||||
// character after the closing ']'.
|
||||
func splitSelection(s string) (head string, idx selIndex, ok bool) {
|
||||
open := strings.IndexAny(s, "[]")
|
||||
if open < 0 {
|
||||
return s, idx, true
|
||||
}
|
||||
if s[open] == ']' {
|
||||
return "", idx, false
|
||||
}
|
||||
head = s[:open]
|
||||
idx.present = true
|
||||
var good bool
|
||||
pos := open + 1
|
||||
idx.lo, idx.loDigits, pos, good = readExprNum(s, pos)
|
||||
if !good {
|
||||
return "", idx, false
|
||||
}
|
||||
idx.hi, idx.hiDigits = idx.lo, idx.loDigits
|
||||
if pos < len(s) && s[pos] == '-' {
|
||||
idx.hi, idx.hiDigits, pos, good = readExprNum(s, pos+1)
|
||||
if !good {
|
||||
return "", idx, false
|
||||
}
|
||||
idx.isRange = true
|
||||
}
|
||||
if pos != len(s)-1 || s[pos] != ']' {
|
||||
return "", idx, false
|
||||
}
|
||||
return head, idx, true
|
||||
}
|
||||
|
||||
// checkIndex applies 5.7 and then 5.5 to an element part and fills in the
|
||||
// selection's form and numbers.
|
||||
func checkIndex(sel Selection, idx selIndex) (Selection, *SelError) {
|
||||
if !idx.present {
|
||||
sel.Form = SelBare
|
||||
return sel, nil
|
||||
}
|
||||
// 5.7 (a saturated number is always above the bound)
|
||||
if idx.lo > maxElementNumber || idx.hi > maxElementNumber {
|
||||
return selErr("5.7",
|
||||
"invalid element selection '%s': element numbers must be at most %d",
|
||||
sel.Raw, maxElementNumber)
|
||||
}
|
||||
// 5.5 (compared on the digit strings, so the order is exact)
|
||||
if digitsLess(idx.hiDigits, idx.loDigits) {
|
||||
return selErr("5.5",
|
||||
"invalid element selection '%s': the range is reversed (last element number is less than the first)",
|
||||
sel.Raw)
|
||||
}
|
||||
sel.Lo, sel.Hi = idx.lo, idx.hi
|
||||
sel.Form = SelSingle
|
||||
if idx.isRange {
|
||||
sel.Form = SelRange
|
||||
}
|
||||
return sel, nil
|
||||
}
|
||||
|
||||
func isDecimal(s string) bool {
|
||||
if s == "" {
|
||||
return false
|
||||
}
|
||||
for i := 0; i < len(s); i++ {
|
||||
if s[i] < '0' || s[i] > '9' {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// ParseIdentitySelection parses a job identity, optionally followed by "[k]"
|
||||
// or "[a-b]". The identity is the written form c-a-ts-seq (strict field
|
||||
// widths) or a decimal uint64 (base 10). Ranges are always allowed at the
|
||||
// server. Whitespace before the identity or after the closing ']' is ignored;
|
||||
// whitespace anywhere else is invalid. Checks run in the order of 5.12 --
|
||||
// 5.16, 5.14, 5.17, 5.8, 5.7, 5.5 -- and only the first failure is reported.
|
||||
// Numbers may have leading zeros (1.5).
|
||||
func ParseIdentitySelection(raw string) (Selection, *SelError) {
|
||||
sel := Selection{Raw: strings.TrimSpace(raw)}
|
||||
|
||||
// 5.16: grammar of the element part and of the identity itself.
|
||||
head, idx, ok := splitSelection(sel.Raw)
|
||||
if !ok {
|
||||
return selErr("5.16",
|
||||
"invalid job identity or element selection '%s': expected <job-id>, <array-id>[k] or <array-id>[a-b]",
|
||||
sel.Raw)
|
||||
}
|
||||
decimal := isDecimal(head)
|
||||
var key uint64
|
||||
var err error
|
||||
if decimal {
|
||||
key, err = strconv.ParseUint(head, 10, 64)
|
||||
if err != nil {
|
||||
// all digits, so the only failure is a value above uint64 (5.14)
|
||||
return selErr("5.14", "invalid job identity '%s': the job number is too large", sel.Raw)
|
||||
}
|
||||
} else {
|
||||
key, err = backend.ParseHumanJobIDStrict(head)
|
||||
if errors.Is(err, backend.ErrJobIDWidth) {
|
||||
// 5.14
|
||||
return selErr("5.14",
|
||||
"invalid job identity '%s': a field is wider than allowed (cluster <= 63, API server <= 15, timestamp < 2^32, sequence < 2^20)",
|
||||
sel.Raw)
|
||||
}
|
||||
if err != nil {
|
||||
return selErr("5.16",
|
||||
"invalid job identity or element selection '%s': expected <job-id>, <array-id>[k] or <array-id>[a-b]",
|
||||
sel.Raw)
|
||||
}
|
||||
}
|
||||
sel.Key = key
|
||||
|
||||
// 5.17: a decimal key carrying the array marking, without [k]. The
|
||||
// written form never carries the marking.
|
||||
if decimal && !idx.present && backend.IsArrayJob(key) {
|
||||
return selErr("5.17",
|
||||
"invalid job identity '%s': array elements must be named as <array-id>[k]", sel.Raw)
|
||||
}
|
||||
// 5.8: only with an element part.
|
||||
if idx.present && !backend.IsArrayCapableKey(key) {
|
||||
return selErr("5.8",
|
||||
"invalid job identity '%s': '%s' is not an array identity", sel.Raw, head)
|
||||
}
|
||||
return checkIndex(sel, idx)
|
||||
}
|
||||
|
||||
// ParseNameSelection parses a job-name selection "name[k]" or "name[a-b]".
|
||||
// Only 5.16, 5.7 and 5.5 apply (in that order). The name part is zero or more
|
||||
// characters, none of them whitespace, '[' or ']'; it is not checked against
|
||||
// the submission array-name rule (a name no array can have selects no job).
|
||||
// Whitespace before the name or after the closing ']' is ignored.
|
||||
//
|
||||
// A value with no '[' or ']' is not an element selection: it is returned as a
|
||||
// SelBare selection whose Name is the value exactly as given, so a caller
|
||||
// converting a whole filter entry keeps today's plain-name match for it.
|
||||
func ParseNameSelection(raw string) (Selection, *SelError) {
|
||||
sel := Selection{Raw: strings.TrimSpace(raw)}
|
||||
if !strings.ContainsAny(raw, "[]") {
|
||||
sel.Name = raw
|
||||
sel.Form = SelBare
|
||||
return sel, nil
|
||||
}
|
||||
|
||||
// 5.16
|
||||
head, idx, ok := splitSelection(sel.Raw)
|
||||
if ok && strings.IndexFunc(head, unicode.IsSpace) >= 0 {
|
||||
ok = false
|
||||
}
|
||||
if !ok {
|
||||
return selErr("5.16",
|
||||
"invalid element selection '%s': expected name[k] or name[a-b] (no whitespace in the name or brackets)",
|
||||
sel.Raw)
|
||||
}
|
||||
sel.Name = head
|
||||
return checkIndex(sel, idx)
|
||||
}
|
||||
335
src/frontend/apiserver/controllers/jobsel_test.go
Normal file
335
src/frontend/apiserver/controllers/jobsel_test.go
Normal file
|
|
@ -0,0 +1,335 @@
|
|||
package controllers
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"apiserver/backend"
|
||||
)
|
||||
|
||||
// Shared conformance table `selection-cases` (design.md, Testing Strategy),
|
||||
// run for the REST surface: identity rows (written c-a-ts-seq and decimal
|
||||
// keys) through ParseIdentitySelection, name rows through ParseNameSelection.
|
||||
// `X` stands for a valid, array-capable written identity (sequence a multiple
|
||||
// of 32768); `c-a-ts-5` for one that no array could have.
|
||||
|
||||
const (
|
||||
selX = "3-5-1700000000-65536"
|
||||
selXBad = "3-5-1700000000-5" // sequence not a multiple of 32768 (5.8)
|
||||
)
|
||||
|
||||
// selXKey is the packed, untagged value of selX.
|
||||
func selXKey(t *testing.T) uint64 {
|
||||
t.Helper()
|
||||
k, err := backend.ParseHumanJobIDStrict(selX)
|
||||
if err != nil {
|
||||
t.Fatalf("ParseHumanJobIDStrict(%q): %v", selX, err)
|
||||
}
|
||||
return k
|
||||
}
|
||||
|
||||
type selWant struct {
|
||||
rule string // "" = accepted; else "5.16", "5.14", "5.17", "5.8", "5.7", "5.5"
|
||||
form SelForm
|
||||
lo int
|
||||
hi int
|
||||
exact bool // check key below instead of selXKey
|
||||
key uint64 // expected Key when exact
|
||||
}
|
||||
|
||||
func accepted(form SelForm, lo, hi int) selWant { return selWant{form: form, lo: lo, hi: hi} }
|
||||
func rejected(rule string) selWant { return selWant{rule: rule} }
|
||||
|
||||
type selCase struct {
|
||||
in string
|
||||
want selWant
|
||||
}
|
||||
|
||||
// identitySelectionCases are the identity rows of the shared table, copied
|
||||
// from design.md, plus REST-only identity rows.
|
||||
func identitySelectionCases() []selCase {
|
||||
return []selCase{
|
||||
// --- Valid ---
|
||||
{selX, accepted(SelBare, 0, 0)},
|
||||
{selX + "[1]", accepted(SelSingle, 1, 1)},
|
||||
{selX + "[0]", accepted(SelSingle, 0, 0)},
|
||||
{selX + "[01]", accepted(SelSingle, 1, 1)},
|
||||
{selX + "[1-3]", accepted(SelRange, 1, 3)},
|
||||
{selX + "[3-3]", accepted(SelRange, 3, 3)},
|
||||
{selX + "[32767]", accepted(SelSingle, 32767, 32767)},
|
||||
// leading zeros (1.5)
|
||||
{selX + "[001-0003]", accepted(SelRange, 1, 3)},
|
||||
{"03-05-01700000000-065536[2]", accepted(SelSingle, 2, 2)},
|
||||
// whitespace before the identity / after the closing ']' is ignored (5.16)
|
||||
{" \t" + selX + "[2] \t", accepted(SelSingle, 2, 2)},
|
||||
{" " + selX + " ", accepted(SelBare, 0, 0)},
|
||||
// a bare ordinary identity needs no alignment (5.6)
|
||||
{selXBad, selWant{form: SelBare, exact: true, key: 3<<56 | 5<<52 | 1700000000<<20 | 5}},
|
||||
|
||||
// --- Syntax (5.16) ---
|
||||
{selX + " [1]", rejected("5.16")},
|
||||
{selX + "[a]", rejected("5.16")},
|
||||
{selX + "[]", rejected("5.16")},
|
||||
{selX + "[1", rejected("5.16")},
|
||||
{selX + "]", rejected("5.16")},
|
||||
{selX + "[+1]", rejected("5.16")},
|
||||
{selX + "[-1]", rejected("5.16")},
|
||||
{selX + "[ 1]", rejected("5.16")},
|
||||
{selX + "[1-3:2]", rejected("5.16")},
|
||||
{selX + "[1]x", rejected("5.16")},
|
||||
{selX + "[1-2-3]", rejected("5.16")},
|
||||
{selX + "[1 ]", rejected("5.16")},
|
||||
{selX + "[1,3]", rejected("5.16")},
|
||||
{selX + "[1][2]", rejected("5.16")},
|
||||
{selX + "[1-]", rejected("5.16")},
|
||||
{selX + "[-3]", rejected("5.16")},
|
||||
{selX + "x", rejected("5.16")},
|
||||
{"", rejected("5.16")},
|
||||
{" \t ", rejected("5.16")},
|
||||
{"[1]", rejected("5.16")},
|
||||
{"3-5-1700000000", rejected("5.16")},
|
||||
{"3-5-1700000000-65536-1", rejected("5.16")},
|
||||
{"+3-5-1700000000-65536", rejected("5.16")},
|
||||
{"3 -5-1700000000-65536", rejected("5.16")},
|
||||
{"foo[1]", rejected("5.16")},
|
||||
{"99999999999999999999-0-0-0[1]x", rejected("5.16")},
|
||||
|
||||
// --- Widths (5.14) ---
|
||||
{"64-0-1700000000-32768[1]", rejected("5.14")},
|
||||
{"0-16-1700000000-32768[1]", rejected("5.14")},
|
||||
{"0-0-4294967296-32768[1]", rejected("5.14")},
|
||||
{"0-0-1700000000-1048576[1]", rejected("5.14")},
|
||||
{"64-0-1700000000-32768", rejected("5.14")},
|
||||
{"0-0-1700000000-1048576", rejected("5.14")},
|
||||
{"18446744073709551616-0-0-0", rejected("5.14")},
|
||||
|
||||
// --- Alignment (5.8) ---
|
||||
{selXBad + "[3]", rejected("5.8")},
|
||||
{"3-5-1700000000-65537[1-2]", rejected("5.8")},
|
||||
|
||||
// --- Index (5.7) ---
|
||||
{selX + "[32768]", rejected("5.7")},
|
||||
{selX + "[1-40000]", rejected("5.7")},
|
||||
{selX + "[40000-1]", rejected("5.7")},
|
||||
{selX + "[99999999999999999999]", rejected("5.7")},
|
||||
{selX + "[18446744073709551617]", rejected("5.7")},
|
||||
|
||||
// --- Reversed (5.5) ---
|
||||
{selX + "[3-1]", rejected("5.5")},
|
||||
{selX + "[32767-0]", rejected("5.5")},
|
||||
|
||||
// --- Precedence (5.12) ---
|
||||
{"0-0-1700000000-1048576[40000]", rejected("5.14")},
|
||||
{selXBad + "[3-1]", rejected("5.8")},
|
||||
{selXBad + "[40000]", rejected("5.8")},
|
||||
{"64-0-1700000000-5[3-1]", rejected("5.14")},
|
||||
{"64-0-1700000000-5 [3-1]", rejected("5.16")},
|
||||
}
|
||||
}
|
||||
|
||||
func checkSel(t *testing.T, fn string, in string, got Selection, gotErr *SelError, want selWant, wantKey uint64) {
|
||||
t.Helper()
|
||||
if want.rule != "" {
|
||||
if gotErr == nil {
|
||||
t.Fatalf("%s(%q) accepted (%+v), want rule %s", fn, in, got, want.rule)
|
||||
}
|
||||
if gotErr.Rule != want.rule {
|
||||
t.Fatalf("%s(%q) rule = %s (%q), want %s", fn, in, gotErr.Rule, gotErr.Msg, want.rule)
|
||||
}
|
||||
// messages name the selection as written (trimmed)
|
||||
if trimmed := strings.TrimSpace(in); !strings.Contains(gotErr.Msg, "'"+trimmed+"'") {
|
||||
t.Fatalf("%s(%q) message %q does not quote %q", fn, in, gotErr.Msg, trimmed)
|
||||
}
|
||||
return
|
||||
}
|
||||
if gotErr != nil {
|
||||
t.Fatalf("%s(%q) rejected: rule %s %q", fn, in, gotErr.Rule, gotErr.Msg)
|
||||
}
|
||||
if got.Form != want.form || got.Lo != want.lo || got.Hi != want.hi {
|
||||
t.Fatalf("%s(%q) = form %d [%d-%d], want form %d [%d-%d]",
|
||||
fn, in, got.Form, got.Lo, got.Hi, want.form, want.lo, want.hi)
|
||||
}
|
||||
if got.Key != wantKey {
|
||||
t.Fatalf("%s(%q) key = %d, want %d", fn, in, got.Key, wantKey)
|
||||
}
|
||||
if got.Raw != strings.TrimSpace(in) {
|
||||
t.Fatalf("%s(%q) Raw = %q, want %q", fn, in, got.Raw, strings.TrimSpace(in))
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseIdentitySelection_SelectionCases(t *testing.T) {
|
||||
xKey := selXKey(t)
|
||||
for _, c := range identitySelectionCases() {
|
||||
t.Run(c.in, func(t *testing.T) {
|
||||
got, err := ParseIdentitySelection(c.in)
|
||||
wantKey := xKey
|
||||
if c.want.exact {
|
||||
wantKey = c.want.key
|
||||
}
|
||||
checkSel(t, "ParseIdentitySelection", c.in, got, err, c.want, wantKey)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Decimal keys (REST only): the same rules apply to a base-10 uint64 key,
|
||||
// plus 5.17 for a decimal key carrying the array marking without [k].
|
||||
func TestParseIdentitySelection_DecimalKeys(t *testing.T) {
|
||||
xKey := selXKey(t)
|
||||
xDec := strconv.FormatUint(xKey, 10)
|
||||
elem2 := backend.MakeElementID(xKey, 2)
|
||||
elem2Dec := strconv.FormatUint(elem2, 10)
|
||||
ordinary := xKey | 5 // not array-capable
|
||||
|
||||
cases := []struct {
|
||||
in string
|
||||
want selWant
|
||||
wantKey uint64
|
||||
}{
|
||||
{xDec, accepted(SelBare, 0, 0), xKey},
|
||||
{xDec + "[2]", accepted(SelSingle, 2, 2), xKey},
|
||||
{"0" + xDec + "[1-3]", accepted(SelRange, 1, 3), xKey},
|
||||
{" " + xDec + " ", accepted(SelBare, 0, 0), xKey},
|
||||
{"123456", accepted(SelBare, 0, 0), 123456},
|
||||
{"0", accepted(SelBare, 0, 0), 0},
|
||||
// 5.17: marked decimal without [k], whether or not that element exists
|
||||
{elem2Dec, rejected("5.17"), 0},
|
||||
{" " + elem2Dec + " ", rejected("5.17"), 0},
|
||||
// marked decimal with [k] is not an array identity (5.8)
|
||||
{elem2Dec + "[1]", rejected("5.8"), 0},
|
||||
// 5.8 for decimal keys with [...]
|
||||
{strconv.FormatUint(ordinary, 10) + "[3]", rejected("5.8"), 0},
|
||||
{"123456[3]", rejected("5.8"), 0},
|
||||
// 5.7 / 5.5 for decimal keys
|
||||
{xDec + "[40000]", rejected("5.7"), 0},
|
||||
{xDec + "[3-1]", rejected("5.5"), 0},
|
||||
// not decimal and not written form (5.16)
|
||||
{"1e3", rejected("5.16"), 0},
|
||||
{"-1", rejected("5.16"), 0},
|
||||
{"+1", rejected("5.16"), 0},
|
||||
{"0x10", rejected("5.16"), 0},
|
||||
{"12 34", rejected("5.16"), 0},
|
||||
{xDec + " [1]", rejected("5.16"), 0},
|
||||
{xDec + "[1]x", rejected("5.16"), 0},
|
||||
// too large for uint64 (5.14), checked before 5.17/5.8/5.7
|
||||
{"18446744073709551616", rejected("5.14"), 0},
|
||||
{"99999999999999999999999[40000]", rejected("5.14"), 0},
|
||||
// precedence: syntax before width
|
||||
{"99999999999999999999999[1]x", rejected("5.16"), 0},
|
||||
}
|
||||
for _, c := range cases {
|
||||
t.Run(c.in, func(t *testing.T) {
|
||||
got, err := ParseIdentitySelection(c.in)
|
||||
checkSel(t, "ParseIdentitySelection", c.in, got, err, c.want, c.wantKey)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseIdentitySelection_517Message(t *testing.T) {
|
||||
elem := strconv.FormatUint(backend.MakeElementID(selXKey(t), 2), 10)
|
||||
_, err := ParseIdentitySelection(elem)
|
||||
if err == nil || !strings.Contains(err.Msg, "array elements must be named as <array-id>[k]") {
|
||||
t.Fatalf("5.17 message = %+v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseIdentitySelection_RuleMessages(t *testing.T) {
|
||||
cases := []struct{ in, has string }{
|
||||
{selX + "[3-1]", "reversed"},
|
||||
{selX + "[40000]", "32767"},
|
||||
{selXBad + "[3]", "invalid job identity"},
|
||||
{"64-0-1700000000-32768", "invalid job identity"},
|
||||
{selX + "[a]", "invalid"},
|
||||
}
|
||||
for _, c := range cases {
|
||||
_, err := ParseIdentitySelection(c.in)
|
||||
if err == nil || !strings.Contains(err.Msg, c.has) {
|
||||
t.Fatalf("ParseIdentitySelection(%q) = %+v, want message containing %q", c.in, err, c.has)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Name rows of the shared table plus 5.7 / 5.5 for names. Names get only
|
||||
// 5.16, 5.7 and 5.5; the name part is zero or more characters, none of them
|
||||
// whitespace, '[' or ']', and is not checked against the array-name rule.
|
||||
func TestParseNameSelection_SelectionCases(t *testing.T) {
|
||||
type nw struct {
|
||||
rule string
|
||||
name string
|
||||
form SelForm
|
||||
lo, hi int
|
||||
}
|
||||
cases := []struct {
|
||||
in string
|
||||
want nw
|
||||
}{
|
||||
// accepted
|
||||
{"foo[1]", nw{name: "foo", form: SelSingle, lo: 1, hi: 1}},
|
||||
{"[1]", nw{name: "", form: SelSingle, lo: 1, hi: 1}},
|
||||
{"foo[1-3]", nw{name: "foo", form: SelRange, lo: 1, hi: 3}},
|
||||
{"foo[3-3]", nw{name: "foo", form: SelRange, lo: 3, hi: 3}},
|
||||
{"foo[0]", nw{name: "foo", form: SelSingle, lo: 0, hi: 0}},
|
||||
{"foo[007]", nw{name: "foo", form: SelSingle, lo: 7, hi: 7}},
|
||||
{"foo[32767]", nw{name: "foo", form: SelSingle, lo: 32767, hi: 32767}},
|
||||
{" \tfoo[2]\t ", nw{name: "foo", form: SelSingle, lo: 2, hi: 2}},
|
||||
{"a*b/c[1]", nw{name: "a*b/c", form: SelSingle, lo: 1, hi: 1}}, // no array-name rule
|
||||
// no brackets: a plain name, kept as written (today's exact-match value)
|
||||
{"foo", nw{name: "foo", form: SelBare}},
|
||||
{"sleep 100", nw{name: "sleep 100", form: SelBare}},
|
||||
// rejected: syntax (5.16)
|
||||
{"foo [1]", nw{rule: "5.16"}},
|
||||
{"my job[1]", nw{rule: "5.16"}},
|
||||
{"foo[1]x", nw{rule: "5.16"}},
|
||||
{"foo[ 1-3]", nw{rule: "5.16"}},
|
||||
{"foo[a]", nw{rule: "5.16"}},
|
||||
{"foo[1-3]x", nw{rule: "5.16"}},
|
||||
{"foo[]", nw{rule: "5.16"}},
|
||||
{"foo[1", nw{rule: "5.16"}},
|
||||
{"foo]", nw{rule: "5.16"}},
|
||||
{"foo]1[", nw{rule: "5.16"}},
|
||||
{"foo[+1]", nw{rule: "5.16"}},
|
||||
{"foo[-1]", nw{rule: "5.16"}},
|
||||
{"foo[1-3:2]", nw{rule: "5.16"}},
|
||||
{"foo[1,3]", nw{rule: "5.16"}},
|
||||
{"foo[1][2]", nw{rule: "5.16"}},
|
||||
{"foo[1-2-3]", nw{rule: "5.16"}},
|
||||
// 5.7
|
||||
{"foo[32768]", nw{rule: "5.7"}},
|
||||
{"foo[1-40000]", nw{rule: "5.7"}},
|
||||
{"foo[40000-1]", nw{rule: "5.7"}},
|
||||
{"foo[99999999999999999999]", nw{rule: "5.7"}},
|
||||
// 5.5 (compared exactly, leading zeros stripped)
|
||||
{"foo[3-1]", nw{rule: "5.5"}},
|
||||
{"foo[0010-009]", nw{rule: "5.5"}},
|
||||
// precedence: syntax before 5.7 and 5.5
|
||||
{"foo [40000]", nw{rule: "5.16"}},
|
||||
{"foo[3-1]x", nw{rule: "5.16"}},
|
||||
}
|
||||
for _, c := range cases {
|
||||
t.Run(c.in, func(t *testing.T) {
|
||||
got, err := ParseNameSelection(c.in)
|
||||
trimmed := strings.TrimSpace(c.in)
|
||||
if c.want.rule != "" {
|
||||
if err == nil {
|
||||
t.Fatalf("ParseNameSelection(%q) accepted (%+v), want rule %s", c.in, got, c.want.rule)
|
||||
}
|
||||
if err.Rule != c.want.rule {
|
||||
t.Fatalf("ParseNameSelection(%q) rule = %s (%q), want %s", c.in, err.Rule, err.Msg, c.want.rule)
|
||||
}
|
||||
if !strings.Contains(err.Msg, "'"+trimmed+"'") {
|
||||
t.Fatalf("ParseNameSelection(%q) message %q does not quote %q", c.in, err.Msg, trimmed)
|
||||
}
|
||||
return
|
||||
}
|
||||
if err != nil {
|
||||
t.Fatalf("ParseNameSelection(%q) rejected: %s %q", c.in, err.Rule, err.Msg)
|
||||
}
|
||||
if got.Name != c.want.name || got.Form != c.want.form || got.Lo != c.want.lo || got.Hi != c.want.hi {
|
||||
t.Fatalf("ParseNameSelection(%q) = %+v, want %+v", c.in, got, c.want)
|
||||
}
|
||||
if got.Raw != trimmed || got.Key != 0 {
|
||||
t.Fatalf("ParseNameSelection(%q) Raw/Key = %q/%d", c.in, got.Raw, got.Key)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue