feat(job-array-one-based-index): control by filter with element selections (task 7.5)
Filter-based ControlJob validates a bracketed -J value before fetching (400 with the parser's message; unbracketed names, including commands with spaces, unchanged). FetchJobList evaluates bracketed job_name entries through the selection predicates instead of unevaluated compound conditions, so -J 'foo[1]' acts on element 1 of every array named foo and never on ordinary jobs of that name. The most-recent-job rule applies only without an element number and without all-jobs, and treats the element of a one-element array like an ordinary job (isSingleElementArray, a documented approximation). The old index-section parser is deleted. Completes bug-1898 on the owned paths. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
db16b565dd
commit
40917a01f0
5 changed files with 251 additions and 134 deletions
|
|
@ -171,7 +171,7 @@
|
||||||
- _Requirements: 5.1, 5.3, 5.4, 8.2, 8.3_
|
- _Requirements: 5.1, 5.3, 5.4, 8.2, 8.3_
|
||||||
- _Depends: 5.4, 7.2_
|
- _Depends: 5.4, 7.2_
|
||||||
|
|
||||||
- [ ] 7.5 Control jobs by filter with element selections
|
- [x] 7.5 Control jobs by filter with element selections
|
||||||
- Validate a bracketed name filter before fetching (unbracketed names, including command-line names with spaces, unchanged), returning 400 with the specific message
|
- Validate a bracketed name filter before fetching (unbracketed names, including command-line names with spaces, unchanged), returning 400 with the specific message
|
||||||
- Route the filter-based job fetch's bracketed name handling through the name parser and the selection predicates; with its last use gone, delete the old index-section parser
|
- Route the filter-based job fetch's bracketed name handling through the name parser and the selection predicates; with its last use gone, delete the old index-section parser
|
||||||
- Apply the most-recent-job rule only when the filter has no element number and all-jobs is not set, acting on the newest row alone when it is ordinary or the only stored element of its array
|
- Apply the most-recent-job rule only when the filter has no element number and all-jobs is not set, acting on the newest row alone when it is ordinary or the only stored element of its array
|
||||||
|
|
|
||||||
|
|
@ -281,3 +281,29 @@ func rowUint64(row utils.OrderMap, col string) (uint64, bool) {
|
||||||
}
|
}
|
||||||
return 0, false
|
return 0, false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// isSingleElementArray reports whether row is an array element and the only
|
||||||
|
// stored element of its array: it counts the rows in store.Jobs with row's
|
||||||
|
// job_id whose unique_job_id carries the array marking (design ApiControl,
|
||||||
|
// 5.10). The store records no array size and the controller pushes a
|
||||||
|
// one-element array as a single job, so the stored row count is the only
|
||||||
|
// signal; a multi-element array whose other elements have all been purged
|
||||||
|
// from the store is deliberately treated as one-element.
|
||||||
|
func isSingleElementArray(row utils.OrderMap) bool {
|
||||||
|
uid, ok := rowUint64(row, "unique_job_id")
|
||||||
|
if !ok || !backend.IsArrayJob(uid) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
jobID, ok := rowUint64(row, "job_id")
|
||||||
|
if !ok {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
count := 0
|
||||||
|
store.Jobs.Range(func(_ uint64, j models.JobInfo) bool {
|
||||||
|
if uint64(j.JobID) == jobID && backend.IsArrayJob(uint64(j.Unique_JobID)) {
|
||||||
|
count++
|
||||||
|
}
|
||||||
|
return count <= 1
|
||||||
|
})
|
||||||
|
return count == 1
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -457,3 +457,42 @@ func TestBuildSelectionPredicates_Errors(t *testing.T) {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TestIsSingleElementArray (design ApiControl, 5.10): a row is the only
|
||||||
|
// stored element of its array when no other stored row with its job_id is an
|
||||||
|
// array element; ordinary jobs never are.
|
||||||
|
func TestIsSingleElementArray(t *testing.T) {
|
||||||
|
orig := store.Jobs
|
||||||
|
t.Cleanup(func() { store.Jobs = orig })
|
||||||
|
store.Jobs = store.NewDataStore[uint64, models.JobInfo]()
|
||||||
|
|
||||||
|
const one = uint64(2)<<40 | uint64(3)<<15 // one-element array
|
||||||
|
const multi = uint64(2)<<40 | uint64(4)<<15 // two elements
|
||||||
|
const ord = uint64(1)<<40 | 7 // ordinary job
|
||||||
|
put := func(uid uint64) utils.OrderMap {
|
||||||
|
j := models.JobInfo{
|
||||||
|
Unique_JobID: int64(uid),
|
||||||
|
JobID: int64(backend.ExtractArrayBaseID(uid)),
|
||||||
|
ArrayIdx: backend.ExtractArrayIdx(uid),
|
||||||
|
JobName: "foo",
|
||||||
|
}
|
||||||
|
store.Jobs.Put(uid, j)
|
||||||
|
return store.StructToOrderMap(j)
|
||||||
|
}
|
||||||
|
oneRow := put(backend.MakeElementID(one, 1))
|
||||||
|
multiRow := put(backend.MakeElementID(multi, 1))
|
||||||
|
put(backend.MakeElementID(multi, 2))
|
||||||
|
ordRow := put(ord)
|
||||||
|
// an ordinary job sharing the one-element array's number does not count
|
||||||
|
put(one)
|
||||||
|
|
||||||
|
if !isSingleElementArray(oneRow) {
|
||||||
|
t.Errorf("one-element array element: got false, want true")
|
||||||
|
}
|
||||||
|
if isSingleElementArray(multiRow) {
|
||||||
|
t.Errorf("element of a two-element array: got true, want false")
|
||||||
|
}
|
||||||
|
if isSingleElementArray(ordRow) {
|
||||||
|
t.Errorf("ordinary job: got true, want false")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -2433,6 +2433,23 @@ func (j JobController) ControlJob(ctx *gin.Context) {
|
||||||
filters = getFilters(req, submitter)
|
filters = getFilters(req, submitter)
|
||||||
logger.Debug("final request filters: ", zap.Any("filters", filters))
|
logger.Debug("final request filters: ", zap.Any("filters", filters))
|
||||||
|
|
||||||
|
// A job name with '[' or ']' is an element selection: validate it before
|
||||||
|
// fetching (validate-all-first), so a rejection is a 400 with its own
|
||||||
|
// message. Names without brackets are matched as written, as before
|
||||||
|
// (unnamed jobs carry their command, with spaces, as their name).
|
||||||
|
hasElementNumber := strings.ContainsAny(req.Filter.JobName, "[]")
|
||||||
|
if hasElementNumber {
|
||||||
|
for _, filter := range filters {
|
||||||
|
if filter.Field != "job_name" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if _, _, err := BuildSelectionPredicates([]utils.FilterCondition{filter}); err != nil {
|
||||||
|
returnSelectionError(ctx, filter.Value, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
//1. fetch jobs from db with req.Filter
|
//1. fetch jobs from db with req.Filter
|
||||||
jobList, err := FetchJobList(filters)
|
jobList, err := FetchJobList(filters)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
@ -2452,9 +2469,14 @@ func (j JobController) ControlJob(ctx *gin.Context) {
|
||||||
}
|
}
|
||||||
|
|
||||||
var jobInfoList []utils.OrderMap
|
var jobInfoList []utils.OrderMap
|
||||||
if !req.Filter.AllJobs {
|
if !req.Filter.AllJobs && !hasElementNumber {
|
||||||
// will query job order by submittime, will get the last submitted job.
|
// "most recent job" rule: rows are sorted by unique_job_id, so the
|
||||||
if len(jobList) > 0 && jobList[len(jobList)-1].Data["array_idx"] == 0 {
|
// last one is the most recent; act on it alone when it is an
|
||||||
|
// ordinary job (or a pre-upgrade element 0) or the only stored
|
||||||
|
// element of its array (5.10). A filter with an element number acts
|
||||||
|
// on exactly the selected elements (5.2).
|
||||||
|
last := jobList[len(jobList)-1]
|
||||||
|
if last.Data["array_idx"] == 0 || isSingleElementArray(last) {
|
||||||
jobInfoList = jobList[len(jobList)-1:]
|
jobInfoList = jobList[len(jobList)-1:]
|
||||||
} else {
|
} else {
|
||||||
jobInfoList = jobList
|
jobInfoList = jobList
|
||||||
|
|
@ -3012,35 +3034,9 @@ func copyRemoteFilesLocally(ctx *gin.Context, files []string, username string) m
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
// parse [1-3] to start = 1, end=3
|
|
||||||
func parseArrayIndexSection(idxsec string) (int, int, error) {
|
|
||||||
parts := strings.Split(idxsec, "-")
|
|
||||||
if len(parts) != 2 {
|
|
||||||
start, err := strconv.Atoi(strings.TrimSpace(parts[0]))
|
|
||||||
if err != nil {
|
|
||||||
return 0, 0, err
|
|
||||||
}
|
|
||||||
return start, start, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
start, err := strconv.Atoi(strings.TrimSpace(parts[0]))
|
|
||||||
if err != nil {
|
|
||||||
return 0, 0, err
|
|
||||||
}
|
|
||||||
|
|
||||||
end, err := strconv.Atoi(strings.TrimSpace(parts[1]))
|
|
||||||
if err != nil {
|
|
||||||
return 0, 0, err
|
|
||||||
}
|
|
||||||
if start >= end {
|
|
||||||
return 0, 0, fmt.Errorf("start index should be bigger than end index")
|
|
||||||
} else {
|
|
||||||
return start, end, nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// fetch job list from job_info table according filter condition
|
// fetch job list from job_info table according filter condition
|
||||||
// need special handling for job_name=arrayjob[1-3] and exec_hosts cases.
|
// need special handling for job_name=arrayjob[1-3] (element selections,
|
||||||
|
// evaluated as row predicates) and exec_hosts cases.
|
||||||
func FetchJobList(filters []utils.FilterCondition) ([]utils.OrderMap, error) {
|
func FetchJobList(filters []utils.FilterCondition) ([]utils.OrderMap, error) {
|
||||||
|
|
||||||
logger.Debug("Enter fetchJobList() function",
|
logger.Debug("Enter fetchJobList() function",
|
||||||
|
|
@ -3053,7 +3049,13 @@ func FetchJobList(filters []utils.FilterCondition) ([]utils.OrderMap, error) {
|
||||||
sortOrders := make([]utils.SortOrder, 0)
|
sortOrders := make([]utils.SortOrder, 0)
|
||||||
jobLists := make([]utils.OrderMap, 0)
|
jobLists := make([]utils.OrderMap, 0)
|
||||||
|
|
||||||
compfilters := make(utils.CompFilterCondition)
|
// Element selections (job_name values with '[' or ']') become row
|
||||||
|
// predicates; every other filter stays a plain condition. Parsed before
|
||||||
|
// the exec_hosts store lookups below (validate-all-first).
|
||||||
|
filters, preds, err := BuildSelectionPredicates(filters)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
for i, filter := range filters {
|
for i, filter := range filters {
|
||||||
if filter.Field == "exec_hosts" {
|
if filter.Field == "exec_hosts" {
|
||||||
|
|
@ -3088,102 +3090,9 @@ func FetchJobList(filters []utils.FilterCondition) ([]utils.OrderMap, error) {
|
||||||
filters[i].Operator = "in"
|
filters[i].Operator = "in"
|
||||||
filters[i].Value = "(" + strings.Join(uniqueJobIdStrs, ",")
|
filters[i].Value = "(" + strings.Join(uniqueJobIdStrs, ",")
|
||||||
}
|
}
|
||||||
|
|
||||||
// special handle job_name[index] cases, here index can be [1-3], [1,3,5],[1-3,5-7] situations
|
|
||||||
if filters[i].Field == "job_name" {
|
|
||||||
filtCdArrays := make([]utils.FilterConditionArray, 0)
|
|
||||||
jobNames := extractJobNames(filters[i].Value)
|
|
||||||
for i := range jobNames {
|
|
||||||
jobNames[i] = strings.TrimSpace(jobNames[i])
|
|
||||||
}
|
|
||||||
logger.Debug("After extractJobNames jobNames is", zap.Any("jobNames", jobNames))
|
|
||||||
|
|
||||||
if !(strings.Contains(filters[i].Value, "[") && strings.Contains(filters[i].Value, "]")) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, jobname := range jobNames {
|
|
||||||
subORFilters := make([]utils.FilterCondition, 0)
|
|
||||||
jobnameArry, err := parseArrayName(jobname)
|
|
||||||
logger.Debug("After parseArrayName jobname is ", zap.Any("jobname", jobnameArry))
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
return nil, errors.New("job name format is invalid")
|
|
||||||
}
|
|
||||||
|
|
||||||
if err = isValidArrayName(jobnameArry.KeyID); err != nil {
|
|
||||||
err := errors.New("bad jobname: " + jobnameArry.KeyID)
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// in job control filter, operator is "eq"
|
|
||||||
var operator = "eq"
|
|
||||||
// treat every section as an OR condition
|
|
||||||
mainFiltCond := utils.FilterCondition{
|
|
||||||
Field: filters[i].Field,
|
|
||||||
Operator: operator,
|
|
||||||
Value: jobnameArry.KeyID,
|
|
||||||
}
|
|
||||||
if jobnameArry.Indices != nil {
|
|
||||||
|
|
||||||
// array jobid: job_id:in:(101[1-3, 5-6])
|
|
||||||
// handle every section in Indices list.
|
|
||||||
for _, jidxsec := range jobnameArry.Indices {
|
|
||||||
st, ed, err := parseArrayIndexSection(jidxsec.Index)
|
|
||||||
if err != nil {
|
|
||||||
err := errors.New("bad job array index: " + jidxsec.Index)
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if jidxsec.Step == 1 {
|
|
||||||
subORFilters = append(subORFilters, utils.FilterCondition{
|
|
||||||
Field: "array_idx",
|
|
||||||
Operator: "between",
|
|
||||||
Value: "(" + strconv.Itoa(st) + "," + strconv.Itoa(ed) + ")",
|
|
||||||
})
|
|
||||||
} else { // handle step > 1
|
|
||||||
idxList := make([]string, 0)
|
|
||||||
for idx := st; idx <= ed; idx += jidxsec.Step {
|
|
||||||
idxList = append(idxList, strconv.Itoa(idx))
|
|
||||||
}
|
|
||||||
subORFilters = append(subORFilters, utils.FilterCondition{
|
|
||||||
Field: "array_idx",
|
|
||||||
Operator: "in",
|
|
||||||
Value: "(" + strings.Join(idxList, ",") + ")",
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
filtCdArrays = append(filtCdArrays, utils.FilterConditionArray{
|
|
||||||
MainFiltCond: mainFiltCond,
|
|
||||||
SubFiltConds: subORFilters,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
compfilters["job_name"] = filtCdArrays
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// remove filter with jobarray[] from filters, for filter in compfilters need special handling.
|
|
||||||
if len(compfilters) > 0 {
|
|
||||||
var filtersNew []utils.FilterCondition
|
|
||||||
for _, filter := range filters {
|
|
||||||
found := false
|
|
||||||
for k, _ := range compfilters {
|
|
||||||
if filter.Field == k {
|
|
||||||
found = true
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if found {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
filtersNew = append(filtersNew, filter)
|
|
||||||
}
|
|
||||||
filters = filtersNew
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var filtercds []utils.Condition
|
var filtercds []utils.Condition
|
||||||
var compfiltercd utils.CompCondition
|
|
||||||
var err error
|
|
||||||
if len(filters) > 0 {
|
if len(filters) > 0 {
|
||||||
filtercds, err = utils.ConvertToNewFilter(filters, &datatype)
|
filtercds, err = utils.ConvertToNewFilter(filters, &datatype)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
@ -3191,13 +3100,6 @@ func FetchJobList(filters []utils.FilterCondition) ([]utils.OrderMap, error) {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if len(compfilters) > 0 {
|
|
||||||
compfiltercd, err = utils.ConvertToNewFilterArray(compfilters, &datatype)
|
|
||||||
if err != nil {
|
|
||||||
logger.Error(err.Error())
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
sortOrders = append(sortOrders, utils.SortOrder{
|
sortOrders = append(sortOrders, utils.SortOrder{
|
||||||
Column: "unique_job_id",
|
Column: "unique_job_id",
|
||||||
|
|
@ -3206,10 +3108,11 @@ func FetchJobList(filters []utils.FilterCondition) ([]utils.OrderMap, error) {
|
||||||
|
|
||||||
pagetokenStr := ""
|
pagetokenStr := ""
|
||||||
for {
|
for {
|
||||||
queryOpts, err := utils.BuildQueryOptions(filtercds, compfiltercd, sortOrders, limit, pagetokenStr)
|
queryOpts, err := utils.BuildQueryOptions(filtercds, nil, sortOrders, limit, pagetokenStr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
queryOpts.Predicates = preds
|
||||||
queryReply, pagetoken, err := services.QueryData(tablename, queryOpts, fields)
|
queryReply, pagetoken, err := services.QueryData(tablename, queryOpts, fields)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
|
|
||||||
|
|
@ -1234,3 +1234,152 @@ func TestJobHandler_ControlValidThenInvalid(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ========== Control by filter with element selections (task 7.5) ==========
|
||||||
|
|
||||||
|
// controlFilterKill posts a kill through the filter-based ControlJob with
|
||||||
|
// the job-name filter name (the -J value as the client sends it).
|
||||||
|
func (f *handlerFixture) controlFilterKill(name string) *httptest.ResponseRecorder {
|
||||||
|
f.t.Helper()
|
||||||
|
return f.controlByFilter(models.JobControlRequest{
|
||||||
|
Filter: models.JobControlFilter{JobName: name},
|
||||||
|
Action: "kill",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestJobHandler_ControlFilterRejectsBadSelection (5.7, 5.16, 5.5): a
|
||||||
|
// bracketed -J value the name parser rejects returns 400 with its specific
|
||||||
|
// message before any job is fetched, and dispatches nothing.
|
||||||
|
func TestJobHandler_ControlFilterRejectsBadSelection(t *testing.T) {
|
||||||
|
f := newHandlerFixture(t)
|
||||||
|
X := handlerTestArrayBase + 40<<15
|
||||||
|
elems := f.seedArrayElements(X, "foo", "RUN", 1, 2)
|
||||||
|
assertSeeded(t, uint64(elems[0].Unique_JobID))
|
||||||
|
|
||||||
|
cases := []struct{ name, want string }{
|
||||||
|
{"foo[40000]", "element numbers must be at most"},
|
||||||
|
{"foo[3-1]", "reversed"},
|
||||||
|
{"foo[a]", "invalid"},
|
||||||
|
{"foo [1]", "invalid"},
|
||||||
|
{"foo[1", "invalid"},
|
||||||
|
}
|
||||||
|
for _, c := range cases {
|
||||||
|
f.dispatched = nil
|
||||||
|
assert400(t, "-J "+c.name, f.controlFilterKill(c.name), c.want)
|
||||||
|
if got := f.dispatchedIDs(); len(got) != 0 {
|
||||||
|
t.Errorf("-J %s: dispatched %v, want none", c.name, got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestJobHandler_ControlFilterEmptyNameSelectsNothing (design ApiMatch):
|
||||||
|
// -J '[1]' (control sends eq) matches no row, not the unnamed array.
|
||||||
|
func TestJobHandler_ControlFilterEmptyNameSelectsNothing(t *testing.T) {
|
||||||
|
f := newHandlerFixture(t)
|
||||||
|
Z := handlerTestArrayBase + 41<<15
|
||||||
|
elems := f.seedArrayElements(Z, "", "RUN", 1)
|
||||||
|
assertSeeded(t, uint64(elems[0].Unique_JobID))
|
||||||
|
|
||||||
|
w := f.controlFilterKill("[1]")
|
||||||
|
if w.Code != http.StatusOK {
|
||||||
|
t.Fatalf("status = %d, want 200; body: %s", w.Code, w.Body.String())
|
||||||
|
}
|
||||||
|
if got := f.dispatchedIDs(); len(got) != 0 {
|
||||||
|
t.Errorf("-J [1] dispatched %v, want none", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestJobHandler_ControlFilterMostRecentOneElementArray (5.10): with no
|
||||||
|
// element number in the filter, when the newest matching job is the element
|
||||||
|
// of a one-element array, only that element is acted on.
|
||||||
|
func TestJobHandler_ControlFilterMostRecentOneElementArray(t *testing.T) {
|
||||||
|
f := newHandlerFixture(t)
|
||||||
|
O := handlerTestOrdinaryBase + 50
|
||||||
|
f.seedOrdinaryJob(O, "foo", "RUN")
|
||||||
|
A := handlerTestArrayBase + 42<<15
|
||||||
|
a1 := backend.MakeElementID(A, 1)
|
||||||
|
f.seedArrayElements(A, "foo", "RUN", 1)
|
||||||
|
assertSeeded(t, O, a1)
|
||||||
|
|
||||||
|
r := decodeControlReply(t, f.controlFilterKill("foo"))
|
||||||
|
assertExactIDs(t, "-J foo dispatched", f.dispatchedIDs(), []uint64{a1})
|
||||||
|
assertExactIDs(t, "-J foo accepted", itemIDs(t, r.Accepted), []uint64{a1})
|
||||||
|
|
||||||
|
// all-jobs: the rule does not apply, every matching job is acted on
|
||||||
|
f.dispatched = nil
|
||||||
|
w := f.controlByFilter(models.JobControlRequest{
|
||||||
|
Filter: models.JobControlFilter{JobName: "foo", AllJobs: true},
|
||||||
|
Action: "kill",
|
||||||
|
})
|
||||||
|
decodeControlReply(t, w)
|
||||||
|
assertExactIDs(t, "-J foo -a dispatched", f.dispatchedIDs(), []uint64{O, a1})
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestJobHandler_ControlFilterElementSelection (5.2): -J 'foo[1]' acts on
|
||||||
|
// exactly element 1 of every array named foo -- both one-element arrays and
|
||||||
|
// element 1 of a larger one -- never on an ordinary job named foo, another
|
||||||
|
// element, or another name, whatever the newest row is.
|
||||||
|
func TestJobHandler_ControlFilterElementSelection(t *testing.T) {
|
||||||
|
f := newHandlerFixture(t)
|
||||||
|
O := handlerTestOrdinaryBase + 51
|
||||||
|
f.seedOrdinaryJob(O, "foo", "RUN")
|
||||||
|
A := handlerTestArrayBase + 43<<15
|
||||||
|
B := handlerTestArrayBase + 44<<15
|
||||||
|
C := handlerTestArrayBase + 45<<15
|
||||||
|
D := handlerTestArrayBase + 46<<15
|
||||||
|
f.seedArrayElements(A, "foo", "RUN", 1)
|
||||||
|
f.seedArrayElements(B, "foo", "RUN", 1)
|
||||||
|
f.seedArrayElements(C, "foo", "RUN", 1, 2)
|
||||||
|
f.seedArrayElements(D, "bar", "RUN", 1)
|
||||||
|
a1, b1, c1 := backend.MakeElementID(A, 1), backend.MakeElementID(B, 1), backend.MakeElementID(C, 1)
|
||||||
|
assertSeeded(t, O, a1, b1, c1, backend.MakeElementID(C, 2), backend.MakeElementID(D, 1))
|
||||||
|
|
||||||
|
r := decodeControlReply(t, f.controlFilterKill("foo[1]"))
|
||||||
|
assertExactIDs(t, "-J foo[1] dispatched", f.dispatchedIDs(), []uint64{a1, b1, c1})
|
||||||
|
assertExactIDs(t, "-J foo[1] accepted", itemIDs(t, r.Accepted), []uint64{a1, b1, c1})
|
||||||
|
|
||||||
|
// the range form 1-1 selects the same element 1 of every array named foo
|
||||||
|
f.dispatched = nil
|
||||||
|
r = decodeControlReply(t, f.controlFilterKill("foo[1-1]"))
|
||||||
|
assertExactIDs(t, "-J foo[1-1] dispatched", f.dispatchedIDs(), []uint64{a1, b1, c1})
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestJobHandler_ControlFilterElementSelectionNewestSingleElement (5.2,
|
||||||
|
// 5.10): when the most recent job named foo is the element of a one-element
|
||||||
|
// array, -J 'foo[1]' still acts on element 1 of every array named foo (the
|
||||||
|
// most-recent rule applies only to filters without an element number),
|
||||||
|
// while -J 'foo' acts on that most recent element alone.
|
||||||
|
func TestJobHandler_ControlFilterElementSelectionNewestSingleElement(t *testing.T) {
|
||||||
|
f := newHandlerFixture(t)
|
||||||
|
O := handlerTestOrdinaryBase + 53
|
||||||
|
f.seedOrdinaryJob(O, "foo", "RUN")
|
||||||
|
A := handlerTestArrayBase + 47<<15
|
||||||
|
B := handlerTestArrayBase + 48<<15 // highest ID: the newest row
|
||||||
|
f.seedArrayElements(A, "foo", "RUN", 1)
|
||||||
|
f.seedArrayElements(B, "foo", "RUN", 1)
|
||||||
|
a1, b1 := backend.MakeElementID(A, 1), backend.MakeElementID(B, 1)
|
||||||
|
assertSeeded(t, O, a1, b1)
|
||||||
|
|
||||||
|
r := decodeControlReply(t, f.controlFilterKill("foo[1]"))
|
||||||
|
assertExactIDs(t, "-J foo[1] dispatched", f.dispatchedIDs(), []uint64{a1, b1})
|
||||||
|
assertExactIDs(t, "-J foo[1] accepted", itemIDs(t, r.Accepted), []uint64{a1, b1})
|
||||||
|
|
||||||
|
f.dispatched = nil
|
||||||
|
r = decodeControlReply(t, f.controlFilterKill("foo"))
|
||||||
|
assertExactIDs(t, "-J foo dispatched", f.dispatchedIDs(), []uint64{b1})
|
||||||
|
assertExactIDs(t, "-J foo accepted", itemIDs(t, r.Accepted), []uint64{b1})
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestJobHandler_ControlFilterPlainNameWithSpaces: an unnamed job carries
|
||||||
|
// its command as its name; kill -J 'sleep 100' keeps working (no brackets,
|
||||||
|
// no selection parsing).
|
||||||
|
func TestJobHandler_ControlFilterPlainNameWithSpaces(t *testing.T) {
|
||||||
|
f := newHandlerFixture(t)
|
||||||
|
O := handlerTestOrdinaryBase + 52
|
||||||
|
f.seedOrdinaryJob(O, "sleep 100", "RUN")
|
||||||
|
f.seedOrdinaryJob(O+1, "sleep 200", "RUN")
|
||||||
|
|
||||||
|
r := decodeControlReply(t, f.controlFilterKill("sleep 100"))
|
||||||
|
assertExactIDs(t, "-J 'sleep 100' dispatched", f.dispatchedIDs(), []uint64{O})
|
||||||
|
assertExactIDs(t, "-J 'sleep 100' accepted", itemIDs(t, r.Accepted), []uint64{O})
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue