From e55f7d700cff9a043def616195df964a001ae7a4 Mon Sep 17 00:00:00 2001 From: Matt Seddon Date: Wed, 12 Oct 2022 12:39:59 +1100 Subject: [PATCH] Merge branch 'main' into add-eye --- CHANGELOG.md | 23 ++ README.md | 54 +--- demo/requirements.txt | 2 +- extension/package.json | 12 +- extension/package.nls.json | 4 +- extension/src/cli/dvc/constants.ts | 4 +- extension/src/cli/dvc/contract.ts | 19 +- extension/src/cli/dvc/discovery.ts | 225 +++++++++++++++ extension/src/cli/dvc/version.test.ts | 100 +++---- extension/src/cli/dvc/version.ts | 66 +++-- .../src/experiments/data/collect.test.ts | 5 +- extension/src/experiments/index.ts | 4 +- .../src/experiments/model/accumulator.ts | 4 +- .../src/experiments/model/collect.test.ts | 25 +- extension/src/experiments/model/collect.ts | 26 +- .../experiments/model/filterBy/quickPick.ts | 2 +- extension/src/experiments/model/index.test.ts | 13 +- extension/src/experiments/model/index.ts | 38 +-- .../experiments/model/sortBy/index.test.ts | 1 - .../experiments/model/status/collect.test.ts | 3 +- .../src/experiments/model/status/collect.ts | 6 +- .../experiments/model/status/index.test.ts | 3 +- .../src/experiments/model/status/index.ts | 6 +- extension/src/experiments/model/tree.test.ts | 9 +- extension/src/experiments/model/tree.ts | 8 +- extension/src/experiments/webview/contract.ts | 14 +- extension/src/experiments/webview/messages.ts | 17 +- extension/src/extension.ts | 29 +- extension/src/interfaces.ts | 10 +- extension/src/plots/model/collect.ts | 12 +- extension/src/setup.test.ts | 245 ++++++++++++---- extension/src/setup.ts | 105 +------ extension/src/telemetry/constants.ts | 7 +- .../src/test/fixtures/expShow/dataTypes.ts | 14 +- .../src/test/fixtures/expShow/deeplyNested.ts | 14 +- .../src/test/fixtures/expShow/modified.ts | 272 ++++++------------ extension/src/test/fixtures/expShow/output.ts | 123 +++++--- extension/src/test/fixtures/expShow/rows.ts | 99 +++++-- .../test/fixtures/expShow/uncommittedDeps.ts | 8 +- .../src/test/suite/experiments/index.test.ts | 39 +++ .../experiments/model/filterBy/tree.test.ts | 5 +- .../test/suite/experiments/model/tree.test.ts | 18 +- .../test/suite/experiments/workspace.test.ts | 8 + extension/src/test/suite/extension.test.ts | 5 +- extension/src/test/suite/plots/index.test.ts | 8 +- extension/src/vscode/config.ts | 2 +- extension/src/vscode/inputBox.ts | 5 +- extension/src/vscode/title.ts | 2 +- extension/src/webview/contract.ts | 4 +- package.json | 6 +- webview/package.json | 2 +- .../src/experiments/components/App.test.tsx | 2 +- .../src/experiments/components/table/Cell.tsx | 4 +- .../components/table/CellRowActions.tsx | 10 +- .../src/experiments/components/table/Row.tsx | 12 +- .../components/table/RowContextMenu.tsx | 19 +- .../components/table/Table.test.tsx | 29 +- .../components/table/TableHeader.tsx | 4 +- webview/src/stories/Table.stories.tsx | 20 +- webview/src/test/tableDataFixture.ts | 23 +- yarn.lock | 120 ++++---- 61 files changed, 1171 insertions(+), 807 deletions(-) create mode 100644 extension/src/cli/dvc/discovery.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 3a0e477df4..7ebbbacf88 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,29 @@ All notable changes to this project will be documented in this file. +## [0.5.0] - 2022-10-11 + +### 🚀 New Features and Enhancements + +- Display failed experiments [#2535](https://github.com/iterative/vscode-dvc/pull/2535) by [@mattseddon](https://github.com/mattseddon) +- Improve max table depth feature [#2538](https://github.com/iterative/vscode-dvc/pull/2538) by [@julieg18](https://github.com/julieg18) + +### 🔨 Maintenance + +- Bump min DVC version to 2.30.0 (Use status from exp show) [#2521](https://github.com/iterative/vscode-dvc/pull/2521) by [@mattseddon](https://github.com/mattseddon) +- Remove stale developer roadmap from README [#2561](https://github.com/iterative/vscode-dvc/pull/2561) by [@mattseddon](https://github.com/mattseddon) + +## [0.4.13] - 2022-10-10 + +### 🐛 Bug Fixes + +- Fix UX of extension using fallback global CLI when Python extension is active [#2544](https://github.com/iterative/vscode-dvc/pull/2544) by [@mattseddon](https://github.com/mattseddon) + +### 🔨 Maintenance + +- Add test for setting table header depth [#2525](https://github.com/iterative/vscode-dvc/pull/2525) by [@julieg18](https://github.com/julieg18) +- Consolidate version checking into CLI discovery file [#2552](https://github.com/iterative/vscode-dvc/pull/2552) by [@mattseddon](https://github.com/mattseddon) + ## [0.4.12] - 2022-10-06 ### 🔨 Maintenance diff --git a/README.md b/README.md index 85087fe1c6..67dbd4ca4a 100644 --- a/README.md +++ b/README.md @@ -13,8 +13,8 @@ [Quick start](#quick-start) • [What you get](#what-you-get) • [Commands](#useful-commands) • [Configuration](#configuration) • -[Roadmap](#developer-roadmap) • [Debugging](#debugging) • -[Contributing](#contributing) • [Telemetry](#data-and-telemetry) +[Debugging](#debugging) • [Contributing](#contributing) • +[Telemetry](#data-and-telemetry) Run, compare, visualize, and track machine learning experiments right in VS Code. This extension uses [DVC](https://dvc.org/), an open-source data @@ -150,7 +150,7 @@ These are the VS Code [settings] available for the Extension: | -------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------- | | `dvc.dvcPath` | Path or shell command to the DVC binary. Required unless Microsoft's [Python extension] is installed and the `dvc` package found in its environment. | | `dvc.pythonPath` | Path to the desired Python interpreter to use with DVC. Required when using a virtual environment. | -| `dvc.expTableHeadMaxLayers` | Maximum depth of experiment table head rows. | +| `dvc.experimentsTableHeadMaxHeight` | Maximum height of experiment table head rows. | | `dvc.doNotShowWalkthroughAfterInstall` | Do not prompt to show the Get Started page after installing. Useful for pre-configured development environments | | `dvc.doNotRecommendRedHatExtension` | Do not prompt to install the Red Hat YAML extension, which helps with DVC YAML schema validation (`dvc.yaml` and `.dvc` files). | | `dvc.doNotShowCliUnavailable` | Do not warn when the workspace contains a DVC project but the DVC binary is unavailable. | @@ -164,54 +164,6 @@ These are the VS Code [settings] available for the Extension: [workspace level]: https://code.visualstudio.com/docs/getstarted/settings#_workspace-settings -## Developer roadmap - -We are working on increasing the quantity and quality of DVC features supported -by this GUI. Remember that you can always use `dvc` commands from the -_Integrated Terminal_! - -- **DVC metafile editor** (2022 Q3) Wouldn't it be cool to manage stages, - parameters, metrics, data and model paths, and other metadata in-place inside - corresponding [DVC metafiles]? E.g. `dvc.yaml` and `.dvc` files, param or - metrics files, even `dvc.lock` - -- **More and better plots!** (2022 Q3) - DVC Experiment comparison is easier with interactive [parallel coordinate - plots], which can be generated from command line with `dvc exp show --pcp`. We - plan to incorporate that and brand new IDE-exclusive plots! (TBD) - -- **Performance improvements** (2022 Q3) - Our extension will be faster and more reliable with better internal usage of - DVC and more efficient data management. - -- **ML pipelines** (2022 Q4) - The extension examines [`dvc.yaml` files] to identify tracked data and - changes, but it does not currently provide a graphic interface to write or - modify stages. - -- **Remote execution** (2022 Q4) - DVC Experiments can be run in remote environments. We intend to integrate this - with VS Code's robust [remote development] features. - -- **Data registry** (2022 Q4) - DVC [data registries] can help you centralize and secure data management - across all your ML projects. You'll be able to construct and handle them right - from the IDE. - -- **More tools from Iterative.ai** (2023) - Expect this extension to become a full-fledged suite for the ecosystem of - tools from Iterative, such as [CML](https://cml.dev/), - [MLEM](https://mlem.ai/) + [GTO](https://github.com/iterative/gto) **model - registry** management, and future surprises! (TBD) - -[dvc metafiles]: https://dvc.org/doc/user-guide/project-structure -[parallel coordinate plots]: - https://dvc.org/doc/user-guide/experiment-management/comparing-experiments#parallel-coordinates-plot -[`dvc.yaml` files]: - https://dvc.org/doc/user-guide/project-structure/pipelines-files -[remote development]: https://code.visualstudio.com/docs/remote/remote-overview -[data registries]: https://dvc.org/doc/use-cases/data-registry - ## Debugging ### The DVC Extension diff --git a/demo/requirements.txt b/demo/requirements.txt index 827224baa0..fa81e540e2 100644 --- a/demo/requirements.txt +++ b/demo/requirements.txt @@ -1,3 +1,3 @@ -dvc[s3]==2.29.0 +dvc[s3]==2.30.0 torch==1.12.0 torchvision==0.13.0 \ No newline at end of file diff --git a/extension/package.json b/extension/package.json index 58a240aa72..b3df31fbe5 100644 --- a/extension/package.json +++ b/extension/package.json @@ -9,7 +9,7 @@ "extensionDependencies": [ "vscode.git" ], - "version": "0.4.12", + "version": "0.5.0", "license": "Apache-2.0", "readme": "./README.md", "repository": { @@ -554,9 +554,9 @@ "type": "string", "default": null }, - "dvc.expTableHeadMaxLayers": { - "title": "%config.expTableHeadMaxLayers.title%", - "description": "%config.expTableHeadMaxLayers.description%", + "dvc.experimentsTableHeadMaxHeight": { + "title": "%config.experimentsTableHeadMaxHeight.title%", + "description": "%config.experimentsTableHeadMaxHeight.description%", "type": "number", "default": 5 }, @@ -1570,7 +1570,7 @@ "@types/lodash.omit": "4.5.7", "@types/mocha": "10.0.0", "@types/mock-require": "2.0.1", - "@types/node": "16.11.63", + "@types/node": "16.11.64", "@types/react-vega": "7.0.0", "@types/sinon-chai": "3.2.8", "@types/uuid": "8.3.4", @@ -1591,7 +1591,7 @@ "mocha": "10.0.0", "mock-require": "3.0.3", "shx": "0.3.4", - "sinon": "14.0.0", + "sinon": "14.0.1", "sinon-chai": "3.7.0", "ts-loader": "9.4.1", "vsce": "2.11.0", diff --git a/extension/package.nls.json b/extension/package.nls.json index 2d51650d3a..68ad2f9249 100644 --- a/extension/package.nls.json +++ b/extension/package.nls.json @@ -84,8 +84,8 @@ "config.doNotShowUnableToFilter.title": "Do not warn before disabling auto-apply filters to experiment selection.", "config.dvcPath.description": "Path or shell command to the DVC binary. Required unless Microsoft's Python extension is installed and the `dvc` package found in its environment.", "config.dvcPath.title": "DVC Path (or shell command)", - "config.expTableHeadMaxLayers.title": "Maximum depth of Experiment table head rows", - "config.expTableHeadMaxLayers.description": "Use 0 for infinite depth.", + "config.experimentsTableHeadMaxHeight.title": "Maximum height of Experiment table head rows", + "config.experimentsTableHeadMaxHeight.description": "Use 0 for infinite height.", "config.pythonPath.description": "Path to the desired Python interpreter to use with DVC. Required when using a virtual environment. Overrides any other extension's settings for this extension's purposes.", "config.pythonPath.title": "Python Interpreter" } diff --git a/extension/src/cli/dvc/constants.ts b/extension/src/cli/dvc/constants.ts index 3845fc3ba0..695bd63a4d 100644 --- a/extension/src/cli/dvc/constants.ts +++ b/extension/src/cli/dvc/constants.ts @@ -1,7 +1,7 @@ import { join } from 'path' -export const MIN_CLI_VERSION = '2.24.0' -export const LATEST_TESTED_CLI_VERSION = '2.29.0' +export const MIN_CLI_VERSION = '2.30.0' +export const LATEST_TESTED_CLI_VERSION = '2.30.0' export const MAX_CLI_VERSION = '3' export const UNEXPECTED_ERROR_CODE = 255 diff --git a/extension/src/cli/dvc/contract.ts b/extension/src/cli/dvc/contract.ts index b88858114d..1484ded5fb 100644 --- a/extension/src/cli/dvc/contract.ts +++ b/extension/src/cli/dvc/contract.ts @@ -1,6 +1,8 @@ import { Plot } from '../../plots/webview/contract' -export type DvcError = { error: { type: string; msg: string } } +type ErrorContents = { type: string; msg: string } + +export type DvcError = { error: ErrorContents } export type Changes = { added?: string[] @@ -22,7 +24,7 @@ export type Value = SingleValue | SingleValue[] export interface ValueTreeOrError { data?: ValueTree - error?: { type: string; msg: string } + error?: ErrorContents } type RelPathObject = { @@ -37,11 +39,17 @@ export interface ValueTreeNode { export type ValueTree = ValueTreeRoot | ValueTreeNode +export enum ExperimentStatus { + FAILED = 'Failed', + QUEUED = 'Queued', + RUNNING = 'Running', + SUCCESS = 'Success' +} + export interface BaseExperimentFields { name?: string timestamp?: string | null - queued?: boolean - running?: boolean + status?: ExperimentStatus executor?: string | null checkpoint_tip?: string checkpoint_parent?: string @@ -57,11 +65,12 @@ export interface ExperimentFields extends BaseExperimentFields { metrics?: ValueTreeRoot deps?: Deps outs?: RelPathObject + error?: ErrorContents } export interface ExperimentFieldsOrError { data?: ExperimentFields - error?: { type: string; msg: string } + error?: ErrorContents } export interface ExperimentsBranchOutput { diff --git a/extension/src/cli/dvc/discovery.ts b/extension/src/cli/dvc/discovery.ts new file mode 100644 index 0000000000..a629d199cd --- /dev/null +++ b/extension/src/cli/dvc/discovery.ts @@ -0,0 +1,225 @@ +import { + LATEST_TESTED_CLI_VERSION, + MAX_CLI_VERSION, + MIN_CLI_VERSION +} from './constants' +import { CliCompatible, isVersionCompatible } from './version' +import { IExtension } from '../../interfaces' +import { Toast } from '../../vscode/toast' +import { Response } from '../../vscode/response' +import { + ConfigKey, + getConfigValue, + setUserConfigValue +} from '../../vscode/config' +import { + getPythonBinPath, + isPythonExtensionInstalled, + selectPythonInterpreter +} from '../../extensions/python' + +const getToastOptions = (isPythonExtensionInstalled: boolean): Response[] => { + return isPythonExtensionInstalled + ? [Response.SETUP_WORKSPACE, Response.SELECT_INTERPRETER, Response.NEVER] + : [Response.SETUP_WORKSPACE, Response.NEVER] +} + +export const warnUnableToVerifyVersion = () => + Toast.warnWithOptions( + 'The extension cannot initialize as we were unable to verify the DVC CLI version.' + ) + +export const warnVersionIncompatible = ( + version: string, + update: 'CLI' | 'extension' +): void => { + Toast.warnWithOptions( + `The extension cannot initialize because you are using version ${version} of the DVC CLI. The expected version is ${MIN_CLI_VERSION} <= DVC < ${MAX_CLI_VERSION}. Please upgrade to the most recent version of the ${update} and reload this window.` + ) +} + +export const warnAheadOfLatestTested = (): void => { + Toast.warnWithOptions( + `The located DVC CLI is at least a minor version ahead of the latest version the extension was tested with (${LATEST_TESTED_CLI_VERSION}). This could lead to unexpected behaviour. Please upgrade to the most recent version of the extension and reload this window.` + ) +} + +const warnUserCLIInaccessible = async ( + extension: IExtension, + isMsPythonInstalled: boolean, + warningText: string +): Promise => { + if (getConfigValue(ConfigKey.DO_NOT_SHOW_CLI_UNAVAILABLE)) { + return + } + + const response = await Toast.warnWithOptions( + warningText, + ...getToastOptions(isMsPythonInstalled) + ) + + switch (response) { + case Response.SELECT_INTERPRETER: + return selectPythonInterpreter() + case Response.SETUP_WORKSPACE: + return extension.setupWorkspace() + case Response.NEVER: + return setUserConfigValue(ConfigKey.DO_NOT_SHOW_CLI_UNAVAILABLE, true) + } +} + +const warnUserCLIInaccessibleAnywhere = async ( + extension: IExtension, + globalDvcVersion: string | undefined +): Promise => { + const binPath = await getPythonBinPath() + + return warnUserCLIInaccessible( + extension, + true, + `The extension is unable to initialize. The CLI was not located using the interpreter provided by the Python extension. ${ + globalDvcVersion ? globalDvcVersion + ' is' : 'The CLI is also not' + } installed globally. For auto Python environment activation, ensure the correct interpreter is set. Active Python interpreter: ${binPath}.` + ) +} + +const warnUser = ( + extension: IExtension, + cliCompatible: CliCompatible, + version: string | undefined +): void => { + if (!extension.hasRoots()) { + return + } + switch (cliCompatible) { + case CliCompatible.NO_BEHIND_MIN_VERSION: + return warnVersionIncompatible(version as string, 'CLI') + case CliCompatible.NO_CANNOT_VERIFY: + warnUnableToVerifyVersion() + return + case CliCompatible.NO_MAJOR_VERSION_AHEAD: + return warnVersionIncompatible(version as string, 'extension') + case CliCompatible.NO_NOT_FOUND: + warnUserCLIInaccessible( + extension, + isPythonExtensionInstalled(), + 'An error was thrown when trying to access the CLI.' + ) + return + case CliCompatible.YES_MINOR_VERSION_AHEAD_OF_TESTED: + return warnAheadOfLatestTested() + } +} + +type CanRunCli = { + isAvailable: boolean + isCompatible: boolean | undefined +} + +const isCliCompatible = (cliCompatible: CliCompatible): boolean | undefined => { + if (cliCompatible === CliCompatible.NO_NOT_FOUND) { + return + } + + return [ + CliCompatible.YES, + CliCompatible.YES_MINOR_VERSION_AHEAD_OF_TESTED + ].includes(cliCompatible) +} + +const getVersionDetails = async ( + extension: IExtension, + cwd: string, + tryGlobalCli?: true +): Promise< + CanRunCli & { + cliCompatible: CliCompatible + version: string | undefined + } +> => { + const version = await extension.getCliVersion(cwd, tryGlobalCli) + const cliCompatible = isVersionCompatible(version) + const isCompatible = isCliCompatible(cliCompatible) + return { cliCompatible, isAvailable: !!isCompatible, isCompatible, version } +} + +const processVersionDetails = ( + extension: IExtension, + cliCompatible: CliCompatible, + version: string | undefined, + isAvailable: boolean, + isCompatible: boolean | undefined +): CanRunCli => { + warnUser(extension, cliCompatible, version) + return { + isAvailable, + isCompatible + } +} + +const tryGlobalFallbackVersion = async ( + extension: IExtension, + cwd: string +): Promise => { + const tryGlobal = await getVersionDetails(extension, cwd, true) + const { cliCompatible, isAvailable, isCompatible, version } = tryGlobal + + if (extension.hasRoots() && !isCompatible) { + warnUserCLIInaccessibleAnywhere(extension, version) + } + if ( + extension.hasRoots() && + cliCompatible === CliCompatible.YES_MINOR_VERSION_AHEAD_OF_TESTED + ) { + warnAheadOfLatestTested() + } + + if (isCompatible) { + extension.unsetPythonBinPath() + } + + return { isAvailable, isCompatible } +} + +const extensionCanAutoRunCli = async ( + extension: IExtension, + cwd: string +): Promise => { + const { + cliCompatible: pythonCliCompatible, + isAvailable: pythonVersionIsAvailable, + isCompatible: pythonVersionIsCompatible, + version: pythonVersion + } = await getVersionDetails(extension, cwd) + + if (pythonCliCompatible === CliCompatible.NO_NOT_FOUND) { + return tryGlobalFallbackVersion(extension, cwd) + } + return processVersionDetails( + extension, + pythonCliCompatible, + pythonVersion, + pythonVersionIsAvailable, + pythonVersionIsCompatible + ) +} + +export const extensionCanRunCli = async ( + extension: IExtension, + cwd: string +): Promise => { + if (await extension.isPythonExtensionUsed()) { + return extensionCanAutoRunCli(extension, cwd) + } + + const { cliCompatible, isAvailable, isCompatible, version } = + await getVersionDetails(extension, cwd) + + return processVersionDetails( + extension, + cliCompatible, + version, + isAvailable, + isCompatible + ) +} diff --git a/extension/src/cli/dvc/version.test.ts b/extension/src/cli/dvc/version.test.ts index 06228b1f46..a557e257a4 100644 --- a/extension/src/cli/dvc/version.test.ts +++ b/extension/src/cli/dvc/version.test.ts @@ -1,6 +1,10 @@ -import { isVersionCompatible, extractSemver, ParsedSemver } from './version' +import { + isVersionCompatible, + extractSemver, + ParsedSemver, + CliCompatible +} from './version' import { MIN_CLI_VERSION, LATEST_TESTED_CLI_VERSION } from './constants' -import { Toast } from '../../vscode/toast' jest.mock('./constants', () => ({ ...jest.requireActual('./constants'), @@ -8,11 +12,6 @@ jest.mock('./constants', () => ({ MIN_CLI_VERSION: '2.9.4' })) jest.mock('../../vscode/config') -jest.mock('../../vscode/toast') - -const mockedToast = jest.mocked(Toast) -const mockedWarnWithOptions = jest.fn() -mockedToast.warnWithOptions = mockedWarnWithOptions beforeEach(() => { jest.resetAllMocks() @@ -57,39 +56,31 @@ describe('isVersionCompatible', () => { patch: latestTestedPatch } = extractSemver(LATEST_TESTED_CLI_VERSION) as ParsedSemver - it('should be compatible and not send a toast message if the provided version matches the min version', () => { + it('should be compatible if the provided version matches the min version', () => { const isCompatible = isVersionCompatible(MIN_CLI_VERSION) - expect(isCompatible).toBe(true) - expect(mockedWarnWithOptions).not.toHaveBeenCalled() + expect(isCompatible).toStrictEqual(CliCompatible.YES) }) - it('should be compatible and not send a toast for a version with the same minor and higher patch as the min compatible version', () => { - mockedWarnWithOptions.mockResolvedValueOnce(undefined) - + it('should be compatible for a version with the same minor and higher patch as the min compatible version', () => { const isCompatible = isVersionCompatible( [minMajor, minMinor, minPatch + 10000].join('.') ) - expect(isCompatible).toBe(true) - expect(mockedWarnWithOptions).not.toHaveBeenCalled() + expect(isCompatible).toStrictEqual(CliCompatible.YES) }) - it('should be compatible and not send a toast for a version with the same minor and higher patch as the latest tested version', () => { - mockedWarnWithOptions.mockResolvedValueOnce(undefined) - + it('should be compatible for a version with the same minor and higher patch as the latest tested version', () => { const isCompatible = isVersionCompatible( [latestTestedMajor, latestTestedMinor, latestTestedPatch + 10000].join( '.' ) ) - expect(isCompatible).toBe(true) - expect(mockedWarnWithOptions).not.toHaveBeenCalled() + expect(isCompatible).toStrictEqual(CliCompatible.YES) }) - it('should be compatible and not send a toast for a major and minor version in between the min compatible and the latest tested', () => { - mockedWarnWithOptions.mockResolvedValueOnce(undefined) + it('should be compatible for a major and minor version in between the min compatible and the latest tested', () => { expect(minMinor + 1).toBeLessThan(latestTestedMinor) expect(minMajor).toStrictEqual(latestTestedMajor) @@ -97,85 +88,82 @@ describe('isVersionCompatible', () => { [minMajor, minMinor + 1, 0].join('.') ) - expect(isCompatible).toBe(true) - expect(mockedWarnWithOptions).not.toHaveBeenCalled() + expect(isCompatible).toStrictEqual(CliCompatible.YES) }) - it('should be compatible and send a toast for a version with a minor higher as the latest tested minor and any patch', () => { - mockedWarnWithOptions.mockResolvedValueOnce(undefined) + it('should return not found if the version provided is undefined', () => { + const isCompatible = isVersionCompatible(undefined) + + expect(isCompatible).toStrictEqual(CliCompatible.NO_NOT_FOUND) + }) + + it('should return minor version ahead of tested for a version with a minor higher as the latest tested minor and any patch', () => { expect(0).toBeLessThan(latestTestedPatch) let isCompatible = isVersionCompatible( [latestTestedMajor, latestTestedMinor + 1, 0].join('.') ) - expect(isCompatible).toBe(true) + + expect(isCompatible).toStrictEqual( + CliCompatible.YES_MINOR_VERSION_AHEAD_OF_TESTED + ) isCompatible = isVersionCompatible( [latestTestedMajor, latestTestedMinor + 1, latestTestedPatch + 1000].join( '.' ) ) - expect(isCompatible).toBe(true) + + expect(isCompatible).toStrictEqual( + CliCompatible.YES_MINOR_VERSION_AHEAD_OF_TESTED + ) isCompatible = isVersionCompatible( [latestTestedMajor, latestTestedMinor + 1, latestTestedPatch].join('.') ) - expect(isCompatible).toBe(true) - expect(mockedWarnWithOptions).toHaveBeenCalledTimes(3) + expect(isCompatible).toStrictEqual( + CliCompatible.YES_MINOR_VERSION_AHEAD_OF_TESTED + ) }) - it('should not be compatible and send a toast message if the provided version is a patch version before the minimum expected version', () => { - mockedWarnWithOptions.mockResolvedValueOnce(undefined) - + it('should return behind min version if the provided version is a patch version before the minimum expected version', () => { const isCompatible = isVersionCompatible( [minMajor, minMinor, minPatch - 1].join('.') ) - expect(isCompatible).toBe(false) - expect(mockedWarnWithOptions).toHaveBeenCalledTimes(1) + expect(isCompatible).toStrictEqual(CliCompatible.NO_BEHIND_MIN_VERSION) }) - it('should not be compatible and send a toast message if the provided minor version is before the minimum expected version', () => { - mockedWarnWithOptions.mockResolvedValueOnce(undefined) - + it('should return behind min version if the provided minor version is before the minimum expected version', () => { const isCompatible = isVersionCompatible( [minMajor, minMinor - 1, minPatch + 100].join('.') ) - expect(isCompatible).toBe(false) - expect(mockedWarnWithOptions).toHaveBeenCalledTimes(1) + expect(isCompatible).toStrictEqual(CliCompatible.NO_BEHIND_MIN_VERSION) }) - it('should not be compatible and send a toast message if the provided major version is before the minimum expected version', () => { - mockedWarnWithOptions.mockResolvedValueOnce(undefined) - + it('should return behind min version if the provided major version is before the minimum expected version', () => { const isCompatible = isVersionCompatible( [minMajor - 1, minMinor + 1000, minPatch + 100].join('.') ) - expect(isCompatible).toBe(false) - expect(mockedWarnWithOptions).toHaveBeenCalledTimes(1) + expect(isCompatible).toStrictEqual(CliCompatible.NO_BEHIND_MIN_VERSION) }) - it('should not be compatible and send a toast message if the provided major version is above the expected major version', () => { - mockedWarnWithOptions.mockResolvedValueOnce(undefined) - + it('should return major ahead if the provided major version is above the expected major version', () => { const isCompatible = isVersionCompatible('3.0.0') - expect(isCompatible).toBe(false) - expect(mockedWarnWithOptions).toHaveBeenCalledTimes(1) + expect(isCompatible).toStrictEqual(CliCompatible.NO_MAJOR_VERSION_AHEAD) }) - it('should not be compatible and send a toast message if the provided version is malformed', () => { - mockedWarnWithOptions.mockResolvedValueOnce(undefined) - + it('should return cannot verify if the provided version is malformed', () => { let isCompatible = isVersionCompatible('not a valid version') - expect(isCompatible).toBe(false) + + expect(isCompatible).toStrictEqual(CliCompatible.NO_CANNOT_VERIFY) isCompatible = isVersionCompatible('1,2,3') - expect(isCompatible).toBe(false) - expect(mockedWarnWithOptions).toHaveBeenCalledTimes(2) + expect(isCompatible).toStrictEqual(CliCompatible.NO_CANNOT_VERIFY) }) }) diff --git a/extension/src/cli/dvc/version.ts b/extension/src/cli/dvc/version.ts index 259307ebc9..e0d7d3378e 100644 --- a/extension/src/cli/dvc/version.ts +++ b/extension/src/cli/dvc/version.ts @@ -3,7 +3,15 @@ import { LATEST_TESTED_CLI_VERSION, MIN_CLI_VERSION } from './constants' -import { Toast } from '../../vscode/toast' + +export enum CliCompatible { + NO_BEHIND_MIN_VERSION = 'no-behind-min-version', + NO_CANNOT_VERIFY = 'no-cannot-verify', + NO_MAJOR_VERSION_AHEAD = 'no-major-version-ahead', + NO_NOT_FOUND = 'no-not-found', + YES_MINOR_VERSION_AHEAD_OF_TESTED = 'yes-minor-version-ahead-of-tested', + YES = 'yes' +} export type ParsedSemver = { major: number; minor: number; patch: number } @@ -16,36 +24,26 @@ export const extractSemver = (stdout: string): ParsedSemver | undefined => { return { major: Number(major), minor: Number(minor), patch: Number(patch) } } -const getWarningText = ( - currentVersion: string, - update: 'CLI' | 'extension' -): string => `The extension cannot initialize because you are using version ${currentVersion} of the DVC CLI. -The expected version is ${MIN_CLI_VERSION} <= DVC < ${MAX_CLI_VERSION}. Please upgrade to the most recent version of the ${update} and reload this window.` - -const getTextAndSend = (version: string, update: 'CLI' | 'extension'): void => { - const text = getWarningText(version, update) - Toast.warnWithOptions(text) -} - -const warnIfAheadOfLatestTested = ( +const cliIsCompatible = ( currentMajor: number, currentMinor: number -) => { +): CliCompatible => { const { major: latestTestedMajor, minor: latestTestedMinor } = extractSemver( LATEST_TESTED_CLI_VERSION ) as ParsedSemver if (currentMajor === latestTestedMajor && currentMinor > latestTestedMinor) { - Toast.warnWithOptions(`The located DVC CLI is at least a minor version ahead of the latest version the extension was tested with (${LATEST_TESTED_CLI_VERSION}). - This could lead to unexpected behaviour. - Please upgrade to the most recent version of the extension and reload this window.`) + return CliCompatible.YES_MINOR_VERSION_AHEAD_OF_TESTED } + + return CliCompatible.YES } -const checkCLIVersion = ( - version: string, - currentSemVer: { major: number; minor: number; patch: number } -): boolean => { +const checkCLIVersion = (currentSemVer: { + major: number + minor: number + patch: number +}): CliCompatible => { const { major: currentMajor, minor: currentMinor, @@ -53,8 +51,7 @@ const checkCLIVersion = ( } = currentSemVer if (currentMajor >= Number(MAX_CLI_VERSION)) { - getTextAndSend(version, 'extension') - return false + return CliCompatible.NO_MAJOR_VERSION_AHEAD } const { @@ -68,28 +65,29 @@ const checkCLIVersion = ( currentMinor < minMinor || (currentMinor === minMinor && currentPatch < Number(minPatch)) ) { - getTextAndSend(version, 'CLI') - return false + return CliCompatible.NO_BEHIND_MIN_VERSION } - warnIfAheadOfLatestTested(currentMajor, currentMinor) - - return true + return cliIsCompatible(currentMajor, currentMinor) } -export const isVersionCompatible = (version: string): boolean => { +export const isVersionCompatible = ( + version: string | undefined +): CliCompatible => { + if (!version) { + return CliCompatible.NO_NOT_FOUND + } + const currentSemVer = extractSemver(version) + if ( !currentSemVer || Number.isNaN(currentSemVer.major) || Number.isNaN(currentSemVer.minor) || Number.isNaN(currentSemVer.patch) ) { - Toast.warnWithOptions( - 'The extension cannot initialize as we were unable to verify the DVC CLI version.' - ) - return false + return CliCompatible.NO_CANNOT_VERIFY } - return checkCLIVersion(version, currentSemVer) + return checkCLIVersion(currentSemVer) } diff --git a/extension/src/experiments/data/collect.test.ts b/extension/src/experiments/data/collect.test.ts index 03248c104c..d58bc94bf0 100644 --- a/extension/src/experiments/data/collect.test.ts +++ b/extension/src/experiments/data/collect.test.ts @@ -1,6 +1,6 @@ import { join } from 'path' import { collectFiles } from './collect' -import { ExperimentsOutput } from '../../cli/dvc/contract' +import { ExperimentsOutput, ExperimentStatus } from '../../cli/dvc/contract' import expShowFixture from '../../test/fixtures/expShow/output' describe('collectFiles', () => { @@ -100,8 +100,7 @@ describe('collectFiles', () => { } } }, - queued: false, - running: true, + status: ExperimentStatus.RUNNING, timestamp: null } } diff --git a/extension/src/experiments/index.ts b/extension/src/experiments/index.ts index 5452a80887..8245c49dfd 100644 --- a/extension/src/experiments/index.ts +++ b/extension/src/experiments/index.ts @@ -23,7 +23,7 @@ import { ColumnsModel } from './columns/model' import { CheckpointsModel } from './checkpoints/model' import { ExperimentsData } from './data' import { askToDisableAutoApplyFilters } from './toast' -import { Experiment, ColumnType, TableData } from './webview/contract' +import { Experiment, ColumnType, TableData, isQueued } from './webview/contract' import { WebviewMessages } from './webview/messages' import { DecorationProvider } from './model/decorationProvider' import { starredFilter } from './model/filterBy/constants' @@ -333,7 +333,7 @@ export class Experiments extends BaseRepository { if (useFilters) { const filteredExperiments = this.experiments .getUnfilteredExperiments() - .filter(exp => !exp.queued) + .filter(exp => !isQueued(exp.status)) if (tooManySelected(filteredExperiments)) { await this.warnAndDoNotAutoApply(filteredExperiments) } else { diff --git a/extension/src/experiments/model/accumulator.ts b/extension/src/experiments/model/accumulator.ts index 7a5c8f5fe3..6bf3a8dc9e 100644 --- a/extension/src/experiments/model/accumulator.ts +++ b/extension/src/experiments/model/accumulator.ts @@ -1,4 +1,4 @@ -import { Experiment } from '../webview/contract' +import { Experiment, isRunning } from '../webview/contract' export class ExperimentsAccumulator { public workspace = {} as Experiment @@ -11,6 +11,6 @@ export class ExperimentsAccumulator { if (workspace) { this.workspace = workspace } - this.hasRunning = !!workspace?.running + this.hasRunning = isRunning(workspace?.status) } } diff --git a/extension/src/experiments/model/collect.test.ts b/extension/src/experiments/model/collect.test.ts index fe5289387a..3ab8bd8645 100644 --- a/extension/src/experiments/model/collect.test.ts +++ b/extension/src/experiments/model/collect.test.ts @@ -1,6 +1,7 @@ import { collectExperiments, collectMutableRevisions } from './collect' import { Experiment } from '../webview/contract' import modifiedFixture from '../../test/fixtures/expShow/modified' +import { ExperimentStatus } from '../../cli/dvc/contract' describe('collectExperiments', () => { it('should return an empty array if no branches are present', () => { @@ -132,16 +133,16 @@ describe('collectExperiments', () => { describe('collectMutableRevisions', () => { const baseExperiments = [ - { label: 'branch-A', running: false, selected: false }, - { label: 'workspace', running: false, selected: false } + { label: 'branch-A', selected: false, status: ExperimentStatus.SUCCESS }, + { label: 'workspace', selected: false, status: ExperimentStatus.FAILED } ] as Experiment[] it('should not return the workspace when there is a selected running checkpoint experiment (race condition)', () => { const experiments = [ { label: 'exp-123', - running: true, - selected: true + selected: true, + status: ExperimentStatus.RUNNING }, ...baseExperiments ] as Experiment[] @@ -154,8 +155,8 @@ describe('collectMutableRevisions', () => { const experiments = [ { label: 'exp-123', - running: true, - selected: false + selected: false, + status: ExperimentStatus.RUNNING }, ...baseExperiments ] as Experiment[] @@ -166,8 +167,8 @@ describe('collectMutableRevisions', () => { it('should return the workspace when there are no checkpoints', () => { const experiments = [ - { label: 'branch-A', running: false, selected: false }, - { label: 'workspace', running: false, selected: false } + { label: 'branch-A', selected: false, status: ExperimentStatus.SUCCESS }, + { label: 'workspace', selected: false, status: ExperimentStatus.SUCCESS } ] as Experiment[] const mutableRevisions = collectMutableRevisions(experiments, false) @@ -176,10 +177,10 @@ describe('collectMutableRevisions', () => { it('should return all running experiments when there are checkpoints', () => { const experiments = [ - { label: 'branch-A', running: false, selected: false }, - { label: 'workspace', running: false, selected: false }, - { label: 'running-1', running: true, selected: false }, - { label: 'running-2', running: true, selected: true } + { label: 'branch-A', selected: false, status: ExperimentStatus.SUCCESS }, + { label: 'workspace', selected: false, status: ExperimentStatus.SUCCESS }, + { label: 'running-1', selected: false, status: ExperimentStatus.RUNNING }, + { label: 'running-2', selected: true, status: ExperimentStatus.RUNNING } ] as Experiment[] const mutableRevisions = collectMutableRevisions(experiments, false) diff --git a/extension/src/experiments/model/collect.ts b/extension/src/experiments/model/collect.ts index f03fc59063..6afc01f00e 100644 --- a/extension/src/experiments/model/collect.ts +++ b/extension/src/experiments/model/collect.ts @@ -8,12 +8,13 @@ import omit from 'lodash.omit' import { ExperimentType } from '.' import { ExperimentsAccumulator } from './accumulator' import { extractColumns } from '../columns/extract' -import { Experiment, ColumnType } from '../webview/contract' +import { Experiment, ColumnType, isRunning } from '../webview/contract' import { ExperimentFieldsOrError, ExperimentFields, ExperimentsBranchOutput, - ExperimentsOutput + ExperimentsOutput, + ExperimentStatus } from '../../cli/dvc/contract' import { addToMapArray } from '../../util/map' import { uniqueValues } from '../../util/array' @@ -147,6 +148,11 @@ const transformColumns = ( } } +const mergeErrors = ( + experimentFieldsOrError: ExperimentFieldsOrError +): string | undefined => + experimentFieldsOrError.error?.msg || experimentFieldsOrError.data?.error?.msg + const transformExperimentData = ( id: string, experimentFieldsOrError: ExperimentFieldsOrError, @@ -158,7 +164,7 @@ const transformExperimentData = ( ): Experiment => { const data = experimentFieldsOrError.data || {} - const error = experimentFieldsOrError.error + const error = mergeErrors(experimentFieldsOrError) const experiment = { id, @@ -179,7 +185,7 @@ const transformExperimentData = ( } if (error) { - experiment.error = error.msg + experiment.error = error } transformColumns(experiment, data, branch) @@ -228,7 +234,7 @@ const collectHasRunningExperiment = ( acc: ExperimentsAccumulator, experiment: Experiment ) => { - if (experiment.running) { + if (isRunning(experiment.status)) { acc.hasRunning = true } } @@ -327,19 +333,19 @@ const getDefaultMutableRevision = (hasCheckpoints: boolean): string[] => { const noWorkspaceVsSelectedRaceCondition = ( hasCheckpoints: boolean, - running: boolean | undefined, + status: ExperimentStatus | undefined, selected: boolean | undefined -): boolean => !!(hasCheckpoints && running && !selected) +): boolean => !!(hasCheckpoints && isRunning(status) && !selected) const collectMutableRevision = ( acc: string[], - { label, running, selected }: Experiment, + { label, status, selected }: Experiment, hasCheckpoints: boolean ) => { - if (noWorkspaceVsSelectedRaceCondition(hasCheckpoints, running, selected)) { + if (noWorkspaceVsSelectedRaceCondition(hasCheckpoints, status, selected)) { acc.push('workspace') } - if (running && !hasCheckpoints) { + if (isRunning(status) && !hasCheckpoints) { acc.push(label) } } diff --git a/extension/src/experiments/model/filterBy/quickPick.ts b/extension/src/experiments/model/filterBy/quickPick.ts index 865704b15f..5656b70f05 100644 --- a/extension/src/experiments/model/filterBy/quickPick.ts +++ b/extension/src/experiments/model/filterBy/quickPick.ts @@ -98,7 +98,7 @@ const getValue = (operator: Operator): Thenable => { isFreeTextDate(text) ? null : 'please enter a valid date of the form yyyy-mm-dd', - getIsoDate() + { value: getIsoDate() } ) } return getInput(Title.ENTER_FILTER_VALUE) diff --git a/extension/src/experiments/model/index.test.ts b/extension/src/experiments/model/index.test.ts index fa98aaaf0d..d92af29aa1 100644 --- a/extension/src/experiments/model/index.test.ts +++ b/extension/src/experiments/model/index.test.ts @@ -19,6 +19,7 @@ import { dataTypesOutput, rows as dataTypesRows } from '../../test/fixtures/expShow/dataTypes' +import { ExperimentStatus } from '../../cli/dvc/contract' jest.mock('vscode') @@ -96,8 +97,7 @@ describe('ExperimentsModel', () => { data: { executor: null, params: { 'params.yaml': { data: { epochs: 100 } } }, - queued: false, - running: false, + status: ExperimentStatus.SUCCESS, timestamp: null } } @@ -109,8 +109,7 @@ describe('ExperimentsModel', () => { executor: null, name: 'main', params: { 'params.yaml': { data: { epochs: 100 } } }, - queued: false, - running: false, + status: ExperimentStatus.SUCCESS, timestamp: '2022-08-10T19:40:14' } }, @@ -126,8 +125,7 @@ describe('ExperimentsModel', () => { }, executor: null, name: 'exp-750e4', - queued: false, - running: false, + status: ExperimentStatus.SUCCESS, timestamp: '2022-08-11T23:04:39' } }, @@ -137,8 +135,7 @@ describe('ExperimentsModel', () => { executor: null, name: 'exp-d6ddc', params: { 'params.yaml': { data: { epochs: 100 } } }, - queued: false, - running: false, + status: ExperimentStatus.SUCCESS, timestamp: '2022-08-11T22:55:46' } } diff --git a/extension/src/experiments/model/index.ts b/extension/src/experiments/model/index.ts index 11faeeaaeb..a1b07f6f17 100644 --- a/extension/src/experiments/model/index.ts +++ b/extension/src/experiments/model/index.ts @@ -23,7 +23,7 @@ import { UNSELECTED } from './status' import { collectFlatExperimentParams } from './modify/collect' -import { Experiment, Row } from '../webview/contract' +import { Experiment, isQueued, Row } from '../webview/contract' import { definedAndNonEmpty, reorderListSubset, @@ -31,7 +31,6 @@ import { } from '../../util/array' import { ExperimentsOutput } from '../../cli/dvc/contract' import { setContextValue } from '../../vscode/context' -import { hasKey } from '../../util/object' import { flattenMapValues } from '../../util/map' import { ModelWithPersistence } from '../../persistence/model' import { PersistenceKey } from '../../persistence/constants' @@ -129,8 +128,11 @@ export class ExperimentsModel extends ModelWithPersistence { public toggleStatus(id: string) { if ( - this.getFlattenedExperiments().find(({ id: queuedId }) => queuedId === id) - ?.queued + isQueued( + this.getFlattenedExperiments().find( + ({ id: queuedId }) => queuedId === id + )?.status + ) ) { return } @@ -318,8 +320,8 @@ export class ExperimentsModel extends ModelWithPersistence { public getExperimentsWithCheckpoints(): ExperimentWithCheckpoints[] { const experimentsWithCheckpoints: ExperimentWithCheckpoints[] = [] for (const experiment of this.getAllExperiments()) { - const { id, queued } = experiment - if (queued) { + const { id, status } = experiment + if (isQueued(status)) { continue } @@ -412,7 +414,7 @@ export class ExperimentsModel extends ModelWithPersistence { return this.getExperimentsByBranch(branch)?.map(experiment => ({ ...experiment, hasChildren: definedAndNonEmpty(this.checkpointsByTip.get(experiment.id)), - type: experiment.queued + type: isQueued(experiment.status) ? ExperimentType.QUEUED : ExperimentType.EXPERIMENT })) @@ -494,11 +496,11 @@ export class ExperimentsModel extends ModelWithPersistence { } private splitExperimentsByQueued(getQueued = false) { - return this.getFlattenedExperiments().filter(({ queued }) => { + return this.getFlattenedExperiments().filter(({ status }) => { if (getQueued) { - return queued + return isQueued(status) } - return !queued + return !isQueued(status) }) } @@ -569,23 +571,11 @@ export class ExperimentsModel extends ModelWithPersistence { private addDetails(experiment: Experiment) { const { id } = experiment - const starred = !!this.isStarred(id) - - if (!hasKey(this.coloredStatus, id)) { - return { - ...experiment, - selected: false, - starred - } - } - - const selected = this.isSelected(id) - return { ...experiment, displayColor: this.getDisplayColor(id), - selected, - starred + selected: this.isSelected(id), + starred: !!this.isStarred(id) } } diff --git a/extension/src/experiments/model/sortBy/index.test.ts b/extension/src/experiments/model/sortBy/index.test.ts index ac95c56748..e3693d93e3 100644 --- a/extension/src/experiments/model/sortBy/index.test.ts +++ b/extension/src/experiments/model/sortBy/index.test.ts @@ -12,7 +12,6 @@ describe('sortExperiments', () => { checkpoint_tip: 'd3f4a0d3661c5977540d2205d819470cf0d2145a', id: testId, label: testLabel, - queued: false, timestamp: testTimestamp } const testPathArray: [ColumnType, string, string] = [ diff --git a/extension/src/experiments/model/status/collect.test.ts b/extension/src/experiments/model/status/collect.test.ts index 48154385e5..b1f6723547 100644 --- a/extension/src/experiments/model/status/collect.test.ts +++ b/extension/src/experiments/model/status/collect.test.ts @@ -1,6 +1,7 @@ import { collectColoredStatus } from './collect' import { copyOriginalColors } from './colors' import { Experiment } from '../../webview/contract' +import { ExperimentStatus } from '../../../cli/dvc/contract' describe('collectColoredStatus', () => { const buildMockExperiments = (n: number, prefix = 'exp') => { @@ -41,7 +42,7 @@ describe('collectColoredStatus', () => { it('should not push queued experiments into the returned object', () => { const experiments = [ { id: 'exp1' }, - { id: 'exp2', queued: true } + { id: 'exp2', status: ExperimentStatus.QUEUED } ] as Experiment[] const colors = copyOriginalColors() diff --git a/extension/src/experiments/model/status/collect.ts b/extension/src/experiments/model/status/collect.ts index 96b1b04b00..8fda28af72 100644 --- a/extension/src/experiments/model/status/collect.ts +++ b/extension/src/experiments/model/status/collect.ts @@ -1,7 +1,7 @@ import { canSelect, ColoredStatus, UNSELECTED } from '.' import { Color, copyOriginalColors } from './colors' import { hasKey } from '../../../util/object' -import { Experiment } from '../../webview/contract' +import { Experiment, isQueued } from '../../webview/contract' import { definedAndNonEmpty, reorderListSubset } from '../../../util/array' import { flattenMapValues } from '../../../util/map' @@ -21,8 +21,8 @@ const collectStatus = ( experiment: Experiment, unassignColors?: Color[] ) => { - const { id, queued } = experiment - if (!id || queued || hasKey(acc, id)) { + const { id, status } = experiment + if (!id || isQueued(status) || hasKey(acc, id)) { return } diff --git a/extension/src/experiments/model/status/index.test.ts b/extension/src/experiments/model/status/index.test.ts index 60efcdf888..404d6d2369 100644 --- a/extension/src/experiments/model/status/index.test.ts +++ b/extension/src/experiments/model/status/index.test.ts @@ -1,6 +1,7 @@ import { canSelect, limitToMaxSelected } from '.' import { copyOriginalColors } from './colors' import { Experiment } from '../../webview/contract' +import { ExperimentStatus } from '../../../cli/dvc/contract' describe('canSelect', () => { const colors = copyOriginalColors() @@ -48,7 +49,7 @@ describe('limitToMaxSelected', () => { expect( limitToMaxSelected([ ...mockedExperiments, - { id: '1', label: 'R', running: true } + { id: '1', label: 'R', status: ExperimentStatus.RUNNING } ]) .map(({ label }) => label) .sort() diff --git a/extension/src/experiments/model/status/index.ts b/extension/src/experiments/model/status/index.ts index 449dc1f047..f24b7074d9 100644 --- a/extension/src/experiments/model/status/index.ts +++ b/extension/src/experiments/model/status/index.ts @@ -1,5 +1,5 @@ import { Color } from './colors' -import { Experiment } from '../../webview/contract' +import { Experiment, isRunning } from '../../webview/contract' export const MAX_SELECTED_EXPERIMENTS = 7 @@ -25,9 +25,9 @@ const compareTimestamps = (a: Experiment, b: Experiment) => export const limitToMaxSelected = (experiments: Experiment[]) => experiments .sort((a, b) => { - if (a.running === b.running) { + if (a.status === b.status) { return compareTimestamps(a, b) } - return a.running ? -1 : 1 + return isRunning(a.status) ? -1 : 1 }) .slice(0, MAX_SELECTED_EXPERIMENTS) diff --git a/extension/src/experiments/model/tree.test.ts b/extension/src/experiments/model/tree.test.ts index e8c114ba20..ed3a576481 100644 --- a/extension/src/experiments/model/tree.test.ts +++ b/extension/src/experiments/model/tree.test.ts @@ -15,6 +15,7 @@ import { ResourceLocator } from '../../resourceLocator' import { RegisteredCommands } from '../../commands/external' import { getMarkdownString } from '../../vscode/markdownString' import { DecoratableTreeItemScheme, getDecoratableUri } from '../../tree' +import { ExperimentStatus } from '../webview/contract' const mockedCommands = jest.mocked(commands) mockedCommands.registerCommand = jest.fn() @@ -141,8 +142,8 @@ describe('ExperimentsTree', () => { hasChildren: false, id: 'exp-67899', label: 'f0778b3', - running: true, selected: true, + status: ExperimentStatus.RUNNING, type: ExperimentType.EXPERIMENT }, { @@ -150,7 +151,6 @@ describe('ExperimentsTree', () => { hasChildren: false, id: 'exp-abcdef', label: 'e350702', - running: false, selected: false, type: ExperimentType.EXPERIMENT }, @@ -161,7 +161,6 @@ describe('ExperimentsTree', () => { hasChildren: false, id: '139eabc', label: '139eabc', - running: false, selected: false, type: ExperimentType.EXPERIMENT }, @@ -169,7 +168,7 @@ describe('ExperimentsTree', () => { hasChildren: false, id: 'f81f1b5', label: 'f81f1b5', - queued: true, + status: ExperimentStatus.QUEUED, type: ExperimentType.QUEUED } ] @@ -352,7 +351,6 @@ describe('ExperimentsTree', () => { iconPath: getMockedUri('circle-filled', '#b180d7'), id: 'f81f1b5', label: 'f81f1b5', - queued: false, tooltip: undefined, type: ExperimentType.BRANCH } @@ -363,7 +361,6 @@ describe('ExperimentsTree', () => { hasChildren: false, id: 'exp-abcdef', label: 'e350702', - running: false, selected: false, type: ExperimentType.EXPERIMENT } diff --git a/extension/src/experiments/model/tree.ts b/extension/src/experiments/model/tree.ts index 10e9af4369..109002062c 100644 --- a/extension/src/experiments/model/tree.ts +++ b/extension/src/experiments/model/tree.ts @@ -30,7 +30,7 @@ import { } from '../../commands/external' import { sum } from '../../util/math' import { Disposable } from '../../class/dispose' -import { Experiment } from '../webview/contract' +import { Experiment, ExperimentStatus, isRunning } from '../webview/contract' export class ExperimentsTree extends Disposable @@ -236,17 +236,17 @@ export class ExperimentsTree private getExperimentIcon({ displayColor, - running, + status, type, selected }: { displayColor?: string label: string - running?: boolean + status?: ExperimentStatus type?: ExperimentType selected?: boolean }): ThemeIcon | Uri | Resource { - if (running) { + if (isRunning(status)) { return this.getUriOrIcon(displayColor, IconName.LOADING_SPIN) } if (type === ExperimentType.QUEUED) { diff --git a/extension/src/experiments/webview/contract.ts b/extension/src/experiments/webview/contract.ts index 08713ee5e8..1409ff6ae8 100644 --- a/extension/src/experiments/webview/contract.ts +++ b/extension/src/experiments/webview/contract.ts @@ -1,7 +1,13 @@ -import { BaseExperimentFields, ValueTree } from '../../cli/dvc/contract' +import { + BaseExperimentFields, + ExperimentStatus, + ValueTree +} from '../../cli/dvc/contract' import { FilteredCounts } from '../model/filterBy/collect' import { SortDefinition } from '../model/sortBy' +export { ExperimentStatus } from '../../cli/dvc/contract' + export interface MetricOrParamColumns { [filename: string]: ValueTree } @@ -33,6 +39,12 @@ export interface Experiment extends BaseExperimentFields { Created?: string } +export const isRunning = (status: ExperimentStatus | undefined): boolean => + status === ExperimentStatus.RUNNING + +export const isQueued = (status: ExperimentStatus | undefined): boolean => + status === ExperimentStatus.QUEUED + export interface Row extends Experiment { subRows?: Row[] } diff --git a/extension/src/experiments/webview/messages.ts b/extension/src/experiments/webview/messages.ts index 57a5edc675..65c57156c3 100644 --- a/extension/src/experiments/webview/messages.ts +++ b/extension/src/experiments/webview/messages.ts @@ -139,7 +139,7 @@ export class WebviewMessages { this.showPlots() ]) - case MessageFromWebviewType.SET_EXPERIMENTS_HEADER_DEPTH: { + case MessageFromWebviewType.SET_EXPERIMENTS_HEADER_HEIGHT: { return this.setMaxTableHeadDepth() } @@ -168,12 +168,23 @@ export class WebviewMessages { private async setMaxTableHeadDepth() { const newValue = await getValidInput( - Title.SET_EXPERIMENTS_HEADER_DEPTH, + Title.SET_EXPERIMENTS_HEADER_HEIGHT, val => { return Number.isNaN(Number(val)) ? 'Input needs to be a number' : '' - } + }, + { prompt: 'Use 0 for infinite height.' } ) + + if (!newValue) { + return + } + setConfigValue(ConfigKey.EXP_TABLE_HEAD_MAX_DEPTH, Number(newValue)) + sendTelemetryEvent( + EventName.VIEWS_EXPERIMENTS_TABLE_SET_MAX_HEADER_HEIGHT, + undefined, + undefined + ) } private setColumnOrder(order: string[]) { diff --git a/extension/src/extension.ts b/extension/src/extension.ts index bdb0968528..99e04f2a04 100644 --- a/extension/src/extension.ts +++ b/extension/src/extension.ts @@ -4,7 +4,6 @@ import { DvcRunner } from './cli/dvc/runner' import { DvcReader } from './cli/dvc/reader' import { Config } from './config' import { Context } from './context' -import { isVersionCompatible } from './cli/dvc/version' import { isPythonExtensionInstalled } from './extensions/python' import { WorkspaceExperiments } from './experiments/workspace' import { registerExperimentCommands } from './experiments/commands/register' @@ -329,22 +328,20 @@ export class Extension extends Disposable implements IExtension { } } - public async isDvcPythonModule() { + public async isPythonExtensionUsed() { await this.config.isReady() - return !!this.config.getPythonBinPath() + return !!this.config.isPythonExtensionUsed() } - public async canRunCli(cwd: string, tryGlobalCli?: true) { + public async getCliVersion(cwd: string, tryGlobalCli?: true) { await this.config.isReady() - setContextValue('dvc.cli.incompatible', undefined) - const version = await this.dvcReader.version(cwd, tryGlobalCli) - const compatible = isVersionCompatible(version) - if (compatible && tryGlobalCli) { - this.config.unsetPythonBinPath() - } - this.cliCompatible = compatible - setContextValue('dvc.cli.incompatible', !compatible) - return this.setAvailable(compatible) + try { + return await this.dvcReader.version(cwd, tryGlobalCli) + } catch {} + } + + public unsetPythonBinPath() { + this.config.unsetPythonBinPath() } public async setRoots() { @@ -392,6 +389,12 @@ export class Extension extends Disposable implements IExtension { this.plots.reset() } + public setCliCompatible(compatible: boolean | undefined) { + this.cliCompatible = compatible + const incompatible = compatible === undefined ? undefined : !compatible + setContextValue('dvc.cli.incompatible', incompatible) + } + public setAvailable(available: boolean) { this.status.setAvailability(available) this.setCommandsAvailability(available) diff --git a/extension/src/interfaces.ts b/extension/src/interfaces.ts index 199c125926..3caed58c18 100644 --- a/extension/src/interfaces.ts +++ b/extension/src/interfaces.ts @@ -1,12 +1,18 @@ export interface IExtension { - canRunCli: (cwd: string, isCliGlobal?: true) => Promise + getCliVersion: ( + cwd: string, + isCliGlobal?: true + ) => Promise hasRoots: () => boolean - isDvcPythonModule: () => Promise + isPythonExtensionUsed: () => Promise setupWorkspace: () => void initialize: () => Promise resetMembers: () => void + setAvailable: (available: boolean) => void + setCliCompatible: (compatible: boolean | undefined) => void setRoots: () => Promise + unsetPythonBinPath: () => void } diff --git a/extension/src/plots/model/collect.ts b/extension/src/plots/model/collect.ts index e4f42f0fe8..4a155c9967 100644 --- a/extension/src/plots/model/collect.ts +++ b/extension/src/plots/model/collect.ts @@ -18,6 +18,7 @@ import { ExperimentFieldsOrError, ExperimentsBranchOutput, ExperimentsOutput, + ExperimentStatus, PlotsOutput, Value, ValueTree @@ -81,8 +82,7 @@ type MetricsAndDetailsOrUndefined = checkpoint_parent: string | undefined checkpoint_tip: string | undefined metrics: MetricOrParamColumns | undefined - queued: boolean | undefined - running: boolean | undefined + status: ExperimentStatus | undefined } | undefined @@ -94,19 +94,17 @@ const transformExperimentData = ( return } - const { checkpoint_tip, checkpoint_parent, queued, running } = - experimentFields + const { checkpoint_tip, checkpoint_parent, status } = experimentFields const { metrics } = extractColumns(experimentFields) - return { checkpoint_parent, checkpoint_tip, metrics, queued, running } + return { checkpoint_parent, checkpoint_tip, metrics, status } } type ValidData = { checkpoint_parent: string checkpoint_tip: string metrics: MetricOrParamColumns - queued: boolean | undefined - running: boolean | undefined + status: ExperimentStatus } const isValid = (data: MetricsAndDetailsOrUndefined): data is ValidData => diff --git a/extension/src/setup.test.ts b/extension/src/setup.test.ts index 72ffa5ccd4..14b66fe3da 100644 --- a/extension/src/setup.test.ts +++ b/extension/src/setup.test.ts @@ -1,4 +1,4 @@ -import { join, resolve } from 'path' +import { resolve } from 'path' import { extensions, Extension, commands } from 'vscode' import { setup, setupWorkspace } from './setup' import { flushPromises } from './test/util/jest' @@ -18,6 +18,12 @@ import { Toast } from './vscode/toast' import { Response } from './vscode/response' import { VscodePython } from './extensions/python' import { executeProcess } from './processExecution' +import { + LATEST_TESTED_CLI_VERSION, + MAX_CLI_VERSION, + MIN_CLI_VERSION +} from './cli/dvc/constants' +import { extractSemver, ParsedSemver } from './cli/dvc/version' jest.mock('vscode') jest.mock('./vscode/config') @@ -39,9 +45,10 @@ mockedExtensions.getExtension = mockedGetExtension const mockedReady = jest.fn() +const mockedPythonPath = 'python' const mockedSettings = { getExecutionDetails: () => ({ - execCommand: [join('some', 'bin', 'path')] + execCommand: [mockedPythonPath] }) } @@ -54,16 +61,18 @@ const mockedVscodePython = { activate: () => Promise.resolve(mockedVscodePythonAPI) } -const mockedCanRunCli = jest.fn() -const mockedHasRoots = jest.fn() -const mockedGetFirstWorkspaceFolder = jest.mocked(getFirstWorkspaceFolder) const mockedCwd = __dirname +const mockedGetCliVersion = jest.fn() +const mockedGetFirstWorkspaceFolder = jest.mocked(getFirstWorkspaceFolder) +const mockedHasRoots = jest.fn() const mockedInitialize = jest.fn() -const mockedIsDvcPythonModule = jest.fn() +const mockedIsPythonExtensionUsed = jest.fn() const mockedResetMembers = jest.fn() const mockedSetAvailable = jest.fn() +const mockedSetCliCompatible = jest.fn() const mockedSetRoots = jest.fn() const mockedSetupWorkspace = jest.fn() +const mockedUnsetPythonBinPath = jest.fn() const mockedQuickPickYesOrNo = jest.mocked(quickPickYesOrNo) const mockedQuickPickValue = jest.mocked(quickPickValue) @@ -248,14 +257,16 @@ describe('setupWorkspace', () => { describe('setup', () => { const extension = { - canRunCli: mockedCanRunCli, + getCliVersion: mockedGetCliVersion, hasRoots: mockedHasRoots, initialize: mockedInitialize, - isDvcPythonModule: mockedIsDvcPythonModule, + isPythonExtensionUsed: mockedIsPythonExtensionUsed, resetMembers: mockedResetMembers, setAvailable: mockedSetAvailable, + setCliCompatible: mockedSetCliCompatible, setRoots: mockedSetRoots, - setupWorkspace: mockedSetupWorkspace + setupWorkspace: mockedSetupWorkspace, + unsetPythonBinPath: mockedUnsetPythonBinPath } it('should do nothing if there is no workspace folder', async () => { @@ -263,14 +274,14 @@ describe('setup', () => { await setup(extension) - expect(mockedCanRunCli).not.toHaveBeenCalled() + expect(mockedGetCliVersion).not.toHaveBeenCalled() expect(mockedInitialize).not.toHaveBeenCalled() }) it('should set the DVC roots even if the cli cannot be used', async () => { mockedGetFirstWorkspaceFolder.mockReturnValueOnce(mockedCwd) - mockedIsDvcPythonModule.mockResolvedValueOnce(false) - mockedCanRunCli.mockResolvedValueOnce(false) + mockedIsPythonExtensionUsed.mockResolvedValueOnce(false) + mockedGetCliVersion.mockResolvedValueOnce(false) await setup(extension) @@ -280,8 +291,8 @@ describe('setup', () => { it('should not alert the user if the workspace has no DVC project and the cli cannot be found', async () => { mockedGetFirstWorkspaceFolder.mockReturnValueOnce(mockedCwd) mockedHasRoots.mockReturnValueOnce(false) - mockedIsDvcPythonModule.mockResolvedValueOnce(false) - mockedCanRunCli.mockRejectedValueOnce(new Error('command not found: dvc')) + mockedIsPythonExtensionUsed.mockResolvedValueOnce(false) + mockedGetCliVersion.mockResolvedValueOnce(undefined) await setup(extension) expect(mockedSetRoots).toHaveBeenCalledTimes(1) @@ -296,10 +307,10 @@ describe('setup', () => { it('should not alert the user if the workspace contains a DVC project, the cli cannot be found and the do not show option is set', async () => { mockedGetFirstWorkspaceFolder.mockReturnValueOnce(mockedCwd) mockedHasRoots.mockReturnValueOnce(true) - mockedIsDvcPythonModule.mockResolvedValueOnce(true) - mockedCanRunCli - .mockRejectedValueOnce(new Error('command not found: dvc')) - .mockRejectedValueOnce(new Error('command not found: dvc')) + mockedIsPythonExtensionUsed.mockResolvedValueOnce(true) + mockedGetCliVersion + .mockResolvedValueOnce(undefined) + .mockResolvedValueOnce(undefined) mockedGetConfigValue.mockReturnValueOnce(true) await setup(extension) @@ -315,10 +326,10 @@ describe('setup', () => { it('should alert the user if the workspace contains a DVC project and the cli cannot be found', async () => { mockedGetFirstWorkspaceFolder.mockReturnValueOnce(mockedCwd) mockedHasRoots.mockReturnValueOnce(true) - mockedIsDvcPythonModule.mockResolvedValueOnce(true) - mockedCanRunCli - .mockRejectedValueOnce(new Error('command not found: dvc')) - .mockRejectedValueOnce(new Error('command not found: dvc')) + mockedIsPythonExtensionUsed.mockResolvedValueOnce(true) + mockedGetCliVersion + .mockResolvedValueOnce(undefined) + .mockResolvedValueOnce(undefined) mockedWarnWithOptions.mockResolvedValueOnce(undefined) mockedExecuteProcess.mockImplementation(({ executable }) => Promise.resolve(executable) @@ -338,10 +349,10 @@ describe('setup', () => { it('should try to setup the workspace if the workspace contains a DVC project, the cli cannot be found and the user selects setup the workspace', async () => { mockedGetFirstWorkspaceFolder.mockReturnValueOnce(mockedCwd) mockedHasRoots.mockReturnValueOnce(true) - mockedIsDvcPythonModule.mockResolvedValueOnce(true) - mockedCanRunCli - .mockRejectedValueOnce(new Error('command not found: dvc')) - .mockRejectedValueOnce(new Error('command not found: dvc')) + mockedIsPythonExtensionUsed.mockResolvedValueOnce(true) + mockedGetCliVersion + .mockResolvedValueOnce(undefined) + .mockResolvedValueOnce(undefined) mockedWarnWithOptions.mockResolvedValueOnce(Response.SETUP_WORKSPACE) mockedExecuteProcess.mockImplementation(({ executable }) => Promise.resolve(executable) @@ -364,8 +375,8 @@ describe('setup', () => { it('should try to select the python interpreter if the workspace contains a DVC project, the cli cannot be found and the user decides to select the python interpreter', async () => { mockedGetFirstWorkspaceFolder.mockReturnValueOnce(mockedCwd) mockedHasRoots.mockReturnValueOnce(true) - mockedIsDvcPythonModule.mockResolvedValueOnce(false) - mockedCanRunCli.mockRejectedValueOnce(new Error('command not found: dvc')) + mockedIsPythonExtensionUsed.mockResolvedValueOnce(false) + mockedGetCliVersion.mockResolvedValueOnce(undefined) mockedWarnWithOptions.mockResolvedValueOnce(Response.SELECT_INTERPRETER) mockedExecuteProcess.mockImplementation(({ executable }) => Promise.resolve(executable) @@ -388,10 +399,8 @@ describe('setup', () => { it('should set a user config option if the workspace contains a DVC project, the cli cannot be found and the user selects never', async () => { mockedGetFirstWorkspaceFolder.mockReturnValueOnce(mockedCwd) mockedHasRoots.mockReturnValueOnce(true) - mockedIsDvcPythonModule.mockResolvedValueOnce(true) - mockedCanRunCli - .mockRejectedValueOnce(new Error('command not found: dvc')) - .mockRejectedValueOnce(new Error('command not found: dvc')) + mockedIsPythonExtensionUsed.mockResolvedValueOnce(false) + mockedGetCliVersion.mockResolvedValueOnce(undefined) mockedWarnWithOptions.mockResolvedValueOnce(Response.NEVER) mockedExecuteProcess.mockImplementation(({ executable }) => Promise.resolve(executable) @@ -413,9 +422,9 @@ describe('setup', () => { it('should not send telemetry or set the cli as unavailable or run initialization if roots have not been found but the cli can be run', async () => { mockedGetFirstWorkspaceFolder.mockReturnValueOnce(mockedCwd) - mockedHasRoots.mockReturnValueOnce(false) - mockedIsDvcPythonModule.mockResolvedValueOnce(false) - mockedCanRunCli.mockResolvedValueOnce(true) + mockedHasRoots.mockReturnValueOnce(false).mockReturnValueOnce(false) + mockedIsPythonExtensionUsed.mockResolvedValueOnce(false) + mockedGetCliVersion.mockResolvedValueOnce(MIN_CLI_VERSION) await setup(extension) expect(mockedSetRoots).toHaveBeenCalledTimes(1) @@ -426,44 +435,180 @@ describe('setup', () => { it('should run initialization if roots have been found and the cli can be run', async () => { mockedGetFirstWorkspaceFolder.mockReturnValueOnce(mockedCwd) - mockedHasRoots.mockReturnValueOnce(true) - mockedIsDvcPythonModule.mockResolvedValueOnce(true) - mockedCanRunCli.mockResolvedValueOnce(true) + mockedHasRoots.mockReturnValueOnce(true).mockReturnValueOnce(true) + mockedIsPythonExtensionUsed.mockResolvedValueOnce(true) + mockedGetCliVersion.mockResolvedValueOnce(MIN_CLI_VERSION) await setup(extension) expect(mockedResetMembers).not.toHaveBeenCalled() expect(mockedInitialize).toHaveBeenCalledTimes(1) }) - it('should call the cli to see if it is available from path if it fails on the first call', async () => { + it('should call the cli to see if it is available from path if the Python extension is being used and the first call fails', async () => { mockedGetFirstWorkspaceFolder.mockReturnValueOnce(mockedCwd) - mockedHasRoots.mockReturnValueOnce(true) - mockedIsDvcPythonModule.mockResolvedValueOnce(true) - mockedCanRunCli - .mockRejectedValueOnce(new Error('command not found: dvc')) - .mockResolvedValueOnce(true) + mockedHasRoots + .mockReturnValueOnce(true) + .mockReturnValueOnce(true) + .mockReturnValueOnce(true) + mockedIsPythonExtensionUsed.mockResolvedValueOnce(true) + mockedGetCliVersion + .mockResolvedValueOnce(undefined) + .mockResolvedValueOnce(MIN_CLI_VERSION) + + await setup(extension) + expect(mockedGetCliVersion).toHaveBeenCalledTimes(2) + expect(mockedResetMembers).not.toHaveBeenCalled() + expect(mockedInitialize).toHaveBeenCalledTimes(1) + }) + + it('should send a specific message to the user if the Python extension is being used, the CLI is not available in the virtual environment and the global CLI is not compatible', async () => { + const belowMinVersion = '2.0.0' + mockedGetFirstWorkspaceFolder.mockReturnValueOnce(mockedCwd) + mockedHasRoots + .mockReturnValueOnce(true) + .mockReturnValueOnce(true) + .mockReturnValueOnce(true) + mockedIsPythonExtensionUsed.mockResolvedValueOnce(true) + mockedExecuteProcess.mockImplementation(({ executable }) => + Promise.resolve(executable) + ) + mockedGetExtension.mockReturnValue(mockedVscodePython) + mockedGetCliVersion + .mockResolvedValueOnce(undefined) + .mockResolvedValueOnce(belowMinVersion) await setup(extension) - expect(mockedCanRunCli).toHaveBeenCalledTimes(2) + await flushPromises() + expect(mockedWarnWithOptions).toHaveBeenCalledTimes(1) + expect(mockedWarnWithOptions).toHaveBeenCalledWith( + `The extension is unable to initialize. The CLI was not located using the interpreter provided by the Python extension. ${belowMinVersion} is installed globally. For auto Python environment activation, ensure the correct interpreter is set. Active Python interpreter: ${mockedPythonPath}.`, + Response.SETUP_WORKSPACE, + Response.SELECT_INTERPRETER, + Response.NEVER + ) + expect(mockedGetCliVersion).toHaveBeenCalledTimes(2) + expect(mockedResetMembers).toHaveBeenCalledTimes(1) + expect(mockedInitialize).not.toHaveBeenCalled() + }) + + it('should send a specific message and initialize if the Python extension is being used, the CLI is not available in the virtual environment and the global CLI is a minor version ahead of the expected version', async () => { + const { major, minor, patch } = extractSemver( + LATEST_TESTED_CLI_VERSION + ) as ParsedSemver + mockedGetFirstWorkspaceFolder.mockReturnValueOnce(mockedCwd) + mockedHasRoots + .mockReturnValueOnce(true) + .mockReturnValueOnce(true) + .mockReturnValueOnce(true) + mockedIsPythonExtensionUsed.mockResolvedValueOnce(true) + mockedGetCliVersion + .mockResolvedValueOnce(undefined) + .mockResolvedValueOnce([major, minor + 1, patch].join('.')) + + await setup(extension) + await flushPromises() + expect(mockedWarnWithOptions).toHaveBeenCalledTimes(1) + expect(mockedWarnWithOptions).toHaveBeenCalledWith( + `The located DVC CLI is at least a minor version ahead of the latest version the extension was tested with (${LATEST_TESTED_CLI_VERSION}). This could lead to unexpected behaviour. Please upgrade to the most recent version of the extension and reload this window.` + ) + expect(mockedGetCliVersion).toHaveBeenCalledTimes(2) expect(mockedResetMembers).not.toHaveBeenCalled() expect(mockedInitialize).toHaveBeenCalledTimes(1) }) + it('should send a specific message to the user if the Python extension is not being used and the CLI is not available', async () => { + mockedGetFirstWorkspaceFolder.mockReturnValueOnce(mockedCwd) + mockedHasRoots.mockReturnValueOnce(true) + mockedExtensions.all = [] + mockedIsPythonExtensionUsed.mockResolvedValueOnce(false) + mockedGetCliVersion.mockResolvedValueOnce(undefined) + + await setup(extension) + await flushPromises() + expect(mockedWarnWithOptions).toHaveBeenCalledTimes(1) + expect(mockedWarnWithOptions).toHaveBeenCalledWith( + 'An error was thrown when trying to access the CLI.', + Response.SETUP_WORKSPACE, + Response.NEVER + ) + expect(mockedGetCliVersion).toHaveBeenCalledTimes(1) + expect(mockedResetMembers).toHaveBeenCalledTimes(1) + expect(mockedInitialize).not.toHaveBeenCalled() + }) + + it('should send a specific message to the user if the located CLI is a major version ahead', async () => { + const MajorAhead = MIN_CLI_VERSION.split('.') + .map(num => Number(num) + 100) + .join('.') + mockedGetFirstWorkspaceFolder.mockReturnValueOnce(mockedCwd) + mockedHasRoots.mockReturnValueOnce(true) + mockedIsPythonExtensionUsed.mockResolvedValueOnce(true) + mockedGetCliVersion.mockResolvedValueOnce(MajorAhead) + + await setup(extension) + await flushPromises() + expect(mockedWarnWithOptions).toHaveBeenCalledTimes(1) + expect(mockedWarnWithOptions).toHaveBeenCalledWith( + `The extension cannot initialize because you are using version ${MajorAhead} of the DVC CLI. The expected version is ${MIN_CLI_VERSION} <= DVC < ${MAX_CLI_VERSION}. Please upgrade to the most recent version of the extension and reload this window.` + ) + expect(mockedGetCliVersion).toHaveBeenCalledTimes(1) + expect(mockedResetMembers).toHaveBeenCalledTimes(1) + expect(mockedInitialize).not.toHaveBeenCalled() + }) + + it('should send a specific message to the user if the Python extension is being used, the CLI is not available in the virtual environment and no cli is found globally', async () => { + mockedGetFirstWorkspaceFolder.mockReturnValueOnce(mockedCwd) + mockedHasRoots.mockReturnValueOnce(true) + mockedIsPythonExtensionUsed.mockResolvedValueOnce(true) + mockedExecuteProcess.mockImplementation(({ executable }) => + Promise.resolve(executable) + ) + mockedGetExtension.mockReturnValue(mockedVscodePython) + mockedGetCliVersion + .mockResolvedValueOnce(undefined) + .mockResolvedValueOnce(undefined) + + await setup(extension) + await flushPromises() + expect(mockedWarnWithOptions).toHaveBeenCalledTimes(1) + expect(mockedWarnWithOptions).toHaveBeenCalledWith( + `The extension is unable to initialize. The CLI was not located using the interpreter provided by the Python extension. The CLI is also not installed globally. For auto Python environment activation, ensure the correct interpreter is set. Active Python interpreter: ${mockedPythonPath}.`, + Response.SETUP_WORKSPACE, + Response.SELECT_INTERPRETER, + Response.NEVER + ) + expect(mockedGetCliVersion).toHaveBeenCalledTimes(2) + expect(mockedResetMembers).toHaveBeenCalledTimes(1) + expect(mockedInitialize).not.toHaveBeenCalled() + }) + it('should only call the cli once if the python extension is not used', async () => { mockedGetFirstWorkspaceFolder.mockReturnValueOnce(mockedCwd) mockedHasRoots.mockReturnValueOnce(true) - mockedIsDvcPythonModule.mockResolvedValueOnce(false) - mockedCanRunCli.mockResolvedValueOnce(false) + mockedIsPythonExtensionUsed.mockResolvedValueOnce(false) + mockedGetCliVersion.mockResolvedValueOnce(false) + + await setup(extension) + expect(mockedGetCliVersion).toHaveBeenCalledTimes(1) + }) + + it('should not attempt to find the cli globally if the python extension is used and the found version is behind', async () => { + const [major] = MIN_CLI_VERSION.split('.') + const behind = [major, 0, 0].join('.') + mockedGetFirstWorkspaceFolder.mockReturnValueOnce(mockedCwd) + mockedHasRoots.mockReturnValueOnce(true) + mockedIsPythonExtensionUsed.mockResolvedValueOnce(true) + mockedGetCliVersion.mockResolvedValueOnce(behind) await setup(extension) - expect(mockedCanRunCli).toHaveBeenCalledTimes(1) + expect(mockedGetCliVersion).toHaveBeenCalledTimes(1) }) it('should run reset if the cli cannot be run and there is a workspace folder open', async () => { mockedGetFirstWorkspaceFolder.mockReturnValueOnce(mockedCwd) - mockedIsDvcPythonModule.mockResolvedValueOnce(true) + mockedIsPythonExtensionUsed.mockResolvedValueOnce(true) mockedHasRoots.mockReturnValueOnce(true) - mockedCanRunCli.mockResolvedValueOnce(false) + mockedGetCliVersion.mockResolvedValueOnce(false) await setup(extension) expect(mockedResetMembers).toHaveBeenCalledTimes(1) diff --git a/extension/src/setup.ts b/extension/src/setup.ts index 86f0bf113b..a051e774ce 100644 --- a/extension/src/setup.ts +++ b/extension/src/setup.ts @@ -4,22 +4,12 @@ import { quickPickValue, quickPickYesOrNo } from './vscode/quickPick' -import { - ConfigKey, - getConfigValue, - setConfigValue, - setUserConfigValue -} from './vscode/config' +import { ConfigKey, setConfigValue } from './vscode/config' import { pickFile } from './vscode/resourcePicker' import { getFirstWorkspaceFolder } from './vscode/workspaceFolders' -import { Response } from './vscode/response' import { getSelectTitle, Title } from './vscode/title' -import { Toast } from './vscode/toast' -import { - getPythonBinPath, - isPythonExtensionInstalled, - selectPythonInterpreter -} from './extensions/python' +import { isPythonExtensionInstalled } from './extensions/python' +import { extensionCanRunCli } from './cli/dvc/discovery' const setConfigPath = async ( option: ConfigKey, @@ -163,86 +153,6 @@ export const setupWorkspace = async (): Promise => { return pickCliPath() } -const getToastText = async ( - isPythonExtensionInstalled: boolean -): Promise => { - const text = 'An error was thrown when trying to access the CLI.' - if (!isPythonExtensionInstalled) { - return text - } - const binPath = await getPythonBinPath() - - return ( - text + - ` For auto Python environment activation ensure the correct interpreter is set. Active Python interpreter: ${binPath}. ` - ) -} - -const getToastOptions = (isPythonExtensionInstalled: boolean): Response[] => { - return isPythonExtensionInstalled - ? [Response.SETUP_WORKSPACE, Response.SELECT_INTERPRETER, Response.NEVER] - : [Response.SETUP_WORKSPACE, Response.NEVER] -} - -const warnUserCLIInaccessible = async ( - extension: IExtension -): Promise => { - if (getConfigValue(ConfigKey.DO_NOT_SHOW_CLI_UNAVAILABLE)) { - return - } - - const isMsPythonInstalled = isPythonExtensionInstalled() - const warningText = await getToastText(isMsPythonInstalled) - - const response = await Toast.warnWithOptions( - warningText, - ...getToastOptions(isMsPythonInstalled) - ) - - switch (response) { - case Response.SELECT_INTERPRETER: - return selectPythonInterpreter() - case Response.SETUP_WORKSPACE: - return extension.setupWorkspace() - case Response.NEVER: - return setUserConfigValue(ConfigKey.DO_NOT_SHOW_CLI_UNAVAILABLE, true) - } -} - -const extensionCanRunPythonCli = async (extension: IExtension, cwd: string) => { - let canRunCli = false - if (await extension.isDvcPythonModule()) { - try { - canRunCli = await extension.canRunCli(cwd) - } catch {} - } - return canRunCli -} - -const extensionCanRunGlobalCli = async (extension: IExtension, cwd: string) => { - let canRunCli = false - try { - canRunCli = await extension.canRunCli(cwd, true) - } catch { - if (extension.hasRoots()) { - warnUserCLIInaccessible(extension) - } - } - return canRunCli -} - -const extensionCanRunCli = async ( - extension: IExtension, - cwd: string -): Promise => { - let canRunCli = await extensionCanRunPythonCli(extension, cwd) - - if (!canRunCli) { - canRunCli = await extensionCanRunGlobalCli(extension, cwd) - } - return canRunCli -} - export const setup = async (extension: IExtension) => { const cwd = getFirstWorkspaceFolder() if (!cwd) { @@ -251,15 +161,18 @@ export const setup = async (extension: IExtension) => { extension.setRoots() - const isCliAvailable = await extensionCanRunCli(extension, cwd) + const { isAvailable, isCompatible } = await extensionCanRunCli(extension, cwd) + + extension.setCliCompatible(isCompatible) - if (extension.hasRoots() && isCliAvailable) { + if (extension.hasRoots() && isAvailable) { + extension.setAvailable(isAvailable) return extension.initialize() } extension.resetMembers() - if (!isCliAvailable) { + if (!isAvailable) { extension.setAvailable(false) } } diff --git a/extension/src/telemetry/constants.ts b/extension/src/telemetry/constants.ts index 4db56136da..fe8149ea04 100644 --- a/extension/src/telemetry/constants.ts +++ b/extension/src/telemetry/constants.ts @@ -28,8 +28,6 @@ export const EventName = Object.assign( EXTENSION_EXECUTION_DETAILS_CHANGED: 'extension.executionDetails.changed', EXTENSION_LOAD: 'extension.load', - SET_EXPERIMENTS_HEADER_DEPTH: 'extension.updateHeaderDepth', - VIEWS_EXPERIMENTS_TABLE_CLOSED: 'views.experimentsTable.closed', VIEWS_EXPERIMENTS_TABLE_COLUMNS_REORDERED: 'views.experimentsTable.columnsReordered', @@ -55,6 +53,8 @@ export const EventName = Object.assign( 'views.experimentsTable.selectColumns', VIEWS_EXPERIMENTS_TABLE_SELECT_EXPERIMENTS_FOR_PLOTS: 'views.experimentsTable.selectExperimentsForPlots', + VIEWS_EXPERIMENTS_TABLE_SET_MAX_HEADER_HEIGHT: + 'views.experimentsTable.updateHeaderMaxHeight', VIEWS_EXPERIMENTS_TABLE_SORT_COLUMN: 'views.experimentsTable.columnSortAdded', @@ -197,8 +197,6 @@ export interface IEventNamePropertyMapping { [EventName.EXTENSION_SHOW_COMMANDS]: undefined [EventName.EXTENSION_SHOW_OUTPUT]: undefined - [EventName.SET_EXPERIMENTS_HEADER_DEPTH]: undefined - [EventName.VIEWS_EXPERIMENTS_TREE_OPENED]: DvcRootCount [EventName.VIEWS_EXPERIMENTS_FILTER_BY_TREE_OPENED]: DvcRootCount [EventName.VIEWS_EXPERIMENTS_METRICS_AND_PARAMS_TREE_OPENED]: DvcRootCount @@ -213,6 +211,7 @@ export interface IEventNamePropertyMapping { [EventName.VIEWS_EXPERIMENTS_TABLE_RESIZE_COLUMN]: { width: number } + [EventName.VIEWS_EXPERIMENTS_TABLE_SET_MAX_HEADER_HEIGHT]: undefined [EventName.VIEWS_EXPERIMENTS_TABLE_SORT_COLUMN]: SortDefinition [EventName.VIEWS_EXPERIMENTS_TABLE_REMOVE_COLUMN_SORT]: { path: string diff --git a/extension/src/test/fixtures/expShow/dataTypes.ts b/extension/src/test/fixtures/expShow/dataTypes.ts index 09b83d7246..789bf27c95 100644 --- a/extension/src/test/fixtures/expShow/dataTypes.ts +++ b/extension/src/test/fixtures/expShow/dataTypes.ts @@ -1,4 +1,4 @@ -import { ExperimentsOutput } from '../../../cli/dvc/contract' +import { ExperimentsOutput, ExperimentStatus } from '../../../cli/dvc/contract' import { timestampColumn } from '../../../experiments/columns/constants' import { Column, @@ -26,8 +26,7 @@ export const dataTypesOutput: ExperimentsOutput = { } } }, - queued: false, - running: false, + status: ExperimentStatus.SUCCESS, executor: null } } @@ -36,8 +35,7 @@ export const dataTypesOutput: ExperimentsOutput = { baseline: { data: { timestamp: '2020-11-21T19:58:22', - queued: false, - running: false, + status: ExperimentStatus.SUCCESS, executor: null, name: 'main' } @@ -159,8 +157,7 @@ export const rows: Row[] = [ zero: 0 } }, - queued: false, - running: false, + status: ExperimentStatus.SUCCESS, selected: true, starred: false }, @@ -170,8 +167,7 @@ export const rows: Row[] = [ id: 'main', label: 'main', name: 'main', - queued: false, - running: false, + status: ExperimentStatus.SUCCESS, selected: true, sha: '53c3851f46955fa3e2b8f6e1c52999acc8c9ea77', starred: false, diff --git a/extension/src/test/fixtures/expShow/deeplyNested.ts b/extension/src/test/fixtures/expShow/deeplyNested.ts index 693464a01e..11e48cc447 100644 --- a/extension/src/test/fixtures/expShow/deeplyNested.ts +++ b/extension/src/test/fixtures/expShow/deeplyNested.ts @@ -1,4 +1,4 @@ -import { ExperimentsOutput } from '../../../cli/dvc/contract' +import { ExperimentsOutput, ExperimentStatus } from '../../../cli/dvc/contract' import { timestampColumn } from '../../../experiments/columns/constants' import { Column, @@ -32,8 +32,7 @@ export const deeplyNestedOutput: ExperimentsOutput = { } } }, - queued: false, - running: false, + status: ExperimentStatus.SUCCESS, executor: null } } @@ -63,8 +62,7 @@ export const deeplyNestedOutput: ExperimentsOutput = { } } }, - queued: false, - running: false, + status: ExperimentStatus.SUCCESS, executor: null, name: 'main' } @@ -655,8 +653,6 @@ export const rows = [ { id: 'workspace', label: 'workspace', - queued: false, - running: false, executor: null, params: { 'params.yaml': { @@ -679,14 +675,14 @@ export const rows = [ }, displayColor: '#945dd6', selected: true, + status: ExperimentStatus.SUCCESS, starred: false }, { id: 'main', label: 'main', Created: '2020-11-21T19:58:22', - queued: false, - running: false, + status: ExperimentStatus.SUCCESS, executor: null, name: 'main', sha: '53c3851f46955fa3e2b8f6e1c52999acc8c9ea77', diff --git a/extension/src/test/fixtures/expShow/modified.ts b/extension/src/test/fixtures/expShow/modified.ts index de2315a32e..c99ebccd25 100644 --- a/extension/src/test/fixtures/expShow/modified.ts +++ b/extension/src/test/fixtures/expShow/modified.ts @@ -1,3 +1,5 @@ +import { ExperimentStatus } from '../../../cli/dvc/contract' + const data = { workspace: { baseline: { @@ -12,8 +14,7 @@ const data = { } } }, - queued: false, - running: false, + status: ExperimentStatus.SUCCESS, executor: null, metrics: { 'logs.json': { @@ -40,8 +41,7 @@ const data = { } } }, - queued: false, - running: false, + status: ExperimentStatus.SUCCESS, executor: null, metrics: { 'logs.json': { @@ -62,8 +62,7 @@ const data = { params: { 'params.yaml': { data: { seed: 473987, lr: 0.001, weight_decay: 0 } } }, - queued: false, - running: false, + status: ExperimentStatus.SUCCESS, executor: null, metrics: { 'logs.json': { @@ -81,8 +80,7 @@ const data = { params: { 'params.yaml': { data: { seed: 473987, lr: 0.001, weight_decay: 0 } } }, - queued: false, - running: false, + status: ExperimentStatus.SUCCESS, executor: null, metrics: { 'logs.json': { @@ -99,8 +97,7 @@ const data = { params: { 'params.yaml': { data: { seed: 473987, lr: 0.001, weight_decay: 0 } } }, - queued: false, - running: false, + status: ExperimentStatus.SUCCESS, executor: null, metrics: { 'logs.json': { @@ -117,8 +114,7 @@ const data = { params: { 'params.yaml': { data: { seed: 473987, lr: 0.001, weight_decay: 0 } } }, - queued: false, - running: false, + status: ExperimentStatus.SUCCESS, executor: null, metrics: { 'logs.json': { @@ -141,8 +137,7 @@ const data = { } } }, - queued: false, - running: false, + status: ExperimentStatus.SUCCESS, executor: null, metrics: { 'logs.json': { @@ -170,8 +165,7 @@ const data = { } } }, - queued: false, - running: false, + status: ExperimentStatus.SUCCESS, executor: null, metrics: { 'logs.json': { @@ -198,8 +192,7 @@ const data = { } } }, - queued: false, - running: false, + status: ExperimentStatus.SUCCESS, executor: null, metrics: { 'logs.json': { @@ -226,8 +219,7 @@ const data = { } } }, - queued: false, - running: false, + status: ExperimentStatus.SUCCESS, executor: null, metrics: { 'logs.json': { @@ -254,8 +246,7 @@ const data = { } } }, - queued: false, - running: false, + status: ExperimentStatus.SUCCESS, executor: null, metrics: { 'logs.json': { @@ -282,8 +273,7 @@ const data = { } } }, - queued: false, - running: false, + status: ExperimentStatus.SUCCESS, executor: null, metrics: { 'logs.json': { @@ -310,8 +300,7 @@ const data = { } } }, - queued: false, - running: false, + status: ExperimentStatus.SUCCESS, executor: null, metrics: { 'logs.json': { @@ -338,8 +327,7 @@ const data = { } } }, - queued: false, - running: false, + status: ExperimentStatus.SUCCESS, executor: null, metrics: { 'logs.json': { @@ -366,8 +354,7 @@ const data = { } } }, - queued: false, - running: false, + status: ExperimentStatus.SUCCESS, executor: null, metrics: { 'logs.json': { @@ -395,8 +382,7 @@ const data = { } } }, - queued: false, - running: false, + status: ExperimentStatus.SUCCESS, executor: null, metrics: { 'logs.json': { @@ -423,8 +409,7 @@ const data = { } } }, - queued: false, - running: false, + status: ExperimentStatus.SUCCESS, executor: null, metrics: { 'logs.json': { @@ -451,8 +436,7 @@ const data = { } } }, - queued: false, - running: false, + status: ExperimentStatus.SUCCESS, executor: null, metrics: { 'logs.json': { @@ -479,8 +463,7 @@ const data = { } } }, - queued: false, - running: false, + status: ExperimentStatus.SUCCESS, executor: null, metrics: { 'logs.json': { @@ -507,8 +490,7 @@ const data = { } } }, - queued: false, - running: false, + status: ExperimentStatus.SUCCESS, executor: null, metrics: { 'logs.json': { @@ -535,8 +517,7 @@ const data = { } } }, - queued: false, - running: false, + status: ExperimentStatus.SUCCESS, executor: null, metrics: { 'logs.json': { @@ -564,8 +545,7 @@ const data = { } } }, - queued: false, - running: false, + status: ExperimentStatus.SUCCESS, executor: null, metrics: { 'logs.json': { @@ -592,8 +572,7 @@ const data = { } } }, - queued: false, - running: false, + status: ExperimentStatus.SUCCESS, executor: null, metrics: { 'logs.json': { @@ -620,8 +599,7 @@ const data = { } } }, - queued: false, - running: false, + status: ExperimentStatus.SUCCESS, executor: null, metrics: { 'logs.json': { @@ -648,8 +626,7 @@ const data = { } } }, - queued: false, - running: false, + status: ExperimentStatus.SUCCESS, executor: null, metrics: { 'logs.json': { @@ -676,8 +653,7 @@ const data = { } } }, - queued: false, - running: false, + status: ExperimentStatus.SUCCESS, executor: null, metrics: { 'logs.json': { @@ -704,8 +680,7 @@ const data = { } } }, - queued: false, - running: false, + status: ExperimentStatus.SUCCESS, executor: null, metrics: { 'logs.json': { @@ -732,8 +707,7 @@ const data = { } } }, - queued: false, - running: false, + status: ExperimentStatus.SUCCESS, executor: null, metrics: { 'logs.json': { @@ -760,8 +734,7 @@ const data = { } } }, - queued: false, - running: false, + status: ExperimentStatus.SUCCESS, executor: null, metrics: { 'logs.json': { @@ -788,8 +761,7 @@ const data = { } } }, - queued: false, - running: false, + status: ExperimentStatus.SUCCESS, executor: null, metrics: { 'logs.json': { @@ -816,8 +788,7 @@ const data = { } } }, - queued: false, - running: false, + status: ExperimentStatus.SUCCESS, executor: null, metrics: { 'logs.json': { @@ -844,8 +815,7 @@ const data = { } } }, - queued: false, - running: false, + status: ExperimentStatus.SUCCESS, executor: null, metrics: { 'logs.json': { @@ -872,8 +842,7 @@ const data = { } } }, - queued: false, - running: false, + status: ExperimentStatus.SUCCESS, executor: null, metrics: { 'logs.json': { @@ -900,8 +869,7 @@ const data = { } } }, - queued: false, - running: false, + status: ExperimentStatus.SUCCESS, executor: null, metrics: { 'logs.json': { @@ -928,8 +896,7 @@ const data = { } } }, - queued: false, - running: false, + status: ExperimentStatus.SUCCESS, executor: null, metrics: { 'logs.json': { @@ -956,8 +923,7 @@ const data = { } } }, - queued: false, - running: false, + status: ExperimentStatus.SUCCESS, executor: null, metrics: { 'logs.json': { @@ -984,8 +950,7 @@ const data = { } } }, - queued: false, - running: false, + status: ExperimentStatus.SUCCESS, executor: null, metrics: { 'logs.json': { @@ -1012,8 +977,7 @@ const data = { } } }, - queued: false, - running: false, + status: ExperimentStatus.SUCCESS, executor: null, metrics: { 'logs.json': { @@ -1040,8 +1004,7 @@ const data = { } } }, - queued: false, - running: false, + status: ExperimentStatus.SUCCESS, executor: null, metrics: { 'logs.json': { @@ -1068,8 +1031,7 @@ const data = { } } }, - queued: false, - running: false, + status: ExperimentStatus.SUCCESS, executor: null, metrics: { 'logs.json': { @@ -1096,8 +1058,7 @@ const data = { } } }, - queued: false, - running: false, + status: ExperimentStatus.SUCCESS, executor: null, metrics: { 'logs.json': { @@ -1124,8 +1085,7 @@ const data = { } } }, - queued: false, - running: false, + status: ExperimentStatus.SUCCESS, executor: null, metrics: { 'logs.json': { @@ -1152,8 +1112,7 @@ const data = { } } }, - queued: false, - running: false, + status: ExperimentStatus.SUCCESS, executor: null, metrics: { 'logs.json': { @@ -1180,8 +1139,7 @@ const data = { } } }, - queued: false, - running: false, + status: ExperimentStatus.SUCCESS, executor: null, metrics: { 'logs.json': { @@ -1208,8 +1166,7 @@ const data = { } } }, - queued: false, - running: false, + status: ExperimentStatus.SUCCESS, executor: null, metrics: { 'logs.json': { @@ -1236,8 +1193,7 @@ const data = { } } }, - queued: false, - running: false, + status: ExperimentStatus.SUCCESS, executor: null, metrics: { 'logs.json': { @@ -1264,8 +1220,7 @@ const data = { } } }, - queued: false, - running: false, + status: ExperimentStatus.SUCCESS, executor: null, metrics: { 'logs.json': { @@ -1292,8 +1247,7 @@ const data = { } } }, - queued: false, - running: false, + status: ExperimentStatus.SUCCESS, executor: null, metrics: { 'logs.json': { @@ -1320,8 +1274,7 @@ const data = { } } }, - queued: false, - running: false, + status: ExperimentStatus.SUCCESS, executor: null, metrics: { 'logs.json': { @@ -1348,8 +1301,7 @@ const data = { } } }, - queued: false, - running: false, + status: ExperimentStatus.SUCCESS, executor: null, metrics: { 'logs.json': { @@ -1377,8 +1329,7 @@ const data = { } } }, - queued: false, - running: false, + status: ExperimentStatus.SUCCESS, executor: null, metrics: { 'logs.json': { @@ -1405,8 +1356,7 @@ const data = { } } }, - queued: false, - running: false, + status: ExperimentStatus.SUCCESS, executor: null, metrics: { 'logs.json': { @@ -1433,8 +1383,7 @@ const data = { } } }, - queued: false, - running: false, + status: ExperimentStatus.SUCCESS, executor: null, metrics: { 'logs.json': { @@ -1461,8 +1410,7 @@ const data = { } } }, - queued: false, - running: false, + status: ExperimentStatus.SUCCESS, executor: null, metrics: { 'logs.json': { @@ -1489,8 +1437,7 @@ const data = { } } }, - queued: false, - running: false, + status: ExperimentStatus.SUCCESS, executor: null, metrics: { 'logs.json': { @@ -1517,8 +1464,7 @@ const data = { } } }, - queued: false, - running: false, + status: ExperimentStatus.SUCCESS, executor: null, metrics: { 'logs.json': { @@ -1545,8 +1491,7 @@ const data = { } } }, - queued: false, - running: false, + status: ExperimentStatus.SUCCESS, executor: null, metrics: { 'logs.json': { @@ -1573,8 +1518,7 @@ const data = { } } }, - queued: false, - running: false, + status: ExperimentStatus.SUCCESS, executor: null, metrics: { 'logs.json': { @@ -1601,8 +1545,7 @@ const data = { } } }, - queued: false, - running: false, + status: ExperimentStatus.SUCCESS, executor: null, metrics: { 'logs.json': { @@ -1629,8 +1572,7 @@ const data = { } } }, - queued: false, - running: false, + status: ExperimentStatus.SUCCESS, executor: null, metrics: { 'logs.json': { @@ -1657,8 +1599,7 @@ const data = { } } }, - queued: false, - running: false, + status: ExperimentStatus.SUCCESS, executor: null, metrics: { 'logs.json': { @@ -1685,8 +1626,7 @@ const data = { } } }, - queued: false, - running: false, + status: ExperimentStatus.SUCCESS, executor: null, metrics: { 'logs.json': { @@ -1713,8 +1653,7 @@ const data = { } } }, - queued: false, - running: false, + status: ExperimentStatus.SUCCESS, executor: null, metrics: { 'logs.json': { @@ -1741,8 +1680,7 @@ const data = { } } }, - queued: false, - running: false, + status: ExperimentStatus.SUCCESS, executor: null, metrics: { 'logs.json': { @@ -1769,8 +1707,7 @@ const data = { } } }, - queued: false, - running: false, + status: ExperimentStatus.SUCCESS, executor: null, metrics: { 'logs.json': { @@ -1797,8 +1734,7 @@ const data = { } } }, - queued: false, - running: false, + status: ExperimentStatus.SUCCESS, executor: null, metrics: { 'logs.json': { @@ -1825,8 +1761,7 @@ const data = { } } }, - queued: false, - running: false, + status: ExperimentStatus.SUCCESS, executor: null, metrics: { 'logs.json': { @@ -1853,8 +1788,7 @@ const data = { } } }, - queued: false, - running: false, + status: ExperimentStatus.SUCCESS, executor: null, metrics: { 'logs.json': { @@ -1881,8 +1815,7 @@ const data = { } } }, - queued: false, - running: false, + status: ExperimentStatus.SUCCESS, executor: null, metrics: { 'logs.json': { @@ -1909,8 +1842,7 @@ const data = { } } }, - queued: false, - running: false, + status: ExperimentStatus.SUCCESS, executor: null, metrics: { 'logs.json': { @@ -1937,8 +1869,7 @@ const data = { } } }, - queued: false, - running: false, + status: ExperimentStatus.SUCCESS, executor: null, metrics: { 'logs.json': { @@ -1965,8 +1896,7 @@ const data = { } } }, - queued: false, - running: false, + status: ExperimentStatus.SUCCESS, executor: null, metrics: { 'logs.json': { @@ -1993,8 +1923,7 @@ const data = { } } }, - queued: false, - running: false, + status: ExperimentStatus.SUCCESS, executor: null, metrics: { 'logs.json': { @@ -2021,8 +1950,7 @@ const data = { } } }, - queued: false, - running: false, + status: ExperimentStatus.SUCCESS, executor: null, metrics: { 'logs.json': { @@ -2049,8 +1977,7 @@ const data = { } } }, - queued: false, - running: false, + status: ExperimentStatus.SUCCESS, executor: null, metrics: { 'logs.json': { @@ -2077,8 +2004,7 @@ const data = { } } }, - queued: false, - running: false, + status: ExperimentStatus.SUCCESS, executor: null, metrics: { 'logs.json': { @@ -2105,8 +2031,7 @@ const data = { } } }, - queued: false, - running: false, + status: ExperimentStatus.SUCCESS, executor: null, metrics: { 'logs.json': { @@ -2133,8 +2058,7 @@ const data = { } } }, - queued: false, - running: false, + status: ExperimentStatus.SUCCESS, executor: null, metrics: { 'logs.json': { @@ -2161,8 +2085,7 @@ const data = { } } }, - queued: false, - running: false, + status: ExperimentStatus.SUCCESS, executor: null, metrics: { 'logs.json': { @@ -2189,8 +2112,7 @@ const data = { } } }, - queued: false, - running: false, + status: ExperimentStatus.SUCCESS, executor: null, metrics: { 'logs.json': { @@ -2217,8 +2139,7 @@ const data = { } } }, - queued: false, - running: false, + status: ExperimentStatus.SUCCESS, executor: null, metrics: { 'logs.json': { @@ -2245,8 +2166,7 @@ const data = { } } }, - queued: false, - running: false, + status: ExperimentStatus.SUCCESS, executor: null, metrics: { 'logs.json': { @@ -2273,8 +2193,7 @@ const data = { } } }, - queued: false, - running: false, + status: ExperimentStatus.SUCCESS, executor: null, metrics: { 'logs.json': { @@ -2301,8 +2220,7 @@ const data = { } } }, - queued: false, - running: false, + status: ExperimentStatus.SUCCESS, executor: null, metrics: { 'logs.json': { @@ -2329,8 +2247,7 @@ const data = { } } }, - queued: false, - running: false, + status: ExperimentStatus.SUCCESS, executor: null, metrics: { 'logs.json': { @@ -2357,8 +2274,7 @@ const data = { } } }, - queued: false, - running: false, + status: ExperimentStatus.SUCCESS, executor: null, metrics: { 'logs.json': { @@ -2385,8 +2301,7 @@ const data = { } } }, - queued: false, - running: false, + status: ExperimentStatus.SUCCESS, executor: null, metrics: { 'logs.json': { @@ -2413,8 +2328,7 @@ const data = { } } }, - queued: false, - running: false, + status: ExperimentStatus.SUCCESS, executor: null, metrics: { 'logs.json': { @@ -2441,8 +2355,7 @@ const data = { } } }, - queued: false, - running: false, + status: ExperimentStatus.SUCCESS, executor: null, metrics: { 'logs.json': { @@ -2469,8 +2382,7 @@ const data = { } } }, - queued: false, - running: false, + status: ExperimentStatus.SUCCESS, executor: null, metrics: { 'logs.json': { diff --git a/extension/src/test/fixtures/expShow/output.ts b/extension/src/test/fixtures/expShow/output.ts index c2bbe6e87b..b2fb5f859f 100644 --- a/extension/src/test/fixtures/expShow/output.ts +++ b/extension/src/test/fixtures/expShow/output.ts @@ -1,9 +1,10 @@ import { join } from '../../util/path' -import { ExperimentsOutput } from '../../../cli/dvc/contract' +import { ExperimentsOutput, ExperimentStatus } from '../../../cli/dvc/contract' export const errorShas = [ '489fd8bdaa709f7330aac342e051a9431c625481', - 'f0f918662b4f8c47819ca154a23029bf9b47d4f3' + 'f0f918662b4f8c47819ca154a23029bf9b47d4f3', + '55d492c9c633912685351b32df91bfe1f9ecefb9' ] const data: ExperimentsOutput = { @@ -111,8 +112,7 @@ const data: ExperimentsOutput = { } } }, - queued: false, - running: true, + status: ExperimentStatus.RUNNING, timestamp: null } } @@ -222,8 +222,7 @@ const data: ExperimentsOutput = { } } }, - queued: false, - running: false, + status: ExperimentStatus.SUCCESS, timestamp: '2020-11-21T19:58:22' } }, @@ -333,8 +332,7 @@ const data: ExperimentsOutput = { } } }, - queued: false, - running: true, + status: ExperimentStatus.RUNNING, timestamp: '2020-12-29T15:31:52' } }, @@ -443,8 +441,7 @@ const data: ExperimentsOutput = { } } }, - queued: false, - running: false, + status: ExperimentStatus.SUCCESS, timestamp: '2020-12-29T15:31:51' } }, @@ -553,8 +550,7 @@ const data: ExperimentsOutput = { } } }, - queued: false, - running: false, + status: ExperimentStatus.SUCCESS, timestamp: '2020-12-29T15:31:44' } }, @@ -664,8 +660,7 @@ const data: ExperimentsOutput = { is_data_source: true } }, - queued: false, - running: false, + status: ExperimentStatus.SUCCESS, timestamp: '2020-12-29T15:28:59' } }, @@ -774,8 +769,7 @@ const data: ExperimentsOutput = { } } }, - queued: false, - running: false, + status: ExperimentStatus.SUCCESS, timestamp: '2020-12-29T15:28:57' } }, @@ -884,8 +878,7 @@ const data: ExperimentsOutput = { } } }, - queued: false, - running: false, + status: ExperimentStatus.SUCCESS, timestamp: '2020-12-29T15:28:50' } }, @@ -995,8 +988,7 @@ const data: ExperimentsOutput = { } } }, - queued: false, - running: false, + status: ExperimentStatus.SUCCESS, timestamp: '2020-12-29T15:27:02' } }, @@ -1105,8 +1097,7 @@ const data: ExperimentsOutput = { is_data_source: true } }, - queued: false, - running: false, + status: ExperimentStatus.SUCCESS, timestamp: '2020-12-29T15:27:01' } }, @@ -1215,8 +1206,7 @@ const data: ExperimentsOutput = { is_data_source: true } }, - queued: false, - running: false, + status: ExperimentStatus.SUCCESS, timestamp: '2020-12-29T15:26:55' } }, @@ -1325,8 +1315,7 @@ const data: ExperimentsOutput = { } } }, - queued: false, - running: false, + status: ExperimentStatus.SUCCESS, timestamp: '2020-12-29T15:26:49' } }, @@ -1435,8 +1424,7 @@ const data: ExperimentsOutput = { } } }, - queued: false, - running: false, + status: ExperimentStatus.SUCCESS, timestamp: '2020-12-29T15:26:43' } }, @@ -1545,8 +1533,7 @@ const data: ExperimentsOutput = { } } }, - queued: false, - running: false, + status: ExperimentStatus.RUNNING, timestamp: '2020-12-29T15:26:36' } }, @@ -1658,8 +1645,7 @@ const data: ExperimentsOutput = { } } }, - queued: false, - running: false, + status: ExperimentStatus.SUCCESS, timestamp: '2020-12-29T15:26:36' } }, @@ -1755,7 +1741,78 @@ const data: ExperimentsOutput = { } } }, - queued: true, + status: ExperimentStatus.QUEUED, + timestamp: '2020-12-29T15:25:27' + } + }, + '55d492c9c633912685351b32df91bfe1f9ecefb9': { + data: { + deps: { + [join('data', 'data.xml')]: { + hash: '22a1a2931c8370d3aeedd7183606fd7f', + size: 14445097, + nfiles: null + }, + [join('src', 'prepare.py')]: { + hash: 'f09ea0c15980b43010257ccb9f0055e2', + size: 1576, + nfiles: null + }, + [join('data', 'prepared')]: { + hash: '153aad06d376b6595932470e459ef42a.dir', + size: 8437363, + nfiles: 2 + }, + [join('src', 'featurization.py')]: { + hash: 'e0265fc22f056a4b86d85c3056bc2894', + size: 2490, + nfiles: null + }, + [join('data', 'features')]: { + hash: 'f35d4cc2c552ac959ae602162b8543f3.dir', + size: 2232588, + nfiles: 2 + }, + [join('src', 'train.py')]: { + hash: 'c3961d777cfbd7727f9fde4851896006', + size: 967, + nfiles: null + }, + 'model.pkl': { + hash: '46865edbf3d62fc5c039dd9d2b0567a4', + size: 1763725, + nfiles: null + }, + [join('src', 'evaluate.py')]: { + hash: '44e714021a65edf881b1716e791d7f59', + size: 2346, + nfiles: null + } + }, + error: { + msg: 'Experiment run failed.', + type: 'Queue failure' + }, + outs: {}, + params: { + 'params.yaml': { + data: { + code_names: [0, 2], + epochs: 5, + learning_rate: 2.1e-7, + dvc_logs_dir: 'dvc_logs', + log_file: 'logs.csv', + dropout: 0.125, + process: { threshold: 0.85 } + } + }, + [join('nested', 'params.yaml')]: { + data: { + test: true + } + } + }, + status: ExperimentStatus.FAILED, timestamp: '2020-12-29T15:25:27' } } diff --git a/extension/src/test/fixtures/expShow/rows.ts b/extension/src/test/fixtures/expShow/rows.ts index 48107b9537..497ddaadde 100644 --- a/extension/src/test/fixtures/expShow/rows.ts +++ b/extension/src/test/fixtures/expShow/rows.ts @@ -2,6 +2,7 @@ import { join } from '../../util/path' import { Row } from '../../../experiments/webview/contract' import { copyOriginalColors } from '../../../experiments/model/status/colors' import { shortenForLabel } from '../../../util/string' +import { ExperimentStatus } from '../../../cli/dvc/contract' const valueWithNoChanges = (str: string) => ({ value: shortenForLabel(str), @@ -92,8 +93,7 @@ const data: Row[] = [ test: true } }, - queued: false, - running: true, + status: ExperimentStatus.RUNNING, selected: true, starred: false }, @@ -179,8 +179,7 @@ const data: Row[] = [ test: true } }, - queued: false, - running: false, + status: ExperimentStatus.SUCCESS, selected: true, sha: '53c3851f46955fa3e2b8f6e1c52999acc8c9ea77', starred: false, @@ -271,8 +270,7 @@ const data: Row[] = [ test: true } }, - queued: false, - running: true, + status: ExperimentStatus.RUNNING, selected: true, sha: '4fb124aebddb2adf1545030907687fa9a4c80e70', starred: false, @@ -363,8 +361,7 @@ const data: Row[] = [ test: true } }, - queued: false, - running: false, + status: ExperimentStatus.SUCCESS, selected: false, sha: 'd1343a87c6ee4a2e82d19525964d2fb2cb6756c9', starred: false, @@ -456,8 +453,7 @@ const data: Row[] = [ test: true } }, - queued: false, - running: false, + status: ExperimentStatus.SUCCESS, selected: false, sha: '1ee5f2ecb0fa4d83cbf614386536344cf894dd53', starred: false, @@ -552,8 +548,7 @@ const data: Row[] = [ test: true } }, - queued: false, - running: false, + status: ExperimentStatus.SUCCESS, selected: true, sha: '42b8736b08170529903cd203a1f40382a4b4a8cd', starred: false, @@ -644,8 +639,7 @@ const data: Row[] = [ test: true } }, - queued: false, - running: false, + status: ExperimentStatus.SUCCESS, selected: false, starred: false, sha: '217312476f8854dda1865450b737eb6bc7a3ba1b', @@ -737,8 +731,7 @@ const data: Row[] = [ test: true } }, - queued: false, - running: false, + status: ExperimentStatus.SUCCESS, selected: false, sha: '9523bde67538cf31230efaff2dbc47d38a944ab5', starred: false, @@ -833,8 +826,7 @@ const data: Row[] = [ test: true } }, - queued: false, - running: false, + status: ExperimentStatus.SUCCESS, selected: true, sha: '1ba7bcd6ce6154e72e18b155475663ecbbd1f49d', starred: false, @@ -925,8 +917,7 @@ const data: Row[] = [ test: true } }, - queued: false, - running: false, + status: ExperimentStatus.SUCCESS, selected: false, sha: '22e40e1fa3c916ac567f69b85969e3066a91dda4', starred: false, @@ -1018,8 +1009,7 @@ const data: Row[] = [ test: true } }, - queued: false, - running: false, + status: ExperimentStatus.SUCCESS, selected: false, sha: '91116c1eae4b79cb1f5ab0312dfd9b3e43608e15', starred: false, @@ -1111,8 +1101,7 @@ const data: Row[] = [ test: true } }, - queued: false, - running: false, + status: ExperimentStatus.SUCCESS, selected: false, sha: 'e821416bfafb4bc28b3e0a8ddb322505b0ad2361', starred: false, @@ -1204,8 +1193,7 @@ const data: Row[] = [ test: true } }, - queued: false, - running: false, + status: ExperimentStatus.SUCCESS, selected: false, sha: 'c658f8b14ac819ac2a5ea0449da6c15dbe8eb880', starred: false, @@ -1297,8 +1285,7 @@ const data: Row[] = [ test: true } }, - queued: false, - running: false, + status: ExperimentStatus.RUNNING, selected: false, sha: '23250b33e3d6dd0e136262d1d26a2face031cb03', starred: false, @@ -1395,14 +1382,14 @@ const data: Row[] = [ test: true } }, - queued: false, - running: false, + status: ExperimentStatus.SUCCESS, selected: true, sha: 'f0f918662b4f8c47819ca154a23029bf9b47d4f3', starred: false, Created: '2020-12-29T15:26:36' }, { + displayColor: undefined, deps: { [join('data', 'data.xml')]: valueWithNoChanges( '22a1a2931c8370d3aeedd7183606fd7f' @@ -1473,11 +1460,61 @@ const data: Row[] = [ test: true } }, - queued: true, selected: false, + status: ExperimentStatus.QUEUED, sha: '90aea7f2482117a55dfcadcdb901aaa6610fbbc9', starred: false, Created: '2020-12-29T15:25:27' + }, + { + displayColor: undefined, + deps: { + [join('data', 'data.xml')]: valueWithNoChanges( + '22a1a2931c8370d3aeedd7183606fd7f' + ), + [join('data', 'features')]: valueWithNoChanges( + 'f35d4cc2c552ac959ae602162b8543f3.dir' + ), + [join('data', 'prepared')]: valueWithNoChanges( + '153aad06d376b6595932470e459ef42a.dir' + ), + 'model.pkl': valueWithNoChanges('46865edbf3d62fc5c039dd9d2b0567a4'), + [join('src', 'evaluate.py')]: valueWithNoChanges( + '44e714021a65edf881b1716e791d7f59' + ), + [join('src', 'featurization.py')]: valueWithNoChanges( + 'e0265fc22f056a4b86d85c3056bc2894' + ), + [join('src', 'prepare.py')]: valueWithNoChanges( + 'f09ea0c15980b43010257ccb9f0055e2' + ), + [join('src', 'train.py')]: valueWithNoChanges( + 'c3961d777cfbd7727f9fde4851896006' + ) + }, + error: 'Experiment run failed.', + id: '55d492c9c633912685351b32df91bfe1f9ecefb9', + label: '55d492c', + outs: {}, + params: { + 'params.yaml': { + code_names: [0, 2], + epochs: 5, + learning_rate: 2.1e-7, + dvc_logs_dir: 'dvc_logs', + log_file: 'logs.csv', + dropout: 0.125, + process: { threshold: 0.85 } + }, + [join('nested', 'params.yaml')]: { + test: true + } + }, + selected: false, + status: ExperimentStatus.FAILED, + sha: '55d492c9c633912685351b32df91bfe1f9ecefb9', + starred: false, + Created: '2020-12-29T15:25:27' } ], Created: '2020-11-21T19:58:22' diff --git a/extension/src/test/fixtures/expShow/uncommittedDeps.ts b/extension/src/test/fixtures/expShow/uncommittedDeps.ts index 270c908cf9..8cab7fc95a 100644 --- a/extension/src/test/fixtures/expShow/uncommittedDeps.ts +++ b/extension/src/test/fixtures/expShow/uncommittedDeps.ts @@ -1,4 +1,4 @@ -import { ExperimentsOutput } from '../../../cli/dvc/contract' +import { ExperimentsOutput, ExperimentStatus } from '../../../cli/dvc/contract' const data: ExperimentsOutput = { workspace: { @@ -53,8 +53,7 @@ const data: ExperimentsOutput = { is_data_source: false } }, - queued: false, - running: false, + status: ExperimentStatus.SUCCESS, executor: null, metrics: {} } @@ -66,8 +65,7 @@ const data: ExperimentsOutput = { timestamp: '2022-08-13T09:13:15', deps: {}, outs: {}, - queued: false, - running: false, + status: ExperimentStatus.SUCCESS, executor: null, metrics: {}, name: 'main' diff --git a/extension/src/test/suite/experiments/index.test.ts b/extension/src/test/suite/experiments/index.test.ts index 5ab8e5b88e..e7e73cb6bb 100644 --- a/extension/src/test/suite/experiments/index.test.ts +++ b/extension/src/test/suite/experiments/index.test.ts @@ -32,6 +32,7 @@ import { buildInternalCommands, buildMockData, closeAllEditors, + configurationChangeEvent, experimentsUpdatedEvent, extensionUri, getInputBoxEvent, @@ -878,6 +879,41 @@ suite('Experiments Test Suite', () => { ) }).timeout(WEBVIEW_TEST_TIMEOUT) + it('should be able to handle a message to update the table depth', async () => { + const { experiments } = buildExperiments(disposable, expShowFixture) + const inputEvent = getInputBoxEvent('0') + const tableMaxDepthOption = 'dvc.experimentsTableHeadMaxHeight' + const tableMaxDepthChanged = configurationChangeEvent( + tableMaxDepthOption, + disposable + ) + + const webview = await experiments.showWebview() + + const mockSendTelemetryEvent = stub(Telemetry, 'sendTelemetryEvent') + const mockMessageReceived = getMessageReceivedEmitter(webview) + mockMessageReceived.fire({ + type: MessageFromWebviewType.SET_EXPERIMENTS_HEADER_HEIGHT + }) + + await inputEvent + await mockMessageReceived + await tableMaxDepthChanged + + expect( + await workspace.getConfiguration().get(tableMaxDepthOption) + ).to.equal(0) + expect(mockSendTelemetryEvent).to.be.calledOnce + expect( + mockSendTelemetryEvent, + 'should send a telemetry call that tells you the max height has been updated' + ).to.be.calledWithExactly( + EventName.VIEWS_EXPERIMENTS_TABLE_SET_MAX_HEADER_HEIGHT, + undefined, + undefined + ) + }).timeout(WEBVIEW_TEST_TIMEOUT) + it("should be able to handle a message to toggle an experiment's star status", async () => { const { experiments, experimentsModel } = setupExperimentsAndMockCommands() @@ -1260,6 +1296,7 @@ suite('Experiments Test Suite', () => { '22e40e1fa3c916ac567f69b85969e3066a91dda4': 0, '23250b33e3d6dd0e136262d1d26a2face031cb03': 0, '489fd8bdaa709f7330aac342e051a9431c625481': colors[5], + '55d492c9c633912685351b32df91bfe1f9ecefb9': 0, '91116c1eae4b79cb1f5ab0312dfd9b3e43608e15': 0, '9523bde67538cf31230efaff2dbc47d38a944ab5': 0, c658f8b14ac819ac2a5ea0449da6c15dbe8eb880: 0, @@ -1359,6 +1396,7 @@ suite('Experiments Test Suite', () => { '22e40e1fa3c916ac567f69b85969e3066a91dda4': 0, '23250b33e3d6dd0e136262d1d26a2face031cb03': 0, '489fd8bdaa709f7330aac342e051a9431c625481': colors[5], + '55d492c9c633912685351b32df91bfe1f9ecefb9': 0, '91116c1eae4b79cb1f5ab0312dfd9b3e43608e15': 0, '9523bde67538cf31230efaff2dbc47d38a944ab5': 0, c658f8b14ac819ac2a5ea0449da6c15dbe8eb880: 0, @@ -1380,6 +1418,7 @@ suite('Experiments Test Suite', () => { 'experimentsSortBy:test': sortDefinitions, 'experimentsStatus:test': { '489fd8bdaa709f7330aac342e051a9431c625481': 0, + '55d492c9c633912685351b32df91bfe1f9ecefb9': 0, 'exp-83425': colors[0], 'exp-e7a67': 0, 'exp-f13bca': 0, diff --git a/extension/src/test/suite/experiments/model/filterBy/tree.test.ts b/extension/src/test/suite/experiments/model/filterBy/tree.test.ts index e6a9c57b14..8a0a8abb6c 100644 --- a/extension/src/test/suite/experiments/model/filterBy/tree.test.ts +++ b/extension/src/test/suite/experiments/model/filterBy/tree.test.ts @@ -24,6 +24,7 @@ import { buildExperiments, stubWorkspaceExperimentsGetters } from '../../util' import { ColumnType, Experiment, + isQueued, TableData } from '../../../../../experiments/webview/contract' import { WEBVIEW_TEST_TIMEOUT } from '../../../timeouts' @@ -95,7 +96,7 @@ suite('Experiments Filter By Tree Test Suite', () => { ) }) .map(experiment => - experiment.queued || experiment.error + isQueued(experiment.status) || experiment.error ? experiment : { ...experiment, @@ -478,7 +479,7 @@ suite('Experiments Filter By Tree Test Suite', () => { columnOrder: [], columnWidths: {}, columns: columnsFixture, - filteredCounts: { checkpoints: 9, experiments: 5 }, + filteredCounts: { checkpoints: 9, experiments: 6 }, filters: ['starred'], hasCheckpoints: true, hasColumns: true, diff --git a/extension/src/test/suite/experiments/model/tree.test.ts b/extension/src/test/suite/experiments/model/tree.test.ts index b04de2b72f..2f9be4723e 100644 --- a/extension/src/test/suite/experiments/model/tree.test.ts +++ b/extension/src/test/suite/experiments/model/tree.test.ts @@ -368,27 +368,27 @@ suite('Experiments Tree Test Suite', () => { }, { displayColor: colors[1], + group: '[exp-83425]', + id: '23250b33e3d6dd0e136262d1d26a2face031cb03', + revision: '23250b3' + }, + { + displayColor: colors[4], group: '[exp-e7a67]', id: 'd1343a87c6ee4a2e82d19525964d2fb2cb6756c9', revision: 'd1343a8' }, { - displayColor: colors[4], + displayColor: colors[5], group: '[exp-e7a67]', id: '1ee5f2ecb0fa4d83cbf614386536344cf894dd53', revision: '1ee5f2e' }, { - displayColor: colors[5], + displayColor: colors[6], group: '[test-branch]', id: '217312476f8854dda1865450b737eb6bc7a3ba1b', revision: '2173124' - }, - { - displayColor: colors[6], - group: '[test-branch]', - id: '9523bde67538cf31230efaff2dbc47d38a944ab5', - revision: '9523bde' } ]) expect( @@ -398,7 +398,7 @@ suite('Experiments Tree Test Suite', () => { dvcDemoPath, '1ee5f2e', '2173124', - '9523bde', + '23250b3', 'd1343a8' ) }).timeout(WEBVIEW_TEST_TIMEOUT) diff --git a/extension/src/test/suite/experiments/workspace.test.ts b/extension/src/test/suite/experiments/workspace.test.ts index 9d85dfa0d9..436be3a485 100644 --- a/extension/src/test/suite/experiments/workspace.test.ts +++ b/extension/src/test/suite/experiments/workspace.test.ts @@ -501,6 +501,14 @@ suite('Workspace Experiments Test Suite', () => { description: '[exp-f13bca]', label: 'f0f9186', value: { id: 'exp-f13bca', name: 'exp-f13bca' } + }, + { + description: undefined, + label: '55d492c', + value: { + id: '55d492c9c633912685351b32df91bfe1f9ecefb9', + name: '55d492c' + } } ], { diff --git a/extension/src/test/suite/extension.test.ts b/extension/src/test/suite/extension.test.ts index 67bc510913..b7bf1431e9 100644 --- a/extension/src/test/suite/extension.test.ts +++ b/extension/src/test/suite/extension.test.ts @@ -470,8 +470,7 @@ suite('Extension Test Suite', () => { RegisteredCommands.EXTENSION_CHECK_CLI_COMPATIBLE ) - expect(mockVersion).to.be.calledTwice - mockVersion.resetHistory() + expect(mockVersion).to.be.calledOnce expect( executeCommandSpy, 'should set dvc.cli.incompatible to true if the version is incompatible' @@ -483,7 +482,6 @@ suite('Extension Test Suite', () => { ) expect(mockVersion).to.be.calledTwice - mockVersion.resetHistory() expect( executeCommandSpy, 'should set dvc.cli.incompatible to false if the version is compatible' @@ -498,7 +496,6 @@ suite('Extension Test Suite', () => { RegisteredCommands.EXTENSION_CHECK_CLI_COMPATIBLE ) - expect(mockVersion).to.be.calledTwice expect( executeCommandSpy, 'should unset dvc.cli.incompatible if the CLI throws an error' diff --git a/extension/src/test/suite/plots/index.test.ts b/extension/src/test/suite/plots/index.test.ts index 22d3a52147..d57be0ea27 100644 --- a/extension/src/test/suite/plots/index.test.ts +++ b/extension/src/test/suite/plots/index.test.ts @@ -37,6 +37,7 @@ import { MessageFromWebviewType } from '../../../webview/contract' import { reorderObjectList } from '../../../util/array' import * as Telemetry from '../../../telemetry' import { EventName } from '../../../telemetry/constants' +import { ExperimentStatus } from '../../../cli/dvc/contract' import { SelectedExperimentWithColor } from '../../../experiments/model' suite('Plots Test Suite', () => { @@ -100,17 +101,14 @@ suite('Plots Test Suite', () => { '53c3851f46955fa3e2b8f6e1c52999acc8c9ea77': { checkpoint: { data: { - checkpoint_tip: 'experiment', - queued: false, - running: false + checkpoint_tip: 'experiment' } }, experiment: { data: { checkpoint_tip: 'experiment', name: 'exp-e1new', - queued: false, - running: true + status: ExperimentStatus.RUNNING } } } diff --git a/extension/src/vscode/config.ts b/extension/src/vscode/config.ts index f4763296d1..d6d3eb3bbf 100644 --- a/extension/src/vscode/config.ts +++ b/extension/src/vscode/config.ts @@ -5,7 +5,7 @@ export enum ConfigKey { DO_NOT_SHOW_CLI_UNAVAILABLE = 'dvc.doNotShowCliUnavailable', DO_NOT_SHOW_WALKTHROUGH_AFTER_INSTALL = 'dvc.doNotShowWalkthroughAfterInstall', DO_NOT_SHOW_UNABLE_TO_FILTER = 'dvc.doNotShowUnableToFilter', - EXP_TABLE_HEAD_MAX_DEPTH = 'dvc.expTableHeadMaxLayers', + EXP_TABLE_HEAD_MAX_DEPTH = 'dvc.experimentsTableHeadMaxHeight', DVC_PATH = 'dvc.dvcPath', PYTHON_PATH = 'dvc.pythonPath' } diff --git a/extension/src/vscode/inputBox.ts b/extension/src/vscode/inputBox.ts index 30457c59dd..f4c1eeae0b 100644 --- a/extension/src/vscode/inputBox.ts +++ b/extension/src/vscode/inputBox.ts @@ -10,10 +10,11 @@ export const getInput = (title: Title, value?: string) => export const getValidInput = ( title: Title, validateInput: (text?: string) => null | string, - value?: string + options?: { prompt?: string; value?: string } ) => window.showInputBox({ + prompt: options?.prompt, title, validateInput, - value + value: options?.value }) diff --git a/extension/src/vscode/title.ts b/extension/src/vscode/title.ts index 1123352db4..fc3fb1f8e3 100644 --- a/extension/src/vscode/title.ts +++ b/extension/src/vscode/title.ts @@ -18,7 +18,7 @@ export enum Title { SELECT_SORT_DIRECTION = 'Select Sort Direction', SELECT_SORTS_TO_REMOVE = 'Select Sort(s) to Remove', SETUP_WORKSPACE = 'Setup the Workspace', - SET_EXPERIMENTS_HEADER_DEPTH = 'Set Maximum Experiment Table Header Depth' + SET_EXPERIMENTS_HEADER_HEIGHT = 'Set Maximum Experiment Table Header Height' } export const getEnterValueTitle = (path: string): Title => diff --git a/extension/src/webview/contract.ts b/extension/src/webview/contract.ts index 4db255c881..b6d36ba64e 100644 --- a/extension/src/webview/contract.ts +++ b/extension/src/webview/contract.ts @@ -45,7 +45,7 @@ export enum MessageFromWebviewType { MODIFY_EXPERIMENT_PARAMS_AND_QUEUE = 'modify-experiment-params-and-queue', MODIFY_EXPERIMENT_PARAMS_AND_RUN = 'modify-experiment-params-and-run', MODIFY_EXPERIMENT_PARAMS_RESET_AND_RUN = 'modify-experiment-params-reset-and-run', - SET_EXPERIMENTS_HEADER_DEPTH = 'update-experiments-header-depth' + SET_EXPERIMENTS_HEADER_HEIGHT = 'update-experiments-header-height' } export type ColumnResizePayload = { @@ -172,7 +172,7 @@ export type MessageFromWebview = type: MessageFromWebviewType.SHARE_EXPERIMENT_AS_COMMIT payload: string } - | { type: MessageFromWebviewType.SET_EXPERIMENTS_HEADER_DEPTH } + | { type: MessageFromWebviewType.SET_EXPERIMENTS_HEADER_HEIGHT } export type MessageToWebview = { type: MessageToWebviewType.SET_DATA diff --git a/package.json b/package.json index 97c7bc3462..094c2c98fe 100644 --- a/package.json +++ b/package.json @@ -35,8 +35,8 @@ "svgr": "yarn workspace dvc-vscode-webview svgr" }, "devDependencies": { - "@typescript-eslint/eslint-plugin": "5.38.1", - "@typescript-eslint/parser": "5.38.1", + "@typescript-eslint/eslint-plugin": "5.39.0", + "@typescript-eslint/parser": "5.39.0", "@vscode/codicons": "0.0.32", "eslint": "8.24.0", "eslint-config-prettier": "8.5.0", @@ -45,7 +45,7 @@ "eslint-plugin-check-file": "1.2.3", "eslint-plugin-etc": "2.0.2", "eslint-plugin-import": "2.26.0", - "eslint-plugin-jest": "27.0.4", + "eslint-plugin-jest": "27.1.0", "eslint-plugin-jsx-a11y": "6.6.1", "eslint-plugin-n": "15.3.0", "eslint-plugin-node": "11.1.0", diff --git a/webview/package.json b/webview/package.json index 101b1da794..e7ef2cc237 100644 --- a/webview/package.json +++ b/webview/package.json @@ -56,7 +56,7 @@ "@types/classnames": "2.3.1", "@types/jest": "29.1.1", "@types/jsdom": "20.0.0", - "@types/node": "16.11.63", + "@types/node": "16.11.64", "@types/react": "18.0.21", "@types/react-dom": "18.0.6", "@types/react-measure": "2.0.8", diff --git a/webview/src/experiments/components/App.test.tsx b/webview/src/experiments/components/App.test.tsx index 947678ed42..760f60fb70 100644 --- a/webview/src/experiments/components/App.test.tsx +++ b/webview/src/experiments/components/App.test.tsx @@ -702,7 +702,7 @@ describe('App', () => { const itemLabels = menuitems.map(item => item.textContent) expect(itemLabels).toStrictEqual([ 'Open to the Side', - 'Set Max Header Depth', + 'Set Max Header Height', 'Sort Ascending', 'Sort Descending' ]) diff --git a/webview/src/experiments/components/table/Cell.tsx b/webview/src/experiments/components/table/Cell.tsx index 8bb1c2c5a9..b7d260199b 100644 --- a/webview/src/experiments/components/table/Cell.tsx +++ b/webview/src/experiments/components/table/Cell.tsx @@ -37,7 +37,7 @@ export const FirstCell: React.FC = ({ }) => { const { row, isPlaceholder } = cell const { - original: { error, label, queued, displayNameOrParent = '' } + original: { error, status, label, displayNameOrParent = '' } } = row const { toggleExperiment } = rowActionsProps @@ -52,7 +52,7 @@ export const FirstCell: React.FC = ({ })} >
- + {isPlaceholder ? null : ( diff --git a/webview/src/experiments/components/table/CellRowActions.tsx b/webview/src/experiments/components/table/CellRowActions.tsx index a85368a9aa..a990a7aaf5 100644 --- a/webview/src/experiments/components/table/CellRowActions.tsx +++ b/webview/src/experiments/components/table/CellRowActions.tsx @@ -1,6 +1,10 @@ import React from 'react' import cx from 'classnames' import { VSCodeCheckbox } from '@vscode/webview-ui-toolkit/react' +import { + ExperimentStatus, + isQueued +} from 'dvc/src/experiments/webview/contract' import { Indicator } from './Indicators' import styles from './styles.module.scss' import { CellHintTooltip } from './CellHintTooltip' @@ -20,7 +24,7 @@ export type CellRowActionsProps = { toggleStarred: () => void bulletColor?: string toggleExperiment: () => void - queued?: boolean + status?: ExperimentStatus } export type CellRowActionProps = { @@ -60,7 +64,7 @@ const getTooltipContent = (determiner: boolean, text: string): string => export const CellRowActions: React.FC = ({ bulletColor, - queued, + status, toggleExperiment, isRowSelected, showSubRowStates, @@ -99,7 +103,7 @@ export const CellRowActions: React.FC = ({ {starred ? : }
- {queued ? ( + {isQueued(status) ? (
diff --git a/webview/src/experiments/components/table/Row.tsx b/webview/src/experiments/components/table/Row.tsx index 77118c9852..26b20a45bd 100644 --- a/webview/src/experiments/components/table/Row.tsx +++ b/webview/src/experiments/components/table/Row.tsx @@ -1,7 +1,11 @@ import React from 'react' import { useSelector } from 'react-redux' import cx from 'classnames' -import { Experiment } from 'dvc/src/experiments/webview/contract' +import { + Experiment, + isQueued, + isRunning +} from 'dvc/src/experiments/webview/contract' import { MessageFromWebviewType } from 'dvc/src/webview/contract' import { RowProp } from './interfaces' import styles from './styles.module.scss' @@ -14,11 +18,11 @@ import { HandlerFunc } from '../../../util/props' import { cond } from '../../../util/helpers' import { ExperimentsState } from '../../store' -const getExperimentTypeClass = ({ running, queued, selected }: Experiment) => { - if (running) { +const getExperimentTypeClass = ({ status, selected }: Experiment) => { + if (isRunning(status)) { return styles.runningExperiment } - if (queued) { + if (isQueued(status)) { return styles.queuedExperiment } if (selected === false) { diff --git a/webview/src/experiments/components/table/RowContextMenu.tsx b/webview/src/experiments/components/table/RowContextMenu.tsx index 6967e2052b..8989134e14 100644 --- a/webview/src/experiments/components/table/RowContextMenu.tsx +++ b/webview/src/experiments/components/table/RowContextMenu.tsx @@ -1,5 +1,9 @@ import React from 'react' import { MessageFromWebviewType } from 'dvc/src/webview/contract' +import { + ExperimentStatus, + isQueued +} from 'dvc/src/experiments/webview/contract' import { RowProp } from './interfaces' import { RowSelectionContext } from './RowSelectionContext' import { MessagesMenu } from '../../../shared/components/messagesMenu/MessagesMenu' @@ -144,10 +148,11 @@ const getSingleSelectMenuOptions = ( projectHasCheckpoints: boolean, hasRunningExperiment: boolean, depth: number, - queued?: boolean, + status?: ExperimentStatus, starred?: boolean ) => { - const isNotExperimentOrCheckpoint = queued || isWorkspace || depth <= 0 + const isNotExperimentOrCheckpoint = + isQueued(status) || isWorkspace || depth <= 0 const withId = ( label: string, @@ -213,7 +218,7 @@ const getContextMenuOptions = ( hasRunningExperiment: boolean, depth: number, selectedRows: Record, - queued?: boolean, + status?: ExperimentStatus, starred?: boolean ) => { const isFromSelection = !!selectedRows[id] @@ -231,7 +236,7 @@ const getContextMenuOptions = ( projectHasCheckpoints, hasRunningExperiment, depth, - queued, + status, starred ) ) @@ -241,7 +246,7 @@ export const RowContextMenu: React.FC = ({ hasRunningExperiment = false, projectHasCheckpoints = false, row: { - original: { queued, starred }, + original: { status, starred }, depth, values: { id } } @@ -259,11 +264,11 @@ export const RowContextMenu: React.FC = ({ hasRunningExperiment, depth, selectedRows, - queued, + status, starred ) }, [ - queued, + status, starred, isWorkspace, depth, diff --git a/webview/src/experiments/components/table/Table.test.tsx b/webview/src/experiments/components/table/Table.test.tsx index d119ea1330..4ac1efeb8c 100644 --- a/webview/src/experiments/components/table/Table.test.tsx +++ b/webview/src/experiments/components/table/Table.test.tsx @@ -9,7 +9,11 @@ import { screen } from '@testing-library/react' import { Provider } from 'react-redux' -import { Experiment, TableData } from 'dvc/src/experiments/webview/contract' +import { + Experiment, + ExperimentStatus, + TableData +} from 'dvc/src/experiments/webview/contract' import { MessageFromWebviewType } from 'dvc/src/webview/contract' import React from 'react' import { TableInstance } from 'react-table' @@ -53,8 +57,7 @@ describe('Table', () => { row: { id: 'workspace', original: { - queued: false, - running: false + status: ExperimentStatus.SUCCESS } } } @@ -102,8 +105,7 @@ describe('Table', () => { id: 'workspace', label: 'workspace', original: { - queued: false, - running: false + status: ExperimentStatus.SUCCESS }, values: { id: 'workspace' @@ -258,6 +260,23 @@ describe('Table', () => { }) }) + describe('Head Depth', () => { + it('should be updated by the user in the header context menu', async () => { + renderExperimentsTable() + const column = await screen.findByText('C') + fireEvent.contextMenu(column, { + bubbles: true + }) + + const sortOption = await screen.findByText('Set Max Header Height') + fireEvent.click(sortOption) + + expect(mockedPostMessage).toHaveBeenCalledWith({ + type: MessageFromWebviewType.SET_EXPERIMENTS_HEADER_HEIGHT + }) + }) + }) + describe('Changes', () => { it('should not have the workspaceWithChanges class on a row if there are no workspace changes', async () => { renderTable() diff --git a/webview/src/experiments/components/table/TableHeader.tsx b/webview/src/experiments/components/table/TableHeader.tsx index ec4ae97ae2..078dbd4b63 100644 --- a/webview/src/experiments/components/table/TableHeader.tsx +++ b/webview/src/experiments/components/table/TableHeader.tsx @@ -81,9 +81,9 @@ export const TableHeader: React.FC = ({ }, { id: 'update-header-depth', - label: 'Set Max Header Depth', + label: 'Set Max Header Height', message: { - type: MessageFromWebviewType.SET_EXPERIMENTS_HEADER_DEPTH + type: MessageFromWebviewType.SET_EXPERIMENTS_HEADER_HEIGHT } } ] diff --git a/webview/src/stories/Table.stories.tsx b/webview/src/stories/Table.stories.tsx index f5625b6627..255f7747d2 100644 --- a/webview/src/stories/Table.stories.tsx +++ b/webview/src/stories/Table.stories.tsx @@ -8,6 +8,10 @@ import workspaceChangesFixture from 'dvc/src/test/fixtures/expShow/workspaceChan import deeplyNestedTableData from 'dvc/src/test/fixtures/expShow/deeplyNested' import { dataTypesTableData } from 'dvc/src/test/fixtures/expShow/dataTypes' import { timestampColumn } from 'dvc/src/experiments/columns/constants' +import { + ExperimentStatus, + isRunning +} from 'dvc/src/experiments/webview/contract' import { within, userEvent, @@ -46,7 +50,7 @@ const tableData: TableDataState = { starred: experiment.starred || experiment.label === '42b8736', subRows: experiment.subRows?.map(checkpoint => ({ ...checkpoint, - running: checkpoint.running || checkpoint.label === '23250b3', + running: isRunning(checkpoint.status) || checkpoint.label === '23250b3', starred: checkpoint.starred || checkpoint.label === '22e40e1' })) })) @@ -62,13 +66,15 @@ const noRunningExperiments = { hasRunningExperiment: false, rows: rowsFixture.map(row => ({ ...row, - running: false, + status: ExperimentStatus.SUCCESS, subRows: row.subRows?.map(experiment => ({ ...experiment, - running: false, + status: isRunning(experiment.status) + ? ExperimentStatus.SUCCESS + : experiment.status, subRows: experiment.subRows?.map(checkpoint => ({ ...checkpoint, - running: false + status: ExperimentStatus.SUCCESS })) })) })) @@ -79,10 +85,12 @@ const noRunningExperimentsNoCheckpoints = { hasCheckpoints: false, rows: rowsFixture.map(row => ({ ...row, - running: false, + status: ExperimentStatus.SUCCESS, subRows: row.subRows?.map(experiment => ({ ...experiment, - running: false, + status: isRunning(experiment.status) + ? ExperimentStatus.SUCCESS + : experiment.status, subRows: [] })) })) diff --git a/webview/src/test/tableDataFixture.ts b/webview/src/test/tableDataFixture.ts index 2b6cc33a7a..a67bf63aeb 100644 --- a/webview/src/test/tableDataFixture.ts +++ b/webview/src/test/tableDataFixture.ts @@ -1,5 +1,9 @@ import { copyOriginalColors } from 'dvc/src/experiments/model/status/colors' -import { Row, TableData } from 'dvc/src/experiments/webview/contract' +import { + ExperimentStatus, + Row, + TableData +} from 'dvc/src/experiments/webview/contract' const matchAndTransform = ( rows: Row[], @@ -41,12 +45,16 @@ export const transformRows = ( } as TableData } -export const setRowPropertyAsTrue = - (prop: keyof Row) => (fixture: TableData, labelOrIds: string[]) => { - return transformRows(fixture, labelOrIds, row => ({ ...row, [prop]: true })) +const setRowProperty = + (prop: keyof Row, value: unknown) => + (fixture: TableData, labelOrIds: string[]) => { + return transformRows(fixture, labelOrIds, row => ({ + ...row, + [prop]: value + })) } -export const setExperimentsAsStarred = setRowPropertyAsTrue('starred') +export const setExperimentsAsStarred = setRowProperty('starred', true) export const setExperimentsAsSelected = ( fixture: TableData, @@ -66,4 +74,7 @@ export const setExperimentsAsSelected = ( selected: true })) } -export const setExperimentsAsRunning = setRowPropertyAsTrue('running') +export const setExperimentsAsRunning = setRowProperty( + 'status', + ExperimentStatus.RUNNING +) diff --git a/yarn.lock b/yarn.lock index e1c769bfbc..29b8d08ffd 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4131,10 +4131,10 @@ resolved "https://registry.yarnpkg.com/@types/node/-/node-17.0.21.tgz#864b987c0c68d07b4345845c3e63b75edd143644" integrity sha512-DBZCJbhII3r90XbQxI8Y9IjjiiOGlZ0Hr32omXIZvwwZ7p4DMMXGrKXVyPfuoBOri9XNtL0UK69jYIBIsRX3QQ== -"@types/node@16.11.63": - version "16.11.63" - resolved "https://registry.yarnpkg.com/@types/node/-/node-16.11.63.tgz#af57f6d2c3fb17a571230d527003dd734a9109b2" - integrity sha512-3OxnrEQLBz8EIIaHpg3CibmTAEGkDBcHY4fL5cnBwg2vd2yvHrUDGWxK+MlYPeXWWIoJJW79dGtU+oeBr6166Q== +"@types/node@16.11.64": + version "16.11.64" + resolved "https://registry.yarnpkg.com/@types/node/-/node-16.11.64.tgz#9171f327298b619e2c52238b120c19056415d820" + integrity sha512-z5hPTlVFzNwtJ2LNozTpJcD1Cu44c4LNuzaq1mwxmiHWQh2ULdR6Vjwo1UGldzRpzL0yUEdZddnfqGW2G70z6Q== "@types/node@^14.0.10 || ^16.0.0", "@types/node@^14.14.20 || ^16.0.0": version "16.11.39" @@ -4499,14 +4499,14 @@ dependencies: "@types/node" "*" -"@typescript-eslint/eslint-plugin@5.38.1": - version "5.38.1" - resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.38.1.tgz#9f05d42fa8fb9f62304cc2f5c2805e03c01c2620" - integrity sha512-ky7EFzPhqz3XlhS7vPOoMDaQnQMn+9o5ICR9CPr/6bw8HrFkzhMSxuA3gRfiJVvs7geYrSeawGJjZoZQKCOglQ== +"@typescript-eslint/eslint-plugin@5.39.0": + version "5.39.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.39.0.tgz#778b2d9e7f293502c7feeea6c74dca8eb3e67511" + integrity sha512-xVfKOkBm5iWMNGKQ2fwX5GVgBuHmZBO1tCRwXmY5oAIsPscfwm2UADDuNB8ZVYCtpQvJK4xpjrK7jEhcJ0zY9A== dependencies: - "@typescript-eslint/scope-manager" "5.38.1" - "@typescript-eslint/type-utils" "5.38.1" - "@typescript-eslint/utils" "5.38.1" + "@typescript-eslint/scope-manager" "5.39.0" + "@typescript-eslint/type-utils" "5.39.0" + "@typescript-eslint/utils" "5.39.0" debug "^4.3.4" ignore "^5.2.0" regexpp "^3.2.0" @@ -4520,14 +4520,14 @@ dependencies: "@typescript-eslint/utils" "5.25.0" -"@typescript-eslint/parser@5.38.1": - version "5.38.1" - resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-5.38.1.tgz#c577f429f2c32071b92dff4af4f5fbbbd2414bd0" - integrity sha512-LDqxZBVFFQnQRz9rUZJhLmox+Ep5kdUmLatLQnCRR6523YV+XhRjfYzStQ4MheFA8kMAfUlclHSbu+RKdRwQKw== +"@typescript-eslint/parser@5.39.0": + version "5.39.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-5.39.0.tgz#93fa0bc980a3a501e081824f6097f7ca30aaa22b" + integrity sha512-PhxLjrZnHShe431sBAGHaNe6BDdxAASDySgsBCGxcBecVCi8NQWxQZMcizNA4g0pN51bBAn/FUfkWG3SDVcGlA== dependencies: - "@typescript-eslint/scope-manager" "5.38.1" - "@typescript-eslint/types" "5.38.1" - "@typescript-eslint/typescript-estree" "5.38.1" + "@typescript-eslint/scope-manager" "5.39.0" + "@typescript-eslint/types" "5.39.0" + "@typescript-eslint/typescript-estree" "5.39.0" debug "^4.3.4" "@typescript-eslint/scope-manager@5.17.0": @@ -4546,21 +4546,21 @@ "@typescript-eslint/types" "5.25.0" "@typescript-eslint/visitor-keys" "5.25.0" -"@typescript-eslint/scope-manager@5.38.1": - version "5.38.1" - resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-5.38.1.tgz#f87b289ef8819b47189351814ad183e8801d5764" - integrity sha512-BfRDq5RidVU3RbqApKmS7RFMtkyWMM50qWnDAkKgQiezRtLKsoyRKIvz1Ok5ilRWeD9IuHvaidaLxvGx/2eqTQ== +"@typescript-eslint/scope-manager@5.39.0": + version "5.39.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-5.39.0.tgz#873e1465afa3d6c78d8ed2da68aed266a08008d0" + integrity sha512-/I13vAqmG3dyqMVSZPjsbuNQlYS082Y7OMkwhCfLXYsmlI0ca4nkL7wJ/4gjX70LD4P8Hnw1JywUVVAwepURBw== dependencies: - "@typescript-eslint/types" "5.38.1" - "@typescript-eslint/visitor-keys" "5.38.1" + "@typescript-eslint/types" "5.39.0" + "@typescript-eslint/visitor-keys" "5.39.0" -"@typescript-eslint/type-utils@5.38.1": - version "5.38.1" - resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-5.38.1.tgz#7f038fcfcc4ade4ea76c7c69b2aa25e6b261f4c1" - integrity sha512-UU3j43TM66gYtzo15ivK2ZFoDFKKP0k03MItzLdq0zV92CeGCXRfXlfQX5ILdd4/DSpHkSjIgLLLh1NtkOJOAw== +"@typescript-eslint/type-utils@5.39.0": + version "5.39.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-5.39.0.tgz#0a8c00f95dce4335832ad2dc6bc431c14e32a0a6" + integrity sha512-KJHJkOothljQWzR3t/GunL0TPKY+fGJtnpl+pX+sJ0YiKTz3q2Zr87SGTmFqsCMFrLt5E0+o+S6eQY0FAXj9uA== dependencies: - "@typescript-eslint/typescript-estree" "5.38.1" - "@typescript-eslint/utils" "5.38.1" + "@typescript-eslint/typescript-estree" "5.39.0" + "@typescript-eslint/utils" "5.39.0" debug "^4.3.4" tsutils "^3.21.0" @@ -4574,10 +4574,10 @@ resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-5.25.0.tgz#dee51b1855788b24a2eceeae54e4adb89b088dd8" integrity sha512-7fWqfxr0KNHj75PFqlGX24gWjdV/FDBABXL5dyvBOWHpACGyveok8Uj4ipPX/1fGU63fBkzSIycEje4XsOxUFA== -"@typescript-eslint/types@5.38.1": - version "5.38.1" - resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-5.38.1.tgz#74f9d6dcb8dc7c58c51e9fbc6653ded39e2e225c" - integrity sha512-QTW1iHq1Tffp9lNfbfPm4WJabbvpyaehQ0SrvVK2yfV79SytD9XDVxqiPvdrv2LK7DGSFo91TB2FgWanbJAZXg== +"@typescript-eslint/types@5.39.0": + version "5.39.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-5.39.0.tgz#f4e9f207ebb4579fd854b25c0bf64433bb5ed78d" + integrity sha512-gQMZrnfEBFXK38hYqt8Lkwt8f4U6yq+2H5VDSgP/qiTzC8Nw8JO3OuSUOQ2qW37S/dlwdkHDntkZM6SQhKyPhw== "@typescript-eslint/typescript-estree@5.17.0": version "5.17.0" @@ -4605,13 +4605,13 @@ semver "^7.3.7" tsutils "^3.21.0" -"@typescript-eslint/typescript-estree@5.38.1": - version "5.38.1" - resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-5.38.1.tgz#657d858d5d6087f96b638ee383ee1cff52605a1e" - integrity sha512-99b5e/Enoe8fKMLdSuwrfH/C0EIbpUWmeEKHmQlGZb8msY33qn1KlkFww0z26o5Omx7EVjzVDCWEfrfCDHfE7g== +"@typescript-eslint/typescript-estree@5.39.0": + version "5.39.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-5.39.0.tgz#c0316aa04a1a1f4f7f9498e3c13ef1d3dc4cf88b" + integrity sha512-qLFQP0f398sdnogJoLtd43pUgB18Q50QSA+BTE5h3sUxySzbWDpTSdgt4UyxNSozY/oDK2ta6HVAzvGgq8JYnA== dependencies: - "@typescript-eslint/types" "5.38.1" - "@typescript-eslint/visitor-keys" "5.38.1" + "@typescript-eslint/types" "5.39.0" + "@typescript-eslint/visitor-keys" "5.39.0" debug "^4.3.4" globby "^11.1.0" is-glob "^4.0.3" @@ -4630,15 +4630,15 @@ eslint-scope "^5.1.1" eslint-utils "^3.0.0" -"@typescript-eslint/utils@5.38.1": - version "5.38.1" - resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-5.38.1.tgz#e3ac37d7b33d1362bb5adf4acdbe00372fb813ef" - integrity sha512-oIuUiVxPBsndrN81oP8tXnFa/+EcZ03qLqPDfSZ5xIJVm7A9V0rlkQwwBOAGtrdN70ZKDlKv+l1BeT4eSFxwXA== +"@typescript-eslint/utils@5.39.0": + version "5.39.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-5.39.0.tgz#b7063cca1dcf08d1d21b0d91db491161ad0be110" + integrity sha512-+DnY5jkpOpgj+EBtYPyHRjXampJfC0yUZZzfzLuUWVZvCuKqSdJVC8UhdWipIw7VKNTfwfAPiOWzYkAwuIhiAg== dependencies: "@types/json-schema" "^7.0.9" - "@typescript-eslint/scope-manager" "5.38.1" - "@typescript-eslint/types" "5.38.1" - "@typescript-eslint/typescript-estree" "5.38.1" + "@typescript-eslint/scope-manager" "5.39.0" + "@typescript-eslint/types" "5.39.0" + "@typescript-eslint/typescript-estree" "5.39.0" eslint-scope "^5.1.1" eslint-utils "^3.0.0" @@ -4670,12 +4670,12 @@ "@typescript-eslint/types" "5.25.0" eslint-visitor-keys "^3.3.0" -"@typescript-eslint/visitor-keys@5.38.1": - version "5.38.1" - resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-5.38.1.tgz#508071bfc6b96d194c0afe6a65ad47029059edbc" - integrity sha512-bSHr1rRxXt54+j2n4k54p4fj8AHJ49VDWtjpImOpzQj4qjAiOpPni+V1Tyajh19Api1i844F757cur8wH3YvOA== +"@typescript-eslint/visitor-keys@5.39.0": + version "5.39.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-5.39.0.tgz#8f41f7d241b47257b081ddba5d3ce80deaae61e2" + integrity sha512-yyE3RPwOG+XJBLrhvsxAidUgybJVQ/hG8BhiJo0k8JSAYfk/CshVcxf0HwP4Jt7WZZ6vLmxdo1p6EyN3tzFTkg== dependencies: - "@typescript-eslint/types" "5.38.1" + "@typescript-eslint/types" "5.39.0" eslint-visitor-keys "^3.3.0" "@ungap/promise-all-settled@1.1.2": @@ -8691,10 +8691,10 @@ eslint-plugin-import@2.26.0: resolve "^1.22.0" tsconfig-paths "^3.14.1" -eslint-plugin-jest@27.0.4: - version "27.0.4" - resolved "https://registry.yarnpkg.com/eslint-plugin-jest/-/eslint-plugin-jest-27.0.4.tgz#ab9c7b3f48bfade4762c24c415a5d9bbc0174a61" - integrity sha512-BuvY78pHMpMJ6Cio7sKg6jrqEcnRYPUc4Nlihku4vKx3FjlmMINSX4vcYokZIe+8TKcyr1aI5Kq7vYwgJNdQSA== +eslint-plugin-jest@27.1.0: + version "27.1.0" + resolved "https://registry.yarnpkg.com/eslint-plugin-jest/-/eslint-plugin-jest-27.1.0.tgz#f6ade75b721aa2b89e3726cb35f42f6623284ff2" + integrity sha512-sqojX5GKzQ8+PScF9rJ7dRMtu0NEIWsaDMLwRRvVE28mnWctZe5VAti394Nmut11vPwgxck9XnDmmjx/U9NowQ== dependencies: "@typescript-eslint/utils" "^5.10.0" @@ -16165,10 +16165,10 @@ sinon-chai@3.7.0: resolved "https://registry.yarnpkg.com/sinon-chai/-/sinon-chai-3.7.0.tgz#cfb7dec1c50990ed18c153f1840721cf13139783" integrity sha512-mf5NURdUaSdnatJx3uhoBOrY9dtL19fiOtAdT1Azxg3+lNJFiuN0uzaU3xX1LeAfL17kHQhTAJgpsfhbMJMY2g== -sinon@14.0.0: - version "14.0.0" - resolved "https://registry.yarnpkg.com/sinon/-/sinon-14.0.0.tgz#203731c116d3a2d58dc4e3cbe1f443ba9382a031" - integrity sha512-ugA6BFmE+WrJdh0owRZHToLd32Uw3Lxq6E6LtNRU+xTVBefx632h03Q7apXWRsRdZAJ41LB8aUfn2+O4jsDNMw== +sinon@14.0.1: + version "14.0.1" + resolved "https://registry.yarnpkg.com/sinon/-/sinon-14.0.1.tgz#9f02e13ad86b695c0c554525e3bf7f8245b31a9c" + integrity sha512-JhJ0jCiyBWVAHDS+YSjgEbDn7Wgz9iIjA1/RK+eseJN0vAAWIWiXBdrnb92ELPyjsfreCYntD1ORtLSfIrlvSQ== dependencies: "@sinonjs/commons" "^1.8.3" "@sinonjs/fake-timers" "^9.1.2"