feat(job-array-one-based-index): client identity and selection parser (task 8.1)
xcecli's format package gains ParseSelection (written or decimal key with optional [k]/[a-b], the server's grammar and check order; 5.8 and 5.7 also on decimal keys with [...], so an upgraded client protects itself against an older server; query ranges refused after the other checks), the bracket-aware SplitSelections, the raw -J ParseNameSelection and Selection.APIKey. ParseJobID is bounded; ToAPIJobID(s, allowRange) no longer passes unrecognised text through; peek keeps today's behaviour via ToAPIJobIDLegacy. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
1fa0107357
commit
bc616e8c4a
9 changed files with 866 additions and 39 deletions
|
|
@ -49,7 +49,7 @@ func (c *JobController) RunJobsControl() error {
|
|||
func (c *JobController) controlJobsByIDs(apiClient *request.APIClient, token string) error {
|
||||
apiIDs := make([]string, 0, len(c.jobIDs))
|
||||
for _, id := range c.jobIDs {
|
||||
apiID, err := format.ToAPIJobID(strings.TrimSpace(id))
|
||||
apiID, err := format.ToAPIJobID(strings.TrimSpace(id), true)
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid job ID %q: %w", id, err)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -78,7 +78,7 @@ func (q *PeekQuerier) Run(jobID string) error {
|
|||
}
|
||||
|
||||
// Convert job ID to API format
|
||||
apiJobID, err := format.ToAPIJobID(jobID)
|
||||
apiJobID, err := format.ToAPIJobIDLegacy(jobID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid job ID %q: %w", jobID, err)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,12 +15,12 @@ func TestPeekJobIDParsing(t *testing.T) {
|
|||
{"12345", "12345"},
|
||||
{"12345[0]", "12345[0]"},
|
||||
{"12345[2]", "12345[2]"},
|
||||
{"plain_id", "plain_id"}, // ToAPIJobID passes through non-numeric strings
|
||||
{"plain_id", "plain_id"}, // ToAPIJobIDLegacy passes through non-numeric strings
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.input, func(t *testing.T) {
|
||||
apiID, err := format.ToAPIJobID(tc.input)
|
||||
apiID, err := format.ToAPIJobIDLegacy(tc.input)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -83,32 +83,53 @@ func MakeJobID(clusterID, apiserverID, timestamp uint64, tag, sequence uint64) u
|
|||
}
|
||||
|
||||
var (
|
||||
// regexJobID matches "{int}-{int}-{int}-{int}" — mirrors REGEX_JOBID in config.h.
|
||||
regexJobID = regexp.MustCompile(`^\s*(\d+)-(\d+)-(\d+)-(\d+)\s*$`)
|
||||
// regexJobArrayID matches "{int}-{int}-{int}-{int}[{int}]" — mirrors REGEX_JOBARRAY_ID in config.h.
|
||||
regexJobArrayID = regexp.MustCompile(`^\s*(\d+)-(\d+)-(\d+)-(\d+)\[(\d+)\]\s*$`)
|
||||
// legacyRegexJobID and legacyRegexJobArrayID are the pre-feature,
|
||||
// unbounded patterns kept only for ToAPIJobIDLegacy (peek is out of scope
|
||||
// of job-array-one-based-index and keeps its behaviour). Identity parsing
|
||||
// for every other caller goes through ParseSelection (jobsel.go).
|
||||
legacyRegexJobID = regexp.MustCompile(`^\s*(\d+)-(\d+)-(\d+)-(\d+)\s*$`)
|
||||
legacyRegexJobArrayID = regexp.MustCompile(`^\s*(\d+)-(\d+)-(\d+)-(\d+)\[(\d+)\]\s*$`)
|
||||
)
|
||||
|
||||
// ParseJobID parses a human-readable job ID string into a packed 64-bit job ID,
|
||||
// mirroring parse_jobid() in src/lib/utils.c:482.
|
||||
// Accepts: "0-0-12874529-0" (regular) or "0-0-12874529-0[5]" (array child).
|
||||
// ParseJobID parses one written job identity into a packed 64-bit job ID:
|
||||
// "0-0-12874529-0" (a job, tag 0) or "0-0-12874529-32768[5]" (element 5 of
|
||||
// that array, tag TAG_JOB_ARRAY). It is bounded like the C parse_jobid()
|
||||
// (src/lib/utils.c): the grammar and checks of ParseSelection apply (5.16,
|
||||
// 5.14, 5.8, 5.7), decimal keys are not accepted, and a range is rejected
|
||||
// because it names no single job.
|
||||
func ParseJobID(s string) (uint64, error) {
|
||||
if m := regexJobArrayID.FindStringSubmatch(s); m != nil {
|
||||
sel, err := ParseSelection(s, false)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
if head, _, _ := splitSelection(sel.Raw); isDecimal(head) {
|
||||
return 0, fmt.Errorf("invalid job ID format: %q", s)
|
||||
}
|
||||
if sel.Form == SelBare {
|
||||
return sel.Key, nil
|
||||
}
|
||||
return sel.Key | uint64(tagJobArray)<<tagShift | uint64(sel.Lo), nil
|
||||
}
|
||||
|
||||
// parseJobIDLegacy is the pre-feature, unbounded parse used only by
|
||||
// ToAPIJobIDLegacy: the index is added to the sequence unchecked.
|
||||
func parseJobIDLegacy(s string) (uint64, bool) {
|
||||
if m := legacyRegexJobArrayID.FindStringSubmatch(s); m != nil {
|
||||
clusterID, _ := strconv.ParseUint(m[1], 10, 64)
|
||||
apiserverID, _ := strconv.ParseUint(m[2], 10, 64)
|
||||
timestamp, _ := strconv.ParseUint(m[3], 10, 64)
|
||||
idJob, _ := strconv.ParseUint(m[4], 10, 64)
|
||||
idIdx, _ := strconv.ParseUint(m[5], 10, 64)
|
||||
return MakeJobID(clusterID, apiserverID, timestamp, tagJobArray, idJob+idIdx), nil
|
||||
return MakeJobID(clusterID, apiserverID, timestamp, tagJobArray, idJob+idIdx), true
|
||||
}
|
||||
if m := regexJobID.FindStringSubmatch(s); m != nil {
|
||||
if m := legacyRegexJobID.FindStringSubmatch(s); m != nil {
|
||||
clusterID, _ := strconv.ParseUint(m[1], 10, 64)
|
||||
apiserverID, _ := strconv.ParseUint(m[2], 10, 64)
|
||||
timestamp, _ := strconv.ParseUint(m[3], 10, 64)
|
||||
idJob, _ := strconv.ParseUint(m[4], 10, 64)
|
||||
return MakeJobID(clusterID, apiserverID, timestamp, 0, idJob), nil
|
||||
return MakeJobID(clusterID, apiserverID, timestamp, 0, idJob), true
|
||||
}
|
||||
return 0, fmt.Errorf("invalid job ID format: %q", s)
|
||||
return 0, false
|
||||
}
|
||||
|
||||
// ExtractBaseJobID clears the tag and array_idx bits, returning the base job ID
|
||||
|
|
@ -129,20 +150,32 @@ func FormatArrayChildID(baseJobID int64, arrayIdx int) string {
|
|||
return FormatJobID(int64(uniqueID))
|
||||
}
|
||||
|
||||
// ToAPIJobID converts a fmt_jobid-format string to the numeric form the API
|
||||
// server expects ("{int}" or "{int}[{idx}]"). Plain integers pass through unchanged.
|
||||
func ToAPIJobID(s string) (string, error) {
|
||||
if regexJobArrayID.MatchString(s) || regexJobID.MatchString(s) {
|
||||
packed, err := ParseJobID(s)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if IsArrayJob(int64(packed)) {
|
||||
baseID := ExtractBaseJobID(packed)
|
||||
arrayIdx := extractBits(packed, 0, nArrayIdxBits)
|
||||
return fmt.Sprintf("%d[%d]", baseID, arrayIdx), nil
|
||||
}
|
||||
return fmt.Sprintf("%d", packed), nil
|
||||
// ToAPIJobID converts one job identity or element selection, written
|
||||
// ("c-a-ts-seq", optionally with "[k]" or "[a-b]") or decimal, to the form the
|
||||
// API server expects ("<decimal>", "<decimal>[k]" or "<decimal>[a-b]"). It is
|
||||
// ParseSelection(s, allowRange).APIKey(): control commands pass
|
||||
// allowRange=true, queries false. Text that is neither a written identity nor
|
||||
// a decimal key is rejected rather than passed through.
|
||||
func ToAPIJobID(s string, allowRange bool) (string, error) {
|
||||
sel, err := ParseSelection(s, allowRange)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return s, nil // already plain int or int[idx]
|
||||
}
|
||||
return sel.APIKey(), nil
|
||||
}
|
||||
|
||||
// ToAPIJobIDLegacy keeps the pre-feature conversion for peek (out of scope of
|
||||
// job-array-one-based-index): a written ID is converted without bounds or
|
||||
// alignment checks, and anything else passes through unchanged.
|
||||
func ToAPIJobIDLegacy(s string) (string, error) {
|
||||
packed, ok := parseJobIDLegacy(s)
|
||||
if !ok {
|
||||
return s, nil // already plain int or int[idx]
|
||||
}
|
||||
if IsArrayJob(int64(packed)) {
|
||||
baseID := ExtractBaseJobID(packed)
|
||||
arrayIdx := extractBits(packed, 0, nArrayIdxBits)
|
||||
return fmt.Sprintf("%d[%d]", baseID, arrayIdx), nil
|
||||
}
|
||||
return fmt.Sprintf("%d", packed), nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
package format
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"testing"
|
||||
)
|
||||
|
|
@ -70,7 +71,15 @@ func TestParseFormatRoundTrip(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestParseJobIDInvalid(t *testing.T) {
|
||||
for _, s := range []string{"abc", "1-2-3", "1[2]", ""} {
|
||||
for _, s := range []string{"abc", "1-2-3", "1[2]", "",
|
||||
"12345", // decimal keys are not written identities
|
||||
"0-0-12874529-0[1-3]", // a range names no single job
|
||||
"0-0-12874529-5[3]", // misaligned array identity (5.8)
|
||||
"0-0-12874529-0[40000]", // element number above 32767 (5.7)
|
||||
"0-0-12874529-1048576[1]", // sequence wider than 20 bits (5.14)
|
||||
"64-0-12874529-0", // cluster wider than 6 bits (5.14)
|
||||
"0-0-12874529-0 [1]", // whitespace before '[' (5.16)
|
||||
} {
|
||||
if _, err := ParseJobID(s); err == nil {
|
||||
t.Errorf("ParseJobID(%q): expected error, got nil", s)
|
||||
}
|
||||
|
|
@ -80,7 +89,7 @@ func TestParseJobIDInvalid(t *testing.T) {
|
|||
func TestToAPIJobIDRegular(t *testing.T) {
|
||||
packed := MakeJobID(0, 0, 12874529, 0, 42)
|
||||
s := FormatJobID(int64(packed))
|
||||
got, err := ToAPIJobID(s)
|
||||
got, err := ToAPIJobID(s, false)
|
||||
if err != nil {
|
||||
t.Fatalf("ToAPIJobID(%q): %v", s, err)
|
||||
}
|
||||
|
|
@ -93,7 +102,7 @@ func TestToAPIJobIDRegular(t *testing.T) {
|
|||
func TestToAPIJobIDArray(t *testing.T) {
|
||||
packed := MakeJobID(0, 0, 12874529, tagJobArray, 5)
|
||||
s := FormatJobID(int64(packed))
|
||||
got, err := ToAPIJobID(s)
|
||||
got, err := ToAPIJobID(s, false)
|
||||
if err != nil {
|
||||
t.Fatalf("ToAPIJobID(%q): %v", s, err)
|
||||
}
|
||||
|
|
@ -105,8 +114,8 @@ func TestToAPIJobIDArray(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestToAPIJobIDPassthrough(t *testing.T) {
|
||||
for _, s := range []string{"123456", "123456[3]"} {
|
||||
got, err := ToAPIJobID(s)
|
||||
for _, s := range []string{"123456", "32768[3]"} {
|
||||
got, err := ToAPIJobID(s, false)
|
||||
if err != nil {
|
||||
t.Fatalf("ToAPIJobID(%q): %v", s, err)
|
||||
}
|
||||
|
|
@ -115,3 +124,54 @@ func TestToAPIJobIDPassthrough(t *testing.T) {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
// A decimal key with [...] that no array could have is rejected under 5.8
|
||||
// before any request (an upgraded client applies its own 5.8 check whatever
|
||||
// the server version).
|
||||
func TestToAPIJobIDMisalignedRejected(t *testing.T) {
|
||||
for _, allowRange := range []bool{false, true} {
|
||||
_, err := ToAPIJobID("123456[3]", allowRange)
|
||||
var se *SelError
|
||||
if !errors.As(err, &se) || se.Rule != "5.8" {
|
||||
t.Errorf("ToAPIJobID(%q, %v) error = %v, want 5.8 rejection", "123456[3]", allowRange, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// A range of elements is accepted only by control commands.
|
||||
func TestToAPIJobIDRange(t *testing.T) {
|
||||
got, err := ToAPIJobID("32768[1-3]", true)
|
||||
if err != nil || got != "32768[1-3]" {
|
||||
t.Errorf("ToAPIJobID(%q, true) = %q, %v; want passthrough", "32768[1-3]", got, err)
|
||||
}
|
||||
if got, err := ToAPIJobID("32768[1-3]", false); err == nil {
|
||||
t.Errorf("ToAPIJobID(%q, false) = %q, want refusal", "32768[1-3]", got)
|
||||
}
|
||||
}
|
||||
|
||||
// Malformed text is rejected rather than passed through.
|
||||
func TestToAPIJobIDRejectsMalformed(t *testing.T) {
|
||||
for _, s := range []string{"plain_id", "X[1]", "123456[a]", "0-0-12874529-0[40000]", ""} {
|
||||
if got, err := ToAPIJobID(s, true); err == nil {
|
||||
t.Errorf("ToAPIJobID(%q, true) = %q, want error", s, got)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ToAPIJobIDLegacy keeps the pre-feature passthrough for peek (out of scope).
|
||||
func TestToAPIJobIDLegacyPassthrough(t *testing.T) {
|
||||
cases := []struct{ in, want string }{
|
||||
{"12345", "12345"},
|
||||
{"12345[0]", "12345[0]"},
|
||||
{"123456[3]", "123456[3]"},
|
||||
{"plain_id", "plain_id"},
|
||||
{"0-0-12874529-42", fmt.Sprintf("%d", MakeJobID(0, 0, 12874529, 0, 42))},
|
||||
{"0-0-12874529-0[5]", fmt.Sprintf("%d[5]", ExtractBaseJobID(MakeJobID(0, 0, 12874529, tagJobArray, 5)))},
|
||||
}
|
||||
for _, c := range cases {
|
||||
got, err := ToAPIJobIDLegacy(c.in)
|
||||
if err != nil || got != c.want {
|
||||
t.Errorf("ToAPIJobIDLegacy(%q) = %q, %v; want %q", c.in, got, err, c.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
334
src/frontend/xcecli/common/format/jobsel.go
Normal file
334
src/frontend/xcecli/common/format/jobsel.go
Normal file
|
|
@ -0,0 +1,334 @@
|
|||
package format
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
"unicode"
|
||||
)
|
||||
|
||||
// Client element selections (job-array-one-based-index, design "CliSel and
|
||||
// CliSplit"). The grammar and the check order mirror the API server's
|
||||
// ParseIdentitySelection / ParseNameSelection (controllers/jobsel.go), so an
|
||||
// upgraded client applies 5.16, 5.14, 5.8, 5.7 and 5.5 itself whatever the
|
||||
// server version. Only 5.17 (a marked decimal key without [k]) is left to the
|
||||
// server. This package stays dependency-free (no apiserver imports).
|
||||
|
||||
// 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 (-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.8", "5.7", "5.5"), or "range" for an identity range
|
||||
// given to a query command; Msg names the selection as written (trimmed).
|
||||
type SelError struct {
|
||||
Rule string
|
||||
Msg string
|
||||
}
|
||||
|
||||
func (e *SelError) Error() string { return e.Msg }
|
||||
|
||||
// RuleQueryRange tags the client-side refusal of an identity range in a query.
|
||||
const RuleQueryRange = "range"
|
||||
|
||||
// maxElementNumber is the largest element number a selection may name (5.7):
|
||||
// the 15-bit array-index field.
|
||||
const maxElementNumber = 1<<nArrayIdxBits - 1
|
||||
|
||||
// numSaturate caps parsed element numbers: any larger number is above
|
||||
// maxElementNumber, so its exact value does not matter for 5.7. Saturated
|
||||
// values lose their order, so the reversed-range check (5.5) compares the
|
||||
// leading-zero-stripped digit strings instead.
|
||||
const numSaturate = 1 << 30
|
||||
|
||||
func newSelError(rule, format string, args ...any) (Selection, error) {
|
||||
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 numSaturate
|
||||
loDigits, hiDigits string // leading zeros stripped
|
||||
}
|
||||
|
||||
// readNum reads one or more decimal digits at s[pos:]. v saturates at
|
||||
// numSaturate; digits is the digit run with leading zeros stripped (empty for
|
||||
// zero). ok is false when there is no digit at pos.
|
||||
func readNum(s string, pos int) (v int, digits string, next int, ok bool) {
|
||||
next = pos
|
||||
for next < len(s) && s[next] >= '0' && s[next] <= '9' {
|
||||
if v > numSaturate/10 {
|
||||
v = numSaturate
|
||||
} else {
|
||||
v = v*10 + int(s[next]-'0')
|
||||
if v > numSaturate {
|
||||
v = numSaturate
|
||||
}
|
||||
}
|
||||
next++
|
||||
}
|
||||
start := pos
|
||||
for start < next && s[start] == '0' {
|
||||
start++
|
||||
}
|
||||
return v, s[start:next], next, next > pos
|
||||
}
|
||||
|
||||
// digitsLess reports whether decimal a < b for leading-zero-stripped digit
|
||||
// strings: a shorter string is smaller, equal lengths compare lexically.
|
||||
func digitsLess(a, b string) bool {
|
||||
if len(a) != len(b) {
|
||||
return len(a) < len(b)
|
||||
}
|
||||
return a < b
|
||||
}
|
||||
|
||||
// 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]", 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 = readNum(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 = readNum(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, the query-range refusal (when !allowRange) and 5.5,
|
||||
// in that order, and fills in the selection's form and numbers.
|
||||
func checkIndex(sel Selection, idx selIndex, allowRange bool) (Selection, error) {
|
||||
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 newSelError("5.7",
|
||||
"invalid element selection '%s': element numbers must be at most %d",
|
||||
sel.Raw, maxElementNumber)
|
||||
}
|
||||
if idx.isRange && !allowRange {
|
||||
return newSelError(RuleQueryRange,
|
||||
"invalid element selection '%s': a range of elements is accepted only by control commands",
|
||||
sel.Raw)
|
||||
}
|
||||
// 5.5 (compared on the digit strings, so the order is exact)
|
||||
if digitsLess(idx.hiDigits, idx.loDigits) {
|
||||
return newSelError("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
|
||||
}
|
||||
|
||||
// errWrittenWidth marks a written identity with a field wider than allowed.
|
||||
var errWrittenWidth = errors.New("field too wide")
|
||||
|
||||
// parseWrittenID parses the strict written form c-a-ts-seq (each field one or
|
||||
// more ASCII digits, nothing else). ok is false when s does not follow that
|
||||
// grammar; err is errWrittenWidth when a field is wider than its bit width
|
||||
// (cluster <= 63, API server <= 15, timestamp < 2^32, sequence < 2^20).
|
||||
func parseWrittenID(s string) (key uint64, ok bool, err error) {
|
||||
parts := strings.Split(s, "-")
|
||||
if len(parts) != 4 {
|
||||
return 0, false, nil
|
||||
}
|
||||
widths := [4]int{nClusterIDBits, nApiserverIDBits, nTimestampBits, nSequenceBits}
|
||||
var f [4]uint64
|
||||
tooWide := false
|
||||
for i, p := range parts {
|
||||
if !isDecimal(p) {
|
||||
return 0, false, nil
|
||||
}
|
||||
v, perr := strconv.ParseUint(p, 10, 64)
|
||||
if perr != nil || v >= uint64(1)<<widths[i] {
|
||||
// all digits, so the only ParseUint failure is a value above uint64
|
||||
tooWide = true
|
||||
continue
|
||||
}
|
||||
f[i] = v
|
||||
}
|
||||
if tooWide {
|
||||
return 0, true, errWrittenWidth
|
||||
}
|
||||
return MakeJobID(f[0], f[1], f[2], 0, f[3]), true, nil
|
||||
}
|
||||
|
||||
// isArrayCapableKey reports whether key can name an array: tag 0 and the low
|
||||
// 15 (index) bits clear, i.e. the sequence is a multiple of 32768 (5.8).
|
||||
func isArrayCapableKey(key uint64) bool {
|
||||
return extractBits(key, tagShift, nTagBits) == 0 && extractBits(key, 0, nArrayIdxBits) == 0
|
||||
}
|
||||
|
||||
const syntaxMsg = "invalid job identity or element selection '%s': expected <job-id>, <array-id>[k] or <array-id>[a-b]"
|
||||
|
||||
// ParseSelection 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). 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.8, 5.7, then (allowRange=false) the
|
||||
// query-range refusal, then 5.5 -- and only the first failure is reported.
|
||||
// 5.8 and 5.7 apply to decimal keys with [...] too. 5.17 (a marked decimal
|
||||
// key without [k]) is left to the server. Numbers may have leading zeros (1.5).
|
||||
func ParseSelection(raw string, allowRange bool) (Selection, error) {
|
||||
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 newSelError("5.16", syntaxMsg, sel.Raw)
|
||||
}
|
||||
if isDecimal(head) {
|
||||
key, err := strconv.ParseUint(head, 10, 64)
|
||||
if err != nil {
|
||||
// all digits, so the only failure is a value above uint64 (5.14)
|
||||
return newSelError("5.14", "invalid job identity '%s': the job number is too large", sel.Raw)
|
||||
}
|
||||
sel.Key = key
|
||||
} else {
|
||||
key, written, err := parseWrittenID(head)
|
||||
if !written {
|
||||
return newSelError("5.16", syntaxMsg, sel.Raw)
|
||||
}
|
||||
if err != nil {
|
||||
return newSelError("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)
|
||||
}
|
||||
sel.Key = key
|
||||
}
|
||||
|
||||
// 5.8: only with an element part.
|
||||
if idx.present && !isArrayCapableKey(sel.Key) {
|
||||
return newSelError("5.8", "invalid job identity '%s': '%s' is not an array identity", sel.Raw, head)
|
||||
}
|
||||
return checkIndex(sel, idx, allowRange)
|
||||
}
|
||||
|
||||
// ParseNameSelection checks a raw -J value that contains '[' or ']', before
|
||||
// the wildcard mapping and '*' removal: the name part is zero or more
|
||||
// characters, none of them whitespace, '[' or ']' ('*' is allowed there, and
|
||||
// only there); after it exactly one "[num]" or "[num-num]" and nothing else.
|
||||
// Only 5.16, 5.7 and 5.5 apply, in that order. 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.
|
||||
func ParseNameSelection(raw string) (Selection, error) {
|
||||
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 newSelError("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, true)
|
||||
}
|
||||
|
||||
// SplitSelections splits a comma-separated list of selections at the commas
|
||||
// outside "[...]", as the API server's extractJobNames does, so a selection
|
||||
// such as "X[1,3]" stays whole and is reported as the user wrote it. A
|
||||
// trailing empty item is dropped; the items are not trimmed.
|
||||
func SplitSelections(list string) []string {
|
||||
var out []string
|
||||
cur := strings.Builder{}
|
||||
depth := 0
|
||||
for _, r := range list {
|
||||
switch {
|
||||
case r == '[':
|
||||
depth++
|
||||
case r == ']':
|
||||
depth--
|
||||
case r == ',' && depth == 0:
|
||||
out = append(out, cur.String())
|
||||
cur.Reset()
|
||||
continue
|
||||
}
|
||||
cur.WriteRune(r)
|
||||
}
|
||||
if cur.Len() > 0 {
|
||||
out = append(out, cur.String())
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// APIKey renders an identity selection in the form the REST API takes:
|
||||
// "<decimal>", "<decimal>[k]" or "<decimal>[a-b]".
|
||||
func (s Selection) APIKey() string {
|
||||
switch s.Form {
|
||||
case SelSingle:
|
||||
return fmt.Sprintf("%d[%d]", s.Key, s.Lo)
|
||||
case SelRange:
|
||||
return fmt.Sprintf("%d[%d-%d]", s.Key, s.Lo, s.Hi)
|
||||
default:
|
||||
return strconv.FormatUint(s.Key, 10)
|
||||
}
|
||||
}
|
||||
399
src/frontend/xcecli/common/format/jobsel_test.go
Normal file
399
src/frontend/xcecli/common/format/jobsel_test.go
Normal file
|
|
@ -0,0 +1,399 @@
|
|||
package format
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"reflect"
|
||||
"strconv"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// Shared conformance table `selection-cases` (design.md, Testing Strategy),
|
||||
// run for the command-line client: identity rows (written c-a-ts-seq and
|
||||
// decimal keys) through ParseSelection, 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.
|
||||
var selXKey = MakeJobID(3, 5, 1700000000, 0, 65536)
|
||||
|
||||
type selWant struct {
|
||||
rule string // "" = accepted; else "5.16", "5.14", "5.8", "5.7", "5.5", "range"
|
||||
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 (written
|
||||
// form), as run by the apiserver's jobsel_test.go, with ranges allowed.
|
||||
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")},
|
||||
{"3--5-1700000000-65536", rejected("5.16")},
|
||||
{"foo[1]", rejected("5.16")},
|
||||
{"plain_id", 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")},
|
||||
{selX + "[0010-009]", 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, in string, got Selection, gotErr error, want selWant, wantKey uint64) {
|
||||
t.Helper()
|
||||
if want.rule != "" {
|
||||
if gotErr == nil {
|
||||
t.Fatalf("%s(%q) = %+v, want rejection under %s", fn, in, got, want.rule)
|
||||
}
|
||||
var se *SelError
|
||||
if !errors.As(gotErr, &se) {
|
||||
t.Fatalf("%s(%q) error %T (%v), want *SelError", fn, in, gotErr, gotErr)
|
||||
}
|
||||
if se.Rule != want.rule {
|
||||
t.Errorf("%s(%q) rule = %s (%s), want %s", fn, in, se.Rule, se.Msg, want.rule)
|
||||
}
|
||||
if raw := strings.TrimSpace(in); raw != "" && !strings.Contains(se.Msg, raw) {
|
||||
t.Errorf("%s(%q) message %q does not name the selection", fn, in, se.Msg)
|
||||
}
|
||||
return
|
||||
}
|
||||
if gotErr != nil {
|
||||
t.Fatalf("%s(%q) unexpected error: %v", fn, in, gotErr)
|
||||
}
|
||||
if got.Form != want.form || got.Lo != want.lo || got.Hi != want.hi {
|
||||
t.Errorf("%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.Errorf("%s(%q) key = %d, want %d", fn, in, got.Key, wantKey)
|
||||
}
|
||||
if got.Raw != strings.TrimSpace(in) {
|
||||
t.Errorf("%s(%q) Raw = %q, want trimmed input", fn, in, got.Raw)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseSelection_SelectionCases(t *testing.T) {
|
||||
for _, c := range identitySelectionCases() {
|
||||
t.Run(c.in, func(t *testing.T) {
|
||||
got, err := ParseSelection(c.in, true)
|
||||
wantKey := selXKey
|
||||
if c.want.exact {
|
||||
wantKey = c.want.key
|
||||
}
|
||||
checkSel(t, "ParseSelection", c.in, got, err, c.want, wantKey)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Decimal keys: the client applies 5.14 and, with [...], 5.8 and 5.7 itself
|
||||
// (tag 0 and low 15 bits clear), whatever the server version. 5.17 (a marked
|
||||
// decimal key without [k]) is left to the server.
|
||||
func TestParseSelection_DecimalKeys(t *testing.T) {
|
||||
xDec := strconv.FormatUint(selXKey, 10)
|
||||
elem2 := selXKey | uint64(tagJobArray)<<tagShift | 2
|
||||
elem2Dec := strconv.FormatUint(elem2, 10)
|
||||
ordinary := selXKey | 5
|
||||
|
||||
cases := []struct {
|
||||
in string
|
||||
want selWant
|
||||
wantKey uint64
|
||||
}{
|
||||
{xDec, accepted(SelBare, 0, 0), selXKey},
|
||||
{xDec + "[2]", accepted(SelSingle, 2, 2), selXKey},
|
||||
{"0" + xDec + "[1-3]", accepted(SelRange, 1, 3), selXKey},
|
||||
{" " + xDec + " ", accepted(SelBare, 0, 0), selXKey},
|
||||
{"123456", accepted(SelBare, 0, 0), 123456},
|
||||
{"0", accepted(SelBare, 0, 0), 0},
|
||||
{"32768[3]", accepted(SelSingle, 3, 3), 32768},
|
||||
// 5.17 is the server's: a marked decimal without [k] passes through
|
||||
{elem2Dec, accepted(SelBare, 0, 0), elem2},
|
||||
// a marked decimal with [k] is not an array identity (5.8)
|
||||
{elem2Dec + "[1]", rejected("5.8"), 0},
|
||||
{strconv.FormatUint(ordinary, 10) + "[3]", rejected("5.8"), 0},
|
||||
{"123456[3]", rejected("5.8"), 0},
|
||||
// 5.7 / 5.5
|
||||
{xDec + "[40000]", rejected("5.7"), 0},
|
||||
{xDec + "[3-1]", rejected("5.5"), 0},
|
||||
// precedence 5.8 before 5.7
|
||||
{"123456[40000]", rejected("5.8"), 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.8/5.7
|
||||
{"18446744073709551616", rejected("5.14"), 0},
|
||||
{"99999999999999999999999[40000]", rejected("5.14"), 0},
|
||||
// syntax before width
|
||||
{"99999999999999999999999[1]x", rejected("5.16"), 0},
|
||||
}
|
||||
for _, c := range cases {
|
||||
t.Run(c.in, func(t *testing.T) {
|
||||
got, err := ParseSelection(c.in, true)
|
||||
checkSel(t, "ParseSelection", c.in, got, err, c.want, c.wantKey)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// allowRange=false (queries) refuses a range only after 5.16, 5.14, 5.8 and
|
||||
// 5.7 pass (5.12), and before 5.5 (a reversed range by identity is a 5.5
|
||||
// rejection only in control commands).
|
||||
func TestParseSelection_QueryRangeRefusal(t *testing.T) {
|
||||
xDec := strconv.FormatUint(selXKey, 10)
|
||||
cases := []selCase{
|
||||
{selX + "[1-3]", rejected("range")},
|
||||
{selX + "[3-3]", rejected("range")},
|
||||
{selX + "[3-1]", rejected("range")},
|
||||
{xDec + "[1-3]", rejected("range")},
|
||||
{selX + "[1-3]x", rejected("5.16")},
|
||||
{"0-0-1700000000-1048576[1-3]", rejected("5.14")},
|
||||
{selXBad + "[1-3]", rejected("5.8")},
|
||||
{"123456[1-3]", rejected("5.8")},
|
||||
{selX + "[1-40000]", rejected("5.7")},
|
||||
// single elements and bare identities are unaffected
|
||||
{selX + "[2]", accepted(SelSingle, 2, 2)},
|
||||
{selX, accepted(SelBare, 0, 0)},
|
||||
}
|
||||
for _, c := range cases {
|
||||
t.Run(c.in, func(t *testing.T) {
|
||||
got, err := ParseSelection(c.in, false)
|
||||
checkSel(t, "ParseSelection", c.in, got, err, c.want, selXKey)
|
||||
})
|
||||
}
|
||||
_, err := ParseSelection(selX+"[1-3]", false)
|
||||
if err == nil || !strings.Contains(err.Error(), "a range of elements is accepted only by control commands") {
|
||||
t.Errorf("query range refusal message = %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseSelection_RuleMessages(t *testing.T) {
|
||||
cases := []struct{ in, want string }{
|
||||
{selX + "[a]", "invalid job identity or element selection"},
|
||||
{"64-0-1700000000-32768", "invalid job identity"},
|
||||
{selXBad + "[3]", "is not an array identity"},
|
||||
{selX + "[40000]", "at most 32767"},
|
||||
{selX + "[3-1]", "the range is reversed"},
|
||||
}
|
||||
for _, c := range cases {
|
||||
_, err := ParseSelection(c.in, true)
|
||||
if err == nil || !strings.Contains(err.Error(), c.want) {
|
||||
t.Errorf("ParseSelection(%q) error = %v, want containing %q", c.in, err, c.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseNameSelection_SelectionCases(t *testing.T) {
|
||||
type nw struct {
|
||||
name string
|
||||
form SelForm
|
||||
lo, hi int
|
||||
rule string
|
||||
}
|
||||
cases := []struct {
|
||||
in string
|
||||
want nw
|
||||
}{
|
||||
// --- accepted (5.16; name surfaces only) ---
|
||||
{"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}},
|
||||
// xcecli raw -J: '*' allowed in the name part only
|
||||
{"foo*[1]", nw{name: "foo*", form: SelSingle, lo: 1, hi: 1}},
|
||||
{"*foo*[1]", nw{name: "*foo*", form: SelSingle, lo: 1, hi: 1}},
|
||||
{"*[1]", nw{name: "*", form: SelSingle, lo: 1, hi: 1}},
|
||||
{"**[1-2]", nw{name: "**", form: SelRange, lo: 1, hi: 2}},
|
||||
// no brackets: not an element selection, name kept as given
|
||||
{"foo", nw{name: "foo", form: SelBare}},
|
||||
{"sleep 100", nw{name: "sleep 100", form: SelBare}},
|
||||
// --- rejected (5.16) ---
|
||||
{"foo[1]*", nw{rule: "5.16"}},
|
||||
{"foo[1*]", nw{rule: "5.16"}},
|
||||
{"foo[*1]", nw{rule: "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 ---
|
||||
{"foo[3-1]", nw{rule: "5.5"}},
|
||||
{"foo[0010-009]", nw{rule: "5.5"}},
|
||||
{"foo[99999999999999999999-99999999999999999998]", nw{rule: "5.7"}},
|
||||
// --- precedence (5.12) ---
|
||||
{"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)
|
||||
if c.want.rule != "" {
|
||||
checkSel(t, "ParseNameSelection", c.in, got, err, selWant{rule: c.want.rule}, 0)
|
||||
return
|
||||
}
|
||||
if err != nil {
|
||||
t.Fatalf("ParseNameSelection(%q) unexpected error: %v", c.in, err)
|
||||
}
|
||||
if got.Name != c.want.name || got.Form != c.want.form || got.Lo != c.want.lo || got.Hi != c.want.hi {
|
||||
t.Errorf("ParseNameSelection(%q) = %+v, want %+v", c.in, got, c.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestSplitSelections(t *testing.T) {
|
||||
cases := []struct {
|
||||
in string
|
||||
want []string
|
||||
}{
|
||||
{"X[1,3],Y", []string{"X[1,3]", "Y"}},
|
||||
{"X[1-3],Y[2]", []string{"X[1-3]", "Y[2]"}},
|
||||
{"A,B,C", []string{"A", "B", "C"}},
|
||||
{"A", []string{"A"}},
|
||||
{"A,", []string{"A"}},
|
||||
{"A,,B", []string{"A", "", "B"}},
|
||||
{" A , B ", []string{" A ", " B "}},
|
||||
}
|
||||
for _, c := range cases {
|
||||
if got := SplitSelections(c.in); !reflect.DeepEqual(got, c.want) {
|
||||
t.Errorf("SplitSelections(%q) = %q, want %q", c.in, got, c.want)
|
||||
}
|
||||
}
|
||||
// a selection rejected by 5.16 is reported as the user wrote it
|
||||
parts := SplitSelections("X[1,3],Y")
|
||||
if _, err := ParseSelection(parts[0], true); err == nil || !strings.Contains(err.Error(), "'X[1,3]'") {
|
||||
t.Errorf("ParseSelection(%q) error = %v, want naming 'X[1,3]'", parts[0], err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSelectionAPIKey(t *testing.T) {
|
||||
xDec := strconv.FormatUint(selXKey, 10)
|
||||
cases := []struct{ in, want string }{
|
||||
{selX, xDec},
|
||||
{selX + "[2]", xDec + "[2]"},
|
||||
{selX + "[002]", xDec + "[2]"},
|
||||
{selX + "[1-3]", xDec + "[1-3]"},
|
||||
{selX + "[3-3]", xDec + "[3-3]"},
|
||||
{selX + "[0]", xDec + "[0]"},
|
||||
{xDec + "[01-03]", xDec + "[1-3]"},
|
||||
{"123456", "123456"},
|
||||
{"0123456", "123456"},
|
||||
}
|
||||
for _, c := range cases {
|
||||
s, err := ParseSelection(c.in, true)
|
||||
if err != nil {
|
||||
t.Fatalf("ParseSelection(%q): %v", c.in, err)
|
||||
}
|
||||
if got := s.APIKey(); got != c.want {
|
||||
t.Errorf("ParseSelection(%q).APIKey() = %q, want %q", c.in, got, c.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -46,7 +46,7 @@ func (f *JobsFilter) HandleJobFilter(array_type string) (string, error) {
|
|||
if err := isValidJobId(jobId); err != nil {
|
||||
return "", err
|
||||
}
|
||||
apiID, err := format.ToAPIJobID(strings.TrimSpace(jobId))
|
||||
apiID, err := format.ToAPIJobID(strings.TrimSpace(jobId), false)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue