diff --git a/extension/src/experiments/model/filterBy/index.test.ts b/extension/src/experiments/model/filterBy/index.test.ts new file mode 100644 index 0000000000..a41e75b065 --- /dev/null +++ b/extension/src/experiments/model/filterBy/index.test.ts @@ -0,0 +1,271 @@ +import { FilterDefinition, filterExperiment, Operator } from '.' +import { buildMetricOrParamPath } from '../../columns/paths' +import { Experiment, ColumnType } from '../../webview/contract' + +describe('filterExperiment', () => { + const paramsFile = 'params.yaml' + const experiments = [ + { + Created: '2020-12-29T12:00:01', + id: 1, + params: { + 'params.yaml': { + bool: true, + filter: 1, + sort: 1, + text: 'abcdefghijklmnop' + } + } + }, + { + Created: '2020-12-30T12:00:01', + id: 2, + params: { + 'params.yaml': { + bool: false, + filter: 2, + sort: 1, + text: 'fun' + } + } + }, + { + Created: '2021-01-01T00:00:01', + id: 3, + params: { + 'params.yaml': { + bool: null, + filter: 3, + sort: 1, + text: 'not missing' + } + } + } + ] as unknown as Experiment[] + + const filterExperiments = (filters: FilterDefinition[]): Experiment[] => + experiments + .map(experiment => filterExperiment(filters, experiment)) + .filter(Boolean) as Experiment[] + + it('should not filter experiments if they do not have the provided value (for queued experiments)', () => { + const unfiltered = filterExperiments([ + { + operator: Operator.IS_FALSE, + path: buildMetricOrParamPath(ColumnType.METRICS, 'metrics.json', 'acc'), + value: undefined + } + ]) + + expect( + unfiltered + .map( + experiment => experiment[ColumnType.METRICS]?.['metrics.json']?.acc + ) + .filter(Boolean) + ).toHaveLength(0) + + expect(unfiltered).toStrictEqual(experiments) + }) + + it('should filter experiments if they do not have the provided value and not missing is used', () => { + const unfiltered = filterExperiments([ + { + operator: Operator.NOT_MISSING, + path: buildMetricOrParamPath(ColumnType.METRICS, 'metrics.json', 'acc'), + value: undefined + } + ]) + + expect(unfiltered).toStrictEqual([]) + }) + + it('should not filter the experiments if no filters are provided', () => { + const unfiltered = filterExperiments([]) + + expect(unfiltered).toStrictEqual(experiments) + }) + + it('should filter the experiments with a greater than filter', () => { + const unfiltered = filterExperiments([ + { + operator: Operator.GREATER_THAN, + path: buildMetricOrParamPath(ColumnType.PARAMS, paramsFile, 'filter'), + value: '2' + } + ]) + + expect(unfiltered.map(experiment => experiment.id)).toStrictEqual([3]) + }) + + it('should filter experiments by an equals filter', () => { + const unfiltered = filterExperiments([ + { + operator: Operator.EQUAL, + path: buildMetricOrParamPath(ColumnType.PARAMS, paramsFile, 'filter'), + value: '2' + } + ]) + + expect(unfiltered.map(experiment => experiment.id)).toStrictEqual([2]) + }) + + it('should filter experiments by a not equals filter', () => { + const unfiltered = filterExperiments([ + { + operator: Operator.NOT_EQUAL, + path: buildMetricOrParamPath(ColumnType.PARAMS, paramsFile, 'filter'), + value: '2' + } + ]) + + expect(unfiltered.map(experiment => experiment.id)).toStrictEqual([1, 3]) + }) + + it('should filter experiments by multiple filters', () => { + const unfiltered = filterExperiments([ + { + operator: Operator.GREATER_THAN, + path: buildMetricOrParamPath(ColumnType.PARAMS, paramsFile, 'filter'), + value: '0' + }, + { + operator: Operator.LESS_THAN_OR_EQUAL, + path: buildMetricOrParamPath(ColumnType.PARAMS, paramsFile, 'filter'), + value: '2' + } + ]) + + expect(unfiltered.map(experiment => experiment.id)).toStrictEqual([1, 2]) + }) + + it('should filter experiments by multiple filters on multiple params', () => { + const unfiltered = filterExperiments([ + { + operator: Operator.GREATER_THAN_OR_EQUAL, + path: buildMetricOrParamPath(ColumnType.PARAMS, paramsFile, 'filter'), + value: '0' + }, + { + operator: Operator.LESS_THAN, + path: buildMetricOrParamPath(ColumnType.PARAMS, paramsFile, 'filter'), + value: '10' + }, + { + operator: Operator.EQUAL, + path: buildMetricOrParamPath(ColumnType.PARAMS, paramsFile, 'sort'), + value: '10' + } + ]) + + expect(unfiltered).toStrictEqual([]) + }) + + it('should filter experiments using string contains', () => { + const unfiltered = filterExperiments([ + { + operator: Operator.CONTAINS, + path: buildMetricOrParamPath(ColumnType.PARAMS, paramsFile, 'text'), + value: 'def' + } + ]) + + expect(unfiltered.map(experiment => experiment.id)).toStrictEqual([1]) + }) + + it('should filter experiments if given a numeric column to filter with string contains', () => { + const unfiltered = filterExperiments([ + { + operator: Operator.CONTAINS, + path: buildMetricOrParamPath(ColumnType.PARAMS, paramsFile, 'filter'), + value: '1' + } + ]) + + expect(unfiltered).toStrictEqual([]) + }) + + it('should filter experiments when given a numeric column to filter with string does not contain', () => { + const unfiltered = filterExperiments([ + { + operator: Operator.NOT_CONTAINS, + path: buildMetricOrParamPath(ColumnType.PARAMS, paramsFile, 'filter'), + value: '1' + } + ]) + + expect(unfiltered).toStrictEqual(experiments) + }) + + it('should filter experiments using string does not contain', () => { + const unfiltered = filterExperiments([ + { + operator: Operator.NOT_CONTAINS, + path: buildMetricOrParamPath(ColumnType.PARAMS, paramsFile, 'text'), + value: 'def' + } + ]) + + expect(unfiltered.map(experiment => experiment.id)).toStrictEqual([2, 3]) + }) + + it('should split the experiments using boolean is true', () => { + const unfiltered = filterExperiments([ + { + operator: Operator.IS_TRUE, + path: buildMetricOrParamPath(ColumnType.PARAMS, paramsFile, 'bool'), + value: undefined + } + ]) + + expect(unfiltered.map(experiment => experiment.id)).toStrictEqual([1]) + }) + + it('should split the experiments using boolean is false', () => { + const unfiltered = filterExperiments([ + { + operator: Operator.IS_FALSE, + path: buildMetricOrParamPath(ColumnType.PARAMS, paramsFile, 'bool'), + value: undefined + } + ]) + + expect(unfiltered.map(experiment => experiment.id)).toStrictEqual([2]) + }) + + it('should split the experiments using after Created date', () => { + const unfiltered = filterExperiments([ + { + operator: Operator.AFTER_DATE, + path: 'Created', + value: '2020-12-31T15:40:00' + } + ]) + + expect(unfiltered.map(experiment => experiment.id)).toStrictEqual([3]) + }) + + it('should split the experiments using before Created date', () => { + const unfiltered = filterExperiments([ + { + operator: Operator.BEFORE_DATE, + path: 'Created', + value: '2020-12-31T15:40:00' + } + ]) + + expect(unfiltered.map(experiment => experiment.id)).toStrictEqual([1, 2]) + }) + + it('should split the experiments using on Created date', () => { + const unfiltered = filterExperiments([ + { + operator: Operator.ON_DATE, + path: 'Created', + value: '2020-12-31T15:40:00' + } + ]) + + expect(unfiltered.map(experiment => experiment.id)).toStrictEqual([]) + }) +}) diff --git a/extension/src/experiments/model/filterBy/index.ts b/extension/src/experiments/model/filterBy/index.ts index 78c42d6cae..4a342b2987 100644 --- a/extension/src/experiments/model/filterBy/index.ts +++ b/extension/src/experiments/model/filterBy/index.ts @@ -5,15 +5,17 @@ import { definedAndNonEmpty } from '../../../util/array' import { splitColumnPath } from '../../columns/paths' export enum Operator { - EQUAL = '==', + EQUAL = '=', GREATER_THAN = '>', GREATER_THAN_OR_EQUAL = '>=', LESS_THAN = '<', LESS_THAN_OR_EQUAL = '<=', - NOT_EQUAL = '!=', + NOT_EQUAL = '≠', + + NOT_MISSING = '≠Ø', CONTAINS = '∈', - NOT_CONTAINS = '!∈', + NOT_CONTAINS = '∉', IS_TRUE = '⊤', IS_FALSE = '⊥', @@ -56,9 +58,11 @@ const evaluate = ( filterValue: T ): boolean => { if (valueToEvaluate === undefined) { - return true + return operator !== Operator.NOT_MISSING } switch (operator) { + case Operator.NOT_MISSING: + return true case Operator.GREATER_THAN: return valueToEvaluate > filterValue case Operator.LESS_THAN: diff --git a/extension/src/experiments/model/filterBy/quickPick.ts b/extension/src/experiments/model/filterBy/quickPick.ts index 999830784b..26f5b77be3 100644 --- a/extension/src/experiments/model/filterBy/quickPick.ts +++ b/extension/src/experiments/model/filterBy/quickPick.ts @@ -21,6 +21,12 @@ export const OPERATORS = [ types: ['number', 'string'], value: Operator.NOT_EQUAL }, + { + description: 'Not Missing', + label: Operator.NOT_MISSING, + types: ['string', 'boolean', 'number'], + value: Operator.NOT_MISSING + }, { description: 'Is true', label: Operator.IS_TRUE, @@ -130,7 +136,11 @@ export const pickFilterToAdd = async ( return } - if ([Operator.IS_TRUE, Operator.IS_FALSE].includes(operator)) { + if ( + [Operator.IS_TRUE, Operator.IS_FALSE, Operator.NOT_MISSING].includes( + operator + ) + ) { return { operator, path: picked.path, diff --git a/extension/src/experiments/model/filterBy/tree.test.ts b/extension/src/experiments/model/filterBy/tree.test.ts index a61907f22e..3a027aa1b9 100644 --- a/extension/src/experiments/model/filterBy/tree.test.ts +++ b/extension/src/experiments/model/filterBy/tree.test.ts @@ -85,12 +85,12 @@ describe('ExperimentsFilterByTree', () => { const filters = await experimentsFilterByTree.getChildren() expect(filters).toStrictEqual([ { - description: '== 90000', + description: '= 90000', dvcRoot: 'demo', id: buildMetricOrParamPath( ColumnType.PARAMS, 'params.yaml', - 'param==90000' + 'param=90000' ), label: buildMetricOrParamPath(ColumnType.PARAMS, 'params.yaml', 'param') } @@ -119,7 +119,7 @@ describe('ExperimentsFilterByTree', () => { it("should return the dvcRoot's filters if one is provided", async () => { const mockedFilters = [ { - operator: '==', + operator: '=', path: buildMetricOrParamPath(ColumnType.PARAMS, 'params.yml', 'param'), value: 90000 }, @@ -146,12 +146,12 @@ describe('ExperimentsFilterByTree', () => { expect(filters).toStrictEqual([ { - description: '== 90000', + description: '= 90000', dvcRoot: 'demo', id: buildMetricOrParamPath( ColumnType.PARAMS, 'params.yml', - 'param==90000' + 'param=90000' ), label: buildMetricOrParamPath(ColumnType.PARAMS, 'params.yml', 'param') }, diff --git a/extension/src/test/suite/experiments/index.test.ts b/extension/src/test/suite/experiments/index.test.ts index 8858f0335a..8a9f0f9897 100644 --- a/extension/src/test/suite/experiments/index.test.ts +++ b/extension/src/test/suite/experiments/index.test.ts @@ -1723,7 +1723,7 @@ suite('Experiments Test Suite', () => { const firstFilterId = buildMetricOrParamPath( ColumnType.PARAMS, 'params.yaml', - 'test==1' + 'test=1' ) const firstFilterDefinition = { operator: Operator.EQUAL,