From a203bb753d7dd93ccade9d5a4459c5cb5b3241f1 Mon Sep 17 00:00:00 2001 From: Wasim Lorgat Date: Wed, 27 Nov 2024 19:01:23 +0200 Subject: [PATCH 01/11] Show a more helpful error message when a restart is required for `%view` (#5536) Addresses #5535. --- .../positron_ipykernel/data_explorer.py | 35 +++++++++++++++++-- .../positron_ipykernel/positron_ipkernel.py | 3 ++ .../tests/test_data_explorer.py | 28 ++++++++++++++- .../positron_ipykernel/third_party.py | 31 +++++++++------- 4 files changed, 81 insertions(+), 16 deletions(-) diff --git a/extensions/positron-python/python_files/positron/positron_ipykernel/data_explorer.py b/extensions/positron-python/python_files/positron/positron_ipykernel/data_explorer.py index d7dbb45e54c..fe6d88b2aff 100644 --- a/extensions/positron-python/python_files/positron/positron_ipykernel/data_explorer.py +++ b/extensions/positron-python/python_files/positron/positron_ipykernel/data_explorer.py @@ -23,6 +23,7 @@ ) import comm +from IPython.core.error import UsageError from .access_keys import decode_access_key from .data_explorer_comm import ( @@ -96,7 +97,14 @@ TextSearchType, ) from .positron_comm import CommMessage, PositronComm -from .third_party import np_, pd_, pl_ +from .third_party import ( + RestartRequiredError, + import_pandas, + import_polars, + np_, + pd_, + pl_, +) from .utils import BackgroundJobQueue, guid if TYPE_CHECKING: @@ -312,6 +320,7 @@ def _match_text_search(params: FilterTextSearch): def matches(x): return term in x.lower() + else: def matches(x): @@ -2581,11 +2590,31 @@ class PyArrowView(DataExplorerTableView): def _is_pandas(table): - return pd_ is not None and isinstance(table, (pd_.DataFrame, pd_.Series)) + pandas = import_pandas() + if pandas is not None and isinstance(table, (pandas.DataFrame, pandas.Series)): + # If pandas was installed after the kernel was started, pd_ will still be None. + # Raise an error to inform the user to restart the kernel. + if pd_ is None: + raise RestartRequiredError( + "Pandas was installed after the session started. Please restart the session to " + + "view the table in the Data Explorer." + ) + return True + return False def _is_polars(table): - return pl_ is not None and isinstance(table, (pl_.DataFrame, pl_.Series)) + polars = import_polars() + if polars is not None and isinstance(table, (polars.DataFrame, polars.Series)): + # If polars was installed after the kernel was started, pl_ will still be None. + # Raise an error to inform the user to restart the kernel. + if pl_ is None: + raise RestartRequiredError( + "Polars was installed after the session started. Please restart the session to " + + "view the table." + ) + return True + return False def _get_table_view( diff --git a/extensions/positron-python/python_files/positron/positron_ipykernel/positron_ipkernel.py b/extensions/positron-python/python_files/positron/positron_ipykernel/positron_ipkernel.py index 79358845ce1..6be68424b16 100644 --- a/extensions/positron-python/python_files/positron/positron_ipykernel/positron_ipkernel.py +++ b/extensions/positron-python/python_files/positron/positron_ipykernel/positron_ipkernel.py @@ -37,6 +37,7 @@ from .patch.holoviews import set_holoviews_extension from .plots import PlotsService from .session_mode import SessionMode +from .third_party import RestartRequiredError from .ui import UiService from .utils import BackgroundJobQueue, JsonRecord, get_qualname from .variables import VariablesService @@ -168,6 +169,8 @@ def view(self, line: str) -> None: ) except TypeError: raise UsageError(f"cannot view object of type '{get_qualname(obj)}'") + except RestartRequiredError as error: + raise UsageError(*error.args) @magic_arguments.magic_arguments() @magic_arguments.argument( diff --git a/extensions/positron-python/python_files/positron/positron_ipykernel/tests/test_data_explorer.py b/extensions/positron-python/python_files/positron/positron_ipykernel/tests/test_data_explorer.py index afa1ca15b9a..1c3ec946e31 100644 --- a/extensions/positron-python/python_files/positron/positron_ipykernel/tests/test_data_explorer.py +++ b/extensions/positron-python/python_files/positron/positron_ipykernel/tests/test_data_explorer.py @@ -11,7 +11,7 @@ from datetime import datetime from decimal import Decimal from io import StringIO -from typing import Any, Dict, List, Optional, Type, cast +from typing import Any, Dict, List, Optional, Type, Union, cast import numpy as np import pandas as pd @@ -19,6 +19,7 @@ import pytest import pytz +from .. import data_explorer from .._vendor.pydantic import BaseModel from ..access_keys import encode_access_key from ..data_explorer import ( @@ -49,6 +50,7 @@ RowFilterTypeSupportStatus, SupportStatus, ) +from ..third_party import RestartRequiredError from ..utils import guid from .conftest import DummyComm, PositronShell from .test_variables import BIG_ARRAY_LENGTH @@ -295,6 +297,30 @@ def test_register_table_with_variable_path(de_service: DataExplorerService): assert table_view.state.name == title +@pytest.mark.parametrize( + ("table", "import_name", "title"), + [(pd.DataFrame({}), "pd_", "Pandas"), (pl.DataFrame({}), "pl_", "Polars")], +) +def test_register_table_after_installing_dependency( + table: Union[pd.DataFrame, pl.DataFrame], + import_name: str, + title: str, + de_service: DataExplorerService, + monkeypatch, +): + # Patch the module (e.g. third_party.pd_) to None. Since these packages are really is installed + # during tests, this simulates the case where the user installs the package after the kernel + # starts, therefore the third_party attribute (e.g. pd_) is None but the corresponding import + # function (third_party.import_pandas()) returns the module. + # See https://github.com/posit-dev/positron/issues/5535. + monkeypatch.setattr(data_explorer, import_name, None) + + with pytest.raises( + RestartRequiredError, match=f"^{title} was installed after the session started." + ): + de_service.register_table(table, "test_table") + + def test_shutdown(de_service: DataExplorerService): df = pd.DataFrame({"a": [1, 2, 3, 4, 5]}) de_service.register_table(df, "t1", comm_id=guid()) diff --git a/extensions/positron-python/python_files/positron/positron_ipykernel/third_party.py b/extensions/positron-python/python_files/positron/positron_ipykernel/third_party.py index 435e039493e..2ab82f3eb54 100644 --- a/extensions/positron-python/python_files/positron/positron_ipykernel/third_party.py +++ b/extensions/positron-python/python_files/positron/positron_ipykernel/third_party.py @@ -9,7 +9,13 @@ # checking. -def _get_numpy(): +class RestartRequiredError(Exception): + """Raised when a restart is required to load a third party package.""" + + pass + + +def import_numpy(): try: import numpy except ImportError: @@ -17,7 +23,7 @@ def _get_numpy(): return numpy -def _get_pandas(): +def import_pandas(): try: import pandas except ImportError: @@ -25,7 +31,7 @@ def _get_pandas(): return pandas -def _get_polars(): +def import_polars(): try: import polars except ImportError: @@ -33,7 +39,7 @@ def _get_polars(): return polars -def _get_torch(): +def import_torch(): try: import torch # type: ignore [reportMissingImports] for 3.12 except ImportError: @@ -41,7 +47,7 @@ def _get_torch(): return torch -def _get_pyarrow(): +def import_pyarrow(): try: import pyarrow # type: ignore [reportMissingImports] for 3.12 except ImportError: @@ -49,7 +55,7 @@ def _get_pyarrow(): return pyarrow -def _get_sqlalchemy(): +def import_sqlalchemy(): try: import sqlalchemy except ImportError: @@ -59,11 +65,12 @@ def _get_sqlalchemy(): # Currently, pyright only correctly infers the types below as `Optional` if we set their values # using function calls. -np_ = _get_numpy() -pa_ = _get_pyarrow() -pd_ = _get_pandas() -pl_ = _get_polars() -torch_ = _get_torch() -sqlalchemy_ = _get_sqlalchemy() +np_ = import_numpy() +pa_ = import_pyarrow() +pd_ = import_pandas() +pl_ = import_polars() +torch_ = import_torch() +sqlalchemy_ = import_sqlalchemy() + __all__ = ["np_", "pa_", "pd_", "pl_", "torch_", "sqlalchemy_"] From a4435508b321b01efbf2e0206299a60f2f8c7f90 Mon Sep 17 00:00:00 2001 From: Lionel Henry Date: Wed, 27 Nov 2024 18:37:04 +0100 Subject: [PATCH 02/11] Bump ark to 0.1.156 (#5543) ### Release Notes #### New Features - The variable pane now supports labels from the {haven} package (https://github.com/posit-dev/positron/issues/5327. - The variable pane has improved support for formulas (https://github.com/posit-dev/positron/issues/4119). #### Bug Fixes - Assignments in function calls (e.g. `list(x <- 1)`) are now detected by the missing symbol linter to avoid annoying false positive diagnostics (posit-dev/positron#3048). The downside is that this causes false negatives when the assignment happens in a call with local scope, e.g. in `local()` or `test_that()`. In these cases the nested assignments will incorrectly overlast the end of the call. We prefer to be overly permissive than overly cautious in these matters. - The following environment variables are now set in the same way that R does: - `R_SHARE_DIR` - `R_INCLUDE_DIR` - `R_DOC_DIR` This solves a number of problems in situations that depend on these variables being defined (https://github.com/posit-dev/positron/issues/3637). --- extensions/positron-r/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/extensions/positron-r/package.json b/extensions/positron-r/package.json index b8f9ca5d661..4eb29cd3300 100644 --- a/extensions/positron-r/package.json +++ b/extensions/positron-r/package.json @@ -667,7 +667,7 @@ }, "positron": { "binaryDependencies": { - "ark": "0.1.155" + "ark": "0.1.156" }, "minimumRVersion": "4.2.0", "minimumRenvVersion": "1.0.9" From 8c7b9a9c2650f7039e53ed6bed39bfaa31a8825a Mon Sep 17 00:00:00 2001 From: sharon Date: Wed, 27 Nov 2024 12:58:41 -0500 Subject: [PATCH 03/11] install python modules in Terminal when `python.installModulesInTerminal` enabled (#5529) - addresses https://github.com/posit-dev/positron/issues/5506 - adds a new command `python.installModulesInTerminal`, that, when enabled, forces python installations to occur in the Terminal - this option is disabled by default - passes an otherwise unused cancellation token to the terminal service, which makes it so that the terminal execution must complete before the promise resolves - This might mean that we can opt to use the Terminal (and pass a cancellation token) in the places where we have the comment ```ts // Using a process to install modules avoids using the terminal service, // which has issues waiting for the outcome of the install. ``` - https://github.com/posit-dev/positron/blob/54cbc06c099bc8910011bc31c3586fb1882d2fd2/extensions/positron-python/src/client/common/terminal/types.ts#L44 ### QA Notes When `python.installModulesInTerminal` is enabled, any flow that passes through the `installModule()` method should run the install command in a Terminal visible to the user. When the option is not enabled, python module installations should occur in the background, invisible to the user. #### Project Wizard 1. Create a Python or Jupyter project with Venv 2. One of the new project initialization tasks should kick off the `ipykernel` install in a Terminal #### Command Prompt 1. Run the command `Python: Create Environment` 2. Start the newly created interpreter 3. Accept the `ipykernel` install 4. See `ipykernel` install in a Terminal #### Select interpreter that does not have `ipykernel` 1. Select an interpreter from the Interpreter Dropdown or use the `Python: Select Interpreter` command that _does not_ already have `ipykernel` installed 2. Accept the `ipykernel` install 3. See `ipykernel` install in a Terminal --- extensions/positron-python/package.json | 6 +++ extensions/positron-python/package.nls.json | 1 + .../common/installer/moduleInstaller.ts | 39 ++++++++++++++++++- .../installer/moduleInstaller.unit.test.ts | 9 +++++ 4 files changed, 54 insertions(+), 1 deletion(-) diff --git a/extensions/positron-python/package.json b/extensions/positron-python/package.json index a9118acbb16..00550c3b201 100644 --- a/extensions/positron-python/package.json +++ b/extensions/positron-python/package.json @@ -837,6 +837,12 @@ "description": "%python.venvPath.description%", "scope": "machine", "type": "string" + }, + "python.installModulesInTerminal": { + "default": false, + "markdownDescription": "%python.installModulesInTerminal.description%", + "scope": "resource", + "type": "boolean" } }, "title": "Python", diff --git a/extensions/positron-python/package.nls.json b/extensions/positron-python/package.nls.json index d6f238d00bc..1bfcdd7504d 100644 --- a/extensions/positron-python/package.nls.json +++ b/extensions/positron-python/package.nls.json @@ -104,6 +104,7 @@ "python.testing.unittestEnabled.description": "Enable testing using unittest.", "python.venvFolders.description": "Folders in your home directory to look into for virtual environments (supports pyenv, direnv and virtualenvwrapper by default).", "python.venvPath.description": "Path to folder with a list of Virtual Environments (e.g. ~/.pyenv, ~/Envs, ~/.virtualenvs).", + "python.installModulesInTerminal.description": "Whether to install Python modules (such as `ipykernel`) in the Terminal, instead of in a background process. Installing modules in the Terminal allows you to see the output of the installation command.", "walkthrough.pythonWelcome.title": "Get Started with Python Development", "walkthrough.pythonWelcome.description": "Your first steps to set up a Python project with all the powerful tools and features that the Python extension has to offer!", "walkthrough.step.python.createPythonFile.title": "Create a Python file", diff --git a/extensions/positron-python/src/client/common/installer/moduleInstaller.ts b/extensions/positron-python/src/client/common/installer/moduleInstaller.ts index a757e4683ae..dce30ab9d37 100644 --- a/extensions/positron-python/src/client/common/installer/moduleInstaller.ts +++ b/extensions/positron-python/src/client/common/installer/moduleInstaller.ts @@ -1,6 +1,11 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. +// --- Start Positron --- +/* eslint-disable max-classes-per-file, import/no-duplicates */ +import { CancellationTokenSource } from 'vscode'; +// --- End Positron --- + import { injectable } from 'inversify'; import * as path from 'path'; import { CancellationToken, l10n, ProgressLocation, ProgressOptions } from 'vscode'; @@ -22,6 +27,8 @@ import { ProductNames } from './productNames'; import { IModuleInstaller, InstallOptions, InterpreterUri, ModuleInstallFlags } from './types'; // --- Start Positron --- +// eslint-disable-next-line import/newline-after-import +import { IWorkspaceService } from '../application/types'; class ExternallyManagedEnvironmentError extends Error {} // --- End Positron --- @@ -44,7 +51,9 @@ export abstract class ModuleInstaller implements IModuleInstaller { flags?: ModuleInstallFlags, options?: InstallOptions, ): Promise { - const shouldExecuteInTerminal = !options?.installAsProcess; + // --- Start Positron --- + const shouldExecuteInTerminal = this.installModulesInTerminal() || !options?.installAsProcess; + // --- End Positron --- const name = typeof productOrModuleName === 'string' ? productOrModuleName @@ -249,6 +258,18 @@ export abstract class ModuleInstaller implements IModuleInstaller { .get(ITerminalServiceFactory) .getTerminalService(options); + // --- Start Positron --- + // When running with the `python.installModulesInTerminal` setting enabled, we want to + // ensure that the terminal command is fully executed before returning. Otherwise, the + // calling code of the install will not be able to tell when the installation is complete. + if (this.installModulesInTerminal()) { + // Ensure we pass a cancellation token so that we await the full terminal command + // execution before returning. + const cancelToken = token ?? new CancellationTokenSource().token; + await terminalService.sendCommand(command, args, token ?? cancelToken); + return; + } + // --- End Positron --- terminalService.sendCommand(command, args, token); } else { const processServiceFactory = this.serviceContainer.get(IProcessServiceFactory); @@ -278,6 +299,22 @@ export abstract class ModuleInstaller implements IModuleInstaller { // --- End Positron --- } } + + // --- Start Positron --- + /** + * Check if the user has enabled the setting to install modules in the terminal. + * + * `python.installModulesInTerminal` is a setting that allows the user to force modules to be + * installed in the Terminal. Usually, such installations occur in the background. However, + * for debugging, it can be helpful to see the Terminal output of the installation process. + * @returns `true` if the user has enabled the setting to install modules in the Terminal, + * `false` if the user has disabled the setting, and `undefined` if the setting is not found. + */ + private installModulesInTerminal(): boolean | undefined { + const workspaceService = this.serviceContainer.get(IWorkspaceService); + return workspaceService.getConfiguration('python').get('installModulesInTerminal'); + } + // --- End Positron --- } export function translateProductToModule(product: Product): string { diff --git a/extensions/positron-python/src/test/common/installer/moduleInstaller.unit.test.ts b/extensions/positron-python/src/test/common/installer/moduleInstaller.unit.test.ts index e2d8b22d4cc..1f8085312c0 100644 --- a/extensions/positron-python/src/test/common/installer/moduleInstaller.unit.test.ts +++ b/extensions/positron-python/src/test/common/installer/moduleInstaller.unit.test.ts @@ -282,6 +282,15 @@ suite('Module Installer', () => { workspaceService .setup((w) => w.getConfiguration(TypeMoq.It.isValue('http'))) .returns(() => http.object); + // --- Start Positron --- + const pythonConfig = TypeMoq.Mock.ofType(); + pythonConfig + .setup((p) => p.get(TypeMoq.It.isValue('installModulesInTerminal'), TypeMoq.It.isAny())) + .returns(() => false); + workspaceService + .setup((w) => w.getConfiguration(TypeMoq.It.isValue('python'))) + .returns(() => pythonConfig.object); + // --- End Positron --- installer = new InstallerClass(serviceContainer.object); }); teardown(() => { From 37a78a1bc324ae580abbed9c4e1adfa6c92e7bf8 Mon Sep 17 00:00:00 2001 From: Julia Silge Date: Wed, 27 Nov 2024 11:24:30 -0700 Subject: [PATCH 04/11] Use data science appropriate examples for Problems filter placeholder (#5528) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Addresses #5524 ### QA Notes We should now see more data science appropriate examples in the placeholder text for the Problems filter: ![Screenshot 2024-11-26 at 3 21 53 PM](https://github.com/user-attachments/assets/9d0acee9-1e84-478d-a837-12618048cbac) --- src/vs/workbench/contrib/markers/browser/messages.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/vs/workbench/contrib/markers/browser/messages.ts b/src/vs/workbench/contrib/markers/browser/messages.ts index 29c07828fc6..87d6bf5b044 100644 --- a/src/vs/workbench/contrib/markers/browser/messages.ts +++ b/src/vs/workbench/contrib/markers/browser/messages.ts @@ -37,7 +37,10 @@ export default class Messages { public static MARKERS_PANEL_ACTION_TOOLTIP_FILTER: string = nls.localize('markers.panel.action.filter', "Filter Problems"); public static MARKERS_PANEL_ACTION_TOOLTIP_QUICKFIX: string = nls.localize('markers.panel.action.quickfix', "Show fixes"); public static MARKERS_PANEL_FILTER_ARIA_LABEL: string = nls.localize('markers.panel.filter.ariaLabel', "Filter Problems"); - public static MARKERS_PANEL_FILTER_PLACEHOLDER: string = nls.localize('markers.panel.filter.placeholder', "Filter (e.g. text, **/*.ts, !**/node_modules/**)"); + // --- Start Positron --- + // public static MARKERS_PANEL_FILTER_PLACEHOLDER: string = nls.localize('markers.panel.filter.placeholder', "Filter (e.g. text, **/*.ts, !**/node_modules/**)"); + public static MARKERS_PANEL_FILTER_PLACEHOLDER: string = nls.localize('markers.panel.filter.placeholder', "Filter (e.g. text, **/*.py, *.R, !*.html, !data/**)"); + // --- End Positron public static MARKERS_PANEL_FILTER_ERRORS: string = nls.localize('markers.panel.filter.errors', "errors"); public static MARKERS_PANEL_FILTER_WARNINGS: string = nls.localize('markers.panel.filter.warnings', "warnings"); public static MARKERS_PANEL_FILTER_INFOS: string = nls.localize('markers.panel.filter.infos', "infos"); From 7a118ad0deee3d1037ae7a5462907146c271b420 Mon Sep 17 00:00:00 2001 From: Julia Silge Date: Wed, 27 Nov 2024 12:09:58 -0700 Subject: [PATCH 05/11] Add more logging for Python interpreter discovery (#5494) Addresses #5286 with some additional logging so we can start to understand why we sometimes see this failure in smoke tests ### QA Notes No changes for when the right Python interpreter _is_ found from the `interpreterService`. If we again see this in tests, we should see something like this in the Developer Tools console: > ERR Interpreter /Users/juliasilge/.pyenv/versions/3.11.7/envs/polars-testing/bin/python not found in available Python interpreters: /Users/juliasilge/.pyenv/versions/3.10.12/bin/python,/Users/juliasilge/.pyenv/versions/3.10.13/bin/python,/Users/juliasilge/.pyenv/versions/3.10.9/bin/python,/Users/juliasilge/.pyenv/versions/3.11.5/bin/python,/Users/juliasilge/.pyenv/versions/3.11.6/bin/python,/Users/juliasilge/.pyenv/versions/3.11.7/bin/python,/Users/juliasilge/.pyenv/versions/3.10.13/envs/bundle/bin/python,/Users/juliasilge/.pyenv/versions/3.10.12/envs/openai-testing/bin/python,/Users/juliasilge/.pyenv/versions/3.10.12/envs/positron/bin/python,/Users/juliasilge/.virtualenvs/r-tensorflow/bin/python,/usr/bin/python3,/Users/juliasilge/miniforge3/bin/python,/opt/homebrew/bin/python3.12,/Users/juliasilge/miniforge3/envs/emoji/bin/python,/Users/juliasilge/miniforge3/envs/keras-connect/bin/python,/Users/juliasilge/miniforge3/envs/my-first-pkg/bin/python,/Users/juliasilge/miniforge3/envs/pins-dev/bin/python,/Users/juliasilge/miniforge3/envs/test05-env/bin/python,/Users/juliasilge/miniforge3/envs/test06-env/bin/python,/Users/juliasilge/miniforge3/envs/tf_env/bin/python,/opt/homebrew/bin/python3.10,/opt/homebrew/bin/python3.11,/opt/homebrew/bin/python3.9,/Users/juliasilge/miniforge3/envs/another-test06-env/bin/python,/Users/juliasilge/.pyenv/versions/3.11.7/envs/positron-test-env/bin/python, Kind of hard to read in a real situation, but seems like the best way for us to find out what's going wrong occasionally in the tests is to see all the interpreters the service thinks is there, plus what is was looking for. --------- Signed-off-by: Julia Silge Co-authored-by: sharon --- extensions/positron-python/src/client/positron/session.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/extensions/positron-python/src/client/positron/session.ts b/extensions/positron-python/src/client/positron/session.ts index 54aaf94cd45..d71b656e18f 100644 --- a/extensions/positron-python/src/client/positron/session.ts +++ b/extensions/positron-python/src/client/positron/session.ts @@ -107,7 +107,10 @@ export class PythonRuntimeSession implements positron.LanguageRuntimeSession, vs const interpreter = interpreterService.getInterpreters().find((i) => i.id === extraData.pythonEnvironmentId); if (!interpreter) { - throw new Error(`Interpreter not found: ${extraData.pythonEnvironmentId}`); + const interpreterIds = interpreterService.getInterpreters().map((i) => `\n- ${i.id}`); + throw new Error( + `Interpreter ${extraData.pythonEnvironmentId} (path: ${extraData.pythonPath}) not found in available Python interpreters: ${interpreterIds}`, + ); } this.interpreter = interpreter; From c5ce275dc502f6b15433b271802cb33e1ba5ef68 Mon Sep 17 00:00:00 2001 From: Marie Idleman Date: Wed, 27 Nov 2024 13:52:00 -0600 Subject: [PATCH 06/11] e2e-test: browser in parallel & enable Currents (#5442) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ### Summary This PR enables the Currents dashboard integration to evaluate the tool’s usefulness. Additionally, the browser is now configured to run on multiple ports, allowing browser tests to execute in parallel. I also consolidated matrix runs into a single machine and enabled multiple threads/workers. This adjustment seems to align better with the current setup and is likely more cost-efficient. ### QA Notes I performed numerous test runs to monitor the potential for increased flakiness with parallel workers. The results are promising overall. While I observed some minor flakiness, these issues appear manageable and can be addressed with further adjustments. --- .../workflows/e2e-test-release-run-ubuntu.yml | 9 +- .github/workflows/positron-full-test.yml | 27 +- .../workflows/positron-merge-to-branch.yml | 7 +- .../workflows/positron-windows-nightly.yml | 12 +- package.json | 3 +- playwright.config.ts | 9 +- test/automation/src/playwrightBrowser.ts | 111 +++- .../src/positron/positronNotebooks.ts | 2 +- .../src/positron/positronVariables.ts | 1 - test/smoke/src/areas/positron/_test.setup.ts | 45 +- .../connections/connections-db.test.ts | 3 + .../new-project-r-jupyter.test.ts | 4 + test/smoke/src/test-runner/logger.ts | 2 +- yarn.lock | 614 +++++++++++++++++- 14 files changed, 775 insertions(+), 74 deletions(-) diff --git a/.github/workflows/e2e-test-release-run-ubuntu.yml b/.github/workflows/e2e-test-release-run-ubuntu.yml index 0daf3a0d6e5..bdf573b40b2 100644 --- a/.github/workflows/e2e-test-release-run-ubuntu.yml +++ b/.github/workflows/e2e-test-release-run-ubuntu.yml @@ -88,10 +88,15 @@ jobs: env: POSITRON_PY_VER_SEL: 3.10.12 POSITRON_R_VER_SEL: 4.4.0 - id: electron-smoke-tests + CURRENTS_RECORD_KEY: ${{ secrets.CURRENTS_RECORD_KEY }} + CURRENTS_CI_BUILD_ID: ${{ github.run_id }}-${{ github.run_attempt }} + COMMIT_INFO_MESSAGE: ${{ github.event.head_commit.message }} + PWTEST_BLOB_DO_NOT_REMOVE: 1 + CURRENTS_TAG: "electron,release,${{ inputs.e2e_grep }}" + id: electron-e2e-tests run: | export DISPLAY=:10 - BUILD=/usr/share/positron npx playwright test --project e2e-electron --workers 2 --grep="${{ env.E2E_GREP }}" + BUILD=/usr/share/positron npx playwright test --project e2e-electron --workers 3 --grep=${{ env.E2E_GREP }} - name: Upload Playwright Report to S3 if: ${{ !cancelled() }} diff --git a/.github/workflows/positron-full-test.yml b/.github/workflows/positron-full-test.yml index 8a7adf73ead..e38a8a01119 100644 --- a/.github/workflows/positron-full-test.yml +++ b/.github/workflows/positron-full-test.yml @@ -96,11 +96,6 @@ jobs: e2e-electron-tests: runs-on: ubuntu-latest-8x timeout-minutes: 60 - strategy: - fail-fast: false - matrix: - shardIndex: [1, 2] - shardTotal: [2] env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} POSITRON_BUILD_NUMBER: 0 # CI skips building releases @@ -131,14 +126,20 @@ jobs: env: POSITRON_PY_VER_SEL: 3.10.12 POSITRON_R_VER_SEL: 4.4.0 + CURRENTS_PROJECT_ID: ZOs5z2 + CURRENTS_RECORD_KEY: ${{ secrets.CURRENTS_RECORD_KEY }} + CURRENTS_CI_BUILD_ID: ${{ github.run_id }}-${{ github.run_attempt }} + COMMIT_INFO_MESSAGE: ${{ github.event.head_commit.message }} # only works on push events + PWTEST_BLOB_DO_NOT_REMOVE: 1 + CURRENTS_TAG: "electron" id: electron-tests - run: DISPLAY=:10 npx playwright test --project e2e-electron --workers 1 --shard=${{ matrix.shardIndex }}/${{ matrix.shardTotal }} + run: DISPLAY=:10 npx playwright test --project e2e-electron --workers 3 - name: Upload blob report if: ${{ !cancelled() }} uses: actions/upload-artifact@v4 with: - name: blob-report-electron-${{ matrix.shardIndex }} + name: blob-report-electron path: blob-report retention-days: 14 @@ -146,11 +147,11 @@ jobs: if: ${{ !cancelled() }} uses: actions/upload-artifact@v4 with: - name: junit-report-electron-${{ matrix.shardIndex }} + name: junit-report-electron path: test-results/junit.xml e2e-browser-tests: - runs-on: ubuntu-latest-4x + runs-on: ubuntu-latest-8x timeout-minutes: 50 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} @@ -188,8 +189,14 @@ jobs: env: POSITRON_PY_VER_SEL: 3.10.12 POSITRON_R_VER_SEL: 4.4.0 + CURRENTS_PROJECT_ID: ZOs5z2 + CURRENTS_RECORD_KEY: ${{ secrets.CURRENTS_RECORD_KEY }} + CURRENTS_CI_BUILD_ID: ${{ github.run_id }}-${{ github.run_attempt }} + COMMIT_INFO_MESSAGE: ${{ github.event.head_commit.message }} + PWTEST_BLOB_DO_NOT_REMOVE: 1 + CURRENTS_TAG: "chromium" id: browser-tests - run: DISPLAY=:10 npx playwright test --project e2e-browser --workers 1 + run: DISPLAY=:10 npx playwright test --project e2e-browser --workers 2 - name: Upload blob report if: ${{ !cancelled() }} diff --git a/.github/workflows/positron-merge-to-branch.yml b/.github/workflows/positron-merge-to-branch.yml index e02cee5e8f7..3cca8d5b85f 100644 --- a/.github/workflows/positron-merge-to-branch.yml +++ b/.github/workflows/positron-merge-to-branch.yml @@ -70,8 +70,13 @@ jobs: env: POSITRON_PY_VER_SEL: 3.10.12 POSITRON_R_VER_SEL: 4.4.0 + CURRENTS_RECORD_KEY: ${{ secrets.CURRENTS_RECORD_KEY }} + CURRENTS_CI_BUILD_ID: ${{ github.run_id }}-${{ github.run_attempt }} + COMMIT_INFO_MESSAGE: ${{ github.event.head_commit.message }} + PWTEST_BLOB_DO_NOT_REMOVE: 1 + CURRENTS_TAG: "electron,${{ inputs.e2e_grep }}" id: e2e-playwright-tests - run: DISPLAY=:10 npx playwright test --project e2e-electron --workers 2 --grep="${{ env.E2E_GREP }}" + run: DISPLAY=:10 npx playwright test --project e2e-electron --workers 2 --grep=${{ env.E2E_GREP }} - name: Upload Playwright Report to S3 if: ${{ success() || failure() }} diff --git a/.github/workflows/positron-windows-nightly.yml b/.github/workflows/positron-windows-nightly.yml index 7125dda251b..14ebba4bf73 100644 --- a/.github/workflows/positron-windows-nightly.yml +++ b/.github/workflows/positron-windows-nightly.yml @@ -102,15 +102,21 @@ jobs: env: POSITRON_PY_VER_SEL: 3.10.10 POSITRON_R_VER_SEL: 4.4.0 + CURRENTS_PROJECT_ID: ZOs5z2 + CURRENTS_RECORD_KEY: ${{ secrets.CURRENTS_RECORD_KEY }} + CURRENTS_CI_BUILD_ID: ${{ github.run_id }}-${{ github.run_attempt }} + COMMIT_INFO_MESSAGE: ${{ github.event.head_commit.message }} # only works on push events + PWTEST_BLOB_DO_NOT_REMOVE: 1 + CURRENTS_TAG: "electron,@win" if: ${{ !cancelled() }} id: e2e-win-electron-tests - run: npx playwright test --project e2e-electron --grep "@win" --workers 1 + run: npx playwright test --project e2e-electron --grep "@win" --workers 2 - name: Upload blob report if: ${{ !cancelled() }} uses: actions/upload-artifact@v4 with: - name: blob-report-electron-${{ matrix.shardIndex }} + name: blob-report-electron path: blob-report retention-days: 14 @@ -118,7 +124,7 @@ jobs: if: ${{ !cancelled() }} uses: actions/upload-artifact@v4 with: - name: junit-report-electron-${{ matrix.shardIndex }} + name: junit-report-electron path: test-results/junit.xml e2e-report: diff --git a/package.json b/package.json index 31fbcc5a0ce..8ce0522e5a3 100644 --- a/package.json +++ b/package.json @@ -46,7 +46,7 @@ "update-localization-extension": "node build/npm/update-localization-extension.js", "e2e": "yarn e2e-electron", "e2e-electron": "npx playwright test --project e2e-electron", - "e2e-browser": "npx playwright test --project e2e-browser --workers 1", + "e2e-browser": "npx playwright test --project e2e-browser", "e2e-pr": "npx playwright test --project e2e-electron --grep @pr", "e2e-win": "npx playwright test --project e2e-electron --grep @win", "e2e-failed": "npx playwright test --last-failed", @@ -129,6 +129,7 @@ "yazl": "^2.4.3" }, "devDependencies": { + "@currents/playwright": "^1.8.0", "@midleman/github-actions-reporter": "^1.9.5", "@playwright/test": "^1.49.0", "@swc/core": "1.3.62", diff --git a/playwright.config.ts b/playwright.config.ts index b2c47df13b1..bd54fdaf563 100644 --- a/playwright.config.ts +++ b/playwright.config.ts @@ -6,6 +6,7 @@ import { defineConfig } from '@playwright/test'; import { CustomTestOptions } from './test/smoke/src/areas/positron/_test.setup'; import type { GitHubActionOptions } from '@midleman/github-actions-reporter'; +import { currentsReporter } from '@currents/playwright'; /** * See https://playwright.dev/docs/test-configuration. @@ -37,7 +38,13 @@ export default defineConfig({ includeResults: ['fail', 'flaky'] }], ['junit', { outputFile: 'test-results/junit.xml' }], - ['list'], ['html'], ['blob'] + ['list'], ['html'], ['blob'], + currentsReporter({ + ciBuildId: process.env.CURRENTS_CI_BUILD_ID || Date.now().toString(), + recordKey: process.env.CURRENTS_RECORD_KEY || '', + projectId: 'ZOs5z2', + disableTitleTags: true, + }), ] : [ ['list'], diff --git a/test/automation/src/playwrightBrowser.ts b/test/automation/src/playwrightBrowser.ts index 9b455efc2a0..2719bb6b506 100644 --- a/test/automation/src/playwrightBrowser.ts +++ b/test/automation/src/playwrightBrowser.ts @@ -30,66 +30,115 @@ export async function launch(options: LaunchOptions): Promise<{ serverProcess: C }; } +// --- Start Positron --- +// Modified `launchServer` function to add support for multiple ports to enable parallel test +// execution of browser tests. Also added helper functions: `getServerArgs`, `resolveServerLocation`, +// and `startServer` to make this code easier to read. async function launchServer(options: LaunchOptions) { const { userDataDir, codePath, extensionsPath, logger, logsPath } = options; const serverLogsPath = join(logsPath, 'server'); const codeServerPath = codePath ?? process.env.VSCODE_REMOTE_SERVER_PATH; const agentFolder = userDataDir; + await measureAndLog(() => mkdirp(agentFolder), `mkdirp(${agentFolder})`, logger); const env = { VSCODE_REMOTE_SERVER_PATH: codeServerPath, - ...process.env + ...process.env, }; + const maxRetries = 10; + let serverProcess: ChildProcess | null = null; + let endpoint: string | undefined; + + for (let attempts = 0; attempts < maxRetries; attempts++) { + const currentPort = port++; + const args = getServerArgs(currentPort, extensionsPath, agentFolder, serverLogsPath, options.verbose); + const serverLocation = resolveServerLocation(codeServerPath, logger); + + logger.log(`Attempting to start server on port ${currentPort}`); + logger.log(`Command: '${serverLocation}' ${args.join(' ')}`); + + try { + serverProcess = await startServer(serverLocation, args, env, logger); + endpoint = await measureAndLog( + () => waitForEndpoint(serverProcess!, logger), + 'waitForEndpoint(serverProcess)', + logger + ); + + logger.log(`Server started successfully on port ${currentPort}`); + break; // Exit loop on success + } catch (error) { + if ((error as Error).message.includes('EADDRINUSE')) { + logger.log(`Port ${currentPort} is already in use. Retrying...`); + serverProcess?.kill(); + } else { + throw error; // Rethrow non-port-related errors + } + } + } + + if (!serverProcess || !endpoint) { + throw new Error('Failed to launch the server after multiple attempts.'); + } + + return { serverProcess, endpoint }; +} + +function getServerArgs( + port: number, + extensionsPath: string, + agentFolder: string, + logsPath: string, + verbose?: boolean +): string[] { const args = [ '--disable-telemetry', '--disable-workspace-trust', - `--port=${port++}`, + `--port=${port}`, '--enable-smoke-test-driver', `--extensions-dir=${extensionsPath}`, `--server-data-dir=${agentFolder}`, '--accept-server-license-terms', - `--logsPath=${serverLogsPath}`, - // --- Start Positron --- - `--connection-token`, - `dev-token` - // --- End Positron --- + `--logsPath=${logsPath}`, + '--connection-token', + 'dev-token', ]; - if (options.verbose) { + if (verbose) { args.push('--log=trace'); } - let serverLocation: string | undefined; + return args; +} + +function resolveServerLocation(codeServerPath: string | undefined, logger: Logger): string { if (codeServerPath) { const { serverApplicationName } = require(join(codeServerPath, 'product.json')); - serverLocation = join(codeServerPath, 'bin', `${serverApplicationName}${process.platform === 'win32' ? '.cmd' : ''}`); - - logger.log(`Starting built server from '${serverLocation}'`); - } else { - serverLocation = join(root, `scripts/code-server.${process.platform === 'win32' ? 'bat' : 'sh'}`); - - logger.log(`Starting server out of sources from '${serverLocation}'`); + const serverLocation = join(codeServerPath, 'bin', `${serverApplicationName}${process.platform === 'win32' ? '.cmd' : ''}`); + logger.log(`Using built server from '${serverLocation}'`); + return serverLocation; } - logger.log(`Storing log files into '${serverLogsPath}'`); - - logger.log(`Command line: '${serverLocation}' ${args.join(' ')}`); - const shell: boolean = (process.platform === 'win32'); - const serverProcess = spawn( - serverLocation, - args, - { env, shell } - ); - - logger.log(`Started server for browser smoke tests (pid: ${serverProcess.pid})`); + const scriptPath = join(root, `scripts/code-server.${process.platform === 'win32' ? 'bat' : 'sh'}`); + logger.log(`Using source server from '${scriptPath}'`); + return scriptPath; +} - return { - serverProcess, - endpoint: await measureAndLog(() => waitForEndpoint(serverProcess, logger), 'waitForEndpoint(serverProcess)', logger) - }; +async function startServer( + serverLocation: string, + args: string[], + env: NodeJS.ProcessEnv, + logger: Logger +): Promise { + logger.log(`Starting server: ${serverLocation}`); + const serverProcess = spawn(serverLocation, args, { env, shell: process.platform === 'win32' }); + logger.log(`Server started (pid: ${serverProcess.pid})`); + return serverProcess; } +// --- End Positron --- + async function launchBrowser(options: LaunchOptions, endpoint: string) { const { logger, workspacePath, tracing, headless } = options; diff --git a/test/automation/src/positron/positronNotebooks.ts b/test/automation/src/positron/positronNotebooks.ts index 7a9b4cf32cd..ae35f5f16f0 100644 --- a/test/automation/src/positron/positronNotebooks.ts +++ b/test/automation/src/positron/positronNotebooks.ts @@ -82,7 +82,7 @@ export class PositronNotebooks { } async assertCellOutput(text: string): Promise { - await expect(this.frameLocator.getByText(text)).toBeVisible(); + await expect(this.frameLocator.getByText(text)).toBeVisible({ timeout: 15000 }); } async closeNotebookWithoutSaving() { diff --git a/test/automation/src/positron/positronVariables.ts b/test/automation/src/positron/positronVariables.ts index f937255ff3d..1e91093d260 100644 --- a/test/automation/src/positron/positronVariables.ts +++ b/test/automation/src/positron/positronVariables.ts @@ -57,7 +57,6 @@ export class PositronVariables { } async doubleClickVariableRow(variableName: string) { - const desiredRow = await this.waitForVariableRow(variableName); await desiredRow.dblclick(); } diff --git a/test/smoke/src/areas/positron/_test.setup.ts b/test/smoke/src/areas/positron/_test.setup.ts index fc098c7a80f..fbb1951ff62 100644 --- a/test/smoke/src/areas/positron/_test.setup.ts +++ b/test/smoke/src/areas/positron/_test.setup.ts @@ -30,6 +30,7 @@ const TEMP_DIR = `temp-${randomUUID()}`; const ROOT_PATH = process.cwd(); const LOGS_ROOT_PATH = join(ROOT_PATH, 'test-logs'); let SPEC_NAME = ''; +let fixtureScreenshot: Buffer; export const test = base.extend({ suiteId: ['', { scope: 'worker', option: true }], @@ -78,17 +79,33 @@ export const test = base.extend({ await use(app); }, { scope: 'test', timeout: 60000 }], - app: [async ({ options, logsPath, logger }, use) => { + app: [async ({ options, logsPath }, use, workerInfo) => { const app = createApp(options); - await app.start(); - await use(app); + try { + await app.start(); + + await use(app); + } catch (error) { + // capture a screenshot on failure + const screenshotPath = path.join(logsPath, 'app-start-failure.png'); + try { + const page = app.code?.driver?.page; + if (page) { + fixtureScreenshot = await page.screenshot({ path: screenshotPath }); + } + } catch { + // ignore + } - await app.stop(); + throw error; // re-throw the error to ensure test failure + } finally { + await app.stop(); - // rename the temp logs dir to the spec name - const specLogsPath = path.join(path.dirname(logsPath), SPEC_NAME); - await moveAndOverwrite(logsPath, specLogsPath); + // rename the temp logs dir to the spec name (if available) + const specLogsPath = path.join(path.dirname(logsPath), SPEC_NAME || `worker-${workerInfo.workerIndex}`); + await moveAndOverwrite(logsPath, specLogsPath); + } }, { scope: 'worker', auto: true, timeout: 60000 }], interpreter: [async ({ app, page }, use) => { @@ -268,9 +285,17 @@ test.beforeAll(async ({ logger }, testInfo) => { }); test.afterAll(async function ({ logger }, testInfo) { - logger.log(''); - logger.log(`>>> Suite end: '${testInfo.titlePath[0] ?? 'unknown'}' <<<`); - logger.log(''); + try { + logger.log(''); + logger.log(`>>> Suite end: '${testInfo.titlePath[0] ?? 'unknown'}' <<<`); + logger.log(''); + } catch (error) { + // ignore + } + + if (fixtureScreenshot) { + await testInfo.attach('on-fixture-fail', { body: fixtureScreenshot, contentType: 'image/png' }); + } }); export { playwrightExpect as expect }; diff --git a/test/smoke/src/areas/positron/connections/connections-db.test.ts b/test/smoke/src/areas/positron/connections/connections-db.test.ts index 5dcf753d432..9982cca9abb 100644 --- a/test/smoke/src/areas/positron/connections/connections-db.test.ts +++ b/test/smoke/src/areas/positron/connections/connections-db.test.ts @@ -29,6 +29,9 @@ test.describe('SQLite DB Connection', { tag: ['@web', '@win', '@pr'] }, () => { await test.step('Open connections pane', async () => { await app.workbench.positronLayouts.enterLayout('fullSizedAuxBar'); + // there is a flake of the db connection not displaying in the connections pane after + // clicking the db icon. i want to see if waiting for a second will help + await app.code.driver.page.waitForTimeout(1000); await app.workbench.positronVariables.clickDatabaseIconForVariableRow('conn'); await app.workbench.positronConnections.connectIcon.click(); }); diff --git a/test/smoke/src/areas/positron/new-project-wizard/new-project-r-jupyter.test.ts b/test/smoke/src/areas/positron/new-project-wizard/new-project-r-jupyter.test.ts index 5d70e00471b..664040f33c9 100644 --- a/test/smoke/src/areas/positron/new-project-wizard/new-project-r-jupyter.test.ts +++ b/test/smoke/src/areas/positron/new-project-wizard/new-project-r-jupyter.test.ts @@ -12,6 +12,7 @@ test.use({ test.beforeEach(async function ({ app }) { await app.workbench.positronConsole.waitForReadyOrNoInterpreter(); + await app.workbench.positronLayouts.enterLayout("stacked"); }); test.describe('R - New Project Wizard', () => { @@ -28,6 +29,7 @@ test.describe('R - New Project Wizard', () => { await pw.navigate(ProjectWizardNavigateAction.NEXT); await pw.navigate(ProjectWizardNavigateAction.CREATE); await pw.currentOrNewWindowSelectionModal.currentWindowButton.click(); + await app.workbench.positronLayouts.enterLayout("fullSizedSidebar"); await expect(app.code.driver.page.getByRole('button', { name: `Explorer Section: ${defaultProjectName + projSuffix}` })).toBeVisible({ timeout: 15000 }); // NOTE: For completeness, we probably want to await app.workbench.positronConsole.waitForReady('>', 10000); // here, but it's timing out in CI, so it is not included for now. @@ -45,6 +47,7 @@ test.describe('R - New Project Wizard', () => { await pw.rConfigurationStep.renvCheckbox.click(); await pw.navigate(ProjectWizardNavigateAction.CREATE); await pw.currentOrNewWindowSelectionModal.currentWindowButton.click(); + await app.workbench.positronLayouts.enterLayout("fullSizedSidebar"); await expect(app.code.driver.page.getByRole('button', { name: `Explorer Section: ${defaultProjectName + projSuffix}` })).toBeVisible({ timeout: 15000 }); // Interact with the modal to install renv await app.workbench.positronPopups.installRenv(); @@ -144,6 +147,7 @@ test.describe('Jupyter - New Project Wizard', () => { await pw.navigate(ProjectWizardNavigateAction.CREATE); await pw.currentOrNewWindowSelectionModal.currentWindowButton.click(); await app.code.driver.wait(10000); + await app.workbench.positronLayouts.enterLayout("fullSizedSidebar"); await expect(app.code.driver.page.getByRole('button', { name: `Explorer Section: ${defaultProjectName + projSuffix}` })).toBeVisible({ timeout: 15000 }); // NOTE: For completeness, we probably want to await app.workbench.positronConsole.waitForReady('>>>', 10000); // here, but it's timing out in CI, so it is not included for now. diff --git a/test/smoke/src/test-runner/logger.ts b/test/smoke/src/test-runner/logger.ts index 31b44948838..6965ed03d53 100644 --- a/test/smoke/src/test-runner/logger.ts +++ b/test/smoke/src/test-runner/logger.ts @@ -58,7 +58,7 @@ function logToFile(logFilePath: string, message: string): void { } /** - * Logs the error to the test log file: logs/smoke-tests-electron//retry.log + * Logs the error to the test log file: logs/e2e-tests-electron//retry.log * * @param test mocha test * @param err error diff --git a/yarn.lock b/yarn.lock index 6c1825933a4..5a4f0b72417 100644 --- a/yarn.lock +++ b/yarn.lock @@ -421,6 +421,13 @@ dependencies: regenerator-runtime "^0.13.11" +"@babel/runtime@^7.15.4", "@babel/runtime@^7.21.0": + version "7.26.0" + resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.26.0.tgz#8600c2f595f277c60815256418b85356a65173c1" + integrity sha512-FDSOghenHTiToteC/QRlv2q3DhPZ/oOXTBoirfWNx1Cx3TMVcGWQtMMmQcSvb/JjpNeGzx8Pq/b4fKEJuWm1sw== + dependencies: + regenerator-runtime "^0.14.0" + "@babel/template@^7.18.10", "@babel/template@^7.18.6": version "7.18.10" resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.18.10.tgz#6f9134835970d1dbf0835c0d100c9f38de0c5e71" @@ -478,6 +485,11 @@ resolved "https://registry.yarnpkg.com/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz#75a2e8b51cb758a7553d6804a5932d7aace75c39" integrity sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw== +"@commander-js/extra-typings@^11.1.0": + version "11.1.0" + resolved "https://registry.yarnpkg.com/@commander-js/extra-typings/-/extra-typings-11.1.0.tgz#dd19fcb8cc6e33ede237fc1b7af96c70833d8f93" + integrity sha512-GuvZ38d23H+7Tz2C9DhzCepivsOsky03s5NI+KCy7ke1FNUvsJ2oO47scQ9YaGGhgjgNW5OYYNSADmbjcSoIhw== + "@cspotcode/source-map-support@^0.8.0": version "0.8.1" resolved "https://registry.yarnpkg.com/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz#00629c35a688e05a88b1cda684fb9d5e73f000a1" @@ -490,6 +502,57 @@ resolved "https://registry.yarnpkg.com/@csstools/selector-specificity/-/selector-specificity-3.0.1.tgz#d84597fbc0f897240c12fc0a31e492b036c70e40" integrity sha512-NPljRHkq4a14YzZ3YD406uaxh7s0g6eAq3L9aLOWywoqe8PkYamAvtsh7KNX6c++ihDrJ0RiU+/z7rGnhlZ5ww== +"@currents/commit-info@1.0.1-beta.0": + version "1.0.1-beta.0" + resolved "https://registry.yarnpkg.com/@currents/commit-info/-/commit-info-1.0.1-beta.0.tgz#c33112685c27896bba29064234fd2341ec9134e6" + integrity sha512-gkn8E3UC+F4/fCla7QAGMGgGPzxZUL9bU9+4I+KZf9PtCU3DdQCdy7a+er2eg4ewfUzZ2Ic1HcfnHuPkuLPKIw== + dependencies: + bluebird "3.5.5" + check-more-types "2.24.0" + debug "4.3.4" + execa "1.0.0" + lazy-ass "1.6.0" + ramda "0.26.1" + +"@currents/playwright@^1.8.0": + version "1.8.0" + resolved "https://registry.yarnpkg.com/@currents/playwright/-/playwright-1.8.0.tgz#4a7ff7f400e3ede3555317d1b8584d77b5a30e42" + integrity sha512-BM9prNzVf39TlM13ZCRRIHDY9r83wjbEgLra1tjA/40vUR6zXx48OSW3pTSapzk+26O9pFC1MKTn/kqK6MU0rQ== + dependencies: + "@babel/code-frame" "^7.18.6" + "@commander-js/extra-typings" "^11.1.0" + "@currents/commit-info" "1.0.1-beta.0" + async-retry "^1.3.3" + axios "^1.7.4" + axios-retry "^3.4.0" + c12 "^1.11.2" + chalk "^4.1.2" + commander "^11.1.0" + date-fns "^2.29.3" + debug "^4.3.7" + dotenv "^16.0.3" + execa "^7.2.0" + getos "^3.2.1" + https-proxy-agent "^7.0.5" + istanbul-lib-coverage "^3.2.2" + json-stringify-safe "^5.0.1" + lil-http-terminator "^1.2.3" + lodash "^4.17.21" + nanoid "^3.3.4" + object-sizeof "^2.6.5" + p-debounce "^2.1.0" + p-queue "6.6.2" + pino "^8.11.0" + pluralize "^8.0.0" + pretty-ms "^7.0.1" + proxy-from-env "^1.1.0" + source-map-support "^0.5.21" + stack-utils "^2.0.6" + tmp "^0.2.3" + tmp-promise "^3.0.3" + ts-pattern "^4.3.0" + ws "^8.18.0" + "@discoveryjs/json-ext@^0.5.0": version "0.5.3" resolved "https://registry.yarnpkg.com/@discoveryjs/json-ext/-/json-ext-0.5.3.tgz#90420f9f9c6d3987f176a19a7d8e764271a2f55d" @@ -2145,6 +2208,13 @@ abbrev@1: resolved "https://registry.yarnpkg.com/abbrev/-/abbrev-1.1.1.tgz#f8f2c887ad10bf67f634f005b6987fed3179aac8" integrity sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q== +abort-controller@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/abort-controller/-/abort-controller-3.0.0.tgz#eaf54d53b62bae4138e809ca225c8439a6efb392" + integrity sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg== + dependencies: + event-target-shim "^5.0.0" + accepts@^1.3.5: version "1.3.7" resolved "https://registry.yarnpkg.com/accepts/-/accepts-1.3.7.tgz#531bc726517a3b2b41f850021c6cc15eaab507cd" @@ -2183,6 +2253,11 @@ acorn@^6.0.7, acorn@^6.4.1: resolved "https://registry.yarnpkg.com/acorn/-/acorn-6.4.2.tgz#35866fd710528e92de10cf06016498e47e39e1e6" integrity sha512-XtGIhXwF8YM8bJhGxG5kXgjkEuNGLTkoYqVE+KMR+aspr4KGYmKYg7yUe3KghyQ9yheNwLnjmzh/7+gfDBmHCQ== +acorn@^8.14.0: + version "8.14.0" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.14.0.tgz#063e2c70cac5fb4f6467f0b11152e04c682795b0" + integrity sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA== + acorn@^8.4.1: version "8.4.1" resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.4.1.tgz#56c36251fc7cabc7096adc18f05afe814321a28c" @@ -2649,6 +2724,13 @@ async-each@^1.0.1: resolved "https://registry.yarnpkg.com/async-each/-/async-each-1.0.3.tgz#b727dbf87d7651602f06f4d4ac387f47d91b0cbf" integrity sha512-z/WhQ5FPySLdvREByI2vZiTWwCnF0moMJ1hK9YQwDTHKh6I7/uSckMetoRGb5UBZPC1z0jlw+n/XCgjeH7y1AQ== +async-retry@^1.3.3: + version "1.3.3" + resolved "https://registry.yarnpkg.com/async-retry/-/async-retry-1.3.3.tgz#0e7f36c04d8478e7a58bdbed80cedf977785f280" + integrity sha512-wfr/jstw9xNi/0teMHrRW7dsz3Lt5ARhYNZ2ewpadnhaIp5mbALhOAP+EAdsC7t4Z6wqsDVv9+W6gm1Dk9mEyw== + dependencies: + retry "0.13.1" + async-settle@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/async-settle/-/async-settle-1.0.0.tgz#1d0a914bb02575bec8a8f3a74e5080f72b2c0c6b" @@ -2656,6 +2738,11 @@ async-settle@^1.0.0: dependencies: async-done "^1.2.2" +async@^3.2.0: + version "3.2.6" + resolved "https://registry.yarnpkg.com/async/-/async-3.2.6.tgz#1b0728e14929d51b85b449b7f06e27c1145e38ce" + integrity sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA== + asynciterator.prototype@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/asynciterator.prototype/-/asynciterator.prototype-1.0.0.tgz#8c5df0514936cdd133604dfcc9d3fb93f09b2b62" @@ -2673,6 +2760,11 @@ atob@^2.1.2: resolved "https://registry.yarnpkg.com/atob/-/atob-2.1.2.tgz#6d9517eb9e030d2436666651e86bd9f6f13533c9" integrity sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg== +atomic-sleep@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/atomic-sleep/-/atomic-sleep-1.0.0.tgz#eb85b77a601fc932cfe432c5acd364a9e2c9075b" + integrity sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ== + available-typed-arrays@^1.0.5: version "1.0.5" resolved "https://registry.yarnpkg.com/available-typed-arrays/-/available-typed-arrays-1.0.5.tgz#92f95616501069d07d10edb2fc37d3e1c65123b7" @@ -2685,6 +2777,23 @@ available-typed-arrays@^1.0.7: dependencies: possible-typed-array-names "^1.0.0" +axios-retry@^3.4.0: + version "3.9.1" + resolved "https://registry.yarnpkg.com/axios-retry/-/axios-retry-3.9.1.tgz#c8924a8781c8e0a2c5244abf773deb7566b3830d" + integrity sha512-8PJDLJv7qTTMMwdnbMvrLYuvB47M81wRtxQmEdV5w4rgbTXTt+vtPkXwajOfOdSyv/wZICJOC+/UhXH4aQ/R+w== + dependencies: + "@babel/runtime" "^7.15.4" + is-retry-allowed "^2.2.0" + +axios@^1.7.4: + version "1.7.7" + resolved "https://registry.yarnpkg.com/axios/-/axios-1.7.7.tgz#2f554296f9892a72ac8d8e4c5b79c14a91d0a47f" + integrity sha512-S4kL7XrjgBmvdGut0sN3yJxqYzrDOnivkBiN0OFs6hLiUam3UPvswUo0kqGyhqUZGEOytHyumEdXsAkgCOUf3Q== + dependencies: + follow-redirects "^1.15.6" + form-data "^4.0.0" + proxy-from-env "^1.1.0" + b4a@^1.6.4: version "1.6.4" resolved "https://registry.yarnpkg.com/b4a/-/b4a-1.6.4.tgz#ef1c1422cae5ce6535ec191baeed7567443f36c9" @@ -2846,6 +2955,11 @@ block-stream@*: dependencies: inherits "~2.0.0" +bluebird@3.5.5: + version "3.5.5" + resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.5.5.tgz#a8d0afd73251effbbd5fe384a77d73003c17a71f" + integrity sha512-5am6HnnfN+urzt4yfg7IgTbotDjIT/u8AJpEt0sIU9FtXfVeezXAPKswrG+xKUCOYAINpSdgZVDU6QFh+cuH3w== + bluebird@~3.4.1: version "3.4.7" resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.4.7.tgz#f72d760be09b7f76d08ed8fae98b289a8d05fab3" @@ -3003,6 +3117,24 @@ bytes@^3.0.0: resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.1.0.tgz#f6cf7933a360e0588fa9fde85651cdc7f805d1f6" integrity sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg== +c12@^1.11.2: + version "1.11.2" + resolved "https://registry.yarnpkg.com/c12/-/c12-1.11.2.tgz#f8a1e30c10f4b273894a1bcb6944f76c15b56717" + integrity sha512-oBs8a4uvSDO9dm8b7OCFW7+dgtVrwmwnrVXYzLm43ta7ep2jCn/0MhoUFygIWtxhyy6+/MG7/agvpY0U1Iemew== + dependencies: + chokidar "^3.6.0" + confbox "^0.1.7" + defu "^6.1.4" + dotenv "^16.4.5" + giget "^1.2.3" + jiti "^1.21.6" + mlly "^1.7.1" + ohash "^1.1.3" + pathe "^1.1.2" + perfect-debounce "^1.0.0" + pkg-types "^1.2.0" + rc9 "^2.1.2" + c8@^9.1.0: version "9.1.0" resolved "https://registry.yarnpkg.com/c8/-/c8-9.1.0.tgz#0e57ba3ab9e5960ab1d650b4a86f71e53cb68112" @@ -3173,7 +3305,7 @@ chalk@^4.0.0, chalk@^4.1.0: ansi-styles "^4.1.0" supports-color "^7.1.0" -chalk@^4.x: +chalk@^4.1.2, chalk@^4.x: version "4.1.2" resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.2.tgz#aac4e2b7734a740867aeb16bf02aad556a1e7a01" integrity sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA== @@ -3196,6 +3328,11 @@ charenc@0.0.2: resolved "https://registry.yarnpkg.com/charenc/-/charenc-0.0.2.tgz#c0a1d2f3a7092e03774bfa83f14c0fc5790a8667" integrity sha512-yrLQ/yVUFXkzg7EDQsPieE/53+0RlaWTs+wBrvW36cyilJ2SaDWfl4Yj7MtLTXleV9uEKefbAGUPv2/iWSooRA== +check-more-types@2.24.0: + version "2.24.0" + resolved "https://registry.yarnpkg.com/check-more-types/-/check-more-types-2.24.0.tgz#1420ffb10fd444dcfc79b43891bbfffd32a84600" + integrity sha512-Pj779qHxV2tuapviy1bSZNEL1maXr13bPYpsvSDB68HlYcYuhlDrmGd63i0JHMCLKzc7rUSNIrpdJlhVlNwrxA== + chokidar@3.5.3, chokidar@^3.5.3: version "3.5.3" resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.5.3.tgz#1cf37c8707b932bd1af1ae22c0432e2acd1903bd" @@ -3230,6 +3367,21 @@ chokidar@^2.0.0: optionalDependencies: fsevents "^1.2.7" +chokidar@^3.6.0: + version "3.6.0" + resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.6.0.tgz#197c6cc669ef2a8dc5e7b4d97ee4e092c3eb0d5b" + integrity sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw== + dependencies: + anymatch "~3.1.2" + braces "~3.0.2" + glob-parent "~5.1.2" + is-binary-path "~2.1.0" + is-glob "~4.0.1" + normalize-path "~3.0.0" + readdirp "~3.6.0" + optionalDependencies: + fsevents "~2.3.2" + chownr@^1.1.1: version "1.1.4" resolved "https://registry.yarnpkg.com/chownr/-/chownr-1.1.4.tgz#6fc9d7b42d32a583596337666e7d08084da2cc6b" @@ -3265,6 +3417,13 @@ ci-info@^1.5.0: resolved "https://registry.yarnpkg.com/ci-info/-/ci-info-1.6.0.tgz#2ca20dbb9ceb32d4524a683303313f0304b1e497" integrity sha512-vsGdkwSCDpWmP80ncATX7iea5DWQemg1UgCW5J8tqjU3lYw4FBYuj89J0CTVomA7BEfvSZd84GmHko+MxFQU2A== +citty@^0.1.6: + version "0.1.6" + resolved "https://registry.yarnpkg.com/citty/-/citty-0.1.6.tgz#0f7904da1ed4625e1a9ea7e0fa780981aab7c5e4" + integrity sha512-tskPPKEs8D2KPafUypv2gxwJP8h/OaJmC82QQGGDQcHvXX43xF2VDACcJVmZ0EuSxkpO9Kc4MlrA3q0+FG58AQ== + dependencies: + consola "^3.2.3" + class-utils@^0.3.5: version "0.3.6" resolved "https://registry.yarnpkg.com/class-utils/-/class-utils-0.3.6.tgz#f93369ae8b9a7ce02fd41faad0ca83033190c463" @@ -3497,6 +3656,11 @@ commander@^10.0.1: resolved "https://registry.yarnpkg.com/commander/-/commander-10.0.1.tgz#881ee46b4f77d1c1dccc5823433aa39b022cbe06" integrity sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug== +commander@^11.1.0: + version "11.1.0" + resolved "https://registry.yarnpkg.com/commander/-/commander-11.1.0.tgz#62fdce76006a68e5c1ab3314dc92e800eb83d906" + integrity sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ== + commander@^2.19.0, commander@^2.20.0: version "2.20.3" resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33" @@ -3549,6 +3713,11 @@ concat-with-sourcemaps@^1.0.0: dependencies: source-map "^0.6.1" +confbox@^0.1.7, confbox@^0.1.8: + version "0.1.8" + resolved "https://registry.yarnpkg.com/confbox/-/confbox-0.1.8.tgz#820d73d3b3c82d9bd910652c5d4d599ef8ff8b06" + integrity sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w== + config-chain@^1.1.12: version "1.1.12" resolved "https://registry.yarnpkg.com/config-chain/-/config-chain-1.1.12.tgz#0fde8d091200eb5e808caf25fe618c02f48e4efa" @@ -3557,6 +3726,11 @@ config-chain@^1.1.12: ini "^1.3.4" proto-list "~1.2.1" +consola@^3.2.3: + version "3.2.3" + resolved "https://registry.yarnpkg.com/consola/-/consola-3.2.3.tgz#0741857aa88cfa0d6fd53f1cff0375136e98502f" + integrity sha512-I5qxpzLv+sJhTVEoLYNcTW+bThDCPsit0vLNKShZx6rLtpilNpmmeTPaeqJb9ZE9dV3DGaeby6Vuhrw38WjeyQ== + content-disposition@~0.5.2: version "0.5.4" resolved "https://registry.yarnpkg.com/content-disposition/-/content-disposition-0.5.4.tgz#8b82b4efac82512a02bb0b1dcec9d2c5e8eb5bfe" @@ -3831,6 +4005,13 @@ d@1, d@^1.0.1: es5-ext "^0.10.50" type "^1.0.1" +date-fns@^2.29.3: + version "2.30.0" + resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-2.30.0.tgz#f367e644839ff57894ec6ac480de40cae4b0f4d0" + integrity sha512-fnULvOpxnC5/Vg3NCiWelDsLiUc9bRwAPs/+LfTLNvetFCtCTN+yQz15C/fs4AwX1R9K5GLtLfn8QW+dWisaAw== + dependencies: + "@babel/runtime" "^7.21.0" + debounce@^1.0.0: version "1.1.0" resolved "https://registry.yarnpkg.com/debounce/-/debounce-1.1.0.tgz#6a1a4ee2a9dc4b7c24bb012558dbcdb05b37f408" @@ -3866,6 +4047,13 @@ debug@4, debug@4.3.4, debug@^4.0.1, debug@^4.1.0, debug@^4.1.1, debug@^4.3.2, de dependencies: ms "2.1.2" +debug@^4.3.7: + version "4.3.7" + resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.7.tgz#87945b4151a011d76d95a198d7111c865c360a52" + integrity sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ== + dependencies: + ms "^2.1.3" + decamelize@^1.1.1, decamelize@^1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-1.2.0.tgz#f6534d15148269b20352e7bee26f501f9a191290" @@ -3995,6 +4183,11 @@ define-property@^2.0.2: is-descriptor "^1.0.2" isobject "^3.0.1" +defu@^6.1.4: + version "6.1.4" + resolved "https://registry.yarnpkg.com/defu/-/defu-6.1.4.tgz#4e0c9cf9ff68fe5f3d7f2765cc1a012dfdcb0479" + integrity sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg== + delayed-stream@0.0.6: version "0.0.6" resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-0.0.6.tgz#a2646cb7ec3d5d7774614670a7a65de0c173edbc" @@ -4025,6 +4218,11 @@ deprecation@^2.0.0, deprecation@^2.3.1: resolved "https://registry.yarnpkg.com/deprecation/-/deprecation-2.3.1.tgz#6368cbdb40abf3373b525ac87e4a260c3a700919" integrity sha512-xmHIy4F3scKVwMsQ4WnVaS8bHOx0DmVwRywosKhaILI0ywMDWPtBSku2HNxRvF7jtwDRsoEwYQSfbxj8b7RlJQ== +destr@^2.0.3: + version "2.0.3" + resolved "https://registry.yarnpkg.com/destr/-/destr-2.0.3.tgz#7f9e97cb3d16dbdca7be52aca1644ce402cfe449" + integrity sha512-2N3BOUU4gYMpTP24s5rF5iP7BDr7uNTCs4ozw3kf/eKfvWSIu93GEBi5m427YoyJoeOzQ5smuu4nNAPGb8idSQ== + destroy@^1.0.4: version "1.0.4" resolved "https://registry.yarnpkg.com/destroy/-/destroy-1.0.4.tgz#978857442c44749e4206613e37946205826abd80" @@ -4151,6 +4349,11 @@ domutils@^3.0.1: domelementtype "^2.3.0" domhandler "^5.0.3" +dotenv@^16.0.3, dotenv@^16.4.5: + version "16.4.5" + resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-16.4.5.tgz#cdd3b3b604cb327e286b4762e13502f717cb099f" + integrity sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg== + duplexer2@~0.1.4: version "0.1.4" resolved "https://registry.yarnpkg.com/duplexer2/-/duplexer2-0.1.4.tgz#8b12dab878c0d69e3e7891051662a32fc6bddcc1" @@ -4535,6 +4738,11 @@ escape-string-regexp@^1.0.2, escape-string-regexp@^1.0.3, escape-string-regexp@^ resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4" integrity sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ= +escape-string-regexp@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz#a30304e99daa32e23b2fd20f51babd07cffca344" + integrity sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w== + eslint-plugin-header@3.1.1: version "3.1.1" resolved "https://registry.yarnpkg.com/eslint-plugin-header/-/eslint-plugin-header-3.1.1.tgz#6ce512432d57675265fac47292b50d1eff11acd6" @@ -4852,7 +5060,12 @@ event-stream@~3.3.4: stream-combiner "^0.2.2" through "^2.3.8" -eventemitter3@^4.0.0: +event-target-shim@^5.0.0: + version "5.0.1" + resolved "https://registry.yarnpkg.com/event-target-shim/-/event-target-shim-5.0.1.tgz#5d4d3ebdf9583d63a5333ce2deb7480ab2b05789" + integrity sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ== + +eventemitter3@^4.0.0, eventemitter3@^4.0.4: version "4.0.7" resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-4.0.7.tgz#2de9b68f6528d5644ef5c59526a1b4a07306169f" integrity sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw== @@ -4862,12 +5075,12 @@ events@^3.0.0: resolved "https://registry.yarnpkg.com/events/-/events-3.2.0.tgz#93b87c18f8efcd4202a461aec4dfc0556b639379" integrity sha512-/46HWwbfCX2xTawVfkKLGxMifJYQBWMwY1mjywRtb4c9x8l5NP3KoJtnIOiL1hfdRkIuYhETxQlo62IF8tcnlg== -events@^3.2.0: +events@^3.2.0, events@^3.3.0: version "3.3.0" resolved "https://registry.yarnpkg.com/events/-/events-3.3.0.tgz#31a95ad0a924e2d2c419a813aeb2c4e878ea7400" integrity sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q== -execa@^1.0.0: +execa@1.0.0, execa@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/execa/-/execa-1.0.0.tgz#c6236a5bb4df6d6f15e88e7f017798216749ddd8" integrity sha512-adbxcyWV46qiHyvSp50TKt05tB4tK3HcmF7/nxfAdhnox83seTDbwnaqKO4sXRy7roHAIFqJP/Rw/AuEbX61LA== @@ -4880,6 +5093,36 @@ execa@^1.0.0: signal-exit "^3.0.0" strip-eof "^1.0.0" +execa@^7.2.0: + version "7.2.0" + resolved "https://registry.yarnpkg.com/execa/-/execa-7.2.0.tgz#657e75ba984f42a70f38928cedc87d6f2d4fe4e9" + integrity sha512-UduyVP7TLB5IcAQl+OzLyLcS/l32W/GLg+AhHJ+ow40FOk2U3SAllPwR44v4vmdFwIWqpdwxxpQbF1n5ta9seA== + dependencies: + cross-spawn "^7.0.3" + get-stream "^6.0.1" + human-signals "^4.3.0" + is-stream "^3.0.0" + merge-stream "^2.0.0" + npm-run-path "^5.1.0" + onetime "^6.0.0" + signal-exit "^3.0.7" + strip-final-newline "^3.0.0" + +execa@^8.0.1: + version "8.0.1" + resolved "https://registry.yarnpkg.com/execa/-/execa-8.0.1.tgz#51f6a5943b580f963c3ca9c6321796db8cc39b8c" + integrity sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg== + dependencies: + cross-spawn "^7.0.3" + get-stream "^8.0.1" + human-signals "^5.0.0" + is-stream "^3.0.0" + merge-stream "^2.0.0" + npm-run-path "^5.1.0" + onetime "^6.0.0" + signal-exit "^4.1.0" + strip-final-newline "^3.0.0" + expand-brackets@^2.1.4: version "2.1.4" resolved "https://registry.yarnpkg.com/expand-brackets/-/expand-brackets-2.1.4.tgz#b77735e315ce30f6b6eff0f83b04151a22449622" @@ -5035,6 +5278,11 @@ fast-levenshtein@^2.0.6, fast-levenshtein@~2.0.6: resolved "https://registry.yarnpkg.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz#3d8a5c66883a16a30ca8643e851f19baa7797917" integrity sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc= +fast-redact@^3.1.1: + version "3.5.0" + resolved "https://registry.yarnpkg.com/fast-redact/-/fast-redact-3.5.0.tgz#e9ea02f7e57d0cd8438180083e93077e496285e4" + integrity sha512-dwsoQlS7h9hMeYUq1W++23NDcBLV4KqONnITDV9DjfS3q1SgDGVrBdvvTLUotWtPSD7asWDV9/CmsZPy8Hf70A== + fastest-levenshtein@^1.0.12: version "1.0.12" resolved "https://registry.yarnpkg.com/fastest-levenshtein/-/fastest-levenshtein-1.0.12.tgz#9990f7d3a88cc5a9ffd1f1745745251700d497e2" @@ -5231,7 +5479,7 @@ flush-write-stream@^1.0.2: inherits "^2.0.3" readable-stream "^2.3.6" -follow-redirects@^1.0.0: +follow-redirects@^1.0.0, follow-redirects@^1.15.6: version "1.15.9" resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.9.tgz#a604fa10e443bf98ca94228d9eebcc2e8a2c8ee1" integrity sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ== @@ -5458,6 +5706,16 @@ get-stream@^5.1.0: dependencies: pump "^3.0.0" +get-stream@^6.0.1: + version "6.0.1" + resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-6.0.1.tgz#a262d8eef67aced57c2852ad6167526a43cbf7b7" + integrity sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg== + +get-stream@^8.0.1: + version "8.0.1" + resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-8.0.1.tgz#def9dfd71742cd7754a7761ed43749a27d02eca2" + integrity sha512-VaUJspBffn/LMCJVoMvSAdmscJyS1auj5Zulnn5UoYcY531UWmdwhRWkcGKnGU93m5HSXP9LP2usOryrBtQowA== + get-symbol-description@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/get-symbol-description/-/get-symbol-description-1.0.2.tgz#533744d5aa20aca4e079c8e5daf7fd44202821f5" @@ -5472,6 +5730,27 @@ get-value@^2.0.3, get-value@^2.0.6: resolved "https://registry.yarnpkg.com/get-value/-/get-value-2.0.6.tgz#dc15ca1c672387ca76bd37ac0a395ba2042a2c28" integrity sha1-3BXKHGcjh8p2vTesCjlbogQqLCg= +getos@^3.2.1: + version "3.2.1" + resolved "https://registry.yarnpkg.com/getos/-/getos-3.2.1.tgz#0134d1f4e00eb46144c5a9c0ac4dc087cbb27dc5" + integrity sha512-U56CfOK17OKgTVqozZjUKNdkfEv6jk5WISBJ8SHoagjE6L69zOwl3Z+O8myjY9MEW3i2HPWQBt/LTbCgcC973Q== + dependencies: + async "^3.2.0" + +giget@^1.2.3: + version "1.2.3" + resolved "https://registry.yarnpkg.com/giget/-/giget-1.2.3.tgz#ef6845d1140e89adad595f7f3bb60aa31c672cb6" + integrity sha512-8EHPljDvs7qKykr6uw8b+lqLiUc/vUg+KVTI0uND4s63TdsZM2Xus3mflvF0DDG9SiM4RlCkFGL+7aAjRmV7KA== + dependencies: + citty "^0.1.6" + consola "^3.2.3" + defu "^6.1.4" + node-fetch-native "^1.6.3" + nypm "^0.3.8" + ohash "^1.1.3" + pathe "^1.1.2" + tar "^6.2.0" + github-from-package@0.0.0: version "0.0.0" resolved "https://registry.yarnpkg.com/github-from-package/-/github-from-package-0.0.0.tgz#97fb5d96bfde8973313f20e8288ef9a167fa64ce" @@ -6239,6 +6518,16 @@ https-proxy-agent@^7.0.4, https-proxy-agent@^7.0.5: agent-base "^7.0.2" debug "4" +human-signals@^4.3.0: + version "4.3.1" + resolved "https://registry.yarnpkg.com/human-signals/-/human-signals-4.3.1.tgz#ab7f811e851fca97ffbd2c1fe9a958964de321b2" + integrity sha512-nZXjEF2nbo7lIw3mgYjItAfgQXog3OjJogSbKa2CQIIvSGWcKgeJnQlNXip6NglNzYH45nSRiEVimMvYL8DDqQ== + +human-signals@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/human-signals/-/human-signals-5.0.0.tgz#42665a284f9ae0dade3ba41ebc37eb4b852f3a28" + integrity sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ== + husky@^0.13.1: version "0.13.4" resolved "https://registry.yarnpkg.com/husky/-/husky-0.13.4.tgz#48785c5028de3452a51c48c12c4f94b2124a1407" @@ -6763,6 +7052,11 @@ is-relative@^1.0.0: dependencies: is-unc-path "^1.0.0" +is-retry-allowed@^2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/is-retry-allowed/-/is-retry-allowed-2.2.0.tgz#88f34cbd236e043e71b6932d09b0c65fb7b4d71d" + integrity sha512-XVm7LOeLpTW4jV19QSH38vkswxoLud8sQ57YwJVTPWdiaI9I8keEhGFpBlslyVsgdQy4Opg8QOLb8YRgsyZiQg== + is-set@^2.0.3: version "2.0.3" resolved "https://registry.yarnpkg.com/is-set/-/is-set-2.0.3.tgz#8ab209ea424608141372ded6e0cb200ef1d9d01d" @@ -6780,6 +7074,11 @@ is-stream@^1.1.0: resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-1.1.0.tgz#12d4a3dd4e68e0b79ceb8dbc84173ae80d91ca44" integrity sha1-EtSj3U5o4Lec6428hBc66A2RykQ= +is-stream@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-3.0.0.tgz#e6bfd7aa6bef69f4f472ce9bb681e3e57b4319ac" + integrity sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA== + is-string@^1.0.4, is-string@^1.0.5, is-string@^1.0.7: version "1.0.7" resolved "https://registry.yarnpkg.com/is-string/-/is-string-1.0.7.tgz#0dd12bf2006f255bb58f695110eff7491eebc0fd" @@ -6926,6 +7225,11 @@ istanbul-lib-coverage@^3.2.0: resolved "https://registry.yarnpkg.com/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.0.tgz#189e7909d0a39fa5a3dfad5b03f71947770191d3" integrity sha512-eOeJ5BHCmHYvQK7xt9GkdHuzuCGS1Y6g9Gvnx3Ym33fz/HpLRYxiS0wHNr+m/MBC8B647Xt608vCDEvhl9c6Mw== +istanbul-lib-coverage@^3.2.2: + version "3.2.2" + resolved "https://registry.yarnpkg.com/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz#2d166c4b0644d43a39f04bf6c2edd1e585f31756" + integrity sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg== + istanbul-lib-instrument@^6.0.1: version "6.0.1" resolved "https://registry.yarnpkg.com/istanbul-lib-instrument/-/istanbul-lib-instrument-6.0.1.tgz#71e87707e8041428732518c6fb5211761753fbdf" @@ -7035,6 +7339,11 @@ jest-worker@^27.4.5: merge-stream "^2.0.0" supports-color "^8.0.0" +jiti@^1.21.6: + version "1.21.6" + resolved "https://registry.yarnpkg.com/jiti/-/jiti-1.21.6.tgz#6c7f7398dd4b3142767f9a168af2f317a428d268" + integrity sha512-2yTgeWTWzMWkHu6Jp9NKgePDaYHbntiwvYuuJLbbN9vl7DC9DvXKOB2BC3ZZ92D3cvV/aflH0osDfwpHepQ53w== + js-base64@^3.7.4: version "3.7.4" resolved "https://registry.yarnpkg.com/js-base64/-/js-base64-3.7.4.tgz#af95b20f23efc8034afd2d1cc5b9d0adf7419037" @@ -7316,6 +7625,11 @@ last-run@^1.1.0: default-resolution "^2.0.0" es6-weak-map "^2.0.1" +lazy-ass@1.6.0: + version "1.6.0" + resolved "https://registry.yarnpkg.com/lazy-ass/-/lazy-ass-1.6.0.tgz#7999655e8646c17f089fdd187d150d3324d54513" + integrity sha512-cc8oEVoctTvsFZ/Oje/kGnHbpWHYBe8IAJe4C0QNc3t8uM/0Y8+erSz/7Y1ALuXTEZTMvxXwO6YbX1ey3ujiZw== + lazy.js@^0.4.2: version "0.4.3" resolved "https://registry.yarnpkg.com/lazy.js/-/lazy.js-0.4.3.tgz#87f67a07ad36555121e4fff1520df31be66786d8" @@ -7386,6 +7700,11 @@ liftoff@^3.1.0: rechoir "^0.6.2" resolve "^1.1.7" +lil-http-terminator@^1.2.3: + version "1.2.3" + resolved "https://registry.yarnpkg.com/lil-http-terminator/-/lil-http-terminator-1.2.3.tgz#594ef0f3c2b2f7d43a8f2989b2b3de611bf507eb" + integrity sha512-vQcHSwAFq/kTR2cG6peOVS7SjgksGgSPeH0G2lkw+buue33thE/FCHdn10wJXXshc5RswFy0Iaz48qA2Busw5Q== + lilconfig@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/lilconfig/-/lilconfig-3.0.0.tgz#f8067feb033b5b74dab4602a5f5029420be749bc" @@ -7505,7 +7824,7 @@ lodash.uniq@^4.5.0: resolved "https://registry.yarnpkg.com/lodash.uniq/-/lodash.uniq-4.5.0.tgz#d0225373aeb652adc1bc82e4945339a842754773" integrity sha1-0CJTc662Uq3BvILklFM5qEJ1R3M= -lodash@^4.17.11, lodash@^4.17.12, lodash@^4.17.14, lodash@^4.17.15: +lodash@^4.17.11, lodash@^4.17.12, lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.21: version "4.17.21" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== @@ -7855,6 +8174,11 @@ mimic-fn@^2.0.0, mimic-fn@^2.1.0: resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-2.1.0.tgz#7ed2c2ccccaf84d3ffcb7a69b57711fc2083401b" integrity sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg== +mimic-fn@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-4.0.0.tgz#60a90550d5cb0b239cca65d893b1a53b29871ecc" + integrity sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw== + mimic-response@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/mimic-response/-/mimic-response-1.0.1.tgz#4923538878eef42063cb8a3e3b0798781487ab1b" @@ -7975,6 +8299,16 @@ mkdirp@^3.0.0: resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-3.0.1.tgz#e44e4c5607fb279c168241713cc6e0fea9adcb50" integrity sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg== +mlly@^1.7.1, mlly@^1.7.2: + version "1.7.3" + resolved "https://registry.yarnpkg.com/mlly/-/mlly-1.7.3.tgz#d86c0fcd8ad8e16395eb764a5f4b831590cee48c" + integrity sha512-xUsx5n/mN0uQf4V548PKQ+YShA4/IW0KI1dZhrNrPCLG+xizETbHTkOa1f8/xut9JRPp8kQuMnz0oqwkTiLo/A== + dependencies: + acorn "^8.14.0" + pathe "^1.1.2" + pkg-types "^1.2.1" + ufo "^1.5.4" + mocha-junit-reporter@^2.2.1: version "2.2.1" resolved "https://registry.yarnpkg.com/mocha-junit-reporter/-/mocha-junit-reporter-2.2.1.tgz#739f5595d0f051d07af9d74e32c416e13a41cde5" @@ -8053,7 +8387,7 @@ ms@2.1.2: resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009" integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w== -ms@2.1.3, ms@^2.1.1: +ms@2.1.3, ms@^2.1.1, ms@^2.1.3: version "2.1.3" resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2" integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== @@ -8088,7 +8422,7 @@ nanoid@3.3.3: resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.3.tgz#fd8e8b7aa761fe807dba2d1b98fb7241bb724a25" integrity sha512-p1sjXuopFs0xg+fPASzQ28agW1oHD7xDsd9Xkf3T15H3c/cifrFHVwrh74PdoklAPi+i7MdRsE47vm2r6JoB+w== -nanoid@^3.3.7: +nanoid@^3.3.4, nanoid@^3.3.7: version "3.3.7" resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.7.tgz#d0c301a691bc8d54efa0a2226ccf3fe2fd656bd8" integrity sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g== @@ -8203,6 +8537,11 @@ node-addon-api@^6.1.0: resolved "https://registry.yarnpkg.com/node-addon-api/-/node-addon-api-6.1.0.tgz#ac8470034e58e67d0c6f1204a18ae6995d9c0d76" integrity sha512-+eawOlIgy680F0kBzPUNFhMZGtJ1YmqM6l4+Crf4IkImjYrO/mqPwRMh352g23uIaQKFItcQ64I7KMaJxHgAVA== +node-fetch-native@^1.6.3: + version "1.6.4" + resolved "https://registry.yarnpkg.com/node-fetch-native/-/node-fetch-native-1.6.4.tgz#679fc8fd8111266d47d7e72c379f1bed9acff06e" + integrity sha512-IhOigYzAKHd244OC0JIMIUrjzctirCmPkaIfhDeGcEETWof5zKYUW7e7MYvChGWh/4CJeXEgsRyGzuF334rOOQ== + node-fetch@2.6.8: version "2.6.8" resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.8.tgz#a68d30b162bc1d8fd71a367e81b997e1f4d4937e" @@ -8328,6 +8667,13 @@ npm-run-path@^2.0.0: dependencies: path-key "^2.0.0" +npm-run-path@^5.1.0: + version "5.3.0" + resolved "https://registry.yarnpkg.com/npm-run-path/-/npm-run-path-5.3.0.tgz#e23353d0ebb9317f174e93417e4a4d82d0249e9f" + integrity sha512-ppwTtiJZq0O/ai0z7yfudtBpWIoxM8yE6nHi1X47eFR2EWORqfbu6CnPlNsjeN683eT0qG6H/Pyf9fCcvjnnnQ== + dependencies: + path-key "^4.0.0" + nth-check@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/nth-check/-/nth-check-2.0.1.tgz#2efe162f5c3da06a28959fbd3db75dbeea9f0fc2" @@ -8340,6 +8686,18 @@ number-is-nan@^1.0.0: resolved "https://registry.yarnpkg.com/number-is-nan/-/number-is-nan-1.0.1.tgz#097b602b53422a522c1afb8790318336941a011d" integrity sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0= +nypm@^0.3.8: + version "0.3.12" + resolved "https://registry.yarnpkg.com/nypm/-/nypm-0.3.12.tgz#37541bec0af3a37d3acd81d6662c6666e650b22e" + integrity sha512-D3pzNDWIvgA+7IORhD/IuWzEk4uXv6GsgOxiid4UU3h9oq5IqV1KtPDi63n4sZJ/xcWlr88c0QM2RgN5VbOhFA== + dependencies: + citty "^0.1.6" + consola "^3.2.3" + execa "^8.0.1" + pathe "^1.1.2" + pkg-types "^1.2.0" + ufo "^1.5.4" + object-assign@4.X, object-assign@^4.0.1, object-assign@^4.1.1: version "4.1.1" resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" @@ -8374,6 +8732,13 @@ object-keys@~0.4.0: resolved "https://registry.yarnpkg.com/object-keys/-/object-keys-0.4.0.tgz#28a6aae7428dd2c3a92f3d95f21335dd204e0336" integrity sha1-KKaq50KN0sOpLz2V8hM13SBOAzY= +object-sizeof@^2.6.5: + version "2.6.5" + resolved "https://registry.yarnpkg.com/object-sizeof/-/object-sizeof-2.6.5.tgz#84ea0760e38876532ab811987dab58a6bbf61230" + integrity sha512-Mu3udRqIsKpneKjIEJ2U/s1KmEgpl+N6cEX1o+dDl2aZ+VW5piHqNgomqAk5YMsDoSkpcA8HnIKx1eqGTKzdfw== + dependencies: + buffer "^6.0.3" + object-visit@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/object-visit/-/object-visit-1.0.1.tgz#f79c4493af0c5377b59fe39d395e41042dd045bb" @@ -8469,6 +8834,16 @@ object.values@^1.1.6, object.values@^1.1.7: define-properties "^1.2.0" es-abstract "^1.22.1" +ohash@^1.1.3: + version "1.1.4" + resolved "https://registry.yarnpkg.com/ohash/-/ohash-1.1.4.tgz#ae8d83014ab81157d2c285abf7792e2995fadd72" + integrity sha512-FlDryZAahJmEF3VR3w1KogSEdWX3WhA5GPakFx4J81kEAiHyLMpdLLElS8n8dfNadMgAne/MywcvmogzscVt4g== + +on-exit-leak-free@^2.1.0: + version "2.1.2" + resolved "https://registry.yarnpkg.com/on-exit-leak-free/-/on-exit-leak-free-2.1.2.tgz#fed195c9ebddb7d9e4c3842f93f281ac8dadd3b8" + integrity sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA== + on-finished@^2.3.0, on-finished@~2.3.0: version "2.3.0" resolved "https://registry.yarnpkg.com/on-finished/-/on-finished-2.3.0.tgz#20f1336481b083cd75337992a16971aa2d906947" @@ -8502,6 +8877,13 @@ onetime@^5.1.0: dependencies: mimic-fn "^2.1.0" +onetime@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/onetime/-/onetime-6.0.0.tgz#7c24c18ed1fd2e9bca4bd26806a33613c77d34b4" + integrity sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ== + dependencies: + mimic-fn "^4.0.0" + only@~0.0.2: version "0.0.2" resolved "https://registry.yarnpkg.com/only/-/only-0.0.2.tgz#2afde84d03e50b9a8edc444e30610a70295edfb4" @@ -8615,6 +8997,11 @@ p-cancelable@^2.0.0: resolved "https://registry.yarnpkg.com/p-cancelable/-/p-cancelable-2.1.1.tgz#aab7fbd416582fa32a3db49859c122487c5ed2cf" integrity sha512-BZOr3nRQHOntUjTrH8+Lh54smKHoHyur8We1V8DSMVrl5A2malOOwuJRnKRDjSnkoeBh4at6BwEnb5I7Jl31wg== +p-debounce@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/p-debounce/-/p-debounce-2.1.0.tgz#e79f70c6e325cbb9bddbcbec0b81025084671ad3" + integrity sha512-M9bMt62TTnozdZhqFgs+V7XD2MnuKCaz+7fZdlu2/T7xruI3uIE5CicQ0vx1hV7HIUYF0jF+4/R1AgfOkl74Qw== + p-defer@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/p-defer/-/p-defer-1.0.0.tgz#9f6eb182f6c9aa8cd743004a7d4f96b196b0fb0c" @@ -8670,6 +9057,21 @@ p-map@^1.0.0: resolved "https://registry.yarnpkg.com/p-map/-/p-map-1.2.0.tgz#e4e94f311eabbc8633a1e79908165fca26241b6b" integrity sha512-r6zKACMNhjPJMTl8KcFH4li//gkrXWfbD6feV8l6doRHlzljFWGJ2AP6iKaCJXyZmAUMOPtvbW7EXkbWO/pLEA== +p-queue@6.6.2: + version "6.6.2" + resolved "https://registry.yarnpkg.com/p-queue/-/p-queue-6.6.2.tgz#2068a9dcf8e67dd0ec3e7a2bcb76810faa85e426" + integrity sha512-RwFpb72c/BhQLEXIZ5K2e+AhgNVmIejGlTgiB9MzZ0e93GRvqZ7uSi0dvRF7/XIXDeNkra2fNHBxTyPDGySpjQ== + dependencies: + eventemitter3 "^4.0.4" + p-timeout "^3.2.0" + +p-timeout@^3.2.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/p-timeout/-/p-timeout-3.2.0.tgz#c7e17abc971d2a7962ef83626b35d635acf23dfe" + integrity sha512-rhIwUycgwwKcP9yTOOFK/AKsAopjjCakVqLHePO3CC6Mir1Z99xT+R63jZxAT5lFZLa2inS5h+ZS2GvR99/FBg== + dependencies: + p-finally "^1.0.0" + p-try@^2.0.0: version "2.2.0" resolved "https://registry.yarnpkg.com/p-try/-/p-try-2.2.0.tgz#cb2868540e313d61de58fafbe35ce9004d5540e6" @@ -8721,6 +9123,11 @@ parse-json@^4.0.0: error-ex "^1.3.1" json-parse-better-errors "^1.0.1" +parse-ms@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/parse-ms/-/parse-ms-2.1.0.tgz#348565a753d4391fa524029956b172cb7753097d" + integrity sha512-kHt7kzLoS9VBZfUsiKjv43mr91ea+U05EyKkEtqp7vNbHxmaVuEqN7XxeEVnGrMtYOAxGrDElSi96K7EgO1zCA== + parse-node-version@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/parse-node-version/-/parse-node-version-1.0.1.tgz#e2b5dbede00e7fa9bc363607f53327e8b073189b" @@ -8788,6 +9195,11 @@ path-key@^3.1.0: resolved "https://registry.yarnpkg.com/path-key/-/path-key-3.1.1.tgz#581f6ade658cbba65a0d3380de7753295054f375" integrity sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q== +path-key@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/path-key/-/path-key-4.0.0.tgz#295588dc3aee64154f877adb9d780b81c554bf18" + integrity sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ== + path-parse@^1.0.6, path-parse@^1.0.7: version "1.0.7" resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.7.tgz#fbc114b60ca42b30d9daf5858e4bd68bbedb6735" @@ -8854,6 +9266,11 @@ path-type@^4.0.0: resolved "https://registry.yarnpkg.com/path-type/-/path-type-4.0.0.tgz#84ed01c0a7ba380afe09d90a8c180dcd9d03043b" integrity sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw== +pathe@^1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/pathe/-/pathe-1.1.2.tgz#6c4cb47a945692e48a1ddd6e4094d170516437ec" + integrity sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ== + pause-stream@0.0.11, pause-stream@^0.0.11: version "0.0.11" resolved "https://registry.yarnpkg.com/pause-stream/-/pause-stream-0.0.11.tgz#fe5a34b0cbce12b5aa6a2b403ee2e73b602f1445" @@ -8875,6 +9292,11 @@ pend@~1.2.0: resolved "https://registry.yarnpkg.com/pend/-/pend-1.2.0.tgz#7a57eb550a6783f9115331fcf4663d5c8e007a50" integrity sha1-elfrVQpng/kRUzH89GY9XI4AelA= +perfect-debounce@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/perfect-debounce/-/perfect-debounce-1.0.0.tgz#9c2e8bc30b169cc984a58b7d5b28049839591d2a" + integrity sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA== + picocolors@^0.2.1: version "0.2.1" resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-0.2.1.tgz#570670f793646851d1ba135996962abad587859f" @@ -8927,6 +9349,36 @@ pinkie@^2.0.0: resolved "https://registry.yarnpkg.com/pinkie/-/pinkie-2.0.4.tgz#72556b80cfa0d48a974e80e77248e80ed4f7f870" integrity sha1-clVrgM+g1IqXToDnckjoDtT3+HA= +pino-abstract-transport@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/pino-abstract-transport/-/pino-abstract-transport-1.2.0.tgz#97f9f2631931e242da531b5c66d3079c12c9d1b5" + integrity sha512-Guhh8EZfPCfH+PMXAb6rKOjGQEoy0xlAIn+irODG5kgfYV+BQ0rGYYWTIel3P5mmyXqkYkPmdIkywsn6QKUR1Q== + dependencies: + readable-stream "^4.0.0" + split2 "^4.0.0" + +pino-std-serializers@^6.0.0: + version "6.2.2" + resolved "https://registry.yarnpkg.com/pino-std-serializers/-/pino-std-serializers-6.2.2.tgz#d9a9b5f2b9a402486a5fc4db0a737570a860aab3" + integrity sha512-cHjPPsE+vhj/tnhCy/wiMh3M3z3h/j15zHQX+S9GkTBgqJuTuJzYJ4gUyACLhDaJ7kk9ba9iRDmbH2tJU03OiA== + +pino@^8.11.0: + version "8.21.0" + resolved "https://registry.yarnpkg.com/pino/-/pino-8.21.0.tgz#e1207f3675a2722940d62da79a7a55a98409f00d" + integrity sha512-ip4qdzjkAyDDZklUaZkcRFb2iA118H9SgRh8yzTkSQK8HilsOJF7rSY8HoW5+I0M46AZgX/pxbprf2vvzQCE0Q== + dependencies: + atomic-sleep "^1.0.0" + fast-redact "^3.1.1" + on-exit-leak-free "^2.1.0" + pino-abstract-transport "^1.2.0" + pino-std-serializers "^6.0.0" + process-warning "^3.0.0" + quick-format-unescaped "^4.0.3" + real-require "^0.2.0" + safe-stable-stringify "^2.3.1" + sonic-boom "^3.7.0" + thread-stream "^2.6.0" + pkg-dir@^4.2.0: version "4.2.0" resolved "https://registry.yarnpkg.com/pkg-dir/-/pkg-dir-4.2.0.tgz#f099133df7ede422e81d1d8448270eeb3e4261f3" @@ -8934,6 +9386,15 @@ pkg-dir@^4.2.0: dependencies: find-up "^4.0.0" +pkg-types@^1.2.0, pkg-types@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/pkg-types/-/pkg-types-1.2.1.tgz#6ac4e455a5bb4b9a6185c1c79abd544c901db2e5" + integrity sha512-sQoqa8alT3nHjGuTjuKgOnvjo4cljkufdtLMnO2LBP/wRwuDlo1tkaEdMxCRhyGRPacv/ztlZgDPm2b7FAmEvw== + dependencies: + confbox "^0.1.8" + mlly "^1.7.2" + pathe "^1.1.2" + playwright-core@1.30.0: version "1.30.0" resolved "https://registry.yarnpkg.com/playwright-core/-/playwright-core-1.30.0.tgz#de987cea2e86669e3b85732d230c277771873285" @@ -9003,6 +9464,11 @@ plugin-error@^1.0.0, plugin-error@^1.0.1: arr-union "^3.1.0" extend-shallow "^3.0.2" +pluralize@^8.0.0: + version "8.0.0" + resolved "https://registry.yarnpkg.com/pluralize/-/pluralize-8.0.0.tgz#1a6fa16a38d12a1901e0320fa017051c539ce3b1" + integrity sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA== + posix-character-classes@^0.1.0: version "0.1.1" resolved "https://registry.yarnpkg.com/posix-character-classes/-/posix-character-classes-0.1.1.tgz#01eac0fe3b5af71a2a6c02feabb8c1fef7e00eab" @@ -9325,11 +9791,23 @@ pretty-hrtime@^1.0.0: resolved "https://registry.yarnpkg.com/pretty-hrtime/-/pretty-hrtime-1.0.3.tgz#b7e3ea42435a4c9b2759d99e0f201eb195802ee1" integrity sha1-t+PqQkNaTJsnWdmeDyAesZWALuE= +pretty-ms@^7.0.1: + version "7.0.1" + resolved "https://registry.yarnpkg.com/pretty-ms/-/pretty-ms-7.0.1.tgz#7d903eaab281f7d8e03c66f867e239dc32fb73e8" + integrity sha512-973driJZvxiGOQ5ONsFhOF/DtzPMOMtgC11kCpUrPGMTgqp2q/1gwzCquocrN33is0VZ5GFHXZYMM9l6h67v2Q== + dependencies: + parse-ms "^2.1.0" + process-nextick-args@^2.0.0, process-nextick-args@~2.0.0: version "2.0.1" resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.1.tgz#7820d9b16120cc55ca9ae7792680ae7dba6d7fe2" integrity sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag== +process-warning@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/process-warning/-/process-warning-3.0.0.tgz#96e5b88884187a1dce6f5c3166d611132058710b" + integrity sha512-mqn0kFRl0EoqhnL0GQ0veqFHyIN1yig9RHh/InzORTUiZHFRAur+aMtRkELNwGs9aNwKS6tg/An4NYBPGwvtzQ== + process@^0.11.10: version "0.11.10" resolved "https://registry.yarnpkg.com/process/-/process-0.11.10.tgz#7332300e840161bda3e69a1d1d91a7d4bc16f182" @@ -9456,6 +9934,11 @@ queue@^4.2.1: dependencies: inherits "~2.0.0" +quick-format-unescaped@^4.0.3: + version "4.0.4" + resolved "https://registry.yarnpkg.com/quick-format-unescaped/-/quick-format-unescaped-4.0.4.tgz#93ef6dd8d3453cbc7970dd614fad4c5954d6b5a7" + integrity sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg== + quick-lru@^5.1.1: version "5.1.1" resolved "https://registry.yarnpkg.com/quick-lru/-/quick-lru-5.1.1.tgz#366493e6b3e42a3a6885e2e99d18f80fb7a8c932" @@ -9466,6 +9949,11 @@ rambda@^7.4.0: resolved "https://registry.yarnpkg.com/rambda/-/rambda-7.5.0.tgz#1865044c59bc0b16f63026c6e5a97e4b1bbe98fe" integrity sha512-y/M9weqWAH4iopRd7EHDEQQvpFPHj1AA3oHozE9tfITHUtTR7Z9PSlIRRG2l1GuW7sefC1cXFfIcF+cgnShdBA== +ramda@0.26.1: + version "0.26.1" + resolved "https://registry.yarnpkg.com/ramda/-/ramda-0.26.1.tgz#8d41351eb8111c55353617fc3bbffad8e4d35d06" + integrity sha512-hLWjpy7EnsDBb0p+Z3B7rPi3GDeRG5ZtiI33kJhTt+ORCd38AbAIjB/9zRIUoeTbE/AVX5ZkU7m6bznsvrf8eQ== + randombytes@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/randombytes/-/randombytes-2.1.0.tgz#df6f84372f0270dc65cdf6291349ab7a473d4f2a" @@ -9473,6 +9961,14 @@ randombytes@^2.1.0: dependencies: safe-buffer "^5.1.0" +rc9@^2.1.2: + version "2.1.2" + resolved "https://registry.yarnpkg.com/rc9/-/rc9-2.1.2.tgz#6282ff638a50caa0a91a31d76af4a0b9cbd1080d" + integrity sha512-btXCnMmRIBINM2LDZoEmOogIZU7Qe7zn4BpomSKZ/ykbLObuBdvG+mFq11DL6fjH1DRwHhrlgtYWG96bJiC7Cg== + dependencies: + defu "^6.1.4" + destr "^2.0.3" + rc@^1.2.7: version "1.2.8" resolved "https://registry.yarnpkg.com/rc/-/rc-1.2.8.tgz#cd924bf5200a075b83c188cd6b9e211b7fc0d3ed" @@ -9578,6 +10074,17 @@ readable-stream@^3.1.1, readable-stream@^3.4.0: string_decoder "^1.1.1" util-deprecate "^1.0.1" +readable-stream@^4.0.0: + version "4.5.2" + resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-4.5.2.tgz#9e7fc4c45099baeed934bff6eb97ba6cf2729e09" + integrity sha512-yjavECdqeZ3GLXNgRXgeQEdz9fvDDkNKyHnbHRFtOr7/LcfgBcmct7t/ET+HaCTqfh06OzoAxrkN/IfjJBVe+g== + dependencies: + abort-controller "^3.0.0" + buffer "^6.0.3" + events "^3.3.0" + process "^0.11.10" + string_decoder "^1.3.0" + readable-stream@~1.0.17: version "1.0.34" resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-1.0.34.tgz#125820e34bc842d2f2aaafafe4c2916ee32c157c" @@ -9604,6 +10111,11 @@ readdirp@~3.6.0: dependencies: picomatch "^2.2.1" +real-require@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/real-require/-/real-require-0.2.0.tgz#209632dea1810be2ae063a6ac084fee7e33fba78" + integrity sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg== + rechoir@^0.6.2: version "0.6.2" resolved "https://registry.yarnpkg.com/rechoir/-/rechoir-0.6.2.tgz#85204b54dba82d5742e28c96756ef43af50e3384" @@ -9636,6 +10148,11 @@ regenerator-runtime@^0.13.11: resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz#f6dca3e7ceec20590d07ada785636a90cdca17f9" integrity sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg== +regenerator-runtime@^0.14.0: + version "0.14.1" + resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz#356ade10263f685dda125100cd862c1db895327f" + integrity sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw== + regex-not@^1.0.0, regex-not@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/regex-not/-/regex-not-1.0.2.tgz#1f4ece27e00b0b65e0247a6810e6a85d83a5752c" @@ -9853,6 +10370,11 @@ ret@~0.1.10: resolved "https://registry.yarnpkg.com/ret/-/ret-0.1.15.tgz#b8a4825d5bdb1fc3f6f53c2bc33f81388681c7bc" integrity sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg== +retry@0.13.1: + version "0.13.1" + resolved "https://registry.yarnpkg.com/retry/-/retry-0.13.1.tgz#185b1587acf67919d63b357349e03537b2484658" + integrity sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg== + reusify@^1.0.4: version "1.0.4" resolved "https://registry.yarnpkg.com/reusify/-/reusify-1.0.4.tgz#90da382b1e126efc02146e90845a88db12925d76" @@ -9944,6 +10466,11 @@ safe-regex@^1.1.0: dependencies: ret "~0.1.10" +safe-stable-stringify@^2.3.1: + version "2.5.0" + resolved "https://registry.yarnpkg.com/safe-stable-stringify/-/safe-stable-stringify-2.5.0.tgz#4ca2f8e385f2831c432a719b108a3bf7af42a1dd" + integrity sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA== + "safer-buffer@>= 2.1.2 < 3": version "2.1.2" resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" @@ -10173,7 +10700,12 @@ signal-exit@^3.0.0, signal-exit@^3.0.2: resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.3.tgz#a1410c2edd8f077b08b4e253c8eacfcaf057461c" integrity sha512-VUJ49FC8U1OxwZLxIbTTrDvLnf/6TDgxZcK8wxR8zs13xpx7xbG60ndBlhNrFi2EMuFRoeDoJO7wthSLq42EjA== -signal-exit@^4.0.1: +signal-exit@^3.0.7: + version "3.0.7" + resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.7.tgz#a9a1767f8af84155114eaabd73f99273c8f59ad9" + integrity sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ== + +signal-exit@^4.0.1, signal-exit@^4.1.0: version "4.1.0" resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-4.1.0.tgz#952188c1cbd546070e2dd20d0f41c0ae0530cb04" integrity sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw== @@ -10280,6 +10812,13 @@ socks@^2.7.1: ip "^2.0.0" smart-buffer "^4.2.0" +sonic-boom@^3.7.0: + version "3.8.1" + resolved "https://registry.yarnpkg.com/sonic-boom/-/sonic-boom-3.8.1.tgz#d5ba8c4e26d6176c9a1d14d549d9ff579a163422" + integrity sha512-y4Z8LCDBuum+PBP3lSV7RHrXscqksve/bi0as7mhwVnBW+/wUqKT/2Kb7um8yqcFy0duYbbPxzt89Zy2nOCaxg== + dependencies: + atomic-sleep "^1.0.0" + source-map-js@^1.0.1: version "1.0.2" resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.0.2.tgz#adbc361d9c62df380125e7f161f71c826f1e490c" @@ -10316,7 +10855,7 @@ source-map-support@^0.3.2: dependencies: source-map "0.1.32" -source-map-support@~0.5.19, source-map-support@~0.5.20: +source-map-support@^0.5.21, source-map-support@~0.5.19, source-map-support@~0.5.20: version "0.5.21" resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.21.tgz#04fe7c7f9e1ed2d662233c28cb2b35b9f63f6e4f" integrity sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w== @@ -10397,6 +10936,11 @@ split-string@^3.0.1, split-string@^3.0.2: dependencies: extend-shallow "^3.0.0" +split2@^4.0.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/split2/-/split2-4.2.0.tgz#c9c5920904d148bab0b9f67145f245a86aadbfa4" + integrity sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg== + split@0.3: version "0.3.3" resolved "https://registry.yarnpkg.com/split/-/split-0.3.3.tgz#cd0eea5e63a211dfff7eb0f091c4133e2d0dd28f" @@ -10431,6 +10975,13 @@ stack-trace@0.0.10: resolved "https://registry.yarnpkg.com/stack-trace/-/stack-trace-0.0.10.tgz#547c70b347e8d32b4e108ea1a2a159e5fdde19c0" integrity sha1-VHxws0fo0ytOEI6hoqFZ5f3eGcA= +stack-utils@^2.0.6: + version "2.0.6" + resolved "https://registry.yarnpkg.com/stack-utils/-/stack-utils-2.0.6.tgz#aaf0748169c02fc33c8232abccf933f54a1cc34f" + integrity sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ== + dependencies: + escape-string-regexp "^2.0.0" + static-extend@^0.1.1: version "0.1.2" resolved "https://registry.yarnpkg.com/static-extend/-/static-extend-0.1.2.tgz#60809c39cbff55337226fd5e0b520f341f1fb5c6" @@ -10656,7 +11207,7 @@ string.prototype.trimstart@^1.0.7: define-properties "^1.2.0" es-abstract "^1.22.1" -string_decoder@^1.1.1: +string_decoder@^1.1.1, string_decoder@^1.3.0: version "1.3.0" resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.3.0.tgz#42f114594a46cf1a8e30b0a84f56c78c3edac21e" integrity sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA== @@ -10739,6 +11290,11 @@ strip-eof@^1.0.0: resolved "https://registry.yarnpkg.com/strip-eof/-/strip-eof-1.0.0.tgz#bb43ff5598a6eb05d89b59fcd129c983313606bf" integrity sha1-u0P/VZim6wXYm1n80SnJgzE2Br8= +strip-final-newline@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/strip-final-newline/-/strip-final-newline-3.0.0.tgz#52894c313fbff318835280aed60ff71ebf12b8fd" + integrity sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw== + strip-json-comments@3.1.1, strip-json-comments@^3.1.0, strip-json-comments@^3.1.1: version "3.1.1" resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-3.1.1.tgz#31f1281b3832630434831c310c01cccda8cbe006" @@ -10911,7 +11467,7 @@ tar@^2.2.1: fstream "^1.0.12" inherits "2" -tar@^6.1.11: +tar@^6.1.11, tar@^6.2.0: version "6.2.1" resolved "https://registry.yarnpkg.com/tar/-/tar-6.2.1.tgz#717549c541bc3c2af15751bea94b1dd068d4b03a" integrity sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A== @@ -11010,6 +11566,13 @@ textextensions@~1.0.0: resolved "https://registry.yarnpkg.com/textextensions/-/textextensions-1.0.2.tgz#65486393ee1f2bb039a60cbba05b0b68bd9501d2" integrity sha1-ZUhjk+4fK7A5pgy7oFsLaL2VAdI= +thread-stream@^2.6.0: + version "2.7.0" + resolved "https://registry.yarnpkg.com/thread-stream/-/thread-stream-2.7.0.tgz#d8a8e1b3fd538a6cca8ce69dbe5d3d097b601e11" + integrity sha512-qQiRWsU/wvNolI6tbbCKd9iKaTnCXsTwVxhhKM6nctPdujTyztjlbUkUTUymidWcMnZ5pWR0ej4a0tjsW021vw== + dependencies: + real-require "^0.2.0" + through2-filter@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/through2-filter/-/through2-filter-3.0.0.tgz#700e786df2367c2c88cd8aa5be4cf9c1e7831254" @@ -11076,6 +11639,13 @@ timers-ext@^0.1.7: es5-ext "~0.10.46" next-tick "1" +tmp-promise@^3.0.3: + version "3.0.3" + resolved "https://registry.yarnpkg.com/tmp-promise/-/tmp-promise-3.0.3.tgz#60a1a1cc98c988674fcbfd23b6e3367bdeac4ce7" + integrity sha512-RwM7MoPojPxsOBYnyd2hy0bxtIlVrihNs9pj5SUvY8Zz1sQcQG2tG1hSr8PDxfgEB8RNKDhqbIlroIarSNDNsQ== + dependencies: + tmp "^0.2.0" + tmp@^0.0.33: version "0.0.33" resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.0.33.tgz#6d34335889768d21b2bcda0aa277ced3b1bfadf9" @@ -11083,6 +11653,11 @@ tmp@^0.0.33: dependencies: os-tmpdir "~1.0.2" +tmp@^0.2.0, tmp@^0.2.3: + version "0.2.3" + resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.2.3.tgz#eb783cc22bc1e8bebd0671476d46ea4eb32a79ae" + integrity sha512-nZD7m9iCPC5g0pYmcaxogYKggSfLsdxl8of3Q/oIbqCqLLIO9IAF0GWjX1z9NZRHPiXv8Wex4yDCaZsgEw0Y8w== + to-absolute-glob@^2.0.0: version "2.0.2" resolved "https://registry.yarnpkg.com/to-absolute-glob/-/to-absolute-glob-2.0.2.tgz#1865f43d9e74b0822db9f145b78cff7d0f7c849b" @@ -11207,6 +11782,11 @@ ts-node@^10.9.1: v8-compile-cache-lib "^3.0.1" yn "3.1.1" +ts-pattern@^4.3.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/ts-pattern/-/ts-pattern-4.3.0.tgz#7a995b39342f1b00d1507c2d2f3b90ea16e178a6" + integrity sha512-pefrkcd4lmIVR0LA49Imjf9DYLK8vtWhqBPA3Ya1ir8xCW0O2yjL9dsCVvI7pCodLC5q7smNpEtDR2yVulQxOg== + tsec@0.2.7: version "0.2.7" resolved "https://registry.yarnpkg.com/tsec/-/tsec-0.2.7.tgz#be530025907037ed57f37fc7625b6a7e3658fe43" @@ -11390,6 +11970,11 @@ uc.micro@^2.0.0, uc.micro@^2.1.0: resolved "https://registry.yarnpkg.com/uc.micro/-/uc.micro-2.1.0.tgz#f8d3f7d0ec4c3dea35a7e3c8efa4cb8b45c9e7ee" integrity sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A== +ufo@^1.5.4: + version "1.5.4" + resolved "https://registry.yarnpkg.com/ufo/-/ufo-1.5.4.tgz#16d6949674ca0c9e0fbbae1fa20a71d7b1ded754" + integrity sha512-UsUk3byDzKd04EyoZ7U4DOlxQaD14JUKQl6/P7wiX4FNvUfm3XL246n9W5AmqwW5RSFJ27NAuM0iLscAOYUiGQ== + unbox-primitive@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/unbox-primitive/-/unbox-primitive-1.0.2.tgz#29032021057d5e6cdbd08c5129c226dff8ed6f9e" @@ -12088,6 +12673,11 @@ ws@^7.2.0: resolved "https://registry.yarnpkg.com/ws/-/ws-7.5.10.tgz#58b5c20dc281633f6c19113f39b349bd8bd558d9" integrity sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ== +ws@^8.18.0: + version "8.18.0" + resolved "https://registry.yarnpkg.com/ws/-/ws-8.18.0.tgz#0d7505a6eafe2b0e712d232b42279f53bc289bbc" + integrity sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw== + xml2js@^0.4.19: version "0.4.23" resolved "https://registry.yarnpkg.com/xml2js/-/xml2js-0.4.23.tgz#a0c69516752421eb2ac758ee4d4ccf58843eac66" From 9598da7bda34e07ff95e26990617bbf3916e3154 Mon Sep 17 00:00:00 2001 From: Jonathan Date: Wed, 27 Nov 2024 17:17:33 -0800 Subject: [PATCH 07/11] Bump version to 2025.01 (#5555) Update version post-branch. --- product.json | 2 +- versions/2025.01.0.commit | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) create mode 100644 versions/2025.01.0.commit diff --git a/product.json b/product.json index 19eebb6a5af..c59816a5b7b 100644 --- a/product.json +++ b/product.json @@ -1,7 +1,7 @@ { "nameShort": "Positron", "nameLong": "Positron", - "positronVersion": "2024.12.0", + "positronVersion": "2025.01.0", "positronBuildNumber": 0, "applicationName": "positron", "dataFolderName": ".positron", diff --git a/versions/2025.01.0.commit b/versions/2025.01.0.commit new file mode 100644 index 00000000000..0f99e5e3d99 --- /dev/null +++ b/versions/2025.01.0.commit @@ -0,0 +1 @@ +c5ce275dc502f6b15433b271802cb33e1ba5ef68 \ No newline at end of file From a36f4d7ccf01d05c97dee2b52020e59229900841 Mon Sep 17 00:00:00 2001 From: Wasim Lorgat Date: Thu, 28 Nov 2024 15:44:31 +0200 Subject: [PATCH 08/11] Improve runtime session service stability (#5380) The aim of this PR is to improve the stability of the runtime session service, with a slight focus on notebooks. This is another step toward https://github.com/posit-dev/positron/issues/2671. I've also added an extensive suite of unit tests for the runtime session service, for two reasons: 1. Some of the issues we're seeing with notebooks are intermittent and therefore tricky to reproduce without tests. 2. I wanted to ensure that I didn't break any existing behavior with these changes as well as more planned changes to add notebook-runtime methods to the extension API. --- .../src/notebookController.ts | 9 + .../browser/positronPreview.contribution.ts | 3 +- .../common/languageRuntimeUiClient.ts | 2 +- .../runtimeSession/common/runtimeSession.ts | 355 ++++++-- .../test/common/runtimeSession.test.ts | 840 ++++++++++++++++++ .../test/common/testLanguageRuntimeSession.ts | 42 +- .../test/common/testRuntimeSessionService.ts | 2 +- .../common/positronWorkbenchTestServices.ts | 15 +- 8 files changed, 1168 insertions(+), 100 deletions(-) create mode 100644 src/vs/workbench/services/runtimeSession/test/common/runtimeSession.test.ts diff --git a/extensions/positron-notebook-controllers/src/notebookController.ts b/extensions/positron-notebook-controllers/src/notebookController.ts index aaac1770755..04af82b2549 100644 --- a/extensions/positron-notebook-controllers/src/notebookController.ts +++ b/extensions/positron-notebook-controllers/src/notebookController.ts @@ -331,10 +331,19 @@ function executeCode( } } }).catch(error => { + // Stop listening for replies. handler.dispose(); + + // Reject the outer execution promise since we've encountered an error. reject(error); + + // Rethrow the error to stop any replies that are chained to this promise. throw error; }); + + // Avoid unhandled rejections being logged to the console. + // The actual error-handling is in the catch block above. + currentMessagePromise.catch(() => { }); }); // Execute the cell. diff --git a/src/vs/workbench/contrib/positronPreview/browser/positronPreview.contribution.ts b/src/vs/workbench/contrib/positronPreview/browser/positronPreview.contribution.ts index 3de51e015b1..d24db8db586 100644 --- a/src/vs/workbench/contrib/positronPreview/browser/positronPreview.contribution.ts +++ b/src/vs/workbench/contrib/positronPreview/browser/positronPreview.contribution.ts @@ -20,12 +20,11 @@ import { ViewContainer, IViewContainersRegistry, ViewContainerLocation, Extensio import { registerAction2 } from 'vs/platform/actions/common/actions'; import { PositronOpenUrlInViewerAction } from 'vs/workbench/contrib/positronPreview/browser/positronPreviewActions'; import { IConfigurationRegistry, Extensions as ConfigurationExtensions, ConfigurationScope, } from 'vs/platform/configuration/common/configurationRegistry'; +import { POSITRON_PREVIEW_PLOTS_IN_VIEWER } from 'vs/workbench/services/languageRuntime/common/languageRuntimeUiClient'; // The Positron preview view icon. const positronPreviewViewIcon = registerIcon('positron-preview-view-icon', Codicon.positronPreviewView, nls.localize('positronPreviewViewIcon', 'View icon of the Positron preview view.')); -export const POSITRON_PREVIEW_PLOTS_IN_VIEWER = 'positron.viewer.interactivePlotsInViewer'; - // Register the Positron preview container. const VIEW_CONTAINER: ViewContainer = Registry.as(ViewContainerExtensions.ViewContainersRegistry).registerViewContainer({ id: POSITRON_PREVIEW_VIEW_ID, diff --git a/src/vs/workbench/services/languageRuntime/common/languageRuntimeUiClient.ts b/src/vs/workbench/services/languageRuntime/common/languageRuntimeUiClient.ts index d9fd4ca94a6..83ec0da31ce 100644 --- a/src/vs/workbench/services/languageRuntime/common/languageRuntimeUiClient.ts +++ b/src/vs/workbench/services/languageRuntime/common/languageRuntimeUiClient.ts @@ -13,8 +13,8 @@ import { URI } from 'vs/base/common/uri'; import { ICommandService } from 'vs/platform/commands/common/commands'; import { ILogService } from 'vs/platform/log/common/log'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; -import { POSITRON_PREVIEW_PLOTS_IN_VIEWER } from 'vs/workbench/contrib/positronPreview/browser/positronPreview.contribution'; +export const POSITRON_PREVIEW_PLOTS_IN_VIEWER = 'positron.viewer.interactivePlotsInViewer'; /** * The types of messages that can be sent to the backend. diff --git a/src/vs/workbench/services/runtimeSession/common/runtimeSession.ts b/src/vs/workbench/services/runtimeSession/common/runtimeSession.ts index c5b6e4a6358..ec9d2016eab 100644 --- a/src/vs/workbench/services/runtimeSession/common/runtimeSession.ts +++ b/src/vs/workbench/services/runtimeSession/common/runtimeSession.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import * as nls from 'vs/nls'; -import { DeferredPromise } from 'vs/base/common/async'; +import { DeferredPromise, disposableTimeout } from 'vs/base/common/async'; import { Emitter } from 'vs/base/common/event'; import { Disposable, DisposableStore, IDisposable, toDisposable } from 'vs/base/common/lifecycle'; import { URI } from 'vs/base/common/uri'; @@ -264,15 +264,20 @@ export class RuntimeSessionService extends Disposable implements IRuntimeSession * no console session with the given runtime identifier exists. */ getConsoleSessionForRuntime(runtimeId: string): ILanguageRuntimeSession | undefined { - const session = Array.from(this._activeSessionsBySessionId.values()).find(session => - session.session.runtimeMetadata.runtimeId === runtimeId && - session.session.metadata.sessionMode === LanguageRuntimeSessionMode.Console && - session.state !== RuntimeState.Exited); - if (session) { - return session.session; - } else { - return undefined; - } + // It's possible that there are multiple consoles for the same runtime, + // for example, if one failed to start and is uninitialized. In that case, + // we return the most recently created. + return Array.from(this._activeSessionsBySessionId.values()) + .map((info, index) => ({ info, index })) + .sort((a, b) => + b.info.session.metadata.createdTimestamp - a.info.session.metadata.createdTimestamp + // If the timestamps are the same, prefer the session that was inserted last. + || b.index - a.index) + .find(({ info }) => + info.session.runtimeMetadata.runtimeId === runtimeId && + info.session.metadata.sessionMode === LanguageRuntimeSessionMode.Console && + info.state !== RuntimeState.Exited) + ?.info.session; } /** @@ -311,8 +316,7 @@ export class RuntimeSessionService extends Disposable implements IRuntimeSession async selectRuntime(runtimeId: string, source: string): Promise { const runtime = this._languageRuntimeService.getRegisteredRuntime(runtimeId); if (!runtime) { - return Promise.reject(new Error(`Language runtime ID '${runtimeId}' ` + - `is not registered.`)); + throw new Error(`No language runtime with id '${runtimeId}' was found.`); } // Shut down any other runtime consoles for the language. @@ -365,28 +369,38 @@ export class RuntimeSessionService extends Disposable implements IRuntimeSession private async doShutdownRuntimeSession( session: ILanguageRuntimeSession, exitReason: RuntimeExitReason): Promise { + + const sessionDisposables = this._sessionDisposables.get(session.sessionId); + if (!sessionDisposables) { + throw new Error(`No disposables found for session ${session.sessionId}`); + } + // We wait for `onDidEndSession()` rather than `RuntimeState.Exited`, because the former // generates some Console output that must finish before starting up a new runtime: - const promise = new Promise(resolve => { - const disposable = session.onDidEndSession((exit) => { + const disposables = sessionDisposables.add(new DisposableStore()); + const promise = new Promise((resolve, reject) => { + disposables.add(session.onDidEndSession((exit) => { + disposables.dispose(); resolve(); - disposable.dispose(); - }); - }); - - const timeout = new Promise((_, reject) => { - setTimeout(() => { + })); + disposables.add(disposableTimeout(() => { + disposables.dispose(); reject(new Error(`Timed out waiting for runtime ` + `${formatLanguageRuntimeSession(session)} to finish exiting.`)); - }, 5000); + }, 5000)); }); // Ask the runtime to shut down. - await session.shutdown(exitReason); + try { + await session.shutdown(exitReason); + } catch (error) { + disposables.dispose(); + throw error; + } // Wait for the runtime onDidEndSession to resolve, or for the timeout to expire // (whichever comes first) - await Promise.race([promise, timeout]); + await promise; } /** @@ -419,38 +433,19 @@ export class RuntimeSessionService extends Disposable implements IRuntimeSession throw new Error(`No language runtime with id '${runtimeId}' was found.`); } - // If there is already a runtime starting for the language, throw an error. - if (sessionMode === LanguageRuntimeSessionMode.Console) { - const startingLanguageRuntime = this._startingConsolesByLanguageId.get( - languageRuntime.languageId); - if (startingLanguageRuntime) { - throw new Error(`Session for language runtime ${formatLanguageRuntimeMetadata(languageRuntime)} cannot be started because language runtime ${formatLanguageRuntimeMetadata(startingLanguageRuntime)} is already starting for the language. Request source: ${source}`); - } - - // If there is already a runtime running for the language, throw an error. - const runningLanguageRuntime = - this._consoleSessionsByLanguageId.get(languageRuntime.languageId); - if (runningLanguageRuntime) { - const metadata = runningLanguageRuntime.runtimeMetadata; - if (metadata.runtimeId === runtimeId) { - // If the runtime that is running is the one we were just asked - // to start, we're technically in good shape since the runtime - // is already running! - return runningLanguageRuntime.sessionId; - } else { - throw new Error(`A console for ` + - `${formatLanguageRuntimeMetadata(languageRuntime)} ` + - `cannot be started because a console for ` + - `${formatLanguageRuntimeMetadata(metadata)} is already running ` + - `for the ${metadata.languageName} language.`); - } - } + const runningSessionId = this.validateRuntimeSessionStart(sessionMode, languageRuntime, notebookUri, source); + if (runningSessionId) { + return runningSessionId; } // If the workspace is not trusted, defer starting the runtime until the // workspace is trusted. if (!this._workspaceTrustManagementService.isWorkspaceTrusted()) { - return this.autoStartRuntime(languageRuntime, source); + if (sessionMode === LanguageRuntimeSessionMode.Console) { + return this.autoStartRuntime(languageRuntime, source); + } else { + throw new Error(`Cannot start a ${sessionMode} session in an untrusted workspace.`); + } } // Start the runtime. @@ -471,6 +466,16 @@ export class RuntimeSessionService extends Disposable implements IRuntimeSession runtimeMetadata: ILanguageRuntimeMetadata, sessionMetadata: IRuntimeSessionMetadata): Promise { + // See if we are already starting the requested session. If we + // are, return the promise that resolves when the session is ready to + // use. This makes it possible for multiple requests to start the same + // session to be coalesced. + const startingRuntimePromise = this._startingSessionsBySessionMapKey.get( + getSessionMapKey(sessionMetadata.sessionMode, runtimeMetadata.runtimeId, sessionMetadata.notebookUri)); + if (startingRuntimePromise && !startingRuntimePromise.isSettled) { + return startingRuntimePromise.p.then(() => { }); + } + // Ensure that the runtime is registered. const languageRuntime = this._languageRuntimeService.getRegisteredRuntime( runtimeMetadata.runtimeId); @@ -480,9 +485,10 @@ export class RuntimeSessionService extends Disposable implements IRuntimeSession this._languageRuntimeService.registerRuntime(runtimeMetadata); } - // Add the runtime to the starting runtimes, if it's a console session. - if (sessionMetadata.sessionMode === LanguageRuntimeSessionMode.Console) { - this._startingConsolesByLanguageId.set(runtimeMetadata.languageId, runtimeMetadata); + const runningSessionId = this.validateRuntimeSessionStart( + sessionMetadata.sessionMode, runtimeMetadata, sessionMetadata.notebookUri); + if (runningSessionId) { + return; } // Create a promise that resolves when the runtime is ready to use. @@ -491,6 +497,13 @@ export class RuntimeSessionService extends Disposable implements IRuntimeSession sessionMetadata.sessionMode, runtimeMetadata.runtimeId, sessionMetadata.notebookUri); this._startingSessionsBySessionMapKey.set(sessionMapKey, startPromise); + // It's possible that startPromise is never awaited, so we log any errors here + // at the debug level since we still expect the error to be handled/logged elsewhere. + startPromise.p.catch((err) => this._logService.debug(`Error starting session: ${err}`)); + + this.setStartingSessionMaps( + sessionMetadata.sessionMode, runtimeMetadata, sessionMetadata.notebookUri); + // We should already have a session manager registered, since we can't // get here until the extension host has been activated. if (this._sessionManagers.length === 0) { @@ -509,8 +522,8 @@ export class RuntimeSessionService extends Disposable implements IRuntimeSession `Reconnecting to session '${sessionMetadata.sessionId}' for language runtime ` + `${formatLanguageRuntimeMetadata(runtimeMetadata)} failed. Reason: ${err}`); startPromise.error(err); - this._startingSessionsBySessionMapKey.delete(sessionMapKey); - this._startingConsolesByLanguageId.delete(runtimeMetadata.languageId); + this.clearStartingSessionMaps( + sessionMetadata.sessionMode, runtimeMetadata, sessionMetadata.notebookUri); throw err; } @@ -631,18 +644,19 @@ export class RuntimeSessionService extends Disposable implements IRuntimeSession `${formatLanguageRuntimeMetadata(metadata)} (Source: ${source}) ` + `because workspace trust has not been granted. ` + `The runtime will be started when workspace trust is granted.`); - this._workspaceTrustManagementService.onDidChangeTrust((trusted) => { + const disposable = this._register(this._workspaceTrustManagementService.onDidChangeTrust((trusted) => { if (!trusted) { // If the workspace is still not trusted, do nothing. - return ''; + return; } // If the workspace is trusted, start the runtime. + disposable.dispose(); this._logService.info(`Language runtime ` + `${formatLanguageRuntimeMetadata(metadata)} ` + `automatically starting after workspace trust was granted. ` + `Source: ${source}`); - return this.doAutoStartRuntime(metadata, source); - }); + this.doAutoStartRuntime(metadata, source); + })); } return ''; @@ -698,6 +712,38 @@ export class RuntimeSessionService extends Disposable implements IRuntimeSession metadata: ILanguageRuntimeMetadata, source: string): Promise { + // Auto-started runtimes are (currently) always console sessions. + const sessionMode = LanguageRuntimeSessionMode.Console; + const notebookUri = undefined; + + // See if we are already starting the requested session. If we + // are, return the promise that resolves when the session is ready to + // use. This makes it possible for multiple requests to start the same + // session to be coalesced. + const startingRuntimePromise = this._startingSessionsBySessionMapKey.get( + getSessionMapKey(sessionMode, metadata.runtimeId, notebookUri)); + if (startingRuntimePromise && !startingRuntimePromise.isSettled) { + return startingRuntimePromise.p; + } + + const runningSessionId = this.validateRuntimeSessionStart(sessionMode, metadata, notebookUri, source); + if (runningSessionId) { + return runningSessionId; + } + + // Before attempting to validate the runtime, add it to the set of + // starting consoles. + this._startingConsolesByLanguageId.set(metadata.languageId, metadata); + + // Create a promise that resolves when the runtime is ready to use. + const startPromise = new DeferredPromise(); + const sessionMapKey = getSessionMapKey(sessionMode, metadata.runtimeId, notebookUri); + this._startingSessionsBySessionMapKey.set(sessionMapKey, startPromise); + + // It's possible that startPromise is never awaited, so we log any errors here + // at the debug level since we still expect the error to be handled/logged elsewhere. + startPromise.p.catch(err => this._logService.debug(`Error starting runtime session: ${err}`)); + // Check to see if the runtime has already been registered with the // language runtime service. const languageRuntime = @@ -707,10 +753,6 @@ export class RuntimeSessionService extends Disposable implements IRuntimeSession // If it has not been registered, validate the metadata. if (!languageRuntime) { try { - // Before attempting to validate the runtime, add it to the set of - // starting consoles. - this._startingConsolesByLanguageId.set(metadata.languageId, metadata); - // Attempt to validate the metadata. Note that this can throw if the metadata // is invalid! const validated = await sessionManager.validateMetadata(metadata); @@ -762,9 +804,7 @@ export class RuntimeSessionService extends Disposable implements IRuntimeSession } } - // Auto-started runtimes are (currently) always console sessions. - return this.doCreateRuntimeSession(metadata, metadata.runtimeName, - LanguageRuntimeSessionMode.Console, source); + return this.doCreateRuntimeSession(metadata, metadata.runtimeName, sessionMode, source, notebookUri); } /** @@ -784,15 +824,19 @@ export class RuntimeSessionService extends Disposable implements IRuntimeSession sessionMode: LanguageRuntimeSessionMode, source: string, notebookUri?: URI): Promise { - // Add the runtime to the starting runtimes. - if (sessionMode === LanguageRuntimeSessionMode.Console) { - this._startingConsolesByLanguageId.set(runtimeMetadata.languageId, runtimeMetadata); - } + this.setStartingSessionMaps(sessionMode, runtimeMetadata, notebookUri); - // Create a promise that resolves when the runtime is ready to use. - const startPromise = new DeferredPromise(); + // Create a promise that resolves when the runtime is ready to use, if there isn't already one. const sessionMapKey = getSessionMapKey(sessionMode, runtimeMetadata.runtimeId, notebookUri); - this._startingSessionsBySessionMapKey.set(sessionMapKey, startPromise); + let startPromise = this._startingSessionsBySessionMapKey.get(sessionMapKey); + if (!startPromise || startPromise.isSettled) { + startPromise = new DeferredPromise(); + this._startingSessionsBySessionMapKey.set(sessionMapKey, startPromise); + + // It's possible that startPromise is never awaited, so we log any errors here + // at the debug level since we still expect the error to be handled/logged elsewhere. + startPromise.p.catch(err => this._logService.debug(`Error starting runtime session: ${err}`)); + } const sessionManager = await this.getManagerForRuntime(runtimeMetadata); const sessionId = this.generateNewSessionId(runtimeMetadata); @@ -814,8 +858,7 @@ export class RuntimeSessionService extends Disposable implements IRuntimeSession `Creating session for language runtime ` + `${formatLanguageRuntimeMetadata(runtimeMetadata)} failed. Reason: ${err}`); startPromise.error(err); - this._startingSessionsBySessionMapKey.delete(sessionMapKey); - this._startingConsolesByLanguageId.delete(runtimeMetadata.languageId); + this.clearStartingSessionMaps(sessionMode, runtimeMetadata, notebookUri); // Re-throw the error. throw err; @@ -854,21 +897,18 @@ export class RuntimeSessionService extends Disposable implements IRuntimeSession // Attach event handlers to the newly provisioned session. this.attachToSession(session, manager); - const sessionMapKey = getSessionMapKey( - session.metadata.sessionMode, session.runtimeMetadata.runtimeId, session.metadata.notebookUri); try { // Attempt to start, or reconnect to, the session. await session.start(); // The runtime started. Move it from the starting runtimes to the // running runtimes. - this._startingSessionsBySessionMapKey.delete(sessionMapKey); + this.clearStartingSessionMaps( + session.metadata.sessionMode, session.runtimeMetadata, session.metadata.notebookUri); if (session.metadata.sessionMode === LanguageRuntimeSessionMode.Console) { - this._startingConsolesByLanguageId.delete(session.runtimeMetadata.languageId); this._consoleSessionsByLanguageId.set(session.runtimeMetadata.languageId, session); } else if (session.metadata.sessionMode === LanguageRuntimeSessionMode.Notebook) { if (session.metadata.notebookUri) { - this._startingNotebooksByNotebookUri.delete(session.metadata.notebookUri); this._logService.info(`Notebook session for ${session.metadata.notebookUri} started: ${session.metadata.sessionId}`); this._notebookSessionsByNotebookUri.set(session.metadata.notebookUri, session); } else { @@ -885,10 +925,8 @@ export class RuntimeSessionService extends Disposable implements IRuntimeSession this._foregroundSession = session; } } catch (reason) { - - // Remove the runtime from the starting runtimes. - this._startingConsolesByLanguageId.delete(session.runtimeMetadata.languageId); - this._startingSessionsBySessionMapKey.delete(sessionMapKey); + this.clearStartingSessionMaps( + session.metadata.sessionMode, session.runtimeMetadata, session.metadata.notebookUri); // Fire the onDidFailStartRuntime event. this._onDidFailStartRuntimeEmitter.fire(session); @@ -960,17 +998,17 @@ export class RuntimeSessionService extends Disposable implements IRuntimeSession this.foregroundSession = session; } - // If this is a console session and there isn't already a console session - // for this language, set this one as the console session. - // (This restores the console session in the case of a - // restart) + // Restore the session in the case of a restart. if (session.metadata.sessionMode === LanguageRuntimeSessionMode.Console && !this._consoleSessionsByLanguageId.has(session.runtimeMetadata.languageId)) { this._consoleSessionsByLanguageId.set(session.runtimeMetadata.languageId, session); + } else if (session.metadata.sessionMode === LanguageRuntimeSessionMode.Notebook && + session.metadata.notebookUri && + !this._notebookSessionsByNotebookUri.has(session.metadata.notebookUri)) { + this._notebookSessionsByNotebookUri.set(session.metadata.notebookUri, session); } - // Start the UI client instance once the runtime is fully online. this.startUiClient(session); break; @@ -1042,6 +1080,132 @@ export class RuntimeSessionService extends Disposable implements IRuntimeSession })); } + /** + * Validate whether a runtime session can be started. + * + * @param sessionMode The mode of the new session. + * @param languageRuntime The metadata of the runtime to start. + * @param notebookUri The notebook URI to attach to the session, if any. + * @param source The source of the request to start the runtime, if known. + * @throws An error if the session cannot be started. + * @returns A session ID if a session is already running that matches the request, or undefined. + */ + private validateRuntimeSessionStart( + sessionMode: LanguageRuntimeSessionMode, + languageRuntime: ILanguageRuntimeMetadata, + notebookUri: URI | undefined, + source?: string, + ): string | undefined { + // If there is already a runtime starting for the language, throw an error. + if (sessionMode === LanguageRuntimeSessionMode.Console) { + const startingLanguageRuntime = this._startingConsolesByLanguageId.get( + languageRuntime.languageId); + if (startingLanguageRuntime) { + throw new Error(`Session for language runtime ` + + `${formatLanguageRuntimeMetadata(languageRuntime)} ` + + `cannot be started because language runtime ` + + `${formatLanguageRuntimeMetadata(startingLanguageRuntime)} ` + + `is already starting for the language.` + + (source ? ` Request source: ${source}` : ``)); + } + + // If there is already a runtime running for the language, throw an error. + const runningLanguageRuntime = + this._consoleSessionsByLanguageId.get(languageRuntime.languageId); + if (runningLanguageRuntime) { + const metadata = runningLanguageRuntime.runtimeMetadata; + if (metadata.runtimeId === languageRuntime.runtimeId) { + // If the runtime that is running is the one we were just asked + // to start, we're technically in good shape since the runtime + // is already running! + return runningLanguageRuntime.sessionId; + } else { + throw new Error(`A console for ` + + `${formatLanguageRuntimeMetadata(languageRuntime)} ` + + `cannot be started because a console for ` + + `${formatLanguageRuntimeMetadata(metadata)} is already running ` + + `for the ${metadata.languageName} language.` + + (source ? ` Request source: ${source}` : ``)); + } + } + } else if (sessionMode === LanguageRuntimeSessionMode.Notebook) { + // If no notebook URI is provided, throw an error. + if (!notebookUri) { + throw new Error(`A notebook URI must be provided when starting a notebook session.`); + } + + // If there is already a runtime starting for the notebook, throw an error. + const startingLanguageRuntime = this._startingNotebooksByNotebookUri.get(notebookUri); + if (startingLanguageRuntime) { + throw new Error(`Session for language runtime ` + + `${formatLanguageRuntimeMetadata(languageRuntime)} ` + + `cannot be started because language runtime ` + + `${formatLanguageRuntimeMetadata(startingLanguageRuntime)} ` + + `is already starting for the notebook ${notebookUri.toString()}.` + + (source ? ` Request source: ${source}` : ``)); + } + + // If there is already a runtime running for the notebook, throw an error. + const runningLanguageRuntime = this._notebookSessionsByNotebookUri.get(notebookUri); + if (runningLanguageRuntime) { + const metadata = runningLanguageRuntime.runtimeMetadata; + if (metadata.runtimeId === languageRuntime.runtimeId) { + // If the runtime that is running is the one we were just asked + // to start, we're technically in good shape since the runtime + // is already running! + return runningLanguageRuntime.sessionId; + } else { + throw new Error(`A notebook for ` + + `${formatLanguageRuntimeMetadata(languageRuntime)} ` + + `cannot be started because a notebook for ` + + `${formatLanguageRuntimeMetadata(metadata)} is already running ` + + `for the URI ${notebookUri.toString()}.` + + (source ? ` Request source: ${source}` : ``)); + } + } + } + + return undefined; + } + + /** + * Sets the session maps for a starting session. + * + * @param sessionMode The mode of the session. + * @param runtimeMetadata The metadata of the session's runtime. + * @param notebookUri The notebook URI attached to the session, if any. + */ + private setStartingSessionMaps( + sessionMode: LanguageRuntimeSessionMode, + runtimeMetadata: ILanguageRuntimeMetadata, + notebookUri?: URI) { + if (sessionMode === LanguageRuntimeSessionMode.Console) { + this._startingConsolesByLanguageId.set(runtimeMetadata.languageId, runtimeMetadata); + } else if (sessionMode === LanguageRuntimeSessionMode.Notebook && notebookUri) { + this._startingNotebooksByNotebookUri.set(notebookUri, runtimeMetadata); + } + } + + /** + * Clears the session maps for a starting session. + * + * @param sessionMode The mode of the session. + * @param runtimeMetadata The metadata of the session's runtime. + * @param notebookUri The notebook URI attached to the session, if any. + */ + private clearStartingSessionMaps( + sessionMode: LanguageRuntimeSessionMode, + runtimeMetadata: ILanguageRuntimeMetadata, + notebookUri?: URI) { + const sessionMapKey = getSessionMapKey(sessionMode, runtimeMetadata.runtimeId, notebookUri); + this._startingSessionsBySessionMapKey.delete(sessionMapKey); + if (sessionMode === LanguageRuntimeSessionMode.Console) { + this._startingConsolesByLanguageId.delete(runtimeMetadata.languageId); + } else if (sessionMode === LanguageRuntimeSessionMode.Notebook && notebookUri) { + this._startingNotebooksByNotebookUri.delete(notebookUri); + } + } + /** * Updates the session maps (for active consoles, notebooks, etc.), after a * session exits. @@ -1079,6 +1243,19 @@ export class RuntimeSessionService extends Disposable implements IRuntimeSession state === RuntimeState.Ready) { // The runtime looks like it could handle a restart request, so send // one over. + + // Mark the session as starting until the restart sequence completes. + this.setStartingSessionMaps( + session.metadata.sessionMode, session.runtimeMetadata, session.metadata.notebookUri); + const disposable = this._register(session.onDidChangeRuntimeState((state) => { + if (state === RuntimeState.Ready) { + disposable.dispose(); + this.clearStartingSessionMaps( + session.metadata.sessionMode, session.runtimeMetadata, session.metadata.notebookUri); + } + })); + + // Restart the session. return session.restart(); } else if (state === RuntimeState.Uninitialized || state === RuntimeState.Exited) { diff --git a/src/vs/workbench/services/runtimeSession/test/common/runtimeSession.test.ts b/src/vs/workbench/services/runtimeSession/test/common/runtimeSession.test.ts new file mode 100644 index 00000000000..ea178cedeea --- /dev/null +++ b/src/vs/workbench/services/runtimeSession/test/common/runtimeSession.test.ts @@ -0,0 +1,840 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (C) 2024 Posit Software, PBC. All rights reserved. + * Licensed under the Elastic License 2.0. See LICENSE.txt for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as assert from 'assert'; +import * as sinon from 'sinon'; +import { Event } from 'vs/base/common/event'; +import { URI } from 'vs/base/common/uri'; +import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { TestConfigurationService } from 'vs/platform/configuration/test/common/testConfigurationService'; +import { TestInstantiationService } from 'vs/platform/instantiation/test/common/instantiationServiceMock'; +import { IWorkspaceTrustManagementService } from 'vs/platform/workspace/common/workspaceTrust'; +import { formatLanguageRuntimeMetadata, formatLanguageRuntimeSession, ILanguageRuntimeMetadata, ILanguageRuntimeService, LanguageRuntimeSessionMode, RuntimeExitReason, RuntimeState } from 'vs/workbench/services/languageRuntime/common/languageRuntimeService'; +import { ILanguageRuntimeSession, IRuntimeSessionMetadata, IRuntimeSessionService, IRuntimeSessionWillStartEvent } from 'vs/workbench/services/runtimeSession/common/runtimeSessionService'; +import { TestLanguageRuntimeSession, waitForRuntimeState } from 'vs/workbench/services/runtimeSession/test/common/testLanguageRuntimeSession'; +import { createRuntimeServices, createTestLanguageRuntimeMetadata, startTestLanguageRuntimeSession } from 'vs/workbench/services/runtimeSession/test/common/testRuntimeSessionService'; +import { TestRuntimeSessionManager } from 'vs/workbench/test/common/positronWorkbenchTestServices'; +import { TestWorkspaceTrustManagementService } from 'vs/workbench/test/common/workbenchTestServices'; + +type IStartSessionTask = (runtimeMetadata?: ILanguageRuntimeMetadata) => Promise; + +suite('Positron - RuntimeSessionService', () => { + const disposables = ensureNoDisposablesAreLeakedInTestSuite(); + const startReason = 'Test requested to start a runtime session'; + const notebookUri = URI.file('/path/to/notebook'); + let instantiationService: TestInstantiationService; + let languageRuntimeService: ILanguageRuntimeService; + let runtimeSessionService: IRuntimeSessionService; + let configService: TestConfigurationService; + let workspaceTrustManagementService: TestWorkspaceTrustManagementService; + let manager: TestRuntimeSessionManager; + let runtime: ILanguageRuntimeMetadata; + let anotherRuntime: ILanguageRuntimeMetadata; + let sessionName: string; + let unregisteredRuntime: ILanguageRuntimeMetadata; + + setup(() => { + instantiationService = disposables.add(new TestInstantiationService()); + createRuntimeServices(instantiationService, disposables); + languageRuntimeService = instantiationService.get(ILanguageRuntimeService); + runtimeSessionService = instantiationService.get(IRuntimeSessionService); + configService = instantiationService.get(IConfigurationService) as TestConfigurationService; + workspaceTrustManagementService = instantiationService.get(IWorkspaceTrustManagementService) as TestWorkspaceTrustManagementService; + manager = TestRuntimeSessionManager.instance; + + runtime = createTestLanguageRuntimeMetadata(instantiationService, disposables); + anotherRuntime = createTestLanguageRuntimeMetadata(instantiationService, disposables); + sessionName = runtime.runtimeName; + unregisteredRuntime = { runtimeId: 'unregistered-runtime-id' } as ILanguageRuntimeMetadata; + + // Enable automatic startup. + configService.setUserConfiguration('positron.interpreters.automaticStartup', true); + + // Trust the workspace. + workspaceTrustManagementService.setWorkspaceTrust(true); + }); + + function startSession( + runtimeMetadata = runtime, + sessionMode: LanguageRuntimeSessionMode, + notebookUri?: URI, + ) { + return startTestLanguageRuntimeSession( + instantiationService, + disposables, + { + runtime: runtimeMetadata, + sessionName, + startReason, + sessionMode, + notebookUri, + }, + ); + } + + function startConsole(runtimeMetadata?: ILanguageRuntimeMetadata) { + return startSession(runtimeMetadata, LanguageRuntimeSessionMode.Console); + } + + function startNotebook(runtimeMetadata?: ILanguageRuntimeMetadata, notebookUri_ = notebookUri) { + return startSession(runtimeMetadata, LanguageRuntimeSessionMode.Notebook, notebookUri_); + } + + interface IServiceState { + hasStartingOrRunningConsole?: boolean; + consoleSession?: ILanguageRuntimeSession; + consoleSessionForLanguage?: ILanguageRuntimeSession; + consoleSessionForRuntime?: ILanguageRuntimeSession; + notebookSession?: ILanguageRuntimeSession; + notebookSessionForNotebookUri?: ILanguageRuntimeSession; + activeSessions?: ILanguageRuntimeSession[]; + } + + function assertServiceState(expectedState?: IServiceState, runtimeMetadata = runtime): void { + // Check the active sessions. + assert.deepStrictEqual(runtimeSessionService.activeSessions, expectedState?.activeSessions ?? []); + + // Check the console session state. + assert.strictEqual( + runtimeSessionService.hasStartingOrRunningConsole(runtimeMetadata.languageId), + expectedState?.hasStartingOrRunningConsole ?? false, + expectedState?.hasStartingOrRunningConsole ? + 'Expected a starting or running console session' : + 'Expected no starting or running console session', + ); + assert.strictEqual( + runtimeSessionService.getConsoleSessionForLanguage(runtimeMetadata.languageId), + expectedState?.consoleSessionForLanguage, + ); + assert.strictEqual( + runtimeSessionService.getConsoleSessionForRuntime(runtimeMetadata.runtimeId), + expectedState?.consoleSessionForRuntime, + ); + assert.strictEqual( + runtimeSessionService.getSession(expectedState?.consoleSession?.sessionId ?? ''), + expectedState?.consoleSession, + ); + + // Check the notebook session state. + assert.strictEqual( + runtimeSessionService.getSession(expectedState?.notebookSession?.sessionId ?? ''), + expectedState?.notebookSession, + ); + assert.strictEqual( + runtimeSessionService.getNotebookSessionForNotebookUri(notebookUri), + expectedState?.notebookSessionForNotebookUri, + ); + } + + function assertSingleSessionWillStart(sessionMode: LanguageRuntimeSessionMode) { + if (sessionMode === LanguageRuntimeSessionMode.Console) { + assertServiceState({ hasStartingOrRunningConsole: true }); + } else if (sessionMode === LanguageRuntimeSessionMode.Notebook) { + assertServiceState(); + } + } + + function assertHasSingleSession(session: ILanguageRuntimeSession) { + if (session.metadata.sessionMode === LanguageRuntimeSessionMode.Console) { + assertServiceState({ + hasStartingOrRunningConsole: true, + consoleSession: session, + consoleSessionForLanguage: session, + consoleSessionForRuntime: session, + activeSessions: [session], + }, session.runtimeMetadata); + } else if (session.metadata.sessionMode === LanguageRuntimeSessionMode.Notebook) { + assertServiceState({ + notebookSession: session, + notebookSessionForNotebookUri: session, + activeSessions: [session], + }, session.runtimeMetadata); + } + } + + function assertSingleSessionIsStarting(session: ILanguageRuntimeSession) { + assertHasSingleSession(session); + assert.strictEqual(session.getRuntimeState(), RuntimeState.Starting); + } + + function assertSingleSessionIsRestarting(session: ILanguageRuntimeSession) { + assertHasSingleSession(session); + assert.strictEqual(session.getRuntimeState(), RuntimeState.Restarting); + } + + function assertSingleSessionIsReady(session: ILanguageRuntimeSession) { + assertHasSingleSession(session); + assert.strictEqual(session.getRuntimeState(), RuntimeState.Ready); + } + + async function restoreSession( + sessionMetadata: IRuntimeSessionMetadata, runtimeMetadata = runtime, + ) { + await runtimeSessionService.restoreRuntimeSession(runtimeMetadata, sessionMetadata); + + // Ensure that the session gets disposed after the test. + const session = runtimeSessionService.getSession(sessionMetadata.sessionId); + assert.ok(session instanceof TestLanguageRuntimeSession); + disposables.add(session); + + return session; + } + + function restoreConsole(runtimeMetadata = runtime) { + const sessionMetadata: IRuntimeSessionMetadata = { + sessionId: 'test-console-session-id', + sessionName, + sessionMode: LanguageRuntimeSessionMode.Console, + createdTimestamp: Date.now(), + notebookUri: undefined, + startReason, + }; + return restoreSession(sessionMetadata, runtimeMetadata); + } + + function restoreNotebook(runtimeMetadata = runtime) { + const sessionMetadata: IRuntimeSessionMetadata = { + sessionId: 'test-notebook-session-id', + sessionName, + sessionMode: LanguageRuntimeSessionMode.Notebook, + createdTimestamp: Date.now(), + notebookUri, + startReason, + }; + return restoreSession(sessionMetadata, runtimeMetadata); + } + + async function autoStartSession(runtimeMetadata = runtime) { + const sessionId = await runtimeSessionService.autoStartRuntime(runtimeMetadata, startReason); + assert.ok(sessionId); + const session = runtimeSessionService.getSession(sessionId); + assert.ok(session instanceof TestLanguageRuntimeSession); + disposables.add(session); + return session; + } + + async function selectRuntime(runtimeMetadata = runtime) { + await runtimeSessionService.selectRuntime(runtimeMetadata.runtimeId, startReason); + const session = runtimeSessionService.getConsoleSessionForRuntime(runtimeMetadata.runtimeId); + assert.ok(session instanceof TestLanguageRuntimeSession); + disposables.add(session); + return session; + } + + const data: { action: string; startConsole: IStartSessionTask; startNotebook?: IStartSessionTask }[] = [ + { action: 'start', startConsole: startConsole, startNotebook: startNotebook }, + { action: 'restore', startConsole: restoreConsole, startNotebook: restoreNotebook }, + { action: 'auto start', startConsole: autoStartSession }, + { action: 'select', startConsole: selectRuntime }, + ]; + for (const { action, startConsole, startNotebook } of data) { + + for (const mode of [LanguageRuntimeSessionMode.Console, LanguageRuntimeSessionMode.Notebook]) { + const start = mode === LanguageRuntimeSessionMode.Console ? startConsole : startNotebook; + if (!start) { + continue; + } + + test(`${action} ${mode} returns the expected session`, async () => { + const session = await start(); + + assert.strictEqual(session.getRuntimeState(), RuntimeState.Starting); + assert.strictEqual(session.metadata.sessionName, sessionName); + assert.strictEqual(session.metadata.sessionMode, mode); + assert.strictEqual(session.metadata.startReason, startReason); + assert.strictEqual(session.runtimeMetadata, runtime); + + if (mode === LanguageRuntimeSessionMode.Console) { + assert.strictEqual(session.metadata.notebookUri, undefined); + } else { + assert.strictEqual(session.metadata.notebookUri, notebookUri); + } + }); + + test(`${action} ${mode} sets the expected service state`, async () => { + // Check the initial state. + assertServiceState(); + + const promise = start(); + + // Check the state before awaiting the promise. + assertSingleSessionWillStart(mode); + + const session = await promise; + + // Check the state after awaiting the promise. + assertSingleSessionIsStarting(session); + }); + + // TODO: Should onWillStartSession only fire once? + // It currently fires twice. Before the session is started and when the session + // enters the ready state. + test(`${action} ${mode} fires onWillStartSession`, async () => { + let error: Error | undefined; + const target = sinon.spy(({ session }: IRuntimeSessionWillStartEvent) => { + try { + if (target.callCount > 1) { + return; + } + assert.strictEqual(session.getRuntimeState(), RuntimeState.Uninitialized); + + assertSingleSessionWillStart(mode); + } catch (e) { + error = e; + } + }); + disposables.add(runtimeSessionService.onWillStartSession(target)); + const session = await start(); + + sinon.assert.calledTwice(target); + // When restoring a session, the first event is fired with isNew: false. + sinon.assert.calledWith(target.getCall(0), { isNew: action !== 'restore', session }); + sinon.assert.calledWith(target.getCall(1), { isNew: true, session }); + assert.ifError(error); + }); + + test(`${action} ${mode} fires onDidStartRuntime`, async () => { + let error: Error | undefined; + const target = sinon.stub<[e: ILanguageRuntimeSession]>().callsFake(session => { + try { + assert.strictEqual(session.getRuntimeState(), RuntimeState.Starting); + + assertSingleSessionIsStarting(session); + } catch (e) { + error = e; + } + }); + disposables.add(runtimeSessionService.onDidStartRuntime(target)); + + const session = await start(); + + sinon.assert.calledOnceWithExactly(target, session); + assert.ifError(error); + }); + + test(`${action} ${mode} fires events in order`, async () => { + const willStartSession = sinon.spy(); + disposables.add(runtimeSessionService.onWillStartSession(willStartSession)); + + const didStartRuntime = sinon.spy(); + disposables.add(runtimeSessionService.onDidStartRuntime(didStartRuntime)); + + await start(); + + sinon.assert.callOrder(willStartSession, didStartRuntime); + }); + + if (mode === LanguageRuntimeSessionMode.Console) { + test(`${action} ${mode} sets foregroundSession`, async () => { + const target = sinon.spy(); + disposables.add(runtimeSessionService.onDidChangeForegroundSession(target)); + + const session = await start(); + + assert.strictEqual(runtimeSessionService.foregroundSession, session); + + await waitForRuntimeState(session, RuntimeState.Ready); + + // TODO: Feels a bit surprising that this isn't fired. It's because we set the private + // _foregroundSession property instead of the setter. When the 'ready' state is + // entered, we skip setting foregroundSession because it already matches the session. + sinon.assert.notCalled(target); + }); + } + + if (action === 'start' || action === 'select') { + test(`${action} ${mode} throws for unknown runtime`, async () => { + const runtimeId = 'unknown-runtime-id'; + await assert.rejects( + start({ runtimeId } as ILanguageRuntimeMetadata,), + new Error(`No language runtime with id '${runtimeId}' was found.`), + ); + }); + } + + const createOrRestoreMethod = action === 'restore' ? 'restoreSession' : 'createSession'; + test(`${action} ${mode} encounters ${createOrRestoreMethod}() error`, async () => { + const error = new Error('Failed to create session'); + const stub = sinon.stub(manager, createOrRestoreMethod).rejects(error); + + await assert.rejects(start(), error); + + // If we start now, without createOrRestoreMethod rejecting, it should work. + stub.restore(); + const session = await start(); + + assertSingleSessionIsStarting(session); + }); + + test(`${action} ${mode} encounters session.start() error`, async () => { + // Listen to the onWillStartSession event and stub session.start() to throw an error. + const willStartSession = sinon.spy((e: IRuntimeSessionWillStartEvent) => { + sinon.stub(e.session, 'start').rejects(new Error('Session failed to start')); + }); + const willStartSessionDisposable = runtimeSessionService.onWillStartSession(willStartSession); + + const didFailStartRuntime = sinon.spy(); + disposables.add(runtimeSessionService.onDidFailStartRuntime(didFailStartRuntime)); + + const didStartRuntime = sinon.spy(); + disposables.add(runtimeSessionService.onDidStartRuntime(didStartRuntime)); + + const session1 = await start(); + + assert.strictEqual(session1.getRuntimeState(), RuntimeState.Uninitialized); + + if (mode === LanguageRuntimeSessionMode.Console) { + assertServiceState({ + hasStartingOrRunningConsole: false, + // Note that getConsoleSessionForRuntime includes uninitialized sessions + // but getConsoleSessionForLanguage does not. + consoleSessionForLanguage: undefined, + consoleSessionForRuntime: session1, + activeSessions: [session1], + }); + } else { + assertServiceState({ activeSessions: [session1] }); + } + + sinon.assert.calledOnceWithExactly(didFailStartRuntime, session1); + sinon.assert.callOrder(willStartSession, didFailStartRuntime); + sinon.assert.notCalled(didStartRuntime); + + // If we start now, without session.start() rejecting, it should work. + willStartSessionDisposable.dispose(); + const session2 = await start(); + + assert.strictEqual(session2.getRuntimeState(), RuntimeState.Starting); + + const expectedActiveSessions = action === 'restore' ? + // Restoring a session twice overwrites the previous session in activeSessions. + [session2] : + // Other actions create a new session in activeSessions. + [session1, session2]; + + if (mode === LanguageRuntimeSessionMode.Console) { + assertServiceState({ + hasStartingOrRunningConsole: true, + consoleSession: session2, + consoleSessionForLanguage: session2, + consoleSessionForRuntime: session2, + activeSessions: expectedActiveSessions, + }); + } else { + assertServiceState({ + notebookSession: session2, + notebookSessionForNotebookUri: session2, + activeSessions: expectedActiveSessions, + }); + } + }); + + test(`${action} ${mode} throws if another runtime is starting for the language`, async () => { + let error: Error; + if (mode === LanguageRuntimeSessionMode.Console) { + error = new Error(`Session for language runtime ${formatLanguageRuntimeMetadata(anotherRuntime)} ` + + `cannot be started because language runtime ${formatLanguageRuntimeMetadata(runtime)} ` + + `is already starting for the language.` + + (action !== 'restore' ? ` Request source: ${startReason}` : '')); + } else { + error = new Error(`Session for language runtime ${formatLanguageRuntimeMetadata(anotherRuntime)} cannot ` + + `be started because language runtime ${formatLanguageRuntimeMetadata(runtime)} ` + + `is already starting for the notebook ${notebookUri.toString()}.` + + (action !== 'restore' ? ` Request source: ${startReason}` : '')); + } + + await assert.rejects( + Promise.all([ + start(), + start(anotherRuntime), + ]), + error); + }); + + // Skip for 'select' since selecting another runtime is expected in that case. + if (action !== 'select') { + test(`${action} ${mode} throws if another runtime is running for the language`, async () => { + let error: Error; + if (mode === LanguageRuntimeSessionMode.Console) { + error = new Error(`A console for ${formatLanguageRuntimeMetadata(anotherRuntime)} cannot ` + + `be started because a console for ${formatLanguageRuntimeMetadata(runtime)} ` + + `is already running for the ${runtime.languageName} language.` + + (action !== 'restore' ? ` Request source: ${startReason}` : '')); + } else { + error = new Error(`A notebook for ${formatLanguageRuntimeMetadata(anotherRuntime)} cannot ` + + `be started because a notebook for ${formatLanguageRuntimeMetadata(runtime)} ` + + `is already running for the URI ${notebookUri.toString()}.` + + (action !== 'restore' ? ` Request source: ${startReason}` : '')); + } + + await start(); + await assert.rejects( + start(anotherRuntime), + error, + ); + }); + } + + test(`${action} ${mode} successively`, async () => { + const result1 = await start(); + const result2 = await start(); + const result3 = await start(); + + assert.strictEqual(result1, result2); + assert.strictEqual(result2, result3); + + assertSingleSessionIsStarting(result1); + }); + + test(`${action} ${mode} concurrently`, async () => { + const [result1, result2, result3] = await Promise.all([start(), start(), start()]); + + assert.strictEqual(result1, result2); + assert.strictEqual(result2, result3); + + assertSingleSessionIsStarting(result1); + }); + } + + if (startNotebook) { + test(`${action} console and notebook from the same runtime concurrently`, async () => { + // Consoles and notebooks shouldn't interfere with each other, even for the same runtime. + const [consoleSession, notebookSession] = await Promise.all([ + startConsole(), + startNotebook(), + ]); + + assert.strictEqual(consoleSession.getRuntimeState(), RuntimeState.Starting); + assert.strictEqual(notebookSession.getRuntimeState(), RuntimeState.Starting); + + assertServiceState({ + hasStartingOrRunningConsole: true, + consoleSession, + consoleSessionForLanguage: consoleSession, + consoleSessionForRuntime: consoleSession, + notebookSession, + notebookSessionForNotebookUri: notebookSession, + activeSessions: [consoleSession, notebookSession], + }); + }); + } + } + + test(`start notebook without notebook uri`, async () => { + await assert.rejects( + startSession(undefined, LanguageRuntimeSessionMode.Notebook, undefined), + new Error('A notebook URI must be provided when starting a notebook session.'), + ); + }); + + test('restore console registers runtime if unregistered', async () => { + // The runtime should not yet be registered. + assert.strictEqual(languageRuntimeService.getRegisteredRuntime(unregisteredRuntime.runtimeId), undefined); + + await restoreConsole(unregisteredRuntime); + + // The runtime should now be registered. + assert.strictEqual(languageRuntimeService.getRegisteredRuntime(unregisteredRuntime.runtimeId), unregisteredRuntime); + }); + + test('auto start validates runtime if unregistered', async () => { + // The runtime should not yet be registered. + assert.strictEqual(languageRuntimeService.getRegisteredRuntime(unregisteredRuntime.runtimeId), undefined); + + // Update the validator to add extra runtime data. + const validatedMetadata: Partial = { + extraRuntimeData: { someNewKey: 'someNewValue' } + }; + manager.setValidateMetadata(async (metadata: ILanguageRuntimeMetadata) => { + return { ...metadata, ...validatedMetadata }; + }); + + await autoStartSession(unregisteredRuntime); + + // The validated metadata should now be registered. + assert.deepStrictEqual( + languageRuntimeService.getRegisteredRuntime(unregisteredRuntime.runtimeId), + { ...unregisteredRuntime, ...validatedMetadata } + ); + }); + + test('auto start throws if runtime validation errors', async () => { + // The runtime should not yet be registered. + assert.strictEqual(languageRuntimeService.getRegisteredRuntime(unregisteredRuntime.runtimeId), undefined); + + // Update the validator to throw. + const error = new Error('Failed to validate runtime metadata'); + manager.setValidateMetadata(async (_metadata: ILanguageRuntimeMetadata) => { + throw error; + }); + + await assert.rejects(autoStartSession(unregisteredRuntime), error); + + // The runtime should remain unregistered. + assert.strictEqual(languageRuntimeService.getRegisteredRuntime(unregisteredRuntime.runtimeId), undefined); + }); + + test('auto start console does nothing if automatic startup is disabled', async () => { + configService.setUserConfiguration('positron.interpreters.automaticStartup', false); + + const sessionId = await runtimeSessionService.autoStartRuntime(runtime, startReason); + + assert.strictEqual(sessionId, ''); + assertServiceState(); + }); + + for (const action of ['auto start', 'start']) { + test(`${action} console in an untrusted workspace defers until trust is granted`, async () => { + workspaceTrustManagementService.setWorkspaceTrust(false); + + let sessionId: string; + if (action === 'auto start') { + sessionId = await runtimeSessionService.autoStartRuntime(runtime, startReason); + } else { + sessionId = await runtimeSessionService.startNewRuntimeSession( + runtime.runtimeId, sessionName, LanguageRuntimeSessionMode.Console, undefined, startReason); + } + + assert.strictEqual(sessionId, ''); + assertServiceState(); + + workspaceTrustManagementService.setWorkspaceTrust(true); + + // The session should eventually start. + const session = await Event.toPromise(runtimeSessionService.onDidStartRuntime); + disposables.add(session); + + assertSingleSessionIsStarting(session); + }); + } + + test('start notebook in an untrusted workspace throws', async () => { + workspaceTrustManagementService.setWorkspaceTrust(false); + + await assert.rejects(startNotebook(), new Error('Cannot start a notebook session in an untrusted workspace.')); + }); + + test('select console while another runtime is running for the language', async () => { + const session1 = await startConsole(anotherRuntime); + await waitForRuntimeState(session1, RuntimeState.Ready); + const session2 = await selectRuntime(); + + assert.strictEqual(session1.getRuntimeState(), RuntimeState.Exited); + assert.strictEqual(session2.getRuntimeState(), RuntimeState.Starting); + + assertServiceState({ + hasStartingOrRunningConsole: true, + consoleSession: session2, + consoleSessionForLanguage: session2, + consoleSessionForRuntime: session2, + activeSessions: [session1, session2], + }); + }); + + test('select console throws if session is still starting', async () => { + await startConsole(anotherRuntime); + await assert.rejects( + selectRuntime(), + new Error('Cannot shut down kernel; it is not (yet) running. (state = starting)'), + ); + }); + + test('select console to the same runtime sets the foreground session', async () => { + const session1 = await startConsole(); + + runtimeSessionService.foregroundSession = undefined; + + const session2 = await selectRuntime(); + + assert.strictEqual(session1, session2); + assert.strictEqual(runtimeSessionService.foregroundSession, session1); + }); + + test(`select console to another runtime and first session never fires onDidEndSession`, async () => { + const session = await startConsole(); + await waitForRuntimeState(session, RuntimeState.Ready); + + // Stub onDidEndSession to never fire, causing the shutdown to time out. + sinon.stub(session, 'onDidEndSession').returns({ dispose: () => { } }); + + // Use a fake timer to avoid actually having to wait for the timeout. + const clock = sinon.useFakeTimers(); + const promise = assert.rejects(selectRuntime(anotherRuntime), new Error(`Timed out waiting for runtime ` + + `${formatLanguageRuntimeSession(session)} to finish exiting.`)); + await clock.tickAsync(10_000); + await promise; + }); + + test(`select console to another runtime encounters session.shutdown() error`, async () => { + const session = await startConsole(); + + // Stub session.shutdown() to throw an error. + const error = new Error('Session failed to shut down'); + sinon.stub(session, 'shutdown').rejects(error); + + // We also want to ensure that the timeout is not hit in this case but don't want to + // actually wait, so we use a fake timer. + const clock = sinon.useFakeTimers(); + await assert.rejects(selectRuntime(anotherRuntime), error); + await clock.tickAsync(10_000); + }); + + function restartSession(sessionId: string) { + return runtimeSessionService.restartSession(sessionId, startReason); + } + + for (const { mode, start } of [ + { mode: LanguageRuntimeSessionMode.Console, start: startConsole }, + { mode: LanguageRuntimeSessionMode.Notebook, start: startNotebook }, + ]) { + test(`restart ${mode} throws if session not found`, async () => { + const sessionId = 'unknown-session-id'; + assert.rejects( + restartSession(sessionId), + new Error(`No session with ID '${sessionId}' was found.`), + ); + }); + + for (const state of [RuntimeState.Busy, RuntimeState.Idle, RuntimeState.Ready]) { + test(`restart ${mode} in '${state}' state`, async () => { + // Start the session and wait for it to be ready. + const session = await start(); + await waitForRuntimeState(session, RuntimeState.Ready); + + // Set the state to the desired state. + if (session.getRuntimeState() !== state) { + session.setRuntimeState(state); + } + + await restartSession(session.sessionId); + + assert.strictEqual(session.getRuntimeState(), RuntimeState.Restarting); + assertSingleSessionIsRestarting(session); + + await waitForRuntimeState(session, RuntimeState.Ready); + assertSingleSessionIsReady(session); + }); + } + + for (const state of [RuntimeState.Uninitialized, RuntimeState.Exited]) { + test(`restart ${mode} in '${state}' state`, async () => { + // Get a session to the exited state. + const session = await start(); + await waitForRuntimeState(session, RuntimeState.Ready); + await session.shutdown(RuntimeExitReason.Shutdown); + await waitForRuntimeState(session, RuntimeState.Exited); + + await restartSession(session.sessionId); + + // The existing sessino should remain exited. + assert.strictEqual(session.getRuntimeState(), RuntimeState.Exited); + + // A new session should be starting. + let newSession: ILanguageRuntimeSession | undefined; + if (mode === LanguageRuntimeSessionMode.Console) { + newSession = runtimeSessionService.getConsoleSessionForRuntime(runtime.runtimeId); + } else { + newSession = runtimeSessionService.getNotebookSessionForNotebookUri(notebookUri); + } + assert.ok(newSession); + disposables.add(newSession); + + assert.strictEqual(newSession.getRuntimeState(), RuntimeState.Starting); + assert.strictEqual(newSession.metadata.sessionName, session.metadata.sessionName); + assert.strictEqual(newSession.metadata.sessionMode, session.metadata.sessionMode); + assert.strictEqual(newSession.metadata.notebookUri, session.metadata.notebookUri); + assert.strictEqual(newSession.runtimeMetadata, session.runtimeMetadata); + + if (mode === LanguageRuntimeSessionMode.Console) { + assertServiceState({ + hasStartingOrRunningConsole: true, + consoleSession: newSession, + consoleSessionForLanguage: newSession, + consoleSessionForRuntime: newSession, + activeSessions: [session, newSession], + }); + } else { + assertServiceState({ + notebookSession: newSession, + notebookSessionForNotebookUri: newSession, + activeSessions: [session, newSession], + }); + } + }); + } + + test(`restart ${mode} in 'starting' state`, async () => { + const session = await start(); + assert.strictEqual(session.getRuntimeState(), RuntimeState.Starting); + + await restartSession(session.sessionId); + + assertSingleSessionIsStarting(session); + }); + + test(`restart ${mode} in 'restarting' state`, async () => { + const session = await start(); + await waitForRuntimeState(session, RuntimeState.Ready); + + session.restart(); + assert.strictEqual(session.getRuntimeState(), RuntimeState.Restarting); + + const target = sinon.spy(session, 'restart'); + + await restartSession(session.sessionId); + + assertSingleSessionIsRestarting(session); + + sinon.assert.notCalled(target); + }); + + test(`restart ${mode} concurrently`, async () => { + const session = await start(); + await waitForRuntimeState(session, RuntimeState.Ready); + + const target = sinon.spy(session, 'restart'); + + await Promise.all([ + restartSession(session.sessionId), + restartSession(session.sessionId), + restartSession(session.sessionId), + ]); + + assertSingleSessionIsRestarting(session); + + sinon.assert.calledOnce(target); + }); + + test(`restart ${mode} successively`, async () => { + const session = await start(); + + const target = sinon.spy(session, 'restart'); + + await waitForRuntimeState(session, RuntimeState.Ready); + await restartSession(session.sessionId); + await waitForRuntimeState(session, RuntimeState.Ready); + await restartSession(session.sessionId); + await waitForRuntimeState(session, RuntimeState.Ready); + await restartSession(session.sessionId); + + assertSingleSessionIsRestarting(session); + + sinon.assert.calledThrice(target); + }); + + test(`restart ${mode} while ready -> start`, async () => { + const session = await start(); + await waitForRuntimeState(session, RuntimeState.Ready); + + await restartSession(session.sessionId); + await waitForRuntimeState(session, RuntimeState.Ready); + + const newSession = await start(); + + assertSingleSessionIsReady(newSession); + }); + } +}); diff --git a/src/vs/workbench/services/runtimeSession/test/common/testLanguageRuntimeSession.ts b/src/vs/workbench/services/runtimeSession/test/common/testLanguageRuntimeSession.ts index 863a5fdea46..7363d755cb0 100644 --- a/src/vs/workbench/services/runtimeSession/test/common/testLanguageRuntimeSession.ts +++ b/src/vs/workbench/services/runtimeSession/test/common/testLanguageRuntimeSession.ts @@ -11,6 +11,7 @@ import { ILanguageRuntimeSession, IRuntimeClientInstance, IRuntimeSessionMetadat import { ILanguageRuntimeClientCreatedEvent, ILanguageRuntimeExit, ILanguageRuntimeInfo, ILanguageRuntimeMessage, ILanguageRuntimeMessageClearOutput, ILanguageRuntimeMessageError, ILanguageRuntimeMessageInput, ILanguageRuntimeMessageIPyWidget, ILanguageRuntimeMessageOutput, ILanguageRuntimeMessagePrompt, ILanguageRuntimeMessageResult, ILanguageRuntimeMessageState, ILanguageRuntimeMessageStream, ILanguageRuntimeMetadata, ILanguageRuntimeStartupFailure, LanguageRuntimeMessageType, RuntimeCodeExecutionMode, RuntimeCodeFragmentStatus, RuntimeErrorBehavior, RuntimeExitReason, RuntimeOnlineState, RuntimeOutputKind, RuntimeState } from 'vs/workbench/services/languageRuntime/common/languageRuntimeService'; import { IRuntimeClientEvent } from 'vs/workbench/services/languageRuntime/common/languageRuntimeUiClient'; import { TestRuntimeClientInstance } from 'vs/workbench/services/languageRuntime/test/common/testRuntimeClientInstance'; +import { CancellationError } from 'vs/base/common/errors'; export class TestLanguageRuntimeSession extends Disposable implements ILanguageRuntimeSession { private readonly _onDidChangeRuntimeState = this._register(new Emitter()); @@ -166,10 +167,24 @@ export class TestLanguageRuntimeSession extends Disposable implements ILanguageR async restart(): Promise { await this.shutdown(RuntimeExitReason.Restart); - await this.start(); + + // Wait for the session to exit, then start it again. + const disposable = this._register(this.onDidChangeRuntimeState(state => { + if (state === RuntimeState.Exited) { + disposable.dispose(); + this.start(); + } + })); } async shutdown(exitReason: RuntimeExitReason): Promise { + if (this._currentState !== RuntimeState.Idle && + this._currentState !== RuntimeState.Busy && + this._currentState !== RuntimeState.Ready) { + throw new Error('Cannot shut down kernel; it is not (yet) running.' + + ` (state = ${this._currentState})`); + } + if (exitReason === RuntimeExitReason.Restart) { this._onDidChangeRuntimeState.fire(RuntimeState.Restarting); } else { @@ -200,10 +215,6 @@ export class TestLanguageRuntimeSession extends Disposable implements ILanguageR throw new Error('Not implemented.'); } - override dispose() { - super.dispose(); - } - // Test helpers setRuntimeState(state: RuntimeState) { @@ -366,3 +377,24 @@ export class TestLanguageRuntimeSession extends Disposable implements ILanguageR }); } } + +export async function waitForRuntimeState( + session: ILanguageRuntimeSession, + state: RuntimeState, + timeout = 10_000, +) { + return new Promise((resolve, reject) => { + const timer = setTimeout(() => { + disposable.dispose(); + reject(new CancellationError()); + }, timeout); + + const disposable = session.onDidChangeRuntimeState(newState => { + if (newState === state) { + clearTimeout(timer); + disposable.dispose(); + resolve(); + } + }); + }); +} diff --git a/src/vs/workbench/services/runtimeSession/test/common/testRuntimeSessionService.ts b/src/vs/workbench/services/runtimeSession/test/common/testRuntimeSessionService.ts index 9ffc0958465..d6e6c187b0f 100644 --- a/src/vs/workbench/services/runtimeSession/test/common/testRuntimeSessionService.ts +++ b/src/vs/workbench/services/runtimeSession/test/common/testRuntimeSessionService.ts @@ -76,7 +76,7 @@ export function createTestLanguageRuntimeMetadata( disposables.add(languageRuntimeService.registerRuntime(runtime)); // Register the test runtime manager. - const manager = new TestRuntimeSessionManager(); + const manager = TestRuntimeSessionManager.instance; disposables.add(runtimeSessionService.registerSessionManager(manager)); return runtime; diff --git a/src/vs/workbench/test/common/positronWorkbenchTestServices.ts b/src/vs/workbench/test/common/positronWorkbenchTestServices.ts index 32923d7e7d3..028f06ec31e 100644 --- a/src/vs/workbench/test/common/positronWorkbenchTestServices.ts +++ b/src/vs/workbench/test/common/positronWorkbenchTestServices.ts @@ -107,6 +107,10 @@ export class TestPositronModalDialogService implements IPositronModalDialogsServ } } export class TestRuntimeSessionManager implements ILanguageRuntimeSessionManager { + public static readonly instance = new TestRuntimeSessionManager(); + + private _validateMetadata?: (metadata: ILanguageRuntimeMetadata) => Promise; + async managesRuntime(runtime: ILanguageRuntimeMetadata): Promise { return true; } @@ -119,7 +123,14 @@ export class TestRuntimeSessionManager implements ILanguageRuntimeSessionManager return new TestLanguageRuntimeSession(sessionMetadata, runtimeMetadata); } - validateMetadata(metadata: ILanguageRuntimeMetadata): Promise { - throw new Error('Method not implemented'); + async validateMetadata(metadata: ILanguageRuntimeMetadata): Promise { + if (this._validateMetadata) { + return this._validateMetadata(metadata); + } + return metadata; + } + + setValidateMetadata(handler: (metadata: ILanguageRuntimeMetadata) => Promise): void { + this._validateMetadata = handler; } } From d7bca1b20a3cbd9906ea74dd7549ac494ac053fb Mon Sep 17 00:00:00 2001 From: Marie Idleman Date: Mon, 2 Dec 2024 10:33:51 -0600 Subject: [PATCH 09/11] e2e-test: upload logs (#5557) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ### Summary * upload logs artifacts for full test suite run * turn on ALL unit and integration tests for PRs 😬 * preload node binary for browser tests - it was causing a failure at startup due to parallelization conflict Screenshot 2024-12-02 at 7 35 54 AM --- .github/workflows/positron-full-test.yml | 21 +++++- .../workflows/positron-merge-to-branch.yml | 66 +++++++++++++++++-- test/smoke/src/areas/positron/_test.setup.ts | 2 +- 3 files changed, 80 insertions(+), 9 deletions(-) diff --git a/.github/workflows/positron-full-test.yml b/.github/workflows/positron-full-test.yml index e38a8a01119..085d7f1c0fb 100644 --- a/.github/workflows/positron-full-test.yml +++ b/.github/workflows/positron-full-test.yml @@ -133,7 +133,7 @@ jobs: PWTEST_BLOB_DO_NOT_REMOVE: 1 CURRENTS_TAG: "electron" id: electron-tests - run: DISPLAY=:10 npx playwright test --project e2e-electron --workers 3 + run: DISPLAY=:10 npx playwright test --project e2e-electron --workers 2 - name: Upload blob report if: ${{ !cancelled() }} @@ -150,6 +150,13 @@ jobs: name: junit-report-electron path: test-results/junit.xml + - name: Upload logs + if: ${{ !cancelled() }} + uses: actions/upload-artifact@v4 + with: + name: logs-electron + path: test-logs + e2e-browser-tests: runs-on: ubuntu-latest-8x timeout-minutes: 50 @@ -185,6 +192,11 @@ jobs: aws-region: ${{ secrets.QA_AWS_REGION }} github-token: ${{ secrets.GITHUB_TOKEN }} + # Preloading ensures the Node.js binary is fully built and ready before + # any parallel processes start, preventing runtime conflicts + - name: Preload Node.js Binary + run: yarn gulp node + - name: Run Tests (Browser) env: POSITRON_PY_VER_SEL: 3.10.12 @@ -206,6 +218,13 @@ jobs: path: blob-report retention-days: 14 + - name: Upload logs + if: ${{ !cancelled() }} + uses: actions/upload-artifact@v4 + with: + name: logs-browser + path: test-logs + - name: Clean up license files if: always() run: cd .. && rm -rf positron-license diff --git a/.github/workflows/positron-merge-to-branch.yml b/.github/workflows/positron-merge-to-branch.yml index 3cca8d5b85f..9227eabaf23 100644 --- a/.github/workflows/positron-merge-to-branch.yml +++ b/.github/workflows/positron-merge-to-branch.yml @@ -84,7 +84,7 @@ jobs: with: role-to-assume: ${{ secrets.AWS_TEST_REPORTS_ROLE }} - unit-integration: + unit-tests: runs-on: ubuntu-latest timeout-minutes: 20 env: @@ -118,21 +118,73 @@ jobs: with: version: "4.4.0" - - name: Compile Integration Tests - run: yarn --cwd test/integration/browser compile + - name: Run Unit Tests (Electron) + id: electron-unit-tests + run: DISPLAY=:10 ./scripts/test.sh - name: Run Unit Tests (node.js) id: nodejs-unit-tests run: yarn test-node - - name: Run Integration Tests (Electron) - id: electron-integration-tests - run: DISPLAY=:10 ./scripts/test-integration-pr.sh + - name: Run Unit Tests (Browser, Chromium) + id: browser-unit-tests + run: DISPLAY=:10 yarn test-browser-no-install --browser chromium + + integration-tests: + runs-on: ubuntu-latest-4x + timeout-minutes: 20 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + POSITRON_BUILD_NUMBER: 0 # CI skips building releases + _R_CHECK_FUTURE_FILE_TIMESTAMPS_: false # this check can be flaky in the R pkg tests + _R_CHECK_CRAN_INCOMING_: false + _R_CHECK_SYSTEM_CLOCK_: false + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-node@v4 + with: + node-version-file: .nvmrc + + - name: Cache node_modules, build, extensions, and remote + uses: ./.github/actions/cache-multi-paths + + - name: Setup Build and Compile + uses: ./.github/actions/setup-build-env + + - name: Install Positron License + uses: ./.github/actions/install-license + with: + github-token: ${{ secrets.POSITRON_GITHUB_PAT }} + license-key: ${{ secrets.POSITRON_DEV_LICENSE }} + + # one integration test needs this: Connections pane works for R + - name: Setup R + uses: ./.github/actions/install-r + with: + version: "4.4.0" + + - name: Compile Integration Tests + run: yarn --cwd test/integration/browser compile + + - name: Run Integration Tests (Electron) + id: electron-integration-tests + run: DISPLAY=:10 ./scripts/test-integration-pr.sh + + - name: Run Integration Tests (Remote) + if: ${{ job.status != 'cancelled' && (success() || failure()) }} + id: electron-remote-integration-tests + run: DISPLAY=:10 ./scripts/test-remote-integration.sh + + - name: Run Integration Tests (Browser, Chromium) + if: ${{ job.status != 'cancelled' && (success() || failure()) }} + id: browser-integration-tests + run: DISPLAY=:10 ./scripts/test-web-integration.sh --browser chromium slack-notification: name: "Send Slack notification" runs-on: ubuntu-latest - needs: [unit-integration, e2e-electron] + needs: [unit-tests, integration-tests, e2e-electron] if: ${{ failure() && inputs.e2e_grep == '' }} steps: - name: "Send Slack notification" diff --git a/test/smoke/src/areas/positron/_test.setup.ts b/test/smoke/src/areas/positron/_test.setup.ts index fbb1951ff62..11959f7915b 100644 --- a/test/smoke/src/areas/positron/_test.setup.ts +++ b/test/smoke/src/areas/positron/_test.setup.ts @@ -122,7 +122,7 @@ export const test = base.extend({ }; await use({ set: setInterpreter }); - }, { scope: 'test', }], + }, { scope: 'test', timeout: 30000 }], r: [ async ({ interpreter }, use) => { From d1f15641b7215d7760a245c439af1f911472d7cc Mon Sep 17 00:00:00 2001 From: Marie Idleman Date: Mon, 2 Dec 2024 11:32:52 -0600 Subject: [PATCH 10/11] e2e-test: skip viewer test (#5571) ### QA Notes Skipping viewer test for now until we understand why it's failing only in CI all of a sudden. Linked issue to test as well. --- test/smoke/src/areas/positron/viewer/viewer.test.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/test/smoke/src/areas/positron/viewer/viewer.test.ts b/test/smoke/src/areas/positron/viewer/viewer.test.ts index 2a9aed2c5ff..092e1d9650c 100644 --- a/test/smoke/src/areas/positron/viewer/viewer.test.ts +++ b/test/smoke/src/areas/positron/viewer/viewer.test.ts @@ -15,7 +15,9 @@ test.describe('Viewer', () => { await app.workbench.positronViewer.clearViewer(); }); - test('Python - Verify Viewer functionality with vetiver [C784887]', async function ({ app, logger, python }) { + test.skip('Python - Verify Viewer functionality with vetiver [C784887]', { + annotation: [{ type: 'issue', description: 'https://github.com/posit-dev/positron/issues/5569' }] + }, async function ({ app, logger, python }) { logger.log('Sending code to console'); await app.workbench.positronConsole.pasteCodeToConsole(pythonScript); await app.workbench.positronConsole.sendEnterKey(); From 1492e58b74678103a84810ae453cfa65cc4662ad Mon Sep 17 00:00:00 2001 From: Jonathan Date: Mon, 2 Dec 2024 10:27:25 -0800 Subject: [PATCH 11/11] Establish Ready listener before restarting (#5554) This change fixes an issue with the R package dev workflow introduced by the new supervisor. In particular, the new supervisor's `restart` API call does not return until the restart is finished, so waiting for the `Ready` state after the restart finishes results in waiting forever (it's _already_ `Ready`). Addresses https://github.com/posit-dev/positron/issues/5532. ### QA Notes This is a minimal change that addresses the above issue, but while debugging the problem, I found another one: namely that you can do this "install and restart" thing only once or twice before it stops working. Requests to restart the kernel that are initiated from extensions are (... _sometimes_) resulting in multiple UI comms open, so RPCs called like `get_env_vars` appear to not return as they're sent to the wrong comm. So we've got more to do here, but I think this improvement is worth taking on its own. --- extensions/positron-r/src/commands.ts | 47 ++++++++++++++++++--------- extensions/positron-r/src/util.ts | 12 +++++-- 2 files changed, 42 insertions(+), 17 deletions(-) diff --git a/extensions/positron-r/src/commands.ts b/extensions/positron-r/src/commands.ts index 2d6942c1f05..9ef8880fb01 100644 --- a/extensions/positron-r/src/commands.ts +++ b/extensions/positron-r/src/commands.ts @@ -5,7 +5,7 @@ import * as vscode from 'vscode'; import * as positron from 'positron'; -import { timeout } from './util'; +import { PromiseHandles, timeout } from './util'; import { checkInstalled } from './session'; import { getRPackageName } from './contexts'; import { getRPackageTasks } from './tasks'; @@ -80,25 +80,42 @@ export async function registerCommands(context: vscode.ExtensionContext) { if (e.execution === execution) { if (e.exitCode === 0) { vscode.commands.executeCommand('workbench.panel.positronConsole.focus'); + + // A promise that resolves when the runtime is ready. + // We establish this promise before the session has + // restarted so that we can ensure that we don't miss + // the Ready state. + const promise = new PromiseHandles(); + const disp2 = session.onDidChangeRuntimeState(runtimeState => { + if (runtimeState === positron.RuntimeState.Ready) { + promise.resolve(); + disp2.dispose(); + } + }); + try { await positron.runtime.restartSession(session.metadata.sessionId); - } catch { - // If restarting promise rejects, dispose of listener: + } catch (err) { + // If restarting promise rejects, dispose of listeners, notify user, and reject. disp1.dispose(); + disp2.dispose(); + promise.reject(err); + vscode.window.showErrorMessage(vscode.l10n.t('Failed to restart R after installing R package: {0}', JSON.stringify(err))); + return; } - // A promise that resolves when the runtime is ready: - const promise = new Promise(resolve => { - const disp2 = session.onDidChangeRuntimeState(runtimeState => { - if (runtimeState === positron.RuntimeState.Ready) { - resolve(); - disp2.dispose(); - } - }); - }); - - // Wait for the the runtime to be ready, or for a timeout: - await Promise.race([promise, timeout(1e4, 'waiting for R to be ready')]); + // Wait for the the runtime to be ready, if hasn't + // already entered the Ready state. + // + // TODO(jupyter-adapter): This is a workaround for the + // fact that, when using the Jupyter Adapter, the + // restart command does not wait for the restart to be + // complete before returning. When the Jupyter Adapter + // is removed, we can rely on the runtime being ready + // as soon as the session restart call returns. + if (!promise.settled) { + await Promise.race([promise.promise, timeout(1e4, 'waiting for R to be ready')]); + } session.execute(`library(${packageName})`, randomUUID(), positron.RuntimeCodeExecutionMode.Interactive, diff --git a/extensions/positron-r/src/util.ts b/extensions/positron-r/src/util.ts index 76b20dd360a..a073ea0439b 100644 --- a/extensions/positron-r/src/util.ts +++ b/extensions/positron-r/src/util.ts @@ -8,12 +8,20 @@ import * as fs from 'fs'; export class PromiseHandles { resolve!: (value: T | Promise) => void; reject!: (error: unknown) => void; + settled: boolean; promise: Promise; constructor() { + this.settled = false; this.promise = new Promise((resolve, reject) => { - this.resolve = resolve; - this.reject = reject; + this.resolve = (val) => { + this.settled = true; + resolve(val); + }; + this.reject = (err) => { + this.settled = true; + reject(err); + }; }); } }