package controllers import ( "errors" "sort" "strconv" "strings" "testing" "apiserver/backend" "apiserver/models" "apiserver/store" "apiserver/utils" ) // Pure tests for the selection predicate builder (job-array-one-based-index // task 7.2, design "ApiSel and ApiMatch" -> Matching). Each case builds the // predicates from parsed filters and evaluates them, together with the // remaining plain conditions, through store.ExecuteQuery over rows shaped // like store.Jobs (store.StructToOrderMap of models.JobInfo). // Written identities used below. Array keys have a sequence that is a // multiple of 32768 (array-capable); mOrd is an ordinary job. const ( mX = "3-5-1700000000-65536" // array "foo", elements 1, 2, 3 mY = "3-5-1700000000-98304" // array "foobar", elements 1, 2 mZ = "3-5-1700000000-196608" // unnamed array (job_name ""), element 1 mW = "3-5-1700000000-163840" // pre-upgrade array "old", elements 0, 1 mP = "3-5-1700000000-131072" // ordinary job "foo" on an array-capable key mOrd = "3-5-1700000000-7" // ordinary job "plain" ) func mKey(t *testing.T, written string) uint64 { t.Helper() k, err := backend.ParseHumanJobIDStrict(written) if err != nil { t.Fatalf("ParseHumanJobIDStrict(%q): %v", written, err) } return k } func mDec(t *testing.T, written string) string { return strconv.FormatUint(mKey(t, written), 10) } func mElemRow(key uint64, name string, k int) utils.OrderMap { elem := backend.MakeElementID(key, k) return store.StructToOrderMap(models.JobInfo{ Unique_JobID: int64(elem), JobID: int64(backend.ExtractArrayBaseID(elem)), ArrayIdx: backend.ExtractArrayIdx(elem), JobName: name, }) } func mOrdRow(id uint64, name string) utils.OrderMap { return store.StructToOrderMap(models.JobInfo{ Unique_JobID: int64(id), JobID: int64(id), ArrayIdx: 0, JobName: name, }) } // mRows returns the fixture rows and a label for every unique_job_id. func mRows(t *testing.T) ([]utils.OrderMap, map[int64]string) { t.Helper() labels := map[int64]string{} var rows []utils.OrderMap addElem := func(label, written, name string, k int) { r := mElemRow(mKey(t, written), name, k) rows = append(rows, r) labels[r.Data["unique_job_id"].(int64)] = label } addOrd := func(label, written, name string) { r := mOrdRow(mKey(t, written), name) rows = append(rows, r) labels[r.Data["unique_job_id"].(int64)] = label } for k := 1; k <= 3; k++ { addElem("X"+strconv.Itoa(k), mX, "foo", k) } addElem("Y1", mY, "foobar", 1) addElem("Y2", mY, "foobar", 2) addElem("Z1", mZ, "", 1) addElem("W0", mW, "old", 0) addElem("W1", mW, "old", 1) addOrd("P", mP, "foo") addOrd("O", mOrd, "plain") return rows, labels } var mAll = []string{"O", "P", "W0", "W1", "X1", "X2", "X3", "Y1", "Y2", "Z1"} func mWithout(skip ...string) []string { out := []string{} for _, l := range mAll { drop := false for _, s := range skip { if l == s { drop = true } } if !drop { out = append(out, l) } } return out } // mRun builds the predicates for filters and evaluates them with the // remaining plain conditions. It returns the sorted labels of the matching // rows and the remaining plain filters. func mRun(t *testing.T, filters []utils.FilterCondition) ([]string, []utils.FilterCondition, error) { t.Helper() rows, labels := mRows(t) plain, preds, err := BuildSelectionPredicates(filters) if err != nil { return nil, plain, err } conds, err := utils.BuildFilterConditions(plain, &models.JobInfo{}) if err != nil { t.Fatalf("BuildFilterConditions(%v): %v", plain, err) } got, _ := store.ExecuteQuery(rows, &utils.QueryOptions{ Conditions: conds, Predicates: preds, Limit: 1000, }, nil) out := []string{} for _, r := range got { out = append(out, labels[r.Data["unique_job_id"].(int64)]) } sort.Strings(out) return out, plain, nil } func selF(field, op, value string) utils.FilterCondition { return utils.FilterCondition{Field: field, Operator: op, Value: value} } func TestBuildSelectionPredicates_Matching(t *testing.T) { x, y, w, p := mX, mY, mW, mP cases := []struct { name string filters func(t *testing.T) []utils.FilterCondition want []string }{ // identity selections: eq / in / between select, notin excludes {"job_id eq single element (bug-1898)", func(*testing.T) []utils.FilterCondition { return []utils.FilterCondition{selF("job_id", "eq", x+"[2]")} }, []string{"X2"}}, {"job_id in range", func(*testing.T) []utils.FilterCondition { return []utils.FilterCondition{selF("job_id", "in", "("+x+"[1-2])")} }, []string{"X1", "X2"}}, {"job_id in range on decimal key", func(t *testing.T) []utils.FilterCondition { return []utils.FilterCondition{selF("job_id", "in", "("+mDec(t, x)+"[2-3])")} }, []string{"X2", "X3"}}, {"job_id between with bracketed values is evaluated like in", func(*testing.T) []utils.FilterCondition { return []utils.FilterCondition{selF("job_id", "between", "("+x+"[1],"+x+"[3])")} }, []string{"X1", "X3"}}, {"job_id notin excludes the element only", func(*testing.T) []utils.FilterCondition { return []utils.FilterCondition{selF("job_id", "notin", "("+x+"[1])")} }, mWithout("X1")}, {"job_id notin with bare and bracketed values", func(*testing.T) []utils.FilterCondition { return []utils.FilterCondition{selF("job_id", "notin", "("+y+","+x+"[1])")} }, mWithout("X1", "Y1", "Y2")}, {"element 0 of an ordinary job's id lists nothing (5.11)", func(*testing.T) []utils.FilterCondition { return []utils.FilterCondition{selF("job_id", "eq", p+"[0]")} }, []string{}}, {"pre-upgrade element 0 selected alone (8.2)", func(*testing.T) []utils.FilterCondition { return []utils.FilterCondition{selF("job_id", "eq", w+"[0]")} }, []string{"W0"}}, {"pre-upgrade element 0 range form", func(*testing.T) []utils.FilterCondition { return []utils.FilterCondition{selF("job_id", "eq", w+"[0-0]")} }, []string{"W0"}}, {"bare array id inside a converted entry selects the whole array (5.6)", func(*testing.T) []utils.FilterCondition { return []utils.FilterCondition{selF("job_id", "in", "("+x+","+y+"[1])")} }, []string{"X1", "X2", "X3", "Y1"}}, {"bare id inside a converted entry matches an ordinary job (5.6)", func(*testing.T) []utils.FilterCondition { return []utils.FilterCondition{selF("job_id", "in", "("+p+","+y+"[1])")} }, []string{"P", "Y1"}}, {"mixed decimal ordinary id and element (job_id:in:(100,X[1]))", func(t *testing.T) []utils.FilterCondition { return []utils.FilterCondition{selF("job_id", "in", "("+mDec(t, mOrd)+","+x+"[1])")} }, []string{"O", "X1"}}, {"one-element selection behaves as an array (7.8)", func(*testing.T) []utils.FilterCondition { return []utils.FilterCondition{selF("job_id", "eq", mZ+"[1]")} }, []string{"Z1"}}, // name selections {"job_name eq element of named arrays only", func(*testing.T) []utils.FilterCondition { return []utils.FilterCondition{selF("job_name", "eq", "foo[2]")} }, []string{"X2"}}, {"job_name eq never matches an ordinary job of that name", func(*testing.T) []utils.FilterCondition { return []utils.FilterCondition{selF("job_name", "eq", "foo[0-3]")} }, []string{"X1", "X2", "X3"}}, {"job_name in ORs selections", func(*testing.T) []utils.FilterCondition { return []utils.FilterCondition{selF("job_name", "in", "(foo[1],foobar[2])")} }, []string{"X1", "Y2"}}, {"job_name in with a bare name keeps the plain match", func(*testing.T) []utils.FilterCondition { return []utils.FilterCondition{selF("job_name", "in", "(plain, foobar[2])")} }, []string{"O", "Y2"}}, {"job_name notin excludes whole selections", func(*testing.T) []utils.FilterCondition { return []utils.FilterCondition{selF("job_name", "notin", "(foo[1])")} }, mWithout("X1")}, {"empty name under eq matches no unnamed element", func(*testing.T) []utils.FilterCondition { return []utils.FilterCondition{selF("job_name", "eq", "[1]")} }, []string{}}, {"empty name under in matches nothing", func(*testing.T) []utils.FilterCondition { return []utils.FilterCondition{selF("job_name", "in", "([1])")} }, []string{}}, {"empty name under notin excludes nothing", func(*testing.T) []utils.FilterCondition { return []utils.FilterCondition{selF("job_name", "notin", "([1])")} }, mAll}, {"empty name under contains matches every element 1, unnamed included", func(*testing.T) []utils.FilterCondition { return []utils.FilterCondition{selF("job_name", "contains", "[1]")} }, []string{"W1", "X1", "Y1", "Z1"}}, {"startwith uses today's substring semantics on the name part", func(*testing.T) []utils.FilterCondition { return []utils.FilterCondition{selF("job_name", "startwith", "foo[1]")} }, []string{"X1", "Y1"}}, {"contains is case-insensitive like today", func(*testing.T) []utils.FilterCondition { return []utils.FilterCondition{selF("job_name", "contains", "FOO[2]")} }, []string{"X2", "Y2"}}, {"endwith on the name part", func(*testing.T) []utils.FilterCondition { return []utils.FilterCondition{selF("job_name", "endwith", "bar[1-2]")} }, []string{"Y1", "Y2"}}, {"notcontains negates the name part only", func(*testing.T) []utils.FilterCondition { return []utils.FilterCondition{selF("job_name", "notcontains", "foo[1]")} }, []string{"W1", "Z1"}}, // shape: entries AND'ed with each other and with plain conditions {"identity and name selections intersect", func(*testing.T) []utils.FilterCondition { return []utils.FilterCondition{ selF("job_id", "in", "("+x+"[1-2],"+y+"[1])"), selF("job_name", "eq", "foobar[1-2]"), } }, []string{"Y1"}}, {"another operator entry on the same field stays a plain condition", func(t *testing.T) []utils.FilterCondition { return []utils.FilterCondition{ selF("job_id", "in", "("+x+"[1-3])"), selF("job_id", "ne", mDec(t, x)), } }, []string{}}, {"plain condition on another field is AND'ed", func(*testing.T) []utils.FilterCondition { return []utils.FilterCondition{ selF("job_id", "in", "("+x+"[1-3])"), selF("array_idx", "gte", "2"), } }, []string{"X2", "X3"}}, } for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { got, _, err := mRun(t, tc.filters(t)) if err != nil { t.Fatalf("unexpected error: %v", err) } if strings.Join(got, ",") != strings.Join(tc.want, ",") { t.Fatalf("matched %v, want %v", got, tc.want) } }) } } // The same written-form bare key matches the same job alone (plain path, // normalised to decimal) and mixed with a bracketed value (converted path). func TestBuildSelectionPredicates_BareKeyBothPaths(t *testing.T) { for _, tc := range []struct { name string filters []utils.FilterCondition want []string }{ {"eq alone", []utils.FilterCondition{selF("job_id", "eq", mOrd)}, []string{"O"}}, {"in alone", []utils.FilterCondition{selF("job_id", "in", "("+mOrd+")")}, []string{"O"}}, {"in mixed with a bracketed value", []utils.FilterCondition{selF("job_id", "in", "("+mOrd+","+mX+"[1])")}, []string{"O", "X1"}}, {"array id alone", []utils.FilterCondition{selF("job_id", "eq", mY)}, []string{"Y1", "Y2"}}, {"array id mixed", []utils.FilterCondition{selF("job_id", "in", "("+mY+","+mX+"[1])")}, []string{"X1", "Y1", "Y2"}}, {"notin alone", []utils.FilterCondition{selF("job_id", "notin", "("+mOrd+")")}, mWithout("O")}, {"ne alone", []utils.FilterCondition{selF("job_id", "ne", mOrd)}, mWithout("O")}, } { t.Run(tc.name, func(t *testing.T) { got, _, err := mRun(t, tc.filters) if err != nil { t.Fatalf("unexpected error: %v", err) } if strings.Join(got, ",") != strings.Join(tc.want, ",") { t.Fatalf("matched %v, want %v", got, tc.want) } }) } } func TestBuildSelectionPredicates_PlainFilters(t *testing.T) { dec := mDec(t, mOrd) in := []utils.FilterCondition{ selF("job_id", "eq", mOrd), selF("job_id", "in", "("+mOrd+", "+mDec(t, mX)+")"), selF("job_id", "gt", "5"), selF("job_name", "eq", "plain"), selF("queue_name", "eq", "normal"), } orig := append([]utils.FilterCondition(nil), in...) plain, preds, err := BuildSelectionPredicates(in) if err != nil { t.Fatalf("unexpected error: %v", err) } if len(preds) != 0 { t.Fatalf("got %d predicates for plain filters, want 0", len(preds)) } want := []utils.FilterCondition{ selF("job_id", "eq", dec), selF("job_id", "in", "("+dec+","+mDec(t, mX)+")"), selF("job_id", "gt", "5"), selF("job_name", "eq", "plain"), selF("queue_name", "eq", "normal"), } if len(plain) != len(want) { t.Fatalf("plain = %v, want %v", plain, want) } for i := range want { if plain[i] != want[i] { t.Errorf("plain[%d] = %+v, want %+v", i, plain[i], want[i]) } } for i := range orig { if in[i] != orig[i] { t.Errorf("input filter %d modified: %+v, was %+v", i, in[i], orig[i]) } } } func TestBuildSelectionPredicates_RemovesOnlyConvertedEntry(t *testing.T) { in := []utils.FilterCondition{ selF("job_id", "in", "("+mX+"[1])"), selF("job_id", "ne", "100"), selF("job_name", "contains", "fo"), } plain, preds, err := BuildSelectionPredicates(in) if err != nil { t.Fatalf("unexpected error: %v", err) } if len(preds) != 1 { t.Fatalf("got %d predicates, want 1", len(preds)) } want := []utils.FilterCondition{selF("job_id", "ne", "100"), selF("job_name", "contains", "fo")} if len(plain) != 2 || plain[0] != want[0] || plain[1] != want[1] { t.Fatalf("plain = %v, want %v", plain, want) } } func TestBuildSelectionPredicates_Errors(t *testing.T) { elemDec := strconv.FormatUint(backend.MakeElementID(mKey(t, mX), 2), 10) for _, tc := range []struct { name string filter utils.FilterCondition rule string // *SelError rule, or "" for a plain error msgPart string }{ {"job_id ne bracketed", selF("job_id", "ne", mX+"[1]"), "", "operator doesn't support"}, {"job_id lt bracketed", selF("job_id", "lt", mX+"[1]"), "", "operator doesn't support"}, {"job_id lte bracketed", selF("job_id", "lte", mX+"[1]"), "", "operator doesn't support"}, {"job_id gt bracketed", selF("job_id", "gt", mX+"[1]"), "", "operator doesn't support"}, {"job_id gte bracketed", selF("job_id", "gte", mX+"[1]"), "", "operator doesn't support"}, {"job_id null bracketed", selF("job_id", "null", mX+"[1]"), "", "operator doesn't support"}, {"job_id notnull bracketed", selF("job_id", "notnull", mX+"[1]"), "", "operator doesn't support"}, {"job_id contains bracketed", selF("job_id", "contains", mX+"[1]"), "", "unsupported operator"}, {"job_name ne bracketed", selF("job_name", "ne", "foo[1]"), "", "operator doesn't support"}, {"job_name null bracketed", selF("job_name", "null", "foo[1]"), "", "operator doesn't support"}, {"job_name notnull bracketed", selF("job_name", "notnull", "foo[1]"), "", "operator doesn't support"}, {"job_name between bracketed", selF("job_name", "between", "(a[1],b[1])"), "", "unsupported operator"}, {"multiple values under eq", selF("job_id", "eq", "("+mX+"[1],"+mY+"[1])"), "", "operator doesn't allow to use multiple values"}, {"element number too large", selF("job_id", "eq", mX+"[40000]"), "5.7", "40000"}, {"reversed name range", selF("job_name", "eq", "foo[3-1]"), "5.5", "foo[3-1]"}, {"bad name syntax", selF("job_name", "in", "(foo bar[1])"), "5.16", "foo bar[1]"}, {"not array-capable", selF("job_id", "eq", "3-5-1700000000-5[1]"), "5.8", "3-5-1700000000-5"}, {"bare value in converted entry checked", selF("job_id", "in", "(abc,"+mX+"[1])"), "5.16", "abc"}, {"plain marked decimal without index (5.17)", selF("job_id", "eq", elemDec), "5.17", elemDec}, {"plain in with marked decimal", selF("job_id", "in", "(1,"+elemDec+")"), "5.17", elemDec}, {"plain field too wide (5.14)", selF("job_id", "eq", "64-5-1700000000-7"), "5.14", "64-5-1700000000-7"}, } { t.Run(tc.name, func(t *testing.T) { _, preds, err := BuildSelectionPredicates([]utils.FilterCondition{tc.filter}) if err == nil { t.Fatalf("expected an error, got %d predicates", len(preds)) } var se *SelError if errors.As(err, &se) { if tc.rule == "" { t.Fatalf("got selection error %q (rule %s), want plain error containing %q", se.Msg, se.Rule, tc.msgPart) } if se.Rule != tc.rule { t.Fatalf("rule = %s (%q), want %s", se.Rule, se.Msg, tc.rule) } } else if tc.rule != "" { t.Fatalf("got plain error %q, want selection error rule %s", err, tc.rule) } if !strings.Contains(err.Error(), tc.msgPart) { t.Fatalf("error %q does not contain %q", err, tc.msgPart) } }) } }