Skip to content

Commit 97a042d

Browse files
authored
Add not missing filter for experiments table (#4308)
1 parent bd32ced commit 97a042d

File tree

5 files changed

+296
-11
lines changed

5 files changed

+296
-11
lines changed
Lines changed: 271 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,271 @@
1+
import { FilterDefinition, filterExperiment, Operator } from '.'
2+
import { buildMetricOrParamPath } from '../../columns/paths'
3+
import { Experiment, ColumnType } from '../../webview/contract'
4+
5+
describe('filterExperiment', () => {
6+
const paramsFile = 'params.yaml'
7+
const experiments = [
8+
{
9+
Created: '2020-12-29T12:00:01',
10+
id: 1,
11+
params: {
12+
'params.yaml': {
13+
bool: true,
14+
filter: 1,
15+
sort: 1,
16+
text: 'abcdefghijklmnop'
17+
}
18+
}
19+
},
20+
{
21+
Created: '2020-12-30T12:00:01',
22+
id: 2,
23+
params: {
24+
'params.yaml': {
25+
bool: false,
26+
filter: 2,
27+
sort: 1,
28+
text: 'fun'
29+
}
30+
}
31+
},
32+
{
33+
Created: '2021-01-01T00:00:01',
34+
id: 3,
35+
params: {
36+
'params.yaml': {
37+
bool: null,
38+
filter: 3,
39+
sort: 1,
40+
text: 'not missing'
41+
}
42+
}
43+
}
44+
] as unknown as Experiment[]
45+
46+
const filterExperiments = (filters: FilterDefinition[]): Experiment[] =>
47+
experiments
48+
.map(experiment => filterExperiment(filters, experiment))
49+
.filter(Boolean) as Experiment[]
50+
51+
it('should not filter experiments if they do not have the provided value (for queued experiments)', () => {
52+
const unfiltered = filterExperiments([
53+
{
54+
operator: Operator.IS_FALSE,
55+
path: buildMetricOrParamPath(ColumnType.METRICS, 'metrics.json', 'acc'),
56+
value: undefined
57+
}
58+
])
59+
60+
expect(
61+
unfiltered
62+
.map(
63+
experiment => experiment[ColumnType.METRICS]?.['metrics.json']?.acc
64+
)
65+
.filter(Boolean)
66+
).toHaveLength(0)
67+
68+
expect(unfiltered).toStrictEqual(experiments)
69+
})
70+
71+
it('should filter experiments if they do not have the provided value and not missing is used', () => {
72+
const unfiltered = filterExperiments([
73+
{
74+
operator: Operator.NOT_MISSING,
75+
path: buildMetricOrParamPath(ColumnType.METRICS, 'metrics.json', 'acc'),
76+
value: undefined
77+
}
78+
])
79+
80+
expect(unfiltered).toStrictEqual([])
81+
})
82+
83+
it('should not filter the experiments if no filters are provided', () => {
84+
const unfiltered = filterExperiments([])
85+
86+
expect(unfiltered).toStrictEqual(experiments)
87+
})
88+
89+
it('should filter the experiments with a greater than filter', () => {
90+
const unfiltered = filterExperiments([
91+
{
92+
operator: Operator.GREATER_THAN,
93+
path: buildMetricOrParamPath(ColumnType.PARAMS, paramsFile, 'filter'),
94+
value: '2'
95+
}
96+
])
97+
98+
expect(unfiltered.map(experiment => experiment.id)).toStrictEqual([3])
99+
})
100+
101+
it('should filter experiments by an equals filter', () => {
102+
const unfiltered = filterExperiments([
103+
{
104+
operator: Operator.EQUAL,
105+
path: buildMetricOrParamPath(ColumnType.PARAMS, paramsFile, 'filter'),
106+
value: '2'
107+
}
108+
])
109+
110+
expect(unfiltered.map(experiment => experiment.id)).toStrictEqual([2])
111+
})
112+
113+
it('should filter experiments by a not equals filter', () => {
114+
const unfiltered = filterExperiments([
115+
{
116+
operator: Operator.NOT_EQUAL,
117+
path: buildMetricOrParamPath(ColumnType.PARAMS, paramsFile, 'filter'),
118+
value: '2'
119+
}
120+
])
121+
122+
expect(unfiltered.map(experiment => experiment.id)).toStrictEqual([1, 3])
123+
})
124+
125+
it('should filter experiments by multiple filters', () => {
126+
const unfiltered = filterExperiments([
127+
{
128+
operator: Operator.GREATER_THAN,
129+
path: buildMetricOrParamPath(ColumnType.PARAMS, paramsFile, 'filter'),
130+
value: '0'
131+
},
132+
{
133+
operator: Operator.LESS_THAN_OR_EQUAL,
134+
path: buildMetricOrParamPath(ColumnType.PARAMS, paramsFile, 'filter'),
135+
value: '2'
136+
}
137+
])
138+
139+
expect(unfiltered.map(experiment => experiment.id)).toStrictEqual([1, 2])
140+
})
141+
142+
it('should filter experiments by multiple filters on multiple params', () => {
143+
const unfiltered = filterExperiments([
144+
{
145+
operator: Operator.GREATER_THAN_OR_EQUAL,
146+
path: buildMetricOrParamPath(ColumnType.PARAMS, paramsFile, 'filter'),
147+
value: '0'
148+
},
149+
{
150+
operator: Operator.LESS_THAN,
151+
path: buildMetricOrParamPath(ColumnType.PARAMS, paramsFile, 'filter'),
152+
value: '10'
153+
},
154+
{
155+
operator: Operator.EQUAL,
156+
path: buildMetricOrParamPath(ColumnType.PARAMS, paramsFile, 'sort'),
157+
value: '10'
158+
}
159+
])
160+
161+
expect(unfiltered).toStrictEqual([])
162+
})
163+
164+
it('should filter experiments using string contains', () => {
165+
const unfiltered = filterExperiments([
166+
{
167+
operator: Operator.CONTAINS,
168+
path: buildMetricOrParamPath(ColumnType.PARAMS, paramsFile, 'text'),
169+
value: 'def'
170+
}
171+
])
172+
173+
expect(unfiltered.map(experiment => experiment.id)).toStrictEqual([1])
174+
})
175+
176+
it('should filter experiments if given a numeric column to filter with string contains', () => {
177+
const unfiltered = filterExperiments([
178+
{
179+
operator: Operator.CONTAINS,
180+
path: buildMetricOrParamPath(ColumnType.PARAMS, paramsFile, 'filter'),
181+
value: '1'
182+
}
183+
])
184+
185+
expect(unfiltered).toStrictEqual([])
186+
})
187+
188+
it('should filter experiments when given a numeric column to filter with string does not contain', () => {
189+
const unfiltered = filterExperiments([
190+
{
191+
operator: Operator.NOT_CONTAINS,
192+
path: buildMetricOrParamPath(ColumnType.PARAMS, paramsFile, 'filter'),
193+
value: '1'
194+
}
195+
])
196+
197+
expect(unfiltered).toStrictEqual(experiments)
198+
})
199+
200+
it('should filter experiments using string does not contain', () => {
201+
const unfiltered = filterExperiments([
202+
{
203+
operator: Operator.NOT_CONTAINS,
204+
path: buildMetricOrParamPath(ColumnType.PARAMS, paramsFile, 'text'),
205+
value: 'def'
206+
}
207+
])
208+
209+
expect(unfiltered.map(experiment => experiment.id)).toStrictEqual([2, 3])
210+
})
211+
212+
it('should split the experiments using boolean is true', () => {
213+
const unfiltered = filterExperiments([
214+
{
215+
operator: Operator.IS_TRUE,
216+
path: buildMetricOrParamPath(ColumnType.PARAMS, paramsFile, 'bool'),
217+
value: undefined
218+
}
219+
])
220+
221+
expect(unfiltered.map(experiment => experiment.id)).toStrictEqual([1])
222+
})
223+
224+
it('should split the experiments using boolean is false', () => {
225+
const unfiltered = filterExperiments([
226+
{
227+
operator: Operator.IS_FALSE,
228+
path: buildMetricOrParamPath(ColumnType.PARAMS, paramsFile, 'bool'),
229+
value: undefined
230+
}
231+
])
232+
233+
expect(unfiltered.map(experiment => experiment.id)).toStrictEqual([2])
234+
})
235+
236+
it('should split the experiments using after Created date', () => {
237+
const unfiltered = filterExperiments([
238+
{
239+
operator: Operator.AFTER_DATE,
240+
path: 'Created',
241+
value: '2020-12-31T15:40:00'
242+
}
243+
])
244+
245+
expect(unfiltered.map(experiment => experiment.id)).toStrictEqual([3])
246+
})
247+
248+
it('should split the experiments using before Created date', () => {
249+
const unfiltered = filterExperiments([
250+
{
251+
operator: Operator.BEFORE_DATE,
252+
path: 'Created',
253+
value: '2020-12-31T15:40:00'
254+
}
255+
])
256+
257+
expect(unfiltered.map(experiment => experiment.id)).toStrictEqual([1, 2])
258+
})
259+
260+
it('should split the experiments using on Created date', () => {
261+
const unfiltered = filterExperiments([
262+
{
263+
operator: Operator.ON_DATE,
264+
path: 'Created',
265+
value: '2020-12-31T15:40:00'
266+
}
267+
])
268+
269+
expect(unfiltered.map(experiment => experiment.id)).toStrictEqual([])
270+
})
271+
})

extension/src/experiments/model/filterBy/index.ts

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,15 +5,17 @@ import { definedAndNonEmpty } from '../../../util/array'
55
import { splitColumnPath } from '../../columns/paths'
66

77
export enum Operator {
8-
EQUAL = '==',
8+
EQUAL = '=',
99
GREATER_THAN = '>',
1010
GREATER_THAN_OR_EQUAL = '>=',
1111
LESS_THAN = '<',
1212
LESS_THAN_OR_EQUAL = '<=',
13-
NOT_EQUAL = '!=',
13+
NOT_EQUAL = '≠',
14+
15+
NOT_MISSING = '≠Ø',
1416

1517
CONTAINS = '∈',
16-
NOT_CONTAINS = '!∈',
18+
NOT_CONTAINS = '',
1719

1820
IS_TRUE = '⊤',
1921
IS_FALSE = '⊥',
@@ -56,9 +58,11 @@ const evaluate = <T extends string | number | boolean>(
5658
filterValue: T
5759
): boolean => {
5860
if (valueToEvaluate === undefined) {
59-
return true
61+
return operator !== Operator.NOT_MISSING
6062
}
6163
switch (operator) {
64+
case Operator.NOT_MISSING:
65+
return true
6266
case Operator.GREATER_THAN:
6367
return valueToEvaluate > filterValue
6468
case Operator.LESS_THAN:

extension/src/experiments/model/filterBy/quickPick.ts

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,12 @@ export const OPERATORS = [
2121
types: ['number', 'string'],
2222
value: Operator.NOT_EQUAL
2323
},
24+
{
25+
description: 'Not Missing',
26+
label: Operator.NOT_MISSING,
27+
types: ['string', 'boolean', 'number'],
28+
value: Operator.NOT_MISSING
29+
},
2430
{
2531
description: 'Is true',
2632
label: Operator.IS_TRUE,
@@ -130,7 +136,11 @@ export const pickFilterToAdd = async (
130136
return
131137
}
132138

133-
if ([Operator.IS_TRUE, Operator.IS_FALSE].includes(operator)) {
139+
if (
140+
[Operator.IS_TRUE, Operator.IS_FALSE, Operator.NOT_MISSING].includes(
141+
operator
142+
)
143+
) {
134144
return {
135145
operator,
136146
path: picked.path,

extension/src/experiments/model/filterBy/tree.test.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -85,12 +85,12 @@ describe('ExperimentsFilterByTree', () => {
8585
const filters = await experimentsFilterByTree.getChildren()
8686
expect(filters).toStrictEqual([
8787
{
88-
description: '== 90000',
88+
description: '= 90000',
8989
dvcRoot: 'demo',
9090
id: buildMetricOrParamPath(
9191
ColumnType.PARAMS,
9292
'params.yaml',
93-
'param==90000'
93+
'param=90000'
9494
),
9595
label: buildMetricOrParamPath(ColumnType.PARAMS, 'params.yaml', 'param')
9696
}
@@ -119,7 +119,7 @@ describe('ExperimentsFilterByTree', () => {
119119
it("should return the dvcRoot's filters if one is provided", async () => {
120120
const mockedFilters = [
121121
{
122-
operator: '==',
122+
operator: '=',
123123
path: buildMetricOrParamPath(ColumnType.PARAMS, 'params.yml', 'param'),
124124
value: 90000
125125
},
@@ -146,12 +146,12 @@ describe('ExperimentsFilterByTree', () => {
146146

147147
expect(filters).toStrictEqual([
148148
{
149-
description: '== 90000',
149+
description: '= 90000',
150150
dvcRoot: 'demo',
151151
id: buildMetricOrParamPath(
152152
ColumnType.PARAMS,
153153
'params.yml',
154-
'param==90000'
154+
'param=90000'
155155
),
156156
label: buildMetricOrParamPath(ColumnType.PARAMS, 'params.yml', 'param')
157157
},

extension/src/test/suite/experiments/index.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1723,7 +1723,7 @@ suite('Experiments Test Suite', () => {
17231723
const firstFilterId = buildMetricOrParamPath(
17241724
ColumnType.PARAMS,
17251725
'params.yaml',
1726-
'test==1'
1726+
'test=1'
17271727
)
17281728
const firstFilterDefinition = {
17291729
operator: Operator.EQUAL,

0 commit comments

Comments
 (0)