diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile new file mode 100644 index 000000000000..ffc0150ebac5 --- /dev/null +++ b/.devcontainer/Dockerfile @@ -0,0 +1,18 @@ +FROM mcr.microsoft.com/devcontainers/typescript-node:22-bookworm + +RUN apt-get install -y wget bzip2 + +# Run in silent mode and save downloaded script as anaconda.sh. +# Run with /bin/bash and run in silent mode to /opt/conda. +# Also get rid of installation script after finishing. +RUN wget --quiet https://repo.anaconda.com/archive/Anaconda3-2023.07-1-Linux-x86_64.sh -O ~/anaconda.sh && \ + /bin/bash ~/anaconda.sh -b -p /opt/conda && \ + rm ~/anaconda.sh + +ENV PATH="/opt/conda/bin:$PATH" + +# Sudo apt update needs to run in order for installation of fish to work . +RUN sudo apt update && \ + sudo apt install fish -y + + diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json new file mode 100644 index 000000000000..67a8833d30cf --- /dev/null +++ b/.devcontainer/devcontainer.json @@ -0,0 +1,30 @@ +// For format details, see https://aka.ms/devcontainer.json. +{ + "name": "VS Code Python Dev Container", + // Or use a Dockerfile or Docker Compose file. More info: https://containers.dev/guide/dockerfile + "build": { + "dockerfile": "./Dockerfile", + "context": ".." + }, + "customizations": { + "vscode": { + "extensions": [ + "charliermarsh.ruff", + "editorconfig.editorconfig", + "esbenp.prettier-vscode", + "dbaeumer.vscode-eslint", + "ms-python.python", + "ms-python.vscode-pylance", + "ms-python.debugpy" + ] + } + }, + // Commands to execute on container creation,start. + "postCreateCommand": "bash scripts/postCreateCommand.sh", + "onCreateCommand": "bash scripts/onCreateCommand.sh", + + "containerEnv": { + "CI_PYTHON_PATH": "/workspaces/vscode-python/.venv/bin/python" + } + +} diff --git a/.eslintignore b/.eslintignore deleted file mode 100644 index ad69cab31ea7..000000000000 --- a/.eslintignore +++ /dev/null @@ -1,271 +0,0 @@ -# The following files were grandfathered out of eslint. They can be removed as time permits. - -src/test/analysisEngineTest.ts -src/test/ciConstants.ts -src/test/common.ts -src/test/constants.ts -src/test/core.ts -src/test/extension-version.functional.test.ts -src/test/fixtures.ts -src/test/index.ts -src/test/initialize.ts -src/test/mockClasses.ts -src/test/performanceTest.ts -src/test/proc.ts -src/test/smokeTest.ts -src/test/standardTest.ts -src/test/startupTelemetry.unit.test.ts -src/test/sourceMapSupport.test.ts -src/test/sourceMapSupport.unit.test.ts -src/test/testBootstrap.ts -src/test/testLogger.ts -src/test/testRunner.ts -src/test/textUtils.ts -src/test/unittests.ts -src/test/vscode-mock.ts - -src/test/interpreters/mocks.ts -src/test/interpreters/virtualEnvs/condaInheritEnvPrompt.unit.test.ts -src/test/interpreters/pythonPathUpdaterFactory.unit.test.ts -src/test/interpreters/activation/service.unit.test.ts -src/test/interpreters/helpers.unit.test.ts -src/test/interpreters/display.unit.test.ts - -src/test/terminals/codeExecution/terminalCodeExec.unit.test.ts -src/test/terminals/codeExecution/codeExecutionManager.unit.test.ts -src/test/terminals/codeExecution/djangoShellCodeExect.unit.test.ts - -src/test/activation/activeResource.unit.test.ts -src/test/activation/extensionSurvey.unit.test.ts - -src/test/utils/fs.ts - -src/test/api.functional.test.ts - -src/test/testing/mocks.ts -src/test/testing/common/debugLauncher.unit.test.ts -src/test/testing/common/services/configSettingService.unit.test.ts - -src/test/common/exitCIAfterTestReporter.ts - - -src/test/common/terminals/activator/index.unit.test.ts -src/test/common/terminals/activator/base.unit.test.ts -src/test/common/terminals/shellDetector.unit.test.ts -src/test/common/terminals/service.unit.test.ts -src/test/common/terminals/helper.unit.test.ts -src/test/common/terminals/activation.unit.test.ts -src/test/common/terminals/shellDetectors/shellDetectors.unit.test.ts -src/test/common/terminals/environmentActivationProviders/terminalActivation.testvirtualenvs.ts - -src/test/common/socketStream.test.ts - -src/test/common/configSettings.test.ts - -src/test/common/experiments/telemetry.unit.test.ts - -src/test/common/platform/filesystem.unit.test.ts -src/test/common/platform/errors.unit.test.ts -src/test/common/platform/utils.ts -src/test/common/platform/fs-temp.unit.test.ts -src/test/common/platform/fs-temp.functional.test.ts -src/test/common/platform/filesystem.functional.test.ts -src/test/common/platform/filesystem.test.ts - -src/test/common/utils/cacheUtils.unit.test.ts -src/test/common/utils/decorators.unit.test.ts -src/test/common/utils/version.unit.test.ts - -src/test/common/configSettings/configSettings.unit.test.ts -src/test/common/serviceRegistry.unit.test.ts -src/test/common/extensions.unit.test.ts -src/test/common/variables/envVarsService.unit.test.ts -src/test/common/helpers.test.ts -src/test/common/application/commands/reloadCommand.unit.test.ts - -src/test/common/installer/channelManager.unit.test.ts -src/test/common/installer/pipInstaller.unit.test.ts -src/test/common/installer/installer.invalidPath.unit.test.ts -src/test/common/installer/pipEnvInstaller.unit.test.ts -src/test/common/installer/productPath.unit.test.ts - -src/test/common/socketCallbackHandler.test.ts - -src/test/common/process/decoder.test.ts -src/test/common/process/processFactory.unit.test.ts -src/test/common/process/pythonToolService.unit.test.ts -src/test/common/process/proc.observable.test.ts -src/test/common/process/logger.unit.test.ts -src/test/common/process/proc.exec.test.ts -src/test/common/process/pythonProcess.unit.test.ts -src/test/common/process/proc.unit.test.ts - -src/test/common/interpreterPathService.unit.test.ts - - -src/test/pythonFiles/formatting/dummy.ts - -src/test/debugger/extension/adapter/adapter.test.ts -src/test/debugger/extension/adapter/outdatedDebuggerPrompt.unit.test.ts -src/test/debugger/extension/adapter/factory.unit.test.ts -src/test/debugger/extension/adapter/activator.unit.test.ts -src/test/debugger/extension/adapter/logging.unit.test.ts -src/test/debugger/extension/hooks/childProcessAttachHandler.unit.test.ts -src/test/debugger/extension/hooks/childProcessAttachService.unit.test.ts -src/test/debugger/utils.ts -src/test/debugger/common/protocolparser.test.ts -src/test/debugger/envVars.test.ts - -src/test/telemetry/index.unit.test.ts -src/test/telemetry/envFileTelemetry.unit.test.ts - -src/test/application/diagnostics/checks/macPythonInterpreter.unit.test.ts -src/test/application/diagnostics/checks/pythonInterpreter.unit.test.ts -src/test/application/diagnostics/checks/invalidLaunchJsonDebugger.unit.test.ts -src/test/application/diagnostics/checks/powerShellActivation.unit.test.ts -src/test/application/diagnostics/checks/invalidPythonPathInDebugger.unit.test.ts -src/test/application/diagnostics/checks/envPathVariable.unit.test.ts -src/test/application/diagnostics/applicationDiagnostics.unit.test.ts -src/test/application/diagnostics/promptHandler.unit.test.ts -src/test/application/diagnostics/sourceMapSupportService.unit.test.ts -src/test/application/diagnostics/commands/ignore.unit.test.ts - -src/test/performance/load.perf.test.ts - -src/client/interpreter/configuration/interpreterSelector/commands/base.ts -src/client/interpreter/configuration/interpreterSelector/commands/resetInterpreter.ts -src/client/interpreter/configuration/pythonPathUpdaterServiceFactory.ts -src/client/interpreter/configuration/services/globalUpdaterService.ts -src/client/interpreter/configuration/services/workspaceUpdaterService.ts -src/client/interpreter/configuration/services/workspaceFolderUpdaterService.ts -src/client/interpreter/helpers.ts -src/client/interpreter/virtualEnvs/condaInheritEnvPrompt.ts -src/client/interpreter/display/index.ts - -src/client/extension.ts -src/client/sourceMapSupport.ts -src/client/startupTelemetry.ts - -src/client/terminals/codeExecution/terminalCodeExecution.ts -src/client/terminals/codeExecution/codeExecutionManager.ts -src/client/terminals/codeExecution/djangoContext.ts - -src/client/activation/commands.ts -src/client/activation/progress.ts -src/client/activation/extensionSurvey.ts -src/client/activation/common/analysisOptions.ts -src/client/activation/languageClientMiddleware.ts - -src/client/formatters/serviceRegistry.ts -src/client/formatters/helper.ts -src/client/formatters/dummyFormatter.ts -src/client/formatters/baseFormatter.ts - -src/client/testing/serviceRegistry.ts -src/client/testing/main.ts -src/client/testing/configurationFactory.ts -src/client/testing/common/constants.ts -src/client/testing/common/testUtils.ts -src/client/testing/common/socketServer.ts -src/client/testing/common/runner.ts - -src/client/common/helpers.ts -src/client/common/net/browser.ts -src/client/common/net/socket/socketCallbackHandler.ts -src/client/common/net/socket/socketServer.ts -src/client/common/net/socket/SocketStream.ts -src/client/common/editor.ts -src/client/common/contextKey.ts -src/client/common/experiments/telemetry.ts -src/client/common/platform/serviceRegistry.ts -src/client/common/platform/errors.ts -src/client/common/platform/fs-temp.ts -src/client/common/platform/fs-paths.ts -src/client/common/platform/registry.ts -src/client/common/platform/pathUtils.ts -src/client/common/persistentState.ts -src/client/common/terminal/activator/base.ts -src/client/common/terminal/activator/powershellFailedHandler.ts -src/client/common/terminal/activator/index.ts -src/client/common/terminal/helper.ts -src/client/common/terminal/syncTerminalService.ts -src/client/common/terminal/factory.ts -src/client/common/terminal/commandPrompt.ts -src/client/common/terminal/service.ts -src/client/common/terminal/shellDetector.ts -src/client/common/terminal/shellDetectors/userEnvironmentShellDetector.ts -src/client/common/terminal/shellDetectors/vscEnvironmentShellDetector.ts -src/client/common/terminal/shellDetectors/terminalNameShellDetector.ts -src/client/common/terminal/shellDetectors/settingsShellDetector.ts -src/client/common/terminal/shellDetectors/baseShellDetector.ts -src/client/common/utils/decorators.ts -src/client/common/utils/enum.ts -src/client/common/utils/platform.ts -src/client/common/utils/stopWatch.ts -src/client/common/utils/random.ts -src/client/common/utils/sysTypes.ts -src/client/common/utils/misc.ts -src/client/common/utils/cacheUtils.ts -src/client/common/utils/workerPool.ts -src/client/common/extensions.ts -src/client/common/variables/serviceRegistry.ts -src/client/common/variables/environment.ts -src/client/common/variables/types.ts -src/client/common/variables/systemVariables.ts -src/client/common/cancellation.ts -src/client/common/interpreterPathService.ts -src/client/common/application/applicationShell.ts -src/client/common/application/languageService.ts -src/client/common/application/clipboard.ts -src/client/common/application/workspace.ts -src/client/common/application/debugSessionTelemetry.ts -src/client/common/application/documentManager.ts -src/client/common/application/debugService.ts -src/client/common/application/commands/reloadCommand.ts -src/client/common/application/terminalManager.ts -src/client/common/application/applicationEnvironment.ts -src/client/common/errors/errorUtils.ts -src/client/common/installer/serviceRegistry.ts -src/client/common/installer/channelManager.ts -src/client/common/installer/moduleInstaller.ts -src/client/common/installer/types.ts -src/client/common/installer/pipEnvInstaller.ts -src/client/common/installer/productService.ts -src/client/common/installer/pipInstaller.ts -src/client/common/installer/productPath.ts -src/client/common/process/currentProcess.ts -src/client/common/process/processFactory.ts -src/client/common/process/serviceRegistry.ts -src/client/common/process/pythonToolService.ts -src/client/common/process/internal/python.ts -src/client/common/process/internal/scripts/testing_tools.ts -src/client/common/process/types.ts -src/client/common/process/logger.ts -src/client/common/process/pythonProcess.ts -src/client/common/process/pythonEnvironment.ts -src/client/common/process/decoder.ts - - -src/client/debugger/extension/adapter/remoteLaunchers.ts -src/client/debugger/extension/adapter/outdatedDebuggerPrompt.ts -src/client/debugger/extension/adapter/factory.ts -src/client/debugger/extension/adapter/activator.ts -src/client/debugger/extension/adapter/logging.ts -src/client/debugger/extension/hooks/eventHandlerDispatcher.ts -src/client/debugger/extension/hooks/childProcessAttachService.ts -src/client/debugger/extension/attachQuickPick/wmicProcessParser.ts -src/client/debugger/extension/attachQuickPick/factory.ts -src/client/debugger/extension/attachQuickPick/psProcessParser.ts -src/client/debugger/extension/attachQuickPick/picker.ts - -src/client/application/serviceRegistry.ts -src/client/application/diagnostics/surceMapSupportService.ts -src/client/application/diagnostics/base.ts -src/client/application/diagnostics/applicationDiagnostics.ts -src/client/application/diagnostics/filter.ts -src/client/application/diagnostics/promptHandler.ts -src/client/application/diagnostics/commands/base.ts -src/client/application/diagnostics/commands/ignore.ts -src/client/application/diagnostics/commands/factory.ts -src/client/application/diagnostics/commands/execVSCCommand.ts -src/client/application/diagnostics/commands/launchBrowser.ts diff --git a/.eslintplugin/no-bad-gdpr-comment.js b/.eslintplugin/no-bad-gdpr-comment.js new file mode 100644 index 000000000000..786259683ff6 --- /dev/null +++ b/.eslintplugin/no-bad-gdpr-comment.js @@ -0,0 +1,51 @@ +"use strict"; +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +Object.defineProperty(exports, "__esModule", { value: true }); +var noBadGDPRComment = { + create: function (context) { + var _a; + return _a = {}, + _a['Program'] = function (node) { + for (var _i = 0, _a = node.comments; _i < _a.length; _i++) { + var comment = _a[_i]; + if (comment.type !== 'Block' || !comment.loc) { + continue; + } + if (!comment.value.includes('__GDPR__')) { + continue; + } + var dataStart = comment.value.indexOf('\n'); + var data = comment.value.substring(dataStart); + var gdprData = void 0; + try { + var jsonRaw = "{ ".concat(data, " }"); + gdprData = JSON.parse(jsonRaw); + } + catch (e) { + context.report({ + loc: { start: comment.loc.start, end: comment.loc.end }, + message: 'GDPR comment is not valid JSON', + }); + } + if (gdprData) { + var len = Object.keys(gdprData).length; + if (len !== 1) { + context.report({ + loc: { start: comment.loc.start, end: comment.loc.end }, + message: "GDPR comment must contain exactly one key, not ".concat(Object.keys(gdprData).join(', ')), + }); + } + } + } + }, + _a; + }, +}; +module.exports = { + rules: { + 'no-bad-gdpr-comment': noBadGDPRComment, // Ensure correct structure + }, +}; diff --git a/.eslintplugin/no-bad-gdpr-comment.ts b/.eslintplugin/no-bad-gdpr-comment.ts new file mode 100644 index 000000000000..1eba899a7de3 --- /dev/null +++ b/.eslintplugin/no-bad-gdpr-comment.ts @@ -0,0 +1,55 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as eslint from 'eslint'; +const noBadGDPRComment: eslint.Rule.RuleModule = { + create(context: eslint.Rule.RuleContext): eslint.Rule.RuleListener { + return { + ['Program'](node) { + for (const comment of (node as eslint.AST.Program).comments) { + if (comment.type !== 'Block' || !comment.loc) { + continue; + } + if (!comment.value.includes('__GDPR__')) { + continue; + } + + const dataStart = comment.value.indexOf('\n'); + const data = comment.value.substring(dataStart); + + let gdprData: { [key: string]: object } | undefined; + + try { + const jsonRaw = `{ ${data} }`; + gdprData = JSON.parse(jsonRaw); + } catch (e) { + context.report({ + loc: { start: comment.loc.start, end: comment.loc.end }, + message: 'GDPR comment is not valid JSON', + }); + } + + if (gdprData) { + const len = Object.keys(gdprData).length; + if (len !== 1) { + context.report({ + loc: { start: comment.loc.start, end: comment.loc.end }, + message: `GDPR comment must contain exactly one key, not ${Object.keys(gdprData).join( + ', ', + )}`, + }); + } + } + } + }, + }; + }, +}; + +module.exports = { + rules: { + 'no-bad-gdpr-comment': noBadGDPRComment, // Ensure correct structure + }, +}; diff --git a/.eslintrc b/.eslintrc deleted file mode 100644 index 62e2aa6c52ba..000000000000 --- a/.eslintrc +++ /dev/null @@ -1,101 +0,0 @@ -{ - "env": { - "node": true, - "es6": true, - "mocha": true - }, - "parser": "@typescript-eslint/parser", - "plugins": ["@typescript-eslint"], - "extends": [ - "airbnb", - "plugin:@typescript-eslint/recommended", - "plugin:import/errors", - "plugin:import/warnings", - "plugin:import/typescript", - "prettier" - ], - "rules": { - // Overriding ESLint rules with Typescript-specific ones - "@typescript-eslint/ban-ts-comment": [ - "error", - { - "ts-ignore": "allow-with-description" - } - ], - "@typescript-eslint/explicit-module-boundary-types": "error", - "no-bitwise": "off", - "no-dupe-class-members": "off", - "@typescript-eslint/no-dupe-class-members": "error", - "no-empty-function": "off", - "@typescript-eslint/no-empty-function": ["error"], - "@typescript-eslint/no-empty-interface": "off", - "@typescript-eslint/no-explicit-any": "error", - "@typescript-eslint/no-non-null-assertion": "off", - "no-unused-vars": "off", - "@typescript-eslint/no-unused-vars": [ - "error", - { - "args": "after-used", - "argsIgnorePattern": "^_" - } - ], - "no-use-before-define": "off", - "@typescript-eslint/no-use-before-define": [ - "error", - { - "functions": false - } - ], - "no-useless-constructor": "off", - "@typescript-eslint/no-useless-constructor": "error", - "@typescript-eslint/no-var-requires": "off", - - // Other rules - "class-methods-use-this": ["error", {"exceptMethods": ["dispose"]}], - "func-names": "off", - "import/extensions": "off", - "import/namespace": "off", - "import/no-extraneous-dependencies": "off", - "import/no-unresolved": [ - "error", - { - "ignore": ["monaco-editor", "vscode"] - } - ], - "import/prefer-default-export": "off", - "linebreak-style": "off", - "no-await-in-loop": "off", - "no-console": "off", - "no-control-regex": "off", - "no-extend-native": "off", - "no-multi-str": "off", - "no-param-reassign": "off", - "no-prototype-builtins": "off", - "no-restricted-syntax": [ - "error", - { - "selector": "ForInStatement", - "message": "for..in loops iterate over the entire prototype chain, which is virtually never what you want. Use Object.{keys,values,entries}, and iterate over the resulting array." - }, - { - "selector": "LabeledStatement", - "message": "Labels are a form of GOTO; using them makes code confusing and hard to maintain and understand." - }, - { - "selector": "WithStatement", - "message": "`with` is disallowed in strict mode because it makes code impossible to predict and optimize." - } - ], - "no-template-curly-in-string": "off", - "no-underscore-dangle": "off", - "no-useless-escape": "off", - "no-void": [ - "error", - { - "allowAsStatement": true - } - ], - "operator-assignment": "off", - "strict": "off" - } -} diff --git a/.git-blame-ignore-revs b/.git-blame-ignore-revs new file mode 100644 index 000000000000..e2c2a50781b9 --- /dev/null +++ b/.git-blame-ignore-revs @@ -0,0 +1,15 @@ +# Prettier +2b6a8f2d439fe9d5e66665ea46d8b690ac9b2c39 +649156a09ccdc51c0d20f7cd44540f1918f9347b +4f774d94bf4fbf87bb417b2b2b8e79e334eb3536 +61b179b2092050709e3c373a6738abad8ce581c4 +c33617b0b98daeb4d72040b48c5850b476d6256c +db8e1e2460e9754ec0672d958789382b6d15c5aa +08bc9ad3bee5b19f02fa756fbc53ab32f1b39920 +# Black +a58eeffd1b64498e2afe5f11597888dfd1c8699c +5cd8f539f4d2086b718c8f11f823c0ac12fc2c49 +9ec9e9eaebb25adc6d942ac19d4d6c128abb987f +c4af91e090057d20d7a633b3afa45eaa13ece76f +# Ruff +e931bed3efbede7b05113316506958ecd7506777 diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml index eaacc33b8d8d..c966f6bde856 100644 --- a/.github/ISSUE_TEMPLATE/config.yml +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -9,9 +9,9 @@ contact_links: - name: 'Jupyter' url: https://github.com/microsoft/vscode-jupyter/issues about: 'For issues relating to the Jupyter extension (including the interactive window)' - - name: 'Debugpy' - url: https://github.com/microsoft/debugpy/issues - about: 'For issues relating to the debugpy debugger' + - name: 'Python Debugger' + url: https://github.com/microsoft/vscode-python-debugger/issues + about: 'For issues relating to the Python debugger' - name: Help/Support url: https://github.com/microsoft/vscode-python/discussions/categories/q-a about: 'Having trouble with the extension? Need help getting something to work?' diff --git a/.github/actions/build-vsix/action.yml b/.github/actions/build-vsix/action.yml index b84d3e0871f0..912ff2c34a74 100644 --- a/.github/actions/build-vsix/action.yml +++ b/.github/actions/build-vsix/action.yml @@ -11,26 +11,34 @@ inputs: artifact_name: description: 'Name to give the artifact containing the VSIX' required: true + cargo_target: + description: 'Cargo build target for the native build' + required: true + vsix_target: + description: 'vsix build target for the native build' + required: true runs: using: 'composite' steps: - name: Install Node - uses: actions/setup-node@v2 + uses: actions/setup-node@v6 with: node-version: ${{ inputs.node_version }} cache: 'npm' + - name: Rust Tool Chain setup + uses: dtolnay/rust-toolchain@stable + # Jedi LS depends on dataclasses which is not in the stdlib in Python 3.7. - - name: Use Python 3.7 for JediLSP - uses: actions/setup-python@v2 + - name: Use Python 3.10 for JediLSP + uses: actions/setup-python@v6 with: - python-version: 3.7 + python-version: '3.10' cache: 'pip' cache-dependency-path: | requirements.txt - build/debugger-install-requirements.txt - pythonFiles/jedilsp_requirements/requirements.txt + python_files/jedilsp_requirements/requirements.txt - name: Upgrade Pip run: python -m pip install -U pip @@ -38,52 +46,54 @@ runs: # For faster/better builds of sdists. - name: Install build pre-requisite - run: python -m pip install wheel + run: python -m pip install wheel nox shell: bash - - name: Install Python dependencies - uses: brettcannon/pip-secure-install@v1 - with: - options: '-t ./pythonFiles/lib/python --implementation py' + - name: Install Python Extension dependencies (jedi, etc.) + run: nox --session install_python_libs + shell: bash - - name: Install debugpy - run: | - python -m pip --disable-pip-version-check install -r build/debugger-install-requirements.txt - python ./pythonFiles/install_debugpy.py + - name: Add Rustup target + run: rustup target add "${CARGO_TARGET}" shell: bash + env: + CARGO_TARGET: ${{ inputs.cargo_target }} - - name: Install Jedi LSP - uses: brettcannon/pip-secure-install@v1 - with: - requirements-file: './pythonFiles/jedilsp_requirements/requirements.txt' - options: '-t ./pythonFiles/lib/jedilsp --implementation py --platform any --abi none' + - name: Build Native Binaries + run: nox --session native_build + shell: bash + env: + CARGO_TARGET: ${{ inputs.cargo_target }} - name: Run npm ci run: npm ci --prefer-offline shell: bash - # Use the GITHUB_RUN_ID environment variable to update the build number. - # GITHUB_RUN_ID is a unique number for each run within a repository. - # This number does not change if you re-run the workflow run. - - name: Update extension build number - run: npm run updateBuildNumber -- --buildNumber $GITHUB_RUN_ID - shell: bash - - name: Update optional extension dependencies run: npm run addExtensionPackDependencies shell: bash + - name: Build Webpack + run: | + npx gulp clean + npx gulp prePublishBundle + shell: bash + - name: Build VSIX - run: npm run package + run: npx vsce package --target "${VSIX_TARGET}" --out ms-python-insiders.vsix --pre-release shell: bash + env: + VSIX_TARGET: ${{ inputs.vsix_target }} - name: Rename VSIX # Move to a temp name in case the specified name happens to match the default name. - run: mv ms-python-insiders.vsix ms-python-temp.vsix && mv ms-python-temp.vsix ${{ inputs.vsix_name }} + run: mv ms-python-insiders.vsix ms-python-temp.vsix && mv ms-python-temp.vsix "${VSIX_NAME}" shell: bash + env: + VSIX_NAME: ${{ inputs.vsix_name }} - name: Upload VSIX - uses: actions/upload-artifact@v2 + uses: actions/upload-artifact@v7 with: name: ${{ inputs.artifact_name }} path: ${{ inputs.vsix_name }} diff --git a/.github/actions/lint/action.yml b/.github/actions/lint/action.yml index 9478550c107b..0bd5a2d8e1e2 100644 --- a/.github/actions/lint/action.yml +++ b/.github/actions/lint/action.yml @@ -10,7 +10,7 @@ runs: using: 'composite' steps: - name: Install Node - uses: actions/setup-node@v2 + uses: actions/setup-node@v6 with: node-version: ${{ inputs.node_version }} cache: 'npm' @@ -36,14 +36,15 @@ runs: shell: bash - name: Install Python - uses: actions/setup-python@v2 + uses: actions/setup-python@v6 with: python-version: '3.x' cache: 'pip' - - name: Check Python format + - name: Run Ruff run: | - python -m pip install -U black - python -m black . --check - working-directory: pythonFiles + python -m pip install -U "ruff" + python -m ruff check . + python -m ruff format --check + working-directory: python_files shell: bash diff --git a/.github/actions/smoke-tests/action.yml b/.github/actions/smoke-tests/action.yml index 9ad6e87cdd26..0531ef5d42a3 100644 --- a/.github/actions/smoke-tests/action.yml +++ b/.github/actions/smoke-tests/action.yml @@ -13,44 +13,37 @@ runs: using: 'composite' steps: - name: Install Node - uses: actions/setup-node@v2 + uses: actions/setup-node@v4 with: node-version: ${{ inputs.node_version }} cache: 'npm' - name: Install Python - uses: actions/setup-python@v2 + uses: actions/setup-python@v5 with: python-version: '3.x' cache: 'pip' cache-dependency-path: | build/test-requirements.txt requirements.txt - build/smoke-test-requirements.txt - name: Install dependencies (npm ci) run: npm ci --prefer-offline shell: bash - name: Install Python requirements - uses: brettcannon/pip-secure-install@v1 + uses: brettcannon/pip-secure-install@92f400e3191171c1858cc0e0d9ac6320173fdb0c # v1.0.0 with: - options: '-t ./pythonFiles/lib/python --implementation py' + options: '-t ./python_files/lib/python --implementation py' - name: pip install system test requirements run: | python -m pip install --upgrade -r build/test-requirements.txt - python -m pip --disable-pip-version-check install -t ./pythonFiles/lib/python --implementation py --no-deps --upgrade --pre debugpy - shell: bash - - - name: pip install smoke test requirements - run: | - python -m pip install --upgrade -r build/smoke-test-requirements.txt shell: bash # Bits from the VSIX are reused by smokeTest.ts to speed things up. - name: Download VSIX - uses: actions/download-artifact@v2 + uses: actions/download-artifact@v4 with: name: ${{ inputs.artifact_name }} @@ -68,6 +61,6 @@ runs: env: DISPLAY: 10 INSTALL_JUPYTER_EXTENSION: true - uses: GabrielBB/xvfb-action@v1.5 + uses: GabrielBB/xvfb-action@b706e4e27b14669b486812790492dc50ca16b465 # v1.7 with: run: node --no-force-async-hooks-checks ./out/test/smokeTest.js diff --git a/.github/commands.json b/.github/commands.json new file mode 100644 index 000000000000..171f33f380c3 --- /dev/null +++ b/.github/commands.json @@ -0,0 +1,166 @@ +[ + { + "type": "label", + "name": "*question", + "action": "close", + "reason": "not_planned", + "comment": "We closed this issue because it is a question about using the Python extension for VS Code rather than an issue or feature request. We recommend browsing resources such as our [Python documentation](https://code.visualstudio.com/docs/languages/python) and our [Discussions page](https://github.com/microsoft/vscode-python/discussions). You may also find help on [StackOverflow](https://stackoverflow.com/questions/tagged/vscode-python), where the community has already answered thousands of similar questions. \n\nHappy Coding!" + }, + { + "type": "label", + "name": "*dev-question", + "action": "close", + "reason": "not_planned", + "comment": "We have a great extension developer community over on [GitHub discussions](https://github.com/microsoft/vscode-discussions/discussions) and [Slack](https://vscode-dev-community.slack.com/) where extension authors help each other. This is a great place for you to ask questions and find support.\n\nHappy Coding!" + }, + { + "type": "label", + "name": "*extension-candidate", + "action": "close", + "reason": "not_planned", + "comment": "We try to keep the Python extension lean and we think the functionality you're asking for is great for a VS Code extension. You might be able to find one that suits you in the [VS Code Marketplace](https://aka.ms/vscodemarketplace) already. If not, in a few simple steps you can get started [writing your own extension](https://aka.ms/vscodewritingextensions) or leverage our [tool extension template](https://github.com/microsoft/vscode-python-tools-extension-template) to get started. In addition, check out the [vscode-python-environments](https://github.com/microsoft/vscode-python-environments) as this may be the right spot for your request. \n\nHappy Coding!" + }, + { + "type": "label", + "name": "*not-reproducible", + "action": "close", + "reason": "not_planned", + "comment": "We closed this issue because we are unable to reproduce the problem with the steps you describe. Chances are we've already fixed your problem in a recent version of the Python extension, so we recommend updating to the latest version and trying again. If you continue to experience this issue, please ask us to reopen the issue and provide us with more detail.\n\nHappy Coding!" + }, + { + "type": "label", + "name": "*out-of-scope", + "action": "close", + "reason": "not_planned", + "comment": "We closed this issue because we [don't plan to address it](https://github.com/microsoft/vscode-python/wiki/Issue-Management#criteria-for-closing-out-of-scope-feature-requests) in the foreseeable future. If you disagree and feel that this issue is crucial: we are happy to listen and to reconsider.\n\nIf you wonder what we are up to, please see our [roadmap](https://aka.ms/pythonvscoderoadmap) and [issue reporting guidelines]( https://github.com/microsoft/vscode-python/wiki/Issue-Management).\n\nThanks for your understanding, and happy coding!" + }, + { + "type": "label", + "name": "wont-fix", + "action": "close", + "reason": "not_planned", + "comment": "We closed this issue because we [don't plan to address it](https://github.com/microsoft/vscode/wiki/Issue-Grooming#wont-fix-bugs).\n\nThanks for your understanding, and happy coding!" + }, + { + "type": "label", + "name": "*caused-by-extension", + "action": "close", + "reason": "not_planned", + "comment": "This issue is caused by an extension, please file it with the repository (or contact) the extension has linked in its overview in VS Code or the [marketplace](https://aka.ms/vscodemarketplace) for VS Code. See also our [issue reporting guidelines](https://aka.ms/vscodeissuereporting). If you don't know which extension is causing the problem, you can run `Help: Start extension bisect` from the command palette (F1) to help identify the problem extension.\n\nHappy Coding!" + }, + { + "type": "label", + "name": "*as-designed", + "action": "close", + "reason": "not_planned", + "comment": "The described behavior is how it is expected to work. If you disagree, please explain what is expected and what is not in more detail. See also our [issue reporting guidelines](https://aka.ms/vscodeissuereporting).\n\nHappy Coding!" + }, + { + "type": "label", + "name": "L10N", + "assign": [ + "csigs", + "TylerLeonhardt" + ] + }, + { + "type": "label", + "name": "*duplicate", + "action": "close", + "reason": "not_planned", + "comment": "Thanks for creating this issue! We figured it's covering the same as another one we already have. Thus, we closed this one as a duplicate. You can search for [similar existing issues](${duplicateQuery}). See also our [issue reporting guidelines](https://aka.ms/vscodeissuereporting).\n\nHappy Coding!" + }, + { + "type": "comment", + "name": "verified", + "allowUsers": [ + "@author" + ], + "action": "updateLabels", + "addLabel": "verified", + "removeLabel": "author-verification-requested", + "requireLabel": "author-verification-requested", + "disallowLabel": "unreleased" + }, + { + "type": "comment", + "name": "confirm", + "allowUsers": [ + "cleidigh", + "usernamehw", + "gjsjohnmurray", + "IllusionMH" + ], + "action": "updateLabels", + "addLabel": "confirmed", + "removeLabel": "confirmation-pending" + }, + { + "type": "label", + "name": "*off-topic", + "action": "close", + "reason": "not_planned", + "comment": "Thanks for creating this issue. We think this issue is unactionable or unrelated to the goals of this project. Please follow our [issue reporting guidelines](https://aka.ms/vscodeissuereporting).\n\nHappy Coding!" + }, + { + "type": "comment", + "name": "gifPlease", + "allowUsers": [ + "cleidigh", + "usernamehw", + "gjsjohnmurray", + "IllusionMH" + ], + "action": "comment", + "addLabel": "info-needed", + "comment": "Thanks for reporting this issue! Unfortunately, it's hard for us to understand what issue you're seeing. Please help us out by providing a screen recording showing exactly what isn't working as expected. While we can work with most standard formats, `.gif` files are preferred as they are displayed inline on GitHub. You may find https://gifcap.dev helpful as a browser-based gif recording tool.\n\nIf the issue depends on keyboard input, you can help us by enabling screencast mode for the recording (`Developer: Toggle Screencast Mode` in the command palette). Lastly, please attach this file via the GitHub web interface as emailed responses will strip files out from the issue.\n\nHappy coding!" + }, + { + "type": "label", + "name": "*workspace-trust-docs", + "action": "close", + "reason": "not_planned", + "comment": "This issue appears to be the result of the new workspace trust feature shipped in June 2021. This security-focused feature has major impact on the functionality of VS Code. Due to the volume of issues, we ask that you take some time to review our [comprehensive documentation](https://aka.ms/vscode-workspace-trust) on the feature. If your issue is still not resolved, please let us know." + }, + { + "type": "label", + "name": "~verification-steps-needed", + "action": "updateLabels", + "addLabel": "verification-steps-needed", + "removeLabel": "~verification-steps-needed", + "comment": "Friendly ping! Looks like this issue requires some further steps to be verified. Please provide us with the steps necessary to verify this issue." + }, + { + "type": "label", + "name": "~info-needed", + "action": "updateLabels", + "addLabel": "info-needed", + "removeLabel": "~info-needed", + "comment": "Thanks for creating this issue! We figured it's missing some basic information or in some other way doesn't follow our [issue reporting guidelines](https://aka.ms/pvsc-bug). Please take the time to review these and update the issue or even open a new one with the Report Issue command in VS Code (**Help > Report Issue**) to have all the right information collected for you.\n\nHappy Coding!" + }, + { + "type": "label", + "name": "~version-info-needed", + "action": "updateLabels", + "addLabel": "info-needed", + "removeLabel": "~version-info-needed", + "comment": "Thanks for creating this issue! We figured it's missing some basic information, such as a version number, or in some other way doesn't follow our issue reporting guidelines. Please take the time to review these and update the issue or even open a new one with the Report Issue command in VS Code (**Help > Report Issue**) to have all the right information collected for you.\n\nHappy Coding!" + }, + { + "type": "label", + "name": "~confirmation-needed", + "action": "updateLabels", + "addLabel": "info-needed", + "removeLabel": "~confirmation-needed", + "comment": "Please diagnose the root cause of the issue by running the command `F1 > Help: Troubleshoot Issue` and following the instructions. Once you have done that, please update the issue with the results.\n\nHappy Coding!" + }, + { + "type": "label", + "name": "~spam", + "removeLabel": "~spam", + "addLabel": "spam", + "action": "close", + "reason": "not_planned", + "comment": "Thank you for your submission. This issue has been closed as it doesn't meet our community guidelines or appears to be spam.\n\n**If you believe this was closed in error:**\n- Please review our [Code of Conduct](https://opensource.microsoft.com/codeofconduct/)\n- Ensure your issue contains a clear description of the problem or feature request\n- Feel free to open a new issue with appropriate detail if this was a legitimate concern\n\n**For legitimate issues, please include:**\n- Clear description of the problem\n- Steps to reproduce (for bugs)\n- Expected vs actual behavior\n- VS Code version and environment details\n\nThank you for helping us maintain a welcoming and productive community." + } +] diff --git a/.github/dependabot.yml b/.github/dependabot.yml index d54cf6b74a53..14c8e18d475d 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -7,6 +7,27 @@ updates: labels: - 'no-changelog' + - package-ecosystem: 'github-actions' + directory: .github/actions/build-vsix + schedule: + interval: daily + labels: + - 'no-changelog' + + - package-ecosystem: 'github-actions' + directory: .github/actions/lint + schedule: + interval: daily + labels: + - 'no-changelog' + + - package-ecosystem: 'github-actions' + directory: .github/actions/smoke-test + schedule: + interval: daily + labels: + - 'no-changelog' + # Not skipping the news for some Python dependencies in case it's actually useful to communicate to users. - package-ecosystem: 'pip' directory: / @@ -16,7 +37,6 @@ updates: - dependency-name: prospector # Due to Python 2.7 and #14477. - dependency-name: pytest # Due to Python 2.7 and #13776. - dependency-name: py # Due to Python 2.7. - - dependency-name: isort - dependency-name: jedi-language-server labels: - 'no-changelog' diff --git a/.github/instructions/learning.instructions.md b/.github/instructions/learning.instructions.md new file mode 100644 index 000000000000..28b085f486ce --- /dev/null +++ b/.github/instructions/learning.instructions.md @@ -0,0 +1,34 @@ +--- +applyTo: '**' +description: This document describes how to deal with learnings that you make. (meta instruction) +--- + +This document describes how to deal with learnings that you make. +It is a meta-instruction file. + +Structure of learnings: + +- Each instruction file has a "Learnings" section. +- Each learning has a counter that indicates how often that learning was useful (initially 1). +- Each learning has a 1 sentence description of the learning that is clear and concise. + +Example: + +```markdown +## Learnings + +- Prefer `const` over `let` whenever possible (1) +- Avoid `any` type (3) +``` + +When the user tells you "learn!", you should: + +- extract a learning from the recent conversation + _ identify the problem that you created + _ identify why it was a problem + _ identify how you were told to fix it/how the user fixed it + _ generate only one learning (1 sentence) that helps to summarize the insight gained +- then, add the reflected learning to the "Learnings" section of the most appropriate instruction file + +Important: Whenever a learning was really useful, increase the counter!! +When a learning was not useful and just caused more problems, decrease the counter. diff --git a/.github/instructions/pytest-json-test-builder.instructions.md b/.github/instructions/pytest-json-test-builder.instructions.md new file mode 100644 index 000000000000..436bce0c9cd8 --- /dev/null +++ b/.github/instructions/pytest-json-test-builder.instructions.md @@ -0,0 +1,126 @@ +--- +applyTo: 'python_files/tests/pytestadapter/test_discovery.py' +description: 'A guide for adding new tests for pytest discovery and JSON formatting in the test_pytest_collect suite.' +--- + +# How to Add New Pytest Discovery Tests + +This guide explains how to add new tests for pytest discovery and JSON formatting in the `test_pytest_collect` suite. Follow these steps to ensure your tests are consistent and correct. + +--- + +## 1. Add Your Test File + +- Place your new test file/files in the appropriate subfolder under: + ``` + python_files/tests/pytestadapter/.data/ + ``` +- Organize folders and files to match the structure you want to test. For example, to test nested folders, create the corresponding directory structure. +- In your test file, mark each test function with a comment: + ```python + def test_function(): # test_marker--test_function + ... + ``` + +**Root Node Matching:** + +- The root node in your expected output must match the folder or file you pass to pytest discovery. For example, if you run discovery on a subfolder, the root `"name"`, `"path"`, and `"id_"` in your expected output should be that subfolder, not the parent `.data` folder. +- Only use `.data` as the root if you are running discovery on the entire `.data` folder. + +**Example:** +If you run: + +```python +helpers.runner([os.fspath(TEST_DATA_PATH / "myfolder"), "--collect-only"]) +``` + +then your expected output root should be: + +```python +{ + "name": "myfolder", + "path": os.fspath(TEST_DATA_PATH / "myfolder"), + "type_": "folder", + ... +} +``` + +--- + +## 2. Update `expected_discovery_test_output.py` + +- Open `expected_discovery_test_output.py` in the same test suite. +- Add a new expected output dictionary for your test file, following the format of existing entries. +- Use the helper functions and path conventions: + - Use `os.fspath()` for all paths. + - Use `find_test_line_number("function_name", file_path)` for the `lineno` field. + - Use `get_absolute_test_id("relative_path::function_name", file_path)` for `id_` and `runID`. + - Always use current path concatenation (e.g., `TEST_DATA_PATH / "your_folder" / "your_file.py"`). + - Create new constants as needed to keep the code clean and maintainable. + +**Important:** + +- Do **not** read the entire `expected_discovery_test_output.py` file if you only need to add or reference a single constant. This file is very large; prefer searching for the relevant section or appending to the end. + +**Example:** +If you run discovery on a subfolder: + +```python +helpers.runner([os.fspath(TEST_DATA_PATH / "myfolder"), "--collect-only"]) +``` + +then your expected output root should be: + +```python +myfolder_path = TEST_DATA_PATH / "myfolder" +my_expected_output = { + "name": "myfolder", + "path": os.fspath(myfolder_path), + "type_": "folder", + ... +} +``` + +- Add a comment above your dictionary describing the structure, as in the existing examples. + +--- + +## 3. Add Your Test to `test_discovery.py` + +- In `test_discovery.py`, add your new test as a parameterized case to the main `test_pytest_collect` function. Do **not** create a standalone test function for new discovery cases. +- Reference your new expected output constant from `expected_discovery_test_output.py`. + +**Example:** + +```python +@pytest.mark.parametrize( + ("file", "expected_const"), + [ + ("myfolder", my_expected_output), + # ... other cases ... + ], +) +def test_pytest_collect(file, expected_const): + ... +``` + +--- + +## 4. Run and Verify + +- Run the test suite to ensure your new test is discovered and passes. +- If the test fails, check your expected output dictionary for path or structure mismatches. + +--- + +## 5. Tips + +- Always use the helper functions for line numbers and IDs. +- Match the folder/file structure in `.data` to the expected JSON structure. +- Use comments to document the expected output structure for clarity. +- Ensure all `"path"` and `"id_"` fields in your expected output match exactly what pytest returns, including absolute paths and root node structure. + +--- + +**Reference:** +See `expected_discovery_test_output.py` for more examples and formatting. Use search or jump to the end of the file to avoid reading the entire file when possible. diff --git a/.github/instructions/python-quality-checks.instructions.md b/.github/instructions/python-quality-checks.instructions.md new file mode 100644 index 000000000000..48f37529dfbc --- /dev/null +++ b/.github/instructions/python-quality-checks.instructions.md @@ -0,0 +1,97 @@ +--- +applyTo: 'python_files/**' +description: Guide for running and fixing Python quality checks (Ruff and Pyright) that run in CI +--- + +# Python Quality Checks β€” Ruff and Pyright + +Run the same Python quality checks that run in CI. All checks target `python_files/` and use config from `python_files/pyproject.toml`. + +## Commands + +```bash +npm run check-python # Run both Ruff and Pyright +npm run check-python:ruff # Linting and formatting only +npm run check-python:pyright # Type checking only +``` + +## Fixing Ruff Errors + +**Auto-fix most issues:** + +```bash +cd python_files +python -m ruff check . --fix +python -m ruff format +npm run check-python:ruff # Verify +``` + +**Manual fixes:** + +- Ruff shows file, line number, rule code (e.g., `F841`), and description +- Open the file, read the error, fix the code +- Common: line length (100 char max), import sorting, unused variables + +## Fixing Pyright Errors + +**Common patterns and fixes:** + +- **Undefined variable/import**: Add the missing import +- **Type mismatch**: Correct the type or add type annotations +- **Missing return type**: Add `-> ReturnType` to function signatures + ```python + def my_function() -> str: # Add return type + return "result" + ``` + +**Verify:** + +```bash +npm run check-python:pyright +``` + +## Configuration + +- **Ruff**: Line length 100, Python 3.9+, 40+ rule families (flake8, isort, pyupgrade, etc.) +- **Pyright**: Version 1.1.308 (or whatever is found in the environment), ignores `lib/` and 15+ legacy files +- Config: `python_files/pyproject.toml` sections `[tool.ruff]` and `[tool.pyright]` + +## Troubleshooting + +**"Module not found" in Pyright**: Install dependencies + +```bash +python -m pip install --upgrade -r build/test-requirements.txt +nox --session install_python_libs +``` + +**Import order errors**: Auto-fix with `ruff check . --fix` + +**Type errors in ignored files**: Legacy files in `pyproject.toml` ignore listβ€”fix if working on them + +## When Writing Tests + +**Always format your test files before committing:** + +```bash +cd python_files +ruff format tests/ # Format all test files +# or format specific files: +ruff format tests/unittestadapter/test_utils.py +``` + +**Best practice workflow:** + +1. Write your test code +2. Run `ruff format` on the test files +3. Run the tests to verify they pass +4. Run `npm run check-python` to catch any remaining issues + +This ensures your tests pass both functional checks and quality checks in CI. + +## Learnings + +- Always run `npm run check-python` before pushing to catch CI failures early (1) +- Use `ruff check . --fix` to auto-fix most linting issues before manual review (1) +- Pyright version must match CI (1.1.308) to avoid inconsistent results between local and CI runs (1) +- Always run `ruff format` on test files after writing them to avoid formatting CI failures (1) diff --git a/.github/instructions/testing-workflow.instructions.md b/.github/instructions/testing-workflow.instructions.md new file mode 100644 index 000000000000..844946404328 --- /dev/null +++ b/.github/instructions/testing-workflow.instructions.md @@ -0,0 +1,581 @@ +--- +applyTo: '**/test/**' +--- + +# AI Testing Workflow Guide: Write, Run, and Fix Tests + +This guide provides comprehensive instructions for AI agents on the complete testing workflow: writing tests, running them, diagnosing failures, and fixing issues. Use this guide whenever working with test files or when users request testing tasks. + +## Complete Testing Workflow + +This guide covers the full testing lifecycle: + +1. **πŸ“ Writing Tests** - Create comprehensive test suites +2. **▢️ Running Tests** - Execute tests using VS Code tools +3. **πŸ” Diagnosing Issues** - Analyze failures and errors +4. **πŸ› οΈ Fixing Problems** - Resolve compilation and runtime issues +5. **βœ… Validation** - Ensure coverage and resilience + +### When to Use This Guide + +**User Requests Testing:** + +- "Write tests for this function" +- "Run the tests" +- "Fix the failing tests" +- "Test this code" +- "Add test coverage" + +**File Context Triggers:** + +- Working in `**/test/**` directories +- Files ending in `.test.ts` or `.unit.test.ts` +- Test failures or compilation errors +- Coverage reports or test output analysis + +## Test Types + +When implementing tests as an AI agent, choose between two main types: + +### Unit Tests (`*.unit.test.ts`) + +- **Fast isolated testing** - Mock all external dependencies +- **Use for**: Pure functions, business logic, data transformations +- **Execute with**: `runTests` tool with specific file patterns +- **Mock everything** - VS Code APIs automatically mocked via `/src/test/unittests.ts` + +### Extension Tests (`*.test.ts`) + +- **Full VS Code integration** - Real environment with actual APIs +- **Use for**: Command registration, UI interactions, extension lifecycle +- **Execute with**: VS Code launch configurations or `runTests` tool +- **Slower but comprehensive** - Tests complete user workflows + +## πŸ€– Agent Tool Usage for Test Execution + +### Primary Tool: `runTests` + +Use the `runTests` tool to execute tests programmatically rather than terminal commands for better integration and result parsing: + +```typescript +// Run specific test files +await runTests({ + files: ['/absolute/path/to/test.unit.test.ts'], + mode: 'run', +}); + +// Run tests with coverage +await runTests({ + files: ['/absolute/path/to/test.unit.test.ts'], + mode: 'coverage', + coverageFiles: ['/absolute/path/to/source.ts'], +}); + +// Run specific test names +await runTests({ + files: ['/absolute/path/to/test.unit.test.ts'], + testNames: ['should handle edge case', 'should validate input'], +}); +``` + +### Compilation Requirements + +Before running tests, ensure compilation. Always start compilation with `npm run watch-tests` before test execution to ensure TypeScript files are built. Recompile after making import/export changes before running tests, as stubs won't work if they're applied to old compiled JavaScript that doesn't have the updated imports: + +```typescript +// Start watch mode for auto-compilation +await run_in_terminal({ + command: 'npm run watch-tests', + isBackground: true, + explanation: 'Start test compilation in watch mode', +}); + +// Or compile manually +await run_in_terminal({ + command: 'npm run compile-tests', + isBackground: false, + explanation: 'Compile TypeScript test files', +}); +``` + +### Alternative: Terminal Execution + +For targeted test runs when `runTests` tool is unavailable. Note: When a targeted test run yields 0 tests, first verify the compiled JS exists under `out/test` (rootDir is `src`); absence almost always means the test file sits outside `src` or compilation hasn't run yet: + +```typescript +// Run specific test suite +await run_in_terminal({ + command: 'npm run unittest -- --grep "Suite Name"', + isBackground: false, + explanation: 'Run targeted unit tests', +}); +``` + +## πŸ” Diagnosing Test Failures + +### Common Failure Patterns + +**Compilation Errors:** + +```typescript +// Missing imports +if (error.includes('Cannot find module')) { + await addMissingImports(testFile); +} + +// Type mismatches +if (error.includes("Type '" && error.includes("' is not assignable"))) { + await fixTypeIssues(testFile); +} +``` + +**Runtime Errors:** + +```typescript +// Mock setup issues +if (error.includes('stub') || error.includes('mock')) { + await fixMockConfiguration(testFile); +} + +// Assertion failures +if (error.includes('AssertionError')) { + await analyzeAssertionFailure(error); +} +``` + +### Systematic Failure Analysis + +Fix test issues iteratively - run tests, analyze failures, apply fixes, repeat until passing. When unit tests fail with VS Code API errors like `TypeError: X is not a constructor` or `Cannot read properties of undefined (reading 'Y')`, check if VS Code APIs are properly mocked in `/src/test/unittests.ts` - add missing APIs following the existing pattern. + +```typescript +interface TestFailureAnalysis { + type: 'compilation' | 'runtime' | 'assertion' | 'timeout'; + message: string; + location: { file: string; line: number; col: number }; + suggestedFix: string; +} + +function analyzeFailure(failure: TestFailure): TestFailureAnalysis { + if (failure.message.includes('Cannot find module')) { + return { + type: 'compilation', + message: failure.message, + location: failure.location, + suggestedFix: 'Add missing import statement', + }; + } + // ... other failure patterns +} +``` + +### Agent Decision Logic for Test Type Selection + +**Choose Unit Tests (`*.unit.test.ts`) when analyzing:** + +- Functions with clear inputs/outputs and no VS Code API dependencies +- Data transformation, parsing, or utility functions +- Business logic that can be isolated with mocks +- Error handling scenarios with predictable inputs + +**Choose Extension Tests (`*.test.ts`) when analyzing:** + +- Functions that register VS Code commands or use `vscode.*` APIs +- UI components, tree views, or command palette interactions +- File system operations requiring workspace context +- Extension lifecycle events (activation, deactivation) + +**Agent Implementation Pattern:** + +```typescript +function determineTestType(functionCode: string): 'unit' | 'extension' { + if ( + functionCode.includes('vscode.') || + functionCode.includes('commands.register') || + functionCode.includes('window.') || + functionCode.includes('workspace.') + ) { + return 'extension'; + } + return 'unit'; +} +``` + +## 🎯 Step 1: Automated Function Analysis + +As an AI agent, analyze the target function systematically: + +### Code Analysis Checklist + +```typescript +interface FunctionAnalysis { + name: string; + inputs: string[]; // Parameter types and names + outputs: string; // Return type + dependencies: string[]; // External modules/APIs used + sideEffects: string[]; // Logging, file system, network calls + errorPaths: string[]; // Exception scenarios + testType: 'unit' | 'extension'; +} +``` + +### Analysis Implementation + +1. **Read function source** using `read_file` tool +2. **Identify imports** - look for `vscode.*`, `child_process`, `fs`, etc. +3. **Map data flow** - trace inputs through transformations to outputs +4. **Catalog dependencies** - external calls that need mocking +5. **Document side effects** - logging, file operations, state changes + +### Test Setup Differences + +#### Unit Test Setup (\*.unit.test.ts) + +```typescript +// Mock VS Code APIs - handled automatically by unittests.ts +import * as sinon from 'sinon'; +import * as workspaceApis from '../../common/workspace.apis'; // Wrapper functions + +// Stub wrapper functions, not VS Code APIs directly +// Always mock wrapper functions (e.g., workspaceApis.getConfiguration()) instead of +// VS Code APIs directly to avoid stubbing issues +const mockGetConfiguration = sinon.stub(workspaceApis, 'getConfiguration'); +``` + +#### Extension Test Setup (\*.test.ts) + +```typescript +// Use real VS Code APIs +import * as vscode from 'vscode'; + +// Real VS Code APIs available - no mocking needed +const config = vscode.workspace.getConfiguration('python'); +``` + +## 🎯 Step 2: Generate Test Coverage Matrix + +Based on function analysis, automatically generate comprehensive test scenarios: + +### Coverage Matrix Generation + +```typescript +interface TestScenario { + category: 'happy-path' | 'edge-case' | 'error-handling' | 'side-effects'; + description: string; + inputs: Record; + expectedOutput?: any; + expectedSideEffects?: string[]; + shouldThrow?: boolean; +} +``` + +### Automated Scenario Creation + +1. **Happy Path**: Normal execution with typical inputs +2. **Edge Cases**: Boundary conditions, empty/null inputs, unusual but valid data +3. **Error Scenarios**: Invalid inputs, dependency failures, exception paths +4. **Side Effects**: Verify logging calls, file operations, state changes + +### Agent Pattern for Scenario Generation + +```typescript +function generateTestScenarios(analysis: FunctionAnalysis): TestScenario[] { + const scenarios: TestScenario[] = []; + + // Generate happy path for each input combination + scenarios.push(...generateHappyPathScenarios(analysis)); + + // Generate edge cases for boundary conditions + scenarios.push(...generateEdgeCaseScenarios(analysis)); + + // Generate error scenarios for each dependency + scenarios.push(...generateErrorScenarios(analysis)); + + return scenarios; +} +``` + +## πŸ—ΊοΈ Step 3: Plan Your Test Coverage + +### Create a Test Coverage Matrix + +#### Main Flows + +- βœ… **Happy path scenarios** - normal expected usage +- βœ… **Alternative paths** - different configuration combinations +- βœ… **Integration scenarios** - multiple features working together + +#### Edge Cases + +- πŸ”Έ **Boundary conditions** - empty inputs, missing data +- πŸ”Έ **Error scenarios** - network failures, permission errors +- πŸ”Έ **Data validation** - invalid inputs, type mismatches + +#### Real-World Scenarios + +- βœ… **Fresh install** - clean slate +- βœ… **Existing user** - migration scenarios +- βœ… **Power user** - complex configurations +- πŸ”Έ **Error recovery** - graceful degradation + +### Example Test Plan Structure + +```markdown +## Test Categories + +### 1. Configuration Migration Tests + +- No legacy settings exist +- Legacy settings already migrated +- Fresh migration needed +- Partial migration required +- Migration failures + +### 2. Configuration Source Tests + +- Global search paths +- Workspace search paths +- Settings precedence +- Configuration errors + +### 3. Path Resolution Tests + +- Absolute vs relative paths +- Workspace folder resolution +- Path validation and filtering + +### 4. Integration Scenarios + +- Combined configurations +- Deduplication logic +- Error handling flows +``` + +## πŸ”§ Step 4: Set Up Your Test Infrastructure + +### Test File Structure + +```typescript +// 1. Imports - group logically +import assert from 'node:assert'; +import * as sinon from 'sinon'; +import { Uri } from 'vscode'; +import * as logging from '../../../common/logging'; +import * as pathUtils from '../../../common/utils/pathUtils'; +import * as workspaceApis from '../../../common/workspace.apis'; + +// 2. Function under test +import { getAllExtraSearchPaths } from '../../../managers/common/nativePythonFinder'; + +// 3. Mock interfaces +interface MockWorkspaceConfig { + get: sinon.SinonStub; + inspect: sinon.SinonStub; + update: sinon.SinonStub; +} +``` + +### Mock Setup Strategy + +Create minimal mock objects with only required methods and use TypeScript type assertions (e.g., `mockApi as PythonEnvironmentApi`) to satisfy interface requirements instead of implementing all interface methods when only specific methods are needed for the test. Simplify mock setup by only mocking methods actually used in tests and use `as unknown as Type` for TypeScript compatibility. + +```typescript +suite('Function Integration Tests', () => { + // 1. Declare all mocks + let mockGetConfiguration: sinon.SinonStub; + let mockGetWorkspaceFolders: sinon.SinonStub; + let mockTraceLog: sinon.SinonStub; + let mockTraceError: sinon.SinonStub; + let mockTraceWarn: sinon.SinonStub; + + // 2. Mock complex objects + let pythonConfig: MockWorkspaceConfig; + let envConfig: MockWorkspaceConfig; + + setup(() => { + // 3. Initialize all mocks + mockGetConfiguration = sinon.stub(workspaceApis, 'getConfiguration'); + mockGetWorkspaceFolders = sinon.stub(workspaceApis, 'getWorkspaceFolders'); + mockTraceLog = sinon.stub(logging, 'traceLog'); + mockTraceError = sinon.stub(logging, 'traceError'); + mockTraceWarn = sinon.stub(logging, 'traceWarn'); + + // 4. Set up default behaviors + mockGetWorkspaceFolders.returns(undefined); + + // 5. Create mock configuration objects + // When fixing mock environment creation, use null to truly omit + // properties rather than undefined + pythonConfig = { + get: sinon.stub(), + inspect: sinon.stub(), + update: sinon.stub(), + }; + + envConfig = { + get: sinon.stub(), + inspect: sinon.stub(), + update: sinon.stub(), + }; + }); + + teardown(() => { + sinon.restore(); // Always clean up! + }); +}); +``` + +## Step 4: Write Tests Using Mock β†’ Run β†’ Assert Pattern + +### The Three-Phase Pattern + +#### Phase 1: Mock (Set up the scenario) + +```typescript +test('Description of what this tests', async () => { + // Mock β†’ Clear description of the scenario + pythonConfig.inspect.withArgs('venvPath').returns({ globalValue: '/path' }); + envConfig.inspect.withArgs('globalSearchPaths').returns({ globalValue: [] }); + mockGetWorkspaceFolders.returns([{ uri: Uri.file('/workspace') }]); +``` + +#### Phase 2: Run (Execute the function) + +```typescript +// Run +const result = await getAllExtraSearchPaths(); +``` + +#### Phase 3: Assert (Verify the behavior) + +```typescript + // Assert - Use set-based comparison for order-agnostic testing + const expected = new Set(['/expected', '/paths']); + const actual = new Set(result); + assert.strictEqual(actual.size, expected.size, 'Should have correct number of paths'); + assert.deepStrictEqual(actual, expected, 'Should contain exactly the expected paths'); + + // Verify side effects + // Use sinon.match() patterns for resilient assertions that don't break on minor output changes + assert(mockTraceLog.calledWith(sinon.match(/completion/i)), 'Should log completion'); +}); +``` + +## Step 6: Make Tests Resilient + +### Use Order-Agnostic Comparisons + +```typescript +// ❌ Brittle - depends on order +assert.deepStrictEqual(result, ['/path1', '/path2', '/path3']); + +// βœ… Resilient - order doesn't matter +const expected = new Set(['/path1', '/path2', '/path3']); +const actual = new Set(result); +assert.strictEqual(actual.size, expected.size, 'Should have correct number of paths'); +assert.deepStrictEqual(actual, expected, 'Should contain exactly the expected paths'); +``` + +### Use Flexible Error Message Testing + +```typescript +// ❌ Brittle - exact text matching +assert(mockTraceError.calledWith('Error during legacy python settings migration:')); + +// βœ… Resilient - pattern matching +assert(mockTraceError.calledWith(sinon.match.string, sinon.match.instanceOf(Error)), 'Should log migration error'); + +// βœ… Resilient - key terms with regex +assert(mockTraceError.calledWith(sinon.match(/migration.*error/i)), 'Should log migration error'); +``` + +### Handle Complex Mock Scenarios + +```typescript +// For functions that call the same mock multiple times +envConfig.inspect.withArgs('globalSearchPaths').returns({ globalValue: [] }); +envConfig.inspect + .withArgs('globalSearchPaths') + .onSecondCall() + .returns({ + globalValue: ['/migrated/paths'], + }); + +// Testing async functions with child processes: +// Call the function first to get a promise, then use setTimeout to emit mock events, +// then await the promise - this ensures proper timing of mock setup versus function execution + +// Cannot stub internal function calls within the same module after import - stub external +// dependencies instead (e.g., stub childProcessApis.spawnProcess rather than trying to stub +// helpers.isUvInstalled when testing helpers.shouldUseUv) because intra-module calls use +// direct references, not module exports +``` + +## πŸ§ͺ Step 7: Test Categories and Patterns + +### Configuration Tests + +- Test different setting combinations +- Test setting precedence (workspace > user > default) +- Test configuration errors and recovery +- Always use dynamic path construction with Node.js `path` module when testing functions that resolve paths against workspace folders to ensure cross-platform compatibility + +### Data Flow Tests + +- Test how data moves through the system +- Test transformations (path resolution, filtering) +- Test state changes (migrations, updates) + +### Error Handling Tests + +- Test graceful degradation +- Test error logging +- Test fallback behaviors + +### Integration Tests + +- Test multiple features together +- Test real-world scenarios +- Test edge case combinations + +## πŸ“Š Step 8: Review and Refine + +### Test Quality Checklist + +- [ ] **Clear naming** - test names describe the scenario and expected outcome +- [ ] **Good coverage** - main flows, edge cases, error scenarios +- [ ] **Resilient assertions** - won't break due to minor changes +- [ ] **Readable structure** - follows Mock β†’ Run β†’ Assert pattern +- [ ] **Isolated tests** - each test is independent +- [ ] **Fast execution** - tests run quickly with proper mocking + +### Common Anti-Patterns to Avoid + +- ❌ Testing implementation details instead of behavior +- ❌ Brittle assertions that break on cosmetic changes +- ❌ Order-dependent tests that fail due to processing changes +- ❌ Tests that don't clean up mocks properly +- ❌ Overly complex test setup that's hard to understand + +## πŸ”„ Reviewing and Improving Existing Tests + +### Quick Review Process + +1. **Read test files** - Check structure and mock setup +2. **Run tests** - Establish baseline functionality +3. **Apply improvements** - Use patterns below. When reviewing existing tests, focus on behavior rather than implementation details in test names and assertions +4. **Verify** - Ensure tests still pass + +### Common Fixes + +- Over-complex mocks β†’ Minimal mocks with only needed methods +- Brittle assertions β†’ Behavior-focused with error messages +- Vague test names β†’ Clear scenario descriptions (transform "should return X when Y" into "should [expected behavior] when [scenario context]") +- Missing structure β†’ Mock β†’ Run β†’ Assert pattern +- Untestable Node.js APIs β†’ Create proxy abstraction functions (use function overloads to preserve intelligent typing while making functions mockable) + +## 🧠 Agent Learnings + +- When mocking `testController.createTestItem()` in unit tests, use `typemoq.It.isAny()` for parameters when testing handler behavior (not ID/label generation logic), but consider using specific matchers (e.g., `It.is((id: string) => id.startsWith('_error_'))`) when the actual values being passed are important for correctness - this balances test precision with maintainability (2) +- Remove unused variables from test code immediately - leftover tracking variables like `validationCallCount` that aren't referenced indicate dead code that should be simplified (1) +- Use `Uri.file(path).fsPath` for both sides of path comparisons in tests to ensure cross-platform compatibility - Windows converts forward slashes to backslashes automatically (1) +- When tests fail with "Cannot stub non-existent property", the method likely moved to a different class during refactoring - find the class that owns the method and test that class directly instead of stubbing on the original class (1) diff --git a/.github/instructions/testing_feature_area.instructions.md b/.github/instructions/testing_feature_area.instructions.md new file mode 100644 index 000000000000..a4e11523d7c8 --- /dev/null +++ b/.github/instructions/testing_feature_area.instructions.md @@ -0,0 +1,263 @@ +--- +applyTo: 'src/client/testing/**' +--- + +# Testing feature area β€” Discovery, Run, Debug, and Results + +This document maps the testing support in the extension: discovery, execution (run), debugging, result reporting and how those pieces connect to the codebase. It's written for contributors and agents who need to navigate, modify, or extend test support (both `unittest` and `pytest`). + +## Overview + +- Purpose: expose Python tests in the VS Code Test Explorer (TestController), support discovery, run, debug, and surface rich results and outputs. +- Scope: provider-agnostic orchestration + provider-specific adapters, TestController mapping, IPC with Python-side scripts, debug launch integration, and configuration management. + +## High-level architecture + +- Controller / UI bridge: orchestrates TestController requests and routes them to workspace adapters. +- Workspace adapter: provider-agnostic coordinator that translates TestController requests to provider adapters and maps payloads back into TestItems/TestRuns. +- Provider adapters: implement discovery/run/debug for `unittest` and `pytest` by launching Python scripts and wiring named-pipe IPC. +- Result resolver: translates Python-side JSON/IPCPayloads into TestController updates (start/pass/fail/output/attachments). +- Debug launcher: prepares debug sessions and coordinates the debugger attach flow with the Python runner. + +## Key components (files and responsibilities) + +- Entrypoints + - `src/client/testing/testController/controller.ts` β€” `PythonTestController` (main orchestrator). + - `src/client/testing/serviceRegistry.ts` β€” DI/wiring for testing services. +- Workspace orchestration + - `src/client/testing/testController/workspaceTestAdapter.ts` β€” `WorkspaceTestAdapter` (provider-agnostic entry used by controller). +- **Project-based testing (multi-project workspaces)** + - `src/client/testing/testController/common/testProjectRegistry.ts` β€” `TestProjectRegistry` (manages project lifecycle, discovery, and nested project handling). + - `src/client/testing/testController/common/projectAdapter.ts` β€” `ProjectAdapter` interface (represents a single Python project with its own test infrastructure). + - `src/client/testing/testController/common/projectUtils.ts` β€” utilities for project ID generation, display names, and shared adapter creation. +- Provider adapters + - Unittest + - `src/client/testing/testController/unittest/testDiscoveryAdapter.ts` + - `src/client/testing/testController/unittest/testExecutionAdapter.ts` + - Pytest + - `src/client/testing/testController/pytest/pytestDiscoveryAdapter.ts` + - `src/client/testing/testController/pytest/pytestExecutionAdapter.ts` +- Result resolution and helpers + - `src/client/testing/testController/common/resultResolver.ts` β€” `PythonResultResolver` (maps payload -> TestController updates). + - `src/client/testing/testController/common/testItemUtilities.ts` β€” helpers for TestItem lifecycle. + - `src/client/testing/testController/common/types.ts` β€” `ITestDiscoveryAdapter`, `ITestExecutionAdapter`, `ITestResultResolver`, `ITestDebugLauncher`. + - `src/client/testing/testController/common/debugLauncher.ts` β€” debug session creation helper. + - `src/client/testing/testController/common/utils.ts` β€” named-pipe helpers and command builders (`startDiscoveryNamedPipe`, etc.). +- Configuration + - `src/client/testing/common/testConfigurationManager.ts` β€” per-workspace test settings. + - `src/client/testing/configurationFactory.ts` β€” configuration service factory. +- Utilities & glue + - `src/client/testing/utils.ts` β€” assorted helpers used by adapters. + - Python-side scripts: `python_files/unittestadapter/*`, `python_files/pytestadapter/*` β€” discovery/run code executed by adapters. + +## Python subprocess runners (what runs inside Python) + +The adapters in the extension don't implement test discovery/run logic themselves β€” they spawn a Python subprocess that runs small helper scripts located under `python_files/` and stream structured events back to the extension over the named-pipe IPC. This is a central part of the feature area; changes here usually require coordinated edits in both the TypeScript adapters and the Python scripts. + +- Unittest helpers (folder: `python_files/unittestadapter`) + + - `discovery.py` β€” performs `unittest` discovery and emits discovery payloads (test suites, cases, locations) on the IPC channel. + - `execution.py` / `django_test_runner.py` β€” run tests for `unittest` and, where applicable, Django test runners; emit run events (start, stdout/stderr, pass, fail, skip, teardown) and attachment info. + - `pvsc_utils.py`, `django_handler.py` β€” utility helpers used by the runners for environment handling and Django-specific wiring. + - The adapter TypeScript files (`testDiscoveryAdapter.ts`, `testExecutionAdapter.ts`) construct the command line, start a named-pipe listener, and spawn these Python scripts using the extension's ExecutionFactory (activated interpreter) so the scripts execute inside the user's selected environment. + +- Pytest helpers (folder: `python_files/vscode_pytest`) + + - `_common.py` β€” shared helpers for pytest runner scripts. + - `run_pytest_script.py` β€” the primary pytest runner used for discovery and execution; emits the same structured IPC payloads the extension expects (discovery events and run events). + - The `pytest` execution adapter (`pytestExecutionAdapter.ts`) and discovery adapter build the CLI to run `run_pytest_script.py`, start the pipe, and translate incoming payloads via `PythonResultResolver`. + +- IPC contract and expectations + + - Adapters rely on a stable JSON payload contract emitted by the Python scripts: identifiers for tests, event types (discovered, collected, started, passed, failed, skipped), timings, error traces, and optional attachments (logs, captured stdout/stderr, file links). + - The extension maps these payloads to `TestItem`/`TestRun` updates via `PythonResultResolver` (`src/client/testing/testController/common/resultResolver.ts`). If you change payload shape, update the resolver and tests concurrently. + +- How the subprocess is started + - Execution adapters use the extension's `ExecutionFactory` (preferred) to get an activated interpreter and then spawn a child process that runs the helper script. The adapter will set up environment variables and command-line args (including the pipe name / run-id) so the Python runner knows where to send events and how to behave (discovery vs run vs debug). + - For debug sessions a debug-specific entry argument/port is passed and `common/debugLauncher.ts` coordinates starting a VS Code debug session that will attach to the Python process. + +## Core functionality (what to change where) + +- Discovery + - Entry: `WorkspaceTestAdapter.discoverTests` β†’ provider discovery adapter. Adapter starts a named-pipe listener, spawns the discovery script in an activated interpreter, forwards discovery events to `PythonResultResolver` which creates/updates TestItems. + - Files: `workspaceTestAdapter.ts`, `*DiscoveryAdapter.ts`, `resultResolver.ts`, `testItemUtilities.ts`. +- Run / Execution + - Entry: `WorkspaceTestAdapter.executeTests` β†’ provider execution adapter. Adapter spawns runner in an activated env, runner streams run events to the pipe, `PythonResultResolver` updates a `TestRun` with start/pass/fail and attachments. + - Files: `workspaceTestAdapter.ts`, `*ExecutionAdapter.ts`, `resultResolver.ts`. +- Debugging + - Flow: debug request flows like a run but goes through `debugLauncher.ts` to create a VS Code debug session with prepared ports/pipes. The Python runner coordinates attach/continue with the debugger. + - Files: `*ExecutionAdapter.ts`, `common/debugLauncher.ts`, `common/types.ts`. +- Result reporting + - `resultResolver.ts` is the canonical place to change how JSON payloads map to TestController constructs (messages, durations, error traces, attachments). + +## Typical workflows (short) + +- Full discovery + + 1. `PythonTestController` triggers discovery -> `WorkspaceTestAdapter.discoverTests`. + 2. Provider discovery adapter starts pipe and launches Python discovery script. + 3. Discovery events -> `PythonResultResolver` -> TestController tree updated. + +- Run tests + + 1. Controller collects TestItems -> creates `TestRun`. + 2. `WorkspaceTestAdapter.executeTests` delegates to execution adapter which launches the runner. + 3. Runner events arrive via pipe -> `PythonResultResolver` updates `TestRun`. + 4. On process exit the run is finalized. + +- Debug a test + 1. Debug request flows to execution adapter. + 2. Adapter prepares ports and calls `debugLauncher` to start a VS Code debug session with the run ID. + 3. Runner coordinates with the debugger; `PythonResultResolver` still receives and applies run events. + +## Tests and examples to inspect + +- Unit/integration tests for adapters and orchestration under `src/test/` (examples): + - `src/test/testing/common/testingAdapter.test.ts` + - `src/test/testing/testController/workspaceTestAdapter.unit.test.ts` + - `src/test/testing/testController/unittest/testExecutionAdapter.unit.test.ts` + - Adapter tests demonstrate expected telemetry, debug-launch payloads and result resolution. + +## History & evolution (brief) + +- Migration to TestController API: the code organizes around VS Code TestController, mapping legacy adapter behaviour into TestItems/TestRuns. +- Named-pipe IPC: discovery/run use named-pipe IPC to stream events from Python runner scripts (`python_files/*`) which enables richer, incremental updates and debug coordination. +- Environment activation: adapters prefer the extension ExecutionFactory (activated interpreter) to run discovery and test scripts. + +## Pointers for contributors (practical) + +- To extend discovery output: update the Python discovery script in `python_files/*` and `resultResolver.ts` to parse new payload fields. +- To change run behaviour (args/env/timouts): update the provider execution adapter (`*ExecutionAdapter.ts`) and add/update tests under `src/test/`. +- To change debug flow: edit `common/debugLauncher.ts` and adapters' debug paths; update tests that assert launch argument shapes. + +## Django support (how it works) + +- The extension supports Django projects by delegating discovery and execution to Django-aware Python helpers under `python_files/unittestadapter`. + - `python_files/unittestadapter/django_handler.py` contains helpers that invoke `manage.py` for discovery or execute Django test runners inside the project context. + - `python_files/unittestadapter/django_test_runner.py` provides `CustomDiscoveryTestRunner` and `CustomExecutionTestRunner` which integrate with the extension by using the same IPC contract (they use `UnittestTestResult` and `send_post_request` to emit discovery/run payloads). +- How adapters pass Django configuration: + - Execution adapters set environment variables (e.g. `MANAGE_PY_PATH`) and modify `PYTHONPATH` so Django code and the custom test runner are importable inside the spawned subprocess. + - For discovery the adapter may run the discovery helper which calls `manage.py test` with a custom test runner that emits discovery payloads instead of executing tests. +- Practical notes for contributors: + - Changes to Django discovery/execution often require edits in both `django_test_runner.py`/`django_handler.py` and the TypeScript adapters (`testDiscoveryAdapter.ts` / `testExecutionAdapter.ts`). + - The Django test runner expects `TEST_RUN_PIPE` environment variable to be present to send IPC events (see `django_test_runner.py`). + +## Settings referenced by this feature area + +- The extension exposes several `python.testing.*` settings used by adapters and configuration code (declared in `package.json`): + - `python.testing.pytestEnabled`, `python.testing.unittestEnabled` β€” enable/disable frameworks. + - `python.testing.pytestPath`, `python.testing.pytestArgs`, `python.testing.unittestArgs` β€” command path and CLI arguments used when spawning helper scripts. + - `python.testing.cwd` β€” optional working directory used when running discovery/runs. + - `python.testing.autoTestDiscoverOnSaveEnabled`, `python.testing.autoTestDiscoverOnSavePattern` β€” control automatic discovery on save. + - `python.testing.debugPort` β€” default port used for debug runs. + - `python.testing.promptToConfigure` β€” whether to prompt users to configure tests when potential test folders are found. +- Where to look in the code: + - Settings are consumed by `src/client/testing/common/testConfigurationManager.ts`, `src/client/testing/configurationFactory.ts`, and adapters under `src/client/testing/testController/*` which read settings to build CLI args and env for subprocesses. + - The setting definitions and descriptions are in `package.json` and localized strings in `package.nls.json`. + +## Project-based testing (multi-project workspaces) + +Project-based testing enables multi-project workspace support where each Python project gets its own test tree root with its own Python environment. + +### Architecture + +- **TestProjectRegistry** (`testProjectRegistry.ts`): Central registry that: + + - Discovers Python projects via the Python Environments API + - Creates and manages `ProjectAdapter` instances per workspace + - Computes nested project relationships and configures ignore lists + - Falls back to "legacy" single-adapter mode when API unavailable + +- **ProjectAdapter** (`projectAdapter.ts`): Interface representing a single project with: + - Project identity (ID, name, URI from Python Environments API) + - Python environment with execution details + - Test framework adapters (discovery/execution) + - Nested project ignore paths (for parent projects) + +### How it works + +1. **Activation**: When the extension activates, `PythonTestController` checks if the Python Environments API is available. +2. **Project discovery**: `TestProjectRegistry.discoverAndRegisterProjects()` queries the API for all Python projects in each workspace. +3. **Nested handling**: `configureNestedProjectIgnores()` identifies child projects and adds their paths to parent projects' ignore lists. +4. **Test discovery**: For each project, the controller calls `project.discoveryAdapter.discoverTests()` with the project's URI. The adapter sets `PROJECT_ROOT_PATH` environment variable for the Python runner. +5. **Python side**: + - For pytest: `get_test_root_path()` in `vscode_pytest/__init__.py` returns `PROJECT_ROOT_PATH` (if set) or falls back to `cwd`. + - For unittest: `discovery.py` uses `PROJECT_ROOT_PATH` as `top_level_dir` and `project_root_path` to root the test tree at the project directory. +6. **Test tree**: Each project gets its own root node in the Test Explorer, with test IDs scoped by project ID using the `@@vsc@@` separator (defined in `projectUtils.ts`). + +### Nested project handling: pytest vs unittest + +**pytest** supports the `--ignore` flag to exclude paths during test collection. When nested projects are detected, parent projects automatically receive `--ignore` flags for child project paths. This ensures each test appears under exactly one project in the test tree. + +**unittest** does not support path exclusion during `discover()`. Therefore, tests in nested project directories may appear under multiple project roots (both the parent and the child project). This is **expected behavior** for unittest: + +- Each project discovers and displays all tests it finds within its directory structure +- There is no deduplication or collision detection +- Users may see the same test file under multiple project roots if their project structure has nesting + +This approach was chosen because: + +1. unittest's `TestLoader.discover()` has no built-in path exclusion mechanism +2. Implementing custom exclusion would add significant complexity with minimal benefit +3. The existing approach is transparent and predictable - each project shows what it finds + +### Empty projects and root nodes + +If a project discovers zero tests, its root node will still appear in the Test Explorer as an empty folder. This ensures consistent behavior and makes it clear which projects were discovered, even if they have no tests yet. + +### Logging prefix + +All project-based testing logs use the `[test-by-project]` prefix for easy filtering in the output channel. + +### Key files + +- Python side: + - `python_files/vscode_pytest/__init__.py` β€” `get_test_root_path()` function and `PROJECT_ROOT_PATH` environment variable for pytest. + - `python_files/unittestadapter/discovery.py` β€” `discover_tests()` with `project_root_path` parameter and `PROJECT_ROOT_PATH` handling for unittest discovery. + - `python_files/unittestadapter/execution.py` β€” `run_tests()` with `project_root_path` parameter and `PROJECT_ROOT_PATH` handling for unittest execution. +- TypeScript: `testProjectRegistry.ts`, `projectAdapter.ts`, `projectUtils.ts`, and the discovery/execution adapters. + +### Tests + +- `src/test/testing/testController/common/testProjectRegistry.unit.test.ts` β€” TestProjectRegistry tests +- `src/test/testing/testController/common/projectUtils.unit.test.ts` β€” Project utility function tests +- `python_files/tests/pytestadapter/test_discovery.py` β€” pytest PROJECT_ROOT_PATH tests (see `test_project_root_path_env_var()` and `test_symlink_with_project_root_path()`) +- `python_files/tests/unittestadapter/test_discovery.py` β€” unittest `project_root_path` / PROJECT_ROOT_PATH discovery tests +- `python_files/tests/unittestadapter/test_execution.py` β€” unittest `project_root_path` / PROJECT_ROOT_PATH execution tests +- `src/test/testing/testController/unittest/testDiscoveryAdapter.unit.test.ts` β€” unittest discovery adapter PROJECT_ROOT_PATH tests +- `src/test/testing/testController/unittest/testExecutionAdapter.unit.test.ts` β€” unittest execution adapter PROJECT_ROOT_PATH tests + +## Coverage support (how it works) + +- Coverage is supported by running the Python helper scripts with coverage enabled and then collecting a coverage payload from the runner. + - Pytest-side coverage logic lives in `python_files/vscode_pytest/__init__.py` (checks `COVERAGE_ENABLED`, imports `coverage`, computes per-file metrics and emits a `CoveragePayloadDict`). + - Unittest adapters enable coverage by setting environment variable(s) (e.g. `COVERAGE_ENABLED`) when launching the subprocess; adapters and `resultResolver.ts` handle the coverage profile kind (`TestRunProfileKind.Coverage`). +- Flow summary: + 1. User starts a Coverage run via Test Explorer (profile kind `Coverage`). + 2. Controller/adapters set `COVERAGE_ENABLED` (or equivalent) in the subprocess env and invoke the runner script. + 3. The Python runner collects coverage (using `coverage` or `pytest-cov`), builds a file-level coverage map, and sends a coverage payload back over the IPC. + 4. `PythonResultResolver` (`src/client/testing/testController/common/resultResolver.ts`) receives the coverage payload and stores `detailedCoverageMap` used by the TestController profile to show file-level coverage details. +- Tests that exercise coverage flows are under `src/test/testing/*` and `python_files/tests/*` (see `testingAdapter.test.ts` and adapter unit tests that assert `COVERAGE_ENABLED` is set appropriately). + +## Interaction with the VS Code API + +- TestController API + - The feature area is built on VS Code's TestController/TestItem/TestRun APIs (`vscode.tests.createTestController` / `tests.createTestController` in the code). The controller creates a `TestController` in `src/client/testing/testController/controller.ts` and synchronizes `TestItem` trees with discovery payloads. + - `PythonResultResolver` maps incoming JSON events to VS Code API calls: `testRun.appendOutput`, `testRun.passed/failed/skipped`, `testRun.end`, and `TestItem` updates (labels, locations, children). +- Debug API + - Debug runs use the Debug API to start an attach/launch session. The debug launcher implementation is in `src/client/testing/testController/common/debugLauncher.ts` which constructs a debug configuration and calls the VS Code debug API to start a session (e.g. `vscode.debug.startDebugging`). + - Debug adapter/resolver code in the extension's debugger modules may also be used when attaching to Django or test subprocesses. +- Commands and configuration + - The Test Controller wires commands that appear in the Test Explorer and editor context menus (see `package.json` contributes `commands`) and listens to configuration changes filtered by `python.testing` in `src/client/testing/main.ts`. +- The "Copy Test ID" command (`python.copyTestId`) can be accessed from both the Test Explorer context menu (`testing/item/context`) and the editor gutter icon context menu (`testing/item/gutter`). This command copies test identifiers to the clipboard in the appropriate format for the active test framework (pytest path format or unittest module.class.method format). +- Execution factory & activated environments + - Adapters use the extension `ExecutionFactory` to spawn subprocesses in an activated interpreter (so the user's venv/conda is used). This involves the extension's internal environment execution APIs and sometimes `envExt` helpers when the external environment extension is present. + +## Learnings + +- Never await `showErrorMessage()` calls in test execution adapters as it blocks the test UI thread and freezes the Test Explorer (1) +- VS Code test-related context menus are contributed to using both `testing/item/context` and `testing/item/gutter` menu locations in package.json for full coverage (1) + +``` + +``` diff --git a/.github/prompts/extract-impl-instructions.prompt.md b/.github/prompts/extract-impl-instructions.prompt.md new file mode 100644 index 000000000000..c2fb08b443c7 --- /dev/null +++ b/.github/prompts/extract-impl-instructions.prompt.md @@ -0,0 +1,79 @@ +--- +mode: edit +--- + +Analyze the specified part of the VS Code Python Extension codebase to generate or update implementation instructions in `.github/instructions/.instructions.md`. + +## Task + +Create concise developer guidance focused on: + +### Implementation Essentials + +- **Core patterns**: How this component is typically implemented and extended +- **Key interfaces**: Essential classes, services, and APIs with usage examples +- **Integration points**: How this component interacts with other extension parts +- **Common tasks**: Typical development scenarios with step-by-step guidance + +### Content Structure + +````markdown +--- +description: 'Implementation guide for the part of the Python Extension' +--- + +# Implementation Guide + +## Overview + +Brief description of the component's purpose and role in VS Code Python Extension. + +## Key Concepts + +- Main abstractions and their responsibilities +- Important interfaces and base classes + +## Common Implementation Patterns + +### Pattern 1: [Specific Use Case] + +```typescript +// Code example showing typical implementation +``` +```` + +### Pattern 2: [Another Use Case] + +```typescript +// Another practical example +``` + +## Integration Points + +- How this component connects to other VS Code Python Extension systems +- Required services and dependencies +- Extension points and contribution models + +## Essential APIs + +- Key methods and interfaces developers need +- Common parameters and return types + +## Gotchas and Best Practices + +- Non-obvious behaviors to watch for +- Performance considerations +- Common mistakes to avoid + +``` + +## Guidelines +- **Be specific**: Use actual class names, method signatures, and file paths +- **Show examples**: Include working code snippets from the codebase +- **Target implementation**: Focus on how to build with/extend this component +- **Keep it actionable**: Every section should help developers accomplish tasks + +Source conventions from existing `.github/instructions/*.instructions.md`, `CONTRIBUTING.md`, and codebase patterns. + +If `.github/instructions/.instructions.md` exists, intelligently merge new insights with existing content. +``` diff --git a/.github/prompts/extract-usage-instructions.prompt.md b/.github/prompts/extract-usage-instructions.prompt.md new file mode 100644 index 000000000000..ea48f162a220 --- /dev/null +++ b/.github/prompts/extract-usage-instructions.prompt.md @@ -0,0 +1,30 @@ +--- +mode: edit +--- + +Analyze the user requested part of the codebase (use a suitable ) to generate or update `.github/instructions/.instructions.md` for guiding developers and AI coding agents. + +Focus on practical usage patterns and essential knowledge: + +- How to use, extend, or integrate with this code area +- Key architectural patterns and conventions specific to this area +- Common implementation patterns with code examples +- Integration points and typical interaction patterns with other components +- Essential gotchas and non-obvious behaviors + +Source existing conventions from `.github/instructions/*.instructions.md`, `CONTRIBUTING.md`, and `README.md`. + +Guidelines: + +- Write concise, actionable instructions using markdown structure +- Document discoverable patterns with concrete examples +- If `.github/instructions/.instructions.md` exists, merge intelligently +- Target developers who need to work with or extend this code area + +Update `.github/instructions/.instructions.md` with header: + +``` +--- +description: "How to work with the part of the codebase" +--- +``` diff --git a/.github/release_plan.md b/.github/release_plan.md index e02d7ee45abf..091ed559825b 100644 --- a/.github/release_plan.md +++ b/.github/release_plan.md @@ -1,23 +1,47 @@ +### General Notes All dates should align with VS Code's [iteration](https://github.com/microsoft/vscode/labels/iteration-plan) and [endgame](https://github.com/microsoft/vscode/labels/endgame-plan) plans. -Feature freeze is Monday @ 17:00 America/Vancouver, XXX XX. +Feature freeze is Monday @ 17:00 America/Vancouver, XXX XX. At that point, commits to `main` should only be in response to bugs found during endgame testing until the release candidate is ready. +
+ Release Primary and Secondary Assignments for the 2025 Calendar Year -NOTE: the number of this release is in the issue title and can be substituted in wherever you see [YYYY.minor]. +| Month and version number | Primary | Secondary | +|------------|----------|-----------| +| January v2025.0.0 | Eleanor | Karthik | +| February v2025.2.0 | Anthony | Eleanor | +| March v2025.4.0 | Karthik | Anthony | +| April v2025.6.0 | Eleanor | Karthik | +| May v2025.8.0 | Anthony | Eleanor | +| June v2025.10.0 | Karthik | Anthony | +| July v2025.12.0 | Eleanor | Karthik | +| August v2025.14.0 | Anthony | Eleanor | +| September v2025.16.0 | Karthik | Anthony | +| October v2025.18.0 | Eleanor | Karthik | +| November v2025.20.0 | Anthony | Eleanor | +| December v2025.22.0 | Karthik | Anthony | + +
-# Release candidate (Monday, XXX XX) +# Release candidate (Thursday, XXX XX) +NOTE: This Thursday occurs during TESTING week. Branching should be done during this week to freeze the release with only the correct changes. Any last minute fixes go in as candidates into the release branch and will require team approval. +Other: NOTE: Third Party Notices are automatically added by our build pipelines using https://tools.opensource.microsoft.com/notice. +NOTE: the number of this release is in the issue title and can be substituted in wherever you see [YYYY.minor]. + ### Step 1: -##### Bump the version of `main` to be a release candidate (also updating debugpy dependences, third party notices, and package-lock.json).❄️ (steps with ❄️ will dictate this step happens while main is frozen πŸ₯Ά) +##### Bump the version of `main` to be a release candidate (also updating third party notices, and package-lock.json).❄️ (steps with ❄️ will dictate this step happens while main is frozen πŸ₯Ά) - [ ] checkout to `main` on your local machine and run `git fetch` to ensure your local is up to date with the remote repo. - [ ] Create a new branch called **`bump-release-[YYYY.minor]`**. -- [ ] Change the version in `package.json` to the next **even** number and switch the `-dev` to `-rc`. (πŸ€–) +- [ ] Update `pet`: + - [ ] Go to the [pet](https://github.com/microsoft/python-environment-tools) repo and check `main` and latest `release/*` branch. If there are new changes in `main` then create a branch called `release/YYYY.minor` (matching python extension release `major.minor`). + - [ ] Update `build\azure-pipeline.stable.yml` to point to the latest `release/YYYY.minor` for `python-environment-tools`. +- [ ] Change the version in `package.json` to the next **even** number. (πŸ€–) - [ ] Run `npm install` to make sure `package-lock.json` is up-to-date _(you should now see changes to the `package.json` and `package-lock.json` at this point which update the version number **only**)_. (πŸ€–) -- [ ] Check [debugpy on PyPI](https://pypi.org/project/debugpy/) for a new release and update the version of debugpy in [`install_debugpy.py`](https://github.com/microsoft/vscode-python/blob/main/pythonFiles/install_debugpy.py) if necessary. - [ ] Update `ThirdPartyNotices-Repository.txt` as appropriate. You can check by looking at the [commit history](https://github.com/microsoft/vscode-python/commits/main) and scrolling through to see if there's anything listed there which might have pulled in some code directly into the repository from somewhere else. If you are still unsure you can check with the team. - [ ] Create a PR from your branch **`bump-release-[YYYY.minor]`** to `main`. Add the `"no change-log"` tag to the PR so it does not show up on the release notes before merging it. @@ -40,7 +64,7 @@ NOTE: If there are release branches that are two versions old you can delete the ### Step 4: Return `main` to dev and unfreeze (❄️ ➑ πŸ’§) NOTE: The purpose of this step is ensuring that main always is on a dev version number for every night's πŸŒƒ pre-release. Therefore it is imperative that you do this directly after the previous steps to reset the version in main to a dev version **before** a pre-release goes out. - [ ] Create a branch called **`bump-dev-version-YYYY.[minor+1]`**. -- [ ] Bump the minor version number in the `package.json` to the next `YYYY.[minor+1]` which will be an odd number, and switch the `-rc` to `-dev`.(πŸ€–) +- [ ] Bump the minor version number in the `package.json` to the next `YYYY.[minor+1]` which will be an odd number, and add `-dev`.(πŸ€–) - [ ] Run `npm install` to make sure `package-lock.json` is up-to-date _(you should now see changes to the `package.json` and `package-lock.json` only relating to the new version number)_ . (πŸ€–) - [ ] Create a PR from this branch against `main` and merge it. @@ -51,6 +75,7 @@ NOTE: this PR should make all CI relating to `main` be passing again (such as th - [ ] Manually add/fix any 3rd-party licenses as appropriate based on what the internal build pipeline detects. - [ ] Open appropriate [documentation issues](https://github.com/microsoft/vscode-docs/issues?q=is%3Aissue+is%3Aopen+label%3Apython). - [ ] Contact the PM team to begin drafting a blog post. +- [ ] Announce to the development team that `main` is open again. # Release (Wednesday, XXX XX) @@ -58,12 +83,6 @@ NOTE: this PR should make all CI relating to `main` be passing again (such as th ### Step 6: Take the release branch from a candidate to the finalized release - [ ] Make sure the [appropriate pull requests](https://github.com/microsoft/vscode-docs/pulls) for the [documentation](https://code.visualstudio.com/docs/python/python-tutorial) -- including the [WOW](https://code.visualstudio.com/docs/languages/python) page -- are ready. - [ ] Check to make sure any final updates to the **`release/YYYY.minor`** branch have been merged. -- [ ] Create a branch against **`release/YYYY.minor`** called **`finalized-release-[YYYY.minor]`**. -- [ ] Update the version in `package.json` to remove the `-rc` (πŸ€–) from the version. -- [ ] Run `npm install` to make sure `package-lock.json` is up-to-date _(the only update should be the version number if `package-lock.json` has been kept up-to-date)_. (πŸ€–) -- [ ] Update `ThirdPartyNotices-Repository.txt` manually if necessary. -- [ ] Create a PR from **`finalized-release-[YYYY.minor]`** against `release/YYYY.minor` and merge it. - ### Step 7: Execute the Release - [ ] Make sure CI is passing for **`release/YYYY.minor`** release branch (πŸ€–). @@ -79,7 +98,41 @@ NOTE: this PR should make all CI relating to `main` be passing again (such as th - [ ] Determine if a hotfix is needed. - [ ] Merge the release branch **`release/YYYY.minor`** back into `main`. (This step is only required if changes were merged into the release branch. If the only change made on the release branch is the version, this is not necessary. Overall you need to ensure you DO NOT overwrite the version on the `main` branch.) + +## Steps for Point Release (if necessary) +- [ ] checkout to `main` on your local machine and run `git fetch` to ensure your local is up to date with the remote repo. +- [ ] checkout to the `release/YYY.minor` and check to make sure all necessary changes for the point release have been cherry-picked into the release branch. If not, contact the owner of the changes to do so. +- [ ] Create a branch against **`release/YYYY.minor`** called **`release-[YYYY.minor.point]`**. +- [ ] Bump the point version number in the `package.json` to the next `YYYY.minor.point` +- [ ] Run `npm install` to make sure `package-lock.json` is up-to-date _(you should now see changes to the `package.json` and `package-lock.json` only relating to the new version number)_ . (πŸ€–) +- [ ] If Point Release is due to an issue in `pet`. Update `build\azure-pipeline.stable.yml` to point to the branch `release/YYYY.minor` for `python-environment-tools` with the fix or decided by the team. +- [ ] Create a PR from this branch against `release/YYYY.minor` +- [ ] **Rebase** and merge this PR into the release branch +- [ ] Create a draft GitHub release for the release notes (πŸ€–) ❄️ + - [ ] Create a new [GitHub release](https://github.com/microsoft/vscode-python/releases/new). + - [ ] Specify a new tag called `vYYYY.minor.point`. + - [ ] Have the `target` for the github release be your release branch called **`release/YYYY.minor`**. + - [ ] Create the release notes by specifying the previous tag as the previous version of stable, so the minor release **`vYYYY.minor`** for the last stable release and click `Generate release notes`. + - [ ] Check the generated notes to ensure that all PRs for the point release are included so users know these new changes. + - [ ] Click `Save draft`. +- [ ] Publish the point release + - [ ] Make sure CI is passing for **`release/YYYY.minor`** release branch (πŸ€–). + - [ ] Run the [CD](https://dev.azure.com/monacotools/Monaco/_build?definitionId=299) pipeline on the **`release/YYYY.minor`** branch. + - [ ] Click `run pipeline`. + - [ ] for `branch/tag` select the release branch which is **`release/YYYY.minor`**. + - [ ] 🧍🧍 Get approval on the release on the [CD](https://dev.azure.com/monacotools/Monaco/_build?definitionId=299) and publish the release to the marketplace. πŸŽ‰ + - [ ] Take the Github release out of draft. + +## Steps for contributing to a point release +- [ ] Work with team to decide if point release is necessary +- [ ] Work with team or users to verify the fix is correct and solves the problem without creating any new ones +- [ ] Create PR/PRs and merge then each into main as usual +- [ ] Make sure to still mark if the change is "bug" or "no-changelog" +- [ ] Cherry-pick all PRs to the release branch and check that the changes are in before the package is bumped +- [ ] Notify the release champ that your changes are in so they can trigger a point-release + + ## Prep for the _next_ release - [ ] Create a new [release plan](https://raw.githubusercontent.com/microsoft/vscode-python/main/.github/release_plan.md). (πŸ€–) -- [ ] [(Un-)pin](https://help.github.com/en/articles/pinning-an-issue-to-your-repository) [release plan issues](https://github.com/Microsoft/vscode-python/labels/release%20plan) (πŸ€–) +- [ ] [(Un-)pin](https://help.github.com/en/articles/pinning-an-issue-to-your-repository) [release plan issues](https://github.com/Microsoft/vscode-python/labels/release-plan) (πŸ€–) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 805077ffdb46..09d019dec4a7 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -8,8 +8,10 @@ on: - 'release/*' - 'release-*' +permissions: {} + env: - NODE_VERSION: 16.17.1 + NODE_VERSION: 22.21.1 PYTHON_VERSION: '3.10' # YML treats 3.10 the number as 3.1, so quotes around 3.10 # Force a path with spaces and to test extension works in these scenarios # Unicode characters are causing 2.7 failures so skip that for now. @@ -28,6 +30,7 @@ jobs: run: shell: python outputs: + vsix_basename: ${{ steps.vsix_names.outputs.vsix_basename }} vsix_name: ${{ steps.vsix_names.outputs.vsix_name }} vsix_artifact_name: ${{ steps.vsix_names.outputs.vsix_artifact_name }} steps: @@ -40,23 +43,71 @@ jobs: else: vsix_type = "release" print(f"::set-output name=vsix_name::ms-python-{vsix_type}.vsix") + print(f"::set-output name=vsix_basename::ms-python-{vsix_type}") print(f"::set-output name=vsix_artifact_name::ms-python-{vsix_type}-vsix") build-vsix: name: Create VSIX if: github.repository == 'microsoft/vscode-python' needs: setup - runs-on: ubuntu-latest + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + include: + - os: windows-latest + target: x86_64-pc-windows-msvc + vsix-target: win32-x64 + - os: windows-latest + target: aarch64-pc-windows-msvc + vsix-target: win32-arm64 + - os: ubuntu-latest + target: x86_64-unknown-linux-musl + vsix-target: linux-x64 + # - os: ubuntu-latest + # target: aarch64-unknown-linux-gnu + # vsix-target: linux-arm64 + # - os: ubuntu-latest + # target: arm-unknown-linux-gnueabihf + # vsix-target: linux-armhf + # - os: macos-latest + # target: x86_64-apple-darwin + # vsix-target: darwin-x64 + # - os: macos-14 + # target: aarch64-apple-darwin + # vsix-target: darwin-arm64 + - os: ubuntu-latest + target: x86_64-unknown-linux-musl + vsix-target: alpine-x64 + # - os: ubuntu-latest + # target: aarch64-unknown-linux-musl + # vsix-target: alpine-arm64 steps: - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v6 + with: + persist-credentials: false + + - name: Checkout Python Environment Tools + uses: actions/checkout@v6 + with: + repository: 'microsoft/python-environment-tools' + path: 'python-env-tools' + persist-credentials: false + sparse-checkout: | + crates + Cargo.toml + Cargo.lock + sparse-checkout-cone-mode: false - name: Build VSIX uses: ./.github/actions/build-vsix with: - node_version: ${{ env.NODE_VERSION }} - vsix_name: ${{ needs.setup.outputs.vsix_name }} - artifact_name: ${{ needs.setup.outputs.vsix_artifact_name }} + node_version: ${{ env.NODE_VERSION}} + vsix_name: ${{ needs.setup.outputs.vsix_basename }}-${{ matrix.vsix-target }}.vsix + artifact_name: ${{ needs.setup.outputs.vsix_artifact_name }}-${{ matrix.vsix-target }} + cargo_target: ${{ matrix.target }} + vsix_target: ${{ matrix.vsix-target }} lint: name: Lint @@ -64,7 +115,9 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v6 + with: + persist-credentials: false - name: Lint uses: ./.github/actions/lint @@ -77,35 +130,76 @@ jobs: runs-on: ubuntu-latest steps: - name: Use Python ${{ env.PYTHON_VERSION }} - uses: actions/setup-python@v4 + uses: actions/setup-python@v6 with: python-version: ${{ env.PYTHON_VERSION }} - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v6 + with: + persist-credentials: false - name: Install core Python requirements - uses: brettcannon/pip-secure-install@v1 + uses: brettcannon/pip-secure-install@92f400e3191171c1858cc0e0d9ac6320173fdb0c # v1.0.0 with: - options: '-t ./pythonFiles/lib/python --no-cache-dir --implementation py' + options: '-t ./python_files/lib/python --no-cache-dir --implementation py' - name: Install Jedi requirements - uses: brettcannon/pip-secure-install@v1 + uses: brettcannon/pip-secure-install@92f400e3191171c1858cc0e0d9ac6320173fdb0c # v1.0.0 with: - requirements-file: './pythonFiles/jedilsp_requirements/requirements.txt' - options: '-t ./pythonFiles/lib/jedilsp --no-cache-dir --implementation py' + requirements-file: './python_files/jedilsp_requirements/requirements.txt' + options: '-t ./python_files/lib/jedilsp --no-cache-dir --implementation py' - name: Install other Python requirements run: | - python -m pip --disable-pip-version-check install -t ./pythonFiles/lib/python --no-cache-dir --implementation py --no-deps --upgrade --pre debugpy python -m pip install --upgrade -r build/test-requirements.txt - name: Run Pyright - uses: jakebailey/pyright-action@v1 + uses: jakebailey/pyright-action@8ec14b5cfe41f26e5f41686a31eb6012758217ef # v3.0.2 with: - working-directory: 'pythonFiles' + version: 1.1.308 + working-directory: 'python_files' + + python-tests: + name: Python Tests + # The value of runs-on is the OS of the current job (specified in the strategy matrix below) instead of being hardcoded. + runs-on: ${{ matrix.os }} + defaults: + run: + working-directory: ${{ env.special-working-directory }} + strategy: + fail-fast: false + matrix: + # We're not running CI on macOS for now because it's one less matrix entry to lower the number of runners used, + # macOS runners are expensive, and we assume that Ubuntu is enough to cover the Unix case. + os: [ubuntu-latest, windows-latest] + # Run the tests on the oldest and most recent versions of Python. + python: ['3.10', '3.x', '3.13'] + + steps: + - name: Checkout + uses: actions/checkout@v6 + with: + path: ${{ env.special-working-directory-relative }} + persist-credentials: false + + - name: Use Python ${{ matrix.python }} + uses: actions/setup-python@v6 + with: + python-version: ${{ matrix.python }} + + - name: Install base Python requirements + uses: brettcannon/pip-secure-install@92f400e3191171c1858cc0e0d9ac6320173fdb0c # v1.0.0 + with: + requirements-file: '"${{ env.special-working-directory-relative }}/requirements.txt"' + options: '-t "${{ env.special-working-directory-relative }}/python_files/lib/python" --no-cache-dir --implementation py' + + - name: Install test requirements + run: python -m pip install --upgrade -r build/test-requirements.txt + + - name: Run Python unit tests + run: python python_files/tests/run_all.py - ### Non-smoke tests tests: name: Tests if: github.repository == 'microsoft/vscode-python' @@ -121,15 +215,28 @@ jobs: # and we assume that Ubuntu is enough to cover the UNIX case. os: [ubuntu-latest, windows-latest] python: ['3.x'] - test-suite: [ts-unit, python-unit, venv, single-workspace, multi-workspace, debugger, functional] + test-suite: [ts-unit, venv, single-workspace, multi-workspace, debugger, functional] steps: - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v6 with: path: ${{ env.special-working-directory-relative }} + persist-credentials: false + + - name: Checkout Python Environment Tools + uses: actions/checkout@v6 + with: + repository: 'microsoft/python-environment-tools' + path: ${{ env.special-working-directory-relative }}/python-env-tools + persist-credentials: false + sparse-checkout: | + crates + Cargo.toml + Cargo.lock + sparse-checkout-cone-mode: false - name: Install Node - uses: actions/setup-node@v3 + uses: actions/setup-node@v6 with: node-version: ${{ env.NODE_VERSION }} cache: 'npm' @@ -141,40 +248,33 @@ jobs: - name: Compile run: npx gulp prePublishNonBundle + - name: Localization + run: npx @vscode/l10n-dev@latest export ./src + - name: Install Python ${{ matrix.python }} - uses: actions/setup-python@v4 + uses: actions/setup-python@v6 with: python-version: ${{ matrix.python }} - - name: Install debugpy - run: | - # We need to have debugpy so that tests relying on it keep passing, but we don't need install_debugpy's logic in the test phase. - python -m pip --disable-pip-version-check install -t ./pythonFiles/lib/python --no-cache-dir --implementation py --no-deps --upgrade --pre debugpy + - name: Upgrade Pip + run: python -m pip install -U pip - - name: Install core Python requirements - uses: brettcannon/pip-secure-install@v1 - with: - requirements-file: '"${{ env.special-working-directory-relative }}/requirements.txt"' - options: '-t "${{ env.special-working-directory-relative }}/pythonFiles/lib/python" --no-cache-dir --implementation py' - if: startsWith(matrix.python, 3.) + # For faster/better builds of sdists. + - name: Install build pre-requisite + run: python -m pip install wheel nox - - name: Install Jedi requirements - uses: brettcannon/pip-secure-install@v1 - with: - requirements-file: '"${{ env.special-working-directory-relative }}/pythonFiles/jedilsp_requirements/requirements.txt"' - options: '-t "${{ env.special-working-directory-relative }}/pythonFiles/lib/jedilsp" --no-cache-dir --implementation py' - if: startsWith(matrix.python, 3.) + - name: Install Python Extension dependencies (jedi, etc.) + run: nox --session install_python_libs - name: Install test requirements run: python -m pip install --upgrade -r build/test-requirements.txt - - name: Install debugpy wheels (Python ${{ matrix.python }}) - run: | - python -m pip install wheel - python -m pip --disable-pip-version-check install -r build/debugger-install-requirements.txt - python ./pythonFiles/install_debugpy.py + - name: Rust Tool Chain setup + uses: dtolnay/rust-toolchain@stable + + - name: Build Native Binaries + run: nox --session native_build shell: bash - if: matrix.test-suite == 'debugger' - name: Install functional test requirements run: python -m pip install --upgrade -r ./build/functional-test-requirements.txt @@ -236,7 +336,7 @@ jobs: shell: pwsh if: matrix.test-suite == 'venv' run: | - # 1. For `terminalActivation.testvirtualenvs.test.ts` + # 1. For `*.testvirtualenvs.test.ts` if ('${{ matrix.os }}' -match 'windows-latest') { $condaPythonPath = Join-Path -Path $Env:CONDA -ChildPath python.exe $condaExecPath = Join-Path -Path $Env:CONDA -ChildPath Scripts | Join-Path -ChildPath conda @@ -260,10 +360,6 @@ jobs: run: npm run test:unittests if: matrix.test-suite == 'ts-unit' && startsWith(matrix.python, '3.') - - name: Run Python unit tests - run: python pythonFiles/tests/run_all.py - if: matrix.test-suite == 'python-unit' - # The virtual environment based tests use the `testSingleWorkspace` set of tests # with the environment variable `TEST_FILES_SUFFIX` set to `testvirtualenvs`, # which is set in the "Prepare environment for venv tests" step. @@ -274,7 +370,7 @@ jobs: env: TEST_FILES_SUFFIX: testvirtualenvs CI_PYTHON_VERSION: ${{ matrix.python }} - uses: GabrielBB/xvfb-action@v1.6 + uses: GabrielBB/xvfb-action@b706e4e27b14669b486812790492dc50ca16b465 # v1.7 with: run: npm run testSingleWorkspace working-directory: ${{ env.special-working-directory }} @@ -283,7 +379,7 @@ jobs: - name: Run single-workspace tests env: CI_PYTHON_VERSION: ${{ matrix.python }} - uses: GabrielBB/xvfb-action@v1.6 + uses: GabrielBB/xvfb-action@b706e4e27b14669b486812790492dc50ca16b465 # v1.7 with: run: npm run testSingleWorkspace working-directory: ${{ env.special-working-directory }} @@ -292,7 +388,7 @@ jobs: - name: Run multi-workspace tests env: CI_PYTHON_VERSION: ${{ matrix.python }} - uses: GabrielBB/xvfb-action@v1.6 + uses: GabrielBB/xvfb-action@b706e4e27b14669b486812790492dc50ca16b465 # v1.7 with: run: npm run testMultiWorkspace working-directory: ${{ env.special-working-directory }} @@ -301,7 +397,7 @@ jobs: - name: Run debugger tests env: CI_PYTHON_VERSION: ${{ matrix.python }} - uses: GabrielBB/xvfb-action@v1.6 + uses: GabrielBB/xvfb-action@b706e4e27b14669b486812790492dc50ca16b465 # v1.7 with: run: npm run testDebugger working-directory: ${{ env.special-working-directory }} @@ -322,13 +418,32 @@ jobs: matrix: # We're not running CI on macOS for now because it's one less matrix entry to lower the number of runners used, # macOS runners are expensive, and we assume that Ubuntu is enough to cover the UNIX case. - os: [ubuntu-latest, windows-latest] + include: + - os: windows-latest + vsix-target: win32-x64 + - os: ubuntu-latest + vsix-target: linux-x64 + steps: - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v6 + with: + persist-credentials: false + + - name: Checkout Python Environment Tools + uses: actions/checkout@v6 + with: + repository: 'microsoft/python-environment-tools' + path: ${{ env.special-working-directory-relative }}/python-env-tools + persist-credentials: false + sparse-checkout: | + crates + Cargo.toml + Cargo.lock + sparse-checkout-cone-mode: false - name: Smoke tests uses: ./.github/actions/smoke-tests with: node_version: ${{ env.NODE_VERSION }} - artifact_name: ${{ needs.setup.outputs.vsix_artifact_name }} + artifact_name: ${{ needs.setup.outputs.vsix_artifact_name }}-${{ matrix.vsix-target }} diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 278c2cf22e4a..5528fbbe9c0a 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -36,11 +36,13 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v3 + uses: actions/checkout@v6 + with: + persist-credentials: false # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@v2 + uses: github/codeql-action/init@v4 with: languages: ${{ matrix.language }} # If you wish to specify custom queries, you can do so here or in a config file. @@ -65,4 +67,4 @@ jobs: # make release - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v2 + uses: github/codeql-action/analyze@v4 diff --git a/.github/workflows/community-feedback-auto-comment.yml b/.github/workflows/community-feedback-auto-comment.yml index 57bbd97bf430..27f93400a023 100644 --- a/.github/workflows/community-feedback-auto-comment.yml +++ b/.github/workflows/community-feedback-auto-comment.yml @@ -8,22 +8,21 @@ jobs: add-comment: if: github.event.label.name == 'needs community feedback' runs-on: ubuntu-latest - permissions: issues: write - steps: - - name: Checkout Actions - uses: actions/checkout@v3 + - name: Check For Existing Comment + uses: peter-evans/find-comment@b30e6a3c0ed37e7c023ccd3f1db5c6c0b0c23aad # v4.0.0 + id: finder with: - repository: 'microsoft/vscode-github-triage-actions' - ref: stable - path: ./actions - - - name: Install Actions - run: npm install --production --prefix ./actions + issue-number: ${{ github.event.issue.number }} + comment-author: 'github-actions[bot]' + body-includes: 'Thanks for the feature request! We are going to give the community' - - name: Add Community Feedback Comment if applicable - uses: ./actions/python-community-feedback-auto-comment + - name: Add Community Feedback Comment + if: steps.finder.outputs.comment-id == '' + uses: peter-evans/create-or-update-comment@e8674b075228eee787fea43ef493e45ece1004c9 # v5.0.0 with: issue-number: ${{ github.event.issue.number }} + body: | + Thanks for the feature request! We are going to give the community 60 days from when this issue was created to provide 7 πŸ‘ upvotes on the opening comment to gauge general interest in this idea. If there's enough upvotes then we will consider this feature request in our future planning. If there's unfortunately not enough upvotes then we will close this issue. diff --git a/.github/workflows/gen-issue-velocity.yml b/.github/workflows/gen-issue-velocity.yml new file mode 100644 index 000000000000..41d79e4074d0 --- /dev/null +++ b/.github/workflows/gen-issue-velocity.yml @@ -0,0 +1,34 @@ +name: Issues Summary + +on: + schedule: + - cron: '0 0 * * 2' # Runs every Tuesday at midnight + workflow_dispatch: + +permissions: + issues: read + +jobs: + generate-summary: + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v6 + with: + persist-credentials: false + + - name: Set up Python + uses: actions/setup-python@v6 + with: + python-version: '3.x' + + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install requests + + - name: Run summary script + run: python scripts/issue_velocity_summary_script.py + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/getLabels.js b/.github/workflows/getLabels.js deleted file mode 100644 index 99060e7205eb..000000000000 --- a/.github/workflows/getLabels.js +++ /dev/null @@ -1,25 +0,0 @@ -/** - * To run this file: - * * npm install @octokit/rest - * * node .github/workflows/getLabels.js - * - * This script assumes the maximum number of labels to be 100. - */ - -const { Octokit } = require('@octokit/rest'); -const github = new Octokit(); -github.rest.issues - .listLabelsForRepo({ - owner: 'microsoft', - repo: 'vscode-python', - per_page: 100, - }) - .then((result) => { - const labels = result.data.map((label) => label.name); - console.log( - '\nNumber of labels found:', - labels.length, - ", verify that it's the same as number of labels listed in https://github.com/microsoft/vscode-python/labels\n", - ); - console.log(JSON.stringify(labels), '\n'); - }); diff --git a/.github/workflows/info-needed-closer.yml b/.github/workflows/info-needed-closer.yml new file mode 100644 index 000000000000..46892a58e800 --- /dev/null +++ b/.github/workflows/info-needed-closer.yml @@ -0,0 +1,33 @@ +name: Info-Needed Closer +on: + schedule: + - cron: 20 12 * * * # 5:20am Redmond + repository_dispatch: + types: [trigger-needs-more-info] + workflow_dispatch: + +permissions: + issues: write + +jobs: + main: + runs-on: ubuntu-latest + steps: + - name: Checkout Actions + uses: actions/checkout@v6 + with: + repository: 'microsoft/vscode-github-triage-actions' + path: ./actions + persist-credentials: false + ref: stable + - name: Install Actions + run: npm install --production --prefix ./actions + - name: Run info-needed Closer + uses: ./actions/needs-more-info-closer + with: + token: ${{secrets.GITHUB_TOKEN}} + label: info-needed + closeDays: 30 + closeComment: "Because we have not heard back with the information we requested, we are closing this issue for now. If you are able to provide the info later on, then we will be happy to re-open this issue to pick up where we left off. \n\nHappy Coding!" + pingDays: 30 + pingComment: "Hey @${assignee}, this issue might need further attention.\n\n@${author}, you can help us out by closing this issue if the problem no longer exists, or adding more information." diff --git a/.github/workflows/issue-labels.yml b/.github/workflows/issue-labels.yml index 98ac4eaca81d..dcbd114086e2 100644 --- a/.github/workflows/issue-labels.yml +++ b/.github/workflows/issue-labels.yml @@ -5,9 +5,7 @@ on: types: [opened, reopened] env: - # To update the list of labels, see `getLabels.js`. - REPO_LABELS: '["area-data science","area-debugging","area-diagnostics","area-editor-*","area-environments","area-formatting","area-intellisense","area-internal","area-linting","area-terminal","area-testing","author-verification-requested","bug","community ask","debt","dependencies","documentation","experimenting","feature-request","good first issue","help wanted","important","info-needed","invalid-testplan-item","investigating","iteration-candidate","iteration-plan","iteration-plan-draft","javascript","linux","macos","meta","needs community feedback","needs PR","needs proposal","needs spike","no-changelog","on-testplan","partner ask","regression","release-plan","reports-wanted","skip package*.json","skip tests","tensorboard","testplan-item","triage-needed","verification-found","verification-needed","verification-steps-needed","verified","windows"]' - TRIAGERS: '["karrtikr","karthiknadig","paulacamargo25","eleanorjboyd"]' + TRIAGERS: '["karthiknadig","eleanorjboyd","anthonykim1"]' permissions: issues: write @@ -15,22 +13,22 @@ permissions: jobs: # From https://github.com/marketplace/actions/github-script#apply-a-label-to-an-issue. add-classify-label: - name: "Add 'triage-needed' and remove unrecognizable labels & assignees" + name: "Add 'triage-needed' and remove assignees" runs-on: ubuntu-latest steps: - name: Checkout Actions - uses: actions/checkout@v3 + uses: actions/checkout@v6 with: repository: 'microsoft/vscode-github-triage-actions' ref: stable path: ./actions + persist-credentials: false - name: Install Actions run: npm install --production --prefix ./actions - - name: "Add 'triage-needed' and remove unrecognizable labels & assignees" + - name: "Add 'triage-needed' and remove assignees" uses: ./actions/python-issue-labels with: triagers: ${{ env.TRIAGERS }} token: ${{secrets.GITHUB_TOKEN}} - repo_labels: ${{ env.REPO_LABELS }} diff --git a/.github/workflows/lock-issues.yml b/.github/workflows/lock-issues.yml index 8c828ff766cb..544d04ee185e 100644 --- a/.github/workflows/lock-issues.yml +++ b/.github/workflows/lock-issues.yml @@ -15,17 +15,10 @@ jobs: lock-issues: runs-on: ubuntu-latest steps: - - name: Checkout Actions - uses: actions/checkout@v3 - with: - repository: 'microsoft/vscode-github-triage-actions' - ref: stable - path: ./actions - - - name: Install Actions - run: npm install --production --prefix ./actions - - name: 'Lock Issues' - uses: ./actions/python-lock-issues + uses: dessant/lock-threads@7266a7ce5c1df01b1c6db85bf8cd86c737dadbe7 # v6.0.0 with: - token: ${{ github.token }} + github-token: ${{ github.token }} + issue-inactive-days: '30' + process-only: 'issues' + log-output: true diff --git a/.github/workflows/pr-check.yml b/.github/workflows/pr-check.yml index 2ac560af995d..c8a6f2dd416e 100644 --- a/.github/workflows/pr-check.yml +++ b/.github/workflows/pr-check.yml @@ -7,12 +7,13 @@ on: - main - release* +permissions: {} + env: - NODE_VERSION: 16.17.1 + NODE_VERSION: 22.21.1 PYTHON_VERSION: '3.10' # YML treats 3.10 the number as 3.1, so quotes around 3.10 MOCHA_REPORTER_JUNIT: true # Use the mocha-multi-reporters and send output to both console (spec) and JUnit (mocha-junit-reporter). Also enables a reporter which exits the process running the tests if it haven't already. ARTIFACT_NAME_VSIX: ms-python-insiders-vsix - VSIX_NAME: ms-python-insiders.vsix TEST_RESULTS_DIRECTORY: . # Force a path with spaces and to test extension works in these scenarios # Unicode characters are causing 2.7 failures so skip that for now. @@ -22,24 +23,73 @@ env: jobs: build-vsix: name: Create VSIX - runs-on: ubuntu-latest + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + include: + - os: windows-latest + target: x86_64-pc-windows-msvc + vsix-target: win32-x64 + - os: windows-latest + target: aarch64-pc-windows-msvc + vsix-target: win32-arm64 + - os: ubuntu-latest + target: x86_64-unknown-linux-musl + vsix-target: linux-x64 + # - os: ubuntu-latest + # target: aarch64-unknown-linux-gnu + # vsix-target: linux-arm64 + # - os: ubuntu-latest + # target: arm-unknown-linux-gnueabihf + # vsix-target: linux-armhf + # - os: macos-latest + # target: x86_64-apple-darwin + # vsix-target: darwin-x64 + # - os: macos-14 + # target: aarch64-apple-darwin + # vsix-target: darwin-arm64 + - os: ubuntu-latest + target: x86_64-unknown-linux-musl + vsix-target: alpine-x64 + # - os: ubuntu-latest + # target: aarch64-unknown-linux-musl + # vsix-target: alpine-arm64 steps: - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v6 + with: + persist-credentials: false + + - name: Checkout Python Environment Tools + uses: actions/checkout@v6 + with: + repository: 'microsoft/python-environment-tools' + path: 'python-env-tools' + persist-credentials: false + sparse-checkout: | + crates + Cargo.toml + Cargo.lock + sparse-checkout-cone-mode: false - name: Build VSIX uses: ./.github/actions/build-vsix with: node_version: ${{ env.NODE_VERSION}} - vsix_name: ${{ env.VSIX_NAME }} - artifact_name: ${{ env.ARTIFACT_NAME_VSIX }} + vsix_name: 'ms-python-insiders-${{ matrix.vsix-target }}.vsix' + artifact_name: '${{ env.ARTIFACT_NAME_VSIX }}-${{ matrix.vsix-target }}' + cargo_target: ${{ matrix.target }} + vsix_target: ${{ matrix.vsix-target }} lint: name: Lint runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v6 + with: + persist-credentials: false - name: Lint uses: ./.github/actions/lint @@ -51,35 +101,101 @@ jobs: runs-on: ubuntu-latest steps: - name: Use Python ${{ env.PYTHON_VERSION }} - uses: actions/setup-python@v4 + uses: actions/setup-python@v6 with: python-version: ${{ env.PYTHON_VERSION }} - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v6 + with: + persist-credentials: false + + - name: Checkout Python Environment Tools + uses: actions/checkout@v6 + with: + repository: 'microsoft/python-environment-tools' + path: 'python-env-tools' + persist-credentials: false + sparse-checkout: | + crates + Cargo.toml + Cargo.lock + sparse-checkout-cone-mode: false - name: Install base Python requirements - uses: brettcannon/pip-secure-install@v1 + uses: brettcannon/pip-secure-install@92f400e3191171c1858cc0e0d9ac6320173fdb0c # v1.0.0 with: - options: '-t ./pythonFiles/lib/python --no-cache-dir --implementation py' + options: '-t ./python_files/lib/python --no-cache-dir --implementation py' - name: Install Jedi requirements - uses: brettcannon/pip-secure-install@v1 + uses: brettcannon/pip-secure-install@92f400e3191171c1858cc0e0d9ac6320173fdb0c # v1.0.0 with: - requirements-file: './pythonFiles/jedilsp_requirements/requirements.txt' - options: '-t ./pythonFiles/lib/jedilsp --no-cache-dir --implementation py' + requirements-file: './python_files/jedilsp_requirements/requirements.txt' + options: '-t ./python_files/lib/jedilsp --no-cache-dir --implementation py' - name: Install other Python requirements run: | - python -m pip --disable-pip-version-check install -t ./pythonFiles/lib/python --no-cache-dir --implementation py --no-deps --upgrade --pre debugpy python -m pip install --upgrade -r build/test-requirements.txt - name: Run Pyright - uses: jakebailey/pyright-action@v1 + uses: jakebailey/pyright-action@8ec14b5cfe41f26e5f41686a31eb6012758217ef # v3.0.2 with: - working-directory: 'pythonFiles' + version: 1.1.308 + working-directory: 'python_files' + + python-tests: + name: Python Tests + # The value of runs-on is the OS of the current job (specified in the strategy matrix below) instead of being hardcoded. + runs-on: ${{ matrix.os }} + defaults: + run: + working-directory: ${{ env.special-working-directory }} + strategy: + fail-fast: false + matrix: + # We're not running CI on macOS for now because it's one less matrix entry to lower the number of runners used, + # macOS runners are expensive, and we assume that Ubuntu is enough to cover the Unix case. + os: [ubuntu-latest, windows-latest] + # Run the tests on the oldest and most recent versions of Python. + python: ['3.10', '3.x', '3.13'] # run for 3 pytest versions, most recent stable, oldest version supported and pre-release + pytest-version: ['pytest', 'pytest@pre-release', 'pytest==6.2.0'] + + steps: + - name: Checkout + uses: actions/checkout@v6 + with: + path: ${{ env.special-working-directory-relative }} + persist-credentials: false + + - name: Use Python ${{ matrix.python }} + uses: actions/setup-python@v6 + with: + python-version: ${{ matrix.python }} + + - name: Install specific pytest version + if: matrix.pytest-version == 'pytest@pre-release' + run: | + python -m pip install --pre pytest + + - name: Install specific pytest version + if: matrix.pytest-version != 'pytest@pre-release' + run: | + python -m pip install "${{ matrix.pytest-version }}" + + - name: Install specific pytest version + run: python -m pytest --version + - name: Install base Python requirements + uses: brettcannon/pip-secure-install@92f400e3191171c1858cc0e0d9ac6320173fdb0c # v1.0.0 + with: + requirements-file: '"${{ env.special-working-directory-relative }}/requirements.txt"' + options: '-t "${{ env.special-working-directory-relative }}/python_files/lib/python" --no-cache-dir --implementation py' + + - name: Install test requirements + run: python -m pip install --upgrade -r build/test-requirements.txt + + - name: Run Python unit tests + run: python python_files/tests/run_all.py - ### Non-smoke tests tests: name: Tests # The value of runs-on is the OS of the current job (specified in the strategy matrix below) instead of being hardcoded. @@ -95,16 +211,29 @@ jobs: os: [ubuntu-latest, windows-latest] # Run the tests on the oldest and most recent versions of Python. python: ['3.x'] - test-suite: [ts-unit, python-unit, venv, single-workspace, debugger, functional] + test-suite: [ts-unit, venv, single-workspace, debugger, functional] steps: - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v6 with: path: ${{ env.special-working-directory-relative }} + persist-credentials: false + + - name: Checkout Python Environment Tools + uses: actions/checkout@v6 + with: + repository: 'microsoft/python-environment-tools' + path: ${{ env.special-working-directory-relative }}/python-env-tools + persist-credentials: false + sparse-checkout: | + crates + Cargo.toml + Cargo.lock + sparse-checkout-cone-mode: false - name: Install Node - uses: actions/setup-node@v3 + uses: actions/setup-node@v6 with: node-version: ${{ env.NODE_VERSION }} cache: 'npm' @@ -116,40 +245,33 @@ jobs: - name: Compile run: npx gulp prePublishNonBundle + - name: Localization + run: npx @vscode/l10n-dev@latest export ./src + - name: Use Python ${{ matrix.python }} - uses: actions/setup-python@v4 + uses: actions/setup-python@v6 with: python-version: ${{ matrix.python }} - - name: Install debugpy - run: | - # We need to have debugpy so that tests relying on it keep passing, but we don't need install_debugpy's logic in the test phase. - python -m pip --disable-pip-version-check install -t ./pythonFiles/lib/python --no-cache-dir --implementation py --no-deps --upgrade --pre debugpy + - name: Upgrade Pip + run: python -m pip install -U pip - - name: Install base Python requirements - uses: brettcannon/pip-secure-install@v1 - with: - requirements-file: '"${{ env.special-working-directory-relative }}/requirements.txt"' - options: '-t "${{ env.special-working-directory-relative }}/pythonFiles/lib/python" --no-cache-dir --implementation py' - if: startsWith(matrix.python, 3.) + # For faster/better builds of sdists. + - name: Install build pre-requisite + run: python -m pip install wheel nox - - name: Install Jedi requirements - uses: brettcannon/pip-secure-install@v1 - with: - requirements-file: '"${{ env.special-working-directory-relative }}/pythonFiles/jedilsp_requirements/requirements.txt"' - options: '-t "${{ env.special-working-directory-relative }}/pythonFiles/lib/jedilsp" --no-cache-dir --implementation py' - if: startsWith(matrix.python, 3.) + - name: Install Python Extension dependencies (jedi, etc.) + run: nox --session install_python_libs - name: Install test requirements run: python -m pip install --upgrade -r build/test-requirements.txt - - name: Install debugpy wheels (Python ${{ matrix.python }}) - run: | - python -m pip install wheel - python -m pip --disable-pip-version-check install -r build/debugger-install-requirements.txt - python ./pythonFiles/install_debugpy.py + - name: Rust Tool Chain setup + uses: dtolnay/rust-toolchain@stable + + - name: Build Native Binaries + run: nox --session native_build shell: bash - if: matrix.test-suite == 'debugger' - name: Install functional test requirements run: python -m pip install --upgrade -r ./build/functional-test-requirements.txt @@ -211,7 +333,7 @@ jobs: shell: pwsh if: matrix.test-suite == 'venv' run: | - # 1. For `terminalActivation.testvirtualenvs.test.ts` + # 1. For `*.testvirtualenvs.test.ts` if ('${{ matrix.os }}' -match 'windows-latest') { $condaPythonPath = Join-Path -Path $Env:CONDA -ChildPath python.exe $condaExecPath = Join-Path -Path $Env:CONDA -ChildPath Scripts | Join-Path -ChildPath conda @@ -235,12 +357,6 @@ jobs: run: npm run test:unittests if: matrix.test-suite == 'ts-unit' && startsWith(matrix.python, 3.) - # Run the Python tests in our codebase. - - name: Run Python unit tests - run: | - python pythonFiles/tests/run_all.py - if: matrix.test-suite == 'python-unit' - # The virtual environment based tests use the `testSingleWorkspace` set of tests # with the environment variable `TEST_FILES_SUFFIX` set to `testvirtualenvs`, # which is set in the "Prepare environment for venv tests" step. @@ -251,16 +367,16 @@ jobs: env: TEST_FILES_SUFFIX: testvirtualenvs CI_PYTHON_VERSION: ${{ matrix.python }} - uses: GabrielBB/xvfb-action@v1.6 + uses: GabrielBB/xvfb-action@b706e4e27b14669b486812790492dc50ca16b465 # v1.7 with: run: npm run testSingleWorkspace working-directory: ${{ env.special-working-directory }} - if: matrix.test-suite == 'venv' && matrix.os == 'ubuntu-latest' + if: matrix.test-suite == 'venv' - name: Run single-workspace tests env: CI_PYTHON_VERSION: ${{ matrix.python }} - uses: GabrielBB/xvfb-action@v1.6 + uses: GabrielBB/xvfb-action@b706e4e27b14669b486812790492dc50ca16b465 # v1.7 with: run: npm run testSingleWorkspace working-directory: ${{ env.special-working-directory }} @@ -269,7 +385,7 @@ jobs: - name: Run debugger tests env: CI_PYTHON_VERSION: ${{ matrix.python }} - uses: GabrielBB/xvfb-action@v1.6 + uses: GabrielBB/xvfb-action@b706e4e27b14669b486812790492dc50ca16b465 # v1.7 with: run: npm run testDebugger working-directory: ${{ env.special-working-directory }} @@ -280,6 +396,43 @@ jobs: run: npm run test:functional if: matrix.test-suite == 'functional' + native-tests: + name: Native Tests + # The value of runs-on is the OS of the current job (specified in the strategy matrix below) instead of being hardcoded. + runs-on: ${{ matrix.os }} + defaults: + run: + working-directory: ${{ env.special-working-directory }} + strategy: + fail-fast: false + matrix: + # We're not running CI on macOS for now because it's one less matrix entry to lower the number of runners used, + # macOS runners are expensive, and we assume that Ubuntu is enough to cover the Unix case. + os: [ubuntu-latest, windows-latest] + + steps: + - name: Checkout + uses: actions/checkout@v6 + with: + path: ${{ env.special-working-directory-relative }} + persist-credentials: false + + - name: Checkout Python Environment Tools + uses: actions/checkout@v6 + with: + repository: 'microsoft/python-environment-tools' + path: ${{ env.special-working-directory-relative }}/python-env-tools + persist-credentials: false + sparse-checkout: | + crates + Cargo.toml + Cargo.lock + sparse-checkout-cone-mode: false + + - name: Python Environment Tools tests + run: cargo test -- --nocapture + working-directory: ${{ env.special-working-directory }}/python-env-tools + smoke-tests: name: Smoke tests # The value of runs-on is the OS of the current job (specified in the strategy matrix below) instead of being hardcoded. @@ -290,23 +443,45 @@ jobs: matrix: # We're not running CI on macOS for now because it's one less matrix entry to lower the number of runners used, # macOS runners are expensive, and we assume that Ubuntu is enough to cover the UNIX case. - os: [ubuntu-latest, windows-latest] + include: + - os: windows-latest + vsix-target: win32-x64 + - os: ubuntu-latest + vsix-target: linux-x64 + steps: # Need the source to have the tests available. - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v6 + with: + persist-credentials: false + + - name: Checkout Python Environment Tools + uses: actions/checkout@v6 + with: + repository: 'microsoft/python-environment-tools' + path: python-env-tools + persist-credentials: false + sparse-checkout: | + crates + Cargo.toml + Cargo.lock + sparse-checkout-cone-mode: false - name: Smoke tests uses: ./.github/actions/smoke-tests with: node_version: ${{ env.NODE_VERSION }} - artifact_name: ${{ env.ARTIFACT_NAME_VSIX }} + artifact_name: '${{ env.ARTIFACT_NAME_VSIX }}-${{ matrix.vsix-target }}' ### Coverage run coverage: name: Coverage + # TEMPORARILY DISABLED - hanging in CI, needs investigation + if: false # The value of runs-on is the OS of the current job (specified in the strategy matrix below) instead of being hardcoded. runs-on: ${{ matrix.os }} + needs: [lint, check-types, python-tests, tests, native-tests] strategy: fail-fast: false matrix: @@ -315,10 +490,24 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v6 + with: + persist-credentials: false + + - name: Checkout Python Environment Tools + uses: actions/checkout@v6 + with: + repository: 'microsoft/python-environment-tools' + path: python-env-tools + persist-credentials: false + sparse-checkout: | + crates + Cargo.toml + Cargo.lock + sparse-checkout-cone-mode: false - name: Install Node - uses: actions/setup-node@v3 + uses: actions/setup-node@v6 with: node-version: ${{ env.NODE_VERSION }} cache: 'npm' @@ -329,32 +518,41 @@ jobs: - name: Compile run: npx gulp prePublishNonBundle + - name: Localization + run: npx @vscode/l10n-dev@latest export ./src + - name: Use Python ${{ env.PYTHON_VERSION }} - uses: actions/setup-python@v4 + uses: actions/setup-python@v6 with: python-version: ${{ env.PYTHON_VERSION }} cache: 'pip' cache-dependency-path: | requirements.txt - pythonFiles/jedilsp_requirements/requirements.txt + python_files/jedilsp_requirements/requirements.txt build/test-requirements.txt build/functional-test-requirements.txt - name: Install base Python requirements - uses: brettcannon/pip-secure-install@v1 + uses: brettcannon/pip-secure-install@92f400e3191171c1858cc0e0d9ac6320173fdb0c # v1.0.0 with: - options: '-t ./pythonFiles/lib/python --implementation py' + options: '-t ./python_files/lib/python --implementation py' - name: Install Jedi requirements - uses: brettcannon/pip-secure-install@v1 + uses: brettcannon/pip-secure-install@92f400e3191171c1858cc0e0d9ac6320173fdb0c # v1.0.0 with: - requirements-file: './pythonFiles/jedilsp_requirements/requirements.txt' - options: '-t ./pythonFiles/lib/jedilsp --implementation py' + requirements-file: './python_files/jedilsp_requirements/requirements.txt' + options: '-t ./python_files/lib/jedilsp --implementation py' - - name: Install debugpy - run: | - # We need to have debugpy so that tests relying on it keep passing, but we don't need install_debugpy's logic in the test phase. - python -m pip --disable-pip-version-check install -t ./pythonFiles/lib/python --implementation py --no-deps --upgrade --pre debugpy + - name: Install build pre-requisite + run: python -m pip install wheel nox + shell: bash + + - name: Rust Tool Chain setup + uses: dtolnay/rust-toolchain@stable + + - name: Build Native Binaries + run: nox --session native_build + shell: bash - name: Install test requirements run: python -m pip install --upgrade -r build/test-requirements.txt @@ -413,7 +611,7 @@ jobs: PYTHON_VIRTUAL_ENVS_LOCATION: './src/tmp/envPaths.json' shell: pwsh run: | - # 1. For `terminalActivation.testvirtualenvs.test.ts` + # 1. For `*.testvirtualenvs.test.ts` if ('${{ matrix.os }}' -match 'windows-latest') { $condaPythonPath = Join-Path -Path $Env:CONDA -ChildPath python.exe $condaExecPath = Join-Path -Path $Env:CONDA -ChildPath Scripts | Join-Path -ChildPath conda @@ -430,7 +628,7 @@ jobs: - name: Run Python unit tests run: | - python pythonFiles/tests/run_all.py + python python_files/tests/run_all.py # The virtual environment based tests use the `testSingleWorkspace` set of tests # with the environment variable `TEST_FILES_SUFFIX` set to `testvirtualenvs`, @@ -443,7 +641,7 @@ jobs: TEST_FILES_SUFFIX: testvirtualenvs CI_PYTHON_VERSION: ${{ env.PYTHON_VERSION }} CI_DISABLE_AUTO_SELECTION: 1 - uses: GabrielBB/xvfb-action@v1.6 + uses: GabrielBB/xvfb-action@b706e4e27b14669b486812790492dc50ca16b465 # v1.7 with: run: npm run testSingleWorkspace:cover @@ -451,7 +649,7 @@ jobs: env: CI_PYTHON_VERSION: ${{ env.PYTHON_VERSION }} CI_DISABLE_AUTO_SELECTION: 1 - uses: GabrielBB/xvfb-action@v1.6 + uses: GabrielBB/xvfb-action@b706e4e27b14669b486812790492dc50ca16b465 # v1.7 with: run: npm run testSingleWorkspace:cover @@ -460,7 +658,7 @@ jobs: # env: # CI_PYTHON_VERSION: ${{ env.PYTHON_VERSION }} # CI_DISABLE_AUTO_SELECTION: 1 - # uses: GabrielBB/xvfb-action@v1.6 + # uses: GabrielBB/xvfb-action@b706e4e27b14669b486812790492dc50ca16b465 # v1.7 # with: # run: npm run testMultiWorkspace:cover @@ -469,7 +667,7 @@ jobs: # env: # CI_PYTHON_VERSION: ${{ env.PYTHON_VERSION }} # CI_DISABLE_AUTO_SELECTION: 1 - # uses: GabrielBB/xvfb-action@v1.6 + # uses: GabrielBB/xvfb-action@b706e4e27b14669b486812790492dc50ca16b465 # v1.7 # with: # run: npm run testDebugger:cover @@ -484,7 +682,7 @@ jobs: run: npm run test:cover:report - name: Upload HTML report - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v7 with: name: ${{ runner.os }}-coverage-report-html path: ./coverage diff --git a/.github/workflows/pr-file-check.yml b/.github/workflows/pr-file-check.yml index 258e07daace7..6364e5fa744e 100644 --- a/.github/workflows/pr-file-check.yml +++ b/.github/workflows/pr-file-check.yml @@ -1,29 +1,44 @@ name: PR files + on: pull_request: types: - # On by default if you specify no types. - 'opened' - 'reopened' - 'synchronize' - # For `skip-label` only. - 'labeled' - 'unlabeled' +permissions: {} + jobs: changed-files-in-pr: name: 'Check for changed files' runs-on: ubuntu-latest steps: - - name: Checkout Actions - uses: actions/checkout@v3 + - name: 'package-lock.json matches package.json' + uses: brettcannon/check-for-changed-files@871d7b8b5917a4f6f06662e2262e8ffc51dff6d1 # v1.2.1 with: - repository: 'microsoft/vscode-github-triage-actions' - ref: stable - path: ./actions + prereq-pattern: 'package.json' + file-pattern: 'package-lock.json' + skip-label: 'skip package*.json' + failure-message: '${prereq-pattern} was edited but ${file-pattern} was not (the ${skip-label} label can be used to pass this check)' - - name: Install Actions - run: npm install --production --prefix ./actions + - name: 'package.json matches package-lock.json' + uses: brettcannon/check-for-changed-files@871d7b8b5917a4f6f06662e2262e8ffc51dff6d1 # v1.2.1 + with: + prereq-pattern: 'package-lock.json' + file-pattern: 'package.json' + skip-label: 'skip package*.json' + failure-message: '${prereq-pattern} was edited but ${file-pattern} was not (the ${skip-label} label can be used to pass this check)' - - name: Check for changed files - uses: ./actions/python-pr-file-check + - name: 'Tests' + uses: brettcannon/check-for-changed-files@871d7b8b5917a4f6f06662e2262e8ffc51dff6d1 # v1.2.1 + with: + prereq-pattern: src/**/*.ts + file-pattern: | + src/**/*.test.ts + src/**/*.testvirtualenvs.ts + .github/test_plan.md + skip-label: 'skip tests' + failure-message: 'TypeScript code was edited without also editing a ${file-pattern} file; see the Testing page in our wiki on testing guidelines (the ${skip-label} label can be used to pass this check)' diff --git a/.github/workflows/pr-issue-check.yml b/.github/workflows/pr-issue-check.yml new file mode 100644 index 000000000000..5587227d2848 --- /dev/null +++ b/.github/workflows/pr-issue-check.yml @@ -0,0 +1,31 @@ +name: PR issue check + +on: + pull_request: + types: + - 'opened' + - 'reopened' + - 'synchronize' + - 'labeled' + - 'unlabeled' + +permissions: {} + +jobs: + check-for-attached-issue: + name: 'Check for attached issue' + runs-on: ubuntu-latest + steps: + - name: 'Ensure PR has an associated issue' + uses: actions/github-script@v9 + with: + script: | + const labels = context.payload.pull_request.labels.map(label => label.name); + if (!labels.includes('skip-issue-check')) { + const prBody = context.payload.pull_request.body || ''; + const issueLink = prBody.match(/https:\/\/github\.com\/\S+\/issues\/\d+/); + const issueReference = prBody.match(/#\d+/); + if (!issueLink && !issueReference) { + core.setFailed('No associated issue found in the PR description.'); + } + } diff --git a/.github/workflows/pr-labels.yml b/.github/workflows/pr-labels.yml index 7ddb781e2a85..af24ac10772c 100644 --- a/.github/workflows/pr-labels.yml +++ b/.github/workflows/pr-labels.yml @@ -12,16 +12,13 @@ jobs: classify: name: 'Classify PR' runs-on: ubuntu-latest + permissions: + issues: write + pull-requests: write steps: - - name: Checkout Actions - uses: actions/checkout@v3 + - name: 'PR impact specified' + uses: mheap/github-action-required-labels@0ac283b4e65c1fb28ce6079dea5546ceca98ccbe # v5.5.2 with: - repository: 'microsoft/vscode-github-triage-actions' - ref: stable - path: ./actions - - - name: Install Actions - run: npm install --production --prefix ./actions - - - name: Classify PR - uses: ./actions/python-pr-labels + mode: exactly + count: 1 + labels: 'bug, debt, feature-request, no-changelog' diff --git a/.github/workflows/python27-issue-response.yml b/.github/workflows/python27-issue-response.yml index 4d51e9921ab4..9db84bca1a23 100644 --- a/.github/workflows/python27-issue-response.yml +++ b/.github/workflows/python27-issue-response.yml @@ -5,6 +5,8 @@ on: jobs: python27-issue-response: runs-on: ubuntu-latest + permissions: + issues: write if: "contains(github.event.issue.body, 'Python version (& distribution if applicable, e.g. Anaconda): 2.7')" steps: - name: Check for Python 2.7 string diff --git a/.github/workflows/remove-needs-labels.yml b/.github/workflows/remove-needs-labels.yml new file mode 100644 index 000000000000..24352526d0d8 --- /dev/null +++ b/.github/workflows/remove-needs-labels.yml @@ -0,0 +1,20 @@ +name: 'Remove Needs Label' +on: + issues: + types: [closed] + +jobs: + classify: + name: 'Remove needs labels on issue closing' + runs-on: ubuntu-latest + permissions: + issues: write + steps: + - name: 'Removes needs labels on issue close' + uses: actions-ecosystem/action-remove-labels@2ce5d41b4b6aa8503e285553f75ed56e0a40bae0 # v1.3.0 + with: + labels: | + needs PR + needs spike + needs community feedback + needs proposal diff --git a/.github/workflows/test-plan-item-validator.yml b/.github/workflows/test-plan-item-validator.yml index 9d0805a9db9b..57db4a3e18a7 100644 --- a/.github/workflows/test-plan-item-validator.yml +++ b/.github/workflows/test-plan-item-validator.yml @@ -12,10 +12,11 @@ jobs: if: contains(github.event.issue.labels.*.name, 'testplan-item') || contains(github.event.issue.labels.*.name, 'invalid-testplan-item') steps: - name: Checkout Actions - uses: actions/checkout@v3 + uses: actions/checkout@v6 with: repository: 'microsoft/vscode-github-triage-actions' path: ./actions + persist-credentials: false ref: stable - name: Install Actions diff --git a/.github/workflows/triage-info-needed.yml b/.github/workflows/triage-info-needed.yml index 1c384d824da5..c7a37ba0c78d 100644 --- a/.github/workflows/triage-info-needed.yml +++ b/.github/workflows/triage-info-needed.yml @@ -5,19 +5,22 @@ on: types: [created] env: - TRIAGERS: '["karrtikr","karthiknadig","paulacamargo25","eleanorjboyd", "brettcannon"]' + TRIAGERS: '["karrtikr","karthiknadig","paulacamargo25","eleanorjboyd", "brettcannon","anthonykim1"]' jobs: add_label: - runs-on: ubuntu-latest if: contains(github.event.issue.labels.*.name, 'triage-needed') && !contains(github.event.issue.labels.*.name, 'info-needed') + runs-on: ubuntu-latest + permissions: + issues: write steps: - name: Checkout Actions - uses: actions/checkout@v3 + uses: actions/checkout@v6 with: repository: 'microsoft/vscode-github-triage-actions' ref: stable path: ./actions + persist-credentials: false - name: Install Actions run: npm install --production --prefix ./actions @@ -32,13 +35,16 @@ jobs: remove_label: if: contains(github.event.issue.labels.*.name, 'info-needed') && contains(github.event.issue.labels.*.name, 'triage-needed') runs-on: ubuntu-latest + permissions: + issues: write steps: - name: Checkout Actions - uses: actions/checkout@v3 + uses: actions/checkout@v6 with: repository: 'microsoft/vscode-github-triage-actions' ref: stable path: ./actions + persist-credentials: false - name: Install Actions run: npm install --production --prefix ./actions diff --git a/.gitignore b/.gitignore index 0fc4c34d7127..2fa056f84fa6 100644 --- a/.gitignore +++ b/.gitignore @@ -5,6 +5,7 @@ log.log **/node_modules *.pyc *.vsix +envVars.txt **/.vscode/.ropeproject/** **/testFiles/**/.cache/** *.noseids @@ -22,7 +23,8 @@ cucumber-report.json **/.venv*/ port.txt precommit.hook -pythonFiles/lib/** +python_files/lib/** +python_files/get-pip.py debug_coverage*/** languageServer/** languageServer.*/** @@ -46,3 +48,11 @@ dist/** *.xlf package.nls.*.json l10n/ +python-env-tools/** +# coverage files produced as test output +python_files/tests/*/.data/.coverage* +python_files/tests/*/.data/*/.coverage* +src/testTestingRootWkspc/coverageWorkspace/.coverage + +# ignore ai artifacts generated and placed in this folder +ai-artifacts/* diff --git a/.nvmrc b/.nvmrc index e0325e5adb60..c6a66a6e6a68 100644 --- a/.nvmrc +++ b/.nvmrc @@ -1 +1 @@ -v16.17.1 +v22.21.1 diff --git a/.vscode/extensions.json b/.vscode/extensions.json index 5ade8dec4885..15e6aada1d50 100644 --- a/.vscode/extensions.json +++ b/.vscode/extensions.json @@ -2,11 +2,11 @@ // See https://go.microsoft.com/fwlink/?LinkId=827846 // for the documentation about the extensions.json format "recommendations": [ + "charliermarsh.ruff", "editorconfig.editorconfig", "esbenp.prettier-vscode", "dbaeumer.vscode-eslint", "ms-python.python", - "ms-python.black-formatter", "ms-python.vscode-pylance" ] } diff --git a/.vscode/launch.json b/.vscode/launch.json index 82981a93305d..1e983413c8d4 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -8,7 +8,6 @@ "request": "launch", "runtimeExecutable": "${execPath}", "args": ["--extensionDevelopmentPath=${workspaceFolder}"], - "stopOnEntry": false, "smartStep": true, "sourceMaps": true, "outFiles": ["${workspaceFolder}/out/**/*", "!${workspaceFolder}/**/node_modules**/*"], @@ -31,19 +30,11 @@ "request": "launch", "runtimeExecutable": "${execPath}", "args": ["--extensionDevelopmentPath=${workspaceFolder}", "${workspaceFolder}/data"], - "stopOnEntry": false, "smartStep": true, "sourceMaps": true, "outFiles": ["${workspaceFolder}/out/**/*", "!${workspaceFolder}/**/node_modules**/*"], "preLaunchTask": "Compile" }, - { - "name": "Python: Current File", - "type": "python", - "request": "launch", - "program": "${file}", - "console": "integratedTerminal" - }, { "name": "Tests (Debugger, VS Code, *.test.ts)", "type": "extensionHost", @@ -55,7 +46,6 @@ "--extensionDevelopmentPath=${workspaceFolder}", "--extensionTestsPath=${workspaceFolder}/out/test" ], - "stopOnEntry": false, "sourceMaps": true, "smartStep": true, "outFiles": ["${workspaceFolder}/out/**/*", "!${workspaceFolder}/**/node_modules**/*"], @@ -83,7 +73,6 @@ "VSC_PYTHON_SMOKE_TEST": "1", "TEST_FILES_SUFFIX": "smoke.test" }, - "stopOnEntry": false, "sourceMaps": true, "outFiles": ["${workspaceFolder}/out/**/*.js", "!${workspaceFolder}/**/node_modules**/*"], "preLaunchTask": "Compile", @@ -103,7 +92,6 @@ "env": { "VSC_PYTHON_CI_TEST_GREP": "" // Modify this to run a subset of the single workspace tests }, - "stopOnEntry": false, "sourceMaps": true, "outFiles": ["${workspaceFolder}/out/**/*.js", "!${workspaceFolder}/**/node_modules**/*"], "preLaunchTask": "Compile", @@ -123,7 +111,6 @@ "env": { "VSC_PYTHON_CI_TEST_GREP": "Language Server:" }, - "stopOnEntry": false, "sourceMaps": true, "outFiles": ["${workspaceFolder}/out/**/*.js", "!${workspaceFolder}/**/node_modules**/*"], "preLaunchTask": "preTestJediLSP", @@ -140,7 +127,9 @@ "--extensionDevelopmentPath=${workspaceFolder}", "--extensionTestsPath=${workspaceFolder}/out/test" ], - "stopOnEntry": false, + "env": { + "VSC_PYTHON_CI_TEST_GREP": "" // Modify this to run a subset of the single workspace tests + }, "sourceMaps": true, "smartStep": true, "outFiles": ["${workspaceFolder}/out/**/*", "!${workspaceFolder}/**/node_modules**/*"], @@ -236,11 +225,11 @@ "program": "${file}", "request": "launch", "skipFiles": ["/**"], - "type": "pwa-node" + "type": "node" }, { "name": "Python: Current File", - "type": "python", + "type": "debugpy", "justMyCode": true, "request": "launch", "program": "${file}", @@ -248,8 +237,8 @@ "cwd": "${workspaceFolder}" }, { - "name": "Listen", - "type": "python", + "name": "Python: Attach Listen", + "type": "debugpy", "request": "attach", "listen": { "host": "localhost", "port": 5678 }, "justMyCode": true @@ -257,17 +246,17 @@ { "name": "Debug pytest plugin tests", - "type": "python", + "type": "debugpy", "request": "launch", "module": "pytest", - "args": ["${workspaceFolder}/pythonFiles/tests/pytestadapter"], + "args": ["${workspaceFolder}/python_files/tests/pytestadapter"], "justMyCode": true } ], "compounds": [ { - "name": "Debug Test Discovery", - "configurations": ["Listen", "Extension"] + "name": "Debug Python and Extension", + "configurations": ["Python: Attach Listen", "Extension"] } ] } diff --git a/.vscode/settings.json b/.vscode/settings.json index 174a850c901e..01de0d907706 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -25,8 +25,14 @@ "[python]": { "editor.formatOnSave": true, "editor.codeActionsOnSave": { - "source.organizeImports": true - } + "source.fixAll.eslint": "explicit", + "source.organizeImports.isort": "explicit" + }, + "editor.defaultFormatter": "charliermarsh.ruff", + }, + "[rust]": { + "editor.defaultFormatter": "rust-lang.rust-analyzer", + "editor.formatOnSave": true }, "[typescript]": { "editor.defaultFormatter": "esbenp.prettier-vscode", @@ -45,25 +51,28 @@ "editor.formatOnSave": true }, "typescript.tsdk": "./node_modules/typescript/lib", // we want to use the TS server from our node_modules folder to control its version - "python.linting.enabled": false, - "python.formatting.provider": "black", - "python.sortImports.args": ["--profile", "black"], "typescript.preferences.quoteStyle": "single", "javascript.preferences.quoteStyle": "single", - "typescriptHero.imports.stringQuoteStyle": "'", "prettier.printWidth": 120, "prettier.singleQuote": true, "editor.codeActionsOnSave": { - "source.fixAll.eslint": true + "source.fixAll.eslint": "explicit" }, "python.languageServer": "Default", - "python.linting.pylintEnabled": false, - "python.linting.flake8Enabled": true, - "cucumberautocomplete.skipDocStringsFormat": true, - "python.linting.flake8Args": [ - // Match what black does. - "--max-line-length=88" - ], "typescript.preferences.importModuleSpecifier": "relative", - "debug.javascript.usePreview": false + // Branch name suggestion. + "git.branchProtectionPrompt": "alwaysCommitToNewBranch", + "git.branchRandomName.enable": true, + "git.branchProtection": ["main", "release/*"], + "git.pullBeforeCheckout": true, + // Open merge editor for resolving conflicts. + "git.mergeEditor": true, + "python.testing.pytestArgs": [ + "python_files/tests" + ], + "python.testing.unittestEnabled": false, + "python.testing.pytestEnabled": true, + "rust-analyzer.linkedProjects": [ + ".\\python-env-tools\\Cargo.toml" + ] } diff --git a/.vscode/tasks.json b/.vscode/tasks.json index e1468bdfc2ad..c5a054ed43cf 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -12,9 +12,7 @@ "type": "npm", "script": "compile", "isBackground": true, - "problemMatcher": [ - "$tsc-watch" - ], + "problemMatcher": ["$tsc-watch"], "group": { "kind": "build", "isDefault": true @@ -34,6 +32,31 @@ "script": "preTestJediLSP", "problemMatcher": [], "label": "preTestJediLSP" + }, + { + "type": "npm", + "script": "check-python", + "problemMatcher": ["$python"], + "label": "npm: check-python", + "detail": "npm run check-python:ruff && npm run check-python:pyright" + }, + { + "label": "npm: check-python (venv)", + "type": "shell", + "command": "bash", + "args": ["-lc", "source .venv/bin/activate && npm run check-python"], + "problemMatcher": [], + "detail": "Activates the repo .venv first (avoids pyenv/shim Python) then runs: npm run check-python", + "windows": { + "command": "pwsh", + "args": [ + "-NoProfile", + "-ExecutionPolicy", + "Bypass", + "-Command", + ".\\.venv\\Scripts\\Activate.ps1; npm run check-python" + ] + } } ] } diff --git a/.vscodeignore b/.vscodeignore index 6788f9b6d8e1..d636ab48f361 100644 --- a/.vscodeignore +++ b/.vscodeignore @@ -1,12 +1,15 @@ **/*.map **/*.analyzer.html +**/.env *.vsix .editorconfig .env .eslintrc +.eslintignore .gitattributes .gitignore .gitmodules +.git* .npmrc .nvmrc .nycrc @@ -21,10 +24,13 @@ test.ipynb tsconfig*.json tsfmt.json vscode-python-signing.* +noxfile.py +.config/** .github/** .mocha-reporter/** .nvm/** +.nox/** .nyc_output .prettierrc.js .sonarcloud.properties @@ -51,19 +57,33 @@ obj/** out/**/*.stats.json out/client/**/*.analyzer.html out/coverconfig.json -out/pythonFiles/** +out/python_files/** out/src/** out/test/** out/testMultiRootWkspc/** precommit.hook -pythonFiles/**/*.pyc -pythonFiles/lib/**/*.egg-info/** -pythonFiles/lib/python/bin/** -pythonFiles/jedilsp_requirements/** -pythonFiles/tests/** +python_files/**/*.pyc +python_files/lib/**/*.egg-info/** +python_files/lib/jedilsp/bin/** +python_files/lib/python/bin/** +python_files/tests/** scripts/** src/** test/** tmp/** typings/** types/** +**/__pycache__/** +**/.devcontainer/** + +python-env-tools/.gitignore +python-env-tools/bin/.gitignore +python-env-tools/.github/** +python-env-tools/.vscode/** +python-env-tools/crates/** +python-env-tools/target/** +python-env-tools/Cargo.* +python-env-tools/.cargo/** + +python-env-tools/**/*.md +pythonExtensionApi/** diff --git a/README.md b/README.md index d27ec8e762f5..e9dd52a538cd 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Python extension for Visual Studio Code -A [Visual Studio Code](https://code.visualstudio.com/) [extension](https://marketplace.visualstudio.com/VSCode) with rich support for the [Python language](https://www.python.org/) (for all [actively supported versions](https://devguide.python.org/#status-of-python-branches) of the language: >=3.7), including features such as IntelliSense (Pylance), linting, debugging, code navigation, code formatting, refactoring, variable explorer, test explorer, and more! +A [Visual Studio Code](https://code.visualstudio.com/) [extension](https://marketplace.visualstudio.com/VSCode) with rich support for the [Python language](https://www.python.org/) (for all [actively supported Python versions](https://devguide.python.org/versions/#supported-versions)), providing access points for extensions to seamlessly integrate and offer support for IntelliSense (Pylance), debugging (Python Debugger), formatting, linting, code navigation, refactoring, variable explorer, test explorer, environment management (**NEW** Python Environments Extension). ## Support for [vscode.dev](https://vscode.dev/) @@ -9,13 +9,35 @@ The Python extension does offer [some support](https://github.com/microsoft/vsco ## Installed extensions -The Python extension will automatically install the [Pylance](https://marketplace.visualstudio.com/items?itemName=ms-python.vscode-pylance) and [Jupyter](https://marketplace.visualstudio.com/items?itemName=ms-toolsai.jupyter) extensions to give you the best experience when working with Python files and Jupyter notebooks. However, Pylance is an optional dependency, meaning the Python extension will remain fully functional if it fails to be installed. You can also [uninstall](https://code.visualstudio.com/docs/editor/extension-marketplace#_uninstall-an-extension) it at the expense of some features if you’re using a different language server. +The Python extension will automatically install the following extensions by default to provide the best Python development experience in VS Code: -Extensions installed through the marketplace are subject to the [Marketplace Terms of Use](https://cdn.vsassets.io/v/M146_20190123.39/_content/Microsoft-Visual-Studio-Marketplace-Terms-of-Use.pdf). +- [Pylance](https://marketplace.visualstudio.com/items?itemName=ms-python.vscode-pylance) – performant Python language support +- [Python Debugger](https://marketplace.visualstudio.com/items?itemName=ms-python.debugpy) – seamless debug experience with debugpy +- **(NEW)** [Python Environments](https://marketplace.visualstudio.com/items?itemName=ms-python.vscode-python-envs) – dedicated environment management (see below) + +These extensions are optional dependencies, meaning the Python extension will remain fully functional if they fail to be installed. Any or all of these extensions can be [disabled](https://code.visualstudio.com/docs/editor/extension-marketplace#_disable-an-extension) or [uninstalled](https://code.visualstudio.com/docs/editor/extension-marketplace#_uninstall-an-extension) at the expense of some features. Extensions installed through the marketplace are subject to the [Marketplace Terms of Use](https://cdn.vsassets.io/v/M146_20190123.39/_content/Microsoft-Visual-Studio-Marketplace-Terms-of-Use.pdf). + +### About the Python Environments Extension + +You may now see that the **Python Environments Extension** is installed for you, but it may or may not be "enabled" in your VS Code experience. Enablement is controlled by the setting `"python.useEnvironmentsExtension": true` (or `false`). + +- If you set this setting to `true`, you will manually opt in to using the Python Environments Extension for environment management. +- If you do not have this setting specified, you may be randomly assigned to have it turned on as we roll it out until it becomes the default experience for all users. + +The Python Environments Extension is still under active development and experimentation. Its goal is to provide a dedicated view and improved workflows for creating, deleting, and switching between Python environments, as well as managing packages. If you have feedback, please let us know via [issues](https://github.com/microsoft/vscode-python/issues). + +## Extensibility + +The Python extension provides pluggable access points for extensions that extend various feature areas to further improve your Python development experience. These extensions are all optional and depend on your project configuration and preferences. + +- [Python formatters](https://code.visualstudio.com/docs/python/formatting#_choose-a-formatter) +- [Python linters](https://code.visualstudio.com/docs/python/linting#_choose-a-linter) + +If you encounter issues with any of the listed extensions, please file an issue in its corresponding repo. ## Quick start -- **Step 1.** [Install a supported version of Python on your system](https://code.visualstudio.com/docs/python/python-tutorial#_prerequisites) (note: that the system install of Python on macOS is not supported). +- **Step 1.** [Install a supported version of Python on your system](https://code.visualstudio.com/docs/python/python-tutorial#_prerequisites) (note: the system install of Python on macOS is not supported). - **Step 2.** [Install the Python extension for Visual Studio Code](https://code.visualstudio.com/docs/editor/extension-gallery). - **Step 3.** Open or create a Python file and start coding! @@ -37,7 +59,7 @@ Extensions installed through the marketplace are subject to the [Marketplace Ter ## Jupyter Notebook quick start -The Python extension offers support for Jupyter notebooks via the [Jupyter extension](https://marketplace.visualstudio.com/items?itemName=ms-toolsai.jupyter) to provide you a great Python notebook experience in VS Code. +The Python extension offers support for Jupyter notebooks via the [Jupyter extension](https://marketplace.visualstudio.com/items?itemName=ms-toolsai.jupyter) to provide you a great Python notebook experience in VS Code. - Install the [Jupyter extension](https://marketplace.visualstudio.com/items?itemName=ms-toolsai.jupyter). @@ -58,10 +80,8 @@ Open the Command Palette (Command+Shift+P on macOS and Ctrl+Shift+P on Windows/L | Command | Description | | ------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `Python: Select Interpreter` | Switch between Python interpreters, versions, and environments. | -| `Python: Start REPL` | Start an interactive Python REPL using the selected interpreter in the VS Code terminal. | +| `Python: Start Terminal REPL` | Start an interactive Python REPL using the selected interpreter in the VS Code terminal. | | `Python: Run Python File in Terminal` | Runs the active Python file in the VS Code terminal. You can also run a Python file by right-clicking on the file and selecting `Run Python File in Terminal`. | -| `Python: Select Linter` | Switch from Pylint to Flake8 or other supported linters. | -| `Format Document` | Formats code using the provided [formatter](https://code.visualstudio.com/docs/python/editing#_formatting) in the `settings.json` file. | | `Python: Configure Tests` | Select a test framework and configure it to display the Test Explorer. | To see all available Python commands, open the Command Palette and type `Python`. For Jupyter extension commands, just type `Jupyter`. @@ -70,19 +90,14 @@ To see all available Python commands, open the Command Palette and type `Python` Learn more about the rich features of the Python extension: -- [IntelliSense](https://code.visualstudio.com/docs/python/editing#_autocomplete-and-intellisense): Edit your code with auto-completion, code navigation, syntax checking and more -- [Linting](https://code.visualstudio.com/docs/python/linting): Get additional code analysis with Pylint, Flake8 and more -- [Code formatting](https://code.visualstudio.com/docs/python/editing#_formatting): Format your code with black, autopep or yapf - -- [Debugging](https://code.visualstudio.com/docs/python/debugging): Debug your Python scripts, web apps, remote or multi-threaded processes - +- [IntelliSense](https://code.visualstudio.com/docs/python/editing#_autocomplete-and-intellisense): Edit your code with auto-completion, code navigation, syntax checking and more. +- [Linting](https://code.visualstudio.com/docs/python/linting): Get additional code analysis with Pylint, Flake8 and more. +- [Code formatting](https://code.visualstudio.com/docs/python/formatting): Format your code with black, autopep or yapf. +- [Debugging](https://code.visualstudio.com/docs/python/debugging): Debug your Python scripts, web apps, remote or multi-threaded processes. - [Testing](https://code.visualstudio.com/docs/python/unit-testing): Run and debug tests through the Test Explorer with unittest or pytest. - -- [Jupyter Notebooks](https://code.visualstudio.com/docs/python/jupyter-support): Create and edit Jupyter Notebooks, add and run code cells, render plots, visualize variables through the variable explorer, visualize dataframes with the data viewer, and more - -- [Environments](https://code.visualstudio.com/docs/python/environments): Automatically activate and switch between virtualenv, venv, pipenv, conda and pyenv environments - -- [Refactoring](https://code.visualstudio.com/docs/python/editing#_refactoring): Restructure your Python code with variable extraction and method extraction. Additionally, there is componentized support to enable additional refactoring, such as import sorting, through extensions including [isort](https://marketplace.visualstudio.com/items?itemName=ms-python.isort) and [Ruff](https://marketplace.visualstudio.com/items?itemName=charliermarsh.ruff). +- [Jupyter Notebooks](https://code.visualstudio.com/docs/python/jupyter-support): Create and edit Jupyter Notebooks, add and run code cells, render plots, visualize variables through the variable explorer, visualize dataframes with the data viewer, and more. +- [Environments](https://code.visualstudio.com/docs/python/environments): Automatically activate and switch between virtualenv, venv, pipenv, conda and pyenv environments. +- [Refactoring](https://code.visualstudio.com/docs/python/editing#_refactoring): Restructure your Python code with variable extraction and method extraction. Additionally, there is componentized support to enable additional refactoring, such as import sorting, through extensions including [isort](https://marketplace.visualstudio.com/items?itemName=ms-python.isort) and [Ruff](https://marketplace.visualstudio.com/items?itemName=charliermarsh.ruff). @@ -92,13 +107,13 @@ The extension is available in multiple languages: `de`, `en`, `es`, `fa`, `fr`, ## Questions, issues, feature requests, and contributions -- If you have a question about how to accomplish something with the extension, please [ask on Stack Overflow](https://stackoverflow.com/questions/tagged/visual-studio-code+python) -- If you come across a problem with the extension, please [file an issue](https://github.com/microsoft/vscode-python) -- Contributions are always welcome! Please see our [contributing guide](https://github.com/Microsoft/vscode-python/blob/main/CONTRIBUTING.md) for more details +- If you have a question about how to accomplish something with the extension, please [ask on our Discussions page](https://github.com/microsoft/vscode-python/discussions/categories/q-a). +- If you come across a problem with the extension, please [file an issue](https://github.com/microsoft/vscode-python). +- Contributions are always welcome! Please see our [contributing guide](https://github.com/Microsoft/vscode-python/blob/main/CONTRIBUTING.md) for more details. - Any and all feedback is appreciated and welcome! - - If someone has already [filed an issue](https://github.com/Microsoft/vscode-python) that encompasses your feedback, please leave a πŸ‘/πŸ‘Ž reaction on the issue - - Otherwise please start a [new discussion](https://github.com/microsoft/vscode-python/discussions/categories/ideas) -- If you're interested in the development of the extension, you can read about our [development process](https://github.com/Microsoft/vscode-python/blob/main/CONTRIBUTING.md#development-process) + - If someone has already [filed an issue](https://github.com/Microsoft/vscode-python) that encompasses your feedback, please leave a πŸ‘/πŸ‘Ž reaction on the issue. + - Otherwise please start a [new discussion](https://github.com/microsoft/vscode-python/discussions/categories/ideas). +- If you're interested in the development of the extension, you can read about our [development process](https://github.com/Microsoft/vscode-python/blob/main/CONTRIBUTING.md#development-process). ## Data and telemetry @@ -106,6 +121,6 @@ The Microsoft Python Extension for Visual Studio Code collects usage data and sends it to Microsoft to help improve our products and services. Read our [privacy statement](https://privacy.microsoft.com/privacystatement) to -learn more. This extension respects the `telemetry.enableTelemetry` +learn more. This extension respects the `telemetry.telemetryLevel` setting which you can learn more about at https://code.visualstudio.com/docs/supporting/faq#_how-to-disable-telemetry-reporting. diff --git a/ThirdPartyNotices-Repository.txt b/ThirdPartyNotices-Repository.txt index c8854a208e5a..9e7e822af1bb 100644 --- a/ThirdPartyNotices-Repository.txt +++ b/ThirdPartyNotices-Repository.txt @@ -6,18 +6,17 @@ Microsoft Python extension for Visual Studio Code incorporates third party mater 1. Go for Visual Studio Code (https://github.com/Microsoft/vscode-go) 2. Files from the Python Project (https://www.python.org/) -3. Google Diff Match and Patch (https://github.com/GerHobbelt/google-diff-match-patch) -4. omnisharp-vscode (https://github.com/OmniSharp/omnisharp-vscode) -5. PTVS (https://github.com/Microsoft/PTVS) -6. Python documentation (https://docs.python.org/) -7. python-functools32 (https://github.com/MiCHiLU/python-functools32/blob/master/functools32/functools32.py) -8. pythonVSCode (https://github.com/DonJayamanne/pythonVSCode) -9. Sphinx (http://sphinx-doc.org/) -10. nteract (https://github.com/nteract/nteract) -11. less-plugin-inline-urls (https://github.com/less/less-plugin-inline-urls/) -12. vscode-cpptools (https://github.com/microsoft/vscode-cpptools) -13. mocha (https://github.com/mochajs/mocha) -14. get-pip (https://github.com/pypa/get-pip) +3. omnisharp-vscode (https://github.com/OmniSharp/omnisharp-vscode) +4. PTVS (https://github.com/Microsoft/PTVS) +5. Python documentation (https://docs.python.org/) +6. python-functools32 (https://github.com/MiCHiLU/python-functools32/blob/master/functools32/functools32.py) +7. pythonVSCode (https://github.com/DonJayamanne/pythonVSCode) +8. Sphinx (http://sphinx-doc.org/) +9. nteract (https://github.com/nteract/nteract) +10. less-plugin-inline-urls (https://github.com/less/less-plugin-inline-urls/) +11. vscode-cpptools (https://github.com/microsoft/vscode-cpptools) +12. mocha (https://github.com/mochajs/mocha) +13. get-pip (https://github.com/pypa/get-pip) %% Go for Visual Studio Code NOTICES, INFORMATION, AND LICENSE BEGIN HERE @@ -244,25 +243,6 @@ OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. ========================================= END OF Files from the Python Project NOTICES, INFORMATION, AND LICENSE -%% Google Diff Match and Patch NOTICES, INFORMATION, AND LICENSE BEGIN HERE -========================================= - * Copyright 2006 Google Inc. - * http://code.google.com/p/google-diff-match-patch/ - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. -========================================= -END OF Google Diff Match and Patch NOTICES, INFORMATION, AND LICENSE - %% omnisharp-vscode NOTICES, INFORMATION, AND LICENSE BEGIN HERE ========================================= Copyright (c) Microsoft Corporation diff --git a/build/azure-pipeline.pre-release.yml b/build/azure-pipeline.pre-release.yml index 5bc61b2fd559..c300039f4ef4 100644 --- a/build/azure-pipeline.pre-release.yml +++ b/build/azure-pipeline.pre-release.yml @@ -18,51 +18,141 @@ resources: ref: main endpoint: Monaco +parameters: + - name: publishExtension + displayName: πŸš€ Publish Extension + type: boolean + default: false + extends: template: azure-pipelines/extension/pre-release.yml@templates parameters: + publishExtension: ${{ parameters.publishExtension }} + ghCreateTag: false + standardizedVersioning: true l10nSourcePaths: ./src/client + + buildPlatforms: + - name: Linux + vsceTarget: 'web' + - name: Linux + packageArch: arm64 + vsceTarget: linux-arm64 + - name: Linux + packageArch: arm + vsceTarget: linux-armhf + - name: Linux + packageArch: x64 + vsceTarget: linux-x64 + - name: Linux + packageArch: arm64 + vsceTarget: alpine-arm64 + - name: Linux + packageArch: x64 + vsceTarget: alpine-x64 + - name: MacOS + packageArch: arm64 + vsceTarget: darwin-arm64 + - name: MacOS + packageArch: x64 + vsceTarget: darwin-x64 + - name: Windows + packageArch: arm + vsceTarget: win32-arm64 + - name: Windows + packageArch: x64 + vsceTarget: win32-x64 + buildSteps: - task: NodeTool@0 inputs: - versionSpec: '16.17.1' + versionSpec: '22.17.0' displayName: Select Node version - task: UsePythonVersion@0 inputs: - versionSpec: '3.7' + versionSpec: '3.10' addToPath: true architecture: 'x64' displayName: Select Python version - - script: npm ci - displayName: Install NPM dependencies - - script: python -m pip install -U pip displayName: Upgrade pip - - script: python -m pip install wheel - displayName: Install wheel + - script: python -m pip install wheel nox + displayName: Install wheel and nox - - script: | - python -m pip --disable-pip-version-check install -r build/debugger-install-requirements.txt - python ./pythonFiles/install_debugpy.py - displayName: Install debugpy - - - script: | - python -m pip install --no-deps --require-hashes --only-binary :all: -t ./pythonFiles/lib/python --implementation py -r ./requirements.txt - displayName: Install Python dependencies + - script: npm ci + displayName: Install NPM dependencies - - script: | - python -m pip install --no-deps --require-hashes --only-binary :all: -t ./pythonFiles/lib/jedilsp --implementation py --platform any --abi none -r ./pythonFiles/jedilsp_requirements/requirements.txt - displayName: Install Jedi Language Server + - script: nox --session install_python_libs + displayName: Install Jedi, get-pip, etc - - script: | - python ./build/update_ext_version.py --for-publishing - displayName: Update build number + - script: python ./build/update_package_file.py + displayName: Update telemetry in package.json - script: npm run addExtensionPackDependencies displayName: Update optional extension dependencies - - script: gulp prePublishBundle + - script: npx gulp prePublishBundle displayName: Build + + - bash: | + mkdir -p $(Build.SourcesDirectory)/python-env-tools/bin + chmod +x $(Build.SourcesDirectory)/python-env-tools/bin + displayName: Make Directory for python-env-tool binary + + - bash: | + if [ "$(vsceTarget)" == "win32-x64" ]; then + echo "##vso[task.setvariable variable=buildTarget]x86_64-pc-windows-msvc" + elif [ "$(vsceTarget)" == "win32-arm64" ]; then + echo "##vso[task.setvariable variable=buildTarget]aarch64-pc-windows-msvc" + elif [ "$(vsceTarget)" == "linux-x64" ]; then + echo "##vso[task.setvariable variable=buildTarget]x86_64-unknown-linux-musl" + elif [ "$(vsceTarget)" == "linux-arm64" ]; then + echo "##vso[task.setvariable variable=buildTarget]aarch64-unknown-linux-gnu" + elif [ "$(vsceTarget)" == "linux-armhf" ]; then + echo "##vso[task.setvariable variable=buildTarget]armv7-unknown-linux-gnueabihf" + elif [ "$(vsceTarget)" == "darwin-x64" ]; then + echo "##vso[task.setvariable variable=buildTarget]x86_64-apple-darwin" + elif [ "$(vsceTarget)" == "darwin-arm64" ]; then + echo "##vso[task.setvariable variable=buildTarget]aarch64-apple-darwin" + elif [ "$(vsceTarget)" == "alpine-x64" ]; then + echo "##vso[task.setvariable variable=buildTarget]x86_64-unknown-linux-musl" + elif [ "$(vsceTarget)" == "alpine-arm64" ]; then + echo "##vso[task.setvariable variable=buildTarget]aarch64-unknown-linux-gnu" + elif [ "$(vsceTarget)" == "web" ]; then + echo "##vso[task.setvariable variable=buildTarget]x86_64-unknown-linux-musl" + else + echo "##vso[task.setvariable variable=buildTarget]x86_64-unknown-linux-musl" + fi + displayName: Set buildTarget variable + + - task: DownloadPipelineArtifact@2 + inputs: + buildType: 'specific' + project: 'Monaco' + definition: 591 + buildVersionToDownload: 'latest' + branchName: 'refs/heads/main' + targetPath: '$(Build.SourcesDirectory)/python-env-tools/bin' + artifactName: 'bin-$(buildTarget)' + itemPattern: | + pet.exe + pet + ThirdPartyNotices.txt + + - bash: | + ls -lf ./python-env-tools/bin + chmod +x ./python-env-tools/bin/pet* + ls -lf ./python-env-tools/bin + displayName: Set chmod for pet binary + + - script: python -c "import shutil; shutil.rmtree('.nox', ignore_errors=True)" + displayName: Clean up Nox + + tsa: + config: + areaPath: 'Visual Studio Code Python Extensions' + serviceTreeID: '6e6194bc-7baa-4486-86d0-9f5419626d46' + enabled: true diff --git a/build/azure-pipeline.stable.yml b/build/azure-pipeline.stable.yml index 159d856b6c3e..024417da0e00 100644 --- a/build/azure-pipeline.stable.yml +++ b/build/azure-pipeline.stable.yml @@ -25,47 +25,129 @@ extends: parameters: publishExtension: ${{ parameters.publishExtension }} l10nSourcePaths: ./src/client + + buildPlatforms: + - name: Linux + vsceTarget: 'web' + - name: Linux + packageArch: arm64 + vsceTarget: linux-arm64 + - name: Linux + packageArch: arm + vsceTarget: linux-armhf + - name: Linux + packageArch: x64 + vsceTarget: linux-x64 + - name: Linux + packageArch: arm64 + vsceTarget: alpine-arm64 + - name: Linux + packageArch: x64 + vsceTarget: alpine-x64 + - name: MacOS + packageArch: arm64 + vsceTarget: darwin-arm64 + - name: MacOS + packageArch: x64 + vsceTarget: darwin-x64 + - name: Windows + packageArch: arm + vsceTarget: win32-arm64 + - name: Windows + packageArch: x64 + vsceTarget: win32-x64 + buildSteps: - task: NodeTool@0 inputs: - versionSpec: '16.17.1' + versionSpec: '22.17.0' displayName: Select Node version - task: UsePythonVersion@0 inputs: - versionSpec: '3.7' + versionSpec: '3.10' addToPath: true architecture: 'x64' displayName: Select Python version - - script: npm ci - displayName: Install NPM dependencies - - script: python -m pip install -U pip displayName: Upgrade pip - - script: python -m pip install wheel - displayName: Install wheel - - - script: | - python -m pip --disable-pip-version-check install -r build/debugger-install-requirements.txt - python ./pythonFiles/install_debugpy.py - displayName: Install debugpy + - script: python -m pip install wheel nox + displayName: Install wheel and nox - - script: | - python -m pip install --no-deps --require-hashes --only-binary :all: -t ./pythonFiles/lib/python --implementation py -r ./requirements.txt - displayName: Install Python dependencies + - script: npm ci + displayName: Install NPM dependencies - - script: | - python -m pip install --no-deps --require-hashes --only-binary :all: -t ./pythonFiles/lib/jedilsp --implementation py --platform any --abi none -r ./pythonFiles/jedilsp_requirements/requirements.txt - displayName: Install Jedi Language Server + - script: nox --session install_python_libs + displayName: Install Jedi, get-pip, etc - - script: | - python ./build/update_ext_version.py --release --for-publishing - displayName: Update build number + - script: python ./build/update_package_file.py + displayName: Update telemetry in package.json - script: npm run addExtensionPackDependencies displayName: Update optional extension dependencies - - script: gulp prePublishBundle + - script: npx gulp prePublishBundle displayName: Build + + - bash: | + mkdir -p $(Build.SourcesDirectory)/python-env-tools/bin + chmod +x $(Build.SourcesDirectory)/python-env-tools/bin + displayName: Make Directory for python-env-tool binary + + - bash: | + if [ "$(vsceTarget)" == "win32-x64" ]; then + echo "##vso[task.setvariable variable=buildTarget]x86_64-pc-windows-msvc" + elif [ "$(vsceTarget)" == "win32-arm64" ]; then + echo "##vso[task.setvariable variable=buildTarget]aarch64-pc-windows-msvc" + elif [ "$(vsceTarget)" == "linux-x64" ]; then + echo "##vso[task.setvariable variable=buildTarget]x86_64-unknown-linux-musl" + elif [ "$(vsceTarget)" == "linux-arm64" ]; then + echo "##vso[task.setvariable variable=buildTarget]aarch64-unknown-linux-gnu" + elif [ "$(vsceTarget)" == "linux-armhf" ]; then + echo "##vso[task.setvariable variable=buildTarget]armv7-unknown-linux-gnueabihf" + elif [ "$(vsceTarget)" == "darwin-x64" ]; then + echo "##vso[task.setvariable variable=buildTarget]x86_64-apple-darwin" + elif [ "$(vsceTarget)" == "darwin-arm64" ]; then + echo "##vso[task.setvariable variable=buildTarget]aarch64-apple-darwin" + elif [ "$(vsceTarget)" == "alpine-x64" ]; then + echo "##vso[task.setvariable variable=buildTarget]x86_64-unknown-linux-musl" + elif [ "$(vsceTarget)" == "alpine-arm64" ]; then + echo "##vso[task.setvariable variable=buildTarget]aarch64-unknown-linux-gnu" + elif [ "$(vsceTarget)" == "web" ]; then + echo "##vso[task.setvariable variable=buildTarget]x86_64-unknown-linux-musl" + else + echo "##vso[task.setvariable variable=buildTarget]x86_64-unknown-linux-musl" + fi + displayName: Set buildTarget variable + + - task: DownloadPipelineArtifact@2 + inputs: + buildType: 'specific' + project: 'Monaco' + definition: 593 + buildVersionToDownload: 'latestFromBranch' + branchName: 'refs/heads/release/2026.4' + targetPath: '$(Build.SourcesDirectory)/python-env-tools/bin' + artifactName: 'bin-$(buildTarget)' + itemPattern: | + pet.exe + pet + ThirdPartyNotices.txt + + - bash: | + ls -lf ./python-env-tools/bin + chmod +x ./python-env-tools/bin/pet* + ls -lf ./python-env-tools/bin + displayName: Set chmod for pet binary + + - script: python -c "import shutil; shutil.rmtree('.nox', ignore_errors=True)" + displayName: Clean up Nox + tsa: + config: + areaPath: 'Visual Studio Code Python Extensions' + serviceTreeID: '6e6194bc-7baa-4486-86d0-9f5419626d46' + enabled: true + apiScanDependentPipelineId: '593' # python-environment-tools + apiScanSoftwareVersion: '2024' diff --git a/build/azure-pipelines/pipeline.yml b/build/azure-pipelines/pipeline.yml new file mode 100644 index 000000000000..0796e38ca598 --- /dev/null +++ b/build/azure-pipelines/pipeline.yml @@ -0,0 +1,58 @@ +############################################################################################### +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for license information. +############################################################################################### +name: $(Date:yyyyMMdd)$(Rev:.r) + +trigger: none + +pr: none + +resources: + repositories: + - repository: templates + type: github + name: microsoft/vscode-engineering + ref: main + endpoint: Monaco + +parameters: + - name: quality + displayName: Quality + type: string + default: latest + values: + - latest + - next + - name: publishPythonApi + displayName: πŸš€ Publish pythonExtensionApi + type: boolean + default: false + +extends: + template: azure-pipelines/npm-package/pipeline.yml@templates + parameters: + npmPackages: + - name: pythonExtensionApi + testPlatforms: + - name: Linux + nodeVersions: + - 22.21.1 + - name: MacOS + nodeVersions: + - 22.21.1 + - name: Windows + nodeVersions: + - 22.21.1 + testSteps: + - template: /build/azure-pipelines/templates/test-steps.yml@self + parameters: + package: pythonExtensionApi + buildSteps: + - template: /build/azure-pipelines/templates/pack-steps.yml@self + parameters: + package: pythonExtensionApi + ghTagPrefix: release/pythonExtensionApi/ + tag: ${{ parameters.quality }} + publishPackage: ${{ parameters.publishPythonApi }} + workingDirectory: $(Build.SourcesDirectory)/pythonExtensionApi diff --git a/build/azure-pipelines/templates/pack-steps.yml b/build/azure-pipelines/templates/pack-steps.yml new file mode 100644 index 000000000000..97037efb59ba --- /dev/null +++ b/build/azure-pipelines/templates/pack-steps.yml @@ -0,0 +1,14 @@ +############################################################################################### +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for license information. +############################################################################################### +parameters: +- name: package + +steps: + - script: npm install --root-only + workingDirectory: $(Build.SourcesDirectory) + displayName: Install root dependencies + - script: npm install + workingDirectory: $(Build.SourcesDirectory)/${{ parameters.package }} + displayName: Install package dependencies diff --git a/build/azure-pipelines/templates/test-steps.yml b/build/azure-pipelines/templates/test-steps.yml new file mode 100644 index 000000000000..15eb3db6384d --- /dev/null +++ b/build/azure-pipelines/templates/test-steps.yml @@ -0,0 +1,23 @@ +############################################################################################### +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for license information. +############################################################################################### +parameters: +- name: package + type: string +- name: script + type: string + default: 'all:publish' + +steps: + - script: npm install --root-only + workingDirectory: $(Build.SourcesDirectory) + displayName: Install root dependencies + - bash: | + /usr/bin/Xvfb :99 -screen 0 1024x768x24 > /dev/null 2>&1 & + echo ">>> Started xvfb" + displayName: Start xvfb + condition: eq(variables['Agent.OS'], 'Linux') + - script: npm run ${{ parameters.script }} + workingDirectory: $(Build.SourcesDirectory)/${{ parameters.package }} + displayName: Verify package diff --git a/build/build-install-requirements.txt b/build/build-install-requirements.txt new file mode 100644 index 000000000000..8baaa59ded67 --- /dev/null +++ b/build/build-install-requirements.txt @@ -0,0 +1,2 @@ +# Requirements needed to run install_debugpy.py and download_get_pip.py +packaging diff --git a/build/ci/addEnvPath.py b/build/ci/addEnvPath.py index abad9ec3b5c9..66eff2a7b25d 100644 --- a/build/ci/addEnvPath.py +++ b/build/ci/addEnvPath.py @@ -3,7 +3,8 @@ #Adds the virtual environment's executable path to json file -import json,sys +import json +import sys import os.path jsonPath = sys.argv[1] key = sys.argv[2] diff --git a/build/ci/conda_env_1.yml b/build/ci/conda_env_1.yml index df5c917dcf4f..e3230ad0096e 100644 --- a/build/ci/conda_env_1.yml +++ b/build/ci/conda_env_1.yml @@ -1,4 +1,4 @@ name: conda_env_1 dependencies: - - python=3.7 + - python=3.10 - pip diff --git a/build/ci/conda_env_2.yml b/build/ci/conda_env_2.yml index 80b946c3cc14..38f551da2580 100644 --- a/build/ci/conda_env_2.yml +++ b/build/ci/conda_env_2.yml @@ -1,4 +1,4 @@ name: conda_env_2 dependencies: - - python=3.8 + - python=3.10 - pip diff --git a/build/ci/scripts/spec_with_pid.js b/build/ci/scripts/spec_with_pid.js index 9815feaac76a..a8453353aa79 100644 --- a/build/ci/scripts/spec_with_pid.js +++ b/build/ci/scripts/spec_with_pid.js @@ -98,5 +98,6 @@ Spec.description = 'hierarchical & verbose [default]'; * Expose `Spec`. */ +// eslint-disable-next-line no-global-assign exports = Spec; module.exports = exports; diff --git a/build/debugger-install-requirements.txt b/build/debugger-install-requirements.txt deleted file mode 100644 index 6ee0765db4b3..000000000000 --- a/build/debugger-install-requirements.txt +++ /dev/null @@ -1,2 +0,0 @@ -# Requirements needed to run install_debugpy.py -packaging diff --git a/build/existingFiles.json b/build/existingFiles.json index 1f5acc727d8e..48ab84ff565d 100644 --- a/build/existingFiles.json +++ b/build/existingFiles.json @@ -500,7 +500,7 @@ "src/test/providers/shebangCodeLenseProvider.test.ts", "src/test/providers/symbolProvider.unit.test.ts", "src/test/providers/terminal.unit.test.ts", - "src/test/pythonFiles/formatting/dummy.ts", + "src/test/python_files/formatting/dummy.ts", "src/test/refactor/extension.refactor.extract.method.test.ts", "src/test/refactor/extension.refactor.extract.var.test.ts", "src/test/refactor/rename.test.ts", diff --git a/types/vscode.proposed.envShellEvent.d.ts b/build/fail.js similarity index 59% rename from types/vscode.proposed.envShellEvent.d.ts rename to build/fail.js index 8fed971ef711..2adc808d8da9 100644 --- a/types/vscode.proposed.envShellEvent.d.ts +++ b/build/fail.js @@ -3,14 +3,4 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -declare module 'vscode' { - - // See https://github.com/microsoft/vscode/issues/160694 - export namespace env { - - /** - * An {@link Event} which fires when the default shell changes. - */ - export const onDidChangeShell: Event; - } -} +process.exitCode = 1; diff --git a/build/functional-test-requirements.txt b/build/functional-test-requirements.txt index d45208f671f4..5c3a9e3116ed 100644 --- a/build/functional-test-requirements.txt +++ b/build/functional-test-requirements.txt @@ -1,3 +1,5 @@ # List of requirements for functional tests versioneer numpy +pytest +pytest-cov diff --git a/build/license-header.txt b/build/license-header.txt index 2a8122642cb2..2970b03d7a1c 100644 --- a/build/license-header.txt +++ b/build/license-header.txt @@ -1,7 +1,7 @@ PLEASE NOTE: This is the license for the Python extension for Visual Studio Code. The Python extension automatically installs other extensions as optional dependencies, which can be uninstalled at any time. These extensions have separate licenses: - - The Jupyter extension is released under an MIT License: - https://marketplace.visualstudio.com/items/ms-toolsai.jupyter/license + - The Python Debugger extension is released under an MIT License: + https://marketplace.visualstudio.com/items/ms-python.debugpy/license - The Pylance extension is only available in binary form and is released under a Microsoft proprietary license, the terms of which are available here: https://marketplace.visualstudio.com/items/ms-python.vscode-pylance/license diff --git a/build/smoke-test-requirements.txt b/build/smoke-test-requirements.txt deleted file mode 100644 index 7d5ac3da00d9..000000000000 --- a/build/smoke-test-requirements.txt +++ /dev/null @@ -1,6 +0,0 @@ -# List of requirements for smoke tests (they will attempt to run a kernel) -jupyter -numpy -matplotlib -pandas -livelossplot \ No newline at end of file diff --git a/build/test-requirements.txt b/build/test-requirements.txt index c732b3bcb228..ff9afdfc8a2e 100644 --- a/build/test-requirements.txt +++ b/build/test-requirements.txt @@ -1,13 +1,8 @@ # pin setoptconf to prevent issue with 'use_2to3' setoptconf==0.3.0 -# Install flake8 first, as both flake8 and autopep8 require pycodestyle, -# but flake8 has a tighter pinning. flake8 -autopep8 bandit -black -yapf pylint pycodestyle pydocstyle @@ -17,7 +12,8 @@ flask fastapi uvicorn django -isort +testscenarios +testtools # Integrated TensorBoard tests tensorboard @@ -25,3 +21,22 @@ torch-tb-profiler # extension build tests freezegun + +# testing custom pytest plugin require the use of named pipes +namedpipe; platform_system == "Windows" + +# typing for Django files +django-stubs + +coverage +pytest-cov +pytest-json +pytest-timeout + + +# for pytest-describe related tests +pytest-describe + +# for pytest-ruff related tests +pytest-ruff +pytest-black diff --git a/build/update_ext_version.py b/build/update_ext_version.py index bfd7ac1e9996..6d709ae05f7f 100644 --- a/build/update_ext_version.py +++ b/build/update_ext_version.py @@ -70,10 +70,23 @@ def main(package_json: pathlib.Path, argv: Sequence[str]) -> None: major, minor, micro, suffix = parse_version(package["version"]) current_year = datetime.datetime.now().year - if int(major) != current_year: + current_month = datetime.datetime.now().month + int_major = int(major) + valid_major = ( + int_major + == current_year # Between JAN-DEC major version should be current year + or ( + int_major == current_year - 1 and current_month == 1 + ) # After new years the check is relaxed for JAN to allow releases of previous year DEC + or ( + int_major == current_year + 1 and current_month == 12 + ) # Before new years the check is relaxed for DEC to allow pre-releases of next year JAN + ) + if not valid_major: raise ValueError( f"Major version [{major}] must be the current year [{current_year}].", f"If changing major version after new year's, change to {current_year}.1.0", + "Minor version must be updated based on release or pre-release channel.", ) if args.release and not is_even(minor): @@ -89,9 +102,7 @@ def main(package_json: pathlib.Path, argv: Sequence[str]) -> None: if args.build_id: # If build id is provided it should fall within the 0-INT32 max range # that the max allowed value for publishing to the Marketplace. - if args.build_id < 0 or ( - args.for_publishing and args.build_id > ((2**32) - 1) - ): + if args.build_id < 0 or (args.for_publishing and args.build_id > ((2**32) - 1)): raise ValueError(f"Build ID must be within [0, {(2**32) - 1}]") package["version"] = ".".join((major, minor, str(args.build_id))) diff --git a/build/update_package_file.py b/build/update_package_file.py new file mode 100644 index 000000000000..f82587ced846 --- /dev/null +++ b/build/update_package_file.py @@ -0,0 +1,22 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +import json +import pathlib + +EXT_ROOT = pathlib.Path(__file__).parent.parent +PACKAGE_JSON_PATH = EXT_ROOT / "package.json" + + +def main(package_json: pathlib.Path) -> None: + package = json.loads(package_json.read_text(encoding="utf-8")) + package["enableTelemetry"] = True + + # Overwrite package.json with new data add a new-line at the end of the file. + package_json.write_text( + json.dumps(package, indent=4, ensure_ascii=False) + "\n", encoding="utf-8" + ) + + +if __name__ == "__main__": + main(PACKAGE_JSON_PATH) diff --git a/build/webpack/common.js b/build/webpack/common.js index d5235db54967..c7f7460adf86 100644 --- a/build/webpack/common.js +++ b/build/webpack/common.js @@ -21,7 +21,6 @@ exports.nodeModulesToExternalize = [ 'unicode/category/Nd', 'unicode/category/Pc', 'source-map-support', - 'diff-match-patch', 'sudo-prompt', 'node-stream-zip', 'xml2js', diff --git a/build/webpack/webpack.extension.config.js b/build/webpack/webpack.extension.config.js index f496aa32ee26..082ce52a4d32 100644 --- a/build/webpack/webpack.extension.config.js +++ b/build/webpack/webpack.extension.config.js @@ -19,6 +19,10 @@ const config = { target: 'node', entry: { extension: './src/client/extension.ts', + 'shellExec.worker': './src/client/common/process/worker/shellExec.worker.ts', + 'plainExec.worker': './src/client/common/process/worker/plainExec.worker.ts', + 'registryKeys.worker': 'src/client/pythonEnvironments/common/registryKeys.worker.ts', + 'registryValues.worker': 'src/client/pythonEnvironments/common/registryValues.worker.ts', }, devtool: 'source-map', node: { @@ -51,6 +55,10 @@ const config = { }, ], }, + { + test: /\.worker\.js$/, + use: { loader: 'worker-loader' }, + }, ], }, externals: [ @@ -69,6 +77,7 @@ const config = { resolve: { extensions: ['.ts', '.js'], plugins: [new tsconfig_paths_webpack_plugin.TsconfigPathsPlugin({ configFile: configFileName })], + conditionNames: ['import', 'require', 'node'], }, output: { filename: '[name].js', diff --git a/data/.vscode/settings.json b/data/.vscode/settings.json deleted file mode 100644 index 6f329d0777a4..000000000000 --- a/data/.vscode/settings.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "python.defaultInterpreterPath": "/usr/bin/python3" -} diff --git a/data/test.py b/data/test.py deleted file mode 100644 index 3b316dc1e8d1..000000000000 --- a/data/test.py +++ /dev/null @@ -1,2 +0,0 @@ -#%% -print('hello') diff --git a/eslint.config.mjs b/eslint.config.mjs new file mode 100644 index 000000000000..8e1aa990a2c2 --- /dev/null +++ b/eslint.config.mjs @@ -0,0 +1,393 @@ +/** + * ESLint Configuration for VS Code Python Extension + * This file configures linting rules for the TypeScript/JavaScript codebase. + * It uses the new flat config format introduced in ESLint 8.21.0 + */ + +// Import essential ESLint plugins and configurations +import tseslint from '@typescript-eslint/eslint-plugin'; +import tsParser from '@typescript-eslint/parser'; +import noOnlyTests from 'eslint-plugin-no-only-tests'; +import prettier from 'eslint-config-prettier'; +import importPlugin from 'eslint-plugin-import'; +import js from '@eslint/js'; +import noBadGdprCommentPlugin from './.eslintplugin/no-bad-gdpr-comment.js'; // Ensure the path is correct + +export default [ + { + ignores: ['**/node_modules/**', '**/out/**'], + }, + // Base configuration for all files + { + ignores: [ + '**/node_modules/**', + '**/out/**', + 'src/test/analysisEngineTest.ts', + 'src/test/ciConstants.ts', + 'src/test/common.ts', + 'src/test/constants.ts', + 'src/test/core.ts', + 'src/test/extension-version.functional.test.ts', + 'src/test/fixtures.ts', + 'src/test/index.ts', + 'src/test/initialize.ts', + 'src/test/mockClasses.ts', + 'src/test/performanceTest.ts', + 'src/test/proc.ts', + 'src/test/smokeTest.ts', + 'src/test/standardTest.ts', + 'src/test/startupTelemetry.unit.test.ts', + 'src/test/testBootstrap.ts', + 'src/test/testLogger.ts', + 'src/test/testRunner.ts', + 'src/test/textUtils.ts', + 'src/test/unittests.ts', + 'src/test/vscode-mock.ts', + 'src/test/interpreters/mocks.ts', + 'src/test/interpreters/virtualEnvs/condaInheritEnvPrompt.unit.test.ts', + 'src/test/interpreters/pythonPathUpdaterFactory.unit.test.ts', + 'src/test/interpreters/activation/service.unit.test.ts', + 'src/test/interpreters/helpers.unit.test.ts', + 'src/test/interpreters/display.unit.test.ts', + 'src/test/terminals/codeExecution/terminalCodeExec.unit.test.ts', + 'src/test/terminals/codeExecution/codeExecutionManager.unit.test.ts', + 'src/test/terminals/codeExecution/djangoShellCodeExect.unit.test.ts', + 'src/test/activation/activeResource.unit.test.ts', + 'src/test/activation/extensionSurvey.unit.test.ts', + 'src/test/utils/fs.ts', + 'src/test/api.functional.test.ts', + 'src/test/testing/common/debugLauncher.unit.test.ts', + 'src/test/testing/common/services/configSettingService.unit.test.ts', + 'src/test/common/exitCIAfterTestReporter.ts', + 'src/test/common/terminals/activator/index.unit.test.ts', + 'src/test/common/terminals/activator/base.unit.test.ts', + 'src/test/common/terminals/shellDetector.unit.test.ts', + 'src/test/common/terminals/service.unit.test.ts', + 'src/test/common/terminals/helper.unit.test.ts', + 'src/test/common/terminals/activation.unit.test.ts', + 'src/test/common/terminals/shellDetectors/shellDetectors.unit.test.ts', + 'src/test/common/terminals/environmentActivationProviders/terminalActivation.testvirtualenvs.ts', + 'src/test/common/socketStream.test.ts', + 'src/test/common/configSettings.test.ts', + 'src/test/common/experiments/telemetry.unit.test.ts', + 'src/test/common/platform/filesystem.unit.test.ts', + 'src/test/common/platform/errors.unit.test.ts', + 'src/test/common/platform/utils.ts', + 'src/test/common/platform/fs-temp.unit.test.ts', + 'src/test/common/platform/fs-temp.functional.test.ts', + 'src/test/common/platform/filesystem.functional.test.ts', + 'src/test/common/platform/filesystem.test.ts', + 'src/test/common/utils/cacheUtils.unit.test.ts', + 'src/test/common/utils/decorators.unit.test.ts', + 'src/test/common/utils/version.unit.test.ts', + 'src/test/common/configSettings/configSettings.unit.test.ts', + 'src/test/common/serviceRegistry.unit.test.ts', + 'src/test/common/extensions.unit.test.ts', + 'src/test/common/variables/envVarsService.unit.test.ts', + 'src/test/common/helpers.test.ts', + 'src/test/common/application/commands/reloadCommand.unit.test.ts', + 'src/test/common/installer/channelManager.unit.test.ts', + 'src/test/common/installer/pipInstaller.unit.test.ts', + 'src/test/common/installer/pipEnvInstaller.unit.test.ts', + 'src/test/common/socketCallbackHandler.test.ts', + 'src/test/common/process/decoder.test.ts', + 'src/test/common/process/processFactory.unit.test.ts', + 'src/test/common/process/pythonToolService.unit.test.ts', + 'src/test/common/process/proc.observable.test.ts', + 'src/test/common/process/logger.unit.test.ts', + 'src/test/common/process/proc.exec.test.ts', + 'src/test/common/process/pythonProcess.unit.test.ts', + 'src/test/common/process/proc.unit.test.ts', + 'src/test/common/interpreterPathService.unit.test.ts', + 'src/test/debugger/extension/adapter/adapter.test.ts', + 'src/test/debugger/extension/adapter/outdatedDebuggerPrompt.unit.test.ts', + 'src/test/debugger/extension/adapter/factory.unit.test.ts', + 'src/test/debugger/extension/adapter/logging.unit.test.ts', + 'src/test/debugger/extension/hooks/childProcessAttachHandler.unit.test.ts', + 'src/test/debugger/extension/hooks/childProcessAttachService.unit.test.ts', + 'src/test/debugger/utils.ts', + 'src/test/debugger/envVars.test.ts', + 'src/test/telemetry/index.unit.test.ts', + 'src/test/telemetry/envFileTelemetry.unit.test.ts', + 'src/test/application/diagnostics/checks/macPythonInterpreter.unit.test.ts', + 'src/test/application/diagnostics/checks/pythonInterpreter.unit.test.ts', + 'src/test/application/diagnostics/checks/powerShellActivation.unit.test.ts', + 'src/test/application/diagnostics/checks/envPathVariable.unit.test.ts', + 'src/test/application/diagnostics/applicationDiagnostics.unit.test.ts', + 'src/test/application/diagnostics/promptHandler.unit.test.ts', + 'src/test/application/diagnostics/commands/ignore.unit.test.ts', + 'src/test/performance/load.perf.test.ts', + 'src/client/interpreter/configuration/interpreterSelector/commands/base.ts', + 'src/client/interpreter/configuration/interpreterSelector/commands/resetInterpreter.ts', + 'src/client/interpreter/configuration/pythonPathUpdaterServiceFactory.ts', + 'src/client/interpreter/configuration/services/globalUpdaterService.ts', + 'src/client/interpreter/configuration/services/workspaceUpdaterService.ts', + 'src/client/interpreter/configuration/services/workspaceFolderUpdaterService.ts', + 'src/client/interpreter/helpers.ts', + 'src/client/interpreter/virtualEnvs/condaInheritEnvPrompt.ts', + 'src/client/interpreter/display/index.ts', + 'src/client/extension.ts', + 'src/client/startupTelemetry.ts', + 'src/client/terminals/codeExecution/terminalCodeExecution.ts', + 'src/client/terminals/codeExecution/codeExecutionManager.ts', + 'src/client/terminals/codeExecution/djangoContext.ts', + 'src/client/activation/commands.ts', + 'src/client/activation/progress.ts', + 'src/client/activation/extensionSurvey.ts', + 'src/client/activation/common/analysisOptions.ts', + 'src/client/activation/languageClientMiddleware.ts', + 'src/client/testing/serviceRegistry.ts', + 'src/client/testing/main.ts', + 'src/client/testing/configurationFactory.ts', + 'src/client/testing/common/constants.ts', + 'src/client/testing/common/testUtils.ts', + 'src/client/common/helpers.ts', + 'src/client/common/net/browser.ts', + 'src/client/common/net/socket/socketCallbackHandler.ts', + 'src/client/common/net/socket/socketServer.ts', + 'src/client/common/net/socket/SocketStream.ts', + 'src/client/common/contextKey.ts', + 'src/client/common/experiments/telemetry.ts', + 'src/client/common/platform/serviceRegistry.ts', + 'src/client/common/platform/errors.ts', + 'src/client/common/platform/fs-temp.ts', + 'src/client/common/platform/fs-paths.ts', + 'src/client/common/platform/registry.ts', + 'src/client/common/platform/pathUtils.ts', + 'src/client/common/persistentState.ts', + 'src/client/common/terminal/activator/base.ts', + 'src/client/common/terminal/activator/powershellFailedHandler.ts', + 'src/client/common/terminal/activator/index.ts', + 'src/client/common/terminal/helper.ts', + 'src/client/common/terminal/syncTerminalService.ts', + 'src/client/common/terminal/factory.ts', + 'src/client/common/terminal/commandPrompt.ts', + 'src/client/common/terminal/service.ts', + 'src/client/common/terminal/shellDetector.ts', + 'src/client/common/terminal/shellDetectors/userEnvironmentShellDetector.ts', + 'src/client/common/terminal/shellDetectors/vscEnvironmentShellDetector.ts', + 'src/client/common/terminal/shellDetectors/terminalNameShellDetector.ts', + 'src/client/common/terminal/shellDetectors/settingsShellDetector.ts', + 'src/client/common/terminal/shellDetectors/baseShellDetector.ts', + 'src/client/common/utils/decorators.ts', + 'src/client/common/utils/enum.ts', + 'src/client/common/utils/platform.ts', + 'src/client/common/utils/stopWatch.ts', + 'src/client/common/utils/random.ts', + 'src/client/common/utils/sysTypes.ts', + 'src/client/common/utils/misc.ts', + 'src/client/common/utils/cacheUtils.ts', + 'src/client/common/utils/workerPool.ts', + 'src/client/common/extensions.ts', + 'src/client/common/variables/serviceRegistry.ts', + 'src/client/common/variables/environment.ts', + 'src/client/common/variables/types.ts', + 'src/client/common/variables/systemVariables.ts', + 'src/client/common/cancellation.ts', + 'src/client/common/interpreterPathService.ts', + 'src/client/common/application/applicationShell.ts', + 'src/client/common/application/languageService.ts', + 'src/client/common/application/clipboard.ts', + 'src/client/common/application/workspace.ts', + 'src/client/common/application/debugSessionTelemetry.ts', + 'src/client/common/application/documentManager.ts', + 'src/client/common/application/debugService.ts', + 'src/client/common/application/commands/reloadCommand.ts', + 'src/client/common/application/terminalManager.ts', + 'src/client/common/application/applicationEnvironment.ts', + 'src/client/common/errors/errorUtils.ts', + 'src/client/common/installer/serviceRegistry.ts', + 'src/client/common/installer/channelManager.ts', + 'src/client/common/installer/moduleInstaller.ts', + 'src/client/common/installer/types.ts', + 'src/client/common/installer/pipEnvInstaller.ts', + 'src/client/common/installer/productService.ts', + 'src/client/common/installer/pipInstaller.ts', + 'src/client/common/installer/productPath.ts', + 'src/client/common/process/currentProcess.ts', + 'src/client/common/process/processFactory.ts', + 'src/client/common/process/serviceRegistry.ts', + 'src/client/common/process/pythonToolService.ts', + 'src/client/common/process/internal/python.ts', + 'src/client/common/process/internal/scripts/testing_tools.ts', + 'src/client/common/process/types.ts', + 'src/client/common/process/logger.ts', + 'src/client/common/process/pythonProcess.ts', + 'src/client/common/process/pythonEnvironment.ts', + 'src/client/common/process/decoder.ts', + 'src/client/debugger/extension/adapter/remoteLaunchers.ts', + 'src/client/debugger/extension/adapter/outdatedDebuggerPrompt.ts', + 'src/client/debugger/extension/adapter/factory.ts', + 'src/client/debugger/extension/adapter/activator.ts', + 'src/client/debugger/extension/adapter/logging.ts', + 'src/client/debugger/extension/hooks/eventHandlerDispatcher.ts', + 'src/client/debugger/extension/hooks/childProcessAttachService.ts', + 'src/client/debugger/extension/attachQuickPick/wmicProcessParser.ts', + 'src/client/debugger/extension/attachQuickPick/factory.ts', + 'src/client/debugger/extension/attachQuickPick/psProcessParser.ts', + 'src/client/debugger/extension/attachQuickPick/picker.ts', + 'src/client/application/serviceRegistry.ts', + 'src/client/application/diagnostics/base.ts', + 'src/client/application/diagnostics/applicationDiagnostics.ts', + 'src/client/application/diagnostics/filter.ts', + 'src/client/application/diagnostics/promptHandler.ts', + 'src/client/application/diagnostics/commands/base.ts', + 'src/client/application/diagnostics/commands/ignore.ts', + 'src/client/application/diagnostics/commands/factory.ts', + 'src/client/application/diagnostics/commands/execVSCCommand.ts', + 'src/client/application/diagnostics/commands/launchBrowser.ts', + ], + linterOptions: { + reportUnusedDisableDirectives: 'off', + }, + rules: { + ...js.configs.recommended.rules, + 'no-undef': 'off', + }, + }, + // TypeScript-specific configuration + { + files: ['**/*.ts', '**/*.tsx', '**/*.js', 'src', 'pythonExtensionApi/src'], + languageOptions: { + parser: tsParser, + parserOptions: { + ecmaVersion: 'latest', + sourceType: 'module', + }, + globals: { + ...(js.configs.recommended.languageOptions?.globals || {}), + mocha: true, + require: 'readonly', + process: 'readonly', + exports: 'readonly', + module: 'readonly', + __dirname: 'readonly', + __filename: 'readonly', + setTimeout: 'readonly', + setInterval: 'readonly', + clearTimeout: 'readonly', + clearInterval: 'readonly', + }, + }, + plugins: { + '@typescript-eslint': tseslint, + 'no-only-tests': noOnlyTests, + import: importPlugin, + prettier: prettier, + 'no-bad-gdpr-comment': noBadGdprCommentPlugin, // Register your plugin + }, + settings: { + 'import/resolver': { + node: { + extensions: ['.js', '.ts'], + }, + }, + }, + rules: { + 'no-bad-gdpr-comment/no-bad-gdpr-comment': 'warn', // Enable your rule + // Base configurations + ...tseslint.configs.recommended.rules, + ...prettier.rules, + + // TypeScript-specific rules + '@typescript-eslint/ban-ts-comment': [ + 'error', + { + 'ts-ignore': 'allow-with-description', + }, + ], + '@typescript-eslint/ban-types': 'off', + '@typescript-eslint/explicit-module-boundary-types': 'off', + '@typescript-eslint/no-empty-interface': 'off', + '@typescript-eslint/no-explicit-any': 'off', + '@typescript-eslint/no-namespace': 'off', + '@typescript-eslint/no-non-null-assertion': 'off', + '@typescript-eslint/no-loss-of-precision': 'off', + '@typescript-eslint/no-unused-vars': [ + 'warn', + { + varsIgnorePattern: '^_', + argsIgnorePattern: '^_', + }, + ], + '@typescript-eslint/no-var-requires': 'off', + '@typescript-eslint/no-use-before-define': [ + 'error', + { + functions: false, + }, + ], + + // Import rules + 'import/extensions': 'off', + 'import/namespace': 'off', + 'import/no-extraneous-dependencies': 'off', + 'import/no-unresolved': 'off', + 'import/prefer-default-export': 'off', + + // Testing rules + 'no-only-tests/no-only-tests': [ + 'error', + { + block: ['test', 'suite'], + focus: ['only'], + }, + ], + + // Code style rules + 'linebreak-style': 'off', + 'no-bitwise': 'off', + 'no-console': 'off', + 'no-underscore-dangle': 'off', + 'operator-assignment': 'off', + 'func-names': 'off', + + // Error handling and control flow + 'no-empty': ['error', { allowEmptyCatch: true }], + 'no-async-promise-executor': 'off', + 'no-await-in-loop': 'off', + 'no-unreachable': 'off', + 'no-void': 'off', + + // Duplicates and overrides (TypeScript handles these) + 'no-dupe-class-members': 'off', + 'no-redeclare': 'off', + 'no-undef': 'off', + + // Miscellaneous rules + 'no-control-regex': 'off', + 'no-extend-native': 'off', + 'no-inner-declarations': 'off', + 'no-multi-str': 'off', + 'no-param-reassign': 'off', + 'no-prototype-builtins': 'off', + 'no-empty-function': 'off', + 'no-template-curly-in-string': 'off', + 'no-useless-escape': 'off', + 'no-extra-parentheses': 'off', + 'no-extra-paren': 'off', + '@typescript-eslint/no-extra-parens': 'off', + strict: 'off', + + // Restricted syntax + 'no-restricted-syntax': [ + 'error', + { + selector: 'ForInStatement', + message: + 'for..in loops iterate over the entire prototype chain, which is virtually never what you want. Use Object.{keys,values,entries}, and iterate over the resulting array.', + }, + { + selector: 'LabeledStatement', + message: + 'Labels are a form of GOTO; using them makes code confusing and hard to maintain and understand.', + }, + { + selector: 'WithStatement', + message: + '`with` is disallowed in strict mode because it makes code impossible to predict and optimize.', + }, + ], + }, + }, +]; diff --git a/gulpfile.js b/gulpfile.js index a344b165a6cc..0b919f16572a 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -20,14 +20,13 @@ const nativeDependencyChecker = require('node-has-native-dependencies'); const flat = require('flat'); const { argv } = require('yargs'); const os = require('os'); -const rmrf = require('rimraf'); const typescript = require('typescript'); const tsProject = ts.createProject('./tsconfig.json', { typescript }); const isCI = process.env.TRAVIS === 'true' || process.env.TF_BUILD !== undefined; -gulp.task('compile', (done) => { +gulp.task('compileCore', (done) => { let failed = false; tsProject .src() @@ -39,6 +38,23 @@ gulp.task('compile', (done) => { .on('finish', () => (failed ? done(new Error('TypeScript compilation errors')) : done())); }); +gulp.task('compileApi', (done) => { + spawnAsync('npm', ['run', 'compileApi'], undefined, true) + .then((stdout) => { + if (stdout.includes('error')) { + done(new Error(stdout)); + } else { + done(); + } + }) + .catch((ex) => { + console.log(ex); + done(new Error('TypeScript compilation errors', ex)); + }); +}); + +gulp.task('compile', gulp.series('compileCore', 'compileApi')); + gulp.task('precommit', (done) => run({ exitOnError: true, mode: 'staged' }, done)); gulp.task('output:clean', () => del(['coverage'])); @@ -82,9 +98,11 @@ async function addExtensionPackDependencies() { // extension dependencies need not be installed during development const packageJsonContents = await fsExtra.readFile('package.json', 'utf-8'); const packageJson = JSON.parse(packageJsonContents); - packageJson.extensionPack = ['ms-python.vscode-pylance'].concat( - packageJson.extensionPack ? packageJson.extensionPack : [], - ); + packageJson.extensionPack = [ + 'ms-python.vscode-pylance', + 'ms-python.debugpy', + 'ms-python.vscode-python-envs', + ].concat(packageJson.extensionPack ? packageJson.extensionPack : []); // Remove potential duplicates. packageJson.extensionPack = packageJson.extensionPack.filter( (item, index) => packageJson.extensionPack.indexOf(item) === index, @@ -206,12 +224,6 @@ function getAllowedWarningsForWebPack(buildConfig) { throw new Error('Unknown WebPack Configuration'); } } -gulp.task('renameSourceMaps', async () => { - // By default source maps will be disabled in the extension. - // Users will need to use the command `python.enableSourceMapSupport` to enable source maps. - const extensionSourceMap = path.join(__dirname, 'out', 'client', 'extension.js.map'); - await fsExtra.rename(extensionSourceMap, `${extensionSourceMap}.disabled`); -}); gulp.task('verifyBundle', async () => { const matches = await glob.sync(path.join(__dirname, '*.vsix')); @@ -222,94 +234,10 @@ gulp.task('verifyBundle', async () => { } }); -gulp.task('prePublishBundle', gulp.series('webpack', 'renameSourceMaps')); +gulp.task('prePublishBundle', gulp.series('webpack')); gulp.task('checkDependencies', gulp.series('checkNativeDependencies')); gulp.task('prePublishNonBundle', gulp.series('compile')); -gulp.task('installPythonRequirements', async () => { - let args = [ - '-m', - 'pip', - '--disable-pip-version-check', - 'install', - '--no-user', - '-t', - './pythonFiles/lib/python', - '--no-cache-dir', - '--implementation', - 'py', - '--no-deps', - '--upgrade', - '-r', - './requirements.txt', - ]; - await spawnAsync(process.env.CI_PYTHON_PATH || 'python', args, undefined, true) - .then(() => true) - .catch((ex) => { - console.error("Failed to install requirements using 'python'", ex); - return false; - }); - - args = [ - '-m', - 'pip', - '--disable-pip-version-check', - 'install', - '--no-user', - '-t', - './pythonFiles/lib/jedilsp', - '--no-cache-dir', - '--implementation', - 'py', - '--no-deps', - '--upgrade', - '-r', - './pythonFiles/jedilsp_requirements/requirements.txt', - ]; - await spawnAsync(process.env.CI_PYTHON_PATH || 'python', args, undefined, true) - .then(() => true) - .catch((ex) => { - console.error("Failed to install Jedi LSP requirements using 'python'", ex); - return false; - }); -}); - -// See https://github.com/microsoft/vscode-python/issues/7136 -gulp.task('installDebugpy', async () => { - // Install dependencies needed for 'install_debugpy.py' - const depsArgs = [ - '-m', - 'pip', - '--disable-pip-version-check', - 'install', - '--no-user', - '-t', - './pythonFiles/lib/temp', - '-r', - './build/debugger-install-requirements.txt', - ]; - await spawnAsync(process.env.CI_PYTHON_PATH || 'python', depsArgs, undefined, true) - .then(() => true) - .catch((ex) => { - console.error("Failed to install dependencies need by 'install_debugpy.py' using 'python'", ex); - return false; - }); - - // Install new DEBUGPY with wheels for python 3.7 - const wheelsArgs = ['./pythonFiles/install_debugpy.py']; - const wheelsEnv = { PYTHONPATH: './pythonFiles/lib/temp' }; - await spawnAsync(process.env.CI_PYTHON_PATH || 'python', wheelsArgs, wheelsEnv, true) - .then(() => true) - .catch((ex) => { - console.error("Failed to install DEBUGPY wheels using 'python'", ex); - return false; - }); - - rmrf.sync('./pythonFiles/lib/temp'); -}); - -gulp.task('installPythonLibs', gulp.series('installPythonRequirements', 'installDebugpy')); - function spawnAsync(command, args, env, rejectOnStdErr = false) { env = env || {}; env = { ...process.env, ...env }; diff --git a/noxfile.py b/noxfile.py new file mode 100644 index 000000000000..3991ee8c025a --- /dev/null +++ b/noxfile.py @@ -0,0 +1,161 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +import os +import pathlib +import nox +import shutil +import sys +import sysconfig +import uuid + +EXT_ROOT = pathlib.Path(__file__).parent + + +def delete_dir(path: pathlib.Path, ignore_errors=None): + attempt = 0 + known = [] + while attempt < 5: + try: + shutil.rmtree(os.fspath(path), ignore_errors=ignore_errors) + return + except PermissionError as pe: + if os.fspath(pe.filename) in known: + break + print(f"Changing permissions on {pe.filename}") + os.chmod(pe.filename, 0o666) + + shutil.rmtree(os.fspath(path)) + + +@nox.session() +def install_python_libs(session: nox.Session): + requirements = [ + ("./python_files/lib/python", "./requirements.txt"), + ( + "./python_files/lib/jedilsp", + "./python_files/jedilsp_requirements/requirements.txt", + ), + ] + for target, file in requirements: + session.install( + "-t", + target, + "--no-cache-dir", + "--implementation", + "py", + "--no-deps", + "--require-hashes", + "--only-binary", + ":all:", + "-r", + file, + ) + + session.install("packaging") + session.install("debugpy") + + # Download get-pip script + session.run( + "python", + "./python_files/download_get_pip.py", + env={"PYTHONPATH": "./python_files/lib/temp"}, + ) + + if pathlib.Path("./python_files/lib/temp").exists(): + shutil.rmtree("./python_files/lib/temp") + + +@nox.session() +def native_build(session: nox.Session): + source_dir = pathlib.Path(pathlib.Path.cwd() / "python-env-tools").resolve() + dest_dir = pathlib.Path(pathlib.Path.cwd() / "python-env-tools").resolve() + + with session.cd(source_dir): + if not pathlib.Path(dest_dir / "bin").exists(): + pathlib.Path(dest_dir / "bin").mkdir() + + if not pathlib.Path(dest_dir / "bin" / ".gitignore").exists(): + pathlib.Path(dest_dir / "bin" / ".gitignore").write_text( + "*\n", encoding="utf-8" + ) + + ext = sysconfig.get_config_var("EXE") or "" + target = os.environ.get("CARGO_TARGET", None) + + session.run("cargo", "fetch", external=True) + if target: + session.run( + "cargo", + "build", + "--frozen", + "--release", + "--target", + target, + external=True, + ) + source = source_dir / "target" / target / "release" / f"pet{ext}" + else: + session.run( + "cargo", + "build", + "--frozen", + "--release", + external=True, + ) + source = source_dir / "target" / "release" / f"pet{ext}" + dest = dest_dir / "bin" / f"pet{ext}" + shutil.copy(source, dest) + + # Remove python-env-tools/bin exclusion from .vscodeignore + vscode_ignore = EXT_ROOT / ".vscodeignore" + remove_patterns = ("python-env-tools/bin/**",) + lines = vscode_ignore.read_text(encoding="utf-8").splitlines() + filtered_lines = [line for line in lines if not line.startswith(remove_patterns)] + vscode_ignore.write_text("\n".join(filtered_lines) + "\n", encoding="utf-8") + + +@nox.session() +def checkout_native(session: nox.Session): + dest = (pathlib.Path.cwd() / "python-env-tools").resolve() + if dest.exists(): + shutil.rmtree(os.fspath(dest)) + + temp_dir = os.getenv("TEMP") or os.getenv("TMP") or "/tmp" + temp_dir = pathlib.Path(temp_dir) / str(uuid.uuid4()) / "python-env-tools" + temp_dir.mkdir(0o766, parents=True) + + session.log(f"Cloning python-environment-tools to {temp_dir}") + try: + with session.cd(temp_dir): + session.run("git", "init", external=True) + session.run( + "git", + "remote", + "add", + "origin", + "https://github.com/microsoft/python-environment-tools", + external=True, + ) + session.run("git", "fetch", "origin", "main", external=True) + session.run( + "git", "checkout", "--force", "-B", "main", "origin/main", external=True + ) + delete_dir(temp_dir / ".git") + delete_dir(temp_dir / ".github") + delete_dir(temp_dir / ".vscode") + (temp_dir / "CODE_OF_CONDUCT.md").unlink() + shutil.move(os.fspath(temp_dir), os.fspath(dest)) + except PermissionError as e: + print(f"Permission error: {e}") + if not dest.exists(): + raise + finally: + delete_dir(temp_dir.parent, ignore_errors=True) + + +@nox.session() +def setup_repo(session: nox.Session): + install_python_libs(session) + checkout_native(session) + native_build(session) diff --git a/package-lock.json b/package-lock.json index f00cbb0a912e..82053df77576 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,47 +1,40 @@ { "name": "python", - "version": "2023.9.0-dev", + "version": "2026.5.0-dev", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "python", - "version": "2023.9.0-dev", + "version": "2026.5.0-dev", "license": "MIT", "dependencies": { - "@iarna/toml": "^2.2.5", - "@vscode/extension-telemetry": "^0.7.7", - "@vscode/jupyter-lsp-middleware": "^0.2.50", + "@iarna/toml": "^3.0.0", + "@vscode/extension-telemetry": "^0.8.4", "arch": "^2.1.0", - "diff-match-patch": "^1.0.0", - "fs-extra": "^10.0.1", + "fs-extra": "^11.2.0", "glob": "^7.2.0", - "hash.js": "^1.1.7", "iconv-lite": "^0.6.3", - "inversify": "^5.0.4", + "inversify": "^6.0.2", "jsonc-parser": "^3.0.0", - "lodash": "^4.17.21", - "md5": "^2.2.1", - "minimatch": "^5.0.1", + "lodash": "^4.18.1", + "minimatch": "^5.1.8", "named-js-regexp": "^1.3.3", "node-stream-zip": "^1.6.0", - "reflect-metadata": "^0.1.12", + "reflect-metadata": "^0.2.2", "rxjs": "^6.5.4", "rxjs-compat": "^6.5.4", - "semver": "^5.5.0", + "semver": "^7.5.2", "stack-trace": "0.0.10", "sudo-prompt": "^9.2.1", - "tmp": "^0.0.33", + "tmp": "^0.2.5", "uint64be": "^3.0.0", "unicode": "^14.0.0", - "untildify": "^4.0.0", - "vscode-debugadapter": "^1.28.0", "vscode-debugprotocol": "^1.28.0", - "vscode-jsonrpc": "8.0.2-next.1", - "vscode-languageclient": "8.0.2-next.5", - "vscode-languageserver": "8.0.2-next.5", - "vscode-languageserver-protocol": "3.17.2-next.6", - "vscode-tas-client": "^0.1.63", + "vscode-jsonrpc": "^9.0.0-next.5", + "vscode-languageclient": "^10.0.0-next.12", + "vscode-languageserver-protocol": "^3.17.6-next.10", + "vscode-tas-client": "^0.1.84", "which": "^2.0.2", "winreg": "^1.2.4", "xml2js": "^0.5.0" @@ -52,83 +45,99 @@ "@types/chai": "^4.1.2", "@types/chai-arrays": "^2.0.0", "@types/chai-as-promised": "^7.1.0", - "@types/diff-match-patch": "^1.0.32", "@types/download": "^8.0.1", - "@types/fs-extra": "^9.0.13", + "@types/fs-extra": "^11.0.4", "@types/glob": "^7.2.0", "@types/lodash": "^4.14.104", - "@types/md5": "^2.1.32", "@types/mocha": "^9.1.0", - "@types/nock": "^10.0.3", - "@types/node": "^16.17.0", + "@types/node": "^22.19.1", "@types/semver": "^5.5.0", "@types/shortid": "^0.0.29", - "@types/sinon": "^10.0.11", + "@types/sinon": "^17.0.3", "@types/stack-trace": "0.0.29", "@types/tmp": "^0.0.33", - "@types/uuid": "^8.3.4", - "@types/vscode": "^1.75.0", + "@types/vscode": "^1.95.0", "@types/which": "^2.0.1", "@types/winreg": "^1.2.30", "@types/xml2js": "^0.4.2", - "@typescript-eslint/eslint-plugin": "^3.7.0", - "@typescript-eslint/parser": "^3.7.0", - "@vscode/test-electron": "^2.1.3", - "@vscode/vsce": "^2.18.0", + "@typescript-eslint/eslint-plugin": "^6.21.0", + "@typescript-eslint/parser": "^6.21.0", + "@vscode/test-electron": "^2.3.8", + "@vscode/vsce": "^2.27.0", "bent": "^7.3.12", "chai": "^4.1.2", "chai-arrays": "^2.0.0", "chai-as-promised": "^7.1.1", "copy-webpack-plugin": "^9.1.0", + "cross-env": "^7.0.3", "cross-spawn": "^6.0.5", "del": "^6.0.0", "download": "^8.0.0", - "es5-ext": "0.10.53", - "eslint": "^7.2.0", - "eslint-config-airbnb": "^18.2.0", + "eslint": "^8.57.1", "eslint-config-prettier": "^8.3.0", - "eslint-plugin-import": "^2.25.4", + "eslint-plugin-import": "^2.31.0", "eslint-plugin-jsx-a11y": "^6.3.1", + "eslint-plugin-no-only-tests": "^3.3.0", "eslint-plugin-react": "^7.20.3", "eslint-plugin-react-hooks": "^4.0.0", "expose-loader": "^3.1.0", "flat": "^5.0.2", "get-port": "^5.1.1", - "gulp": "^4.0.0", + "gulp": "^5.0.0", "gulp-typescript": "^5.0.0", - "mocha": "^9.2.2", + "mocha": "^11.1.0", "mocha-junit-reporter": "^2.0.2", "mocha-multi-reporters": "^1.1.7", - "nock": "^10.0.6", "node-has-native-dependencies": "^1.0.2", "node-loader": "^1.0.2", "node-polyfill-webpack-plugin": "^1.1.4", "nyc": "^15.0.0", "prettier": "^2.0.2", "rewiremock": "^3.13.0", - "rimraf": "^3.0.2", "shortid": "^2.2.8", - "sinon": "^13.0.1", + "sinon": "^18.0.0", "source-map-support": "^0.5.12", "ts-loader": "^9.2.8", "ts-mockito": "^2.5.0", "ts-node": "^10.7.0", "tsconfig-paths-webpack-plugin": "^3.2.0", "typemoq": "^2.1.0", - "typescript": "4.5.5", - "uuid": "^8.3.2", - "vscode-debugadapter-testsupport": "^1.27.0", - "webpack": "^5.76.0", + "typescript": "~5.2", + "uuid": "^14.0.0", + "webpack": "^5.105.0", "webpack-bundle-analyzer": "^4.5.0", "webpack-cli": "^4.9.2", "webpack-fix-default-import-plugin": "^1.0.3", "webpack-merge": "^5.8.0", "webpack-node-externals": "^3.0.0", "webpack-require-from": "^1.8.6", + "worker-loader": "^3.0.8", "yargs": "^15.3.1" }, "engines": { - "vscode": "^1.78.0" + "vscode": "^1.95.0" + } + }, + "node_modules/@aashutoshrathi/word-wrap": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@aashutoshrathi/word-wrap/-/word-wrap-1.2.6.tgz", + "integrity": "sha512-1Yjs2SvM8TflER/OD3cOjhWWOZb58A2t7wpE2S9XfBYTiIl+XFhQG2bjy4Pu1I+EAlCNUzRDYDdFwFYUKvXcIA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/@ampproject/remapping": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.2.1.tgz", + "integrity": "sha512-lFMjJTrFL3j7L9yBxwYfCq2k6qqwHyzuUl/XBnif78PWTJYyL/dfowQHWE3sp6U6ZzqWiiIZnpTMO96zhkjwtg==", + "dev": true, + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.0", + "@jridgewell/trace-mapping": "^0.3.9" + }, + "engines": { + "node": ">=6.0.0" } }, "node_modules/@azure/abort-controller": { @@ -143,41 +152,92 @@ } }, "node_modules/@azure/abort-controller/node_modules/tslib": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.5.0.tgz", - "integrity": "sha512-336iVw3rtn2BUK7ORdIAHTyxHGRIHVReokCR3XjbckJMK7ms8FysBfhLR8IXnAgy7T0PTPNBWKiH514FOW/WSg==" + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" }, "node_modules/@azure/core-auth": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/@azure/core-auth/-/core-auth-1.4.0.tgz", - "integrity": "sha512-HFrcTgmuSuukRf/EdPmqBrc5l6Q5Uu+2TbuhaKbgaCpP2TfAeiNaQPAadxO+CYBRHGUzIDteMAjFspFLDLnKVQ==", + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@azure/core-auth/-/core-auth-1.5.0.tgz", + "integrity": "sha512-udzoBuYG1VBoHVohDTrvKjyzel34zt77Bhp7dQntVGGD0ehVq48owENbBG8fIgkHRNUBQH5k1r0hpoMu5L8+kw==", "dependencies": { "@azure/abort-controller": "^1.0.0", + "@azure/core-util": "^1.1.0", "tslib": "^2.2.0" }, "engines": { - "node": ">=12.0.0" + "node": ">=14.0.0" } }, "node_modules/@azure/core-auth/node_modules/tslib": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.5.0.tgz", - "integrity": "sha512-336iVw3rtn2BUK7ORdIAHTyxHGRIHVReokCR3XjbckJMK7ms8FysBfhLR8IXnAgy7T0PTPNBWKiH514FOW/WSg==" + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" + }, + "node_modules/@azure/core-client": { + "version": "1.9.2", + "resolved": "https://registry.npmjs.org/@azure/core-client/-/core-client-1.9.2.tgz", + "integrity": "sha512-kRdry/rav3fUKHl/aDLd/pDLcB+4pOFwPPTVEExuMyaI5r+JBbMWqRbCY1pn5BniDaU3lRxO9eaQ1AmSMehl/w==", + "dev": true, + "dependencies": { + "@azure/abort-controller": "^2.0.0", + "@azure/core-auth": "^1.4.0", + "@azure/core-rest-pipeline": "^1.9.1", + "@azure/core-tracing": "^1.0.0", + "@azure/core-util": "^1.6.1", + "@azure/logger": "^1.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@azure/core-client/node_modules/@azure/abort-controller": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/@azure/abort-controller/-/abort-controller-2.1.2.tgz", + "integrity": "sha512-nBrLsEWm4J2u5LpAPjxADTlq3trDgVZZXHNKabeXZtpq3d3AbN/KGO82R87rdDz5/lYB024rtEf10/q0urNgsA==", + "dev": true, + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@azure/core-client/node_modules/@azure/core-util": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@azure/core-util/-/core-util-1.9.0.tgz", + "integrity": "sha512-AfalUQ1ZppaKuxPPMsFEUdX6GZPB3d9paR9d/TTL7Ow2De8cJaC7ibi7kWVlFAVPCYo31OcnGymc0R89DX8Oaw==", + "dev": true, + "dependencies": { + "@azure/abort-controller": "^2.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@azure/core-client/node_modules/tslib": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==", + "dev": true }, "node_modules/@azure/core-rest-pipeline": { - "version": "1.10.3", - "resolved": "https://registry.npmjs.org/@azure/core-rest-pipeline/-/core-rest-pipeline-1.10.3.tgz", - "integrity": "sha512-AMQb0ttiGJ0MIV/r+4TVra6U4+90mPeOveehFnrqKlo7dknPJYdJ61wOzYJXJjDxF8LcCtSogfRelkq+fCGFTw==", + "version": "1.10.1", + "resolved": "https://registry.npmjs.org/@azure/core-rest-pipeline/-/core-rest-pipeline-1.10.1.tgz", + "integrity": "sha512-Kji9k6TOFRDB5ZMTw8qUf2IJ+CeJtsuMdAHox9eqpTf1cefiNMpzrfnF6sINEBZJsaVaWgQ0o48B6kcUH68niA==", "dependencies": { "@azure/abort-controller": "^1.0.0", "@azure/core-auth": "^1.4.0", "@azure/core-tracing": "^1.0.1", - "@azure/core-util": "^1.3.0", + "@azure/core-util": "^1.0.0", "@azure/logger": "^1.0.0", "form-data": "^4.0.0", "http-proxy-agent": "^5.0.0", "https-proxy-agent": "^5.0.0", - "tslib": "^2.2.0" + "tslib": "^2.2.0", + "uuid": "^8.3.0" }, "engines": { "node": ">=14.0.0" @@ -221,9 +281,17 @@ } }, "node_modules/@azure/core-rest-pipeline/node_modules/tslib": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.5.0.tgz", - "integrity": "sha512-336iVw3rtn2BUK7ORdIAHTyxHGRIHVReokCR3XjbckJMK7ms8FysBfhLR8IXnAgy7T0PTPNBWKiH514FOW/WSg==" + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" + }, + "node_modules/@azure/core-rest-pipeline/node_modules/uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "bin": { + "uuid": "dist/bin/uuid" + } }, "node_modules/@azure/core-tracing": { "version": "1.0.1", @@ -237,14 +305,14 @@ } }, "node_modules/@azure/core-tracing/node_modules/tslib": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.5.0.tgz", - "integrity": "sha512-336iVw3rtn2BUK7ORdIAHTyxHGRIHVReokCR3XjbckJMK7ms8FysBfhLR8IXnAgy7T0PTPNBWKiH514FOW/WSg==" + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" }, "node_modules/@azure/core-util": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/@azure/core-util/-/core-util-1.3.0.tgz", - "integrity": "sha512-ANP0Er7R2KHHHjwmKzPF9wbd0gXvOX7yRRHeYL1eNd/OaNrMLyfZH/FQasHRVAf6rMXX+EAUpvYwLMFDHDI5Gw==", + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@azure/core-util/-/core-util-1.2.0.tgz", + "integrity": "sha512-ffGIw+Qs8bNKNLxz5UPkz4/VBM/EZY07mPve1ZYFqYUdPwFqRj0RPk0U7LZMOfT7GCck9YjuT1Rfp1PApNl1ng==", "dependencies": { "@azure/abort-controller": "^1.0.0", "tslib": "^2.2.0" @@ -254,9 +322,65 @@ } }, "node_modules/@azure/core-util/node_modules/tslib": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.5.0.tgz", - "integrity": "sha512-336iVw3rtn2BUK7ORdIAHTyxHGRIHVReokCR3XjbckJMK7ms8FysBfhLR8IXnAgy7T0PTPNBWKiH514FOW/WSg==" + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" + }, + "node_modules/@azure/identity": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@azure/identity/-/identity-4.2.1.tgz", + "integrity": "sha512-U8hsyC9YPcEIzoaObJlRDvp7KiF0MGS7xcWbyJSVvXRkC/HXo1f0oYeBYmEvVgRfacw7GHf6D6yAoh9JHz6A5Q==", + "dev": true, + "dependencies": { + "@azure/abort-controller": "^1.0.0", + "@azure/core-auth": "^1.5.0", + "@azure/core-client": "^1.4.0", + "@azure/core-rest-pipeline": "^1.1.0", + "@azure/core-tracing": "^1.0.0", + "@azure/core-util": "^1.3.0", + "@azure/logger": "^1.0.0", + "@azure/msal-browser": "^3.11.1", + "@azure/msal-node": "^2.9.2", + "events": "^3.0.0", + "jws": "^4.0.0", + "open": "^8.0.0", + "stoppable": "^1.1.0", + "tslib": "^2.2.0" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@azure/identity/node_modules/@azure/core-util": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@azure/core-util/-/core-util-1.9.0.tgz", + "integrity": "sha512-AfalUQ1ZppaKuxPPMsFEUdX6GZPB3d9paR9d/TTL7Ow2De8cJaC7ibi7kWVlFAVPCYo31OcnGymc0R89DX8Oaw==", + "dev": true, + "dependencies": { + "@azure/abort-controller": "^2.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@azure/identity/node_modules/@azure/core-util/node_modules/@azure/abort-controller": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/@azure/abort-controller/-/abort-controller-2.1.2.tgz", + "integrity": "sha512-nBrLsEWm4J2u5LpAPjxADTlq3trDgVZZXHNKabeXZtpq3d3AbN/KGO82R87rdDz5/lYB024rtEf10/q0urNgsA==", + "dev": true, + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@azure/identity/node_modules/tslib": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==", + "dev": true }, "node_modules/@azure/logger": { "version": "1.0.4", @@ -270,118 +394,142 @@ } }, "node_modules/@azure/logger/node_modules/tslib": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.5.0.tgz", - "integrity": "sha512-336iVw3rtn2BUK7ORdIAHTyxHGRIHVReokCR3XjbckJMK7ms8FysBfhLR8IXnAgy7T0PTPNBWKiH514FOW/WSg==" + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" }, - "node_modules/@babel/code-frame": { - "version": "7.12.11", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.12.11.tgz", - "integrity": "sha512-Zt1yodBx1UcyiePMSkWnU4hPqhwq7hGi2nFL1LeA3EUl+q2LQx16MISgJ0+z7dnmgvP9QtIleuETGOiOH1RcIw==", + "node_modules/@azure/msal-browser": { + "version": "3.14.0", + "resolved": "https://registry.npmjs.org/@azure/msal-browser/-/msal-browser-3.14.0.tgz", + "integrity": "sha512-Un85LhOoecJ3HDTS3Uv3UWnXC9/43ZSO+Kc+anSqpZvcEt58SiO/3DuVCAe1A3I5UIBYJNMgTmZPGXQ0MVYrwA==", "dev": true, "dependencies": { - "@babel/highlight": "^7.10.4" + "@azure/msal-common": "14.10.0" + }, + "engines": { + "node": ">=0.8.0" } }, - "node_modules/@babel/helper-validator-identifier": { - "version": "7.15.7", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.15.7.tgz", - "integrity": "sha512-K4JvCtQqad9OY2+yTU8w+E82ywk/fe+ELNlt1G8z3bVGlZfn/hOcQQsUhGhW/N+tb3fxK800wLtKOE/aM0m72w==", + "node_modules/@azure/msal-common": { + "version": "14.10.0", + "resolved": "https://registry.npmjs.org/@azure/msal-common/-/msal-common-14.10.0.tgz", + "integrity": "sha512-Zk6DPDz7e1wPgLoLgAp0349Yay9RvcjPM5We/ehuenDNsz/t9QEFI7tRoHpp/e47I4p20XE3FiDlhKwAo3utDA==", "dev": true, "engines": { - "node": ">=6.9.0" + "node": ">=0.8.0" } }, - "node_modules/@babel/highlight": { - "version": "7.16.0", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.16.0.tgz", - "integrity": "sha512-t8MH41kUQylBtu2+4IQA3atqevA2lRgqA2wyVB/YiWmsDSuylZZuXOUy9ric30hfzauEFfdsuk/eXTRrGrfd0g==", + "node_modules/@azure/msal-node": { + "version": "2.9.2", + "resolved": "https://registry.npmjs.org/@azure/msal-node/-/msal-node-2.9.2.tgz", + "integrity": "sha512-8tvi6Cos3m+0KmRbPjgkySXi+UQU/QiuVRFnrxIwt5xZlEEFa69O04RTaNESGgImyBBlYbo2mfE8/U8Bbdk1WQ==", "dev": true, "dependencies": { - "@babel/helper-validator-identifier": "^7.15.7", - "chalk": "^2.0.0", - "js-tokens": "^4.0.0" + "@azure/msal-common": "14.12.0", + "jsonwebtoken": "^9.0.0", + "uuid": "^8.3.0" }, "engines": { - "node": ">=6.9.0" + "node": ">=16" } }, - "node_modules/@babel/runtime": { - "version": "7.17.8", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.17.8.tgz", - "integrity": "sha512-dQpEpK0O9o6lj6oPu0gRDbbnk+4LeHlNcBpspf6Olzt3GIX4P1lWF1gS+pHLDFlaJvbR6q7jCfQ08zA4QJBnmA==", + "node_modules/@azure/msal-node/node_modules/@azure/msal-common": { + "version": "14.12.0", + "resolved": "https://registry.npmjs.org/@azure/msal-common/-/msal-common-14.12.0.tgz", + "integrity": "sha512-IDDXmzfdwmDkv4SSmMEyAniJf6fDu3FJ7ncOjlxkDuT85uSnLEhZi3fGZpoR7T4XZpOMx9teM9GXBgrfJgyeBw==", "dev": true, - "dependencies": { - "regenerator-runtime": "^0.13.4" - }, "engines": { - "node": ">=6.9.0" + "node": ">=0.8.0" } }, - "node_modules/@babel/runtime-corejs3": { - "version": "7.10.5", - "resolved": "https://registry.npmjs.org/@babel/runtime-corejs3/-/runtime-corejs3-7.10.5.tgz", - "integrity": "sha512-RMafpmrNB5E/bwdSphLr8a8++9TosnyJp98RZzI6VOx2R2CCMpsXXXRvmI700O9oEKpXdZat6oEK68/F0zjd4A==", + "node_modules/@azure/msal-node/node_modules/uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", "dev": true, - "dependencies": { - "core-js-pure": "^3.0.0", - "regenerator-runtime": "^0.13.4" + "bin": { + "uuid": "dist/bin/uuid" } }, - "node_modules/@cspotcode/source-map-consumer": { - "version": "0.8.0", - "resolved": "https://registry.npmjs.org/@cspotcode/source-map-consumer/-/source-map-consumer-0.8.0.tgz", - "integrity": "sha512-41qniHzTU8yAGbCp04ohlmSrZf8bkf/iJsl3V0dRGsQN/5GFfx+LbCSsCpp2gqrqjTVg/K6O8ycoV35JIwAzAg==", - "dev": true, + "node_modules/@azure/opentelemetry-instrumentation-azure-sdk": { + "version": "1.0.0-beta.5", + "resolved": "https://registry.npmjs.org/@azure/opentelemetry-instrumentation-azure-sdk/-/opentelemetry-instrumentation-azure-sdk-1.0.0-beta.5.tgz", + "integrity": "sha512-fsUarKQDvjhmBO4nIfaZkfNSApm1hZBzcvpNbSrXdcUBxu7lRvKsV5DnwszX7cnhLyVOW9yl1uigtRQ1yDANjA==", + "dependencies": { + "@azure/core-tracing": "^1.0.0", + "@azure/logger": "^1.0.0", + "@opentelemetry/api": "^1.4.1", + "@opentelemetry/core": "^1.15.2", + "@opentelemetry/instrumentation": "^0.41.2", + "tslib": "^2.2.0" + }, "engines": { - "node": ">= 12" + "node": ">=14.0.0" } }, - "node_modules/@cspotcode/source-map-support": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.7.0.tgz", - "integrity": "sha512-X4xqRHqN8ACt2aHVe51OxeA2HjbcL4MqFqXkrmQszJ1NOUuUu5u6Vqx/0lZSVNku7velL5FC/s5uEAj1lsBMhA==", + "node_modules/@azure/opentelemetry-instrumentation-azure-sdk/node_modules/tslib": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" + }, + "node_modules/@babel/code-frame": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", + "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==", "dev": true, + "license": "MIT", "dependencies": { - "@cspotcode/source-map-consumer": "0.8.0" + "@babel/helper-validator-identifier": "^7.27.1", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" }, "engines": { - "node": ">=12" + "node": ">=6.9.0" } }, - "node_modules/@discoveryjs/json-ext": { - "version": "0.5.7", - "resolved": "https://registry.npmjs.org/@discoveryjs/json-ext/-/json-ext-0.5.7.tgz", - "integrity": "sha512-dBVuXR082gk3jsFp7Rd/JI4kytwGHecnCoTtXFb7DB6CNHp4rg5k1bhg0nWdLGLnOV71lmDzGQaLMy8iPLY0pw==", + "node_modules/@babel/compat-data": { + "version": "7.22.6", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.22.6.tgz", + "integrity": "sha512-29tfsWTq2Ftu7MXmimyC0C5FDZv5DYxOZkh3XD3+QW4V/BYuv/LyEsjj3c0hqedEaDt6DBfDvexMKU8YevdqFg==", "dev": true, "engines": { - "node": ">=10.0.0" + "node": ">=6.9.0" } }, - "node_modules/@eslint/eslintrc": { - "version": "0.4.3", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-0.4.3.tgz", - "integrity": "sha512-J6KFFz5QCYUJq3pf0mjEcCJVERbzv71PUIDczuh9JkwGEzced6CO5ADLHB1rbf/+oPBtoPfMYNOpGDzCANlbXw==", + "node_modules/@babel/core": { + "version": "7.22.6", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.22.6.tgz", + "integrity": "sha512-HPIyDa6n+HKw5dEuway3vVAhBboYCtREBMp+IWeseZy6TFtzn6MHkCH2KKYUOC/vKKwgSMHQW4htBOrmuRPXfw==", "dev": true, "dependencies": { - "ajv": "^6.12.4", - "debug": "^4.1.1", - "espree": "^7.3.0", - "globals": "^13.9.0", - "ignore": "^4.0.6", - "import-fresh": "^3.2.1", - "js-yaml": "^3.13.1", - "minimatch": "^3.0.4", - "strip-json-comments": "^3.1.1" + "@ampproject/remapping": "^2.2.0", + "@babel/code-frame": "^7.22.5", + "@babel/generator": "^7.22.5", + "@babel/helper-compilation-targets": "^7.22.6", + "@babel/helper-module-transforms": "^7.22.5", + "@babel/helpers": "^7.22.6", + "@babel/parser": "^7.22.6", + "@babel/template": "^7.22.5", + "@babel/traverse": "^7.22.6", + "@babel/types": "^7.22.5", + "@nicolo-ribaudo/semver-v6": "^6.3.3", + "convert-source-map": "^1.7.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.2" }, "engines": { - "node": "^10.12.0 || >=12.0.0" + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" } }, - "node_modules/@eslint/eslintrc/node_modules/debug": { - "version": "4.3.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.3.tgz", - "integrity": "sha512-/zxw5+vh1Tfv+4Qn7a5nsbcJKPaSvCDhojn6FEl9vupwK2VCSDtEiEtqr8DFtzYFOdz63LBkxec7DYuc2jon6Q==", + "node_modules/@babel/core/node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", "dev": true, "dependencies": { "ms": "2.1.2" @@ -395,162 +543,727 @@ } } }, - "node_modules/@eslint/eslintrc/node_modules/globals": { - "version": "13.12.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-13.12.0.tgz", - "integrity": "sha512-uS8X6lSKN2JumVoXrbUz+uG4BYG+eiawqm3qFcT7ammfbUHeCBoJMlHcec/S3krSk73/AE/f0szYFmgAA3kYZg==", + "node_modules/@babel/generator": { + "version": "7.23.0", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.23.0.tgz", + "integrity": "sha512-lN85QRR+5IbYrMWM6Y4pE/noaQtg4pNiqeNGX60eqOfo6gtEj6uw/JagelB8vVztSd7R6M5n1+PQkDbHbBRU4g==", "dev": true, "dependencies": { - "type-fest": "^0.20.2" + "@babel/types": "^7.23.0", + "@jridgewell/gen-mapping": "^0.3.2", + "@jridgewell/trace-mapping": "^0.3.17", + "jsesc": "^2.5.1" }, "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": ">=6.9.0" } }, - "node_modules/@eslint/eslintrc/node_modules/ignore": { - "version": "4.0.6", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-4.0.6.tgz", - "integrity": "sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg==", + "node_modules/@babel/helper-compilation-targets": { + "version": "7.22.6", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.22.6.tgz", + "integrity": "sha512-534sYEqWD9VfUm3IPn2SLcH4Q3P86XL+QvqdC7ZsFrzyyPF3T4XGiVghF6PTYNdWg6pXuoqXxNQAhbYeEInTzA==", "dev": true, + "dependencies": { + "@babel/compat-data": "^7.22.6", + "@babel/helper-validator-option": "^7.22.5", + "@nicolo-ribaudo/semver-v6": "^6.3.3", + "browserslist": "^4.21.9", + "lru-cache": "^5.1.1" + }, "engines": { - "node": ">= 4" + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" } }, - "node_modules/@eslint/eslintrc/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "node_modules/@babel/helper-compilation-targets/node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", "dev": true, "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" + "yallist": "^3.0.2" } }, - "node_modules/@eslint/eslintrc/node_modules/type-fest": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", - "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "node_modules/@babel/helper-compilation-targets/node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true + }, + "node_modules/@babel/helper-environment-visitor": { + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.22.20.tgz", + "integrity": "sha512-zfedSIzFhat/gFhWfHtgWvlec0nqB9YEIVrpuwjruLlXfUSnA8cJB0miHKwqDnQ7d32aKo2xt88/xZptwxbfhA==", "dev": true, "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": ">=6.9.0" } }, - "node_modules/@humanwhocodes/config-array": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.5.0.tgz", - "integrity": "sha512-FagtKFz74XrTl7y6HCzQpwDfXP0yhxe9lHLD1UZxjvZIcbyRz8zTFF/yYNfSfzU414eDwZ1SrO0Qvtyf+wFMQg==", + "node_modules/@babel/helper-function-name": { + "version": "7.23.0", + "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.23.0.tgz", + "integrity": "sha512-OErEqsrxjZTJciZ4Oo+eoZqeW9UIiOcuYKRJA4ZAgV9myA+pOXhhmpfNCKjEH/auVfEYVFJ6y1Tc4r0eIApqiw==", "dev": true, "dependencies": { - "@humanwhocodes/object-schema": "^1.2.0", - "debug": "^4.1.1", - "minimatch": "^3.0.4" + "@babel/template": "^7.22.15", + "@babel/types": "^7.23.0" }, "engines": { - "node": ">=10.10.0" + "node": ">=6.9.0" } }, - "node_modules/@humanwhocodes/config-array/node_modules/debug": { - "version": "4.3.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.3.tgz", - "integrity": "sha512-/zxw5+vh1Tfv+4Qn7a5nsbcJKPaSvCDhojn6FEl9vupwK2VCSDtEiEtqr8DFtzYFOdz63LBkxec7DYuc2jon6Q==", + "node_modules/@babel/helper-hoist-variables": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.22.5.tgz", + "integrity": "sha512-wGjk9QZVzvknA6yKIUURb8zY3grXCcOZt+/7Wcy8O2uctxhplmUPkOdlgoNhmdVee2c92JXbf1xpMtVNbfoxRw==", "dev": true, "dependencies": { - "ms": "2.1.2" + "@babel/types": "^7.22.5" }, "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } + "node": ">=6.9.0" } }, - "node_modules/@humanwhocodes/config-array/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "node_modules/@babel/helper-module-imports": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.22.5.tgz", + "integrity": "sha512-8Dl6+HD/cKifutF5qGd/8ZJi84QeAKh+CEe1sBzz8UayBBGg1dAIJrdHOcOM5b2MpzWL2yuotJTtGjETq0qjXg==", "dev": true, "dependencies": { - "brace-expansion": "^1.1.7" + "@babel/types": "^7.22.5" }, "engines": { - "node": "*" + "node": ">=6.9.0" } }, - "node_modules/@humanwhocodes/object-schema": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-1.2.1.tgz", - "integrity": "sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==", - "dev": true - }, - "node_modules/@iarna/toml": { - "version": "2.2.5", - "resolved": "https://registry.npmjs.org/@iarna/toml/-/toml-2.2.5.tgz", - "integrity": "sha512-trnsAYxU3xnS1gPHPyU961coFyLkh4gAD/0zQ5mymY4yOZ+CYvsPqUbOFSw0aDM4y0tV7tiFxL/1XfXPNC6IPg==" - }, - "node_modules/@istanbuljs/load-nyc-config": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.0.0.tgz", - "integrity": "sha512-ZR0rq/f/E4f4XcgnDvtMWXCUJpi8eO0rssVhmztsZqLIEFA9UUP9zmpE0VxlM+kv/E1ul2I876Fwil2ayptDVg==", + "node_modules/@babel/helper-module-transforms": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.22.5.tgz", + "integrity": "sha512-+hGKDt/Ze8GFExiVHno/2dvG5IdstpzCq0y4Qc9OJ25D4q3pKfiIP/4Vp3/JvhDkLKsDK2api3q3fpIgiIF5bw==", "dev": true, "dependencies": { - "camelcase": "^5.3.1", - "find-up": "^4.1.0", - "js-yaml": "^3.13.1", - "resolve-from": "^5.0.0" + "@babel/helper-environment-visitor": "^7.22.5", + "@babel/helper-module-imports": "^7.22.5", + "@babel/helper-simple-access": "^7.22.5", + "@babel/helper-split-export-declaration": "^7.22.5", + "@babel/helper-validator-identifier": "^7.22.5", + "@babel/template": "^7.22.5", + "@babel/traverse": "^7.22.5", + "@babel/types": "^7.22.5" }, "engines": { - "node": ">=8" + "node": ">=6.9.0" } }, - "node_modules/@istanbuljs/nyc-config-typescript": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@istanbuljs/nyc-config-typescript/-/nyc-config-typescript-1.0.2.tgz", - "integrity": "sha512-iKGIyMoyJuFnJRSVTZ78POIRvNnwZaWIf8vG4ZS3rQq58MMDrqEX2nnzx0R28V2X8JvmKYiqY9FP2hlJsm8A0w==", + "node_modules/@babel/helper-simple-access": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.22.5.tgz", + "integrity": "sha512-n0H99E/K+Bika3++WNL17POvo4rKWZ7lZEp1Q+fStVbUi8nxPQEBOlTmCOxW/0JsS56SKKQ+ojAe2pHKJHN35w==", "dev": true, "dependencies": { - "@istanbuljs/schema": "^0.1.2" + "@babel/types": "^7.22.5" }, "engines": { - "node": ">=8" - }, - "peerDependencies": { - "nyc": ">=15" + "node": ">=6.9.0" } }, - "node_modules/@istanbuljs/schema": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.2.tgz", - "integrity": "sha512-tsAQNx32a8CoFhjhijUIhI4kccIAgmGhy8LZMZgGfmXcpMbPRUqn5LWmgRttILi6yeGmBJd2xsPkFMs0PzgPCw==", + "node_modules/@babel/helper-split-export-declaration": { + "version": "7.22.6", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.22.6.tgz", + "integrity": "sha512-AsUnxuLhRYsisFiaJwvp1QF+I3KjD5FOxut14q/GzovUe6orHLesW2C7d754kRm53h5gqrz6sFl6sxc4BVtE/g==", "dev": true, + "dependencies": { + "@babel/types": "^7.22.5" + }, "engines": { - "node": ">=8" + "node": ">=6.9.0" } }, - "node_modules/@jridgewell/gen-mapping": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.2.tgz", - "integrity": "sha512-mh65xKQAzI6iBcFzwv28KVWSmCkdRBWoOh+bYQGW3+6OZvbbN3TqMGo5hqYxQniRcH9F2VZIoJCm4pa3BPDK/A==", + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", "dev": true, - "dependencies": { - "@jridgewell/set-array": "^1.0.1", - "@jridgewell/sourcemap-codec": "^1.4.10", - "@jridgewell/trace-mapping": "^0.3.9" - }, + "license": "MIT", "engines": { - "node": ">=6.0.0" + "node": ">=6.9.0" } }, - "node_modules/@jridgewell/resolve-uri": { + "node_modules/@babel/helper-validator-identifier": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.27.1.tgz", + "integrity": "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.22.5.tgz", + "integrity": "sha512-R3oB6xlIVKUnxNUxbmgq7pKjxpru24zlimpE8WK47fACIlM0II/Hm1RS8IaOI7NgCr6LNS+jl5l75m20npAziw==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.27.1.tgz", + "integrity": "sha512-FCvFTm0sWV8Fxhpp2McP5/W53GPllQ9QeQ7SiqGWjMf/LVG07lFa5+pgK05IRhVwtvafT22KF+ZSnM9I545CvQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.27.1", + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.27.2.tgz", + "integrity": "sha512-QYLs8299NA7WM/bZAdp+CviYYkVoYXlDW2rzliy3chxd1PQjej7JORuMJDJXJUb9g0TT+B99EwaVLKmX+sPXWw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.27.1" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/runtime": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.27.1.tgz", + "integrity": "sha512-1x3D2xEk2fRo3PAhwQwu5UubzgiVWSXTBfWpVd2Mx2AzRqJuDJCsgaDVZ7HB5iGzDW1Hl1sWN2mFyKjmR9uAog==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/runtime-corejs3": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/runtime-corejs3/-/runtime-corejs3-7.27.1.tgz", + "integrity": "sha512-909rVuj3phpjW6y0MCXAZ5iNeORePa6ldJvp2baWGcTjwqbBDDz6xoS5JHJ7lS88NlwLYj07ImL/8IUMtDZzTA==", + "dev": true, + "license": "MIT", + "dependencies": { + "core-js-pure": "^3.30.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/template": { + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz", + "integrity": "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/parser": "^7.27.2", + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.23.2", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.23.2.tgz", + "integrity": "sha512-azpe59SQ48qG6nu2CzcMLbxUudtN+dOM9kDbUqGq3HXUJRlo7i8fvPoxQUzYgLZ4cMVmuZgm8vvBpNeRhd6XSw==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.22.13", + "@babel/generator": "^7.23.0", + "@babel/helper-environment-visitor": "^7.22.20", + "@babel/helper-function-name": "^7.23.0", + "@babel/helper-hoist-variables": "^7.22.5", + "@babel/helper-split-export-declaration": "^7.22.6", + "@babel/parser": "^7.23.0", + "@babel/types": "^7.23.0", + "debug": "^4.1.0", + "globals": "^11.1.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse/node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dev": true, + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/@babel/types": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.27.1.tgz", + "integrity": "sha512-+EzkxvLNfiUeKMgy/3luqfsCWFRXLb7U6wNQTk60tovuckwB15B191tJWvpp4HjiQWdJkCxO3Wbvc6jlk3Xb2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@cspotcode/source-map-consumer": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/@cspotcode/source-map-consumer/-/source-map-consumer-0.8.0.tgz", + "integrity": "sha512-41qniHzTU8yAGbCp04ohlmSrZf8bkf/iJsl3V0dRGsQN/5GFfx+LbCSsCpp2gqrqjTVg/K6O8ycoV35JIwAzAg==", + "dev": true, + "engines": { + "node": ">= 12" + } + }, + "node_modules/@cspotcode/source-map-support": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.7.0.tgz", + "integrity": "sha512-X4xqRHqN8ACt2aHVe51OxeA2HjbcL4MqFqXkrmQszJ1NOUuUu5u6Vqx/0lZSVNku7velL5FC/s5uEAj1lsBMhA==", + "dev": true, + "dependencies": { + "@cspotcode/source-map-consumer": "0.8.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@discoveryjs/json-ext": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/@discoveryjs/json-ext/-/json-ext-0.5.7.tgz", + "integrity": "sha512-dBVuXR082gk3jsFp7Rd/JI4kytwGHecnCoTtXFb7DB6CNHp4rg5k1bhg0nWdLGLnOV71lmDzGQaLMy8iPLY0pw==", + "dev": true, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz", + "integrity": "sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==", + "dev": true, + "dependencies": { + "eslint-visitor-keys": "^3.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.11.0", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.11.0.tgz", + "integrity": "sha512-G/M/tIiMrTAxEWRfLfQJMmGNX28IxBg4PBz8XqQhqUHLFI6TL2htpIB1iQCj144V5ee/JaKyT9/WZ0MGZWfA7A==", + "dev": true, + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.4.tgz", + "integrity": "sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^9.6.0", + "globals": "^13.19.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.0", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/eslintrc/node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, + "license": "Python-2.0" + }, + "node_modules/@eslint/eslintrc/node_modules/debug": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", + "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/@eslint/eslintrc/node_modules/globals": { + "version": "13.24.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", + "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "type-fest": "^0.20.2" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@eslint/eslintrc/node_modules/js-yaml": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", + "dev": true, + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/@eslint/eslintrc/node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/@eslint/eslintrc/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@eslint/eslintrc/node_modules/type-fest": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@eslint/js": { + "version": "8.57.1", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.57.1.tgz", + "integrity": "sha512-d9zaMRSTIKDLhctzH12MtXvJKSSUhaHcjV+2Z+GK+EEY7XKpP5yR4x+N3TAcHTcu963nIr+TMcCb4DBCYX1z6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/@gulpjs/messages": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@gulpjs/messages/-/messages-1.1.0.tgz", + "integrity": "sha512-Ys9sazDatyTgZVb4xPlDufLweJ/Os2uHWOv+Caxvy2O85JcnT4M3vc73bi8pdLWlv3fdWQz3pdI9tVwo8rQQSg==", + "dev": true, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/@gulpjs/to-absolute-glob": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@gulpjs/to-absolute-glob/-/to-absolute-glob-4.0.0.tgz", + "integrity": "sha512-kjotm7XJrJ6v+7knhPaRgaT6q8F8K2jiafwYdNHLzmV0uGLuZY43FK6smNSHUPrhq5kX2slCUy+RGG/xGqmIKA==", + "dev": true, + "dependencies": { + "is-negated-glob": "^1.0.0" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/@humanwhocodes/config-array": { + "version": "0.13.0", + "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.13.0.tgz", + "integrity": "sha512-DZLEEqFWQFiyK6h5YIeynKx7JlvCYWL0cImfSRXZ9l4Sg2efkFGTuFf6vzXjK1cq6IYkU+Eg/JizXw+TD2vRNw==", + "deprecated": "Use @eslint/config-array instead", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanwhocodes/object-schema": "^2.0.3", + "debug": "^4.3.1", + "minimatch": "^3.0.5" + }, + "engines": { + "node": ">=10.10.0" + } + }, + "node_modules/@humanwhocodes/config-array/node_modules/debug": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", + "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/@humanwhocodes/config-array/node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/@humanwhocodes/config-array/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/object-schema": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.3.tgz", + "integrity": "sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==", + "deprecated": "Use @eslint/object-schema instead", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/@iarna/toml": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@iarna/toml/-/toml-3.0.0.tgz", + "integrity": "sha512-td6ZUkz2oS3VeleBcN+m//Q6HlCFCPrnI0FZhrt/h4XqLEdOyYp2u21nd8MdsR+WJy5r9PTDaHTDDfhf4H4l6Q==", + "license": "ISC" + }, + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@isaacs/cliui/node_modules/ansi-regex": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", + "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/ansi-styles": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", + "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@isaacs/cliui/node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@isaacs/cliui/node_modules/strip-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/@istanbuljs/load-nyc-config": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.0.0.tgz", + "integrity": "sha512-ZR0rq/f/E4f4XcgnDvtMWXCUJpi8eO0rssVhmztsZqLIEFA9UUP9zmpE0VxlM+kv/E1ul2I876Fwil2ayptDVg==", + "dev": true, + "dependencies": { + "camelcase": "^5.3.1", + "find-up": "^4.1.0", + "js-yaml": "^3.13.1", + "resolve-from": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/nyc-config-typescript": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@istanbuljs/nyc-config-typescript/-/nyc-config-typescript-1.0.2.tgz", + "integrity": "sha512-iKGIyMoyJuFnJRSVTZ78POIRvNnwZaWIf8vG4ZS3rQq58MMDrqEX2nnzx0R28V2X8JvmKYiqY9FP2hlJsm8A0w==", + "dev": true, + "dependencies": { + "@istanbuljs/schema": "^0.1.2" + }, + "engines": { + "node": ">=8" + }, + "peerDependencies": { + "nyc": ">=15" + } + }, + "node_modules/@istanbuljs/schema": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.2.tgz", + "integrity": "sha512-tsAQNx32a8CoFhjhijUIhI4kccIAgmGhy8LZMZgGfmXcpMbPRUqn5LWmgRttILi6yeGmBJd2xsPkFMs0PzgPCw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz", + "integrity": "sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==", + "dev": true, + "dependencies": { + "@jridgewell/set-array": "^1.2.1", + "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/resolve-uri": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.0.tgz", "integrity": "sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w==", @@ -560,22 +1273,22 @@ } }, "node_modules/@jridgewell/set-array": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.1.2.tgz", - "integrity": "sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz", + "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==", "dev": true, "engines": { "node": ">=6.0.0" } }, "node_modules/@jridgewell/source-map": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.2.tgz", - "integrity": "sha512-m7O9o2uR8k2ObDysZYzdfhb08VuEml5oWGiosa1VdaPZ/A6QyPkAJuwN0Q1lhULOf6B7MtQmHENS743hWtCrgw==", + "version": "0.3.11", + "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.11.tgz", + "integrity": "sha512-ZMp1V8ZFcPG5dIWnQLr3NSI1MiCU7UETdS/A0G8V/XWHvJv3ZsFqutJn1Y5RPmAPX6F3BiE397OqveU/9NCuIA==", "dev": true, "dependencies": { - "@jridgewell/gen-mapping": "^0.3.0", - "@jridgewell/trace-mapping": "^0.3.9" + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25" } }, "node_modules/@jridgewell/sourcemap-codec": { @@ -585,93 +1298,132 @@ "dev": true }, "node_modules/@jridgewell/trace-mapping": { - "version": "0.3.14", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.14.tgz", - "integrity": "sha512-bJWEfQ9lPTvm3SneWwRFVLzrh6nhjwqw7TUFFBEMzwvg7t7PCDenf2lDwqo4NQXzdpgBXyFgDWnQA+2vkruksQ==", + "version": "0.3.25", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", + "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", "dev": true, "dependencies": { - "@jridgewell/resolve-uri": "^3.0.3", - "@jridgewell/sourcemap-codec": "^1.4.10" + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" } }, "node_modules/@microsoft/1ds-core-js": { - "version": "3.2.9", - "resolved": "https://registry.npmjs.org/@microsoft/1ds-core-js/-/1ds-core-js-3.2.9.tgz", - "integrity": "sha512-3pCfM2TzHn3gU9pxHztduKcVRdb/nzruvPFfHPZD0IM0mb0h6TGo2isELF3CTMahTx50RAC51ojNIw2/7VRkOg==", + "version": "3.2.13", + "resolved": "https://registry.npmjs.org/@microsoft/1ds-core-js/-/1ds-core-js-3.2.13.tgz", + "integrity": "sha512-CluYTRWcEk0ObG5EWFNWhs87e2qchJUn0p2D21ZUa3PWojPZfPSBs4//WIE0MYV8Qg1Hdif2ZTwlM7TbYUjfAg==", "dependencies": { - "@microsoft/applicationinsights-core-js": "2.8.10", + "@microsoft/applicationinsights-core-js": "2.8.15", "@microsoft/applicationinsights-shims": "^2.0.2", "@microsoft/dynamicproto-js": "^1.1.7" } }, "node_modules/@microsoft/1ds-post-js": { - "version": "3.2.9", - "resolved": "https://registry.npmjs.org/@microsoft/1ds-post-js/-/1ds-post-js-3.2.9.tgz", - "integrity": "sha512-D/RtqkQ2Nr4cuoGqmhi5QTmi3cBlxehIThJ1u3BaH9H/YkLNTKEcHZRWTXy14bXheCefNHciLuadg37G2Kekcg==", + "version": "3.2.13", + "resolved": "https://registry.npmjs.org/@microsoft/1ds-post-js/-/1ds-post-js-3.2.13.tgz", + "integrity": "sha512-HgS574fdD19Bo2vPguyznL4eDw7Pcm1cVNpvbvBLWiW3x4e1FCQ3VMXChWnAxCae8Hb0XqlA2sz332ZobBavTA==", "dependencies": { - "@microsoft/1ds-core-js": "3.2.9", + "@microsoft/1ds-core-js": "3.2.13", "@microsoft/applicationinsights-shims": "^2.0.2", "@microsoft/dynamicproto-js": "^1.1.7" } }, "node_modules/@microsoft/applicationinsights-channel-js": { - "version": "2.8.11", - "resolved": "https://registry.npmjs.org/@microsoft/applicationinsights-channel-js/-/applicationinsights-channel-js-2.8.11.tgz", - "integrity": "sha512-DGDNzT4DMlSvUzWjA4y3tDg47+QYOPV+W07vlfdPwGgLwrl4n6Q4crrW8Y/IOpthHAKDU8rolSAUvP3NqxPi4Q==", + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@microsoft/applicationinsights-channel-js/-/applicationinsights-channel-js-3.0.2.tgz", + "integrity": "sha512-jDBNKbCHsJgmpv0CKNhJ/uN9ZphvfGdb93Svk+R4LjO8L3apNNMbDDPxBvXXi0uigRmA1TBcmyBG4IRKjabGhw==", "dependencies": { - "@microsoft/applicationinsights-common": "2.8.11", - "@microsoft/applicationinsights-core-js": "2.8.11", - "@microsoft/applicationinsights-shims": "2.0.2", - "@microsoft/dynamicproto-js": "^1.1.7" + "@microsoft/applicationinsights-common": "3.0.2", + "@microsoft/applicationinsights-core-js": "3.0.2", + "@microsoft/applicationinsights-shims": "3.0.1", + "@microsoft/dynamicproto-js": "^2.0.2", + "@nevware21/ts-async": ">= 0.2.4 < 2.x", + "@nevware21/ts-utils": ">= 0.9.5 < 2.x" }, "peerDependencies": { "tslib": "*" } }, "node_modules/@microsoft/applicationinsights-channel-js/node_modules/@microsoft/applicationinsights-core-js": { - "version": "2.8.11", - "resolved": "https://registry.npmjs.org/@microsoft/applicationinsights-core-js/-/applicationinsights-core-js-2.8.11.tgz", - "integrity": "sha512-6ScXplyb9Zb0K6TQRfqStm20j5lIe/Dslf65ozows6ibDcKkWl2ZdqzFhymVJZz1WRNpSyD4aA8qnqmslIER6g==", + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@microsoft/applicationinsights-core-js/-/applicationinsights-core-js-3.0.2.tgz", + "integrity": "sha512-WQhVhzlRlLDrQzn3OShCW/pL3BW5WC57t0oywSknX3q7lMzI3jDg7Ihh0iuIcNTzGCTbDkuqr4d6IjEDWIMtJQ==", "dependencies": { - "@microsoft/applicationinsights-shims": "2.0.2", - "@microsoft/dynamicproto-js": "^1.1.7" + "@microsoft/applicationinsights-shims": "3.0.1", + "@microsoft/dynamicproto-js": "^2.0.2", + "@nevware21/ts-async": ">= 0.2.4 < 2.x", + "@nevware21/ts-utils": ">= 0.9.5 < 2.x" }, "peerDependencies": { "tslib": "*" } }, + "node_modules/@microsoft/applicationinsights-channel-js/node_modules/@microsoft/applicationinsights-shims": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@microsoft/applicationinsights-shims/-/applicationinsights-shims-3.0.1.tgz", + "integrity": "sha512-DKwboF47H1nb33rSUfjqI6ryX29v+2QWcTrRvcQDA32AZr5Ilkr7whOOSsD1aBzwqX0RJEIP1Z81jfE3NBm/Lg==", + "dependencies": { + "@nevware21/ts-utils": ">= 0.9.4 < 2.x" + } + }, + "node_modules/@microsoft/applicationinsights-channel-js/node_modules/@microsoft/dynamicproto-js": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@microsoft/dynamicproto-js/-/dynamicproto-js-2.0.2.tgz", + "integrity": "sha512-MB8trWaFREpmb037k/d0bB7T2BP7Ai24w1e1tbz3ASLB0/lwphsq3Nq8S9I5AsI5vs4zAQT+SB5nC5/dLYTiOg==", + "dependencies": { + "@nevware21/ts-utils": ">= 0.9.4 < 2.x" + } + }, "node_modules/@microsoft/applicationinsights-common": { - "version": "2.8.11", - "resolved": "https://registry.npmjs.org/@microsoft/applicationinsights-common/-/applicationinsights-common-2.8.11.tgz", - "integrity": "sha512-Cxu4gRajkYv9buEtrcLGHK97AqGK62feN9jH9/JSjUSiSFhbnWtYvEg1EMqMI/P4pneu53yLJloITB+TKwmK7A==", + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@microsoft/applicationinsights-common/-/applicationinsights-common-3.0.2.tgz", + "integrity": "sha512-y+WXWop+OVim954Cu1uyYMnNx6PWO8okHpZIQi/1YSqtqaYdtJVPv4P0AVzwJdohxzVfgzKvqj9nec/VWqE2Zg==", "dependencies": { - "@microsoft/applicationinsights-core-js": "2.8.11", - "@microsoft/applicationinsights-shims": "2.0.2", - "@microsoft/dynamicproto-js": "^1.1.7" + "@microsoft/applicationinsights-core-js": "3.0.2", + "@microsoft/applicationinsights-shims": "3.0.1", + "@microsoft/dynamicproto-js": "^2.0.2", + "@nevware21/ts-utils": ">= 0.9.5 < 2.x" }, "peerDependencies": { "tslib": "*" } }, "node_modules/@microsoft/applicationinsights-common/node_modules/@microsoft/applicationinsights-core-js": { - "version": "2.8.11", - "resolved": "https://registry.npmjs.org/@microsoft/applicationinsights-core-js/-/applicationinsights-core-js-2.8.11.tgz", - "integrity": "sha512-6ScXplyb9Zb0K6TQRfqStm20j5lIe/Dslf65ozows6ibDcKkWl2ZdqzFhymVJZz1WRNpSyD4aA8qnqmslIER6g==", + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@microsoft/applicationinsights-core-js/-/applicationinsights-core-js-3.0.2.tgz", + "integrity": "sha512-WQhVhzlRlLDrQzn3OShCW/pL3BW5WC57t0oywSknX3q7lMzI3jDg7Ihh0iuIcNTzGCTbDkuqr4d6IjEDWIMtJQ==", "dependencies": { - "@microsoft/applicationinsights-shims": "2.0.2", - "@microsoft/dynamicproto-js": "^1.1.7" + "@microsoft/applicationinsights-shims": "3.0.1", + "@microsoft/dynamicproto-js": "^2.0.2", + "@nevware21/ts-async": ">= 0.2.4 < 2.x", + "@nevware21/ts-utils": ">= 0.9.5 < 2.x" }, "peerDependencies": { "tslib": "*" } }, + "node_modules/@microsoft/applicationinsights-common/node_modules/@microsoft/applicationinsights-shims": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@microsoft/applicationinsights-shims/-/applicationinsights-shims-3.0.1.tgz", + "integrity": "sha512-DKwboF47H1nb33rSUfjqI6ryX29v+2QWcTrRvcQDA32AZr5Ilkr7whOOSsD1aBzwqX0RJEIP1Z81jfE3NBm/Lg==", + "dependencies": { + "@nevware21/ts-utils": ">= 0.9.4 < 2.x" + } + }, + "node_modules/@microsoft/applicationinsights-common/node_modules/@microsoft/dynamicproto-js": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@microsoft/dynamicproto-js/-/dynamicproto-js-2.0.2.tgz", + "integrity": "sha512-MB8trWaFREpmb037k/d0bB7T2BP7Ai24w1e1tbz3ASLB0/lwphsq3Nq8S9I5AsI5vs4zAQT+SB5nC5/dLYTiOg==", + "dependencies": { + "@nevware21/ts-utils": ">= 0.9.4 < 2.x" + } + }, "node_modules/@microsoft/applicationinsights-core-js": { - "version": "2.8.10", - "resolved": "https://registry.npmjs.org/@microsoft/applicationinsights-core-js/-/applicationinsights-core-js-2.8.10.tgz", - "integrity": "sha512-jQrufDW0+sV8fBhRvzIPNGiCC6dELH+Ug0DM5CfN9757TBqZJz8CSWyDjex39as8+jD0F/8HRU9QdmrVgq5vFg==", + "version": "2.8.15", + "resolved": "https://registry.npmjs.org/@microsoft/applicationinsights-core-js/-/applicationinsights-core-js-2.8.15.tgz", + "integrity": "sha512-yYAs9MyjGr2YijQdUSN9mVgT1ijI1FPMgcffpaPmYbHAVbQmF7bXudrBWHxmLzJlwl5rfep+Zgjli2e67lwUqQ==", "dependencies": { "@microsoft/applicationinsights-shims": "2.0.2", - "@microsoft/dynamicproto-js": "^1.1.7" + "@microsoft/dynamicproto-js": "^1.1.9" }, "peerDependencies": { "tslib": "*" @@ -683,32 +1435,52 @@ "integrity": "sha512-PoHEgsnmcqruLNHZ/amACqdJ6YYQpED0KSRe6J7gIJTtpZC1FfFU9b1fmDKDKtFoUSrPzEh1qzO3kmRZP0betg==" }, "node_modules/@microsoft/applicationinsights-web-basic": { - "version": "2.8.11", - "resolved": "https://registry.npmjs.org/@microsoft/applicationinsights-web-basic/-/applicationinsights-web-basic-2.8.11.tgz", - "integrity": "sha512-11T7bbP4ifIBg95E9mYZv1g/vcWvw/KaWKRcGMREP3+vBTLBwMB8r2e9Zd583bOVx+9/gRvfIg+Z/lInQqAfbA==", + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@microsoft/applicationinsights-web-basic/-/applicationinsights-web-basic-3.0.2.tgz", + "integrity": "sha512-6Lq0DE/pZp9RvSV+weGbcxN1NDmfczj6gNPhvZKV2YSQ3RK0LZE3+wjTWLXfuStq8a+nCBdsRpWk8tOKgsoxcg==", "dependencies": { - "@microsoft/applicationinsights-channel-js": "2.8.11", - "@microsoft/applicationinsights-common": "2.8.11", - "@microsoft/applicationinsights-core-js": "2.8.11", - "@microsoft/applicationinsights-shims": "2.0.2", - "@microsoft/dynamicproto-js": "^1.1.7" + "@microsoft/applicationinsights-channel-js": "3.0.2", + "@microsoft/applicationinsights-common": "3.0.2", + "@microsoft/applicationinsights-core-js": "3.0.2", + "@microsoft/applicationinsights-shims": "3.0.1", + "@microsoft/dynamicproto-js": "^2.0.2", + "@nevware21/ts-async": ">= 0.2.4 < 2.x", + "@nevware21/ts-utils": ">= 0.9.5 < 2.x" }, "peerDependencies": { "tslib": "*" } }, "node_modules/@microsoft/applicationinsights-web-basic/node_modules/@microsoft/applicationinsights-core-js": { - "version": "2.8.11", - "resolved": "https://registry.npmjs.org/@microsoft/applicationinsights-core-js/-/applicationinsights-core-js-2.8.11.tgz", - "integrity": "sha512-6ScXplyb9Zb0K6TQRfqStm20j5lIe/Dslf65ozows6ibDcKkWl2ZdqzFhymVJZz1WRNpSyD4aA8qnqmslIER6g==", + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@microsoft/applicationinsights-core-js/-/applicationinsights-core-js-3.0.2.tgz", + "integrity": "sha512-WQhVhzlRlLDrQzn3OShCW/pL3BW5WC57t0oywSknX3q7lMzI3jDg7Ihh0iuIcNTzGCTbDkuqr4d6IjEDWIMtJQ==", "dependencies": { - "@microsoft/applicationinsights-shims": "2.0.2", - "@microsoft/dynamicproto-js": "^1.1.7" + "@microsoft/applicationinsights-shims": "3.0.1", + "@microsoft/dynamicproto-js": "^2.0.2", + "@nevware21/ts-async": ">= 0.2.4 < 2.x", + "@nevware21/ts-utils": ">= 0.9.5 < 2.x" }, "peerDependencies": { "tslib": "*" } }, + "node_modules/@microsoft/applicationinsights-web-basic/node_modules/@microsoft/applicationinsights-shims": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@microsoft/applicationinsights-shims/-/applicationinsights-shims-3.0.1.tgz", + "integrity": "sha512-DKwboF47H1nb33rSUfjqI6ryX29v+2QWcTrRvcQDA32AZr5Ilkr7whOOSsD1aBzwqX0RJEIP1Z81jfE3NBm/Lg==", + "dependencies": { + "@nevware21/ts-utils": ">= 0.9.4 < 2.x" + } + }, + "node_modules/@microsoft/applicationinsights-web-basic/node_modules/@microsoft/dynamicproto-js": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@microsoft/dynamicproto-js/-/dynamicproto-js-2.0.2.tgz", + "integrity": "sha512-MB8trWaFREpmb037k/d0bB7T2BP7Ai24w1e1tbz3ASLB0/lwphsq3Nq8S9I5AsI5vs4zAQT+SB5nC5/dLYTiOg==", + "dependencies": { + "@nevware21/ts-utils": ">= 0.9.4 < 2.x" + } + }, "node_modules/@microsoft/applicationinsights-web-snippet": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/@microsoft/applicationinsights-web-snippet/-/applicationinsights-web-snippet-1.0.1.tgz", @@ -719,6 +1491,28 @@ "resolved": "https://registry.npmjs.org/@microsoft/dynamicproto-js/-/dynamicproto-js-1.1.9.tgz", "integrity": "sha512-n1VPsljTSkthsAFYdiWfC+DKzK2WwcRp83Y1YAqdX552BstvsDjft9YXppjUzp11BPsapDoO1LDgrDB0XVsfNQ==" }, + "node_modules/@nevware21/ts-async": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@nevware21/ts-async/-/ts-async-0.3.0.tgz", + "integrity": "sha512-ZUcgUH12LN/F6nzN0cYd0F/rJaMLmXr0EHVTyYfaYmK55bdwE4338uue4UiVoRqHVqNW4KDUrJc49iGogHKeWA==", + "dependencies": { + "@nevware21/ts-utils": ">= 0.10.0 < 2.x" + } + }, + "node_modules/@nevware21/ts-utils": { + "version": "0.10.1", + "resolved": "https://registry.npmjs.org/@nevware21/ts-utils/-/ts-utils-0.10.1.tgz", + "integrity": "sha512-pMny25NnF2/MJwdqC3Iyjm2pGIXNxni4AROpcqDeWa+td9JMUY4bUS9uU9XW+BoBRqTLUL+WURF9SOd/6OQzRg==" + }, + "node_modules/@nicolo-ribaudo/semver-v6": { + "version": "6.3.3", + "resolved": "https://registry.npmjs.org/@nicolo-ribaudo/semver-v6/-/semver-v6-6.3.3.tgz", + "integrity": "sha512-3Yc1fUTs69MG/uZbJlLSI3JISMn2UV2rg+1D/vROUqZyh3l6iYHCs7GMp+M40ZD7yOdDbYjJcU1oTJhrc+dGKg==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -763,11 +1557,11 @@ } }, "node_modules/@opentelemetry/core": { - "version": "1.11.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-1.11.0.tgz", - "integrity": "sha512-aP1wHSb+YfU0pM63UAkizYPuS4lZxzavHHw5KJfFNN2oWQ79HSm6JR3CzwFKHwKhSzHN8RE9fgP1IdVJ8zmo1w==", + "version": "1.15.2", + "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-1.15.2.tgz", + "integrity": "sha512-+gBv15ta96WqkHZaPpcDHiaz0utiiHZVfm2YOYSqFGrUaJpPkMoSuLBB58YFQGi6Rsb9EHos84X6X5+9JspmLw==", "dependencies": { - "@opentelemetry/semantic-conventions": "1.11.0" + "@opentelemetry/semantic-conventions": "1.15.2" }, "engines": { "node": ">=14" @@ -776,13 +1570,31 @@ "@opentelemetry/api": ">=1.0.0 <1.5.0" } }, + "node_modules/@opentelemetry/instrumentation": { + "version": "0.41.2", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation/-/instrumentation-0.41.2.tgz", + "integrity": "sha512-rxU72E0pKNH6ae2w5+xgVYZLzc5mlxAbGzF4shxMVK8YC2QQsfN38B2GPbj0jvrKWWNUElfclQ+YTykkNg/grw==", + "dependencies": { + "@types/shimmer": "^1.0.2", + "import-in-the-middle": "1.4.2", + "require-in-the-middle": "^7.1.1", + "semver": "^7.5.1", + "shimmer": "^1.2.1" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, "node_modules/@opentelemetry/resources": { - "version": "1.11.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-1.11.0.tgz", - "integrity": "sha512-y0z2YJTqk0ag+hGT4EXbxH/qPhDe8PfwltYb4tXIEsozgEFfut/bqW7H7pDvylmCjBRMG4NjtLp57V1Ev++brA==", + "version": "1.15.2", + "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-1.15.2.tgz", + "integrity": "sha512-xmMRLenT9CXmm5HMbzpZ1hWhaUowQf8UB4jMjFlAxx1QzQcsD3KFNAVX/CAWzFPtllTyTplrA4JrQ7sCH3qmYw==", "dependencies": { - "@opentelemetry/core": "1.11.0", - "@opentelemetry/semantic-conventions": "1.11.0" + "@opentelemetry/core": "1.15.2", + "@opentelemetry/semantic-conventions": "1.15.2" }, "engines": { "node": ">=14" @@ -792,13 +1604,13 @@ } }, "node_modules/@opentelemetry/sdk-trace-base": { - "version": "1.11.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-base/-/sdk-trace-base-1.11.0.tgz", - "integrity": "sha512-DV8e5/Qo42V8FMBlQ0Y0Liv6Hl/Pp5bAZ73s7r1euX8w4bpRes1B7ACiA4yujADbWMJxBgSo4fGbi4yjmTMG2A==", + "version": "1.15.2", + "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-base/-/sdk-trace-base-1.15.2.tgz", + "integrity": "sha512-BEaxGZbWtvnSPchV98qqqqa96AOcb41pjgvhfzDij10tkBhIu9m0Jd6tZ1tJB5ZHfHbTffqYVYE0AOGobec/EQ==", "dependencies": { - "@opentelemetry/core": "1.11.0", - "@opentelemetry/resources": "1.11.0", - "@opentelemetry/semantic-conventions": "1.11.0" + "@opentelemetry/core": "1.15.2", + "@opentelemetry/resources": "1.15.2", + "@opentelemetry/semantic-conventions": "1.15.2" }, "engines": { "node": ">=14" @@ -808,9 +1620,20 @@ } }, "node_modules/@opentelemetry/semantic-conventions": { - "version": "1.11.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.11.0.tgz", - "integrity": "sha512-fG4D0AktoHyHwGhFGv+PzKrZjxbKJfckJauTJdq2A+ej5cTazmNYjJVAODXXkYyrsI10muMl+B1iO2q1R6Lp+w==", + "version": "1.15.2", + "resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.15.2.tgz", + "integrity": "sha512-CjbOKwk2s+3xPIMcd5UNYQzsf+v94RczbdNix9/kQh38WiQkM90sUOi3if8eyHFgiBjBjhwXrA7W3ydiSQP9mw==", + "engines": { + "node": ">=14" + } + }, + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "dev": true, + "license": "MIT", + "optional": true, "engines": { "node": ">=14" } @@ -821,48 +1644,65 @@ "integrity": "sha512-a5Sab1C4/icpTZVzZc5Ghpz88yQtGOyNqYXcZgOssB2uuAr+wF/MvN6bgtW32q7HHrvBki+BsZ0OuNv6EV3K9g==", "dev": true }, + "node_modules/@rtsao/scc": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@rtsao/scc/-/scc-1.1.0.tgz", + "integrity": "sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==", + "dev": true, + "license": "MIT" + }, "node_modules/@sindresorhus/is": { "version": "0.7.0", "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-0.7.0.tgz", "integrity": "sha512-ONhaKPIufzzrlNbqtWFFd+jlnemX6lJAgq9ZeiZtS7I1PIf/la7CW4m83rTXRnVnsMbW2k56pGYu7AUFJD9Pow==", "dev": true, + "license": "MIT", "engines": { "node": ">=4" } }, "node_modules/@sinonjs/commons": { - "version": "1.8.3", - "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-1.8.3.tgz", - "integrity": "sha512-xkNcLAn/wZaX14RPlwizcKicDk9G3F8m2nU3L7Ukm5zBgTwiT0wsoFAHx9Jq56fJA1z/7uKGtCRu16sOUCLIHQ==", + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.1.tgz", + "integrity": "sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==", "dev": true, "dependencies": { "type-detect": "4.0.8" } }, "node_modules/@sinonjs/fake-timers": { - "version": "9.1.1", - "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-9.1.1.tgz", - "integrity": "sha512-Wp5vwlZ0lOqpSYGKqr53INws9HLkt6JDc/pDZcPf7bchQnrXJMXPns8CXx0hFikMSGSWfvtvvpb2gtMVfkWagA==", + "version": "11.2.2", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-11.2.2.tgz", + "integrity": "sha512-G2piCSxQ7oWOxwGSAyFHfPIsyeJGXYtc6mFbnFA+kRXkiEnTl8c/8jul2S329iFBnDI9HGoeWWAZvuvOkZccgw==", "dev": true, "dependencies": { - "@sinonjs/commons": "^1.7.0" + "@sinonjs/commons": "^3.0.0" } }, "node_modules/@sinonjs/samsam": { - "version": "6.1.1", - "resolved": "https://registry.npmjs.org/@sinonjs/samsam/-/samsam-6.1.1.tgz", - "integrity": "sha512-cZ7rKJTLiE7u7Wi/v9Hc2fs3Ucc3jrWeMgPHbbTCeVAB2S0wOBbYlkJVeNSL04i7fdhT8wIbDq1zhC/PXTD2SA==", + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@sinonjs/samsam/-/samsam-8.0.0.tgz", + "integrity": "sha512-Bp8KUVlLp8ibJZrnvq2foVhP0IVX2CIprMJPK0vqGqgrDa0OHVKeZyBykqskkrdxV6yKBPmGasO8LVjAKR3Gew==", "dev": true, "dependencies": { - "@sinonjs/commons": "^1.6.0", + "@sinonjs/commons": "^2.0.0", "lodash.get": "^4.4.2", "type-detect": "^4.0.8" } }, + "node_modules/@sinonjs/samsam/node_modules/@sinonjs/commons": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-2.0.0.tgz", + "integrity": "sha512-uLa0j859mMrg2slwQYdO/AkrOfmH+X6LTVmNTS9CqexuE2IvVORIkSpJLqePAbEnKJ77aMmCwr1NUZ57120Xcg==", + "dev": true, + "dependencies": { + "type-detect": "4.0.8" + } + }, "node_modules/@sinonjs/text-encoding": { - "version": "0.7.1", - "resolved": "https://registry.npmjs.org/@sinonjs/text-encoding/-/text-encoding-0.7.1.tgz", - "integrity": "sha512-+iTbntw2IZPb/anVDbypzfQa+ay64MW0Zo8aJ8gZPWMMK6/OubMVb6lUPMagqjOPnmtauXnFCACVl3O7ogjeqQ==", + "version": "0.7.3", + "resolved": "https://registry.npmjs.org/@sinonjs/text-encoding/-/text-encoding-0.7.3.tgz", + "integrity": "sha512-DE427ROAphMQzU4ENbliGYrBSYPXF+TtLg9S8vzeA+OF4ZKzoDdzfL8sxuMUGS/lgRhM6j1URSk9ghf7Xo1tyA==", "dev": true }, "node_modules/@tootallnate/once": { @@ -938,35 +1778,29 @@ "dev": true }, "node_modules/@types/decompress": { - "version": "4.2.4", - "resolved": "https://registry.npmjs.org/@types/decompress/-/decompress-4.2.4.tgz", - "integrity": "sha512-/C8kTMRTNiNuWGl5nEyKbPiMv6HA+0RbEXzFhFBEzASM6+oa4tJro9b8nj7eRlOFfuLdzUU+DS/GPDlvvzMOhA==", + "version": "4.2.5", + "resolved": "https://registry.npmjs.org/@types/decompress/-/decompress-4.2.5.tgz", + "integrity": "sha512-LdL+kbcKGs9TzvB/K+OBGzPfDoP6gwwTsykYjodlzUJUUYp/43c1p1jE5YTtz3z4Ml90iruvBXbJ6+kDvb3WSQ==", "dev": true, "dependencies": { "@types/node": "*" } }, - "node_modules/@types/diff-match-patch": { - "version": "1.0.32", - "resolved": "https://registry.npmjs.org/@types/diff-match-patch/-/diff-match-patch-1.0.32.tgz", - "integrity": "sha512-bPYT5ECFiblzsVzyURaNhljBH2Gh1t9LowgUwciMrNAhFewLkHT2H0Mto07Y4/3KCOGZHRQll3CTtQZ0X11D/A==", - "dev": true - }, "node_modules/@types/download": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/@types/download/-/download-8.0.1.tgz", - "integrity": "sha512-t5DjMD6Y1DxjXtEHl7Kt+nQn9rOmVLYD8p4Swrcc5QpgyqyqR2gXTIK6RwwMnNeFJ+ZIiIW789fQKzCrK7AOFA==", + "version": "8.0.3", + "resolved": "https://registry.npmjs.org/@types/download/-/download-8.0.3.tgz", + "integrity": "sha512-IDwXjU7zCtuFVvI0Plnb02TpXyj3RA4YeOKQvEfsjdJeWxZ9hTl6lxeNsU2bLWn0aeAS7fyMl74w/TbdOlS2KQ==", "dev": true, "dependencies": { "@types/decompress": "*", - "@types/got": "^8", + "@types/got": "^9", "@types/node": "*" } }, "node_modules/@types/eslint": { - "version": "8.4.1", - "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.4.1.tgz", - "integrity": "sha512-GE44+DNEyxxh2Kc6ro/VkIj+9ma0pO0bwv9+uHSyBrikYOHr8zYcdPvnBOp1aw8s+CjRvuSx7CyWqRrNFQ59mA==", + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-9.6.1.tgz", + "integrity": "sha512-FXx2pKgId/WyYo2jXw63kk7/+TY7u7AziEJxJAnSFzHlqTAS3Ync6SvgYAN/k4/PQpnnVuzoMuVnByKK2qp0ag==", "dev": true, "dependencies": { "@types/estree": "*", @@ -974,33 +1808,28 @@ } }, "node_modules/@types/eslint-scope": { - "version": "3.7.3", - "resolved": "https://registry.npmjs.org/@types/eslint-scope/-/eslint-scope-3.7.3.tgz", - "integrity": "sha512-PB3ldyrcnAicT35TWPs5IcwKD8S333HMaa2VVv4+wdvebJkjWuW/xESoB8IwRcog8HYVYamb1g/R31Qv5Bx03g==", + "version": "3.7.7", + "resolved": "https://registry.npmjs.org/@types/eslint-scope/-/eslint-scope-3.7.7.tgz", + "integrity": "sha512-MzMFlSLBqNF2gcHWO0G1vP/YQyfvrxZ0bF+u7mzUdZ1/xK4A4sru+nraZz5i3iEIk1l1uyicaDVTB4QbbEkAYg==", "dev": true, "dependencies": { "@types/eslint": "*", "@types/estree": "*" } }, - "node_modules/@types/eslint-visitor-keys": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@types/eslint-visitor-keys/-/eslint-visitor-keys-1.0.0.tgz", - "integrity": "sha512-OCutwjDZ4aFS6PB1UZ988C4YgwlBHJd6wCeQqaLdmadZ/7e+w79+hbMUFC1QXDNCmdyoRfAFdm0RypzwR+Qpag==", - "dev": true - }, "node_modules/@types/estree": { - "version": "0.0.51", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-0.0.51.tgz", - "integrity": "sha512-CuPgU6f3eT/XgKKPqKd/gLZV1Xmvf1a2R5POBOGQa6uv82xpls89HU5zKeVoyR8XzHd1RGNOlQlvUe3CFkjWNQ==", + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", "dev": true }, "node_modules/@types/fs-extra": { - "version": "9.0.13", - "resolved": "https://registry.npmjs.org/@types/fs-extra/-/fs-extra-9.0.13.tgz", - "integrity": "sha512-nEnwB++1u5lVDM2UI4c1+5R+FYaKfaAzS4OococimjVm3nQw3TuzH5UNsocrcTBbhnerblyHj4A49qXbIiZdpA==", + "version": "11.0.4", + "resolved": "https://registry.npmjs.org/@types/fs-extra/-/fs-extra-11.0.4.tgz", + "integrity": "sha512-yTbItCNreRooED33qjunPthRcSjERP1r4MqCZc7wv0u2sUkzTFp45tgUfS5+r7FrZPdmCCNflLhVSP/o+SemsQ==", "dev": true, "dependencies": { + "@types/jsonfile": "*", "@types/node": "*" } }, @@ -1015,18 +1844,57 @@ } }, "node_modules/@types/got": { - "version": "8.3.6", - "resolved": "https://registry.npmjs.org/@types/got/-/got-8.3.6.tgz", - "integrity": "sha512-nvLlj+831dhdm4LR2Ly+HTpdLyBaMynoOr6wpIxS19d/bPeHQxFU5XQ6Gp6ohBpxvCWZM1uHQIC2+ySRH1rGrQ==", + "version": "9.6.12", + "resolved": "https://registry.npmjs.org/@types/got/-/got-9.6.12.tgz", + "integrity": "sha512-X4pj/HGHbXVLqTpKjA2ahI4rV/nNBc9mGO2I/0CgAra+F2dKgMXnENv2SRpemScBzBAI4vMelIVYViQxlSE6xA==", "dev": true, "dependencies": { - "@types/node": "*" + "@types/node": "*", + "@types/tough-cookie": "*", + "form-data": "^2.5.0" + } + }, + "node_modules/@types/got/node_modules/form-data": { + "version": "2.5.5", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.5.5.tgz", + "integrity": "sha512-jqdObeR2rxZZbPSGL+3VckHMYtu+f9//KXBsVny6JSX/pa38Fy+bGjuG8eW/H6USNQWhLi8Num++cU2yOCNz4A==", + "dev": true, + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", + "mime-types": "^2.1.35", + "safe-buffer": "^5.2.1" + }, + "engines": { + "node": ">= 0.12" } }, + "node_modules/@types/got/node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, "node_modules/@types/json-schema": { - "version": "7.0.9", - "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.9.tgz", - "integrity": "sha512-qcUXuemtEu+E5wZSJHNxUXeCZhAfXKQ41D+duX+VYPde7xyEVZci+/oXKJL13tnRs9lR2pr4fod59GT6/X1/yQ==", + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", "dev": true }, "node_modules/@types/json5": { @@ -1035,18 +1903,21 @@ "integrity": "sha1-7ihweulOEdK4J7y+UnC86n8+ce4=", "dev": true }, + "node_modules/@types/jsonfile": { + "version": "6.1.4", + "resolved": "https://registry.npmjs.org/@types/jsonfile/-/jsonfile-6.1.4.tgz", + "integrity": "sha512-D5qGUYwjvnNNextdU59/+fI+spnwtTFmyQP0h+PfIOSkNfpU6AOICUOkm4i0OnSk+NyjdPJrxCDro0sJsWlRpQ==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/lodash": { "version": "4.14.181", "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.181.tgz", "integrity": "sha512-n3tyKthHJbkiWhDZs3DkhkCzt2MexYHXlX0td5iMplyfwketaOeKboEVBqzceH7juqvEg3q5oUoBFxSLu7zFag==", "dev": true }, - "node_modules/@types/md5": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/@types/md5/-/md5-2.3.2.tgz", - "integrity": "sha512-v+JFDu96+UYJ3/UWzB0mEglIS//MZXgRaJ4ubUPwOM0gvLc/kcQ3TWNYwENEK7/EcXGQVrW8h/XqednSjBd/Og==", - "dev": true - }, "node_modules/@types/minimatch": { "version": "3.0.5", "resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-3.0.5.tgz", @@ -1059,27 +1930,27 @@ "integrity": "sha512-QCWHkbMv4Y5U9oW10Uxbr45qMMSzl4OzijsozynUAgx3kEHUdXB00udx2dWDQ7f2TU2a2uuiFaRZjCe3unPpeg==", "dev": true }, - "node_modules/@types/nock": { - "version": "10.0.3", - "resolved": "https://registry.npmjs.org/@types/nock/-/nock-10.0.3.tgz", - "integrity": "sha512-OthuN+2FuzfZO3yONJ/QVjKmLEuRagS9TV9lEId+WHL9KhftYG+/2z+pxlr0UgVVXSpVD8woie/3fzQn8ft/Ow==", + "node_modules/@types/node": { + "version": "22.19.3", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.3.tgz", + "integrity": "sha512-1N9SBnWYOJTrNZCdh/yJE+t910Y128BoyY+zBLWhL3r0TYzlTmFdXrPwHL9DyFZmlEXNQQolTZh3KHV31QDhyA==", "dev": true, + "license": "MIT", "dependencies": { - "@types/node": "*" + "undici-types": "~6.21.0" } }, - "node_modules/@types/node": { - "version": "16.18.25", - "resolved": "https://registry.npmjs.org/@types/node/-/node-16.18.25.tgz", - "integrity": "sha512-rUDO6s9Q/El1R1I21HG4qw/LstTHCPO/oQNAwI/4b2f9EWvMnqt4d3HJwPMawfZ3UvodB8516Yg+VAq54YM+eA==", - "dev": true - }, "node_modules/@types/semver": { "version": "5.5.0", "resolved": "https://registry.npmjs.org/@types/semver/-/semver-5.5.0.tgz", "integrity": "sha512-41qEJgBH/TWgo5NFSvBCJ1qkoi3Q6ONSF2avrHq1LVEZfYpdHmj0y9SuTK+u9ZhG1sYQKBL1AWXKyLWP4RaUoQ==", "dev": true }, + "node_modules/@types/shimmer": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@types/shimmer/-/shimmer-1.0.2.tgz", + "integrity": "sha512-dKkr1bTxbEsFlh2ARpKzcaAmsYixqt9UyCdoEZk8rHyE4iQYcDCyvSjDSf7JUWJHlJiTtbIoQjxKh6ViywqDAg==" + }, "node_modules/@types/shortid": { "version": "0.0.29", "resolved": "https://registry.npmjs.org/@types/shortid/-/shortid-0.0.29.tgz", @@ -1087,9 +1958,9 @@ "dev": true }, "node_modules/@types/sinon": { - "version": "10.0.11", - "resolved": "https://registry.npmjs.org/@types/sinon/-/sinon-10.0.11.tgz", - "integrity": "sha512-dmZsHlBsKUtBpHriNjlK0ndlvEh8dcb9uV9Afsbt89QIyydpC7NcR+nWlAhASfy3GHnxTl4FX/aKE7XZUt/B4g==", + "version": "17.0.3", + "resolved": "https://registry.npmjs.org/@types/sinon/-/sinon-17.0.3.tgz", + "integrity": "sha512-j3uovdn8ewky9kRBG19bOwaZbexJu/XjtkHyjvUgt4xfPFz18dcORIMqnYh66Fx3Powhcr85NT5+er3+oViapw==", "dev": true, "dependencies": { "@types/sinonjs__fake-timers": "*" @@ -1113,17 +1984,18 @@ "integrity": "sha1-EHPEvIJHVK49EM+riKsCN7qWTk0=", "dev": true }, - "node_modules/@types/uuid": { - "version": "8.3.4", - "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-8.3.4.tgz", - "integrity": "sha512-c/I8ZRb51j+pYGAu5CrFMRxqZ2ke4y2grEBO5AUjgSkSk+qT2Ea+OdWElz/OiMf5MNpn2b17kuVBwZLQJXzihw==", + "node_modules/@types/tough-cookie": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/@types/tough-cookie/-/tough-cookie-4.0.3.tgz", + "integrity": "sha512-THo502dA5PzG/sfQH+42Lw3fvmYkceefOspdCwpHRul8ik2Jv1K8I5OZz1AT3/rs46kwgMCe9bSBmDLYkkOMGg==", "dev": true }, "node_modules/@types/vscode": { - "version": "1.75.1", - "resolved": "https://registry.npmjs.org/@types/vscode/-/vscode-1.75.1.tgz", - "integrity": "sha512-emg7wdsTFzdi+elvoyoA+Q8keEautdQHyY5LNmHVM4PTpY8JgOTVADrGVyXGepJ6dVW2OS5/xnLUWh+nZxvdiA==", - "dev": true + "version": "1.100.0", + "resolved": "https://registry.npmjs.org/@types/vscode/-/vscode-1.100.0.tgz", + "integrity": "sha512-4uNyvzHoraXEeCamR3+fzcBlh7Afs4Ifjs4epINyUX/jvdk0uzLnwiDY35UKDKnkCHP5Nu3dljl2H8lR6s+rQw==", + "dev": true, + "license": "MIT" }, "node_modules/@types/which": { "version": "2.0.1", @@ -1147,28 +2019,33 @@ } }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "3.10.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-3.10.1.tgz", - "integrity": "sha512-PQg0emRtzZFWq6PxBcdxRH3QIQiyFO3WCVpRL3fgj5oQS3CDs3AeAKfv4DxNhzn8ITdNJGJ4D3Qw8eAJf3lXeQ==", - "dev": true, - "dependencies": { - "@typescript-eslint/experimental-utils": "3.10.1", - "debug": "^4.1.1", - "functional-red-black-tree": "^1.0.1", - "regexpp": "^3.0.0", - "semver": "^7.3.2", - "tsutils": "^3.17.1" + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-6.21.0.tgz", + "integrity": "sha512-oy9+hTPCUFpngkEZUSzbf9MxI65wbKFoQYsgPdILTfbUldp5ovUuphZVe4i30emU9M/kP+T64Di0mxl7dSw3MA==", + "dev": true, + "dependencies": { + "@eslint-community/regexpp": "^4.5.1", + "@typescript-eslint/scope-manager": "6.21.0", + "@typescript-eslint/type-utils": "6.21.0", + "@typescript-eslint/utils": "6.21.0", + "@typescript-eslint/visitor-keys": "6.21.0", + "debug": "^4.3.4", + "graphemer": "^1.4.0", + "ignore": "^5.2.4", + "natural-compare": "^1.4.0", + "semver": "^7.5.4", + "ts-api-utils": "^1.0.1" }, "engines": { - "node": "^10.12.0 || >=12.0.0" + "node": "^16.0.0 || >=18.0.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "@typescript-eslint/parser": "^3.0.0", - "eslint": "^5.0.0 || ^6.0.0 || ^7.0.0" + "@typescript-eslint/parser": "^6.0.0 || ^6.0.0-alpha", + "eslint": "^7.0.0 || ^8.0.0" }, "peerDependenciesMeta": { "typescript": { @@ -1177,9 +2054,9 @@ } }, "node_modules/@typescript-eslint/eslint-plugin/node_modules/debug": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.2.tgz", - "integrity": "sha512-mOp8wKcvj7XxC78zLgw/ZA+6TSgkoE2C/ienthhRD298T7UNwAg9diBpLRxC0mOezLl4B0xV7M0cCO6P/O0Xhw==", + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.6.tgz", + "integrity": "sha512-O/09Bd4Z1fBrU4VzkhFqVgpPzaGbw6Sm9FEkBT1A/YBXQFGuuSxa1dN2nxgxS34JmKXqYx8CZAwEVoJFImUXIg==", "dev": true, "dependencies": { "ms": "2.1.2" @@ -1193,79 +2070,119 @@ } } }, - "node_modules/@typescript-eslint/eslint-plugin/node_modules/semver": { - "version": "7.3.5", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.5.tgz", - "integrity": "sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==", + "node_modules/@typescript-eslint/parser": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-6.21.0.tgz", + "integrity": "sha512-tbsV1jPne5CkFQCgPBcDOt30ItF7aJoZL997JSF7MhGQqOeT3svWRYxiqlfA5RUdlHN6Fi+EI9bxqbdyAUZjYQ==", "dev": true, "dependencies": { - "lru-cache": "^6.0.0" + "@typescript-eslint/scope-manager": "6.21.0", + "@typescript-eslint/types": "6.21.0", + "@typescript-eslint/typescript-estree": "6.21.0", + "@typescript-eslint/visitor-keys": "6.21.0", + "debug": "^4.3.4" }, - "bin": { - "semver": "bin/semver.js" + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/parser/node_modules/debug": { + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.6.tgz", + "integrity": "sha512-O/09Bd4Z1fBrU4VzkhFqVgpPzaGbw6Sm9FEkBT1A/YBXQFGuuSxa1dN2nxgxS34JmKXqYx8CZAwEVoJFImUXIg==", + "dev": true, + "dependencies": { + "ms": "2.1.2" }, "engines": { - "node": ">=10" + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } } }, - "node_modules/@typescript-eslint/experimental-utils": { - "version": "3.10.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/experimental-utils/-/experimental-utils-3.10.1.tgz", - "integrity": "sha512-DewqIgscDzmAfd5nOGe4zm6Bl7PKtMG2Ad0KG8CUZAHlXfAKTF9Ol5PXhiMh39yRL2ChRH1cuuUGOcVyyrhQIw==", + "node_modules/@typescript-eslint/scope-manager": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-6.21.0.tgz", + "integrity": "sha512-OwLUIWZJry80O99zvqXVEioyniJMa+d2GrqpUTqi5/v5D5rOrppJVBPa0yKCblcigC0/aYAzxxqQ1B+DS2RYsg==", "dev": true, "dependencies": { - "@types/json-schema": "^7.0.3", - "@typescript-eslint/types": "3.10.1", - "@typescript-eslint/typescript-estree": "3.10.1", - "eslint-scope": "^5.0.0", - "eslint-utils": "^2.0.0" + "@typescript-eslint/types": "6.21.0", + "@typescript-eslint/visitor-keys": "6.21.0" }, "engines": { - "node": "^10.12.0 || >=12.0.0" + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-6.21.0.tgz", + "integrity": "sha512-rZQI7wHfao8qMX3Rd3xqeYSMCL3SoiSQLBATSiVKARdFGCYSRvmViieZjqc58jKgs8Y8i9YvVVhRbHSTA4VBag==", + "dev": true, + "dependencies": { + "@typescript-eslint/typescript-estree": "6.21.0", + "@typescript-eslint/utils": "6.21.0", + "debug": "^4.3.4", + "ts-api-utils": "^1.0.1" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "eslint": "*" + "eslint": "^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } } }, - "node_modules/@typescript-eslint/parser": { - "version": "3.10.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-3.10.1.tgz", - "integrity": "sha512-Ug1RcWcrJP02hmtaXVS3axPPTTPnZjupqhgj+NnZ6BCkwSImWk/283347+x9wN+lqOdK9Eo3vsyiyDHgsmiEJw==", + "node_modules/@typescript-eslint/type-utils/node_modules/debug": { + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.6.tgz", + "integrity": "sha512-O/09Bd4Z1fBrU4VzkhFqVgpPzaGbw6Sm9FEkBT1A/YBXQFGuuSxa1dN2nxgxS34JmKXqYx8CZAwEVoJFImUXIg==", "dev": true, "dependencies": { - "@types/eslint-visitor-keys": "^1.0.0", - "@typescript-eslint/experimental-utils": "3.10.1", - "@typescript-eslint/types": "3.10.1", - "@typescript-eslint/typescript-estree": "3.10.1", - "eslint-visitor-keys": "^1.1.0" + "ms": "2.1.2" }, "engines": { - "node": "^10.12.0 || >=12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^5.0.0 || ^6.0.0 || ^7.0.0" + "node": ">=6.0" }, "peerDependenciesMeta": { - "typescript": { + "supports-color": { "optional": true } } }, "node_modules/@typescript-eslint/types": { - "version": "3.10.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-3.10.1.tgz", - "integrity": "sha512-+3+FCUJIahE9q0lDi1WleYzjCwJs5hIsbugIgnbB+dSCYUxl8L6PwmsyOPFZde2hc1DlTo/xnkOgiTLSyAbHiQ==", + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-6.21.0.tgz", + "integrity": "sha512-1kFmZ1rOm5epu9NZEZm1kckCDGj5UJEf7P1kliH4LKu/RkwpsfqqGmY2OOcUs18lSlQBKLDYBOGxRVtrMN5lpg==", "dev": true, "engines": { - "node": "^8.10.0 || ^10.13.0 || >=11.10.1" + "node": "^16.0.0 || >=18.0.0" }, "funding": { "type": "opencollective", @@ -1273,22 +2190,22 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "3.10.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-3.10.1.tgz", - "integrity": "sha512-QbcXOuq6WYvnB3XPsZpIwztBoquEYLXh2MtwVU+kO8jgYCiv4G5xrSP/1wg4tkvrEE+esZVquIPX/dxPlePk1w==", + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-6.21.0.tgz", + "integrity": "sha512-6npJTkZcO+y2/kr+z0hc4HwNfrrP4kNYh57ek7yCNlrBjWQ1Y0OS7jiZTkgumrvkX5HkEKXFZkkdFNkaW2wmUQ==", "dev": true, "dependencies": { - "@typescript-eslint/types": "3.10.1", - "@typescript-eslint/visitor-keys": "3.10.1", - "debug": "^4.1.1", - "glob": "^7.1.6", - "is-glob": "^4.0.1", - "lodash": "^4.17.15", - "semver": "^7.3.2", - "tsutils": "^3.17.1" + "@typescript-eslint/types": "6.21.0", + "@typescript-eslint/visitor-keys": "6.21.0", + "debug": "^4.3.4", + "globby": "^11.1.0", + "is-glob": "^4.0.3", + "minimatch": "9.0.3", + "semver": "^7.5.4", + "ts-api-utils": "^1.0.1" }, "engines": { - "node": "^10.12.0 || >=12.0.0" + "node": "^16.0.0 || >=18.0.0" }, "funding": { "type": "opencollective", @@ -1300,10 +2217,19 @@ } } }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.3.tgz", + "integrity": "sha512-MCV/fYJEbqx68aE58kv2cA/kiky1G8vux3OR6/jbS+jIMe/6fJWa0DTzJU7dqijOWYwHi1t29FlfYI9uytqlpA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0" + } + }, "node_modules/@typescript-eslint/typescript-estree/node_modules/debug": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.2.tgz", - "integrity": "sha512-mOp8wKcvj7XxC78zLgw/ZA+6TSgkoE2C/ienthhRD298T7UNwAg9diBpLRxC0mOezLl4B0xV7M0cCO6P/O0Xhw==", + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.6.tgz", + "integrity": "sha512-O/09Bd4Z1fBrU4VzkhFqVgpPzaGbw6Sm9FEkBT1A/YBXQFGuuSxa1dN2nxgxS34JmKXqYx8CZAwEVoJFImUXIg==", "dev": true, "dependencies": { "ms": "2.1.2" @@ -1317,108 +2243,119 @@ } } }, - "node_modules/@typescript-eslint/typescript-estree/node_modules/semver": { - "version": "7.3.5", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.5.tgz", - "integrity": "sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==", + "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz", + "integrity": "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==", "dev": true, "dependencies": { - "lru-cache": "^6.0.0" - }, - "bin": { - "semver": "bin/semver.js" + "brace-expansion": "^2.0.1" }, "engines": { - "node": ">=10" + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/@typescript-eslint/visitor-keys": { - "version": "3.10.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-3.10.1.tgz", - "integrity": "sha512-9JgC82AaQeglebjZMgYR5wgmfUdUc+EitGUUMW8u2nDckaeimzW+VsoLV6FoimPv2id3VQzfjwBxEMVz08ameQ==", + "node_modules/@typescript-eslint/utils": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-6.21.0.tgz", + "integrity": "sha512-NfWVaC8HP9T8cbKQxHcsJBY5YE1O33+jpMwN45qzWWaPDZgLIbo12toGMWnmhvCpd3sIxkpDw3Wv1B3dYrbDQQ==", "dev": true, "dependencies": { - "eslint-visitor-keys": "^1.1.0" + "@eslint-community/eslint-utils": "^4.4.0", + "@types/json-schema": "^7.0.12", + "@types/semver": "^7.5.0", + "@typescript-eslint/scope-manager": "6.21.0", + "@typescript-eslint/types": "6.21.0", + "@typescript-eslint/typescript-estree": "6.21.0", + "semver": "^7.5.4" }, "engines": { - "node": "^8.10.0 || ^10.13.0 || >=11.10.1" + "node": "^16.0.0 || >=18.0.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^7.0.0 || ^8.0.0" } }, - "node_modules/@ungap/promise-all-settled": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@ungap/promise-all-settled/-/promise-all-settled-1.1.2.tgz", - "integrity": "sha512-sL/cEvJWAnClXw0wHk85/2L0G6Sj8UB0Ctc1TEMbKSsmpRosqhwj9gWgFRZSrBr2f9tiXISwNhCPmlfqUqyb9Q==", + "node_modules/@typescript-eslint/utils/node_modules/@types/semver": { + "version": "7.5.8", + "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.8.tgz", + "integrity": "sha512-I8EUhyrgfLrcTkzV3TSsGyl1tSuPrEDzr0yd5m90UgNxQkyDXULk3b6MlQqTCpZpNtWe1K0hzclnZkTcLBe2UQ==", "dev": true }, - "node_modules/@vscode/extension-telemetry": { - "version": "0.7.7", - "resolved": "https://registry.npmjs.org/@vscode/extension-telemetry/-/extension-telemetry-0.7.7.tgz", - "integrity": "sha512-uW508BPjkWDBOKvvvSym3ZmGb7kHIiWaAfB/1PHzLz2x9TrC33CfjmFEI+CywIL/jBv4bqZxxjN4tfefB61F+g==", + "node_modules/@typescript-eslint/visitor-keys": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-6.21.0.tgz", + "integrity": "sha512-JJtkDduxLi9bivAB+cYOVMtbkqdPOhZ+ZI5LC47MIRrDV4Yn2o+ZnW10Nkmr28xRpSpdJ6Sm42Hjf2+REYXm0A==", + "dev": true, "dependencies": { - "@microsoft/1ds-core-js": "^3.2.9", - "@microsoft/1ds-post-js": "^3.2.9", - "@microsoft/applicationinsights-web-basic": "^2.8.11", - "applicationinsights": "2.5.0" + "@typescript-eslint/types": "6.21.0", + "eslint-visitor-keys": "^3.4.1" }, "engines": { - "vscode": "^1.75.0" + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" } }, - "node_modules/@vscode/jupyter-lsp-middleware": { - "version": "0.2.50", - "resolved": "https://registry.npmjs.org/@vscode/jupyter-lsp-middleware/-/jupyter-lsp-middleware-0.2.50.tgz", - "integrity": "sha512-oOEpRZOJdKjByRMkUDVdGlQDiEO4/Mjr88u5zqktaS/4h0NtX8Hk6+HNQwENp4ur3Dpu47gD8wOTCrkOWzbHlA==", + "node_modules/@ungap/structured-clone": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.0.tgz", + "integrity": "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==", + "dev": true, + "license": "ISC" + }, + "node_modules/@vscode/extension-telemetry": { + "version": "0.8.4", + "resolved": "https://registry.npmjs.org/@vscode/extension-telemetry/-/extension-telemetry-0.8.4.tgz", + "integrity": "sha512-UqM9+KZDDK3MyoHTsg6XNM+XO6pweQxzCpqJz33BoBEYAGsbBviRYcVpJglgay2oReuDD2pOI1Nio3BKNDLhWA==", "dependencies": { - "@vscode/lsp-notebook-concat": "^0.1.16", - "fast-myers-diff": "^3.0.1", - "sha.js": "^2.4.11", - "vscode-languageclient": "^8.0.2-next.4", - "vscode-languageserver-protocol": "^3.17.2-next.5", - "vscode-uri": "^3.0.2" + "@microsoft/1ds-core-js": "^3.2.13", + "@microsoft/1ds-post-js": "^3.2.13", + "@microsoft/applicationinsights-web-basic": "^3.0.2", + "applicationinsights": "^2.7.1" }, "engines": { - "vscode": "^1.67.0-insider" - } - }, - "node_modules/@vscode/lsp-notebook-concat": { - "version": "0.1.16", - "resolved": "https://registry.npmjs.org/@vscode/lsp-notebook-concat/-/lsp-notebook-concat-0.1.16.tgz", - "integrity": "sha512-jN2ut22GR/xelxHx2W9U+uZoylHGCezsNmsMYn20LgVHTcJMGL+4bL5PJeh63yo6P5XjAPtoeeymvp5EafJV+w==", - "dependencies": { - "object-hash": "^3.0.0", - "vscode-languageserver-protocol": "^3.17.2-next.5", - "vscode-uri": "^3.0.2" + "vscode": "^1.75.0" } }, "node_modules/@vscode/test-electron": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/@vscode/test-electron/-/test-electron-2.1.3.tgz", - "integrity": "sha512-ps/yJ/9ToUZtR1dHfWi1mDXtep1VoyyrmGKC3UnIbScToRQvbUjyy1VMqnMEW3EpMmC3g7+pyThIPtPyCLHyow==", + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/@vscode/test-electron/-/test-electron-2.3.8.tgz", + "integrity": "sha512-b4aZZsBKtMGdDljAsOPObnAi7+VWIaYl3ylCz1jTs+oV6BZ4TNHcVNC3xUn0azPeszBmwSBDQYfFESIaUQnrOg==", "dev": true, "dependencies": { "http-proxy-agent": "^4.0.1", "https-proxy-agent": "^5.0.0", - "rimraf": "^3.0.2", - "unzipper": "^0.10.11" + "jszip": "^3.10.1", + "semver": "^7.5.2" }, "engines": { - "node": ">=8.9.3" + "node": ">=16" } }, "node_modules/@vscode/vsce": { - "version": "2.19.0", - "resolved": "https://registry.npmjs.org/@vscode/vsce/-/vsce-2.19.0.tgz", - "integrity": "sha512-dAlILxC5ggOutcvJY24jxz913wimGiUrHaPkk16Gm9/PGFbz1YezWtrXsTKUtJws4fIlpX2UIlVlVESWq8lkfQ==", + "version": "2.27.0", + "resolved": "https://registry.npmjs.org/@vscode/vsce/-/vsce-2.27.0.tgz", + "integrity": "sha512-FFUMBVSyyjjJpWszwqk7d4U3YllY8FdWslbUDMRki1x4ZjA3Z0hmRMfypWrjP9sptbSR9nyPFU4uqjhy2qRB/w==", "dev": true, "dependencies": { - "azure-devops-node-api": "^11.0.1", + "@azure/identity": "^4.1.0", + "@vscode/vsce-sign": "^2.0.0", + "azure-devops-node-api": "^12.5.0", "chalk": "^2.4.2", "cheerio": "^1.0.0-rc.9", - "commander": "^6.1.0", + "cockatiel": "^3.1.2", + "commander": "^6.2.1", + "form-data": "^4.0.0", "glob": "^7.0.6", "hosted-git-info": "^4.0.2", "jsonc-parser": "^3.2.0", @@ -1428,7 +2365,7 @@ "minimatch": "^3.0.3", "parse-semver": "^1.1.1", "read": "^1.0.7", - "semver": "^5.1.0", + "semver": "^7.5.2", "tmp": "^0.2.1", "typed-rest-client": "^1.8.4", "url-join": "^4.0.1", @@ -1440,12 +2377,147 @@ "vsce": "vsce" }, "engines": { - "node": ">= 14" + "node": ">= 16" }, "optionalDependencies": { "keytar": "^7.7.0" } }, + "node_modules/@vscode/vsce-sign": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@vscode/vsce-sign/-/vsce-sign-2.0.4.tgz", + "integrity": "sha512-0uL32egStKYfy60IqnynAChMTbL0oqpqk0Ew0YHiIb+fayuGZWADuIPHWUcY1GCnAA+VgchOPDMxnc2R3XGWEA==", + "dev": true, + "hasInstallScript": true, + "optionalDependencies": { + "@vscode/vsce-sign-alpine-arm64": "2.0.2", + "@vscode/vsce-sign-alpine-x64": "2.0.2", + "@vscode/vsce-sign-darwin-arm64": "2.0.2", + "@vscode/vsce-sign-darwin-x64": "2.0.2", + "@vscode/vsce-sign-linux-arm": "2.0.2", + "@vscode/vsce-sign-linux-arm64": "2.0.2", + "@vscode/vsce-sign-linux-x64": "2.0.2", + "@vscode/vsce-sign-win32-arm64": "2.0.2", + "@vscode/vsce-sign-win32-x64": "2.0.2" + } + }, + "node_modules/@vscode/vsce-sign-alpine-arm64": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@vscode/vsce-sign-alpine-arm64/-/vsce-sign-alpine-arm64-2.0.2.tgz", + "integrity": "sha512-E80YvqhtZCLUv3YAf9+tIbbqoinWLCO/B3j03yQPbjT3ZIHCliKZlsy1peNc4XNZ5uIb87Jn0HWx/ZbPXviuAQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "alpine" + ] + }, + "node_modules/@vscode/vsce-sign-alpine-x64": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@vscode/vsce-sign-alpine-x64/-/vsce-sign-alpine-x64-2.0.2.tgz", + "integrity": "sha512-n1WC15MSMvTaeJ5KjWCzo0nzjydwxLyoHiMJHu1Ov0VWTZiddasmOQHekA47tFRycnt4FsQrlkSCTdgHppn6bw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "alpine" + ] + }, + "node_modules/@vscode/vsce-sign-darwin-arm64": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@vscode/vsce-sign-darwin-arm64/-/vsce-sign-darwin-arm64-2.0.2.tgz", + "integrity": "sha512-rz8F4pMcxPj8fjKAJIfkUT8ycG9CjIp888VY/6pq6cuI2qEzQ0+b5p3xb74CJnBbSC0p2eRVoe+WgNCAxCLtzQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@vscode/vsce-sign-darwin-x64": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@vscode/vsce-sign-darwin-x64/-/vsce-sign-darwin-x64-2.0.2.tgz", + "integrity": "sha512-MCjPrQ5MY/QVoZ6n0D92jcRb7eYvxAujG/AH2yM6lI0BspvJQxp0o9s5oiAM9r32r9tkLpiy5s2icsbwefAQIw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@vscode/vsce-sign-linux-arm": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@vscode/vsce-sign-linux-arm/-/vsce-sign-linux-arm-2.0.2.tgz", + "integrity": "sha512-Fkb5jpbfhZKVw3xwR6t7WYfwKZktVGNXdg1m08uEx1anO0oUPUkoQRsNm4QniL3hmfw0ijg00YA6TrxCRkPVOQ==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@vscode/vsce-sign-linux-arm64": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@vscode/vsce-sign-linux-arm64/-/vsce-sign-linux-arm64-2.0.2.tgz", + "integrity": "sha512-Ybeu7cA6+/koxszsORXX0OJk9N0GgfHq70Wqi4vv2iJCZvBrOWwcIrxKjvFtwyDgdeQzgPheH5nhLVl5eQy7WA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@vscode/vsce-sign-linux-x64": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@vscode/vsce-sign-linux-x64/-/vsce-sign-linux-x64-2.0.2.tgz", + "integrity": "sha512-NsPPFVtLaTlVJKOiTnO8Cl78LZNWy0Q8iAg+LlBiCDEgC12Gt4WXOSs2pmcIjDYzj2kY4NwdeN1mBTaujYZaPg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@vscode/vsce-sign-win32-arm64": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@vscode/vsce-sign-win32-arm64/-/vsce-sign-win32-arm64-2.0.2.tgz", + "integrity": "sha512-wPs848ymZ3Ny+Y1Qlyi7mcT6VSigG89FWQnp2qRYCyMhdJxOpA4lDwxzlpL8fG6xC8GjQjGDkwbkWUcCobvksQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@vscode/vsce-sign-win32-x64": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@vscode/vsce-sign-win32-x64/-/vsce-sign-win32-x64-2.0.2.tgz", + "integrity": "sha512-pAiRN6qSAhDM5SVOIxgx+2xnoVUePHbRNC7OD2aOR3WltTKxxF25OfpK8h8UQ7A0BuRkSgREbB59DBlFk4iAeg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, "node_modules/@vscode/vsce/node_modules/commander": { "version": "6.2.1", "resolved": "https://registry.npmjs.org/commander/-/commander-6.2.1.tgz", @@ -1468,10 +2540,11 @@ } }, "node_modules/@vscode/vsce/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", "dev": true, + "license": "ISC", "dependencies": { "brace-expansion": "^1.1.7" }, @@ -1479,161 +2552,149 @@ "node": "*" } }, - "node_modules/@vscode/vsce/node_modules/tmp": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.1.tgz", - "integrity": "sha512-76SUhtfqR2Ijn+xllcI5P1oyannHNHByD80W1q447gU3mp9G9PSpGdWmjUOHRDPiHYacIk66W7ubDTuPF3BEtQ==", - "dev": true, - "dependencies": { - "rimraf": "^3.0.0" - }, - "engines": { - "node": ">=8.17.0" - } - }, "node_modules/@webassemblyjs/ast": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.11.1.tgz", - "integrity": "sha512-ukBh14qFLjxTQNTXocdyksN5QdM28S1CxHt2rdskFyL+xFV7VremuBLVbmCePj+URalXBENx/9Lm7lnhihtCSw==", + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.14.1.tgz", + "integrity": "sha512-nuBEDgQfm1ccRp/8bCQrx1frohyufl4JlbMMZ4P1wpeOfDhF6FQkxZJ1b/e+PLwr6X1Nhw6OLme5usuBWYBvuQ==", "dev": true, "dependencies": { - "@webassemblyjs/helper-numbers": "1.11.1", - "@webassemblyjs/helper-wasm-bytecode": "1.11.1" + "@webassemblyjs/helper-numbers": "1.13.2", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2" } }, "node_modules/@webassemblyjs/floating-point-hex-parser": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.11.1.tgz", - "integrity": "sha512-iGRfyc5Bq+NnNuX8b5hwBrRjzf0ocrJPI6GWFodBFzmFnyvrQ83SHKhmilCU/8Jv67i4GJZBMhEzltxzcNagtQ==", + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.13.2.tgz", + "integrity": "sha512-6oXyTOzbKxGH4steLbLNOu71Oj+C8Lg34n6CqRvqfS2O71BxY6ByfMDRhBytzknj9yGUPVJ1qIKhRlAwO1AovA==", "dev": true }, "node_modules/@webassemblyjs/helper-api-error": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.11.1.tgz", - "integrity": "sha512-RlhS8CBCXfRUR/cwo2ho9bkheSXG0+NwooXcc3PAILALf2QLdFyj7KGsKRbVc95hZnhnERon4kW/D3SZpp6Tcg==", + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.13.2.tgz", + "integrity": "sha512-U56GMYxy4ZQCbDZd6JuvvNV/WFildOjsaWD3Tzzvmw/mas3cXzRJPMjP83JqEsgSbyrmaGjBfDtV7KDXV9UzFQ==", "dev": true }, "node_modules/@webassemblyjs/helper-buffer": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.11.1.tgz", - "integrity": "sha512-gwikF65aDNeeXa8JxXa2BAk+REjSyhrNC9ZwdT0f8jc4dQQeDQ7G4m0f2QCLPJiMTTO6wfDmRmj/pW0PsUvIcA==", + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.14.1.tgz", + "integrity": "sha512-jyH7wtcHiKssDtFPRB+iQdxlDf96m0E39yb0k5uJVhFGleZFoNw1c4aeIcVUPPbXUVJ94wwnMOAqUHyzoEPVMA==", "dev": true }, "node_modules/@webassemblyjs/helper-numbers": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-numbers/-/helper-numbers-1.11.1.tgz", - "integrity": "sha512-vDkbxiB8zfnPdNK9Rajcey5C0w+QJugEglN0of+kmO8l7lDb77AnlKYQF7aarZuCrv+l0UvqL+68gSDr3k9LPQ==", + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-numbers/-/helper-numbers-1.13.2.tgz", + "integrity": "sha512-FE8aCmS5Q6eQYcV3gI35O4J789wlQA+7JrqTTpJqn5emA4U2hvwJmvFRC0HODS+3Ye6WioDklgd6scJ3+PLnEA==", "dev": true, "dependencies": { - "@webassemblyjs/floating-point-hex-parser": "1.11.1", - "@webassemblyjs/helper-api-error": "1.11.1", + "@webassemblyjs/floating-point-hex-parser": "1.13.2", + "@webassemblyjs/helper-api-error": "1.13.2", "@xtuc/long": "4.2.2" } }, "node_modules/@webassemblyjs/helper-wasm-bytecode": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.11.1.tgz", - "integrity": "sha512-PvpoOGiJwXeTrSf/qfudJhwlvDQxFgelbMqtq52WWiXC6Xgg1IREdngmPN3bs4RoO83PnL/nFrxucXj1+BX62Q==", + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.13.2.tgz", + "integrity": "sha512-3QbLKy93F0EAIXLh0ogEVR6rOubA9AoZ+WRYhNbFyuB70j3dRdwH9g+qXhLAO0kiYGlg3TxDV+I4rQTr/YNXkA==", "dev": true }, "node_modules/@webassemblyjs/helper-wasm-section": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.11.1.tgz", - "integrity": "sha512-10P9No29rYX1j7F3EVPX3JvGPQPae+AomuSTPiF9eBQeChHI6iqjMIwR9JmOJXwpnn/oVGDk7I5IlskuMwU/pg==", + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.14.1.tgz", + "integrity": "sha512-ds5mXEqTJ6oxRoqjhWDU83OgzAYjwsCV8Lo/N+oRsNDmx/ZDpqalmrtgOMkHwxsG0iI//3BwWAErYRHtgn0dZw==", "dev": true, "dependencies": { - "@webassemblyjs/ast": "1.11.1", - "@webassemblyjs/helper-buffer": "1.11.1", - "@webassemblyjs/helper-wasm-bytecode": "1.11.1", - "@webassemblyjs/wasm-gen": "1.11.1" + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-buffer": "1.14.1", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/wasm-gen": "1.14.1" } }, "node_modules/@webassemblyjs/ieee754": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.11.1.tgz", - "integrity": "sha512-hJ87QIPtAMKbFq6CGTkZYJivEwZDbQUgYd3qKSadTNOhVY7p+gfP6Sr0lLRVTaG1JjFj+r3YchoqRYxNH3M0GQ==", + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.13.2.tgz", + "integrity": "sha512-4LtOzh58S/5lX4ITKxnAK2USuNEvpdVV9AlgGQb8rJDHaLeHciwG4zlGr0j/SNWlr7x3vO1lDEsuePvtcDNCkw==", "dev": true, "dependencies": { "@xtuc/ieee754": "^1.2.0" } }, "node_modules/@webassemblyjs/leb128": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.11.1.tgz", - "integrity": "sha512-BJ2P0hNZ0u+Th1YZXJpzW6miwqQUGcIHT1G/sf72gLVD9DZ5AdYTqPNbHZh6K1M5VmKvFXwGSWZADz+qBWxeRw==", + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.13.2.tgz", + "integrity": "sha512-Lde1oNoIdzVzdkNEAWZ1dZ5orIbff80YPdHx20mrHwHrVNNTjNr8E3xz9BdpcGqRQbAEa+fkrCb+fRFTl/6sQw==", "dev": true, "dependencies": { "@xtuc/long": "4.2.2" } }, "node_modules/@webassemblyjs/utf8": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.11.1.tgz", - "integrity": "sha512-9kqcxAEdMhiwQkHpkNiorZzqpGrodQQ2IGrHHxCy+Ozng0ofyMA0lTqiLkVs1uzTRejX+/O0EOT7KxqVPuXosQ==", + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.13.2.tgz", + "integrity": "sha512-3NQWGjKTASY1xV5m7Hr0iPeXD9+RDobLll3T9d2AO+g3my8xy5peVyjSag4I50mR1bBSN/Ct12lo+R9tJk0NZQ==", "dev": true }, "node_modules/@webassemblyjs/wasm-edit": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.11.1.tgz", - "integrity": "sha512-g+RsupUC1aTHfR8CDgnsVRVZFJqdkFHpsHMfJuWQzWU3tvnLC07UqHICfP+4XyL2tnr1amvl1Sdp06TnYCmVkA==", + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.14.1.tgz", + "integrity": "sha512-RNJUIQH/J8iA/1NzlE4N7KtyZNHi3w7at7hDjvRNm5rcUXa00z1vRz3glZoULfJ5mpvYhLybmVcwcjGrC1pRrQ==", "dev": true, "dependencies": { - "@webassemblyjs/ast": "1.11.1", - "@webassemblyjs/helper-buffer": "1.11.1", - "@webassemblyjs/helper-wasm-bytecode": "1.11.1", - "@webassemblyjs/helper-wasm-section": "1.11.1", - "@webassemblyjs/wasm-gen": "1.11.1", - "@webassemblyjs/wasm-opt": "1.11.1", - "@webassemblyjs/wasm-parser": "1.11.1", - "@webassemblyjs/wast-printer": "1.11.1" + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-buffer": "1.14.1", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/helper-wasm-section": "1.14.1", + "@webassemblyjs/wasm-gen": "1.14.1", + "@webassemblyjs/wasm-opt": "1.14.1", + "@webassemblyjs/wasm-parser": "1.14.1", + "@webassemblyjs/wast-printer": "1.14.1" } }, "node_modules/@webassemblyjs/wasm-gen": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.11.1.tgz", - "integrity": "sha512-F7QqKXwwNlMmsulj6+O7r4mmtAlCWfO/0HdgOxSklZfQcDu0TpLiD1mRt/zF25Bk59FIjEuGAIyn5ei4yMfLhA==", + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.14.1.tgz", + "integrity": "sha512-AmomSIjP8ZbfGQhumkNvgC33AY7qtMCXnN6bL2u2Js4gVCg8fp735aEiMSBbDR7UQIj90n4wKAFUSEd0QN2Ukg==", "dev": true, "dependencies": { - "@webassemblyjs/ast": "1.11.1", - "@webassemblyjs/helper-wasm-bytecode": "1.11.1", - "@webassemblyjs/ieee754": "1.11.1", - "@webassemblyjs/leb128": "1.11.1", - "@webassemblyjs/utf8": "1.11.1" + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/ieee754": "1.13.2", + "@webassemblyjs/leb128": "1.13.2", + "@webassemblyjs/utf8": "1.13.2" } }, "node_modules/@webassemblyjs/wasm-opt": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.11.1.tgz", - "integrity": "sha512-VqnkNqnZlU5EB64pp1l7hdm3hmQw7Vgqa0KF/KCNO9sIpI6Fk6brDEiX+iCOYrvMuBWDws0NkTOxYEb85XQHHw==", + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.14.1.tgz", + "integrity": "sha512-PTcKLUNvBqnY2U6E5bdOQcSM+oVP/PmrDY9NzowJjislEjwP/C4an2303MCVS2Mg9d3AJpIGdUFIQQWbPds0Sw==", "dev": true, "dependencies": { - "@webassemblyjs/ast": "1.11.1", - "@webassemblyjs/helper-buffer": "1.11.1", - "@webassemblyjs/wasm-gen": "1.11.1", - "@webassemblyjs/wasm-parser": "1.11.1" + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-buffer": "1.14.1", + "@webassemblyjs/wasm-gen": "1.14.1", + "@webassemblyjs/wasm-parser": "1.14.1" } }, "node_modules/@webassemblyjs/wasm-parser": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.11.1.tgz", - "integrity": "sha512-rrBujw+dJu32gYB7/Lup6UhdkPx9S9SnobZzRVL7VcBH9Bt9bCBLEuX/YXOOtBsOZ4NQrRykKhffRWHvigQvOA==", + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.14.1.tgz", + "integrity": "sha512-JLBl+KZ0R5qB7mCnud/yyX08jWFw5MsoalJ1pQ4EdFlgj9VdXKGuENGsiCIjegI1W7p91rUlcB/LB5yRJKNTcQ==", "dev": true, "dependencies": { - "@webassemblyjs/ast": "1.11.1", - "@webassemblyjs/helper-api-error": "1.11.1", - "@webassemblyjs/helper-wasm-bytecode": "1.11.1", - "@webassemblyjs/ieee754": "1.11.1", - "@webassemblyjs/leb128": "1.11.1", - "@webassemblyjs/utf8": "1.11.1" + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-api-error": "1.13.2", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/ieee754": "1.13.2", + "@webassemblyjs/leb128": "1.13.2", + "@webassemblyjs/utf8": "1.13.2" } }, "node_modules/@webassemblyjs/wast-printer": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.11.1.tgz", - "integrity": "sha512-IQboUWM4eKzWW+N/jij2sRatKMh99QEelo3Eb2q0qXkvPRISAj8Qxtmw5itwqK+TTkBuUIE45AxYPToqPtL5gg==", + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.14.1.tgz", + "integrity": "sha512-kPSSXE6De1XOR820C90RIo2ogvZG+c3KiHzqUoO/F34Y2shGzesfqv7o57xrxovZJH/MetF5UjroJ/R/3isoiw==", "dev": true, "dependencies": { - "@webassemblyjs/ast": "1.11.1", + "@webassemblyjs/ast": "1.14.1", "@xtuc/long": "4.2.2" } }, @@ -1686,10 +2747,9 @@ "dev": true }, "node_modules/acorn": { - "version": "8.8.2", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.8.2.tgz", - "integrity": "sha512-xjIYgE8HBrkpd/sJqOGNspf8uHG+NOHGOw6a/Urj8taM2EXfdNAH2oFcPeIFfsv3+kz/mJrS5VuMqbNLjCa2vw==", - "dev": true, + "version": "8.15.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", + "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "bin": { "acorn": "bin/acorn" }, @@ -1698,19 +2758,31 @@ } }, "node_modules/acorn-import-assertions": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/acorn-import-assertions/-/acorn-import-assertions-1.8.0.tgz", - "integrity": "sha512-m7VZ3jwz4eK6A4Vtt8Ew1/mNbP24u0FhdyfA7fSvnJR6LMdfOYnmuIrrJAgrYfYJ10F/otaHTtrtrtmHdMNzEw==", - "dev": true, + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/acorn-import-assertions/-/acorn-import-assertions-1.9.0.tgz", + "integrity": "sha512-cmMwop9x+8KFhxvKrKfPYmN6/pKTYYHBqLa0DfvVZcKMJWNyWLnaqND7dx/qn66R7ewM1UX5XMaDVP5wlVTaVA==", "peerDependencies": { "acorn": "^8" } }, + "node_modules/acorn-import-phases": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/acorn-import-phases/-/acorn-import-phases-1.0.4.tgz", + "integrity": "sha512-wKmbr/DDiIXzEOiWrTTUcDm24kQ2vGfZQvM2fwg2vXqR5uW6aapr7ObPtj1th32b9u90/Pf4AItvdTh42fBmVQ==", + "dev": true, + "engines": { + "node": ">=10.13.0" + }, + "peerDependencies": { + "acorn": "^8.14.0" + } + }, "node_modules/acorn-jsx": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", "dev": true, + "license": "MIT", "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } @@ -1765,10 +2837,11 @@ } }, "node_modules/ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz", + "integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==", "dev": true, + "license": "MIT", "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", @@ -1780,6 +2853,46 @@ "url": "https://github.com/sponsors/epoberezkin" } }, + "node_modules/ajv-formats": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz", + "integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==", + "dev": true, + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "node_modules/ajv-formats/node_modules/ajv": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz", + "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ajv-formats/node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true + }, "node_modules/ajv-keywords": { "version": "3.5.2", "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", @@ -1801,18 +2914,6 @@ "node": ">=0.10.0" } }, - "node_modules/ansi-gray": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/ansi-gray/-/ansi-gray-0.1.1.tgz", - "integrity": "sha1-KWLPVOyXksSFEKPetSRDaGHvclE=", - "dev": true, - "dependencies": { - "ansi-wrap": "0.1.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/ansi-regex": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", @@ -1844,25 +2945,16 @@ } }, "node_modules/anymatch": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-2.0.0.tgz", - "integrity": "sha512-5teOsQWABXHHBFP9y3skS5P3d/WfWXpv3FUpy+LorMrNYaT9pI4oLMQX7jzQ2KklNpGpWHzdCXTDT2Y3XGlZBw==", - "dev": true, - "dependencies": { - "micromatch": "^3.1.4", - "normalize-path": "^2.1.1" - } - }, - "node_modules/anymatch/node_modules/normalize-path": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-2.1.1.tgz", - "integrity": "sha1-GrKLVW4Zg2Oowab35vogE3/mrtk=", + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", "dev": true, "dependencies": { - "remove-trailing-separator": "^1.0.1" + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" }, "engines": { - "node": ">=0.10.0" + "node": ">= 8" } }, "node_modules/append-buffer": { @@ -1899,21 +2991,23 @@ } }, "node_modules/applicationinsights": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/applicationinsights/-/applicationinsights-2.5.0.tgz", - "integrity": "sha512-6kIFmpANRok+6FhCOmO7ZZ/mh7fdNKn17BaT13cg/RV5roLPJlA6q8srWexayHd3MPcwMb9072e8Zp0P47s/pw==", + "version": "2.7.3", + "resolved": "https://registry.npmjs.org/applicationinsights/-/applicationinsights-2.7.3.tgz", + "integrity": "sha512-JY8+kTEkjbA+kAVNWDtpfW2lqsrDALfDXuxOs74KLPu2y13fy/9WB52V4LfYVTVcW1/jYOXjTxNS2gPZIDh1iw==", "dependencies": { - "@azure/core-auth": "^1.4.0", - "@azure/core-rest-pipeline": "^1.10.0", + "@azure/core-auth": "^1.5.0", + "@azure/core-rest-pipeline": "1.10.1", + "@azure/core-util": "1.2.0", + "@azure/opentelemetry-instrumentation-azure-sdk": "^1.0.0-beta.5", "@microsoft/applicationinsights-web-snippet": "^1.0.1", - "@opentelemetry/api": "^1.0.4", - "@opentelemetry/core": "^1.0.1", - "@opentelemetry/sdk-trace-base": "^1.0.1", - "@opentelemetry/semantic-conventions": "^1.0.1", + "@opentelemetry/api": "^1.4.1", + "@opentelemetry/core": "^1.15.2", + "@opentelemetry/sdk-trace-base": "^1.15.2", + "@opentelemetry/semantic-conventions": "^1.15.2", "cls-hooked": "^4.2.2", "continuation-local-storage": "^3.2.1", - "diagnostic-channel": "1.1.0", - "diagnostic-channel-publishers": "1.0.5" + "diagnostic-channel": "1.1.1", + "diagnostic-channel-publishers": "1.0.7" }, "engines": { "node": ">=8.0.0" @@ -1927,13 +3021,6 @@ } } }, - "node_modules/aproba": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/aproba/-/aproba-1.2.0.tgz", - "integrity": "sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw==", - "dev": true, - "optional": true - }, "node_modules/arch": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/arch/-/arch-2.2.0.tgz", @@ -1956,8 +3043,9 @@ "node_modules/archive-type": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/archive-type/-/archive-type-4.0.0.tgz", - "integrity": "sha1-+S5yIzBW38aWlHJ0nCZ72wRrHXA=", + "integrity": "sha512-zV4Ky0v1F8dBrdYElwTvQhweQ0P7Kwc1aluqJsYtOBP01jXcWCyW2IEfI1YiqsG+Iy7ZR+o5LF1N+PGECBxHWA==", "dev": true, + "license": "MIT", "dependencies": { "file-type": "^4.2.0" }, @@ -1968,8 +3056,9 @@ "node_modules/archive-type/node_modules/file-type": { "version": "4.4.0", "resolved": "https://registry.npmjs.org/file-type/-/file-type-4.4.0.tgz", - "integrity": "sha1-G2AOX8ofvcboDApwxxyNul95BsU=", + "integrity": "sha512-f2UbFQEk7LXgWpi5ntcO86OeA/cC80fuDDDaX/fZ2ZGel+AF7leRQqBBW1eJNiiQkrZlAoM6P+VYP5P6bOlDEQ==", "dev": true, + "license": "MIT", "engines": { "node": ">=4" } @@ -1980,17 +3069,6 @@ "integrity": "sha1-+cjBN1fMHde8N5rHeyxipcKGjEA=", "dev": true }, - "node_modules/are-we-there-yet": { - "version": "1.1.7", - "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-1.1.7.tgz", - "integrity": "sha512-nxwy40TuMiUGqMyRHgCSWZ9FM4VAoRP4xUYSTv5ImRog+h9yISPbVH7H8fASCIzYn9wlEv4zvFL7uKDMCFQm3g==", - "dev": true, - "optional": true, - "dependencies": { - "delegates": "^1.0.0", - "readable-stream": "^2.0.6" - } - }, "node_modules/arg": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.0.tgz", @@ -2000,50 +3078,17 @@ "node_modules/argparse": { "version": "1.0.10", "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", - "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", - "dev": true, - "dependencies": { - "sprintf-js": "~1.0.2" - } - }, - "node_modules/arr-diff": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-4.0.0.tgz", - "integrity": "sha1-1kYQdP6/7HHn4VI1dhoyml3HxSA=", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/arr-filter": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/arr-filter/-/arr-filter-1.1.2.tgz", - "integrity": "sha1-Q/3d0JHo7xGqTEXZzcGOLf8XEe4=", - "dev": true, - "dependencies": { - "make-iterator": "^1.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/arr-flatten": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/arr-flatten/-/arr-flatten-1.1.0.tgz", - "integrity": "sha512-L3hKV5R/p5o81R7O02IGnwpDmkp6E982XhtbuwSe3O4qOtMMMtodicASA1Cny2U+aCXcNpml+m4dPsvsJ3jatg==", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", "dev": true, - "engines": { - "node": ">=0.10.0" + "dependencies": { + "sprintf-js": "~1.0.2" } }, - "node_modules/arr-map": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/arr-map/-/arr-map-2.0.2.tgz", - "integrity": "sha1-Onc0X/wc814qkYJWAfnljy4kysQ=", + "node_modules/arr-diff": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-4.0.0.tgz", + "integrity": "sha1-1kYQdP6/7HHn4VI1dhoyml3HxSA=", "dev": true, - "dependencies": { - "make-iterator": "^1.0.0" - }, "engines": { "node": ">=0.10.0" } @@ -2057,26 +3102,14 @@ "node": ">=0.10.0" } }, - "node_modules/array-each": { + "node_modules/array-buffer-byte-length": { "version": "1.0.1", - "resolved": "https://registry.npmjs.org/array-each/-/array-each-1.0.1.tgz", - "integrity": "sha1-p5SvDAWrF1KEbudTofIRoFugxE8=", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/array-includes": { - "version": "3.1.4", - "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.4.tgz", - "integrity": "sha512-ZTNSQkmWumEbiHO2GF4GmWxYVTiQyJy2XOTa15sdQSrvKn7l+180egQMqlrMOUMCyLMD7pmyQe4mMDUT6Behrw==", + "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.1.tgz", + "integrity": "sha512-ahC5W1xgou+KTXix4sAO8Ki12Q+jf4i0+tmk3sC+zgcynshkHxzpXdImBehiUYKKKDwvfFiJl1tZt6ewscS1Mg==", "dev": true, "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.3", - "es-abstract": "^1.19.1", - "get-intrinsic": "^1.1.1", - "is-string": "^1.0.7" + "call-bind": "^1.0.5", + "is-array-buffer": "^3.0.4" }, "engines": { "node": ">= 0.4" @@ -2085,47 +3118,33 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/array-initial": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/array-initial/-/array-initial-1.1.0.tgz", - "integrity": "sha1-L6dLJnOTccOUe9enrcc74zSz15U=", - "dev": true, - "dependencies": { - "array-slice": "^1.0.0", - "is-number": "^4.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/array-initial/node_modules/is-number": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-4.0.0.tgz", - "integrity": "sha512-rSklcAIlf1OmFdyAqbnWTLVelsQ58uvZ66S/ZyawjWqIviTWCjg2PzVGw8WUA+nNuPTqb4wgA+NszrJ+08LlgQ==", + "node_modules/array-each": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/array-each/-/array-each-1.0.1.tgz", + "integrity": "sha512-zHjL5SZa68hkKHBFBK6DJCTtr9sfTCPCaph/L7tMSLcTFgy+zX7E+6q5UArbtOtMBCtxdICpfTCspRse+ywyXA==", "dev": true, "engines": { "node": ">=0.10.0" } }, - "node_modules/array-last": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/array-last/-/array-last-1.3.0.tgz", - "integrity": "sha512-eOCut5rXlI6aCOS7Z7kCplKRKyiFQ6dHFBem4PwlwKeNFk2/XxTrhRh5T9PyaEWGy/NHTZWbY+nsZlNFJu9rYg==", + "node_modules/array-includes": { + "version": "3.1.8", + "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.8.tgz", + "integrity": "sha512-itaWrbYbqpGXkGhZPGUulwnhVf5Hpy1xiCFsGqyIGglbBxmG5vSjxQen3/WGOjPpNEv1RtBLKxbmVXm8HpJStQ==", "dev": true, "dependencies": { - "is-number": "^4.0.0" + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2", + "es-object-atoms": "^1.0.0", + "get-intrinsic": "^1.2.4", + "is-string": "^1.0.7" }, "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/array-last/node_modules/is-number": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-4.0.0.tgz", - "integrity": "sha512-rSklcAIlf1OmFdyAqbnWTLVelsQ58uvZ66S/ZyawjWqIviTWCjg2PzVGw8WUA+nNuPTqb4wgA+NszrJ+08LlgQ==", - "dev": true, - "engines": { - "node": ">=0.10.0" + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, "node_modules/array-slice": { @@ -2137,29 +3156,6 @@ "node": ">=0.10.0" } }, - "node_modules/array-sort": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/array-sort/-/array-sort-1.0.0.tgz", - "integrity": "sha512-ihLeJkonmdiAsD7vpgN3CRcx2J2S0TiYW+IS/5zHBI7mKUq3ySvBdzzBfD236ubDBQFiiyG3SWCPc+msQ9KoYg==", - "dev": true, - "dependencies": { - "default-compare": "^1.0.0", - "get-value": "^2.0.6", - "kind-of": "^5.0.2" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/array-sort/node_modules/kind-of": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-5.1.0.tgz", - "integrity": "sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/array-union": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", @@ -2169,24 +3165,36 @@ "node": ">=8" } }, - "node_modules/array-unique": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.3.2.tgz", - "integrity": "sha1-qJS3XUvE9s1nnvMkSp/Y9Gri1Cg=", + "node_modules/array.prototype.findlastindex": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/array.prototype.findlastindex/-/array.prototype.findlastindex-1.2.5.tgz", + "integrity": "sha512-zfETvRFA8o7EiNn++N5f/kaCw221hrpGsDmcpndVupkPzEc1Wuf3VgC0qby1BbHs7f5DVYjgtEU2LLh5bqeGfQ==", "dev": true, + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "es-shim-unscopables": "^1.0.2" + }, "engines": { - "node": ">=0.10.0" + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, "node_modules/array.prototype.flat": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.2.5.tgz", - "integrity": "sha512-KaYU+S+ndVqyUnignHftkwc58o3uVU1jzczILJ1tN2YaIZpFIKBiP/x/j97E5MVPsaCloPbqWLB/8qCTVvT2qg==", + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.3.2.tgz", + "integrity": "sha512-djYB+Zx2vLewY8RWlNCUdHjDXs2XOgm602S9E7P/UpHgfeHL00cRiIF+IN/G/aUJ7kGPb6yO/ErDI5V2s8iycA==", "dev": true, "dependencies": { "call-bind": "^1.0.2", - "define-properties": "^1.1.3", - "es-abstract": "^1.19.0" + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1", + "es-shim-unscopables": "^1.0.0" }, "engines": { "node": ">= 0.4" @@ -2196,14 +3204,37 @@ } }, "node_modules/array.prototype.flatmap": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/array.prototype.flatmap/-/array.prototype.flatmap-1.2.5.tgz", - "integrity": "sha512-08u6rVyi1Lj7oqWbS9nUxliETrtIROT4XGTA4D/LWGten6E3ocm7cy9SIrmNHOL5XVbVuckUp3X6Xyg8/zpvHA==", + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/array.prototype.flatmap/-/array.prototype.flatmap-1.3.2.tgz", + "integrity": "sha512-Ewyx0c9PmpcsByhSW4r+9zDU7sGjFc86qf/kKtuSCRdhfbk0SNLLkaT5qvcHnRGgc5NP/ly/y+qkXkqONX54CQ==", "dev": true, "dependencies": { - "call-bind": "^1.0.0", - "define-properties": "^1.1.3", - "es-abstract": "^1.19.0" + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1", + "es-shim-unscopables": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/arraybuffer.prototype.slice": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.3.tgz", + "integrity": "sha512-bMxMKAjg13EBSVscxTaYA4mRc5t1UAXa2kXiGTNfZ079HIWXEkKmkgFrh/nJqamaLSrXO5H4WFFkPEaLJWbs3A==", + "dev": true, + "dependencies": { + "array-buffer-byte-length": "^1.0.1", + "call-bind": "^1.0.5", + "define-properties": "^1.2.1", + "es-abstract": "^1.22.3", + "es-errors": "^1.2.1", + "get-intrinsic": "^1.2.3", + "is-array-buffer": "^3.0.4", + "is-shared-array-buffer": "^1.0.2" }, "engines": { "node": ">= 0.4" @@ -2217,6 +3248,7 @@ "resolved": "https://registry.npmjs.org/asn1.js/-/asn1.js-4.10.1.tgz", "integrity": "sha512-p32cOF5q0Zqs9uBiONKYLm6BClCoBCM5O9JfeUSlnQLBTxYdTK+pW+nXflm8UkKd2UYlEbYz5qEi0JuZR9ckSw==", "dev": true, + "license": "MIT", "dependencies": { "bn.js": "^4.0.0", "inherits": "^2.0.1", @@ -2272,15 +3304,6 @@ "integrity": "sha1-9wtzXGvKGlycItmCw+Oef+ujva0=", "dev": true }, - "node_modules/astral-regex": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-2.0.0.tgz", - "integrity": "sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==", - "dev": true, - "engines": { - "node": ">=8" - } - }, "node_modules/async": { "version": "3.2.3", "resolved": "https://registry.npmjs.org/async/-/async-3.2.3.tgz", @@ -2288,26 +3311,19 @@ "dev": true }, "node_modules/async-done": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/async-done/-/async-done-1.3.2.tgz", - "integrity": "sha512-uYkTP8dw2og1tu1nmza1n1CMW0qb8gWWlwqMmLb7MhBVs4BXrFziT6HXUd+/RlRA/i4H9AkofYloUbs1fwMqlw==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/async-done/-/async-done-2.0.0.tgz", + "integrity": "sha512-j0s3bzYq9yKIVLKGE/tWlCpa3PfFLcrDZLTSVdnnCTGagXuXBJO4SsY9Xdk/fQBirCkH4evW5xOeJXqlAQFdsw==", "dev": true, "dependencies": { - "end-of-stream": "^1.1.0", - "once": "^1.3.2", - "process-nextick-args": "^2.0.0", - "stream-exhaust": "^1.0.1" + "end-of-stream": "^1.4.4", + "once": "^1.4.0", + "stream-exhaust": "^1.0.2" }, "engines": { - "node": ">= 0.10" + "node": ">= 10.13.0" } }, - "node_modules/async-each": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/async-each/-/async-each-1.0.3.tgz", - "integrity": "sha512-z/WhQ5FPySLdvREByI2vZiTWwCnF0moMJ1hK9YQwDTHKh6I7/uSckMetoRGb5UBZPC1z0jlw+n/XCgjeH7y1AQ==", - "dev": true - }, "node_modules/async-hook-jl": { "version": "1.7.6", "resolved": "https://registry.npmjs.org/async-hook-jl/-/async-hook-jl-1.7.6.tgz", @@ -2331,16 +3347,24 @@ "node": "<=0.11.8 || >0.11.10" } }, + "node_modules/async-listener/node_modules/semver": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", + "bin": { + "semver": "bin/semver" + } + }, "node_modules/async-settle": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/async-settle/-/async-settle-1.0.0.tgz", - "integrity": "sha1-HQqRS7Aldb7IqPOnTlCA9yssDGs=", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/async-settle/-/async-settle-2.0.0.tgz", + "integrity": "sha512-Obu/KE8FurfQRN6ODdHN9LuXqwC+JFIM9NRyZqJJ4ZfLJmIYN9Rg0/kb+wF70VV5+fJusTMQlJ1t5rF7J/ETdg==", "dev": true, "dependencies": { - "async-done": "^1.2.2" + "async-done": "^2.0.0" }, "engines": { - "node": ">= 0.10" + "node": ">= 10.13.0" } }, "node_modules/asynckit": { @@ -2348,23 +3372,14 @@ "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" }, - "node_modules/atob": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/atob/-/atob-2.1.2.tgz", - "integrity": "sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg==", - "dev": true, - "bin": { - "atob": "bin/atob.js" - }, - "engines": { - "node": ">= 4.5.0" - } - }, "node_modules/available-typed-arrays": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.5.tgz", - "integrity": "sha512-DMD0KiN46eipeziST1LPP/STfDU0sufISXmjSgvVsoU2tqxctQeASejWcfNtxYKqETM1UxQ8sp2OrSBWpHY6sw==", + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", + "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==", "dev": true, + "dependencies": { + "possible-typed-array-names": "^1.0.0" + }, "engines": { "node": ">= 0.4" }, @@ -2381,14 +3396,6 @@ "node": ">=4" } }, - "node_modules/axios": { - "version": "0.26.1", - "resolved": "https://registry.npmjs.org/axios/-/axios-0.26.1.tgz", - "integrity": "sha512-fPwcX4EvnSHuInCMItEhAGnaSEXRBjtzh9fOtsE6E1G6p7vl7edEeZe11QHf18+6+9gR5PbKV/sGKNaD8YaMeA==", - "dependencies": { - "follow-redirects": "^1.14.8" - } - }, "node_modules/axobject-query": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-2.2.0.tgz", @@ -2396,15 +3403,21 @@ "dev": true }, "node_modules/azure-devops-node-api": { - "version": "11.0.1", - "resolved": "https://registry.npmjs.org/azure-devops-node-api/-/azure-devops-node-api-11.0.1.tgz", - "integrity": "sha512-YMdjAw9l5p/6leiyIloxj3k7VIvYThKjvqgiQn88r3nhT93ENwsoDS3A83CyJ4uTWzCZ5f5jCi6c27rTU5Pz+A==", + "version": "12.5.0", + "resolved": "https://registry.npmjs.org/azure-devops-node-api/-/azure-devops-node-api-12.5.0.tgz", + "integrity": "sha512-R5eFskGvOm3U/GzeAuxRkUsAl0hrAwGgWn6zAd2KrZmrEhWZVqLew4OOupbQlXUuojUzpGtq62SmdhJ06N88og==", "dev": true, "dependencies": { "tunnel": "0.0.6", "typed-rest-client": "^1.8.4" } }, + "node_modules/b4a": { + "version": "1.6.6", + "resolved": "https://registry.npmjs.org/b4a/-/b4a-1.6.6.tgz", + "integrity": "sha512-5Tk1HLk6b6ctmjIkAcU/Ujv/1WqiDl0F0JdRCR80VsOcUlHcu7pWeWRlOqQLHfDEsVx9YH/aif5AG4ehoCtTmg==", + "dev": true + }, "node_modules/babel-runtime": { "version": "6.26.0", "resolved": "https://registry.npmjs.org/babel-runtime/-/babel-runtime-6.26.0.tgz", @@ -2430,97 +3443,42 @@ "dev": true }, "node_modules/bach": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/bach/-/bach-1.2.0.tgz", - "integrity": "sha1-Szzpa/JxNPeaG0FKUcFONMO9mIA=", - "dev": true, - "dependencies": { - "arr-filter": "^1.1.1", - "arr-flatten": "^1.0.1", - "arr-map": "^2.0.0", - "array-each": "^1.0.0", - "array-initial": "^1.0.0", - "array-last": "^1.1.1", - "async-done": "^1.2.2", - "async-settle": "^1.0.0", - "now-and-later": "^2.0.0" - }, - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/balanced-match": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", - "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=" - }, - "node_modules/base": { - "version": "0.11.2", - "resolved": "https://registry.npmjs.org/base/-/base-0.11.2.tgz", - "integrity": "sha512-5T6P4xPgpp0YDFvSWwEZ4NoE3aM4QBQXDzmVbraCkFj8zHM+mba8SyqB5DbZWyR7mYHo6Y7BdQo3MoA4m0TeQg==", - "dev": true, - "dependencies": { - "cache-base": "^1.0.1", - "class-utils": "^0.3.5", - "component-emitter": "^1.2.1", - "define-property": "^1.0.0", - "isobject": "^3.0.1", - "mixin-deep": "^1.2.0", - "pascalcase": "^0.1.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/base/node_modules/define-property": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", - "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/bach/-/bach-2.0.1.tgz", + "integrity": "sha512-A7bvGMGiTOxGMpNupYl9HQTf0FFDNF4VCmks4PJpFyN1AX2pdKuxuwdvUz2Hu388wcgp+OvGFNsumBfFNkR7eg==", "dev": true, "dependencies": { - "is-descriptor": "^1.0.0" + "async-done": "^2.0.0", + "async-settle": "^2.0.0", + "now-and-later": "^3.0.0" }, "engines": { - "node": ">=0.10.0" + "node": ">=10.13.0" } }, - "node_modules/base/node_modules/is-accessor-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", - "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", + "node_modules/bach/node_modules/now-and-later": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/now-and-later/-/now-and-later-3.0.0.tgz", + "integrity": "sha512-pGO4pzSdaxhWTGkfSfHx3hVzJVslFPwBp2Myq9MYN/ChfJZF87ochMAXnvz6/58RJSf5ik2q9tXprBBrk2cpcg==", "dev": true, "dependencies": { - "kind-of": "^6.0.0" + "once": "^1.4.0" }, "engines": { - "node": ">=0.10.0" + "node": ">= 10.13.0" } }, - "node_modules/base/node_modules/is-data-descriptor": { + "node_modules/balanced-match": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", - "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", - "dev": true, - "dependencies": { - "kind-of": "^6.0.0" - }, - "engines": { - "node": ">=0.10.0" - } + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", + "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=" }, - "node_modules/base/node_modules/is-descriptor": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", - "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", + "node_modules/bare-events": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/bare-events/-/bare-events-2.3.1.tgz", + "integrity": "sha512-sJnSOTVESURZ61XgEleqmP255T6zTYwHPwE4r6SssIh0U9/uDvfpdoJYpVUerJJZH2fueO+CdT8ZT+OC/7aZDA==", "dev": true, - "dependencies": { - "is-accessor-descriptor": "^1.0.0", - "is-data-descriptor": "^1.0.0", - "kind-of": "^6.0.2" - }, - "engines": { - "node": ">=0.10.0" - } + "optional": true }, "node_modules/base64-js": { "version": "1.5.1", @@ -2542,6 +3500,15 @@ } ] }, + "node_modules/baseline-browser-mapping": { + "version": "2.9.19", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.9.19.tgz", + "integrity": "sha512-ipDqC8FrAl/76p2SSWKSI+H9tFwm7vYqXQrItCuiVPt26Km0jS+NzSsBWAaBusvSbQcfJG+JitdMm+wZAgTYqg==", + "dev": true, + "bin": { + "baseline-browser-mapping": "dist/cli.js" + } + }, "node_modules/bent": { "version": "7.3.12", "resolved": "https://registry.npmjs.org/bent/-/bent-7.3.12.tgz", @@ -2565,15 +3532,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/big-integer": { - "version": "1.6.49", - "resolved": "https://registry.npmjs.org/big-integer/-/big-integer-1.6.49.tgz", - "integrity": "sha512-KJ7VhqH+f/BOt9a3yMwJNmcZjG53ijWMTjSAGMveQWyLwqIiwkjNP5PFgDob3Snnx86SjDj6I89fIbv0dkQeNw==", - "dev": true, - "engines": { - "node": ">=0.6" - } - }, "node_modules/big.js": { "version": "5.2.2", "resolved": "https://registry.npmjs.org/big.js/-/big.js-5.2.2.tgz", @@ -2583,19 +3541,6 @@ "node": "*" } }, - "node_modules/binary": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/binary/-/binary-0.3.0.tgz", - "integrity": "sha1-n2BVO8XOjDOG87VTz/R0Yq3sqnk=", - "dev": true, - "dependencies": { - "buffers": "~0.1.1", - "chainsaw": "~0.1.0" - }, - "engines": { - "node": "*" - } - }, "node_modules/binary-extensions": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", @@ -2605,31 +3550,23 @@ "node": ">=8" } }, - "node_modules/bindings": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz", - "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==", - "dev": true, - "optional": true, - "dependencies": { - "file-uri-to-path": "1.0.0" - } - }, "node_modules/bl": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/bl/-/bl-1.2.3.tgz", "integrity": "sha512-pvcNpa0UU69UT341rO6AYy4FVAIkUHuZXRIWbq+zHnsVcRzDDjIAhGuuYoi0d//cwIwtt4pkpKycWEfjdV+vww==", "dev": true, + "license": "MIT", "dependencies": { "readable-stream": "^2.3.5", "safe-buffer": "^5.1.1" } }, "node_modules/bn.js": { - "version": "4.11.8", - "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.11.8.tgz", - "integrity": "sha512-ItfYfPLkWHUjckQCk8xC+LwxgK8NYcXywGigJgSwOP8Y2iyWT4f2vsZnoOXTTbo+o5yXmIUJ4gn5538SO5S3gA==", - "dev": true + "version": "4.12.3", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.3.tgz", + "integrity": "sha512-fGTi3gxV/23FTYdAoUtLYp6qySe2KE3teyZitipKNRuVYcBkoP/bB3guXN/XVKUe9mxCHXnc9C4ocyz8OmgN0g==", + "dev": true, + "license": "MIT" }, "node_modules/boolbase": { "version": "1.0.0", @@ -2638,45 +3575,24 @@ "dev": true }, "node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.13.tgz", + "integrity": "sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==", "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "node_modules/braces": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/braces/-/braces-2.3.2.tgz", - "integrity": "sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w==", - "dev": true, - "dependencies": { - "arr-flatten": "^1.1.0", - "array-unique": "^0.3.2", - "extend-shallow": "^2.0.1", - "fill-range": "^4.0.0", - "isobject": "^3.0.1", - "repeat-element": "^1.1.2", - "snapdragon": "^0.8.1", - "snapdragon-node": "^2.0.1", - "split-string": "^3.0.2", - "to-regex": "^3.0.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/braces/node_modules/extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", "dev": true, "dependencies": { - "is-extendable": "^0.1.0" + "fill-range": "^7.1.1" }, "engines": { - "node": ">=0.10.0" + "node": ">=8" } }, "node_modules/brorand": { @@ -2729,29 +3645,95 @@ } }, "node_modules/browserify-rsa": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/browserify-rsa/-/browserify-rsa-4.0.1.tgz", - "integrity": "sha1-IeCr+vbyApzy+vsTNWenAdQTVSQ=", + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/browserify-rsa/-/browserify-rsa-4.1.1.tgz", + "integrity": "sha512-YBjSAiTqM04ZVei6sXighu679a3SqWORA3qZTEqZImnlkDIFtKc6pNutpjyZ8RJTjQtuYfeetkxM11GwoYXMIQ==", "dev": true, + "license": "MIT", "dependencies": { - "bn.js": "^4.1.0", - "randombytes": "^2.0.1" + "bn.js": "^5.2.1", + "randombytes": "^2.1.0", + "safe-buffer": "^5.2.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/browserify-rsa/node_modules/bn.js": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-5.2.3.tgz", + "integrity": "sha512-EAcmnPkxpntVL+DS7bO1zhcZNvCkxqtkd0ZY53h06GNQ3DEkkGZ/gKgmDv6DdZQGj9BgfSPKtJJ7Dp1GPP8f7w==", + "dev": true, + "license": "MIT" + }, + "node_modules/browserify-rsa/node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/browserify-sign": { + "version": "4.2.5", + "resolved": "https://registry.npmjs.org/browserify-sign/-/browserify-sign-4.2.5.tgz", + "integrity": "sha512-C2AUdAJg6rlM2W5QMp2Q4KGQMVBwR1lIimTsUnutJ8bMpW5B52pGpR2gEnNBNwijumDo5FojQ0L9JrXA8m4YEw==", + "dev": true, + "license": "ISC", + "dependencies": { + "bn.js": "^5.2.2", + "browserify-rsa": "^4.1.1", + "create-hash": "^1.2.0", + "create-hmac": "^1.1.7", + "elliptic": "^6.6.1", + "inherits": "^2.0.4", + "parse-asn1": "^5.1.9", + "readable-stream": "^2.3.8", + "safe-buffer": "^5.2.1" + }, + "engines": { + "node": ">= 0.10" } }, - "node_modules/browserify-sign": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/browserify-sign/-/browserify-sign-4.0.4.tgz", - "integrity": "sha1-qk62jl17ZYuqa/alfmMMvXqT0pg=", + "node_modules/browserify-sign/node_modules/bn.js": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-5.2.3.tgz", + "integrity": "sha512-EAcmnPkxpntVL+DS7bO1zhcZNvCkxqtkd0ZY53h06GNQ3DEkkGZ/gKgmDv6DdZQGj9BgfSPKtJJ7Dp1GPP8f7w==", "dev": true, - "dependencies": { - "bn.js": "^4.1.1", - "browserify-rsa": "^4.0.0", - "create-hash": "^1.1.0", - "create-hmac": "^1.1.2", - "elliptic": "^6.0.0", - "inherits": "^2.0.1", - "parse-asn1": "^5.0.0" - } + "license": "MIT" + }, + "node_modules/browserify-sign/node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] }, "node_modules/browserify-zlib": { "version": "0.2.0", @@ -2762,16 +3744,10 @@ "pako": "~1.0.5" } }, - "node_modules/browserify-zlib/node_modules/pako": { - "version": "1.0.10", - "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.10.tgz", - "integrity": "sha512-0DTvPVU3ed8+HNXOu5Bs+o//Mbdj9VNQMUOe9oKCwh8l0GNwpTDMKCWbRjgtD291AWnkAgkqA/LOnQS8AmS1tw==", - "dev": true - }, "node_modules/browserslist": { - "version": "4.20.2", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.20.2.tgz", - "integrity": "sha512-CQOBCqp/9pDvDbx3xfMi+86pr4KXIf2FDkTTdeuYw8OxS9t898LA1Khq57gtufFILXpfgsSx5woNgsBgvGjpsA==", + "version": "4.28.1", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz", + "integrity": "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==", "dev": true, "funding": [ { @@ -2781,14 +3757,18 @@ { "type": "tidelift", "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" } ], "dependencies": { - "caniuse-lite": "^1.0.30001317", - "electron-to-chromium": "^1.4.84", - "escalade": "^3.1.1", - "node-releases": "^2.0.2", - "picocolors": "^1.0.0" + "baseline-browser-mapping": "^2.9.0", + "caniuse-lite": "^1.0.30001759", + "electron-to-chromium": "^1.5.263", + "node-releases": "^2.0.27", + "update-browserslist-db": "^1.2.0" }, "bin": { "browserslist": "cli.js" @@ -2826,6 +3806,7 @@ "resolved": "https://registry.npmjs.org/buffer-alloc/-/buffer-alloc-1.2.0.tgz", "integrity": "sha512-CFsHQgjtW1UChdXgbyJGtnm+O/uLQeZdtbDo8mfUgYXCHSM1wgrVxXm6bSyrUuErEb+4sYVGCzASBRot7zyrow==", "dev": true, + "license": "MIT", "dependencies": { "buffer-alloc-unsafe": "^1.1.0", "buffer-fill": "^1.0.0" @@ -2835,7 +3816,8 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/buffer-alloc-unsafe/-/buffer-alloc-unsafe-1.1.0.tgz", "integrity": "sha512-TEM2iMIEQdJ2yjPJoSIsldnleVaAk1oW3DBVUykyOLsEsFmEc9kn+SFFPz+gl54KQNxlDnAwCXosOS9Okx2xAg==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/buffer-crc32": { "version": "0.2.13", @@ -2846,11 +3828,18 @@ "node": "*" } }, + "node_modules/buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==", + "dev": true + }, "node_modules/buffer-fill": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/buffer-fill/-/buffer-fill-1.0.0.tgz", - "integrity": "sha1-+PeLdniYiO858gXNY39o5wISKyw=", - "dev": true + "integrity": "sha512-T7zexNBwiiaCOGDg9xNX9PBmjrubblRkENuptryuI64URkXDFum9il/JGL8Lm8wYfAXpredVXXZz7eMHilimiQ==", + "dev": true, + "license": "MIT" }, "node_modules/buffer-from": { "version": "1.1.1", @@ -2858,30 +3847,12 @@ "integrity": "sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A==", "dev": true }, - "node_modules/buffer-indexof-polyfill": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/buffer-indexof-polyfill/-/buffer-indexof-polyfill-1.0.2.tgz", - "integrity": "sha512-I7wzHwA3t1/lwXQh+A5PbNvJxgfo5r3xulgpYDB5zckTu/Z9oUK9biouBKQUjEqzaz3HnAT6TYoovmE+GqSf7A==", - "dev": true, - "engines": { - "node": ">=0.10" - } - }, "node_modules/buffer-xor": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/buffer-xor/-/buffer-xor-1.0.3.tgz", "integrity": "sha1-JuYe0UIvtw3ULm42cp7VHYVf6Nk=", "dev": true }, - "node_modules/buffers": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/buffers/-/buffers-0.1.1.tgz", - "integrity": "sha1-skV5w77U1tOWru5tmorn9Ugqt7s=", - "dev": true, - "engines": { - "node": ">=0.2.0" - } - }, "node_modules/builtin-status-codes": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/builtin-status-codes/-/builtin-status-codes-3.0.0.tgz", @@ -2894,31 +3865,12 @@ "integrity": "sha512-i4uu6M4zuMUiyfZN4RU2+i9+peJh//pXhd9x1oSe1LBkZ3LEbCoygu8W0bXTukU1Jme2txKuotpCZRaC3FLxcQ==", "dev": true }, - "node_modules/cache-base": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/cache-base/-/cache-base-1.0.1.tgz", - "integrity": "sha512-AKcdTnFSWATd5/GCPRxr2ChwIJ85CeyrEyjRHlKxQ56d4XJMGym0uAiKn0xbLOGOl3+yRpOTi484dVCEc5AUzQ==", - "dev": true, - "dependencies": { - "collection-visit": "^1.0.0", - "component-emitter": "^1.2.1", - "get-value": "^2.0.6", - "has-value": "^1.0.0", - "isobject": "^3.0.1", - "set-value": "^2.0.0", - "to-object-path": "^0.3.0", - "union-value": "^1.0.0", - "unset-value": "^1.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/cacheable-request": { "version": "2.1.4", "resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-2.1.4.tgz", "integrity": "sha512-vag0O2LKZ/najSoUwDbVlnlCFvhBE/7mGTY2B5FgCBDcRD+oVV1HYTOwM6JZfMg/hIcM6IwnTZ1uQQL5/X3xIQ==", "dev": true, + "license": "MIT", "dependencies": { "clone-response": "1.0.2", "get-stream": "3.0.0", @@ -2929,11 +3881,22 @@ "responselike": "1.0.2" } }, + "node_modules/cacheable-request/node_modules/get-stream": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-3.0.0.tgz", + "integrity": "sha512-GlhdIUuVakc8SJ6kK0zAFbiGzRFzNnY4jUuEbV9UROo4Y+0Ny4fjvcZFVTeDA4odpFyOQzaw6hXukJSq/f28sQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, "node_modules/cacheable-request/node_modules/lowercase-keys": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-1.0.0.tgz", - "integrity": "sha1-TjNms55/VFfjXxMkvfb4jQv8cwY=", + "integrity": "sha512-RPlX0+PHuvxVDZ7xX+EBVAp4RsVxP/TdDSN2mJYdiq1Lc4Hz7EUSjUI7RZrKKlmrIzVhf6Jo2stj7++gVarS0A==", "dev": true, + "license": "MIT", "engines": { "node": ">=0.10.0" } @@ -2954,18 +3917,64 @@ } }, "node_modules/call-bind": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz", + "integrity": "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.0", + "es-define-property": "^1.0.0", + "get-intrinsic": "^1.2.4", + "set-function-length": "^1.2.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/call-bind-apply-helpers": { "version": "1.0.2", - "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz", - "integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", "dev": true, + "license": "MIT", "dependencies": { - "function-bind": "^1.1.1", - "get-intrinsic": "^1.0.2" + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/camelcase": { "version": "5.3.1", "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", @@ -2976,9 +3985,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001320", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001320.tgz", - "integrity": "sha512-MWPzG54AGdo3nWx7zHZTefseM5Y1ccM7hlQKHRqJkPozUaw3hNbBTMmLn16GG2FUzjR13Cr3NPfhIieX5PzXDA==", + "version": "1.0.30001768", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001768.tgz", + "integrity": "sha512-qY3aDRZC5nWPgHUgIB84WL+nySuo19wk0VJpp/XI9T34lrvkyhRvNVOFJOp2kxClQhiFBu+TaUSudf6oa3vkSA==", "dev": true, "funding": [ { @@ -2988,6 +3997,10 @@ { "type": "tidelift", "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" } ] }, @@ -3036,18 +4049,6 @@ "chai": ">= 2.1.2 < 5" } }, - "node_modules/chainsaw": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/chainsaw/-/chainsaw-0.1.0.tgz", - "integrity": "sha1-XqtQsor+WAdNDVgpE4iCi15fvJg=", - "dev": true, - "dependencies": { - "traverse": ">=0.3.0 <0.4" - }, - "engines": { - "node": "*" - } - }, "node_modules/chalk": { "version": "2.4.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", @@ -3066,6 +4067,7 @@ "version": "0.0.2", "resolved": "https://registry.npmjs.org/charenc/-/charenc-0.0.2.tgz", "integrity": "sha1-wKHS86cJLgN3S/qD8UwPxXkKhmc=", + "dev": true, "engines": { "node": "*" } @@ -3330,43 +4332,6 @@ "fsevents": "~2.3.2" } }, - "node_modules/chokidar/node_modules/anymatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.2.tgz", - "integrity": "sha512-P43ePfOAIupkguHUycrc4qJ9kz8ZiuOUijaETwX7THt0Y/GNK7v0aa8rY816xWjZ7rJdA5XdMcpVFTKMq+RvWg==", - "dev": true, - "dependencies": { - "normalize-path": "^3.0.0", - "picomatch": "^2.0.4" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/chokidar/node_modules/braces": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", - "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", - "dev": true, - "dependencies": { - "fill-range": "^7.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/chokidar/node_modules/fill-range": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", - "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", - "dev": true, - "dependencies": { - "to-regex-range": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/chokidar/node_modules/glob-parent": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", @@ -3379,31 +4344,10 @@ "node": ">= 6" } }, - "node_modules/chokidar/node_modules/is-number": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", - "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", - "dev": true, - "engines": { - "node": ">=0.12.0" - } - }, - "node_modules/chokidar/node_modules/to-regex-range": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", - "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", - "dev": true, - "dependencies": { - "is-number": "^7.0.0" - }, - "engines": { - "node": ">=8.0" - } - }, "node_modules/chownr": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.2.tgz", - "integrity": "sha512-GkfeAQh+QNy3wquu9oIZr6SS5x7wGdSgNQvD10X3r+AZr1Oys22HW8kAmDMvNg2+Dm0TeGaEuO8gFwdBXxwO8A==", + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", + "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", "dev": true, "optional": true }, @@ -3420,15 +4364,41 @@ } }, "node_modules/cipher-base": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/cipher-base/-/cipher-base-1.0.4.tgz", - "integrity": "sha512-Kkht5ye6ZGmwv40uUDZztayT2ThLQGfnj/T71N/XzeZeo3nf8foyW7zGTsPYkEya3m5f3cAypH+qe7YOrM1U2Q==", + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/cipher-base/-/cipher-base-1.0.7.tgz", + "integrity": "sha512-Mz9QMT5fJe7bKI7MH31UilT5cEK5EHHRCccw/YRFsRY47AuNgaV6HY3rscp0/I4Q+tTW/5zoqpSeRRI54TkDWA==", "dev": true, + "license": "MIT", "dependencies": { - "inherits": "^2.0.1", - "safe-buffer": "^5.0.1" + "inherits": "^2.0.4", + "safe-buffer": "^5.2.1", + "to-buffer": "^1.2.2" + }, + "engines": { + "node": ">= 0.10" } }, + "node_modules/cipher-base/node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, "node_modules/circular-json": { "version": "0.3.3", "resolved": "https://registry.npmjs.org/circular-json/-/circular-json-0.3.3.tgz", @@ -3436,32 +4406,10 @@ "deprecated": "CircularJSON is in maintenance only, flatted is its successor.", "dev": true }, - "node_modules/class-utils": { - "version": "0.3.6", - "resolved": "https://registry.npmjs.org/class-utils/-/class-utils-0.3.6.tgz", - "integrity": "sha512-qOhPa/Fj7s6TY8H8esGu5QNpMMQxz79h+urzrNYN6mn+9BnxlDGf5QZ+XeCDsxSjPqsSR56XOZOJmpeurnLMeg==", - "dev": true, - "dependencies": { - "arr-union": "^3.1.0", - "define-property": "^0.2.5", - "isobject": "^3.0.0", - "static-extend": "^0.1.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/class-utils/node_modules/define-property": { - "version": "0.2.5", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", - "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", - "dev": true, - "dependencies": { - "is-descriptor": "^0.1.0" - }, - "engines": { - "node": ">=0.10.0" - } + "node_modules/cjs-module-lexer": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-1.2.3.tgz", + "integrity": "sha512-0TNiGstbQmCFwt4akjjBg5pLRTSyj/PkWQ1ZoO2zntmg9yLqSRxwEa4iCfQLGjqhiqBfOJa7W/E8wfGrTDmlZQ==" }, "node_modules/clean-stack": { "version": "2.2.0", @@ -3473,35 +4421,14 @@ } }, "node_modules/cliui": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-3.2.0.tgz", - "integrity": "sha1-EgYBU3qRbSmUD5NNo7SNWFo5IT0=", - "dev": true, - "dependencies": { - "string-width": "^1.0.1", - "strip-ansi": "^3.0.1", - "wrap-ansi": "^2.0.0" - } - }, - "node_modules/cliui/node_modules/ansi-regex": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", - "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/cliui/node_modules/strip-ansi": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", - "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", + "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", "dev": true, "dependencies": { - "ansi-regex": "^2.0.0" - }, - "engines": { - "node": ">=0.10.0" + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^7.0.0" } }, "node_modules/clone": { @@ -3539,8 +4466,9 @@ "node_modules/clone-response": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/clone-response/-/clone-response-1.0.2.tgz", - "integrity": "sha1-0dyXOSAxTfZ/vrlCI7TuNQI56Ws=", + "integrity": "sha512-yjLXh88P599UOyPTFX0POsd7WxnbsVsGohcwzHOLspIhhpalPw1BcqED8NblyZLKcGrL8dTgMlcaZxV2jAD41Q==", "dev": true, + "license": "MIT", "dependencies": { "mimic-response": "^1.0.0" } @@ -3565,62 +4493,31 @@ "node_modules/cls-hooked": { "version": "4.2.2", "resolved": "https://registry.npmjs.org/cls-hooked/-/cls-hooked-4.2.2.tgz", - "integrity": "sha512-J4Xj5f5wq/4jAvcdgoGsL3G103BtWpZrMo8NEinRltN+xpTZdI+M38pyQqhuFU/P792xkMFvnKSf+Lm81U1bxw==", - "dependencies": { - "async-hook-jl": "^1.7.6", - "emitter-listener": "^1.0.1", - "semver": "^5.4.1" - }, - "engines": { - "node": "^4.7 || >=6.9 || >=7.3 || >=8.2.1" - } - }, - "node_modules/code-point-at": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz", - "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/collection-map": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/collection-map/-/collection-map-1.0.0.tgz", - "integrity": "sha1-rqDwb40mx4DCt1SUOFVEsiVa8Yw=", - "dev": true, - "dependencies": { - "arr-map": "^2.0.2", - "for-own": "^1.0.0", - "make-iterator": "^1.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/collection-map/node_modules/for-own": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/for-own/-/for-own-1.0.0.tgz", - "integrity": "sha1-xjMy9BXO3EsE2/5wz4NklMU8tEs=", - "dev": true, + "integrity": "sha512-J4Xj5f5wq/4jAvcdgoGsL3G103BtWpZrMo8NEinRltN+xpTZdI+M38pyQqhuFU/P792xkMFvnKSf+Lm81U1bxw==", "dependencies": { - "for-in": "^1.0.1" + "async-hook-jl": "^1.7.6", + "emitter-listener": "^1.0.1", + "semver": "^5.4.1" }, "engines": { - "node": ">=0.10.0" + "node": "^4.7 || >=6.9 || >=7.3 || >=8.2.1" } }, - "node_modules/collection-visit": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/collection-visit/-/collection-visit-1.0.0.tgz", - "integrity": "sha1-S8A3PBZLwykbTTaMgpzxqApZ3KA=", + "node_modules/cls-hooked/node_modules/semver": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", + "bin": { + "semver": "bin/semver" + } + }, + "node_modules/cockatiel": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/cockatiel/-/cockatiel-3.1.3.tgz", + "integrity": "sha512-xC759TpZ69d7HhfDp8m2WkRwEUiCkxY8Ee2OQH/3H6zmy2D/5Sm+zSTbPRa+V2QyjDtpMvjOIAOVjA2gp6N1kQ==", "dev": true, - "dependencies": { - "map-visit": "^1.0.0", - "object-visit": "^1.0.0" - }, "engines": { - "node": ">=0.10.0" + "node": ">=16" } }, "node_modules/color-convert": { @@ -3638,15 +4535,6 @@ "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=", "dev": true }, - "node_modules/color-support": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-support/-/color-support-1.1.3.tgz", - "integrity": "sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==", - "dev": true, - "bin": { - "color-support": "bin.js" - } - }, "node_modules/colorette": { "version": "2.0.16", "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.16.tgz", @@ -3682,51 +4570,17 @@ "integrity": "sha512-3Lc0sTIuX1jmY2K2RrXRJOND6KsRTX2D4v3+eu1PDptsuJZVK4LZc852eZa9I+avj0NrUKlTNgqvccNOH6mbGg==", "dev": true }, - "node_modules/component-emitter": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.0.tgz", - "integrity": "sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg==", - "dev": true - }, "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=" }, - "node_modules/concat-stream": { - "version": "1.6.2", - "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.2.tgz", - "integrity": "sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==", - "dev": true, - "engines": [ - "node >= 0.8" - ], - "dependencies": { - "buffer-from": "^1.0.0", - "inherits": "^2.0.3", - "readable-stream": "^2.2.2", - "typedarray": "^0.0.6" - } - }, - "node_modules/confusing-browser-globals": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/confusing-browser-globals/-/confusing-browser-globals-1.0.11.tgz", - "integrity": "sha512-JsPKdmh8ZkmnHxDk55FZ1TqVLvEQTvoByJZRN9jzI0UjxK/QgAmsphz7PGtqgPieQZ/CQcHWXCR7ATDNhGe+YA==", - "dev": true - }, "node_modules/console-browserify": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/console-browserify/-/console-browserify-1.2.0.tgz", "integrity": "sha512-ZMkYO/LkF17QvCPqM0gxw8yUzigAOZOSWSHg91FH6orS7vcEj5dVZTidN2fQ14yBSdg97RqhSNwLUXInd52OTA==", "dev": true }, - "node_modules/console-control-strings": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", - "integrity": "sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4=", - "dev": true, - "optional": true - }, "node_modules/constants-browserify": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/constants-browserify/-/constants-browserify-1.0.0.tgz", @@ -3734,17 +4588,39 @@ "dev": true }, "node_modules/content-disposition": { - "version": "0.5.3", - "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.3.tgz", - "integrity": "sha512-ExO0774ikEObIAEV9kDo50o+79VCUdEB6n6lzKgGwupcVeRlhrj3qGAfwq8G6uBJjkqLrhT0qEYFcWng8z1z0g==", + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", "dev": true, + "license": "MIT", "dependencies": { - "safe-buffer": "5.1.2" + "safe-buffer": "5.2.1" }, "engines": { "node": ">= 0.6" } }, + "node_modules/content-disposition/node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, "node_modules/continuation-local-storage": { "version": "3.2.1", "resolved": "https://registry.npmjs.org/continuation-local-storage/-/continuation-local-storage-3.2.1.tgz", @@ -3755,31 +4631,22 @@ } }, "node_modules/convert-source-map": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.6.0.tgz", - "integrity": "sha512-eFu7XigvxdZ1ETfbgPBohgyQ/Z++C0eEhTor0qRwBw9unw+L0/6V8wkSuGgzdThkiS5lSpdptOQPD8Ak40a+7A==", - "dev": true, - "dependencies": { - "safe-buffer": "~5.1.1" - } - }, - "node_modules/copy-descriptor": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/copy-descriptor/-/copy-descriptor-0.1.1.tgz", - "integrity": "sha1-Z29us8OZl8LuGsOpJP1hJHSPV40=", - "dev": true, - "engines": { - "node": ">=0.10.0" - } + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz", + "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==", + "dev": true }, "node_modules/copy-props": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/copy-props/-/copy-props-2.0.5.tgz", - "integrity": "sha512-XBlx8HSqrT0ObQwmSzM7WE5k8FxTV75h1DX1Z3n6NhQ/UYYAvInWYmG06vFt7hQZArE2fuO62aihiWIVQwh1sw==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/copy-props/-/copy-props-4.0.0.tgz", + "integrity": "sha512-bVWtw1wQLzzKiYROtvNlbJgxgBYt2bMJpkCbKmXM3xyijvcjjWXEk5nyrrT3bgJ7ODb19ZohE2T0Y3FgNPyoTw==", "dev": true, "dependencies": { - "each-props": "^1.3.2", + "each-props": "^3.0.0", "is-plain-object": "^5.0.0" + }, + "engines": { + "node": ">= 10.13.0" } }, "node_modules/copy-props/node_modules/is-plain-object": { @@ -3828,11 +4695,16 @@ } }, "node_modules/core-js-pure": { - "version": "3.1.4", - "resolved": "https://registry.npmjs.org/core-js-pure/-/core-js-pure-3.1.4.tgz", - "integrity": "sha512-uJ4Z7iPNwiu1foygbcZYJsJs1jiXrTTCvxfLDXNhI/I+NHbSIEyr548y4fcsCEyWY0XgfAG/qqaunJ1SThHenA==", + "version": "3.42.0", + "resolved": "https://registry.npmjs.org/core-js-pure/-/core-js-pure-3.42.0.tgz", + "integrity": "sha512-007bM04u91fF4kMgwom2I5cQxAFIy8jVulgr9eozILl/SZE53QOqnW/+vviC+wQWLv+AunBG+8Q0TLoeSsSxRQ==", "dev": true, - "hasInstallScript": true + "hasInstallScript": true, + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/core-js" + } }, "node_modules/core-util-is": { "version": "1.0.2", @@ -3841,13 +4713,14 @@ "dev": true }, "node_modules/create-ecdh": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/create-ecdh/-/create-ecdh-4.0.3.tgz", - "integrity": "sha512-GbEHQPMOswGpKXM9kCWVrremUcBmjteUaQ01T9rkKCPDXfUHX0IoP9LpHYo2NPFampa4e+/pFDc3jQdxrxQLaw==", + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/create-ecdh/-/create-ecdh-4.0.4.tgz", + "integrity": "sha512-mf+TCx8wWc9VpuxfP2ht0iSISLZnt0JgWlrOKZiNqyUZWnjIaCIVNQArMHnCZKfEYRg6IM7A+NeJoN8gf/Ws0A==", "dev": true, + "license": "MIT", "dependencies": { "bn.js": "^4.1.0", - "elliptic": "^6.0.0" + "elliptic": "^6.5.3" } }, "node_modules/create-hash": { @@ -3883,11 +4756,75 @@ "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", "dev": true }, + "node_modules/cross-env": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-env/-/cross-env-7.0.3.tgz", + "integrity": "sha512-+/HKd6EgcQCJGh2PSjZuUitQBQynKor4wrFbRg4DtAgS1aWO+gU52xpH7M9ScGgXSYmAVS9bIJ8EzuaGw0oNAw==", + "dev": true, + "dependencies": { + "cross-spawn": "^7.0.1" + }, + "bin": { + "cross-env": "src/bin/cross-env.js", + "cross-env-shell": "src/bin/cross-env-shell.js" + }, + "engines": { + "node": ">=10.14", + "npm": ">=6", + "yarn": ">=1" + } + }, + "node_modules/cross-env/node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/cross-env/node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/cross-env/node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cross-env/node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "engines": { + "node": ">=8" + } + }, "node_modules/cross-spawn": { - "version": "6.0.5", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz", - "integrity": "sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==", + "version": "6.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.6.tgz", + "integrity": "sha512-VqCUuhcd1iB+dsv8gxPttb5iZh/D0iubSP21g36KXdEuf6I5JiioesUVjpCdHV9MZRUfVFlvwtIUyPfxo5trtw==", "dev": true, + "license": "MIT", "dependencies": { "nice-try": "^1.0.4", "path-key": "^2.0.1", @@ -3899,6 +4836,15 @@ "node": ">=4.8" } }, + "node_modules/cross-spawn/node_modules/semver": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", + "dev": true, + "bin": { + "semver": "bin/semver" + } + }, "node_modules/cross-spawn/node_modules/which": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", @@ -3915,40 +4861,36 @@ "version": "0.0.2", "resolved": "https://registry.npmjs.org/crypt/-/crypt-0.0.2.tgz", "integrity": "sha1-iNf/fsDfuG9xPch7u0LQRNPmxBs=", + "dev": true, "engines": { "node": "*" } }, "node_modules/crypto-browserify": { - "version": "3.12.0", - "resolved": "https://registry.npmjs.org/crypto-browserify/-/crypto-browserify-3.12.0.tgz", - "integrity": "sha512-fz4spIh+znjO2VjL+IdhEpRJ3YN6sMzITSBijk6FK2UvTqruSQW+/cCZTSNsMiZNvUeq0CqurF+dAbyiGOY6Wg==", + "version": "3.12.1", + "resolved": "https://registry.npmjs.org/crypto-browserify/-/crypto-browserify-3.12.1.tgz", + "integrity": "sha512-r4ESw/IlusD17lgQi1O20Fa3qNnsckR126TdUuBgAu7GBYSIPvdNyONd3Zrxh0xCwA4+6w/TDArBPsMvhur+KQ==", "dev": true, + "license": "MIT", "dependencies": { - "browserify-cipher": "^1.0.0", - "browserify-sign": "^4.0.0", - "create-ecdh": "^4.0.0", - "create-hash": "^1.1.0", - "create-hmac": "^1.1.0", - "diffie-hellman": "^5.0.0", - "inherits": "^2.0.1", - "pbkdf2": "^3.0.3", - "public-encrypt": "^4.0.0", - "randombytes": "^2.0.0", - "randomfill": "^1.0.3" + "browserify-cipher": "^1.0.1", + "browserify-sign": "^4.2.3", + "create-ecdh": "^4.0.4", + "create-hash": "^1.2.0", + "create-hmac": "^1.1.7", + "diffie-hellman": "^5.0.3", + "hash-base": "~3.0.4", + "inherits": "^2.0.4", + "pbkdf2": "^3.1.2", + "public-encrypt": "^4.0.3", + "randombytes": "^2.1.0", + "randomfill": "^1.0.4" }, "engines": { - "node": "*" - } - }, - "node_modules/d": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/d/-/d-1.0.1.tgz", - "integrity": "sha512-m62ShEObQ39CfralilEQRjH6oAMtNCV1xJyEx5LpRYUVN+EviphDgUc/F3hnYbADmkiNs67Y+3ylmlG7Lnu+FA==", - "dev": true, - "dependencies": { - "es5-ext": "^0.10.50", - "type": "^1.0.1" + "node": ">= 0.10" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, "node_modules/damerau-levenshtein": { @@ -3957,6 +4899,57 @@ "integrity": "sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA==", "dev": true }, + "node_modules/data-view-buffer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.1.tgz", + "integrity": "sha512-0lht7OugA5x3iJLOWFhWK/5ehONdprk0ISXqVFn/NFrDu+cuc8iADFrGQz5BnRK7LLU3JmkbXSxaqX+/mXYtUA==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.6", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/data-view-byte-length": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/data-view-byte-length/-/data-view-byte-length-1.0.1.tgz", + "integrity": "sha512-4J7wRJD3ABAzr8wP+OcIcqq2dlUKp4DVflx++hs5h5ZKydWMI6/D/fAot+yh6g2tHh8fLFTvNOaVN357NvSrOQ==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.7", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/data-view-byte-offset": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/data-view-byte-offset/-/data-view-byte-offset-1.0.0.tgz", + "integrity": "sha512-t/Ygsytq+R995EJ5PZlD4Cu56sWa8InXySaViRzw9apusqsOO2bQP+SbYzAhR0pFKoB+43lYy8rWban9JSuXnA==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.6", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/debug": { "version": "2.6.9", "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", @@ -3986,6 +4979,7 @@ "resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.2.tgz", "integrity": "sha512-FqUYQ+8o158GyGTrMFJms9qh3CqTKvAqgqsTnkLI8sKu0028orqBhxNMFkFen0zGyg6epACD32pjVk58ngIErQ==", "dev": true, + "license": "MIT", "engines": { "node": ">=0.10" } @@ -3995,6 +4989,7 @@ "resolved": "https://registry.npmjs.org/decompress/-/decompress-4.2.1.tgz", "integrity": "sha512-e48kc2IjU+2Zw8cTb6VZcJQ3lgVbS4uuB1TfCHbiZIP/haNXm+SVyhu+87jts5/3ROpd82GSVCoNs/z8l4ZOaQ==", "dev": true, + "license": "MIT", "dependencies": { "decompress-tar": "^4.0.0", "decompress-tarbz2": "^4.0.0", @@ -4012,8 +5007,9 @@ "node_modules/decompress-response": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-3.3.0.tgz", - "integrity": "sha1-gKTdMjdIOEv6JICDYirt7Jgq3/M=", + "integrity": "sha512-BzRPQuY1ip+qDonAOz42gRm/pg9F768C+npV/4JOsxRC2sq+Rlk+Q4ZCAsOhnIaMrgarILY+RMUIvMmmX1qAEA==", "dev": true, + "license": "MIT", "dependencies": { "mimic-response": "^1.0.0" }, @@ -4026,6 +5022,7 @@ "resolved": "https://registry.npmjs.org/decompress-tar/-/decompress-tar-4.1.1.tgz", "integrity": "sha512-JdJMaCrGpB5fESVyxwpCx4Jdj2AagLmv3y58Qy4GE6HMVjWz1FeVQk1Ct4Kye7PftcdOo/7U7UKzYBJgqnGeUQ==", "dev": true, + "license": "MIT", "dependencies": { "file-type": "^5.2.0", "is-stream": "^1.1.0", @@ -4038,8 +5035,9 @@ "node_modules/decompress-tar/node_modules/file-type": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/file-type/-/file-type-5.2.0.tgz", - "integrity": "sha1-LdvqfHP/42No365J3DOMBYwritY=", + "integrity": "sha512-Iq1nJ6D2+yIO4c8HHg4fyVb8mAJieo1Oloy1mLLaB2PvezNedhBVm+QU7g0qM42aiMbRXTxKKwGD17rjKNJYVQ==", "dev": true, + "license": "MIT", "engines": { "node": ">=4" } @@ -4049,6 +5047,7 @@ "resolved": "https://registry.npmjs.org/decompress-tarbz2/-/decompress-tarbz2-4.1.1.tgz", "integrity": "sha512-s88xLzf1r81ICXLAVQVzaN6ZmX4A6U4z2nMbOwobxkLoIIfjVMBg7TeguTUXkKeXni795B6y5rnvDw7rxhAq9A==", "dev": true, + "license": "MIT", "dependencies": { "decompress-tar": "^4.1.0", "file-type": "^6.1.0", @@ -4065,6 +5064,7 @@ "resolved": "https://registry.npmjs.org/file-type/-/file-type-6.2.0.tgz", "integrity": "sha512-YPcTBDV+2Tm0VqjybVd32MHdlEGAtuxS3VAYsumFokDSMG+ROT5wawGlnHDoz7bfMcMDt9hxuXvXwoKUx2fkOg==", "dev": true, + "license": "MIT", "engines": { "node": ">=4" } @@ -4074,6 +5074,7 @@ "resolved": "https://registry.npmjs.org/decompress-targz/-/decompress-targz-4.1.1.tgz", "integrity": "sha512-4z81Znfr6chWnRDNfFNqLwPvm4db3WuZkqV+UgXQzSngG3CEKdBkw5jrv3axjjL96glyiiKjsxJG3X6WBZwX3w==", "dev": true, + "license": "MIT", "dependencies": { "decompress-tar": "^4.1.1", "file-type": "^5.2.0", @@ -4086,8 +5087,9 @@ "node_modules/decompress-targz/node_modules/file-type": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/file-type/-/file-type-5.2.0.tgz", - "integrity": "sha1-LdvqfHP/42No365J3DOMBYwritY=", + "integrity": "sha512-Iq1nJ6D2+yIO4c8HHg4fyVb8mAJieo1Oloy1mLLaB2PvezNedhBVm+QU7g0qM42aiMbRXTxKKwGD17rjKNJYVQ==", "dev": true, + "license": "MIT", "engines": { "node": ">=4" } @@ -4095,8 +5097,9 @@ "node_modules/decompress-unzip": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/decompress-unzip/-/decompress-unzip-4.0.1.tgz", - "integrity": "sha1-3qrM39FK6vhVePczroIQ+bSEj2k=", + "integrity": "sha512-1fqeluvxgnn86MOh66u8FjbtJpAFv5wgCT9Iw8rcBqQcCo5tO8eiJw7NNTrvt9n4CRBVq7CstiS922oPgyGLrw==", "dev": true, + "license": "MIT", "dependencies": { "file-type": "^3.8.0", "get-stream": "^2.2.0", @@ -4110,8 +5113,9 @@ "node_modules/decompress-unzip/node_modules/file-type": { "version": "3.9.0", "resolved": "https://registry.npmjs.org/file-type/-/file-type-3.9.0.tgz", - "integrity": "sha1-JXoHg4TR24CHvESdEH1SpSZyuek=", + "integrity": "sha512-RLoqTXE8/vPmMuTI88DAzhMYC99I8BWv7zYP4A1puo5HIjEJ5EX48ighy4ZyKMG9EDXxBgW6e++cn7d1xuFghA==", "dev": true, + "license": "MIT", "engines": { "node": ">=0.10.0" } @@ -4119,8 +5123,9 @@ "node_modules/decompress-unzip/node_modules/get-stream": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-2.3.1.tgz", - "integrity": "sha1-Xzj5PzRgCWZu4BUKBUFn+Rvdld4=", + "integrity": "sha512-AUGhbbemXxrZJRD5cDvKtQxLuYaIbNtDTK8YqupCI393Q2KSTreEsLUN3ZxAWFGiKTzL6nKuzfcIvieflUX9qA==", "dev": true, + "license": "MIT", "dependencies": { "object-assign": "^4.0.1", "pinkie-promise": "^2.0.0" @@ -4132,8 +5137,9 @@ "node_modules/decompress-unzip/node_modules/pify": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", - "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=", + "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", "dev": true, + "license": "MIT", "engines": { "node": ">=0.10.0" } @@ -4143,6 +5149,7 @@ "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-1.3.0.tgz", "integrity": "sha512-2w31R7SJtieJJnQtGc7RVL2StM2vGYVfqUOvUDxH6bC6aJTxPxTF0GnIgCyu7tjockiUWAYQRbxa7vKn34s5sQ==", "dev": true, + "license": "MIT", "dependencies": { "pify": "^3.0.0" }, @@ -4153,8 +5160,9 @@ "node_modules/decompress/node_modules/make-dir/node_modules/pify": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", - "integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=", + "integrity": "sha512-C3FsVNH1udSEX48gGX1xfvwTWfsYWj5U+8/uK15BGzIGrKoUpghX8hWZwa/OFnakBiiVNmBvemTJR5mcy7iPcg==", "dev": true, + "license": "MIT", "engines": { "node": ">=4" } @@ -4162,8 +5170,9 @@ "node_modules/decompress/node_modules/pify": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", - "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=", + "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", "dev": true, + "license": "MIT", "engines": { "node": ">=0.10.0" } @@ -4180,23 +5189,6 @@ "node": ">=0.12" } }, - "node_modules/deep-equal": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-1.1.1.tgz", - "integrity": "sha512-yd9c5AdiqVcR+JjcwUQb9DkhJc8ngNr0MahEBGvDiJw8puWab2yZlh+nkasOnZP+EGTAP6rRp2JzJhJZzvNF8g==", - "dev": true, - "dependencies": { - "is-arguments": "^1.0.4", - "is-date-object": "^1.0.1", - "is-regex": "^1.0.4", - "object-is": "^1.0.1", - "object-keys": "^1.1.1", - "regexp.prototype.flags": "^1.2.0" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/deep-extend": { "version": "0.6.0", "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", @@ -4213,27 +5205,6 @@ "integrity": "sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ=", "dev": true }, - "node_modules/default-compare": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/default-compare/-/default-compare-1.0.0.tgz", - "integrity": "sha512-QWfXlM0EkAbqOCbD/6HjdwT19j7WCkMyiRhWilc4H9/5h/RzTF9gv5LYh1+CmDV5d1rki6KAWLtQale0xt20eQ==", - "dev": true, - "dependencies": { - "kind-of": "^5.0.2" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/default-compare/node_modules/kind-of": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-5.1.0.tgz", - "integrity": "sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/default-require-extensions": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/default-require-extensions/-/default-require-extensions-3.0.0.tgz", @@ -4255,76 +5226,47 @@ "node": ">=8" } }, - "node_modules/default-resolution": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/default-resolution/-/default-resolution-2.0.0.tgz", - "integrity": "sha1-vLgrqnKtebQmp2cy8aga1t8m1oQ=", - "dev": true, - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/define-properties": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.3.tgz", - "integrity": "sha512-3MqfYKj2lLzdMSf8ZIZE/V+Zuy+BgD6f164e8K2w7dgnpKArBDerGYpM46IYYcjnkdPNMjPk9A6VFB8+3SKlXQ==", + "node_modules/define-data-property": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", "dev": true, "dependencies": { - "object-keys": "^1.0.12" + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" }, "engines": { "node": ">= 0.4" - } - }, - "node_modules/define-property": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-2.0.2.tgz", - "integrity": "sha512-jwK2UV4cnPpbcG7+VRARKTZPUWowwXA8bzH5NP6ud0oeAxyYPuGZUAC7hMugpCdz4BeSZl2Dl9k66CHJ/46ZYQ==", - "dev": true, - "dependencies": { - "is-descriptor": "^1.0.2", - "isobject": "^3.0.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/define-property/node_modules/is-accessor-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", - "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", - "dev": true, - "dependencies": { - "kind-of": "^6.0.0" }, - "engines": { - "node": ">=0.10.0" + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/define-property/node_modules/is-data-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", - "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", + "node_modules/define-lazy-prop": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-2.0.0.tgz", + "integrity": "sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og==", "dev": true, - "dependencies": { - "kind-of": "^6.0.0" - }, "engines": { - "node": ">=0.10.0" + "node": ">=8" } }, - "node_modules/define-property/node_modules/is-descriptor": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", - "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", + "node_modules/define-properties": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", + "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", "dev": true, "dependencies": { - "is-accessor-descriptor": "^1.0.0", - "is-data-descriptor": "^1.0.0", - "kind-of": "^6.0.2" + "define-data-property": "^1.0.1", + "has-property-descriptors": "^1.0.0", + "object-keys": "^1.1.1" }, "engines": { - "node": ">=0.10.0" + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, "node_modules/del": { @@ -4357,13 +5299,6 @@ "node": ">=0.4.0" } }, - "node_modules/delegates": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", - "integrity": "sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o=", - "dev": true, - "optional": true - }, "node_modules/des.js": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/des.js/-/des.js-1.0.0.tgz", @@ -4377,55 +5312,48 @@ "node_modules/detect-file": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/detect-file/-/detect-file-1.0.0.tgz", - "integrity": "sha1-8NZtA2cqglyxtzvbP+YjEMjlUrc=", + "integrity": "sha512-DtCOLG98P007x7wiiOmfI0fi3eIKyWiLTGJ2MDnVi/E04lWGbf+JzrRHMm0rgIIZJGtHpKpbVgLWHrv8xXpc3Q==", "dev": true, "engines": { "node": ">=0.10.0" } }, "node_modules/detect-libc": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz", - "integrity": "sha1-+hN8S9aY7fVc1c0CrFWfkaTEups=", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.1.tgz", + "integrity": "sha512-463v3ZeIrcWtdgIg6vI6XUncguvr2TnGl4SzDXinkt9mSLpBJKXT3mW6xT3VQdDN11+WVs29pgvivTc4Lp8v+w==", "dev": true, "optional": true, - "bin": { - "detect-libc": "bin/detect-libc.js" - }, "engines": { - "node": ">=0.10" + "node": ">=8" } }, "node_modules/diagnostic-channel": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/diagnostic-channel/-/diagnostic-channel-1.1.0.tgz", - "integrity": "sha512-fwujyMe1gj6rk6dYi9hMZm0c8Mz8NDMVl2LB4iaYh3+LIAThZC8RKFGXWG0IML2OxAit/ZFRgZhMkhQ3d/bobQ==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/diagnostic-channel/-/diagnostic-channel-1.1.1.tgz", + "integrity": "sha512-r2HV5qFkUICyoaKlBEpLKHjxMXATUf/l+h8UZPGBHGLy4DDiY2sOLcIctax4eRnTw5wH2jTMExLntGPJ8eOJxw==", "dependencies": { - "semver": "^5.3.0" + "semver": "^7.5.3" } }, "node_modules/diagnostic-channel-publishers": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/diagnostic-channel-publishers/-/diagnostic-channel-publishers-1.0.5.tgz", - "integrity": "sha512-dJwUS0915pkjjimPJVDnS/QQHsH0aOYhnZsLJdnZIMOrB+csj8RnZhWTuwnm8R5v3Z7OZs+ksv5luC14DGB7eg==", + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/diagnostic-channel-publishers/-/diagnostic-channel-publishers-1.0.7.tgz", + "integrity": "sha512-SEECbY5AiVt6DfLkhkaHNeshg1CogdLLANA8xlG/TKvS+XUgvIKl7VspJGYiEdL5OUyzMVnr7o0AwB7f+/Mjtg==", "peerDependencies": { "diagnostic-channel": "*" } }, "node_modules/diff": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", - "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.4.tgz", + "integrity": "sha512-X07nttJQkwkfKfvTPG/KSnE2OMdcUCao6+eXF3wmnIQRn2aPAHH3VxDbDOdegkd6JbPsXqShpvEOHfAT+nCNwQ==", "dev": true, + "license": "BSD-3-Clause", "engines": { "node": ">=0.3.1" } }, - "node_modules/diff-match-patch": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/diff-match-patch/-/diff-match-patch-1.0.5.tgz", - "integrity": "sha512-IayShXAgj/QMXgB0IWmKx+rOPuGMhqm5w6jvFxmVenXKIzRqTAAsbBPT3kWQeGANj3jGgvcvv4yK6SxqYmikgw==" - }, "node_modules/diffie-hellman": { "version": "5.0.3", "resolved": "https://registry.npmjs.org/diffie-hellman/-/diffie-hellman-5.0.3.tgz", @@ -4476,6 +5404,7 @@ "resolved": "https://registry.npmjs.org/download/-/download-8.0.0.tgz", "integrity": "sha512-ASRY5QhDk7FK+XrQtQyvhpDKanLluEEQtWl/J7Lxuf/b+i8RYh997QeXvL85xitrmRKVlx9c7eTrcRdq2GS4eA==", "dev": true, + "license": "MIT", "dependencies": { "archive-type": "^4.0.0", "content-disposition": "^0.5.2", @@ -4493,23 +5422,12 @@ "node": ">=10" } }, - "node_modules/download/node_modules/get-stream": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-4.1.0.tgz", - "integrity": "sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w==", - "dev": true, - "dependencies": { - "pump": "^3.0.0" - }, - "engines": { - "node": ">=6" - } - }, "node_modules/download/node_modules/make-dir": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-2.1.0.tgz", "integrity": "sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA==", "dev": true, + "license": "MIT", "dependencies": { "pify": "^4.0.1", "semver": "^5.6.0" @@ -4518,14 +5436,28 @@ "node": ">=6" } }, - "node_modules/download/node_modules/pump": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", - "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", + "node_modules/download/node_modules/semver": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", "dependencies": { - "end-of-stream": "^1.1.0", - "once": "^1.3.1" + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" } }, "node_modules/duplexer": { @@ -4534,20 +5466,12 @@ "integrity": "sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg==", "dev": true }, - "node_modules/duplexer2": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/duplexer2/-/duplexer2-0.1.4.tgz", - "integrity": "sha1-ixLauHjA1p4+eJEFFmKjL8a93ME=", - "dev": true, - "dependencies": { - "readable-stream": "^2.0.2" - } - }, "node_modules/duplexer3": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/duplexer3/-/duplexer3-0.1.4.tgz", - "integrity": "sha1-7gHdHKwO08vH/b6jfcCo8c4ALOI=", - "dev": true + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/duplexer3/-/duplexer3-0.1.5.tgz", + "integrity": "sha512-1A8za6ws41LQgv9HrE/66jyC5yuSjQ3L/KOpFtoBilsAK2iA2wuS5rTt1OCzIvtS2V7nVmedsUU+DGRcjBmOYA==", + "dev": true, + "license": "BSD-3-Clause" }, "node_modules/duplexify": { "version": "3.7.1", @@ -4562,26 +5486,55 @@ } }, "node_modules/each-props": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/each-props/-/each-props-1.3.2.tgz", - "integrity": "sha512-vV0Hem3zAGkJAyU7JSjixeU66rwdynTAa1vofCrSA5fEln+m67Az9CcnkVD776/fsN/UjIWmBDoNRS6t6G9RfA==", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/each-props/-/each-props-3.0.0.tgz", + "integrity": "sha512-IYf1hpuWrdzse/s/YJOrFmU15lyhSzxelNVAHTEG3DtP4QsLTWZUzcUL3HMXmKQxXpa4EIrBPpwRgj0aehdvAw==", "dev": true, "dependencies": { - "is-plain-object": "^2.0.1", + "is-plain-object": "^5.0.0", "object.defaults": "^1.1.0" + }, + "engines": { + "node": ">= 10.13.0" + } + }, + "node_modules/each-props/node_modules/is-plain-object": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-5.0.0.tgz", + "integrity": "sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "dev": true, + "license": "MIT" + }, + "node_modules/ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "dev": true, + "dependencies": { + "safe-buffer": "^5.0.1" } }, "node_modules/electron-to-chromium": { - "version": "1.4.92", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.92.tgz", - "integrity": "sha512-YAVbvQIcDE/IJ/vzDMjD484/hsRbFPW2qXJPaYTfOhtligmfYEYOep+5QojpaEU9kq6bMvNeC2aG7arYvTHYsA==", + "version": "1.5.286", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.286.tgz", + "integrity": "sha512-9tfDXhJ4RKFNerfjdCcZfufu49vg620741MNs26a9+bhLThdB+plgMeou98CAaHu/WATj2iHOOHTp1hWtABj2A==", "dev": true }, "node_modules/elliptic": { - "version": "6.5.4", - "resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.5.4.tgz", - "integrity": "sha512-iLhC6ULemrljPZb+QutR5TQGB+pdW6KGD5RSegS+8sorOZT+rdQFbsQFJgvN3eRqNALqJer4oQ16YvJHlU8hzQ==", + "version": "6.6.1", + "resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.6.1.tgz", + "integrity": "sha512-RaddvvMatK2LJHqFJ+YA4WysVN5Ita9E35botqIYspQ4TkRAlCicdzKOjlyv/1Za5RyTNn7di//eEV0uTAfe3g==", "dev": true, + "license": "MIT", "dependencies": { "bn.js": "^4.11.9", "brorand": "^1.1.0", @@ -4592,12 +5545,6 @@ "minimalistic-crypto-utils": "^1.0.1" } }, - "node_modules/elliptic/node_modules/bn.js": { - "version": "4.12.0", - "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.0.tgz", - "integrity": "sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==", - "dev": true - }, "node_modules/emitter-listener": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/emitter-listener/-/emitter-listener-1.1.2.tgz", @@ -4622,48 +5569,27 @@ } }, "node_modules/end-of-stream": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.1.tgz", - "integrity": "sha512-1MkrZNvWTKCaigbn+W15elq2BB/L22nqrSY5DKlo3X6+vclJm8Bb5djXJBmEX6fS3+zCh/F4VBK5Z2KxJt4s2Q==", + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", + "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", "dev": true, "dependencies": { "once": "^1.4.0" } }, "node_modules/enhanced-resolve": { - "version": "5.12.0", - "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.12.0.tgz", - "integrity": "sha512-QHTXI/sZQmko1cbDoNAa3mJ5qhWUUNAq3vR0/YiD379fWQrcfuoX1+HW2S0MTt7XmoPLapdaDKUtelUSPic7hQ==", + "version": "5.19.0", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.19.0.tgz", + "integrity": "sha512-phv3E1Xl4tQOShqSte26C7Fl84EwUdZsyOuSSk9qtAGyyQs2s3jJzComh+Abf4g187lUUAvH+H26omrqia2aGg==", "dev": true, "dependencies": { "graceful-fs": "^4.2.4", - "tapable": "^2.2.0" + "tapable": "^2.3.0" }, "engines": { "node": ">=10.13.0" } }, - "node_modules/enquirer": { - "version": "2.3.6", - "resolved": "https://registry.npmjs.org/enquirer/-/enquirer-2.3.6.tgz", - "integrity": "sha512-yjNnPr315/FjS4zIsUxYguYUPP2e1NK4d7E7ZOLiyYCcbFBiTMyID+2wvm2w6+pZ/odMA7cRkjhsPbltwBOrLg==", - "dev": true, - "dependencies": { - "ansi-colors": "^4.1.1" - }, - "engines": { - "node": ">=8.6" - } - }, - "node_modules/enquirer/node_modules/ansi-colors": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.1.tgz", - "integrity": "sha512-JoX0apGbHaUJBNl6yF+p6JAFYZ666/hhCGKN5t9QFjbJQKUU/g8MNbFDbvfrgKXvI1QpZplPOnwIo99lX/AAmA==", - "dev": true, - "engines": { - "node": ">=6" - } - }, "node_modules/entities": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/entities/-/entities-2.1.0.tgz", @@ -4685,47 +5611,58 @@ "node": ">=4" } }, - "node_modules/error-ex": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", - "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", - "dev": true, - "dependencies": { - "is-arrayish": "^0.2.1" - } - }, - "node_modules/error-ex/node_modules/is-arrayish": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", - "integrity": "sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0=", - "dev": true - }, "node_modules/es-abstract": { - "version": "1.19.2", - "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.19.2.tgz", - "integrity": "sha512-gfSBJoZdlL2xRiOCy0g8gLMryhoe1TlimjzU99L/31Z8QEGIhVQI+EWwt5lT+AuU9SnorVupXFqqOGqGfsyO6w==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.2", + "version": "1.23.3", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.23.3.tgz", + "integrity": "sha512-e+HfNH61Bj1X9/jLc5v1owaLYuHdeHHSQlkhCBiTK8rBvKaULl/beGMxwrMXjpYrv4pz22BlY570vVePA2ho4A==", + "dev": true, + "dependencies": { + "array-buffer-byte-length": "^1.0.1", + "arraybuffer.prototype.slice": "^1.0.3", + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.7", + "data-view-buffer": "^1.0.1", + "data-view-byte-length": "^1.0.1", + "data-view-byte-offset": "^1.0.0", + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "es-set-tostringtag": "^2.0.3", "es-to-primitive": "^1.2.1", - "function-bind": "^1.1.1", - "get-intrinsic": "^1.1.1", - "get-symbol-description": "^1.0.0", - "has": "^1.0.3", + "function.prototype.name": "^1.1.6", + "get-intrinsic": "^1.2.4", + "get-symbol-description": "^1.0.2", + "globalthis": "^1.0.3", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.2", + "has-proto": "^1.0.3", "has-symbols": "^1.0.3", - "internal-slot": "^1.0.3", - "is-callable": "^1.2.4", - "is-negative-zero": "^2.0.2", + "hasown": "^2.0.2", + "internal-slot": "^1.0.7", + "is-array-buffer": "^3.0.4", + "is-callable": "^1.2.7", + "is-data-view": "^1.0.1", + "is-negative-zero": "^2.0.3", "is-regex": "^1.1.4", - "is-shared-array-buffer": "^1.0.1", + "is-shared-array-buffer": "^1.0.3", "is-string": "^1.0.7", + "is-typed-array": "^1.1.13", "is-weakref": "^1.0.2", - "object-inspect": "^1.12.0", + "object-inspect": "^1.13.1", "object-keys": "^1.1.1", - "object.assign": "^4.1.2", - "string.prototype.trimend": "^1.0.4", - "string.prototype.trimstart": "^1.0.4", - "unbox-primitive": "^1.0.1" + "object.assign": "^4.1.5", + "regexp.prototype.flags": "^1.5.2", + "safe-array-concat": "^1.1.2", + "safe-regex-test": "^1.0.3", + "string.prototype.trim": "^1.2.9", + "string.prototype.trimend": "^1.0.8", + "string.prototype.trimstart": "^1.0.8", + "typed-array-buffer": "^1.0.2", + "typed-array-byte-length": "^1.0.1", + "typed-array-byte-offset": "^1.0.2", + "typed-array-length": "^1.0.6", + "unbox-primitive": "^1.0.2", + "which-typed-array": "^1.1.15" }, "engines": { "node": ">= 0.4" @@ -4734,12 +5671,64 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "engines": { + "node": ">= 0.4" + } + }, "node_modules/es-module-lexer": { - "version": "0.9.3", - "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-0.9.3.tgz", - "integrity": "sha512-1HQ2M2sPtxwnvOvT1ZClHyQDiggdNjURWpY2we6aMKCQiUVxTmVs2UYPLIrD84sS+kMdUwfBSylbJPwNnBrnHQ==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.0.0.tgz", + "integrity": "sha512-5POEcUuZybH7IdmGsD8wlf0AI55wMecM9rVBTI/qEAy2c1kTOm3DjFYjrBdI2K3BaJjJYfYFeRtM0t9ssnRuxw==", "dev": true }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-shim-unscopables": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/es-shim-unscopables/-/es-shim-unscopables-1.0.2.tgz", + "integrity": "sha512-J3yBRXCzDu4ULnQwxyToo/OjdMx6akgVC7K6few0a7F/0wLtmKKN7I73AH5T2836UuXRqN7Qg+IIUw/+YJksRw==", + "dev": true, + "dependencies": { + "hasown": "^2.0.0" + } + }, "node_modules/es-to-primitive": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.1.tgz", @@ -4757,72 +5746,22 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/es5-ext": { - "version": "0.10.53", - "resolved": "https://registry.npmjs.org/es5-ext/-/es5-ext-0.10.53.tgz", - "integrity": "sha512-Xs2Stw6NiNHWypzRTY1MtaG/uJlwCk8kH81920ma8mvN8Xq1gsfhZvpkImLQArw8AHnv8MT2I45J3c0R8slE+Q==", - "dev": true, - "dependencies": { - "es6-iterator": "~2.0.3", - "es6-symbol": "~3.1.3", - "next-tick": "~1.0.0" - } - }, - "node_modules/es5-ext/node_modules/next-tick": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/next-tick/-/next-tick-1.0.0.tgz", - "integrity": "sha1-yobR/ogoFpsBICCOPchCS524NCw=", - "dev": true - }, "node_modules/es6-error": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/es6-error/-/es6-error-4.1.1.tgz", "integrity": "sha512-Um/+FxMr9CISWh0bi5Zv0iOD+4cFh5qLeks1qhAopKVAJw3drgKbKySikp7wGhDL0HPeaja0P5ULZrxLkniUVg==", "dev": true }, - "node_modules/es6-iterator": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/es6-iterator/-/es6-iterator-2.0.3.tgz", - "integrity": "sha1-p96IkUGgWpSwhUQDstCg+/qY87c=", - "dev": true, - "dependencies": { - "d": "1", - "es5-ext": "^0.10.35", - "es6-symbol": "^3.1.1" - } - }, "node_modules/es6-object-assign": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/es6-object-assign/-/es6-object-assign-1.1.0.tgz", "integrity": "sha1-wsNYJlYkfDnqEHyx5mUrb58kUjw=", "dev": true }, - "node_modules/es6-symbol": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/es6-symbol/-/es6-symbol-3.1.3.tgz", - "integrity": "sha512-NJ6Yn3FuDinBaBRWl/q5X/s4koRHBrgKAu+yGI6JCBeiu3qrcbJhwT2GeR/EXVfylRk8dpQVJoLEFhK+Mu31NA==", - "dev": true, - "dependencies": { - "d": "^1.0.1", - "ext": "^1.1.2" - } - }, - "node_modules/es6-weak-map": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/es6-weak-map/-/es6-weak-map-2.0.3.tgz", - "integrity": "sha512-p5um32HOTO1kP+w7PRnB+5lQ43Z6muuMuIMffvDN8ZB4GcnjLBV6zGStpbASIMk4DCAvEaamhe2zhyCb/QXXsA==", - "dev": true, - "dependencies": { - "d": "1", - "es5-ext": "^0.10.46", - "es6-iterator": "^2.0.3", - "es6-symbol": "^3.1.1" - } - }, "node_modules/escalade": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", - "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==", + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", "dev": true, "engines": { "node": ">=6" @@ -4838,101 +5777,62 @@ } }, "node_modules/eslint": { - "version": "7.32.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-7.32.0.tgz", - "integrity": "sha512-VHZ8gX+EDfz+97jGcgyGCyRia/dPOd6Xh9yPv8Bl1+SoaIwD+a/vlrOmGRUyOYu7MwUhc7CxqeaDZU13S4+EpA==", + "version": "8.57.1", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.1.tgz", + "integrity": "sha512-ypowyDxpVSYpkXr9WPv2PAZCtNip1Mv5KTW0SCurXv/9iOpcrH9PaqUElksqEB6pChqHGDRCFTyrZlGhnLNGiA==", + "deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.", "dev": true, + "license": "MIT", "dependencies": { - "@babel/code-frame": "7.12.11", - "@eslint/eslintrc": "^0.4.3", - "@humanwhocodes/config-array": "^0.5.0", - "ajv": "^6.10.0", + "@eslint-community/eslint-utils": "^4.2.0", + "@eslint-community/regexpp": "^4.6.1", + "@eslint/eslintrc": "^2.1.4", + "@eslint/js": "8.57.1", + "@humanwhocodes/config-array": "^0.13.0", + "@humanwhocodes/module-importer": "^1.0.1", + "@nodelib/fs.walk": "^1.2.8", + "@ungap/structured-clone": "^1.2.0", + "ajv": "^6.12.4", "chalk": "^4.0.0", "cross-spawn": "^7.0.2", - "debug": "^4.0.1", + "debug": "^4.3.2", "doctrine": "^3.0.0", - "enquirer": "^2.3.5", "escape-string-regexp": "^4.0.0", - "eslint-scope": "^5.1.1", - "eslint-utils": "^2.1.0", - "eslint-visitor-keys": "^2.0.0", - "espree": "^7.3.1", - "esquery": "^1.4.0", + "eslint-scope": "^7.2.2", + "eslint-visitor-keys": "^3.4.3", + "espree": "^9.6.1", + "esquery": "^1.4.2", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", "file-entry-cache": "^6.0.1", - "functional-red-black-tree": "^1.0.1", - "glob-parent": "^5.1.2", - "globals": "^13.6.0", - "ignore": "^4.0.6", - "import-fresh": "^3.0.0", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "globals": "^13.19.0", + "graphemer": "^1.4.0", + "ignore": "^5.2.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", - "js-yaml": "^3.13.1", + "is-path-inside": "^3.0.3", + "js-yaml": "^4.1.0", "json-stable-stringify-without-jsonify": "^1.0.1", "levn": "^0.4.1", "lodash.merge": "^4.6.2", - "minimatch": "^3.0.4", + "minimatch": "^3.1.2", "natural-compare": "^1.4.0", - "optionator": "^0.9.1", - "progress": "^2.0.0", - "regexpp": "^3.1.0", - "semver": "^7.2.1", - "strip-ansi": "^6.0.0", - "strip-json-comments": "^3.1.0", - "table": "^6.0.9", - "text-table": "^0.2.0", - "v8-compile-cache": "^2.0.3" + "optionator": "^0.9.3", + "strip-ansi": "^6.0.1", + "text-table": "^0.2.0" }, "bin": { "eslint": "bin/eslint.js" }, "engines": { - "node": "^10.12.0 || >=12.0.0" + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" }, "funding": { "url": "https://opencollective.com/eslint" } }, - "node_modules/eslint-config-airbnb": { - "version": "18.2.1", - "resolved": "https://registry.npmjs.org/eslint-config-airbnb/-/eslint-config-airbnb-18.2.1.tgz", - "integrity": "sha512-glZNDEZ36VdlZWoxn/bUR1r/sdFKPd1mHPbqUtkctgNG4yT2DLLtJ3D+yCV+jzZCc2V1nBVkmdknOJBZ5Hc0fg==", - "dev": true, - "dependencies": { - "eslint-config-airbnb-base": "^14.2.1", - "object.assign": "^4.1.2", - "object.entries": "^1.1.2" - }, - "engines": { - "node": ">= 6" - }, - "peerDependencies": { - "eslint": "^5.16.0 || ^6.8.0 || ^7.2.0", - "eslint-plugin-import": "^2.22.1", - "eslint-plugin-jsx-a11y": "^6.4.1", - "eslint-plugin-react": "^7.21.5", - "eslint-plugin-react-hooks": "^4 || ^3 || ^2.3.0 || ^1.7.0" - } - }, - "node_modules/eslint-config-airbnb-base": { - "version": "14.2.1", - "resolved": "https://registry.npmjs.org/eslint-config-airbnb-base/-/eslint-config-airbnb-base-14.2.1.tgz", - "integrity": "sha512-GOrQyDtVEc1Xy20U7vsB2yAoB4nBlfH5HZJeatRXHleO+OS5Ot+MWij4Dpltw4/DyIkqUfqz1epfhVR5XWWQPA==", - "dev": true, - "dependencies": { - "confusing-browser-globals": "^1.0.10", - "object.assign": "^4.1.2", - "object.entries": "^1.1.2" - }, - "engines": { - "node": ">= 6" - }, - "peerDependencies": { - "eslint": "^5.16.0 || ^6.8.0 || ^7.2.0", - "eslint-plugin-import": "^2.22.1" - } - }, "node_modules/eslint-config-prettier": { "version": "8.5.0", "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-8.5.0.tgz", @@ -4946,136 +5846,102 @@ } }, "node_modules/eslint-import-resolver-node": { - "version": "0.3.6", - "resolved": "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.6.tgz", - "integrity": "sha512-0En0w03NRVMn9Uiyn8YRPDKvWjxCWkslUEhGNTdGx15RvPJYQ+lbOlqrlNI2vEAs4pDYK4f/HN2TbDmk5TP0iw==", + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.9.tgz", + "integrity": "sha512-WFj2isz22JahUv+B788TlO3N6zL3nNJGU8CcZbPZvVEkBPaJdCV4vy5wyghty5ROFbCRnm132v8BScu5/1BQ8g==", "dev": true, "dependencies": { "debug": "^3.2.7", - "resolve": "^1.20.0" + "is-core-module": "^2.13.0", + "resolve": "^1.22.4" } }, "node_modules/eslint-import-resolver-node/node_modules/debug": { "version": "3.2.7", "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", - "dev": true, - "dependencies": { - "ms": "^2.1.1" - } - }, - "node_modules/eslint-module-utils": { - "version": "2.7.3", - "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.7.3.tgz", - "integrity": "sha512-088JEC7O3lDZM9xGe0RerkOMd0EjFl+Yvd1jPWIkMT5u3H9+HC34mWWPnqPrN13gieT9pBOO+Qt07Nb/6TresQ==", - "dev": true, - "dependencies": { - "debug": "^3.2.7", - "find-up": "^2.1.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/eslint-module-utils/node_modules/debug": { - "version": "3.2.7", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", - "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", - "dev": true, - "dependencies": { - "ms": "^2.1.1" - } - }, - "node_modules/eslint-module-utils/node_modules/find-up": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-2.1.0.tgz", - "integrity": "sha1-RdG35QbHF93UgndaK3eSCjwMV6c=", - "dev": true, - "dependencies": { - "locate-path": "^2.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/eslint-module-utils/node_modules/locate-path": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-2.0.0.tgz", - "integrity": "sha1-K1aLJl7slExtnA3pw9u7ygNUzY4=", - "dev": true, - "dependencies": { - "p-locate": "^2.0.0", - "path-exists": "^3.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/eslint-module-utils/node_modules/p-limit": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-1.3.0.tgz", - "integrity": "sha512-vvcXsLAJ9Dr5rQOPk7toZQZJApBl2K4J6dANSsEuh6QI41JYcsS/qhTGa9ErIUUgK3WNQoJYvylxvjqmiqEA9Q==", - "dev": true, - "dependencies": { - "p-try": "^1.0.0" - }, - "engines": { - "node": ">=4" + "dev": true, + "dependencies": { + "ms": "^2.1.1" } }, - "node_modules/eslint-module-utils/node_modules/p-locate": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-2.0.0.tgz", - "integrity": "sha1-IKAQOyIqcMj9OcwuWAaA893l7EM=", + "node_modules/eslint-module-utils": { + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.12.0.tgz", + "integrity": "sha512-wALZ0HFoytlyh/1+4wuZ9FJCD/leWHQzzrxJ8+rebyReSLk7LApMyd3WJaLVoN+D5+WIdJyDK1c6JnE65V4Zyg==", "dev": true, + "license": "MIT", "dependencies": { - "p-limit": "^1.1.0" + "debug": "^3.2.7" }, "engines": { "node": ">=4" + }, + "peerDependenciesMeta": { + "eslint": { + "optional": true + } } }, - "node_modules/eslint-module-utils/node_modules/p-try": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/p-try/-/p-try-1.0.0.tgz", - "integrity": "sha1-y8ec26+P1CKOE/Yh8rGiN8GyB7M=", + "node_modules/eslint-module-utils/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", "dev": true, - "engines": { - "node": ">=4" + "license": "MIT", + "dependencies": { + "ms": "^2.1.1" } }, "node_modules/eslint-plugin-import": { - "version": "2.25.4", - "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.25.4.tgz", - "integrity": "sha512-/KJBASVFxpu0xg1kIBn9AUa8hQVnszpwgE7Ld0lKAlx7Ie87yzEzCgSkekt+le/YVhiaosO4Y14GDAOc41nfxA==", + "version": "2.31.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.31.0.tgz", + "integrity": "sha512-ixmkI62Rbc2/w8Vfxyh1jQRTdRTF52VxwRVHl/ykPAmqG+Nb7/kNn+byLP0LxPgI7zWA16Jt82SybJInmMia3A==", "dev": true, + "license": "MIT", "dependencies": { - "array-includes": "^3.1.4", - "array.prototype.flat": "^1.2.5", - "debug": "^2.6.9", + "@rtsao/scc": "^1.1.0", + "array-includes": "^3.1.8", + "array.prototype.findlastindex": "^1.2.5", + "array.prototype.flat": "^1.3.2", + "array.prototype.flatmap": "^1.3.2", + "debug": "^3.2.7", "doctrine": "^2.1.0", - "eslint-import-resolver-node": "^0.3.6", - "eslint-module-utils": "^2.7.2", - "has": "^1.0.3", - "is-core-module": "^2.8.0", + "eslint-import-resolver-node": "^0.3.9", + "eslint-module-utils": "^2.12.0", + "hasown": "^2.0.2", + "is-core-module": "^2.15.1", "is-glob": "^4.0.3", - "minimatch": "^3.0.4", - "object.values": "^1.1.5", - "resolve": "^1.20.0", - "tsconfig-paths": "^3.12.0" + "minimatch": "^3.1.2", + "object.fromentries": "^2.0.8", + "object.groupby": "^1.0.3", + "object.values": "^1.2.0", + "semver": "^6.3.1", + "string.prototype.trimend": "^1.0.8", + "tsconfig-paths": "^3.15.0" }, "engines": { "node": ">=4" }, "peerDependencies": { - "eslint": "^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8" + "eslint": "^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8 || ^9" + } + }, + "node_modules/eslint-plugin-import/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "dependencies": { + "ms": "^2.1.1" } }, "node_modules/eslint-plugin-import/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", "dev": true, + "license": "ISC", "dependencies": { "brace-expansion": "^1.1.7" }, @@ -5083,6 +5949,15 @@ "node": "*" } }, + "node_modules/eslint-plugin-import/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, "node_modules/eslint-plugin-jsx-a11y": { "version": "6.5.1", "resolved": "https://registry.npmjs.org/eslint-plugin-jsx-a11y/-/eslint-plugin-jsx-a11y-6.5.1.tgz", @@ -5129,10 +6004,11 @@ "dev": true }, "node_modules/eslint-plugin-jsx-a11y/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", "dev": true, + "license": "ISC", "dependencies": { "brace-expansion": "^1.1.7" }, @@ -5140,6 +6016,16 @@ "node": "*" } }, + "node_modules/eslint-plugin-no-only-tests": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-no-only-tests/-/eslint-plugin-no-only-tests-3.3.0.tgz", + "integrity": "sha512-brcKcxGnISN2CcVhXJ/kEQlNa0MEfGRtwKtWA16SkqXHKitaKIMrfemJKLKX1YqDU5C/5JY3PvZXd5jEW04e0Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=5.0.0" + } + }, "node_modules/eslint-plugin-react": { "version": "7.29.4", "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.29.4.tgz", @@ -5190,10 +6076,11 @@ } }, "node_modules/eslint-plugin-react/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", "dev": true, + "license": "ISC", "dependencies": { "brace-expansion": "^1.1.7" }, @@ -5215,9 +6102,9 @@ } }, "node_modules/eslint-plugin-react/node_modules/semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", "dev": true, "bin": { "semver": "bin/semver.js" @@ -5236,28 +6123,17 @@ "node": ">=8.0.0" } }, - "node_modules/eslint-utils": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-2.1.0.tgz", - "integrity": "sha512-w94dQYoauyvlDc43XnGB8lU3Zt713vNChgt4EWwhXAP2XkBvndfxF0AgIqKOOasjPIPzj9JqgwkwbCYD0/V3Zg==", + "node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", "dev": true, - "dependencies": { - "eslint-visitor-keys": "^1.1.0" - }, + "license": "Apache-2.0", "engines": { - "node": ">=6" + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" }, "funding": { - "url": "https://github.com/sponsors/mysticatea" - } - }, - "node_modules/eslint-visitor-keys": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.3.0.tgz", - "integrity": "sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ==", - "dev": true, - "engines": { - "node": ">=4" + "url": "https://opencollective.com/eslint" } }, "node_modules/eslint/node_modules/ansi-styles": { @@ -5276,6 +6152,13 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, + "node_modules/eslint/node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, + "license": "Python-2.0" + }, "node_modules/eslint/node_modules/chalk": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.0.tgz", @@ -5311,10 +6194,11 @@ "dev": true }, "node_modules/eslint/node_modules/cross-spawn": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", - "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", "dev": true, + "license": "MIT", "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", @@ -5325,13 +6209,20 @@ } }, "node_modules/eslint/node_modules/debug": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", - "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", - "deprecated": "Debug versions >=3.2.0 <3.2.7 || >=4 <4.3.1 have a low-severity ReDos regression when used in a Node.js environment. It is recommended you upgrade to 3.2.7 or 4.3.1. (https://github.com/visionmedia/debug/issues/797)", + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", "dev": true, "dependencies": { - "ms": "^2.1.1" + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } } }, "node_modules/eslint/node_modules/doctrine": { @@ -5358,32 +6249,69 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/eslint/node_modules/eslint-visitor-keys": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-2.1.0.tgz", - "integrity": "sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw==", + "node_modules/eslint/node_modules/eslint-scope": { + "version": "7.2.2", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz", + "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint/node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/eslint/node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, "engines": { "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/eslint/node_modules/glob-parent": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", - "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", "dev": true, + "license": "ISC", "dependencies": { - "is-glob": "^4.0.1" + "is-glob": "^4.0.3" }, "engines": { - "node": ">= 6" + "node": ">=10.13.0" } }, "node_modules/eslint/node_modules/globals": { - "version": "13.12.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-13.12.0.tgz", - "integrity": "sha512-uS8X6lSKN2JumVoXrbUz+uG4BYG+eiawqm3qFcT7ammfbUHeCBoJMlHcec/S3krSk73/AE/f0szYFmgAA3kYZg==", + "version": "13.24.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", + "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", "dev": true, + "license": "MIT", "dependencies": { "type-fest": "^0.20.2" }, @@ -5403,33 +6331,40 @@ "node": ">=8" } }, - "node_modules/eslint/node_modules/ignore": { - "version": "4.0.6", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-4.0.6.tgz", - "integrity": "sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg==", + "node_modules/eslint/node_modules/js-yaml": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", "dev": true, - "engines": { - "node": ">= 4" + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" } }, - "node_modules/eslint/node_modules/levn": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", - "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "node_modules/eslint/node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", "dev": true, + "license": "MIT", "dependencies": { - "prelude-ls": "^1.2.1", - "type-check": "~0.4.0" + "p-locate": "^5.0.0" }, "engines": { - "node": ">= 0.8.0" + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/eslint/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", "dev": true, + "license": "ISC", "dependencies": { "brace-expansion": "^1.1.7" }, @@ -5437,21 +6372,36 @@ "node": "*" } }, - "node_modules/eslint/node_modules/optionator": { - "version": "0.9.1", - "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.1.tgz", - "integrity": "sha512-74RlY5FCnhq4jRxVUPKDaRwrVNXMqsGsiW6AJw4XK8hmtm10wC0ypZBLw5IIp85NZMr91+qd1RvvENwg7jjRFw==", + "node_modules/eslint/node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", "dev": true, + "license": "MIT", "dependencies": { - "deep-is": "^0.1.3", - "fast-levenshtein": "^2.0.6", - "levn": "^0.4.1", - "prelude-ls": "^1.2.1", - "type-check": "^0.4.0", - "word-wrap": "^1.2.3" + "yocto-queue": "^0.1.0" }, "engines": { - "node": ">= 0.8.0" + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint/node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/eslint/node_modules/path-key": { @@ -5463,39 +6413,6 @@ "node": ">=8" } }, - "node_modules/eslint/node_modules/prelude-ls": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", - "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", - "dev": true, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/eslint/node_modules/progress": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", - "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", - "dev": true, - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/eslint/node_modules/semver": { - "version": "7.3.5", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.5.tgz", - "integrity": "sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==", - "dev": true, - "dependencies": { - "lru-cache": "^6.0.0" - }, - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/eslint/node_modules/shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", @@ -5529,23 +6446,12 @@ "node": ">=8" } }, - "node_modules/eslint/node_modules/type-check": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", - "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", - "dev": true, - "dependencies": { - "prelude-ls": "^1.2.1" - }, - "engines": { - "node": ">= 0.8.0" - } - }, "node_modules/eslint/node_modules/type-fest": { "version": "0.20.2", "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", "dev": true, + "license": "(MIT OR CC0-1.0)", "engines": { "node": ">=10" }, @@ -5554,36 +6460,29 @@ } }, "node_modules/espree": { - "version": "7.3.1", - "resolved": "https://registry.npmjs.org/espree/-/espree-7.3.1.tgz", - "integrity": "sha512-v3JCNCE64umkFpmkFGqzVKsOT0tN1Zr+ueqLZfpV1Ob8e+CEgPWa+OxCoGH3tnhimMKIaBm4m/vaRpJ/krRz2g==", + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", + "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==", "dev": true, + "license": "BSD-2-Clause", "dependencies": { - "acorn": "^7.4.0", - "acorn-jsx": "^5.3.1", - "eslint-visitor-keys": "^1.3.0" + "acorn": "^8.9.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^3.4.1" }, "engines": { - "node": "^10.12.0 || >=12.0.0" - } - }, - "node_modules/espree/node_modules/acorn": { - "version": "7.4.1", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.4.1.tgz", - "integrity": "sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==", - "dev": true, - "bin": { - "acorn": "bin/acorn" + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" }, - "engines": { - "node": ">=0.4.0" + "funding": { + "url": "https://opencollective.com/eslint" } }, "node_modules/esquery": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.4.0.tgz", - "integrity": "sha512-cCDispWt5vHHtwMY2YrAQ4ibFkAL8RbH5YGBnZBc90MolvvfkkQcJro/aZiAQUlQ3qgrYS6D6v8Gc5G5CQsc9w==", + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz", + "integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==", "dev": true, + "license": "BSD-3-Clause", "dependencies": { "estraverse": "^5.1.0" }, @@ -5596,6 +6495,7 @@ "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", "dev": true, + "license": "BSD-2-Clause", "engines": { "node": ">=4.0" } @@ -5682,10 +6582,11 @@ } }, "node_modules/execa/node_modules/cross-spawn": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", - "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", "dev": true, + "license": "MIT", "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", @@ -5746,49 +6647,7 @@ "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", "dev": true, "engines": { - "node": ">=8" - } - }, - "node_modules/expand-brackets": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/expand-brackets/-/expand-brackets-2.1.4.tgz", - "integrity": "sha1-t3c14xXOMPa27/D4OwQVGiJEliI=", - "dev": true, - "dependencies": { - "debug": "^2.3.3", - "define-property": "^0.2.5", - "extend-shallow": "^2.0.1", - "posix-character-classes": "^0.1.0", - "regex-not": "^1.0.0", - "snapdragon": "^0.8.1", - "to-regex": "^3.0.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/expand-brackets/node_modules/define-property": { - "version": "0.2.5", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", - "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", - "dev": true, - "dependencies": { - "is-descriptor": "^0.1.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/expand-brackets/node_modules/extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", - "dev": true, - "dependencies": { - "is-extendable": "^0.1.0" - }, - "engines": { - "node": ">=0.10.0" + "node": ">=8" } }, "node_modules/expand-template": { @@ -5804,7 +6663,7 @@ "node_modules/expand-tilde": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/expand-tilde/-/expand-tilde-2.0.2.tgz", - "integrity": "sha1-l+gBqgUt8CRU3kawK/YhZCzchQI=", + "integrity": "sha512-A5EmesHW6rfnZ9ysHQjPdJRni0SRar0tjtG5MNtm9n5TUvsYU8oozprtRD4AqHxcZWWlVuAmQo2nWKfN9oyjTw==", "dev": true, "dependencies": { "homedir-polyfill": "^1.0.1" @@ -5829,20 +6688,12 @@ "webpack": "^5.0.0" } }, - "node_modules/ext": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/ext/-/ext-1.6.0.tgz", - "integrity": "sha512-sdBImtzkq2HpkdRLtlLWDa6w4DX22ijZLKx8BMPUuKe1c5lbN6xwQDQCxSfxBQnHZ13ls/FH0MQZx/q/gr6FQg==", - "dev": true, - "dependencies": { - "type": "^2.5.0" - } - }, "node_modules/ext-list": { "version": "2.2.2", "resolved": "https://registry.npmjs.org/ext-list/-/ext-list-2.2.2.tgz", "integrity": "sha512-u+SQgsubraE6zItfVA0tBuCBhfU9ogSRnsvygI7wht9TS510oLkBRXBsqopeUG/GBOIQyKZO9wjTqIu/sf5zFA==", "dev": true, + "license": "MIT", "dependencies": { "mime-db": "^1.28.0" }, @@ -5855,6 +6706,7 @@ "resolved": "https://registry.npmjs.org/ext-name/-/ext-name-5.0.0.tgz", "integrity": "sha512-yblEwXAbGv1VQDmow7s38W77hzAgJAO50ztBLMcUyUBfxv1HC+LGwtiEN+Co6LtlqT/5uwVOxsD4TNIilWhwdQ==", "dev": true, + "license": "MIT", "dependencies": { "ext-list": "^2.0.0", "sort-keys-length": "^1.0.0" @@ -5863,12 +6715,6 @@ "node": ">=4" } }, - "node_modules/ext/node_modules/type": { - "version": "2.6.0", - "resolved": "https://registry.npmjs.org/type/-/type-2.6.0.tgz", - "integrity": "sha512-eiDBDOmkih5pMbo9OqsqPRGMljLodLcwd5XD5JbtNB0o89xZAwynY9EdCDsJU7LtcVCClu9DvM7/0Ep1hYX3EQ==", - "dev": true - }, "node_modules/extend": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", @@ -5900,108 +6746,18 @@ "node": ">=0.10.0" } }, - "node_modules/extglob": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/extglob/-/extglob-2.0.4.tgz", - "integrity": "sha512-Nmb6QXkELsuBr24CJSkilo6UHHgbekK5UiZgfE6UHD3Eb27YC6oD+bhcT+tJ6cl8dmsgdQxnWlcry8ksBIBLpw==", - "dev": true, - "dependencies": { - "array-unique": "^0.3.2", - "define-property": "^1.0.0", - "expand-brackets": "^2.1.4", - "extend-shallow": "^2.0.1", - "fragment-cache": "^0.2.1", - "regex-not": "^1.0.0", - "snapdragon": "^0.8.1", - "to-regex": "^3.0.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/extglob/node_modules/define-property": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", - "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", - "dev": true, - "dependencies": { - "is-descriptor": "^1.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/extglob/node_modules/extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", - "dev": true, - "dependencies": { - "is-extendable": "^0.1.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/extglob/node_modules/is-accessor-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", - "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", - "dev": true, - "dependencies": { - "kind-of": "^6.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/extglob/node_modules/is-data-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", - "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", - "dev": true, - "dependencies": { - "kind-of": "^6.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/extglob/node_modules/is-descriptor": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", - "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", - "dev": true, - "dependencies": { - "is-accessor-descriptor": "^1.0.0", - "is-data-descriptor": "^1.0.0", - "kind-of": "^6.0.2" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/fancy-log": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/fancy-log/-/fancy-log-1.3.3.tgz", - "integrity": "sha512-k9oEhlyc0FrVh25qYuSELjr8oxsCoc4/LEZfg2iJJrfEk/tZL9bCoJE47gqAvI2m/AUjluCS4+3I0eTx8n3AEw==", - "dev": true, - "dependencies": { - "ansi-gray": "^0.1.1", - "color-support": "^1.1.3", - "parse-node-version": "^1.0.0", - "time-stamp": "^1.0.0" - }, - "engines": { - "node": ">= 0.10" - } - }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", "dev": true }, + "node_modules/fast-fifo": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/fast-fifo/-/fast-fifo-1.3.2.tgz", + "integrity": "sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==", + "dev": true + }, "node_modules/fast-glob": { "version": "3.2.11", "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.11.tgz", @@ -6018,30 +6774,6 @@ "node": ">=8.6.0" } }, - "node_modules/fast-glob/node_modules/braces": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", - "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", - "dev": true, - "dependencies": { - "fill-range": "^7.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/fast-glob/node_modules/fill-range": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", - "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", - "dev": true, - "dependencies": { - "to-regex-range": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/fast-glob/node_modules/glob-parent": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", @@ -6054,40 +6786,6 @@ "node": ">= 6" } }, - "node_modules/fast-glob/node_modules/is-number": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", - "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", - "dev": true, - "engines": { - "node": ">=0.12.0" - } - }, - "node_modules/fast-glob/node_modules/micromatch": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.4.tgz", - "integrity": "sha512-pRmzw/XUcwXGpD9aI9q/0XOwLNygjETJ8y0ao0wdqprrzDa4YnxLcz7fQRZr8voh8V10kGhABbNcHVk5wHgWwg==", - "dev": true, - "dependencies": { - "braces": "^3.0.1", - "picomatch": "^2.2.3" - }, - "engines": { - "node": ">=8.6" - } - }, - "node_modules/fast-glob/node_modules/to-regex-range": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", - "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", - "dev": true, - "dependencies": { - "is-number": "^7.0.0" - }, - "engines": { - "node": ">=8.0" - } - }, "node_modules/fast-json-stable-stringify": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.0.0.tgz", @@ -6100,10 +6798,21 @@ "integrity": "sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=", "dev": true }, - "node_modules/fast-myers-diff": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/fast-myers-diff/-/fast-myers-diff-3.0.1.tgz", - "integrity": "sha512-e8p26utONwDXeSDkDqu4jaR3l3r6ZgQO2GWB178ePZxCfFoRPNTJVZylUEHHG6uZeRikL1zCc2sl4sIAs9c0UQ==" + "node_modules/fast-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.2.tgz", + "integrity": "sha512-rVjf7ArG3LTk+FS6Yw81V1DLuZl1bRbNrev6Tmd/9RaroeeRRJhAt7jg/6YFxbvAQXUCavSoZhPPj6oOx+5KjQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ] }, "node_modules/fastest-levenshtein": { "version": "1.0.12", @@ -6146,22 +6855,17 @@ "resolved": "https://registry.npmjs.org/file-type/-/file-type-11.1.0.tgz", "integrity": "sha512-rM0UO7Qm9K7TWTtA6AShI/t7H5BPjDeGVDaNyg9BjHAj3PysKy7+8C8D137R88jnR3rFJZQB/tFgydl5sN5m7g==", "dev": true, + "license": "MIT", "engines": { "node": ">=6" } }, - "node_modules/file-uri-to-path": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", - "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==", - "dev": true, - "optional": true - }, "node_modules/filename-reserved-regex": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/filename-reserved-regex/-/filename-reserved-regex-2.0.0.tgz", - "integrity": "sha1-q/c9+rc10EVECr/qLZHzieu/oik=", + "integrity": "sha512-lc1bnsSr4L4Bdif8Xb/qrtokGbq5zlsms/CYH8PP+WtCkGNF65DPiQY8vG3SakEdRn8Dlnm+gW/qWKKjS5sZzQ==", "dev": true, + "license": "MIT", "engines": { "node": ">=4" } @@ -6171,6 +6875,7 @@ "resolved": "https://registry.npmjs.org/filenamify/-/filenamify-3.0.0.tgz", "integrity": "sha512-5EFZ//MsvJgXjBAFJ+Bh2YaCTRF/VP1YOmGrgt+KJ4SFRLjI87EIdwLLuT6wQX0I4F9W41xutobzczjsOKlI/g==", "dev": true, + "license": "MIT", "dependencies": { "filename-reserved-regex": "^2.0.0", "strip-outer": "^1.0.0", @@ -6181,30 +6886,15 @@ } }, "node_modules/fill-range": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz", - "integrity": "sha1-1USBHUKPmOsGpj3EAtJAPDKMOPc=", - "dev": true, - "dependencies": { - "extend-shallow": "^2.0.1", - "is-number": "^3.0.0", - "repeat-string": "^1.6.1", - "to-regex-range": "^2.1.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/fill-range/node_modules/extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", "dev": true, "dependencies": { - "is-extendable": "^0.1.0" + "to-regex-range": "^5.0.1" }, "engines": { - "node": ">=0.10.0" + "node": ">=8" } }, "node_modules/filter-obj": { @@ -6216,6 +6906,23 @@ "node": ">=8" } }, + "node_modules/find-cache-dir": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-3.3.2.tgz", + "integrity": "sha512-wXZV5emFEjrridIgED11OoUKLxiYjAcqot/NJdAkOhlJ+vGzwhOAfcG5OX1jP+S0PcjEn8bdMJv+g2jwQ3Onig==", + "dev": true, + "dependencies": { + "commondir": "^1.0.1", + "make-dir": "^3.0.2", + "pkg-dir": "^4.1.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/avajs/find-cache-dir?sponsor=1" + } + }, "node_modules/find-up": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", @@ -6229,53 +6936,53 @@ "node": ">=8" } }, - "node_modules/find-up/node_modules/path-exists": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", - "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", - "dev": true, - "engines": { - "node": ">=8" - } - }, "node_modules/findup-sync": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/findup-sync/-/findup-sync-3.0.0.tgz", - "integrity": "sha512-YbffarhcicEhOrm4CtrwdKBdCuz576RLdhJDsIfvNtxUuhdRet1qZcsMjqbePtAseKdAnDyM/IyXbu7PRPRLYg==", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/findup-sync/-/findup-sync-5.0.0.tgz", + "integrity": "sha512-MzwXju70AuyflbgeOhzvQWAvvQdo1XL0A9bVvlXsYcFEBM87WR4OakL4OfZq+QRmr+duJubio+UtNQCPsVESzQ==", "dev": true, "dependencies": { "detect-file": "^1.0.0", - "is-glob": "^4.0.0", - "micromatch": "^3.0.4", + "is-glob": "^4.0.3", + "micromatch": "^4.0.4", "resolve-dir": "^1.0.1" }, "engines": { - "node": ">= 0.10" + "node": ">= 10.13.0" } }, "node_modules/fined": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/fined/-/fined-1.2.0.tgz", - "integrity": "sha512-ZYDqPLGxDkDhDZBjZBb+oD1+j0rA4E0pXY50eplAAOPg2N/gUBSSk5IM1/QhPfyVo19lJ+CvXpqfvk+b2p/8Ng==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fined/-/fined-2.0.0.tgz", + "integrity": "sha512-OFRzsL6ZMHz5s0JrsEr+TpdGNCtrVtnuG3x1yzGNiQHT0yaDnXAj8V/lWcpJVrnoDpcwXcASxAZYbuXda2Y82A==", "dev": true, "dependencies": { "expand-tilde": "^2.0.2", - "is-plain-object": "^2.0.3", + "is-plain-object": "^5.0.0", "object.defaults": "^1.1.0", - "object.pick": "^1.2.0", - "parse-filepath": "^1.0.1" + "object.pick": "^1.3.0", + "parse-filepath": "^1.0.2" }, "engines": { - "node": ">= 0.10" + "node": ">= 10.13.0" + } + }, + "node_modules/fined/node_modules/is-plain-object": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-5.0.0.tgz", + "integrity": "sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==", + "dev": true, + "engines": { + "node": ">=0.10.0" } }, "node_modules/flagged-respawn": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/flagged-respawn/-/flagged-respawn-1.0.1.tgz", - "integrity": "sha512-lNaHNVymajmk0OJMBn8fVUAU1BtDeKIqKoVhk4xAALB57aALg6b4W0MfJ/cUE0g9YBXy5XhSlPIpYIJ7HaY/3Q==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/flagged-respawn/-/flagged-respawn-2.0.0.tgz", + "integrity": "sha512-Gq/a6YCi8zexmGHMuJwahTGzXlAZAOsbCVKduWXC6TlLCjjFRlExMJc4GC2NYPYZ0r/brw9P7CpRgQmlPVeOoA==", "dev": true, "engines": { - "node": ">= 0.10" + "node": ">= 10.13.0" } }, "node_modules/flat": { @@ -6301,9 +7008,9 @@ } }, "node_modules/flatted": { - "version": "3.2.4", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.2.4.tgz", - "integrity": "sha512-8/sOawo8tJ4QOBX8YlQBMxL8+RLZfxMQOif9o0KUKTNTjMYElWPE0r/m5VNFxTRd0NSw8qSy8dajrwX4RYI1Hw==", + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.2.tgz", + "integrity": "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==", "dev": true }, "node_modules/flush-write-stream": { @@ -6316,39 +7023,42 @@ "readable-stream": "^2.3.6" } }, - "node_modules/follow-redirects": { - "version": "1.15.1", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.1.tgz", - "integrity": "sha512-yLAMQs+k0b2m7cVxpS1VKJVvoz7SS9Td1zss3XRwXj+ZDH00RJgnuLx7E44wx02kQLrdM3aOOy+FpzS7+8OizA==", - "funding": [ - { - "type": "individual", - "url": "https://github.com/sponsors/RubenVerborgh" - } - ], + "node_modules/for-each": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz", + "integrity": "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-callable": "^1.2.7" + }, "engines": { - "node": ">=4.0" + "node": ">= 0.4" }, - "peerDependenciesMeta": { - "debug": { - "optional": true - } + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, "node_modules/for-in": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/for-in/-/for-in-1.0.2.tgz", - "integrity": "sha1-gQaNKVqBQuwKxybG4iAMMPttXoA=", + "integrity": "sha512-7EwmXrOjyL+ChxMhmG5lnW9MPt1aIeZEwKhQzoBUdTV0N3zuwWDZYVJatDvZ2OyzPUvdIAZDsCetk3coyMfcnQ==", "dev": true, "engines": { "node": ">=0.10.0" } }, - "node_modules/foreach": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/foreach/-/foreach-2.0.5.tgz", - "integrity": "sha1-C+4AUBiusmDQo6865ljdATbsG5k=", - "dev": true + "node_modules/for-own": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/for-own/-/for-own-1.0.0.tgz", + "integrity": "sha512-0OABksIGrxKK8K4kynWkQ7y1zounQxP+CWnyclVwj81KW3vlLlGUx57DKGcP/LH216GzqnstnPocF16Nxs0Ycg==", + "dev": true, + "dependencies": { + "for-in": "^1.0.1" + }, + "engines": { + "node": ">=0.10.0" + } }, "node_modules/foreground-child": { "version": "2.0.0", @@ -6364,10 +7074,11 @@ } }, "node_modules/foreground-child/node_modules/cross-spawn": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.1.tgz", - "integrity": "sha512-u7v4o84SwFpD32Z8IIcPZ6z1/ie24O6RU3RbtL5Y316l3KuHVPx9ItBgWQ6VlfAFnRnTtMUrsQ9MUUTuEZjogg==", + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", "dev": true, + "license": "MIT", "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", @@ -6408,35 +7119,26 @@ } }, "node_modules/form-data": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", - "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz", + "integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==", "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", "mime-types": "^2.1.12" }, "engines": { "node": ">= 6" } }, - "node_modules/fragment-cache": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/fragment-cache/-/fragment-cache-0.2.1.tgz", - "integrity": "sha1-QpD60n8T6Jvn8zeZxrxaCr//DRk=", - "dev": true, - "dependencies": { - "map-cache": "^0.2.2" - }, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/from2": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/from2/-/from2-2.3.0.tgz", - "integrity": "sha1-i/tVAr3kpNNs/e6gB/zKIdfjgq8=", + "integrity": "sha512-OMcX/4IC/uqEPVgGeyfN22LJk6AZrMkRZHxcHBMBvHScDGgwTm2GT2Wkgtocyd3JfZffjj2kYUDXXII0Fk9W0g==", "dev": true, + "license": "MIT", "dependencies": { "inherits": "^2.0.1", "readable-stream": "^2.0.0" @@ -6455,16 +7157,16 @@ "dev": true }, "node_modules/fs-extra": { - "version": "10.0.1", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.0.1.tgz", - "integrity": "sha512-NbdoVMZso2Lsrn/QwLXOy6rm0ufY2zEOKCDzJR/0kBsb0E6qed0P3iYK+Ath3BfvXEeu4JhEtXLgILx5psUfag==", + "version": "11.2.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.2.0.tgz", + "integrity": "sha512-PmDi3uwK5nFuXh7XDTlVnS17xJS7vW36is2+w3xcv8SVxiB4NyATf4ctkVY5bkSjX0Y4nbvZCq1/EjtEyr9ktw==", "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" }, "engines": { - "node": ">=12" + "node": ">=14.14" } }, "node_modules/fs-extra/node_modules/jsonfile": { @@ -6527,109 +7229,87 @@ "node": "^8.16.0 || ^10.6.0 || >=11.0.0" } }, - "node_modules/fstream": { - "version": "1.0.12", - "resolved": "https://registry.npmjs.org/fstream/-/fstream-1.0.12.tgz", - "integrity": "sha512-WvJ193OHa0GHPEL+AycEJgxvBEwyfRkN1vhjca23OaPVMCaLCXTd5qAu82AjTcgP1UJmytkOKb63Ypde7raDIg==", - "dev": true, - "dependencies": { - "graceful-fs": "^4.1.2", - "inherits": "~2.0.0", - "mkdirp": ">=0.5 0", - "rimraf": "2" - }, - "engines": { - "node": ">=0.6" - } - }, - "node_modules/fstream/node_modules/rimraf": { - "version": "2.7.1", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", - "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", - "dev": true, - "dependencies": { - "glob": "^7.1.3" - }, - "bin": { - "rimraf": "bin.js" - } - }, "node_modules/function-bind": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", - "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", - "dev": true - }, - "node_modules/functional-red-black-tree": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz", - "integrity": "sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc=", - "dev": true + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } }, - "node_modules/gauge": { - "version": "2.7.4", - "resolved": "https://registry.npmjs.org/gauge/-/gauge-2.7.4.tgz", - "integrity": "sha1-LANAXHU4w51+s3sxcCLjJfsBi/c=", + "node_modules/function.prototype.name": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.6.tgz", + "integrity": "sha512-Z5kx79swU5P27WEayXM1tBi5Ze/lbIyiNgU3qyXUOf9b2rgXYyF9Dy9Cx+IQv/Lc8WCG6L82zwUPpSS9hGehIg==", "dev": true, - "optional": true, "dependencies": { - "aproba": "^1.0.3", - "console-control-strings": "^1.0.0", - "has-unicode": "^2.0.0", - "object-assign": "^4.1.0", - "signal-exit": "^3.0.0", - "string-width": "^1.0.1", - "strip-ansi": "^3.0.1", - "wide-align": "^1.1.0" + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1", + "functions-have-names": "^1.2.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/gauge/node_modules/ansi-regex": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", - "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", + "node_modules/functions-have-names": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz", + "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==", "dev": true, - "optional": true, - "engines": { - "node": ">=0.10.0" + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/gauge/node_modules/strip-ansi": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", - "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", "dev": true, - "optional": true, - "dependencies": { - "ansi-regex": "^2.0.0" - }, "engines": { - "node": ">=0.10.0" + "node": ">=6.9.0" } }, "node_modules/get-caller-file": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-1.0.3.tgz", - "integrity": "sha512-3t6rVToeoZfYSGd8YoLFR2DJkiQrIiUrGcjvFX2mDw3bn6k2OtwHN0TNCLbBO+w8qTvimhDkv+LSscbJY1vE6w==", - "dev": true + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true, + "engines": { + "node": "6.* || 8.* || >= 10.*" + } }, "node_modules/get-func-name": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.0.tgz", - "integrity": "sha1-6td0q+5y4gQJQzoGY2YCPdaIekE=", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.2.tgz", + "integrity": "sha512-8vXOvuE167CtIc3OyItco7N/dpRtBbYOsPsXCz7X/PMnlGjYjSGuZJgM1Y7mmew7BKf9BqvLX2tnOVy1BBUsxQ==", "dev": true, "engines": { "node": "*" } }, "node_modules/get-intrinsic": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.1.1.tgz", - "integrity": "sha512-kWZrnVM42QCiEA2Ig1bG8zjoIMOgxWwYCEeNdwY6Tv/cOSeGpcoX4pXHfKUxNKVoArnrEr2e9srnAxxGIraS9Q==", - "dev": true, + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", "dependencies": { - "function-bind": "^1.1.1", - "has": "^1.0.3", - "has-symbols": "^1.0.1" + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -6656,23 +7336,52 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/get-stream": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-3.0.0.tgz", - "integrity": "sha1-jpQ9E1jcN1VQVOy+LtsFqhdO3hQ=", + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-4.1.0.tgz", + "integrity": "sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w==", "dev": true, + "license": "MIT", + "dependencies": { + "pump": "^3.0.0" + }, "engines": { - "node": ">=4" + "node": ">=6" + } + }, + "node_modules/get-stream/node_modules/pump": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.2.tgz", + "integrity": "sha512-tUPXtzlGM8FE3P0ZL6DVs/3P58k9nk8/jZeQCurTJylQA8qFYzHFfhBJkuqyE0FifOsQ0uKWekiZ5g8wtr28cw==", + "dev": true, + "license": "MIT", + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" } }, "node_modules/get-symbol-description": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.0.0.tgz", - "integrity": "sha512-2EmdH1YvIQiZpltCNgkuiUnyukzxM/R6NDJX31Ke3BG1Nq5b0S2PhX59UKi9vZpPDQVdqn+1IcaAwnzTT5vCjw==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.0.2.tgz", + "integrity": "sha512-g0QYk1dZBxGwk+Ngc+ltRH2IBp2f7zBkBMBJZCDerh6EhlhSR6+9irMCuT/09zD6qkarHUSn529sK/yL4S27mg==", "dev": true, "dependencies": { - "call-bind": "^1.0.2", - "get-intrinsic": "^1.1.1" + "call-bind": "^1.0.5", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.4" }, "engines": { "node": ">= 0.4" @@ -6681,19 +7390,10 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/get-value": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/get-value/-/get-value-2.0.6.tgz", - "integrity": "sha1-3BXKHGcjh8p2vTesCjlbogQqLCg=", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/github-from-package": { "version": "0.0.0", "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz", - "integrity": "sha1-l/tdlr/eiXMxPyDoKI75oWf6ZM4=", + "integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==", "dev": true, "optional": true }, @@ -6766,104 +7466,23 @@ "dev": true }, "node_modules/glob-watcher": { - "version": "5.0.5", - "resolved": "https://registry.npmjs.org/glob-watcher/-/glob-watcher-5.0.5.tgz", - "integrity": "sha512-zOZgGGEHPklZNjZQaZ9f41i7F2YwE+tS5ZHrDhbBCk3stwahn5vQxnFmBJZHoYdusR6R1bLSXeGUy/BhctwKzw==", - "dev": true, - "dependencies": { - "anymatch": "^2.0.0", - "async-done": "^1.2.0", - "chokidar": "^2.0.0", - "is-negated-glob": "^1.0.0", - "just-debounce": "^1.0.0", - "normalize-path": "^3.0.0", - "object.defaults": "^1.1.0" - }, - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/glob-watcher/node_modules/binary-extensions": { - "version": "1.13.1", - "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-1.13.1.tgz", - "integrity": "sha512-Un7MIEDdUC5gNpcGDV97op1Ywk748MpHcFTHoYs6qnj1Z3j7I53VG3nwZhKzoBZmbdRNnb6WRdFlwl7tSDuZGw==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/glob-watcher/node_modules/chokidar": { - "version": "2.1.8", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-2.1.8.tgz", - "integrity": "sha512-ZmZUazfOzf0Nve7duiCKD23PFSCs4JPoYyccjUFF3aQkQadqBhfzhjkwBH2mNOG9cTBwhamM37EIsIkZw3nRgg==", - "deprecated": "Chokidar 2 does not receive security updates since 2019. Upgrade to chokidar 3 with 15x fewer dependencies", - "dev": true, - "dependencies": { - "anymatch": "^2.0.0", - "async-each": "^1.0.1", - "braces": "^2.3.2", - "glob-parent": "^3.1.0", - "inherits": "^2.0.3", - "is-binary-path": "^1.0.0", - "is-glob": "^4.0.0", - "normalize-path": "^3.0.0", - "path-is-absolute": "^1.0.0", - "readdirp": "^2.2.1", - "upath": "^1.1.1" - }, - "optionalDependencies": { - "fsevents": "^1.2.7" - } - }, - "node_modules/glob-watcher/node_modules/fsevents": { - "version": "1.2.13", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-1.2.13.tgz", - "integrity": "sha512-oWb1Z6mkHIskLzEJ/XWX0srkpkTQ7vaopMQkyaEIoq0fmtFVxOthb8cCxeT+p3ynTdkk/RZwbgG4brR5BeWECw==", - "deprecated": "fsevents 1 will break on node v14+ and could be using insecure binaries. Upgrade to fsevents 2.", - "dev": true, - "hasInstallScript": true, - "optional": true, - "os": [ - "darwin" - ], - "dependencies": { - "bindings": "^1.5.0", - "nan": "^2.12.1" - }, - "engines": { - "node": ">= 4.0" - } - }, - "node_modules/glob-watcher/node_modules/is-binary-path": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-1.0.1.tgz", - "integrity": "sha1-dfFmQrSA8YenEcgUFh/TpKdlWJg=", - "dev": true, - "dependencies": { - "binary-extensions": "^1.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/glob-watcher/node_modules/readdirp": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-2.2.1.tgz", - "integrity": "sha512-1JU/8q+VgFZyxwrJ+SVIOsh+KywWGpds3NTqikiKpDMZWScmAYyKIgqkO+ARvNWJfXeXR1zxz7aHF4u4CyH6vQ==", + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/glob-watcher/-/glob-watcher-6.0.0.tgz", + "integrity": "sha512-wGM28Ehmcnk2NqRORXFOTOR064L4imSw3EeOqU5bIwUf62eXGwg89WivH6VMahL8zlQHeodzvHpXplrqzrz3Nw==", "dev": true, "dependencies": { - "graceful-fs": "^4.1.11", - "micromatch": "^3.1.10", - "readable-stream": "^2.0.2" + "async-done": "^2.0.0", + "chokidar": "^3.5.3" }, "engines": { - "node": ">=0.10" + "node": ">= 10.13.0" } }, "node_modules/glob/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "license": "ISC", "dependencies": { "brace-expansion": "^1.1.7" }, @@ -6888,7 +7507,7 @@ "node_modules/global-prefix": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/global-prefix/-/global-prefix-1.0.2.tgz", - "integrity": "sha1-2/dDxsFJklk8ZVVoy2btMsASLr4=", + "integrity": "sha512-5lsx1NUDHtSjfg0eHlmYvZKv8/nVqX4ckFbM+FrGcQ+04KWcWFo9P5MxPZYSzUvyzmdTbI7Eix8Q4IbELDqzKg==", "dev": true, "dependencies": { "expand-tilde": "^2.0.2", @@ -6922,6 +7541,22 @@ "node": ">=4" } }, + "node_modules/globalthis": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.4.tgz", + "integrity": "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==", + "dev": true, + "dependencies": { + "define-properties": "^1.2.1", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/globby": { "version": "11.1.0", "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", @@ -6943,15 +7578,27 @@ } }, "node_modules/glogg": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/glogg/-/glogg-1.0.2.tgz", - "integrity": "sha512-5mwUoSuBk44Y4EshyiqcH95ZntbDdTQqA3QYSrxmzj28Ai0vXBGMH1ApSANH14j2sIRtqCEyg6PfsuP7ElOEDA==", + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/glogg/-/glogg-2.2.0.tgz", + "integrity": "sha512-eWv1ds/zAlz+M1ioHsyKJomfY7jbDDPpwSkv14KQj89bycx1nvK5/2Cj/T9g7kzJcX5Bc7Yv22FjfBZS/jl94A==", "dev": true, "dependencies": { - "sparkles": "^1.0.0" + "sparkles": "^2.1.0" }, "engines": { - "node": ">= 0.10" + "node": ">= 10.13.0" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, "node_modules/got": { @@ -6959,6 +7606,7 @@ "resolved": "https://registry.npmjs.org/got/-/got-8.3.2.tgz", "integrity": "sha512-qjUJ5U/hawxosMryILofZCkm3C84PLJS/0grRIpjAwu+Lkxxj5cxeCU25BG0/3mDSpXKTyZr8oh8wIgLaH0QCw==", "dev": true, + "license": "MIT", "dependencies": { "@sindresorhus/is": "^0.7.0", "cacheable-request": "^2.1.1", @@ -6982,51 +7630,176 @@ "node": ">=4" } }, + "node_modules/got/node_modules/get-stream": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-3.0.0.tgz", + "integrity": "sha512-GlhdIUuVakc8SJ6kK0zAFbiGzRFzNnY4jUuEbV9UROo4Y+0Ny4fjvcZFVTeDA4odpFyOQzaw6hXukJSq/f28sQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, "node_modules/got/node_modules/pify": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", - "integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=", + "integrity": "sha512-C3FsVNH1udSEX48gGX1xfvwTWfsYWj5U+8/uK15BGzIGrKoUpghX8hWZwa/OFnakBiiVNmBvemTJR5mcy7iPcg==", "dev": true, + "license": "MIT", "engines": { "node": ">=4" } }, "node_modules/graceful-fs": { - "version": "4.2.9", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.9.tgz", - "integrity": "sha512-NtNxqUcXgpW2iMrfqSfR73Glt39K+BLwWsPs94yR63v45T0Wbej7eRmL5cWfwEgqXnmjQp3zaJTshdRW/qC2ZQ==" + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==" }, - "node_modules/graceful-readlink": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/graceful-readlink/-/graceful-readlink-1.0.1.tgz", - "integrity": "sha1-TK+tdrxi8C+gObL5Tpo906ORpyU=", + "node_modules/graphemer": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", + "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", + "dev": true + }, + "node_modules/gulp": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/gulp/-/gulp-5.0.0.tgz", + "integrity": "sha512-S8Z8066SSileaYw1S2N1I64IUc/myI2bqe2ihOBzO6+nKpvNSg7ZcWJt/AwF8LC/NVN+/QZ560Cb/5OPsyhkhg==", + "dev": true, + "dependencies": { + "glob-watcher": "^6.0.0", + "gulp-cli": "^3.0.0", + "undertaker": "^2.0.0", + "vinyl-fs": "^4.0.0" + }, + "bin": { + "gulp": "bin/gulp.js" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/gulp-cli": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/gulp-cli/-/gulp-cli-3.0.0.tgz", + "integrity": "sha512-RtMIitkT8DEMZZygHK2vEuLPqLPAFB4sntSxg4NoDta7ciwGZ18l7JuhCTiS5deOJi2IoK0btE+hs6R4sfj7AA==", + "dev": true, + "dependencies": { + "@gulpjs/messages": "^1.1.0", + "chalk": "^4.1.2", + "copy-props": "^4.0.0", + "gulplog": "^2.2.0", + "interpret": "^3.1.1", + "liftoff": "^5.0.0", + "mute-stdout": "^2.0.0", + "replace-homedir": "^2.0.0", + "semver-greatest-satisfied-range": "^2.0.0", + "string-width": "^4.2.3", + "v8flags": "^4.0.0", + "yargs": "^16.2.0" + }, + "bin": { + "gulp": "bin/gulp.js" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/gulp-cli/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/gulp-cli/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/gulp-cli/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/gulp-cli/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "dev": true }, - "node_modules/growl": { - "version": "1.10.5", - "resolved": "https://registry.npmjs.org/growl/-/growl-1.10.5.tgz", - "integrity": "sha512-qBr4OuELkhPenW6goKVXiv47US3clb3/IbuWF9KNKEijAy9oeHxU9IgzjvJhHkUzhaj7rOUD7+YGWqUjLp5oSA==", + "node_modules/gulp-cli/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/gulp-cli/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/gulp-cli/node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", "dev": true, "engines": { - "node": ">=4.x" + "node": ">=10" } }, - "node_modules/gulp": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/gulp/-/gulp-4.0.2.tgz", - "integrity": "sha512-dvEs27SCZt2ibF29xYgmnwwCYZxdxhQ/+LFWlbAW8y7jt68L/65402Lz3+CKy0Ov4rOs+NERmDq7YlZaDqUIfA==", + "node_modules/gulp-cli/node_modules/yargs": { + "version": "16.2.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", + "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==", "dev": true, "dependencies": { - "glob-watcher": "^5.0.3", - "gulp-cli": "^2.2.0", - "undertaker": "^1.2.1", - "vinyl-fs": "^3.0.0" - }, - "bin": { - "gulp": "bin/gulp.js" + "cliui": "^7.0.2", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.0", + "y18n": "^5.0.5", + "yargs-parser": "^20.2.2" }, "engines": { - "node": ">= 0.10" + "node": ">=10" } }, "node_modules/gulp-typescript": { @@ -7077,106 +7850,187 @@ "readable-stream": "2 || 3" } }, - "node_modules/gulp/node_modules/camelcase": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-3.0.0.tgz", - "integrity": "sha1-MvxLn82vhF/N9+c7uXysImHwqwo=", + "node_modules/gulp/node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true + }, + "node_modules/gulp/node_modules/fs-mkdirp-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/fs-mkdirp-stream/-/fs-mkdirp-stream-2.0.1.tgz", + "integrity": "sha512-UTOY+59K6IA94tec8Wjqm0FSh5OVudGNB0NL/P6fB3HiE3bYOY3VYBGijsnOHNkQSwC1FKkU77pmq7xp9CskLw==", "dev": true, + "dependencies": { + "graceful-fs": "^4.2.8", + "streamx": "^2.12.0" + }, "engines": { - "node": ">=0.10.0" + "node": ">=10.13.0" } }, - "node_modules/gulp/node_modules/gulp-cli": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/gulp-cli/-/gulp-cli-2.3.0.tgz", - "integrity": "sha512-zzGBl5fHo0EKSXsHzjspp3y5CONegCm8ErO5Qh0UzFzk2y4tMvzLWhoDokADbarfZRL2pGpRp7yt6gfJX4ph7A==", + "node_modules/gulp/node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", "dev": true, "dependencies": { - "ansi-colors": "^1.0.1", - "archy": "^1.0.0", - "array-sort": "^1.0.0", - "color-support": "^1.1.3", - "concat-stream": "^1.6.0", - "copy-props": "^2.0.1", - "fancy-log": "^1.3.2", - "gulplog": "^1.0.0", - "interpret": "^1.4.0", - "isobject": "^3.0.1", - "liftoff": "^3.1.0", - "matchdep": "^2.0.0", - "mute-stdout": "^1.0.0", - "pretty-hrtime": "^1.0.0", - "replace-homedir": "^1.0.0", - "semver-greatest-satisfied-range": "^1.1.0", - "v8flags": "^3.2.0", - "yargs": "^7.1.0" + "is-glob": "^4.0.3" }, - "bin": { - "gulp": "bin/gulp.js" + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/gulp/node_modules/glob-stream": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/glob-stream/-/glob-stream-8.0.2.tgz", + "integrity": "sha512-R8z6eTB55t3QeZMmU1C+Gv+t5UnNRkA55c5yo67fAVfxODxieTwsjNG7utxS/73NdP1NbDgCrhVEg2h00y4fFw==", + "dev": true, + "dependencies": { + "@gulpjs/to-absolute-glob": "^4.0.0", + "anymatch": "^3.1.3", + "fastq": "^1.13.0", + "glob-parent": "^6.0.2", + "is-glob": "^4.0.3", + "is-negated-glob": "^1.0.0", + "normalize-path": "^3.0.0", + "streamx": "^2.12.5" }, "engines": { - "node": ">= 0.10" + "node": ">=10.13.0" } }, - "node_modules/gulp/node_modules/v8flags": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/v8flags/-/v8flags-3.2.0.tgz", - "integrity": "sha512-mH8etigqMfiGWdeXpaaqGfs6BndypxusHHcv2qSHyZkGEznCd/qAXCWWRzeowtL54147cktFOC4P5y+kl8d8Jg==", + "node_modules/gulp/node_modules/lead": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/lead/-/lead-4.0.0.tgz", + "integrity": "sha512-DpMa59o5uGUWWjruMp71e6knmwKU3jRBBn1kjuLWN9EeIOxNeSAwvHf03WIl8g/ZMR2oSQC9ej3yeLBwdDc/pg==", + "dev": true, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/gulp/node_modules/now-and-later": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/now-and-later/-/now-and-later-3.0.0.tgz", + "integrity": "sha512-pGO4pzSdaxhWTGkfSfHx3hVzJVslFPwBp2Myq9MYN/ChfJZF87ochMAXnvz6/58RJSf5ik2q9tXprBBrk2cpcg==", "dev": true, "dependencies": { - "homedir-polyfill": "^1.0.1" + "once": "^1.4.0" }, "engines": { - "node": ">= 0.10" + "node": ">= 10.13.0" } }, - "node_modules/gulp/node_modules/y18n": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/y18n/-/y18n-3.2.2.tgz", - "integrity": "sha512-uGZHXkHnhF0XeeAPgnKfPv1bgKAYyVvmNL1xlKsPYZPaIHxGti2hHqvOCQv71XMsLxu1QjergkqogUnms5D3YQ==", - "dev": true + "node_modules/gulp/node_modules/replace-ext": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/replace-ext/-/replace-ext-2.0.0.tgz", + "integrity": "sha512-UszKE5KVK6JvyD92nzMn9cDapSk6w/CaFZ96CnmDMUqH9oowfxF/ZjRITD25H4DnOQClLA4/j7jLGXXLVKxAug==", + "dev": true, + "engines": { + "node": ">= 10" + } }, - "node_modules/gulp/node_modules/yargs": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-7.1.1.tgz", - "integrity": "sha512-huO4Fr1f9PmiJJdll5kwoS2e4GqzGSsMT3PPMpOwoVkOK8ckqAewMTZyA6LXVQWflleb/Z8oPBEvNsMft0XE+g==", + "node_modules/gulp/node_modules/resolve-options": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/resolve-options/-/resolve-options-2.0.0.tgz", + "integrity": "sha512-/FopbmmFOQCfsCx77BRFdKOniglTiHumLgwvd6IDPihy1GKkadZbgQJBcTb2lMzSR1pndzd96b1nZrreZ7+9/A==", "dev": true, "dependencies": { - "camelcase": "^3.0.0", - "cliui": "^3.2.0", - "decamelize": "^1.1.1", - "get-caller-file": "^1.0.1", - "os-locale": "^1.4.0", - "read-pkg-up": "^1.0.1", - "require-directory": "^2.1.1", - "require-main-filename": "^1.0.1", - "set-blocking": "^2.0.0", - "string-width": "^1.0.2", - "which-module": "^1.0.0", - "y18n": "^3.2.1", - "yargs-parser": "5.0.0-security.0" + "value-or-function": "^4.0.0" + }, + "engines": { + "node": ">= 10.13.0" + } + }, + "node_modules/gulp/node_modules/to-through": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/to-through/-/to-through-3.0.0.tgz", + "integrity": "sha512-y8MN937s/HVhEoBU1SxfHC+wxCHkV1a9gW8eAdTadYh/bGyesZIVcbjI+mSpFbSVwQici/XjBjuUyri1dnXwBw==", + "dev": true, + "dependencies": { + "streamx": "^2.12.5" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/gulp/node_modules/value-or-function": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/value-or-function/-/value-or-function-4.0.0.tgz", + "integrity": "sha512-aeVK81SIuT6aMJfNo9Vte8Dw0/FZINGBV8BfCraGtqVxIeLAEhJyoWs8SmvRVmXfGss2PmmOwZCuBPbZR+IYWg==", + "dev": true, + "engines": { + "node": ">= 10.13.0" + } + }, + "node_modules/gulp/node_modules/vinyl": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/vinyl/-/vinyl-3.0.0.tgz", + "integrity": "sha512-rC2VRfAVVCGEgjnxHUnpIVh3AGuk62rP3tqVrn+yab0YH7UULisC085+NYH+mnqf3Wx4SpSi1RQMwudL89N03g==", + "dev": true, + "dependencies": { + "clone": "^2.1.2", + "clone-stats": "^1.0.0", + "remove-trailing-separator": "^1.1.0", + "replace-ext": "^2.0.0", + "teex": "^1.0.1" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/gulp/node_modules/vinyl-fs": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/vinyl-fs/-/vinyl-fs-4.0.0.tgz", + "integrity": "sha512-7GbgBnYfaquMk3Qu9g22x000vbYkOex32930rBnc3qByw6HfMEAoELjCjoJv4HuEQxHAurT+nvMHm6MnJllFLw==", + "dev": true, + "dependencies": { + "fs-mkdirp-stream": "^2.0.1", + "glob-stream": "^8.0.0", + "graceful-fs": "^4.2.11", + "iconv-lite": "^0.6.3", + "is-valid-glob": "^1.0.0", + "lead": "^4.0.0", + "normalize-path": "3.0.0", + "resolve-options": "^2.0.0", + "stream-composer": "^1.0.2", + "streamx": "^2.14.0", + "to-through": "^3.0.0", + "value-or-function": "^4.0.0", + "vinyl": "^3.0.0", + "vinyl-sourcemap": "^2.0.0" + }, + "engines": { + "node": ">=10.13.0" } }, - "node_modules/gulp/node_modules/yargs-parser": { - "version": "5.0.0-security.0", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-5.0.0-security.0.tgz", - "integrity": "sha512-T69y4Ps64LNesYxeYGYPvfoMTt/7y1XtfpIslUeK4um+9Hu7hlGoRtaDLvdXb7+/tfq4opVa2HRY5xGip022rQ==", + "node_modules/gulp/node_modules/vinyl-sourcemap": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/vinyl-sourcemap/-/vinyl-sourcemap-2.0.0.tgz", + "integrity": "sha512-BAEvWxbBUXvlNoFQVFVHpybBbjW1r03WhohJzJDSfgrrK5xVYIDTan6xN14DlyImShgDRv2gl9qhM6irVMsV0Q==", "dev": true, "dependencies": { - "camelcase": "^3.0.0", - "object.assign": "^4.1.0" + "convert-source-map": "^2.0.0", + "graceful-fs": "^4.2.10", + "now-and-later": "^3.0.0", + "streamx": "^2.12.5", + "vinyl": "^3.0.0", + "vinyl-contents": "^2.0.0" + }, + "engines": { + "node": ">=10.13.0" } }, "node_modules/gulplog": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/gulplog/-/gulplog-1.0.0.tgz", - "integrity": "sha1-4oxNRdBey77YGDY86PnFkmIp/+U=", + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/gulplog/-/gulplog-2.2.0.tgz", + "integrity": "sha512-V2FaKiOhpR3DRXZuYdRLn/qiY0yI5XmqbTKrYbdemJ+xOh2d2MOweI/XFgMzd/9+1twdvMwllnZbWZNJ+BOm4A==", "dev": true, "dependencies": { - "glogg": "^1.0.0" + "glogg": "^2.2.0" }, "engines": { - "node": ">= 0.10" + "node": ">= 10.13.0" } }, "node_modules/gzip-size": { @@ -7207,9 +8061,9 @@ } }, "node_modules/has-bigints": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.0.1.tgz", - "integrity": "sha512-LSBS2LjbNBTf6287JEbEzvJgftkF5qFkmCo9hDRpAzKhUOlJ+hx8dd4USs00SgsUNwc4617J9ki5YtEClM2ffA==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.0.2.tgz", + "integrity": "sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ==", "dev": true, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -7224,20 +8078,45 @@ "node": ">=4" } }, + "node_modules/has-property-descriptors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "dev": true, + "dependencies": { + "es-define-property": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-proto": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.3.tgz", + "integrity": "sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/has-symbol-support-x": { "version": "1.4.2", "resolved": "https://registry.npmjs.org/has-symbol-support-x/-/has-symbol-support-x-1.4.2.tgz", "integrity": "sha512-3ToOva++HaW+eCpgqZrCfN51IPB+7bJNVT6CUATzueB5Heb8o6Nam0V3HG5dlDvZU1Gn5QLcbahiKw/XVk5JJw==", "dev": true, + "license": "MIT", "engines": { "node": "*" } }, "node_modules/has-symbols": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", - "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", - "dev": true, + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", "engines": { "node": ">= 0.4" }, @@ -7250,6 +8129,7 @@ "resolved": "https://registry.npmjs.org/has-to-string-tag-x/-/has-to-string-tag-x-1.4.1.tgz", "integrity": "sha512-vdbKfmw+3LoOYVr+mtxHaX5a96+0f3DljYd8JOqvOLsf5mw2Otda2qCDT9qRqLAhrjyQ0h7ual5nOiASpsGNFw==", "dev": true, + "license": "MIT", "dependencies": { "has-symbol-support-x": "^1.4.1" }, @@ -7258,12 +8138,11 @@ } }, "node_modules/has-tostringtag": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.0.tgz", - "integrity": "sha512-kFjcSNhnlGV1kyoGk7OXKSawH5JOb/LzUc5w9B02hOTO0dfFRjbHQKvg1d6cf3HbeUmtU9VbbV3qzZ2Teh97WQ==", - "dev": true, + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", "dependencies": { - "has-symbols": "^1.0.2" + "has-symbols": "^1.0.3" }, "engines": { "node": ">= 0.4" @@ -7272,52 +8151,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/has-unicode": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", - "integrity": "sha1-4Ob+aijPUROIVeCG0Wkedx3iqLk=", - "dev": true, - "optional": true - }, - "node_modules/has-value": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/has-value/-/has-value-1.0.0.tgz", - "integrity": "sha1-GLKB2lhbHFxR3vJMkw7SmgvmsXc=", - "dev": true, - "dependencies": { - "get-value": "^2.0.6", - "has-values": "^1.0.0", - "isobject": "^3.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/has-values": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/has-values/-/has-values-1.0.0.tgz", - "integrity": "sha1-lbC2P+whRmGab+V/51Yo1aOe/k8=", - "dev": true, - "dependencies": { - "is-number": "^3.0.0", - "kind-of": "^4.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/has-values/node_modules/kind-of": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-4.0.0.tgz", - "integrity": "sha1-IIE989cSkosgc3hpGkUGb65y3Vc=", - "dev": true, - "dependencies": { - "is-buffer": "^1.1.5" - }, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/hash-base": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/hash-base/-/hash-base-3.0.4.tgz", @@ -7335,6 +8168,7 @@ "version": "1.1.7", "resolved": "https://registry.npmjs.org/hash.js/-/hash.js-1.1.7.tgz", "integrity": "sha512-taOaskGt4z4SOANNseOviYDvjEJinIkRgmp7LbKP2YTTmVxWBl87s/uzK9r+44BclBSp2X7K1hqeNfz9JbBeXA==", + "dev": true, "dependencies": { "inherits": "^2.0.3", "minimalistic-assert": "^1.0.1" @@ -7362,6 +8196,17 @@ "node": ">=8" } }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/he": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", @@ -7394,12 +8239,6 @@ "node": ">=0.10.0" } }, - "node_modules/hosted-git-info": { - "version": "2.8.9", - "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.9.tgz", - "integrity": "sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==", - "dev": true - }, "node_modules/html-escaper": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", @@ -7410,7 +8249,8 @@ "version": "3.8.1", "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-3.8.1.tgz", "integrity": "sha512-5ai2iksyV8ZXmnZhHH4rWPoxxistEexSi5936zIQ1bnNTW5VnA85B6P/VpXiRM017IgRvb2kKo1a//y+0wSp3w==", - "dev": true + "dev": true, + "license": "BSD-2-Clause" }, "node_modules/http-proxy-agent": { "version": "4.0.1", @@ -7518,19 +8358,26 @@ ] }, "node_modules/ignore": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.0.tgz", - "integrity": "sha512-CmxgYGiEPCLhfLnpPp1MoRmifwEIOgjcHXxOBjv7mY96c+eWScsOP9c112ZyLdWHi0FxHjI+4uVhKYp/gcdRmQ==", + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", "dev": true, "engines": { "node": ">= 4" } }, + "node_modules/immediate": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz", + "integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==", + "dev": true + }, "node_modules/import-fresh": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", - "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", "dev": true, + "license": "MIT", "dependencies": { "parent-module": "^1.0.0", "resolve-from": "^4.0.0" @@ -7547,10 +8394,22 @@ "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", "dev": true, + "license": "MIT", "engines": { "node": ">=4" } }, + "node_modules/import-in-the-middle": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/import-in-the-middle/-/import-in-the-middle-1.4.2.tgz", + "integrity": "sha512-9WOz1Yh/cvO/p69sxRmhyQwrIGGSp7EIdcb+fFNVi7CzQGQB8U1/1XrKVSbEd/GNOAeM0peJtmi7+qphe7NvAw==", + "dependencies": { + "acorn": "^8.8.2", + "acorn-import-assertions": "^1.9.0", + "cjs-module-lexer": "^1.2.2", + "module-details-from-path": "^1.0.3" + } + }, "node_modules/import-local": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.1.0.tgz", @@ -7609,13 +8468,13 @@ "dev": true }, "node_modules/internal-slot": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.3.tgz", - "integrity": "sha512-O0DB1JC/sPyZl7cIo78n5dR7eUSwwpYPiXRhTzNxZVAMUuB8vlnRFyLxdrVToks6XPLVnFfbzaVd5WLjhgg+vA==", + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.7.tgz", + "integrity": "sha512-NGnrKwXzSms2qUUih/ILZ5JBqNTSa1+ZmP6flaIp6KmSElgE9qdndzS3cqjrDovwFdmwsGsLdeFgB6suw+1e9g==", "dev": true, "dependencies": { - "get-intrinsic": "^1.1.0", - "has": "^1.0.3", + "es-errors": "^1.3.0", + "hasown": "^2.0.0", "side-channel": "^1.0.4" }, "engines": { @@ -7623,19 +8482,20 @@ } }, "node_modules/interpret": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/interpret/-/interpret-1.4.0.tgz", - "integrity": "sha512-agE4QfB2Lkp9uICn7BAqoscw4SZP9kTE2hxiFI3jBPmXJfdqiahTbUuKGsMoN2GtqL9AxhYioAcVvgsb1HvRbA==", + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/interpret/-/interpret-3.1.1.tgz", + "integrity": "sha512-6xwYfHbajpoF0xLW+iwLkhwgvLoZDfjYfoFNu8ftMoXINzwuymNLd9u/KmwtdT2GbR+/Cz66otEGEVVUHX9QLQ==", "dev": true, "engines": { - "node": ">= 0.10" + "node": ">=10.13.0" } }, "node_modules/into-stream": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/into-stream/-/into-stream-3.1.0.tgz", - "integrity": "sha1-lvsKk2wSur1v8XUqF9BWFqvQlMY=", + "integrity": "sha512-TcdjPibTksa1NQximqep2r17ISRiNE9fwlfbg3F8ANdvP5/yrFTew86VcO//jk4QTaMlbjypPBq76HN2zaKfZQ==", "dev": true, + "license": "MIT", "dependencies": { "from2": "^2.1.1", "p-is-promise": "^1.1.0" @@ -7645,18 +8505,9 @@ } }, "node_modules/inversify": { - "version": "5.0.5", - "resolved": "https://registry.npmjs.org/inversify/-/inversify-5.0.5.tgz", - "integrity": "sha512-60QsfPz8NAU/GZqXu8hJ+BhNf/C/c+Hp0eDc6XMIJTxBiP36AQyyQKpBkOVTLWBFDQWYVHpbbEuIsHu9dLuJDA==" - }, - "node_modules/invert-kv": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/invert-kv/-/invert-kv-1.0.0.tgz", - "integrity": "sha1-EEqOSqym09jNFXqO+L+rLXo//bY=", - "dev": true, - "engines": { - "node": ">=0.10.0" - } + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/inversify/-/inversify-6.0.2.tgz", + "integrity": "sha512-i9m8j/7YIv4mDuYXUAcrpKPSaju/CIly9AHK5jvCBeoiM/2KEsuCQTTP+rzSWWpLYWRukdXFSl6ZTk2/uumbiA==" }, "node_modules/is-absolute": { "version": "1.0.0", @@ -7671,38 +8522,30 @@ "node": ">=0.10.0" } }, - "node_modules/is-accessor-descriptor": { - "version": "0.1.6", - "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz", - "integrity": "sha1-qeEss66Nh2cn7u84Q/igiXtcmNY=", + "node_modules/is-arguments": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.1.1.tgz", + "integrity": "sha512-8Q7EARjzEnKpt/PCD7e1cgUS0a6X8u5tdSiMqXhojOdoV9TsMsiO+9VLC5vAmO8N7/GmXn7yjR8qnA6bVAEzfA==", "dev": true, "dependencies": { - "kind-of": "^3.0.2" + "call-bind": "^1.0.2", + "has-tostringtag": "^1.0.0" }, "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-accessor-descriptor/node_modules/kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "dev": true, - "dependencies": { - "is-buffer": "^1.1.5" + "node": ">= 0.4" }, - "engines": { - "node": ">=0.10.0" + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/is-arguments": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.1.1.tgz", - "integrity": "sha512-8Q7EARjzEnKpt/PCD7e1cgUS0a6X8u5tdSiMqXhojOdoV9TsMsiO+9VLC5vAmO8N7/GmXn7yjR8qnA6bVAEzfA==", + "node_modules/is-array-buffer": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.4.tgz", + "integrity": "sha512-wcjaerHw0ydZwfhiKbXJWLDY8A7yV7KhjQOpb83hGgGfId/aQa4TOvwyzn2PuswW2gPCYEL/nEAiSVpdOj1lXw==", "dev": true, "dependencies": { "call-bind": "^1.0.2", - "has-tostringtag": "^1.0.0" + "get-intrinsic": "^1.2.1" }, "engines": { "node": ">= 0.4" @@ -7712,10 +8555,13 @@ } }, "node_modules/is-bigint": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.0.2.tgz", - "integrity": "sha512-0JV5+SOCQkIdzjBK9buARcV804Ddu7A0Qet6sHi3FimE9ne6m4BGQZfRn+NZiXbBk4F4XmHfDZIipLj9pX8dSA==", + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.0.4.tgz", + "integrity": "sha512-zB9CruMamjym81i2JZ3UMn54PKGsQzsJeo6xvN3HJJ4CAsQNB6iRutp2To77OfCNuoxspsIhzaPoO1zyCEhFOg==", "dev": true, + "dependencies": { + "has-bigints": "^1.0.1" + }, "funding": { "url": "https://github.com/sponsors/ljharb" } @@ -7732,15 +8578,32 @@ "node": ">=8" } }, + "node_modules/is-boolean-object": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.1.2.tgz", + "integrity": "sha512-gDYaKHJmnj4aWxyj6YHyXVpdQawtVLHU5cb+eztPGczf6cjuTdwve5ZIEfgXqH4e57An1D1AKf8CZ3kYrQRqYA==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-buffer": { "version": "1.1.6", "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", - "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==" + "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==", + "dev": true }, "node_modules/is-callable": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.4.tgz", - "integrity": "sha512-nsuwtxZfMX67Oryl9LCQ+upnC0Z0BgpwntpS89m1H/TLF0zNfzfLMV/9Wa/6MZsj0acpEjAO0KF1xT6ZdLl95w==", + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", + "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", "dev": true, "engines": { "node": ">= 0.4" @@ -7750,39 +8613,33 @@ } }, "node_modules/is-core-module": { - "version": "2.8.1", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.8.1.tgz", - "integrity": "sha512-SdNCUs284hr40hFTFP6l0IfZ/RSrMXF3qgoRHd3/79unUTvrFO/JoXwkGm+5J/Oe3E/b5GsnG330uUNgRpu1PA==", - "dev": true, + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", + "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", + "license": "MIT", "dependencies": { - "has": "^1.0.3" + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/is-data-descriptor": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz", - "integrity": "sha1-C17mSDiOLIYCgueT8YVv7D8wG1Y=", + "node_modules/is-data-view": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-data-view/-/is-data-view-1.0.1.tgz", + "integrity": "sha512-AHkaJrsUVW6wq6JS8y3JnM/GJF/9cf+k20+iDzlSaJrinEo5+7vRiteOSwBhHRiAyQATN1AmY4hwzxJKPmYf+w==", "dev": true, "dependencies": { - "kind-of": "^3.0.2" + "is-typed-array": "^1.1.13" }, "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-data-descriptor/node_modules/kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "dev": true, - "dependencies": { - "is-buffer": "^1.1.5" + "node": ">= 0.4" }, - "engines": { - "node": ">=0.10.0" + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, "node_modules/is-date-object": { @@ -7800,36 +8657,19 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/is-descriptor": { - "version": "0.1.6", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.6.tgz", - "integrity": "sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg==", + "node_modules/is-docker": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz", + "integrity": "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==", "dev": true, - "dependencies": { - "is-accessor-descriptor": "^0.1.6", - "is-data-descriptor": "^0.1.4", - "kind-of": "^5.0.0" + "bin": { + "is-docker": "cli.js" }, "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-descriptor/node_modules/kind-of": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-5.1.0.tgz", - "integrity": "sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-extendable": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", - "integrity": "sha1-YrEQ4omkcUGOPsNqYX1HLjAd/Ik=", - "dev": true, - "engines": { - "node": ">=0.10.0" + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/is-extglob": { @@ -7842,15 +8682,12 @@ } }, "node_modules/is-fullwidth-code-point": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz", - "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", "dev": true, - "dependencies": { - "number-is-nan": "^1.0.0" - }, "engines": { - "node": ">=0.10.0" + "node": ">=8" } }, "node_modules/is-generator-function": { @@ -7899,8 +8736,9 @@ "node_modules/is-natural-number": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/is-natural-number/-/is-natural-number-4.0.1.tgz", - "integrity": "sha1-q5124dtM7VHjXeDHLr7PCfc0zeg=", - "dev": true + "integrity": "sha512-Y4LTamMe0DDQIIAlaer9eKebAlDSV6huy+TWhJVPlzZh2o4tRP5SQWFlLn5N0To4mDD22/qdOq+veo1cSISLgQ==", + "dev": true, + "license": "MIT" }, "node_modules/is-negated-glob": { "version": "1.0.0", @@ -7912,9 +8750,9 @@ } }, "node_modules/is-negative-zero": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.2.tgz", - "integrity": "sha512-dqJvarLawXsFbNDeJW7zAz8ItJ9cd28YufuuFzh0G8pNHjJMnY08Dv7sYX2uF5UpQOwieAeOExEYAWWfu7ZZUA==", + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.3.tgz", + "integrity": "sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw==", "dev": true, "engines": { "node": ">= 0.4" @@ -7924,34 +8762,38 @@ } }, "node_modules/is-number": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", - "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", "dev": true, - "dependencies": { - "kind-of": "^3.0.2" - }, "engines": { - "node": ">=0.10.0" + "node": ">=0.12.0" } }, - "node_modules/is-number/node_modules/kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "node_modules/is-number-object": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.0.7.tgz", + "integrity": "sha512-k1U0IRzLMo7ZlYIfzRu23Oh6MiIFasgpb9X76eqfFZAqwH44UI4KTBvBYIZ1dSL9ZzChTB9ShHfLkR4pdW5krQ==", "dev": true, "dependencies": { - "is-buffer": "^1.1.5" + "has-tostringtag": "^1.0.0" }, "engines": { - "node": ">=0.10.0" + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, "node_modules/is-object": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-object/-/is-object-1.0.1.tgz", - "integrity": "sha1-iVJojF7C/9awPsyF52ngKQMINHA=", - "dev": true + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-object/-/is-object-1.0.2.tgz", + "integrity": "sha512-2rRIahhZr2UWb45fIOuvZGpFtz0TyOZLf32KxBbSoUCeZR495zCKlWUKKUByk3geS2eAs7ZAABt0Y/Rx0GiQGA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } }, "node_modules/is-path-cwd": { "version": "2.2.0", @@ -7974,8 +8816,9 @@ "node_modules/is-plain-obj": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-1.1.0.tgz", - "integrity": "sha1-caUMhCnfync8kqOQpKA7OfzVHT4=", + "integrity": "sha512-yvkRyxmFKEOQ4pNXCmJG5AEQNlXJS5LaONXo5/cLdTZdWvsZ1ioJEonLGAosKlMWE8lwUy/bJzMjcw8az73+Fg==", "dev": true, + "license": "MIT", "engines": { "node": ">=0.10.0" } @@ -8021,19 +8864,26 @@ } }, "node_modules/is-retry-allowed": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-retry-allowed/-/is-retry-allowed-1.1.0.tgz", - "integrity": "sha1-EaBgVotnM5REAz0BJaYaINVk+zQ=", + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/is-retry-allowed/-/is-retry-allowed-1.2.0.tgz", + "integrity": "sha512-RUbUeKwvm3XG2VYamhJL1xFktgjvPzL0Hq8C+6yrWIswDy3BIXGqCxhxkc30N9jqK311gVU137K8Ei55/zVJRg==", "dev": true, + "license": "MIT", "engines": { "node": ">=0.10.0" } }, "node_modules/is-shared-array-buffer": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.1.tgz", - "integrity": "sha512-IU0NmyknYZN0rChcKhRO1X8LYz5Isj/Fsqh8NJOSf+N/hCOTwy29F32Ik7a+QszE63IdvmwdTPDd6cZ5pg4cwA==", + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.3.tgz", + "integrity": "sha512-nA2hv5XIhLR3uVzDDfCIknerhx8XUKnstuOERPNNIinXG7v9u+ohXF67vxm4TPTEPU6lm61ZkwP3c9PCB97rhg==", "dev": true, + "dependencies": { + "call-bind": "^1.0.7" + }, + "engines": { + "node": ">= 0.4" + }, "funding": { "url": "https://github.com/sponsors/ljharb" } @@ -8041,8 +8891,9 @@ "node_modules/is-stream": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz", - "integrity": "sha1-EtSj3U5o4Lec6428hBc66A2RykQ=", + "integrity": "sha512-uQPm8kcs47jx38atAcWTVxyltQYoPT68y9aWYdV6yWXSyW8mzSat0TL6CiWdZeCdF3KrAvpVtnHbTv4RN+rqdQ==", "dev": true, + "license": "MIT", "engines": { "node": ">=0.10.0" } @@ -8078,16 +8929,13 @@ } }, "node_modules/is-typed-array": { - "version": "1.1.8", - "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.8.tgz", - "integrity": "sha512-HqH41TNZq2fgtGT8WHVFVJhBVGuY3AnP3Q36K8JKXUxSxRgk/d+7NjmwG2vo2mYmXK8UYZKu0qH8bVP5gEisjA==", + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.15.tgz", + "integrity": "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==", "dev": true, + "license": "MIT", "dependencies": { - "available-typed-arrays": "^1.0.5", - "call-bind": "^1.0.2", - "es-abstract": "^1.18.5", - "foreach": "^2.0.5", - "has-tostringtag": "^1.0.0" + "which-typed-array": "^1.1.16" }, "engines": { "node": ">= 0.4" @@ -8162,6 +9010,18 @@ "node": ">=0.10.0" } }, + "node_modules/is-wsl": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz", + "integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==", + "dev": true, + "dependencies": { + "is-docker": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/isarray": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", @@ -8183,9 +9043,9 @@ } }, "node_modules/istanbul-lib-coverage": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.0.0.tgz", - "integrity": "sha512-UiUIqxMgRDET6eR+o5HbfRYP1l0hqkWOs7vNxC/mggutCMUIhWMm8gAHb8tHlyfD3/l6rlgNA5cKdDzEAf6hEg==", + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.0.tgz", + "integrity": "sha512-eOeJ5BHCmHYvQK7xt9GkdHuzuCGS1Y6g9Gvnx3Ym33fz/HpLRYxiS0wHNr+m/MBC8B647Xt608vCDEvhl9c6Mw==", "dev": true, "engines": { "node": ">=8" @@ -8204,15 +9064,12 @@ } }, "node_modules/istanbul-lib-instrument": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-4.0.0.tgz", - "integrity": "sha512-Nm4wVHdo7ZXSG30KjZ2Wl5SU/Bw7bDx1PdaiIFzEStdjs0H12mOTncn1GVYuqQSaZxpg87VGBRsVRPGD2cD1AQ==", + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-4.0.3.tgz", + "integrity": "sha512-BXgQl9kf4WTCPCCpmFGoJkz/+uhvm7h7PFKUYxh7qarQd3ER33vHG//qaE8eN25l07YqZPpHXU9I09l/RD5aGQ==", "dev": true, "dependencies": { "@babel/core": "^7.7.5", - "@babel/parser": "^7.7.5", - "@babel/template": "^7.7.4", - "@babel/traverse": "^7.7.4", "@istanbuljs/schema": "^0.1.2", "istanbul-lib-coverage": "^3.0.0", "semver": "^6.3.0" @@ -8221,207 +9078,38 @@ "node": ">=8" } }, - "node_modules/istanbul-lib-instrument/node_modules/@babel/core": { - "version": "7.7.7", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.7.7.tgz", - "integrity": "sha512-jlSjuj/7z138NLZALxVgrx13AOtqip42ATZP7+kYl53GvDV6+4dCek1mVUo8z8c8Xnw/mx2q3d9HWh3griuesQ==", - "dev": true, - "dependencies": { - "@babel/code-frame": "^7.5.5", - "@babel/generator": "^7.7.7", - "@babel/helpers": "^7.7.4", - "@babel/parser": "^7.7.7", - "@babel/template": "^7.7.4", - "@babel/traverse": "^7.7.4", - "@babel/types": "^7.7.4", - "convert-source-map": "^1.7.0", - "debug": "^4.1.0", - "json5": "^2.1.0", - "lodash": "^4.17.13", - "resolve": "^1.3.2", - "semver": "^5.4.1", - "source-map": "^0.5.0" - }, - "engines": { - "node": ">=6.9.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/babel" - } - }, - "node_modules/istanbul-lib-instrument/node_modules/@babel/core/node_modules/semver": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", - "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", - "dev": true, - "bin": { - "semver": "bin/semver" - } - }, - "node_modules/istanbul-lib-instrument/node_modules/@babel/generator": { - "version": "7.7.7", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.7.7.tgz", - "integrity": "sha512-/AOIBpHh/JU1l0ZFS4kiRCBnLi6OTHzh0RPk3h9isBxkkqELtQNFi1Vr/tiG9p1yfoUdKVwISuXWQR+hwwM4VQ==", - "dev": true, - "dependencies": { - "@babel/types": "^7.7.4", - "jsesc": "^2.5.1", - "lodash": "^4.17.13", - "source-map": "^0.5.0" - } - }, - "node_modules/istanbul-lib-instrument/node_modules/@babel/helper-function-name": { - "version": "7.7.4", - "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.7.4.tgz", - "integrity": "sha512-AnkGIdiBhEuiwdoMnKm7jfPfqItZhgRaZfMg1XX3bS25INOnLPjPG1Ppnajh8eqgt5kPJnfqrRHqFqmjKDZLzQ==", - "dev": true, - "dependencies": { - "@babel/helper-get-function-arity": "^7.7.4", - "@babel/template": "^7.7.4", - "@babel/types": "^7.7.4" - } - }, - "node_modules/istanbul-lib-instrument/node_modules/@babel/helper-get-function-arity": { - "version": "7.7.4", - "resolved": "https://registry.npmjs.org/@babel/helper-get-function-arity/-/helper-get-function-arity-7.7.4.tgz", - "integrity": "sha512-QTGKEdCkjgzgfJ3bAyRwF4yyT3pg+vDgan8DSivq1eS0gwi+KGKE5x8kRcbeFTb/673mkO5SN1IZfmCfA5o+EA==", - "dev": true, - "dependencies": { - "@babel/types": "^7.7.4" - } - }, - "node_modules/istanbul-lib-instrument/node_modules/@babel/helper-split-export-declaration": { - "version": "7.7.4", - "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.7.4.tgz", - "integrity": "sha512-guAg1SXFcVr04Guk9eq0S4/rWS++sbmyqosJzVs8+1fH5NI+ZcmkaSkc7dmtAFbHFva6yRJnjW3yAcGxjueDug==", - "dev": true, - "dependencies": { - "@babel/types": "^7.7.4" - } - }, - "node_modules/istanbul-lib-instrument/node_modules/@babel/helpers": { - "version": "7.7.4", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.7.4.tgz", - "integrity": "sha512-ak5NGZGJ6LV85Q1Zc9gn2n+ayXOizryhjSUBTdu5ih1tlVCJeuQENzc4ItyCVhINVXvIT/ZQ4mheGIsfBkpskg==", - "dev": true, - "dependencies": { - "@babel/template": "^7.7.4", - "@babel/traverse": "^7.7.4", - "@babel/types": "^7.7.4" - } - }, - "node_modules/istanbul-lib-instrument/node_modules/@babel/parser": { - "version": "7.7.7", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.7.7.tgz", - "integrity": "sha512-WtTZMZAZLbeymhkd/sEaPD8IQyGAhmuTuvTzLiCFM7iXiVdY0gc0IaI+cW0fh1BnSMbJSzXX6/fHllgHKwHhXw==", - "dev": true, - "bin": { - "parser": "bin/babel-parser.js" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/istanbul-lib-instrument/node_modules/@babel/template": { - "version": "7.7.4", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.7.4.tgz", - "integrity": "sha512-qUzihgVPguAzXCK7WXw8pqs6cEwi54s3E+HrejlkuWO6ivMKx9hZl3Y2fSXp9i5HgyWmj7RKP+ulaYnKM4yYxw==", - "dev": true, - "dependencies": { - "@babel/code-frame": "^7.0.0", - "@babel/parser": "^7.7.4", - "@babel/types": "^7.7.4" - } - }, - "node_modules/istanbul-lib-instrument/node_modules/@babel/traverse": { - "version": "7.7.4", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.7.4.tgz", - "integrity": "sha512-P1L58hQyupn8+ezVA2z5KBm4/Zr4lCC8dwKCMYzsa5jFMDMQAzaBNy9W5VjB+KAmBjb40U7a/H6ao+Xo+9saIw==", - "dev": true, - "dependencies": { - "@babel/code-frame": "^7.5.5", - "@babel/generator": "^7.7.4", - "@babel/helper-function-name": "^7.7.4", - "@babel/helper-split-export-declaration": "^7.7.4", - "@babel/parser": "^7.7.4", - "@babel/types": "^7.7.4", - "debug": "^4.1.0", - "globals": "^11.1.0", - "lodash": "^4.17.13" - } - }, - "node_modules/istanbul-lib-instrument/node_modules/@babel/types": { - "version": "7.7.4", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.7.4.tgz", - "integrity": "sha512-cz5Ji23KCi4T+YIE/BolWosrJuSmoZeN1EFnRtBwF+KKLi8GG/Z2c2hOJJeCXPk4mwk4QFvTmwIodJowXgttRA==", - "dev": true, - "dependencies": { - "esutils": "^2.0.2", - "lodash": "^4.17.13", - "to-fast-properties": "^2.0.0" - } - }, - "node_modules/istanbul-lib-instrument/node_modules/convert-source-map": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.7.0.tgz", - "integrity": "sha512-4FJkXzKXEDB1snCFZlLP4gpC3JILicCpGbzG9f9G7tGqGCzETQ2hWPrcinA9oU4wtf2biUaEH5065UnMeR33oA==", - "dev": true, - "dependencies": { - "safe-buffer": "~5.1.1" - } - }, - "node_modules/istanbul-lib-instrument/node_modules/debug": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", - "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", - "deprecated": "Debug versions >=3.2.0 <3.2.7 || >=4 <4.3.1 have a low-severity ReDos regression when used in a Node.js environment. It is recommended you upgrade to 3.2.7 or 4.3.1. (https://github.com/visionmedia/debug/issues/797)", - "dev": true, - "dependencies": { - "ms": "^2.1.1" - } - }, "node_modules/istanbul-lib-instrument/node_modules/semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", "dev": true, "bin": { "semver": "bin/semver.js" } }, - "node_modules/istanbul-lib-instrument/node_modules/source-map": { - "version": "0.5.7", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", - "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/istanbul-lib-processinfo": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/istanbul-lib-processinfo/-/istanbul-lib-processinfo-2.0.2.tgz", - "integrity": "sha512-kOwpa7z9hme+IBPZMzQ5vdQj8srYgAtaRqeI48NGmAQ+/5yKiHLV0QbYqQpxsdEF0+w14SoB8YbnHKcXE2KnYw==", + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/istanbul-lib-processinfo/-/istanbul-lib-processinfo-2.0.3.tgz", + "integrity": "sha512-NkwHbo3E00oybX6NGJi6ar0B29vxyvNwoC7eJ4G4Yq28UfY758Hgn/heV8VRFhevPED4LXfFz0DQ8z/0kw9zMg==", "dev": true, "dependencies": { "archy": "^1.0.0", - "cross-spawn": "^7.0.0", - "istanbul-lib-coverage": "^3.0.0-alpha.1", - "make-dir": "^3.0.0", + "cross-spawn": "^7.0.3", + "istanbul-lib-coverage": "^3.2.0", "p-map": "^3.0.0", "rimraf": "^3.0.0", - "uuid": "^3.3.3" + "uuid": "^8.3.2" }, "engines": { "node": ">=8" } }, "node_modules/istanbul-lib-processinfo/node_modules/cross-spawn": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.1.tgz", - "integrity": "sha512-u7v4o84SwFpD32Z8IIcPZ6z1/ie24O6RU3RbtL5Y316l3KuHVPx9ItBgWQ6VlfAFnRnTtMUrsQ9MUUTuEZjogg==", + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", "dev": true, + "license": "MIT", "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", @@ -8474,13 +9162,12 @@ } }, "node_modules/istanbul-lib-processinfo/node_modules/uuid": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz", - "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==", - "deprecated": "Please upgrade to version 7 or higher. Older versions may use Math.random() in certain circumstances, which is known to be problematic. See https://v8.dev/blog/math-random for details.", + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", "dev": true, "bin": { - "uuid": "bin/uuid" + "uuid": "dist/bin/uuid" } }, "node_modules/istanbul-lib-report": { @@ -8533,19 +9220,26 @@ } }, "node_modules/istanbul-lib-source-maps/node_modules/debug": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", - "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", - "deprecated": "Debug versions >=3.2.0 <3.2.7 || >=4 <4.3.1 have a low-severity ReDos regression when used in a Node.js environment. It is recommended you upgrade to 3.2.7 or 4.3.1. (https://github.com/visionmedia/debug/issues/797)", + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", "dev": true, "dependencies": { - "ms": "^2.1.1" + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } } }, "node_modules/istanbul-reports": { - "version": "3.1.4", - "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.1.4.tgz", - "integrity": "sha512-r1/DshN4KSE7xWEknZLLLLDn5CJybV3nw01VTkp6D5jzLuELlcbudfj/eSQFvrKsJuTVCGnePO7ho82Nw9zzfw==", + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.1.5.tgz", + "integrity": "sha512-nUsEMa9pBt/NOHqbcbeJEgqIlY/K7rVWUX6Lql2orY5e9roQOthbR3vtY4zzf2orPELg80fnxxk9zUyPlgwD1w==", "dev": true, "dependencies": { "html-escaper": "^2.0.0", @@ -8560,6 +9254,7 @@ "resolved": "https://registry.npmjs.org/isurl/-/isurl-1.0.0.tgz", "integrity": "sha512-1P/yWsxPlDtn7QeRD+ULKQPaIaN6yF368GZ2vDfv0AL0NwpStafjWCDDdn0k8wgFMWpVAqG7oJhxHnlud42i9w==", "dev": true, + "license": "MIT", "dependencies": { "has-to-string-tag-x": "^1.2.0", "is-object": "^1.0.1" @@ -8568,6 +9263,22 @@ "node": ">= 4" } }, + "node_modules/jackspeak": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, "node_modules/jest-worker": { "version": "27.5.1", "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-27.5.1.tgz", @@ -8613,9 +9324,9 @@ "dev": true }, "node_modules/js-yaml": { - "version": "3.13.1", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.13.1.tgz", - "integrity": "sha512-YfbcO7jXDdyj0DGxYVSlSeQNHbD7XPWvrVWeVUujrQEoZzWJIRrCPoyk6kL6IAjAG2IolMK4T0hNUe0HOUs5Jw==", + "version": "3.14.2", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.2.tgz", + "integrity": "sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==", "dev": true, "dependencies": { "argparse": "^1.0.7", @@ -8653,8 +9364,9 @@ "node_modules/json-buffer": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.0.tgz", - "integrity": "sha1-Wx85evx11ne96Lz8Dkfh+aPZqJg=", - "dev": true + "integrity": "sha512-CuUqjv0FUZIdXkHPI8MezCnFCdaTAacej1TZYulLoAg1h/PhwkdXFN4V/gzY4g+fMBCOV2xF+rp7t2XD2ns/NQ==", + "dev": true, + "license": "MIT" }, "node_modules/json-parse-even-better-errors": { "version": "2.3.1", @@ -8674,12 +9386,6 @@ "integrity": "sha1-nbe1lJatPzz+8wp1FC0tkwrXJlE=", "dev": true }, - "node_modules/json-stringify-safe": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", - "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=", - "dev": true - }, "node_modules/json5": { "version": "2.2.3", "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", @@ -8697,41 +9403,111 @@ "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.2.0.tgz", "integrity": "sha512-gfFQZrcTc8CnKXp6Y4/CBT3fTc0OVuDofpre4aEeEpSBPV5X5v4+Vmx+8snU7RLPrNHPKSgLxGo9YuQzz20o+w==" }, + "node_modules/jsonwebtoken": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.2.tgz", + "integrity": "sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ==", + "dev": true, + "dependencies": { + "jws": "^3.2.2", + "lodash.includes": "^4.3.0", + "lodash.isboolean": "^3.0.3", + "lodash.isinteger": "^4.0.4", + "lodash.isnumber": "^3.0.3", + "lodash.isplainobject": "^4.0.6", + "lodash.isstring": "^4.0.1", + "lodash.once": "^4.0.0", + "ms": "^2.1.1", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=12", + "npm": ">=6" + } + }, + "node_modules/jsonwebtoken/node_modules/jwa": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.2.tgz", + "integrity": "sha512-eeH5JO+21J78qMvTIDdBXidBd6nG2kZjg5Ohz/1fpa28Z4CcsWUzJ1ZZyFq/3z3N17aZy+ZuBoHljASbL1WfOw==", + "dev": true, + "dependencies": { + "buffer-equal-constant-time": "^1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/jsonwebtoken/node_modules/jws": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/jws/-/jws-3.2.3.tgz", + "integrity": "sha512-byiJ0FLRdLdSVSReO/U4E7RoEyOCKnEnEPMjq3HxWtvzLsV08/i5RQKsFVNkCldrCaPr2vDNAOMsfs8T/Hze7g==", + "dev": true, + "dependencies": { + "jwa": "^1.4.2", + "safe-buffer": "^5.0.1" + } + }, "node_modules/jsx-ast-utils": { "version": "3.2.1", "resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.2.1.tgz", "integrity": "sha512-uP5vu8xfy2F9A6LGC22KO7e2/vGTS1MhP+18f++ZNlf0Ohaxbc9nIEwHAsejlJKyzfZzU5UIhe5ItYkitcZnZA==", "dev": true, "dependencies": { - "array-includes": "^3.1.3", - "object.assign": "^4.1.2" - }, - "engines": { - "node": ">=4.0" + "array-includes": "^3.1.3", + "object.assign": "^4.1.2" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/jszip": { + "version": "3.10.1", + "resolved": "https://registry.npmjs.org/jszip/-/jszip-3.10.1.tgz", + "integrity": "sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==", + "dev": true, + "dependencies": { + "lie": "~3.3.0", + "pako": "~1.0.2", + "readable-stream": "~2.3.6", + "setimmediate": "^1.0.5" + } + }, + "node_modules/just-extend": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/just-extend/-/just-extend-6.2.0.tgz", + "integrity": "sha512-cYofQu2Xpom82S6qD778jBDpwvvy39s1l/hrYij2u9AMdQcGRpaBu6kY4mVhuno5kJVi1DAz4aiphA2WI1/OAw==", + "dev": true + }, + "node_modules/jwa": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.1.tgz", + "integrity": "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==", + "dev": true, + "dependencies": { + "buffer-equal-constant-time": "^1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/jws": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.1.tgz", + "integrity": "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==", + "dev": true, + "dependencies": { + "jwa": "^2.0.1", + "safe-buffer": "^5.0.1" } }, - "node_modules/just-debounce": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/just-debounce/-/just-debounce-1.0.0.tgz", - "integrity": "sha1-h/zPrv/AtozRnVX2cilD+SnqNeo=", - "dev": true - }, - "node_modules/just-extend": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/just-extend/-/just-extend-4.2.1.tgz", - "integrity": "sha512-g3UB796vUFIY90VIv/WX3L2c8CS2MdWUww3CNrYmqza1Fg0DURc2K/O4YrnklBdQarSJ/y8JnJYDGc+1iumQjg==", - "dev": true - }, "node_modules/keytar": { - "version": "7.7.0", - "resolved": "https://registry.npmjs.org/keytar/-/keytar-7.7.0.tgz", - "integrity": "sha512-YEY9HWqThQc5q5xbXbRwsZTh2PJ36OSYRjSv3NN2xf5s5dpLTjEZnC2YikR29OaVybf9nQ0dJ/80i40RS97t/A==", + "version": "7.9.0", + "resolved": "https://registry.npmjs.org/keytar/-/keytar-7.9.0.tgz", + "integrity": "sha512-VPD8mtVtm5JNtA2AErl6Chp06JBfy7diFQ7TQQhdpWOl6MrCRB+eRbvAZUsbGQS9kiMq0coJsy0W0vHpDCkWsQ==", "dev": true, "hasInstallScript": true, "optional": true, "dependencies": { - "node-addon-api": "^3.0.0", - "prebuild-install": "^6.0.0" + "node-addon-api": "^4.3.0", + "prebuild-install": "^7.0.1" } }, "node_modules/keyv": { @@ -8739,6 +9515,7 @@ "resolved": "https://registry.npmjs.org/keyv/-/keyv-3.0.0.tgz", "integrity": "sha512-eguHnq22OE3uVoSYG0LVWNP+4ppamWr9+zWBe1bsNcovIMy6huUJFPgy4mGwCd/rnl3vOLGW1MTlu4c57CT1xA==", "dev": true, + "license": "MIT", "dependencies": { "json-buffer": "3.0.0" } @@ -8768,16 +9545,12 @@ } }, "node_modules/last-run": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/last-run/-/last-run-1.1.1.tgz", - "integrity": "sha1-RblpQsF7HHnHchmCWbqUO+v4yls=", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/last-run/-/last-run-2.0.0.tgz", + "integrity": "sha512-j+y6WhTLN4Itnf9j5ZQos1BGPCS8DAwmgMroR3OzfxAsBxam0hMw7J8M3KqZl0pLQJ1jNnwIexg5DYpC/ctwEQ==", "dev": true, - "dependencies": { - "default-resolution": "^2.0.0", - "es6-weak-map": "^2.0.1" - }, "engines": { - "node": ">= 0.10" + "node": ">= 10.13.0" } }, "node_modules/lazystream": { @@ -8792,18 +9565,6 @@ "node": ">= 0.6.3" } }, - "node_modules/lcid": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/lcid/-/lcid-1.0.0.tgz", - "integrity": "sha1-MIrMr6C8SDo4Z7S28rlQYlHRuDU=", - "dev": true, - "dependencies": { - "invert-kv": "^1.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/lead": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/lead/-/lead-1.0.0.tgz", @@ -8825,84 +9586,75 @@ "node": ">=6" } }, - "node_modules/liftoff": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/liftoff/-/liftoff-3.1.0.tgz", - "integrity": "sha512-DlIPlJUkCV0Ips2zf2pJP0unEoT1kwYhiiPUGF3s/jtxTCjziNLoiVVh+jqWOWeFi6mmwQ5fNxvAUyPad4Dfog==", + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", "dev": true, "dependencies": { - "extend": "^3.0.0", - "findup-sync": "^3.0.0", - "fined": "^1.0.1", - "flagged-respawn": "^1.0.0", - "is-plain-object": "^2.0.4", - "object.map": "^1.0.0", - "rechoir": "^0.6.2", - "resolve": "^1.1.7" + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" }, "engines": { - "node": ">= 0.8" + "node": ">= 0.8.0" } }, - "node_modules/linkify-it": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-3.0.3.tgz", - "integrity": "sha512-ynTsyrFSdE5oZ/O9GEf00kPngmOfVwazR5GKDq6EYfhlpFug3J2zybX56a2PRRpc9P+FuSoGNAwjlbDs9jJBPQ==", + "node_modules/lie": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz", + "integrity": "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==", "dev": true, "dependencies": { - "uc.micro": "^1.0.1" + "immediate": "~3.0.5" } }, - "node_modules/listenercount": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/listenercount/-/listenercount-1.0.1.tgz", - "integrity": "sha1-hMinKrWcRyUyFIDJdeZQg0LnCTc=", - "dev": true - }, - "node_modules/load-json-file": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-1.1.0.tgz", - "integrity": "sha1-lWkFcI1YtLq0wiYbBPWfMcmTdMA=", + "node_modules/liftoff": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/liftoff/-/liftoff-5.0.0.tgz", + "integrity": "sha512-a5BQjbCHnB+cy+gsro8lXJ4kZluzOijzJ1UVVfyJYZC+IP2pLv1h4+aysQeKuTmyO8NAqfyQAk4HWaP/HjcKTg==", "dev": true, "dependencies": { - "graceful-fs": "^4.1.2", - "parse-json": "^2.2.0", - "pify": "^2.0.0", - "pinkie-promise": "^2.0.0", - "strip-bom": "^2.0.0" + "extend": "^3.0.2", + "findup-sync": "^5.0.0", + "fined": "^2.0.0", + "flagged-respawn": "^2.0.0", + "is-plain-object": "^5.0.0", + "rechoir": "^0.8.0", + "resolve": "^1.20.0" }, "engines": { - "node": ">=0.10.0" + "node": ">=10.13.0" } }, - "node_modules/load-json-file/node_modules/parse-json": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-2.2.0.tgz", - "integrity": "sha1-9ID0BDTvgHQfhGkJn43qGPVaTck=", + "node_modules/liftoff/node_modules/is-plain-object": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-5.0.0.tgz", + "integrity": "sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==", "dev": true, - "dependencies": { - "error-ex": "^1.2.0" - }, "engines": { "node": ">=0.10.0" } }, - "node_modules/load-json-file/node_modules/pify": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", - "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=", + "node_modules/linkify-it": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-3.0.3.tgz", + "integrity": "sha512-ynTsyrFSdE5oZ/O9GEf00kPngmOfVwazR5GKDq6EYfhlpFug3J2zybX56a2PRRpc9P+FuSoGNAwjlbDs9jJBPQ==", "dev": true, - "engines": { - "node": ">=0.10.0" + "dependencies": { + "uc.micro": "^1.0.1" } }, "node_modules/loader-runner": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.2.0.tgz", - "integrity": "sha512-92+huvxMvYlMzMt0iIOukcwYBFpkYJdpl2xsZ7LrlayO7E8SOv+JJUEK17B/dJIHAOLMfh2dZZ/Y18WgmGtYNw==", + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.1.tgz", + "integrity": "sha512-IWqP2SCPhyVFTBtRcgMHdzlf9ul25NwaFx4wCEH/KjAXuuHY4yNjvPXsBokp8jCB936PyWRaPKUNh8NvylLp2Q==", "dev": true, "engines": { "node": ">=6.11.5" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" } }, "node_modules/loader-utils": { @@ -8932,15 +9684,9 @@ } }, "node_modules/lodash": { - "version": "4.17.21", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", - "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" - }, - "node_modules/lodash._reinterpolate": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/lodash._reinterpolate/-/lodash._reinterpolate-3.0.0.tgz", - "integrity": "sha1-DM8tiRZq8Ds2Y8eWU4t1rG4RTZ0=", - "dev": true + "version": "4.18.1", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz", + "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==" }, "node_modules/lodash.flattendeep": { "version": "4.4.0", @@ -8951,44 +9697,55 @@ "node_modules/lodash.get": { "version": "4.4.2", "resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz", - "integrity": "sha1-LRd/ZS+jHpObRDjVNBSZ36OCXpk=", + "integrity": "sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ==", "dev": true }, - "node_modules/lodash.merge": { - "version": "4.6.2", - "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", - "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "node_modules/lodash.includes": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", + "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==", "dev": true }, - "node_modules/lodash.some": { - "version": "4.6.0", - "resolved": "https://registry.npmjs.org/lodash.some/-/lodash.some-4.6.0.tgz", - "integrity": "sha1-G7nzFO9ri63tE7VJFpsqlF62jk0=", + "node_modules/lodash.isboolean": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", + "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==", "dev": true }, - "node_modules/lodash.template": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/lodash.template/-/lodash.template-4.5.0.tgz", - "integrity": "sha512-84vYFxIkmidUiFxidA/KjjH9pAycqW+h980j7Fuz5qxRtO9pgB7MDFTdys1N7A5mcucRiDyEq4fusljItR1T/A==", - "dev": true, - "dependencies": { - "lodash._reinterpolate": "^3.0.0", - "lodash.templatesettings": "^4.0.0" - } + "node_modules/lodash.isinteger": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", + "integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==", + "dev": true }, - "node_modules/lodash.templatesettings": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/lodash.templatesettings/-/lodash.templatesettings-4.2.0.tgz", - "integrity": "sha512-stgLz+i3Aa9mZgnjr/O+v9ruKZsPsndy7qPZOchbqk2cnTU1ZaldKK+v7m54WoKIyxiuMZTKT2H81F8BeAc3ZQ==", - "dev": true, - "dependencies": { - "lodash._reinterpolate": "^3.0.0" - } + "node_modules/lodash.isnumber": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz", + "integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==", + "dev": true }, - "node_modules/lodash.truncate": { - "version": "4.4.2", - "resolved": "https://registry.npmjs.org/lodash.truncate/-/lodash.truncate-4.4.2.tgz", - "integrity": "sha1-WjUNoLERO4N+z//VgSy+WNbq4ZM=", + "node_modules/lodash.isplainobject": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==", + "dev": true + }, + "node_modules/lodash.isstring": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", + "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==", + "dev": true + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true + }, + "node_modules/lodash.once": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", + "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==", "dev": true }, "node_modules/log-symbols": { @@ -9103,6 +9860,7 @@ "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-1.0.1.tgz", "integrity": "sha512-G2Lj61tXDnVFFOi8VZds+SoQjtQC3dgokKdDG2mTm1tx4m50NUHBOZSBwQQHyy0V12A0JTG4icfZQH+xPyh8VA==", "dev": true, + "license": "MIT", "engines": { "node": ">=0.10.0" } @@ -9134,9 +9892,9 @@ } }, "node_modules/make-dir/node_modules/semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", "dev": true, "bin": { "semver": "bin/semver.js" @@ -9148,35 +9906,11 @@ "integrity": "sha512-c3sIjNUow0+8swNwVpqoH4YCShKNFkMaw6oH1mNS2haDZQqkeZFlHS3dhoeEbKKmJB4vXpJucU6oH75aDYeE9g==", "dev": true }, - "node_modules/make-iterator": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/make-iterator/-/make-iterator-1.0.1.tgz", - "integrity": "sha512-pxiuXh0iVEq7VM7KMIhs5gxsfxCux2URptUQaXo4iZZJxBAzTPOLE2BumO5dbfVYq/hBJFBR/a1mFDmOx5AGmw==", - "dev": true, - "dependencies": { - "kind-of": "^6.0.2" - }, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/map-cache": { "version": "0.2.2", "resolved": "https://registry.npmjs.org/map-cache/-/map-cache-0.2.2.tgz", - "integrity": "sha1-wyq9C9ZSXZsFFkW7TyasXcmKDb8=", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/map-visit": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/map-visit/-/map-visit-1.0.0.tgz", - "integrity": "sha1-7Nyo8TFE5mDxtb1B8S80edmN+48=", + "integrity": "sha512-8y/eV9QQZCiyn1SprXSrCmqJN0yNRATe+PO8ztwqrvrbdRLA3eYJF0yaR0YayLWkMbsQSKWS9N2gPcGEc4UsZg==", "dev": true, - "dependencies": { - "object-visit": "^1.0.0" - }, "engines": { "node": ">=0.10.0" } @@ -9203,52 +9937,20 @@ "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", "dev": true }, - "node_modules/matchdep": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/matchdep/-/matchdep-2.0.0.tgz", - "integrity": "sha1-xvNINKDY28OzfCfui7yyfHd1WC4=", - "dev": true, - "dependencies": { - "findup-sync": "^2.0.0", - "micromatch": "^3.0.4", - "resolve": "^1.4.0", - "stack-trace": "0.0.10" - }, - "engines": { - "node": ">= 0.10.0" - } - }, - "node_modules/matchdep/node_modules/findup-sync": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/findup-sync/-/findup-sync-2.0.0.tgz", - "integrity": "sha1-kyaxSIwi0aYIhlCoaQGy2akKLLw=", - "dev": true, - "dependencies": { - "detect-file": "^1.0.0", - "is-glob": "^3.1.0", - "micromatch": "^3.0.4", - "resolve-dir": "^1.0.1" - }, - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/matchdep/node_modules/is-glob": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-3.1.0.tgz", - "integrity": "sha1-e6WuJCF4BKxwcHuWkiVnSGzD6Eo=", - "dev": true, - "dependencies": { - "is-extglob": "^2.1.0" - }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", "engines": { - "node": ">=0.10.0" + "node": ">= 0.4" } }, "node_modules/md5": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/md5/-/md5-2.3.0.tgz", "integrity": "sha512-T1GITYmFaKuO91vxyoQMFETst+O71VUPEU3ze5GNzDm0OWdP8v1ziTaAEPUr/3kLsY3Sftgz242A1SetQiDL7g==", + "dev": true, "dependencies": { "charenc": "0.0.2", "crypt": "0.0.2", @@ -9287,27 +9989,16 @@ } }, "node_modules/micromatch": { - "version": "3.1.10", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-3.1.10.tgz", - "integrity": "sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg==", + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", "dev": true, "dependencies": { - "arr-diff": "^4.0.0", - "array-unique": "^0.3.2", - "braces": "^2.3.1", - "define-property": "^2.0.2", - "extend-shallow": "^3.0.2", - "extglob": "^2.0.4", - "fragment-cache": "^0.2.1", - "kind-of": "^6.0.2", - "nanomatch": "^1.2.9", - "object.pick": "^1.3.0", - "regex-not": "^1.0.0", - "snapdragon": "^0.8.1", - "to-regex": "^3.0.2" + "braces": "^3.0.3", + "picomatch": "^2.3.1" }, "engines": { - "node": ">=0.10.0" + "node": ">=8.6" } }, "node_modules/miller-rabin": { @@ -9368,6 +10059,7 @@ "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-1.0.1.tgz", "integrity": "sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ==", "dev": true, + "license": "MIT", "engines": { "node": ">=4" } @@ -9375,7 +10067,8 @@ "node_modules/minimalistic-assert": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz", - "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==" + "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==", + "dev": true }, "node_modules/minimalistic-crypto-utils": { "version": "1.0.1", @@ -9384,9 +10077,9 @@ "dev": true }, "node_modules/minimatch": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.0.tgz", - "integrity": "sha512-9TPBGGak4nHfGZsPBohm9AWg6NoT7QTCehS3BIJABslyZbzxfV78QM2Y6+i741OPZIafFAaiiEMh5OyIrJPgtg==", + "version": "5.1.8", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.8.tgz", + "integrity": "sha512-7RN35vit8DeBclkofOVmBY0eDAZZQd1HzmukRdSyz95CRh8FT54eqnbj0krQr3mrHR6sfRyYkyhwBWjoV5uqlQ==", "dependencies": { "brace-expansion": "^2.0.1" }, @@ -9395,9 +10088,9 @@ } }, "node_modules/minimatch/node_modules/brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.3.tgz", + "integrity": "sha512-MCV/fYJEbqx68aE58kv2cA/kiky1G8vux3OR6/jbS+jIMe/6fJWa0DTzJU7dqijOWYwHi1t29FlfYI9uytqlpA==", "dependencies": { "balanced-match": "^1.0.0" } @@ -9405,37 +10098,24 @@ "node_modules/minimist": { "version": "1.2.6", "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.6.tgz", - "integrity": "sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==" - }, - "node_modules/mixin-deep": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/mixin-deep/-/mixin-deep-1.3.2.tgz", - "integrity": "sha512-WRoDn//mXBiJ1H40rqa3vH0toePwSsGb45iInWlTySa+Uu4k3tYUSxa2v1KqAiLtvlrSzaExqS1gtk96A9zvEA==", - "dev": true, - "dependencies": { - "for-in": "^1.0.2", - "is-extendable": "^1.0.1" - }, - "engines": { - "node": ">=0.10.0" - } + "integrity": "sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==", + "dev": true }, - "node_modules/mixin-deep/node_modules/is-extendable": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz", - "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==", + "node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", "dev": true, - "dependencies": { - "is-plain-object": "^2.0.4" - }, + "license": "ISC", "engines": { - "node": ">=0.10.0" + "node": ">=16 || 14 >=14.17" } }, "node_modules/mkdirp": { "version": "0.5.5", "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.5.tgz", "integrity": "sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==", + "dev": true, "dependencies": { "minimist": "^1.2.5" }, @@ -9451,46 +10131,40 @@ "optional": true }, "node_modules/mocha": { - "version": "9.2.2", - "resolved": "https://registry.npmjs.org/mocha/-/mocha-9.2.2.tgz", - "integrity": "sha512-L6XC3EdwT6YrIk0yXpavvLkn8h+EU+Y5UcCHKECyMbdUIxyMuZj4bX4U9e1nvnvUUvQVsV2VHQr5zLdcUkhW/g==", - "dev": true, - "dependencies": { - "@ungap/promise-all-settled": "1.1.2", - "ansi-colors": "4.1.1", - "browser-stdout": "1.3.1", - "chokidar": "3.5.3", - "debug": "4.3.3", - "diff": "5.0.0", - "escape-string-regexp": "4.0.0", - "find-up": "5.0.0", - "glob": "7.2.0", - "growl": "1.10.5", - "he": "1.2.0", - "js-yaml": "4.1.0", - "log-symbols": "4.1.0", - "minimatch": "4.2.1", - "ms": "2.1.3", - "nanoid": "3.3.1", - "serialize-javascript": "6.0.0", - "strip-json-comments": "3.1.1", - "supports-color": "8.1.1", - "which": "2.0.2", - "workerpool": "6.2.0", - "yargs": "16.2.0", - "yargs-parser": "20.2.4", - "yargs-unparser": "2.0.0" + "version": "11.7.5", + "resolved": "https://registry.npmjs.org/mocha/-/mocha-11.7.5.tgz", + "integrity": "sha512-mTT6RgopEYABzXWFx+GcJ+ZQ32kp4fMf0xvpZIIfSq9Z8lC/++MtcCnQ9t5FP2veYEP95FIYSvW+U9fV4xrlig==", + "dev": true, + "license": "MIT", + "dependencies": { + "browser-stdout": "^1.3.1", + "chokidar": "^4.0.1", + "debug": "^4.3.5", + "diff": "^7.0.0", + "escape-string-regexp": "^4.0.0", + "find-up": "^5.0.0", + "glob": "^10.4.5", + "he": "^1.2.0", + "is-path-inside": "^3.0.3", + "js-yaml": "^4.1.0", + "log-symbols": "^4.1.0", + "minimatch": "^9.0.5", + "ms": "^2.1.3", + "picocolors": "^1.1.1", + "serialize-javascript": "^6.0.2", + "strip-json-comments": "^3.1.1", + "supports-color": "^8.1.1", + "workerpool": "^9.2.0", + "yargs": "^17.7.2", + "yargs-parser": "^21.1.1", + "yargs-unparser": "^2.0.0" }, "bin": { "_mocha": "bin/_mocha", - "mocha": "bin/mocha" + "mocha": "bin/mocha.js" }, "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/mochajs" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, "node_modules/mocha-junit-reporter": { @@ -9542,72 +10216,75 @@ } } }, - "node_modules/mocha/node_modules/ansi-colors": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.1.tgz", - "integrity": "sha512-JoX0apGbHaUJBNl6yF+p6JAFYZ666/hhCGKN5t9QFjbJQKUU/g8MNbFDbvfrgKXvI1QpZplPOnwIo99lX/AAmA==", + "node_modules/mocha/node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true + }, + "node_modules/mocha/node_modules/brace-expansion": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.3.tgz", + "integrity": "sha512-MCV/fYJEbqx68aE58kv2cA/kiky1G8vux3OR6/jbS+jIMe/6fJWa0DTzJU7dqijOWYwHi1t29FlfYI9uytqlpA==", "dev": true, - "engines": { - "node": ">=6" + "dependencies": { + "balanced-match": "^1.0.0" } }, - "node_modules/mocha/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "node_modules/mocha/node_modules/chokidar": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", + "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", "dev": true, + "license": "MIT", "dependencies": { - "color-convert": "^2.0.1" + "readdirp": "^4.0.1" }, "engines": { - "node": ">=8" + "node": ">= 14.16.0" }, "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" + "url": "https://paulmillr.com/funding/" } }, - "node_modules/mocha/node_modules/argparse": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", - "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", - "dev": true - }, "node_modules/mocha/node_modules/cliui": { - "version": "7.0.4", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", - "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", "dev": true, + "license": "ISC", "dependencies": { "string-width": "^4.2.0", - "strip-ansi": "^6.0.0", + "strip-ansi": "^6.0.1", "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" } }, - "node_modules/mocha/node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "node_modules/mocha/node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", "dev": true, + "license": "MIT", "dependencies": { - "color-name": "~1.1.4" + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" }, "engines": { - "node": ">=7.0.0" + "node": ">= 8" } }, - "node_modules/mocha/node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true - }, "node_modules/mocha/node_modules/debug": { - "version": "4.3.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.3.tgz", - "integrity": "sha512-/zxw5+vh1Tfv+4Qn7a5nsbcJKPaSvCDhojn6FEl9vupwK2VCSDtEiEtqr8DFtzYFOdz63LBkxec7DYuc2jon6Q==", + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", + "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", "dev": true, + "license": "MIT", "dependencies": { - "ms": "2.1.2" + "ms": "^2.1.3" }, "engines": { "node": ">=6.0" @@ -9616,19 +10293,14 @@ "supports-color": { "optional": true } - } - }, - "node_modules/mocha/node_modules/debug/node_modules/ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true + } }, "node_modules/mocha/node_modules/diff": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/diff/-/diff-5.0.0.tgz", - "integrity": "sha512-/VTCrvm5Z0JGty/BWHljh+BAiw3IK+2j87NGMu8Nwc/f48WoDAC395uomO9ZD117ZOBaHmkX1oyLvkVM/aIT3w==", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-7.0.0.tgz", + "integrity": "sha512-PJWHUb1RFevKCwaFA9RlG5tCd+FO5iRh9A8HEtkmBH2Li03iJriB6m6JIN4rGz3K3JLawI7/veA1xzRKP6ISBw==", "dev": true, + "license": "BSD-3-Clause", "engines": { "node": ">=0.3.1" } @@ -9661,13 +10333,43 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/mocha/node_modules/get-caller-file": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", - "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "node_modules/mocha/node_modules/foreground-child": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.0.tgz", + "integrity": "sha512-Ld2g8rrAyMYFXBhEqMz8ZAHBi4J4uS1i/CxGMDnjyFWddMXLVcDp051DZfu+t7+ab7Wv6SMqpWmyFIj5UbfFvg==", "dev": true, + "license": "ISC", + "dependencies": { + "cross-spawn": "^7.0.0", + "signal-exit": "^4.0.1" + }, "engines": { - "node": "6.* || 8.* || >= 10.*" + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/mocha/node_modules/glob": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", + "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "dev": true, + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" } }, "node_modules/mocha/node_modules/has-flag": { @@ -9679,19 +10381,10 @@ "node": ">=8" } }, - "node_modules/mocha/node_modules/is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "dev": true, - "engines": { - "node": ">=8" - } - }, "node_modules/mocha/node_modules/js-yaml": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", - "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", "dev": true, "dependencies": { "argparse": "^2.0.1" @@ -9716,15 +10409,19 @@ } }, "node_modules/mocha/node_modules/minimatch": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-4.2.1.tgz", - "integrity": "sha512-9Uq1ChtSZO+Mxa/CL1eGizn2vRn3MlLgzhT0Iz8zaY8NdvxvB0d5QdPFmCKf7JKA9Lerx5vRrnwO03jsSfGG9g==", + "version": "9.0.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", + "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", "dev": true, + "license": "ISC", "dependencies": { - "brace-expansion": "^1.1.7" + "brace-expansion": "^2.0.2" }, "engines": { - "node": ">=10" + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" } }, "node_modules/mocha/node_modules/ms": { @@ -9733,18 +10430,6 @@ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", "dev": true }, - "node_modules/mocha/node_modules/nanoid": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.1.tgz", - "integrity": "sha512-n6Vs/3KGyxPQd6uO0eH4Bv0ojGSUvuLlIHtC3Y0kEO23YRge8H9x1GCzLn28YX0H66pMkxuaeESFq4tKISKwdw==", - "dev": true, - "bin": { - "nanoid": "bin/nanoid.cjs" - }, - "engines": { - "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" - } - }, "node_modules/mocha/node_modules/p-limit": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", @@ -9775,59 +10460,79 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/mocha/node_modules/path-exists": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", - "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "node_modules/mocha/node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", "dev": true, + "license": "MIT", "engines": { "node": ">=8" } }, - "node_modules/mocha/node_modules/string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "node_modules/mocha/node_modules/readdirp": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", + "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14.18.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/mocha/node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", "dev": true, + "license": "MIT", "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" + "shebang-regex": "^3.0.0" }, "engines": { "node": ">=8" } }, - "node_modules/mocha/node_modules/supports-color": { - "version": "8.1.1", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", - "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "node_modules/mocha/node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", "dev": true, - "dependencies": { - "has-flag": "^4.0.0" - }, + "license": "MIT", "engines": { - "node": ">=10" + "node": ">=8" + } + }, + "node_modules/mocha/node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=14" }, "funding": { - "url": "https://github.com/chalk/supports-color?sponsor=1" + "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/mocha/node_modules/wrap-ansi": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", - "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "node_modules/mocha/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", "dev": true, "dependencies": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" + "has-flag": "^4.0.0" }, "engines": { "node": ">=10" }, "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + "url": "https://github.com/chalk/supports-color?sponsor=1" } }, "node_modules/mocha/node_modules/y18n": { @@ -9835,28 +10540,45 @@ "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", "dev": true, + "license": "ISC", "engines": { "node": ">=10" } }, "node_modules/mocha/node_modules/yargs": { - "version": "16.2.0", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", - "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==", + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", "dev": true, + "license": "MIT", "dependencies": { - "cliui": "^7.0.2", + "cliui": "^8.0.1", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", "require-directory": "^2.1.1", - "string-width": "^4.2.0", + "string-width": "^4.2.3", "y18n": "^5.0.5", - "yargs-parser": "^20.2.2" + "yargs-parser": "^21.1.1" }, "engines": { - "node": ">=10" + "node": ">=12" + } + }, + "node_modules/mocha/node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" } }, + "node_modules/module-details-from-path": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/module-details-from-path/-/module-details-from-path-1.0.3.tgz", + "integrity": "sha512-ySViT69/76t8VhE1xXHK6Ch4NcDd26gx0MzKXLO+F7NOtnqH68d9zF94nT8ZWSxXh8ELOERsnJO/sWt1xZYw5A==" + }, "node_modules/mrmime": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/mrmime/-/mrmime-1.0.0.tgz", @@ -9872,12 +10594,12 @@ "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" }, "node_modules/mute-stdout": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/mute-stdout/-/mute-stdout-1.0.1.tgz", - "integrity": "sha512-kDcwXR4PS7caBpuRYYBUz9iVixUk3anO3f5OYFiIPwK/20vCzKCHyKoulbiDY1S53zD2bxUpxN/IJ+TnXjfvxg==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mute-stdout/-/mute-stdout-2.0.0.tgz", + "integrity": "sha512-32GSKM3Wyc8dg/p39lWPKYu8zci9mJFzV1Np9Of0ZEpe6Fhssn/FbI7ywAMd40uX+p3ZKh3T5EeCFv81qS3HmQ==", "dev": true, "engines": { - "node": ">= 0.10" + "node": ">= 10.13.0" } }, "node_modules/mute-stream": { @@ -9891,39 +10613,23 @@ "resolved": "https://registry.npmjs.org/named-js-regexp/-/named-js-regexp-1.3.5.tgz", "integrity": "sha512-XO0DPujDP9IWpkt690iWLreKztb/VB811DGl5N3z7BfhkMJuiVZXOi6YN/fEB9qkvtMVTgSZDW8pzdVt8vj/FA==" }, - "node_modules/nan": { - "version": "2.14.0", - "resolved": "https://registry.npmjs.org/nan/-/nan-2.14.0.tgz", - "integrity": "sha512-INOFj37C7k3AfaNTtX8RhsTw7qRy7eLET14cROi9+5HAVbbHuIWUHEauBv5qT4Av2tWasiTY1Jw6puUNqRJXQg==", - "dev": true, - "optional": true - }, "node_modules/nanoid": { - "version": "2.1.11", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-2.1.11.tgz", - "integrity": "sha512-s/snB+WGm6uwi0WjsZdaVcuf3KJXlfGl2LcxgwkEwJF0D/BWzVWAZW/XY4bFaiR7s0Jk3FPvlnepg1H1b1UwlA==", - "dev": true - }, - "node_modules/nanomatch": { - "version": "1.2.13", - "resolved": "https://registry.npmjs.org/nanomatch/-/nanomatch-1.2.13.tgz", - "integrity": "sha512-fpoe2T0RbHwBTBUOftAfBPaDEi06ufaUai0mE6Yn1kacc3SnTErfb/h+X94VXzI64rKFHYImXSvdwGGCmwOqCA==", + "version": "3.3.8", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.8.tgz", + "integrity": "sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w==", "dev": true, - "dependencies": { - "arr-diff": "^4.0.0", - "array-unique": "^0.3.2", - "define-property": "^2.0.2", - "extend-shallow": "^3.0.2", - "fragment-cache": "^0.2.1", - "is-windows": "^1.0.2", - "kind-of": "^6.0.2", - "object.pick": "^1.3.0", - "regex-not": "^1.0.0", - "snapdragon": "^0.8.1", - "to-regex": "^3.0.1" + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" }, "engines": { - "node": ">=0.10.0" + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" } }, "node_modules/napi-build-utils": { @@ -9952,62 +10658,35 @@ "dev": true }, "node_modules/nise": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/nise/-/nise-5.1.1.tgz", - "integrity": "sha512-yr5kW2THW1AkxVmCnKEh4nbYkJdB3I7LUkiUgOvEkOp414mc2UMaHMA7pjq1nYowhdoJZGwEKGaQVbxfpWj10A==", - "dev": true, - "dependencies": { - "@sinonjs/commons": "^1.8.3", - "@sinonjs/fake-timers": ">=5", - "@sinonjs/text-encoding": "^0.7.1", - "just-extend": "^4.0.2", - "path-to-regexp": "^1.7.0" - } - }, - "node_modules/nock": { - "version": "10.0.6", - "resolved": "https://registry.npmjs.org/nock/-/nock-10.0.6.tgz", - "integrity": "sha512-b47OWj1qf/LqSQYnmokNWM8D88KvUl2y7jT0567NB3ZBAZFz2bWp2PC81Xn7u8F2/vJxzkzNZybnemeFa7AZ2w==", - "dev": true, - "dependencies": { - "chai": "^4.1.2", - "debug": "^4.1.0", - "deep-equal": "^1.0.0", - "json-stringify-safe": "^5.0.1", - "lodash": "^4.17.5", - "mkdirp": "^0.5.0", - "propagate": "^1.0.0", - "qs": "^6.5.1", - "semver": "^5.5.0" - }, - "engines": { - "node": ">= 6.0" - } - }, - "node_modules/nock/node_modules/debug": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", - "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", - "deprecated": "Debug versions >=3.2.0 <3.2.7 || >=4 <4.3.1 have a low-severity ReDos regression when used in a Node.js environment. It is recommended you upgrade to 3.2.7 or 4.3.1. (https://github.com/visionmedia/debug/issues/797)", + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/nise/-/nise-6.0.0.tgz", + "integrity": "sha512-K8ePqo9BFvN31HXwEtTNGzgrPpmvgciDsFz8aztFjt4LqKO/JeFD8tBOeuDiCMXrIl/m1YvfH8auSpxfaD09wg==", "dev": true, "dependencies": { - "ms": "^2.1.1" + "@sinonjs/commons": "^3.0.0", + "@sinonjs/fake-timers": "^11.2.2", + "@sinonjs/text-encoding": "^0.7.2", + "just-extend": "^6.2.0", + "path-to-regexp": "^6.2.1" } }, "node_modules/node-abi": { - "version": "2.30.1", - "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-2.30.1.tgz", - "integrity": "sha512-/2D0wOQPgaUWzVSVgRMx+trKJRC2UG4SUc4oCJoXx9Uxjtp0Vy3/kt7zcbxHF8+Z/pK3UloLWzBISg72brfy1w==", + "version": "3.45.0", + "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.45.0.tgz", + "integrity": "sha512-iwXuFrMAcFVi/ZoZiqq8BzAdsLw9kxDfTC0HMyjXfSL/6CSDAGD5UmR7azrAgWV1zKYq7dUUMj4owusBWKLsiQ==", "dev": true, "optional": true, "dependencies": { - "semver": "^5.4.1" + "semver": "^7.3.5" + }, + "engines": { + "node": ">=10" } }, "node_modules/node-addon-api": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-3.2.1.tgz", - "integrity": "sha512-mmcei9JghVNDYydghQmeDX8KoAm0FAiYyIcUt/N4nhyAipB17pllZQDOJD2fotxABnt4Mdz+dKTO7eftLg4d0A==", + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-4.3.0.tgz", + "integrity": "sha512-73sE9+3UaLYYFmDsFZnqCInzPyh3MqIwZO9cw58yIqAZhONrrabrYyYe3TuIqtIiOuTXVhsGau8hcrhhwSsDIQ==", "dev": true, "optional": true }, @@ -10253,9 +10932,9 @@ } }, "node_modules/node-releases": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.2.tgz", - "integrity": "sha512-XxYDdcQ6eKqp/YjI+tb2C5WM2LgjnZrfYg4vgQt49EK268b6gYCHsBLrK2qvJo4FmCtqmKezb0WZFK4fkrZNsg==", + "version": "2.0.27", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz", + "integrity": "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==", "dev": true }, "node_modules/node-stream-zip": { @@ -10270,18 +10949,6 @@ "url": "https://github.com/sponsors/antelle" } }, - "node_modules/normalize-package-data": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz", - "integrity": "sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==", - "dev": true, - "dependencies": { - "hosted-git-info": "^2.1.4", - "resolve": "^1.10.0", - "semver": "2 || 3 || 4 || 5", - "validate-npm-package-license": "^3.0.1" - } - }, "node_modules/normalize-path": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", @@ -10296,6 +10963,7 @@ "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-2.0.1.tgz", "integrity": "sha512-D6MUW4K/VzoJ4rJ01JFKxDrtY1v9wrgzCX5f2qj/lzH1m/lW6MhUZFKerVsnyjOhOsYzI9Kqqak+10l4LvLpMw==", "dev": true, + "license": "MIT", "dependencies": { "prepend-http": "^2.0.0", "query-string": "^5.0.1", @@ -10305,18 +10973,6 @@ "node": ">=4" } }, - "node_modules/normalize-url/node_modules/sort-keys": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/sort-keys/-/sort-keys-2.0.0.tgz", - "integrity": "sha1-ZYU1WEhh7JfXMNbPQYIuH1ZoQSg=", - "dev": true, - "dependencies": { - "is-plain-obj": "^1.0.0" - }, - "engines": { - "node": ">=4" - } - }, "node_modules/now-and-later": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/now-and-later/-/now-and-later-2.0.1.tgz", @@ -10350,19 +11006,6 @@ "node": ">=8" } }, - "node_modules/npmlog": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-4.1.2.tgz", - "integrity": "sha512-2uUqazuKlTaSI/dC8AzicUck7+IrEaOnN/e0jd3Xtt1KcGpwx30v50mL7oPyr/h9bL3E4aZccVwpwP+5W9Vjkg==", - "dev": true, - "optional": true, - "dependencies": { - "are-we-there-yet": "~1.1.2", - "console-control-strings": "~1.1.0", - "gauge": "~2.7.3", - "set-blocking": "~2.0.0" - } - }, "node_modules/nth-check": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.0.1.tgz", @@ -10375,15 +11018,6 @@ "url": "https://github.com/fb55/nth-check?sponsor=1" } }, - "node_modules/number-is-nan": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz", - "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/nyc": { "version": "15.1.0", "resolved": "https://registry.npmjs.org/nyc/-/nyc-15.1.0.tgz", @@ -10425,32 +11059,6 @@ "node": ">=8.9" } }, - "node_modules/nyc/node_modules/convert-source-map": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.7.0.tgz", - "integrity": "sha512-4FJkXzKXEDB1snCFZlLP4gpC3JILicCpGbzG9f9G7tGqGCzETQ2hWPrcinA9oU4wtf2biUaEH5065UnMeR33oA==", - "dev": true, - "dependencies": { - "safe-buffer": "~5.1.1" - } - }, - "node_modules/nyc/node_modules/find-cache-dir": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-3.2.0.tgz", - "integrity": "sha512-1JKclkYYsf1q9WIJKLZa9S9muC+08RIjzAlLrK4QcYLJMS6mk9yombQ9qf+zJ7H9LS800k0s44L4sDq9VYzqyg==", - "dev": true, - "dependencies": { - "commondir": "^1.0.1", - "make-dir": "^3.0.0", - "pkg-dir": "^4.1.0" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/avajs/find-cache-dir?sponsor=1" - } - }, "node_modules/nyc/node_modules/p-map": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/p-map/-/p-map-3.0.0.tgz", @@ -10472,57 +11080,14 @@ "node": ">=0.10.0" } }, - "node_modules/object-copy": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/object-copy/-/object-copy-0.1.0.tgz", - "integrity": "sha1-fn2Fi3gb18mRpBupde04EnVOmYw=", - "dev": true, - "dependencies": { - "copy-descriptor": "^0.1.0", - "define-property": "^0.2.5", - "kind-of": "^3.0.3" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/object-copy/node_modules/define-property": { - "version": "0.2.5", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", - "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", "dev": true, - "dependencies": { - "is-descriptor": "^0.1.0" - }, "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/object-copy/node_modules/kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "dev": true, - "dependencies": { - "is-buffer": "^1.1.5" + "node": ">= 0.4" }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/object-hash": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz", - "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==", - "engines": { - "node": ">= 6" - } - }, - "node_modules/object-inspect": { - "version": "1.12.0", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.0.tgz", - "integrity": "sha512-Ho2z80bVIvJloH+YzRmpZVQe87+qASmBUKZDWgx9cu+KDrX2ZDH/3tMy+gXbZETVGs2M8YdxObOh7XAtim9Y0g==", - "dev": true, "funding": { "url": "https://github.com/sponsors/ljharb" } @@ -10552,27 +11117,15 @@ "node": ">= 0.4" } }, - "node_modules/object-visit": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/object-visit/-/object-visit-1.0.1.tgz", - "integrity": "sha1-95xEk68MU3e1n+OdOV5BBC3QRbs=", - "dev": true, - "dependencies": { - "isobject": "^3.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/object.assign": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.2.tgz", - "integrity": "sha512-ixT2L5THXsApyiUPYKmW+2EHpXXe5Ii3M+f4e+aJFAHao5amFRW6J0OO6c/LU8Be47utCx2GL89hxGB6XSmKuQ==", + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.5.tgz", + "integrity": "sha512-byy+U7gp+FVwmyzKPYhW2h5l3crpmGsxl7X2s8y43IgxvG4g3QZ6CffDtsNQy1WsmZpQbO+ybo0AlW7TY6DcBQ==", "dev": true, "dependencies": { - "call-bind": "^1.0.0", - "define-properties": "^1.1.3", - "has-symbols": "^1.0.1", + "call-bind": "^1.0.5", + "define-properties": "^1.2.1", + "has-symbols": "^1.0.3", "object-keys": "^1.1.1" }, "engines": { @@ -10585,7 +11138,7 @@ "node_modules/object.defaults": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/object.defaults/-/object.defaults-1.1.0.tgz", - "integrity": "sha1-On+GgzS0B96gbaFtiNXNKeQ1/s8=", + "integrity": "sha512-c/K0mw/F11k4dEUBMW8naXUuBuhxRCfG7W+yFy8EcijU/rSmazOUd1XAEEe6bC0OuXY4HUKjTJv7xbxIMqdxrA==", "dev": true, "dependencies": { "array-each": "^1.0.1", @@ -10597,18 +11150,6 @@ "node": ">=0.10.0" } }, - "node_modules/object.defaults/node_modules/for-own": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/for-own/-/for-own-1.0.0.tgz", - "integrity": "sha1-xjMy9BXO3EsE2/5wz4NklMU8tEs=", - "dev": true, - "dependencies": { - "for-in": "^1.0.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/object.entries": { "version": "1.1.5", "resolved": "https://registry.npmjs.org/object.entries/-/object.entries-1.1.5.tgz", @@ -10624,14 +11165,15 @@ } }, "node_modules/object.fromentries": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/object.fromentries/-/object.fromentries-2.0.5.tgz", - "integrity": "sha512-CAyG5mWQRRiBU57Re4FKoTBjXfDoNwdFVH2Y1tS9PqCsfUTymAohOkEMSG3aRNKmv4lV3O7p1et7c187q6bynw==", + "version": "2.0.8", + "resolved": "https://registry.npmjs.org/object.fromentries/-/object.fromentries-2.0.8.tgz", + "integrity": "sha512-k6E21FzySsSK5a21KRADBd/NGneRegFO5pLHfdQLpRDETUNJueLXs3WCzyQ3tFRDYgbq3KHGXfTbi2bs8WQ6rQ==", "dev": true, "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.3", - "es-abstract": "^1.19.1" + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2", + "es-object-atoms": "^1.0.0" }, "engines": { "node": ">= 0.4" @@ -10640,6 +11182,20 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/object.groupby": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/object.groupby/-/object.groupby-1.0.3.tgz", + "integrity": "sha512-+Lhy3TQTuzXI5hevh8sBGqbmurHbbIjAi0Z4S63nthVLmLxfbj4T54a4CfZrXIrt9iP4mVAPYMo/v99taj3wjQ==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/object.hasown": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/object.hasown/-/object.hasown-1.1.0.tgz", @@ -10653,35 +11209,10 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/object.map": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/object.map/-/object.map-1.0.1.tgz", - "integrity": "sha1-z4Plncj8wK1fQlDh94s7gb2AHTc=", - "dev": true, - "dependencies": { - "for-own": "^1.0.0", - "make-iterator": "^1.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/object.map/node_modules/for-own": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/for-own/-/for-own-1.0.0.tgz", - "integrity": "sha1-xjMy9BXO3EsE2/5wz4NklMU8tEs=", - "dev": true, - "dependencies": { - "for-in": "^1.0.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/object.pick": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/object.pick/-/object.pick-1.3.0.tgz", - "integrity": "sha1-h6EKxMFpS9Lhy/U1kaZhQftd10c=", + "integrity": "sha512-tqa/UMy/CCoYmj+H5qc07qvSL9dqcs/WZENZ1JbtWBlATP+iVOe778gE6MSijnyCnORzDuX6hU+LA4SZ09YjFQ==", "dev": true, "dependencies": { "isobject": "^3.0.1" @@ -10690,40 +11221,15 @@ "node": ">=0.10.0" } }, - "node_modules/object.reduce": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/object.reduce/-/object.reduce-1.0.1.tgz", - "integrity": "sha1-b+NI8qx/oPlcpiEiZZkJaCW7A60=", - "dev": true, - "dependencies": { - "for-own": "^1.0.0", - "make-iterator": "^1.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/object.reduce/node_modules/for-own": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/for-own/-/for-own-1.0.0.tgz", - "integrity": "sha1-xjMy9BXO3EsE2/5wz4NklMU8tEs=", - "dev": true, - "dependencies": { - "for-in": "^1.0.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/object.values": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.1.5.tgz", - "integrity": "sha512-QUZRW0ilQ3PnPpbNtgdNV1PDbEqLIiSFB3l+EnGtBQ/8SUTLj1PZwtQHABZtLgwpJZTSZhuGLOGk57Drx2IvYg==", + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.2.0.tgz", + "integrity": "sha512-yBYjY9QX2hnRmZHAjG/f13MzmBzxzYgQhFrke06TTyKY5zSTEqkOeukBzIdVA3j3ulu8Qa3MbVFShV7T2RmGtQ==", "dev": true, "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.3", - "es-abstract": "^1.19.1" + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" }, "engines": { "node": ">= 0.4" @@ -10755,6 +11261,23 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/open": { + "version": "8.4.2", + "resolved": "https://registry.npmjs.org/open/-/open-8.4.2.tgz", + "integrity": "sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ==", + "dev": true, + "dependencies": { + "define-lazy-prop": "^2.0.0", + "is-docker": "^2.1.1", + "is-wsl": "^2.2.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/opener": { "version": "1.5.2", "resolved": "https://registry.npmjs.org/opener/-/opener-1.5.2.tgz", @@ -10764,6 +11287,23 @@ "opener": "bin/opener-bin.js" } }, + "node_modules/optionator": { + "version": "0.9.3", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.3.tgz", + "integrity": "sha512-JjCoypp+jKn1ttEFExxhetCKeJt9zhAgAve5FXHixTvFDW/5aEktX9bufBKLRRMdU7bNtpLfcGu94B3cdEJgjg==", + "dev": true, + "dependencies": { + "@aashutoshrathi/word-wrap": "^1.2.3", + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, "node_modules/ordered-read-streams": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/ordered-read-streams/-/ordered-read-streams-1.0.1.tgz", @@ -10779,31 +11319,12 @@ "integrity": "sha1-hUNzx/XCMVkU/Jv8a9gjj92h7Cc=", "dev": true }, - "node_modules/os-locale": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/os-locale/-/os-locale-1.4.0.tgz", - "integrity": "sha1-IPnxeuKe00XoveWDsT0gCYA8FNk=", - "dev": true, - "dependencies": { - "lcid": "^1.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/os-tmpdir": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", - "integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/p-cancelable": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-0.4.1.tgz", "integrity": "sha512-HNa1A8LvB1kie7cERyy21VNeHb2CWJJYqyyC2o3klWFfMGlFmWv2Z7sFgZH8ZiaYL95ydToKTFVXgMV/Os0bBQ==", "dev": true, + "license": "MIT", "engines": { "node": ">=4" } @@ -10813,6 +11334,7 @@ "resolved": "https://registry.npmjs.org/p-event/-/p-event-2.3.1.tgz", "integrity": "sha512-NQCqOFhbpVTMX4qMe8PF8lbGtzZ+LCiN7pcNrb/413Na7+TRoe1xkKUzuWa/YEJdGQ0FvKtj35EEbDoVPO2kbA==", "dev": true, + "license": "MIT", "dependencies": { "p-timeout": "^2.0.1" }, @@ -10823,8 +11345,9 @@ "node_modules/p-finally": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz", - "integrity": "sha1-P7z7FbiZpEEjs0ttzBi3JDNqLK4=", + "integrity": "sha512-LICb2p9CB7FS+0eR1oqWnHhp0FljGLZCWBE9aix0Uye9W8LTQPwMTYVGWQWIw9RdQiDg4+epXQODwIYJtSJaow==", "dev": true, + "license": "MIT", "engines": { "node": ">=4" } @@ -10832,8 +11355,9 @@ "node_modules/p-is-promise": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/p-is-promise/-/p-is-promise-1.1.0.tgz", - "integrity": "sha1-nJRWmJ6fZYgBewQ01WCXZ1w9oF4=", + "integrity": "sha512-zL7VE4JVS2IFSkR2GQKDSPEVxkoH43/p7oEnwpdCndKYJO0HVeRB7fA8TJwuLOTBREtK0ea8eHaxdwcpob5dmg==", "dev": true, + "license": "MIT", "engines": { "node": ">=4" } @@ -10885,6 +11409,7 @@ "resolved": "https://registry.npmjs.org/p-timeout/-/p-timeout-2.0.1.tgz", "integrity": "sha512-88em58dDVB/KzPEx1X0N3LwFfYZPyDc4B6eF38M1rk9VTZMbxXXgjugz8mmwpS9Ox4BDZ+t6t3QP5+/gazweIA==", "dev": true, + "license": "MIT", "dependencies": { "p-finally": "^1.0.0" }, @@ -10916,11 +11441,25 @@ "node": ">=8" } }, + "node_modules/package-json-from-dist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", + "dev": true, + "license": "BlueOak-1.0.0" + }, + "node_modules/pako": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", + "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==", + "dev": true + }, "node_modules/parent-module": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", "dev": true, + "license": "MIT", "dependencies": { "callsites": "^3.0.0" }, @@ -10928,33 +11467,48 @@ "node": ">=6" } }, - "node_modules/parent-module/node_modules/callsites": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", - "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "node_modules/parse-asn1": { + "version": "5.1.9", + "resolved": "https://registry.npmjs.org/parse-asn1/-/parse-asn1-5.1.9.tgz", + "integrity": "sha512-fIYNuZ/HastSb80baGOuPRo1O9cf4baWw5WsAp7dBuUzeTD/BoaG8sVTdlPFksBE2lF21dN+A1AnrpIjSWqHHg==", "dev": true, + "license": "ISC", + "dependencies": { + "asn1.js": "^4.10.1", + "browserify-aes": "^1.2.0", + "evp_bytestokey": "^1.0.3", + "pbkdf2": "^3.1.5", + "safe-buffer": "^5.2.1" + }, "engines": { - "node": ">=6" + "node": ">= 0.10" } }, - "node_modules/parse-asn1": { - "version": "5.1.4", - "resolved": "https://registry.npmjs.org/parse-asn1/-/parse-asn1-5.1.4.tgz", - "integrity": "sha512-Qs5duJcuvNExRfFZ99HDD3z4mAi3r9Wl/FOjEOijlxwCZs7E7mW2vjTpgQ4J8LpTF8x5v+1Vn5UQFejmWT11aw==", + "node_modules/parse-asn1/node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", "dev": true, - "dependencies": { - "asn1.js": "^4.0.0", - "browserify-aes": "^1.0.0", - "create-hash": "^1.1.0", - "evp_bytestokey": "^1.0.0", - "pbkdf2": "^3.0.3", - "safe-buffer": "^5.1.1" - } + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" }, "node_modules/parse-filepath": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/parse-filepath/-/parse-filepath-1.0.2.tgz", - "integrity": "sha1-pjISf1Oq89FYdvWHLz/6x2PWyJE=", + "integrity": "sha512-FwdRXKCohSVeXqwtYonZTXtbGJKrn+HNyWDYVcp5yuJlesTwNH4rsmRZ+GrKAPJ5bLpRxESMeS+Rl0VCHRvB2Q==", "dev": true, "dependencies": { "is-absolute": "^1.0.0", @@ -10965,19 +11519,10 @@ "node": ">=0.8" } }, - "node_modules/parse-node-version": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/parse-node-version/-/parse-node-version-1.0.1.tgz", - "integrity": "sha512-3YHlOa/JgH6Mnpr05jP9eDG254US9ek25LyIxZlDItp2iJtwyaXQb57lBYLdT3MowkUFYEV2XXNAYIPlESvJlA==", - "dev": true, - "engines": { - "node": ">= 0.10" - } - }, "node_modules/parse-passwd": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/parse-passwd/-/parse-passwd-1.0.0.tgz", - "integrity": "sha1-bVuTSkVpk7I9N/QKOC1vFmao5cY=", + "integrity": "sha512-1Y1A//QUXEZK7YKz+rD9WydcE1+EuPr6ZBgKecAB8tmoW6UFv0NREVJe1p+jRxtThkcbbKkfwIbWJe/IeE6m2Q==", "dev": true, "engines": { "node": ">=0.10.0" @@ -10986,12 +11531,21 @@ "node_modules/parse-semver": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/parse-semver/-/parse-semver-1.1.1.tgz", - "integrity": "sha1-mkr9bfBj3Egm+T+6SpnPIj9mbLg=", + "integrity": "sha512-Eg1OuNntBMH0ojvEKSrvDSnwLmvVuUOSdylH/pSCPNMIspLlweJyIWXCE+k/5hm3cj/EBUYwmWkjhBALNP4LXQ==", "dev": true, "dependencies": { "semver": "^5.1.0" } }, + "node_modules/parse-semver/node_modules/semver": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", + "dev": true, + "bin": { + "semver": "bin/semver" + } + }, "node_modules/parse5-htmlparser2-tree-adapter": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/parse5-htmlparser2-tree-adapter/-/parse5-htmlparser2-tree-adapter-6.0.1.tgz", @@ -11007,15 +11561,6 @@ "integrity": "sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw==", "dev": true }, - "node_modules/pascalcase": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/pascalcase/-/pascalcase-0.1.1.tgz", - "integrity": "sha1-s2PlXoAGym/iF4TS2yK9FdeRfxQ=", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/path-browserify": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-0.0.1.tgz", @@ -11029,12 +11574,13 @@ "dev": true }, "node_modules/path-exists": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", - "integrity": "sha1-zg6+ql94yxiSXqfYENe1mwEP1RU=", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", "dev": true, + "license": "MIT", "engines": { - "node": ">=4" + "node": ">=8" } }, "node_modules/path-is-absolute": { @@ -11057,13 +11603,12 @@ "node_modules/path-parse": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", - "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", - "dev": true + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==" }, "node_modules/path-root": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/path-root/-/path-root-0.1.1.tgz", - "integrity": "sha1-mkpoFMrBwM1zNgqV8yCDyOpHRbc=", + "integrity": "sha512-QLcPegTHF11axjfojBIoDygmS2E3Lf+8+jI6wOVmNVenrKSo3mFdSGiIgdSHenczw3wPtlVMQaFVwGmM7BJdtg==", "dev": true, "dependencies": { "path-root-regex": "^0.1.0" @@ -11075,25 +11620,40 @@ "node_modules/path-root-regex": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/path-root-regex/-/path-root-regex-0.1.2.tgz", - "integrity": "sha1-v8zcjfWxLcUsi0PsONGNcsBLqW0=", + "integrity": "sha512-4GlJ6rZDhQZFE0DPVKh0e9jmZ5egZfxTkp7bcRDuPlJXbAwhxcl2dINPUAsjLdejqaLsCeg8axcLjIbvBjN4pQ==", "dev": true, "engines": { "node": ">=0.10.0" } }, - "node_modules/path-to-regexp": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-1.8.0.tgz", - "integrity": "sha512-n43JRhlUKUAlibEJhPeir1ncUID16QnEjNpwzNdO3Lm4ywrBpBZ5oLD0I6br9evr1Y9JTqwRtAh7JLoOzAQdVA==", + "node_modules/path-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", "dev": true, + "license": "BlueOak-1.0.0", "dependencies": { - "isarray": "0.0.1" + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/path-to-regexp/node_modules/isarray": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", - "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=", + "node_modules/path-scurry/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/path-to-regexp": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-6.3.0.tgz", + "integrity": "sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ==", "dev": true }, "node_modules/path-type": { @@ -11115,21 +11675,44 @@ } }, "node_modules/pbkdf2": { - "version": "3.0.17", - "resolved": "https://registry.npmjs.org/pbkdf2/-/pbkdf2-3.0.17.tgz", - "integrity": "sha512-U/il5MsrZp7mGg3mSQfn742na2T+1/vHDCG5/iTI3X9MKUuYUZVLQhyRsg06mCgDBTd57TxzgZt7P+fYfjRLtA==", + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/pbkdf2/-/pbkdf2-3.1.5.tgz", + "integrity": "sha512-Q3CG/cYvCO1ye4QKkuH7EXxs3VC/rI1/trd+qX2+PolbaKG0H+bgcZzrTt96mMyRtejk+JMCiLUn3y29W8qmFQ==", "dev": true, + "license": "MIT", "dependencies": { - "create-hash": "^1.1.2", - "create-hmac": "^1.1.4", - "ripemd160": "^2.0.1", - "safe-buffer": "^5.0.1", - "sha.js": "^2.4.8" + "create-hash": "^1.2.0", + "create-hmac": "^1.1.7", + "ripemd160": "^2.0.3", + "safe-buffer": "^5.2.1", + "sha.js": "^2.4.12", + "to-buffer": "^1.2.1" }, "engines": { - "node": ">=0.12" + "node": ">= 0.10" } }, + "node_modules/pbkdf2/node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, "node_modules/pend": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", @@ -11137,15 +11720,16 @@ "dev": true }, "node_modules/picocolors": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", - "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==", - "dev": true + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" }, "node_modules/picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", "dev": true, "engines": { "node": ">=8.6" @@ -11159,6 +11743,7 @@ "resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz", "integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==", "dev": true, + "license": "MIT", "engines": { "node": ">=6" } @@ -11166,8 +11751,9 @@ "node_modules/pinkie": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/pinkie/-/pinkie-2.0.4.tgz", - "integrity": "sha1-clVrgM+g1IqXToDnckjoDtT3+HA=", + "integrity": "sha512-MnUuEycAemtSaeFSjXKW/aroV7akBbY+Sv+RkyqFjgAe73F+MR0TBWKBRDkmfWq/HiFmdavfZ1G7h4SPZXaCSg==", "dev": true, + "license": "MIT", "engines": { "node": ">=0.10.0" } @@ -11175,8 +11761,9 @@ "node_modules/pinkie-promise": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/pinkie-promise/-/pinkie-promise-2.0.1.tgz", - "integrity": "sha1-ITXW36ejWMBprJsXh3YogihFD/o=", + "integrity": "sha512-0Gni6D4UcLTbv9c57DfxDGdr41XfgUjqWZu492f0cIGr16zDU06BWP/RAEvOuo7CQ0CNjHaLlM59YJJFm3NWlw==", "dev": true, + "license": "MIT", "dependencies": { "pinkie": "^2.0.0" }, @@ -11211,13 +11798,13 @@ "node": ">= 0.10" } }, - "node_modules/posix-character-classes": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/posix-character-classes/-/posix-character-classes-0.1.1.tgz", - "integrity": "sha1-AerA/jta9xoqbAL+q7jB/vfgDqs=", + "node_modules/possible-typed-array-names": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.0.0.tgz", + "integrity": "sha512-d7Uw+eZoloe0EHDIYoe+bQ5WXnGMOpmiZFTuMWCwpjzzkL2nTjcKiAk4hh8TjnGye2TwWOk3UXucZ+3rbmBa8Q==", "dev": true, "engines": { - "node": ">=0.10.0" + "node": ">= 0.4" } }, "node_modules/postinstall-build": { @@ -11231,23 +11818,22 @@ } }, "node_modules/prebuild-install": { - "version": "6.1.4", - "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-6.1.4.tgz", - "integrity": "sha512-Z4vpywnK1lBg+zdPCVCsKq0xO66eEV9rWo2zrROGGiRS4JtueBOdlB1FnY8lcy7JsUud/Q3ijUxyWN26Ika0vQ==", + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.1.tgz", + "integrity": "sha512-jAXscXWMcCK8GgCoHOfIr0ODh5ai8mj63L2nWrjuAgXE6tDyYGnx4/8o/rCgU+B4JSyZBKbeZqzhtwtC3ovxjw==", "dev": true, "optional": true, "dependencies": { - "detect-libc": "^1.0.3", + "detect-libc": "^2.0.0", "expand-template": "^2.0.3", "github-from-package": "0.0.0", "minimist": "^1.2.3", "mkdirp-classic": "^0.5.3", "napi-build-utils": "^1.0.1", - "node-abi": "^2.21.0", - "npmlog": "^4.0.1", + "node-abi": "^3.3.0", "pump": "^3.0.0", "rc": "^1.2.7", - "simple-get": "^3.0.3", + "simple-get": "^4.0.0", "tar-fs": "^2.0.0", "tunnel-agent": "^0.6.0" }, @@ -11255,7 +11841,7 @@ "prebuild-install": "bin.js" }, "engines": { - "node": ">=6" + "node": ">=10" } }, "node_modules/prebuild-install/node_modules/pump": { @@ -11269,11 +11855,21 @@ "once": "^1.3.1" } }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "engines": { + "node": ">= 0.8.0" + } + }, "node_modules/prepend-http": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/prepend-http/-/prepend-http-2.0.0.tgz", - "integrity": "sha1-6SQ0v6XqjBn0HN/UAddBo8gZ2Jc=", + "integrity": "sha512-ravE6m9Atw9Z/jjttRUZ+clIXogdghyZAuWJ3qEzjT+jI/dL1ifAqhZeC5VHzQp1MSt1+jxKkFNemj/iO7tVUA==", "dev": true, + "license": "MIT", "engines": { "node": ">=4" } @@ -11290,15 +11886,6 @@ "node": ">=10.13.0" } }, - "node_modules/pretty-hrtime": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/pretty-hrtime/-/pretty-hrtime-1.0.3.tgz", - "integrity": "sha1-t+PqQkNaTJsnWdmeDyAesZWALuE=", - "dev": true, - "engines": { - "node": ">= 0.8" - } - }, "node_modules/process": { "version": "0.11.10", "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", @@ -11337,15 +11924,6 @@ "react-is": "^16.13.1" } }, - "node_modules/propagate": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/propagate/-/propagate-1.0.0.tgz", - "integrity": "sha1-AMLa7t2iDofjeCs0Stuhzd1q1wk=", - "dev": true, - "engines": [ - "node >= 0.8.1" - ] - }, "node_modules/public-encrypt": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/public-encrypt/-/public-encrypt-4.0.3.tgz", @@ -11382,21 +11960,28 @@ } }, "node_modules/punycode": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", - "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==", + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", "dev": true, + "license": "MIT", "engines": { "node": ">=6" } }, "node_modules/qs": { - "version": "6.5.3", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.3.tgz", - "integrity": "sha512-qxXIEh4pCGfHICj1mAJQ2/2XVZkjCDTcEgfoSQxc/fYivUZxTkk7L3bDBJSoNrEzXI17oUO5Dp07ktqE5KzczA==", + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.0.tgz", + "integrity": "sha512-mAZTtNCeetKMH+pSjrb76NAM8V9a05I9aBZOHztWy/UqcJdQYNsf59vrRKWnojAT9Y+GbIvoTBC++CPHqpDBhQ==", "dev": true, + "dependencies": { + "side-channel": "^1.1.0" + }, "engines": { "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, "node_modules/query-string": { @@ -11404,6 +11989,7 @@ "resolved": "https://registry.npmjs.org/query-string/-/query-string-5.1.1.tgz", "integrity": "sha512-gjWOsm2SoGlgLEdAGt7a6slVOk9mGiXmPFMqrEhLQ68rhQuBnpfs3+EmlvqKyxnCo9/PPlF+9MtY02S1aFg+Jw==", "dev": true, + "license": "MIT", "dependencies": { "decode-uri-component": "^0.2.0", "object-assign": "^4.1.0", @@ -11452,6 +12038,12 @@ } ] }, + "node_modules/queue-tick": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/queue-tick/-/queue-tick-1.0.1.tgz", + "integrity": "sha512-kJt5qhMxoszgU/62PLP1CJytzd2NKetjSRnyuj31fDd3Rlcz3fzlFdFLD1SItunPwyqEOkca6GbV612BWfaBag==", + "dev": true + }, "node_modules/randombytes": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", @@ -11490,7 +12082,7 @@ "node_modules/rc/node_modules/strip-json-comments": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", - "integrity": "sha1-PFMZQukIwml8DsNEhYwobHygpgo=", + "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", "dev": true, "optional": true, "engines": { @@ -11515,86 +12107,12 @@ "node": ">=0.8" } }, - "node_modules/read-pkg": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-1.1.0.tgz", - "integrity": "sha1-9f+qXs0pyzHAR0vKfXVra7KePyg=", - "dev": true, - "dependencies": { - "load-json-file": "^1.0.0", - "normalize-package-data": "^2.3.2", - "path-type": "^1.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/read-pkg-up": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-1.0.1.tgz", - "integrity": "sha1-nWPBMnbAZZGNV/ACpX9AobZD+wI=", - "dev": true, - "dependencies": { - "find-up": "^1.0.0", - "read-pkg": "^1.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/read-pkg-up/node_modules/find-up": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-1.1.2.tgz", - "integrity": "sha1-ay6YIrGizgpgq2TWEOzK1TyyTQ8=", - "dev": true, - "dependencies": { - "path-exists": "^2.0.0", - "pinkie-promise": "^2.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/read-pkg-up/node_modules/path-exists": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-2.1.0.tgz", - "integrity": "sha1-D+tsZPD8UY2adU3V77YscCJ2H0s=", - "dev": true, - "dependencies": { - "pinkie-promise": "^2.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/read-pkg/node_modules/path-type": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/path-type/-/path-type-1.1.0.tgz", - "integrity": "sha1-WcRPfuSR2nBNpBXaWkBwuk+P5EE=", - "dev": true, - "dependencies": { - "graceful-fs": "^4.1.2", - "pify": "^2.0.0", - "pinkie-promise": "^2.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/read-pkg/node_modules/pify": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", - "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/readable-stream": { - "version": "2.3.7", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", - "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", "dev": true, + "license": "MIT", "dependencies": { "core-util-is": "~1.0.0", "inherits": "~2.0.3", @@ -11627,49 +12145,33 @@ } }, "node_modules/rechoir": { - "version": "0.6.2", - "resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.6.2.tgz", - "integrity": "sha1-hSBLVNuoLVdC4oyWdW70OvUOM4Q=", + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.8.0.tgz", + "integrity": "sha512-/vxpCXddiX8NGfGO/mTafwjq4aFa/71pvamip0++IQk3zG8cbCj0fifNPrjjF1XMXUne91jL9OoxmdykoEtifQ==", "dev": true, "dependencies": { - "resolve": "^1.1.6" + "resolve": "^1.20.0" }, "engines": { - "node": ">= 0.10" + "node": ">= 10.13.0" } }, "node_modules/reflect-metadata": { - "version": "0.1.13", - "resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.1.13.tgz", - "integrity": "sha512-Ts1Y/anZELhSsjMcU605fU9RE4Oi3p5ORujwbIKXfWa+0Zxs510Qrmrce5/Jowq3cHSZSJqBjypxmHarc+vEWg==" - }, - "node_modules/regenerator-runtime": { - "version": "0.13.9", - "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.9.tgz", - "integrity": "sha512-p3VT+cOEgxFsRRA9X4lkI1E+k2/CtnKtU4gcxyaCUreilL/vqI6CdZ3wxVUx3UOUg+gnUOQQcRI7BmSI656MYA==", - "dev": true - }, - "node_modules/regex-not": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/regex-not/-/regex-not-1.0.2.tgz", - "integrity": "sha512-J6SDjUgDxQj5NusnOtdFxDwN/+HWykR8GELwctJ7mdqhcyy1xEc4SRFHUXvxTp661YaVKAjfRLZ9cCqS6tn32A==", - "dev": true, - "dependencies": { - "extend-shallow": "^3.0.2", - "safe-regex": "^1.1.0" - }, - "engines": { - "node": ">=0.10.0" - } + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.2.2.tgz", + "integrity": "sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q==", + "license": "Apache-2.0" }, "node_modules/regexp.prototype.flags": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.4.1.tgz", - "integrity": "sha512-pMR7hBVUUGI7PMA37m2ofIdQCsomVnas+Jn5UPGAHQ+/LlwKm/aTLJHdasmHRzlfeZwHiAOaRSo2rbBDm3nNUQ==", + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.2.tgz", + "integrity": "sha512-NcDiDkTLuPR+++OCKB0nWafEmhg/Da8aUPLPMQbK+bxKKCm1/S5he+AqYa4PlMCVBalb4/yxIRub6qkEx5yJbw==", "dev": true, "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.3" + "call-bind": "^1.0.6", + "define-properties": "^1.2.1", + "es-errors": "^1.3.0", + "set-function-name": "^2.0.1" }, "engines": { "node": ">= 0.4" @@ -11678,18 +12180,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/regexpp": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/regexpp/-/regexpp-3.1.0.tgz", - "integrity": "sha512-ZOIzd8yVsQQA7j8GCSlPGXwg5PfmA1mrq0JP4nGhh54LaKN3xdai/vHUDu74pKwV8OxseMS65u2NImosQcSD0Q==", - "dev": true, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/mysticatea" - } - }, "node_modules/release-zalgo": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/release-zalgo/-/release-zalgo-1.0.0.tgz", @@ -11735,24 +12225,6 @@ "integrity": "sha1-wkvOKig62tW8P1jg1IJJuSN52O8=", "dev": true }, - "node_modules/repeat-element": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/repeat-element/-/repeat-element-1.1.3.tgz", - "integrity": "sha512-ahGq0ZnV5m5XtZLMb+vP76kcAM5nkLqk0lpqAuojSKGgQtn4eRi4ZZGm2olo2zKFH+sMsWaqOCW1dqAnOru72g==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/repeat-string": { - "version": "1.6.1", - "resolved": "https://registry.npmjs.org/repeat-string/-/repeat-string-1.6.1.tgz", - "integrity": "sha1-jcrkcOHIirwtYA//Sndihtp15jc=", - "dev": true, - "engines": { - "node": ">=0.10" - } - }, "node_modules/replace-ext": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/replace-ext/-/replace-ext-1.0.0.tgz", @@ -11763,17 +12235,12 @@ } }, "node_modules/replace-homedir": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/replace-homedir/-/replace-homedir-1.0.0.tgz", - "integrity": "sha1-6H9tUTuSjd6AgmDBK+f+xv9ueYw=", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/replace-homedir/-/replace-homedir-2.0.0.tgz", + "integrity": "sha512-bgEuQQ/BHW0XkkJtawzrfzHFSN70f/3cNOiHa2QsYxqrjaC30X1k74FJ6xswVBP0sr0SpGIdVFuPwfrYziVeyw==", "dev": true, - "dependencies": { - "homedir-polyfill": "^1.0.1", - "is-absolute": "^1.0.0", - "remove-trailing-separator": "^1.1.0" - }, "engines": { - "node": ">= 0.10" + "node": ">= 10.13.0" } }, "node_modules/require-directory": { @@ -11794,19 +12261,41 @@ "node": ">=0.10.0" } }, - "node_modules/require-main-filename": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-1.0.1.tgz", - "integrity": "sha1-l/cXtp1IeE9fUmpsWqj/3aBVpNE=", - "dev": true + "node_modules/require-in-the-middle": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/require-in-the-middle/-/require-in-the-middle-7.2.0.tgz", + "integrity": "sha512-3TLx5TGyAY6AOqLBoXmHkNql0HIf2RGbuMgCDT2WO/uGVAPJs6h7Kl+bN6TIZGd9bWhWPwnDnTHGtW8Iu77sdw==", + "dependencies": { + "debug": "^4.1.1", + "module-details-from-path": "^1.0.3", + "resolve": "^1.22.1" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/require-in-the-middle/node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } }, "node_modules/resolve": { - "version": "1.22.0", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.0.tgz", - "integrity": "sha512-Hhtrw0nLeSrFQ7phPp4OOcVjLPIeMnRlr5mcnVuMe7M/7eBn98A3hmFRLoFo3DLZkivSYwhRUJTyPyWAk56WLw==", - "dev": true, + "version": "1.22.4", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.4.tgz", + "integrity": "sha512-PXNdCiPqDqeUou+w1C2eTQbNfxKSuMxqTCuvlmmMsk1NWHL5fRrhY6Pl0qEYYc6+QqGClco1Qj8XnjPego4wfg==", "dependencies": { - "is-core-module": "^2.8.1", + "is-core-module": "^2.13.0", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, @@ -11832,7 +12321,7 @@ "node_modules/resolve-dir": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/resolve-dir/-/resolve-dir-1.0.1.tgz", - "integrity": "sha1-eaQGRMNivoLybv/nOcm7U4IEb0M=", + "integrity": "sha512-R7uiTjECzvOsWSfdM0QKFNBVFcK27aHOUwdvK53BcW8zqnGdYp0Fbj82cy54+2A4P2tFM22J5kRfe1R+lM/1yg==", "dev": true, "dependencies": { "expand-tilde": "^2.0.0", @@ -11863,31 +12352,16 @@ "node": ">= 0.10" } }, - "node_modules/resolve-url": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/resolve-url/-/resolve-url-0.2.1.tgz", - "integrity": "sha1-LGN/53yJOv0qZj/iGqkIAGjiBSo=", - "deprecated": "https://github.com/lydell/resolve-url#deprecated", - "dev": true - }, "node_modules/responselike": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/responselike/-/responselike-1.0.2.tgz", - "integrity": "sha1-kYcg7ztjHFZCvgaPFa3lpG9Loec=", + "integrity": "sha512-/Fpe5guzJk1gPqdJLJR5u7eG/gNY4nImjbRDaVWVMRhne55TCmj2i9Q+54PBRfatRC8v/rIiv9BN0pMd9OV5EQ==", "dev": true, + "license": "MIT", "dependencies": { "lowercase-keys": "^1.0.0" } }, - "node_modules/ret": { - "version": "0.1.15", - "resolved": "https://registry.npmjs.org/ret/-/ret-0.1.15.tgz", - "integrity": "sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg==", - "dev": true, - "engines": { - "node": ">=0.12" - } - }, "node_modules/reusify": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", @@ -11899,15 +12373,14 @@ } }, "node_modules/rewiremock": { - "version": "3.14.3", - "resolved": "https://registry.npmjs.org/rewiremock/-/rewiremock-3.14.3.tgz", - "integrity": "sha512-6BaUGfp7NtxBjisxcGN73nNiA2fS2AwhEk/9DMUqxfv5v0aDM1wpOYpj5GSArqsJi07YCfLhkD8C74LAN7+FkQ==", + "version": "3.14.6", + "resolved": "https://registry.npmjs.org/rewiremock/-/rewiremock-3.14.6.tgz", + "integrity": "sha512-hjpS7iQUTVVh/IHV4GE1ypg4IzlgVc34gxZBarwwVrKfnjlyqHJuQdsia6Ac7m4f4k/zxxA3tX285MOstdysRQ==", "dev": true, + "license": "MIT", "dependencies": { "babel-runtime": "^6.26.0", "compare-module-exports": "^2.1.0", - "lodash.some": "^4.6.0", - "lodash.template": "^4.4.0", "node-libs-browser": "^2.1.0", "path-parse": "^1.0.5", "wipe-node-cache": "^2.1.2", @@ -11930,15 +12403,56 @@ } }, "node_modules/ripemd160": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/ripemd160/-/ripemd160-2.0.2.tgz", - "integrity": "sha512-ii4iagi25WusVoiC4B4lq7pbXfAp3D9v5CwfkY33vffw2+pkDjY1D8GaN7spsxvCSx8dkPqOZCEZyfxcmJG2IA==", + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/ripemd160/-/ripemd160-2.0.3.tgz", + "integrity": "sha512-5Di9UC0+8h1L6ZD2d7awM7E/T4uA1fJRlx6zk/NvdCCVEoAnFqvHmCuNeIKoCeIixBX/q8uM+6ycDvF8woqosA==", "dev": true, + "license": "MIT", "dependencies": { - "hash-base": "^3.0.0", - "inherits": "^2.0.1" + "hash-base": "^3.1.2", + "inherits": "^2.0.4" + }, + "engines": { + "node": ">= 0.8" } }, + "node_modules/ripemd160/node_modules/hash-base": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/hash-base/-/hash-base-3.1.2.tgz", + "integrity": "sha512-Bb33KbowVTIj5s7Ked1OsqHUeCpz//tPwR+E2zJgJKo9Z5XolZ9b6bdUgjmYlwnWhoOQKoTd1TYToZGn5mAYOg==", + "dev": true, + "license": "MIT", + "dependencies": { + "inherits": "^2.0.4", + "readable-stream": "^2.3.8", + "safe-buffer": "^5.2.1", + "to-buffer": "^1.2.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/ripemd160/node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, "node_modules/run-parallel": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", @@ -11978,18 +12492,51 @@ "resolved": "https://registry.npmjs.org/rxjs-compat/-/rxjs-compat-6.6.7.tgz", "integrity": "sha512-szN4fK+TqBPOFBcBcsR0g2cmTTUF/vaFEOZNuSdfU8/pGFnNmmn2u8SystYXG1QMrjOPBc6XTKHMVfENDf6hHw==" }, + "node_modules/safe-array-concat": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.2.tgz", + "integrity": "sha512-vj6RsCsWBCf19jIeHEfkRMw8DPiBb+DMXklQ/1SGDHOMlHdPUkZXFQ2YdplS23zESTijAcurb1aSgJA3AgMu1Q==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.7", + "get-intrinsic": "^1.2.4", + "has-symbols": "^1.0.3", + "isarray": "^2.0.5" + }, + "engines": { + "node": ">=0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/safe-array-concat/node_modules/isarray": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", + "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", + "dev": true + }, "node_modules/safe-buffer": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true }, - "node_modules/safe-regex": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/safe-regex/-/safe-regex-1.1.0.tgz", - "integrity": "sha1-QKNmnzsHfR6UPURinhV91IAjvy4=", + "node_modules/safe-regex-test": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.0.3.tgz", + "integrity": "sha512-CdASjNJPvRa7roO6Ra/gLYBTzYzzPyyBXxIMdGW3USQLyjWEls2RgW5UBTXaQVp+OrpeCK3bLem8smtmheoRuw==", "dev": true, "dependencies": { - "ret": "~0.1.10" + "call-bind": "^1.0.6", + "es-errors": "^1.3.0", + "is-regex": "^1.1.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, "node_modules/safer-buffer": { @@ -12003,9 +12550,9 @@ "integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==" }, "node_modules/schema-utils": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.1.1.tgz", - "integrity": "sha512-Y5PQxS4ITlC+EahLuXaY86TXfR7Dc5lw294alXOq86JAHCihAIZfqv8nNCWvaEJvaC51uN9hbLGeV0cFBdH+Fw==", + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.3.0.tgz", + "integrity": "sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg==", "dev": true, "dependencies": { "@types/json-schema": "^7.0.8", @@ -12021,55 +12568,51 @@ } }, "node_modules/seek-bzip": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/seek-bzip/-/seek-bzip-1.0.5.tgz", - "integrity": "sha1-z+kXyz0nS8/6x5J1ivUxc+sfq9w=", + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/seek-bzip/-/seek-bzip-1.0.6.tgz", + "integrity": "sha512-e1QtP3YL5tWww8uKaOCQ18UxIT2laNBXHjV/S2WYCiK4udiv8lkG89KRIoCjUagnAmCBurjF4zEVX2ByBbnCjQ==", "dev": true, + "license": "MIT", "dependencies": { - "commander": "~2.8.1" + "commander": "^2.8.1" }, "bin": { "seek-bunzip": "bin/seek-bunzip", "seek-table": "bin/seek-bzip-table" } }, - "node_modules/seek-bzip/node_modules/commander": { - "version": "2.8.1", - "resolved": "https://registry.npmjs.org/commander/-/commander-2.8.1.tgz", - "integrity": "sha1-Br42f+v9oMMwqh4qBy09yXYkJdQ=", - "dev": true, + "node_modules/semver": { + "version": "7.6.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.0.tgz", + "integrity": "sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg==", "dependencies": { - "graceful-readlink": ">= 1.0.0" + "lru-cache": "^6.0.0" }, - "engines": { - "node": ">= 0.6.x" - } - }, - "node_modules/semver": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", - "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", "bin": { - "semver": "bin/semver" + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" } }, "node_modules/semver-greatest-satisfied-range": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/semver-greatest-satisfied-range/-/semver-greatest-satisfied-range-1.1.0.tgz", - "integrity": "sha1-E+jCZYq5aRywzXEJMkAoDTb3els=", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/semver-greatest-satisfied-range/-/semver-greatest-satisfied-range-2.0.0.tgz", + "integrity": "sha512-lH3f6kMbwyANB7HuOWRMlLCa2itaCrZJ+SAqqkSZrZKO/cAsk2EOyaKHUtNkVLFyFW9pct22SFesFp3Z7zpA0g==", "dev": true, "dependencies": { - "sver-compat": "^1.5.0" + "sver": "^1.8.3" }, "engines": { - "node": ">= 0.10" + "node": ">= 10.13.0" } }, "node_modules/serialize-javascript": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.0.tgz", - "integrity": "sha512-Qr3TosvguFt8ePWqsvRfrKyQXIiW+nGbYpy8XK24NQHE83caxWt+mIymTT19DGFbNWNLfEwsrkSmN64lVWB9ag==", + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.2.tgz", + "integrity": "sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==", "dev": true, + "license": "BSD-3-Clause", "dependencies": { "randombytes": "^2.1.0" } @@ -12080,31 +12623,36 @@ "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=", "dev": true }, - "node_modules/set-value": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/set-value/-/set-value-2.0.1.tgz", - "integrity": "sha512-JxHc1weCN68wRY0fhCoXpyK55m/XPHafOmK4UWD7m2CI14GMcFypt4w/0+NV5f/ZMby2F6S2wwA7fgynh9gWSw==", + "node_modules/set-function-length": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", + "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", "dev": true, "dependencies": { - "extend-shallow": "^2.0.1", - "is-extendable": "^0.1.1", - "is-plain-object": "^2.0.3", - "split-string": "^3.0.1" + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.2" }, "engines": { - "node": ">=0.10.0" + "node": ">= 0.4" } }, - "node_modules/set-value/node_modules/extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "node_modules/set-function-name": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/set-function-name/-/set-function-name-2.0.2.tgz", + "integrity": "sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==", "dev": true, "dependencies": { - "is-extendable": "^0.1.0" + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "functions-have-names": "^1.2.3", + "has-property-descriptors": "^1.0.2" }, "engines": { - "node": ">=0.10.0" + "node": ">= 0.4" } }, "node_modules/setimmediate": { @@ -12114,17 +12662,45 @@ "dev": true }, "node_modules/sha.js": { - "version": "2.4.11", - "resolved": "https://registry.npmjs.org/sha.js/-/sha.js-2.4.11.tgz", - "integrity": "sha512-QMEp5B7cftE7APOjk5Y6xgrbWu+WkLVQwk8JNjZ8nKRciZaByEW6MubieAiToS7+dwvrjGhH8jRXz3MVd0AYqQ==", + "version": "2.4.12", + "resolved": "https://registry.npmjs.org/sha.js/-/sha.js-2.4.12.tgz", + "integrity": "sha512-8LzC5+bvI45BjpfXU8V5fdU2mfeKiQe1D1gIMn7XUlF3OTUrpdJpPPH4EMAnF0DsHHdSZqCdSss5qCmJKuiO3w==", + "dev": true, "dependencies": { - "inherits": "^2.0.1", - "safe-buffer": "^5.0.1" + "inherits": "^2.0.4", + "safe-buffer": "^5.2.1", + "to-buffer": "^1.2.0" }, "bin": { "sha.js": "bin.js" + }, + "engines": { + "node": ">= 0.10" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/sha.js/node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, "node_modules/shallow-clone": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/shallow-clone/-/shallow-clone-3.0.1.tgz", @@ -12164,23 +12740,82 @@ "integrity": "sha512-sQTKC1Re/rM6XyFM6fIAGHRPVGvyXfgzIDvzoq608vM+jeyVD0Tu1E6Np0Kc2zAIFWIj963V2800iF/9LPieQw==" }, "node_modules/shortid": { - "version": "2.2.16", - "resolved": "https://registry.npmjs.org/shortid/-/shortid-2.2.16.tgz", - "integrity": "sha512-Ugt+GIZqvGXCIItnsL+lvFJOiN7RYqlGy7QE41O3YC1xbNSeDGIRO7xg2JJXIAj1cAGnOeC1r7/T9pgrtQbv4g==", + "version": "2.2.17", + "resolved": "https://registry.npmjs.org/shortid/-/shortid-2.2.17.tgz", + "integrity": "sha512-GpbM3gLF1UUXZvQw6MCyulHkWbRseNO4cyBEZresZRorwl1+SLu1ZdqgVtuwqz8mB6RpwPkm541mYSqrKyJSaA==", "dev": true, + "license": "MIT", "dependencies": { - "nanoid": "^2.1.0" + "nanoid": "^3.3.8" } }, "node_modules/side-channel": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz", - "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", "dev": true, "dependencies": { - "call-bind": "^1.0.0", - "get-intrinsic": "^1.0.2", - "object-inspect": "^1.9.0" + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "dev": true, + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "dev": true, + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "dev": true, + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -12214,55 +12849,72 @@ "optional": true }, "node_modules/simple-get": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-3.1.1.tgz", - "integrity": "sha512-CQ5LTKGfCpvE1K0n2us+kuMPbk/q0EKl82s4aheV9oXjFEz6W/Y7oQFVJuU6QG77hRT4Ghb5RURteF5vnWjupA==", + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz", + "integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==", "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], "optional": true, "dependencies": { - "decompress-response": "^4.2.0", + "decompress-response": "^6.0.0", "once": "^1.3.1", "simple-concat": "^1.0.0" } }, "node_modules/simple-get/node_modules/decompress-response": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-4.2.1.tgz", - "integrity": "sha512-jOSne2qbyE+/r8G1VU+G/82LBs2Fs4LAsTiLSHOCOMZQl2OKZ6i8i4IyHemTe+/yIXOtTcRQMzPcgyhoFlqPkw==", + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", + "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", "dev": true, "optional": true, "dependencies": { - "mimic-response": "^2.0.0" + "mimic-response": "^3.1.0" }, "engines": { - "node": ">=8" + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/simple-get/node_modules/mimic-response": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-2.1.0.tgz", - "integrity": "sha512-wXqjST+SLt7R009ySCglWBCFpjUygmCIfD790/kVbiGmUgfYGuB14PiTd5DwVxSV4NcYHjzMkoj5LjQZwTQLEA==", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", + "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", "dev": true, "optional": true, "engines": { - "node": ">=8" + "node": ">=10" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/sinon": { - "version": "13.0.1", - "resolved": "https://registry.npmjs.org/sinon/-/sinon-13.0.1.tgz", - "integrity": "sha512-8yx2wIvkBjIq/MGY1D9h1LMraYW+z1X0mb648KZnKSdvLasvDu7maa0dFaNYdTDczFgbjNw2tOmWdTk9saVfwQ==", + "version": "18.0.0", + "resolved": "https://registry.npmjs.org/sinon/-/sinon-18.0.0.tgz", + "integrity": "sha512-+dXDXzD1sBO6HlmZDd7mXZCR/y5ECiEiGCBSGuFD/kZ0bDTofPYc6JaeGmPSF+1j1MejGUWkORbYOLDyvqCWpA==", "dev": true, "dependencies": { - "@sinonjs/commons": "^1.8.3", - "@sinonjs/fake-timers": "^9.0.0", - "@sinonjs/samsam": "^6.1.1", - "diff": "^5.0.0", - "nise": "^5.1.1", - "supports-color": "^7.2.0" + "@sinonjs/commons": "^3.0.1", + "@sinonjs/fake-timers": "^11.2.2", + "@sinonjs/samsam": "^8.0.0", + "diff": "^5.2.0", + "nise": "^6.0.0", + "supports-color": "^7" }, "funding": { "type": "opencollective", @@ -12270,10 +12922,11 @@ } }, "node_modules/sinon/node_modules/diff": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/diff/-/diff-5.0.0.tgz", - "integrity": "sha512-/VTCrvm5Z0JGty/BWHljh+BAiw3IK+2j87NGMu8Nwc/f48WoDAC395uomO9ZD117ZOBaHmkX1oyLvkVM/aIT3w==", + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/diff/-/diff-5.2.2.tgz", + "integrity": "sha512-vtcDfH3TOjP8UekytvnHH1o1P4FcUdt4eQ1Y+Abap1tk/OB2MWQvcwS2ClCd1zuIhc3JKOx6p3kod8Vfys3E+A==", "dev": true, + "license": "BSD-3-Clause", "engines": { "node": ">=0.3.1" } @@ -12322,210 +12975,38 @@ "node": ">=8" } }, - "node_modules/slice-ansi": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-4.0.0.tgz", - "integrity": "sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ==", - "dev": true, - "dependencies": { - "ansi-styles": "^4.0.0", - "astral-regex": "^2.0.0", - "is-fullwidth-code-point": "^3.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/slice-ansi?sponsor=1" - } - }, - "node_modules/slice-ansi/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/slice-ansi/node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/slice-ansi/node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true - }, - "node_modules/slice-ansi/node_modules/is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/snapdragon": { - "version": "0.8.2", - "resolved": "https://registry.npmjs.org/snapdragon/-/snapdragon-0.8.2.tgz", - "integrity": "sha512-FtyOnWN/wCHTVXOMwvSv26d+ko5vWlIDD6zoUJ7LW8vh+ZBC8QdljveRP+crNrtBwioEUWy/4dMtbBjA4ioNlg==", - "dev": true, - "dependencies": { - "base": "^0.11.1", - "debug": "^2.2.0", - "define-property": "^0.2.5", - "extend-shallow": "^2.0.1", - "map-cache": "^0.2.2", - "source-map": "^0.5.6", - "source-map-resolve": "^0.5.0", - "use": "^3.1.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/snapdragon-node": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/snapdragon-node/-/snapdragon-node-2.1.1.tgz", - "integrity": "sha512-O27l4xaMYt/RSQ5TR3vpWCAB5Kb/czIcqUFOM/C4fYcLnbZUc1PkjTAMjof2pBWaSTwOUd6qUHcFGVGj7aIwnw==", - "dev": true, - "dependencies": { - "define-property": "^1.0.0", - "isobject": "^3.0.0", - "snapdragon-util": "^3.0.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/snapdragon-node/node_modules/define-property": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", - "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", - "dev": true, - "dependencies": { - "is-descriptor": "^1.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/snapdragon-node/node_modules/is-accessor-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", - "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", - "dev": true, - "dependencies": { - "kind-of": "^6.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/snapdragon-node/node_modules/is-data-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", - "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", - "dev": true, - "dependencies": { - "kind-of": "^6.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/snapdragon-node/node_modules/is-descriptor": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", - "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", - "dev": true, - "dependencies": { - "is-accessor-descriptor": "^1.0.0", - "is-data-descriptor": "^1.0.0", - "kind-of": "^6.0.2" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/snapdragon-util": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/snapdragon-util/-/snapdragon-util-3.0.1.tgz", - "integrity": "sha512-mbKkMdQKsjX4BAL4bRYTj21edOf8cN7XHdYUJEe+Zn99hVEYcMvKPct1IqNe7+AZPirn8BCDOQBHQZknqmKlZQ==", - "dev": true, - "dependencies": { - "kind-of": "^3.2.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/snapdragon-util/node_modules/kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "dev": true, - "dependencies": { - "is-buffer": "^1.1.5" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/snapdragon/node_modules/define-property": { - "version": "0.2.5", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", - "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", + "node_modules/sort-keys": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/sort-keys/-/sort-keys-2.0.0.tgz", + "integrity": "sha512-/dPCrG1s3ePpWm6yBbxZq5Be1dXGLyLn9Z791chDC3NFrpkVbWGzkBwPN1knaciexFXgRJ7hzdnwZ4stHSDmjg==", "dev": true, + "license": "MIT", "dependencies": { - "is-descriptor": "^0.1.0" + "is-plain-obj": "^1.0.0" }, "engines": { - "node": ">=0.10.0" + "node": ">=4" } }, - "node_modules/snapdragon/node_modules/extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "node_modules/sort-keys-length": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/sort-keys-length/-/sort-keys-length-1.0.1.tgz", + "integrity": "sha512-GRbEOUqCxemTAk/b32F2xa8wDTs+Z1QHOkbhJDQTvv/6G3ZkbJ+frYWsTcc7cBB3Fu4wy4XlLCuNtJuMn7Gsvw==", "dev": true, + "license": "MIT", "dependencies": { - "is-extendable": "^0.1.0" + "sort-keys": "^1.0.0" }, "engines": { "node": ">=0.10.0" } }, - "node_modules/snapdragon/node_modules/source-map": { - "version": "0.5.7", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", - "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/sort-keys": { + "node_modules/sort-keys-length/node_modules/sort-keys": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/sort-keys/-/sort-keys-1.1.2.tgz", - "integrity": "sha1-RBttTTRnmPG05J6JIK37oOVD+a0=", + "integrity": "sha512-vzn8aSqKgytVik0iwdBEi+zevbTYZogewTUM6dtpmGwEcdzbub/TX4bCzRhebDCRC3QzXgJsLRKB2V/Oof7HXg==", "dev": true, + "license": "MIT", "dependencies": { "is-plain-obj": "^1.0.0" }, @@ -12533,18 +13014,6 @@ "node": ">=0.10.0" } }, - "node_modules/sort-keys-length": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/sort-keys-length/-/sort-keys-length-1.0.1.tgz", - "integrity": "sha1-nLb09OnkgVWmqgZx7dM2/xR5oYg=", - "dev": true, - "dependencies": { - "sort-keys": "^1.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/source-map": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", @@ -12554,20 +13023,6 @@ "node": ">=0.10.0" } }, - "node_modules/source-map-resolve": { - "version": "0.5.2", - "resolved": "https://registry.npmjs.org/source-map-resolve/-/source-map-resolve-0.5.2.tgz", - "integrity": "sha512-MjqsvNwyz1s0k81Goz/9vRBe9SZdB09Bdw+/zYyO+3CuPk6fouTaxscHkgtE8jKvf01kVfl8riHzERQ/kefaSA==", - "deprecated": "See https://github.com/lydell/source-map-resolve#deprecated", - "dev": true, - "dependencies": { - "atob": "^2.1.1", - "decode-uri-component": "^0.2.0", - "resolve-url": "^0.2.1", - "source-map-url": "^0.4.0", - "urix": "^0.1.0" - } - }, "node_modules/source-map-support": { "version": "0.5.21", "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", @@ -12578,81 +13033,30 @@ "source-map": "^0.6.0" } }, - "node_modules/source-map-url": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/source-map-url/-/source-map-url-0.4.0.tgz", - "integrity": "sha1-PpNdfd1zYxuXZZlW1VEo6HtQhKM=", - "deprecated": "See https://github.com/lydell/source-map-url#deprecated", - "dev": true - }, - "node_modules/sparkles": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/sparkles/-/sparkles-1.0.1.tgz", - "integrity": "sha512-dSO0DDYUahUt/0/pD/Is3VIm5TGJjludZ0HVymmhYF6eNA53PVLhnUk0znSYbH8IYBuJdCE+1luR22jNLMaQdw==", - "dev": true, - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/spawn-wrap": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/spawn-wrap/-/spawn-wrap-2.0.0.tgz", - "integrity": "sha512-EeajNjfN9zMnULLwhZZQU3GWBoFNkbngTUPfaawT4RkMiviTxcX0qfhVbGey39mfctfDHkWtuecgQ8NJcyQWHg==", - "dev": true, - "dependencies": { - "foreground-child": "^2.0.0", - "is-windows": "^1.0.2", - "make-dir": "^3.0.0", - "rimraf": "^3.0.0", - "signal-exit": "^3.0.2", - "which": "^2.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/spdx-correct": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.1.0.tgz", - "integrity": "sha512-lr2EZCctC2BNR7j7WzJ2FpDznxky1sjfxvvYEyzxNyb6lZXHODmEoJeFu4JupYlkfha1KZpJyoqiJ7pgA1qq8Q==", - "dev": true, - "dependencies": { - "spdx-expression-parse": "^3.0.0", - "spdx-license-ids": "^3.0.0" - } - }, - "node_modules/spdx-exceptions": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.2.0.tgz", - "integrity": "sha512-2XQACfElKi9SlVb1CYadKDXvoajPgBVPn/gOQLrTvHdElaVhr7ZEbqJaRnJLVNeaI4cMEAgVCeBMKF6MWRDCRA==", - "dev": true - }, - "node_modules/spdx-expression-parse": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.0.tgz", - "integrity": "sha512-Yg6D3XpRD4kkOmTpdgbUiEJFKghJH03fiC1OPll5h/0sO6neh2jqRDVHOQ4o/LMea0tgCkbMgea5ip/e+MkWyg==", + "node_modules/sparkles": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/sparkles/-/sparkles-2.1.0.tgz", + "integrity": "sha512-r7iW1bDw8R/cFifrD3JnQJX0K1jqT0kprL48BiBpLZLJPmAm34zsVBsK5lc7HirZYZqMW65dOXZgbAGt/I6frg==", "dev": true, - "dependencies": { - "spdx-exceptions": "^2.1.0", - "spdx-license-ids": "^3.0.0" + "engines": { + "node": ">= 10.13.0" } }, - "node_modules/spdx-license-ids": { - "version": "3.0.5", - "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.5.tgz", - "integrity": "sha512-J+FWzZoynJEXGphVIS+XEh3kFSjZX/1i9gFBaWQcB+/tmpe2qUsSBABpcxqxnAxFdiUFEgAX1bjYGQvIZmoz9Q==", - "dev": true - }, - "node_modules/split-string": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/split-string/-/split-string-3.1.0.tgz", - "integrity": "sha512-NzNVhJDYpwceVVii8/Hu6DKfD2G+NrQHlS/V/qgv763EYudVwEcMQNxd2lh+0VrUByXN/oJkl5grOhYWvQUYiw==", + "node_modules/spawn-wrap": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/spawn-wrap/-/spawn-wrap-2.0.0.tgz", + "integrity": "sha512-EeajNjfN9zMnULLwhZZQU3GWBoFNkbngTUPfaawT4RkMiviTxcX0qfhVbGey39mfctfDHkWtuecgQ8NJcyQWHg==", "dev": true, "dependencies": { - "extend-shallow": "^3.0.0" + "foreground-child": "^2.0.0", + "is-windows": "^1.0.2", + "make-dir": "^3.0.0", + "rimraf": "^3.0.0", + "signal-exit": "^3.0.2", + "which": "^2.0.1" }, "engines": { - "node": ">=0.10.0" + "node": ">=8" } }, "node_modules/sprintf-js": { @@ -12674,29 +13078,14 @@ "node": "*" } }, - "node_modules/static-extend": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/static-extend/-/static-extend-0.1.2.tgz", - "integrity": "sha1-YICcOcv/VTNyJv1eC1IPNB8ftcY=", - "dev": true, - "dependencies": { - "define-property": "^0.2.5", - "object-copy": "^0.1.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/static-extend/node_modules/define-property": { - "version": "0.2.5", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", - "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", + "node_modules/stoppable": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/stoppable/-/stoppable-1.1.0.tgz", + "integrity": "sha512-KXDYZ9dszj6bzvnEMRYvxgeTHU74QBFL54XKtP3nyMuJ81CFYtABZ3bAzL2EdFUaEwJOBOgENyFj3R7oTzDyyw==", "dev": true, - "dependencies": { - "is-descriptor": "^0.1.0" - }, "engines": { - "node": ">=0.10.0" + "node": ">=4", + "npm": ">=6" } }, "node_modules/stream-browserify": { @@ -12709,6 +13098,15 @@ "readable-stream": "^2.0.2" } }, + "node_modules/stream-composer": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/stream-composer/-/stream-composer-1.0.2.tgz", + "integrity": "sha512-bnBselmwfX5K10AH6L4c8+S5lgZMWI7ZYrz2rvYjCPB2DIMC4Ig8OpxGpNJSxRZ58oti7y1IcNvjBAz9vW5m4w==", + "dev": true, + "dependencies": { + "streamx": "^2.13.2" + } + }, "node_modules/stream-exhaust": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/stream-exhaust/-/stream-exhaust-1.0.2.tgz", @@ -12734,11 +13132,26 @@ "integrity": "sha1-1cdSgl5TZ+eG944Y5EXqIjoVWVI=", "dev": true }, + "node_modules/streamx": { + "version": "2.18.0", + "resolved": "https://registry.npmjs.org/streamx/-/streamx-2.18.0.tgz", + "integrity": "sha512-LLUC1TWdjVdn1weXGcSxyTR3T4+acB6tVGXT95y0nGbca4t4o/ng1wKAGTljm9VicuCVLvRlqFYXYy5GwgM7sQ==", + "dev": true, + "dependencies": { + "fast-fifo": "^1.3.2", + "queue-tick": "^1.0.1", + "text-decoder": "^1.1.0" + }, + "optionalDependencies": { + "bare-events": "^2.2.0" + } + }, "node_modules/strict-uri-encode": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/strict-uri-encode/-/strict-uri-encode-1.1.0.tgz", - "integrity": "sha1-J5siXfHVgrH1TmWt3UNS4Y+qBxM=", + "integrity": "sha512-R3f198pcvnB+5IpnBlRkphuE9n46WyVl8I39W/ZUTZLz4nqSP/oLYUrcnJrw462Ds8he4YKMov2efsTIw1BDGQ==", "dev": true, + "license": "MIT", "engines": { "node": ">=0.10.0" } @@ -12773,38 +13186,33 @@ ] }, "node_modules/string-width": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", - "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", "dev": true, "dependencies": { - "code-point-at": "^1.0.0", - "is-fullwidth-code-point": "^1.0.0", - "strip-ansi": "^3.0.0" + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" }, "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/string-width/node_modules/ansi-regex": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", - "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", - "dev": true, - "engines": { - "node": ">=0.10.0" + "node": ">=8" } }, - "node_modules/string-width/node_modules/strip-ansi": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", - "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", + "node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", "dev": true, + "license": "MIT", "dependencies": { - "ansi-regex": "^2.0.0" + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" }, "engines": { - "node": ">=0.10.0" + "node": ">=8" } }, "node_modules/string.prototype.matchall": { @@ -12826,27 +13234,50 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/string.prototype.trim": { + "version": "1.2.9", + "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.9.tgz", + "integrity": "sha512-klHuCNxiMZ8MlsOihJhJEBJAiMVqU3Z2nEXWfWnIqjN0gEFS9J9+IxKozWWtQGcgoa1WUZzLjKPTr4ZHNFTFxw==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.0", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/string.prototype.trimend": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.4.tgz", - "integrity": "sha512-y9xCjw1P23Awk8EvTpcyL2NIr1j7wJ39f+k6lvRnSMz+mz9CGz9NYPelDk42kOz6+ql8xjfK8oYzy3jAP5QU5A==", + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.8.tgz", + "integrity": "sha512-p73uL5VCHCO2BZZ6krwwQE3kCzM7NKmis8S//xEC6fQonchbum4eP6kR4DLEjQFO3Wnj3Fuo8NM0kOSjVdHjZQ==", "dev": true, "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.3" + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" }, "funding": { "url": "https://github.com/sponsors/ljharb" } }, "node_modules/string.prototype.trimstart": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.4.tgz", - "integrity": "sha512-jh6e984OBfvxS50tdY2nRZnoC5/mLFKOREQfw8t5yytkoUsJRNxvI/E39qu1sD0OtWI3OC0XgKSmcWwziwYuZw==", + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.8.tgz", + "integrity": "sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==", "dev": true, "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.3" + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -12864,16 +13295,18 @@ "node": ">=8" } }, - "node_modules/strip-bom": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-2.0.0.tgz", - "integrity": "sha1-YhmoVhZSBJHzV4i9vxRHqZx+aw4=", + "node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", "dev": true, + "license": "MIT", "dependencies": { - "is-utf8": "^0.2.0" + "ansi-regex": "^5.0.1" }, "engines": { - "node": ">=0.10.0" + "node": ">=8" } }, "node_modules/strip-dirs": { @@ -12881,6 +13314,7 @@ "resolved": "https://registry.npmjs.org/strip-dirs/-/strip-dirs-2.1.0.tgz", "integrity": "sha512-JOCxOeKLm2CAS73y/U4ZeZPTkE+gNVCzKt7Eox84Iej1LT/2pTWYpZKJuxwQpvX1LiZb1xokNR7RLfuBAa7T3g==", "dev": true, + "license": "MIT", "dependencies": { "is-natural-number": "^4.0.1" } @@ -12911,6 +13345,7 @@ "resolved": "https://registry.npmjs.org/strip-outer/-/strip-outer-1.0.1.tgz", "integrity": "sha512-k55yxKHwaXnpYGsOzg4Vl8+tDrWylxDEpknGjhTiZB8dFRU5rTo9CAzeycivxV3s+zlTKwrs6WxMxR95n26kwg==", "dev": true, + "license": "MIT", "dependencies": { "escape-string-regexp": "^1.0.2" }, @@ -12939,7 +13374,6 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", - "dev": true, "engines": { "node": ">= 0.4" }, @@ -12947,90 +13381,42 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/sver-compat": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/sver-compat/-/sver-compat-1.5.0.tgz", - "integrity": "sha1-PPh9/rTQe0o/FIJ7wYaz/QxkXNg=", - "dev": true, - "dependencies": { - "es6-iterator": "^2.0.1", - "es6-symbol": "^3.1.1" - } - }, - "node_modules/table": { - "version": "6.7.5", - "resolved": "https://registry.npmjs.org/table/-/table-6.7.5.tgz", - "integrity": "sha512-LFNeryOqiQHqCVKzhkymKwt6ozeRhlm8IL1mE8rNUurkir4heF6PzMyRgaTa4tlyPTGGgXuvVOF/OLWiH09Lqw==", - "dev": true, - "dependencies": { - "ajv": "^8.0.1", - "lodash.truncate": "^4.4.2", - "slice-ansi": "^4.0.0", - "string-width": "^4.2.3", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=10.0.0" - } - }, - "node_modules/table/node_modules/ajv": { - "version": "8.10.0", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.10.0.tgz", - "integrity": "sha512-bzqAEZOjkrUMl2afH8dknrq5KEk2SrwdBROR+vH1EKVQTqaUbJVPdc/gEdggTMM0Se+s+Ja4ju4TlNcStKl2Hw==", - "dev": true, - "dependencies": { - "fast-deep-equal": "^3.1.1", - "json-schema-traverse": "^1.0.0", - "require-from-string": "^2.0.2", - "uri-js": "^4.2.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "node_modules/table/node_modules/is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "node_modules/sver": { + "version": "1.8.4", + "resolved": "https://registry.npmjs.org/sver/-/sver-1.8.4.tgz", + "integrity": "sha512-71o1zfzyawLfIWBOmw8brleKyvnbn73oVHNCsu51uPMz/HWiKkkXsI31JjHW5zqXEqnPYkIiHd8ZmL7FCimLEA==", "dev": true, - "engines": { - "node": ">=8" + "optionalDependencies": { + "semver": "^6.3.0" } }, - "node_modules/table/node_modules/json-schema-traverse": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", - "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", - "dev": true - }, - "node_modules/table/node_modules/string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "node_modules/sver/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", "dev": true, - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" + "optional": true, + "bin": { + "semver": "bin/semver.js" } }, "node_modules/tapable": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.1.tgz", - "integrity": "sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==", + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.0.tgz", + "integrity": "sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==", "dev": true, "engines": { "node": ">=6" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" } }, "node_modules/tar-fs": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.1.tgz", - "integrity": "sha512-V0r2Y9scmbDRLCNex/+hYzvp/zyYjvFbHPNgVTKfQvVrb6guiE/fxP+XblDNR011utopbkex2nM4dHNV6GDsng==", + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.4.tgz", + "integrity": "sha512-mDAjwmZdh7LTT6pNleZ05Yt65HC3E+NiQzl672vQG38jIrehtJk/J3mNwIg+vShQPcLF/LV7CMnDW6vjj6sfYQ==", "dev": true, "optional": true, "dependencies": { @@ -13064,9 +13450,9 @@ } }, "node_modules/tar-fs/node_modules/readable-stream": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", - "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", "dev": true, "optional": true, "dependencies": { @@ -13100,6 +13486,7 @@ "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-1.6.2.tgz", "integrity": "sha512-rzS0heiNf8Xn7/mpdSVVSMAWAoy9bfb1WOTYC78Z0UQKeKa/CWS8FOq0lKGNa8DWKAn9gxjCvMLYc5PGXYlK2A==", "dev": true, + "license": "MIT", "dependencies": { "bl": "^1.0.0", "buffer-alloc": "^1.2.0", @@ -13114,21 +13501,27 @@ } }, "node_modules/tas-client": { - "version": "0.1.58", - "resolved": "https://registry.npmjs.org/tas-client/-/tas-client-0.1.58.tgz", - "integrity": "sha512-fOWii4wQXuo9Zl0oXgvjBzZWzKc5MmUR6XQWX93WU2c1SaP1plPo/zvXP8kpbZ9fvegFOHdapszYqMTRq/SRtg==", + "version": "0.2.33", + "resolved": "https://registry.npmjs.org/tas-client/-/tas-client-0.2.33.tgz", + "integrity": "sha512-V+uqV66BOQnWxvI6HjDnE4VkInmYZUQ4dgB7gzaDyFyFSK1i1nF/j7DpS9UbQAgV9NaF1XpcyuavnM1qOeiEIg==" + }, + "node_modules/teex": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/teex/-/teex-1.0.1.tgz", + "integrity": "sha512-eYE6iEI62Ni1H8oIa7KlDU6uQBtqr4Eajni3wX7rpfXD8ysFx8z0+dri+KWEPWpBsxXfxu58x/0jvTVT1ekOSg==", + "dev": true, "dependencies": { - "axios": "^0.26.1" + "streamx": "^2.12.5" } }, "node_modules/terser": { - "version": "5.14.2", - "resolved": "https://registry.npmjs.org/terser/-/terser-5.14.2.tgz", - "integrity": "sha512-oL0rGeM/WFQCUd0y2QrWxYnq7tfSuKBiqTjRPWrRgB46WD/kiwHwF8T23z78H6Q6kGCuuHcPB+KULHRdxvVGQA==", + "version": "5.46.0", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.46.0.tgz", + "integrity": "sha512-jTwoImyr/QbOWFFso3YoU3ik0jBBDJ6JTOQiy/J2YxVJdZCc+5u7skhNwiOR3FQIygFqVUPHl7qbbxtjW2K3Qg==", "dev": true, "dependencies": { - "@jridgewell/source-map": "^0.3.2", - "acorn": "^8.5.0", + "@jridgewell/source-map": "^0.3.3", + "acorn": "^8.15.0", "commander": "^2.20.0", "source-map-support": "~0.5.20" }, @@ -13140,16 +13533,16 @@ } }, "node_modules/terser-webpack-plugin": { - "version": "5.3.1", - "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.1.tgz", - "integrity": "sha512-GvlZdT6wPQKbDNW/GDQzZFg/j4vKU96yl2q6mcUkzKOgW4gwf1Z8cZToUCrz31XHlPWH8MVb1r2tFtdDtTGJ7g==", + "version": "5.3.17", + "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.17.tgz", + "integrity": "sha512-YR7PtUp6GMU91BgSJmlaX/rS2lGDbAF7D+Wtq7hRO+MiljNmodYvqslzCFiYVAgW+Qoaaia/QUIP4lGXufjdZw==", "dev": true, + "license": "MIT", "dependencies": { + "@jridgewell/trace-mapping": "^0.3.25", "jest-worker": "^27.4.5", - "schema-utils": "^3.1.1", - "serialize-javascript": "^6.0.0", - "source-map": "^0.6.1", - "terser": "^5.7.2" + "schema-utils": "^4.3.0", + "terser": "^5.31.1" }, "engines": { "node": ">= 10.13.0" @@ -13173,6 +13566,60 @@ } } }, + "node_modules/terser-webpack-plugin/node_modules/ajv": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz", + "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/terser-webpack-plugin/node_modules/ajv-keywords": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz", + "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==", + "dev": true, + "dependencies": { + "fast-deep-equal": "^3.1.3" + }, + "peerDependencies": { + "ajv": "^8.8.2" + } + }, + "node_modules/terser-webpack-plugin/node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true + }, + "node_modules/terser-webpack-plugin/node_modules/schema-utils": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.3.tgz", + "integrity": "sha512-eflK8wEtyOE6+hsaRVPxvUKYCpRgzLqDTb8krvAsRIwOGlHoSgYLgBXoubGgLd2fT41/OUYdb48v4k4WWHQurA==", + "dev": true, + "dependencies": { + "@types/json-schema": "^7.0.9", + "ajv": "^8.9.0", + "ajv-formats": "^2.1.1", + "ajv-keywords": "^5.1.0" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, "node_modules/test-exclude": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", @@ -13188,10 +13635,11 @@ } }, "node_modules/test-exclude/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", "dev": true, + "license": "ISC", "dependencies": { "brace-expansion": "^1.1.7" }, @@ -13199,6 +13647,15 @@ "node": "*" } }, + "node_modules/text-decoder": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/text-decoder/-/text-decoder-1.1.0.tgz", + "integrity": "sha512-TmLJNj6UgX8xcUZo4UDStGQtDiTzF7BzWlzn9g7UWrjkpHr5uJTK1ld16wZ3LXb2vb6jH8qU89dW5whuMdXYdw==", + "dev": true, + "dependencies": { + "b4a": "^1.6.4" + } + }, "node_modules/text-table": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", @@ -13208,8 +13665,9 @@ "node_modules/through": { "version": "2.3.8", "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", - "integrity": "sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=", - "dev": true + "integrity": "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==", + "dev": true, + "license": "MIT" }, "node_modules/through2": { "version": "2.0.5", @@ -13231,20 +13689,12 @@ "xtend": "~4.0.0" } }, - "node_modules/time-stamp": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/time-stamp/-/time-stamp-1.1.0.tgz", - "integrity": "sha1-dkpaEa9QVhkhsTPztE5hhofg9cM=", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/timed-out": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/timed-out/-/timed-out-4.0.1.tgz", - "integrity": "sha1-8y6srFoXW+ol1/q1Zas+2HQe9W8=", + "integrity": "sha512-G7r3AhovYtr5YKOWQkta8RKAPb+J9IsO4uVmzjl8AZwfhs8UcUwTiD6gcJYSgOtzyjvQKrKYn41syHbUWMkafA==", "dev": true, + "license": "MIT", "engines": { "node": ">=0.10.0" } @@ -13262,14 +13712,12 @@ } }, "node_modules/tmp": { - "version": "0.0.33", - "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz", - "integrity": "sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==", - "dependencies": { - "os-tmpdir": "~1.0.2" - }, + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.5.tgz", + "integrity": "sha512-voyz6MApa1rQGUxT3E+BK7/ROe8itEx7vD8/HEvt4xwXucvQ5G5oeEiHkmHZJuBO21RpOf+YYm9MOivj709jow==", + "license": "MIT", "engines": { - "node": ">=0.6.0" + "node": ">=14.14" } }, "node_modules/to-absolute-glob": { @@ -13292,70 +13740,58 @@ "dev": true }, "node_modules/to-buffer": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/to-buffer/-/to-buffer-1.1.1.tgz", - "integrity": "sha512-lx9B5iv7msuFYE3dytT+KE5tap+rNYw+K4jVkb9R/asAb+pbBSM17jtunHplhBe6RRJdZx3Pn2Jph24O32mOVg==", - "dev": true - }, - "node_modules/to-fast-properties": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", - "integrity": "sha1-3F5pjL0HkmW8c+A3doGk5Og/YW4=", - "dev": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/to-object-path": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/to-object-path/-/to-object-path-0.3.0.tgz", - "integrity": "sha1-KXWIt7Dn4KwI4E5nL4XB9JmeF68=", - "dev": true, - "dependencies": { - "kind-of": "^3.0.2" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/to-object-path/node_modules/kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "dev": true, - "dependencies": { - "is-buffer": "^1.1.5" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/to-regex": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/to-regex/-/to-regex-3.0.2.tgz", - "integrity": "sha512-FWtleNAtZ/Ki2qtqej2CXTOayOH9bHDQF+Q48VpWyDXjbYxA4Yz8iDB31zXOBUlOHHKidDbqGVrTUvQMPmBGBw==", + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/to-buffer/-/to-buffer-1.2.2.tgz", + "integrity": "sha512-db0E3UJjcFhpDhAF4tLo03oli3pwl3dbnzXOUIlRKrp+ldk/VUxzpWYZENsw2SZiuBjHAk7DfB0VU7NKdpb6sw==", "dev": true, + "license": "MIT", "dependencies": { - "define-property": "^2.0.2", - "extend-shallow": "^3.0.2", - "regex-not": "^1.0.2", - "safe-regex": "^1.1.0" + "isarray": "^2.0.5", + "safe-buffer": "^5.2.1", + "typed-array-buffer": "^1.0.3" }, "engines": { - "node": ">=0.10.0" + "node": ">= 0.4" } }, + "node_modules/to-buffer/node_modules/isarray": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", + "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", + "dev": true, + "license": "MIT" + }, + "node_modules/to-buffer/node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, "node_modules/to-regex-range": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-2.1.1.tgz", - "integrity": "sha1-fIDBe53+vlmeJzZ+DU3VWQFB2zg=", + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", "dev": true, "dependencies": { - "is-number": "^3.0.0", - "repeat-string": "^1.6.1" + "is-number": "^7.0.0" }, "engines": { - "node": ">=0.10.0" + "node": ">=8.0" } }, "node_modules/to-through": { @@ -13379,20 +13815,12 @@ "node": ">=6" } }, - "node_modules/traverse": { - "version": "0.3.9", - "resolved": "https://registry.npmjs.org/traverse/-/traverse-0.3.9.tgz", - "integrity": "sha1-cXuPIgzAu3tE5AUUwisui7xw2Lk=", - "dev": true, - "engines": { - "node": "*" - } - }, "node_modules/trim-repeated": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/trim-repeated/-/trim-repeated-1.0.0.tgz", - "integrity": "sha1-42RqLqTokTEr9+rObPsFOAvAHCE=", + "integrity": "sha512-pkonvlKk8/ZuR0D5tLW8ljt5I8kmxp2XKymhepUeOdCEfKpZaktSArkLHZt76OB1ZvO9bssUsDty4SWhLvZpLg==", "dev": true, + "license": "MIT", "dependencies": { "escape-string-regexp": "^1.0.2" }, @@ -13400,6 +13828,18 @@ "node": ">=0.10.0" } }, + "node_modules/ts-api-utils": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.3.0.tgz", + "integrity": "sha512-UQMIo7pb8WRomKR1/+MFVLTroIvDVtMX3K6OUir8ynLyzB8Jeriont2bTAtmNPa1ekAgN7YPDyf6V+ygrdU+eQ==", + "dev": true, + "engines": { + "node": ">=16" + }, + "peerDependencies": { + "typescript": ">=4.2.0" + } + }, "node_modules/ts-loader": { "version": "9.2.8", "resolved": "https://registry.npmjs.org/ts-loader/-/ts-loader-9.2.8.tgz", @@ -13434,18 +13874,6 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "node_modules/ts-loader/node_modules/braces": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", - "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", - "dev": true, - "dependencies": { - "fill-range": "^7.0.1" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/ts-loader/node_modules/chalk": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", @@ -13480,18 +13908,6 @@ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "dev": true }, - "node_modules/ts-loader/node_modules/fill-range": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", - "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", - "dev": true, - "dependencies": { - "to-regex-range": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/ts-loader/node_modules/has-flag": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", @@ -13501,43 +13917,6 @@ "node": ">=8" } }, - "node_modules/ts-loader/node_modules/is-number": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", - "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", - "dev": true, - "engines": { - "node": ">=0.12.0" - } - }, - "node_modules/ts-loader/node_modules/micromatch": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz", - "integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==", - "dev": true, - "dependencies": { - "braces": "^3.0.2", - "picomatch": "^2.3.1" - }, - "engines": { - "node": ">=8.6" - } - }, - "node_modules/ts-loader/node_modules/semver": { - "version": "7.3.5", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.5.tgz", - "integrity": "sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==", - "dev": true, - "dependencies": { - "lru-cache": "^6.0.0" - }, - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/ts-loader/node_modules/supports-color": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", @@ -13550,18 +13929,6 @@ "node": ">=8" } }, - "node_modules/ts-loader/node_modules/to-regex-range": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", - "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", - "dev": true, - "dependencies": { - "is-number": "^7.0.0" - }, - "engines": { - "node": ">=8.0" - } - }, "node_modules/ts-mockito": { "version": "2.6.1", "resolved": "https://registry.npmjs.org/ts-mockito/-/ts-mockito-2.6.1.tgz", @@ -13615,13 +13982,13 @@ } }, "node_modules/tsconfig-paths": { - "version": "3.14.1", - "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.14.1.tgz", - "integrity": "sha512-fxDhWnFSLt3VuTwtvJt5fpwxBHg5AdKWMsgcPOOIilyjymcYVZoCQF8fvFRezCNfblEXmi+PcM1eYHeOAgXCOQ==", + "version": "3.15.0", + "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.15.0.tgz", + "integrity": "sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg==", "dev": true, "dependencies": { "@types/json5": "^0.0.29", - "json5": "^1.0.1", + "json5": "^1.0.2", "minimist": "^1.2.6", "strip-bom": "^3.0.0" } @@ -13733,21 +14100,6 @@ "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.10.0.tgz", "integrity": "sha512-qOebF53frne81cf0S9B41ByenJ3/IuH8yJKngAX35CmiZySA0khhkovshKK+jGCaMnVomla7gVlIcc3EvKPbTQ==" }, - "node_modules/tsutils": { - "version": "3.21.0", - "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-3.21.0.tgz", - "integrity": "sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==", - "dev": true, - "dependencies": { - "tslib": "^1.8.1" - }, - "engines": { - "node": ">= 6" - }, - "peerDependencies": { - "typescript": ">=2.8.0 || >= 3.2.0-dev || >= 3.3.0-dev || >= 3.4.0-dev || >= 3.5.0-dev || >= 3.6.0-dev || >= 3.6.0-beta || >= 3.7.0-dev || >= 3.7.0-beta" - } - }, "node_modules/tty-browserify": { "version": "0.0.0", "resolved": "https://registry.npmjs.org/tty-browserify/-/tty-browserify-0.0.0.tgz", @@ -13766,7 +14118,7 @@ "node_modules/tunnel-agent": { "version": "0.6.0", "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", - "integrity": "sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0=", + "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", "dev": true, "optional": true, "dependencies": { @@ -13776,11 +14128,17 @@ "node": "*" } }, - "node_modules/type": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/type/-/type-1.0.1.tgz", - "integrity": "sha512-MAM5dBMJCJNKs9E7JXo4CXRAansRfG0nlJxW7Wf6GZzSOvH31zClSaHdIMWLehe/EGMBkqeC55rrkaOr5Oo7Nw==", - "dev": true + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } }, "node_modules/type-detect": { "version": "4.0.8", @@ -13800,38 +14158,90 @@ "node": ">=8" } }, - "node_modules/typed-rest-client": { - "version": "1.8.5", - "resolved": "https://registry.npmjs.org/typed-rest-client/-/typed-rest-client-1.8.5.tgz", - "integrity": "sha512-952/Aegu3lTqUAI1anbDLbewojnF/gh8at9iy1CIrfS1h/+MtNjB1Y9z6ZF5n2kZd+97em56lZ9uu7Zz3y/pwg==", - "deprecated": "1.8.5 contains changes that are not compatible with Node 6", + "node_modules/typed-array-buffer": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz", + "integrity": "sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==", "dev": true, + "license": "MIT", "dependencies": { - "qs": "^6.9.1", - "tunnel": "0.0.6", - "underscore": "^1.12.1" + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-typed-array": "^1.1.14" + }, + "engines": { + "node": ">= 0.4" } }, - "node_modules/typed-rest-client/node_modules/qs": { - "version": "6.11.0", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz", - "integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==", + "node_modules/typed-array-byte-length": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/typed-array-byte-length/-/typed-array-byte-length-1.0.1.tgz", + "integrity": "sha512-3iMJ9q0ao7WE9tWcaYKIptkNBuOIcZCCT0d4MRvuuH88fEoEH62IuQe0OtraD3ebQEoTRk8XCBoknUNc1Y67pw==", "dev": true, "dependencies": { - "side-channel": "^1.0.4" + "call-bind": "^1.0.7", + "for-each": "^0.3.3", + "gopd": "^1.0.1", + "has-proto": "^1.0.3", + "is-typed-array": "^1.1.13" }, "engines": { - "node": ">=0.6" + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/typedarray": { - "version": "0.0.6", - "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", - "integrity": "sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=", - "dev": true + "node_modules/typed-array-byte-offset": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/typed-array-byte-offset/-/typed-array-byte-offset-1.0.2.tgz", + "integrity": "sha512-Ous0vodHa56FviZucS2E63zkgtgrACj7omjwd/8lTEMEPFFyjfixMZ1ZXenpgCFBBt4EC1J2XsyVS2gkG0eTFA==", + "dev": true, + "dependencies": { + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.7", + "for-each": "^0.3.3", + "gopd": "^1.0.1", + "has-proto": "^1.0.3", + "is-typed-array": "^1.1.13" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typed-array-length": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.6.tgz", + "integrity": "sha512-/OxDN6OtAk5KBpGb28T+HZc2M+ADtvRxXrKKbUwtsLgdoxgX13hyy7ek6bFRl5+aBs2yZzB0c4CnQfAtVypW/g==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.7", + "for-each": "^0.3.3", + "gopd": "^1.0.1", + "has-proto": "^1.0.3", + "is-typed-array": "^1.1.13", + "possible-typed-array-names": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typed-rest-client": { + "version": "1.8.11", + "resolved": "https://registry.npmjs.org/typed-rest-client/-/typed-rest-client-1.8.11.tgz", + "integrity": "sha512-5UvfMpd1oelmUPRbbaVnq+rHP7ng2cE4qoQkQeAqxRL6PklkxsM0g32/HL0yfvruK6ojQ5x8EE+HF4YV6DtuCA==", + "dev": true, + "dependencies": { + "qs": "^6.9.1", + "tunnel": "0.0.6", + "underscore": "^1.12.1" + } }, "node_modules/typedarray-to-buffer": { "version": "3.1.5", @@ -13858,16 +14268,16 @@ } }, "node_modules/typescript": { - "version": "4.5.5", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.5.5.tgz", - "integrity": "sha512-TCTIul70LyWe6IJWT8QSYeA54WQe8EjQFU4wY52Fasj5UKx88LNYKCgBEHcOMOrFF1rKGbD8v/xcNWVUq9SymA==", + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.2.2.tgz", + "integrity": "sha512-mI4WrpHsbCIcwT9cF4FZvr80QUeKvsUsUvKDoR+X/7XHQH98xYD8YHZg7ANtz2GtZt/CBq2QJ0thkGJMHfqc1w==", "dev": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" }, "engines": { - "node": ">=4.2.0" + "node": ">=14.17" } }, "node_modules/uc.micro": { @@ -13882,14 +14292,14 @@ "integrity": "sha512-mliiCSrsE29aNBI7O9W5gGv6WmA9kBR8PtTt6Apaxns076IRdYrrtFhXHEWMj5CSum3U7cv7/pi4xmi4XsIOqg==" }, "node_modules/unbox-primitive": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.1.tgz", - "integrity": "sha512-tZU/3NqK3dA5gpE1KtyiJUrEB0lxnGkMFHptJ7q6ewdZ8s12QrODwNbhIJStmJkd1QDXa1NRA8aF2A1zk/Ypyw==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.2.tgz", + "integrity": "sha512-61pPlCD9h51VoreyJ0BReideM3MDKMKnh6+V9L08331ipq6Q8OFXZYiqP6n/tbHx4s5I9uRhcye6BrbkizkBDw==", "dev": true, "dependencies": { - "function-bind": "^1.1.1", - "has-bigints": "^1.0.1", - "has-symbols": "^1.0.2", + "call-bind": "^1.0.2", + "has-bigints": "^1.0.2", + "has-symbols": "^1.0.3", "which-boxed-primitive": "^1.0.2" }, "funding": { @@ -13897,10 +14307,11 @@ } }, "node_modules/unbzip2-stream": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/unbzip2-stream/-/unbzip2-stream-1.3.3.tgz", - "integrity": "sha512-fUlAF7U9Ah1Q6EieQ4x4zLNejrRvDWUYmxXUpN3uziFYCHapjWFaCAnreY9bGgxzaMCFAPPpYNng57CypwJVhg==", + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/unbzip2-stream/-/unbzip2-stream-1.4.3.tgz", + "integrity": "sha512-mlExGW4w71ebDJviH16lQLtZS32VKqsSfk80GCfUlwT/4/hNRFsoscrF/c++9xinkMzECL1uL9DDwXqFWkruPg==", "dev": true, + "license": "MIT", "dependencies": { "buffer": "^5.2.1", "through": "^2.3.8" @@ -13916,40 +14327,51 @@ } }, "node_modules/underscore": { - "version": "1.13.1", - "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.13.1.tgz", - "integrity": "sha512-hzSoAVtJF+3ZtiFX0VgfFPHEDRm7Y/QPjGyNo4TVdnDTdft3tr8hEkD25a1jC+TjTuE7tkHGKkhwCgs9dgBB2g==", + "version": "1.13.6", + "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.13.6.tgz", + "integrity": "sha512-+A5Sja4HP1M08MaXya7p5LvjuM7K6q/2EaC0+iovj/wOcMsTzMvDFbasi/oSapiwOlt252IqsKqPjCl7huKS0A==", "dev": true }, "node_modules/undertaker": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/undertaker/-/undertaker-1.2.1.tgz", - "integrity": "sha512-71WxIzDkgYk9ZS+spIB8iZXchFhAdEo2YU8xYqBYJ39DIUIqziK78ftm26eecoIY49X0J2MLhG4hr18Yp6/CMA==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/undertaker/-/undertaker-2.0.0.tgz", + "integrity": "sha512-tO/bf30wBbTsJ7go80j0RzA2rcwX6o7XPBpeFcb+jzoeb4pfMM2zUeSDIkY1AWqeZabWxaQZ/h8N9t35QKDLPQ==", "dev": true, "dependencies": { - "arr-flatten": "^1.0.1", - "arr-map": "^2.0.0", - "bach": "^1.0.0", - "collection-map": "^1.0.0", - "es6-weak-map": "^2.0.1", - "last-run": "^1.1.0", - "object.defaults": "^1.0.0", - "object.reduce": "^1.0.0", - "undertaker-registry": "^1.0.0" + "bach": "^2.0.1", + "fast-levenshtein": "^3.0.0", + "last-run": "^2.0.0", + "undertaker-registry": "^2.0.0" }, "engines": { - "node": ">= 0.10" + "node": ">=10.13.0" } }, "node_modules/undertaker-registry": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/undertaker-registry/-/undertaker-registry-1.0.1.tgz", - "integrity": "sha1-XkvaMI5KiirlhPm5pDWaSZglzFA=", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/undertaker-registry/-/undertaker-registry-2.0.0.tgz", + "integrity": "sha512-+hhVICbnp+rlzZMgxXenpvTxpuvA67Bfgtt+O9WOE5jo7w/dyiF1VmoZVIHvP2EkUjsyKyTwYKlLhA+j47m1Ew==", "dev": true, "engines": { - "node": ">= 0.10" + "node": ">= 10.13.0" + } + }, + "node_modules/undertaker/node_modules/fast-levenshtein": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-3.0.0.tgz", + "integrity": "sha512-hKKNajm46uNmTlhHSyZkmToAc56uZJwYq7yrciZjqOxnlfQwERDQJmHPUp7m1m9wx8vgOe8IaCKZ5Kv2k1DdCQ==", + "dev": true, + "dependencies": { + "fastest-levenshtein": "^1.0.7" } }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true, + "license": "MIT" + }, "node_modules/unicode": { "version": "14.0.0", "resolved": "https://registry.npmjs.org/unicode/-/unicode-14.0.0.tgz", @@ -13958,21 +14380,6 @@ "node": ">= 0.8.x" } }, - "node_modules/union-value": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/union-value/-/union-value-1.0.1.tgz", - "integrity": "sha512-tJfXmxMeWYnczCVs7XAEvIV7ieppALdyepWMkHkwciRpZraG/xwT+s2JN8+pr1+8jCRf80FFzvr+MpQeeoF4Xg==", - "dev": true, - "dependencies": { - "arr-union": "^3.1.0", - "get-value": "^2.0.6", - "is-extendable": "^0.1.1", - "set-value": "^2.0.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/unique-stream": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/unique-stream/-/unique-stream-2.3.1.tgz", @@ -13983,94 +14390,34 @@ "through2-filter": "^3.0.0" } }, - "node_modules/unset-value": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/unset-value/-/unset-value-1.0.0.tgz", - "integrity": "sha1-g3aHP30jNRef+x5vw6jtDfyKtVk=", - "dev": true, - "dependencies": { - "has-value": "^0.3.1", - "isobject": "^3.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/unset-value/node_modules/has-value": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/has-value/-/has-value-0.3.1.tgz", - "integrity": "sha1-ex9YutpiyoJ+wKIHgCVlSEWZXh8=", + "node_modules/update-browserslist-db": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], "dependencies": { - "get-value": "^2.0.3", - "has-values": "^0.1.4", - "isobject": "^2.0.0" + "escalade": "^3.2.0", + "picocolors": "^1.1.1" }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/unset-value/node_modules/has-value/node_modules/isobject": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-2.1.0.tgz", - "integrity": "sha1-8GVWEJaj8dou9GJy+BXIQNh+DIk=", - "dev": true, - "dependencies": { - "isarray": "1.0.0" + "bin": { + "update-browserslist-db": "cli.js" }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/unset-value/node_modules/has-values": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/has-values/-/has-values-0.1.4.tgz", - "integrity": "sha1-bWHeldkd/Km5oCCJrThL/49it3E=", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/untildify": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/untildify/-/untildify-4.0.0.tgz", - "integrity": "sha512-KK8xQ1mkzZeg9inewmFVDNkg3l5LUhoq9kN6iWYB/CC9YMG8HA+c1Q8HwDe6dEX7kErrEVNVBO3fWsVq5iDgtw==", - "engines": { - "node": ">=8" - } - }, - "node_modules/unzipper": { - "version": "0.10.11", - "resolved": "https://registry.npmjs.org/unzipper/-/unzipper-0.10.11.tgz", - "integrity": "sha512-+BrAq2oFqWod5IESRjL3S8baohbevGcVA+teAIOYWM3pDVdseogqbzhhvvmiyQrUNKFUnDMtELW3X8ykbyDCJw==", - "dev": true, - "dependencies": { - "big-integer": "^1.6.17", - "binary": "~0.3.0", - "bluebird": "~3.4.1", - "buffer-indexof-polyfill": "~1.0.0", - "duplexer2": "~0.1.4", - "fstream": "^1.0.12", - "graceful-fs": "^4.2.2", - "listenercount": "~1.0.1", - "readable-stream": "~2.3.6", - "setimmediate": "~1.0.4" - } - }, - "node_modules/unzipper/node_modules/bluebird": { - "version": "3.4.7", - "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.4.7.tgz", - "integrity": "sha1-9y12C+Cbf3bQjtj66Ysomo0F+rM=", - "dev": true - }, - "node_modules/upath": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/upath/-/upath-1.2.0.tgz", - "integrity": "sha512-aZwGpamFO61g3OlfT7OQCHqhGnW43ieH9WZeP7QxN/G/jS4jfqUkZxoryvJgVPEcrl5NL/ggHsSmLMHuH64Lhg==", - "dev": true, - "engines": { - "node": ">=4", - "yarn": "*" + "peerDependencies": { + "browserslist": ">= 4.21.0" } }, "node_modules/uri-js": { @@ -14082,13 +14429,6 @@ "punycode": "^2.1.0" } }, - "node_modules/urix": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/urix/-/urix-0.1.0.tgz", - "integrity": "sha1-2pN/emLiH+wf0Y1Js1wpNQZ6bHI=", - "deprecated": "Please see https://github.com/lydell/urix#deprecated", - "dev": true - }, "node_modules/url": { "version": "0.11.0", "resolved": "https://registry.npmjs.org/url/-/url-0.11.0.tgz", @@ -14108,8 +14448,9 @@ "node_modules/url-parse-lax": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/url-parse-lax/-/url-parse-lax-3.0.0.tgz", - "integrity": "sha1-FrXK/Afb42dsGxmZF3gj1lA6yww=", + "integrity": "sha512-NjFKA0DidqPa5ciFcSrXnAltTtzz84ogy+NebPvfEgAck0+TNg4UJ4IN+fB7zRZfbgUf0syOo9MDxFkDSMuFaQ==", "dev": true, + "license": "MIT", "dependencies": { "prepend-http": "^2.0.0" }, @@ -14120,8 +14461,9 @@ "node_modules/url-to-options": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/url-to-options/-/url-to-options-1.0.1.tgz", - "integrity": "sha1-FQWgOiiaSMvXpDTvuu7FBV9WM6k=", + "integrity": "sha512-0kQLIzG4fdk/G5NONku64rSH/x32NOA39LVQqlK8Le6lvTF6GGRJpqaQFGgU+CLwySIqBSMdwYM0sYcW9f6P4A==", "dev": true, + "license": "MIT", "engines": { "node": ">= 4" } @@ -14132,15 +14474,6 @@ "integrity": "sha1-llOgNvt8HuQjQvIyXM7v6jkmxI0=", "dev": true }, - "node_modules/use": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/use/-/use-3.1.1.tgz", - "integrity": "sha512-cwESVXlO3url9YWlFW/TA9cshCEhtu7IKJ/p5soJ/gGpj7vbvFrAY/eIioQ6Dw23KjZhYgiIo8HOs1nQ2vr/oQ==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/util": { "version": "0.11.1", "resolved": "https://registry.npmjs.org/util/-/util-0.11.1.tgz", @@ -14163,34 +14496,31 @@ "dev": true }, "node_modules/uuid": { - "version": "8.3.2", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", - "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-14.0.0.tgz", + "integrity": "sha512-Qo+uWgilfSmAhXCMav1uYFynlQO7fMFiMVZsQqZRMIXp0O7rR7qjkj+cPvBHLgBqi960QCoo/PH2/6ZtVqKvrg==", "dev": true, + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], "bin": { - "uuid": "dist/bin/uuid" + "uuid": "dist-node/bin/uuid" } }, - "node_modules/v8-compile-cache": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/v8-compile-cache/-/v8-compile-cache-2.3.0.tgz", - "integrity": "sha512-l8lCEmLcLYZh4nbunNZvQCJc5pv7+RCwa8q/LdUx8u7lsWvPDKmpodJAJNwkAhJC//dFY48KuIEmjtd4RViDrA==", - "dev": true - }, "node_modules/v8-compile-cache-lib": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.0.tgz", "integrity": "sha512-mpSYqfsFvASnSn5qMiwrr4VKfumbPyONLCOPmsR3A6pTY/r0+tSaVbgPWSAIuzbk3lCTa+FForeTiO+wBQGkjA==", "dev": true }, - "node_modules/validate-npm-package-license": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", - "integrity": "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==", + "node_modules/v8flags": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/v8flags/-/v8flags-4.0.1.tgz", + "integrity": "sha512-fcRLaS4H/hrZk9hYwbdRM35D0U8IYMfEClhXxCivOojl+yTRAZH3Zy2sSy6qVCiGbV9YAtPssP6jaChqC9vPCg==", "dev": true, - "dependencies": { - "spdx-correct": "^3.0.0", - "spdx-expression-parse": "^3.0.0" + "engines": { + "node": ">= 10.13.0" } }, "node_modules/value-or-function": { @@ -14219,6 +14549,93 @@ "node": ">= 0.10" } }, + "node_modules/vinyl-contents": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/vinyl-contents/-/vinyl-contents-2.0.0.tgz", + "integrity": "sha512-cHq6NnGyi2pZ7xwdHSW1v4Jfnho4TEGtxZHw01cmnc8+i7jgR6bRnED/LbrKan/Q7CvVLbnvA5OepnhbpjBZ5Q==", + "dev": true, + "dependencies": { + "bl": "^5.0.0", + "vinyl": "^3.0.0" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/vinyl-contents/node_modules/bl": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-5.1.0.tgz", + "integrity": "sha512-tv1ZJHLfTDnXE6tMHv73YgSJaWR2AFuPwMntBe7XL/GBFHnT0CLnsHMogfk5+GzCDC5ZWarSCYaIGATZt9dNsQ==", + "dev": true, + "dependencies": { + "buffer": "^6.0.3", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, + "node_modules/vinyl-contents/node_modules/buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + }, + "node_modules/vinyl-contents/node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "dev": true, + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/vinyl-contents/node_modules/replace-ext": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/replace-ext/-/replace-ext-2.0.0.tgz", + "integrity": "sha512-UszKE5KVK6JvyD92nzMn9cDapSk6w/CaFZ96CnmDMUqH9oowfxF/ZjRITD25H4DnOQClLA4/j7jLGXXLVKxAug==", + "dev": true, + "engines": { + "node": ">= 10" + } + }, + "node_modules/vinyl-contents/node_modules/vinyl": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/vinyl/-/vinyl-3.0.0.tgz", + "integrity": "sha512-rC2VRfAVVCGEgjnxHUnpIVh3AGuk62rP3tqVrn+yab0YH7UULisC085+NYH+mnqf3Wx4SpSi1RQMwudL89N03g==", + "dev": true, + "dependencies": { + "clone": "^2.1.2", + "clone-stats": "^1.0.0", + "remove-trailing-separator": "^1.1.0", + "replace-ext": "^2.0.0", + "teex": "^1.0.1" + }, + "engines": { + "node": ">=10.13.0" + } + }, "node_modules/vinyl-fs": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/vinyl-fs/-/vinyl-fs-3.0.3.tgz", @@ -14283,26 +14700,6 @@ "integrity": "sha512-2ham8XPWTONajOR0ohOKOHXkm3+gaBmGut3SRuu75xLd/RRaY6vqgh8NBYYk7+RW3u5AtzPQZG8F10LHkl0lAQ==", "dev": true }, - "node_modules/vscode-debugadapter": { - "version": "1.35.0", - "resolved": "https://registry.npmjs.org/vscode-debugadapter/-/vscode-debugadapter-1.35.0.tgz", - "integrity": "sha512-Au90Iowj6TuD5uDMaTnxOjl/9hQN0Yoky1TV1Cjjr7jPdxTQpALBRW09Y2LzkIXUVICXlAqxWL9zL8BpzI30jg==", - "deprecated": "This package has been renamed to @vscode/debugadapter, please update to the new name", - "dependencies": { - "mkdirp": "^0.5.1", - "vscode-debugprotocol": "1.35.0" - } - }, - "node_modules/vscode-debugadapter-testsupport": { - "version": "1.35.0", - "resolved": "https://registry.npmjs.org/vscode-debugadapter-testsupport/-/vscode-debugadapter-testsupport-1.35.0.tgz", - "integrity": "sha512-4emLt6JOk4iKqC2aWNJupOtrK6JwYAZ6KppqvKASN6B1s063VoqI18QhUB1CeoKwNaN1LIG3VPv2xM8HKOjyDA==", - "deprecated": "This package has been renamed to @vscode/debugadapter-testsupport, please update to the new name", - "dev": true, - "dependencies": { - "vscode-debugprotocol": "1.35.0" - } - }, "node_modules/vscode-debugprotocol": { "version": "1.35.0", "resolved": "https://registry.npmjs.org/vscode-debugprotocol/-/vscode-debugprotocol-1.35.0.tgz", @@ -14310,96 +14707,78 @@ "deprecated": "This package has been renamed to @vscode/debugprotocol, please update to the new name" }, "node_modules/vscode-jsonrpc": { - "version": "8.0.2-next.1", - "resolved": "https://registry.npmjs.org/vscode-jsonrpc/-/vscode-jsonrpc-8.0.2-next.1.tgz", - "integrity": "sha512-sbbvGSWja7NVBLHPGawtgezc8DHYJaP4qfr/AaJiyDapWcSFtHyPtm18+LnYMLTmB7bhOUW/lf5PeeuLpP6bKA==", + "version": "9.0.0-next.5", + "resolved": "https://registry.npmjs.org/vscode-jsonrpc/-/vscode-jsonrpc-9.0.0-next.5.tgz", + "integrity": "sha512-Sl/8RAJtfF/2x/TPBVRuhzRAcqYR/QDjEjNqMcoKFfqsxfVUPzikupRDQYB77Gkbt1RrW43sSuZ5uLtNAcikQQ==", "engines": { "node": ">=14.0.0" } }, "node_modules/vscode-languageclient": { - "version": "8.0.2-next.5", - "resolved": "https://registry.npmjs.org/vscode-languageclient/-/vscode-languageclient-8.0.2-next.5.tgz", - "integrity": "sha512-g87RJLHz0XlRyk6DOTbAk4JHcj8CKggXy4JiFL7OlhETkcYzTOR8d+Qdb4GqZr37PDs1Cl21omtTNK5LyR/RQg==", + "version": "10.0.0-next.12", + "resolved": "https://registry.npmjs.org/vscode-languageclient/-/vscode-languageclient-10.0.0-next.12.tgz", + "integrity": "sha512-q7cVYCcYiv+a+fJYCbjMMScOGBnX162IBeUMFg31mvnN7RHKx5/CwKaCz+r+RciJrRXMqS8y8qpEVGgeIPnbxg==", "dependencies": { - "minimatch": "^3.0.4", - "semver": "^7.3.5", - "vscode-languageserver-protocol": "3.17.2-next.6" + "minimatch": "^9.0.3", + "semver": "^7.6.0", + "vscode-languageserver-protocol": "3.17.6-next.10" }, "engines": { - "vscode": "^1.67.0" + "vscode": "^1.91.0" } }, - "node_modules/vscode-languageclient/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "node_modules/vscode-languageclient/node_modules/brace-expansion": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.3.tgz", + "integrity": "sha512-MCV/fYJEbqx68aE58kv2cA/kiky1G8vux3OR6/jbS+jIMe/6fJWa0DTzJU7dqijOWYwHi1t29FlfYI9uytqlpA==", "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" + "balanced-match": "^1.0.0" } }, - "node_modules/vscode-languageclient/node_modules/semver": { - "version": "7.3.7", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.7.tgz", - "integrity": "sha512-QlYTucUYOews+WeEujDoEGziz4K6c47V/Bd+LjSSYcA94p+DmINdf7ncaUinThfvZyu13lN9OY1XDxt8C0Tw0g==", + "node_modules/vscode-languageclient/node_modules/minimatch": { + "version": "9.0.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", + "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", + "license": "ISC", "dependencies": { - "lru-cache": "^6.0.0" - }, - "bin": { - "semver": "bin/semver.js" + "brace-expansion": "^2.0.2" }, "engines": { - "node": ">=10" - } - }, - "node_modules/vscode-languageserver": { - "version": "8.0.2-next.5", - "resolved": "https://registry.npmjs.org/vscode-languageserver/-/vscode-languageserver-8.0.2-next.5.tgz", - "integrity": "sha512-2ZDb7O/4atS9mJKufPPz637z+51kCyZfgnobFW5eSrUdS3c0UB/nMS4Ng1EavYTX84GVaVMKCrmP0f2ceLmR0A==", - "dependencies": { - "vscode-languageserver-protocol": "3.17.2-next.6" + "node": ">=16 || 14 >=14.17" }, - "bin": { - "installServerIntoExtension": "bin/installServerIntoExtension" + "funding": { + "url": "https://github.com/sponsors/isaacs" } }, "node_modules/vscode-languageserver-protocol": { - "version": "3.17.2-next.6", - "resolved": "https://registry.npmjs.org/vscode-languageserver-protocol/-/vscode-languageserver-protocol-3.17.2-next.6.tgz", - "integrity": "sha512-WtsebNOOkWyNn4oFYoAMPC8Q/ZDoJ/K7Ja53OzTixiitvrl/RpXZETrtzH79R8P5kqCyx6VFBPb6KQILJfkDkA==", + "version": "3.17.6-next.10", + "resolved": "https://registry.npmjs.org/vscode-languageserver-protocol/-/vscode-languageserver-protocol-3.17.6-next.10.tgz", + "integrity": "sha512-KOrrWn4NVC5jnFC5N6y/fyNKtx8rVYr67lhL/Z0P4ZBAN27aBsCnLBWAMIkYyJ1K8EZaE5r7gqdxrS9JPB6LIg==", "dependencies": { - "vscode-jsonrpc": "8.0.2-next.1", - "vscode-languageserver-types": "3.17.2-next.2" + "vscode-jsonrpc": "9.0.0-next.5", + "vscode-languageserver-types": "3.17.6-next.5" } }, - "node_modules/vscode-languageserver-protocol/node_modules/vscode-languageserver-types": { - "version": "3.17.2-next.2", - "resolved": "https://registry.npmjs.org/vscode-languageserver-types/-/vscode-languageserver-types-3.17.2-next.2.tgz", - "integrity": "sha512-TiAkLABgqkVWdAlC3XlOfdhdjIAdVU4YntPUm9kKGbXr+MGwpVnKz2KZMNBcvG0CFx8Hi8qliL0iq+ndPB720w==" + "node_modules/vscode-languageserver-types": { + "version": "3.17.6-next.5", + "resolved": "https://registry.npmjs.org/vscode-languageserver-types/-/vscode-languageserver-types-3.17.6-next.5.tgz", + "integrity": "sha512-QFmf3Yl1tCgUQfA77N9Me/LXldJXkIVypQbty2rJ1DNHQkC+iwvm4Z2tXg9czSwlhvv0pD4pbF5mT7WhAglolw==" }, "node_modules/vscode-tas-client": { - "version": "0.1.63", - "resolved": "https://registry.npmjs.org/vscode-tas-client/-/vscode-tas-client-0.1.63.tgz", - "integrity": "sha512-TY5TPyibzi6rNmuUB7eRVqpzLzNfQYrrIl/0/F8ukrrbzOrKVvS31hM3urE+tbaVrnT+TMYXL16GhX57vEowhA==", + "version": "0.1.84", + "resolved": "https://registry.npmjs.org/vscode-tas-client/-/vscode-tas-client-0.1.84.tgz", + "integrity": "sha512-rUTrUopV+70hvx1hW5ebdw1nd6djxubkLvVxjGdyD/r5v/wcVF41LIfiAtbm5qLZDtQdsMH1IaCuDoluoIa88w==", "dependencies": { - "tas-client": "0.1.58" + "tas-client": "0.2.33" }, "engines": { - "vscode": "^1.19.1" + "vscode": "^1.85.0" } }, - "node_modules/vscode-uri": { - "version": "3.0.6", - "resolved": "https://registry.npmjs.org/vscode-uri/-/vscode-uri-3.0.6.tgz", - "integrity": "sha512-fmL7V1eiDBFRRnu+gfRWTzyPpNIHJTc4mWnFkwBUmO9U3KPgJAmTx7oxi2bl/Rh6HLdU7+4C9wlj0k2E4AdKFQ==" - }, "node_modules/watchpack": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.0.tgz", - "integrity": "sha512-Lcvm7MGST/4fup+ifyKi2hjyIAwcdI4HRgtvTpIUxBRhB+RFtUh8XtDOxUfctVCnhVi+QQj49i91OyvzkJl6cg==", + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.5.1.tgz", + "integrity": "sha512-Zn5uXdcFNIA1+1Ei5McRd+iRzfhENPCe7LeABkJtNulSxjma+l7ltNx55BWZkRlwRnpOgHqxnjyaDgJnNXnqzg==", "dev": true, "dependencies": { "glob-to-regexp": "^0.4.1", @@ -14410,35 +14789,36 @@ } }, "node_modules/webpack": { - "version": "5.76.0", - "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.76.0.tgz", - "integrity": "sha512-l5sOdYBDunyf72HW8dF23rFtWq/7Zgvt/9ftMof71E/yUb1YLOBmTgA2K4vQthB3kotMrSj609txVE0dnr2fjA==", - "dev": true, - "dependencies": { - "@types/eslint-scope": "^3.7.3", - "@types/estree": "^0.0.51", - "@webassemblyjs/ast": "1.11.1", - "@webassemblyjs/wasm-edit": "1.11.1", - "@webassemblyjs/wasm-parser": "1.11.1", - "acorn": "^8.7.1", - "acorn-import-assertions": "^1.7.6", - "browserslist": "^4.14.5", + "version": "5.105.0", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.105.0.tgz", + "integrity": "sha512-gX/dMkRQc7QOMzgTe6KsYFM7DxeIONQSui1s0n/0xht36HvrgbxtM1xBlgx596NbpHuQU8P7QpKwrZYwUX48nw==", + "dev": true, + "dependencies": { + "@types/eslint-scope": "^3.7.7", + "@types/estree": "^1.0.8", + "@types/json-schema": "^7.0.15", + "@webassemblyjs/ast": "^1.14.1", + "@webassemblyjs/wasm-edit": "^1.14.1", + "@webassemblyjs/wasm-parser": "^1.14.1", + "acorn": "^8.15.0", + "acorn-import-phases": "^1.0.3", + "browserslist": "^4.28.1", "chrome-trace-event": "^1.0.2", - "enhanced-resolve": "^5.10.0", - "es-module-lexer": "^0.9.0", + "enhanced-resolve": "^5.19.0", + "es-module-lexer": "^2.0.0", "eslint-scope": "5.1.1", "events": "^3.2.0", "glob-to-regexp": "^0.4.1", - "graceful-fs": "^4.2.9", + "graceful-fs": "^4.2.11", "json-parse-even-better-errors": "^2.3.1", - "loader-runner": "^4.2.0", + "loader-runner": "^4.3.1", "mime-types": "^2.1.27", "neo-async": "^2.6.2", - "schema-utils": "^3.1.0", - "tapable": "^2.1.1", - "terser-webpack-plugin": "^5.1.3", - "watchpack": "^2.4.0", - "webpack-sources": "^3.2.3" + "schema-utils": "^4.3.3", + "tapable": "^2.3.0", + "terser-webpack-plugin": "^5.3.16", + "watchpack": "^2.5.1", + "webpack-sources": "^3.3.3" }, "bin": { "webpack": "bin/webpack.js" @@ -14669,14 +15049,68 @@ } }, "node_modules/webpack-sources": { - "version": "3.2.3", - "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.2.3.tgz", - "integrity": "sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w==", + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.3.3.tgz", + "integrity": "sha512-yd1RBzSGanHkitROoPFd6qsrxt+oFhg/129YzheDGqeustzX0vTZJZsSsQjVQC4yzBQ56K55XU8gaNCtIzOnTg==", "dev": true, "engines": { "node": ">=10.13.0" } }, + "node_modules/webpack/node_modules/ajv": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz", + "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/webpack/node_modules/ajv-keywords": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz", + "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==", + "dev": true, + "dependencies": { + "fast-deep-equal": "^3.1.3" + }, + "peerDependencies": { + "ajv": "^8.8.2" + } + }, + "node_modules/webpack/node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true + }, + "node_modules/webpack/node_modules/schema-utils": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.3.tgz", + "integrity": "sha512-eflK8wEtyOE6+hsaRVPxvUKYCpRgzLqDTb8krvAsRIwOGlHoSgYLgBXoubGgLd2fT41/OUYdb48v4k4WWHQurA==", + "dev": true, + "dependencies": { + "@types/json-schema": "^7.0.9", + "ajv": "^8.9.0", + "ajv-formats": "^2.1.1", + "ajv-keywords": "^5.1.0" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", @@ -14707,51 +15141,20 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/which-boxed-primitive/node_modules/is-boolean-object": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.1.1.tgz", - "integrity": "sha512-bXdQWkECBUIAcCkeH1unwJLIpZYaa5VvuygSyS/c2lf719mTKZDU5UdDRlpd01UjADgmW8RfqaP+mRaVPdr/Ng==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/which-boxed-primitive/node_modules/is-number-object": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.0.5.tgz", - "integrity": "sha512-RU0lI/n95pMoUKu9v1BZP5MBcZuNSVJkMkAG2dJqC4z2GlkGUNeH68SuHuBKBD/XFe+LHZ+f9BKkLET60Niedw==", - "dev": true, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/which-module": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/which-module/-/which-module-1.0.0.tgz", - "integrity": "sha1-u6Y8qGGUiZT/MHc2CJ47lgJsKk8=", - "dev": true - }, "node_modules/which-typed-array": { - "version": "1.1.7", - "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.7.tgz", - "integrity": "sha512-vjxaB4nfDqwKI0ws7wZpxIlde1XrLX5uB0ZjpfshgmapJMD7jJWhZI+yToJTqaFByF0eNBcYxbjmCzoRP7CfEw==", + "version": "1.1.19", + "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.19.tgz", + "integrity": "sha512-rEvr90Bck4WZt9HHFC4DJMsjvu7x+r6bImz0/BrbWb7A2djJ8hnZMrWnHo9F8ssv0OMErasDhftrfROTyqSDrw==", "dev": true, + "license": "MIT", "dependencies": { - "available-typed-arrays": "^1.0.5", - "call-bind": "^1.0.2", - "es-abstract": "^1.18.5", - "foreach": "^2.0.5", - "has-tostringtag": "^1.0.0", - "is-typed-array": "^1.1.7" + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "for-each": "^0.3.5", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2" }, "engines": { "node": ">= 0.4" @@ -14760,16 +15163,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/wide-align": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.3.tgz", - "integrity": "sha512-QGkOQc8XL6Bt5PwnsExKBPuMKBxnGxWWW3fU55Xt4feHozMUhdUMaBCk290qpm/wG5u/RSKzwdAC4i51YigihA==", - "dev": true, - "optional": true, - "dependencies": { - "string-width": "^1.0.2 || 2" - } - }, "node_modules/wildcard": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/wildcard/-/wildcard-2.0.0.tgz", @@ -14796,55 +15189,138 @@ "wipe-node-cache": "^2.1.0" } }, - "node_modules/word-wrap": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz", - "integrity": "sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==", + "node_modules/worker-loader": { + "version": "3.0.8", + "resolved": "https://registry.npmjs.org/worker-loader/-/worker-loader-3.0.8.tgz", + "integrity": "sha512-XQyQkIFeRVC7f7uRhFdNMe/iJOdO6zxAaR3EWbDp45v3mDhrTi+++oswKNxShUNjPC/1xUp5DB29YKLhFo129g==", + "dev": true, + "dependencies": { + "loader-utils": "^2.0.0", + "schema-utils": "^3.0.0" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^4.0.0 || ^5.0.0" + } + }, + "node_modules/workerpool": { + "version": "9.3.4", + "resolved": "https://registry.npmjs.org/workerpool/-/workerpool-9.3.4.tgz", + "integrity": "sha512-TmPRQYYSAnnDiEB0P/Ytip7bFGvqnSU6I2BcuSw7Hx+JSg/DsUi5ebYfc8GYaSdpuvOcEs6dXxPurOYpe9QFwg==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, "engines": { - "node": ">=0.10.0" + "node": ">=7.0.0" } }, - "node_modules/workerpool": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/workerpool/-/workerpool-6.2.0.tgz", - "integrity": "sha512-Rsk5qQHJ9eowMH28Jwhe8HEbmdYDX4lwoMWshiCXugjtHqMD9ZbiqSDLxcsfdqsETPzVUtX5s1Z5kStiIM6l4A==", - "dev": true + "node_modules/wrap-ansi-cjs/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" }, - "node_modules/wrap-ansi": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-2.1.0.tgz", - "integrity": "sha1-2Pw9KE3QV5T+hJc8rs3Rz4JP3YU=", + "node_modules/wrap-ansi/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", "dev": true, "dependencies": { - "string-width": "^1.0.1", - "strip-ansi": "^3.0.1" + "color-convert": "^2.0.1" }, "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/wrap-ansi/node_modules/ansi-regex": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", - "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", - "dev": true, - "engines": { - "node": ">=0.10.0" + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "node_modules/wrap-ansi/node_modules/strip-ansi": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", - "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", + "node_modules/wrap-ansi/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", "dev": true, "dependencies": { - "ansi-regex": "^2.0.0" + "color-name": "~1.1.4" }, "engines": { - "node": ">=0.10.0" + "node": ">=7.0.0" } }, + "node_modules/wrap-ansi/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, "node_modules/wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", @@ -14863,9 +15339,9 @@ } }, "node_modules/ws": { - "version": "7.5.7", - "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.7.tgz", - "integrity": "sha512-KMvVuFzpKBuiIXW3E4u3mySRO2/mCHSyZDJQM5NQ9Q9KHWHWh0NHgfbRMLLrceUK5qAL4ytALJbpRMjixFZh8A==", + "version": "7.5.10", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.10.tgz", + "integrity": "sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ==", "dev": true, "engines": { "node": ">=8.3.0" @@ -15053,44 +15529,12 @@ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "dev": true }, - "node_modules/yargs/node_modules/get-caller-file": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", - "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", - "dev": true, - "engines": { - "node": "6.* || 8.* || >= 10.*" - } - }, - "node_modules/yargs/node_modules/is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "dev": true, - "engines": { - "node": ">=8" - } - }, "node_modules/yargs/node_modules/require-main-filename": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz", "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==", "dev": true }, - "node_modules/yargs/node_modules/string-width": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.0.tgz", - "integrity": "sha512-zUz5JD+tgqtuDjMhwIg5uFVV3dtqZ9yQJlZVfq4I01/K5Paj5UHj7VyrQOJvzawSVlKpObApbfD0Ed6yJc+1eg==", - "dev": true, - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.0" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/yargs/node_modules/which-module": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.0.tgz", @@ -15166,6 +15610,22 @@ } }, "dependencies": { + "@aashutoshrathi/word-wrap": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@aashutoshrathi/word-wrap/-/word-wrap-1.2.6.tgz", + "integrity": "sha512-1Yjs2SvM8TflER/OD3cOjhWWOZb58A2t7wpE2S9XfBYTiIl+XFhQG2bjy4Pu1I+EAlCNUzRDYDdFwFYUKvXcIA==", + "dev": true + }, + "@ampproject/remapping": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.2.1.tgz", + "integrity": "sha512-lFMjJTrFL3j7L9yBxwYfCq2k6qqwHyzuUl/XBnif78PWTJYyL/dfowQHWE3sp6U6ZzqWiiIZnpTMO96zhkjwtg==", + "dev": true, + "requires": { + "@jridgewell/gen-mapping": "^0.3.0", + "@jridgewell/trace-mapping": "^0.3.9" + } + }, "@azure/abort-controller": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@azure/abort-controller/-/abort-controller-1.1.0.tgz", @@ -15175,42 +15635,86 @@ }, "dependencies": { "tslib": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.5.0.tgz", - "integrity": "sha512-336iVw3rtn2BUK7ORdIAHTyxHGRIHVReokCR3XjbckJMK7ms8FysBfhLR8IXnAgy7T0PTPNBWKiH514FOW/WSg==" + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" } } }, "@azure/core-auth": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/@azure/core-auth/-/core-auth-1.4.0.tgz", - "integrity": "sha512-HFrcTgmuSuukRf/EdPmqBrc5l6Q5Uu+2TbuhaKbgaCpP2TfAeiNaQPAadxO+CYBRHGUzIDteMAjFspFLDLnKVQ==", + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@azure/core-auth/-/core-auth-1.5.0.tgz", + "integrity": "sha512-udzoBuYG1VBoHVohDTrvKjyzel34zt77Bhp7dQntVGGD0ehVq48owENbBG8fIgkHRNUBQH5k1r0hpoMu5L8+kw==", "requires": { "@azure/abort-controller": "^1.0.0", + "@azure/core-util": "^1.1.0", "tslib": "^2.2.0" }, "dependencies": { "tslib": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.5.0.tgz", - "integrity": "sha512-336iVw3rtn2BUK7ORdIAHTyxHGRIHVReokCR3XjbckJMK7ms8FysBfhLR8IXnAgy7T0PTPNBWKiH514FOW/WSg==" + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" + } + } + }, + "@azure/core-client": { + "version": "1.9.2", + "resolved": "https://registry.npmjs.org/@azure/core-client/-/core-client-1.9.2.tgz", + "integrity": "sha512-kRdry/rav3fUKHl/aDLd/pDLcB+4pOFwPPTVEExuMyaI5r+JBbMWqRbCY1pn5BniDaU3lRxO9eaQ1AmSMehl/w==", + "dev": true, + "requires": { + "@azure/abort-controller": "^2.0.0", + "@azure/core-auth": "^1.4.0", + "@azure/core-rest-pipeline": "^1.9.1", + "@azure/core-tracing": "^1.0.0", + "@azure/core-util": "^1.6.1", + "@azure/logger": "^1.0.0", + "tslib": "^2.6.2" + }, + "dependencies": { + "@azure/abort-controller": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/@azure/abort-controller/-/abort-controller-2.1.2.tgz", + "integrity": "sha512-nBrLsEWm4J2u5LpAPjxADTlq3trDgVZZXHNKabeXZtpq3d3AbN/KGO82R87rdDz5/lYB024rtEf10/q0urNgsA==", + "dev": true, + "requires": { + "tslib": "^2.6.2" + } + }, + "@azure/core-util": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@azure/core-util/-/core-util-1.9.0.tgz", + "integrity": "sha512-AfalUQ1ZppaKuxPPMsFEUdX6GZPB3d9paR9d/TTL7Ow2De8cJaC7ibi7kWVlFAVPCYo31OcnGymc0R89DX8Oaw==", + "dev": true, + "requires": { + "@azure/abort-controller": "^2.0.0", + "tslib": "^2.6.2" + } + }, + "tslib": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==", + "dev": true } } }, "@azure/core-rest-pipeline": { - "version": "1.10.3", - "resolved": "https://registry.npmjs.org/@azure/core-rest-pipeline/-/core-rest-pipeline-1.10.3.tgz", - "integrity": "sha512-AMQb0ttiGJ0MIV/r+4TVra6U4+90mPeOveehFnrqKlo7dknPJYdJ61wOzYJXJjDxF8LcCtSogfRelkq+fCGFTw==", + "version": "1.10.1", + "resolved": "https://registry.npmjs.org/@azure/core-rest-pipeline/-/core-rest-pipeline-1.10.1.tgz", + "integrity": "sha512-Kji9k6TOFRDB5ZMTw8qUf2IJ+CeJtsuMdAHox9eqpTf1cefiNMpzrfnF6sINEBZJsaVaWgQ0o48B6kcUH68niA==", "requires": { "@azure/abort-controller": "^1.0.0", "@azure/core-auth": "^1.4.0", "@azure/core-tracing": "^1.0.1", - "@azure/core-util": "^1.3.0", + "@azure/core-util": "^1.0.0", "@azure/logger": "^1.0.0", "form-data": "^4.0.0", "http-proxy-agent": "^5.0.0", "https-proxy-agent": "^5.0.0", - "tslib": "^2.2.0" + "tslib": "^2.2.0", + "uuid": "^8.3.0" }, "dependencies": { "@tootallnate/once": { @@ -15237,9 +15741,14 @@ } }, "tslib": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.5.0.tgz", - "integrity": "sha512-336iVw3rtn2BUK7ORdIAHTyxHGRIHVReokCR3XjbckJMK7ms8FysBfhLR8IXnAgy7T0PTPNBWKiH514FOW/WSg==" + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" + }, + "uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==" } } }, @@ -15252,25 +15761,76 @@ }, "dependencies": { "tslib": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.5.0.tgz", - "integrity": "sha512-336iVw3rtn2BUK7ORdIAHTyxHGRIHVReokCR3XjbckJMK7ms8FysBfhLR8IXnAgy7T0PTPNBWKiH514FOW/WSg==" + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" } } }, "@azure/core-util": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/@azure/core-util/-/core-util-1.3.0.tgz", - "integrity": "sha512-ANP0Er7R2KHHHjwmKzPF9wbd0gXvOX7yRRHeYL1eNd/OaNrMLyfZH/FQasHRVAf6rMXX+EAUpvYwLMFDHDI5Gw==", + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@azure/core-util/-/core-util-1.2.0.tgz", + "integrity": "sha512-ffGIw+Qs8bNKNLxz5UPkz4/VBM/EZY07mPve1ZYFqYUdPwFqRj0RPk0U7LZMOfT7GCck9YjuT1Rfp1PApNl1ng==", + "requires": { + "@azure/abort-controller": "^1.0.0", + "tslib": "^2.2.0" + }, + "dependencies": { + "tslib": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" + } + } + }, + "@azure/identity": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@azure/identity/-/identity-4.2.1.tgz", + "integrity": "sha512-U8hsyC9YPcEIzoaObJlRDvp7KiF0MGS7xcWbyJSVvXRkC/HXo1f0oYeBYmEvVgRfacw7GHf6D6yAoh9JHz6A5Q==", + "dev": true, "requires": { "@azure/abort-controller": "^1.0.0", + "@azure/core-auth": "^1.5.0", + "@azure/core-client": "^1.4.0", + "@azure/core-rest-pipeline": "^1.1.0", + "@azure/core-tracing": "^1.0.0", + "@azure/core-util": "^1.3.0", + "@azure/logger": "^1.0.0", + "@azure/msal-browser": "^3.11.1", + "@azure/msal-node": "^2.9.2", + "events": "^3.0.0", + "jws": "^4.0.0", + "open": "^8.0.0", + "stoppable": "^1.1.0", "tslib": "^2.2.0" }, "dependencies": { + "@azure/core-util": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@azure/core-util/-/core-util-1.9.0.tgz", + "integrity": "sha512-AfalUQ1ZppaKuxPPMsFEUdX6GZPB3d9paR9d/TTL7Ow2De8cJaC7ibi7kWVlFAVPCYo31OcnGymc0R89DX8Oaw==", + "dev": true, + "requires": { + "@azure/abort-controller": "^2.0.0", + "tslib": "^2.6.2" + }, + "dependencies": { + "@azure/abort-controller": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/@azure/abort-controller/-/abort-controller-2.1.2.tgz", + "integrity": "sha512-nBrLsEWm4J2u5LpAPjxADTlq3trDgVZZXHNKabeXZtpq3d3AbN/KGO82R87rdDz5/lYB024rtEf10/q0urNgsA==", + "dev": true, + "requires": { + "tslib": "^2.6.2" + } + } + } + }, "tslib": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.5.0.tgz", - "integrity": "sha512-336iVw3rtn2BUK7ORdIAHTyxHGRIHVReokCR3XjbckJMK7ms8FysBfhLR8IXnAgy7T0PTPNBWKiH514FOW/WSg==" + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==", + "dev": true } } }, @@ -15283,55 +15843,333 @@ }, "dependencies": { "tslib": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.5.0.tgz", - "integrity": "sha512-336iVw3rtn2BUK7ORdIAHTyxHGRIHVReokCR3XjbckJMK7ms8FysBfhLR8IXnAgy7T0PTPNBWKiH514FOW/WSg==" + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" + } + } + }, + "@azure/msal-browser": { + "version": "3.14.0", + "resolved": "https://registry.npmjs.org/@azure/msal-browser/-/msal-browser-3.14.0.tgz", + "integrity": "sha512-Un85LhOoecJ3HDTS3Uv3UWnXC9/43ZSO+Kc+anSqpZvcEt58SiO/3DuVCAe1A3I5UIBYJNMgTmZPGXQ0MVYrwA==", + "dev": true, + "requires": { + "@azure/msal-common": "14.10.0" + } + }, + "@azure/msal-common": { + "version": "14.10.0", + "resolved": "https://registry.npmjs.org/@azure/msal-common/-/msal-common-14.10.0.tgz", + "integrity": "sha512-Zk6DPDz7e1wPgLoLgAp0349Yay9RvcjPM5We/ehuenDNsz/t9QEFI7tRoHpp/e47I4p20XE3FiDlhKwAo3utDA==", + "dev": true + }, + "@azure/msal-node": { + "version": "2.9.2", + "resolved": "https://registry.npmjs.org/@azure/msal-node/-/msal-node-2.9.2.tgz", + "integrity": "sha512-8tvi6Cos3m+0KmRbPjgkySXi+UQU/QiuVRFnrxIwt5xZlEEFa69O04RTaNESGgImyBBlYbo2mfE8/U8Bbdk1WQ==", + "dev": true, + "requires": { + "@azure/msal-common": "14.12.0", + "jsonwebtoken": "^9.0.0", + "uuid": "^8.3.0" + }, + "dependencies": { + "@azure/msal-common": { + "version": "14.12.0", + "resolved": "https://registry.npmjs.org/@azure/msal-common/-/msal-common-14.12.0.tgz", + "integrity": "sha512-IDDXmzfdwmDkv4SSmMEyAniJf6fDu3FJ7ncOjlxkDuT85uSnLEhZi3fGZpoR7T4XZpOMx9teM9GXBgrfJgyeBw==", + "dev": true + }, + "uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "dev": true + } + } + }, + "@azure/opentelemetry-instrumentation-azure-sdk": { + "version": "1.0.0-beta.5", + "resolved": "https://registry.npmjs.org/@azure/opentelemetry-instrumentation-azure-sdk/-/opentelemetry-instrumentation-azure-sdk-1.0.0-beta.5.tgz", + "integrity": "sha512-fsUarKQDvjhmBO4nIfaZkfNSApm1hZBzcvpNbSrXdcUBxu7lRvKsV5DnwszX7cnhLyVOW9yl1uigtRQ1yDANjA==", + "requires": { + "@azure/core-tracing": "^1.0.0", + "@azure/logger": "^1.0.0", + "@opentelemetry/api": "^1.4.1", + "@opentelemetry/core": "^1.15.2", + "@opentelemetry/instrumentation": "^0.41.2", + "tslib": "^2.2.0" + }, + "dependencies": { + "tslib": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" } } }, "@babel/code-frame": { - "version": "7.12.11", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.12.11.tgz", - "integrity": "sha512-Zt1yodBx1UcyiePMSkWnU4hPqhwq7hGi2nFL1LeA3EUl+q2LQx16MISgJ0+z7dnmgvP9QtIleuETGOiOH1RcIw==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", + "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==", + "dev": true, + "requires": { + "@babel/helper-validator-identifier": "^7.27.1", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + } + }, + "@babel/compat-data": { + "version": "7.22.6", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.22.6.tgz", + "integrity": "sha512-29tfsWTq2Ftu7MXmimyC0C5FDZv5DYxOZkh3XD3+QW4V/BYuv/LyEsjj3c0hqedEaDt6DBfDvexMKU8YevdqFg==", + "dev": true + }, + "@babel/core": { + "version": "7.22.6", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.22.6.tgz", + "integrity": "sha512-HPIyDa6n+HKw5dEuway3vVAhBboYCtREBMp+IWeseZy6TFtzn6MHkCH2KKYUOC/vKKwgSMHQW4htBOrmuRPXfw==", + "dev": true, + "requires": { + "@ampproject/remapping": "^2.2.0", + "@babel/code-frame": "^7.22.5", + "@babel/generator": "^7.22.5", + "@babel/helper-compilation-targets": "^7.22.6", + "@babel/helper-module-transforms": "^7.22.5", + "@babel/helpers": "^7.22.6", + "@babel/parser": "^7.22.6", + "@babel/template": "^7.22.5", + "@babel/traverse": "^7.22.6", + "@babel/types": "^7.22.5", + "@nicolo-ribaudo/semver-v6": "^6.3.3", + "convert-source-map": "^1.7.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.2" + }, + "dependencies": { + "debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dev": true, + "requires": { + "ms": "2.1.2" + } + } + } + }, + "@babel/generator": { + "version": "7.23.0", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.23.0.tgz", + "integrity": "sha512-lN85QRR+5IbYrMWM6Y4pE/noaQtg4pNiqeNGX60eqOfo6gtEj6uw/JagelB8vVztSd7R6M5n1+PQkDbHbBRU4g==", + "dev": true, + "requires": { + "@babel/types": "^7.23.0", + "@jridgewell/gen-mapping": "^0.3.2", + "@jridgewell/trace-mapping": "^0.3.17", + "jsesc": "^2.5.1" + } + }, + "@babel/helper-compilation-targets": { + "version": "7.22.6", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.22.6.tgz", + "integrity": "sha512-534sYEqWD9VfUm3IPn2SLcH4Q3P86XL+QvqdC7ZsFrzyyPF3T4XGiVghF6PTYNdWg6pXuoqXxNQAhbYeEInTzA==", + "dev": true, + "requires": { + "@babel/compat-data": "^7.22.6", + "@babel/helper-validator-option": "^7.22.5", + "@nicolo-ribaudo/semver-v6": "^6.3.3", + "browserslist": "^4.21.9", + "lru-cache": "^5.1.1" + }, + "dependencies": { + "lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "requires": { + "yallist": "^3.0.2" + } + }, + "yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true + } + } + }, + "@babel/helper-environment-visitor": { + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.22.20.tgz", + "integrity": "sha512-zfedSIzFhat/gFhWfHtgWvlec0nqB9YEIVrpuwjruLlXfUSnA8cJB0miHKwqDnQ7d32aKo2xt88/xZptwxbfhA==", + "dev": true + }, + "@babel/helper-function-name": { + "version": "7.23.0", + "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.23.0.tgz", + "integrity": "sha512-OErEqsrxjZTJciZ4Oo+eoZqeW9UIiOcuYKRJA4ZAgV9myA+pOXhhmpfNCKjEH/auVfEYVFJ6y1Tc4r0eIApqiw==", + "dev": true, + "requires": { + "@babel/template": "^7.22.15", + "@babel/types": "^7.23.0" + } + }, + "@babel/helper-hoist-variables": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.22.5.tgz", + "integrity": "sha512-wGjk9QZVzvknA6yKIUURb8zY3grXCcOZt+/7Wcy8O2uctxhplmUPkOdlgoNhmdVee2c92JXbf1xpMtVNbfoxRw==", + "dev": true, + "requires": { + "@babel/types": "^7.22.5" + } + }, + "@babel/helper-module-imports": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.22.5.tgz", + "integrity": "sha512-8Dl6+HD/cKifutF5qGd/8ZJi84QeAKh+CEe1sBzz8UayBBGg1dAIJrdHOcOM5b2MpzWL2yuotJTtGjETq0qjXg==", + "dev": true, + "requires": { + "@babel/types": "^7.22.5" + } + }, + "@babel/helper-module-transforms": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.22.5.tgz", + "integrity": "sha512-+hGKDt/Ze8GFExiVHno/2dvG5IdstpzCq0y4Qc9OJ25D4q3pKfiIP/4Vp3/JvhDkLKsDK2api3q3fpIgiIF5bw==", + "dev": true, + "requires": { + "@babel/helper-environment-visitor": "^7.22.5", + "@babel/helper-module-imports": "^7.22.5", + "@babel/helper-simple-access": "^7.22.5", + "@babel/helper-split-export-declaration": "^7.22.5", + "@babel/helper-validator-identifier": "^7.22.5", + "@babel/template": "^7.22.5", + "@babel/traverse": "^7.22.5", + "@babel/types": "^7.22.5" + } + }, + "@babel/helper-simple-access": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.22.5.tgz", + "integrity": "sha512-n0H99E/K+Bika3++WNL17POvo4rKWZ7lZEp1Q+fStVbUi8nxPQEBOlTmCOxW/0JsS56SKKQ+ojAe2pHKJHN35w==", + "dev": true, + "requires": { + "@babel/types": "^7.22.5" + } + }, + "@babel/helper-split-export-declaration": { + "version": "7.22.6", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.22.6.tgz", + "integrity": "sha512-AsUnxuLhRYsisFiaJwvp1QF+I3KjD5FOxut14q/GzovUe6orHLesW2C7d754kRm53h5gqrz6sFl6sxc4BVtE/g==", "dev": true, "requires": { - "@babel/highlight": "^7.10.4" + "@babel/types": "^7.22.5" } }, + "@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "dev": true + }, "@babel/helper-validator-identifier": { - "version": "7.15.7", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.15.7.tgz", - "integrity": "sha512-K4JvCtQqad9OY2+yTU8w+E82ywk/fe+ELNlt1G8z3bVGlZfn/hOcQQsUhGhW/N+tb3fxK800wLtKOE/aM0m72w==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.27.1.tgz", + "integrity": "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==", + "dev": true + }, + "@babel/helper-validator-option": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.22.5.tgz", + "integrity": "sha512-R3oB6xlIVKUnxNUxbmgq7pKjxpru24zlimpE8WK47fACIlM0II/Hm1RS8IaOI7NgCr6LNS+jl5l75m20npAziw==", "dev": true }, - "@babel/highlight": { - "version": "7.16.0", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.16.0.tgz", - "integrity": "sha512-t8MH41kUQylBtu2+4IQA3atqevA2lRgqA2wyVB/YiWmsDSuylZZuXOUy9ric30hfzauEFfdsuk/eXTRrGrfd0g==", + "@babel/helpers": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.27.1.tgz", + "integrity": "sha512-FCvFTm0sWV8Fxhpp2McP5/W53GPllQ9QeQ7SiqGWjMf/LVG07lFa5+pgK05IRhVwtvafT22KF+ZSnM9I545CvQ==", "dev": true, "requires": { - "@babel/helper-validator-identifier": "^7.15.7", - "chalk": "^2.0.0", - "js-tokens": "^4.0.0" + "@babel/template": "^7.27.1", + "@babel/types": "^7.27.1" } }, - "@babel/runtime": { - "version": "7.17.8", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.17.8.tgz", - "integrity": "sha512-dQpEpK0O9o6lj6oPu0gRDbbnk+4LeHlNcBpspf6Olzt3GIX4P1lWF1gS+pHLDFlaJvbR6q7jCfQ08zA4QJBnmA==", + "@babel/parser": { + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.27.2.tgz", + "integrity": "sha512-QYLs8299NA7WM/bZAdp+CviYYkVoYXlDW2rzliy3chxd1PQjej7JORuMJDJXJUb9g0TT+B99EwaVLKmX+sPXWw==", "dev": true, "requires": { - "regenerator-runtime": "^0.13.4" + "@babel/types": "^7.27.1" } }, + "@babel/runtime": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.27.1.tgz", + "integrity": "sha512-1x3D2xEk2fRo3PAhwQwu5UubzgiVWSXTBfWpVd2Mx2AzRqJuDJCsgaDVZ7HB5iGzDW1Hl1sWN2mFyKjmR9uAog==", + "dev": true + }, "@babel/runtime-corejs3": { - "version": "7.10.5", - "resolved": "https://registry.npmjs.org/@babel/runtime-corejs3/-/runtime-corejs3-7.10.5.tgz", - "integrity": "sha512-RMafpmrNB5E/bwdSphLr8a8++9TosnyJp98RZzI6VOx2R2CCMpsXXXRvmI700O9oEKpXdZat6oEK68/F0zjd4A==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/runtime-corejs3/-/runtime-corejs3-7.27.1.tgz", + "integrity": "sha512-909rVuj3phpjW6y0MCXAZ5iNeORePa6ldJvp2baWGcTjwqbBDDz6xoS5JHJ7lS88NlwLYj07ImL/8IUMtDZzTA==", + "dev": true, + "requires": { + "core-js-pure": "^3.30.2" + } + }, + "@babel/template": { + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz", + "integrity": "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.27.1", + "@babel/parser": "^7.27.2", + "@babel/types": "^7.27.1" + } + }, + "@babel/traverse": { + "version": "7.23.2", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.23.2.tgz", + "integrity": "sha512-azpe59SQ48qG6nu2CzcMLbxUudtN+dOM9kDbUqGq3HXUJRlo7i8fvPoxQUzYgLZ4cMVmuZgm8vvBpNeRhd6XSw==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.22.13", + "@babel/generator": "^7.23.0", + "@babel/helper-environment-visitor": "^7.22.20", + "@babel/helper-function-name": "^7.23.0", + "@babel/helper-hoist-variables": "^7.22.5", + "@babel/helper-split-export-declaration": "^7.22.6", + "@babel/parser": "^7.23.0", + "@babel/types": "^7.23.0", + "debug": "^4.1.0", + "globals": "^11.1.0" + }, + "dependencies": { + "debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dev": true, + "requires": { + "ms": "2.1.2" + } + } + } + }, + "@babel/types": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.27.1.tgz", + "integrity": "sha512-+EzkxvLNfiUeKMgy/3luqfsCWFRXLb7U6wNQTk60tovuckwB15B191tJWvpp4HjiQWdJkCxO3Wbvc6jlk3Xb2Q==", "dev": true, "requires": { - "core-js-pure": "^3.0.0", - "regenerator-runtime": "^0.13.4" + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.27.1" } }, "@cspotcode/source-map-consumer": { @@ -15355,56 +16193,86 @@ "integrity": "sha512-dBVuXR082gk3jsFp7Rd/JI4kytwGHecnCoTtXFb7DB6CNHp4rg5k1bhg0nWdLGLnOV71lmDzGQaLMy8iPLY0pw==", "dev": true }, + "@eslint-community/eslint-utils": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz", + "integrity": "sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==", + "dev": true, + "requires": { + "eslint-visitor-keys": "^3.3.0" + } + }, + "@eslint-community/regexpp": { + "version": "4.11.0", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.11.0.tgz", + "integrity": "sha512-G/M/tIiMrTAxEWRfLfQJMmGNX28IxBg4PBz8XqQhqUHLFI6TL2htpIB1iQCj144V5ee/JaKyT9/WZ0MGZWfA7A==", + "dev": true + }, "@eslint/eslintrc": { - "version": "0.4.3", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-0.4.3.tgz", - "integrity": "sha512-J6KFFz5QCYUJq3pf0mjEcCJVERbzv71PUIDczuh9JkwGEzced6CO5ADLHB1rbf/+oPBtoPfMYNOpGDzCANlbXw==", + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.4.tgz", + "integrity": "sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==", "dev": true, "requires": { "ajv": "^6.12.4", - "debug": "^4.1.1", - "espree": "^7.3.0", - "globals": "^13.9.0", - "ignore": "^4.0.6", + "debug": "^4.3.2", + "espree": "^9.6.0", + "globals": "^13.19.0", + "ignore": "^5.2.0", "import-fresh": "^3.2.1", - "js-yaml": "^3.13.1", - "minimatch": "^3.0.4", + "js-yaml": "^4.1.0", + "minimatch": "^3.1.2", "strip-json-comments": "^3.1.1" }, "dependencies": { + "argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true + }, "debug": { - "version": "4.3.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.3.tgz", - "integrity": "sha512-/zxw5+vh1Tfv+4Qn7a5nsbcJKPaSvCDhojn6FEl9vupwK2VCSDtEiEtqr8DFtzYFOdz63LBkxec7DYuc2jon6Q==", + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", + "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", "dev": true, "requires": { - "ms": "2.1.2" + "ms": "^2.1.3" } }, "globals": { - "version": "13.12.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-13.12.0.tgz", - "integrity": "sha512-uS8X6lSKN2JumVoXrbUz+uG4BYG+eiawqm3qFcT7ammfbUHeCBoJMlHcec/S3krSk73/AE/f0szYFmgAA3kYZg==", + "version": "13.24.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", + "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", "dev": true, "requires": { "type-fest": "^0.20.2" } }, - "ignore": { - "version": "4.0.6", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-4.0.6.tgz", - "integrity": "sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg==", - "dev": true + "js-yaml": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", + "dev": true, + "requires": { + "argparse": "^2.0.1" + } }, "minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", "dev": true, "requires": { "brace-expansion": "^1.1.7" } }, + "ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true + }, "type-fest": { "version": "0.20.2", "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", @@ -15413,47 +16281,145 @@ } } }, + "@eslint/js": { + "version": "8.57.1", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.57.1.tgz", + "integrity": "sha512-d9zaMRSTIKDLhctzH12MtXvJKSSUhaHcjV+2Z+GK+EEY7XKpP5yR4x+N3TAcHTcu963nIr+TMcCb4DBCYX1z6Q==", + "dev": true + }, + "@gulpjs/messages": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@gulpjs/messages/-/messages-1.1.0.tgz", + "integrity": "sha512-Ys9sazDatyTgZVb4xPlDufLweJ/Os2uHWOv+Caxvy2O85JcnT4M3vc73bi8pdLWlv3fdWQz3pdI9tVwo8rQQSg==", + "dev": true + }, + "@gulpjs/to-absolute-glob": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@gulpjs/to-absolute-glob/-/to-absolute-glob-4.0.0.tgz", + "integrity": "sha512-kjotm7XJrJ6v+7knhPaRgaT6q8F8K2jiafwYdNHLzmV0uGLuZY43FK6smNSHUPrhq5kX2slCUy+RGG/xGqmIKA==", + "dev": true, + "requires": { + "is-negated-glob": "^1.0.0" + } + }, "@humanwhocodes/config-array": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.5.0.tgz", - "integrity": "sha512-FagtKFz74XrTl7y6HCzQpwDfXP0yhxe9lHLD1UZxjvZIcbyRz8zTFF/yYNfSfzU414eDwZ1SrO0Qvtyf+wFMQg==", + "version": "0.13.0", + "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.13.0.tgz", + "integrity": "sha512-DZLEEqFWQFiyK6h5YIeynKx7JlvCYWL0cImfSRXZ9l4Sg2efkFGTuFf6vzXjK1cq6IYkU+Eg/JizXw+TD2vRNw==", "dev": true, "requires": { - "@humanwhocodes/object-schema": "^1.2.0", - "debug": "^4.1.1", - "minimatch": "^3.0.4" + "@humanwhocodes/object-schema": "^2.0.3", + "debug": "^4.3.1", + "minimatch": "^3.0.5" }, "dependencies": { "debug": { - "version": "4.3.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.3.tgz", - "integrity": "sha512-/zxw5+vh1Tfv+4Qn7a5nsbcJKPaSvCDhojn6FEl9vupwK2VCSDtEiEtqr8DFtzYFOdz63LBkxec7DYuc2jon6Q==", + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", + "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", "dev": true, "requires": { - "ms": "2.1.2" + "ms": "^2.1.3" } }, "minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", "dev": true, "requires": { "brace-expansion": "^1.1.7" } + }, + "ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true } } }, + "@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true + }, "@humanwhocodes/object-schema": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-1.2.1.tgz", - "integrity": "sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==", + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.3.tgz", + "integrity": "sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==", "dev": true }, "@iarna/toml": { - "version": "2.2.5", - "resolved": "https://registry.npmjs.org/@iarna/toml/-/toml-2.2.5.tgz", - "integrity": "sha512-trnsAYxU3xnS1gPHPyU961coFyLkh4gAD/0zQ5mymY4yOZ+CYvsPqUbOFSw0aDM4y0tV7tiFxL/1XfXPNC6IPg==" + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@iarna/toml/-/toml-3.0.0.tgz", + "integrity": "sha512-td6ZUkz2oS3VeleBcN+m//Q6HlCFCPrnI0FZhrt/h4XqLEdOyYp2u21nd8MdsR+WJy5r9PTDaHTDDfhf4H4l6Q==" + }, + "@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "dev": true, + "requires": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "dependencies": { + "ansi-regex": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", + "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", + "dev": true + }, + "ansi-styles": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", + "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", + "dev": true + }, + "emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true + }, + "string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dev": true, + "requires": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + } + }, + "strip-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "dev": true, + "requires": { + "ansi-regex": "^6.0.1" + } + }, + "wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "dev": true, + "requires": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + } + } + } }, "@istanbuljs/load-nyc-config": { "version": "1.0.0", @@ -15483,14 +16449,14 @@ "dev": true }, "@jridgewell/gen-mapping": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.2.tgz", - "integrity": "sha512-mh65xKQAzI6iBcFzwv28KVWSmCkdRBWoOh+bYQGW3+6OZvbbN3TqMGo5hqYxQniRcH9F2VZIoJCm4pa3BPDK/A==", + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz", + "integrity": "sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==", "dev": true, "requires": { - "@jridgewell/set-array": "^1.0.1", + "@jridgewell/set-array": "^1.2.1", "@jridgewell/sourcemap-codec": "^1.4.10", - "@jridgewell/trace-mapping": "^0.3.9" + "@jridgewell/trace-mapping": "^0.3.24" } }, "@jridgewell/resolve-uri": { @@ -15500,19 +16466,19 @@ "dev": true }, "@jridgewell/set-array": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.1.2.tgz", - "integrity": "sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz", + "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==", "dev": true }, "@jridgewell/source-map": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.2.tgz", - "integrity": "sha512-m7O9o2uR8k2ObDysZYzdfhb08VuEml5oWGiosa1VdaPZ/A6QyPkAJuwN0Q1lhULOf6B7MtQmHENS743hWtCrgw==", + "version": "0.3.11", + "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.11.tgz", + "integrity": "sha512-ZMp1V8ZFcPG5dIWnQLr3NSI1MiCU7UETdS/A0G8V/XWHvJv3ZsFqutJn1Y5RPmAPX6F3BiE397OqveU/9NCuIA==", "dev": true, "requires": { - "@jridgewell/gen-mapping": "^0.3.0", - "@jridgewell/trace-mapping": "^0.3.9" + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25" } }, "@jridgewell/sourcemap-codec": { @@ -15522,85 +16488,124 @@ "dev": true }, "@jridgewell/trace-mapping": { - "version": "0.3.14", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.14.tgz", - "integrity": "sha512-bJWEfQ9lPTvm3SneWwRFVLzrh6nhjwqw7TUFFBEMzwvg7t7PCDenf2lDwqo4NQXzdpgBXyFgDWnQA+2vkruksQ==", + "version": "0.3.25", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", + "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", "dev": true, "requires": { - "@jridgewell/resolve-uri": "^3.0.3", - "@jridgewell/sourcemap-codec": "^1.4.10" + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" } }, "@microsoft/1ds-core-js": { - "version": "3.2.9", - "resolved": "https://registry.npmjs.org/@microsoft/1ds-core-js/-/1ds-core-js-3.2.9.tgz", - "integrity": "sha512-3pCfM2TzHn3gU9pxHztduKcVRdb/nzruvPFfHPZD0IM0mb0h6TGo2isELF3CTMahTx50RAC51ojNIw2/7VRkOg==", + "version": "3.2.13", + "resolved": "https://registry.npmjs.org/@microsoft/1ds-core-js/-/1ds-core-js-3.2.13.tgz", + "integrity": "sha512-CluYTRWcEk0ObG5EWFNWhs87e2qchJUn0p2D21ZUa3PWojPZfPSBs4//WIE0MYV8Qg1Hdif2ZTwlM7TbYUjfAg==", "requires": { - "@microsoft/applicationinsights-core-js": "2.8.10", + "@microsoft/applicationinsights-core-js": "2.8.15", "@microsoft/applicationinsights-shims": "^2.0.2", "@microsoft/dynamicproto-js": "^1.1.7" } }, "@microsoft/1ds-post-js": { - "version": "3.2.9", - "resolved": "https://registry.npmjs.org/@microsoft/1ds-post-js/-/1ds-post-js-3.2.9.tgz", - "integrity": "sha512-D/RtqkQ2Nr4cuoGqmhi5QTmi3cBlxehIThJ1u3BaH9H/YkLNTKEcHZRWTXy14bXheCefNHciLuadg37G2Kekcg==", + "version": "3.2.13", + "resolved": "https://registry.npmjs.org/@microsoft/1ds-post-js/-/1ds-post-js-3.2.13.tgz", + "integrity": "sha512-HgS574fdD19Bo2vPguyznL4eDw7Pcm1cVNpvbvBLWiW3x4e1FCQ3VMXChWnAxCae8Hb0XqlA2sz332ZobBavTA==", "requires": { - "@microsoft/1ds-core-js": "3.2.9", + "@microsoft/1ds-core-js": "3.2.13", "@microsoft/applicationinsights-shims": "^2.0.2", "@microsoft/dynamicproto-js": "^1.1.7" } }, "@microsoft/applicationinsights-channel-js": { - "version": "2.8.11", - "resolved": "https://registry.npmjs.org/@microsoft/applicationinsights-channel-js/-/applicationinsights-channel-js-2.8.11.tgz", - "integrity": "sha512-DGDNzT4DMlSvUzWjA4y3tDg47+QYOPV+W07vlfdPwGgLwrl4n6Q4crrW8Y/IOpthHAKDU8rolSAUvP3NqxPi4Q==", + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@microsoft/applicationinsights-channel-js/-/applicationinsights-channel-js-3.0.2.tgz", + "integrity": "sha512-jDBNKbCHsJgmpv0CKNhJ/uN9ZphvfGdb93Svk+R4LjO8L3apNNMbDDPxBvXXi0uigRmA1TBcmyBG4IRKjabGhw==", "requires": { - "@microsoft/applicationinsights-common": "2.8.11", - "@microsoft/applicationinsights-core-js": "2.8.11", - "@microsoft/applicationinsights-shims": "2.0.2", - "@microsoft/dynamicproto-js": "^1.1.7" + "@microsoft/applicationinsights-common": "3.0.2", + "@microsoft/applicationinsights-core-js": "3.0.2", + "@microsoft/applicationinsights-shims": "3.0.1", + "@microsoft/dynamicproto-js": "^2.0.2", + "@nevware21/ts-async": ">= 0.2.4 < 2.x", + "@nevware21/ts-utils": ">= 0.9.5 < 2.x" }, "dependencies": { "@microsoft/applicationinsights-core-js": { - "version": "2.8.11", - "resolved": "https://registry.npmjs.org/@microsoft/applicationinsights-core-js/-/applicationinsights-core-js-2.8.11.tgz", - "integrity": "sha512-6ScXplyb9Zb0K6TQRfqStm20j5lIe/Dslf65ozows6ibDcKkWl2ZdqzFhymVJZz1WRNpSyD4aA8qnqmslIER6g==", + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@microsoft/applicationinsights-core-js/-/applicationinsights-core-js-3.0.2.tgz", + "integrity": "sha512-WQhVhzlRlLDrQzn3OShCW/pL3BW5WC57t0oywSknX3q7lMzI3jDg7Ihh0iuIcNTzGCTbDkuqr4d6IjEDWIMtJQ==", + "requires": { + "@microsoft/applicationinsights-shims": "3.0.1", + "@microsoft/dynamicproto-js": "^2.0.2", + "@nevware21/ts-async": ">= 0.2.4 < 2.x", + "@nevware21/ts-utils": ">= 0.9.5 < 2.x" + } + }, + "@microsoft/applicationinsights-shims": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@microsoft/applicationinsights-shims/-/applicationinsights-shims-3.0.1.tgz", + "integrity": "sha512-DKwboF47H1nb33rSUfjqI6ryX29v+2QWcTrRvcQDA32AZr5Ilkr7whOOSsD1aBzwqX0RJEIP1Z81jfE3NBm/Lg==", + "requires": { + "@nevware21/ts-utils": ">= 0.9.4 < 2.x" + } + }, + "@microsoft/dynamicproto-js": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@microsoft/dynamicproto-js/-/dynamicproto-js-2.0.2.tgz", + "integrity": "sha512-MB8trWaFREpmb037k/d0bB7T2BP7Ai24w1e1tbz3ASLB0/lwphsq3Nq8S9I5AsI5vs4zAQT+SB5nC5/dLYTiOg==", "requires": { - "@microsoft/applicationinsights-shims": "2.0.2", - "@microsoft/dynamicproto-js": "^1.1.7" + "@nevware21/ts-utils": ">= 0.9.4 < 2.x" } } } }, "@microsoft/applicationinsights-common": { - "version": "2.8.11", - "resolved": "https://registry.npmjs.org/@microsoft/applicationinsights-common/-/applicationinsights-common-2.8.11.tgz", - "integrity": "sha512-Cxu4gRajkYv9buEtrcLGHK97AqGK62feN9jH9/JSjUSiSFhbnWtYvEg1EMqMI/P4pneu53yLJloITB+TKwmK7A==", + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@microsoft/applicationinsights-common/-/applicationinsights-common-3.0.2.tgz", + "integrity": "sha512-y+WXWop+OVim954Cu1uyYMnNx6PWO8okHpZIQi/1YSqtqaYdtJVPv4P0AVzwJdohxzVfgzKvqj9nec/VWqE2Zg==", "requires": { - "@microsoft/applicationinsights-core-js": "2.8.11", - "@microsoft/applicationinsights-shims": "2.0.2", - "@microsoft/dynamicproto-js": "^1.1.7" + "@microsoft/applicationinsights-core-js": "3.0.2", + "@microsoft/applicationinsights-shims": "3.0.1", + "@microsoft/dynamicproto-js": "^2.0.2", + "@nevware21/ts-utils": ">= 0.9.5 < 2.x" }, "dependencies": { "@microsoft/applicationinsights-core-js": { - "version": "2.8.11", - "resolved": "https://registry.npmjs.org/@microsoft/applicationinsights-core-js/-/applicationinsights-core-js-2.8.11.tgz", - "integrity": "sha512-6ScXplyb9Zb0K6TQRfqStm20j5lIe/Dslf65ozows6ibDcKkWl2ZdqzFhymVJZz1WRNpSyD4aA8qnqmslIER6g==", + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@microsoft/applicationinsights-core-js/-/applicationinsights-core-js-3.0.2.tgz", + "integrity": "sha512-WQhVhzlRlLDrQzn3OShCW/pL3BW5WC57t0oywSknX3q7lMzI3jDg7Ihh0iuIcNTzGCTbDkuqr4d6IjEDWIMtJQ==", + "requires": { + "@microsoft/applicationinsights-shims": "3.0.1", + "@microsoft/dynamicproto-js": "^2.0.2", + "@nevware21/ts-async": ">= 0.2.4 < 2.x", + "@nevware21/ts-utils": ">= 0.9.5 < 2.x" + } + }, + "@microsoft/applicationinsights-shims": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@microsoft/applicationinsights-shims/-/applicationinsights-shims-3.0.1.tgz", + "integrity": "sha512-DKwboF47H1nb33rSUfjqI6ryX29v+2QWcTrRvcQDA32AZr5Ilkr7whOOSsD1aBzwqX0RJEIP1Z81jfE3NBm/Lg==", + "requires": { + "@nevware21/ts-utils": ">= 0.9.4 < 2.x" + } + }, + "@microsoft/dynamicproto-js": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@microsoft/dynamicproto-js/-/dynamicproto-js-2.0.2.tgz", + "integrity": "sha512-MB8trWaFREpmb037k/d0bB7T2BP7Ai24w1e1tbz3ASLB0/lwphsq3Nq8S9I5AsI5vs4zAQT+SB5nC5/dLYTiOg==", "requires": { - "@microsoft/applicationinsights-shims": "2.0.2", - "@microsoft/dynamicproto-js": "^1.1.7" + "@nevware21/ts-utils": ">= 0.9.4 < 2.x" } } } }, "@microsoft/applicationinsights-core-js": { - "version": "2.8.10", - "resolved": "https://registry.npmjs.org/@microsoft/applicationinsights-core-js/-/applicationinsights-core-js-2.8.10.tgz", - "integrity": "sha512-jQrufDW0+sV8fBhRvzIPNGiCC6dELH+Ug0DM5CfN9757TBqZJz8CSWyDjex39as8+jD0F/8HRU9QdmrVgq5vFg==", + "version": "2.8.15", + "resolved": "https://registry.npmjs.org/@microsoft/applicationinsights-core-js/-/applicationinsights-core-js-2.8.15.tgz", + "integrity": "sha512-yYAs9MyjGr2YijQdUSN9mVgT1ijI1FPMgcffpaPmYbHAVbQmF7bXudrBWHxmLzJlwl5rfep+Zgjli2e67lwUqQ==", "requires": { "@microsoft/applicationinsights-shims": "2.0.2", - "@microsoft/dynamicproto-js": "^1.1.7" + "@microsoft/dynamicproto-js": "^1.1.9" } }, "@microsoft/applicationinsights-shims": { @@ -15609,24 +16614,44 @@ "integrity": "sha512-PoHEgsnmcqruLNHZ/amACqdJ6YYQpED0KSRe6J7gIJTtpZC1FfFU9b1fmDKDKtFoUSrPzEh1qzO3kmRZP0betg==" }, "@microsoft/applicationinsights-web-basic": { - "version": "2.8.11", - "resolved": "https://registry.npmjs.org/@microsoft/applicationinsights-web-basic/-/applicationinsights-web-basic-2.8.11.tgz", - "integrity": "sha512-11T7bbP4ifIBg95E9mYZv1g/vcWvw/KaWKRcGMREP3+vBTLBwMB8r2e9Zd583bOVx+9/gRvfIg+Z/lInQqAfbA==", + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@microsoft/applicationinsights-web-basic/-/applicationinsights-web-basic-3.0.2.tgz", + "integrity": "sha512-6Lq0DE/pZp9RvSV+weGbcxN1NDmfczj6gNPhvZKV2YSQ3RK0LZE3+wjTWLXfuStq8a+nCBdsRpWk8tOKgsoxcg==", "requires": { - "@microsoft/applicationinsights-channel-js": "2.8.11", - "@microsoft/applicationinsights-common": "2.8.11", - "@microsoft/applicationinsights-core-js": "2.8.11", - "@microsoft/applicationinsights-shims": "2.0.2", - "@microsoft/dynamicproto-js": "^1.1.7" + "@microsoft/applicationinsights-channel-js": "3.0.2", + "@microsoft/applicationinsights-common": "3.0.2", + "@microsoft/applicationinsights-core-js": "3.0.2", + "@microsoft/applicationinsights-shims": "3.0.1", + "@microsoft/dynamicproto-js": "^2.0.2", + "@nevware21/ts-async": ">= 0.2.4 < 2.x", + "@nevware21/ts-utils": ">= 0.9.5 < 2.x" }, "dependencies": { "@microsoft/applicationinsights-core-js": { - "version": "2.8.11", - "resolved": "https://registry.npmjs.org/@microsoft/applicationinsights-core-js/-/applicationinsights-core-js-2.8.11.tgz", - "integrity": "sha512-6ScXplyb9Zb0K6TQRfqStm20j5lIe/Dslf65ozows6ibDcKkWl2ZdqzFhymVJZz1WRNpSyD4aA8qnqmslIER6g==", + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@microsoft/applicationinsights-core-js/-/applicationinsights-core-js-3.0.2.tgz", + "integrity": "sha512-WQhVhzlRlLDrQzn3OShCW/pL3BW5WC57t0oywSknX3q7lMzI3jDg7Ihh0iuIcNTzGCTbDkuqr4d6IjEDWIMtJQ==", + "requires": { + "@microsoft/applicationinsights-shims": "3.0.1", + "@microsoft/dynamicproto-js": "^2.0.2", + "@nevware21/ts-async": ">= 0.2.4 < 2.x", + "@nevware21/ts-utils": ">= 0.9.5 < 2.x" + } + }, + "@microsoft/applicationinsights-shims": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@microsoft/applicationinsights-shims/-/applicationinsights-shims-3.0.1.tgz", + "integrity": "sha512-DKwboF47H1nb33rSUfjqI6ryX29v+2QWcTrRvcQDA32AZr5Ilkr7whOOSsD1aBzwqX0RJEIP1Z81jfE3NBm/Lg==", + "requires": { + "@nevware21/ts-utils": ">= 0.9.4 < 2.x" + } + }, + "@microsoft/dynamicproto-js": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@microsoft/dynamicproto-js/-/dynamicproto-js-2.0.2.tgz", + "integrity": "sha512-MB8trWaFREpmb037k/d0bB7T2BP7Ai24w1e1tbz3ASLB0/lwphsq3Nq8S9I5AsI5vs4zAQT+SB5nC5/dLYTiOg==", "requires": { - "@microsoft/applicationinsights-shims": "2.0.2", - "@microsoft/dynamicproto-js": "^1.1.7" + "@nevware21/ts-utils": ">= 0.9.4 < 2.x" } } } @@ -15641,6 +16666,25 @@ "resolved": "https://registry.npmjs.org/@microsoft/dynamicproto-js/-/dynamicproto-js-1.1.9.tgz", "integrity": "sha512-n1VPsljTSkthsAFYdiWfC+DKzK2WwcRp83Y1YAqdX552BstvsDjft9YXppjUzp11BPsapDoO1LDgrDB0XVsfNQ==" }, + "@nevware21/ts-async": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@nevware21/ts-async/-/ts-async-0.3.0.tgz", + "integrity": "sha512-ZUcgUH12LN/F6nzN0cYd0F/rJaMLmXr0EHVTyYfaYmK55bdwE4338uue4UiVoRqHVqNW4KDUrJc49iGogHKeWA==", + "requires": { + "@nevware21/ts-utils": ">= 0.10.0 < 2.x" + } + }, + "@nevware21/ts-utils": { + "version": "0.10.1", + "resolved": "https://registry.npmjs.org/@nevware21/ts-utils/-/ts-utils-0.10.1.tgz", + "integrity": "sha512-pMny25NnF2/MJwdqC3Iyjm2pGIXNxni4AROpcqDeWa+td9JMUY4bUS9uU9XW+BoBRqTLUL+WURF9SOd/6OQzRg==" + }, + "@nicolo-ribaudo/semver-v6": { + "version": "6.3.3", + "resolved": "https://registry.npmjs.org/@nicolo-ribaudo/semver-v6/-/semver-v6-6.3.3.tgz", + "integrity": "sha512-3Yc1fUTs69MG/uZbJlLSI3JISMn2UV2rg+1D/vROUqZyh3l6iYHCs7GMp+M40ZD7yOdDbYjJcU1oTJhrc+dGKg==", + "dev": true + }, "@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -15673,36 +16717,55 @@ "integrity": "sha512-O2yRJce1GOc6PAy3QxFM4NzFiWzvScDC1/5ihYBL6BUEVdq0XMWN01sppE+H6bBXbaFYipjwFLEWLg5PaSOThA==" }, "@opentelemetry/core": { - "version": "1.11.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-1.11.0.tgz", - "integrity": "sha512-aP1wHSb+YfU0pM63UAkizYPuS4lZxzavHHw5KJfFNN2oWQ79HSm6JR3CzwFKHwKhSzHN8RE9fgP1IdVJ8zmo1w==", + "version": "1.15.2", + "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-1.15.2.tgz", + "integrity": "sha512-+gBv15ta96WqkHZaPpcDHiaz0utiiHZVfm2YOYSqFGrUaJpPkMoSuLBB58YFQGi6Rsb9EHos84X6X5+9JspmLw==", "requires": { - "@opentelemetry/semantic-conventions": "1.11.0" + "@opentelemetry/semantic-conventions": "1.15.2" + } + }, + "@opentelemetry/instrumentation": { + "version": "0.41.2", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation/-/instrumentation-0.41.2.tgz", + "integrity": "sha512-rxU72E0pKNH6ae2w5+xgVYZLzc5mlxAbGzF4shxMVK8YC2QQsfN38B2GPbj0jvrKWWNUElfclQ+YTykkNg/grw==", + "requires": { + "@types/shimmer": "^1.0.2", + "import-in-the-middle": "1.4.2", + "require-in-the-middle": "^7.1.1", + "semver": "^7.5.1", + "shimmer": "^1.2.1" } }, "@opentelemetry/resources": { - "version": "1.11.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-1.11.0.tgz", - "integrity": "sha512-y0z2YJTqk0ag+hGT4EXbxH/qPhDe8PfwltYb4tXIEsozgEFfut/bqW7H7pDvylmCjBRMG4NjtLp57V1Ev++brA==", + "version": "1.15.2", + "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-1.15.2.tgz", + "integrity": "sha512-xmMRLenT9CXmm5HMbzpZ1hWhaUowQf8UB4jMjFlAxx1QzQcsD3KFNAVX/CAWzFPtllTyTplrA4JrQ7sCH3qmYw==", "requires": { - "@opentelemetry/core": "1.11.0", - "@opentelemetry/semantic-conventions": "1.11.0" + "@opentelemetry/core": "1.15.2", + "@opentelemetry/semantic-conventions": "1.15.2" } }, "@opentelemetry/sdk-trace-base": { - "version": "1.11.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-base/-/sdk-trace-base-1.11.0.tgz", - "integrity": "sha512-DV8e5/Qo42V8FMBlQ0Y0Liv6Hl/Pp5bAZ73s7r1euX8w4bpRes1B7ACiA4yujADbWMJxBgSo4fGbi4yjmTMG2A==", + "version": "1.15.2", + "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-base/-/sdk-trace-base-1.15.2.tgz", + "integrity": "sha512-BEaxGZbWtvnSPchV98qqqqa96AOcb41pjgvhfzDij10tkBhIu9m0Jd6tZ1tJB5ZHfHbTffqYVYE0AOGobec/EQ==", "requires": { - "@opentelemetry/core": "1.11.0", - "@opentelemetry/resources": "1.11.0", - "@opentelemetry/semantic-conventions": "1.11.0" + "@opentelemetry/core": "1.15.2", + "@opentelemetry/resources": "1.15.2", + "@opentelemetry/semantic-conventions": "1.15.2" } }, "@opentelemetry/semantic-conventions": { - "version": "1.11.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.11.0.tgz", - "integrity": "sha512-fG4D0AktoHyHwGhFGv+PzKrZjxbKJfckJauTJdq2A+ej5cTazmNYjJVAODXXkYyrsI10muMl+B1iO2q1R6Lp+w==" + "version": "1.15.2", + "resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.15.2.tgz", + "integrity": "sha512-CjbOKwk2s+3xPIMcd5UNYQzsf+v94RczbdNix9/kQh38WiQkM90sUOi3if8eyHFgiBjBjhwXrA7W3ydiSQP9mw==" + }, + "@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "dev": true, + "optional": true }, "@polka/url": { "version": "1.0.0-next.21", @@ -15710,6 +16773,12 @@ "integrity": "sha512-a5Sab1C4/icpTZVzZc5Ghpz88yQtGOyNqYXcZgOssB2uuAr+wF/MvN6bgtW32q7HHrvBki+BsZ0OuNv6EV3K9g==", "dev": true }, + "@rtsao/scc": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@rtsao/scc/-/scc-1.1.0.tgz", + "integrity": "sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==", + "dev": true + }, "@sindresorhus/is": { "version": "0.7.0", "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-0.7.0.tgz", @@ -15717,38 +16786,49 @@ "dev": true }, "@sinonjs/commons": { - "version": "1.8.3", - "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-1.8.3.tgz", - "integrity": "sha512-xkNcLAn/wZaX14RPlwizcKicDk9G3F8m2nU3L7Ukm5zBgTwiT0wsoFAHx9Jq56fJA1z/7uKGtCRu16sOUCLIHQ==", + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.1.tgz", + "integrity": "sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==", "dev": true, "requires": { "type-detect": "4.0.8" } }, "@sinonjs/fake-timers": { - "version": "9.1.1", - "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-9.1.1.tgz", - "integrity": "sha512-Wp5vwlZ0lOqpSYGKqr53INws9HLkt6JDc/pDZcPf7bchQnrXJMXPns8CXx0hFikMSGSWfvtvvpb2gtMVfkWagA==", + "version": "11.2.2", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-11.2.2.tgz", + "integrity": "sha512-G2piCSxQ7oWOxwGSAyFHfPIsyeJGXYtc6mFbnFA+kRXkiEnTl8c/8jul2S329iFBnDI9HGoeWWAZvuvOkZccgw==", "dev": true, "requires": { - "@sinonjs/commons": "^1.7.0" + "@sinonjs/commons": "^3.0.0" } }, "@sinonjs/samsam": { - "version": "6.1.1", - "resolved": "https://registry.npmjs.org/@sinonjs/samsam/-/samsam-6.1.1.tgz", - "integrity": "sha512-cZ7rKJTLiE7u7Wi/v9Hc2fs3Ucc3jrWeMgPHbbTCeVAB2S0wOBbYlkJVeNSL04i7fdhT8wIbDq1zhC/PXTD2SA==", + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@sinonjs/samsam/-/samsam-8.0.0.tgz", + "integrity": "sha512-Bp8KUVlLp8ibJZrnvq2foVhP0IVX2CIprMJPK0vqGqgrDa0OHVKeZyBykqskkrdxV6yKBPmGasO8LVjAKR3Gew==", "dev": true, "requires": { - "@sinonjs/commons": "^1.6.0", + "@sinonjs/commons": "^2.0.0", "lodash.get": "^4.4.2", "type-detect": "^4.0.8" + }, + "dependencies": { + "@sinonjs/commons": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-2.0.0.tgz", + "integrity": "sha512-uLa0j859mMrg2slwQYdO/AkrOfmH+X6LTVmNTS9CqexuE2IvVORIkSpJLqePAbEnKJ77aMmCwr1NUZ57120Xcg==", + "dev": true, + "requires": { + "type-detect": "4.0.8" + } + } } }, "@sinonjs/text-encoding": { - "version": "0.7.1", - "resolved": "https://registry.npmjs.org/@sinonjs/text-encoding/-/text-encoding-0.7.1.tgz", - "integrity": "sha512-+iTbntw2IZPb/anVDbypzfQa+ay64MW0Zo8aJ8gZPWMMK6/OubMVb6lUPMagqjOPnmtauXnFCACVl3O7ogjeqQ==", + "version": "0.7.3", + "resolved": "https://registry.npmjs.org/@sinonjs/text-encoding/-/text-encoding-0.7.3.tgz", + "integrity": "sha512-DE427ROAphMQzU4ENbliGYrBSYPXF+TtLg9S8vzeA+OF4ZKzoDdzfL8sxuMUGS/lgRhM6j1URSk9ghf7Xo1tyA==", "dev": true }, "@tootallnate/once": { @@ -15821,35 +16901,29 @@ "dev": true }, "@types/decompress": { - "version": "4.2.4", - "resolved": "https://registry.npmjs.org/@types/decompress/-/decompress-4.2.4.tgz", - "integrity": "sha512-/C8kTMRTNiNuWGl5nEyKbPiMv6HA+0RbEXzFhFBEzASM6+oa4tJro9b8nj7eRlOFfuLdzUU+DS/GPDlvvzMOhA==", + "version": "4.2.5", + "resolved": "https://registry.npmjs.org/@types/decompress/-/decompress-4.2.5.tgz", + "integrity": "sha512-LdL+kbcKGs9TzvB/K+OBGzPfDoP6gwwTsykYjodlzUJUUYp/43c1p1jE5YTtz3z4Ml90iruvBXbJ6+kDvb3WSQ==", "dev": true, "requires": { "@types/node": "*" } }, - "@types/diff-match-patch": { - "version": "1.0.32", - "resolved": "https://registry.npmjs.org/@types/diff-match-patch/-/diff-match-patch-1.0.32.tgz", - "integrity": "sha512-bPYT5ECFiblzsVzyURaNhljBH2Gh1t9LowgUwciMrNAhFewLkHT2H0Mto07Y4/3KCOGZHRQll3CTtQZ0X11D/A==", - "dev": true - }, "@types/download": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/@types/download/-/download-8.0.1.tgz", - "integrity": "sha512-t5DjMD6Y1DxjXtEHl7Kt+nQn9rOmVLYD8p4Swrcc5QpgyqyqR2gXTIK6RwwMnNeFJ+ZIiIW789fQKzCrK7AOFA==", + "version": "8.0.3", + "resolved": "https://registry.npmjs.org/@types/download/-/download-8.0.3.tgz", + "integrity": "sha512-IDwXjU7zCtuFVvI0Plnb02TpXyj3RA4YeOKQvEfsjdJeWxZ9hTl6lxeNsU2bLWn0aeAS7fyMl74w/TbdOlS2KQ==", "dev": true, "requires": { "@types/decompress": "*", - "@types/got": "^8", + "@types/got": "^9", "@types/node": "*" } }, "@types/eslint": { - "version": "8.4.1", - "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.4.1.tgz", - "integrity": "sha512-GE44+DNEyxxh2Kc6ro/VkIj+9ma0pO0bwv9+uHSyBrikYOHr8zYcdPvnBOp1aw8s+CjRvuSx7CyWqRrNFQ59mA==", + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-9.6.1.tgz", + "integrity": "sha512-FXx2pKgId/WyYo2jXw63kk7/+TY7u7AziEJxJAnSFzHlqTAS3Ync6SvgYAN/k4/PQpnnVuzoMuVnByKK2qp0ag==", "dev": true, "requires": { "@types/estree": "*", @@ -15857,33 +16931,28 @@ } }, "@types/eslint-scope": { - "version": "3.7.3", - "resolved": "https://registry.npmjs.org/@types/eslint-scope/-/eslint-scope-3.7.3.tgz", - "integrity": "sha512-PB3ldyrcnAicT35TWPs5IcwKD8S333HMaa2VVv4+wdvebJkjWuW/xESoB8IwRcog8HYVYamb1g/R31Qv5Bx03g==", + "version": "3.7.7", + "resolved": "https://registry.npmjs.org/@types/eslint-scope/-/eslint-scope-3.7.7.tgz", + "integrity": "sha512-MzMFlSLBqNF2gcHWO0G1vP/YQyfvrxZ0bF+u7mzUdZ1/xK4A4sru+nraZz5i3iEIk1l1uyicaDVTB4QbbEkAYg==", "dev": true, "requires": { "@types/eslint": "*", "@types/estree": "*" } }, - "@types/eslint-visitor-keys": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@types/eslint-visitor-keys/-/eslint-visitor-keys-1.0.0.tgz", - "integrity": "sha512-OCutwjDZ4aFS6PB1UZ988C4YgwlBHJd6wCeQqaLdmadZ/7e+w79+hbMUFC1QXDNCmdyoRfAFdm0RypzwR+Qpag==", - "dev": true - }, "@types/estree": { - "version": "0.0.51", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-0.0.51.tgz", - "integrity": "sha512-CuPgU6f3eT/XgKKPqKd/gLZV1Xmvf1a2R5POBOGQa6uv82xpls89HU5zKeVoyR8XzHd1RGNOlQlvUe3CFkjWNQ==", + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", "dev": true }, "@types/fs-extra": { - "version": "9.0.13", - "resolved": "https://registry.npmjs.org/@types/fs-extra/-/fs-extra-9.0.13.tgz", - "integrity": "sha512-nEnwB++1u5lVDM2UI4c1+5R+FYaKfaAzS4OococimjVm3nQw3TuzH5UNsocrcTBbhnerblyHj4A49qXbIiZdpA==", + "version": "11.0.4", + "resolved": "https://registry.npmjs.org/@types/fs-extra/-/fs-extra-11.0.4.tgz", + "integrity": "sha512-yTbItCNreRooED33qjunPthRcSjERP1r4MqCZc7wv0u2sUkzTFp45tgUfS5+r7FrZPdmCCNflLhVSP/o+SemsQ==", "dev": true, "requires": { + "@types/jsonfile": "*", "@types/node": "*" } }, @@ -15898,18 +16967,42 @@ } }, "@types/got": { - "version": "8.3.6", - "resolved": "https://registry.npmjs.org/@types/got/-/got-8.3.6.tgz", - "integrity": "sha512-nvLlj+831dhdm4LR2Ly+HTpdLyBaMynoOr6wpIxS19d/bPeHQxFU5XQ6Gp6ohBpxvCWZM1uHQIC2+ySRH1rGrQ==", + "version": "9.6.12", + "resolved": "https://registry.npmjs.org/@types/got/-/got-9.6.12.tgz", + "integrity": "sha512-X4pj/HGHbXVLqTpKjA2ahI4rV/nNBc9mGO2I/0CgAra+F2dKgMXnENv2SRpemScBzBAI4vMelIVYViQxlSE6xA==", "dev": true, "requires": { - "@types/node": "*" + "@types/node": "*", + "@types/tough-cookie": "*", + "form-data": "^2.5.0" + }, + "dependencies": { + "form-data": { + "version": "2.5.5", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.5.5.tgz", + "integrity": "sha512-jqdObeR2rxZZbPSGL+3VckHMYtu+f9//KXBsVny6JSX/pa38Fy+bGjuG8eW/H6USNQWhLi8Num++cU2yOCNz4A==", + "dev": true, + "requires": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", + "mime-types": "^2.1.35", + "safe-buffer": "^5.2.1" + } + }, + "safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "dev": true + } } }, "@types/json-schema": { - "version": "7.0.9", - "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.9.tgz", - "integrity": "sha512-qcUXuemtEu+E5wZSJHNxUXeCZhAfXKQ41D+duX+VYPde7xyEVZci+/oXKJL13tnRs9lR2pr4fod59GT6/X1/yQ==", + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", "dev": true }, "@types/json5": { @@ -15918,18 +17011,21 @@ "integrity": "sha1-7ihweulOEdK4J7y+UnC86n8+ce4=", "dev": true }, + "@types/jsonfile": { + "version": "6.1.4", + "resolved": "https://registry.npmjs.org/@types/jsonfile/-/jsonfile-6.1.4.tgz", + "integrity": "sha512-D5qGUYwjvnNNextdU59/+fI+spnwtTFmyQP0h+PfIOSkNfpU6AOICUOkm4i0OnSk+NyjdPJrxCDro0sJsWlRpQ==", + "dev": true, + "requires": { + "@types/node": "*" + } + }, "@types/lodash": { "version": "4.14.181", "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.181.tgz", "integrity": "sha512-n3tyKthHJbkiWhDZs3DkhkCzt2MexYHXlX0td5iMplyfwketaOeKboEVBqzceH7juqvEg3q5oUoBFxSLu7zFag==", "dev": true }, - "@types/md5": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/@types/md5/-/md5-2.3.2.tgz", - "integrity": "sha512-v+JFDu96+UYJ3/UWzB0mEglIS//MZXgRaJ4ubUPwOM0gvLc/kcQ3TWNYwENEK7/EcXGQVrW8h/XqednSjBd/Og==", - "dev": true - }, "@types/minimatch": { "version": "3.0.5", "resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-3.0.5.tgz", @@ -15942,27 +17038,26 @@ "integrity": "sha512-QCWHkbMv4Y5U9oW10Uxbr45qMMSzl4OzijsozynUAgx3kEHUdXB00udx2dWDQ7f2TU2a2uuiFaRZjCe3unPpeg==", "dev": true }, - "@types/nock": { - "version": "10.0.3", - "resolved": "https://registry.npmjs.org/@types/nock/-/nock-10.0.3.tgz", - "integrity": "sha512-OthuN+2FuzfZO3yONJ/QVjKmLEuRagS9TV9lEId+WHL9KhftYG+/2z+pxlr0UgVVXSpVD8woie/3fzQn8ft/Ow==", + "@types/node": { + "version": "22.19.3", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.3.tgz", + "integrity": "sha512-1N9SBnWYOJTrNZCdh/yJE+t910Y128BoyY+zBLWhL3r0TYzlTmFdXrPwHL9DyFZmlEXNQQolTZh3KHV31QDhyA==", "dev": true, "requires": { - "@types/node": "*" + "undici-types": "~6.21.0" } }, - "@types/node": { - "version": "16.18.25", - "resolved": "https://registry.npmjs.org/@types/node/-/node-16.18.25.tgz", - "integrity": "sha512-rUDO6s9Q/El1R1I21HG4qw/LstTHCPO/oQNAwI/4b2f9EWvMnqt4d3HJwPMawfZ3UvodB8516Yg+VAq54YM+eA==", - "dev": true - }, "@types/semver": { "version": "5.5.0", "resolved": "https://registry.npmjs.org/@types/semver/-/semver-5.5.0.tgz", "integrity": "sha512-41qEJgBH/TWgo5NFSvBCJ1qkoi3Q6ONSF2avrHq1LVEZfYpdHmj0y9SuTK+u9ZhG1sYQKBL1AWXKyLWP4RaUoQ==", "dev": true }, + "@types/shimmer": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@types/shimmer/-/shimmer-1.0.2.tgz", + "integrity": "sha512-dKkr1bTxbEsFlh2ARpKzcaAmsYixqt9UyCdoEZk8rHyE4iQYcDCyvSjDSf7JUWJHlJiTtbIoQjxKh6ViywqDAg==" + }, "@types/shortid": { "version": "0.0.29", "resolved": "https://registry.npmjs.org/@types/shortid/-/shortid-0.0.29.tgz", @@ -15970,9 +17065,9 @@ "dev": true }, "@types/sinon": { - "version": "10.0.11", - "resolved": "https://registry.npmjs.org/@types/sinon/-/sinon-10.0.11.tgz", - "integrity": "sha512-dmZsHlBsKUtBpHriNjlK0ndlvEh8dcb9uV9Afsbt89QIyydpC7NcR+nWlAhASfy3GHnxTl4FX/aKE7XZUt/B4g==", + "version": "17.0.3", + "resolved": "https://registry.npmjs.org/@types/sinon/-/sinon-17.0.3.tgz", + "integrity": "sha512-j3uovdn8ewky9kRBG19bOwaZbexJu/XjtkHyjvUgt4xfPFz18dcORIMqnYh66Fx3Powhcr85NT5+er3+oViapw==", "dev": true, "requires": { "@types/sinonjs__fake-timers": "*" @@ -15996,16 +17091,16 @@ "integrity": "sha1-EHPEvIJHVK49EM+riKsCN7qWTk0=", "dev": true }, - "@types/uuid": { - "version": "8.3.4", - "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-8.3.4.tgz", - "integrity": "sha512-c/I8ZRb51j+pYGAu5CrFMRxqZ2ke4y2grEBO5AUjgSkSk+qT2Ea+OdWElz/OiMf5MNpn2b17kuVBwZLQJXzihw==", + "@types/tough-cookie": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/@types/tough-cookie/-/tough-cookie-4.0.3.tgz", + "integrity": "sha512-THo502dA5PzG/sfQH+42Lw3fvmYkceefOspdCwpHRul8ik2Jv1K8I5OZz1AT3/rs46kwgMCe9bSBmDLYkkOMGg==", "dev": true }, "@types/vscode": { - "version": "1.75.1", - "resolved": "https://registry.npmjs.org/@types/vscode/-/vscode-1.75.1.tgz", - "integrity": "sha512-emg7wdsTFzdi+elvoyoA+Q8keEautdQHyY5LNmHVM4PTpY8JgOTVADrGVyXGepJ6dVW2OS5/xnLUWh+nZxvdiA==", + "version": "1.100.0", + "resolved": "https://registry.npmjs.org/@types/vscode/-/vscode-1.100.0.tgz", + "integrity": "sha512-4uNyvzHoraXEeCamR3+fzcBlh7Afs4Ifjs4epINyUX/jvdk0uzLnwiDY35UKDKnkCHP5Nu3dljl2H8lR6s+rQw==", "dev": true }, "@types/which": { @@ -16030,178 +17125,219 @@ } }, "@typescript-eslint/eslint-plugin": { - "version": "3.10.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-3.10.1.tgz", - "integrity": "sha512-PQg0emRtzZFWq6PxBcdxRH3QIQiyFO3WCVpRL3fgj5oQS3CDs3AeAKfv4DxNhzn8ITdNJGJ4D3Qw8eAJf3lXeQ==", - "dev": true, - "requires": { - "@typescript-eslint/experimental-utils": "3.10.1", - "debug": "^4.1.1", - "functional-red-black-tree": "^1.0.1", - "regexpp": "^3.0.0", - "semver": "^7.3.2", - "tsutils": "^3.17.1" + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-6.21.0.tgz", + "integrity": "sha512-oy9+hTPCUFpngkEZUSzbf9MxI65wbKFoQYsgPdILTfbUldp5ovUuphZVe4i30emU9M/kP+T64Di0mxl7dSw3MA==", + "dev": true, + "requires": { + "@eslint-community/regexpp": "^4.5.1", + "@typescript-eslint/scope-manager": "6.21.0", + "@typescript-eslint/type-utils": "6.21.0", + "@typescript-eslint/utils": "6.21.0", + "@typescript-eslint/visitor-keys": "6.21.0", + "debug": "^4.3.4", + "graphemer": "^1.4.0", + "ignore": "^5.2.4", + "natural-compare": "^1.4.0", + "semver": "^7.5.4", + "ts-api-utils": "^1.0.1" }, "dependencies": { "debug": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.2.tgz", - "integrity": "sha512-mOp8wKcvj7XxC78zLgw/ZA+6TSgkoE2C/ienthhRD298T7UNwAg9diBpLRxC0mOezLl4B0xV7M0cCO6P/O0Xhw==", + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.6.tgz", + "integrity": "sha512-O/09Bd4Z1fBrU4VzkhFqVgpPzaGbw6Sm9FEkBT1A/YBXQFGuuSxa1dN2nxgxS34JmKXqYx8CZAwEVoJFImUXIg==", "dev": true, "requires": { "ms": "2.1.2" } - }, - "semver": { - "version": "7.3.5", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.5.tgz", - "integrity": "sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==", + } + } + }, + "@typescript-eslint/parser": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-6.21.0.tgz", + "integrity": "sha512-tbsV1jPne5CkFQCgPBcDOt30ItF7aJoZL997JSF7MhGQqOeT3svWRYxiqlfA5RUdlHN6Fi+EI9bxqbdyAUZjYQ==", + "dev": true, + "requires": { + "@typescript-eslint/scope-manager": "6.21.0", + "@typescript-eslint/types": "6.21.0", + "@typescript-eslint/typescript-estree": "6.21.0", + "@typescript-eslint/visitor-keys": "6.21.0", + "debug": "^4.3.4" + }, + "dependencies": { + "debug": { + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.6.tgz", + "integrity": "sha512-O/09Bd4Z1fBrU4VzkhFqVgpPzaGbw6Sm9FEkBT1A/YBXQFGuuSxa1dN2nxgxS34JmKXqYx8CZAwEVoJFImUXIg==", "dev": true, "requires": { - "lru-cache": "^6.0.0" + "ms": "2.1.2" } } } }, - "@typescript-eslint/experimental-utils": { - "version": "3.10.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/experimental-utils/-/experimental-utils-3.10.1.tgz", - "integrity": "sha512-DewqIgscDzmAfd5nOGe4zm6Bl7PKtMG2Ad0KG8CUZAHlXfAKTF9Ol5PXhiMh39yRL2ChRH1cuuUGOcVyyrhQIw==", + "@typescript-eslint/scope-manager": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-6.21.0.tgz", + "integrity": "sha512-OwLUIWZJry80O99zvqXVEioyniJMa+d2GrqpUTqi5/v5D5rOrppJVBPa0yKCblcigC0/aYAzxxqQ1B+DS2RYsg==", "dev": true, "requires": { - "@types/json-schema": "^7.0.3", - "@typescript-eslint/types": "3.10.1", - "@typescript-eslint/typescript-estree": "3.10.1", - "eslint-scope": "^5.0.0", - "eslint-utils": "^2.0.0" + "@typescript-eslint/types": "6.21.0", + "@typescript-eslint/visitor-keys": "6.21.0" } }, - "@typescript-eslint/parser": { - "version": "3.10.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-3.10.1.tgz", - "integrity": "sha512-Ug1RcWcrJP02hmtaXVS3axPPTTPnZjupqhgj+NnZ6BCkwSImWk/283347+x9wN+lqOdK9Eo3vsyiyDHgsmiEJw==", + "@typescript-eslint/type-utils": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-6.21.0.tgz", + "integrity": "sha512-rZQI7wHfao8qMX3Rd3xqeYSMCL3SoiSQLBATSiVKARdFGCYSRvmViieZjqc58jKgs8Y8i9YvVVhRbHSTA4VBag==", "dev": true, "requires": { - "@types/eslint-visitor-keys": "^1.0.0", - "@typescript-eslint/experimental-utils": "3.10.1", - "@typescript-eslint/types": "3.10.1", - "@typescript-eslint/typescript-estree": "3.10.1", - "eslint-visitor-keys": "^1.1.0" + "@typescript-eslint/typescript-estree": "6.21.0", + "@typescript-eslint/utils": "6.21.0", + "debug": "^4.3.4", + "ts-api-utils": "^1.0.1" + }, + "dependencies": { + "debug": { + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.6.tgz", + "integrity": "sha512-O/09Bd4Z1fBrU4VzkhFqVgpPzaGbw6Sm9FEkBT1A/YBXQFGuuSxa1dN2nxgxS34JmKXqYx8CZAwEVoJFImUXIg==", + "dev": true, + "requires": { + "ms": "2.1.2" + } + } } }, "@typescript-eslint/types": { - "version": "3.10.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-3.10.1.tgz", - "integrity": "sha512-+3+FCUJIahE9q0lDi1WleYzjCwJs5hIsbugIgnbB+dSCYUxl8L6PwmsyOPFZde2hc1DlTo/xnkOgiTLSyAbHiQ==", + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-6.21.0.tgz", + "integrity": "sha512-1kFmZ1rOm5epu9NZEZm1kckCDGj5UJEf7P1kliH4LKu/RkwpsfqqGmY2OOcUs18lSlQBKLDYBOGxRVtrMN5lpg==", "dev": true }, "@typescript-eslint/typescript-estree": { - "version": "3.10.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-3.10.1.tgz", - "integrity": "sha512-QbcXOuq6WYvnB3XPsZpIwztBoquEYLXh2MtwVU+kO8jgYCiv4G5xrSP/1wg4tkvrEE+esZVquIPX/dxPlePk1w==", + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-6.21.0.tgz", + "integrity": "sha512-6npJTkZcO+y2/kr+z0hc4HwNfrrP4kNYh57ek7yCNlrBjWQ1Y0OS7jiZTkgumrvkX5HkEKXFZkkdFNkaW2wmUQ==", "dev": true, "requires": { - "@typescript-eslint/types": "3.10.1", - "@typescript-eslint/visitor-keys": "3.10.1", - "debug": "^4.1.1", - "glob": "^7.1.6", - "is-glob": "^4.0.1", - "lodash": "^4.17.15", - "semver": "^7.3.2", - "tsutils": "^3.17.1" + "@typescript-eslint/types": "6.21.0", + "@typescript-eslint/visitor-keys": "6.21.0", + "debug": "^4.3.4", + "globby": "^11.1.0", + "is-glob": "^4.0.3", + "minimatch": "9.0.3", + "semver": "^7.5.4", + "ts-api-utils": "^1.0.1" }, "dependencies": { + "brace-expansion": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.3.tgz", + "integrity": "sha512-MCV/fYJEbqx68aE58kv2cA/kiky1G8vux3OR6/jbS+jIMe/6fJWa0DTzJU7dqijOWYwHi1t29FlfYI9uytqlpA==", + "dev": true, + "requires": { + "balanced-match": "^1.0.0" + } + }, "debug": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.2.tgz", - "integrity": "sha512-mOp8wKcvj7XxC78zLgw/ZA+6TSgkoE2C/ienthhRD298T7UNwAg9diBpLRxC0mOezLl4B0xV7M0cCO6P/O0Xhw==", + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.6.tgz", + "integrity": "sha512-O/09Bd4Z1fBrU4VzkhFqVgpPzaGbw6Sm9FEkBT1A/YBXQFGuuSxa1dN2nxgxS34JmKXqYx8CZAwEVoJFImUXIg==", "dev": true, "requires": { "ms": "2.1.2" } }, - "semver": { - "version": "7.3.5", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.5.tgz", - "integrity": "sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==", + "minimatch": { + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz", + "integrity": "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==", "dev": true, "requires": { - "lru-cache": "^6.0.0" + "brace-expansion": "^2.0.1" } } } }, - "@typescript-eslint/visitor-keys": { - "version": "3.10.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-3.10.1.tgz", - "integrity": "sha512-9JgC82AaQeglebjZMgYR5wgmfUdUc+EitGUUMW8u2nDckaeimzW+VsoLV6FoimPv2id3VQzfjwBxEMVz08ameQ==", + "@typescript-eslint/utils": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-6.21.0.tgz", + "integrity": "sha512-NfWVaC8HP9T8cbKQxHcsJBY5YE1O33+jpMwN45qzWWaPDZgLIbo12toGMWnmhvCpd3sIxkpDw3Wv1B3dYrbDQQ==", "dev": true, "requires": { - "eslint-visitor-keys": "^1.1.0" + "@eslint-community/eslint-utils": "^4.4.0", + "@types/json-schema": "^7.0.12", + "@types/semver": "^7.5.0", + "@typescript-eslint/scope-manager": "6.21.0", + "@typescript-eslint/types": "6.21.0", + "@typescript-eslint/typescript-estree": "6.21.0", + "semver": "^7.5.4" + }, + "dependencies": { + "@types/semver": { + "version": "7.5.8", + "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.8.tgz", + "integrity": "sha512-I8EUhyrgfLrcTkzV3TSsGyl1tSuPrEDzr0yd5m90UgNxQkyDXULk3b6MlQqTCpZpNtWe1K0hzclnZkTcLBe2UQ==", + "dev": true + } } }, - "@ungap/promise-all-settled": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@ungap/promise-all-settled/-/promise-all-settled-1.1.2.tgz", - "integrity": "sha512-sL/cEvJWAnClXw0wHk85/2L0G6Sj8UB0Ctc1TEMbKSsmpRosqhwj9gWgFRZSrBr2f9tiXISwNhCPmlfqUqyb9Q==", - "dev": true - }, - "@vscode/extension-telemetry": { - "version": "0.7.7", - "resolved": "https://registry.npmjs.org/@vscode/extension-telemetry/-/extension-telemetry-0.7.7.tgz", - "integrity": "sha512-uW508BPjkWDBOKvvvSym3ZmGb7kHIiWaAfB/1PHzLz2x9TrC33CfjmFEI+CywIL/jBv4bqZxxjN4tfefB61F+g==", + "@typescript-eslint/visitor-keys": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-6.21.0.tgz", + "integrity": "sha512-JJtkDduxLi9bivAB+cYOVMtbkqdPOhZ+ZI5LC47MIRrDV4Yn2o+ZnW10Nkmr28xRpSpdJ6Sm42Hjf2+REYXm0A==", + "dev": true, "requires": { - "@microsoft/1ds-core-js": "^3.2.9", - "@microsoft/1ds-post-js": "^3.2.9", - "@microsoft/applicationinsights-web-basic": "^2.8.11", - "applicationinsights": "2.5.0" + "@typescript-eslint/types": "6.21.0", + "eslint-visitor-keys": "^3.4.1" } }, - "@vscode/jupyter-lsp-middleware": { - "version": "0.2.50", - "resolved": "https://registry.npmjs.org/@vscode/jupyter-lsp-middleware/-/jupyter-lsp-middleware-0.2.50.tgz", - "integrity": "sha512-oOEpRZOJdKjByRMkUDVdGlQDiEO4/Mjr88u5zqktaS/4h0NtX8Hk6+HNQwENp4ur3Dpu47gD8wOTCrkOWzbHlA==", - "requires": { - "@vscode/lsp-notebook-concat": "^0.1.16", - "fast-myers-diff": "^3.0.1", - "sha.js": "^2.4.11", - "vscode-languageclient": "^8.0.2-next.4", - "vscode-languageserver-protocol": "^3.17.2-next.5", - "vscode-uri": "^3.0.2" - } + "@ungap/structured-clone": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.0.tgz", + "integrity": "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==", + "dev": true }, - "@vscode/lsp-notebook-concat": { - "version": "0.1.16", - "resolved": "https://registry.npmjs.org/@vscode/lsp-notebook-concat/-/lsp-notebook-concat-0.1.16.tgz", - "integrity": "sha512-jN2ut22GR/xelxHx2W9U+uZoylHGCezsNmsMYn20LgVHTcJMGL+4bL5PJeh63yo6P5XjAPtoeeymvp5EafJV+w==", + "@vscode/extension-telemetry": { + "version": "0.8.4", + "resolved": "https://registry.npmjs.org/@vscode/extension-telemetry/-/extension-telemetry-0.8.4.tgz", + "integrity": "sha512-UqM9+KZDDK3MyoHTsg6XNM+XO6pweQxzCpqJz33BoBEYAGsbBviRYcVpJglgay2oReuDD2pOI1Nio3BKNDLhWA==", "requires": { - "object-hash": "^3.0.0", - "vscode-languageserver-protocol": "^3.17.2-next.5", - "vscode-uri": "^3.0.2" + "@microsoft/1ds-core-js": "^3.2.13", + "@microsoft/1ds-post-js": "^3.2.13", + "@microsoft/applicationinsights-web-basic": "^3.0.2", + "applicationinsights": "^2.7.1" } }, "@vscode/test-electron": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/@vscode/test-electron/-/test-electron-2.1.3.tgz", - "integrity": "sha512-ps/yJ/9ToUZtR1dHfWi1mDXtep1VoyyrmGKC3UnIbScToRQvbUjyy1VMqnMEW3EpMmC3g7+pyThIPtPyCLHyow==", + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/@vscode/test-electron/-/test-electron-2.3.8.tgz", + "integrity": "sha512-b4aZZsBKtMGdDljAsOPObnAi7+VWIaYl3ylCz1jTs+oV6BZ4TNHcVNC3xUn0azPeszBmwSBDQYfFESIaUQnrOg==", "dev": true, "requires": { "http-proxy-agent": "^4.0.1", "https-proxy-agent": "^5.0.0", - "rimraf": "^3.0.2", - "unzipper": "^0.10.11" + "jszip": "^3.10.1", + "semver": "^7.5.2" } }, "@vscode/vsce": { - "version": "2.19.0", - "resolved": "https://registry.npmjs.org/@vscode/vsce/-/vsce-2.19.0.tgz", - "integrity": "sha512-dAlILxC5ggOutcvJY24jxz913wimGiUrHaPkk16Gm9/PGFbz1YezWtrXsTKUtJws4fIlpX2UIlVlVESWq8lkfQ==", + "version": "2.27.0", + "resolved": "https://registry.npmjs.org/@vscode/vsce/-/vsce-2.27.0.tgz", + "integrity": "sha512-FFUMBVSyyjjJpWszwqk7d4U3YllY8FdWslbUDMRki1x4ZjA3Z0hmRMfypWrjP9sptbSR9nyPFU4uqjhy2qRB/w==", "dev": true, "requires": { - "azure-devops-node-api": "^11.0.1", + "@azure/identity": "^4.1.0", + "@vscode/vsce-sign": "^2.0.0", + "azure-devops-node-api": "^12.5.0", "chalk": "^2.4.2", "cheerio": "^1.0.0-rc.9", - "commander": "^6.1.0", + "cockatiel": "^3.1.2", + "commander": "^6.2.1", + "form-data": "^4.0.0", "glob": "^7.0.6", "hosted-git-info": "^4.0.2", "jsonc-parser": "^3.2.0", @@ -16212,7 +17348,7 @@ "minimatch": "^3.0.3", "parse-semver": "^1.1.1", "read": "^1.0.7", - "semver": "^5.1.0", + "semver": "^7.5.2", "tmp": "^0.2.1", "typed-rest-client": "^1.8.4", "url-join": "^4.0.1", @@ -16237,168 +17373,239 @@ } }, "minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", "dev": true, "requires": { "brace-expansion": "^1.1.7" } - }, - "tmp": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.1.tgz", - "integrity": "sha512-76SUhtfqR2Ijn+xllcI5P1oyannHNHByD80W1q447gU3mp9G9PSpGdWmjUOHRDPiHYacIk66W7ubDTuPF3BEtQ==", - "dev": true, - "requires": { - "rimraf": "^3.0.0" - } } } }, + "@vscode/vsce-sign": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@vscode/vsce-sign/-/vsce-sign-2.0.4.tgz", + "integrity": "sha512-0uL32egStKYfy60IqnynAChMTbL0oqpqk0Ew0YHiIb+fayuGZWADuIPHWUcY1GCnAA+VgchOPDMxnc2R3XGWEA==", + "dev": true, + "requires": { + "@vscode/vsce-sign-alpine-arm64": "2.0.2", + "@vscode/vsce-sign-alpine-x64": "2.0.2", + "@vscode/vsce-sign-darwin-arm64": "2.0.2", + "@vscode/vsce-sign-darwin-x64": "2.0.2", + "@vscode/vsce-sign-linux-arm": "2.0.2", + "@vscode/vsce-sign-linux-arm64": "2.0.2", + "@vscode/vsce-sign-linux-x64": "2.0.2", + "@vscode/vsce-sign-win32-arm64": "2.0.2", + "@vscode/vsce-sign-win32-x64": "2.0.2" + } + }, + "@vscode/vsce-sign-alpine-arm64": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@vscode/vsce-sign-alpine-arm64/-/vsce-sign-alpine-arm64-2.0.2.tgz", + "integrity": "sha512-E80YvqhtZCLUv3YAf9+tIbbqoinWLCO/B3j03yQPbjT3ZIHCliKZlsy1peNc4XNZ5uIb87Jn0HWx/ZbPXviuAQ==", + "dev": true, + "optional": true + }, + "@vscode/vsce-sign-alpine-x64": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@vscode/vsce-sign-alpine-x64/-/vsce-sign-alpine-x64-2.0.2.tgz", + "integrity": "sha512-n1WC15MSMvTaeJ5KjWCzo0nzjydwxLyoHiMJHu1Ov0VWTZiddasmOQHekA47tFRycnt4FsQrlkSCTdgHppn6bw==", + "dev": true, + "optional": true + }, + "@vscode/vsce-sign-darwin-arm64": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@vscode/vsce-sign-darwin-arm64/-/vsce-sign-darwin-arm64-2.0.2.tgz", + "integrity": "sha512-rz8F4pMcxPj8fjKAJIfkUT8ycG9CjIp888VY/6pq6cuI2qEzQ0+b5p3xb74CJnBbSC0p2eRVoe+WgNCAxCLtzQ==", + "dev": true, + "optional": true + }, + "@vscode/vsce-sign-darwin-x64": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@vscode/vsce-sign-darwin-x64/-/vsce-sign-darwin-x64-2.0.2.tgz", + "integrity": "sha512-MCjPrQ5MY/QVoZ6n0D92jcRb7eYvxAujG/AH2yM6lI0BspvJQxp0o9s5oiAM9r32r9tkLpiy5s2icsbwefAQIw==", + "dev": true, + "optional": true + }, + "@vscode/vsce-sign-linux-arm": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@vscode/vsce-sign-linux-arm/-/vsce-sign-linux-arm-2.0.2.tgz", + "integrity": "sha512-Fkb5jpbfhZKVw3xwR6t7WYfwKZktVGNXdg1m08uEx1anO0oUPUkoQRsNm4QniL3hmfw0ijg00YA6TrxCRkPVOQ==", + "dev": true, + "optional": true + }, + "@vscode/vsce-sign-linux-arm64": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@vscode/vsce-sign-linux-arm64/-/vsce-sign-linux-arm64-2.0.2.tgz", + "integrity": "sha512-Ybeu7cA6+/koxszsORXX0OJk9N0GgfHq70Wqi4vv2iJCZvBrOWwcIrxKjvFtwyDgdeQzgPheH5nhLVl5eQy7WA==", + "dev": true, + "optional": true + }, + "@vscode/vsce-sign-linux-x64": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@vscode/vsce-sign-linux-x64/-/vsce-sign-linux-x64-2.0.2.tgz", + "integrity": "sha512-NsPPFVtLaTlVJKOiTnO8Cl78LZNWy0Q8iAg+LlBiCDEgC12Gt4WXOSs2pmcIjDYzj2kY4NwdeN1mBTaujYZaPg==", + "dev": true, + "optional": true + }, + "@vscode/vsce-sign-win32-arm64": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@vscode/vsce-sign-win32-arm64/-/vsce-sign-win32-arm64-2.0.2.tgz", + "integrity": "sha512-wPs848ymZ3Ny+Y1Qlyi7mcT6VSigG89FWQnp2qRYCyMhdJxOpA4lDwxzlpL8fG6xC8GjQjGDkwbkWUcCobvksQ==", + "dev": true, + "optional": true + }, + "@vscode/vsce-sign-win32-x64": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@vscode/vsce-sign-win32-x64/-/vsce-sign-win32-x64-2.0.2.tgz", + "integrity": "sha512-pAiRN6qSAhDM5SVOIxgx+2xnoVUePHbRNC7OD2aOR3WltTKxxF25OfpK8h8UQ7A0BuRkSgREbB59DBlFk4iAeg==", + "dev": true, + "optional": true + }, "@webassemblyjs/ast": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.11.1.tgz", - "integrity": "sha512-ukBh14qFLjxTQNTXocdyksN5QdM28S1CxHt2rdskFyL+xFV7VremuBLVbmCePj+URalXBENx/9Lm7lnhihtCSw==", + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.14.1.tgz", + "integrity": "sha512-nuBEDgQfm1ccRp/8bCQrx1frohyufl4JlbMMZ4P1wpeOfDhF6FQkxZJ1b/e+PLwr6X1Nhw6OLme5usuBWYBvuQ==", "dev": true, "requires": { - "@webassemblyjs/helper-numbers": "1.11.1", - "@webassemblyjs/helper-wasm-bytecode": "1.11.1" + "@webassemblyjs/helper-numbers": "1.13.2", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2" } }, "@webassemblyjs/floating-point-hex-parser": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.11.1.tgz", - "integrity": "sha512-iGRfyc5Bq+NnNuX8b5hwBrRjzf0ocrJPI6GWFodBFzmFnyvrQ83SHKhmilCU/8Jv67i4GJZBMhEzltxzcNagtQ==", + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.13.2.tgz", + "integrity": "sha512-6oXyTOzbKxGH4steLbLNOu71Oj+C8Lg34n6CqRvqfS2O71BxY6ByfMDRhBytzknj9yGUPVJ1qIKhRlAwO1AovA==", "dev": true }, "@webassemblyjs/helper-api-error": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.11.1.tgz", - "integrity": "sha512-RlhS8CBCXfRUR/cwo2ho9bkheSXG0+NwooXcc3PAILALf2QLdFyj7KGsKRbVc95hZnhnERon4kW/D3SZpp6Tcg==", + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.13.2.tgz", + "integrity": "sha512-U56GMYxy4ZQCbDZd6JuvvNV/WFildOjsaWD3Tzzvmw/mas3cXzRJPMjP83JqEsgSbyrmaGjBfDtV7KDXV9UzFQ==", "dev": true }, "@webassemblyjs/helper-buffer": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.11.1.tgz", - "integrity": "sha512-gwikF65aDNeeXa8JxXa2BAk+REjSyhrNC9ZwdT0f8jc4dQQeDQ7G4m0f2QCLPJiMTTO6wfDmRmj/pW0PsUvIcA==", + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.14.1.tgz", + "integrity": "sha512-jyH7wtcHiKssDtFPRB+iQdxlDf96m0E39yb0k5uJVhFGleZFoNw1c4aeIcVUPPbXUVJ94wwnMOAqUHyzoEPVMA==", "dev": true }, "@webassemblyjs/helper-numbers": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-numbers/-/helper-numbers-1.11.1.tgz", - "integrity": "sha512-vDkbxiB8zfnPdNK9Rajcey5C0w+QJugEglN0of+kmO8l7lDb77AnlKYQF7aarZuCrv+l0UvqL+68gSDr3k9LPQ==", + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-numbers/-/helper-numbers-1.13.2.tgz", + "integrity": "sha512-FE8aCmS5Q6eQYcV3gI35O4J789wlQA+7JrqTTpJqn5emA4U2hvwJmvFRC0HODS+3Ye6WioDklgd6scJ3+PLnEA==", "dev": true, "requires": { - "@webassemblyjs/floating-point-hex-parser": "1.11.1", - "@webassemblyjs/helper-api-error": "1.11.1", + "@webassemblyjs/floating-point-hex-parser": "1.13.2", + "@webassemblyjs/helper-api-error": "1.13.2", "@xtuc/long": "4.2.2" } }, "@webassemblyjs/helper-wasm-bytecode": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.11.1.tgz", - "integrity": "sha512-PvpoOGiJwXeTrSf/qfudJhwlvDQxFgelbMqtq52WWiXC6Xgg1IREdngmPN3bs4RoO83PnL/nFrxucXj1+BX62Q==", + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.13.2.tgz", + "integrity": "sha512-3QbLKy93F0EAIXLh0ogEVR6rOubA9AoZ+WRYhNbFyuB70j3dRdwH9g+qXhLAO0kiYGlg3TxDV+I4rQTr/YNXkA==", "dev": true }, "@webassemblyjs/helper-wasm-section": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.11.1.tgz", - "integrity": "sha512-10P9No29rYX1j7F3EVPX3JvGPQPae+AomuSTPiF9eBQeChHI6iqjMIwR9JmOJXwpnn/oVGDk7I5IlskuMwU/pg==", + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.14.1.tgz", + "integrity": "sha512-ds5mXEqTJ6oxRoqjhWDU83OgzAYjwsCV8Lo/N+oRsNDmx/ZDpqalmrtgOMkHwxsG0iI//3BwWAErYRHtgn0dZw==", "dev": true, "requires": { - "@webassemblyjs/ast": "1.11.1", - "@webassemblyjs/helper-buffer": "1.11.1", - "@webassemblyjs/helper-wasm-bytecode": "1.11.1", - "@webassemblyjs/wasm-gen": "1.11.1" + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-buffer": "1.14.1", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/wasm-gen": "1.14.1" } }, "@webassemblyjs/ieee754": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.11.1.tgz", - "integrity": "sha512-hJ87QIPtAMKbFq6CGTkZYJivEwZDbQUgYd3qKSadTNOhVY7p+gfP6Sr0lLRVTaG1JjFj+r3YchoqRYxNH3M0GQ==", + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.13.2.tgz", + "integrity": "sha512-4LtOzh58S/5lX4ITKxnAK2USuNEvpdVV9AlgGQb8rJDHaLeHciwG4zlGr0j/SNWlr7x3vO1lDEsuePvtcDNCkw==", "dev": true, "requires": { "@xtuc/ieee754": "^1.2.0" } }, "@webassemblyjs/leb128": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.11.1.tgz", - "integrity": "sha512-BJ2P0hNZ0u+Th1YZXJpzW6miwqQUGcIHT1G/sf72gLVD9DZ5AdYTqPNbHZh6K1M5VmKvFXwGSWZADz+qBWxeRw==", + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.13.2.tgz", + "integrity": "sha512-Lde1oNoIdzVzdkNEAWZ1dZ5orIbff80YPdHx20mrHwHrVNNTjNr8E3xz9BdpcGqRQbAEa+fkrCb+fRFTl/6sQw==", "dev": true, "requires": { "@xtuc/long": "4.2.2" } }, "@webassemblyjs/utf8": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.11.1.tgz", - "integrity": "sha512-9kqcxAEdMhiwQkHpkNiorZzqpGrodQQ2IGrHHxCy+Ozng0ofyMA0lTqiLkVs1uzTRejX+/O0EOT7KxqVPuXosQ==", + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.13.2.tgz", + "integrity": "sha512-3NQWGjKTASY1xV5m7Hr0iPeXD9+RDobLll3T9d2AO+g3my8xy5peVyjSag4I50mR1bBSN/Ct12lo+R9tJk0NZQ==", "dev": true }, "@webassemblyjs/wasm-edit": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.11.1.tgz", - "integrity": "sha512-g+RsupUC1aTHfR8CDgnsVRVZFJqdkFHpsHMfJuWQzWU3tvnLC07UqHICfP+4XyL2tnr1amvl1Sdp06TnYCmVkA==", + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.14.1.tgz", + "integrity": "sha512-RNJUIQH/J8iA/1NzlE4N7KtyZNHi3w7at7hDjvRNm5rcUXa00z1vRz3glZoULfJ5mpvYhLybmVcwcjGrC1pRrQ==", "dev": true, "requires": { - "@webassemblyjs/ast": "1.11.1", - "@webassemblyjs/helper-buffer": "1.11.1", - "@webassemblyjs/helper-wasm-bytecode": "1.11.1", - "@webassemblyjs/helper-wasm-section": "1.11.1", - "@webassemblyjs/wasm-gen": "1.11.1", - "@webassemblyjs/wasm-opt": "1.11.1", - "@webassemblyjs/wasm-parser": "1.11.1", - "@webassemblyjs/wast-printer": "1.11.1" + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-buffer": "1.14.1", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/helper-wasm-section": "1.14.1", + "@webassemblyjs/wasm-gen": "1.14.1", + "@webassemblyjs/wasm-opt": "1.14.1", + "@webassemblyjs/wasm-parser": "1.14.1", + "@webassemblyjs/wast-printer": "1.14.1" } }, "@webassemblyjs/wasm-gen": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.11.1.tgz", - "integrity": "sha512-F7QqKXwwNlMmsulj6+O7r4mmtAlCWfO/0HdgOxSklZfQcDu0TpLiD1mRt/zF25Bk59FIjEuGAIyn5ei4yMfLhA==", + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.14.1.tgz", + "integrity": "sha512-AmomSIjP8ZbfGQhumkNvgC33AY7qtMCXnN6bL2u2Js4gVCg8fp735aEiMSBbDR7UQIj90n4wKAFUSEd0QN2Ukg==", "dev": true, "requires": { - "@webassemblyjs/ast": "1.11.1", - "@webassemblyjs/helper-wasm-bytecode": "1.11.1", - "@webassemblyjs/ieee754": "1.11.1", - "@webassemblyjs/leb128": "1.11.1", - "@webassemblyjs/utf8": "1.11.1" + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/ieee754": "1.13.2", + "@webassemblyjs/leb128": "1.13.2", + "@webassemblyjs/utf8": "1.13.2" } }, "@webassemblyjs/wasm-opt": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.11.1.tgz", - "integrity": "sha512-VqnkNqnZlU5EB64pp1l7hdm3hmQw7Vgqa0KF/KCNO9sIpI6Fk6brDEiX+iCOYrvMuBWDws0NkTOxYEb85XQHHw==", + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.14.1.tgz", + "integrity": "sha512-PTcKLUNvBqnY2U6E5bdOQcSM+oVP/PmrDY9NzowJjislEjwP/C4an2303MCVS2Mg9d3AJpIGdUFIQQWbPds0Sw==", "dev": true, "requires": { - "@webassemblyjs/ast": "1.11.1", - "@webassemblyjs/helper-buffer": "1.11.1", - "@webassemblyjs/wasm-gen": "1.11.1", - "@webassemblyjs/wasm-parser": "1.11.1" + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-buffer": "1.14.1", + "@webassemblyjs/wasm-gen": "1.14.1", + "@webassemblyjs/wasm-parser": "1.14.1" } }, "@webassemblyjs/wasm-parser": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.11.1.tgz", - "integrity": "sha512-rrBujw+dJu32gYB7/Lup6UhdkPx9S9SnobZzRVL7VcBH9Bt9bCBLEuX/YXOOtBsOZ4NQrRykKhffRWHvigQvOA==", + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.14.1.tgz", + "integrity": "sha512-JLBl+KZ0R5qB7mCnud/yyX08jWFw5MsoalJ1pQ4EdFlgj9VdXKGuENGsiCIjegI1W7p91rUlcB/LB5yRJKNTcQ==", "dev": true, "requires": { - "@webassemblyjs/ast": "1.11.1", - "@webassemblyjs/helper-api-error": "1.11.1", - "@webassemblyjs/helper-wasm-bytecode": "1.11.1", - "@webassemblyjs/ieee754": "1.11.1", - "@webassemblyjs/leb128": "1.11.1", - "@webassemblyjs/utf8": "1.11.1" + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-api-error": "1.13.2", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/ieee754": "1.13.2", + "@webassemblyjs/leb128": "1.13.2", + "@webassemblyjs/utf8": "1.13.2" } }, "@webassemblyjs/wast-printer": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.11.1.tgz", - "integrity": "sha512-IQboUWM4eKzWW+N/jij2sRatKMh99QEelo3Eb2q0qXkvPRISAj8Qxtmw5itwqK+TTkBuUIE45AxYPToqPtL5gg==", + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.14.1.tgz", + "integrity": "sha512-kPSSXE6De1XOR820C90RIo2ogvZG+c3KiHzqUoO/F34Y2shGzesfqv7o57xrxovZJH/MetF5UjroJ/R/3isoiw==", "dev": true, "requires": { - "@webassemblyjs/ast": "1.11.1", + "@webassemblyjs/ast": "1.14.1", "@xtuc/long": "4.2.2" } }, @@ -16438,15 +17645,20 @@ "dev": true }, "acorn": { - "version": "8.8.2", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.8.2.tgz", - "integrity": "sha512-xjIYgE8HBrkpd/sJqOGNspf8uHG+NOHGOw6a/Urj8taM2EXfdNAH2oFcPeIFfsv3+kz/mJrS5VuMqbNLjCa2vw==", - "dev": true + "version": "8.15.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", + "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==" }, "acorn-import-assertions": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/acorn-import-assertions/-/acorn-import-assertions-1.8.0.tgz", - "integrity": "sha512-m7VZ3jwz4eK6A4Vtt8Ew1/mNbP24u0FhdyfA7fSvnJR6LMdfOYnmuIrrJAgrYfYJ10F/otaHTtrtrtmHdMNzEw==", + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/acorn-import-assertions/-/acorn-import-assertions-1.9.0.tgz", + "integrity": "sha512-cmMwop9x+8KFhxvKrKfPYmN6/pKTYYHBqLa0DfvVZcKMJWNyWLnaqND7dx/qn66R7ewM1UX5XMaDVP5wlVTaVA==", + "requires": {} + }, + "acorn-import-phases": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/acorn-import-phases/-/acorn-import-phases-1.0.4.tgz", + "integrity": "sha512-wKmbr/DDiIXzEOiWrTTUcDm24kQ2vGfZQvM2fwg2vXqR5uW6aapr7ObPtj1th32b9u90/Pf4AItvdTh42fBmVQ==", "dev": true, "requires": {} }, @@ -16492,9 +17704,9 @@ } }, "ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz", + "integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==", "dev": true, "requires": { "fast-deep-equal": "^3.1.1", @@ -16503,6 +17715,35 @@ "uri-js": "^4.2.2" } }, + "ajv-formats": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz", + "integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==", + "dev": true, + "requires": { + "ajv": "^8.0.0" + }, + "dependencies": { + "ajv": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz", + "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==", + "dev": true, + "requires": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + } + }, + "json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true + } + } + }, "ajv-keywords": { "version": "3.5.2", "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", @@ -16519,15 +17760,6 @@ "ansi-wrap": "^0.1.0" } }, - "ansi-gray": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/ansi-gray/-/ansi-gray-0.1.1.tgz", - "integrity": "sha1-KWLPVOyXksSFEKPetSRDaGHvclE=", - "dev": true, - "requires": { - "ansi-wrap": "0.1.0" - } - }, "ansi-regex": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", @@ -16550,24 +17782,13 @@ "dev": true }, "anymatch": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-2.0.0.tgz", - "integrity": "sha512-5teOsQWABXHHBFP9y3skS5P3d/WfWXpv3FUpy+LorMrNYaT9pI4oLMQX7jzQ2KklNpGpWHzdCXTDT2Y3XGlZBw==", + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", "dev": true, "requires": { - "micromatch": "^3.1.4", - "normalize-path": "^2.1.1" - }, - "dependencies": { - "normalize-path": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-2.1.1.tgz", - "integrity": "sha1-GrKLVW4Zg2Oowab35vogE3/mrtk=", - "dev": true, - "requires": { - "remove-trailing-separator": "^1.0.1" - } - } + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" } }, "append-buffer": { @@ -16597,30 +17818,25 @@ } }, "applicationinsights": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/applicationinsights/-/applicationinsights-2.5.0.tgz", - "integrity": "sha512-6kIFmpANRok+6FhCOmO7ZZ/mh7fdNKn17BaT13cg/RV5roLPJlA6q8srWexayHd3MPcwMb9072e8Zp0P47s/pw==", + "version": "2.7.3", + "resolved": "https://registry.npmjs.org/applicationinsights/-/applicationinsights-2.7.3.tgz", + "integrity": "sha512-JY8+kTEkjbA+kAVNWDtpfW2lqsrDALfDXuxOs74KLPu2y13fy/9WB52V4LfYVTVcW1/jYOXjTxNS2gPZIDh1iw==", "requires": { - "@azure/core-auth": "^1.4.0", - "@azure/core-rest-pipeline": "^1.10.0", + "@azure/core-auth": "^1.5.0", + "@azure/core-rest-pipeline": "1.10.1", + "@azure/core-util": "1.2.0", + "@azure/opentelemetry-instrumentation-azure-sdk": "^1.0.0-beta.5", "@microsoft/applicationinsights-web-snippet": "^1.0.1", - "@opentelemetry/api": "^1.0.4", - "@opentelemetry/core": "^1.0.1", - "@opentelemetry/sdk-trace-base": "^1.0.1", - "@opentelemetry/semantic-conventions": "^1.0.1", + "@opentelemetry/api": "^1.4.1", + "@opentelemetry/core": "^1.15.2", + "@opentelemetry/sdk-trace-base": "^1.15.2", + "@opentelemetry/semantic-conventions": "^1.15.2", "cls-hooked": "^4.2.2", "continuation-local-storage": "^3.2.1", - "diagnostic-channel": "1.1.0", - "diagnostic-channel-publishers": "1.0.5" + "diagnostic-channel": "1.1.1", + "diagnostic-channel-publishers": "1.0.7" } }, - "aproba": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/aproba/-/aproba-1.2.0.tgz", - "integrity": "sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw==", - "dev": true, - "optional": true - }, "arch": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/arch/-/arch-2.2.0.tgz", @@ -16629,7 +17845,7 @@ "archive-type": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/archive-type/-/archive-type-4.0.0.tgz", - "integrity": "sha1-+S5yIzBW38aWlHJ0nCZ72wRrHXA=", + "integrity": "sha512-zV4Ky0v1F8dBrdYElwTvQhweQ0P7Kwc1aluqJsYtOBP01jXcWCyW2IEfI1YiqsG+Iy7ZR+o5LF1N+PGECBxHWA==", "dev": true, "requires": { "file-type": "^4.2.0" @@ -16638,7 +17854,7 @@ "file-type": { "version": "4.4.0", "resolved": "https://registry.npmjs.org/file-type/-/file-type-4.4.0.tgz", - "integrity": "sha1-G2AOX8ofvcboDApwxxyNul95BsU=", + "integrity": "sha512-f2UbFQEk7LXgWpi5ntcO86OeA/cC80fuDDDaX/fZ2ZGel+AF7leRQqBBW1eJNiiQkrZlAoM6P+VYP5P6bOlDEQ==", "dev": true } } @@ -16649,17 +17865,6 @@ "integrity": "sha1-+cjBN1fMHde8N5rHeyxipcKGjEA=", "dev": true }, - "are-we-there-yet": { - "version": "1.1.7", - "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-1.1.7.tgz", - "integrity": "sha512-nxwy40TuMiUGqMyRHgCSWZ9FM4VAoRP4xUYSTv5ImRog+h9yISPbVH7H8fASCIzYn9wlEv4zvFL7uKDMCFQm3g==", - "dev": true, - "optional": true, - "requires": { - "delegates": "^1.0.0", - "readable-stream": "^2.0.6" - } - }, "arg": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.0.tgz", @@ -16681,147 +17886,106 @@ "integrity": "sha1-1kYQdP6/7HHn4VI1dhoyml3HxSA=", "dev": true }, - "arr-filter": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/arr-filter/-/arr-filter-1.1.2.tgz", - "integrity": "sha1-Q/3d0JHo7xGqTEXZzcGOLf8XEe4=", - "dev": true, - "requires": { - "make-iterator": "^1.0.0" - } - }, - "arr-flatten": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/arr-flatten/-/arr-flatten-1.1.0.tgz", - "integrity": "sha512-L3hKV5R/p5o81R7O02IGnwpDmkp6E982XhtbuwSe3O4qOtMMMtodicASA1Cny2U+aCXcNpml+m4dPsvsJ3jatg==", - "dev": true - }, - "arr-map": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/arr-map/-/arr-map-2.0.2.tgz", - "integrity": "sha1-Onc0X/wc814qkYJWAfnljy4kysQ=", - "dev": true, - "requires": { - "make-iterator": "^1.0.0" - } - }, "arr-union": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/arr-union/-/arr-union-3.1.0.tgz", "integrity": "sha1-45sJrqne+Gao8gbiiK9jkZuuOcQ=", "dev": true }, + "array-buffer-byte-length": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.1.tgz", + "integrity": "sha512-ahC5W1xgou+KTXix4sAO8Ki12Q+jf4i0+tmk3sC+zgcynshkHxzpXdImBehiUYKKKDwvfFiJl1tZt6ewscS1Mg==", + "dev": true, + "requires": { + "call-bind": "^1.0.5", + "is-array-buffer": "^3.0.4" + } + }, "array-each": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/array-each/-/array-each-1.0.1.tgz", - "integrity": "sha1-p5SvDAWrF1KEbudTofIRoFugxE8=", + "integrity": "sha512-zHjL5SZa68hkKHBFBK6DJCTtr9sfTCPCaph/L7tMSLcTFgy+zX7E+6q5UArbtOtMBCtxdICpfTCspRse+ywyXA==", "dev": true }, "array-includes": { - "version": "3.1.4", - "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.4.tgz", - "integrity": "sha512-ZTNSQkmWumEbiHO2GF4GmWxYVTiQyJy2XOTa15sdQSrvKn7l+180egQMqlrMOUMCyLMD7pmyQe4mMDUT6Behrw==", + "version": "3.1.8", + "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.8.tgz", + "integrity": "sha512-itaWrbYbqpGXkGhZPGUulwnhVf5Hpy1xiCFsGqyIGglbBxmG5vSjxQen3/WGOjPpNEv1RtBLKxbmVXm8HpJStQ==", "dev": true, "requires": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.3", - "es-abstract": "^1.19.1", - "get-intrinsic": "^1.1.1", + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2", + "es-object-atoms": "^1.0.0", + "get-intrinsic": "^1.2.4", "is-string": "^1.0.7" } }, - "array-initial": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/array-initial/-/array-initial-1.1.0.tgz", - "integrity": "sha1-L6dLJnOTccOUe9enrcc74zSz15U=", - "dev": true, - "requires": { - "array-slice": "^1.0.0", - "is-number": "^4.0.0" - }, - "dependencies": { - "is-number": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-4.0.0.tgz", - "integrity": "sha512-rSklcAIlf1OmFdyAqbnWTLVelsQ58uvZ66S/ZyawjWqIviTWCjg2PzVGw8WUA+nNuPTqb4wgA+NszrJ+08LlgQ==", - "dev": true - } - } - }, - "array-last": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/array-last/-/array-last-1.3.0.tgz", - "integrity": "sha512-eOCut5rXlI6aCOS7Z7kCplKRKyiFQ6dHFBem4PwlwKeNFk2/XxTrhRh5T9PyaEWGy/NHTZWbY+nsZlNFJu9rYg==", - "dev": true, - "requires": { - "is-number": "^4.0.0" - }, - "dependencies": { - "is-number": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-4.0.0.tgz", - "integrity": "sha512-rSklcAIlf1OmFdyAqbnWTLVelsQ58uvZ66S/ZyawjWqIviTWCjg2PzVGw8WUA+nNuPTqb4wgA+NszrJ+08LlgQ==", - "dev": true - } - } - }, "array-slice": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/array-slice/-/array-slice-1.1.0.tgz", "integrity": "sha512-B1qMD3RBP7O8o0H2KbrXDyB0IccejMF15+87Lvlor12ONPRHP6gTjXMNkt/d3ZuOGbAe66hFmaCfECI24Ufp6w==", "dev": true }, - "array-sort": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/array-sort/-/array-sort-1.0.0.tgz", - "integrity": "sha512-ihLeJkonmdiAsD7vpgN3CRcx2J2S0TiYW+IS/5zHBI7mKUq3ySvBdzzBfD236ubDBQFiiyG3SWCPc+msQ9KoYg==", - "dev": true, - "requires": { - "default-compare": "^1.0.0", - "get-value": "^2.0.6", - "kind-of": "^5.0.2" - }, - "dependencies": { - "kind-of": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-5.1.0.tgz", - "integrity": "sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==", - "dev": true - } - } - }, "array-union": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", "dev": true }, - "array-unique": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.3.2.tgz", - "integrity": "sha1-qJS3XUvE9s1nnvMkSp/Y9Gri1Cg=", - "dev": true + "array.prototype.findlastindex": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/array.prototype.findlastindex/-/array.prototype.findlastindex-1.2.5.tgz", + "integrity": "sha512-zfETvRFA8o7EiNn++N5f/kaCw221hrpGsDmcpndVupkPzEc1Wuf3VgC0qby1BbHs7f5DVYjgtEU2LLh5bqeGfQ==", + "dev": true, + "requires": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "es-shim-unscopables": "^1.0.2" + } }, "array.prototype.flat": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.2.5.tgz", - "integrity": "sha512-KaYU+S+ndVqyUnignHftkwc58o3uVU1jzczILJ1tN2YaIZpFIKBiP/x/j97E5MVPsaCloPbqWLB/8qCTVvT2qg==", + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.3.2.tgz", + "integrity": "sha512-djYB+Zx2vLewY8RWlNCUdHjDXs2XOgm602S9E7P/UpHgfeHL00cRiIF+IN/G/aUJ7kGPb6yO/ErDI5V2s8iycA==", "dev": true, "requires": { "call-bind": "^1.0.2", - "define-properties": "^1.1.3", - "es-abstract": "^1.19.0" + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1", + "es-shim-unscopables": "^1.0.0" } }, "array.prototype.flatmap": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/array.prototype.flatmap/-/array.prototype.flatmap-1.2.5.tgz", - "integrity": "sha512-08u6rVyi1Lj7oqWbS9nUxliETrtIROT4XGTA4D/LWGten6E3ocm7cy9SIrmNHOL5XVbVuckUp3X6Xyg8/zpvHA==", + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/array.prototype.flatmap/-/array.prototype.flatmap-1.3.2.tgz", + "integrity": "sha512-Ewyx0c9PmpcsByhSW4r+9zDU7sGjFc86qf/kKtuSCRdhfbk0SNLLkaT5qvcHnRGgc5NP/ly/y+qkXkqONX54CQ==", "dev": true, "requires": { - "call-bind": "^1.0.0", - "define-properties": "^1.1.3", - "es-abstract": "^1.19.0" + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1", + "es-shim-unscopables": "^1.0.0" + } + }, + "arraybuffer.prototype.slice": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.3.tgz", + "integrity": "sha512-bMxMKAjg13EBSVscxTaYA4mRc5t1UAXa2kXiGTNfZ079HIWXEkKmkgFrh/nJqamaLSrXO5H4WFFkPEaLJWbs3A==", + "dev": true, + "requires": { + "array-buffer-byte-length": "^1.0.1", + "call-bind": "^1.0.5", + "define-properties": "^1.2.1", + "es-abstract": "^1.22.3", + "es-errors": "^1.2.1", + "get-intrinsic": "^1.2.3", + "is-array-buffer": "^3.0.4", + "is-shared-array-buffer": "^1.0.2" } }, "asn1.js": { @@ -16880,12 +18044,6 @@ "integrity": "sha1-9wtzXGvKGlycItmCw+Oef+ujva0=", "dev": true }, - "astral-regex": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-2.0.0.tgz", - "integrity": "sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==", - "dev": true - }, "async": { "version": "3.2.3", "resolved": "https://registry.npmjs.org/async/-/async-3.2.3.tgz", @@ -16893,23 +18051,16 @@ "dev": true }, "async-done": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/async-done/-/async-done-1.3.2.tgz", - "integrity": "sha512-uYkTP8dw2og1tu1nmza1n1CMW0qb8gWWlwqMmLb7MhBVs4BXrFziT6HXUd+/RlRA/i4H9AkofYloUbs1fwMqlw==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/async-done/-/async-done-2.0.0.tgz", + "integrity": "sha512-j0s3bzYq9yKIVLKGE/tWlCpa3PfFLcrDZLTSVdnnCTGagXuXBJO4SsY9Xdk/fQBirCkH4evW5xOeJXqlAQFdsw==", "dev": true, "requires": { - "end-of-stream": "^1.1.0", - "once": "^1.3.2", - "process-nextick-args": "^2.0.0", - "stream-exhaust": "^1.0.1" + "end-of-stream": "^1.4.4", + "once": "^1.4.0", + "stream-exhaust": "^1.0.2" } }, - "async-each": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/async-each/-/async-each-1.0.3.tgz", - "integrity": "sha512-z/WhQ5FPySLdvREByI2vZiTWwCnF0moMJ1hK9YQwDTHKh6I7/uSckMetoRGb5UBZPC1z0jlw+n/XCgjeH7y1AQ==", - "dev": true - }, "async-hook-jl": { "version": "1.7.6", "resolved": "https://registry.npmjs.org/async-hook-jl/-/async-hook-jl-1.7.6.tgz", @@ -16925,15 +18076,22 @@ "requires": { "semver": "^5.3.0", "shimmer": "^1.1.0" + }, + "dependencies": { + "semver": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==" + } } }, "async-settle": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/async-settle/-/async-settle-1.0.0.tgz", - "integrity": "sha1-HQqRS7Aldb7IqPOnTlCA9yssDGs=", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/async-settle/-/async-settle-2.0.0.tgz", + "integrity": "sha512-Obu/KE8FurfQRN6ODdHN9LuXqwC+JFIM9NRyZqJJ4ZfLJmIYN9Rg0/kb+wF70VV5+fJusTMQlJ1t5rF7J/ETdg==", "dev": true, "requires": { - "async-done": "^1.2.2" + "async-done": "^2.0.0" } }, "asynckit": { @@ -16941,17 +18099,14 @@ "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" }, - "atob": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/atob/-/atob-2.1.2.tgz", - "integrity": "sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg==", - "dev": true - }, "available-typed-arrays": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.5.tgz", - "integrity": "sha512-DMD0KiN46eipeziST1LPP/STfDU0sufISXmjSgvVsoU2tqxctQeASejWcfNtxYKqETM1UxQ8sp2OrSBWpHY6sw==", - "dev": true + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", + "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==", + "dev": true, + "requires": { + "possible-typed-array-names": "^1.0.0" + } }, "axe-core": { "version": "4.4.1", @@ -16959,14 +18114,6 @@ "integrity": "sha512-gd1kmb21kwNuWr6BQz8fv6GNECPBnUasepcoLbekws23NVBLODdsClRZ+bQ8+9Uomf3Sm3+Vwn0oYG9NvwnJCw==", "dev": true }, - "axios": { - "version": "0.26.1", - "resolved": "https://registry.npmjs.org/axios/-/axios-0.26.1.tgz", - "integrity": "sha512-fPwcX4EvnSHuInCMItEhAGnaSEXRBjtzh9fOtsE6E1G6p7vl7edEeZe11QHf18+6+9gR5PbKV/sGKNaD8YaMeA==", - "requires": { - "follow-redirects": "^1.14.8" - } - }, "axobject-query": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-2.2.0.tgz", @@ -16974,15 +18121,21 @@ "dev": true }, "azure-devops-node-api": { - "version": "11.0.1", - "resolved": "https://registry.npmjs.org/azure-devops-node-api/-/azure-devops-node-api-11.0.1.tgz", - "integrity": "sha512-YMdjAw9l5p/6leiyIloxj3k7VIvYThKjvqgiQn88r3nhT93ENwsoDS3A83CyJ4uTWzCZ5f5jCi6c27rTU5Pz+A==", + "version": "12.5.0", + "resolved": "https://registry.npmjs.org/azure-devops-node-api/-/azure-devops-node-api-12.5.0.tgz", + "integrity": "sha512-R5eFskGvOm3U/GzeAuxRkUsAl0hrAwGgWn6zAd2KrZmrEhWZVqLew4OOupbQlXUuojUzpGtq62SmdhJ06N88og==", "dev": true, "requires": { "tunnel": "0.0.6", "typed-rest-client": "^1.8.4" } }, + "b4a": { + "version": "1.6.6", + "resolved": "https://registry.npmjs.org/b4a/-/b4a-1.6.6.tgz", + "integrity": "sha512-5Tk1HLk6b6ctmjIkAcU/Ujv/1WqiDl0F0JdRCR80VsOcUlHcu7pWeWRlOqQLHfDEsVx9YH/aif5AG4ehoCtTmg==", + "dev": true + }, "babel-runtime": { "version": "6.26.0", "resolved": "https://registry.npmjs.org/babel-runtime/-/babel-runtime-6.26.0.tgz", @@ -17002,94 +18155,57 @@ "regenerator-runtime": { "version": "0.11.1", "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.11.1.tgz", - "integrity": "sha512-MguG95oij0fC3QV3URf4V2SDYGJhJnJGqvIIgdECeODCT98wSWDAJ94SSuVpYQUoTcGUIL6L4yNB7j1DFFHSBg==", - "dev": true - } - } - }, - "bach": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/bach/-/bach-1.2.0.tgz", - "integrity": "sha1-Szzpa/JxNPeaG0FKUcFONMO9mIA=", - "dev": true, - "requires": { - "arr-filter": "^1.1.1", - "arr-flatten": "^1.0.1", - "arr-map": "^2.0.0", - "array-each": "^1.0.0", - "array-initial": "^1.0.0", - "array-last": "^1.1.1", - "async-done": "^1.2.2", - "async-settle": "^1.0.0", - "now-and-later": "^2.0.0" - } - }, - "balanced-match": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", - "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=" + "integrity": "sha512-MguG95oij0fC3QV3URf4V2SDYGJhJnJGqvIIgdECeODCT98wSWDAJ94SSuVpYQUoTcGUIL6L4yNB7j1DFFHSBg==", + "dev": true + } + } }, - "base": { - "version": "0.11.2", - "resolved": "https://registry.npmjs.org/base/-/base-0.11.2.tgz", - "integrity": "sha512-5T6P4xPgpp0YDFvSWwEZ4NoE3aM4QBQXDzmVbraCkFj8zHM+mba8SyqB5DbZWyR7mYHo6Y7BdQo3MoA4m0TeQg==", + "bach": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/bach/-/bach-2.0.1.tgz", + "integrity": "sha512-A7bvGMGiTOxGMpNupYl9HQTf0FFDNF4VCmks4PJpFyN1AX2pdKuxuwdvUz2Hu388wcgp+OvGFNsumBfFNkR7eg==", "dev": true, "requires": { - "cache-base": "^1.0.1", - "class-utils": "^0.3.5", - "component-emitter": "^1.2.1", - "define-property": "^1.0.0", - "isobject": "^3.0.1", - "mixin-deep": "^1.2.0", - "pascalcase": "^0.1.1" + "async-done": "^2.0.0", + "async-settle": "^2.0.0", + "now-and-later": "^3.0.0" }, "dependencies": { - "define-property": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", - "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", - "dev": true, - "requires": { - "is-descriptor": "^1.0.0" - } - }, - "is-accessor-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", - "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", - "dev": true, - "requires": { - "kind-of": "^6.0.0" - } - }, - "is-data-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", - "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", - "dev": true, - "requires": { - "kind-of": "^6.0.0" - } - }, - "is-descriptor": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", - "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", + "now-and-later": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/now-and-later/-/now-and-later-3.0.0.tgz", + "integrity": "sha512-pGO4pzSdaxhWTGkfSfHx3hVzJVslFPwBp2Myq9MYN/ChfJZF87ochMAXnvz6/58RJSf5ik2q9tXprBBrk2cpcg==", "dev": true, "requires": { - "is-accessor-descriptor": "^1.0.0", - "is-data-descriptor": "^1.0.0", - "kind-of": "^6.0.2" + "once": "^1.4.0" } } } }, + "balanced-match": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", + "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=" + }, + "bare-events": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/bare-events/-/bare-events-2.3.1.tgz", + "integrity": "sha512-sJnSOTVESURZ61XgEleqmP255T6zTYwHPwE4r6SssIh0U9/uDvfpdoJYpVUerJJZH2fueO+CdT8ZT+OC/7aZDA==", + "dev": true, + "optional": true + }, "base64-js": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", "dev": true }, + "baseline-browser-mapping": { + "version": "2.9.19", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.9.19.tgz", + "integrity": "sha512-ipDqC8FrAl/76p2SSWKSI+H9tFwm7vYqXQrItCuiVPt26Km0jS+NzSsBWAaBusvSbQcfJG+JitdMm+wZAgTYqg==", + "dev": true + }, "bent": { "version": "7.3.12", "resolved": "https://registry.npmjs.org/bent/-/bent-7.3.12.tgz", @@ -17109,44 +18225,18 @@ } } }, - "big-integer": { - "version": "1.6.49", - "resolved": "https://registry.npmjs.org/big-integer/-/big-integer-1.6.49.tgz", - "integrity": "sha512-KJ7VhqH+f/BOt9a3yMwJNmcZjG53ijWMTjSAGMveQWyLwqIiwkjNP5PFgDob3Snnx86SjDj6I89fIbv0dkQeNw==", - "dev": true - }, "big.js": { "version": "5.2.2", "resolved": "https://registry.npmjs.org/big.js/-/big.js-5.2.2.tgz", "integrity": "sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ==", "dev": true }, - "binary": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/binary/-/binary-0.3.0.tgz", - "integrity": "sha1-n2BVO8XOjDOG87VTz/R0Yq3sqnk=", - "dev": true, - "requires": { - "buffers": "~0.1.1", - "chainsaw": "~0.1.0" - } - }, "binary-extensions": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==", "dev": true }, - "bindings": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz", - "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==", - "dev": true, - "optional": true, - "requires": { - "file-uri-to-path": "1.0.0" - } - }, "bl": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/bl/-/bl-1.2.3.tgz", @@ -17158,9 +18248,9 @@ } }, "bn.js": { - "version": "4.11.8", - "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.11.8.tgz", - "integrity": "sha512-ItfYfPLkWHUjckQCk8xC+LwxgK8NYcXywGigJgSwOP8Y2iyWT4f2vsZnoOXTTbo+o5yXmIUJ4gn5538SO5S3gA==", + "version": "4.12.3", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.3.tgz", + "integrity": "sha512-fGTi3gxV/23FTYdAoUtLYp6qySe2KE3teyZitipKNRuVYcBkoP/bB3guXN/XVKUe9mxCHXnc9C4ocyz8OmgN0g==", "dev": true }, "boolbase": { @@ -17170,41 +18260,21 @@ "dev": true }, "brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.13.tgz", + "integrity": "sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==", "requires": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "braces": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/braces/-/braces-2.3.2.tgz", - "integrity": "sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w==", + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", "dev": true, "requires": { - "arr-flatten": "^1.1.0", - "array-unique": "^0.3.2", - "extend-shallow": "^2.0.1", - "fill-range": "^4.0.0", - "isobject": "^3.0.1", - "repeat-element": "^1.1.2", - "snapdragon": "^0.8.1", - "snapdragon-node": "^2.0.1", - "split-string": "^3.0.2", - "to-regex": "^3.0.1" - }, - "dependencies": { - "extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", - "dev": true, - "requires": { - "is-extendable": "^0.1.0" - } - } + "fill-range": "^7.1.1" } }, "brorand": { @@ -17257,28 +18327,59 @@ } }, "browserify-rsa": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/browserify-rsa/-/browserify-rsa-4.0.1.tgz", - "integrity": "sha1-IeCr+vbyApzy+vsTNWenAdQTVSQ=", + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/browserify-rsa/-/browserify-rsa-4.1.1.tgz", + "integrity": "sha512-YBjSAiTqM04ZVei6sXighu679a3SqWORA3qZTEqZImnlkDIFtKc6pNutpjyZ8RJTjQtuYfeetkxM11GwoYXMIQ==", "dev": true, "requires": { - "bn.js": "^4.1.0", - "randombytes": "^2.0.1" + "bn.js": "^5.2.1", + "randombytes": "^2.1.0", + "safe-buffer": "^5.2.1" + }, + "dependencies": { + "bn.js": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-5.2.3.tgz", + "integrity": "sha512-EAcmnPkxpntVL+DS7bO1zhcZNvCkxqtkd0ZY53h06GNQ3DEkkGZ/gKgmDv6DdZQGj9BgfSPKtJJ7Dp1GPP8f7w==", + "dev": true + }, + "safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "dev": true + } } }, "browserify-sign": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/browserify-sign/-/browserify-sign-4.0.4.tgz", - "integrity": "sha1-qk62jl17ZYuqa/alfmMMvXqT0pg=", + "version": "4.2.5", + "resolved": "https://registry.npmjs.org/browserify-sign/-/browserify-sign-4.2.5.tgz", + "integrity": "sha512-C2AUdAJg6rlM2W5QMp2Q4KGQMVBwR1lIimTsUnutJ8bMpW5B52pGpR2gEnNBNwijumDo5FojQ0L9JrXA8m4YEw==", "dev": true, "requires": { - "bn.js": "^4.1.1", - "browserify-rsa": "^4.0.0", - "create-hash": "^1.1.0", - "create-hmac": "^1.1.2", - "elliptic": "^6.0.0", - "inherits": "^2.0.1", - "parse-asn1": "^5.0.0" + "bn.js": "^5.2.2", + "browserify-rsa": "^4.1.1", + "create-hash": "^1.2.0", + "create-hmac": "^1.1.7", + "elliptic": "^6.6.1", + "inherits": "^2.0.4", + "parse-asn1": "^5.1.9", + "readable-stream": "^2.3.8", + "safe-buffer": "^5.2.1" + }, + "dependencies": { + "bn.js": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-5.2.3.tgz", + "integrity": "sha512-EAcmnPkxpntVL+DS7bO1zhcZNvCkxqtkd0ZY53h06GNQ3DEkkGZ/gKgmDv6DdZQGj9BgfSPKtJJ7Dp1GPP8f7w==", + "dev": true + }, + "safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "dev": true + } } }, "browserify-zlib": { @@ -17288,27 +18389,19 @@ "dev": true, "requires": { "pako": "~1.0.5" - }, - "dependencies": { - "pako": { - "version": "1.0.10", - "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.10.tgz", - "integrity": "sha512-0DTvPVU3ed8+HNXOu5Bs+o//Mbdj9VNQMUOe9oKCwh8l0GNwpTDMKCWbRjgtD291AWnkAgkqA/LOnQS8AmS1tw==", - "dev": true - } } }, "browserslist": { - "version": "4.20.2", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.20.2.tgz", - "integrity": "sha512-CQOBCqp/9pDvDbx3xfMi+86pr4KXIf2FDkTTdeuYw8OxS9t898LA1Khq57gtufFILXpfgsSx5woNgsBgvGjpsA==", + "version": "4.28.1", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz", + "integrity": "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==", "dev": true, "requires": { - "caniuse-lite": "^1.0.30001317", - "electron-to-chromium": "^1.4.84", - "escalade": "^3.1.1", - "node-releases": "^2.0.2", - "picocolors": "^1.0.0" + "baseline-browser-mapping": "^2.9.0", + "caniuse-lite": "^1.0.30001759", + "electron-to-chromium": "^1.5.263", + "node-releases": "^2.0.27", + "update-browserslist-db": "^1.2.0" } }, "buffer": { @@ -17343,10 +18436,16 @@ "integrity": "sha1-DTM+PwDqxQqhRUq9MO+MKl2ackI=", "dev": true }, + "buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==", + "dev": true + }, "buffer-fill": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/buffer-fill/-/buffer-fill-1.0.0.tgz", - "integrity": "sha1-+PeLdniYiO858gXNY39o5wISKyw=", + "integrity": "sha512-T7zexNBwiiaCOGDg9xNX9PBmjrubblRkENuptryuI64URkXDFum9il/JGL8Lm8wYfAXpredVXXZz7eMHilimiQ==", "dev": true }, "buffer-from": { @@ -17355,24 +18454,12 @@ "integrity": "sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A==", "dev": true }, - "buffer-indexof-polyfill": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/buffer-indexof-polyfill/-/buffer-indexof-polyfill-1.0.2.tgz", - "integrity": "sha512-I7wzHwA3t1/lwXQh+A5PbNvJxgfo5r3xulgpYDB5zckTu/Z9oUK9biouBKQUjEqzaz3HnAT6TYoovmE+GqSf7A==", - "dev": true - }, "buffer-xor": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/buffer-xor/-/buffer-xor-1.0.3.tgz", "integrity": "sha1-JuYe0UIvtw3ULm42cp7VHYVf6Nk=", "dev": true }, - "buffers": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/buffers/-/buffers-0.1.1.tgz", - "integrity": "sha1-skV5w77U1tOWru5tmorn9Ugqt7s=", - "dev": true - }, "builtin-status-codes": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/builtin-status-codes/-/builtin-status-codes-3.0.0.tgz", @@ -17385,23 +18472,6 @@ "integrity": "sha512-i4uu6M4zuMUiyfZN4RU2+i9+peJh//pXhd9x1oSe1LBkZ3LEbCoygu8W0bXTukU1Jme2txKuotpCZRaC3FLxcQ==", "dev": true }, - "cache-base": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/cache-base/-/cache-base-1.0.1.tgz", - "integrity": "sha512-AKcdTnFSWATd5/GCPRxr2ChwIJ85CeyrEyjRHlKxQ56d4XJMGym0uAiKn0xbLOGOl3+yRpOTi484dVCEc5AUzQ==", - "dev": true, - "requires": { - "collection-visit": "^1.0.0", - "component-emitter": "^1.2.1", - "get-value": "^2.0.6", - "has-value": "^1.0.0", - "isobject": "^3.0.1", - "set-value": "^2.0.0", - "to-object-path": "^0.3.0", - "union-value": "^1.0.0", - "unset-value": "^1.0.0" - } - }, "cacheable-request": { "version": "2.1.4", "resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-2.1.4.tgz", @@ -17417,10 +18487,16 @@ "responselike": "1.0.2" }, "dependencies": { + "get-stream": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-3.0.0.tgz", + "integrity": "sha512-GlhdIUuVakc8SJ6kK0zAFbiGzRFzNnY4jUuEbV9UROo4Y+0Ny4fjvcZFVTeDA4odpFyOQzaw6hXukJSq/f28sQ==", + "dev": true + }, "lowercase-keys": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-1.0.0.tgz", - "integrity": "sha1-TjNms55/VFfjXxMkvfb4jQv8cwY=", + "integrity": "sha512-RPlX0+PHuvxVDZ7xX+EBVAp4RsVxP/TdDSN2mJYdiq1Lc4Hz7EUSjUI7RZrKKlmrIzVhf6Jo2stj7++gVarS0A==", "dev": true } } @@ -17438,15 +18514,42 @@ } }, "call-bind": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz", + "integrity": "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==", + "dev": true, + "requires": { + "call-bind-apply-helpers": "^1.0.0", + "es-define-property": "^1.0.0", + "get-intrinsic": "^1.2.4", + "set-function-length": "^1.2.2" + } + }, + "call-bind-apply-helpers": { "version": "1.0.2", - "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz", - "integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "requires": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + } + }, + "call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", "dev": true, "requires": { - "function-bind": "^1.1.1", - "get-intrinsic": "^1.0.2" + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" } }, + "callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true + }, "camelcase": { "version": "5.3.1", "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", @@ -17454,9 +18557,9 @@ "dev": true }, "caniuse-lite": { - "version": "1.0.30001320", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001320.tgz", - "integrity": "sha512-MWPzG54AGdo3nWx7zHZTefseM5Y1ccM7hlQKHRqJkPozUaw3hNbBTMmLn16GG2FUzjR13Cr3NPfhIieX5PzXDA==", + "version": "1.0.30001768", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001768.tgz", + "integrity": "sha512-qY3aDRZC5nWPgHUgIB84WL+nySuo19wk0VJpp/XI9T34lrvkyhRvNVOFJOp2kxClQhiFBu+TaUSudf6oa3vkSA==", "dev": true }, "caseless": { @@ -17495,15 +18598,6 @@ "check-error": "^1.0.2" } }, - "chainsaw": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/chainsaw/-/chainsaw-0.1.0.tgz", - "integrity": "sha1-XqtQsor+WAdNDVgpE4iCi15fvJg=", - "dev": true, - "requires": { - "traverse": ">=0.3.0 <0.4" - } - }, "chalk": { "version": "2.4.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", @@ -17518,7 +18612,8 @@ "charenc": { "version": "0.0.2", "resolved": "https://registry.npmjs.org/charenc/-/charenc-0.0.2.tgz", - "integrity": "sha1-wKHS86cJLgN3S/qD8UwPxXkKhmc=" + "integrity": "sha1-wKHS86cJLgN3S/qD8UwPxXkKhmc=", + "dev": true }, "check-error": { "version": "1.0.2", @@ -17703,34 +18798,6 @@ "readdirp": "~3.6.0" }, "dependencies": { - "anymatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.2.tgz", - "integrity": "sha512-P43ePfOAIupkguHUycrc4qJ9kz8ZiuOUijaETwX7THt0Y/GNK7v0aa8rY816xWjZ7rJdA5XdMcpVFTKMq+RvWg==", - "dev": true, - "requires": { - "normalize-path": "^3.0.0", - "picomatch": "^2.0.4" - } - }, - "braces": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", - "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", - "dev": true, - "requires": { - "fill-range": "^7.0.1" - } - }, - "fill-range": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", - "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", - "dev": true, - "requires": { - "to-regex-range": "^5.0.1" - } - }, "glob-parent": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", @@ -17739,28 +18806,13 @@ "requires": { "is-glob": "^4.0.1" } - }, - "is-number": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", - "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", - "dev": true - }, - "to-regex-range": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", - "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", - "dev": true, - "requires": { - "is-number": "^7.0.0" - } } } }, "chownr": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.2.tgz", - "integrity": "sha512-GkfeAQh+QNy3wquu9oIZr6SS5x7wGdSgNQvD10X3r+AZr1Oys22HW8kAmDMvNg2+Dm0TeGaEuO8gFwdBXxwO8A==", + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", + "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", "dev": true, "optional": true }, @@ -17774,13 +18826,22 @@ } }, "cipher-base": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/cipher-base/-/cipher-base-1.0.4.tgz", - "integrity": "sha512-Kkht5ye6ZGmwv40uUDZztayT2ThLQGfnj/T71N/XzeZeo3nf8foyW7zGTsPYkEya3m5f3cAypH+qe7YOrM1U2Q==", + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/cipher-base/-/cipher-base-1.0.7.tgz", + "integrity": "sha512-Mz9QMT5fJe7bKI7MH31UilT5cEK5EHHRCccw/YRFsRY47AuNgaV6HY3rscp0/I4Q+tTW/5zoqpSeRRI54TkDWA==", "dev": true, "requires": { - "inherits": "^2.0.1", - "safe-buffer": "^5.0.1" + "inherits": "^2.0.4", + "safe-buffer": "^5.2.1", + "to-buffer": "^1.2.2" + }, + "dependencies": { + "safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "dev": true + } } }, "circular-json": { @@ -17789,28 +18850,10 @@ "integrity": "sha512-UZK3NBx2Mca+b5LsG7bY183pHWt5Y1xts4P3Pz7ENTwGVnJOUWbRb3ocjvX7hx9tq/yTAdclXm9sZ38gNuem4A==", "dev": true }, - "class-utils": { - "version": "0.3.6", - "resolved": "https://registry.npmjs.org/class-utils/-/class-utils-0.3.6.tgz", - "integrity": "sha512-qOhPa/Fj7s6TY8H8esGu5QNpMMQxz79h+urzrNYN6mn+9BnxlDGf5QZ+XeCDsxSjPqsSR56XOZOJmpeurnLMeg==", - "dev": true, - "requires": { - "arr-union": "^3.1.0", - "define-property": "^0.2.5", - "isobject": "^3.0.0", - "static-extend": "^0.1.1" - }, - "dependencies": { - "define-property": { - "version": "0.2.5", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", - "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", - "dev": true, - "requires": { - "is-descriptor": "^0.1.0" - } - } - } + "cjs-module-lexer": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-1.2.3.tgz", + "integrity": "sha512-0TNiGstbQmCFwt4akjjBg5pLRTSyj/PkWQ1ZoO2zntmg9yLqSRxwEa4iCfQLGjqhiqBfOJa7W/E8wfGrTDmlZQ==" }, "clean-stack": { "version": "2.2.0", @@ -17819,31 +18862,14 @@ "dev": true }, "cliui": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-3.2.0.tgz", - "integrity": "sha1-EgYBU3qRbSmUD5NNo7SNWFo5IT0=", + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", + "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", "dev": true, "requires": { - "string-width": "^1.0.1", - "strip-ansi": "^3.0.1", - "wrap-ansi": "^2.0.0" - }, - "dependencies": { - "ansi-regex": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", - "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", - "dev": true - }, - "strip-ansi": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", - "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", - "dev": true, - "requires": { - "ansi-regex": "^2.0.0" - } - } + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^7.0.0" } }, "clone": { @@ -17872,7 +18898,7 @@ "clone-response": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/clone-response/-/clone-response-1.0.2.tgz", - "integrity": "sha1-0dyXOSAxTfZ/vrlCI7TuNQI56Ws=", + "integrity": "sha512-yjLXh88P599UOyPTFX0POsd7WxnbsVsGohcwzHOLspIhhpalPw1BcqED8NblyZLKcGrL8dTgMlcaZxV2jAD41Q==", "dev": true, "requires": { "mimic-response": "^1.0.0" @@ -17903,45 +18929,20 @@ "async-hook-jl": "^1.7.6", "emitter-listener": "^1.0.1", "semver": "^5.4.1" - } - }, - "code-point-at": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz", - "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=", - "dev": true - }, - "collection-map": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/collection-map/-/collection-map-1.0.0.tgz", - "integrity": "sha1-rqDwb40mx4DCt1SUOFVEsiVa8Yw=", - "dev": true, - "requires": { - "arr-map": "^2.0.2", - "for-own": "^1.0.0", - "make-iterator": "^1.0.0" }, "dependencies": { - "for-own": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/for-own/-/for-own-1.0.0.tgz", - "integrity": "sha1-xjMy9BXO3EsE2/5wz4NklMU8tEs=", - "dev": true, - "requires": { - "for-in": "^1.0.1" - } + "semver": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==" } } }, - "collection-visit": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/collection-visit/-/collection-visit-1.0.0.tgz", - "integrity": "sha1-S8A3PBZLwykbTTaMgpzxqApZ3KA=", - "dev": true, - "requires": { - "map-visit": "^1.0.0", - "object-visit": "^1.0.0" - } + "cockatiel": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/cockatiel/-/cockatiel-3.1.3.tgz", + "integrity": "sha512-xC759TpZ69d7HhfDp8m2WkRwEUiCkxY8Ee2OQH/3H6zmy2D/5Sm+zSTbPRa+V2QyjDtpMvjOIAOVjA2gp6N1kQ==", + "dev": true }, "color-convert": { "version": "1.9.3", @@ -17958,12 +18959,6 @@ "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=", "dev": true }, - "color-support": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-support/-/color-support-1.1.3.tgz", - "integrity": "sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==", - "dev": true - }, "colorette": { "version": "2.0.16", "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.16.tgz", @@ -17996,48 +18991,17 @@ "integrity": "sha512-3Lc0sTIuX1jmY2K2RrXRJOND6KsRTX2D4v3+eu1PDptsuJZVK4LZc852eZa9I+avj0NrUKlTNgqvccNOH6mbGg==", "dev": true }, - "component-emitter": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.0.tgz", - "integrity": "sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg==", - "dev": true - }, "concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=" }, - "concat-stream": { - "version": "1.6.2", - "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.2.tgz", - "integrity": "sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==", - "dev": true, - "requires": { - "buffer-from": "^1.0.0", - "inherits": "^2.0.3", - "readable-stream": "^2.2.2", - "typedarray": "^0.0.6" - } - }, - "confusing-browser-globals": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/confusing-browser-globals/-/confusing-browser-globals-1.0.11.tgz", - "integrity": "sha512-JsPKdmh8ZkmnHxDk55FZ1TqVLvEQTvoByJZRN9jzI0UjxK/QgAmsphz7PGtqgPieQZ/CQcHWXCR7ATDNhGe+YA==", - "dev": true - }, "console-browserify": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/console-browserify/-/console-browserify-1.2.0.tgz", "integrity": "sha512-ZMkYO/LkF17QvCPqM0gxw8yUzigAOZOSWSHg91FH6orS7vcEj5dVZTidN2fQ14yBSdg97RqhSNwLUXInd52OTA==", "dev": true }, - "console-control-strings": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", - "integrity": "sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4=", - "dev": true, - "optional": true - }, "constants-browserify": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/constants-browserify/-/constants-browserify-1.0.0.tgz", @@ -18045,12 +19009,20 @@ "dev": true }, "content-disposition": { - "version": "0.5.3", - "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.3.tgz", - "integrity": "sha512-ExO0774ikEObIAEV9kDo50o+79VCUdEB6n6lzKgGwupcVeRlhrj3qGAfwq8G6uBJjkqLrhT0qEYFcWng8z1z0g==", + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", "dev": true, "requires": { - "safe-buffer": "5.1.2" + "safe-buffer": "5.2.1" + }, + "dependencies": { + "safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "dev": true + } } }, "continuation-local-storage": { @@ -18063,27 +19035,18 @@ } }, "convert-source-map": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.6.0.tgz", - "integrity": "sha512-eFu7XigvxdZ1ETfbgPBohgyQ/Z++C0eEhTor0qRwBw9unw+L0/6V8wkSuGgzdThkiS5lSpdptOQPD8Ak40a+7A==", - "dev": true, - "requires": { - "safe-buffer": "~5.1.1" - } - }, - "copy-descriptor": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/copy-descriptor/-/copy-descriptor-0.1.1.tgz", - "integrity": "sha1-Z29us8OZl8LuGsOpJP1hJHSPV40=", + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz", + "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==", "dev": true }, "copy-props": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/copy-props/-/copy-props-2.0.5.tgz", - "integrity": "sha512-XBlx8HSqrT0ObQwmSzM7WE5k8FxTV75h1DX1Z3n6NhQ/UYYAvInWYmG06vFt7hQZArE2fuO62aihiWIVQwh1sw==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/copy-props/-/copy-props-4.0.0.tgz", + "integrity": "sha512-bVWtw1wQLzzKiYROtvNlbJgxgBYt2bMJpkCbKmXM3xyijvcjjWXEk5nyrrT3bgJ7ODb19ZohE2T0Y3FgNPyoTw==", "dev": true, "requires": { - "each-props": "^1.3.2", + "each-props": "^3.0.0", "is-plain-object": "^5.0.0" }, "dependencies": { @@ -18121,9 +19084,9 @@ } }, "core-js-pure": { - "version": "3.1.4", - "resolved": "https://registry.npmjs.org/core-js-pure/-/core-js-pure-3.1.4.tgz", - "integrity": "sha512-uJ4Z7iPNwiu1foygbcZYJsJs1jiXrTTCvxfLDXNhI/I+NHbSIEyr548y4fcsCEyWY0XgfAG/qqaunJ1SThHenA==", + "version": "3.42.0", + "resolved": "https://registry.npmjs.org/core-js-pure/-/core-js-pure-3.42.0.tgz", + "integrity": "sha512-007bM04u91fF4kMgwom2I5cQxAFIy8jVulgr9eozILl/SZE53QOqnW/+vviC+wQWLv+AunBG+8Q0TLoeSsSxRQ==", "dev": true }, "core-util-is": { @@ -18133,13 +19096,13 @@ "dev": true }, "create-ecdh": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/create-ecdh/-/create-ecdh-4.0.3.tgz", - "integrity": "sha512-GbEHQPMOswGpKXM9kCWVrremUcBmjteUaQ01T9rkKCPDXfUHX0IoP9LpHYo2NPFampa4e+/pFDc3jQdxrxQLaw==", + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/create-ecdh/-/create-ecdh-4.0.4.tgz", + "integrity": "sha512-mf+TCx8wWc9VpuxfP2ht0iSISLZnt0JgWlrOKZiNqyUZWnjIaCIVNQArMHnCZKfEYRg6IM7A+NeJoN8gf/Ws0A==", "dev": true, "requires": { "bn.js": "^4.1.0", - "elliptic": "^6.0.0" + "elliptic": "^6.5.3" } }, "create-hash": { @@ -18175,10 +19138,53 @@ "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", "dev": true }, + "cross-env": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-env/-/cross-env-7.0.3.tgz", + "integrity": "sha512-+/HKd6EgcQCJGh2PSjZuUitQBQynKor4wrFbRg4DtAgS1aWO+gU52xpH7M9ScGgXSYmAVS9bIJ8EzuaGw0oNAw==", + "dev": true, + "requires": { + "cross-spawn": "^7.0.1" + }, + "dependencies": { + "cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "requires": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + } + }, + "path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true + }, + "shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "requires": { + "shebang-regex": "^3.0.0" + } + }, + "shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true + } + } + }, "cross-spawn": { - "version": "6.0.5", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz", - "integrity": "sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==", + "version": "6.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.6.tgz", + "integrity": "sha512-VqCUuhcd1iB+dsv8gxPttb5iZh/D0iubSP21g36KXdEuf6I5JiioesUVjpCdHV9MZRUfVFlvwtIUyPfxo5trtw==", "dev": true, "requires": { "nice-try": "^1.0.4", @@ -18188,6 +19194,12 @@ "which": "^1.2.9" }, "dependencies": { + "semver": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", + "dev": true + }, "which": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", @@ -18202,42 +19214,67 @@ "crypt": { "version": "0.0.2", "resolved": "https://registry.npmjs.org/crypt/-/crypt-0.0.2.tgz", - "integrity": "sha1-iNf/fsDfuG9xPch7u0LQRNPmxBs=" + "integrity": "sha1-iNf/fsDfuG9xPch7u0LQRNPmxBs=", + "dev": true }, "crypto-browserify": { - "version": "3.12.0", - "resolved": "https://registry.npmjs.org/crypto-browserify/-/crypto-browserify-3.12.0.tgz", - "integrity": "sha512-fz4spIh+znjO2VjL+IdhEpRJ3YN6sMzITSBijk6FK2UvTqruSQW+/cCZTSNsMiZNvUeq0CqurF+dAbyiGOY6Wg==", + "version": "3.12.1", + "resolved": "https://registry.npmjs.org/crypto-browserify/-/crypto-browserify-3.12.1.tgz", + "integrity": "sha512-r4ESw/IlusD17lgQi1O20Fa3qNnsckR126TdUuBgAu7GBYSIPvdNyONd3Zrxh0xCwA4+6w/TDArBPsMvhur+KQ==", + "dev": true, + "requires": { + "browserify-cipher": "^1.0.1", + "browserify-sign": "^4.2.3", + "create-ecdh": "^4.0.4", + "create-hash": "^1.2.0", + "create-hmac": "^1.1.7", + "diffie-hellman": "^5.0.3", + "hash-base": "~3.0.4", + "inherits": "^2.0.4", + "pbkdf2": "^3.1.2", + "public-encrypt": "^4.0.3", + "randombytes": "^2.1.0", + "randomfill": "^1.0.4" + } + }, + "damerau-levenshtein": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz", + "integrity": "sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA==", + "dev": true + }, + "data-view-buffer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.1.tgz", + "integrity": "sha512-0lht7OugA5x3iJLOWFhWK/5ehONdprk0ISXqVFn/NFrDu+cuc8iADFrGQz5BnRK7LLU3JmkbXSxaqX+/mXYtUA==", "dev": true, "requires": { - "browserify-cipher": "^1.0.0", - "browserify-sign": "^4.0.0", - "create-ecdh": "^4.0.0", - "create-hash": "^1.1.0", - "create-hmac": "^1.1.0", - "diffie-hellman": "^5.0.0", - "inherits": "^2.0.1", - "pbkdf2": "^3.0.3", - "public-encrypt": "^4.0.0", - "randombytes": "^2.0.0", - "randomfill": "^1.0.3" + "call-bind": "^1.0.6", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.1" } }, - "d": { + "data-view-byte-length": { "version": "1.0.1", - "resolved": "https://registry.npmjs.org/d/-/d-1.0.1.tgz", - "integrity": "sha512-m62ShEObQ39CfralilEQRjH6oAMtNCV1xJyEx5LpRYUVN+EviphDgUc/F3hnYbADmkiNs67Y+3ylmlG7Lnu+FA==", + "resolved": "https://registry.npmjs.org/data-view-byte-length/-/data-view-byte-length-1.0.1.tgz", + "integrity": "sha512-4J7wRJD3ABAzr8wP+OcIcqq2dlUKp4DVflx++hs5h5ZKydWMI6/D/fAot+yh6g2tHh8fLFTvNOaVN357NvSrOQ==", "dev": true, "requires": { - "es5-ext": "^0.10.50", - "type": "^1.0.1" + "call-bind": "^1.0.7", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.1" } }, - "damerau-levenshtein": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz", - "integrity": "sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA==", - "dev": true + "data-view-byte-offset": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/data-view-byte-offset/-/data-view-byte-offset-1.0.0.tgz", + "integrity": "sha512-t/Ygsytq+R995EJ5PZlD4Cu56sWa8InXySaViRzw9apusqsOO2bQP+SbYzAhR0pFKoB+43lYy8rWban9JSuXnA==", + "dev": true, + "requires": { + "call-bind": "^1.0.6", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.1" + } }, "debug": { "version": "2.6.9", @@ -18296,7 +19333,7 @@ "pify": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", - "integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=", + "integrity": "sha512-C3FsVNH1udSEX48gGX1xfvwTWfsYWj5U+8/uK15BGzIGrKoUpghX8hWZwa/OFnakBiiVNmBvemTJR5mcy7iPcg==", "dev": true } } @@ -18304,7 +19341,7 @@ "pify": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", - "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=", + "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", "dev": true } } @@ -18312,7 +19349,7 @@ "decompress-response": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-3.3.0.tgz", - "integrity": "sha1-gKTdMjdIOEv6JICDYirt7Jgq3/M=", + "integrity": "sha512-BzRPQuY1ip+qDonAOz42gRm/pg9F768C+npV/4JOsxRC2sq+Rlk+Q4ZCAsOhnIaMrgarILY+RMUIvMmmX1qAEA==", "dev": true, "requires": { "mimic-response": "^1.0.0" @@ -18332,7 +19369,7 @@ "file-type": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/file-type/-/file-type-5.2.0.tgz", - "integrity": "sha1-LdvqfHP/42No365J3DOMBYwritY=", + "integrity": "sha512-Iq1nJ6D2+yIO4c8HHg4fyVb8mAJieo1Oloy1mLLaB2PvezNedhBVm+QU7g0qM42aiMbRXTxKKwGD17rjKNJYVQ==", "dev": true } } @@ -18372,7 +19409,7 @@ "file-type": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/file-type/-/file-type-5.2.0.tgz", - "integrity": "sha1-LdvqfHP/42No365J3DOMBYwritY=", + "integrity": "sha512-Iq1nJ6D2+yIO4c8HHg4fyVb8mAJieo1Oloy1mLLaB2PvezNedhBVm+QU7g0qM42aiMbRXTxKKwGD17rjKNJYVQ==", "dev": true } } @@ -18380,7 +19417,7 @@ "decompress-unzip": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/decompress-unzip/-/decompress-unzip-4.0.1.tgz", - "integrity": "sha1-3qrM39FK6vhVePczroIQ+bSEj2k=", + "integrity": "sha512-1fqeluvxgnn86MOh66u8FjbtJpAFv5wgCT9Iw8rcBqQcCo5tO8eiJw7NNTrvt9n4CRBVq7CstiS922oPgyGLrw==", "dev": true, "requires": { "file-type": "^3.8.0", @@ -18392,13 +19429,13 @@ "file-type": { "version": "3.9.0", "resolved": "https://registry.npmjs.org/file-type/-/file-type-3.9.0.tgz", - "integrity": "sha1-JXoHg4TR24CHvESdEH1SpSZyuek=", + "integrity": "sha512-RLoqTXE8/vPmMuTI88DAzhMYC99I8BWv7zYP4A1puo5HIjEJ5EX48ighy4ZyKMG9EDXxBgW6e++cn7d1xuFghA==", "dev": true }, "get-stream": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-2.3.1.tgz", - "integrity": "sha1-Xzj5PzRgCWZu4BUKBUFn+Rvdld4=", + "integrity": "sha512-AUGhbbemXxrZJRD5cDvKtQxLuYaIbNtDTK8YqupCI393Q2KSTreEsLUN3ZxAWFGiKTzL6nKuzfcIvieflUX9qA==", "dev": true, "requires": { "object-assign": "^4.0.1", @@ -18408,7 +19445,7 @@ "pify": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", - "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=", + "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", "dev": true } } @@ -18422,20 +19459,6 @@ "type-detect": "^4.0.0" } }, - "deep-equal": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-1.1.1.tgz", - "integrity": "sha512-yd9c5AdiqVcR+JjcwUQb9DkhJc8ngNr0MahEBGvDiJw8puWab2yZlh+nkasOnZP+EGTAP6rRp2JzJhJZzvNF8g==", - "dev": true, - "requires": { - "is-arguments": "^1.0.4", - "is-date-object": "^1.0.1", - "is-regex": "^1.0.4", - "object-is": "^1.0.1", - "object-keys": "^1.1.1", - "regexp.prototype.flags": "^1.2.0" - } - }, "deep-extend": { "version": "0.6.0", "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", @@ -18449,23 +19472,6 @@ "integrity": "sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ=", "dev": true }, - "default-compare": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/default-compare/-/default-compare-1.0.0.tgz", - "integrity": "sha512-QWfXlM0EkAbqOCbD/6HjdwT19j7WCkMyiRhWilc4H9/5h/RzTF9gv5LYh1+CmDV5d1rki6KAWLtQale0xt20eQ==", - "dev": true, - "requires": { - "kind-of": "^5.0.2" - }, - "dependencies": { - "kind-of": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-5.1.0.tgz", - "integrity": "sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==", - "dev": true - } - } - }, "default-require-extensions": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/default-require-extensions/-/default-require-extensions-3.0.0.tgz", @@ -18483,60 +19489,32 @@ } } }, - "default-resolution": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/default-resolution/-/default-resolution-2.0.0.tgz", - "integrity": "sha1-vLgrqnKtebQmp2cy8aga1t8m1oQ=", - "dev": true - }, - "define-properties": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.3.tgz", - "integrity": "sha512-3MqfYKj2lLzdMSf8ZIZE/V+Zuy+BgD6f164e8K2w7dgnpKArBDerGYpM46IYYcjnkdPNMjPk9A6VFB8+3SKlXQ==", + "define-data-property": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", "dev": true, "requires": { - "object-keys": "^1.0.12" + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" } }, - "define-property": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-2.0.2.tgz", - "integrity": "sha512-jwK2UV4cnPpbcG7+VRARKTZPUWowwXA8bzH5NP6ud0oeAxyYPuGZUAC7hMugpCdz4BeSZl2Dl9k66CHJ/46ZYQ==", + "define-lazy-prop": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-2.0.0.tgz", + "integrity": "sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og==", + "dev": true + }, + "define-properties": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", + "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", "dev": true, "requires": { - "is-descriptor": "^1.0.2", - "isobject": "^3.0.1" - }, - "dependencies": { - "is-accessor-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", - "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", - "dev": true, - "requires": { - "kind-of": "^6.0.0" - } - }, - "is-data-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", - "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", - "dev": true, - "requires": { - "kind-of": "^6.0.0" - } - }, - "is-descriptor": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", - "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", - "dev": true, - "requires": { - "is-accessor-descriptor": "^1.0.0", - "is-data-descriptor": "^1.0.0", - "kind-of": "^6.0.2" - } - } + "define-data-property": "^1.0.1", + "has-property-descriptors": "^1.0.0", + "object-keys": "^1.1.1" } }, "del": { @@ -18560,13 +19538,6 @@ "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==" }, - "delegates": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", - "integrity": "sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o=", - "dev": true, - "optional": true - }, "des.js": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/des.js/-/des.js-1.0.0.tgz", @@ -18580,41 +19551,36 @@ "detect-file": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/detect-file/-/detect-file-1.0.0.tgz", - "integrity": "sha1-8NZtA2cqglyxtzvbP+YjEMjlUrc=", + "integrity": "sha512-DtCOLG98P007x7wiiOmfI0fi3eIKyWiLTGJ2MDnVi/E04lWGbf+JzrRHMm0rgIIZJGtHpKpbVgLWHrv8xXpc3Q==", "dev": true }, "detect-libc": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz", - "integrity": "sha1-+hN8S9aY7fVc1c0CrFWfkaTEups=", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.1.tgz", + "integrity": "sha512-463v3ZeIrcWtdgIg6vI6XUncguvr2TnGl4SzDXinkt9mSLpBJKXT3mW6xT3VQdDN11+WVs29pgvivTc4Lp8v+w==", "dev": true, "optional": true }, "diagnostic-channel": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/diagnostic-channel/-/diagnostic-channel-1.1.0.tgz", - "integrity": "sha512-fwujyMe1gj6rk6dYi9hMZm0c8Mz8NDMVl2LB4iaYh3+LIAThZC8RKFGXWG0IML2OxAit/ZFRgZhMkhQ3d/bobQ==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/diagnostic-channel/-/diagnostic-channel-1.1.1.tgz", + "integrity": "sha512-r2HV5qFkUICyoaKlBEpLKHjxMXATUf/l+h8UZPGBHGLy4DDiY2sOLcIctax4eRnTw5wH2jTMExLntGPJ8eOJxw==", "requires": { - "semver": "^5.3.0" + "semver": "^7.5.3" } }, "diagnostic-channel-publishers": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/diagnostic-channel-publishers/-/diagnostic-channel-publishers-1.0.5.tgz", - "integrity": "sha512-dJwUS0915pkjjimPJVDnS/QQHsH0aOYhnZsLJdnZIMOrB+csj8RnZhWTuwnm8R5v3Z7OZs+ksv5luC14DGB7eg==", + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/diagnostic-channel-publishers/-/diagnostic-channel-publishers-1.0.7.tgz", + "integrity": "sha512-SEECbY5AiVt6DfLkhkaHNeshg1CogdLLANA8xlG/TKvS+XUgvIKl7VspJGYiEdL5OUyzMVnr7o0AwB7f+/Mjtg==", "requires": {} }, "diff": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", - "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.4.tgz", + "integrity": "sha512-X07nttJQkwkfKfvTPG/KSnE2OMdcUCao6+eXF3wmnIQRn2aPAHH3VxDbDOdegkd6JbPsXqShpvEOHfAT+nCNwQ==", "dev": true }, - "diff-match-patch": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/diff-match-patch/-/diff-match-patch-1.0.5.tgz", - "integrity": "sha512-IayShXAgj/QMXgB0IWmKx+rOPuGMhqm5w6jvFxmVenXKIzRqTAAsbBPT3kWQeGANj3jGgvcvv4yK6SxqYmikgw==" - }, "diffie-hellman": { "version": "5.0.3", "resolved": "https://registry.npmjs.org/diffie-hellman/-/diffie-hellman-5.0.3.tgz", @@ -18669,15 +19635,6 @@ "pify": "^4.0.1" }, "dependencies": { - "get-stream": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-4.1.0.tgz", - "integrity": "sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w==", - "dev": true, - "requires": { - "pump": "^3.0.0" - } - }, "make-dir": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-2.1.0.tgz", @@ -18688,37 +19645,34 @@ "semver": "^5.6.0" } }, - "pump": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", - "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", - "dev": true, - "requires": { - "end-of-stream": "^1.1.0", - "once": "^1.3.1" - } + "semver": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", + "dev": true } } }, + "dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "requires": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + } + }, "duplexer": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/duplexer/-/duplexer-0.1.2.tgz", "integrity": "sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg==", "dev": true }, - "duplexer2": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/duplexer2/-/duplexer2-0.1.4.tgz", - "integrity": "sha1-ixLauHjA1p4+eJEFFmKjL8a93ME=", - "dev": true, - "requires": { - "readable-stream": "^2.0.2" - } - }, "duplexer3": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/duplexer3/-/duplexer3-0.1.4.tgz", - "integrity": "sha1-7gHdHKwO08vH/b6jfcCo8c4ALOI=", + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/duplexer3/-/duplexer3-0.1.5.tgz", + "integrity": "sha512-1A8za6ws41LQgv9HrE/66jyC5yuSjQ3L/KOpFtoBilsAK2iA2wuS5rTt1OCzIvtS2V7nVmedsUU+DGRcjBmOYA==", "dev": true }, "duplexify": { @@ -18734,25 +19688,48 @@ } }, "each-props": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/each-props/-/each-props-1.3.2.tgz", - "integrity": "sha512-vV0Hem3zAGkJAyU7JSjixeU66rwdynTAa1vofCrSA5fEln+m67Az9CcnkVD776/fsN/UjIWmBDoNRS6t6G9RfA==", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/each-props/-/each-props-3.0.0.tgz", + "integrity": "sha512-IYf1hpuWrdzse/s/YJOrFmU15lyhSzxelNVAHTEG3DtP4QsLTWZUzcUL3HMXmKQxXpa4EIrBPpwRgj0aehdvAw==", "dev": true, "requires": { - "is-plain-object": "^2.0.1", + "is-plain-object": "^5.0.0", "object.defaults": "^1.1.0" + }, + "dependencies": { + "is-plain-object": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-5.0.0.tgz", + "integrity": "sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==", + "dev": true + } + } + }, + "eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "dev": true + }, + "ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "dev": true, + "requires": { + "safe-buffer": "^5.0.1" } }, "electron-to-chromium": { - "version": "1.4.92", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.92.tgz", - "integrity": "sha512-YAVbvQIcDE/IJ/vzDMjD484/hsRbFPW2qXJPaYTfOhtligmfYEYOep+5QojpaEU9kq6bMvNeC2aG7arYvTHYsA==", + "version": "1.5.286", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.286.tgz", + "integrity": "sha512-9tfDXhJ4RKFNerfjdCcZfufu49vg620741MNs26a9+bhLThdB+plgMeou98CAaHu/WATj2iHOOHTp1hWtABj2A==", "dev": true }, "elliptic": { - "version": "6.5.4", - "resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.5.4.tgz", - "integrity": "sha512-iLhC6ULemrljPZb+QutR5TQGB+pdW6KGD5RSegS+8sorOZT+rdQFbsQFJgvN3eRqNALqJer4oQ16YvJHlU8hzQ==", + "version": "6.6.1", + "resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.6.1.tgz", + "integrity": "sha512-RaddvvMatK2LJHqFJ+YA4WysVN5Ita9E35botqIYspQ4TkRAlCicdzKOjlyv/1Za5RyTNn7di//eEV0uTAfe3g==", "dev": true, "requires": { "bn.js": "^4.11.9", @@ -18762,14 +19739,6 @@ "inherits": "^2.0.4", "minimalistic-assert": "^1.0.1", "minimalistic-crypto-utils": "^1.0.1" - }, - "dependencies": { - "bn.js": { - "version": "4.12.0", - "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.0.tgz", - "integrity": "sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==", - "dev": true - } } }, "emitter-listener": { @@ -18793,39 +19762,22 @@ "dev": true }, "end-of-stream": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.1.tgz", - "integrity": "sha512-1MkrZNvWTKCaigbn+W15elq2BB/L22nqrSY5DKlo3X6+vclJm8Bb5djXJBmEX6fS3+zCh/F4VBK5Z2KxJt4s2Q==", + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", + "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", "dev": true, "requires": { "once": "^1.4.0" } }, "enhanced-resolve": { - "version": "5.12.0", - "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.12.0.tgz", - "integrity": "sha512-QHTXI/sZQmko1cbDoNAa3mJ5qhWUUNAq3vR0/YiD379fWQrcfuoX1+HW2S0MTt7XmoPLapdaDKUtelUSPic7hQ==", + "version": "5.19.0", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.19.0.tgz", + "integrity": "sha512-phv3E1Xl4tQOShqSte26C7Fl84EwUdZsyOuSSk9qtAGyyQs2s3jJzComh+Abf4g187lUUAvH+H26omrqia2aGg==", "dev": true, "requires": { "graceful-fs": "^4.2.4", - "tapable": "^2.2.0" - } - }, - "enquirer": { - "version": "2.3.6", - "resolved": "https://registry.npmjs.org/enquirer/-/enquirer-2.3.6.tgz", - "integrity": "sha512-yjNnPr315/FjS4zIsUxYguYUPP2e1NK4d7E7ZOLiyYCcbFBiTMyID+2wvm2w6+pZ/odMA7cRkjhsPbltwBOrLg==", - "dev": true, - "requires": { - "ansi-colors": "^4.1.1" - }, - "dependencies": { - "ansi-colors": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.1.tgz", - "integrity": "sha512-JoX0apGbHaUJBNl6yF+p6JAFYZ666/hhCGKN5t9QFjbJQKUU/g8MNbFDbvfrgKXvI1QpZplPOnwIo99lX/AAmA==", - "dev": true - } + "tapable": "^2.3.0" } }, "entities": { @@ -18840,57 +19792,104 @@ "integrity": "sha512-/o+BXHmB7ocbHEAs6F2EnG0ogybVVUdkRunTT2glZU9XAaGmhqskrvKwqXuDfNjEO0LZKWdejEEpnq8aM0tOaw==", "dev": true }, - "error-ex": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", - "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", - "dev": true, - "requires": { - "is-arrayish": "^0.2.1" - }, - "dependencies": { - "is-arrayish": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", - "integrity": "sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0=", - "dev": true - } - } - }, "es-abstract": { - "version": "1.19.2", - "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.19.2.tgz", - "integrity": "sha512-gfSBJoZdlL2xRiOCy0g8gLMryhoe1TlimjzU99L/31Z8QEGIhVQI+EWwt5lT+AuU9SnorVupXFqqOGqGfsyO6w==", - "dev": true, - "requires": { - "call-bind": "^1.0.2", + "version": "1.23.3", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.23.3.tgz", + "integrity": "sha512-e+HfNH61Bj1X9/jLc5v1owaLYuHdeHHSQlkhCBiTK8rBvKaULl/beGMxwrMXjpYrv4pz22BlY570vVePA2ho4A==", + "dev": true, + "requires": { + "array-buffer-byte-length": "^1.0.1", + "arraybuffer.prototype.slice": "^1.0.3", + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.7", + "data-view-buffer": "^1.0.1", + "data-view-byte-length": "^1.0.1", + "data-view-byte-offset": "^1.0.0", + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "es-set-tostringtag": "^2.0.3", "es-to-primitive": "^1.2.1", - "function-bind": "^1.1.1", - "get-intrinsic": "^1.1.1", - "get-symbol-description": "^1.0.0", - "has": "^1.0.3", + "function.prototype.name": "^1.1.6", + "get-intrinsic": "^1.2.4", + "get-symbol-description": "^1.0.2", + "globalthis": "^1.0.3", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.2", + "has-proto": "^1.0.3", "has-symbols": "^1.0.3", - "internal-slot": "^1.0.3", - "is-callable": "^1.2.4", - "is-negative-zero": "^2.0.2", + "hasown": "^2.0.2", + "internal-slot": "^1.0.7", + "is-array-buffer": "^3.0.4", + "is-callable": "^1.2.7", + "is-data-view": "^1.0.1", + "is-negative-zero": "^2.0.3", "is-regex": "^1.1.4", - "is-shared-array-buffer": "^1.0.1", + "is-shared-array-buffer": "^1.0.3", "is-string": "^1.0.7", + "is-typed-array": "^1.1.13", "is-weakref": "^1.0.2", - "object-inspect": "^1.12.0", + "object-inspect": "^1.13.1", "object-keys": "^1.1.1", - "object.assign": "^4.1.2", - "string.prototype.trimend": "^1.0.4", - "string.prototype.trimstart": "^1.0.4", - "unbox-primitive": "^1.0.1" - } + "object.assign": "^4.1.5", + "regexp.prototype.flags": "^1.5.2", + "safe-array-concat": "^1.1.2", + "safe-regex-test": "^1.0.3", + "string.prototype.trim": "^1.2.9", + "string.prototype.trimend": "^1.0.8", + "string.prototype.trimstart": "^1.0.8", + "typed-array-buffer": "^1.0.2", + "typed-array-byte-length": "^1.0.1", + "typed-array-byte-offset": "^1.0.2", + "typed-array-length": "^1.0.6", + "unbox-primitive": "^1.0.2", + "which-typed-array": "^1.1.15" + } + }, + "es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==" + }, + "es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==" }, "es-module-lexer": { - "version": "0.9.3", - "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-0.9.3.tgz", - "integrity": "sha512-1HQ2M2sPtxwnvOvT1ZClHyQDiggdNjURWpY2we6aMKCQiUVxTmVs2UYPLIrD84sS+kMdUwfBSylbJPwNnBrnHQ==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.0.0.tgz", + "integrity": "sha512-5POEcUuZybH7IdmGsD8wlf0AI55wMecM9rVBTI/qEAy2c1kTOm3DjFYjrBdI2K3BaJjJYfYFeRtM0t9ssnRuxw==", "dev": true }, + "es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "requires": { + "es-errors": "^1.3.0" + } + }, + "es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "requires": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + } + }, + "es-shim-unscopables": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/es-shim-unscopables/-/es-shim-unscopables-1.0.2.tgz", + "integrity": "sha512-J3yBRXCzDu4ULnQwxyToo/OjdMx6akgVC7K6few0a7F/0wLtmKKN7I73AH5T2836UuXRqN7Qg+IIUw/+YJksRw==", + "dev": true, + "requires": { + "hasown": "^2.0.0" + } + }, "es-to-primitive": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.1.tgz", @@ -18902,74 +19901,22 @@ "is-symbol": "^1.0.2" } }, - "es5-ext": { - "version": "0.10.53", - "resolved": "https://registry.npmjs.org/es5-ext/-/es5-ext-0.10.53.tgz", - "integrity": "sha512-Xs2Stw6NiNHWypzRTY1MtaG/uJlwCk8kH81920ma8mvN8Xq1gsfhZvpkImLQArw8AHnv8MT2I45J3c0R8slE+Q==", - "dev": true, - "requires": { - "es6-iterator": "~2.0.3", - "es6-symbol": "~3.1.3", - "next-tick": "~1.0.0" - }, - "dependencies": { - "next-tick": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/next-tick/-/next-tick-1.0.0.tgz", - "integrity": "sha1-yobR/ogoFpsBICCOPchCS524NCw=", - "dev": true - } - } - }, "es6-error": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/es6-error/-/es6-error-4.1.1.tgz", "integrity": "sha512-Um/+FxMr9CISWh0bi5Zv0iOD+4cFh5qLeks1qhAopKVAJw3drgKbKySikp7wGhDL0HPeaja0P5ULZrxLkniUVg==", - "dev": true - }, - "es6-iterator": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/es6-iterator/-/es6-iterator-2.0.3.tgz", - "integrity": "sha1-p96IkUGgWpSwhUQDstCg+/qY87c=", - "dev": true, - "requires": { - "d": "1", - "es5-ext": "^0.10.35", - "es6-symbol": "^3.1.1" - } - }, - "es6-object-assign": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/es6-object-assign/-/es6-object-assign-1.1.0.tgz", - "integrity": "sha1-wsNYJlYkfDnqEHyx5mUrb58kUjw=", - "dev": true - }, - "es6-symbol": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/es6-symbol/-/es6-symbol-3.1.3.tgz", - "integrity": "sha512-NJ6Yn3FuDinBaBRWl/q5X/s4koRHBrgKAu+yGI6JCBeiu3qrcbJhwT2GeR/EXVfylRk8dpQVJoLEFhK+Mu31NA==", - "dev": true, - "requires": { - "d": "^1.0.1", - "ext": "^1.1.2" - } - }, - "es6-weak-map": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/es6-weak-map/-/es6-weak-map-2.0.3.tgz", - "integrity": "sha512-p5um32HOTO1kP+w7PRnB+5lQ43Z6muuMuIMffvDN8ZB4GcnjLBV6zGStpbASIMk4DCAvEaamhe2zhyCb/QXXsA==", - "dev": true, - "requires": { - "d": "1", - "es5-ext": "^0.10.46", - "es6-iterator": "^2.0.3", - "es6-symbol": "^3.1.1" - } + "dev": true + }, + "es6-object-assign": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/es6-object-assign/-/es6-object-assign-1.1.0.tgz", + "integrity": "sha1-wsNYJlYkfDnqEHyx5mUrb58kUjw=", + "dev": true }, "escalade": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", - "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==", + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", "dev": true }, "escape-string-regexp": { @@ -18979,51 +19926,49 @@ "dev": true }, "eslint": { - "version": "7.32.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-7.32.0.tgz", - "integrity": "sha512-VHZ8gX+EDfz+97jGcgyGCyRia/dPOd6Xh9yPv8Bl1+SoaIwD+a/vlrOmGRUyOYu7MwUhc7CxqeaDZU13S4+EpA==", - "dev": true, - "requires": { - "@babel/code-frame": "7.12.11", - "@eslint/eslintrc": "^0.4.3", - "@humanwhocodes/config-array": "^0.5.0", - "ajv": "^6.10.0", + "version": "8.57.1", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.1.tgz", + "integrity": "sha512-ypowyDxpVSYpkXr9WPv2PAZCtNip1Mv5KTW0SCurXv/9iOpcrH9PaqUElksqEB6pChqHGDRCFTyrZlGhnLNGiA==", + "dev": true, + "requires": { + "@eslint-community/eslint-utils": "^4.2.0", + "@eslint-community/regexpp": "^4.6.1", + "@eslint/eslintrc": "^2.1.4", + "@eslint/js": "8.57.1", + "@humanwhocodes/config-array": "^0.13.0", + "@humanwhocodes/module-importer": "^1.0.1", + "@nodelib/fs.walk": "^1.2.8", + "@ungap/structured-clone": "^1.2.0", + "ajv": "^6.12.4", "chalk": "^4.0.0", "cross-spawn": "^7.0.2", - "debug": "^4.0.1", + "debug": "^4.3.2", "doctrine": "^3.0.0", - "enquirer": "^2.3.5", "escape-string-regexp": "^4.0.0", - "eslint-scope": "^5.1.1", - "eslint-utils": "^2.1.0", - "eslint-visitor-keys": "^2.0.0", - "espree": "^7.3.1", - "esquery": "^1.4.0", + "eslint-scope": "^7.2.2", + "eslint-visitor-keys": "^3.4.3", + "espree": "^9.6.1", + "esquery": "^1.4.2", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", "file-entry-cache": "^6.0.1", - "functional-red-black-tree": "^1.0.1", - "glob-parent": "^5.1.2", - "globals": "^13.6.0", - "ignore": "^4.0.6", - "import-fresh": "^3.0.0", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "globals": "^13.19.0", + "graphemer": "^1.4.0", + "ignore": "^5.2.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", - "js-yaml": "^3.13.1", + "is-path-inside": "^3.0.3", + "js-yaml": "^4.1.0", "json-stable-stringify-without-jsonify": "^1.0.1", "levn": "^0.4.1", "lodash.merge": "^4.6.2", - "minimatch": "^3.0.4", + "minimatch": "^3.1.2", "natural-compare": "^1.4.0", - "optionator": "^0.9.1", - "progress": "^2.0.0", - "regexpp": "^3.1.0", - "semver": "^7.2.1", - "strip-ansi": "^6.0.0", - "strip-json-comments": "^3.1.0", - "table": "^6.0.9", - "text-table": "^0.2.0", - "v8-compile-cache": "^2.0.3" + "optionator": "^0.9.3", + "strip-ansi": "^6.0.1", + "text-table": "^0.2.0" }, "dependencies": { "ansi-styles": { @@ -19036,6 +19981,12 @@ "color-convert": "^2.0.1" } }, + "argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true + }, "chalk": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.0.tgz", @@ -19062,9 +20013,9 @@ "dev": true }, "cross-spawn": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", - "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", "dev": true, "requires": { "path-key": "^3.1.0", @@ -19073,12 +20024,12 @@ } }, "debug": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", - "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", "dev": true, "requires": { - "ms": "^2.1.1" + "ms": "2.1.2" } }, "doctrine": { @@ -19096,25 +20047,45 @@ "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", "dev": true }, - "eslint-visitor-keys": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-2.1.0.tgz", - "integrity": "sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw==", + "eslint-scope": { + "version": "7.2.2", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz", + "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==", + "dev": true, + "requires": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + } + }, + "estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", "dev": true }, + "find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "requires": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + } + }, "glob-parent": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", - "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", "dev": true, "requires": { - "is-glob": "^4.0.1" + "is-glob": "^4.0.3" } }, "globals": { - "version": "13.12.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-13.12.0.tgz", - "integrity": "sha512-uS8X6lSKN2JumVoXrbUz+uG4BYG+eiawqm3qFcT7ammfbUHeCBoJMlHcec/S3krSk73/AE/f0szYFmgAA3kYZg==", + "version": "13.24.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", + "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", "dev": true, "requires": { "type-fest": "^0.20.2" @@ -19126,43 +20097,49 @@ "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", "dev": true }, - "ignore": { - "version": "4.0.6", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-4.0.6.tgz", - "integrity": "sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg==", - "dev": true + "js-yaml": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", + "dev": true, + "requires": { + "argparse": "^2.0.1" + } }, - "levn": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", - "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", "dev": true, "requires": { - "prelude-ls": "^1.2.1", - "type-check": "~0.4.0" + "p-locate": "^5.0.0" } }, "minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", "dev": true, "requires": { "brace-expansion": "^1.1.7" } }, - "optionator": { - "version": "0.9.1", - "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.1.tgz", - "integrity": "sha512-74RlY5FCnhq4jRxVUPKDaRwrVNXMqsGsiW6AJw4XK8hmtm10wC0ypZBLw5IIp85NZMr91+qd1RvvENwg7jjRFw==", + "p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "requires": { + "yocto-queue": "^0.1.0" + } + }, + "p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", "dev": true, "requires": { - "deep-is": "^0.1.3", - "fast-levenshtein": "^2.0.6", - "levn": "^0.4.1", - "prelude-ls": "^1.2.1", - "type-check": "^0.4.0", - "word-wrap": "^1.2.3" + "p-limit": "^3.0.2" } }, "path-key": { @@ -19171,27 +20148,6 @@ "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", "dev": true }, - "prelude-ls": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", - "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", - "dev": true - }, - "progress": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", - "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", - "dev": true - }, - "semver": { - "version": "7.3.5", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.5.tgz", - "integrity": "sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==", - "dev": true, - "requires": { - "lru-cache": "^6.0.0" - } - }, "shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", @@ -19216,15 +20172,6 @@ "has-flag": "^4.0.0" } }, - "type-check": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", - "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", - "dev": true, - "requires": { - "prelude-ls": "^1.2.1" - } - }, "type-fest": { "version": "0.20.2", "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", @@ -19233,28 +20180,6 @@ } } }, - "eslint-config-airbnb": { - "version": "18.2.1", - "resolved": "https://registry.npmjs.org/eslint-config-airbnb/-/eslint-config-airbnb-18.2.1.tgz", - "integrity": "sha512-glZNDEZ36VdlZWoxn/bUR1r/sdFKPd1mHPbqUtkctgNG4yT2DLLtJ3D+yCV+jzZCc2V1nBVkmdknOJBZ5Hc0fg==", - "dev": true, - "requires": { - "eslint-config-airbnb-base": "^14.2.1", - "object.assign": "^4.1.2", - "object.entries": "^1.1.2" - } - }, - "eslint-config-airbnb-base": { - "version": "14.2.1", - "resolved": "https://registry.npmjs.org/eslint-config-airbnb-base/-/eslint-config-airbnb-base-14.2.1.tgz", - "integrity": "sha512-GOrQyDtVEc1Xy20U7vsB2yAoB4nBlfH5HZJeatRXHleO+OS5Ot+MWij4Dpltw4/DyIkqUfqz1epfhVR5XWWQPA==", - "dev": true, - "requires": { - "confusing-browser-globals": "^1.0.10", - "object.assign": "^4.1.2", - "object.entries": "^1.1.2" - } - }, "eslint-config-prettier": { "version": "8.5.0", "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-8.5.0.tgz", @@ -19263,13 +20188,14 @@ "requires": {} }, "eslint-import-resolver-node": { - "version": "0.3.6", - "resolved": "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.6.tgz", - "integrity": "sha512-0En0w03NRVMn9Uiyn8YRPDKvWjxCWkslUEhGNTdGx15RvPJYQ+lbOlqrlNI2vEAs4pDYK4f/HN2TbDmk5TP0iw==", + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.9.tgz", + "integrity": "sha512-WFj2isz22JahUv+B788TlO3N6zL3nNJGU8CcZbPZvVEkBPaJdCV4vy5wyghty5ROFbCRnm132v8BScu5/1BQ8g==", "dev": true, "requires": { "debug": "^3.2.7", - "resolve": "^1.20.0" + "is-core-module": "^2.13.0", + "resolve": "^1.22.4" }, "dependencies": { "debug": { @@ -19284,13 +20210,12 @@ } }, "eslint-module-utils": { - "version": "2.7.3", - "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.7.3.tgz", - "integrity": "sha512-088JEC7O3lDZM9xGe0RerkOMd0EjFl+Yvd1jPWIkMT5u3H9+HC34mWWPnqPrN13gieT9pBOO+Qt07Nb/6TresQ==", + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.12.0.tgz", + "integrity": "sha512-wALZ0HFoytlyh/1+4wuZ9FJCD/leWHQzzrxJ8+rebyReSLk7LApMyd3WJaLVoN+D5+WIdJyDK1c6JnE65V4Zyg==", "dev": true, "requires": { - "debug": "^3.2.7", - "find-up": "^2.1.0" + "debug": "^3.2.7" }, "dependencies": { "debug": { @@ -19301,81 +20226,59 @@ "requires": { "ms": "^2.1.1" } - }, - "find-up": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-2.1.0.tgz", - "integrity": "sha1-RdG35QbHF93UgndaK3eSCjwMV6c=", - "dev": true, - "requires": { - "locate-path": "^2.0.0" - } - }, - "locate-path": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-2.0.0.tgz", - "integrity": "sha1-K1aLJl7slExtnA3pw9u7ygNUzY4=", - "dev": true, - "requires": { - "p-locate": "^2.0.0", - "path-exists": "^3.0.0" - } - }, - "p-limit": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-1.3.0.tgz", - "integrity": "sha512-vvcXsLAJ9Dr5rQOPk7toZQZJApBl2K4J6dANSsEuh6QI41JYcsS/qhTGa9ErIUUgK3WNQoJYvylxvjqmiqEA9Q==", - "dev": true, - "requires": { - "p-try": "^1.0.0" - } - }, - "p-locate": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-2.0.0.tgz", - "integrity": "sha1-IKAQOyIqcMj9OcwuWAaA893l7EM=", - "dev": true, - "requires": { - "p-limit": "^1.1.0" - } - }, - "p-try": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/p-try/-/p-try-1.0.0.tgz", - "integrity": "sha1-y8ec26+P1CKOE/Yh8rGiN8GyB7M=", - "dev": true } } }, "eslint-plugin-import": { - "version": "2.25.4", - "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.25.4.tgz", - "integrity": "sha512-/KJBASVFxpu0xg1kIBn9AUa8hQVnszpwgE7Ld0lKAlx7Ie87yzEzCgSkekt+le/YVhiaosO4Y14GDAOc41nfxA==", + "version": "2.31.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.31.0.tgz", + "integrity": "sha512-ixmkI62Rbc2/w8Vfxyh1jQRTdRTF52VxwRVHl/ykPAmqG+Nb7/kNn+byLP0LxPgI7zWA16Jt82SybJInmMia3A==", "dev": true, "requires": { - "array-includes": "^3.1.4", - "array.prototype.flat": "^1.2.5", - "debug": "^2.6.9", + "@rtsao/scc": "^1.1.0", + "array-includes": "^3.1.8", + "array.prototype.findlastindex": "^1.2.5", + "array.prototype.flat": "^1.3.2", + "array.prototype.flatmap": "^1.3.2", + "debug": "^3.2.7", "doctrine": "^2.1.0", - "eslint-import-resolver-node": "^0.3.6", - "eslint-module-utils": "^2.7.2", - "has": "^1.0.3", - "is-core-module": "^2.8.0", + "eslint-import-resolver-node": "^0.3.9", + "eslint-module-utils": "^2.12.0", + "hasown": "^2.0.2", + "is-core-module": "^2.15.1", "is-glob": "^4.0.3", - "minimatch": "^3.0.4", - "object.values": "^1.1.5", - "resolve": "^1.20.0", - "tsconfig-paths": "^3.12.0" + "minimatch": "^3.1.2", + "object.fromentries": "^2.0.8", + "object.groupby": "^1.0.3", + "object.values": "^1.2.0", + "semver": "^6.3.1", + "string.prototype.trimend": "^1.0.8", + "tsconfig-paths": "^3.15.0" }, "dependencies": { + "debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "requires": { + "ms": "^2.1.1" + } + }, "minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", "dev": true, "requires": { "brace-expansion": "^1.1.7" } + }, + "semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true } } }, @@ -19416,9 +20319,9 @@ "dev": true }, "minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", "dev": true, "requires": { "brace-expansion": "^1.1.7" @@ -19426,6 +20329,12 @@ } } }, + "eslint-plugin-no-only-tests": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-no-only-tests/-/eslint-plugin-no-only-tests-3.3.0.tgz", + "integrity": "sha512-brcKcxGnISN2CcVhXJ/kEQlNa0MEfGRtwKtWA16SkqXHKitaKIMrfemJKLKX1YqDU5C/5JY3PvZXd5jEW04e0Q==", + "dev": true + }, "eslint-plugin-react": { "version": "7.29.4", "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.29.4.tgz", @@ -19455,9 +20364,9 @@ "dev": true }, "minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", "dev": true, "requires": { "brace-expansion": "^1.1.7" @@ -19474,9 +20383,9 @@ } }, "semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", "dev": true } } @@ -19498,44 +20407,27 @@ "estraverse": "^4.1.1" } }, - "eslint-utils": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-2.1.0.tgz", - "integrity": "sha512-w94dQYoauyvlDc43XnGB8lU3Zt713vNChgt4EWwhXAP2XkBvndfxF0AgIqKOOasjPIPzj9JqgwkwbCYD0/V3Zg==", - "dev": true, - "requires": { - "eslint-visitor-keys": "^1.1.0" - } - }, "eslint-visitor-keys": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.3.0.tgz", - "integrity": "sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ==", + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", "dev": true }, "espree": { - "version": "7.3.1", - "resolved": "https://registry.npmjs.org/espree/-/espree-7.3.1.tgz", - "integrity": "sha512-v3JCNCE64umkFpmkFGqzVKsOT0tN1Zr+ueqLZfpV1Ob8e+CEgPWa+OxCoGH3tnhimMKIaBm4m/vaRpJ/krRz2g==", + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", + "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==", "dev": true, "requires": { - "acorn": "^7.4.0", - "acorn-jsx": "^5.3.1", - "eslint-visitor-keys": "^1.3.0" - }, - "dependencies": { - "acorn": { - "version": "7.4.1", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.4.1.tgz", - "integrity": "sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==", - "dev": true - } + "acorn": "^8.9.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^3.4.1" } }, "esquery": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.4.0.tgz", - "integrity": "sha512-cCDispWt5vHHtwMY2YrAQ4ibFkAL8RbH5YGBnZBc90MolvvfkkQcJro/aZiAQUlQ3qgrYS6D6v8Gc5G5CQsc9w==", + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz", + "integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==", "dev": true, "requires": { "estraverse": "^5.1.0" @@ -19612,9 +20504,9 @@ }, "dependencies": { "cross-spawn": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", - "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", "dev": true, "requires": { "path-key": "^3.1.0", @@ -19657,41 +20549,6 @@ } } }, - "expand-brackets": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/expand-brackets/-/expand-brackets-2.1.4.tgz", - "integrity": "sha1-t3c14xXOMPa27/D4OwQVGiJEliI=", - "dev": true, - "requires": { - "debug": "^2.3.3", - "define-property": "^0.2.5", - "extend-shallow": "^2.0.1", - "posix-character-classes": "^0.1.0", - "regex-not": "^1.0.0", - "snapdragon": "^0.8.1", - "to-regex": "^3.0.1" - }, - "dependencies": { - "define-property": { - "version": "0.2.5", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", - "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", - "dev": true, - "requires": { - "is-descriptor": "^0.1.0" - } - }, - "extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", - "dev": true, - "requires": { - "is-extendable": "^0.1.0" - } - } - } - }, "expand-template": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", @@ -19702,7 +20559,7 @@ "expand-tilde": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/expand-tilde/-/expand-tilde-2.0.2.tgz", - "integrity": "sha1-l+gBqgUt8CRU3kawK/YhZCzchQI=", + "integrity": "sha512-A5EmesHW6rfnZ9ysHQjPdJRni0SRar0tjtG5MNtm9n5TUvsYU8oozprtRD4AqHxcZWWlVuAmQo2nWKfN9oyjTw==", "dev": true, "requires": { "homedir-polyfill": "^1.0.1" @@ -19715,23 +20572,6 @@ "dev": true, "requires": {} }, - "ext": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/ext/-/ext-1.6.0.tgz", - "integrity": "sha512-sdBImtzkq2HpkdRLtlLWDa6w4DX22ijZLKx8BMPUuKe1c5lbN6xwQDQCxSfxBQnHZ13ls/FH0MQZx/q/gr6FQg==", - "dev": true, - "requires": { - "type": "^2.5.0" - }, - "dependencies": { - "type": { - "version": "2.6.0", - "resolved": "https://registry.npmjs.org/type/-/type-2.6.0.tgz", - "integrity": "sha512-eiDBDOmkih5pMbo9OqsqPRGMljLodLcwd5XD5JbtNB0o89xZAwynY9EdCDsJU7LtcVCClu9DvM7/0Ep1hYX3EQ==", - "dev": true - } - } - }, "ext-list": { "version": "2.2.2", "resolved": "https://registry.npmjs.org/ext-list/-/ext-list-2.2.2.tgz", @@ -19778,120 +20618,31 @@ } } }, - "extglob": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/extglob/-/extglob-2.0.4.tgz", - "integrity": "sha512-Nmb6QXkELsuBr24CJSkilo6UHHgbekK5UiZgfE6UHD3Eb27YC6oD+bhcT+tJ6cl8dmsgdQxnWlcry8ksBIBLpw==", - "dev": true, - "requires": { - "array-unique": "^0.3.2", - "define-property": "^1.0.0", - "expand-brackets": "^2.1.4", - "extend-shallow": "^2.0.1", - "fragment-cache": "^0.2.1", - "regex-not": "^1.0.0", - "snapdragon": "^0.8.1", - "to-regex": "^3.0.1" - }, - "dependencies": { - "define-property": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", - "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", - "dev": true, - "requires": { - "is-descriptor": "^1.0.0" - } - }, - "extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", - "dev": true, - "requires": { - "is-extendable": "^0.1.0" - } - }, - "is-accessor-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", - "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", - "dev": true, - "requires": { - "kind-of": "^6.0.0" - } - }, - "is-data-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", - "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", - "dev": true, - "requires": { - "kind-of": "^6.0.0" - } - }, - "is-descriptor": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", - "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", - "dev": true, - "requires": { - "is-accessor-descriptor": "^1.0.0", - "is-data-descriptor": "^1.0.0", - "kind-of": "^6.0.2" - } - } - } - }, - "fancy-log": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/fancy-log/-/fancy-log-1.3.3.tgz", - "integrity": "sha512-k9oEhlyc0FrVh25qYuSELjr8oxsCoc4/LEZfg2iJJrfEk/tZL9bCoJE47gqAvI2m/AUjluCS4+3I0eTx8n3AEw==", - "dev": true, - "requires": { - "ansi-gray": "^0.1.1", - "color-support": "^1.1.3", - "parse-node-version": "^1.0.0", - "time-stamp": "^1.0.0" - } - }, "fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", "dev": true }, + "fast-fifo": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/fast-fifo/-/fast-fifo-1.3.2.tgz", + "integrity": "sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==", + "dev": true + }, "fast-glob": { "version": "3.2.11", "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.11.tgz", "integrity": "sha512-xrO3+1bxSo3ZVHAnqzyuewYT6aMFHRAd4Kcs92MAonjwQZLsK9d0SF1IyQ3k5PoirxTW0Oe/RqFgMQ6TcNE5Ew==", "dev": true, - "requires": { - "@nodelib/fs.stat": "^2.0.2", - "@nodelib/fs.walk": "^1.2.3", - "glob-parent": "^5.1.2", - "merge2": "^1.3.0", - "micromatch": "^4.0.4" - }, - "dependencies": { - "braces": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", - "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", - "dev": true, - "requires": { - "fill-range": "^7.0.1" - } - }, - "fill-range": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", - "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", - "dev": true, - "requires": { - "to-regex-range": "^5.0.1" - } - }, + "requires": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.4" + }, + "dependencies": { "glob-parent": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", @@ -19900,31 +20651,6 @@ "requires": { "is-glob": "^4.0.1" } - }, - "is-number": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", - "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", - "dev": true - }, - "micromatch": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.4.tgz", - "integrity": "sha512-pRmzw/XUcwXGpD9aI9q/0XOwLNygjETJ8y0ao0wdqprrzDa4YnxLcz7fQRZr8voh8V10kGhABbNcHVk5wHgWwg==", - "dev": true, - "requires": { - "braces": "^3.0.1", - "picomatch": "^2.2.3" - } - }, - "to-regex-range": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", - "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", - "dev": true, - "requires": { - "is-number": "^7.0.0" - } } } }, @@ -19940,10 +20666,11 @@ "integrity": "sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=", "dev": true }, - "fast-myers-diff": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/fast-myers-diff/-/fast-myers-diff-3.0.1.tgz", - "integrity": "sha512-e8p26utONwDXeSDkDqu4jaR3l3r6ZgQO2GWB178ePZxCfFoRPNTJVZylUEHHG6uZeRikL1zCc2sl4sIAs9c0UQ==" + "fast-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.2.tgz", + "integrity": "sha512-rVjf7ArG3LTk+FS6Yw81V1DLuZl1bRbNrev6Tmd/9RaroeeRRJhAt7jg/6YFxbvAQXUCavSoZhPPj6oOx+5KjQ==", + "dev": true }, "fastest-levenshtein": { "version": "1.0.12", @@ -19984,17 +20711,10 @@ "integrity": "sha512-rM0UO7Qm9K7TWTtA6AShI/t7H5BPjDeGVDaNyg9BjHAj3PysKy7+8C8D137R88jnR3rFJZQB/tFgydl5sN5m7g==", "dev": true }, - "file-uri-to-path": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", - "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==", - "dev": true, - "optional": true - }, "filename-reserved-regex": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/filename-reserved-regex/-/filename-reserved-regex-2.0.0.tgz", - "integrity": "sha1-q/c9+rc10EVECr/qLZHzieu/oik=", + "integrity": "sha512-lc1bnsSr4L4Bdif8Xb/qrtokGbq5zlsms/CYH8PP+WtCkGNF65DPiQY8vG3SakEdRn8Dlnm+gW/qWKKjS5sZzQ==", "dev": true }, "filenamify": { @@ -20009,26 +20729,12 @@ } }, "fill-range": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz", - "integrity": "sha1-1USBHUKPmOsGpj3EAtJAPDKMOPc=", + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", "dev": true, "requires": { - "extend-shallow": "^2.0.1", - "is-number": "^3.0.0", - "repeat-string": "^1.6.1", - "to-regex-range": "^2.1.0" - }, - "dependencies": { - "extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", - "dev": true, - "requires": { - "is-extendable": "^0.1.0" - } - } + "to-regex-range": "^5.0.1" } }, "filter-obj": { @@ -20037,6 +20743,17 @@ "integrity": "sha512-lO3ttPjHZRfjMcxWKb1j1eDhTFsu4meeR3lnMcnBFhk6RuLhvEiuALu2TlfL310ph4lCYYwgF/ElIjdP739tdg==", "dev": true }, + "find-cache-dir": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-3.3.2.tgz", + "integrity": "sha512-wXZV5emFEjrridIgED11OoUKLxiYjAcqot/NJdAkOhlJ+vGzwhOAfcG5OX1jP+S0PcjEn8bdMJv+g2jwQ3Onig==", + "dev": true, + "requires": { + "commondir": "^1.0.1", + "make-dir": "^3.0.2", + "pkg-dir": "^4.1.0" + } + }, "find-up": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", @@ -20045,45 +20762,45 @@ "requires": { "locate-path": "^5.0.0", "path-exists": "^4.0.0" - }, - "dependencies": { - "path-exists": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", - "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", - "dev": true - } } }, "findup-sync": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/findup-sync/-/findup-sync-3.0.0.tgz", - "integrity": "sha512-YbffarhcicEhOrm4CtrwdKBdCuz576RLdhJDsIfvNtxUuhdRet1qZcsMjqbePtAseKdAnDyM/IyXbu7PRPRLYg==", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/findup-sync/-/findup-sync-5.0.0.tgz", + "integrity": "sha512-MzwXju70AuyflbgeOhzvQWAvvQdo1XL0A9bVvlXsYcFEBM87WR4OakL4OfZq+QRmr+duJubio+UtNQCPsVESzQ==", "dev": true, "requires": { "detect-file": "^1.0.0", - "is-glob": "^4.0.0", - "micromatch": "^3.0.4", + "is-glob": "^4.0.3", + "micromatch": "^4.0.4", "resolve-dir": "^1.0.1" } }, "fined": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/fined/-/fined-1.2.0.tgz", - "integrity": "sha512-ZYDqPLGxDkDhDZBjZBb+oD1+j0rA4E0pXY50eplAAOPg2N/gUBSSk5IM1/QhPfyVo19lJ+CvXpqfvk+b2p/8Ng==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fined/-/fined-2.0.0.tgz", + "integrity": "sha512-OFRzsL6ZMHz5s0JrsEr+TpdGNCtrVtnuG3x1yzGNiQHT0yaDnXAj8V/lWcpJVrnoDpcwXcASxAZYbuXda2Y82A==", "dev": true, "requires": { "expand-tilde": "^2.0.2", - "is-plain-object": "^2.0.3", + "is-plain-object": "^5.0.0", "object.defaults": "^1.1.0", - "object.pick": "^1.2.0", - "parse-filepath": "^1.0.1" + "object.pick": "^1.3.0", + "parse-filepath": "^1.0.2" + }, + "dependencies": { + "is-plain-object": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-5.0.0.tgz", + "integrity": "sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==", + "dev": true + } } }, "flagged-respawn": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/flagged-respawn/-/flagged-respawn-1.0.1.tgz", - "integrity": "sha512-lNaHNVymajmk0OJMBn8fVUAU1BtDeKIqKoVhk4xAALB57aALg6b4W0MfJ/cUE0g9YBXy5XhSlPIpYIJ7HaY/3Q==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/flagged-respawn/-/flagged-respawn-2.0.0.tgz", + "integrity": "sha512-Gq/a6YCi8zexmGHMuJwahTGzXlAZAOsbCVKduWXC6TlLCjjFRlExMJc4GC2NYPYZ0r/brw9P7CpRgQmlPVeOoA==", "dev": true }, "flat": { @@ -20103,9 +20820,9 @@ } }, "flatted": { - "version": "3.2.4", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.2.4.tgz", - "integrity": "sha512-8/sOawo8tJ4QOBX8YlQBMxL8+RLZfxMQOif9o0KUKTNTjMYElWPE0r/m5VNFxTRd0NSw8qSy8dajrwX4RYI1Hw==", + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.2.tgz", + "integrity": "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==", "dev": true }, "flush-write-stream": { @@ -20118,22 +20835,29 @@ "readable-stream": "^2.3.6" } }, - "follow-redirects": { - "version": "1.15.1", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.1.tgz", - "integrity": "sha512-yLAMQs+k0b2m7cVxpS1VKJVvoz7SS9Td1zss3XRwXj+ZDH00RJgnuLx7E44wx02kQLrdM3aOOy+FpzS7+8OizA==" + "for-each": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz", + "integrity": "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==", + "dev": true, + "requires": { + "is-callable": "^1.2.7" + } }, "for-in": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/for-in/-/for-in-1.0.2.tgz", - "integrity": "sha1-gQaNKVqBQuwKxybG4iAMMPttXoA=", + "integrity": "sha512-7EwmXrOjyL+ChxMhmG5lnW9MPt1aIeZEwKhQzoBUdTV0N3zuwWDZYVJatDvZ2OyzPUvdIAZDsCetk3coyMfcnQ==", "dev": true }, - "foreach": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/foreach/-/foreach-2.0.5.tgz", - "integrity": "sha1-C+4AUBiusmDQo6865ljdATbsG5k=", - "dev": true + "for-own": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/for-own/-/for-own-1.0.0.tgz", + "integrity": "sha512-0OABksIGrxKK8K4kynWkQ7y1zounQxP+CWnyclVwj81KW3vlLlGUx57DKGcP/LH216GzqnstnPocF16Nxs0Ycg==", + "dev": true, + "requires": { + "for-in": "^1.0.1" + } }, "foreground-child": { "version": "2.0.0", @@ -20146,9 +20870,9 @@ }, "dependencies": { "cross-spawn": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.1.tgz", - "integrity": "sha512-u7v4o84SwFpD32Z8IIcPZ6z1/ie24O6RU3RbtL5Y316l3KuHVPx9ItBgWQ6VlfAFnRnTtMUrsQ9MUUTuEZjogg==", + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", "dev": true, "requires": { "path-key": "^3.1.0", @@ -20180,28 +20904,21 @@ } }, "form-data": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", - "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz", + "integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==", "requires": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", "mime-types": "^2.1.12" } }, - "fragment-cache": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/fragment-cache/-/fragment-cache-0.2.1.tgz", - "integrity": "sha1-QpD60n8T6Jvn8zeZxrxaCr//DRk=", - "dev": true, - "requires": { - "map-cache": "^0.2.2" - } - }, "from2": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/from2/-/from2-2.3.0.tgz", - "integrity": "sha1-i/tVAr3kpNNs/e6gB/zKIdfjgq8=", + "integrity": "sha512-OMcX/4IC/uqEPVgGeyfN22LJk6AZrMkRZHxcHBMBvHScDGgwTm2GT2Wkgtocyd3JfZffjj2kYUDXXII0Fk9W0g==", "dev": true, "requires": { "inherits": "^2.0.1", @@ -20221,9 +20938,9 @@ "dev": true }, "fs-extra": { - "version": "10.0.1", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.0.1.tgz", - "integrity": "sha512-NbdoVMZso2Lsrn/QwLXOy6rm0ufY2zEOKCDzJR/0kBsb0E6qed0P3iYK+Ath3BfvXEeu4JhEtXLgILx5psUfag==", + "version": "11.2.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.2.0.tgz", + "integrity": "sha512-PmDi3uwK5nFuXh7XDTlVnS17xJS7vW36is2+w3xcv8SVxiB4NyATf4ctkVY5bkSjX0Y4nbvZCq1/EjtEyr9ktw==", "requires": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", @@ -20277,98 +20994,62 @@ "dev": true, "optional": true }, - "fstream": { - "version": "1.0.12", - "resolved": "https://registry.npmjs.org/fstream/-/fstream-1.0.12.tgz", - "integrity": "sha512-WvJ193OHa0GHPEL+AycEJgxvBEwyfRkN1vhjca23OaPVMCaLCXTd5qAu82AjTcgP1UJmytkOKb63Ypde7raDIg==", + "function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==" + }, + "function.prototype.name": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.6.tgz", + "integrity": "sha512-Z5kx79swU5P27WEayXM1tBi5Ze/lbIyiNgU3qyXUOf9b2rgXYyF9Dy9Cx+IQv/Lc8WCG6L82zwUPpSS9hGehIg==", "dev": true, "requires": { - "graceful-fs": "^4.1.2", - "inherits": "~2.0.0", - "mkdirp": ">=0.5 0", - "rimraf": "2" - }, - "dependencies": { - "rimraf": { - "version": "2.7.1", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", - "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", - "dev": true, - "requires": { - "glob": "^7.1.3" - } - } + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1", + "functions-have-names": "^1.2.3" } }, - "function-bind": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", - "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", + "functions-have-names": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz", + "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==", "dev": true }, - "functional-red-black-tree": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz", - "integrity": "sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc=", + "gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", "dev": true }, - "gauge": { - "version": "2.7.4", - "resolved": "https://registry.npmjs.org/gauge/-/gauge-2.7.4.tgz", - "integrity": "sha1-LANAXHU4w51+s3sxcCLjJfsBi/c=", - "dev": true, - "optional": true, - "requires": { - "aproba": "^1.0.3", - "console-control-strings": "^1.0.0", - "has-unicode": "^2.0.0", - "object-assign": "^4.1.0", - "signal-exit": "^3.0.0", - "string-width": "^1.0.1", - "strip-ansi": "^3.0.1", - "wide-align": "^1.1.0" - }, - "dependencies": { - "ansi-regex": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", - "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", - "dev": true, - "optional": true - }, - "strip-ansi": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", - "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", - "dev": true, - "optional": true, - "requires": { - "ansi-regex": "^2.0.0" - } - } - } - }, "get-caller-file": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-1.0.3.tgz", - "integrity": "sha512-3t6rVToeoZfYSGd8YoLFR2DJkiQrIiUrGcjvFX2mDw3bn6k2OtwHN0TNCLbBO+w8qTvimhDkv+LSscbJY1vE6w==", + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", "dev": true }, "get-func-name": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.0.tgz", - "integrity": "sha1-6td0q+5y4gQJQzoGY2YCPdaIekE=", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.2.tgz", + "integrity": "sha512-8vXOvuE167CtIc3OyItco7N/dpRtBbYOsPsXCz7X/PMnlGjYjSGuZJgM1Y7mmew7BKf9BqvLX2tnOVy1BBUsxQ==", "dev": true }, "get-intrinsic": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.1.1.tgz", - "integrity": "sha512-kWZrnVM42QCiEA2Ig1bG8zjoIMOgxWwYCEeNdwY6Tv/cOSeGpcoX4pXHfKUxNKVoArnrEr2e9srnAxxGIraS9Q==", - "dev": true, + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", "requires": { - "function-bind": "^1.1.1", - "has": "^1.0.3", - "has-symbols": "^1.0.1" + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" } }, "get-package-type": { @@ -20383,32 +21064,51 @@ "integrity": "sha512-g/Q1aTSDOxFpchXC4i8ZWvxA1lnPqx/JHqcpIw0/LX9T8x/GBbi6YnlN5nhaKIFkT8oFsscUKgDJYxfwfS6QsQ==", "dev": true }, + "get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "requires": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + } + }, "get-stream": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-3.0.0.tgz", - "integrity": "sha1-jpQ9E1jcN1VQVOy+LtsFqhdO3hQ=", - "dev": true + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-4.1.0.tgz", + "integrity": "sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w==", + "dev": true, + "requires": { + "pump": "^3.0.0" + }, + "dependencies": { + "pump": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.2.tgz", + "integrity": "sha512-tUPXtzlGM8FE3P0ZL6DVs/3P58k9nk8/jZeQCurTJylQA8qFYzHFfhBJkuqyE0FifOsQ0uKWekiZ5g8wtr28cw==", + "dev": true, + "requires": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + } + } }, "get-symbol-description": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.0.0.tgz", - "integrity": "sha512-2EmdH1YvIQiZpltCNgkuiUnyukzxM/R6NDJX31Ke3BG1Nq5b0S2PhX59UKi9vZpPDQVdqn+1IcaAwnzTT5vCjw==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.0.2.tgz", + "integrity": "sha512-g0QYk1dZBxGwk+Ngc+ltRH2IBp2f7zBkBMBJZCDerh6EhlhSR6+9irMCuT/09zD6qkarHUSn529sK/yL4S27mg==", "dev": true, "requires": { - "call-bind": "^1.0.2", - "get-intrinsic": "^1.1.1" + "call-bind": "^1.0.5", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.4" } }, - "get-value": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/get-value/-/get-value-2.0.6.tgz", - "integrity": "sha1-3BXKHGcjh8p2vTesCjlbogQqLCg=", - "dev": true - }, "github-from-package": { "version": "0.0.0", "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz", - "integrity": "sha1-l/tdlr/eiXMxPyDoKI75oWf6ZM4=", + "integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==", "dev": true, "optional": true }, @@ -20426,9 +21126,9 @@ }, "dependencies": { "minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", "requires": { "brace-expansion": "^1.1.7" } @@ -20481,77 +21181,13 @@ "dev": true }, "glob-watcher": { - "version": "5.0.5", - "resolved": "https://registry.npmjs.org/glob-watcher/-/glob-watcher-5.0.5.tgz", - "integrity": "sha512-zOZgGGEHPklZNjZQaZ9f41i7F2YwE+tS5ZHrDhbBCk3stwahn5vQxnFmBJZHoYdusR6R1bLSXeGUy/BhctwKzw==", + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/glob-watcher/-/glob-watcher-6.0.0.tgz", + "integrity": "sha512-wGM28Ehmcnk2NqRORXFOTOR064L4imSw3EeOqU5bIwUf62eXGwg89WivH6VMahL8zlQHeodzvHpXplrqzrz3Nw==", "dev": true, "requires": { - "anymatch": "^2.0.0", - "async-done": "^1.2.0", - "chokidar": "^2.0.0", - "is-negated-glob": "^1.0.0", - "just-debounce": "^1.0.0", - "normalize-path": "^3.0.0", - "object.defaults": "^1.1.0" - }, - "dependencies": { - "binary-extensions": { - "version": "1.13.1", - "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-1.13.1.tgz", - "integrity": "sha512-Un7MIEDdUC5gNpcGDV97op1Ywk748MpHcFTHoYs6qnj1Z3j7I53VG3nwZhKzoBZmbdRNnb6WRdFlwl7tSDuZGw==", - "dev": true - }, - "chokidar": { - "version": "2.1.8", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-2.1.8.tgz", - "integrity": "sha512-ZmZUazfOzf0Nve7duiCKD23PFSCs4JPoYyccjUFF3aQkQadqBhfzhjkwBH2mNOG9cTBwhamM37EIsIkZw3nRgg==", - "dev": true, - "requires": { - "anymatch": "^2.0.0", - "async-each": "^1.0.1", - "braces": "^2.3.2", - "fsevents": "^1.2.7", - "glob-parent": "^3.1.0", - "inherits": "^2.0.3", - "is-binary-path": "^1.0.0", - "is-glob": "^4.0.0", - "normalize-path": "^3.0.0", - "path-is-absolute": "^1.0.0", - "readdirp": "^2.2.1", - "upath": "^1.1.1" - } - }, - "fsevents": { - "version": "1.2.13", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-1.2.13.tgz", - "integrity": "sha512-oWb1Z6mkHIskLzEJ/XWX0srkpkTQ7vaopMQkyaEIoq0fmtFVxOthb8cCxeT+p3ynTdkk/RZwbgG4brR5BeWECw==", - "dev": true, - "optional": true, - "requires": { - "bindings": "^1.5.0", - "nan": "^2.12.1" - } - }, - "is-binary-path": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-1.0.1.tgz", - "integrity": "sha1-dfFmQrSA8YenEcgUFh/TpKdlWJg=", - "dev": true, - "requires": { - "binary-extensions": "^1.0.0" - } - }, - "readdirp": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-2.2.1.tgz", - "integrity": "sha512-1JU/8q+VgFZyxwrJ+SVIOsh+KywWGpds3NTqikiKpDMZWScmAYyKIgqkO+ARvNWJfXeXR1zxz7aHF4u4CyH6vQ==", - "dev": true, - "requires": { - "graceful-fs": "^4.1.11", - "micromatch": "^3.1.10", - "readable-stream": "^2.0.2" - } - } + "async-done": "^2.0.0", + "chokidar": "^3.5.3" } }, "global-modules": { @@ -20568,7 +21204,7 @@ "global-prefix": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/global-prefix/-/global-prefix-1.0.2.tgz", - "integrity": "sha1-2/dDxsFJklk8ZVVoy2btMsASLr4=", + "integrity": "sha512-5lsx1NUDHtSjfg0eHlmYvZKv8/nVqX4ckFbM+FrGcQ+04KWcWFo9P5MxPZYSzUvyzmdTbI7Eix8Q4IbELDqzKg==", "dev": true, "requires": { "expand-tilde": "^2.0.2", @@ -20595,6 +21231,16 @@ "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", "dev": true }, + "globalthis": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.4.tgz", + "integrity": "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==", + "dev": true, + "requires": { + "define-properties": "^1.2.1", + "gopd": "^1.0.1" + } + }, "globby": { "version": "11.1.0", "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", @@ -20610,14 +21256,19 @@ } }, "glogg": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/glogg/-/glogg-1.0.2.tgz", - "integrity": "sha512-5mwUoSuBk44Y4EshyiqcH95ZntbDdTQqA3QYSrxmzj28Ai0vXBGMH1ApSANH14j2sIRtqCEyg6PfsuP7ElOEDA==", + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/glogg/-/glogg-2.2.0.tgz", + "integrity": "sha512-eWv1ds/zAlz+M1ioHsyKJomfY7jbDDPpwSkv14KQj89bycx1nvK5/2Cj/T9g7kzJcX5Bc7Yv22FjfBZS/jl94A==", "dev": true, "requires": { - "sparkles": "^1.0.0" + "sparkles": "^2.1.0" } }, + "gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==" + }, "got": { "version": "8.3.2", "resolved": "https://registry.npmjs.org/got/-/got-8.3.2.tgz", @@ -20643,119 +21294,268 @@ "url-to-options": "^1.0.1" }, "dependencies": { + "get-stream": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-3.0.0.tgz", + "integrity": "sha512-GlhdIUuVakc8SJ6kK0zAFbiGzRFzNnY4jUuEbV9UROo4Y+0Ny4fjvcZFVTeDA4odpFyOQzaw6hXukJSq/f28sQ==", + "dev": true + }, "pify": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", - "integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=", + "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", + "integrity": "sha512-C3FsVNH1udSEX48gGX1xfvwTWfsYWj5U+8/uK15BGzIGrKoUpghX8hWZwa/OFnakBiiVNmBvemTJR5mcy7iPcg==", + "dev": true + } + } + }, + "graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==" + }, + "graphemer": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", + "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", + "dev": true + }, + "gulp": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/gulp/-/gulp-5.0.0.tgz", + "integrity": "sha512-S8Z8066SSileaYw1S2N1I64IUc/myI2bqe2ihOBzO6+nKpvNSg7ZcWJt/AwF8LC/NVN+/QZ560Cb/5OPsyhkhg==", + "dev": true, + "requires": { + "glob-watcher": "^6.0.0", + "gulp-cli": "^3.0.0", + "undertaker": "^2.0.0", + "vinyl-fs": "^4.0.0" + }, + "dependencies": { + "convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true + }, + "fs-mkdirp-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/fs-mkdirp-stream/-/fs-mkdirp-stream-2.0.1.tgz", + "integrity": "sha512-UTOY+59K6IA94tec8Wjqm0FSh5OVudGNB0NL/P6fB3HiE3bYOY3VYBGijsnOHNkQSwC1FKkU77pmq7xp9CskLw==", + "dev": true, + "requires": { + "graceful-fs": "^4.2.8", + "streamx": "^2.12.0" + } + }, + "glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "requires": { + "is-glob": "^4.0.3" + } + }, + "glob-stream": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/glob-stream/-/glob-stream-8.0.2.tgz", + "integrity": "sha512-R8z6eTB55t3QeZMmU1C+Gv+t5UnNRkA55c5yo67fAVfxODxieTwsjNG7utxS/73NdP1NbDgCrhVEg2h00y4fFw==", + "dev": true, + "requires": { + "@gulpjs/to-absolute-glob": "^4.0.0", + "anymatch": "^3.1.3", + "fastq": "^1.13.0", + "glob-parent": "^6.0.2", + "is-glob": "^4.0.3", + "is-negated-glob": "^1.0.0", + "normalize-path": "^3.0.0", + "streamx": "^2.12.5" + } + }, + "lead": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/lead/-/lead-4.0.0.tgz", + "integrity": "sha512-DpMa59o5uGUWWjruMp71e6knmwKU3jRBBn1kjuLWN9EeIOxNeSAwvHf03WIl8g/ZMR2oSQC9ej3yeLBwdDc/pg==", + "dev": true + }, + "now-and-later": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/now-and-later/-/now-and-later-3.0.0.tgz", + "integrity": "sha512-pGO4pzSdaxhWTGkfSfHx3hVzJVslFPwBp2Myq9MYN/ChfJZF87ochMAXnvz6/58RJSf5ik2q9tXprBBrk2cpcg==", + "dev": true, + "requires": { + "once": "^1.4.0" + } + }, + "replace-ext": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/replace-ext/-/replace-ext-2.0.0.tgz", + "integrity": "sha512-UszKE5KVK6JvyD92nzMn9cDapSk6w/CaFZ96CnmDMUqH9oowfxF/ZjRITD25H4DnOQClLA4/j7jLGXXLVKxAug==", + "dev": true + }, + "resolve-options": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/resolve-options/-/resolve-options-2.0.0.tgz", + "integrity": "sha512-/FopbmmFOQCfsCx77BRFdKOniglTiHumLgwvd6IDPihy1GKkadZbgQJBcTb2lMzSR1pndzd96b1nZrreZ7+9/A==", + "dev": true, + "requires": { + "value-or-function": "^4.0.0" + } + }, + "to-through": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/to-through/-/to-through-3.0.0.tgz", + "integrity": "sha512-y8MN937s/HVhEoBU1SxfHC+wxCHkV1a9gW8eAdTadYh/bGyesZIVcbjI+mSpFbSVwQici/XjBjuUyri1dnXwBw==", + "dev": true, + "requires": { + "streamx": "^2.12.5" + } + }, + "value-or-function": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/value-or-function/-/value-or-function-4.0.0.tgz", + "integrity": "sha512-aeVK81SIuT6aMJfNo9Vte8Dw0/FZINGBV8BfCraGtqVxIeLAEhJyoWs8SmvRVmXfGss2PmmOwZCuBPbZR+IYWg==", "dev": true + }, + "vinyl": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/vinyl/-/vinyl-3.0.0.tgz", + "integrity": "sha512-rC2VRfAVVCGEgjnxHUnpIVh3AGuk62rP3tqVrn+yab0YH7UULisC085+NYH+mnqf3Wx4SpSi1RQMwudL89N03g==", + "dev": true, + "requires": { + "clone": "^2.1.2", + "clone-stats": "^1.0.0", + "remove-trailing-separator": "^1.1.0", + "replace-ext": "^2.0.0", + "teex": "^1.0.1" + } + }, + "vinyl-fs": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/vinyl-fs/-/vinyl-fs-4.0.0.tgz", + "integrity": "sha512-7GbgBnYfaquMk3Qu9g22x000vbYkOex32930rBnc3qByw6HfMEAoELjCjoJv4HuEQxHAurT+nvMHm6MnJllFLw==", + "dev": true, + "requires": { + "fs-mkdirp-stream": "^2.0.1", + "glob-stream": "^8.0.0", + "graceful-fs": "^4.2.11", + "iconv-lite": "^0.6.3", + "is-valid-glob": "^1.0.0", + "lead": "^4.0.0", + "normalize-path": "3.0.0", + "resolve-options": "^2.0.0", + "stream-composer": "^1.0.2", + "streamx": "^2.14.0", + "to-through": "^3.0.0", + "value-or-function": "^4.0.0", + "vinyl": "^3.0.0", + "vinyl-sourcemap": "^2.0.0" + } + }, + "vinyl-sourcemap": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/vinyl-sourcemap/-/vinyl-sourcemap-2.0.0.tgz", + "integrity": "sha512-BAEvWxbBUXvlNoFQVFVHpybBbjW1r03WhohJzJDSfgrrK5xVYIDTan6xN14DlyImShgDRv2gl9qhM6irVMsV0Q==", + "dev": true, + "requires": { + "convert-source-map": "^2.0.0", + "graceful-fs": "^4.2.10", + "now-and-later": "^3.0.0", + "streamx": "^2.12.5", + "vinyl": "^3.0.0", + "vinyl-contents": "^2.0.0" + } } } }, - "graceful-fs": { - "version": "4.2.9", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.9.tgz", - "integrity": "sha512-NtNxqUcXgpW2iMrfqSfR73Glt39K+BLwWsPs94yR63v45T0Wbej7eRmL5cWfwEgqXnmjQp3zaJTshdRW/qC2ZQ==" - }, - "graceful-readlink": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/graceful-readlink/-/graceful-readlink-1.0.1.tgz", - "integrity": "sha1-TK+tdrxi8C+gObL5Tpo906ORpyU=", - "dev": true - }, - "growl": { - "version": "1.10.5", - "resolved": "https://registry.npmjs.org/growl/-/growl-1.10.5.tgz", - "integrity": "sha512-qBr4OuELkhPenW6goKVXiv47US3clb3/IbuWF9KNKEijAy9oeHxU9IgzjvJhHkUzhaj7rOUD7+YGWqUjLp5oSA==", - "dev": true - }, - "gulp": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/gulp/-/gulp-4.0.2.tgz", - "integrity": "sha512-dvEs27SCZt2ibF29xYgmnwwCYZxdxhQ/+LFWlbAW8y7jt68L/65402Lz3+CKy0Ov4rOs+NERmDq7YlZaDqUIfA==", - "dev": true, - "requires": { - "glob-watcher": "^5.0.3", - "gulp-cli": "^2.2.0", - "undertaker": "^1.2.1", - "vinyl-fs": "^3.0.0" + "gulp-cli": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/gulp-cli/-/gulp-cli-3.0.0.tgz", + "integrity": "sha512-RtMIitkT8DEMZZygHK2vEuLPqLPAFB4sntSxg4NoDta7ciwGZ18l7JuhCTiS5deOJi2IoK0btE+hs6R4sfj7AA==", + "dev": true, + "requires": { + "@gulpjs/messages": "^1.1.0", + "chalk": "^4.1.2", + "copy-props": "^4.0.0", + "gulplog": "^2.2.0", + "interpret": "^3.1.1", + "liftoff": "^5.0.0", + "mute-stdout": "^2.0.0", + "replace-homedir": "^2.0.0", + "semver-greatest-satisfied-range": "^2.0.0", + "string-width": "^4.2.3", + "v8flags": "^4.0.0", + "yargs": "^16.2.0" }, "dependencies": { - "camelcase": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-3.0.0.tgz", - "integrity": "sha1-MvxLn82vhF/N9+c7uXysImHwqwo=", - "dev": true + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "requires": { + "color-convert": "^2.0.1" + } }, - "gulp-cli": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/gulp-cli/-/gulp-cli-2.3.0.tgz", - "integrity": "sha512-zzGBl5fHo0EKSXsHzjspp3y5CONegCm8ErO5Qh0UzFzk2y4tMvzLWhoDokADbarfZRL2pGpRp7yt6gfJX4ph7A==", + "chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", "dev": true, "requires": { - "ansi-colors": "^1.0.1", - "archy": "^1.0.0", - "array-sort": "^1.0.0", - "color-support": "^1.1.3", - "concat-stream": "^1.6.0", - "copy-props": "^2.0.1", - "fancy-log": "^1.3.2", - "gulplog": "^1.0.0", - "interpret": "^1.4.0", - "isobject": "^3.0.1", - "liftoff": "^3.1.0", - "matchdep": "^2.0.0", - "mute-stdout": "^1.0.0", - "pretty-hrtime": "^1.0.0", - "replace-homedir": "^1.0.0", - "semver-greatest-satisfied-range": "^1.1.0", - "v8flags": "^3.2.0", - "yargs": "^7.1.0" + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" } }, - "v8flags": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/v8flags/-/v8flags-3.2.0.tgz", - "integrity": "sha512-mH8etigqMfiGWdeXpaaqGfs6BndypxusHHcv2qSHyZkGEznCd/qAXCWWRzeowtL54147cktFOC4P5y+kl8d8Jg==", + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", "dev": true, "requires": { - "homedir-polyfill": "^1.0.1" + "color-name": "~1.1.4" } }, - "y18n": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/y18n/-/y18n-3.2.2.tgz", - "integrity": "sha512-uGZHXkHnhF0XeeAPgnKfPv1bgKAYyVvmNL1xlKsPYZPaIHxGti2hHqvOCQv71XMsLxu1QjergkqogUnms5D3YQ==", + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "dev": true }, - "yargs": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-7.1.1.tgz", - "integrity": "sha512-huO4Fr1f9PmiJJdll5kwoS2e4GqzGSsMT3PPMpOwoVkOK8ckqAewMTZyA6LXVQWflleb/Z8oPBEvNsMft0XE+g==", + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true + }, + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", "dev": true, "requires": { - "camelcase": "^3.0.0", - "cliui": "^3.2.0", - "decamelize": "^1.1.1", - "get-caller-file": "^1.0.1", - "os-locale": "^1.4.0", - "read-pkg-up": "^1.0.1", - "require-directory": "^2.1.1", - "require-main-filename": "^1.0.1", - "set-blocking": "^2.0.0", - "string-width": "^1.0.2", - "which-module": "^1.0.0", - "y18n": "^3.2.1", - "yargs-parser": "5.0.0-security.0" + "has-flag": "^4.0.0" } }, - "yargs-parser": { - "version": "5.0.0-security.0", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-5.0.0-security.0.tgz", - "integrity": "sha512-T69y4Ps64LNesYxeYGYPvfoMTt/7y1XtfpIslUeK4um+9Hu7hlGoRtaDLvdXb7+/tfq4opVa2HRY5xGip022rQ==", + "y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true + }, + "yargs": { + "version": "16.2.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", + "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==", "dev": true, "requires": { - "camelcase": "^3.0.0", - "object.assign": "^4.1.0" + "cliui": "^7.0.2", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.0", + "y18n": "^5.0.5", + "yargs-parser": "^20.2.2" } } } @@ -20799,12 +21599,12 @@ } }, "gulplog": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/gulplog/-/gulplog-1.0.0.tgz", - "integrity": "sha1-4oxNRdBey77YGDY86PnFkmIp/+U=", + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/gulplog/-/gulplog-2.2.0.tgz", + "integrity": "sha512-V2FaKiOhpR3DRXZuYdRLn/qiY0yI5XmqbTKrYbdemJ+xOh2d2MOweI/XFgMzd/9+1twdvMwllnZbWZNJ+BOm4A==", "dev": true, "requires": { - "glogg": "^1.0.0" + "glogg": "^2.2.0" } }, "gzip-size": { @@ -20826,9 +21626,9 @@ } }, "has-bigints": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.0.1.tgz", - "integrity": "sha512-LSBS2LjbNBTf6287JEbEzvJgftkF5qFkmCo9hDRpAzKhUOlJ+hx8dd4USs00SgsUNwc4617J9ki5YtEClM2ffA==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.0.2.tgz", + "integrity": "sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ==", "dev": true }, "has-flag": { @@ -20837,6 +21637,21 @@ "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", "dev": true }, + "has-property-descriptors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "dev": true, + "requires": { + "es-define-property": "^1.0.0" + } + }, + "has-proto": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.3.tgz", + "integrity": "sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q==", + "dev": true + }, "has-symbol-support-x": { "version": "1.4.2", "resolved": "https://registry.npmjs.org/has-symbol-support-x/-/has-symbol-support-x-1.4.2.tgz", @@ -20844,10 +21659,9 @@ "dev": true }, "has-symbols": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", - "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", - "dev": true + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==" }, "has-to-string-tag-x": { "version": "1.4.1", @@ -20859,51 +21673,11 @@ } }, "has-tostringtag": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.0.tgz", - "integrity": "sha512-kFjcSNhnlGV1kyoGk7OXKSawH5JOb/LzUc5w9B02hOTO0dfFRjbHQKvg1d6cf3HbeUmtU9VbbV3qzZ2Teh97WQ==", - "dev": true, - "requires": { - "has-symbols": "^1.0.2" - } - }, - "has-unicode": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", - "integrity": "sha1-4Ob+aijPUROIVeCG0Wkedx3iqLk=", - "dev": true, - "optional": true - }, - "has-value": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/has-value/-/has-value-1.0.0.tgz", - "integrity": "sha1-GLKB2lhbHFxR3vJMkw7SmgvmsXc=", - "dev": true, - "requires": { - "get-value": "^2.0.6", - "has-values": "^1.0.0", - "isobject": "^3.0.0" - } - }, - "has-values": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/has-values/-/has-values-1.0.0.tgz", - "integrity": "sha1-lbC2P+whRmGab+V/51Yo1aOe/k8=", - "dev": true, + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", "requires": { - "is-number": "^3.0.0", - "kind-of": "^4.0.0" - }, - "dependencies": { - "kind-of": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-4.0.0.tgz", - "integrity": "sha1-IIE989cSkosgc3hpGkUGb65y3Vc=", - "dev": true, - "requires": { - "is-buffer": "^1.1.5" - } - } + "has-symbols": "^1.0.3" } }, "hash-base": { @@ -20920,6 +21694,7 @@ "version": "1.1.7", "resolved": "https://registry.npmjs.org/hash.js/-/hash.js-1.1.7.tgz", "integrity": "sha512-taOaskGt4z4SOANNseOviYDvjEJinIkRgmp7LbKP2YTTmVxWBl87s/uzK9r+44BclBSp2X7K1hqeNfz9JbBeXA==", + "dev": true, "requires": { "inherits": "^2.0.3", "minimalistic-assert": "^1.0.1" @@ -20943,6 +21718,14 @@ } } }, + "hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "requires": { + "function-bind": "^1.1.2" + } + }, "he": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", @@ -20969,12 +21752,6 @@ "parse-passwd": "^1.0.0" } }, - "hosted-git-info": { - "version": "2.8.9", - "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.9.tgz", - "integrity": "sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==", - "dev": true - }, "html-escaper": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", @@ -21055,15 +21832,21 @@ "dev": true }, "ignore": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.0.tgz", - "integrity": "sha512-CmxgYGiEPCLhfLnpPp1MoRmifwEIOgjcHXxOBjv7mY96c+eWScsOP9c112ZyLdWHi0FxHjI+4uVhKYp/gcdRmQ==", + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true + }, + "immediate": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz", + "integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==", "dev": true }, "import-fresh": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", - "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", "dev": true, "requires": { "parent-module": "^1.0.0", @@ -21078,6 +21861,17 @@ } } }, + "import-in-the-middle": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/import-in-the-middle/-/import-in-the-middle-1.4.2.tgz", + "integrity": "sha512-9WOz1Yh/cvO/p69sxRmhyQwrIGGSp7EIdcb+fFNVi7CzQGQB8U1/1XrKVSbEd/GNOAeM0peJtmi7+qphe7NvAw==", + "requires": { + "acorn": "^8.8.2", + "acorn-import-assertions": "^1.9.0", + "cjs-module-lexer": "^1.2.2", + "module-details-from-path": "^1.0.3" + } + }, "import-local": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.1.0.tgz", @@ -21121,26 +21915,26 @@ "dev": true }, "internal-slot": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.3.tgz", - "integrity": "sha512-O0DB1JC/sPyZl7cIo78n5dR7eUSwwpYPiXRhTzNxZVAMUuB8vlnRFyLxdrVToks6XPLVnFfbzaVd5WLjhgg+vA==", + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.7.tgz", + "integrity": "sha512-NGnrKwXzSms2qUUih/ILZ5JBqNTSa1+ZmP6flaIp6KmSElgE9qdndzS3cqjrDovwFdmwsGsLdeFgB6suw+1e9g==", "dev": true, "requires": { - "get-intrinsic": "^1.1.0", - "has": "^1.0.3", + "es-errors": "^1.3.0", + "hasown": "^2.0.0", "side-channel": "^1.0.4" } }, "interpret": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/interpret/-/interpret-1.4.0.tgz", - "integrity": "sha512-agE4QfB2Lkp9uICn7BAqoscw4SZP9kTE2hxiFI3jBPmXJfdqiahTbUuKGsMoN2GtqL9AxhYioAcVvgsb1HvRbA==", + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/interpret/-/interpret-3.1.1.tgz", + "integrity": "sha512-6xwYfHbajpoF0xLW+iwLkhwgvLoZDfjYfoFNu8ftMoXINzwuymNLd9u/KmwtdT2GbR+/Cz66otEGEVVUHX9QLQ==", "dev": true }, "into-stream": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/into-stream/-/into-stream-3.1.0.tgz", - "integrity": "sha1-lvsKk2wSur1v8XUqF9BWFqvQlMY=", + "integrity": "sha512-TcdjPibTksa1NQximqep2r17ISRiNE9fwlfbg3F8ANdvP5/yrFTew86VcO//jk4QTaMlbjypPBq76HN2zaKfZQ==", "dev": true, "requires": { "from2": "^2.1.1", @@ -21148,15 +21942,9 @@ } }, "inversify": { - "version": "5.0.5", - "resolved": "https://registry.npmjs.org/inversify/-/inversify-5.0.5.tgz", - "integrity": "sha512-60QsfPz8NAU/GZqXu8hJ+BhNf/C/c+Hp0eDc6XMIJTxBiP36AQyyQKpBkOVTLWBFDQWYVHpbbEuIsHu9dLuJDA==" - }, - "invert-kv": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/invert-kv/-/invert-kv-1.0.0.tgz", - "integrity": "sha1-EEqOSqym09jNFXqO+L+rLXo//bY=", - "dev": true + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/inversify/-/inversify-6.0.2.tgz", + "integrity": "sha512-i9m8j/7YIv4mDuYXUAcrpKPSaju/CIly9AHK5jvCBeoiM/2KEsuCQTTP+rzSWWpLYWRukdXFSl6ZTk2/uumbiA==" }, "is-absolute": { "version": "1.0.0", @@ -21168,26 +21956,6 @@ "is-windows": "^1.0.1" } }, - "is-accessor-descriptor": { - "version": "0.1.6", - "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz", - "integrity": "sha1-qeEss66Nh2cn7u84Q/igiXtcmNY=", - "dev": true, - "requires": { - "kind-of": "^3.0.2" - }, - "dependencies": { - "kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "dev": true, - "requires": { - "is-buffer": "^1.1.5" - } - } - } - }, "is-arguments": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.1.1.tgz", @@ -21198,11 +21966,24 @@ "has-tostringtag": "^1.0.0" } }, + "is-array-buffer": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.4.tgz", + "integrity": "sha512-wcjaerHw0ydZwfhiKbXJWLDY8A7yV7KhjQOpb83hGgGfId/aQa4TOvwyzn2PuswW2gPCYEL/nEAiSVpdOj1lXw==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "get-intrinsic": "^1.2.1" + } + }, "is-bigint": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.0.2.tgz", - "integrity": "sha512-0JV5+SOCQkIdzjBK9buARcV804Ddu7A0Qet6sHi3FimE9ne6m4BGQZfRn+NZiXbBk4F4XmHfDZIipLj9pX8dSA==", - "dev": true + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.0.4.tgz", + "integrity": "sha512-zB9CruMamjym81i2JZ3UMn54PKGsQzsJeo6xvN3HJJ4CAsQNB6iRutp2To77OfCNuoxspsIhzaPoO1zyCEhFOg==", + "dev": true, + "requires": { + "has-bigints": "^1.0.1" + } }, "is-binary-path": { "version": "2.1.0", @@ -21213,44 +21994,43 @@ "binary-extensions": "^2.0.0" } }, + "is-boolean-object": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.1.2.tgz", + "integrity": "sha512-gDYaKHJmnj4aWxyj6YHyXVpdQawtVLHU5cb+eztPGczf6cjuTdwve5ZIEfgXqH4e57An1D1AKf8CZ3kYrQRqYA==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "has-tostringtag": "^1.0.0" + } + }, "is-buffer": { "version": "1.1.6", "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", - "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==" + "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==", + "dev": true }, "is-callable": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.4.tgz", - "integrity": "sha512-nsuwtxZfMX67Oryl9LCQ+upnC0Z0BgpwntpS89m1H/TLF0zNfzfLMV/9Wa/6MZsj0acpEjAO0KF1xT6ZdLl95w==", + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", + "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", "dev": true }, "is-core-module": { - "version": "2.8.1", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.8.1.tgz", - "integrity": "sha512-SdNCUs284hr40hFTFP6l0IfZ/RSrMXF3qgoRHd3/79unUTvrFO/JoXwkGm+5J/Oe3E/b5GsnG330uUNgRpu1PA==", - "dev": true, + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", + "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", "requires": { - "has": "^1.0.3" + "hasown": "^2.0.2" } }, - "is-data-descriptor": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz", - "integrity": "sha1-C17mSDiOLIYCgueT8YVv7D8wG1Y=", + "is-data-view": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-data-view/-/is-data-view-1.0.1.tgz", + "integrity": "sha512-AHkaJrsUVW6wq6JS8y3JnM/GJF/9cf+k20+iDzlSaJrinEo5+7vRiteOSwBhHRiAyQATN1AmY4hwzxJKPmYf+w==", "dev": true, "requires": { - "kind-of": "^3.0.2" - }, - "dependencies": { - "kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "dev": true, - "requires": { - "is-buffer": "^1.1.5" - } - } + "is-typed-array": "^1.1.13" } }, "is-date-object": { @@ -21262,29 +22042,10 @@ "has-tostringtag": "^1.0.0" } }, - "is-descriptor": { - "version": "0.1.6", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.6.tgz", - "integrity": "sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg==", - "dev": true, - "requires": { - "is-accessor-descriptor": "^0.1.6", - "is-data-descriptor": "^0.1.4", - "kind-of": "^5.0.0" - }, - "dependencies": { - "kind-of": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-5.1.0.tgz", - "integrity": "sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==", - "dev": true - } - } - }, - "is-extendable": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", - "integrity": "sha1-YrEQ4omkcUGOPsNqYX1HLjAd/Ik=", + "is-docker": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz", + "integrity": "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==", "dev": true }, "is-extglob": { @@ -21294,13 +22055,10 @@ "dev": true }, "is-fullwidth-code-point": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz", - "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=", - "dev": true, - "requires": { - "number-is-nan": "^1.0.0" - } + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true }, "is-generator-function": { "version": "1.0.10", @@ -21333,7 +22091,7 @@ "is-natural-number": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/is-natural-number/-/is-natural-number-4.0.1.tgz", - "integrity": "sha1-q5124dtM7VHjXeDHLr7PCfc0zeg=", + "integrity": "sha512-Y4LTamMe0DDQIIAlaer9eKebAlDSV6huy+TWhJVPlzZh2o4tRP5SQWFlLn5N0To4mDD22/qdOq+veo1cSISLgQ==", "dev": true }, "is-negated-glob": { @@ -21343,35 +22101,30 @@ "dev": true }, "is-negative-zero": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.2.tgz", - "integrity": "sha512-dqJvarLawXsFbNDeJW7zAz8ItJ9cd28YufuuFzh0G8pNHjJMnY08Dv7sYX2uF5UpQOwieAeOExEYAWWfu7ZZUA==", + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.3.tgz", + "integrity": "sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw==", "dev": true }, "is-number": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", - "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true + }, + "is-number-object": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.0.7.tgz", + "integrity": "sha512-k1U0IRzLMo7ZlYIfzRu23Oh6MiIFasgpb9X76eqfFZAqwH44UI4KTBvBYIZ1dSL9ZzChTB9ShHfLkR4pdW5krQ==", "dev": true, "requires": { - "kind-of": "^3.0.2" - }, - "dependencies": { - "kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "dev": true, - "requires": { - "is-buffer": "^1.1.5" - } - } + "has-tostringtag": "^1.0.0" } }, "is-object": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-object/-/is-object-1.0.1.tgz", - "integrity": "sha1-iVJojF7C/9awPsyF52ngKQMINHA=", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-object/-/is-object-1.0.2.tgz", + "integrity": "sha512-2rRIahhZr2UWb45fIOuvZGpFtz0TyOZLf32KxBbSoUCeZR495zCKlWUKKUByk3geS2eAs7ZAABt0Y/Rx0GiQGA==", "dev": true }, "is-path-cwd": { @@ -21389,7 +22142,7 @@ "is-plain-obj": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-1.1.0.tgz", - "integrity": "sha1-caUMhCnfync8kqOQpKA7OfzVHT4=", + "integrity": "sha512-yvkRyxmFKEOQ4pNXCmJG5AEQNlXJS5LaONXo5/cLdTZdWvsZ1ioJEonLGAosKlMWE8lwUy/bJzMjcw8az73+Fg==", "dev": true }, "is-plain-object": { @@ -21421,21 +22174,24 @@ } }, "is-retry-allowed": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-retry-allowed/-/is-retry-allowed-1.1.0.tgz", - "integrity": "sha1-EaBgVotnM5REAz0BJaYaINVk+zQ=", + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/is-retry-allowed/-/is-retry-allowed-1.2.0.tgz", + "integrity": "sha512-RUbUeKwvm3XG2VYamhJL1xFktgjvPzL0Hq8C+6yrWIswDy3BIXGqCxhxkc30N9jqK311gVU137K8Ei55/zVJRg==", "dev": true }, "is-shared-array-buffer": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.1.tgz", - "integrity": "sha512-IU0NmyknYZN0rChcKhRO1X8LYz5Isj/Fsqh8NJOSf+N/hCOTwy29F32Ik7a+QszE63IdvmwdTPDd6cZ5pg4cwA==", - "dev": true + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.3.tgz", + "integrity": "sha512-nA2hv5XIhLR3uVzDDfCIknerhx8XUKnstuOERPNNIinXG7v9u+ohXF67vxm4TPTEPU6lm61ZkwP3c9PCB97rhg==", + "dev": true, + "requires": { + "call-bind": "^1.0.7" + } }, "is-stream": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz", - "integrity": "sha1-EtSj3U5o4Lec6428hBc66A2RykQ=", + "integrity": "sha512-uQPm8kcs47jx38atAcWTVxyltQYoPT68y9aWYdV6yWXSyW8mzSat0TL6CiWdZeCdF3KrAvpVtnHbTv4RN+rqdQ==", "dev": true }, "is-string": { @@ -21457,16 +22213,12 @@ } }, "is-typed-array": { - "version": "1.1.8", - "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.8.tgz", - "integrity": "sha512-HqH41TNZq2fgtGT8WHVFVJhBVGuY3AnP3Q36K8JKXUxSxRgk/d+7NjmwG2vo2mYmXK8UYZKu0qH8bVP5gEisjA==", + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.15.tgz", + "integrity": "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==", "dev": true, "requires": { - "available-typed-arrays": "^1.0.5", - "call-bind": "^1.0.2", - "es-abstract": "^1.18.5", - "foreach": "^2.0.5", - "has-tostringtag": "^1.0.0" + "which-typed-array": "^1.1.16" } }, "is-typedarray": { @@ -21517,6 +22269,15 @@ "integrity": "sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==", "dev": true }, + "is-wsl": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz", + "integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==", + "dev": true, + "requires": { + "is-docker": "^2.0.0" + } + }, "isarray": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", @@ -21535,9 +22296,9 @@ "dev": true }, "istanbul-lib-coverage": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.0.0.tgz", - "integrity": "sha512-UiUIqxMgRDET6eR+o5HbfRYP1l0hqkWOs7vNxC/mggutCMUIhWMm8gAHb8tHlyfD3/l6rlgNA5cKdDzEAf6hEg==", + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.0.tgz", + "integrity": "sha512-eOeJ5BHCmHYvQK7xt9GkdHuzuCGS1Y6g9Gvnx3Ym33fz/HpLRYxiS0wHNr+m/MBC8B647Xt608vCDEvhl9c6Mw==", "dev": true }, "istanbul-lib-hook": { @@ -21550,198 +22311,43 @@ } }, "istanbul-lib-instrument": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-4.0.0.tgz", - "integrity": "sha512-Nm4wVHdo7ZXSG30KjZ2Wl5SU/Bw7bDx1PdaiIFzEStdjs0H12mOTncn1GVYuqQSaZxpg87VGBRsVRPGD2cD1AQ==", + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-4.0.3.tgz", + "integrity": "sha512-BXgQl9kf4WTCPCCpmFGoJkz/+uhvm7h7PFKUYxh7qarQd3ER33vHG//qaE8eN25l07YqZPpHXU9I09l/RD5aGQ==", "dev": true, "requires": { "@babel/core": "^7.7.5", - "@babel/parser": "^7.7.5", - "@babel/template": "^7.7.4", - "@babel/traverse": "^7.7.4", "@istanbuljs/schema": "^0.1.2", "istanbul-lib-coverage": "^3.0.0", "semver": "^6.3.0" }, "dependencies": { - "@babel/core": { - "version": "7.7.7", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.7.7.tgz", - "integrity": "sha512-jlSjuj/7z138NLZALxVgrx13AOtqip42ATZP7+kYl53GvDV6+4dCek1mVUo8z8c8Xnw/mx2q3d9HWh3griuesQ==", - "dev": true, - "requires": { - "@babel/code-frame": "^7.5.5", - "@babel/generator": "^7.7.7", - "@babel/helpers": "^7.7.4", - "@babel/parser": "^7.7.7", - "@babel/template": "^7.7.4", - "@babel/traverse": "^7.7.4", - "@babel/types": "^7.7.4", - "convert-source-map": "^1.7.0", - "debug": "^4.1.0", - "json5": "^2.1.0", - "lodash": "^4.17.13", - "resolve": "^1.3.2", - "semver": "^5.4.1", - "source-map": "^0.5.0" - }, - "dependencies": { - "semver": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", - "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", - "dev": true - } - } - }, - "@babel/generator": { - "version": "7.7.7", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.7.7.tgz", - "integrity": "sha512-/AOIBpHh/JU1l0ZFS4kiRCBnLi6OTHzh0RPk3h9isBxkkqELtQNFi1Vr/tiG9p1yfoUdKVwISuXWQR+hwwM4VQ==", - "dev": true, - "requires": { - "@babel/types": "^7.7.4", - "jsesc": "^2.5.1", - "lodash": "^4.17.13", - "source-map": "^0.5.0" - } - }, - "@babel/helper-function-name": { - "version": "7.7.4", - "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.7.4.tgz", - "integrity": "sha512-AnkGIdiBhEuiwdoMnKm7jfPfqItZhgRaZfMg1XX3bS25INOnLPjPG1Ppnajh8eqgt5kPJnfqrRHqFqmjKDZLzQ==", - "dev": true, - "requires": { - "@babel/helper-get-function-arity": "^7.7.4", - "@babel/template": "^7.7.4", - "@babel/types": "^7.7.4" - } - }, - "@babel/helper-get-function-arity": { - "version": "7.7.4", - "resolved": "https://registry.npmjs.org/@babel/helper-get-function-arity/-/helper-get-function-arity-7.7.4.tgz", - "integrity": "sha512-QTGKEdCkjgzgfJ3bAyRwF4yyT3pg+vDgan8DSivq1eS0gwi+KGKE5x8kRcbeFTb/673mkO5SN1IZfmCfA5o+EA==", - "dev": true, - "requires": { - "@babel/types": "^7.7.4" - } - }, - "@babel/helper-split-export-declaration": { - "version": "7.7.4", - "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.7.4.tgz", - "integrity": "sha512-guAg1SXFcVr04Guk9eq0S4/rWS++sbmyqosJzVs8+1fH5NI+ZcmkaSkc7dmtAFbHFva6yRJnjW3yAcGxjueDug==", - "dev": true, - "requires": { - "@babel/types": "^7.7.4" - } - }, - "@babel/helpers": { - "version": "7.7.4", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.7.4.tgz", - "integrity": "sha512-ak5NGZGJ6LV85Q1Zc9gn2n+ayXOizryhjSUBTdu5ih1tlVCJeuQENzc4ItyCVhINVXvIT/ZQ4mheGIsfBkpskg==", - "dev": true, - "requires": { - "@babel/template": "^7.7.4", - "@babel/traverse": "^7.7.4", - "@babel/types": "^7.7.4" - } - }, - "@babel/parser": { - "version": "7.7.7", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.7.7.tgz", - "integrity": "sha512-WtTZMZAZLbeymhkd/sEaPD8IQyGAhmuTuvTzLiCFM7iXiVdY0gc0IaI+cW0fh1BnSMbJSzXX6/fHllgHKwHhXw==", - "dev": true - }, - "@babel/template": { - "version": "7.7.4", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.7.4.tgz", - "integrity": "sha512-qUzihgVPguAzXCK7WXw8pqs6cEwi54s3E+HrejlkuWO6ivMKx9hZl3Y2fSXp9i5HgyWmj7RKP+ulaYnKM4yYxw==", - "dev": true, - "requires": { - "@babel/code-frame": "^7.0.0", - "@babel/parser": "^7.7.4", - "@babel/types": "^7.7.4" - } - }, - "@babel/traverse": { - "version": "7.7.4", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.7.4.tgz", - "integrity": "sha512-P1L58hQyupn8+ezVA2z5KBm4/Zr4lCC8dwKCMYzsa5jFMDMQAzaBNy9W5VjB+KAmBjb40U7a/H6ao+Xo+9saIw==", - "dev": true, - "requires": { - "@babel/code-frame": "^7.5.5", - "@babel/generator": "^7.7.4", - "@babel/helper-function-name": "^7.7.4", - "@babel/helper-split-export-declaration": "^7.7.4", - "@babel/parser": "^7.7.4", - "@babel/types": "^7.7.4", - "debug": "^4.1.0", - "globals": "^11.1.0", - "lodash": "^4.17.13" - } - }, - "@babel/types": { - "version": "7.7.4", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.7.4.tgz", - "integrity": "sha512-cz5Ji23KCi4T+YIE/BolWosrJuSmoZeN1EFnRtBwF+KKLi8GG/Z2c2hOJJeCXPk4mwk4QFvTmwIodJowXgttRA==", - "dev": true, - "requires": { - "esutils": "^2.0.2", - "lodash": "^4.17.13", - "to-fast-properties": "^2.0.0" - } - }, - "convert-source-map": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.7.0.tgz", - "integrity": "sha512-4FJkXzKXEDB1snCFZlLP4gpC3JILicCpGbzG9f9G7tGqGCzETQ2hWPrcinA9oU4wtf2biUaEH5065UnMeR33oA==", - "dev": true, - "requires": { - "safe-buffer": "~5.1.1" - } - }, - "debug": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", - "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", - "dev": true, - "requires": { - "ms": "^2.1.1" - } - }, "semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", - "dev": true - }, - "source-map": { - "version": "0.5.7", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", - "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=", + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", "dev": true } } }, "istanbul-lib-processinfo": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/istanbul-lib-processinfo/-/istanbul-lib-processinfo-2.0.2.tgz", - "integrity": "sha512-kOwpa7z9hme+IBPZMzQ5vdQj8srYgAtaRqeI48NGmAQ+/5yKiHLV0QbYqQpxsdEF0+w14SoB8YbnHKcXE2KnYw==", + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/istanbul-lib-processinfo/-/istanbul-lib-processinfo-2.0.3.tgz", + "integrity": "sha512-NkwHbo3E00oybX6NGJi6ar0B29vxyvNwoC7eJ4G4Yq28UfY758Hgn/heV8VRFhevPED4LXfFz0DQ8z/0kw9zMg==", "dev": true, "requires": { "archy": "^1.0.0", - "cross-spawn": "^7.0.0", - "istanbul-lib-coverage": "^3.0.0-alpha.1", - "make-dir": "^3.0.0", + "cross-spawn": "^7.0.3", + "istanbul-lib-coverage": "^3.2.0", "p-map": "^3.0.0", "rimraf": "^3.0.0", - "uuid": "^3.3.3" + "uuid": "^8.3.2" }, "dependencies": { "cross-spawn": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.1.tgz", - "integrity": "sha512-u7v4o84SwFpD32Z8IIcPZ6z1/ie24O6RU3RbtL5Y316l3KuHVPx9ItBgWQ6VlfAFnRnTtMUrsQ9MUUTuEZjogg==", + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", "dev": true, "requires": { "path-key": "^3.1.0", @@ -21780,9 +22386,9 @@ "dev": true }, "uuid": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz", - "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==", + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", "dev": true } } @@ -21827,20 +22433,20 @@ }, "dependencies": { "debug": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", - "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", "dev": true, "requires": { - "ms": "^2.1.1" + "ms": "2.1.2" } } } }, "istanbul-reports": { - "version": "3.1.4", - "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.1.4.tgz", - "integrity": "sha512-r1/DshN4KSE7xWEknZLLLLDn5CJybV3nw01VTkp6D5jzLuELlcbudfj/eSQFvrKsJuTVCGnePO7ho82Nw9zzfw==", + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.1.5.tgz", + "integrity": "sha512-nUsEMa9pBt/NOHqbcbeJEgqIlY/K7rVWUX6Lql2orY5e9roQOthbR3vtY4zzf2orPELg80fnxxk9zUyPlgwD1w==", "dev": true, "requires": { "html-escaper": "^2.0.0", @@ -21857,6 +22463,16 @@ "is-object": "^1.0.1" } }, + "jackspeak": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "dev": true, + "requires": { + "@isaacs/cliui": "^8.0.2", + "@pkgjs/parseargs": "^0.11.0" + } + }, "jest-worker": { "version": "27.5.1", "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-27.5.1.tgz", @@ -21892,9 +22508,9 @@ "dev": true }, "js-yaml": { - "version": "3.13.1", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.13.1.tgz", - "integrity": "sha512-YfbcO7jXDdyj0DGxYVSlSeQNHbD7XPWvrVWeVUujrQEoZzWJIRrCPoyk6kL6IAjAG2IolMK4T0hNUe0HOUs5Jw==", + "version": "3.14.2", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.2.tgz", + "integrity": "sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==", "dev": true, "requires": { "argparse": "^1.0.7", @@ -21918,7 +22534,7 @@ "json-buffer": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.0.tgz", - "integrity": "sha1-Wx85evx11ne96Lz8Dkfh+aPZqJg=", + "integrity": "sha512-CuUqjv0FUZIdXkHPI8MezCnFCdaTAacej1TZYulLoAg1h/PhwkdXFN4V/gzY4g+fMBCOV2xF+rp7t2XD2ns/NQ==", "dev": true }, "json-parse-even-better-errors": { @@ -21939,12 +22555,6 @@ "integrity": "sha1-nbe1lJatPzz+8wp1FC0tkwrXJlE=", "dev": true }, - "json-stringify-safe": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", - "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=", - "dev": true - }, "json5": { "version": "2.2.3", "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", @@ -21956,6 +22566,47 @@ "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.2.0.tgz", "integrity": "sha512-gfFQZrcTc8CnKXp6Y4/CBT3fTc0OVuDofpre4aEeEpSBPV5X5v4+Vmx+8snU7RLPrNHPKSgLxGo9YuQzz20o+w==" }, + "jsonwebtoken": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.2.tgz", + "integrity": "sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ==", + "dev": true, + "requires": { + "jws": "^3.2.2", + "lodash.includes": "^4.3.0", + "lodash.isboolean": "^3.0.3", + "lodash.isinteger": "^4.0.4", + "lodash.isnumber": "^3.0.3", + "lodash.isplainobject": "^4.0.6", + "lodash.isstring": "^4.0.1", + "lodash.once": "^4.0.0", + "ms": "^2.1.1", + "semver": "^7.5.4" + }, + "dependencies": { + "jwa": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.2.tgz", + "integrity": "sha512-eeH5JO+21J78qMvTIDdBXidBd6nG2kZjg5Ohz/1fpa28Z4CcsWUzJ1ZZyFq/3z3N17aZy+ZuBoHljASbL1WfOw==", + "dev": true, + "requires": { + "buffer-equal-constant-time": "^1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "jws": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/jws/-/jws-3.2.3.tgz", + "integrity": "sha512-byiJ0FLRdLdSVSReO/U4E7RoEyOCKnEnEPMjq3HxWtvzLsV08/i5RQKsFVNkCldrCaPr2vDNAOMsfs8T/Hze7g==", + "dev": true, + "requires": { + "jwa": "^1.4.2", + "safe-buffer": "^5.0.1" + } + } + } + }, "jsx-ast-utils": { "version": "3.2.1", "resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.2.1.tgz", @@ -21966,27 +22617,54 @@ "object.assign": "^4.1.2" } }, - "just-debounce": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/just-debounce/-/just-debounce-1.0.0.tgz", - "integrity": "sha1-h/zPrv/AtozRnVX2cilD+SnqNeo=", - "dev": true + "jszip": { + "version": "3.10.1", + "resolved": "https://registry.npmjs.org/jszip/-/jszip-3.10.1.tgz", + "integrity": "sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==", + "dev": true, + "requires": { + "lie": "~3.3.0", + "pako": "~1.0.2", + "readable-stream": "~2.3.6", + "setimmediate": "^1.0.5" + } }, "just-extend": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/just-extend/-/just-extend-4.2.1.tgz", - "integrity": "sha512-g3UB796vUFIY90VIv/WX3L2c8CS2MdWUww3CNrYmqza1Fg0DURc2K/O4YrnklBdQarSJ/y8JnJYDGc+1iumQjg==", + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/just-extend/-/just-extend-6.2.0.tgz", + "integrity": "sha512-cYofQu2Xpom82S6qD778jBDpwvvy39s1l/hrYij2u9AMdQcGRpaBu6kY4mVhuno5kJVi1DAz4aiphA2WI1/OAw==", "dev": true }, + "jwa": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.1.tgz", + "integrity": "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==", + "dev": true, + "requires": { + "buffer-equal-constant-time": "^1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "jws": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.1.tgz", + "integrity": "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==", + "dev": true, + "requires": { + "jwa": "^2.0.1", + "safe-buffer": "^5.0.1" + } + }, "keytar": { - "version": "7.7.0", - "resolved": "https://registry.npmjs.org/keytar/-/keytar-7.7.0.tgz", - "integrity": "sha512-YEY9HWqThQc5q5xbXbRwsZTh2PJ36OSYRjSv3NN2xf5s5dpLTjEZnC2YikR29OaVybf9nQ0dJ/80i40RS97t/A==", + "version": "7.9.0", + "resolved": "https://registry.npmjs.org/keytar/-/keytar-7.9.0.tgz", + "integrity": "sha512-VPD8mtVtm5JNtA2AErl6Chp06JBfy7diFQ7TQQhdpWOl6MrCRB+eRbvAZUsbGQS9kiMq0coJsy0W0vHpDCkWsQ==", "dev": true, "optional": true, "requires": { - "node-addon-api": "^3.0.0", - "prebuild-install": "^6.0.0" + "node-addon-api": "^4.3.0", + "prebuild-install": "^7.0.1" } }, "keyv": { @@ -22020,14 +22698,10 @@ } }, "last-run": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/last-run/-/last-run-1.1.1.tgz", - "integrity": "sha1-RblpQsF7HHnHchmCWbqUO+v4yls=", - "dev": true, - "requires": { - "default-resolution": "^2.0.0", - "es6-weak-map": "^2.0.1" - } + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/last-run/-/last-run-2.0.0.tgz", + "integrity": "sha512-j+y6WhTLN4Itnf9j5ZQos1BGPCS8DAwmgMroR3OzfxAsBxam0hMw7J8M3KqZl0pLQJ1jNnwIexg5DYpC/ctwEQ==", + "dev": true }, "lazystream": { "version": "1.0.0", @@ -22038,15 +22712,6 @@ "readable-stream": "^2.0.5" } }, - "lcid": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/lcid/-/lcid-1.0.0.tgz", - "integrity": "sha1-MIrMr6C8SDo4Z7S28rlQYlHRuDU=", - "dev": true, - "requires": { - "invert-kv": "^1.0.0" - } - }, "lead": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/lead/-/lead-1.0.0.tgz", @@ -22062,71 +22727,61 @@ "integrity": "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==", "dev": true }, - "liftoff": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/liftoff/-/liftoff-3.1.0.tgz", - "integrity": "sha512-DlIPlJUkCV0Ips2zf2pJP0unEoT1kwYhiiPUGF3s/jtxTCjziNLoiVVh+jqWOWeFi6mmwQ5fNxvAUyPad4Dfog==", + "levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", "dev": true, "requires": { - "extend": "^3.0.0", - "findup-sync": "^3.0.0", - "fined": "^1.0.1", - "flagged-respawn": "^1.0.0", - "is-plain-object": "^2.0.4", - "object.map": "^1.0.0", - "rechoir": "^0.6.2", - "resolve": "^1.1.7" + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" } }, - "linkify-it": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-3.0.3.tgz", - "integrity": "sha512-ynTsyrFSdE5oZ/O9GEf00kPngmOfVwazR5GKDq6EYfhlpFug3J2zybX56a2PRRpc9P+FuSoGNAwjlbDs9jJBPQ==", + "lie": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz", + "integrity": "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==", "dev": true, "requires": { - "uc.micro": "^1.0.1" + "immediate": "~3.0.5" } }, - "listenercount": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/listenercount/-/listenercount-1.0.1.tgz", - "integrity": "sha1-hMinKrWcRyUyFIDJdeZQg0LnCTc=", - "dev": true - }, - "load-json-file": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-1.1.0.tgz", - "integrity": "sha1-lWkFcI1YtLq0wiYbBPWfMcmTdMA=", + "liftoff": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/liftoff/-/liftoff-5.0.0.tgz", + "integrity": "sha512-a5BQjbCHnB+cy+gsro8lXJ4kZluzOijzJ1UVVfyJYZC+IP2pLv1h4+aysQeKuTmyO8NAqfyQAk4HWaP/HjcKTg==", "dev": true, "requires": { - "graceful-fs": "^4.1.2", - "parse-json": "^2.2.0", - "pify": "^2.0.0", - "pinkie-promise": "^2.0.0", - "strip-bom": "^2.0.0" + "extend": "^3.0.2", + "findup-sync": "^5.0.0", + "fined": "^2.0.0", + "flagged-respawn": "^2.0.0", + "is-plain-object": "^5.0.0", + "rechoir": "^0.8.0", + "resolve": "^1.20.0" }, "dependencies": { - "parse-json": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-2.2.0.tgz", - "integrity": "sha1-9ID0BDTvgHQfhGkJn43qGPVaTck=", - "dev": true, - "requires": { - "error-ex": "^1.2.0" - } - }, - "pify": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", - "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=", + "is-plain-object": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-5.0.0.tgz", + "integrity": "sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==", "dev": true } } }, + "linkify-it": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-3.0.3.tgz", + "integrity": "sha512-ynTsyrFSdE5oZ/O9GEf00kPngmOfVwazR5GKDq6EYfhlpFug3J2zybX56a2PRRpc9P+FuSoGNAwjlbDs9jJBPQ==", + "dev": true, + "requires": { + "uc.micro": "^1.0.1" + } + }, "loader-runner": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.2.0.tgz", - "integrity": "sha512-92+huvxMvYlMzMt0iIOukcwYBFpkYJdpl2xsZ7LrlayO7E8SOv+JJUEK17B/dJIHAOLMfh2dZZ/Y18WgmGtYNw==", + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.1.tgz", + "integrity": "sha512-IWqP2SCPhyVFTBtRcgMHdzlf9ul25NwaFx4wCEH/KjAXuuHY4yNjvPXsBokp8jCB936PyWRaPKUNh8NvylLp2Q==", "dev": true }, "loader-utils": { @@ -22150,15 +22805,9 @@ } }, "lodash": { - "version": "4.17.21", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", - "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" - }, - "lodash._reinterpolate": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/lodash._reinterpolate/-/lodash._reinterpolate-3.0.0.tgz", - "integrity": "sha1-DM8tiRZq8Ds2Y8eWU4t1rG4RTZ0=", - "dev": true + "version": "4.18.1", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz", + "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==" }, "lodash.flattendeep": { "version": "4.4.0", @@ -22169,44 +22818,55 @@ "lodash.get": { "version": "4.4.2", "resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz", - "integrity": "sha1-LRd/ZS+jHpObRDjVNBSZ36OCXpk=", + "integrity": "sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ==", "dev": true }, - "lodash.merge": { - "version": "4.6.2", - "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", - "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "lodash.includes": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", + "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==", "dev": true }, - "lodash.some": { - "version": "4.6.0", - "resolved": "https://registry.npmjs.org/lodash.some/-/lodash.some-4.6.0.tgz", - "integrity": "sha1-G7nzFO9ri63tE7VJFpsqlF62jk0=", + "lodash.isboolean": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", + "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==", "dev": true }, - "lodash.template": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/lodash.template/-/lodash.template-4.5.0.tgz", - "integrity": "sha512-84vYFxIkmidUiFxidA/KjjH9pAycqW+h980j7Fuz5qxRtO9pgB7MDFTdys1N7A5mcucRiDyEq4fusljItR1T/A==", - "dev": true, - "requires": { - "lodash._reinterpolate": "^3.0.0", - "lodash.templatesettings": "^4.0.0" - } + "lodash.isinteger": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", + "integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==", + "dev": true }, - "lodash.templatesettings": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/lodash.templatesettings/-/lodash.templatesettings-4.2.0.tgz", - "integrity": "sha512-stgLz+i3Aa9mZgnjr/O+v9ruKZsPsndy7qPZOchbqk2cnTU1ZaldKK+v7m54WoKIyxiuMZTKT2H81F8BeAc3ZQ==", - "dev": true, - "requires": { - "lodash._reinterpolate": "^3.0.0" - } + "lodash.isnumber": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz", + "integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==", + "dev": true }, - "lodash.truncate": { - "version": "4.4.2", - "resolved": "https://registry.npmjs.org/lodash.truncate/-/lodash.truncate-4.4.2.tgz", - "integrity": "sha1-WjUNoLERO4N+z//VgSy+WNbq4ZM=", + "lodash.isplainobject": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==", + "dev": true + }, + "lodash.isstring": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", + "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==", + "dev": true + }, + "lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true + }, + "lodash.once": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", + "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==", "dev": true }, "log-symbols": { @@ -22312,9 +22972,9 @@ }, "dependencies": { "semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", "dev": true } } @@ -22325,90 +22985,43 @@ "integrity": "sha512-c3sIjNUow0+8swNwVpqoH4YCShKNFkMaw6oH1mNS2haDZQqkeZFlHS3dhoeEbKKmJB4vXpJucU6oH75aDYeE9g==", "dev": true }, - "make-iterator": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/make-iterator/-/make-iterator-1.0.1.tgz", - "integrity": "sha512-pxiuXh0iVEq7VM7KMIhs5gxsfxCux2URptUQaXo4iZZJxBAzTPOLE2BumO5dbfVYq/hBJFBR/a1mFDmOx5AGmw==", - "dev": true, - "requires": { - "kind-of": "^6.0.2" - } - }, "map-cache": { "version": "0.2.2", "resolved": "https://registry.npmjs.org/map-cache/-/map-cache-0.2.2.tgz", - "integrity": "sha1-wyq9C9ZSXZsFFkW7TyasXcmKDb8=", + "integrity": "sha512-8y/eV9QQZCiyn1SprXSrCmqJN0yNRATe+PO8ztwqrvrbdRLA3eYJF0yaR0YayLWkMbsQSKWS9N2gPcGEc4UsZg==", "dev": true }, - "map-visit": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/map-visit/-/map-visit-1.0.0.tgz", - "integrity": "sha1-7Nyo8TFE5mDxtb1B8S80edmN+48=", - "dev": true, - "requires": { - "object-visit": "^1.0.0" - } - }, "markdown-it": { - "version": "12.3.2", - "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-12.3.2.tgz", - "integrity": "sha512-TchMembfxfNVpHkbtriWltGWc+m3xszaRD0CZup7GFFhzIgQqxIfn3eGj1yZpfuflzPvfkt611B2Q/Bsk1YnGg==", - "dev": true, - "requires": { - "argparse": "^2.0.1", - "entities": "~2.1.0", - "linkify-it": "^3.0.1", - "mdurl": "^1.0.1", - "uc.micro": "^1.0.5" - }, - "dependencies": { - "argparse": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", - "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", - "dev": true - } - } - }, - "matchdep": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/matchdep/-/matchdep-2.0.0.tgz", - "integrity": "sha1-xvNINKDY28OzfCfui7yyfHd1WC4=", - "dev": true, - "requires": { - "findup-sync": "^2.0.0", - "micromatch": "^3.0.4", - "resolve": "^1.4.0", - "stack-trace": "0.0.10" - }, - "dependencies": { - "findup-sync": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/findup-sync/-/findup-sync-2.0.0.tgz", - "integrity": "sha1-kyaxSIwi0aYIhlCoaQGy2akKLLw=", - "dev": true, - "requires": { - "detect-file": "^1.0.0", - "is-glob": "^3.1.0", - "micromatch": "^3.0.4", - "resolve-dir": "^1.0.1" - } - }, - "is-glob": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-3.1.0.tgz", - "integrity": "sha1-e6WuJCF4BKxwcHuWkiVnSGzD6Eo=", - "dev": true, - "requires": { - "is-extglob": "^2.1.0" - } + "version": "12.3.2", + "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-12.3.2.tgz", + "integrity": "sha512-TchMembfxfNVpHkbtriWltGWc+m3xszaRD0CZup7GFFhzIgQqxIfn3eGj1yZpfuflzPvfkt611B2Q/Bsk1YnGg==", + "dev": true, + "requires": { + "argparse": "^2.0.1", + "entities": "~2.1.0", + "linkify-it": "^3.0.1", + "mdurl": "^1.0.1", + "uc.micro": "^1.0.5" + }, + "dependencies": { + "argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true } } }, + "math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==" + }, "md5": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/md5/-/md5-2.3.0.tgz", "integrity": "sha512-T1GITYmFaKuO91vxyoQMFETst+O71VUPEU3ze5GNzDm0OWdP8v1ziTaAEPUr/3kLsY3Sftgz242A1SetQiDL7g==", + "dev": true, "requires": { "charenc": "0.0.2", "crypt": "0.0.2", @@ -22444,24 +23057,13 @@ "dev": true }, "micromatch": { - "version": "3.1.10", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-3.1.10.tgz", - "integrity": "sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg==", + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", "dev": true, "requires": { - "arr-diff": "^4.0.0", - "array-unique": "^0.3.2", - "braces": "^2.3.1", - "define-property": "^2.0.2", - "extend-shallow": "^3.0.2", - "extglob": "^2.0.4", - "fragment-cache": "^0.2.1", - "kind-of": "^6.0.2", - "nanomatch": "^1.2.9", - "object.pick": "^1.3.0", - "regex-not": "^1.0.0", - "snapdragon": "^0.8.1", - "to-regex": "^3.0.2" + "braces": "^3.0.3", + "picomatch": "^2.3.1" } }, "miller-rabin": { @@ -22508,7 +23110,8 @@ "minimalistic-assert": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz", - "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==" + "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==", + "dev": true }, "minimalistic-crypto-utils": { "version": "1.0.1", @@ -22517,17 +23120,17 @@ "dev": true }, "minimatch": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.0.tgz", - "integrity": "sha512-9TPBGGak4nHfGZsPBohm9AWg6NoT7QTCehS3BIJABslyZbzxfV78QM2Y6+i741OPZIafFAaiiEMh5OyIrJPgtg==", + "version": "5.1.8", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.8.tgz", + "integrity": "sha512-7RN35vit8DeBclkofOVmBY0eDAZZQd1HzmukRdSyz95CRh8FT54eqnbj0krQr3mrHR6sfRyYkyhwBWjoV5uqlQ==", "requires": { "brace-expansion": "^2.0.1" }, "dependencies": { "brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.3.tgz", + "integrity": "sha512-MCV/fYJEbqx68aE58kv2cA/kiky1G8vux3OR6/jbS+jIMe/6fJWa0DTzJU7dqijOWYwHi1t29FlfYI9uytqlpA==", "requires": { "balanced-match": "^1.0.0" } @@ -22537,33 +23140,20 @@ "minimist": { "version": "1.2.6", "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.6.tgz", - "integrity": "sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==" + "integrity": "sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==", + "dev": true }, - "mixin-deep": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/mixin-deep/-/mixin-deep-1.3.2.tgz", - "integrity": "sha512-WRoDn//mXBiJ1H40rqa3vH0toePwSsGb45iInWlTySa+Uu4k3tYUSxa2v1KqAiLtvlrSzaExqS1gtk96A9zvEA==", - "dev": true, - "requires": { - "for-in": "^1.0.2", - "is-extendable": "^1.0.1" - }, - "dependencies": { - "is-extendable": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz", - "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==", - "dev": true, - "requires": { - "is-plain-object": "^2.0.4" - } - } - } + "minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "dev": true }, "mkdirp": { "version": "0.5.5", "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.5.tgz", "integrity": "sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==", + "dev": true, "requires": { "minimist": "^1.2.5" } @@ -22576,105 +23166,93 @@ "optional": true }, "mocha": { - "version": "9.2.2", - "resolved": "https://registry.npmjs.org/mocha/-/mocha-9.2.2.tgz", - "integrity": "sha512-L6XC3EdwT6YrIk0yXpavvLkn8h+EU+Y5UcCHKECyMbdUIxyMuZj4bX4U9e1nvnvUUvQVsV2VHQr5zLdcUkhW/g==", - "dev": true, - "requires": { - "@ungap/promise-all-settled": "1.1.2", - "ansi-colors": "4.1.1", - "browser-stdout": "1.3.1", - "chokidar": "3.5.3", - "debug": "4.3.3", - "diff": "5.0.0", - "escape-string-regexp": "4.0.0", - "find-up": "5.0.0", - "glob": "7.2.0", - "growl": "1.10.5", - "he": "1.2.0", - "js-yaml": "4.1.0", - "log-symbols": "4.1.0", - "minimatch": "4.2.1", - "ms": "2.1.3", - "nanoid": "3.3.1", - "serialize-javascript": "6.0.0", - "strip-json-comments": "3.1.1", - "supports-color": "8.1.1", - "which": "2.0.2", - "workerpool": "6.2.0", - "yargs": "16.2.0", - "yargs-parser": "20.2.4", - "yargs-unparser": "2.0.0" + "version": "11.7.5", + "resolved": "https://registry.npmjs.org/mocha/-/mocha-11.7.5.tgz", + "integrity": "sha512-mTT6RgopEYABzXWFx+GcJ+ZQ32kp4fMf0xvpZIIfSq9Z8lC/++MtcCnQ9t5FP2veYEP95FIYSvW+U9fV4xrlig==", + "dev": true, + "requires": { + "browser-stdout": "^1.3.1", + "chokidar": "^4.0.1", + "debug": "^4.3.5", + "diff": "^7.0.0", + "escape-string-regexp": "^4.0.0", + "find-up": "^5.0.0", + "glob": "^10.4.5", + "he": "^1.2.0", + "is-path-inside": "^3.0.3", + "js-yaml": "^4.1.0", + "log-symbols": "^4.1.0", + "minimatch": "^9.0.5", + "ms": "^2.1.3", + "picocolors": "^1.1.1", + "serialize-javascript": "^6.0.2", + "strip-json-comments": "^3.1.1", + "supports-color": "^8.1.1", + "workerpool": "^9.2.0", + "yargs": "^17.7.2", + "yargs-parser": "^21.1.1", + "yargs-unparser": "^2.0.0" }, "dependencies": { - "ansi-colors": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.1.tgz", - "integrity": "sha512-JoX0apGbHaUJBNl6yF+p6JAFYZ666/hhCGKN5t9QFjbJQKUU/g8MNbFDbvfrgKXvI1QpZplPOnwIo99lX/AAmA==", + "argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", "dev": true }, - "ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "brace-expansion": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.3.tgz", + "integrity": "sha512-MCV/fYJEbqx68aE58kv2cA/kiky1G8vux3OR6/jbS+jIMe/6fJWa0DTzJU7dqijOWYwHi1t29FlfYI9uytqlpA==", "dev": true, "requires": { - "color-convert": "^2.0.1" + "balanced-match": "^1.0.0" } }, - "argparse": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", - "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", - "dev": true + "chokidar": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", + "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", + "dev": true, + "requires": { + "readdirp": "^4.0.1" + } }, "cliui": { - "version": "7.0.4", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", - "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", "dev": true, "requires": { "string-width": "^4.2.0", - "strip-ansi": "^6.0.0", + "strip-ansi": "^6.0.1", "wrap-ansi": "^7.0.0" } }, - "color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", "dev": true, "requires": { - "color-name": "~1.1.4" + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" } }, - "color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true - }, "debug": { - "version": "4.3.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.3.tgz", - "integrity": "sha512-/zxw5+vh1Tfv+4Qn7a5nsbcJKPaSvCDhojn6FEl9vupwK2VCSDtEiEtqr8DFtzYFOdz63LBkxec7DYuc2jon6Q==", + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", + "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", "dev": true, "requires": { - "ms": "2.1.2" - }, - "dependencies": { - "ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true - } + "ms": "^2.1.3" } }, "diff": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/diff/-/diff-5.0.0.tgz", - "integrity": "sha512-/VTCrvm5Z0JGty/BWHljh+BAiw3IK+2j87NGMu8Nwc/f48WoDAC395uomO9ZD117ZOBaHmkX1oyLvkVM/aIT3w==", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-7.0.0.tgz", + "integrity": "sha512-PJWHUb1RFevKCwaFA9RlG5tCd+FO5iRh9A8HEtkmBH2Li03iJriB6m6JIN4rGz3K3JLawI7/veA1xzRKP6ISBw==", "dev": true }, "escape-string-regexp": { @@ -22693,11 +23271,29 @@ "path-exists": "^4.0.0" } }, - "get-caller-file": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", - "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", - "dev": true + "foreground-child": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.0.tgz", + "integrity": "sha512-Ld2g8rrAyMYFXBhEqMz8ZAHBi4J4uS1i/CxGMDnjyFWddMXLVcDp051DZfu+t7+ab7Wv6SMqpWmyFIj5UbfFvg==", + "dev": true, + "requires": { + "cross-spawn": "^7.0.0", + "signal-exit": "^4.0.1" + } + }, + "glob": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", + "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", + "dev": true, + "requires": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + } }, "has-flag": { "version": "4.0.0", @@ -22705,16 +23301,10 @@ "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", "dev": true }, - "is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "dev": true - }, "js-yaml": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", - "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", "dev": true, "requires": { "argparse": "^2.0.1" @@ -22730,12 +23320,12 @@ } }, "minimatch": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-4.2.1.tgz", - "integrity": "sha512-9Uq1ChtSZO+Mxa/CL1eGizn2vRn3MlLgzhT0Iz8zaY8NdvxvB0d5QdPFmCKf7JKA9Lerx5vRrnwO03jsSfGG9g==", + "version": "9.0.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", + "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", "dev": true, "requires": { - "brace-expansion": "^1.1.7" + "brace-expansion": "^2.0.2" } }, "ms": { @@ -22744,12 +23334,6 @@ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", "dev": true }, - "nanoid": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.1.tgz", - "integrity": "sha512-n6Vs/3KGyxPQd6uO0eH4Bv0ojGSUvuLlIHtC3Y0kEO23YRge8H9x1GCzLn28YX0H66pMkxuaeESFq4tKISKwdw==", - "dev": true - }, "p-limit": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", @@ -22768,23 +23352,39 @@ "p-limit": "^3.0.2" } }, - "path-exists": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", - "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", "dev": true }, - "string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "readdirp": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", + "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==", + "dev": true + }, + "shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", "dev": true, "requires": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" + "shebang-regex": "^3.0.0" } }, + "shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true + }, + "signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true + }, "supports-color": { "version": "8.1.1", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", @@ -22794,17 +23394,6 @@ "has-flag": "^4.0.0" } }, - "wrap-ansi": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", - "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", - "dev": true, - "requires": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - } - }, "y18n": { "version": "5.0.8", "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", @@ -22812,19 +23401,25 @@ "dev": true }, "yargs": { - "version": "16.2.0", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", - "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==", + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", "dev": true, "requires": { - "cliui": "^7.0.2", + "cliui": "^8.0.1", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", "require-directory": "^2.1.1", - "string-width": "^4.2.0", + "string-width": "^4.2.3", "y18n": "^5.0.5", - "yargs-parser": "^20.2.2" + "yargs-parser": "^21.1.1" } + }, + "yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "dev": true } } }, @@ -22862,6 +23457,11 @@ } } }, + "module-details-from-path": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/module-details-from-path/-/module-details-from-path-1.0.3.tgz", + "integrity": "sha512-ySViT69/76t8VhE1xXHK6Ch4NcDd26gx0MzKXLO+F7NOtnqH68d9zF94nT8ZWSxXh8ELOERsnJO/sWt1xZYw5A==" + }, "mrmime": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/mrmime/-/mrmime-1.0.0.tgz", @@ -22874,9 +23474,9 @@ "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" }, "mute-stdout": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/mute-stdout/-/mute-stdout-1.0.1.tgz", - "integrity": "sha512-kDcwXR4PS7caBpuRYYBUz9iVixUk3anO3f5OYFiIPwK/20vCzKCHyKoulbiDY1S53zD2bxUpxN/IJ+TnXjfvxg==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mute-stdout/-/mute-stdout-2.0.0.tgz", + "integrity": "sha512-32GSKM3Wyc8dg/p39lWPKYu8zci9mJFzV1Np9Of0ZEpe6Fhssn/FbI7ywAMd40uX+p3ZKh3T5EeCFv81qS3HmQ==", "dev": true }, "mute-stream": { @@ -22890,38 +23490,12 @@ "resolved": "https://registry.npmjs.org/named-js-regexp/-/named-js-regexp-1.3.5.tgz", "integrity": "sha512-XO0DPujDP9IWpkt690iWLreKztb/VB811DGl5N3z7BfhkMJuiVZXOi6YN/fEB9qkvtMVTgSZDW8pzdVt8vj/FA==" }, - "nan": { - "version": "2.14.0", - "resolved": "https://registry.npmjs.org/nan/-/nan-2.14.0.tgz", - "integrity": "sha512-INOFj37C7k3AfaNTtX8RhsTw7qRy7eLET14cROi9+5HAVbbHuIWUHEauBv5qT4Av2tWasiTY1Jw6puUNqRJXQg==", - "dev": true, - "optional": true - }, "nanoid": { - "version": "2.1.11", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-2.1.11.tgz", - "integrity": "sha512-s/snB+WGm6uwi0WjsZdaVcuf3KJXlfGl2LcxgwkEwJF0D/BWzVWAZW/XY4bFaiR7s0Jk3FPvlnepg1H1b1UwlA==", + "version": "3.3.8", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.8.tgz", + "integrity": "sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w==", "dev": true }, - "nanomatch": { - "version": "1.2.13", - "resolved": "https://registry.npmjs.org/nanomatch/-/nanomatch-1.2.13.tgz", - "integrity": "sha512-fpoe2T0RbHwBTBUOftAfBPaDEi06ufaUai0mE6Yn1kacc3SnTErfb/h+X94VXzI64rKFHYImXSvdwGGCmwOqCA==", - "dev": true, - "requires": { - "arr-diff": "^4.0.0", - "array-unique": "^0.3.2", - "define-property": "^2.0.2", - "extend-shallow": "^3.0.2", - "fragment-cache": "^0.2.1", - "is-windows": "^1.0.2", - "kind-of": "^6.0.2", - "object.pick": "^1.3.0", - "regex-not": "^1.0.0", - "snapdragon": "^0.8.1", - "to-regex": "^3.0.1" - } - }, "napi-build-utils": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-1.0.2.tgz", @@ -22948,60 +23522,32 @@ "dev": true }, "nise": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/nise/-/nise-5.1.1.tgz", - "integrity": "sha512-yr5kW2THW1AkxVmCnKEh4nbYkJdB3I7LUkiUgOvEkOp414mc2UMaHMA7pjq1nYowhdoJZGwEKGaQVbxfpWj10A==", - "dev": true, - "requires": { - "@sinonjs/commons": "^1.8.3", - "@sinonjs/fake-timers": ">=5", - "@sinonjs/text-encoding": "^0.7.1", - "just-extend": "^4.0.2", - "path-to-regexp": "^1.7.0" - } - }, - "nock": { - "version": "10.0.6", - "resolved": "https://registry.npmjs.org/nock/-/nock-10.0.6.tgz", - "integrity": "sha512-b47OWj1qf/LqSQYnmokNWM8D88KvUl2y7jT0567NB3ZBAZFz2bWp2PC81Xn7u8F2/vJxzkzNZybnemeFa7AZ2w==", + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/nise/-/nise-6.0.0.tgz", + "integrity": "sha512-K8ePqo9BFvN31HXwEtTNGzgrPpmvgciDsFz8aztFjt4LqKO/JeFD8tBOeuDiCMXrIl/m1YvfH8auSpxfaD09wg==", "dev": true, "requires": { - "chai": "^4.1.2", - "debug": "^4.1.0", - "deep-equal": "^1.0.0", - "json-stringify-safe": "^5.0.1", - "lodash": "^4.17.5", - "mkdirp": "^0.5.0", - "propagate": "^1.0.0", - "qs": "^6.5.1", - "semver": "^5.5.0" - }, - "dependencies": { - "debug": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", - "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", - "dev": true, - "requires": { - "ms": "^2.1.1" - } - } + "@sinonjs/commons": "^3.0.0", + "@sinonjs/fake-timers": "^11.2.2", + "@sinonjs/text-encoding": "^0.7.2", + "just-extend": "^6.2.0", + "path-to-regexp": "^6.2.1" } }, "node-abi": { - "version": "2.30.1", - "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-2.30.1.tgz", - "integrity": "sha512-/2D0wOQPgaUWzVSVgRMx+trKJRC2UG4SUc4oCJoXx9Uxjtp0Vy3/kt7zcbxHF8+Z/pK3UloLWzBISg72brfy1w==", + "version": "3.45.0", + "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.45.0.tgz", + "integrity": "sha512-iwXuFrMAcFVi/ZoZiqq8BzAdsLw9kxDfTC0HMyjXfSL/6CSDAGD5UmR7azrAgWV1zKYq7dUUMj4owusBWKLsiQ==", "dev": true, "optional": true, "requires": { - "semver": "^5.4.1" + "semver": "^7.3.5" } }, "node-addon-api": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-3.2.1.tgz", - "integrity": "sha512-mmcei9JghVNDYydghQmeDX8KoAm0FAiYyIcUt/N4nhyAipB17pllZQDOJD2fotxABnt4Mdz+dKTO7eftLg4d0A==", + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-4.3.0.tgz", + "integrity": "sha512-73sE9+3UaLYYFmDsFZnqCInzPyh3MqIwZO9cw58yIqAZhONrrabrYyYe3TuIqtIiOuTXVhsGau8hcrhhwSsDIQ==", "dev": true, "optional": true }, @@ -23205,9 +23751,9 @@ } }, "node-releases": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.2.tgz", - "integrity": "sha512-XxYDdcQ6eKqp/YjI+tb2C5WM2LgjnZrfYg4vgQt49EK268b6gYCHsBLrK2qvJo4FmCtqmKezb0WZFK4fkrZNsg==", + "version": "2.0.27", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz", + "integrity": "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==", "dev": true }, "node-stream-zip": { @@ -23215,18 +23761,6 @@ "resolved": "https://registry.npmjs.org/node-stream-zip/-/node-stream-zip-1.15.0.tgz", "integrity": "sha512-LN4fydt9TqhZhThkZIVQnF9cwjU3qmUH9h78Mx/K7d3VvfRqqwthLwJEUOEL0QPZ0XQmNN7be5Ggit5+4dq3Bw==" }, - "normalize-package-data": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz", - "integrity": "sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==", - "dev": true, - "requires": { - "hosted-git-info": "^2.1.4", - "resolve": "^1.10.0", - "semver": "2 || 3 || 4 || 5", - "validate-npm-package-license": "^3.0.1" - } - }, "normalize-path": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", @@ -23242,17 +23776,6 @@ "prepend-http": "^2.0.0", "query-string": "^5.0.1", "sort-keys": "^2.0.0" - }, - "dependencies": { - "sort-keys": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/sort-keys/-/sort-keys-2.0.0.tgz", - "integrity": "sha1-ZYU1WEhh7JfXMNbPQYIuH1ZoQSg=", - "dev": true, - "requires": { - "is-plain-obj": "^1.0.0" - } - } } }, "now-and-later": { @@ -23281,19 +23804,6 @@ } } }, - "npmlog": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-4.1.2.tgz", - "integrity": "sha512-2uUqazuKlTaSI/dC8AzicUck7+IrEaOnN/e0jd3Xtt1KcGpwx30v50mL7oPyr/h9bL3E4aZccVwpwP+5W9Vjkg==", - "dev": true, - "optional": true, - "requires": { - "are-we-there-yet": "~1.1.2", - "console-control-strings": "~1.1.0", - "gauge": "~2.7.3", - "set-blocking": "~2.0.0" - } - }, "nth-check": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.0.1.tgz", @@ -23303,12 +23813,6 @@ "boolbase": "^1.0.0" } }, - "number-is-nan": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz", - "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=", - "dev": true - }, "nyc": { "version": "15.1.0", "resolved": "https://registry.npmjs.org/nyc/-/nyc-15.1.0.tgz", @@ -23343,84 +23847,28 @@ "test-exclude": "^6.0.0", "yargs": "^15.0.2" }, - "dependencies": { - "convert-source-map": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.7.0.tgz", - "integrity": "sha512-4FJkXzKXEDB1snCFZlLP4gpC3JILicCpGbzG9f9G7tGqGCzETQ2hWPrcinA9oU4wtf2biUaEH5065UnMeR33oA==", - "dev": true, - "requires": { - "safe-buffer": "~5.1.1" - } - }, - "find-cache-dir": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-3.2.0.tgz", - "integrity": "sha512-1JKclkYYsf1q9WIJKLZa9S9muC+08RIjzAlLrK4QcYLJMS6mk9yombQ9qf+zJ7H9LS800k0s44L4sDq9VYzqyg==", - "dev": true, - "requires": { - "commondir": "^1.0.1", - "make-dir": "^3.0.0", - "pkg-dir": "^4.1.0" - } - }, - "p-map": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/p-map/-/p-map-3.0.0.tgz", - "integrity": "sha512-d3qXVTF/s+W+CdJ5A29wywV2n8CQQYahlgz2bFiA+4eVNJbHJodPZ+/gXwPGh0bOqA+j8S+6+ckmvLGPk1QpxQ==", - "dev": true, - "requires": { - "aggregate-error": "^3.0.0" - } - } - } - }, - "object-assign": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", - "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=", - "dev": true - }, - "object-copy": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/object-copy/-/object-copy-0.1.0.tgz", - "integrity": "sha1-fn2Fi3gb18mRpBupde04EnVOmYw=", - "dev": true, - "requires": { - "copy-descriptor": "^0.1.0", - "define-property": "^0.2.5", - "kind-of": "^3.0.3" - }, - "dependencies": { - "define-property": { - "version": "0.2.5", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", - "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", - "dev": true, - "requires": { - "is-descriptor": "^0.1.0" - } - }, - "kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dependencies": { + "p-map": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/p-map/-/p-map-3.0.0.tgz", + "integrity": "sha512-d3qXVTF/s+W+CdJ5A29wywV2n8CQQYahlgz2bFiA+4eVNJbHJodPZ+/gXwPGh0bOqA+j8S+6+ckmvLGPk1QpxQ==", "dev": true, "requires": { - "is-buffer": "^1.1.5" + "aggregate-error": "^3.0.0" } } } }, - "object-hash": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz", - "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==" + "object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=", + "dev": true }, "object-inspect": { - "version": "1.12.0", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.0.tgz", - "integrity": "sha512-Ho2z80bVIvJloH+YzRmpZVQe87+qASmBUKZDWgx9cu+KDrX2ZDH/3tMy+gXbZETVGs2M8YdxObOh7XAtim9Y0g==", + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", "dev": true }, "object-is": { @@ -23439,48 +23887,28 @@ "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", "dev": true }, - "object-visit": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/object-visit/-/object-visit-1.0.1.tgz", - "integrity": "sha1-95xEk68MU3e1n+OdOV5BBC3QRbs=", - "dev": true, - "requires": { - "isobject": "^3.0.0" - } - }, "object.assign": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.2.tgz", - "integrity": "sha512-ixT2L5THXsApyiUPYKmW+2EHpXXe5Ii3M+f4e+aJFAHao5amFRW6J0OO6c/LU8Be47utCx2GL89hxGB6XSmKuQ==", + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.5.tgz", + "integrity": "sha512-byy+U7gp+FVwmyzKPYhW2h5l3crpmGsxl7X2s8y43IgxvG4g3QZ6CffDtsNQy1WsmZpQbO+ybo0AlW7TY6DcBQ==", "dev": true, "requires": { - "call-bind": "^1.0.0", - "define-properties": "^1.1.3", - "has-symbols": "^1.0.1", + "call-bind": "^1.0.5", + "define-properties": "^1.2.1", + "has-symbols": "^1.0.3", "object-keys": "^1.1.1" } }, "object.defaults": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/object.defaults/-/object.defaults-1.1.0.tgz", - "integrity": "sha1-On+GgzS0B96gbaFtiNXNKeQ1/s8=", + "integrity": "sha512-c/K0mw/F11k4dEUBMW8naXUuBuhxRCfG7W+yFy8EcijU/rSmazOUd1XAEEe6bC0OuXY4HUKjTJv7xbxIMqdxrA==", "dev": true, "requires": { "array-each": "^1.0.1", "array-slice": "^1.0.0", "for-own": "^1.0.0", "isobject": "^3.0.0" - }, - "dependencies": { - "for-own": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/for-own/-/for-own-1.0.0.tgz", - "integrity": "sha1-xjMy9BXO3EsE2/5wz4NklMU8tEs=", - "dev": true, - "requires": { - "for-in": "^1.0.1" - } - } } }, "object.entries": { @@ -23495,14 +23923,26 @@ } }, "object.fromentries": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/object.fromentries/-/object.fromentries-2.0.5.tgz", - "integrity": "sha512-CAyG5mWQRRiBU57Re4FKoTBjXfDoNwdFVH2Y1tS9PqCsfUTymAohOkEMSG3aRNKmv4lV3O7p1et7c187q6bynw==", + "version": "2.0.8", + "resolved": "https://registry.npmjs.org/object.fromentries/-/object.fromentries-2.0.8.tgz", + "integrity": "sha512-k6E21FzySsSK5a21KRADBd/NGneRegFO5pLHfdQLpRDETUNJueLXs3WCzyQ3tFRDYgbq3KHGXfTbi2bs8WQ6rQ==", "dev": true, "requires": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.3", - "es-abstract": "^1.19.1" + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2", + "es-object-atoms": "^1.0.0" + } + }, + "object.groupby": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/object.groupby/-/object.groupby-1.0.3.tgz", + "integrity": "sha512-+Lhy3TQTuzXI5hevh8sBGqbmurHbbIjAi0Z4S63nthVLmLxfbj4T54a4CfZrXIrt9iP4mVAPYMo/v99taj3wjQ==", + "dev": true, + "requires": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2" } }, "object.hasown": { @@ -23515,66 +23955,24 @@ "es-abstract": "^1.19.1" } }, - "object.map": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/object.map/-/object.map-1.0.1.tgz", - "integrity": "sha1-z4Plncj8wK1fQlDh94s7gb2AHTc=", - "dev": true, - "requires": { - "for-own": "^1.0.0", - "make-iterator": "^1.0.0" - }, - "dependencies": { - "for-own": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/for-own/-/for-own-1.0.0.tgz", - "integrity": "sha1-xjMy9BXO3EsE2/5wz4NklMU8tEs=", - "dev": true, - "requires": { - "for-in": "^1.0.1" - } - } - } - }, "object.pick": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/object.pick/-/object.pick-1.3.0.tgz", - "integrity": "sha1-h6EKxMFpS9Lhy/U1kaZhQftd10c=", + "integrity": "sha512-tqa/UMy/CCoYmj+H5qc07qvSL9dqcs/WZENZ1JbtWBlATP+iVOe778gE6MSijnyCnORzDuX6hU+LA4SZ09YjFQ==", "dev": true, "requires": { "isobject": "^3.0.1" } }, - "object.reduce": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/object.reduce/-/object.reduce-1.0.1.tgz", - "integrity": "sha1-b+NI8qx/oPlcpiEiZZkJaCW7A60=", - "dev": true, - "requires": { - "for-own": "^1.0.0", - "make-iterator": "^1.0.0" - }, - "dependencies": { - "for-own": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/for-own/-/for-own-1.0.0.tgz", - "integrity": "sha1-xjMy9BXO3EsE2/5wz4NklMU8tEs=", - "dev": true, - "requires": { - "for-in": "^1.0.1" - } - } - } - }, "object.values": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.1.5.tgz", - "integrity": "sha512-QUZRW0ilQ3PnPpbNtgdNV1PDbEqLIiSFB3l+EnGtBQ/8SUTLj1PZwtQHABZtLgwpJZTSZhuGLOGk57Drx2IvYg==", + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.2.0.tgz", + "integrity": "sha512-yBYjY9QX2hnRmZHAjG/f13MzmBzxzYgQhFrke06TTyKY5zSTEqkOeukBzIdVA3j3ulu8Qa3MbVFShV7T2RmGtQ==", "dev": true, "requires": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.3", - "es-abstract": "^1.19.1" + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" } }, "once": { @@ -23594,12 +23992,37 @@ "mimic-fn": "^2.1.0" } }, + "open": { + "version": "8.4.2", + "resolved": "https://registry.npmjs.org/open/-/open-8.4.2.tgz", + "integrity": "sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ==", + "dev": true, + "requires": { + "define-lazy-prop": "^2.0.0", + "is-docker": "^2.1.1", + "is-wsl": "^2.2.0" + } + }, "opener": { "version": "1.5.2", "resolved": "https://registry.npmjs.org/opener/-/opener-1.5.2.tgz", "integrity": "sha512-ur5UIdyw5Y7yEj9wLzhqXiy6GZ3Mwx0yGI+5sMn2r0N0v3cKJvUmFH5yPP+WXh9e0xfyzyJX95D8l088DNFj7A==", "dev": true }, + "optionator": { + "version": "0.9.3", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.3.tgz", + "integrity": "sha512-JjCoypp+jKn1ttEFExxhetCKeJt9zhAgAve5FXHixTvFDW/5aEktX9bufBKLRRMdU7bNtpLfcGu94B3cdEJgjg==", + "dev": true, + "requires": { + "@aashutoshrathi/word-wrap": "^1.2.3", + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0" + } + }, "ordered-read-streams": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/ordered-read-streams/-/ordered-read-streams-1.0.1.tgz", @@ -23615,20 +24038,6 @@ "integrity": "sha1-hUNzx/XCMVkU/Jv8a9gjj92h7Cc=", "dev": true }, - "os-locale": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/os-locale/-/os-locale-1.4.0.tgz", - "integrity": "sha1-IPnxeuKe00XoveWDsT0gCYA8FNk=", - "dev": true, - "requires": { - "lcid": "^1.0.0" - } - }, - "os-tmpdir": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", - "integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=" - }, "p-cancelable": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-0.4.1.tgz", @@ -23647,13 +24056,13 @@ "p-finally": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz", - "integrity": "sha1-P7z7FbiZpEEjs0ttzBi3JDNqLK4=", + "integrity": "sha512-LICb2p9CB7FS+0eR1oqWnHhp0FljGLZCWBE9aix0Uye9W8LTQPwMTYVGWQWIw9RdQiDg4+epXQODwIYJtSJaow==", "dev": true }, "p-is-promise": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/p-is-promise/-/p-is-promise-1.1.0.tgz", - "integrity": "sha1-nJRWmJ6fZYgBewQ01WCXZ1w9oF4=", + "integrity": "sha512-zL7VE4JVS2IFSkR2GQKDSPEVxkoH43/p7oEnwpdCndKYJO0HVeRB7fA8TJwuLOTBREtK0ea8eHaxdwcpob5dmg==", "dev": true }, "p-limit": { @@ -23710,6 +24119,18 @@ "release-zalgo": "^1.0.0" } }, + "package-json-from-dist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", + "dev": true + }, + "pako": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", + "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==", + "dev": true + }, "parent-module": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", @@ -23717,34 +24138,33 @@ "dev": true, "requires": { "callsites": "^3.0.0" - }, - "dependencies": { - "callsites": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", - "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", - "dev": true - } } }, "parse-asn1": { - "version": "5.1.4", - "resolved": "https://registry.npmjs.org/parse-asn1/-/parse-asn1-5.1.4.tgz", - "integrity": "sha512-Qs5duJcuvNExRfFZ99HDD3z4mAi3r9Wl/FOjEOijlxwCZs7E7mW2vjTpgQ4J8LpTF8x5v+1Vn5UQFejmWT11aw==", + "version": "5.1.9", + "resolved": "https://registry.npmjs.org/parse-asn1/-/parse-asn1-5.1.9.tgz", + "integrity": "sha512-fIYNuZ/HastSb80baGOuPRo1O9cf4baWw5WsAp7dBuUzeTD/BoaG8sVTdlPFksBE2lF21dN+A1AnrpIjSWqHHg==", "dev": true, "requires": { - "asn1.js": "^4.0.0", - "browserify-aes": "^1.0.0", - "create-hash": "^1.1.0", - "evp_bytestokey": "^1.0.0", - "pbkdf2": "^3.0.3", - "safe-buffer": "^5.1.1" + "asn1.js": "^4.10.1", + "browserify-aes": "^1.2.0", + "evp_bytestokey": "^1.0.3", + "pbkdf2": "^3.1.5", + "safe-buffer": "^5.2.1" + }, + "dependencies": { + "safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "dev": true + } } }, "parse-filepath": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/parse-filepath/-/parse-filepath-1.0.2.tgz", - "integrity": "sha1-pjISf1Oq89FYdvWHLz/6x2PWyJE=", + "integrity": "sha512-FwdRXKCohSVeXqwtYonZTXtbGJKrn+HNyWDYVcp5yuJlesTwNH4rsmRZ+GrKAPJ5bLpRxESMeS+Rl0VCHRvB2Q==", "dev": true, "requires": { "is-absolute": "^1.0.0", @@ -23752,25 +24172,27 @@ "path-root": "^0.1.1" } }, - "parse-node-version": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/parse-node-version/-/parse-node-version-1.0.1.tgz", - "integrity": "sha512-3YHlOa/JgH6Mnpr05jP9eDG254US9ek25LyIxZlDItp2iJtwyaXQb57lBYLdT3MowkUFYEV2XXNAYIPlESvJlA==", - "dev": true - }, "parse-passwd": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/parse-passwd/-/parse-passwd-1.0.0.tgz", - "integrity": "sha1-bVuTSkVpk7I9N/QKOC1vFmao5cY=", + "integrity": "sha512-1Y1A//QUXEZK7YKz+rD9WydcE1+EuPr6ZBgKecAB8tmoW6UFv0NREVJe1p+jRxtThkcbbKkfwIbWJe/IeE6m2Q==", "dev": true }, "parse-semver": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/parse-semver/-/parse-semver-1.1.1.tgz", - "integrity": "sha1-mkr9bfBj3Egm+T+6SpnPIj9mbLg=", + "integrity": "sha512-Eg1OuNntBMH0ojvEKSrvDSnwLmvVuUOSdylH/pSCPNMIspLlweJyIWXCE+k/5hm3cj/EBUYwmWkjhBALNP4LXQ==", "dev": true, "requires": { "semver": "^5.1.0" + }, + "dependencies": { + "semver": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", + "dev": true + } } }, "parse5-htmlparser2-tree-adapter": { @@ -23790,12 +24212,6 @@ } } }, - "pascalcase": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/pascalcase/-/pascalcase-0.1.1.tgz", - "integrity": "sha1-s2PlXoAGym/iF4TS2yK9FdeRfxQ=", - "dev": true - }, "path-browserify": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-0.0.1.tgz", @@ -23809,9 +24225,9 @@ "dev": true }, "path-exists": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", - "integrity": "sha1-zg6+ql94yxiSXqfYENe1mwEP1RU=", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", "dev": true }, "path-is-absolute": { @@ -23828,13 +24244,12 @@ "path-parse": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", - "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", - "dev": true + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==" }, "path-root": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/path-root/-/path-root-0.1.1.tgz", - "integrity": "sha1-mkpoFMrBwM1zNgqV8yCDyOpHRbc=", + "integrity": "sha512-QLcPegTHF11axjfojBIoDygmS2E3Lf+8+jI6wOVmNVenrKSo3mFdSGiIgdSHenczw3wPtlVMQaFVwGmM7BJdtg==", "dev": true, "requires": { "path-root-regex": "^0.1.0" @@ -23843,26 +24258,33 @@ "path-root-regex": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/path-root-regex/-/path-root-regex-0.1.2.tgz", - "integrity": "sha1-v8zcjfWxLcUsi0PsONGNcsBLqW0=", + "integrity": "sha512-4GlJ6rZDhQZFE0DPVKh0e9jmZ5egZfxTkp7bcRDuPlJXbAwhxcl2dINPUAsjLdejqaLsCeg8axcLjIbvBjN4pQ==", "dev": true }, - "path-to-regexp": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-1.8.0.tgz", - "integrity": "sha512-n43JRhlUKUAlibEJhPeir1ncUID16QnEjNpwzNdO3Lm4ywrBpBZ5oLD0I6br9evr1Y9JTqwRtAh7JLoOzAQdVA==", + "path-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", "dev": true, "requires": { - "isarray": "0.0.1" + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" }, "dependencies": { - "isarray": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", - "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=", + "lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", "dev": true } } }, + "path-to-regexp": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-6.3.0.tgz", + "integrity": "sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ==", + "dev": true + }, "path-type": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", @@ -23876,16 +24298,25 @@ "dev": true }, "pbkdf2": { - "version": "3.0.17", - "resolved": "https://registry.npmjs.org/pbkdf2/-/pbkdf2-3.0.17.tgz", - "integrity": "sha512-U/il5MsrZp7mGg3mSQfn742na2T+1/vHDCG5/iTI3X9MKUuYUZVLQhyRsg06mCgDBTd57TxzgZt7P+fYfjRLtA==", + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/pbkdf2/-/pbkdf2-3.1.5.tgz", + "integrity": "sha512-Q3CG/cYvCO1ye4QKkuH7EXxs3VC/rI1/trd+qX2+PolbaKG0H+bgcZzrTt96mMyRtejk+JMCiLUn3y29W8qmFQ==", "dev": true, "requires": { - "create-hash": "^1.1.2", - "create-hmac": "^1.1.4", - "ripemd160": "^2.0.1", - "safe-buffer": "^5.0.1", - "sha.js": "^2.4.8" + "create-hash": "^1.2.0", + "create-hmac": "^1.1.7", + "ripemd160": "^2.0.3", + "safe-buffer": "^5.2.1", + "sha.js": "^2.4.12", + "to-buffer": "^1.2.1" + }, + "dependencies": { + "safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "dev": true + } } }, "pend": { @@ -23895,15 +24326,15 @@ "dev": true }, "picocolors": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", - "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", "dev": true }, "picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", "dev": true }, "pify": { @@ -23915,13 +24346,13 @@ "pinkie": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/pinkie/-/pinkie-2.0.4.tgz", - "integrity": "sha1-clVrgM+g1IqXToDnckjoDtT3+HA=", + "integrity": "sha512-MnUuEycAemtSaeFSjXKW/aroV7akBbY+Sv+RkyqFjgAe73F+MR0TBWKBRDkmfWq/HiFmdavfZ1G7h4SPZXaCSg==", "dev": true }, "pinkie-promise": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/pinkie-promise/-/pinkie-promise-2.0.1.tgz", - "integrity": "sha1-ITXW36ejWMBprJsXh3YogihFD/o=", + "integrity": "sha512-0Gni6D4UcLTbv9c57DfxDGdr41XfgUjqWZu492f0cIGr16zDU06BWP/RAEvOuo7CQ0CNjHaLlM59YJJFm3NWlw==", "dev": true, "requires": { "pinkie": "^2.0.0" @@ -23948,10 +24379,10 @@ "extend-shallow": "^3.0.2" } }, - "posix-character-classes": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/posix-character-classes/-/posix-character-classes-0.1.1.tgz", - "integrity": "sha1-AerA/jta9xoqbAL+q7jB/vfgDqs=", + "possible-typed-array-names": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.0.0.tgz", + "integrity": "sha512-d7Uw+eZoloe0EHDIYoe+bQ5WXnGMOpmiZFTuMWCwpjzzkL2nTjcKiAk4hh8TjnGye2TwWOk3UXucZ+3rbmBa8Q==", "dev": true }, "postinstall-build": { @@ -23961,23 +24392,22 @@ "dev": true }, "prebuild-install": { - "version": "6.1.4", - "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-6.1.4.tgz", - "integrity": "sha512-Z4vpywnK1lBg+zdPCVCsKq0xO66eEV9rWo2zrROGGiRS4JtueBOdlB1FnY8lcy7JsUud/Q3ijUxyWN26Ika0vQ==", + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.1.tgz", + "integrity": "sha512-jAXscXWMcCK8GgCoHOfIr0ODh5ai8mj63L2nWrjuAgXE6tDyYGnx4/8o/rCgU+B4JSyZBKbeZqzhtwtC3ovxjw==", "dev": true, "optional": true, "requires": { - "detect-libc": "^1.0.3", + "detect-libc": "^2.0.0", "expand-template": "^2.0.3", "github-from-package": "0.0.0", "minimist": "^1.2.3", "mkdirp-classic": "^0.5.3", "napi-build-utils": "^1.0.1", - "node-abi": "^2.21.0", - "npmlog": "^4.0.1", + "node-abi": "^3.3.0", "pump": "^3.0.0", "rc": "^1.2.7", - "simple-get": "^3.0.3", + "simple-get": "^4.0.0", "tar-fs": "^2.0.0", "tunnel-agent": "^0.6.0" }, @@ -23995,10 +24425,16 @@ } } }, + "prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true + }, "prepend-http": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/prepend-http/-/prepend-http-2.0.0.tgz", - "integrity": "sha1-6SQ0v6XqjBn0HN/UAddBo8gZ2Jc=", + "integrity": "sha512-ravE6m9Atw9Z/jjttRUZ+clIXogdghyZAuWJ3qEzjT+jI/dL1ifAqhZeC5VHzQp1MSt1+jxKkFNemj/iO7tVUA==", "dev": true }, "prettier": { @@ -24007,12 +24443,6 @@ "integrity": "sha512-5xJQIPT8BraI7ZnaDwSbu5zLrB6vvi8hVV58yHQ+QK64qrY40dULy0HSRlQ2/2IdzeBpjhDkqdcFBnFeDEMVdg==", "dev": true }, - "pretty-hrtime": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/pretty-hrtime/-/pretty-hrtime-1.0.3.tgz", - "integrity": "sha1-t+PqQkNaTJsnWdmeDyAesZWALuE=", - "dev": true - }, "process": { "version": "0.11.10", "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", @@ -24045,12 +24475,6 @@ "react-is": "^16.13.1" } }, - "propagate": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/propagate/-/propagate-1.0.0.tgz", - "integrity": "sha1-AMLa7t2iDofjeCs0Stuhzd1q1wk=", - "dev": true - }, "public-encrypt": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/public-encrypt/-/public-encrypt-4.0.3.tgz", @@ -24087,16 +24511,19 @@ } }, "punycode": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", - "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==", + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", "dev": true }, "qs": { - "version": "6.5.3", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.3.tgz", - "integrity": "sha512-qxXIEh4pCGfHICj1mAJQ2/2XVZkjCDTcEgfoSQxc/fYivUZxTkk7L3bDBJSoNrEzXI17oUO5Dp07ktqE5KzczA==", - "dev": true + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.0.tgz", + "integrity": "sha512-mAZTtNCeetKMH+pSjrb76NAM8V9a05I9aBZOHztWy/UqcJdQYNsf59vrRKWnojAT9Y+GbIvoTBC++CPHqpDBhQ==", + "dev": true, + "requires": { + "side-channel": "^1.1.0" + } }, "query-string": { "version": "5.1.1", @@ -24127,6 +24554,12 @@ "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", "dev": true }, + "queue-tick": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/queue-tick/-/queue-tick-1.0.1.tgz", + "integrity": "sha512-kJt5qhMxoszgU/62PLP1CJytzd2NKetjSRnyuj31fDd3Rlcz3fzlFdFLD1SItunPwyqEOkca6GbV612BWfaBag==", + "dev": true + }, "randombytes": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", @@ -24162,7 +24595,7 @@ "strip-json-comments": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", - "integrity": "sha1-PFMZQukIwml8DsNEhYwobHygpgo=", + "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", "dev": true, "optional": true } @@ -24183,71 +24616,10 @@ "mute-stream": "~0.0.4" } }, - "read-pkg": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-1.1.0.tgz", - "integrity": "sha1-9f+qXs0pyzHAR0vKfXVra7KePyg=", - "dev": true, - "requires": { - "load-json-file": "^1.0.0", - "normalize-package-data": "^2.3.2", - "path-type": "^1.0.0" - }, - "dependencies": { - "path-type": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/path-type/-/path-type-1.1.0.tgz", - "integrity": "sha1-WcRPfuSR2nBNpBXaWkBwuk+P5EE=", - "dev": true, - "requires": { - "graceful-fs": "^4.1.2", - "pify": "^2.0.0", - "pinkie-promise": "^2.0.0" - } - }, - "pify": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", - "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=", - "dev": true - } - } - }, - "read-pkg-up": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-1.0.1.tgz", - "integrity": "sha1-nWPBMnbAZZGNV/ACpX9AobZD+wI=", - "dev": true, - "requires": { - "find-up": "^1.0.0", - "read-pkg": "^1.0.0" - }, - "dependencies": { - "find-up": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-1.1.2.tgz", - "integrity": "sha1-ay6YIrGizgpgq2TWEOzK1TyyTQ8=", - "dev": true, - "requires": { - "path-exists": "^2.0.0", - "pinkie-promise": "^2.0.0" - } - }, - "path-exists": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-2.1.0.tgz", - "integrity": "sha1-D+tsZPD8UY2adU3V77YscCJ2H0s=", - "dev": true, - "requires": { - "pinkie-promise": "^2.0.0" - } - } - } - }, "readable-stream": { - "version": "2.3.7", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", - "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", "dev": true, "requires": { "core-util-is": "~1.0.0", @@ -24280,51 +24652,31 @@ } }, "rechoir": { - "version": "0.6.2", - "resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.6.2.tgz", - "integrity": "sha1-hSBLVNuoLVdC4oyWdW70OvUOM4Q=", + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.8.0.tgz", + "integrity": "sha512-/vxpCXddiX8NGfGO/mTafwjq4aFa/71pvamip0++IQk3zG8cbCj0fifNPrjjF1XMXUne91jL9OoxmdykoEtifQ==", "dev": true, "requires": { - "resolve": "^1.1.6" + "resolve": "^1.20.0" } }, "reflect-metadata": { - "version": "0.1.13", - "resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.1.13.tgz", - "integrity": "sha512-Ts1Y/anZELhSsjMcU605fU9RE4Oi3p5ORujwbIKXfWa+0Zxs510Qrmrce5/Jowq3cHSZSJqBjypxmHarc+vEWg==" - }, - "regenerator-runtime": { - "version": "0.13.9", - "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.9.tgz", - "integrity": "sha512-p3VT+cOEgxFsRRA9X4lkI1E+k2/CtnKtU4gcxyaCUreilL/vqI6CdZ3wxVUx3UOUg+gnUOQQcRI7BmSI656MYA==", - "dev": true - }, - "regex-not": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/regex-not/-/regex-not-1.0.2.tgz", - "integrity": "sha512-J6SDjUgDxQj5NusnOtdFxDwN/+HWykR8GELwctJ7mdqhcyy1xEc4SRFHUXvxTp661YaVKAjfRLZ9cCqS6tn32A==", - "dev": true, - "requires": { - "extend-shallow": "^3.0.2", - "safe-regex": "^1.1.0" - } + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.2.2.tgz", + "integrity": "sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q==" }, "regexp.prototype.flags": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.4.1.tgz", - "integrity": "sha512-pMR7hBVUUGI7PMA37m2ofIdQCsomVnas+Jn5UPGAHQ+/LlwKm/aTLJHdasmHRzlfeZwHiAOaRSo2rbBDm3nNUQ==", + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.2.tgz", + "integrity": "sha512-NcDiDkTLuPR+++OCKB0nWafEmhg/Da8aUPLPMQbK+bxKKCm1/S5he+AqYa4PlMCVBalb4/yxIRub6qkEx5yJbw==", "dev": true, "requires": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.3" + "call-bind": "^1.0.6", + "define-properties": "^1.2.1", + "es-errors": "^1.3.0", + "set-function-name": "^2.0.1" } }, - "regexpp": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/regexpp/-/regexpp-3.1.0.tgz", - "integrity": "sha512-ZOIzd8yVsQQA7j8GCSlPGXwg5PfmA1mrq0JP4nGhh54LaKN3xdai/vHUDu74pKwV8OxseMS65u2NImosQcSD0Q==", - "dev": true - }, "release-zalgo": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/release-zalgo/-/release-zalgo-1.0.0.tgz", @@ -24361,18 +24713,6 @@ "integrity": "sha1-wkvOKig62tW8P1jg1IJJuSN52O8=", "dev": true }, - "repeat-element": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/repeat-element/-/repeat-element-1.1.3.tgz", - "integrity": "sha512-ahGq0ZnV5m5XtZLMb+vP76kcAM5nkLqk0lpqAuojSKGgQtn4eRi4ZZGm2olo2zKFH+sMsWaqOCW1dqAnOru72g==", - "dev": true - }, - "repeat-string": { - "version": "1.6.1", - "resolved": "https://registry.npmjs.org/repeat-string/-/repeat-string-1.6.1.tgz", - "integrity": "sha1-jcrkcOHIirwtYA//Sndihtp15jc=", - "dev": true - }, "replace-ext": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/replace-ext/-/replace-ext-1.0.0.tgz", @@ -24380,15 +24720,10 @@ "dev": true }, "replace-homedir": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/replace-homedir/-/replace-homedir-1.0.0.tgz", - "integrity": "sha1-6H9tUTuSjd6AgmDBK+f+xv9ueYw=", - "dev": true, - "requires": { - "homedir-polyfill": "^1.0.1", - "is-absolute": "^1.0.0", - "remove-trailing-separator": "^1.1.0" - } + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/replace-homedir/-/replace-homedir-2.0.0.tgz", + "integrity": "sha512-bgEuQQ/BHW0XkkJtawzrfzHFSN70f/3cNOiHa2QsYxqrjaC30X1k74FJ6xswVBP0sr0SpGIdVFuPwfrYziVeyw==", + "dev": true }, "require-directory": { "version": "2.1.1", @@ -24402,19 +24737,32 @@ "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", "dev": true }, - "require-main-filename": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-1.0.1.tgz", - "integrity": "sha1-l/cXtp1IeE9fUmpsWqj/3aBVpNE=", - "dev": true + "require-in-the-middle": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/require-in-the-middle/-/require-in-the-middle-7.2.0.tgz", + "integrity": "sha512-3TLx5TGyAY6AOqLBoXmHkNql0HIf2RGbuMgCDT2WO/uGVAPJs6h7Kl+bN6TIZGd9bWhWPwnDnTHGtW8Iu77sdw==", + "requires": { + "debug": "^4.1.1", + "module-details-from-path": "^1.0.3", + "resolve": "^1.22.1" + }, + "dependencies": { + "debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "requires": { + "ms": "2.1.2" + } + } + } }, "resolve": { - "version": "1.22.0", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.0.tgz", - "integrity": "sha512-Hhtrw0nLeSrFQ7phPp4OOcVjLPIeMnRlr5mcnVuMe7M/7eBn98A3hmFRLoFo3DLZkivSYwhRUJTyPyWAk56WLw==", - "dev": true, + "version": "1.22.4", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.4.tgz", + "integrity": "sha512-PXNdCiPqDqeUou+w1C2eTQbNfxKSuMxqTCuvlmmMsk1NWHL5fRrhY6Pl0qEYYc6+QqGClco1Qj8XnjPego4wfg==", "requires": { - "is-core-module": "^2.8.1", + "is-core-module": "^2.13.0", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" } @@ -24431,7 +24779,7 @@ "resolve-dir": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/resolve-dir/-/resolve-dir-1.0.1.tgz", - "integrity": "sha1-eaQGRMNivoLybv/nOcm7U4IEb0M=", + "integrity": "sha512-R7uiTjECzvOsWSfdM0QKFNBVFcK27aHOUwdvK53BcW8zqnGdYp0Fbj82cy54+2A4P2tFM22J5kRfe1R+lM/1yg==", "dev": true, "requires": { "expand-tilde": "^2.0.0", @@ -24453,27 +24801,15 @@ "value-or-function": "^3.0.0" } }, - "resolve-url": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/resolve-url/-/resolve-url-0.2.1.tgz", - "integrity": "sha1-LGN/53yJOv0qZj/iGqkIAGjiBSo=", - "dev": true - }, "responselike": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/responselike/-/responselike-1.0.2.tgz", - "integrity": "sha1-kYcg7ztjHFZCvgaPFa3lpG9Loec=", + "integrity": "sha512-/Fpe5guzJk1gPqdJLJR5u7eG/gNY4nImjbRDaVWVMRhne55TCmj2i9Q+54PBRfatRC8v/rIiv9BN0pMd9OV5EQ==", "dev": true, "requires": { "lowercase-keys": "^1.0.0" } }, - "ret": { - "version": "0.1.15", - "resolved": "https://registry.npmjs.org/ret/-/ret-0.1.15.tgz", - "integrity": "sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg==", - "dev": true - }, "reusify": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", @@ -24481,15 +24817,13 @@ "dev": true }, "rewiremock": { - "version": "3.14.3", - "resolved": "https://registry.npmjs.org/rewiremock/-/rewiremock-3.14.3.tgz", - "integrity": "sha512-6BaUGfp7NtxBjisxcGN73nNiA2fS2AwhEk/9DMUqxfv5v0aDM1wpOYpj5GSArqsJi07YCfLhkD8C74LAN7+FkQ==", + "version": "3.14.6", + "resolved": "https://registry.npmjs.org/rewiremock/-/rewiremock-3.14.6.tgz", + "integrity": "sha512-hjpS7iQUTVVh/IHV4GE1ypg4IzlgVc34gxZBarwwVrKfnjlyqHJuQdsia6Ac7m4f4k/zxxA3tX285MOstdysRQ==", "dev": true, "requires": { "babel-runtime": "^6.26.0", "compare-module-exports": "^2.1.0", - "lodash.some": "^4.6.0", - "lodash.template": "^4.4.0", "node-libs-browser": "^2.1.0", "path-parse": "^1.0.5", "wipe-node-cache": "^2.1.2", @@ -24506,13 +24840,33 @@ } }, "ripemd160": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/ripemd160/-/ripemd160-2.0.2.tgz", - "integrity": "sha512-ii4iagi25WusVoiC4B4lq7pbXfAp3D9v5CwfkY33vffw2+pkDjY1D8GaN7spsxvCSx8dkPqOZCEZyfxcmJG2IA==", + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/ripemd160/-/ripemd160-2.0.3.tgz", + "integrity": "sha512-5Di9UC0+8h1L6ZD2d7awM7E/T4uA1fJRlx6zk/NvdCCVEoAnFqvHmCuNeIKoCeIixBX/q8uM+6ycDvF8woqosA==", "dev": true, "requires": { - "hash-base": "^3.0.0", - "inherits": "^2.0.1" + "hash-base": "^3.1.2", + "inherits": "^2.0.4" + }, + "dependencies": { + "hash-base": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/hash-base/-/hash-base-3.1.2.tgz", + "integrity": "sha512-Bb33KbowVTIj5s7Ked1OsqHUeCpz//tPwR+E2zJgJKo9Z5XolZ9b6bdUgjmYlwnWhoOQKoTd1TYToZGn5mAYOg==", + "dev": true, + "requires": { + "inherits": "^2.0.4", + "readable-stream": "^2.3.8", + "safe-buffer": "^5.2.1", + "to-buffer": "^1.2.1" + } + }, + "safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "dev": true + } } }, "run-parallel": { @@ -24537,18 +24891,41 @@ "resolved": "https://registry.npmjs.org/rxjs-compat/-/rxjs-compat-6.6.7.tgz", "integrity": "sha512-szN4fK+TqBPOFBcBcsR0g2cmTTUF/vaFEOZNuSdfU8/pGFnNmmn2u8SystYXG1QMrjOPBc6XTKHMVfENDf6hHw==" }, + "safe-array-concat": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.2.tgz", + "integrity": "sha512-vj6RsCsWBCf19jIeHEfkRMw8DPiBb+DMXklQ/1SGDHOMlHdPUkZXFQ2YdplS23zESTijAcurb1aSgJA3AgMu1Q==", + "dev": true, + "requires": { + "call-bind": "^1.0.7", + "get-intrinsic": "^1.2.4", + "has-symbols": "^1.0.3", + "isarray": "^2.0.5" + }, + "dependencies": { + "isarray": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", + "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", + "dev": true + } + } + }, "safe-buffer": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true }, - "safe-regex": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/safe-regex/-/safe-regex-1.1.0.tgz", - "integrity": "sha1-QKNmnzsHfR6UPURinhV91IAjvy4=", + "safe-regex-test": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.0.3.tgz", + "integrity": "sha512-CdASjNJPvRa7roO6Ra/gLYBTzYzzPyyBXxIMdGW3USQLyjWEls2RgW5UBTXaQVp+OrpeCK3bLem8smtmheoRuw==", "dev": true, "requires": { - "ret": "~0.1.10" + "call-bind": "^1.0.6", + "es-errors": "^1.3.0", + "is-regex": "^1.1.4" } }, "safer-buffer": { @@ -24562,9 +24939,9 @@ "integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==" }, "schema-utils": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.1.1.tgz", - "integrity": "sha512-Y5PQxS4ITlC+EahLuXaY86TXfR7Dc5lw294alXOq86JAHCihAIZfqv8nNCWvaEJvaC51uN9hbLGeV0cFBdH+Fw==", + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.3.0.tgz", + "integrity": "sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg==", "dev": true, "requires": { "@types/json-schema": "^7.0.8", @@ -24573,43 +24950,35 @@ } }, "seek-bzip": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/seek-bzip/-/seek-bzip-1.0.5.tgz", - "integrity": "sha1-z+kXyz0nS8/6x5J1ivUxc+sfq9w=", + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/seek-bzip/-/seek-bzip-1.0.6.tgz", + "integrity": "sha512-e1QtP3YL5tWww8uKaOCQ18UxIT2laNBXHjV/S2WYCiK4udiv8lkG89KRIoCjUagnAmCBurjF4zEVX2ByBbnCjQ==", "dev": true, "requires": { - "commander": "~2.8.1" - }, - "dependencies": { - "commander": { - "version": "2.8.1", - "resolved": "https://registry.npmjs.org/commander/-/commander-2.8.1.tgz", - "integrity": "sha1-Br42f+v9oMMwqh4qBy09yXYkJdQ=", - "dev": true, - "requires": { - "graceful-readlink": ">= 1.0.0" - } - } + "commander": "^2.8.1" } }, "semver": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", - "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==" + "version": "7.6.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.0.tgz", + "integrity": "sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg==", + "requires": { + "lru-cache": "^6.0.0" + } }, "semver-greatest-satisfied-range": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/semver-greatest-satisfied-range/-/semver-greatest-satisfied-range-1.1.0.tgz", - "integrity": "sha1-E+jCZYq5aRywzXEJMkAoDTb3els=", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/semver-greatest-satisfied-range/-/semver-greatest-satisfied-range-2.0.0.tgz", + "integrity": "sha512-lH3f6kMbwyANB7HuOWRMlLCa2itaCrZJ+SAqqkSZrZKO/cAsk2EOyaKHUtNkVLFyFW9pct22SFesFp3Z7zpA0g==", "dev": true, "requires": { - "sver-compat": "^1.5.0" + "sver": "^1.8.3" } }, "serialize-javascript": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.0.tgz", - "integrity": "sha512-Qr3TosvguFt8ePWqsvRfrKyQXIiW+nGbYpy8XK24NQHE83caxWt+mIymTT19DGFbNWNLfEwsrkSmN64lVWB9ag==", + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.2.tgz", + "integrity": "sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==", "dev": true, "requires": { "randombytes": "^2.1.0" @@ -24621,27 +24990,30 @@ "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=", "dev": true }, - "set-value": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/set-value/-/set-value-2.0.1.tgz", - "integrity": "sha512-JxHc1weCN68wRY0fhCoXpyK55m/XPHafOmK4UWD7m2CI14GMcFypt4w/0+NV5f/ZMby2F6S2wwA7fgynh9gWSw==", + "set-function-length": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", + "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", "dev": true, "requires": { - "extend-shallow": "^2.0.1", - "is-extendable": "^0.1.1", - "is-plain-object": "^2.0.3", - "split-string": "^3.0.1" - }, - "dependencies": { - "extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", - "dev": true, - "requires": { - "is-extendable": "^0.1.0" - } - } + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.2" + } + }, + "set-function-name": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/set-function-name/-/set-function-name-2.0.2.tgz", + "integrity": "sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==", + "dev": true, + "requires": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "functions-have-names": "^1.2.3", + "has-property-descriptors": "^1.0.2" } }, "setimmediate": { @@ -24651,12 +25023,22 @@ "dev": true }, "sha.js": { - "version": "2.4.11", - "resolved": "https://registry.npmjs.org/sha.js/-/sha.js-2.4.11.tgz", - "integrity": "sha512-QMEp5B7cftE7APOjk5Y6xgrbWu+WkLVQwk8JNjZ8nKRciZaByEW6MubieAiToS7+dwvrjGhH8jRXz3MVd0AYqQ==", + "version": "2.4.12", + "resolved": "https://registry.npmjs.org/sha.js/-/sha.js-2.4.12.tgz", + "integrity": "sha512-8LzC5+bvI45BjpfXU8V5fdU2mfeKiQe1D1gIMn7XUlF3OTUrpdJpPPH4EMAnF0DsHHdSZqCdSss5qCmJKuiO3w==", + "dev": true, "requires": { - "inherits": "^2.0.1", - "safe-buffer": "^5.0.1" + "inherits": "^2.0.4", + "safe-buffer": "^5.2.1", + "to-buffer": "^1.2.0" + }, + "dependencies": { + "safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "dev": true + } } }, "shallow-clone": { @@ -24689,23 +25071,60 @@ "integrity": "sha512-sQTKC1Re/rM6XyFM6fIAGHRPVGvyXfgzIDvzoq608vM+jeyVD0Tu1E6Np0Kc2zAIFWIj963V2800iF/9LPieQw==" }, "shortid": { - "version": "2.2.16", - "resolved": "https://registry.npmjs.org/shortid/-/shortid-2.2.16.tgz", - "integrity": "sha512-Ugt+GIZqvGXCIItnsL+lvFJOiN7RYqlGy7QE41O3YC1xbNSeDGIRO7xg2JJXIAj1cAGnOeC1r7/T9pgrtQbv4g==", + "version": "2.2.17", + "resolved": "https://registry.npmjs.org/shortid/-/shortid-2.2.17.tgz", + "integrity": "sha512-GpbM3gLF1UUXZvQw6MCyulHkWbRseNO4cyBEZresZRorwl1+SLu1ZdqgVtuwqz8mB6RpwPkm541mYSqrKyJSaA==", "dev": true, "requires": { - "nanoid": "^2.1.0" + "nanoid": "^3.3.8" } }, "side-channel": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz", - "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", "dev": true, "requires": { - "call-bind": "^1.0.0", - "get-intrinsic": "^1.0.2", - "object-inspect": "^1.9.0" + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + } + }, + "side-channel-list": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "dev": true, + "requires": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + } + }, + "side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "dev": true, + "requires": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + } + }, + "side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "dev": true, + "requires": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" } }, "signal-exit": { @@ -24722,54 +25141,54 @@ "optional": true }, "simple-get": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-3.1.1.tgz", - "integrity": "sha512-CQ5LTKGfCpvE1K0n2us+kuMPbk/q0EKl82s4aheV9oXjFEz6W/Y7oQFVJuU6QG77hRT4Ghb5RURteF5vnWjupA==", + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz", + "integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==", "dev": true, "optional": true, "requires": { - "decompress-response": "^4.2.0", + "decompress-response": "^6.0.0", "once": "^1.3.1", "simple-concat": "^1.0.0" }, "dependencies": { "decompress-response": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-4.2.1.tgz", - "integrity": "sha512-jOSne2qbyE+/r8G1VU+G/82LBs2Fs4LAsTiLSHOCOMZQl2OKZ6i8i4IyHemTe+/yIXOtTcRQMzPcgyhoFlqPkw==", + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", + "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", "dev": true, "optional": true, "requires": { - "mimic-response": "^2.0.0" + "mimic-response": "^3.1.0" } }, "mimic-response": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-2.1.0.tgz", - "integrity": "sha512-wXqjST+SLt7R009ySCglWBCFpjUygmCIfD790/kVbiGmUgfYGuB14PiTd5DwVxSV4NcYHjzMkoj5LjQZwTQLEA==", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", + "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", "dev": true, "optional": true } } }, "sinon": { - "version": "13.0.1", - "resolved": "https://registry.npmjs.org/sinon/-/sinon-13.0.1.tgz", - "integrity": "sha512-8yx2wIvkBjIq/MGY1D9h1LMraYW+z1X0mb648KZnKSdvLasvDu7maa0dFaNYdTDczFgbjNw2tOmWdTk9saVfwQ==", + "version": "18.0.0", + "resolved": "https://registry.npmjs.org/sinon/-/sinon-18.0.0.tgz", + "integrity": "sha512-+dXDXzD1sBO6HlmZDd7mXZCR/y5ECiEiGCBSGuFD/kZ0bDTofPYc6JaeGmPSF+1j1MejGUWkORbYOLDyvqCWpA==", "dev": true, "requires": { - "@sinonjs/commons": "^1.8.3", - "@sinonjs/fake-timers": "^9.0.0", - "@sinonjs/samsam": "^6.1.1", - "diff": "^5.0.0", - "nise": "^5.1.1", - "supports-color": "^7.2.0" + "@sinonjs/commons": "^3.0.1", + "@sinonjs/fake-timers": "^11.2.2", + "@sinonjs/samsam": "^8.0.0", + "diff": "^5.2.0", + "nise": "^6.0.0", + "supports-color": "^7" }, "dependencies": { "diff": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/diff/-/diff-5.0.0.tgz", - "integrity": "sha512-/VTCrvm5Z0JGty/BWHljh+BAiw3IK+2j87NGMu8Nwc/f48WoDAC395uomO9ZD117ZOBaHmkX1oyLvkVM/aIT3w==", + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/diff/-/diff-5.2.2.tgz", + "integrity": "sha512-vtcDfH3TOjP8UekytvnHH1o1P4FcUdt4eQ1Y+Abap1tk/OB2MWQvcwS2ClCd1zuIhc3JKOx6p3kod8Vfys3E+A==", "dev": true }, "has-flag": { @@ -24806,166 +25225,10 @@ "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", "dev": true }, - "slice-ansi": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-4.0.0.tgz", - "integrity": "sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ==", - "dev": true, - "requires": { - "ansi-styles": "^4.0.0", - "astral-regex": "^2.0.0", - "is-fullwidth-code-point": "^3.0.0" - }, - "dependencies": { - "ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "requires": { - "color-convert": "^2.0.1" - } - }, - "color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "requires": { - "color-name": "~1.1.4" - } - }, - "color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true - }, - "is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "dev": true - } - } - }, - "snapdragon": { - "version": "0.8.2", - "resolved": "https://registry.npmjs.org/snapdragon/-/snapdragon-0.8.2.tgz", - "integrity": "sha512-FtyOnWN/wCHTVXOMwvSv26d+ko5vWlIDD6zoUJ7LW8vh+ZBC8QdljveRP+crNrtBwioEUWy/4dMtbBjA4ioNlg==", - "dev": true, - "requires": { - "base": "^0.11.1", - "debug": "^2.2.0", - "define-property": "^0.2.5", - "extend-shallow": "^2.0.1", - "map-cache": "^0.2.2", - "source-map": "^0.5.6", - "source-map-resolve": "^0.5.0", - "use": "^3.1.0" - }, - "dependencies": { - "define-property": { - "version": "0.2.5", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", - "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", - "dev": true, - "requires": { - "is-descriptor": "^0.1.0" - } - }, - "extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", - "dev": true, - "requires": { - "is-extendable": "^0.1.0" - } - }, - "source-map": { - "version": "0.5.7", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", - "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=", - "dev": true - } - } - }, - "snapdragon-node": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/snapdragon-node/-/snapdragon-node-2.1.1.tgz", - "integrity": "sha512-O27l4xaMYt/RSQ5TR3vpWCAB5Kb/czIcqUFOM/C4fYcLnbZUc1PkjTAMjof2pBWaSTwOUd6qUHcFGVGj7aIwnw==", - "dev": true, - "requires": { - "define-property": "^1.0.0", - "isobject": "^3.0.0", - "snapdragon-util": "^3.0.1" - }, - "dependencies": { - "define-property": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", - "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", - "dev": true, - "requires": { - "is-descriptor": "^1.0.0" - } - }, - "is-accessor-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", - "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", - "dev": true, - "requires": { - "kind-of": "^6.0.0" - } - }, - "is-data-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", - "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", - "dev": true, - "requires": { - "kind-of": "^6.0.0" - } - }, - "is-descriptor": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", - "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", - "dev": true, - "requires": { - "is-accessor-descriptor": "^1.0.0", - "is-data-descriptor": "^1.0.0", - "kind-of": "^6.0.2" - } - } - } - }, - "snapdragon-util": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/snapdragon-util/-/snapdragon-util-3.0.1.tgz", - "integrity": "sha512-mbKkMdQKsjX4BAL4bRYTj21edOf8cN7XHdYUJEe+Zn99hVEYcMvKPct1IqNe7+AZPirn8BCDOQBHQZknqmKlZQ==", - "dev": true, - "requires": { - "kind-of": "^3.2.0" - }, - "dependencies": { - "kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "dev": true, - "requires": { - "is-buffer": "^1.1.5" - } - } - } - }, "sort-keys": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/sort-keys/-/sort-keys-1.1.2.tgz", - "integrity": "sha1-RBttTTRnmPG05J6JIK37oOVD+a0=", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/sort-keys/-/sort-keys-2.0.0.tgz", + "integrity": "sha512-/dPCrG1s3ePpWm6yBbxZq5Be1dXGLyLn9Z791chDC3NFrpkVbWGzkBwPN1knaciexFXgRJ7hzdnwZ4stHSDmjg==", "dev": true, "requires": { "is-plain-obj": "^1.0.0" @@ -24974,10 +25237,21 @@ "sort-keys-length": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/sort-keys-length/-/sort-keys-length-1.0.1.tgz", - "integrity": "sha1-nLb09OnkgVWmqgZx7dM2/xR5oYg=", + "integrity": "sha512-GRbEOUqCxemTAk/b32F2xa8wDTs+Z1QHOkbhJDQTvv/6G3ZkbJ+frYWsTcc7cBB3Fu4wy4XlLCuNtJuMn7Gsvw==", "dev": true, "requires": { "sort-keys": "^1.0.0" + }, + "dependencies": { + "sort-keys": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/sort-keys/-/sort-keys-1.1.2.tgz", + "integrity": "sha512-vzn8aSqKgytVik0iwdBEi+zevbTYZogewTUM6dtpmGwEcdzbub/TX4bCzRhebDCRC3QzXgJsLRKB2V/Oof7HXg==", + "dev": true, + "requires": { + "is-plain-obj": "^1.0.0" + } + } } }, "source-map": { @@ -24986,19 +25260,6 @@ "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", "dev": true }, - "source-map-resolve": { - "version": "0.5.2", - "resolved": "https://registry.npmjs.org/source-map-resolve/-/source-map-resolve-0.5.2.tgz", - "integrity": "sha512-MjqsvNwyz1s0k81Goz/9vRBe9SZdB09Bdw+/zYyO+3CuPk6fouTaxscHkgtE8jKvf01kVfl8riHzERQ/kefaSA==", - "dev": true, - "requires": { - "atob": "^2.1.1", - "decode-uri-component": "^0.2.0", - "resolve-url": "^0.2.1", - "source-map-url": "^0.4.0", - "urix": "^0.1.0" - } - }, "source-map-support": { "version": "0.5.21", "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", @@ -25009,16 +25270,10 @@ "source-map": "^0.6.0" } }, - "source-map-url": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/source-map-url/-/source-map-url-0.4.0.tgz", - "integrity": "sha1-PpNdfd1zYxuXZZlW1VEo6HtQhKM=", - "dev": true - }, "sparkles": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/sparkles/-/sparkles-1.0.1.tgz", - "integrity": "sha512-dSO0DDYUahUt/0/pD/Is3VIm5TGJjludZ0HVymmhYF6eNA53PVLhnUk0znSYbH8IYBuJdCE+1luR22jNLMaQdw==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/sparkles/-/sparkles-2.1.0.tgz", + "integrity": "sha512-r7iW1bDw8R/cFifrD3JnQJX0K1jqT0kprL48BiBpLZLJPmAm34zsVBsK5lc7HirZYZqMW65dOXZgbAGt/I6frg==", "dev": true }, "spawn-wrap": { @@ -25035,47 +25290,6 @@ "which": "^2.0.1" } }, - "spdx-correct": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.1.0.tgz", - "integrity": "sha512-lr2EZCctC2BNR7j7WzJ2FpDznxky1sjfxvvYEyzxNyb6lZXHODmEoJeFu4JupYlkfha1KZpJyoqiJ7pgA1qq8Q==", - "dev": true, - "requires": { - "spdx-expression-parse": "^3.0.0", - "spdx-license-ids": "^3.0.0" - } - }, - "spdx-exceptions": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.2.0.tgz", - "integrity": "sha512-2XQACfElKi9SlVb1CYadKDXvoajPgBVPn/gOQLrTvHdElaVhr7ZEbqJaRnJLVNeaI4cMEAgVCeBMKF6MWRDCRA==", - "dev": true - }, - "spdx-expression-parse": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.0.tgz", - "integrity": "sha512-Yg6D3XpRD4kkOmTpdgbUiEJFKghJH03fiC1OPll5h/0sO6neh2jqRDVHOQ4o/LMea0tgCkbMgea5ip/e+MkWyg==", - "dev": true, - "requires": { - "spdx-exceptions": "^2.1.0", - "spdx-license-ids": "^3.0.0" - } - }, - "spdx-license-ids": { - "version": "3.0.5", - "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.5.tgz", - "integrity": "sha512-J+FWzZoynJEXGphVIS+XEh3kFSjZX/1i9gFBaWQcB+/tmpe2qUsSBABpcxqxnAxFdiUFEgAX1bjYGQvIZmoz9Q==", - "dev": true - }, - "split-string": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/split-string/-/split-string-3.1.0.tgz", - "integrity": "sha512-NzNVhJDYpwceVVii8/Hu6DKfD2G+NrQHlS/V/qgv763EYudVwEcMQNxd2lh+0VrUByXN/oJkl5grOhYWvQUYiw==", - "dev": true, - "requires": { - "extend-shallow": "^3.0.0" - } - }, "sprintf-js": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", @@ -25092,26 +25306,11 @@ "resolved": "https://registry.npmjs.org/stack-trace/-/stack-trace-0.0.10.tgz", "integrity": "sha1-VHxws0fo0ytOEI6hoqFZ5f3eGcA=" }, - "static-extend": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/static-extend/-/static-extend-0.1.2.tgz", - "integrity": "sha1-YICcOcv/VTNyJv1eC1IPNB8ftcY=", - "dev": true, - "requires": { - "define-property": "^0.2.5", - "object-copy": "^0.1.0" - }, - "dependencies": { - "define-property": { - "version": "0.2.5", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", - "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", - "dev": true, - "requires": { - "is-descriptor": "^0.1.0" - } - } - } + "stoppable": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/stoppable/-/stoppable-1.1.0.tgz", + "integrity": "sha512-KXDYZ9dszj6bzvnEMRYvxgeTHU74QBFL54XKtP3nyMuJ81CFYtABZ3bAzL2EdFUaEwJOBOgENyFj3R7oTzDyyw==", + "dev": true }, "stream-browserify": { "version": "2.0.2", @@ -25123,6 +25322,15 @@ "readable-stream": "^2.0.2" } }, + "stream-composer": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/stream-composer/-/stream-composer-1.0.2.tgz", + "integrity": "sha512-bnBselmwfX5K10AH6L4c8+S5lgZMWI7ZYrz2rvYjCPB2DIMC4Ig8OpxGpNJSxRZ58oti7y1IcNvjBAz9vW5m4w==", + "dev": true, + "requires": { + "streamx": "^2.13.2" + } + }, "stream-exhaust": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/stream-exhaust/-/stream-exhaust-1.0.2.tgz", @@ -25148,10 +25356,22 @@ "integrity": "sha1-1cdSgl5TZ+eG944Y5EXqIjoVWVI=", "dev": true }, + "streamx": { + "version": "2.18.0", + "resolved": "https://registry.npmjs.org/streamx/-/streamx-2.18.0.tgz", + "integrity": "sha512-LLUC1TWdjVdn1weXGcSxyTR3T4+acB6tVGXT95y0nGbca4t4o/ng1wKAGTljm9VicuCVLvRlqFYXYy5GwgM7sQ==", + "dev": true, + "requires": { + "bare-events": "^2.2.0", + "fast-fifo": "^1.3.2", + "queue-tick": "^1.0.1", + "text-decoder": "^1.1.0" + } + }, "strict-uri-encode": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/strict-uri-encode/-/strict-uri-encode-1.1.0.tgz", - "integrity": "sha1-J5siXfHVgrH1TmWt3UNS4Y+qBxM=", + "integrity": "sha512-R3f198pcvnB+5IpnBlRkphuE9n46WyVl8I39W/ZUTZLz4nqSP/oLYUrcnJrw462Ds8he4YKMov2efsTIw1BDGQ==", "dev": true }, "string_decoder": { @@ -25172,31 +25392,25 @@ } }, "string-width": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", - "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", "dev": true, "requires": { - "code-point-at": "^1.0.0", - "is-fullwidth-code-point": "^1.0.0", - "strip-ansi": "^3.0.0" - }, - "dependencies": { - "ansi-regex": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", - "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", - "dev": true - }, - "strip-ansi": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", - "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", - "dev": true, - "requires": { - "ansi-regex": "^2.0.0" - } - } + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + } + }, + "string-width-cjs": { + "version": "npm:string-width@4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "requires": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" } }, "string.prototype.matchall": { @@ -25215,24 +25429,38 @@ "side-channel": "^1.0.4" } }, + "string.prototype.trim": { + "version": "1.2.9", + "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.9.tgz", + "integrity": "sha512-klHuCNxiMZ8MlsOihJhJEBJAiMVqU3Z2nEXWfWnIqjN0gEFS9J9+IxKozWWtQGcgoa1WUZzLjKPTr4ZHNFTFxw==", + "dev": true, + "requires": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.0", + "es-object-atoms": "^1.0.0" + } + }, "string.prototype.trimend": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.4.tgz", - "integrity": "sha512-y9xCjw1P23Awk8EvTpcyL2NIr1j7wJ39f+k6lvRnSMz+mz9CGz9NYPelDk42kOz6+ql8xjfK8oYzy3jAP5QU5A==", + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.8.tgz", + "integrity": "sha512-p73uL5VCHCO2BZZ6krwwQE3kCzM7NKmis8S//xEC6fQonchbum4eP6kR4DLEjQFO3Wnj3Fuo8NM0kOSjVdHjZQ==", "dev": true, "requires": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.3" + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" } }, "string.prototype.trimstart": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.4.tgz", - "integrity": "sha512-jh6e984OBfvxS50tdY2nRZnoC5/mLFKOREQfw8t5yytkoUsJRNxvI/E39qu1sD0OtWI3OC0XgKSmcWwziwYuZw==", + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.8.tgz", + "integrity": "sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==", "dev": true, "requires": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.3" + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" } }, "strip-ansi": { @@ -25244,13 +25472,13 @@ "ansi-regex": "^5.0.1" } }, - "strip-bom": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-2.0.0.tgz", - "integrity": "sha1-YhmoVhZSBJHzV4i9vxRHqZx+aw4=", + "strip-ansi-cjs": { + "version": "npm:strip-ansi@6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", "dev": true, "requires": { - "is-utf8": "^0.2.0" + "ansi-regex": "^5.0.1" } }, "strip-dirs": { @@ -25300,79 +25528,36 @@ "supports-preserve-symlinks-flag": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", - "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", - "dev": true - }, - "sver-compat": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/sver-compat/-/sver-compat-1.5.0.tgz", - "integrity": "sha1-PPh9/rTQe0o/FIJ7wYaz/QxkXNg=", - "dev": true, - "requires": { - "es6-iterator": "^2.0.1", - "es6-symbol": "^3.1.1" - } + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==" }, - "table": { - "version": "6.7.5", - "resolved": "https://registry.npmjs.org/table/-/table-6.7.5.tgz", - "integrity": "sha512-LFNeryOqiQHqCVKzhkymKwt6ozeRhlm8IL1mE8rNUurkir4heF6PzMyRgaTa4tlyPTGGgXuvVOF/OLWiH09Lqw==", + "sver": { + "version": "1.8.4", + "resolved": "https://registry.npmjs.org/sver/-/sver-1.8.4.tgz", + "integrity": "sha512-71o1zfzyawLfIWBOmw8brleKyvnbn73oVHNCsu51uPMz/HWiKkkXsI31JjHW5zqXEqnPYkIiHd8ZmL7FCimLEA==", "dev": true, "requires": { - "ajv": "^8.0.1", - "lodash.truncate": "^4.4.2", - "slice-ansi": "^4.0.0", - "string-width": "^4.2.3", - "strip-ansi": "^6.0.1" + "semver": "^6.3.0" }, "dependencies": { - "ajv": { - "version": "8.10.0", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.10.0.tgz", - "integrity": "sha512-bzqAEZOjkrUMl2afH8dknrq5KEk2SrwdBROR+vH1EKVQTqaUbJVPdc/gEdggTMM0Se+s+Ja4ju4TlNcStKl2Hw==", - "dev": true, - "requires": { - "fast-deep-equal": "^3.1.1", - "json-schema-traverse": "^1.0.0", - "require-from-string": "^2.0.2", - "uri-js": "^4.2.2" - } - }, - "is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "dev": true - }, - "json-schema-traverse": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", - "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", - "dev": true - }, - "string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", "dev": true, - "requires": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - } + "optional": true } } }, "tapable": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.1.tgz", - "integrity": "sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==", + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.0.tgz", + "integrity": "sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==", "dev": true }, "tar-fs": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.1.tgz", - "integrity": "sha512-V0r2Y9scmbDRLCNex/+hYzvp/zyYjvFbHPNgVTKfQvVrb6guiE/fxP+XblDNR011utopbkex2nM4dHNV6GDsng==", + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.4.tgz", + "integrity": "sha512-mDAjwmZdh7LTT6pNleZ05Yt65HC3E+NiQzl672vQG38jIrehtJk/J3mNwIg+vShQPcLF/LV7CMnDW6vjj6sfYQ==", "dev": true, "optional": true, "requires": { @@ -25406,9 +25591,9 @@ } }, "readable-stream": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", - "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", "dev": true, "optional": true, "requires": { @@ -25449,36 +25634,82 @@ } }, "tas-client": { - "version": "0.1.58", - "resolved": "https://registry.npmjs.org/tas-client/-/tas-client-0.1.58.tgz", - "integrity": "sha512-fOWii4wQXuo9Zl0oXgvjBzZWzKc5MmUR6XQWX93WU2c1SaP1plPo/zvXP8kpbZ9fvegFOHdapszYqMTRq/SRtg==", + "version": "0.2.33", + "resolved": "https://registry.npmjs.org/tas-client/-/tas-client-0.2.33.tgz", + "integrity": "sha512-V+uqV66BOQnWxvI6HjDnE4VkInmYZUQ4dgB7gzaDyFyFSK1i1nF/j7DpS9UbQAgV9NaF1XpcyuavnM1qOeiEIg==" + }, + "teex": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/teex/-/teex-1.0.1.tgz", + "integrity": "sha512-eYE6iEI62Ni1H8oIa7KlDU6uQBtqr4Eajni3wX7rpfXD8ysFx8z0+dri+KWEPWpBsxXfxu58x/0jvTVT1ekOSg==", + "dev": true, "requires": { - "axios": "^0.26.1" + "streamx": "^2.12.5" } }, "terser": { - "version": "5.14.2", - "resolved": "https://registry.npmjs.org/terser/-/terser-5.14.2.tgz", - "integrity": "sha512-oL0rGeM/WFQCUd0y2QrWxYnq7tfSuKBiqTjRPWrRgB46WD/kiwHwF8T23z78H6Q6kGCuuHcPB+KULHRdxvVGQA==", + "version": "5.46.0", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.46.0.tgz", + "integrity": "sha512-jTwoImyr/QbOWFFso3YoU3ik0jBBDJ6JTOQiy/J2YxVJdZCc+5u7skhNwiOR3FQIygFqVUPHl7qbbxtjW2K3Qg==", "dev": true, "requires": { - "@jridgewell/source-map": "^0.3.2", - "acorn": "^8.5.0", + "@jridgewell/source-map": "^0.3.3", + "acorn": "^8.15.0", "commander": "^2.20.0", "source-map-support": "~0.5.20" } }, "terser-webpack-plugin": { - "version": "5.3.1", - "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.1.tgz", - "integrity": "sha512-GvlZdT6wPQKbDNW/GDQzZFg/j4vKU96yl2q6mcUkzKOgW4gwf1Z8cZToUCrz31XHlPWH8MVb1r2tFtdDtTGJ7g==", + "version": "5.3.17", + "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.17.tgz", + "integrity": "sha512-YR7PtUp6GMU91BgSJmlaX/rS2lGDbAF7D+Wtq7hRO+MiljNmodYvqslzCFiYVAgW+Qoaaia/QUIP4lGXufjdZw==", "dev": true, "requires": { + "@jridgewell/trace-mapping": "^0.3.25", "jest-worker": "^27.4.5", - "schema-utils": "^3.1.1", - "serialize-javascript": "^6.0.0", - "source-map": "^0.6.1", - "terser": "^5.7.2" + "schema-utils": "^4.3.0", + "terser": "^5.31.1" + }, + "dependencies": { + "ajv": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz", + "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==", + "dev": true, + "requires": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + } + }, + "ajv-keywords": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz", + "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==", + "dev": true, + "requires": { + "fast-deep-equal": "^3.1.3" + } + }, + "json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true + }, + "schema-utils": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.3.tgz", + "integrity": "sha512-eflK8wEtyOE6+hsaRVPxvUKYCpRgzLqDTb8krvAsRIwOGlHoSgYLgBXoubGgLd2fT41/OUYdb48v4k4WWHQurA==", + "dev": true, + "requires": { + "@types/json-schema": "^7.0.9", + "ajv": "^8.9.0", + "ajv-formats": "^2.1.1", + "ajv-keywords": "^5.1.0" + } + } } }, "test-exclude": { @@ -25493,9 +25724,9 @@ }, "dependencies": { "minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", "dev": true, "requires": { "brace-expansion": "^1.1.7" @@ -25503,6 +25734,15 @@ } } }, + "text-decoder": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/text-decoder/-/text-decoder-1.1.0.tgz", + "integrity": "sha512-TmLJNj6UgX8xcUZo4UDStGQtDiTzF7BzWlzn9g7UWrjkpHr5uJTK1ld16wZ3LXb2vb6jH8qU89dW5whuMdXYdw==", + "dev": true, + "requires": { + "b4a": "^1.6.4" + } + }, "text-table": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", @@ -25512,7 +25752,7 @@ "through": { "version": "2.3.8", "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", - "integrity": "sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=", + "integrity": "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==", "dev": true }, "through2": { @@ -25535,16 +25775,10 @@ "xtend": "~4.0.0" } }, - "time-stamp": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/time-stamp/-/time-stamp-1.1.0.tgz", - "integrity": "sha1-dkpaEa9QVhkhsTPztE5hhofg9cM=", - "dev": true - }, "timed-out": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/timed-out/-/timed-out-4.0.1.tgz", - "integrity": "sha1-8y6srFoXW+ol1/q1Zas+2HQe9W8=", + "integrity": "sha512-G7r3AhovYtr5YKOWQkta8RKAPb+J9IsO4uVmzjl8AZwfhs8UcUwTiD6gcJYSgOtzyjvQKrKYn41syHbUWMkafA==", "dev": true }, "timers-browserify": { @@ -25557,12 +25791,9 @@ } }, "tmp": { - "version": "0.0.33", - "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz", - "integrity": "sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==", - "requires": { - "os-tmpdir": "~1.0.2" - } + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.5.tgz", + "integrity": "sha512-voyz6MApa1rQGUxT3E+BK7/ROe8itEx7vD8/HEvt4xwXucvQ5G5oeEiHkmHZJuBO21RpOf+YYm9MOivj709jow==" }, "to-absolute-glob": { "version": "2.0.2", @@ -25581,57 +25812,37 @@ "dev": true }, "to-buffer": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/to-buffer/-/to-buffer-1.1.1.tgz", - "integrity": "sha512-lx9B5iv7msuFYE3dytT+KE5tap+rNYw+K4jVkb9R/asAb+pbBSM17jtunHplhBe6RRJdZx3Pn2Jph24O32mOVg==", - "dev": true - }, - "to-fast-properties": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", - "integrity": "sha1-3F5pjL0HkmW8c+A3doGk5Og/YW4=", - "dev": true - }, - "to-object-path": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/to-object-path/-/to-object-path-0.3.0.tgz", - "integrity": "sha1-KXWIt7Dn4KwI4E5nL4XB9JmeF68=", + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/to-buffer/-/to-buffer-1.2.2.tgz", + "integrity": "sha512-db0E3UJjcFhpDhAF4tLo03oli3pwl3dbnzXOUIlRKrp+ldk/VUxzpWYZENsw2SZiuBjHAk7DfB0VU7NKdpb6sw==", "dev": true, "requires": { - "kind-of": "^3.0.2" + "isarray": "^2.0.5", + "safe-buffer": "^5.2.1", + "typed-array-buffer": "^1.0.3" }, "dependencies": { - "kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "dev": true, - "requires": { - "is-buffer": "^1.1.5" - } + "isarray": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", + "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", + "dev": true + }, + "safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "dev": true } } }, - "to-regex": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/to-regex/-/to-regex-3.0.2.tgz", - "integrity": "sha512-FWtleNAtZ/Ki2qtqej2CXTOayOH9bHDQF+Q48VpWyDXjbYxA4Yz8iDB31zXOBUlOHHKidDbqGVrTUvQMPmBGBw==", - "dev": true, - "requires": { - "define-property": "^2.0.2", - "extend-shallow": "^3.0.2", - "regex-not": "^1.0.2", - "safe-regex": "^1.1.0" - } - }, "to-regex-range": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-2.1.1.tgz", - "integrity": "sha1-fIDBe53+vlmeJzZ+DU3VWQFB2zg=", + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", "dev": true, "requires": { - "is-number": "^3.0.0", - "repeat-string": "^1.6.1" + "is-number": "^7.0.0" } }, "to-through": { @@ -25649,21 +25860,22 @@ "integrity": "sha512-gduQwd1rOdDMGxFG1gEvhV88Oirdo2p+KjoYFU7k2g+i7n6AFFbDQ5kMPUsW0pNbfQsB/cwXvT1i4Bue0s9g5g==", "dev": true }, - "traverse": { - "version": "0.3.9", - "resolved": "https://registry.npmjs.org/traverse/-/traverse-0.3.9.tgz", - "integrity": "sha1-cXuPIgzAu3tE5AUUwisui7xw2Lk=", - "dev": true - }, "trim-repeated": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/trim-repeated/-/trim-repeated-1.0.0.tgz", - "integrity": "sha1-42RqLqTokTEr9+rObPsFOAvAHCE=", + "integrity": "sha512-pkonvlKk8/ZuR0D5tLW8ljt5I8kmxp2XKymhepUeOdCEfKpZaktSArkLHZt76OB1ZvO9bssUsDty4SWhLvZpLg==", "dev": true, "requires": { "escape-string-regexp": "^1.0.2" } }, + "ts-api-utils": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.3.0.tgz", + "integrity": "sha512-UQMIo7pb8WRomKR1/+MFVLTroIvDVtMX3K6OUir8ynLyzB8Jeriont2bTAtmNPa1ekAgN7YPDyf6V+ygrdU+eQ==", + "dev": true, + "requires": {} + }, "ts-loader": { "version": "9.2.8", "resolved": "https://registry.npmjs.org/ts-loader/-/ts-loader-9.2.8.tgz", @@ -25685,15 +25897,6 @@ "color-convert": "^2.0.1" } }, - "braces": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", - "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", - "dev": true, - "requires": { - "fill-range": "^7.0.1" - } - }, "chalk": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", @@ -25719,46 +25922,12 @@ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "dev": true }, - "fill-range": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", - "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", - "dev": true, - "requires": { - "to-regex-range": "^5.0.1" - } - }, "has-flag": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", "dev": true }, - "is-number": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", - "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", - "dev": true - }, - "micromatch": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz", - "integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==", - "dev": true, - "requires": { - "braces": "^3.0.2", - "picomatch": "^2.3.1" - } - }, - "semver": { - "version": "7.3.5", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.5.tgz", - "integrity": "sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==", - "dev": true, - "requires": { - "lru-cache": "^6.0.0" - } - }, "supports-color": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", @@ -25767,15 +25936,6 @@ "requires": { "has-flag": "^4.0.0" } - }, - "to-regex-range": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", - "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", - "dev": true, - "requires": { - "is-number": "^7.0.0" - } } } }, @@ -25810,13 +25970,13 @@ } }, "tsconfig-paths": { - "version": "3.14.1", - "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.14.1.tgz", - "integrity": "sha512-fxDhWnFSLt3VuTwtvJt5fpwxBHg5AdKWMsgcPOOIilyjymcYVZoCQF8fvFRezCNfblEXmi+PcM1eYHeOAgXCOQ==", + "version": "3.15.0", + "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.15.0.tgz", + "integrity": "sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg==", "dev": true, "requires": { "@types/json5": "^0.0.29", - "json5": "^1.0.1", + "json5": "^1.0.2", "minimist": "^1.2.6", "strip-bom": "^3.0.0" }, @@ -25905,15 +26065,6 @@ "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.10.0.tgz", "integrity": "sha512-qOebF53frne81cf0S9B41ByenJ3/IuH8yJKngAX35CmiZySA0khhkovshKK+jGCaMnVomla7gVlIcc3EvKPbTQ==" }, - "tsutils": { - "version": "3.21.0", - "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-3.21.0.tgz", - "integrity": "sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==", - "dev": true, - "requires": { - "tslib": "^1.8.1" - } - }, "tty-browserify": { "version": "0.0.0", "resolved": "https://registry.npmjs.org/tty-browserify/-/tty-browserify-0.0.0.tgz", @@ -25929,18 +26080,21 @@ "tunnel-agent": { "version": "0.6.0", "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", - "integrity": "sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0=", + "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", "dev": true, "optional": true, "requires": { "safe-buffer": "^5.0.1" } }, - "type": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/type/-/type-1.0.1.tgz", - "integrity": "sha512-MAM5dBMJCJNKs9E7JXo4CXRAansRfG0nlJxW7Wf6GZzSOvH31zClSaHdIMWLehe/EGMBkqeC55rrkaOr5Oo7Nw==", - "dev": true + "type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "requires": { + "prelude-ls": "^1.2.1" + } }, "type-detect": { "version": "4.0.8", @@ -25954,34 +26108,69 @@ "integrity": "sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==", "dev": true }, + "typed-array-buffer": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz", + "integrity": "sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==", + "dev": true, + "requires": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-typed-array": "^1.1.14" + } + }, + "typed-array-byte-length": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/typed-array-byte-length/-/typed-array-byte-length-1.0.1.tgz", + "integrity": "sha512-3iMJ9q0ao7WE9tWcaYKIptkNBuOIcZCCT0d4MRvuuH88fEoEH62IuQe0OtraD3ebQEoTRk8XCBoknUNc1Y67pw==", + "dev": true, + "requires": { + "call-bind": "^1.0.7", + "for-each": "^0.3.3", + "gopd": "^1.0.1", + "has-proto": "^1.0.3", + "is-typed-array": "^1.1.13" + } + }, + "typed-array-byte-offset": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/typed-array-byte-offset/-/typed-array-byte-offset-1.0.2.tgz", + "integrity": "sha512-Ous0vodHa56FviZucS2E63zkgtgrACj7omjwd/8lTEMEPFFyjfixMZ1ZXenpgCFBBt4EC1J2XsyVS2gkG0eTFA==", + "dev": true, + "requires": { + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.7", + "for-each": "^0.3.3", + "gopd": "^1.0.1", + "has-proto": "^1.0.3", + "is-typed-array": "^1.1.13" + } + }, + "typed-array-length": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.6.tgz", + "integrity": "sha512-/OxDN6OtAk5KBpGb28T+HZc2M+ADtvRxXrKKbUwtsLgdoxgX13hyy7ek6bFRl5+aBs2yZzB0c4CnQfAtVypW/g==", + "dev": true, + "requires": { + "call-bind": "^1.0.7", + "for-each": "^0.3.3", + "gopd": "^1.0.1", + "has-proto": "^1.0.3", + "is-typed-array": "^1.1.13", + "possible-typed-array-names": "^1.0.0" + } + }, "typed-rest-client": { - "version": "1.8.5", - "resolved": "https://registry.npmjs.org/typed-rest-client/-/typed-rest-client-1.8.5.tgz", - "integrity": "sha512-952/Aegu3lTqUAI1anbDLbewojnF/gh8at9iy1CIrfS1h/+MtNjB1Y9z6ZF5n2kZd+97em56lZ9uu7Zz3y/pwg==", + "version": "1.8.11", + "resolved": "https://registry.npmjs.org/typed-rest-client/-/typed-rest-client-1.8.11.tgz", + "integrity": "sha512-5UvfMpd1oelmUPRbbaVnq+rHP7ng2cE4qoQkQeAqxRL6PklkxsM0g32/HL0yfvruK6ojQ5x8EE+HF4YV6DtuCA==", "dev": true, "requires": { "qs": "^6.9.1", "tunnel": "0.0.6", "underscore": "^1.12.1" - }, - "dependencies": { - "qs": { - "version": "6.11.0", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz", - "integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==", - "dev": true, - "requires": { - "side-channel": "^1.0.4" - } - } } }, - "typedarray": { - "version": "0.0.6", - "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", - "integrity": "sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=", - "dev": true - }, "typedarray-to-buffer": { "version": "3.1.5", "resolved": "https://registry.npmjs.org/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz", @@ -26003,9 +26192,9 @@ } }, "typescript": { - "version": "4.5.5", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.5.5.tgz", - "integrity": "sha512-TCTIul70LyWe6IJWT8QSYeA54WQe8EjQFU4wY52Fasj5UKx88LNYKCgBEHcOMOrFF1rKGbD8v/xcNWVUq9SymA==", + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.2.2.tgz", + "integrity": "sha512-mI4WrpHsbCIcwT9cF4FZvr80QUeKvsUsUvKDoR+X/7XHQH98xYD8YHZg7ANtz2GtZt/CBq2QJ0thkGJMHfqc1w==", "dev": true }, "uc.micro": { @@ -26020,21 +26209,21 @@ "integrity": "sha512-mliiCSrsE29aNBI7O9W5gGv6WmA9kBR8PtTt6Apaxns076IRdYrrtFhXHEWMj5CSum3U7cv7/pi4xmi4XsIOqg==" }, "unbox-primitive": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.1.tgz", - "integrity": "sha512-tZU/3NqK3dA5gpE1KtyiJUrEB0lxnGkMFHptJ7q6ewdZ8s12QrODwNbhIJStmJkd1QDXa1NRA8aF2A1zk/Ypyw==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.2.tgz", + "integrity": "sha512-61pPlCD9h51VoreyJ0BReideM3MDKMKnh6+V9L08331ipq6Q8OFXZYiqP6n/tbHx4s5I9uRhcye6BrbkizkBDw==", "dev": true, "requires": { - "function-bind": "^1.1.1", - "has-bigints": "^1.0.1", - "has-symbols": "^1.0.2", + "call-bind": "^1.0.2", + "has-bigints": "^1.0.2", + "has-symbols": "^1.0.3", "which-boxed-primitive": "^1.0.2" } }, "unbzip2-stream": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/unbzip2-stream/-/unbzip2-stream-1.3.3.tgz", - "integrity": "sha512-fUlAF7U9Ah1Q6EieQ4x4zLNejrRvDWUYmxXUpN3uziFYCHapjWFaCAnreY9bGgxzaMCFAPPpYNng57CypwJVhg==", + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/unbzip2-stream/-/unbzip2-stream-1.4.3.tgz", + "integrity": "sha512-mlExGW4w71ebDJviH16lQLtZS32VKqsSfk80GCfUlwT/4/hNRFsoscrF/c++9xinkMzECL1uL9DDwXqFWkruPg==", "dev": true, "requires": { "buffer": "^5.2.1", @@ -26048,32 +26237,44 @@ "dev": true }, "underscore": { - "version": "1.13.1", - "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.13.1.tgz", - "integrity": "sha512-hzSoAVtJF+3ZtiFX0VgfFPHEDRm7Y/QPjGyNo4TVdnDTdft3tr8hEkD25a1jC+TjTuE7tkHGKkhwCgs9dgBB2g==", + "version": "1.13.6", + "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.13.6.tgz", + "integrity": "sha512-+A5Sja4HP1M08MaXya7p5LvjuM7K6q/2EaC0+iovj/wOcMsTzMvDFbasi/oSapiwOlt252IqsKqPjCl7huKS0A==", "dev": true }, "undertaker": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/undertaker/-/undertaker-1.2.1.tgz", - "integrity": "sha512-71WxIzDkgYk9ZS+spIB8iZXchFhAdEo2YU8xYqBYJ39DIUIqziK78ftm26eecoIY49X0J2MLhG4hr18Yp6/CMA==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/undertaker/-/undertaker-2.0.0.tgz", + "integrity": "sha512-tO/bf30wBbTsJ7go80j0RzA2rcwX6o7XPBpeFcb+jzoeb4pfMM2zUeSDIkY1AWqeZabWxaQZ/h8N9t35QKDLPQ==", "dev": true, "requires": { - "arr-flatten": "^1.0.1", - "arr-map": "^2.0.0", - "bach": "^1.0.0", - "collection-map": "^1.0.0", - "es6-weak-map": "^2.0.1", - "last-run": "^1.1.0", - "object.defaults": "^1.0.0", - "object.reduce": "^1.0.0", - "undertaker-registry": "^1.0.0" + "bach": "^2.0.1", + "fast-levenshtein": "^3.0.0", + "last-run": "^2.0.0", + "undertaker-registry": "^2.0.0" + }, + "dependencies": { + "fast-levenshtein": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-3.0.0.tgz", + "integrity": "sha512-hKKNajm46uNmTlhHSyZkmToAc56uZJwYq7yrciZjqOxnlfQwERDQJmHPUp7m1m9wx8vgOe8IaCKZ5Kv2k1DdCQ==", + "dev": true, + "requires": { + "fastest-levenshtein": "^1.0.7" + } + } } }, - "undertaker-registry": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/undertaker-registry/-/undertaker-registry-1.0.1.tgz", - "integrity": "sha1-XkvaMI5KiirlhPm5pDWaSZglzFA=", + "undertaker-registry": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/undertaker-registry/-/undertaker-registry-2.0.0.tgz", + "integrity": "sha512-+hhVICbnp+rlzZMgxXenpvTxpuvA67Bfgtt+O9WOE5jo7w/dyiF1VmoZVIHvP2EkUjsyKyTwYKlLhA+j47m1Ew==", + "dev": true + }, + "undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", "dev": true }, "unicode": { @@ -26081,18 +26282,6 @@ "resolved": "https://registry.npmjs.org/unicode/-/unicode-14.0.0.tgz", "integrity": "sha512-BjinxTXkbm9Jomp/YBTMGusr4fxIG67fNGShHIRAL16Ur2GJTq2xvLi+sxuiJmInCmwqqev2BCFKyvbfp/yAkg==" }, - "union-value": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/union-value/-/union-value-1.0.1.tgz", - "integrity": "sha512-tJfXmxMeWYnczCVs7XAEvIV7ieppALdyepWMkHkwciRpZraG/xwT+s2JN8+pr1+8jCRf80FFzvr+MpQeeoF4Xg==", - "dev": true, - "requires": { - "arr-union": "^3.1.0", - "get-value": "^2.0.6", - "is-extendable": "^0.1.1", - "set-value": "^2.0.1" - } - }, "unique-stream": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/unique-stream/-/unique-stream-2.3.1.tgz", @@ -26103,83 +26292,16 @@ "through2-filter": "^3.0.0" } }, - "unset-value": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/unset-value/-/unset-value-1.0.0.tgz", - "integrity": "sha1-g3aHP30jNRef+x5vw6jtDfyKtVk=", + "update-browserslist-db": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", "dev": true, "requires": { - "has-value": "^0.3.1", - "isobject": "^3.0.0" - }, - "dependencies": { - "has-value": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/has-value/-/has-value-0.3.1.tgz", - "integrity": "sha1-ex9YutpiyoJ+wKIHgCVlSEWZXh8=", - "dev": true, - "requires": { - "get-value": "^2.0.3", - "has-values": "^0.1.4", - "isobject": "^2.0.0" - }, - "dependencies": { - "isobject": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-2.1.0.tgz", - "integrity": "sha1-8GVWEJaj8dou9GJy+BXIQNh+DIk=", - "dev": true, - "requires": { - "isarray": "1.0.0" - } - } - } - }, - "has-values": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/has-values/-/has-values-0.1.4.tgz", - "integrity": "sha1-bWHeldkd/Km5oCCJrThL/49it3E=", - "dev": true - } - } - }, - "untildify": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/untildify/-/untildify-4.0.0.tgz", - "integrity": "sha512-KK8xQ1mkzZeg9inewmFVDNkg3l5LUhoq9kN6iWYB/CC9YMG8HA+c1Q8HwDe6dEX7kErrEVNVBO3fWsVq5iDgtw==" - }, - "unzipper": { - "version": "0.10.11", - "resolved": "https://registry.npmjs.org/unzipper/-/unzipper-0.10.11.tgz", - "integrity": "sha512-+BrAq2oFqWod5IESRjL3S8baohbevGcVA+teAIOYWM3pDVdseogqbzhhvvmiyQrUNKFUnDMtELW3X8ykbyDCJw==", - "dev": true, - "requires": { - "big-integer": "^1.6.17", - "binary": "~0.3.0", - "bluebird": "~3.4.1", - "buffer-indexof-polyfill": "~1.0.0", - "duplexer2": "~0.1.4", - "fstream": "^1.0.12", - "graceful-fs": "^4.2.2", - "listenercount": "~1.0.1", - "readable-stream": "~2.3.6", - "setimmediate": "~1.0.4" - }, - "dependencies": { - "bluebird": { - "version": "3.4.7", - "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.4.7.tgz", - "integrity": "sha1-9y12C+Cbf3bQjtj66Ysomo0F+rM=", - "dev": true - } + "escalade": "^3.2.0", + "picocolors": "^1.1.1" } }, - "upath": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/upath/-/upath-1.2.0.tgz", - "integrity": "sha512-aZwGpamFO61g3OlfT7OQCHqhGnW43ieH9WZeP7QxN/G/jS4jfqUkZxoryvJgVPEcrl5NL/ggHsSmLMHuH64Lhg==", - "dev": true - }, "uri-js": { "version": "4.2.2", "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.2.2.tgz", @@ -26189,12 +26311,6 @@ "punycode": "^2.1.0" } }, - "urix": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/urix/-/urix-0.1.0.tgz", - "integrity": "sha1-2pN/emLiH+wf0Y1Js1wpNQZ6bHI=", - "dev": true - }, "url": { "version": "0.11.0", "resolved": "https://registry.npmjs.org/url/-/url-0.11.0.tgz", @@ -26222,7 +26338,7 @@ "url-parse-lax": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/url-parse-lax/-/url-parse-lax-3.0.0.tgz", - "integrity": "sha1-FrXK/Afb42dsGxmZF3gj1lA6yww=", + "integrity": "sha512-NjFKA0DidqPa5ciFcSrXnAltTtzz84ogy+NebPvfEgAck0+TNg4UJ4IN+fB7zRZfbgUf0syOo9MDxFkDSMuFaQ==", "dev": true, "requires": { "prepend-http": "^2.0.0" @@ -26231,13 +26347,7 @@ "url-to-options": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/url-to-options/-/url-to-options-1.0.1.tgz", - "integrity": "sha1-FQWgOiiaSMvXpDTvuu7FBV9WM6k=", - "dev": true - }, - "use": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/use/-/use-3.1.1.tgz", - "integrity": "sha512-cwESVXlO3url9YWlFW/TA9cshCEhtu7IKJ/p5soJ/gGpj7vbvFrAY/eIioQ6Dw23KjZhYgiIo8HOs1nQ2vr/oQ==", + "integrity": "sha512-0kQLIzG4fdk/G5NONku64rSH/x32NOA39LVQqlK8Le6lvTF6GGRJpqaQFGgU+CLwySIqBSMdwYM0sYcW9f6P4A==", "dev": true }, "util": { @@ -26264,15 +26374,9 @@ "dev": true }, "uuid": { - "version": "8.3.2", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", - "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", - "dev": true - }, - "v8-compile-cache": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/v8-compile-cache/-/v8-compile-cache-2.3.0.tgz", - "integrity": "sha512-l8lCEmLcLYZh4nbunNZvQCJc5pv7+RCwa8q/LdUx8u7lsWvPDKmpodJAJNwkAhJC//dFY48KuIEmjtd4RViDrA==", + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-14.0.0.tgz", + "integrity": "sha512-Qo+uWgilfSmAhXCMav1uYFynlQO7fMFiMVZsQqZRMIXp0O7rR7qjkj+cPvBHLgBqi960QCoo/PH2/6ZtVqKvrg==", "dev": true }, "v8-compile-cache-lib": { @@ -26281,15 +26385,11 @@ "integrity": "sha512-mpSYqfsFvASnSn5qMiwrr4VKfumbPyONLCOPmsR3A6pTY/r0+tSaVbgPWSAIuzbk3lCTa+FForeTiO+wBQGkjA==", "dev": true }, - "validate-npm-package-license": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", - "integrity": "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==", - "dev": true, - "requires": { - "spdx-correct": "^3.0.0", - "spdx-expression-parse": "^3.0.0" - } + "v8flags": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/v8flags/-/v8flags-4.0.1.tgz", + "integrity": "sha512-fcRLaS4H/hrZk9hYwbdRM35D0U8IYMfEClhXxCivOojl+yTRAZH3Zy2sSy6qVCiGbV9YAtPssP6jaChqC9vPCg==", + "dev": true }, "value-or-function": { "version": "3.0.0", @@ -26311,6 +26411,69 @@ "replace-ext": "^1.0.0" } }, + "vinyl-contents": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/vinyl-contents/-/vinyl-contents-2.0.0.tgz", + "integrity": "sha512-cHq6NnGyi2pZ7xwdHSW1v4Jfnho4TEGtxZHw01cmnc8+i7jgR6bRnED/LbrKan/Q7CvVLbnvA5OepnhbpjBZ5Q==", + "dev": true, + "requires": { + "bl": "^5.0.0", + "vinyl": "^3.0.0" + }, + "dependencies": { + "bl": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-5.1.0.tgz", + "integrity": "sha512-tv1ZJHLfTDnXE6tMHv73YgSJaWR2AFuPwMntBe7XL/GBFHnT0CLnsHMogfk5+GzCDC5ZWarSCYaIGATZt9dNsQ==", + "dev": true, + "requires": { + "buffer": "^6.0.3", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, + "buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "dev": true, + "requires": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + }, + "readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "dev": true, + "requires": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + } + }, + "replace-ext": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/replace-ext/-/replace-ext-2.0.0.tgz", + "integrity": "sha512-UszKE5KVK6JvyD92nzMn9cDapSk6w/CaFZ96CnmDMUqH9oowfxF/ZjRITD25H4DnOQClLA4/j7jLGXXLVKxAug==", + "dev": true + }, + "vinyl": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/vinyl/-/vinyl-3.0.0.tgz", + "integrity": "sha512-rC2VRfAVVCGEgjnxHUnpIVh3AGuk62rP3tqVrn+yab0YH7UULisC085+NYH+mnqf3Wx4SpSi1RQMwudL89N03g==", + "dev": true, + "requires": { + "clone": "^2.1.2", + "clone-stats": "^1.0.0", + "remove-trailing-separator": "^1.1.0", + "replace-ext": "^2.0.0", + "teex": "^1.0.1" + } + } + } + }, "vinyl-fs": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/vinyl-fs/-/vinyl-fs-3.0.3.tgz", @@ -26368,103 +26531,70 @@ "integrity": "sha512-2ham8XPWTONajOR0ohOKOHXkm3+gaBmGut3SRuu75xLd/RRaY6vqgh8NBYYk7+RW3u5AtzPQZG8F10LHkl0lAQ==", "dev": true }, - "vscode-debugadapter": { - "version": "1.35.0", - "resolved": "https://registry.npmjs.org/vscode-debugadapter/-/vscode-debugadapter-1.35.0.tgz", - "integrity": "sha512-Au90Iowj6TuD5uDMaTnxOjl/9hQN0Yoky1TV1Cjjr7jPdxTQpALBRW09Y2LzkIXUVICXlAqxWL9zL8BpzI30jg==", - "requires": { - "mkdirp": "^0.5.1", - "vscode-debugprotocol": "1.35.0" - } - }, - "vscode-debugadapter-testsupport": { - "version": "1.35.0", - "resolved": "https://registry.npmjs.org/vscode-debugadapter-testsupport/-/vscode-debugadapter-testsupport-1.35.0.tgz", - "integrity": "sha512-4emLt6JOk4iKqC2aWNJupOtrK6JwYAZ6KppqvKASN6B1s063VoqI18QhUB1CeoKwNaN1LIG3VPv2xM8HKOjyDA==", - "dev": true, - "requires": { - "vscode-debugprotocol": "1.35.0" - } - }, "vscode-debugprotocol": { "version": "1.35.0", "resolved": "https://registry.npmjs.org/vscode-debugprotocol/-/vscode-debugprotocol-1.35.0.tgz", "integrity": "sha512-+OMm11R1bGYbpIJ5eQIkwoDGFF4GvBz3Ztl6/VM+/RNNb2Gjk2c0Ku+oMmfhlTmTlPCpgHBsH4JqVCbUYhu5bA==" }, "vscode-jsonrpc": { - "version": "8.0.2-next.1", - "resolved": "https://registry.npmjs.org/vscode-jsonrpc/-/vscode-jsonrpc-8.0.2-next.1.tgz", - "integrity": "sha512-sbbvGSWja7NVBLHPGawtgezc8DHYJaP4qfr/AaJiyDapWcSFtHyPtm18+LnYMLTmB7bhOUW/lf5PeeuLpP6bKA==" + "version": "9.0.0-next.5", + "resolved": "https://registry.npmjs.org/vscode-jsonrpc/-/vscode-jsonrpc-9.0.0-next.5.tgz", + "integrity": "sha512-Sl/8RAJtfF/2x/TPBVRuhzRAcqYR/QDjEjNqMcoKFfqsxfVUPzikupRDQYB77Gkbt1RrW43sSuZ5uLtNAcikQQ==" }, "vscode-languageclient": { - "version": "8.0.2-next.5", - "resolved": "https://registry.npmjs.org/vscode-languageclient/-/vscode-languageclient-8.0.2-next.5.tgz", - "integrity": "sha512-g87RJLHz0XlRyk6DOTbAk4JHcj8CKggXy4JiFL7OlhETkcYzTOR8d+Qdb4GqZr37PDs1Cl21omtTNK5LyR/RQg==", + "version": "10.0.0-next.12", + "resolved": "https://registry.npmjs.org/vscode-languageclient/-/vscode-languageclient-10.0.0-next.12.tgz", + "integrity": "sha512-q7cVYCcYiv+a+fJYCbjMMScOGBnX162IBeUMFg31mvnN7RHKx5/CwKaCz+r+RciJrRXMqS8y8qpEVGgeIPnbxg==", "requires": { - "minimatch": "^3.0.4", - "semver": "^7.3.5", - "vscode-languageserver-protocol": "3.17.2-next.6" + "minimatch": "^9.0.3", + "semver": "^7.6.0", + "vscode-languageserver-protocol": "3.17.6-next.10" }, "dependencies": { - "minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "brace-expansion": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.3.tgz", + "integrity": "sha512-MCV/fYJEbqx68aE58kv2cA/kiky1G8vux3OR6/jbS+jIMe/6fJWa0DTzJU7dqijOWYwHi1t29FlfYI9uytqlpA==", "requires": { - "brace-expansion": "^1.1.7" + "balanced-match": "^1.0.0" } }, - "semver": { - "version": "7.3.7", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.7.tgz", - "integrity": "sha512-QlYTucUYOews+WeEujDoEGziz4K6c47V/Bd+LjSSYcA94p+DmINdf7ncaUinThfvZyu13lN9OY1XDxt8C0Tw0g==", + "minimatch": { + "version": "9.0.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", + "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", "requires": { - "lru-cache": "^6.0.0" + "brace-expansion": "^2.0.2" } } } }, - "vscode-languageserver": { - "version": "8.0.2-next.5", - "resolved": "https://registry.npmjs.org/vscode-languageserver/-/vscode-languageserver-8.0.2-next.5.tgz", - "integrity": "sha512-2ZDb7O/4atS9mJKufPPz637z+51kCyZfgnobFW5eSrUdS3c0UB/nMS4Ng1EavYTX84GVaVMKCrmP0f2ceLmR0A==", - "requires": { - "vscode-languageserver-protocol": "3.17.2-next.6" - } - }, "vscode-languageserver-protocol": { - "version": "3.17.2-next.6", - "resolved": "https://registry.npmjs.org/vscode-languageserver-protocol/-/vscode-languageserver-protocol-3.17.2-next.6.tgz", - "integrity": "sha512-WtsebNOOkWyNn4oFYoAMPC8Q/ZDoJ/K7Ja53OzTixiitvrl/RpXZETrtzH79R8P5kqCyx6VFBPb6KQILJfkDkA==", + "version": "3.17.6-next.10", + "resolved": "https://registry.npmjs.org/vscode-languageserver-protocol/-/vscode-languageserver-protocol-3.17.6-next.10.tgz", + "integrity": "sha512-KOrrWn4NVC5jnFC5N6y/fyNKtx8rVYr67lhL/Z0P4ZBAN27aBsCnLBWAMIkYyJ1K8EZaE5r7gqdxrS9JPB6LIg==", "requires": { - "vscode-jsonrpc": "8.0.2-next.1", - "vscode-languageserver-types": "3.17.2-next.2" - }, - "dependencies": { - "vscode-languageserver-types": { - "version": "3.17.2-next.2", - "resolved": "https://registry.npmjs.org/vscode-languageserver-types/-/vscode-languageserver-types-3.17.2-next.2.tgz", - "integrity": "sha512-TiAkLABgqkVWdAlC3XlOfdhdjIAdVU4YntPUm9kKGbXr+MGwpVnKz2KZMNBcvG0CFx8Hi8qliL0iq+ndPB720w==" - } + "vscode-jsonrpc": "9.0.0-next.5", + "vscode-languageserver-types": "3.17.6-next.5" } }, + "vscode-languageserver-types": { + "version": "3.17.6-next.5", + "resolved": "https://registry.npmjs.org/vscode-languageserver-types/-/vscode-languageserver-types-3.17.6-next.5.tgz", + "integrity": "sha512-QFmf3Yl1tCgUQfA77N9Me/LXldJXkIVypQbty2rJ1DNHQkC+iwvm4Z2tXg9czSwlhvv0pD4pbF5mT7WhAglolw==" + }, "vscode-tas-client": { - "version": "0.1.63", - "resolved": "https://registry.npmjs.org/vscode-tas-client/-/vscode-tas-client-0.1.63.tgz", - "integrity": "sha512-TY5TPyibzi6rNmuUB7eRVqpzLzNfQYrrIl/0/F8ukrrbzOrKVvS31hM3urE+tbaVrnT+TMYXL16GhX57vEowhA==", + "version": "0.1.84", + "resolved": "https://registry.npmjs.org/vscode-tas-client/-/vscode-tas-client-0.1.84.tgz", + "integrity": "sha512-rUTrUopV+70hvx1hW5ebdw1nd6djxubkLvVxjGdyD/r5v/wcVF41LIfiAtbm5qLZDtQdsMH1IaCuDoluoIa88w==", "requires": { - "tas-client": "0.1.58" + "tas-client": "0.2.33" } }, - "vscode-uri": { - "version": "3.0.6", - "resolved": "https://registry.npmjs.org/vscode-uri/-/vscode-uri-3.0.6.tgz", - "integrity": "sha512-fmL7V1eiDBFRRnu+gfRWTzyPpNIHJTc4mWnFkwBUmO9U3KPgJAmTx7oxi2bl/Rh6HLdU7+4C9wlj0k2E4AdKFQ==" - }, "watchpack": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.0.tgz", - "integrity": "sha512-Lcvm7MGST/4fup+ifyKi2hjyIAwcdI4HRgtvTpIUxBRhB+RFtUh8XtDOxUfctVCnhVi+QQj49i91OyvzkJl6cg==", + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.5.1.tgz", + "integrity": "sha512-Zn5uXdcFNIA1+1Ei5McRd+iRzfhENPCe7LeABkJtNulSxjma+l7ltNx55BWZkRlwRnpOgHqxnjyaDgJnNXnqzg==", "dev": true, "requires": { "glob-to-regexp": "^0.4.1", @@ -26472,35 +26602,77 @@ } }, "webpack": { - "version": "5.76.0", - "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.76.0.tgz", - "integrity": "sha512-l5sOdYBDunyf72HW8dF23rFtWq/7Zgvt/9ftMof71E/yUb1YLOBmTgA2K4vQthB3kotMrSj609txVE0dnr2fjA==", - "dev": true, - "requires": { - "@types/eslint-scope": "^3.7.3", - "@types/estree": "^0.0.51", - "@webassemblyjs/ast": "1.11.1", - "@webassemblyjs/wasm-edit": "1.11.1", - "@webassemblyjs/wasm-parser": "1.11.1", - "acorn": "^8.7.1", - "acorn-import-assertions": "^1.7.6", - "browserslist": "^4.14.5", + "version": "5.105.0", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.105.0.tgz", + "integrity": "sha512-gX/dMkRQc7QOMzgTe6KsYFM7DxeIONQSui1s0n/0xht36HvrgbxtM1xBlgx596NbpHuQU8P7QpKwrZYwUX48nw==", + "dev": true, + "requires": { + "@types/eslint-scope": "^3.7.7", + "@types/estree": "^1.0.8", + "@types/json-schema": "^7.0.15", + "@webassemblyjs/ast": "^1.14.1", + "@webassemblyjs/wasm-edit": "^1.14.1", + "@webassemblyjs/wasm-parser": "^1.14.1", + "acorn": "^8.15.0", + "acorn-import-phases": "^1.0.3", + "browserslist": "^4.28.1", "chrome-trace-event": "^1.0.2", - "enhanced-resolve": "^5.10.0", - "es-module-lexer": "^0.9.0", + "enhanced-resolve": "^5.19.0", + "es-module-lexer": "^2.0.0", "eslint-scope": "5.1.1", "events": "^3.2.0", "glob-to-regexp": "^0.4.1", - "graceful-fs": "^4.2.9", + "graceful-fs": "^4.2.11", "json-parse-even-better-errors": "^2.3.1", - "loader-runner": "^4.2.0", + "loader-runner": "^4.3.1", "mime-types": "^2.1.27", "neo-async": "^2.6.2", - "schema-utils": "^3.1.0", - "tapable": "^2.1.1", - "terser-webpack-plugin": "^5.1.3", - "watchpack": "^2.4.0", - "webpack-sources": "^3.2.3" + "schema-utils": "^4.3.3", + "tapable": "^2.3.0", + "terser-webpack-plugin": "^5.3.16", + "watchpack": "^2.5.1", + "webpack-sources": "^3.3.3" + }, + "dependencies": { + "ajv": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz", + "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==", + "dev": true, + "requires": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + } + }, + "ajv-keywords": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz", + "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==", + "dev": true, + "requires": { + "fast-deep-equal": "^3.1.3" + } + }, + "json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true + }, + "schema-utils": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.3.tgz", + "integrity": "sha512-eflK8wEtyOE6+hsaRVPxvUKYCpRgzLqDTb8krvAsRIwOGlHoSgYLgBXoubGgLd2fT41/OUYdb48v4k4WWHQurA==", + "dev": true, + "requires": { + "@types/json-schema": "^7.0.9", + "ajv": "^8.9.0", + "ajv-formats": "^2.1.1", + "ajv-keywords": "^5.1.0" + } + } } }, "webpack-bundle-analyzer": { @@ -26650,9 +26822,9 @@ "requires": {} }, "webpack-sources": { - "version": "3.2.3", - "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.2.3.tgz", - "integrity": "sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w==", + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.3.3.tgz", + "integrity": "sha512-yd1RBzSGanHkitROoPFd6qsrxt+oFhg/129YzheDGqeustzX0vTZJZsSsQjVQC4yzBQ56K55XU8gaNCtIzOnTg==", "dev": true }, "which": { @@ -26674,53 +26846,21 @@ "is-number-object": "^1.0.4", "is-string": "^1.0.5", "is-symbol": "^1.0.3" - }, - "dependencies": { - "is-boolean-object": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.1.1.tgz", - "integrity": "sha512-bXdQWkECBUIAcCkeH1unwJLIpZYaa5VvuygSyS/c2lf719mTKZDU5UdDRlpd01UjADgmW8RfqaP+mRaVPdr/Ng==", - "dev": true, - "requires": { - "call-bind": "^1.0.2" - } - }, - "is-number-object": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.0.5.tgz", - "integrity": "sha512-RU0lI/n95pMoUKu9v1BZP5MBcZuNSVJkMkAG2dJqC4z2GlkGUNeH68SuHuBKBD/XFe+LHZ+f9BKkLET60Niedw==", - "dev": true - } } }, - "which-module": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/which-module/-/which-module-1.0.0.tgz", - "integrity": "sha1-u6Y8qGGUiZT/MHc2CJ47lgJsKk8=", - "dev": true - }, "which-typed-array": { - "version": "1.1.7", - "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.7.tgz", - "integrity": "sha512-vjxaB4nfDqwKI0ws7wZpxIlde1XrLX5uB0ZjpfshgmapJMD7jJWhZI+yToJTqaFByF0eNBcYxbjmCzoRP7CfEw==", - "dev": true, - "requires": { - "available-typed-arrays": "^1.0.5", - "call-bind": "^1.0.2", - "es-abstract": "^1.18.5", - "foreach": "^2.0.5", - "has-tostringtag": "^1.0.0", - "is-typed-array": "^1.1.7" - } - }, - "wide-align": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.3.tgz", - "integrity": "sha512-QGkOQc8XL6Bt5PwnsExKBPuMKBxnGxWWW3fU55Xt4feHozMUhdUMaBCk290qpm/wG5u/RSKzwdAC4i51YigihA==", + "version": "1.1.19", + "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.19.tgz", + "integrity": "sha512-rEvr90Bck4WZt9HHFC4DJMsjvu7x+r6bImz0/BrbWb7A2djJ8hnZMrWnHo9F8ssv0OMErasDhftrfROTyqSDrw==", "dev": true, - "optional": true, "requires": { - "string-width": "^1.0.2 || 2" + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "for-each": "^0.3.5", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2" } }, "wildcard": { @@ -26749,42 +26889,93 @@ "wipe-node-cache": "^2.1.0" } }, - "word-wrap": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz", - "integrity": "sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==", - "dev": true + "worker-loader": { + "version": "3.0.8", + "resolved": "https://registry.npmjs.org/worker-loader/-/worker-loader-3.0.8.tgz", + "integrity": "sha512-XQyQkIFeRVC7f7uRhFdNMe/iJOdO6zxAaR3EWbDp45v3mDhrTi+++oswKNxShUNjPC/1xUp5DB29YKLhFo129g==", + "dev": true, + "requires": { + "loader-utils": "^2.0.0", + "schema-utils": "^3.0.0" + } }, "workerpool": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/workerpool/-/workerpool-6.2.0.tgz", - "integrity": "sha512-Rsk5qQHJ9eowMH28Jwhe8HEbmdYDX4lwoMWshiCXugjtHqMD9ZbiqSDLxcsfdqsETPzVUtX5s1Z5kStiIM6l4A==", + "version": "9.3.4", + "resolved": "https://registry.npmjs.org/workerpool/-/workerpool-9.3.4.tgz", + "integrity": "sha512-TmPRQYYSAnnDiEB0P/Ytip7bFGvqnSU6I2BcuSw7Hx+JSg/DsUi5ebYfc8GYaSdpuvOcEs6dXxPurOYpe9QFwg==", "dev": true }, "wrap-ansi": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-2.1.0.tgz", - "integrity": "sha1-2Pw9KE3QV5T+hJc8rs3Rz4JP3YU=", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", "dev": true, "requires": { - "string-width": "^1.0.1", - "strip-ansi": "^3.0.1" + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" }, "dependencies": { - "ansi-regex": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", - "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "requires": { + "color-convert": "^2.0.1" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "dev": true + } + } + }, + "wrap-ansi-cjs": { + "version": "npm:wrap-ansi@7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "requires": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "dependencies": { + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "requires": { + "color-convert": "^2.0.1" + } }, - "strip-ansi": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", - "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", "dev": true, "requires": { - "ansi-regex": "^2.0.0" + "color-name": "~1.1.4" } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true } } }, @@ -26806,9 +26997,9 @@ } }, "ws": { - "version": "7.5.7", - "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.7.tgz", - "integrity": "sha512-KMvVuFzpKBuiIXW3E4u3mySRO2/mCHSyZDJQM5NQ9Q9KHWHWh0NHgfbRMLLrceUK5qAL4ytALJbpRMjixFZh8A==", + "version": "7.5.10", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.10.tgz", + "integrity": "sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ==", "dev": true, "requires": {} }, @@ -26904,35 +27095,12 @@ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "dev": true }, - "get-caller-file": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", - "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", - "dev": true - }, - "is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "dev": true - }, "require-main-filename": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz", "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==", "dev": true }, - "string-width": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.0.tgz", - "integrity": "sha512-zUz5JD+tgqtuDjMhwIg5uFVV3dtqZ9yQJlZVfq4I01/K5Paj5UHj7VyrQOJvzawSVlKpObApbfD0Ed6yJc+1eg==", - "dev": true, - "requires": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.0" - } - }, "which-module": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.0.tgz", diff --git a/package.json b/package.json index 34d76ae4d9e8..9f689b60ff34 100644 --- a/package.json +++ b/package.json @@ -1,15 +1,15 @@ { "name": "python", "displayName": "Python", - "description": "IntelliSense (Pylance), Linting, Debugging (multi-threaded, remote), Jupyter Notebooks, code formatting, refactoring, unit tests, and more.", - "version": "2023.9.0-dev", + "description": "Python language support with extension access points for IntelliSense (Pylance), Debugging (Python Debugger), linting, formatting, refactoring, unit tests, and more.", + "version": "2026.5.0-dev", "featureFlags": { "usingNewInterpreterStorage": true }, "capabilities": { "untrustedWorkspaces": { - "supported": "limited", - "description": "Only Partial IntelliSense with Pylance is supported. Cannot execute Python with untrusted files." + "supported": false, + "description": "The Python extension is not available in untrusted workspaces. Use Pylance to get partial IntelliSense support for Python files." }, "virtualWorkspaces": { "supported": "limited", @@ -20,11 +20,13 @@ "enabledApiProposals": [ "contribEditorContentMenu", "quickPickSortByLabel", - "envShellEvent", "testObserver", "quickPickItemTooltip", - "envCollectionWorkspace", - "saveEditor" + "terminalDataWriteEvent", + "terminalExecuteCommandEvent", + "codeActionAI", + "notebookReplDocument", + "notebookVariableProvider" ], "author": { "name": "Microsoft Corporation" @@ -45,8 +47,9 @@ "theme": "dark" }, "engines": { - "vscode": "^1.78.0" + "vscode": "^1.95.0" }, + "enableTelemetry": false, "keywords": [ "python", "django", @@ -56,33 +59,59 @@ "categories": [ "Programming Languages", "Debuggers", - "Linters", - "Formatters", "Other", "Data Science", - "Machine Learning", - "Notebooks" + "Machine Learning" ], "activationEvents": [ "onDebugInitialConfigurations", "onLanguage:python", - "onDebugDynamicConfigurations:python", "onDebugResolve:python", - "onWalkthrough:pythonWelcome", - "onWalkthrough:pythonWelcomeWithDS", - "onWalkthrough:pythonDataScienceWelcome", + "onCommand:python.copilotSetupTests", "workspaceContains:mspythonconfig.json", "workspaceContains:pyproject.toml", "workspaceContains:Pipfile", "workspaceContains:setup.py", "workspaceContains:requirements.txt", + "workspaceContains:pylock.toml", + "workspaceContains:**/pylock.*.toml", "workspaceContains:manage.py", - "workspaceContains:app.py" + "workspaceContains:app.py", + "workspaceContains:.venv", + "workspaceContains:.conda", + "onLanguageModelTool:get_python_environment_details", + "onLanguageModelTool:get_python_executable_details", + "onLanguageModelTool:install_python_packages", + "onLanguageModelTool:configure_python_environment", + "onLanguageModelTool:create_virtual_environment", + "onTerminalShellIntegration:python" ], "main": "./out/client/extension", "browser": "./dist/extension.browser.js", "l10n": "./l10n", "contributes": { + "problemMatchers": [ + { + "name": "python", + "owner": "python", + "source": "python", + "fileLocation": "autoDetect", + "pattern": [ + { + "regexp": "^.*File \\\"([^\\\"]|.*)\\\", line (\\d+).*", + "file": 1, + "line": 2 + }, + { + "regexp": "^\\s*(.*)\\s*$" + }, + { + "regexp": "^\\s*(.*Error.*)$", + "message": 1 + } + ] + } + ], "walkthroughs": [ { "id": "pythonWelcome", @@ -90,6 +119,16 @@ "description": "%walkthrough.pythonWelcome.description%", "when": "workspacePlatform != webworker", "steps": [ + { + "id": "python.createPythonFolder", + "title": "%walkthrough.step.python.createPythonFolder.title%", + "description": "%walkthrough.step.python.createPythonFolder.description%", + "media": { + "svg": "resources/walkthrough/open-folder.svg", + "altText": "%walkthrough.step.python.createPythonFile.altText%" + }, + "when": "workspaceFolderCount = 0" + }, { "id": "python.createPythonFile", "title": "%walkthrough.step.python.createPythonFile.title%", @@ -97,8 +136,7 @@ "media": { "svg": "resources/walkthrough/open-folder.svg", "altText": "%walkthrough.step.python.createPythonFile.altText%" - }, - "when": "" + } }, { "id": "python.installPythonWin8", @@ -129,16 +167,6 @@ "when": "workspacePlatform == linux && showInstallPythonTile", "command": "workbench.action.terminal.new" }, - { - "id": "python.selectInterpreter", - "title": "%walkthrough.step.python.selectInterpreter.title%", - "description": "%walkthrough.step.python.selectInterpreter.description%", - "media": { - "svg": "resources/walkthrough/python-interpreter.svg", - "altText": "%walkthrough.step.python.selectInterpreter.altText%" - }, - "when": "workspaceFolderCount == 0" - }, { "id": "python.createEnvironment", "title": "%walkthrough.step.python.createEnvironment.title%", @@ -146,8 +174,7 @@ "media": { "svg": "resources/walkthrough/create-environment.svg", "altText": "%walkthrough.step.python.createEnvironment.altText%" - }, - "when": "workspaceFolderCount > 0" + } }, { "id": "python.runAndDebug", @@ -156,8 +183,7 @@ "media": { "svg": "resources/walkthrough/rundebug2.svg", "altText": "%walkthrough.step.python.runAndDebug.altText%" - }, - "when": "" + } }, { "id": "python.learnMoreWithDS", @@ -166,8 +192,7 @@ "media": { "altText": "%walkthrough.step.python.learnMoreWithDS.altText%", "svg": "resources/walkthrough/learnmore.svg" - }, - "when": "" + } } ] }, @@ -248,6 +273,11 @@ "category": "Python", "command": "python.createNewFile" }, + { + "category": "Python", + "command": "python.copyTestId", + "title": "%python.command.python.testing.copyTestId.title%" + }, { "category": "Python", "command": "python.analysis.restartLanguageServer", @@ -283,16 +313,6 @@ "command": "python.createEnvironment-button", "title": "%python.command.python.createEnvironment.title%" }, - { - "category": "Python", - "command": "python.enableLinting", - "title": "%python.command.python.enableLinting.title%" - }, - { - "category": "Python", - "command": "python.enableSourceMapSupport", - "title": "%python.command.python.enableSourceMapSupport.title%" - }, { "category": "Python", "command": "python.execInTerminal", @@ -306,9 +326,9 @@ }, { "category": "Python", - "command": "python.debugInTerminal", - "icon": "$(debug-alt)", - "title": "%python.command.python.debugInTerminal.title%" + "command": "python.execInDedicatedTerminal", + "icon": "$(play)", + "title": "%python.command.python.execInDedicatedTerminal.title%" }, { "category": "Python", @@ -318,19 +338,13 @@ { "category": "Python", "command": "python.execSelectionInTerminal", - "title": "%python.command.python.execSelectionInTerminal.title%" + "title": "%python.command.python.execSelectionInTerminal.title%", + "shortTitle": "%python.command.python.execSelectionInTerminal.shortTitle%" }, { "category": "Python", - "command": "python.launchTensorBoard", - "title": "%python.command.python.launchTensorBoard.title%" - }, - { - "category": "Python", - "command": "python.refreshTensorBoard", - "enablement": "python.hasActiveTensorBoardSession", - "icon": "$(refresh)", - "title": "%python.command.python.refreshTensorBoard.title%" + "command": "python.execInREPL", + "title": "%python.command.python.execInREPL.title%" }, { "category": "Python", @@ -343,11 +357,6 @@ "icon": "$(run-errors)", "title": "%python.command.testing.rerunFailedTests.title%" }, - { - "category": "Python", - "command": "python.runLinting", - "title": "%python.command.python.runLinting.title%" - }, { "category": "Python", "command": "python.setInterpreter", @@ -355,18 +364,13 @@ }, { "category": "Python", - "command": "python.setLinter", - "title": "%python.command.python.setLinter.title%" - }, - { - "category": "Python Refactor", - "command": "python.sortImports", - "title": "%python.command.python.sortImports.title%" + "command": "python.startREPL", + "title": "%python.command.python.startTerminalREPL.title%" }, { "category": "Python", - "command": "python.startREPL", - "title": "%python.command.python.startREPL.title%" + "command": "python.startNativeREPL", + "title": "%python.command.python.startNativeREPL.title%" }, { "category": "Python", @@ -404,6 +408,26 @@ "type": "array", "uniqueItems": true }, + "python.createEnvironment.contentButton": { + "default": "hide", + "markdownDescription": "%python.createEnvironment.contentButton.description%", + "scope": "machine-overridable", + "type": "string", + "enum": [ + "show", + "hide" + ] + }, + "python.createEnvironment.trigger": { + "default": "prompt", + "markdownDescription": "%python.createEnvironment.trigger.description%", + "scope": "machine-overridable", + "type": "string", + "enum": [ + "off", + "prompt" + ] + }, "python.condaPath": { "default": "", "description": "%python.condaPath.description%", @@ -416,22 +440,26 @@ "scope": "machine-overridable", "type": "string" }, - "python.diagnostics.sourceMapsEnabled": { - "default": false, - "description": "%python.diagnostics.sourceMapsEnabled.description%", - "scope": "application", - "type": "boolean" - }, "python.envFile": { "default": "${workspaceFolder}/.env", "description": "%python.envFile.description%", "scope": "resource", "type": "string" }, + "python.useEnvironmentsExtension": { + "default": false, + "description": "%python.useEnvironmentsExtension.description%", + "scope": "machine-overridable", + "type": "boolean", + "tags": [ + "onExP", + "preview" + ] + }, "python.experiments.enabled": { "default": true, "description": "%python.experiments.enabled.description%", - "scope": "machine", + "scope": "window", "type": "boolean" }, "python.experiments.optInto": { @@ -442,16 +470,20 @@ "All", "pythonSurveyNotification", "pythonPromptNewToolsExt", - "pythonTerminalEnvVarActivation" + "pythonTerminalEnvVarActivation", + "pythonDiscoveryUsingWorkers", + "pythonTestAdapter" ], "enumDescriptions": [ "%python.experiments.All.description%", "%python.experiments.pythonSurveyNotification.description%", "%python.experiments.pythonPromptNewToolsExt.description%", - "%python.experiments.pythonTerminalEnvVarActivation.description%" + "%python.experiments.pythonTerminalEnvVarActivation.description%", + "%python.experiments.pythonDiscoveryUsingWorkers.description%", + "%python.experiments.pythonTestAdapter.description%" ] }, - "scope": "machine", + "scope": "window", "type": "array", "uniqueItems": true }, @@ -463,76 +495,23 @@ "All", "pythonSurveyNotification", "pythonPromptNewToolsExt", - "pythonTerminalEnvVarActivation" + "pythonTerminalEnvVarActivation", + "pythonDiscoveryUsingWorkers", + "pythonTestAdapter" ], "enumDescriptions": [ "%python.experiments.All.description%", "%python.experiments.pythonSurveyNotification.description%", "%python.experiments.pythonPromptNewToolsExt.description%", - "%python.experiments.pythonTerminalEnvVarActivation.description%" + "%python.experiments.pythonTerminalEnvVarActivation.description%", + "%python.experiments.pythonDiscoveryUsingWorkers.description%", + "%python.experiments.pythonTestAdapter.description%" ] }, - "scope": "machine", + "scope": "window", "type": "array", "uniqueItems": true }, - "python.formatting.autopep8Args": { - "default": [], - "description": "%python.formatting.autopep8Args.description%", - "items": { - "type": "string" - }, - "scope": "resource", - "type": "array" - }, - "python.formatting.autopep8Path": { - "default": "autopep8", - "description": "%python.formatting.autopep8Path.description%", - "scope": "machine-overridable", - "type": "string" - }, - "python.formatting.blackArgs": { - "default": [], - "description": "%python.formatting.blackArgs.description%", - "items": { - "type": "string" - }, - "scope": "resource", - "type": "array" - }, - "python.formatting.blackPath": { - "default": "black", - "description": "%python.formatting.blackPath.description%", - "scope": "machine-overridable", - "type": "string" - }, - "python.formatting.provider": { - "default": "autopep8", - "description": "%python.formatting.provider.description%", - "enum": [ - "autopep8", - "black", - "none", - "yapf" - ], - "scope": "resource", - "type": "string" - }, - "python.formatting.yapfArgs": { - "default": [], - "description": "%python.formatting.yapfArgs.description%", - "items": { - "type": "string" - }, - "scope": "resource", - "type": "array" - }, - "python.formatting.yapfPath": { - "default": "yapf", - "description": "%python.formatting.yapfPath.description%", - "scope": "machine-overridable", - "type": "string" - }, "python.globalModuleInstallation": { "default": false, "description": "%python.globalModuleInstallation.description%", @@ -557,72 +536,6 @@ "scope": "window", "type": "string" }, - "python.linting.banditArgs": { - "default": [], - "description": "%python.linting.banditArgs.description%", - "items": { - "type": "string" - }, - "scope": "resource", - "type": "array" - }, - "python.linting.banditEnabled": { - "default": false, - "description": "%python.linting.banditEnabled.description%", - "scope": "resource", - "type": "boolean" - }, - "python.linting.banditPath": { - "default": "bandit", - "description": "%python.linting.banditPath.description%", - "scope": "machine-overridable", - "type": "string" - }, - "python.linting.cwd": { - "default": null, - "description": "%python.linting.cwd.description%", - "scope": "resource", - "type": "string" - }, - "python.linting.enabled": { - "default": true, - "description": "%python.linting.enabled.description%", - "scope": "resource", - "type": "boolean" - }, - "python.linting.flake8Args": { - "default": [], - "description": "%python.linting.flake8Args.description%", - "items": { - "type": "string" - }, - "scope": "resource", - "type": "array" - }, - "python.linting.flake8CategorySeverity.E": { - "default": "Error", - "description": "%python.linting.flake8CategorySeverity.E.description%", - "enum": [ - "Error", - "Hint", - "Information", - "Warning" - ], - "scope": "resource", - "type": "string" - }, - "python.linting.flake8CategorySeverity.F": { - "default": "Error", - "description": "%python.linting.flake8CategorySeverity.F.description%", - "enum": [ - "Error", - "Hint", - "Information", - "Warning" - ], - "scope": "resource", - "type": "string" - }, "python.interpreter.infoVisibility": { "default": "onPythonRelated", "description": "%python.interpreter.infoVisibility.description%", @@ -639,261 +552,23 @@ "scope": "machine", "type": "string" }, - "python.linting.flake8CategorySeverity.W": { - "default": "Warning", - "description": "%python.linting.flake8CategorySeverity.W.description%", - "enum": [ - "Error", - "Hint", - "Information", - "Warning" - ], - "scope": "resource", - "type": "string" - }, - "python.linting.flake8Enabled": { - "default": false, - "description": "%python.linting.flake8Enabled.description%", - "scope": "resource", - "type": "boolean" - }, - "python.linting.flake8Path": { - "default": "flake8", - "description": "%python.linting.flake8Path.description%", - "scope": "machine-overridable", - "type": "string" - }, - "python.linting.ignorePatterns": { - "default": [ - "**/site-packages/**/*.py", - ".vscode/*.py" - ], - "description": "%python.linting.ignorePatterns.description%", - "items": { - "type": "string" - }, - "scope": "resource", - "type": "array", - "uniqueItems": true - }, - "python.linting.lintOnSave": { - "default": true, - "description": "%python.linting.lintOnSave.description%", - "scope": "resource", - "type": "boolean" - }, - "python.linting.maxNumberOfProblems": { - "default": 100, - "description": "%python.linting.maxNumberOfProblems.description%", - "scope": "resource", - "type": "number" - }, - "python.linting.mypyArgs": { - "default": [ - "--follow-imports=silent", - "--ignore-missing-imports", - "--show-column-numbers", - "--no-pretty" - ], - "description": "%python.linting.mypyArgs.description%", - "items": { - "type": "string" - }, - "scope": "resource", - "type": "array" - }, - "python.linting.mypyCategorySeverity.error": { - "default": "Error", - "description": "%python.linting.mypyCategorySeverity.error.description%", - "enum": [ - "Error", - "Hint", - "Information", - "Warning" - ], - "scope": "resource", - "type": "string" - }, - "python.linting.mypyCategorySeverity.note": { - "default": "Information", - "description": "%python.linting.mypyCategorySeverity.note.description%.", - "enum": [ - "Error", - "Hint", - "Information", - "Warning" - ], - "scope": "resource", - "type": "string" - }, - "python.linting.mypyEnabled": { - "default": false, - "description": "%python.linting.mypyEnabled.description%", - "scope": "resource", - "type": "boolean" - }, - "python.linting.mypyPath": { - "default": "mypy", - "description": "%python.linting.mypyPath.description%", - "scope": "machine-overridable", - "type": "string" - }, - "python.linting.prospectorArgs": { - "default": [], - "description": "%python.linting.prospectorArgs.description%", - "items": { - "type": "string" - }, - "scope": "resource", - "type": "array" - }, - "python.linting.prospectorEnabled": { - "default": false, - "description": "%python.linting.prospectorEnabled.description%", - "scope": "resource", - "type": "boolean" - }, - "python.linting.prospectorPath": { - "default": "prospector", - "description": "%python.linting.prospectorPath.description%", - "scope": "machine-overridable", - "type": "string" - }, - "python.linting.pycodestyleArgs": { - "default": [], - "description": "%python.linting.pycodestyleArgs.description%", - "items": { - "type": "string" - }, - "scope": "resource", - "type": "array" - }, - "python.linting.pycodestyleCategorySeverity.E": { - "default": "Error", - "description": "%python.linting.pycodestyleCategorySeverity.E.description%", - "enum": [ - "Error", - "Hint", - "Information", - "Warning" - ], - "scope": "resource", - "type": "string" - }, - "python.linting.pycodestyleCategorySeverity.W": { - "default": "Warning", - "description": "%python.linting.pycodestyleCategorySeverity.W.description%", - "enum": [ - "Error", - "Hint", - "Information", - "Warning" - ], - "scope": "resource", - "type": "string" - }, - "python.linting.pycodestyleEnabled": { - "default": false, - "description": "%python.linting.pycodestyleEnabled.description%", - "scope": "resource", - "type": "boolean" - }, - "python.linting.pycodestylePath": { - "default": "pycodestyle", - "description": "%python.linting.pycodestylePath.description%", - "scope": "machine-overridable", - "type": "string" - }, - "python.linting.pydocstyleArgs": { - "default": [], - "description": "%python.linting.pydocstyleArgs.description%", - "items": { - "type": "string" - }, - "scope": "resource", - "type": "array" - }, - "python.linting.pydocstyleEnabled": { - "default": false, - "description": "%python.linting.pydocstyleEnabled.description%", - "scope": "resource", - "type": "boolean" - }, - "python.linting.pydocstylePath": { - "default": "pydocstyle", - "description": "%python.linting.pydocstylePath.description%", - "scope": "machine-overridable", - "type": "string" - }, - "python.linting.pylamaArgs": { - "default": [], - "description": "%python.linting.pylamaArgs.description%", - "items": { - "type": "string" - }, - "scope": "resource", - "type": "array" - }, - "python.linting.pylamaEnabled": { - "default": false, - "description": "%python.linting.pylamaEnabled.description%", - "scope": "resource", - "type": "boolean" - }, - "python.linting.pylamaPath": { - "default": "pylama", - "description": "%python.linting.pylamaPath.description%", - "scope": "machine-overridable", - "type": "string" - }, - "python.linting.pylintArgs": { - "default": [], - "description": "%python.linting.pylintArgs.description%", - "items": { - "type": "string" - }, - "scope": "resource", - "type": "array" - }, - "python.linting.pylintCategorySeverity.convention": { - "default": "Information", - "description": "%python.linting.pylintCategorySeverity.convention.description%", - "enum": [ - "Error", - "Hint", - "Information", - "Warning" - ], - "scope": "resource", - "type": "string" - }, - "python.linting.pylintCategorySeverity.error": { - "default": "Error", - "description": "%python.linting.pylintCategorySeverity.error.description%", - "enum": [ - "Error", - "Hint", - "Information", - "Warning" - ], - "scope": "resource", - "type": "string" - }, - "python.linting.pylintCategorySeverity.fatal": { - "default": "Error", - "description": "%python.linting.pylintCategorySeverity.fatal.description%", + "python.logging.level": { + "default": "error", + "deprecationMessage": "%python.logging.level.deprecation%", + "description": "%python.logging.level.description%", "enum": [ - "Error", - "Hint", - "Information", - "Warning" + "debug", + "error", + "info", + "off", + "warn" ], - "scope": "resource", + "scope": "machine", "type": "string" }, - "python.linting.pylintCategorySeverity.refactor": { + "python.missingPackage.severity": { "default": "Hint", - "description": "%python.linting.pylintCategorySeverity.refactor.description%", + "description": "%python.missingPackage.severity.description%", "enum": [ "Error", "Hint", @@ -903,40 +578,16 @@ "scope": "resource", "type": "string" }, - "python.linting.pylintCategorySeverity.warning": { - "default": "Warning", - "description": "%python.linting.pylintCategorySeverity.warning.description%", + "python.locator": { + "default": "js", + "description": "%python.locator.description%", "enum": [ - "Error", - "Hint", - "Information", - "Warning" + "js", + "native" ], - "scope": "resource", - "type": "string" - }, - "python.linting.pylintEnabled": { - "default": false, - "description": "%python.linting.pylintEnabled.description%", - "scope": "resource", - "type": "boolean" - }, - "python.linting.pylintPath": { - "default": "pylint", - "description": "%python.linting.pylintPath.description%", - "scope": "machine-overridable", - "type": "string" - }, - "python.logging.level": { - "default": "error", - "deprecationMessage": "%python.logging.level.deprecation%", - "description": "%python.logging.level.description%", - "enum": [ - "debug", - "error", - "info", - "off", - "warn" + "tags": [ + "onExP", + "preview" ], "scope": "machine", "type": "string" @@ -953,27 +604,10 @@ "scope": "machine-overridable", "type": "string" }, - "python.sortImports.args": { - "default": [], - "description": "%python.sortImports.args.description%", - "items": { - "type": "string" - }, - "scope": "resource", - "type": "array", - "deprecationMessage": "%python.sortImports.args.deprecationMessage%" - }, - "python.sortImports.path": { - "default": "", - "description": "%python.sortImports.path.description%", + "python.pixiToolPath": { + "default": "pixi", + "description": "%python.pixiToolPath.description%", "scope": "machine-overridable", - "type": "string", - "deprecationMessage": "%python.sortImports.path.deprecationMessage%" - }, - "python.tensorBoard.logDirectory": { - "default": "", - "description": "%python.tensorBoard.logDirectory.description%", - "scope": "resource", "type": "string" }, "python.terminal.activateEnvInCurrentTerminal": { @@ -1006,12 +640,45 @@ "scope": "resource", "type": "array" }, + "python.terminal.shellIntegration.enabled": { + "default": true, + "markdownDescription": "%python.terminal.shellIntegration.enabled.description%", + "scope": "resource", + "type": "boolean", + "tags": [ + "preview" + ] + }, + "python.REPL.enableREPLSmartSend": { + "default": true, + "description": "%python.EnableREPLSmartSend.description%", + "scope": "resource", + "type": "boolean" + }, + "python.REPL.sendToNativeREPL": { + "default": false, + "description": "%python.REPL.sendToNativeREPL.description%", + "scope": "resource", + "type": "boolean" + }, + "python.REPL.provideVariables": { + "default": true, + "description": "%python.REPL.provideVariables.description%", + "scope": "resource", + "type": "boolean" + }, "python.testing.autoTestDiscoverOnSaveEnabled": { "default": true, "description": "%python.testing.autoTestDiscoverOnSaveEnabled.description%", "scope": "resource", "type": "boolean" }, + "python.testing.autoTestDiscoverOnSavePattern": { + "default": "**/*.py", + "description": "%python.testing.autoTestDiscoverOnSavePattern.description%", + "scope": "resource", + "type": "string" + }, "python.testing.cwd": { "default": null, "description": "%python.testing.cwd.description%", @@ -1286,6 +953,10 @@ "internalConsole" ] }, + "consoleTitle": { + "default": "Python Debug Console", + "description": "Display name of the debug console or terminal" + }, "cwd": { "default": "${workspaceFolder}", "description": "Absolute path to the working directory of the program being debugged. Default is the root directory of the file (leave empty).", @@ -1443,6 +1114,7 @@ } } }, + "deprecated": "%python.debugger.deprecatedMessage%", "configurationSnippets": [], "label": "Python", "languages": [ @@ -1452,7 +1124,8 @@ "variables": { "pickProcess": "python.pickLocalProcess" }, - "when": "!virtualWorkspace && shellExecutionSupported" + "when": "!virtualWorkspace && shellExecutionSupported", + "hiddenWhen": "true" } ], "grammars": [ @@ -1480,13 +1153,22 @@ { "command": "python.execSelectionInTerminal", "key": "shift+enter", - "when": "editorTextFocus && editorLangId == python && !findInputFocussed && !replaceInputFocussed && !jupyter.ownsSelection && !notebookEditorFocused" + "when": "editorTextFocus && editorLangId == python && !findInputFocussed && !replaceInputFocussed && !jupyter.ownsSelection && !notebookEditorFocused && !isCompositeNotebook" + }, + { + "command": "python.execInREPL", + "key": "shift+enter", + "when": "config.python.REPL.sendToNativeREPL && editorLangId == python && editorTextFocus && !jupyter.ownsSelection && !notebookEditorFocused && !isCompositeNotebook" + }, + { + "command": "python.execInREPLEnter", + "key": "enter", + "when": "!config.interactiveWindow.executeWithShiftEnter && isCompositeNotebook && activeEditor == 'workbench.editor.repl' && !inlineChatFocused && !notebookCellListFocused" }, { - "command": "python.refreshTensorBoard", - "key": "ctrl+r", - "mac": "cmd+r", - "when": "python.hasActiveTensorBoardSession" + "command": "python.execInInteractiveWindowEnter", + "key": "enter", + "when": "!config.interactiveWindow.executeWithShiftEnter && isCompositeNotebook && activeEditor == 'workbench.editor.interactive' && !inlineChatFocused && !notebookCellListFocused" } ], "languages": [ @@ -1507,10 +1189,8 @@ ], "configuration": "./languages/pip-requirements.json", "filenamePatterns": [ - "**/*-requirements.{txt, in}", - "**/*-constraints.txt", - "**/requirements-*.{txt, in}", - "**/constraints-*.txt", + "**/*requirements*.{txt, in}", + "**/*constraints*.txt", "**/requirements/*.{txt,in}", "**/constraints/*.txt" ], @@ -1539,7 +1219,8 @@ { "filenames": [ "Pipfile", - "poetry.lock" + "poetry.lock", + "uv.lock" ], "id": "toml" }, @@ -1551,12 +1232,31 @@ } ], "menus": { + "issue/reporter": [ + { + "command": "python.reportIssue" + } + ], + "testing/item/context": [ + { + "command": "python.copyTestId", + "group": "navigation", + "when": "controllerId == 'python-tests'" + } + ], + "testing/item/gutter": [ + { + "command": "python.copyTestId", + "group": "navigation", + "when": "controllerId == 'python-tests'" + } + ], "commandPalette": [ { "category": "Python", "command": "python.analysis.restartLanguageServer", "title": "%python.command.python.analysis.restartLanguageServer.title%", - "when": "!virtualWorkspace && shellExecutionSupported && editorLangId == python" + "when": "!virtualWorkspace && shellExecutionSupported && (editorLangId == python || notebookType == jupyter-notebook)" }, { "category": "Python", @@ -1568,7 +1268,7 @@ "category": "Python", "command": "python.clearWorkspaceInterpreter", "title": "%python.command.python.clearWorkspaceInterpreter.title%", - "when": "!virtualWorkspace && shellExecutionSupported && editorLangId == python" + "when": "!virtualWorkspace && shellExecutionSupported" }, { "category": "Python", @@ -1584,20 +1284,14 @@ }, { "category": "Python", - "command": "python.createTerminal", - "title": "%python.command.python.createTerminal.title%", - "when": "!virtualWorkspace && shellExecutionSupported" - }, - { - "category": "Python", - "command": "python.enableLinting", - "title": "%python.command.python.enableLinting.title%", - "when": "!virtualWorkspace && shellExecutionSupported && editorLangId == python" + "command": "python.createEnvironment-button", + "title": "%python.command.python.createEnvironment.title%", + "when": "false" }, { "category": "Python", - "command": "python.enableSourceMapSupport", - "title": "%python.command.python.enableSourceMapSupport.title%", + "command": "python.createTerminal", + "title": "%python.command.python.createTerminal.title%", "when": "!virtualWorkspace && shellExecutionSupported" }, { @@ -1611,14 +1305,14 @@ "command": "python.execInTerminal-icon", "icon": "$(play)", "title": "%python.command.python.execInTerminalIcon.title%", - "when": "false && editorLangId == python" + "when": "false" }, { "category": "Python", - "command": "python.debugInTerminal", - "icon": "$(debug-alt)", - "title": "%python.command.python.debugInTerminal.title%", - "when": "!virtualWorkspace && shellExecutionSupported && editorLangId == python" + "command": "python.execInDedicatedTerminal", + "icon": "$(play)", + "title": "%python.command.python.execInDedicatedTerminal.title%", + "when": "false" }, { "category": "Python", @@ -1634,17 +1328,15 @@ }, { "category": "Python", - "command": "python.launchTensorBoard", - "title": "%python.command.python.launchTensorBoard.title%", - "when": "!virtualWorkspace && shellExecutionSupported" + "command": "python.copyTestId", + "title": "%python.command.python.testing.copyTestId.title%", + "when": "false" }, { "category": "Python", - "command": "python.refreshTensorBoard", - "enablement": "python.hasActiveTensorBoardSession", - "icon": "$(refresh)", - "title": "%python.command.python.refreshTensorBoard.title%", - "when": "!virtualWorkspace && shellExecutionSupported" + "command": "python.execInREPL", + "title": "%python.command.python.execInREPL.title%", + "when": "false" }, { "category": "Python", @@ -1659,12 +1351,6 @@ "title": "%python.command.testing.rerunFailedTests.title%", "when": "!virtualWorkspace && shellExecutionSupported" }, - { - "category": "Python", - "command": "python.runLinting", - "title": "%python.command.python.runLinting.title%", - "when": "!virtualWorkspace && shellExecutionSupported && editorLangId == python" - }, { "category": "Python", "command": "python.setInterpreter", @@ -1673,20 +1359,14 @@ }, { "category": "Python", - "command": "python.setLinter", - "title": "%python.command.python.setLinter.title%", - "when": "!virtualWorkspace && shellExecutionSupported && editorLangId == python" - }, - { - "category": "Python Refactor", - "command": "python.sortImports", - "title": "%python.command.python.sortImports.title%", - "when": "!virtualWorkspace && shellExecutionSupported && editorLangId == python" + "command": "python.startREPL", + "title": "%python.command.python.startTerminalREPL.title%", + "when": "!virtualWorkspace && shellExecutionSupported" }, { "category": "Python", - "command": "python.startREPL", - "title": "%python.command.python.startREPL.title%", + "command": "python.startNativeREPL", + "title": "%python.command.python.startNativeREPL.title%", "when": "!virtualWorkspace && shellExecutionSupported" }, { @@ -1707,30 +1387,24 @@ { "group": "Python", "command": "python.createEnvironment-button", - "when": "resourceLangId == pip-requirements && !virtualWorkspace && shellExecutionSupported && !inDiffEditor" + "when": "showCreateEnvButton && resourceLangId == pip-requirements && !virtualWorkspace && shellExecutionSupported && !inDiffEditor && !isMergeResultEditor && pythonDepsNotInstalled" }, { "group": "Python", "command": "python.createEnvironment-button", - "when": "resourceFilename == pyproject.toml && pipInstallableToml && !virtualWorkspace && shellExecutionSupported && !inDiffEditor" + "when": "showCreateEnvButton && resourceFilename == pyproject.toml && pipInstallableToml && !virtualWorkspace && shellExecutionSupported && !inDiffEditor && !isMergeResultEditor && pythonDepsNotInstalled" } ], "editor/context": [ { "submenu": "python.run", "group": "Python", - "when": "editorLangId == python && !virtualWorkspace && shellExecutionSupported && isWorkspaceTrusted" - }, - { - "command": "python.sortImports", - "group": "Refactor", - "title": "%python.command.python.sortImports.title%", - "when": "editorLangId == python && !notebookEditorFocused && !virtualWorkspace && shellExecutionSupported" + "when": "editorLangId == python && !virtualWorkspace && shellExecutionSupported && isWorkspaceTrusted && !inChat && notebookType != jupyter-notebook" }, { "submenu": "python.runFileInteractive", "group": "Jupyter2", - "when": "editorLangId == python && !virtualWorkspace && shellExecutionSupported && !isJupyterInstalled && isWorkspaceTrusted" + "when": "editorLangId == python && !virtualWorkspace && shellExecutionSupported && !isJupyterInstalled && isWorkspaceTrusted && !inChat" } ], "python.runFileInteractive": [ @@ -1754,14 +1428,12 @@ { "command": "python.execSelectionInTerminal", "group": "Python", - "when": "editorFocus && editorLangId == python && !virtualWorkspace && shellExecutionSupported" - } - ], - "editor/title": [ + "when": "!config.python.REPL.sendToNativeREPL && editorFocus && editorLangId == python && !virtualWorkspace && shellExecutionSupported" + }, { - "command": "python.refreshTensorBoard", - "group": "navigation@0", - "when": "python.hasActiveTensorBoardSession && !virtualWorkspace && shellExecutionSupported" + "command": "python.execInREPL", + "group": "Python", + "when": "editorFocus && editorLangId == python && !virtualWorkspace && shellExecutionSupported && config.python.REPL.sendToNativeREPL" } ], "editor/title/run": [ @@ -1772,9 +1444,9 @@ "when": "resourceLangId == python && !isInDiffEditor && !virtualWorkspace && shellExecutionSupported" }, { - "command": "python.debugInTerminal", - "group": "navigation@1", - "title": "%python.command.python.debugInTerminal.title%", + "command": "python.execInDedicatedTerminal", + "group": "navigation@0", + "title": "%python.command.python.execInDedicatedTerminal.title%", "when": "resourceLangId == python && !isInDiffEditor && !virtualWorkspace && shellExecutionSupported" } ], @@ -1831,12 +1503,172 @@ "fileMatch": "meta.yaml", "url": "./schemas/conda-meta.json" } + ], + "languageModelTools": [ + { + "name": "get_python_environment_details", + "displayName": "Get Python Environment Info", + "userDescription": "%python.languageModelTools.get_python_environment_details.userDescription%", + "modelDescription": "This tool will retrieve the details of the Python Environment for the specified file or workspace. The details returned include the 1. Type of Python Environment (conda, venv, etc), 2. Version of Python, 3. List of all installed Python packages with their versions. ALWAYS call configure_python_environment before using this tool. IMPORTANT: This tool is only for Python environments (venv, virtualenv, conda, pipenv, poetry, pyenv, pixi, or any other Python environment manager). Do not use this tool for npm packages, system packages, Ruby gems, or any other non-Python dependencies.", + "toolReferenceName": "getPythonEnvironmentInfo", + "tags": [ + "python", + "python environment", + "extension_installed_by_tool", + "enable_other_tool_configure_python_environment" + ], + "icon": "$(snake)", + "canBeReferencedInPrompt": true, + "inputSchema": { + "type": "object", + "properties": { + "resourcePath": { + "type": "string", + "description": "The path to the Python file or workspace to get the environment information for." + } + }, + "required": [] + } + }, + { + "name": "get_python_executable_details", + "displayName": "Get Python Executable", + "userDescription": "%python.languageModelTools.get_python_executable_details.userDescription%", + "modelDescription": "This tool will retrieve the details of the Python Environment for the specified file or workspace. ALWAYS use this tool before executing any Python command in the terminal. This tool returns the details of how to construct the fully qualified path and or command including details such as arguments required to run Python in a terminal. Note: Instead of executing `python --version` or `python -c 'import sys; print(sys.executable)'`, use this tool to get the Python executable path to replace the `python` command. E.g. instead of using `python -c 'import sys; print(sys.executable)'`, use this tool to build the command `conda run -n -c 'import sys; print(sys.executable)'`. ALWAYS call configure_python_environment before using this tool. IMPORTANT: This tool is only for Python environments (venv, virtualenv, conda, pipenv, poetry, pyenv, pixi, or any other Python environment manager). Do not use this tool for npm packages, system packages, Ruby gems, or any other non-Python dependencies.", + "toolReferenceName": "getPythonExecutableCommand", + "tags": [ + "python", + "python environment", + "extension_installed_by_tool", + "enable_other_tool_configure_python_environment" + ], + "icon": "$(terminal)", + "canBeReferencedInPrompt": true, + "inputSchema": { + "type": "object", + "properties": { + "resourcePath": { + "type": "string", + "description": "The path to the Python file or workspace to get the executable information for. If not provided, the current workspace will be used. Where possible pass the path to the file or workspace." + } + }, + "required": [] + } + }, + { + "name": "install_python_packages", + "displayName": "Install Python Package", + "userDescription": "%python.languageModelTools.install_python_packages.userDescription%", + "modelDescription": "Installs Python packages in the given workspace. Use this tool to install Python packages in the user's chosen Python environment. ALWAYS call configure_python_environment before using this tool. IMPORTANT: This tool should only be used to install Python packages using package managers like pip or conda (works with any Python environment: venv, virtualenv, pipenv, poetry, pyenv, pixi, conda, etc.). Do not use this tool to install npm packages, system packages (apt/brew/yum), Ruby gems, or any other non-Python dependencies.", + "toolReferenceName": "installPythonPackage", + "tags": [ + "python", + "python environment", + "install python package", + "extension_installed_by_tool", + "enable_other_tool_configure_python_environment" + ], + "icon": "$(package)", + "canBeReferencedInPrompt": true, + "inputSchema": { + "type": "object", + "properties": { + "packageList": { + "type": "array", + "items": { + "type": "string" + }, + "description": "The list of Python packages to install." + }, + "resourcePath": { + "type": "string", + "description": "The path to the Python file or workspace into which the packages are installed. If not provided, the current workspace will be used. Where possible pass the path to the file or workspace." + } + }, + "required": [ + "packageList" + ] + } + }, + { + "name": "configure_python_environment", + "displayName": "Configure Python Environment", + "modelDescription": "This tool configures a Python environment in the given workspace. ALWAYS Use this tool to set up the user's chosen environment and ALWAYS call this tool before using any other Python related tools or running any Python command in the terminal. IMPORTANT: This tool is only for Python environments (venv, virtualenv, conda, pipenv, poetry, pyenv, pixi, or any other Python environment manager). Do not use this tool for npm packages, system packages, Ruby gems, or any other non-Python dependencies.", + "userDescription": "%python.languageModelTools.configure_python_environment.userDescription%", + "toolReferenceName": "configurePythonEnvironment", + "tags": [ + "python", + "python environment", + "extension_installed_by_tool" + ], + "icon": "$(gear)", + "canBeReferencedInPrompt": true, + "inputSchema": { + "type": "object", + "properties": { + "resourcePath": { + "type": "string", + "description": "The path to the Python file or workspace for which a Python Environment needs to be configured." + } + }, + "required": [] + } + }, + { + "name": "create_virtual_environment", + "displayName": "Create a Virtual Environment", + "modelDescription": "This tool will create a Virual Environment", + "tags": [], + "canBeReferencedInPrompt": false, + "inputSchema": { + "type": "object", + "properties": { + "packageList": { + "type": "array", + "items": { + "type": "string" + }, + "description": "The list of packages to install." + }, + "resourcePath": { + "type": "string", + "description": "The path to the Python file or workspace for which a Python Environment needs to be configured." + } + }, + "required": [] + }, + "when": "false" + }, + { + "name": "selectEnvironment", + "displayName": "Select a Python Environment", + "modelDescription": "This tool will prompt the user to select an existing Python Environment", + "tags": [], + "canBeReferencedInPrompt": false, + "inputSchema": { + "type": "object", + "properties": { + "resourcePath": { + "type": "string", + "description": "The path to the Python file or workspace for which a Python Environment needs to be configured." + } + }, + "required": [] + }, + "when": "false" + } ] }, + "copilot": { + "tests": { + "getSetupConfirmation": "python.copilotSetupTests" + } + }, "scripts": { "package": "gulp clean && gulp prePublishBundle && vsce package -o ms-python-insiders.vsix", "prePublish": "gulp clean && gulp prePublishNonBundle", "compile": "tsc -watch -p ./", + "compileApi": "node ./node_modules/typescript/lib/tsc.js -b ./pythonExtensionApi/tsconfig.json", "compiled": "deemon npm run compile", "kill-compiled": "deemon --kill npm run compile", "checkDependencies": "gulp checkDependencies", @@ -1859,10 +1691,13 @@ "testSmoke": "cross-env INSTALL_JUPYTER_EXTENSION=true \"node ./out/test/smokeTest.js\"", "testInsiders": "cross-env VSC_PYTHON_CI_TEST_VSC_CHANNEL=insiders INSTALL_PYLANCE_EXTENSION=true TEST_FILES_SUFFIX=insiders.test CODE_TESTS_WORKSPACE=src/testMultiRootWkspc/smokeTests \"node ./out/test/standardTest.js\"", "lint-staged": "node gulpfile.js", - "lint": "eslint --ext .ts,.js src build", - "lint-fix": "eslint --fix --ext .ts,.js src build gulpfile.js", + "lint": "eslint src build pythonExtensionApi", + "lint-fix": "eslint --fix src build pythonExtensionApi gulpfile.js", "format-check": "prettier --check 'src/**/*.ts' 'build/**/*.js' '.github/**/*.yml' gulpfile.js", "format-fix": "prettier --write 'src/**/*.ts' 'build/**/*.js' '.github/**/*.yml' gulpfile.js", + "check-python": "npm run check-python:ruff && npm run check-python:pyright", + "check-python:ruff": "cd python_files && python -m pip install -U ruff && python -m ruff check . && python -m ruff format --check", + "check-python:pyright": "cd python_files && npx --yes pyright@1.1.308 .", "clean": "gulp clean", "addExtensionPackDependencies": "gulp addExtensionPackDependencies", "updateBuildNumber": "gulp updateBuildNumber", @@ -1870,39 +1705,32 @@ "webpack": "webpack" }, "dependencies": { - "@iarna/toml": "^2.2.5", - "@vscode/extension-telemetry": "^0.7.7", - "@vscode/jupyter-lsp-middleware": "^0.2.50", + "@iarna/toml": "^3.0.0", + "@vscode/extension-telemetry": "^0.8.4", "arch": "^2.1.0", - "diff-match-patch": "^1.0.0", - "fs-extra": "^10.0.1", + "fs-extra": "^11.2.0", "glob": "^7.2.0", - "hash.js": "^1.1.7", "iconv-lite": "^0.6.3", - "inversify": "^5.0.4", + "inversify": "^6.0.2", "jsonc-parser": "^3.0.0", - "lodash": "^4.17.21", - "md5": "^2.2.1", - "minimatch": "^5.0.1", + "lodash": "^4.18.1", + "minimatch": "^5.1.8", "named-js-regexp": "^1.3.3", "node-stream-zip": "^1.6.0", - "reflect-metadata": "^0.1.12", + "reflect-metadata": "^0.2.2", "rxjs": "^6.5.4", "rxjs-compat": "^6.5.4", - "semver": "^5.5.0", + "semver": "^7.5.2", "stack-trace": "0.0.10", "sudo-prompt": "^9.2.1", - "tmp": "^0.0.33", + "tmp": "^0.2.5", "uint64be": "^3.0.0", "unicode": "^14.0.0", - "untildify": "^4.0.0", - "vscode-debugadapter": "^1.28.0", "vscode-debugprotocol": "^1.28.0", - "vscode-jsonrpc": "8.0.2-next.1", - "vscode-languageclient": "8.0.2-next.5", - "vscode-languageserver": "8.0.2-next.5", - "vscode-languageserver-protocol": "3.17.2-next.6", - "vscode-tas-client": "^0.1.63", + "vscode-jsonrpc": "^9.0.0-next.5", + "vscode-languageclient": "^10.0.0-next.12", + "vscode-languageserver-protocol": "^3.17.6-next.10", + "vscode-tas-client": "^0.1.84", "which": "^2.0.2", "winreg": "^1.2.4", "xml2js": "^0.5.0" @@ -1913,79 +1741,73 @@ "@types/chai": "^4.1.2", "@types/chai-arrays": "^2.0.0", "@types/chai-as-promised": "^7.1.0", - "@types/diff-match-patch": "^1.0.32", "@types/download": "^8.0.1", - "@types/fs-extra": "^9.0.13", + "@types/fs-extra": "^11.0.4", "@types/glob": "^7.2.0", "@types/lodash": "^4.14.104", - "@types/md5": "^2.1.32", "@types/mocha": "^9.1.0", - "@types/nock": "^10.0.3", - "@types/node": "^16.17.0", + "@types/node": "^22.19.1", "@types/semver": "^5.5.0", "@types/shortid": "^0.0.29", - "@types/sinon": "^10.0.11", + "@types/sinon": "^17.0.3", "@types/stack-trace": "0.0.29", "@types/tmp": "^0.0.33", - "@types/uuid": "^8.3.4", - "@types/vscode": "^1.75.0", - "@vscode/vsce": "^2.18.0", + "@types/vscode": "^1.95.0", "@types/which": "^2.0.1", "@types/winreg": "^1.2.30", "@types/xml2js": "^0.4.2", - "@typescript-eslint/eslint-plugin": "^3.7.0", - "@typescript-eslint/parser": "^3.7.0", - "@vscode/test-electron": "^2.1.3", + "@typescript-eslint/eslint-plugin": "^6.21.0", + "@typescript-eslint/parser": "^6.21.0", + "@vscode/test-electron": "^2.3.8", + "@vscode/vsce": "^2.27.0", "bent": "^7.3.12", "chai": "^4.1.2", "chai-arrays": "^2.0.0", "chai-as-promised": "^7.1.1", "copy-webpack-plugin": "^9.1.0", + "cross-env": "^7.0.3", "cross-spawn": "^6.0.5", "del": "^6.0.0", "download": "^8.0.0", - "es5-ext": "0.10.53", - "eslint": "^7.2.0", - "eslint-config-airbnb": "^18.2.0", + "eslint": "^8.57.1", "eslint-config-prettier": "^8.3.0", - "eslint-plugin-import": "^2.25.4", + "eslint-plugin-import": "^2.31.0", "eslint-plugin-jsx-a11y": "^6.3.1", + "eslint-plugin-no-only-tests": "^3.3.0", "eslint-plugin-react": "^7.20.3", "eslint-plugin-react-hooks": "^4.0.0", "expose-loader": "^3.1.0", "flat": "^5.0.2", "get-port": "^5.1.1", - "gulp": "^4.0.0", + "gulp": "^5.0.0", "gulp-typescript": "^5.0.0", - "mocha": "^9.2.2", + "mocha": "^11.1.0", "mocha-junit-reporter": "^2.0.2", "mocha-multi-reporters": "^1.1.7", - "nock": "^10.0.6", "node-has-native-dependencies": "^1.0.2", "node-loader": "^1.0.2", "node-polyfill-webpack-plugin": "^1.1.4", "nyc": "^15.0.0", "prettier": "^2.0.2", "rewiremock": "^3.13.0", - "rimraf": "^3.0.2", "shortid": "^2.2.8", - "sinon": "^13.0.1", + "sinon": "^18.0.0", "source-map-support": "^0.5.12", "ts-loader": "^9.2.8", "ts-mockito": "^2.5.0", "ts-node": "^10.7.0", "tsconfig-paths-webpack-plugin": "^3.2.0", "typemoq": "^2.1.0", - "typescript": "4.5.5", - "uuid": "^8.3.2", - "vscode-debugadapter-testsupport": "^1.27.0", - "webpack": "^5.76.0", + "typescript": "~5.2", + "uuid": "^14.0.0", + "webpack": "^5.105.0", "webpack-bundle-analyzer": "^4.5.0", "webpack-cli": "^4.9.2", "webpack-fix-default-import-plugin": "^1.0.3", "webpack-merge": "^5.8.0", "webpack-node-externals": "^3.0.0", "webpack-require-from": "^1.8.6", + "worker-loader": "^3.0.8", "yargs": "^15.3.1" } } diff --git a/package.nls.json b/package.nls.json index cfbbeb7d41d5..57f2ed95b2c0 100644 --- a/package.nls.json +++ b/package.nls.json @@ -1,12 +1,16 @@ { - "python.command.python.sortImports.title": "Sort Imports", - "python.command.python.startREPL.title": "Start REPL", + "python.command.python.startTerminalREPL.title": "Start Terminal REPL", + "python.languageModelTools.get_python_environment_details.userDescription": "Get information for a Python Environment, such as Type, Version, Packages, and more.", + "python.languageModelTools.install_python_packages.userDescription": "Installs Python packages in a Python Environment.", + "python.languageModelTools.get_python_executable_details.userDescription": "Get executable info for a Python Environment", + "python.languageModelTools.configure_python_environment.userDescription": "Configure a Python Environment for a workspace", + "python.command.python.startNativeREPL.title": "Start Native Python REPL", "python.command.python.createEnvironment.title": "Create Environment...", "python.command.python.createNewFile.title": "New Python File", "python.command.python.createTerminal.title": "Create Terminal", "python.command.python.execInTerminal.title": "Run Python File in Terminal", - "python.command.python.debugInTerminal.title": "Debug Python File", "python.command.python.execInTerminalIcon.title": "Run Python File", + "python.command.python.execInDedicatedTerminal.title": "Run Python File in Dedicated Terminal", "python.command.python.setInterpreter.title": "Select Interpreter", "python.command.python.clearWorkspaceInterpreter.title": "Clear Workspace Interpreter Setting", "python.command.python.viewOutput.title": "Show Output", @@ -15,133 +19,142 @@ "python.command.python.configureTests.title": "Configure Tests", "python.command.testing.rerunFailedTests.title": "Rerun Failed Tests", "python.command.python.execSelectionInTerminal.title": "Run Selection/Line in Python Terminal", + "python.command.python.execSelectionInTerminal.shortTitle": "Run Selection/Line", + "python.command.python.execInREPL.title": "Run Selection/Line in Native Python REPL", "python.command.python.execSelectionInDjangoShell.title": "Run Selection/Line in Django Shell", "python.command.python.reportIssue.title": "Report Issue...", - "python.command.python.setLinter.title": "Select Linter", - "python.command.python.enableLinting.title": "Enable/Disable Linting", - "python.command.python.runLinting.title": "Run Linting", - "python.command.python.enableSourceMapSupport.title": "Enable Source Map Support For Extension Debugging", "python.command.python.clearCacheAndReload.title": "Clear Cache and Reload Window", "python.command.python.analysis.restartLanguageServer.title": "Restart Language Server", "python.command.python.launchTensorBoard.title": "Launch TensorBoard", "python.command.python.refreshTensorBoard.title": "Refresh TensorBoard", + "python.command.python.testing.copyTestId.title": "Copy Test Id", + "python.createEnvironment.contentButton.description": "Show or hide Create Environment button in the editor for `requirements.txt` or other dependency files.", + "python.createEnvironment.trigger.description": "Detect if environment creation is required for the current project", "python.menu.createNewFile.title": "Python File", "python.editor.context.submenu.runPython": "Run Python", "python.editor.context.submenu.runPythonInteractive": "Run in Interactive window", "python.activeStateToolPath.description": "Path to the State Tool executable for ActiveState runtimes (version 0.36+).", "python.autoComplete.extraPaths.description": "List of paths to libraries and the like that need to be imported by auto complete engine. E.g. when using Google App SDK, the paths are not in system path, hence need to be added into this list.", "python.condaPath.description": "Path to the conda executable to use for activation (version 4.4+).", + "python.debugger.deprecatedMessage": "This configuration will be deprecated soon. Please replace `python` with `debugpy` to use the new Python Debugger extension.", "python.defaultInterpreterPath.description": "Path to default Python to use when extension loads up for the first time, no longer used once an interpreter is selected for the workspace. See [here](https://aka.ms/AAfekmf) to understand when this is used", - "python.diagnostics.sourceMapsEnabled.description": "Enable source map support for meaningful stack traces in error logs.", "python.envFile.description": "Absolute path to a file containing environment variable definitions.", + "python.useEnvironmentsExtension.description": "Enables the Python Environments extension. Requires window reload on change.", "python.experiments.enabled.description": "Enables A/B tests experiments in the Python extension. If enabled, you may get included in proposed enhancements and/or features.", - "python.experiments.optInto.description": "List of experiment to opt into. If empty, user is assigned the default experiment groups. See [here](https://github.com/microsoft/vscode-python/wiki/AB-Experiments) for more details.", - "python.experiments.optOutFrom.description": "List of experiment to opt out of. If empty, user is assigned the default experiment groups. See [here](https://github.com/microsoft/vscode-python/wiki/AB-Experiments) for more details.", + "python.experiments.optInto.description": "List of experiments to opt into. If empty, user is assigned the default experiment groups. See [here](https://github.com/microsoft/vscode-python/wiki/AB-Experiments) for more details.", + "python.experiments.optOutFrom.description": "List of experiments to opt out of. If empty, user is assigned the default experiment groups. See [here](https://github.com/microsoft/vscode-python/wiki/AB-Experiments) for more details.", "python.experiments.All.description": "Combined list of all experiments.", "python.experiments.pythonSurveyNotification.description": "Denotes the Python Survey Notification experiment.", "python.experiments.pythonPromptNewToolsExt.description": "Denotes the Python Prompt New Tools Extension experiment.", "python.experiments.pythonTerminalEnvVarActivation.description": "Enables use of environment variables to activate terminals instead of sending activation commands.", - "python.formatting.autopep8Args.description": "Arguments passed in. Each argument is a separate item in the array.", - "python.formatting.autopep8Path.description": "Path to autopep8, you can use a custom version of autopep8 by modifying this setting to include the full path.", - "python.formatting.blackArgs.description": "Arguments passed in. Each argument is a separate item in the array.", - "python.formatting.blackPath.description": "Path to Black, you can use a custom version of Black by modifying this setting to include the full path.", - "python.formatting.provider.description": "Provider for formatting. Possible options include 'autopep8', 'black', and 'yapf'.", - "python.formatting.yapfArgs.description": "Arguments passed in. Each argument is a separate item in the array.", - "python.formatting.yapfPath.description": "Path to yapf, you can use a custom version of yapf by modifying this setting to include the full path.", + "python.experiments.pythonDiscoveryUsingWorkers.description": "Enables use of worker threads to do heavy computation when discovering interpreters.", + "python.experiments.pythonTestAdapter.description": "Denotes the Python Test Adapter experiment.", + "python.experiments.pythonRecommendTensorboardExt.description": "Denotes the Tensorboard Extension recommendation experiment.", "python.globalModuleInstallation.description": "Whether to install Python modules globally when not using an environment.", "python.languageServer.description": "Defines type of the language server.", "python.languageServer.defaultDescription": "Automatically select a language server: Pylance if installed and available, otherwise fallback to Jedi.", "python.languageServer.jediDescription": "Use Jedi behind the Language Server Protocol (LSP) as a language server.", "python.languageServer.pylanceDescription": "Use Pylance as a language server.", "python.languageServer.noneDescription": "Disable language server capabilities.", - "python.linting.banditArgs.description": "Arguments passed in. Each argument is a separate item in the array.", - "python.linting.banditEnabled.description": "Whether to lint Python files using bandit.", - "python.linting.banditPath.description": "Path to bandit, you can use a custom version of bandit by modifying this setting to include the full path.", - "python.linting.cwd.description": "Optional working directory for linters.", - "python.linting.enabled.description": "Whether to lint Python files.", - "python.linting.flake8Args.description": "Arguments passed in. Each argument is a separate item in the array.", - "python.linting.flake8CategorySeverity.E.description": "Severity of Flake8 message type 'E'.", - "python.linting.flake8CategorySeverity.F.description": "Severity of Flake8 message type 'F'.", - "python.linting.flake8CategorySeverity.W.description": "Severity of Flake8 message type 'W'.", - "python.linting.flake8Enabled.description": "Whether to lint Python files using flake8.", - "python.linting.flake8Path.description": "Path to flake8, you can use a custom version of flake8 by modifying this setting to include the full path.", - "python.linting.ignorePatterns.description": "Patterns used to exclude files or folders from being linted.", "python.interpreter.infoVisibility.description": "Controls when to display information of selected interpreter in the status bar.", "python.interpreter.infoVisibility.never.description": "Never display information.", "python.interpreter.infoVisibility.onPythonRelated.description": "Only display information if Python-related files are opened.", "python.interpreter.infoVisibility.always.description": "Always display information.", - "python.linting.lintOnSave.description": "Whether to lint Python files when saved.", - "python.linting.maxNumberOfProblems.description": "Controls the maximum number of problems produced by the server.", - "python.linting.mypyArgs.description": "Arguments passed in. Each argument is a separate item in the array.", - "python.linting.mypyCategorySeverity.error.description": "Severity of Mypy message type 'Error'.", - "python.linting.mypyCategorySeverity.note.description": "Severity of Mypy message type 'Note'.", - "python.linting.mypyEnabled.description": "Whether to lint Python files using mypy.", - "python.linting.mypyPath.description": "Path to mypy, you can use a custom version of mypy by modifying this setting to include the full path.", - "python.linting.prospectorArgs.description": "Arguments passed in. Each argument is a separate item in the array.", - "python.linting.prospectorEnabled.description": "Whether to lint Python files using prospector.", - "python.linting.prospectorPath.description": "Path to Prospector, you can use a custom version of prospector by modifying this setting to include the full path.", - "python.linting.pycodestyleArgs.description": "Arguments passed in. Each argument is a separate item in the array.", - "python.linting.pycodestyleCategorySeverity.E.description": "Severity of pycodestyle message type 'E'.", - "python.linting.pycodestyleCategorySeverity.W.description": "Severity of pycodestyle message type 'W'.", - "python.linting.pycodestyleEnabled.description": "Whether to lint Python files using pycodestyle.", - "python.linting.pycodestylePath.description": "Path to pycodestyle, you can use a custom version of pycodestyle by modifying this setting to include the full path.", - "python.linting.pydocstyleArgs.description": "Arguments passed in. Each argument is a separate item in the array.", - "python.linting.pydocstyleEnabled.description": "Whether to lint Python files using pydocstyle.", - "python.linting.pydocstylePath.description": "Path to pydocstyle, you can use a custom version of pydocstyle by modifying this setting to include the full path.", - "python.linting.pylamaArgs.description": "Arguments passed in. Each argument is a separate item in the array.", - "python.linting.pylamaEnabled.description": "Whether to lint Python files using pylama.", - "python.linting.pylamaPath.description": "Path to pylama, you can use a custom version of pylama by modifying this setting to include the full path.", - "python.linting.pylintArgs.description": "Arguments passed in. Each argument is a separate item in the array.", - "python.linting.pylintCategorySeverity.convention.description": "Severity of Pylint message type 'Convention/C'.", - "python.linting.pylintCategorySeverity.error.description": "Severity of Pylint message type 'Error/E'.", - "python.linting.pylintCategorySeverity.fatal.description": "Severity of Pylint message type 'Error/F'.", - "python.linting.pylintCategorySeverity.refactor.description": "Severity of Pylint message type 'Refactor/R'.", - "python.linting.pylintCategorySeverity.warning.description": "Severity of Pylint message type 'Warning/W'.", - "python.linting.pylintEnabled.description": "Whether to lint Python files using pylint.", - "python.linting.pylintPath.description": "Path to Pylint, you can use a custom version of pylint by modifying this setting to include the full path.", "python.logging.level.description": "The logging level the extension logs at, defaults to 'error'", "python.logging.level.deprecation": "This setting is deprecated. Please use command `Developer: Set Log Level...` to set logging level.", + "python.missingPackage.severity.description": "Set severity of missing packages in requirements.txt or pyproject.toml", + "python.locator.description": "[Experimental] Select implementation of environment locators. This is an experimental setting while we test native environment location.", "python.pipenvPath.description": "Path to the pipenv executable to use for activation.", "python.poetryPath.description": "Path to the poetry executable.", - "python.sortImports.args.description": "Arguments passed in. Each argument is a separate item in the array.", - "python.sortImports.path.description": "Path to isort script, default using inner version", + "python.pixiToolPath.description": "Path to the pixi executable.", + "python.EnableREPLSmartSend.description": "Toggle Smart Send for the Python REPL. Smart Send enables sending the smallest runnable block of code to the REPL on Shift+Enter and moves the cursor accordingly.", + "python.REPL.sendToNativeREPL.description": "Toggle to send code to Python REPL instead of the terminal on execution. Turning this on will change the behavior for both Smart Send and Run Selection/Line in the Context Menu.", + "python.REPL.provideVariables.description": "Toggle to provide variables for the REPL variable view for the native REPL.", "python.tensorBoard.logDirectory.description": "Set this setting to your preferred TensorBoard log directory to skip log directory prompt when starting TensorBoard.", + "python.tensorBoard.logDirectory.markdownDeprecationMessage": "Tensorboard support has been moved to the extension [Tensorboard extension](https://marketplace.visualstudio.com/items?itemName=ms-toolsai.tensorboard). Instead use the setting `tensorBoard.logDirectory`.", + "python.tensorBoard.logDirectory.deprecationMessage": "Tensorboard support has been moved to the extension Tensorboard extension. Instead use the setting `tensorBoard.logDirectory`.", + "python.terminal.shellIntegration.enabled.description": "Enable [shell integration](https://code.visualstudio.com/docs/terminal/shell-integration) for the terminals running python. Shell integration enhances the terminal experience by enabling command decorations, run recent command, improving accessibility among other things. Note: PyREPL (available in Python 3.13+) is automatically disabled when shell integration is enabled to avoid cursor indentation issues.", "python.terminal.activateEnvInCurrentTerminal.description": "Activate Python Environment in the current Terminal on load of the Extension.", "python.terminal.activateEnvironment.description": "Activate Python Environment in all Terminals created.", "python.terminal.executeInFileDir.description": "When executing a file in the terminal, whether to use execute in the file's directory, instead of the current open folder.", "python.terminal.focusAfterLaunch.description": "When launching a python terminal, whether to focus the cursor on the terminal.", "python.terminal.launchArgs.description": "Python launch arguments to use when executing a file in the terminal.", "python.testing.autoTestDiscoverOnSaveEnabled.description": "Enable auto run test discovery when saving a test file.", + "python.testing.autoTestDiscoverOnSavePattern.description": "Glob pattern used to determine which files are used by autoTestDiscoverOnSaveEnabled.", "python.testing.cwd.description": "Optional working directory for tests.", "python.testing.debugPort.description": "Port number used for debugging of tests.", "python.testing.promptToConfigure.description": "Prompt to configure a test framework if potential tests directories are discovered.", "python.testing.pytestArgs.description": "Arguments passed in. Each argument is a separate item in the array.", "python.testing.pytestEnabled.description": "Enable testing using pytest.", - "python.testing.pytestPath.description": "Path to pytest (pytest), you can use a custom version of pytest by modifying this setting to include the full path.", + "python.testing.pytestPath.description": "Path to pytest. You can use a custom version of pytest by modifying this setting to include the full path.", "python.testing.unittestArgs.description": "Arguments passed in. Each argument is a separate item in the array.", "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.sortImports.args.deprecationMessage": "This setting will be removed soon. Use 'isort.args' instead.", - "python.sortImports.path.deprecationMessage": "This setting will be removed soon. Use 'isort.path' instead.", "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", - "walkthrough.step.python.createPythonFile.description": "[Open](command:toSide:workbench.action.files.openFile) or [create](command:toSide:workbench.action.files.newUntitledFile?%7B%22languageId%22%3A%22python%22%7D) a Python file - make sure to save it as \".py\".\n[Create Python File](command:toSide:workbench.action.files.newUntitledFile?%7B%22languageId%22%3A%22python%22%7D)", + "walkthrough.step.python.createPythonFolder.title": "Open a Python project folder", + "walkthrough.step.python.createPythonFile.description": { + "message": "[Open](command:toSide:workbench.action.files.openFile) or [create](command:toSide:workbench.action.files.newUntitledFile?%7B%22languageId%22%3A%22python%22%7D) a Python file - make sure to save it as \".py\".\n[Create Python File](command:toSide:workbench.action.files.newUntitledFile?%7B%22languageId%22%3A%22python%22%7D)", + "comment": [ + "{Locked='](command:toSide:workbench.action.files.newUntitledFile?%7B%22languageId%22%3A%22python%22%7D'}", + "Do not translate the 'command:*' part inside of the '(..)'. It is an internal command syntax for VS Code", + "Please make sure there is no space between the right bracket and left parenthesis: ]( this is an internal syntax for links" + ] + }, + "walkthrough.step.python.createPythonFolder.description": { + "message": "[Open](command:workbench.action.files.openFolder) or create a project folder.\n[Open Project Folder](command:workbench.action.files.openFolder)", + "comment": [ + "{Locked='](command:workbench.action.files.openFolder'}", + "Do not translate the 'command:*' part inside of the '(..)'. It is an internal command syntax for VS Code", + "Please make sure there is no space between the right bracket and left parenthesis: ]( this is an internal syntax for links" + ] + }, "walkthrough.step.python.installPythonWin8.title": "Install Python", "walkthrough.step.python.installPythonWin8.description": "The Python Extension requires Python to be installed. Install Python [from python.org](https://www.python.org/downloads).\n\n[Install Python](https://www.python.org/downloads)\n", "walkthrough.step.python.installPythonMac.title": "Install Python", - "walkthrough.step.python.installPythonMac.description": "The Python Extension requires Python to be installed. Install Python 3 through the terminal.\n[Install Python via Brew](command:python.installPythonOnMac)\n", + "walkthrough.step.python.installPythonMac.description": { + "message": "The Python Extension requires Python to be installed. Install Python 3 through the terminal.\n[Install Python via Brew](command:python.installPythonOnMac)\n", + "comment": [ + "{Locked='](command:python.installPythonOnMac'}", + "Do not translate the 'command:*' part inside of the '(..)'. It is an internal command syntax for VS Code", + "Please make sure there is no space between the right bracket and left parenthesis: ]( this is an internal syntax for links" + ] + }, "walkthrough.step.python.installPythonLinux.title": "Install Python", - "walkthrough.step.python.installPythonLinux.description": "The Python Extension requires Python to be installed. Install Python 3 through the terminal.\n[Install Python via terminal](command:python.installPythonOnLinux)\n", + "walkthrough.step.python.installPythonLinux.description": { + "message": "The Python Extension requires Python to be installed. Install Python 3 through the terminal.\n[Install Python via terminal](command:python.installPythonOnLinux)\n", + "comment": [ + "{Locked='](command:python.installPythonOnLinux'}", + "Do not translate the 'command:*' part inside of the '(..)'. It is an internal command syntax for VS Code", + "Please make sure there is no space between the right bracket and left parenthesis: ]( this is an internal syntax for links" + ] + }, "walkthrough.step.python.selectInterpreter.title": "Select a Python Interpreter", - "walkthrough.step.python.selectInterpreter.description": "Choose which Python interpreter/environment you want to use for your Python project.\n[Select Python Interpreter](command:python.setInterpreter)\n**Tip**: Run the ``Python: Select Interpreter`` command in the [Command Palette](command:workbench.action.showCommands).", - "walkthrough.step.python.createEnvironment.title": "Create a Python Environment ", - "walkthrough.step.python.createEnvironment.description": "Create an environment for your Python project.\n[Create Environment](command:python.createEnvironment)\n**Tip**: Run the ``Python: Create Environment`` command in the [Command Palette](command:workbench.action.showCommands).\n πŸ” Check out our [docs](https://aka.ms/pythonenvs) to learn more.", + "walkthrough.step.python.createEnvironment.title": "Select or create a Python environment", + "walkthrough.step.python.createEnvironment.description": { + "message": "Create an environment for your Python project or use [Select Python Interpreter](command:python.setInterpreter) to select an existing one.\n[Create Environment](command:python.createEnvironment)\n**Tip**: Run the ``Python: Create Environment`` command in the [Command Palette](command:workbench.action.showCommands).", + "comment": [ + "{Locked='](command:python.createEnvironment'}", + "{Locked='](command:workbench.action.showCommands'}", + "{Locked='](command:python.setInterpreter'}", + "Do not translate the 'command:*' part inside of the '(..)'. It is an internal command syntax for VS Code", + "Please make sure there is no space between the right bracket and left parenthesis: ]( this is an internal syntax for links" + ] + }, "walkthrough.step.python.runAndDebug.title": "Run and debug your Python file", "walkthrough.step.python.runAndDebug.description": "Open your Python file and click on the play button on the top right of the editor, or press F5 when on the file and select \"Python File\" to run with the debugger. \n \n[Learn more](https://code.visualstudio.com/docs/python/python-tutorial#_run-hello-world)", - "walkthrough.step.python.learnMoreWithDS.title": "Explore more resources", - "walkthrough.step.python.learnMoreWithDS.description": "🎨 Explore all the features the Python extension has to offer by looking for \"Python\" in the [Command Palette](command:workbench.action.showCommands). \n πŸ“ˆ Learn more about getting started with [data science](command:workbench.action.openWalkthrough?%7B%22category%22%3A%22ms-python.python%23pythonDataScienceWelcome%22%2C%22step%22%3A%22ms-python.python%23python.createNewNotebook%22%7D) in Python. \n ✨ Take a look at our [Release Notes](https://aka.ms/AA8dxtb) to learn more about the latest features. \n \n[Learn More](https://aka.ms/AA8dqti)", + "walkthrough.step.python.learnMoreWithDS.title": "Keep exploring!", + "walkthrough.step.python.learnMoreWithDS.description": { + "message": "🎨 Explore all the features the Python extension has to offer by looking for \"Python\" in the [Command Palette](command:workbench.action.showCommands). \n πŸ“ˆ Learn more about getting started with [data science](command:workbench.action.openWalkthrough?%7B%22category%22%3A%22ms-python.python%23pythonDataScienceWelcome%22%2C%22step%22%3A%22ms-python.python%23python.createNewNotebook%22%7D) in Python. \n ✨ Take a look at our [Release Notes](https://aka.ms/AA8dxtb) to learn more about the latest features. \n \n[Follow along with the Python Tutorial](https://aka.ms/AA8dqti)", + "comment": [ + "{Locked='](command:workbench.action.showCommands'}", + "{Locked='](command:workbench.action.openWalkthrough?%7B%22category%22%3A%22ms-python.python%23pythonDataScienceWelcome%22%2C%22step%22%3A%22ms-python.python%23python.createNewNotebook%22%7D'}", + "Do not translate the 'command:*' part inside of the '(..)'. It is an internal command syntax for VS Code", + "Please make sure there is no space between the right bracket and left parenthesis: ]( this is an internal syntax for links" + ] + }, "walkthrough.pythonDataScienceWelcome.title": "Get Started with Python for Data Science", "walkthrough.pythonDataScienceWelcome.description": "Your first steps to getting started with a Data Science project with Python!", "walkthrough.step.python.installJupyterExt.title": "Install Jupyter extension", diff --git a/pythonExtensionApi/.eslintrc b/pythonExtensionApi/.eslintrc new file mode 100644 index 000000000000..8828c49002ed --- /dev/null +++ b/pythonExtensionApi/.eslintrc @@ -0,0 +1,11 @@ +{ + "overrides": [ + { + "files": ["**/main.d.ts"], + "rules": { + "@typescript-eslint/no-explicit-any": "off", + "padding-line-between-statements": ["error", { "blankLine": "always", "prev": "export", "next": "*" }] + } + } + ] +} diff --git a/pythonExtensionApi/.npmignore b/pythonExtensionApi/.npmignore new file mode 100644 index 000000000000..283d589ea5fe --- /dev/null +++ b/pythonExtensionApi/.npmignore @@ -0,0 +1,8 @@ +example/** +dist/ +out/**/*.map +out/**/*.tsbuildInfo +src/ +.eslintrc* +.eslintignore +tsconfig*.json diff --git a/pythonExtensionApi/LICENSE.md b/pythonExtensionApi/LICENSE.md new file mode 100644 index 000000000000..767f4076ba05 --- /dev/null +++ b/pythonExtensionApi/LICENSE.md @@ -0,0 +1,21 @@ +Copyright (c) Microsoft Corporation. All rights reserved. + +MIT License + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED _AS IS_, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/pythonExtensionApi/README.md b/pythonExtensionApi/README.md new file mode 100644 index 000000000000..5208d90cdfa5 --- /dev/null +++ b/pythonExtensionApi/README.md @@ -0,0 +1,55 @@ +# Python extension's API + +This npm module implements an API facade for the Python extension in VS Code. + +## Example + +First we need to define a `package.json` for the extension that wants to use the API: + +```jsonc +{ + "name": "...", + ... + // depend on the Python extension + "extensionDependencies": [ + "ms-python.python" + ], + // Depend on the Python extension facade npm module to get easier API access to the + // core extension. + "dependencies": { + "@vscode/python-extension": "...", + "@types/vscode": "..." + }, +} +``` + +Update `"@types/vscode"` to [a recent version](https://code.visualstudio.com/updates/) of VS Code, say `"^1.81.0"` for VS Code version `"1.81"`, in case there are any conflicts. + +The actual source code to get the active environment to run some script could look like this: + +```typescript +// Import the API +import { PythonExtension } from '@vscode/python-extension'; + +... + +// Load the Python extension API +const pythonApi: PythonExtension = await PythonExtension.api(); + +// This will return something like /usr/bin/python +const environmentPath = pythonApi.environments.getActiveEnvironmentPath(); + +// `environmentPath.path` carries the value of the setting. Note that this path may point to a folder and not the +// python binary. Depends entirely on how the env was created. +// E.g., `conda create -n myenv python` ensures the env has a python binary +// `conda create -n myenv` does not include a python binary. +// Also, the path specified may not be valid, use the following to get complete details for this environment if +// need be. + +const environment = await pythonApi.environments.resolveEnvironment(environmentPath); +if (environment) { + // run your script here. +} +``` + +Check out [the wiki](https://aka.ms/pythonEnvironmentApi) for many more examples and usage. diff --git a/pythonExtensionApi/SECURITY.md b/pythonExtensionApi/SECURITY.md new file mode 100644 index 000000000000..a050f362c152 --- /dev/null +++ b/pythonExtensionApi/SECURITY.md @@ -0,0 +1,41 @@ + + +## Security + +Microsoft takes the security of our software products and services seriously, which includes all source code repositories managed through our GitHub organizations, which include [Microsoft](https://github.com/Microsoft), [Azure](https://github.com/Azure), [DotNet](https://github.com/dotnet), [AspNet](https://github.com/aspnet), [Xamarin](https://github.com/xamarin), and [our GitHub organizations](https://opensource.microsoft.com/). + +If you believe you have found a security vulnerability in any Microsoft-owned repository that meets [Microsoft's definition of a security vulnerability](https://docs.microsoft.com/en-us/previous-versions/tn-archive/cc751383(v=technet.10)), please report it to us as described below. + +## Reporting Security Issues + +**Please do not report security vulnerabilities through public GitHub issues.** + +Instead, please report them to the Microsoft Security Response Center (MSRC) at [https://msrc.microsoft.com/create-report](https://msrc.microsoft.com/create-report). + +If you prefer to submit without logging in, send email to [secure@microsoft.com](mailto:secure@microsoft.com). If possible, encrypt your message with our PGP key; please download it from the [Microsoft Security Response Center PGP Key page](https://www.microsoft.com/en-us/msrc/pgp-key-msrc). + +You should receive a response within 24 hours. If for some reason you do not, please follow up via email to ensure we received your original message. Additional information can be found at [microsoft.com/msrc](https://www.microsoft.com/msrc). + +Please include the requested information listed below (as much as you can provide) to help us better understand the nature and scope of the possible issue: + + * Type of issue (e.g. buffer overflow, SQL injection, cross-site scripting, etc.) + * Full paths of source file(s) related to the manifestation of the issue + * The location of the affected source code (tag/branch/commit or direct URL) + * Any special configuration required to reproduce the issue + * Step-by-step instructions to reproduce the issue + * Proof-of-concept or exploit code (if possible) + * Impact of the issue, including how an attacker might exploit the issue + +This information will help us triage your report more quickly. + +If you are reporting for a bug bounty, more complete reports can contribute to a higher bounty award. Please visit our [Microsoft Bug Bounty Program](https://microsoft.com/msrc/bounty) page for more details about our active programs. + +## Preferred Languages + +We prefer all communications to be in English. + +## Policy + +Microsoft follows the principle of [Coordinated Vulnerability Disclosure](https://www.microsoft.com/en-us/msrc/cvd). + + diff --git a/pythonExtensionApi/package-lock.json b/pythonExtensionApi/package-lock.json new file mode 100644 index 000000000000..e462fc1c888a --- /dev/null +++ b/pythonExtensionApi/package-lock.json @@ -0,0 +1,157 @@ +{ + "name": "@vscode/python-extension", + "version": "1.0.6", + "lockfileVersion": 2, + "requires": true, + "packages": { + "": { + "name": "@vscode/python-extension", + "version": "1.0.6", + "license": "MIT", + "devDependencies": { + "@types/vscode": "^1.93.0", + "source-map": "^0.8.0-beta.0", + "typescript": "~5.2" + }, + "engines": { + "node": ">=22.17.0", + "vscode": "^1.93.0" + } + }, + "node_modules/@types/vscode": { + "version": "1.94.0", + "resolved": "https://registry.npmjs.org/@types/vscode/-/vscode-1.94.0.tgz", + "integrity": "sha512-UyQOIUT0pb14XSqJskYnRwD2aG0QrPVefIfrW1djR+/J4KeFQ0i1+hjZoaAmeNf3Z2jleK+R2hv+EboG/m8ruw==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.sortby": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/lodash.sortby/-/lodash.sortby-4.7.0.tgz", + "integrity": "sha512-HDWXG8isMntAyRF5vZ7xKuEvOhT4AhlRt/3czTSjvGUxjYCBVRQY48ViDHyfYz9VIoBkW4TMGQNapx+l3RUwdA==", + "dev": true + }, + "node_modules/punycode": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.0.tgz", + "integrity": "sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/source-map": { + "version": "0.8.0-beta.0", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.8.0-beta.0.tgz", + "integrity": "sha512-2ymg6oRBpebeZi9UUNsgQ89bhx01TcTkmNTGnNO88imTmbSgy4nfujrgVEFKWpMTEGA11EDkTt7mqObTPdigIA==", + "dev": true, + "dependencies": { + "whatwg-url": "^7.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/tr46": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-1.0.1.tgz", + "integrity": "sha512-dTpowEjclQ7Kgx5SdBkqRzVhERQXov8/l9Ft9dVM9fmg0W0KQSVaXX9T4i6twCPNtYiZM53lpSSUAwJbFPOHxA==", + "dev": true, + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/typescript": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.2.2.tgz", + "integrity": "sha512-mI4WrpHsbCIcwT9cF4FZvr80QUeKvsUsUvKDoR+X/7XHQH98xYD8YHZg7ANtz2GtZt/CBq2QJ0thkGJMHfqc1w==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/webidl-conversions": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-4.0.2.tgz", + "integrity": "sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg==", + "dev": true + }, + "node_modules/whatwg-url": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-7.1.0.tgz", + "integrity": "sha512-WUu7Rg1DroM7oQvGWfOiAK21n74Gg+T4elXEQYkOhtyLeWiJFoOGLXPKI/9gzIie9CtwVLm8wtw6YJdKyxSjeg==", + "dev": true, + "dependencies": { + "lodash.sortby": "^4.7.0", + "tr46": "^1.0.1", + "webidl-conversions": "^4.0.2" + } + } + }, + "dependencies": { + "@types/vscode": { + "version": "1.94.0", + "resolved": "https://registry.npmjs.org/@types/vscode/-/vscode-1.94.0.tgz", + "integrity": "sha512-UyQOIUT0pb14XSqJskYnRwD2aG0QrPVefIfrW1djR+/J4KeFQ0i1+hjZoaAmeNf3Z2jleK+R2hv+EboG/m8ruw==", + "dev": true + }, + "lodash.sortby": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/lodash.sortby/-/lodash.sortby-4.7.0.tgz", + "integrity": "sha512-HDWXG8isMntAyRF5vZ7xKuEvOhT4AhlRt/3czTSjvGUxjYCBVRQY48ViDHyfYz9VIoBkW4TMGQNapx+l3RUwdA==", + "dev": true + }, + "punycode": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.0.tgz", + "integrity": "sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA==", + "dev": true + }, + "source-map": { + "version": "0.8.0-beta.0", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.8.0-beta.0.tgz", + "integrity": "sha512-2ymg6oRBpebeZi9UUNsgQ89bhx01TcTkmNTGnNO88imTmbSgy4nfujrgVEFKWpMTEGA11EDkTt7mqObTPdigIA==", + "dev": true, + "requires": { + "whatwg-url": "^7.0.0" + } + }, + "tr46": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-1.0.1.tgz", + "integrity": "sha512-dTpowEjclQ7Kgx5SdBkqRzVhERQXov8/l9Ft9dVM9fmg0W0KQSVaXX9T4i6twCPNtYiZM53lpSSUAwJbFPOHxA==", + "dev": true, + "requires": { + "punycode": "^2.1.0" + } + }, + "typescript": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.2.2.tgz", + "integrity": "sha512-mI4WrpHsbCIcwT9cF4FZvr80QUeKvsUsUvKDoR+X/7XHQH98xYD8YHZg7ANtz2GtZt/CBq2QJ0thkGJMHfqc1w==", + "dev": true + }, + "webidl-conversions": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-4.0.2.tgz", + "integrity": "sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg==", + "dev": true + }, + "whatwg-url": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-7.1.0.tgz", + "integrity": "sha512-WUu7Rg1DroM7oQvGWfOiAK21n74Gg+T4elXEQYkOhtyLeWiJFoOGLXPKI/9gzIie9CtwVLm8wtw6YJdKyxSjeg==", + "dev": true, + "requires": { + "lodash.sortby": "^4.7.0", + "tr46": "^1.0.1", + "webidl-conversions": "^4.0.2" + } + } + } +} diff --git a/pythonExtensionApi/package.json b/pythonExtensionApi/package.json new file mode 100644 index 000000000000..11e0445aa8da --- /dev/null +++ b/pythonExtensionApi/package.json @@ -0,0 +1,43 @@ +{ + "name": "@vscode/python-extension", + "description": "An API facade for the Python extension in VS Code", + "version": "1.0.6", + "author": { + "name": "Microsoft Corporation" + }, + "keywords": [ + "Python", + "VSCode", + "API" + ], + "main": "./out/main.js", + "types": "./out/main.d.ts", + "engines": { + "node": ">=22.21.1", + "vscode": "^1.93.0" + }, + "license": "MIT", + "homepage": "https://github.com/microsoft/vscode-python/tree/main/pythonExtensionApi", + "repository": { + "type": "git", + "url": "https://github.com/Microsoft/vscode-python" + }, + "bugs": { + "url": "https://github.com/Microsoft/vscode-python/issues" + }, + "devDependencies": { + "typescript": "~5.2", + "@types/vscode": "^1.102.0", + "source-map": "^0.8.0-beta.0" + }, + "scripts": { + "prepublishOnly": "echo \"β›” Can only publish from a secure pipeline β›”\" && node ../build/fail", + "prepack": "npm run all:publish", + "compile": "node ./node_modules/typescript/lib/tsc.js -b ./tsconfig.json", + "clean": "node ../node_modules/rimraf/bin.js out", + "lint": "node ../node_modules/eslint/bin/eslint.js --ext ts src", + "all": "npm run clean && npm run compile", + "formatTypings": "node ../node_modules/eslint/bin/eslint.js --fix ./out/main.d.ts", + "all:publish": "git clean -xfd . && npm install && npm run compile && npm run formatTypings" + } +} diff --git a/src/client/apiTypes.ts b/pythonExtensionApi/src/main.ts similarity index 89% rename from src/client/apiTypes.ts rename to pythonExtensionApi/src/main.ts index d30a81582a7e..2173245cbb28 100644 --- a/src/client/apiTypes.ts +++ b/pythonExtensionApi/src/main.ts @@ -1,57 +1,34 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -import { CancellationToken, Event, Uri, WorkspaceFolder } from 'vscode'; -import { IDataViewerDataProvider, IJupyterUriProvider } from './jupyter/types'; +import { CancellationToken, Event, Uri, WorkspaceFolder, extensions } from 'vscode'; /* * Do not introduce any breaking changes to this API. * This is the public API for other extensions to interact with this extension. */ - -export interface IExtensionApi { +export interface PythonExtension { /** * Promise indicating whether all parts of the extension have completed loading or not. - * @type {Promise} - * @memberof IExtensionApi */ ready: Promise; - jupyter: { - registerHooks(): void; - }; debug: { /** * Generate an array of strings for commands to pass to the Python executable to launch the debugger for remote debugging. * Users can append another array of strings of what they want to execute along with relevant arguments to Python. - * E.g `['/Users/..../pythonVSCode/pythonFiles/lib/python/debugpy', '--listen', 'localhost:57039', '--wait-for-client']` - * @param {string} host - * @param {number} port - * @param {boolean} [waitUntilDebuggerAttaches=true] - * @returns {Promise} + * E.g `['/Users/..../pythonVSCode/python_files/lib/python/debugpy', '--listen', 'localhost:57039', '--wait-for-client']` + * @param host + * @param port + * @param waitUntilDebuggerAttaches Defaults to `true`. */ getRemoteLauncherCommand(host: string, port: number, waitUntilDebuggerAttaches: boolean): Promise; /** * Gets the path to the debugger package used by the extension. - * @returns {Promise} */ getDebuggerPackagePath(): Promise; }; - datascience: { - /** - * Launches Data Viewer component. - * @param {IDataViewerDataProvider} dataProvider Instance that will be used by the Data Viewer component to fetch data. - * @param {string} title Data Viewer title - */ - showDataViewer(dataProvider: IDataViewerDataProvider, title: string): Promise; - /** - * Registers a remote server provider component that's used to pick remote jupyter server URIs - * @param serverProvider object called back when picking jupyter server URI - */ - registerRemoteServerProvider(serverProvider: IJupyterUriProvider): void; - }; - /** * These APIs provide a way for extensions to work with by python environments available in the user's machine * as found by the Python extension. See @@ -250,9 +227,9 @@ export type EnvironmentsChangeEvent = { export type ActiveEnvironmentPathChangeEvent = EnvironmentPath & { /** - * Workspace folder the environment changed for. + * Resource the environment changed for. */ - readonly resource: WorkspaceFolder | undefined; + readonly resource: Resource | undefined; }; /** @@ -349,3 +326,23 @@ export type EnvironmentVariablesChangeEvent = { */ readonly env: EnvironmentVariables; }; + +export const PVSC_EXTENSION_ID = 'ms-python.python'; + +// eslint-disable-next-line @typescript-eslint/no-namespace +export namespace PythonExtension { + /** + * Returns the API exposed by the Python extension in VS Code. + */ + export async function api(): Promise { + const extension = extensions.getExtension(PVSC_EXTENSION_ID); + if (extension === undefined) { + throw new Error(`Python extension is not installed or is disabled`); + } + if (!extension.isActive) { + await extension.activate(); + } + const pythonApi: PythonExtension = extension.exports; + return pythonApi; + } +} diff --git a/pythonExtensionApi/tsconfig.json b/pythonExtensionApi/tsconfig.json new file mode 100644 index 000000000000..9ab7617023df --- /dev/null +++ b/pythonExtensionApi/tsconfig.json @@ -0,0 +1,34 @@ +{ + "compilerOptions": { + "baseUrl": ".", + "paths": { + "*": ["types/*"] + }, + "module": "commonjs", + "target": "es2018", + "outDir": "./out", + "lib": [ + "es6", + "es2018", + "dom", + "ES2019", + "ES2020" + ], + "sourceMap": true, + "rootDir": "src", + "experimentalDecorators": true, + "allowSyntheticDefaultImports": true, + "strict": true, + "noImplicitAny": true, + "noImplicitThis": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true, + "resolveJsonModule": true, + "declaration": true + }, + "exclude": [ + "node_modules", + "out" + ] +} diff --git a/pythonFiles/get-pip.py b/pythonFiles/get-pip.py deleted file mode 100644 index 2c411ecf21e3..000000000000 --- a/pythonFiles/get-pip.py +++ /dev/null @@ -1,27086 +0,0 @@ -#!/usr/bin/env python -# -# Hi There! -# -# You may be wondering what this giant blob of binary data here is, you might -# even be worried that we're up to something nefarious (good for you for being -# paranoid!). This is a base85 encoding of a zip file, this zip file contains -# an entire copy of pip (version 21.3.1). -# -# Pip is a thing that installs packages, pip itself is a package that someone -# might want to install, especially if they're looking to run this get-pip.py -# script. Pip has a lot of code to deal with the security of installing -# packages, various edge cases on various platforms, and other such sort of -# "tribal knowledge" that has been encoded in its code base. Because of this -# we basically include an entire copy of pip inside this blob. We do this -# because the alternatives are attempt to implement a "minipip" that probably -# doesn't do things correctly and has weird edge cases, or compress pip itself -# down into a single file. -# -# If you're wondering how this is created, it is generated using -# `scripts/generate.py` in https://github.com/pypa/get-pip. - -import sys - -this_python = sys.version_info[:2] -min_version = (3, 6) -if this_python < min_version: - message_parts = [ - "This script does not work on Python {}.{}".format(*this_python), - "The minimum supported Python version is {}.{}.".format(*min_version), - "Please use https://bootstrap.pypa.io/pip/{}.{}/get-pip.py instead.".format( - *this_python - ), - ] - print("ERROR: " + " ".join(message_parts)) - sys.exit(1) - - -import os.path -import pkgutil -import shutil -import tempfile -from base64 import b85decode - - -def determine_pip_install_arguments(): - implicit_pip = True - implicit_setuptools = True - implicit_wheel = True - - # Check if the user has requested us not to install setuptools - if "--no-setuptools" in sys.argv or os.environ.get("PIP_NO_SETUPTOOLS"): - args = [x for x in sys.argv[1:] if x != "--no-setuptools"] - implicit_setuptools = False - else: - args = sys.argv[1:] - - # Check if the user has requested us not to install wheel - if "--no-wheel" in args or os.environ.get("PIP_NO_WHEEL"): - args = [x for x in args if x != "--no-wheel"] - implicit_wheel = False - - # We only want to implicitly install setuptools and wheel if they don't - # already exist on the target platform. - if implicit_setuptools: - try: - import setuptools # noqa - - implicit_setuptools = False - except ImportError: - pass - if implicit_wheel: - try: - import wheel # noqa - - implicit_wheel = False - except ImportError: - pass - - # Add any implicit installations to the end of our args - if implicit_pip: - args += ["pip"] - if implicit_setuptools: - args += ["setuptools"] - if implicit_wheel: - args += ["wheel"] - - return ["install", "--upgrade", "--force-reinstall"] + args - - -def monkeypatch_for_cert(tmpdir): - """Patches `pip install` to provide default certificate with the lowest priority. - - This ensures that the bundled certificates are used unless the user specifies a - custom cert via any of pip's option passing mechanisms (config, env-var, CLI). - - A monkeypatch is the easiest way to achieve this, without messing too much with - the rest of pip's internals. - """ - from pip._internal.commands.install import InstallCommand - - # We want to be using the internal certificates. - cert_path = os.path.join(tmpdir, "cacert.pem") - with open(cert_path, "wb") as cert: - cert.write(pkgutil.get_data("pip._vendor.certifi", "cacert.pem")) - - install_parse_args = InstallCommand.parse_args - - def cert_parse_args(self, args): - if not self.parser.get_default_values().cert: - # There are no user provided cert -- force use of bundled cert - self.parser.defaults["cert"] = cert_path # calculated above - return install_parse_args(self, args) - - InstallCommand.parse_args = cert_parse_args - - -def bootstrap(tmpdir): - monkeypatch_for_cert(tmpdir) - - # Execute the included pip and use it to install the latest pip and - # setuptools from PyPI - from pip._internal.cli.main import main as pip_entry_point - - args = determine_pip_install_arguments() - sys.exit(pip_entry_point(args)) - - -def main(): - tmpdir = None - try: - # Create a temporary working directory - tmpdir = tempfile.mkdtemp() - - # Unpack the zipfile into the temporary directory - pip_zip = os.path.join(tmpdir, "pip.zip") - with open(pip_zip, "wb") as fp: - fp.write(b85decode(DATA.replace(b"\n", b""))) - - # Add the zipfile to sys.path so that we can import it - sys.path.insert(0, pip_zip) - - # Run the bootstrap - bootstrap(tmpdir=tmpdir) - finally: - # Clean up our temporary working directory - if tmpdir: - shutil.rmtree(tmpdir, ignore_errors=True) - - -DATA = b""" -P)h>@6aWAK2mt$eR#TliG(h+O003nH000jF003}la4%n9X>MtBUtcb8c|B0UO2j}6z0X&KUUXrdvMRV -16ubz6s0VM$QfAw<4YV^ulDhQoop$MlK*;0ehKz6 -^g4|bOsV`^+*aO7_tw^Cd$4zs{Pl#j>6{|X*AaQ6!2wJ?w>%d+2&1X4Rc!^r6h-hMtH_d5{IF3D`nKTt~p1QY-O00;p4c~(;+8{@xi0ssK6 -1ONaJ0001RX>c!JUu|J&ZeL$6aCu!*!ET%|5WVviBXU@FMMw_qp;5O|rCxIBA*z%^Qy~|I#aghDZI*1 -mzHbcdCgFtbH*em&nbG}VT_EcdJ^%Uh<#$rfXmjvMazjtt+Y{4fL(0@tjn1(F!nz|6RBOjouLCQKB%tCsn -f_O;(TkT9D!5I2G1vZWcORK< -*}iONjWAr8Zm1&KuL0jC{@?djd+x5R}RGfYPBawx08>U(W?WmDk1T9S4?epCt{Z(ueTz)EC*E`5mT15 --&2~-DsS-6=uU3I|BmObEPJI*Sr)^2!Om@h-$wOJl_c@O>A_3OHg5wqIeD(E7`y@m0ou*N^~8Scf|wu -`N_HtL5`*k&gASg%W(oQp9a7<~IpnR_S}F8z9z|q{`1rb)-o!>My0eex)q(ByedFLGyO7=Ikq8}(HcH -6i;acy-%V$hD`fEosH@wgA+8z#{H{ToXOd_?&uMj~(yRVmD7BE?-`X6FU!78rkLs#HE1jqSOWnjp~Z3(}j4wN{#<0DmEaw -w2fbN$l@K=F!>KqO9KQH000080Q-4XQ_aV~HNXG>03HDV01N;C0B~t=FK~G-ba`-PWF?NVPQ@?`MfZN -i-B_Ob4-5=!Z$M%;t!XVKc9b}U{5?(?Egj!;iWEo#VY8e`cO+3psdiM#D?U$24DrcGE{QX%^A1rwho7 -bo%%^4nEOe11`ih5ds}r~C4-D(by*bnzy~VhcmspFPs+92he4iKm495?R6(6IB9*bzqWO6Z``e?dj4> -$ei>cuLo8^bh>J0qwmAsn45g@9MQ{TAMQ=}M~B1K+Woqz5;+g_LK&{q3XhT~awQHE!$j2T)4`1QY-O0 -0;p4c~(=FwQ!+P0RR9!0ssIR0001RX>c!JX>N37a&BR4FJE72ZfSI1UoLQYb&)Yo#4rqn_xuX$SgsPJ -3leY=j7%q3*bq8})@_Z_B-k#f{~ot+ASB2V>&bck^4xJALFYoL2O3Leg*}O$!hKQ7DMaVKUUslOCh)if@+itrPZeClT~ -1iR*^N=_&VilHX7ezR{Ys!P3i6v#8#CnCLX(r^h#(D9Q2`wcYz#AqB@vfzGIq$A8sk{)NWEK&TeAplO -P?6fq6Q1^6a*0l)grsP?n#H~**AHt%UnWjY1bq&q0|@WSC{?>xZeNm!(&pOOE&dqH}AXz$)6~;-HFq; -xJFdD4^T@31QY-O00;p4c~(c!JX>N37a&BR4FJg6RY-C?$ZgwtkdDU9 -obKAHPf7f4uG7lkBlGE!=dmYW`dihW;o~E`ZcCNi@JUohoY@8{Q1wh-1$NzhG@j(J4?IbgOIlV{(b{C -7?A9fc@1wrttV^vAk^$p`qy{EM#ouDPzHJmWfRJmkLP0Eh5`jUu}2}!od0gsCy2o?*rZyPR2(bSUO$% -<|5NYz|kB9(b;g#Fd#^2(tThkgbn-15A&&!1SkV-;QOc(aEUs)`n$97g+j+@~e@<#e6Bez$)8kE7$CVsa!Y&$ksdzhuK>@*WHllam(J%Bz^1 -QFuJ>TBK59wcM7qX?8>Fvf*h#xnw(L7rDKHEljCe&?`s#rJVk^W1OOEdeuJ+V^6W(P%hAYhU;hjIOt? -2vJB0fWh56koK;Ps{O-tR;9d?}OpA)80<2VnFw5Vxw9d@n9FLVJJhuS000yys;B?3CXqmx?Fhd=u2$L -Ckdn)rXm$@sB4hWuO=_IQ}D!OgUn}Uj7lOnIGY#4r=RnmQ%m5le`faf>hg931HhzU-^Y`1Ea-aQB*l>FFREh)$5jY2R>#sl -UWuDTJ2(W2$w`i9+Bh+a@^EZli~*{QY3)2@XMbNRCX=Qyv-{?{i!Xhm5ElvxeI#=`~ -D?LO(6baf!usZ{VAodthv$pdbX0 -|q%mA+?{9piAA{!!stmrt0q3V$EuC6g`A+nT|BM2+3s>qh=U=AFthLvH+k0!N|r9wJ!j!=fA$q`6UAS{-Ga -(eL*g?2>_1%t|33HNce3`{^o3NV21-EIponyvONX0_bnWq$thPGHCZ|R4{P7TcWCqn9dCn}ym+Ans=a ->N`DS#m=&*%-GN%tdJ~G8IL_C>r_Dv+ba=a! -VgIRWan$LZbsL6xMVoz~81qqTbPQPn$khB?V(*t%SlRv3Mo`_qk>@hbk}Cq^~|6y?>LfkAH@&35JAK5 -1H1mT%Gd{5bFm(8}bk^PW|M^=@6rHY?DanXt&!q{>@Tx$k!rQJLg}2s(t4ScUH=DGin8&9C__68M{q(n|SlK>S*QE*7Gx8RY1 -y_%fH?47QqMfUPB}6DyjinRLhBK%obEtt2A~Q7~W)A$hSzb)&uj}Tv)}7c^_QTFovq0k$sq=1AQ=^!BI_En|a4Ggg|)oU`+0c4al`EZ^4``D&Jo4JP3a?0bO$_CR%5e3 -;C?%8VZV>f;=0;gV_JE2j!!vqIu#XFj{2k`%(_hZtog5#Zd^}r!I6FFD4`YhLjqyVzYvPBOINd1HRAuH7*&S^3x&tM`*%12XpXMC8OX0OawPi)z@ur}DW+Ps43vA!#)8oaph8K7Qr=zY?W=&dW*ZMPZ18Dt|G;kK{ -AiVjxgnL587Vp4G7UWB8xZ(w71#7OpxuiK^#`{kx*0~-@h@ox+-R-gUCgZ+yuT3l!DoqOa7ypKZ>Yg> -z|kR2?e8i|`TDmVHU%*J>b1&?5-QBhwE>Opiex9vY;4iv*of}Mn21$91#TvwkZQj%szLUU)K5W{bCh( -2Yclp_+C7LKSr6XH=Z$l@y0|F&G?qQO;cJjb*=-vK(_jaq);Q-KvB1#&Vlm%a>)MdAlWL9EkQ4GqgQ3 -#cym3KhY)n&Bg7+YWJ#OscgtVdS0DZRpX()g-PD3 -%YgU&v7i+@)gc%pnw*b)O0bLkv_MU&s$$2iVCK{?YTI$3Ud-omnn$bD)cA_YTv0sIx$}Gdd -HE%@ukz>K$yxvWLX&7GC<|s~W~5iG3FtTNLAap0_*CC#LCUAJrYs>D8_w*_}zSS*WgOg}n3GpON#EHz -!Lt<@@G_s`eq-R!wn@Z((Y6c~b9xzD@s1MAzcYD$_XxN;c0jQirMpBeZU6GkcjOB60 -4Q`YS~WSoHZvD_R*%G~z9xkChTKxJ1E^rGw9VcCl1l(C164}0Jz$lMqMWYkNE*J~={u=?%UHGEOpX0q -^cAsRY|r%$zgMlp?`EP0w$iWxjR176^{_S`Zof(V1*vr;4qTY(QrNgTe60CC*NJ)h%*`!1Eywg(oQ}I -Pr?VR6({Xd?{0URA{RqlRR%j;=CEU}SaFrh&@c(BNS=wIUSH=(Q1dn=jzMl@*CZk0sr`CVmFM+YisCo -{Pgk555Ea`*%l%j5uPIzW86SMD~Otez{h#5(@Q2izPO;ch~?nv;iGy3%%Rt)Ri4qs&7(D(F)RuHScAK -vM`S-<-DlYcDGhPL|{Bsb303kw^4`BcY)HCSI|*hkQHnVZOsfv!E-gd539!X|u&fb&!9z5|JbT+#&cS!ES;s51 -gt5Rd=K63LopF(|!6rx;Y+xYW{ORIiT95)Y&of1ZPIJh=Szb)z;%J3Lu_uZv0WMh35$G&8jj}$R5XFj -T1gnbG*Ql2T1bk&U_VmURq)QZC5GxrMj-65NRU@P$SMpAP6EhtCjA%oeASnpPuM6+0U_`>XZ*DV;m~e -PGttebj=R^-C0J>mK5*~iYJo@Z>P6ALS_LMCiAsf$_-MMkt@tpFfx#M7mC&*5ZPP4P~m&b2jzCSr$XR -p^E&V!}?2T2$W4S>G2ZU2)Inpmx>A~WXiXY@CS5Y>w<>B@Y^zD_IeX?Tft+?=%I7ir;mAnISOy@Z-CX -52MBZ -08mQ<1QY-O00;p4c~(;e)76y~3jhG&Bme*w0001RX>c!JX>N37a&BR4FJob2Xk{*NdEHuVkK4u({(iq -=s~|8{$h^QvT~vSyB+j`u;G{+D!XFL?Vnwc`wJ9#kUEWy<`rrG^?2=rP(#1`IHmE-6#C@5a_jzV{i^b -xF%nwR@FDtoMM^(A2#bR-FrH{2~oH$5(DD}2`{9sMh{VvUZud99cXzbOlF-PG}HAY1k{iZst#CJM(EA -d8KeE+p}+ElV!iMPsK`7O1s)9hYVg=x}S<{u@|O`Y7^j?6o`UkP0~)zpo`cUH-x8jswo#)9%=6kDguo -@6d7Q|Vlm`X|NYVrG~yxJ=cjTrtP}zSq?~_7v|AN|i5lsd(#|okvrs(xyAp9Hq;0Q@O^J9g&wj`oa%B -vb)sP$8OIX{C;HV12NRCW$w-`W)-AP9qX*nO|M=&f2SLjJJY~kG>zHpqpk{jnM&IX+N`BJWX@z5ySgI -JP>tAhE|Tt*d&6T%#;VS;<<-?yp>`r82Lmg)ONuo+%B^+HO5p2mDW3kBeypzqKJdyPm1~le#qdQhJVy;s&HBxYVpYXwJHFUdEMVhhn^4oBqqr=o7my)Kl6X -Hq~G!5$hTa3WDiCk5MroWg=I(OQ!LN56$Ex)$%Sw=u?%S{#1!R2nZHyW|=%D$Mo+4x=q2&kVddgENoX -F%kM~H55&ZZ573Oqh#S(JAa@oOY@+L%pYvm;^Cn4L*T>GsXGLc9d^UArY#HD*))L^eUc}9@ac(=RUw{ -O(>A%nL!)@BsmfD#mOzlU$}T&Fdu_4D!Hu=cvZN<#Rk>TmDr66wYH6gH)m$c|GjiQKCd;n-gQjZMOL5RNQliq-}*DPR8_>VzZeX5t$RYCG%o)4uZ!yn|PB_psYD>vOTB(v55wwz%%}$D0?;D5 -9tSdNjhJ$TAS*}lvR9QtW>N4|ft*M~t;G{gX`A5WNJJ~~fJish -6W8sG$bBFd8ugSml7IjG$2Z_8m-MW`q23>;K;MH&Oe2{iZzCaBym;5hJy-LA9tBN*TuxCVx27d|jg6u -VYxFWW-Jm_v9Ja+DtsZ6x7QSNIi>2w9>xbVLZQkegb0SJGwqbgRgS$aV!B)Oz>Zwi -@}5%Gz$H8n7a`zDHyVRRiBp`ZeC-^$Dh_`qMFlG+a;$Q%0selk?yP2#4o&4_)5mqx`T7Ya+o8cK -yP)a-ANEKOCtgY=W4sYzTQKkcAH}Hb$zPkH9*6)wibE#`j5~4^nC7Nw~HyJV}ncwqf~ieYY=+2JB(8u -9@xF{Qc@s-9ET^{!WVQ3$tPtgeAKbp`jCV735!Zt$|j;`Ro*tF7gTWMct?d1MkaE9c)o%uou@CUtTp5 -}&Nx|u0as%#f&OFnIJ8!v8d^_REmQFdDe|7SjqGBB(T?&X;tN=d>UZoQg9Qmckhmm9F7btQ5k))&75r -}#qp@Dm%L^H<0=@|x4M4>j#62dV{(Ev-I5zp5#8}0E#MBYB5{t^w{s&@=l9W$w?5i!~62#|dCFs#%5w -(|Z2Z_4;b?ZgDT|c{91u<`*t-l@~zFt2c9-go7?gnWC++$L+dV|OVAXD>7vl<$U%y%BXyI@o?lp*v*Q -5nLP3<=TKF|bX^aZ=l1L5~m45$|S+jW|1w=#CR&knT1Tcqrdzzye|TY*MY0^V}?B7J5;pm7c>CE>5UD -=_>s%@;GRota}$3903*>Cr%j)fNDl6N$6|D)qt#^+k}2jj;4s|&!P-ys2Q{F!tya|sjMkCCrLlFVg{G -XsdEi`1`nIFe-_R3jS+p~=BO`YoP-EL`!ZAv0D+|As@Jtj%#zf|3_lq6`dF8I6QGKlrZG*IJp*$S;M_ -k&F%X$0j)1QBXAm|l0y3r^623u&W$gn59e-F7f(FFr;#x|4)FVSw8H-6qM#5H~sHCnuKzbngny?Q85t -l%u0iVQYe4c7TgZEa`95>$F>m~fX99q5r29V3Rl>4r3*Mc2#FtoHKdg}A7%CG29KBoie2~KIP0Q@@Gz -Wi@^W~33$Vg2@RL-BQ77znd|A1{{GNf5kb=)_<;bkXr?JuyOFYy(h(hg2(uK6oI2gPy+(*ni;jf#ynM -K2jBH>nIHonS(|i8+d&mT2L5MaT|e66mB?n=xjB2k`6gy2|KouR2;)d)#7(t}HxH18|3lB!DL -6rGYF1jBsLD-wX~;Nf@2gKU$TFn{=Ow^g2Xl+(i`TCslD9)VU)a>Cr>f{FBZ)c-nzY?D;DFDosr33<_8}GlmACBq!Hdaop=IM+oB4)ND&j^o8Pi6S?Wv*N{(TJEe -mfa^TDPYVVRY;{5HL;)7huq4eyf|DM<(7CptEq3?0@r>Xf?8Q5A@1Mz}*B5xaKs62i~PO{%STE&R&jI -`upaym&|7n2U3BqS~Z&Ruy3LSJ}%|s#P2qjAnNP@f03IOYTNFU*(`k)ulHzqDR$#bF23~X8GjJ3sKbl -%n+v1-O#o^SN2P)SYUExJqKIuYnkULHY~3$W9#>}xMV34}UyfWn{>1XnS1dnUOweg=u -&0?;&ukXDNiNQ>x*QRXb1iy`pe}2`)+Dri3s&gVPFgZr*pni=Z)gLsNEA3--n7{znBK#Yw{-G^cXn&x -4|Iixc)`ZX8aCl>?!mfXft{#l-~U9)y?4(2)gv5Z9bFrWtpNb`b!?(8MiiTIm(3Hyc1#ZsJ)4)ig7=NAXHLYZY33{pB&8s -r1i;8+UXAYv%!G~9Wc%E^Z)C1^EsOxI-~rx`OuCAYmZDQb!e&onY7BYNO98@2?Zd3QuicrJ;GIV+mid -dfi=A$)OFPkibA8O%^?f*ZH!id8?IO)79oAw`XPORXj@#+v*Yr{$W6k)#c;hiT%`^HRof*mcS!ezxfG -62eQHqG~hoa$t>_$jna?t4RD5i+On7_n`3)EyR+M5mqtg}$e)c-loh)xEilAIA#c -j347t7`ay9E~MLX#9smFUaA|NaUukZ1WpZv|Y%gPPZf0p`b#h^JX>V>WaCyxe{cq#8^>_ajtPY2hQAgM7ieW -X7ZMnvG3z|!UA!`-^Wu5d2i+3vsuWNhOM$t& -%*s<13z5Oz~=64hA>HinEH#mB@>%xZ8~fM=VcPe8AX=Vp}Pyisww^Y)*jKLS$S;uxOKHYh3ja|FT4>V -lI-3r)(>#B}+7rBX-Ysu;>DQ0EE@8$n6SIy-7s61_*#vyAlsJk5BU -5h@FagHDYJLz~naLBX%wn{J!AZ_q!5)UY3Ybl8xB=bqTOFoKlogEOOWcuOj|1=d?^&$RUu;m?yc3l!Y -91pT7Zd{8X&7^rEO<^YbD}c{&;l`_5TcBCC%`$}$yF?Ohjvu*#&e%Ril6oL+vq*}oiA=g#5H9k0&e3G -jCBj+IbzyPW50EqM$Wjo|xwH5gncTTSN`iHIG05{ufe*)w*t1W3yyPX|AXJcSKL2w{M~gAr4e91aFQU -0%F7dmFz#xtUy?yqmzf0I?If2$)z{LK)8#*KhFLU@*D(7~}ez`0VY)<@MwgH*UC8AOnCMEO}Ofc0FV7 -K_BnoK*frMub2vT6*M-HJR0aF$3(3b_lKLw^>MHUY5*S4^8x9)DfwJ1#GF>VJ->W?a(*1#WyNih=~Xv -7Rq+-3BvMXmZqD9MjsqnsuHR2T3R$g_Y{n+}M#v&3+xNf%X~zN2H+lof>+0+(HjH|6c0RGo;*TfSv=r -=1I?G+qAJK5Z6ci}o<;ThO_1WnpzPvu2Tm!X4b)@MSnO{h^{f^k%?{J>;6^|Z#JUKr*jn6MnPUFjq^I -vO#E(jku0vrr7Qbkx^t7RC+=xO2@Gy;Tnaru5SX77^SEoUGBaw-McjxKvt$k`AEQnl1w&d1YE7$DmB>n=^9_R|c&Tw}!J2+Qo}pliJlnBS@&zz1E5NdWABr| -e2plrk{(YdSPlbX2z*ivm7#PzcAARB!e$2)eogfN;l@*2+T3RE*(apucRs~@SFbeB8#J->Tj->@xv>C -WpB>*8UFqna3py*>G3OE9kQN#it#1)szq*QEItl1VK3~T|pqR?MxyNVv4UI1bsmL&aKvw0XTP{dWJBb -0qC69HSht~&H68MYZ0sWKB)2z(f^S3|=_(9YQN7%>IgkeG;pW{RF{)bP_VRO4;7>OH`^X^mr{B5>u)= -%0niL;N;kEiX7^KpewYC=wGJBJ?5_Dn1C&9~zyS4d{=%1P_LDz0)9cMyN#Mp?f9)$UyJsyF(y4WblU) -go}+5grs{R3NMqP4}#&%4Lv>IH=VgnIKfz>ez^9%4oXjjR64iQLm^nNK6(@4Q02(Xm`FVsLkxjF;y42ekvepIo*=8c&2p*kW6pS@c -F0XF2s~=V4A34<5FZ81^dC97mIzej3x(FBk0fD^TXP@Sv}R0n}iJ4t;$3%0pL03i&Zl8#a8Fb_}_*#+pzpWJx`C0cMAlnrd4w=F&X|O(x-_f -;Ize&?OykV@No34K1e($x$rdejFIiDKT(OS&Oyl2?CF+BIYS%FEw?wKWLIXL*_L_Kka%cq>{T`in}FO -75F((NKx&Y_JX0?r4SQKS+(`v@fcu#7hI=tV6{q@Ht;*qC+f$DFhri9F_KE|T5d!~YRwHKR?8mAC3V< -^!|8XkCRL@fot;6Xcp!Jv3k?x$SO_6}r5e83wt=fxq|v=R^!pl$WdbUR6igw~V22Ey4@8a}DJ7O?)DN -hEc|3NH7__j~JV4keSGlt%_{u=Ym}mjWH3>h^;8F0FLwvRVmKrJT$Q8Lr9F|Oj)f5ix$OB4*K56U=5s -ToWfH*Z@A_d_7AK}ka;1H_z5IWNIjFH%W6MsiaQxo17u%oUin^wp&+3>hlcQJ2fBt&w+@aXUwnw={E3fUed3M1{cJ@GzBm!;e{5xYbAeOeb#M6y_IbfO_TL=x%}L+-(D*LP9S0oAef_fBQAN_G@6eC8$KVj``=-k_(^&8c? -aNx2r#b$soo8|*4Da~l2aObv;w5%4y+xrv9GaP%xZmGsSUJ~x_X?@t{LuDH;5+p~HPjYZ$^(m%=T->8HW;G5E%pi;@1g*j0rX7&%dVu@027%k -)iBj%q^3b`Pglf+P9FKxVKaE6E4t5425m|;yw}rf?OD^Qob5)l -xh!fsrct{$S{JoUGQCaO8;+tftq1dmUJXJkm&4zv1+Axu -orv4~I(IQ_8@a?M+;U>oIw+z>({mVi&bdF(CvI=STr@1#m7gtZmdR&OB89Mv_MsNUo#S+@ZDV@f}lyR -H5%N=oAkW+7YEpI835ucq~l%53`G_!XovP_}}_rhJpHvuxJuEwpZSyD2yKqNE#9B!5p|kELr;|84{co -MQ8ZYTr455g^0{H6OsREkqDYulz>_jk?6te6SUbPjBs3aXGE)i_K>XeUpdx*Vf9dwqcS6dw;8K^Ecf& -V_ss_z@lDejcRV1G6lj{-ALc-p1jVP#p9df*2*p+9RWA;B%k%-xq89Ex|i}?4Q+@R*<-qWE&SlcjL6q -~(0SX+>j*hKD{O?-6M{6Se&(ETt6N!3RiW_yChhF;8d-rpIf&7Vu-FYJlR`1F=Pa -FTqgFB%N#&NOO#Gu?2S(hA*EGjm77FOa0oaelDAX~k511&W~WIrSK!)q$P?m!sZ{5!BcbXyj5>e|J!( -)ZaeGh*x*?Fy?87dcwut({UXazoluEZ+l00e5-5DbUJ`-C2tn_&2mpu4&DGblO2X?djpUFq#QOt6>g? -1A$@RI1r#HA{Z5ZvP+ILW4jBa4*ZhG%U?B9T#JUIQ%?=UBo`#HRt-0U4`4p -ygUOoGK1{CCvuxDM$DBYqhwYNx-Q)zfv#737^kxoDKSLS9%<_lQu8LnT=+~Z0vk8r@KrBgY7XtF%5I68bf!$>&6*S_f`_3ZYsT^ZntRyydsc4L|>kZvd~RoJ&@$ -ikE7T@VWku)Buwm9-w@?|>n_?WRXy$5!%ckQ@p2(@@Q^#U(KE21s2xj{pyCh~S5#W+vb$G^dNQ@=P<% -1-T|HgVs95BNDScJs=R!DMnQA!9bm3?g$Ys#pd)roB=A+V(`oZ8hl0%4s0Psub!E4(+z?RU8R+mwT6shwm=>*Vuke -2eg}m$Jop^qiz03w!_y&@TiZ7X#g>fBReJj6h5|x55AF4!(i|qP)h>@6aWAK2mt$eR#W78W^4=)007! -C000{R003}la4%nJZggdGZeeUMWq4y{aCB*JZgVbhdDU85bKAI*e)q4yG7pieB%Yl)mj`Wmt2~YqyJm -9P#Fj}t*Tn<+z3BpEwf@GRv=R@wi8jQQpws5uD4}Yto+F9| -Ne9_Kfk;<|Mlv_yNP&{CG|x7mKps2ky(=YM0_pq<-|@evofCFt0L7^T;8qbl`^`i64kE#29v97(a_}K -luG@xQKmNWMyIM{__O_af-k0o929oD>@znz5%@5{wKVHITlmTIOFW-+uX(+!fKb4Fyiv7GWi9>aU!+k -z9uLd|r}Pg$m|Et!pMGT@iQ%kL8&%XNCnrfRjS-)+@}jDAHEQ)awoF5Nv??tilz+!6bu-UdrA;O2g{9 -$%btK-YLRB*FD2S|Z#^7d#BpshWNHJ|HHjZF&NEC+fe<9lxhX{Yrg?jH4b%-qg{VX%`kcYJ@giK&|h6 -qRRFRsttoL!$qLRTXC^y|Cn)rYqqBhe~Gg! -|JAj|6W&(5+D&wTB-VlNwj-0!cxLyn=F@Az9ok3#@o$|<5m*L_yW*Z=p! -fR^uvdsnBPX$O7*V8o@Z;%Pa{hCRW8MbKHN?;|n8t&!POWMTno~uyF9$$>x>#3a@7?xFu($9Vs$ukTV -8h9c(8OAs-Rk&{XU(TV{!D~2M};#L$HgzOu-~muo#mC1>DDc-(mlB;T%F^VMqFviX|1Pll+HU5)}+UDWKz-0~m1VbZ5@qDU^QR;^0ds0tn&AqcDs`Xf#{AM`d -HN=+j$Z1uAt|}p3~_ScQc~T5NOfM>gAl5I(A6EFRDqYzz>~}C>rX_~4YNBsCac%VRVf&uO -Kb_fvsp5)Q4=?t?Ki~rDWY~<<~7z_XslQlx3r-nG`hDKd19MASG?|0PWX0VWHN>z=&BNvRdBzu%RM!zcHKn9T>KK~pwa{ -anP8<0>ntgtJoq(vJMw+ttHfJS&LM&O<|Q}&7^`w+fW18t -rtWWYjQGBvm#VT&nADF+S^x3{m7D`~6TT -VY?#IFmd|Rf5CL|hX3xoBGUAV{_db*_lo}>YwNYzlsP58)15F7eQp69peQ`KZw`-75K4c7*8mTG{I}| -9LN!)r4*utE+5ixFZak`O#WV>7GYKRy3AR4oTULK*7G#M23sE8 -sqJYlH;YsX*r|$jm8z<9NfJ(y8^^Jk>*YM5USQ!nmO*mFsF2cp&MKSFcXB&(2@{pEb6_>aPsXFO&QbozQ -u;Uv|h9WzJgM75br=uC!(}wN}RRI!o>)N+#0SOq@}Z5iV-&n8yWeJ?0C>u-F2UFPB*Y4?zlD275|0*5 -8Uzt{1r807$>8rX^caa=~SNQDz0DXSW$+vBBDAhE%DNS_Q@+y|~`U@ -8@5-ZkQtTP(_l_Yse;5NNW^{VsVy(n>!FNuEeq4%T_4^3bm>>a9~qsi4{^DNR5n+I$y?>7W7OzcKsD3 -pe=3WUWrmut^>>m=0G6ZwkxRaEP#7pKtXqzYeLR4T7*#pHOhv-!D1bh->a3X_#h#3+lf76 -)C%<)uAf9f2I9eIDOqTdkEHX?a&u>3k-3u?|Q18um$(R6SWf&k?wqd0p?aVgnm-;RoW?ay>qxGlsWe==QIT+& -q*l1VPlCNY!hi7?1g)Qqw&*^HrtkwrA4BD`bS|OY;5k)8Z7m)#)tX{g}>`7sF{vayYJfP8rs~y#*Rw3 -MN%}I14;4dtxX(1jXB^O&1{wd4dz!l4Z@3nqMR(Hl7j@-I<*BU!~?!&gm@l`TuYLItdezhk2e7wnNT}7C$*)3 -Cu!=Cw?FYQjPAC&sfO)rL$5;JRqCEYp4^n2g<>(fTGC`_({|fXUsL8D$SV|%ESRGSO;9#|qJFc{kZ?p -(YwD<2G+;aN#kR$rJs%|z(hBjoHK8`t_bc9&t^t+iH26?~yXOqDH|yGo-bd&W+}~sihF*D=Q0r&xbpR -*qrP4ndQwOR#+)E8sgk=U#Map`eJ^0_T -{q6HC_j&{dQ}RVfyvWD*zA{RYIp=5Zl+!zYQZ7K(xnDG!`LYm4PuDyhF_>RrnLo;o(aq8S}%S>PMc@9666OwsN*7nUgIbRDoN*B4T=fVkNw47cO27l%3r3$Dl4qZ3tM|I}*Ly6T9W5SJ__(5jbawvh*}ID`; ->}0#;p4SD%okyq_Rpn4#$ -w}V1L9VCdwM+$)t867IA?{Y&GLr9H4BpXu##6lg=?SF)nd_>vEt+O=F^xmRCX%oMcXkoYdL8UXk1o(| -l8@T<5Z#OrbhfVk{;%j%&&2jm-C4PoroiK8e`3eEEUT@gM-~t3N8RKe3SH=$FdA%63R^LHnMb`-Tq(={QHn+EJiR#frEY&o}o(aqVJ(X+(lQ8tM -f!}AfL?WVl)m7VE5(0n!5eo7T4V@mNQF@&)(E`#8q;JAlw9%^#zO@4l$dZ^u*3@+(fgMKmYY_r~r~w9 -sA!P=Y=1HjGun~^I!gY?06?A9NCzETK8ftC;I*#1HL#JJv$P_juU>qg{OV7MntL -wdsaTG*yXBfo#F@`N$g_eQYPa3oHF&1YhUcYh|q=neMuib*2~$ZOmxr1OPS8GW!mS`=$EKS-?QumUsHJHVN<(I9- -96BfB6RO^w521kd#WMAfbC4l4$$N4=-O>>i)!xFPg=iXegPV?;(97|G4|uYc&|V$WTHPg@kXg}VfG?4 -5y_uX>tEDwT#k)~H>W$c{@&ePqB7r}r=Tw7` -(E01=4Jl|H3#}1x?x)(x4WZP>QyJipm2BIY{?jPgO}F7+ZMR<0lqTS4syqM?V#|!f6QC#&)Ij#xA*eO -xVMGaa?Yp0^rioD>F&KQ+lvqFD~rM0g`Z=)y}nGcbc3rueJ6IvZjRkjn|~RROFecopMsNKs%rYP=`^a -ULoN^9F&eBB`_?nhBS#xHs?P_O;#ji+e0mJ2K1C=015ir?1QY-O00;p4c~(;=0Hdeq0000~0RR9M000 -1RX>c!JX>N37a&BR4FKuCIZZ2?nJ&?g_!!Qhn?|urA(+Zt^8Egw|$DPJ@*{zh~CQ2f3Y#}KddHcC3tq -@2^;@8JNNSVP_raS`8T*Tm$)b{YrMkUAOoa=FbIZ}RzGHQF@94?0kH8~#P4Zcdo9X!4RWosSOXqx6{B -88ePs3^bK!%zfD>Y*!HOG402h)uz!X!XeoYLpV35d;Sm%v~khP8MJf -%P)h>@6aWAK2mt$eR#Q9GlIB1O001u>000^Q003}la4%nJZggdGZeeUMaCvZYZ)#;@bS`jt)mh(<+qe -;a_g}#(54HhYNP!j&4ETyWT#7Cbw814n9~KLPmMEK9nN&$?H$t%gduN8EMEXav=*!`Z0Er~daAr93%{ -PoZb=o+l?W{5S#46pkqH9#n-aq)gwQE($a|k_R@%xP;T7+PCfBf*1t`kRxEi)cazEq0~VCxYbCnOi#ufbCrf72{SVo7PL~DQ5O8=0+reU<)XVYjF@*n;?o -s3+tBt1b(ArPr{F3WU>Lh%pP^$))+L}wG{_m4FF^{><78GVj5nXX9?dq^Eei@EHVo+@bRFM#cY+Wj-J -Lt4*9;it#Y!JiiB~9i1e@o$-)~*Fbp!feG!?yBj@&*$V{jwX|y8rOBaphPL4w^=@EEQ)*I+OTZE@Iu3 -WAz_A>&Z@=2hMBn_C++D+-Vj73C$L&i>(4M-B9Nm|U9MV_n6QH1j9a(T?jkO6SkO1rZ?5P0KTT0bR-; -dtN|sGpyBQ+$j0`@(81ENSCiC%8e+_n0yt2X};e2zzc=ai&5Ei3!H$u|Vda1s?O{nL{7wRb3WI@S?Sj;>pUiGeb+4!co)rbTtlg}vjmE;C@e1z!YvB=w)Wo -&FCtniHn)Tc)ac_ILV*TYgnq^{uegP%o_g!3Ktr*FrT-D360k)kfVvkbsdD^w4xX6EyOM;(Osu4EQgc-2kr;f0}fVIZ`kuuDB8peNsIaLBx_jxBs -rKER3xPdOO53F7ErPuwliP~4w|>S6in@Q3u -iqrQ!Yhz#dh8=$OA_YSL!pcJYiK?^nrw+f;d#vc&FnK6dh(4Fyv9`=h0bDjSd>mHh0`xz%BUdZX{h6QqplZhZ$rtraGpNnF()eE4EvMkrvzFKu-%T5zME<2kyQ%~2+R?u9uPv% -IFP7uhS$>1G=c0*tXzB*RqwaPA_>eBvrM%3Cbfn=!7LLXcf^W;9$&6<-v`5vFC8H5Uy -~4Of{HadEa6h?FTwbsx7|PPqFL1fpm9)p74vZhqA&-qUsMQG?hT*dT0wnWB_s2L-Y=l1qkT=aLiEQyx -Puj;k~lG2?mV=L@20OM(hIjiB>>xMs}q`EZ+rC#J;ry6&+?NWWm(Kif{fl2MF~|WcB8Z-MrGEXGj`SL -FFHI+%Vwi+-mjW7c8S|pD08_guBc$N4yJ5A2&#bQOh2IMouj>qSlnSByqF*zp97l;fX!;qThx}$?Gs?G|FN+^Xlf(I>S1UAJ?P)TRH4cIuKnfMl+jk -_>Xp;%Rc}7d7_&77qJ=EK!s@fjr@mKI1k8wzJTIo5~WTG~n -G9_vjBPJ>#>Z$t&nPmV-nN^uB*(aToK)mq>n-nnw9j-n?X9n7rUlww@E)^CFRs-!!^h7oT9>sgXfp95*bX`4Z8XXBEODC~Q*hd$2)Hz3>uO3(MQm2N;YVS!Rt;s -jj$Nx(_sZIL$eAdz@Sdx`uVIWjaWPqO7E1Qe#_)K3Y3Mn=>40jq1-&}<6n%!lQ4UQ><#nEZ}He#`BT* -!C>6Rhri>|2t&tn5(H0Nb@Q~lI84wP)h>@6aWAK2mt$eR#T2=^A6+)008+I001Na003}la4%nJZggdG -ZeeUMb7gF1UvG7EWMOn=WM5-wWn*hDaCwzjZI9cy5&rI9!75m+TzG{dIJAX(E}AAA+~IQ9AiHgGy#_k -2M6H>XM3to0YXtf4eTEceQS#ok{$N`qXE?9V%t$t!(w3Fn3M(72lKy$m&Ayg*;qjAEZTMfS`+M2mhey -@fj%zbgDwB2G?!%)wnpLG$!|bsG6&sdcwZ{#6BMZCoyPfQ^{86-}(jYG$I9-uF3T>on1ChIjapV8w!| -s%WY^~5OuQS<};wdXsU5mmh9XPy`?ZfM^_&lALK;#uYj>PZ%>RY#Xj<^w)!;m}>+zXqRqT+pRbJ0FZt -=dMk_AIF?MQt)8NHi#wcUn{?FuDoL@3AVhXbWM^acPA;DE$C7W@@+hvb*ss=ZJbMadRbW0bg0s1S(#B -;swObZPVqny(6c0JMH&=&N=nd1Nt8waizKt|R;3!(tYmt{yuU0qL@7})t=KA$_`I}d_*ZJG;Z`qC -|7e8KIG*=hp?Zr3Si|@A=H~&gjs})5Y+^`Fwm%*^_+*+FFEpJ4guW<~fW;xm1SVS{P>^9Q}aojRv^_p -G%nSQq`h7VTryQ38beDObnQQ?Dh?KX)H>q8b~X3t-~{3;zu*4bV>mGWK~I}m7Ld)+!ZNK(|?81h>6nk -;rh^7vbwjIfckd7i@C6^zPZRx-*-$RAWYoTm>R%bZSImoh)$*oHFbBSifC<;*#!JGlu5h}UX7^Mc*#B -eM#o^`GBdZNdh~5QKvN`K6#~-F%M_l43tB>2oB?k#f -R43Z>jEEcN91KNwNpGvGKPGEJlJU@zU92lqBoNHVZs|xBOC_EP(OH)M?dDo*1zrEa>s}21zY|CIZ@s+ -f1-pLgYFS8IADQVpU*K3+ME|uw~hCc{KNCxY2OA-MLouv`ru -Cg5G*Uf=54e0`kQ_#oqtk=IL(GThnM7V@4JiL~%B;<0svSfG2`#qj|L%ID^zj@ysEl+1= -`%o&_2p4o8?>nrr~0$u^l9(*fb4e4ljzVPF-?UAS$VrC;SEdt%tYk5G*Y -^+y0;HCG>W!UoQ813X8V<$zN9w!`Ia7nIMbZljZsn6Ho8^XUL((dvVB4Ha?-NuHFg>93T$jJ1s54Pju -z7rk$4A#?9JoOZ2Y+;uC0c?Uv6pra_ooVer&R4ZCrRRv!$2$HAPEPF0~ddANEt$O2QdjmPS|eecC#R?9t@1loO~Ga#phx=^tIbbN4uc`gev5AoJ2 -BLcFnAU1V1o%781aYr3*?tQQzu~|4ug2Ix|;V{HI6sADOCr#2NC0qCiJ&L>P;QjJeK^bTi0Cm|95ZVc -9xv#0Tz>$O#zC_PmDW7)>L-To4ii%l>|I{ULw-3Sg4I`St_B95|`U7w@5{5<4}lz}KeGva_!C#ojta< -^S(D=MwL^p=`z6=0)F|j9eAwwB3v%3~>J71plJGGy;@R2;$~B9UI8q;O=Z(nLyJNslj8>RA444T6IWI -Snz_q5tvsRiLjKey9g))yQ2-;JCiS3<7t5)Z@L3Rqv)f1iv6<_G2)y!os}5=>0G^8^lKB2KTYNNxM9o -M^b5T-)JY@5T}@J!w4M1dvG_RoM?OL7P~8 -B$&%Jfvx}7>Gk^uP`car|TcfvZDjR$+;M*OJ;IZAG%l%UT5?bD@2HtRIz7GQo6ax?)>ClnM;)Y}HZ!+lMSW5>e;O^s2n9Krh -J4lNpV`xD8c*Mbp`3018M;92M^q{&t7Lm+ndw5)wSyDB^Hj^X|%oIbGEOXlw}_$&lN -8k(dM}4l$%eZ=S}cy)8;>2q-kd}StL^?F^`ji|89w&*{i;GRy2|# -_=MSyk~@>l7+hYAGY7l63UdU6xeUh5lFf5Tuz3~|mL#y*W)iRTJeG;xsX|)umHq4pvicZkVZni;+^5npMd4>c3D+0|XQR000O8`*~JVvbSILlnej>*DnA79smFUaA|NaUuk -Z1WpZv|Y%h0cWo2w%Vs&Y3WMy(LaCzMtYmeJD^1FWpqacWcYIRM~$DIKeNYdmGplymI?R{7*1g*uj)s --bxly>75{qHw3d{dIWz1{;j1Dn_+XE-yQHyK6I+kU&}V(5#Z?b!|dU5`~=R?Uvx?>VmpyXo5ld(()as -Oxw9m$B;kfj5K5R#6nKR@I?v`+?Q%ZU;d6XDhO<820*S&-FL4ABU=55z^t<;XZ2Sd2>wJOW35iu6fGd -47``$zOBTNvbWt(wM|i{?8DgAd?itIRhQ*=yeZorHr(D8NJNHP2#t4JG;LFDi@N%i=S^_{jNZ^4?*(8 -!g-#lFNTsVeV -*SQ^D1_iC{kPi*`0e*i2;@g7DniakT8++>n&>7`Jo5R=~z}?oYgs-a=ik|muqup8t=JG9##X0$qJWp1 -uobPsk7a1i04YB+|AVGlxse{d;kZr!%Gg4NY6XGOy_u53lX>#pdF0|Ue#EX2^lQT2jn> -{YhQARe_BpJmVVX7qm#de=8ZMeLpcXlntnEEY+k*%1471Y0HDjTP`O>lq_VX|my58TOjc%Te&w+uQt_ -jwdhV`K;Oeaiy!Ngx*PdwRk`f)BTyGlwEUjFKFx_i9&|pOmkk{ApM|Z4aRF&BKN@0V;~-)lypwG%7ke -+k78g2Xy}3Wygo7uE)2Mm>Fc5v+}%n$3%e0aIASux_>o4Fk-B&jB#8I7RY&3eiAY&Fay;sy?s-ujfo+ -p-WKlYSMM^02c{9m8^_u)SDj%~d1JwfCmz$-mX$ShLoMDLaLq95vkJ)QVS@xA+T?@iX<#&+g7JP^xwR -bSV#NkjhC2O1ds4&EqrVSCBWR~1^AU;Jq_FMAW>(l(4GfuqCN5NHOo=|GVrZ}6kp%{=P7Iaa2tmeoBK{AG!s-sDSy5d5q667>Uv0PRSp;Ap1 ->YOqG57WRICxPJ0wt1=&1g4S(&lCZZN^*omX+rRMDf}qNqe`q#DfnjHQKNWj1QPjR0JR0hq*Ru(r?ma -k$^L&%K;Eg7=XD)#4wNa$DVsRsiUSv_u=~y&fK$iBH@J5?t7aSca$g)pKERS>i9RKX2U3WWe=01@^5j -U^XO2X@z}?*7N*WfWkCSHS_oh{3rt(I^~FUl$C@%$fr)7HWgCHh -W>$37P3xvo1b33m&e_XL&m;or4yyX(JmUBnzHFZ1|(v$z#{c9O1e;)VHT7B%)o)1Ii$IoO;Zj9^S%ho -EYO)>gEN5&8S+f{g_*G)AxjcKzkI3#$g) -q(%5x@M#n!6l-g@S}v%D@xra30ilP8T(2MgJfY4KX^a28u$Y$ -d+#31H@V#g=&FOISUL;+GMd#0#F&ENHTE`D@mPV)#1xf*=f1mFG=h3L6dYO=Af5t<_g+c3P -h)mx{uhJIk_-1R!jyoLG8RCC9aK$V2MrW$G(m8faU}CYijkcV}m`Yo*p^op+%43DOHUm9TPC#~VNFjw -%Vxe?|xB*azp#D-=5+m*;NJ+MX|SDvR(mo!^#$iF269|Bw5L|B6>2)$(pW;@hezH_9|g!1|)Z%Gt@EQ -C@DTwba84z)Y|(I={41BAQfnYP&!T-t>$%Oy=~XkMhfN9IKPV0sRZ;V -2?_O2^H0-B=>7v_PJ6%l<2jnStIv6&aof$jOq+&Qj}11ad70bsCH{T)u?d3TWVvU=8$WVR7N3=?y7< -74A5K*t-LEgKq*ZBss#Nx%N-hR{JR0cdwrXNG4X~(v;7J&_bYKTCWe}Tpgxb6YSR{nbieBnuhxX20ms -%pHASZ7C@e{r5d4c7kY!}Huzc#t-20WsG+I91!gzbyZzW8!8N%-{Vv}IZ$&Cg3&oPwpU>7IWqs~s>LC -+9p*$baJbWNbjq7i>gqd+g|uHidYC~l-&$Kt>4MIkeHQ~(xBO_ -vyZKpip$~9?%1wD%_-<@a37EMsq;1u4I|;{3k0W@W6I+5)w!N28&*{5|qBSk|P-`buAjbkRj$V`QNdl -2%Ren|d61C1|ZnNBf?qP0b*5*m!?4uZ&WG}VNaq{2T4j?^o-s}X0zkGQ)SM}G}e1wXB=EjZQd5Cel<- -Al~ctTbWOTkqXs_kNwN^!wskFAjOr3w1l|eMB9c+kG -^`O0exUDlDNRx&XgGFoxD~k~*KDZ3vXAWC%YAvyBwHwVgf7KVQe)52xE0SDV1ahpk3 -s0L?9(5I*3^W+|d28+zv&J5L8=yGJq?x&g4o?mjNid7QaAG=mes;F8O{x>}I6(Zj6hI;b`1uIm9+kgv -2Ju(DM!Z8&45Hu#HLstrHb{XtK6P6Bp%`75qzL#_M8*F;DV8i%Hd_py&525q5@ALjOBK3ynX? -(07QWVA;PoL}se%Nhio$*Jje*#cT0|XQR000O8`*~JV&UflGXaE2Jga7~l9RL6TaA|NaUukZ1WpZv|Y -%gPMX)j-2X>MtBUtcb8c_oZ74g(c!JX>N37a&BR4FJo+JFJX0bZ)0z5aBO9CX>V>WaCx0rO^@ -3)5WV|Xu*zX2#H%(rx^;n|U9>^d?jlLigJDQqnYP);qDoS`_P_58KO|C;)1X{d4onw2wx?aK3)VbwDVh^&^kT7q(GX^qX5{uq@`q^ -HYC+%uNbedgFXTahcCr^Tz?_IZL01TvK~(qXEJEyIR^@mesN@CtSu{7=OESXuq(fAWRN=T1oviR!sX7 -*c`aQ2%ZZv>E^6>Vdc=PAS`{Jkj-yh!HeY{IZBQ!(>oNeyBvPR=0neJp`UaMyz0nBxZAl%LC|iwv3hRFE31~BE7ofAw%M`sos>e(iDC@nh -zp$cOw6Qq?*Vaiu7;pYqe!u<++o0q&356AV~`vvDjIrXt3I?hO3N)sVmz3Yc>cyGK;8N{xBzG5rvl4{ -`I((SHHt(_9>LvD6Fa>dJ{rb~xH7>o1g=xivWnB1R8)amH%C^tH=$MtK#+@+U)f*{Cxhb2$e{~_#~Fg ->Rd#h-JQ-4p3b^YMkk}4}l|fV;#k0wwu5r`7E|}`-U4bf!L3C}Lbup8}sMPA2>tmYSCfO((9X<`&M20 -80X|jyR`u&56ZG_64IWZ!TEMVUi#!0hiZCzn}J2z@1{n3KZ<=B3F5W&2njc7Q4YaE@dL40u?A^?WuNc -w~61x`u+*qQTB%^?+{sW0n~vSZmq8$d9#X?Oy4HI>2xnGy5!<;cR5&lGrkUf|yP$Rs0FqNt0X-jyZU! -!K}X^@(U7(g9h=xbDf7;~Qi4nPvF25;+nVPu&i4V+iOW`PQSGEFA@HAcYozM=`hLmJM_3strmobv5=B -=5tJ4^3A$03-S+tK0Lg?`|zRo_3qunJOr?|faowc45%n-(Hsi+r^rh?0NEh58JP#i--EPm8Mv1^g-av -dPU;NuIt?;tvf=%50U?1_s`0b>|4*>l!&ATc*SeWJX^dSt?0C~NJ6q&d6GEm>NfNS&` -wI8e%TEHq!HwJY`1G{k4dgdBFGV1G(RLJL8mF52c&G>v+?4dUnH4{f78&wB4sL-JGs0+!44ZEK;D9W8 -}b}nD^P_;{e~4O9r1oOJTHFUTdii%M>$}Mgd^=Sx(2|q5ll!VR?4lKdh3J;~2>*EhJ|KsInXxS(b252 -8zn^2NeFXESLBn#NjUi)Zqo4gtw6VN7~|;$Mtx4SWJ3wn1M?9Lt2hC#FP;R0Lm@NcQyv_#e^3s+m1QG -b_QO0HC9=`A+wsS-2zB~5v|(fBsc4ufi@L+KJw}aMdUvnH;j8wGpvI8UTw(`IiP*Bdc8Hp!tHG`Wre| -@m#}1=#1I-T?Uz_|SgqoU25SXVbh`r4Ta33Bm<>_hu!<=~_f!@|)qE8bV;As$yvr2g|4I1xga!4>^?#CnuOmJqdrn_QLU3VU=W -%?#f`J_AIHDQhl#Rppv&lRL!*v&5mD|D1W&=ARN&Yu*v|nH7KTCH -=T^bW`%v7xrAZTcl8_S@wW@p{YKqS-v<8z2MYCyaDCQ5O8q%M$Xn -AKakhB!gMup>->6qof)h(ad+xiBIbsW6V_kG36*FxZ@QtOFv?BHYKbhC`NHbkF}uFupbYUF?)kNk4vBn)U80 -jZism=>o_Tvt?QPVvammNgoydroFyw{!^f>8*Oh3tpUYJ0Et#u!#Iq5UO*LMhSPA*C62!@?thyQgXM& -&6OE83{4$gJId57YLKZ3WDcT&iKWQ)E=o4RUCAB6)iUzkE?1QIO0MvvuL;Lz<23@6aWAK2mt$eR#SE#e -+IY-003?t001EX003}la4%nJZggdGZeeUMV{B_f5Liacd -WCXAUfdet%$mBinou2e2f6h)(v+Lnc`*t*V>swgt!7qVirQTb-@87D=)R_$t0R;AEJw%;wf*|)Ei7Kv -PmQ)Epgs@g!VR2olwJYsmR@9%H#r}p*k{`&L$_Tdx1e3;){@vHeg@9aJ-ep=lAvgj^-p5HGY-rrm= -KJ&%xl7GB?SX@m|^oO{GeU3``Krz*vfxp7;LKQpqbOYT -p}NqSpl5>IR+D_-hiGi5pXK2!gZR;JU~P@&|hYGZ4OZ`cOLEA)Q3FXg@5;8f^66UJ})r9Vz}lNBqGaa -zVIhcCs3qSi0y-=+AhbXAK?=Fgs{u5);8!|ObinjiK@BhKMT&e@DTMor{IUq&OcBo5K6%`~vqja?gao -{O#Id@>r1Y&!E1Hx3-q$%T!X+CYa32C+dBd5+f926mTF=ifDK$e$t31zREM0pz2|kj5-nD%XrdLsfA; -HZohoU*KV5O2Oks0spU#Ax|BWq*Tsm3kF?9(|CUHx6GCJ&mM@%e$Pk;Iw4MXfTZ~g5i`OE@ -&XWT-z7MWQb`#SCIQe4i~(XT#6$CKaKuO!TGQ6F&a)7I?Oq&A-P(L|c^CjfAljc3+QeGuX{qhI}HWWP -i!hh?SbJS`H=`%vByGyihf9$I=B))#F}c^n*}FEA^)@j&3rhn`{Psi?D*6k|<=VO7DZCuN#%wn3pSTU -0FC1>SfX#%>)SC!$o9%?M<8Cy$Bxa-)=y^$c)*4UHY-4@7s|35RK)+oY?&dk9=TNn|4=LXmmlTL`RsH -izOFZ^c*H_>Nn`{ov|AHgp|S-$`8wJMIZ=#}qF;c3!NZHxGcupD8-P-DQFc9LW-yh5|#N4*B3&?TINJ83xd40`1EGbTW!)Y8bjI&g!LB=R|l_#_O@wjMW_Qx@d~w}zfQQA0bQY>p*i2|#7N6y7dhn5>BYHd3^wzCMNo@M3QZ4m!kC+At?O*o|9c1DJ#^+N` -@qlWnF$fp3IoTYVa9pD*He+Dj(B{8ZmSIXsDg?08j)n$E{LsYiZXWUhqqj-5+4*mIq29!fpD17BruB!D>i`XpHJW*=-Y* -^W%=tun?=2^6T-CY0Jw5VLuZIYn&&b145T1?r#sQoZyWk}T#x`!5TEd@FnX}HX-;f_0(*McHCKZkzj4Ut)o4g3oHoGuo~JtE& -TX;bAiV5$hIJCStoZ9wd-YT42$zi=x>YKS;#+15cLd??Im0)Qw!?c#!eS4zI -XcEMfG1%g8t7E6>diR1_Lr-W+TE#8(Tt|nxU@7|>bg6*2lP30`Fl#ka7I0GSv!1!x3D3=;q~zFdF>Xs -`%M1Swtv;o-5c?5(jxlKI5GYdYMi?L4(oEpF89;`W1!ZJq_);kU6nTXYDtqOm9Q;FJB%lEWp;v=snAV -nN}s}(FDpbRpHm2#IEY+oBm$U+RJ&kzs0b@C&w*pbQG`NRPxuu9re}q<--qQYtY -mF=PR}%+3bD9@_+WuPjbI1gidKH)w(VN374Lu5W7XXcJp>Z*)z*%UfA0ZsmI8|L@!vSz0IWL ->o$z(SHF@O9KQH000080Q-4XQ{G90L}?xX0OxK103QGV0B~t=FJEbHbY*gGVQepBY-ulJZDen7bZKvH -b1ras-97zt+s1Og>#w-N(;@jD3EOegbU0C_isEN`n#3AO$(xR<;RC@Vi5LX%08lcY>;Jv`bzdMT%XBj -BR5O-H;BN13cW=LLZyiO^NwZFBy-;Pam({TlJ@+Z2zRPqG^+&&5~BDcC9xjtGt{idA^I)Tj*Bnq$vwE*IBWG_epC3-DmAuB`JP69VAtyi^ -V{tNdf=rdZz$tt54>-z1O->RZ=&)iB*+V@>#8Pq3a~K@Y?$}0p53>eDEOe8tiufb~ES@}3h%J7N>q^Vb20+MQ2)EXo@(wT!>ut&n -V77#b!Q>D{YPoH@W|Frzw4+X;`I?}`SR*&_WI?I7i0C7#x{RnAy1eu7uc;Ht6!6Rt7R-R69a9b7qE -VLx2q5*^5(1nLYxC?lX(t&^8+l@08a9;y50h}op48>Z9KaJHYn@3O44`93gE=sHgih?_9%-iP4es?dR -8QxpsOK$oaR|fi>*z+^RifGD}Ox)`_<*D)=e{;Lo?I;`?Jg0^?zQxyz1+v-=|u*#*Vhx0Pnr5>#}#8y -iAjz!!K7aFK6dx=dWMRet2^^2G(w8O`XoF8us4%J`kI7q4OqQmh~oSXTZg6UFHVdU+C#MKYXBfRxI>; -)9GS&@#cZHZBpZ`s$IK=JtTdu>EQ)E>+4Iji`PlB)^@E|*Oza9el>f2cJ;a+pWJDaHL2+W`=c%Az}hD -#Cm@Yfon(!k@phSM#PX2IJ?*e8H{K`CWs0$}{fh@4De+jBcJH`{Kn1gL(}jv -2e(sCaRqOrqh-h(0EEsCJMy(b_1fdB>}#G39u>|pgb6m&>x5xAEAM+mN}kW=+bmzqSm6%6**8; ->)??{l9~;Sg!|!ilg}4t+_IJb&S%V^5-LJJq0lZX|t|tx5tct50NMcW0f>EL1jqHXg;3?U-si3Y5wu0 -5zrP@{t)W&|l&Wy3Q*MZ)SaXMgqsAgLM{{k}rZjx=@Vv`Q8n{dx$%WwcRa@@M{CwzTs!0_6T1|af~^u -xl*5{p*c1rC(Z5jrGoTMq-sV+8^UM;nXfvESLuPM6e)_;%FRnf_Le=T;J*Z^egvf6l^3|TRQ`FPX?IVhkN^nwck{s5(3PTfwsS8{(=@5P7bB -PLW4frWChtmFy6LlQPt&1i}((<4xv{sK6Cr@Y#KAozpgc)7FyP{OU$;xc}ynU2>G6kav;DW+#_AA%wT -WBs*$%W9&Z9%n<=q*07e`rPEff0Z3G2P|LbI)9e4xQK(5iwCgaJqVdj@@L`=Gyl|{ZoLiv`^5SB3C~F-+_hQe9jevf05PGV8~Aqj;3`EMiv6wl0Cii#9plc+sbmI5rf+z2dAsEIszp^9? -5!_C2C*<*dT0cht$Ix3%ykKM1Q9-_ -dHxTWSV0H<07jV=#c-2YA5&9+N&{t{j(ZIxB@g8{JBOW+h+z&=lOrUybQaVr*T?VyWxK8n-CnDk%y7d -+Z!e&y*lKAg+6gVCr>PkJPDqktaV<^rZ%*26UVU$EFfSBelTwe6vsS8m%TTTuKCTs?4Ln(Z8%4iLNAmw~hwb+SNvEiYFq4(aI)%B7}VfY&H;Xhe9)ni96XUVH&+42 -S?b0o>mKL6_Tt;))shp9Z=@=-_Ce0;eEAahKI)L2QJ!mUtk+t|irfW(VM*1~&^Cu4wmPF@QTzCzA_q1p_5ED;7p38rC -=QJ7=+9ZUi09jwWKJeB~_6?n-Q(^Lco1gBeH-)3OFKRb{at$y%zvo?ZU@=I2*q_44xa?InXczkGB3=K -SnOBY}N5Nfy>STnvx@#(LY;f#`i*YYP$n={|16(IJ`MrZk3^ld&MfwzZ{&ZOEVEquPr%mm -8Zl;qG$rABPc@)dR5~Z`-GBQNxh21EPP6Kp=%@`fayiB?Xc^+Wo3f*lWWK*W^G%p8$sQrb)a3HYlQ -pqs{&DFHcjM;tYMTi4ePuw^wiJlnsalfV$Z#*8)Yp}O>4A2R~$%IFk@CaVulb0Nf@4fAj&c1}ci8#27 -zwYtZ9DgOztq-4(b>lrVyPBnq2iKBhZz8g}1%>>Ym!0%19QKB%<%Z4ZK5NtUSPiogh1cNI{V1#O?|cM -aD4typlhIuE81^FnS=Wzc!F@4~oddS=9_-2EKB3)JXhvFJy!>gRixiv<(Cudm!D3_t-Lm->eYV(N{2S -7IcVO}_#;OEKQo~1zg*1&_)c@Q7hm~A1AGEW7 -2`^T0v)u^wsn4#1k9Z1BR~@X72mR`T -13l^ur4Q^$#F62^=o%AeL}73O3|S|&uF56@6M&=2d6pD%gnRMwLVf-0h1Dt~fN5%er?ZPUBMKyPa2jm -C8k7^h$&-wn8v{U;ag%IxNDz_IZJo?4sva$;N&G_BJ!&}ocR)9NLeY`pK_S{FahW_XaS)_WLk6)>g$C -7pxY`gqvMU0F3&O`fH^u6Tsbn*r1Znk_f*j4Eex7tCQxBdW?f6pfee2OZ{Cw;24XK9^HEKwqle{e7(b -;6HCSZ~mi%Fgp?;1v@#JK{Uj%{Yt<}(;&=@ByW$Jyf;2mV;5u#6Qlq+nvH65h%+D_Go#g<7|poP^yNX -)bAYWMVTmLXJu5bPX;o?CC8Sg}@bb(4&TURuHfLL7S3}m*CO;Iv)|_rmMfqwxwx64{S7j`jlpe34#M) -IJc*R_#@2`DMwSZ(~)iN$pM%a=8pM&e>4mF_52ETC}x&0wdDkik_o_M&@7;bGa_ -l7+-W?+KtWs6+0N_h>x-){%DgkV&%jt3ZWjk+4|^W`FWxVnL+1;ltZ)L}Gqa$>2=I4ez#64saiVW{1UiTo82>9PY8{Q30#@7#R3mm15`?J42NudG -MDL6bjW89p??7{gXP4bWO_u_=m;C2)WvDr3Ajtu1GH>v&IqJkFjpR&8M&g~M<4CFx(nv_g-Hymlb*vFq_N3t>Lld#a8;N0!3O2z3_ri}*lo~Fv!v(-DwWe-1oZMgZ;v3cDVDizw -n(j+exK%BSc?-d1i^_PGi+=Z*Yggoo5UL`dp_=RDsDibXnchpZDaCBEuRn}V|A8 -df=h!JAo(kpy7i8wd%3AFTIKs`fVWMuQ%g((fF>e`Il$U_mR8oVl~ -s}hwdtBy{to{m`Ps{v`(rq|cQ5}gJS|C=|JQiBGpz@B+A^)j(~sw0;u;eW45CEX8chz_U13%#->y5zo -gSD^GpbD|KJpAux`)Sr2IgKoKWyFW5bOt}yP^NL7=S|Vk!*OETgyoP)(rDqtbTA88@wnqfpJNd9GH6Re|IY9ZCs6*@-7mzONw>G*O`~%L%%|j(Yl57KN95 -T%K<^7Y@_O-2MEfFo5q}fEh{#HY4lRy@*hZJ%Wb3n7o2oPQ)r1=i1@H_{OvZdEYlfGp#z=73SAVmQ{s -x?FX#hKPf=t8rI2Ja=tEEne%_O{%ws*4yr4nPv0_Cf!yyaz~S>pn{2}gDKOo`&5B6BPN5!*vzXaB+q? -kc>n!tMDUSrkiZDNF|3h9R{XsqfT_y-P7eR1Xm-w++Un+I6k9r3i!eu0}*ItJwp4da9mxR$8cgO&ujB -m?nI^1n9~H{U<Gj1q>yLonNU&t`*U&8F4hbhozwD!0##0zPFDJa`fFUI+Uf;(Y|~^-P6HQ^k4Ml+MR3qc -S;QQ

dbPd3PYyX07AGvS`hkw%MWAP9FHbZJ8U?NrV}*BO=vurX56-OQbyUZwe&UJ@9U%M+MNNV3DBsaj`2hU1R<{GMf@y&HYN>c9(;ojoYn&m84F5`8d``ZoOj9SAeT*PyBfV>S3~hrd -(&>?e8x9tT-9h}9dp2WwbD&hbd~NxUt{-*+6|nTA-{u_Ci^Q0)fDR6wv)AD;XqSrwqY?X9xV48)s!#} -N^F29!fcU3$4Te~=X}l*8*v-@-3Pm|eA-{mb(~_gwQ+bnuanK-*;Hur -&}{O$&Y{2W9u)-_$KJUUelAx!-9BRUT^a696!Q&}byp${BXNfIogNc^Z2&v=qHK`-T7Jj3ymW4zF@oy -kF;;S2qRi@HoP@n1Yu=Qf~qfpg_+GzLxPaOXtKy^M~dydog+TXXEp=H{p1_;X?Jn&$zWO&OFdO2IBNz -I+D`jE*VcFk!sU3IhqGl&{oceh@g?AU0D*y{G}qz@X%NiUr!yz{FIq!9@n{8w;S;PDu<#Kd|6I#5h2m -t-v_Mdx5Ci=9tnbQ2Oq>jX6ki@#bRo^IOMOr?`piwQwG~*Q*j2owJN62Eq0scLBwNO3LDqJfH~~(s<- -zp#;7){@D6JzK@4LdoDCusqnDpM)RZHE*NYGAV&5E|69OeCKbmT-BK{Ciy^W8d6^W^w^JTj@{`xf3iz -2CayV*d?7$`BhD-4qo+#W}_es5II*t+-hy(RU?!+OGB#&bO^#(RCE%3esc -v!gYKG`*NLX)+&^**;-Ns$>Qr-PLt`S=uv*~C2#wL`X4{@`p!dVKsfzekDj$9l%T+zs;p_)Nf((Om+< -`UOYN9y>>!2e#Nl=ydHDz&QRg5Zyj9=LP0;!Ut{gt$9#MIcmh*IMSgcSSF{c5hJ&6jh*2>wag$z6$#X -dDufC5hHP9Mh%YH0?Ihy?GF98LaDw%8#^_xL3#O1@wC{Vk&q89CfWULC855Pjw(BWfVw1?f$JPOny#N -R(EXo;w{%Pw09tfig7Cx*AfdShBJ -`(vA|dS}QP_QRiJW8YwZA=d7p_jr%lJVG4iwouu7JH=^x_i&0SXP=O+9y)xf>VP~}0d`_n6d7W1_YE< -Y6{NkTpUfp13~_Z4V?%qnUM1-cHr6`1%K*yAX`0VlbjW&-TK6N28rSzh%Mf&5>#S;<)2G{}eo9HfPbGhIo)sAGNB -*)g#V#bj2qTqJwk;V*Rc3)9>@a2`j5x0FsPmwEU{Z%@{2fGGJic@c!-^a=2S~`u9wCTHe8M( -&SArdZm-Zvu9M>}VCwyaz6im<1GqU`slvR@6RGMWT}0@3{I$1&l3X#c3VsVrk#s&9(R4k4mAKq$iPiT -z1Ie5*MR*~YvQO|Q(sfoYGO2Y)3G; -X{mh0bhAx}CeA`30SIIlel6`bJ}Oe`V=Hu+tW(ek+tsMK==8MxtHKmyNQdF=B*Ho9Kv85Yj|gEB8(@t -Lg{tl&_wr? -S6~0@v{T73nWqa4`^Bb`2#sAd)8?))Zc9oS3d*TJZfP<-S_p!wfgd*wF)OSTRrH6CZW_Id6rP+B%VEY -GSJ??BGYWcim(fS)7!-r-(;-v$=5`b_7Y>=wxKXh2aWV#bMkY-D8rLtuM!fil_XA+_Bgb#47@XY5x^x -%U3`27HVu3T8A>Yqt3Tq`&eqDi{9Xj`Tp;r$j*+B{n5`@pRgq=9(B)wq$CtsLRX@?!hPH`7Xj0~^T@? -}{CBu&>8UbQE(G$*5fUAfIzy=3Qj+F@Mc)FrYi3ocV6PWJMVb!@l-U4Wjj23;@a9xvyGjAjShdr_1== -Uq-z$tpo4aC|^8RO?D7tOd=brdVBv(WK0XcPe92*rP#~jTTVn79`FFUNeS`moA7!K}!WPk3K96>SWNN -=mS-x9xz}prC6-B;A{jg^XeD#-9o-S&Plz-o-l``!^3wwgUj|@y4J2gtdbudnLD)xlF=S%QwvdH-XXSEv^6 -x|*)VRc*&3o^iGT~1e{j&FWx^F~K#Y4XG?>b%gs7#YJ!hy_Kzz!vD_ -nXFhv$tMOu-?r2WLx=5ZhzTeT_U?vE7&CTCwQ1QX3c|lk6@FV6F-mC#>QcJn0vUu1QBkg)jE^2)_q*{b)*8J(6w0%21DB9$O*FM| -@B^sY6tF>YlioIo^!_2T)4`1QY-O00;p4c~(;(W`@cs0RRB_0ssIc0001RX>c!JX>N37a&BR4FJo+JF -Jo_QZDDR?Ut@1>bY*ySE^v8;l0j~RFc3xeK82MPNGS)1l&VtIN?EjHJmr=3iI|7ts-CI61@Gg- -tk@Ic%N>(CYdEc`{B@JzJ>>y=~6FNFc28~+5e~DruP -;TXD+?_}z6J%loVt}VQtQa;e|i%DaD7}5m@bfL(4JKZxQQ;zUgU=hCz1bQbAt;4b!e{am@R|E5mNRIP -)h>@6aWAK2mt$eR#R$OWPFeW005{7000>P003}la4%nJZggdGZeeUMV{B?AlhDcG+6Lto-D&#@SwkX@G#3k8hO^8}WHy3sPrEWKcs1Ud~RHhuH^W+w_xGSO9OH;q8m#+MxP&ty}e>Ld2q8P(j=ZhLXIcMEl(q -mBiELhXvC{dhcOdlM*UU)283b*lWRhCBfRF>;3%dW&nD~t_iB1|ntPlAJ8twpVX81(du_{&GG!}&-kF -Ju+ek@N(ZTKUS3PPSBw!^Cd><|9hLmz*!e*Ny}m-o!*l_SxEjI(byq3EKcBbbxI}Q>E;9;m5-_*b~8u -s*|RLy2ny#{f0jX1Q;BMJZOS@&Uz1trXh!-W3R*!&M>_N2Y&+}>RPd}jc}te49Zf4x%tj6+; -<4er*D<+1(vDWs)x=`Xd!WlSu(J?jO*}IUyo-=U(sidcUc7+vxFyjj8+9Ue+A%|eTVR{e{4*f=SDCfs -nG{P46Eqhy!npU$spK!Zf=Vcj3SDCLP{+$h!(iNkLS)yl^XkLn5HkdF=M{K~YR&ZI)MG*ak8iHZyLcI -6fim3wy_Xc0ppw>(aWUQ%(IyWysSb3A&m~3^jJ~v+CjN6i6(^@) -vB^K(-+#W}i+>>ZZ@&Jyr1b?-N1le)LR>$qck6|_JE9t0$b%<6>iqhE42)ri8&g?sv(+CMV#pL|jpr-fDhxu2=bXyK -Hi%a4D1ZoD)Z3bY{Vf|7BKZne0DMO9KQH000080Q-4XQqOyBujqT?j1EGu{mr(NrNQSCN9;2?@BO>h>g -L9S71HFbFlCHzdy!A^3ALe2RN6ZqV!Q-2+CkHit`y+)LP$3T?%v&dIk@0pJlx2hdI$FmhBtU%e|O)ylff?m@f$dX+Mif9IrFM2 -9;t%GXgj>+(%@{aV_^?pEql71Lpt8l?u}GuE*Bd)EQd&U^xrN{l~=;>v&6A}rl%jQ3a%TIZkMOeL-B} -(t8zxaZ``RoqYZYbxr8WsXQglMu!iF2cF0@)5d#NBqt(!-3u8z#8nS?6h27D(ijKQXkc{$(9M?0e@Uc -@*W|_!q-~bPLKo>=wj(ZSIr!>Y)qvB#4fu#0#)4(aFLQ`ttad_v&+6$YV;5#_Z)nzZPTKgY=eubBikL -a&eZU$QD%PU(d!Iw6Jr6Um9RX^v#V(TjAVos~_uCS0iS^kd_UAp)l$QpYh&cdRqXxXJJLiw` -G)fY&@HpsaN4ii=2o*tTHh;6R0r3RhJk^vC^Cx$)o7?4K-IJoCYFf8K~PsKFufGkdx}n2L1J_XXkpbY -iNYC)X>_J2oZ%cx?e7c{`-QfmiX*jZ9=xa(={n&paj*}cU}WjI+3l=oQ4RL+( -?Y&Yv`?GhL67?Ux;u$))JfV83?4XJKdE&;WwZpZy=-+$6iGQ$4%8AZfc -YXz>}K=U8GqQ$CyEqZ?{XWT6WKyTu7|ya=3C(w#Q73Q5&=(khy1$DXE4ox#)MgXeT}CvM*!oV)--Mu) -tjMn!*9w9>?L}?vLKXLVz6T*K9VG^Xq*`IJ|lz2{b}_%*Vw90cuhi>Zt-h2`6vGZP)h>@6aWAK2mt$eR#U^Vgc8LJ001N -^000{R003}la4%nJZggdGZeeUMV{BOi0Bj-E$L`4Sx+QWLzg7U(G -lNwqF1C5+bwUm?h&XprF)jULGfmMI!Je`*sZ$hX?L{g#hz56#TN~~B#w0Gx}HgCzNcmZ=Z=U0Rk -c-LtaHj*gD1hDu33^-QV5-&xcBEc!iFFm37#QoL_#27=VV=WKOE-~xlbCLNCEtbg8s+Q~KRF9n!?2jp -bq=<#l}aiRSH$JcB_N?0dXWtmFWY;y99_FK6!rh4}5>Slb*$muKcQMAlVjn}c6r4y2C9q$LJ$R#7R;o -Fwg9{5rpv50wEC{*Q3ox&Gwud`SceWtA;kDZ7Z^ot35 -G812&%v^#ru}BGRwxMQCLurF-M>oo0Li-Xbxsn^aVazUkh}kEEr(WJ$?}>u0|ZUT1kxBYCPG38a7DNztx!5;1f;%i)IpK$6vO -ZL*MLkaWwe9QytNDSUr?*UF)1MC};-51JQkbAbOCY1vJ~;qN6=H;lE0p^iYHB^%y@rK~{y@dAZafVPey2Iw^`d1x -AqTYAwD0o$cNmM8%l@)hAT4$O-_Kqc(1y;-6GTCj>euj=9o!6C`nF{klf-=b -Hi-i*8-Y>RTHM@zeL!|_88Z&|R6_~rQ70}$uKI?znq48ghAZQg7mE~+iAN{-k>dojzjQU={C4}6ZRpS -_83OJI2%k{)8$h;D7DPbxAhl$skT4s;C!Gl_u=#ZdNfiDKI615jvu^gbHp6@O7U9euh -oVC1iYsrURUC16;teea|Wd=^Yiavq&G27M2-i($|d?wniaXqYz!xK#hV0nJW#YTij$pBS>lG`&JlhhZ -%r?mJGIj@Ha0{`sg5vB)9}fqG(&OFug7n9Y(81T$D!XZavcHRJfN}Z1$(%n8k8q+_Bt9J#N!%y2wF?# -*wz*nCU+N{Z@ywSuw@VU=$*fFP}doi|xu&X~;!au8|#UvBGuu#sj~29h3Jki8AV<+9h*x!_aewrY7_f -K|BilJJyLdVn+`~Rm>$uMvq91GdxM`nV=ZA@DT$jq+Rmztu)>jJ?k3S)>0UiM|%PIC4xXl4r5L-iN$y -4bQWjU$yZ*QMHh92-?kS+H!u5meY*lW^+-8Cm36cEenEx-$b7FS1j`YRf>5;amS4j*>`L{*2ZdTN7Zt -RwLxJNJwOc{q)5@X~J`RWw*y;tAott@=s@Jd0+rM_bfOu1fQKgm}`cnzXSLX?>8*EOkX9~Iql=BO7zK -)eD40hQScf6_@Y+4s)o3Mqj6*~!PgE578(yUk07KdKTk;zg4^Q~DzCQ(=;ai7^zDL^@S;v%u8OXpndh -+&uUX2+}DxYc1Qn6|nDsdlWovb&sS6L#n}ys-(5wb#5CEp~MvN+G9ldI{g+ZL84r$0yJ6CzL_=rk0vs -aVZ*XRjdg1?Z5&*Q-C@i8$V|9DQLodOIrlF0|WdMelW{~PeHdabyIt0vAfU6?t3~?TFXz{!gF%P4i=t -4!kXHA@ON6A>JL%qXSW?QpJ@sJJI!edR(4ylPR%F&CsONnD=Zml!U;MkDO9>iUIPx*M53w&Ks`072dp -+i$ANkiINZ-ryF6eJ08sqVZ<=BAPxLj^wsjdWDY;^MIR(D!Pp@@jdaYFiTDiFJ@pfQOoeL#uRAP`qwj -5eJ6@hZE1yDfF@mV9fsgOp5-t(Fnxp22iwvD)?O_EQJ0CmXV5YBv0`!{ER;24GMZuWIkd+>goj)eC2to}8jkTi -6Rn1IWL9JEYsq!;8d)Sr(#k?;8E=sMiPSHTAV#Vgzl+!^;YeZFB)raA5c8k_CM-E7LE@X_vU1$is*E8 -qsD8bHV$tFTwI1NsI%5_M9+RzOkDP_jt?7gNTt(VpU_F0<)4$JpvMB+LDPTMPE%A*gSi#01aLSm7lE2 -*D90k2zXrXIm^St0ZJxGPD)?r}BW=F%-KD?WcmIq_t!a&@6_Rmd68-ujS}Pnm9QO40wq{Sj}8YA`-Zk^Yym)w!Yc3vU?x8B3W3p#S!LUc#XL#ab%JQisvEC(O8QDg9G|41W9X*|0L7 -k_svj7hCz+=W!mL=Abb&V9Wj)Rvv8DmcaF>M}6TXP^(uUU1BEB`?mE7ICg;JcH*LEX7uZ3^ILZ}3F-i -kly!<--n@kCxn`Q`xZZ~+Vuf??S3D@G1XK5vCPZ;P@)4L;Y3!=sY3=P$4nRE*Ze*lgDYsrJ_xhwv?H7 -Usg%K_!M(B}={OFm|W=>7?O^4e@%CxxR}hV#6r6Wy3(L1L!kiEB>Fzi~lE&u=X20gK<1}0q&SCTDFBG -raSqH;g1O(aSa604|^4^Ui-~+bfL%im;w%=&L0B@KA6uS-+-_EPkqdQjfj&O3&Z~w?kwjp6l;SXgHlI -KH4W@2nd$Oi27)0MPx5?7tCzI8ipUivK}YytYY?w-pTy=zyanhk_*j|)cS~mXAbYC+pcb4sqP@nBkg3 -7!xLFg0rM#k6=Iy_>l_K8RP`3uO2_p21Y+zrhcJ2`U*lM>&>|P~uF=k`I2$0t;b79~tk5+ugD48oSm0CRS(4#7VO?~Tu!T95uroXtfZB6|$ -8|GCKzRzOVz8$p;RyxZ;6aB;sEQj<p8Qe6|dr`rz -P(Ie7JtpT(og?x(#m2%FIV=7GNN`85xfuEo08L$s$55^ZW=lPrIarW+)EA7P}s8^N#ObRRzjYQK0JO=*K6ekd}YS5%@qq*;av02PK<-2q4+sYCQyxV% -8--+q>&kAS~+aAF;0c`oqKF~+y2*N1XIq~$ecD?4WK`-Qo~&3sKCyJ_5EGTmN!=ev$vg_2cvhgEJ1xKbzTbNZe3Nm7As -L%UgG))FN1w6(ek_Ym;K!#v(OMUYpg6ymaA3GZCwBH>izeB`*=OPyi|)tPqT#}5DF5^N>vTht)$5#(O -l^jw^Jj37Ne2Xo3jzaXIH1VI#;FAe8oqP*k6=2OwCy#=X0U4DHB>NZCK7LtXm1s#4e?oFj?hgQ;@9J8 -)L=lEiGj*78qSQ*6vO=5)ZkkEqm*}poAAEZp+TudsRWu5sc2JMwBAA(}?#pFS#+}S_=rIMRDdWIOktMaeJpDM+vd#Y%N%!swbA+Rcg)FOPH?ZNP073!SF4 -s32ulA6i>>MeQu0p5Vcu}z|0b@v|uL;F*Hr>@NOyzeu9usu6Yh$F{Im?!>Crdjn~pH{o066VAsmgqdjMzS}FE$B}iAb-An;a2~ -d+Hog7`gyY6;(yKl}}DUBT;BCeO*2m|s@w`ps-8@(aD3gNINt+o_@q6Ks&BO*B5V@eAC4qZj41GfO;L -LyX{cN#hO_FpS~XZn`+M)&3h!W1gOtby5_SGh#WpdHER!MdKBOv%WWB)v06$tkoSY-ffxgz1wc*=HrI -ai?CIIqAhXDv|m?@L~6pcel50iHpc#cXDK<5qm2-4;hW7Zva1q8>LmbU+sy%@M+tG|O>Henp81o?kFku{*JuDXsx;8dO_@FoDxRiH-EoFg>%<@JXB*zqtmdMfl?Yx%wVdP4dT(`yz7{ -R$`#QKSWnc(Lxi^>IHabP$`ONYUP%^FH9Riqs@#*a*n3%$XPJdB(1+1 -Itk9&9JE*r{23>xMv0G2hj6;I+8mKFJO4sH}gd=8`;#YWW! --CMIw7VD+Vm#_q4VhN^LEFNk&7giRcQf4Lo1+iM -gJOd7pdJ|ItKGA0!%DmKq7n>FdD{`$bVZDcQ9s -g?_V+cRLHa7FDyd`IF%3yU`i7LTOaF*1t*y=3$;kq4-pr^iIjJ}B`08Y}sW&GQSxlIdSFc}x+Y+H-2@ -N-Cb#T&mf~T&`ZEA7G(3&YNMja`IYLqp$a7Y%uha1N}&n@#$(XL>zO-DVMQI7*b8Zwb_7)@f?x|gakE -e2~Ch%&3J5V0;VP`lFO_rWPVaaq`>YFZAvTL=#a&V2Mz@bFoQz(+zT3&(TG>snNk9y5wdIAN&p6Jofc -CLppSCquDk~`efch_+t -@CnGchN`kzN6nOZuT!4dB0vcQi}DT~&x1S6M^$hg+87FygYq8l4P!tpQN`@*dAH<84sde&ZT4R2D7Bn -A(uD7T2aOkTZuovj%C);!W9zw=4Bo5|{1fCJj~AH1oN9Kr|$e$MAz3&(q@ZW3LIixhb_#egW%lc2wb} -?{9Hqoc!~r)6>)E?_Rz9j=JUc>f5cWS4pSn_n}yQ2w3-etN*eZ{Z18R%e*USz!!LEtaf++qo-`VGZe# -#zhN&@`roJWHu)pG4l>wHi$SdCqUh`n{`*~VH$}UJ8EyG6Q1gbjnVMP*ZuN*=+$&kIpM}QkBJZW^@BG -l6W&25*-(#*lXQ$ceaJ=_aNWddGrq^?J(lRFek`t4Bt%7i7i;)il+JJIVXsq}uY3B)6vR@+2)E2+)3$ -v?XuhpVMrr_Ap9!ZGQDm_qwl6JeeZKOa3d)T`7h`o7v`pOUdxxxsPTO>GFR#9ZcjwT0#oj9 -_007aX-69=QV^cIzB7E9M}A`xPCaXTg+)k-nvrKVPuJF`m2B*{)L38#>*5B4p1hrsbr-Cd2W>`c?|+J -E0k;}N@Diu^7>$6;vQR28X?Od4G`H2ljrMYV^ID^)b5=(qLJ1mTEB^O~tf`(r-`v9yC&Mz#lfW=DVCd -Hn@Of*S?-UcMN&3x>;K7mcm=IfwG&aXil8G-Tjs-fQzPl}f{rse8SX=SBleAB-)TQl@5~=}a*mUk2>B -cT^X!JN$T2Y=N_-dk_K;lzcTSkkGnE1agg*dO*(KsP(*Twi?F&htf`lqdG?^+cpy!X19r>jcS&Izo8 -#s(=Iv2=O~9S(C)oP8s@aO^r%d6()cR0`g6%Ku8}Cbu^PLTztO8E?R&4|{`DVtP_K`p{%7ijpy9^SAI -TW^Zov07i6_xXdLa9TXON_Q6Pe{3*uv*tKXG-bA~ct-5$n_UCi3HxV;5NciG)3Nm)(3xOyioK*1aJ&0 -_c{6s~s+IEEmDyy}*|8pSG@gl1=s=&};=@qp0*606h2CIS~iRgNTFF{`vO(+x9BmIY4x6RauQmMM{xx -E`FTe{NuyhYv}unKWkO$bg$%%KoljL*(;gpX$JC=r8aP6z5w -TA4nC)xcnMkv+l0I_dS|Ade0VK;>?KAia(sY7o>-qLUI@W5DyCJl#DKEKHVFpTg!dw?{NF+`7OFWib+ -Rs@ZHl8>FMp5hY|n)$wB}CAOHXWaA|NaUukZ1WpZv|Y%gPMX) -khRabII^ZEaz0WG--d)mv?k+c=W`?q9)Ee~3G<)w4P5F3y1Nb|&fFG%lI+CA}Lg27w?;w9Rc<(uz`de -1rM#S5+iMQlk1LGl#<&4YW;>#bU8uo+?sN6uqviE)$hx-GQ^$_3@Zh1>0tlv%JitP5%;s^U5Dk+q^cF>!3_wnMrLf7~(452E7jiQ&4kvSgx@><-pax4h8H;#D -rVTru9|@gj<%#X{37>-`?sGCyl+zR8nZ?ArRsc72t1bzMrs0<5(YS*f1ZO$Wb0mipn}va;uTWOr(C#r -nzj)oI1v3E8dKho%GJX61_A^i9bZs(T}vI2BdLTX+=bu&jaV=L6^EYitR2ErscrAB&oZ%bAWbZhWeD# -*ETRF*ii_hEdS^wHu9ktyWSl1q$pm~gRfJ}M|7AR*nR?T=P+OE$^ -Ufr2FTZj;#4r!0Zo7(4#1$uSZip0407%c1^wJp=M%8oou&ligY#WkkQNP8vlW1?CHeB1eEK^$ZB!2jQ -?u*i-$fbGB`8Lw)>r3rcI7F}Da4S0DT4|M2D!FMc$)ru}nm$-H3Z2j--$GYMhdX692gF(Z{q2IZE1S4 -WYd^5zBpR9xU0)jwtYPvrWdq)obrm*v)Um{1D_H8Mbc?rQ%N0N;WYQ)w+9wXULu#aE9ST12nA9>fehz -Uw~&gvZ_q6~>L+S*?7cHOk~ohTqW>nUO*RAu>w4dDByFSdP$K!O3MS=Y5oJ#@vQs#dH=EB5ECl>FRsg -dd;;gr>uWd0-rh=iD0wv%w>ulFwb7H3%)1@ZAp8Y)@wcHEJ_Q#FI(YgwmhUnGFc&*`ytG#yI162Vv0sd!w*$pa-Iu_Okt7V=V{DpU*1DebltO9~;!8AF4w|Bk -3)vptZI?0o?mSh}36&%fU;4Z$LCSWvXI_u50Ax;$knlL%&7nb)n$G#qrdfE`*f8+QlFL@N -a)$w<5nqOzx3E&x7c7EK;-_+UJobcpV*oHkR*^LabckE#Pg(8-X)7Zpe1)#F0Z3d#Ky2lIexwx -{MPc$V@>Al)47w1LhD5nQi5A(#7?kMyc#HOHZaWC|OQiftlhTwWy{ufX(A(M(3#FQx*Xaa(_o?xxP|p -IVSo)QX0P6Jyo>K6uN^?2wu{e_&p%%`!BTeuj -!^ls-72>6e&pNM!grx8=G+XankMC-yxi!Vq)6f?kBU!$+quLwXc2^z%gjm40u0GOKVm`Eb8qT;AVs@a -soN8IZ?&SOml}7z5F4&>X^B;D5noqW@K%yQM!iR>KjDC7#P5=+L;L8~md7h#Dkbq3Zvot@}npL -jw_4A^bSOid%{4LFbfRT*N5ZJ-B;XoHDUJPBbv7Xx(7t1>j8$qgu!tni>aoqf!f%avBt$5*8q7rZWLX -nLq%19!OVG%pAO=;eoL4M`$;mzp2CX8u(VALoRs{TanYsYUtepCRAlAjT|6c95Cjnzz{(d#~H#UaaZ=}d_9IWvZPE425U@ -Q|+z~uh(^78uoAKqQ2H<$0P-&}o6Z~lIL`3bXuY0en`@Ykz%Z_>*j{}xCnEq>pr9@@5_yV(|N!({`m5N;oDQ23!CI4E9uUgfw -FfGzqDuXmtv~@-G4mAXKP(r3#vinwaoJ`y1v}#K~RO~abT;IbssVR=x3G-mJ{{Sls*eXn~Gpvx|W~jh -{s4=!~#PC~J09+xBDTSI3xn;Omvj7NZV3Vj|V^)m8Ataf}!MK6jdJmAb*H(_ -_OUqJ7XJOP3^GM!;WOx4ktuJ*~;6mf91nbhQbDcy;T#3!V7r7cz~Kt?BAci$8l1r9xvvvFFYXPW9~bV -_v%4j#J34cI8C!;u^tQzZ=W(f}Ca&sRUb`SAG@qlJLfd4)#H-UGmAW|*c)HUh5Kqy?A+0K&@(;Iyc78 -H@UYR~M3sZxYF8=FKjR$B>w{c2lw -&@SW+r#umWf{$W`RPErtN%l$Jz+fn!-LYfmb$>zbP|hVO;^0T-sm6}Gx8w=`ci^QF501V9dH$HKmQ9P -|{OSW>N_1iZKGO!3AtdEa7%rS-GVjaykAFxWT~1^=*zUsR;ROx)&@UaC1sCn-vfMH0-Po$wEloi@{7< -O3oO&f-7|lzspu&{32R%G5$__*I83;WHJVD7y;4E88agLtMeRf2Yt6sH3RQe83$(tgN&(dz;V>Zlq8<@Lao=ZPZJ^*;ofCaY5$NDGDd(=(OQnr;0{HUf?Y%YkN2pfagI1IuixiYV##upM5YO4uzd^nHoTfgUAoeP+<~)2dKlAvmvB#zQhlK2_$92CTjRx(5 -rwls<2Io)5&S$*I~&fDHi1*5m7@l_5K#v16WZ<+n~DZR&2f0!`ZWwKj?>PycS#s=Wm?{*H2=_$q>I&(jf2E{7v1ZsE2Q=q>D3Tnzk-_I{A;MJ)Ce1faZaXCBQS@wkPiPH}0ujG2~yELGj&=vKIt!`ey56m^aZQa8N%m>J+=~aI@~AvnCYJtG7O=sxu9E0V(N^F^>a6nKEkVW6w4w=K -tITrpU`Yh(tU(Em^7YGOo$bnOAE34ehXVmWiC(>me>ZyWd6AGzza=(4MKB-iRIYIB(8uKBpY%L!t>Yj -ah!j4@g9lFn4&A3N+nhQYY3bj7CpYpX?vGVqS^;)J4r`z}Y9F8F3~@bizK^(6aXgXZc#a=%I(yhunQk -+?4!6tu&{53Va`Tg(y=m-I+PnX}EhRQDnd#68Lg2a4$#o{>r5&)U*eW*v#rOrUU -5mJ(BIhW6X8GYe%Inv<=?x%Sj$sO=wurkw-;;q1`ktuN|_N!|W=G$)oOR)d|LB6&(RPkaI0imMD44(> -2qPnZi?J#wtNmZR7;l2|fU56aiFJTvJQ$hu}h_@@KhKZN3?9m27t$Ruku_Rh%bg|f_6@47CKb3%QIik -!KrU!$?)1yJW-+->|Rk`q0JUy%wTl2MDcm#BOdJcj3LS**X@5 -ewt`{6Z=aT23*s-82UEE8n2xrzdbET#irqik1kNCpkfyt#sqGU%`8X$ZB;}acoUt1s*|OuOZ5=(y3ti -n9AGcQjO`C4GbS;paJU%Y2 -k0>FFH2NJgl5SW##}R&n%YV7o{soyD!ydrH71K_0STNSh90DLskvw87_!UMXxnBq -S5-Z{D|c8!UZN9quhtE&!EJnaI=aysq5ef1b)bshfAPVcJ>o~*L|=R>1Nw%i$KFegGqv}>*F8J-vZG1 -_%Q{x_3}2F|xF>!9DL)f4Glvf^mSE9f&?h9vG2f+=8a?J`;_{d1Kh9so>PRKb4Srs}EJRMF0oY?2mqa -Y`@3Y&LpHKA+KhsvfWqJnkexf0ZYmquEV71GO>D2C8t##xp3HleCf2G7>9z{}HZCPhxDdl|&Rk6 -fq4Q(w-AhP(BoH*|557G3i9U+#VO>nabPqZZ(z1?ZG -bFM)9K#^3v>f-9gBc)DrsZH-C`1!VOT7eHK(V<&y)bhxQ;mSO@d*!dmTpCrwI{cmZT(H^4^yY+>79|XZ;IZ=B>vCwDV -8VuWb}LVYLaVTiTBH8_PpmwSuRYHs-mQ}7H~ -B`|K;N9azA@Cq2^yP3yzFQOD$J&FmQCH$!TqC0**I+ZM0ZOTCl}pJQxg8L80{}OqEQ}-4QuW*~x(5FJ -n0|po|O?`It;!kaHy?&HZW04a1t97o}hY&Su$})=G~cxfYOf$<5k}n79`z76v+2=7bolzf@|}*DFd>% -Wqj!ebhH$+}f~!XSiJ~M$H*so93;+#nBb>3bm^_7g(BKe8B2Q_|a+EQk`EG>6+?b2k67;_4S{#Vaswv -&We(2o>C#U6Y`u?WkxMCGs6&~87@ZM7$SMfT1_A{aE0r>b>8yaI3^bg@o`F?Bu~HnAsjP4*)b_uL{q> -Oe>HTTdy5D+J%87>Vh?bT7@Mie_QXcId-N#U=_+{PAO?=NczqMzMhA=;-;wWvgk_NMiVDL9J$mr=HuY -LGXiogfsJ)7Df1gu0*=Xz-4atyv(}&4uOdj_<_ZWOhl8|9v#|_Wy+PS}t?L`nBV*7AZBWj^Fk;?W=?u -Lt|i2ThOx4gkqE6d1|g -jkU~qUnlq*@$tJs&vtPy>$NTxMIJn<0j|H-X5$4;<%&ex!O2;*YuVVN|u7k8)5`?>MBCBjHpUS@9sY{ -(D&V$U~&(K7m1b*yxJR! -`c2}E2m*w2L^rN)LjQ4^J>)F4f`1Wrauri?IPX-l3xAu*FVWWuww5_XJug)AHfv?G#l7%Qh#xRafBNU -oL@h6sMcpgg_hX8Ljt00QooVod-UQr* -eQdbSBjoJNuJyjH4oLIJxPB*{Jy@j1h&x-H?Pr13_D<`Y5z|8K9DnNme7(VT;^&te1x+enc-yDS+ySj -+eFj))4@eozWktG4*%;c6BWsaO&D!Ba(y_69RJZh_pjr7a!fyw1Q2z?Y(G#QgvNeM?BD8evY*icj4q0~{z -+pwriCM{<8`(6eOEoOg*q@H%gPO=u3W^LfESn41xkz{^Yd31lPHBJECKZx1xZ4KjGI!>En*kA*|1}@h -q(TB8$q#IGV5?11)TQ^;f`g8bTrCog^_`Z4c(er9|;DT2Ef%@a2YvYRoYjoaIk}QzKN_$7MB&N_|O3`!(U$+|Ux%s#&?#w^n_5EU)o1gEOai3$FeHq -X>{0!LbdSqvE-UXh6djqz2h`1}XH)>;=wvA*iCViQz%J4-7T#(OZW{s>AjvzmuU(OM788bCV5IvIIa* -9=3QP72_`41LQCPW6^m$)Ii3Diab|1eUNRVOZD~$svlZx-bAK@_w8<+>1ScYn`$iC7t%Y^PQ`Wt{uee3JTkOLw)xPM3mY2Ts9v34Nyx11QY-O00;p4c~(>XdH_p -U0001V0000X0001RX>c!JX>N37a&BR4FJo+JFLQKZbaiuIV{c?-b1rasJ&UmofG`Ze_FT~ups+GP8$< -;pC~3-=|G$6*Hp`aPQbN@*g$_`J<)t2scH*1-GZ9*mYV(2AoVfbRM)?f`T!O8zsV`QJ?77H)jX><@T+ -@d7A8~*OP)h>@6aWAK2mt$eR#W1KK|^)~005W{001HY003}la4%nJZggdGZeeUMV{dJ3VQyq|FJE72Z -fSI1UoLQYombzF<1`R{&tEYL545K?@LXw;?)DC`5^yVVa1X0h*G-(ps$&P+X)hrDcV_IQb=oWt`jTdT -e*VTDPtr6^9$9{1dk(o)jtM2y9+;HShz3P<%~WBN6zvjGH`+J|4=Hv@X>^S?Qu5pht!%FX#cE!-wvjx -TxUQk7z4oo@R`6crZUrA3@?$ayc9=5T3gx&#S(ZzY?U?1;9>w5)A6}EB|MQ?q4R=w}MH*?+6{NK;TFJ -K!bJYrR$*kRy^$Ki@cBV_0N%9qZs)U6?_@$r_3e7Dr*tIIJK$lQ)cI-fk($Qi{ZBQZ`(5-7)x4{5w_@ -LNMlGjm-!V_5(A}kRWxwcMr%YGTwM`#SUT={@6>ovuD$$X?w6$fn!Zb(%#hn(0;&O+EmvJOqr_`{Jaa -FuTN6+SqG)dH;+tYehwBwWy0TsEXvJoiOTF&5$}#g~=0EnC_J6s2YdT$JP11Le#LZeU>>o{LiCxLGu-4dlQAe0XPmSc_J8+>7=&?mLbl9A=cZ;dDG%nG71K4XRdOaOnWP`D~7(@m^jc%9c -zuPz(hRu{{MAnLUd7R>|H;P1F-rIAAe?}5*1d|QG@S -pz=K|@yqD>7cs^l2^v?- -Ql^1>YwSkQfn!m1D;Nw$qbjU1Q2Co*(9d3Qke_dlF2*`VBi+*-h1~Vn@1jYV0rqn?RSM;yafF|E0kqG8~aSHXmepGxcT4+zT2>DN!~G^lx)G&Um9)@QJj~kt-UUzdo|?ZyPj=DE88O^pUm!+a2t=!!g?QhH?Ip{RKnt -^nBfoVBYo53xW9kKp_mywnh#9J^h$>YekLi$P6{Gmm+x9htD9IdY{kS59E?9u-55yu8qM?mkoUSW`ZM -kiF6EKN0cs?8b6zGn_@4v0h$jIK9Vx2Puwhe%)xd57I+#5ScJuoDw7>}XBW#phA6MXY|h(spk$OVaA|NaUukZ1WpZv|Y%gPPZEaz0WO -FZLVPj}zE^v9xSzB-8HWq&OuOKuSL~3nix>%rqv7SYdPIn6wX)wv|KBR#nOSG+47PTZ5k2lDF?>UF0L -|vR@+5+7G6UXGaUp^jkihV7lWo}?tE19<&r-#@Z$@IDBP)w|ohqJWV!uzT+-bTnUr(_po$5vbQ_huR`S9` -Mk00mk`d=3pAFi*bIQgfH*SZju8mRdorI2%WRn+v?6t8iu6x*#Ak1f|fzzUbMSxKL4)vbj|Ql)#IB++ -a(%gR(Kc9CW~ZgqzCGg{6V`~*QjpWj!i=9y*~3)oJ#91S^B?6Bj!WEo0`XGD74tZmfP(yT04UdSc8-i -ZU#J8r0P4I~PAj4e@7ZG@%5Kjl8fps;i;8r|_Z>Jsw3q^wge(I>^khSh2I3z(;v0@)lgSB-fk--`#nW -FP61i312`@EKHjcNXYlWX0{VW#?>lxyDYG=38Fsbj+gZ+xaKAH4Aupda1B*#bh30Ws_l}I6c -$>Y5mkbf*NKY;VIfSEaZ$l=EFB75v704gk48l8YI2^D_=i~V2(SaxGNTpbW%UK}Ci$WmgD4MrSrkxba0c-_bb6 -yfZ@k7?t9Ipl@xwr8q45gOyj@z?9FEQ%mwfJg#+EZO7S4>g6`|#^9}7ovYgq0=n|ae4w_h#qI-AX5-J -Z!fO7LeQ%VX=oYrM9rZd7QSx7S;*Xk(a%?mSra%7lpemZSxYSpL8f1~)?BA%8^jX3s0F0uP0(f7rg^s -70bFtBLf_HoI!}$xiD!p~(+Q3uXo~Du$IR_x7AuQPg8@2a6Fa%|NQFn&68d@-&lV6Nyq}sjY0o-=($V -MtTntjP%bDU^qMxo&~D-A5IJ1_#2xuDQU+sA_*sqK4H>pB~JR`g$d?2JIeI&$lVI!oad0#39#}eAOGj -d&Qy>h{s|_mt;1fYe90as+p&#f~|Ld51GQz9L)iHA~Rx2MsUQk4?j8 ->jIL*d>ji#qHUPoTB!Fb|`LL)Py?X+%XNjM#hj)$fJtNz}JD4R0=RI}j{>iWcCwlk3p`8ThuGk#CDT= -)9W)TY+eE5$J0s(G~;YBO#iQ6i3e(JY=(10msZ{;|TO{g>v2P-C2jBe6%3zCP9udnvxDT6EKfSG2r_Qfu{6(I -Ny+~ZxfxgmMp39%AQ-rY7n|-a)D4NcjOYI+!xj!l$!!Wh6{Ot~pFJ=E%@ZzpaFN?Y=ITRe1N>e?dG8i~`gO|QRZqOW -bVYv)MSAD?@bX1gHEVh<1}G%+;d41u>kbWqpwT7N$?(+1%P=djUf}5coOQHfyPWV4(>{yAx> -cibFFkaGZ#b?Taoz69$un7cFk@iQenV4YaB`n%j;rtvAP~4htdXS^et675nR7zMiwftEM;f`!O$YCS5 -mlCs{l~U*KE!Q@D6sH$|C0MSovB){R@48Gis(OkKyRwt#Nd{yoP+(e?M||q37HL`UQkbo|1X -z2Z0J-NM-vTIM?#Zdpp%PC19Kor98=X{{fFq2BNi+wPN-KI@siA*2Wle -+(7ydoILxvfo*f@v->YF_V+zu+8#(G0%D}`Mp)?hwYKMitLjWUAI2{9v{`qUz;mqg1BKInjdZpEd4r$MD{dhZJJD^N=V1QY-O00;p4c~(;zHf)J80ssJ&1^@sb00 -01RX>c!JX>N37a&BR4FJo_QZDDR?b1!3PWn*hDaCx;5&Y5WV|X41t0TUbE*w_Y!(5yO6NGgkThV> -}VrPT79(S-|xs@QMPV-X+zJ&kg3a*yR*7}Yc}qn#+NtDWyZyDzVfTBpdD!iiDwRgV99Pm7)d{QfRGB6 -~tKTX$tIn*&(xS_UZG+5Vyk7=rLM0MHV4Ww~QZw(kutmphHMMy$oPd;f+LIVTNi=XjNm{h%9{attEG6 -r(EVCp@D#5IUP2e_al8*n`y63lAt)9^E<^nS#7|B{Vv1bI5R@1SM0-;8YvPM~Pv;|crJ%qQEOiaQC^y -^L=)xw?(4r(3d%o-aoUZxgBlfxKZkH+V~OiORU?7em7kioYf#LBIc5f&hp7sBw+$xCW>wABAYsncu*B -4I;qPN$QN;FSy0cK_PMGOzBA?MnoITYK9q -tawV&SJB?_I}7wP_}6oT;W>ww6R%FdElB0AgHWvUMTD5ba>jV1<>2*P`|pR>DIdrj8TP4C52xv$CiX$ -2|5|>U{3kYB(4lm_3rFwZqWo~}`m`@{fEvGP%{!Q}Ht*TSn`v5sw2g0A@H4)(w*ne;-pJugJO@x%yiL -2&c=c1k(Wo=ZSWZi>dPxJEEj5_F@&5o&O9KQH000080Q-4XQ|fU;s?`Gk0FDa)03-ka0B~t=FJEbHbY -*gGVQepBZ*6U1Ze(*WV{dJ6Y-Mz5Z*DGdd8Jn0Z{s!$e)nHNczLjQv2WR903Xn08Bnad1W9j)Lt7ZGE -vMQP+mqz>(jfo)D9Mh0=C*Zyu`H4w$&V6Ex-W7rK^%ld%~F!D@`Xo*mGXQKc_E943j&q&_lT857@0g2 -G~3mZ8!Dt_1O+YpkY9f+K0*MtZCl?d1UQN(!q^EgxPlQ$^9;~4mq!tETFBEyErOvd7^?vew~N -j7`gV&Bjuzl!W8P2pT)Ttn2@x-Ba2)q6IUdhT^(frUa((k9aA4x-&+zkV^Yb>aC&y#+dH4Op>14zGea -E`@X!V8C4<^m}AB@)5>+tH+?W*Q(8O&fZgC9St3`N -<=HrzJ#^{5$aX9lc)$KThEw_tibEx>A&A231T6?wl6`HF0#0?Y8O>C|U1;GBOw74+#<ak4 -BcW|0u86;=hSnxa#^-FdBk1=xWK!lCY>GKWV3r638Ud9&Tr-q+npnbTdCa}sj$S4r{xOtNQeQMbG#Mv -gZWq9h5i*`u9s1YxNQ-Us-YXJmfPQ*Hb4Uh4_JF>pFu+McJo-n6&-*xQNpc?)bIRcu&wA$uBnFR(|@@ -RxuB-IIF~MN(id_gKf)El@uZ%~B7`{qMQGj->Yb*3^bzZ_e!X|0brJLO-+gt(lFWy*afWmTPsB)AI@U -v1Jf0tL(xZ&&$I3VcN~#ZO%I{GkwmdP@VdDHQP)f;ye6O^UNJKh6jBUaL#Bh0$!{&HVi31K9NDld8)T -}6z>_BbXyHkZwS18!?D5tlN>~A9?+`wr9_K6tdb4y%)VyEtSGv{frliH0e2tH5HzjR^BY~QH1AZv3_I -wfp8ouVBqWpFQ=*=cuyN3GI$oy8kn3ZB*wG!;^n!X8bKGu&?tT@DRHrws)i@@^z`Uj=i=gAShs}vBd3 -K%|>utgW=_P+a0GSZZ{TEIp876HnH=EUF4qx&T_N912jI1+#NU%^vRR03q7y+>ocKsU|xg3E1!xY>I!3_-SNTUS}sky274$p3yb{E$eAcG51od5CR -sX2|)?2WLjbp=P`xCEM+aYBwp=PbSPa(XMOGHdKhoma_w4byH_tP@>oW$jX-LwdPoZ472jFI>zL>5KT -;O>ZV{-R!$UMUDQchR1M|uEZLMrvd#oeH|%i8s@(8jsGp};M3XhG0H&M@!*Y3dcXJn$#r?&_bqy!(CNlzioo|0_S6c5zpCL!ZQ`4~_j^hDH -jQ)~;GM_LTc3lMV&|0R*jFIkETjwk5=eZ>S2%*bb|SRT-q324tYMb^-qJQmHKATpX-AoW61(W` -AG*YEM;b@Fn$Zg-aI<1-5hs1ziCd|Ol?rUi%K$Z${)UTsOmDoWUvOsB1&Jcdtg1Z1C11^5BUI3+clhU -PKJ$V0)K7D5sQ&Hu!$#Z3^93W2#LxDEW&HbUXXAWG0TP(>u<2A=ZALQvSLEXjf8V5&`~;$WZEH -LXa)Brl#Z~8K7m>mQB+6nEX=gD#mHb>HpdSJ|E!7;K^UOM!yy4g7eNN@$W4L0a^Zn!lz6I;jb&{ -Z7+~4r&Hx5lgS<~hLUmt3Hix| -6SHqHQ=8)VR9$T@2r`ivDh782R6D6kE(1bHwuTO?9?Xyf3ei&tO`-N2(8&~QDCG~IG?`zW^DA?V)OhY -~@P>S%P^%r_>ejB&E$MWFcw)A~&*l4-d4sK~-@H#S+q!Ab+QDQA3rZb|T?h5n-CFDc>@FNCu -uLSz;0-|78W38>&Z|hi2a>a)~477pG0jD^+s$%Ia`W>_{V-eoHFnWYO^4cL+o21d$h^J -(&B&UuQl?(hT`#d+UG*nLL8j~j(=MIkeWgd|H)IEV7VD-303L@yC>!16esOu%oh|9^_Gp}1G{fiXo8L -ZO_bgVmbDC*!_2sf>>F(OIISKNqTTWZ~N9S4_l-ZCO2`q?5N7L5Ba@ASG+$OJdU6?`x)>e?t1bQi_?; -x)c*@DR;)nTP2?3-0bPtp+ywjGJ;sX~c>zHKBBBL+0#kO~cXVs;Tm%O2^Ki|R?$dyat?;xt^n*YqKVT -9>p6ZF59qJ|`b$z9_QEoLhxw&}4R@I;h>`T}Zz{$V0Bj&Q&4;CW4v*kbtVOhNS1#_(9mW&DV;oxSSqWT6sbe}7wgIiGQj-3l`6+ktM716ngwMJVDy1)?GaHJycP4eP{La>E%i)=_2as -M*zV39Ylxlm5;s^$JwNmwuC$x`E)NtVmqQS4Av4q~80+KKgMUh^*gc|0IngOcmXcN}`S-Ioqp+2873f -_>WDnGh@6b}RFJ*t0+0<~tZ#a)m>Oek|y+)mJ&HdwQ1wkMuZ%Q0$IaOAyvx4J{i!;dJP)}|t?>jG5sM ->R;Q^2TZVHmQiCF9U9rO?)a65J@#!}f+;H7Y}~hpw-+=7EwOFD$rTj%w%@n_-+KWT;WisX#?k^>pY2< -jhGZavV~DM77~dNrl_@&C3a9CL>Yq;9g<4^;V9=3f&EiZxlH|?dRd4=c1CL@VZdXrpCmX9qJO=RQL&@I>P~> -bbyc5(FwqR(z2#WrTTM3zk7pOuNUuIUC!RM7@4`;y;OW`i*??^H&V|t+UB%s-+A!(>i_TdX&L6oQ?dg`u4B$E+8?| -T_p=DZ6{dFRHz9IiXdu`CMwrvSL7RSM3KZ7$iOo;5nz{DtgP^1jr5}So_NO{U*kQR=j=+eA#_s?w1s> -ciNE6ncQIo-|3ZAmjQ2B-2Rd$Y&apxD?$kvDu#bT+$O={NUk%GKqO!4&9RW|g*pxSH9nU?wFxrNZejf -q9ahvbsy|Hs*nPffnF31jUoSYy4um<)z_T4TPz8aNjFTbDj0ySEOxfzb{0r{|``00|XQR000O8`*~JV -T3!3Cp$Gr~OVaA|NaUukZ1WpZv|Y%gPPZEaz0WOFZMWny(_E^v9B8C`GOHu8Oc1*;;c1Z;% -lbhL -LbSm}daRZ6x*_NHe_bK(#F!kINIh=m%4r_8L3bQ8HLc{Mrdn07WnpL>57k -y42c~?s12t*rs!Icq}2-KeTT=8S5o)upi%9B5GrGb2acM>q|7Sx}vMhXFr(n>U(QVS}2ipoMZqpL(`x -B|ZOq8ZsK+EQ9^^clc>$KcvRwk_+*fOX+M&!J32147Hba%z~WxuN~R?9cL*Ys1W-;lgsV&&}P%#nsKt -Nfg;tq8^l$bz(2tJcZSA!%AjYKtQ&5_b8O?THcvcD1#A=&b`wGJkgZ^4!W+-qqIU7@jYmq(TYfqE3ys -VoCCzQ-AgI$SPKTtR;yLXE28)Ei9VtWr+Ba-+Nd?L8yob|jJ*9oKFf}8Rs?<=@?Op`ZyL*D3QO%T -%~&FDhRP~frQD85x&`aOC8p3lKINoK+3yH*5{?ifDD6N~XA%sx_Nnz{S%ClWb9co5(O{UAu8I5XS_6g -A%rZCUTc!05fV`hs5%?t%!eXTZW1Qb>-g{C`q(>BvObErr6{(m1%XHA(!1z+2*D8Av7;9XJ~$ls)g## -hn=jYc#9kIivyI^FmYybh<`#T;~z=$41LFQM{FpkRrMR0JDOVUhztNBT^APRIq|TN<~*R -Ly1%{ltF(wn9e+CIrNMpWVNEAQg~D2rwO~a!`F~serykMUw3{!2{eHX+Ah -@{7h~Y>onlE0AeIbTAO|oG*pnf1cN#L6gPu%8>!>SaRN6WD>FVU2N_<0>3u^)-APEc&OI>p+)Wf?HLR -^oqqU7I>oikU*m%a>jtwGGu#W6F8Ty{9f@ypIWPi(+mH-Eu(ctd6lP0(g%XBTWqGEqn-Kbv!r%qS(!N|d^kMQK_kaiMySkYB(Ab -t8AI0lTkVeReDU$eZoEEVdx%pBbalh}b&jB#Z3i7L{gVsA~0%{!1x9UK^!7IpaBV}-tlJADDYXqnOnp%3-j-f@rbmj9`DzzzvuU-RY80A%~{47TkqSk(YV6-TuX3j`X&MD%rYLb -zNQ-s-rbSzc+&wPv-Ay<|KZbryNxD+6I>NR(twYi`yh3+e3CicQkHj_trP!Fg?|8$V5wh9Wks6k|o^;uOw6m@_og-)Dr|kk@Ni6#Zy1DrC^QY_IFK#z8AqQxWw9!-D-lJcg -QVqD-^GlZGSOJ+iX6cM_77g8Y`$pTAZswSC1+3=+%{$3OY<{oZbFg5*M(h-Wq&7xx;#0v1=Lhwj>BwH -y_PsuZ%)~OR)~O#pHh$t;hz20E5v{-!`TpI???s~tR_3F5B@oR54HR$&VRNm$KMS4CrGRdQ9cK -X~d@CSuR*!%#}0>;bp%jnJxu!7sr1vv8Akv{>QsdRdMU}@P&Xrq25?`>`$>5vbT#U|GA$nAk!ZlTf+e -0FAaRqOlBi2JZ|Nkj9qR6arb!dE|XPpk%jh&kWNKW~~NIk9X4MCOI{0>^jGah-lo)t -^C-}Z6BOOP%gGm}HF^6ss1Y(Z9uVjk^M^&g4W=3?ZO2u1Rv2eAH)t{H8W?I%+z7szPu(`=Kmk6RU_Xd -9fp*k8Dr)g7KdcEwMLHVC_0l2ctSnOi4!wV#;0Q8rzZ~R*>6??}vy3W`cVq`kF$5`$PD(+{QP;@g6qJ -Zg8&DjMbWeNE?Vcvofz=w -wK0}dt%1ADyb-w{K3(~OZkqt&VLt1Q<2_G1cHYn2I8VVnvEQpmbT8RdP0q}y`;wT5x55}T~0x2tZRba -OT4_Xi^_N)VsL1useawrO(Pe75GN(U~tT;c@@}P)h>@6aWAK2mt$eR#UyLkZq9 -#001Zx001HY003}la4%nJZggdGZeeUMV{dJ3VQyq|FJy0bZftL1WG--drC3{U+cp$__pcy46q5m4+a8 -7iK@|*0H()?96ie5aAYf_f*kVJIDoHt2fBg>S7A@caW4Ed-kF8k86)Z@ICvp3v|nr(9all8L+&eBdc}6)NOPnlp^~#4*V1*b`98^cE~_I$JAwP -FBNbTB_ZS$!ZY(2R^r4!w#gq>nTg}%xz6e0Zg{dIaS6o+ZxnX!Dz_+zjt?k+~1!P!IS;zR1r)PmU>7$tg0J~}?%xLi(xA_5i!eGnZ)TpjBJa9)f04 -{ZO4-^WCM);?W7~?(HN -_sbuyoT{4sPYj`|DiPqA_i-~mylO@(3U)ea(N`LPl9$!9zq0G9^=)>pXd1*O)H%!>m9sC(d&I_;z-@T -#4D2MxkP`FJp|3E{b^(#;i_OmSXI%$pBWIF1zFvazm;?RO&{FyMiotAH_89Cf*zN+TSgC{E-%l7nB%p#{_h28&~4E-cLU~ -@dheCWgUSkDeD+cS+kMk?9frD&DV=NN_Dn#UY33+b>NA)Q$_FB=4?%a3dxEOKFgcs!X*Wew#Mrnqr?Dnh -_ZUw?tTIL09^3sjr#rP(R2DXP;#dxkNJzcEP11ALfWu7}E^fpo&eq$p7I_}(xH4Kf>-eR?=2blOmsv? -r!T5i}Ix{jeImZAExbiHRje@-1aN-8hSv`|8Q+i*F>sMQ}F+yw>2`7n_~ApCnCsY?ChK;|oM~(D?*dD -5SC^8XvwEQ}><_)v3K6^v`~I!A$}$6c~c4a7A{`HxuVavT3=x9?N|hqP!g96KTO!0Ncz2kI$JHk%1v~ -)?cjpWI=d}5u)Yy=SmctBKmwmLiJu1!MQXTQCStIYAt1VLmvM$HF&Em{=}=#Im~ef*&m_RKTHuCa4Uu -n-);MgsQu5{@RoHHt#xo>H{YmHHVo51J3q}_pa3^H!%MFq>zznbbIc!uJ-Zlz%fpAvW$%*o-8Wn^p`y -<<;0*9N9~a-_l1o{f+iB&aT75lN(6B_j4~>+@zpxoK$XbEi!mxtmwKU+#Fn#h8Ht#9D4!x6M -OD}wZv*P8!0{{Sr3jhEh0001RX>c!JX>N37a&BR4FJo_Q -ZDDR?b1!CcWo3G0E^v9RR!xuFHW0n*R}cgW$f&LKY(TwiHbv2*DX>j0n}tA2Bbi-_WJzk1ZIJ)oAt}m -|Y@ShQoWUj3f5hX01^@QQB!{ED#i?c&CTuI-6a#H!LV?LqBQ`2Poc%tC6mE%VGF#KW- -e)4*9d;7^kZkU?Dg&Y>i1uMx+5j|OwpEZ3ANw`?UWMMuNp+h@WbH1uRb0D1pSg_SuL2ghf*4T-wsX>c)Y}Tg@A@=RzI@N$1I6~zXoog2k9(nmFbx)vS2o8F#|;4EzlAT7^xBI9eM+4x#Lej -EQW$?1*~k2AauFBbGYR@tTlIpmm;K~5{d*D^xTiHp`eIdlcWO~SdZnZ0*?yv$B -|nSp+hc6a)3*228)#FF*vUAJjZ*m#Zql^ifi-DNfPHt5~^nF?U-PV|aff+Y6F!1=LGWVC=vyhCQ_!_h6Z-faK{d3{V1X{DpgmMz78XL8@-U^B>_U>ND&**%Xl$f)Kg -<=9Htdh_=nB7(Yor48g%}{cIkii?wCP__hvhGM7tj+oh`nb9j^RzbB{0ELwJ#JGMGI`KJg{GcKK{5IK -{-<*dz3O=#%{0MuQg|WWx6TFSXq0^w*Tm9$ayI4o|@;4A&ta(i76@6aWAK2mt$eR#U&o`{3gO005^30015U003}la4%n -JZggdGZeeUMV{dJ3VQyq|FKA(NXfAMheN|Cw+b|G*_pcC!f(?$?bC?UIWb2@`kaT_NicsvctyPvhNp4 -rh{`*d{9mi?Ayx7v+eZKp?yNhT$Z5(O1ZKT*oVmL}&*Fx3P(Z1TKGP)(Ya~(Gp$Y{9dvWL;;UONn#EZ -4%iXfSl5qf96VMsZ0CDd?VCV1;g5uF5IkayWhzVjXwA#h?=G6tdZFZ?_rQeZRci>~`-(_D)DkeQ|Ttm -y7$`?YFxPySmt2Vf5Yh_U3CiZ2p7M3R_GF26)MerdLlkBQHicV7hl*j|F?;z>s`mkk;U?!(GCd;w>bP*+ZQZO6i -cfsFgOr#;>twP|p~4XL=ZbO2Asml8pd~1zDwJbxv-yf`J78^VhU!gP)~yKyvU1>8*I2o!qRrtTk%Wv? -nHPAeYAok5Q>X(?cR&2b640kN~grpT9A!v}w6p7 -8}aTNKAifp^`B2Fcznd-z4Kr*gY7EAWjI!G3zDqu3OU~0-F4LO9K51Y -e7(P_1W#h^P-W16wq$0^}<+9)$Q0VkjSBB+9wGUBC6wmll{yOKLdg5!WRauKWz{2 -sDT(&!&hO9KQH000080Q-4XQ;EZrJn8`e0Bi&R03HAU0B~t=FJEbHbY*gGVQepBZ*6U1Ze(*WXk~10E -^v8uQ^Ag#Fc7`-6(g!DAZ1a{QFkxVO0Cpe+FqgvIbi^|V7p_pSyc7kI|j@`l=a0}Z{G8p=bMzzFyEcsg=Faeo*x0!mFV*|iKrLq%srh4APrhcm!OL%M ->`Q{__tWTiA=PWY#jyuYA&VcK`bN9*X6!ow4ApS$T&fP0BeW< -&GIT1t4V=nJHUnspj1;M|W*a -2L|$c{U6hr;(&$@65TX02C$b#YNX8Jf3@KqJ~I?Tb^C86s`%~LgFvV6N#X~*HO -6I`oQ{AiZV-AgM8I*YLUYHh`gW4Y6uZKE!_m<6pcvtvKvzxN3d-q?QR|kY~Eww4y&F^tyutM -ouvqTK0}09h_3|%CVt9Z!`=zBySTkJ^g1^U`fmt+oQ#w8;Wrlc2bN@8SLhmN}|4>T<1QY-O00;p4c~( -;`3unU81pok=5&!@n0001RX>c!JX>N37a&BR4FJo_QZDDR?b1!LbWMz0RaCwzjZEM^(5dOZuLg!Mj!S -%IYmvf<{?GY$xNZP`&EJm@lySmttE6GWhkpJG%tF5&Yn*ESnNi(CFXI>TB^;{^_^8KE&{c0z8hj_1gE -F?kH9{#}XK%_lW#~$eX$p?1K(YYc5-?Su0U#L_$`r0eX8E$j*oxzWlW$bBRRCk0mTogU7Z?V3wxFbRW -jhFX@>~VekVK;N+eoQ4$2DgFmqHd|@s=J1pJLMCEg&;qxns*&$jb}P%4f*x2;6s0A51!(c3adc^dqbp -W{&aQq^(sf#-_OoIU0*M9TA{L#1dICM8G8W~RRcHyDM)TxtQCjFjxUKJziMJl^Aeo4pM>BdM;}3wpf` -gix!~$EA6S#4OWK?BC5cqq4eGTmsTCIidCG3j{N~eq)^nuV>1ANPw7Z|p<`Q!U;I$eHEnnN)3$hC)UH -k1z*d>Cnnk;(KNl{qriK_aeI*gI?&1_zpy(L~o)IArspn{wR93XXVcb1p{R=BUDvi9`%f+|w+LG^=Lt -#sA^3Z+k<0#x{7_R^|ixFc1StyZhL#ZuaUbKXm5(U+}P2tG-YnD28Yd%y(gxDcI*5rpqBZFQm{tEvSZ -8OIP=1b*{lkov3)0`=#0FZdq0l#}QmC*gg}*^nK=O;fp)O2KKn483`eE;u9WAZ$#!LQ_XhiFga3)EhE -e1?FhK7;j4_Qt6&?K`KbyfeIp>=rqe-Bn1NPr7VnBH@h)Prs0*a7oj|ekFe)UAq4 -%WCuuBsO(iP_H;>hKZW5F|*)?@04^4YuqFbQ{Q9v>VhQ^jS+VwAB;ldkTR)nBD;O*y&d!OYVGyJ`$)+ -9c2VzQ!>XO+abo4!5D!U{k?g+HG80gM|K@u?I0n#SHyrA#V!aK01()lQ2JVCNO1&s=%Q34`J5$3N%23 -Y5CTvwNt60}vn(dgqLmo -CV{roD9w{?LtJJSR$t90K{7GCOx4#OM2ua21AlBWEOTWOTBa3H%Knr$(4R5SyXi{nq6jq{uGg>(3;Wx6U8 -6xS^-hNn*vjwtYhLRRLf{JQ&?xu^Znuv8Wy~CwhL1bRNa=aR1;? -CuM)kB%YeWXqGCF-r^$bOKulLw2o^o$(cKXW3RE5FH(b|su^)81NhWy5Bkz2Oh|bdxv7Y_sK*VvWkm} -ZB8>%ZWF8YHAUD|xjh%9rNSVr7W6(|2WNvV1Na95S`v^2fQ@4-3CasK>(dD^F(X2J$G0+X@y;V~)fvuw#3)|5C| -3^B|@)x;`218cJ~kDf-(jZI;Yg-WY`lk%GwNVbh@s}2VRlIG&$B%i!l)8ye1r4Nr;Y(egXO&&Zk*N5= --98+Kd!#MZNb;%||pZoX6i6*PxP)h>@6aWAK2mt$eR#O~ch$~Z3ybsJFby+PX2nh5jbiU0amqSJCpLR -a0-EN4w1$!I^q<@&5e$?b*de`s>-N^C|m#R(4#-rtNN9RM)!U%NC|ulzhrw6{4N8H`}(TtE@bdjkm=% -NpEUHZ2P?_GVcVKw_j -}(znokHrWTBwpnw{+jP5Y*L5}4x8WbtyxwfGYNQbNPNs&u_D)N{dz6bXA&(w5j -V>y(rpsnwLDQy6yC6GB%-Tv$hj3=aL(6&)&X$^L7dw_WU{QSEwidlyi593aGQDshcT#S;4qvnPC%p6d -I(g^UN6o(Ia37Ho-mE@HSg!ZDzOT)lJdV6=EgW1y)sxWK}nttc5jJZBv)J|1b3PxqKMvElC~$OaQ3-L -%-B&RTg>s0ssP{n=*O}{b*|)+?A##`DcqO`q=8^nFyjyHR@-zZyLO$F!b<1=40>@5);)+!v={kk~ZVQl8w?eTb0MFC9Yqw -oH!jcjiSoIcX))2je-!EXdauC3~@ovBiZI_k66z)<$HdKI)w5yi9N&8k3s{VIc)#-IvFS2q3x?2>I>7 -pyjWlv3Vj-QJvYj)4dvcBcZ^M)@G30%%V9pqQBuDfz6he&}hEv`{QZE|!}(q^%Fzj1L&VmwXZ?a`92X -g4I=EokC2lQYLIub;YHK4G(`BaogFcsO)$SYVga0MpTciy9`P?)WjfhaiSU43rTLme9+(VNo(5eB~d6@~J7p`P{Jk&)Kx&A1M=wH>IN%ahh1!?J -2&NpM{yYrVUMh#cDYosF}%THWPSSOHaEy={hmgm4vz;cP|{P*V<$aQ}JFR-pD>2FOiJ@+shFkUUIV -~53=K@nMBOzxXIYc4K>$ND$75-)XBR=|toQZ#No^jEePS9L#33_h%VnzC$AXtDV%XPp#e0Uu$7r)*JCXDq -;@*x+Y@EcC$-Vjl^9s -Rqk(smHS=ir3br!q%65)B=e}9x35$m9f8OaySi3F+n9tC;s#_2j+cE<|!9R -iJLQ()({$y374#Z(Yqp5k`gZDF=b=&JETXJOc!C)4661dQ4Xv(vS-!f%;h{SC)=~2>ep0&5QbB_1nu! -Py+Lny}I=T7k9Ug0joixgj`=n#4>vK+^nM*<`?_(e_UH@It40h6FPK#|~L7*3<5r;+?S)SYYxLOz61* -o79gV8Y&k>j^Ta+_C>TA&;aynWC0>{OtVv#j|(M9{>Kknx+o!^l#F@OIUotxee8_&O4MBWC{{6h>7I= -#o62R=VupZp~0}#s>4-C*CLr5u+azaRaJkB?OK6)4DOff-+v9D=)?x0#1|f0`LoS!6#$~nP2;SFbHv6!+2+Kk3bYwPW-1T>Xl%9Y -|UgT5r^#jRM -24!+d-yiy#lzm9L%vQ2P0Sg5GbT@;J`R)fitv4T7JDlvANu*NFJaJyeak}Ivs5!i8!jOayKhhvx@U2P -?Os#U)M#>#r$&6j71zU+(cK0?b8bmti35JK;gQ+?Qsg|Avh7&(q}~zozM^a7(nl(0{oOcR^P5dcE+&+ -4;&I_|B!DKW|km;uCoRv0?!Nf`8Z$@thr=K*y_A%8c;K!z>EW-?F1%(v&9O>A0Z0I)`z*UnE^vlYb++ -=m3XL!CArJM_b5(?B*DC@$({-=GdknRV!AkB%T -n5S=sPxi9|SS>)8?*#nm?9`a7&KHrQPkxR^tNS04EKOBcAE(czA@9>?yP6pWEC>{ci&14s*OGRh&?JF87>VhBfWp_m>hF1qa{nzAPXaDco6WyLZqnBrl%J -UfQQnF)Q*wDQdK9Ic9FYPBx%paLC$1mjAxR$tApyVn6@0R)Pt!naG=B+s<@`>4D3l+?pIJ9U1Db4OFY -gk)$fL!ijQZ5IpTj*e3OFunuTWz04yCF>OKpnkw8k<9{>Ti*>lg`_5>Nls_!cZ&?t -FeS9FlTLw;CiCAiadCf57`5xx2vq`pWOS>p&V3dmAV%rx{JDB~=Dq}NT|ZCzS%1S^g}ZQOk%xF~7YjB -k^CkJ6bptvliOQwzGueY%CBQvYG+vyT)7VrGkCSW~8!bE@hJrVP`Ir&K|339iN!1%8XF?c=^+BiKsKL -_eg6di4uE2FPt6AP}wmJeyro8$B~6kJ7nx6l%>X&8Z1^#dVKo+ -6lMy*W>hxy4Cy@zu37eTml7DvYj0ld0UUF@V5vaMf&`8RT4&1T;z@Kkr0i&TYij=u7-pAOeMgxtOu%e -g)Pl#+drBic#WgRPgGqr%iQ7<`cYK -NYXl@!iPlG(ICf51qQ&W3$>7<~q08hvVWBmvst1~j7-^C)uez~VXO#4m<42854Y-=$r5D=gpAW5=?z< -=Xmqlt^Q6&w00kJxGhZhgI44PNQ-$;gR15-B2*-u7!YRcLHX^QMn$)>7Zdi9u3;yCwfP-QJPJ> -lYL!@E}_5on08{Q`#6?8Q0|?7YGbdDwOH4;u;iBa!V_Bp^f|hjb2%IByELj&^{N;4KBGzR54sL3%Z3_ -ag=)Cb^C)$q{M1yw3`#gHH+-{zV4JIdgR)FQGIxG?0xIav&OMIzUWaUBS&|}4`Q}y?pJ6F_MWWQ~i(6 -G;Vb`venumyBq;kqJ$$BdH3Dq;8OWyK1$%5XfB?HGrl_c+Q*hx|tShO={Ge-t!fNhA=u7lCf~KBrBx|_Y-}sJf#Np^v%euqDQIx!M_6x>~+ -#%q>upbEX6^Q5KhFb+Rs1dTtBsQ#|f3Y3z>LaVa0Rz-Yawm`qC5cXbEz`vgRgvR+5bh2v-Hn|Y=FTr~ -k3uz+vWj)2R+VUN+#JCVl2O{KussZRIV7|Y|FK4CFo!B^9iQA#pd=nT8sG)pRi+y^)8J~|!LHn;q*JB -_%!;xD#)0L~gD~@$Reyy51rCMX3Ir+fBZ1M<`toYn!ZqLrO$K(5p?m4ups9mW2yK--tB{zNHMm11zs~ -X<2%xsxCR@V{-e97}euyiEB^fU1OZk>M_@n}0JLVN87g>62XvEtR*!;$lpa@92+wxPT8@&Cse?iYZP> -p!0gM%U?uIh72!6ODGBPfdtSMm9aF24mfR}@ax=Fmf>p>zgzyHjd`xO -8%&eDjr|;khkzNd^#YcUtnETeM^+{M<<*(y4fn45a~?P)4&Sud40+Z@kGX{;R`5DB=pIA4m;0fP!%e -dRm7#^DuBPd`#r;+d5dWgCjvdmL88JhM)&a4V*HuT*F{q*|>DU^me8IsBH5FG*uTVPAiq?c)`}he&83 -BMmh9m%^?T)@k#>-Mf_Gk(_fhR;Lcwqa9=rn>{Z4l2h!&LdCk~C+GKn_kmEwmm;*QEz+e< -uu#D^$m&wt0QcmERKF=GTW19&P#FI#}Eh-BKiVEhZ?dMys&-D}_qxe{ZzATy;l{`kG@ZyX)2eMiLdHOyy6pox!8#B+V) -$Nkc@DAzPX*Xs^JBjrBMf3_Mf#iC`ZpJ3ccX>Khj9Qf2yQ8|#}{ALJbFcox?Qs^v<5{(W-%W6!SB}LP|^y9Wvo%`O8o_AoBBrjlzt}DbU|%~`nZ&G-%j --X(_oT!DJaz|f5Z{^tsz5#a-jXbD;QP2RrXEP!@iLO-fJWunnsvyaa-UMmv!7Ux;3|#-L{deHILnT-= -ICL6C3jc+IMq%!3WG~4Yp@az4QZCVVd@cK{C()g2iI@a>pZjp{nN!A;1R6A7K4|I)3bA -Z4d}Oxa#bCaZR_zzas2R$!VxUJ~-@D4f5d%hTDDs{p9_qstD4rkEsS1SAcBYD>s4u)}Thh8%%P#WjLu -RQ5+H9Tc-50mkD=_|X*^eG2)pFd=gZUBWKZN%C-b%M9h5JEhEvK7ESTcQRr==#Phk@y6lZ$Is}N;m5* -yAeIb!+y#7PWa7Waj(;-tz`W+aC%3247Y16=+Cun;{;F>b}q>qEBrC>R|wJZk -u%i5QpZBIP*nt{Y~8k7ae8uc4Vt4{U>N3PyW3_b(D;O!ij#`pddF)*n4yqsziIp3Ou1|NMXJtWpAPSQ -@|{RBa>uMzU2G+IJ7w_(zz2l&)4-1j`b*xdwq-$yP}z^358!9|wmbqA9NmM;@O+;Styw=Gp7o>HGvStY7+&*FqcPO|MC9TYt(b0=?I0_#$7i@R6)Xy4ps$xFNI) -#dlYxS_SEeq^^>pnSpvLPkP6pRwZ?_E|^gbjnV!&sNIn%XBP}X_B5u4E-Jk3Ul`+GZBipy_T?zprD*P -_n-M63+{Q3w96-f-1<<#I~<{~=lMhaCXb51hi|k*0e#a;)dhjpY=7S7V@3AoUEO_Qz?5UQW%OA?4nta -?f9W&hMI~?XD;8z2c>AA1Bk(=kNH1q@c4V}fp48s4U}HeyJdWW1(WuUI+Ou(G -XkJlYICLmd5H=^_H`II*v{8&4o%uv3P~bOKR=hH;(nsSEiYV|)AF>x52dY?&Gw>rFO~u`eV$grZLd5s -~G#P!6nfj+s$}3jfzXqLfyDy{QfFTz~0bsl=B2M?RLDrHsN$H>5T7qE}aHA26bRd@>Foa_V8vy -BSbT>l`K;Txc0v^;0B`Q0Rc%ibu(zN6HWxDlMi-YV=%_6F#?Ag}`6go4@B&k8Ea#eD_n~okKafMV}r$ -+(uDUgXC8klxzM=Ye8pr@OHYy>CR-rE~>uomgDi|x}Dp6}Rg1W76$rkeJ4OxE=d)UbWL5*Dcmn! -thI?Dlxg2ku|3w0-ATYmyi*@coWW{{OLXqRHjwdL1pfsa^|sT)kND3mr+S8*MOci@=aVr>dmi~tklYY -85n*c!CgnMN7tjVVlLx3OXpN+2)!qPvbG=rnT)27^Rsj212FE#TN_kQ}i}I0|pCoYSVGWgj0JRev==&IOw5?agwb$w<?p2mO0TF4%Q?cgmrouksb--^?;)>H9WJrHxUL1B9<02{^(w -G6-Klr>`q|H~&QiKs@8Tu+lLxam&oexe)D5X@SqOsT?q%F^xwUjCNFM1!0Y8L$iW&)>?y2fb0{0f#Wu -U5zblYQr%=^Ngq4p=mc@IISFySJB>JxCjy;x8~)A5Pkz(m($0CQ+Q_D)nE -+o1-d#}tcR4LDlkXjOL=d22q4pTh21wnrobtngn~nL}Y@C}3VuU9YJJEsGCyeK2~)_7Ga>PMN$ab -xJ0nQNIWP&z6b7TJ-O_O3f_Z^9}b_YRRfFT_-m6%Cb(C?M^3NY(J8!cu0bP@G!DbUdKQXo`p?FROl{yNom6@U)*MXq`izQT)k3#ZI|rxdtSEm{0T-}oOezW}26hlx7Br8 -m5{u!P1S0iV!cD#8FBeM!!qVYOP11foi%dJu9f+B3VUS -nv#&Jq_@Cc)p*KsTL> -o#jDtTJz-A`1d|a)br2W*`ttR=Q>i$V&@8Ukt#mp_IO2IXAfiF#NkmStx)Rf*4XK~Hw!BB2-&JWR?T# -hvzv_-+I(TT6p{3;!GO9KQH000080Q-4XQ{z~G$36@I0IM$m03HAU0B~t=FJEbHbY*gGVQepBZ*6U1Z -e(*WY-w|JE^v9}8eMPOIP%@Ug3zLL-Rfo$3z+A9D%58f*(x$RVubYb9y%O!vUFeM^T-nopkb -qt>$Kw0nE-qfae;-3(cUz{JvA~A@OCeej!{EqCzHRH8Z9$$E?G10PFYMwno)#i2clfVEey3M|QHwF;B -kO5JdukDWrjq>~^eFgdKfff-}P{aD$R$K!a -DFNDDfp_oqN(@O^3<&Mm(cWUnzN-1A?5_9Gb2Q44%dJJ|>1L>D4;FeWc+bjq=({0$Q;iFbLO~(5#Xr) -nJPviw`-5P#TCMml^pf)xfzJu?=Nme@o-hC3nNU|(1hzGml9agrZ#e4>uq^x6&8|(SYRmMA_(_uF6~A -GmKQK~xAf(-V0=*DQwk^6RX?KL(^@5TeXLTj>l_T-tz<|43Ue{#9NY{2sS{11VfpOvEN3;#tx!il24Cvc)8cBI$fK*)^%?(tOojc!Gb!}b7|WVB%-_b)KAQmZ>dVEHePp8p1$mygi-87o!@y6@DWu;SEF= -u&9a{HZ9e8A%kDiW;V3}~@N@*Z_A!JJ^T6_pjJ6DHp`tzacY#DT* -%gyI{x-3Ku+wLM5U0TwKqggom+yC;_>!d^K-726S7Ri)WTAqwm=a`uA!+BR%7LtM@wLL;2aM}*WtDE7 -|UWIb6qvz9g4p(>~piR|$rJ#an&0M00QW$_EGB@4F>tnP|L$=*_TqW7p|U(Eh6XMgDW7Eky->|5&JhW -#lIz`nJ<@58>OPQbb_9LfAi{MU2->lR9gZ_Rvot~%-UPCFrF%6_0V`cr>^q;#N??CW+zYd98z&YDy$Y -1&@7E=lH;G<1D`HV+GV2%bGfR%e4`Vs>xY7fEE}>FR#l)b~VZR(BmQ@PX{`*fWS+>hQCZIOL%#Q_W#co*Lc35N)liy@ce!jP7tYfwUT5+WPDG5cbN$DhtYlW9U(K{R)IoaMGS`H3Vq~ktGc7m}#8UDgnCdp!^rYlZWR~UoS#}j&NoBcU4Spy2N4S0vUbLm-;L(Q|L?Ra$4*6X-VVE@bQFH_vqVL=YQ;NGYwzgnOVEPbW@8OnZU$agW!ML&0p&F@H#j%r4WuS@ -BjL4~O1fCV2HkVeQMun2(C|U=MMpjA{+o7C&~$?1X5WH;LNBE9y%|5`QED~x`sFwQD8lAH->Sn>!?U| -hVZJ@_{{Xy0BLZIE-j?^Yxn`A4QOJxg{fS;4bA>OeppP%2Eqv%j^OM$*efWzsP1bXa=dczHNP_i)XhJ -<$DFoz0!4=`ZAt#gD6_fvGe4uTKp-C4Eh>AV@htc>F+RH0tH_+aX*1~rdDR->~rhB_+3;6SZOF=m(%O -4q~$y+l|8apCIXzVa>WQGY~gZ=bz!y8uRi6>6ag|`Q`y)Iq3WFyAhlH@g17zIeJPrbQH7Q3DaFEYFd5 -`ar0f9Y(bmo^#ZX3}8&H>KloVH4LmNhZB}0?b{8WdoIcnne0|C>p9P0L7zK={Xs4rOq%xHdziOsVH3* -E&VBqoELbi;V7{vbM~#4)-y;fctb#3nZvmOKnQG?=LpkOGKq@zg8i~L23i -k6~~SX*+Y4X>cuGp#vcrkXt^Gl9j`&gnExE&RkQ0rQ^hB?tGEh@i28ah> -K8m7FBzs;}IxE+CrXWT0!ed#${s+G`!iLV#&mMJ5qPCH@MOyou3E`vrs*z^%?B>l!-8)puf+9NatZwFd&>r5Re8c_}5!^YLG>p%NJ+7+pg#W4UwFl=uasfMlw{=C`YS``$SKnYinR28irDOkH -^z>A*WE33fSIz-Gy;v^d4=`AGPz-uS5-nZPTh2d%GUr=GFOXIgE6toIh%AH`9R8`9hP*xA0y}Ip$0e% -y+u+Kg(qvgicXe3G|$|p>lVrqC-aABgdy-28h-A@z&_N2)%L|ixH1!F4-@9y)81&s4h0 -rf>Zb1MgFqg>9V*EB`ZuU4~Vb_2Fbbr)&mt$KP#zH|_CgTRj&$SV~T5w*-K$GM_5Xmg`1ijk`vA5E3v -ei{lYs`m*+=&HlVO#a8j6a<*d2V-E*#>_IIJWDZ)JmlDHqK?e?0M5d=k}r?KgA8je|IAu((sc!lST1NlJuF(a3Fx15%gB;B&aUUf7rYCF;5D0H;Q7(n -FIa|8>cyVq=3}}i}1|gEYA`UG-3wXL$2lYKXEZM2Q&S0Bd{xLAKqAO$-RCcddjQnNn^TdWN=Pyp|WF8 -JYvP!w6_XELMe?;G&DB^B_@6Esac~eG=m1}$GLZykupw^tp$&ce!VYotkdkVj)HU0nrN(~#z{t -1snQzX3c}3xNQS%vEnh#aNkWqRQ_Gta;%aJQ$Q=Z*vxNMO+!@ZY%;N<;39_nkq>h^On<=FUIvK+DuuGSf50yD-~<-~FB3C5#^;6R^` -OKQOJZmmq~N-gVm51887)phN8##|a&`xuPnBLKx)8?GK0sbgu8f`zRF1PaIEXg)>7AS3DHYxs1)7))r -v^x~BWZ{2Uz)s}yg)&0yt0Ge2V>}j;F}TuYtUbtF{!ZUE7gK3bPP~!9dK%I8-#)#IQ?_M2)+i076A1O -8p(4j_vflVTw7mLPj?Z*qcJIm&_A)l8T(9#B)|-^;00Z<(o`+kzZRu`J0fz%g5}zwB=hYdT6KYQ)W~b?X2v#B500F$&u~n#7c+8_$kk=j)xm4f+b)7HR=yG -j>mF;ZSlb{_X=aXa}f*4~+)O(0&kmpCp&tE21Wact)E`LReuxQb`il8|8k2r2A#zNfm&GCV;D}wHZ6G -vFaw$|_6A>0^ip+z${h>HOJ#TJ2LH(Uf(+&+AsZ`nvO^DP@~H?M|cCrz30Urc!JX>N37a&BR4FJo_QZDDR?b1!pcVRB<=E^v9RSX*z~HWYsMuOL(ukXBph -u+SFN7kF>PoJeWa~G4oDgk>x9SnPzuPtP7czboH5{L+Krp3aGp|tCcGIlDXZs1T -Hj_AQgQ|KT#lB75hCEYcJx&{(Oo>vM*hZIa8>`F<$ToE}r66n(9h{fHkY5cX^dvyuN`L%(J&wx0i3mW -PW>cayp+631JT_RtCX$YEPvsqX5mIsp>S!47s04fhf&XpT7}MF^=XzpSGB5p$u-N0N|%)$jrb;n)^tSR83N#;bjUR -ntDGuDjJ2CW?%DcxuH%{}v10HMbkC+lX<4tR3JyXZ!bOKbH8r4Kz}9j!8lkI9l9|uT6b|Lwti@S|lCv -xUGHcA7QMuZ=V~*YTcnEdQyeNt+4*?g;M!S+bknnFXb02dj>=hd`|iBQWU^$25ll-!O;*V~B-ydNoJGGv86m(=iVcKY@1P!C -+klfZlRL2!MV`Btq@^kugUlQc1vl`yd!6BpU3tGZ)C<+hDK6KzPOCfJZ -HqR^Hua8FVWp0z7X99?k09psNY;We^VSV0KrH>fj!NYW^&WtkO#0cxlhO+`RJ(_54OFqxQOOglA5`7-XFkORGV>;crL9)9)V+sZ`uHbCPK2#6N$ -eHoG+o6OA>`McZaIq=kdN_uZry49aKRRQcoeZ -D2jyM#fd?11i+e$cAtWTAh1W}2lf*=_}2sPp^oFq-gHkXMm2pw$# -X?w%LH>>ATBJOeRzFC>{q2Z>w}xFHzO_OfTo_dK?;1L^Vc`0^P4HYqjw-~B3aI0(EXXX^Nyb8wn6_0S -2j;fCKHhHF^T#a>K|+R@P6O(;aK)O3>}F)9zzuSdCiDxHJx6iwZ@qX+N#K_y|6>vwEkbk0>mxdz-fuY -aqlV0PHMP;JS^NcE$jWR5s3Z}`F)ZZ36FeFANm+r3`R9)B5w7JguHM|VMPKG%+$CkMPssvCcu#N&k&z -ZpX$*pgOyOm3-V+47WIF&J-i`D4azNYNWThy#piZk?A$2^ixoZ^Zb?w<{E2ngCIiPhAwP9%S|H>V`Tk -`;SDq34PoL>KwmjB>fT3i=b*;HSHd?w2CmnMobN@9=@gw%FIIy9A_)XN~=!1yhFeCW;j>LWcUXUM>B> -ku0A`VoydahxrPiPZ8;=+-{k9O^tw9XM&8yd$nee9_{9wD%gPT{fP@7)=Kf`igSXTRAgo1)}d+zsMZa -Gc{$h55u$yE~jHKRI_XIq-icQ)A#|+HUe#0X*%Pz?3!%`bjeS4^T@31QY-O00;p4c~(>4LP_-V2><|q -9{>Oz0001RX>c!JX>N37a&BR4FJo_QZDDR?b1!pfZ+9+mdA(U}kK4Er{_bBvxHv>gWg_WEw+e9G+ilZ -JH^FZ1?t`(AX`8myl|_Y;yzw3S-#arTMM{=^DNuBAh$V{i!g+aSIF}zPr5m=ckCWUy7HYT4%bk0zM6) -mQ)nu#HfvKvggsvsCAAc4_D{J$uIaNUBzPxXw7LC#g`;ga7!tTXEZa%kFArtnoYI0SIB4M;JG4xel@$ -4wejncdl>t}&l@V3c|+A&>=QkD5y6#1_*D}iH3nwKCFxbk(8^OdM&wpNFOC^wG!#=Kr4sTH4trNC --C;+;p0QX9)G&IxqW;bqddq)Y(yg*#a}`24{&0guUb51!gjLBHgYT4qRC`=%(W^HvTVkf(P8-^uh)*< -i_ROFskW)wW->w7NX=5`BM!Vj&{~v{J+dsGOeX6>)HQqDs~3CZ$o|Ij34_1sbV|RtgXQH`>4QLBnOK1 -&rb@|_W(TFg&n1|pDCCA2m&lq=i1zayHj!E^2XmF|;qGPz2Cs@-lnpzGykL7NHlVKI&pXld(AK~VPU3 -W7?7K><9a|_0j{1rH#Y%j~IOlF#zAHhkOv^8A4!;mCM_}?12WR^=*%VHa|**kU*rd6qH^j6zcfMt%YRtag5lukz7okfC=V(g#HfNs-32DS2AddM)GT~0eFvc-9@wevFxW`P(rq!j%+3v% -aR_YUk2+H<$(6R=j*RWnoq1XG&MR$hj5D`&4RKL=2sLYZ^1JSgYkbg3ANovi+=`CSLsaL9&-Lif6UYh -^@VeF!VBbVvfC!_s))ZGKE<%qVLGjI&Of!G@4`-MWSDf#`1O|H!%@NhJ@>yRx+CzCMA8G4`P-lt)aKL -hi)+lo7Lt{+*i|hT9Vas;7hy^=z4v0^T|VzA-EzxD8XE(gtF9^VJkzOeF{ -K+6bgYT8qS(+Y6!Q8M6xD1yqd4Y+X)yG{vv*@H0rVw`5Ad}eiqZTXp`0{^2Wt)l@~tw`E)2avM@jAvNjfY&N7^5( -=|a*$r14ns*k@W*hq7RjTt>H-aN4s8UZNsXBC4y3C73Y#q90K -MuR@E>yIgm@KfS(TS7@4*_j%4bKvK&Sg`KrGOTFjcWPgt6Iiv;^g2>D3|^$8h&&_Zzbe$Z36I=}?6fmRiI*%CE}Ew4 -t>^cAxa(9@2+;w~v)nQp=ZLU>@)%3bJ|Bio61rBn7Bis9XzATxyy7pWbh2T%yOZ~!Eo^|XrOe-lIU50 -PRwusFM6MEFlk45|45hD2fl1X}ePQv&)3g{a}Xf(7k%WC^RDcS!qdUYU?cLdh}4JMJ(s?@wu@n23fgR --BLNVgF>qDFTkjY71GvnX~VpD`of}{}<$FWVysabC;pnjlbbb7sAoH)f%i%rkkJLZThi{(d{*}snc(h -$YHKASWK~1nLc5DPk*}q@&4m4_tW0?(2Jc(_>sEKsNeg6aY9`OQ8B;(_S?CeH{3fKbHnIr;zlC!$#R{ -`E^Lfjy!|K#GE7o1<67xWbj4&{qtUZnU_7jhJ)Ii~MKCowX#m{}7ow@@27)$NeqGC|VRxX-y=v~z{kK -{xJ=$C1m8e_)1C&36{I87`R90u86I{5#`XzP)XsMduF~>XL(9LHMsnrVrdu$A<>7I#02GSm`tEfM(;j --93@kF!67p!SooBxY7M-c6QVp`KbL3ygk7!{%9;QY^$f~+?8K!XQfHg+pcVF%*ARGOM*;uW5=4qwA+N --V&^_x5&ARQ7%J2&DC88I8R_b1?1!=Ev_a4c+cn>&UDYn=7EmW{TBeTi ->%4ybt*l0IYrOyOox6&S)MTJV?0w!BWaAyb`byB17gHCm#4zDv>S1x=$)1MB{!yMrLKV)`;Nt_ch|Udhz|5+4 -SV7Ek_Q2K3Ob@Zmy1i7|CAz+-P-5ilD~^2=9V5Z*(t^CsGKhn*k2sK86zSTyYyeZS+B%VN_XUG$`2)T -PFc;wAIltoyTFEyf5PpWB7FzsVB(-pCU;Dd;qmi^{BDXrGV -=y{N-F_SbXCNh--gIA~*;_mfEMaFWJ-TT6XjZX1oDK-w>}5dlcUYd``WTIw#tbOFr^UU7I|Bhuf&*&U}_#ur;l1oJ2+i|6*Ffo69|ICgvhg2q0F42B)Lr -+g;v*aR0H~bAq?Aj%ohi*JPxL4YuYre+6L%x}6UZtN{4^v?Ts@JwtJm*M$U58G)UaHjJ;S4Ky&gRP)h>@6aWA -K2mt$eR#Pt{mhhPb0071f001KZ003}la4%nJZggdGZeeUMV{dJ3VQyq|FLiEdZgX^DY-}!Yd7W3=j@v -d6efL)k1OzJsGLdH^%F8wnLDQnZHU)~sLgq>%n}|fJBxSD~usx;=YsNN(8qB#hm38;xjKc1Bq!%^IG6;m#gwt*Fc(opY(A$ay9H#!T`W= -c+$uLOJM2D4n%HJ8Ppec!#lrlRq$Ta5=aMkv`U{vuw4OyI(`%tV>#R%YB}S}FUQNn^Qf4OuO4 -%;Dkl{r!iBhcy*^$+5BIa1{P1$wXCgxA;M8rPtIb@Il_Ap3y=pG}b7sc}0Qc3JyT9|wcRV$0R%}B1ZRjWdjR9u&9Mt>JURJfSU#!(A3qyxv;5$z|x7H6 -IXS+?D5HhIO3fzPrJvM*ZPeP!RJwat~TFLlLAoJi~b?FlD7RYWnWo{)a|D5$ -x@0@Elo=R7lj-%rH%4ky=jiCk1yvQ<;qqH)JS)Wq|pf$bFW_CLda4dK7yEvg;t;rf8Kp>~ED^|e*>qM -ww!v%t+4D3u0kfu+#3Z#baJ*|9FLLeD7{%~M+DNc+&(u;3d0jr?};CQ9d@IkWq~Y>?Jta5;zv?d=mKK`#peh5UaeW_X^wTYY-e@$=x#X@0Uwt=mQGEmIO}H`^%Dc$jfm|1zi$~kLoAW!q{@d%_GITF=r -IY9MQs*;rqzh14Ys!wRJxK$3cJ@6szMGMIX}x)))u -@E+d&py>rsg>(xd3O -VaA|NaUukZ1WpZv|Y%gPPZEaz0WOFZfXk}$=E^v9RSZ#0HHW2>qU%|O3Dg(CCeHb5k9gvYcCBgbr=MB?U1cT6>g0g06NrY4l2ygsPyzAiDyWXEeyEZ0(%o;f&lHnY)*pdyB8j>rAv*0Qn`w1Ki*%x1HSQ>{Z(d1y8cg2m -;G;Lm(McmLnoJw1j{Rc+Z0y^bo7z3dbjm{b!RTQ#&GY7i9zozI*Ek7Y=A$1$rGO;0yqr8Lwm9Zs&-pf -oWU{IuRtVJw4Oqq&U}f`*e#%ec)Ux3iXO^GFqtx>I%$cA)kogiKC8poWdKWH1!{fi?uN;+-^SK2pm{U -y`jct-idtaBCPkv_>vV%9*?XalxqpZ7%#-2IfMj2Gg}hO0KVp*ZD90BwmhZe}yjg3$IA0!JqHG#^m=N -97rJW&0w5io)BLzkkj!vLD6JJ`jEhb)@;Y>j&jcxjcwiLK6Kjh7rh?|dD$wtA-ADW+>ti}=MmjP#nue -e70L7b1Y)HU5XJ%H@D$*71-Q?y(gX>AW9*Yo@$Un;tngeBV)N`N%$pFFR`x~D+#8~XHHK7O2L4O~UoX -h&iu@)8T+V_}LhO2uh14?Y{D8=M4rkxh<9vCvvT`0+VX_~Qut1t(?sIiaZ7?!-R2B0n;F@;a6jJ9^r) -=Fx$XnoTk?Q3%@ww{b0V4#1cJ*f9g9|;GA|A~ExvEW!wr=SS ->7{2)jnIC2b6;Ep9yUhV10Qe*edKQFVhmMfu)`)0dv1>q5bK{pP#Y!^T8GV!HW(fL7xkSni+b};auFX -Je-TwsyY|KHh3RHyxDcx?#=dm_n?vS-a1=fTZeE{UEWh=v1gZ(m5?jEw%B??&BWIsj_Q31zX_oP8GE#+GXDX{zUD4PJWhKs<)6@7x#s44v -THW0MgJ02$IT;l3kmcVwjCig5yao=rnVqkvgrpr=t-3Ytjv0F9+H!&VeO12|NCpOgX{%M0LXT21^o-a -~bA0Z0pnpzuB4X7VGJq>mIPEUiOL%Ywi}Rhd8=~d&7rx7Fd#VYuE7=UQauugcvCUX#!*e#5Ma46SfEq -V(EzywTwF)Y!)U5H)a-t2{Y>tV88iUCocI$9mfq}l;i_dB17NepvZJU%0?^)RSsj-6j#qSoaVxz!46G -MAMBd56SG|s+5=>_R1tXl#w@(Apgp67Ov6RB)I=kJQB%EH;x-k2>q2>yx -|F#$4}#WpMhypS={UK9~Xr+qiTcnjZ4y?q`XNJ*hgWqO3;zFcT? -qagiDu?fs3iwII>V`e5!4`;3C0#*!6AkOCK925%ztew2vSD5pc-%f>g5I`!^ha%i6GYS -y8i`GO9KQH000080Q-4XQ`2<~E;|7L09pe804D$d0B~t=FJEbHbY*gGVQepCX>)XPX<~JBX>V?GFJE7 -2ZfSI1UoLQYl~T=W!!QiK`zb_sSPNYqpohU=JM=W@20I2PQCd;QZe?eoZ$BkzHjfxe4LQj2_oGj#*)? -#YQ)XPX<~JBX>V -?GFJfVHWiD`eg_PZH+b|4;@BI`+Ze|0CA0R-7rTYPTv0@l@Z_pNqHPP1X+ppiBE5p@=fQ)xO3cRIqcv}FpQC*mE&}e9Vn7+2W@4swg-y?Wg$Q -*l|W1XC)+OPjU7mk2hl6~*^Zt!M;CG|A~Cv5+(>;rkp?yBg5Qmz`>LVgYG*DruAw4*kv(a{;4 -$e?vDXW+`z-lP2oP)h>@6aWAK2mt$eR#T7SymDOu00932001Ze003}la4%nJZggdGZeeUMWNCABa%p0 -9bZKvHb1!Lbb97;BY-MCFaCv=_O=`n15QX6n6U$E2N&qQyv{2k%0Hr>(h-!S2-YRA# -06+4;8u?Tvm#)GbRr1!GwQ&3ZF?7nk~?t{O(0a$EY}iQ=-lAPL`p2Hlu?R$I8u^}Yq_Q0L^Ayj9v*KX -6hry^Ls0ndqhz~o3>|wp^W4g08!`u~RgcLMSLMlq-HFJaVy{Wbb^=|9ygN&KsPmOckBo4-u&h_9Kxqu -N3~25O?k*4tT)OhVQfXGrEVGC5qz>1QX%FqFjgmS#GyjMKw9c8ib%vPNzJT-WwE6>3O9KQH000080Q- -4XQx<_rxOWBs0Nxb<03-ka0B~t=FJEbHbY*gGVQepCX>)XPX<~JBX>V?GFLPvRb963ndDU1^i`+I4e& -1gq91338xXnY-vT%jANz2jn8k&?ogy3lH*#_jx(9=vF7L?@dX -8ojis6iRnu*J~jgmF>6!xBG`3zTt&_9;{fh03Qoz&32EtQJPm3r-v`PPIgP%L{q}YtmVZuKR~{xubng -S%0B(sJY3pbgXSgI+Rrg4xnF}XK9 -`{yoNkquU4y~;%M--?7JeukuC*syhm9MlsHm+08;ivXxo+<_HWIw!AYTWlBTK_OfI8h` -U(>P%7p_GQ40zXcg0v?Z6m106NDtlp6-? -=y$r!Yg5qmUZqB|xf@;5JkH4RBWTQZzGX!Vt^&!U?nb>&BLNkmjmeYfkx7Q)!uCY(e`B?{l6LSVx -x?YdCZCD|K>>WwHMKZEsHc7W}dTyAlJE5e__$}K>S(&ZI?7afpHFm;qYuEGQD!th -Y@YH${?E<6gvmbcInUfz_`pm+m#UTk9wjR%WZdE?J!Fe;caEv&o*{_Kztz&H3@i(+Ch@C*|O?dmLAVL -ZrPB3Rsm!PGLoo>=_p2*ouX&$SI7MA5@wLMgPBY@uDHslKJ$Rfg4P&%VK8o0vUvBLlA7uo&I9Vt>d`1 -<-QAn?`D)ro^;jz`IJI@f{Q#31GKY4JL*$d5oQfh~cXn=*0d+NIk@%lXx#0#DXVJ%!&~Q5k-|S!noxa -3bf8|r0|rHwAk}QQYL1}feQ7vI%Vv+G>uR*j36*xDameSca3rCWKV>XTN=wvIc=jR8r%Ex>es8^|GLU -hz8;f&QF%2+^~pA=!T?6)D4jzB;2NkHsS65nhh-(Z)5I<{4B6|EFpAR1*F49lN?cO=;O2@qLZ9-2aw? -r>+GUXH8>^l+Q!N2X}C_^2t0`5SRR3_yUD|pfb)V;-czZ4vHZtq|?D+QA1#2+2v9A9F -3tW&ENWs*s%e7$-;iDGb%E2VIf%Km~}C8Bp`6UK69Lej-(NH`=O=GL2Xzfu~I93O7PFiX+4$DA}i1o0 -#xEnj%W(m>?=`Fd2CoMl67oAC_$X^ -7=*xRblBise2|^vvRmF!)yE#uC;4rN$-KHKW#e{jMy#(?mTYErTy)|$>(9et=RI>C$}8AxR4o*w(>zx -2jj5evZUpBk*Ddx`R3<q&kXQs@(XCnkeE@Lrc4kGoFt~lwZ>3I%v|HNFIqjQh}#rON4U&*G%O14Ai -2e!+W*$u841HE>vo>&R_P}~?>f$Z82x!pou|V=OwLr#|BxOZ`H-Q72L0JRb?BJ9!OPi4yk{;n13RXgo -;t*U0D=lz=p#E@*1Z}hI@6~WpvdB71*5p3dzQIYMp78qh)#931}}kb6BrOP8lo!UKiBDp(1i;AK*Moi -j0rYVpicz6KO;5yIo(@%f}-I(0(1LQ`}ws@iJ;>Q%YE!|Pyn&08xngt}E?;g7F?+LQb>E{JQQ|w -~$TGp^swlreg1);g0^!UV~vf6bw#eTnKcO|Hz6Xv(M`)p|{1p0B#sWoDYux9pKj5k1Zu{`Tcfm}=<#L -EMW7sRU_H)cY+Pwu>w+>?xprjT`u_dTq!Ap~^g#JBTVh+^q?Z@6aWAK2mt$eR#N~_vuN4@003JA001Na003}la4%nJZggdGZeeUMWNCABa%p09bZKvHb1!#j -Wo2wGaCvo8!H(20488X&tU0w($p?%C5(w=HAp}T!tlY+(iAa-@xVx0E$4R%{mYtc?)bV@qdw%JIA7C` -2YM#hxAJmB5FT5CcQ4<>*!3?7h3>`Y}Oo!I|rN-evMbU3i*Kv -!%I8A?EhM=ZTpJCMvaUspe#UspJa^vwRNsO7)x=EOvB@q$S%bTar2x$8+`y&MNxDXV} -#tiXDe?htInD3bUJ;fF;FDO5fCyp?5S0e8-HF+1nnt;^9+ti1G*UmI%2@o2pAW5OsjyFgeFkOc819%y -GX0`&V^G`pY^P!93+*IY+u2<4{zLACVyf2;!#K7E5T-MwmnrAF}>IbvH&j~gAcRJtsxLk!HMFHYkz~F -kztCW)^ME_2ePw74)WoF+J}uJ(C38x%2V0)zD7H>c&KGvtMUuy|)>fWAxQKdX3H~rWOAHP)h>@6aWAK2mt$eR#QViOU@tw0015U00 -18V003}la4%nJZggdGZeeUMX>Md?crRaHX>MtBUtcb8c~eqS^2|#~tx(9!D@iR%OfJdH&r?XwPf6ucQ -c~gq08mQ<1QY-O00;p4c~(>2N#Fa(6#xK!L;wIF0001RX>c!JX>N37a&BR4FKKRMWq2=RZ)|L3V{~tF -E^v9}TitWxwswE_UxA@#?@EnKC)>I0j9T4h>+EiJW|Qp1&dr1Ckzo>&7*m8okd}3~d;j)(J^&CPDSO* -I^r3EVR-%A|gL8i86JU}gCs!LKwz8b>5arz^6tF^+>;cD60DlU6v|qbe(4WX=! -v@XUa^b=}7_5h5yJS3tbuaXw_CgM3??S(Wp9lQ0n!%sMhYQ_CM6heQN8nEbh{stWEz}t$ -uHnY24GtZJDa3Rw{i{s;#P;EBJh}s`XarT|)$f9r{;U!c6|HIqZN1_sir&z~6-|OL|}bvjO>bCx_nSou4~85rrDN%hON$o --K!Z4by6^H&d&h*QNo`!GCT}PEK;Q63eY@KOop -Tiuqm45a$;1unt{6p5LL|*VN0_0E@qi4lXc?86*^{2yQAHBY9XDN#I&0PE2iNVKt;2k{H8T;+IaQoeIgVby(>u(6V90o^DmM(fPE8 -v8#^*jP2mpG5t7--iX20*%8kJr#w{FCB!Tt#rJPsf9EiODzf%GBjfqs>FesF*B4N7YLXAKpqNj#9WRW -Ife8H}jXNg;!KT#N3?$ZUW&#$d4>Pf<<=VQ_Fyl$#i`kV$gW~hbx1I;r0(bE#*)+`#;er2}pRR2*0E|J#36Z*X$dF-jpaoRq1-I4>Q?ch{a8r$X2~5h$Ekw@K>2@+1!u(`53e(bX$I6ziUr}Vl| -Yn>cW@*v9MUg7FCt|R9iK -vzXg@N|OD#yw5JsGK2?<%2GAk?Va`YMEmODn%)4*cE0BsiQ>D9YJn{yG&Myg*yeF9#VsCQK{Txb^zO= -aX@7g!4a=g@j7c-0Kh?jhX+M(tsvPASU!*i5u|{SU)j`2tiXH#1%~PUb1I(owp$4jB?I0ov9|13RpM$ -R+-r==m?OoOg`0{qt_fy1WHXeUVd4_Fz@rYz<8fjCwGij5D9zPfyPhQPRMVis2!k(7GM=!}>f;uNzTG -o}%la?Y`!7C4;h(_2Ym&JLT>2~;aw>iW?d$4gBlfZ)S9u4Cb`JVvyMRn5la%m4E-2B-_2!=&NEl0X4j -G}!WDDBH?P$o@)~bR62%!NrBIfw8_huuT9)V^LxD!zN0G`F4{)D+IxF_%zWeJuF|Dk#iOCHEM0fVdl{ -Ea%ie4t&x+R=2I$Uy{Z#NWU9S^&aZNN9Tm0N8t!Kne!m8gX|3fj5?0u5tgSYK>?kcEAF91f0ZZ^VJ~~ -*i397z&;>yfss3q$O@Lz{XMh>m)pw6Gr-2;YuW&)6n##m&xqcP>TM98W28sIG{_tzTDGi)?41){UjqV -4yIs)m9tjXnu`T`vk+GJmRgs~uqEL}ZjDvejZD}8b;EgmOv>u6UBday+9oPf~%=~h{wFaN;@qir|z^jKapUbXbzH1sH~1!j@tBVJFW4N2 -h=Omw){_o}FBvF3!+3)w)E>@=6OV>ob3RJ_eo&Y_hQ1r~H9^5yY!I4UoqD33QXj`ERg#NMCY8G0c)M8 -nZ2DT{Z~y;!Y~Q_c)Z^kxNSf5dyKB5(yA1Ov4gH{8E(|JNFoYOso276?k!^c(6o}wY?82_oynlv5IPn -Ehk9iw=os*Jlo)AiNFw;-EqqVt%T&Np+{`(9037qMUy_4P>>!t&f}9X!%$VGF^;kWwvKdS|Lw{GTZTS -SN`B{JDDH2M_VK{(z^xGC4Xp}FXXm)nASBsG6wu;K9w#}*Jl5hw9C8!6#bP;I7n}+@N!W?kG0B^*+=8 -VOFkhI`&`L~dw4mgluKnKB|Zp@q&$ei>gI?2DT;@yMl -?A{5SDSM^1uCWp0@quA}>&DoaZd~5nmgG8aZ(Z+^(Kngi-licFou02YUVuY&47w(?B@nJ3v4WTjA!d@ -K=jZriSgVrU7Bq(oVtkN9P^Tf*4aU8S0LmT%9{0?=gLeioqrwMEEXtNx7)VzdO)#|9kS(GUip4`YTwDF#0yqJ~g{Cwazfw#HGiKPWbGo^1v1+ARc^d9ADk -SOmNnBgW_!aFYp-rF6&Y$O<=zzXYLhcs8%DS}*EintH=4IM>$s=vgik#t{ -%|k_>P1C3f>P%?f!t==lb2f{mAf5e(UVSy6SvB=ixQ_07$99Mu_Ai$_$ZXgBp%jGAgH9`#j6!!0wMPF -B3>v^nqjLoPDhg@{Y?Yg(bOJBOWTxq!j=BuJra0FIw*tWr^VVk^ha3vs7Hx`e1>>@3IC@v2=w8Z$BYV -MEZXi6FuPJ_Aad%NB`;pNG6%C$e5TvRdcHH~AgS9DAgu3P8Iit^S^@M}?J@&TpIaY`zuOK1Znw=qT*^ -|o5!Gbtu647*aiH>&3T}AG0-~hvlUsZE|@8tTr6XVUyUqE4dyRlYyWH8=Ou ->)hvdTS6X<*G#dkx1R5 -7;aH+{BZ;usFk-8GN;?lL(qqB{Vk)ktXmfDWGf#HMEeDzZ|b$TuO&t9%yGA5$c}Q067EDf&~XVrO;%N -p7Af2%1_5WdvVq|PUY3rhQR5j~ADrG15sg;9j{evlCzjhITMPFRQ*&Gcs=pnFG@+f_R>-0m-(QTl7_A -@azLxAYIQV$zP^dDe=1v@94E9Zsmh}m}jKz0J7NXbO@FHN*vUoZd+SIZ#SQPjsBe~+#ed|VI1!Q&*l! -MS}76jbL*>X_w4hA2&0EEHniFVse`22>NVl04T4HX%7Q*5kxCOPoFnkdJoOfCQ|q97D^0VuA;aMb~XW%im28Th3E@A|7Jnt={uocCdFyL~W14q^<(>-_Ep2vOT -Za|zk5vie(Qj7vE;Mux`#4t`e#Nwarw{@$d`}D17Eanwf*PLgQi?~(`1l^covTNrVeE)6p+)^y;=w374rL=Gqp_t^Ix6 -q*wo-RdDgN1uf8jL|4`4eEx6Ncn;@XH84S!gB;CflBT0K@jqxXBc(R -@A`>x@i5rcV))2Mrrvd(o#!k-NOxn$Yrng?Vu*dVNc87_`Ufq -%Zvsp-YpA`W!cE{CMwcX<&c(YmTWsR-9o>VUQtQObC6Z;p&nuov25uQvv#cA(WNykY1y!5_;McH#mUG -aS?t|SPKJa~#~gD?%g7#l3}^K))Jav1_OIJs%Qd#3BcUhi-zQ`SwqDQ4VT{1eZ)Gd(?*dF -lk|6t!G-<*i-3ra4KY;V>Vcn)Lw<0WaGLJHV?s`t8FQe#PLE-Qy1HS2IObh3RBe53UebQgCN9v0epe; -JWSslT)%?doF}32kH~_08r056I^hQ=XYq|DR){-+bSgN^#M)klNoALfzEaz5}&ZdBnMzC-4 -3CVnBzFRhz*H?Kl?kKUHxa>3qYNMd4l_Pz#Jtdj4xh4&)*#xMr@XdjS4fic0OKe`=E$X*zKKLhW^g@^ -qZ^wQpL@DP@6nON(FUG=XX9=kdg@5}Bgx`sXcHM*=YE|c;1LIx5V&r3Lp!@!7S_m;{ZBI90g84&SJI} -|70ON0Ruk3}H0*Af -?XwW}px^2FUc!HONz|FReqZS=_*%&dFlZ|Cz3yRId=TRONy#`_5UQ_v{eQB+G -C(C+MtdO}cG54=zT}MrAo_fQFSL8r3seXRA;~{#B&R#KgMTX5e3wPJty*UTC3VRo}0aum`ymgDI4W1O -x)mxi}%X^&W?%?`Y!2k=DQdGmyIJyRcj6p3%ZvyhQe)I}hArId2v%mLRZC_G6kKE@Df9zi8nujlP9W8 -{ZiizF=TSQd)139PkH*IYrj<(KQ@*CkMNVkowG+($2&6l6&_ASC-7Pwx3u~}AnNtp2!MA!hrO(qdOk| -zs)&kHKiGJ}Z{1qyqG!5ive@{pxpZN(0Ai*=>#P0+KmN}q9|&HIa7{(i%41wOxX9xza#1?_l6-PsQw2 -wU-`{^^bQ%E>f^5LL@%Hz7=q+!(hNitR3;jW-#`w=mTiv<{N@Czv(S01N8_2O|&K~S}n75C3-CjO9>;2g6VG(|LqFRX;w=RS4LNo{OFScXvkvnghQT$h~ -u>)XCuR$0u6Oy>qP{7tyYUz8UuxZ>cL1y&hw)BXNeA-Qy!-VBAVwT#gHD$5EKacb<0I?WG?>j>HF$D> -@2#m2}W7buX&2UAe=V!RxD?AMJ@$n(ma~8O;m_KCPI*9gWC447;unF>fkt@#b& -AA-k8uzkBnazr5?UD1a!b<$A#OD)}eziQoVKoD6@(w+bkB_AY`Al|CDq=yX62+dtvOU_R+j-2s$)9L> -A=;+=fLm$qrZ^s{%{6Enp~%%B$|TTF1152`l&_S{N*3Zegb9#6r&Lsa;*|dat2~=nO`Jfs)7R -343cOdKO(U49PhKdLy?LPoeO9KQH000080Q-4XQ=hR{A73H>0Qijn03`qb0B~t=FJEbHbY*gGVQepHZ -e(S6FK}UFYhh<)UuJ1;WMy(LaCz-LYm?hXa^Lq?%u(kOc;@0HJ6FjurW09~?o`pqDoZ}6YEOZ%9@jm`o-=HixdNr4p^&mYrOQt`W~~o~H2mb#1 -lm&YOK#HFddNJTA9No}E>@ebaW(x?XlovsLCn)yXz^ux+lc01fkJE6>(#vlHFT9+A^eAGgh~vVI}EnR -o&K%FC^siKmr0GspMbz*)6*5%T`HID*mO=Z+%k0V#*dMmdkfbNz}9W -W@iF^zLToc13%cdvRvIfsa02Qw{kV32H#h>>X!%X`>g36H-}okc~ZZH$*z9hNV$DnZ6(f`-~3do!yX5 -b{C0LWP2ohGnlFJnB`{8F5PMqHZyuV5Gd;I0JX9$lyJjV~DhKMXsuk=614U1xM7$rGmm>hyO}i_*V%g -MP+iWc}($k0haHKb)=i@)pRLQbkZVI(*_7>R}^y~#b8tsWRf%z2sR<4!ntRFvzr?QpxQjSroE88pC75 -kfR<2dOhJ$z2jM*3dkYMMT@w5i6e{3Tk2ALTEH3PcUao>T|gM|lC_!Sn-Uo`SHD?QruoupCGZY3cCup -C3JXT0HvCmye#kc=E%u;hvjPZCtlM(_h1_wa_xnk!kA=aUJfmtJKo8zdTgiRZ+`s+boBB^kmg?BbRTA -mg%PSY7O^4)Cj@r|Kg|T&wu#w<)iN%L0*h>Z=D>2-;1s(_GPy@J1YuUy`m5caWM(@!ek~UC_C)d`cbO -Gw!_EIX)f|HQmUQ2Iy*bt(ozBtyR2D3BC;r^XJ_9bg+S86dlKarB-byp=_@VbF96#a9zL&Lo!RkT0Io -3K|ByG(&uixX=L=PLc3>+2Yz9U2R&p&0COGcnnCdJj@y(p}Hp@6IY=M&NW}ZUe6}PLd>ZX-3%(=LPgA -p*NBNXJLIRBO&&8ZU}-uUm25)R;6z_x?qxTQ1ss)AEN@JJWzPF!;)2ST8itc+lZ1dzN;9X-hk?yJ -5B_M?gO0c^mYy+w%kQCdLX_&?{HS4S>tq2(*UuffccTl5EU&hg#Ou;e{b6JT~*Wnmu+Lw1KotkoBP8J -5$0zz*+w^^v3fJG!_rJdwH~8ReYf)nlm|5K+MBrrR;j`xWad#AE>Zvx5Ap}qzaIA>0 -ViDD)o|ebv1Xk;st64u0f#V&I9e%HS*Vk&tvy)=y0-H1U7#K4Dqa~wb4UIIR5EmkYPvs(WhQGWwg#=d -0K(?(zb%-WJOR4e4odiW1_sImD10~yIPX5LxBD;>_WStLyplPa`D1C2ufJ9F4&W#UfjK`>axALE1>IL -)g0QTq~9+acnM5Ybv+c~MFB+kkrA?_l!A`Y@#znQAqEQuk9M*>)vImQ-Dtjt|Df+`##{egxg$40*93r -AzOg|Q0GXjdeJ!_J`WG3Tnhq#E_+zA=koi^aHL@q1++E7m3J48^VZBVOwZR+!$`Icx5Xo(Mg|^ZZ#Ku -aLS7ile6pD~Pzx>Nnt=9?Tg%QsmJr{qt_jRZPfQnq68}>clYpt*B*p~0}N4Ob`4u)7NkPt0O8X$+3Ih -mFcpbV;T2B0evI)D6XN&;yw+g;UZ|I{UME35A;f-5!;d)S<=8-jeb>P3Kwrpgc{NbV|J5KB{)i|<(~s -{j%VM#pQO+}EIvq_IUd48;L8Pq$sfCpZeqyaWzu#rejl|OUad#JN)OYm7uA -_hEj&guIV|0a}h1#DITAoON!V0_iG#>yARtVsSKmD4K??*cNLOB5ScRA} -hH(dcWzEewaZio_W{W*JqYdlMS}asV@8eUsh*PK`%y_xA-UeSntcUql>r$v$g`TZB;pc+{U;6#5~o#^ -f7dY;Ea%JBfOdnwaUCuP1tBTto&i7w`F(4@Ob*aRu317}*=*gm2ImF&d}+b_KdDVG*Nq;1mS}!`1QLmg(6Tev`kFg;%3># -dx4M;P+#H9C%47u$6hv{v&ATc+?3B9(K{R@f{EmB8bj=$pTEJ+H)kSA|}Wg*yUdg6Crt=O7G5ltXK59 -D}N?MZVd<_Ey23RGec=)g$L0Td^-X_`*Q2c04eXNUqFDNh2y)ixyH$>5}!<~R-eK0$+E5z`(S{F#uPZ -`S?UX0X7dUV6T|$uOL(J=Ey-7UT;N=jYlPghv}p{n`gZ+F~X}su8US0tk -|1%Blus!6Wy@$QHywR#Vux<0EVByRr&P;%50Bb4mK!U+0ym -eAP-gl`fH!p;qNe`sV$RKS(3)FNWnKl%-B8ej+g*Va+Jj0QABSGk-=qbtYjOPO?T1Xzc5GW9psx#RN$ -lA~M?*vAr|-TDJpbbpNbptK7+Z{)aIsI`W4CBxA;IpLP!sC#+8<<(6VymaRssYVN*V891Oa2Bg?dx%_ -aOhtQk&*uJc&bjgb)1H6UsASGvo_p;{oh;WU&J+iRRGxPLk^lo>Mwt4JRJDlr{cr3F;}AqL$ZZ%!I1LL(>7#ChL< -mCi$SVxI^vJvvC2u^-qKR#nEQC{`4rors_5XOIB<$hn>(ms~i5+#IRO$PBWJ1m=-@^&+AHQ>F9!X-Z( -)QjXh~T*3BH^lo*v9cO4ntWYLqPAxMNVKDsX3T3f?#G!px*bR5z4LKoAX(kfbbX@@01>5+32wMEbo{j -76ckzP}?enzOl+pUB{6xEbgQXn3iGFjK*q}h#sO^148$f-FK?6FHJYvZ#(XB~MZ_3>&5NKR*rIy4k;P -}TMZlmk08Xi~K%AcjYwB888qjQv+qRjpq3X&6t{;zn$41UqhBV}*0 -s?@SS!gastA8XhkDQaKl)+Kil -UWB05N>-VdZe(bB6r2DR5_ftUrylyz$cPh4hwWq)@vlx)`FZ;Be7fso23ekHMtH$nHu^WxV_Q4?h3S| -5gj%U(?&G-bzNBb%I^B@damX7P98F&(>*Bnm}pMGg=|~~-fO|-#L@%<=X$`Ipdh;7^^bE0 -28B{uAn`~!!(!H(h3p$Y!~s%_2$8Vn;Qm!9jBqZBI2mEdV?K&WmhO3sN1R#kpyccD| -D>kOXPNMbY!HYd`^tzj%M!)lDS&S>^SC&XFZa2Zmpo8d -MXE4A(0gMI_}0WH%+g18j?%%?HtWZ?Dm_bLeC=QZ{`pc0liD^$|>SD^r0O2t)fz%wsQZ-8aPB%Rd2R-?PdqtpX{!d+e@8=-_4#lrfuT!+C -SBE%LErfz%Tr4W=!F{~b>IQj#9K$s0^-t -MT^WKt2=|B<&%@1@X>oxlPFJ<72!-wkx}d63Jqd{(4RVwt7f}6lG%JwbtXOBYegzE1U5YrB^*!vx6{W9 -{?rGOlN>#aDT*y#Au4^3O4E`r!jWzVM%^|y8DH0#zReZ4tMVz58iS-@NyY6WDI4Iivl6MW`=(qpui50 -*X|aTx0ZGtU&!wD6zRbbyxkkX(muuh8D>`9$1qfkd{5bsI)~n;@On<_7>V<1eBx(;-`CDG4amJ-hykK -xeb=~)eGW!FV-W`uAT;lMDX>UeI{gtB;h(egTo(4X*umq;iAC^N*f1$87Rje!$H0Nnvob$1AvT=5Rv+ -;KOHytPiL3ufv3a`SJF1(;lSL_p34Q(2v)ka2Vlf0Idxt6JDqMMUyw%G4NN6u#qOA>Kza_G&Jw)jQUZR_|tJ -rm{ZlNZE^XHf;9~Zoi{QOzL!>#kf2zz!WpH4)hJGsJsAWGLV#lcSrn0VQNwgs(Oug6ztLN9YBDhEIsR -sw@tW}#x`1P@dfN1GQ4eLj_cm?qSNqg=nck&eoI*m229SnKtB!Oj`$fk-%!y+l#N1CqYnp15EMwdlyn -6niqwSQC|bp?U-UxOD-7IlGDdgU(;a%g#WpiijT9;+Q^0!H#dCcL5{-aV`!`obD|H^m@TGGHMY;P?32 -<~LS7+j1mp3>qKou##M4|nUg$Mu?=4P`(y4;lYRcbcu946KC8u+Pe_5jO-u>#al7QU^}OVnos2agOPO -ia*Vps#4I>ls1^Ru`)lM$l0@j(P*DO#4Yt>~}WA(JtpOf)Y8c@u#m*3~CcSfx`GdK(F%*6=@inlwR+a -CDIrESxL}7DORVs372u)ABAC}WoiNs&zwJ#oc3Ey*8a!419g7-Pe&g`E2u27C=B`_+MpeUL -c9lnvq#p4HU+Zh_j -ttO9lcgYL3j^`mqGA=BKArbJ2AlrA`@zMwC{nvB`OHBz%xjLp!xM-~V)buM2>2@wO#w*IOsLUN4$GIdG@ie$Dd9e{Lt%Qq;Ibys3Z43RUx*mu|(Oxg4 -s) -6_nzVc6&n7e=u3;M}ERbV!cr+p&ZOTjFU6Xw2@yD|Ug;UiL5W^K5c%fTtd{1!OTvbcfbdY9G{d@quto -_ZbIKPyTau=-C3iV7b?-GKr$tDU-9lH#i_d*P*@02+#CEip?%1UxxhB?~|nM~df8lQY4AbF<%EHCJLp3Z|FwYr8=ryVtCV-{Jm -e?MP3e4?*3{1r1K9<^=LCelKQZgGX8fw}csLpzk}_OS5ojqLvCeYB!LEK7kb@nWnd2Zj&A -i5r-~m^pau|9)tcQQ_OcihQvc#Mm96;y9x2f*AFw9d_5KKMgj)O#3f(-^omvB;Z^Ycei --Jy_+0>jkG3hI2JeuW^kEn4p&@I=c!lb&8!~*}cuz|%H+A*PLFz2khQf*PJHap@XQ*JC`XPIKLj2 -%0M?$m>NFZDfzNK$aJUy&JS2t{(!Fg0+&^G(zsv%(wK`#;#luql1jP3exVLz{9DzC2J|MOQX;T$XztZ -m2^f<@SYN)JY=UHVJOK6o93(SiRdXKz_xoF2dyI5~!aCsjXk9aHZ?Sbb6ZS7$sT=7{U6j6V14t8Fgos -*NVYaN_s+EObw}#0N|j_GWyk#f3XceH*18`5jn1#*~z47l&y#`jh;m9Kdo>@C=^tBYsm)$hiaV(49a&go-7R9|l(e_Gks@ngTqGEd8K1u1s10yQPI!~5j{gnJBP7?|XYNj)U!tC -0fZ0%wCGPqFWM5bqfQz4{KUZv*OlUV=v5|KW8iq6oku=sqQ}B?fRL(dC5Xo@i2w=< -j|dNw?J&;h(c%qAr5JQWIsVG%&x|i;h2`%1-LGbZH(}VlJUxvsXtuL3PV49IAgx1sHx*GHN)&BK$LTlBk9LckCETNa -^vzEV8dYYsm=bQ6Zm~6N+4O0rW;IPRP)3VvPPA^S&w}xB4o1v(`3`NzI(mwNR{lsV`b$#C|611CNzCd -JR?psQhwU?SOsBu3jC;Gv;4k@o@l93P&mM7rGKXGuNUSuB=n`leeC_I$*5{5mKEenp6J#0h$}IgIx|i#gp#O7r=ATPAbm$X=Xf#qD*1O -wH(C7pBZ9h>8f5NIHFL3t8X2*-Xx*!Q7!lVe7qd! -NF2P|oE?`z-bpi^Fi;&>iPG=CYK>G)C6k|%3Nr{QXtHU$ln1u(_PnOo^=x}KxI>T<6Rh#vzPu(=X-HI -4Vi5+o0+goaDuRin2lSn(Eaqi98M2}9=JdJMnNwLXKserPA&V@VQ)rxG19(G-UdCv3PFEI_33ON#n{Y -;j`pDNNTp6&(NFIyw2F<|)r7Ms?o2B%$V%Yv3{5FwxNoeqXSA5j7?S{eL>(TJw0mDFC_ZkG(&9Bx&n)u{^hjpr -C%ck3brp{$3(qMI!CLlS?{FE0~pL+E1(PILAffywY1VmAU6cLm@?c|(GkW;u -bpxbJ3?N14diG6fN1Ntfn)H|fRL -M(Njzp_$-5@bIiq8L=Nb%$J+8s&hiV(&u7`E|VvZz-0aFD2CjakRPwXKw~<{5-lnl3iZyKgv^YPaw06 -AvL8Ie@~5;-K9&>?!aoKT2%mm7$(YO?VKFeh}W(8w%Fr%a~6i&y{m_$-c|y^eeajOLYKJWgQHZld1sd@L8VJcx_}U=?9D{-5(>5Bc(CbM -uhn-U0q>hYt&mxnPuS@94)fv5>F5cOzmHi0yKV+2$ho$@2 -$Hyco_Ro@(Mx6GACxls`#ElBOn19BKEqpZZo8Mv^)bm^nRuE{B{Lt{o{(T*Zzdw#5f73~e+q -}b_BN?I%Tc`FPU>4&~GYku%fK(JQnrpIm=;SYXf3BSD&-en5yl0E}#|Co-x9505 -a&50!_{uo6~Q1c<(^fNa-K~CD6?&udbn?vbPkdwP#sKhm_Nk)uj_6b@m)5I4{QJnW4_BL5sq}bAS)9B -`Z^z9D5f|iov2_ige+yS)mx@g~1^vxqce>yZYA?@mdn@`S-E-Ef&h*WxM>C=zF8^gv}H?arTiS2nGxR -0XCO3)}GH|R5!kW2F6fXrP-@CQE@Jn+q2T8~H3m0!Hqt4Gd&r8pR=5XtkdIk%CofqQ|Di9H4$__AX7w9$c&egr@!Tvf&#-j{*-i@*fQ+_kX?JP_aLF5i -CnjmGg?`!P2$F)b%p@kZ{Z!n-Hof2%h4!N(JNc^ommaAqo+IF(Y_x?_Is*owdQwrV6x`&y#kpKd1+G% -gQdY6rkvuCSN}Z%@P;Kehvy5D*M0mZF97T86O;Z$?WqW=S5P{>3aj{d-3|_rG$7(#GtJ9hhl!vz+;YZ)4$+jQa<5Z?KedVFG0uTC-dpT9d!OrZ!UjtEog{ -vSVsg&THCnRcGBh!CGi|WX$n(_)6(&CKe^5s`pSw%4;p3GD~Kx?z$3d_Cv@)4$%@n2Q@j5KDTJU=HR# -Nr5hTE7xU^BeF-6A)zmprojIxIN(*G`$$JCsgX2WL4f#Fk=X3fCQl2ImyT(u77^f)i$~~?!_Dwje*c(oNbGI%bpZWUbL1^gmBmagOa=+RJrfkMuzQWFeJ -yUi4aJwbX2M8SiTL`qv*sc!JX>N37a&BR4FKKRMWq2=hZ*_8GWpgfYdF@zjZ`(Ey{;pp^co-@dvbN9OT&(C8%z)PwS+*~M -AO?mnP2AA{TjTc`a{V7>goN!iR -eD0Rd7JH#P49-iJsJRsiKeOT#(_B~~Rc7BTq2cN`nyzRemoVUQyT&~sd~S?$7j%k6^3!XzouJx`Kx5L -~E3kvBFNAg^6?J!s|9aiA3*F%B$)R?r3L_$l3TpsBro?TDf;t*{J`@vl_kr_w_b)=J4`5CoY_jUku*p -$j(SwLQ{T&NmDL5E?EwV>Wr$gx*PWJot=8qf2fNx-iV{I0G}emPgDZ -Gzb)8C$TQSKt^_UZYevELlyc#3^f4vR#D+Xo>0?+fG|#L&K3z*xJ6vVdZ+2{_{2Y85*_U+si}f>bvy#WDpRC;-JMvnrp#`VoXSuPg-56YE=8p_f6c) -s(qVVE91|RWP1ryEY4$D16CrBa3|G%X(i2Zw-uhf9PCy$~C2`Y*M_a=^l%bZacx<4XRJS6oDh<-1SA1AFt8RWt^t;GI#~iCzr(pcVFe=d4NKXdmkVh1%pA&60s=vIg5A-& -klyvVksk9L}LF3$u#EVWlfdWD)`t;DB4mdLpTo+6ZlI?C|R>T!LQJ_NIqG=nB*+R59sMzcTn>V4Tvgh -CZ|loY6`zf-bGp=U~hr7__3}(-LvZH$#s@gI3}haBxz$PuH*Oth#b9`CeK6#M1p@$$F*wpRE_N;I`hQ -Pnox6|Egu>YeJ2nJMeER!XbmYmGe*6;r*pF$NYcH_W7Yc=QhhuM6ZP4&%zE3YeR|~>~NZj7kkqWP!~h -CLX~1CPep-S?ybm39BwTvii9nDT9zF6;MlTSkuott4DdnQ!>&jG^MChw#l-&#Nn7dLoG{l1A@jtY;9L -YH?BLMW&f(*&`}ed+J^~Z@T?xzyr1_MNvh_0A&e@VwNBeq~tfC95>n0wcyg;lR9o{|z)5UGd -)Sd%_B{oBO8eQ~lAg5ADxEvCo+`AYyC~~ecc0kWEjSFo;n(HfY=O3g0(?e`ij}?j*1SO;u`QJ5`IL=( -Lbu=M0`HZ;t=PxO>4-^W1)!Bovm5kzb=#0J1KG0YaVx6zue5ng8jNV#UnrBUv<1MzhnQaWvqz+9B@B$hEAXUk$?G*^`cyUgz -%Aw5OeE{4{j8bb9JM&t1-Cb~nQxf2y^QOT+I|Zm@N~q;}7Y(jy|}O!PuU$An(2TATZ4_Ls=p*G)!R!7 -uKH;Q_dD_);_gLwABO2e0kr-l($?sU%g~kl;ig2Nr(=P)h>@6aWAK2mt$eR#U~1Yoq%R003+_001KZ0 -03}la4%nJZggdGZeeUMY;R*>bZKvHb1z?CX>MtBUtcb8dCfa(bKAzX-}NiDa54pZNzjs9$JT^y6!1H5-a$AwkqN*jgr2&ELV -x#;q9w<6@+mLYhfx_W>cA{Kus2MDGkhvx9^WW9EJZp`thPKE@-N2Z&}2!b&@Rsb+h%;@#(v>PZyh)Pv -t6?lL#?0d(4oWZr*km=S3MMGQBgq@-j_<4q=&2Wgb@Z`kH}R7fyi~1f(y*1}TGar0lKz9RWYzT^xN3- -yQ-5!gt3X`!qI}MYxQ9%JOg-r||Ec%oX5YO{#nn!X0Mu1CPZj%+y0T%cG^d&+=OyRdFH1RW!Mc=2G!c -sQ`Bz@9*?>cET`9k}wn_aYeIU9T|`JczEJCPW6e+Q3UCZbs+9euac+$v8xRcTsaM4T-7GZTsGFS;M7J -eEzD@`-e%C=90sm;c6LxNWDZx?3K9UU6B_fwFaAy6oufyM;Do^v!qQ?tmi5$5tZj0F9p5i1d=lPm|N1GB?u -20H}jLQ3)Vq9|7CU^pC?a}})C#R5p37RG6F@u<{OcAr11mEvyoCDSOdzHXBTEz5+Svqp(~z*jHVTmp3m0V($&Vs3dCW0S -E0aptV>;s$2GY)vc>j4AXN7pC;080Pl;LH~bkzvl{(n2=Z%jfw7k_0E;v$989(lxJ)&HI#1W&`V85eW -SOG3RvQRgwB5Al3;xtYW?A}aQ-!AG@74(iNnVDN2-f(^=t0H*mZQ@wmA#EzMGm%A?IT>{eMBxBKq*Vu -SW6I_KjI60Ut~+!7vr)ZdTG?O#$A#+Do{%)vNTzX`)DnW#Ux6F0%Zg>E+ptsB=n&JL&tMg%s)yIHSuA -aZ@GGlH*D;-lX%;wC`MC}Z?Ah6)-22UFyC_sioCY;)H2fhb^M*maU7-Edv$R59nFhqIt9+$XZRuJU`k -7v3jR2ndw?BADurBn9TX=@Ga-^}5+z-0ew);9Bui89Z*Is)5O;2F2r(X#57I;dH#b_+_2xh0w&&0H{< -=G0k2FB@I?(hQ;FXZl0i6Lrc@Zm!UPRzDp$S|AgWBn+LeXj^X$I^IkqnSBnn%EVkhr2uAqGv>f9r`Uk -VpJq2__#D7y?Ga`~+fcm5F;PCb@*gpq4Ak3mC0>7#TlIO;6($T;VzEN~5a<6oo0 -E+rZ}@RM2g4nF7cm_h>P!elu;z@!1}u!iBgi~L^CEDL5aTp6S#%qWpie$6abW&zhaBu6tm^L~an4O5x -`#2XWYh2M~i-Y=RLH4B2U9tYC};a6IMCBWOm@OXZ|S?)hT$hK#t-jbRywkZX26+{_i;=9x19|j0r8w; -%f@3VUev0h&=I2(h3(KnzgU^h2u0m7>R2#oHKC|X_-IBc6|mj=SOd_NI2ZfD?sMQ)BE -dZxxn*-h%5)#)m_P+K@FP+TKR|f?J~O=n%1?0A_tFA9g?v~6_z^`Qy^S8t -I{|EM6vLJkFHn6Yj|~~eRG~Wi4ZP|uYeg=Lu(-OF>udI7a?PI1W?I5VC%wvvkz4|Y!)P4Ol4z~~PF|= -3&fTwU&}fo;8esuVraFUY5rjX6cTw(pC-1&L`gj33l1AE`)1AGTS-_U{hW0!Y77Ex%;5)Lh{LjNPh+Ebp;b8( -=&+WrVT10ryV}ZKa9LQO@#046>)dWpJcI(PHi3$ifTzNA&CJK={waKqNA)Zq&POTnSe`!MdShgGEzIj -=cOV0h6!GOTjt(?u%63H;4YxM^PndAVEWb2fCU8d0$xO2_Ws6}_-O6peb1xp&hWAJvkZF2_I#a23sAdlexnwh)IOZg;^I7e$y$cK -VC^_QwZbwJnx4+$9_#}NF$Rv5_`!2$%x^LdsP+veGWZ(7k1KvFbY6(c?$Rsp$2`#@KgXd2Om4PWN&Am -ZE_L_}MICIYF3uXi4mA&@;U_z65RTkc+;h}RdK{oy%%eQhI5RRN7@jzUefE|QZ!mI1NK5-hoP!Tt>9a -8;Pu12PDcVcqq(_p05kA~RlXL^QHPc&iZ+tWpHH$cU&{Sz3+z$b6`#81<1`jJ0KpdhxBLYH$r^GO43e -H~z1?5B<{No6%a*k8A6rD0_D5_|+%cMxw!@f26DF<#_+OxA -Eibh3OckkdNQ2Egmpx(aavaY3PVfhi$-tO)*tljaz*JK#(_iL8Sd2Xn)?2q^ZZTJ8S+^RH+T-)cDF-_ -Q15yxe;gSU~DFUWJH5r~v0UN#YU1n=EtQV`R%aW2AiHM+U69!n+d&$^HnuKnQk|MY8Wz=oA#EZsO(vZ -%`7(L6$=>pJAL657&k`z|#f0OtW#g;X`lm`Z^{TV`7`M3r#{8JpV`F{O5{^>NJ()WU&h;%U^Kw1KBFg -+%s-ve-`C=cz@VH*Is4Rh{$Z+@=oU>$YY&JxTGW5#!xn)ZI_qq8xVB<5Keta=;YN;jmi4*a#H3QR?F` -8>!Asy>WK&4zyvZaxU??T7aX0JtR>Yni9GNgMwuPgj!r+LDXUh7DvRzIS<5oax2s#DRBm0U-)<_Azdp$~NGW+))L~9C;M1n341RBmTP@Vp?!o>w3YHJ~4tJTU9NON53bL243z$9E6a_={=Us7L%|Luj_Tg1W)U?IS=+Mr%<*JFiJ<2PvI_cL7myU?`dNtqe)a2T{> -%8sbe5+TbD^{_|X)%o0QZM25{;Hp&~V+jU0&DxD6nRtTmoJP!o+Kg1X&!xf$Qu_tY=N)jJ*w2FJX12z_5UV_R%r%6nd<#pa?J6JlN_qi+ZYaGyr{(5{dnzIRxK5?IKe#Xa -*^qZ>N6fYItx!us!_f3tg8ZqZBwR?|~KLT;5{|xCqJhHS`ReFhW>xcz!v+d*xY@fElQzi~6O5U3+s=(Z2E4fL$b -2Q?SNYE41p{D;o%zS3>$6(_6MkfWHE?bV^`CR*qolK@}fNkC_-=W@{i-h^pdcoPg0PcoJhyG-zn*@~l -iL=(@Ra*Z_#LuH&%_so#YdTw}VvWkCH+$IVUk01Y2geU}vRlAccyKztzu1jNgr?*O}i2C}jdI(r8c`< -zBB$Ts7XC?p@)`RrbSh?x6gSmHif;c`tX<v}@3>g*#4D<`mapwm$-YuHzD=9-?_sHIQuK5)rBkcVqdu$! -*R&j{1-SR=Q#fEw`F%Lhg7*K<0iB^fkpqD3v|J@IM}K#_+~5{>dzA^tzgcn~HPItnyNA_MKh%kUA*u) -*jlbJPjRk1UYhT^%UhmYu}}apj_FP}*m&j--$>agc%(>=2v|EIwC=>cnlI*0U -m}C+L$#qaksfXs}5KsiEil)hmp)B-LWdrdZMGji|IKXI*Pp@HA$F`X3?TkF)&A7$JEc!>GNcfl=*Gs2 -q&!A8%m##Pr*wH(P?wI(yq(?CtyqP)h>@6aWAK2mt$eR#Wqj+MRm{008e6001Qb003}la4%nJZggdGZ -eeUMY;R*>bZKvHb1z?HX>)XSbZKmJE^v9JSZi(>V!%Fl@P?tyyA{h?VCm2fK_Ji) -ZL^U@jil^&LHpbH9Fn^E^0wIWA(qH<4$t*Dhl--;Ugg|MRT;M52rbxNuu`dKwo;nqs#?jlNtmpRl -?ZC0y<37bbzG?`qnPa6TsvQ&FntwF!tipnyxQB7H}rC?fY)lL+QV^M7dtChsQte7Rn#x`1{5VJkhhdH -EcncG`clRms${K88kCKI`>mA24oy~ZK!t1|7AIhe^xt1T-e^s}-wX}&F3*nJC~cm)j%d)MxVwG%GCoV -L~m2@h}odiVFc>~DAfdiY6>_7!J#v{_ip^7Dto3h(Y#$XeK}KG=<_vQjQRxjoe6=Role-b&a#L?rBEa -9YAXHFYTx_Rk9M3Cp>$6VF`BI)xTiXwaowRR}sZ-w4E|HPyYHBk%035z0XQ`Td7?A3r}_Cf`YI8(xZP -*RtrQs$fCZRE5x4zaLagm)wY<1GfCD(%Dv4_`eg{z^2KBGC^pzXj38u<-J>rv#jD{cPSl2L7WIhG5l`M -rhmNI3xe{?Z_B&A8v)7sAAlSn6}4M%AR#;cdFHg{ ->2GRYp%_hJf3zW;w2ZgC6jX$&O1T>@3dBWECq;osD;@8@(Lx4*Q3hIrU2Z@syGtwP}TvX7Tl;R9SK9J -Z47KJ(sQgh3vhtk^mdLoeTAfZuBqDM(VokJ_l{?fC;{pTntub-Y6P>2i3(;C;0UtRq)~DzDPd9i6{sF -7laj)&gel~4?aix6rZZ(aX7|oj%dhhey&jJt`YF^(OX|Eqzz|S0!&)4LEQc;R{Ak7ElWv3;&kd5tI#% -h!(TS4~M(`M4#OQ?hyqqic;p3+}l&`RsJVH6iU>(5&+W_dPp&40-Td`V+94D^4GDJ*!jo$y#B}Wj%UKkK>g#bpbSq9a(VBV -&s9O_0V{E8o~$#E)6{K;53kplbrs#r0!Cc{-A=dj0?oQ8bsv4z+_Y?SLw##RDr|tb=HJ=Q}P-zAQy>ZI -}r+*4Ab|n?Vv{aXakVK5lcT=`b{ODm~FiK+iSMim%;(g&=DQ- -1gIhZz@nUp(qx80{iQ7NTimI5@&d#4?MJ#xX_dFik7^CX)hXH0s@auit{|Q*YbqkK5#2R%V2~@JF!Y1 -IxibOKFDX6D?^8bY`>BDhAC=J9lL~Vu4xv&CGo{jK%ahS0&+(f>RQO?f#`46kkR&AT;sg3F@L?h}6_e0+q;{0ddF6nEqx7!R^qRhG0hW++CJEw#1~Pvqwx|!T3%2F -?#|O3|@ZT%_+)leDNcwY$IN-KRqQ1}W)P?`q}ccO4HpCCH`u -n1^WUR?;fetr_AqB~w|Shu3461L1BTIUcWN|0HaZ$x77a`7*L>IKbEu}HWDX+6UkGDTJ_~n++47Atttg1p#Dfo8PPF^}c>IYf9MdDqk9BVWYSG>)e~y=wJ(^baA|NaUukZ1WpZv|Y%gqYV_|e@Z*FrhUvqhLV{dL|X=g5Qd9_(xZ`(K)efO^* -)Q8x!in5&r_Q60F$aK@4>|~mtX*aV73YnJZn2jt7q7=s{`rr3llAXH|Uc*;c8NL6a)7GIQ)ypZ`#wiGMzPOav$W9&YEGlYt+L< -*lb2e$i;%UMUz@Pj3=`hJf4?F4HHlBJev#12=FBF7Dt+u3V>_AVU1#3Kj3p5OtV5h2o|l -Eyn!CHm>s26Ef(_Wh&fZ^cv!4i1s4H(6(#J`QfL7@e_Gbm7P-`GXxLb);OEYG+QraAsE -jx)s_uTz<|hje!Nr}@VYJNxDCq`Y%Fp%->`Srn`Ws>=;8G58A<*raL%$Z3`*F=g7g5Ih2472awRPMqA -g%m|FlPp*;*VG1zUiXY|b+ni;TzF_0`SAZ_X^qXq2A)>+It4?5E4~)bj8KF4v+Ndn9ta^OW(s9#7!-^ -!nrZ`}2<<&;Rb^(zELeh+;8sntB`@Bq9Lvhp1r*2NlI2PAP~&um(%@Qnf{%wpt8{=o~qou;ah6xl+Y+ -#PAbE(YvL9R%H6K3hKkGv)NFzkn@_?8?ei(OS0AtCdbrr-Zs`d9SA~u#mk&s%2KdT*if@2*OooH5}D+ -MgEL^n5=i0PId~z;somoT&G)AV@PCF&1m*z%iLZw=3-Dx -M9)A9+ar&@E#on=38;-&nXTy(gik~i)JlE`3xUHFax)3)MT&iYSFYc<@m#h2F+Su!pd{8y*fp -NxSO546dj3Jaj5)_CxfNi1^DreUy+tW`~2~KX0MDT5C22X*bm-Et -}bFasYuif{5RXV+)W&2_UX+uyJH<8}(wYY7Qj_LE{nr*`jwKR{)AC)n1T54X1&TM!_vnkC-xr7qN}H& -B!OPXIMjbLv}!J2`cTYj8jiWNB_O6#+~{T5B6eR+(AANP8MU6>=5n0%&YI74&X{9pu8wSv8HKO=6=cQm$+C!6B(l_h^krl-V -bI~)m+2#~Z7>q?b*F!;z4a|x?BC6Gq -c+Vuld^GRqIT5J-#YY{3m1pMAi0H7Qiyhqnj`+REq4^LKEn&(S&#Mq}>SMg>U?r5!!*u+dh7b}48dUc -yZ!|Bt>SdZ+NF#)hg#=3`oGm~iDprp)zkjq+RjiRpA45_s^Ci>(dK<_wtUn)%YW+FsnRd%TIcX+V-3R -=p!EM&7`<28+n-YRC20JWQ%M(u7w>EiNOOT=uo=`RGc#XV6T-5l*X2~X6~WxpX@Xmxg{YE#wfQRc$Th -WaKmY2YkbBf?0CPZfSzqz8Ue^*~*&eVqpj5pxo3+ak{q^PS}kQujs5DT&^;jzuYx -nE*efW?Ck1h!N9V{cLSYwdX%mwKV+lrzV1aIq9#b{A2fu&Jxu$e{$MJoF9_dzkM17)Fq~Wa44^(F{oS -6PvK+l1Ry6bJ*y?^7m>?z$ed>WcOD_AXCLH`xAU=6Lmkn7nc6VI4GK{fFXpnkhJfpiCt{)Ny_%>nJvS -P?Vl>qxq&cuhAu1GTtw7zpk1X;ldrJSnD&gBe~y<_t?<|H7-Ud -mO5aWRJ^V)$_KpQE;$5LSb}IjM>0iH?S_09vE -F)WIJm2r8Z{72g8*`U`a%ro~Nwj-KG(6PcUUpxGo&;VFjNoXsWr62$JQYJ6OW@y!(~&;JF^0h4g$H`v -b%Ovjz;1~6)K-O{2_mK^U#2gXCsT^P*#tB7y+=RHb)S77FlM|HdTcvfC!_xWP)h>@6aWAK2mt$eR#UU -|S?w1C001in0018V003}la4%nJZggdGZeeUMY;R*>bZKvHb1!0Hb7d}Yd3{vdjuSZ$eebU*^*qoGGtR -E|VWp8kj39^+f}qSGSRvGMx~mft$8Oo~OtOf7-{ZdIvSR3m#Fwg5r%qK_Z5rzW)vYOmwc01DKd^_DSA -h@fy+2u&*3@b_9)x|P%rZ%8XE)TSMij~fc1CK^2Bkd>cCu+I<@}>|Vyj$erDn4oU0iDnu#47I?26-kl -3dVD(`Z%7psbG>E}v>q6xELU7$pQWIX<`L=5?U(mcsw{i+1T@Ri -wg$>rN889nE3@W`f~BRl3#mI{t{Pdc ->8@-OjsN^r*F&p%Re7RU8*(jPy?kDsZ4EE^fJr$|T)u~5g_o+ja4Su~Dowfi4;RXWSV$}ZTl2VvKyXe5$^U-~%=rygGn+QhAQ29`GB{lYeG8IU9EKzL=9D7Z=J+3u4VP_g%PnOdwN}&&|u~4j-JuUgdQs~O4&Huu+w{R^j2> -NW*5|p?d^ucDhAhT%`}PHO@f^?U1Lrb%1nefZMacix$xb%9L0~VcBst-eS&%rKaO{t#c>p4vmv05!Qt -m2>}BkK&|%{-dLGv@MBxXZt<)_?b7fReQY#Ok$EAK^XgxI{=pi~yUA0&uzpw64h<4JsGq$B{H{-RjR)EJbJQ=3t;DPN@t^y*v -k!0PH}9vPXY+Syo+tkSP)h>@6aWAK2mt$eR#Oaf8puKd004Xj001HY003}la4%nJZggdGZeeUMZDn*} -WMOn+FJE72ZfSI1UoLQYt(3`*+b|4+@sVX^iAt3vyoX%|{99cMueRrSB0q<(Oy+Q6HHR)@n`cquS1TQ7`N3)js -39={shvkH2qMC$(;60s1Byq|bmZ9!u_n8LS{Tk6m)OvG%d-G@ehztTzdi6h=6(E0*crg<;<@&i(# -HOn42)G_j|NJh+~OY$S2FPTZpCR#k6+3c~%Xfn9kmsvYGMFHfzBMkCHSp&Oi!z-m5u!{=_#CQBg;sCG -ufMa6AOv0Xap4tV;5Jr~goTErH}ks;M@3NfUYv80h#3iI{~u4AZx>DwYu8!h=%rN)j5F3T=x({Ibo6} -(UPOf=fOa~Dm4jy%@r@h)OW{a%lFAu=MKeR+<8^@G%n;X7ALOTzPqZn7?VwAkI0|HIf0mHfxby)}P01 -;WByUSjcTglSb54AX@XViX<$U6a{l{{c`-0|XQR000O8`*~JVE`z@J2MhoJUn>9r9smFUaA|NaUukZ1 -WpZv|Y%gtPbYWy+bYU-IVRL0JaCy}lZFAeU`MZ7vPF=3lPNuW1+imV;n-?po*ZAceC%rZ2Q4k49s40R -2Ks##M{q{W%K#%}MI(FRjZo0%0N#J>Z2l=KHQWLhJJdZa_DY{}KZg2jeMB#qtVzuJM%3aE4(T}`b@|= -y9Qf!FcmavG_kD?KQ&+*yRd|&?Se3m@_X7>EsI&D$hf5GHAN<3$^KJu$u_Le$`O0f-n(n7Dp|^xoXKY6adAU)p1pPtr -lk4#n%xPj(mZd#oC>)CI6t!Nly2Bv+in0kFzGhFVk??%-`bl~QvdMz=LLukBouE%#&Q*Bz-OwHO6H-a -j*c&8&o7fV7q60&+2!HU;pJg+dU!Ikl07oJ-pIUj`4+jSsDqi5sGdn5YQt-sicLwi!>V+a^F>l#uM{C -)kjt~9vjcJ>ZWzhfk{29dk~Nbo9)`$Pt0d>ewS(!d6Tq`ha9g6b%3KWt0ZBA^fZvH0cEgA-L$f_FtoN -AuxfEKYA|HW#nO)^>k!M6KegIjGMn_<@k}oP`tkx4eH75J6UyWCz5qchzu&#E&c*XQ9b1zDg0&FEoz} -iP?PL(46po+~3Ew|?ac#Iqk6HC(HODRew_10X0kO}UO6w8+rviDa|h_nmwBPmx5%u*k3%G~$`%Z!F2K --Uiq9HIIJ!L6jI%1;633Bwn8R~(2VNhB*}k_@3?x{?K1jz8e@SkhY~@}G&lu3hnG64~oqq{b6|o{UEx -3IO?74C5goIg~AU7ZBe8o~stocs%|Nor&l*IG86E!WBTF8Ow}i$D?M;A(Dz#ZWN2_3ZY5CQp^*Q=UYM -v27mA{&A_$5XQo8)A`KC!24G4HvS2Efwy -6_P`wPLl2I@T!i&8P+X%V!*g6u14EdZm1CR&gZHZyr+r7u|yI)e)Y4+7D*j1>(yt_dwND_Ed^hrQTW>0|thWn5!^P -2dRzsN^BGbO}_|`x+Z0OCcMmp$T~6zpTY&`NAv$>LItrHp^`dGuC3069A{cKgzh=S}RRasjUFe90&yI -J_|q<^FnC~H3r&2OBklXej&OfWHjpJg}|J~z~ob*G<<TB=IPSl9R5)f}Ohpa{7f7+GD&JjucA;I3K`ZM6~dX40^!XPq -fy$R0sR}D+nFc0?V9zuw=Rq#l}8Ys$y#YcTa!+)gLB9DhsklH#eY_5BV@JmLh4q=su47%JD0#-^A~L$ -)8X~S9=Jp^-0fXU-J#mskC-KpF2wD^C?EXk0pr)Oea~i5&aV*`3p2v9o -Evh`jETqJS!7XTN9JricGa%_hlg9TyH2HY%gG@d#Z&}5r+s~g{8Zjhci5a#zkE=GVK3dp4<@KW -ww6U|tubTcR^sci7cCetd=6T&Npf*EEH`^|Zo2%h$wx;M*WbOrX?9I_@G=<)aP8(R@5CCC!CEAj0RQp -v7EkhVCAq;8|lFMD-gju55dmRmRiC%7bpR!Vu7hq%{?-!yftfWH(esCM4qF&SpGd9TpS&C``chE)w0B -EjK2oFaL|JDtv{~STZ|9pLRI=EuE(RC{(mNx5^uFCiom-PZ#SCU0TUW_nuGwp9g9hS4hQ91- -5EDD_Vd|4XBe2$O5z5r``v<72U7bNRxPaAlOXref#Q=BwR?B!nJ@5 -+102j$?;tAk1zAfiw?;5}TQZWaS$fUn*S2V^XJ(Xq1JN03cRNfD8)!V00KNh$0D5EypX`)K-6M;h`lX -Ij0CaqA6A^hLO(E(jr)vUOyWCX;)j(?-b&fhEthWFekg0P3c-jf?87o6-)Tb^fNrrig2k2wJPQ*Sw;kJnhxM8J{SzYuh8v!-E -V1(H$&EXqe`Zz^w-d)azlLPaEk3np@P8iB1ZOEXT#5o0SndJHA;hBgwu@RA`vZ2+wV{lV0xeaWRAQsB -fsUgGk+rQRThc1mQJO0w_i+dL_#wqe9|F@i;b1@^j;WaufL}DJ9nVvZlCK_(-yGdAo%qt|Ek7IN) -z072ITY{DZzTUd&jup{-NpD(t}SOw&Gdck(gA~XA+ZcW<5~H%r2oOI%-Z(az<|uxX2;Qb|>=n6x1#lOLUiEOWY2&wzyEm#1eJv*(AeXLrpFS!72k -rYnu4wb#7-`q?BF(FeHK$8z0A}+%MErv0%3FmKoxufR=;=nDtIjaRq$Wyo)7~uakpT{%(Wje0I?ym77 -bL1HgvpfP1DE*xgANsn*R_*rB_$?I$mJM>pmZ?}xEWHn_u?)QRs&G$@VW#<7W~7FFap?e2Q8U>9hgi_ -x3ULEo&xdkq=U0Zlfb`~Kb|Rmi7%en*L>WH9T;6L*8*(B_L1$L`g?Z<0(V0)+|-&b2<&4cE8fi2`x1- -adV01H1|ftHWj0kI9o)3(b5#GZM>r@so!~QtC9Lf{5>ym=J~MKlwLH4qLvKvH+V$}--WARAex3`)n{t{R3bP^Hm -{Ac!JX>N37a&BR4FKuOXVPs)+VJ~oNXJ2w< -b8mHWV`XzLaCx0r+m72d5PkPo5Z;H%fvOKMutm{afB?A^ZO}XfftHp=HoFq3l9V0q*LQ~0g_LY>5(8^ -X6nW-8bF`vqm9`8TF6yiSWB49c!Y|fpzZY`9s(B^OFm}g2eSGzC%igOauo5aE1 -$m1dKtpyFW71|o+k?eD#&V1fMuBb{u)O4!Uq?9|0P2fD$V&{P7Q2p=$t$IEx3{+{+i^6xKbNc+e*VCf -SHYQdTzxIAKL4pubhvgDT1kqTUks1_<2~pfd?vB!YO`8VPJ(9lLvm(l<2$#Ovj$}IdBwu4O+-b;ZBXcOzHDa2 -!Q=QCy;9b#-8Gma}p5uo`r-(D@p>HDCA#M*sWi6THk>ti5H4%AvS7Wp~!yesq$d1uHf3*V1FJM+8<+M4?|4dzGWEDuJ)2wnp(a|_>W-)iD&kK8XRCY25 -I_JE~6jE2M|Qe5ep3vBLhuoHYx_KoUf`C4nG{ni`x5RRNANKoew96LEsPtIFv%7H@$4)GScu!v$6(*|OW8b^)Y0+7I@4m05+UWApqY&&o#{eHFFFG;wVZebN~24LO(k|+`)jdjGQQ$air@c<7DO({YgD0cO(vb6T}gfY#2XcnNy-&KueibBc -4;i?byMA9|bl-RiQ4iqo^N5j@UdrWTAEAbxdq9m6ntxo)y%39e*6!w(l&VHS@lUJ;K$jGRivGgjAC3G7f5-lI(Q-bHqQ5&Mk5P|4{@BL%eotQ<=nOl#xM$eDL@#A#?9|>vv)LUOAE@`X -iq$GqhXaXt!45DwIO6FQxUN}HwL||4GwF_XEzS#=8<8@OnUCWQH?xYWVBrbA=R#%ylFRZ&><^Y5c#A{ -Er3qyFjNTd~JF9NsmRM6gA7b_-3LM5t77%J>dPTZ6YtIfXIqfV??)%pSOPIzCE;{haSqhh -nS`#H6KhY6f7fC>~MY&U=R%O;=#~o2skp^Q0Ph6dat207XLzswsXL>p1i7*k_7(DSx5=h!{uFxI7OpB -h0i=h)UpH>eWZlSjtRdtn>`YGtjs__3te&m8@z}*m(XAV{ORUS*gIc0$O|HReT`27mLYtjoI3BMLLa` -3#~3@3i$SUYpg7KCz=_n>DguUPYO@u{s&M?0|XQR000O8`*~JVd&wyNI{*LxKL7v#AOHXWaA|NaUukZ -1WpZv|Y%gtZWMyn~FJE72ZfSI1UoLQYQ&LiLR47PH&Q45ERVc|wEKx|#&nrpH%qv#N%}+_qDTW9Zr4| -&W7N_QwC;)M0NoH!X9+#4m5*GkaO9KQH000080Q-4XQx!0vPk8|V0Nw)t03iSX0B~t=FJEbHbY*gGVQ -epLZ)9a`b1!3IZe(d>VRU6KaCx;-yKciU4BY({tR@?9^#MVhp`D68hOR{+2r(T9ktIP=f(AkUy_Dlej -JW7j4Ki)=$UB}JZ(DH6adALXThE=`?BrINEkB?S${J9uvp#~8J|M_&2}GaGvS#d{Ohj*_=B=$!2djmrD0hQRM|N)V|>HQ3kA^Y)iB7O?@kxlWnvN0iI2WQfnD$_>(lXs%6A{2~w;wpu-sZK<6=_i2)= -y8!1v!1KSLE295Qgu3B?~WgT9Kplu97Kn0)7$QD@%te2m{B$Itzife2Lpp1$_fa -J>)h(ug`!&2BrH!*nwlQ-#9&P|HzV(##k{)nq3Y&kQ)chb=LH|xj+u0Jc&ze~LphsTPF;&~h9 -dRU%xpzxRkdiK+0L~Zy03rYY0B~t=FJEbHbY*gGVQepL -Z)9a`b1!6Ra%E$5Uv+Y9E^v9Z7~5{!HuT+J!3lk^I>l&T2F!rF8cQ}5XuBeC@-Wv^7>SO#=;D#ITQA7 -J?;KL3NXd@VEcHV|9iID*hm4|#d^-C?etP|iL{T*0<+>J%{4MLst_8EJjVKC!Jz7&C*{C>{(>-<_ZRI!iCk-=`nWX4BK@Y?^+YemDcfD -A&SMFnxqhi3VpMFVoBEd3ui9f{n7eT=RkrMfBI_7oYFu)002x$-l2oem+STKYqR+j`u<1UtXZ#K~_*H -$sq%WXJ0d>-4~CwA#1<}s+=uJdc)LE1M{)=`CKY+DuiH>5cYKq3|XE+eArl9JD&mkmhHww;{gl0Db(1 -72lm09k9EZcF*$kT!;Ngnj^CLgTUu-w(ZSC_D&mM8p;b<{Oz`E-$;RAZBg`qJ=1UY#s#+0Vfyx?)Kf% -;SR8}7iHXk5DVOZ9V;!Eh?Cu9~PgYctO%-TQmbD;v_hjgxclU8L0% -%E9$iwNao!rqYz>Ejss&RlO_F|g~_Rj{?&%bAoXXC)(I;Zju=fne9_{yKbO+q-Nx6{TkO(F@UHp{;8HMn6r7eFhggwt4~iXm-FV1CTn|(6BK3 -wB)CySjnZtP6<3oy(HB1s1#X(Z@Fkz+f3?J)JYQ1pkS4;Jtl9+>!+$w3r5t6R!>y~8W0nWE9S2jzOVc -M+I<_tB`+XeX$5G*moIO>P;mn>ggn-P3Nzcan-BDs=_UgSnH8HH{fB}~dPvt>8ku*uW3ts@y#@U-tVY -g=D^|*3th3fv_}(?(=6o=cD_UYPM6C-<1)?1Q;3`d9uS!-d6ZZuqrt};HPKOdGc;10ybu)=ICfF9*T3 -vZn3YfM|eF|9@+y6t*G>XGiIv8O+qkSVV3-I7?2=3dUPsA=TBytV`T(|D>?I^Cl_y5=^{KQ-H@?py$2bW_j{z5v#xGD7>Et4792kbj|Aw^S`Ez6C{6-w -=hQkgF2p~^XT0Nx)Asry%zwc7L{;lCTDi{^*q%DG?_}NlZOK`pkZWH -}z*^~VnBE@p9aKajs>$)z0$#1lfN1|kD0NXCVOSJ5j?>Qo6747#+v>W1P5h6DQPqZtZFO*J&(1|e0pw -bvxJIaD%^}TuSUM?|OdOcF8x>l(++3UMaV>UFscDb|cmhuirG^{6J -V5Y2y+{aF*3g&eMy*VORZJCirX!OO^PvsYYQg^?hoZNUy`?s&`*O)_wJw99IK|7kj1s(>XpO;3c617}B)uuG(@@S0$ -_Pkwb8-KneF9BH=R3mmKgRCz`ji1Z&gqy1D{aUR`;`0v9YsGAhjli@-LyIDbF#HFoQ@s{!}2hGk(A#T -H2$a^pJ8ecU>j>d19u*0EQudqr(3BP@uY;UKdWE1AvdL@Nt~YSk=Zwh_B#y%h8Yeb;^7@ZydNv~{{Q7 -gg4Vbx((A4+#Bfo2>1;dry`Ap%S)Y^ES}FGWJ@t+X{UG+8-LJVXHgH`pu~`2l!iEGA!ds_sOI8(lqZu -@aG)~BkyRn!gKvF8_sQUN4)(t2g(C%+(2=NMboYxVBs#<`&cZ(N3*@7{WqMs*n|%c_88h`E^hnH1M%# -gM#}ubPm2RMa$=Lm7u2)tvXY+J==T&Nt;+cB& -GTsf9{!*-lPoV#7SfT&B=MXp!3CrWJuxR!_$MBd|%!xO42eQS(1gO9KQH000080Q-4XQ)1|iV`u{a0N -4ot044wc0B~t=FJEbHbY*gGVQepLZ)9a`b1!CZa&2LBUt@1>baHQOE^v9RRZDN&HW0q=ub8qIEuvMCs -{j`Tf*S3mIpolTVGy)5>t#)m3Q2k682;}aQZG{8I6x2`7R%wxH{U#Yw59|9)JuI2vg?iYaBa<3)Su9o -@Ui#OXrW4=<8F5h%DwC>{)oCYw(3RmAnwVw8oX3)MredKS~fz-ugJCNFg2Chqb1A=zcd)}7rPIp>x6F -qwAR?&Zr3Q`99$dQ3ID-pZ;h&VKp$i5FH}8RI2sFN5;=qeX*!6$&L`QNK)^^511GpcklJ~n6t)$C>>? -WHP8shwhby>Y+VDH6g?(ZrFr(Gee7qgCTRf75)Y5ZaL`nD@s$;pF7L0ny;0-14#(8^tOJ4pl8dvSa$# -?blaQHnQ>&09iSAQVg<~E~4P!4^uPi+(Lm#tHU**=TAO7WNfxk?U_oYJJ?XG&?zBp81}TZ6g#4;vqYz -B6<6m!Lfnj`h?H7SW;{y>T8}hx2_NibX$IO)>hvyIx_3)Qasc1)a?2hWO=bWU`7b0$vN{aK)LV)Mmv! -df~mDqI%fF)fLRLi7^R77e7#IZrtzFWzBqkv+c@&o3oBzwB8xPD7FL&9g)Q!f_qJ#H9T!+vk70Zy;`O$c$cAm)fO8SD8Y`vx(q! -3t5olvt-U((+USM07vp|HtVhHp&NTUVlOiwQfwZ&3wLH+A@}q;B!cq6qKc{cWcGEBs3dHG`*Rsl%dyG -s0PD58gm4T_bD@_=xt&pTd?E4k8vbhJ>`ceFjIxVs*fwh~tP}h&q-lw(KGfBM_SLqJEh6a7V*4W4{J` -BVpld=mC*>zB^{R>c-buJOS9Qy0eFdS##|#r^=;JkwR;nDV!XjjhT~^Y+`|1Rw`rjsVd(x5WVv&M&1iZDY-|=-g~K{Rd1`6<-$1(2No&}{UnM1|4JeQ7LX$1B$LLU>{qYp-YW*gDE -=8r5M#YW{JD@D%jDJZQX^Wqv5_h4@3P|#XOX6K>1i87tAks>@9)3QWrz$L_)^A0VbWG7cpTVY_nQVQs -OIEPi&l{9C|n%?r)glBt5}X{?$w%e>h9n$kwt?u7` -TxJp_xevY%#MtI=k?j$Q*QlqPIP@6aWAK2mt$eR#RFqbX4mM003Dg000~ -S003}la4%nJZggdGZeeUMZEs{{Y;!McX>MySaCyC2ZFAhV5&nL^0;Q)8*`63njyuV)tE>Os-33U3yca89qDf>D1QrkWeHY4Cl~jh!>M}J_7J8K1fkMvbc{y)hW&5F$I) -A9RSu_JmG*7iEiu^9FxYA-YQ*y=3rUD|&eZT(K#aVLt`t0{WGPm86(@OVgn}7yZSq3Lv%Cp -t6Zne+;GI;M~RH5CgHVNB)BXA7L4hOu8Q3MPzqDX)cYnL%y<+X*i2@kXrcWiHh<$=i%f4|%z -NEud&uqNoxh2^mTVpGkb9eAbu9guv|}nwOb)h*Mb<0+d9x?7IH>?FFBU(Fm1AC^l`hCY}rPmd3&)DM7 -SJ5{*WqEzX)+|#S$6MR}_uL7i&7F=skpK -=-Q;!b3b69mDfsFcv6G@5Zn%9*hOmf53ncEQsn9D}{VV}7BThVvDIy}x`5i$_EyNpvBNP7*+TLnA~xv -DA>a!Ppxfyr*&{G^v3H`S2VhCHF$-DxpEkT)0$C-v?te&mx)2G1r>%k2?(40^nTZ)-%}Y?dBy5*y -QOr6b0B*vxQQb&~Z_*my4H?}H&SOC%>EQMj7;r92(JdYv!=nIex2N}x_PNR1$DOb{@KP}GTGei1#E!n -Db(4-yAh+gu0xS8-o*I5JIb&ua0JfI3N(J~c)borP$-;#K{<#UbYc!&}EfU_)?TG?d{6Tim>&d9ZP5ILH+qk@$y8H}B|z=6&dqUkSrAZaACeqR7VJZ$z5Ia|W2%#n -}aW`PHkKT`zLYR(!+mgg4`E(<8+h5;|&N9a&h68-R6J8-WxN5Hi3X+3@+8aowYm37x|H4#2AyFSm|9& -tF$n&{1^2wDMWY%4@x(3(I$jVQ=W5`& -95B+k6(ul&f*aj*OpDo@+1^t!pxq-?%7!thUX_yfd4^{+h*vLUU1TUzq$gID*)+Pwu0)?zB#CcMKtmN -IJ07JC~tKkXGBkq -B!%`x!Uh~O#II`$CqEM^y*i#LoS9HX#lXFP@0T$DobqS%1M?uBE&FnGcrIh=QljC~3r@xW0v;-3F%U! -IZMN_mN>IeUM8%GQ8at`RFlyLKXIAIyxwD=DMbxmkq4zu_l{COr^HZR)25A^abWdeI{GA{$Kh6FW*SF -oChlI+RyNI@gDbI0ajk-*a#hox1?ufQ8(Hg)p9`pts=GfP)?xqc-H_B=M57Qmn1@#RG&bv;p+aN5kjt -r9WxlXJO`W1h_dILM0|Q(x=sPTtKP!?t!Z-$cLi1jn7N5nNn5;QJY+QLMhGkEk&Nep|af<0yd3kTkR@@d!F$Eb_B$fzB(yr|p&@B(UozA&{aS@7wRg(S*PkI|>cAh! -M~+_I`&iFnxRNi-hrHU>mJBGDo67R>HFjPmx0d;x%`-!B12a~Avs#m$JNkKa18{|=lwD2v&&)SBb$CD -dZrM*%|%rs8|Zpg#Z+5gB}r6-JFJ489CH*5G>d9Zd-s-$B+*MDZ>1rCUlv)BBu!#(w5Yh$%IAinU;w! -~k3fzGTSNUi@*9+SW}2B6*n>H6#__9w53Xf`##_0`pl*HHhP`^rEQ=PQi2TEYGYY?Q^tC1ss|oL;C)) -{8&pPLj2Ydfo(w?v;g8l0L6usImzsI>Jj63R;LiuAiOuS3Wid~!9xP3%I>hDcqO1cZs+~CX=-pVdsU{ -Vd0o7S?V^1oJx*_v^b>bVzcB2%+;>Yw;+@O(q4Gtyjp|8!+m`XxF4hh@{<%uOBTX$0TUKGov(3$B#SS -CeX`Ins;q~_2E4g;V=;rwLfcr@>qmZ=$d*SmH#O*z_t@OvO)gj(7`&bLL0mXM~8&Bi0?l8YqQI|IB2o -G)$1PwnDF`u_s!534A#|Th?orEg5dG`DkrcA?||2)6_Llo?1qerJZbFXmiP7j)ywtjR7A=ZMcbm7H|p -~s}JDw|9SK)DUY+c`F5SFn~v-#Zl{kflK#7@e;k|X=j{U)rzZr>Isx|KorDP71MN{q({ -xWSKRFOI&s{bST-9q#Ki_+JqJBJ(obYG_3k_Ffhuf?vro2l_CB_A|$4#7Q({6S3|9)})^?{MLYve-&V -;+}6ow6QzBA)rKx|F~PH7%#Nd*7q`v8Uz>rwlFcEnM0INS#y-^^k4p}WO(7x1CBeuiA|7&WuoeI_c?v -PNFQq?H+vt$%W^vpm1&4q`i7kq!YzAQhVGrBu`zhb2dLP{#eP)>5^p@dqbclJ74#1QcYRANm=mRKhLH -3jb_Dea)KhX1h#z>OBQST?-8q_vn2b5;OndI8R9W#4F;WbL_CB^NIz5Fcb((a30(k7Fp?%aavawJL`l -T(Mr?YKLi%4e*(CDFErCKaJ?&P1qpnlg1kywd)Amjlc4hzM!PR-0lVk4}8<=7&jxowNgtR>!B9Ch7XSEFN)wH?_aUy5R5o8Cx(I(*2vEZ4S2)Aasa!wH -gaoo7N}53d1fZ1b21`p648&r`|V0~9-hNM5ZM~f)=3C#R=_e~PAMMX#KyIau*}nrf2XxlfEP|o-MLH(Z09OL-MVtFCwAI#%zgmNC -T>7fz(ZiI0r=ffsSPaBMVY76@LMYho9P_e{~T0Hp*-M3jXKc?@N_m4xIdLJ_%DVW@YA-I8DnP`?F+2X -A>-IMCY!__SwjtdqSpCcLH)CN%@qf7qLH^|E;YBP(NsE7jV;QC6t(BH1q%vQ!J-c3Rt*k{RU*SzmBCi -&mGryqmEyp2;0M@oVf6;NB+=mNmF)x)MLZkU`5g91O{2pVD3M}zE=+Kc;FU6*@8cP;=bN=p{rD&ic`!ma9;nao7FewHCG`f7lm&OE!XT3Ud -HU~0dj>jM`g=UnO0Jp=Ek6{dr+muP?@EoGVH^LUIPB%1Mc7KNUn++pW4X~HN*9a3_`t*uieC2Xj*{$Y%9W(^K -wA*Ch@g0KXvo0qO%y^0&ZY`uZ4NhkdZ^a>L}>(Xfw>C=S%hQU_z_;(DH7vIu%U$S)FI%KCs -6l`V9S~6WrQ6K@jY6D>yLui5Z&3PpUbKL2KUn#D4b15-e+`JwOJVFiow8cG0%_!^vc9leo2{Kgo8<}} -LFrv!JZmmrq0|x96z>GsbPlNicnlPKyt7jF?m+x4yA#|5{)gZeN){WYt;Y%JZye5)lAwA&i48q@_d^4 -!E)CpwY|lk?4%m8LUd(~NX0dAd;3#$6cmfY7c!JX>N37a&BR4FKusRWo&aVb7N>_ZDlTSd1X*dtJ^RTyz5siewP*! -`_NmUh4M&w@OY(=Qz^x4q;({!NJhK)c>lg@M=43Fi^enZ?#whzW4`4A@;(v+GG`l5$iXRyhmcq>MwE~ -fu=TVjZq57y!j2LZjF691j)6{)2f!nSh}%fl*MP}^cnSq;859a|55@S2L;{!?syKGad?6=m{Mao*>&2Mwy{Q!REIPu~ -DtS&I4PEdSIu{Zm5s2`~u%|sc!JX>N37a&BR4FKusRWo&aVb7f(2V`yJjHW2>4ze05=w@Wuln-U5KSDJRCkt!?e0#iY{V6FBm)huXxI4E;yUv2lHvR -*w@A{6aH?X*@qUJ2*rYVT~x!m+|ucKUv)d{{LfDpm3HPMX|0$Y-~LOz=7=?OW+g7;97}wEA79MwD{6R -T;QxgyyBqrS@EzTt2iruZ~x*mt>=Qp4k#kx3Y!sX~1D06)%;~T47^d+4w`p-ZCe1k%d&$p-4$V3ItXw -0!%t~(IUqWGF-;Bbs7(nrkmAjm1*HUyOzRb#dT&YdA?dPJV}zo|9>)>sC=zOke-D?LR}_;vWDJp065E -(XPGs7b*SMA3zqq)j(3XUk2Lk#2A`(z>3y;XR;gCz)+bw*MBvD8pWHOkKSak$848=ngtru!aqWgkU&gFKm -AC!dqeSco1Y@8f=$7+2+*RyQ+wD7K!H0B_J;w;C3d)6*(HNsVNtN9Llcz-1@X15A-m_s9Wj&WLFciFc -RROd7G%GT%~Y4g2-w+c)GLz^=5&BsQEmDt3E2&JZM*0uxw6N_zOQ)!2^5)|1l=G4Gh2XN6E^-Ph%IcA -^DPBCY3uWJvQIFY(9Yus8d-&kBipxVT_R5klpYM_`Xc$?t_TIR1K3hMr_je=#{HBc7z??u615UA`*zLAY8z)2U3{mX(_M3i#URF=$SFgK)>Cvl71Fdf_d>y(8hdCyQ5_ -|(i?^a>Bn5ubZ+b&nHmTnai^{jKTp@bz$O|uxW|Z~B`w~}D-f%cMw89wt64V8-GWz3Q^p#9H}m~Z*cD -YgPCh?5V!6HAV<0l$Q&7-Ekha$9J_M%PUSRnHNNn(?W0=fnX@drkWUMR*ml!@%*R;Qt@KQDa@n-d*!oC=1WWz -#XTlreSkv^&KUy@x1s^tJrXCn>iQ%yc=HEMY;?;pmi=a*YTp)rO$qX9#Oy~4ueI#PrNq#dYbMYjbG3o -XQO;~qJ2}AREyy2V^0Y@V4+QMkCe;aL0uQhvHBj4|om@;LL{;NV}x6M9$h_+}VqW2Or4O{P(!(gkLB@ -N@7Q@W#+IxWpy4Z`SjU|@&;d5!rCoYwm^x;b<&ghqKwn=4#!+c5Np9(A9X6kx)O{=(egJBtj}&6t}%) -fTs|*-dyo#Cn#h!?!2w=SVycs;o=aTz%C}f$onD*`n5uh$`l74dc>yj8YN1c(EU=LPir$fD!^2Psf`9 -?urFx?4<^!)`y~jRS~bYLGYE#CW_X?Yt%OBl8@09_YO&PN*>udG}}n?2Bc3Brbf}kaiS@9UAn8CQs{K -ap@E7j75k3O;G9#M3=ezeC#rT56S}gFkJeA5i~w>vesd#vwsa$DVod*j#UL^IeyS~eoQ-+!=ZQ40=e` -tfR(}CdO9KQH000080Q-4XQ;LG9=Fb8E0CNWb04D$d0B~t=FJEbHbY*gGVQepLZ)9a`b1!pcY-M9~X> -V>{aB^j4b1rasbyZ7m+b|5i`&SUzVJ*;lz(r~t_-FSp}|)4%EOCLJ%TsTi7j`kcL*rz4R=qeArMLKA>$+2pd;9AklX8E2s -G9Nf?Z<@Y)bgGk$j@aaG;)VhfsbaJg2TD1vWVA8aXk;!&j?hEd$f+ElmO^{%=L{4D83zVD4PdfS^$g7XyEUn(z=Qd`t_ -g`xSip{P8kM#iHw^_w18oa2VG#nZp#8b2Rf#f6W)hPz7*=P<{!&DPt?3M+&CkfZL{~>^lYK0FN*(AO9KQH000080Q-4XQ&9y+WxoUf01^)X03`qb -0B~t=FJEbHbY*gGVQepLZ)9a`b1!sZa%W|9UvPPJXm4&VaCyB~OK;;g5WeeIOr1k*Kue(6_F}*Va@Ya -|if+*6W&~PVqHH!4DUehQb+P}wGn68Ul9J7-I>a_L&yU|cBCFb153Y4dX-x&bt))JL?2iAC&ZqFX_R? -ssQg~=;C6Z3EmNm-UIt-!xvv;c|H!773g43);BbO#W=*3NTk7b|k(XowtvVnEIcH=R`aXW1LX7GUViS^*f59+FxEWHpM)u;ptNok*&4p+3^F2mBsk -1;5dW1+Nc0qL9vcN$E8P2hffNWu}AUogF+Jnla} -e%Z0e*uw?uo*=yNX8}s+v%kPkDcq)X%OBx-5t=&4vqU@=DRumOgCN1Y9InBe$oaC7vbz1z#cg!EgR;? -i$d8k;MHF-H-}Q0)Utvtk=LY`v23bxl3|()2?SWGP)hkjTi86P?qOQZ%dnZ=(>?9dmLAmgWsmB!`*uO -BjMwmfGDED0h>r-nx6Wk>$y%DuHY{ULBQg16>Z}AG#xrgKa_Hj4S4ge7cQJFsC7cnhRBtD3R$BuCryT -?pq8teGuHKFg`)6GE3rXwZN0efhm4vyu1h>zYN!sx6ra^8nFa9 -|yk$@sZ9c-m(5bCV~5|FLqs@`XmC^u9rpsaspt8YC_q!$c_+Ehh0*5X?EaHBp2ZFc8mSO??SZn4O{11 -ZX*j5);)(!=wt+S1%FS0y_}k%YNcji -*Z4>arv$UrtqBwyBud+WLFKsZm8+kY|df?zd+I1%W)kR5699M|@PxtWi?Jp3PaH^n-$e*aa3Pn?`kVR -O7`I*!H%En84ph6=hT1h$p!kU0Hqo9Qtd#Dwqz{MoTd0H8qZ{2~VwLn_uck4_as5}v -o4wQ>d-YqOo3a;B>>pNsMP^CbodR#DS|tE^(_spm`HklC#X?!N1b|-&qRpoiFsfQt$LE=Si%v`p?rFq -lBJ-_F*c!JX>N37a&BR4FKusRWo&aVcW7m0Y%Xwl#aC;K+eQ@q?q4w|g~+W -fC#8X6y1S%w1A)>m+mL?P-H5StZBHX>#LU>cPMZJTb7!oVt!&y-s9`~x` -M75KC2%kYz3^qjxGNNV1Hq6;2DcCb5%7>8%x&G_cZ3J{*;DLwJx*tmTfxhGePARWgdAXrwlkHcl8CEs -fp?x33@)htr=_<|g8`BO@0DO~lX2x=~Q9RF$MVDsyd;N@nlnid{OnvbFIWx$5ew5^hRaB=nF`<&-WP& -DZ5l7ApUpBYcGb8SiK`$_k0V{ONoo@B`kL9Ud6IKleu|AEk!G8G=bNy{YgXdpZ9MwBN-SS3j`x9hvj` -`8@gNVm#$HK25&97>^sHIQD|YRu%dAZGQ7^ddKn$e_HLmL2WG;s(q;n>HJ&c>NdGw7k}O+IVhZ}a;e8 -(0nwzzN6FjE>mPsp?J}H=LuffIMGEeeOVij=v7GqO8TedSXU1ZBE`HG^%^OclOZ_}IQdtzq5W{okt@K -q>yA%pJ394eLO9L8g -dFy+3$i|Ctv8@W_;^-s|=#gPV2p%!xZu-oHkn{~ZyuL(MxVkw)aJo>X3}Yuux;3zHF`QOP>c`lb_}Hs -V%(M7mBK8yUVeGr(0ljiU3k&v*zz57nA6i`wrg8kKZSxhm6sqVOM|s-d@LliQoTu*%dj780>(}JACbY -5V=cyGQzT~B0F`JNXv8oN0Wxtl>;_L~FxhRz^BWn-=^;gNA0`y5%>!JJcI -Uxk{C^I1q3;R!yqB=1FwYLVtBI+;zn96zq0H6_Q=O-mw^}KzrZ((=6GSkwq^*%x2$CM;rsCzM{DoK`5 -`W(x8Tp;tf#vrw3);EH7>$o3M1{*I{8ZKyKkM8)$AXrd15>Pib$KPe`}Re+3?lgbW_+nU+F@WvSbgw+@g155R(xoxl%*AUS#=~R>Bz}q?0`J|(>c_lx(tBK9TpV7>XZZ^ -y=2H;_M-{h(dCGC7O`L2I_j>rG0R2JgG>nus%I)X=%Z6plr@IRq_X}lO8`oqV(anG#E8fQI$vFBa#Vf -X!cSxhw_1cXPiL3ZaZo(K$=rL1=p!l=Va0G^?O$41%dBd|&@nyIcsQen~fN4A4Cr@MVWzvQSd~tFG%b -iwv+o2^ot)kKbHDW7C8f-$G!mAX8G%IRCKI+nc+bhX{hkg~vnx?}_;1uekL?fe4);#uFAaDwrm`5?h< -Nw}refxg+H+aAMyG__NY@D910^T5<4y^xkXxG3(>td+EKm!loP7=W@f5f9l;2;dR4sD@F{_{c^ppm~a -`^+TD;w%0SjadbYEXCaCuWwQgY7ED@n}ED^@5dElSO)RLDy$DbFv;)&+7BOHxx5N=q_xGD|X3i}kpal$5 -vtP)h>@6aWAK2mt$eR#U4zE>cPj005pZ0012T003}la4%nJZggdGZeeUMZe?_LZ*prdVRdw9E^v9RT5 -XTpHWL2sU%^vwsNGZ46u3{u?E$&8X@mCKUYnvHf`uYWw5?lNR7GkXqv(IX8NLoFd3T$hfoyDv91iF8n -IW?*`;M!#81LB2Nga8w+PYG$=-Jo28~7+!t5>2|RohF}_KJ`Ds^)C{@2OMm7vcoZ|5eGh#rIN)e%d*v~Jq==1Y#O_ -+-4#PMO1|2U;>6TtK%(~j%l?wHFWOq=?Aunt-|q+9P1WV>?KE_JWhM>nP?Yz)Z^T&mg(}#Drvu+%XFO -+LzkU0I-{S+bPq0zkkeUCZd7Q%+If&jdyEt=FZJ2oAsss^?b&2dycD49nCuV%niNk?x9OiusPxyF!#; -KLH8@^sK_*C*PcVA%iFsbl!08N4-8uMi2Cvnd;PbDr;f+EZtIdrWuFM#h()a3nUiJALa7WZITpcYj7C -^{~ -%`p2O=zS-8&e$_FEWTy!p%){62f~1FCs_5?k@!;ohptjd2G0x~!+CM8#vaHSG2EsVcgN$vcJkK_p@LxYk0RYPd0y))77fsdu>v7lGf82bhGu2fs(t;*(6arC!W06u;>2_Pn?2o&0f -JVf8=T!jSnl-1Jh0f{?00PXu;CaW0|%B%R+(fjI$4&*j1fZjMf#nztB$3fC7HCZ?|3|qgfEQzS0l9qG -vz!BCz)mZ$M^_i};0~V|Dvdsj=NniW3WQA9Rc`zLPwDZ4IE|yXmmbzT!%yF^@_^iwaeao8Cl(aEkE&! -YJx4C@Y=-($jOm!ccT5qL?)zKO9&%GP(KbJbtw>KFHgyy@$+&1ji1=Z@e{W62Yh{|tX18?d$yFS*Dk$n%;qpkln|jIke6Xwl6QNM5$J3{HE!8X2y~& -h;{>v6`-XqOFfU2Sz?`e&#;s-9#4QXWu{k2`!jp!<{(C$mtO8@gkA#j@BXEML-{b-xt6<`9zxxT#*e` -%`bihJ8Ky{g=L0_ONj#(=5Rs=P7Ublo!bzikzwd+6@XU@)T2SIDUhuJ_zR`(p&I2G*8J}g$%Xa+JTRM -ZhTxUUc@q2+Le(={X_JizaZVGZ1rz1$;@@QjeLU_ysq3*xxU4WX**V7xqvscZBvGVlF+WR(OM`TjlKf -aFCiYdW-4qQrs307z=~CJ;;vz1S;pZR|@YBhXl!Y0!p(wZw@=j4dD^h~}CU`}xOj*{M?X5jBMGtt>#H -@Tv$=>s3`B;UR>b<^ulTq9`s9Uwb}kyhofEeKHZsb*S@%EMQgv60xVnsdO$Hf@!4lJ*)y9s%+aNNcbZI*<6qXT -JTk-@$OMjebZc%V1gtt#BqKxUty}5aB~k%9Y -(%yKY-OPf!+1ADMql$c35YhWgGT42LGk02gVgrRil#U7NV>nxVfH)uA>_7E!nh*XFT;Xw!&{2vg8~u^$_8I#LtA|;h+Ga&Sf_GhK)IIL#^lx5@&%3{ecwxn|O!UOl8?V!on?B?8Elp7n~undn4#?2HhgM7h8i@9`W#lBeuQB%+-&^kfwoI-ix^k7(Vl6;%F61?^D_x)B(_1IT5 -LSOU@@9lfMW75%(gAVi>7XnS*5#G~)HGB6eu0$ -XqCfd`H`GP<<1q448RsbtkjAKKsO#nY;fZFxwBdVB>0|uV@T6Z;YH&%}oYg#N>Va9UVu?)#0^i@8bUE ->f&V#ix>Q4r>Vt9d^6dwUd61w@eo<*N(7*+&avnz6|f4W;TYV2ol+>Ol{0Tum3~eId*n&rWTb3QmE=07*PZ{w`ww{F<@`!V_P3XFX2ebsHmYp7Pq+6N6(z?CKdBjixZD~l2K${(_F3d -H1I|@gU>b;vAZ%WbY)`P(1QZcPIZ3JJ2U$>34(8w?$8Jj7~~G)1*WN?Yx$^X7`bgc&y6}y$a-W?9njvJRxKuk!$5>I_Nl2)xS<{5-H-Pmu(zj#zMpo3&9ia59x@GiM5}(H -g||y(V+Dog4QYIm@VLgV^TCPgoSjCnEobb`|Oo=E~MFsq&KDGk46H&(ZKzCHpdt5*vGEY5R*% -k8a&@tVS31`6Oo@U899m~62nWSz^fr@ABDKXodcY2Ak_c;#pgiN9G=J99r>Y^3TiZ*vCi -BefU{Z&W_}6tH#6gb>E;=Prm8k+9;J(?G>HGS=6NMAz_4#|FonnT!M@CSK?y^{kyHY1jiWEZ+J_Kru6 -u6;*OJiXkLcI?>}Lc3$m(xqq2ISGi<&69W4d@kM>>tU{i_>&A@+ny^ohj_)^9jTU;ltFN!uVi|NMb=f -B=LJtitmJeE(z-{zZc_~{*1MMHc-u}o672ma -EVPpn$5krtE}wmO3nIwbyX*pgvp&1sAS{7jAwmWZ@i9^kTYpt0XPiKoyP{6&*gqHoKZ+sjnCPgQ3VZw)fHmL{*`9{ZS4k!^&-w6Y5#fmRaTfH=CEE)2H<>Vk>o04)w=`4yXX2wdCZPp9F@ -br&cw&*1@N3qN!7^!F-H;=J__gKhCO%h)Ht|CuW&_oMp+^q9^hxAR;txrPGvx<7SU&q-@_$R#h-N9G8 -$}haLlPYbAOJKSrz_mkw!?k`Gw``RIl5jtPNhfZcC&vXmnRT3+ScPpG}c`>7mjvHukaA2^;l%m?DVl% -xo}SQkceUhEw)go(x5())5+d6?3F#U>bR@_e9Hj_^xSkxd@2yrh`Np<7Qt#GpDY!rYm1)LLE5S8qVdD -AIv19M5*J;^u&8O=ve!+FU`C3NuHrCL(Bw&hvoTS}5~A7jQLKemVM@^_VCxzCzGrfR;!eu_)HxROU_J -4%G!${V+OQ2aLdSI^*czK*w%#KbDhxl^&&zs|nHtpez1RtlKt@8#dI(;zu;D_dCU*voyfsCHYOY^+NR -jVja!M~*B!xg9be*abBQ{dg$I`}>ta%ntBsU1BL%!b3rWGIx2v?V)8U{054s%%SMhA#M6#Hb15XT|`Hw)SRp_^Hn83ZQ!E?*uMxo8lM2iFu|Xod&xnCHh$5L&9`|Fh&| -3jBokvPfvQdq56fTxI}3Xggdzzt`@ArPmRC&()cB*6jYp16VbflQ|r_x$Zb0mHZ4<6R;Co_y?H6ic!JX>N37a&BR4FK%UYcW-iQFJob2Xk{*NdA(IlZ`3dlz2{eq!hr-*-? -*$)#D}Qh0*LljRgsfS8n=#ZY-gAK_lzBHlD1Uh0=eYFGjHb2dv@|X|9o}z9a`QF2x2r6M^v7Bp%u&WJ -kK)vsI3R>vfklS~=V{Cy1M+hS}tWbgU;(cXS@dQxfm)cuOOz{b=g -2T!BR+>D%)tqj2phAC;Rd)}63fmUY;JaCJzmBb&meo_0%jC7iv5Mr*mtfG&%EvI=gIMDuouuOMGh@(M -1vq*N;L?vY%fq??SPM2FoRU!%3!x+0c3iFf@~E4-PDk}-tPVMe!Yl`HlA>S24%`KBRJ@Vcu~XrA_x3n<60V~_*~K^f)uQq6?ebF~utpe8rLwG*bcajc< -6@3tF;}^JP~klUF`=R5F0nccq8&&daD)I}!{iWfAE*YtfxhRKlmJo_TbkJsMt+QNN_%K9O-c|U&=w9> -_&{#u79WBnHF-w(x-J~bzFGly;Jl5x1UXiQL=5e=Ftr(KhA8qvWVmfBh?05=AuL~z$P8$Uk}VyS>k{? -CR*>@*aML*PPcAat52=5}M_gN{``Y;nxQOS<)14f5+l<8)!C2j^3L2}Y+ra~EEDd_H8?;&Ep2pko_Wr --qI&iA@_=I~L}}?zB+F!$DW@Dgu`4lxfImviG?0#OZjwZjK(^&2|I$f%=KKj};<>w -ey3Zz-P~c6>y2w22~Mo$Yr+4%dm<2hjAO0J)OP0D>h8r6DAexxtz!@k;EUo{!hrP=Wub7f}Vq{kzJWu -egOUC?4LuQ89~p&e#auRdjyw$8AZ=R#VS(V42k|yNSS|7{69>F#$j>P`@}B$4Nyx11QY-O00;p4c~(; -|#ZZvi1^@tn7XSbu0001RX>c!JX>N37a&BR4FK%UYcW-iQFJy0bZftL1WG--d-B@jJ+cpsXu3y2aC?I -XBkOKW=zy-E6U4j;A(6}40AP{JYwzVqW^t&q)1V=5;xgCO+XQu;vMhhx#y0J21rDTM#N*NU(dXXq}PSrmb0IcelLFi(A%ILgteFwP8xDd@U+gE2rZ4)A{V=d{!KvemVb8T%P@Ll9A6$wW3i2 -ExduV;DEfWdF~u5=2Uu3E&>j -a2u+rtDC|8^2MvbX7s`52grPBur)>IdY|IJWCMN3`Kjb2%YHWHws+L~*2ucHXz$0V6L9Og@=Kbp0qC> -pwEMUh5PRIvpq;8U^Ex-Gm}ct(Q1CLTH|dG*$V@(RqnoCAGiwN%iGhp#9fE#B -l$Kd!ymUo)HQ#EcoS#C|y0G>`SRV@-1QsRu!kmcUs6|PpQv>Q8=>&ivBweD>Ys#D6Kgc5H48_z+++kL -2q-w=Rcfo6c)Fd_crEzYdnDk$>5S>Y=EA5XM)Si -vh&PftTU?GzDi$YE``zrEM#aUoTIuek9u%j(hu_x;@VBEeU4 -K(kSa>syCu=G&hNy387$eP|y2S`h-F2`$%He3T7i;jE>tURQ?#J!lJHzf~$t!3EE@$Y3POf>Y4*a^hs -7O9FDzNX~3W3_i2;-WZmBe9g150rI&36-~0uktxnLpz+sz+C1fela&HsjIqk2>w+2Ef-J^-k-5I2NsbI`+}#ij(C=S)LKCrg-?l_j)&gmvuZCKihK&Nk4T#s;&_}-IUj(Ha^3pg6 -pD4`d-iKMHc+p4}kwTG9%-%i3(2PlY!E_aMvd9^Un8B9~MXOGYTv&q3Xe6V`wN>(4d+nIl@(B+de$xx0CsVn)KIyu&jPNprfH0gM@wy|9Pk`5k13*}1 -eo-R(er9ql`=qs=@4?@rCV*`YE|1HVvRuKs6SFtpetSxx%8I?}`S4nOqf`E8ahESKys$Mpg3P($)ikF -W;Z12jI1+#NUqNUeR^F3^WFLL&#pTlU8eF$ -2(i98S2n1T9Y&No_BdI3NV*mG>8B!7@%W2w&R+i@b<2OS#2!i+g`M|zhe`M7+q1d~-yN^K-%w|c#iy} -$bimd~DdCSYKxE23vgf^c`-PCoZjL3qB1)0<)$yK#a9(O_%Nu!G3em285Y$LTu4U>B$cfXQCqNVx3_aeI!_=Bs%TlnJW4z(<|E!9PNh}avMg5ql<6gQQM* -n6-%VmGysRmqEp-8FT~{VWGc{>`&5_D#g#fwWk{TGny$O_W)s;+m%qd<$CR#>hfP~9%3!D29j3(M_GTeZ(d>=MC0An#ha_!PZ3ibDj>-#m1x6Fqv4HnElG -Dx`JiXBSz2(d*$V|2UOlCP5;$MZ82qV$0T?rpvx&kCG5kvgzD5jwx+mw<(sO6d7QY`w&uW1FxD}>RCF -9Hw8>=>7vDj_(?F8FO$Sxz@?DOsWct-4ZJE>U%D#$yh -&5j|yK*!twx}ca4qTU63&bVgJxeY7+`H8&+qm|&nrqT8M{;{^lF -t;AIA&QGi->%LcK6_t!V9R+XvJs)##oo2`}KCgF1)W7$Mt>Kx?*vOo>VZCNSdslCXd>|4lU4zrkel7K -NJmZW$L5H=2-RDJ_w$jDE&IWX*=5>wtArEIxPcWd})d-(`usMhE6u8T&Q=!zM`x+vsBS1Eg5*CNA$;a -vv$@$lL*F8H$?6f%nWS=#`W#wP4>C*dndqJq*^E0k$&pIp-8(`8Chx@yGup(-sDnCl?syXBiTiY7%Vj -l}TrKKe8Zi3ac_NsuH-!Y(md6!~|61QLT_mS{-J5|azXbqwnykuc{&Rf@hLc2L^D;N-^m({>O(38hyK -P3L{Bj8HX1%KkGHf&dW4m$4;CQW!aGwPL@Xoedm|6WzLCf4bJi$KI#jItqmE?nqi8$}IrG3WGpa#mcbHwKwFt|BSK&0SQ_phOB()bFy$V1ocj?8Z6zmZWX^)k!3Z4?q{x!_?!? -fiVQ`V+Tv|}wXQ`;r$#qkyl8VfsRfX%_!w{$1chXl44hzx!D7hqfsDL-^&j4KAkhw2ULwz -mn&{iYUnrT|sc_a`qN`2nDI6SwH@0aKth!6|KoL?^X<rKqn4i=~~G7VLXXo -2n`Xs@%*5v;<`e0V{(!%^^*IjV%J%dDnS;r~zE1y_+KV=K`t=rvfZXQj{r -F9rPDT!U$F=QH}Nc%B#b~~Q)`3q%a%Z7vvO6kYi(y!i@`-dA6(ZE%3mFHR*s$tr~puXCIJ|lzRd -DyHw0$czp!QnhpX-Vt%uzXh$v#oq -VC{FNgs2z1<4s}aP}?Ai>0Q+n85Z&H;Lkrj0q0A0*(e1=wb!{I9#{3H*pf*;)2~Z*1%CEAEk~mp=(R( -~GTj!(6+vmR5olxY)dp36R~O?n-1f0fM}B+<&TLnCdi<53g|zm}-wt=g>=SlS-_cTOW8$WjJ=|HCDlM -RR`M;hl-sC8} -7?F@1YIl(9)sDB0zOWu44FPOBcHBVsX8~vSlnm$5L4{#7R>K1Z%&Fd5*ct5ngCLv@ZlngPC -8Z_ySl1=KU*MTrRR!sU!|8&kPYj6m{sB+6-d#X@3E}>Y+^kyRHs+(iUa0LWGicA|^ytpu@Z2Y?gH^h! -UlSdU@M(mFVd>}*G3iEm->p^R7)|4AkcOUupB>so~js$tzlLs -7D*7V~Zjw@EhdU@_$^H>QbeHF`W3Q3`ySxM6x#xbR-d1}0+Uxz{!Q#5vh7(*yNQJ4U^exQ|o7nHG(P1-3X0FzY)bj -EZ&T|1VzXx#cXJPZZ~B@SlBQAESve+D+`pnd%I(7}9^SP~lKwV%B!Zz3My~4wO@K+-HuDZc~qG``u^4 -|JEbgF6<66_%E)o{XMaUOwUPgJ;lvjgv2qQ{U1Wb#B)VeO?%7I(TP5hvWGMofKFqx<=0T*%`t1PT5m -1#GmissucPSX-7xq;txk-P6%v0eie_j9YZvfA%tGq-5I?XtL973J3b)*ITd`Bg7>Ws!Kh=za6GF0)K6 -!(Ciz>Bb)8l4zhOtFqV%>`4T%&(8DxT)a|AH5Zqa#OaH2F5W7Q-#^DYnmHF&JEBRP&BZklD^Eb$n|hb -w&&M23CI&Fb8%7T@Pm9t)x5U+Ri`O@UnPCzq8%Y;c7MWXiL1PzY>^zNkKy0|s!46|HL%Y>K##-7{bMb -FCH}Ag;VheW38UXK03Hr(OgLym`AEe$DIS6_fg09LsscPJPdk8Hru_)K7QV)`7JX~+d5`qA^Oo+_8!l -Oeo`W~Z)SIehF@*Ops)5Zy~u}ApTa6o%eZe9dXB?(XzdEiwJa^b>5a44u-#cujqR#7Tfah+9>%pX))< -Xf3nqf42jVW#qXxB4xbD96u|PxnOy>zYYV!vW7zlL{&sUdo!7kYLcZ`I2{$@qTlk^G>Ys5OloA47(8CDY?gp{sD>)K<7s8IIw8P6 -Ne|0_gX+lIx<}FUHv&C{M0mUwr)VI=cFB`P1dQ>oZnuxBfxyaFW}Jz`r8_wOSQP0Yyg`(78cb5X(Nki -SL0dQ}<$nPx(ya_Fy}Tl`#8@f?;Ygy_WJ!Y^rK^c5<>*RhaEhp8uoVU;q2XjG;{fdZ=-T;4AYno7)Hc -8}}GzgvHpy=P$ksPwBst@5dgWojpH!{-XZ~RFrPJcVKG!;0RIysnf8KkMUmHgM@Ywo^??)cX_jsS_A4U=s_Cc+b!#yZ@kV^@EA=hY7HP4mt?pqxz(W%Z(uYe#I*qe-T{TZNf1?tSdD#2X5B -QvAv9E;D&mF|pnx0%Jj;yc1jA{OfRez3>7zJ~BjEgPNxK>`4mOhV_wtD({0$B&n9Uq$?R`6_T6_jj4!LrVN9-qZ8 -l?iaZyKjcD{3AFenzQLO4joQt{PpZn|rJ%uU4D1ZA+>~(DG)4`~i^~Su>!)3&5)f?c)(o2m?=Rm+m)8 --mBBcQnhx;h*b(E=l86|2u6UYCG%PF!mvjPqw{rw_9jYze(DAprDF_DXEO_FDU+Ch^aTf=CV0qS-;ir&S&HStg0cIjh17(W< -697g4dnxIFL|eiePIfu;?t+9G$LnCSQ^^I`F={8lR}je!`cnM-&pAg{sRm -#wYHMF=Fv{na1MjOg!B_znc%9|DkqMdmo+x!&j9Cck_{R&AKpVli@dxX8!w;1Xv$O#?FLFFnM3b3inR -)yK?Q*nRiH-h3JdFuyY+<0L+l)d2rWUx|#ZAhD+n}QRTsmUA|_?`{$Rn?b=Lxo0&7 -)&iw!#k(^+?)P|L~&_Hg=n3c!E*JZoil2x17_>ejyi-LB|v(1024fYWW@UiNpm4U%2m1=7rHfY`X)U+ -dB!lo>#rfIpKS++qhmT4#~3aqv65djI;}_4V86m)9SzFR$L2WjJTOtKcSTmLW_%kTIFgLVn^28Yt7O4 -rUD}^&naop8~i(1-z*pWMN%;P@GQkG$dyj!^e$Ez@-$1Ct%Xi%@E12j6<65;c3Mhy6-n$fe79L{!Of6 -l_5~k2s-mB+gr;F54nL;mHVz}F^s6Ft6fc$Ytx(|`@#0fP@CLZd?5}E0;DZ8c&;oL9-Ql&S63fz=Au$ -t322Mc@Z|wDVlrw79&BQ*=N-hJiISO?-N&G(e->W>EDf&0w>d3gDRMQQHF^I-qAESM>bK=HGxK@)@#*XK2YG5@%``UX(>TX3Ej+$LrT0qVu0 -9yyCB~&Tr0JCDX3{C+ek{#CSEoCVsFNeB!xH&~$f8*5mH;$za^F%-v`4iE1RDgU~Skw6p7iK3Y>xGZ* -v`ruBBGr~DKu5)4X(3%!^Ib54K-GfE$oRdg@+dOcwc4MVI?OF?WOA;Sb -J~fCEQedMi`h-%j%@<<`J|-wmfO;hx2m&7o{L3wU662$VK*Ltm#3&4D$zx_(`jWcW0Vbb%Z>#mxyqXEt1xJ0@+TitxvMCH3W -%D3-s=q?-32Kts5s2~zKJ+scS-2NabF;Z8(!5`b$esN -tG|Mjmc&5AH8?y9g@Sw-b1UaJz@+-N-nl(&1vPTsL$zJ`G0bUvOdYI=%q{aK(Kzhrvx}#ao%Ls}0sPG -|=jgLyO+Vx;p;Zk`rqjWrx#0_uQI87fsG>Loq1Mav98qbCayllEuj^Gy}e&XiOpYlE&u -VF$`-VpqKJO0`q(yQhvxiA6@`9n#8FNImNnPTQwY1ZP~V`l15;FKJFs`*qK&>m5a8p??6vbU`0n9ADu -7xy5E-ECuhpppV&rRwEVi>B!js)6Sz^+buF}%Jy~ubmIMnHd*DBvu!PJM-gShqv#MJzlzH>%q8_!_(D ->$5d}m5JoSh~_vUSB?RVD(+tO%6B|!jjjTmmSpY~O7c%P_5>|1QEdk!r%WBb+pV-MkxYj7!Z`gcYS*S -3$7sn$16yWFa_^@IaPt_IRmE?;!?49WX-IJ%j;|Ay#H-Fbtyr`?XkJLoaJixU$r#FdGzrJ&%8)Amtd; -vAKO?pm6{oP3}rh8D`M09PzC$xT5SEP*xv2m9!0ySTmc@jtIBu1nS> -1xKYgrhd%=ML*jaD_0>nmKhCl()5|yS3k$|kl7M9Tiw!u?`Wd>^}USVI}jIY^l)wStI5G3 -QWRRtmwTMDM&+b!p4E#?qdC0rVA8ph(h2Wt~Q$gnfUP!|?f-`zVGFHgU*9v=5KPm!(1X@?5Q%j`{0;SA)-VDo@j3^skMTb2?1~3q5Lw1q4;aG077swy3Oq)BELEiOiAm!Vv-l -cLEEh?UDXm2{GgH^OVKXaBwED@Lf*w`_~K=Y-fU0`^91G!V(0t^zb}qb*rtVnwJ;?xRLUwL6qgm(fk| -t7kZhwRTP{MjVFIc?E7M;=w6Xm!;^T+6o@8^tNAt8+5F2VCx5o`!DZJq{ -S!q4a||ad3$X*!5E^c-)N*~Eo#b=C$c?-&>myn={A0ftWZ1Q?&!H+N!Ep?0?bp|bpd$GK~^ZMXO+8IX|0Jt#x$TbN6+>r;AaEZq2|Fd -NY1%_@#s1i_^_m6D3^yrFJXGgd=h6DN^`bOS{nH3;fQ5Jm2`V~4gD*@@2tX1r)skZfL7|Mqe9fmI!;} -2?qA=SqdW+g7|^2AH8|z5*)X&kx%)%4D1#8JX5@QHBfRy0iR3U~AJFiNtpBGE+?TIbXm}Z`Qrj5^ArI -y~TxJKSo7sKv9ie=kGfz?*94QcTPDa7m2>{ni5zahtq(zKG<~=ufNQ^j5JM@^+$DEZYC9@;jb7}J(O^ -{LVNK~WNmEvG}E^_%8`7VCr+5b7Gy}$gOd^8aQQ-mbVY-}9pj)&Men)4RPF+Sk9JBKlB_X7`f5opglA -X7KEVse`R6so3%xjqmw4G7y!!2X5!#_V|6_S$(}e6hKHHb9iZCksw}jnL>6iYmjV^zX#$D-5j%;MB3`?oW5_Jj||XG38fnH9#n9n}FEu(M -IM)%DWG+O(bELuYU0g7sD~6JLwKXu3msv-R%AwmrPlp8!4Ub4PQ=+vj&9)EPPHUq9_xFf>?)QT%7d^+ -^28&NrOKG>OZUo3)LL$Nn=3gy|(eaFQ<&@9Yc2Yjl^w&&b*y%QbE4)4f;IZ98wNyH_$xjmOjNRv%a!( -~bV;Iod!rUEyXuzH&PSgVCn=%B4h)ulYc_dmL+fs4Jbz;ruQ~uPN5t$SaBCZdEQ#XEHTFmzimTj$M~L -hiB#`D4D>JD82Pk!=>h8f&V5GRdD#rl)4!l7GBxGU>UUy{C2>c+C4SOqszZ$ztFJheC$#4F2LABg6YL -yzQaM)e1{O*Rg-SgQo5$?=IJ&jKf4nny<*QgVg5Gm`mWFlY_#V9kGy|MO1PeM2kOj^GLJaNg_xmEHnb -pEtLkgh($BB95JueMAByQ^D8sq9qG6Ls74e$Vd4Al-jpo!0Z>SX>q(7Awx+7s}dI5s3vHA1a?eX(Y-m -Akklpiw7O_{#)ZiQIxxfpJOx9k1bob6bH+T!kH3?TAhzu477KHtKq}Y-|UtDz#4y8+Rb}Epq=p{^N-F)M7a+;gcW*+5P*V2m)Dz0tV4-^*V^LjFVuFy_1Uo=nWo8}a&*vH739H$!@}+ -*xW(QK**(e89D!w09j1GOS;F44m6zt-Asr4D<7S4|lm2;>8GTNd<;@BCZIS8ByH|hsAP8@+z3ai$k;W -wY*KmM8IE)<)g?^QW&V3RS3Lb5JRp{QE9RdS^?R&iRx)`DjWzq@m&A%}sGZR|p&(8b8v3h>Rg}L3Xwq -}FX*VJ~$;kEik#}mI|q?gJUMLT`$YWmYWf4l1>{aFF#Oq@xdt&6IXxu76pPN&T~tZh$iGBR~p`sES>s -H>xGc1UlEI!j%BnBv>cDt~-(7^li6wsr--QfQHrt%)GHw)>ln{oEJj6NyT3!fHZQrWwO9rt>7sO~+-j -;Zzzg+CY{bq;FUdn_#7Tfzjq3&Fg12<=Dwk*-*yL?=nbm(>3Xg2)Rg=_!sRiHg4jt#-^C4lTO)XkAM? -@i~;9;(~!u>Wb<|*Yx)qYj1wUbDz*t<9HIxUlu5DOBIptOqGmV2%q0CL;}q@#HNHJ6JfI1bd*@EiWB` -d#a3mwVO<9MoA>Th@9T(P~b>IC24W5O)D8YB=zWiV|=uR-ff8*fVq`dWO%_~nfyU-N6{W@;9N_wEd=} -4UO>9nl))uUM0)?ZgxCNCl$RMrgOPLs5q@8S>pv*TXAQhG;XY1C&@QU0`FA+%?AB~*84b6h^DJ)~bw{ -ZCL!0|XQR000O8`*~JV(~nj$y9EFM+YbN$9smFUaA|NaUukZ1WpZv|Y%gwQba!uZYcF+lX>4;YaCxOy -+iu%N5PjEI43r1kfFvk(d=ugVsqNNA0k?=96lf3#OL8b}MRM8Qr4=RczjtPrcgb;+>cN(|oU>={$FWu -^*iA081;{j4+QO9#?FeoyYGIWg3}SvIm-%G=h*_lcMB_gN8fzx0iZC&i-R19h57&1O^UIqLcfZZ=Z~w -X;!Fx2h$_%zqWJb`LNGA9>#N0|cuORNwu9aS0sw~5hPp-9Q3a{W{i4dzKQEO%zsx-yS8W33oHxCbgz! -F7-+Q1q$g3@v!>4szjB8o7AU_%gBiXKI9Z;Lp#ATwA<>yYTz;bC}ua`@)(T%5cPg45I2r-##{b9{3)J -v%*$gTtfqC^`$LN0Y%TLi!V-FbMBL=0Cp9LKP9K8OVr8NxLC7#3H~@@M~4LAi`J_34O7OK!md{y~#Bi -Lqv5MFEb&JrGY@BSQ#cO;8AFa1g>IOi;W?O2y@B^L#PavY7Lg6WKch%1~Lv+7RyDUF(TS9hI^G(#}p` -;B#Cl8LGyUag`eOlzoI-@`+Q-iCvNL9zi0vOTekjRaxy6rj(QH0?iX?GPt>ZcXj>#e&)vGM`QH}=*K6 -xfUm>eAQ;YISY=}PG#Cu1@j(k|a85ku#tK`QeQxzxJs=NY{1eyd1@AP$#CeNlS| -Ubj3BZQ9Ry8dVHDL-eUtGyZ4g -N~@pS-0}bsPK1EwqP$Od98(V?qhI&RZ?01bu^|ndYw9&LI0Hf$xNx5ZP>b7tD@q*mk0`1Gs7o_LTz;8* ->N{Q|$PIy6;cHZvU_Vp?^VIxCCVO1)~ara-^x_>bVR_uyZ8k1CQX={QQLdr_*+A-fXv6|s_&~1l}MEa --DO+mj5y+n9tNz4R=?aKoiz@)t*p}jC2Cz6t(g1>3`G$g25DK`(gz}fb!Eu}#{lsR+z%@jW9SkTDexV -hsl7JuBNdDfeM0Z>Z=1QY-O00;p4c~(;`uX|s*0ssL21^@sb0001RX>c!JX>N37a&BR4FK%UYcW-iQF -L-Tia&TiVaCv=|-*3|}5Xay1S6G%NrBY_zE0vdNV+fckkp~b$o|{}-gJTEV*;*m~ch1i5rqG8iIp2MK -{(P@4rSw!OTNg_1SZ;-OwXvSas#Z{e_QFe}6G~~4U@R;tb2vzS=wBPFLTN3mgFeYd=R)saqWs`gI@@k^aMgB%#%-;6ktGm8p07Cld|@?tks_pyl%AuI+5JG=tQSqp~GC -z$&%2e0^4#MqMaV!nevRN&K}&j_yTrk+y%mHWEj6--{@@gXhs-g-$%4KU=Y6gPHG%;T|gR|%t9L@p&n -^EMU{~@v+F#Io=yLb^@#h5A&qV=iRxprx973Fhz3LnHtheK;tk+&YHuSwMCsw=-{6GPKeG}GIOp0y^l -0K4tIgD%Nq^ZQi1Q^jQv;SMU1yu_|1tq7f~YpRZD78*nzws#)ues@^R#%B&UHLl_jGY^-&oydM!LM08 -Z?;|ucK~z*Z$*z#pwtMb33|;!8q$BXY1|V#tJLFEQDIprjL2PB@j2bapd(V6%f1tefX`oade4sx)*B^ -@xqj_%v?Bm1$6QkP)h>@6aWAK2mt$eR#N}~0006200000001Na003}la4%nJZggdGZeeUMZ*XODVRUJ -4ZgVeRUukY>bYEXCaCrj&P)h>@6aWAK2mt$eR#WNCue~A$008w9001EX003}la4%nJZggdGZeeUMZ*X -ODVRUJ4ZgVeVXk}w-E^v9BS8Z?GHW2=@;hDBsR&rdwBQU3slgA -WIIpo~nu5crQ+8y!s8~&hMi;DmIc9lx3Js=MNM3 -U|1fsGOElaE{}r08}e+v0p=?XBqQ2DCmWaOEa5EzTz5y9LwS)SlZ%hJPp(oJGkKE6-n;0s?u+f&GlkR -8O|7i@en2TM(MGVhn(xq?C+mR@j6*Ox^@^^85Y2w11Jn_)4y1r&rlf(}n6eXRS*HbT -P8xEoJ}4cS2GIZfJ1$BA6{p2(yrYd`W)U(8zTe2LbZyQ+(i+QP5YOivQf0bKl;A3D1eA(Gbj@XJtYk! -;dB089mgo3`xdB#DF!$wUtS#<{mY3=Loc@?{Ae$Nm22HpNrXtC}89TJG2k}`p8byzeqM@=$75l4Xy(a -1Bhu@?WG9%Ct24+652~~u}c<;!)=w53e163yiU1R8g78MqK0{%Qmab_Lyl`4`Q3rqe)Wd~eLSH=k3Lz -?`I3O&Z=71V=QYLywkW`xbIKZX6P97YdIrU%)Dp2_?aubu03y -B)A(Z#j-3_#`@(m_!ItXd*wZ5ncnotx1Ap=8BZSG6+ -iE3!&IZ-I32Q{}Alou5!aYOWj=FIIn4M{uc`!a_s2@_212UaPM}6=2rocy{2DFm%y(fvfN2MCqw^D{o -s1fza|rx{B1+qLq$ym-9&ZN1NEBLqrJ7|>}?;LV=tj@c55mb#|!Lf@$(>@vD<#o&`ptLu9^k!c}w7cx -)Y#$qU-OB^)Rx6P$X=|j3G8s&bLq>fAlt!pSDo-<3c>+2)u&1r_$x6Vz*eqwsVKt&zynL!TQ=ItTw1&FSC$iy=mLKiXz2<1 -77_CibYA5cpJ1QY-O00;p4c~(=zz6u&A3IG5qCIA2;0001RX>c!JX>N37a&BR4FK=*Va$$67Z*FrhW^ -!d^dSxzfdCeMaZ`-)}yMF~wK@kbFg}dRf4+eY~mTQ3myL&@TrPHk?}<7zuuAOe6|WU7YbKWD*D_a2@>VfH -w-vLW6o)tON(pb?(>*IbwGF=ey}_eswIm;zS{TGl*`(P6s|zmDhUQ-=o}RX@0u?EH%9*C9JLzws0ysZ@ -;~M|0%nEJ+?#ZAuUP)R+6=%%I;a!u%1Nk4V78LnxB9EF|&(;?U-P7&K3(aDkjLrSyu{Dd8gOnOlvgDl -Eq??S5!)py|xU#t#>@`?I&4lhJk?=4nSe93CK(@=AboLkZWyqH?VvQ_zJcoJHgmj79VeQ#(2~1xCH{= -^V$lw;$U$ZudBnFY&oyscL -(OXrIE#oD(PZPY*^IxoImk~e~XW%YG0AjI_ive3HN&o1T~|eh8(5ZJrFj=|B?>A+#QLVKnOrQX(#^<+L&9d%DQDe3S -X`_+M!+0()NwggjFL>IM27l4#@>k_l$5GlGHr~|KjzfLb(&wikcO0km5j`i%uSU9vXSi+o~WaGO}fa? -Mnq_2H$fC!VE$tl7MGPAk7{K)`+6qx0Ncv**t6>t&(8qkV_&td1Rz!fEDQZ!dOF;fEY~pjyxja_f*2s -aFRXTr;Z!m0D)l-K?wY#cVNdzTB$B2k+@qgb$czY%!+7@X~bE*WGYU)Q-(B^`Mn{YP@f~qwnq~v&O>u -oKA=&t&Rbr4GD-+Xa!q%f=~$E((YWfQ+laTG8+wt_(d8=^w -Qxd>%^x;0>$eU{69oKW-)5;m*K3Vo^^(SzC@rU*J=7rO~7q5E@TC!^a-y;NUM6N+RuXbP=7iuY;e2#b -^F4KG8}<%QK)>p2W}EusaX!uB?j{@bpEV1z(K~%KX8JG(7rCSs#VDS(*pHDJ_Z^kxie5 -&!J?HAP{a6tjlpzen0fQXvLyr%q3BAHjPFzBAY@v&%Y<=n?J8_Plg%>< -pn(_n$2(NV^b}$OXH*+kDV{I>V+83rDO$|9<@rKhg(Y7AK?x@KCPV3M`oh{vIpO!onTPvf=n44vgt!S -Rcb`BYoJ8%`$&M7(_&9qB~RRI<6%K-O`q_cI^Z%=+62Q-R5MdkF^}D -#?vJkKGF54cjcz{mEajm&C|RAb=b_fh>=$Ug%$|&jG6+UZ}2qtd|xDVPay5nTjtAv)5#E@DOIP~^3=t -kv-pn8YpN0uM;!Lk6JB(ioGz{KDOg3$LwHtjJyQ%`6ZsD?bwaniRbex2TTX%bL(d~(n(r#X8W52`KYX0!oD#M*@c8WW>2V4H>(GR`Y^!_bd`DSM33 -YwC@`qBqlwG2eOBwyHJQ_=mlpyX*yp0qusa5fWh;%NX!A|8fxJelZmV8G_MKY3nbMx+@~zNZQ&xOx_A -N9cM=wmuqZ>(L^7UAtLUDK+*wXL^U5z%SkGJ@F@VIL0Eo=vhMw`lwHQ*=(NutuN7H9{LE}GNK2RwyMr -_X)?n@FlRP2Mv*!Bitn3NxhbHg!YGZPmoK|J!X -|?dK4e?S#1K4zK=_8&0&WG35^oaXj*M=2>hFW}jRW=G#6WaSMGt^*&aC@{VZ&Czw(THjUB2h!^VLUCF -)*YR-A#x6T0erS9Mlihxws6Yj3p&pbnNu2$>5C#KIGtV}P$1AVzFm*h2S~)_GkZ -!~QN3Ia0H$Q!-}$bGL1DC(i6Hx8#w~zqvB&1$`4Zpt5km4-)KGL`hcd -dLs1L3(K~pPxW9Wylg(^VBP)B!@q0RG-5#&CSp84wMj$&Xd`%W#*`%-uIODRpWsDmu-tjwrek4nnq@# -=vYC%YuYc8o-7|I1i%yn($(C2rB&CTGGCI;ov_6vNu%7eF`tj@Kz@{<6kJq)maE@DRZ}&~u*7iTyaD? -?M!=A5ybKLJ;Ai#&$AIM*R`jPa%gOQ{WOv2GiQx~hFjYf%Q{c)%R{2(Q| -T}$fpx?IQA?w_?gu)(>vs*q!t+=#CU96Z(6FcK>2XHyn@nKwM|K07cF_t_FnoTAh7?EttZj>ocgk_#8 -wvRnLAS=}n%v?ygkJm`P)h>@6aWAK2mt$eR#W^sWd^bt0016c001KZ003}la4%nJZggdGZeeUMZ*XOD -VRUJ4ZgVeia%FH~a%C=XdEGp1kKDGI-}hIrvuCUZosrvqxB^>DgC>qI2JFP}OLKr>$ebC@uDTjY5jDG ->1o`jxypR-0QM2O&y#sF1#=8>vkbK_XsM&1xx>K#}lonEKOWk+n`p}n6Ep|;O%3Albsw6(Rn9XJv7nk -DQt%7$|)jX8-jo38%y{dbmZ<|B46>B9rwQug#R!Df*?3HMn65f}!=yBqqKXfVwF#BVBycW3Lre8KKt{ -|(`b6IJ1aZ&Eurt3x3+}yw-_RoE}SN*Y7+CFI9Z~7KCU0v+DW-nj}?##S-2Jjc+MXx%!uGB)jgflM04 -`#s&p3;_WUfipC+jMy=H+K@3%@L%wa5l1T>T)Bi@@G}laJP -hZ2wmSbP*>ZYBYG4eS^8!831M3Xb}`QEjlT>_WiLGgy1mH{J1lSA8e-?fY6ET70jz3mSkAUzK_<`_1j -}AWiIJ3H+q{C;BS!>v-49s48xks4exqVjF?P*;uI4toNmKQ -j+iw-Cecr@&P5=VKA9%(VXQ-MiOsWPPLF0M9Aik7$j|Rv=)WT^zb<0`+z&t+YmMVXQPrC(XR8DNZqrVmcWI}_M;6JA) -8y#k6BRkf80Z*r=>`T3|nWY;ljCF9BE;{gTI~lPft7 -4C}G|i_(6f#OwGFAI9$Q67Z->Dpx4T(^ZZ8jFX>rUkR~mP`NhS>R_#OquZorp(l$_;%)mhZ>jb+j>`T -iW*9o@ZL98UML-oL~)~Jb!Qa7x}uEn}(kp0E-?<3`KO*3ZBdOl`6b2OefMXojOv8TfUPR_Os8T>vc5D -#U4>mN3!f9XAMYqU;oP#P{|6=23VS;PpK8?1uGt|NFzCObVOlcl8L5nzA -1+c|)V0YdlaVpTL?297Ps@g3CiYFas0V)Lq0Yn@puQ#ET4t7Ea3H33LbqyD?nhArb0WVjhPQ>Gu;WCaZh(wIZYSp!!0RAU{xo9KMF44GkOzEg)|r4fEJ^m>EHW|Jj -g9!johM|cOHrx*N)OoV8^CS9ZxOBIbr&&MJI|cYl9%LU?n{Valz@-7E9QD-}rCi9CSio}5 -6J!+lZ$4XuCZX>#HIJSFeS-&xis4BhFa>YDK({pG-;JE~vI9esk`#VZfWKi>Qlm-kbMqwQ0ds%w8oV= -fB)!r0Q5M1rrUBuiS?0-Tk~8~EyKHNYfrIrmV#uSAuwB_%mC!eB1gTQ@5L}{>%p -^EkO${rT{XCC`y;Znby*Y4@2wDhT2YvnY=?#eRVGaJq{%Lz`bDp0;q#r~moazy2v?IL4j$BVcTn_ -swT8hne*^r*Y_gprLPJWA5Zl2qYEZWjzt%f+=rDb?IdmM-vgTkAD3*YYSQZ%$>0BhlE#eKd396KL -uUUESe~3uA|hh^0TafinX$~cM)@LqlEi^6=`;hEFx26 -1=Jpl;Gd$MEJsM_B?$hv?Bz||ODbuq$X)$@y+;b`Manhwc$7y`bVT}DnFk=5@%pM`%rXPWc$!!2u)&$ -dqZNJSuB%hF0xR+JA{eSHph900li()CFFAW~>xm=p~c4;##8Z072Q=jtF3d(2ZliPrR5JVCqdkt1zBaP7>%lc^M<5<)PZm$Fo7QO~PdH?3+5 -@mCX8v?z+491Vle4zE;!1WWHHc|bOg^>>eF+O+)pvt`ka!az<={?Kg^>jn*?;_hpVf$ok7liKryWyUD -i~IlR1o=JfSa3?V2Gu!4^mcu+ojb<^IkVt(D?7P&i~qwU_C&Kr_jiE=LjUZ6EN;MNFlc|!z3@`v_yeT -&bLEbP?7KQo^gG97hNpoEXabgCI^}6{-tK}F$f3rSG1n(3xW}z|*&!`J+V7P3=QnS0;~Kd4w%iPH8#} -j62e^=333`q;C89L@oEToBV}NKf7&Z)B0|TpV+0z~{#|KQJEl-k+mm2du17oIgKtuLt?wyqQlGLAB>< -4tTn?t{Bc1yHbOUg|Vf)HO-Q)5cei<&lcK+h2}fI8Jy(m3sF1qZ{h3Qj?B04CBtnY=g&DOn%d#n@RUlFJQvM=fX^3huzmCLhCq -847J7_;)`%`+@xr2qBhR)q+0*A>Vd2LME$40ufWuKY!*W6+Jhr)MQ~SEknpEZk@#A6V0i8+GA1 -@B>Y|5Yk(mqhbD1s3RrfTb0cT+71^fTElHmcH@j}ezpfWN1P~hZlA&@L_yovCsVB`qmOB1~rX@wnGA^ -1-o5`IC*&7&lY#|Tt*utDb7>3PI|q_Azu;6#RIh~Y4OLyn;jj6}UPWy~r_1`>aedf7A`Mpv{Xfg(TBK -BaLWhdT(#xAuVOe3I*gJU2LnRuj7|MDg*znzGJ9#&*JmF~Z>UA#v<15_O#gKs{+r2;YlcBK^5wm1zBD -HfUcNmx0n15MVXg>^UI@(_7;V>JvR%gr}QNxjaGkX^@tb50n5%^2}W88|)C`sVobT -!_7`&TMlC&&i_p -db_}udo-k}$2cALc1iAvpY`ZEfFyiv+aTzh5f;bq%3HysvG)-qoIL=n-fk4qL-&4jxHyIf!{x6oy;;+ -I3^K7LcyWH1XlyZ7grouvB0j$;0P-1n1B^Xa172 -{9sj$KaY;A%@vpXJvi3jrGXKVI|v7bE4k<4ZUghP3Pi}?`3o(qi)Tc{I`vBj|w?0r24O1Z;LsaJ!ze25Ey>C;{1Ppjmk`ncO -qE)WXCk=r3s#1)xV$ktL5eAILJS5d7^g@(TR(@@Fi3gcb(eF0e0q*;1z)go5CJUei+fgnVK08C&5#kO -O2w@LzuQ7qbRU%x)FR&kTzgb#G21$(t})jYP*zxU6-H{0@eK!NR)P40E_MFRzXbF{beY;|{WKTv?xJ` -ABFzGFQ!gxm8@k``pvU(IxhF0VG14*(MX0;lm$J66Ag~LBfi%i!Yk4ynz6^qWu-rf@2z`HWDNvbRhPi -aDZ{)xVWv2qO24;Il(A&i@Lp)J}kKS9Y2}djti0Io)0ss;d^U-(?B@7>Cao!^j2XCgW61djARDr!k;% -6e^@O_OVW1Nv(EajMyKbamK)ANhSMqc~SrIkgYZM=iodm<|VGA?XDGtcuTK3BCqfDWXcpw%??H*% -gP8=Wu$!#qR9y23aYQ+@#O_P4a2$@G)iXm&=HlX4u$186y_czUxbHwtzNYZ@qBE+gb8qw*<*I_z@G{RVo$ERZ5&Nd*Zu(832H -W_SZCB|Z(I+X9LVA}3M9&G`7h}t!_XKCYCv}G&0L+ud=S28>acDWdKk&R{7xq@MQA`gATt40MO{E7H= -75zO*Z18b7NJbx{IrM^KR1k9PDLK>+n7ITtxl^hYHw}780&AXucX$`u#ve6M}ad;a{zf4}4`L$h#+I9*4%;ABH~BJM>V+DJTii)IbZU>g#LWc1Z9zh2p94LM7|=+ -x3;XUJEXE{g1zU+1Qe1^$O=Z8&HGh=uQRFagQ9=>_QuJIsmm0a^kK`vVXjw8$=N&*b}E8CcylqJ`whQ -;=R)N^0xY<0J&s!-*&%klUhRPvO83!Tg -doUnh-aXpMt3ad9eZmbO1i*Ry5f=O$f%Da)K>8=i5o|ASfOYLMLdM&8I+2RW1f-C+@ps0d4FJ7pt?`f -=EqaF9jA|b3k=LYF~nb@{qDZV6j>bz`#lwrH0BA+!*XMDSOj%z*H(ZNua%MKSN~!HX7nF@-{ieYU_;A -)v93*RYRUWeTocEyC-VNpb;Vu9J(6{9tQA!3%f-%^jndSm7^C;D5kujS)HRR}dHlpRwoAgP&WeMUvOJ7 -q6ceN^5nwzVTGL+1CFe1UkbNM6R6MIB^(A~KCsKp`kt;J4ZPRzCDA2dbNE`c$Xqm9e_VI3sC{O~b|Fr -MR*!ri8#B`IgZ4*UYyq-`{r);QXBYBfx{B-Lu{p+BmQO_;V->%&bpd+By$du}OFBTd1=CA7R|k=f~_y -4gpNEdb)2D1D7~HTEH|o~?1y!4tcI&A9)lDGT`_v1DYy=%;tJstUfF12Ken*HVe#H{B*Cw_ZmY7-W|t7P|2eoGF)A?G3d;C -`Tt?dvbSo>!d5?=t)7S4#aqbg65Z+7xH3VRYQL!&zM$11!SJRb?&QgytWruwdeilYgX$Ld53L3v)qP} -+bYCnv<(2oItanb_lKpJhbiUGR9CiAiHD)c6)y#@Rkum+TmAjQW$T2>WBsSF2;FC!?tp(epO9})+6wO -K3Ix3g(l?D}eeok0G4iwHrFaVQZXpcs|hiz|zqtA)5C?zrMq|CI?#uIREfJn_Qs-*RJiV8Pgt2>S@`lbN4)MG!B%I5lhg7Y}FZGr4KtBHQnG$)4{jUh{fI|yYny`>L!@JCMH3{)A -FsZ!Oeara1Kjr>j>!Q6)`&vH;o+ffyQkVKsp+-OnE5fkC^r_|50p>WM?Wp4}276!P59V?)pjH*qci%> -O6fZ**Cbe<@{ -OKNMW7AlMO#dwoZ1G#se0jPJ5@vhDHL#9woQR!ZGca%QUUfpv9DXwoi4UC@MN25B%x$|++Y`iT>{ta7 -_Jt|pZYjQK#_p)x$|M2L@WN_`O!7e1*X!BgxD${`U?}Y$WI&=x`^2Qd(# -MwJdb_s`|@YWStbxx#v+(uY6)$4V;(dcNMW%=8;Hj -z{rn`7|Fs8mMRaDx#HX6`dd&u>@w*gDkDo*aD0Sj?(CEy!JvJ4hylcNt;rYS4TY&|IvNMv|;VqZYHO@ciS=dq&5EU(Lsz@F3wqmaUv3ThxZoP=Sg%{x+0;t|8qbj-BcK8VhLmD?f*mM`J5TPpTyJ -etItvv;os$ap5t_$>oP`|o9eqrQ`ycvUtOg#g1@0}Uw^3CVGUl*QQI+*bBZa_5NvzQd@pZ4!U{a -`Isd5pmdP3ojU`B$;X|eQI!sycgpEu(Ja0Y(##j*4OX5UbrozcbJg?^y&%da48{22f+j+mcI)O86C*T -i5v>azE5Uc#?oJs0-M@$lHnoa)%}&DzMmINK5{%v8D?e@T>%1nuZG*PbXt={@`w@Cm49$R2)#R9|pgg?(hb55gga)LBBhxfC3d~>0JIp2IKbNfyIlR88hr#9 -H51DM^Uay~W)1jS>j-ASmpP{wCsCe^z)4OkK*bHp?OCgEw5&pOcUssmH(CcyC!xH21vW|bgl;|WsDup -3m_%*YKAQDfOU6WmRgB)5y*jrxly`_W+nW#EFa(`*!Mxyr#ge-xG#dwDyI^#=ML6hzybAxu#w|E60dD -UqY-C8L*;a~aDrCGB -21Y2BKHAMnr3k|G+I#y?=4XqoIGPdsJq`_rujE=@$Qn@RP4jJDo@g^lrwUtyy|KBaDH2kh -?3y#{oduLxH#Kuf62gW=@n%##kiuG|p?CSUbao3;sF%0Zh!cL|UEN}n5AF&Wo;eT7QV~$_fq3@ZQIrc -j@xI(<&?4FKfhamg--Hh#wO9KQH000080Q-4XQvd(}00IC20000004e|g0B~t=FJEbHbY*gG -VQepNaAk5~bZKvHb1!0bX>4RKUtei%X>?y-E^v7R08mQ<1QY-O00;p4c~(c!JX>N37a&BR4FK=*Va$$67Z*FrhVs&Y3WG`)HbYWy+bYU)Vd4*F=kJ}&+z4I$Z?xD7nMe3od6e& -IIcGX^5Mb+LdY$ow+Fd)E5^Y1&@aXvQv;DQ*KH#6@&ilX?ANokc7Y|&y+iJS%Fw$#GL1&D44ErO<)0y -RUvjZzdvAq4f_g#<1Nu?emRPhQr0wAuyf(SQ8#Ngjgo9z%rF+w~a!=G^W{8H3?ElWf9Zm66RrM%QTfb -Jk-HJlj15XM42iPf6UFi82n253a>{t{4W(Q`HAbT^&7*ho0}%7XJ>Du=lm5FoV4C`3nVXlM6>7gvQOx -7BbqzUwQ6(dDrm*8rANYn&lk@6+Ca^fh??_*O0juBfw8 -gL$DE2hvydc1xn9T$hq9O&-ZoeeV%)DJ94=rt5-$uG_&+DKQ0!qHLl?)j&-jrGpbiFVZchN=U -C#H$BYSz=gaqme)J@;h2Whse9vOWejF&BPiH#EEAj^dzJ13dKE4T)qGt~g$^B}S+Kj|z~*-LeS%Kyxt -P1kZ`td_BadWy7b7)fC>^JkQb!Rw?uhd-W(54jzeJt^ChPI&|L|a0tj8uNflnfp{>UzVW$tBiy{U@oj -5%>NdFjgS^;Dvqw(7!RUit5m-MEtwbN2t(DX#@QX&Od%@s}Uvldq2K&9lW#u}b^~P)h>@6aWAK2mt$e -R#Uy^YJwR8005Z;001)p003}la4%nJZggdGZeeUMZ*XODVRUJ4ZgVeUb!lv5FKuOXVPs)+VP9orX>?& -?Y-KKRd3{vDj?*v@z2_@N;XopF4IEgcO635o-~y`9-XkZTHnZ#4mhH6k@0lcZnr>lpNo>!&dGp>ld7l -5oD3y#7_Gr+{NY;S1dum{3Jp|kP20>AXp6Y%$4I<0)Jj`*%XZ#&;K+&UfJRv_9J-GmK8d53&Y -=%*j@^#iKdgQJDz!$x%p?=h8>vOzlU5a)L(2LxY&@6)d22c}@n1>IOa~GA+Iibxm@E3;a97olVp|1A< -D%npxyS#*MC@Fp$S06TUVkHxO|<@$WtqZVQVT0o6lJZEVJ9Jr4EUM5Sl{qpK`onS7HWMfNC|hJfwf+b -AguHEa~(+V&<8SmUN)^X6uJNkL&N5v0y40D*uwq&dy$O*zcKdS1c)upjMW87rKUtGH@?$(6+1V`u(?* -CcqXGZd1n=ic9(+l73{UGxs6lRNy7rG&7<$J%&svfz!xQild+uw2dft9jeEoM27XzVA-3{%js~MN4%! -Bgu;aZ!;bDLm>CU5*{^C=`$JJEm*dpq8$;lN@Jsf%Ht$7=vl?SeB7eEc)0pi|ARh6Svq>fR -?FoK867S|M2Y*Gr%;Gtida@SZ)Fu{RL1<0|XQR000O8`*~JVr<)D?@&W(=nFjy> -F#rGnaA|NaUukZ1WpZv|Y%gzcWpZJ3X>V?GFJg6RY-BHOWprU=VRT_%Y-ML*V|gxcd4*L`Yuhjoe)q2 -s8U-5?vAqV|hp}!9)-g)oN>LT->}a(mBgyT;=zrfyv1O;p+PoN_@4Nf--FFJ5^cuX7!VA)X1}nTWnzW -`-6{(FSEi|Iq6K4in0g=jitF}W(ax9~iW|``GV|{=$N;lK1aamSd(~(~Fj4SQIYSFUopjyd6Kanx-a| -m4NCuNQ9K>Kr`s#VPON+Uft;Y<&jkHK>o_)|e2X-nzJxGS?ibsKlI+1*6~PqD$$8Y; -ERvYGzhH?7q)S4lpD6aH5ItTr9vWn4*wtOq3gL+b^_kw`xj-Q^2YA7EQk)4l#}|b0skY$a?Z8rfb$~D -G6^-|wbC*%&gE2OLvW-8Szm_dCL{R6v3|v5A~azLSO)+>wh2R&CJ*WDfdwA~V69pRT^%yItD}GFkZT- -k(K(2i`xZoD1_LeKv+}!~rdG7L&tf@D(8kYI5A4Fv3gzH*qIpe!ng!>XaBT)W5K{S@VlT8vZLmR}+7# -rHe0slEN{EtO8wkAylTwe3?_ -U;e!LqE2C@Gl+(nIju%#T;jDy>@#Wo|(_a>PVT0yrWcj3iK?@=r8FM#BEA{G*Jl?lSkJU-EdY0wv+%w --zhXPA3!Bseez2D*gU2?;u2t5UkB+K!6oG{ArVKi&>>X}P`EqD9Xjl3L2k^KWuO9KQH000080Q-4XQ{ -p@lM(64RKcW7m0Y%XwleNs(}+%OEi` -&WoSA%R4Y}$J#2SN3#B{kwG`u-C>ifMwreY$4yFIS{+MJl1nP?|SxRh9I#Sk1&B~`!j*DMtTI;z(_wMa`e)7^e^kG8mviq~OFCV4W=$A-k2z|`PM^ZZQFz%hV2Mja -VkH(-ECq+jQB9CMY@n81HSR#xbZYSovNBWw5wGc)QC`7{;Yq5juHzDL)OZH}p`>Mk(>YY@JeKr#6f{v2bv0S&^VQuDFfgw?#I5a?lF)uGBphgf!>5_Q^aZ5j4X4Fqx;W*>p41|t|*uEDgAcV!=E(2zfik#ROmTY -wgV&i}Y#VsmDE@4fsY@dZ1se7bP)h>@6aWAK2mt$eR#S(lq{Tr3004aj001 -xm003}la4%nJZggdGZeeUMZ*XODVRUJ4ZgVeUb!lv5FL!8VWo%z%WNCC^Vr*qDaCv=GO>fjN5WV|Xj3 -QM^q{V^*MOvvGC`BLvt$IZ$a*|2n*0GK4bXg((JN`)0-6p~pXJ$NaX5PH;K`Y}xsIKF(BRrYPlGdmJ+ -);ZkOx|1VxROke6x3YP@(E?54ArRLzIjIvhG~t)&KaMTdi=(%^&mUAi*JJKnhAmC6oyNO#zWK5abc+) -D{WMbc0n?ulxWHWcYSGnQ^6Q~oOMV@=pGGR&129G-Ph>L%Tlrd%d#MZG}uBla?zBB9#NFoaK|cvIRSs -dyFFNEcG0X@{pe@gui8%G0Uc|YV`ak))=rt@Nv^{|H_g}UltlOoD;`Nkq9vHyUX{GuMMt5C;R^f*tGdtR00o#!N@y)}xsNx3~i{<$d?n>D -*7enYHNoUe5>If>bW1q$&N(@gtnH0@`3Tc6s&@t>cn~q2Yv}3!Ci{IR#lV(`Nnh{lALv&LxSn9z92%p{cB|PnlVS5ofD7dp7AmaSXfet(MYC;$i5X}VCKdJUDW!Ro-uIY -oQxS;Wj%>J#l_$ykS+Wx;H}Ue~T4RKcW7m0Y+ -r0;XJKP`E^v9ZR!wi)I1s(-S8%mhKtdg1do`dQx=D%x#s+B7y`+I4OCy;rMG7Pp*9iRIJN(cGt+Xi?s -1KIK84hROyf-xMdLcDoqHQT}BkEp-%KB0tqDG32=u?j-;!QiM^oHE^nhIXB$hq{i&kD*lNL!f)+fQT)i>Bw$>$%<69ejbS9L$4rj2v_k3 -R)XLkC=A(of5X=g&Nj>=xP(F7#y$)-*+Ym6IvrSmeKggUb{c#&9qjaBrFhDor0@2amv#l)Ra0yS{n*> -D@2Hz8@5jtE)$ldx5{SAnG8F594tP<`~bWp8>Dck?KPdW1Z>jz3!Gh-+*zMRCAftXZ{p!JwGj&dG>Hh -WWMA!if0ug0stwO~Z+xMfJVCY6S+FKQ}1okqvS>;L5BI1jAz#Lf>s?*kB%nZexrwz(o#?Tbl -C$Woq&Q#@veofh&mk@ipW(dgFjxds$>M>$npHkXOyROkV9VqeoG1sx7kBRbWhcM|U`cK%4w_>{~MPk+ -cWsU(yb4G7kpF1$=V|PxE6bITAX-h_lVN-Uwro76a}pMH#$a6&DcmS1 -av=#9kaqPVgjKbhae04RwpF4)CNp2%*fa&`_y#ii6<1bPurFkVRX5BV6|9%*U^ -HMubPRVnkus2W9pRB%?>tc`@6aWAK2mt$eR#V~K6nixQ001)p001li003}l -a4%nJZggdGZeeUMZ*XODVRUJ4ZgVebZgX^DY-}%IUukY>bYEXCaCuWwQgX{LQpn9uDa}bORwzo%Ni0c -CQ7Fk*$jmD)NzBQ~%u81&NKDR7OiwM=<5E&m;sO9rO9KQH000080Q-4XQ{h^v-UR{x01^cN05bpp0B~ -t=FJEbHbY*gGVQepNaAk5~bZKvHb1!Lbb97;BY%gVGX>?&?Y-L|;WoKbyc`k5yeN)knn=lZ3_gActPD -&(>4?v<1mHJRA*GlbURrwrPh;`ug*(TTgeQkpSBzK3DfM;jdFf-1w?0}u9FMy47;@BfdWu54I;Up>0h -HA7n2U%bN&lcUN3w?nG&)AcGE?AajOb(iigPyuhs*bgW25=YcpJ6T?q;)y`)M2RTbNG#~BdQwNMAlLl -Tq=jgSBv8)`-Y=Br|z!-_@>U%kQz|A_nJm0dt1z}kQ$|GJe_;=V4J+)$($Vnez~k*t$lPSLt}S}qa4BFe -vzif`{ZCDRZF|<*Qiv8-0j(bmJB@DERXtlM?+@_k9YpFY)u~D37KEvt-aNDxzKJ>Qr)ZI$nXiML-|4>T -<1QY-O00;p4c~(=*3H8@=1poj(5C8xw0001RX>c!JX>N37a&BR4FK=*Va$$67Z*FrhX>N0LVQg$KY-M -L*V|gxcd97FLirh97{_m#{4y87k)dLK;kkSnlLIX*lKhof6>|>9zw&Y0i&JH2(-g6}1Jy}xd!0wE7E; -^U*TpY*o8>mHbWl~uunnpOO73+*Hz}f?o960-I!Sx;QCZ^9kisLwnWZP-uSflG&s(O6XHmZznSt+gSo -opaYWe)0bl>VI#?$x6- -TD73uEg^U+z^k{T)SnC2?~T#smRPGxKv!&Wh89N2_x|Y?dvJV+%*ZZPiALpk`eTt++G6AfWqSj8DQ{X -7_ilU+sXh*>3kfYZ^HW-rReoSI|j2M18-*4x -U*;VjQAb+=p!qC8I#s4VA?RHshl_oacRZLdiivg35K}dV9g?BI!CC&O|C`xCG_v$pQ6`9dEqA^PJ8-Y_fIGCPnE=Wo-5Uyx$AqBUT_U}wQ5u-78NGGTz(zN0dMlF! -m9$859$v$V$990DVxg?;l%2KR@YD!7CsVmLm2`pGm?%}`FMM3KS~r?N{{$(><|-Xx>ICaII2mor=}bQ -8~H92Xs>I~kb_H1wUUU@%>BSfM3N*N$?Fu^fz@J7}^J@kC1b&>rZr1*E@dsa6697Eb`V^CH|mxj5({W -HXI+CL4_?b%|3ZEUOl=<-S8D@jogK7fqI@>?Nb)bZBM6mC0l+HE4FQCA=7-vj6mXn{uR -VFrZ#MtD!G>j$(Z4-uRSntdY0s>5pcYzHvd%`l-!OYDrEL1OW*VCO6al}iVWOYe+V**_;&;eSBWkeTufsmcZPH#dBTC?1u%u7uf^~2iS7Af+y^yHrXI7-P;{D -S6;%Yuao$l=l4^N{bIewO82Ua4_ol2J54Xxds$;gWlrM`0<56fV!}Fd0oqg4uhUlIIdDuc@|QO6T|C2 -f_gdg0G(h5w%!VI8@(tuz0gu(0a|d*x6UF1j6dHQYh*rn7jYMW~>6kE(B)>tZ9LYY~ -J!rkA=qUuycB))Q5XiOAHFw7F7GY=R-ja^_m6i^3`W -dFVPxN7->T;IF8Et_83)?{YBsAw$n?ChVWC%?oJvw|yGA;HnhaR~PcK4CwFeg73 -~p|o1JXX29-@B%P)h>@6aWAK2mt$eR#T6qSSrFG000zg001cf003}la4%nJZggdGZeeUMZ*XODVRUJ4 -ZgVebZgX^DY-}%gXk}$=E^v9xJ!^B^Hj>}%dkTb6la-mWf_teC5HmcyHMk~ey;fTTd8ihH!%3JEIB!#xF`lNWRPd-nI2$MNI896$c+ -$@A~|(W7F^U%n-(o0gA(j>$6D{Od`!YWdpt$jZLHto*H{;Ay&d;rSp~XNzn)o1Q5M-M$+O>dTqGOqBAJ17)pY0@UXn0yN|`vm4+XUnd&Oh77wn74ZK1Flbgh-1-mj -Z?3~Y;rUZ_`Ctq*05${SPN#@Cnv9tAD_jqUp&wmwIF6;S}Wh -*>U{W;TH869?n(MfvI6eL?WT&?Bv`esa(K2I$|h@8J*%s>t92STZCZBiTjkGymNrTNzuD0)r{cuF)(QpsUxu!dcJ|6H4l3UA85t^gW(uo-_?fU1 -(m+xGT-9Ud1rgvQA^v&kGg2qF*oJM_^l}x@qM-NT+=a%Cor8=|U|_Ga9#Et0uYDVWac)OdWiqo|lF0$ -HY$sNbT+I;Vz+Tb!4`S2o}OqdKSjC!sB?hx7VWtEkzlIE~2Qj>)D>HbDcgu{oAYZ1{#wdJnvgl!#V53 -bD%u^e)9$}L8NPqk`>yC&eamM!c_z6W&vAm3jpt_ufJCJ@6xOoIJHA4JVDr2Uuh8CLWR6Fr65cfK)<= -lb#c*N&S>MtF;v$e*KkZytCz|BKRgU)xG^K7OVa~}wGvOrIe5*eDUSf{0tRvvs4vEwWOx~I!*Di>Qca -Hn)QZKuU>4P=cd9Tr3K(WBvoP4h>riGe6MDh^A7^EJ1zYt+6XG%;vJr7`k7L@vmN=r#mEPU}OB8hMHn -0jC)t0KN%SzYzR>7Y_B?`ER8?)xMS&&h4k--+BaHz -w~X?_cE1r_BQ5yt!F>Y14Y}xo>Z9H|E$`!mt~<)UBItH>~q+bhy0kD%S_Me4s`08CIU81V-}5%{=WM0 -#~&4F?-#G05VLgyu=89MTBk^xrB+=9TZ~SrJ**zxTUAlkSlO8IIwZu9jv_EYy$oj1P4OiA#kkID+@T8 -%(!=kfS|=$!31_wyOrR*+zHx*$QrqAt;W9Af2xk5w7p8oNW|e&bdJq>j4!3QhUMof-r1%uHK)VvXaYyc@t@AX)HTqbmb6NSala1wDY8*NZb_neB*hH#-O?U2keo@S(+LHvJxl^OhBgg@_*S`*m5ixJ -^{B%YilsjY+XE2JAo@1&(QkBm~hMIKgmUfzH{ov9&&6WRK7CE>HO$i&Mu@aBeyZ3D4blLbJ>O#UHCM0l~BN`DJ>ap{p2_4x2X>?vi7j@ZHG`qpr -fiI4Ox92Vll$`a2L7H=(bRA19SR@Nt5LwBu!>6;S3?Eu -ZjQwjRMzR4O*=7X;E -L1!y^wp3+Z>58m_kFadt|YrbffLWj#e?o7YsbKL17F4XS6x|Liq?y&!J94Wx?7F#}Sp*RC(5h0@U_?BU3m*!Bzd%qMEF2ShM?k&K%fpXDhSJ+DO3Q=j<|rExSb -Dj|2yCSLIrP6><$YUR$RTp~D(f;38z#Xj5?z2HQ>0CQ2&UV$yI#mZ1#z_|9YtBshTVF2Q^^Az%w5eIB -9v_j9{Ofb_oHpf=t#zBw?$yoWTSwFywXDO2b7?&`I(xNZ4gS^fQf~#IOqy63qj87do&!8h|+*`rT=hz31-%WGclNIwJ#}8p8Gm-ewT7{u}@~zx$>i20U~9V -R#J!gxOn&UJv}>!ij!?jb=F5;@}=^2Vbaw6gnDGg9h;4gEs3uJE-R)X}c$&b#+_z~sqR -mp}kl=|{V9?`J%BcsxLAfF5|gE#VfgH%O=+YVa7=YnFoB2`UiRDWC{yj`p+?BnKF~Ej#c=^cvdqPH+H -?ny!N94eA2GUUs)RIC5GY92DgNJ~?2*9hiVp#2{uy^igUo;f4Y;qNq0yL!p3%w?T?P)PgT79}Nl&Wmm -T&G+BI)QgG3bN!A#!V=_}jtN!k3rMS)V6gMd7(33p_u_kNj_u!vX@rXj^lhPltpE~`Fg*`D4!V>^u=J -OY4#|woLDuEyt01R>`Ah2X1guot}CQ1oS^ylAyo;xBzkXV>E$(E!U95wy6f~5rF4#oz^y#n&*nd}o$; -0WqYPL1%l95JR9?k&tgSgL9RmPbbJkgFv{n+87>vhsVW6Z0`f?M>m-G=|NqE^itpll)lN0QLDGw(@4a -M>ZsBOFt0ec>`duJvU=G4m<+KZ8R}HFPbgUo{55QNN4K==n5^#&H%>1Cft$|!G+k?k$Qzt@Ff5)L~Y? -x-XvE{0osAltY&i)mM+?7WD=kVwqP$cGU~AzEJM5X!9k02QsM-nM-J1%cnfLCRS6=~RArG;;382RLW3 -Excka$gfjOwv%j1{o!5<$|Z~Xq{{XdFz2;!DEvq<4iM+hTr2n@!^ctW(?*Ihxj=A2p~G8hm?frEcBii -nXka8&sh+Sq7cfqG(&GH|`A+O518h9Hl_T4941xdTO0)qcO+1yIeayzeU*fttRml+#4YLjb(x_8#&{>?Etm*skpf8!sZefkCu3($EtAz1nr}r4607yWfi$~p+p1X{9$t -VR>y{Dl;IP_O$zhdMhgs8fx;gyAR}a5(HHVl#R!@aaq*$Wx92caO!GX=mCMc_@?OCgc4i^<99ojNpH` -Fck;?aHVmh=sb=EXmpwgw=80btGxH=tW<40v8+{C?p(z!+Kv5vNi5K^JDLaXK2q#gA(5s~3g(AuCdtA -()>fHMxW}04l(ACV_1b9QmG0otMn%G+TqQp+(9VTwY#at_$NKSG~~=ef^!x7~-^FrZtShaX{E^P2|UG -e{?j}#hKn@&)G11Phn{T2zpyWdo-TyIjyBBg;1@d13NqHsE2;g855O*0KddGZQu%H&nQf^{aXuDcCS3 -O@5P|ER{a}E^_U*wLb)k|{<}1`c@FfkMsF{4focTb+fn#Uo-M`m0?~8!%i&(S@D%X^Ox$GSV)zJlw2C -u5Nxiy6L6QxI;~1rB%*u@4?YX=fL{AK8mqJ6NeAkM0#=@uU&=rD*Gxd#n;EM{Ucp@=ofUAr-Su3C)cx -P^b!thTGJEhqx7V1A6;5!2CqX;m(v#>D)GnN$S;dxMnz8Q3~^N*^s3I{;v!%_Sw_y8DgPa+-Oto@u1j -48RL33WFBIlImGvxy#@kuzyOMky -7%Nk7)&=!;r=AsxI;a3zl1Z}Z0)hGrxGCN`6T^l<`hL@0#BAkZi;uEr{HeCx`CXl+m(KK{MF@$hZV6h3v@&5U#j_2PR_s$*5p0-_*z(2{-KvD0K74#=)(dLYEDipj -BP|0^V!`COVF_#_bl(-|=8Gbn&g>mU1u`-$vDzC|YYoBd~Y`On1e}3w3}n??->6U`L-L+*k07#(NMw= --(;$HCMV_Me5s*>{XzE2M?W|C3rdP4GAnVkEwjX)}z2osU{3O;tjByCRK|4A#Jn93DBUEE6@`4xwT3@ -Hv~qXO>iV{!o`X5m0p1(UuZ*)%=@BKFKczaQO&!ObME!TX}@qX`|$3evC=h-6kqI9WeEGS6LS=r_Tr*9KW|4rK1TFlM -)GE6HAtAK00jESv!sbVY`Ml00DMf#hb9j^-{5Oc*D^TXfZgU4piFf!%lO61vy&IkPk;VdNknaD6Vy`va=a5mt)0+cMx` -obB{NilG$$qN#X$0ut`eFk89QU@Q6!YJQ>IK6vMMGAe2X4^!vNPw*C5?qYAhp`L5!T*+V*EG(KGShHI -2=pVocdA!Fg1}m9HHG9UPwps4B&e$-d;1$;X(aYJD-hC4#zvzMM+7rnU4}Ix7XFlmKSe*Kvp>)N{2xH -!t6850vb`DPz(>)!fqH($eOzJOA%AX@p&(9l|948HJDiB-1O^|+V?R~kEy|&Ujl^)LWXz}V83^F`+pR -+5&3RIKydhbPydgg*Q!aq;;HQ*@i$%*qnvk)zKj>4EQ<*be4UM#Shc0MlHbt6}Po$j3Kk1+?aFy;Lmd -L5VKBJ5}Y8uQ=&6Ti6~3>w<+ev{ZB08PwSzl8)(;5}Cf_VWknM?KpY5zE1ZQMG_SQsnjrjt5O-*Qm;#%I^|9kRzR -9^m_pz1IU-ntVvA#Di>l4AthaFk;FR}arw-F8?5TjQt}ob5ViSdehd=`KF=JK={52&2Lhg -A95SU!U6%~ghd3-s1H0mrPl}8QCL05vtJ<+E@MgfeJ-{}>F0F3#?^yRv=zH5?_;R=Eclzwu1$OHA+jO -vt3O1YVg6UN5@1o?htij-oIfQrGt)WvmIu$nCNoS`_w?7#$z_+U~VHS<|ZTti$#dVTrsR^h$w#WkOj} -Fu$8`+OQFX^6GueP+;mY$ImM!O1f$hoP~}W-wM$JYk47t}0ozI9PxW -t_4702tA+aD_D}VsVZ#IRU%Mr{m7`6WyS03PrS&HV)EU-R2Y{22^!L9Q=?dsve6Tmj@D7(?eRhRU~pm -tT5kbTvP@iSFu;$%$u1S?r=gL)WJ#Py1;#bLaFpVBr2<|)NQd_}n{W&rZ}EUBg{QkoqlDf}`DoMHnY@ -$Rw!gWg1SfSuvFObjU@z+B_qPRX-n%o$8P19Qu`GbdgE3kZ(^IQ3+M%C!Dy9)4u!W&f#2`J^P3Ia!E& -hBGco0vdzQ!09p?-VP?L0Z$suApdNt&ks3OF>#39hv)$txG(0M*05Zf_zu&C_?F1(WOy -!m0g+cv7Q^%z@q;@=Xa}ailWIYdY!}~-Y`ljorGg3xlg$SDdN^ckF%OovU%(6X<2jRJH7QUcG=3Kk+e -s3%j!)g4V$pkF!aRencM2Z1>;ZxHs1-LVmf0%=7KJWVj@U1QCZ&-Ont9m@#ZOREh!RrFtM2(RbWlmha -d%qyH5`pQ3mXj0MmAKhhOA#zn{l3&>RHd*5U4by%`C9C7nDIAACAOX`;6u9yyw%KDYE}nZ+8%#UCq8bit_L*S?CTm8&B;c7>nl}77$!zEKJwBpnE^}a&b7qU_lPSM -jf8YZedxc$W5dEyCc-zpY2KO7}qd_mn;$1msbA0FK&={kNX;dQJRPMn)B`rv{aaocx^2A%OFXm8+0QupKJ`|@C!1(8UaqwktP|y`AJD~OR{|K1(?tUClfR=nJio+un!}bmnP -A&?%rNmPHpl8?cG6lPN8E8%&3?@|E=OBP?3B0$AcBt9C9^J{sJE4yp(VrM+yfKZ%dqQYMdb)MZu;rn)AWBk>3&;ax_4-UByk=)hkiDMBI>8y=9f3tKmyj^g-J3!Efy -c!^{?9stRg*kQYmy^Q7Wx1qYm&JNqy|Ws0yhC{8ADPML9O#z7j~f@dz2Q$AulfecAigT)CtX)+GJ*Sf -09v|F(POZBpIOI-NGC?ECXlJ5$S#WoFHhEf)OsYuhK;h(@UM{kTDq1*mQjXi}k8?5kkm*?>S#=zVa^J -P95%C}b~U^ci!Z36mi>=v0KiYb5-5PY3AR9QO#ZZi!&%LF`MI=NbBaMsm7E{mW3`aSmwB!nQh(~*aS# -}_~ru^_0<+Lq2#dm&qMp2^(!8*v#$0?RhW(7h}c_cpqH^;oh(7&AB7Ga0v$Lm_U;sLvH;YbV)caUiE` -a7Sp!*R;|1i4qZWpDR$K -=1FZBBB|z1$U8Y<5K1M`HCSa$>#1A4V3uj7#rUBz;aPy$l1{rX>`(cZTqnkV1<7aRJSjPCk5#p)O?l+ -^YVt{8qY!5?Dw312;S>CYfkknN;b8L9bk_{>bC@pPLXZB7ZStQ3?G>FAmZoU(D1zC&cn9`}%A9JAp-d -A_;_Si(Lyj(5`E53fv8`YL7+9Nbr@kb{|3aJXQm#E39A#;!`$!+D{zAEtE2H1k+klnIOw{(9(GllyyY -nR>XO1D(CLg>ql(L9C22Y)zpCSb8SQtyJ`y_tcjBQ*6f7Qy}sIe{t%^%^@Bdyg3?Zh_`pN%>D%p+O=!ROcW=@G4jkXWq}8y!ljNLT`c7bwVG$dGQw94Jb@d0<_8dBpgj+&d&E20rjvs4!{ -z2w|w~Uq53cNyLY#>DX{$S{sR9!qTSSdL;k&`R9Dg?*eO;eB0ODbpK|sJ&&#cm#xfznk -wcLgU3Dt6~DTK*@bJ0vCXF@?%MslF_^}V1wv-Nsie5YTI2@KbxTyuKxZBs&qiUQ*G7+;xodVna~~qnsDHHfj3EEH+%&tyvpYW(!Eiyzd6WV8!8~z-cstR6tFkX`*F`8yvIM(`(P^R+?z19$3WnSP_60OGX*YGv -9sWawy|-+OTv7FFOf!t-WjHFTG|sl&5m6b+Zs_G{X?LSvl6D=X^^nr;6^=G>8hl50YyFDJ(8xVCIKXD -Ebc9UzZov6a4pA-&krLpKtpM(=1F=TOmA+G?`hQ&mOlwOjK)e(#9jk#C55Hu`8yWT6z~K3ys02-B{Pmtn3T`aZ@XS1-bc<9M$MREM~%ZT_hUJ9WWP-eHNmQ*sfXjPJ^POOuj6Kxb4T`@-)Oe?zf -em91QY-O00;p4c~(;hq@{F^0{{T&3IG5d0001RX>c!JX>N37a&BR4FLGsZFJE72ZfSI1UoLQYtya;h+ -c*$?_g83L7HshK2L!yZ(0y3A?1i>(mmm~-oTybKRg!ZXO8>nhTe9VAw{)R~#J1+l(V5ZAB>bwCaimp3 -KlFJUHqqrJ;Y6G8X&jmz;X2t@=)qxwhBG@KCF0Eri%mSy^cMCYdT5u7U0N#VkGD^{lQqqv;jNp -~80eLm8Zt0_CD>7PwLVD&mkRIVJ$1L-C0E0V*gw$tnkK)Oht_d(<1X9>||@zRV64$?K1b%VOE5{sebhl;whqYUc9Bf -p?tNUbwp?)3_)Xm7$;LT;(ym@hq0@@Z7I9bqtXw9<{xwL3Eg~O)mCwlK<_H6KfIEFv#dKMn5YmHw-@s -CB0@s=%{Ay;(s5SmDm|kumR(2mF&5>HXqFJYF7{zwwvs4;74+{!a8%`I;T3Kirp|?&53Fkv>q1w7(Xo -<|Fv)SGo$fe`{rqKG}CP#vNu7Ry~Ta^)qrVMZ$FR}2TwCUZGo>2@=FWak$=XUA8;G|8(-0JHgx!lA(b -O`Xj^~|ixPYFgk9>>apP*ROA&0CkY}$#{DktHI|V?)KG6MgMb}YLvF#JXxP@P++03iSX0B~t=FJEbHbY*gGV -QepQWpOWKZ*FsRa&=>LZ*p@kaCz+;U6b3k@m;?Hq@n}EZ>S>4n;$ -AQ^l@%Q^@+b7XA`SAgaB{8E1R`0kCGfLv_XEW*ogLeiaiX=WH -*pkt{&5XS~XpI@GmnTG%%iEEjo^@fNVQQiI4ttAGH+OIv3pS8B2oWeub@0$>%LX)W4TH0)62qJizJoU -e4Bo@>Dl3bq%e5;t__o1)zn6|0LH=k=DXz#~=}>@7Ew;dzru6 -h)H>upUoV%Iy}8Y(J&4zcpgAY2==@$J$5=N1s*4IeSrL?VSBkqs(|YXFm$~e0Km*&e=~D3{Lb?AS+3) -MU~4YsX^i#cS`oWxdJv=jQ^m|a|@cD_HNbd>YA5D?i_2*oR4$pR4uY%Q#d62Xh%ktU2RhkjyyETPXq6 -4^oBP|ylfiT%vp`Ur!KWFABqZ80N9m?kC`CWIfo$`kqr>8v1NNui&7;8=+H;I@XhhfYv4R65B_#$Rat -=cn?>Kj?`J?ku|#QM|Ail8z;H_ZlWvMqbkyMa^KmH*78d%<0SoR~d}s@x5cdSF?B!9YlsDO~xE4gu>6 -QCJWzHf)17DXS&5Kd4>vf&-*u`7Xx`IBv^3aqWnKf^BX)9A08(VOlxd#HEIfJ3eBu#pDyZDd~6%8Vm$hS!O)C7{zkisOZe)Y2!5?r3nFn38#Ds^M4Rp6Ci*!}9?jzSmzO`kU#%X -!U(Nm)&54&Yg4R30lJ8|DXr$P%y~cz;#Pv$19o#Vh^}e`dHZ0RI)XwdSc9uO0@CJB_YtvII2sYiJdGV^hE5D!IPBo2827CB_4^sZp -8K%L>NlXBYVzBn3ixuIfNN<2?|0fv$&W;97L*C_D6aQ(b}Cw#d9Ys_S4(%oB>0+Ghp|S+P~7&vkcuWL -`~N*_nWHg>d~%k>&54vuS>qeKTjKReId5nmTl@Xo9q&&F}00^P+VcaS-AWG1YJ5M6L#*aWgx`nB`$OB%3B3)_9C{^jD*lPxQL7W7gO+7l-a -AwHXP{ANFM9BwPR53s#7y}jPGiR2V+?V;~xNe^q76&M6DZD0Y0qb}^?w=pS -tPG>_bxoGhSq4y>=o8I0{Az+t+14`}NjtpXTaK?zvMSk}P>2~T87A08D@z#q-Gw0anETGeS3UN@x9Ws -$(jkY0WJPFS^0MUG#cO7!@_GTx)E&z>S`&v_amwzQAPzXd2z&FU>52JWKCMqEpMBnZ}KET$ -OkJT2jj)2_967Hdb>>lwJ<6t#Ota&4&W+qTk)`_iZ^wFVF#S#Y_J9o{%ASkyNQ*^4&H1Jq@SQ>)UggCVj)oG4}J;n9u<7Ub>CEwzHCXqk2vL*47x`$Zvh9+-)i_>VtfyDUpfl0V(0WFz_ipsqYo2#oXF@w*y#Dj@@CuuZl+S711S9wUbdx -LDLJPJ0}pXH;OYne!PV&VjxUn@9on6HyQ%yI$!l&h!>{xtm5~x($%FMm>|}7_AOD8YRKs-=B2^A!q(YUAbr~^6_rkMY+4~KjUIO)z|ryD;y^}@Rhvw_XX!K(>m4zG^ -(+C=*|q!_dalu$R=HK;!3pDp=HVDb%K5$8AZ0C{h0Dm0r`0|+MX_LG+_jLg37@Z_>}7T@fOYzG!wH$q -X`L-#m`rorglwT)(pm(e>(JnjY=TTp{(2Y|*G!fSHe7)i-{IQ;;O2;g;B61R>I7npByc~;4=v4MRCa% -iMxU9)%2e2w#Zh6|4wudo=HW7U!Qn#lVk-S)?L#hlK4&tFQo3sc>yxuI5>G#U7!bqIhqiyole8ax_>I-TJ -xcMN;+K8e*TcBE_#y|7aMECxvlkPQrhBqT2kfRH3ORsOr;}2AqKfL&1L9800WG!BK(k14;*zQ_pisVX -ULMh%H9X$G{g6u_?Q)GHMYCWiUDS4jON7F?CQue?`Df=?@feoJ*R*e{?aervKrjJ8;!oD|-VyfW$by6 -RvVIQNyo<~Pef&@*qqwqR4)&S+;{wn0Q;k0|d(TniRf^pHn7v>)}Bhd~3B0q$lrx10Xbd;4)$GgBY_R -W%gNg9Mi4;0LWz5HV3wUJ*tnvZ=z2=R(dpH2yzo@r40P@D>sZx=&w69*6OX1w?us{y)+KW8%}Ji+tiB -sWb(d&3*-uJu|GIVl(WtYLi^G?uLko%NyZVqs9JMt!XATgE}JDwNP@9fm!UK6q0hAX0rNJ$8RIUYBXZ -#+}F$I8`^*hbcykC5yf`>Ks(&K1}Cjobf@l(fl#hX9~rNNhmy>>k)LS*YD@A8jK}$y%En`--XN{0Q!! -ruZ%k#7#`RM)HQrOKs?&S7@{|QSQBe|no+>+3A+VkC(F*t!K3WS#JT{#R=rQ3x%Fg+!W4)D^yhWKtxE -fnIk*O$k0Br4_x5tpR{>+0g3b^;2_o=l@gLIHSIrk-{ZrL7)X@~@m=?@2g~Qu+gPK}5jyL%hybty8F) -Y}44~|2*^icO)-y*OjC_i))-(ecuMcA9S2=5hmcl~E -X@YJSz55vFFG_@>%?Y=FoQ(IMoQ{!5a25V5;k2t(Ql$#!k$q3W4Sb6=HAD-=^2{X1@`963#ebrvnnRG -TV}5+5~94)0!E#{;0w}iq0$I2Q3ACVSngt0H9b^FPd*)Oo{gPjcu!)q%fFWH-Vcp;c?O7K@cQ|!*?vEvg_8taW{*4O42?cX)0syprb9p^Ibb&M0g -|Wq5RFgx?wftljs5C56|<|+>^X+{pe0uhP`vU;?eTNEz%hIT1SAoXc-Nt>XIp_V~2MZa(E27P}AexyB -lxa?Ce38;XS5BlCT%>g;yf@*bx2P-=g3Mq3H&weQ-VKD)5P(>8*D>NV=n0C|0w{aQE)I;C=NiHTP}}X -mGiFssI4Ub2sW2>;)F|_~O%AFW=xZ&i(=60{qZB{OecY%lf#CGA*RsPpTLT4<92LxPA$#rQN^#Jb}p&_QJ5t9)sWPuH2T*+nH&lMr;u_GH~by -r=kQt%q-fz1ytLM>hE@1vz!faY^`*;o6HeIl)}p$bju`2K1#Vnm_Ywza^i6Fv2|0V=aF3gO$hE)&E&x -n|X|u4|sct@14PE~Euns*dH~ox%pT}UNq={=qBdXeG&O!qh6NcVfty|m8g@dy7e0TqX^Qa1R -OO0VdpZU4u?X{eQrsjRu|zAiKjS&9I2X@V -*L`+};_y3Sr=@dqL;0PrK^iCEY`vH6|oy-SZhgdXESGbYG%h*m*`~1C|+I4F8wP@4#6U8;3+};MiOA( -1qOdWW)YA$VS7dFSDfsJ|@Wrh)g5w!iR@&7)kC?j|b_1qpf2Z^_gt=f6AnLj%@?DvzZzCPk8NQV=lNe -{7KB$-0rKWlV!+N3XVGk2LBSk|L~4MYF{9RL6TaA| -NaUukZ1WpZv|Y%g+UaW8UZabIR>Y-KKRdEGo~ciXs?-}NhS{C+vnP`{t#^~y8@sdZIx -a&?ki{`YvH)dACzJoa_W^(a2|3Pm_RF#li^TK3UqHs=@#vlW=cZ6{Evr_GWl>7eOumVdm+%QC+zCBMBa<}Lp^Zl&tJ(_33BetTVL)9a*Zi@M57emmbZIAAj3tu#fGW<>=PPWahv$@NGaHc@NxYLP<@K{ptjhQ>v|zeoobn@`6|=%x^cKWqi<*UDK@f_?Om -;IV5ruxx*c^|G-v$fzCoL|C>; -`Tn+bpwRDD$1hWio2p+iMm`!1+vi2SK?MGO{A@t;&j`tYFduQN7&bHaMX1OVK1tM{0O2kzsauwx%jFa -jri*;`ZyY88L&{6i9>NQCh5tP$3x@G5y~&J((9s<8oF5txFEK^fHkK=x9SplVi)2#1^ui9U0h)6l%)W -FPmN&Wh%>bSd-C}a6RKTFn92?lRl -Wvt1F=BdktjeiVuel9NHxzv|CF)?B-#<6pyk;Fi)~U#DlOYIZ4I&ssN=}mt}pYsnhAn+ -40{1p?S5))j}*JP>=cV_~__JoV5$_*?bs--)9|ff^tk9liYtcOS#czn}gI_W#5CZ{B~x --w47F7ti@PS1}|-k!}!r(HNwrfE8LYBpspcX^4tL!8(tFy=s -%wrRgber#2q?QXAXZX7fq%3&xloF26KJUd(w5Y*{xuK@)kD~sx8I0klcRaCj!4qA}byV@Ehms0I&HJi -*#KZsGa|(7p5$a9b^8dF3W^uYXMD%m%cHY%+z|9l>SU7Q$O$Rs8bu~L2-X?ROTddDUA6FRCR+fII -gtSq7HyWXr+|-^<#OuYJZ#Qzr(H{-Gs|!@aaGr4*8~D7b%y%7H>P9l{t$zrnR6*F&=+Af3eF0V<858Z -ZUpeN%yhj`bLovj9UOF>i2WbvbrwMAOA=`j_ZD{ZpoDJTTiC6G7jj&uqq!UbI)S;7`@<4%A7W}6xG*%SXQ#iFhrL5u-r9Nnb#rfduFSj4^r^A -7)&gy1iM5N`P5@udfkmJb1dF*O=TEH-~x$lAGF2gvb=h*bXHP8)b=Q}`hFxa7(5D4L)HG_U9AF<+iDZmksQyVw~wmkVITmhO_65Y(airFY -1V2rx)FDc2!y6YGmd0!F+aY8*^r)YSo5`(*`Kjp*(C$-|?b9qv@nP2NdRm!PvQaf~rcMB));b6z4}K`+1m -LWJm=L#Fwzxm!`k!mS6N01pU2!~`3&x?5s!@+&crEmx%nNcPz77~+zz3ins||Q1y;u)F;#$&zl4{&6j -YuXK_vNZRd@||H10xZFjQe%pBmIi&3Id@FTmx2b_jZ;a8^8v<-DWl#8;&rz>OLE~81C_K;c*iFnt3k5 -S1UszZ^WOpDF7vb=WiwF3xFiBIm{to@&Z!;t4<;VmudXzvw;5+knm$NPGN#|-gf*y-k*<=W3h*C)zRE -QqZ)^9i}NRXpV@gZt_bfMBL^^q)ErtVZnum24zHVZPrL$1oy`s6GjZ29wh%b)$6QSp%>b*_;%A73EO` --!EPDRjsi*s>+khgW;_!wZ5-i?_Y0%RH@~{te*t4VGVUM2uewJ2;J=#rsIw|xV*efEw?D;S+3r)zrTS -t4#r#tM6>nro3<)6Iz*#>}W{Faa`hvLK{byXEO3=2Bw5oc9G~RZ$;iT< -#rHGGp#m<#<-8+A>XEE5Tu^zK`S0-L1tx^>?Mm~u@$b>z=-5%#NpAw) -RMyt$Ie`u>DD_~6qko*3Ri?bFZ~{cV=_@}L8eHS(l#`*!vLHtL2faJ^Q!vlS4`w4i16XY~Oqf)XGs~N -1feH$#m~SBD=9*h2`VkgB=3B~RJf=&0{_}EGUTN~suSzC6AUW(O^&Kc~{=@EP-_^2nfuQ%6_FqQ>0Rj -JhHrNnN(qetBzN0D|^a7&DTx0YUousQC?3^y>l{|n}sBf`;;8d<(RG{M2Ma1!mdN0;-iTEsi0_TfG<} -Z{`3Byc#$bWja{WlE9I-EV!#(#y;?t1LbYW;;k0)2$}nlcuvXoB*4t^m&S3flChL@IG+O@YmWu?Li9z -@$$YLt$PEWr#}7Mt@?W$ksXKS -yAy9XjXhKFka=h#E{1DB_ApOaU;QlSt&N2^eWF7%L0=7rVG`}G(9*czn#gXJ!GM;Q<$wk=BH-}$$U<= -sR)d4!XO-V#I#bBy5_ZGUwUS)9k480|_JPT^)RD9f=~}Es<8VLd6(^MYvn$4D73+M<1eY2E1PB6KoSR -)F%EBz-fG%os$9@I89K90E_rRMPf!eim#0T!4W{W`Jc8}bnrP(@yYq#uj`Mz72 -W1@gur1&F`kHAOqe2|#i_dyB>2R{WEg!pGL%;4=UTWzUuG;;H9{m-2SjM=UfZZ;eH8dy3`$%3Nj@)#1R?;r1}W?!ArN -t~m)#JMG1-gVIW@pn_ZVY`0fUxdz9tqA91sSrAYC>(r)y@!?c9i63vB3s0DARdT-BnW*moiHryd@g?v -&RNoKd(kLf(FuT3t^k1qb;8SpL84*Vk@L|B(WqShSZMMNJ-qDmWG#&a?drSo&kx2o^lk>eT$2T#Y9Uq -J;Fm4DkRsOJkuwx(IP!^Pihjm+ghPBLl7x&QfHEUCuHiE!f71!YXmR$J)EnVMYinDE-80y>+Gvm>B5n -CLBJv4YZ6%DBjqw6lhM98$jLyTVn-anm+hY%^=>)6xwoO}^HWJ<#+{I1M4B1AE~J%HG)b}z*L0<=L?8 -|{owKa_TanC={6*NFF+47uY0n#|2PWV(I8=M7|%bX@Ot1}5sITulqHiz{BE2MT87 -RVKD*NJHqee2`R@sliBCI+sB^cN;hkUFmAHq;eEoFxk5qZ8gP0Q_`IKdl@L5eGtq@5SX6CUALqGJ7_prmXLmU$Md-};L2$yvgv*sXuR8HdkW;dYl%RykQ${u=U0jH -ms~f48zT~jY(=K^@NxUS5_om{MCyQ&vv06%mGWp;3I4MMiL!XwQG4PDVWz^M)jt-*90JpKLCKYF}DxN#bp03s -A@8_Ww<$dcq%-cwfi{Pf52dEkBS*L2`{(2?{2f8J9t~v8%{xs#CTzMs6Y8G3f|7c`Ruu@Ny -@od+?h)t=Qon5k27-3($(^mHX(K>HmU$UyUjeG$G56@mw}B3-9s=()-O6e`Cup25Ap#8MnArP&ziHlK -a{TVeh}N!L7Lzp-7I4lc*RI#iQ1uz3`kzyH^|fFZj>5Lys0=v4|NAwJlbO4?S#r -%t@{o`n}VQJ0hLPDA4yt-P`qtYuc8uPL0o(!m5D6_ZTljjvvtYu>uqA9afpZ0BCbC=}AgthYw-l3%2~t2h?(Sk -`n^9VgYNwR<|MnYt|O{b7C-S>x)lQam34*3zV{&1AKE;gR2O*YY{|E5cW5&JFI}IZ|cJ8SU%xQwNtP^ -IokI5hBNcVIDKH#Et*YPI;7wU!M*SP?aA-OpQ0AN$q$;MIlwUo*zJc8nc2ZllvS9oas{pw1PzGp;C86 -J(aDR4H#%tsS`ij(Hdi{WLET)ddefw}x;_Ac0|~L=;MpIZJ$v@#*&m-hJAlQx{-iKiS{JQ314XAV!fU -7Q)~N}J#E0xdc>XnkQ80TJI+1)eQWO`wBke_1&4Lm%<(j2ukTonp>QcSej(6C%zI4iHCxn%8(x?A6)xMGld>GVG3rV|W$d){8OX6l@C -p)%XLa7<;KIP$*4T*h6;Z-MIGNKU$8730F3=&l?VMto-i+Zz2ZP)h>@6aWAK2mt$eR#R!EM9=9W000bx001BW003}la4%nJZggdGZeeUMa%FKZa%FK}X>N0LV -Qg$JaCyx=Ym?iywcqzwu-wU5nrPWLZPLs(Oq=*|>lrup^`pIWJsz4XLK3qQ$&%Erm7V?X_dM_hs9k$* -?yY9*l?dPfI5;?OfTziujYwAQcH3Up%{p1OyPaspWZ1O(ZIxVzq!+vPN>oV+58Itcy0(V$x=BWCxF7e -u$WBg^-Jv_2C&Sp+%kix3#=33FZT_^}4&vmb-gRw1LhWWbw(WLM54P=k4ZW(Ltu7cg`?22YUk6>V-`A -D;{%hT>>a93g_3bVh4;_wQ7X8>97Re)+m003P7Rh56)*^W}ioU$qibe9g9^m&2InG7$TKsD-nkAIHZQ -#)f1MTWAE3QOSwY`GeU9O8>4DG&OiXo_#b>;H1#5rZVvcDAlpxXVNejaPk!w_n~HlSa(dS#v+YdUmdS -+8oVyVvT;YjLzCqFFaumQCB#%W_-)DvGAuiDNCVpf|vjYWolV`3Wt$vL8fJ4*aFKngk%aZyx?ByY9=b -vu5~`9{;UvFCUe#&N0;5OQo7Uu8~GB_M^ATK-8iKQphg$^|mTRbER7Rgb$uHS9RYuxDbov=k`VCKaGJ -BAD4nCO!xk*8Acdwi4ULjecQ*HZQEt(v|5X?7?vC1EQqeYYb&uGvTfa5ss_*Dd#q{;2xQPa&1@&evMR -^Y^weMtQK*7Xf>Ma89&vvK;8ek9!rh8>xjZPSvy%iq!BCNuf`_u{;$(hDk2SAMrbimEvlyZ-Kvl79jc59wJw`C$#I05L*veK@Vd9xSdGHD3gU&_RJa -a)J!Fw@o>03}^Zq{hF@5tB0jKnsT@#92VjuaJ7L)aRK~x-xV+zHL9W8mItb+pNs&!7;4xk%$~*I7VQV -*#-p4V@}J7$oBVWBR{|%?zj{%XF!^#1!=c$z$&txxxnOg-W9)&O4f_iqA7JPjZ~eZ3OymvvJeSrXv>U>K%62IFd}=Kx<%PEZ>XeU -dA+C-T|hsV -3jLstNE<%DN`^F?I23?Bpg)MBlvFC!U~EGrhzR^S+>Gha6kWD!EY~+J1i1L8I&Yrt3hVTIVh@^=aSYz -aX?DfofDp51T7NSxB<;*TaPnUI(H|MdpNL1F$-bqQ9l;2g1fpAgrsHL<0Kty%Q>J{Di}-kO2f%OJsFT -(XK5zZ>$AF9wdtH^q!GCUwag!%qq{h@W3v_?&BUp~{M}dYsTrsX!gN;-u-V7x=ol?fcvz^;alL22#|~CK@-$#>NepNW -1cYMw1j%U^aoS2YVV#5G>l1S6B%9YgcOglo#!OOhB~u0x5kZ#^r`6s~1~wRf4r;nFdMOz=qilEzn&vm -e3Khi+YKB*49yYL?Ri=y0Y7wJE12KN9Dw$I6fEpG_ -8lp6PZ?#vF^gOu$0}+UXEJst0vebf%3hsNT^>f?U>&hmVRgbN%uqMd2g6#^cGWHx+t%nxZu$mf~QjOT -=9h^Z9BhLPP2z0$|FUn7BgW5u*1zNU@W79Tl48#H@KrZ9dJYXaCB>*;7Gr@w2v$=z~z8Ll$?9XYGbjT -e?qY;zVIK*m`n8J|;>^hhg(To0J -$bzw?Z5T$TXt`Zev!|3q@MJCX3b$ZYKc#D%hQB8F{?EL?S3>Rw?<$qCW9rggM`Bwpp6BKj8IW65 -c&HCl{Haiz!;Myuv__rl(!*igs8CQ62>{=Ghk5`q^|AuTP|=1SmD}U9B4S!dcN#MSN5XMV16SG))^P} -*N)I9s(@u*Z@IlcMQvncXnEiy29O^e*eTIzCTGmPaYWEVdx4x49$(d^o_%3PZ>v9g(M++mQgRE?N@tH@VIq`baTM(rVZYudV1MS`&!D; -gH$;;%Muf9v(zItwxl~apg=H#E4NicJd^tlFoZ~GebVBnMTp@WmKMggb++l4k%u`HJxp_q-mNnT2YvT -P1Of!7Ths<6^PA>|ebxH_DXHHG9VZ6j};X-^dzYxi7ingU`^|BA0CU`wpF9EXW -AfTfdu1g-}JZM-FAlPDUtz}Xf&=%}!E!c`XC8T#Cf`T|6$PSn=2@G8)lEnzg|zDtxJ(*;zJ>CIkNmsd -JAcmyA>#fbBK)9-;UR;X4F`z0b`wck1jaywK~lq#d7no#-do}_}4NOda?P!>XS{|OL`{|I+N^6tG0$y -EaZkoN%Kcu%5WY|RjWI6nb|^P2$N+};pjO#pq&#-L5?{r&=J~pT{{5iv0YfuM7GY*I|zoaXqGkBl?)0X83l0g -c7&q}!<3!hCmzX2BLZnfeswG8sKa>>Zy@pv0}ivf&C{O3P)Ptk{L><;5M2v}TIJax)}|e92Gm-hSqn{ -3;rpi`^^O-FsH8*l82<8jNz^Vtts|J|c@&Q7w?t0V413Z!OYWKd%3%I3(f5f@v_EDx2v$yUCd!_b%nR -z&+hOjlLCpIV6~_+y2dj7ll3GeW3Jy8CRhlqEm0+(QsBpam-#Fc1bY+ldn0w@FHv!EGc~ -<$gP+7U2=cly;8hT->`7s}j6F1Lz0&L))WaWwW#WC^@fcNXS`j%e57VAJM$RKs=(YtZ3>XOOkF4=11~ -Jp#0eU7q|D2>~PpNvsVfNMTN0@BodD>sA+*s3%jYKbhCpAXtOA0*oZ8C_-QmGr6GsPC@P3!KU1Z1r_@ -%gBgRnjWuBz$LcY08GxoU6Vo{2j(XG)OF};wAJkbX$pLYT~rU(Os^!v~)_Cp$A##SGd0qi%bkI@CO$2c23TJsur)`Kf7h2dnqm`iP2J|vDC5^pRD*6A7sd_s%3 -nG2cgH75%_|vp48>HXg+)88O99BoWjPje`^|wFh#R?6jq9BKKQDiCtw9s2(zHr?Ly7k-)j{z5zxY(~J -hM1=5j|~_PXCtd!9QPy^y_S3qrKMQVBZ5T1Ddh{tPdTz>A&{uA%6{O!bU6;4b~CZQnW=8+Fw_Zp8`GO -x!iz==R$o|syu+{W+4{tDq_Owg)5<8CZBcDYqf88g==P6XfGy -AK~>&mwMo;@$KInEfRDE+JoH3eL(?l5jZPW&PJ?n^x*DHsPUJwOq*nJAiT&is%QdXg_XMb{jWF1Cnrq -pM&5%-76az!Y^xDKH&=o&{VceS``d`At_+?NVBqLV#4Q)#OtLhRf2-Al+G5LGpr!H{(AM&a8Hb*DLL* -2;@R5FvSYg5zy=hWr&1Ss?*ZU3fg&T0h`psr5jE`u7OSv)zuVjm_YZn^rC=;hyi$P#O^=P-9UUNGVd% -ioaiZewaQ(GiGmp4JClyUJb##dja!Mq{Mjk|L6!kYX7(@)gEL8bQfirLdWhHxU?aNvJjwIqA!)GV&Ki5cba=hjF9GPnxq4I`{6E4R=lw -LqWM^?9$x6W;ibdDiXngc7|2ZrM+3*I0|n52tPwXvLxaaBq#19TH^VH-lG20yz=srU4pM{(=j!wyhGGj)bB~QCG!Y6V41XI|QssOx;|L^%bt%*_o7CXA%Ua -B>q{!b6|myFrtt%D~I8Ns^x7H-8T{iIGI`C?y__p?oN{S*zYNm2~%x^iFqO(2{oK-6+=!*fX+Yxq$m` -NYxDvE{Q}MG4iX<{v>{>iXPAIsR#eU!&1zv`*P7hlWZGAQt%tKSAip6Iy9-faAVOwV2Og{i(JI!}fUN -KZX|1rkK#33^fkt~^YiDQVN!ae%VN5!KqbG?${W+>OTAD+K%X&!IJ~)JnI#Xl=HsN(isY}XfXSr%DSD -vDeYEBj|FS4PUdd!kt^$iC_Hs#k}|B*-!>KH+dY3Ym;8sY*-w_a|PQQYjSHCU~p1x^hJzb^YKAxbW{> -vllrl?Ca#JPgT;SEOs{HCoe*fD>Mc#^8n4#agg)G;MbQO=JF9}WXIMxy0`Ks#qQxSv}UtV9mygq7yENrmNlqs5To><%yex -|#!242pJ$lS-BhM1P~3T|%q)<$aicB0yEI|I*;CT-VRtw660u#?OOluIp~W|y&y5h)jimz|VY5hTifDfc5L!i!Pn5G_#Fpj&dwfMos&K?$v3X% -c9lwr&tCm>0sD)+1~~!)ru=_!#lrUHMKjJWXEXNuA*Us>W{Mq_f|TyAD}P36FQQg_)ItWLtspG$c#l1 -0IQ-GCqLy4|}Kzt(*O>JIqm1?2#FZz`Ngza+?HGqaZ2IPgI#x6h&z6=HrG+7#hpDlcbxydh(yePj8<+ -f4q<}v+V)Y-3B!CDtG+BL;xqBhI-$DD${Go`wT+5tjTNKZOf%F@fPTPkHf&=g_SSrVCPil+KxJUY7$J -6XCtMfU^kp)!)m3p(gyC!0#=V|XzY)skR2*$L%)=IOJ+|7L|CKuiCS%n$TiqO1>35^{M@&1pFJiyw-M -58)q}L4QGVypCOhEwHOgcOP)dFhGAlw3sv)8Tnrcyk8j4U!ptPqk{mMU)#B7kA57vJ0x4J8CO|e!jmvaL3;U1GTTrFRC4jF9l+92G --uqT(#fP8PU4?wNFHnL46SLtgXqY8Ou4Q=2A`Ym-QuHM%hjP`9L#blT3m^|(15kVvKWj)pX^&1xIp91w>W4&uUvQM`N&)uHq*vEMzLD;$(J!w)JumgUmUwdi%EPYhYM*1p -hNEP0Q=T>3(!34D-mm6MKH##_EUNw^s9LN7fe5p&>j~jfYy~L6ihfhOJP)YtUiV9Z>rWPk%eM-zgr6L --$V2iYj_W226C=nT5irV~c*tq-){(FYN_IA)AWQ7V$`(tM46K$*^a+seh;#r#1FyV5pf|T(-pC}FVD -_1P+HT>M1u4J*s-h+C)3~SVUdtagJ%0G&-hkETl<*yZc~?jPq=THpJ37wtfNWM5Ga)WY-(Qa1sA{7--KB -Ga1k4?OB4RMPUhcPLPnMX5WTnCCu2mZAp7 -Z}LpR5XN>a=JLhp)xb8R-ZQ1}3jrnW^nC2W7Wnw{BdS_lQJZJrG-vklUE4Ed1niO>wfs;Tu+(pwDseQ -wOB0JT{TM(jc?g<=TcqJ%@+W#*v;XZ+@Y&pB8NB>EG(uTV#-dB^f5fqdNirx>dupeLg@&U%jeM(i2qk -Gu>k=HOXU~x@y9)hcxTl{t;vmvgG9xB<2k9P&V~=;K?7^)s%cD^A8)XsJLfNJi@qk~IRHx!HWYI5xQM7}!|9AS1pn=~UunRY5lN9Mbz~PGB$`oAS^?lz+ImQG^SdmB1UYARjRfIP{6Kcrj2a!ft -yD7Albl{$&%MK+8Oo8hCI$T29p>(j7BZ{1E|@X2Y`kLi)ceWq<~w58dJPc|H~gfpbpH4kXrSyz+qUEmmb29O{>vKHs-5Odqjc&08WL-nQ^O)j7zKQAXRa#d9k>FP%)KB4Es&SAG -t-*#|@OVC1~=bY{lw2EI8Ajkz{Mztk;E-WlaL>1-k6i8WBpnZ=w>?) -_M}(?#QiE5fZqTxKsTO#_@=4B<>1`t -80gvG@%lC!=LRZ3W>N3`-Z4(g5TNB%n1&MA1_{Q%Y{H+ze8x)hq5!i78?^iQZ$>>AQ6@?dO+?*7mhuk -duAmLs$)aES`L;22!4@3R?Zo`{g?Zm`Gc`DL} -xj)qFX#NT5M(fSMDtZFja6S7M9mr@LR&Wk-HmIPTtpQVjc@$IVfAU9^KI3k;99@2zyMnRZVSSf;ZM%W -hn$mMr%@@|vwaP7}XjNrQR`ns|o}7ru3Y-noEHU`=;zRdai~#|CB&{~KfV!v}lDA3orYMPI80{FUGgZ -}NIm{-XvfVYd4beIO`zK761MaW7W&M}T&wp^TgTV9?Io0iA=C_#tEIAhyl5HRy21%w-Khc1a>L(mhp?T<9i_?;j#oAUJT3eNCE09cu;i2b -q$bM4AX;aenpvmPDnV6kludh~6pe}!>20=nFavMGM8I|ivU$|?N?2z#QfMJ(7PgHiV*--R4%0t6`e(% -fb&I#>Q7S0!zZ?jHDEe@os^lt2WRJ?j$84gbYk4;&yeITHW8!$ZMbZ4TW&OwQj=RfYAT9QBzsHZVqip -GHZIUi4up#1wdOVyJorcwO9k9iXz0Zw^4obqeUgI3d2oF~R?41Y~G3HAq5;T@*La^$*eP=JJY8uo47z -e)0|G29E7b9pr(Ep(Ji=oy$WJ=*nz!cJ%`))2i-=Px6SWVs-j|TGjOOpN>>Y&Z$2YfUH@6u -qh)(D+Ntu(j_B;R=BL?d%fZeMbT=dQ~*zo+=g%~;z>^N%gbp{a)mflqa5{vFop-NdYtLJObePCX*>v` -RYSco8VSz6=S`fL2}0{<)1`7yPZ?$}flgmtOY(61V-Y#3gZmvvfzt| -NeaIrLbbi_lVGYMuckSU$z#?TUAd=^`?wN(>`iWD4^#Z>Bf`HZqZ*96#w#@&e+0hjJFSdepr^W0;)zJ -?iTTL@I|P=l51+0qPb85qDv+j9?K}GNAWycJ|QSXQ)-U^mCkxAd3cNuiK*vAm5*r{de6x#gL;b2kp^2 -B}o0w0}bcd%i{5qr_Xx}Uh>fk!#JCb2s%+qZ~buU+^?&<2 -F1+f>tuBfI|uN)4OfHav;F+G2gB*(iId%!EUe(>AMyq*o``s`%| -FsJGj>~(zMM1sIZ{ke~h~M}LPkEQwhI+P --tIDy!PpLdPFuhPhu#Z8==x#N_A)871WUQ2gpcCZc5+;-~R{PGDst9RbTrDO>D$f?w+pu#k;@Z}G8>F -drr4JUGmruIa*xRF}KYjge+7UK&J!6+cP030B9?ud!71I6`o{Vl8%6E%74VJr@;7BV+qqod~Fc&-w<% -$?`I&2~FY-n@AH0&iP7$Nj -DjX%o$y1@ix-=?!YnL6tc_BM77ozc7Gu##l*p7XV-uQ9TI(O)sb~3{TU)C%yVnVkLC0Wu$OPnb6maIY -dvhP>G`;N?;aMTpoDUZRv!M>m+khh4!nfnx-k({sMJf2eI6cF#RhT=UpOF8xjFBr^kDX?C@a8FsBf1+ -&8?-JD$IuCx1i0L~7{3l8AdvS+57}9Si>5nN6dpCD(D(@ppM`jIia`$ViJ>U)~%5#cW5H7Eq~ -6kh>Zf3)6}!8tUHI!r3eA{4A6=qVd`kX$Nj1Z;HV~S=_`9XU4b9>@(+4=z7Bia@(;zCtd~fVs#w}|bH0HI=+{Xv!8^<=l3n~;4){Q`c}fmQMRHj%13Bw)fFwT9ykdudL!f{t>TSpM8&1}50ni*2 -c)7wG03%muNAa`mTwzvxRjh;{-f(u7b82X8l*0sP5^Qw{FxqNn>+Z0{^1MDc_HWn{L+gK&d`_`acGk| -x2V`&NMSCe;YYpMkk_GOyV3o{5zJ5aU0PbZ%QD++HkIUBGf&t@Fxw5oPYnlFqCjEFGSwgtIjc@kJ(Rj -{Nh;K(wNr1|UMY!^K6O20?9bn^cd7x$gg!}<{Lle6!1zK?flHfdAITtxj2ngNx72c0lNpd>?A+$Yfk&N>k=S&1$|*CGf6!1%v0O+UBeQV6kn8RQnV=@GC1>f_0`OLp -LEdvUrxF1gg?;Q=_^`v=7s`EP_$#o-=1x$5Zwz?`!5J$e`U{=>KpUotDov@%bxLte)%q0dN1HwxTny{ -NyyIHTbA}kSr%i_hQ?a?I|)r1_&wZ2%IuQ-H$`sc6bd4*>oQUq13C;iR$8yGt0aO9Xc-`UQ389!Ha4% -9W5L@Nkv{b(7pnf@imsHLHng3hjIbgJV^%r0B+ahF-o4L^u*IVd4fIV5_|4IK#nyRa%R79geOB&v>ac -{rqy>3o-VR?)$F7uwGI3J8kXiBqxI_Npsjpg*S>)mCdU0VW=4(c#T=_Gn##{>gF -l^sIaoL_gWm&2CysdvP(zL1g%0WqLjiR$(+kadjT`q+as0xt!!+#NOC!*5Hso?pZi{l&Nim}a~oU$(1So -tf?FLS&;Xi8(W%Bp_sqKo=(H!y;q{hbh6$IAVT(e}8jGBQj)tO*ASfg_24F`H>r!vJP1v)eo9^uE6tz|opI+ybUCNet+iGRoqSg35zwJn8M!eSbF{Lmcr#B>Xl -I9!Ol76+^(+CL1x6E9NOdnENNI|ieJo}s#CMUuHWzF}SLeZXKGP?8ewj*sPK&{K8wp%(kqr%lMG~ -C}S6-W(6Wx}r-e!Kjej;V&Mu~61zat%#;Wi7&4KDB_r)+NO75Y9|9NB|kHiVXLbeJ0`c4<6d4+Ux2(!hQT2 -JH;7KCoOHnS-g-ry7yQZwO1!G2JB;1z&NRLJi%RcWZ#6!xia8GoPS(TBqA3tTNy#QY6-U`89Unj3SIQ8A1ZyI>L@-rlB+sp0d8tuGlA4$%c -B;y1e`^}HB4vSXIsaB>)`Ukz%>kE-8?je#A-BjB)vP?wGqBG%vYOK#`Z-SaoyOLiODJhSq^1NrSl3=M -WqUTyzOlXs&gj)B(prOpWeRyaX$;PLT*!G;ilL`X?v@B(rOFIF%SRA(F_egiMv!jx4LgrIYc!*8SN|%^loP|FWl_aqi6raK+j83GN7|UD!Pa=q$RMpzArO9NjcTk<<_HT$0p8Z -dB|xYTD;8d1ixZ1-e9brN-RWhfC=R+Qr8h5}sa#vm4_MP9`S;i`90!60g%6`u*NI{lQL`b{cg&+04^L -$8Zp)H2sqok{clk^ -%?B?0Q-5v*bO*hwT=G1mVL;f4p{aBL`Kue<&L8%fbB|uMvNIjiOjYH+QGud>md%x;rS$J$pw+zo^WCe)V?I71C0j# -SG;$iT$V$9hhFZos7*;-Yx8M%8Z%19#Vh5;aE4Xni=gJubAn%*5dCt?xu(2of<{DOE@#$0zT9KIQb-l -et*YpEuN$<^A)ZupGoHY+sL)c(d{zCS8vDJpO{1@>GfYtvd#YhP)h>@6aWAK2mt$eR#O+blrnz>000# -b001BW003}la4%nJZggdGZeeUMa%FKZa%FK}baG*1Yh`jSaCx0q-*4MC5PtVxL8vGu7pfkLJq!jitZU -ODXwh|P_GAPCEzvd`nN&$Cv0Lb+H%^}KoCp3JKlYF_uX0UDs3FgwQ^$bO1YZJ@Mg>HcJNs0%| -@zC`>5?~ZS;=0LnSP0Ufvj!wcpl>MF_}aEaD3~MER -pdLNqFp}#Xc96V{gUb(im-iz|2I@y(5DwNVN4nAUtyOAVpb(XeElM92`Ai*TN!gf@vc+blPSGAoMv&J -B1#vR-p%N7{e)Y^pWcmH9HmMSc^VruW3|0vZWQLD%@Iu=8)ERn#x5yzB>uU3k)cz-Jv?1m^cD?tpEKM -f^KH|hRz&V2+_skj)?|6e-k94rh|;u}4a3INu*hWI{iC3fq5f|(=dvbr*ncT(~6b_PceFB1n?f=14jcm{nZUb!S#@^;JbHtszw)Q? -z<%AAGoh8H{oid{L#pVy@zjvMKUDQj3)e|o7791(aS%%6_=i9@$&X|tbn#1$d5&jOs|H9hs#4FcrJ~3 -DY&YucnBxu?~qYvG{rz`!on{I-VCauw?fMez`JO-oYZdlixBb%-aO -b7OM%01Q!yn=3{sLLMH(FW6bO;2B@0%%UYwVYnUZ4K@l#dGV?6qae7IPSl*2m1@ki!^<#1A=yop^R+g -Wn}kIVU_IZV(7uRjw|@RxG?_Bw$QO7FIv}71b8qBaN)1Zqy_aMDMJIwlp4y|v!@~U3L>{g)ypM?Vz4) -2tzxJAE`rmia&oRJjQ3~gO2C%%DVxA1B`TR?5&`p(AKFuvaodJ+ut;l42zW2|e+<@^I&>?CM^>K{f2f -G~<-H087N-HUqr!P$xk9<2?s{PsjaQApFo2fnEk8{qYBt8UO&qTmS$f0001RX>c!JX>N37a&BR4FLGsZFLGsZUv+M2ZgX^DY-}!Yd9^%ibKAyt --}Ngt@MJ_fB$UU@omL&S$;8fFPxH8AJ9RU%1_p^Gg$M)?02Iyo>c96q_KgK)Hz__yAh3J(?Ai0)vnY3 -M(+y(X-=CFyyV=+4p=qjKKWNNde{^rxO}#0%XPd6s34U6Yt8_na56Z!3AmaNDv?vwK+=O{amkci -XI!Ja|89m3psrpJy;mSFZLrjtjZ00TmcImG9S*urOVIt;@O}a1QwRdDk^vtYg)zbFbf44%w=#v$m6)^ -8Ld6-uE&J0EyeD>t@&HgNE_u#m~=vdGl(zx!i8Es;uvH2mLz(Ah*0a8nJ00RkPhXiw21P2u~y|OIa7P -9stC4y7#W^*ShQMOu&`*cMG~HfZCawa$gS8ecBUZ6+iNS)lIj{tMY#&&4QnGvd(w1S3h<`Cjr`g8mmF ->+N>xIvwTnZ;WW -4u@M3|}+wRLtu>#SXi{-x)yI;ern@gHck|f`EG9RQEZY2n5RUO42dmzE4l!ZV*qS^4fvI1saQ{zIcj@ -11FNNeAKK*1mn3cp@#KvJ;FwgiC?@7^J*@7|@<(GBL9^f75HFu;yfBb?@acvs6 -Ov2UZ2^Rf|o{;2HAVCmt>jwyT=~+Y(r>fgUW)i(-aN=cCuhQdR|lIdh52Qv;fQs|b3gChXV~QgJao;? -LdF74-i%8WX^f5c@!RS&0S+Sgi6Mv;{nW`TYA|UOrnM%Hg)z4@+Q&>veN;<^bfLLt`Mb#wm5*RQv!?B -^ZAJy;*6PmkR+jB4F4P-mgVo7kJZPBYfB+Ll#=&raZJUvHyV4VM&S9b1~$1sO<92sJ|=&nB;h5LLsj} -yx1rlK-g$4H_g5-7NX0`9)LuQIGYkAg=m|;FIO~^4cZ=}A-gj$=!>6Uz5edUAD_RZ*{bnwhoS8+FD}} -AeV3#41ayF?b=q{>i@ud>(A~-*xPZwvT>)*D3JF;(xu5JWyhRjn^s(U~qV}vr}>)0`O{T4<|zRzoKo3ljjBAQ_7>F@yN8yxL2miK8zR*=E1IV -&)L9hNfllBcpCR1^T0qEmG{m!`0P?2agbG+cm!fpOX?^kR+z}XhECWpAHiJi!0?kxmO~7Yxb~e+gp%f -DNxIzXyWn#AkCq`@b_hYDUxyjbD)E!j94xKxwTw&(mtu1B}||fA0Ez~Rg|rd;3?yykWa+l!2`P#uR&l -GZ;M0Tn`J>~1xB%hnDw&2d3hqDI)g#xJl!c4CLpNry5@kort__R=upsNXwP_oovy#Uv6vis*UoOA78CmlYx&W8Dpq~LMF!7;9(EUhA%`_x-1t=a_^A -x~7>)4G95}7Epz`iHTJw(pqgIPwN1s8S5l?~XHbqNl>sQ0@S3NKTZ-OgG5wH=c@ --OlWe13suhPvwqLj!lDu7~%43RsX;1IXC!3sK1&{KcjQUmKoL@BS7-1;B{Ng*us^l7yQTm91@?DY9rH -cy0)CBar~z3Itp*6&K=89mtB~uWJZeZS{!RcX#9z#1zo`yOS4KKF1hJoV -k^*zjJKEFY=spqCIoPp^|)3B2^4Lwg6)v_}eUpk!LWZ|t?D2K@fZ`lum_&x(vGL8X1ffaR_lM@#zZo) -`+AT33n&MidSwFLbH{02If$0(GQe~ro&`|k21%zs_&3+ug<;8z@OC9@R#1TO%Xx|iVS)obaqwdz|435 -r{}P2gjI8oEn84`2mD^Mm;FuLL-Z!p2;+>J!d74usOsfR{9Uq4mel$~0XFbj53Mwmj_SDU!E?ovh -(HTL*E^}9Y4s!xUmcmRW(B)5f^^LD8Zo!=86?$Vw;1pQ*Tad8|dByF9TvpcT_8Up8s*Jqo2|c0P^!pV -IcB&OS^d0*Xs`5Xpf(hRVGj%F3F> -b@$T&#$CrMx9eDTV(HR9Rh-Ql)`SkZ))pz+!&C+kj$P^D&(1;8A+db)Qt^Z`u@>gPgM -67*L}0S*n2Yk!k-j~xdlKt)h@xY)R!;L9&7P`#BS&MKtT&MHZ_IDdN1T^ZOwO~B=Y#RiP~a$A?1Bcp` -8TMp+v;VWyHEso9XXYB_%8_=NeXQr6g&g>R_5mG8>Hd|R}S0!IzT84$cf-tbR=0Ypt%I3b1If!a=`%? -K7j&6P;o&gOzaKogAN|Yk3+_8LjhH##8a2z~5eLVJ=*P+vFH0E@-ECZ$xnjnK2 -%LE6en#ElwM-T!%?fG%dAJDhy>x=3Pj<^Ayxy#(n0?gklL>>SAW4#|Hbau9xos?0b_ab&A)z05Hf9li -p-@Q3U6qkloAJ_g>i>tD=!7GTGPq1U^%oquAUfN$yA~8=bSF4$3ebOZ7QRH?2B@N@e>5QLzio)3IZ`B -hnicd-D%xy`Hb1zG5Wt1pYFEUd8Ba6hH=HUtfTuM-uwdpku+JPIW)gxRq;-=ACgrAKAL52Co=9Gi -c$8kAc8cNwC+!+BMcgG7wYvht}d!2O*S!D!gVzN)f8%b)1v<7>&C99WId{FOi>s{Nst7nc|> -#zFX}(7TT1lfm0bu7DB_&*;F&K6=E4pQ_Gw+^z?rXxyuX!2m=H9k<~!F7P|mt&T9u|D3)3SA?NQvqKh -CUyhK4p^0**GVr9y$7jrAz|t&|M}M%9K);LjlZb^)GNP`4_yys1QQC4LDru?WP-lgUNy*QW1{e@ -W_tEFXDC}4r$hddC1r~#68q&+EwyEqqF$o!5LuIs6c)J;mv{W=4_uiF-$33%3*m@mfbnG=PP1Tg1-;$q4xzArl)n(2H -G2(?WPsnqWRtqNwhh$h9wx@k+Y!b*J5Spi#*+lq*e(XB&piFchGDr}qT%<}x#vubJAH- -c*7gvd4Z6~i2u+a55ti;hN+E8um%(T;~UZk8E@|Kn~#{XqiooqT@FVMke2|{0toFOp5_azvj1({z|7s -!%&`*Y;2TMwd+Et=vX9ct6^`zbP?B&O9Z#C(84Rb+VQwD%l}1?dS(cDqva+`J&AL@>o?R|cxT%p;BpA -ckv-5N<{tRzRpb@2O&~|CGlFZ!tl7-}*c{cYF+U$A2f%#Ncz51#cOY7=hJCh%L*3QrfuYqcNwb{AZ!WUq^vBD$$83Ea#E+)Z6qkW2CbnF7c7~{75E*=@ZHt=+c8c@5q=T*& -P$Qz%+|Wcm};}>IAq5vjS=(lp=F&9>JqjdN}A!b`#eeWDayTN6zm#yAbnixEJ`ZzwZIO*6D_x;8g*!f -KK35GNi;$!omU2=!;&k~P -Z5aelMxjwDf~TcY%<=TIdE6gJ*OkoceLD;7KJgm6Wc~{Sm_0``&914cRldGEG0|VaCzO$S#%3uH)@Z7 -&ogllC-lVt;E5+!3mi)v(fw4aC->Pl-&--K{p0fT|TW}vCG#rjrg!4KZ^EZ~|jkYB4;mqNmxrtFfGB`CU&y~pv8o$)9* -1;sEijn`=gQDC)d~hNpF=;vA_EpTc@t4unIGmssue-fO`LtD$tS30Rci>El8N;=YynYw#$1KDTIa_MRvaj5S)ps0w-Wc!>&tbX_Dkev$Qo}N@1W5BV+66X`gK&fQ`_ba>I -YjBf_{~luF#sJgCj`L(ajq)%+F;`ePh)E9Y)1&>|<-uk+P`#3&X+B<9XU)lQ6VM?MDYs?jzKE`ZyQ8P -Bp&q>~k!V2>KFvH#}^P?$nyI=VG$s(d%6t?q-^w<6!*#?-kj3}e?|ct4yYV=A|3;lL~QR0PCz&AmH2k -3F!mig3C9iuU%YTqS}#;ykM5*BNq`S+jO9F$LC^1D>k98F&iJ=>!Aowpi`uueW|PKe(Wq+E05+AI -N=J?3X7%P;cR@WBQC|^^;@yd8H0w`e4FldfTclVNS@ZqSWOVYP+i|ft;7Ph>A?{_jYEVbJkK^9iOLpT -~v6fi+ZF&0LJFa+CqpajV7Zd0LjEK5(?q|JpxrenglTO@dNwC83+$}IX|GYm>VJ>DXxYL -`#uBu6sMSOL~^>$vnc}y>uXH?%#RsO0xPX}`&s8mXwz+d}Xbuqv^-}P5F)BJgVnJmVGIm&{T#-6zdeS -Iu%a25vLN#-#apgDjog?E7M8iKo-;+KdA$ezg6*a$~=_CMdda7a{Xh7nU>><0=x`=ByWh#R53uZaKXiK^NfHO;u@HeUPPcoll-WYy-QfB6*YC=r0z -Rl|`umvD@UpD(5QOm_ynW!>~KG;>99r`b9jRG88tjk&ejdgzs94CAVhkL(_8D -vBKx@$%607IaN86HL5dKNPP28@Po^P~Fot)+4l;Kt77W4xuOxp|RO}rZ4G$oO>pf<~pIwGiIX%#opTj -J)hLH7r*)=sLUx+J4KnTG*d5pht&Z@dse}tfnPXIs4y8@`nQ}axYr5Ui+2cMI?Una5o!JOcxb2UWXq# -uf^TqO`;$KHZ|8+3~Y4=RRQ=E1{XM07kEGGJVxEAj_$Me?o?fBm`H -ZupVuTGt{9-sf2hD0|=K|V%=N4mMLO`nwUSKK|-o&wHj`N`s(7}9Yr)L0p3O;lnoH7K}yOe(SBHE`>9 -U*RdYyrN=pbJmbfUpuFM+}cdNRMf_uHn3qn4C;tmrZ)Yts0i^$cwgPisTd7IgZJR+V+~EwfPJGv-`p@ -3Ax8zNsz&pR4WW0bbj&9~!)?R>jH=q+;Nc%d{gKq*a`^o0%O2%PCOA%!m)_pyy*Vy8OCs5`Sb5;aIVQ-3c3 -gQF96yZL`$Y#CwVZSEjSnzR#g5~e6Vk!s18SNEivaC;JQ|MCavVE95kI{6@1LGu@+NQ&-b6pBQX0_5w -41a7J$4s#YSzI0JqKRPC6<;}a;dm~Dc|$Q7pmhDTy_wn;$pevBYN7thTt|RXuhOW@zouGfv9+UYF*>q -XbTIZ8r=&%Ydh5+FVouSM2{sVp^boz!?)-V*W+p(S_3`T3=};b+;4OD?atjU$A8{c?32ZTm^@;KkFV+@q+xSJZ_ -EIJ%}9@=A#mOI)$@l$T^C9K1gNn_`rahPg+TgGGfpMF4>^AoOPPgW4jUgJ$0Os7()}&JYbS2mMp37%w`=`Pb2THk4%4RpW^tB4KD>FQBiN}k>bIE-#&6lF4VPE+I>#sU2;B)Yf$)o)6AgCD0%AS ->3Q}7Mm2DF<&@cH3kKCNh66c!Pca|)-&r7k4$Uo1Hx`2RQI3Dn1l#I;wx6VDIydx#ovFWe2bu)aqu#r -Jr=_i{QJGm#=N&c&2Hc)~Tv)StUXds!_OY%%H3T_gt9(et%db9~mW6AsANU^?vvw}%=Y|VZ5`c$tXWOpX -w^vEfHu1B?`s`cK3Ugee;Bkyqia}Rh%*G$2TL{)xTq7_KC%)a6qe(l@PEN>aH!&Hf#a}iNDn1B`Jt@O -82KLC8(Xi%fydM)Zi&XlV8j(wr3=eAZyqcJ)ay(TCqucr~r5fR}YCTN)7yON+OY_OhYdVUxIzlv0C$Q -|16TjB%WUUUpjM?>|*5!%#r5101mzqOAF4#hTz6TrMtHaYnr;mF(0C2COujueWs=l*<1;;q}r~frR&LhwHExHnvK}p>Uax*ZdY24~tK -+n4R_{)JcU@aFqD)my&Lg7xH7(=flB`|k+cURWi9$b<;An4_$Y2L2#&Vh!<)jcQ_`GLeY)*r$Q%(ljA -z+ZpZe_mOf=c4LZ}}Z6X>Mz3?? -322H&0DJWGxI>}*)tr+k6%OJs^8I}Hn%ZZiRat34K*6Brf_Dyk;TeW2*!zsXu2*hN433_OnK~-(I*;j -T-yqFd*?6*5JNm?hJ#XWZ$v@;mCFORUsKce;&2)-DnK2JxU_-(Ix4Q^4yAwJtfe -Vnbk%MUubnjM4G;nwV?MYRj-1Or7ZTGf5S0COxesjkHs$SU_U#IW=siD5ZW5_nd2vb)h^$5-Bze`R0K -+GyZ9O%OV6@LVw;~VuL{1gmWCwhG)xZ??z*KO)Fe4;)UqrYDU${;UkPpp;)?BJrvxNE7PU)Rg8Zfvlr -27w3brU16w51Zw`Bw;~|>8Qy-_g{#K-kx=30n=OA(zSj1I(39%EHKLMaNWLdO+0*cRTT@W#dbcLtWS1 -6jbOjh_sGe{I0qjlI2d^D(1B55reh8{RhHowW0|T}moveNjEkiF(-K;EP4$GX=b)o73zA?il*Foz$Ap -)?!BDg5vo4LQbZKvHFJE72ZfSI1UoLQY0{~D<0|XQR000O8`*~JVQbZKvHFJfVHWiD`ejZi^q!!QuM>lJ%Uz|8{;>7k{EKxs>I -G{z|QIuVt<%I+qmzprd#x5R1bf({yKW@n~mYCu1OYY*U>K&?mh<>R)uR -7IbtiuQ+FaD8hN9}X1H$gbasplw)z)YP)Fhq#tzk(xzHQa#Z}0#o}6X|;$))y?KYb;^E|m_EH}oK-ip -A372KkbzaXc*W`#BIfGm2T8$n+uz(iU^_mcKK-P)HxdBpO)kaOt5VO4w_5q)IriF~iOguDBz(CM^@tr -LV7(oGY5|BTfWGx1_+CVL0ev3=V`FNkA1Gq*#{>2<-Ahu<>%&&?N5O9KQH000080Q-4XQvd(}00IC20 -000004o3h0B~t=FJEbHbY*gGVQepQWpi(Ab#!TOZZB+QXJKP`FJE72ZfSI1UoLQY0{~D<0|XQR000O8 -`*~JV_%`ETzZ3ufh(`bbD*ylhaA|NaUukZ1WpZv|Y%g+Ub8l>QbZKvHFKlIJVPknOa%FRGY<6XGE^v9 -RJZq2JIFjG}E4U7FZ3DIjw~r5NocEBKB-j9%OfZ|-`H*dBXoiK;BQnaEjMO|Fjk3u!o(95Qt%|09jE8S2x4l!A?R<=wk){1W5bSF_VuGqe*s^+XVvl%>Neb -cTh@hB=-`P(2naT0Y8gA6`AV@+LM;D=zU6$RtC5}*J))^^RWqUcw!?!ddU5=yaV&x)qrSF-5gOULS_& -j=EHn*J!dvs8j{HZPlDR|$X1ITN1>(Q3pM>p09TY3(j!uoSR(6kR7vKxNpigA+TvGdfc+K?-FLGvCWj -^+153E?4X={EsYud^B0Ovg6j!Ye!p(@!94|4((fyD5 -zISJM_62^fE2zx*DLnkr-bGo#b4O4vwL|Qj=U|wxs^Gb=?OrF}N>si$t4zP*+sxE4_@0t^AT2(-ESfZ -F2E5)7zk$;XB0;+YdzLqTEyK9ywpoyO61}jSjK@TEB00@`2K^zY{mW|;OmE>tpGoQ0z -#GLk3ij~%UWJM@Ph{wSu4uwbiHzBi{=94Y*CZ)4@^`L|uez@3qOVGSe+N49z36$#d+ts9sa8Xa2#E6i -r9U>c*?l!zrtp>^v7Zc?M83zbojt*O{q!|GOf5c{?d^hZxf1?)l53h$R4S`v{a}~9h2K+C8&G;qG|k# -hw7e4u#=X}gN&JB4_zxZs$wST3Xh2&q6fMwgCo9=s@}3_?nLLO-AI0N30Amd7D&#+$AD&)ca+0bT4Fre)jO -T+w(d$9MYj6cX&VLKgOOi$y-Z$5wO>|r{#ptlgTaxVA3=su+-CFaN!2#dWvqhfQ{3P;xIh%n<7HG;v` -O-7du*x$>AA!r-5Ph7I{QiD6n}OM5xyFMI7$KvD3P&~*J!#U>|JroxldS1y(KREQeWN*_`P*Hi&(HSIjhR -AwO-?XwODAD+e~U`>UGEXf9-Bf1b=A}oD)m>uj%fJceNLkL2R6x4HtJFrXbB4t%L~Rk014=d8~npMKQ -J95Sm30A}pl0iXgJ2A0&DRuF&Lzz^26qHj9XP=eHVm(56k7_}`PVu8Y#MMFzkqg^d~Eh}LiCiy>%G=&EinKbp11b9VTh~;vM -ja0d(8QV1Z7UT590an4FuN+(di<=f4%SAKU8|{Nej?e)=6%JQz{<1UgY9MWh)ZBP)_Pk?9SjdsT1dl} -;v0=~Ee8n`u`I+qI3O9-n)JKJ&ves$6EfNRJvcUq`Y -kU%f3b(bx$_LOFy1e2oN7EtjD3VWc+y!>aY(vsc(U+gmF4J&%o7D}NESy+Y9O)zmY`Jtn`49EZ-#pO9 -mh=G$o{1@^Qbd62%=$lj0eeKHDH=BZF;a?C5Z6B_wf2+_jZ<*e#uQ%nPX_zi-V2^KFbP&FXCN5`z7-#tuQ1m2PV8tCL+T1LdV3N$Ok@#@X6LXm1eT#OPP=7mNNVIDxSh#)oB -%GcQcvzBnnq1~g+IFHB$pzCGC_NAn!B<(Nfd4&WV{8;)di*Hnd^zS#k>X!XsJzn9qLNt28m*)W79%6c -x~8F23ZPy~zE&m%8=YiMVqpxBQ{XV;cG}=P637$pof`=}QVA;P2=D@(s{#YL7T{ou9{mZ%!R!P%MpLf -H;f#)Lfe|-6ksb31<(Z5QH#pZ-0|E-GhZ@&nWl3# -ww+l`DL388XDfnK7!eTPQ@ideC&zq -QOla5wA9`$IG@fOu?0SlHvOybQ6!Rrjg8=iT3iHS@s(q3E)*`f8xCL~%Iqo0mR$%>;*-}tCqarfJ4Od -esb(bLx)Bc-aF5Oo5Og!7>rd@ymiWrD)%pre!`wFTN5?Y_ZgRT6ciiwuxQU7$6JSl%}-hIs;g!%)+=v -sgjmK0D)FEp48kv6l^&$;L#EY?Qr}Lk^Zy4&oS|j1G<&m+66VOLRUSF35bDz~Mv$1mucq&!}gamJ%Rn -zPl`A?!qu#s0FcM!0i}>$sIgi7+3QkTNmd|A|1`EF|nEQ$AvOu+5&9954ff4yV@~%;Jiyg#G*xJjmxN -xc$P4$u&YhJjHDhB)*i){j=_i#DRQu?@siK -#Mi8QJz|$p2SNWxxeU5;JFQc!L4gv=`&!Q>N0Ao6QkSj&|ynbPFw&>aU$UdjPDw(TlN~D!Ol?n8&8i6 -eH0PgoqeMXAl@DH;4on#SFjrr;(WYNAj*{$>=gy?vg13Bp{rwiwP-2B -~G=%oaOvKG1y)rCO|I%DiL%)vNxO;M=_A}A-Gg|BLjT1Hh6i$G39&_omyAAdG%!rJrj`|b^RBQiMpq^l3pRuV3 -$+ZS^EQBR3Dh9L@iy#z2Pdj{g>_z)2lo*&-oX-}CN4$4_|KhtYV^VCP!jnY-Aw6OT5?442M-(vx4+^b -^%#!|u2$q`YJJs4!uNpb3~hcO0C3H%|bQ<8}8RwVvpZht38Oeh*m;hWGWF;5%4qsxEWve{*sIQldi)H -0!pS#$3$|tqf;VoaUOq));Nr=MmL6CfRcO8&+ce4=LZPS`D3vHbGRm7HO>6?1VOx#&t@yGP5Y~qoHKXiYXESIe{-zy@{A99$$MA`IuQ -EtF*OuIy1)3=uT0&b?^|y?XG&HVG@CKpx*39U@054^e}(3A1^0tlR=I}4hYc!HFSn6W2Yh5qn)UF$ml -9!S{|dvrJjS8xe8pmV!J_B*jY(!8!H_^xC>je9gwdM5@D+DdDUX3`zXn;l;RVpL5DpTIx!d!K-Zkv9? -KkI#qT5)NF^YqGWL4tFrztvNn3^3V)LkJ&(RnHiv{T8N!H*zrE3B}<)v7t94HP#JlUy%1IfbFe&tE1t -5NwVBFX!%anw4Iw37IM?u3C+4d)uI2Z?R0Z695f-bWQDKO5mVtV*Zn&j$%&`2yxR2;sK;%gcd8}CH~%Y@*p8HASUU;vdquQh7kM8Z}qed3X;Plw%@siIMdTC6bQ{LSW6FU?=0Yy})LE;1ASsF~9W6Y$@L$cyFu?{il-$AVr>NFVj@Cj)5GvpFjK*sADrViP5%?{-Ef?dq4s06PC7AG;{vtX^(C1*q^mf8NL;W?mRcu!t(794ceNM7n`ljYrhpJ9LFFWjHKL$d8H_FHF=IIAC -uc1{1^vHJu{O*{LBNN5SORe0wc+_l@*d9lhkUjn}mt>W->6z@6j4~ZqL7F3@;)4Qm21uw~4OQgNa|cw -eVV~uHDrJj?{F%IZqiB+K|)f$}skmQ1z|^KSp(k1Tv(X5DGWbB20K^^+7<2LU$5pQx4ZX8q_>$avvYF -jkmVhk@;I#hq`n%%N1HFEFmDA1%>`t$A=mAMXLJXn+be_3%mqOVO~=z#)ZpB#e>T9cpqpK#vimHMbg) -OyuVuR9zzimZh2_Fr#^8P=CEV~l<$+m(Cb=x>X~;nb-NWd)u!)(xz@0+-)z8z;kylSW3ZEwufE~opY} -$lcd~aboS*LS+TLgj^W%S)=9!6K?Uith5~M}AXrYE3g~Yx>Gw}(eOVRGsdAk|k)uRi^L{Y)x=2u==@Q -q~ReFX<>(y(^*l?4x^dt&gyJ^(K3xzC(l&k&E}1y>m?kJG!7;Bn`YWxAA0vSGwjm|}8lo}k -m+j>V$nGg3*&6q*++rE42jmB^Pkb|N3e6HLD;}zJQDLrXBRE*71Hm+}I`z8Dif$2aZ#@uMOipvB6iCI -pibz^$34g>Fa-HU5C3c6@z+pn -<2iEyPet<0Nekk#RRRB_}rWK~?ZNz=|An6W3xdXMoH%u*jl*}tfvDMcyB$~GgQ@AZHM?~73BlU$kLT_ -v@@=EYG8>LhX0HaxOm^gcmN -}pKeZ-f4KTC33o%GSMnbuIF<_bvB{kkL{I7A&Xx+ZZvdp?WWCQbZKvHFLGsbZ)|p -DY-wUIUtei%X>?y-E^v7R08mQ<1QY-O00;p4c~(=y03zQ=1pokK6aWA#0001RX>c!JX>N37a&BR4FLG -sbZ)|mRX>V>Xa%FRGY<6XAX<{#8VRL0JaCy~O?QYvP6#cKKAXH$G0Zk8JAVYzo!2$&7kfz%Y!;l&2Dr -O^-3P~k%i@y6VKSWWs<0YGeVL^h}l?HIo!Y|{y;$+;BYfPV&^lM4-!-nNjFPn~9h( ->5nJePJ{-4&{XiZu+R8a#gj!VZpJz{jsbMSQuceL96?iO?6Kg3lqjwNHA#YWW7s-k85K3L=DoR}3=!b8sN$_fbgsRmLwl2uUSnsTncgDcjU~`u^8xCq~VTqIkL9ckGx!t&O8q2&9b -^U4AU}6k{TM)7(%p#KI^3T3YDG{rDaawPc5mMj|y7g@^VIg{>5CMCj@_3L%7hxt#--$NYK6H#QU?$f# -}lRjOi)F0_to}0vXIqS_BB=-t{br+@{}uEWZC(riIIWNINHKo)%vPqBtK+=$x2O^8-8PNK!+AW59* -+VhcwVqVmF5f_od{%#KuMjRN2KM7t-yTWgVQW#Q?6$OWj( -IDuse(U(Qsq?U^x+bZRGeWB>g63$~G`wL1w+=hrEndQ6*PGUu>{3nLD5!br?%{s_CCxqQ$Y*j-~o|CARACSyDat)@3n -F>r!K&biSTe4vadt9}KntZvwBvr^t5$y4L%@lD15{ejxs7GL2%Re%mmDX2jE@Z78fFVWKbKQjGk^Nyw -SWcq!{{Z8ODB3`~#ZXUlt4u`kgi`?PX@M#v+LneotbqLHvOZ_Lj^yzUjU>=|Yi6ZWoHB?+UPvqMSe4t -lov_a(ks+zEpWk2vCdF{7dFMN8m9U#kOqw;s4Vy#4kvkB@7ay)VVp -hDMsuLP$I;b+)Z3sVKg--44E<$#Y8nYi)AR<*NWB#?Rw)q;p_q4wRV2aS#^?oqYt}TFCD*o5lhw*tu2 -v2gydGJTp;kYN^HFA;Jgr%^Mqm-$;KOm^tt`x|)uJehC0^^O#PNw%E!aR2%#Ar(YGDnGCsT$a-qu#cM -UQ5bVd!ap`Y5H4S^BxZb*QVgO-LX?lGV8Vl4r1=Vjj*o5zsa-^uaZ2w&sw%2bQl`f)XtTd2bdbS- -Ni`v0_Hy$(n}zYjDP|HS;FJ^#9x*e?A`1$+*E?rGwL!JuZ^ya9UiSW!zEV}bHQuS-8c@xk0wX;TQbZKvHFLGsbZ)|pDY-wUIV_|M&X=Gt^ -WpgfYdF@>5Z`(!^|E|AcOCvA^u)HQkaR=S0Xqq|~<9dlNajqx~gQ3Nh#hM~nE@ekG^1t8A?3)k8N#s8 -6&L3K7AG0&N^Z3o|D*3vunwAySKCcP+l#9q{GUX^JPvC~bB4X;c19rEA^@SX0ybxBTiZo|nKcFd6f=3r-B1m7k?zb<{Lf6S05tHqFvDJuVk8A -Qvq!+aV%rHBOWGHWmCk7~bR;8>Brrr^d-1ew`L$3(CniE6xi&`v?3oG`QhE -$H;%%Y!+?R7(v4cgTEX)(xOOURDzQep5-l_amjqt`i>Utgj~4PTFlM5iCGbFt9#O05E -kI!_q9tkfyS7qG{AS{Beq1EtyG{%zVx+vMiNW%%M|x&t5i`6vWPHg|k>IH$3(aF_KF#dGOxfpO8z*t*?bNwEfglJ@f&FGvw)vVfZOzq^d81<(N8|lP -lSPpV!378-H5~s&H$5^#dfRThyp5O5Q)wMbmJ3q%a^XlDjj%MG8IlceH%-aRkcrdyyehfc(sg>plOY? -tlCt5anQ|O0U)Kfg^?=3EuJ_v|L&zK%Tx*EL#tpDQw}GroHwRjNGXc1>;-Qy-0|1{Bx&!2{f$QSEp(s -hbX$naf#zNbm^9j9~_K-FpLn1bHUF1BQn^nDLJ9<&Axger;sBQD8CN;0WdiZCLq-wL{a#3NQv#@S)*F --0rg3EK))NOWLB?Hnt1B15Gy53liu#5Q}kvHqIs4mihNq)OOX9ZD^J7e*Cf8^9N2K4_`lY?6XsubAfR -z189J6;VljmiYDZ>I@pJ-i1q*l7Z2Bfbr2j(2mYxkMHJZ`9eEH|xALLMH@G9Q_&Y|3mA%RL^8sKQn-T -QV#qy>p{X#(bF9WG_J2MfeBG>GCLO+D$V;0DK*w7g_*nQ08NczW} -k}a0aWVC42LxPw&l}Sene~3UE-vYcMlt7A!Rw8#vl<(OLxFrVZo=6f`b4V)InuP?cKI7+zlIO;yVL1l -VCE=b2z_1yk6Pw~H09h3&ZpuQ5Y-j0|Jij--b9WyGhbF?R)@i^&gLSHK@2X0(;h5E(3?;c1Y*N)K20}2@SjCEModW>7oW@%7Cp$uuR8El?eFJ --Znu2dulQ8H(Cq(nadKtlsW#oETjmPW=cl-S=H07Z%Xh*1Ye8vKmi3he1-6hzhMvyvL7Ab4>KJ~H&X(s|P`@9K9C@O#;KIN(c -sO-W78C&v76*{z@U@qciBkjpDYDu>rx!!3tb9218A%8{cEQ;#B`4@oa#71(Ms!>TFKzNo6g68EbJG*lDSi#U} -7hn;Sz(2-Itso3hu;h6I`=;9&m*&4=n$baTI~)L|!ChaPnL-X8UEcC0$#@Oi!IYc}6efgDBJlu9SSyPI+0{jDrUCr*DTAvCh@MFp3H6BB$LmRH1a#xFLPVend@qo9eI%IW1^KM+CW5{! -MMt`CKZVg4X5Z-)LImxP6PC1Rl+8rrFKS+6V2k#Xx}zJvbh{ovvh%DTRVA)vSc@$RIDv~zUSwj92QW3EulES!W5b -_4*Z-;L_o@0jqPXw5gYQI{-?@r=z^kj?Mvcu)^{^sal^uOr)Vp!&)YM5uI|rZ+`0koJa=W}IEgq|Vt2 -EDC$(asavfgzop3$vdACrvL#La*MeHFug+6eP*6}Ar$fXdngn7vg6aSjNxOu3bmr) -`Z*;A#Z7(Lq#4H|J?!13BbB(y>qL>5vp`)mTE>FzVfR(uDCem2b{6^2I|Easka?^#`6Dg~T$A75Wr0g -|Zg{<)>jDW>-D>8qe0N~DKqx8i3QRrFWy^Mt)P#55C45-9-(_v6_&&18Z&PCLG3%AZ`=lLwV#)i5*(0 -eZd~B(G*F_x*O~{%Czd{WOG!;_g -tp#x!V5kl3KAZPFyf;(6`4I9sMN5xW1lLbHmQ_(yH}v#A-GWXNm4`8y1|3?;Hf8?vhBN8&)#W7`vbU+ -H@*&CwAKtjHYqTO^nHfTFi}Rk;p73P<_9a?7fE+Wd2kD7{!c+{r}RoQ`_r^D@bTxnvl%**xu*%TL%F!QqIy@ -vEA71D@+wT@UL-$_1!zed1}uo!!`^L9aQp7;_T9N#vXiueD2RWma*0b6TT{8jCqilDYJQ9V1sokILFc -#f|NDd1w2avXzfY~{h_)ZdO)I$v`CA}tsaqOC(>H^#wR-7ZrDYv5_k^#Jl{-P=9q4**&vP_fwZF~_hn -e)xbI&NAR@=kl7PR3Vk6LJ%9@QyYIn!odH7(CVUw1n;F_>W(n01#FgBWoq?J0H9bp7mQj}Oggf!<@Yd -(B>KS;}&d3{X9hMw8LI(w2gI~!K6Nepxi0QX40s)A!ba^N -?PgL+tPY)dv%F7*u_51Dx|4_1sJS8B;tX0~uzf>nusH@_ev?<}N`}cFf1Hm2$+D|$=jYuu>K71e!#~w -lvAf%5ihD<(^4ip?P7l>B^iRL#FTd}6( -;*=Y9ok*4I;6|4x8Cpmoog}rcO}m7r%x=7&K@W=}t)}0G)IC0*H1&ex#u!)KRYk(5V9B79Tr -qWFwVnp873g9`l&=aD_iFlEY&~jegp*$&2=Lm{wgNl#4x{Oq*gYtIRFTjZK`q*_4Zi*Kw( -c!278$xjS1t@Dq@Ge33{Tg!fyo{SQz}0|XQR000O8`*~JVWnpN=CK~_%wrBtVE& -u=kaA|NaUukZ1WpZv|Y%g+Ub8l>QbZKvHFLGsbZ)|pDY-wUIW?^G=Z*qAqaCz-LYmei`k>BT6^l{-ED -%NoA00-O&(B+)Beh2KP^3aqGpo$af4{2gSM`I^>?JthfD>YSNH(jhtLt6WOtsy$ --5{22GssUvUEQ2j>dt!KEQhwOdvl|1Z*QyS_H5m?TQMAV@VC&9@-qRy%b!2IUc7qy`qg*u-v41rx4x~ -GgZk@D*Zxg5AEmy0H^{EMsU^D_+D=`4U-f#_AIlw1qb}Z;Te-U2@61a-?1rjs%3580lz-jJW+}~;hQP -*O9(MB2rJiG1;vADRwVGYED;D>%S+!lUE0=dAF0|+XWYx*7Y=+*f^j&jb*464q_vjdeekYgJx{{rJ|D -(AB7#@3bKUB4S>3!R6Vb;IN71Ce|$Tw|ME$P;xfm<=8PWEkmFJV6okRS2R9!d5NZuETx)aT2ufB`xnX -R$1o8)@Epg%_`zp*u{)Pa7#~x{-QKep*V}k>2NuZ(%pP>V{^1-wtoueY4WL-t)V`-78!R- -#k4Y#V;b#hmB(lLqU>ksNla+M$%1=4=#_MjsicUNlb7Vvz5uPqSuMcJ$t0Q9e8kZ`nq(`Oyc2@EMMyc -@Z^gI7ot7YdOQKrTQl$&p>{Ec+KXuqRHQ!zn&=?R_<}yCjvc^6d`DcT?5Xa99lGtzAFvt>v+C0GqPkN -c+-Tx*8vCl>(ooODdq?O<=BJU)I27=GAWBGyrL_NB&*7=UNeXqG9%1uF!QCNaouUFN8b|Sp}2`2A@nEsa{Qv_bH&>=(G+#nRo@tW&H}OD*DMkoK} -*`YG@aC<}YzU9)aAgp~2fHSW8*T2@2E);_Y)V{1C1C!pAXANzHMl=7Z}fveWOVV=<|2_p`SKeWrXK2t -JQmwk`hyar^4SLME!xfwB0F!0BgIU8Sq9gZ3WM_P)Vot>dy0UCC09E<|^`+K^VFUYzr7O;$0FhE=IJb -RT*&d3nKO>{A?aO;67uK}Dv9QbPqQut%ra!U8Pb0hRJ_)g8p`M6>)Tq|l=c84J}BV2MRP{V*o9f_s{q -qO29X^27YO^I6bpr!{~AQm9G)v#FPmQcN{*He3?jL^(CKkl(s;@vEzzumBHc_$Z4MbD0CKlc`ylm$5j -GwX9*IbYz~4Ta?|hy;B#z~CG39@!e&ZYg0}jX?Ogx^3FdqKCvwKG&PoFLuOZ;i%mK93WCr0BPMaglWg -GNM992aeWP(^S*7gCYXqef2Yftw*iH*{&T$p{!>GCcx?U*UT3s}LF_6RrxxS%4GbR9TzRm2Cm5gJK6owt;4FCkW?$8-#k#EOJq(&DxAZDorIOFy8G&H34Jw2rpzqM%EA(m2aB6Kw7V --9XU@y3R9S%Kc|aZ+y-{_sg*LmsjSGrAd#1Qw(RpluPC0@$_-rZ9eZpVuuyYn%Eoe^b`I^wD$F2De(y -o`<@o*MC6B0v`5ou=+iajT|A6&OzcsLE^ZhK<}k&Ck(e}PC}Dwh7U(?A0;g4|mu7*aIV%$goLe`E@8V7#V5 -xbv$`Sg+z;hHHjNjnib4KHsX1KBwTsz*Tm|c7)52QZF0S8zUaxrdBI8LGfFqe_i(fsco1RLVOkyS&v8 -}pd(*({E}*~(MfMw%HH)TjLazPcp3+OpozHE@nF3YB7*jZf_>W`+{3)YkolQSs$DnD~jSUS$>ubhc@t|u*&llqIhi`u -XHw2R*qEwqN?AAFcnhN*}ojFkrxH$tjqm)vD+YFj@>=a1huR&1FTl9RH~ey76Gtt+vXe|wSahs;)OA5c?(fjkzy`1a~|Gi2Me=8A6c_9a&MYbUSH&MMB ->g_ZyeAS&WWNE`Hy)L8vbgJaU(>Gc#n?YVn03#S$U5C8Xuy$cz(~btKn}F-PKAgKbGK4CU+;I>_vk1_ -*2<~;-Ty?v6W|_?w4h^LLT{|FW{d{IYOX6VvO*YVrRQMGWiPUG=4wf5ICcJlo5`2n@Rpm#x$SHn<*f6 -HRO5Zwk-COzW8K#sUJEX;bFAimfp-v$pOw^9pWmfXcwnbne~Dq^*I0j-;U -)ll=!#u2zK6fUz~V+orggwA&_+lYF1KX-6F9Wq2O}>ZaO9#&2i%B9R*40-|O_qHR0k$*k@O&yVmzWpCRu~6THBW4Dp*Mii>kr~zz -WP@e4x&;~v4~$$%{k~o1-U1DMhsEuMlX4?1CtVnKQ1$|>3SuVE`<-(;bfdv8oDaye^*{F#$2wfh*GB^@GfQA6TwVjH-#g{V>53mTU3}3)fD!H;7s;n5?d`}frKr+ -V@Taz)Zp0wv;(SwvOkF1uH -0=R8X^eG-&l_xASUQ@f4zlq2vE0ovX)?s`uSzImy_qOGtRM`&PSLZ-ktk6#mPX}bO4EX%F^XbwqtBf=1uyX`F1B8W -7PQ~jj`MZW5&yHek)YcWsXX)BDPy%b;%Zu6o7=zQK}wg-H-TdjZxdLX2D8RBa*aYck$2={Tm|;++cq>CE16j|Nw;<{e57n>{ZCBk -^5Mq)^#9$A_m~8Z@G@2rZTMXICKpM4sd9n_VPbO)Na5R$ei&*BDHrm=?v(&?p_~CeiZmyD&W^@7&+4M -MB;&?OhjxqGE-OBtTRb|N#yG}~#5~X4C?w^rA91|bk+UOGpsv#$7LYWP9RvlY`E17)X7pd##%k9xzlpym`y6@Irp2MwHm>h-{YOtZJRZ8J9V6=w-psS1?R$dla -PzT!WDrrV{+=n`Kz?16p_-IQI{D5-=aS?Y?bhj6~!&O0$V~Q!f-=$-5owxc7ZDa_6)5N>lq$gsPP`j~h`X;@RvE{_lIR)*jRtMc{omT{_$N=6^J_q@13ihyCZd{(;cYjr^s%r7p-2) -zEuq=ESY+=fCxVD&4&0RscNIJe#22bGwxNl&gm<>d0(qV5_qe5_s&paPG%J^VSttScNNb<0zMvCiOzZEZ23Nc#kPtAZkv7B6)V9S>QR(ik>z(gK)=TKa%{mK&hw5 -^Fe95NniX`JXI8B;bYl{LlRiR?;adBNt9Se)b -Dk*B|h!~CM)3uRe}M@4#cw0!x<)FGKBgo(3OpOF?)v@aMf!u6B?p8|Drz!lZxdIRdhbI`%bX-^en+rr -t~}VNW(rB)1(R_eA(2In0h01*h_noe4CyVgxZPRt2$ruIRU7J;x9LfK6yFVAL`jU|It`jX>0Gs7oaOj -1&KJEOb41?bGnxV3-VEyI^6`e>!CatJ!>i>rTEj2-+QeQx|NcSQr6g%7{Fsi>k*{txof^&URLF -{&FK^1_9qjD#PUO6KR6*O8hZ|*u0Cm_U)vxI_lWfWX2uxH!)FuM888zgW0kG6*b#x&bJg^FD4^^?1DC -)$Gy$hNo*;Zv4`#L3uV@E6VW8CUkjvLdo26kCP6>D0Jy;RZrqH9|(!U{7Sf(D$3yPZxRJs8POBA6kdB -F4{AUR_Vc;}iZ9^tw!KyNhi0Tg-sS+~F9gawFF)8ItfZUxQLp9ZTe-l}qw=$&=*4Q^Id(VcWLY;B)NA -c%WDrZW=(klBbv|I)s+%my!TS3|^wlsCHtcR>-@CR4kjh6p~daq!F^{<cw^JxRYr-~!{>QQ9)%*S#(;a?%siE%O_E$En$k%oMUoK)F>< -BOZ)%%D&ZV`Y+HZAK4Nfng*aLs`l>v)eQ6+qQ^O~2P(HSytlT5FOdBbVMQwlWDi!PbWj++yy)j6!_6n -=CNFENI-#GFD5L!Pm0G;2@|YB^h-hhRrUj<&jpI?+l-2 -yBiBds)ZRWRClQpBbVYJHMQC-92YNIK4uJFv_Lz_BJ$|80~L&!@(Z>zECH>qW~Uu)sa7WZqmm<-!ovN -Z!_?{OrJ()8-dwgc8q<2iRVKoc`YQ=KVp -}!fEmlia48XM0YA-QLK}4}>+h)ugrG#Kpp3S_`47%`>U_ty*pHm#Gnol~7G -$y&Y8P6ooy>Iz&iyepqHgqnA76Na6x;t%te69~qrzQ^febY&uoyQ%M7}L@33l{#xo3cdsEf|zdcc?j#g7I;}fB -)%Rpfi-T0V+s9^D-!pIZUhXxCri4F{bpIUL}8SWQufIlVhE(+FCqVfhFN1;_vV_hA8S0l;u^fiN%6tV -aepv|{1ugX6pu`I&-dS1EU1mmu2GUeH4i;d?@%rI2IGW{d{ -B6J+XBdC{iX!{nqfDy_&uPft`YeM()kZ<$Ft^ujs64{&jj|SRa9a$C))ks5p(PlTKR;KHnhg|E%eF*M -F+aHM|#Xf97(LRpRiccH~}-=Zf>6qw^oCytkjs0iNm~g)*rBmbl7lq1!#|iV^=;b^k)$z1s>sG@@AXqB0i -Ft|p#{`CNP*bQ9T=J9F~x)X1x^u6eNJDkziBU?$xmEgl_A^$9MD)&8k_awo -@1=E9_y>4W~@(xYDBlL;UnEoK1kDCgLeT5_u#2idpW;_Om-gRzIO_LU>#W`1{E!h0ea^;xiHrM7<55xM#(-jp@=;^(N2@oOAW-i8#a2KJ;D``j@;@;#^y*j7hwa!ee#3|Mx(6Ai3yFgM`RBc%moHC -`MoO3-P7@G!Xt?Hp3NmCf?!|Jj4q7URZQzS8 -H&tSwPD0dk;`gb!>lf|!X)#T<~%GhUEx6Vc?EN0VC&t+b+XZSQ^pHs(s`|kbaO#IyLsWSz=IEatYmV3 -<#Mrvy;Ra3ucth~-uttCcBH#wno*)fpaGXr4sQYc;Y<9YTpwIK=bp9?p%E=gCT?ujMb;3J>(W8aE -g%$G-oL4DXGfW+51)%G!ZNB(49IvirhzXkp*E3_nnf~*Gb(Bjb&Vpx$-oFQv?}LXPx?NBJXAlU5c5bf -i~WuW;&keiTezY=VN;nb{&i*g$$%?F8=A2CrYip@{+$P2UN-s^)?;6|5Bo;(B)=RJM!TamW~SHG#ypG-!lYPrvIQ{6`PL!MbT)$>w89!98gtY7d4jQS -s{UN!tshN+|u_I9nE7hzS`IUPo$!**uh7$o4(me$IX3qb##J|TeT|LL2*y=~8<$$kT3@*82I;&RaQ-0 --;0&PH(@eM>-*&Ah>n*=aA>@N~)#rYx}b4d#~n`LTb;WI+kzNF6G*t;c*IhK0W)-_cey?St74RPVSSU -h5VkmfDmQ!IGQu4Xgvd)QBLJ(@EBDBkLVlUzgC4_(`Kr!-5Pmo)I*4Urer&CWSxZZ^%eJI7N9zCmMq^ -PsxUJ2-L9I#n&+7mm(u|lEGiv3IV_IRpF;U>O%xdn}Pip)z^YH;It)(XC@X%w>_w3;lIDc#St0PViHfq-wl+C2z83y -3OuVHOps=F53fs;8FVa_jjrN^d?C^C`=iub8j~tcT4Ab$bz@6aWAK2mt$eR#Pd}T -EDyo002oA001`t003}la4%nJZggdGZeeUMa%FRGY;|;LZ*DJgWpi(Ac4cg7VlQTIb#7!|V_|M&X=Gt^ -WpgfYdF5D5Z`(!?z4KQrltZLIArR!!3bE0|O%J_suRpCCAaJND*B -mt%kGn@#f8pCX>l;PAcgnS>d!$=|`z{A~Tda5^0%>t+djK3?F5VGb;*Rmw6mTXDlPjn_kL)=|zRK`1) -*1X7@~F+BzuK6&XCG%UGN{AE?eXAxuAPz(#-kKm3CA*nJPTEs-?>Lf{o5=#by}25mwhGqj -@laj4XB#iE?S3k*k{T5gT$TtVzR#aB@wC3T+5Z))|z;AtPWG-Zp8O5r^THsEUX!cWjp6XE-)H0b^K&x -J!2;*LUR{Hi$bCx`dHV*V%d)E4NM^evQ(V)v9&`nUW|E2AA523F}Zg&4W^HR6Y@RYbraPERRpb{#kZN -7m?NQiVcgS$z&2ma#LyJL{XQS(^?^(9GpxmMHvUd#_1{(jnuF7S55FU2)SLw4mQ7C{_){$a`xlfv!Bl -2|1cM?%iUa@!R2(N5an~n$#$tq5^ROZ^mRhr$VH*9BXd~;9oCbjf?IBT -AZS2${aKWjr6MdxKs_ucpNve4orTzECq(%X4;wd@VN>YiLKmk3lbc5x92yxNF#vbdo=8z_iKSqe`)5$ -^z+Om8ZHdByd!TZj(;3tU_UbiItf1weoSPP45m?^kU(*jGy=loQ_QYVQ)6O0A88Us>o4t5vI -;vpVoL{1-XoYl$=?0Z33}q-SKoxMtrEb>xCB!?aNK9T?hjKfe%!QHw--|hlRlEwtciJl5RrJ;vZ5HC` ->o;dVqtc!Vz4{Yoa-{{wS2i=Ua-5lwW0x1-Uo6Bgw9#9X+#2#LrD0!M0A(UKy7Itmg8DN4rVV2J*_=g -Dlv5!R2~7zThnq^^#x6PpUmc=qITO!Mdx1XfAu_15_BT%$*Vpfu2Rku)eGN`z$xHS{wzBwKTU4;GX|C -~56VwJMRiM#)hv-^|WKbUW;J03%&%R`P8E{NP93bhnNdqOIEhV(c!GFvMjkW&0DLb2Z(q3@2j;dyfs9 -AE;S-1)IZrgHVGT~c7Xyt6(U$DBLP`-@BNZgq8I -(wt)4L%p}-40LVMt4S65oeeREWFd`pp@|I#gGYht_3VL_+kHkAXyR5@)zHNbWA=dx89_YYVC?lVu>ZT -)*NFe~RTlxmLB{=$YoNn5aM^%%uOeFR9a8KNwW(pft~Mp*$Ke*lq0m^525#e-pK}y#(1QIi-1zASMx* -PSwJ$B7Tq`t};-Jnr4V)^TbXlGIL=n?H=4c+dd9rAR5)73P#Mt0w*f)n^i=&;79t)IoN9GG|Lr=tc@yVd^7(x!d&Rb$}f$ieq)d$oePET4R#$LNF -W?D#Px&BPLKq(mAx}e(MGbbd?#6*Uta -WCLM@3D$GTbpbX>HYqk*m@~WP1#zgb?aOV^cQwyKoo$<1ISGysPHwmcvCW2kFwHWuwn+h4%d4x{9?wA -r1GVd>OdF~)bG2Whd6(n~GjQvMou}dI8wO6V$JWx%?8C@}3g06&US+Uw-}V*nKlmPlrXpREOuHmnQpL -Rcc0(>{TFPj7d#t}K`^N`8>Vb8ymuosiGLTE-br6tgv8`$j^(&;4EDI$m!#Abw7 -5!-&(&ckc!JX>N37a& -BR4FLGsbZ)|mRX>V>Xa%FRGY<6XAX<{#Ma&LBNWMy(LaCxO!+j8r+6@AxNV0a!Pr4pN_(-%#Z);Vz+P -nr|gai-09JQO5C5^9QI0m?ec*LQ6IBmhcslGZPlNMPSDYj4SYqjbyGs;b1gm8v!)=^pKQyBlq^+Ozi5 -$a>5C^T(g=Z;SWe+`j+z?!(`v>?^r$r|hm3ny)J1Keb9v*>}95dsFs<_|HMqYrOoi28WTO+Q=p^UPWC -hooiuK^(rd4${Vd-Whu1j_}#2btNFUU_b>eQe8r9E&b;S!DNEjh#e1lL^Rk9i4`3{{*o(ULm)70Ep8U -h@KYqM>xc#BH|LO6Y?>`iGU)_Fqy!-m@_Q7A-fVa}8?)=y8>u4l5sVU|ohQHsW+PxMVp~*DAc-MancO -%{I>W$KHm$fYs3Us4aY;onzQ}lQmO!@3+6`kF`BzrH;+;Qdhl;PO(NK{75e-)k4=pFl@$T1ML-3g{eW -yLc)*|3#pk3zT?of)V0n|IHYfipb-ncm6vnGW)?b;XSlCLg`SvB!uOlZJ6t;@r9=3s?5Mvzyv@c{*n} -GC4xsFs&+LiR-OmCEmhE_V~^H9sct+z9&95pzql~AiwNcUbCapFYGAW9TJh4x1u^R?k&W!NQKPml=Ui -Rq_2Xf8wkTq%&k#IAD?DD6eNQzhBGW)0V^TpucqC|H7RSs -<3zP-1BXTuM_Ic+YI4D2vr;NdkgI91EVD3!LRvv_&ZzvR5*-^6A(Y(l5IKEWFji#L8=>(`kPfi&@Gsrzj@z7cLV90FWij%R_=#Rn}E?2c&^Gg0KtTjkuR`W -g8jhL<6)=G;cYUb>pJK@vXF^JeMm$O-|W;kz_%aqii*=k;jf`o0w_B6L4M)zmozCXmeLjw{xTu^)qgD -4_97|~iAr26pn3*QQ-k1J4}sJ#`;p=m1ONLHu%0WV7!5^4K;FGSwC&3OW;&x%6UvMq{npd+fywEyN4X -5Mws@Fwve;fW`FjXU{sID66PE2XM%4YD#hDpt5OdXb9kK}s30g0^6nYRvwiq -3(9p)X<^zftJKk#Oz%@P{{woeaS(40|M<&V5XeKf+-h}bP~M`_y -*&^+)MXQqe}BN8ny>{GGD=8?z%6t{EUP1IO1+vXjTiH9^HUF_8Gt^o`-W>XB-W1@vdlr4Y|2Ew%~_*t -1wh^8bx)8H{;T9#w(Q4;zvmI7yA44eUQNB09n08?#035azycGQi=32cQCm#JfUp6D6MD^Ju~;sd|Je1 -LAv<&h0#OzzGsKrxALX9DX0`-m9zff^o9G;Nbh0hFKm3 -*vVJ|ek*+vVIB}512V`zaD5HFf#EiTv+lgwegBlNXge$m_X&(}=qMCDbp;}}|~?^Z4`Z0w=r*6R}+1J -E@Fb!m0jZuv@9vOP`mIJ^4Kl~ONXRqRIlgBc`bq+U>2hMvuwtnd -Pd=3X8=0!6d8u~_SboLCV5Tb^hXMUA&_fK%<}%RJw%VJ%vKHrJ$;y5r$uKI!u`?bkhgw_TSV9$xl^mw -HuV0O%OxB`qK#GaH2)m!ZNaj|BC||YV~av(gCNzRrjWu#LLKAiK!2hI%1M;yyKUi$jPG=IN6~lIOYJ) -LU{6teq-l?U>#f#GzsU-8yN;Y3BYkFCpS-e;fXpPi8-lNQtd%GE -ecE~cS?8@2&BaY!>+j~z(0LuaDXg#C{FA(A63BCe8)k6+L}5|W@=ay$6`-2RvRN{KuU1gAsTM1SwPfz -8bBPnf=HJI1RT<%BGqE;*=3V*FjdkT=k9UEPUjQOu%}DR#t#>c~&1K0x)YE6TQ0y((FqLaCBVqPV77AltcXJjMNQ~_VEn?Y@JBdz2dmWo`t)Mb(Q_@WY#UCWLaTY82NPvuX1;V%`MJs4W$>GS2{O -BsZG0>7#@&(v}V+h@e8SQ>H>i7iVp0iZTN>H5I5QP0RxAe%oSTN_Iwo5Cq-qn)K7o@SO6&4~6<1#b7w -pkpB9_HB;vH+P}-GgSkNpKr24kh-D-AD9;GeiRn-$&$}x%^U%^|KtDO>ILOx7Z{ctNa2}R`9bQ#;pWp -9NKo37KFJ5>$^%LO)+?r6q?~MFo{c-#olQvpg0>O{4_Zb3_d-(=vwjlbJu&%pAUqazEQ8!cY~^XPT4i ->0l{FF{&_UE78Y#v6TF{Z2LquUk;0-PbQrmbqmOXQa6d5Y+t&DJr;^@yRz5L2A3yi+F1PcK_-Y)^4F>vqVSajM`J>*wTuY<{ -pF()zX}OdFYC$TO9V)#TG=IvCp^x -L#fkz}v;@E7ij&(FH5H3-_LNQHz=*F|{??@bM~qlRc9zH? -EOXT3_B4)yJ6@SzTiCs^`v}9-wS7jk#Ys0BaSNBfVIN{MtzUbD*TFcNO!f)sO1M2 -WEat5njFtX4vV-;le*bBc$2ai5jFM}%@7YvyjdHl4b-{IvHp1OD`Dl@xNT?;d*l2C+e@=nj_C&6dHBD -}HHxtvZPHVU-q1EvW6`_+&+AW3_gcbnyXz$X&9R7+7_DswH(P1e2lUvMK#1b~?5@k0PF<~;H%+m25z&E46((jyd?S49qtv^ -JlS9{L?vb+xMB6SZbHU!O3^Nho=W~5WZ;CLQ;+xD4XT|ngKjF?}K*k6R-EVoO9KQH00 -0080Q-4XQ=5KpUlRiW0Nx1z051Rl0B~t=FJEbHbY*gGVQepQWpi(Ab#!TOZZC3Wb8l>RWo&6;FLGsYZ -*p{HaxQRr}0a*033dzi&Bu>Fzkb*9cbEdwr4QRa40r}AGACQGCDGQ~NeY=A4U|A7eHjCSen^`3cca_0`Z9ND@PJr0BZYP -%tW(?RMHwj#j$>9_(p3m#kqHpCff8EXjB;E9ghC%YGCco_wLRVmSaJq)Gp`W<|SpNDI0DY*;+GwST2g -ggJA)1&sNR?58hBl;AF%NNDa}Dgx_zb1W^wPV!{`@t96YB*XSjfj$2wIMtYdv-1PsZ#eZmRmucxm2xo -_7x24d4o#uO5vSxX_&tC#tR+RWo&6;FLGsZb!l>CZDnqBb1rasUuq-2Vc?Yt~1Fkl7@%Ovl)s2Va>Fp!F{vaX~iANMOCLC=cQl8t8E34N$!Njdup`3G7;Ofo7e!7{s)3S63b -P)ul$g7QSdt@+}D(vKNhd3+V1Mc@6*X>++s|YJaLT`qx@kHA -ua2SHOkOkhRh2uQ5!g{zgCG4iNx3(Uq5Dm=trO*U%e6@CR3zI`HSfz{FR7%EX~*0}oGbPj83O++{BQq -#$J_A4!!V)Pb;;2fKhWS+&T^#Am9XQfXV)+u -xLq7!nIm2(w!piUIMv;qm7v!h2pMxVAI&5&G8`w -%dY5N0|(&2ket3~#&8xJxkdNN^ny!Eyccg7)ERhD$Fpr#UwD<}RuAdu98H!=AwCP|XS7xA@B^9GM`i5(<=NHjpaL;7lg}0FATAg4*H;nwkpwmZ0Y0_sxI7i9x2)DD@kE5))!ne -eIaY6zdB`M1@V=W5QL}1%d{dW(W=-TwGxb@71S6E~Q0I=^DYA81cY((+ee(Qcdp1$D0KVCC0OnhDk9- -3XIJ7B;k3=z%0lBaPF`P4zgY=LnHwnKn(<-+HwiVK&5MEugEVdE7uNt1)L?#8}czdkGVS#vZ?q15|rU -7lx$0V>IvYn!e%tvuZloz<&buMPvKE`@ERRkqw4k?F(;g@agh9+Neh2-N-*W)OL^ -wUj`oT9V7Q6mzmk1D+jk1{2$2OkJ6FgKZ|w$7G1#1toBUO)oW_QWei=R5BHIOkY`Qs7NasgMX70<0c2 -QjPh`q%53wU*Bj_Dv+}tvHDOsAI>3Fg|>E4O#i^TV($0^A0zd?cDi;6njbIKLWz5!560|XQR000O8`* -~JV8W&e%>I(n>Y$X5yF8}}laA|NaUukZ1WpZv|Y%g+Ub8l>QbZKvHFLGsbZ)|pDY-wUIa%FRGY<6XGE -^v9ZT5XTpHWL2sU%@&!#Ky^L+K+%Kh-Ii~_pRsSPFS5k?H&Q(C3swk322ShTjKP?^`}c)N2RAQdF(dE+QLosh -H(@jY()WxFONWhUqgS-8c}+FT$eDTbErn2(CqMQl`#78NZ5)tmv)EIRbzh|lVBa@-06l!486mE{7@F? -_wX#rMh=TjsY=-m!1}Qv`rXX>X+bQ@yO~D1j7P5&J;-fGG_pJInT -&kw9RS7x2u-1n$_5%#YzF*)%LW)&iPlnhnj6k39jV2EUky|qgb=eh9?&va-9l|ahMIjbsTzQ_anw~X~ -KIw5aHbz;rHXvBRw!5S0#kW`UzBzvwFo!_>-mL8kX1qagUvR*9ytXnswlB)>dG#3q#zxtiI4Mtq!W}P -I@oe>8P!c6%KM@rrmwFleA{uai=w6BQW);MC5^OK##2m3}te~9YQDlzO=_r1<8Bwb7W-uN!z6i*(NGC+2^JinQ;6W)b(CHQKU*yZt(E}+x_nrKsc -?dQE4T<(iTznYtpSFF5LU&YS01z`krO(sxa8jn@F#97lKwpl7HRCj6lfLb7k8vU2N`yLbA40ZzJB-X+ -rPcOEnmHU`QgnRX+@}mA)rN?wTg7}xE^F4Jm1+JU;tS^=SSM%2{V774aDJM85FruKkC0OES6Jek-;a}y96VYPxjy~p-&Gx?b? -^hU1sqHpT4`Y^Q?&|Vx$4d@tca9VbzH3ePFz||<;SRK?yAAqJq#~)Ykz3da(#TLw6TvKE+K;&dIzU#8$l{limakS){-tWLq%ur~!orEAi7S3?WguxV4|A% -cW(Ibo+-X2U~CVS90TmB#H-m-z2`t7&pI$ux(moq$hzMfqZ`3Fwa6>aRf0ButXA9Lr%!H+$O=U7Ld=N -kW+|4D`9Hh_RUa=jl}%_9*q9=6Rd%X9TZZS~B_2bGFQaJ6YdHhn@^1P5Wr*vXxrXT8D52#j+Y_Mc_4H -Ju0W6B%C_ssvzn6wDI>^uNL9wBWm}kU?#c2GNw?2ZVcsooIW2BmV_R0{|3Q8AO}4p=JPhwW}ZBU0q+p -357~vVL(AfKD4|NHA@@HrQ+Hoxf^rhBj2Cn^Qd;PjWjIhsLfa*Knk#@8o)4hYM-Ck>*x-Li- -8ntVZAd?UXG@)sW4U)-0kmxNJGzMw6?F8|TqT5F+B8-_&Sj7z>)JAOTvvBU&+q&mH8RIC{NdKte?%i= -7>q=%!z1^$!nw%VEjx!pqPfB-w2{{;}h$c{g-_J>5oorjX+oJT651lTRW&C3|TuA0ngU7Op$Ix5~UW| -mRusZ9Ov=ESAmr&|^EV91#bl5X-~;=GJHYZFUMe5$X2Th`!!Y`Rv0OxBqy$m`y08wCu)s1G=oKJt&)kZLF=2C^gG`9C -J!?A1@U7}$mKlwV?2f-iA`CjUh>PjUH_Grrqvj6TGFVCvHK#4Kiw8>h9;o#E}Wg=A4ya0=H&!`el(s5|s6$)Cx0$ --4z*50PB6BPwGb7fDJ9!y`8n7L&8_ft`xseP<5jpgC<8r4wKlDV2MF=(bt7|CiJI5F}kzU@}kL7dbMy -Glx%%z(Kgx=zA^D6bJ57gxN9u+qfH=_D7@$O1U?$_s -{I(->?x1Msthx(f|z9tDU7xSL`O6vh>dzUB@CFF7-N#poj4$iuKk$Rj9R$OUnQu?g}vnq&jGD-l -&M7kog7~Q)NWNcTU+V2o9pL5?~konI49RA9JW@0lQU~;AU4RR__8y>`;V4h!4l84A#u6wS%DKdz5(B?VNu}L{#dx8lfpRftC#YlQkkaIY60ium!Hk*JaYe< -OSjM)gi2X&Oudvd>qgHc0+7$iekdI5HLthU|18L^Dz1D^`WA$EzOtD#DlhZZnYVu{siOE+LuXL-DgGL -tg$VwC7Q@2~@1aHO9X7er+Ug$1gxbn8ANAJGvX5gS9+tSOZma;RO}m={%0U= -lLpQ8rLsmuRfZ!~RB292Q5W7XeNHeJ0#>IvaS0tOkgCT^P6NY_A1_qV%a0#(mp<6|s~_Ti?_v%`_EhP -LS$F@S7*Kajj_kOxztn+hkh7KLIE*u_ti)KTGBVamz$-P_&0bLfKLTCjy^UhqzA;1#8AZjMW7H_lf0e -XApWVH1QY-O00;p4c~(;Z00002000000000V0001RX>c!JX>N37a&BR4FL -iWjY;!MPUukY>bYEXCaCrj&P)h>@6aWAK2mt$eR#QrNIX~6`008#`000{R003}la4%nJZggdGZeeUMb -#!TLb1z?PZ)YxWd2N!xPUA2ThVOogQTCElsU;3vD(y-O6oiC|1X`|X+Dubx9XqleP23;XaZ+@pcLtgVHEHsFp3+jz_0(d@LvoO51mp+k4n4QEU!;i^$ -EFQ6kO*dgp|DY2m)$sp9~1e(MQbh;TNWpu~dg+~d7x9FZ?CYEIx33t6VZel$c0(7UI_AvhXtxhFrSjX -6pvl2k!JIcVnT`ouLE$G4ZgQJ%_bRUQc?$$Sd9tf?0$IbmSdt1NUJio5tCtO`K#-a&tF*<_f2{j&z6$ -4sXau(us35|EvbJj3s_gKq-#X?NVCG14w83Ihe;z>HbC&eU{Ta0-tjK$*b$8;=U` -3p8$VdV77pc{+=FTn{ZA31{DsPrXLkPANX)R|{Ly_mhMo94+AZm9aNW@h6qE7(6!O9KQH000080Q-4XQv-5B!j}R70D% -So03HAU0B~t=FJEbHbY*gGVQepTbZKmJFJW+SWNC79E^v9BR84Q&FbuuxR}k){#!?$w4+Dawz%D!Ww( -e8}24mB*iqt1iXcC4^LrbJ0e0^NBz(xw9`FL@npo%S!7p0Se5mBXUP -rr9sxiOA}%8Zvug&tQ>xp|C4}q+>tCLE(&*7|DvICkOIzP9|+aUSn5Lk*G+xE8SY-JQ$wec+aYIrUkz -ricO#IHG4H42$>`s1)5K7gI+kdgH*_nO|mJa3M!>Kxh%)LrcAzG%VCEtErGp@;pQ$pmkQ)Ji8{lR*MW -;L73_U&-0B-POz~7FYcV&RjVRNVy1J;h0B5ijVoTT<)4&QITu-N6T)__}_?3ROw$V8bGy2}!z%&)|3( -(~-1IbOfH*OEK6L~lp(Bgqw(w=fC(BqpF4t=erXFMd6N`{k=GSM9H;WZxHJQ6H?RY$#}%&Y5nH;sM@M -3PaAvH6WSgi7oiKla;%$Sg!=Q|r+SBAUCHH9S?^RpPh6?1!|EtB=RI@qo}&s~5iTRkf;h?zjT!C90%RiiQhRT -CnWmNYb-$4*wyE7)43keav#hH0xamKkI)N@$n$YQ05&MpcPB%F!R+gzV#@tU}V -Q0U#PRT-=Fc19E8B_r&lP9wbzH6)#=^MKEOS-3EUg=BwHL(@_AT#TWXr**b*Z=Vc8=7da^o`jo|}4Mg -bMA)o)Ns0|XQR000O8`*~JV4;ZV{dJ6VRSBVd -0kc8j@vd6eb-kEv=5d6N8V#XBNJcHQSLhyaozE;LRN-QFvpVfU>xZjXbW{d#10jd6B3;6i+?N4sUE2%U>Cp&{hbh6QCJ1yap?$Ce3e{ -*atEWN8ot(89eZ<#T{vec@9mbYnkKv^iAc3Kd~yOfv{V;;+G_Vju4^tj$`SHWxuc75RCU6Pi##R8}j9 -xRtOq~OZo{-1*vcWYvR4AS8JL7^eg7D^?AI#OjC!R4zt1E{TS&0;>UxUIW=nyY5s_AQ$el6l(+G5_`M --3@6V?~D^irr+J&zQ4V_3;v%3%dsG7aRX&1b0-p~EppR3wRH$qos^bSGIai7`L~mpckq{&-YkgI)T$E -&AQ5OgQcn++0(NBmfM%f?H{@fp4OM_$jR7=Jd^Vy3r6Ff>VquTOFC#K8x#C_q%vzl&L+aaP>&?f_YPH -$L(=#t+K^jyhRV_--{><$*+4I9@z4{FQh23a5J=<}Cu42KuG?v+U$f!K@F9Mr!=wKptS|OhYZ9=gN^8OhN0RC_{lNC>fLHcc -)=3$Eb8)|4>Et)qaUnbnLl?SCU7)c|;^}3Nj$o)iZzz}il~w2`sjU&hNDDe{SRYv7k{}D3q^>O*w5(v -3g&GZ7cft2a{&J$06VKF5lfev&NeZF2C<09?%X8lqxi=+o7idv8T$0hA(F~*RB3Eu=r$Xf+Y|{2`-!E -QhCR2!#`+@4`3JuOLlaTi0>&whx>fipdxV{*c*@0=3OjgT39v-)!CevxaP$JW(hXUvhACR)1yfy3}Gb2izJ|Pe0z -Ja(_GHj#afuR6|E*&#=deqi9IVCaqjHhmLB$rp<{vP67@Ys7K&AAuIf0UNQ@jp;Y0|XQR000O8`*~JV -wzP?{a|Qqa0TloMDF6TfaA|NaUukZ1WpZv|Y%g_mX>4;ZV{dJ6VRUI?X>4h9d0%v4XLBxad9_$=Z`(E -y{_bCKQ$9>W9VJfNv^C%k+b|3ThPBv=een#Hk!YKZBnl#B#|ZM@cSq`lw4Jwg%LhlM$cJ~&J$F1h2!d -aE$!n%Lurh@(EsDyoR4;Q86v9L@x9WKC_jIM?nybMxv->t)b?lWi1QPyGoQRXh(k&N{ -``VrG+e6K8DXtmGhCES&1r6HoVMa9ak*9W-DA6Yeeo=has75OGoXGD~m*Hz8&GbvfY4@Wodh6PKLDrb5r>jDv?(I}DXCSi5DPS60Dq5m-%BXSJow={whTo7OV;pXY3g2u=1F-8CpM9(xGE?Ok$v7sSLH)1eJF1!JD`GHk6%f(WLI$XiJ -K83iD?uR|yDo>qqS3-iQt#Rs9CSTu|RrgRCJ$ja|^E8Fmg?~W;jo)&KE*jHV4v|JXf+~uNdZl0Pd$2P -DTV?u7}NffWTr)Eh?n3-=_A8ReBh=u{u)@7Bm9%eI&c5Bqd`Q%mLve3VooVkX~+Gx9NB3;?M>Rg?k%_%eR+U4T77u -m&xU;;BM-P+)5#46;Ty>Q9BRdMX!YZ(I7`p!k9b;!1G6hbAFF-9+S#>CzW0x4$L#9@@DdoR2~us9Fx) -WCGlqRR7p(s_3*DFbF|#7xb8~MGYZYO7M0kZb>wMRt%GQKSS*k93iqfC(_+3?pT_&TTO8Zrk$M{)Df& -;Y<@~0?W;bDZF1I{$ko%`;-@DFxnr$<7WPI9DSV|28L~mDX!|$@h7MKPU=hVh1uES0Os>!LFoGinXhTYBP*YlZTwYZ~*r{=&#b5ML^et|x46`%rVI#*!@*u`Wht%&(a -HC)+xe{+X_V$49df`Fi-W{OlnVN}qY6S0LazHipgzbd+`J!zSyMam9eE8~VmxlXmqa!Kl+!yR!1urf+ -r+$fPV}Gv_5(tt{vG05DjkZo)pfyWaT#V`!?6+<`)`KrMq?f5*GqcHUj -kazqrVrlLz6&nzl?-?Yid~6EP=?Zpm&rDD@jm-UTPLu8#`6sitKJPTH&aR-&aivz2vZ -eL#ZusVQ5ujh?HlzawxXY6on^&90Hfg2vDde=xQ*$g>6Q_^d>K%y9lX2<#(j8jfXt5JGtTR_-ikb@(~ -l{IJ-X@UTWdi!HT?L~huAfsqj%ccymP$lsfV3xV9QDk8u-Z7}fX4sZtr0Qqgk=kUC#LBjJ*V3HRnXC|SN{4!ahTIovHiUEA?ZN9 -zudM(+Vx;|Z@D8{`%2cX98@%W%Hi7Wr;_#;8*`;cTq60fhDD}TO{<`%^_D -a|Lgc*I_YIbABV4c7iZ3oM(%{+z&v&YJ?w=0W7eS)4sSE(`TsTleuMXZp!4m02H{U~>p{D%rG6vQ+dc -XRP)h>@6aWAK2mt$eR#R*fJ+7Dl008m;0018V003}la4%nJZggdGZeeUMb#!TLb1!6JbY*mDZDlTSd0 -mdd4uUWcMDP0*lb#^aegKIa{S8uKwT;k{vP$@S%St@Br^)QwnKi~-Q^x8!Vh%G_7iEDY^q%`C#4`pbj -KWBm*pe}ZC`@z8qMO|%qJi(_YH(W@mToM5?!>!TZR~P`5aom^Me&C&psE_@7PpkfhEPTmaQOw>U08Li -T8T$^mrwr)Z8-`wyC#J*%PYqtwf)}G2T)4`1QY-O00;p4c~(=c!JX>N3 -7a&BR4FLiWjY;!MUWpHw3V_|e@Z*DGdd97F5ZW}icec!JbY#u5V@*3!)Pz5kzw>6N)LE<7Uf?z@IjwC -h|xh1)B1VjJc8FDX_EEO$KzepnIdS=cHo#%OW16xzK3c+uL2f1w&Zc%#&-WVnInmz%wSds(^w}&TTH6 -1$;OVfI&v9VYQFO7CZmXpDZY#8f7nf00k`=7mDwz6gme&4X|?(RnKXYkOl5us-Ah~NDE1AXoroWXCJj -zqOL_K(0I%TP!{gl?JUYt05x&|GCencx -TP(8d_U`?MyEiwF_Yd#NUqAj_(yyQHe}@I+$;})HfUHu&Ie0Id+Yy~lG5<;TIh|)fe+>zeLTRj|RD>$ -s#yoXU%^4T6|ITPiCwm2-dgy<=dJS(Qyl}5Qu5ECU)wqJx!X)_EqH|)6^N8f<&dLm&w_j#Kf+EDVvHN -*|yqD-MS5hHEFlU8$M16tU%t2~D%FKrfevSqF(#1aNBqHp5xSs+g#9t#Qauo~$V{d$N(OsTJ>%27oof -V&SsLY1sXG5m5F2Q&be@%l)RODVd*DgbeC!_A!Vo%3FsCo#kLlEE^tfWwCA3CcJL_rHr8%Z7aB$a}V& -^6^xGGJEJgqWB~2-jEE!OGacjX|!nx#(F~tkJ&>XOldTh)Rc+Fey?3=hG7d#R*zH3NC^8e}Xp)-7B^8 -ly=#fDPtIulLmPOcX1+_)px-Pn^PqsYcE`r%!G~t9*W;W-|~?Llb84Xj=}8Ev=PV3UxlytZ&( -iU3uJ*=fxIDw2AFr>D(ph9LEs=?8}LDB>(r3eU@go(FuhuHcC`s)Ss|2-mfWWYLc^eD-^!9Sgw>e+5(J#F@Jup -ikZ(z(IX5>Y2m7VvRnT2<3I@wYPEP86IY@K9G+yg>9tNUo(mcA-Zvhei8Ws6b0EcO&PI!z8UekxH{_a -a5|&DH-n98qE*hfwiG2aVBXO^P=fM@o&wJ#*x(H82KQiBSrFt*3yiT!P(kEh!fuC6HIt4?$AoSHa<`Nw-ZQyPEWwmf>>Iw@JDuV=(+&nw&g`rto^v-E?~0DD -38XNq6*0TEOQa^GT!~P4)cw{9Ebp{Cqg%)7hwUw5K~qv!g@aR`7g?dl5B3U13afi5sz#ya6iexHT5}k -s`G!r`hFybjAA%@{f|J3(@>6nhem;>eIYCUEA=xk=UP)AzchUVgE@2H_#X=J+G@oh$UWoc3Du2*Ui2 -W*HoVCV9H%TA6|m><)|t7Ee?6{r;&OLhKGmnB`v2Sex2-uYH^whKscHuvP`Cc!JX>N37a&BR4FLiWjY;!MUX>w&_bYFFHY+q<)Y;a|Ab1rasw -N_1!q&5(}_ph)RDFF>ZIqaz=RvT@!(nwKqNV1pJ3b~;eoB^BIraP-v|MyfGFoy1)Rg{Dac2S?tRnOE~ -by@_bm);5`dAr-y^syIxRtRZ9qb%!G1+B>{p`oF6b=O8R;g57Cu|`q?<#RCXupze&~^7SUPt#@qQ1(b;ilQI%JBeZyzfC)+Z3G1oUDPx -m>WZ_@LKZIXe9q2?{&^*IE*``zbuijQjS8sJi-RK<0F}4co+A(;DP)^-$dCTO32t7BvV(~LCetQdhoD -$3aEtThZbU^23-P;;4)c$hW4RPFJ0OWz2yN51kRFlIc-EbpAe7dxcP5-fJrI2ks{JbL98^!qSf9pQ_8 -}GXTto1nSaJX`&mgyopvD%GEs4A0NSGDWrM_VP^v|x4;fmoIpvXpg(>8S -E@^a2gEILF}x@b+YZpz)6T->dp#`g$fDqh+V5>%8D`0Y~bP%E~rs(jL8Svtf$|FCY6)0jT&q*H^yeL> -3Luulx82_|LUxeB~HL3Kt+^U3oH`t8HQoJs+ebpES%pv7_U-#U{5uYCoC2EnCMk&FujtM8Hw82R~vih -I%UGL4!ATMXER*v#F3ok##VIQ;96~}fDvD$OrZ~b5(dcYNeykqq@s^U*f=-<$SJLiJxL7CE$-10;i{D -UcC^CWmzLb-k7wbMrn=El`GUx1E2XJBuA5+`Z0xfGrkv1B8aVu9$OY?w+ -S=P2dH@{YFVnCl&TufYBY_AvW8YnGp(^DInH+yhaak8_nlR=(udUsM -qTBVDvo9fPMG_pIH$862{{H65$rFuKBMacbATC; -5pw7|5X9u4A9U|7sEc!JX>N37a&BR4FLiWjY;!MUX>)XSbZKmJUtw}*b1rasg;Pz -7+b|Hl`&SIkp$Rzdp{3A33Jd+Xl+r_?hY*Y+k8QP%Bu3-ixTXKSBRk$CyCJKCEX}+bzj-sN$nBvtfjc -&B$dSlV0JHaw7(Pz+JE}iLPf}gND`jFbKIPP$!B~wXyWS|Au?zD29ODnKy677w~0+&={r6Ug|YerfpGsa2wOB8deAvTeHI&1TC5tp)->9r3TNAz3(j+$py5B?r;sxTUq -&~>AW;rsF&}Aa+oBt?TqzcTzs<_-`v3^jWsR!L&pJ(%O6LCWZ3LvSCn)vT3RcVsRVDkXzirOvJ=%-tk -Er?cW6sqJLlYcsa&y>*7ri8-bIWo0s7fqYYL<6bxjyYt3}v7ewW&mYh_kKEs{J*%2Fs*mhcj8gJFv6b -*ZG@QdE9Zau_9L8otuSgC;RCGPz5Vq{ary$xMqDGb{GtB~YEi@)bX?2f#E9&o@TzkP;GbLx#|z7q##L -c0%jW#~+N@QrFlSmyJ{y7CUS1vGPZ9_d;!1+;mvMhs_h4RhT67K* -{r99Pl}Ml6T8(7%_(cPw@!KjOX1`(`J*~Vt!;kdCF$;kdD*IqG5i7KdD9BJ+04;(fBakQQ`kb_oo4xW -yw&11pfGqdkUYVgg>P7GzAP<*u8XA0*a+j#+K=6Ye(>G&zn3#4_arY43> -di)SDR{NgIWS?cF_Lzudz=5~D0KJ56BV-XZG6o{~&2drs$PX&>Vht$T@u$M}~eeZk}Q@CTKr3I=Wm7T -`&ymvS$$7A;{83p{%1kX++9J~q|d8q4b#?`DBSP$n$ewGI+rfX*28#D09W&lRI4zEv-cm&4Z5_sH9VU -VaCH=8BrMhRQ}-xT@d5IH+TegRNR0|XQR000O8`*~JVst49#4gvrGkput$9{>OVaA|NaUukZ1WpZv|Y -%g_mX>4;ZWo~0{WNB_^E^v8`l;3OHFc8Pz{Z}07i?t=DNgxzM_RuaJgRBi*_vDx$+j1i6NJf$g`R{j< -9XDYyOi%XR=YD;4wm8&ETgX+xa}$X6tx`Fw`1wuuPv&HTQmX^lQ!V5UI`c{xJA(J7#+cyo_1Ev%n-Xt -HvXkXz1jgz#g#{!5;0fD;5z^Z~@6Qh-AdM}@4}^|x`6u%Zn9K)>?c=hC#u*>xRu^0~#LcE1G@A|*pA~ -1*;flzuF1WU08U)Lir`PX4Uw&-gmMDwnQLYZPsCbxZf*DZXBwnN&^Ce8in`4xIrGy4SQ1B91W7QUGV4 -bjFmc`&jrZE2IbdG%+gpj8_&p&{*UgvR_Rw|7qY!0l#d)J!hwmLzI -J&-1ia@W^y429g_97lsAAr!GA0V%8LIGhu|7B?5qd6Z-)kd0Z>Z=1QY-O00;p4c~(>5CJmEA0ssIX1O -Nac0001RX>c!JX>N37a&BR4FLiWjY;!MVZgg^aaBpdDbaO6nd0kUYZ`&{oz57=XxwHXN=hQ<3Y(TfeI -$+3xA;1n@Q7ASY6O$!@l%4tCkCKu$TL344Og_F3iiY4hg3MBN><5T*Aa?{R$KOce3ciO(-Wgk!l0cz; -B^QzPtZppTgCTcmN&l;=YO#aY6Ppl_Zw()1^9J*rP@g68%L{yft#`PDyN{rVn+o)^SS&uHi)<{M0ig2 -?##->Uh4g9;UdyGfa>JA2d8S4y=EM$qBl@%;IAMahY5^ri4%-?&VPi%?@EwrIh?21klOzMO(%s|!X_d -UGgNgkLhS5d}7GXEX-aA=A?2#C<8Kz0{^vt*x1}`z=DZ17SN@q&2Ci5dFQORPv0%gDgGIpKHOmt_6G@ -yc9v4$hYLT~Vsaxb#?K6!!@nTR@s1cG{B<_Go@wF(0RHob8qLpJeb*d-oitX5{EmKc@2o@cJnr}64M` -@k7JSkqtz=+TB1oDiHpoxLo{bVcsS|Azz`#yLmH5IO^zky&d%!>=!)=ig+u#1@(B6H~<7e3~nMKK|H# --G12=3k43CH`Pu4H@0w?6X`Tt8m160Gi%r@6aWAK2mt$eR# -V42Ti@ph000FS001EX003}la4%nJZggdGZeeUMb#!TLb1!CTY-MwKb97~GE^v93SZ$BnxDo#DU%@&bB -Hua6X471*3%KZAnxKn2WDnT{2@nK2TB2-bWl<%m_4?ZW_dYYEUi`9~c0VK*H8UK}JTvo9dea!|MOkm8 -&({+j9*rA*rH%Cc3oGlwY`Q16ZoRJhCog^fd*>#lH5-we+N(QX>7|IT;>t$Wa;0pL@@mtRx>B9YTe(r -idqoe@?%v={l-o7BUUgDG)w{X)S=SJ`yqb$kudMXO&c(0V`MLN|Yg5lfE}fqQ^+q>ocB^m%n|f8tT>D -*T%qnkT&Uw>hvV^IuP?f~WR%)M>`c4&ND;kHewX9N#fJ<|;L6|yQE9EnaTGr~~ew~eWd8bhtiv(m&|D -h(6Nuf%SuT_2%te9=BY(hUT;Gdm|Cw~=7W2%dZz~3WrS&FR^g{dEVv6i<=NI1O{-UK7Uua!_`sdBIJ5 -IjUc_E!^HwrItH0%IB*jAw}Y#F;tSw)wW?zE|-r(=l4LQJ>1ettd` -#){gIQ%-ts6N8+l2rgdT=-kMtR+ICFEBVS;gv({uQn*jM!?wHmw&={vparuF@lI!2Q@i4s -Jv)f;$FsjQ^puy31AL#nts|YmGf^G3NJ;teiR$h%bj-5x6}e1+8PdQ4KMdIboRHo73~h?CO`n&tvTl8 -uO6hh3GXI96)!C_A@%}*nNt6EQ`M8y1L*-8LQVcyYtXB5@&O++qo!|^Fs*}a)W2n>GY$OP3)&BD+5AH -q;G>|MUi!ElK|sPexuMb0CxwNu!V>!>Xe)Z_=wD4(W@d2HFK@d7%B%?dEdB;r%#vd3P;`nCUt7;>Z!) -D)bp1w&(6}$tH}{i_FTlt!l6Q(?t=m$yd-3Q?iIf5r{p9Swada$Gae0#VIe8^ari9^KEkL7QKVLcC|D -(noiI6}?jL*<0R$KnZ7k_r;-U?WhT1cF@FLa%B=O8&Uc8o=d$z%%;C{(xLlzFRo| -Mzy|5g6ycXD1voJ?cO?3E)zhhW|}SR(9ctf$x6<@NQ|t6$K<(~4F?d8E!~8&fFkG5OoUV5d!F@m1#Nm -zm!*e5hfwx%lq8o2|4fcR_=k_wFY6=WeKf>FeWNa7HrhpH%t=<7oL6_!f=9T&21|$CLsav52VQ3N3L> -hY-9+jRuy1VgdwE8^S4IKvg?VCe~H$@LZ7QQgQ1CQJ|+nyPGhZh#YXe#{(l!5`)$u`=sC1TR0s0FsA8 -JF${v;j_jnzEQe<{tXflr(!(x1%JIMN1j89RyO<3%)p(3k7=`HlCpc8n0|C?qgKEJYZ9c4OyExee2NC -p6G__u8iTQI%%(W$nO;e;}IxKe?4=XcjQstNhxVG}Bb7E}C!0SfEe$*#+T2)2V9QqAZtLi{9$fRY}x^ -=SOK5&}S?dm+~WDu*`Br`(wBk{f?id;QTyd$C*nk)zbP%fb}r*2zm$W>90b7rhwku$MFfsalytH6|Ul -`-MOY59L_%~}iMK!JrW4ykDJY!=w)VxXkSS$h709F10CZ+`gtUb6 -MgzD^W8YNT&tZfCMhBEF#)o~3cC)fdZHO9C8Pb>u{y=CW+L{;*I4}eeB-EHnxj!83dT%oVw0mME2L?L -kXR+=+P?82(FcYR0zg=Blegez2TE)_5=EV~fuRm&B3AS#(h?rOJ(OrytlfzNhF9kI`-ym)|!&>s`iAQ_m;KFpUm+zs){7w11lO -T$Qi%!Ise*59yj{_N}}o9H?s0yqsm@nJ&4-6&M8TO;}ykS@z{$C9?l0^VfRtmRU9)?DTY#A>a--c*~~ -H2<{Dx3}B7-5+Pq&Y%DJ#Sbrk{7cXxZj}+Y!p(f8l4oZlL`6S(m%azK)2|qpNw~(R`P!fwlWynPp|u} -ovr$uJ4%UH_&4e_Q?FU+PSp+p2R0&yOU|ePI{{Hcu_;>jI>fP&~-gghLKfU^O#NPwAV~)eOwi6t0k77 -lI-;~7#+o9#2P(W9w>u=5_f+4z8xoktvwu-vhsObA%=O~7B*z~~G&s0+$YHPh(qyM)x@snzI(p-+wL_ -7dJVt!x;O*34+;{}6lj$G>spPF`{r=#9?@9{8Xw<>UjhiwrlCR>MY6)p3U+SE@Pt8bBr6%N3+)qwEyV -9=hp>>YDnq?+4iQWi(km;zYeFj!`%Y!4p_;z`}O+2Nppk%g8Prg6L*B-0OdcP}EQRcIQ@Jcqiwcz@mR -b2jZ76E%FMv2M&`@sqym46TViKYhl-A$uhtRCq_9U*yu6s`cU0=x8NOqb1lJ_1{ohme{G&w*b?8V0O$ -;fJ3_Z>wGQv(;X-E!`sVGPn_RXbVZ<~uPU?b989~(7HH1J;3o?7^OwGz(8q|m$mY=_&5eC4tDD1##rr -Dy2opaa5_7^%oH*A9W;U7=%&)+iL6eicRuVQXXTxPNTHm{ZN21qd1H*y7m>o<8{_gh~mjc+UjO^~3Jk -PnLa5dU(2x0Ti?pgmOYZ%cC!GjZb;$xpW4b0fynI16Vv)+B4;{Uy?*61r>vad7iEcT%MKTb}PNYYt)c -9>&WbKlN0rzB#I#902I$&SOEY4%mM%aAOHXWaA|NaUukZ1WpZv|Y%g_mX>4;ZW@&6?ba`-Pb1rasjZ#r-!Y~ki&#yT -0WSI-hM_~guDhk65E!$9}Mol%4CM8Ly>#yI{T5RWo%S&>X@4oxK<0z#=sf7Q@1W272n{qFWW`t5oNMc -P2_$T!aWSSZ4A<8o)&Oe#VSS+;{R&&L2FO_4dbekIMG9|q@dO|)&VfY${Ur{)jjo&8l2$UW6ijwDf?~ -rkH3=ix9FE2}l=nyV=Wc5hu6+9nDkG2UHfo{S&N> -QT=Kdlum`ut%k_vxFZRyO6Up+fWyXUC%3|iEpUuAoH?Q7WaSQ(9Jm4~yxxXWt6=f4WXc?b?F1(b5|=Q -k;o5bjq&)T_g*4(JS5A;$P)h>@6aWAK2mt$eR#Q^tJykCS001To000~S003}la4%nJZggdGZeeUMb#! -TLb1!FXX<}n8aCxm(-*4MC5PtVxaZmCO-52D#pq3W79mds9dvZS1o=C!)#B{3|jnp3L;zKe#ebL?!Vc{Vwrx -^Me)GFFSi&QxoKYM8wEok_zd6H&KHTT*8KWO_5Hw5rMWtqU8LZ-QS+pSo7UaZ>V%wo>H9=ALVVj~f@F -*~SUYem&TK#p0jm6T)Bgkt3zX3|rRm@nfae}O&A?57ddY#dgof^ -WCDU#e6lH!ibKA>6U*pO>=#~Zi*S{N5N1A%~!Ak6Y@?;>>ubO{78mNg~QC9rahO$D%4GFvHKQ?1HQ^7 -F8~v$i$o^Z8cTU6&*KdfxVSrY{^wHV^Qx$pDy03K$CTrMssLKB>>q9LCV9UN!;n$`W|$8m|{=<;^+;rjDtef9Qo^X2`= -+pC-FC~}fzt>vnlx}Xr!xtvIVKI4 -O1D@4KBKbyP_e@BZOHMJYz#>-Cy@O1$EPJt9;XQ2XhfNV0=cS&2kjR`BcmJ&^0=J{d^Ro+zGgypEc;! -F%N*$*`;A+6WMB~J6Jptv>RLEQQRN&W8DOJmx`1DSZsCLhn5?j!->R?lK!Nai<8n7GD>!KUtW3|3V%v0^Ew`KTIl}W9T6WBEd~{FAKc@L@ -U*}35F9=@h%*@d!C}`HocMY0%MIR`NoXJdT(tgou -*K5fN~95Q4`MknOXD;kJ`ZgH!uTxlF-W3?Zb~7~U9x&Kk}F5CPI;^ghEN0&D8t`+x$-`XKDCv(6_c@F -4p8CmKe{Dm$ZDLDL`vd*ffgJ2Z0qEWJ2MrXC@l`uNk7rofmcPhn59SN{X-8wZQ!x82aLR(E!X;~DKjm -L<(*`z~;cZY6%lbAdkp;1nG5Wwn|umx+Jyu3it^jBb#H?HUa26mz{`7}|uJLCsm?G>RQ$g6~*|&S93h -0)JQe!SVsJw&e|XR{-1@dul~Xud`*g@}kFarh@BmHSjkJEW10SFp{&<-y&|6?3|*XlcOM3owN@szOPy -8+`SGtmJOlzG6XayVkYw==CQc0$^BjO>z>H_i4sMhaL0n^l^~d|u~EO{C)dV-z#k8NYaNnHY-nIZF{} -?mI|+S`3{W!buzB6ues4W=8g5+746&qAcU_Cyn2;vhXEjK{cUpM-3cPRKr`B$0K$e~juQ-Y@UZQYcz` -gv4g-3$f6U1cl4^T@31QY-O00;p4c~(;qP-p$w1^@ud5&!@l0001RX>c!JX>N37a&BR4FLiWjY;!MYV -RL9@b1raswODO$+cpsX?q5NuA1VWifVFEec*w9U%^IU?Qlwdj!3bnprffEnsFGA%qv(I%-I02p2y;IDk>{gX&TH+*zT<~1-I!oYTgWN3V5ZlJkL4KztCFgF~r)++{A@6X@j -T*Unl9dF7M4`BC|g0*@VF#;dn!vWeM&*q0dj)mAVnO)+K{|v|87xSi>WG9>0vAeeYMmkSg;%%alex36 -^d}dXMh1iWMAI!at7(oLQ=h3T#=)T-rU`NxNk#UklADw4FT&ZOAbyWAFBMt7y9e-F_$=MsH-qJ;rP@{)!fv3T%1; -Or1wJT3HO0C%g&ud{O%^vPyEi#sX@TE%|6#cdcYky=1y6Ga6M31uu|pg<1q0GUdpGLv8^s2ePuM)-ddM_02jE!|!Y-35T05+57ryfJ1Bdh -2*Lj*U(xn6AOsIIxpOxN`0JQMOi47{FRo(@J4ICYENLW+`@k(YA{8>XLr*RsrMMl$2a9re$9;LlLJ6v -#u7s55T06;o^a4JRnpUtKC~!nxv_L3n-y_7J%7d5t_1 -LwMEc}2XqSi2Cx6p#o4zLfn1&sF~1GZ@roLcqnmw3*GI -Zf@+Ji!25ay?E^Ho%HOK3b81|nv)y+)Sl^tDcnR_Pk?(CUJ{`A5E=VI>{)@ -(F%u9y`?)2zD?-He)O8)#v#5rM)MGyt?M`)$PK_2>saxhM-W`0#j{f0~Q>99w|zqTv!CpR|h!}{z?$) -gB|qpt%qnB8B0c$>DjC4ZUsjYcmp?((GcJ*Qm6U41KB8*o0}PuWg9g;uO^AdtFLU&%sn@uwQvRE(nw% -iPkPw6@gUK?9dsXzAS&-|BYO&{d3T5|oDS?+%33{!Rh6MYW@Qx5b+ef&e=m|JB`Wyehq-d$h_j-;*qt -E>D(AhL`G1FJGRpcj!S?YnJf`yo`zVd;uyOxV~$=wR09g<+Q@dEr{7~SR4SpgXB7SjA2Cgc?pfWyZ!q -gUW@{VFoCC{`|7VriD02>0SS4&=P(9e+~|e5S>el-i~+O^KUx&15ir?1QY-O00; -p4c~(=`e-Wdi0RR9S0{{Rm0001RX>c!JX>N37a&BR4FLiWjY;!MZZfa#?bYF92V|8+6baG*Cb8v5RbS -`jtjge7n+b|Hv-}NbO?n?rhaJIKW3){$`(AlF-_i7aTJZqJ8-bqfHZ@;taWNm3-!iGDY^#A|vlqAV#X -wmh^&`~`&gxH!0*8j#T1}Lx^7`JSE=!xSB$b;?1P%E`G695gn;~4Z5g55b>K_oyQ -L=TK}!1!mtARfloDxe%9A7DWY2O+>c@)C@ktr#V(!8B1IAHl5u^%6~ZGKw?)0R>d-YmyvKmxNuu&Qy7 -^5)<@O(OG{l@_CQGC~m+8;Uq=UjqtVtCo|dJ6#KRQpjDD2N}YN>2BlPu&8%OBi71|k7E5@41)0p_lLA ->6kdI7^4)?~#Gs{%8&8Vk)XJSL#!MjUHYQqQXlgHcR67hk(n)1lUe}xlKaMKn(RM7|sdVOXJPUk;1nH0*Oo7>_#&&urX`_*g3oVs -4Mc3?e5&f^C>=UXz`?@739SfEMND2A*1IvvOZEdJ1>^4;ZY;R|0X>MmOaCyxd{cqdG^>_Uh2O37nl%|_>7&@UY<|Il -gz_A0_YgPybL6Ilx8j8p9NXuH0|9$V>7mpNayK6U80b=U-?%n(TIUOSA1(giC%xl@|sraRlP5rH}k8IoEQ6$yh-vrS>-e&a{x_hUWMeUtQ -jwoJR~63w2svyY3}oAaCb)n)wQ`h5)F-p&6xj$+G1YWC7XG!>0AR!t^#wVzH1eDu3|Q_&ju=T)*zCP(Cg*Mjgh$=G^ -LD_Tff;gtYw6acrS3Q4_TA|$Lx^F|(ey%#mzN=gaX4Ipx8j|@LE`3EGa$T7!~Gjhobq!e`;i#n+(tXY -<#oTo|d2NTh(%8I8{2m}Nz1C-Uc?;&R`2(P>VP^Nhz1bJQY&$PI%Gu}vWz$!`eElGi*$@4tqk~n#U4-72IR4Z$9hAFkT~HF5W6M24a=!@A&YS`$>^HIOvEdREe29)EG6rMY+^~|RNDRWbt0&x> -NX_seo8uv0Xg}BtT@lLQLKr@L73)KJWqlPJ! -m<7tI!&3_RqFypk$0A#I7)eA~6Ba}$M$#)hVnZpe=1Imo&ZR9X|pG%%+UEUM;YMfG`LiEHUudx;F8wq -W9bM-aMYH39(pEQ1Yo)H(QwypxuSzH}mzHLVqn*3}|)h6-`gp<-tR-0l(Zug?gGK|&kBthQxDrc_I0xjR14`0=yE$v4wpQFpQhp6&(YBf~3joxMo`# -ujNaSi+B9<$?^8&IJ-IiaeDk=dVK9o3^<8_Tvo@iZ`l+yUx)VFi=?^(2ge*}*ODxgM*!*Daw;7fj**t -_Y<%%~*V`+FfhN|<)F@k7+5unauaLl3bT6cpIYu1x?J}BJOw%%f@Z6{KTu}-*7^cO-8f?xAFh__PO`f -6YLf%}n2bx)Y_}Q^~gEX)T)R^)r`wB=h$fH4qP?AxH2oO`}dkfl>pw}R>>X32|trUY+V`yU9htPPc+L -sz5Yb~^FiZMMTIlZTO!KJ55$;0Zf;1|j)RZ>W>Vp_{D7NhI8#K-WK@tfZ1PYXHF -f1U5m6}TOy;c6F^Tp--^8K_UeeF5jy_UX6k}Xh0{&0;GbkQA=n+;VQ8@7@h1I?anAZH|+m57i}lw`d4 -swTSx0w%-g*29!@{u!A3Oo?YmcMGj4iQwZs&CQ`CW{btuVptCLB(Jr4^G4GpJY#ENI -6~5F8!y(0?pOBb-RCTdC -@PN75>s}m)ekNT0TIb;W$>Avs!A)$LcG%4f^KhaIb1bSscf^Dk=RJ@>$4$B(2MIQOhN)K#7R|+tfvHP -f0VM6=l1nW02Ny6bm^MwXe?S_HxVQ~EWtTq8rr9^Rjs2Tu*Rmk~Q8ke^ -)pk}Pn&=Yct&9z;hC$}QdXpeYX!(jNRQk?DkWF$=+m1z>g!$g`_~z>3YH24%VX%Hop^N!j9Ig03i*d{N4sZIJ{X$p~trWJiEBK`s8A`l}{9kg(04A2TB -taw^XAtwO#j8HQl$m2@9d@R%FP{0wC9-G=HehY~#n!M;jj!{JAyFpehUr&-d8)8z~ffS$@>3hK3AX -xcNlRt$;p*`6#y(h-KcS-Y%y6=~1KgIk^s(qNCoJ|v1+ReVadV^TIQp6c*`1BBwPb!^$}bv&Ekcmh%% -hme3Toq|F4hAw2+)&`Tk(=w{nB#*5){V|K{tm}iFR?~qD2Til90d!XvyHSD#3MP)o=4)LjSOXz-y+?u -RB3n5Yt6j-J;U&8whd*qKaB=UcZrFJhsTY1QD6)1^U%oej^yAkYeQ|M|ez0o)|yn~j}fkuA@f9HgB<_o%|58l44lMg$2oZA8ttnDiqnX&3N0;_l`aJ -I|8iFBCn*9b9C^n)|_W`s%i48=cve9MM$}+tfHNg>nfQ*;TBjKsHEyoi}2Gk42~TI_iz{KslORXj667 -65+tb4?^5nkP7F7vi=q&O<9sVxTqP}5y8=3QCvzc*Z~3VZNhN2fzbq-Caiai%)3Onyiqe_m5ni)!6e_ -X1%6`XQ@JCEp9Wk`mqS#_9jqMyS59Cz$yZ%uzM4t?(&W_g)C;FSPXBl$i)+d#Uk$c^*PlN-H@51zglA>#-;7j!i*GZ+Nk0cVNOoyE}*y`>oEHL0$&R9jYEyBte{+5@&aoKchbh4kmlQT|f!SWkgur>mDu}EWM+ -(2r_&aoIqKa$sm+G_fHT5%*<#*|mI2zm%xCj}N`BOXthmNy{bVU*r!M(Xsd2Xj~1`-RD}#u4&YMPMCx)EJ(aDhq+(PkshV|3vgJ`9<=NpWRwhdijl~P~7}UE -}MYj=%KJ-TpNeOqQz0L*|eC^bBOV8LZycn>4rLUzbleHHcVp -Ev~pmIrTl@MCAt@tGC5R(Mnq)o}o?(E9Tl<~&&feB<;U&spp?cHD*8P@7;_XPP;=lpn=R0U222B9RRA -3UXqZY|}7!+4~fd((a_KZO2rxa2|htSgXRTys}HPY`V{>MBdrY1K_1yo4 -_Ah;lF}ETkMx!S5Pq21)={rKZIt_(!?CXn(KnyTCByfG3iq~fCjl}<(FbUkb6M;QAMET8vH$iGx>?Uf -BvS(dOR)@ndGF$^pFMKae$i5x3FkSn?R`WOlX=A^Whn6PyjG=FX)N;s+!huLNipr=JHK$GWdDhg+1y9 -P^=I=kBtT9{vm$LrD<$47DrkUkK053tx42v&0k26CTF6&sW0s9M^0bKewa8M9+es88r<7ix~&k4$Zau -0g0R!7q0AMY`?5PI5IRQdW_Whx86M_8sh^9?iwBb#WEa{Of@($v)Jlkdi8%4nYKM$z4vRBJt?y>FMd|*Yxm-xLirGDD%ABWW`d<%k^ -3ot!P$dm#1PTMJ?CmtxQD%56iU_Rhhy2tPm|W?AorD(ZLh3-d5X*XxciPw?}2wW@VA&)7MGf$b*AyU6 -pkU$ct9qwRtwvPh?#erM_9pwn~~t->#BoWr1b}E-#l$pjF?NrbSgY*iEB&BZb#0cLyEbXf_13iN> -KwX>s-y-|7j?OYu7rB`5yn$2FW#x=t*qO!%p3iBk>&7cfrAW1E8*kcSrzAHv6M~AZQ5-m4g6)D%;hYZ --(12|^B(!G0Q>_1f8P|_kS@N>ilp9NyyLs?k~~jlxm0(|%zq|DvXnJHy8vo>2)b)a_3lm9w0!kW%?{s -P0utTBWmoCONMV2VW1_Ik6T}i6F!5DZMe(gH(z1@Kn`K(GiP -He&GF~UYmUX<&3iwq00ihreaRt)}Gth*hYk*>WHii`iv58=LfqZJjKV7{2{@qU>_k+vjGS0K&MmNy+F -$#H%&0-{RNpVkdY!buq<gTn-eQN -)Z&S&6w%7t&G+0Fb0^^-01sLgDDUxEVXwww&WI7};023~2prhglJC6)C@4(HdHyIjOB_X ->FyJEgdz~-jH&mtKS`9b8cJ|hB#6B&rF#j{B-IqqBW#Z>su#L;NWKs*)xNyxG;^L_YVK8Alfd~!8;cI -~VNG`GHIX$4qf!#*PiPXd^ZmNI}<%^Yj0@_;?v4ruco^Y!{l -NJ;2K9VIBNh4(6~ltFy9e#QEEIm*@X{fB7@|e`Vc7;v>k!lP{m2ec4Yua8yR* -ABsOs*-?y`NRYR5!gIRt?h%>1lk=`coA)4&$Z~rIT7pA4+7^4WneTtovVSFM5<)Ua>hc1t`yE|dbdZB>A@r -!<190F!pz|dph}ITXb-oad1b;n+*q|0&h1%emdei{)rFPwjzYVu$L6~g?FgxjT(1G#}8~}jJ6(9u-Wu -1XlO>$v;vx8UU`uVjasOe?hN%jM$Lub%`7O+1!?VUpZBzh49G{x+%_#mpZ06+^bL8r@@1#Kc`;J(B3Q -d}}v9ULq~J%&N!sdE>iQpT$(H{i#hhSFTtdQFbJ@)w}3CdQY)0`n1~!w`Wu4~R7IVcm?Y9tZ+s#1$iI!aM~d0fG+;6yrJ8M{p -Uh%T$X0q#!T)7+?JK;o|aqOi|f7Dr*lxYklgnTl9ZhGPP>q%R2~N8UxGA4aIZ4uDY(-!%=y2VzHws$|UAx2liB>g?RMbUw-R -RaOs>m(_PL@eQLA$V+HG$^*k$SwyOZ^@~cZ&J-h2-uqpX5Y*`wzIS^x?^AhrN84o;^+c{244Xq>$X9u -LTFI+&<_au7Y11)D@PGhPhR8!2;N+w{kB>LVsdS3O3EuRqxz7?QZx|d&=nz_B{$v<<6dV=u+Rd99=V5 -R1s$UV_*lz11nOI44SCG`+qlLI-$YkIa>W$27KB92 -1<78r1gM+JI7r@d8jF+w94onHhtmj=ib!yqE$kX3aF@mm8iv*H|Qi$FQ+^aTYgF8fHhSo}f7!3ZE+&1 -&#RVY+(v8z?hj-MQO}aS%rNW_)DZP;f~h!}MwaI~Hr%)G;)}QMf4H=r3^uVQy7ZlaExfn7mks)Aci`K%=raF@ -jSftF8~jy#>STi`CgNU=<)QaZf__&27c&Xt`lh)d@m7rmB`lERw-SvNu3=03wUU5|Xez)CvA<#si}Pv -EfIahI>p?~V68w1xeHmd6tMY>gC#Lot_s@W%QQ9X{RShF_XM;HLbJ^8B2LJv~^+Q0DgUD3OhkP(|WjH -Vq-Y&ZP2NNM{Kv)JClXL8@WHxYG_IG=oPL0f3%zMOqILWwbm0;UbY-Ib<`BJ?P-q;GIX*Ok@{x&iaq2 -d1zw5SYqq`rq#LBDN@9;Oc#u_p)mGvxFsfQC}beNav|oMyc4Q2tB20I4_)Fe0ZMGX$K0im=f`X1m*vu -XngT4B3;IAPYP?wJ)+dMH1Abs{|3_?puCy1SIRuo4 -q`r!6yTXB?IJq%6fWWG{r4(zwkYEwv3ksnqaN1&WPk1eTz##CuEb#NxQFc5fr+ol!&=7Eev9hHSU-+E2b(?&ij}1GVj)fz-esMs(TFrFYl5ybTygqqn9sV_ -GDgN9WKgpmejCl&VT_x#?{GP3PHe3%Eh(H9QYcF*nx1k)SmtM*^95v{_^6*SBKc_^duC|RRg_%h{&n9 -qMVw)PAZ?QE<6Tr6_p%Z;R--F_Faj1Y_d-7r~x>+A&yKnfGccZ?OORyYYHhLoXknQr{G92GD=n;GEbW -nMM9MS+BGc^5^|S85Y@_RjuWPYfKaM7YR-4}0|v1ODp)h67Y%#zsCg_u8l%i4!%YrRZ6U(^6`n_Jxz1 -6Sf{W8tQMLUW&)~&k2*X|~hSh+UFuy;4FJ7GfS!l36Eh#oqmldxS~R&hZt~4`T!02|{_){ -z;BeD)J006D$Rw5}l3#Pl&ReoGLjgDxAm~Jy*NhmG^;FqrL&=EPQ8!fdfa+?c57?F`@zd9SAj#9!2F4 -t1>vi8t@Fd;->Kt`J~^IC8?dDbw_WqjJ`jA{ml<=F5_>1c>VhPL;UgLzt2ft^)~Dk>9$@Q{hY}>6;&p -2zZCRMnzRXTdnA<1Im)veZ~&SI}k@kK-L?}>$%_Ah;M;aZ66=#gVTvu-f&^PAEmPj -M8osAJUJAf&mrp#|;+-%plHQDFbkhC3ljJ|s(h}JH+I$2AJ;0hbV1-iIE}j)zS0GHAnmAEk4(;Cr8g6 -Wp9WvYm0Vr{W4FLEERIICuY`z*KGnGI44ybMn&6N^BTfMk;>Z)mX%p7Ps9Zp?X~fLoOw&v;t^cND*7bir6~@=CRc@1*St^QG5R-!kSM+L-Q<#*ga8=`|iQHFk; -eXy34Rf0VMci}HIL#>tU2G-@?}v?)TLh#fTGD|M1@*{`L|S2eZsuC6hq-VauXuHWRF -WZRgI_Ri)&$Ln-}Jup-KLU#iW6fs%=tjGBync$2bD8@dh5GWd>c7(`BCesuU?To~!H2QxW4r`)q11o2 -dQQb?-^%rrT&HUgG7^5>1GfwO%wdnF|`H3oRdfj3wS#0`y>-cvXTLMIV>L3iDB1S}D$_8*z_d^{KY@J -y_mC(qf29LL4^Y9efJQX&5jpbvjQb?7d#g0Rru3xMDG6~vamE30Fje|=0Ja}cjiWF#n!S-$OE;C-qbd3Yz^&m%|BlTh=l?%KHw4Jz -`?5R=?+A0uV@&%M<`&@dWXT1PJC!fD^*lb|Dj-6CG7Tkb7XmBo$dIuf0w+YgR*_3Wm{-8oW%wD>`_F} -sDQXY>rNeNB8;g(47E8{B8DC$MGEp(=4Ewc#92APx)5=Lo3kz|-pK_LX<|T}QYrdw&}rB(n>GpWgWeP -nvNIx*?&b1`DiJZX0AyydNX1xX3v^$-p#-ylR;+Hbd6y@(O7CkIT$dLQREsHmoIY0bND~yPNYRxE-D* -c32I@$|IbQn!#$lw^ShT4d!^)yuakHFI)h?)tdfCwRz=!3U!idVr09-*Lws{VI2UZ%(Wc^c}R24Yjy6 -#x{2e=0uHv}W>??ou3 -8^a(6cElxT~Z<^0H;=_pkApq6M_toLv-~(-e`}6YR>4L!n11Is+k-gFF|Iy8R9x-dvuKT=EoU0Y_d6i -etPzT`2ey2Luj{#3Azivk@+H+AWp2vo~Y7};UEyo9mz8KbV8l!=z+w4 -FhEjDPR1c`uDKj%DZ#{;utiVrM*GiFKU4TTro75TB-N-6W=E|!D(=OCxKD7vZ%8N2F_3nCkL!45ux_^ -_+j+?GRb$%R!-p^X9bkeHnJ5@~xy_N1#R@_8tSR?W#{ -tJ3o1VMS7pURb5VQbhBs8&HJrV2{*6#?{uG6jY;X)3%K3}_vA92h~4^j&Db=8`^F7##&8<@Mz4wF@Fm% ->8vH3pmuu`bD?g7(G|npe26Rf&c0dDja|(m||gOA98DHq;az+WFFjBBqEHt#aSxVWY=I%2zQ2SseBi=PLGXuYqO`l=aWQ0}8GQnh?!!QdM??vusT5IA%#VgYmB@3i# -1B1trs&qDt-SElXaT&meR`f$DxZx@+K6Pa)Nr3x0(G7>D?ZBWA*3stG#yy?nEDR*aRC>!a!5DOW!R~1 -G5A2k)PmZOMR2<9sm!db4q*%SD@aIv290Y*(6h`V1%xI2;hty4QMqa767&&=r5RH0|Xr1>qhGY&QL(` -Vv{_Vjr-rUro4j84i2Qd0kC1>V}CUu|bXIL|>rm$-Wt|Wsw*iW~Hhd16HQ5y=xP@0B#CVdyCz6GHl?l -siID`Vhx&aoln)aBGvUuc~&wFYhKPFwIH8^nf&%GcIQc^dsoEsy5ZfvS;VD@vX$9%H*c7}{3L()5<~Q -sY%thDBM4EQv4Bpd@ri1D8|urR-cGPUXW{G@|ojmR{02GG=o560nPzLUFc_v6W(o8eV9Uh#$nQ8SWhwJg2r&_8q=IhzrqqGddqbbbP2Hv9@D1lMV%~hDpdSFPJdUJ5vWcb#|T -@6>ZqF~aGpzwk$FrA|5nFH_wcyNQHUSc;bv7_JE;A!RzVo-dUR?q`mC|97wX8Se>f}xxE}NKXq{pGNe -MOt&w#el=v}svD4qzT_9@xY4xK%m6;(=(55 -4vOSRVnk_C}HSNq{{i((mB*ySg~s}hS1`y5lh&x?QH08!?K>G;!>@2JW6`BPQirk_3`yQ-?uXA(at*- -T}dL^;ZJTFM3s6rpX&hrU7XX?ruYFku-e`~1l!>RQF24S6M1!PEM-amzIyAgejE$V -{Xuk)|8#Vz{b3@UUsn#q-uAu1$(-uC1N_)7rOhZ26uAKKf*xR)@7P4GTvf};eW$NJAHbN)uIT>rFFZ- -QSTX>+H1s(S$QWpRJ8Fzkf)D~^7PwBNbwB6IZ7x@xAaRZ){g;oxPNP(Xs4tvpI=5F(sgHGbW~Mkeout -nDgwRGd366k56zO>Hj#5Q{^UB?X)tK>bf?keOt-o=`35P+x0R~o7_jO+rPH&ozM?fiF>;zU^R8+1kuY -XR3||{8GSM{>>jsaG_inv2d1MCEFTT+Aqesk9GaL44pIa9_v9v?^AFu1^W6jIcsd=Eyma^*EB -H*$r|Qfj$uymQuzxMbJ;LN&mTH7ZDYyu^#QkX$k#^@dr2xg`U&Z}M>|@aBqH>G$eMgmoz6=d(yvkh0) -}bDMv`m}=1qE#%0s7m()T!N4)ZhfOnl4eCegksml&cG<%*d@ZJ;_I))*@@-uQQ+nL -Oo;MUe8d>4pUmJ2da6U?bc_07tf*NfXD`0=>Fg&Vz>?JAOM0Pxdt<6jxsaKM#cryj>zPM;NsC^9uumE -S)$Q9=&|LUDHRBst8P(SpQ~E&+f^w&E6?IjLrzNa+I;F42+|Tc3C1oC)N{5lTwG+JnjaGyNkXq`h2&d -4=)l$xOp(4Pqub-djL$y?oaY0an63JcjYM1osoOVX12K-E&V6hIh;I5IoNo+|89dsETGN -J?vr9$FIGeZqx_nQ-#!P5X-_2vw&p3&8FxOx$UJ1Sh$i6!oEY3O#Q%|1S&0YcG(ntXnn_2*Kn8vVUKB -XJ3lCeG`1=yYr`2q5ePh5g(H8nEdHij=T7G=t=~7)mfNKtF^)K@^{j(~l^$$HF=FfXTv|{N8u!hXj&Z -rm5c3J3R@2P@wcgoQdb+OYs)~MueaQo{1OY&*Cdr1W!ZJo?$F6c!JX>N37a&BR4FLiWjY;!MdZ)9a`b1rast&>eo!!Q -tq_ddnQt5ymA{n@kc5#}jOW$nCM6{v>Ia^}-9!%=g}t#+>s{ue9ly42FUi -V&zcRS!8a<6!Vd7%98VIA$YNdy9d~j!7)!f5x1H82*$#_sLw@0%lu(#vz4wr*i3TD__LZt|6>irp4El -&dWzBKtmIp?DQyDoiSz|=23w_x?6d)!5jW@KZTYz1H@EfQEd4jP_1`T0bm&{Ewx{ -2j=jl0IMV7RSD=t(GhT^|d#(8PVlZ9*RyFSlN670yobiKw7jmL^D1CF*Xm-b+OvB>{WO9KQH000080Q --4XQ)WGdPksad0Ei0!03ZMW0B~t=FJEbHbY*gGVQepTbZKmJFK}UFYhh<;Zf7oVd8JlOZ{s!)z3W#HS -`?4~hro8xi+~gjc9SA#zL5A91QH{SWF``+l2j7+*LO%tGG#k^Ss$F39L~IX^JYli^->v9527?uwmRZ| -p_NU;MHAIZb_6=cTiSuvtN}7wT>GD)MbDH5H5pt0RCjL0+n8;S9;e;g-f$^cyCUnMZz1wFJ@0A$2BMO -)oBp-Q6=*rA67+!;#w=f16FAmAl)UDk^oqRUH%9r%DXQS#fh*`h7(KbT->n@v8seEw{NUOs{yf;6!c@ -30pfF1cA0@bq=OZ^#z%>|FF~iQ4lIqwobl7Uzaa~TwDz5vMZS$U)O%&NOA>*f0y=VjG%B>}NE?5V7o< ->nrK~2gHl&|@iuFm_d*+`K@1V4L=*<74Q%<5^T5nrJn -wSlch9i6A&bgo5k&YPzr3oYP$hbc7Ch@L{G;-cD)E4XZkerckC&r>7vL@UXP(hDgux?fmYz*ie*iXk^ -q&n%iaNnhpGZZ5|KO_O#P*UivreUnd?Aue3PFNI}K_N}WS`ASAG)K)0*O-TPCV|-KjOrG5)H~O(BHl4 -6E?BFfn8PvL)Z|jUv6fd46EhjPtVyv~yMk;OGAfV`XH9CUkY~Cm4FcXfM!0r@%w|+y$Ql9rKEc0AzVQ -k{2uUm-)+Y~`9f4aujwvdi%ZXCmH7K2Pc>t5_ok@N6ql1ah(}`I>Y?7G9^hI)!bgWNMtxK_{X&MFa_I -z>iInJ?Zu$}U5Y|;c<&t!{MchVmR;Zprm(GI#io8(djeynb>xiu-Udb@yWF%xW=C1^~F1*EhXbMujwW -uC0uAVB8g>+|*5kF)jJtOBmN(1wW;Tym@Bu%$NDOQdm`G82^pPZwBsO%_IVt7&$yiD;aQMKX(%zHrbY -%EMss-9LYyT;5#V#z%b5>CQ&K5MBo@;fF1KXG-9BN#Yy1b6Cp(X+cH(rsLIONIPa}D@)fgqagRdV8N! -6%&P|-=@fM+sIhW;=-sIrZ{U2qluv{EIPoBSi`TMk<-47iEf+w*`qV_C)DX=(Hyb;jZPfL)hnU!J7Z< -ljFFn&5su{uQPKf~kYX6@(UbJOsRkjklIt|*-DYR`teb#^YGn?2uB9+UVhxOUj)%ks>6`Wu`U98xF5^ -}qvKRG!DvL@65>65+9AhFOWOxTt?`fGl3(N+z4$?yWgDfXfHe{lN8*Wc5bf#BA#Lo&A>MU&aY9Ra~}W -gbG`@ugwZP#Ub>?y(bC68ZcBxa}%zgX8`*IKB|86Dx@zH1T1z6()T2AT-{0D>n^r1urb=mP>)oXhc5!&A?=U;Jf9_kE^wr0Sv8R)Pga{!~s5-iRhW&{E)oh>#LsghJu^{?L@DciE!1%23nVFnV*1Qz6c -vmQ7x_Iw&$#p!dV0P&t$(`;8uZCp|n{_Xt`&BJ^&2|H`vY?=;6#s`VNj8Ul<)2%v+~npXifEfHn{B7_ -Qij)=3~Iy-OI0^%iVtchaJ_xxK0baJIJONh`KQq~>o^y=RxkD;+vdnhW~94LY^!4kLx)7GnGxTsq5NQ -8S!qB2qTgsuZ^33sdMp=IBqu9j|t@FaDMQ92>0PY8u1dgWsZ?Y&(=Ve3XCWv_0Fic&~faxpj(jDS(H# -I+>4dr0!>btXomkq~)KNzZe51Iy8Io<ZxAA?paP<1SX{-l;hZpeiwHAG`qdeK|r -4y1?Mbi+SilyCk{AS>It?v7y>(bYX_=$Zl81qIA}Phj3N-#-QGCa~xl{6o^iy`f6(gKbok<)bYICdbSy6EzQ!6w;=;F*d-8Dv0a`EHe>YH>FB}Z2-sI?z}tk1abBD;$40>yUuTJXTP!Yu0?^`RQXV7%Mp#4_-SP16wZF$Wqld5kUXWP_#H#*40U0T1U(e@U7CC@ -wI9PpZ0cQ}qC6=w?kWb_vCE%Mp-v+s9<7z3A{^YGz$JA#yOAmCxNjy2HqPGnR*^Gg(jb;t4s>`jupcf -ujAa@nfln=EdP92AFeh`T7K<;Uk53N|+0gMB0oLSfU||ut%y}YgH^n%GLpx=<>#ODR?t%J<{4*aRi@Zs!3~NA -bAc*H_ekrb8TLhE@!EUNY20MJ&^I*sZK|<(!JexwZ*Wk6a~HvujW`-5U8Ml}_IE0mko?KwfZTc6HC(0 -qMj3{MU)U;Kg>Z)Oz(;lfGTmDlnHdQy{m?F%?Z>{_>?Q_fkl&%UpKbnW3aBqr>DGW;y~GTOrOSPTP*p`*il*ssrWp@Y0>`I54;ZaBF8@a%FRGb#h~6b1rasjg!xc8!-&V@ADL5dfCv_4-nX%wuOc6(o0Vv7-y`>ia -NHDoTWqQyVrJ-`O~ILeHlFR_xof??l~MG4Uzl-=okYhc%Uw=;V~hby~8zpAxTZsmxGa_(y!=kU=_a~G -^2zQcPLlwK6{U%yeCY?nq)Q&(`@rQ;oNhcn$j@q3l-h;Uhc;kLN7PDiWnfzxz==;a`l52QC)g9B~7gT#5S-+(cw -dC-(ISkpIJAq8>24P0im4ns}HtGnXZOTL?R4u?rUOR-*uabQlwS^Hc&4HT;P=Fc<+`g5SXtm5}6nd+W -+r*j%x4l|{qV^U1ku@6-Z;DtA$Whxhl;w?+B0B=)Ozvt3@pkl^j2oQLRUX|AJLdb|6p^0N`FYPBeJA+ -1)EsPSK!8R(Xdv@RgMUl1vwwncIVokxYK``sP7O?n`Y -Pvqu}`M(>$BmQrn*^vfTmbrHKNoz#jK5nIC_6{rk?;Eb(z65*OX7+x|9t}2uA$fP>xj1!(cNVoGFPV2 -%3XDIYL&tj@`s(WAI$i&A`72wodBE^5Y?2{$#I%iwyr@S)gi+mtl7GVafY%lK ->%%wZ?{k)>WH3!v{&(n=<_Oq=l6=LNZ4W5~4PNM`YffLZGmKt|n_|kcG> -u`K+$1UecOy>Xmc3xZg%}H -C-QGl|vJ6kNmU=aj?fSgfw(kJ&)Du5J8w4VKXCoPXDMNLUGQN6w`^oAFyu6x5SyVkft1d;zqEAU9A)5 -?NLGp%xob+JJ+2>6L3Qyx*NO_k?hK%}~IW#z`81&hD0o3Ylu^l^qh-k-ts*~P%I@C3sMsy!%Yn?9s&r -B*FLc(^aar+!U>THUhbhVRE=U-vp{)J=`jq%S=t=LaFApCQc_YqkbbW1=o|c5Nz5A2vDP6ku5i#@1R0 -$2_hwjr%=K6NGlkxWJb7Q>FzmiyUR`xTqC%{ILRQPX>%rabRw4sN1NVDo5Vk)qB_>r^da;R`OdE4dW` -j3r0&>h?P2;0!gmTp1b)VAB)b`i0BhT!~IrjMD&zuhKmr#M-mcy9-J$fl -Boco89Rz)DqDKq{nZ)KB%x8T)i~eX(R02h3{4i{cou2bc*Q-I!%eJ+_)=@wz=U@`589JIrHL9~J~Q(5 -RAD7id)B!SG{c3~D5G(ysFee_MHln_5D^A_r88BMA$ppPLAn*$S8GSCb|89P~GNz8r7 -sLC<&Fe_w;_DkrHFJ|kQ#-bX<=NDO?{S}ad>>W7$t5?ocxx&kKHyazU5w$;fl+iFi}AaKZ$b1rgot{5 -P!WnO>BWifah4+MB;Ily^#{^FP)h> -@6aWAK2mt$eR#VBC0E@{C002rS001EX003}la4%nJZggdGZeeUMb#!TLb1!psVsLVAV`X!5E^v9B8f$ -OcIP$xH1y98y_MIc#1BX2X=L5P;wj11T6QpSuhiu@|5*>3RlUh=;8wCCDH!~z9QdW}g8fa~a!!`Ykvp1*rLr&q7lg -ZuG@R~27X!abcg!t_r+O53 -gjw@K%$cr*T6(ZCEaGz^h-Qc5T-gR0$CvLr37i?DM-jA*tZeQD8gP6(yZY|p{n2kUoSUz}c?uq(0EJHb8;@qS -|ajZmyKn)Csz*Tlhh2x69M>kLRanR{ -)$Hf8{5Gt)*DAEx#Ab!V#SQnFGlaHcD*HBDQX4?D#+N3n$Vn%beL7!5hJN48ip3S5Y*10_X`Z9o_WYH -@8kuAUs$`t?-VJ)5e>PDDDZM0vQ2>Z3HiwUMHBWRE0?1NE;#1s(N7eO7B5&twD8wR>X0ExZx@9KtbEm -A`K4srohXb$j*9>h9?5oXatBAe$P4}QCX30dBrZ8jWBy@!Q1Glz;t66oU0dq{xlWv{*DSk8PT*xB|u) -=Age8Jmhs9o);ZZ4(7qkfHoQ)?{5>HHWRUZAD^z1iBc-%;#UE02zrsDyq53`Zuz`Un-~c5r8gNDAm#r -RH0pWx^sxi}ijj@E#j1}YxU1|kWX4{P0ShJpb)Ra*)RNR?K{+!j)@V#5ud(d3LxuK^Qo^H|9CfQeEIi%bh;T -O%AjrF6qxw1p#q@QnelBJZdwID!>6pT$;?u`K^pOEv8bh%!|ETdNx}MRB3w%*M>S7uJl-HrbgN^}tE# -{mk2(5j(-FKW1~?T55S$?r5rIVd18qSOw(Xl`J0Zbbo)Uv+#u;ozjVaTtwKm2R85ZI%M -<#!0@|hA{W+v0d6O_T@>J$sH8E2PY7Kq2Ehy#b?&vOb(C`?Pas+{N5ckccJH`2D9D?cz*xizqV(bJ75 -iCG1gGPmt&6}F+wrOuNRjMgK+VHI*7PnyTso`X+B;b2o#qw$fELg>HVBt;1q7O%mLdVP5D(m+L<(#m`pcNmxzoc-TAwRVLDwAx^vOPBnzvZ0*%gJt!&~^N(4@8aOOM -M?9vJuJ9zuZ8Kx=o39dJ(pGj!8#BsGT%gMS#8qaz!h&PXqL=vZ5K;)^UV{(k;H@iGYW#Q3?;?a=GTR6 -6KQg)onvXAn3Je18P#y5-ZOUIIpqs5dJzUffIFOJ%R)%dY`}h{vw2MjETKL9bst+25B%0^c4XiL280; -EmR~?L5lcJ&%XQc7G9r$EfH4PPw}!P3|gPT2|$717OYU}&K50Vn=Fh0q;0lHM!66@-`ecKp;vLVASPT@Bg^Grz>%I25FZCIC8FlQN -lm8?4-wZcxb46hV}8*MyKKl>u2|B%OA>> -xNwQlr7N(jfvdZl!`cqV$wI%x*tqplGL$>rh?SzP(NpJsfPzc5nl#w{Mh2p}Hm!&?Fi^+@CIY8(*=XG -*l=M?h8RBLM>}D`N+8z&eTDwV5-z9IBS#k_&Avk9AhC;Xb2KTLn16TvMP@{DNI5CE2>U?866&adq|UCcF0QpQXd5Gki$AKs+aKIA}27I|xYu!68#Kfi3%_7J%N5rlkNTw+-^3!4L>8u;Dw=2MWh9;0|+V;50%QAAv -$>AZ`f?1OFu0Zd_O%W`$(+$!62{nW-wL*;T6)9xpVj-&iP)V~noGhnxebwatC<$qQXSOlN8O3zbTr@6 -;}D>k{(pG=Z3PLs6_(P|<0rlOU*dJw>oN9oubf%tH@^XGyHUmo$U<WUV?$>9>cu9KE3#GjiM$K>dPuW(|QgYv`lFi=q0;=e}k8paqJ>n -@#5ZA+-`g{&3Z6z4s>0@&cdw;=4N=;>jW4b6q4xy4?MOoP$5HgnraO{nVVaFgH;^{Eb&t)75?qNEIwSVH?|xSNbAtpn?8m+*&j+JX!7lo?4PBv0nEoFDi(j)1DK^iptk&P$|ww%3UnMeVD -Z9Nmy(3cI)JYsWg>7w*4nGonOSv=2$1-TEs$hWZHEg(G_gEKfsPz4kuIwCrtGKUoX}$^`|X=Vc9P#^I -@tN!TXJ$i4l<8Zbc~}pi~d3e-7dr0?!7RK2RizBYi$RXB4dukLq<8ZV`ve0VjS(Cr&~Yen{ebE}IBw4 -b4bZ4FG|kNIQr8#Ht&b&cRf$7y4|p=J-lWDL(p9#8%Bo;Ab7EtWbH=wJb01{?LzCZvHqPoI((Mub`33{)yD22a4Yn5zh9yT3 -Zh7Bw$Z0z|sv}YZp9H_xrI%@Fy>#2o}&rt)I5yfNlnFKRgZg}bD_(7z72TEEVzC(vcUPl_%u2&uNl_z -|}n{v;_ARTzu0XL9#GtCGjd+xe%xC1+atSd*Jq6JIIfRHV^+24WdJyWVZm)V?QUXRQl{9uZzL~NC)A4 -`hz5k#S5dJu!lQ~bE`{>!&E!%w(+Siq0N97zykB}4d--7b1N9EXah*aPc2+%P+Mb66p`1HqY7U{c?6f -2}bcQXeUAiQ#J0JZN70QuTsBU?_*;J_;8V85Aj?dVga=K=3V$MmKq -$yEdV}YrQlYZ!eNtz@@|P=C@6aWAK2mt$eR#T5fn -iJaz008bC0018V003}la4%nJZggdGZeeUMb#!TLb1!sdZE#;?X>u-bdBs`lliRit|E|9RktY*sO0jj@ -^l?*Va`h?kO>LiRU*gVOJRFDwCFCfA1;D#gJ@((8-37phNL`zmwB?thh{a+bzkT84zE;{Yp>?HZ(*2N -DXsc9d!iQ3Ax3b!XpUNaPx4ZDuihW(kQp^gi_AFDC6%V$Q8|K&757NH1JiCp<;+|K0E415S4>j-(#OK -u^W*0KEF}nmYxK%o4SGAR@;$_UP54HH7!>8{m_%d@GYFQ_#1kM_0lE%u?BztMHz*AJsK4yAvwGguzDk -C&o1+9{;urKIal%^Hmi!@!#X6ZFh_|u|!dOeTXMm1HwnqPyu7gCE1L_5rZboPY(O;)*Ksvuh^&Gmuho --b0RxkjZ-GMRqP+I?L6DOFDlE&0&Q{uKMBfW4~ax7~2gn$SDTea)aVmiyT?s_iD8f@%H$Rp~$0_ -Q;#W~Jcp+SK66aSb~pE}*V=()jItUvdP12N(-!5aR@Sv#TqBL9K-m6}TH9i%EbxBc43z+*&O+jTNRd9 -Q7Ri)cZnZjv;^vvr47~;3=PC+lxzt(tyo7k3D43<=YYZ5Tg|nP>8S<>NALFWn>C(iG|bXB^;vio;qam -ruKLW&jlT3NSG--W?tfn^H^Keq|35CZ}DJD2HwMj5<~w%e7rKwc%I3LgK_UD499h7kfP&(>v|ar;2m; -PeB_xk*V)}*VvZd}3vk>MaNq4~6FRKUQSy1W7Wyh*hI3cDfbbh$8h`epS1y|xyOg9=$NLT+u&K8vSRz -O6-9zd*`w+ho+s?GVE0fVg${L)BdqFDX3};BhlW)cRo=C3RAjvgewDgUMxXsAujyb}z=tWht} -qJjCD?`J;~-5_K{FkYN{PY>uSd!w=>W$y{mb_Fn_?6gJ9M?$JKX)FP9`p*50$NEbTP1%z&6ximWS_DlK>4UEU0qLq~|&MvP|MeYH@t$D&hZgMLjZ5+FRR$C(+DKH9h@0w<`{=K(JGOtdzlA|OTPmiNaJ@F^S^k`DV47@O~S1A(VvNIKhqz-8e}zR636_ErPp*odOh -Ng>w|p{ZC4&cY-eX~uJ!#hz1t1FO!U#R7+~aNHfb@`qizXA32eq)<1~X2U2@bmCl31^^WzNCvcmc@ku --O?B!6w?grGMx@j2ipx~+hRMPBF!%VuAN@A`3415Njl3i(N+=tVaj34$D9A(RVBiG9V -EPi6O`xj{t(PUE^*EWszLeFi$2WMb-_jlu-#}S`BRx>yoPp=i@ZD#VX<1tX)2YHV2(6Z?V5}z%X>a!`AeK6s2+uDoMeBS<9h@>TOl7-#k`jF@&kI~<8r>}P~a|70C<~ -b8+m_BHeYA#b2E?W;Kgy|45K+<;>0)$etRSK$~j`v@u3Ao0AR~$al*G^JUn*UnGo9pECyRJ4#cssCh8 -K;VhCiXM5hy7G4;vYj{H3Tk!RqdL1!Kfek~qp)SX`D6x=;?f*pYwZC*7DswcPP#)stJ@iZrBCm!d0f&AfBwLh3D?~E9Yt?Lb_zhAAHE4c6hjXwma>qcTDe%HF@vHUi -HpPyz(NK1o=FsP%R=F$+NDmcV0fTF>!Af#K{Zdwif}dsX@8O=fn>dKj9R2yl@(YqpR#4Ss&i2CcoLe{ -8y;z@LT)z4?efIqP&0Fltm!Fc==f8RS+u!~E%Rl__Pk%mjg%!5|^fyjnC}>BAaAk#w;nam!3*uGFF44 -eF*3taeKm0K7lF+E(cjv9#pV0nL9PVj;>%`uQ3NK0owGk%DO32sv+EuLfCI!cxqe~{Ymo82OU3Luaj) -`T`l~IHiFSnqK-R*IjE_vo>#Ndt|@}Sb;3NmRqkn0Aw&dj+DGrlWvJr*&PBu50vu;t-4)I7U|0B|z02 -=TIkLdRYoz&;g00_p|Y2u%u3L*05wf_5*auf&j)-0Xy3NStOa- -Z@}erbc%YX-e7w`c{JdH4>&Fvz!k=4wq#ESgp2XKREc|XJ)-;5oc#(ENq$taidwN2@DV9){BKSF%bER`HTl -nKPqk9kWrJR$WnjX0V^y1=76V5NjOu!bqa;I-Fgd7(L&y~F_|71M4WlUlAJ`6EpVZN?N5coE7g1Yw^> -$750ZO5Gn7Tc92IWy!Fm(INC(_>aPmsI7aQR`%BjO>U|2?8d=p|XN|8e>DdfGEow*G_5j8i@CSUr3;u --^R>Xw2~m`-aL%0j@6aWAK2mt$eR#QBwb)5_f007D& -001BW003}la4%nJZggdGZeeUMb#!TLb1!vnaA9L>X>MmOaCz-oZFAhV5&nL^0;R?YS)D1%?${l*a>i} -sjHZe0vE&(#9Sw~mad(J#1Pg$YPHFnvySo72d9sr9gF92s#5w|t#bRHd1rr3pJ1cW(r7%p3oLiAGuG6 -)=5+5)!QQy)o;DHeazS%E7U*vjK(J!?;j7eGINS&r^JQy?ASW --<4i27RTv=mjmj!Crh!c?<5yhfX!;=P2(!mt7x|Vgm1b|YcC8BbB!05rl{3HH@cRkcGs`{YB<^exZ#d?z3R)}kh3M8>YyGF=1RmC7N+*qUEq7+A#&kV{x0P%^G%zKt>bkQ -psJhLDThR*xbYZr<@9H>8EFbU_Qekfwy_PIAFB*atl-EV*4DL7Zxm7~}0Dyj(cBaT2z|KXDGjvJ8WRA -ZmX9_+#L9l1l~?a1ghI7nzS1e2A_$M+}#;$cP>LYj*&7Mhr|%t5DZ|CGm@n8n*s)w3N<;%;xOSNhETV -sk8B&3xTe)(ufi9N2H#EAuqfp*W8TsXpWlHMM@-E-DwR -&o0x<$7V(^kE1@A2C}|ozqAFlnwE$5>YL(vN7%%GV9o5d0s?A0fNvXwB-hgRe!8)42;G&(px=gDAM?4 -2r7z<0LeJE%OV0&Q>^3B-W~&}MSlf!@G64B7jN{){z^Ebj+{d(TB -67L1QE5%(l;?)SkG*~89ljU6G*fdq?h9IH9$^!4V>M3!R3uni6RQ%K;Z<%ENUkC&^U45?NGppWG)PUY -L)7~CfnN$nPyn_zm{hrSdZj>eoCJAxWrRLhX;qa6*GlJ^3#D`o`?CF;$Yu41s)R5CI~19^Hmrm?Te@< -Ssnqng21U{0(4~d&k$U#*8T*R;dsqej#U81{!^5bq5&&Kj30Xany`xG%Fh@0%8F&-w25f^Q0B9UaNVv -6{J%7#)pZW?3{zZhCUbR?NIoD1%IKYc-qYgM0fD1#Q2vp;j4;A!H*Rm)}*l;VbqR>f3V6e<;IJ5qJFNZ=c~X?n@j?t6XZ26@9-ksCp%h4vvSdL!u -}}4$pJK*dIk=}tU_k7A;sa$~i7bLN?eK+NgJ<@v -vJsnw&@Sk^=5g`Z;T)o6=8Q};$FfM~iJXfEwFd``mynhbt}nqu*g_yUM_&F|F=)~XCkO_Fck+49HPSU -NR-lf`U|p$wymL5+5EmCzv)b5*dlqW(YFMk>B??6DC8n8D6pcebQPm3%BO1i#se#s`{TO27CHfKZm}B -8o*D&_V4z_YM0)(~==&uE`P4C4AEZaHEMR&x3PI{6>a -%NBjB6|Mh&~=(jJemz`X?oeM$r&TMbx7OCLLmULyqB)$`|day4PbpmU|Dn_EOtVszH4@T{FKcZvL^k6!(P%_-X~eh*4E-Tuu>XeJj&ClDHdjR>l`V@{6q!Ml -IV212(;GuDJ672qdod{S-KP^iLo4;6>BIu~Z+z%I_N^+uwwU7~3GpnX^vJK2%qWiVbfA7hiwkL8yTNk -Q@dU4QT@{yzTp)#^>M>;MZ2YwW+y#wnVaM+6gE^+)C$ApS^uoXUP?mIA1MCr8L%v|(Lzvx{<@6GK++7 -5qnR1KLsl`tr<_@=Pygl?EppG3PEXz2}l@Z~19X|5T(mvq>jf}q@7u8-dl9zI0BW7KKAy__Nf_D~MSp -7STEir<6`(LWH4%-5gFSyoz3B?xElQ&TewNmIX>LjLkb?ejB9W)k+TKDomG|)#d-Zt60IrJ}uCOIu`A -K!~r@7iXP)6M;4f>Uuld8&GOH^usUplhxRuu{Lk1%KxS`d}t3`VjQr&^Dn%qBG%*rfWUO1s|AT-mlXI^=pwG0NYBKgU3&wJ(*;Jj#^jT#EBBRKi=TD4{|$Va#bij#z4N%oY -Wr|5Q@OqE}75N22+F9RcRZyFi`lR;DG~_T4hy=)k2yb970oyR4!$T}tUB@1 -KS7zM04+d63KDT^R5{6w?IaTM>fkuQSuvg}sZKCZ0=dX}MKMzjq=$(P^@Ev!GJ8l!ZPg@4M_l7%k_S> -$GcQi?O+F^@S^7PX6NX?~S+bGdc|Gtuje5aeZT2X(arVl^e%~=x#i+UNK&Od*VvoH%egDJB5rj~Y<>H(1#D=eA$_zjcZXs=%mc(cB -6q-!T{?pBf^W`RkhtPKxx#Af{KmNZ4514;Zb#iQTE^v8$Ro`#iHVl6EU%{y_HgfT`*&aFrDKK0&AV84-U9({5f`j6tYi&BwmE;@_!~XY?vV -1?fV1CF+l=$%@`B5?3fqVoCg_%|%dgH##yU|-~p6WsSfZy0WejPEyqVu$cxF6UH@{PM88gkke(_!7xc -F=J?eYm-OfA{cfQ4|~O;0wkzBd$Z+;}%O^Ho~ET%XbHy&un;X3gFLlpc(~^7|scm$t<78IC=-S7x1R~{Q9xBv_oH3nFEvS*O7!rDeEHx{+$9SrPA+;+%s|#IypfkUUkM|1NR?4)BvA)oKAM -eT6jX(>J5T^3S`H~3%7w@KTia|jude-UFQnNP@jcL_G@lUEvwp*mn!zbsbIz%bK&M^F;rI~cvR}1LW?= -(WI9gwqoNCc-43=mil97Xn$Pk>TH@;9G+=l`ixkwDf9a2PGMi$rE2hV=_2n*_s)s+eUbXp=9HpI6t{p -!iqYo^SCvW1uK6uW+kSm0iW9N}M#lPubhe=CZ -*!$a6&S&sAAd5^aq1PUTx0W$_Da0TADY(d%953L>OlcDke$#fupS>r4Y>j-Ka^x;k{;BTV>6-vKD1>p -4d1r-_gV>t@gVnKP88>rV)B!HnIcxqKjq>(t)@hm9U!=R)?G@ZPcrb25yO0!~`Uk0H#5J0ewQv`@me^{6Aen-G`5ryPW7q -F4Xd>&>IQxYNIfT8Gw{wZ#~@bOQ6P)h>@6aWAK2mt$eR#RGmvL_-1004sx001EX003}la4%nJZggdGZeeUMb#!T -Lb1!yja&&cJY-MhCE^v8`S8Z?GMhyP0U%{y#A`iCO4I3I1bB7>vi(*+DG%2>BE7Y?@*|*Af!rjTPvF* -Q)yt7{HC}{%(v1A^PR4}OknmLmReWO{55eLU#>C#WI>jkb-EN5v)W)OKZ8((e|LD?2$!$Z$e+7iOABi=R -qnf3j{F;b_y?;IH27c%}!cOPe2;ykv3n0K#LVYo7H&%A_gH%Le;h!G+{ -~iY?DLL`{3?UMtjEPpxgCi_dC -rqvXdaO?DMEmhJ7Nf7o?5`ui#nSIDA5aOb8Yu&8YUGEf{n8tC~8gp6hr#^^fBP%zP3fLQLbkWegqS^` -4r{3AlkT%#r#do(aX6Gp+o^iFTADut_?&+j6_BOgkm7HWVC3@$&ATZ}~yX&-EJJ@a%`= -^ali_Q&;m7InpXXetmR{Rjxw7eygMXH~=|notzYPMDwqB?PMTL(6tLEUOcl?s}Dc-_yh1jvV%;GW6Jx -@4tHy;*wF40%9u^#-7!r?3$Qnn+6I6)wEYC;+0=;1o+1`&NC7Kx@JfM}rLB<5N?3nnD=I6yjr7OXSLm+{rDbaKY*Ei$LmAGTOnNo;xSbJTJ&bfI;`795#iO(M_GienBBMna?5i^o&O}#Ya7Oei4{Ft_ -NQ~>p93+=~*~_+%Kv7>|&B78Won>*AGTENbCJz4cJJQE6dc8Ial&@m~)j2bZ7`-2AO1oi9BTRUG->X; -?VOWlsaxi_K?v4$O>r&lZT#o&rI59v9$a*@o8jU*=Yw2ko86M&f0KBhgpZv@ZM?n_Q8qjW={8I9q7ZqivBpErtRlrPrLp`^1Ajjt!}Iy -~PhiJFa&R}Gw`aB^2LJ#Q7ytkq0001RX>c!JX>N37a&BR4FLiWjY;!MnXk}$=E^v9RSX -*z~HWYsMui#V+l>kL;`sg7Ax-Hv^wd>Gz9k3t}XoKXp;JLiy;MA`0Tm=PeBMV`xdzH=d?D -Eg!Cx>gys?wi8Mw$W_eD)!K-JK1d5gEU+Auoa?WYgq}MEutt|EM!f4s&=!1Rg1N1YbI(gtE?7U^NnC` -#cTUGUD0u+h4Kr2rziW~>^jux-ux&Fld^YxSGn!~$PR7r@4x5e-?IA&eo2}8vaky~*=6~?Xv$WBw&IS -Xky&qKrTylDH*F&eUdew%-tbzS*m<|fmC$Xk3ZX}%w0YUATl}x&sz=09Hhr4fAR8l8!>deuEJSC~b%O -qEv;|(2e<8f5c9F&B>?q7@VR*?6Uo75Vzy0Ioo8R&u-n{<(#k-f^zq)2uEWS+H(`2z&V3YvxD<7_GBg -`xMEzZgJJjZ24#kFQ+jF%iw)g;RWgAc^t|H3-e-pdkXn=Lo2=ACA>e^-j4Eis%E-%ZQF4HDy>9A9JjM -do!Z=u0WqEC)cO$@5r?YMl-WmW06#0H3ne&Io;*TB4FmO9n3zcJZA3);7XgjvoLW+)lDVs27)vOF&JQ -kD>s$@nJLzK+OGSXIiKj{*QW%+KmtRQNr}98SDw9p`6hk3lI=S0-)kYgjHNC1xd;^6QR^c>_xw+MPnc -$0?p)oWtgDVl*;&Xt0d5XwQG99l*&TWxYXi^pmDGUKkLYvF+_TZJywwY9LK|w?ZqJq#31hw%z)#0IZ3 -A+WLY8B04onl!IBNxKj5AM#S4zx=y5@Dm?q~y;h%+JJXFi3YS~6Y8Gt3EFSB7^5;FP-nk9n`yxCcx^* -~|E^K}ajfoVAqiE5v7XYj;2kXRia!<@=3Y`N(O&JHAv&S$uz0(;|xNq{fYhAX2n#PK@Xd!>gbQF1$zY -s5!$oI9Xsf1rh=j006bUueuowR}uNpCY&=(_JM^96gCrcIo^%N&di?n?Mv=q69A;aRLjbkfFX4IT8lu -M24IpceFpSEv)7Z_930hB%A>U|!fp4RyoS{sAWekSc25&L3{vc-N{Ogru|z(XW#I^~?43s{xQKNVb4zJ_&~Ujly&P>v8(c$v -NyB>hffZ7c*|V7fK^QewTZmHJC3W>i)W4LS}xTgr0BnX>1X{#)xN55vk<=;kp_ET-6oY(~36 -QawV~s7>0ss0A>QgJm%6^xCkMU{5T|`xO|CHGAKMrSRtI?O4PuV3P`dSOlGdioTt~HLZ7I!7ZJ^Xlko -`G(t7hc<1;a=<|d+nQ#MmQ8LJ_L=z8k6ZV{4epYx;cB2{w5ecQ{REVX}R^f6=WI!&;KD2VGcucn>1Pw -47Rsq`KOW&i8hWeP`R_uVQD*0WPSe)^zS@KvOyqg -pK5F-xyhoadiiW9ysil7f=BV;0z^zhMj4(j;BXWuPY5?C0|y-*HO{-RY~zfXQ5Jnw-{DbiUTr6G6~A< -c*vMT92DXaYuCqTWtYla*gUcRLN5`v&5x}dC(0H;T=|p18`!HJz@zkxV`0UlGI<5(gMDD(;R&h#%BpH -Xz!m`k)t{OUEk1rUJm#9+gX7rHq`laT&M+dLHR;csxXuPygy$k3*Q95b<>ZRs87!&TX)QUAwT0=CuzT#jYjQhPI-miFZ_kbhWMKBhy2jQd@y;1;8wP -SMAv`Qd<>TFsXtfg>xrO6B@juGLI!hRNV^8=mU9ZM%esBB -nse}f$DCeKzMZPi+`{~>e)TRxrS?ff#tqRh0+}}##wTzCBrj97oVcGhXiqqYu?3#rQ7Uhkt|A*fZ*?W -)mvhJ0jK@M85j(Vc1oso>l5SVavtmXyg0&$mCMe2_E?Rx9x%3{NeFsag#`<{AC1Fa8`P5-8*E8}VxzZ -IdzDy2uHjvjY@ml$Fnotw=r-#yjM@GhMcv6L!aW;=nEY3P=jTG1>O(h_L -7%6BHox1K$Z;S4IN*#z4_c-2)g;{m>~b@ekHsq8FC88<>&19QWJjv$}wtxc&z6@><MtBUtcb -8d390EP69y;zVA~s;UFZq(ZrKi1Bn+8Vz@44w!p~Dbm?@6`1IBVqe7U|B;Rk_0ZcwR&IAa-N3YaECIw -!B3z#!yz|_L3B&VKJhRonF1dLCfhzA(8nhgO44HLQB+<7#9;PI^WfePf -C(7)TUk3<}-XVBc*I)FXWWv01*$6)rWBIw-Sz4{5v<7@6aWAK2mt$ -eR#QPhT(9B-001cq000{R003}la4%nJZggdGZeeUMc4KodVqtn=VR9~Td97AUkDNFVzW1-NSVapOhA3 -C0Uge=gyV58tMYFlIMwSOmW5w8xZMtXj>$l1V8oKFB4&eeWm+SG>SA|7qwDm;l{a&d3rm?ys{@K7c5p -nBIIG>Y$jTc%mnUnk8NWO`hMwo&M<++8eqW40&q7$wf&;BjynyY*M1Qu%HjAYN$FyCH3?fOS!b;X?Jnwqn{-YY5ht(I}Y0p1v|4H|%{%EbX -;pLWWN1Tg2_jf^-QmRm`NUg*dz# -QVMkdp2`x98PO6rvs*M}2YI(;Uh-0ewpskzMdz0jsGIuU?uDuX7muFW*@={4iKFA<@e7$v^c~N!Tx4M -q`@rD3c^pQDEEk96Z_b!Exk~nWOa{E^x=nfVS -toBlnc(HLmi7;&I%sfe%6Z5Js#-5a6ort8dtTy7%Ojd*z7N@Sb6Z4 -@9~Y%=|Am=53c^uGwk-6i?OF0a=KB-f!v1o}47N-;?}2u~3XG02hHgSwwIP+pEdc=ps9n!W{+F;<`jv--svTTTcG_ayN_@!Wx*gkWZ%%NMvkv66WwQK+4yzVVBi45q>fjC6=pWJMpI8{Crv7xRXC?~L*k$)3G&%6%auQg&O8j|8azz(dRkG7RWW)B2*U==-J6G1T`-HNQsF3~ -frK&I39s#Qil0|p%!)s|Jj4@MVhYNraAFS=j<4MpUL?;!;Gh1Ns52_ROdrrx$e)Gyl1t0&b6zcW=Nw! -6iA;e@PcUh9@+x!nuO9KQH000080Q-4XQ|r5509_RT0E|Td02lxO0B~t=FJ -EbHbY*gGVQepUV{=}ddCgpVbK5wQ|KFbi%S_5tB2jT>@Ag(#-bu!B9(A)x%J$A&mF+MP2}+nyq -=q1+sHWz-Uv~o_!G~mfDqD4`nu$aL4WQAFUv~qRt94$KEX@}SnJtc_{+X*_&C8|ziQiK>w?7MEzg0yF -&!KIhy3Y#nTP0L^loa`jmAf@iXQstZQk7%&VO`2R6UA`4nmogqJUR{_!Qx>|Lfxo~9fdoEJr243VfxnW|tVRb)xLm2oVxEAbU35lgjB`7Xp2j@eSiB82(xr=zO*y2vA;>It -p*7ZFB!N<;lNkqS1VP%y_R&&ncCZBPG*!RKXFF5hKIKBiw6VwIQTeO|uKt1PBPbzc0;Q`vs8(VSHJ_s -@E)Gd-4mmdGroC5B}l!u)>~Q5hC_UWPc~un_BYLSK-g+7nkooybmwV-<?CS^f6q<=de}d!MfPBzZY`{cQ60+u# -5C?2qp}Xbpp(ePsTnh$^5G`*?Y2L8?V>5I`1rtzgHHmz36YL29jH4pvkPt)1$)6vW{zgi-Pu**b&0+O|{lgHtvM -q+8k0sCjAN(PD#8ngw{z->c^r)yYuwGxGlShVB?k3a{R${ANI%S%?~Y>xcOl1lM;DolU&3#doFRWz_N -#8UZ1u;a7Y)UfNA`X`T -uFTt!Q<5(=JbLBJ1^db%vjwOL^6nd&}NYT12+qjnx8AZq5>$gmxhc>|ilN{|WM`(wpKwvk1ip#rfDFC -^}qVq|H_$R@DgpnbHLV0x`o(Y$8UX(N8q^={3l&}1r=0^c+}Ir;t>wFX-y%Fqmi%l6P1y?S@yjTwFY< --^7QgjW~m=boM@Y$vbMGUUYqtmg*#_xAkdEANQjz5wYgWwa9IGLLP0239YegEhbo*irfLr4ayhir4Vt -c2+aVNWwL=yFIGuNEOP}sw$BWL<@E+`T~~3S0YWuXo!g_I5+UWLlTCcaLll8ZoKO$!_H`#(f@_cE8jX -rAO0JSY63DLopM?RxSsmGvw+t;1$fZuNaMj;&~?f(lyL}vfu}xBdfX*DYSVp0@&VSbK%vkSfG;5B2}l -DzBk_Qa<4(WrQbtR*5**fy8@C11pmN+Plpz80WN19WC>W}!sdumGQR^|tB~7qEBOjpyz@cD%^PKF>5a -~zcH8?Bz0xb<^^MYs5^1mCGvE><=u!zHk>mhha=yVX2TWdvX=Vd -ASql!dN9tk&huY%mV14K!*Yucm*2384X{$tFkrfr;tN5~%UBde!yEJ|AhFk;%M$cpli%?F -wEo+=HLzt}A#jKNZs|-*@Q`NPhC@G$=pY6-*zsZ;-CJZd-VD_i?@NAcQkqiuX@C9rwp3V#9<*DDA ->9_89MLXIEUY}_$|hzgnSd+=LLm%wo~^+tm}3R+yJBI`6|v)iMt%Ap+Jp;0ZvNcvQ}Vlt79R%xTw(yc -LB*~2P^I1r6z26GvsG1W>m8iRua2gsb)TH=RX4)BS*l$k7nMr>P9sA=>PH5V*3XfUeAR)Wn!1_TXTUV -@0YEY(ZZlpzX|WnSzMe*%JsZ-LX2IMJKJK4t%ih!-6xh(3l{fF4UyRsmpLmB9lg8`K0u)Tjs|tiA}6O -Z#?G4f4?F%&8|A5P=GBk9?yh|Ne)w{P!QO&S&fwL<8h#(92VVoyrWhO>a8{qsI4d$>HnZ92iTx=TOt1 -DwAZ8lmKtUVSCirAfyN^AjC>54rWB=l}dLwAekenvo4JH$TjfGZfLV9=$mk+XzSXF8_*Qw*;1>wvOqGiHT@nGdl)Svf^Lq*Xi^26m*t~{mj!!P-d -o2{t>tj%o7Nb*BtniQO>pmkO3g;BQsXUld1uZRX`CUX -_WjYfwelODq4HckFlk=P$({o?}W4XVkUj$}Qyyh*eE1Z7D)+l@o12wGmO%ypRi-O@9Z8_@mKg#D1;j@ -Icu{bt@P|52p}r`pqyc)P=324)D=34O(}fwpJs&edNuYMm>YCJCnK}f>OW})VVMbrA1NutbRKb8-h?U -Q|-q#+I}OmV7FacuGQQ|2TQ&Ygxb<{=NRW7iDzim5laA*8O9{YD^Ly0wiF-;7JldjIWdITVea)aidv{9=9fWnD}aJD(@ub{Hd#x3sel2*hMacHf#Jc!?Mx?;&~x|RD;X0htoO9>J3D@?j>p=9<7{iy(NZ6H;~uqM_H5KMjV -K3gx3|wq9cH<__u%70&mFULE;@32G;%%3SAPN29wi|Hzr-*^*}q(T-QZ@EF@-O -beM1u)s6}C&8exMpthdcAa{!&#%tjh7_c`fx!Lrqieo*vo1KHQE`9{r}wR1oBLog7Bnp{dRQH#>e -XgHnHSjdgAq2D)%D)p2(&L1Uxe@JPBnA&$-D%jx+kWCKy1c6YMWdcjS9ENlX!01yS;8F)Sf5c8tX$DO -Kltf2wE#>q7gfpcrYFk@uX^Pz)Ndj#!FP!wEF*Pzva^E`xkcO;$tR5>AdCF7PT4g%7Yw3D%ZJch?#%^ -|Z#Qt&0{O4nK^0z@y#bK>6Wx%jvjz$vD~>5t*Uwskkthlo)c~{iMVy;7^+myZWy#FlS2(R9qTYd7lN_VBJEI -KtAgR-;~df*lSrRn}CWaN;2nj&?T6oGA8$SPe6DwW`FPDK@%ln5>qy -{SSqtZ?_G1A;V=rh?u3V)&Nbo*fqN6gb@7vp-V>5!lO`5flvjAdOt{Iw3#_TAp@ -X_jLA41`0W*=qq>L=@DgjKIY12~`E=p||K7SD`6FTj_#v14e#X&=L___cn>>&h)jB_@^Mx^F20!hm& -$qf^g>t_uxFjc~LSuDN|R0(yhZa}OW(Mu>NPdSsK6Y$;A$dOpr1cyD?F;Lk -6a_ywGI2%#XDvOTLJ&DO---cR!_#^{H3tw4Z5MmC7F)(aHm -ExfN}{F0iu6WF^s-jDk+T_muPE43A>NdhlRPeVL*&DRT)Z_z1r0Bc}Lp9syUtXz5r+6|F;ic(T4-VAz -H|S_sg#?JXy!#H&^dGbs`pkmi&d7k4~6wkP5M9mUs*YP;2ZZ@3Jz#A9w4lOTxTYZwhn8CXx8R|}`9uK-TkVj-3WBLKS -#P(6&57E}EVi+%P6@5Ef;MUGET{`#N)@$8w4d~ls(Sat~lMb}1`V?g?%*o`5^;B*I6-;~ucQXdW>fk0 -%jiQDLcTt}T`Cb5JWDiF*i#i=;X^^8sM_&VuEYR<||K_}XY=}c_NCZ6`lJnh`u#whVQwnb4RvZKI&uY -rRnH#caKrtO6O$el>*4Q+0?PAdwt^{P=>eebC^f~lOeTI(27H@luSeQ))o%|+w>tbEg@=`p))cE_-yw -?U&W-f(xwL(H!)FcC^`Z;d%eMcX!Wn++@nqg~5Sx(ta7G*>5akWse}`SCGbiVP_Ybc$thQTU@^&>ppf -YtOi5EEw54l=A$uqj&A`0G$I?wx15%g9+OBspFYU%(Tb#&}CRd9+YJW6E^tkj@txwp2$u;ckQN8)AY9 -jFKjD-9CfbvUcl-uJZcfX_GJ$PPSCKkm0fz4RLfGq!$JHE&-aHGcOo3k$PUV(Q|e_7u5=@M3B&ztv!6 -TA5H&f7i15?7xk0huK$-AQL@&*svlu+;r_TlS?XAAfiEN&09ZIzLGa4R!Cav^o5PtrvX1% -b1XKh>aiu9+d=xNja&p#Z&svI@mmyrVXHnKoVN>pnU?^2{MoH4}%n9A+gTm12!2Q{U120wK2owBIq_X?-2t0l5>q5a@*^qAvT3z(tk7yBx2- -T1%t8iQt+!jLw!^Eoi;d9@bB7kGtf#(`)-Bna)=8~Iy$CF=Y;!J^1B0|2?hEkib`bhelsCF*gesTpS7 -n{3Ye5V@8Kcddiq6T}=8d$)pyC%sg+t%X1Tj8pQrERxvjV{2BW;Egt;6*UxVY$xKqXg~1o -#sfJ6WA3t-+{5{3I@~}T@Qlkg5mQHb|F%B>EC}3yf=Y2&^A()eAovK{se{NMr+4uLvEne{xxvpj>|>1 -SP)iV*!2Kfz6JNn(^AfrXn=F%S`@WF_kROZbViCgof?p>yiQ_AO-Z#){Cg?QK*|T2H)Eg{wN?=5wy2; -1i`3vTJ*eoLjjSPAPDqt=?F`AD(5$UaDGrmRxQgV_HTroORvK`XE`#Z?VHgR%78WpcJ1)%Drub*YtTP -$A`raq-Exz2X#!U>k&M0<^e0Pgc&jwskaRvL-LcPJP1M9q+CBt-BO)`?U(gEpJgu37GH3lo*YsdTi#h -^uUIl|vBq}j+GaL%RL=wpC=U~Uius>^E2Uf36wIgP(j=r-W$t=|WsmDS4V5*N@gvAac{Gp@`DBq -S8JumMpotQo3tP;lkVJ#D^?|FndJZ#H6Y26Llwn^Dt-G)Td%!Uek`TCnLIcFA?5nh8AliG**OG+nj=# -f6`LaXX^4Tv(0Y{}-BEVwiw`z(bN10uTG1(1iPVYDiyTTroPl-ws76SmjNsp_XTMO*HTbdW}z#~s3n!O}PH{fV%ghTye+k3DfXF8qEdFx{uGlf5 -uh6lrp!ySijbyB_Fc5A-lu>9je7npyd}2?>gNBdcnA;!V+%KGhJUFUZb`sEZeBQ<;GjgrRBt4^JC(s=ukFI&ER4*uXvu* -BhHt1zt5!z?<9>1Pl|7nB;)^gVJBBXRB2b<16EW*Z2@}^8`Lw^HEY03#-bLA(PTw+JgXV&*MK^jb{SG -^AShtNk>sGkBhJC~pQzxk{{+$=g;Pf^Lh-qinDMd5zk%h5g2cYzID9Tn -V*5(?QMl;j%T6)xb!N#z&gq;v`Y7ofZ+j{t>LO3q2gWyujR(RZe(S<|t<8m~@%2KAV&wXP?i{ZQ}40a -38F!-1P1>n|CWm0EpG7a2U#;*j_D`gmN(CcGBm2!aPH^^pg%Xkz2Ve4?g9N9o%GZl+j&hhhHGf|k)$Z -wJIM^aQ_@pQ@tciJG2Q8R@{#VqbVjIJW64f0>$cPNy@k?_xR<7oC9Tsdk=TlazYAo6|HhOoh^lwM|Ex -PMUda>mY&remI}Zm18a+MY6Lje(DW#R@=qiG_?4j;-4iHGNOmJYiY2}=kyJ#CV@oaX}r9nNPl=bHkWX -pV1GWu&UTLBxzLn6%cG5n9<6wAq`hg`i`MKmcIL&Q$Hdr6Ozsyd}C9J#cc`%Y<9HL -9WbD)u-K=HDPCE2MG4BbA5FCWRdf;AMPvp`%ZL5jo3EyS7Jma!O9KQH000080Q-4XQ){t}Psu -UXv?{XOhd?B_#_*zk6r)K`tM5(q4aSedPl8QWg6E{#moQJ -v=!aBRQ}Sb3rM#folw8$i#wL1YnU-Oc78MgP71mXn$uLi4Z1?&T0{^~ClY|vl?7o_+?@21ljP9e7RvS -H%$|&ZAp&vnK1?Qt+5bX&HRW^PCpR8? -lyClG8f|e5jEzBHLJoTP7qZ0=TUN&$@EH0bVYO71#WIWI3mqq41Q&KpAu(F3Zkh~#`k`bU%^C*qCB{Bj6dPMg5lSG_*L-g8GiCWqDh=lxTVYb)+pTXYAIM -KHK7K8ng@q?#wac*Ej5URQcb!q=QhnDCLvQz66cJj3D|m&QaDR85u}XQpSi+&#%fTdulC-E9%2aTWqL7T>$ryu -c8)-6@5C~sLY0@Vhq8V+O@9-0J7BL`^G8wH9>@Fq1X -rH{l%ZZf44ARKz1mXD86}61(UvL;27G}GPpS49T|<#7%{5oV6s&*1d~3qI7-gXTSFb33TDnAcI#sx%_ -#YP-~}73)U@o%r^I{ap>&dNnKEm^VJA#=m7W=unQdTW?Y9uTSX6SCRvXR#Z!(&SloT`hN^v969zj8*5 -)AHB-lZi;s}2dr4ZtOR`?b||(2o<~-OaMc0v=wD&cqQopi0kvLrX;tJ*(oz)Wl!)MXgeiL?Mi8HLpKf -@S#Fji$&De<#IggTy5Y+!7GGF6NWK{@1#->vP3W_h+G*4sW`c1pg}N)1izr_wkgHM4Yk>o_)cYF)a=&WWGk$L82ZNlhKe7P`u>ij&}o?c -3>~}9Yvo%Gthe3vS$t%%+!rCF19DM-151ECyA2aMkw{*Pm|-pgf21biv$*17KgIO9Q5lB=ZGJ~;2q0XRU#gIiW9#12ZfG)of#)z2?LP9?0?&$&p~QM2=W2)uS1fM*)w42rRb@DXUKdX%VI)XrOAVQ@ -%Gwx8JBJ?Gs+#sJ8l*dc06P%I-LMuN^jeNE0b8B?LpJ;d*vpcg`QFhU9yiJ^g#KkQ*mVU-yU*6N}yM6 -3fF{J}i_^WG{*ggR9>)TOBqX>;}Di6lPatVCgDZ=4`N06=5SEV -e|+}8{VgmerW6ny1KXb1t+$F?-9owuQD`bS;Cr8nPxtfqQ{e -?c*BcU+Sz6cgCHzOdfQ=DAnKqgno*s@QZ)ex@E}jRoRJjn$%1^G(mVx$uOaYc{40~{kNkCqcGld3m;S -!(#&~q+oOnI-qxzDq{J^=N_XobDbQ79P|@y16YqvfC4H#8mXu0PYPX3FaQmG>=8vOUoNvx3NlBp2jw_ -~&=x@4ezut4I+K*Mirje-t#DWhIxXii3dH9@JdkY}ES8QGZUWD^&3t99IiB)Sc{wpi1n=GRnXSM%G|! -0!)EIQ7AEGIPd_tPH71)o*R)80IE -SiQu+aHxx|QJAixOXnG524{@P_zcm07rxQzZo{TZpa8aAu3G!E7>hxtH6}Xn=zZn`K1=)-60!*d^H34 -`5J?#~Xy0S5?G6HyStAP5iHG}9k6cCKrq#)3Y?dFc>;A;d;3ZgnFC6ws@2#5ttn{_&x#qGhK|4l4+!s>v`UrY)AJJ@2t*szBI8! -8lP0x&dg=WVhAW$1l0@`h}uJ~yX6?s8o= -O97_Ow4w)U(;iI8^?-}aAhG(U=p|G9_UcE|(SwwVO>Np}Fm#wE&ez9J -^9!4n%JsUmBG*v=-Vf7vzmrg=$Db62e@7^spZ(Epbc5?Kky$RW~<&0 -9TiGH|Rq!0U-8n?zd68YTIF57dng~6p;%8_`~!X7UW{pY&H?Hk#f92cR=3V(;Vfp{@Jw@pfzo{JPMEI -;dA1Pqtlb)v*XjCvy@=7R=sp3w9GxtV=ZoiOkoCy@>5J&a$&8k8PMwK4(GGWEWNt~6ylo?&-D -0Xr|-?_DD(5h$;smD7tZN<8VfG@s+xh&3E#;V;c-LE9B$ssObe{#$?Gq~?i+fXoBFZSW2QYQ>V`Rdkg -q${7FvsSYX??$IScyPMg?u(>4mlonorjV=zCPiVreRnz{RF^6_ze7fe?L6g)!fodAPw=FQJ332PjrMO -w{tD{8v>(DrUHcImM63<1NJhti)!hdtMvB5EK8?@ymHi{EY$ZVl_r(umcKPNY@!w#Cr%gtRR3pu@lo` -I;wM3VzSFF%`)4P|NjY?_@dEIbuJq1zi<2m$Jt_%T3}HcvRSMQCDs88gE}_PsKKgPwPVES5BF`W%_pnja-Q$w&ojW+O*JySt$<}*D8}O~?*ve -DCc`2}kwhpA!m}CE{d{ssoSGqT?Dlf;RuQm*HXDMR+hWRUv@%~i7pwJt~mp8mOYfw-?o6xM$UTZ^mDA -E#DgwIQ! -%rSEn&1LKTYhZVSQR$$k|A%y0O?@Rd1sv@WAZJ2fhZJHTVU7P+|r^1bnh=YJRLKEy@RO&r$gc{#-g8ErsNB4FPcUpCZ(&++{D1TjupO4a>!iy(EtORWv_i;U)H1o --T&N!fKF`ZvH5E|}WV(J+>(lCBs8TznEm>8vCskpdAX4QN~SL2q?&SMXgCgq_skSR2@ke>5eha@`}m*{{8kCPIuQr(np -rqbv0S9ehSwlw(Yc0ou26Ii<1=xBu45|1$0Aga3y#rSs;0K~wtR|E#9|6~#>OC%=NN0W>h-x0x-N`~y -%+0|XQR000O8`*~JVb1@;TK^Oo4j#mHxBLDyZaA|NaUukZ1WpZv|Y%g|Wb1!yfa&u{KZewq5baHQOE^ -v9hJZp2@Hj>}JOl`6{GE882_B~B`KvUR0ZQ5+JZ!HPrB1Sxsq@_)bX20#ELII{BCs -j|ff&}eiw`h^CQB>5(<%Ze+OR~7sD{4HDD@>U61vqIKgE2||_ebdOcOOoX1NbK;ERr2~;)YtY_Dto22 -eJ85nZa=)}+o}@lw8>k=k2bB`v2Nc02xi_dju`yj{ofBSv-59Wp8xObH-BHzqv!QrU%U`Sr>|ajyv^4 -Y_wG8`>bq}+GOfPL8yrC2eBX31r@Yd4@A}5}ez$MT)lW6FSRE~oGg;~J;w$w+g7$Q5J*s!?u05rT74tRlGpF}2Q%MX-DFMPZ8 -QD>XrT1Nbza=`O;!p+o>Hi$%J@J6{DAZ+N@DeWB5BqKl -qW3mclH*rDuyQtMeVtG7!<{eYbsGZzQxMVZN{eexbVda>asM1Y7bAG84$PKp>|KUM$)1*K9 -3ir3r-#9RL2vyS}Yi*Y=#j)VnQb*aZ-hH;Pr_hO?ag^y9Z_&`UD`@Cms>qb;>8ZEPc<*c(}MJuHcn9` -aRQfp+j7*!DFLXxnv7vSRdC;l)N&oc_Y|OZJ2T1yd3#>ZHu1Nvz;i*7EngXyHvw_$|yhjl?^m#|mz5% -@v5{zj)DQ1F=@X<~7LwX-Hd`Fiz!&XbBJEqy!QI!AL-I(y|^h>5Bop@qsOPdY!W5T69m6Won>>Q$dJ}zUHdP8(uE$TVip@QGR`$`F&xk{%s0aR-pD4$uV* -vOO}SB@1fx)`DiEc=^0$!etHDp&OVM|eo6+>(A4!r;yrtUfc`_jfgZBmpMBJK(9f3>Xl&TM5p}86U?+ -!>S}D5>S7Js;;jLzWB@0r%1N+Tey4O4nxE*r$6^upn#D8Ytn<2kY4B@c#o++NU#TM6p-%v}GTd<5B-; -s78P*)a{tJD`7oCxdWz$Aj%a-O|6Z)<402;LFV&t+egL^q=P7#b;%VJ)!7iY>Z -U0M_WdB~x(VZGnOrdI+wT$!gLp;AHrE8Qir_wvOzwwcJp^3Z&9v5Uv%AFnR*QJg{Iuw(;-4RBug!)jK -MHmx}Ci$KS~A70N3)PK%e;(K3TI&~Vz}=e!a#9Z-2>AgiLwvW4Q+X2q;s@bCvy4~BS|4lSXLBNBE)4+ -W$Y{`ZEz)mj7;e6YA))}Vw{56c-Ha8%^Ss^Nv$hyqQKAU_<$(X5~q@L7V84F&)@Xg=uoOzKgLMIrC92 -IopkF|2%zj@puxtx@bb3=QI=kdpNVfP6JFS6U@~K%WaVn@KbWAFOr{i5OJI*9Pb@8jd2gTtgqkI+-cq8P4XaVksZJlk+k%Vvd(I*K-R!kO1E+cnfkQ!)Eos1HNP_kz=i{aMdD;NiSzHfoI;~QHhZfn -ioPWiEQ~H*v31`W1rGlE9S)H?WXHAtiua96EAvz -F4?_|q|(TL_?l_>*7q`S>qA$!F}2`>+lcKMtd^pjX=uebtP>A+P%GV!`3lu|RG=M{{7hGj4tz|^0OO= -pd?me0e1;OWqPIj0pd0R*UdY6vrAQv*F23C-4GG#n*{)M7b1n(@iUqeZ83VNkPnj+F|wBI&YdDpcI=nN|ash2E6EJvY6 -a;}3sJ2)i`9%C8>K%_3Q#43vH#?m+c15zo^p^*<0%mOZd7gl2(=8PheBIDe2`TLU6BLpF5(GHJXEdaaSk~JMoLg1NQNQrFwa -QIHHzb%A@0aZT#H*wTH5F_ZYnTQX2srVY`Z|bw^RBJ+F@*xA%3Gg#+l!ryazW0os*e{@}A5OfI|5AL( -MJfo*$5@7aYW6Cu*)K1)($QZP-z(~}IR6=ckJ$NEO2w@&h}6bQ+ -Hg4DN!59bWN!s{7L)^U1oc26X^l*F44p{;WrJ8?5Xu1s;;M-z -xB0EcmXM=2;I=jZthRd*8j?tPN)4?bVWhwnM-E<(C+X(n#FbuW_&W1tYnOjm_*7o88@@lQcsQ>iFMDC6l(Mbaf@~&<(rCqXO*Bmc` -K#Oc^>mcX#3WzyU5AaWj}1-g-5|0}`+B(IfIkaJ88&B(QkaU)9dN7=~i!o_jiYGkWufJnl42Q45(FJ{ -opODfdv{idNPbJd)c>&&gAx>@v;>{*Jcf7G?*buNAP=fH*MZC`3M#BX~ws-0^NJ%Rz9oCdu-$r2QJ)L -(?rJlxQUA!Ni& -TQWNnE0ifjhi#`YMp)NN_H1-)g{HxTLhFbTBok&xFhtnUSX-sM^Q7eATt|j#L=AEluY1|s7XIualZtFrK46 -{$ho9}K^@!ImP%P8-dYgB4bKN4h8c+)5)b6IA=|9U!zK(WuX=#9J>_ib+8qD}1d31+>vGZZ4b>3xZbD{8k_j(6*7Lnn4$I5wFxTk9OYKQ>f4HP%B??(GpD`>5{2Utt -L-%j%eLHl2v#4UIld?D?1X#H>Kn{`D|-xh6x%(21tkySAj -kNi=xHDfgy~|01)v&^g?#A0%u0ULszTV<`~F$k%gXGzQGKMCF%;d#~RUiIuA8YtrcaFn`|Q5h=Vpj7q -`r-`$Un^Plo%WyIQ{28!Gx9E)4Tq#)D}YWTvT3N -gla0$(JRuzlYj>w`u?Ve+!CWHoaTc1-{+j@l{&G=+Ua}iPZ~OJIsZp8WPQsH`lxTUSc4%>mp6(l_iUmPvqrhI@3XF$tMNVLkkAYY^szs6>A7 -kKnC)?64Ru7u$I5>`!m~3T#y|p!PUfMBH>s~teSyUeW{A~Kc>^j`D54J)WLa -iq#X!7QveEbJ^+%uZmrJRG#yo?74H|M#s8bnM7Cv5SM&L=e1s;y)66Tiy9UkvHNz5qNmz`C(4laZA}! -)AWrJ&iZi=BApiD)j&I{IA-sA-tJdy)8^N@NXFYwYb#P+VMz^`@Z?D{Y&hse6FSJ`F#U|7fUfMBWU!L -Avc*w9{;B`N%aunwDITe9@$dlu_6eGjobTlp>4nd2)ANSmlsdnHkaiF@-b_4wTJci$L -id+)i8ZKa_c)3mP^kPp -Zb=W4a46gwd{YXw99BjfoQ@TkRSw<@cUoV7iSb;~M7xMXw3gVk1qh&AgFjR+4lDGFQHtMcYwLP -@ag@W`Q`+9y3lFdRqfL}ZRJ#+mIx+!IQKQQ0aA2p4Evj?cmlv&`*O<+=1}JKzP!sB|%L$WL7U*F0&ph -F=tq*fPqBq7sXN!=$VcjW}#i+YOJ1B*t8HYj~v1Go6v-F*Go-3D`#2TKs*)ui^$ChQl#Y1baMg>ucUo -?ckgP6_ny|I1tNeyxgaXT(=g#vB0-a{PxzL&J+20oYe7eGHz33Kf>4`E88q9ZQg=4=lxsZci&bWU0g^ -nKTgW|5p6OlE8JXp(AhmY$mM0WQD%oIvbc17Y2r)Y)a2yw2qS2kK6Apbo_~P?RQK7xWao^-^p$9D@6T -9}YMv{Wj*u(LV8lz+?A<=YSW-b9&0B@g|=?W(ZoZPFZXB5Fv=~qe;Co<5_q?t0VKV;!eOqiXm3 -r5nzZ}X70BY%)#3cfA47q@Df5p1VHEg@z@vkeS1m++`(rwRh)awpVv_NChv4eU8|2?lj;QJpD$_~9Oq -3aa-{+N9Kpv?gUe3ceAVoPY3pGhr8dG^x8f|&Zud?`ti)*Rz&h74wJLL5%#K<@%_leOx((>A#RcMV|q -DtbW(8=DNEVf$uY+gJjco`n2x?Cpp?VB6!Z~V2jmc+-S*P;0T#=Y<*5yWBQ -X1WBtZ%of)`18*~pdX2+GY8dzoJ|vo_^<<}^WKbFwkJuj5E;ambPE(VQgmGjcTbvFteepPYplTrgQ1X -$2>rrvvr}uyUp=>?dc)Q`0w*Q+(jedoEM73!-@Og=4`%@x$gu(<-oK8e5G5{$1K(5CMtLbLje(qH97m -{$kohPL6MpF_E;EiegoV9^JUMYS*rINQdLZ1v@^CHKv&KO2eD#x8XCdHCWp^sOe2Y3F+AkjRKRjqX;5wmm-va-;qmu%D -W@E~mdujK=KT2tNEjv^yZ -1X-^fBh^5#|Up)0H6Z+aVED#xwjIG&X14ACnZ!%yc!3Fq#^DI3~p$shQVIyWdWNN9={fWld-Dg*{VcX -aXV(iz1z+mx^;bZXpvL91f;Y(`8;O|3=jt^$%vPV}!0IRGuTM3n*)J&ts<@9vTgtR~+<4KNMHEgZrp{ -g90>k5F_>N3v7tqHnIRhRS37G`+mS5<-U6--;D-}9Cj!2>$N;R*+2w0A(cCO0Qj9bcmF`az -U`5#i0Y8mq`La`q)B8(okcZ@Q}tvT=y+6f4|5*sHyx7lo-Y1E>Nec$FRoHj_~ -`23Ds^ -*%C;`L37OY5ZlcX&+Gg*(L|!C^O29Mnp(Emd8y|Ae)I3(;TE1bpo -SvLKk&j-kP20Q(M1Fpe_CI=kMd2$Ze!2R(kFTBdKoIQvA*CK>>u09UR%Fu4S-j;O(iHgKtjW-Pi{fim -i_Sj-8X+t4ioVQJj1Azt$mmw;q&wJmv4cDt~HpMTJUnzoU{lhi!KW=fj~XlDjU94KxbTRF|dh;2joY< -q%ASJ_TwaLy%Y_AdSP=oJZ1Cbpt?Gjr~w2zL6v7T0r=ilQwzIQV{PSi{NIV~zZ9#|TyEZQV*ImfC|B~U)8Y?P9~d3;ct2fz;^Hv1+|g*@T)34J%HI{`OU?_J -{iYyZqgiA{FYZskDBI_7 -M0yRq3SWu%WK&c`ZZV9qxW`2l&!hTYk$M{HPyoHAZnv@%bMH5(vs^cZ|n9lwS$lf7d!6&rtm)Iw&PfN -FVs$RXZX$ye!y{5+lr;Apj+un9^jGGv+}p3;$5C%b*ZN{b3{jLAS^DmMX-UBgD+6xims;xJs67F&<~k -DhsazUXuhg3{;-{Y$nF0DP)h>@6aWAK2mt$eR#RJ|zX2x(003kX000*N003}la4%nWWo~3|axY(BX>M -tBUtcb8d97DXkJ~m7z3W#H%At1ZXpjwGneM3sN9D9RkQ6 -F|Ka%MPu^X845rfJftF3WGJEsYh*i3PyEfhInDgk5Rjp!`F#*3DyWV5)OL~CJM;Y>rm< -}JGOWLv4TBzdDWqNvuXl7?XWlU;3kU5Yh{!UO|LrBF@Nd%4ymB*G3Rrqw&xC4E;)4=OV%m4iOQNr -^qupTWRoQ+t{0Z_yy|^#DbEqJGu8{ac1HJ}^7x!2!}>;>_4wVmtdqVTAKlI+$s=)TwrxB>AV-C-YTbK -do!Hi@s6{Pr3AnBMw$w<^^!6CV;TlpqgJ?JaK7h)JTd8~E>i`6Ad&VJx!E^!o -+tt;({POJ|KyhG926plP#rO>DTG`M7JOoa8zFa1ON>nM_M+(OzG#RU$W27zN8s&Zlnh;yp!iEG4V};ue -ZAxgg^Pvu6aH=WEGkg{9P=Wvc>of5~W`~T?S{mE2#70ZScOb*4RVPN8&wH2Q2y2J9Ie;)Ott^Dq^-H7 -c2A*^dq3&N#{^2iw1pLf$Wn&8vq^Z0$e+zppeRoh9soP*V2pY>mlAm`p`b0ASCSidJ>5bLs%*oMD&?k7eO|fVq1-iG((dV2)DTQY0in!6po6K~qqoC@f>7DdOrDmc3&^BHYXlDKzAq+;C^dY_~ -ylgEkwSVj%!u70y$e?N_+QkvNdW165eJhU`8Zk@_Q8J>5L8^W)xmEgiU8lPqC; -5H;sFqz!BXg9GG0f_|Bcr+maxyUtR7D12FasvD3Gw@G#i&P&WE*I)0W(6H%)Vjr@Py&n!;Be3{799wT -3Z>43Px=SYKH$jKiIg6oM2&P)|F+PR~%rR1k+&`=NK#Es8NL}$H^!`(ZpNfqT-!q)&*O;wDyBF3t7EwO8)6%mvxh#RE?&oAA!XxqPz{7DsjxRKaQGi?_?d5bfGMHf=*3=B -*J%o$hIUu?RBpC_EPaj?~!&v7s{<9w}pCB@e{jn>NKIRkC2(_f5XeI=20Q?@<+}<1Rar?nPS2q9% -amt-%3bZxWQJQm5-iW9T;WWP_^%&YWGh=483{jedU4o?mdztf4EQ3-|A2z18=GJ2C)(*3pSsCyN`2_n -FQ8JK6A3`V9-$Q323kcWOl4^Jgzkmv1&XrVwgYoVne|TD)zX*|cT@*VB!0oZq{V#o@^>*!SDSTb)Vj# -9~19=kK@7a?nUq@trB`-9XkhdOgUgGiQkYO-I(7`5qZwH=2hXmUTObd+XdB-#O*nJAtOUoadmaGtSR& -na`oag0bNzV_vG_AxCECbmV*7oX#8m5ti4uD-YlQCrXy?Ujo7>UX)~^{OOInY4qvfKZy -K)P)h>@6aWAK2mt$eR#T-cbkP4R0017n000#L003}la4%nWWo~3|axY|Qb98cVE^vA6ef@XaHnQmN{w -r{mcTXf&W}0;S@w9cjZtS)`P3@f6Y45%yE7Kw*Gp0x_N%>=Q`@g^W06+i)B{^}^-aS|6v@uCwFc=I5g -Tc(;fp}UhZp(CbQHv*^KK^_N|N8>}_oX;WOZa#p^Q{M455%){BJ)ZnVwoqh6!nD^dy9Ai|EMoR@rx|0 -w8+Ji=u^?h0zLJqH~1?+xGk1q9^Z<*sKv6903xl#G|i-tHxs$2MVgCAF<)e9oKK{5s=RPsOi$FJ9~&ogN;Xh~pRH>G9G2;pyS=5j=e__KyB8emXqb9|{R%f@b8+qD1@w$ -rMQ_6QtLPlnw&Zf)T0~a*|Hd3G^$UE#sLKv*JpYIdn%XWI0bO9LNgLO8`oi&eJ-s=}oIs(biV4*V{UU -H)m(myW;Fj0KqR2Y~OlU)c~#{9G<25@&420hK?PATJr29Yrlhd?#J;Yx9<)g1KYu*Ols21^OdIG51h1pZ!R@kGK?P53 -{*tuKoFA(A_trFoofjTX0~=`x>;&Y)zC5Tgt+9@`k72`qwt4F$Zx4(MKsId))P02k>pahBG%k=O$&WH -k&9pm-%|bO{kA(7Bw%b18B;fzgl4TY7=Bi&5PAEc#p|5_n@0Bg^reGf2RIsDk3N=qu>v9M~X1yr9WqC -XuxwaadPd03o3^p!d4I2y_~|dnT@NBr+uYt=)Q+!o=QE4L*s!p!Q9pxrIt3PUOlkOU9_P1W -62si3$#;=XF1F$cqJ;DD_{8H4i;1AiIZRua5;P&sR5QTjODP_I_TSt7AOkz2Y -^;B7g%QqBapN7zQrQZ>EE9pjGz8^@D!xc4?vizhCwGfkCY5XvL?mEA?s%#htEr3*GCG>MLo|Rgg|cT9 -*l%|0J9~Y&EpX)Arq8WTLOO5vY0DugrZKLKHzUJza5%~r*W2{AZXs~r<1yQI)u)<-;WnO5t}zBus%rO -;^xh%yr~b5t>@*!261}3@SYO4)t5O=MS2`;(LZV(#bPj|rb^SRo>ihZ6z_WT_-#@4Mj&F#q3F%i+

Wx=2OD|;*#H2`M4ua4B_?s -c**7)Vo;eWxZ`ThRk3+P=@MKZriOW;6|YyF<}6-d~7BGAD@h096 -USNJ2@D`YJPCE_wxbH$)bWaD3c(%aAJ66b5=W}bQ1S}jL|aG4FMi&8cKH7e_K}!}`#?VCw_cIKSoexDo@5f -}4#MD={$|Bs>$kg5)+iqE(WP=J>t&gf`iJnabXJ{@H7!shfHmubsckjW-9~`qOl+e2??LisC*L_V2Q5 -REi!!Zb^H*GTjKhH`qI9*tbOWhmc}3Ewb3n!CTpQGm3+R`rd0mv373)4&y}XbaSAOy1!87s2P<$zVZH -k0pG_8lKpU~HEP28RUo8%yULe*irMBk6#`<*z_Xr?d^%M`UCUezvm`Uw6NwTex~p -+?oj@01I_2Y>D#o}9ioc36~0C^Q(+4UA^Ql0iI&DGn+$(P>iTIHVoK6#Y`-0wog==&C_2)M33iJvcgr -=U)z9oB%@4_fAg_UK{~BrHqg{006I^Wv~D3=+!?4Z?<0#`qB2_b${@!*oJo}r_aRT+y1N9N%YC9ulKQ~zFu?h5`g|=Rc!sr5~udhEDyxJMR=_@e*tybv4|LGA5-#N{azI_ -f_@C?`(#_9S(<^lvmiQBCi;kgM`hd;o`Hpra-R!q8HBG1z}*Ltl|J$fJ`%`&Q96>{#q3N68^Flf(pt9 -%c1wd&G;i3WgqoMzU0)F@z$6zxr2f4a;v=gk-}!i -{wCX9Kq3;)nFUZdwWQLpqd#io&uD+>enQfonSBNm(|97;EN7V;uz@m4H14AP6W`7LG|=$7DSs#;J@|Qr^Uqn&t*u$%q@YK_w-tWyKZ%gh1e`Vp&d5Bwob1(;Lsq0^e(D2 -c`opf=N1^O5#k$hEYZcEmKBC$icEz!E3A$Ps*Y~lTlWui%j)LW0KYc$rzUUjR_@%g%`~*Xlk;Xs6gCA -14W=cXbdfiCCu9*SK^#;CGcz=gAuN|Q$c-$AhFJ2lpR@lx(*_M3Q%EL?L^CJM`{4?^w9zbRRsVF{-fO2pS&{b=Qb;?D3W;q7vUvgm8sZ8tI`vNM1AtilYX`1 -pH|TYN6j;#BR#n#PIYfCX3{YR23i>b-GArgY-{l&r-`Fq7D%1DkU9%}B<;)7Gi%o-1Sy)f-MKmU2fk81B5po)zwEbjq)Z+X7V>t#=K;!Aq8RY3D;pB|OQ -k9xi^zZWw35$NsjKDq1!My04s>8J2`e8QimGJ(CP`Sd+KDoqh|LaT+7mnR2mQ);zhZxf3Jumnt7RS6% -l5`T>6@xO|uD|_^4BZ-Q$)!ZIDI(^8BWkS=B3O&U~#zuEV-hjM5FEoq;ezalCkPfDR_(A5Ug;O9spN| -)~dAq!tL$Sl}eiqO1@uy#Y<^u2@$`}anp2RU0{FgW()w)u8IzpWTzj9$l^=EkI&vW4Cw4wXY^r%1O3G -74l$Dh@sjVCVWStc_zKF{@|`kFdp`aEsMw2Z5T#ZoWSqZ&UpLg;#q9iop@y2HdVQ2a7UfxdX2Mj%eiq -m4OierS#F4Dcr)epr_3e9jG~ecFlH;MT -`u?+e)ZgEKX0ug!N~gUS@s&gwtj`!Tv!b|MF2qGDOPt -@gyfUB}#B5Tk(e}1ls?xHmc|8K$p!0mb?HFW?%vX9|vmEhRu1plQ5GM*J$z4gt*-PJcTwfZ(trJQI>s12Pvg(nBP3CA2_sSyDLkt!9a-nbu_6MfBcU -!xIY5m%M;*H~}Rm?_WYlnw%E{dao;FIe$4MCDT57Iali*~bNI0#wjk>qlvBZ4_C3(zXZ1^(~!W>=D1v>^B=1oVjgKTDRlv76_hH(eWG@4NHV -nNOxouI{+(c|dx=bwHi`cGk^4Rlj&VQ&QcsnxCblIcpG8({Av(?b*=X;b1(KxU)SbYr0(2Js7N>R}5rw9i#_w_thyRM*|p}{l&>t&q2%3{=(_oHoGyxiY5Am>OV3=WjEkm2Y-OpKpBl?1TA -fn%BusN--)z2Xlche#3OH{?u$_%zU2INDZH3<0Jkb|)n%)3CVS84TXn!Ng*%)Es3{O)##^h~e!0pD+Ode9&LMivryVgh8`ubpngM6f -E)($*Ue7cC5G~Sp(%V@jn3po3r2@Zd3$= -NRE0<2)lCNijh)QdHu%-qTg}@4&)F^L;)cRG}+7tvGa_)wq*5eyIJ61Y(S>pxGTk_aduD^Pz+w5TZ&Z -}PFq?FtZhl2bJ(UZAxFx=#%lxnX%TclFs!09Bly1qJ=3RSnUT?@r2?3^nk4L51}`3nIMU%B6hPh9y7L -;2;b=MReqGkk_tr*Snx=n$7D|1*ZDH4u7;>^c5{BSfIABgc<9Ku+6gd_dJH6B?Q -=dvX604o_NUMiv+_zli@UX$#a3~v{K8=}VMJ2OaT*$S3b=CTqqRDXKoDv;>(y#HPIiJ~V1XuG8a)~`-)X(mQ_ -dp86o~}QJFA*u5AaCuaDV*l@Fz$0vrkSe;GkCl!PMFK*H*+H9$f+G%eq)lz=#g9d&FLR$s?%wr~yRrB -j4ch%L*Q2R#JSWcfogdQZU=+W(&uCSu@|69hy1cY`2W+s7KiBS=ITOZW29-x}^ZaJ1T0~nB0wW>u&nZ -Zn%`N*SfWuh;Wn%kmAgtIn6ewYQ3AUz8HV;mtMq7Wlr5JwDSbLF_x1Ho=-Yx3nRG5v>9gt+Nas^c3Cx -!ShT39uGMtjSFy<`zg^Gb+wS0z2Y9EpeMQ}WOaF?y($F#P;w^l8UeR?4A_3zmwIR!G&o%KQ}-68!3ermyyTh=m?HMSk;;LCJ=t* -r&xuRWkvWAk$t0>^I9N($E@b;Bo@P%Et?r5(EVukg&|4IZD!qS3L@w2L9xTOavDgbF6L-Ez}6onPqm1 -LuflNvO2?OX9P|Q+N-$4!!iq@upjQ)T^+jB-ixEG!vr`*K5%Q>hWBB6-4?vnn_tBi>$kuN8+mRmW>n} -tJd3*(Qf(7QR`s>hMo-ylcg#4MZ=jx9V$-$H6qOe4V=5EWCQxV^lT6SWV~aX67{_e!23FtUrC|}nZrD -tP)(qo`qHSH?qGnI8HEN1B`D2*02So)yD-Ey_<5d8imBeh(Oso3MO8I9ZX22Bqo&*g+0wOM!^=7nX;k -uT-Z8|Cf^Y=d^jYB>T??>YRqR}T*xb=z29@on*A14`pA;o -8|8oq|2TZmEmL_+*K -gC?I4LEDB&7m62zH3D}bHqAV7&tZ%ne#y2YwksfQ6bLeZ#0>#$DEUqdaK)*PDD<}0})UXGKqnn-s{(J -Ue+23?kHdHXcsxiJrLL8(2F(zCZqV;hSXIW)AEC}0RVT_(AM=M!dfY7{gHXfrgHy)p%zxi~@#uS1rn; -`1zG|PzWrQE^RloJZ08QDE)%vU&9#-Mwp0K8wxY&sNEHBa=<%om;S=+&#e{PxWdlOesK33OEC(y4-gq -VbgMrn>_Eq)@)`I8+D=`1s6ZgAR-g{O>xKWfxwUl!>wpy)qcVA5K|x#K!~0^S9b!jW`cQkLdL7HZ~y`uV>X}jw~-#2S610K@J&4;XSJ&041j-t&zT#^AJ`6uGyLexo##3Rw;h@ucZd` -x?2qG5@XTOA$35tVl`J#4Gs}weI5XWh&iITSTE4f`;KXQNL3T+SC;hmbQM@AgSH-n<7*-Z*s(FUd#zm -GUbO6SjYh`|`U~xdsKT{G{3C&PB=Hu^fp$W3|ZH7{~pO!$XSX!@-^Y5n-%xz2TBcTY8rn1T*I4~zD7| -@X24prO+GGCKk4;}hcRFDb?u<)zgf#9zHSX^U55%TS>(t4>9DPB`VhjtD$De45~HRI!63`Tb>qXbT$R -MLRIudoz0|&3ZO37Fch7EMycpqYWQAh06ngKp3>O<; -P|=0MWPRDfKJ2St1aT|oV)Sy)=S75>P;-!KSjSadu!5>I&|(Y!+PIm8lc_CItqPEb`)zlElSk~ysn0n --Po{waq#{fFr+Cr#uu{}wRPL!U0jz!PePREBe<%SA3>y!qj%pU8XInjOevlc#Ml&?^Jy`f2hVbdhGyJ -DTpZcK}KI0prbiG_3a)N_Anu(hc^jwbqP5XngGO8Qm5>_% -j6MBfhZlR+o<-!Qa7<+I+(uP^AiwZ7y?6o@!L;cswWs8~PbI=&k1%jL8o6M!m{zOcE9Ua=FH`==o`qL+_lOwz$u -DWndTpBrZK5UtjR4nFhqv@Z&x$xPK2+;w`YNncS!a>g7_Pu;sUo;s%-qS?!tq6n$t)eF$Mjk9W->ZEJ -SMBk*@(lp4j&z>R0X^fXvH}TleMoyCPu+#nk3Z`S{r2YoS$^#ryY9CMVwgeR0VmaA$t8n(F9QB*muh8 -byuxw5JvZ9o}y=g#%|9MiWf1ax0g4tIH3rdT)@ChPyrRnC60fj{$djBrl`yTD*5#3Lbl#dh6OD(G54b -S(=8zDiTYkt5q2SZTcmkk7aocpWhlm(W?;z-NNVRfc0n$ya-5~-3T)r%gXnC;WHat)$mBjY&;|{q)2Y -8nb1guGl~;l~?7T5mBn2wl02GnNAsAMFY=RDhIU!vOk0#U_uq~^qPvvl1PPM_7^Y}m<9iJZXJV_H7E7 -cC93$m69Q`_E#DYL!J?w~0uXtf7~e`pv$D`JW{I&q2LdwwX+InPYeCs -Y}-*VdQn+jL-q>is04o?lU`_3=AzUQo4DrK(n?u7-4X4z4W2!>fNz|67b5(Bl)nvyS21L{dDlh#5CGd -?^v);rstRlideHu -PG0$hhN*T*USF^a!du*ABIy=P(NAvhgr5Ouo8X$ -Spz(Pr+(+>vbvCW~a?TjM!C4DkX$d=I%}S>c-`kE;TsHYX_g^x6b$Ov9K1N2eg|t2vXRX#f?tbk^Qx+S#6t6yMROqUlf&k%s7Cw+L@!QMI|tP -LOj_mP9cou8KLNSdVtR_eac34Zdshavs|7h}MKkt@d&Qj5=h1M7UUf9wHLfsbnBBcdKD(2D4;9D`|U1S -nUo)&zlr?MkKt?42TdbZd>HulvVGDVsr9&@G(`_`&SxYy1lM9TF= -TzSy2CbrUKd@?5(h5_-k7ljSo@|jOFyzPtNd9cj!&zvqk`Kus{Acw)T4~fAOGq|dB=WSy*!s7fkSZq! -=oSsWgqYqs{lQbywoX*#^hM?)>3h~sqw*?OhCD4(VzzV$xWZ*Ztto_nTn6Qgu5|&@hs9mkFd~Jx6_>a -3aWdF`f8DF-SD{?8YvnG1`1Gh1Y2j6}Ao9MrLZ0#@2#Tp)nW@acst>w2brBx0TmeuQ}^LyXGOjI%~Zoo=@ -Y-tD))oq)H?BqhPK7C`+!O5F)?{sjo%G@g*yqMVcp~GW~4o-rS`zFJxyfN?BeJ^VFeR}`ojcxk7ly@@ -tgnGF$@d`B&61I1-saYqPpD_aWHT6D)CV55`}b>~@%-mCV@__05PA0CF>XrJUK -e3x_t?=2BMDr_MCeF|apO>T?CMEdbDeseH6Z-mf8Ulv`=>NPl-Y|mxgJ~R~$|htERBI@0h3Nl -z3G8~G-ltS(y1xk}o6Zir2{yxp)n!>7-n6aZ;u9+ih(4`yf -ciuIP9*{STUA3RCmfR43}YuyyfvT-4d|)7sSl4g9oFXvp9M7n4Bx`FJ}!_dkHjH-pe=BHQ5MVDg#}o7 -jt5iQI==(FpabiOIp!uk$JnSPJC19M2Sv%gJpF#>EB5`S;OSbnVYLo(e=qjK`G@v+7)|{ulQ(2MuP)% -Pi2l!mHpE-k(i`u*or*`-7tjc0P##x{EXAOCEVgA0Gx{nMG!>Y;a_9yTk2>V -bHeR>jU&Uw!_6cOE~~*LKTrm*|G*DP2NBhe6KpKhWc@B0ge{84xP`U|*Ub1Qk(NVkAd`5g|>UWCo+alWMMiPZ&z7%E2>=U{j`aY__fvl_%&bz2z&F`4W> -eS_zjPXvdg={T&uVjxYdAOWl~Vn@p)Pv$v=>FosjckKF4poga01(UsrgXzyC>Z=n=d3CVWQUo- -BqR)prPHyi@UZ*$37gpCBG%bw?dvl^4@hc>xtLG~VW9%h#W&@Vd>=y -_=Rd*bQ~3y!bfVA(SD0JWlW^&(c|&4N-xbz*P?733i*;7?2pF%0+KnD6LaZVv;%fWA}R&5zxb9Ck`-F -RVR2xwkxb?Rv5?0<(SFU2Vgi9ARfwHg~Gs_#!nT2vD1`#09QtP1kAkv67@cJZQ|q3{_zRvghR11sx}Z -c*QTS^7j;^zxnStM`5C-@yjE#!&UnZKK}0HJNnow0l&>lJ@P)O)jDj-$M -qV>lx*b$-m7u5|wJ|1_({2kkjbp6)EGn5!tWv-au<>qqepNf+2(HeR(xNU&SXYbTTG+b3baP(-PZ1>} -HyK8mdwpFI^Ro`WA73X8+w~J2*#ckD&PK1?9Ka^w>z;f8g=Uy-PvDhvZ*b{VoDfi7+L>KB9zTO23C}a -w~R9r4zMpTPu|UGbf~z{iFZtAFr!7pA0}D>%V%PypG^+7#TtUcGjBl99PbOLdLpb<85ncWJob@b>iEf -_c%)mnZ!K==y6NwV`5`T!E4i6hQ*}QSm_Nu06(n+VhiE>=OS`p~1)9tEYbs)qz^-oop+nxtuuw}kqGvuO>PdA`{dpN;%+vP?eM!LXgjA{Q3YRvq24g?eja9% -@1@oSg49Fa8;?Y>XFEt1*8BvD7*l{(Cqp-02J>aKiR+6!)&^`@q%}wx!p2Gm}PoD6VNkjUVXlJJQdt( -fV*rskw#%#ZPm~mWGA4W3tWa7%ja%*x?|P3zZpDg=A0WrH3rTg`XCYV`KnV^tj_ -Za4<~Q2#7OvsiCunSdeWLG}=}a(4wd+jJJ2kL$885deu*lGJd{ydV28UNCgjQy0&eF#^Pg-Ffv(hk`> -E@LhAkRTHqVit|d2|!fFw*0eV)%Zu3Y8kZmXN^kCyLX=<$yi9zK~Q>tRFM1y#emQM1UkU`{EOxIi1#0 -{VV%eam?B@9Jo#duP|xoO!fg_cmVOfRzb#^ix!R+BN7xb?3>Mzcsfh537YG8D&UJi+9#2YC4<#ap333 -fgLn#5w~O2G-NY(K9*~Di&%7mX?@dy1g4PB#E$#{k!R?N==%_x1yL#mL*T0M?gxQc+WW4qCt`6*+bmf -1wd7l@!R;`ZaasL70?FNYucJZ$AC^{O54i-BtifO%M#!^y6#pG6HzOL%9o|a&7bHDjEoQ19loP6;bi6 -A;&E`cg2=6yQvK_lEKd%0jM*hz2Bp%vGT79*G83R)V@2;O#aH94D>Xw{#kx}ScqY@eu+ZjE#+PgHr*i -4lX7HdKiKPt+zm3baJt=5rt}EqE#ja9Sw!Q@CqLMq>_S`P`#(9HmZ`NL5Rl|D0=tsMxzuFxBaCH2lS# -(aqA(BCB0{-r05mRV(tRK{0m~)iBab-fYZs_>CoBpF)48!VLcEi}?kM$fLg-Tx;346|pPOsT~rga|`) -z=AYMGh{Fc4yr?uEu_+KSZNgheoG%VgFXB2i9lpyJOikx|H0Zq1h^jwvA1$@tW;Phly3=FoFVZn$kaL -yVKIF8rz$p7hN>E2q_H5ba}S1L36k{8AHhtPj6y}sJ)y#J2>IBIRMDkve*_Bp`w-)s9>-@ej(>k)LxA -PHnEPJ1=B@@3JgNG?MtoG2bDUsl^IEyWv#57WF#**hkJ8t0Qnu_ZRbFn&{5?BbFq5c0qcRl>xl3o@M` -qv(G137d5+P^kN6tj9q!sA^$(>CtsXu8;>oJXvC<6KK`SKEJ*>?uqtk<x%|f!Ki}I@=9iXU4H-IyO%#WMdBpkc-hhvI;lar&GlQ -fk*X64WmF_7f>^}mjQ)=Hlp_?)|FUmofZ6S7zwLr%ioh$f)Nu^MK@7&)ld1`C?CtEltx#Qtzuv@;Or#uUxWIW-9=}M4sT`nchLI?ZqRxtrE$lTIW)O$Pj6OPO-%E4wDVw7u7XAZWCGSbie-0 -+QE+e#DPSE>H##NyFFK~YEprYJfT?kvXfoj&#ero~5-a(U#4h#=d!lgdgcszP_&l%eXSxi`jX&4A)@b -!|Bc0W||$ba8~6l8PhA>Wp8aWI10{loyU)4iGCmT-p80kPJFwyceB)dP7ay0DDK6@Jsw>)#F5ioJ-Qq -!sUrooma9sZ=z}|^LIH7{-QE?HEsGZJ6g1yeW=MMC^p>CQ}sR6lCFzFCdCZ8EG6i8WIRTZGajpBCzd~ -3{~u6G0|XQR000O8`*~JV1`i|L^ZNh*@+$-Y7ytkOaA|NaUv_0~WN&gWaCvZHa&u{JXD)Dg?0tQI+s4 -u0|N0aNeJMekV)T-vtz!8q+j63-Ecq;@X&*%;K@yZ;fdB)5lG!Bv>^Cp_js;1{>T|jKg>5Vn*qz;-ot ->SXotG!U=F`n!l#i3_YA={qlg&TjlZ_|AG#}5?IG8PFQBlI%-fXe)1fIXjXNw}ax~_t7)CqRBwstpnw -zmHq1n;7G8l3lnx1(?8NfA@wcX2UI$}-8bASr|ExQK^~;HrqSDjs)(NfF0EJ_$zGQE?S_gDMZAY!S@j -qJ#?hu!@olS_U`~baA%8veh*JD)UKo7ZouS9|uuc=A$G6h`~4?&8KlzMHRLXTiJor++(oefTb(IVZx7$U`VJl(ygWKSI{!yEcyn}qf>7T8*n{BR!Ta;0SKl8Wybs=e|Nh{5z1+pq|nCTS0Z|pzjXJZ3bL#=}e_6%sdii-? -b0xXqiHY@UIbd9|n6)`-7($LQh?kD~#lj#iV%8qt}+p3_yL%d?q)Yw2%*Qghg`wdLWmX&1%@}>7z|p^%P5^Q=YG)*o=4*`jwy~`eAXHa -U}qT4le7xvGw3DO-lN{{4FLRj(A|(fj}w^eC>=06mQN3|@gRuP7!{>9Pvn}DvjxbF6soBR{eZ=cl|!!E37!S5ZVUc(AAZ(ixYsy`P|2B0dN -+fSf~axLZ(5r#fZJ0eJtBZJ!UVtWbi@i3L;@T2Y>wNqxx>lvTTKOybqnNg#I#2qaNj=Au7D7=j1|4R*!vAm|26EJ1)z<7 -(9FP@6x^bHtBWlH85yFdn1KGs>sT3^q2tjVtQ)SrySb-Q3u8KO28b%7oaVVw&Db8(3V&o_Y}JfSVDFV -N}L8_nqct73A=)&@-|jOC%XQhvt!3dcpT41A)z1G8^0>;*SyTTO_Al=XcN?Vt&(L<|#8B-Zrc~gE&r& -H{rGjSeMUVL8k$Jsosn3*^S_VAsRu6;0{>I=2ejc$2Qwy?rm~l;^5d4{YG$zCmw@jys)x?lhGDl2POg -iS6t+$#oN4y37k0Q6tRZY%!2s7rrA1h3CH1*1K_@&C+hKA;$reOi*rO2!iMy?x-j}^Uw)E5UWE%6E-L2p)Y$ZEe -J6pll*4~%9!T0B{uq5pDFhDLu$8B-e4xZ1d>FDY&bL54+5@v1`!*acfds$q)fTqQk&P#cNR^nY8-4yX -e?)LZMnAQ&ktG{0axNjCh@@stOfxpH^vvOiw{c)0j35^=L-By8Hz^dlV0&(T#erm;qr@FM( -QMNy21{SMoQ%mDDfa!VFKZfYHXqhJI31d7cVzlX*6xpR&eX5zWLq=s0dEAi4&s=k#O>TlE!y+^CAho) -CnDEU>F(Db2bZ3jr^Oh~9^=;_^x#DF -jo*!ZS3niuoNJ-zv<_gb*906tW?|q2C&0ekq0kDFZJp6}a3Wb!W5-z%bxQ96-v$N-XMDk?X!iiocbLF -dzrZyzNv`Gv3B$mm_${$02U6HEUib95A9*7-e*oKXUY46{v_rCp7AhyOd(R6g<&ac2WQr(2n21WDeS+ -1G#pFPG!OH~*0W6l?!m%(Gyd8EWyEWKa&x?87<>|f7VO6RoS!NF63@MD-0p13^6OFLgWDjK=-^OV^!! -45_lF$!QHY!>9a6-Zk5n~_3-#$nT{VKnS>EJy{BRWiQi@+QB?0!_1^Ei0&^)4d;9z*A1nA|MR=Vd@5l -02m0emYT`y4dqkw1%-9kUB+esDKM`06?K2p(3XRir89a`3A8VuzZ0Z#Ff#*;rt3aX-M-!ffj^-)5AydF$i-|Mo$W&^wiW0u~Y}-K<6 -VAad4{3`OuJUB!<6ONW4}!9W(&-Ii@q7bB}xR6CT~G8VH%N;`yM9z$m -4N3yF8j%vwfyir|Mdy>aW^N7yj}qXRTIkHHK?dXn`_k-w!UbDeV# -P2bk|I;4_p3JGj2HX|F`JM2WKdb(FYzy`8t5Z=I8mVJe^Mo>bE$3_{HeTF*I|Ss -js9M;~%1HRzvTAFU4coklyF;*GW#l`D=BMQ4yt#;UDSZG#l~v8DFo^_rh%knaxW;(jf`SP!NmJQ^A1_ -Ghm$zi67Sss)eZ@y@{$QHA>do6v_@&cAx<#0USIRFAB^#ozDa(Hw~9HvX2C(M=7$kFVm7SmEp -@nVpl$n}LYw;2&y2&gL^35Pm<$m8ZVE64wqc7Jopst5Bkk`E6Od1Il5tl=C&Ufc;j}NmdsTTZfj;0F-MG -^7u*~|q$9;WuZSWUs(GdN~ln-xsm6g_}-cRHbO`CT>wVkBhKz++y6LHj!X$Y`;^Usu!A0R17ljokjFV -s)o_`K9La!yj4hz@0`nF=7I6O5MQUe>Z=^^20TDP`%1i^#s=JZ33qcToAQ~{$>_J6&qk7jW~6R#arKk4z>0X9-^Mh(CI6nKaB47V#A-pj#|v3 -5Uc$s4iwP*NQg;EDMU-9fOT8W~h^4(WNp(hiL1Sp1C8K;CYpC;?O+&boAIK{j2Y!+P-k?t}(|mM8I4z -Q^1i0mS{ILS|&JA2icAmgV=p*plNHz}DB4YxvDZ0LQnxW;Ym^N+aZ=)Gcgso1spNX*3a`Im+MsTWHDfx|;9i@Jx&-0>K(26~4z#va-v^ -9HFI{i(5UuJCB4?7C~{c$`%LQH+r`=$hKouA -G%M{{+>~3DCL -qk8>fq2aDsg{RE14(Y5q%iLc{;&Mq(~zcDanC}qa@>$VG&0+WV{i7Oh_Asc8PJ%*HH;i_f{x1>r>;7;SkkK -jACfp1rB6r%^epcc5oF}ViqLwLl{r{<21$V$ReHyf^2szp!z+)Cdn%-+JAWRH1wV -<=T+%C{uI~3md;PvJRe{AlVYN>dw16ogPZBIiaU||H@gq`4A|0V(0eBIzqE@e*rAZYUbYG+ITGx8f4C -Kgh2s4i+4WZGqF88R@aS2dutRCV+WloZj2J$M-SXMb-%#in?!afWaG*&WxB3>A7chIPXm?2DJKQ2cRl -*21jF%Veohx9P`LpONd4PJDE7Fw{7fdXs2#a06|hV^=M70DAGP0@Ok%<5nfA+5%o6@&>iU9EebY;nOmSj0TCR#yiyTT`g%ohK#G7tau9RCj+0i4 -@#%&yUTt>$Fcd=m+69rGxym~5ZsnD3b%!8Xap3#Oaps3VX~V^nb&$ZWBJz*aI|>o0RsOv!m -t|P7p8UvO5mXE5~u-0KBur?QpR6sQrSFNqmOuzn$U2BjBFO$@~p;9Myk*hrej2=fea0WShDCzCJSv&j -H{0p6z;_J^+Ia~Z-S$Y4Zb#m=r&KrCEjvieL1rL`YNNlOuE-flTlT|#*m|p@_eYwm^|GTYXQa-&f*>; -1-=ko;4h$t{#tnL7}9-TY^#d8BHB?c+?2qXd7sxdg~4#amq1Zr&;4e=x(oPX%GdZ1x$ZD7q^Y4^ifG? -q!x-hM17JjI%zJ>K7CoIXZmE`la0Gb53LY?9Ji3-wa_4uUTyTK*FOyj& -Zo>|=8eSAlXDGjn5Gqzai#OhPIZ5!w7+wP8b*Z;(5^*hgV9)V;Bb;%Ac2voUiGkgMnyoFB$S2rzGLt3 -rJY4v0{}_`GNbpX)we$Ug0<&OHl2tXFbP0kS_TG2q{2I5WH3b{A{d`n>$S41zFXBg)|OF`QvSoSVJR$ -LP6~!rx9S_`Eg-_=J+Vc2z6gz?o7eMDc@3tJ5*&3azK>lO?*xXGwaes!XHe<|v!ZD@8zEdKMRagxPPs -`0RPhW*diS2Rss{-+GzRA{0hYxCQ{I5S`fa8o`$4z-i^8M$0ht*5Jh5Z8?ia=)=U)ssA93E%$}?Mqzi2XW46*&kS`1%aLnwuQzy&y5TUcsobqwfJmZ3?5!W -T&ArA&X{7^4sDGaN2ERjo#TXhLc%U$$WOPSSmk>6!(SHLGz^A43=WY}(*TKa|$tZQvSpS1UU%BJ(s-A -15>%(nirr~1`1#jXk3?WBi -R+^ywg4(u8bdhz2n(_zLnG>?W_DYO*KISa+ty9WcD6SZ@vvN1qH=rW2zKPa0gDXA*o6nP>Uw|s>jtsz -t&-xZ=m!1OjHw{gFI35-XnkF2Cle(LD&tr(;U?FrQp*a`bwb%w)eKxl5$R$a3Lv#8?XKgVo_m-onBGe -IQ-}|`RgCywQJpIW<2fl7WYZyem(dvuM(NCZmyW5tl+0?56!&~TVMT+ycD8|21(_95q@Ia;t7@T3_%86pzscuaNB)G -bRF?jC~&XtKzqEeR=9?r~k=-zZTplsOV~c?}Fr&((Xu10pur*(5$7s-!}vNni%^br8uOW(tRU}FQr -WHGoxr;h~hzLH5o`V~e3G)9gPmWSmjo~Z5;c9B_dgLCOHBP;=hsl9}ctgDo657TIY9UK*l8KMZ$L$7yGz(I7-qa!w6E_(^@B`ss)m}6ni{WQRX4U^yE@vZ%1*5>0TUnH7GG+V{L^q -fXF2Xs24tvB|b*w8vTD^Zrpgx{~Uj+VW`a3`pj_y%_0d{$c8^<+AY$LQmjF62(hjx9!y9hGP-jXv-h@eCKB?jiz9geF>Y -g6%9o=yM;M#6?DLO8h>Zp*h*E418r2uFt3A7~>Np_!8?jKxK-v4?noM)}xHydrV+@{7IkU!9Q5dkK0L -@FBqS+f@>^L6<9cwa3)ddBKXwbk6Z&GEwT0t?t$8@KpVagTUX#DECkgIW+;iAE(8E})J)9C -|vD8(=p-(1nvTtC#g_iJ@lYQd$Ntr0tW!RKX5>7m^wC35?wjWU`u -o=zqx!m`bhXwNssZTN4;9a%M>(LEZ!H0q*cjh1z?dQu;W1aOgDuB%nXm!Yb+b*Wo~A&7z0+cSG=RK`N -qrXi4GqXMS=jMnzN0EWS-94%UgEcqmef0`%2I)rWF)L?t&a0EEJ`??vYU*-#HaKnIVC}Nc=I -I<{I#ZXYI10S>X46E)fW$yN$|NQ4Kws*eR-8LJ-RfE2&Xc;ld_Hk)-eA?XB=OqT!Vcx=p!x#_}mYt4) -C<4_&3KitrX}K@?InZmHb--Soc4sIp&)I%H@?r934-07oN!170I8+L>E73L^-{t;iJ5_idc^4>-PE=uXUeNAlWIRv0CS~d7D#6 -<~nkAZastB6-u&*)djDMTK{xILPFX9j34C5`ibBPKuhMQztW=f3{xxtN#O@YwGdh_VUJD8VKN9R0xa8 -h9Do@iY~Hjdngg5gw91|LQG5Lqy{}`r<1>14mmU{A(3JaglN7*S6wE7O=FbqOy-FQ(H`Q`l6wUZx?$u -RMOq};M(S>>$E`ZO#JLPjrjB@H2@dl>!lB?@NUe(E$~lh>uIF}xH3XSa>j=>D-SF|6VuoqO#~MR;SyT -hS~3yro}GAApMoCQgn?8rxc)?lvAbv?Lmjb2F6#yGtj1m)93?`iH%5yk1do4w0}Xu>%c^hE@OBNq6G2 -HG5lvQ5X%>XMwA;Y2FD_lv$T~VyGa7P(Qft+`3)sBQ8r90arcDWFiAD`^sHV8-G-Jv0q6JN~fWa-l=C -WC3_(@IUQKhj>4Rr}=Ebmpe_FsWyB+jPS`C+y&1=Hk|2`|}~xx;63R?o~HS=hc}|>cp+Di6s;Lpn){4=UZI -3-a$6t8{RHuX7JvWYA8-yXxvf>Q>!?Alyx#)q;yTGY$G)nw8}wyp2cJpAsH0C#Btvw-8441WRp#rc34 -`8&)xf3^)`66&;71f#F@la2{xKlcAPH4#0Ko#J7(p2TsnM%-wDBoopYWhJyABKb~_#S2AeANgo{$>Ha -bZ@&S$bY(h8#=tPx?=Do+7CNdCxy{S*bdH1UkQ{!r}8-tTysJ_1lKA2uswPkCQpx}q*nh-5Y -mEHxDOb{Xrto#nfZruiT_|s;-+LiP7z_^>x{ogKt}8(;B~OpOeb61~@?F00XQNC&!YmVSUeTt&yzmW#sSVjRq27L;?Xw0b21kCMja$Pkp9$gMDSG+ApQkr>_^rXF`#p(JoRUP27ZDafkI5fIm=@5bn>0! -e27@#ztHGe7tv1ituCs*xRo7Sm<5P2h_rU-=2|vnsifM92jE2DgJKGryFi3@hgE%DXYZaeQMCX?`Yw~ -fI!%7H?RoS3|quatRKudv$@TlBoB;ko`Nq$WMGq?{5R0O@rHr@7vkfP9|;Y_+v-sv5;TRik--69JLkS -@y^dK$|RuF~nTMi~keQ%G4{iiF&)~84lKWJXH=}>Mn}=tB!ka#T*DUuYTJb9>2||QJ@!$%SW8 -7wRW13XG@}7T3vYWV?^k6kKBrY2oq}&kVIOOvWbhWud=eAvffKEg-N^U~vbPN{bQ(CAV?LKv6=YOWZ=R2)*yk -N7q8@L&26zCqD)N`;XBjkQPWpz@7~TOycHNWf#TGO$sZ<&UKbd6U-_GJWlLxh=jFvx21MGgwp?j@e9x -4`o=6~J?2}&jsb@$dLy5RP1IjarloD75Tq!zTQ{*@O -?=Aug0*9Iih6Q=VBq@@A)ws6t7Gry>hl5NSJE!32cyt6z$2ph3Ea6a86>3?ErQayD$Tqppbq+&^uxXA -Yd5{ED0dXBkF)u5fB2AH8+55dY~0t9E&To-GPYlC3WfOHkT1bx1I}MXbkmcuNSce -h)j7vyA*OUXWc+HuB{`+Vjlpl6q6;wq3AT4@!goH<2_WYwVZVO55QPA`0~py*IG}a??fh=gyN5tO+sb --(=sZe`1`C{ku>s%w8UpP2W#TAS37$7X@iH!8JGF?LXrnbRyqqG*;r4>+a7?Vu(;rC71yW(OZ4^ol_V -k%DsN803vnUB>M6yi%1e)1nRJ*E?oy(urZbd7Movc^T1v~C@rR%sr#BxaL3&dfvIo85H~3;)-lUTcH| -ec-Q}G2yETjlpb=e`Bb-KivN~FW;blKZ1dL$26Te^%~Gm2pN& -bH5*75DS|}l`Yu!|@phh#qF}`Iz1~CGTeYd~w8jU=W`5h7KAm?3CZdz0uTiYCG{zQ?WBZJ^JEoyhQ)t -TNCaS0MFgfb`6>KycKvDkz@d< -N6McoAq(Dj+`7(wxpKi_Td;VrY|kFpI;udUGXmu(_tlmW6f=$!rNY|^jg>yY%3ah=1C2tLR%MYOLxJC -=p%=$!5u(^9ONM4qrRAwBn#**2^C3t}{FlDBTyv`!)n(OnKEJvaA|wH06||0`yGcIXsDh_EDNH**s^; -jljl!hzv^8hgBgK*3wQ%>5j&?#Qd(O^-I;%m?N&tf=0U7|MQz*q3eo{L_eNPEOnR1Lo#+Y=BI&DXb(k -Sc;Yiyxu2AMtD~(+*3bqum=5OHluavpS8Blt3WFv{ --0Hewn-cM1?e|e%)8tt#IJS9IJ|KDzm!)Pwh2+dk*b(gHkh8zdYQW^tbGt7tB9k@#guPHh --MzNx6Xo|L0<(#!JqAE7saSW&rr5(&nj2$1-bzJF8tKk@~q?0|$F(F)T{@2;*39Pdsf#c}EQ~-$;(A6 -}^W_}i=yce`~w^4bH1PnhAq4<*)0RG--{pww=HFG(7h_=rY6-CoFnev$S*NO`(E8~=iUR+1;(8)t0y5 -MHlSh4S;m@)j&lZG@6qPR}R;iq2zV4T_)qT9C}ed+*+_i#Y4l7Yyp0_+VQnH2Pnb#M_FwakV{FkXZ4M -s#z4rOCL*p9@~PSvKt>GQPZYoM<|KVho*{pP650>{e1oB+Jv#DO5lB)J{Y$T;arHczZ29rAOu8-yRD= -c$|-_=*sZX9OLYin30QP`tuD=R_(W1-GIXd!9M2t!kV}YUt?H3O6JsAE*nBOdI4qv!)T~N0vWSPj7^7 -7sz&UKz>dVQ{B~R3o@8hn()uyePikVm9G$6zXx!Z4!}W(sQ3>No4uvSZR$@0wd6n8m<&L| -x6%zI074xs2mZO#1dKHW-w-3|w4@Qgwe%i}D*8n}dmFhgI%L>*)OOt$`?Uc)Q3_xvF5ldfw{FD)8$?% -aGJXU_T+D>U$-JH7(m)>e6q6cTAEQNbv6uvimG-BM;FvE|N9kL-WVRHLY1!Pm|y4BQr=+u1@ -n>WY$;P2Jd-{_eA<9$LGsJF=&&w<2>yA-O$G&PCXZJE_1p7m0CID-7FOK=K`MQ1*6xVE`;qYLn=B`!Z -p<2)|+*H!%$3*UYJahkr&#|sg!%FN5e+8C(s(Gi~=0!akVdMY)x -ZH@KVdpU0-Gb!W3GCXy{cq1N5dE$)9sTL5M`NBGVws=r -)&K<#{X@qfAD{fLJdx^W!&cw+p@aM6dbmnv{6Z+8teut7nThLBR}><7O#-9M29uN9Wskj&pcMF^7Bi; -AmoUkCVX9^g1`}!kFMfY$J0kl0%~a~RnME$E~=WXsNC5Jwzs!8cXl>+;S)UB+1Y;S^w88=Gq^fE^Ff< -5$dX_&8{uA?ulgy>*@jiPW@d}Yy!L{f9hVs3q0z&}NvFEoFPl#1jdGst=4YLA$m!cUR@qhInS1UEYsz -?d-YELPyPn0I)-t^{xP;ZyP~LEQ?PBTJV_kugMyaVNZVI8tfw8CDh&q_-ahq&x(avjgyCJ$kBP8KJwT -2_5%@N6LTN?txF%lz$VAOjV@`nSvlS)`T>T_0vt%l>}YaJn8cm*NR%NX_fypka@D>^CV+S8>$G0+PRM -yRUMRqMh(u?$!G0^vTERzl>rEfV)BT`QAeH>*S{Qp(|YlZe&!(xA# -s@EziVt=WiUi!I+0JgjBJQq>GcD#gC(*>LyW9FkDv$n~MOagVR74&7AI=t&NMCv+X>7p@UwA*&DB3vZ -?Bk&R>pfpb46sKW`y}9Y2qkyGs(>l)3BXGKExd!Q+Z$6j!@awUi9Q!v?qX?y!+6+D~_X4_gmXc)H4k( -5{bA=8nX8f>fK}gn&?*sK3gg#F%Z<(wGFOz*Feun=z{LK8J_!|GAP!i43#qx88(UPo&GN0MtO= -(Y1{;F3?Q{`eze!u>Rmx87y+Ng(A%FU3az*Swq`1-yqPeBvVhow)Dy`414c;KsKdQ}$V^^O&*RbRRGB -cL1AUOllYV;|R$ti{cSVK3CEXMiWo5;!Kl!ce -1m0kbuBPLwb+Sd|<5g#bydK|4eloN9wW-f4TFK7TH&J`HVj7Had&5HtJB@y+G_bg2(*TG$ru>bms@Md^Uy%sSwa`>+M}R7tUbDDV~$e*FvKvD?GgSM< -3G?vH2VNV)ovNu+xX6J9b{34EtS_7nv9-{t|7vlg4b0tx=CYXA$j$T&1E+HLO;JfcwCB{XsJ82@;Kr;^eQ%i5;Q;Z94 -}$;=A5#4X9}#aP&X4L4+hWA4_;zg6X9Z7&abYBi69*2cO^R)CR7O!Yh)$6P2xMpV^yMEJwib8@~$gvq -l5$?@GM2|QchkC9LZ8{++9TqFK0glitxtYy(B6jRc9enk;l#(YD+%yZsgI!9kqJJD{1p93VF#$ -9NAqjb51z&~4K1S}mAM?pfYucTZjeAfftrl&VhFZZ@5P$&59fOSng#8Ixa+8fPp -4ai1Y&zK)0UD;=pp#_~s$(sefj<%3e#dP{}_Ep?$mMaVXh!*Up}#6-2i&&S=*$L -uj?E_vBlhw`R7&qzmN^>t2G>;B%-65}psW7T!?wJ)s!aQ}hmd?VO=L67kvRy6Nvtga}bYX#~!A+Xi_p -_A`?&+NxYHk8fN)P()i(DwkzwO{9(AR?fvR2%pdloS5!w -!Sv8ZY)4Fbo@bQgmzm#3&Mz%vL(-&0XQR9{3Bvwe^=*!r0cMZEOD~e*8v -YGS>*xs(aahnIw%sbTGJz5D})iqu7+b4y1P!&7*?ev<-v3BF@T34^fuh&1p(i?tc%@l*R9Mzb)*!I3$ -)4+X}D|nswWePvVA!>sA14e`$d0j7^&X}lHon`MFhhHp{50%?!TA_Bx1UF$509sB&Re2m~J8|kBYo3H -!&UvY*y%J@)-2GoLFV1!u2TCCDR~-eqpM7#0x_yk*z=8KQ?& -L>Obh0bzu;%yfN&QUc*NVK7hBaZtcKmdk1kFU@9mpakN)i}9UX(R!V<30TU*$$@Y0O2TKQ_&Fa$}xm_ -hhDE|C`R(5mK6rbd-|7M%Qs)`Ccw`yi!=%$MY$U$jd*2uvFnSDP}Ok?qbM2>*H3MmthCe=vYqoJDDc* -(*g|^qb>u9v__?_Fo^pc>{y_S~PR?COA0xNAT+O^_t1}BGqKd0va>a~E2&tDxJ9|z~B!JF?-UIj-dXGhS+5AP4&y*qs0H9GS -a8+v&dygh}VM~AOJgAYML4%sJxcHg@;cRGq1W>FtUPo?`b8E8?d08khR{R_^2&vXY2{czJh>D~Xlo1c -H@gZ6!!pXP@cJhEZ)YuNeL*!G~#rV}``6!!0nV4RFedjmU+2pgQw3$bfpen%%HHGRlq$LMl{zRbI0J} -?mztvq0iuw+JLP8xP^kqm|kcH*x&b%%yMHD|FG}_=Oc8GUowbgBg7Ae;{j&U^jgd{>irw$+ -WfGT>^_we5K`K6B?S2)se)eV20x7s28<=a1jw6_L!`&rKHi -f)4@~Le0`5E|Cyy+I-v4@7HeW>`$#0DLJ58rzaus;%(Mto1G{HD6S)htRM;YVEA^qvZ0w@S*loS36>9 -rC6PGkpo0|sDVX6GDiR)nQHQ73hi?wPKR)mO@ZHh*VIO$?`!hsu@MHF4^<%+hK=c^qWz1Z*5?QzTG(q -qGf>&9|$hk@Z3F0-MH-ETJfOpKG-m7cqOE|)Ro4g24p|v6z$EBskx>Fzms=e|a{7GX~^2ns$-GFpU!q -S+uB^Hc^=o|K>b2J=bQ$J=`*GcfDNkVio3rsUv98=|jDAPvG(t;z?gzMzAg5RUYwh*UD -k0V=T(lZXLVCBuY|M!14fj68xKssBLudF}YS*ZPU9#C!qJf$Dtp{48{@|25B~$zsg`YIQ%%Hp7`dSB{Fa261S{rO7?Hw -0x%Ap^i+&gjsmy{S};uUoE$3!v!zKfsRPfrOi9uBmQsPUEJ)#k?N&jQn3h+zqd#{CO-iKoL#;Q{sJO8 -$EpQX&wAN@$js{eiB#Q1$$S7$vpU%-r<2J@S0!)aKratWPvY}K59GF7?ODt#v+dmw-Or?0~Qn;6}ahb -d;V#Y*mK+$cUpaJC=j%g9ffKoFEJoufwfVF|WCT071v9M*mb%K|?%Y6+<{y*Gq|ULZX{agvEKm? -uo_0cuM7I`dJ9%Us<^q8E6PNuy9`J&7$RDXftf$yGw8oCHwF#1C^=LV3#!NGo)nc=fUuP%I_*hCYWKA -E>|TJx&- -Yq0ax*Kb>EcvOA0y|dNDYTe-LufE#-8vgEl{iiQ?zxwj4uMMI4@TYll8>L7%GHuLYfHztK51i(KL~VY -Kj~b@jz~bW|(6?enGhAlaxt?BJ=38f1b~kD>OfQzUdAZBp&C=5I%DW@k0tW)NcD|Lno-3081kFWC!== -0yD%f(1WO>1MCD$(DT?$!kUKiqdPVXbYz02qd3Q)k}btHUaiQ9R5UcOR-q7f48P-dC=VK3a;-r4Q-dY -vG|Y)62nV5`&RYva9bx+mUq^6c9DgfP+)^wzp(`;tkm)pjj%x-Vb4vx%yfS|aXh&nlg;ZDw@U@Au*iZ -MXU~Dg8Vny~en2IP!Z8OX>Uy<-?jim>uI4%DvZeov9GiN{6WDj%lYWLWMIbVhZ*NO9%#%%+ORkjWCr28l4!MvxrlrVErSXQ}clQ94E^WrIgb~5Dk -Y#e49{gEY!6|AGJnKFy=hol9%gt@ -G;x>eDf|mLwpyx4ylmQGM$1T7vY1EM=>0r@Ox8X`0|ad`)NZ9hPng@Gh)Ufgg=aB-^m{)J?H$FusdD_ -ZGo!+&!X+ww+_A#S~=`=fr{65jJcWjiRE(_Flp%>qa1Fh~M9&(Um7+-CJdZo@BSc!f3UL+vwSNDE3M; -M!crLo4BD0^>AWoa-t!H9UyHQo4fT$%QhIO=3!~VdpdAXVhrT_U2#mUaue7%De0%^}rpBff -Jl94WE)~Q9Jh#&xwI>t=nT%5Z^90qY%BIfF1PxXgGg#xx}uY{F`xS7QwxHwsrgF$*pr3knroBS>()RU -+5AjSi54Wt93_FmA0iHgQS+T*if2?X`}a;JG`;nsv--=F*PbSa>y65g10cB*r#+6`L1hTR2OZ_xPV?f -Oc2!4%qsH{v8)?Tjd71oTFdrmOM{iW46fbiCl`MQq=P4!R31KUl|1alqvWMY4%^=2iYA;{aqhfw)v;9 -1z-Yw>Ec`XNQ<%6h}%}j1w4@AOkWPgf(gIq?0VP-|;j369y+vLl~!&9^GL$G-FqUtrY&SxXXJM_vfId5>$7Nd6J0Svmd8oeV>)6`(qM` -#p(hz>dXmY4ZKMQvx*bQMMygk@F|l!J2!0Bxk#lz<%!#SP2sAOm#S7Vzd~b@FWg8Q_nnq61GvqG81q2 -i_&SQ45kkO7smoQTUwcWEuNWOGQDYGF*X2eoyc1b28D2nCt5`*|roEM+Y#n>IREYC4c>2zhLOq5QYwd -|XQ?7tbfx+XI-G;Jb-L5H)RVG#FZV%xry*UF@3o=F2y3}qBu$I+O>tVSv7g|N1?V9+A@BRXr)=l&-1# -tF~z&tNwCNKP8`c{EQA`h2>*{6lnZE5un>T6KT_hc3rVpq!`6B2xARyroQtD3PI4xn+m7YSa(ZG=3_6 -o&-8`v#bCdEFJS5dY0eOU|7NtH2FXT<1>NN?H%!DciVsUMm8&D6#}QGz+o4pL%;MW6l|rL5f8~Lu#lX -VrlH(?p#hHnGRefo;B&XDr69Jr<`j3;VXQWEM^oE$;fBff42#^Z9&pD4z-6H#7c=EfwcfRaT)slT;wU --qAunOdJX2V*k->aaY#MEyFwZL0v$&&8_~yYKOU2AMh$YJ!c>Wvsj~F#KBEK;1Gm~hOQTxY5&{Wjvo%Kydzd5H$ -e{@t`wP5)mK)VbR=c<4Ar!+H3{L9F#PyqeV_1=hEsVWhw07A5H;F1}5k+L+C4A0g(T+F-VdD)J(JnNU -5GbFiZ9SPzVd`M!(#1FI4U95h4T=Kr-+60=YNCE3;<=l5O5RdEi-NtkBro=Zoh`h`5y;+ek^hd2Pl1q -1D#7vvV8sn`YHV++clX-g?vWq6u61A8+xx5Gt-WUXb|!-&De6Bc%}K>`hs3MFjN0N^`_lpe5v5yj=}B -8Mw~m_~@4ts@0%5;VmMMa5!WM3^N^zjqf|)$Si-QG8;Q*uzWDravTCs^UYze{m4(*!nl1`p(r`Y*Ma3 -K?rb$AzEcSV6`aB%`R%5F-(X7|u-7vf{KI${$X^SH+DKzDdhuBv!CqgxFiTO7CxO9V02M@9n<4bNMtk -ahGmTyfH_RWMjXD`yZN;;npxcJi(gna#ZPdwbM_03JN;G(|4c&r^U)Ea|+;jHQhxIXFCqD*` -AQq@D}2D>I1`k)Ect2^7=u^2?DI*w4<(j&1PaI*)KuDv9d1iTUWQU~NtPM`w1-Arn12O5DBUhCt<38N -OEnft)FrJOm(+f&P`JWZcm*v#P$yGc0S|=ZUpIWN(7^}gy0gOYvBv!2@ugQTP5D`SPsfkm*K6B45*o% -*u&7P!^YnC)W_@2ivfR=BlljLd@{UL68xQ6gEBM7D@{0cepLh%&@qdXwtg~%5?6< -WebUfCF=*<@`gLrkYwAH6#QVgthF6NR=D@PrZtwrS3q^S2=p#tl69Zgqld0SSx9!;L9DPgs{8Rpv5)! -AxTrlF$qKqn^VBW5Kj*#Fg^fVkR#6Uv3i(P|J`4frRD%i$kVWjA!7wjG0|2`X{4m2LKi -pmPyAO~&v={n7iuv%b61Xp7Pz5+G-vWWAOn+jc*XwK>=aC;NGQmio+0z~&RP^@c#_W&(Tpk3DHdtg)b -3n+piZm3Wl6xo#dkf8wdxQM44^t!K-(KQYZOZbLDh`pk?Js3{B?6IXOT6%==*mro6PAtAE<&r8i245w -5(O9jIqoX~nUQrI-T36_wb6Sk!A|AiS4Da4Z&4OItB$K>>QYD66ukz$yzIlEyfvP!1yJVx=zH;jjvoY -uE6SM@rCQ++C+l1{aCUp3U`hpjP!B05r@u+6`3`2;A&W)-25$x};@#4X;gI2Ck-6`)XRWMeqRQ#-1t@ -wGsidEI_w`x_j`>$M8_1>%3-fDi6%hcJM@B{0hKS?M}>?lvuSlAkk{EjPxoo`ezvKf(XrI@0mVm8wu_X7;Rfj0~ -F=K3IHV+_sr$M9FslU)GgJmJaqVyhc$=|Hd8uJCrj>_Z9<=|XzC2%GKjByAvbSunsD!?zA@#GT>l_>2-Wa{0BVw`A{s}l>Kr5@=5`n#j^Zt(W#>`dG|s~4Q6S> -8F<+Av{yFBb@5w&Q+%{`@D8CAF;b?EkWF@MP|E?PSdb$&(ICnoPUSJ(l&ENa_O7)exMpQFfAjHBQ6Qh -~Odab@(w)$S)(~Y)Oi*hP~j4z_J-N#ju|0(!9wG&?;us5`JDPw0EJ=o(f(VK80UoLcFe)k;c?`Rab= -L)~e(h4fsvUO8ERCVTh<@u4foOHsM{PI8p+pHo0x|7JMUP-T>$l_X3F+fo(|*!yCq>iW0*6D~9;6*an3FF; --OstI+BjkB^Y9hG@-IgOfxMu&?ecnf3a&aF{};PX||S1>M4uVH(WXabH8Nn_bais`f^-w5ZRhHks7dD --cONC0iZ3dv{^A3hbuz!U1^#wjT>}G>^b(E#H;fNrCYR8 -L*eIKoUYh`Jz+*!Ovv*)x1#p|o9_mBCS1aKlQBXL8BJb;SUHUGl!(bV43alfND0?l%AT>(L0)dN#Lsk -_5jkH;4|~qu$=A7@%MyPu07ioflqAE;3#3qA^wJf%$KBMHmJd5}u{#W0NKkmEW&g0B=1CFj;71sBqp* -Fm%^_Gp8W<KNl-XY+yS@l8B!mL{X566f5KV; -jL{jlSl@_42yEdDp-mpnV(*Y%td_=^p4glru5@q6jkr`I3W9|*andc><6Y)mb4NVKc%1bOjrKV#H -yD681tU-z4|0DVgS133m(6^q9;X)Cen^@E0SuG9;7{5h8@<+P3^kYwD~ttVnLXyInIB-*FeoY`8aF&O -JmJjmRFE@g@T0JF9r_hiGyH{cGo&^w2XP${3n>#CszviqPL^G3G6Ex1S8Wb8RI{-*QdntBiqSzJJfKA -+MH%mavIKF=1M0T3uuS|IopLc{@XqFdd+n%g$GdAs_!+`~Z6oYBe%hD!t8Q?1(z7gyp$F?*rrPsze|v -{9k7@1(&k9K$7uOf?PQ7(@QWZzpidSsy^J_Z(#QsLiKFYAbjuoTW70KaK>|-Wl(hR}Xt$3_OYjy@(HD -!>TA-})MJ#G9+7@&yBAqC*nEZI|Zg+J3$eNN4G3DXc;BvMbF&=5pm4N(fUMCiK5G5R)@MUv(rC6+20X)Esm5?=Njh$p=oKFvMkGy<*Cj{$kuii6L?_~;ND1j -gSwZF`pP&+ee36#$Le9V>T5Ru&1-Zgy^KMcqUcD-1y -{$gDPhy3Ao8s;9er$?lh5!IPU0lRy3O&*{yFax(t&pZ?gg>T+;phTtSYqilZ$olxFT+ -ziZI9zB!t8H&$`0vl-u?Wc_TXF>a)?WZAt+oT5`oo&4e_XUFntcu{g4i>Bm|HMxngqPv`^Plz7@*^7b -zH=e0D8xNpQRsdk)rXO=OmFZ|vEpI!!ovgxP6(^KPW*DiN>5kb_qPm+VuzwPnJW(HBKCVHO5_@{+f>E -*)a}*;9K-!~-}b+i)t?(%%RO8`ltYsq>N;N5nEvC~61wV@rEiXP>DXW+bPs=Q)m?Hb_T0EM-yDft(Z} -d+g>agUBqUZF^N!(Xfq|)XG(a(2(L!S_=!Dp>mRmp}oQF(}GuU+XVC9EfTh)L3G^5ysw-kY^!L07mau -|)o^1F!rn-Z~==nUnv5@jQ={M@J?tuTcjhgVTyq?9N!AkkAq*wj)lrnV;mONGf4%#1|CX`~ZRo9R<&I -wDKI!Pgq7ff@{){tuvS73xVUc}#T*ObANqHb7EZRiYo?tWk4W$~!U1qJ3i$$T+i$wlqd3qoj -nRStPx*+M48K2hnF;(hNp(+R}qE941DN#`iVxVy|N;jf`8HLeM>I$U!`&k$|WY*6TSXuHO(H!r_G7Oa -vPo1|*<5G#BQN1du(l~UZp0hv-AlwnOC`2Lm{*D$V>UL0%HBL%Yaiack8ja)ND$NTFei~q^V785-E4O -k^A!-+SanoBPLCFcUcT`ejn;*En(@Ag&?Y-9Ry~XyvmF=BzdoLG!H*f~+L6v`Cd*0QB%*G03h$)B;Eq -Ub$(hg=tLJ6n1RdIGHKxoO!FUdIF(mX@8I#Q5CohmN(Hg;h$priO_T$h*lJxZ>wG0k -tyP)vf6ckDr;plpOEn8Fg(2SxUVxHy|B1B2oW|M`y;o!STx!(!Vfpk}zq2xjZ=iEe^5#An4r$*>lrBq -uZ(CSkrz|)sC|#5KeH=$)^Y^zo9N~^Q`sX3~3KV!V>PvG%YQOn3?C`C3lF>b(4 -AzNL9fNG8u9QWJS6p{#Y_V+_6TUtg1pgl#(duMp7j0~(ec25z+Sm?%!xs1(!WM7&kI_K;(q^>+7BCQk -1b+z*{3R&(9RIN(c$a9CJE`TTd{M2tTbtaWbNM@?|IIUc;JS5xT$n?8Vcen!tBx~NmPA!vEYHRDZJfn -aOj?m))JBAei`Ko(X5MY4A3zI~QnsY!<|VwGc{nxUa&T>r$fgq{3uD1j=iQQb;|93efK9sxUs8jvw2B -cuHzi~HT&$BEh(=U6V#blmG`4C(nawlQzyR@_PAE-j*y?q^+52Miv;Wz06#dKtsSMWY@x-_AkkX~2(f -{m<6H<)F8ce1RKZX(dYhZ26LBucWhXPMeJS|XZKda(NvyMh6u38Tsz^3*k;W&~xD -C}mCFe{rBs!51d{CmOB38(1BEBkO^71lu3DDtI7n2gY4?nZ7)UIQYjqo<^JN(k -c;_3J^&$U|+UULZ!6G3fS)+O|MHEmn%umuY}LbC1RKlBE_d}bz+7z}#79xxELz+<6?xLnQPka^!V+I` -7XrxLwqH_7ZgAJ|5)GCh6RCPA`|Sc10TU-beGcy06Mrwl9;xV@F|xM2Y(Y*GBwSv-D9+2opxwglPc@tc-|wz3TO5fP&s^SR)aw9I9QAXG9Jl}Q*SXneqy>#A8J?~P7FMA7e!jy5JX<5LX`< -1mHeL79|Z5vu==jSCTdq2VzofEcO)g4e~YjMd9c+G*r2D$s7A?BSQN^(x8` -#wec-QNb!xUZE}3VipIVmmb$M3|NY9MSqq>hdHD4kUwynT^6O%_%=@WcRD^~S{bs%Ghkfdg~1Nxbv1L -#;!)To`D>#8EpDV(TuFD5Dt9py*t -L@y8^RwIcorCcKp;ecSo!&rWJvjyL2eNuO~N|ADA?JQ=}sibYCglHtR)<6Q}&z~41S&_lso<7uPA6r5 -bP;X#8-1HXhaskdsT#@1tHZ1ai#&7h+91OA`pWnHpQ5phM7=g;1y_LKJBnEp_VkA8K*8&=?qZvUvZJ2 -7O2t+@ma^R#>Y=spg$$_gJPw}cy!7FNyDX{hQYuL6{!13DaC@U)+?hQ;T9KYY4LAvki`MQxY-fm$T4v$fu6{2iuuVNOVtyPq5+s -bkseSKlMryOT9`DVv$AhzD&Jm(zuQ?~N{z=8OqcI#-;Yk&^pAV$hhl?In$vJ%P#HS{g9NSor>6G!C?e -WG(`lW&wU+&3DJ9tn^g3_-uIxy_8|&tM+#L(npW@Sw)le=`_l0?wj@3Q2*BwJdRkL5j*B0Xj_6ynqHO>ik&^(jna1G=0IVQShQ1Hfe#q?7Ip@i##X0gqu6Mw9- -(a--NZ6wAsLAHB9WjZ9=uq!umzM*9A@f}K}^{8*z~Vl@fPG7FEkMA?YV>a=0S*~iLdJh3h&u}0IF0!8 -12O9P3K-MHE;-OVLd3`92%>!qcO8~e;P;Zo_*ktb+%o0=CNW2i_?+VlMN7g@xVjVFUzhS==$XiF7Qv( -nbq{HUE(REJY)aR-?e7y^BAXVJ#)*{ua7}BzpGS!Gn39}`K6{(`S{>@{^v68a9aF6m;bT3JS~CIXK8<~Vp<7`CJ4hcZo{;SbC3wF3g6HG1uZF!k*bb(Lz$Z>W1qE)OS%>s#~;yJ1 -zL=41AO*Rjc(SE1gvRv^~8>QMmHU7cxSX52B4>v$RqrD>(`&Rf9?nw^FZRB=d} -!9c=1&_<5-*^+%eHJwj%B%@{X;%BqQ2sLr*_*^l0RX;9-x37x#?SJ=%w27skqT1#Oz$I(zT&wy4o0U@ -7SkXtA?0E$oxlhcCAoOQE~NNXLTENWe`F%Q9MXfX|gPA&h4Ozu;#E1aSpbrXQv|cP)J0?yY|yI6c}Ac -MKr2}ZDdx1KN!deKMFoa*hNM;7@$tAp>b?GpC(m>YnEg9mnz_%93+O)9<1XoRfz!qanm+~DIX~Y=Drg -t$3kP+!oHCW9hJ)F(-Mt|H(T|RWj-q7QI;Jd_J#`W#@uZ`_BO1AeJU`y0{^qVrkVc-D(u{Q94J5HF5))X`@_j#cy>h?IW$e>oI}Ea2 -x+{4cxYz!G^bNXsUus%dp*?|62A@(!=g_nSdd#Zeoj#^&kRD^(0e=uu>1x_u9*WB|AAS^&$U!PXu~X2Vobt+vN@uu3iHlR=4<+ik@o4ENuYt%g87d&G-2Uv)xqJljbaoo@852=Oly89mP7 -#qUlLYDnvJ(}pV-2q@CFSoDx@kFF}+U|B!TnMS+z*xR -}A3M8tevbe(v|)dYCdu@q6=DvJsptT~FZ7@aVk1Dq@a^ng%5C^f -UVOb^hb-jSDO!jOF}7aO(Ur>72#X9^XdwO?fY+JS&HqM3mlJUrcvo$mG&_MJcjn&)C;c6{kpG#mxnJG -;Th#lM0-{dudk(i!kA`;ki&VEP%1gtirKcegwBl6ZVlWd-9LIiar`K<)y_FKv+8gPv2S@2Qa2x^{>OS -77H3zU*{^5JN!jZR1Xv?tSIyU%lBsLbHEbYb1cP^+vM2jo8`wgt0(nXE58lYmH|6D{T7fPigwATGL+} -RC2CQO67HamvN}7h$8rp=OZ@fH>F-?Co|EE2hXuAoapsm;^J{sCY;{^6R#8~!N4LSMkeK%u1O`T73#&P;LKO#=zgsuAtReLr%)xB) -7cK>NJ&WpNc>?hJb3x)b+6aM$!c_V#ht_#l-3bW8h7?(d-a-oZ-bmAcjGnAZT&uone_`QQ%!>CwQf@5 -S#u4Ji0ddJj7Ymqtpix@`}fc$ylZl2(@nt|8Qr|+jG=iZgbZ3xLj7=ThZ@rvpRu;cKPFFK^5;1 -Q4_T(Ne1)wlt%l)DMC8cn$zT-o5oflbP71=FVAA5)NODS05+`*@O`LSbd0}35bJ$i8iKL7LU(r?(|B~ -nUBjo=PxmsAAihz*^$p);HE0KC^1yK(|BN<#IKyYej%|9xg>4plR5*3aOs6#C%01>i?QJRO&X+9L%m8_p4-Fx7jF`OoQncBwMrUeqwhe;u%5(Gj-a#m+!^b)kx%R=$W -!=iuYNW#JiZNG-eF1dtZD;Bbc|8z&=9B&-tI$cqqZZG%lX^uuHOhWkk#xmq2ldH@v0ceG~rBsDjpV2m -$SisF*zzzkt(Wrf5(X3#&r{KaYb#_1|-#9~R%O(}sFH4wBlJ;u^%=pXCxc!Gtd1~1;KaXz;|nM=|ECB -ywm+^cMuWh_XK>=mRI37r@LSS<+=_E)ZPrv*0T>gZsEc!Wp!inlG5@=`1wfzWT{RaR6wC>%^O;-Y;kE -oxuX>kNcc%C?{?J|kbUMVuQcSCk~sW+4mrL}Lh^5nz$cG`W#yAB4zC6LTtLRw*>|0;%3Z3hW9_<)jXc -4CBg0+2dI>!ykx*se3S}P@t_V(bbVLG?;sd*T$rh;*K&R)1(SDm67o}#FHSgI_%kCVA2fhSV)5Ds^F5 -6^2ODw!Jv@vi4)g&Zo8BSU$r@N?1aA9qt(fwkIOmFdi>h51(6@_7iI#{AbM)&Yq#5;McFR@&`tf -7#pq>d!u;Il>M*XMQ>!{ns-qsMH8nIq%`e59HczuL>K@Xqp?A8F^inD -Q_CpMM{Rb3v^NJw$NrqG`fg>{y;}LQ79{*$^joLhrG5Ijh+&@+0ce^K8vGKN#eNBF^7>VeW~+8lR1bO -Hw1^rP(EX9>yZ2i@-IUtW5P(&920k;e2{l6O*?GL>QC5i$xP6wh2t!*5D;m`XZFgO9l_AgPBpu)h*mv -rb3^6QPm9U`WYCryH)Fe8{ID2KW6Pt4+K;*%aBN>gcf=s73fH#r=R99@#8ZmC*jM} -w*YhA{&7BuClkF6>3WS$P&f-UO|itHp-N>PJwI3ak4l(ZbnvjL#)>7enkOxdEVCq;3yP0PHjmvtHA3_ -h7a3g;Lzp2H{&30`ZA7OBSxN(XAjwM~Ejg2iA8$rP?TL}acdTrSiW?3!jdsGRd|tB5Oq8#d9L^M`mBu -7UHD@De5wFdTYZ;@eUdjsys(V}wl<-wkK2NJB$rhZMnk>ZE0C1-Juvw1hTT^QxtK|6RF07axmEs8e7Ic4Er8qj@Wx75HhVru=SQa}y0cL6k$Y?A&u+%NzXfC$OIyLkrZf~Q -?~EFApyFpX>vOE9eGL*<7!Rb`0|>hytFG*UQOR6XqW;>+z2wK3Q3<1m+NW@Z$I^P)I}PYW$D)NLuvf! -1U@~Ht_SZ|=%?&gFM}6SFR0=NXy}3Sq-uhK2PWFPIw||9yc79bZXo^0R_pJzY7I%y%K18j>c9!+s-B3 -+^qBjbbth<`owd>twaVgY$FQrDaJE5f}3CUPBMp=1VCv{x9-p0lT>7eZ(NxqBBC;E<=M5yaL9m5%iYr -__ezfoaN{-k#?ow#~E%Hg4ugzD~1I2CjS6(Y4hS*}*|)&i1%Ovw=LNBJ -_mvt71|^f<*G&t;0p!V&o+Hb10d0y4oo(5X=FM|}-2qReJNYW~1$LA|X-`5&DOmqf=@apS_@dpu^sl3 -FoYs@4$WPUq@@c{Xn{q7FU -PA)5pRLvS7xk~kxXS1@Mpc4L1TRO!%!=w5qsRiZ3SMw_MRBVVrFYSyGBRld7QK)`)@&)9OSAV7m=d-Vi4xy&9K>-pJ;dMT(WlB4`3oyBICG$V=HV5SGEM~dh81YEku`?n&O&mtkut=A~_VUhDI9$!cvl}v=OIfp4TVx@PV9;3=gK1W0x)c@YZ5e-CnU_dR@*{l(Zjg^Q}D|JlUEvQ5?-fNhZ?!XVe;!l&J -9OteEl)%Cvy_h=fuf2~(gpQLx8}g;Dx3=9gOXo364V&ca03`1#5Ef;flP&?7m4=+%D%o|X)M`%#D7H! -dtTZ0mAQfIFkCK$c*%58gvL%9z+Ax^{Msq*7OV$#fczlc|Z5|cbRCW?|V)T1y -IY&jGe)`&*j5f)%&@>ue>k;5SfIQNB7p)0MVgh5wC68`Ku%Alh~GNM>D{TA{K38(cxTDn^!vEgGxFwS -|jL}HN!S>#-12PpCgaccUAO|=|XT=7r|VVsNd@;4x*nDP6UiWY^-7^Ftx76BXmCI|Z<=yf^ohalj?ANwVrMxxmNM{TCYJ-}Iln|(g0 -(nZy3uSUbgX37?(THS>OwzNNi}r;!bDN&t$k?)Lr7=np>~dm&Sl4N%^tLTNdwx(T%OT1*^>!KK=t8)J -XA5%!zur173{I0#8`~5ItBhiUKzmIal2 -hzn#25d#LH#%q%dzY7pVld56;;@(=*m}QTy9#fU+qAqH&@hjLRzSMmp1AsEu)681Y=(l&Q@T6KWStUVN_LV2P%;@9L!so3=xsXfOPu^sL7 -!nmypUaIQlVfB74w?~1*2ZE*2rwHFiVDGz+iP$MFV!Ls1GA5T7=pz9w#%3C^J!hj!rfwK;qH`@_Ezu+ -Y&mhi@$p+ty6s8W>PyoHXd7Tbk-^)VKq}RwDnjEz>-sUJRi+i!KBXo1LfyLQ4P|f!y26##tBLvDoFSV -3e^k)Us^vZcae+K#hyhTa{Qq>Pb>0L9p@Z(geJ(3<*KnNu3>8UWcG3j16PQJWAm&(f+d0tQLJ2f;KQ2 -d25nAr1|R48?GKj6LUEP7v7m0q+}q-qB!IKUB1pYl0yTr~)Ga3+5{y!9jLPo8Blz#z-Q9z4zui^#Acf -eFuH=tYk_k!eu#Sb=$Ag2l_T*Qb%JAO -x=eKkR#j+)XIlWqLPD(6UZY}#fd@%&xz5(M1@H5M!2}kdl@{in_SOqkfm5*m&jD0y!Qr$-zdDGRtd1%JaEG<92*vZBD=u0#7fJ(Of=SVFTliG6-ay!%N6v|LmOZj)@!g6Z;$=qW=R6c*Qp^_-@oGtNgMt$MbY5^2?24PM9>ukL -v9GcMO%=O*ldW(gBqBC}`~1yM*M}o<;p#*XVf^k{c+J(UmrB@XBtlht1;^c`JFD4Kot%XbwH_1Y&OTe -He*LZnV{m-Nmlo#Tj!JhoLBY5X7&TF=OcofT7=bI -R8{owpDF&6Re38{^J*(Ou$bZ%EZdfGW$~mjY{h(~*$C7wIu;-8m^?Yp259eW#C=>cHZ&L_Rf(hqCz;Ss@2tddh?eUN+%rJ<_&#x -frvUZaV4DP%n(j`bcyF2bb*-94}8x9XI)daNb-h0gQ^WT%5MJFdNWcpZsAcKdzx-n8kMC2EdCIcr9Cyp`~#SV?KTOu+v^P`V)!LB%1{ -$4&TBKOMg3u^nTUqrbJhBdT*m81Vc7q^y>F`;pK`)r7ioZdjkX2m>TvOD|>ZFstl0e)_t)Xa18uya -PLB*%0F<^a2nQ1mlN+YBZHQPhC>#H6+o~}&U4{ZIY~~P0uvw=Y)U~Qb?7*lO_}|#6gu4>u^AG42{i=R -{Rr;hjZ{hBEyJ@d+j+YCV`u-_!7YDZA?vZE700f}JI?Q;3Mw6k+Zeo;88 -DpVnf_lI#<3$yk$XoD?!v;OO~?J^WeB=o4Gy+mVJ#lKV_Edk%FX4cH_Y#k3m(< -J$8bpE^h-xEJkVdvbAMWJXi@T)Kn`SNUNDSZ=lXET_l83!|BOFvTmOu=5uIH;G@X9$xuRRd6R_vl(KB -{Tu^@-gr_W2q=_qBD=xCc@XA!RM(B~6fe3oIH+q$D)057EmFK$RCwRL7X-$?w4M&@1hEF(A1mTz>@bg -(#7Tofy}%8=?V4)Xby_+*YcV;OWnpUK}SO`d6jej(3>#jMDE=^i%Q$>4pF!F@6=@xNj -$7i3;&ZpxG7vUkK>qxc842=7}rH`E;PE`n~7=gXWAYk8lFc#5Q8P2M0mh5Y^_2Y5v$Z6thcG>z(Hy@= -)a2#G~3=xHUd?0!j{1P|26SNLFqT61YBBeARLqxJ*0EG+;B_ne=Ndyfv3~>Z^*6Lmz&V2&N*v(%d;}d -u0r?VJu7alCMV$2UWA7K7)3W885SRPNrR4+sfiLgDNgp^|ePm3|@s5GCiog*1gDwptMa5NdT7C^2Y> -WV37OCh%23H(>$Fs5Io%8H0Jy7fALY!@7XiH;M#*srvyJOHP~^7kMW~PV$cbF6`V&eBxq;&MYY2Wcpf -f8o$>wN^8E&k$VMBb0jwxqB=KP{8sdGPXzeBBr1+vdWbh)ip>u(|I_$vM4q{l3ooDb2K|%d#pUcAN+- -u?aYb3KldbdFY?5!@yVo0c^VWn4w8qo3d-v}_jcGc)ya(eypA82)#~ogy`^x4RJCnV0iH_M=jCNp7!O -Cece2H^>T>JXP7A3U9bZE!Nbz{<(8H2TxjAIW@-%^Zq0q2EaZ&|A?mGcSvBZHHnx)D`JxhJIvCdMy6$7U&CAC2r--q$%+DW(aOSVZUn3E0j0bem$YB%l;eDlHNdiCxsqvif@1F1s0~ -xqB#AU083kRC|u0aYu7Z;mNCTS~*;69)10~xAwaA+KViN$;Mo)p^z8W_-!^l9gHt3r0c(09##-b!9&ncA+l`gKW?sYGdf95_J!ARURNG1CXW;PBFja}-D%Q%QC_Iy`AoBAR_q_Ey0 -V%_NkD2Nq6A3KrSKm7FT6SJw%3vBa&ic-opQ$lHZP@3six5DL)W57OOO>^ZoRaGn7Zplil-KKYcA*x> -}h0g@HnSR+_B_h4r9$f<|uN2bGc_(O0W7Uqn1z8HR<=9F~KBB(0gs&!h%#rH&dtMw@Vv}iKje1~|JH& -IkCS_egGh$Vu9%Za(GV6r;IwA>g`;3RWI*99dsyz?|TOZk*7TuPKPa$d22xt70R_UX%4m5P@>FuWnK1 -fA{(?60ucQyIB@E2sce)yN8QQC_x#2fL9b9n7aif1suCp1L{tKOn7!wvfKis@OLNgWmP}-I;%#97slZ -0={V^Z&A$bwfeJcnAhT-!G~|qB=T*E3_XiXg_`vL*jdrY&Pz26aqO^>|9>`SmLud^%FTeg`Y75>T_x1 -Pz09l<&V>e*18$k(%?v!BpUC8+aH!{4bCXQ-MS$QFZG>s#Q}WAVA(!*f6Ekgs?5l);;o3>r?1EF0iI5 -G49Cn8z^kR36t6cpGjP-4XLL$Fyv?M1MW@PVRB_*TPUSF)$H*2lCU)MJ)b?VTxvh}*X^Lk_J9{j2JJP -#)%vVaN+1}f+6%^^9m^=Ht5N}z1LZBouvw$9r-uHe=^`04N;WDr?=3I^Pum{K6Ojf1K4{PA$Lj759=! -(*Dz9MiVu;>;w~i%Z>{^OP3@)&Yt)wO|phJe<^m@Q26p;vY1$Vm#zYTBGorv3m*Gru_bJ{ip@|Af=Gx -6P>iWz>ls7*RYYaplNT}mI?<355kZ4J4vmV?B7+Yl!7jizARHHq7dE#*`re& -Izt;rqJt>h3MFwgrJ3h7&XTwNkcxn~k%EuEv1jF>u^H{AK2%@)fb2B-=Zf=r;4SciN?{yD3R@ey~QXdo;nhdnHreOPpb=p6lb|>gyRry2EcH`bHMjDicq< -W(0j@%HW?-*7gWVwW1Wae60SQxAH8V8V~+jb&&tmJ54GFl9|imu#)71-cP4hb-JREJ;>g5A`SieYqjC -c=63bB-wKU80MSnT`m63mg}d?FZ^e$}KM!J^s9+yti*Hve#ldsE6%oMnu~IQxxKferrZ98M!uE2g9Uf -K-;HpPQW$$IA3rgq?+5Sj(Otl4?V7sUrhRBM&v&EK}KO3qe+Tr5q?h^{hrtQtU#L+9+F$LdL;`;S8E> -f-H-NSJFHsigLu>CkiE?@+);ZQTV7>_V$;}lb2UtXjxAg?>fw%vEm{#GkUSGK@H=E;E+HtK{m+8i)|$ -xo$mbKHBk%NGhni%jbieDWlktOVM -tdC8V%Q+h#u(UNfJwZFjg|AUN;0gMIB6E+$o0-6v0CxuSVwXZi1|9L0BlOI(hX1s0=uWI|c$n;gu3$prGgU+Q|<#?>qH#TlTYx94K9ZZO24;P#L>m -g%xY(oQ!^QpNHdHbRYnLX%AkkrH&-s_E|*`~xb)p|8JvV;aL!Q_-McPFn`N&99{WNQ2KC3CqV$W{?1b -v+WzUtYaryN(xb!vu3?*3w#1skQ1svJt-z^}Ba+zx^T-6~^Krdarjsy7JxV;wv&!YSkOmYu=uEW4bZb -(x2{v&lw}%$PL$mz^sn_pzN*-t`?9cdiRTe{1t;U-Bf*wurGxi^u)0ySykcu@PISBe1}$uU}JsS?s8twK7I4ZY_P0!yIGsx?f8-(G(RDn+g6~zSAh-^Co4yU0C -Rp3Y{)4G^Ik*l0v^lpW^v*kq`QS9QY -e1DG|+POY^%Lfp-Z%tx)xd6LAN4@ZaHqeN@o9h5=f}hzM(6>xbXikzxfk=r48sM2@P23awktXd8PU3; -(#gL$_hC1Zw!@5dj4!Y+aFykI*`(j-UX6St(A -@P!o>mP!>Qztu`nUsz|NooUA)+y&#-z@68}@4D#NapH3Tjdn393pz;4cx!u@k->)ptidL~2hK}Ky_>C -*$H>i()07P}EJ`)B=qbLd35OsLqAumll`=&o}hisgmGp)m0^Yx?p;}`v;yidjY>#Lb9dhOHcNgUOGef -MvG>GwJMn%4t#o?k^u_+SyB;O1*we9gN213Y}8Y0rPxZV^)e-E7SQt3_OR8@^|7xvy^2F;pC#O)r+Y> -#pvP`{#BFjZwId&O&LmnVIGi7toN~`h=Vv(ecO1{RRShh~xlj7*ZZQickr|QXhs@V2F%fX_ -!>w1KZ%F_*ZU9R&qt+8Zu+sDK(Bs?K7jeS1Zyj;_%GycqEVRqaOPhb7Ki{*qOnTy`t|Jf>*?#)qY8$&OLhnOSOTZ3D2K#xH8i -`_DDX>?y^SL18TT;6uesj$!|~A%-!qa%09;A#3A-2;1%%D5S1)#yxG56v*1_&W|1rt9TTj06pOU<~_2 -{MlltkUFZ@jYe>g)OX`tCZtdiKz76<@YL`+{FS|Mt7xr=3?Xb|3A*`S4O6xdiCDjj;sZ@Oh_m9E#89#3t&F;j&w`0Qe0gG7a43OEwV9y -?q8f`+DLa&=A5}X0SeW94mU-0RNAJY)*h8&;F6Ro{*dxxAd%-ayum*U4< -l64q%zO7@u;u@mj8gq;-^C@SLbbVAz4C>54{2aHqK`wU0uS@ngAzklpwMWQbQtd*YiB&FP;?UI<*B6a -;x@^Al0BA5IN)lAZNOBQZx#$&6Z(7x8v9Za7}{poj(+xDXsrwOV~`pY87M(zpq#_mmSZg>U%X$(+$m@ -AXyfLtB;YqbaL+v?TPx^^629oV=Pt8rg3EG_+qq1@-e&d{_5PW^qWty&Ef!XA5+{2($rmnH`c9F -lLGP9-=~nH3UjDwU9DkIcLb^XBHAj}Y$+D`?INRvXyiQdc8MsnV -cQD`>|)kQ3(cs+`1j!7#$kspVVd_Hk)Z@BD|3((x09gIuHtKY*frg($M*slB`^pN$WhhM!a^bh9v|D}*T=`6 -cbLk>aa^Z_EZGnQ##v?$?BnCtCs64mJ5S&CG0V?sBsvu(*)ioEHf%l#N@}Ct06&trnhJOEy?0d2}sTe{)-Fg#&NZjkSsuPIU9x*}ti+w@f2t73`6cl -|3u)%u4n}GZj|%1lqxSg?xU(6=8zBvYb?|J06~hWuUB4*xj?{WID!F)^0URjD@A^7WWwDv`XJ*0~_zK -uyBJx6c(_l?$5Y0|CUxdXW0mogpQ{e#4MNsDuXKsE$Zk;X|zBAS87krvn24?fW0G0)|l&jgcz1a0v4; --Ol-#DEzy^y@bT|^Qe&et>qE{wYNq*o&$<9*Ky5-ce{ZA2J~R>-3%H+JC&d;oX5t*1hWUgtg%6f*c;=a+8@0gqZLc6?%4B}L -hEHQrXgc!d!Dzvh@mSVW_AKv-SGsYciJW7-n)5;ybQM7f=loyURlU5nMQxf)7wV!YCfSs7d2a)QPRse -vgod6At9E216w^GMe>{N8Vu4N=xdf5HToh|O<)>yAra!K1z#=4BbdK4# --_-nIimJX4*jq7Jh(4eZ-Z(U`p#Vg)M?f)R10GSzOQ!^Yr&BC9cv3U}r*M*BS_$~SM`2;iLwJ -Ud6kxmvqPVCm4q@WRDdxlVqQ)T~X`-Y*qYRT{&KRtFqkz^1{WsYKGKx6l=hG_3Lng`V8}uct`q}oJFE -Gsw>VGzT%rG6;R+!Tf5(G@GNLl;^5qDL_t|5h?D(suUEPQEcE~yY=R_Y4yd)di6=Gy1cZZ68>+{7Fwh -ic&Mqoa83lEcp{6Idv`H*NXPOY17r1+T7qerwsh0YUoDoj5hlO&BQ5oIjQS786&dP)1`j?e(zuSRvP^ -bY>U<^YZ`%xGc;iG8>|YY0WEx3nO@-LNgF|e0Wq>+v7?+jqM#Y9a^cJXWch1v+jIKvM4m-21OiIa1yY -U!NT)^88Kbss%)B|*msf4v)N0xmW@Lbd~FaHzdIO+va2Am_VL*7_0uyrCDE_GGar#ZPu8O&v)5tC7(~ -|&9z9vNcyzDCmlPx%UryvSk$OXxI3fEn0DfKnZ4;F~r%Gg^PJpbAsn(>bXe0C`@ChCrz`dDpwD`BO`B`RFQ{Rlx?$f^drC$?`~s -wd>20zghLMSovn4sr6K0C|qJzsBVtsCb+h$B5JMoJ_)0+MDsT2b7s0x*9pfuRp=ylH#(7T4&`jLgk;P -h4A(qx|h|+)wL?ho)cCsxhf>CE~?km^%~f0_ng$n1R@vVjbdn`dhQLG63Pe)HcYVu?Di$CFH)^+C(lR -O^XXHRX;e0H9ot08;mjNg)LNzAtJE6~POXAp@sTZ&p26=U%>z?p$SlqKzy@DT(UlQ%ifo1rd2PUbb6M)-xX#5Rh -6WH`g@UjiR{JkH690hK!hhbIW8rUQ-xWD!=u7k)NeSY947&qi8e8wWsnQ!w!Qetg9n&gdW&$Cb+>|(mbVyopXsKGntDif(xAM;B)V`y?zywyD|I{-$ -1DPezr+HAwn{%w2ypj|+f-Xd1!FLKfv(7+Vm8MpZ)R81-vSei)160srW{PE$ -r#Gyhd(d*G5Iq>t5B~CcPvp*U_C64sHyJeui9IhqotohQeY2;gg$3m|fWL7f@pW1MOXEaMP@5t{mOPI -zXO?fya10C7Npr_Ej`~VyCMm?W7+Txh0BE4Hf?Vn6^&254dOE+v5PU) -pywT9KG7YC>&#Rv!i-*OS=3?ISsjU8Sz(gKoAu7}&Uz*#m!)l@8M>gcb}g+KNW0HgH&I5I`+QS%(jvJK^!9y4=~yjX#JIZ?emhG)8 -u(t!=A1A*l(rrN~{}N%1BJ -p~=uYhXb;;RE_b^s#vf&RYsYz;kFWKo=*h~0I*eEQQ$650<}c)&*TwV5{U|eAFd_U7P>UKM`7G62Ic! -XRoWZ(ll!gqgZ5|1=U;reD74jK70OnDFFdSr1^y1TRtV3(nyJ$BV$SYTxoUynP{O={EO9mFt<=MOVX) -I>ce8GgQO=W8;28TZh2ghEPul@jsRLwEG`O*i3kcfHO_oF&s(SRV<57on@;)I)$YwiuER03KESO#BsZ -W*?hk3#loBLaI#JQFLEl_VkF-EGuj*Vmyk0jUG&P(sAe -Ji)E^qHz`0Y?=ZPq`a(od3E_w7?h#562Q`Q5F&Ar@{q2y)*x1?;QuJ8=smJ3CeZRM9XHx6RU=xnPLCze1>M+FdUnq2zJN$_#zo -%b|!dmDfke}3-YmVd2q}~bHGFmc+72#Eqj(>Xx7?#9XZCg8OmX_oeR-`k(seuJ~k(#adJAJQrF;U>-D -X9hHtNdsl|C9ByVPN2W5=MeVFM00v|_A%xIYSk -BjbAk2b8!q+I$2Bc_Ezs-kX+l3{K-%n!EWNX_%g)Yap>1`v|1B>_;gJ@C(iQk26&dB;mGsZCd?1Gu2p?KnsFbCerv>kx;v$<;0r$TB?=incqluD2~7amA(`A -1lJ|2qu1|^s%Fao{)Up!?IeQ*8ApzBSKB~;wI$0BKW^cx-h?E?6#8U4shRyzo_DaY=r}%*n**iX+YAV -uQZTToh?4Vtfr)emy21k1Z>4wa}~8O3st)XJptKcSrZ&zqA}%_#qUbPs -M~KFIc_hFfZ>^ikNel#j+U+cBvbmBk5Lpx9ku#eUU%@wNvVjn89|aC~%IoC?}HkWH#AniN&*XM>F0b* -P(g*zS&cKIgI&E{yDSj&Uyed^QhCNK<0svWiiQlHjjo$Kq^o| -2XXs^OgwC1chhK5I&(Mt!)WaTCNd#?;EzPq78C*Fj}GaF|E*TSnkZ;Wlq4*!!ifXLOs&^kEut3J|^F! -{lOvL`bn0sxFYXx$o#f?V0cvsIF0jw78 -vDmbpR{Es?wMvq1KWcg9t^F&PX&3Q_)ZN??jB`Ztoq-v3t;`>!PSUrFo_mc$}VO*@N%9kN%cWK*r?Z%jZ8lQ)b8cc{+jFhM9CQU;X|2-IsgGqy3kUp!UJbeE<(%ULEYdOupOu -arfKj&ywel4)Cvnw9p}en?dr9beDc@Qp|PRe2t$ffRk}PvQ_X0)%cbYP+I?hcjPWmZB(_>z$HjMHL9Y -(5#lU7`b7ZgTa6!R#Pokr9fluN*fxPrDjv&&F@taoCN-W`GnEO#jO1Xs-R~dgBYfDwpAvPZc-T1)N>IRCQy$@xZSccD>Y`< -mzKOta2R;qRZ9r8`s2dR2jce{VM4g*4Obt1OEX~Rpp`F^x9O9x%Eh0HLhmzjymW)y4>`-+w_Nct1$Gd -PK+Ku*QM-oSRKE7x*nqA`#m2UpSl5kQHG1}_adkWDu#&ow19^%B52?)>*q4?TLCfNOw__l?8;00rI+| -??@*1Ls(ZNEpaD4a8BjicHry~D#PLIrBM1s=h6vgyAm~8^L8em$6iM|fsF4C?12Y6jO#djimC*U5n12 -BKgFFD8M|re}r@OhDQb~{f-^bqH<^k|<6OqB-z-n_g=_?KOf3xYB-9clWAlQc0xH1-vq -iGDX2eb^{Le#mQ>!udAwYD7@RtqU5Jo1 -p?&ct%O+1vM!SHxa83+eqTt%OC+wYtcotwhT|FDKy!OJB-pqnl`WCRATWOwSMHcE85w_8{ke -`pUJEETDo~@sc343Zne&`8PTjd+up?>ut(R(%s{y@iWGn)G#tOpV%_AfDu6JN3*o{05^$nPX3W%0fM_ -#$5#30z4ppQ%0T&(Lu+QNkL3C=AE7zo|-@}zurcVVv-gKhIc&sEO<4Be#CY+V@#*^04UXNt2`N*W&Hk -2lJhK^*YNLFe=&S}xt#?jXMH^WC-PokXGVm8OFeSaR5Z_PCve_XR>69_0Bi>nUplb!4U2w}ng$)u) -W0^J*9b?;@9X1Cpy`+f*1c;t(%iu^TSB4jcR_D6-Cy;aEVYgIli1;m>kzu?%Ze+*o2h@ty@#P*_DQjH5U+sB>t_*;)di-6X_=Xj5c+xj=ES+hv#6=tDn{r_{HR -y6W35z{BtcvuFl~H(OCrH)Z}Bh;*=l%l)&D)?z((og;#jb&A*P6+8wd=0#on&o(<}BeI-{#aLyZoh%( -m!C3or^K+S99YC(;vKU16!%t`^8|vu`=;y4&DkZOhT9L+N^)&{dJO@&)=;0yV~BG8Q}G7x7qs4&USzx -RKFbGoIrI#6B+JyFNnvW4<(75gQ*E8F^V?WZ|X1k>n>2FJstnjP#T7_)U7Ar9Bq)RQ4Xe%VyrenIte{ -e0=QSW2p5ph^}{G%y(VVTS==$oVCf^5~S4R1i0of?@OHHx7oCR+Be3{PkbAdT`TuZ^`%O-^h^To8 -n?o$19aFJf-fz7!?V-K*-cEz=n@%lV6o`>g{IVFz};Q6pty@S^xPN>gUm9zU5d -%a{3JqOWEYKhz-|oIf(xD8(vxgpBSiOSZ0M~r-z3#dcM}OaFzsOa5ObR@QTuEG2DhyWuIaS|uv}Rbm5 -o0k-=1^rHOnVZdHC%@1zNbUA!p;s^U*fd!K9t(sIxAl4434vKBL&QXPpiSp9BROlD5DTC~ER -)MN)?7Ya)p8%?RRT{iM46qu1R7;{2CGqdj#_ckIM5gjUR;FpraG64$rNP>E -$vU>IMT$TGz^G&Wgf&@4tinOisN_~^8t3`-z&7P?LHug?HT(@PhI2mkRg2BJiBqB -`34_J{MKanKl_x>RJ=a1mM^xMfn_6W0Z*xqoWUAE~Owhl$ec+=~LHk^C*YWcn)#5YC>4+HO)+JR8sUc -^SE^^%PkdwQXcX&4Xble)1P`C%VDM#NglXLPm$)N}i*PgsBG_wlqz_iDYH9EyCz=wxJh`@MjQ5!EF)9UhiVLuo_7%+*@IcNY|j~6JfzU$wnX6DCazMd$umOJ>0@OfR7&Lou|7$cJ`kgEa -}j@5A9AgnPaiJ^%WbDyKhrXz6DFbCO`<2tC9gIUZWJ(>ndtFm8inpPYu%HNe}2Dun<7`P02a|EwEHWUa?j9E&{bk!*<)Q5qAJM -!mo(+1^hFV*oKky;+v3$(g%Z#k5L39ZMt}jFNTA^UL&-qNH8zh*j1uf3n3a -W$F->UCE`Cb!GdK$2^0K_KY&%Ou1)$Koqm@-Lz|-{ld5Y4GlNgxWSS -mAcOIKFP+_c1?TKBToMudjlK|XGTJtrVsA4zsHeoJC?N|B_L3xuxc>vV*K-h@(G+3wYq01GE3h;FF2bQ1?L(<|M1xx29a}gZOhy#?h>Ct -yT8}4jrZ8fdu*p}JiJ4#&dGQTxEpnVXbz#CwZoS>1W$JjkbRkSr}JKBs%p8SNs+|nlgW5W+%;4N;Sf0 -eGq!UxH|z|Hv5EE0VG!t0e#v=|`y=3)Ve*tjrMfEH*X#M3vem2EvEvAIF%QY&S7AaWOfye8R%un}8Jw -?k*lw4}>0q4BJT|`IdIGKej$Bu*(ozW|Xw9#v;o$|lguA|-{LXZL2|FEUXei#%n3gA^It~a~4~&R(k& -bBnO{U{XdPZrj*{L@7n_7W$cRblQlsRQSLg2=%q7h`MVukk2-Y2)Z>4?bev+Uh02zq$+I7X3M;25bl2 -9|#|>!RO(jUjv`Ga^Zb@>Klx$W(8^`BiQr!&=^`sOdvrMcgb}zMI#;rL(&+wO1w)Vxa}rG!{syISdvk -q_o8r%`1xyc`Ue~mu0u>AEjGxsB}!l-}ws~&hVEoQQPulyw}~ESel4=$C#mna)MtJ=~;(vy(#;=9W_4 -Rw~uWdx;yN62yz2g!_=5pbqaAKIvQ<0IBQI-&lk~M`9XMUsM6#hI(bG>3M9fH=N7f0ZSg6b1GxystMtK!Hj3t~0@xNNBCA^}kU;#?n=0 -@)oKe~v7PE`uvtJDg=RdsesjB&!ljgbs4Cj5hyGr1;-NimQJ>bt(u^!`cjVizRI1P20!Z$&B@<1$7tEjn*zxfJ}4RXG0p^CY4h2u&|Z@$8sh4~$+m65?htEl -tCJ`?YW&tbiduva`(jzJe#Y;b>qsCpz4^ZKzt6F3vP=_Z9$csuU*r~|lWxQ$|BC|m;AshgRJ)vaQ#i` -mcU%L4xns9GXxSJR@ngsLPFaO%ct>M1~@35LIAQ|Krp4106peL -o4r=uEeA~5VE9DHIbP2_404D^7GO9??iF#Ho!pO)o^`YfHEWi#K|X8Ok1N*EF;60jWE)IAfzq6xdaJ3 -u=EUudAshf)LJ8@D^>9Vj-3Cd?>NvU9$c9Y|E*rg+q-U@ZdR5QoAS0 -mbBvbqlFK%ecJluLH@5+7Kp{ET?c{y(3RAlE;YfO)JkIj&w9h7Fi$Cv^;8zuXVjv=fpVG8vSHGlruliSr{?onF#IncP;QGHws*gan3!aP0b-Dnb)(Mz$@ -mQByjC5I&)$!!^$5`Rg8=B~U&7m`eb_Es@D+D1M{~7{bD&bd$5N<`*%%5Bjs!1?_ -xft@1-x*YwD(nQGJ}deS#D^e)Q81o)CRZ&E@vd-%qNOOk^&K+Dkzo~_*+_1@8NEuWF*!J0Xdm|4@C*D -g7et%6Odp7IvF5`SVkhkha8>`{Ygziz+m846G=#nX!@a%5V>r@B)kLO=e9lT@YOiO_FtB2FY6UAiJbQb!3W$fLHEgeXu&seN%SI -Kg-$rcD-jD^Z8jha>h1YR-(~05_R&Dj>!^r}7!Yb@>idia~9KV^D@n(1m%#kn*|!X!Py&unT5F8lBq&Lb_{}81m+uPB~L$A5&gm<%ba>- -BqS@5IX?YdAh#~^{^6l7u&N?(!c8r(t>O|(ll@pg{KfCIGmCs?Jhz#lLZb6=Y$D0!qG)nL)d$%qDa$t -;`cHT-RdkNZasF4QI=`Iy|VUNE_|$8#_>knfd&XK;ErvT)|wylvS|NmKJ4=l9YYMyOU7V)4JK#|Cw!C=M+!Ac4Zvs`%3NCLrJ -19$t`-pxg;nZfQr>g~WznuiO}Mx(dg}`1UZkE<9bH{Tg&)dDExG;7+#uR8OcQmesEJ{`u5Bl6zxk3Z< -c2#%nNbYK8}VGq0otN&U@6{lo`(7b?Uh1COXK;>-C1}o<4*9EJ=e4IR{Z+Oyg%rn*dRI_4%TT4@G4}q -IXZ=XV!~(L$_!2Royf2x>)}GZo`F#>>ZkNk8PSdot#juGDMf?stx?|axK*Dk=0ea+FH#WT7OJ(R5FB+ --N&G~{7W(Qyq@GG8DFq?f@%bZP=VS=|hDNfJD#gR|@eRza6tPLO<2L#+Dw0r@prZk{UR&1i -gK!wbN0fWENagtMwhza^TCbqrUIL|a=0Poifab8RZWA7T|5HZzMFu<;5)Egw2Sic~>fuDFTO_DaC#0{ -<2JWRX6qK{Dz&sk~6Al^@PC)zlB8;#WnfM++zOPLCiP^UnubyLf -gpdGom^5BJskE4sqVT7ZfQ2G0htStsgstql4M^ZJ+Lkfl~p8+UykQSzd7i4`!mcBhh{f43ldfrR)037 -M5d$S43-SNX2&ku@6mV!Q!$g7nsXRm#NxJ_Xlt6pm?8@zN;*H8j$uecGf`QPsa1Bon7%u7MW%TnjTrbe|if+_L&amhe{#6^SRy^?BSke~NYXMa!zIp_1c&2CjEuWL6+7h~bawqhjCI>ziRF0d6 -);+f!YvpcJB+VC1ER`9jkQRT2~7WQ(*1@|3Gh1OQ76NbuOl5?ykg?3Wp!7Wb>j-tzFP -K7;vbPk1`&A$-kXWp*TX9eY`%j3K21;K_>>25SEVs9{ubh*nk|1 -B5werGzh~nR9l{TrAqWkz64ms^E9a5Z?zw^KdaW!8v@-Rs`v56r$q@4@r65uZi^s?n7|9FQ3hD>Owl= -I$4e2t?3l4IEl^9uRKbpO?9@FKWx_4+Z*?Rtyn35Mk7 -I=7j9r11N7S^6ix3ZNLw0TBNrnA6;7r}7u#f9)z|KjVThZYqgGC{CTu=bRZyH9S`X4XJn>Fn3~(vEj& -LVSVtkmIup-RYC9+WQ`puybUb`OwFYPCc@a;F3r3>)7>JQTMg>6LuEnsw_lNt}W9}iqG&e@P5et|}$a -^MG>2RJ}~Db1OA8!}`PCl-t8v}>m3T4}Ub@9mj|z%_+rxZ7ZEoDs{ML#*(u&9_5~DqBiYxKESj4mTOg=Wa`9szAMKx)}lv -`-l*j#i3y?0Khhbl?7xbDsdc6QJAGpzJBiMIGW`#e-^r2T-;Hh;=d+S{#*j -3I)267SvhMr@RQkJ547XS52-$ICe>QJeUthhO48i7V0%dy(GB*H(i?tWW0MqPD)U5K7}eL^XVX+RHA+U$Wien -x}#42j<+ZS)a-CTwqS>^=rPR3Y(X$J>v!#dP<_eWA`6r@k1N5GM&;`0;DxV+ub$7wO}+r*n96Er%7th -Ymg{)TiMYU{BBYW{46R-e!nDGl -oq&29pyV9|?MXb2oKEwqYSEM`=c4S9iyBN!K;m1Z6dy_z6T-Qa=<9M_f7o`m>ROPtq$qTr%KvDB^A{R -u(^!0Ty0U`q%ZMLXY$aqwu~24iaXx57!I#1nRO7%VEF6W)%!pDKn(Hv7%no$B5OD9o-=UMW3jSp5-6< -v-9T^tCljw^~2PNK?57H#NF7z_K}~$G#1Wx<(?RTh2F%p=SacciWeVL6uAbep(n8Pi?&iI^4QwpWGU? -0GIb0<-bMmiQ^U8f0}OCp5%bcCE{lRyt%M%cU}RKF9NMnd0$_z2StOvvMKQ&JmiNraQl_kRJhz<1a*m -a@HA>HhlQi^+Yz#zOTJZah@Ob%nJ|$g4g{6aTqv!aai&D<-qSwK$x8eOo&yVO@ZJ=W7q8A>ariypcbM -yXrK0ckHe}oZpbktW7FnjZ$&$)GOM%OnMGY@O6(&l00B0b|hkr6wNut6MQ>DV_Gj6KFnkWkjJk{NyL+HXD087Kj+YhKdZ8lHV5gqZWrlOK;G%DWE`!BkiXqnIm+ -alq;w8?I__`J9YNbif7Z(+|^>4t-qj+S?{?P;}c-KA%b+Zd?Qui3GJ@ly0WG{Gc9Yv-5H95!49n5)ca -M1c6?_;D?ZVzAB_9XK_iNylL2#JNwovpi7Gmwf}NZXTwQG?Y2aVBm1IA&SN@GB -A<8Ha**&a{6_UZG4VGyEK^#0%#OTx_$Nk!Z}lFQk?$b_-)qd&Cxy&ILJ3&PDs2civ96U6sc_HU-^`JjjY!#};jDgN`*X{DU}?(EoQQ!;j -1K_GLKLI*;;-biYPQiKGQK~MhhUnnbop*XM|zy79ld0Sh?x-#B_1Q0TV9`sIsdNatCvgOb`^7VM90W~ -{yyZe{g-ATGTJ=0T(Tk9KEMV{K5XSem40%39ns(9GEzv(3wMJ`fcCOgUfz|_dWmg?(AaeEHFC^O|<^O -rdY2fb`MZDEwMtX5GIcE?W69mM(_pc8kd_17cH(P=>8-S!CR0FdRnP}2pWaoJMi)S}5TctPe-R4~A^; -QFJ`TQ|3NU>-Afzjh}tb`)L*DLZJNFF=M5o|Z`r1Ms4liWCh?3OM-1%}HT72nu$M?ux7|T~!JqtKk1~ ->(=dCx0AEM_#_=9=hH}G3yW# -CLCUEM$H-x|3K#4INOZfoNuX$W)Nac3@*Ko>zHWV+jr=qh4K|UMAOfsORyXcc9qq)Y{a!C4`!U2ETLh -bdL8cZ&B_!0SsW8h1189zt0F5qPRYQuH@oP!X3iqofYryi -I#yyfE?8AWMZtbUxmi5KkN&52~DCXv)imf>Uj;SK&<$c`eqkzz{c#9|{}PxEZk(8PKCKDaq`ubalO5E -pW1uiVlut-88JbD-p#wi_{z4B@KKFrQ!?3h#Z -|^Y9hLBKz$BZ=;%a_U=HJ -t28_=SNXIx8c1AR4bQMISZ!`S!6NOsKFQ=)t?rrF#6c%Dl(&YHSQiPC%D#Sr&tKc}rb&ARACX8aAkq_ -RRf4XN$p#s5Sa4wAnUKq7|dzL#6$mNcoNsX#ibiq5rIy_Mi@GIvg%gBsn;M@>`m4466;(xcd*imot~k -)_Ho(f*whXA&6Y@5g3=$H$fSX1juXAab9mdZI@gsLn`|hgmmem$*K2>?WL*8sEUANp&`r&W=)utx;`o -TZmu&u};lUx?`LrHmUDrIYthqbYfJ|C@MV+&=J*29*-rT9CnkKMBuF7OgUR|ZklF$zMilDgXt=vZ<9; -XxF3vP(u`))2zA5IrSYq>vP&pz4+!5Zyksf!z?N;4%uJNLu*eKcAUkL_99~EhPa4VRD6J!vW>_osY -t?Cfz=bUSk5b%2Eo#^O;nBw)|ALbJ|<8VW?;>8CJy+5iH;^5PQna`$LfS5+Yrxu%^L8If#(}nBrb%l0 -~0%Q;r^!00K0`8GC*F)Gp-kbDvR2bScP(MfM#3clWpC#2#Jeb9aLHES*>a=ZW6L$u`!g>jX#d?bh5mg -PO_IEJG^^|Ieo>~SMZDp9uqFYn4^Gl`DXIZrsA+DP{*|L8Xs2F?FnA%w4kX0nu~WEN#`UVU~Is(M53J -dB`U8+l?K8wyk_`%#;<1;Z!F@ZC;T8V&CgFU{u`k?r*x+#cF8>Ffo^j&&(!2WE!VgGJnwhkSZaz&ql~ -0(<@KM)`IGYw9;R=^LX0o(#+c%AFb2%#vnG=(j8~~G8K)$#p=duk9iSPFjP4BfBX-}9F9Q7~hjcRS*G -XgzUqQ?68*GVP^iQC<1ja*dVsvdt6?8yVo-)jxFh%8IYJ4%FVRkvt>P9nK%AVj7DnVg0^HSRsHPFp;l -!aLZ0iTjbqnD7Wt8#Og>IR&d%>W!L0ufqD;yT7R9`=7pSs+0gZUCNTBeE8PVK5-Rp8ZOz_=x74f~H~| -*IXN98q_-2r6(FqK-TfGTX05OiQ~f$ -Kmt)VYz>HPfSL6$n9K#7loTF$4wDT%k@O1|xPwi|2fvq?y`8J(~mviP7umUG#!8FB~9c#arf?o6Kw4a -Y{Dqcy!>-x1uk8BOTLJR8o@e?Z#0Zw}(Np^R)H|{47x3+h8;rmW|W5IRW;bHY5Dj&AV#(abA5O;U&qm -BFghzf1dBY3My?U;(b?!p&V&8jMiM-e!qa*iO3qJja$Oje-j*_^DmQTfinVrb$T^q|xowbDHw_q&#Ev|}v;}N{Ov($i)7ebY7Fk2qNccE0KQs@Rlk1CL%NA@=5ThJvHaSDt9~6Dfln{a+)2x&Yhla7d`?}jpx?A2&(>*?fE{7-MfeKmfFDO1Iql?Ma#6l) -HxC}Z5WgnXb_A&}%K>kBBbDQm>Em*hvk`}=(RkubnUtNx7>AN4ejc{oSO2|lsJ6l -lqKO1viVYiMcM6^M=D}UR(V{I|^&tyq&B%_Qs+ccvCoPspj7)v&h7cLC#@OGP72Vk*e`w?58pO48*4v -?bKfCHA-9ZD%;T|e0(Azp@the068cjpvtfZreXeNi5{ho8jz&9|?$6@b3BH%n7Z(rz#zbfBpcGf6bs) -DV9{s72FF{BU%(@F^`w+Eg`YrRex~&Bmlyiz73ksxa{7Timm_CZx|n>-1W(be(uAv#{9M)Nna^nf)%_ -6teLu%^e`QmG|Pkcc^)KVdpQ0tQ&zZ6jYA~cC^SMK=eO$>1=Z_PU9nJo=31O`-a-DRSxguM|dr7Dwat -c@h++>v?`qc)u8be@a^};AZePlxE~+daA+UlcOal_&WghFddzvAQdD6gaTLc7-#Ff~gtRRrY$Co5Fv2 -CUaX>H{Y(yrjFGW20S@yE8ZhaA-vBTYUG4U -6KB=l80^+IrW4vQDnxKPT!zG02+nPM;OSCWfx$*@HD``#wJt*O>^(rD8#L!z&aSZ4f*qkzlb%adeA6}Z{jg9<_txYPzlY|a2$AkqYVI!=q`O{Mb$x0PvD#s*=vSsU!Vfjbl!x$!Ltxg{lLtxWXJ@^S|-ubn2v{qpw6%=UX4w -bM|J&XRVf}<5G-9goZPz#B7sgW>ux#fDx+**`zw_q!1Gj6F9SQI! -MOXBSUVVlTsxOo@Z`o9X*01A;j>Xs1F+xq4V^9%#>sJ>JNg3E7Oz!02Cp5WWffz^h<_AZT@apEtPaI2 -O*h4W28@Yzz?_{L8;vYKoV9{BHxvB`pdUrFMR+VI$YnQ!7}^u_{6Q;|@uXG_nyS_}3ooF-YF$oci==R -hRMYjKq>4Rju20n?eg4@xe9Fy7XD<9eTbRO@`x7(Zwd}R%4VD2H^csjdIuE^~{eZ=um|wc6Eh5GDUg)A))jF)$&rl*Vtv>J0imgI}pI@V?!}cbKr(Y0wW|alBhr{2!Pn2O+6=SsIz*Tu+kf;ZfCtr*xQfHu`)#b+Ll0@u-4{Gn+d}Wu0=FfA;woUsj4G)8;R(WO~vC$Y -x -ZzS>qR52mE|XIuj5CWpAQ0mDpxZ@|*ft%UCz<0EED+m{eKQhMDcuL+X;Nz@h|)V7LDfv=m*eBtO)(pU -4;hNQJ~}?GN5GzE@5(_RA6H+$t2#(Efb(vnjnenUc^?mCyt;={pHwxEP^W_H?{43ARZT`?)GU*CGaC{ -?-_b@;tPp4Ylm4JTyNq>o+?~O(v#_h{L08uwEbpqD16p&ef4MaFQR&I}Ed}3Xg$ntar%8{HjWK~&D&bc+o$x|a^r_yho^T}YB2M~-w>@Gzjo%QsPjLOX9`zQILSwjCcKc-;QwGi$2 -V-~kXgsKm3W5R0E|8E3av$SGOyIkRY4-EHKULOCq6A8fYggAq1oR{X)@A9Lp+ptgw10Yu{Q-i&@jpfs -03`P<;{jrg-(fk57!=&|#e+;JpcT*WjV%al@WMMiydWKUe536f<4=qrku%2xub5$G5em@@mlRV41WLY -PZbeWh#&-6KY>#kB$}|Z{L#B!pT+ym0jr;)8@F>R{0$v|mzC01U-8oKxX)6^g!ic724o)yb;^Ty&ZP) -cZ_9%6xXwUBLCeL<@ERjYg%*%Oel{Wdg -vy?SE=hYJ%Nig$t+S8A>a+2MInRF}{WoP0n@^L+{Tu~dSW;Ds*%ga>E|hc9ggRVO3U1`J7olkJ2%>PwVrGUOO$$^&+jA5Jv5+E-TeYBA+7`aK -|shjqCMoPsV&>CWWI-j&Q`q3#nD7gG~?BHtKM#Dr87F|ln1;Z)D!8IN*bd}y8$sGkSRh=N6yYYW7%`X -n+Stv6M0kbWfB%SqU`nrfxoFj6a_b>+`Maw&Y<8Qb`>{G?z8Z9^|aU{QleHB;T1f=U|1j6~yInBtu(sgAj^S#b68P -7W^$R|Rv;J(5MLQP>{bO(D%ey;rJO9Gl`8~VyVNdolj653Ej#`nnUjfZ{rn)Q -hWQ%H7fj(@67U)Pv$1pePm43o3@M3vqs|fFNANG6!>C{EG&kL?Dm1gHdxjAHA=gH|lfmCLUn#c+D>0 -;qWteg_jZ-rlqZDg0l3E4+R_gI0WYD7m;;^Dsk65dCw@HzzM^lgZcoSf!+Pzn47|~<h`{370 -o@GFjwzYOBzzkHa4#T>VLi>-a(Y438xbl-4${QdxoMCY0bBBgZyEW6L#Gv6mfg^l8>6PCG-8Kn{ZP8G -TKehsLm50araE`kt)Fc+|k*cbb4W#Hg_qtbtm6J)s7GaZss=1DSXAf1wiq|j&GgQ_cb@f>X7nA>;!<` -vkesAr>1X_XTUZ2Wu@8N{bF(*hi>~!!R4X@mAnATFB`tICtH_?@z^lu?t$Q)VnxW39MS10QB=SmC>Y!c|?-!LB5q8;kot6p>3)Cn$eaoeog6^uv9I< -~0l#kf(%xfi*wh*6?NkrBsR~=?>psr&U7=lW_K^l2lMU*m)x@Oy -FmQaE2{NPhRnNEetcucmYj=lMNo~-7lO+|ze+9k9E=CEOrPQtKfC7T|J(3!N^4jOV`M+13%`vc#EjuE -R9h&IGN>8!HQkl|CPje@^!m5aAwTi5(A;@M^{`e90eF}{lv8Kxjl@cjF7M)+LAD@@5A;n!jh`g64zFUcjLEgc{)pQ4IhXqKPLD#S0=?nAD+F(Zln5$0KK -44!=C0^X*gw5A7e2;F43RGvjB-8HQ+@yvVA{G^p89r>8P1ZqFQM+cd)cjn$2-P%f9vu7KXx!EHdVzC> -5)E4FY*kt@{s1!c0&$qp*ccPlUMA^ONmAQKtM?J|Xq<4cae=ygV>5=ahklioSly;06Ur;_Y!tUF`c=4_ty% -|H=OW~Kpq|svnd|+)JJ&6o&3#Q3O7~cRBWfowsJ(0^$=^7QQqc-gwQNx;70J(#wNpuSAqpV1LF>36ov -C66KKyxG@?H3zf)$|bCa!{V+{%29<~w}A6PMGF49sCj5Oz6Ga85@71%#z7d;ao97z@wxG1#~FiHxdljG{aah5(#rX^(xzEj*u*!O4%>c&h4RXP}uhds3$+5io7o(d9C%H6rnJob# -hW9suf4z10y|vI}%bE5w#VsBPmaNX3^9rv~DRD0P)JMvHuz-9WKN_KDj!9xsA6=D=IH_7U}{4#lS?`> -d~0u_c5TX`u|7iz|b9d02bXNZzt09KTV}I(&B&(W!^o8cv18Kciglsny>Y7zK}aQ<$xDB#Y6ik-;RXh -)Gh-)+uJ!qReZc7lM+xo_(^Nl&mPWfusH$8bC)_-d4U^H(l2-ivZC%CMuj%z}Vv9FOamaq5?{44(DNq -)hzg8^(4JjHt=n6?yM=6mYve+s2fp3_9)!0RvlCO3awoz;4_|mxtMn8Bhs;N#OFB1P(mZ6L|HhGPm?c -{di~SWeiz*@P9$`oJEW~go5uy=Qey7t;~t09iIYd-irk^eO&}KE{nY~AOe|IT4aP8@?B@9xy$ -FbwWX}v}phohp^E-Ep^1K@(dEpGwv?QRR|0$A2pJlD2DJt^>NEcfsm7B)$vUEOXr*pP|1uv3p3U$H*;UMl6h -FN%yIektB1ixFItwujQG|tP0%&sm)k0!X##!;(Du7vt$zojk9W#_z0LnP%0E7b541VGQF5sGUn9^X<> -R-PqOpPemJJ<3y$=9ET$F1tb3l0`uUI?fyZdIK=z#6;BYcUyXH%@mJq}(jt@y&vYD8*iZKP9L)$bXPv -gv8EJGd=)xxTU;2j{I^FbR>0ugJ;kl$3e@5>&adTMWALc-iIH_br2w>e{wR8H+R@=+y -@OVH-AosQ<`ZpY*mZ_o#{1uvcsur(LF@u`ZaVAMDXa}3$3RorOImVtDG+a=4CVCnlhixY)`C3{28POd -#`x9N06=2GQm6q3CD3`0r=}`=5q8b6o<4J9+J0|f`nE!c-mhA7yAbvW>K;FZNXFpKnmHkEz4?k@r)itPo2!y -?Q7ruOUbfjz|!yX*E8uyP}4Ob!P9+am^*ucX0g@3l&M}brHZP?or(q=2M`Wwmd`A{^>y>TWfFvfu=3i -7B>e?IlYlH`uXPdeW`tT}*6;9!jpK--6%5>MgV8#AHEb$lT6p`5NXwX^9ZQI(okJxD|--Wdqir2pj%G -k(B)w9*#eRLdp3PijP<)~LoBkrXa1Y3q@WKPHJlh1O(lNOfBH6Izs)F|Gv7gbb*)q=tEe8cDr=xcR_& -x#KY`hG!r)(NC0`40CYG(334V+F4|}lNX9Pl46Q)Z-Hol!fu!?L~Y(VU{f(cm$XVMWJj!R=tz*ji -$+X*`3cr%E**Uv+Br4C|519`SmUv>GS)mA*d=aP=B*nUZ~KDO>Jo}*e62W`~}`* -1_|Y_V>21+*=1Vzo%A;U=tDM$>FD67!Q<8%|Re@{`&|odcSoK`;KkUst<~@K?Si=!C!0p16(X@9r}H$ -xD+;6PZtp%tM!_u|*yD+Zw8B%neJT1N`{cQWoTEtef5X#(_#k~?^9d;hW6=- -?UyurL;KW{d%cO`^Hcaly1gpkKWuXra$+rMMC9&_w{%3R?$5MJxsRM$VS8(*_p||mD<0aCg)g!TO9OG -;ousQEuC%_cZs!N{8`Zoj{!tH1U6F1p=Fb4Kj3NRw@3jJ&(gGUnzHln7K!hqdiU1pe-bN&#sjDon(s9 -EmjBfR&;a3`z*0#s+HQ;n}`3bydJ%tzJ7i59)49Sb^P=CHNAOv^!}ZWb_ -0jaU5ZaztyBvEdb}7Lz4097RkAWg-Yh>`(w9n=TCA$RRIAJS!a-ZAmBoFLpdPj_KYu)=l+ls*$qf(F8 -v|>*WSDr@6CfW$OpJS3VZ@y$wd$O@9`SiljY<6=`A5CF8C^_ej47T}zBBuBdkEv3}(i#S5~a93%4$;>Sc4uO}eZHqNwCxw$Eiz{YoUC0{hrHK6tM*;WjJ)QiYy;>Z -PEwXPD9cXciYMF2qnXn<8DjTr_3uGo1zNzyuDX))69I-`X!y4Cmq{*h1L@gus- -Srql?uw1<5azSbN>Vo-rfTWPyC8c$C8OO)k}keBG|@A~SR=Yb*c6iFhC=E_R@^h%Ip#jMFNTr4tlgc5 -nUZqb4~pL|h5!<~gv1+6@8PGpUwQmbp5Njz+f$BsH(s=uPQ_-|G31gU -w;?S0aHg(=V;@oLldBHfPFxNj#xXKNMwH|}GXBxWeJ3b~_}m%^7P>Zoa~W8&?zoDOxwMk -zRp24m1sj|@h_(ANBMt1xln`LvsL==?64AoVG>cW2z|p+Y}-S5JN|iYJoL``w+5`d8lD-)e&Hs -1r!y3|ISWf8MzA=cCX&BSc%p4_0-P#e>6o}#aKMqEM986-;|J~U-7cdDU3@@`(zz_zyNNfh{K2Uyonn -vrSmH7+5Lus%pg1`;!N7+m@_1Z#g?(`>&Qh(7850m9L(82Wr#%}&OL&%+8+aDCv{! -)r+%wLi~p`rU-16{7G#Fk(3iR)nDz(iQg6pJi7>rscnQjpKDi&28Z*wLXGYXKW5v7Ie&P_#!cRuDYDt -g}d(+cB0XQvJhbHV$31BmZ<0y3f6n7_}=uZu7ilke8yw2tlnDCKxqq5&ZW8&sh{r19j>atZdn#1tZa3 -@Yyym;Le_gY7)PVnDXGmxlpZunY?OT5CAe&$C!*-=dK6)GFJJ_k3X#w0m}5ym+|%v{m|i`IY&qLb -}R0{K}V1`!uX^jMHgyf`dht>e1OL9S8LVM@E~J_*?|PP2*gDYlVa8HwDGRU$dW8H-Zmk9=}T7HRC{JW -NOFS>~aJ5-3_dl~p&`PGP*zeLZu9L_VZtXBDe~BQ~f;fs&u20wFaH$Evq=e*}oY@pzC?$UwSv>2u0>G -I!+3F`L+7BD)+rn=sN`&8KLh#+I^QQABJQJQ*EvOlJZ#K4n`uwA?kd)yzlItMe@HvWvLxj?pQl*d~T9 -L>rLfV}CJNKk5=)NFapbmf5ZS;xXd8+Ng>$cCW9iC*Q)8-3?vd`*Gj?c<8nA@L?@&oHV*#Qx6~Nwz$} -CEf5P%PSSSxU?Y6sntf!yJ@z^PTX$G{*SHKX!YJv@*;yW7K|ZK>@&rvK;i>(wogAu6xA|=7({f)EL{@ -&KajKNx2nd*uvo0mQuJka_#F_?Ru0>IwUaIwQ?GT_Q>%TleO+_#)V -QZ6AB%s!8A#;xZ$NoNTsXL?WVDlkg{^IEB{d8Ey++-86$#l@gQz_mtS{Tx7irX=Y||8?ya?`z0f%c1&jiv+VYIo1G?wc1`Hsw=35~&lFKd6hO%-Ho!9Y!5LaN0f^BaNVl@r^{0Nm -Nqt+fV%NH`_-A1(THASzHi5i#&Y$!0Vi)19SNj=%g3BFr$b41Tc|6^V3VCcehA-H9slE(Mm{IGBowFl -}>=A?$x;Bav5mRi1PVKd_3Qe`X6P=q1l=~fk_zS8hxMpyW&trn8$~zS7PYhS{=QZ15eQL?1@L_y(~^H -3l-;Uue_dNkTw%UuU5CIhgEte?%Mvd$9vDv53(lNrr>-k$))8lE}es&fynjKi@m*k-Zje0#1~C5x;8V=U!3%jYm^L%Cj8s`4FCB8znvz{hH)Z7g$giZwsS(w}4LTj}WXxrtE--ygXkjGmYMg7~wW% -800n!BuAISrrq6q3`z|@QJg_r|Bf&1mLw_`oP_vfw%``X*fwnCpjlmNYcqTubA+SaK`U!{WYn)t|BBB -f3YtN&ZaV}{-_v?o`oU>b<1E@f&Zw$u5EDz{082ztT2OB3M&SW(ET|Q5jTRo3)Yz8dK$;CzX- -{)V>K81wwXznn=PFW%dxpX!Co2mi_>fc;6oG{Jxozjtdm_j+!{Dwr6dIKm4!C!9W}HUqj(% -BqFkE_OIn>^Z@GrVVgAY{57Kwz$}>94w)a*;3;azP+*{e9MYUE4n}5E+gusD@kOnTo6Fb3$`muj^}yK -(#qjsVPOHF383Q}8w#170R;5c0*y)_(uoj!sf4<_MyuPra<_8X5Nic| -`}7(-$V&zb}tApTt@X$k5R{45mqz@Rvody8Gk*~D=%yW!D)C5Xczc(Q5Do= -BOI8N<0)w#OPyPmG2iPQf`CXF7~z!jK0Wg)(;ipQxz`>?Uy0>H%w|#NZg*WXu(42oCl{t~9)Xf5|a-E -+m;24@}6Hs5h4i)m$jJNt6bfLOS~W?NKN$UTYh~tZ!-{Z9NDnD;pgH>cADk1v6<;BJms1<}EDj)723D -K%+fp+PmMPgqZaB_tZr -`eJHPQo{^jyvS+*>27+-{+msKJ?@i1;hvrCq2ziAY -;Ha4W_!`5U{}gyl)xg70GaRn=)JV^fe3(dok$D1<;m??T6?_pR5Q1kL=3Ij<7ks0v@|E(Cm;qZY57hHFc{^@g5_kvnpaz|ZEP83=e_D`xVmOS4l6bDFv3i1l;XH^HY}sB>i<_BwY*<>e1gXt@fP -QDa^uEa4Ll+Z%fl?{B=D}7Tvpar*5|?8iyo?&4|(XZVipnekyM*PWYmNF1rCg0*@VN>%!FI`5Qaf*pT -G_whhqaj(#`WESKv#<{3f1du$4Z_ID=6_m$^FoLyzQOJgb-wmvdfB>Q|i?XjPKxR>y+n)ite-seX0*v -oSC8=6I1L+RNwH%jMYnCU~}u9R^{m8+wK`Z3tmcHVjZ{J=C?XUiI|qhPu@&Rd3S-mZjQSB#@fU=gVR; -^}b$9(qSmvT*_7lT`v`9d8g>Aqa^&2G-33dU6HtqAog!`b4Uc&gSVx1+!hgbFq|A;i$?rvPZ}p$MLI^ -5`MGg&snSp?-H}KWSuJHsOIn%nr|DEW$@Y=NX{#t)Kp-tK9Y#ICp5^JFt(rU`iW>l{>R>pAR#phwe+<}&nt#q2~Yi(afe -koRp`XLuMj_~(wyyI>Kj;$PKCEY55{(s4daej$6m*jk4*Qzztpcm))YVK`LkN9BIE{3~8Bai6rBgHUPtb-zS43QcL%fa`No}JJGNu8eVS*MWqYVCNIehNEtrkLA{f?+W5#Ga -7Ct0a^z(vDd9aL`8~<}(*wtCNG6BqKTS9(e5MQpxN;z56 -Nj|Xav{pG|(lXLHU+WDBCD1snUBTavY -US0(Zt1|AUdes5_E={Uw#dZhnqX5eb4rJY%n*nF*citwTu=LVc<6GXecVW6`ZCs~K!rPC$kz?Lno+^g -`Ve2w*Ei%a?u!Dv?39YzFyW~(*I9Blb#Y=K9N5@Pu;k;7+!Sy*WlYCXs+OJe`S%0B`*FLFU*l{s>Shg -IIi8Jmm|b>3pA!=?n`KHhx&uv_6fhc0az(DF^msO=(wJEl&PC?}$H=F$L}RnY>9Ef>I-6=Tt}#LLtc8 -gLEOE$FvdGM-75}NN9ZWUQaArP``f*f=&<+p9F(L?N3DYi5!A$k -d}Zc6O8GX+_S%huhl!5GIEr7emgK5aLDR@Z%M=Kn8;9SHYV+YSCo{EJ4>ReYdyN>5VTH&fTq?>s8LR3 -z|ceuxj9A$x=3)zFLBva}k(Ct(e4qx(8p>vJ~$!#7;#i3$w?1;E<0Ivd8@TKLyCe3ljnk!!U#|W`GUJ -l60Ggk7eH(n}xv=u~gI1SG9uxehg=SITh+CeWIned}3hW;rt~q-H*3W+uGV{8nA!vwe53nY@gfn&UF&n@KQ>knwX=Tq7d-qH?I^1moq8Icqa8g)HW3vzl4Hd=h&5GoU -0=Ht83dETkL<270sJ44C}RCLK -4xVHZCTU~PN;yb_>47!IGhE@a!S_9z+^pPpGvP+X}6V)nv}I{s*)s`^wjsA1{53*CL=1@#`;lzG7dQp -SVH**v8046THwUJ0*}V;U4Hu%mPGJy`wG64$s`bKW0X`>XLkSic)QozW+q--di!*_Qmt+?abryY&S$J -cySO2RWU-#?Mcr;B|H52V4B~qj0hrsVzmNH4hhrn1z~NzWzbKMy7M&tG;sAB9H=JSNTzFin=zp_TW-A -e+sJ3cYHn3YPxQ6e*=|ldhSbm6)wkQF|H6Kr(hq(MxZkGD~(5dcnsXfS<2aFT#tW0K1w~bw4^i72P^5 -m0>lqnmB6zTEdSCJ%)Tv?pRCxL -;zAd1?dp^dgsWbNf5jf0a&I4h0>T6FmrP<2h?rrERbqZ6^%`{i*?W!snk-dEJJoZro8@~PKaw@TnNU@szZ -NvSN1;N?$bvWqv3VJa6{N!~!3Q2(!p&S@D0vMEas@(qwGyjVc8a)PH|iH%5Fs{YLb1mSG3G=##@cKK4 -M*5o+DjN9ZBBiHHIV{_fwuso-y02lzh2vJxG8QVJ(rwEDvlSB#nYqku22oBu`H|cOrjdROhk3@QbPfU -hDqDTws)k2@kp#}uo|19CFY79j03V4 -`eUm%j@F(R*Qp2~%jnf?~;h|3efDj(&LZ4{)Q_L -`1eORroR?80j-h^D*SQ7A(PRA`b7EHpi0rilnf&+5im9ZnD{&6^pFtQl1#Jz+S41*+MVfS0NO1sAlSR -9s}Zw14EzTINOZ_rOhf)n@tMbBznagtv}s0ou3)f`$f_F0GWmrACpQ>2%=T3+I=LMx#;>8q=lq)Y4H^ -kT5dIJsw(0_YLuoNTo`PL6eT>M<({E`#!GP$OKHvR{>2gH7?Z^eVCrlpM2BhcXBw`SAcUsFgrHSS{BT -EtMXbWrHahPP9@1ZteF6{fdh6s>eP>;f44mKyS)Wn-uxorcTsl)jJTCqr{uei(-UUrq;H0m@9S^@<=B -=wULcanwV4SWl7ekMQC4}S{OR4P0SA#+Ywt$oCUcE=xiD{i)Qf<37?r{ph%u7!XE%$j_t~}=d6@8RZ9 -)?Di12XZA1wY7XvQhh=@uLKOnMqYa-x4A!pHq-27Fbczav!ch!S;%86c4M3dQr0$!lr`uID5-U*a -cZ5~hA63B;3aKQvdZhM%R1}4&U!nU*bPcIc>Rq{QGa8rEWb9@*S7lFp;tB~(hD3&b6u1)&r?Zj-G;Qq -pSIUT{kAA+%91vfgp&QvDwT93mJWZb6KYtDZreQ>!IoNU)}9mf=a!sxF1hQhyG#nm3YnPzsy;9N^^ov -yb%eOARjFi+Nl-$Tg*`MYO`?7lQIK^2HaB2AycP7{jmpkz(I_|y`T?1*w}NqSkNw51Q7aRTqzQ+RGDx -(Ni7!lVI7{PQ=Z$xxnb&xVG{N0G)QNXqhQKu@UPR88jU3Uu^EDRll9!_=;Pj;C39_(fl`N_~udoL#>Y -esxAO;6Manx#3{A+7OV(kJi3AWB-NwBraOWK=F4rd0XnRP{x;*oTNt!gZ9qjFFilo$ecdQ0G9MI&qfI -LK0x6eUo1?tv|=)^K18*8*-Ngliot*=Q!(XlE2OXCG^+_S^)W=OgX;8lNlJw~$IiH&!=!oz0XT@mk0h -P7h`!k!sIP;>H%C-$<#)weUWKObkXdcba-*ZZfubD@cJ4E`+247igAB*}+gT@cZI -TrCK7pfI;{VA`sr6c{GJsCA4X;X^86RipZ@AQ&!0a3CHwE`um0@$PwfD?%`*eUXFquMOZMMCJ^PnupF -R7wXN{qlpq|MG)>gP(u;{b`^{Xy=0d{y%>C!JmKm{$G6gzP&k9YBod5{_hWd^xr@K^nZT%Gb=3Xsryp6tz|1rYkSBlh;^XhVc=GcXPd<6^Mr`S8V)pT79`J64!#;B+kue@Jj?F7$2 -7eEb&1>UO{LU=-nrJHs-@ESYNSP#fkcE|PwP*oUJ_yEoo0J>}?Y#+V8@3*7i$o -HY=fz3F5&TdQs=;EKzzR7U1@L9wrqZZ?|#WG4Wdyj$wIf0BZjG=`Y)jc@W!dMc*0U>Ospx-tknMZv%0 -OiNJ@f{#d2F`XDQZGY3MAHS{HNusqUO0ltUow1Hu>>y40Hp4D9&Ftf-J%7}A_OVLQ?+A$6kaU3MSU*~ -Sf&#eJQr?@;{xvZNvClsxOlcgpy~9#-`<6YZ?8vjJT4)vkZ&kxw%}GGhz}&Fs~1@Th74U@gI^HSvXP* -gQQUzEIDANw-!e6%tU3}X=ZA*WqrCCWY*R-;)HhU5{*gK-wBK4$r0AC -3&#mS*yKmFrYvW{4=(c6jX1sbIv2g%HyDj$j_ymPfCmav96%`~*)lnTm;Lp3jCEJpD9V|+#}ngF4{Ft%}885U{K)~2hY?x2Yi1vrlDRUu*3l#zCp -getFGnpp#lcP*2MiJY-8 -=bA7HTGJ(z9DLT{wZ)|9HZLzLXkv%bW!8oEx&9;ht+e$*wGeK%Ts;7a!p_2`afCTQH~0JHoctm_+-Ob -uFg4(8+YjAB9Zeps(&V8`PQSE*LXeKLrtQP8X|zE`ct! -DEsGT;sgp`7AL%`;k*VBS#_LQa-*5NW1HB7n1%-Wm5+-F2~vVmh7hF9K^$U&npwqW7fj!NggMV2i~@A -l6UP34%aaH{7t#ntsrvfE$=DGjL5a3RFfhO}fZ7G`8f#K0`vw|4g!V+7$Dzq$zR(b8HngQKfEbO24|c -j;%HER{0p4=y4kuq?-mUFPZDI+A8ztb{8x|A&=(+8H{Eg8W{z|w(PGV -&Hh(&A99bI+uY+f}hY3Ws>7Rf0#JeLjYt?bH*3@dS5w29q%e!l09|bYuuH*TWp(!sXBa4rLV_na7i1H -;iQ_G!doC&Wf2MywBq}!`$^Ac;A~#JrP0Lq2>{oWB`KZi6dx5F&3N&Kf|oPHKp=K)Q8QWGOr%1-IT$7 -Fnqg?Y7lUhY{6w;ZrL(Zh{>d9ZgOmchj%a4yW2#63G!BzojU!&WW1a(0Y;9z1Lihd#VuYr5v14qa;P -aBEK(p+dy<_jl}v+8uk!GAzdn#9hZI0ceA%-17pOWJ^zk&#XMl -vvf-bkac6GYt!(?w=6#&BXrglE3#V5U}1&x43*N_;OGq7}-AcFccCU56AXZHhjRK! -}f&VQD198BY`n1#v79^Gx3a(QvcW`LUyxkvFSu*KH&g!pIf-et#9EJ(^~Tk&GDp(>mquh^f?ozYnKT4 -;B+%b+vzH;nr~{(S@=*KAvF=d1vg*X -y>W`EgYu}*#2qBZ$1<;X>@NW*8*c@T_n3Ok12?7iTm8D7bKgN*t@f1ph*C`pfA89gp|6m3CYKDen9sP -=&T%h_m2w_`BsJO+iA_j%B27Iq0-WN8W`6ld*t90rzC36cx22-A{~ww(Cd0VEWEso@e^&M81Qz(x)?` -AkbpW9wDEWylZFY)cUwRg>`r_#(CC!#r}ye5zI(uH}?AJzxtkYDtvt`I9K@wigz_@kZUWyW|vnzsC$2 --!JgXJ`6gOSiFP?e_nE6*N++gvAkAGHTcy`u#077Yjvn0a++ZvF-22W0#8iz^Lyf@)}TA-_2H5zf60= -cv+NYSf^e9ng?#G%w0^(SO+e-;Cg=p%gs&=@sUx5?F6e;vm;iGUOU0!krf${|aDMQvj%4R7`H_&3oeX -|tC7m6*KO|Z_j=(`+?+;%W6ly}jFY5N&c+*@lt~Ch;q7-7WY*Lj(1ukQ~fm%t@7Ic}PnZw`|_L^ZEPS -1Jv6uP4VLxg4^ON!O{SzkRZI?x|%ZA>&ui=7QeV?5!;Yr~)w@)cs)S)!HS-rYU#+jrl7`>q}#xWz@?! -SG-X8w(42*I&QByI-J<9rm123+WtT^#+Pmg^nhK=)tTZU$mVgLiA1JZ5_m{u1VC -Px`^bmR4ueIY%0P4yM+)SCaGpXvklT@1#C|ZtUrCi)=F>1p=Of_rZ45Fh_|BzmWDTpwO!!Ulp3VLzm2UxH49jWgMa -@}csFR4nb_9)JPW99>Ou`E@rveLO5hGHs}0`(Px#jITbOf4&j`F*!WD`dyrV6o+TOi@%T0K8(ciZx@r}%S&-S0b=9Ri<9v -&q>j&yPCkDapZy}<1LoQJl{gunj<101)j1WZh>edgk=*I=3IyoGlj>Lz<)5Bkm3F{o_U=0zFc@$qh9b+Pvc?kcGuEytQSec{qv -#SZbj-alSs{;AU`0{up4kzPF6wAlS`6*Tt1q(RmLJ}LQBliEWoR*py@?okL+$A5O;QF2^hTN%o_(-6%12Jg0Jn`dHCH=f4e(gB9{b> -Mb_mB@3&UkO^H$T{~p#~t0 c{Aypn=R#0;{Nu^Nm;45Y`aGKamu!Yz=7r*ZT3E<4uB!g%ce|>!|4i -3Z~Wbb!p?_>7c*8F#S*W#xGVeOB^+aZW-CIIx^j(|UOR?)3P592YtG=gS#1yHJ8PThLj}j2h{`L_(X4gyOZx&XQFX7Km8CS;JwGC?7q!%)~iZ=M4I!<0OfbD# -{Cx&?}HoC2_5cnsQ=VKfK+D)(`tNTD}a$Z@QhlWvb+l*k6Rf)Xu@PX`hrXrRQM26UMGD6Vs4uSy1j-9 -7%8vBxl$69f)cy^j;&3ZX0%J7`W7?hO1MC4j+XKshT4bu@8@>FyY2K`ojnq+c=f8u($8b57j9Rv%>QB{kb)n}cXDQ${xu2D>C -jNVG8xs)#=(-mNUrfG)K3rGW{-ULI!Hh>?T?uW@)Ut)W30#$zx -CjQ4{=gv7w9nHiQ@vqCx17gnJ(ceu|x*5;cO|LKFcE!I+y@^J;$g-qv -aiDDmHCtdP~4Iq(-`A859aUg;>!)WqT@>xkihiln_P(k$gBn(>IlxLziqBM49!WT~Mi3!tA$*T*p7gpdI3mMLK?s~riCu6QDGVq>UjTZN1QhfJ+{PLtk=N^LLn|8V6Wb -b^0kt=Nz!WTpS|6tuukDSoD0u7kh$Z*7Nk_H4{&tB_ZPZyiUE~2s*SQ-=^FdH8jNo6>8?<2RqMFsvq! -sMOPNuEa>nl`1bV;?&?^;(@_@7Q^rXf`;(0*uPv~!z%QQ=cAgU3?C9E)sq%AUn63!jn0`Y{NENXHv1g{R2EZ2TcvkttE+0zHL+CMc#t|v*Sm9Xdo-Y(JpN6XMwaAYtpU#w -Be<_sO`|DA(k^E!z5tk%YMvVUig`@4;2=$K-ARH|;Mgn#cP}Y9@B&#m@_qZQ04^o=32(nQrT|O^RVC+ -cTEx*ETB9HnTQ>xmhEXCYPA&DyV^VLKESo>`Ai>p?z!WTsr0CM5Z!+7%9oo6P2aFv0trfSr)NFRGUIO -2dS{mnxr-mC;8zh&an{v}4t*o<`NH>$!rikk3@bi^+3t)DwUCT#saP^+7+5o75?KnlOaL^uY=uv_7id~;vaaIG~YQs$AL@bC5Z9=B -4s#g#h(QsRF>fNL^!@ob4UkoLK!8Ddg`S$bHRT!2v%QekmhR#?I2p_84u1BZ#-5r8BN>=r-0*!PHIqI -)Itt?Sr^X2UG|Gf0pL#u39x?e!AGDtE&u1#ynO6OpSI(dPtzX!IM0-PAZlGeXi5|yB3>r3C)3W#CvJ2 -r*NFeI0OE%(QqpL{EhLM%(}5Z|zNH?CGKe_gcT@LsuZ?W6@Y98v&O`a={F%oYp)Z4_WZ_^sK5$kCw%P -J(t*`^*b1IV$fk(4L>h_1KeRQd>EOxm{q -|)?q<=}H0oIhPSw-Ngzc*bA&u`#I&JSyQdI3=~gJAS@ctek8P9Y1-k`g;|iwdy}o&HqE!T*I~%H_x&q -gdFi*ApdX^t~|b=gMk;!w#)1_6jIhNKGu-iMLy3Kp@ZwTnZm^cdS5}XQl!`gNGFlfygpr>o?H}fgud* -yHD*2xpajivrRJQ|hA9_r5ITzhVjx^sMHEW1PsgXn?<*+?WE0RDLeNLv)+mjuNkodl*3eqy&?cegS(V -;tGnr_X;A`kcanwN?!rU(E6{Ix<;97E(@Kq&+2og{mA(Z^5tE-DkncTApG*d{mDZjSSIQnH=MQ9|fqi -{*1iYiW9+B!PkMd00W7+^du`Kb-z&s^pN^I!DP29n5SqHG)=TVc}JyOUX3@=ouU-HaOk$?#M?ekZ%5A -QQo^)MF2r;|FdSlW+b|JgGce<@KTCCbyErM|f+JF<|;grCx#^iLcG{QoU;<)#NI0y#&cFO77TBn*3GZ -`UqwL#ba0eqtP+77c09XaP_0H;I$76I(OQjma-x%ubM`g*sI~lXBz6E&~DUgK)^v5C*C}cmB-KSjxgm -&r!;2x4ZaM_{vs9&-1Apz?pa1Uiz8)>AB!hNAnxeTH0lC|IncdKV`V+3{Qf -?!Sd#(OU#+@zXLnWm?v~wJd(V>){1|urKw3{o(400cY1lOgg~9PaA4!R`IK3n_geJ-F-6Z>n_2%_(nZ -(mLD+8a}t76PdZKL(L2$Q8}gg~62ItYI03xgDXTI=S1J3X0P9F-lNB2;#D*SMS0_r*gH`SC+ab6UYH@ -IIO}<{YdW&C}+5M2Y}k+Qf?HePBZf+ -f%bXkVuH;!SC<%Lz$Som^F=Ac9o$aEOO6H~%~f@ru&bx-?C=`jL3NU@VReJyPCH+es~H;%jTLrUPUDp -|$a2gkx6LddE!OBY$!Y_}^}n4;0h`YQ(uiO}+i+ClSnm|f=?fSYWZR=Nvwx<~!40=T1{fGfCGPE($bZ -B^v?Zzu|5V%*#l0nN9zCbzJleYKVrs1L>YJlE%P>!$j+;kY7OD`RSFDr84sAy6p@8oOlXb*yMaT%(L7 -vVVsSid$$!bg7tY|Dx#mp-`?~_nz$JiE%NWtS`^IoXV`^grM1R|M*v9~okK)8#oTpOXAx_UZxf?GF(` -?byey4ub6b|Te2zqf~Li2}-Ax)-y(-|ppWr=xFP(+95!i-*k+Tm|CoF|?Vg<-0ZL%XTIy9CU(WSnaciqDsI=Q{du=zfTf*JSZV9v3Zrk~Ks@*DM>9X6 -Zh}YR|F;HtGvDR)A3GOM)zM17JdR5z1iJMz5;KfTBwHu%=qH}2%#wKCtAOYE7!)r+3a*zKU0=T}9%H0 -F-R`Xwj4^VrW#y0r3GFv9`W6%+~tihhB>}_g(PD1+-;I5i353+^?`kxPS=E6{f{KqFZ^203^fwU&lCw -87ohnIaWc%6L9x&ij%>N$+iT>mpGasdwEAx`|$+iybSOOVZ#z9!NQ7hBOhoTgdgdKuI&@DB5A{@hA_6 -GEh}Z$e4e??Zq~&flVf--Z&^@Y^xQ)D5WorLXW>7O|0CC%%V*N~9MS&R=Zca1=JPxiKqV61;p;q=MIXKK0I{+g?{YfZ4KiEeW146*YgWw4+l` -WivF4NIdhP12?7=@V&WDi>4yk`6yZ(O-#_P#bUdM7b2!ZV5%_{mafl-j&n7w!>6c?u$@%Am6%xOb_Y@-d+tE{4u8X%oUc#*D6m?XHHbi3xi=@eBk^4_5Z?)^^=OFlFh5Cm@S89PzYoA;nH+j@oPm*tM#g#ggn67vkQ -fr>iR66sQlK*gegwtY@Knw7dFCYLeEn91xeeo~8!8a>f)M40detff3wms;0c7E0R|U-Xwi*;KT~(+gb -kxCo$1c0D2iL#HT_q0g+{Y8R=%e_@Y)7g8476wHpPNk;!wRydGYB#p$ymc3`~jkMERzS%4MTTIpR?=7 -p7J#aewyAa*o{^eDjlbzodJdhci1l#R!*N%-|=_n5uH0lbRd6`#QfozX{Y)N!91GAdE`6#h#yNOG#OA -CtZen69pkFcAP>}OSEuxfePQFnJY5=Ns1u01Fm0BGS&Tj|70AJxVX9l#!d7P8;IM_iR&~^?TEfzQ<>? -{AX9%IA5I$1_F_bb$>sn4NM{Hr?WVJ14YZ*mZ`rr+O)!pGvrzIB$NwsOf=&``-{~|L!7E@DLP{-=gK@R{zpI8=dYiMFAXVs0twL}bEfZ@bi(M-oy$7J8ug9ZRK9K+>x< -7uE8Zs`fTQ58&^2SBHFCs(P3=QF&#dua1_K*zTUh#(Am#yr}Ku^wN6wNVIYcilPy4Aq8@Gg|BM8Op@J -599+@%ak443|crK4KR0LKyxL!;W^*LAg;R+C>!9b4sE?sNE}J2lc%YKu}zIUNg<4G=;KN1U~EGhn<_& -?JV_x1%oCJSfFMrcb(58i`$SDT%+eZ4s&4CO5vQiJ?W1V{d-@ -nI7$*kFditcPr_uk)tY7yO(ey-9tEK--ZQj^s4RGI5^y-Z{XvyqH -GU$)$u`_kZNDjk2Uz|{a|S0fLfIlnrMi!2e_vBpbC`3eXaI2N4|WdJ$oh`z#u3?0JH|cXOqhyrZ@fJF -G9q&ext1Sh^cebOTn!|cknQuDTRw|o@&_06^T@j$#Wc3?B^gwAi6$XoeX5;PK* -g9b(omY`1E%$v)!0elwM;dM$CrFyK$hP-XW+7nnB)Esxa4O7Ak@g+{~+=N3MJgYi$Bi{`nk`q&~wG=| -Q`5N0+N5xvHoycYAdeTaE|M8FAk-#5#7}B0mf^lT(xl27yQZxsCsM|j#m32@CZcSS_h!Y>uzrMaUeST -r<)-<1uM9_hj&%OrN!-nw}U()05I6S%ADn7NwFE^JX#2%W>qtX(LMp8Bkomg!&Jq>~;9gGI?G+W=;&Y -xi8g%6&NFn-3miI5y_NP%&YpHn`5PFKlS6G62at3ym)y2)+I9W*{Eh1JeeW6*}shE^TBQkEf|tqU_~t -Sjw#3mF9Hf=exnP$@%2@gUl@vLlGC2oKg>9ZihGlQyhHG9P|K}A)k#W}}Sw@i| -?c)ASqg2SQIQ3q2xVeF}w>cL3IfRtyFkB!H>2FlrZ+dTOwm}!db5}hULG(G5GabB9z?2$CQg9sZLaK(7pKq|01tw -vJXf%1fH;SC*5kLNxN-?cD?MbW`_mMV)5x#PIGlQlL>n(;2d+gYsGFmYM6*bNw{l?&BeLSL8ldJuvb@ -chvb#iCFuI>+)kz2UJc4FvD;h~&eoG+Tv|G0N#3jBtumE8o(om3#L0m4-800;^2eUdBs0@CjFhQB`T? -$9Yycjt;JF0vln2qz^2H6iyAj{_?*Q&~4YNNH3L{f1nc0&7^4p$Y&kHPIA6ji -0L|ul8Q#gKNe~B-6_I-1?83KF$fNB`V}}k*67+A&acyeK@YV(qdp7#~AswnoBxBpiy%K+_2^V`mJkDB -sZ)%a=Jmyp%AWi+k&4#E{MRP20qT@;GSdwC^?_ui&rF}o3n;hvdFtx$yxw_>?F4oLo`Zn3KL-Zgt>`8 -_1`p+?w!eKHyj#fcMwMGer%{F}7WyX9AY^KD3(~7;^@<@CMY8 -?9B~Vklqt1=u$P@4HqeDnjODj$1Y6IsS3wZzaybgy7r_9p}9CH1^s1mcyVz&aSksp$G@E6wPlyx_=@Q -k^v@R`4zG?cYX^wyy>hzTss%QN!1!0c#X{Gqc8dl6ZE%YT{&_KIB5L$)ZHM`%l|qw_>oLrlDT!1`-L{ -b#Dx0=iYc>-9{GXxerwG$F%ob##p6j!C3Yh&>)i#W)GrHPV^AZirNDcP3GMcBDim|hxI^$TYO<~~&Yc -0L_E4bGWqk=W{%TlLGw~DKhV@_dEMiW#`Q_-{v-dRAK04#h0e@z3La!XdR5fRhXb^#iTxS~KU24~8hC -VU9AzEdd1d6Z@7MszLIJMu$Q{Fq4uzI4(5LwK}s@30g)a#7JL=#xETcoxcg8PXMQn_)ov!~vg``EM=pnG&A -9YY+O+B*rsjeLU-1eE-91T|-m2EmTO{S>g&0{kHXhzIy?}rtXY7J6t$e2|iBLFE*Rl4O;3*cAqO*Y|> -H5oNBSXw1uWMbk)z(OM7(cf8BMQI0acP!;gnBI1{xeC2S9oi-um -n4EaeH06J+$wW!D4L_m(dIX8XIbCZE%HIj>K*zaUBCM`lFQHk+6p3m(AUql?F{uMr$TZ_oWx}@+&qh+ -FGRUwe4@-+Vx_WUfpy=O1pPyhz>c$&N3Shcs1`X3uE4_qTwm<1g%pBD7-p^Cg@EE(Foi%^3&yhrHAWE -J}s$H@xTym?#GuVjgn`KehZ9YQwRjGt7WiTA0jv49$>JQ$XN?0R8gPePlg7-xRSz~eRpdWE$Du&f?XF -%DHH2GTWK{6$xtJOr<&uaETUqsx_f6Mlo9PbT>dBacU7pO@HS9B%cxM00m+8 -xwdZV#%wWYdD;iEA{s73Hf+^Eszjd=BwnmwcY6>gxEn-U7&5Q3BI^!DF1Ax&De_GnP!ccN!x=>}7?(2 -;pfXV-FX1E;w1vuBe-`#e`;f&$wSbj5f$GhWq)mxbr;x*+Z2pe$u|}jo@jFwNmUFmqphdUut4-61=pKfV)4>qSVh+Vof$sSacrQ>%?(cBm?73P90}h%?d<8 -vL#Mo@%(|-vC-i%$f{-#sp{N%~1rgn7s_8gn@(#Kvc;+ZA6**mzR;(Wp$CM5>p2>u?BXKSH&ri~Zq9@dQ~_CJbvP;o$xXx_fQ8q}ipQQaQf!-WZG+CIHT&)B(Kjs)Ba8Y1X -MxfiW37$X+QVq0=!v7(YYi(12D|y8cXGG0IYwP0)pq}sWhjdlw1_E*3bsC=%=OYp)b0rr_3GpQ~a#&bQ3ON`V7=xWf$OZ_k=;|W8YMUH3c -q9?u+3SH3>Z-2^{I!S$CN<=8^H^5=^7n_$zH9p@{!$95WxX+&cPAVgCV{0%RERaRaUMj``hk- -9@o`nIqx!4Q@-UqUv$^MX}F1BG}Y0n|-F`Na%9GD`YkiKx|r;$El9l0gN&a{KxxnaJI=C?SMVCEn90) -AXG@6aW -AK2mt$eR#QEaJPHp1006K7000&M003}la4%nWWo~3|axZpeZe(wAE_8TwEl^8#!ypXYa|+(;p65@Tbi -*p^9)e)Srok4GeY_eusqKx9;p3Gs}{PecQU$EIgGE9~CsNGxZ*(v`*f4Ixz*x -&|Z$53Ud&(UB*PL3^mgk;Y7f^>l@7&h~EObz8He%1}rZgg^QY%gD5X>MtBUt -cb8d2NqD3c@fDMfW+ykOKsD;Z9JB3qj9NhB&pMZ6-`oi?=tmDY)qNzxRhfTI&jJOBMSh+=CkeOM;tEB -n?_JNc!Jc4cm4Z*nhVVPj}zV{dMBa&K%eUt?`#E^v8;Q$cRqFc7@!6|8!p07J -PO&>>BV=8zgi~GXJ>~qvYM=MfsJMulpd<;=*+~dvaNUq-5bYL0y -ub@!3X~u(H`XC1P7yoGh-`zX#*@{H?enCT_`%=$yO{!MHbKly7c=A6QWMDlwwo;1yD-^1QY-O -00;p4c~(c!Jc4cm4Z*nhVVPj}zV{dMBa&K%eVPs)&bY*fbaCx;@QE%H -e5PtWsARG*n3qwfIJr(dnm!{o-Z747oeH3WvY!jhHjilnZ!~Xm3NQn|9I_VxJAc#aBk9YTdcRX608l^ -3>mj=v2_>X;CmsTkm2EIwLTP*fkomeB9ym*4TQaac0R}03PoL9WDpbhKff7xz7zxQL^vbt*gh=(Svctm_N*vGEo@O_ZiF5Ka=#8=&WFt(s)or}0hS-kW~ziZZnm-%045nasc8`d~(%ZywL?qYH=Iv&ArGiSVtRr>i_fz$+m&YLSZgo+H!sh1O1Bpruoa#s&fdCFy5=k*%t)EB0AjwX2xFW6DEeLnN_$e5Q -4%61@Mf(VAj`enTM96PXFdrnMT#dEj$CO0V-1Eh3Y(MR`BE+qG|lYdfcja$xo4iLHu2Wi`AESAQ(^;! -S>A*@H|MkZL;qFD$)ecB{aTv=AgCG-?I^67K!yKGf2(VXt1rWy^*_C$U!!X=`l>K8CBH6S1PJUqx7KX -hT%l{$dCZcvC~1us|wSRSFY6W8CF}Z8YHTXZSJGriRNvC)fKW?5LI0XA{UoMCmJF-GKWki2wtm6E-8Y -)z9KCqAtGHv8ckvqSMwpWk}>=KjPstq#}{LH8d3RDj$5=9s2<<#~_$V;4IRWRZI%EDUNQOG*WzB4gz*nJ=%uP?(XU@#`!vXeL*E%VIkIX -O|V16&N0;x3Y^59B1h;sYm@U2z!*qB-(kUb23EmO*aX84u5m(7uiieoXD?_2kBK+v0JyVQ+7Ln%i^3z -5+Y7$dV&}0s`IQChTF$4?`(;94S?cR67B$6G~hZ&+sFv*a6huMkEE78l!n3ggX&tu-6)~XVBpcz`dzL -Y&*oJR-nm -5`Rj$6o>4+<7**c6YKJgK>Q>YvoiU~^6xb6yVQ`0XL_jf~Sl%dO1f}j!WE4W0l?s6rgo)SX%Sb|jcf* -xT*F@}EM+7{wX=VrP?)LTvEXf*HS8&xJfp`4`!cs%&=ufS>BLogNc*U4Q8`6Nh%PJ^Ug)kzp$a7zHS1 -+w`yOMmJB|sYyW&++cc?kh|<`*++X5QZ31r2ffg1nZ%cXJjbn~B7cJrtIBs}!@)V7&aqEf?LEJbO6nZ -%sa>>Ffw@C3KAfbv}ac?u*9&yOv?r)uK(N@;!JW9FT}#7Z-8!8+{lzqIxTZ5Lt?2b2CrIzjBM`P$VVU1PJC~P-nZ8 -`aU0PJ1T)7Ql|p7G5|W-5UB}S4-zM_kUB|G+_v@gi6ugG;e{VFewb+S9;0Etl{oThL-Eka#Zh&!fH;V -JTMRIXW{!HOpD%0fZF^0m^z18yC<%Q;Y({p@}p&xp?yuy--&3jlBdG|>?#aBiiHGI};D)y*=HJ}!h03`ze03iSX0B~t=F -JE?LZe(wAFJob2Xk}w>Zgg^QY%gPBV`ybAaCx0k%Wi`(5WM>@0zaAb}16r4~` -v2*%wt*!a+cHj0dQGW4a$cu5_6bAcu`miB?*#jB$bLRlFjJjjTTX-hu(DAqy%AKnIgq&%XnS8&9h2XH -w-nuxITwEgjl`Z)w2@?s*#Ie7)2(suY)4UHBF|L;LrA4mF(*>db_{3mW~jDIa+-au6uXQ-+!7KV>h@@ -D7VJWU#xmV1TGP%oQUjI<=lqbncyqEF`=nnvn^n>5M<9Qqs#+}!q;p~GZy`oAz(b}hd!Ke5DmlsMmZ8 -NbP!I)OR*v}g%fdT4!{0v6&MP)h>@6aWAK2mt$eR#SzRWD^nr006fF001HY003}la4%nWWo~3|axY_H -V`yb#Z*FvQZ)`7PZ*6d4bS`jtosqFl12GJS_dJCq_FLGH7?H|?s;c`bm$+PLE>0b%v~Q1lz0yOX2*?m -6j{W)fAK~MJ0bLuW0V>BBx+YsL2w}*?a*DlCNCpoMv%vEhePSm5TKH{|F>+}zy|`s?+lXKbVty=4i0|&cnw2p9wlX7iHwJJ@Jkrp{Bm*p -?cwe7$;IT`@c0<+@%F0;+}->_SK+_#(mXp1Bm5fxy@Ql5wJ7FIEkgJ=xj10(19|^|k0py4F4{=g6H~x -l%~$-9t$_1_iGsbVDv+3XWZ$uJ$v(clVrPH<`aC)yD)n>k*m*?H(*S9E-B#WhFq_(TFP< -5GQpn-DnD^FA>npeoW;`K64I8!B)9F)4W=E~%{Y_gP*HKYs-x+qoDGD8xt?G0`4u42v1K=rtI|OY|75?>g?pjL<{pDq_v!beWjgrn>s7XIuDafPFZK -!Mh)M?EA5SF)IWZFhk8tE4iN!NUdBt#?)Zkn5e}eTT2uTp%o=|nf3OUuDH%KI=ylsA%Z>%;=FMZmD<# -Sz3_cXuhQ$k6W{pAyuEAAn*n5_h4?K;KV_5l_4A?W`zVk@4-le&SaDuCrb_Xz|y>3VKcaKc_Tg4|ZH8 ->8s!H4XMd?E#B*%T^%WD5wRDJzT`Vq^*5`pr -R@AXY9|}NoNJMgeppmP-^MV{u-Tnvx7W-oW(01T(A&=<&z%}ka;D -uB2II8`zL28h+3VYPF)|=h4vXUBc#HOuXUU4lni8o0D6+q)%AYo8cbRrkYYP0Wv9& -===!v_me$^X5*PhNlOab&l-EVdAGZk8>}Wer_UB1`>nQvM?}a3&4ZCi*ws%h_0Sk -I+*-5V*yM5SJ)_=k+>|lLAu7c}=BFyMgJZ1F$jt*V525luxX5Uzl_5(8I@E2pf4>-qr@UVlFnnxoNmTBtx^yG7}U9n1zJxbC#;s -q5%nS>BQ7S19zr+t1OoBi_wV61&hC%CJjXoZ&*h@1Xi-WuwS_sf5&Zl7JE#WdYcYvO9yc6ubDcOX)h$ -`*!Fv^wAL28(`~LI@1k9Y_X{TbMf@ZH+omt9>8ruj9#BmW0Ksok0G2~vXd{}7R;U#QMftzU0XY3mM-C -aiPhFUs*_p`j$vh79Z6fCK7jiP;6Rp$Ib&%g%2omr*;BxF7lkG0^nP=iP+B|g0eVL4vB)p94KWK8b{gGUHHMsOL4}cF -t#{Oh@^Dp1JtY0FB34V4GY@poQ#zQvmZFrYz6QaIrE?pyKrvV9<{@Zwsts=UOLsop{w~!z(S;N6eloN -a~&(t$aP9tY~W!9m3!BsA|*RysGDf~46+Ljv_b&38rcBS3>j0c_%rAblL$!x$m6_e7weovi`G)`d$4qf5sh?WLq`q8X! ->*%NFY%1G#U)_!`Kf^y>hf^7rcPod)o?zn92#Nda<#%I$W_u=-ZauE3yE#N8BrN9^rqSUJvd1vTdEy> -s21Q-SN=q!dtLMaHXd)oTl4A5I1oG09t{$x+Cl1h|eEhf4E?u -A%s!O60{5%v&pF;28IwTgmC_)$TOb$8SfVr879$`f#W6>K&>-p7wZOB42aFUC -aa)5~4~({&?OqnUr?Czb{5E;nGU+KR?&W@;i?(w*#s|EeYcyrH-;VYffOMJu+898YxY2t*Wkn&+I*(z+?Y|wXfcM21>WX6|z{WSze}MDWDSmqNU!OS+h@w!u;jlAz3@X -UDqhyq1Ef>&8>O*nxD32J7tS9-E2Ew-_rMcWxr)(9dIJ95 -TT(H;V(i(-Go}6R1#~@bBnfPv1680HESy$Mic6_4`&#N!QfXCd`aa+!1 -w#>GYE(h!fefmM|S)^cJ0NS#C!$t!?YTb9tmVf7%CW;W2zpq50?@>kVF*FKc45z^UKC!RBGi@W2dG!R -#F`R(NWSg^!EZz1J9kb^9G+Wr0S4#jyso-G?g}M{zy94~LkuBl_e9RE%Q*$$^1FoR<*brc_JoRGXHZz -%$T+>I-wZLrmU@3cEPk8ttbi%7b0LK50)5;Pqb834UU@&4}B!#&A_KnA9I|)2DJ937JAxQQ>4TSrnZ% -w-cDKzg#l736X=2Nbj_PlBD(|ojN8;%&L@NmIDPw+rK=6Ez -5zi(|Y|2oEx%g|N`vA{4oxKDMR=5#ma^c*>=JrYSv3+jiQ^^&sMtcCYxkUN!sWiz;a46;z8%%91pmim!|98fvn5BrqV$~hcxr87E-g_N -<7Em>C}yyC0RYv=?*@q+2r;sg=T9c{ONBk?m-;-6N}x4RU=l8lDuGF^|d|J*JvIqx;|~e?l`Z!lH1+? -;7daa@GU&;^R?@^>B@kww4u*4M_RIutxAy307bNxI~~CJw8<+6&i62*AMo+#MZ8z3KAmYJU0iJNKtY< -r9NnwXAHR{s=(eGk;c@b7?Auxm`%S1mvKl+F7#CMXM-p*?M_~GVeYe!UTu77M!f4NjJu{TxX*s!#irO -xnLx#Lbce0J$I#}ljK?zEk4m?@Bz#|0eMES1TZlWhmbBOVz0Up{Eh)b)sMcp<{zhNhX!syAvo^!+2ULm%X0xh%WZk$Nxbmj$W`pZYt_Yx8J?&E|mRJ_yg80XD2UFWpw-ksRy1wA&#wE -6ohqXI;_D_7;K!%RIjevjJlJgvo-?Dsjit(toD!`N`ih}ZA=9}$!wD-)!(y4?7K!qQAAa)Ze5_K>B<+8T;nAvTn|4rr0<}>Ze@GYM=R%IwWc#iJWtH) -YHxVk4e@5CUiOx!@6{<721k2(pvSSPl_#!LEs4QrtE$gPq6ugZ`@GZQWuKnG8|3<^S!hGhk@5xLHW0 -o0D+bPCZ6KA4JqGY0O&g#<(G=YRd*~JdMU5=3DN-P*IR5*-p=?=pg9VBL^&zpX;hQ&a-VEIY-;wcTy5 -5IGKO7UspI;V>g|;e2dQ#R_O7EYI<ChV8( -!n4qF%!RXIHuOP9R*f38J9e%3{?$H!{MG35DqOnO07!NfB~6E+WN?dPa_t;u^tj5l|E6$_o>Ah&S2WR -#4bhh&4@Fi7mNJ#5eO=!U@Bll&}Qm`e=|q`I=~>5!NDGdXJ#-~b2|PAv9@H@+A{B%(AibzLQOFvVWwwuWun*={C7y$T+n?Y61t9AWk*xQ7K^XcRKVg8b9XR6zW -pWbbW-yQEz@I|!VYDi2S$mG|LxM9+1jFo+n7n|S9m;DkaWfR-8tEOQ*8x@zIgcnD#S5pJ5S6)U686 -R-I6Ve?@h{Sh^!`JNJNnG@b%E*|l2YRe)Y>$rN;N4DaBgQC-;#S#bWh2kj%s -g*ZG|la#Uo*XVxVkKp0-ejFvRbciYC(1Iyf{C*SvrtO-ov9>VP}#@Y2jeR)?)8$gM1mxfN(*`Dujx9f -0PA{t=h$L4)Rz${gy2(=QzN%_=`$$t5~o1^YwAgI?mE*p@CcLZC=TWCjSdx5qG?!^6fPP!lWU%ENiw{ -H4^3Pf@e_RXF=tIDwTFII9JHEM^(e-=o8I3nH$;qICIP+&z4VTxvzeRZ#e?=V3@R_z-`k0 -j-4{oViehFXP@|xfs4Z&Jdoqtfov5EP&iTo7saqQ94@4r0bipcNT)i)UVM+O?$4sKt(w=6D|{-4tR4N -yx11QY-O00;p4c~(=DC2d}>1pol%4*&or0001RX>c!Jc4cm4Z*nhVVPj}zV{dMBa&K%eXk~SBX>)XGV -{#Te4%P#f;^lThpaL({8PUVkioMmZ+EwMQS7!$1U>TcSw=4r6f+{2U(YM -{SMFhNLrLaYLsF|c!p(<*3X(2q}R)(C=e+yWl^OmQ^`{dK+!dc9soazt)QDoKKKO=M^djI7m@_kxqbqKD -vTp23*?6S8p%Nu*qC!hsc%L|2m6Lcbyxign@T6D^W8!I^QSooT1FLm>3fMMmYa08x9VtCEp$Fc^T*lw -qaNA6StyQc0>bO+&HzMp9$7ju_l-u;i3qhKjI!1ddcGxbi8PUl0V%{l_{EjIJ@G8JgbQczsldY(7*34 -69Vqm3gn%1nQiwNn-?O-zudG!nKNe&D$l&dGClSR7!D8Gm;@K1j3Aojd!IGqgMn?r?WSaiKd_Kmc4Lkz2W@$W -71(5dCqx0}huZ+dhm}jcewCg|0NBQ3viuaR(L6ySj^30$wHZ0oT%DM`_Sfa_iQ8OzIYKA^G;(#j>wS^ -ZS*dk^n6-!)MU}_~96T1g^xv;$Ew_dpJ1Ay{TrJeBk6Y-u`Kd9kwQm1K*d;O0-uARK6~^S!9^Ik*d%X -Cf^p8qLH?`c?G^n)YyA+x9qC!myE+Qa6SWz9_4qSYJ=$pdP6W_SNuz0UI$;>Me~WBv{|E}o)Gd?C)0s -r5I^_Bp}G6Ac=2`6w%0iG(SLO;(_stu`2l6ypyO0`^PNk{&X>IqF&EoN*4~HL3`0)eftFB)kVonnQ9_=RcQg_Pl5P>o7}iH(Dqe?8j>mK6pZ&qYRaO -0t7RerqSUbTh~=4$m5xNx0dLP!3_D6;7{^;MVxqTens!<@W|_?+2N!oBj9%HlIgF(UDaG?gnVmikYUx -kT6z);}s+Imc-HF8wfTRHxQ>1C=(F4;Xb`K(b!#uHY^Ftvk62&BpJCTOu*Zy5Z@_d*ak%S_~KKXiB!d -($tpfuF8_{A3z)h3=G8y@sj!>2s=FUc!XQo(E7($`B<4hKh`fqnPHK?~78~`Lt26lXB|ZauqqHoa&3= -1nU0iS*xHDjHh*O>q+NAvMoMy(p!)nl+pR@(&DBCeQDV8!T4*?#!Zted0BWCWL)xy;Sw#R$#JyV -+zqLUZ$OJ~@WVbYd>>cs!X}?c1Md$a`nD`4BZT$bWB5Z`us0E<3k23SO_>jC#&_a!i4ACc2aQ%-JFOh5z2-!zgweb{Yc^g -A%IhN{ijipxI5G^;?8Rgh=ByP)h>@6aWAK2mt$eR#Uggy;>Lv006Ta001Qb003}la4%nWWo~3|axY_HV`yb#Z*FvQZ)`7fWp -Zg@Y-xIBE^v9ZSzB-9wiSM#U%|>KEV+uBjg6o&Fb~OI+I{FIXpGH+VT>t>@^CGYib&1uB*=g7IlM^J# -dff02Zl!$$&crLGjCeiX_8aLzP;LT`xlq~>64PeKmS(qe6y)K*^rjE+3vtZDLdj;8}-oA;&HK_b?n!k -DXlW4yS~tU$CN+w8g6|r6cdVTeqe8Sq1^>pA)A(Jzwuu;H$VM{saC?~6wr&u8oo9Atatj3DbVsJm1GVx>UH&x2!V>4Dn) -)~9j7(_jdiiiKe0jsl)=J#%D!pgFsrMSI6>iq=QNk)Y5PG6=B~FS*wx*h(UNtFY}e!%`)!%qmTU$DUK -wD*6+!F~8b4-bJKO^5qh;(^Eu_#)=F5(P>odJp*UQvv|N;~9B>-ViD36 -R8J3x-?*;i3x7>f(#0&2?UOmFbIR>x -Mxc&05}_ak3loHld7ydcHEXOZaAgVdF?NpB3-oXxytJAwc~!wVQGfw3ePlpENSldR=IO{5SjN>r -M9R9Ehtizt5e+VBQnHsv;I12lvcM`OQ|P{rLLO&?W#pag3TLJuN4Uz9c;uj`d`TG-vVYj)PD7&;#wz% -5wxpVZjr=|_pg!^@6kX4j>@wOBEa6wWvg{%%>?kqPE_UE29+Ff1m;|XRJ|qc&@IW3@azHnj$LWk~)~G -mHpeAdifYNo3lngNT&Dvg2mEux_o87Cm-QW_+Y#vDua{{bAK~M)WnG%F=*7i=bN_b|vGCljpW_${4z)|`O=U5d*aOoJA~hAj5&+~e!GH*a5p7v6JaOZkRJ*XeXQGl@yBAdx -o*K36%Qz5d_DOOP-{w(KY_Fo!M}y23R0x|w%7^NV3q!~Dri1(MlCkuvB`p`F7ak01tuyj-n0!$zM|OU -|KnPLrfn#jPgx|FKWFn@l3`A&GC^vjJm78THG~hQ(v##L+0wBhE=bDI!!A%V*(Z)KAYez`z!}~ITI&! -@q{?y%RX1}O*7Z~x&St&|2C9g&uvCjo-HTj4-c-wWdTfhK-QNY(x$jHCxBl9I7weEA -;{V2TYovq{6~7fNG_!Xo+bGC`nyG!o?^B@Bv1fEEG9F-(Fwdk-s?lqABH$cVysiQ*}Md*FRk;=Lr{ml -fz!wYp*0Y@?kT@f!~9Cw?@9VU%k2;dIi^ZOzeOw=zI#3=n&M3%$O@_7-ZVg#1CAu2I%N{TH(%Z!0V~T -UdEVwv@;H{JN*7flmTEvbqq*J5>K6BGq;ED0fR&3v}O}$W34ra>By!V=WYCL;X4TcvGF0fzL-#^bwq@IxP|j6!@qYyuBE*4K>9h>AM -=*{wlqn1xq_A*N*o_%Kj{br7ia{NfzE4(_$l6!-@28%BjfVHQ+iNb#N&0F^@o;m#fnZ$o}Uy( -kD{6e>$+LtG8XApkN;G~}47SxMKql-}`Nvs8nL+ma3HTT-B+!Y1nsgkwm6(hZE#)!PcXze?M}7Ky-q= -SaH}yAtgAdCtLIP7{ZYE=*pw{FwA*Q$`9oJb5Xwz7=CILgGJ$D)p{37nM~tSU>>eBsIJl(|GDs^(2kH -s+-aZoV}QuF^C!PW!{~$iuLNQqelokmWtc4(7nrA*~YfA4Sf3f83G1??GoyCsDSU1P)i{Xk?z1=p&tL -rOjy7yKoC)sknNU4#l9Dhtek~qf7}qdJRVT}EJ;K4pf*p3L^tJ_^^PdKMgF^i>fz~72i^$I0gbvq351 -h`AT?tsKfEF~%97a5uaX&@8BmN3Ci#AN#uOK0(}oYQDe)~5v`iMOgdj14qyG3Mo+7dDt{g-PKFh(v4N -^gS7kX$e57>;&z$(gXQ&r4lu+qMd;v(m8*STo|Z9CkzW|(K@5_d1pGoO3B5=(v*S{(Wl6-Un~=`WtSa)-~v-Md!WV1g+5bZSzGb#uXcG+tGJs$>x$!yEB}fy?11mUl$ugUXXvy8H7r0wEkBgOCH;Bz_C2gw(zvqSGaUQyy`jC)pk+J8 -wddb2!U(5myxE%vH-!_F=0~9F7&}^!^U@#szOwEKmWBB}HQu@WJ(TrHk4aO}@mav)1^%-VjO|03M&jv -sf+A;bqhtp4QPyTdFU5+~9`H&n39rJgGWZC-q(Z{n_F6JQ5lj_D5bN-Qp=e93Fhv~oSaSZq@{`LK1KFG2%;JXBZZ;t~yX*-z1r -6~qELg{_4W-$y$fn%MoWPs-Dw-Yupz`?QO8e+NWzu?H>$^*X~@vgo&T=+4=81evx9{qnr{Qm!I^!qPR -O9KQH000080Q-4XQ>Q(-((V8N0I~uA03!eZ0B~t=FJE?LZe(wAFJob2Xk}w>Zgg^QY%h0mVQ_F|axQR -rZBadsgD?!;^DClGAfa}z#Bl1(ow}o3Fh`_7kZqR!`nCy>5Sqz;A94KBp&R5`wQ6EOX@`$OX=Y{&itg>ID*-M2GEp$6uA>{iI5LLeN#`^9?Ncsj?{ZswGc|i%#C~Ka8iL{3q4YARwJxsBHjk -by?p_W|^xSy#0EHndf@7k3suOjlYj$0L_G~EIlk{`7MOAw&rthaaHJN%ktV$3Zewe2<4zFp!<>L^H(i -)Ex^hFg_fo`;zO**kV29*`g|xBov6ZXUTf^~}@tayeD&%HJiFX}k!5XB@p&yZ}&30|XQR000O8`*~JV -8NawNHvj+tRsaA1D*ylhaA|NaUv_0~WN&gWV_{=xWn*t{baHQOFJob2Xk~LRUtei%X>?y-E^v8EE6UG -R&`ZnANsUiVOwLGE$jmLsFDg-R1By6<1r(GO^70E4xzfNYi&9fEi&2#ZfrL=i0sv4;0|XQR000O8`*~ -JV{LU}Q2?hWFIS>Ei*9)~vwD9v -WB9ugC9IUaQjN?mwN`otPbt)vt#}1)!S8#)>iw(u)AMK8Ig75>?s3W1AKk-ZHk)OTv&2B!Xh>8IGTCW -iAiwBW@KR*&z8})m|q_Ql>)TGUjYsi6G! -z0oPL#seZR89P;_*TdV#@n6Fzn}`83u?H`FabWdh`vLCGtbN3L&Osa&aKsvLI~&UWVOCp-`uS4=OOFU -INBYp`)kSWh0N5>p!r`B@rvthPYK2mTOqmLAF=rEdiD19^KpA#-dcMHxza8RE+p0HK)gZTGAIFQ$7LV -~!R1_WnG`0d!enWmz^>ZphekD2>ankbg_DK4n-)@k^aZXouN~n(+c<_2EXFK##nf0)ihofHD^(zTfZnk{KZO9a;YL`L;2;X?R#lS;>WxkIPZo{rc{ -ArulG{G8dqa~foGk-EeBJMWx#cYu1VOHPD5T{D9o-|~PdVft$QAV!bQ?KJb@oMu8QafZ*S}KEN$`-u` -1QYh(w4_c#?b=-cQFl`yr`%@Opc5g5O_HcPZlKJU-CHI!8dQ4UFHwm+f2Pu1z0)6e5@TGVF;-r!QVtJO5xa_H>hn -Pd*2hCuU5=sy^gr@^>C`FQo$BBs` -4^Lz9$jko4`}r5<2$8L@1XR}b5s)dWiJlgdP8ep~*cANLtKXxiIBZ=k;sTyB@c_m~i$O=qW#oDgbGPH -LQ+j^hQG@s9+dfZKn-*L?xPgEgJkAMwREG_iZUN+7JkaBR9|6u-VQp>BPEH<#Et(Au&(#UuSsG(LapO -#wrcZ=14VaTRuRApq7tj^qAPYhD7driFuC0R*FX_nd|8n-n*9H5HeethfpSd?`Zj1Pv5*~)04omjZDI -8#heJEhHh5Z03a-;Q(@FeN+R<*;}-EnJ`OXCW -62(=C;u#*K|Vk5EzaA16oI~G!tj1Pa0DCYUBCk-YJtZWGUqe79F@!BFw`GSYd-&N(R45$)R#9ySWg=e -ent2X92^38Y8zg%NmFz>pZUllZ7!bWxFZ}*E9TRm^w{^Q@#F6QXBjp`s>)%z8e5%YBV;6u$Ky&Gfc_J -H^|-qa&_mnrl`}QXG=(s+VDe&_}Hw%aJo_o -Kv>fJvAgm@i%j{DE~1pOl4e8_E)wQlzC{s&M?0|XQR000O8`*~JVsK-IyX8`~JSOWk6E&u=kaA|NaUv -_0~WN&gWV_{=xWn*t{baHQOFJob2Xk~LRa%E&`b6;a&V`ybAaCvo-!EVDK42JJKh1XLeWjw&7Ubf3lJ -51UcF>pFG3K1r4_x3YMN>Zuil-T}$ejE2G9zm&o%ws~Oz#WH}GFW$VyhVL2rzOAPi?YUK` -?7vvZJHbg>hGOVC1g{5RgX^VDn(tgAa@G`iBwEu_!H+rsz5c4&=#&6t7nlD+z+FFI4@RnJGxS#9SbDg -vtvkCFsw2yDW%Y2Uuxmg5cj#+y}(3yh{XfFVZD_ -B(!O2#6MPoE4^K)c!Jc4cm4Z*nhV -WpZ?BW@#^9UukY>bYEXCaCu8B%Fk8MOU^G!RmjXO$S*2UNY2kINzE%M)=?OVaA|NaUv_0~WN&gWV`X -x5X=Z6JUteuuX>MO%E^v8Gj=>GXFbGBOp2G4emJ$z8sfTXR4I;}SDNX`toEUVn^mqh(vwb8kxL4@-#CG{!^g+>l&v-7uU$o -njI!`RZ5-!6>wW^F|Y_^n=mbPGRL5ocF67PR7MptxBi|&RyrdfhWVi)?VL*4^T@31QY-O00;p4c~(;w -9HPC@B?176^#cGN0001RX>c!Jc4cm4Z*nhVWpZ?BW@#^DVPj=-bS`jZZOpydvZKhhrhCs*gjL<_GM&9 -d-^grV&boM0%JB1r~@2^7XbA59>>NyRlw+FM&(!9cKNgA*G9-5|sUW;O(}VbD#2Y(i>7#2bv-FzF^lHZ-+Cn -GGE*x7o7D&({FQ!)U{h8#>&?_$I|SOuQk{4H|3$dcy=88rh_8cR-d~1^5PuH#E9oqPOdz1o`t7lh=h1 -c!R^61mA?=28=VJHaNP8m`#E$SB$X@k}TIe##bm<}%&k*v1mYxC>D=4<-HPqj(lxy*@(4rzo(8w?3W4A?g5yn2{N<_>^bXe>-v)EFVG_xF~(GwiQ*ojYW8 -_G-pdG6B+-1wo$1iC-#xn{DXki9zX#W6mGC<7~^&S_>h%Eqf5+*^H}yqpWYPL|Z+{jH2yCKehC%lzps -pOg?243T3En`Lo@6GnD9zGEBkVH-jqJOUrOMC1jBSG#l=o&~N|!Rn@L*W$g~*N$kM7+OA>cpcaCbB^a -{uk2Q&UU+2CPBrTX!wq^)@M$8u$l9{o#)5a620IsfjbrgivpGAW5aIFUDQtbHPV`M;a2a!Si$9!8eWu -Ip4MN=Z(lsUNTuM9~J-U#qa93i|4LRYVa@Fc}Nl%Y6Q>6CksH%9?hfyq(mML$|3wQ-}SNo7ZGQa4ZKG -Ypty14}zWxK;+m{d+bM##w$ahsH>G12$#p7SOm#vf1gNoQrysgRA5#(eFtcwdbsAr`i)xk*4i+fuA~E -%SGbB;U)6>+|h7o2~VpZ@G}Ggj1GC1f$asQ%DW9YA5@~tV=>TvY7^+WcgxTz$3-tGI&yBVHCObXo$8t8dyDr1kq;%uFo^?GJbGplo1dC$v2W=5EUz|UA ->*#i&0BrI}0UQ>}lVFg`N`+G55ENLg~ko5TMuKd^zMxEFc)1L2*dD@2+zdOv(bU|_})@44_RwF4~Jj4 -#^wjEmQnj2J!H!E)_3Xh^l&TxjS%nvX=N5(#qWV<3V5yEIat#sR&Oq086W*obQBczO@9VYp*rgdtX2o -Da?`!J`2d}a2zs`zS~RViOnQ+2!a=m)|7GET -d(lkL5G=dUe;fn!tKfa-;4V5g2Ai$Il$dYa_coW7O9DoVlMCn3<7zT}B;)^0S3A_oyg&zn&3$Z}}yQl ->fFrXPAO1g>TO$_cpzi0$lh;LAEtK>KMih_ky@oNAS{yHCwhhBIUG8-7(VB`jkHYu}-0kzT%g)GDjkY -&tB_`({%6uu$T4H0he1z8JK!71P0R#y!6omreih+GXE!-@x`( -?{;3xN8qPe+DLrJr5hi1NY0ve9pnIoB()($$H@L=6&t-Loda -Ftb!Rm%>rtxYahP#V?qE1;O8}meXC9QZy3We+W>)wufad$EMbhNX2e)j9XaCGF)7@pQbH;L$-I7Pr -x^ttJvkl2l*^#etnrBCmWrG8E6?hJ+tv6w`HRuazLg-jf41Ni_DHnZ_tUtE+j!>(7 -#7IM%i+IHNX>j}SCn;v6EKQHi6&t4ZuIi4Q(7TbdNpQ*T41zQu2gl5>I`V{iS6?%W#=H+SI*F+Wb%(Q -$D_Ge|<*PxTYrmwr6G*K2fdtH%N}3A*(XQ$g6lvc}@48F~p{W5Qf*mlo7(bNGpCIZ&|lpQ)K^8G}D-zRc!g)`NGZ ->8`VH!+-7@yuk5|~P>nSOS=A_$Tq1WI!iP)6Z@_*l;?fDCHRBLmEw>jSt4 -8e>>sdEy!P#*>;H940GX1uFVZ3k`l7jhV;_Cqv_(IC`ww8G=>G-K_hVlLhWW(*9yp8JvVk2F2m8$etvNUozzf{rXtB0H2}Jbbuw(Qk48S$1zaqQ1TS)c%B)fQ!0Y`){<`po1_JU|@oFc!|a1WU-0Y_ -J7j(OtYpN*#>u&a0DY3o>w#S>A5S96NbI{ZG{3AUuCz&^qUp1VO|tq;+a1-4Knl&xC%1)lBX=Ph(AIG -VUFaHV2jluy-SFCUI}%jcKfiG3cdl7JM^dBj^4i7?|4)(+4UZzub%@ziGa`5_s0<*)tpD!Q6LTh!IXJ -DaxZ*b*o0Vm$TP9(7B4bHf&kL9a#u2oFmQG)0EN{48JNE -NUSE{Zy(^(SUw>P0{-)-q3vPauo3!ocSWuq9?DD+k7wSs%%a+<_XXR8a@B)T&IuIUVFc -?7VTv1rU)V!cA5S1Q_8HjlPg~kXW;PP)D2KD_g_02s58ZKRR^w~gZ~adv~ls-Y(h-E#oIF53CgGo*=> -1{IOPv865nA&v&(Tl+r7Zl+Q|NH?&R)qzbI_vNVRc?cJN>hW52UJs?WATXZGZHEz{MeCOl3ahn>TeSrNSCWVyxOR_9*1$+e6FJVD#WkCSWT3<=F%a=UQ2h%Nc -R8dyRNRC&D;Q|szG{Adt5hSK#+Yo6k&wSfsP60utdHx%HlS+0SV(GdQ*(t$ptj+2}?*p%6!FVu%oT(q ->#Rkm~=XZ9@)doFU!3-S{4<@(GN>McGvU1v|+zL!IU^y*Zkf_^sHC5*~u}V76BE1xv&&XNiS0Tq%L -B4k$Cwb(+bLo+rK^gM@(ZFLEt1peMM=&H{hCqRSk#@1F%LdREz;qUx8jGT(AbP3lJOtbmL!A8qg7VYZP8gGBEHYxw -u~f1NM4}>jH+;^kRJ}dJ~5mlwK|j3^p+06#g4ZBj9$xI#ZhfP@ddSK(O2?43-ViN%e4-7Z9+&IOSo+P+&_}Ag%c?ln{J}J#+Ec>v&qBIw1MmO?rAyio$RTVNV -@uYF=2ewrMm{c&G_h1^YG*cTz<0B4x6B1XgqUw!U8ZfX5>j}_XFt*}>ae)ID8KTNipvSBF5{!N^ -<86YT0;(Qt9r=}s&8I&cN6?L&JMG#PU5uYWyTq!BYzyZ{f_G%0Ulq$M0>-K;nF=)sWC-W5t%(Z-cJwc -TnnrI?n&#t6>~_*BU0(r@)5bY$E;+Z%sEu#WH?CVW3XA0NOFNl*Uxhwgfq|){shGlgV`lEPd?i{EUcP -Nx^;ZgWz9UskCVYIu8-QRY%0I3WK*HCKiK2OkP?Fl^Og{c{kn6qdhEoZgVQ~)Ztr98 -U&6;9;K#wsBDPqZrxOS(NW5LK1_m-@ekP2MCVf;SDNiXkl>nOB+=nSLhHe*l6jBoT3Y!OEx=th|Q|V+Y`3#l -azQCV>USAacj^iGdl!llPD?NA^?TnArwl%aFz52!)Su14XbhO7B7pjz_KFg;%Tw;cUTRvcUu`!u -VqwhSp=o=CHx!%@I^1{iEr?Dam3&S3A!;0TvO&ZSfyU8seq<9w!zaS79EkB7|=9cqSHjUfs^GZaMUQZ -fSLmLi#G|eGy)`pa~J$qdlbyYkvS -sus?lVr-;C}P55(hh#$UH+x~2bq3@1Z%Yrlb>k=0Ltr7eMcFQS0O)&tN4~_u{rtj-!3j|&0&EzS7x84 -sXZ_G}h^Zp|C8sB|TvHX7gGw@zPiv#|h0e*)$ssZDx&)_B(4c4j#v?-r$kZ!#i$1G%jUI(rmOEbMuVn -2*9^4$pYoEp~JLjytp(#Lo0F>}BRM-TsJ3pyAGX8{vDe;Q#hI&bl=JXZjh0+fO4SHKm6gZ6X#Ze*5ET -Pj{r0^p)q^oJn9=TByd(!Na35*<*#clcZf2l4`l9slN`68JCkP(%rdWuO)63Qnx=3Ih5|39%2HzSQ|u -=l8psQ82>P4o)Odolc0h)Ij(;l1yO1KjJI}OdShrFXGoeD -&-R)8w!uJ&$S*bL^(V?1d&B*T@FF%JSb46f&SZC#+wicCG`oqX1cFlWtkv)tfe}TO@9T;c_JibEM=e_ -7ATa*AQ5Z2D>Wp&!6k^;xkc|ab-lZ2T%mtZdtTynR&gxkx`6ISY3R-iT{=?nQhRpsMudsr#XqECv1eq -|nAhqlfjRCW~I$mF^*urnNIdd)qMvL5s8intvMh{1)@Pdiv2@msd#^mZ=f{f$^VRbUG!W9iq!^7He4=Y-d5@2Ob|rd?P>n~ll6{2HY<8>l>( -*W1pwGA)t)yY*yXfJpqrgx1ds34sdUDtW4HT-fGyP||6XE<17ISyFvyi=S@1imm -0yuYwO)SQf8Qy&&h1HFqH3laDc8MSQJ3xu~SOo6qtyF>fawV$m1^h6onT_I*5pGJ*8Cexs}&GpRm&;i --ayMDML%1QIq-f@~du5`GLk!Tyg>F~%M5>2=nIAvx}a~_rQq(^qrOm#1{bW97$x -*4vv4Vj>ofdrrs7l@F#1t-xVw%Ez-BPGvT=i2y-l^MkJwl9YM0|;v!|vW)r`A|w%o@F7_b!1)HIha2V -x3&95S+&p>77>N`4;uzGA{2AZM7QKQ1T$9(>*q_BY!=McY9yaQf)m?Mk46Fd_1!DfxIfm=z_8ojojgW -`*@()!#HfuLuMj11P}2!5`uYHk4=k-Rqk}wIw#g{YtVmN)UYi31GC?v)kTzaKsxYE-NenZPW^RE{vV$ -DAs60{EoXn8(f;DQk*|~iKs3cH>(ao{CgCClfM*466{lWd=xA}VOA=SOEIh|whs9%LITgj0%qcQnOgq -pUbh?R|4N7cCd=mi?fGmH2TLwo#OH3~U0{@*d0FFNZf8b3TE_#JZ76pJIOC*2-H)EFUEIe6;w-|0}5i -Uf!#K9L?6D)y#&;b~Hpb3_kc(Ozhl3+tJ8wT{p|56!fudw+2Lm6=F*Jhjg#cXelix@&0_a+m->~u1L3 -tfN}6`bw?E(bDzo9s+KBtUHiHnxDbt^xQ<&1^4zI=k9|HD@^){`7d0T_zCAN6_nZy2xv%v -qOfeYT+{O2wn`kBE5dri#;p4`y<`P5DObQsD|$;KcaPwh(PRtJLJlQh}Kw$iwo0VXr97o>>a>!%Z9d- -Q~cbuRPB@jk(0Iqsj6^=O|`K*5gMocK;ByNjOT{atlZt#FUW1j4lSkl{gbQO)BOy-c;r$vK~H<92mAIHw#kt;Ag&aQ_r -^F?o@I>_+5)}0f54b~vc!IQ`@S$2@3g#z2Wa_n{JfIke1C{0;u6*4sNY$*5rCeSb2 -#3zE|cX`Tiw)U%&suJOO>I$@5Z3vjZQJ$5pxNed<}B*tuMb&(vdGZL0frv -`=m(#*a%)RanPnOolXw0lo`#ZDrdF(ef45uIN}YxnYgbVtd(INDCJ(E2Supadta1!!`x<4cB -tnX*Y?BU`0;O-->!R+*8svZf(jJ)!e5ko;S* -;ol4PosbbN&BTv#ysh!XyEY=3BBP(n2k?jw+X}XD=)*%O5QSV&=Rj7cM5&dB>oxJ{AlR!dY+LSy3`*s}`=r`KI3jFZ?HK^2Vcaf*6?dk9 -;u{p!KuyYbgKSb3SPN$NoQY7^Ru7P)xj5k*RR8=PA8Jlapx4YEE82Ceft@AdbKn=Z=P7qBsgRSg=qtN -|)#~@aY)2#GG*ecyuw%=qvn9!wVq6-2m(vY%$g;^&1q^#KPhDwVn;|3(g1jJft=$;4QI?6N{H2E$$m$ -vd*H{A)HtYd-_rW6!psZ#+HTbG+6=>z`{rPf?}HdD-=upGSL&Cv2UI;$}jWHOzUTZZj|s7`YBynl8_M -bbAR!ZKVavltmx{8KGOHp=?qB!WgU3bUpZyQ^B@2_@Rk(~==s?;YqK6`qW$THq4%_C{gC!V9(ovD$JOc~+kpBuxz&E847Wk=)_7dny<*qTdzi;I!eh!1%1|*Gaje##Zj0! -VS|@Sz71uI>n`>?5mDLcfA63_Gw+%A({&jutYg_62oy@ir6K5lxSc3wiWvZ`NdS{CCDT=X> -gW5LYgGM6nwkzP-xqdiHT5@tcZKaFy7|*_uI7K<67upZ_hdbg3lLX6J;jxxT*t$V0)|~0bb#+z?jX58 -NfHGHeY({L+YllRhD&OvnCOsqJ!7!+}6W7}WOG9y`lxz5WMxe4gn22{*U{&Za8B2;tts+0z3&xST?wL -@!PbYNMu9EZAljItcx*?oTc1xIRs=loY$c{$R9-j)4k<1jDC|B)k(L6{|4IZNdE9ym;$;Q5kcTw7RmF -o{2qQ=kbXb`!l6f<<}fm7;c~ -28UXX{HC-f#D+dA9|bkDcP2Gx6i@>I!>S^!W;9>q1|%3655bzOR!hKVrUld}6k;t<$}w3PwNK>m23Am -@^3v_-dY4Z@P0i;>8v`w@@mruWEb5v>Wf)x`9Q`87&J5X9$N_4Oy=?aqV0<>~@K94N(kthp#=4nu2ya -$Xj9ux$bdy2~-yeqG6D{be^vqK6JY~XLqTMkRI)K6>QLOy%tV+v|Fjd$srZr4(z^yw?LZ1tDCLgjf-! -}xJ8ZC5e6UA1v#v877xvblQ-EcGu|bEAEvinhD0LHWKbLPo#|%_B(Tq#U&aNg@szdc3 -b5osLZhyrOv9T@4Hv#WO}TU3ZO3yU(4*IGT`UD?WD25!VSWs~}gCv>1f8G@I>&1rHd8_g?K?vLGL4oI -Ju0nBdkNA>OyKzd9?f0M#Lj^TPUe9!i8;S*lkP=ah_%Dow~q9qP+n;hwQy4_KAc^BCb_&7UPS8&_BFw -1<7D$wgas$tlnK=2&K!+!WZ^_miuZ4l1fB_)YZB`u?fJC@H}^zQSbAp5@zh?K57>Ob{L%tZg*y)T&Vr -x^O^7l$BZ&zghRKrJ%c8?8wzx>BN0aJN%wy9qwij6x=ft$1X1 -(T3(Cb$GemlbWulERO@6c~dq*#{)HB-;hSt2TgH0ue#FFvJqH0J17bmKBr;-h{7O1EAi}S1lVwEO8f__+3f@{R*i>8!BCd7x9ve!Rv}ixHN*nz| -``;1hs@(zz8vFLj+4!1s*T!C=|BnHZok+T9!*Cn9~US}*J`5-XLG>KA*35WGYK5ozZ#$xy&90zD{b3v*Cb$5yLGDYjQq<}p&t4fIXWfl&cE+|*!67txCYb -lZ0b!K(A7AVzEY!~k>sP;#as{0_I4tfuS-BsHGGIWE~%1Iz}a!3-)b$v-FZM@xx;-cExK&_=zeaMQH< -_Q$`sV~X6uc7tzfS=ToHZ8|I4@rY4gygkZ$TnIhnvf^1&k2kp)vIsr;!EI-sY!YtmyY$> -?g?1cO%FT+13=~1Kb5}d(hZ}vzqBbuNf)7Px-(~wt!=opwQ727KS2LW=U3m(&MSCdlGk@H(Z8f_{V<$ -s!9G!JB1NDhK+jYgT5MJz+es7i*6OgH$t?qKEA=E>TqH2B!G<7{Wdvst}@>7;6T(vBL`QmGB1^VOS3e -Vwh`5YOiY<*4Hy5O(HWo64{qR57X$Fku264_(sYcj+yE0s&2W>k0}_++_}1;Zr>W$VoTi5vWagM-)n@ -IA=+igC(7`CN4v3eF}1zN)h#OBWAy##mnBl6DrNUKY!FrAWU~UDI2H5&9v-XoTqV=3!;FpT1ZIl(@-r -b$E#$DUuE^qX=E+0mVE%?hQ}bjoz{vvjuY0Rou^T(ra}yIy*yLOJ#7@uWywgZy}~Vs*g}Kv-|hetIw6 -IpORw04^aeC%$O;PDwwNw^N_Rtz$b89O@6Dhij~p4YO>4zS?9*COLOL=Ut-rhWLcuGanVPjm|=tAImk;yf>>X&X3(Xkqj^W7|PS~xxjfp!w4 -&s$WmEwuEQiFtk4yJ(Nw9oW?46XJmI<7}tBpVGY)!PoG=HubPs?31gN<+UQH^(&wM{rc-3BE3Lv0|S| -GueWDswN7|%IW&4;JJBF*OOoQC^0f-MLH)zL{!_i(POwOPUC27y}-A2?HuK@MI2hRX70^7h;^tZaK62 -2?4&mrc6A}hyn?-qx2H+ThTG}thAtnpq@SGcH@a*}dx+t6csmgHEM{x+2?-u2jsayUbDTbKhu#s&gWQ -r;0X}9tq_5$G#P{oCyUyD9w4M&kDJ;V)$A_0=r}+d0mGrip+%h;uTx%6-Pu+XZibBJP#Fi}SQ;txr8R -s42XFN;_p5rxgZPrDzO-)TDJ80gXAKBU+z036&N@-!~JCh#?WXC;%VGReydb^9)9d$uvJUC^pk(o)vH -<1z-M?8ctkUJr;q3(U}S?Upk+y+_Q4hIh2QTsLx2~MWYtl7D_()Fp;iA~w#-lMngyM1RC&jfHmt+GlQ -FEi3yD+J#IduQoSoC>V4z57JIT>|`{wQ7ga8#rCEeTdgWAyBVN;xGF9C? -vQqk-Umz;$;U3a2lK};ZZP#5MRQXFuVxb0ACWAB5YB@$Sb-D6feeXP;`;hFtV%(0_6=fJRU4Xut0eO0 -Zjg7s*vC=3xehD`-0$?X+oUw%cQ!q1<`{v&ij~Dk2dTG77S<34<#SkDH_#}EZiqy`L2$<@=Q+Lvi{dY -u}l0wpaoy1U|8aN<*POxEN#>TW2qS--YQ>zZiC#**F*ceBZGc7wBL>F*Zmow-*3>s&)u3>!zlWso<8l -6wicd)-5!3d{bR3DO*_(4MMF%we(WB?jSO$0&+me|rn^Jo-)zJ6)qZxi^6mM&-I(nSi&J@)FLHc6T_` -*XSzjef6%uxVGr$i`INqFE7(9~SJAKzTyrNkXVhG8pS_9%iL;{EQ3@GI>sBS|QM -|$=`u%wHRB$4(WpQTjk-Z92)#M8eDbpvXnk-%$^HK7cl3Z=&-1YBk9WoqD>0tQo*g?BxnglOd2Bi_^; -qTb1kj8@Qm)Cd%6ZL72q1#0A&65;xjNMiHJHJ(-XS?ku`rvrFc%*mzxYE~q>r8dXaBtBNSv$vFMV9O^ZJaOS*k@lW}?;d?wB{GR@1N3;{#k7U&3xi -*GjNT3x$D&t3%{PW&Ri~$p`SnN*a6(Gug5^OA=MQ2wAD^%+J+}5pl$EDWUtunkI+0eFr>7&rXS+u<<~w^?cZ0Mg}m>8)hprw>=X6B%SrG+b1_Z6G)V6^pV{9Y(baqy~n?zrv4=q^WS@?548P!7xoX8B?wF -*FwMX;4PzvM5HLeBC_}>tilZ1t;RKA}4Dog039^CTmdUWC8U%rtI1PXb01Oi^u!>*EV$dbOBu2i@kfj -&I(B$SVA%=;T*8#EY+5qFriW~+nNKLT?gao>*q9w#q1(bso%;MDZ*>uxHq+*-_l(j`&X<@zWvpR -$2RHZVb;s*)Js?OUCT>!bYDtn&($WEFRTTA8qx@!<<*@>AnW8&Js&V<3Ec{LswGs@XTg5>RjUUTLGfI -Eb9X+=mYbyB)w`Uc-)QefttzmfFS`_=DGJx9>%|_Xng0X@zgBfZLH`C=>iMCfI8dbt5L9LeHIBI2e`l -U_Mks|e}CugL4WrC{?6Njetds_f7c}5Q#q26!V?K$bfVo(33_1r8%_C#J3l@Y6NyZn&d+E35+8{qZOv -Uz2K|<#GA<8`8X0^aGuyMrAEBL($}&?&f}i(s&NlABthz0;tG9KEYuYny*tYj()(|=~?}dHSc&N9t98 -FPs)TInac!DR9h*Jxd0{UPPWiVW~)`KQBU?wIQP7kW$nbUzrC2^Kl+38-}ornblS;EOx!J)gj)sCuRr -pch(nhDFhVW-W5f3%YQ3Tsw*F<&Dl+BxL2te@AGVQG<9?j8G#eE(<$kb2tbIzLTJoyB5}Sw)W9Xm>i^ -9?B(;9a0F9JASx=??yryd4IhZ=icy8p#m^Nk=xvEh{zHPy$5Pl&p#sf6HWyg-a6`RnL_x5p4!SrKLWW+b2`|YPZ%1R$c<6UogViVsTOGiqEBIP=q6;*7}tICPtP(+lF4t60!06H67HgQ0OUno_5HCit2QoA{p?BtQ$kEbxmCpuZm)+lA_mRBT0~2owl`tqO_MnDht`Vs0i>9W -UZ3k2kLc_5$%k7d(>vu>8kUR;^<=2KQ+VFW$4A?*w%iZ~4SQ6=`G%4KW*kowRFL(PIIIG>XPLmiKJI* -R!L4bg_lkMd7}_|5F#+pL(`RHK)H;1RfvMIoa3f@R&oH3FiW04()v4H%AFn2zHH|ujhMJ+o9=Gq6ET8Rs6?EM<^mj1-*{}W|y5i@qPJcM%uYD_nEV-aCxul#1;R4C@g*${=l4iniX$ -P;-V5wOlsV@NzY%W8-5^u=GMnX&X@q0OkIPO2HIi`FYPMo+bl9Vr}C`)rRvC}W{+us$Pz+jG -=4wi^P%Zs-}RqeC!y>0a7Kl1l`3Tb1>lG#B1iF*y@eI(ncW{~K6jm%Ty>5qb%#zg7Nl%>)K>^0D59${ -JrX~o~E1^Gi^{6`+D{+@@Lz2%|mWQ?qCXIZRscQu^Pa3PZ|6|Yy;;6G>BcZ=D< -4u0Bl71c`Ub#@vebYQeNZ@Vt|W@{c43{RXTD?P;pVRi3=^-f}MTH{LtIZX#9iAL-ZQ)s|kYj4<#Tc1= -MAx1M-^TlLrR*+ZZ>7x|4HgY+NXB>~UGrp#rTdAzX_;5eVaP9bGaFn9PTk=Q6G)QUwH{Dy2Oo8y|Z?t;nLM7=y0I4Ef6kS-)L^a7EL@=PR#Ge{}F`$PhR#jnEc0c{u;%Ax_GeQE_ -{_=EU{$fwRc1E%6tf3WuEbp|9}Hre_d@FysAEsXh|at;f0q;y6~0+;Kddq0`MaE66%d#wWz@V0y>7o= -bplU%$UHJ{g~3=l`erTF<{`=DROBEz`x;Ta|ec4N=FG|@!JXhic!BZEP`NRGO*bZ`g%vylAeJgukw_1 -*|CD4{}RQ*lE^wFe}&@ai62|sIok)@ZqQFBiF)U -q^;r>=ziWv{NWUlq@WeAfF#2YRjq#_p%hyBy7&-Ls4*laC=${T9%y-%EW6;LS?kewg7t^}Siag6Tr?| -bqNhL-TIz?xKtVHW37^veeNFiD~gxV&#uMpqL9mEYW3HYB1qlLZhud|TPgk}*md6nt_-)NU1dkza -v!#azI*3|iR|1}1^K2xbV_i&mIX6OWSgggaaxYr6X^IZ8>9&+Inc?U4P&EES%U!Fip-A!)BK=` -95{JwiwV{IiJ1|(I%L*sNl&)LAL>IRf-|_3ZEP4E=T+CDx6Pco -D-<890I51?h2i<9o<@QD7m-tvQ$Kt{*UHPbtZ)#R-_Xg`AYn6kH<5Ya|;^UJN+lJ=ojzxSD3{B-LBGv -RBu=cC?=?s>nl3kOGvnkK(zuVF{;v(H7t}Ki)0VS>I4K-WO+!WxGtyk!EEkII~Chz^(h34ZFoN5vILb -LC*;xZ^50malgl$q@0E7BqfiL0g-|-xT48E%iT-{t&c|L0bgczKD$fZ(r;K#eZ_4pJLz?Lf{laF& -K`ZFhNr^i6SILF*t+ZFoobK0n=ZWg3_-o?%{ALK>;=y;1p+;5O{(tF>2~n1PU;_Y{8FhDD+Eg!^A5)@ -&$ePwP}C+$~t^g0>Nu9Vjv9SbV2oUC1zRn#eovQeuHf+Szx^Md)w|mSyTW{FPl%muL9L1SR(YxW=qLy -s|aKvI`HtoFM}J?%+e=)+q9oq7LGwTEM6oCkSWApul&C1Pi~*}Cx&oHGr~+gb0Cj|E70iZhXQQ ->5r~e)eWF}5obBksi~he5AR5tdHwtJ5o8NIMy$l6~YO_F~}1w-*a}j*GA*TIT>xR^p -GDlHWH;2Dal>{`s*<@+|(`cHycYM-BZu!~Sv9(7!Y6AGi63{$|9w7xMFg5D)BmFK=lo&Q@pfbPK2mQg -0{aK3E6l#1cCx&xB*aqW99NEHyvw-RDj@?SSk}`D_v-?0zhF(~iI@>nzCPZt#v!EhuQ*%?V~rP4i&xt -7$vg^)|gu!g;&eJ<$~Rd|f%Y$eM7u3>m~!Z>*3Au(jO@*$NuXLAVGR11J?aG3~bjAw}os7LSCMhk*cbI|`ku6?(s|dF&Zv>&gs`H7UE~n5XhKer@v)EfH -%E_{6XF`OkmZ=l`p1{vnPtC6e3?jJn@v7qPd!$MrVJF^yu1F#lmkg+KI#e(3z$b>}7{X>7Ic>4QE+*s -ZMgYb>fmPkWd=6uY}ez8WSZz9vq4NM{YVcl%PWP%aY7r$^`-lfjkbsu2>H_r_e^+~>7!#Xi4go|QuBS -kl!Yz|~^R1`C~c$`w1W=>oqc;&xS46rN@nEnQEc9+Q-!?jBuDor_g!(Jf6yHKe;pS{O2VavmvbxZ!<< -t1+6;l-@r>Jd{_p*^s!{SBIYO*LQ8(BLy82hEK)akRG4V7VR`Wi`;aM>ATbS$snH0nir5M5{Gg)R1_u -n)2$KX6-$ic?6#I;ZGPWBdWiF`N-tVrf>?Z -BW{Yd|l8p}^qy)V`N@5A%oxzHcL`SV47UN~lO08NHqXlD7%HZVrv41v%X%8(d9Cr;oPPJRVVidl>zu# -U@4NUtJEELeib0DBRn-jcchnS^|Dn0zTBOnm-S(kd_|avCCPsw>Fa3t+KvGX6%2c+GL4sR@he~stS{(;;Y(r_xMoBx2quxg -gl7AK=BL*+cwxw#8^@cioX2jfy8d>1`YJnA;PU@2XnyYd59kyBNT1L3(~taB=>3=Ayp*~Q`BRp$8d_>aNt~}K -qkPnqwWWoVoB-NLpa8-<0rpfvs*NemsyJQw0nifIxzVyu?0Tq&Zy125=+HX2K!7IWWHbJ_wuHt))H<+ -m@cl!>@+9lwI2(j#tss9KLp*dnnTU@xpeDJb)vQFi5gmqhxhiKiaD)sG~}?xp)7W(i<7N;b7)SdqKNe -7$>|u@<>6$pw*yCB`qKqT`t#~iYKKAn$dE_15j+V8=nvZwd4KP>4X_Mh=uT)t*1oUl-La6LgHTZl13U -J(9p8;HmyJ@xes=z<@9&+a_K0-yrif2vx9Sn`Dl(_;Xr*lieCD}Sn#u6iV%I_d`28b3Z`XsxZIR3VA) -`@pwXQV}z!;t{(T>k&ds1rupp|E#xQ}!++zz)jdUOaj&LEgPtglX|(y@EMkt2MmkK~a|!(I+kuMS<IXI?_9O^{_AvfMXlTl5+^14#vId(F>u+7)C0_CqV#U$#^T|l#l)xz-a_guA2RJn%1xuuG57xPdm_g!dSkj`gx5Tcj0L9ZC7Zr^az-ckI1Y6cKtIEEKI -QkCA=UO8?y7grFr`vm-5`_;1vZ=XD#ZXz;mMn{T~3OROZD8u?uf;`Y+!7KY*G4}2ohRzitSg6vR!@nD|BR1{+4UV%t*BnC|MMm+Bk`ly*nJZmh&ywzq -UFfc@fJ9q35bnzS3ApVL;(JA!*!q<5bi4eD;1IbD3;qd6=dHXrd^3(S>0yqQwo^^zxh~na_r3O;T%8$?{udh3q7nT@t-<#UeR(=)SAH05kP!*iNGR`mc-T9YoQ^fr;Z@nunI<)gmagq -NK6Pu^M{4l>VYvvbu{e7ou+v!W=)m>x14X~6yeFod>Z*25qf(Wo&{|9rte;^&KMH+TJx4?n23Cso`+7 -M-!&mZ40*w|4ppT@J>aMrC`iQOnreP6TltfudZV*UQqFrIUF5XJF;v;F{$lXY5nu*qM^sdqKoFokqbucT2I=i=-);^ClOM|df1bcz~Q;Wc1p3&zb}s@K(Sc9S|iqVzbgsaMsZ7vX_u)>9wJxVD1f -b{9cf5IX)rbPf*jr*?%5K(4XI_fy7n**10G3Q{yDm%2Q$28=K7`Os=$$P{56N@sKZK%DDIm_eu~-Q)S=gW1WOMT^0KFeU@*xygVTcheEETd3-=Xl>ihrWoYLx*DSw!8(A4x{_-ZsP{e8K -p!I3Fh^kMQifT^cRX8E@xjwZz?_M#|e%&S*@RyRZPSz1){X09_*U&`z%1~)uNYS_C+qHXr+v3fCu^sD -86c4fV3?FSgW`m>_*%Wy3LESKOx(g}o#4|!v6EZJWeY7Ul&h -yba4_m8kLw7|P^Z6-vbOUL`mV?q^n0vKaPy4|ADf@dhf+P;{VE9j4-fr@nLi!&lSd5%QxvhHB!M9aMX -f&&Y{f_z$0-~mSKhf|B=)H_-PT2nY;D6UUV+)}SOf0vZhP@^>qgoY=I*l&(~k}#Y^Qx0z -GX(YxprhPWkTB+&C34}a&J=#_kO4N-_Y;Uvn$e)dw0}&Z!1!UyO-ViI-$14vWiMJ**UBP&BR;)mK4>p8_O(1b#?Kp1!6e6Hyr -D-B}JR63Kn95cXB&o{Qe-EG)G9P0fR=Eyw+VhRH&{e3!wm7mbrQ<SCaGxZM+|03ZV7F3l9>LmG$dGo68^z&H -TAY>v#07X?;ZKH!h14db09MdEhz?36#@QJIa^Vx$~$2+!suK!`J6g*9s24+NVeTuF$2SkaXg2PpFAmr -NdjCxN6Up1BzN_lv%fl+k;kT@B?6|4aQaybx~nDNDj!wGq|^LlKr$-eP0?c?Gx*siYk_6W}l3d(>tZA -(5qSyiL6r*Fqgw=c;i%jLIig1np(2n^Yt+di{okX?TC4~lR~EY*25EH1_MJB#Z9G;EkF1wXS%P!FZNq!e(G+FAyiCc)H>_BcC<@SKe`>{>n_*z9=sCWXSVSvJkLO0~BS1}TrKqf;>EE~`?4qKJ -4jNVJG6#g06Ss>|-HM}7ww5$OijB2%~23-duwrq}uzk9?J9(JAszL=UsNb20xm^4L ->3j=MXl*lj&Dn>(qviN}5a1ugypIe+(0Jw8WpGYkinL3;aIO8PVlm5`s|u=~u$@VSvgDl4gAglJ`Vu-~d0~ss&Sf2ZU+U51EhO>*v -G%*7+;nvyLpVryX;iz8{=t){Qk+D9^#AA3|*sb+5$ldyy21!t(Ed4BV78Iqa%vkXgPj!vj8&0D-R&DM -PMYrgNH(toVc9c{E#DWGwcmS)GU{)s&B;8^6`bjWe!Q8jb=fx9b;pzz#qZ4U8nMAj8rSe9`sdJn%3ik -K}~jlId}O&HO9Am6`g;4_J{SUfI77Nnh5=$$b}x7{DWqJdv-3wv3}U9+`)^S&+g|>sik8#CjuqER|bo -RR&Y?^eNNnLk$J;a!K$*6%9O)wlukhi`nV0q)5&mqZ(;0a-=>^$A`sp!n+wX_#xl3Q}x_Tso0@2`h*l -N_r#swN&;x@aBWuJ43oDgbpoPr`qS*5l!NWuZr60Z9mL|i)$wk6W!N!!02-H?r>C -6?oSq8BdRF%dy%$@lUt*$khk~ITal92KJWU#lOlrkT*JpZ5D!D>B5Pk#NxcCBf*B>O--V%qpz7ABgpn -Z}ihaOh^`}+d+7fCutlYY^5o4z?eqII-BlX!0dq4B+D5r3)uK(Mx|2S*rIUp9*CDj-m#Sl_#mXAvuws0zb-AO3m*jvpz?fJ -l9L)~2BaOQx9zyiqpzQfFhwIu8|P8LTNKd}lJ<>Et5f_^%An*ZIO~SDjJ?2v1F7)HvOqKq1V5IN -{f@1Jg+69XJKi3TOlcoBY~v@=azsl8-62YkuLN(!C;n8khn&pW;dyw!0P%boE*Xs#O6o=;8cpvPpR56 -o6457xI%e00j4hx`ne{6h-%~orLGo@cc&E&L+Z{uIAWqX+IY<#B--9ddUMfCRH0q_B&N&>uN8feLRN8DS -X2qFBfHk02DmrW;?B6FmX*>`dyD|c3SAt*4L%q$(w8F^w;1hHqzC!GH_}@<6NJ{(~Jg-&-Xo`CU}$Tu -?V+AYA+aKl(Zw~lKFzm)Q6e7egV~W;VsTF>!1=@jga?<*YBeBy4k|>Fad$aGq>mM)QN-7jYr=<%P%{s -YV*mXx)p(M5j#_;;Nj{}y-MSCe?c$$K(ofez=z`T0L++V#UbHSy(P9i1NY-`jk?jNu8MCRUph2e -m55hIOY(D&D|53p9r=$KFvi*F-k3kzofFMF5AcjB?fl(BOBcIyZ?tZi(wDEc(*+};KB8xYw9fMoD6M^ -mJXmTs)CtEy?|%tg$)eLR+;MMfTRMbc@zD)4FV<_Zhh5x!2ebTCWiOCT3fY!FQdx5Z+q9*0 -3!Kw_JRLZ`04z-hGYlX0<51rPH_6I=F>!6txXrZRPZAOH+V*mw1|NH?f9~YfQPuZ^@ryw%3-Z#lNZ3$ -8~x6C*F)*Twv78%rc -KOODYZ4+SqGTQV{?*{lOsQc`0{t&lqA0qoTZkx}dluoX~UXgHF4= -+O1m)#S)hEj|K%}H8<+Y_wnobVAG%<=m~-Ksg30GxJC1l-L9<SLP -*XStgM<6C8&H(cv&&i@CDsAAdT){lKalsG0;`X#ND72LGaC`$>tG&B7EGkhS69Td3*+~Jkj>$*tOrP$ -)&tlM_y4>D8-yaW&E2?vAcWWr83~!?1mX^EE5<|_)B@?EACFMh8Nr2=qqh)lxj(T<|hGf1}hjgi*AjD -15@sav!*!QpQW@cg}O6r^PEzlmD+bq~mTXZs@(uL>9AO(YxvedV6oHiPKx+2yrXNfLYw=Pd2hdEti5F&dw!(~8p7rN}9mUiA+Qha*9aZG&t)C)<)$$af0@o -sybZDi=bj3XrfKLcY--8zjbBVbIZ?gkGjN6cjMK9IYdUDcs)+ziDH0!LhUDN$H!oKz0J7I@Vpgq&ECKxj4V6G$DJ$ve(zU0+<{0 -$m;sk<^t}`CJg585)Ne?W~I;>X~=ggP=&Nh30`K7JOYvAQTMta84-sh7pFM?y}@*}Jc?9&J3@Rg+I)p|~ -|aA5GM_Z9vK7+QbjX_(U3qtvGX)9J&L@zbz7SxvNM2jmui>IOC`#z{L5Iu`VI!4!-DzlVO~AAo``T5v -SsEQ}BQ`7NWmO=BiY4KLkb1|1JyO&LEm#W_49F6S#UI1rh>#1SK2l}LR94^g{-xjzKxo1tUkWi>d&5r~zF07p-R;-F?e8DL-C -8{_~tZ^KK!Z=RiFo?jPc0R;gRb&$_BeojI8f;<2)|apbNFcTKwx_!$@YYlS|4w>_BsPJwc#ot&bW3g_ -d&MPzx9E?8w-;;tg```Mh@xAP4?(wl9`w7kTLx|24$+=%TSLQe55_hT9Auwa-;I!?cq`+q7eK}Ri8UfksjX(Ui4bqv!+$L7Jhvjnz<%$$KeNV_&b>Z_*zCyNmAMGO&qwIw&pIFSH3I -vZ^tyhLS!uqPRla!(ZbLjX@MD#SYd*AF0{6qg@Rvr0X%mFm+USiBMqOJCL7S9b(e2Ql(_on4UJ@&S;KEBN^+>NuH?BM|>GK4|UG(bOr%KYt2!!h6%0NkrwPO@977C -}XDk!(NumPVrV*_OU(l5v?u}ioDQrNRf?CC4D3|=XX9-zt9QU_(xPH`yI1IKE`F`CQ_rcLjrLM@GtgxVYdL;smucLQ6w^pZAsd2l#Uy5E?gre%Gqg<{%?YGiBs4rEJ`C=B_w2P -3?uUqgw+W@;_9UkIpcHk$9OAg1_f5Ro5S{yw^1}Y&B89eIh(L4t#B?cnX*ezMfC;#nFA@;HWMBc0hMG -W#(vl}=tm|XPaX<;-u-a!kr4jX2juyxEXR4G8m>0Wd%17wMc|NHHPAL#UhK-xb;!oPXo&sg~Vpr6_th -^>1b-v-YR2*e-^LtzAlAPOZR6vHWk#Hdf(09VY)c5RdmZP1PSukeUtTfqmVHhA2iCj57l?G>fZ`{|Kz -gE(T(UaZiy$&Z4Yz43-s_=ZO4#}wEu(H5gy-$S<7qoCjAGf;RNHrq;1=(b->Zuqv%n@3wi#tNyM_t7q -(kAYjw3#N7trf7Q$P`ssQHXy|}#nMem7257B*|B(YcKUOd!)pga`x_WCkYD!3%67X(r=^X*T-qPQ>qaVf2fIrNRy> -y%K@8ax+W-w!x-mQzm8P=x4I##pKl?qUr=l+%Usnuu-A}o;itPsoYSbPx*=v^<4Mo1amAz6OFwZ1B(A -bnM27Qda+BdIY2j)L?EwaAPHi&S*+X^y3L5w$0J#-L+g-mYN%Y*G!=VB3%eE30+oc2?EkSc{k&#tKl~ -WI^g)i6wii`CycZfpSvkuc~srQ^S?1hqEvZvFy`O&M>T(2I$I`!~XH=8q2xKfYe^G`sfX$tHy1RQ@!t -p9Cuj`>xCKGcO_1w3B2<|Qdwi!yK!)|9LhW@th7gNIs`BZ%jI@DmsbX*706JfkU0R+k(Mq8R;L#zMNc%h;|Y&ipENw%K7%z$v6}9q -WlQ{Il9w{N;)JVgvml%MR(%`XD`hL>aa1cI{DIqbifhtJq!tyIBs=zi<6xDlsqUEVRF=?Efgjhz#hJbp0z4kr>e={l6UJRRnJ(AMkXQHowZ^W8+tmOhurh*|Nt$eEx8f8L -r6qDqx?F~9)CZ!qESVD>;v38LhG>y^4;8UMQrH=N)jg%PfQ*4H2NnI1PY&aQ)l1Aco-Py@UM)BPUuOn -gTKbhjXk>AZ%lHtKBZjcP{=x$(p(7tCbsj%CEWbQtEEnZy(2}bi&4cR-H{miIbTg}=tLN)TJqh~Q-@H -W#<5_Y;d7{WU1~O9RT4ugGMM&dFj|La7q?zHh#>;NNS@KgkVPs^Pr%XA^^D&H0Q)U@bA|r+6k+Ju;0g -*K{gERhvAnDf~erLz~KRUrTQ{Ml0@XzySD1xmpiQp&+qtptO6uQz$5(CNg2Z^94j$*{81pqAGM!k~F! -EZ~ZZxY$@mc^$&ym;eX!35eEAqoDQ_`EF;$G6_S6;r9*O&8r?MEJ(?5^QT<-xSpLZop^waUQ&pyFA#GaR=|YCK}%?`Y=kRdx=lMM -vG%s%LoWTu-i!TlCuwZ-x8rQR9kV~(8U&1&6~!*k$#qVb|CxcdlMk}BOrZJ~{b~ -KlXYW+aQLh?NOc88EUlU8%dcdv>OG>ec@74v -i?tuKkTo4{QpNz_!Zax=E;5ncnDufF9oek7^b%2^7a2K<`Wo(BP32jFiH~ZhELPO$W}97L3pLWo0@mH -q2^|IPwgff$(G0b8i}{EKaOq^Yn!SmwyTV%zf)7 -gHmKeliNWo>D~%<>{XXNZU33#RB{l;P2;A-}+^6N%kb&5z;wfU|_~3rA&Gchu(_6o2vbzLt;l>{sF}8 -JOj`%l#cgb;yNF^4Yfg}`JRdYgzex4pKKV!r{i8nf}3nnY&4Bw0s!85e0i`}%`VKGguwI{F*fU94p$= -`_)TrFfp!n-W2Ymwyc>rVXsi+lYyc{1R;w3@$3P0c?^vaFPOXUvrb8O{Y;8FTsT9RvULmVdot;Gf>|u -lnJCpwwLbl5krS5fPTrJ4>tq)RVq74p&ItHeB4hL=4`B|kS*vgEg8o=;KR2&Pzu3X-_R -)w;!ePCnQ{;jUI@>WZA>xAPe=*hy6K29IAEe&JMf>d#IX%GFYf^I|xzv_w`}-O)-smx9prgo!vA{OX? -cxpClo1GJ*v+q8F3U*z(5wpY))p~2ifYgqIw=IX?E`uGxiir|Z56&v^A#@{b+!(0{{8r7QsF!`My`73 -0)(6hX_#Mv3!YK=XyM76R=wxILF*UD3vYq$7lE`0`}TtlJ+&DfHH&?qqY*mHa*a4SeTlWN5<-`&EN$y -4No!syY%*(Sn;J*%M4c6VNrpPciZQpX#mHh*TrEI+-oH39%}AMVoO^k8r5U7)ov(LR%I0hr%uw+QkV> -TIbRUr!d!2pw;T<}W(oz~&PZ!_*if`uGKlzkMGr>-J<1>agW!xjy%&@4^Si{wV)7?p;rT2-_Qvs_Du+>L19w8czR9c1-93ky?DZQxqmb4s3abp#5 -Cj`I~bQSL;)a9XvzrxwImDGD#IoWsugPw1%%k)%)qw3Me`^k8}_#j#k>G1-lQ!0~3N>Xo=u@7tr(%#q_vOsPrrLcCsAIN9z^Y6(G* -?j%P-v5+J^DO({)9HVHl3ytGe>lO9sEa}vLBiyUumni101U5SyCN9{Ll{XBAVDD{NPRjALF{&~S+)z7 -CL24(kWDcKCpT%+l@t@?)-wmC8*7Gt(^j`1jiy^$-R6ar?8@T%@ZNg9l^$c@E**+*nP@WIp?pW^-_~S -T2E6so!5h}D&<&y65Z{WXD{l^yZ89C+HByn?XExi9r$nMz{?#gWIeXKaY-p2~zNwnS5%J;X!i9d -lZTW+j6|E`55f$3{#WjYp^i$jZP3md_yqEAeoub^8Q=PgOLuuSo*b@o4H)E|9${{u$7Rl&YZ>&f34wP -1qU-_$Tzk{N>LjGfaND=cY-^x -n9yMj9bX~O5&Ck{40^9wgm_e`|kHQ=-kLEGhz%a9j>U^)n^7|7-NlUJ5Xt+B~MZZKOpJp^ty09Kf>kg -SHb`)K%7}8iY?5?8J8syJw`0~V%;X{-BH!BTRAPTa`3v5AXJBWw$)Aqe-RRc;xJx=la{;%$Y5TwtkEn -v!ER3aHyXWwA3D8$epDyD+hqIbil>Q5kJ90dq;&s&pjCDxTv4Fw(_!FRATuj*UFj+DR4gw$I{EZFS^7 -3xpy{j`;z8h~fv)HarD&ub#!K2WqDAOcW*L!lPhNn?rS#=UB(gz6Z53JbBx2ZONysCas9fi{RBIcF|M -v;gz8L1WaGOvr%J$to%JJ8=_w!x8t*l>u{S#{;w}d1JuWSVdDFlZh;?vROa8Ki{6a|a+{*v8o585Rg6 -LJ%egz-(s8QUGMv(GKEH!ibM7-Un9M6umD3Eh&0P`Gh~m6Lq*3u7O%Xy`^X)|2gq+`sb-M79NR>zRqR -QzyH;B}#5eUF$lzE}`3?8oaqP#l$wNws9(STU1v{ltsJZ7POTOHz$aF0BwC=8Md`Yha2zuBSjguqFv8 -^9~?UqF*@<*_oX_1IylCezYdNC_WE3J)%ZQP#?cKQip?YSC@{QgGw*zaZtFv?v0u|@Fjj2MDFC+>;yx -@BAkEPx{bL0A*KL#jOLL0V$TPn#3c#1{6pg)DUFbnrgjjvN@Ya{2%o;sT@|Wf17PbG#@{g0lv!pJDc@GW5plAnrY#snx}vQq6USojvt#jJ{6{UZIyt1HxgloW-OdU>!IqLG)07}LlfvBgqjI -&Zq=Es68(4diBgHWWGlXX#Mfo%iNxAoY=+i;H0_qVss2a`4o;{0(l}alXH0%PT9R$KJAgnWG6R -W8dY2&n855#pV~nS5;w-t~Z4E4WDInYTwk+d?O7CzKR*WUX*Cb(Xs+z6Mzu#EvlNTio>z0$P4e+Id_X -M4+p-ma)u}t;f^g$6CydBSoe9VP~V^9=Dc9M%tSDtjCpg<-P6NJD)nq=LZct9=g|`Lim}>LCf}=h>Yy -GuceqDK**9_!O;aA#6YzIhT*W7HFrC(XuvYNOrT69G3RKf!2*Xc6K!0g*6~M_u>Hn~^SQsZ0{_!#1o? -_sC`WWYVO&=di1<>YEvCrfuLLa=StzSUaV?~=5#RI=RTsyC!ndL8HXy3DZg{h_WO@htgSah(M;>R=19 -ZwCU4=(ly4Aj2xxqXGcjU6FLrVxiKqy|! -rXz+3P7iE(KLA&{dZ&&Y@JT5jNTGuikAJV3LZ*oeMEm+4l$T)pf9=4{ -2%>v&ex>fBVyIBGZZv-Nax5)mtUV#+8 -TN7rxYaz0=TdqGp27tC|9JSTewn6HZI_wSxNw{Ge6>n)bJl#!=pzV_DTiDzKk*(im1$KnmYIxzF8#cR -*9CYnhxKr0Ntc9hE-NyS5QmO5{ci5tXj9eIKtD`pT}V;@ytNVG1S!CbyQeOcKB-E&~u+h=w -e?#VcZ@nl?>YE_#AnO0&`;wfY3X^)@UOZHw&s!Fzh(y -n>Er@??%p{{$zQr7zE5a-CJ$as=T@275o`@w=98ex11cGF1?+zW}tDC -Sn-m+2uimFN&`za4{K>;5P`0nf3OZ?bmu3+T>o(g%Ir5Jpi7pSUPwRepkr%*OD%>BDYKWy>5<;4{K%S -oFw?%GWy>i_=Niu$`BPkKJdUsR(0j=j%+f3mM|_{V*JL`ImTFdQVw6%+{sgGdU;Fc1N;b+yCR4KRU&D -Di14C$!V96^&LX+XSL^)x-@;lFhbAjw=-<)jIkXk0R@efQt!I)*w=CKEb7h$;zg#hQWr!=z -VtWxQ-ldk;)30zGZ<1KQbL0beyB~D#D@8VXxY50Rau(Z7hTy%?WyQ$N10mcRYLfIz`wX&Y*j5g>LOZr -+;nJPquKR$yj@I88O&vE@h<&%z72=W#E=hbR$}24pHS;);x6fNS(_af$7kA;SHD>{8!UYtyNte -KU?uzahcw0Vx7p__tuo(mb7M)KO-F*=s)@08$ZLt0(AN8k36JYN&;rRFFkMdEZN_JV~U$7ZTRN_rw=rl?{bJOMv|jBKD$>a)1qQ{lZZm(^up&$xk!Gj>#Zb -BBC-X937ZSQphe -YtS7;~$3OCyhOFd1=o}3?TYlEyP?De{)-BY;ic10M4OhMfV@5Am_H<=f?qW*_?|@*@5RI+D*W8yOz+r -ArlvdJsF5o0vp>L%PmM}HlcibnX@s?>^CQE~ -AcLn7+t7h(12+ehh45n1HoR!Juxm_sN&7p&%nZtV$D$%roO@rwguOSCQZQ{o7R8}-nYKHPa4!K#dxee -haQ2HhNT;q_th=4hXfeOv$JdK0LN&RG!~=DF#0U=8dh&5ZhNJQpFp#d4J)oD*(;Dkww{+3UyM!u!6MJ -|6RWZ~fwe8!bt+;}_mO%$qTkoOsaR|tN1PLIJZF9sAPV8d -_oKorY(S^~*n^9ZB6t|e8=pY`iAx>rrl!b)zkGz?Vukm&V -%0%@WNRKcmc#7D(@Y<@&h|X+}fOQBtdJW(vqu -07}a|`OJ>X45Yj{t-& -!YF9#^{&g+}$9pQN~4;)bF=0TRPh+TpA+Zi0#ywUMaLiynDTPtt+*|sd|TS)fpwPhI2zgYU`aPAL#ev -Q?B{ngJ}+b{&eFae_!gnimO1ntfeYsiz}TS5Q{w?WK|+8hxO#+O6!-2J4wga;r+tXOp@*yZ3 -36KZI*DP403tDLqkOy?crHRPY=-vl}Xv$#Y*+Wvq5A~vpv$;=#ccHU%3=14S%$r!r;XpZG9=GM1=*PIw -1&+I2u+DbrlhLE^2_4m{O)B~E6pHBuD4lGgPrYgAj-Lbqxw(v8=BJ>K(-n**6eqPSiNor4 -$arC-|GLMN__x}w*siul{`q-1iM -j9dA8n;R*%r47$gM-=_RGA>JilwW!kpsgtIBePf`JKu|PqVf2XkzXg8CzMf7|FUl(d7oEi;kS4pb4;a -oR9;6X0& -15Ct>OmUspqFOtfT_3)C<#<3Z!LZ}yu@%JDE2JXN!EOko5zcBDh>^wWH=sXoK}{TE`uo-1x9K=;=?Ph -^$94Cw>89?oZ*JsEtEL*v7e@%H?*vc0to4wsz|SdzBpa(1{9tq1yjl@=7xdG(Ojbg)Dxe)8dEI6@~_~ -D`zJlFY8Ti4qVM7D{jdM)!(Vrk@cdu@68`rSgfQq4-v0NTh2#I8)8z45e?qq}9jg0*r|hv)f9I3k>M~ -5$jYz$o_$Q9PV#lkD)A4`(wL$j(=ezl#yY_!@CqLpM31TP;ZwY(|hdy;}im`nrlh`|akS#5q>~MmhTS -a(9&6UfqNQuN-vVtT(w>rePBmt7`x(a(TV?8B^Zq2SZwHL2m4X`rC-9!WNVoLI3X>GNLGh;4nryfpliS4UiiqnyZEZs629W4JcS`-)p;*{5_^)qf -6QX_LM{H#Tzb+)~wBn*H-hTB7lwIe0d6?PC!#B6~uS^po=8yVS8pmTz!p|=s9&;NGz?J7CfW}?E68)z -u3eSn?mkEc!WHgHC*4#r+s3$_d%WU!2G=;Dm4Q?CU<+N3|IqhqadfZxL9g(WSH^XeU4yjFdCOj80qJ0 -rGt#fPiEFk&IRloHB&Dw4Sb(suKoxTsI&K>W1D|;+oY -pm)IpT2ojwzrv|zW6F{HubMaG<%tkg!=l|tNy9N0sL6vm{}{02~3SFhwCz@YB1)x6_TZDc?MKdqs(y1 -$ZmRf=StS3isJ4L&ixyXBO=ztNVcYHa$f* -CpsOQ~PQ#Tb6!?52Jd^CyO3~o+LFl>TisKZ7SM;P<97{y57lx_(plb1erTTj8u -orM;xk;B^yG}az74!#)@`TB7u9iP~s%g&*FXUhJnoH))E0(O{Q#FQd@s|7b3=zgBhY@>=jaY_1*_Uv^ -?P;wgMHjw;_|k9&h13Ka&yaQ?4|_0HZwahYOR?oFl1y1KmTEMH#Bw>p8gQWw9EAD5#);}T!H3i1NEDMd1QU5N*?eMnnF-SX -Ye}XQtOAH$A|{@o(>=o(lQk~t@d0Hl+1apM=f12`o-|7$j`~h@&$GVloJZ(Cj7tzZ1=hkWaA3(MR^>t -&h4;leXsTxLwWfy^aX(Y1r0LVA4`!n<)&V5g`VgURrODg`1!#7%78Z!soAq4S^p%V`UyYcbOaRORl`KRCfg7ts@sIQp(nvvcf>xfy1v+~55ujv -4I}@KKlgRoz&YGh$zgC~f-8ouFt`FPy4y%>nnT$pwy>f%o^IV@7`gpiNg_u6PTh#w2qdv9nXj;mL0g? --<+AZ^LvCefYdU{;L -wC&5tu6RP^PW6y&9NMQw7AU~J_<%xi<%pN#|m^7|b4ZWck$Ukc{S3cnjlY<*#~{!uXhx((DF`E9t}x4 -Qv;yq*8a-2gw{&QEvqHw);Sx)FUplj!5wv~+!|y1P@<=Z3D38;0H??3kY@qO@(vR$;hv-(f~v&(rOK1 -R_NDiwRWjy>MgQEbeu$tdIbQ+#H%(w_ZSyOUUb&$E9)o(zD2Q`6u(U2bcOCtvXeBJ|g}GoR`L7)8QUI -3ve8j<|^aIOw-XqgHz$PKx$3qr@YLP<5`eKJG``hY70cg)8Sd}s{#moG`iY)YY=6lHIiHmLoo!{G3gz -)Z{LWSaqgS;QS4!A=u0CU5i%>y<`j~53ZbTe+sy?ZkDR?$gPD#DtKZZytdW=?O&Xas--XCF)hds@HOT -9jz8*-3_2!coAkf6yV*w1<#k#^E*+H|OL$CKZx2Iel)#B^0=i8qY?yvCj1zkAcGkBRl^Y#4Fc*WI`dp -;3mK{aXak1Wvgyj<~b5`Mg%%KT(8xEBZ}R2|gINkW-k?%+Mg{u!m1Si;2ZRN~$^*9%VA*`Ue@K&;5o5 -dQPg#y98`D94q?2G=k(%VR9J@6b6aq2RQ0oW%D_T0A6W4chEG*!cCQDv -5V27IrP9zeIR!22hiS0Bnfy8!XFma{apE&fW_hqr%yPSEy_s$YjH|BKW7tReK%DSiT#&k$uLQrOOIQg -|ad>$3a*Quk&{j%r)7=sRDrAI~vv6@9nO&I5=>AQ~Y$NpJKB0g?dGef?z;nYl7^MefX9RmZN~hqWR?f -dDBL(}#~a+?_?YEFm1sI;AAQl9kJ?KLX=C`g4n*2rwgVOOnr6&{bS%;L6;%c++$wn -klo^meieiSTA&$dldgP>OF^r3MImg{iU6?LatPPkW4R6R7=VH+h^-K^l1OUvN?w74%m7jXgS^eIFAM= -jV)Q?;-y=!{$XoTz@j&1|-$TCqw1s@|M5%?oCgSZ1CpyTeuK5g~t&6CMr`1H}-Al8V*AVz>3vN7 -6YT}wMV7Jo`nKnl}k4pq_HG& -;y-{o291?0W7wa+?o^{x%@|lZVjP9nW7m*+KkaS6ITe)?Z1mpOsh`9tjiOZw!daY=fjLU1eAPxxy?@>MPVpE>FKx&y% -Eo|sadGP?_t=jrkM+S70!(^NKv_d{@tH3!iniyZ^^|yBz{E)xoAy(BX3{%>SOlr_VcBFX`H&#oEk>rC -kO1sjCz+{5D4)~k$7<}zYpHXaP3ukU%0qLt2MdB0VoHN+UDCl{7&K0?`_V8>QMR7{1yBG0D%g8Wa?tL -+5ExA5&Msq_=YM+XCl*Xz&)enLdl|pSowv%YK#iZ2Dl&oYf)IOwnME!G!GH!AE=+gKf>3=xO3J;HOtO -G|;ZiSg|2Y?BvpMdG#e)3m1`qxkV0hve?BN+^57#ia!h9$|*?Ri!lA-PTNPJqc^k_4QBC4iC63O)2@+ -7D)PNziYMeqrv)fqo>=@TUMOz{#?0{21M!g#fx3W)rWhEQ!NFz#fVLIAMRK-X~&UdIz$Q(tm~&*inmE -phyIQ{A?pTn^G~%!1i0pg5KlG*RsuwXXRkwHlCbP;KJC4#}Bdj@kJYw{=U<=1M2;d3n&kdVPQL@}Ph9`u^nQLC -L?9g=;lriI`=ZycGP9<@PWfp?gBt+Hku*19NPzEAo{lp9Odjy(H^oF@;3oo?RWn;T*`xZhnXb?)JrvD -A(^7q>rIEYC`77JhBIJ*C%{_?Snn8rv>E>$J%=;sA^oZcvncp>_=XU;znAzuJJD1icij@2W2Y17CYkJ -4pQ==3Wa~Tc5Z>>jZN<9xTXy_gGbqW==_0yP7T)dlkbhzIUN%fp&AA<$;W+?8^aTXvV6UsO1qPkM?{^ -O_v9(`9kp8nJInBTwiW$-H@a!rJP$RtpOUtZcF@XXD#6}g8Gl72$E1*Bt7@v{vyA`p?7f|E(NlWuDdl -lw^5RW(T81Y1WwFsZN5(e>!L9erb2b+R_?Kw+sdNV23b!w#&aN)9QD)zd#xZ_l&dw^7=2ais4h=;<+c -4NMBt_(d{JUeJPY#Yg_@X4qD|gIK!ZWI`gS~$8;xB(#dR<_D^N|35=O5dXqK4X3s`qE3BSv^eMh+R#M -|Cx%Sx3Uj!&H#*f(21wZ3?mj4Irl+A*R4qJ*FGpbWR+f$RwLb$mbG_MXhz>Qtm@_uh%wSw$?P -VaTLQl{=Iq@0_e`uZte>A+#@CaiAZC?U9Rw&60@pFfS)nChX;AhIQo5IR5Rdx?|RmS;sIG)X&msS>I{ -D*)@`;rpn?7`xhQ7ogp19bazQEsW6zC^~mM}BFhErajm)E`=rB#G!sMkvBni#8+Qwe&z?RTeL`Q-$iW -w4t?wGG=9Vas+;gZrx%F{VT7MWv|y@eoG+}A&Qx!Re7`fbL -YGdoy&rI=W{UeW@Zy@SiB+^N&%rMwP`u=O -_q;sn{Gt9jkJ8e-3@dFuSSuht#^q7wi<1= -us8!Y9KrZ82JVpE6t(N=)+;4jcM;!AW6{lD9XR1_9=FU^6EK@Yd%VehfU@=Xb1-L+6MIkprr1&a!rdM -IwzPxZ|LBSzR;|#NDUpT4#SclR%P8QBB~M1J>6FWR?(?tF^~o0_*y%fl_W^UdBRK1mT_79$Dmps+@Dj?8Uw$F9KmG1U$bx>vIxv$)Vmwg -PQcP-ut8K3G}&WremTwUl>Z2JWc{xBq~BBMjV4@kdz_x6t2aBYP*j=2%1H~U!?`bAfx+vdP~hZdgsW# -Fi*2c5i{sd8Sk!+HH+hRT7Dm$@G5M6YRnhrgeE>vGwC63G`5U+qZsKj2*Jft#6aZ`*~ehS@Y~fOrhT}_M#4 -=?Mtpo5f-;-ZN)2(I*mKXm{{7H&rWd{Eo+{Y)Tj!5a%iUS{fL7lCigTqLLc~Ew@_TOuP3IxKU2|2>ZO -mLdngdO?2qAzfA^2}mW0ValJu`$V+?wY@~y4IXZy&~Zv7EBldJMScp9e(5~bOnQ0l*Pp08l_Up~k8a0 -XN~I78wbgM-izf@MF|)G!3-EHWQSx-r;w0;Z}ombDRKPHt8wwX+<|^Vz%hE@KkHiapd|XwH2WQpDKc1hk@;KPn+J~!GrVU<{S%w6=S5MhfPV`RamN3+bwJXdef&#T=R+lJkTZG#GI8&+Tw4^{oP=%HlTP{qhSXW(y9^!kK@`a)FsieBuwm0bKa&&8A+v -?nQ@p3lHfhu#r(l-eDseq}Vd<>-iaq7?mXG!lv1wgJUq$9ZS+C~#aU_{(#kPQ&QbyHR&TFA{~L{_aim -8P9{Pn>l%ZF^E{~GwvmeXqRRRq;qN?Trhffr-RT}cds(MFvo?KRl_hRM7x&>f{{{B#STS^B#P7-daR= -pik$cbqi&GNbE8m_xgxg~N2rk&%x)q$?t`!<@%7qEJM(&;d0FwYK3u_6>YfQ=UfS-dvZ{d;(3xiQ;U( -Z15mk)vQZB9mQ^XrR()_V}9jLH|sbXH4P%x{{O60=Y(7CG-C{umx-gpGnY;AM4WDwJx6Bm+H+51qA%Z -VSz$WN{os-kR*!gm&-heL13&H1iA;*};vPCRS78M-3&MSdDIB4ro!d<%>dOdrN6J74SmLRO)+6L8!_x -ED&->g$!9-S7^=q9-^GV^@^9|QvD`V4RqF`b7E@m!V-kPxM^%ahg84pj5ydUm* -_8#{Uw?|y)pt4io5AJLq>R#3l(KBOg)>E|_K5+WLHS_HyCcey02E9*8K2arZ=PR3mUG-Is8hFlnjRGVYe=4#;?U)s@67$V6o<-42zV0piNgAORbFPrnk%Ep&LryKqaeL<{mwyQO|dm+^@wo -{9`N;kI4>vC&C~ogAQ~PF@O1H~9W@nJfFa21eqyk@(GIwFac&x_W00zgJ89xi>z|MRz8~yEuSw+fQqz -m9N(1`BySuUwcKY%LznDuZbsgt;qa^wm9$k{fpyM1YRx*``r_ic0B6)>oRrC02HT+E6%lUyv(bylvD! -kTDimXKVt{aqG?W$J9ecOSE5E0#+%(RL$3EeUoGZ12van?T$nZ+lI^CoSr)=vnh>YBG!qm?2}_rs9$? -h)NncgRWInV|Kq^A`S45KXVk)uVG*_FAH^hTmTphs4X?fbaaxkh{}!;AOG9?0EFn7SFc~5y_CyFAdN4 -CdXYzX_5kyV(#&cVqO?=KE>@mO+2@;8{+2UJx6FYd%PGA`muA%*f*eSERT7VlZa$Y-Ea|2DV^J6cN4l -UB~HnHFLU)fF~UdxICY`Js%&!~9vSk!Iyfw&Ipp-GjAF1yil1ls#5mUL-qT6ZYKQq8NT+@4-!H5qSL^ -#?nab{AANR}g;EIQxWgxeBfoyb_%gfTz<$H`@Vfpr+`nkBOoPJYmG|>T>HJ&BdS --uhImZ^DQa5h2)U$pZCh*Jf-gqEKHFcC4@^IAfKh$!srJ}l|%`^Q^puD#Jk9w8Nqx28wKlz{c9{$B)U -kk>+4*4N62V*FjKxvBPP=e)f5~DbZB0zE+$6+M8)`g#Lhp(kygl;mKO{1KIV2=RA(2$@uBsfq{;Tfp2 -Xy%{H=OrQV!(u@nAB4AU1JEI}>2j!TY-@|J!DFy7z{Q~30hHSr*wn_jUzCLFiPmZ^j)7#G+e*LnIVcW -#EeQvnyq2hIod{Yl(KZylbz<=C8?d`Ufga0R-g4WfII(#o0v!_xL_pYYOG23i?P>0t8;Q;9J0e&=H)j -SZW+c$Dsj0KSZIO2}FY@HiGHcmeB<~Au0GYM!_)pob1X-1hmFE7?#xQpGiiWNMHC~-9SL;h3BL)-13+ -VS-;+JHc<;!DYKAVe*1tm}ZyvOlxU-|2WA#i0-+Wlc=r=di95GT5OlF>u1{TI~5YIJh7sY)7Hk_V%O( -<-%<<1IcO*vg+cB83ymUE-JBoy=Q$e|Dbr+BcfUA9ht}K~s{~m9@V*mee2n$06?VvOb-|JUr~VegD>) -R0|G7K#%impPx%q6H(d}0&U+^siF6fUSCcZeDdIBPU3FI>I1UtIn+$v28|&p8tU|oOD!SlWF^9IlavU ->mjg{HjcM1)XXqVThuS!H*5b;QCqJ|1ygTkh`4T-(Cz{XinpNDTVR4&vZStKEwY}Poc9?rYm!Y#oXx{ -nkcoriIu{GVd8|N?%WbAauuGH`M$+g_LWK2VVQ(iksn0YnCLiAsIwViu~KEjaJdOk~MY#}sH=AfwkS` -QcMak(WjD!dwF$3;8hmdf5PI>=0^rH>@gg@>@OXIP?98)|j$&bhbKBf0AMpfG4p?fXn&^Xz&JS0M1<+ -LYt|Fw@m=vb@NSk)n8I2UK}wgEX6vP@3W6ajx!SkYB1Xa0jt`3=dIh?JkbYi@pNFj_X1Z9#ZwhD#>^e -JEIxuY~kM?t3$=DgweKnk=&~q(c~$;#b^bI>kY=3*o=4B8{aFtyc2dOBY{uqt!NdCYCVx{=_FAF$d+G -b8o~O44#!s!303O!uKIvwYVUP{UghtA7Y??VJ#YkX;G0M#N6|D)n9EMoS%aayQ4k3B4n-z?3tm*QVID -A2dYAvI?C`a+Th{t&{JQgyhbyxQ13|>AH*~EzK0H6Gs1d5op*VDeySo$0Qr;Vq_w|np{^^ekGz&$?yW -~K!HyMjTtc-5deQo6dTB%pp=LbcU`V%gLfQ)B)6Fg&Od8Q*ME62*cC7{Gfe>?j;2<4h_$bKNgNbzy$M0g*X~tZTi99 -e5Tj-M@m;Fs>S?78RgGrW;0C$z-c}F409?7Ukhxn%jGR+%ujkEID%Nve)hYmh&j2Xeb=(*K}*%p;R -@7%HPvc3M{{e+0*zYf>CQTbs8_4^^OK=XxUfcBD{St=pdV}&N4nA&3vQsIe3eg8?84-e(-O5%F|~WW8 -V9cztwR^ps`& -1;>PwSIXb$Z<~Td#1m3F67TcTG&S_bThqaikA?%vT?p?%>LCBsY0d8k`4t$6$YKfJarvQ?|t7rCOkdK ->0P-)dSOK-Pt_?}w^uE*Pn}6MXU)FQ;mQ(1&QXYqbumX;JdZ-6yawwS&Ak4~j`$AmvgTuDd2EGLV1AJ^xJQ`{!A&uJ -j2H)W5k4;1FjZIkQ6MsG6?_lstwrwnuM{ey?0CNx-gudhq1lViq6YAoXmPx|JV&9hfXS6G0{cw($CwBUq9>TmzYYxGlfZC0!d(13$ -bMi#UvBNFVgC0nZST1;|V@D#BPeWENV6zLQ+DuLM@{>7*}*%By1P?&?Vs}@WWkj6N7(W$};!R4)U%qaLL6VmYCq=$rfd@XI_+KX$U1 -`+EuW=2ChBGJ$CqPmoRL0+~c!mk5-EBsf+k^0 -D7t}=f<(4F-VG@Cg!;|z(J!C!B>S_V~bKE{)xXKq(ac`ueU|5z-P!xVvA8SA@GGNWK&hHpLEan@qtq} -hG}DR)OGck|PxE+s*abbftUhuDi>X_f5TFl|cyV8zUIQ$@zX2*Y^K^5t$+Ca(qBsGOi)?8igNy}R@l- -*}m8IG4gPv%hFKqFc%3#T40TSgc9t#3SFdjw{EZ7cQjqu^k8f+gkRYDiTN*UtwX#_I1AD3=(pJC%;ug -@$ei;f4v>IyW$u|VOf2Kf*=+znG9RwWl9{pP*v)9y`(5dhw)&RPn{E^;LRKpby|9{*x|X)z`TaNk9gH -3=^NtuER|TGC8rOkUOB^U{60)dd)N6UKQ@mBYo8_sx0K0s9qFALm$+&YN8R0Xa30+j%2lhabG7eJB8q -F`Uf7#Fb5^Uzq0ShMZbRX4_1W+!l@INyV-jLte3MloN<5T|TRtFMaTk40ddP~|4ND*bo}cmgU|TzkE) -&^fgQh$$$3t3T%G~2nA6LYq1?g*9;~og2OEb%*OQ}Ymp3wP7mUyZNt1;4$=k+zETyF>6- -Oxmdz@PXVUAa~@H!l?;-to!R&H;s$tQ9bh8uLK7FbEo|#IQY|3{uWL_ -xFH8-C}VolBcxy-j^;Lpl^9H!&@_;Tp%hReaG%Q%qRrw2rNIaXm_>V37;Nap5kS;z)5(DmQ4F+RxJ{?N!Z(fq!P|PzF#&XPz!WwH7fVubH#ECVc4D9|0Lf6;Hdq ->M+kn5dw-X5fr(N({I4%FD;8b(@@*AA)WfQ%ewK@#^?z#h}a^@qdxh}$!=k;u?oU3|YW)dj3$FFlI2m -i0Uyd^Lm$6q}agnuY^K{8X|>j*4Oiw0K!#Ag -a2AiR6jiFXSjuahqu3jTj&FCiGH+?EcZZIzLkf7;#B;KQRM4D@VFc(hPm(i>~M6w_su&JkD!i-$0I~e -Mh-RlNlO;>=?^%?s7s;_&zaaIMFlf>@Nm1RsHvN?#DzTg$+2(GY@j_)f^+k(KBpHLw70GZB|C2iDg3y -KcS@F@W4~$lw|t+hQ0?ec*B_mGcQo%NQ9GkGDdz?$BHuJFFT{lQV_&|PR{|pw#cM^87;K&2g_32oIV| -obuqPFpvtc4{{T9pD^#+hLV`f)ZB%e%TLP_;VlZr$~>lXOW`p_W)@-%Rc5Y;JL`8FxDCs=vE9Dh00SN -HUIn`JOoc<*0tq}QX+{u+5&Z>xgb*fmdDRL`&@JZAT!wEGk580dF+EAL0cDBuQn1!US|j|YBuJYu>Pu -&x1c3w@5a{3*yDl{=b4rQMmjfOm!4cu5|3=eP8@*_1AHvuBQ3yPt#^U#l -ME06lfYu15bbSdYtrKiGqRB6p5Fp`6U^lY7t|s^XaN2Y#os6EWz!;3V&sp3k#{%S0W6YU6n+8}!9Ig= ->tvl-Zn8E!<<*+s7-N7}*h&CHkVp(Pbt{amL -=wOz2TWX6dYEpGDeI3R{&`pv4yYuX0YVIf5DA!QNzu(qWwUspL!kWz!{(p(Y*1}U?k$7bzKOpgUa>IWgm7#yddcUFaHUA|lL;s5P{s}5W-^23fsN4bL%@0&wv>#C!_h% -?sU+4N}{o9QA&jwUY&#*fC1$)emi^yw*!PZT-ru~BG3&rLVjy@r)%a5765Fv3PFWeo!9B$FM8u?sThv -1Ch1#+)!GHSw8@6=DRh|}Xmw=pY1BGpN19Ac+u(B9s7BDn2>bTv7>=>7Hb4$J;hsny;e*y^E~qemXYV -sCpRo~DA5(*#NJL4h40x8uaSJ2ZZO;bWtBn5O62vTX)&Po1)8cQIHuu+=sD|pGKyn)nig3#9-SpvFy_4<>4)C8LJGjKPF?8&cZ!`ZjyM+yi0;i%$|%HMCAqreqg@)y6-47KAlPi -MIwwjI3e>2Sd(?H>#w&oWF!|jtex61Bv!{F;JN@+VAAHbglqD#NV;GJmF)& -j4sTmskA)^n0)*#&!e{2YlE!>oS)TUo&NnrDt02i~*O+vS@uW0j4At?ZvAXp>?GpZD`X$?_exDG-|Qh -=`@aSPx4V1AX}g%X=~BKi``6@T2u7E_#VvB4{-qg%9ax|zbK8}?J%pejKFd=5A4=e8a<&|0FKI3fdg6 -aF^qP;|jkSvuclN)sYh*4qEsoySjw5hwAdnNonG@)z@rEk_HL>veUXBmW~jIX)dp(g2dI5}Y`YWaXGH&Z?*#gKD}TF_f5)MPEBfPgjN8ZQj0rb -g-;c^n5~eKNPnxdxns#!7rL&0FJRPU|%1q^qmR?CDz7I!eKhcU3rmxcM+N1k>|d$dueu%oG0BdT8PrG=spmDywK?^rmbGQobT9dn|+G9aEbEE>q$g0B^9OPh7>2E$2zdi!D=jXFrEa(2&^V5MNJL17GK7$~$%-`eigLjGvn4m_qMXP7vcPi!EW -lHe)I_!koS=6X~-vQOt1i$Kjp9;s{X4?7pHRQBKM&Vr9Q^r`umeCs&gww9PTk+J14{Sln!noIH9HIN# -o7f`}Iv(36*E6~|HD#eY2V@9nr4|ZQsBpsfwR=vhvm(T`Ezcm?*gqiLnznIHYUc3xbEasF8-PJ6iAzH -}GWf!jW-Lf0;_Bx}Ac1D{=Mh%Q^q?);6IOc`C*kQdmTrmtCk&EQsvbE5<4Lx*a9c%Wi`K)yQ%|5V~VY -1lPTVAa;pyTW2Sx4N0^Y_WnqicD48rW5312i&7)GS3VhfIoa=Nf~7hf70f_-%S;@qjs|C(HcvGVJw37 -^C;1^Q!&HOHl@Ypi*3!bvIpm?y`fL>S-{;I?|9q?RdjR+n -Nxv?je+)7I;whg&%wLE905KfJ5G>B(3=SsO7>4>(wg8;ID9{sKAz@v#xlLU_ZX0!M3=$ZDE-%>zyjCL -jc|hdKgVuE&-&&b$v_;e>;QNdMB<&;1F~K&c>jV(1k3C3W>-8&!hK8Vh3m_^42)ME@W>ev0oD8+V)FUJ2<&@^*&h3!hnOGlOx$09@e{Dh}v_lz{|Bp_k)vky>QKSU9CxMB -%JIr)TATrZ^R5;G<0;kSt?SsTP8Y1%CI$v7(1CcRuvD6FXm;Q;CU*ToAB6>%+O%-C98^qB&gzh`D -t=m*h)Y#ZpFxA`$`h^)Sq4)UlX|C(&&Dg)xa&Mkqt#hDjTY5*qSs!kv+ILPmWJ}JZa9J@v&OA}@$T7v -qwS1mb>=+MtaE`c!viFLaMYxX{H2jdsM&%gp+4HrS^G}FgCWRHTd{{Ue?Z?{ol~fkjxAb6+KAlGejP` -Idyi2|T>Q3y2XW#G$Aq9BcXp*}5~P6>occSD -JP7zXdVhY*=`;I(NmDei=H4^>tJ)?yk}8S(fnTX(HY5N?}+!Mt3)}pk7qpp6rFDN8vUhQxA7-Yv=o)X#|)k)~&tN*;+pUWGvFX)8BF-lkW0<&cr!yWP}Hs+k-1LJj~b4c^rR(L`Q -O7lfkfma;!`=DMk`Mgj4E2QTSBr?krmwH;SOe+;@bJ@f9Nq`f9b`6PuaQK6W*60l$m-PNFqdZenxtbp -SqbJ|cqk41gR8n_YG-J6THGCytY?=$plS7>;iG(|SZG8^8%0u2N@J>5MvXo7aGTWc7BnwSMI$RGomq4 -!1oYZpmzH$}|3wha*_re9g=Ti*7!hYbo{~E*`+qAyD>p#YqUmgD$xO_SGdt_l)j$i<$pbSBRqyy~J-d -{|D^%>pVR6yEq45ViS3i1HZZP*2L09oKt!md2zbMlg~8`mKKF-cLNfLmEYgn@zTmGpo-;tZ&+=`D19# -W8%NIqP-sUy&DP^GG2wpkgB!;NHBl4|H?9T<;Tr3k8N{)@xuSkixB~2mHfQ&@NRBA-AbXr{j*OLQ5%Q>)awH~BNN7yw!9p9OKhU(f$?)Di{rZ^&ZDwcm$lzyBMmxcu@ -#TRq$cLDu5s(gMI&=*ws>n?tg1@&1J)FkK`^zsqdPKboN!UqxwJJOxVx?L{po}(Y4@gh_-4`{ -NF;MB$4>loJMv8K$krnpjD%vB}4%)FqcDKSFbZ%E6=<9T0Zd -Y>-Uyqb(uMk|8=}C@#D!Uo8S*c`>2HJ$Cvuep&l+v%t7Z%>QIWvALS4g{sTs^@Ja)9q_Yqk#5vBx6RW -)0pyc)w>*|LS)z^Sl$mK_^DACib^w7lt+SeqA1>(eqc@F!JXBql$Y?DQIul(}DA|K{gzjny>y`)d?;Y -h8@2(uTGSs{$}TXD)oT2>_khDWze4oTezUTP9zM(pcZ1TD2X4dA*>NBo{H8(s#wTL(V=r$vY}a9c4StR2^ -7X;XUj?ul-eNB&|BW>~yqaF}Pp)x0WqE7tFr1AKv|B@LI;mn%5W%^03iFLG#;Hu6d?z7zd)>C`rFZVP -8(Ff4{H)03*R`v?_MOLIzw-pS8Uqk~U>`;(UGXcx92RJylFsC$O2QAVq~p7WOa4&hd2nnU+y5SV_w9QyC*%$+fB2#J%Gt0$ek6sD@@1|drW%KNro6zD$g9Va~+?qxpqV{_xk>S3(>tEX5Izq_}CkD1 -S0eRfgS8cZ3OA#v=bsW(NCrubU<%Qb$R -6}Eus-;b>1-)~)DlvqX)v}Ybw6;SupioeX -@8{ra_*(98zf=vyWKRQ~qDM^|8>%vo)agXx~Q*{zDFk@H}ekm6&3YQ3-L -WgvRzxnE+oW_g&?~Db1R@g|p-M?K7efFGxQ4oRSf1ukWis3lPp$to7>lggtJ{K@CL%`PItcftoA^PB{lR+rXKj;gJhGSt*m^#|Fz^|xGgP|-Bf5Ppw+ej_bTG`_+zo?)nT1s?XKaKI^{U`fO+zLl`HG4VdXOnIis_@K(R1O&OQ0{!?ivdVQTfMLiFKnx -54LxPZ2@jr1rnDM1cW{b;a1$k2Lw(uLIUu*8lOE+miN=A0{(4R -VuLK`trm-2qFHzdX=xs;4iQ6Ln<-DG32LKU+YSjZey7w3i@;y`(-ARqyfESIbg!Pt~h80+?APMMEfvX -+71r>k8cJj>r+9-4+oaQl?1NbFWelOi4Z&(e#~Uj|Kxr(mI)-eEVu{uV@HhwG>_aCHHO^gNO3T$8Ep+ -q3I^uWO$uNG|_r+}-}?*j~Ya3`ZT+Zxj -8MAJr9oJ)ee^XI2-yct`j&T=5EwX}J1bdK*qWYW7z%E=q*J+(HtdpIJ(OKf^Gs6fOTc45E13tOr=7zV -JE4zpEg+W}ggk-2pqVztVX`bp|tz=3)?EJi?|eiTKN7P*4i+PpV$q6{VZ8R_tGFz^;BkJ=5Apqxo;+D -mmoMl%SVVW9+=I0ek88a`s?J|4kAx5Gkl%$1kT!{Q7=;tjMS8`dtGQc&B~V0EK?#Lro$*_aF}RIEb38 -W>`4N78y{yz&v{m!w?SZ`VMRNkmtkgQ#{m{ONvK1te_>o63_A+`e~tvRT9=GYRctnD~^*>7A%^(jx(1 -JRc0B1)(yx#A0w8gjuRI7!7B}JS-ZUO(o@OnRbyFAW9BYu4&q)EW&+;b=tM7)y>}g)SDH?~2MNM(wwJ -p1ol+)?7sDCiy)6sPU?;q|veYR#+?zpNf4e6$%kDEI=+1B#G>p`vQENbVPb1vjS-XqHfFq&-lVn<9cN -hxj}6cbRG`11$`9@!wwiQ=E+Fik>&_+{9o%nDG4h*Sdj%w -!Keff^o1b*fZ;wx#ubRT6tq(5O=|~={up#M@huy6g{Bq3*8i`Se28xI8{7t$#6QVjCGkWVn5bOuD#ZZ -sVcT|Kn1VS+76oF&wfbL43q^w&Pc#MK4m>0_I+i3g7%G|1~;(1-s2a!Vu%x6p -aP?aFXD^pq+K6%TdUh4wp-cC*-^ -97x-L123+ZvC4>hme@jZ7 -4uvEad0=f`EA}2Z(^gt)f{%lWVC@Wh$J>&dOy06c4quw}pC2NE$+>2=o=4=3JWUs#d7TvUzU -Nmi$n(kvh59@U?oWX2^s?lxE)!Z02JM(uj;Nf>eIBU@~uEOyPyi}-h6nc8|-T@K0~Y2NNo$7Wr -HV6H#{Fk}+nBV6wS;#Lavu&|ALD=PA2iq035{u8w|2mqnp2^z5;dwzT7jG@WHoBEuxAcR#WF_K%xa*8 -g#&on?IRsed^0N~-GaA3N(m{~z5IH)ghF@Bj<~Q-ts4E3kaz@!&tEmjAcU^+WK_FAx0f$Uhx#EujSTp -H@&ykga!x#w$^Ykw~?$o>vPj1YO|eWV}OAx$N<(wY)vN?G@9aVcoEx_yEtVw -!%PROcBmYA3Nd`~_89U=E6^cF77hUu#L9ug>r~v=!SB59LGugTFN&1-$L0__ewrO-5=lbaEzrO>AuUS -Cf>E^t7^TZVS#qu9#NOaYo9~{p=HR7C#|3PT5mrdD5IdkQrGXxrOs%%n#Q$;Ilvr+cFGEndILn1=w?k -lsp5dKEaR-7B60K-;NTS8yi+ct0y0PqhbZy)pYyboFaVe(pwp8R?@$(YlYPw{#Y^a(ixgW2H~7uHvGe -w+uf+!YH!P%-Dw41P|>qW6>*TA~V0o-ho%ByJvfX3Q>=v;yya!OppHrf%2fK!yDa)6b!OjYW)HN&zHkzXI9>ETh)mwRSq1#<-c!<(tjx$2xXxgKLvT~0}<}MDCto!BmmdwB8*Ey+#Yr)#;o) -oFLgRG-;V8rG4uK@4w?ZLlF73imddc%5|Qma>T7pHF4<@)ZfXIJYHGgZ~?^p|$V+k^8A()7XZr{pO^@ -k5)$vUL^(Zu)~%zLCqlVY@T(nCqp}9h{v_WWQ(mxe+26T~_$VukKrmrUFlo=NNk2^GtDJ>kbbSTx9RX -V@g5LeC&O`6Illf=jFEd8Rf~$XBW?DF1^2ZcMET^B)-%I3-t(I-X2cgDhK#5)_e0Py!hxH9sJ8$HSok -Tn;q-Xuf91cQazuT()Ahbb)rxQzKiklNSm2;;D-&us4Z1{8Djd6-79zXTlL<@PJJ3e2Tg -OFs|WwAj{IId#R%k-%}Ma?`jfd3dFUtQ;jx?_(jz-0oCbG%`#_05CSua=KlqQmlRRzWTlkM%*oRfvgg -=`u|KoqHYr#M4|3Fz3!x;KM)c^f~OYM-}x_=zsQFV*EpPz*IX`a6SQS|+@`zL~oc(KGVd}ViH|o~!piAVVzUqYoRi|)L=CYS!K~zu!na_ -gO)LjOW@8|QU(W%$r!+X~zuF_7}0K;BgWR0j2mhw3>ji(KG`npKTGaV1p -_Gq?g9Q0MUAl^us)x1GfcSake2Oj{EKSj=#c=AbuO)v6)-P(VSfnh4Z+)Mv3JJ>2sdC6Cm~{Wj*k={! -3-|aE-g%Tmz~M18Ie68NCEk5!KS>ByDf1v+taw)m>o$3pCF`SKbK%i(*{&?oY#6Ipnvz1#VO7U4v>-W -dVaO_4K?D##{5KuBxppjaFKB-<)RkGvo_4^`jv?|*1kITG8 -KzvJiU(1iqsr~tK)}|z=3P_dw_j*-yJ;|afKq&ZKhc+;u(OSn`K)SgizprOw5*P4hel(tiyDzS+SpK? -8H*WNL<=gAm(GZ0X-){HIW2Hx#ZWV6+bKg}}&=>PY#t^?HE6(=%TBF*0UYx(KzJC9SyEvOe2Z;h+-6! -4USGF%r=5pyB=RO@ozW!FnU|ZH<6#|Xm-oHgkRjwu4c0%ZnWK{qOBd`!+5M`uAv(T3+P=Yk -3x5NDM}~1&HOV`vk==)~gBmndff0!so5ElYKw`BzV=8(ES(}Mi>7w80XA=5X=T-|&!P@Y0ca(?0a444 -GftEk}W<-0w%i894aqa-hmezFBn&d$s0-Jkrjyv4bD7csOx;~ZFTXqlc9^lqETFlwqT}>`FZ|Zd+l=H -B}yIZXnn4yJ!m4(;n$;c0h=ma=KA6zOC_)Qe1>WWTQR;~x{6Xr}9cKTLe>Sbhr-7*-c*r#}&>G -xQ@xEuN;IKtgzwHC#FiXnM^Z^4+S8V=qNxkX-6YQi1XZAqGvBhZRkkz@&EYUsJY_^O!H514KtyW@C#I -&0ieg8RVgJH4Vj=ECl2n?Z%^l&|!Pzyp<%GZ$UG+0^>zPn8wwG#0?V=Epk)2ddM+Dio`sbr*jg-FHcO#}-{06v8iC5t{?UMQ2T{os8D* -R$MeKwEhR2^|7yGNR6=T&kdGbOHOI>Mv%9sczG1fjQ}rgtE>tP-_ZB8q+FXw* -Fb80xt8Cc7N{H2o|0#-y)2cIOc$Wb>EwyYOSW)qCBkcQiedh|BWDn~60IRJ&0F-IuopgrQ^NLITn0j8 -$yLvZb_Fjj&?#_xQGDF*&JY+6a`LvO@O+o82uF9fjl=kVpfD-rZJT#>`Y(F-F!KBUBJQ$(Ir)&sueAD -?+zmS@nvST%@HH&)FBtqN9RHIu4>H}0SnmJw{(l*sA&^h|&5n|iBXY9CAL65dddbj;Mv7&}nsj?r@Hh}k3`#mybWfgf=%nE22fe)tY!>PWw1)PV;NHBap5mfgonK!2%vsUO@(% -l*HEXTCtV_NSPK1IY{Ea18qm!kJ*zCgRc_F$+iG_{*XFt&!~z5IAtn52O*EKf -f<>x^oxp>Yq;R{0w-)m@ZD$3YFDtu!iy024t7J;pZM_P^wz<5GK7+7x4|$eLP)k%w)(u-ORe>jii*Qd -zl3(i24PqE1W32P)h-de1`}pFwQYBIwfXi3bfU?J$AdWZv)5g;3g5Hdr>-Xi3vy&RO%E)`LIf*7N>kS -07<#Y_a--}GY?<*;jYPQ4oc0&*;LaD{EQa2m$JwbY(6PbR4;>YLyCl$$dz&I_Q?SRX5z6EEteiH?%th -!BpIkZ-u_~bgm&g+c6?B|{fuwR+NzSW-F!3dDPaeYF7}LE3lMltLJWMcLuVM7dqUrhQ4w9s(c~5%uq> -mOMsHW6I{l%JS$ix~{WjS_4_}eVuTe?g3+&l3Dz`RsR<(No%Sw-7t;u%%+4+J>a%Ml(deWp$hb+BfUE -0t>S?ark;m^!IW3+p4%RE{v%L7~^O1B@#d_U0sb|C-9fwnz;Iqp9Tb%3vIOa+0o`)_QVEJD?*CS%mkTwjk{nYI>RlGRL`QB?&Zrv)`0C4Ws25pO^BSh<0{a1E`0iw7(MV$y1IHY;d}Rf)68=#>f!9G#vAaxsbJfeGT%y+9pv=EROed~N|R5Y5 -eIrmbIn06nAa+YHc)IO*Bg85=@1G?Igz^vK;xB^b#*~y!>R8FneH`C)-&x$c*9MuvjxqBSZ_(X1rp-s -7)y&qU-KE|^P>iC6rid|SwF#g6s~RaxcUudaB7BPPFFWNYvNnMP&`!7QKoV+Sm}#86{XN8<_l5!RXzh -Hn{MHZBXzlC%$CY4yt(-;tsO2BHhrF&5quI!MqNv&xCDG1IPb=ptj5Y{=+eIu;I(*ih)a6yy!UT4As@ -<6lyN3Wv-f&zcbDl;SCBY+_>HnVraQHdG`GYEek|qwsatTgIF>)F;+^nHD%#md6eS&Dhi? -uFWq$giah8IX(wqr4uEb~Nr#iTXC0+8fn%NJ@S-js?0pAjxk%kI+S*NeD5n$DuO?JQbvkqn+%n9J--n -p}`aeGh5{s$6nF6;3nGQNl)^-go#+REB|seeixMF5gW!IrC4;?Jd+{rCRrc%I8rVHg%}r=j&*o0`SIl -u_J3}%k2VMQjv?EK_>V{%)EAtFxFQE8c`)IXUvv@8T7FiD4dA8z{Zn?4oL?TJH+(KkTwMR5n8nCOMIk -juX`?R23SEGykk7+MKyVT -rE?vUzET)GX&w`y+n8%sBd{af+VFF>VXm~s{#wC|6#Yz}pii^&Mog+Z7S>#w%V8YBH{#qeKqPVGRJ)_ -@#QrCDU}uD{BmdC6)k^{;$=hlNt*Zlq^qBqU|t$TX5%de{@nKtgP^>BfN$SQ8-73`V2X#(fa;Q$i#(z -qRG_DTB(@fcz2fx)?Mov~|;P|BRViR-Wrv4Z{<~onpA)HXwYYReE8J^+coh?L?d=diK(j2*SQCI*rrL ->Y^eF8>?&i_OTeQ>T`_M3~0geyF~-Q)j>x{I$8}@5SJUoYT{{xHA=c`c`B8*#8(OYT5qP-cSPWyqP8d -?+MqpIyn#2W0s6URS;r%3wPQ=ku(W}pVMicbv&ZRy^YFD~l(qurl-V&f?6n1m70K{K&oPUxYXn5#j-I -a`ey-OBmM-zc!>h@kl2ZlA7wL9K-xl@E*cg{s%Z*+kmVhcZ4kL-eXZ&Q|fFgZMZ6rP5r$H{MhGvS)i) -cLbtm4^PJe+R}yqzasA84pUF6U$l?c~Ycm8Xho8sPvc8c{vm$WzI`hHwF;q}tlNm_{;fNt)4VO*xI_q -)C=V`wH4lIV~g2^?u&+7EDGTxU!p~5P^2?X4S#p|2N*Jsw@=Xi-r -oQR~eqWFNe4vw<=NX{=#{NlDvs4~G4o-Pxe=Q4+f3ura%VmmLLde7p;Ba~~vrzb<&?-j%zpmo;sT36` -ifc_h2Ep5e7?Ln-9{L=9xAW!g`acWex9;t#D5za(72Klv{B-(R9AJL=i7&knkF9E5!o!H%E`c?2-_7x1qGFer7j&=VhK&FoY3xxYn;59}ik)~ -y}wrJv^ZJUI$lc9;dhM?V7fJ5ewy{)nf8N7Q6TdPI7JbauRkWrr|q{*7-e-H|5piTRMn{Sy3WoH&Ym( -vKE8bo7M9=uu$`=10>R^m#y({7c9d9$q;p{tC$k1@TC&j6hnzEw~9b{Al&C~OBr@mCNt -Iepm|=}m9aMds-InOT+KWv);ulW&zc{g&AJUaQ --lP2(g+|3U3(e%nq_b!{h$;i>42K=c(jm@HVzC^|m7a2Z(Ir%%!q3pW@|WmEc|kigRz$ODTDvmjV-R#x!M63283?ua-*iYeKRMwNddIQQ{0AjPmw~SyeLApiIs#?9CkeK- -?s1w|OCnx1dglK3@wf05sHui^=M!EB?Azokw0_Hy -G~fJmj%dMTHY63~r_VmUXL=qqKQBFIX>jh%zrer_3U8|@!oC9bJ*%UxC?9P!jAPpil#jnaZ*+$+$D~J -Bm;nUgFHOmdBA#Cf2g08BtjjGLCbGRTK>SLLX`f`pV?f2NxwCD4wQ{t=RSV*nB&$y|*!1&LqUk_Dp+` -w6bGMZ8<_HfcZC`*tAiVQ9r(S832s1e7Gs -q>D|y8khSZ1$j=`lC0EK)~Ki#E*9`(VeH>fi`VaTc4F$=LxY@t+kokv=`)3+4;;jPliB*u#pUCf@U#z -+6+k7=*=%J@Q`9-3IfWq6l9-dV?TNmZoO50!Mov1fZr@B4S}|g=2WctAuYwVYJ)RrK{%ivwdE)OU89K{hYp@ip;DtmY&Hjf7z?7E~g0w -IN4=2nR(flhdi588&jt%KTIkV;QL1^l}cH{t{&kg!$apa!r__*gX?kWZ5zc72c7r?V~OLkzVx9Gh1(Buo4jB9mHV=di@Q#Jz;#wchxr9Ss()%)63;@n&NDs;9hou@ziq>={bqbQVFSrvl~nT)kT?#=CEFvo_!YL -2+oxZ*)O!&n&UokHk}>g#2&7(%(4w*I)_x+hA!&9f$t>M^ftG{{A4PoIG+< -B>4#-?< -rm57g+7D66{N64pQn8*$&1%U_lIwf`DO$n -L8PhjcGw*S|`lKy`QmeeR_dq4C|Gay?$%bMM*?w$$hFz`gP@fgVP<~q`#kQd;ptugg}1$kE6m)l%x6p -C6|Qa;UFsu?57@6J>#@Ig8ho4)&v4*}F7lZlx1F;gkR0|t^lJg0WB*E#Ut(4s~32_qA}o~&7=-(c3)R -gGrB&^)}emY$R?H|5EEcro?jYV(RH?%BH%B&8j^yE%te7&RR4fI~2<_4 -4v{0UbyVNx}(V^kO_nAuchtbadA_$uo4jSp%61=1sM9q&JenZ_^pJtzo0JYjZNd5J|&(Q0+;&HdaY*K -o@Uy7dQ!QxpWNaRGZQha?8#I0Q3IrUKU;2anC+Vw8LCeL7E%8f?dT(K6h;3~XXOu$5Y9c{VPQd7aflD -NctO_Y_6@j}zX>_b{AEeVac3z(2^JF_2LVdW%>=G|#Xg=_6KdXdJZdvpZKJz%QUnD&8$b!4-Qf5~u5XKu`h6uQz4wePuX^jTf-sbAL+bi^Ds_Gmrt9h}(%mY;sdvKC0#u4y;A-2~ -$wqZUWn2xWxr|WBLG`o2MUshw`(q%J?4|NoP0-Ciqjl{yH~hd3T8#X2u*B!+R-D`9M4-bozX7i#FePD -4OMG+x6fDs!-2k@d^EX|gFL&;ZHh~UprvYtTds2mW_I`xI(~*sJ>uIy -=3$t-{NYx?*!nR^nHRky2i))`xy&*<~bK2!GNQyRN_60%I8i1Y^+#mBK1{$%mZm8 -X-vCnbo~@VrgwTfi2j|K0&oK#DIyph!nEjc-=ZRY@@KBy!@um;%r5ip`!cei@chS3Sojr$7NZIcp8E3 -4z5_nFqby~B1r5C=hvvPq!!;&7N1inRvxlU^U!-;n)O7u4R5%QONyUlSalN!9vhzQf@(wX3CQk-n%|i -Ah8C#wVD)MWMd(4AXF(q>W*J_bXKmdKV)NQE5ML1axZUZn=3G_YaQ(&uw?uXfhCksW!-)>b-7s1|Lp& -9n9VVG1?V%B`HMEQ*-EJo?e#Xkak_2#+^0qOSCGmf -?fm4;}by7B{5Np0m0ttC&c~v9An&*Cg!zl@5QLu{wz(`^dIyX{~s@Jb2m@_CAN>?G*8zNJnM(A4h#hs+X4HptGC$~%i2SG_-)~HOXoU|V{Ol-Ihj)DUmspIOKlB~be` -!+wDpL9r=y7;|0o0A|zo8zvwZ}g-a|uc3v}wnzab&cz@Tn&5FL1B}oou}9#!%Ym>?`xopbI~q{GA^DN -YGs)`A>aWx-JR!7>L1e4UG1v9Fh`|sxRU44a|LmQGcTFII3po)%1M$OH;G{l^^Ly6@I#!fBk%a=kb95 -_I!Wm@qqvKeE;q7zI8zZf7J#}+boq`t@%RF-q@&?d&zv)teO4E@@UVP*&gk5~2=WKUh^0H -!xW5ireI^MJHw)#}W3A~E})iZ+Yb->`Kz6C~yr_~FQD?hCj;Z?v%JHtxK&Apbe7&cWLS``D7t~fTDLD{)f2Lvg -=)NK75F&FiwVAbLAj61@`9q^j59<o33r_$S@MEHz{6KPv{@=7R4mT -!f&=_I3HwX10A^}D>~VuQuQB8DAT=7vs!-|88ssMkd+f5zSvoZRqn@tbW``sYp@Fz|5VX?x{Pb)L_smZ~Q5J-+5l5A9doRS -}uAV))1{0V9tbX%Va;X}X$`gi&_ifIp%xO_K>9DA|0U*)HD9{j1O)DZyP@n?Si#-F$F+E*68Z)V|-X5 -oFMzwIl%paeejhgK$#eBv4O5fEUG%*i1rYI6bsbNR}) -%2(mEp{BqH$Mc|Z3lt8Pi!$<27#LkijkcC%1ZrRU)@G@v-e!oa@$f>|GF_>0M49!NUJy;UdcA#)>LaH -;JE%v*LtK+9DEBGrC2mg(!{rVrPVcvbdEAXae^cx=vCsnYz!;e&ljlkgio*aby&nf2U_77v_@ -~=-_p5v$GaYTim7si|OAY_f2@*d -R6~RIsp6gv4i!*o{g0y%LqBoyTGI!@~{%f9XkENG#}Xp&MxRb}-f~*4Je<6x0&=-!K}o4EUwcqhMr7( -`8B4Ws!bJcv;9-JQ1BLEe&u_uSn)Lpk<$1vO&K&6!!5b$dxSJ -NPfR_?y3XIa%j2m2dn|Wzkuk)wG*Ofag;fAgSvz~V~5U0hK8bc{u5}Pa7(~B0MO2hyIVg9Vmf}YZ1N`Bsg|m#a@4~*x)ZMwm|=Li>)^F< -C|@i{l8(co#65ttL@JQcm2s~`!jHC{MFF@?#O`O4ej?M`zLCiO1D&;Gf(vi(N)%bM>D?>Sp8Nys_Y9l -FZ&K?n^K`F#%x_>yI*$0;vyYW%-3{Pk)|){H#bu+RTxgx6;u3aB=zM6KgyGulop%mAb@xlbQz^;bGn| -WD$5h<39X2b6NVrhOMKGp%atJ-olv1V%1ll>Ut4kTT9|YnXZ#90ZBG~SrHr{`WTgqAl9NY-m!(8EG4) -)@tBU6fot(y5LOVul140q9Lav|-8XDX>Ku=)z{_()o7b1x-sS+&qWL7Y?LDLeyad7Z2<>Ni%Cc?dMSV -c9G8InWvt|E3+-Gl_B(mqKlcj3sP-mgr&X%kO=wokQJ9t3eYHYXuohkt`Q -u-(e}X?6L8r5-%Fs%WAl#LAU_#7?z&a46Vr-!A+ -|??P_*|-KNNq5D$Nx!%<6_f>cXelmJc8Xbb~rJ5?_&R|9Vj*>9yf|csQ*f-?5Awo_a!94;Zl$u=e%a^ -^GB-{!n9H^iqi|4FO=?Nz9_Hh~PG&jd6n2>nh(ydw0h#!FWPMi{fmEo${MQkW%YFpicFa$$)20@rW)4 -mOd9jS>rHX8P6}SzVfK4Oc`ykF~rJU#EM2^vKb^b)-!oFS%55rF!@nGV25#}5|ymvJIs1IeI^gj6Mn8iNUANr7Nf`_?3D8i_Nt}`nRz -7eSSc-J2#R2(@%-aGmr<0HC#Xr_`!#}4?HGMD+xzRvR9dG`yLG?K!u*XOnc-}g`F6mUh^ioer48fv#VK9PgFejjw=cTh_U?o -HxN-J>3KaX`@S}G5O<9k{D&5zZ?9MO1A3GoW&SOX0?d$H8uIM6qbo7lKgg=Zl|8@UYDonqT53?Zuj_# -8E66ckVIPamBQa?zWX0up`Txb-mgq;yH2aHr%f(U0$_$c}&#}s^vV$Tr^xh*~;bJ|gLM0e#W26-O$Me -1GZ!@fwquZH`IVnYI~&`t2Lj+bFhEaG_@9s@|%_$6>UvJp?ui0bw226A$2UBJfmfS(qL_KVZF36c*D-yGLQ -?uYEFzXdACv!3`i7x&8Bi-5-aRD@6wJ-Rbjk<@wTj!QassJ3#s^Ol*nspYMe8}q?;z=I>OhDiWR^vF8 -uBB=+zK15y+-CvCU>qtxK16A-m4^YdOAno*24&^}5H`8~eZ(X)Jx81M2g&3-g9yDv?6c6HVW!) -|RT&fjIeox)f#fzBy`@RM%IHw69<}-*88~Y8a>Zh3O1Hr|G8UgQCOu&2mV$hv@uy81iYRB8z3{xUlt-7Y_7X3SoaGu>|ZG?9H0jHZv-FXwL@P7>r=U2LgjP -ZZdlmw&b&Q=|ABPB3eN5;5_{QQNv^wkmR#P6YsaLxi1hcNl;E5j`=~xY&me)Rx_MFWf)0$V?baPM?n@ -ogX_1P()?7g`T}Wro!mMOQ(w7WbDKz#IGKh^~71;OA*!qYa7W-hE!^g -8MYhN={|K0Ssjv7LcrM6;XZ%uyhrlL|4{^Gd`4-r?4tWf-lT*k-si1Z?vlih2x$r#-Nrj=0Xp|RGR0MN@NJw -e&NolmK=jw-#u%p5UT~b?-tB7&+!+6;}y6}`d_!C5OFF?dfE8RV@;qlUE*}ves-5kzY{IK)$(^p**rt9`(`}_OE>k -_)@5UOV*$vGis2{8v?)6EtSNww+6c5Yc=jLp(tG(EUv6^V)y&tI#zZ&#BpQ<+Yk6fVLgIyLVbSg#Yg0 -2;au{0n*8Y(uJHUJR9CU2Bi`m#&xUTR@zy;~4`Pqw^^s<` -RQY)MbBFMMJ@R`8@VBFW2v?9WiGe#VLJ5?_DVzjZm>{&nmEIZh?wqkIy=KCzJEsH5E9!o?A(pr9k7fPF+154zR#kQKtw9f|$EbL8L)0gn!o{KHqCeIQ -PfAJ___j;hEVYVEiuBR(?1pL$LhK5*U+fS}K?;V-%%pZ?$sI=YsRa>{)ue+iEyaAR2}`5wEFqrpV_n{ -n<<`j=9@cJl|_HbBp_a`0LOt-`p$WBG}0+jha0u|Xr$ZE$A4M-rT`+lCqVQ+cJ}f71^7kfi#v>FJ2RW -nWmyzGw%1-=_2O!Z)-He2a{*2dx=&b#1?mcPwLnU9ov~o+Y0jzl^fsjjwzNNBJgyYy*Oy6Ys&1+Wy;! -Cjp-EbiVk{=(V1rYu|$r#a-JsV(q^Eomj}i*(}ykt>QkS#Ta;*5G}po2=9q3_2&M#0Mb}nuoII!U&4{ -aGy}%O5o;q5ZiVaIE>poX>BF6iY&1$8Awl@8oQx;TJv7(3gbzT3>xY#$pd+r-Bh@sTz;&zA>B^u~SLY -hJd_8b4??irGSQ~7Dj^w@U7FA5$u0~G-tz^Bn5{oR_WXv8^A3?^&@e(D`-OkPM&>p|M`PEQWv3uY-PT -$ZLbe|Iuqn!MzUVu!KvnsejapI<}?C)JB&{~MrFAR)l+rNVMGfnnqG$(Y{evkIk0ms`+Hcv8PI7~YOp -ehGcpew1fogK&IvHF^yOjefY7b(2rM>@wSDVjX} -`1IR77euhnLN-;w?LuPQfBO4A`x(VEo(809c)vERc7LgHF9XXW=e1g=6i`{snXs}Tp?FWd}b-_xC_2l -e9lun~Rs%{?xY1H51aW~SUZtM%1LS4Y(Cf`=!Tx~YdxM6RKmn^9=`}NGjD_rx`g3opfm3^G|Rtrr25Z -Ux;8DAZQ+|DVgnS;6P-iPd{HjH0oo$gcnMPI)mo_#v6N9b}$=fb|ofI43e3Jz)it@EkJymkIhjAI-9i --3pFT#(B_H2A*W+X1+S!Yv8gNdph6nySz4)TaG8DH1yGbfwly*y5MaXtIW%E{Fo^4Eebc@bykiRTUWoi8>hh(r@f+`|J21P7W{o8X(k6=>(EXe^~oM?i14#fj?2SvBwU9!CBg&Y1?KXTC(Y6W># -9f(bxScY0;)-nz_-UfBfZ+zd%|t;OBsn>1k1?FZPZ!_xI$?Of*Z=ez~7$f^=Te<#BcVACMYU1*)A7n?FWNHh-5R7ppO!z(Azpt<7{R(9|sVf$=KiA_-$A)8hs?Z0dP -Azqfm&l1NF02bC1X=okva#T_&3FdV& -sG&{Z&1$uWp#-y0#Z;v(@5tCLt_p~VO&}BBQETV#_c2#ukeE`M|A3MmN<ytp& -_ct*8dy+N8flJJ8V*J&gqO3h5|82@E$}p6B2gL7_xbwjrBac)^?6jdCfjKdfDAcZ6zs7^?yIRxLb!{U -cU#l@uc+Y^DxXgUaV=8B?od(_gv#mGlQwVVh)lHwJzxpwtsu0Q_^Mg%EqK}BB=X3bDwmR($`+T%@PhP -wkvJSU1_pn5*64-!XG%t23UdF{QT2DVi~jD3Kd2S`c+#JJ5d_A6*@;zrc>aj|5WLF|IlZIjn?SxcKT# -hI^+V3DIO=Ap-y}}cBP@!Nhf8tr5uqQwQRMJ8_SZRiG(qjJ4+T^3&_XS+Pd|C_Zyfs_(-3vAI)Ax)@z -LEoBI=-}vV&F#k7{{AFzR(_yrJ= -M7{z?LSo9KMX_uPrQMFkS+lKQTO~B*bJQA0m&GzhB*7r@PE;zKiZl2RA(OFDDievFJwnN_G^Xf{HMOB -uZ~DV!+Mj>MkAy8X8hR^Sq#s|fDb>i!2hHx=g?BPD*;~fZjZYqria;@=lXz8vl$i)&wo?iLfWXCc{7$oZ72+ -Fb5K{&0IiaX*H6o5uwAOMvc{N9(!r=8C#BbHS4&J2BymTAg4FGAa@U3g`f-mNJVx$X8zVzk>`OV`|o- -0BaE|*EvLT3$~?M+|#8$4dlk!K-6p%!cf=*;|lDD_=Pkm>B)VA50_8$ILec(E0*)d>n=$V)Ka$=CBK^ -xRp5#RRc>0}T6p-Ist(t{~|(4$S+M#2XqBqWDasO_X8{ar0X+dRzDiC5SIlAz2WLPl$bf6s(%;%YL1t -0E(-RTlgsEUJzBOv6zA|k`Yz(&*6XTk=aq63J>28gl&066SiYC9y`&naeLNY6_ZaR_g^+|>#V>h5MrLOJbqM>++^|HylXF(q2JAitEQ+Qg+ePw5Utokf_ -Uw$nAAuad?kb7SWn*Dv!^9*p_e&1;oZ7!2dr%=kGcFb<<1+~LD#>-R?I|^JRBP}%mT1%>qr0cUW7`(E -Rgx#^1xUgb`7jZC@_IRqn@x>QPrZoxo*ys7whd2IiKi&R0bbwa%Mwy&=NTTzXU!2bVGt*DpI;D=lvb+ -l4+df5PSU0M-p~frviYzYP)ygqp^UT+?=U#%_bu}p{#ip5cS~v8DTp6U*N;D|k&Nj&mBThh9w ->d=$&V@PWa5=476e2cQd)WDkEe|Ivg|=+11-;88d4r_UR%i)^)ic&yZfKC>%Yxfk78Dj1R4U06TOMveEK0Uh$G -sdVy_OHtavG8CdBI0p-#jF7Flh1V0MT;GQ#kEx@eef8T;tK2`Rn|=ue&3j*o+STgHEfwtz-Am<*)cVl -zAVa)IaU`8U6gdQ@-P#UmpHLnwp|O6rymPKp~IIJhXs6@k{6v-Nf)C -Z4KjJ5ljjnUEp7vr?7vczwT2!N07K6k6LhoIIajIN6a|>2wEL);X^@VM^NwwejSQe`GNOh;&_TfJ{kE`sl!Je{SatI{!RY1ScZ -9o6jt^dI&x?wJPA(@(Qe6-Z+A9*Keg@pOQF>nFgD)T8TnhVQ#)FE;!yAsc=uE9{Ga-RbX{njHWAy(hp -Fk{c|YI=@D*>AS@TVeI9+!1gMPS;Ie-4r$|V1|t)Y=`3huyH33vY*F8w7i-pQe3;qk@S_|3tH82&V>K -OmUPz<~an{_|h9pX=%gUyE6etu1!1LD3@u5IC_DS;$*XD6f}_3q5njg;a9Q^!j>H!j<3=alSmyBZ0jnm^(rEewkA -QTnS8T9?*?}QZxu}pwWPyYf=_E8h7dGS>h|pjmGjvI;KQgqB7L6X+DA)0eQ&8astjxR1pC~JU!MBGhJ -!!RTm{B*)Ekgt!=%eeVz`I)Q(=F#VjbnH;yo0wQupdWw8T5Li$KD8$^UNW=1A043)e4rv}OqDdH{CLm -QxaQZKPQov?&jiBZG*9CV3dy@=}V4rJr}a2L9i4C5?x=}3epngZ{{{%sRc*hr9GK|*sL>oVtxCDS&z}1lCguw#haq*)#xD(bGdR~QOn9zoRLhoi2?%+cTk`&Goth_Fe4{f?nP4T&cvY -Vk&y?RL^x@fpJTjY+8lY=1$Np-_JNoPtKFdqQ;A{#hL(3*NbkeZoB4`My;&vrUVdLj`yBfQ$z=%{N--IC*_3H{1{UhfB<6HZdwb9h?O7D)u32?M;{knC_n;Ync(w -YS|*fZpe=ymF*r*JAg{!g7-zz+hJJ0f~dPP&`08?~=4=QHPVVspEJx7q^jE+NJdc^LQY+2~xcLH9Cgf -oyRd9D|q~+plOw=QArYQNi6LC`YpRUPZHfKU0N7fkf_|8${w0I^Qp^u`)z5_XPmOvC}m6f$ooGY!O@u -ON!zYn_GCd+K`QxFrU4z4uKlp(!I`HdsVatg+t|bS0JweJi9N`yJp<%BJ7j1d=KB_!gTj1c*N&|x1KA -Lo0jM7@&AzbW=oG+TesjnPqFWbI-+lU2ckzHdI3>4w4xURB$}r`ptN(_=}dR~-=`|7BGS$;`Osp4q&d -gz#$Z}z4SI}=5xQg@Y)NOG%~Bvh?pXwGoD)PCzcFv$4fOrFdLq$>+|VSI8OnoiOForT+8$Bz;6;*6BE -wir#}bwVOzdVZK_4uwt7byJE~RELX??e8%m8s!NbNpJFnG%LU}t^d!f+2S%;?|>!qw^}H=xHuXXSN=n0I$2AKlea#%kd7pe-+Zu%dw^@b=zA>ZAQxCPZyr@N{naKIo_s -J(~p@8PKYd=19=VJl>UZFxw5DrP3O=AVr>5b^|7g((Cu@DynT@r($+$Sw1O6T+Glj?MTbjE#>-b!5$k -;+uOk+%c`&Wt9o7f+0>E_kS-_QYYZYle3M${tMK-QqA7GycWN#mn$CM!s1(%h9;vDCcR1lI48LL%$?j -pfcne^w2ue&vSn9U>YB!$bM6clIx-+}dpV{EPWtpVP9BF6b{8vcFuYj>dJrk -d_T~ww2#%r2W5TQ(qO&{&+*bMt9_a>o5|52ogdO97A#Zdq^(+$XFb$e(2F)AEyUGB0uxtRCMT0>=+G= -K5}C84*0%5(gz=f^+UnpqgJ-VCn7r%P4uTvA5T8~i0Mc94E+q|<;MU#z9YFG(K8-9$qz_~eC7nxPsif -Ew|&L^IG!Fw_T)2V7<~X-0(}M?u#Z664ieErts{#*gNgKq4jezmANDizgS_$2kep~AMRxArjM`Cq2Rl -KGSNL~n`%k{>9{;Wf(Xus@I!^xdAaYE|#_d!bNN#ru;4fq1I^<5CTPu48q8)%OA>GF|yM -wXm6(h?#YMXI~dc7=#jm#H)*1}CHE;|UT@1{)(U5PD9#neCVVALe{SlR17DD7)0W8VS#;jY&A#fH%Df -32U!aV#xdJPQWG&vt$@9Q9En{f$z`moCUnkNe{2|1e`dZ+yG#)>nnfK6iwS|M0l0CwYYuCxuvxgBQ#&0%-UC{mTc& -q7G{bR)Oxz$p0-IyXT*yHaCk9;@+wH3eXUz4Hgz%N;t7t&vXAhADLC=H*2ZNw;}EgK-)I(N*u>Sd+Q) -Tg3xG*qnc*qQH-gxRhP()Uk6Kz;(2e!jCcd?bPq}1-PJRgR -v|P`R4>ZFp?LsPxE5t$t$6o3@5ZLo!*?P9`5D_|6(%w0iBq)Osi;$YMC*ix2Z#rk5#&zg%>q5Lmcdtq -JhX?WG*ArMn`qp|*94?|k`tkbqU)WPpPb9VXqmaEXDr{ucT~39%OPe^+=dCG+>E^N=*;GrdG$Tf?Fvl -;rjlqJqs9|1d^h9c#qpN6Bm?1_YK78aT?x;PF`xZp_L6Wqy=#|wR!>2^R$_o#5(R9RN%3|_{Yu%QZRs -Yy#@xE$u$>b|530&?%l*y`9H$bH>uPUZAvDEvI*|+Nf|soW2BA-;-6#8kfAwOsOuQ*RpEv_m*&@6?dl -N12fPpT^$B(4+l{!^wI{DGTpw_$7aHF2)-ftPbypK#lzD;NACrQg$qFGA1yUAw$Ls{4tOL05q -)qU+`&UW9FE1k=>(1_g)a|DzneAUZ&Xu@J%?p# -wxm#c9`Y-r)_j4HE)g(z2dd)P0wjk?U!qPuuK_&egA@313*Xbr+P2a7u!t1b1z~?H6#X3V%jNVUnAge -0wc=D=ef1dt;_TlUK2-MzxY4k24_P1#qEC(S>Oc_g(jc%5sIhN6!aK$TWTKdGEYgKdT8Um8oB$-LpDh -X^Jxdcuri)KE1Gzr#YI~zWEWu(I>G>U{S!d}*-C(smnE`s`*`k7;x0k}804T;%rZ9|P6PLl`VRz0R5m -LTD7o*rU*3XPLZ(My!2c&_Jq-BDrb!p{(MHeoIZz}Ls+-SD%xWm#M)Jss(wCc-?DTB0Aq&p{R^VCe3d -sP*?I_c?T=ehX21-7T9MpLnj5F4jF!skRDPpUNu&7_}#C$=Bq0A0Wtvq<0dpK{9spo(dGa;(1c%nri^>tz+G>$-&APZ --+T}sj4om5zzq?5pkGd)j3+2mb%bH=A6e3guf%nT;W?eU-3VebM4DluFn -@zm;t!~fkpE-UP5%Q|`U})e$e&U-9VOZLsM%)NF~342$7~7&9YjoU^iz@?*pVbZBP{t(MD%@7`;h@g( -ofx#I@(4Lw(#tzo08d)N!)=fiX0O=JJd|_`kca<}a~wXHF?S?NQXf_e>X@U=K6p;$FQ-RS -DgIHTp+0m?NqP(bVfaz1jXw&)@P}0s!am8@561 -7e&ic=iPw2UA%zz(ZVE6@du!otlOE5vuA&r|Ttb`eUQuvTWMeL#_ar1^HW@@;@I7_{og0(e_m -kX^SwQN~h&yo`+SZiJPx}F!&yIm!QL*o#bWth6>`kE;C`&2xyh)2Q8puBZy1#NyugH`7v3XF9nP? -6N8C=OuqC#Mcs4)ZZ?B3Qo92IWvFz24N?`mXs@&V2h>fPc`XBT%+o^vRUg9%bdd}!)dv;`w~a!`4tx% -^RbvCuQ-X_b)AOqLxqYQ;yYIn!(L@l)>`=Zmm^1KtFNF*STf4DDw1s6xK)j4uL6}`)6&pIm)6k&N^SA -zBgR(9AGF|ZMQRbuYmO^g^Wd3m^n4hQR#^CYbXHyZU{w!o=Bjxq^p;5GG -L*4VDkvCKPJ+G&ZeWjZIjv%&vJ*#a2x9bk#9DT4F17Pj!$`itC{yX9u%=Gx#XJzTfC;hB~ -dK~|DLgJ54{W>D?z0-a(EmqLEXSBM_f!8rTW(y-(8(Pv~dMvu -MW=;7k%&#VMR{H(mTyF2PbRe^rmn2s{uhokWD)zrbKm}7?|9%#4=jKJKn -B*(BlHdwZjB0o93#UvI{@hsfMcMP4@rYxm-)f`n&h4xF_zT{{E1Z`fuMhH4y^a2@^?<&AIYo5_pHqi9 -5d(^KhQE~|r}kjXsPjkG$7e;xU|OxUlYF7$L|B6|qlxxC;Mkx69!Hq21~RbXlf>nAhFM3LcB`#;c|3X -?GKy?{^ZS0AuV_3Y;9BADRJuH$74u9G6$I>1D0b%va(C@XBab`E$R6imUi99o`zXRo2-oM^THV6Cq)< -uRlv9yP!f2i(R1azsz!Y-yA|92Ws>CwxXrTxsU7Aw81&?*ei~4k{8|E6m{If7+p-enr^Q3p#CQ=eNK^ -GwDk=?upQsYn$?u`nmOFM99N7LDSRd?0e&{${}+)I@OlVfSV -&-o4Q3EBol2!vY^DxY$c-vyx`H@WMw=MB%*02LV_V?yaPNGlv=hvGX!&n~+jlF*%XObDy+Wpq?xxaiy -2N-d_m>R!-_P^t|#NWizz&Er8ZMr#e!<(|L%x5I8-4OTcgR`8V!sS0?9`4++ -3n|#AeKY>L}@M`hg-m=c(Oblc6(gn(n+xA_xui_TXYn~uHWo&x@d%|l2;Cf+5t+TBs=><-+(RpxPk~FQ;jCoV;!w4181p -MqMAt;~D(s)|nJuisZjR*%cZ9yb~s9@&1APPQ?m`aO6a_nj`vpBiUUaEBNc$xsMsMjy?lAt2mvZq(B8 -d;-cfkJ=5Axiv$1l6%-kr)fZs~8l ->=zgH-Ix4!qK|~IqkDuv2!fyy93s&}(i1{S0wQo6BM=NCDH4GobT<{>twcm0=}tWU3C)iN4)jBXkbjZ -Ngg%i|bi_J`$mj0e;_v#eJtEp&M;0A?2Q+bv*&XrTrwFhgjH#pLm4c6h -1fNk7AeByms)?g1eFMHLkJknrM1Tgras?mn|mV0X|ZAjh9Qay*2);iG|N_k9OX5q6X?_B|a=`A_|q5F -9-dQ2C4haxnGy)HY7X&zl!y-I{G5MvBZw(aQ% -#R!TWm?Y>ykDL4@7~tCX0wvEeeJ$IbhMW}WI=V#I{X_1e0xyi@88fCDf+J{7gT;>t@K9Uf}3@yZfZbu -gZsnnY8JMe&|i2NykDR6Rh2>J_`9`ff2-s6{%#nvdUb2epCMLRg_k3#=ozi~sOAlJ@_;!@cM}WMlV-e -&l+Z3$i8WI2jMv~~Vd+Q;lQO%5_rgwNK`f_x{}f3M;#yB;q3V_agFehI)wG6gkIY?)_B$q@j3N$%nP8 -`4yGapB-wBh8{duD{0!%^;Pp-T9ed@ygbpZ&<&tEf<%rYWj*c~fwIbn?1fU(zvwaQ?_4R{Py=yW!mjr -Ms3>h4{&#%Famf%c;SFk{A_jS=Pk?#u6u^A;e1)xg4jfS2?k>yNy$m&HPFID@d57#sJPa2vc@qJTL){ -T^_O$(}+PPwtu$49M;~!dE{*&3xBeG}m~@Srdymf<`+@~MQ?vq=DS^zr(6sf2666Ajyo>P -h5Qx`#HK^Mie9B!gtr2fZnd4aJt63^15l~;TPlwO##$s)Tl^>A=1(t8Y(0k;mZ@ix((!O|6FWa%BjEuX7FaLN0w@&c4U(0XG?;0!`^B-jPu8v66D*cb9}vuY -glSH3*Y6d%;(c2)c{gM6EygK2zT(6m(ziY2mCTD|tEQhmQIK5V(3 -)!(Z=i_F_nhR*dmna}GP(40u#vW{FB`)11{yI>Hz!Q;mE`+M4%sRN?yG_(+Rle7RO{Y#f=O))=Zq!MSz6UoYPA%`cg+l;}KQYk`9y3Fiwsl(A%((8^fNl$tvkS9JRPQk)tsgYIC8HbLr~9(;v^0+L>3Fy38 -_97&oVQ6UpQZu)gT`ektVu(r#mNSI-9VARc?v+iX&L77Z@7Y#a#A_vwaIcj4Kz_fog$Y;q7zj5DxBJN -G@5Y;1rYgq3wYS0K6&O9!RPV-b0{^ThcL#E6e08#>C_lF^2JSzEU8bYTe%2k)O(R@;It$+hFRarX5cdGfof={E3r(J56pj>glg -Yc4P1%qg23>%!Lfb_HlEZz#Rr(Ib|b3cMPfJ=nYPzThRkg7$Zn&*&~dEELGch!HnFKGavKN}&=r)ZR -_`qX_o>g=-bQ~=rPy8DH9keH@N`!19njV0J;Vrs`+F?QfGnA-30Z=)( -pNFYUcv1K^I)JS0JHpTrKU?e*#sNzeiP9&c(G&`7NsY<=}rARmp!FRTV>dTN$fLJnVk5UT=H=FN2 -IqWjhH)SPD7td{Uj8c`tE^-HQ*KqoAX2|4sx~rQX9ss_5T0@j*`C|Zv#%&%u1XluMR_F95h)W&)J$$|kKhG=f%Mt}2`-vJ6mY_sTX8I_N)?$N@ -^7C)_1(CGYUvT&wGeE3kZt{DfeV8;aS{2m)8~iNP4df`mWZNJH>j=BY*Jw+`O#*B*Rm<-EU( -bqe9c)sGjgy^@0179=TLTf#D7;6b1CyRy4ZV@zm6JlR7L;3W_#|^qj4&X+idBK^Z2*;-=4jtuaHx_z#cLSXMbmD@r) -b=RXt}sz8jNpJo|VGu#mRDRL2GS!>W+R449yDQt-F?p?s#&p?xy+Xh-JT9)eMnH9R^VD+$I>9$72-E4 -*rJZ^Lvfu<#D>lRYAKe+5lu&Zq03nu%{ZPbx>Yz3|x;a6TDKwioJql -5J8j{+kpmhd@M_HM0f#*+B?LAF@I`I6qnJ|c~@oFrb_L5lZbizo3x8zE_b=n2FA?+<&8O?;m2)LcXPz -Au07@YE`v;7-6tbugBe$Naj7CwH?a`9Mu -OOzhPdg4oE0zTjn^-+$1(4T1XAn*KiuYQ02i8@f%m!{SoUPaWWcJpbU&B&in=%a%1Nn>T7+3_U*j4fs -Rm4Ae)s6zniP>;$EhR*%e{ptrG!jGT#&E(XPO(T(`C-+E_<;UgW{6nbyq4PYrvd|AtCw-KCQtHT=5%e -(vg%N)nRbAe2t~+?Z2_mHIrYhNBnKVo{Gl(G*AQ(|4BjWlQBOi|^|>+KJe5CB?5uAg^c0$=2zm4 -XXYEgoj4z?mGazaD(beIN2~MsaZ3|t%O2F5(cT(+wOLNwd<$^N>q3q6;Couil3GZ?-x;uoyd-Z;4yo$V)7mB-SNFL>*-A#KSOMJaV9xR=gQ!#A)6cgc|)TocMdj;T^X!wZ&36~x`sM4S`kJBidky|jP|qCQoLo2oJp_6f -*!n!$$C=?^-_eMFFvOsH7DM%_=h1)#RRHG2%Q>|TeLuCd%)Z78RNZlFRLV!;-T -JSP&s&6SWKwSkr3)ldAvx8*H6_h^m$%H{WXYWNKp-=P2~;mnmPmBW3XisEV@+#EW45k$O&wRJtRGFT# -|PVabEr<$!YAf^tJkP>CbVAwRcws}6zp0bymvJ;6B<6f2UDOh+7iw)!yOWNw4J2hpR ->s4i7c_S`_#ajzp<~elL5hHi?zGl5_Pku6O-U_&{G=ujzq(p?6WG|#3+A6!K8KP^!?Ue6?5BEy;wTa! -Ef(3@L2pObHbbwacYQ?Y(jQOJLOHsZ(4#FXj+CAsbkzB-Li1ecZ=c`?@J!me!7T(a0gx|HrTCXaz15- -+_>G$k8JF`LM}Da7SX#4&Aqs!Z{Lb#^iOm;UGZuVl2d5=#`uT~0Ai}sOA6e0A{zx>2Zz{rGf+E^zX%d6trm>;XpdF(UVGbhlPC -w?k$1dd_+`*DjBjO-TCt%$_uD2T(+{Bw#xsFAnG^p&5V|Z{d3$2LJ -FN>$tynTWLFsz`zE>BY4%VQMiU(9}N$kKnw+!xp*e&H?u`L}YH96guwJpZqBKbGvr{#Wv7zZEO_PZ#? -tUGn3Fe$%;$qZE!}6ih%A2JMiGMkxYAArvK$9eiOlgkv!MJ@j<|Ch9<62cAKXR1k$6MYv-Gh&<%Pvd; -+k4uq)uCp!Hu`r5%7_TlE)fzOVN4vNv}$S7gZL9c`52PDg&5AK2vl295wn9cF@Cp7;7`q}~E4xf(174 -ex;M-L_*DmmJ2_E#PkppU`!{e*pQd301A@y}E`jD6(Nk)t$-(Vsu~F~}5sgw+p~(fvm7KkamB4x+n`_ -yv8LXIx9JQ-Fuy9%7IuZomh~{vPE9$2Pz>(&?VN@t($NUwTUlO|!(Ni`=92(Y)I?y!sUmy6iXz1wM*M -$4R%(Nv{B*C(S-+xPHMw6Cd5u-aJNa&McdtI-Im#E%Ockb$s30es1M-wE6mJ$92Is$gh*ppB^dcZ2kT;f{YP@g -U%k5-JKlJE2IR$TwQT~DG5vuw{WZ*$Rk1CypKNr1RiJneS#->K$91zuA0`xy&o&A?2p89{WNjr-ang} -~2onW7VOVeMQ6t@y%YJT3JAf3Fzk!X)Y*HK!!)~7CMnX!=`#_$fS|hWHdR3_ -Vd(DWgX$skmBsfHds&i)oEIWNTh)k;7h}LqEjnMjb@xM7*Er&5Ive;ib@1czH0s;#m|HQ*#CqYKgo}d>B%yQVcsFRH0Ty4&*Hp7fvu#cyrQE?VWW88CuwCd4@?QNS4?)(O>I{kjDS5UQ??uj}lepN8EPi@a4xXCl`3`o3Y -p1z?~uFWX;WX~B1p&1rTqn|7D1CR@Si#4jHgGZ15Epg2sx*zm^(kcGM#s1tW{#J<^$MA!doTgw1qHr7 -~QJ6+3l)`9y$DKHa5afq#?sra+{PcV7UJ!*p6`wE3i1aA#pxI9}eRR+qqqym3IQRSQGdX??6ht2}utS -H0I_7$lkDuKSLeN2uvOBdj`6L9W!{P2u3WW}Glpi?7FBP0m(FjWpV(i@uLSJ4+90epe{<))bcdhYJyE -!r``|3wciuka=;77$OhClNg@NtjWhtnH9(iVT_6wlV-6cgm%PLU8Iut4+2#ZxTxq$?^rk{qM5_xt{x- -oL%O&1beRo(1?%9OB_wfd9lH9-aj_G6etBAs)L1{u76I>>BuI4pE!a57}Sx1im~~nPu#Z{+2>IoGK2c_>fN;<#IS_VOj;C{M`SoSfLg(Bgx^G_gSggyKWJAvT{%59vfUg)njU7|30ZAjJ2bLh^*FEa+bzN|c>_crR#EE-b?>s%-YPWt(q -4^($7yo3T-&xV$F7j&&iXk*gV!Ne;AdH~N-GY9i43fqn6oC)~-+%SpoNk64mWYau!ZnT^P3<^xXdPk1 -r%Ih3dDA0j`)Q%au0yGg~PkFs(66Q2KRR8f7Ris- -8WWwv3#pNaL1&ZH{Zi)3~L{@p<`w)k&F70fJswMOZ$tNOJJ^GASK#V|(%rV*5^fYAkA<}S%Xr0WbV~i+8eEuado -U8{0x%_(~pr2_2%nxk<`=JfkA?It$bzNV`h-%kJIZt-ypRD0gobRPhTaCNsFBw2i*B4bimD*Vd{5gtZ -`!ovK)Gt}ZHKlTxBh0$}%sw4vQDc6aLE(>Mq;IQyuMs+H_q1EDh?W<}Lrr)R7DK9$BO -@|v#NFPUF=r~SqNJrfWl1zU4?OBPd#`4&QoFhnr}xi{xVQ+1b4#Yq`wSW+t_ypU7!B7IlBilGndA|Ry -Gqdjq(rn{h)sd}N1RXGh&Q(-VqcsXS>vn#QZX$ijQl1xQl;-a{Yuu`$IJ1~G6#A_P~Z=Mv`BqwBbEDZ -k4dhkhet<1k$hVv|Ncdcg`#}-{2(rzPI^LqKJR8&>#E0-$W0eq9umyeh)&S-47x(OcHw>fgU1+6 -owKQjD0tFIOuWGBiV(27#s8WBZre6gIYTlhtk7W(ZrEm!}oyT`vcSDXC`@fK@@#BnLX&(eIJp2dcuf< -x^eeZ=tpxWr4K>Fga0}``r_&A2flB2cF|{kZFjr*XIlDbq^FLCu>JoB!BBSW8AU!4iRe*0JvzpsqvpA -982V@f9nlW`(;gimpGkl$Kbp+;mD4}%5<45m>{xR9>ibyFyS)u1>p2&m3Dbb1Nl2>S1rNo4D|q%qD>9e2LBesZ#9{jtgl`EE-YHj5~&zz|!}HgdR=q>W~4ptp4Z+vx;67+|m!oen7ZmfZYJ__e1&nLr|9zwR^GNH@b_)9stmZ#j>33%Hn^k -^eH!&Q>AqdAQnj}#YB1w`)ce{D0lR+>+z!-vlKj}h!=6sSv>Ga_6&yRF{j2%h-W9sM&u><;P+Q5Iro| -dMEy**^j(3iXv3P!e026qo=8VTXlZ<_-yqI%LmYfHyHj@TxJ#DWgbH>sE&qC2|&c -G&NMhau}zP^=>LPniu^U?2Hvdd*#Em{xGa!)9)0C#?&4w1}=7r)kSN-+Xe9GGnMELm3i>G;!ntuQW{d -ND8+7N5~dng)rVZja!~XeLmlm8%oxi}ewi4rGv5g!u{I&Zcwmk=feRmarIqE*#yahGOJW55~f&CN$TM -H0XQ+&UzZzk#okM?LDNCS|QiM*;<(Q?moR*iS?jsniBd_ih{I)L*G@90&D%q?`7(GE&;xn+O~64v>r2 -j#;f5KyCkmK*B+x$hN~_6rc8a$i}nQ0iVZ=nFcF&(E-8Is8BPOoYxQRAgvznH@^MDyr;Arf%xK=u&+( -=Omf}WftAO<5JcD3S_myFlW?sDGyfq0k0A6Z{G8u0)%v?yZ;;Q=<$r|r4lMr5R(SNBO-{F|krE8ZDkMzkuHa1ixvK&Jh>k>oNJh>!H~1@jk$k7q(G85ylY@osn{ -jSd}$pTb^k<4(zh-82ZN8ZXS=yN?K9doUk>=c!YIAS}j;b6}jf!2{el7*uVn37ub($HDGF&$=%$h5?vEk)O-jiIp;o{Seam9?oNROmv1dLVSX7_kiF15mYoPWNESm -{4`84=EEcB{5XzME-ob)pjzWmJ@l<2!7-y1cL$4y^;p`6_b!klfRYru8<@nRlgG7Gu8RgDjad^kQ(GJ -qxnsvgd2zX%oKKY5fpjR>uci2O$48LfSx@al!S`;LNkfJL`^C>QiD6?a` -SBtWt&e{Tq3%65CgVnDRUNx)rfnYaNO%iu5*;q1%nthRWC>(Y`uBfU;02hrzPtJU`v=fJ98`lQg6rwaq8ph@4dS4z&sew-(l410f0-vq -Bw;e<`9h*9iRW}XX{NO^oVEpS)4n>1W0Zl;~oZl6SJ`(3Jc{&0o?#W#vi79=m?#)M -zOzq`ZN=^EaIgW~J%+=LyK70p3tl2l*BpMcK`5A`QR7{iucLynIQ=8B&|lx0G+?m(9DX`cO6@9!_nUB -D4!%C&AdF1;HD)^;KOZvINcZ~yxsc4brFdc>&Gr&9eh%wrBnhXV(m`v&J>2XWXhykUQKi@=}VR(Jx2MypFhD2VT_yIh5btPtswUTQrbPbNxq;zpq4`3%sk1%ffj_a$xQcypT -arQ1VRnq#L`|CxSYGc&5KjdilT=xkhSz2!_G~PT9b|#T6+}+O-OYx$BPL$7CZGZOm!eACM9Le9X`I -6*HJ?e)`(cp2v9`tBYkBxhw>^6O$ONc=9!+Zn6Q#cg2Cv}1-Y$=(JxLG&|>3c$YXrjP((&Q`RU61yIH#Zjj56DQPoRBg**n8Fh-XCjG55`6y10O(QPNoSBx$1{4+~w{alVNS!tzxwgH8X*z -yOgS+@Vtx;b-fn->Gdr~f4JesEqtC^WUNe-N;3X*IWaC6gVXMs2Vn_>8`P!Bk;-ix3?rb3DS%~OR;Ok -`CCq+YMu^dkn@f0u)S-_(P@0}Ma7kQo-(jrfVctrxPcj?24@qHCS@Zh%>EjK}$QW!@bW+gH}zTGwtzKier$Ud7|d&$rQfhQRq%I2UE#pji -j0xinAdeUZr|n#)p{m-h)ePbEoM;VLt%BgGmt59>`b-=8ig}*J4HD)optxWgcNt!^p}C5Ywy4RFtq+LtaF -O&U!QX=*LMD;XVd%`>TmGjf4bH$F!3*~^V^9p4Bz1)Nnj)eLpViJBu>K=3Gc@V0w<~c#~ -PKafj6AxL6Z!zKNPY}yMbXg@kbTIA(2u$m^`W`lPoO@yl)#RP3yJ+iCr5dP`W%Vj;|P%+a56!U-hd?D -0qvjSMD%D0pnla7ki(N^tkv3x50PAIu5!7{k8oo7&4`!hX$!wG)-NFHT;ol;vTEonTjr|U2q%A-v4%GeqT&V-$^?zmx>v`;zY&s9LrOFVQ2kYSn -mgs6ZoV1{4017_!%%VeCzu9o#c?Y+#q%FIX|8@J9iuvHt&d3K1)_V+i-nOw` -&=mG$ZOpdOWcJYDmtQhNQmUEfvb6YZ6h{qJ?}SqVo``0!T&Dx7uIVmjzyxH*}qEvcf3F>+(poIshg`a -)Z{1b3zlT;2uOhQyP~I-!zSxX^w*1fP+EO5OYd}${fjFs&K2t5?MJ@KkXQ5TNI8q#AC -4wBoVcW(c%5`?zR=w;0ugLt!H084NhO{dcpbP)7%i4nllQ5j!k!ijLL9MYs6Y*gk@HD6_D=rA%81J=4 -GtW$?hyXkitw?TKy%mgwvZPXorq+icdXI-Nt9V+qcpra#k`l-;Kq*iZ}l__3Vf*9?+F%1%HL!3{WVuy -orO-aId58U^8-Ko635D@HCs4kI-g9BW}dFa(@NKQSe5}UOZ%RB2T#6LT}Z(U&91%i^2=O$Z8Dg~f;)m`2?yQ* -g58|iaU#I%l2hU07ps@?fOMxT_|=rJDa<{aeEM4O*gu-hHNIa}qFWs$N!X3j-M2rHD|qI7%HCm90Z_NNP|fg#&bF;Z(@hhw|J$Zm -om*v2`NnsYTt+k@2!1vv@2Im>puwA1zVm<#7%mhx_@MIp6^Tvi#Q?C1G}zMt3T;@J;T7GWQqPh9%z~(CrgEcXkg6;zq5|P&oBW;L)H5kyr -FA_C)d*0AC7eoiBUbPVO^pzkKHDJRBkfe9#Aak5DVmhYg!Wc -A{z}Za0#*;LtRe)Ri1Uzjtf0_H2E6k$F@uL~Z<^p>5s)AM3n@$wuU5xx`V?m6$OjcM>lWm6nljd`uY% -Lb>{8hi1s)ydxbRN3btt_BIy~PvGNgR{sbWkHHce$zmo4GgJb`x#31@C!^ysR3kvqj(hcl1IC5TfUfz -7dTiI>FNyu)LhM)6Q=1Q>Q#UyksOkKCz^T84)vPDB{%7W7hBscTz(Vg=^>r1`Ke)W%fy_y=ue3>!NyxCQ;ZAiQ%kqhP1R!}}DhVCVMlv>BY(>cA`BOu>!Fxvd&)Bza=J4)_bih^8CE9tXFz&~ -OKV_zpDFZL8_tHV45QtrXoSb$#eqBKx!GPEifx4P>rot*^m@t*{)!o04;|Wi|hjNKPlTy#s@ZzeE8MH>#(2DH68Qof~M&B7{^UD)5LF;h5T-blF@ZSG;k4X|CGwQoYU2J}jwuJ{F}}ChixSKLdXF=@06@j-z@lLJH -|m4@Mm%koh4*WR5{J8w^k)70b>!zej8uoGCVb$ecO$>9g@B`{746rQ2Q_qEadxkAdmu -`vuCQj+&J|w84zqDh~N$0n=+&c?o6`gy?wQwqhvuuE<7hOXKxT*d?c^V$t1$9;{$IxEmTRw}Kmp`?8c -zgQ2Iy`Xhv%gPu{NNNCMb@ka_1phg-~Mm2pP#ZS|LeCK`h&s#*Nc2w>OXnaclH@YZ~{U}m_jKS -rf?J`K@y`t7>0HoJc=Mlia?2k#rilSl<8CcWO4= -golvN-+@DD0Q9)o&%d!CO10&{=TRthQn|BEJubru+5EpmI|tUjO74K2E@wxhH1e#KSjFa@&*NJ@+wka -9-`_#%$besv@*5z?sTUbJR?FQA~YOp?2Te-`yGTtF8UrodJq)-}(f;r7wQos5RV(I6b5JLGdvVrMk0@ -rkIsP5J<1i2iU;Z8PIjv4<6~*lxY9>5nRIpz8$~;kQI6e;`?Ig!SG=@_2v<&l$|~# -c0{E+Z_IIP!zMa)37*m2n0x)Q2Iz4uBI1UGeypt_z+6vu@Ec+A<1*LXd2xhdxTdbsW(fuiAJRV+Yf~( -)G=mPg^YuPi%5!(8NgbDd6zTr{6B*P?SXS@*3ixb*iP?Go5=wURZGunODNiF!N7Vy -evo)eYl7)s7Ed;Fh@hMCTjA~|g_Ukp#1z}{C$x1v#rvq%0Nb*cDZa5Lw|YCc_pegxUjGtUp0-qEV0<- -Oo=}}p)*A-M#n>j1qqLG?ar%*5`>)Iv^#5P8^@pwfeY5ow*7dif>l9?-n>-RxqZYdP1~Erubk?bwrV>hcMB#rQHUZREsea2s{sLR0dd0J%2antN^Jm~ -rB`r(o1U+Ss*e`f<**zWgOm^NcYLpF;ysM>_+-d7zwvF2)I<8af15A4FA -*&eFPtn5>4$uZP<^&>r^?G3p=VJGD24D~to7uoMLJ!LUeLfe$)61kOie;#TmZx?Q)(@rA#=b<%!a^Au -sm83_vf6ogTa5<1r;YHAH*TT)_tdG -}|0(@g+uMzqRp;qj*8ZMaH*N(TB{Sv03yYMgtZH*{sv=xU|q=xT=oYit|ZI?$YrNT~Sz~`0NDI+M(Sg1)t9kldIOWf~FTEdV$r@mOGunvFCfh=8x##McFWU1K6 -K+34Q5e>rgJRsL^B49p$1-Lt3OTTvocqpgsqOXWx$_+C?mK99v%TMq<>$1cZL-lek69Q-1F -s&1NR!Ly_L^#_}?N)nG#xvFQ-HzN3)Xo{b(G=!`r~B&e^vin>*xB`0gQ8lpg~PD-y$hCkCA<9isYmtRXSrACHXZL8Unz!tfSN=i -%@Ib5d`wjgY?mN2aSM;)b=QE9Yh?)$iA%b791JQR16N@dxlq)g*AOTI4WBo`oaP_AxUBu}RQ-DrCa*OFW3j3xrg_E#j_2$FQ`;`UcBi0z5T4BWWGo(J(j7 -%Xg(sEEdn)HQw5Xg=3OsRLoq^B#c^WpH?~zu+p7iGkx6KJ>;(On27-!yM#Vu -3Mv)@-Da@Mmy}9{rPkin;e>$Gda_pix4z<{ISCSY1^uS??v$j5^mHn|FFsL3%t8ZQ|ne=^j05pDBcQX -5-$#Mh1TLUsvGqrs#UY7x6@SrZ)@urr -d`44Hj9oKa$SHhg#2@sIexJ9@UE}#Z*e^`$h+wr+#_LAjRj>(9`GbrZN`w1LCjp5}J;1C#Bjr_i$p~A -}XNq}65Zwvc93FZ6Y$lMZJ^0xUjx}Dq8c;w^$P!+e!=pt80FG15Vbb<#)I$UoCkz|qu&8riAbtLr3{1 -`)ttbdh+p0`V^Lbh@NiawMi5U*>PN6(`9=1*ST-=DbC})L(@B -lrsuWQYR7HhJ=bT!{%O5fx)0uX6AgkV*Pr1z&XHErn6T^tTwE-~rxz5wMX2w?zKPrDD(%8j~W?Y219CrRWHUh>xe|)Z$kdOAL#9`6qKAr4`nE8C~4gb -@Gae&z9VMJeKzR3x2>@|LDX|RQ2a4eTS(Km?B9S0uhu#aRh~tHPnPxn6-_NBq*FjC=~u_FI$LgoM&Rs -8?D$V*%M`$rb<;`v?mR-2Sd{@_GSck89V9ttYO~Yu&^~q6W7FU4m -_8m>~J5n#MYt32a8Me=Mt~%W(mxXg;~4p8y=;cU(4U0gv#~;yu)58$tAdKLy7JF3?Twu`6b5{Zeb -h4@ruxvaXAiEvX`0<9UVW)F=vz?%R?HP#mD2K2XwWC%yMrYo|I-}-KW^#o?C77gqBR!NBv$Ry<%2YcH -7H~-Nh#i(=t;%B*YM_NtT7Fn#tP9eai!>E{=ekwOTjT|%ABhS((rcXc)N7G+BC-cyfE9W@t69Nb$U1J -}S&<8feOt5`L1CjE>8hL_DpGZ_R43glQ=gVEj4st54#O4XARx3`{urU)T$oYJDxqtxzn2Q(X~TsT6=Y -rim_b%T^;#FUVShH_B6X(AQVjb2uH6d21dq%M?X;LlTF{lHplKN+n!#Jshyrm6Y6d+y=>w8HyFz@%S; -pb@aM<{LMlkEeUVza{v2yN8woqa17%gaYr%AZM4E&OrulM1;)NAA%^U;YoFwm>&g5@SE%d95$+0aWFu -)8~Tm`E@6I%2wAS<^e7%?U;D+ac%bSLzkSMb-UVVl2|xEWyKlXbO9dz(G+p&zFJtwAhZ9vR4pQT;T#< -)7Z$y)3e3bfY{Un`KmGn*P6soCP(ZZyTt2)GsHaDOg3>R6NnD(I6k>vnfevj(b-#!JCe@0q;lvK^tI} -lf~S(msXjZSrh1p*tE*IadWaZ;L-G-*h}o>iI^rJ;nzZamh~s%HK*p!yJq*GS(FShfD;iW(;<%iqXaT -8;eZEo?zm=y8L)Wpva*Y~YxmhMmLbz<6ac$|iVK18*x6q?$2f1+tj!^54sz>j-R-c -FwH-Oxr>pv2b5YSa|ze!V{+9BWxkv}mCmF9mk6AX(|F=>*_yf!AKIS8Bp$DwVT3YrgdLE71`QSteF;V -bbQJ6%kQf8DBvc4?J|2l9-gkn;fPdxZFmCke@+^Ke8%*g#Zm5wo?#!t}WX{)Wl~W>krSSZAKpj~14jCc`0zI#&{Iw>$VS0eTucH)cy)+uw8G2H#>GkmP(D=&qe(7FM!o`2&*DmkxkUMuYZWG{@%HNgjc^f@2?RSL=h{v#Yq&Q5Co^lpCK -%S*mGLg#>-9j6gRc&Gj80BaBB^N_Gzb4yzw-Ck<(gVzU7jj4a#gYy~$lu1>Fmc=_W?7$vG%V0u;1&l3`mBG2Z8buLmJ;%bSt-p1}(Cd0)|gh`o?NVH?*uM -z;;Eh>Y6g5dIQj`CASj#D9vg7SJdE1YxQF5Mi~G{5h6k7QRPV*2ykHu_`4&tvuv5M#Z0@tO!yTW!_L0 -u)(bF4wj7iPj&_TY+HYKR~yp$mF)13NDG!8Ci&ot5$EFT3?PiV)7%~)=g4v-csyMKS9(Q*Ey?;@7GlpC3{}LQuOAyPY2^76-p8yCNAE?4{VE$4O|~3L_rXG6U3ndEaT+gz*P&IiZre9*4uvRxUHYLOeAu?Dx>4UnqX<*JeH;tUdiuq2pk8W4a+;c+b0)C7tNwmJ4vzr>Md7(bj|*(g`fXhaa020Ev-csJ^Y -pZWDzG#zNSWWJ!KLZwve-&Q;ub=Oqt!rAW4*v0IbaG;a|GY)W$h#)^XIGQz{XF3M^he!+fRS^h9P -Fd3zjovq5a7fgWySNk}W&IWq#KoMFVhgq5MkDH=2=FHb$Xty9h@@?Fau83Mc0Us1uCe!T$xAjI;*|d^Up -KW_<5+f1dg7y(dG&q7OShip^xLVQb~u7z<%GR%THpSH6M~QsmyS6cW!szXp^8$4Nn1a^Zu>)pz)0oU`J*&nF^ -Czz)itH`pxc``ogo@FqnBHSZ|_weIk?a~co>J$@Xki@u~$AUv+iVPyT2ne>e7Q^L&>3o?SR_-&tA4K~ -==n)~AuJr;P$k`|Ya0BI#q>P{V_z1t?doo@^WM)xlAwUor_K}Vd53Q~}^pAPc61bt7o2Rb=Ke;aA>P5 -;qFwWFM0&};wKheqp{*Zx17;L?9>%m^b%e%n-+l8-0;t4aD}m1IHnzxw1=cgO!L`oDjJL6G|G`!oO7Z -|RDwf{*2wB>rvalHb~|`}&?wjIM}~LNNGes1OaeGGRpRQzr0D!5fS=00kyHB15+M -U>nyf*vgF|{8Lm&?q|TnE>pV;Y$L&j*l>L39LIYJbst*4qO>sFR$U<-v7)+Pi4ma4&Vt+7B`aLrGK1- -MGPX}{1>WUdRt-oL=fNVm2e}xM9>xK&FuT5sWQE7 -_&2hFH%Zz>zh@9DF^#-9~R+WMRu###0de?Y2E;;X@%(f7Jo{V8B~g4D`4z6dcn7zlh&V6nk>^)VFf)y -z+Qyp=~?Wg}}KpC>A8UdjeND;$kU{O=b5zFZ}{7)%`^QnrS>U?5U+;65s!WuOSQvJl3)WTS;}AIOk?E -05cH_3h$Yt@RV>RZ(+W-Ah$^?(Gr6Xr_N%?1o7!`1{xl?mnptjjT{~22|4mKhl8oru4TRg!W_QA8**t -?icV+ZrIQ67w}JR*w5}4@K0{o&+ZrSXE$t{*zs2bJAjXg9W0(-{29hjB{;}FZqm#H37^WVu=U&P4A+9 -@oG#bn7_do5EuQYl43~_`23pzyCQFkwz|9J7}Y>H1FI5;()0KJQ=%a7?_6h=J;Edk=t)fEF`5T++Siq%!E6?)ny-n=+ePG8Q)ti{| -SK7PZX|dm)QQ_ktLyTz>5rKWOLJ3aq%(;FpvMJ<>+mpv)XsZ2?NuQCaHc -@%MKJ98Yksza5lSfAENS-VH1Y#t$Ae=)X2$0f;x;<9I%_M$cOBq(Z#N>hp`cqOaBp8i!p*6#?w=x -eB5C}eF#355Jh(NfuRsI^H&8)^Unayu@b+M1nL=BtDkZk!A{lHv(2YI6UW)hyIsxJBa^QxC_PiOobu* -#ZrlN^OTfq<~HaJD>-AwpCfGZ|8TKxLd`bNTC_7d9pwB$Y|b$w-mZN&O@f9YS&+F}nIUB3M)m88;ga7 -m2&o~p7`_I5D;*vvF8A5IteGq3x1oG$QZUiZ(PE1XVS$h7^ -*p4M)}mJJdQd?=kF6+WJ2qt@`4e4YZxZN@^|>=$At7u&(rA|tUE)D8vgw?CYHL!a?^s#9;*?AM_2v#? -y@HDi{H}ah=q~O?6N47TT*1?)DCa`A-h9vYPXB`W!$r2Y18fRT(i-x{Vm?N7h5L@4tuLx1ks89&ePjB -*KU=Hh``4@gPTt*=kORkmc(Abkqy~xqx)_*j#9rR!6qA9s%po&>aPT)$KVuBc>NE?0h9#PjbaRebJhT -J?trs1K?9|Vs;C#J>g;SkSqGZ$bG}E1vwpTxdyiOE7|7yLN9$;p@X#)<0tjPDQH#I+$25)qQfDaCvVk -L26V)Y3%F;1Kt_if*}XW>D7N+oIm$<+y97DAEaFC3$P(SHE!9HUEIV((X6CU@ykIov~Io)#}uymdbR( -0{Ct}+XlC++X -K-rlJlDg{XUFk3#s>ci)%;`cyEMHU(o;;gl^f -eDCpKd4B^+x!U5-gXJH$_*Xjw{%VVVwL@Ur;@?-M<6PGnRh}bL#%^rJ0ui9oiLPb`O}HWW9qin!5Zt8P6FzBN(R+!r^eG@O-|58N|A6&Q@kmh|=kJ#Nh{+!Pt@7DvvKG|AzKu7p8p|<|*3p(#?9rSNhPL*_dOAcTr-&bHxd#F13NHeC;bzNz -`kL9`sB@NhX%j>&>oT=l+>n*1QJ(bwp(i1%lTx?=w3aTrMoL5#ynJ1R`hs(B>>QS^Swmq|{#o^oV&v` -8h)dR;Crbq662?y_=7k-?A -C*#fLVg7?E$=5;$K(nqm)Z;5B$0RFj=bAG710MKjx+aa$Ux$J@j;CT?uw)XkWr2|UKv@M0j-6X*p$R1 -AvA5n=9WAJ55_PmRm-VS!;gDDJQ{!J%F6e#os_A+lz0-7nnI{Tm{n5G}%;)+m14?b_t(u)5boexf5e( -i9g&(Z|Wvyr3FDKm$k@=(xIak|S>|HcmZw}dNcNhX?R!9W!M%L2D<7mMi{jge4sD1LN*Z0>0Ys8vjSh -y2;Y?Hy@g`PdAh2t%<>sxhxLeIzN3|z5Owq#&PE_Cn;$-=#j<~Un|nTUqi9Q3r2?QtC3?i -&H8I;;ImR#1I9%rB3BN(j>6wObrS@lA##5uQ2*#svl2H1ii -S}@YCfmbG-$Gp*_p8$(_U3(DQ!7E*Fca2`9p?$C -$RQ>Et1Y(Fd?596?qiG`zw(%3|dRKDU^6HA=TJ?-x3bz|(T@IvnYSzYV`M$uRlf9LY%JWd44B4)f90^ -!eNxvwyr;Xon{LfKq%Dx3BIGa>xH;i+;khzgY15F`)#3li2$EfN+c?Q3yq07{Vx+ASoOtuoch|82(cd -O0Z9-2-hGU+9qLLk -V#Tz&bcF>jliiCpisw5z}6)NyOB2S4eh>!NBR2oS&cd6UMzOUtQ5zPoqk$=Si`6v(UZabuJ_C4%E<;R)3^F+4WkQ%r9 -WFceRDl-^Hv7jY6=SL+o!;Wus$9)Ne`dnTZQ#LnHgs%%4AESSOZ)gAL#qV8M>fVLLztv5NWI2C~| -1m$0tr%|+0!0Mh!1rBPs-)b$1ay0t9lM6DRt9WiiXlxB{TW_Y*Y&t{GZ+yF5qt2(&p>v2OwAE&-@l2+ -*K1OrCwyJ-a}*HpJBc`EzjSCZzwFTZVGJJ3BnWWg+q>KcZWq0Z)7Vp*cJQcB!fGCE21bYzN~$WUU*48u&l00q8zcN}_*C>avyih(}~7!W_13w`-Q!1y3vcz>k10zV^P -sQOV-Yc)Zi*Y05UwGd%9YtU|W32x2DXL<4%h89`%Z85+Ht%LscY&@P(4rnsho~Wk}MCdMby?8kxFR~i -GvK)x)!c7Ad5oGd!9g_fU5IFJ+pOGJ2d@4#5tnzE4DW-2`2lvGHxu;o3mO@YYbeSX!$c)v^za399xr2 -PB=~6xA^YM|)R|MsGk7%2mz29Qc)?bMw#;V_o&GCr%)5U39^rZp*XkB#@Ze%DXJkWnW3(SD8W{)~>FZ -&E?Twb|^Kg%MSp(hs7JwFsDKS`4DJm{xW3Itc7G9zpSqLk?ebwyr8mpRe)Ez>+mm5=tt$ea^ogw(@1* -B1*`hqL045y@Uy5SAi9jUXks*_QZVJ!I$-mwX^^a?q}p`e@D%4`g)$dG~bHpwP=ZoyrcT~Ln3-A5V;GWH19U-=l)?5F=yE;TdY|a_k^Y --ihDRjFewrekJI%3Jz9|NM>G^aS({C2bv3;xnUMq53;dVV@c>)JkI=J2+`@?!S~4R)u~>k=<_Xwd)g) -dkk??qvO-o^SEfH`(o0If4D`Igc@h`OI)1g=eh(0C{wQqvC+?V2==qb4gGJ-9X=42=&?b%Ry?OTmIQ9 -7{df9Yj+-B#=h=8&b&{6y6B6te9XW5JotG`5q_M1$0*;pEtzAe3lP=9bKe19;cHB@d0TvAW+t^gtUeD -}w`*SR{Gi;jNyt8v9+c_a1<|mj46yFh)!rMV|6c9+OX(RIIZS`@qYCI3SM!p@nO{Z@XH(H#pHPB$bc{ -}y1%cbVn`$f3v#Vz*P*s-LQGbeOBme`nCWhVwkgK6XxhivKnw*Yi0Jf-lFJDm?o{np9!VLwKW|uoVTF -+RV4&X#y7=OCP2s1?2P@4cGqmw6hI}Ol>L&s&Xo9?8{^r^#XTxK6()8WAhi$e@xj83gUX1ZRU -U2H!i$bw@!dl?4S*9PdF1lZ{tc$R2&DvE^5eRU0%I05s_nV9b<6>!Ctp1BDvt781V_Waj#z_+9}uxGW -ut3B(3@T_F<;$D`ky;^>ybdPzA;mb7t1TX@hwddZwqBQmPoi_yIf$~f+y6x@=t$>GT{W`7wQE!fT?bH -z_q9R0^vV=Zpf7+xr89tegHxxWZZHj3}_#V!^No(9u3#%tH5wFv2f!jvbXH=qiPriV!A(fwGj@6n7H1 -6FejtRkWjy|EU-kgrm!d(0ZhZF5}$|{?>o(u9Op{f?^(RIYeThAa5o;DA2&0c}_KBKSbnBVE6`pRA>* -BuZ`y;xrQvW0kb8#KPz+EAf~@tg;AC2=WHcBrCCW`32#8HhoQdw6-8*GYa&0?-vj_}Y($elCSJONKsr -$)(X61eW$w$=+w`h|xJMdM`T->e5gDp!U2a&4>EyY``z#kz4q --`6R0;mZWcgGm|K9+N#Zpq}#pG7Seq=^>60x|2JOeYkL3J*Z3~mzrs?A+JrYYJxhc{Kx*q_AaHc`o)` -pU6i)rT7W^RYR?>ZXW>!YpMK*7I~PTPgj(!C-a2irp1gv4k&6W=A!Q+yktzD+XR9Wb(ozA3s-M1;54G7R0}(%xuvv0}&h7h_4A8d)b^JuzADtvbwC#BJq7s%0r>SaDQ50G(Tj9fG@FSxGO!L>|PQ+YMom-)2wY -UhN(H2cNL_0J;CD^Y1)O1I2u;CYymXWf}K2D?H&!PTit;3CXG`tb%m~qB+@@b$GXXV69wmr57?`7U)? -Ae&Wz$RtMQPE2hw|Ao~2s6l=@M&-bOJ$n@N*zvQ%S -}Fk+{;Y0fxutTWl2B458hI`(pbAA@=G+1G&wjPaNrUs{S&b%LI=l92sZ{PvzOIrrLozUS0ij3Cta>Rh -l7hMwJJLhr-LOE{)opc-f)1Qaz~nmy3H8MUu87ON!aI5@}BjK(B3?7>+u*CrrPyfN`R4ie4gEDbwKngeLQO@tB?m~VA{_ -c0~A4}PfRyWHE%ExTvOLeBcCo62xLbE@k`*WzT2NI>B4+a*gDz#IvkjUN+%mAF0lJxtiHK`qsVwe0zf -4hc`7@cIkPi$%7NN`OU$QM{jQ)x5Y-pba31GtIdN7j5Y3LvcO#FP0ha68*TA>sz3QH+2@N!O`LrnU_n -W=g_DyIg<^j0`(Bq0LVS!rQ&zUl78>#r8v>40#2SGf}DhR-E18P#_*tJ -m+LLsux)UX$w+3!ukMjk4RV=UQ5&{XHM_zijfr*g)jxUDhlQc#&k3R53zS-Z<-^p$E41oV6a0?HJ_7AMe#hLhyal)bQ?-N!qvk6O(KyvGYVbmLWj5=VmgSa6)dEtLY^Nqc# -gM51+j5ODWsntG&Bug{i)Dr0brOIQw{ASZV!u(#^el0@veE)+4qk&53O~e@4AWdp@4-9AWaNZ0Gl5;q -r9!R9NRRMEQHrsCHqY1C=lb4wYLVBmz8?R4yiA`JN|vQ}w7&LIPT#4wgW=MeUAF5YG7*YBQNbc9_CZ>P$x0?|aGnjWtJrlCh3 -J`FI{_Ib*wuLnGfbR|rfoJV;|-iRP$=jHuYF)pCct!bdQq%QCKWqrD+cWzcHUlASg@FI$%nj)M*Ds;w|EWEE!q9(t{cj;E_%rmmCR0D;M6Djy|pwZJiSvdotbhRj|E1Vr$Gzwp0!rY+5DoJ|_~}v -qwn!ALK6!7b3e!s~@#b^vTe_=#hB*K>5Ex(i`BG?ZqE5;>>*oiK0D3i@+#v1>J8M~ -7wvv~yh*SDl5nmf*v_L`Z8tLEimDzfG~3Z;G=Ct_%lC@m@73!ZzDldBU|xmdy9!qbx5CrX+d5;N_3@@ -d$1*DvQY+6rm%+xoYgBNBb>4p!Pygn&9fL= -=-`CR`Bg4E?TuVTgy3yml@JX9t4?!ORV#)XGiwpl~2ua%a^b8-2uPSx}QKGfEtG;mB~iEbdS^@;ALtg -ELP;Blvlp1%g9NITm_=L+hX;tp_l@94J5pE0)jn*I199;8w*!GJrZXG@}lusW7L^Vn|IgM*x(-gDK25 -&QW5jv`)1`O5801OILZZFlETskD0U%XmoZGFKu2?W6`12u;q?|MWG0+2#TAZCad08k<9(HiZtNwl$6l -3}R&{Ca#Q|BGxicogZY-AY$qG@gYw+eF=_w?LdeEP-N}F8fgoI9-*@}UMqGAZ(TYslzsrWeHVg5xn87 -=3rLbOqGc_jM7B^ew;vV%075ySDWz6__s(I;);mKtC^i<1?V9^pg2AAq8u^r1vmH3YUX9(UejvhlHLwh;45X$o>S%0)Lk?^KE@ -9y4lUFl;_d4V<76+GsI_1BK+N^yI*o<=#xW)_U-b_8Shq_v)4zCT7eSa_I5W*3DvWhNR}rw)gEl`D~= -t{ImgHuTnh&d%Do7_&F@1ll>Pi53-9=DYNfLk2~bdRvUBZ#RYC-xJJ|$>$gUI?w~jF9a8J`Z+xTe;CF -5j@E2@o;GJ3xX?fX~-$Fc1J>T*0&mK82#rpsj_CxHf{Q`O~OU>?!hIU@yA!fAPHZdTdf)gU>`6c2&)@ -i4~+*amzoy?2rG8sWCYRl`E>dh2z!3p#QhT2wEwVGd_eG!u~Il)_SPhW@HvH!*lo@(Pm!pd`gk?Cgew -Ne~<#i-#_w@M7Bs?>!G%XAmbDB#zlDZQsTu4ZMB5kJmx9pX$5r;wA@Ns#r?l4myGHGU1sxy~{8TMsLP -0YQ^O_mqp?U>_z^{+hYq0F4xO4DqtLOdo*CB0~LrMT;<2p*?z9^8YjZ#nKE9{4SOECju6gCiPJ28sihRDp&hbXx*B_Z|DP>eYxI+bhhaY|~TahpS{0_Z;t6F*r+vUp6NhNQgp&U-Gp!( -U_k=gHH(SdE_2~>#>-|rO$#+99R^5o_*ZUk@~W)hLgjm71X-EBZI(LQ_siI>huAeU!j*bFP!|}0%bo{ -&;L)u+5hQ%{2NI7!yf(;){@XZJQCfM+>>zQL#NorxL%Ph7;n9^E1=!v@8At6Ch1RMEtPI9xCFFy`cnJ -EAZi~yLB#vaM`Bw(j5gr8Vq~1$@MainC>Q?~tVKWiZ?Jvp&nC~e5vs9ZYwcZeID~CU*{$6;+6s-E+Iz -I&+mPA|p23E&acs-9hS2unP3k?`lD^w}A^(FQf7xI&ZLs$K{1(=J-%ztcxaUtGE%(vx_wOO?hTwpI3g -K=X!+s(-ZAEavzlF3nOXb!bj6jIoJ~FObkIYBY45V<;3T-#iPV`pxo*8{hwr-s{pU0*wX!lws@H?3&H -WYoQer|ct6F>P(EUqB4QNG!ByKnp3T?7AQ+j~Zq{XY2$D3(6ypV`-?mwe~lZFO94W*83^?!E(UkMpqJ -dSdLwcZAu4v&?X?WnCBq;ytB{En6RqK7X)-bdcY>a@0TRi`m;^&Tw&An+o8yKaxJ7+Q#?T)Y$L -X^T|LM^AJ&iT{T`kO=Jr%s;8)tae@lAzq89**%Kn+=wdOM3xe-1duqeA@_vatxa(wNJNj0IC(DwN*Zb -9+@D#N73B8il8T!SD7e#ZjJ1T(rob9UoLL&_3>>-(b -J@fZCit$D3uIU5IF6q2gUqp4t~Ce__Wdd^7yXp7Vfr9o4 -N?hU|lXKLo^rt;z=Pot?qXx7;dm$gYvUi+e%A){CmGfQIXytxh^dA2;|*fIgdtaY)2Gc^b`MEpq&=UmpunX|jiE1l&LSWuh^VJX^e(-JtLD4 -ysYK#PE{>!93`dVP=Q2d|a88yy-?jkSRE{?|K#v0B9=Ya4#XmZPJb?kNn8z^?hWD)_(`qqxS -3hnwIZr#KDJ@g$t%|btV&qfgX_B#Vvrwj^+)E$=b?F>yV+ZJ1K{#GJsa)mJ$Q@eLI3&p(c|$Nw*#%OP -V~ihXIKn*UeYg}+cp7RM{F -cb8UQ+FB8qk|O;>Q#|?s=9a}iMx2luztAL1zEpiT1Ryv`G2WuktxNQtr|>H?7HL?e(u -z#V-f`bIQQU!ctNcBa$6?SJn&=TwAq#WF%4d@O0sF}pDklhgV4gwjNTE}+Hj3*pfq_z6_tj(U%v)W3L -a~yNysq-yibDkNgH&7!mTbDtOj_Uh-2`OmX5|>q}~5&lmzNC$+g*zZDVlW&;U~p(ebeDt~!z0x_}11z -pZB^uDe2qXvqMV)sWi!~D2ty&+D+jvDwy8Zn8it{&}5UEZt}w<#T+&PLEQ>e=wcC!1V?xesQb6IVfp{ -N~Pe5-R$B)KiJyS+wAudi|!o&>`*5M$?>5Nq}WG2Xl%c!)UfU92ZA93Fs}dLfnB^MyR%)0$x_@a_wsq;L3WBd~#K0doAkqtCo3z^n>)P>W! -sA*vM0YZ$tI+o~MQ+m(y*f{YgFv?V#q<$$`P@-Q=wPJ|WJjd)8-sqwv;-i?jcOgnrt!=MSp<5+52i?X3d;eIGOv~=;)0+K -lZSB!D@v|BQ4Ogs2J+^*1g4vw0hw2a-R2*sW#nS*EH)|&nb9cL|Yw|6&@_X6F6KMX?l+lIf2>|=0jC4x}U}oA4es*^4_d}#-vT%Y`iE} -NAC`UzR+B%Ko3h~K_UUNveV3_A)J&!0F9l`M9#sHNmOtmGlVg0(^bYU>;RVb*+k#*Dp%mkF=#qK;1gJ6xYG5`U(^p`M2y=Q$4Egv*yenm-XrHIhtwJGqZdG(2v9|>mOJDA -prg7S3fQe{OhEDfPj>CNyS#pD*ObjtTxW7M~e*S6zi-?lb+*Oy;_GOHC^=TqCZQ)#%#b-MqU-qE!awc#zbmGZX3d9 -a>TIE~g1iw98*fz9(85TFo{u=FnoQv#|uXh(FGw1;*i4~e<#Z0+@qn`)W-SM5bn$KGe<6JXAl$$KV+A -ZeNF@}9rXoa8s6p~B0Y9omQq~zs}a{xcu^4(oW)ryOo#TI--r8swAP2#UA2lj7GG<5n(#;K;)oS+Yd{@4gN;M$uNyV-}kp2dw}Y(k -@GT?NAZkc4_ldm3sy0#+x -&|tn@GATpa{5fJF4d|*h*pyR6Sd>I=h$sjp+`idX;+#95)DzVe#y4aQF;vBGH`noO&PhDS%QP&34A*R -rnZjcp0wrD-^U%*?v-Q{tB&FoV}tawRpVGXQ1Z!vcl)Q*@0!2XxsHUYNu4pJ7X!BfVz0ZGphI#&6qcW -l-rze18}UAd+=HDSs?&M -FRRP7;ARDn_I8Y)9yO8bEEo3B=3y?><*G=>6i$vPIdz##XmsrfMhD|v>+3Lhgr_2N(Nogyyi#t-~jw* -_*As!)^6*xrBk7_FkP3a#-@+Q=#>Be;uIHWr33aRBeri52S1LRhj#J9y>=iYh{`!SD&_*o3v~Yu^v~zRMtOQo@-wx$75ow+&8?f$ql2Q|*nsidX!wz40s58iy9sZWQw^8v+aej -Q!mDnc3gK?N*zR5Qk8t8U4SrHRi`XMlX4FeJBYihZbMpes?jrSc;G1Ukgn2c3VkS#<3uiM?f<3#TytBd1S2R+A}9pm7>-gXv74x$ipOH;r?DmZ)sK%{P!b>QGDz}il -p%hlsG}x;emLnv>era~ImKx7A@8J*#Qp)c;!pDbL$d`XNAjPD5Bjd;BV`zdN9O9F(~5tE|E$J9#YeMC -{J8-vIk?2A5476h)DF}955H)L9+%ksFP87pyz}hcie7mEsH3lR79Yj00fpK1Y$UR1~kV<`h8a(_+fAQ1T>Q7|Re^E*lz7Gxt_P8FP8}j5KDWo*H{>ICKv~>l^mnput^DM -Sk;CfYu2~qC$|^5_mLg?EcJWbd2$+NF&!HUZ+Mv+StC@lgVRAO^<9qL|PYCn}sWSmo&5xrhT-7c_ce72yK)v6p!rI^J5Ruxvr%vt4U=F=wtTc3&<7xBBcouD|K6$cul{j(2a>5In -6%kxQ)Ar?ARAq!U}JA-`~bwdYHE%iF*umaF1A39ZiB3g@ExBS|d3>MIDQh7?j5{9v@^C%`H(ivYxiFQ -v0$Da1j-X*=vpuQRvP^7!5kb9|1p}&;u`xyiPjxpTtFo0yZn`A+xGOML*OdMZ@3n>|ow2^)mF965X8D -nrRRYbnwahWjeUh+B16+PV=j^%*XBJ1eC=SJIwP^wwF97{?dFK3N@N -Q}6tt(}d#i@9VRa1NTZ&az5hNSQzoEv6*mCM&6vtpq=mVidpl>+Ef!Di>#d^#LH%-Zj2~)Vu3jauXY9c&M}jqG3v -$}1gyiKkJ}#i-i`NL!2i1r1^Jr}1^F8e1^J0Xp%99~X#&M@n1U%3+uaL9;1rGE5VX6S{dW{We;TnLZU -R55&0%~5*4Rhs_Rs@?j_L&-{z`_&cn$J#I>gT%3ho~Pb(|bk=7{{-j){K?6y)b6DmZw?jtT~T=!=BOk -p#ek-7Wl?8hP*$S3y2S>+oH?VuXJ*215Vz_N6qip=$vH!F~DgH^#0(sUe65e0 -S6-b@Ztm5^$B4cWqI`w!$!bxGIB7(e&(2H_9!-&ehC20>w6(Pus+xbb4S3)hYml$}!6FLYUGt>%vY(Wg~2^@T)UJu1?@M -z6g6x==UrN^qD*_W)u!kLkoF83<9zxe}1#6qBNC6KJ)Yw0D#!93iSCg}x{_8mJp6riov8rkO|C6Rcuj -J8`+~z?AH^Z-spD8)i(`6Qo%tJAg11yx9?4e2lD^%kp|tiha8KLti?C|9+pHzyv6rgto@7F@bDHp~_I -CMpVjcl{S3T-BTR+&`2MNY>_&docufFZH92W--EF`KM1b?Dr+r4n0!l5laP3N@FdG#x1@D<|9Z`Wkhx -@!eAjm?wmFE9rL>f>o1Ka8{b -SO!YtXRX2@VMWbgZ?3@PyQ%#vMy7JSl2wMkyJhJ*rLh9?yeJkFGY8S*O4lx%=$cm(GRkM`k@k34kVeN --F9E@|l`ocn&F -ECh`{j5kyW-xQ3JE0$^H0jjRq%bX6nQMm%rdDq5lHT`PJgKXb0vWNLo`L#nD;c@IorWr?G#T@r;NUU% -SyYkdo*?;WgNTt1&SMm7b0;)|#3xuidi8sUR$fHGXdIJSjGaxGipsd(P;3%1TBKu(LQ)-hBseS3$Ybc -uRel)~rox;3yeRMpF#o$jS*@GUp%p+3c|Ks6P*U9QNwrWFv3Rk=QT$uPB)hB=k@i8U}ATPPz8Z>0`-K -8>`X2c`q3OVQM2;a=bc$x}gZtCLs)Q*Tnd!s=^qNg|%Y=u+&eQ!_M~Xmxj}7GBS#CuKb}Q093Fp1u(% -7IlbCj_hXfAdpz|EfHlrE^EzsHF_^kZ6#`WqCB8YsW5h4*BzW-3j#`w$JO+;$KOau&}?J78G!;diP6K -odV9p>PL-)4*y>qjCroH+q*f4z!H9xO)F@xYWzlO9ce+Ye$>puG#T2p8TD -SHBv{eq*{ialv6WnY>8EStUO@3JzhpIZdx$)lI3@ygVVA9@UhVe@<`Husi8Pn3>M@QK!_%#sl<2gW(+ -ov?BAI6qOmt#5?=?#~3p^wb0J{ycE6o4xz#9FVI0*e4I0*e24%+|G5Dk$8fzTMagCBGcr}y9*qW2)1+ -F!?Th(qiR-lN-}&!j{L%!#4{vY_Et8MysFoI1ut2<&+4 -ml+o%K2|+uRnVg(Li`z5$Y=iL7=BJZoLC9`(c<{fUdQMGq5|sZ;D|qQ{~kJ{@GJ(&x!X;yjMh+j=j4Z0ck1pDDWiZ47ikAmDcxX#1 -xa=nwqd{}clq5D55SppD&6Qon0Z`_{0vhjJnVNes(XPD^=gLNQtermO%50##mjtCJ(^Wh(B;@vPUKd22&k&zjwCBUx_R5pD8mG;B%f%|-8xL;?I0N{gR(r5+f^x5Ks=+MhXr#rb)`w~G+)* -!*K3W~#}|nH8A)vWJn+d$Yu7k-U0+7+3y=Lx`(#tlSe>xbk2T8!J-cv@qP~WY2OWhUiM6ggEN7`jPXuiu@im##6EH09(RSGp}FLvynQr~A; -&b^VHZDi|NBPB4~fHH84v0(9!~zf@t_3qa+zBLwD-@~Fsy9Po_{nR+ke=2K>yzv5AFOH-L2ZS&x>dWL -8^8dglh~R8OR_mY=TLym*&;;}LkK!u$E_UGu|$HQJIfb$> -KqbzK%+d4I-on -fUBbRe$U0P9oUi?ThwYA0aFrQ5IK2A>nzOIUojPj*aw?0Z<@BBfHMvK7LCv{=|fi-$qtn^^6zg+!l`s -J?va=$CTT$|TnMGiWbmp|mc{Eyu(^;7jJ3_Pa(m9OzHY+mNo5SPEa<6j=}>tKZ`^4Bj+?+;_%SN<>8< -1N$v-fvgXt^f7qBEPM??xstP`&Yaj*Z6t|KUl&4Ki=Or>-&%H?K{)_Gjkh-2iyA&ruJWsQQPE0L6dw~ -PlFFhO!N_4gFl?|Kj(GZk?S^VO%LFNesCa_9D^G%cHpahu^rgO@vq^Z<*cv|v3`g -iQ0xm4e)O53z&^xrJKoyOJLZ3=XrhnY?kBHB1xIdehjg)j)I<>axXeLKM;xug`#J|~B#+X|UzppAdvu -vV)4v8S{=ii8Wqug?Q8J8|7T{krY~kY!MWtJ}<)MvnN9e+R+Jpb#4=eG_{NlD~uRc=Z-ks(A=h6a!qm -=>2BKrZ$dyZ3pEN{Omd+EZju5%0-$fJ8_mB^9dz_AXY63ez^*}m|KdWUA<`c1K8H5K;>!$%wNS5EqVU -jRRh`r~a?%ENWrR`dNathkZ22K&Qt4~MnAZ;v^cem+|U_B}7kgXz -C<2+7|b(lOQ{l$HM+_XR%Gw3@eH)pOeR%b)qna{?ZR{_)xP=F~=;x4FA*viFt!_E*vNe~$-uY-jj>Vd -Rwp`#v9^zwuEB`v=6YJ!;J=(UE{p>z=|&Xm;?7;xPjuWPQR%qkIrXX?q;Jl$6}uwb#30#Yc0)Dgzs_l -_I19&(|IBD%}m%K$9u5w2WO17Pt2)xEB0U2H2=0SM#d(!DXYw=p2$rPH*77+up2A6A}bVS`TRG9L{e7 -X28YS`V|^OXJi=qu{#!xzEEm)EAP}|pD;))D>C8$t{)l`|1^>r8|5v7P%xp97k0- -W$d*z?!t^tBCXg#km6oN>v-qiu0o-dkC6*B -G&<8p7xV0yq2$7qTt#0-`IjI0S*OtxUW-1zT@YrmV9^&TfNZvx?_L6ptUFnI1qD_umurqX{T1M@SY$S -)+XTyw&b%UX0~R`HAC40JD6jSS`2_A*4Vjl!E<-I+}69yn9s7Zpjad6EH)7=w2;g*!<|a3vj9htda4R -x}vxrpQv9;!e||+Rh+U@!kS*yncO3sBvG5WDaf#Al+^_2C6%HU&B(%^arK&Jei>3l#gDa!?LaS*ez2# -52dLTA^N`dy20x~Pbd3s!hmv{&qCjxm)6HkN5Tdo&dLHYW0YhhTT?R-T{-w}Ti(rqC@o&;Ig2iaUFK2!K@w+8{InL5e>EZsu>HqWpQ6A?XSz -rGn3w}cfKVR(kFo1@jJv^fc93n9YMj-g-3O9Q+6ov<%$RSaR9X+J@XI2tNk2vONPNk061*3k2lcNmuC -yeOFYz}pFE>NGLnImLE51fF0(l69!a5AQkD-gu7-hRzPTlLQbScBxq^z2|_kH8MO9rQCl86D9f=I!wcQvOODc*t>TBLjL+SW`h9raXA7sSbeCAW`7OZ9@4fS-`(&F*6rrITmJEmfq#0-Ki)C$w{ -KaJxS#pEFHE87$Ki1whnHR$#8;n`nn95m2jr$|`s{&rvcj%JBUvVMiS1RlCg(^;E`W1ODCol&n36Z=r -KX?u{jw%}KS1leTP4AH6hhO5)5VU7G<)W?M2~}9Ub(l@=MGx{kGkIR!bCSEvQV!JcO{Tk?`}hE^uzAt -ic<_Sw<`oyOV{U|N7~($%d%6ixz#y=!3nrp7(G_x&=2A)CmM6IM -0k>56DHK&V+^6I4kx=c_HyXyRsB8it&x!lcvK8@<3RJ-tlU~QG86TRd+4>5G -I6ZzC}(S5rL=c+dWv(|Rgz!McWF9+FZIH6>5VJU1Mr(e^bi-tm2y8)p9~vsO6 -=>0l(@GSi{wKvZCt4z43*5todGQ}&%;7dmS)jBaN(=UrZk`3SALw0?2G?;Bgb%L3?%y#L{}ZOZ+|*>2 -~8E|+^H`B`cH9y9=dnZW!>W6MEvv%hTK?IZLNtRr-CR@ZXvMb`By;A}=T&sqVjziAQg5vPsXacRny>9*H{PPA@SNIq_EQ_SXB=IE{s2`@z??BU^HC -mi7P>K;ibY|__Wrvow;JYB0%obZ&E)kymOyiq|iSfQuY3KU((9l?ruyM-5be#8B`e-5e=bD*f`RDURf!>e%eO~$wcu*}Ez91!(*6OZIRbxI5V!lgd-lr~|9=^}C -velLA`i}C-75O@~ZS)e5H(ShZdc1q4h=3jH)yH@0l%Q(R+?w+>M~&w!q5(u@ZTN=%iGJ<}1946Ng?1w -Dk5u*l_~dH7y>MTLF|H0u;a{To7xRBAA7bI(Uj7%gaQvq<9RJ5I@>{a!rx*A^^ae*T41;K#f?)_EFzV -+t#XH)VNmjzXU6cK=hc&A4C!c3C6D%@kUH`{JFG%K$PgipOv|A`=z -pYcKIxhr`R#xY`m;n&gdKXdba=dTUv~#~@rR2egpV<7g8nqmen^RZ@&Ja3e~20GCsWwL)DdDwY2eTd_ -^{1=VKCk|cqk1VbjN?0=ur@KNKxDvlJR~zMkEQZZ+Qyf8+Whq_+37onFUMWr#-;WRMVI* -U$`E`Gcy|Q6VHO@B{h&QOvAuIHT+dfF!)Npdp;CrEwtPs!5+hveJDBm@^(<9yyi*0hx_Hec;v-zuZ9i -6UEYSXes`>J}41cP!<-ujakM85L!_Y6=W#?J>_KXfp`?*E=%;eudu>k+;7A0Q84_e~j!+VUSo?xX`j; -r7JkAbgaQt|H}pz&KOMa!c74c`keeg~Ytdx&4yG(Nl?N3{d`TWJ^ITi)f35oj4JoSezMy40eanfOqsis1@Bb;X4@0KC8c?6iF!mb{!3oAC9i((UUyAiLR&&Fn*6=CV -U}MhcR~c>ZCl0lNA?~33dD(qM^FWCDW=)VPD;-`>3&@BM;zhv2)w@nsa3>)+J`E?mDZCAmFGB~nRSeK -F3C<$U=~Jkb_RSxWRDWGIGCYH~d%jxK8bmHqwWXFCkHHo>ne&7t(x&G!kqBo47Z=M?{t09?6UZ6~UZtjogl;~GU5+X;es0F{Jb$Md_+ -8qCtVa1JHYea5>kAje*Y!GhHL7lw9?s&ILd@?<>)`-qcF1eqRr1*G -BC7kB=NdZ;(M<)ZSl38Ci;1JGFuW$fD+bM+OhWSM=NpDsI=Lo#s-!XvJu8*7VWxZGlHf<40oO8{Q2dqt9t7U_TXM*ea?(rT1!;=MMqbo#EPeC%uWkUp)A`6!}8Nq0LKeAYJhdLsnVjKVkWZ(w};%{QPPU#`}ti&8nBfoQG8((CcU=O7;f!DdE -WRISJ>CT>HLxZH=O@};v)a0^Z%}886jbqpfMbVV4B{45cJR0?T}*}B>4!^e_^>gx{L8&L-MF+BmSqwg -*@n0v7@)W4zG{JhdV~GW8p(#2l=Q9|Jf)pL>|ec_#>IOU;SwD -2|iVS^vDbCcm<`8qPrh|>gd67kpAfOV4uqeAF2DJdjApS3%}?j#K*P6V;<)(Nn(|a<5)KSEtezvVzTb ->!qfOelkO*%bHu;jCWwKrk~r+|Sfq9v<$d$<3x8(rUyDF02Y>Z}clP{^fgg~5)6p5N-+7=%{81TzKRO -1IV3zxBPY>=pIRjPPT{!TYgZy4_xcouSAo<$Gvd5V7^Y=ay_)3g=3@{!I%HwbHK*hnZWgNbKRSWHlI? -y4Rw;l5~5~6P9Az|?Pv9I7C;~?N05c==pAmAGi`tRbPe?7!M;Gn;!c>sJBJ*Xvtb@!!NNe%qjoG~B0&m}KO?~>MD -=G(NoXeMk9TCRA-QRAxvV31xIC0$wtCuAl$8~J(7$NRqXv9w>y0N*CWwOdglAFdzVQGH~g8?(TTyXa9 -Twg9qrrGD!5;YNdgUeNNgNkjZ%t9dIg4A$ -s_Ek=zboj(jBGiR4$rCZl@vc}DA>;A0Wpkn6jTrvk5 -Ymdkc!Mi)2eUa(Q9$#&k)Xedns?gLmU!LfE>xeDQu%l3-r$q>Lf`yg+~RwhxeUVdHR{;sVtJJTJw7xQ -Na@YF$h2|_1I*|VA`w*%yV3M!zccu{WHFrB_j(Lq$#!KJIf2}m_E)CrGb58J4+W}1XKp+$+)l}P@O43 -@g2qV^T)VHGIS4#cKbb}!Uf}ZVa3MMcb$Ftg`&B^~5?i7SpfpJC?q}(pRxe1zFEc64_ag7Qcy@O$%w6 -dTtJ!=+CRX5y?dE(h#QeQpGLm4KArMyGuffe*=!#=J+?{8)m;L~%zMTvF?Bss}s(?Rbm6dV6PG@qQPM -JPr6l5^;1}YufKUcjUggcTE5T`ppbPWfJDcsoZoo92uBJ6dC{Uz;O;Cb;KyaS&_?$!~L^aGmI?W;|K; -o@-=t>sIlEf{oq;#1kQI751GWUdw9pv2+N -Nv7YN%{4t2SN1QDF%Kfz471K3fSygfg%n-@7_S+MyHGPqAbbpi!S6_P+xW{u%T$p$ud -|9fmA#Fh0$mfN6en+s-S}zjwABp}bZwZw#!zts~{R5+2mZ%%wOwR($K*hNC@F)xqkjpmL@sjc6V5y~{ -GI{k7+p9p=r#FsE{JE^7%19^W-GvOVJeH^10{9NG&8wkbrOQJL9V9>=l@UH{g_VSqg8yrIr{e4Ny^Q{0GbM(L_s)Jw$M6^YsT5)x+qe=ONzk3H~yI}? -_wnCwssu`z|`mowrtcF$s{T3b1?-Hsm=f`c<8GvnMhET-3g@syl)ygN{F_^{Xl`e#yC6+pSv9K3cM2e -sbZzVFi-8s!h#Pwxo5Q78gBjYcQV^m!TVaiEcc`2H9*mVwaa!EHNgu~~v#?LR4w)MdqP3GR!*D7qGRp -v2$0-D*3t%g&?Oe!yC?VLrJRxOhy#_2t<+dGYzVey)*+R)J`Mnirp!&dVdTS=LH!B4>KL140SYN`$$B -1icusoTpz=B8|RkDij-O)7`|=_uf$ExWhkoxUE+J(5m5>0Wp|;Ei{p;tTfMSev`C*$?b`m-z>y!T=IduRg;Ag~t*-wZUzR72@4-9r#d#;MWP6+q9Jnp(=@N0Hu$57YX!>UAUtBzX?l747weoX4+LODb$AAsjGaWXXLI{}y`3sAK(A -{Y!1sqLMkuRKoNFrQ3x3AY!O>ff+YpiAsT&OQKo)X5zCLedG&|I`CPGEb>!e24sZ`xl^Kzfc6R8PA6g -HU2Q0qFv_DdhSixpBzi9^89NO|kP4-s;P9S73*Dm@96ZM3lvC~N=FpA{U7L{s$pxVugvdj&^{!C%z{D -Se8SfT!%Wk7yPPUU)Pkn4C96ag{q)_>J_a_7B_br9jg;z47nZ5ihkID%Jknl?#8{@!vBCM)~C6|D^VG -zbpSUY^tvl5{?XV&l1G*b-rXtmVLtJX0#gwE>VG -txG5YB9+0ERaP^jQXGZEn*c`776sE332s7u8kfzA*?B93Ae86U?F>7nRoC&H7D$dP}PoQ -}EohW_;D9u6oJ==G}x3QKVi}X{&X<^_PU|I+9%=LxW383!=4qsV!>1=8FqTUTYV -$$C1L^#ABp~wp*|X{CvSh3apL$EQ@lZYmNMp~n4{a%Y31J#zf|pv&IUB;dJIRf -|Ip9aEE3p!S;)6(gh<)D7V!GGNX;3mNCvek01Llag1SwqSFrkOMdz5$vZ(#|N>~4A2F+yk%+dVJJ=Q|$YhgFcy`-KnQ+_zzN$Gj -OJ)>U27WUB9kH|^SVXj>DK~2bYE0wM$>U>2kE1>)N_9QB{AsXwcRTEE`Bs0+H1(3Csxi&l-cm}xYXF1 -(JCv{1j63UXQtK=#dc@H1Ux6Vb%qd7x2mlrW9qlj$NN5>8U>yCw2l=8#8s2RmEBQ2I$+;^1FLEahBiacDqvYxYk?#v_YrW-#c!}DR>#Z8~0YJt -d?HLP`gWgJL#_q0M&M>Q1Edo%sRUe9&uW?n0alX(pGI9i~@S=h0f9zPI^eP`Q%>M5L}4m6oMfabUC1c -yEFb%1oMbk<|Q_7n@DW=acQqMS>%jtAl38nrv$@aT9BmKa8=f4cn!L6Mx4`5OIxLSf3?n}OH6t8J?{W -8z~4qN>4lNl)GWE0lv^kiGkkUv!3M%sTjVR;yx?3{J1Q7Z=Da`5K9Zis&B;1isg%|W$iEEcry<{V1{ynb@@!{jHTf``7%#Hhntuu7Y{lwqKwXF&v}sN{ -`DY;@aFojwH&s10^#Dp=45A`(S|oCNy?VUEV{TiARwHWLPn!-Y|mYm+e`;i+nJh@v?p@u_D04#gs5IxdB?UFZ?xSLO;st2)H?#<*Q3?!21ValtVUmzfB>|D -FlfHIS3Wz9~+1^W$BH~e}J&AXk{N8=RX^POHW8spt#?(z6?|w#+mSJkDyl-dasyQ3Hn?D!0(g`eAo6H -vXCqK3G(`?q*c`0L)ozAc<-2!?*uJT10d3{7>7s6hTGQlt1sc`aToPb)HhlYRKe#*U_Ey>`dF*rOAvn -i|!ikwM-xZ3l?U=Xp+SGI6Hj^(V>?=z+e-@K^)EK4LrAhgvu|I)8T -5xT7)}Kl;35Q8eXd=hbn16 -J>@2?)}5MS=#(5(2hM19DO|GMVL6$jYJ?!Q69C=Qa9;+rRt&RptB2LRa$=rEJt!NLO-0{;#yJWwI<@4 -&(X6#_qjg}ec&KfuCt(ay~FJuLik4ET3o;eiT)e+L#Gs1W#Du<%GhZ9ktj27VhizSvV|pD%5$B6UeMy -B|=>Tz=i?g(xyquHnfcgD0KUQ? -oQQ*VvTx-VTQKZ*92tHp>Xk>&6eLiTw9~+JTOnB#16Y$Gyh4ak~xMvJPJb0%bz$@BDJXs8X;RUl-L&; -$Ci6qZBSZFL><7Z=x!h*bXsWgk^N^qbw%eRpVT-N5=7YL*bwBb*`7zmwgqo-2Luc0kr5PLHsCuAd3FK -tks68zbg|JXQR?7xn9Dt1$ge -~fAJG1PlF4S$hci*}p)Q**rs%eyT<((cHS_l}@nQRHB(Pq4#w96!MPeNh$0n|s3XE)&S7eYJYu-oK^;GTewDIfpSH?jx(d#sKmN6a66rizZ(K0fGZb{9ayM^gT;+e3N -cm;P4#eWcVe^< -iw2CCEbAO^N=wJrVdvJJ4elbG@ik**_EdzKV=!hON_;y#ihnLZ}fFzI1wm4?=7xH~8zR+hvWqA>V)*Lj#uO(#4zcZC``r_}JO%Q3 -MKp$qVLTt1`k7U#4z%ghz6VW{WBFq!j1ak+cF>0*q!FIIAE7xV1@C1gPjhOCNpMf6+k3J&S7cddyib6 -2e&swDC_^!B5pRnuTCuvH&KKxWvQjsdnOYhth+EV>IH~AM)G!L{-cR<`Bp%_rfXZ%>vVT_u*>=1kI62 -bzq@C~^LGA|&{7Jt>@+Ff0trqdXUFB_Iv<1<90kaKLw&*P|tE#DgkO2mt90}_;Tf6xfKpQ?I`_YC#Uf -apoKO8z@pTK~T3fIDN(Sxr=7Dz7(dCSXS5VZHWvx_P7L3Ff!y}d#6>e59{=TK)GJH8_xS$@KSdxeV5+ -jhXP-JjqUG-nD~BDx#t5uRURnUMA3p0Y-z0XT5qD9^MObUbwb6J7}4Z9?kQiN7lTPTG4 -U7l1>sq`n(PU1c;XRosyVVtrX5$R61$cce*AfHc!Rl4-%!YEQ-&2GNBcZeGl}^Wr>xh(99rRaWx|K%T}B?89M-P5_)-DagcPhwAO -`1ln&&mdpG#MsDQkT(&(eKtPrn)*Xvtm(YUUE5V+G9-Qt{RomG{msx!l6x@IBQ97JcPH7n&}o>ttd%Pbzb2RDrM#R;0hp|e? -rIfw+RtGz(5@j#yE?SC`(X3K7xYq#(@SFz5izeL}xs_%eCq6djq=#0JxNr?99H^`oj?M%nXT3_vdCyp -H -pf@6#f}+#Oa+SBFI~R1$_@#!#j11pgkzw9)|SWAvfCX8foZL-iSrJPzw(4;dKn}{T|4hI>Oja=ptfoy -+GfMB6|k~{BBz&_O*#$^>A$W6CisQn@wcvW{FW5N --j%47e&;U8o!TN_+#spnhWYhgGRVIf^-3z&MBW^}*qig`Fn|5O5$12v_wp^whyMukP1pb16`;>UeOvl -ds1Ljkmh9pGMg<7cc~$tx -8`vqhrP7{Han=wKdFM$K8jUZaTh7c>WNu(qC9Dw9Oy=&|sTPTfg#Kn*seHC=Cqo0`GOE3XE?YuSJu5R -g)YX?ZcS5_}TwpGM_6Tob4h9&3=jZYKdju1Z*gw=?i7fsvoXeUG{)cuZPZ@YEk1TsdKQi|)e}z^h-)7 -5^OWe^ny8a0*^=vJQ@S*gW5Ww^^I&d67nfd<55If -vc}eK8Ia>z(~uG@KA#@FuBNDwglsR3&~356L9=*d9d71Q|tKrj2{IRVUi>W0w!q^#W0NCmFPE6hEND5U>ZlsPrXgj9a6WTIKlRid&l -Q@CU%SQp|?TFE>0ipX#2JY`P|zC+J`uZ_k?EnM!o=k`<%b~;^6ly?567JuI;fWUD3A}$`<&;zk=mxPu -gy;n}Ozz+vYR?{qI60@&TUvn&m0$uptYzJQaO*wHFPE;+{(#xa -0y@lh4>X`DwTiBlbc#fHWe(sO*IPhEDdf5>P@vPj&wm)fi@_deT?kqs&rSG$+DFO5F3eO6`562BWW~& -O(!(HiDp_lUK90oEJ1z?PsK#w>N6epYh<25YbwiPg(|fKmpCh^&4EDl$y^u`Kd1Ub|-oKdS4 -3$q>?cv{Q_y#ci;m&|LFUUEJCr%GoZw2Eq+5dx_NKBY<^{rcpeico6o|C5I2jEkBhs3dfCEV;3dVFPw -d~nUjIiMmq3RjXB-LnmDip$?mXyl(S*sR6W0C^`pP|#BKX$Iy3MgY6C@rf}22G@8YQ9-M497<&N#C_AY@x;rKcA)>6Cs!1Fe9= -CvO9F1lS$)_exMH{k_Hb4H`dX))yhs$G&vFyfZ5H~t9Uu05 -fw2I->ckzJn3`Pr-v*k7`|OVZZO*z==hA#K-^HcaTy$ECx#s}pLGOpJaXPF67MU}iSd2jg4GmLC2_=k -Vac8kVR>B}lwo6;e1QXVC>->VDPf?@+%-B6bhJj)iG`tZBg?IEBUzmu*rq|ytdp%%BNAxu_~N~KG&N7 -+qG%hFk9zCqEQr=5r-W)HaD{DY^wJ)_RZ -*hnf~xB176JKMZHVx#it(6s9d8C-GM}oZdpEkHDJ^<2HId`yOpCx(El+}n0eF6&!w1(nY9w$wJ2L*~1-m_4>}_w1ASCyMsJw&(|K -`^|zsxc_1hg`pTuATWedI7Y!3v1@cd+aVl72^dFk{L=z@K=0bsTlkE;UFqmI|4FI6pOT<=9!9?v@Cj< -Cc?kNs|1Y$&H41x+bYOd28zy_OyAihV9oO!)9bzxVZ%lA6fTw#vygh~fio)%^mCzm(!_nS}iC}Lt-{4 -(_hu&dzvX^|~a3A9((!F*AziU6puCBck%3Ztz-;IVsbXV@!qHG-BCD~y5-&FkD$U4{y=n42i;VzPO_j -rX2c;&^IA@%GEh#>wXg3Z46NEG-k6y+`cj|$AjheQ{u?VIze6Z=?f5zPnHyV=|EUDeENXc!0^^P}2u+ -WvoxTD{)jveP*KgFK}l7kpdLV!tx8>6?L&IUl~~AtI4o08y7Xd=%bb&$DHk$9%usevw^TK -eU}hWj#GTKT)nmscf$y2l9&%(Sf97!j%l$%h`IxC6Fy>lcerfMoX&zPfa)R#%~&rx1b$XOtItSx}Vd6cI|{B$`xbkf6vW~yqM`l6 -*?4;fQfK319MDps*14@+^qm7*6@@px$-vMOCM54a=71u9#o_halj7ak8Yp>9X#pbsO&)b+bWTy!v?y; -U$f_%b==kMoAWhe_-&lI1_-zbz?pp2nGP?`&qgQl&^p8+iH)dNG#&~Yf&b7^*mf=;up3NgeH55J$nqTI%`A!{ovTPzQCHf&y5LPm(|G^amgL -u#v)4kf2k_>b42{Nulg1S|8d2Sp)Y|`BuwJ;_8Wmi+i&m&rZkC@6bvE6o|S~?Pvc(-*G;(u~C!1uTFySw>p`XBgt{Qv9pKXSY%syG;VjQ1p -}g3kGwxOa%5x)kBSE3~kkJkw*K0qJ4v5ORK~_vG4PvkF=x(xJ*GEpVl{8=tn`{qvcviT*&?9A2>3tITQM$T3fP|M3|R@r($ -+vN)u-76XlxTA!2ZKNp0xx;{4_oVzo#e&z8PT&~@+du?|HSI4rUb_i2THy$1qDjoYGGLhcims=k_MQ; -2J}Rfb11sB9+D56B@l=~S!>x~}VTJaiMHjw5vLp@}&pWb|C5p&dde`ph!7cl~eAnzDI#qgL>7lQ=>-hJwBxtjIl;R`gr3}=XD{$RPLKFBKXMd=fo!ly=>B -YfyiEW&X9*?vmT_TlSub^_|l4^q~jnyCNP{s??*khnrU<@OjTP}b%aZQY7>q@>D@Z0xgS+LwWTzVHBU -!04CkCsz=RTc%&zWz3clwaTnZFITHhmB$e_u8TPMs8cEtN1t*rq`E1*9TnS!ffz5ARBz~5S(Lyq?%Ji -gGakEQP)s>Qaj}YYwCE(Qz=YF#A_$m;!>NOFWN+hjB5uI-?5EKc9Z9qw)1gD)dpvRTb%5d73reHLpbF -#FoCY!EbXQNe>u5f(7|H3zJU)k;31rKune!t^QMgFnr%qh1!GV~YI)-2#UPp1`l9D7g~+7W-i`a -yb_b1m_XI%p6jwLV{(`Oo_gE8`*R3=3-oz&ap*w$$emKdD>wAd2ytBbGT&yA4@ZY7`}w-0kiEGFXNyW -Ju0twJ0LgWp%16dLQnFE_dBD6;`RPG%SpgQ-tqamjv4@3j+Z6J`AP_7ng>8X0lS<6G{ -zrVSnw(D-Bo|nmQW`FK{exzdU<*UZCEjJUyp0yt&|~YK^cn7B5O&oE`qL^bW*4t)cL;U2ar)g=e*?hV9kTjZr{vi&u!=`{*EOr%yq$v!CGYn*OENjtV~|P8;FcbNR%c*WW46yCZKSU)w?Si!nL5 -7b5nG#CvugrS{-7jsA`Os-C;&|Kq(CaDUOa3Kc=_va-p{_6_)J7dnC2nL~Z13H=)Ddwz=3 -o@0YwasDoojq5q6H)sGcl_>(4ghv1|x;V1sJ|2K$}^#2rb`dl{Y7sQD-&=F}saP3e0w)I -T6AUw6GU|(aJ5@I)a+N(WO6{?N^H{M@u8cw;-hMkjJ?b@DRAl5yNgBmxPGsNJF@s^uNT?+Ab0HZd|)Y -CBHjCHV4B?E-g@(I4~NZF|qG|2I$!SgMsFZ>mJCR;So8q@(h7YmBAP>(A;2A$gOC0F!;^=Nhgt|K#O9 -KjvV59-B6ie-3GLwR996Vvj%@Ra_9DmAZf=Qy$zv`d>?(!>cim81l*MhrX?vNErO?52q#BC?gU$~slt -6~nsJ#Dyk4JuOf@l?uyY{nTJ?8#7%I_(>Fk=yDx^^WbGEZRR|DQqMf7A;W2k^^yw|5uYyWy-Vc706SE -ig+ox0TGeHfeS6UM^pcv+f`LXkyC8aIT||tP(+xfngjf!d=3J^v?C07jp?I>cj#Oo-@>-t(`8xJdzec -t!JPB9`u9)(0s^y&mJ($dT<0+$A;D!AfQ^gM;4K>@S4qNg3poB*00>pg=@3mU8TmJt-!%RanaH5 -(u3nF!s&d59GNkeoHi=pHT^FUr{Qo=%ECx(yj2iT?(PrAmFFlyE`LFs)Y~1PlpA`ulLLndFD&Bj^Pz% -8hvOcge|+f&m7bwwKu!QTzuzlXgE^71mwOZnakK$x;E -&WPelZ8B7AwklY1+$sBrS&G%IXirFrBmMEuD+%AkM3>14L=@qL5(OAjb97jjgDrSk1l=@R)g4Bzp -N*j*2IY^7~n1!7B&eU^XNFTqsTcvy|_}_D-*a4U%YYZ#V#--ydUMOWNn{z#*LR5lv^^Gk7Eej=NrnBy -dEd{a(M-AE+ayAP^(IVWRtd32IT{<_xa0czjQ*K1ekTKiS%tE>%}$5wAWce* -WNaATxF@;AXu>+)XXw(JKK^VsnwG0uu;C5YCy$$e8A#^6UruCp@2C>L7-CoXId+x!_!U$L%H2l!pqM@ -#hGcuh`~i13oDg2kln#2+R{&Ht!gMouM^{+$k*9tg^#d|j1(OfttUFg4dd)&yLKL_QqsG$NCX>zxJC< -OX6VCxSGZmgIT}Vgw@`=YzC#4nr!7de9H(RQmTVa<}t&lEI`78dEG?8hCQ;i6Z@;($NpjbPRV;6$1Jn33KCnK+@J5+@kjs1u4p8%rXgjV#eHM4==^?tYe^XPn?*HyOjjJtqTU?-7V- -CoR!iw;DzF5wpFeA=nc*$mba+^lhArV0+>mB6p{gIDA{L(fdT&MsRkqOnMhL*!WHo?!0C&2hb_5YYO?JNcOHBHfeOPcn_z<)!U_PYW89%<61xMVJD7J -dC0NGzK<&+Kwx04dW8hPkBnYKYp>D4lO73l-(wQILr`h!to!%{i>ei+kNo(Kx=l)tKfC8n`h6qL&7SMdPD2Hx+?lBZr$<#n4i^LOXt-uha#97W)Lvy -Mt2H$ZYG(BEY)%BsLK+Jfj^1h8<-5T!0ws8O5T= -z&V3K}#)|erW{{#5K{v-JM-Bo{vFYHtJLJ%6oA#B4cn80BgBPbf7$-S8uCXr9$zFqI^t(mgn00He^{~ -d7Sw&Cxq -ja>O>_*$gFc@4$(x!ET-Rkr#60lqH(Pr=vackpGev1qh`^va%IOoncYxX5Z#jbuu6>}p{KKJf}k}q-jXZUJ2_^OzaeK;o>0yxc_P#M3(oY0(tAC;O=%UsI1scuhJ7?-(NJY`p5~x?Xv{K^ -Lkb$I}X7nDxAmFE3khuzTj}oM>m6jd;u8af1I{>W6@8b^}BOFpz6=({eV~qMv^o^Z%hj%F&N(<3#MTV -M{ttDA&4Xh6h_ET4Na)G+IRY9F4&i`)97u${Vw$F#k6400|oCAUP^tASk%tOwzJ?jH`>U}#<^f>N0#m -P;L8BOTN)~O&ogYVu%RRtZ7A~##G>E*t?;{aND}+S5WJg}6WG4Wu0uuaoNI5Oz~1ev?|zDnXKk+?#yi -$h)Q)x=NK(kgu-*4n=I)#^6See_$K*6!zB^y~0_~>&mSLw@C0sl=>d>=0T+P%ttX>s@>DgN!4#zQmk!*FS(8=Cobr -0qT1kh@`1f01P5kD*(y>r2u?wQxLVUUAg~c{ButHClI3RC}&RFVaEz-4l0v2x8_kTQ=y2y -BbWv?@&Vfmv$Nb4>eQYVDONxfSNz8A+JXkEX2`k;DboI7_e5xx{MC)q>Pq+YX--^%6cf9XsPdU%0fT} -dtn)9m9iC>Phlgct09SlC6W6(v*63RlOv;9M4Gd{MGyZBmZ_Y6YYITtvrOtHbqRwFMnm1%+z*m9@=)n -ZsE*pjNp&R7D9TQgE-o2F*Bbm3@Sr%SQYn$yTp`z;F#>6I!hdo^_6r7sGm?Dpl0Pf&c6NU4m0MA3LCN -exIgkB2I;dDzQs#9$bt6e{!LfN;-$kM5t>Ook|3_%A=sRUjPh+48E^^R%e{zNeKB~xke;6fB>+?gDvj -*aeKGE$2UCb*8?lU9>nco}nixeBTe@X$*^xseE;^IG+2Kf`=?xrNuq6@MW{6_?lp9^Aqjs)waavv=_M -CwFEO#lu38Sw^754fz=lbcbX4(p5K72jOXcK2IZ#>DB6mF9tl#%jJah%`m36fF7^ -`R?V<@6?4PR?c~U78GgQUcOv#MP{ECBW*Mf`SE+6ncUWo1MH)kS%o?+TM-bU{GX$|0aMzcTaMu^8!TVb5%5BJL?U9rgpZp#T`;zRoDkxs_GSt?u>!^<>m^+47g-W!a?L&xOMm -4J#qU;68@eX3CezmxS9F3!suEnSq?uLp*mZ6`cGC-Rlqm+@T7^le1*$U$EHK!Mwj8-NotWl6|&bSf4MVgEJ0 -J*WZ&{)Oxx8!1b%R$~}9I!GM)%8QmfcXTyUQqF;Ot;T8d%Vso^j6Ca86!b%g_fcnwur8c&m5o1p$`5~ -ElHxW;(#XxfOFx3Ului%x)YIx<>6;V)P5MGCw_q$gnxkY{HTa~#+7rS`6nso5Z^CtvyK{Vy= -Ui(auqHeeiOREE9g-)ox9Vf_FbK=5y1+kNaTk2C(ya>TM+WekBl$hSbTq$`LYa)^SvedH_EdFfNn;bM{r&;MP_Bc3p7_9xIi9ge^p`LJ=`DikY`I(+kyvjG3b& -tZz&=DdKL>OEP!7K8+>z%+SvA$D$6uKwWdj(4uesUq|x;Uoi4Zg5f>>{uN*WmM`$CZ3~uSz9S*O6!-5 -Jph47TO6U6*OPIR1KNgF5ezucfMzP5$_o*n(YtG?$+zq;ZF#sm>C1R*4e;{--+AsxL%_ykRFf!_uT`1 -SxIA?R}(7Vy~uuEA)+ke8!mmT-*Cg#w73k-Cr4#czQFY6Of2z9d0| -ujblCEoA2M+GrvSvU*kMy+L+S%W1N@0&Dj2`w)eO02KfGVes?$jq$%poU83$dDI2X|x1Su&pW6G{Z^3 -kI6CF5c7$ZLW!s|A8!NyZB_bUTGgt;yRI1)|+bP(Djt1nM-2yOxP^p8{1E*6{gCk4MtkeVasdnifjXa -W6MI3vn@GDq-nLpUf#@rSrh!N%f_=SxK87vu=BG{TMXz_%x3T|2*|~?omdENfu{p5nRp0 ->1I_q9NBiK5^eEE!L%KfB?_pj`q4-WAcD4+oJ#WyiWG-itDMl({gs+e{BWr6Pq?}cK8L1GI%Fc$C~$4 -JC3QHx_zACb>0Sb^<}t5#G{?w~0pbW -PHDW0L{Nf+7a9@*t3vXCZ3Qfh&B%~Y;;$xa4mMp^xdq^=VRjKmzbfV-3KclH3-13zv-v-m_F^|c$x73 -P+vw#Er-2#-TyO)h4=mGHExjt8r0ZLzIbHw|mG@*E5nfPO|?-;KSLI83Mdm3sn(IP6hHl<8TUe!%CHw -Awy&Guh(y@|f#<$*v2ebz-Ijy59pZSdR)VE9~t|wGI8e9YBe4h&piYmOfn{a~|kWZ`~kYdKzR0$7yUj -Zn-W@8sqAP2htyFdp|S+Bi9RoZ&4DTMiwgK0ehnoU4EjNdHGiF1O8L*xGzmnfezHv%$>Z;UB~-I)kE@ -`yLVOS17VEQ*I0@6seNpGKpFy48w#92H6HFclrab}jTpxSf;&~))A1e|=bAUfhu5x=6^^V}JXqSisnK -1Ox>CR&uP4=-hxO_(-0?6QA`>e8Y+V4Cjc&*MIg50p1@04xt|e59h{<7OU!5#t6u{h8Mk*F;I=Vfv%O -%Ozn^>LYXV$C|4n#!%fW1~qa@rrU3 -XVuU6OmzHm2WvFC2mEL%76=bzjkAV#LVMv{s;`-hxls&C01+HaSu@zNjt1XQgU;#^w_3&T2mq1Jp&l;!R6}6*ZEvI%<4Qv4mRDXXpI+kz_kKm&@(q#0W(Z5P9z}xx!e -|{U%;L)c#lCiU^Z#NNsLI)7QMmH-b!AuqkEciYL%F`gOcg<$4ov1@G2fm2b|dC0WSI=SD^}y%?w1%>% -K0^kwc{OQ?G7}Y!z|2I_mBDVrA^bHqu3hr6|8jp$uNNxfFXHoJ;T4tKv1(fV5PCVvFJQS|QQ)mc~^rK -d&Htv$$o6j?XyHkroc-zs;oH!V_5=d>eH({)f+tX6arv9BFSHj2-)<{pxaYkhdZ$i!u=7Fs-CGsB2fVix-lIYPCVs1cA$YsF-tn7qn#pL-yMVQ`d( -rd`ys=?qm-pdMl*BM=X+FMF%wLosILGV)55_~(yhrOX(%4S;Y6EM3NqiR!Wbj8wFSW_w- -#MmLFON5kjXqLFm#+$9ZP*;YDPVkzrMxJ*Jh;)d|eB5GZW3 -lb(c1`^vrIQy@8qFe}dYBW6)hEVOJ-ZA7$W9;bvf8exmE7kh&%iqc= -l&}}2jZHA}(xalR!CaAvxQkRs|T@k{up2!Cw=C~8i;)B-GvjYv@?LguZDPG}(_2YZXv*|j8-AyG|Dx= -`uvjMIIlCzQYY8fXlc5Aob*D}BESX%fc_q7X02R?BniMv&z%)u2PdqX36O&Gjj$o -7V7QVH~9PVtJ3rE(2%HNKt@ka*;d%Yr>bnBl3z%bV8V?3LsU9LT4Vo0)d5G5?_)(IVFPV3yVp1(o~+w -}yPGAuYZi4mSHj;x!Kjk-B~`F-&3sVGq&FU$#|!+Jg0catk=Ih3F3K5QA8y+p`hjT?!9kkzBFopXAIx -IT!eL^6~QJWadI^d_Nf%|G2$M#_-Hra+c$IVfz-I*bVl!$f1ixbBfk(1XMKM{*4Cz57ZfMtu*>q7kX< -J3ht_|?CVP@iaOp(w$eB-Qv>Fi7j*~hD;Whf`l#Hi%^#ScHn&AZ{&*biP$uZxaeWE5yXy8ueE$gqq6t -NOH*O~{A%|DrA=6u3@h=v6$G*U4a_zNRB=&(X>q}PP6x?0XC~{+$VhrhJbafo?+2~l3YIUdz5aBC6KV -Q)~Ja&h{v|aOj?ru!TAqV -WRYu~$dbMVTMU6s8_fm#yp33(W{?OUD!eqo#_^XM&(|mxS|OOvt&7l)pd;=IG+&eJ{VMeb{fg*9>CQK -!3o)&V3!pSE7LRsX!EQ{dA0bj*%$jmJOFSp(IFal{174#(fcG}1gLjXFkX`!&DpdS^gqrnbfLqiN62%2ZROWbZ -WXGHUv|_e6d2(q&HO_jR9I=Hx&DnMPCe8885U}9jla=)DGS%)GERYIRe7jDbC9ISx^!Jm0N%NC7kFbw -4CfX3^&){En66I}IcUiGFXcSHHO5&Y{e!c3fNI&t{|Apl~$m}Oe{lI5YoJL`aAP9;=VS1y(8!LvePo; -n9Xm6t5hs4P}T^EIWr&&Pm&GqzKUneB?ykY$A`3gR_;iH1R(QTJXf4BJu(Vmfu-a2a}zO%{TZSI%86M -9j)>%nbLQOUol=obWbKbzRa-XV1FTnp&Eb#50RiuWcH9N+or_8QyGM{kzBy?pw1qux%2-kf(^VegcC> -(XsRIHLAn@mpSqAb&?-`L~2yyL=E>MN6-hpUu5zpdRvhIqI$Fcud@CUgc5zxLeS%YWZl587k{iE`_{&-9 -iqMwLtZ2+37O~N;9jZ6oUY?pA -5$Gzs5^%=JGO{NUae^OBCyy#4Umy6*Q297bxT%4q@A}A+}E@Fi8gCvH~fhV} -WYxYI}lVywBzex{vCu^Q9*$-R_?5KG*oUk*wSEP{B9#?p8u~(;y{=JY94Rfa(Nj#&5>ctX!vhf@hJ1B -Mli9qIa88qaIH~ee^JYNUx9Re0Z(%k*qb`(DKlhAm#zYq%oeEV;>sGO_j!*-86nbCg)4YEQug`ynEom -hs;zt9G$@1bS|%L_e?o|P@Z=_*MOH@!6Df3LD~_nP@KXzTvTYI2=@)WUZop6EDtg@N)_`9!9Of0k3HO -Wf(tu=rM12QBc?e_(%D!C%QaF2|Co0eQmHP(sknH%OBWco%Zqf)ZEjTs^clxgBt5s~zVI*Lvs4%eQhI -(TCX7bzLwekHDGEHCSyY+3Kk3B*e^r6}JQcQaxKn$ex)sh*cm~dakqYo=B4cT{LXV8M;qE76aw=x%d? -TAUeqM)1LS6E-J>SG9ensQ+eVtca>BD2G0aGCz0((5Ya%b$;s}O8^kxNiGiJ=!g(tKX*88=_`Vy+?di -JgxFe|bEP2d1>tdSemCZ)wu^iBPNgjTDcBu!bJ&S|gpeETq(-(D=hy&3w -z6cQMtE@1Vc}2ILE05ipE$K>XrS7wA>8(c{9at6P^2<><5pji;Ns&7g#EwYYd0Dss=4H5m4>BKdba~b -hbq|Qe9WC|d@b`l)ux23x)0M}mpDn0_of=X+lSD-XK~w?wtW?+-hJ5A?iss}#1VV9V)RSR6=L7`l6OZFMD1JNE~g*wlX+pX= -WkGKujj$pXtK9SqHn=~y@{K8?~?JJtishS^Q`qAy3?FyyX_UHOhM$`8#@cU!skGH?yJ@DV -${(kqse{=i$-2=bsul(8_UfwSYrkX0OoQ=7Ep#_sGOk38e*6koX7 -F85aPn&_3QD9mo<(Em3?u7MA8E6W9Rc^L-4#o;`ocweYT1LkiI7*(!k6~SAH7kj>& -6a_w5=8Qfd$q)iqxV?+GD<*{Ex<_-XAdFv-a?D)bn+cE~00-ESdRXSDaW27fdN98v+UQFD#t@3kNm~gy_AXzr_Xk+aV-=8+oJT-an4RdqlE9;0CAAuMl##2qp -Gaf_L}#27nvj2FTts8NS;(DP(V|+!i9<2)W%kk?zf%@!P9(gZk}R9NS+Hd$$g3z@5C~1p?Yan%@Qs(J -m}ne-jo}8HGGE>hrj+Ds~~IPqW%cXpa0MX?QO1cUta}AFBVjN=Ud&`9OCM+XLeC@%mB;7% -fH92wGUTqOyi}E|8IY;d%FDe_WM}BKe!$Hc8kBeL*RF}_`5p1u<~ah~P=g-lgb -p`O14E>5}>b>FM?yZ7>-Zol%3!a5>rWQ`}$>K>7ae-t|XMogT&=qUlJu8mDYysgsF;7BrlYljxtp -0pZBVe|)2fa>tsBTmlB2UIsU3FHaP>czzz;+(DE4neOr0bf#)`l*Shp}j!cJ8ZK$8`B>|E*Eciqs|Vme}KbiwQDq!~-p&%)%N -d|r4e^oIRZ!+{#o8x#o~D&uO|%I%oDAvz!L2jXRe>c}Px*taKRbV;}?dHAj!xU%E)(m>2ss;n1$6;-# -mV?g!L&&TqJ=)>;mz#lt=Im-l+_0j{@+q>0;P)l=6>kqkJa~B(Dx+7sPPqoi^&Up%ef3a)JHbIE8CHi -t;9z#R8Cvx?d*V%@7=kTn_#-HekBPgaA?fJ-8MHbECeKh2IWdqz^NCT;z -zQp64aHL5M_9+x$}>S(l2ZyX`05kmXdVaX20goLMgE9PVk3&@qlFwOT~0~5h;ejpGuz1TMv(WiD7mZur_`>f6~6Zkn0&+kJ~sKeTlG69 -wKvecJ+8K8wk64U&s8PqPN{c8fpq`1S6A^}ZF^5eZ5s*ki@hs+Pe<)y+u?ozOuY-S)LydPRlwrCH!pq -r5RkjE;d{z7-U9;S&F%yIJ*fFkrfny0S3$|XtwHhL(HEk>W%tUycZJ_(oRkC=kgN`{W4lC;KePMbXLi -3&6a5uE{x&$Y(PQ;HdMt;HHUk^TBp+hfC*<3Pw_6STymzhtXz%*DhWy{{U4JNTZ+ln#ZSOK2odnV1Np0?-sppe14L9eJ+phJs+Vd=dwox<_zT-UPl&+%l|iZ)ixA=D}q{D0M~w`4cYvQ?fu(bdbmW(Z -Q86r1-C`nbrh|R{-QX1P!{pSD?g0FE?f?+NQCyKCh%G3@1;XrHRN&_eiU9N#!00JBdq*}m+ns};9*Xo|qhi4p3kQSpUNw6&JOb*95v>RdEBb% -MeKWP3HyEdz~|NeFE`qwuM+TrVmXMO^`zkk&S(EZ|yAB1EHn1uFzUy7nh7^QHWq7Vw+n^p*fCTSESDH -26V7@-h|`V{|Qc&FPN*$v~j77w&Hbx^@Bm4l}5)?jL<%hB6cHT^uhv7NH<>KNM75y;!=Z->C}?X`mLN -V@w4yq)^?0syk3()Ms4*+hPUe_QZ?q`U0@E(R3s$@&e6X=u-g>`9cjy5fHQdopUH>_qg||0i~&MB^P7 -cbUXwNA3;F;rGlIneJ(tUH*~yZU67O;~$xP;Ge^>x3s91nq+vng7|dSf?*?zk@vWK%2%fFCj%Gj*nii -pvVD@Bd<>f?vk-c?tw(FehWEL%CIgnR9b8y7MDSKK{prMf=()nuek?%V&tF1Rhkin#_w&H_Cwz}Y8xD -S`%Ocu4%OT-9fBR6E{dNr44{8GSQw1PbKU!IKU%%s2zh6x{zV^5B-P?<7;HUTJY3$_#@AdcFL*5r>3|H#)?t1@XqtE33i#fkXv=V>KRR654xA0x^PY&6s! -!#>vIvZgug(JnX4t#m_<=3J}|1bz%eCu2y*Qv&j=GjwS9NE| -)FFdV%%}2=(M3XGgQk{kzrl5PRqQBds7Haz~(YeNnpqLX)FQDn_U-t9&lTDzL{xRCu%A-9lhS>1P2uq -18K?ZzID3^Et;qz+R%ui>poCsiGPdMu)~QlfJt+0(YSFgsT1ITvJyNsF9AX6`k@hCsxXefKFBp+4bC} -P!5LMUx=Y*Oh*rOaj8{>NmQjg7C}0aw@Oe+UjgjnA?*4I6&|#hTZnR{E|7kln1fl^xA|=9;7xi-y)e5 -mDfqsUL_M>(%6J(mv;>6`5ZjJ+lvl|Pk-G&Rh-oK|PM$!jD*2Rh;ln-AO`i(x^L4$S=uu^HG9Vi_+@+YDR -dKrY@|%IF8AJtFXZ)B=n-}xvKdz`;%2=pLKOg#m8n>FC8(~PCgPt%EI#RDHhBcnx~Oq`#OL}&9|zAee -N@N>wNd?GFeh$-GKb?TK)YW}M&7ir^BNx&d+U2lwAb`9A0XQ!pdpWigNV{OcEW=UTKPjaf~2{lBzY4TGBw`Yn_jh$H@ZFhOkRBYYwj0k3JYeJvic+-!C&IhlD -;peA|ggTJNrx&@S|h>L=sTf>paA?L(Ctz*x%za=T1)s~pF@RgO6b(K30#O%o5#(}@g+!u1{#@^lrZaN -0ah&=+<)p1QivxPJRC5KRPMao5(u#59?R7%H-zbekM-g1JauqTAi{#D%!a?DL|K7AwV0#z9Pr_bIcwnPG13XtA`wU6V1^9CJ*h_0sT?@{eb8elpL8#SmKBQ!Lxu0uyYwLz@>TK*Nr=Y!oi)G+ds -mWc7DSx@+=vxrDeU6#N-9Zu8l{(qPTt;3yM&){1Jbde$BH#o?v -dns*0pVmEc#=zE^;CItkzm&k3{rxMi!6TYLWoCatK7yk@`f7|sh+oUUmsFwSM|QV`(Jt*e%{jXt -vCUJuNw=H&UuTm^0V&l8`S&0f{y5}{61#9H?6zZXZH*ECpYY~`vv@y8}`}#0{+Pj`|N%J|Kx^!W~u=E -%2i>=UxW1|uAs{u?~x**SA=`EPLQMzgiHMBm7KI*EF%C`4}9V>uOJ{+ZB*Pgmkts{M2<>jJc=YoSYO> -tr|HMa9}He(6&pLtosV4G^;jT0;2y3$+HYJgg^w|)FG$vaymmt4Xps|o(&|gHHaW+f+vlR_fU`v8$h>&A@DO~T%5qjE~vacbO_L-;H5}HKV}VM)vh9~11$7tG+ -GjU^%(R6Z{?IMoF5nZ*2T9M#)@?8f$T2_w$1GBxyBp2ieC9a6H`^aQ1MV< -!EERk`D`@aoll0SHS>UIaTKHh*zbG%cIxiiuh7Pl!x_cAFg`sta`PJGp~?b@B?;#7=0P-VebgEE)t$2w!wF7zK8w^MDLQT}#RWM~H@<{}`?=yCHL3^!31sKwK|g -ibU>kLQ>JM!-OSm`1yR8Ptkbb}yn?*5|I!#Izv>B*f7TI2EFgVCdue=(-Ko`8F+29JcXK4bPYrm%Oc* -!ymYljoXBRC(<4@}n9+{;f_->Il04`JE4fWS*_R6q*WKe{UXL)|&>%~e4m(OC$q*Lv$ZNf>a!)-kihh -lINQ3sVILz&y3h(c#3Iws=pX(vovs3Gnf`;pHc9VYs>Gso|uo7Kt$`D)d_Cog3lg#n2X|`q9=r&8L -+mnkbxTxG^|J59l(CXA^qL0G<1!$`9PZYq>&Gj)i#|t6n&Uf@%5@WEqTn8nKK>~A0!s3QHjYdvo- -s9+k{2LhT8%%>4hRc2zu}QP)f6*yOeNe%D^)nOM4p3CG(`<6HCJwA5qD#PxW?PTz4V#`kPw{bN#qrRs -D>`#mXf_3R#DLb`x{sZ?Q5z`%(}LBT0Pt1W&2Qh>pItypgW!Ui7p -s-=In8%t26`Nm@;A+X^rN7Y}aiAxOdD7DAhafIY$3>_H<^h_rkzr@LR%7lPxdD86%Kg%m08UY<4B^4v -27ZpV{cT?W*@3cVMd+&aCBaMvOy-{ds9EUqZ!BE?~l{)y3)eoY_U&=uo%xt(w5G5-!bZn%`#Fzuh?f8 -8(pe`?MTZ2OPq`@x|B!AXPyQ5ZuJf&y`vz(E41a2$bAd;_=JFNpx*Pw{Q`PVDSbGkn*0QSau|8>&o-H -yWgNl$a*Ft5m*Qpnr;Qskc&k_nSiYeb55k(^FKjcV4C5FnKQ|q;^kH=nK-!cL%L)aQZ6~(LK48?c2M1 -V(PuHT~Y?^@EgbX+Q#&qo63qeT+a8GHVj?F`~ELX?7$j-x0=m}JvX(%^=$_Hout|YZGYRghCS~+zp_7 -jenpc^JyV|UVb&rOxK)SAH2zTr5fH)g{*UvSUcT<>coAIC3{5uc -4EL5wN7S?|2hx6Rfs6+P|t`Xv*+A3IBP@(a-2^sD}ChhIFZSNhASEa;2=Onz;HS -q5&`}VTZACs9n$L1an9U -%TKm`z|X#ZUtLM%1#6a6koAKZABNHnROfNBF5GCi`?c2(rsR1#lo3F%`1j28U;X|%nqwNeVnVV;dhXN -|!4oUhbim1RI_sSkosiWBG*X>c#e90&py)#)5&@wYcgw-VtHAVf0PFB{GZMF*D(AW^Zk%811SPS34){u7) -L1z2e+6^At;4II0}E+aX>zG7ThHP#k=W6v1`({=vI(#ALn>aQ|;{ioo(NagT)q2KaIkB5e)VqL>Y2bj9ev -%Kh(ELUGiAdXb1lpWFh)wO^|m2`OP_$b9iPfu1ktM$i|ygSH5P92bdxecG|QH(xV*L;=XLCAEoQYm0D -&mu^u)gDP0Tc8x(O6*_HgO>_q8N(Y8g>ueFW?UkarN4|y -Z&!-snw@;eY8G65<>Cc&cOnDfJZTD0@hu?;qp_T*o9n%trbnRT?bUMKbrJ<8E*B6@U+RmEWKoUn8R~5E#JwdDc-YAAc -JQ<2#E_rycfHPKNGhK?Wcs_C!NZ&uYk$uvx^P6wZcPt$ERl80$9isw`msBR>jdfQq<945j(HO4!58XO -}@7s0$vfGBbOs586`6yPZ{&cIyI1rN3?fQrXv{EyK(vK9fD{-wyapp0&@ip*!tg#63L@T4S`oTsCn=z -~xjq^uRtDR#xvnLWVS)kO7lR0Eh-aWSH9NU-EI9?9jV`PhuZa%`QVku|8EcqnSU)#f2JBOKIHHdPm52oP@E{rCruHG4qi9`R1!0zl}w+rChye$BDC(KAv` -Y1%Ad6BM(5}fZ2+-GH=&Y6M*iT3j2$gJ}1o$8^EL>J8{y<0OFaAUDJh+r;dC!9Drp@!5dubf-xRziUL^g2D~SHR*J0|yJG;3rC}9(Ij6b2@|d6{iw;`y`HV*5-w -W&wYH>kC_$HDa}i6OZeuhf7!M&6~9B&LS5nSb`DD}B -EM;@OF0C?Otf_e?dco#}-q4rh(Tbmk!T);3rh+G{nQ)f4U>^t>8#?vU$GJ=&0HK>?Ym$?j?_Xk=kY0y -+-@)!iFIitxc0R@F^G>zw2jNF7+eln>{?z1EaE$SXf-xNnbV6r^@LmRXI^|PY$ -H_!Q|;rwj;4>%2iHku6Yo`yKN+wDLYvODk)(5E7%?48>w@ZBVbM0d~>6K_v?=-tucU0byg-t67>n)*B -iv~l0v9|w8Y7QgLwHpYySy9je{9`<%H+y=zdo}t-r)NVYO?r)p@Dg~6}dlm^yb~v@oOC|fijZ?$vp6b -cbwWR*J=}LADXcZsmjQ1W8S})VD14&F{aP~$Ix$jmJ(~g3GZ=$6=wehELX$NR~#sbm*6fXU6?Bg=NYm5EwT?VjU$A5AeKd3;z -Tm}>AjQ)cP^y4?MJ-8$qlxapu(o^^d2Wgyu`I;YWg`Ec|+yDp0DH0f8gxfQOugl!v63F#t$Z^m-#^w;u3Mww9?z1{aJ -VN=UCvA!9P;v(oi-!AktT!9{nPN(7VsB$>mbJIAk;(P$g9=qrJ*y)>P#8mCz_7QA<{~IPIQT&f%FuxK -17F*p -Raxmec}&Y8W9NDI4MfvC~s*vc0NZEd$^&tufK2@dwb4P -$Q~;5FUDZxzB!k?J-yJk;X_Wn1?bsZAOEF&;>HhurQ6)zcMFO|zNfIalab2;S;BQHOynEk+f7@C}uy@_&@@-d8;Vl{*YeF=xApBWwMdyDGd5v -YjoeN*4Db(5mjBNYp*K_QeF3;*=yPht0;C66 -QlD)aX=_U%-b1sv5`EtS2X}!?H31@BY+Mh4I8pB3tYL*WYp&bW9?whzSUey6e@99y7+GKII8*gQ&RXS -dMvU5`H*+zwN&(0>*6hqRGNZqSP81(QC4)3#~gYYR?&cJ*UhCU{m^#wgudSk1PK=pzV6?DDSkmE)*g9 -{(sD9GwBw^FOZ=J+r+()5(H?o>f1;K440)+N^tiBDLh_bYyWEW%`Eq>}pDYe{hir3zt`hG%h)<3b)++lz1($j^4E+WMh8LZ`ItAL!^dPuBJ|-qx<&g}>=N4=x -=+m)jGJ{#tDO-O~;DoyXgEPd8R}pmrYf5@mwNTE7$^Bkl6m*iZVi))-&Cu0jKipJuUyvgXsP$tZ}hx!PxK;ec(z`*~E?x{W--^{%f$b+#!;2T7G(!1kW>ZV~XdZZ$xEWoejgeCoh3M>=eeL=97(?$L0B -qP950SEjH_2BlfCkxIG?gUi&@JX2vH*iJ_fFcpq?8seIkiIV0qGY7Yk#UqTo=-Ta+W!!D3Yr>XcG%Ap -W6>~GAu}bV}DSCx_{h_hE>@if4zDH|A7#B{gjn!`p>LyM%da~K7!kDYdd5fnX1&PNV#MO*EO;O|+5W(qb3Cf)aMl;>ES26Oo51~bSMXsxNO*M(I} -YY~-)ZSEMEbzckV6ld^c&-Oz7XK*Xymh`0Ol?46=FBBwD?cyRBSQ)#|1=Jd=j -)G=qpquIgE1H{fe0BMcmOA66k1AP*2l_R1CsOK9I@B0H|I*3gaAx1*I9m0gXpykn`uHn%bnJkA0Jpe4 -5$&@XYU;Ipw%k=lN0dGh8S3Kn({d#^S=rg|?Ds@e^p@QLyXz(Yl_&+n_2b}ud*?!2ylIVu9NCYNe0z( -K4-5xJ6Ou;aO!=FK|g4``p_YOeWTblQ!jQvZwUAp^U?JigEo{YPiFaI2B6}t--@~&9lQ=#Mz%g{H~!> -Apo#n`@kzfsPO^WoIa2sd=Lm#q9M0lS@#y;0r<&-T*9Y)4W^y3a+Ddro3wlp7vIv3(c{?gzj82!d}9e -dzrSzDo|buiI5X@qfd~+ktEM#rj*Q1@F*;{AQ7_Gev4%#=Am-A*4O}g -+TBt4uf22`dc!-Q(e2ye5lqg2?0%sfyYs8~wL@733vlExastowHo+}jUhhJ^jl#Ws7{8jlcK#P}osEP -+)a(s{zV-+D*D^Qvdj_h{fa&U>?ke9=P<{S5_Hmv6>N0`fxz2xenZWN{=fAp4;PdPJeZ&aRT9bLsMLT -i11BRM8*}EbfMthP}PfsDkvZZH}CZH>#4PmJu%L5zYctzp!3I^*97+%kbrD^3H91Yd!j=sBe(whw?z{z7J0q8uySk21NZJ2^r&}kxQlZSkn?h5Z3YH&XhPTC0^gROVoC7|nkj|!XW&IZWS -><0Eo&|Hgq8D&PBrc@nHwImtREuut?wTO0A2>QAlYswqamrb0M)8o>k&!Q^>Cbzor1SoxQFbBCz9uq2 -bgu$sTD8e($n?jg-;lbB&x))N#Sn8y;%@e0rXN_42CNJg2g&M#f*XMe^UES~?sXPj@D{&HxmOzroYI_ -KQnA;h?^zy=^iGl>-P*(G!(_c@UH`lOu0T&tc)7x?($aT4jB2I}d!O1Tz^HcuGqrJeQ6gR(ey^pTO6K -m0{iZUKk@vOG$GXw%@*A0|g-#cT&+$hUysYjt8JRj}*^lUxJgsQ=+z4ky=u@Y!JRkE{uYbg|&qpN4uFHnZpSJy&Q9AsqpGjs#S*EMua&lY2hEsd3^ ->A*V(oH{+7aVAv{n&S++f7u5~xs~EMHNtU(2dblu?3);gaS&;lCqct9&hDyigz*# -nehGKVI#(W`84Uvt*zQ-3V4t<~7UcyD5~9I@KS0>0rPF}yH9%m$t@=e$r1Ogb(;(#Wl>lr4HoAO$|+G -&ibYRvQZ>y6Oa;nR|Sbv95Bqx0{lL2L?^>))5=57(7P^ksRI1c)AyJ7=&`czyN`+dETmv(otX&P4#(3 -9FBrRNg1Q3b8-PaX4~UhW$VJ6aQ|{P+kyj&IBFvpHy$yqp%=+T>G{&Xr{qg7bUd4Br!ZdCbP}Yy|VpfYs#F-$L9)qG<9+UM7ML3%4<%rviSn|*Hnyif3ZTz9ZON^SQ1 -Zfw}V-+M=M{Yu)=MJEFwLK*h7($;o;R*lCBqam*M?|1TJ{RD#&`#LH@LoN0L_QYpG!B9VY347d8y7sW -D#(R?JWx6dG=7jN>$=*THe6zGt_drv}y`@qog*Z2bOSU=V0n{G_)pY^>lGwm -D35Pgu7(ADzM}%d^2xm8c2D|Tq{|AV1SIV7s`0_sw9sjebe~BUgZrUF+75fey2%->55qs$hLV^?l -K^vaLKJBTS?65C?Z>C|{jtIY$oaE&0&H!S&SJH+EV`5JkVCd(56X-4wA>keLp>I2hB!8y}l09#La?`` -q$yHaNRy08Cj>Kc>(o&0UTB{qDcif{GSI(NPoEJt<$huKX=Wq(Ok$fEtTvFeWqi_w1tB;B6@7VzQl@E -br9*SYeePx)qZCqu^d_dq4^fhsou3F$8LGpKwY`~I4LzGUE^Uh~hF4E)n;{`r!D-@oS1b -0EMENf3!35;Lq&L|<6_USf@zK5vd8Y7ANnDvh`m?OYJM*BAG$q@t;J1|Xa@u`2mKir^ku9b4-p2IEXe -PhE<2`}Tyxm9;X=uW}&3S24lYT_TU8U#Up{WOLMky!cR<5%eC9EcR7iY1bhZHD^jUT2qGd4w}~r+;IS -Bzv!1u%O!J&Jh;Tm^Fw{|(LtJ% -3cCR9Nhqr;M-fld5}Q5hl8|F5F*zdiN8j>`XH+8@dZ2@s_yf`V}jL`Vd}35Xy`62c+!(|#>^yvHj1-8 -Xo5(0nI7_7-yB&hj^^oxkmOU}DdfWS@6B&xt*|5hHI?>0m5YzBS}R90G-tE!d;q9H8`E;e^q#O`avFM=^XP4>7P>Aw%>=20u -KB#*_D>48$=(f+bzM_`j-vcX#}7nHLz{v(4F+d!>;jkL_kl`IsSq1U`;W1c}Z{fp`r%W!NAfx)_8J{g -y1oRNctF9Ko`@7eeinl#P1=dOjtx~$2Pn!bwOP#2`P^lKF#bwWQV*uhIi?1M+Ayq405zv)qeucKPjX! -XrH$~qUOJ>!ZyE-WjseVc*ptUq|vTm+WK2}{d1uIpPct2%K!G9-$!=}gbq~b7Phl=SC{XdQr~W>+dJ(Jq|ke-x7ZW#+pBQAZzSh$p* -Z*k;_16f>NY;k_B1+??5?I83D_PKyGz{PXV1HROBe0GxgODx^^$s7LyADqmwvWDdp;PEZCcgF)cq5yelJp^pgLq -Fd6ulTefcbGC2=5xc83j`t{hNuO4Q&O{|Oc&yD6sM|Z-FRDW$W&p;omV13_E+}d}&g|S>-@a&p$T!-O -+1W{gFXkK)V{@8`RyR~q(eYv#Zc?w%$_*~)LOv#mDfGn`Pcjq6e{w$@bQ0Y_^J(B}^$#v$5>G -UrwPJ$;=r7Q1Y|ht4yF+p55anduf;$`x^|=z7eoh0>)p6LCQj3cv03ov;ccT0p-VyY0k#8J-K2*3pqz -|P#sKt5}-0mgAx;{GeGl=yD;pRNSh>Ij$$%Y4{&T&P)2X6<@iNu?wbpDl25{16NhY}Krz<8jKEfBXw? -)aw^JRjO4er*|kwB0(2SD^ME+40V->S=h{k3ePVLP`ToiAEi4jRg}UI8N_}AY~tSPdWvH@k;epzF!e9hK1E*cR9F)6(T7=&~Ejg7{bt?iZ{l+=o3y|c~B+5>`pJ_^>F07^+ -9ExcW#54pA3chv+e3vR$@3m3uj!oTEHX#TdYaO4`z2n^4CX%JagirBdbz?w7(lkjGT)ox^x(Ue`PBVVc>My1= -$aoo{(m!NW>X#t*D43^CfiiF6$fucdxvX;kkk8UX*vT$7;ak0UD%$na&y$=0X?OS~;WE5KqWt$+bS%@ -=9P%4<^hjecs)YN6-s)f3OSbvyo=yKqC#Zl3wOj&IBI~+}qMz<<7917acZ8*0+kL6e2Sq@Ju;R`{n9t -{$vmYnNp9+)S8a3}l3vCJI>g1RUaSH!x?zqJZ(aKOc8I3x4s*eSyOudomyhr#2#r07u&Ou7KDt7rpdLa?TlQmYBr!T?vZmYOL{*iL3vBAo)h --Hpga-5As}Wh4I#E7e#O -}KN;*LqyKEwV(h@pStuM|8A=s`7s>;lJCYKfH_Bf`nde6dBUYW43T?3PJBQV*!kNll5lwyf`~VaTyvp -YF96A&2Ah*#J}!>oBQM}G*0?*5m}>VG)=_7LJsn|HQvMb9>!4W)g1)%5m0s`>B};`{e! -`M`I-F~g5#(h!Ei8(ZHH9fBdy?q5m3BtoDYe}_N}gSP*{&kOXZeKVCx-#d}Wo~X~>EjyBAcLhq*eY14 -uzwn+9Pd$cMeVdMChjhg^8z3v3!X+^S9) -&Nd2y5Am#k0o!OPMtKbCzECfyS3F*{(6eXCcP|!I`VJaQn4~DdQPU+WM<*moGqg>lf_{w>_;lj3;;tW -b0MA7}dtF_-@uXy8jSP%b>8=Av2EOl^>ID^mU_Cpzru~!*BR-n}Bg`FY~>ca=$tMy7CUVFnvu|9r|kg -`}G*`_24V#e(D~(JO8?0NaMSEEFYP!Z;f7U7`lbNIQpxT5l8zw%52h=4|OK|(1D+J9r!lM=(#egSK&+ -NY*lw&FSYzYtp>BV?EoyViRJHRmjy%cnndXsA^1-Qeu{A)I>}2v6-Iq`8a436IMuMm-x3l`$N{wPHew=$-6!{m*DTO97Rb9t3cEAhC7VSh7KQj4yA -^wMp38k7yzfJYIfq&4hQ@`&kA2AUQ6oZb0SS%B&A4^&fM`0StkHEa~$;^cQnZ_|Gb@lV0_w@>5X)BvE{$#2|(e6h@LbfkDXL2M{B%4Hlv>Mtr&@x^dWz%p%|(ZZ=Mdo)e^;Qu=)U -2&h27owxA@!`717tjXR9dnd8mDHQmt4uHkJ!@31=JbQOLiu -X>`Ta@2sA-}Z$EB4Jz;%y_bz1c2`hjx#OZRGaqUK$PVbU%;xSd1fYj|ytnO`#is{@a=w;k?IF@SEz%; -WL$ZW@#lV7#+O&Ov|`JIT!ZN#Eqjp8qoRhCr4dH(Czvqd~2Kc!Eomgps&Q --F}Uzk0t@_t!440}T5!#1u;ejXOPJle+OU)Ee$~`kI-fF-LX8Gt~z#MT_~u$#x>Sg4^~ye2>ZB3H5K#A*k+AA^dOlJk*Z$!wC$vI}~E(b;Eg0CE@9_r}i2AA}}jxzdkcwm7Lcoh3JDrWZ9 -@U6UX+R1jYGKU(WWKLr11W{R?Dru@zlFbgbyNE~O9B-Gc$!7srgA#3@BO8}8D9U=^-tF^2i`lU-&xnh -x(~T^%6(dB)cxZ;bq@WQLIfp`Ua}w5&2M*~D4l)6GhB_>5qwdQ!3GB>zpdytub+TcNhupqgZjfA$IVk -#BZDj?kdgoSNSp$%d-7QUT#as(u$0KlUC#l`dg?iTKbHMAccx1;)p$>;A^YG{(6bq3Bb4HvhSeVxVnB -gH>js`)pgLKcDA@^-KQNZ-xq9z{Hde0htN0EuniN)Y=kBIi6Mg1d-)(8*QB;5++)T*WUn##VzMXuA*Cha{IK!5Cp^0)QLKbI#1A9R<7mPd+;Qm-$s`*`-y -Dn2^}DYP4&_$7grW)vV!A**R)QrzXoM$OV`EVHhbSGTxLceW#JvCbbxBXl52-l?+o62+4g!<yq;vO3+5m(#2mD(pCSFg4}8i{yKy*>vxlJIgINgm`Iu|^7alL(Z4yaI5oBhEn+$NapMbB) -i`DRCoI&ZpCJU9sx{U%OCW?HL!+5)61dT~`4fyv4HE(GRALJwHG2Z -GUa_=M|zezV;Ere~`-+l+t*8h-$1TL1+6a#fg`{ePUCF>OEblekhq3B7G!WX$_UmnF#j`(z)Wp=snd^-W?REKlYt+&~!7y-u8VhaRD1?!{`zg$Nvlqs -v%$BoC8R~Q|p>M(_K5TH7oc$t{ufPv&GM!_|QL|oL}Ir4raI9@?RO1g3|ML;S~WlAoLV2N2-SRM@;AC -8YMAszL3;}Kx$p1}~5vrC+AW2w7q<7AJkAFUDz1+NORcyG?xJN|D&DE{ApP`^CsR}hN-970hbf{+xlp -%H|FFcL*z5=Jo!#}I;mP_Vgz}2H!JpDFUgx>lE3tfd5_ot)81j`O(pl1koj&5gMm9RLZDsWLlyg~P->^ -k8Szeq!27$V_`aZx3vcV+{!wq31-;$Gu${2}EhT058ofyT5kghQ6VldJ5i5ZUt*kXK++VWG8+ro%6Zo -{FC*W7`$#(x9KJ7mP{|S8B(G&0+@JZO=)35qF0$+@_oRoV=r}1#jy;}wur@pcKeAF-XGSc~7A$#xnS{ -?SnNpRP`n%Df|Uo}&WRS(#Q{xEd(V=Z1Cmpz9`bwuL;y;z!o@Vl<%3+CKdqmy3|M=0{-p>A>j)9o>SD -H<`$fFBamYOL^MrT%wp>; -sc7&iZSzN(|Iv20>)34{%<> -PhO{AaW**`q@TUh()ydPp3NI^JAQ7DYUAPG|tiDD>)Vleh;4Exf=9e?{BY@929Z>4PvZ7)84H)h$mA^ -6U0W_#lF(-^jwv=F-$6@=_DWe*(bUKqR`-{+3^jW&eX;|}!JLZWZ4Dq;&mzf#6*Q3s~>Gj~^n_cmXKz -BP*QzCz;NjEhSCn?U!R>=qTbm&62e}YoT+CV*n2K>@8%*UXGPxQ9q={aaeKg -Xd5X2X+4CCT_1V3ySblwpWAbiUHUAXHw(qwn`aKPFf6CeZ&Z>aVmi4!*`pzI4_)8DRpA4cOSM2<-hm< -(ReuXoLxJ+({9(dO~Upb6Q06QL|9OA)4zG&B5B_QYLG^K7)gI+#iJj&vzDLgYKJyyw+5>YSbv~nF+Q^ -b&KNku@oB*uMtnEvUBq60(PBvuyOVM&h~Ehe#AcPYe0bk{vCpAq%Lvp+ryfza>WXbn|`0M_}Wt`l{y; -L>c|q*=9AXVbfexT4hzl`b8jb5pq+3|)Io!v(E9WqsfJ;1Rqx_UQ#I%GHG(@N8qWUE(q>_j2ds+T%E| -x~j5mk*B>?J=sz;n{XyKEVV3AU$NT<8c7by0bm*_vO}xj2ekHDM_|D#?RNPrU*%W-d_Jcq&7wTl4ZKR -zkl2qB-rGjTg@t*I=U4%p*s1R9Q>6zRrNpcH%5B_u2$P6~Nd~qCq}O8F9Z%ljlnSU$waA=4d1+&clgNl9cIjV}c -(jhqOO^`N00qqVm8$Xw=SKS7c9XKlNqzx6kym<@-5wzsaPTUn2e59$NiMO6)mE9psLxp#E`#IMV~C!TanU} -@lkq~$-tSn+3GqFKm3JV0|+SyGh8J7G)wHb7Vz4qpqh$H)Yz$@ -a2kF27C=!f>Fh9XA?$yelp}bvbo?nr=?Ur*@7r1wNDc;Im~rrwqm3U*V;+%{IK)Q2`v}qe|PHsiY??W -S-?56&$!MvL36HZmt%n(+-*P7lz36$hZfczCy_rsHe0WY6VHJ#%*3nTS&AG%bX9}Z=YZv -hwZUn9MAme)WUgL(m5=p{g~u;h7Zs&Q%(f%dMpRea|Hq}8wKU6MddvHxkh(4%?xA#>+}&rns_dAstcs -ha$@Rmck}JDH#?JSGJq=|G2|RXf)1;SUxDaw=I&N&LE_5`37T{Z=I(Mbt;I>;qh>sG7%r8NA*0&KrBq%D{JOrf@5gcs@8{|o!k9At -5Azgu)~5b>8brA4x9*z)w9MTPeI=3QY8?kDf&TyNaBtvP-7?@O^AC+?i|Z6LEzU+|rK*y(e&OPljzuS -j`&V7({&`(1ltr<~Nc@)DWLPSqr?SWNJrLbWoqzO}R4T?Jj%OJRMLQDgNsMD^EHqO#J>2VjzF!L+Xol -YPNRH4vVyGTJ>nzf*jLa5E*Q -bFA>Ysk@JHduX_8Pel@WH-n~j*BjeWL+YQTWg?nD{*X@z{A!3f+lL|%?BL1>X=5CY0Q6PWU4E!I|Cez -0Bh?l921jjG$Yz1ssDxxDeWl^C>!4|*c>&oyL(UjA`=S*_HY}k;JjdFfHo(h^3wYP*cDd9mD7I&%#7E -v(AYr?oPa&%1+94=G?YYj@p@I6g#lM9vEJ;Oj*$uGqDq9b=XaERq`gwOG98YKYsvxR)Mm&YyQc^FSM& -%&nDO)_yiJb5BD>Wb2lr2*d4iGh8GRc}I+)xYHJ?qg{fJ51e~j>Q9j9plq#jm6>?q==% -7AXO~O{il`|IBRRQJcBN4&DGY1EcQ{`mxOF?QJLAe2%p*vxw>m+*8qHul0i4pxfUR -!+Ln#!lp^2+SK&Yr1Q;3g;&XaKj{zCZk~qj|t4IrKMc5=-!5pjXB&@?IO8ASN>r?yaRsN*=rc^=W -V%s{pU<~jiJ8W7*QoXdj6=vo=#WekP%WA$ke$*zvz1n`pddggY;#>h8ba0ou8L}#@UvO^-bRPN1%_a> -fW(aYksL9@K>FZd@d7}v{V{8Z%_y?JaPwSY0ouauH9J>y2*>co|B5!ou%!=2qnhHUvsY|{uJ4JnDCD8 -?ksJlHlPb1e816|yM22e7S<+&F`1PGN+Z8^ypbzT$g+|jgJXr{WXbA{hWCNhR}$j&%LS@KdfwoOyGm{ --8#h4~ytE8&yU&ano2M~OCaYkD~jl{=KVaZTcAcjcg3rY3P?#vM=|^F%MLy33NNz+D)7;VL>vV3fBD9 -U|4j;LkFAA&Ww89Pji1H}v9hJr_~>OBfs`2(AhnF?yhHOc4a8lVcI~>56v~SK<1~k^J3d3=_Wi=QJ>u -vf<#iGRW>Bc4B3Nk-wT?{(N``IxdZ>2h93bD;1ky_M-M`?dx3})n#YNQ0Sni#B@!Sn8v43-*hIrQrFW -%SrpKN2@g7>SRMh#wS!K*?>UVUIWU(H6Nd5ur6P=tgVCSlCD>y-#+BmV10TD1;A -^0)P(Es|F(7|+lpdKbk0}g-1VM^9)=(&p!A(GLpr6EUikV4WM;k0TY0NnHH3vg=yIPOCnEMH_{)RqQc -DG6pY#QHDcUy@!onk?yBz0xYbRF0l6r$!n)gq;VL_1HrdWh_+THW_qbWN#aXc!A2A_2qB`J1b-UcJvl -#|v}Q7N@?ni7&co|*+v^D?Ng1Sat}0?FxHVV@WE7+u4%dum8Wns2VmkC>pTeB8v?qjQZRE`ew_C8^aH -w(!CVZSY;KMO6iL9qGtU;wJe}_wO8LO4*;X#1}nPKU9SEIES=4$+xU3lGGpnI)PldTVH(P5C7R_&NhD -6woy=h;r!13Voq%r73~-Ezs!HXHNLHve=@!Bfkgh>_x?^KKi>DJeLn&q_`)0sgd!;t#%UC%F>+N@g)x -LiX&lf9Mo_=3V|=BJ=xTUJtdnDF1BMT;gbv%BJ@af2~4<@TyBMOQaBrY)xwv5ItRJw*I52(DA4y<{%I(`dHj7A%4ZdnZ%!pVbQX!`w5$U|foBS$Wpgw&>JZ -D-JyDaaygr^qTzS@nAuLviM~uzTiIghUK@MC=S_O2RD8Gfl!NsyvlSP9y>+;FUHwr%RFbTlr5J^tFU! -XjWUk|E!YpgwzIn}{xM^NV!`lwp>sRW4#PF|^JVcN{kIHKSN7GiTpLHv=veBuTCNd<-B91|({+Mt7%Xs`RKj8x^f4KY4wj>k@Pzu0 -Heve=&);Dh`srlK5`Q-&=vPb5-w6r&7}H+~ ->BoLS=uiAijLVH6yZK(ZHI>=dORoFIc;8|t(Fvqbv)Vq$=l9Xhi54DN91&ECv2hAVSu?*J#JokNX)h -u(ZegPE&}&E;dr_0?k(Sd#{#ta< -Xvt9a?US#<1ViNIH-_z#4;A$jQ|^_l4ibNw4kAt2IXt5$Y{ohC7Vq9_1KTq$ -doe|tZHKupFSjp-20O(wdZ0nIA4kcl^Xn -u+PC-vOd-W<_E4ej0p!I|QLW0hc&0Cq?q_FUcJzMRG>|qNRY#eT^4h`r8&k2WcC&9xfom-MyHqi+h5p -D3iAd^x*$Sbp?rs6@q?~?&Gg|lGH`i?T;2h9_Z)rLrCIb!$o#M>WsNPJ7<<=y)YLj!ZMN`UpIQsTFxS --&rew)>SKihldJH>Rw6W6-uC#y_91&Ri}Xvme;m=^R56@>fHv3%}tR6q83gB1>tPx^QSjmY+DUhc_$M -JI5gRU9c1)??wv0r+a(2CP##ysg}8vN}h|j=>pzruP+X}DOg<3S4=c1t5Ln=mMMp!f@ZlX{csY>Km;& -a8UT&3I?r$W7bte=^Q66y^8+!ixDgQ8P$+#Q&yjr6kxLqOm|{SRpJLTB00n>?aEQ^gmnI%*p0R -5c)P@uhGAx=xa;T}pZK~CW5)+((+cK|1r?|AvENKIWAJ2ES&rY!`jIXo}+}Ar3-?w*UlSp}4>rY1k<@ -NVdLSkmpRiD8!FNW|)W0_-7?V=zK>P{v6xQEGYZrl814POJj*sdz+j}B(BZNh<#HG1ltge??RXa!aL#vCiu48b};RdIbH3A{%pxmEqS!3wV9h$-2K#I?>^ylyg?nZn9puuEliQ -4nt@D;RuGk=Dd4Y+Df^q(b%Etn8U#esskqVB$NPEedQ7+O3#u69gV+riu#KaeiEV?qsuyGnq#S`8h6g -{Dj$uQ9difH0gpUWzZM!qlv7Rm_3(y>C<*nk1F2Y4ZT9e^OFv7@JuIJMGACScV0Z>Z=1QY-O00;p4c~ -(=H1gJfv1ONaS3jhEc0001RX>c!Jc4cm4Z*nhVWpZ?BW@#^DZ*pZWaCvoBO>g5i5WVYH43xtM`Z86Sk!A}DJ>+a|r(78=1wRgWE52P$|nXabtxt7u1cy>$V -sFN&hk!h5K;|PJpgAgMO-J5CurPS6U`uZBwatdD@-owLHj%;DkhJK**Z -^3@@1B1j*NYD$x`9~H^z&Nn4}rK2=$zVVTvOI5R|sz+4*9D~Re*w6`1|5b5Jeo1RSg~>aIo9x -<>Ut|T3=n@orGJ$!^Uecnp)Sh&Zs*(T<)v#sX1IHvTp!VACgodMVN)S!ew3Rn5^77FZ=VsdDQoPI=<$ -{xTU3Ck2dJ*RJv`*@?P)2v#$Hl{Ek#W4)e)0dWSwu$z}}dC-Y20!7ERJ3nXXcjggH$vPbcaBk&{)yqnV+_5N+4lRGJ(n -i;zKN;U`A}GJS%IBSo&Vn=gY8Gpro_SYY5!o^GE7pZ(;`y2f`E>YAiXF&D;eY+qf2&7l865SmCoa3EC -A2TO`e6edAU{LeY1R=`G(4$rX02&BDAFtf#7!22XCq%FejxC$DpH8v#loc4J#FkwaKQcuK8iqvPhvNh -pqma8|^`Z~~Sof_qARnkbP>LVIyU6m(RxrXvQFtb$8ak8-a;8qvV6;aOB!klM?4(j?xeG`E -t7EgO9Pj9-OmooVBqVTk43NXkHURajNlz-JZgMYJrIMR1+S0r)ZTD`E&J4;!g7=VlRU&wQv5$V5xJc6 -|LA*cT`wDs_*wGVc(&pVadgp!C23tFbXLNjy1y%EXa!H$;AZspw%mqvtQqpR6oWBO`_^CWVpKKLa%E! -pM3JwG)|_8w5rqI8uF|{lfJ9Il)^Vkl@}u?I%0b?t4V`PS6*BhUv9acqzGbh0P{x04vYi!1|HkOVaA -|NaUv_0~WN&gWV`yP=WMy?y-E^vA6R$Fi4MihSMR~&^0gGAi0)k<5UR3Zr^R=Gr68bwiL2 -7Ca9awlaYQ6QD5Q+9*Cx3<5X6MDUYTgkQ^mhg-XqhyWFs6@o#v)*!?5;bt&}9%@Z -zHz-iC2=0=3#1{na@dXNvz%;~mPp>e5`8LIMZ6M=35a>#xXNF604#?@;`hG^GX$L;J&B9yG*Z`@Z#*B -?$fs8G%PC;)WViaA0bSuY7u+KCAE>hZ+j47=Z5mKTX9z`%mNHm5?)F1}3g?n#uJDg6SJ$Qio_Gr`|Od -ih3)~qC*;xlGUd>u!eh$$<@gxONyM9}Yyu5Jm@zVv$DJuMxFiOz4FzG#?$JPDZi;^<#FTa&U)Y)pbTgRQ#`p#Wvzgt{vze8D_F#u(JDa_VG -Sd?cDi_)?V&A3HfE^~!>__bNfvsJNagy_d0_-RyRn6(V#8eg<3OWD7*2k8l2w&S+cU~K?l%@sFE55hO -`S#s$X&l$g)GW`w8}<4Qh>^zEVHH-SiJiYaG&S8R^r9@z+G`UxL=4`*6`Dkey~!ey8VPqO9jmhsHo#_9}&~Pp{BBo^7`!#d~PpCLeT997Z(S2rF-k$-1?Uflg>VDg*u6hwpZfQZqvTiVkP -JFV&VtN^%pCqG#WoU)31}K7_xnLNhS*^LLOLQ)#~u;o}=##y#BQBPi{w@@$Im8-6}Pr@FLin5-3OycT -12+ZczCj1KrfHRxc4$VM>C!=OU0By}zbYCaXzgfz^~Z9-e;r^7$8=?1pBCZS2?25jK1=iWPjPn#9RN` -fNz$STZNFasnn6uX+*}tRFcc2jy%Y-ctfW=N3@V$O`pG7EYc`|kY&fZ*L*-Jsw#|n* -jZr6aXxV?zjdLFR65G^>({yZLL@sy8!&hohHU1s}K`3q1>0|XQR000O8`*~JVIlqJu?>PVf7J2{x9{> -OVaA|NaUv_0~WN&gWV`yP=WMyvA=%pc`RysrD3F9E|RR1=Xm&sx!G&4z3%6Czx(a~wvRvk^OG;O&!2qq=@(yrx -_$ilm!JIi?LT?%?{0p#J^SHq`||PrZ}+dCzuG=|czL(ozuErs@W1X~y?P%1{_)43U*ErY{`TQynYk^eEjL*(SJn -4+gA^d`TC2;yXSB3UT)w0YK#BzQO_S~>!)AiQ9r!h-uvYDTRG1on)uDPf3SV@>ecp?_rBSl-o3ed{QK -R@9LXmSuirl2fA@0?^Sw8}|3kd8ZGUm+}{uVuayS;tbzWVj;4-c=e@yArXy8rI+`Q -xw5+V_ulciWqX@8A9s9sA?<>%-66i|4Pm$GeyJZ?gHAh_~DG*DpVOc#O~g`0(=n`(JtK=hrV|IQ07M- -Q$mM-p%2kzxZbR{O(Qkbo=w&>$}Go%~wBv_v-#deE9SG7k97U+-=X_Y(MeZn;-r$Q=ie1udj}5pFKqL -&h8(#yZiV%&;I+}2Y(-({&jm5tMT~mlOO!%6#QePFSq;GHv2= -&ef$y)#z21wv)aD9+kSp?_x;bWZli^G@Aj`xp8d<0-#pts`r@zKzkc-e=|^8Y`|BU$y)o1A*I2)7>Hf -!`!USJ#F{;PsuiyR}9ghb7^6Ar0{v|%~(Z^3dfAZ|F^!~Fa&%XHd>#w)ZzI?iUw0-r_(`QdU`R4PFo^ -D@#^Yp7Pzy9=tZTtG}?%fnKnZGs3|7VK7k0yV7h?#tO_xAbyt2a6HzsB;ui4MMcx&83`?{~4xFYfNcq -PFL;MSlAA|7#6jJ-q&#MPI`}pFO|-<@xLX8crrQ!t=-I;#ZFkKR(d^*oQyAesTXQ-uT~sc>DIJKYaM%FTecq!QI -QBKY07gACB_jkMWVm5C7TuFZI~A_uv2LZU3OxI!nA%$9(tFSjXU{QR=&wR?nBpUi8eJA3yo?`1aXfzk2e?N1uQD -rzc-Od-~+#Z=OB*@{4bueiZ-9A&gk4Pkwm*h+&0M{ri`XdLfr88~Ets&p-Y4>nH#7Q@Y;Pyc&-E_Vn9 -lUw-@9(@+1$w_iT})2C14@9*7&(=4~;*yG38=WTR(+?L+TZMfy*HvW3XkE8P2ydC=#~rypC72iIE0?bi6`IHMme9$3!FW443-^4mCWqs5(poblIIv)z8gJj@m^`?nHLZ -zXy(?d6JgYmK*-=vzFn-PX=u`{A3TCnF}U?PuYAoyoY`o&6@}$jNIAccj2`vq -`4Ml~VtX@lu^6Q`x>I@9H4n#r6T^s^*$2bjSr2~WS@Evubm=u(V@674_IGD`u_B%R#?jV-zRjn|<&j{yKN;p}0oz=+7gP#A}_#<1M{og)`>sjQNW7E!l33bt@}g-p?={XSLA%Jtnjq-SO=#`_3mBNQ^#u6;C*#wQ -a>e@p!;TKqh#q0E -I|C^gQHv*R?Z8F`%M#WSQxNa#?A1}P{+{(FY|yF2D}?=oIkQco@o0!8X5UZRJ;J(6J!6*I9-VG!XNxz -}{c7)q?bbckZPB-GkLbdTZpCad=2*s3Vt!*~79Zj*-3`Xf9nr=S>l>RE#}UgN7Cy7x+HYduv|D3VSB) -{(cz?mBW7+96txb1S?Bn=rp@U(2@oRLNg=!4*C~o?+v)cEY7)zKgJ2l?ln73G{5^EG4)YKa*&TBEX(N -(rE{he&TuyHk_IG}?`BtrOFo@nE)E?73LW!9E@iyz-dd@D%aeL01b~$2mO0&O5eey -DRovXG1Q$)5)5aiCxC>#IrEslktV2G$uLr4|X1}#XB_ngT;%1jA%F3{A%|&ZsH9@3HK2m=seth`s8RR -)*VABd=e+YZftC;IayFV%EaQ`HMQnEv^~~sMh9aAEG*;e#hK<^v+{(Kd0>(o1K(`})7|kD@l?Vzrod> -{)o$IxpH;R?OkO3vG*&AH6y4G>+vL%)k$6lPyK8yGE{kOe19V&m%Z+m{x!+owH_M_1pSOb -Lno}EW)gtmD8!AuiS^^;pX|2zpF)o_>lt&JM}ZM;kXj$ImVpgJu&AAP6Oh&voO_fC_`x5RIwyDy9|VL -bb_k7K;duPYu4aIBcJ5l;wHh)$RA8R2>dJ*QpFv=CoM%t$ySf>x|x{Il`HXI?Ov7 -2Z||;vDWnK9b7V_TQML34!#ykUd#9UBi8MV?w5lVtS&7U7lY-mcu|1@Yq`$c7d>!EG|4;=$pe_PQ!$U -lk}Z~OVudVd)8!E7BVfcpVuaE2f`1Y)5l!$YvE7FG2Q -C^8LXh5f_4L!ge?=iSkrmnMevDJH^7P$d%;Cm7k(Xk?Dz*L*SAyQ2K`H3|5P7#T -1-)EOswl$WVOf8Vo#m1$<&}SnXkxgFoax?tx1p*kU@wvURY*aN5o3X2}NN0AC+C0~2S%47|sp@iOtc( -TQ+K2K%(L0WPT^uz~L?toWKX9z&ZEhr-w531ji1HKKJaRMRrfShs+I7)2+v8N6HKvk|zY(S?r3h^W`t -2teiNbZ1A$V+gk52f*Trsp|AMS_5>2!4~E)I^oX0`rGl7?uKqJK}rD5&Lm?LZ2~MNE7;*)o$RD0^p1-42C9D;Nc6vJC+z1br8;{TZbq?>^1Du=~yzta~Hob;63B!0G9wAwo|u31dG -Q5&43ex?tKt64ki@H$D?B);P2&(I83_Ah7DWHb_17GTVp_BL=33u8W??aHJ&hWLk>JXNi2&I67FKE0# -;)WyRWfpE?hmqB{3|ZJ-r;RE|7rt3;Yj~l8TAP(lBuRLv`kGZ_6+aNUP3Q+HYWho6c^Jg(4r)1DL!R< -OnUD{#M*(3nPjS0*rT!4If94155=PW`rZ>VFoS&}?}o>I7i -nR5q6k6k9>*6|dioTsel~%n_8YxEbCt`~z07K>7*7n#OXXPK+%&*xjjmFNes@c7*|@E2X>zM@$88oR^XBiA5)2Vb-8CXHlB|8$MzfK1_ -a7!%FQ&gFOH@2VQ@sXiBZn|Y8PC>9*FmA`JHTxQJFs#04xBqF(h*4SWtY7u?hGy;$yMgjWOVFkGi@dP1 -*mRY6QAxW70d)gehG(rT8S(htJOnN&xpATkF%NntHe}KUJi$zPvRbEx-ErTfOozLH5YNPiui>|}vjbc -*3^HNF-SnWJ?g7SEbR{@C)6bnJ7(AKMpG>qAhXMTAC9J<=W!}jkxR7Jj{ -LYfQA`j24T}^@ufz%lFv@lVmk{J|xB&X9!^1TIFK6y8$FNv?m?5U+W -LjFsDB0VsyZspT%zbqGX(=qDh(<{@xNuO@ix_5)Nl -I!!h}otQLZ@4;&6cNp^m6HgYoW7yNDR%R*lri9?kWKU0*o=z)dsah78Z>#2BM{8m&rfzb(rtY^baR|$xMGjyuinFmN5h60M8 -xF{(%*Ox0ddh4zdxO^bL-q;zGhTC4M-;B^YcY>{cc(J`N=4Hp%iZ(#pi21J@upI*l;&vS~LkEQe7&?J -QDQ)@?y|$admVP30j02z=d4N(9)+GQ+(2E0C|d82SJ*E^KLDdCLi3(kE|i#HhwD(_^wZU_cAtAezLcP -BubkMq~#B(%L9#3DGPNhW?Bl7~u!cKwcw43_L9KYO1$|p#glGth9I86h8Zj}$bB<$rrkz8fb4Zn$s377sX=-~YB2$X%u+PO?q!$nyGF^ZCal85f -yhVJ13-K551q&9omjWdYJt=$uGiULaO0%>8{WAV!CaSh#&-(Bi?sYTzky4l4cOeGuVpMGTa6vnT=Au| -g!=*{!DB`zm{p)8Ua6Vo!Y9<)uK5jIQo1jjvhd&qErKI&? -rTZh6MB{v=YFf0M74{5T50&H+R5Qw>xyEptpF?+R&H&?D(3tUnR>{x99!w!>bTvYfj=`M`?SOOsYdWT~iFyaHMx@H}+kSzh#7jz}o)! -@a4&SfVL0L;LbRMSdIX~k+mac@JXk?%&rHfd7qA>q8T-AR1wi~x`}PGbpO0HLrlgISHPQoh4Q8YA^_B -qJ3{qms%G3EYcMrG!JfxauklBmKi_@ss7W)5DDbQj=(v_`z8hw~3Ap9F_U;k^7>7OJHJS9zbPaCdix8 -l9MHMtCF>Jf4K92-cw>tCa>^Rmc?>h@;k7;;U_^r!tL$U17MOQ)nKu@Tvcnzg9d@8)2fKgOpUFg -L9P08Va71vI(U9*V&%x;DP8-X~Ff#!dFXkL?XK4tWQAHQxS?Ckn4O~*ei6b%griRWw_fQ2Jx#bd -Z`VZ#FHJ;_s<%r5~agTUn#VNa0^-Iomzo8ps5fWY&cvB%M?>6Yy=>4?VVNgb -CL#-El7rT;}zyXFER<;E>s6FKtaQBlxYmny*Uu9+hlJ_cmUjRw1*YZo_02<;}ln1f&#Xl3Pphkt5#6A -Vhsi-p`C-2tWcA|RANYT6YYl43#UeE8hD4)6i?rFCzSXyMegK7OPY?05E#h)#?e@(&9y6{ZU<#z& -~1g;jvk-FhhV^=eJ*xYGM$4^3373h-&rlI-I}TkwVZR3YBSHJ{eq -ON$uYI2M_tPx>`MtkQQPjpej7wfav!x3lwzazrj>Nx9Rgy6xQwm}x_Dr>)9fkz)1ib~t&E-1 -;+;)Cub<;D(eBFNP6k3QdWlnK_UpfEjh0hnw@v`JL2px>j(QSV5K^VjEWc5HB-6%kcOjYn}&B09X_%h -lTz|%rMej?FKICNI?2-X;WeV_qN#gQwzmAQ;)4hf}Ds1wcqr~K*<;gwHgw}yOV3PO4QMKgZo52V;GZR0|z$Jy!X5?#xtSpuRL=GayA-7 -0cKr#uGjKBy|gFA>#z+2$qNk*DQ;VWJ&>o^#e^FZ3s=~1WM4R2nc@LC41#VomkK88J8t@6N5;H+D{;_ -(8PP@5~&B<{2%g27nw$qo)ax%k1-p8kPE&|NMmc3>3NQfK3VC -$uyGXlmeTUS}sdog6O^fbeS5RyR6)x8|bs*uesN-a&iac=rohuU3(u*PA4ZTR0qewrXZu>S$ --n-+W{^?@KDh|bjyOQ_^3JrSSSiYoepl!YM1_l|CN!)3gR^?I=P_N{Gu-*m9K^yo;j$Q;5g_JTUg$aG -LRw@>L)!(^W|G$f`QpE{r#$6gu@CvS(h(2R)JuI+Q~}!A$<%!Bd|9-#lQeNH1pOFL8v;LtRUs@_>uiR -spB-YbTEbMw7<4%8?*~tf(EU0dSio^nGzd95J9;E1@OP)oqsq3msH|QYz|}82_NqhKzm(=mmmWS#Kan -f&jw#J*d~=aZ#74=mN2jQcEXpCXSH4Ak)pN2({V32FwLW@IF!MshPO*$0=m>};bR9&*7plPobV-t^QP -z2Qxw4}oGjGUfXyxz%T8D`ma$5&uIwNfVQJVv!*}z5z68^WJ!2xcSRNJ%OX_l#V4!AE>o79fxbpx5u* --E|`@`Ls_Isi)S@7HHf{I9=&TP3__LpFb$OP}Pksvh7+RZW_5x=SrWTn#u{d7 -7p`{Iy;jw+IEg6&qnAcdGS%a7ld@rG5+L97MW-^iG+ibhwOP-+5CL`M7t+m8{Hl4SH^F@kE#D?-vB}` -z+*c07QoD0Q^Z|Eb;%$F75AY?B9PuIWJxP?7WDx<-(Ww^HA($a}6=OeENJXhDm0JJ>0YFP09BE0kD;Z -+K#8#tr-@0YNT_q20ep$J5|0wJ9fjG{$CRruKhj$7loeduSpsNKhhNDaUCU -BT^vQb@V77yw*zvy0TNxbax;CIv?(5~=(PxFUAt0EfqMb}W=+qhC3c(bOGx;C_m=sgd*1>|BL4l+bEe -+6gRRW8%tCP59GQL|t?i3G~*>y|zP!L2k`T2K5>7vOCX`C8<~`S{FvF^x}ZG#}^F}_gYjS -99O&OUV$~+VrZ%Z*r^>gPOsM8T|tFNl&K}n9sMCZkbNPpAasbZcxJnSOWM-g?Cwt+$A=qQGQq?XQ|JO -Db=Ma@x9-AhszM;ZVQP#S*)IALl;`n&mW*0O6@HxZ@z!A2e(Xj#;Wo_YaS{NWVa}mEr)JpCbwl^ss}) -*>Y=8(@vr4CnJC&THpadJFV$pN01!4;+jtocB!6>@F>v+(IejUE1 -e%rU0df^rOA4B8MX{N`!l3|b45-ARQC;cXn2>gc?zL63VzPx<^;k>`Ns%=Tz|58sVOc1)pL8(st?#<+ -X@6BLTrO()0>09Li|NrdS$&j;YKZW_R)H49#e1|WQO1Jh- -<4v8X9Z`Twe;i82u?j-)I4y2E(-Aa6GdMw6*D;fIh<;MYTZfn>H?4iqb?0V>uy#`Wp0sN3nGCmw$&DY3LhDPjT7`RTDfO -Z(h^>*0atXI2%ONP$4QO(1Zb;=GKE~=vtDsHeY6M&mFkL_|Z#f*R&G4*R*S{ghP^_pwH -&r&QQXq;Aq)M^S5mYykoRq__?DTfgQZa7mXw;NwO93|pxRF~KpFFuFku7v^hJb2^Cc-7TSd)lTphAf) -XEF6n49wywaQfiIqUP!&4$G5SfmO3|g7jUMXMtazwXqW}$Fulr>W=w7#G?bRXzF$$4KW7!+tu{AB(MS -(HEg;Z#=TI7VQ+8_T4D7bJ*w~j*`f@JA@vgB!%)Cz%#iDgjmolgQ~2s~_^(;s3mU8JOA&M&&x+B|}SA -+p%9C{VO!a$ps9!)Pfx^rVS}WfF3unusIyTb!6m&jE&G=g1~vV}+0;uw3Ul3Y|7rP8FzHv-=;dqD -SENQf&Cu|f)MVq+NpfQXpFE~SAG$unC`<)@9ippb)ZwPo*e9?-oOb6muv6{$vBcHsckOTn;EyEd|vSM -k2)e7Pk+Z%Ve^SUvXzy4Py{CY8CToQ7p*zk#*}I^^z&kRjxRP%pLt8nW0c_eD3`9^mP27~(OKb2;GRq&cWa1*V){0 -k=OpA+09ddd1I2DbMA^ty`+ngEbg!L_P$f`7--T_`NHS2T@kydzw{fUvHLfTHR26O%B`A@#-LWQrM%_ -L3F#;}?u!w$|4tme0gR9BQl*D5N8|t1~RCrMO<6LNmW{$)IRw;SUs_qpwd84+nEhU^DrzA~WC@*EuPw -RTafv^YJFj!pkI1})odZcb%_KWVdG^>B72QfjB)$=Wbd1F?qBju@{$Lx0BA3h8E1e-qG{;Bt5p&|~+a -i;$RKnnM#3Q0e12Yrp$i#A%D+Vu}vWW@``#)Q8Bs2W8P=o$gSfdEv7|TmlA2c1uoasP-q=TPA@ -}qRLZc^hgkf!YM4S4t)TtrRaZu*I(*5xbFeSxLLn7f$tvB!xuEU#ff8_E+|>e5SQ?YEOxNwq1L=SCWxLVySD*MtE+*%-1A^_gu1j2ys=^ldH-BEZCmUcAxb<9ZdO3PT2`xxEpXl_lC9n7hLgty_ -r$}};cB$?bYM{+Z>%TtPexS2H`cIdd;MPK4sRF&ienIbh*XRFg+DcwoZ!30gGTMiw%hwhyF)!)-~0|^Q>mU=ZA8Q3CnR>}?#nM -m*y73dSy!s=EOau8%)f~B3oCx$;;Yy>D;E1mK|3RRM_$2i%NL;4tQagb0HP+2=X1eb5dD6(GI)V(Uf; -FxBED=P$GfheM)PAd<1b$3=ZfZ{E -=}Nn&aGT23F5a`Ldp(pMXp$znl)B{(JRV7+U#GjF$WurZ`97W{JZKgivG~+;HFU3)EU4ojHc2cg1rng -b@!q!d=2V4LCPIF0XeA5&cQaQ!ti~J$s3Y42m#{Aeh;8I44M3d?uSG*#L-jDAwakpA`uRW>iAvkvV53m5+RhHZ1^v7=Vc#)Rbd>v2$Z- -LS#rh>rQ!}LPPgD$d5PAaHyEg%mg6NrOxyj1V!cV&^T4+g9g9v9>-H7t{fxpt7zz68?&{ckg3HK{gBY -{L?D5r%IdSXPol8GB@Y>r7K}v4?DtbPCH(`s*D9Nvf!);NG(Ca-Tbe1$E~_u+LqjDMc}%j5BIiFm*I` -+Sb~z`k!q$_CFkZE&3_R?clT7we{SY~l$GyAyELQdKL>IygSb>)H*oN-4K(GxpVaoF);>@CJZfj=R7y -`zFx2iylt|if66iF-OmOY(w{2ac)KFt*PnsR3Y>+Rz8qZ4py+MAtt-)XkTio&UW)Wu@ZarxaODhI -ItfL71!t<5%UN?QX>;$-oHVjb%1nbBQ(hS!HggzA@h|%!h6+~?Q$N+8uauuyxJlUop!Tbu$2NxNq~%- -l-^8X33t1yGnJ84a7)HbfcH%;mbq9rcUkuW8Lt&E1ZAciyEo6(VICY~87TT2Wsg47GMV3Z?FKHXlKs6 -XEn!DfN9nrA;%_!dF!IRl^7$m>U8{AMQI7?}?a5a+b+4WP*z>|2J7Z0ktcip%Q6cuzmHK<`3r(X38YP -3&?h?a#t5>_6$2s%ZEM5c%bEG5X3Dw3ODIus>H&tE>cMEBfhf|?hx+b3I$>5>kqX5}_25N>1HJGsy3RtCD~qP$^Q#S5pG_2bI2+kD%`xT0IYP)gSuBmmxrZL%p_O5`%myaF$B)CkO9}-P?1MU42vv*09J-M?^EDDftyx -^HC_bOHFeUS)60_x1_~)8RoTaCu08KhDqjHM&9lRxq0!qlzt(ZcRur-+(bfN=IT=~xlugDpjg-eYtKv -Sbz-h4o!v4JG_h7$yQIgp-JjmjJJ}Q(#EG>0z;Mg_!gxRU#l^4v=*PdXlQs{6euXa -8Z+?plZ9#b+a?@+q(^!Q-KDE?p=`YMzG9BZaL)Ts#zH800{Qz=$ZG!G<2_>w7Ge_6`jHmKBJ=DocJ@z -5af37VMr0TQw*(tQrqNFG9uEt@H)EJ(Ji!lETdRV@kl1{*<$+XeF^|Q6V4ExEfET(LcOThiA*DsV}NzoRk*>2UF}ZqFX4RFu5}S)R#HJ&a`jo9+`~x>Bd! -~a+OGw9q^XVEx_dHmB!5Qiy(wzYhj$D}Z<<*b*Ufog=OMm=wCKryfJ-({3ZzQf5fWkYJbF|23Io}|56 -B2%U2!0)O1uy(l?AF&2{$@xNNANYWF(!0s+%Reix=u%rDsuaSYj#* -FJS^X@j_^@;9$aLYH=tEgoGf;h&2-Cy3Ux`@{|5Hc?@y8n8zwHH{z$CME5$B1rsBSC!a_On@R{(D9!aCyQ3 -J{EmL@Hmr|8!H$O?(RasEoy&x(oeAMDQL92L$f#OW3&k{cFt(1!=NZqUFfA -O5j_>&)#2h=6T1cdHhG-fi}tmCVNJ$MYu4e1D9-S^aGHgvC*KsCH;jBK#NofhxWN>&Gc1vJu8P#4{|+ -;lgC%CWi_qJ?W7(3dD1=$^9AIAniW_Cu#FI;P#?EV&2$C;LqdhN13R07-j5_qwU#I&6r{x|_^OfiL4X -$$(ehWS)gr5GNDMyll_8Df+$C8#Q&WdE0;|n!NeM-GduC6uW4@2nt!U(LJ1cXo~m@W22TyW;EN?-@qj -uR1z3`ZomX%=T-@iytxQWEMNVuU2q9hTcvE(bf0MBs)zAFG`*N8pk(25soSUEgw5}8m-T#fFvQW9>5d{AEE2O1e(*MjL%%pu1g#G&q8TvjboB -1YvK{m>xG?d9q(y4Nj(%#g1(atIb#EwVN=%!3rKqZR5+$@p+wUetsVx%!raAGt5kz1HRV7B75&oKxJ; -^XWd2L>myHNOh>~BqDKhMsG6Alrx5Mwd+nagro@13fj4;T7~n*%SAG!tIlT&`(k(C(%UG29tDqs -`57nr&i|!Q+U8^UO6NhMmTYmcaQQu_ejp4yrofSUO+Y`0(DY-m-i$lU86>DKD+eKf(gB!<}i#l&b5F>9%;|R9JCvSj(^Evcl@ -C4klH8>U9Jkx|+s$V+1uI_cWj%~Ip!+{&}Knp0N7n3covf=0(Ia1}^*(OZ~89s)<(JJMB%`bJF;fZg! -Qv`jLqCk=2sxD8}hjXpK?lrhPI%`aJ6^8I|=Ez%eUvzb^dRT95^2iFEsZv+oCwAsB01ud|_vCc4Mhxp -~80SDu6);^o5WppIQ_B-qGX;hl88V2=EWMEtfINz)h?)Jy)OM6}L+sH^_EHbi(Y?0i{4x(}BNQHcG)k -Gyq2HmbLsT1V=BA)!s&>uk^Ex(g$z{LlUdiZs^DLIJw|Y8_@;-6iOIMJn6;xTh3FfSZ)6lf!g~dZm_m -%uvNB3HhbC02e?%EXl(+TxPS9T4YWhwa>->hZod{|Yl?XixZ@uH)99bB$3yyt^z+E74cAsUp^W#$ztN -LmQwOa5@7z+mS1B%&+prrrBqhiB3dNRX8DJZ8LnR-iI3^4Tm1;ZLpq4KG>*Q4K)>jc3pp*Zit4@hFVh -0=vh1XK~+kss9B>A*|_CRBF$QngBvzQnj$EIS@+Lp7nWd#Ed-4O9HBW^9EWfuBY86UKGcN!-s99zB;^ -tLA -d~_J(~L(lP40kUDucW}aSk3DEP_yu5=zoq0FsC&RC2-{8-hR!-nyzfo=4)S{k7X0%1hO<<4)G)ISx8> -F8@$rnWd+eKfZc1wAanigdl%i>7!pH3rQv`g!e0+36nt~v7(Vggyws?yHTz1E)ZA{%svX#Ptd&I~#a6 -rEuMYd1!EDHKU0i!nHjIyi{h*X4KAz2aFmqSfG(XP2#kvF#+2<#cNGKro6Rs^LlK;eA0~Y3vOQ#IQ5Z -Swg#$XUSY>Se!09C3M>(^96e1EfQI8fxO{fVZAQFRyl4}@J0QN#4ero;bnJ -t56}HdE|c_nN^hjG1LVs!57coyci^qyMbqIxs%KLa#6ozq3>rF1OG9?m)8gKI*wkWrG+~MLGj5`L9kM{kg(?YVttLBoEbnx(gxv+9c!0b;MqZE(;x -0H9%%h21smtu@UZFJlQgmsmX=QYI^R^d5`d1#qu?270qEFB_c=O`_<9C)?_8sdBYBL9hkNXh#-)33!2OH9ui_&`O$^w74k9E -c2c*}(Y;n=NZZU?S$qR=NcIK(ReF`9Ko$OO!Ea -fc`qO_hqB;M-whx`Gs|n7h9cGgrs^u!x^Ygp2<2oCyV@RgL|(tmw$4k%>E??{zhDxLKm)ER&?Zmp;c_tqqfrFC#yGP6RQVhhqk$M*gT^hy9=3PoM^N`_HQA)MSZD{k(7%;A!yEkWEI)2U&-wbl -9A%zHWIAHm@MFDJeC@gpMo)RHYR*%EM=l11cE(;rUr)0g5b9*=4?;BWI0>pm2$nx-lK?no_ga~&kOLp -t%EOGPRnM=}Hy}fp1y_>TR(O>c*vqz1{d#OhL=3+BG(>Wq2HeGkO3D8ig#Pc=eGU%Y?@-GvLq_Ghs>1PzauKy03h%r^3xt&4wShCa@T9ue(~a)2IG7WO -QQYhh;CP)L-lKcr&*^2Lc@Wj~%(#MVm$dJ?{|xH&rg<@=Cz?o4`tmM0a7nku?>(G=6PE|cc=J0v4o?c -4_KCWPx5Op?K$^t5*%|ML0gHJoaQf8@b`VG+k>ZUX=B(_ClH+n2)q(+ME`NsZwQy2tc;fO)9{7q#z+QTVl>DL5Q|3E`|WRmEvr~&4Y^4dVs);u%YFT -8{9wZdezrpAd=rc+1b=Ex`D^@kzb75C;Kt49tKq-NuIJ8z|--Glq^bVnpbd1XE197Uqu&28EIkSrTIs -&;z8H*x4xd<*%?1|NP40h|c!qI+!~h4h9qws-e_@NT&An;ME2$3kIxzMNxCQq|t|p7G5EqM&f;Y&yEv -0o-mquZHEpOS~LUS3q!@UQ1yvw+TM6dEYDbRdQQFm-2$^{+*8QwPc(Hu6TW=m+&@k=%Wq^)M?0Adhtd -@YQLWqoDR=Rn)4Y5UG$H|Ih1_y|_sdIQrt<4JGQO#MojH2;fCV -9EYpEpnN+=J;xn#`w@~`YmQlTk9PDX+yxB70~xK|A&!kD6rt_lz$!|?0_55JNCXy*5NP_y^jbEXbS?5 -YE2;@7(7f$zsb!!TyPAJtc09*V23x)L^c^OOpv=(%yfuVZ;oQu`%Gb2nb`0^Ms(!K7G|OpYRbJB^3b1 -C~VrZ#nczl;#8RKoqAfgnK^0D^_RThu6`)HfcQ%3wQa{Q~2zd{X!FM1c2jwLDZ{V@Jhmw+@|-vcn|&6 -E_}%@dhwE4kNdei764>yt>_p*QpXvfbox?N@)fS52f(qA< -Lfu6o}}erFDDe!7p?>XOUYs>CI(Y2-y%hoAL_{$Z$lU1{4VFJk~yNH`$ZygrIf -NDUTKb}bjG#+}5r^7`anCOi--hDt(y#Mv7B^MvlJNn^4!@?qg4OO?%|C*df&0Wh|D=u7^&UjFNHl ->MwO1Tv>q}>z+CB{I3!s#Te)p%k>pO`hTLT1*~ps1ZqiW9ap>h8@OahK8~C>h*1wHPiGLzcd^w_cmSedhZi-;RN@ApKE`tPH6uft2Q3muab;cfSK~zGv -%F^?@4sAZ&mG`iOp?8^+*{v0o_IPqPYrpV*wRVzsoX>sxQ}1}Yt^yKYWgR+Yhva7h|0`l?XaoF`OyJl -?%tjyvztof(;t|V36Qk3%c%UQ`&2w2A^S{!#+^8kZw{Z%ly$#wjU^` -AT`snJn{(JDpAels6o;yb&?{K$^K^aI1GorCG -hTng`C~IKM}JbrvL#4qE)X`%NGAoPm>PkywqT>s1FrOvuC7uxl&tUF2qgh?`8_q3lT*+W_9Ow1*d!qVBo$4;)y(>4&d?24Hvl{_tnh -GQQ)jhd;!XgPU)DV0{>g(<9ReqSDeWN2}`87fMSL4E2<9)?4VH{0oVV0i#WUcq}Jb?#mY?dd}Y?l;7A -zgWv@ffQ>_hBw#ygzRtgoG=wc&aZbbN -d=}Jhfyha0nHN-$bzUtu}zie=7*m^*>q$Fvc-{cU@O|4#msdA09QW=)YBN&<9bCz6j%Yue%=hyz)Rfe -QbzTz9Y*WcgV{BKZ80|XQR000O8`*~JV%jaiiJOcm#-39;vApigXaA|NaUv_0~WN&gWV`yP=WMyAWpXZXd6iUMZ`(K!eD|+d#4irux^asD*9!_%R_xTMzhK!Y@?_A`$|g#Y21zAQfBg;RuUu*ac -Y2bRJHy%8)i#@#AL{j=h7%eMjnqO>Y%(V4Xl#BX~;=T*W6u3d$ -zww_w?Ep@+q`3n>m(>oL?Me~sBXwHu93upUEVxzs4>k?(Q-0k0p5RHZXMKh^3Ru=SupwN>yG^_m9=tK -RmO3AeqwhFH0mYJN%{VRk$P-RL=g(l0HbbERj;YsN1qp`pjCX;y;LZ!}7PEhUH7VhZb(_~2_c2G)Btl -6TPS-Dm+1$ZP=){aRy+J%_go}C&5A<01q4GidOcOQr)&cod=Y#k!>snb2)c3^B1dfgH}=tnnq0eB116 -)AMX9+91k7Mv^1Na~t)3-9p)LKOOnv7$9o={PS{8w|*$pTouX>2g8QqnV_vi6)v)b#>#HuQmHPTKf3y;_Oc!SwynU9g<{+s4 -qRoi^QfktFmlg%%`$4`dGNfilLnsb`!IspAOPyHDNj-G}byyDrf(LFC#){mJ8hTq?~*Be$lYPO(n6!a -DXlYu2bA{R=6#b;h~!=Eed{LrDb1QO*i4 -Hn`;D0=uIg$SHW?p(D6;4Stva{2><}YBme*>0001RX>c!Jc4cm4Z*nhVXkl -_>WppoNXkl_>X>)XPX<~JBX>V>WaCzlfZFAa468`RA(HGa%!7aw%IJa?LwhG(ekl5hkrBc4FluAegOd -=#!Gla0eexDu*kdVNJTx$2~s8nobdY+!?e)?s`$H(~}x~$(cTXfsJs<*mzy1Z>)eV{F}$4AH18w+ZOa -wL7*qpQFbBo*BSze~@v@qIFx`O>j<5R&6b;cIdrQ$AWQZTeD6th^Rqg%?akNWqYF4kqMVLMz9fiUh0- -e1)&!GziTX0MmUCM&nK>Y%N?GEDT~+l^rtHbOBXkO@*r>RWB}H0wPzuOf}D=4$CU)2qnU=!i`RH75F- -ogBgxlP{mgmA-c1}FLW=xQ79*LLfD}u9nk$kj{`qGKTPGxL1>2yw%RZhf>bcnb8PH2ErJ<2wojpOrHb -HT2u-%{o(3V-PXbpC7d$lcT^xsZtwlIdOB>#_`gAK4c1kzG7a>k_KO1<`!Qxx#2ww@Z6-{|ejn|fH@J -71vMyL@0-dOiF35j{u{Z)htBXm}-F15voC4#RDw&wlAn^Rs`#HJVI!5iUnWjWUb>yx@9eFHm&?ePl$a -nLzvMFT-IC5AJv1O&*^$7b(cyWgW)>w!MjI-Odp_wX9Bury}jzX(ZArl6opI8|u-dV!4t;I7`edWRLY -%jRvf_rSYvG<&Ujw@Zz7hiY_R>-3se{o7iH?)#nlcDG(8>I$(mF&i__$SV^M0XYd^NmpoFdMY~l1Kg{ -yLC>YJ{Z+utj*ws!*$9#8VlN+hfj=_m#ger_Uy~e?ALtUzLkaGHeHrfpW$oV*Nbi(O^r8wO+yCOh1zj -v}$RVH`UJ!&Ox(t+N(YqSfR?e!`%1QNkrApNAVr1Kg>aFW1CY9r(qhT0Ks4QlVo+BuJW6yE;6zct}-b -V}W>R7$D)#%j!ZM8es^-hVR894E_Zgrdgsh5su%O{q6xNlz7ZmsKPx7TT2_Iu5C%j(qdqqx@oCEV2hp -|edpW}c{>B_Gv38k{|^o~6{^T=Gz^{&?ys8(7Icx`lxpy-01~vU&3&SG}2dg7Qf2QN^F7O6h&y?B=TF -@yE3qsU13xjvUWcs3K!Kyk4ds1VatQXQ8Aj1cT4Q(a7q?f30@^=ACt2>(#8@yH36Pu6=t=Z>d^2J2wK -|ey46d)Vmm~lrJvUW39Fss#ML;U9;7^>)&O8o*$WcR}9IrBx;al6_oIXv3~v_XW7%KM96D<;F-7{3C- -o>x0&!Aq>hlwfib3oQ^ns4kx(#60Nn`P=E*{_xEf_`Ws4VDIJu+up{$5Hzp$?cr;nQwsLJMH_DYn1IVBb3nBLSvFk)>u!x1RCe{;Vk5e7^n7f -+BkYKMU&!LZh}^pX;kuU>9YCqtsH3JuuoLJfN|6I`4H`jE)>_?iUu}__3YU*GmdJ_+!x69jwe7MuHJB -z7LZ67pgD}{&Cs()>`-0`NA39NxYm7dt{7MnC|Pwh5J#ZZff4C! -qL&4wb-0Obw83OB_-LH=wqh}zAB_CDD1>WFTrw)6|1D4WX_K=U$H -Hyd3cm?gCB$wuoyKmL4*RkIA9OYxgsKu;+|8jRQ^tC7K*j%j47cu!N5RU!V -hMnvBf;fwhm3rL4;cYSl}S3@x=?eR;7gl*sd|sN)M!uzYIZV&QTKvgiF#GIIEu2~(gk -&{56XkY#(>aZM=^Is(BxiN{KKpGn)9_6KvpT+w9aX*tE-mT>0oZm0SlHRe>#WpGuO5@;sZl{XEG#JlW -k&3z}gvEA13##Y?bf~wgR#@`BEGF~i}SIeK&U2ePG?dmu;?|pkTV2@n{_oN$lgSa!E^ggbfS>m+K&Xc -ACQ99Lx5DjIhG#R)kSHGglH|LyAPVjRmQnE^be(r{>UCoj8gQ!MEHNZ!A0TwbN%ec&=heMa;j3wxV1} -tjK_;qCkU^ZnO1RE_+`fKG=GF>NRxSW5xI?qk_7w1T-g@o{lkuB0sYvAf9Gg@2M`)oUrOX?4uA4ckY= -z~d}`)Ke8sZ-Ah`;mI*`@(^w{?Pe8QV%Nsd!)XsJe|~+mHz^%qnDW7D+Br%hWFJviSG`I9Z63figs>OYyA&GH7G+fHjiyspU+mH{7ge|Y4yW(K -uy>yQ#c;p53fEeIc-tWup_8tw(c^oc33o893Lt--`Y8#!6xvOSkW9zlA`r_xD_^6%d#|H-g^*jZY4U_ -}H&ntTcU-x~p`(uJ{8}ebnm;a}MU9p$`qro1bxBKr12SlFwoVZ3F6nQV>@VOY8XcBugS?fS7(F!T}3k -71PHodIIwJ9;dZ&Tkc8XKi)Nsj8vmpSnPoJ1((X{uMYXsYk0sa`=KKCkY2C|yV1CS>$;XCQCpB6erv( -TCVrrI|_Ir{3THu=`X#^#5PP^G)jg{dZQ|KlvuL`wo>Tio0%4iJF&pZ?d1(DgFUaO9KQH000080Q-4X -Q^o1<1$P7h0RIjE04V?f0B~t=FJE?LZe(wAFJow7a%5$6FJow7a&u*LXL4_KaBy;OVr6nJaCy~LZENF -35dQ98F%XV(sH56<6q@D&bsRfq>e$9F9HA7kmPYn+>s_G1&XC+zki`2&bQYr$GXJ?*$=9w2=T~$ -Bu(0>|^VKnUZ$MZfsj3&L`pvL#AaRrMF!bI~mmrOuUg$Ufhv-*<@!RYsP8%rKAt26|HqqGo^kJs3T4k -fpoA|LS;h1#J86@jh5B>yp56R;f~z;Y!G{nR_9(;oy3Q>8O|ppjV&VU?ta=T|Z9uyzSuwXx9bT|?*g_ -Gf~qboZbu1k9YTff*XquNY>T^pv!kuVPBGX|8`E3&1ne-(kwdTJtU305P;+6-*PmQ8Le;q!N6knNFN) -kwyfgBKtqJ-rYhCHh8f1uKNuU=)iMA&@V88!o<1mJ8^PU*KDJHtUWTFL&fiO4BLTFGJ$n9kpNxl$ -B=ECPbV7p8K=jSBmI93`fJoi(@|+F2?=&90rpabYa?^Erz}2s5^t{ayFgJ`yRj?aWAnGvr6Pb;)o!(i -j)k|GA>M(`U{oU5J3^bhP@({IY3Sw1&l^ymt0gsq2xNvmqJQG?|?@jm2JVB=F~gXeP`7rl(N1pYWl!y -!A~7>V)DD763i)lz$1bl2&Hrj9+a`p^|?#2?N6Of`=;}`-2qt6$x`<~fBg8)rE=A1L|SdZLm5*qQ`Ow -)Oto5Zxt?2UqWnDAFm2H8=!DeC{d#1aqn)P4Mxzl3X3XCDq*c7jj+#e~G_y3aRG~e$$|cTGX -_1?MGZt>)wx*#N5+8Cf=1e2hc5Wq1Rr4bck{AeqADwzvr{SQUmyuHQYn{_%KV-(82G2oP@2=SbrT;kN -CM1_u!Z|ij*`hE1TwX0sAcIog7_@@9Q9s7?&5ihZ-`oADi8i+0U*$InM53trcP@D3Y3g^h%&&FG%rDOk4777nUO!S)nIss$ZW(okuZw7r!&qxRpmun#ZO&6znTCbph|^mE+$Ztypg`S&kq5;o=; -pevGo0nt**8mABNDI}J@Ek33M+@*YnX{&T-}y%hUnz=V0n0=YR{AcvxzI)0&bcz)|Yq@R(kjH(yl{G{ -mblZiXs_&J3N%nREfr_lCpZwvc{H7Ow4rM_uTIsWe3;+=c}>lgRl?Z2Pj^iHAHYw`(k6|b8gKbSNyi} -=`ribZ_>v8$wz3zGx5_dr3izuE$|P-M@doOD_3bl&y6-GX4mtk7J2LJ%}6951t0001RX>c!Jc4cm4Z*nhVXkl_>WppoNXkl`5Wpr?IZ(?O~ -E^v93S8H?HN)r9~8G!_bR6i^l|ukJf+EaFr3bZ^l@@M_=8T!o}Zo5VlC)e%av4KM1wdGBvZPI|CCYW(XEn?LZ`U#*Pc -#AMuEtLpT(Mdh&pFm4PzxU39(P&QABYfG{qrhM^r<8Dzen~5`m-5CRft;G8wBBUv&~|^973OCf4@c*T -Tz0K+D2td}+$ltSc#CYMoJI(4;q=P8TYUiaXSgBAT&mN;8oP{U`z($skslmdhORHPU7eCq!4CexMr}t -+d=wo8VzaSxh%YwvLsNn6C@`^P-sV<6XQ4p%NfK8p);hbiwF`S_n$xFnfaMZ>flL@;yab1TwYufmBAG -3jP{vyx+uu3=2NWRe*RotW4-lx&`_3^p++fMJV)HD4}8gCSOL<$K#3gg-Qi^DJ6WCt}7@hRqR%&b^R%&<|`;T-d{ICh93yP2~y?yB%g`kQwAgPSnHIjorpb&vRq5NnmC{I -P0M=fZ8EPWX-F0XGF)SrH(Fa6o9KV5u%3t6NBw@9$nBsWPUp%_tVPh|xhhk~o&Y;cJe{g30xc=46JAB -`8&;e1Y`>lyXwraxPZ2e*^{jBajcH`nu_gX9Xag_yxS2J#N#5Gpq@kVKPsGD>aeUyvV~3=Qug_q2ct?Pm<61*%>m${S?J3G|g)J72ckO8tvo=m7~hb;~O0>AvMo -XUT0@#e&nSf4e-)jWPB%dBVPU&?1aueBW}#g>)B#(yKonmvmw4rKGA#XIRE(FUI!v1*OlN0*KMRC3R| -dLwQ@rlp?=>_VddLVXl1ko?kqqtgdV_4->*_F+*c#B-`nVgqLQ7az -2HET?P21T;*||!oUCJT!F^L(vK<)n#ntfJl{@Ggad>kjPe9W7_h#SHQjZHIF8cmglCh9g9Au?Efb{iX -)XOV1u@e&4?P-|nXWSC9IYX%77o2PRl^gnXf!L^<>=L7C7wwi}$+@XC*J?0by8g|QN?XUMK-LIqWD)(_L9XpI0Mp;5P0{+C+SzOD{ml;=M$T04bDKBmDd2r;& -e=Tcegy}h?vgdOpm!P5qdeCxHlcQq9@ma(1W5u;XxTdp_OLW-)ffV8%?WY-y}O;^QHedn;5UR!x()Y>g%`=`SA~sKMrH8Uz*Gbo -TBzXqSUV>I_2dZtreS!tN-Qpd(X5V~wUUUnzJ88s=19^j_q!ZiTt}hl$z=*Ai8-`BwBND4kQ|mb~jmB -IB<40jJN5fwZ>V-QPp{{@%8h=FJe-rscRS_86yZ`WM7+0OG-Bdc4l!Mk(iz(@_PH!FXJ|ZJA# -QFl^n*fUyCZ8v=3d^xfZ2kZ{$tTjTxxFH!5Ro|T(|^xK{mDBiV59)4rXJ!Aegb?*-B4c5Yc;Ldy!7qIEa8= -m=QCWQP0Nif^<5v*P=t|o+t51YEO$36MsNY|b~ia(hDhZ^4fNW((s;t1`Q`L45WYFc7jDs5XTTs?fet -$OJ@j&m}g9TDx`ZLccsdg<#qK_@@3nDR5T`y4`7S~IEnV9RD}#)kV+wN?Lz5J`PS^Fj{TOdS=g_I}96 -=is@KtWcCdjqy+!~1`YrKDF6TfaA|NaUv_ -0~WN&gWV`yP=WMyZfA3JVRU6}VPj}%Ze=cTd6if1Z`(Ey{qDcw(0quqx?0>~z?vYyoYZNJB~ -D;FD1ss|Xz65gr9_XUoO*x#9x2JclXXUc*2lZYyLWew?$OcV4_$UY_xg0xyXy9bUAnxvyZVz}@I7iA( -P+kWXXI4oLeZ5@n53ml|09(Wkv3C`VT&5IYcH2h!t)a^Sm`+%(kZzE81V12v$2>nOj%asG8Rti+~TX5 -YZJZznC`VH?Xh9uIHAchAnso!jK`WUan;OG^Xi1!A3y#Mh=cACb(Erk_q8-&%VxnzS;>{oospBmY16b -PXRr|63iF-rrJ5R<(K|whj-00ZrJL!zsvp!SIWfX4Jxi%!CaY8TjKt1qsSAolPKFa{OL!3BNaV?{+8{ -pDiAm&elv_`$UFSB6O_*AJ{!7mN -@r!FW6XE=zY|#Z>tbnqzyHN^ZBEjb02tuq?e@74O2%(2Ps%rnvl!`>j(x$NEaMvp%G>)xo}9S-UG -Za^Kn?+ix0tB0G;fbJg#_jki?L^NcsDfVO@B6&q|3dl@@$%LJd%9f=+qP-Rgs)S}@!Dw^L5)(xND>7g -HuO2F$PeZ*3Srd9rGNoL*7Cjq@*}?Sfbr&e>{RGKwm4ZO^YtIb>8*1gV@ -ve?e^)r_J`9p(Zdjymp;_(fBH!gIchY}DZegCPoV+RMk6&kqlohYvt6Ctig+e-9y$zFqtS?!uwDrg_y -=2c-qe%7ICj`ctR%g8^CtY=A?tl8E1f(u7)!rQ0y}^M%YPS~$7SyX%>^VSE|f3?Yq%r=Zzg~-K*}i3E -_#gn5LNZlwpMeSGP)Zkx%Ae4G%@!gc5n<>+i8gf#zjn&-zj_zQHU1gWX4g@&+UMCCh{qRBWE|@Cc@%k -EGx`3H@uL#u+0r;@SvVJSYE_=Qt?}(9=Id+_FkUD6WfF!=FDLHEdpy3D$RYIJ -V0)RhmRSN>~597i-2Qd+(c#65->P&#z`ZQ$(QUoo8l1X@GAiBusBs3A!7!Zv$nnCg=3{8*;=T4^5t9X -FB?bc~kdF6?JWZ0G^x;?(v2)l!`n_ekkV13Rp0^)iZeOi`-wt4wuE2lZo!r0~IwCqFq#=tw*r2Q9^X| -2SVtcH`GXA(o{Uym41lL%6Z2%q9ET%B=L)*L1k$te~|)kK+Pc!GT0xJde0h1EXe;|^D)XQ#H3v^C_Xdba~C|A=AstnXv*#;*qY;UE;1gEPqrf<1?-w9ja6}_!Jq9_wRQQXyhlMX))+beaHV5KQM`e56dJ -eID6U^tt0ssK81ONaX0001RX>c!Jc4cm -4Z*nhVXkl_>WppoNZ*6d4bS`jtl~YY`8Zi*P^DCajg-BZpiFzsRVIh#LN&vBoB2JOnoy}S`HnI)r{`- -y>2vw6vtxuje^WJ>f?e_Zz!|`1*!!#L3sA9AH=p>ZH$ceR&Ms**p9pU$_Q{PG=@s(yb`u(kc -5$uv4wECX$4wVNe3l2R@fSREiDn8DgBGEG(c_k$eClQknX5YkRC!8pN(by))ca=1GLu#S@??J$!;A?* -%)`T6{h4I`|e1S6$*>}M#-GXQ<;-?1mUm?n5(G3rfztXP)K?z1QWyZ!b_tEAj$ra`#{z)g6nSoob99- -!B}*J5A|D^T_9d@(KVFd>dwsWyCb-CT0rVXwlX_zt71WJ^hGL#4Po!7+^dKcI2Mmdc0XTbh-Yg&#>-_ -q%hm&q^_5{S9q6bew>u?Sn7gt<`KuUBTaTw-%Lw+4`$&y>clPt&!sob&kHLurkfvP|y4#aEDNglX7f!yrs|JtE4`nlwliLPbA#>k-1zN=Fe`dd=JEniF=I`vA+LMa{{5Cg(ALiwpzX4E70|XQR000O -8`*~JVy+WEXXafKMKL-E+A^-pYaA|NaUv_0~WN&gWV`yP=WMyf&c5WV -|X4C0FeING?o1vU)|tfJVd5r09lQRHOM5@i#mNQ0yjsK0(6CD~3|8`$MZnwj@F^JX~R?)gIx!>93#Cg -W&0ONR6?nMcobA-3D;(sIXXZp2n7CMnVxCt<13KZTSm&}K_1Y(eJ`I#$97L_YFYF7=)p(mA;^9EhKBX -H&N4Fcn3qM9Q4d%Hr4TwW)tVObcxqduljyJflrjGyZ-RGoEW-;i{dZUUW@^ySu+ZaW*`lP)dq@tfiP+ -ZeDrTR1#_BjM$V;o1VS?0t?ZUnIE(ea%)6EH-rowIZcbo?X+s^hcr@b3^SEiDL0&x)wz2^V)s<(l2WF -~@J!f-9zr-`D*Hnl;0v9Jyz-_}WhlpI?YJrILBprniYDA5Q+ncx8&tC>H&UbkPejU<<-{!Qz0K}UN{x -IXNt+0bH0Wz}?}-ce{oSwJSXk&&FgV=SGWJOu>M`f@M>qE#c#WhhlIVsxAcM0KoGQ&osG4>M?ePIZ6# -9lSq7A3g^1PINFd&w`kC)%(t0jfA7y2H?aX4GP+#?oB!;AhkOrqFJ0b*2Tm~s^o2f=h0N8f-5AI6jM^ -2KLA8ZT$VB%#qfrjQn4yc|cXNf^^&6))z=Fd#}eKS=DwJSXx|;vAHPhLRa~Oi0_5{(|;eBq%f5u~&{Z -r(9s7h~bIstN+zQsnxcw7m$;n_at&s+V$zp2+R(q@3-!P<<#FkhCU7Y^m%|FrhZ7YAOU2^A~(FFzRj+x^?_+di>{VN)-qUZSWog>7as4R9wi^I -E6Tk9}x1xt5A@dGldJ&E^B8&3-9BlY6$ue)_JT~Fta!% -3&pNhPzkF*mT@c{Z#IYzxdrPV2S6(pj&2mdm`g`(sP2pWZvFKGhMy5paEqg`-tqjqJXjsk!$t*Ao^ic -D@{bX_EHOku_IC7e9{B{pLBw-OU_*XiYoG8~%>0IT%2M<~cW9x!_}-?Kx -^&d13*}Cc|Xxr4aVg>(IRKs0q6kXNDUR`34Ol?cHy@-CsyJqUD^Dj_K0|XQR000O8`*~JVLXo%E+I -iAtkhF5$pG0Jn~j}zk^19z;y@^?_OPqw0)FioKl9#ru7PJIiS!>`>%KQZj%>@u3xl#qBgtSD+W$VoAbUyPEH|jG6 --{IOyUes@twpAY3=ppGn#U3f02H59vT~gq~To^3=8(JLPz{>j3@QN0e?{;DjMgg~;7K`~YsKFto#o|1 -c{sRlV$V(zNnfh$Vyg?Py@9Xy2Zai}bEM{Tg`t^3lZa=zHx7x3@TdR%3ndb(z^35ti7$p72v6b3&x?I -2Z(;mLRhNKU0-aewJ&8*X-@!JXK?Lg6_eOjnxF4tewDLe9ppLWNF~P!*Cu95*9bT5rlCvId -6;!PdB;5FCw&mU|akA3^a<}S$yi?*({tjge;E!CXDb`i84h$e@2mSl*;~*Q#6Lvz~FWk@RZsYR!RuSaN$A(c`~NQfyW5-buvHlfP5CV -j^B|%m3+(L;Ru^kDQ`^gn1<)WpRip~Qs8*daSMJTGDvm&H$Ge1n7{s&C2ZBnK)Okh-8I@F^A?x#^QSW -<0dZN`}>D=8N$`p7>kDln*iuL4(o+iD}x57>RlGw;Tn$BHa&Eu_=(sup;P)h>@6aWAK2mt$eR#OCs1w -Azd003?e001BW003}la4%nWWo~3|axY_OVRB?;bT4IdV{meBVr6nJaCxm(ZExa65dO}u7$s5#66ZpyO -3kH_N{G4h9dELUPH9zHi#>)_%`V+tl5)Skvwp!in7iDy{sPR-JUjEuGlMTLn;*Dy-+5CQdqZ~`xNtX~ -4L`ye-^oWm+QSa?udjb0h(>n@25Abu_0`~`M)iboUdd -1jqxf=Xq-yKVTe1L5nEDImJY6Zifj2-I8ZZ*(d$X3JsGq1w`BE{pw=5+J5wuLqGbMHdd%1&%2~Zewox ->9m2AGI{l#1dvGUmzNnsy?la%|QH)kc>bzMDS47&T=I?@*wkYUaCT$|DLM^9Y4TU>dg8rq>lyBb!K2B -myHz@EN-|fSk_l-A|}AS>Vd)m$wy&U62ae%-H;?n_C;$p``baQa4l?=`>PMRT={q8SI+4r!NFZg+7yv -V2QBPnC#LnA&@V8@)qxB4+|JfAK}x$_XpF(;|=)|({xJx%aQnIn{ooCsuUAuPl*%2#Px^oDPnNvjlIR -A&3@!9rfv|xXy(HJ<^zA>4Ijn>ALbAKd=|J4K!8|MY{_gQ`9N_(kQ+%%#%LH%bzb@-wO3PuX$))jgw* -B;IVlPljmUQYO+8GdSY`E6NJZ!k@C1Z3U3gZUv_tAU=`NvE{q2(IBc}^L_sNNgzfx*JK;a{v5cDXOQg -z`@YGYj|1B&fk_4~ay{hxY$fQNuA=N@pU_pe+cm#tQ!|58l~HU{5CbNfCl#M{`nmjhR^O*a}BHC53juf|p@bXdamTDx-CzH)VAD{qJEg2`96AcT5Zh -v#Z|K)o^-h`!KI;EogWOzmNXGuhVL^qLgV3u8wM@w`;#>=U9M?i;L_t8>^awofu8hy0GgPbhO3|UI5z -?xl~>zQBo&HhUx|s1*3VI&ouH#s{xtj>D~%et_1(gmMH?}8=1oOnp6TB|K3r!y-Qh5X9Guj!pIDY$0& -?lS|mgg?%^Wmkbd?h#stEU(*_R1w#GE+(((|iFv%KpJCy>9itVF!w4K{83F?9k>^KLUV}zscoeGEi3o -V7z-dp(N&%+r_Qg(b42tT|Kjx&7k(cW;wz?;m+-pF(B-`4tEzrZ2(QN-<2jUFqi)A>G#Qym^5y{eiIg -ZaRg^PM%EivC*@_&D?3!Sugfe>_VsI?YJzPe>?XL>tghzpbw7K4Cr-7>C6&`ifUtRM6~}ZK%d0Mblu~ -ol5<=Aye>cg$!IR->zh -SztQR=I?evTc>xv*w!xRwD>OSL&Rl;Eu)OzmV5N5@f)yo0v?q3JMkgrc44tG??_F_O&$+%n*fG_3#;e -z>?oSZoV*M`nhI(Yw(fygetggzlI^s9Uuc)UcLHnmtGZa!ejm%ZnAkX|D4=-k~LKFCp$<6Jk)09m;^0 -xLO;JCg&^UtMnG87|5j5j(r&cK^n=r?+oJ%1VS>n-$}lR4aRVBq-e#+l9*;lw=~d92hd{=}31;lIL^w -+QHp=Ip+Z*=Y7Ky$`*`7vdV$K*fDNJXKy{@oLyU?R5j<(aNYG4-@)2sv74#Kl&F?O9KQH000080Q-4X -Q^5*gq7()I01hbt02}}S0B~t=FJE?LZe(wAFJow7a%5$6FJ*IMb8RkgdF`2PQ{qSv$KUfQy4DwJ6>t! -q*SZ(TqO$T-3D%bT#u~yzQX3L#65Qgh?z>+%Bny#_g2?J!30mlM|9fV79{N||=!k#8dGoT>g?6ja>>5 -otZ}%G4kl=fiI)cGiLQmQEwksTHcq0k64-@Y%+i^tJQ}clqE|^3BG3q -KXG7ZF!yF)3Kx_d+5R#-CL#dgj{fiz>L=dFw&v6{b4NHG8g(Gm#E)`#*}Z|b^l_wcDP5^>HvQSqu}u` -WZBx3w1mVM!+Whe-7}Cj+NtjcseEh!Et`*e3nQk%Q*a^z8b7Y)l!T`^=s7sJ8Y) -s|*%Yj1Is)5WgdJn<@ed4$6a9(X2!;}7zO-ge8Y7@Dd}|G^dN%{8cW>caUDxZ~!R;CPVy4lG>$i+#Po -|EIfpJz!xWOC;jsZGNy>W$#`gyC}8r;(8FIt0c(=gzor$Zh3b$!rk+_dXD^l$Wj&uC@=3@M`&Q!=?E| -4OliAtxSYawG#`JN|O&x467M4$v9FSUgBvW>ea@qd?#hna+1wL&tOP7xP8QDxqg!Ti|*@8h$Dp_YUs9 -jn%~{tMyIerZ=-`DCN;fxIY|R0t4k2wuPZD9M4l}IQRU3Tsw8t=Cpi1m&q4$d4L-OTasST?0(tu5;;n -xEagogE8Q9HWn48nIvrt-ZC4rrZlYA`>ib=zZNQ&!lFFxZu{Qt?D~Bg8S4rhKTa|1UYs-@^AL;UO8|m -`AP9vSh_otCg<8=kn6?k2NbOl~lBwdl$6-ig*btTf3cwLEfC04v;+NV*}fGf8LiI+JuJuN#qW#Op?+8}T|xI -?3xK=_IeSNN4dni*%NiI$fMbW3V`k>!&np4DjYm$C{eD-qTTzuR{7c=k1!7A(ooUjqw6Sxm(h!2Y&XipEG|}|$Y+i&^-5~tQA%9Fa-yXa`$+1en!)Qd*(0>XKa%~}KD1m$tFV1=34QS -%+@*;sLPm?u4WfsB*KT(2kU!c@IzxCn#(u){)w%2RzBJqD@%ORm2;aX8#L$t)pJ7=G)5}?|UZIyIOr% -nDD{p%EG_3VF#()Vk7V6HCZmhr9s5dpPn2_hEtM8f1^Nd!=Yo9iq9O2&2#63A4Hz-TKJpLz@Y8gE(r) -iDuuL`fds|sH@P=zo44=Vg4TUg<>bo{+u;aOW);W=AW;aOW);W=AW;aOW);W=AW;aOW);W=AW;aOW); -W=AW;aOW);W=AW;aOW);W=AW;aS_N!mkcxa-rv{`Y!coj~kUK^t+J4ft%PO|Jx8_dlXp~{a1^-ONyT7 -gu@}*-mAL?8Ko!4=-|6-$rbN7QH5Nla6le^`81+?q)^Qi9^H4(1KK*Nt=*H!z!oE%4K7N+w_2iOoZ{A -R>g3punJ#I*i4h{gLt%!LnMQXL@wOwqul=1#FRDx-fw*5^vaxf5Jr3g`$Et<0Bb%$cl-5Leo-cp-CmdV4zU|w7xoTAqYK!3ALz;6b;Vo;xg -d5Y??9t5Af@a4Ss-14*tS5L&+SH}i;A){k#5X+dm{bX-;PMXim7;nq}TeJKYT2#*;Wr9iyJXt=iy^1X -8U;f_<^MF4@vP*z8#W&e9L$Mv|>PX&&=)1hKGOJxc|3@3m%Ofi>c!Jc4cm4Z*nh -VXkl_>WppoPbz^F9aB^>AWpXZXd97A|Z`w!@{hv=UYE%_;gbS@sx+d!B2#|!9U&2OJR7KWe7TBxVMZ4 ->?^wZy4+Zb%1*Xn9T3f_J5X6DW8tS>H3KXlu@?+vNnYj=lUmu~x`_6ItHy{KG}zv48~VksnxsIBIlgr -T+iCZdQnlwMMZwMJzJ?MYz;kRATaBemh0)Pn2@4&aB}nviWcme%W7ijY~Mq`|A+D4o23n8r${>!Ie%@ -;S}63FEOVrXAO23s);k)pm`VZ{Pk2ij(jLwW5e74r^$_4cE_no@UZk(rPh_t$8}bxX0FBOMp -D<$7&)8r~15)aM3~(mj`4Bb#wNi$nq>Mc^P9bSLcxShQ|?{ht5mqQC3;!>;Gi-DpBB8n-5XuRZOzC -N!Q-#v`xWAo4ihN$kWtC2}tD0+ee7CFk5S5t%ggN0iqfz-Ue@_RLY{kPCDaF&vT2_NQtXDY;DYg~?vf -8xjkW%GT+N7U&&x-%)kJvafF)O&>Pu^hXmQCO=0gfd}(Du>kd5q?E4Htuod@2QAoMy>2$IE}L(znnY6 -%E9V~S4m&?ML@p|og;r~7a2cbQQ8jmT##+sUTzVEw1m`SV3CWYU485L(E2a&%8z7-pW;ciqjIex*_gBW6sxoTgGeogtmBg*vRAx5 -Z${I0#%E#mTV6-3z$p!8;bPqyl%(N6SaTG9*nzI;D^agHKlav8JbS@ZX7!z7}3u}^ft`8SR8cFMqE~N)B(&rxKbb -Ae7Me8+@NO38@Wlkg;pxx{3M=|eH;vl(Sh-kVGESIPIB#oLKeHLA)UC%UiE;sR$#zrE0Vk`424E_G8s -*ZdF1tr)N+Zc&G{@|R$sj|guiTRtZ>P2tsxFBoFv7U=2iWO=J-idI4E>I^OY5@PNH@?np}m$5!V=w{9 -w>MvY6qU=5t|{NB<>7&-XxeMvK$l??%&MC+PXV#J}g`20eD~#0t=N$Ms)A!Z6mFOZ*4QHRrK-a4Vdzt -7GbYc9@na$EW5K9OdnF^>p6o=y>NR>42}E$A27I&UA9<{WKkq4F67!r^DSJ9DIB>uLb=^&8Ht}BeA+|NED1XA&_cU+x_-Cq_lEb^fJ+rGR{=hqSk-;t&dYhFwn;N -9xe#+MLk{F$X(ZmmXyUi0e&9b$x_Ie#%5!(0;`h&J`7+oiy1xucQIu|xU49O+KaS;`ONr#xSpyWTPdC -L`iY)Rq`&vDWrC#8!RsID~O9KQH000080Q-4XQ}jUtd0`j;0O~XV03ZMW0B~t=FJE?LZe(wAFJow7a% -5$6FJ*OOYjS3CWpOTWd6k-Lj}=FfhQIf(NQo~-YM9EXtV^!7D**;iYcQ}4XypBZ8aSAvY5K6I8++Mbz -wtZ;_DW`?T_l(LRAoj+#^sGm=B-=b{#Wil{PFBbd3^TZ;gjbN%l*esAN;NSld-o>Zj~4Bcjf7E|I7aB -;<7wAyxo=kb@}P=-}}qUi~RlJ!>6nLn~R&nvAnt5@2+mjn;diZa9Jlvxj=PJS-P`i|vlRcl)Ws26A3x_(?{CU?4{n#*`*K}oGg5nZ-0jNs;oZ$|$=LVh^Wjr@b8%IUySMx6jeH8?rd(XT{mbE)^FJKk?%#dp) -u*esxgEH^*&RPzf0f~nPhOVCyX)k%{J6W?9dkF&KE1x&zsbpu_iuJr*Sm6YT|V;e`u!i0dITfSzc5lB -9g@7#{l4t>`JJ_Y-5syuO&po|e@0cNaJCpW6J$#M{aA=W?0aIDVaZ_nRX4W2bM+{z|g%Q}+2 -s63mVMmS$C6@5-m^-Mdehr%55lmcO69`0LY`FUq|qKbODXJ3qho0#HRPS{%c=@)xzxZ{RYJRiZr$v>Ebdisr|6e-1JY4-&>-lgc0ZJx!y3bN94NH3wZ>{o^uK@I0qK+P_QEk1h{~<7v5nxV~ZNPxo@} -5Va0>>NwO=UOvw^|Lk9R_~eKGBdFXuNy5)BP*(2h-SP0DT)ak+G5nPF{a-ib1BR5od7U=$=JHeG!)5L --5#Un_Io%@#_~GLxjO}Lk=Ka;-^6<-Nh}Kw3xpU{=%5Zn-`8%fh>+c@e>{J5R(m+@{u?mXGe4gPuVRStrmoEYOY+{wRhOD~rG#d -kh`ObERE#@v&Whc6%e?flh?pP!vQxcB(g4`w9}K)TP_9f`FqMSEl2kOz82 -rX!Va$X+CKj1kWJ>z;_Reuon4mDj^ -$gc@&RuidNf~8sX7ub|P=Ld-c0`rSwVFa(2Us5__6Wfl(kd$Tk(!v1&*YX_3I){4=cOWh7F`h4!b*!e -@?8>~)aU|zEo6c8;@r-bS4>wu=H}BxGzTo1N4W15Q(9Dil0p5svE*y*%vhkhV%T*fn9?1$mkmo`%W1? -(u(L=*^ErA_Xqd=6LKU-*0fD2ur4F#ZNH7`cnBZFFhIxHpGy{?5gc~eyv-IGDKaoXeMu&wC=?=lBhjq -tvTyJFG9lm5PN7vL|{)NzBZq1$4AthB;e0fAt>)<2pFrOoTHXg9KeVKVIJXW;T`Kv?P?WJhi;&KeBA_ -Rue@st&(Uu%*9Z3?U8yp?yD7JJl%6i6G;7W}W(X&_0!LX|Y&u(~{6zWN0m8n<_;t*L`RxX_xoQy`Q37 -K2gJPSwrF_>cmRT;N)_htwH5GLM-~|wGD07p5&INKW$rLLE|;5C%8G(J(w#N|u_zeRv;-ibRSZwz)~5pKFpzkO{3D}-G~cy_n3>6HUSPZD`9sNFbYhcBo7!tU@SPqif0y -VZUtq>R7z;cx@~(EyCb##-tqw+ytHbkIwI5rQS!~)!FX@K22$tIwpELQjc`X)^R2@fk1ZOQhw3S7k{#{Xo*9w*gzsBCIVGuxVjwl-Pj6Z*HD>kWkurp5LUDzzJdpg -U<}|>=H>?s56R6F?ueaZoB087CgVbu)`Va>)=`}lJSxJ*D5aib7c{=46M#j)uB7aLQawU#%0lwYHb1OjvxsTlWT77j#+N|PAykg?x? -ln0RwT_RD_UOiAq)7via(AWz?6IA^yF}TyD8nwA^u$2BrbMsWU<{#@<1V4z|W3vOpC#)}{&cDI~HXe% -94=0oOBc>Zra$s{(+-Y?vd)qt;(hr1sSL6V8o0+bRe^vA9ezN2Om42*e4AZRjVHI!{(-3WHNJb2BOD`I$>u3;f5J4NB9 -|av9n`(j~pczTfA~a_+cVx{@IA)4FOUK8=osFDR1t98(7Cq@2I$f119Vb -+X&4Z3?h6<}eT=D=1Z&68gJS$PK1h)L^;UvPgzy4&4$?`XZjgVI_$OE4vbyym7;wSnova|GdKsHqc=AduC&=E3lj$lU@(G=fu|HATmeFJ%V0kFNQ5ZMyHy -+ohO`b=3Nbz-M{Gbz2v|+NgKBgFHbycz3XD2nKy@|rsVKS8@JQ)Zb&?4^3jZzy&%jCYkx?(7jX#Z>Bu -ggtrsX+0Kaf&vf8o5ya1oF~GKhxEr(tbWpJ|66_MEcG&zPwLD?C(f4qAwz7)u-DGS1`!>?V>&z)=;F$ -!#i;aSI6s)KLo5@#xWJL`+s?Y^wTIsw^BVoaho{>OtC?TXum&$`}lN5)2>AlVMvNxT6`PUhk8yF1%FfvPlVqS|GO@zto#mFvbvC=3R7FK1=eQpGq7f@|%fG+{!RU9?C@9-XN2P2-1jym~ -b&~UCGh}qblsZ^{WWl>#NZQ2uJw?YLg8ZKJxnOIxcC%`P{g*}=2!(p+K8q8j>&}ync7_HrER}>d*oQJ -Fxn(A+&zkl%+Psu0ZoKO;QTv)!_ZK}Q_D;)g`MCDPpdO(3V*P@)VWyNfoK7?U3jcoEk3N|CY?P0-$#f -$8eG68&5SDJlLZ&bjcHOyxAVIxQ&V`pHK^=x1wB+LmSrVlKK4NR-ckpgRX`8Bt^DU($eK5vf1=SAHju -LV6~f`u(lcBwY08!xmc32b51DYk!zKvp%2pAq(g1k|J45Io79f~qX2bb>-8C>hGSDP@J=%48i{?ARcO -5B5D~kr9G2B0+4RQZ#R|Ed-Vsf&i>#Ow$O!6Ct{8IVlMoekRXH@HDkhmf -_prjJn^8+HC+$d_b4#FZUSW)e@IL)kl3fp~n3TmYxnGZ5k*Mlz;1gtMVfI!{979K7KX|S%QCq~tmagB -UHt1>!~E2=r8^noh7B4L7t>(M>QMJ$FiMrHtHl)*_5B%@9Vs*_^Vb+INPrWos*cxWN*BG??MLJ&NoDj -LNw5~K?Hq1X;Re;gofoLQMCPNSYF%nJPq)@hcDQ3%&#E~Qw?aFz#Q!_n@?_4E{C@aS?r;AZ5jY16i4@ -X-30q&;Zh2I)#ZAc*G|z;0YDj@TnXx1yea^+H11W~YhqG?i>iAj2GE#~F#(6vd{^_?yLwDcGz%QR+`F -44chH8Nq)v6=XTu&OydpolNgDCnGuQE$KuoG~Njh3cym^#m+1j+13AIHHjhzbm=3KIC -LZf|4QcMt@hs;kc|Eac=ClOQ1nhzsICRMCbscHZ8xM_F2+us~r^hW>?+?2#F6$-eR>AsG%AZOP;nM-> -ey3Zt5lkN$-8YM%=}v@BAvQQKVQaLZPqwc4d3#L(C>85mUnIUTr}a)l~!nU+nm$!KnWkW%6cWft2&7r0Q3oyNBV??jg%9CA-9V+FWmE9Ql`Q}Wj{ta83|%8e0UF*cbGf@9B -O}i{$tRwIW@!8ta@61iAY#kNz!;-8nLGXw^oD~TR#*N!~XBuQ^76nbQpyL&^ -gn~v*Q2E!keT6irL_wJZ0UqkJpqvWw;xI!a+J_Mbkk83pp1q=ff?NfK2oR+G@7S@<0NiJVGDdMr~w0PEml5WRO~91WVg`4&<#(^^oPbQSf6VDg~$kln2>Fgbd@c97 -5y+*9(F}AtfgMmCYthKrAQ;0itD0)=$=rHVD}ccxXR?^Q;_V7}eGF@vE=@hZ0FAYd96QBbgl(p0sLYmIgz!hipE2P+^l{^QM#V -(WIc?0isCUS4?>ldu)x54DVoR+GK1A=$ZgSRLaiMs?`&ba@M@=h6-PU}#YFJ15ER(NINgFz4!U;JswfY -})9SS23|kCg}T#lDk8c;EvFHYl0(yKFXm4!BLJxf^JFxT7l1pX#mHrT%5q-!9! -0XX~*RQM=~d}F$c03sZ8R=?Nke+HRYy+BiBs6fNcWTI8p1Fy^LmK=F`OGQQ44(d1_`RN@qIFix0D9q^ -Kl@nWF$A3x&_{YwKKZ5;0+P26+O|Ajs+~lC`hZu?a9Z~-% -Uc#9oN(Y5qm`x^Eb+F`>`dciXCULKdy?Rci@}XlM04)8Ig=&YOLfpEfk#igM-Lb9l-N8)X(9|yz&8`r -Ayz5Cl6EfZ$gnF5Y-bWlR(+Yi#|R#d9&fu9H3I@+2{9^*tq!u!>-I1~<#nZk0t8cs+vXQL_~s#~6UU?*m$RrTAL@f?g2lxA -9`MO3}QXA5$VC6yO~#TW^{esQ|&+?4Fe;NlU?>>`Z|uYXYNPXuGk?U?a# -~5VWW*!`z1AG^j0$&4@227?u^3VWwcStJ^;!JJlaJ{wQW4)L7Nl9?3>f+SExo&;BGOHg4K*X-?K?;e+ -5jtJOs13G%0CwBL -lmgsB3p1aYSnACJbCk&*Z}rf|8$n*6(&FK;bUF5*du6m!q%iYXc=*AB3}p21+uDoMJ8Nx@)MN5|*_MZ -3CrSxW_+R0ISl~E01=7_Ai~x@|87AwcMXD@pd5dXJ?d+aq3*TI%}vMP&P32p$_G@zqDfiX*!L3D;=RbhYShC@>l<8;?9RqRF~-U`(dR_kRO_VZxN(a%htpoQ&t_)Q2X- -b$H0xuMhNQP4z=RqX}i{Qz;I4X{c}JGe3x+=R-9-8m2KB+W{In@5?&GhvUdJf5{v=@zXWY+ -_JUh=F>T#z-$4)$=E_NUJA>uOc3PAI4Hitq=Z{s;T62*1= -h*3l?E%;5Z={N7IyKxG3+!@oZf%Ru>09P0;b-wA*B$=Ba> -)O;NCVX9C~ya=^^tBqx-aeR)Z@OVH130N9&t=nxiWPdm4xGrMz4o+Qnl0L6Lx3k$sUhk2`I@FSu*)ZDE{A8%45!_`e -5gNo8ck_DiU!kUK>hJ7dZsI}?N*x>8>X3+YpI2z$mC4YQE7fir9@!tnljje)-iOXrI(0&Lzk% -2T)uy6MXtJD3w}*!io`lZ-Fc$fx$z@SDrb_21r(x~cmhPzG({xE=c3?_Rtl6TPd9_k9L-JT=tR0hZv~ -6r|b5?5D5J?V1XlMYxQ}=;b!r$Ee%GSX`pMu`;wgY09jHQn74S#fV+a%tCbWIA~u*b~cq6FtkbCP-oy -Mgxj03Zr|!x+(usd8LYstW|f(zns#>ukB8aQZ^u!Z;46izde#Hw@LySRav2Z4dMWp4C3f3 -GQ$37JX*Xo?1!VpbdV%8oW&H(51h62on0cLJJb44`l -)9p)$sHsc)QbLMI_|??3f=_jkQN?|Vec1eR-u2Ghs?xJ25mR$)y^LAlB{!aDX_YgVWq{c9f4q*7k?Yt -Iq93H$lZd&t3dk$UYsMy(XtMruB$MLSQ@cqV(#F$FNExoc+nkT6(A&++3sM~HP)JFKD;J?F|_qSIuOP -G*mTajVtJlyGiu-ZSi6CXRK1ZGqV+C`~1nPMUTctKwj9j;pYKI`5#mw2KH%3Ag81I1VbLVE5%h-JM^# -e#m_$*X8(lxy5SqFz&4XqIV$g>r;`3o~Bxq{IO8WzOtHbaM#^)d~*##3B9hw`fBBaV~ -Iqm7U#=CdIX;&+95uLZh*z0%!bXMB3vmH5j~G8w1lcvOFiK=`KvkO&7bmF=DQM*7HOp9?9O)3F^N;>x`!BA2@8-Sj`FD5Q=a2V)y?^uK_4d)jt -Gn&~?e?dK|9$`Z^^5rZ`|p2zbN}+iyNAc^7Ma{bzUY-oAYC!`=Ti?ES-=xAE)a4-b$25(95vJUq_7zkIxV@$T-`_RY_Z_|K1e@yJ*|{2Y(^?%np -QkAA(S^Bgh6zr6f|?aAxc+cU0xyFI&md-wSFyI1o~K6-fb?(zPcALBKD_4e1ljlXQ$pI$uvZTsT>^geUu~b>y^Wb}f4qBh_ZV;U#gE^-zJD1ve -|rD&?#bENz4e+UtLwZ&UKzIgNQ=a_g5@TZTTee~~f$CD4Ae){zJUzq -(TPoIDG@t0q2pM3sod$N7;tL?iNf4_@uetCBv61BYu75U-k|F1oK{qW|mzHi*}j|KhRcK_}6=HcD#_S55i$ -WBP#KkaUe@{j$!4WWPe!R^*kT=e42-(JTSd>OZYa{p}%{mJWxhsWFP!-u!;xb#m?;@&-{w7;Lqp0@3) -FXJ!&)qmT^pZ($g(Wtz469a$z)kn|&j7I$Dk9TighFHG%=Jnmwv=AV~cQ -Re{uiQi#Pv03{WV^i^rJ$7je}C?-qCd_~zyP>$vcLeE06%55Ilyr=NcM;O^CrAH4hNw#7?H|;fj}(8(?YREb`e^*ArF{LV*ZHTk=eqth&N3=D?scp`oxP3X)<1o9_hS3QCF)G>8S -B6O{u=8yze?|~fBt&=ef~hB^eko=ul&#Vy@?xRHoy5*t9yDs{`KqM?;oVSO%40`hcJ|{|JCDOe1m%A_ -TP{-=Ja}dcoX{j_8nUG{f{rd+rA5Hr)3E{^z0AMZf@L{ub=Dtw`Sj~QJpJAi^y3;m)bZKZ&p-eAlV>0Q&#ym!_J@z -3#Rb2*37eX3)7bah)b`^xCf;sSE$KF__PC8t&-gJi|2l4aNqcopWV9&NcA4-^4_C?aeoi%SMmYuAN8c^Ok4FyT@wf(K2(2TSt$J!ve5) -@wb$nk27m?;ISV!@h1H??=41ZJDU>sZ^!Vmcw*w#lNU%ar&s`2o4qW~INio8GN~gzJzO3J&iSVE?zx^ -Zu`@O$estbFcKc*ykJ|VV3wZ1<4lrD;`nj2TIS9ic!W*EOGpn%iz%!nL81Um;|q#_A|5FF_R -urm1^DjTV?ckpIR9%Ha<4+WRutCjTW1idi;GqV-l`xign6+yk|(3ugD}?2+7$I;rJlt6^~HTdi)v}H= -e_b{xajf-)v`$8Sm)=#3VweVx!K&LXUXGjHJbT#%9H{JF`f6tPZmaV{+$R(R3Qy63#a?L3Z~c%3k87`(UvwLmvg(;TtTqpgP|}nS!M1-IZ?!rb# -7y!V)4>A`x7jk-3o6|hDNGBdv|u&jVFS@h=#a}eLf&iHmmCv+$E9&YkIfJJg -Mu8$XN(jNV`2y+1~j4$Y|6=A#NXoLjQ9_BP^;0{_~F2DZM`Yls;oxE{ju$d`#ZPBBSHiam&C0H_7s6Y -7>|Qp#kw3wzUq_nHE@Z|J7S9Q5;0k2vBs^jY}mPw?RdmsDSS5XzB>rk1!uO0b<1i -ls@mDcm_)o&{Ce;zjyWol63+>FV9P?+Qx8cAIrPoka~_uR8u-XJF)>9LjX;@dykczcKq?Mig=;mX1I@ -v_?=Gcgb89RfS`uC<9&xOJ`-mAA|BlWSOhW8YMP|biRbIeXWTcS8csBp)E*_SssvrfQ6C$)Pl`dgeS! -4ITV+muU8jr>6%cBaHd!&4GB__usRz{9y3w5;$_{L&c5}*! -!VX1(kzP3y<)p!W_>KF2+I+Mp5GFFqlRw9qQnhHn7A;9UW@l+w|J>oarFQ@iFt1-Y-n@AkH$vrYhVJF -BfIR3j^^S#VzZFB;%>zUC&E0iAKCpy@WOcVDc;NYA$(S-Ze0UMz!GDk0yua~LcAeD63+?IQp0&fC`iU -`$GAt|}M?3nNA-xt;Fs&` -iV*64|8#?^*HY`*2J@NNeB-SlGglAdCxJ7RwfIhk|94X=?-x8Ml5EM4h;^p?N(PASGNS8=nZ&O1)G%6 -R<=O5Q2h&5mH0|I~YAS^uW$V=z&zR_{IQ;P%kRTNsQ=HsQ)J+PFVv-z!I0aJwz>GFhV0@J%}-@L3WSb -iu>6&9Bd!hkA!~;s}n&T62-KSZaA_uFSIFCTD6Fvu@qo*^@Z$(9joRCQO -rV>y;oij4Efg`H_QPMq8r*m(WR@JGRZ5HaB(_kq*S1~wS)<(guPB48n;*iNlIAxX`oB*- -i#KVlUEiNUSJgFHK4B?dOmVdJaXT(~4eat#~-OIE>8W>U~=G(XJ5j;(1tf{>LhW1+)zp2Xp}z*usU7! -sqw-wdVJ<`n{#d=LZ;=Gia~n0$VutWi#8J?Kl=&Ukl(KD&6@lmiPv0GE}XYv2f2D!LWBrEx$$3@LO9L -?g8th!C-bEX5QFoOfmJ#||1qMJgA=p#oT^j55?>jMp$x -%{vsqYv4hu(3~f>iadZ&FocY7h$*&;QEuuh7MI0mDH8^#5F|%%wG%Vrq{=e%-tL|&y*yykoL}#63pksguV=I9Oi7TPC0WtOqlTa&*Pb!%Rz>w -1Tlod!m;>hEhZPii|lF{q1f`9P$70ZrWijuJI -j*KSU91GI9QDf%VQc%#I+eioV%~rN%7c_^BS0drO$+36(Gc#@d8Re_Yy05vRg!@MHIGh?c#Utce)l;9*0Li2KJ!WJ3Lr6 -c%4a!i*DuCGnGXEIB?RU@6n~sq+Vae=KQp+3pXf=?)Df46^dXQ%tBp3;X%NHD+rMR%Bt9klbERZFLo8_%PjtB0A{orKk}&`ElSUc2y -3_3QiRc9^N6X)?u2Acy)+@~-q2=3PQ$}uV5S>FNKAt#NJrMw4s7`Xo5Fg;=G7_qC@4A10mL=ui3jSZW -3rSdL9R*R{1q>KvQQ^`NwRlbMks>Jh&HQsl6a7{ZzK&!N~GbsHd9&95ToZ}DCp~v@p+xu2te5})*6Qk -y~#eh#)`#{#1CHp(1U1V+x(!gTI`#t6M!#n75o76>cGVNzbr)rEL~p|;d=~sDj%50go1zsnUje*Zl_` -FfhkoZG9L(KdkOLWmE43$iaZZXNOG0LO9s#1Sy{rW{nZ$YQTEASSgwFa(&*p3Go%VcJs>5?m$nfgc4Gn*&G081XUIg!nvx-)u?f2aihHSp`km{OvH&u>@-nI29AIwnctri( -IyW23vV25@rd3u!?pT_#%X)xat#f(7$Tp11JhFl74;qjLqK$p(OORM3N%F_N;oMyM}tgaA -E~7Nl?J&GB@*5G3Ku6<3@FEJzFPwt^l+4B^W%FoquM7?~J}az?&T8dZf7NIIOmuqLjl)FsC!0ZS5W(n -*5hl8a|%w^`pfhPuuwm=ZO$8wlG*x@O#{D$K}Kz=wT9j1H8$fy;uB#8N1I2eb2EtmnbHi -DBTWtc{FSRS3IQZ!#V*9wlIn7P+3fGhAO!JJSQe8mo2+ga;Zh*sGI}4!KEkh -<@II(kldp1G9rXX|FuWFfhI9XPv^fhnb0ipmc*p$y5*sj7WpK{+dcuPQ -NTgXY)eKsvC_t&G_6hR}4HpW2nJ|k~N2ZSvDh$ZpI{v)ivXR&#OL_FiAXZ_$y29dp7-`nFA)Fs -T(3eq?~Bz+)xXGY1)dIXC&!WOgbB)A%;kA9+vkUI5c8LJ)%3Ha)1FjsV~T89~M}-V|nRtZxOJR`9Spg -G(pf0Q)v*0wT^PI0HO!Yy)ejDn-h=4GB%UXwZ;*QhuLf_r642r9QY%#&@mHlAUMHGlc&`(opC3ucRoH -F9Sm`a6}SebB$O?T5$sipBxyGsLsg0Gt8o@uAMS=NJ6xl^}V%i^I$>K|aqLanVn3)KYN)id_8r -Xp45Zkmub?^f%Gn516WbMLJ5{9BQ19Y1xu9mrt7MwcQVWQ6IOv4>ivL+K09h!L}=@>5f1oW)ni-0Q|( -z|KNs%87G=^&+G#zlqgYx8mhEakLLXN(GJ)EKj=+$6uuvrykCq_9gb@{xo|7;$wcv{vW@P~?!$4j{bb$Y6lD%v>o6R!~P9~a5jxz`gf}e;+Dpi(Vr

A(A8wg4i+N54F+|^%q|YJy0-J&rCe$w!8Pa)K -w?e>zTaPJ9YQVJ-4|Zufr$M%RbTWwMT}#FwdelRF5MM%q4^3qbOi8FuIY#iwVW!VDJ*`#?$!7&PtioA -+z$;~073}JvWj+a)FY6X*+Ra9lsDRpJ;eB%X4OQ7rmlwchw|vabFi5s)bckXT+JZA>^W**U(lu)~c|| -ne+yW{?6^AS)4Wu-!p?O5#4OpP+X%4Z4!n%`BnugZ36qSIbA8rcaw&sv^0+~xxc?w>3!<=+#VwqxWCO -C*(g;^9#%n8P>Xyj|g1T5J+k67oxzfsSt$w0&xFc&YbqB{sSnS~G#h@q4NOR^NlW!*w8LqQ~I00ZvCP -ubkyU@JNs1;$169tK!sjU=d=3hsR1B~){8Vfnb00)*vktb(w)x=D0bte#fU*vkDrhhGn)9{*PO!D{1p -&Lrt$1uTt#MVvJc7xy3kQL9LRXWPQ6f -cX&7VFmKj1}1^2rh76cFFU#6qJN4i0VN|UkdqTH?l;QI>x`6@inPk^8fJCg!OT;218LtOU`RxI4mQZY -M>U`F%^8~j>*s2b#HtC@~MF9T8NV@M}ZynS-4oFf(ZB`+SJF0_Mgd3K|V>oS?Da_i*+lahfTza_!h}8 -GVE~kHO<9aa(qw{Qt;E{1caAn8#_wh4c7@cFt4nll^4?38CU@}O`r@{dwR!G>5$Z$oF`F=-Bx%k8Z$* -I$EjN>C3Heu0o3@xJL*eNB=|&K!*pD0e1Nc^pz&ykcv+U5Q{?S@cZ5{CpDhERh;VHxQc+DVKtnnPy_} -1z7;$Ep&0Qb_@0p42k{nNBb^_Xtc~92fPD&_P>u?@KkkZO@d_^gTV|m$%0}czJ({{{yF*dR2Oqu5_G+ -G_AK#b4Co@(te#Tm9iHl|6E1@oG5hr`lMg?Wi&v2NmS2oIU19K3@tl%!*W#!!k2VBOF%lN8osrQjMk0 -+vG5m9dT~8LlG}xw)VrRo!xSCmMwjVr3~|6qpHxszj_TduhB*qzhtQC_<1VW -&EG6)V*r?*FMr2<=)TbKDA{te)4-+t+JKb}{`dqFF(rt$-7k2>%Sqv5}qj=c_pr|Y3RR`lKfeCC)qs~SP1KV#ik-rrPWh0PtfTQVdOC58tOH01T1M1D&%4j1)9`0p@ -yOjaLeUws%oqUWDvlOlBU@ti{s*SWu}mBGi0;^H?8pQ9W#^h2QdkfQcVIX-39q}L`eVARda%+-E;_6= -#`E^x=l!WEfaYqGhkwwbag<7qA5MZ29~%%x~-ID5= -Fg4P*`B@X5s8UIV?tzpjfaSk{+4Fd!|X*UvVge`t`il7}71=OOggIBkTh|24P9WI;F{`6{zu1^v!f<LCW-t%T(egF$q2_7WunSU*u-H;F=lF*#QLL-05?ujU@oQ -p@_Al)X&PUSS{pz3rQ#-f|VB}J+~;B2gx&*6QBn+Ii_>_2P!5UZ;$-5}j|86ibG3$M?L9bC|Az%nM90 -syv4&s2J?8=BBsNOmcRFjkV@B;6*Vc-@OJz#wgG!bud80NT0$CyLlbf)2TMSV7uYfX55TXQ$LM2hRAs -Q|rjGbD0d%OztV}Czo9SO1DTjl)K&wgOgAD0?

Xcbr*q}v1sCuLnQ|7wa1hCl)nuv4XiS*mR?YBO+ -OGqJKTo~6YPo_MT*DG3oWqum%wpNfwd28FDh+!eU_VP=_8m`w|fpw8I}0tih($dXaAdd-|DBOK!CfGhMXB}U)6lwLItFj7k1nCvmZ)g={@Zgs{$ -2wljZj*SGulhEdh=k9>Ju;Zw7kjy6sQAk`KF0PW7mJynyTheF&taH||fdNSxSecEwV-acqyDo>YrWMG -pb@F}CCR^6pwZ;*!RB1<4eiGidLmcVAw_VoEX?O?(39yr}DTP8~rCBSq6ur*aA7IJ0u+0YPHXnAt$o? -RTOL~3*246Od>fsTEHSwOONS{bDk~XW(RT9loX-Kzm(}5lUuN`JX#46nYlE0xeI#X-25*n@LPAi?`Vz-4;@+eoS()0X2e`ggSuT4Oplf7@H@n7sdPu^K3a=MbJrH -3Igdi0foiR;3)z1CwmP%Yxc58k)GKIj?0V6Tn=J1q5>7Ff5$nvfu5N&^Y^T_~%R?koV>K>G{)a(Xn`OE?pk97DQI#ZC@ZL5dKPO -0xlLhi++meIN0;`8W30V5ym%Szu*8ixJWw-4>F8LLDU=6y`ZGQUI!j7>8m9xEZghmtypFdzzzG8veQ=P@Xf>({V?M@Sy`&VGIq{YYif{gvki{$YC7DO4%MWK!Omo -l4qjBXQ%>*J4IxDqNw4HjT>`dEA?1>aX23$2vo^(+75R3=0MslX0w-8uIh*4WGE%cBr=2!rk+!!A+3y -SHZIEs&T`1koT5Fk2&U`gZLVbsVhc@H$q~qvTq1gZwlZe=ZD6REc3c7@jX>9dHSh8YIg)e{+Al0<29hQWGGEFt_@Iw#zzp5$rhdRjCS9PpGx-I6zyEe`0Jcy{dR+Ypo@t&Df%fC1T -xfH}n8ExF`4<7z&;cgAmZ8dI|>vJluHuETRGCcscC8LocRgq75pQpwSstEd$hV8bt3AI#y6v_wH-$_C-o+t_Hj@-G=s*PX)vT*)o-x1(uGP9-yKSg{xqnEviO@Qe2^Mo#G_Tvf*fl~n99 -S2!a3O5e{eZmJK4Ro}wr;MHP=j<^tEEIe%OP`xeYavpdTS!2*(HzP@&rQ2{p6vYGTosFHJ5BDh@@L0? -aoX}mmGLH9sUMLD6&6YZP!Pzp%WqkW}yyx>Je?wS?&2v(yh#Y${1xfEfK!}EhcMKS!mBg2Bzk5D^&pr -Kw}z9Q#2y-1)4TUw}qbLei&a1ShHn^@#tir0})9BuOw!y2St-?V6>CG+8&#YPy4mTkZvhW>GSj0i7nyP#-N43X%S%6PASF{WF$ONfrOByog7c6NDf%#u|;f8{Ok7Q2a6llV?~j&K -G>St+7-m*z>jwXf;W2o1@3Fq?+hCgeKn6BuBNq=&INaauAR6CShCwK~AkJV>`y%%EF!2NSYi8W)^6kf -4yhRQF9*7FlG5c#)-24!34FGcQ?E0qGVpJ?MdyDeVd|6{2OqoR0TewWmNGcG1&roDUW0)Cf@8+-=-lD -h=tjPy@<1^GsPL%`M;YCC$5#3IIw9Np)^07L#AoY)BC=HHYZez@0qrKm`oKfaiCk5k=61iP-)y$xpJA -d8{BEAHW+glvN7gZ2iYlK)S6GAY~RD#QgHh)HfZx(nmecUde7~EamcWiiFT8BE+w9US<>0ZP%Y1a(&b -YLOF11D2MSxq)JZ-k2S&9&2~~Sfda2L4VmgP|5w*$gLGS@#o=GGysK%1P$3(f?Lw6<3U|6ff7>+0Ng^ -(B0cN7u0&`w?1*F?dbe8F%tkB}RfQvJpC@C2ZGcHwzFzar=BCes%Y{DX^`SGg#x6`&}BmnrNU@LMGvq -T%=(A8=dN@gi}${v8-1i?tgsKsu5oqJz~9@1?hAdflu1E_XB0tuKJZipnBLBqD;B{^==5}^8ahZ9Eje -i?d5w;Av*?P42ZhGJj93ba*sI?=mL*HH3$R}p1a@cTdPGIJ!&`XQ;yAy3A^~0P&$}UvKnnmByOnex@8HO?&>o1$8Je -TTSFQRv$9i8OJMJLCY4enP>}B7Hgi2#PaBDZ%q(?uFX?OG2v{<4UKegWtgT>xjaqQ=P#Ieg^nLWPNfo -aiTWvbgl>I8Qh5)IZGS1UO;34fr7T@hsCVI|bnb8|qt%}@-o=jzCk!?za5!5v<3hiOshFD?S4Q~Eq9# -R9(KPhutnd9`gEkZu#Hd7%&$dxQrgp9MatlDeg5Qu!^Dxi138ffE|w!yZJUo7GUVmIBgk$&&0*5E6tu -nRCO9X36b?uC~|OHRTiuIl(qLM5lN2R(qZHYhX%3ru(x41_gt#(>YZob0$VAHvX~Y@Mkvb{C2efu2hM -dvq7;~U>R%R1L~g#ji{;w&tt~%=&*HM47F#obUCyt7zk^f=9x_{YBLe}YG6Ge-4-gC8)hnbB#wMLIHw -6nq!5E76FayJ8I&?h8#3$?xzy~XxiZ&`DG6ouh_thtG-!l~Yq}tgx`lOX_V;O1EYuY(0eVeVYwPz|@$ -muaHd`7sn?c;mkWYyVJZ;DF#147mNvU#;9G!iuOv*e@+qEFbBO9;~r`h!-jiSw@K>`!X1V!dxt;=FT9*}PHjByPc4W5+>^EVZwYQ!)JGz}qw+i56g>LO>uc3t!1#f?26-4+itIW2Ra -z8$1WAsI?a-C9KPUw~4~j`B^i&DyRO*^c_b^Rg9?ZmVQig2E(A#io+OCNAB#ZQ7@+fSnYD@>mu4DyMa -FDiy`!O4e~H2c+BHB@3K3g;5ixJxlCDjN5VgjZtSI1Y8)1w~-ZUp#CUA4fa?`B+1d$ -Ax7NViFFMmnLbN}i}d(YL|@E@HihOs~_|_GBQ;u0*!eVyZic&sp`n1JZ4UK3r_x>!!yZ;L+VJ>`$?Hz -*_)D4hh(B#=vDWtk4SU7~UiAxHhjCupmalzZ6M(H>9vEOR}iUOh1Us9!fb``6TSkP@7;ee*qmom2sBn$WQ)F92(HjnHK)Al-He8EkKdX_1=!O6JX|8t{IS7~MT{MJUTNb>u@ -DfEl^l*Le9nNVl6O>_~Jt?Zm2PV%V?h@*oQub$dFg;=w2AR;>>kD8$qCB`bkEAl-H_*ea$iv&n-3Tw{ -z&8B%=k{3Or48<*&_3`k9Gx;YWq|0>2Fbp|Xfkvx76>=HCZKC|J&@VFEI#g+j(&2u(zNYqw)pN+rhz` -0^KNViFyvBg7t0ODB{zs@p&K;b8hw|l&q^ECCMcE&r-24h>0e_3PCVG%B{t4}nTU(=ikiL#*!PFpi!1 -3Ur81{pq`auM_(c1^CsM-E801=u|c$$De}5lZ$fg2`RN*h<8!^M`I#FaeH~k!tr|z0cY_NVk-|F(2Y- -06wG@Gzj#r1W<+qxGWQ(JY6VCegexb6g4{FzO*lx_keV}wRz$pLZ+%{?dMzQ`N2yizT0}P)3^5K(QY` -oYE4{L)ne!!kZw18Uh-Hwk;-|F%jqMG928Qd;*=KaYm>VPX>_}*$XfIT{aRxWjzf;MX+$_R8|R${7c` -5V8Vo-mZ?;q-Kh$O%$-VYvXXR*lX-K!6(8nHms2Smhq-+`J;IhN&Eid3P6Q<1Bo;&Ml^ME_`-)M8$3Q -4y*Nje%ga+6|BjNC7Gr2-L@-Qy7(Sj??Y1B#~;JgYvj@?)(rq}x91Kb7ff`NsnTSv!3M34IxspNY+Pi -w77cP9`cg9nP~chodi_2kAD+kr2Frc^0Qm@bkVqVH}%3iy_GL4)1X(wq;UUgCDhVZr|Bd?c;N -@33D$M!wJuaaJb)tN6D%lM|0I@iaW$4O%kRDN_fe+djnO4*9kw4TKLZ!RjhS+n0T -g=@46UMf8$x32N!dVvj|lkpK^(A=T)%?8-_n;@rf%+1FN^G5Lq@<84daAl>%4Gu5L!WGiT0X+R?p5+m -!L=FsFQA=|^=d15zwj%P5iKYd*8AJT0_plY6HQ+eJ4?v}vnw3^LA+i84S-Iw4Y1`1p!V56i^3d>_bHw -UEKL~@g0A?0Ro5b;8{)C)wF#B_AVt`_Ka_O*GoF%hi;8BDrdC&wI+ZnJR-*hHUAOT2(gci6;z$eh)50 -kc@Ar~SH+(O1JRs=>59;3}7`fOMPfjFV3_<{@vMDoF@F&w?Y=&#so8G9CkoO`FqpU?K!MB-?%s42Q*X -E#>rtlS`gYK%|{P>oXWxl@;O96HTEX*7NC}A&JsA5<=&C#mA6tH+hm0Iz(>mbpUEj2Cwk0I>p%9=G?J -PGQDQiia0(?;bPs^X=w+f+YK%46bd@{b@w0~qKRydJo|+VUhUnGT?ndgdn#Z+F7kv9$rMW}h9x0Fn(U -FJyV19%1C9gMAW3Q(J{$0^_121^F-#u#2R>M*?c~rrj|d~fq0C%lqqKFD -$Kgr0&3BS@Ol45=XVE`t2_=M80|5^o;mtfH#vVj5@@(kg2Wu%#IV_4Mg)wMpMGj1{54w2hg9n=u3UZx -ALC?VyZY0qy9&5T`gr!OCn2W`n?DP|n@C+H=oe;W+NQzAkPn#x^VSh3Npd{cr)){&Sq}%P(1^c -qIT43#MXE@eVZv*|rwr0CvZ4iR8U9xo{d)L8ATDu0OOUR1wuB6XaT8qvG0=&iH2abe5cq}*N0uq_-N! -K2cIy|Fn5ke10w^gNUHa>gykR=c(!5*yyQdqdF+Al<_PnqgR9xo|iD2LroS6l({=gnDXWAv* -iV!zl0VyU?c?Z?2^DF|z0RKecltyTN^MxAGL%Pj}XXTiLLBaP_6aiyT&pg%U;sG&qE#Sg9sie%4vL -%GW+1llCt-!lOy6u*LZO|Ti3oZkSuk%old9(q{xp|Z!>o!9VRK;dV>ch}g2zgUE4h9+}jA4tOc`{A4->^@b+U?h%%|!n{t7>5y)VM|k7BiU%5im-f@H8XA8{i7 -MszekuXUf1&UcqOFLWb7fBcbq!3wVlz)Oro+bL!n^*_vj_+|Gql#q8s|PAhOr{OM17LS_SoQjF3MDgb -Xz^(*CSZ0FhL#6Ha!BRb#S6emr*4|%Gt(aBt?1@uv2YwK7=lHX&$88y4&06vAkv!p7$&IASv6UAw3j)nGAM-z#5}L;ZdPJ+|^Yhv>tw1G9(s -!o=?y`iXJY6r-9B7$S09wh`bwMQNlr25`i@JZLW+LUjx(De9rsX^5Ub}a(KbipZe*c&`O$pYh8quq@% -cHnjX7nRHUVOkZ!AJn}>AE3d^Q*!#qH!!E&dkFM+EnxSmNmrQD;wH5f!(i@3oV#xY~!UXN?B5z;aQ6@ -*NyVqHIY6%uz^4f~{vcJJ^5HH`4^mw^vA&)s4@nlttUH#U!59kNU$@yuiCtk`RRh(&Sa9zmH^_!Y)>N -w=P>?g200tsTZvw@nHVr`rB#n*n)AZPTWxKI{tKuY0KKZi~c{<6=ojvZDuSkpq#hB)?mLguZ$%#qQY* -A|C`X9&*w>M6`MSmO@W1yt;%q{4Ep&N8zCjBu821u9!0LSO*@bX5pvrlDUZIR!1S -{Lq)1!L!M0cS@34K!+61w44gd?0fFFG<2i}e!}!Pr*xiFyF2{iEx?6$$Q4z%^{WciFnPw-23>(`DEwxGlrpE6nb>|{(&=(%I!q#!W{DWGg -)GWI4Dx8h(!BIBLdyt;zG<$Lbr0N9mJ##8Rr|8-*eg$p3VJke2!-!G!H&kHXCC;0&0^(%9pn!vps^9; -h3l;8@jq3p%WidkNjSTlc&p1MxC0cp7s3#E$-W`e?u4x{LM_D8(5dO`T!MSVqS0@$?a5rW5J6tegc88 -s!!Jduj_n1(0mLH}YYPOD}xgJhqdx;tbB{%KqS_(+Fl&ZTGU6SuyRWQGlrzx&LM7YJFzjdR)nVVq(hR -ya^kiL=^$D5Xyn=}K2Q!&!o>x{9zI!PmKzx>04E9YY!liHbG^afkTtSf3I4s?=@;orz^H>XyK!=+TQ^A-l0i0*8TXnM24{pa0ixVy1 -k89uvSO(c{kzqqNQ|+C#eBDF@Y+EHeoad*9%nXvSQs&pRcpYiq>DMeQTLI}do5Xosg6AG(sfTgdm?FLE1+=fATTcW8nlIq3E{ -g0^7K*|L7}*d*r8GJAe9gG9dBUBbDRAD$j1h@3;W<~Rki(-k)4YUmD7I`6t0WWMA;39LM!g2MB(!_}A -kkTK5SQFX;PX7)t4fdsqZi2mNR`MB9yL7N3d>yc0`#cu#*97Z1LsjZ{nF8kKUnv}U4Z0SC9ri}&kq!mmlg}$-dP$x*J#j>6AGMkWYi-)?N>c&8LL=}e -;M4@rs>NhHx8jH5O-B=e%NoGPK+pE?@-GZe+0?h9{n-g;11R1rH2M?=eD9E;~cENWIt>=g8Zmcw{qep -?MshQe0Bd^$o2~Uo}U{uExHTJQ-G21v0%jzXa)I0^up3H0D%H~z&6Hauvm$BP8m@HQS2#VhfKd_rpn1 -#LcTR|$T?LkyRFTV`zIV@VJ=4(Ur^cY(n2I5`eDvw<|N4|lzAL3?|@~RZFAr;6bh{fCNo^%WJJUpmca -`EiT1aGCtQ6=pU4`vtTJXrvxS1oBSv_EV%y$0ssH~_&xqCCuuhazk~tEX~3r!a#aXoY9hNpRQ7nqSW1 -7Y{940qM48c?O2{r)^eje$xK(*Ib!Uz0)ZNe9L5 -UCLa>2>#Hk;J>}kQ_u-It9toy;Q)L=HXOArNJCQ1qXDZLD|0&%O>DPj+UjCz*3y%PreyeXO0wOQl;gD -6#kXgzJLX}Ylh!mRQVf`(84Q_)SGQc(=i6X1EqPohF^*2mqsz!IJ7?6>R$Ajg79C&0Mp2&7V7Fnk;~8saoXGp@ugqsQ3(_r5(A`y2UEOrMefdNE!P96;<54u2#slh) -XVJfOrMBH_Q?g~6S-Fu~D2hzRBpnszhg3=g#%?G@ENC4|M_PJ`%13@>WlFYWa`G2AP(NkwQc*HOMNw& -)3NMXz_-RXHcRxT(b7S}M)UxoL(YoF-{`!=#4Ody?sja6@bW6R@pZ^BM#qxkcrD)OAI$E$}`^wqAlgM -aq<+}35cHHehh!9PL{Z4bKHdYpNM^Na=8=BXCYxJRiSfk|H3R^cW<(8S8$^9FIJy2~bN`)rNnRI)47~ -x6i>8nc5^dScBM-OQ`Ck}1GrnX-0F!cp18-1h9$un`g;gwW7{e -&_B3u|&Rk`<1Ojggynm<7)@TE*VQ5;T!#!X4wF5A-X1oY1kt&xM;z-(~P9v(Qe^AzJL&ozG01M%~Uzg -E7b)9YUz2De48y33a2mh94;2&G7YqEr&Z=*kN1YX}FyWUz?70TK2`<5BX`s2`4!=^&1 -2I9pIi^Kg-jqUAVT(0sX=&*DKq6f++!Zp^%7^2y>Hl!bwl8G9j>+qeFL@j3`p8EwTYW6UW_Y!nInMau -fOW~hwb?D_@dZ0G~YoV4*>db1L{gYElex?nlaw~M7ugD(BmLy^$m6-E#PK9o7Ahsqe+rM_{#DV>DC-h -b-#?(cek-uH->F(TJKG?+gA&m+=qwQ_3;3W{~Q71psYty!ag%&++qO)BMOzxEs<8?&G9vIh*Vi_{zEv -1)}#w^H*F7VR=|lbP%@M+j(6d)Msr0Werb$@Am8M2K@#JDj4EJm-p^s#o!3HJLpQ#;sPWgme4oUSQ|4 -Zd?l-3#>*?tChsfS*<#bRep#!A6Mb}bly>QX;%@P0JrB_Bn~F5AnwbBwmUyd{eXS%#7Zr<15k*>5T_+z7%ePuP>;H|sl`Q{ph5_(gK%~g0Jw$C|hua`N;9COUM&Icd-{ -(t4ur+<3(Rr&JSXHUO+@w9yU<=3D6bNO%H`@zKr<>i~}^7Z}AUvA!B-ImYpUSF4+hw|sU|G2rmy~^)D -{q)P*n^#wlclYJh?alSu$MP!g`Q+l)k3M^Q^LUd_et!Me>)X4ZukXv+-`pNBsLoUEMR*&+qc6H;?7R&ps;kJdX_VyKn!bJh{Cs&w1 -}dd4BzHegD_%*K;MG-MxLhzxm;pEc3&MkN$7|Qp%sN?*FZPb94Kb`Btf$G>G_e=P6rekrf6-j@68*EbJy_-w>uxqAEh4|n(Z{7-kUZ+?8opMH7!I -?G|!kJtA2%+sllQ_m+Qs_VS;;{`O^g^3`|cU!OdG{^YBd-~BP~%}(dloZlSj=BJ --yg0D+fb$|8t@m(gK0sj2-`Dg!>Pdxeb*_Y2=e#h*;c=qzEr!QWVFTQ?Wo|JE%Jb(G@vv0qA^1OWW?e -lNGe)05^QeIqN|GLF&=I?Fte{S)Q8S^GA4^+w19p`5Oz&;{N^X``5p|Rxb1DyPwLN|Je4eGw_;G%f}!8V_81gPk!=;yik`m-oJ -3R_b=3*7uqt8Utc)hzp!k1Aw$j=>L<(c>kG~4JbQf&bo^?~c*=9Oxcq~Ut9|{$hxOz4KYo~ij?IVY)>pp$y-Oqu<-+k)U4>&?w{|{C*=XUotp8D{JKmGK}t2gCMa7_$}q -doup`NhRNf$v{__sz4}ShA$Fk?wzVK_ktoxF`jV*84&iuMo|5;< -Fd%Ns4e`{@7FE27e25cFjt?O}F#-5?~yt|zFaIag--^Ow|mTkGLXFk%-ZOcDnzwGO=WPE-)GfF>3%YZ -rh%W`bzvG`N*lb9SN6P8k3EZ8^3 -b~PgO6loN9~s-)48m94`XGnOGd40<~0tU*+$0SGMVgQ-?Flrl^sV`+m0>MTr`GPCGNX&gSl#O{U!&|BQUN^1RF@Tij~451WV$3@j$IS&lqzEZK`ahnJ -IK)L6*MIlPJz?wd3L58K8H`TRdr?O7M{Kz_SNw5}Y1K2Q5KF5Bsr+qJQA509o3mBfq>NfQ^o@-?jak@X+=DrsSQ?XK-HjmFd2-i{D{<^ -#ubFVwjmL^b;*NN1oUn6@8L)%-F&n|*#BW*30&?wmB2OMmc3|zAb4KNO28I_;1m;-D$>NU=IAl&S8;y -Vk=%d>q0LH!+4rO%QYyrU8nvLky*_wm%*mHto!OOM-U#^_bwq!?o4l^j#iJRC2_FH*j?_hFVr~~Rh6XoP@Rp4h#S`&l^Wfs{=-jk -l%Lw7NGQvOO7n&l)c`7xz3aFN9<%H?(;q(W`&CM9SGf44uCTwy2Oi#`ydiK9KT}89477`NCQdO*9EF` -LSBHYpa#AS?I_@5+2c9Y^M^n+!bxv9#Nl}ZtCfe~e~mSbGgexOF4YjQx#VX2z)1%40#*ay#O~Am${Sr --1F;E$i4rTA55f^S&KP!;^gv@W!xi>b3AKqfmGjOiFalHJ2(wT^2gX>+g6m0Hh_4j@4>ZMy0I8VVMXb -?iAV?A%@Zb)5ZXC~f$l5ps76ppO^A0dSPvYd`A3zScMlile=o1o%!FcOQT-)?&O#BcO57-eKi9=-P06 -ZWwW{S_i05*U)uU-U+i5eI>FE?;{K$}9-Fw2vu2SUVYvd~yV)v^*}*p>W^*btZymzz=%YYq7nfG8?O}xrdPQz~|Qo*n|ozxcd5+SNb2JMT<#l$$b -%>SaW+KPt-Q)DUzGJuTP)YCo0PQ*3waFM@3#{Ae2H8uic28wzAg;d?&BQ4zZ;*mXPDvNjO -*l3nlb*ogVCKU3A7ulYe06d@O&?cc#Ut_;Le68wo+-jp0g! -?W%hMwJwTN(7HDQz2W58fF?rNNv5_CJ}{HSWeg7r6Gz%H>>TjXF=s-#NPo)Bc9TcuN|0Jt)(z7&elI3 -Ni)cf3+SypycI5!hz*piQfpP&MaugwZq=$`+Vl@@O>+*@jWfV9q -%iGT?k-Y^6}>{5v0#V$?$AE2lK8Ogzle#02|2FfGXju6^KZu=ECDj+rA}uq)V3vd@NbFd(^QIRS?)XB)d -9FCx1_9mqnlWFUGcLPLit`_srED*<)o!HpBl5-W$2z3Jj;H3^q5t4G*uCp;$g6153$cVTQUmPIl*h}z-UnNlGKO>!+CCBc^qn&c4D78vFZ^MUll81cl0E2wUek8h+mmy({K33&L71p_fVLPsu@JQMXNkNYZ5;yW9mQ1u -iWK|WI(Biw+KuF%$+|xva`QrtUs*7?O(wcinG_KNS%oJ9ol`cjEBtUe)?lzO3jY>>J<|>DoCaxDSQXy -oTe1vspQ1v~4tecOMT4D`d)(Nhy#EbJZb0#Y$$K4OYC~UQ(A{BX9173EK%bg?FgPx89C_g)Hj4Z{bC0 -+nNdWQ7DKo2A6xN&bX_eJ+d+ysuh7D@bARdB$ox|mH9p$AH$Ff?gKJY}2;XIdZpaIo5qSR|1@5xW*_d -IzVs;B*XVgJ`3xY-&s}6CiD@gn+Z^o{}IuKw%5CX=XJuN8ndSU~6#lqQx9tRt_!z0SI^y&V`f?Yu9Ss -w0Yj(A)=@$FYeNmd?)0v^*j68rpAi_5mVYO-C`*n4zUY`C~=)Q0GrIoD`J2)Jn2|vr`gvNcb5|3Z-vY -whE1B+Ne^+2g59?%%%T1*hhmgFumf5I1H5AG_!f)T7^~zPaA=6sh4|5TIiyk*6a%bC>OG)=MjSSzn*f --3^gIq?x0$IS*zN;WWXIJJo(j4|K1a@eN`gp%hv_+VP8Dph(5bDN>vaqPdmXwS89lZ%NU<9kSV4`iP~ -AoXzV>Efjk2rXo)UrsJmg8p9#0 -N}t_O;<$vuJE@y)ic>Jk|{b@$c1Stzs_I)AA#i{M#S;){wi}&gV^NxX6mF#_?l8H=~S{s`BV%6%2L}T -7ZNYxHxlVgZ+CPXm9mO>BX+1+qqoN9c3cXmQZxo0PbK(Op^PAlZ;{7kBV|#WW<%r!e^>JqOqcO%lU+2 -WH%0|uIg}1n%TZ&=Vi;4{^Xe+$Y-sQWC~2_JrkFZq7N`Q{i3CcsMG6k5PsfUY0K#Dog@=GUBo7xkYtJ -Jti=N-G*L@Q?5k9(LHa30C -26aQ?*Y`BZ%EVypFD&r$E5~J0;-L5O8wdyLt~>m92?I0F)s6iO3Gk&nWorK)NsNGi0`*0XB9#(LLTeO ->luqL${W0wyS|cw^X4iTODd$hPr`aTjEhg0(Y`_!!$@w@Y#4HrrJ*8c85qT4W& -*4)mmIJ!`6SDox~KSst#3=8lqBFq^Uo(WD0}!DcrNo(>15RC908Mu82fs-Pbx4&YLPFv&{S(m)m>XsN -0?n*uSv5+F{LF@)Fxmn~Kz_*>FjF#nY05=JT21ni7JiU7bPp!*(oI;o5r5V2=5A%oN5&=eHo%hpCoXp -ClukOM$*P;JvN#0upZ;>D>$!);EuEJd5pCJ0TJ!K$q?L(H@uhF|7-;@<~2Js@KQ$_}R+5ay1~(Iu}9! -|s{&S_KEP(ME)5iZZ>)!vRh_e`UZO;e=$qFZ!eklrU$hhH@GX&_IZbhVe95NhSG8T%%aBDN?gqOld_s -C%X<>lE*b5grjH^7`8V9f+7FSW)>*(L+%{zi3nAG!!b0+8h4CG -SgPyY4wW&{T=CcT~Q|*Pk;t-@QtESXvf)6fW_Cr-ewxy6~uhx2%`x;Jf)p -}OfcJRX@nqfix5RO$T?$o(tXQ-4fI-~wV&NkKFiPQK8p>Kn6R9V7eLS{WyJr5W*HotW_jdiv1vFe^cNRRRG@tUEpiq@4l5lD -xL~ky;B!hs@O*VBla;196C>rHO8hWgYr=>;9k=QzF|qxb1*}Yj;Cve6HB(b@YlK8X0yfo(A>%+t=@zk -@!G^-YYJsVfr?7JgjCmBk5HknBZNi|IMfIqHf}PzAG(j#m3MQP#AV5HRD!Olne_k}nz~XwUESEYAphf -YxH6c{_ILW;OMJsjCfDcrJWPVY=RfXPkZG4 -6SExjkeawwLO%>rQjo-%n?=S1V*>cH9DT?I!qlWHMiSH})30^|^4DD{%3ER+F|jodlAbI?Lm8_j1on8 -`F}jvpYdmh|KmZPI*n2r7@2$<}J~5z5KXWoJFbDm)HtN6@Y|g%dWj1KC7K09Mr* ->NaUk-L)f;E7ei0%a`ej^PmWrv$brTv8@kCfW?!8R#9be?jHSCIidv2(e^R1+}yxTsOE=F`=aVm^_X| -*CO2k9_~n2mQE|6SPrd7RRm9IHdCPL!Aa>IC`dN#RKbB!I!)52L3p|t-GXc(o(rLH)M}duGQ}W!$Ly< -BGlZrl$tAlQ(1(zTg(S*|)6@?kdgQp9k~{}Tv<$_w;{jC%W4CyL{We_>k*}+5Tack6q;B%V)m{Z0ntV -B)$>)?a4#@y#WW}m#jCO30my%`d{0-$bfNJjp?2C4T$`%iX>3~s37s~QvzVNqZZe&JJi0O8@*==VIVo -)Mcw@r&Kt4faB_sQfTiHp*hsDVU|?nEO%rTcEBp{hZS+hRS-pmf?ewhCW%OX1szrK{CdN+gByX0Mh?V -Qs643HpdYNhTP2;_6~H$a|e=AAYEbOsnNE4N_!ySa*ZSS4y$ff*;_}*nvvvtRW|^^t%n~*HkbPq)xd9 --5T%_mG)y%;|Cm+m@NWh81{FN5ADuu?O4l~^T>JMRV^itl00w -OP05EKdpjuHl<$AhZ6c?JEkKM$9HInr=sxPaaXtPnZYK>$A$*N%M=z>(U@`xWT7OL4W6!hfEo2_AR4;p`})jJCs>Q1LUQ3tMfSo^c0zuPAu;~*)Z+BVG -IQ33{rx7vnc`BYYjqNW%~FIzQ?R>*4%-Eys>5^4&8xSMJoR{8DMq%S0I9@Zu8{}rETaj5PJF@*LQ*oz -Hx#!^F!Jvp1kNTdSF?SWG?F6gEdA?iLWKJ2yrr*4Ds44S`2Q}(i{xl_-kEJK)RG!Hkjt3r*UASzT3Wl ->&Y5jJt1R&Nz*bc5MUI@^Mv7_b~0K?tp5w+GG+$zl@}sm!9vts19#f?-dFX+?u*Gs~h=4Kj4_0!b&rK -rwr#cG5I)2w{~-ShQKuV4JB8KGca^!(t^rb@UhR+CZuiBgn_QO1L5nV!2`mB2qtiRd)T!|`c8-+4ZJN%J;UUe;sW`2qyMFEmNMCu)f%MLdJ8n?giuP2dKmSB;^Q$`0( -ha%sGHlb)(ru?2G`&Od$*YvTR~^MD0WbpI9Lm(ZM;&9X^XW6hgLOYC9}PNB#k)(rj5v;kffA6&@`p(cL9nRUiAvB)d?tB{Hqm?|WX8P*FeE+m@&OcytgomhQ|~R!HZu;r -Dk>rJ$PcTEYPT7vP_fRcceOY>s!mfKZruv2AYPNvIHQq88*9}x~>swf;lgX5|=tT4q~&$60uL)LzRRq)v?*OS&(HE>M)HXtA`zhEYzl4ldZ*8H#1hKbb((rs@((isoCW=i~q&?3qwdK!O^(&7QLYM@ZY%TCBL5lnYI=JK_`GkD^jw%@@rBwwiZZ#YXp2kn%swy!o{iqIT_S8^)d0U>N;1&azB=75RsHjp2R<;=#`h}BZR{V* -CDd#&)G;&@;-LuF&NwupGzOdRaywO`D(KgB>~5c+L4r^M80AuO}J!s@u&lmV?!uin2?_+2?&81Y*-Ldz~=Vy34x>5CpSkiE47}Ar#KrGFYg+BD-Z*fO8R@p-5h`(PDUJoBdnn -i9|m$4rSmv%fe;IzSu*iR%^#NC98VcrKt`DQW7SbojzW^<8#dxbe2<^vWGhMY-Ob@1r~+YOdl) -;WkcOeA?$WH$swkI6}I?Mo9Rg1P5*WeO$z8^TII!L88Mq#q(y@TCaB|fVq|UN)uGu-{s^NsGuh%tXhL -ACCNT8bHmVvIwQ9dccG8hzvnqTCuH0zUZno}u>!r9mkanPHJu(zDd3R)JjBJQPc~CTU3IK1 -4R@;dyIdGuMOCTg4*b29*l3W0q_a5s0mXh8-5nxmuc=E$jWW&OG1C=fE8gU?LXD3qw{I%>9E$aFx{}7~wFiY|HAQdkEnY-Kv$UJq -Ez3YkAW&@|fi=b3N>7_6get2Alu}hj$ByUH7xA$|eTYYXiNDQb@ -6_jqs}mYgODz6#MEeB+R#?id1;mMI#q4_5ynQ<5ZW2@2=j2g3x*mn&{YIK4qPq>|QG*?%TsjhRqIVhC --aOvps$0471#YC8)G}ab|%^m2=(KeUf#Rnhx-hmB-jliY1f0U;!3QdWeNRi4rH -BipRyJIJFr{vnVF`A|W4Uf9 -k?pW4wy(VXyGMNaheh#DghYGCo^_bh`OniLSQb1u&;c9L)a2tn=e(%vNP-Vh*2aa*c@3$5B+Yw21w# -gx%z8b}GFc1)t}^XjsO4K#W8&kg~y(Q2U^j@LNqf|B2=T?(tps5fJHrVdwC)ND*-hF&*@h0Gn$Q~b(- -!yDG~YYqrQR?att8mR4RI}z!i(uhj<>8&8Im=Jxfo^QL26BQv74FZ6&0su`O{Mrz*s<=btt{h&gS73v -TmCYL`st8!A9pf~uR4%L+TN^2cc5_LuXJfj>TbZW>s^A3#Jimy+z5~nUyUl)TUy$Cen7Y})vDx=0{!) -UWlL2kGZu9;x=j~PdMU+LNo4S^F9e^(zi?|h05rXP0w)zbRQ|}=XdiU1ip2JiH3%@HsahXo()mum2Lg -JyYi#D}k$k-$xyS=L{uX>RTC_Dr}T06RV5H%2Zq~aFM=kaAB`t-J!rFdlod-|x+n;Gt2E>m~3QPf5jx -4}MfSh*6p3g>Aq!+yc!i)`E5$CfR<9eg;s%?lh(6HVV-Rc(y`g#R_#bP@tA5=l*Fr-4-6uel)pS-zu) -W<_+&W<2vJyWB8UDrxr9p((|e>-T}|pCA0=?JNIBT?E0Y`x>yUfd_(fsXKpOMdwYp -8uh&&XmicuRYE!T%FUFW;F2*rC5~%P{YTCRSwtAZ3$HF>o*|w -*0vpvRkPFjtT_X~x|YDwDa%kZvemr6-!;Xq=fH^k>Gud@>nc^4Vyd);8SWttKGa2;$Hi87+zoSTRTpji}KACGi~Vy!y%fxz<#~`j7 -Zy^%x|;%l~mS99ToVX7H14Lr?vn=V+d!LQga8?TDG<1nShAK;c~487W==QmCeolCRHU`BSEh}c8QyjK -2q`=1m~tD==RhrBS()wj_ZS9%y9n@bc06>65UZQ9fwW?N$%8dE7NG1^?N;-6Xl(YqVhrJKdj<{o;#o7-0Qv{ze)6v1cpmetajGrYCst<1x3 -@;Kxp-T*UI!oDBaEuGp`2Ci>PXalp{} -7D)w28quL;?z|9S7I|Z@xHovpUFY(}%{g#&D{RgUMhn=)GLISlzNq%1Kr_N(9yio#%IeV7!zK!mvjaa -u)hS|X7%_2wn;P&d_kqZ39M;90WA5cpJ1QY-O00;p4c~(c!Jc4cm4Z* -nhVXkl_>WppoRVlp!^GH`NlVr6nJaCwzfU2hsY5Pj!YjKT|%=-!a3QroD~T{e)S@+B5ji{q;TOGfhc#FHbNt=lIN-VVcd$AL{lV24flylHNG&QFk~=zR?@C%|?^v8>WesYpI2z#NA0VcwvHN&tS$NKARqq) -8d`j5BwXN~g##1Y~rp~8NA3<@kJfc`BnuS_NGj?pBINx`Y811cGRo>W^yZ(#_(MZ@IG?i*?Wl8S{3O# -v4)7o#0KJ*W3lw4V1>)=vhnaP>LKSS6Z)uy6MXtJD3x2J~@o`lZ-F&6mJR{;aPBlGsduy+c#kh2M5E7GBU&+4j*CjQ!GK!&KA3-%&*p<#bL6Ux8oRu@s@}pYZ?7Uo3h3yv2R=2=1c)Vt~-^>r@6aWAK2mt$eR#Ocr)3TWo000g)001KZ003}la4%nWWo~ -3|axY_OVRB?;bT4RSVsd47aB^>AWpXZXdA(cjbKAJl{_ej5wLe&%N~)b}+H0=qRdMV@bGGcUon|t5Ga -iV9B-RwEBSNmyigIA4);8Tah8+`mc2EPCe2r-Q29)i -9i*|o{<&Rj@IRgAOFX^Ki!#YGtD?f_(!^@IVFJ69bzwklwE^Wtsd}eJ>c#Q#OOWVXK2)PLRX4QOsvBc -X@n~XU{4~$XBAHeYr)Q5|F>9B)cu?0#Ixmu?8mJYxQf02LH{~MFoVZX>noJ8_Y)DtL!Wd=qS-A#dKdD -V#sYqw4FmYmqJmfBw&f-7i1@dNDllr#Xs!1)KJCN&LfjE82JjrZ*~uH449T*r|d>( -pV)KlU+df_yq(Z=o-$XrbboP%&N2x3RtT?j_=-IeYjJj$tU%3baOMB+kl{ASGQ+Fc$_g_#iYzGCD$s -Rf#foWCgJirNv(+f3Fft6FpbqhKN^@hGKi5_X+*?o^P6m#=GmO(#g;~)KdEGtI?5me2SfsOu6ehUE_VZ*0(&uTO7k%|6w1>YS9)&91GG<5u$3B@m-ZwwH=(ic=?hRT;%g4YOQDp4949 -nz7Cc;8Z7v4TJiJGTZ{07S$>nlSBT(#!!zlK+!_IhtPj0I4_V2ZNpk3eiU4wCl9d+jiW~zy?Xr&4x6K -Jb2|C7FSlEcMO6hERLnWWmBJqtRuQV5HgpyWo#&5@N1&N~5c@C2-mP3}H-fE4p%`xyka> -qiiEmd01tf`r?B^V&dZegjkCY@27W;JgaxhN-9FpTgeYEvCOfUOwz8^O&;VZ>o8L8-aqMr=fZd7$d`K -ro0C+vijx*{yK~?VBY9{z6s4`xU~95Q{(Ii~77?hW2P3t-$6Me-;@W -Z{^i+I8>&L4%I%L9kWyIB#6F3>7p{h+?{1}0%D?LsQpp&tmy8`x3rU{>sZ+UA;3Bs;Yy>YboXl7kNI2Mo$SweQjaC?h -egzl1`U7m0I8m3yD%jP{<3gb>>=xl9C&^JukTRSX1Ca@DgjyBV -{)5IYuHQlt(-Jq&CFe(t1d1y&PNS0X0bMZ09)5QPh^IT5$lB33*hUkz_ly1p>SIoSa5N!vlZc$ir5|fnuNO%x8c7`ISI`&reI2-GRTWUSam^hG)h1A7 -g7PQ<_r@7&QiOCbAi32n}r3z1d7q5?OhhYdQ1-2kGH0-PTaRx2nVK&rC58&wX#um!K#^r2uw^|q=z9r -BJq(pG?%9QG9EP$0S`BVE#y2PI^y+LyS~LX6L5)N+tD0XApkBNXwAL_C6!Kh;cWR@8w<_4KSJ3vE)Sh -#M3EG<*A|)#o^D==@WjQSaN&#vEsaKvkOxa3N})@c*8>?rBb-=*HH$ONzRc&1ZlHUdW@41q$S+Xbr}Y)I74R}kgflQSJJeWR&JE> -2FV25Zo4R!VZE^<1f)bYxtnWkZ=)pLXGB#`3c~!_M>U2mBmlVt3Hdd5uhS|?9c*^caGN}#raWcM -F7<)k8dgY#CGxE@mA32R(y7@ -gx)p*{-O+{kRK3~aQ--GqV^SzBCZQ-yC}V^HTf-(>&F54r9A7@i$OLkmGlZLn1#<6wAxP}Msd+RG8#l -&x{?+%0wMxsap^O_Z9q?$)NNSY^G&L5gZi%kbwcb$t0~!EP9}ka@$*o}UnaBb&dRy@;Z5=Y+k&*q|PpWQ(%%t|j&jJ9&Y)u0t)eix+SP_My<_&i -qfwV6UwXn`s3A7+QyEuTXe2?1%2isLhsm%$u!<~|)F7orDhPQ{Gx{b#;Z+!@v%&9yLi;{4wxBDl5d!+ -*-d=>B)wTrgwUW}&t35(F2klhwC)H4CO^?76yC-6m_nnU?ql^1rM(66aI{s=jKb&0MT!v4l!P3jKOL- -WFOY5g5SUUMIk%zNzsed{<4whb@$+I8AmHy$MgN1juBlMa4_+_{@c>b3?r;$q_j3rty*D^iU4O(klB- -RZEL_@dTxqL7=g3+f2^_^=+AIXs#f`TTbHc#n4uG8El??}>xiLQ^h98c~=s?+zQoBOxp+q==^l!SiyZ -4tyn{};Ri7BVu;f{=Zl>C3CPXBQ#ANTMgg^1sHnA0W{Cvo|+qAMYolOL`r;G_Sh2dN)4ZUDk>Pp%yP9+_DtxUbC>>9)mU{Kv;#FZDyWdO=QiFM -BfoH`f%FZ>8foGN=rcDWnYrV;XZKZun569oFgl1&}5_!TFHj~sNxdcfHNb<@$klGKnFRw{Clz^9SSX= -Tp4V>j-7f_B%CWBofDANIY;^0z1P0IiQj+`tLib2=;ObtLicO5QGQ>)c&#zS@ZtQEueEWI^N5vR%SI% -{rdL%2O`>;voEbGSLHTDDc+qb2XsqV-m`f5vrMo4?^hA?1dx#EQ)wH&Jk=lspxY0X{R$x_(IL4eKJ&` -WrdFv3BbxSaf~X+QS+BON*^6&z@vWTu>SXb!T;)c#x~rJkMhan8A6Nm}40|ta(62W9)#|VQQj9UYhby -_2_N`z4MWuy$EDh8fdF5@)co%S?MqI>0rV3p+7cD<(9<_U8IZi9(|7X!xSIo9^AE<7w)p7p%v){{Wl` --!N=WJbSS)!c^h)GW&bj0Ty%xL_+v5E8zF4YWNi&~@fVDp9R8=FS49z)e%P`HuBQ{7@f -t?nCr*SO`9vph{@c1d0=3XT>gdN6S)(aDh-Nl4I_7#z6>mJ=sqpS&gUUuftSnDbqG7?AkURN`XaZS4% -&Qk}%)9j%+1+8x@YTF|jDGW6h>w!Vsiy`lQ!NoWGWYoR(`{3aQ918cLDLP3;R5NF!GquPY9TA -R{$nJ^gOI1{)@Mq9=jKFgyYzF`dj?4o?q=a}>M0d*7GD6`4@J5Wao!2h4*fg^53s0&{~kd(n&4lMV0Q -B(<{LxEfrRHXPOtQ9H#4xD$ewh$*S)Rms2N&|-S^iwVe-~{hX=^AI6tH2Qga+6#(^FSJswLT~C)o_`_ -amro2ct2eR(71^sxm8%{o`%b)10b#Rz}Zj|-Oh-8yU+$yKSDEAnJ;ze?wV!{7uYn^zWsGDOOU_Q+SL>t~uo|%ZiQr){}U}^9Sob(MdN$_4#si?Mesx%}mOQrLVWt>({NXxlnBGP@r -D5>XZB6XluNpX_R(YW02*L3_{-&kwfN;yOOL5+=_l&mFzgOVbB*eU|nWZbaqek?R$QgXX&O_m%U~q87 -ZpRZ~WV~AP}A2zvfP${i?PXwat9{>_OhZL$QBXmb0H5qHGb~?Kl4L#4&PI2B#U6`JnE-XrS$&uN(|B2 -i=W#%O}YBw8X4-uJJs!It3wq)Xk(&=Ah`I{C*z1cttEQcV9jaPF{r(_vJHG`%2zDN$neb>>ppNlg_>9 -2^;R)4aIj$@S;nC?Hc;!^D}hR?~$<8e(wlqQfs;#aY9^@Cvq{Uow3XDdcBTTmtF6OKR9}d)83Qb!248 ->LPdP|=n3_D)&3x=eae28hm?pcXoO=p1czd|0xN)+|-!g4W#dBOuaoII#8E>`tA|^D|e~8Q?GswcD -a#b=V(<1h*B}#2YDu=%HEd|Bu(vuy2mtZHLXqYWS9@0H+NX6YNG#eRG%2XzUrznyrE-i*?GX)FhpgE=lC7`~utG|X>B!@aIS(r^y%Xs*P9IcoSb=$b3-Y&{Bi_T@*E3UrmypRJbNeZ=;?CU_#p_#5Tz{Nl(RN-KHz)ON;{ySeGc@HfPCW&X -(j9ESffh5syI)^(-IJq0aqX}#O+pF^@z%l$20{})h80|XQR000O8`*~JV!W -ubY6)*q*v19-M9smFUaA|NaUv_0~WN&gWV`yP=WMy%g}QzRfPV8Ja-M -MVBxV;7)g%0sU#X-NtL?~BkPTUKLQ-JI^Gy_`=!@jF>uFJKL@tA|wI%bQtMnI|JooQSMXKKb?k$`{Z7 -@Zy{D^@}f`fAi{j`Qqzuzx*HNKl$w^7oU{ZKVO$`A8!6~b9Z%HzPx{XU2Y!BpYH$t=Jxg~KmYQ}`@5S -rS5Nm3<<0HQ_1#l>lfU`=;$znrcQ;Qr+4J|;f4#oFe|P;*?tdyzzcOb2{+sKk$2V8+uK%xLZ|?6N^W( -$2`v-lu*Kf;@A2i}WUh3+BvEF^iOa1&*e)Hw0#nzfR#IOGR^YZNWw!GxGkLBg{EY(b_xYH=dHnRh<~ODM>FVKM%Xc@oe|fn1rF>TM4xh@?efjRg)6e&J=kw=O-QN88a -P{zkS^MeX`no*c|Mc{?ys_Vv5BKlOo2$F>aQ*h?aSxvp@l>wv-u~15L-zmW{_V|AANcD1-P?Q|?)vHa -;g`pcbNGjEzAs;2Kjxj5KV08kKjhPV_x{J*n>X3{>zg;%caPWQ>ao1zyT_mZK2u+DBd^XIDPP^^@S5G -xakurugUf@oMLYmnGNwH&;*Gf3D^`I)0ir{h{3EYCL@G`T4I-!QY?sZMnJAus`SA=N~ -y>KIq>PR^`X*^8WGqr}wv)IYR!n{L72i|NQOuugkM<{#^d$*~^#DzIpxU-{o&}rt@EO{q{&Vzr0HbzA -gDw4_9|jAM(a?fImHd`Q<-nk7r-J`1-}`KXdnAy?Fi2^H;CRSKq!Y&&qetUcP?u<@aAdds)8w{^fVyz -Iy(7DX*@tKTa`|`D>H>f2R1S9P*d@oXNM>Pggg$kNeU8oXh)|H+cKD{CxG->s;nH*Efl%a+O=;-G~3L -HN3sQ`-`48yZn7Ze^+jPDtGr!m*sB{H;J7@-#@Hwj`H{Qy-d)*`TVjp%O73c{p)Qm!K>{4)y+>i^jEj -{_Yarli~GkXe)^|p+4pd><1>rHO8Nd(e)FIGSDt_K`~OFy^2tRG{OXFu%BTA2;r^F$^&<=E$Db14|NE -)@0+4cVJ|;%q+`doyaGTGU7T|qOa&C{Dz+c|I2W(H*Z+^bJzrFv<2hI5(uijnVU9%()*Z=nZ`tD8Q{p -!ct#O&3(jg6e6AMf*FUw!t+7q2wd^Y6d>?2q4VbM-+c<1x4L#^?P*?z_*qU-MbYXP^D|a(q6`s(es-G4jdx3hniUw*2AZSAYS{_rl%?XO+`GN1Fm0M-}3e|d3{llH^wKY#b -)%V%Hz@cS39UcY?t#rLmYeEZE0FQ4WAa>)4nV7k8i`RakWPAvZ0w+}L2tV6lnBRu=!>*qhbdhx%WXO9 -|v?tycgzWm|!w?BOK^7;S#;oFzLfBrK6`8OA()Z;P@uF>?%V$-u}G&BFfSG#ock$=s0Sz_8{@Li3gUd -GYqW$j154D0POnq?2HxAlwsV3#?VU;4X+sfG6b!p?^_9K7iK6CI@X(kU+Y4T2vdX8J?MSk#0JMv1|WT*U& -X3Um-7Id0y5&{Ud6qZt`A^DbwG^GF=ONdTQ=2nwl?gSCk!$HKyZiL~hdAVdb4{LWc8zQ1+^_CPzhZS4kCxZUh6P>pT=ZP@T+GO#$D -&6rXoBmYF&f|F`n<>w!8etr(R7+YGim6_G@8*fF;X?I8QqL-MmM9IjXiFyIj+&&=TQvk6!nn|U(^&T?1{#I@KBHnr!vbyl;KMKS_c91eHeLL`5kx3&NEH-(m*xm3o~01&cS4)IdDu47SOj{_c<>D`;g`^tpXdeltyE++_~FJ&R`CK?Ud8V -b;uvfr8K^){DnPbT;_{$)0bReh4X6-Zu$!&ocJxVG;UzDxtT%}To9Oc?yWf8a{r`v>TSjHT*HVfmzL4p&Q!-+i9|KCm!Gc`5}8cHq^19o;Ea_ps -6&ChMCqR%LNX`%?*~jgc;#3F;ayqXxy3W=D2BDqQY|G=L@Es&ClpU=mD@38hP1xzB>pC8b@0Wmg0 -cH*XUU*r?zUor!>@yI-AmbmHGHy+Xf=RAlmVTr=@R05Hnp6|H*qN!rn;3{%l=8JuSgUZ{{zhckoIS&BjAbUXma98qer>M#o<`PLbQn20WO|h5n -*D9?|iLjz@GnqT>4^4@r*FAZ0HF>9nT0g%Z3Z$5n0P@Iu=kL=@#exCNIekbjLknw6 -mc*?$OgdI)o+BY}_xbC7&zgO+4WdP|5s@!7)!#nsyCVA -KFn_tsIgY!~1&MIBW0{Z~PPo>^NYEVFMJ|@Mt(-*UI34S^Fkt>zID;^uX~tj@SEnzwi=_Yc}-60lOXz -2kbat#{pxfvY{t#)N!MZ;Q*uA&|}hL?saSM5@>OQKxRWvoR>q_u(}O;nGHQPt2hlPbt4#P(9p&bG}rv -D%}W}*BrrLF$q6P8650qJ8Z5cNF&nEU?jjufRO+r0Y(ChVAQgq2N;RI*R3ZnX_&}t=m}N=k``bjz(|0R03!iL0*nM0f%7-me -8bsiLyxi7tvA*UtPt-NU9ML}a=FW9{xttmlN^Y32sX2`7(sZH4hB<}_mt)o5)s>;b$H2QtF6%2+{_eN -&n#p;{yjN~Qxfu2CJ0?7&_E0C-}l8Cku*fk -M7W;Gjn0L2OvD^RRJv7Y{7BrnMi^aPR>NLC<8sFw{r2_7m?tU$2>#R?Q*)7j7iC|00Y+xGJ#c}aetCy -=Z_vI5BpBrA}tn5_yFD^RRJu>!>k6f01yK#}0I)swFecu5746-Yv%vY{uCtU$72wklApn5~N0s&MTJM -k*MoV5EYPI?r{RtQ%OVU-4L!j~1tS%VR4`J(NChKM`qscm10xNLG%(UmzCM$eo(yf4XiY<(!ff?Y&9^_z(@ll4U9A}(!fXqBgh=t&;yJ#Fw)NWV!}%rSZQE|AS -xSrf{_MBVDs6~6O1%4(!fXqBMpp*I>_1g<6Jj*Ne3$(taPx_u_qmjpo=x=a6{*6NI*@34r)qMd`@`Q6 -px8zL0uSkp_jl*8uX*dI?6D1Fw((D2O}Mf5cy@pwZKRRBOQ!%Fw((DkG*aSUeduz2P++{bnHn7BOQ!% -Fw((D2P0VhZ0G?-IvD9-q=ON(ER72<>0qUUl@3-q_N0T64o0B4+0YY=bTHDvNCzVbeA&0qRTk$%P(3trN}N(Uh1pjnKRiJvS -oXCQ{kKNCzVwjPx@eTJVw%RytVeU4ZWps6&Crqc|XNkb1XGQh~#abZRbxo+rf+0YZL4D87OBL -j>KFfzc%03!p83@|dl$N(b)j0`X`wvLE+p5ua*0agZB8Q7BnMg|xeU}S)i0Y(NGK|RdPNDnYFz{mh2q -t-nxykvlt0agZB8Q2r_lH9@c1S12C3@|dl$N(b)j0`X`z{mh2qaS-*c*y`O1FQ_NGO#CvnhlvF8+w9~ -0Y(NG8DM09kpV^q7#Uz>jK0TBUXmZ^304MJ8Q7BnMg|xeU}S(1)a-_k(qKakBGe#4X@%$kM#ehV4PJr -(l?^??%D|osFfzc%03#EOOfWLR$OI#3MAlLbZy8M2`#7+GLsfsqA978q -GzWPym4!V)Qq -G2+U}S-j1x6MaSzu&=kp)H;7+GLs&2!!0B@3)9u(H6)!k#QJvcSj!BMXcyFtWhN0wd&|8Y!oS>e~>&l -9rt7mb@fC7~eQe&{Ud6(`g3Hq@ibGq!YDeLl2{e(ZlGmd5L8>p5q!ljh;qNr>E1?>FM-zdOAIwYdJlf -9!`&-N6=&Q63cKr#|?T0J%gS>&!A_}Gw50AS?O81R;5R!N2N!lN2SN+C6?iMj$7&3=-KGm=-KGm=-KG -m=-KGmxK^V_r$?tpr$?v9<|S#)jvTksv(vNFv(vNFbI^0pbI^0pb8xLek3o+?k3o+?kIhRg!|@z<(sR -;t(sR;t(sR;t(sR;t(sOaGMUO>~MUO>~MUQo^+hW}oJr_N}3NmCz=jv!$9XYEbXLYh@9gVY-MeAhII+ -}Dxr0itTx}Cfva8F>n4SJDz+ -xN*YIqP(dxB#O47YeuM|ag&!6c&Zaj8l=b2%8W=Zv@1!e8397U)0q?}A~R!_3L$JEk~E&dQ4ut5@)E= -3I4Wwv@fgx;L0~iXQNbz|xVFQnP@3{CbKF32b7ja2sd=n0I8vCM`EG0Ead2KnGO|FvjF_gAz$(Pd)^r~v2r)EH7+m0DPAu$LQAb#F979F))4 -0h?0t4dg0YhOn9E77(cJg1rR;%cPjznx|p@uyTrSynP3u(zhBv&xG8aGs@02FczCjvLLoWM>M0>;997 -!J*_n1=N*T$>RpblXTKcPFWQ>)?4g&w0Q(H|6{o7e&#L{T+xIxmP24c8p-a;|ei{V+LK0U0$s#m%8=e -#M`_G1fb54jt*|@af6OJxHRH^}Rr4o#4fwE`<=l)`_#^T;%oi1l)Ax-`?ZIOGC>m5v?l;7kZiwZce -+Cn7VK>#EvyJb<4;`n`YIOa66QP(e72Aj=joGBIxwG^JIBPdO -Wz-pnR77Yq6Q4VF&oR;FvTU*UTc^GRg|+BbiQNm-%#j0}SuIL+Adom2%n(W1{W^l6d@Sy&RJ(CWC}zqhcyIT%#o^V-!eXi7TZ1RISPsM1u8Opfr#o<2`ScZ98tFLG&SPBaOQ^i@rkhbs81{i!*f9f -9$^(9RO1PgPY`^}M{ZL=-RH;%fHxG_?zPU6$i!7E9ZLQ2H&96> -%Hj$Nma-ko)0R&gzbTw+l@46eePHWOA1!J{BO6eR63Wsu1HAUHd9C`Q(=AWCUmcnP7jAuJhnLDt2JcD -=@AezEmU8y9+4<<3=bA#0aBL;h-`aBP2(adO&qCL0l=hU|$-_DBTdp1F1+l(naZ!nJNFm4@VMawKT12 -HW+v>-%y&Ds_X{2O>~3B_|(iNF~0x%At+Rf*w$TT=ocgaEfi@lnI_3`MRK_Y24%`Mu6#L|DtcMsaaj@ -25pJ_2e^&(&D7XGLu4bbHMKcaBo*3I=eX3DsD=SO%0N;w)n5}{g61jZ0yL3%aE7YF9T(5h-Xn>zHz4D*KphJNuZdP+$uR>7q`NouP0PG=|ccA%o~s@4q#hG#^GGg2*;?6MQ@mXMuhMdq$kNPa>bK$WNBA*$3Ww%HH)C -Xp{S2bPV;6#%RF?0|eKHWI94;6jg+Z&>#^M_E3_<9pMy3RzWi94=27UNJa(k=Urjn -&UaHOec{@(@(rqXQqi0ouvjNrvyqw){HCe03$Pn0(D2P^xalgR8hc#oUWr8mqwNR-#x;$ckrR13N{?0 -5?pB-z1%0IJoxEOGMHK|LAjx_3bAFST`1yMbjUdJ|6G9p>b3x}fWT)T?4j~J)&>|{JIi({}6#SL@an5 -h@l9&oqh$s=mO~DQX!>QLW(NV=zNR3Xy#Nc?w4lOe7$bXIi=U|U24KPzSvRM`qae9bkN^k6H{wlH6~?&9OFUF -w#p5czp!&mI5s#*k;4HQhus|)|2RPx~jjBQ?6T*Qb5^)saQ=^nsJktP;3L7m{3ig$Tj8U6jBu1PXi}D6bw){mS_pn2E$@f{6*@KIp6rD?lm_5R2 -+#F=?Yn>d*ZJm|5mrv#fQlFXyQQuO0+>!ihL=RM;bSINkv35JgHGB;qPI}FK^GOx}e#Vkj`3vqG -gQv>CwW)(Zj1-38FgMFGm^h+I69Xj_!|IRI?ojvYI8jr%F@hk)7VKGe6kCFnv=Lo71go-Pociw0T87H -prgSSz1KC843oj7~gti6AA7dK5D-2e#?!u%q84#yv3iMS-r-C=hA&0oDIMBu!b*}|D`I7u4NiL+C>12 -b>UnmTg#e#?sMo_U(kmV~Dz9<+GJlp9vcik&bPckx4a9jq#MLMu-hoW3IDf)UD3Rxf#n0`MLfFbWvpK -89(i{Lk?)5Q`IUBRK9TxL)iQ>a)dT?8E#30dq?`Q%cldNU@}p*m%5uD|y&b+698a2e9rmAFVuY*$RG4 -efhwMk?4)cu#F7d?&VYLv$q{0-PC_x>uJ|L3P(MYgSMFs~}AWk`H);95Jvi)&j;lO+*d0iaVwR-pbGH+e}Y{3DbOmlJ1yk`^=)H%P= -hjnp6gM6!$uq>1yEs`ur=C0?EPo4mxRMJlkeIP|1Tq_Wa9*d7)A6wK>n0a?EwtBG-8_Ms_?{i@`#z2B -&Nb;^(mKQ*YYlS22QeXlrcX!a2SWIgb7@M=c~awgspYmJO=(JeBr^b$oCOU_6x3o3~}rbxGYQKer0D@3(Ee1d4cw% -2mc7oYK}r#6oztEtX34fR1jBQ_yw612TnYK6V`7)V-Rkew_)W&c;nFStpbTz|d)3rQjGJN=H~SwEQZ) -WHFj9BTBUS4t1|it$LwO*HmUeR-Q@@o{uS;Qq4re6c9`5V}cBI))nJx(%7}@9Jdn(XiHScEe5aZV3PggdKAKA=0hjDIA&o>ht-0UAToKdV+GSoO3%*oI(4rl6s*Jhs6-&Sj`QTFxC -O!SLKI}!tb&&ch1HCkyrfX5;&UfmbP__ug=mFGtNXwOwqOZjsKbZEIl#VT&9x;2;*B%vUVRB --A(cW0`ry!0Sr+Iew?##%)7VfRNp^Z^$p&mhXDK0RC$FaNwYcQiJ*zy6=#q&sa!1&QsiQ4K)=fGs244 -p#NQ5}$?1i}7{IrwQy*eU5Af5-Q{GzB>r=pf(*3g6!K{TDuqL@F#JMr^Nr?<4K?Anu8Q}-G=btbFObS -Q^aaLFw?8L0&&$7Nezhmz#f2D`?FI4-da*3PI1(YVP=yeVK(Ac)bPim@eXBI}s|0Hdl(#B}6M!C67kA -%z39aD`Lv^C4b>4kHmlj*=j`P$^n)v4({&h?-va(h{6ju@T$>en>Lx*eC4HbxU5-IUZP`o@M6`Ae`t5 -K`CNTifBqNQJfR1zVQ}JJ;Tc2A(-cpKz6mFvDGk&A)HM&BaO}+>L&{C!jW2@f2O7Mj#VUYGhD -asVr=2j`LGqg85PTh#Q*HGO*hSXe9=91hZ4usv&89L^i6j_y9Kmj%FhN+SRg!HZXoN+95uOuU}Q(75* -x@9MhYk@dXK4Iig%%MYKL^?w$MLBQiI7F=YK9BCyy_&K}%4KqX$1tw0kOiuYW`*NHj%7$$ShCS&DikA -#Q}$+)jkfJCb+0Ct0j)4sPU_Gtz$BPj=|*@}$FsTkvIR% -0cNToI@fi`3jr*6Bd9M^R_%2zf;jpELG-Kcvl%F4(@Y3=b*rhNC_CR%o!9HqQP5ilmi&O|>v8TAqctF -)aTa_U|Sb!8@h%wJB0S(li&^I0Rrd#O`(u@3_m~$no5LFe -uRSWRI_nnj*XxIlQ4!%iqxO6k0l;^T=Ww1rlteRp|hyqhw3mVTCu*zg+LAn){eIE)Xo*05PBe{R%I-Q5>iqk@Div -$T?zTAd>Z6aaTX7(XB~Eq3ojvs66iL_wm3>wM(KS{_jFa)Xu5TbF;u~#tR|S2@DAC~(V;faanVbbvar -mh?-Co}3^AmRzi1Dli&fj{MrtCIkjJtvCBL+zF-?kXKOvw~9DsjQ1|DjpjJotmI7p$Y#Z^y5K?{|8Xm -xsbgX%!o!t6X{>M0P>xSSJqF-!tB%$Ds#rBhLn}ipu1*pHU3iqPjBTDro@xZo_KjoKQVNpsw&Yg@y` -%q788%{fk%nfA@Ac^e9Gx3mdqN{dcfaoJiiQH=0bHdWk4LE)~Ek@IHa_jd$7I}RYY&$xRk$1hT;-2>E -J?wkX}Qk*2-Hzgibga&((3j$|MhA9+N#SP<9U&6@^52g1T(Umf}@KoN>;Sm2S-%DLb4T->*29$nZ0-) --hwBL6BHm)*2 -1R0^ASGg$BDxz0Xn}%K;zIjwo_iFSs7+5IcQ5CCRYM?qe=7Dpb`J`+s;>RLA400W98y`~sk_6k_<5Ks -kH1QA#t`dOw3;UqRuPMQet`XAhu@qCs>Gc98B4tSg68*->OQ?H|xfuljSLC@QP(ylHBSI%@3a4DcA@T -+O_0ak%AAo6~wA;tIkEwet%D-{$)|N<*B6hF}-b1e-qLiUfb!JsZ@|BmtU+GRbm0+f{!)e#4dvzrbDN -Jw`%PLF9>ARvTVa*%wLNJvUa#A3ai#im&VihDbD9>AWoE55j-74z9NT{m1Ym@5j9@6_n^i-t8wx-dZ) -vG|33cW(|weEM%19h)8q*xA>Q7@p2)s^l^{upK65uBYYL`S8rJY0q5cf?o6u7^D0-0x!oCKqmc8`aQ? -LtGfQizEqC8^3@PlTK -fDV3WqsZotA;mH$kEn1Gzhd)1^##dVeF3U((RRQD?3Thv}Mq;wmq*+Q@Ekm}3HCvSQ&PuG0$NW1(=!M -~JMlCtN1qwdu>7UEL@rm{E65_TC}Z2c~%G0vowI3@+ZfBBurPvY^Qte$e+iNd1xRR`vIB -&TsOPn49`)wb-+AZSpUZcN4ip3oPMKMLlXVU>|zpRF$39E2*RjZ=9*_)g|GH#eeJpD0succ`pL!3m=DHiayE28DtC3wg*u~$N)q4&8YOiaIWqnf@mzEIukl8l -4dLOpZ^Q4|xAEdUD2yGx*^B$GoJ!ZbPP3eHVsyGrP;oqr3}y;i*gu*nvH3<>H}Q3bmJ4y0mE+bMhR!uKaTz5Id -Y>0TvZK*E`i`T+l%>c>Ww7FcO=gIXlC-5vac;(TJxoydYNV@U8zu$t$dxKofuL8_fmE4N&7ee{ohsBD -ezZ;GJjsLZyk1cETBg=iQYMufdSe;6)YytCz@DNXqPZ&L=#;!LC)xAm^Q3Z)uoX*J -TB{-DO7rpLCaU`e6;!uH}=4U;AL^i)=4p^OO=a@O#tl?pD$%4cMr8|HhrN%d^ -pdGkS`j^Nbt$baun~JQd+Nn_Gbf*epF+NR;A|I_Y -24cbhK>3!XxJ7S(-1~Tmmx$V{hD|l_dfN-J55a&RTDLB?snAC1CBzN)fw&Q>z}5S%+Ri(1L;@X#NL-; -miNC6nL6y_Ea(zT?Is~CSdgxVgN|PYQP>Y}!HK{N|VjJU%mu&ZhB*(8*HaoTmtl~1%ADQp2C4*Q1*rx -OZyO&BH8&Vridq&+W(JYUq`W=yAm074TSJAePNJ#fo6(R;{%b*0wRGG5+edX -%=OkVQuk#R5Wp$fVz?qE!r(L<|*J?5-|B=@Uv1IY)&NRK1aEFuQ_!rKnIPmA<30pdd;aTp3T*MnD8&tEkLPSCyPHA2=SBLtrU_2(@<2IZT3hv$S=No4mwzh&*J?Rp}w0u2OsX -JfT2LALWDtPVN$y1v^#bDKcY#v(Bcf?iH+t-YvLQ-$R~*SA-Fgk&m@qY9uzVgp{^PRT@FZwN#G|JT~8 -{)V-SKo)Xfa_OI&5mkKXP+`EpnDTAu=T32RP<06uplD)*oR3k-fouE?pYLw&{F;!P}n6lT1#zkjuT-u -kk{6olxwM8TY6}o_vUo^mV0C@##Y%uxbIIE~v%*6r>9hxUY1r%Mh*V0C|9K -n+?YwJtmplrm*@?@yWn8Ru}UI^ovJpZi#AG|K?qd!DC#UU)S3!qwqk(lvg;ozb+4hc74PdYd10j`b#e -|Zv?>%qhj!hn@SqxP9j_3t*De!9_I{3=yu>A1xT=>F3rnwtw3CgFCQ~+KyQJ_E3Yn_pFz!nSQT4`Y)_ -mvRDs``}V4VbLRehiM4i$%?5KHGDROx(dtykyj^u}w^AFVm_C6=|$ap5JL^Kn8=-v=XxN*V+J@WUDn$ -zFKBojSo$l8g+~IU^+_b&hJ+#aF%W6-!?T;H%2}RXa;XlX0fU@jgBjlBz5Z@t#C~r~+5)UpvXRoVV?c --uFt$Na$#k6zQojMpFEa(@8ukJuBPQRnf$&!KGj+D3azB+N*n9>RwH?hssf6a%|HHo?etowd#7Sr%F> -{cBTqOggBSfNVH-AD$e)WaONcA7960`)xP5=)j~-QfZ3_Q)3uBwSd`^)O&Y=fuAi6iuZ7<@)B9ej$uh -)JR|PDAYlsbqqAESh`5}YFc9UG#bwS9+&P^(aQ$T*s1N9~N)IiJO^^Uj&xf?|{R36E1>OeP_rhv@Etv -HezD*NdwpOcy%=eT{A^2zPhQ#eV=$O9KQH000080Q-4XQwDh@YEBaX0J}^80384T0B~t=F -JE?LZe(wAFJow7a%5$6FKTdOZghAqaCy~SZByJxlK#%Gs2zVexD%{OFMIQ{v#|^Yn;9Djz%l1=I66eP -n%20HSV?V$`}JFIPi2;*#`euj#Eshx-BP_|Wj^_2R!ZX8v*~~0hvOHgXX4fA(ec^UvH0QD`O!~eOWS8 -V&&2hr6z6SqTh+x{95stlRGoO;{BN~h7wY@n-KMVQMc=eyzOKr;7jxC-U#DC*&*$aGa^2jQt!S -2_pBPiMKP&rgUfh>#*s7^J^|ifkTJuE>EH0YX9-p^m(U%Kx^JqpKPgS&ftouhb)v6b}M|(nk^Uc?4h{ -@&$;&8ndm%42yF3YZLKb8x-lB1^X+v;Ycmbu&QeXEWL@w#X~h>L1{+g5jCUnmKC(Kq7av0pW{Tfb7(y -1HqL_EBqX*_Nf~nq~i>1p7`rHXAW7YSEU9sBQ=6*D}Fw`etG -`pS{$Ce6+a(dULKxZzx__NRhm|3mHq5U)!n^<;6kWXwME@OD#6tNua7T}UaB64Kb*cgy?(2uKRLZVJH -EOSC+C;qP+S~dUY{Pld3AUxF5X;ToL?Ou2ys=GP_b6#l#;!QmukqnMrm?U_C>Ys?9$&V^L9$W^+K$Qk -EJs6ysQ+WM4?>d{_%@!xNhoOvtHGO6#AX0mZEO@NIbNa!j3}U$hvA2vTvk7KR<{>E>)wV{;*aixKiCu -s-+tGWZg7vBz|bRUN?PxsQSi@&02v?uc+SK-6;JiY0F~167y1lq%H4T<)gLo)v7nXW%@lTm+H&grhVM`Df(qlx(>&Ck^Tl; -%jrj$}?*x|e-fscg?o3;J91@LToxxv#_us%6jJ+pZ%w8nwE~jmDraXbrkUyWHyn-!k`G8vR>4#ZJfir -yVsh?Mb6{#N52?QT@|v^#A?K3QE|-=iF&+tU1zY=3Y6|bnbMK8~o-Bh0 -idJdKDH~2J-2)UARs_h}oD|0LxyI$7ct-YjQ2JDcSF+v_E)Y~I^@dl&PH>3{SX<$%tCY-|pK=_F#79t -rtUg8!Nd)Op?Q5GK7&@ppJNMj&nHOe`3=y_N#_jb}~=FaC%{}3?s=McjX&r&B&I+}qqPUbLbBa;M1Qw -C^Ia~7C43*HE+(hVvHh-gEP?1Lq^OLok~B)@B-4`7Q-`h8;R~Tuy7q}1Kl4YPGJVcW?+HI)LfTa)j={)F -=`H;_Ny}-JvU90(0{D5T*EAx#rGD<%&%`5B>f}jnnukL$G$(cGOe+HigBZETvYs-JI4b~gv4;oIK7d0 -@T%Ke>Nq4sC -=mh$vZtC!;j*T^K!c0+7>%8cmr_aa4WVhSAgy8#}WBJ@EN7N?E%kLS)W75kWxmbkZeJ6&NG&?Z7QWm> -#2Co1{^WNy$7OsRpzG0tbr0sv(b3%1qmmq1R(8Vr@cz@ERrE;f>6RyII0M$G#;{1Z`#h8~SsP4pR-pd -dbeoh~g4LD2w&dBZ6}ou$g&XkrvSlx*Vq0-0LY&gd08L2-qMGK7rV>A&-(@@|`Az2VS5>GCA^5JE3$( -r4tY~;Q9safc9xe!LtD5qbU!eJk}@Pd->Er6{XEdgsles&K$z6T@aIqhNQ=L7?~)0Lbee5qbVgNB&hT -g^N2$%kVX`5$geYpamw*2Y=Ebw$G@e0EHN;5CwXbeK4UcoQd_^uKW|+(0BQeMb6%Nr+F8VZ}^;w9y;V -6P5{dr$dYa#?TPZL%U5xemD@)6yi_5tDUzzGGV4J5_-0par+e -jaEq9|)RxEpsBG;z90^PW;LdN?VtZF_U0aIIjr;A>rpy>0oI3+nAfVKQr+G4(;? -bP7K$@2id6X!EYurG(}hQK=iz>!ZRkKB)+TV -*(MCjxfa_NxM9MbDcoEQ3%SI5_{qzXwEreh@M;S8a{ji0@)#%C%;fiO^lg_IDKi}(AAYB4NQmtz@lz& -`nT7}FIFIroV`zE~0V>?eK!t!T8FnD4&)Nd -H8l3*EJP|g?3>%;tgyoyAzWK1W}CgoIQNMuChy*43<*-`R$zCdJExS~f6O|D3uGjJxtDpo}W2SQRBw8n7=gEBZ4K_ZUz -jVK2jdP93QL4PJFrwBlPh}^NC4oMnXpQ5GDhX=-DqKA7X$_vA56#POM_B4q%G{20)Zz$lqejd}~h@3C -1;bSL{%jwuOPE_-Vo{+nT*FAZvLAxHwmNMK#Qy`J-v-JRM4GB<*@5z^#4Fx5d=WIxuj3h!q7D(oi3Hp -%iGlFTDijZE(EkRYGEqtgQUt$cW7^ZtvHI1FAQ-3arXj_s;RG*$IjpMe=@r6t(bkGhf<#*|XdnTT2P| -V?XJ=$sLINi&G+c$6-9AP26ON2HgQ(Fsg{C -t@4ynXny!JA^fR(k#}V_=P}sw50=75P*qmNw2htBQ@7# -07bYX{!ge!ZtueFpto{uXfAbjFdUV;RVqI$xwiX;(TaO4>P(GU_wXq)bF#&tqc4Vc35?O3CBU{v2hNB -pivu4U2~O1C*XirfDA-XGVd5H9)c?P4~ad_tuuOERkDd_;)^129ad83MP?(2FK>*%ouLYPk{oc;bd|h;%__4Sz>ar`{b8d%qv3~5TZnwGlrJ -VPB-|h(Y@9fR%+4;%r=%pvV`eHq6%eCrs*3{+T -v^p{Cio5%DIeoq^%Y_4+yoeYa=+Ax1H8Hp;AR7uiQ2EA{1832P2_G(=UKry%{ak56drUHo4Br#&%11)ING3auY;^Vplg=bN -_G4|m^emP`F4W~ZM!HP2>Jvu3FuZB+^_=1Ss-jrLFqZ5aK4>W8{{5G(b*(@zZRew{w7?WO0b^^=-*lF -zhk{7JL!OD%(b7~LLGa^AP)$Ew+MMqDQ7>C!%6yIdEydXdVcTDt3YQ%3sfUgc2wp=kXevj#>{FO((f` -FdlXm0lGc8|$VlYqiS77D$~G>#ijDEY -f1{Emq_041^v~r?|tv^%u=2F$F_ONleSSpCHATFmcCOo5i~88dyXeoqV3uaf6+bmjJ|iLhqNumV~`LO -7?djHWJB-n)b=0t$S4p`&qT91;?ZQbwdGA)79XCFm?Fu$@bUV0lR9|+&i;Boy})gzFu#c#6>rVTy4l> -WW{aXPhC$ZS-ZnK&r88`3g;vGj%W9z=a5yskm4aV3j@up-J8CK_*7J@2dl!OwD>m%KL(_ifEOnSa3&A -~ayD2BrrYUj<)@X?Rv1Q`-d*ZLdx%^;bU~>ifgfP^R_2@7@jZLsGBV}cmmtOsNdNs^Rx8$mBq$jh(jb -gLii7_K1_uhf;NGxMDppm8G_^dAOH2lB1dU<+sJyQdHWtaF*MV4yp{?W_8dprp?-tdd`W&du6KQn0#t -4xC8vQku||K165F+KZ#xNxp0^tNC5*2}7OKBZRdc{qO01=x*${aT8@f6p7MZ}G3+oa~uD@e==f98aA; -IU0}}3jxWH$alFYy7$-rjnPjIKWp^umOQ%nCQZJ20@zG2$WoVf6zRjwD$N3yz@3LhSU)Og=mjbic6lw;CK>z~JOj()uOatHmhpnHV7W8m&^67? -s?pM&aht;QY=eX_M@`e#L_Qd8#PX*D*6XZrc@Jd~de&z*k*P)h>@6aWAK2mt$eR#V|nhIlfW^40?6Dw;s4A<^$Y@8e2y}q_^bz|@M_cl(hFR!ef+SfQUx@&J -wAqxw4ZLXhgY^-ctzjW_#ZFzn3`jdALHkXH^>$gs*|F<<9ZEUWe7;ZLJPH(JljvB|~4pxUZo*NAh#y> -tj935U?9h#>}%YCk~Aw=5k#dehB^8v7gP7aH-uZrtVhM&s(np5Y!101U26qwUVVMspjz^Xy>R{Gb)k52BwjI5hB63 -^v_FPqW@z27ZRMUNrEttaaPK&l$MG`u;g<%{qCW!A%3dz&hz0_!sO-*ag1GI_VntCDyuU;FlT9TK|%D -!bb2F)|y@5uNcfO@Kpnc>?2=et=Uk$&Z=Y|`3AcZ*2$X;W*_+$>x7NTuNlm)`!@_`SMpm1v)Or@-GuA -{-(fK8`#shP8`$>^%=-9%p*_04qYn+-H|R%fSepj^nDr4D_!BnT2K|)v!QjtW9~}dK&RUy`{(_-h)A& -o)M`+-$7|gEvYX-Ls{0$pagMP~<-JsvGE?6zUH}E2%;|m5g13i9#O_o6)WF4D(Z2S -Q;d-uMv)o7oya%3yZYk1^O(KK?PgKy#yxA7|CDk9>lS9)q7`otUv6|CDuNp1U#QxvuHt^2LPF?!Qj|k -D}xAIc(fD&tysR#w@4E8*nl5Mr$ABjlPSKH(D1XZ?rB(-e{d7Z@?+?2Am>qz&^+uea{DZ1D+&rpegbO -oFZ?)ljIFFMc#l@_+7H{cX`15S}Q;7RfZI!WF@Q{)YJlDt7aljIF_lDvTyBX7Vd@&;UtyaA` -k8*qxe0sA0tz@^ArvF`Z{>Ds9~uu(Uj>tfU$=2JIXr>GmPQ`8MOMcwEmMcrs!jJg3Aqi*zFjJnaf7Kq;51WMBV6OlDflO>IOPV-9S^+4Mm!wZnQ3@y8)Nd-9D+?iSAbPdOpLhJ5b%2% -oKG4E~dI=)Q#3Ys9UDG0hgj~z@=1o<*D1Ey3smC-MEqzb)$75>INryP~G{K2|lol>PF$ER5v;)p}J8^ -A=RBv-T9V@MRlW#VyatanK)71t$ganm87T}aEiK7WjX35%Y@UYJL>jnm{?S|%rHSGQ>1Rdr3@39>P9D -}sN3RpTd3QKVS>JkQ8%u;5OwFf-5#l1q`J{ZG3vIs-4^Q3ce~MelDdHwa=ZPhZYR`@k1R#q=%f^NyK% -cIr{sL;ow{)gPLaCN$rM#LIw?in=){NDZJ}CCn -d-I}Cg{3^>ULn5>{9A>qPo%Ai|TF@bu$Ls0d@O~DJ`m7=5@KPscvJKFix -9QOzA{*SI6s?scuc%H}*iTty!^E<}1ngDHF -accI{gh6qThY|{3`@D&O=&Sq0MFhq0X+Md(urXrQ{5Koc4C+~i77pL-7X9hz-6dAUv*okyDElBzUl_` -%=2zandCbF1PM%}WQvhvg|Th~2}hB}Ycxc#LPi!X%%%fqFPO5M9su#BSM^oP4hv=#)+lgfH%6OAg>t -XPBUMbx#g7ivt-q)G=js3=>OCDWh(SVS?6E+z9VebvuYDWrj)qy6)+F-58HzhrZ^k?tJR@7E@YOx5Y4 -#QMW~PqxBSHO2A&I+bj-bBrq%Lw#1Y&!(;~3jn=b2!^BBU>C-ToeoQ%auX}gyrgT8v`G!e8bz8h{uzz -K}Zc9vQQQbbNJ2WvRd)jxf!US_%%|-N*b++U -Hp8al0T=(n^6BpDi+LAL@UbkpncfRTtZOM@sCKlB#qweZ>-7>?(4RxECl0A{LI`mbhx}8wBOmzdE{R_ -@4)QtlW@s=DJb& -CuW8Fd3Lp}KK(^`WngofsxQW6F+M*Ug@>ioKiC33boF>vm$8Sg0Ft{i=nelj;~I(|m< -yE)LFhM6?RJU1S!X75?s(a3;8?9^VPT*Zj-P2dyE~s0!!X)1?0Xo~ahKbL`ftGd -Mmc@aV6(%z3_PIC^um|b}IyLGBI`da8hzyf_)s42(qHe&IrEb8pUmVyqUN?IVydOVhQQfi?CNk=_sBS -0J?ZoT0P`5>OTVhHJbzjM<+d|zKsX{AEM8{8As2hD0iYaBP+n-@#p>By`0<_S&ZW(o>lPi+CJLb?=cE -is~bvvPMnPK9Sx}B(QnPFm~ZrPTcnz~DE$&smU5p@GDMcw%u;q$lTSg1SyGhbP&TgKdgeOTR2m>X~{y0`z?H&bF6NTIh115PnF;6e*b02g|zu;mC7z$xYiT~}k -v`|W!e}qZ?K6s#ojxe#DJ_R_%+<;Te4LHTzfK$v3IK|w6eK0rrF2~$vGbOu+cX0ZYMRfb5?iq;gn!0x -%b&C!Tv{1K2bc@b>wX6-y7u~Ycr!1mdbox|2bz6=w$@fbn)D4;{b#Ne-qxg6syoI`Dr%&MmeNeaAOUZ -859f)q3T~af5m6;opHrLFZZ8?fhE~M@&LUiX-x9spK7cW|vzUa;mDZPpA{KKb!78BiIMjtO)Klg-5!?)dR$*JKdO5Meu*rlbaLXWlUL+~#N6yAaks -ui#@v?eIMrcp%XS=}%x&2O?~}Q&i1j7)qV6g(cjf&OCoea1{gPUA&kc23_T%J-lrGkn$c -~=M4=H7v;4Rc`c|(q5X`sk2$$!m)h`PabrCurQGo+MR-Cq3?qq^C=d-qE$o8V_a-MEsPx|1uJJL(oK4 -V0}f$=?Jov$|cZFWH6rah&)i7V5UBZYS@hbh4`34RxDU-E4wqWth~eyDW8E&M&E{+lygR@uh*H^Gih3 -oxdT+f$FwUw?%c&!0NV8H#$jCH(Gny1#e!nz-Lmfx@T~JNlo3g>YfsHi;kX}eoARsU}90-4mRZEd)@i -eEqWWgjJll|CcAYnrLnr%l-27?D$m?_{A%V-I&rff$6|H6NGUDny?PYg7OUIaR!V=O+mcdRM7J!Z^eM -W1GPlv(Y;x-TIF)DaT5}g=?y2v`nWyz7(>^q?^6N{gvmYlg`*GMb)S|nl?zy3EQAjCUU*aO96xk(c?7 -=R{KdIY6NICuUORBR8-Xgl4Eb6A0Qu6VxnY(80d1vmbi0)l^e2L^83m!8{ndr7Kw?%YU-sq--y7|b~% -w040yfgRI51w+NxvRq561yaS4}9fUmw-kqYjo2Vcs_0~!fXsDYFORc(V>YjJ%&d(@;7RxB{ffYS -7P_!@*Xd%^&>)82{Zc9cfJ2TM9I~HUcDJ{pBSX8%H>NbW6tG`y=HFaN^)NN7S=(*7H64~)3E({aP9(V -`GmpE8nl5cg(mY4XXZnXAs+^bjWrsGR^p|$F+sr$;L?uxGrl&NmfN!|IXyCNrb%T@-;PU@C<-Lm6eLB -?K=dku)XSw%H<*VKJwQny8QyD&^FF{KN`#Nu`PQ{5871cc;4bptM@x&iwzOk}DXt;UMHew-du8FshqfOdV6!)IB%U4PM(-ivw}dvtAtNU`LLN#eq) -Nbz2q(T6W}E4ln_|7Nc&*VH{X)D5(d>IPayb%S=xsBWOsQ{4_02ZDM^7$#_3S=B -A0Zl8up+fX;_zNYS)y61+vEvh?zAH2md5$&e5P`Bv7SBvVFojoOrDf0~zd|-(KUq$P>WvW|t_LN0+%T -|~;F-)8+4h-n5Zr)FQ;%iOab3@%0)$L*6f`Ys2Nn$-4)egOj7-=2l~L_h8gmURiFet&bYRvnyMp#>!e_v^iK -=TUk5R7!A*k^zX-I9vp0Lt_)5MZ#Xs_o!MN|a6G|V=T4tkUu|5oac1r0=*(cWvc7h0((U?iOZR(XxVo --7TW{RAzPYS_QC)0fZ=uaOUxEph6>bei&zZ!=J8yo6^ZX29f9bU6_?Y@Ouj~`v?^*a5f&e -DyyAG+nhv0D!uSvvT`1ILzbJGylA*iAPdYwT~F-yRxHk#+61dOd5sJ}8wi9#>XsX5&z*9w@1m5DkYywRciLd$~Rpnj4C(8_Iz$fbr -Fyg^vp#g?G=uhGp^Kf!P0}OgNS#5w(4=1Y)Fzn&voCX;8aI)S210PP-8}xyv>Ky=2)r-*&Q}tr>!&JR -cPOVhE82?tPUW|WB*PB=k4S+2Meh*-qfmZ?SFz{-CT?YOUz(ofBF~A-J{{&#)fLHCgDtUIAtmQR$paK -S73y@dzeSo~8KLyAux(*<(XfHrs(e(g%MK=J{6(#lxZsvBMfw+@9O$OpN?gR|PJ*-bf0Z6RTVj%9`PM -d+abvqpf;=b*48Hk&+A{sJJMqrU{m=jg8h#uY6tZes_4# -Rc_~Z?zkN#r5?2+t@*1YX4m`X?PRBxSYkL?wdihYVTb$`RyM7R8Ctn`RH2!DoeAO*j_&b7+15H{PwND -l;_w>9>Tu{7#Gt^e)(^JDI2YsT+ktaaW#wGZR{|xxSD>#!AF3_#q^TjJ&LafRZNgn{Ud;?m|oK3-vZR -d^tQoc0OMl%$u%Aa78kRaeDrO=REQEJRo@OUE~b|>{5xQ3W|B|71DKkbAo-NP2dMnfUNYeSfNwQjO)` -%E2$0`s{{)cVXm{QTl;3Ip43OVx{{oP+!4g2u2LB2$uBe+_5=gxtSJX{L1BBjJw^}k0^i|r73+g7ru} -olGP&fG$`WkJ<1$C3zq3_V9GT?(`%1;rh?zU#q!QF($74?&*D+H?h4foN%5vu&-W`f>BsJi8nLRSe?_ -gqrIX%ZB1RhT-XGUBQhlSy7DP~CJv^7|Wvswe9ZFvGlTx<`RFsf>K4~AFPzFiUGlZ&p -4>#n!gsPhlJ={lnNZoz?WDd>}svahg%{fBV{Rg)B4?_9CUr@bc2L -FHoteC?UBmT@k5_vL62lstH}l={sHMM{nB}iN&JHr{UBt+691r0KWMR$iGR?cAGF!v#6RfL4^+)fN@I -JA^aE8kcrpG#kA9#kZ&KyGKK(#_do@kv1Nwpb25XwiN$zT;(KMCQL#owA(^O6ms#Y9LQ#n1XT6Hu{pXNiWkAN!g(|l+J5>VxRnh&i)0;;@E^P!bUK$Z7tKC~JMsPaC|hgKv3RomoOQT=mCON*oPGYKP6a&vMA?ggsO -${c(6)cqWxae?@gh@9O(X`MEDc%IO>x^~*v4V2#LU}k?osH -zQD_9CHiZEcL^ON6T0Fgh<2s=|7-g{2!P+0(|Dy+Wvp>eGg9poC8gSN1BQ>JG)Hy+){t%`rN!6RK*%r -@cXFTw5DGyh*644WITFp>b{aHXRzeB-386Wg3`1?Tq&KR -B`bZ|+)SnZ|mREd1pyCwN^(CRIuJm%bP)nh-_Zw;{gjxHBP$gUw@LK}o!qDlHn%LH`GQ7(2IoXzhe^=92{yBCB4of)D}v5nMJ4#6kP3NgsPh{DVN;dPz -fGe7udlpJxi#5*ihkf1S@%t#^yS7g}CbH30BhwS{}bZu$n*c(HIOIY9#|g8ox-e{u2?7_)7%q -KN2yhFB8oE=)FWlHDR%gUm=*w^l?nX_b=r8&WDfQbn~Ic{>Jf@wNtCZ8_$h~%DO&2935U?9dfF&+|`X|R!`7hN(rA5YeRFxZxpaE4vH$d2&yOzOx -)qjh95N0AA|0LJ6hA_8n?JYKCKKmnI8km;v21Q?P2PS#lo`Tv(Mke8=b1fJ*17sxZyLIQW|JAO -nnyVXZL8W+As%X^cf0Uov2o%IH!(b~I-$0;k#Q1k9qpe1TkME -jkk4=*aro5U?&3S98&HuPcRzLf5_>gi!Ba5w3F(7$bCtZKohGMyS;73c2#uu~H-fT(&<@1fWDoFS)==^Z;}>tSAvunkB -%qJT~z~lR1QcpbVwQ?Y&E%63p$rOP>+U?Y&E%6Wr!Tp6R=j=P2qJBx45Z7=--2OJ5V5>+k6xg#0}dlJ -zJ2JqmRC6aF4$H#!T+WKBxZWV0rng=DfOrD?KRlM*%AtVw4fnXE}?A(^a6DVt2zq_dDr*0i04q_ZZSg -=DfOorPqwCLM)jvL+pcWU?lmgk-WNorGkvCY^+2vL+pcWU?k5g=DfOorPqwCY^<3vL>B{WU?k5hGeoP -9foAHCM9yRS(8#Z*{n&aoNU&#ZJhw7tVzk7Y}OR=KTu89q;yU;Yf?HVn>DfB_9ko6+xs(Flae`^tVye -!OxC1Vg=Dg(u%DAPX?>H)n)F(bOxDz5$1PCSq?Ju3Ytn&8CTmivCYv>BO_Rx*l%mOGO*#%qK+2kQ8j{ -JHl$6P2O-jgQvL+>CGFel*m6tVX4U@^5wr!FGrL0N&B$=#fdsRn*Qr4uMl1$dLoq;4MWldVVWU?kDU9 -wq|5-!=Sslz3=$(odG$!1MTweYOzeyfWt3PA2#SzBJeZ)>mWV~4?o%Mc_l1}S2em%Vc5QHZB7#C04}l -|bZw0Z>Z=1QY-O00;p4c~(=T(R=Fm7XSeBgaH600001RX>c!Jc4cm4Z*nhVXkl_>WppoWVQyzeEeI!b* -@Y{%F7-ue -%`*afmFev`>nS|`CP?$ucRid{8p{56~PSp9vh#)CJ$607lSjepFpB -JN`RU@XToKK=qwFJDhzwC~Jb2@uF_}ekZA*UO*2(`U -wr-@++K9(Gg<;oX*kU-6i^4Eui{Z(27=&SbK`s!6odv~k3m9e))pf&8CWfmP!xqDIfkk21wHP+j4TIU -%4c`EU6)!ig$E9e8tC<|GS`J$dqZ4a~cP59;u)_<=VFkvG>$qq+Y&mQ>Jn2K`F#KPN!|>~dau}C4D-I -(~kmRsyIh^Eho%}EdpyjaTu;uWik9Lf%6o(-j>W6W8({Q+!&0+MBhr=N56dVT1lN?4D`8bT$DGt|6;j -lv8#&x8#9JU;`9G>*C-SQFwhao3940Q@WoXugtC>Mv(MUuln=v00fUC*7vwKy-qkhZWK#==Q)7;=ik_ -{(xC4x{fBhasmp3^~PN$SDp(PH`A=io*yeABWLNio=jo9EP0YFys`6A*VPDd3$hp_I?->JdGcQ#m&Xx -0{k#=ndC6k6o-MuJRAnkQXEFl`TQ_CDTBlK$b5b{<}hQI`5eYlD8mn9xu-Y`IiDX!>wJC~t;_Jk7^-d -JhlP3x$SDp(&c$IgF2N6@>k|Ahx}JA_7(M6Nr?dItbbh!-9A=O*pThuGJ`M|hSl}>PPsL%u52KSAau| -K*1=*DH -;2(WABTaRsr)cHnTEsYBUjxT=B9-Fu#lI)epslN5I8)C{4nsG&kxh#5}YsQb6C$0V*!^@ -FCpyHp>;kE3w~IS!-5~41BWr*wq9auq+-?7dWmd5ynv52>*XcD%DLwy6iGI&1Bv+@E_0u*ki3MzVdL` ->xb>#WOU!}8!akiAhhhH~D?hBq;qvklW^lO7yo4Tywdy4lGB&Ow#=>$K0LbTug}g+W94<32Q3!_%@x$ -nQs=Bo@_vx}ZT*y9Mc3#35Kdi-JEkCT2AJ*bEiq_L&L&ySkDg&^~2`)VO-!0{j -grW1TJu@{II}b^j$`Ncsu!FTen7C%*PMoBd6J?^WwTS#(*8d;WG0Qdi4^5AD%;A0&+e-EaW8$;jrL`_ -3DQO4sWNrHGE&qJ1`Qai -s?8O{rx3Bqr7_za?HA9|1hxMMPGu{su!ePkUAunMHhoNp?4hw!5@>J*i#2jY#>B4eYsF%=tjzjRn0*8 -hA;W<3VA#fP-{Ga0x>LrBdIP^FSdG7VYGq0Br@)EcXBhOnaV4qI!ISyFa8P*T$<%j9*bo_=ipTl#=OQ -7`(_vse3UP8|g>zywl*Xa7dMZC0@ -6)k6Z@Y0=s2?t~ZmrDx@Eq!g^*F5e9EZSRp>C}ZKb+0sLi}(xhfDOsrsO5ect4$R?qwl)34z0f_~H0D -4t9%eHx3Jacn%yE@)81v%dD3WI1Jg?^Ck2+TtvMD4dz*&gHP4A2x%-1=LFj94^xj>*a^_{BXQa$JV>~9ELpq^M?fv -LoVa}bcN(4^f+8*UcwkZ40Su_CCcM4)G6}Akf+EG>v7na^M{?7!)##}!(rDL4(s{hIdE9d4?`}4!;p7 -kUP9n7HlXsoj{|b9dI`w+IE>aMypIENuJ>_3F8qDXkdqvSnyX$y;4oUJI1D+(VaT)d!%kd3%vSJr^TU -?ImcuzXEYwQ~b!%GhMG`oS#?x?E@WV6baCW_f0zNO7|t%2m^Frl$LaLa5_)!6?=cP`EfF8L#+I42!v_0yguQ!y;qVYHqmJuEy;S7Lg&h^jSH?C|za4+|I;(h>rOh11sJsx`J2tR -1!-HkHGAr>zxn>ZRd!*p$;HbnGzH;_WcXB4%iZ1rDQ=TpV^{Kg@Ay{jlY*p&W*6YV~mTHXZtyd-ZS;e -psh^IQ#TrTRogk569KRY<8_5wj4H-$@V4e~iPaKX9M-eLp!Uq -R=?bvJT4@O#JFK@&CvaHrrAQ^(VJAKWp3RKqu;sAj@T3oRdUL*~IM90D97f}O9L|0zQUM&!_QL?(yg6 -)4TEdB+;^0%Tqd9Ez5|+c%hn$xX{4iug)5DOb$x9f)VW|0@;t -pmb5=Eqk3)!VZ>ly9RX<@jC>S6S3svWMyc9@Oo&bGsr!zOdsR!i9G;XlCPGT%)na9Hmtj_h~S)d|CF_ -^cha7&aBdkf%E1C)*B}_%K}|7}m1GC00vh7Y}d$hv|efegM{K9;TzC*7!hKH*7I%DuzwZO3d61>t!Vb -49_?#Az(QB&2(CBST8G4X0e3U5kK@oBt9e-!xqC9!;?PBeIe59-LTdp99nKzr)EufL1=bTBKrlQWxC- -)FsxTAL0JhNyv<5j4x7qhAu9pdzNon}r{ply`5r9+xs2p6-Y$}Jhl^+(;5 -(0;{o@hqb`TQ_iCpnBQJrfUF)APf^YnsdS!+P(fql1U}wO9^Y4lfvoOH2>%1P*7PEFt*e1>`XLF!h9= -dYqSFA8I*lIczG2_3{#MboNCYm9ncPvY+9|<}lPLI1Gf&@I@R#UIMMBIsiVdT4TLi4qFbJ%3+W-U)7r -MLL{`#Rkc<`ei&Vs@FEU94r}exY58HOi0W8TrHY%II8oR9We*RQiKa#lO^BA60t&%4pc_^?UuZm63mGH2MAT0uOYD!(R8Szwd-Ux-jfd -x2f -1{41TVv~t~3bDn+KZDqg#hZ5D6x};j*79cDP!1Dsfyg8JHbfrLpF`vk-3pOMvAm5Er!p2I|}Vb5hEQKQC0tX{9qL@eFO({>QCZoLK*v1q*}6R~1FK1cf@vN_uQ7a$g&qrZ -g6=V%!spQFEm$mi%g5cwSaHN-HYR&<%SK~q(g)#y9zc8F^Gs2VZq0K_n$cBCDI7RJ+VZE1%2O{Ue41{PRkw#yp)i9t|v{+V13Vi;00x~4S}C-+a(^j}F;Yc3MWb0 -~Ash(3Ou#4si=>Xe+1vK7s!OOL!1@R+N`O4cMl -`p-2BbBec2T0|M@83z~-$th{HEC%i9K<&dzDqiD5XT06AFhrYh;ObSJ>Vn^#Q#1?vYTlB66v*s{wC@5 -g#HTYUP6DB^hWkoHWST%P0d@`&D)9Q_yg6cS2LT}oow07mTTE^JzGu&`Yc^aBcVS>dNR@%NKZ`bl^01 -*hWcORzmxO&6zNVvf132{V7s4!EoaZFm%V}$HssW$F8xU{u_8635*{KsjI@ -DF!o#HU`*ia)lKG9g`8vt`PThQiWPYn|(oN9d_v+?bBy*>=`8LViYi%M4PyANC;pt| -$+rqpT>nY33J@3l1gT+w9#mqsvNUycOdwF6rjE9R0cC1xh=k@_*_t-m{({snzIr-2?J8%3zbPm;?J8? -SllmN~VQ_VP+ViA_v0>OKG3_dMQ^TmeK&px@G98qbc9p;JpuR+^iVf<^q^hi7L@dflyUO9z@oD&Wa!gOk)$ws}kg6sQEAvfK!{EGhlG;^9r-ea#o76Bk57+PxsV!#og8wF2c^ -wxM3TU{>?BMSbN^`ptw}YX0k5qNzrHk*}Noo_(eL$-EE6Bwq|C(ek&ggzfqCyKM=_69reV5LAyA!LE- -hOm>B?&m;PM&&B7lF9SUNc(iDry4-LA4T_)Cov-L7&o(Ueo3+pV*`zpEswsM0SKaVI_opY<@QYQEA -nAFlE+_4HPwJ8>|$F7Ve?{-pu+F;c_8n$awb|ASN&83y5TQq?zlG_;fppJc0D1bmHDFZJd6z*1Hc`RM -V3hmKU1D<^yXv#b8?mq)(JLZ9?UM+YnZ>dh)az3z`X{qA{xdH4Oj{>tF~^*!qMcW-Ta>ixIc?Vr8SJs -Wm)cz(PYcGd>#y;1L?- -x;dygXQYpcFS#r-*Gq4<$b=t*6FUF>Ge9R{z&a3EPuDVy6)ddU)|NUbKTSaXgRcy!6U(=!DGSW!L{J$ -!4tug!BfH0!85@xf?o#D2G0f02fqql2wn_c3SJIg30@6e3tkW22;L0d3f>Oh3EmCf3*HYt2tEux3Vsv -(HuyOBB=}wMY4BO_dGJN>W$;z-b@2P~mGO_q4~~B_zB+zr{P6gv<44AijvpWYEL_01EG^w}@_47!^4g -uI-RrP>9d@t7?seF`4!hT3_d4uehu!P2dmTGwujACE7CEexg=mq*N_mJDd90L)Xw~QkN!Qe>(+`risp -ZiRlD?_cpdTcSQ>#foNIIuhi++%_POUcmAQgF+evpcstZqFOIo+Q1ROEDv)>DzwZCX!7PV-SuM&73Rs -3#+D(|pvEk+*3+>dDC4G#~Y34cupN -=Y_d|-?4KZmiYG89pBv< -9N*~njz_)QmF;`OoBiI(k;>7pAFW*LZ+3e(PE>Y>*N?WOva)i0(7#pL?rtAjza8~9`-5ZGZgvKn(eT* -LhWx*sXt+J-Z$yJi_ttiQFsxioI@pR%-Wx`zlE1zj4bS&Cqpg*d5UR?NusADyyBg%jAtNRVsn8|OlQOeY}SYy`p+iYI43g2NLn4LHA_gE -k7pxzfVr`>ZvauMb%3>{N|EWHPJfBi2bXE9c`(W_^6Z8fPGjf55J=tnsJpOvp-qc% -Pji^^9;2@3SeZHCcN$4YdYq&*q`tV(r=3)&kaERr>ss>3vmsB#Y|>}UYyoC7;+mNq6y&q>nJsde9Zt>cz%zSUnC*S`qca=olKAZ6m`&^~ -htHO9-8;ZmOP?K1&1{=&B;RM}XCvX%K3jNZA5LaN^~^4w*|W_xCBwr95LWFdSuF -@8d3Q+tislxH)770K-UXOOeqQ)A<^{aQcIZ0f@^+cO)j=jXE<q9p+*i%RV%0L)Nejd6K0gP&I5r*02p(!#3n -`Y@?LPu#G;(u?=-1Y~ur`+P)51!#3n1w$-5ZM6VoyI?*dfpcb@!-4V7WefAE($NZkCd}s$2&<-r19aw -m68)ygKYcC75ahc6(y`KTvF1)rKwDY~T6|~V|>JMhr<&>nrDn} -DQ1XhWTu(>6fc;Itw8fMnEqe$cMcBJDApcxaC%%tO2E(1v>GQf&v&hPr6bwu(oV5ZV$Pdk47Tp{*v&L -)$~Uq|k=!A4R5}94e|KIhO9vw@g)3$Qji?QUdO1o{LCw4Ff -PMW$UGw878`9nI_;UqBL%J=DGRjioVJIy18B=jYVW -`m<)N)6%tPA_+Ov;Gpf1!~{&3JUTc@4Bxu(b$M)|k=6&R1u3#0g@i4bb+hN6G^2DnXlHBOcmn!sZ2Sz -;7z2jZUV5Hsr}NZ9_a_2W;(vj~Jkh#29`&%+bSM0K-&kXz>U?>pip8e0|k-M2dW -7LoOoH1}QaWL!Q6Uc9M=*F*`pU!NL!X*^o76L!M;u2vm*PkTqsQ)|d@>q7{FTHD*H|XEs!g*^tMX4OL -?{WR2O7HD*JeEFD2>joFYVOGnUJV>aY*W<%AO4SAf|Pzz$Vuw*v1A7^$JjojtJVs&QDnnZ8c%bm}r;BX}f@S -Nu0LbCUz@mTdf^&0d2!7?INLVaN34M+ptRe(4-^uia&mC`gBCi*H;;B1GAkZ+J-&skY|`^8`k?dSvs; -r%q}n;F&OQVC)x&P8=gXzJkd6#BSjnSFrV49v4)?8%kHy<3uYJj6mkwLN1TKsApB(Eh)=X9VfJD^g)} -hR?kObXvNF49pKW4xjiAlWw`GU6lW?Rwk0A{kYYd5YdDdtbXS59)YYd5Y{>BmkR{t#V+cn?25oj?d1$K%E7{tSIiw>?wcc-;9z&KT(Ka}3!y0Y7$B<=NJ2J1wkfyaGS -vJQ1|>x3Cxd7}Ax~ -CT6qG^~_fDwTzLr!Du^)v=8ml5g%#$NLx+93?l7p2Rl0{Jhat>E%bUn$VI&1&j4+-p7+;7qU|)V9m(I -pjLPv-yUA#0&}Osmp{*utp`ks8NZT$Qnfc<8nMc|#(vih<+C^ -?*cae@biL{G`HtE{CqKMh7f6r_+U-Nnl37Rb$vwb=;5wpvkjyQP?=_JuE%VWrrZDAK#q|H0>&>l_Lyd -FaW!=+6}AkY2l5AxFysP5nI2eqVMe_%*Q7WDmoF1D~6-tU*6jzBHRZb(CtEIJg479J1s_+px37V6>g=tbsf; -qg}GKBO+U*&HjOhwwf^i7;-VULwcuOQm4I`k@mctHt8C)v#&p3qwb-tCah%JA)P?mu(QSv+IBl@(8;{ -t%32(>4Uu+PpzUH0y8+sEJ8SILju@PFaeLSchPJEckXby!hSx(|P1qwiZ71;v;OF&42n(Q -`@S5tGx-zG;jNn1{BSuw`65Vi%8~^>U^x4{bGJ^ -MbZ_+GTaxP$zTRkjuG=9qJ67HsslDVmCN#G%oemu$$tMY;O%40uOC9Vaw>Wi+o*FS)8^Z(=KikyWN9G -7q9rsk4I+zAkqNsB3F;Nco3Nn?FCsqk~wWwtB1Cluw`65a){sYR~)npj7Lfi?eZ-7oBf+d$O+H(L1e~ -kR+MM9ny;^7c9_p>CyPhym<=Qz#>FGczf2qQAzY>n`LIUXroA-*L7N}GhxTZ~=Jn -JfA@)iQ0go@Vig)vHHbIc-~Nldd^!Ms4lO#)Jj6MJ}~PF11B2wZ+t4TjWwZ%%ygiOKst)y?E4yTF%Wi -kY~2!532v(n)Ju~y)}>*k=nIpMs4j&5b~)lW=idRuPrR7Epn+XJhhjF+WGs~XYREPYyJ#g8}dSVZL}_ -}*LJWaazUuw$f$jYz6Bwl+CCv!Kx(^4NNl_|)B?PA$*El)uk9is$@kh&%T4WiE2Fl5JFBPm;!zu_-xO -(|kU(8DYD0FPkj#nN^we=euN`F6)^(Wq?MFLlzZ=n3Uv}27-02TC -(|;%jqjI#;PXCZh(W!oKI7qt54ox2SDg3E&zO%h859pcB##Z$B&J!nAu6*;t>5Eq`uZyI(^|NPAUOID -S{nXi~F0DUvasA?@(`PPK)+&3W(UBuluY=10xGctHq2zE}4wYOTm#a#yjmtG9*T?0$k{jc4L&?o?xvA -vVxZF~5o6EQ-iL}U?QgH_qT8$gfwIXqUPT#pnz2M#`v?6hb7Fw0pp^uj-Xu!wo6c}+aUZ}v3i}tvVF% -QQlRAA7<@oEJ|JshuAVA#X)ITaZ9aJ*iDfe**)6>aERz3@PxRxd_B)au3Phg!YxM^&v}jDJ -s9O9DiEto{2Ig>6CZHCCvCm -SM4Yj8K1U}YvN>A)JrIk}(eFd#bF>DL&(R-1CzQI@3s`7JUMNl7pUnY)lBsq3jvVp38=^|?)=`~^v3{SW4`28X(j~anb$X%Si{RxXx^b;NypP`pM -QR%Y)y@CDM6$fLaN0b){|@O%nKeBRN@O{AEy(YZEDH@9(BC6nmI`eA=3hycwFcD9XQay;RM0+9lB`tF -!5}sesgqD3X3bMCAr1^o>Lx_>fGp=z0gKCWLKU63oD|xk!F`R6B&oAbzlXg}a#Cll3HdFOlQL^GSZ|X -oPgm*$zY+`SE~+HIOtQR#;jFm-3dw1es`KB_N2ZyYz=8iu)_GcHQ!g`pHH?~wr1$@!=5jto>p*ikpW? -v0rG6s}4#;vk!voVMJx3M4gk(9INTWkkv`?p2YCJ-`3xNts7cr?LT-|@4WO>gHa0d6W826o&C~<2sK)(`yZ2>-t*LG?SDdYde=iIh!BLdBMBKs@~>YdS)R3!_y3*bv`kb_7r*o+ttJf?- -EO2eY2!%#lhm|I9QS{boZd3fH(iC&p$Hn;&q+=>grWL^ctqOigq3~0*(jswuKPFigSw+hE -6O!dz2H02kQS&;)i)pDtdqCqXZ4Anw3|B}WQ&opX5w4I#7H?Jd5~&h1 -YHBG|55v~MeVHNKk4lkxm>L~*ol&A5rf(sD`YNd@MV0y*sVu*Loy641DfJCfHWQOm7l -OeVf#zE)@F?iLx$zGiI3X<;KCjOR6jk{JR=+IZ?4ndFe9G!_54b1lz -jf;=`-glYn990-i@v3c8wiFtaIKA(%C?e)&iwQhHPD;mnj3Txl!Z0$sk)LCb1`(|e&8m=Xji(iaiieHXjiN6z -nH-0sKEq*Ngpv<-8vp=ekO2TG0001RX>c!Jc4cm4Z*nhVXkl_>WppoWVQyz=b#7;2a%o|1ZEs{{Y%Xwl?V -W9x9LH71-|wfGSb*$c$yL=mPXYn5q)1Anm65C%g2^({j-`oLGt2BqmLklNgL%k-KztlR;y46{K=26(@ -o{j#IK0@|d8KcmPeNBu-P^a@w?kjobk9@In?$$EHGR9PfAjDEt*$-tsb}u&51+Z->p#=&->vQ4AKe=C -H;&Yfefn7K#$cz{zj>l|XY|ssmW^y|yfhr#uI=^qj&I-X_IC!u<2P<~hCAKS@%`)e|Lu22d&9x??y%O -oy*C(+YFEP!cDv8sA9YWMhp%==mj*lC-Hna#8-f$H=LY+|QSWXy{FgaVyV!fVyL&(U|N2DjT7MV*rB2 -kgx;HyfyN&bLw$7cpa^ck0_Qg|M=e93hK6CbB?PTr2MlJl9)`{AKTJ1>f=&7So0D|i2baXT9RZpYO#? -ksFMo*Kr^%FHSjh>04fx)(CNe^+qpCLW8v4?Y{hZeRyPoi<#3#8uw+kTd`ZD8Bak#Ex&y-3=|xm_aBI -JYg*LlfIxCeirYo+Hr!qgP13ar7#QHZl4<$u0i2Yoy=!>U^HGtz-1}Nz`D!Umzoo1K){*I_~y5Y1+W( -4bnC~-5nCerd`rOoWo0`X*@@6lD6?2xfMs-80?XzaR*^GQ3zf8_6{IIQ%v_ZE_~>kZ?T?ze}zt>HEjznv&Mw`30Er^Xbh&2mh!-80`AXY4h6$@f50I_C4tXU8%7Q~tbv0*{1SP*L## -F_=M_6D(HL993s8y3Wh1+ij5tT+${EQl2gV#R`3aUeGNL993sYZk_EPJtLYNr4!`DG)D#1atuD`E+Vc}1)QidYLk%qwCEh?PJQ>k>iCD`E+V1&UbB8N`YOvF1P=*UuE> -aQmAhv9LkVa$qirC*HmRJ)KkJzvvmUzUxB3=ZKm<6$BL9AI2YZk=b9&zBSh*@hwb099pBNkW_h6Aw{f -VgCjI0M8W3cWy_p@=g;T%<>w0pbD`aUDHk0>Nw$D}f?b-XNA(6A}>nTNAt@=0Pk`#L8O{ORR}9Jz^sO -@zQ(5#VBHbYk~*yEIi^lfLNx8WgrHlxjc{9^6o(zcjXuB{H=+_1u?IP%e5v0i(^gT@-+gs*??H0h>Kb^p;!C}RIr6TBi;EQrfoN32*7^VWo6LCku@DnE#QJz -~v)xEPPvupm|}hKLj86bvEygcFz5X-EI27s7gX;D0435d&GN6dqG*_N*@SFw&&#DOP>1=a+si0AJSOBAuh -nkdpE2F@+)3mq~LLu(%pTLL{u187k|T$DAzgIHoslndfgm#;0?I^tzoH8FohJOht-CLo@JB9?(T!z0e -HCNex?ku?$En{@;=vw5L|FV?NHBKG%)%T>fIh$Y23-Xks(#J-A{wGod{Xy)nhy}~n3R*Q$sv<7LBlfl?d=)WoO-Mkz4$Iey1hK4GC$lD+0AhlVMR~KXC= -gd~*8LI?`>&eN9EddwV$FhBvmn+Sh&2mh)|xOJi1}3$`4zEdL9AI2vmUYLK+Lb2kbqdTAYPPJ6VSTae -Jutt7V8KoRvp9=MXU-^#5{L9xzP5$h5aaefdNrHB{Yn#cfgh9V9`iZ}zrGf~7D)`UzE -=I&BQCXA$Ag%E#hG0-p;!>>A{4RaK& -%9cI6sJaMXbC*EGgE3A6osXB-cs7#X8*dhupf-SDaNrideBARs|_ywE!UI6)_KDe?_b~5U;|fKZO+QR -=^|XK`io!i&Vt2V%7@V;eg|Cz;T#&CIXhj0motXZ-)jfhxtF^9IzbLEQ -k3Q>-a?zGK(0(EAqEP%jd90i$AH;?OvG0p@h66Fb)dcTMu -pnjyF<-0WK`e15Gz;Qc)avE{;*9q>Ae?Vscxat((FBC^ZN6rB5Nlo#dG-1jFu|N^CizZkQ`!1T0fS7%;j#b3`8_xXdHGdGBEM~$q!=Inh=24a3E%_32#MQkTt=Bn71aD6vQ$`4B>o=7{cW#V%&|G-XRr-r)}4f!@M9a$RZ9{4p$ -bjKoIjBHUbVWsYRT@;U%?*4GLm%GgKgUZPyXRyddVC32%#-uhkV}5$pVd*w>j*`9Z8X5c3vsQO<-~DG -+A}Vi|}t1aSt4GXyax-+Y1?7fsMOZQwAS$`yyFZ7UAj -&rB?0p2OZ2G5>xu>r9kr5u3RSVty4d?@X+gMJ#*2IYSUm-tT^o2Rvfk;sY6`QB4U9t;VXz)4lk%fYy= -$Ujfu5*n?q(y$Vzq4+K0pVG6$K8tIlEV&0)Q69Om`#{EdluuU+Fg%o`K=U*@PPbr@daHN=60!y5lM2b -p!N4&q7>ucIJlD|G@83ycYV5pjNFf`6;d-kvV?wla4Y&KDQ>o(cw5^cCdKEZa>PvOr9G>@z2`$jW -z6P=6rMfj(F_E$Fnry|y;=ELc#srx)6^LEiN)X%6f^7HWYfK2<E6QimAOTEzaB@Rj)-VrcEPAw2FfK}NCyv1_}IA -m$aZi9Ji{?5cYE8>MI)irRJ2{L9Ch+W%t1Tn9Oy|-P{0uU<}#EJtk -zsrOw4aB@67JyiDAm+DS;}x+IfLO60R?7+EjKyn3Dq;v1p@{MIW->Yzh+W%t1aYZ~xS(2{?{+`S^frg -YBj!Q8q}IgZyv;F-x9T){#AF;Q5WBV&i0!BJeb<(L(S%^%wQA9X>tq=gO%(WAT^&J8uXr|zHA`a6l2{ -8!tXUEpmc*JRv1UoESrYR*{uq|TnkBIjkXW-MHY|w^OJXe`u@;b6b0k(Qi6!0yD~T0LV#ShJU=pkRBx -bz{fh1NOiIsrFzTSjlN$l@U%)%swXZ#XL4B-@sA)F#Hgi|Dj@Z7zLP26dMUfLo_tOc65BJq^OrXq2a^Vy7w+F93;g#|fIJl*Fpi#H&tX-ksn{Tu{Z%R} --@&F27RM)^i8~R$*+IIgNbK5HB(@*vJ9exl_OIBn?gUR_Uro%C*xM&&-|P@<6VGd6e|KU@N -t~gHmz2btxJ^85Uy<0gtw?M?(p@pVNzAV&7HDFY#O(IOzM7c#iHjj|rHMUAEc1zFB%Xy&T*rOl;Xu@T -MdE4Oip2IalQ%o&Ph!@c5WLwTa3>T?;zfP4W3@;O;U&_^^g)JGwLMN8#`a?%%(CeXv -`5a_>(6#^_FG)Eo4la@`KP`%%BwySoG1*`W5yV7L?ghc<=Uv06L&N0|NTL4Pz1yNOTDp7&|^SM5?~Z_ -l34^PTIv-6!{-Ig{?D}U%0xhOF5F6nTQ1zDVYrneMONQ*QDBKqslNoAu;lLBp21P4c0j -875OtJrXp-TsGUu2ZE;cPg}{yfu=39x04S8%`g$j10DsMwldU6J_s~z9;p|+o7PR;Og;oO?ZSzA@)4j -J=CRpKJ^-}BJT@Ci`QHljXu5oV7h+)^o9_JJfuLpca4~osXjy00eFr=Yw8A{t^6aiML;!<{ocEVSv|`U42kx%J|U5T%pr4n_@k+;K2NP=(aL7yX6!8thS|88*u$7jCbghYt1ub&H -7{e#j?FZ-i!m$rQ)^hXRTOSqV>uBfX%8>owX6iH7^?KgOUf{kddNTUZg{ro}$S8XsHIq9gH; -@{Dx!1nDltT?~fVG+dXjpx_C!X}Ifi4}*4W+{L($LHnGSdMRBSI@KOvl+Lw3!6=<b7!&cQ|l;{S?qJ90 -3dk01UM#_fY9-06=WzJqc51l(wU>}&1Qa#KO`pJH>HY||w>iF>!X0@EHp{BM+_(S7EUonWrfE*ecYWh -Xztu+6pUa-IAF!#35XOLY?WZ8L2)(`O#-DAL44zlF_hqRlqMzm0L5X$v!b8{=W7aW~(^u+6juqy6yv5 -Vv}wfxW+pVVh|SFU&VEZc}Xq>64GPB5An`^Z50+cbjVqt_%*I)fT#ili)*)+gw|25=_2>;V4y?)8t<; -98uGq`{aKyY%^`cqbl}oWrl8RxDPt`$PoQyjJ8Yl(O+WNX4-aB;1Ldol^ePZQ{XiW+eF(g(F2?rR&wa -JOZ4z{4BI^0F3$tJ2dwab`Eu|>j79`<*&qBHhNCoH_6J}YXe&UBg6#3Ovl_%Gm;J$yFl@=HThHaYggLv?B4BI5($Lip}Fl>|5F89Mf$FQZYhTOl0;poQ%vcyYbb&G*t@Iyp}l -`aMpz$3hAt$H!=>vM=RJQ6VQn?3mm!!}F!0h{10Yc)(A9Cw_sR>ZX6uT$Y=jZ_T$$REaGJCI-%;Vh1X -4E!!1BHyf-F$S*FUtrj#Xxw!g!)<1)qA{Rz4*v$jRx`Uk(6BR+ssUMZ_$>_EaRg;Q{%Z`|aWruC-okL -0BmC^4>1QH!6F`oCi(#8%;94KXVVfgp|HH3i*yae*ck*Wtj+_%v;uB2WN}W2K^_v)t#134iZ(!KU9r! -K5FSL56?lMG6(nPKaJRyw#0mD&>Kv%qak?etIk;zXmY*k#^)-+c6z|ZRhZfO%qADB#UW7JMlc%T`77s -EC|c;=dXAH#MK;Spy1Jq+7<3YYHhF>1LA)8ieCh6%#Y@Hp<+=C=uBg@a<{4+w(s4=@_$2S0m<@Y`V`d -juYH4sigj^Z}7D{vk%A{9yj$1dD_ZoZPz@wduL&;kPjwrUyUy;~!zv-i2_dd@Gm#yC30 -F|~3BeqIm%4x*7s0uQ%`Z(!ICA3Ot2UdM2h9?YV@!f+&C-50)((J(#GOXHtmG)xZ$WDG%@c&MJ>g_~L?Ft`2#!dB$KO^gnBWDM6KzxOa|^Mh;u5k{l*po?E%m^vquf5tG?=a0g(dvz -nrP`-5e%-M^zleMe8{>|O)v-d|`>q=kkjxG&$y1P$W!+O6v+U|F5cTXODrPtpXyt04H_HlGt29*!Ehsq=|Ao%=8#~>XZrE+$q{lVUFe{|>Wm&dP-zdHWf_|5S*$8U|_9=|h -wcl_h=`{NJBAC5noe0}o9U1U{&y_3LZALHU$s&9aX -`jDtOeYg2$0%FL;1k^5VS~YN%<&L*W4wQPY|Sdd4kOQPZM_!2vQY#4$wDCYIE|%B>9X!w`<&y9KirHxaB+eXf$Y}{A&Kz&bX%QsO9Pi9&8 -6?ge$75Ou!OYt@9@A0?X5Pl}nASot^EQr0R19fi=4~90Xvgp-X5Pl}nASru^EQr0^mj8_n0Xt=BT*6w -58!yvnutAsdu5@nI_0FFnZFcKcX@kmrg!UH%SiP}hb0LLRy9SIM_PZ+d55*~;jF>+N%NEKu -D6%sVW6BUxU02ftApbt@nB(C}8sgT6Ay+nm1F5=~@ki@mXUs54~*_lxxi7O4WuaLwgg+zrUuDm5GByn -vlPlY6|_T{ON#8tgSg(NQD<*ATNW?vzJXr2lQ@TaPftb2t7ern6BkN}|aR7fD2t3m?#B`YMrk6aZJ*e -2VfLINEpDpzR?BViB6Y!AoGC#-19t~T=V%EYybiPCjbBdaA|NaUv_0~WN&gWV`yP=WMycaX<=?{Z)9a`E^vA6U2B^hM|u6OUooI0vI(-cdv<1alqiWT8&J@NNH~PVVY8CP@`lx}vOBV -+D9W=o0wmjDFe?ED%>8OE=4!6ydBpSN^9(;hza(96_j&8Bt}Z;qRL@PdU%;nZJ^l9O%$alQt=j*#hcE -AJKYVt5^Wom+(~V14`%i6c?%Uru_>O~(b6e-vH!mD&T<$-4a77{e_C2}1b+K`2{nA6LPxm&@Z*4zx?y -0ry^S%BgXjf4H}CHT(a`p~mBz8~CrKLyeQY3v0CBzSEDNIdkmD$<^aWPQH8f#HmLgJKi|lxVEp6{WUv> -8rK?){fz^IPaPlx=+ZkaczO9yBPrldi@lo~ep&3@*6?QpoM?DYz$+TQt>Kn9$Q=QTgM3yTB-QZeG~8a -&@aM&Oi(|hc4${@|7sTG;1ivWY>`c-E{*r*jrFvDsEe(HJe58QCBK8*Ld{yi%%K4hOCSvce3s~HpZwO -d?LM_h05k>3??s-LRY1e|F2`=Uzm4E{iTq_{gj6nl%0{E@hV8hu@(skmo9)^ -M_<;hzY&tKpvtxT)cviDT=t{<%QK@<$C@#KcQ?Ym*^ITmr_Afpj -C_AU`O;L(U1dzXkC@L0qRcr@b1M~+6^*t|G*mz$M}aTq173CE^BLB5uGX;>HP%McjZ#B5s -gOiMVl$5^)1A5jWrxapMzB;s#-sh#SW!5jS9yxN+7c;>OM;;s$IIH;z#vZtQFlH&BzfQA&xpvGXXz4c -;+{8|Y}njlE064R|!-23#U;z+(|N&bvh1VCWKYWA74iWA74i12&0Uk?u8%bDR2`ay;V3-eW25I*B`o; -`Sr%DJbqbiQA#L{fHaz=JQf@6m`GdzXkCMUF+>4#izf+=^YVS>*aB6t}}F!4oxa#61PY?MK`TrnrMxB@W^SJdWaa5Vu2dR}(k -T8CBdU(Ijr5QE>;fN*u)Puu2??+hLU?Ox%J+gIOgG;&xaic+y7mxTm1FagYgGB@V@1Cvp3E+(E388Wl -IVXEfq=c-#)+20RjR<3vXCxFd1%E%a90Qy^}f$>gjO#h2GCy7X7vF`OJ%apNE*;s!jLRf0EaG~%AuaM -FvovGZ8O?G;Wsh&x0$i9?6CN~(z)r%@tq>}(P@7-kf!#6jGEOT>*M*FoG4s|4g)M>q+19L3$zR*B$6f -5klotHh7E{j3te6I9%Qqg7H=S+7;%@VIe|(LC$6ILj-`N+_Phq)EQ0&u}ZLa9af2hxa+h^aO@c!$L&Yl)uT#q)>9r;f&-6al>`{a?I3Q4RpK{}+s`TiJgZ -iT;a@btq;>^+uM;s__F;Bn8XRpRGyJFF6i;s$IIH?H3}iW_JRRtezXRtXqp48 -;w2-V}Eb;&v$RYT|Bck6Un8-Q&0c&#cECgt&19=WQJKViC9Bs1np(!>E#pTP0o%IMv}~fKeqOIwu{(9 -VDEr(<%Wx)~J$d;#Mqm%_b>t;&v!*hgIT7+zzY6p}75sJBU?+V~=H(1X0{|dfa|iiG#Ry$4+!GHc9Uiw2aZg%t-`B+L2q&vO?xr3NDA_2K$6cr5o*m+@)8h_8+TnY1LXRp5(mCl++z#T_qe>+0)U3E29(NF{1m`^#abxe9ZNRCMxPyd~K@|7Q5Vx -0A5@N2B5X2q8;|@UFUWyw8xFCuf3_Vuoq=UHiBvX=KYMvbs;8|_J0Xo{K5{ -Jh8xiF=X@Da;CSn4D#6|pRNO&`d*&6l!{e@zxE)rBgSfH -xA`mywF%&n@NwvJsD{(u*N$g$6LJB&Z6fa~XZZG0?C~oXM)?6hH;+{e{S*PN5gp-q3+<+H>xC4ZfI7I -N_fPSM&W}dkH6t{!8{SQLNMAa2JvZorYa8#@K!%6wP&#}in^T -PpySS59O+%r$yA*>PyaXUtpgdlE*Rf1!TGaq*~aXW?sM&dSz+o8At&nj_i-WCsb-HW_Ccsq#Tp2DD#I -(ge6xB*9}q}bbcG$3YAD)Bbp5^n=8@iyR*=9mIp;%&eqEr0{K#M^*P-UeFYZNQ_fqu>|~h@D57jvMGm -PKjdy9C$D#-p1Y~-UeLaZNMeo23+E8z!RLR1Rq)AZNMgPr#hPykMKMbx1UoIL~sL+#9dtPSt4%7EIVcO>owC -vJ!0c6i(l#T~*bsh*B|wiS1E$NQ`(ZimP1AZ~|M;>afJRNP)xiSC;ek6GmH$lHs;+w+-CI;NPKbT&C@ -r^GQ+$%18*L3n$aiCa8Sk+>sqFA8z{32w(=zz`Pq#Is4SQQTgGN_^UHs+|&W*I0rZyf~Iqf`d$djdzD -r0=PunI-3+vP9*L~+zU?JlULlc;*_Aa2K)631Xb2XXsFlR -=2vPjKS|OJhoO`;EBkk+>sqFA8z{4F+^5Zm)j#$vY)pg8_Xk?f_27%#7mpYrkcI52^=WW048;4WkH5Sm}lq}9z!0LHQs(IVb;dTgazqZLb1$U6KfL=~Xq6Y(t8ySf^5_cqSz;36+ -OK>~dCg-vHrkc2YoRT_;+pldh$OKb<(WJxT_KGH3%-iCQMc$6Q9eEq@ybdV|Fc#3k+qF5|L3rCQnsf+ -mN4qNqv#O;?%;uzyBTIL{b>^# -yE3bUO|`gFV3I86z7ao&uQ5M#J`dMSCiBX39EUU1&_o28^i!98)KB!J)^-{1~nl=unmAoBwH@wUS#aR -_d&Zg*a5QeN1(;BDxJ$lJv=iM(AKZ`auDK94<y8!oVbH5S?1L=StD^*j|I -e$!z*r_#wgwH;GJIz?n$Hz`*P*x_9=C(Iagecyo4eg*L6Nv4ao?ZB?U)x3haSf&afFj0tP-!mfB -{xes3z`es|0jZ!}L-P#qDR61espSp|~C4q=UEtk4D_weIw625_cr-`;)l+6t@>~I}~?)#69UzB|gL*W -K>BIt0aix_9N~fR*4QL#VJPOj>Ns-#EqRNr?_h*ZonpS106|mgNse##=c`HZt#vt+(1Y3xUu(G;UwTX -)+}=nx1((`gyL@L;eg^?B5_CJUKHX6IvQ~U9*4O7!pUml23iAg12%~ps7c&FM$*8mDCwd`VqHZ1 -CCbQV*g0oQE@K{ar?Erhgc@qLEK(eiDQ{yz>^|whgDMB+<*?n9b~mhzqv~4Y-V;I8vUo%`i=AJ=Np?_{YLM~`cA*GzS-z+udQ#cZ(eBhdsq7O@3V4_u5E9xuU+Upe7e`ayuC?q_L% -Kly?AzOqw&C{%bVx=m)H91TbmCShu!M!&~eZ9Hn!Bkwi?fDZJ(!qs9x{J!A6(9`I6?PXaf~O!$JEbtbstiCJiZOP+L19M?QGZ)k#xo|@Grxaz4{ZGy|5n)fupbx+N -D6I}SztT*WbFV(vSyi_l)ep;#*S3fP)dmfKosa{aMSy<=u&cqhA9#E5?ktHtAHWNh2=GCGvZ8kYlokCoKv~f{0m_OF0+ba!1W;D=FhHuPpjAA~%U -uEDNp39(5RY*y5g?x7Rw_Vdjivzc^wLy?0P*OyS^~s#+iD9C4{fU>Ks>Q6d5;bO6!+-B-+-~?J^EXK@ -*W)qDDTnV0hIUX?*Ynt^bY`)nYCP;=MjMFcgS+F=OX}>*|c1c)KP%S-(D`>g+~D@DX`qm!N)+h>X*=R -CkNjJQ2pp!Udh3C15`h%m%BOm9)RlSaI5%6J_b-pkyfEP{t=+62v`2S0JDlZ#kn5`rV7IKH~}y#s8iH -)5}2w7^n4265~?1s!21BIdRj&0{{&Fg(<&-I4Up<7DnA2|>M1IJKR{JayQutefa(sli^~5Qpem?cRQ> -^g>JGJwD*pwbs;FI5^g(>VsfyY~MgIy=K6C#DP(E`{JONZbbpH-eK6D=fD2ZVepd^O>0GO53Dar)DcU -485qD+u`H>;>!TzLLkT~ZZwibI}fG%E>Z@^|Wzx=DCK_#1U8E2&c)^#X(HF5ww@iqWhlT+wv~)osE7{ -*zI4pE|_@@nJ^QlUINn3}$7a)QhYxsI21I(V9b*RXjUe3}$7ui-TTbP(6E{V%Ps-G%E`SWeK9R1}@+Z -gQ}{P;)!G>M4jv_#l5=BsH&>?sHYiJkKamB)-x=QR9P#~QTYh -|52NxKyw0e62>+K+`StX~l@322w1{VMdD($Ul%N1nvH_DQInl(=x0e~s$^%uh{gNmtu?#nEkI}3=kR_ -Whi4qf_9QIxkr6$0PY`r8(PJry$c}bL>XqSzbLC9V9}!9pLG8?`>uWrA*Q#)O(w`M2VAh3H9D)<{^EugnIXwgGr(+q24{_P!cIisCSPaPSPn$sCSRwF -_J2H?(gmC^HKHU3A?wq%ebl+kJG(9ex8(QNfWM@c~9wCSM?@*f4|GP -syE^L`z^**y$RpnuQ0CaP5J(Qig8tM%J=vCjH`N6zQ5mQT-BR$z29M6)thp?cy3cNQc|w>n~bY^Q?B> -(jH`N6uJ?0{t9nze_hrUay(!oG8OBw;O|JJf##OyduJ?P4t9qM!fA#08dhzvsZ|_CMiJnmJ9k`b%nJM -_fyLaaXvJgZ%Xb-ab9fkrsSQJ9o{r|P70JS5m_hrv|EfSxq~x%o>4`@__P-oCHY;Td?}UOX_md+l>8Iy` -$zfxgdXHS~_mo|Irq@5Qb-uUpfO4{Tdi~YSwTr#O2l9V=AP-VU|JzvGyl{ -E#LUw@Dmv?s7*EYS6piX18zqPuxeZIH7dU5U2;Xh@Z|GDS4dQsJU7{@$**IUC{?$wC=hik>FK -z9t_t&59t!}GC;qcN!-A=omeYJZY?!&#_rPZ~abL;D?8@;}Y6%M~^ZDXhBdz`h6OHZwx?e!05Z`$Bfg -HI1$9(-o7H@H2xGx+S_bA!(hUKxC0@WsKG2CojjJow7stAno%zCQTI;G2VQ4Zc12&fvR)?+soXe1GtR -!4C&N8oWOE@!%(epALRD`1#-$gEt1h9Q%ngZza9K;@aEw6!`${8QvIvbojC1$A>qEw}#I -TUl_hP{KW8+!hqo+P_W;Oc_nZyvJ5krth3_)TD5<`#}g6xkWNDM(@2tr^CLGGgvgss -j2JCw9^E1b>&JOqkCw9&b%gM~yGokJrD(LU!c`)19i^f`Cgmv1ghpL3Tp12PEM=iFsqlq>@FIiF+SkK -_gWoV#p}lfr<{Ik?3z*_QYgXbA0*AlRzK+x_?tt1%1DvG-W+nRq)Fgz4%u8LRl;@^r&6R#QhpQG_%{Y=leBEdP;|oHCfhN -Tov^hzxXCa%l|`ZKm*nXQ47ES9&rSIWT(Z0T8c=`&lj|<$Q*Cl7@$^2+FqxD^op(8*>X6NahxZ1Qksu5OnxltnUJ4{-T76a5CvlJE+_BmWgu3vO$msVY^QQ0KRewdcgo2&1t3q#Qw(|g_oNQA7Qckw>{G*&~Q~qHk+mwHllkGhIC@0$#f0UE$RQ9RA$zZo4*{ -1NrN|uzMv63YvXRKsNi5WXtQd-7NmPpE2$r1?}D_J5LVT-L6w|Gs8xh>{K(rqW*D#KYR5zB?5yu)r5A}wNdz9=zbo8A<4z(f*>3Eel#)~Be0hv1v7BYFtF**c84?=#l+q$A});Z+496k^N@Jh}8h3B#YhCMw*4 -Y07~5=+yzkT7U3>{Qn!fi0x-KpbQgfxEy7&@rEU@K0x-Kp?Di$4WGZqCO6mAInw=utF;MCh;f{e)rwE -?@a;L~LcSx9>B6!`(og#P)%AF$k>7d7fD{*5v*GpAhhLnTYel|_4V94ie_0>_TF$S|=3m8$mFydT8Mbd!k@bG}w(GY|E^TKS>b1)^c^E)d}^dv}4 -fS~JrH;xfr&7fAKr>_r{#J#}nl^>mCn8Zqi<#Hb@i9Wm;NQOEumb;PJ6MjZsksN;T&I)u4pWF5kPG&? -}p7BSNfVa}N?AljTWTR`^obXGW?k8_4Ve(N -ln!Z6JCI5VH?N_{C-;i1v%kMiA{6n~fkZ>gl$OMvyxiHyS~1>Xrw`2aH~jyF8_p(F>vn@*BM%f6#MM$zBlcf|{)$!reA{LG=7Il!@4xUfv7B --Y(y&ycxvuc8hKh_I5egpmq>F`wb^`c|QnyyX>{+4I%99@~f41gs`{Ew>)nN;j6&6JnsqNtH9Y=-W0- -DfnU+QCxq)|k3Vk;;d(hI%ez9jUQWa)D$!mxKeBmW2-nL`NZuG?^~L0kAzZGWG>bYzxLh4+P-}=axlz -#@!sY4|g{*Zhm*Xw7J%lG!GTTG+q)KLgh_>|21`$1pk=Y@lCp|J-M6`8p_K4{IAW}e9z}Rh8#Dk=OtR -RpikQMoLE5p66o_Srzc-^WAi6|$rhwf8OVhtu&I>;J%pGPDG$|LWU4zfqyDXSp)zS*~nXq8*s#JTW0vQ>1uz`6APjAPDiqgj6k=;pxf5NOT~>(~~Tb>_CL4JBvtoAi>j9Nl}V{0IxKy73X() -hA|2;kl*RvEXptt-|0>+iZD36yQL!rO5^b0aL*Pc7~G(wdo(DEyo-6TD2KyK%CQ}V7i?0q`A*pdyVQCLE6U*Td~%l;B^PWHo4p=v;d9>>PtT0KxJoAcD^0p1};|`CnZsee!cg#oZR;Thet6pUyt?K@ysH37AB+h~rXTh5FS+Lk -Xqv=<#SKG>7LC1-;b4RakuWxLupZoJ>uL?8?mYsXNrDHzU>4u#FtBwhrq3E~7Cd+rXO3=nTm=u;?;cL$VD`-XMEJvJ -K0;K{khE8(O?Uc86pe+Pp!whh!T%yg~MdWE)m^gKQAVHgtJ|>=4N|tnmifB9d)j?Q8UiWE=P`HJU`S4 -g8`ST_V{Aep!t+k!%Cs52H`yi7Q>cA4a1{wt?@5(J7K`;QL{;iewx3ei*$X*#^EJMzct^f$xXWEs|~E -`(d<;WE=Q?82uvI2EHFg!$`J)??u2&c9Xs~AbY&L(m|G?PZ^43=#vn#n>CUuLtiN&+t4TH -r2yH`cXx(V^mVi=4E=ls^#Wx>KdGAWj1m*e&`&F;0aH@QGW1Ca6(Ae>6qQ(peq1meSxMasrpsxwlDAw -jeW?31D9S^a%?YBhV!BvJosvAnyqrTCE2a;P&Jev~x}IfAu9(hQvAJS8=g{VY=^Qwl3#RK>)mSfG=cl -7gl%NNMXmC8Y-0y`Z58Z`~Qqc9Qwo{hRv#_LKSEY?JW)7d<@t~%IFo_a -V>E~+12Pv)1WJIU_WmbyaweDKNX{&+sVpQyh@=Yvb*my_Lx>i@&@!3&dJ`?uEl;7W3PWZG@rc<#dU!% -G*gJUhI6_339W1>3=+tw884Mn^L;W*x*9$p-NdB**E75q5&U0BdwRS6l}sBA>lG^z+{YC-6_ny`rQ48R@=F@Sl@zDE#KbwaGLV0954+!JY)|Bw*=wBmO?ParhK -#7MC)d3-%9+~}vlt^BA()>qLxg~xw4|JMy#E;vUBYw1BB!0j-;s;)k_<`q$A9z9H2c9E-;7cZcw9gSg -+7~2#%%>mX2c9E-be|)B;5p(4UXb{KFA_iC9PuNpMdHWkbHtBsa>NfjNBrnMNBn4?BYxmH;>UP$#1Fg -}@uT}3@dGbM{OBe}{J?X>kJ-r)Kl;xRKcdYMKkyv!vC-^a( -9Pgw1g1qla@Pi>sIz4D#(CNW-bG#3xEhPBSI>-CKJqi9^M*Nh5d5a&nB}e?YEyai*t8GIBzm)hf%>{{ -HXz_Ez&pAElrljDPTKtUQM>pjxeyP)g?n?@O&f;gB9x3r7Tt61Sl=!7ik1N3syrk0uyrAHh5kKJN62I -nJOZ?=@CvbWg;s?CpP7g=?oW&2kpwk1q81Vzn5kDR}PsES@mrDHTrKI9_Mf|{vIX!?25z1@n4-<8#ae -u^o6L|YS!pG)eg8}Ylb_Fe83KosHr*{j3o23l%>eqJn$7H6wn&IpPOiPVr-Qa>S4J_1@d%h~Kl~ -mnQXa*_@o>=ZIgrw_6eM11@Iucuwlk9BGN4QY5eVg&w~v;^)%%IpW7^+YsUxDt^GLna#;rJu6Q9Od3C -$m$Z6>ieF0nuB;y51+5<69zRF?TpB;x*LicdOR{<>74nWMSrr_|l_LHX-`uT;`0=dxA%3aVBP4!T2X}!t#P5LkJ$w9`!z}UBo*Km;Y$WkFioa3(=3brP09Ql&E{Wgs! -QC#Y(MEowA%4@(`VzmiVuLHM{KsL;PUQ)g=B^QT&+ZQp8`kJbqz~ -HYxFQCwCd*ca_ys)#mPsoZPLK;*UsiPC`BIsxo6$d8*31>WUw6m8{hxEZM;2@k`S=fqPQ?!dg98ZOg6 -IBPD)eRc2QnzpzFd@RC)TG5msg{L+#QuBtLiYxMxn5kK%kdHfX>=M2f_E}@tc)an6VOz}&rGH)93bG3 -SKTk27j**EcHoJ+0M4jaY -g)`)x!}#XY~k)pHuw4vwEuW_@!14cYw>c)x!|KE5+~V0GF#GZPM?spr6&WdW7ZpJ$w8iA$~#`Cpfvwl -x5~>^)$pEoTxgdl=yL5{FG&uDt>8MW`_7V#m`thoZ?3}1&N=tdeDEtvdo@KHhBI37f1Y@)r0mW6~DAr -kMEPa9PzuU)#F?7lasrI8r)^|R8^#HRfwNc{J@)9J>?a@)Z+)f5m`N)$ImH#S6^NPUQqF){YGT`0bUd0_bvEcCG~K`&j^0b>E -X)pyCQzh;ujLXutuA!yfxLoX;KgH<$i$65%F_h+?DR_au&bm4{)KIV#JT;CHHn+IXxIp$uxeB_@x%V -l=z|H>s*z&O|m)3v%C7=VObT$Uy;QxCH|UO{2cK!Ru6h9N&K!Xe&OD()Z*ueA9y_~HgJ2pXkRd?XGJW -2#_9pVFIVyFhtv{3c}AOx4W^&0$;DaT}Q+ZykIsbm(;^4evbG##lIrN&saSb5r3O}fQv -qh@@!7V>S?k$4~ZY~mb7{};^z+Tq8~qBUi5u_%Z=jatR6I9^1)qB@e8dU&mKQV{I1S#ahtoG)#K{?me -lHzrtvc#KbjXKe&03PNE$zR=IW>Txy{{GA%3ai_k4Z}a7`3H9_m^sevbHEDgJt$-;yeR45wr{e$MKVd -i?yjlT<4Eyy#E9 -}G^t0L#?S5TO0zkEm(1n_GnY!^uPCWUKP{H{$-{C?8b4?8GsN$zHYeH_6#Qsk6XNFtf6a)WOXHVjbGj -1zLgHrxzx3QLuDay&TaL0h9i89eh+mq`$yxlaD)n&0&pAD=()it+-{OehcTwhy_sK)y?)a8(-uEr|G5 -(UgkA6z>KKdzW^f0_HJ-3VQ3m)I%viKdH+r@CoZS6`OevbFiKF9mOmwRrPE5=`u;OBCBTnT>e+%E8vy -f59_WwQ8%=XO2wevjnzkkwzmt=&y?_&MU|9DYvl*UaGuzSQGefS2s>bHrbf!w=j~GN*5cANWQ%{J=dq -J!ET_tny!ZS8W3Uz*Iht~ouvogRkxxh#I)7QdAEg#{a!(wx$*UC!x|p4)Zg^l*wF-S|=bT)_t -4iob47k5uuapITSyAyyAr%_p#Wgv8(M?H*73o(nce_jY|-{G8RpD1OfBVTiw~f(=sQ7gp-2$l{k~@k< -pyNBo@PmzHSbh+kT;f!o_<6u*%8J@4&y2=S9uyZT>YLH{)&eyP>tIg5YA3pM~=ZcS!sQV(bKcrMt$Sv -^b^zpK4n&g$W6b4slqX))AaWEGyEHYY>;TpItT6>Q)XzqA@Zm(_#*muvO- -CVq^knAO9i@k^~9&f|Aguz@3fVOEc4tH)I}e&1O=oX5`*KWFuD#P3M)3rnNueIHs{9 -|{YLa@67yKJ`e9KW9zoWffSLb#)-glJ7?^*D3Nj=izTO9981wZHXaC^JHk8cUj?V_7n6zb9I=h4ySv% -VI8-H0Ffa%(aJuDQj}2!6nR()c;zNBh+*)C0Up5!Cg-AyGrBd4(@Wq@8;mHbZ=Lx_@%@ztklD0b8-iFIj3h$iQiQ=r&RH;H1X^8@aX7rIDy44OydXc -$?8G>C5c~})Pw65OzM#mKd1PG=eJy0JyOLlC4NTn3#}ekieFl>K}!6M;$LIMFSPi56Mx40zv@{D)pfMT3h_w)~=NI(SEstpF6j^ro3O3;1?d>s+ZtzlPrF6e~( -G#fA1Oe2XD|G;aidorr&>ot}v7?UMWGKdHx+;^&CJZi ->Guiyycj#n08|go-KY(@dGben^Q>qoZ@eY|3pbW+__!9P7jyVhY}jJ7jN{+!uGpw>ZU* -<~6r^d=o!s^`L!|&G`|;FSL39Z}w*w*Od4{ZOz_papKoE;L*|fuU{Jfx+?yPh+mq`Su^726#se>zx4c -8L;MZ#A71sC&0UhlPv+TwR*!GRUzNuXB42V=kMQ6wNBmN&N9yrA+T0aZ>hZm~>#8;MjveoRhHs__dIKj4P=CB!e?+}$X}A7**{bl -#33e&FTOIk~JJ;03dK(0|RX9*+1cI=Cy%>fwl=Q~a*-_|d)=CE9$S-(s?Q8mng=tRBGiNaxIoGSm5Sm -(}B{V#B(6{G8(Fh#&0>5`HgE^HTvd2w@k@zcX!QVIvf} -56UugAk9zUA<`TiE?@k@!H%j#ju@i)ZZ5dYy--4TB`JHSQ9U%xzl;A^J%fj0-Z94LN<`0J+lfj3z_4e -?i`__G6Cbe!%yerZ;ZE8@p6Yfk*!0WQz!oYKwRnh`&@x$D{LakaV2DSn3do6X%yKeg^CkH22TpB3k%< -8jCP-1#k6Nj<)uo~nv-a=h>8{FZcYS9*Sn%jT4x-(q-QSe4m#jW#Lo3rqD#vpI#IVL|r=KfG8s-fyZh -pMdv?-X|Asc;EC>L%lEMeWBj3>fA1u(<9`4M>#!Ghu>9B599EA&f;Hn-gi}^ZKH}Zw@DU1y@H(hg*iP -4_(>K&`HhD7O+O9sn_)J@A2h_DcHa=c`BNS7gUAaKKQ=&eTe~EglT -b`U{HC9V_{}gI;tv|)PrGl3-~3q}#gBe6;(ujp>y&z`)s((4*-2g+Pm-PB)}7I8bZef>_JjGI(LC50? -*xEwyD+i9|I`n{R#PIYI~;8WAtj`>A>>w_~vZ2qF|)5YmzK2zOfL(}8FX#W;m9_{Vv0o@qg+)Ww-@1d3U? -OaP6~Gy;cg1|7U5nB_ZQ)Q3J(_HK?=tdp4(tdkew8rSTq;oO^fG(yk+q=?PX@sT#$*yb5Xuz@x?GhO! -#7)p`Gz?G0@OXc^EI^cFv>48yeb4j~1g1?W{+O(S~-~qs4m~+If!_;|=Y^M~m@>vBh)ojx3&w*Uo;Fi -`UM6l#6%AF1=j5cK+MBcd;c>=N*l1$zYiH4F9$`0ExN5b!rF7-!&Pryon7 -ofc{NxP72P0zP3u8qvosNF(}93(|<5v>=V>j0I^#XDvu0I%h!>QOd%0F?V7D+LhdG5zsDUQ(e-6b`5t -U0)jQ#1hlKyoOraLUAo;a0qwdq?+OdrMceHY(5~1ny+`LQNbb?;-?EHF@6m5tklv$h3(|Y^I~Jt(=yx -qh@6qpBQ17=5(s5p}p#Eo1t9J-KWkJ0^+3FvH7cHo_(^~19?`aF_m*gP5&@+~7wI?=6Iqnk{)cZk$^i -g`&f_kg3mA+-4v!IG7PJjD(i|L)qK|0Lex1b3q9p)cc&;&F%1TR_81QZ{Fmo2CR3e$kDSWpKPrX#v)U -lS&vw9ijk&;*qB`G*$N0mW&b*DR<4vJJ0WOa&CDvvI>>I-W4SK`&TP#}lSA{6`kl@xWCr*F;Pwo3n$784X&n!rn+@D*JF1Z(9v?yJ3e_=to=Kj)xloy5;q`dG~7E~ei(ji%Tk5xqdG$2d -uu?nc4{w`jsEgevp{`L+~6;VG0@kVXwI~1lfkN0Uy?>>g<`nnBNuew$`fIC1{OoMdp#{hLqb|wEcP<@ -}mbcMbQRE1^N$Sy#AqgrWTccCs+VEuIAWs*b1WuN0IKowV<2CxTE-zXcv-vHHXFC8irh?W}tbdl}@R8 -iRn89K;hFSJth9#BIM>AcCNbiw_2nTSiY<3Q<3R-ZFy1(un#21eZ}1_8C_{XyXSu)DRaRbnpYs9s}TA;5uHr -=G6~+_<;^NglHM`@dF*TUF7Nq1N=Y-ZkMV0L5v^hz{Bj~BmBSwp3MkkuTdOPHY0dMO+EnW3Qws~9#J+ -`cv6k}h_b1|?Jx=?!c@h$9Y%#jn35Q`Lo1OOqm6Mlv=|v+refR-tw#pg1#mBnB#E+Lz|AnSB*N6jxEV -&8M3~wbH^a!22=fu+W*CVQWrM}dFfyf$sf}?nj8ti324ma|BUjp(!5BBgNS1aMAZ~_{E$wUuvww)_$l -F=u*}p_|&)#8wSsBGa`-f09df&G@y`I)5qB*7S->XVfyMs`xC*zAi+&EUzR4E-J -Ww5J&%$2-tm5n<4anLDp_VjljO10oI?5v%(V>ww_UZW5hpz!wadr^q>ws05U5xMzz$(lxT7Ma^in4>g --vq3pwA_^$sG$})UChZ_fK`~CBcC=3wa|%hf8GYHpIuz?Yk*al9ZbqQfc3i+@VkIjlpRF*9^mdrBu9r -v^&s~50UL(4@CSf(-aE$kb-*gf9`2+>mYH-ZvxgL)uP`5ss#fU)wcoDEYI -%%RWbICB*ukW2!*)dcLAFx6PNsV;955vITsmeMbtS`>^#zvsDlCi0I*)LxHmrpte?vxsdAB)MSXPgpM -X`An7J#lKm-AonnnE{tU1tTwL+zfc0tx{0qQ(wH`59B -V)*5UH%fVJ_3bde+XC~=*6u(0Ic6G7~uZ`))bpwGh@=ocrl&u2&`W$=^du9jxv3+DXi~g_N@M&y{5@0 -K5~9DGOEe02Uxw4mQ9@_Nqv#gO?G9%=#8{$vhSG8=Z%bJ!V`61**nsbse8o!jm)PA?2N>57qI@P7;gQ0fK`|SL}}?eGI9uHfABtVvuH8I4} -hD0W*%Akhi=W0rBD4D3KOYs;8(U5&FjlopMK_2upL|9*OJuNk}CZl`>Q+P)R-|@065NvQ0@erN)%B`1 -`0P%=SD0`>Y=?|SuKS1)n(!d5S5^&VF5T=jlc?^5;NRPRXjK2$G1^`cWRHT42hFDvzCQg0#k#!+t<^( -Iko4fTdlZv(~riij2IDuPvHs)$pOq~b%xeTwH4sVN3iY^9h -N!=c!Jc4cm4Z*nhVXkl_> -WppoWVQy!1b#iNIb7*aEWMynFaCz;W`YbMoB>{l}BO#)P6FZJKBkdp!TJ3C -RXJzCKIf?Udp3ce3d6)w+c8p&k;0HK{B@@&0H!x-(4wW -zMwDk8TZj?xz1=pJ`p)-JyS}Gp*-`HwM}JHm+Q~^xU(TpV>P9`14P1UAXwXF50(&gwAMRc=>o^#&Y=jfBpaf|3vMYL@=`YGoK9gaTjd|I2M&p1aY&iC0O+EE<+bP?6 -Xai4RJTb$W>=X{GJTyWl79N~Eb$r3w9h!>P@LIsIOk -A|*=L>O7P)=S8M8C3`7p)jb;}t9KERIi*5a_Yol)fAuJhJnBFD}d6hpgLM2oy8MX<-g{UX@s;MDo3;sg$y5h!N-j -+0h#r+(3i7UTW0bKGLe?>Z?I$NiFXeHB;I(R~b#)9I0fr64u=2&Qf*4i^!%Bc*g)pod#IQmbRtUoiV%P-2u!0!YHDH(s>V; -tnSHm!UWOEqS7{e5v$M7a&*nw#CYFJ00hP430+8e_0dDUl!yD_;!zs^L(kL=9?~;J0d~1o5_#8 -m2c_Q^Od;hB54bF(igH#IR}D})u_=MQVGS{?A%-QUgsF~U1u?8 -4hBd^nMi^EQ!`fF38($17h+*grhcP80SfbPZYFK7Un1C3jV|%G#Y)Tjrh7H89rQP8o>`ZaVRZ_zk!~U -j(38aRN0K?dnFaiuqykW@_9mcR(z8c0DhNeWL-Y~{6^oB8pp&G^*#%frHP{aP-u!b1Mri92FE-@vDiP -h9F9b^H+bdUy32@!^gx7C*D+Js>TYf?2VF(ojDu^Pr0*4}CuW7vO*PJ&@L`P(;gkiEOd@$VRx?u-k>%K(iuZ9)Gu=2*R5@1+C46860zR#BE1eFpP -!&nVNQv#}C0fw6`SgQlW1V1kf%hYh>1#6a7hl{8>Y2V1t_hgCgc+5Ch$s1M>!vb&E8^d^sPJm&oh84n -an7UzTN?;7DMlq};7$(ZC^%7kPh84yzh37G>*&BAC6cWS9iecD9fMK(I3}ZE%2r!%=3~Pwt1Y$UW7)} -s|6Nq74H=KB4SW+ngO^F0yI6)Xr1Q<@dF^nrE>R6(S2g4+(mt8l!$pvdJegb0H2r#T6hOsvsgBms~ri -KOW8(0lXFboUU0;%CZ-Y~?lq*9`~H!N793#En|FIWp%DN$ldP}rw#SXL>4F>KhBa6luil#pOpHL8X+# -IU4pI0iM03)UJ{!+43#_^RPRdrDvoV>Jx-l)&DwcinJxHLMVZ4a9J$CA!G#hLbwfa2=*ZEozue2?r_7 -S4!X|x=?D^e~AuPN=PsqL&2Ir4M$KZA!y%F0t`bntgB-Wx2>xR8e30$xy=%R~24MPlj*A1_M8m_^VAj(DCz7cSVPNaqt?xJ&U{a7#@N)2NSV^hMvZ -a5T%{Y{D0@`jtlF!qKahQrhiLk#0eiHP?bE-@u)QNyw&x`6E)B};T7HEi$l!^N9?ecCs0rG&(kh@pKW -g1TYLb=DN%yq5;ZLHhGlibC2E -+L=$sq3ylxm5tofS~l8r9?F)Z*z*=VH%_J(~;i3Zg$#_-zgH%#xYriNu04ycApFf7~X -lDp`f+wMdS=jVH0s$qjLjO&JR!J0q~2f{FJX2uv6s9}vT>|H6L5r%bj3^(fyua_FGV~MV$eM6*%d4e`{DkVebvrFogqcu$Djy3pQAT)-W!Vz-7Y;qOk7%k4aBg-8kVSGjA5*XHN>#LHH^!K4 -aBg-8cwQXSVIhBPXb%RaGy&JF{~kmalM+Uj$!Ocgi*uT8ir-VzG_%Q3}a6MmknbzEZJ_j7ByUA4a+=< -5;a`X%S>UP-$0V7VRDQYJP9t8aPN<`+2_(g3^!Y^CQ-u}!=cnLHYL2(FfJPws9}s@fhmElVQfmk?hS7 ->j7^E^7#65uY)XVu!vQh8qSkONYM3bO+zF<*kZa%A2V29qdBdN=SPV;=H$qv%lB(fQ*08rHabgYMch+ -z;6qb1s(OAPJ3v}#BxEFmS)^I2cubMR+Z7Z{+PC_7taVxWi7>3rch8PxDqZ*RaAIthlu6iQ%I0%EwjDIu|ju^N`F&_%9>qrq?;rUbXp -xtFZa8jhF3xLhsN0v)cC@E5}pPr|=ySU}-Gm+Is!@TF -ab9Jtd7>=Q8*uQzB**Xbb9mCj@@UD~awuU7wbiTV>hVdiFYPh;JEWxl6VA#KTV+qzU3E2>Z5~AjfI(q2X8FsF?kQBz{YR -ytutr&$PC>mCf!rtX-xOYQRuI5c)=t*cuVeL!dAiW#7h0ecD;(lnME9u>6UJSD}>|9(SdpA}M!_D??_ -={l)h6Po_CmTxCQ8m1D3`-X1BF8X=8?=VQiD7nzoy#ch4mvHM@TzstVG2iEG~8$h9kwJOg|Ran=>lDK -XV`y%4vS&z3`b7kXvAyiC36-n%YoI0jNl@75hd1zk5{9=LcCL++1-k -rvBl$JZXffPq?}m4s1okAtU|2y6Lr(&#VT~}X17R31&_!M+A;WNq8jht-f;;G(t00yi1NEn{zb6sH0v -+}wbRY_QSF2$P3p@!-;g#|v1Qc$vXjnpFytx{t@Dj=-mQXZ|DO^X&`69j5ToTQ~eEj9WLBpoMFpaPwsnczv!<6n5rzeH;n@2D*5)a0C?gu9A>+Zg^ApzS6>pfWp`q -mT2KsbtIxGR^u`WXBwryek8%Lf3cdROafzAd5dAZK8G>v-$2)Fv6?{`ZnRiU(#I?)Ruf=Yt*IC`5W@; -$SW+gjs+;H{+`6i`kc5-!dnevPeMTqD}-SMG3>p?rJ!aQV;J@^!!n5o_Eu -AItKnKa2?{T}TCL3sbk5YQJBB4{SVfEBPz!Vr!`d6es=6AMU>KH3U<|A37{+Q?Aq;B)h84oFLKuc+5( -;7%?=-Bu)vyG^tEz_UD3hS57lwI-&Y6Ue7}kwq*x!^mQNykBMrKK!1l&#nH!^#xVTm{Fi(yH%8pLpQH -H@p(lmNpLH5|D&91Vu+sFSecefW{#j-*f`6oyyLlnAw-1onmlm8*I0H0-@ZC)ivq&=Ots&DEe9j(1m| -C17~1mgo$7!_L5#Z#8TnhH(p>ff&Y1bhv8RKnxp*VMz;}L=8)p=nTZLq=imH4C5s_12Jp_7&g^0j7|!=zZf)G!^pxu0aF!n8^)bRFK-#~s2Gb2v -ZWsLkOBTnQzR!wPXYkSn1ehZW?o^5n1*$YJP8D8ylfIIJLt@xDI(u7rwP4%gyJPtW-XdNx{$tvsC2j%4)YFcZuZUl5_pTXP`-o?q=&ID5lRnZUqZrRfiHnuIIu4v*kVmmv?kEQ-oAu -}9M*`#VT#rSdbn9%q7Dv|IBk%_GCf?vVeAjfe2Eecb3?P6W-R`2sC^}x<*;g$!)o~)#=e9%hgE|d#(G -%b4-0&W>Ks;U!r>+8VG1|b!%?zMM|fUnrQ>xvH=mQALCViJlCm|)&Juz2u!0n4jQ)9ESD7*dO+9u&ki)osxP-$5#SlNiA=_J`WSy>r!=n1(5)Mm{_e9*)MBAjU7fovx -0q=7j6k+|MlMu%w?Zlpe+$meddXw>3-p>2SS-q;5^ZVgG(Q<;&qf^%CB`gy6T31bSFO4hz=lG~%$pmk -6bYF^5HZm@pSY56d_#)59h85=7ie92V)}P41&qw5=y{f13B#POJF_RtS=$pu!bD=t -{;~4)8Va$1=~wtUqVnn4EgSN~4{=}&Ylz{+@yf==gXyK54 -#3sj?cs}~-QjlY`pv;)aD6(Q?6;;j2h-N}XuGvLp0!H1a-RskX!E`jF|L#@-Te)f+ -v`E%pl=_EZ&accIw&(XhH7Y2KK_JpnsuI&sT+JE@W#ul@rcx>!7(QY4)%;#TLDjzaEAJ%!;%!f@LPUgc&9^RY}Z|33le7K#5JM-aA9`4SEyLq@bAM -WMhz8j{yRnH|!xX=qZ5=!+#k_1ydmnAVnFC$wIy~{>ehVH2#U3Z;OI% -vY()!o9P1-bkls0f^LpKM?p8ipQoUk-7iqkN$r7C59Ifbo7Y2hL%B&kOhGrJPg2m$=ocyIX7mUJ-Hc9 -C(9P&k3c49RMnN|tx|lcJ$)YQ{?Pf%maof#^uHm+u5mUp>h_2qYn-N{QZ8syjZrg4~bkVlmjOdDOyBV -FKpqtSz5wYBiewl)9MrSGLX7no*bTj%@3c4Bn8U<}e3qO?Fv1+C -Zn7bkk(o3l9TbD?dB)j{@B^mCg42gFrXUq@Bz^26WRzItvd0U8@>9^N#@CG>^^x`~yJO&dY-Q@1}Wdc -IL%@H_c;np)#bG=CRqE7yi98k52v>zfLim$L4X29tC<=lO(vh}mpE -vX$*-|n98A;6kGR7zD;*b}KeUw;`4I~}VRdT$*JBRa*~zDIkArDCo&3*#mxF0Kd5SD9tcu9TVxMDaHu -<&80w*&z@&g`l(BAnAy^PxGhIalj^em#SXlUo3^D+l*Itzu{I+F_v4#*D57dH6?`vlbGv+A|^1^d|5 --Q*WK{DQ?^H{ln${DOUk=^B2a$1m9V>}r0Y&o9`_yG7;$e!*tmDKcj@n>`S9ip=?r%pR*cMdo~0W`0V -i$eeG@%(&#UCEu$i}ctOh(E*<(6=+a7&`!!~dF(mZ;D!!~Qu6Gz|Vu+5piijTgxR+hV6#g-XZITIn;7>T5B}vD -Am&5iOhobLsG|P~N^`{)pGNfVsFAm$8OemS3aoElzsmY`FIc#T=WWdn}9JWc)SNYL@Q@EeW0~*x-;jm -4TzT=Pnm%}zm`bMAq0f+5ODhmH0hwT?DEx&%5qiK@;{H$kx#L+ZI`u>>xF-Pqz=4Ubc6OLvX(t-b!!} -c4NPJ-+=bF6>J3;Qz;rwR7+L7M$JN7MZJ^xnVVs7;T=*6c4iY$uV_^6alTZ1bZx|20SL94b2JuX5PVp -{Bh5hQn5?=4W^BHICXTB#AcrTMB0@YdW9V+23*4W=PS$=ct`QdK8%b1BY#jq@ia2$l)|a!Wmna#)_jp -MgNJTHbc@vvw!ArmLVnnFC4aWNV;_PuN=-&q|y62g)^N*XZI>c(+oEk4N7B`5;2IKNMof^kJ5gfqiKF -U8vTFcXqq2=)z7}c(KJ8$wqs+~SgAzvX7){vrrD8-n0vVEnK*5%Lh8{GzRgkl1i1JsN5*O+(oD -1OaMUt}a{DevZEiFj-{YtiwshR@b2OVfO8EyIw#m`3{|86y#kuG?8>@r*bP@z-V?_{=i>+m26%Yxa** -hGym!_f-{vn6c>`2_re#B9m9UbDw9JK&dG{isQuzf_NL%hq;G`l`!_Z~;n?0S^lPdS=>5iObph9L;oX9{qr$cJA`=;gQJ1O+FC3NSWE?>C;nwgp|pzY#_h+!o??_I^R0mx;)ywu`_() -?sRAk>&wIGh4J=q=OOE5?+>S2yMx=qv!~L(KW+d1bpO=x>Zt6$ox$#ngTamTKvxbXw?_LnKh{y~DQ!* -1TjR<0aI$rKuy^+Vv(bIdb?derhqH9t^N&CO^zoQ|oP$p9?v1VwcDDA$`=jaT&TwmD*BZ{MNBdosel> -rLBiF&na -_T1aP9i`@WmT9M=#ylxxG8yo9s^y?%e&-?8~#S%)UDN+U)DIS7&d`-kiNPdw2H!?1Ou+-FyAs+xOo2@ -U`@!e0XEy{{T=+0|XQR000O8`*~JVcR(3*&&lF&W~DH+N -;o6ik?6o)Qu~RIO^Mg>JwVN3{p`#(c4462uhL9@D7bXDamEB}36-u?o27DlUQ$ALUZW=m8DGK*(6`N# --gxftPh;ahJ-KWNW!bs=0!)aRK#OO-B=Z)vM6*eHfB%2tyV!`Nk%3B!&o-#2{ -h8zoxCHc2ff6uStGNgw4ST-Euj>n17g@YfhLQwfN`3w!UAd<0P9&+lO^R^Yh-NH*bN%#WE; -;0BYgLLz=4CVaXk(JGi=ycCvVauKFU{@x!$kA)Ztd}jfZNIzJ;_}oKqk9#Z#8Mq-VNfyrYRGQ?7oj3x -fK=V=h?}2e#CvGY~N}yiVQ?|rn1!IM>tH^&W_mY4Y==2(VESF4?)x9amBb@*bBC~TW4&IOaA+)^OPBkDl&P7fXSwSm4+aUo3r&hA}GX25OCtqk+fH(D;FfR=eBlj- -JSSvpec{Y#U5#2rU@2hNG@A?zM(67!L=Q-Kk=68B0@4$=sLZEX7kaxe%C1+$1jd!#MOOEN_So`V*MDZ -w$-qG2B%XaIr&{>$7U;i(nSTi;z5__rRwR2vvc1$+35^J$I^$R`U8*a94QM3S8@`SGJBwYsj?F7-xD2N>HZIrM=}|KY@!Pf63J5;sjXdABU3c){qgdbGz36xvJ;B4MJ)f)zG ->qChJ=nAr-uP1DR_=O4#tjhz1Y5&ncLO&dR$cvAx?McZb|12)mD(GL{rBotAHw`>CibrQIt$2soAC0C -eypD7is+qmAy*TaSD=c~p~B8yf%tdYv#Z{nT#xNhP4^`7;Vo$xn>_1JuZwh7(bS&gwq4}D9*y(Jwb$g -tU$!4yPPdYx~X? -2a~1fRb$e~q#_$^s!5kwPO?NS5QD*626&5fAx(^ut=BT|s`*%Z^?MlxoT^xkP?MF4ro@zmb(pvvK5GB -CZoP(WAYZdh#u5+6Oi3g})_=?o@We+R)Uu*!2SY&!d_}KP@Vy79DpWE=YVV*&;W!IA -PB~=$G?CLMT;Wt$E}E!7q*Bcg7r8PnR2?K$4jo$^FF9ECMZmU{aLoJH@$k@1)6hN*iNmKt+nI;4VB5U -|jA```56jckfu`hq&Q51H|GPRNceCpcP%?dDu$;j&G!1GV&7J8|ZkRqBY2H0XKY -PD7)G6(mV*XNiF7L10XUI@u*p0TO!#DZbhr+{Wbg0?b?v-3Y`yWtC0|XQR000 -O8`*~JVHgyz`sJcjB?J;w0>~JhqEloUdkm{HyJ)@6mHX@a)_@Jzq!+1HqTt<`H}l^72v=9t5 -8d@2hGQBHd;RgOPj{n9?{B(bdsVw4cf~Z(Vkx8_Q%{9VBB3YsL&UL9b17a~C%6e#zE0Rs$c6)>GzBni -)eZ3SHmpfSWGbLxKW38z{)Tb1UZ+w7z7a|$c-0%Fvljr45nJCXrqKQO{5bXRD;g3SZH5wt>lqrrbI>NJLufdR;3j9k~D`R*(C+}4EZwrO= -sY6%LaAqknmu23<203e8dM^Dm%DNQ&^<0cSb -OJ9cW<7Dnqcl=CWx{lKwyU#bUMSXCKs_&4*dJ_u(gO&EB*&=ZOJoTO4BRsly!_m-vw%HGc?zlgj(O}| -Gm!@6E9roslMZ~MB_X@ -Bbc-tG|1XE?d^P=9>?lN;%(R*N(yskL7?8?6>#kLuaM>)~+5W||%D4OymW`n`d@5pp>*=r&~O2P+{td -#lxILF^~kZE0-8-PZ64SpDP;vyZu@4<9~cf2&<`lk7vt5qX}#w(-1r!g16@%gdz~i5QK4J64kSsP;)x -Y@&KKZj-Egqm`o_;~af%6lkz-<uZhGeF4 -lCHuab91+uOYbGUE01@!m8^$5T&AYu0@h8plO%X)6rIMwPPxSbcUKP!Udf6VSK2ly&p8Yc)S%qht2$q -=^sjx%vigXYS|YPm#JLSCfH`o~`q!q0yESJ?h+NfZ7P*xq(E?_uYQa9gZJ9pATmJ4Tei)JeDe5t~`7j@ay(FTUg0$hm!Szl932m1`P8*cR9@-Cyv -{jyPo^#^zkyfKi@G0*|%G~Sb%I&EsrP{i|;Ip^UZnWc!Bj9i`u_XO9KQH000080Q-4XQ?1hxsB!}U0N -e)v044wc0B~t=FJE?LZe(wAFJow7a%5$6FKuFDb7yjIb#QQUZ(?O~E^v93Rb7wTI23*7S6pdcK%x}rR -%&|vCHJ0teD687xx2glP=7EQPiZm^2Ge*z{mCr& -LBGM?)$VA%;WRU1EfkX!=#-PN^q_wUDH$!4c;Ut(lWdr=+)>IMPnuGuFdWP#1v|?k0*vR$5UFBJI3p!nDJbBiKRA<>Oq6L?REzgH{mVnNlB -5%T8g;k=7p#GNdm2$5vv08)otT%M2H@kd8SavwGmr-N9fRz(-f9&qty-(Z%X`_)IP7@=G`f@1B6EFEZ -0K0CfZ4kszGNdR)(33<7ob(;~M&d&wG}NHqd2&x&An(`qdjb*awmQev4vb9*Ky~JP -|JVb$FKnRck|aDd$I9ChoF+|4s2%MFj^PD_81xl;l2)7w%dnZ!1i0>qfgFy%@#4uXe46nsNPuRorQ=TE--;dnkB#4!zL5%nnSMe}j6nDinF7g0Ei2Q4_x`B7 -po<}H!mB+fv&)ld>AhY4x-qd&o3iw0#%8}`CsbHZ`dQexOPdHKIQlv=HK^P=P==sk&yl*X2OH3EAFd- -lp*u$=te@69HC>;S~nvxEgP%7-EY^-yYUn$*|UdFVqAwe4=F(|*_a(C!c|VnlxT&|v!Y4{xNqS}ij;N -v*?$vo^ZObE8*~02cbw7a$XJH}aWqmpPOaF8fr4F6P5e6^Q*QY!woJAIB94FOr`ZD-IUH_pkz5ueu*P --70F-_pwW^XLH5%BdUOs{PSO*FGdUzs(@Bv{jrMaBXYTzd%aq0{&)b@YKdePcY8mG&$O#w%ZZy0w-z> -RM$4tZ{kdG$EtgpnhghwbnUFZjU#42|1JXaXz};HEAa9bdYqZ{I&I!)m8z&mzTl*m_eW8Ee68<^aS#L -BenUjl`nKSP$Q|E@=C_CD8nL0D)GIjFg2H(+%`f1vKt8*Lt8#$!^15ir?1QY-O00;p4c~(c!Jc4cm4Z*nhVXkl_>WppoXVq$t8}_k(L@Lt4F(?t0=mkpp{v=J(w7?|8uc8HCaA?fyZ?m4y@f+agN19S%JvwZUKk7iP-z256tNGOvr-Vz0M -9?mkq_fAsh0L2lsp`YjWg!QQ%DGksrJdKl}kHjGbD+U+lthuLI|nX$(pjvB+l^i|EGplKMl_xXvW7f` -w?MS-?39{aN$?1^dVr{+!vC#{xceLYX}@M8qs_`Y%5K{;fZCW($!t_oh%CQR;{Xw_)0bU(PPsfQL|0_ -Jw;qfZANlC$3`ydEnUG3pukwb|=!pn^j94i-L@jg0N#BvX5K$BjXO>;@I~*2qA?v>PC9BB*z-_1NPpE -MET)t-U;FdDD;B4a5jRJStpxdRnsYRyg1t%XnXhrGK7jgKsRC&&gLPX&D|R00Q<7<9a -PFdQD7jemTEeW9fRh4UwKa>j2Qr3Q1(p{rpx}_fX@7Y1Com2UPX;IBAENZfC*!mJXvB`shwOk|91O -=NN0)1pgUzZ);)*QY;y;C6tmzGxm -#>?4qe~7j_V7QRfB$H&Ww3}RAUc;1APa2U*YURF01ofgHCVPYpiX+PL}tV3#VWN^kZiRSs%N;AJo{PA -4X!=(*xk@4a3kiqp2H=U5+3{^~3sSpGyuR)k9*U+B#mOJMKwho_|Z3^~GnY$r!H@XPdv$)_ -JYJNlT8(lRn_zg9`Dfmq_zbW`l$#2xMAAz?7ztvUag5Of}+k)R#^V@>oR`WZ8-%;~Bg5Oc|yMo`P__F -?8!S70btCsx;+!TDXtHuT2RP%d+-&6B@g5OK|&CEyOmf%~G-=uKCw`NLwBKQ+Ee_W9bpv6ag -hHg-a3&ICTW6RFa_AI++jaUo3`9^7Tb6FOnqm -XP-S%`D(OZyc*BasXD8{US$Y{8$`}fer>EYLTwP2Hj6N41&e6tjNF2pVC>+O1vS>ROE0N;?3YN&9X2x7LNsswR?|>p9&*wv*z=NSk$Zo!}Tsu9sH -Dxqp*VcUG(UV?6mdn(Vfu5=NdT|$6ZRlE~ySf01W}%U~gJ$d$^wqVZHV8{`En^TmCHz5pb(Zr7R8`jc -18E9Y2I1Q%Qk5=Rb!!m&djapVvvg~1Mi#F0awaO@C -B961ENv_qhuraZqxAi`l!=n&MwFf|`L1PaFvA&ZKjSl%MQh_LnZlz6Cl+J`4wJWqL^ry{$G5=%_PvNh -M;oUWRDtTPvT#0EZGk7c1#sbQDt; -H_od|d##a?W?-|C>6S4v@jz12$Q0_a#>o8C_}tc_2We%#95$Lpqf(LEvJ(^k5jV@!SdP6+Ggd0~&GPT -%WvFnvYJP|0lk-22mr+mmQ1h{up>XVFNE~??3ddfC#F3YwaO`DB9C;ZE$6kiSk(Z%x>}5zCc^L}FUM7 -ncyi778JjLw8-JgTX>-5d=JnwBj4!MgT(qg{6??EPKi{SlJS)nyEN);|=eS=tivAh^-0h#OrooH@RGfHN>KlmROr~IFFaHIH6dV!8~sBFceE)B3~?hu>mNs-=fbmPpTtFI@cN@WE9|5E0U!r#`NsLWsSNy*AK3%pq{f0Vh#=8wX$`6F>;{;K*<@Op4vJFZ -;va{mh5jvPch)rz+0^Huq}sk0Sz`Y=3>}jN42tYKhoK9GpmgIIg;!fYB^2?te}3IC -oAMcIO&B%MtS)qN?&B9!n+h(slAs+qlR|auT;bzhy9H9QbB&Ky?U&hdi)rO(q4*>wO6J#vpCXTijTEd -uVt^}y!KLjti2?Tw3ouM_L4Z#UJA$BOX5g-DI9Ari6iZ$aIC!~zE`spia1t55=SaX;aCMp9H}6MV-+N -Eq=FQVRglDAX&z%s%wx<3UXl?3gfKUiu`PwIn{HHhz{|OgTT9zNMcYb?TfB3J-joeOWMd<$+^9^+LKN -l^uFJ5*DM!(WOgAw%SC>3)5<1vez9;kztlHGIW*2q-HM1CX`3JgC)UIG9uBC3$$%%}4f3UnfOWvfDHC -lg@emOqYjLu;nS%pDj6$Wos;ms<%S%o*Nu(S$;esR%5cC7AZzP-7FtT4B8n@2ns$XkUHS#Lnpt|5C;& -CI66aaFFNbTe#v2$O6IZsJjGvWuG-taTHOjGGv2bQ1%Wn<(QY2q!U6I0@QE6za0>mO3{n>X-%P0G -_}DD}`l;N@r>x)Z^-~o7J1v+<1I3vzKk^sUlFX-k{S>w2GC!ZSfFplF;n-i0c+OwE<{Xgp%_k;$0}A# -RE2Ou=k-sqD(lUtOyw5NiHKUpSwrWOOT+IT=+6^&yJT5^0NY}uU!pf>)Zz6tr32tQ6Fsqbp?1GxxydC -Pbl2p@C_o|5WOpQH!5s7N@#$Fb6>7915UfE5_6Dh6JUiY=8sWl&Y#XhEmTDsQS^A>8ET5Icm)1@~h58 -2kW_MW#&+tk`mKhp2iS{+^M?0IW-Os%tZAL`MYl85Z-T6fP|rE6;47jX;QwVbJI=AO5lX=>)S{Ye_V; --=&wd%D)!^H%AZTJOdBQ~8#&bj{lHma|OFdUSu<{qPfAo9uZDO-yaFbKiQA_oG#9U9XpTBR~0v)o#jpRnkZuup(_z%G|B`S{2T)4`1QY-O00;p -4c~(>KpmKBa2LJ&07XSbz0001RX>c!Jc4cm4Z*nhVXkl_>WppodVq<7wa&u*LaB^>AWpXZXd97G&Z{k -Q2{?4x`DOy49SOb~Ko+fiy9U&$`MXrG4B^?=P#b*)htA*!RI3ap))eAjxu~2+PKNB?6q$NF -W^il>f#dx{FKUYX|FSqu_9!ebqmkK^puG$#y&J|-6%DH}n=3i+eBl6sU6!UqQybn)las$6175G=u=kt -v@iUJ?@8nZN0t1xr&-C#<67Qz&|V*yim{xO;f?t!__Jr;zF+>qAhEL`qf*)@ucQ$?sNAwG8hS7IFZ=4 -}&xoX~~U%3ez))?q8vTy6A6xYvg997=CUAY3Zrv`SsDv(zUZ#A25S4t{|^82X0vqZy+pWb;VWApx}Xd -v9_-oKC1cc%t9iqfvV>dHM=kgaWq+a3YDX)`CMYtWs{EH&7e`9_&%)9vtnPUcWbaGTL{0lYu=RQ+GI` -Ha)gSlU`@qZ;$A4I(i(AZ41F=EOW7OW|@<>E}lc=O2U&U5|@iGG5rbQ4WXd$Y3aT)gxO=LDxX|r$a?d -k7z!CIVtc`|bLdy%a|)!c)0^VR9i(R=x{$JqZyifyfPB1VS(ddza54x+|nqb-IzEHlz=2@HQ -`6t>(|IPt6w5bc~btK4cGWPXwQ{N@cE4G@cX!m=f1ILT3sra97ON(OR&S#!B&Lt>g?IO36An95ekGAE -tyXh*se~f$Jd~J4nfeRGT^(Vje8U8Ygvx_eaD6kM;Qr6MQM9U#V2axEJhZqgndE11ee2^`2U_YB^E0U -ZE2{&(+i($A~KgI+HpgV -b4M_ZxP>RPx?;U_XG~YWpCg2xaKn%aF-kI}miq>rHv-1(hsVC{jC=pG5$Rt(CxN$G0oZ^)sZIuy0r+c|s|@B7A%K{dXni0*GXzg$zwW&;_&i&l=8U^8c7A>(rXY#wuF^T3o??=YDs<}QW_q@S5 -nLe~rj=%VO*i(~F@CT{)*G6nwBo2}FLk^9j1D1t)DUF85wF&(L(NM-Gd-EuG#^b!8JmhznMD4oIi|54 -#glPPi8G)-& -%}GIem&9CvQTg5;7uYdtZgxR{O*EvWxef6Pjscdt2z*+2eZw&)si1^IiLZEq}S?G3XiRJls~UX;$imc -TEY`%xS$iX<5_3t=;Vn?A!C4urvDh?b}td5WHb289zPT4Es)}J#mJkTYH459~z;J#bd5ZudaxgfuxJe -X5DN72h!qC@%2QkSv7DT0RT1-@CooIO`F16L7m}XG8*>F#G!$$Ouk_e@q{!Zc{);Ww>uO6KwI(dD+36 -}hBjx)AY7S1W9yQcmCCFgCMTdojFDhma4e!&_HcmpQ;L23f-B18?PK3M!rB}%Y#aN8NWCO%e=l)PYI; -7(=2HOHzhcrkP#~$tE{1mbMvLn6!dyQ#1G`;f8tj>K-}od>GJ~$@0i^*fvE3@AGyB+5>vee5Xu -xsHXl8kG)wp^3O>)1HsnfvOitEPZkS%oct&iR7Cbuo=D{uUOM7{{n6n$k^8;qijpZL{#nT(Xx0)-z_ZY2 -v(UvjOxM@BEz!EQCw6Dj8{AzTW>6t)zozjn-y4cP+$)h(O*0kkPHX37X|~6eK6qr=IFfKeE*9wIKAT0 -0TJ@(eug*_l-N01*M++Bl90W|Ax{l={P~VeVv}YI5)cbkocZX;sPH$4+!x)+KL|SS`_No8L6EkM;E?^ -7S?6p)q(GS&dDV6pjjsAh5d%2XthrDV%@1aw@y4C7bj{oM9565FLoH!5muGETS%yzOnNacgkQzh$dt5 -AgbdTJg_P2j54T2>P~MG0nLd0aev%6`90<)es}w0WrURkCRXcA5(Z -*OO+b?-gCcveGiwHhZi-SXSYiox8-zwhnk&U>xGWUUS3?%V9Ked#6%=4rk0F22~Dr^mxyCV!$d5&(l` -mHOlfW?;)aJtDheT5yI3Q*i7*0Uk9@&nIp>Pxn9Koa$alHXA)9k-&1PvLLS}?aG(Oc_s_X;6)R!vX4; -5#IM>JYxi2J6PvRLyKOic|{T32*?b5jSzLGcnaU)S@TV*Mu~^!3`5AAPp&~&@X)H#Hsg=#{SaD8Tj?EIDKhR316taXAkA%+cBU6T0 -68$L^^k*^>xvh?N-+P&d`8r^q#Yj|FxJ9vM9EJ}e>6gWo`vw184jHnb#%nFVJ;MMV3ZD=%_ZpR(GTkoITLD -%tpdg^)9pnk&}xUFHQ;Zc9+^?SZkLvcA@r`RU*P?A4WJO<=UB9mwmCSsk3{*Ll$I2cDXWebinha6oM5 -yK6cug_1vp2P&W0gqAS8$mG0^gV#!C068KPm5@SF=#Zsy&HKjII -^tA!qo7;N*=DPmvrcN~Uv2$#Z(|tVg@wvRX7%MbQt$14|N~ZIIHqZ-rVkE@68YDw|Dhj^{X -x06O`biWcSnTU%|8II4_0CZNzWeCtF~7%q6rft-GM@G(G37P`jip#WeYC25y7)uv^Z+<(Dbgwtce;u$ -=>yYQ1l}`FanRy`k@KtdiaR{(Wcnt7>c&atH3E=iUe2nQ*U-R_on!JGjGVYL+fVNp(9Nc;Q4T7X#N1z -Te&igLcE+tGP+l2cGQ1SLjDD77elR?@|kIiN$gPmJkQK;zwet9BqpeTKmX)VzC{ -0B@Z?ZV!-ttdq1;jfwwr-aLm@}+Scj%zKDzRGA^b1&=_vUPDxBzz(PLcTWI+b<_}FQG%1VYd|)qx#s| -q{Bb;yjkEt1`^mt-Q+cjLuAj?6qp(_U~d%7!!g5R%7&wF${H1=TNcs{iq4};ycj5e8I4BNa)WqH*?)6 -(*fCAbnM;*4ojXs;@uWw_9bY(g8ZK_bT(_8Aa#BN5qAjPimF@EYdt%n}Hhf^P0X6|?|6XAb@%l45XY- -k^Tf%+`{xylJhO^yTD++|0^iQc8 -C)}&u$FF)4z@f+bMAc&x;6(MKFaD|K4@|8yRCmYVxj~nR#EHts*v9z@-z7h1+u!F;Wr%eX@6aWAK2mt$eR#VoIXEGoK006!Y001EX003}la4 -%nWWo~3|axY_OVRB?;bT4yiX>)LLZ(?O~E^v9RR)25WND%#>PcdqAD(DCoQk`^d)YB0l2|a!Z8&y#iS -;k&quVxqRuG7*_e{XGL8!)+4I;&E{?97{aZ)V57xTt>Uwtqhu({Rx3kDWf<4kz8e>5T71?SkAjqlw}x -F8qkPGGxRxZR8IgMSe@F6$P-hYJ1m#;D*Dq$DUavw@i^-lBvLe|Ckva_*(|kW)lk@_=ZcN@l$Q3N`3& -Crc$N$Kr!F2kQTcH@idjMEY^01RBJ=^ZiCwI-~R!KO7|9ZqKIbJSJRAXrk+`tGU-ZT6ko(fs=97`fQ4 -w}tFJWW(Ms_RT@orZWF$>@W-Ud=AJRm8p?tNoDlHXbq~+3lLt!_xlQ9LpAjLwiQ+Nnr$QR*nQXzgLL% -!TuQ!GNzu~i$UHhQn&{di78rco)~Gr<(tOyfnw0|XCwz=USxYuZ|yUKdK;*+QJG5W16qPt?63#&K?QK -QpCaq3c$ApDjadl2lapjjbQu+}gkfir!I#Zm4}^t5Sl3X-Hfouxn_KKL7}#-!MkBU=(Y%jH4zH;7gwe -?!#p6QfK@~pF6WzXY4-Sz!ys66#Yw)_+}e%0E3nCh1sES0F3&x?gJt^w}aupeYDlz4cu|xap-O`qYh0 -wGk4IP4?8oO&S%q!({B+ujO``1VpfTqOS}Z+MncJu8J|ZwrT&QaY7`iSwDzAF+8i*Bi6S3YWV`#R9!6 -5E621s21ic}?Bq2?czA28`!StPU7br!4n;80_)ui9saPr012rY0Be#e)9zKf((O}drZSl3Ypu~*ma_S -I$k-Bp`t?!a>Hq5in{$|Z79t1Xq>P;0XY*1porY?WMqKl*1c(I}1#zug(u#i_&0G&#*;uwA%VX@gPQ_ -`#YBwh!wjCyqTCr6>Ckz~pmhXF3aTJ`J2+$=<;>+G%W}H0+;kAk6o&v)uyhyE*a$3#~V0KhGI?qe-tn -%siNgac46J%Vu9EL{2MBe${HVAo8_NFjCU>YAvvVuz)abOY%I9J?43J&7x&yw~vmo(dq?KlM;tk?%ya -}aggcifyl=SCF=*YjzY`YdcGCs2b%futiGqI6IqW#kJLVnQdrOi#$1Wi@Jbo>(JvNqnBy-@DZtDw&zM -cGC;SaNPOO-Tl{kZ24m<}Q`C=9Ot8>6_KKWZ;puRVS0!zli#J!0{Z0XJ4~9lA(E>VL<-p=Ue(|Jq6z=94Y9^6f++>gjyY$!*HVzKnN`PqIC -Q4^OWdkxKTbyz)9K*Is2Y%@^QUq}dA!Rg*2Q9sNq}GE-}K_p@a4rvQ3-spi*6P%80~+DTpQpKn9*6on -kF>r%I`4yNa_{ZzPO4ioQu_z!MUukZG|?qGcX;dp)3tMy+=<1r!Q)!M^1m0!1CzTq*gG&Z>>QY?AMM8 -M$rXIhE#FqygCx$C(Pvp#+dd&m2?p!o9juYB9+^jpH3?MvaPilev5o}cw7Zu&9jBO-Zp*8hX(>X|qTk -yXC&zt_vPG!L*FwSNInO9KQH000080Q-4XQ^$+OgLe%80M{@804M+e0B~t=FJE?LZe(wAFJow7a%5$6 -FLiEdc4cyNVQge&bY)|7Z*nehdCgjFa~jDJ{;pp!vFZYlDUiX+w=6yzB7QNOH#Mxs}nojJv06Cbob029-1F^)_&jZvtIYS-5<2sS?}unN4ACb@ZgXQ7o1%MVlF -(}W#|5sGa=cf|BG;4o85Zio(B>m&cY5P580H5+zI`FIUuzT5(!;TgaTw2{GPl1iU-V}vCtqI^nD&m$6 -j%~S}r3`ICdy}PvU2=@&ok)g4wkn==V;*?T}B|WUVA_PGtuqYqiEyi;z{%k65eGXhMXM++wG$%WjBPv -KuaWaL=cDC+EHw24WIne^q&Of;Ws^+QA*W7VbO{OIBxCVaP(CU9ZE1?g}vt<4>0|8G;xIk=ALl4CpohvThTkY$W+;(g$&|_u#OM$O8svc7Z|6x$^3Z3T2?ceEtXirURl)+Y5g -$wiWrZu=rwa;t{~yGj4H@>I%UFBVizzD{(wMi^a1Y2CY(i*&my-537};kyTcDxw?lT?|HM9?-rSt_ho -9bp7N)=`46G%I<;oQhj9mq`7p}2z2)JzDoPPkv=~=he9e$$Kce=xVdoW;~s~dL8u1{}<-SgYt=?%NSy -}7;`v@IBy^F+m@Op}sR70)1Y=|htU5?i=ZZ~YU@E3rU#$`eBuh>kjzLrVY>*$y1K4Mv -kQm769PXmX5FVa&Q1Qre%<9M<%~qe~wA#$xj&eU>0CCRt(Vdz>ij|z@I^biU7m-|J#Dz)KXu>O<;w)|H#1j?f02TV3fVt9@gqp%maqCGD4EK55k$Yzn}P;@D20B=|h28D-&;IV; -caijyxXFT9um?l-d58#Dfh|lJDX=9G5P?B@O0XN|uTtfd1@I(^B@Jvkc8m3EKbEOzdPBcx;7JSqr|N0JcNOaM#@)zXZ^IaO -dh_p3yPY@#W2TCo5;J@u|S8hJKieRNcscVWxXgYYy680UsIcnBm&&7@z@Sk49s?9xM3c(^6+6UfEjNm -!Q{VPZRXaiJr!O(pqT=}lTp*PZz1K!mP)$K%9n#^V~QaD|w@=dPItfP=_X81d*jYbsE*g&;wlG~wX`N -1TQ{Rb}_ybsO+Gx>Reyt=>4ksr))>tLOa=dF;4ZLz=SB{Ck86B)+yK;wznTE@jG|?2@ -Fsl(PRLJeo>EOKE-Qky$N+mPRY-`w^6fj^LRq9s>Ar(l)uwJ5nHC-qOA*%XA3F0kETi=z(hJIdu -q+>v)jmbX)jD@P?slKPi$fkI@y$fx~)yZj#QNakP0>`%|>jN+F3kJ9{jOk&AfljQC!IC(#W-rH`(lNn -Giz!}ElRE`}c?Ia>aq8oq_NJi}Gs;&?o1keO=>76p0Ya!j=z^PRGCnzY<)=0Lr(catG3#XbTseHjUy0Jq#5mIg{m@C)<6n*E*7^`l50Jq8=8AMeM0U%c4+7wj$K)ATKO6m6)ZvB5=U8k -REV*O7JiYBD^=pZ{oznTlB4fhf&}ir*?1%DLlj*03c=5Zb{ATx+Ei^pjb;tviUOEX#PYX -!`N{saPB<+0Zq!)C0Z%W=%_B2T1@B>XhVH->?9Lv&H@6!@=}W^Q(W00>&DeC0T4D60K8S_iaF7}akT> -S9vI+u*eNhWezFNmvl3@jcS05fjKJ}xqzgT3yTi`T*ja~>(E6Kn$_>&TQ2au%)(749FO20${wV@>6M( -&!BZude%C`bJceiZ4wD;rIsMp>*Q~YeOp0$sRy6bdUr_CA-`lr-)+(J<$!qDY)?oEa588vwrvw*kL$r -vYMzKd(0{*PO~8|6Mv{P8Y3B@BJt4&g@8;tzk@nh_4sA8b@U{y{ -JoCuquy-1e)+ntq}NHt?@CKrJ6cnnwpmNDvnXHNtEE=+8SIyT+K(d_y -nF~c?K`8E`-xrF_*Z&Xo8H&JM0ZUoAoFj7U{wZ7Zmn5b4H*i%w_Y#Z$sop&KQt}6FbxDn2*?=`VVp -gS3`A9TAw9ZM2q-rPBVI9|9E1b(&2v3#l<#Me&6^h)Pun+%GY9;twteoJBU7^%T!c6};h?S>d>^5zMS&JgF&bw^}~iG&RDX!x;&;kQzXA;>-*g14yg- -CTH&d$TU$E(b?HhPuT}U-gn~M3r%pZ!*b=rDb()o;EhmhqP$;XB2Nmfl)}zk_IwdJrr|!kBbT_*yOnN4W(>r5= -+PIZ^bj?;i&{dPCr=0qNZpSJ7AoN%ISo@dc8s3S5!m)OIt-0hZT4|WI*^KVMQz6*_VD4z|MAy{vB)Mh -XJPEAj?>pl!95}n_{^p_LYB~_wmzd3`U*OiJ#~-rH2B`uaO?=+iY -*->%=diB^R(u1YvUMWx&F*X?U9gNEw`3fh`egTWvEm0l82jD(EpW;rja6tHsz}_dY1Ork%_QjHMY>1q -&ye7$@jL^-Si~40~>>;C2v94?HMD_{#Uam%*)!Qw2gOrxKoxg3g<`&O{(Sz|Ee{;;XMX)&L=O-n#n7q -wGB4cf1oKJ#cU~JgT4zY$}#UX)FHb?%@g0eXWyKtWzY=;(2;6J>Ne@7ULz-y_5*zaXpqLghJl^PtIHL -4#7sE4d^dT?=I-;yXz3RMrZdJ^f*~nxZ&ylcR=l~7JxE{G%_Tkd2!8*ZPdem$evg>{R_G}QbUDw-ik} -?t_I%wT#c`9(A~n(=`|ZZsv~t$D4(e{tW*mjYJ@3(%yVWE1m+Uizi}u;=d-Iu>`8;sPS>Q%?O&W>?(k -6N=v5b~@6aWAK -2mt$eR#WxAygfw(007Pk001EX003}la4%nWWo~3|axY_OVRB?;bT4&uW;k$iZ(?O~E^v9RR$FiCMi74 -ISB#PuNQn!nPL)ejm4Hc5!A7>xQJySgk71SdF4|ourN6%83vdiXJ*TRl;+grr`R3vwmzU=sc(R`-4oo -LQ+wpC9n!3Y3;gi_Q)+GdM1Xr;Y6A_1@Oc0ocO#aO{k1VNH5R=Vn?LsGl8Ag#Kd`B)fs6eJ*&Tvq?gG -R^E2Fa9VxnOZ*m=u~`)kdoFg@|D;Rb`JAMg|jDZcE0~RFNvUHd{)yHqah+pnrFFOB83rEf{bPUJIpZtEGPnj(XuwFAZ_QeYx_)X6XG;~SZPBGvA%d2v`5<8ru$4K) -kpP7W(-l26!ayXFpQR#vCKHx!3sWu8EpYUcTpvzWkB7NgJzk%moS)9XX -0#)S~S76M~Q^ksJ+02%p<-0!F~S>VR%@3Pg5U62aekty`kG&cpXL!!69X&P#Ww9dBRP-y~8l(cK=J^e -w1ROkn-5iAjMjcLxi#6Yp|brQU|ivR}BD|{Vz-oOc7A1IbOOemn^r+xKDYdN6?bzzZhB#dP4oeBsSq-?jjJ#9fPvW}a*EzQrj~W|BIYpotjQRq3xZUQGgd0 -_*4wv<;#L73cZKN`a*GypfwxgxbMyzyU$tv%jyY!T?HLiwa%UJ7d7q$q-x1m1^XYR5|qSj_e$zj2}P9Qu^@#eR -{}0_lV0Cme8#{dvC5;>r|64A*(qXDhIQ#+8Vu=VeFG@I_P97+JKw=oJiCgEu~0=vT@d(}seJ9Y)4P!Eh -0T!HZ|p%XIYLT6?_22c?H)KGJZs{kh=m4a;kFcXv{BvoXKHuT7aX_^RIW<^jCAI>XU`*P{DYkN$nCxb -}YJ8t>0V*Kz*@6aWAK2mt$eR#S&;{S~MJhuegrrE5KoZi5m9)|=zS|S`;Y9yF!Z?s}W?u2eT5CSN6vQ;P -z~CP#wIFF&Q4@;9CCIcQbIrmR(HYec!Jc4cm4Z*nhVXkl_>WppoNY-ulFUukY>bY -EXCaCzeb08mQ<1QY-O00;p4c~(c!Jc4cm4Z*nhVXkl_>WppoNY-ulJX -kl_>Wprg@bS`jtjaE%>+c*%t>sJh16q0srq6yH803CL>`%$3V4Z7Jwkp>DaQ8p2Y6i6zr5#+z`3@OQS -Vh7d1mdN4b%$u1v1kF@D><9d_wt$BGAm(Hl09Hf~qoxUmRa6lZN&s -ap(lTypjG^1LZ}M}WKuiNSmitNt4&J`5946jh?eOj}2#XzSuI5MZ14@(E#|Jo`qjOsj96&2?uZr39|U$T_l(rNp{9WqHiU7q>>F;HJ@i)8eFL(FU9iPe{Q -A$|Y}x{6dRCz{96Y6Gjkfn8Tvj&PiXlKFY~MJHqi5kY8bGip4+umY+Rz`X1a(V-&A>2?L_EGa(T=%H( -j~-lqfnLNbVLi-H}Pl`-yXnCsK5B}DTBwS7t-+f6f;ai%UweIFHc}Ck3#+7X9PNI7T*SsMPtQj2T`EH -vLmO`q4!!YRf4kdkblMuJnOT+P?(V{jIC`+qkRFPagWrwAlpbN_Q#f*2+I`nE~?{ZBJ2!44gEGPUZSi -L&9)Pztfi<1;UD)}(O&E&OQ^Wy=o0SkD4&FWbCKQN-GQ`JY8BQ!=9X9U?hHQ6dj2TNH}EGm_vskDftO -T+)ila0`(>EF@-xZg=1j@*#r2OKdB^7+d^=qm(=oV6XI?R0RmJFT68n=TK3`p1(R6wI2l=tvVox3P44 -ZSO`-TX(7u4J2k7+3j=RBI81S^SSg=OJ*OJ8w-@{vzGk|WnRlEh{mr=)@n5zd@)v|m*6~}M$S4gSmg2B1P_``YUlkvJ4@5BGev}@GdSS?$OY@0E{!J5u1TzZWEnue$fQ57R3}shnMka2`)2jO{@*dj?mi1Qr(X^NHvdzg>U%wE6&UCsD( -AIPQE)hvReIaCINrgOnfjk~iMw3>;nxXyIP>CR~6RH7|a`jw|OYaY-)E-8}=fT9NfPwOINV%7-KiV-5 -#Z;Nsa;qQ(Wx%N^+*GFcS#g;%XShqx$6{fZ(Eta>R-qyGU=O9KQH000080Q-4XQvd(}00IC20000004 -M+e0B~t=FJE?LZe(wAFJow7a%5$6FKuOXVPs)+VJ}}_X>MtBUtcb8c>@4YO9KQH000080Q-4XQ%t2gx -WW=( -(2XAkzHcDccDzyL2ODoB#>NKY*s0V+Gt)3K8fmT;xHy$6gKfYIwlQ8ni06qgEfCKIhkG={~JhfBRaMrLUu$@HGs&q -GQq<8AR`s%B?Hl(m|R$&=7Dyj;@vqF!`_8^&2WfW(k1B|FjOj3*^o>3&06MzJzgqUtoqHT2@aEC`*2N -cwjV5JVEv`b}8f?w}-)#3%{qBQu-;e*qy?Q6Oh#Cd$;XvX12uR%z<8s6*yL4>c89Dy0P&zz@!LWI;2Lx&lTUHne$s -l1jhh=Umxdb{9TM^C7!346&0d*kEZ0TreZDk7TVdO*+a-0z3)i^+iqKwKD#XXrU$k@$<>t#_<!~Fm0qB*7phwx0*kduyq1m1$MX6Dndig9{m{7Be*x^#5_x4R!*kD3M -6LjQH-bJxez45d}Jp3UDm^ED3irm0`G~MW=Zo<3%|Yx>s{$-!7O2dI5HjRKL44i77h`O(pIc$`3+AlR -fh{+uT+*6q$y`;-8fdl!gIMl>V-Kqy+8Fr9qY+0F70WX|5PKG?(zPKqSQ$=6^R=8CLgGFdxw$^j)kg; -^EyCyWZ~yumHBZSYAcjB}tU(Lv1R6g3ef&1tsc0qbwkBL_1jECR8PS7cZeu(-Dyq>ZvJ1alnvLUuKzu)kwn%K0i-nrYp4gt&#rsQ+zdZFrtmNbIvT3mKvUG=4%t7SX-=fPvf)k;Eg-o!D?K -*)89$d&Go~wcyRgN46&lh89Pinhksw5$Sy4jeROd3Tt9+C%SP#BW{*Tw@#Q -{cUUx=qtBe@%=X<0TO**j_WGN)TVrxQEICOgq!MwQb}2Z<>}QSTemogdiMg+_-VwrpEOYvgMPVR2*UhaPGLGEGhQSNc>N$zRxS?+o6MQ$$ -lGWRO?I`<~`Huo;~KBwi64zek}T8>*;=wj!RDR2hQWg_FnC=1;QaNfbpsy9Ay{^z -uT(x+*lR4ul4Jg={d|sA^+lEdvtA^I$uC0E1p7aMY_I>w-0IU9={wOV(v;(7Iw>wT7)JYucKzu36Wu8 -`e$hmNjeLw(eNxth?4d>%R5CdT2edo?6eW=hll*K+fTfbJk1imG#`e1#uK3Siw -FV?S_W0}FsQ092%M&?B3WM()sk{QjM$(+rMWzJ>J|Laqz8XAjSUZ8`K&cU0$WNyH_<}&9a^i}Z3F?jK -3U}DY-cq-q+O5V6DHTh!6g}NSpJ!%2npMp6Xa_iRl#Mq(`qX3&+1Yl)&J+dAHz=Ff85?vHqmbM7C%E& -sEIStqXWckFEC#Ohn5-k1XasX6iX}wlyiGYKFUCf9bNkAs_bB?$Gz~grV2lQ_W -=Z#6T-9ln0_8V(r559V-&wOri@wR9{ymwz@LpTcu+s9pTnp0X*_0(;0a^WxMkeM)5ddr-T1up_`Ec-i -)Z@;{0weOtiXCGU>hrNvWEr=_k+Y?aW+sK=1U2lT3AiBQ2wSpoG>q$m(45YRddpuGN;WM^O||xykXunZ<({^9r*9AdC$CWJ} -@7ekIZ3n)I4RLHqV;pOg~P?OZDch`7s#N`(|?*SRRBSzVy(1VWoAU?u!jR?1xMLzM`CHJpWmQ?K-@ -S2(7DNE5&Ag6m_Rq{krIZh>p}EQ9-dG*o2Y)+6hx@WKhFYbGx#acosjx&+u#f0bJTR9>k~c7@ok>z8u ->Uo~>P!=)RQa>{Ud;Gb<~#_z{9jU5c*`HOx9MjeS$8v9$pcGPlY$_?y40n3gcJBg`1L!e!%gD9jv&Tb -~(LDy;f-i;^#5yvPduq7+|x(XP^RY}vYP`_5gh-xeo#=YcMKxex~h5&<(I^pS6GLWRJD-LeTMu2Gy{3xm*rc9?TRXMeX5s^BqVqx)`j~)l8NqM -%j^GvX`L~FZ{gec0e%Pu{5gJs)A%EPY+S_0@DLusqhQF-;q!P3&*0O`$x-klspf=^T&L -xSg;_0PJEy28W!q2R%h_4ToeOsyA2mT?hM__CRw)CdXih2E7*3p=Sk@uKB64%Ee`gS=#V;0=sQLwH7o -hS{v&+5b5^a~JryZo|o3q12-<8$qvbSGVdW1S^9F6I@(?NWusme$Kn0W+|K8aM-65rnK*I#we_szDO3 -ga-vjDNuiBjMHGo%B)L2LGApu?fRsCh0b67`JoC^Z@e-d+vYy-jxMX8q%J9r^eO!+wK@8WfeNo=yf -&WLo@p5yw*WZ6TAYJCE0R)WunwH;g*jH~^9Ed@A&`CG`-0gAzc~9dKo%5X-tLcjz5t1RCBlvs5O#6LP -W?6@?D4nX>F;*yAKUgACrdC34l;_c!2-f2x_k6HfUqY=diD3b{wXOK!zGyc2bmYbI`qL_H3`7-3RK$I -7>X~l+~jHtYRi&7HeehX{68u_ahIHg0~Q7>RvB=-P#bX4nzTmk>VQ*reZXl~g}_-?jlg;9;c`k(G80a -DK<2WgWhQBXKxT@T2xO*RMFQ7cWdb)dqsuHi`3P@+<+nOuo2qy%VmP-0Y}pBcw(M%I4ya|j#&gqN6ua -XoJGAf+O1tR2nN_$SB&|>RS$hBrS#Z9~&JI>&g`lRqU||d}Ok;pT1aAz2Z9cviPf+VT-Fe{Pp~FVn_! -wkr75)DR_5X#8!x#d)d_u&psb!u~)c%2^#s|aKn(QC1`*kRi2w`WBF@#_DF9H%tQF~M*BQKzo+5&|Es -sD~NVGs}D_k8ki>{9#$ExXy}TJD#mfBRmwkUra8NLzG&A -Eg(t$c4z~u^G{jtQA7H15dZVvr+%N&hd@E+exLk(+SdU5q;2|fFNv$Nca?3iA<*QJd@aYUOboru;3j0 -BmD^ukRvRwY+Xf5B{Rc)BQPF||S)WCWr4reI(X~enu)h+`ausl`F*m$w4n|svjS2<1OGHNB_>FPdsiSnNDDerWjuMaQY5g;;q{NquOHM7Nze>tKD5 -adG>$rGU==JitRPsphaJj{XKqf=>wKKHB>$fXofoik&3zqleE3mX*SlQpUj8*-m7WEe*VHMMXq$oXj9 -D{#R#Pmy?q9B2uv5JaH0`Q^0pERs4+Sf1>RqYc!5H{6wlnzBTq%ZV_#kXt#-WhiG?+c8_THiS~eK4~h1OXpf2ZglJER_KaxHiS~kMb3}Vdv -{yuXO|&;edrP!;M0-!P4@A?5mL}S-Bz=se2T6K}q>q#I36efZ(!(S@LeiroeTt+{lk^#qK18m6?Nzzj!Jx$UxOX|uCQ#S8Y^qrL7C-&fg_dzd6sZy`!ibt^_kg-v -G8Jo&o2kKT!I}gzTrtspw%A2mO$c8u7o(%h260dT@_<>#DdNZNFEVyF)jymI;ii$t=4IHI;84k2o;`n -4dVi&pI*ww1fIer;mp`Q6h=K*}J>(F7E<>8r{9rPCP&K>MPw^y9j#UX`4$ZK`P)fRLayU$lC8&l&l_9 -}4cLR{>ZsU|nllke1n+tD69J}e5%#>Ob8_@q@HX6amW(d#=HZ?`{9<7>ws$nB^IITn5!V3A%Bb~vY&3 -umf%Hr87TK4?&KK;Wos{Yoe<9z}vozawGG!aYj+iwjPC1k>3kJ+_bKcs9zTrXN=SXn!mtDMENukT*38 -U9I#396rg_ceZrxVb*O}y{38frcG-b!c13thht>ix^4Pl?T>yTLF2{3M?e4(68eYANhl6Mof=EfFS+z -5(9);B9aZL{s#Jmnl4*%Z2|&)axrM2zZ={3Y;S7_|L~1pEx=5H02`(?{Z0^o4{ -`PvaQbNB4sg}j&&3rV-omCO#}X)sm;~r9j$%ChE)Ely*dzq{F_yRYCV5`$U+P0FZsrif#>JHVZ2@4eF -r|v0ZaOGRJcVD}uka3{s6VUVa%CFCHrMyhf#4fqTj)~a -))rdwg-=6v=s2NEzujuzN9%chd(rj%q2+9MwIu%i@kL~x`tOh9v53$EKNk3+U+Vr3P)h>@6aWAK2mt$ -eR#S$EYt^#=008d*001BW003}la4%nWWo~3|axY_VY;SU5ZDB88UukY>bYEXCaCuFQv1-FW5JY=@#X< -@-@?lGEUBrq_5<(goQXY%4wwIi4#oj83{@yZ?Ak{FBdCUXU(vQlQtHG;8v@1qkHt -ANJFFqL}HB>Hb_^24zz7v*t6C;7OeEc5=GG_geQVF&rzZQVA{Zcs}D1QY-O00;p4c~(<{b0001RX>c!Jc4cm4Z*nhVZ)|UJVQpbAVQzD2E^v9ZR>6|mI1s)2D^xWHLoLhL;3SnvaRB4YkT8$ -}rY5QEmP=zdSQ{gIC9#CR-ZbEmBNAa9Jk*kqve -?N|MXM3lR#1y0hiDr5`au6elS}GWX)3OTDEOltAt6i&Ej1OWCI)oRUpZ5ww#bCp@K#V1c|a3gDNArU| -cc2!AP136GD8;I89~OS_0gVM5+Ad`Wkq5&%Uu7Vn(}}*j($?wbl(mMl^>|i<%NFACnD;{g>14f3>ao( -tlq4ZCj&YYE~5YSuw0lEuqRnU7c*{Rr|&wcnMHD!HE|8gGeO`41e2OyP!%?p<>vmmiesnbXwfoduQX9 -!SNfjmswMwB9xH;;4N$y40=szx6f%m*r(i-arjl{M}4zVN+q5Im(17gZ)H#aK%`2p)(u0(nF_;}gmhi -T^>sy50z~efi~=594ERd`DHN$vf<5i@G4B%=B}7Afw|xSEXIP0msbaX=gHDV$QpO`VQR| -$@nF8n*PUNUG(Tf`Jlk5e}(J5rlRAP1+{}n#LQ~P%ZnChst&II8ojzy4|iSygn7h4maDYL7Kg$gCLbj -uFyvyJ-tkET07lWjohx{O}OaN8Gq<;9-PnInx_yInuPosgNhJi&)g&q4$Dq_7KPaN~-heI$dxoobnb(##}I!%snohD1TPTdnrbx42kfLLf^sbwPL$ -^Egh#XZdCi~HfUjP$BA2!~Vscf_$Q{p>OD@j=wKveNYC=mqXk7A5cpJ1QY-O -00;p4c~(=)#1YeR3jhEWDF6T?0001RX>c!Jc4cm4Z*nhVZ)|UJVQpbAVQzD2bZ>WQZZk42aCxm-ZFAe -W5&rI9fu28zN+tQN(`lV*Qd!o;P8{dPcE(L@mjhFf#4|@F71F2L --PDNi;<{Wgqm;C6r)Qy%eDT -`8?X;potc!_7R32KY;p(3eceot+(vIVP7p2GbWtj)a25T&kzXI|Tj;Ni(q52mK(TLV&3$qwCw0CJ9SD -{`fNsWM8ZGdOh`vF9o4QC{f~xELagL;C{|C*&h%Z$S_{wBT8fnaY|^)Vo8#ezih~Yr-Vk4f}&EU;bSH -l=mZRgrywec@_EhViqA>!ISnQA`aSS>6~aj1&YVSTNhO;T$qCO=g*D~zHHU_Y -$7RM6Q?}*oIJtd6cW5btB)mQe!J$6)C+IWX2@Pb=>W@BCe+kR_9CP3k*w)542;Q;iZI`}ZZK; -@n1yd4b?vSOu^hZn5v*#MN&$Uwvavm$twU(__7U{j(eUV{gIP_Xk(O0b1wbgk+l@kSb#>Z~D;DMNfqQ -yCeVv|7u0Hu^pRaFbr#)Ddz?~x&e+yF{7f?prAUael@Y0A?wkQwbq8i`^!1~-jjrC~i2`@cih0RfwFaVCO@3pW3~G -f{K0H0Vf!eur-=lD1}GrMuy`SfZLM#`!vl*cuiDR)n0;EjcJ!A0RaX#GF41tZY!Xz{W5klgsOK60B$n -RfTD$7YtTD@Vn530tpq-$)q$a4K+w^45Z0+RDr~aCAakuit8ea17s5tKy?!3+JQ#+Q}}l -^gIK2w=_i3+!Ry`c?;pIo-#l36Ls8KTI5xF)VkaN -7BXMyodjq~|>+BwdHclWP*`T%3xQwP2H+a;eNcc==$^+rhWb;{Rkh>puWot1EmOK_m4z+BS|Sb_;A$1 -1T)bbnK?X3BYpbMKGasuc9o$FhY}I(#rT(M$VUExqZr`s#a|=5|907o$8V8tp{kpl$p)^r0ZBonGLBiR2z_ZR^%l|)s{9=Mhzs=+Kqb3i; -cT8_wcvjo%0jnVIqAca95NI0NiO(=VUK&6L?(aTbcxmA~!hSAHL&zUINADx2&)}n7jRa!Vv!-ej2zh1 -vNR~!bZa7Hno@7$`T6~LLIS0KEVBcPsJNXt;BJZ4lHlFSU~kvy*s3&dgG)6N`yjWijjDZ<3<5V+B9LgMJeQ0n4(QLXtF|GvKT51zI`l -O990*PfnpvH4GWPZq7t@zYoM$wj&P616P!@s9y_-*3ZdbO!lweNpz0MxyCDK-2CQ|6za}x=>|&;1C36 -za4Yu1k+Z@CqR4g}pS1AZMq168?b`6NO -ZWZNY!XO`_tt8?DA&P4qybjFs5H89U;KCUD~&2=QkY=Km@;b -PaZ6QntGSU1tzsh3V-&GSJGynSoc*lNRm!0c)ul*N|;V8C9x%%&Yn&6f{wqTS9-O1H{Q740r=Qu;M+ssMCylkvX9O*Q9Rxk+ -i)xv2us%}olU!c7%`4sKG~Rc@+Bu#KC{9!?SBR4fC-ZELyxHBD?~&#QMm0Odn-YtM?!8L<_rA -8fjtrP8r-tI#6K#L8*mt&Mq8-Y`Jk9;>fc*74$LcemaEh)urk_r2b_Hz3~rpzpc~2<{#jg4ez+el;MK -w%>K39`MF_bhkOTsG|0W%Mj0lP}F=ItDMW3Gd;E6qwz+aAUNCJj)PUZZG%0Zu}dAW+LKhh(JanzibK? -@u|bv!^~q|#IHpE>$E6AKxdGj_BDhc(x3L~abv?DOHhxKn@yZKRh -{atRGx%eaT4hCHm-|WmQW>=godtu)h7sep)`+TUx|HH?1uP#)w9SOcBp?KZ}nw(5eUq!-bRco?->n?{ -P_Zl;*IhpXgK-Co$Xw)f!-58Kurg}=+><=Eu`RY_{T+_HySP!jwppvWK?KjIA=tAz`xLMGg?TvL`lOM -#6%jdH7$+O1PKZ)D7wdws(39W6#SV{%+MUb*QE5R-NO!=BIwIM!s4iSYh`yU5Du2FAxZFNbw2-U~TA) -$|gN10@eCSbFqReY;rZ22xj^jc{#BxMnm(4LeGiF4AaCe1FHfKrwW$O-(;`ExuVQ@DcMhdG5sa|$=uF -qgDm1_Zy||Js{?9k9U3FzydX@5J1xn;N7bn7v$}b8y?yRGHi1s*~A8I~AFd5bz8N#t(0QP&|L|3c1#c32HX -#>_Q10;{O(im(aR~GSdHzB!nq|O%~JNE_jSBtRvHgbx>gC2y@5}Y-w|4E^v8`R85c5Fc7`xSB%udrmGOa773|RB%lH8Z~#=f&7?Irb~Sb=`S;k)m -y_+%ja1d18IRwadE*(Z)UhVMKN$F|Br^vl;S(bxu!ftLuNEo1zyAP>7c0RkjUf0ArN#s_B7C`Bt?%yF -P;h~1#LJ^11I*xGZo1u0NxL_KZ##>wbrcn(N=TX1+^7?miyLUY@2u%TeRHNLij)Pm(I!`wfeh}H#NLE -NohdpIJ03BgL*EB{d=LdOu)E73 -5;74Mg|2*An0|IZmb8Zq_o+LoK3S^XMfaIVu*LG^lj|vo|lmGpd~!S1v#?Ehko>NT~6yGWMw?!HLiLB -K||b``2?^d^gqCXx}OBx#5_858L*9e2H>!T!GMJt49o+%#%Ptl=h`5}{Rqq_A>F(>akaydqaTl9Rzr1 -!vwFUHLG(01Tkt0nG_@P4GkQC`bZNURnPht;4|b=7B381EbHvwnPILm-*S}A0)lqPCrY`R>R1Y9(jw1 -KFf=gSfz}1zcf6yisnh5rCk;Vtkq#qJL0J0|XQR000O8`*~JV2pbF$Pz3-092Ecn9RL6TaA|NaU -v_0~WN&gWV{dG4a$#*@FL!BfGcqo4dCgZ_Z=*&Oe)q2!CE6Oc1m*G;H4lm7bX|Gv$as~ksujX;2(*~7 -W`;Q4U*9tfhQVC2Nn2@AWQ8;5_8rb`j>(8_b}W911o?xeo`}4ZeDat`U`NhR&n|3o}cRDZ|7f@-J_eg`DDm7}1o|1eWk3N)5=8ceb4WfAqb4gKsnS+qv%g -xk*T)8Pd0r2rCU|hrsX_~I7t21b_L6Q6NmB^LA?=0=LDz@Wg={k2KpNjd!_qvf!!cEsyT5~QMDWSOS_ -M@KShV)vz*0yq&Taj;DPVi-Nfn-+&1E9p%9K-S@vTLeVPzwqUVxvjXSdUFqpBC`DyX7JF4$fXRZ$w?|A~k$n`hOH$z36(;)mi%$y%AsD -!a8srDDDS#7bi&}H_yZ-6C<2Eo-cr1w_xkxepTHY5f&4$w@MEpp{IMDy`JKIPQEc(eQDm!{F<_VpX~p -4#NJ*v%NQ5VhyHGmv$PAo5|Zit=)qYoItpEB==C(8{Mrg-& -MSXQ^G#kjBhPU$JFtWCiFDIX#g#Sd7Is!mBO0o;ec<;7pP2y41N%;AI28kdnrWwKHpScx&dw*jX= -TC8q)lf!m0s2LFVdSvq!nH}yy7DGC4^5vQ7@2(`u%BA<(Nv-N+M2pG1WB7U41RzCMO}kW -Vc*hWxPsZ{7hITEkIh^jVdUWkb;`zBF6ATNVoeX_NU>FV`8xEk#70z#q(X{6ovZi8LJK$&}}^@yt<60 -4DZ9oECOlNA204+FNQ%}^{-l=oCTq?84! -rY!P{FN47!iceyqjd!GoS6?fu?aq)VKXI1q%dsb{tr=1R{-HE!o4|6%GlcwYu0V}luwWeg15ir?1QY- -O00;p4c~(=RDAwlg1pojh82|tu0001RX>c!Jc4cm4Z*nhVZ)|UJVQpbAcWG{PWpZsUaCz+*+in_1^qs -Fb5>gBa-h!Q`Q6(bTm^e-ysK8O;C$yKrY_iOt+1X$!tKZ%;mtAI-1w#^7eJC&9Gjs0uaUFVWvEuRFOu -&0aeKGSXyz{w8$O=x{ol_6a#}{yV+)Ml{C6L4+p($xWN(fwE_4dk6l;-@KmifNA}j5k>Dg@mH{dkye+etp*a1OIlRZQzf6P;f`Vm!p1vpx_e)_ioPy`u)t}LeI15l<+G$WetrmzPT8FylHyNGa+~!WP+qF%an+Tk9TRqk< -hFbJee|1P`W;pz!p4da)Bp-EqJL6X@RJq0<#WR6c~>P2f={Mv&0uN)ureApCP8w2@zYSEm#r=80TyWV -n*`NDsG;|Zp`r77i$)w%={F58aXr&F`!I9n6Mx~>BJ9bz>qU;j;L=RIn4Xkg1s1%chTIA536H_aRI1w4AR`lS76(V>JomY6$uz~> -(wwUHxhMPAn_~s`W*~nLuF6t7VVR5_HCQBOn{>>YB*{s0Ib0p`|$XkJD{Ph5(6RR*Sl{ryLCy7y+QDl -wT#k&JWH+0{xFeFx}E3l!kyF1IMB3RNzMj`pBbZLP??7sCA+2PlUssl^YSkudlk#y?|`EzR3F5v1QIX -89am+NgAi&>tbW2aD23#G3s-nRImTMkiyO3-Ob(2MDrgf5^=)FY)BU;&RQlf?BF<>p(n>1s4p861)JEva-@^!nSZ##9 -GDfUB__~AOJ{T$G?a6L4FC@E+H|hGLQGsC&i#_577nfpJXlPnvVAGv23zo(rX4LpVqgv+X_xmg>hS-a -iy^N=f|7mEgGC3!q+Iv6gnUBK|SA!V$WP$@68(8}|@KX}m6IzbcvVAUpAomMgz*6XFYHve(?=!r}w_f -M`);%|)HNP%pixOJ)SPt%!IE6DSa|bu2qq$yLGR_GV$x81gF}+h8QBIEaCU+pBM+RkJip49jvtn1|W1 -)T1LDq)<%USA1u}ITgMJrQ{>;33UmI~we`xdNr`TJ>X*y&SK00pwvmqZhbs$u7zw7||$xNjcntqmFZk -#We{iG*WuOj1>~#)osQkm>q -8J)O{v|thQ9Ji&q#T@r;i+7{b>O6G=4$~k$pha3HVFD>TXf~Ix6f`}Y;Tt3r2!td_Qu#zO&XP?50>y1 -DS>oVx(*zckkh)+G9>;$X%&F%Nd|Q&dU1~toMwv@B?p%~xx)!gIO^4jBf}6ss0IF|3+X0bD3SC^cON% -jP(L~LUwZJWNXRWAU6IBM1q~y5cDqE}#%hIH?Lld0>z2R_l(ZI@TH}}zE51A)FTV`STjxDl|$%hZNI8 -titnizHYU#n!ZBJ~~|BtHF`mRj3eBrtU{%)}xSy*ver=5_=}t&;vN%XGVrC|k-YtviV-k)u>?Jo^77; -D@1FHSUttV|r|EPUUrOKc%)u4`*{G>=w9G)0`3#k;uW^a|&ROgHCg!*(Wh2Puc1v|DVw4%WSr{c^C7) -)yto-RXOeS_|c~Mz0GP%n|`dF9HPwuZS04%(Z2e>wf*GQ<~)c*EQ-go`nH@6aWA -K2mt$eR#RPX68&8P002b-0018V003}la4%nWWo~3|axY|Qb98KJVlQ7`X>MtBUtcb8d4*BIPQx$^z2_ -?|?XVWHYHSy%v6{ki>WefCo#I1Py@;C%Nnu|O>;7g%*88; -}AqaO3p)an}3)E`KJ&hyI<*8};!`UOx+0|XQR000O8`*~JVEYrOXU@HIs7oq?F9RL6TaA|NaUv_0~WN -&gWWNCABY-wUIV{dJ6VRSBVdF_4ecH2gh=zl#$FEw6*4h38GOg^;3>^QPK(T-zzEjh`qqG2Ea6cK>{1 -Avk_-kh_qv9EWZS65ZnS64Uo#lgb^5tm6;+#HEjGduVkf7#pL+ZV^> -a$RLN^F{>6Bk|<%;nM^7=QHtbRzz!Y9{rRpLumFgi_@Y`6Va5SnWy6Qx|x@SI4@_-eN?65MY$@HsL9| -r_>fj2g7J!`%C1*U3Eyt2G+m@cgRsuiRJ=SrK6!P15;pG|5fzCzK6`cX=Jcz#7iVwI@keO4H>=8pm`- -P_W>uxrsmK<~vV!KATB_)NXbxlgCpt1NG_0_X{Ca^t~N6oL)!qEpiZZ!t%~;ZPi0onhkBW3jeJa>EnsiWK{l5cLd^|!jcNt3Kf<3|11-xY;1nIh6@#pJ-%R -_878d^5%|&Lwb@QuC$(o?V+iy<6nfGIoCa*HyYA>G>Hyw1oZo;+vOp5$KWBFZMW#9ONZy)*utSkgDh& -G{bV$0H5{e7IN04Ra2I^UbPu4)rJ=vf7;%57Ugx?cA&2;D4?s;=nixT=;^E$QH7dW%s>ej<^5P>g@_| -ul#M9TG^s({>Q#jd!Y=T%E*Gh|kJb{{bOrNWR^^RSf&4SdKdlLT9L&qQF=TvS3EndbW5SJERm^R@*2Yw@ -I~W)BXBnBTt$(c+Ho$zOGM0ZIq?26=c2u+3Hbp~Zwv}vQB)H$?45%nm12Q6r1+E2?W7=XN -H!m$ph^;vj#Fb^TwhYVIBp}EmsY_k=nzNc1fIws0;8160sK<8C1B6>Hy2a$`TDyb)j1{AOY-(UwcQ&Akl5+EB_iZ -?Hg#ixf){s3zmtr$cgpN78=pSHRh1>@+^BO+s%rOhlXtD8s2hey>c#;*Ia=K=RLhBPM8pc1{jGM&AmY -(!GnCPu387p+C+?iKYc>d~Kp!gn%f%pW*T~;8~Bgq;t836M3vn14tuG+9mzwbW>$A5npNmb0q); -gSf=ZchYSA+4bmG9W>9Hp@^G7UF)M#d8B7CJVG6#Pg_%V$@gYUsb>WP>CXsuG3tU(6VCT!9FO{2PoA1 -I5f$lF)*>mJJgsX7oR(bcchmVi3_-+U`QB&EJQ%(!4i3QU1*aFmP4Sa*p!sZvY}GQT@?k>L)k2=V={B%QR3V9+{-CJ3jvziq?IGz9fo;W__VqE` -8;(NW+^@+~40n{E*SX!C2-IMEuq8hld!xpge#Qw$@>kBt%5k@TfbLVAUXyHw7qCVQHDx!zx*Q;_}k=; -b;d0v6G1ew6dcc!1vh@Ro3u_a2TSK9U!C;|Mc%ev`69TpI@E5IXQlIe$vTNKw~57L9UGqLvQ;V0cybO1kQWsVv(8G|1Te$i`WA}p9%K1MYSt_b){jH5Y7OLf=8;F=-hlYo^Jvnb -0~KxJ_Tq4x@a)gTUt@N8AkT6bt(;{>;);|jdtn(-@U|O@W!#9~Gh{V8x*ZCWujj -o9g2o%3z1{eZKQVb%^NWpJ|a(wdUVtV%a;`Hp*vzG{net2{8uWwJ^oID@2A}GB`$0@5ptxRY-C=9_6G -~`d9GoR(nD`*S?9T>U-S$3PQ$KnpjLdlQF@#cV@(1HvNSb4|P$Oeu>OTfH>Zt;V-*?FX9r4};*8@cRN -tz1QiYG2M6rK~LSHR$RTc#@zRC(8ulZG?TbHXAiAc*M8F -Mi|@bxzU$-Tg5{+_wjVsI7)2JsMDnOQShnC6D`*cM;tJMW=xt-LnH@Be7C(Bj{YP52q6 -R2Y6O1$p>MT!oZEq3)DL_sagda0T@0mjq*g50>IZeV@dFXGVH+AfwjYaV8K4I|){_{GMjg+{L>`F#TV -Vg7+Jdvf03xi7{zD%&u!Mj;RSAA91_7U~OfW>qAEQ73q}_IA!G0K&fY5}XaRh -L|A%cOdnL?ptDa)i)>!FWVsN&(FjS=6+zE#i|3XWl|@LRo-NP6ff$UOofcbl%e(lr8K}4yr5<}4cH}r -`0(~Vs%~m`s{|f~(`k~$&@@~v6CiaUTXu6Dlw(x%;z;B}2(ypPj2M%l%B{|)GBJR-6X+mbfMc)#2W3* -$;fv|nU&Mb1`1Cz|8dJV+f_Yq9{~x -g2nEl|2t*ji%#i79-AV2ZVuj$yWSC`%^Hq{cLSfq~&0%5QrBd1qpfT=YL9}adg+jUTQ5{#=vX#herY8 -(-IWSoyqmI{Sb3!k5k6oASfF?dJaoKAWGQ|C1p1>-*qw>OJj{w3!@9@gwugZ=V -}ztS`Tl|yX^QNN5TQ~o<32snaPnhMCwHkAA{eFwfP#*D-g7&QSW^pF^#F$V_?Zi|q5qaDXmB}!n;i+Lm -$S|>W7W?gQ=3~`~TBqWeB;j|XaQ2jU}=K-lLxeXtE(L$DUWC!rBtfyW7%xy0|A^6>KT0q_;Q6W>1O7d -&5E>}P@bSaq@7spqxp*(@x7Ts{J`$_m2XNHhc^!?L4 -u3GzBw&eM;E2;p2qwp0_TYjZ#vUyZTO;ZKN?+{B(rIJ&koxE!t1m!P#M#Rw -m8rY2tGJ8CGtY+KAtH*ZwM?Gy~5NvGF4GsJ*cfGYNU*u;4W%fMCXRy_tRSJHBVV+srq%9s -pbLR$)?sWBjoZeTtRu1=q?)i`~3^yVf8e|v -Nohn!53n*-EAoa%xK{%(&4ds(5=B;*IX&|A$;g7Jk_w;VPf!@XzO>PKdO;1gQ5}v8%K)EJm>v3h^Ve( -T_3C9Y1vRCU?#<&lB9xB5em=8y1$*jsJR@J&7b-vAMrMbdc@zX878w$%?ULS>0qR{Bv^f5KzYym1+WIHG|D+Iq#>1J26Gq4KXja^@HG@KDAN -VYLrM#pRdBoFT84WZBfc%NcR0d!td3+Nf`~7(&9xZdk65brfBy5oC_I7q6T=e;&&oEpi464Ei+q|##S -QA!;P~wM$@$5}$W5$BD88n&!;>Z8mEsiwVNLv^{mIm+1HEQ*R6~4HRYh#7uW0V1O6^E#S;)c80- -mcvYlzWNFoOcX|txPKs3=ladqivTts|H}{*AP$m)r4wfCxX3Dba~=tNUDC?pvt(N-1dYnmdUG~Fp9?* -eWW{9N|I0apdN?^y)GpFA)YOXz@uE*nyZkuq2fD{ZA#NlM*j)(<1wuc3zXTfgz^ -~^A9#BIQ3ZP*uOgaX8QKx#pj&LS}jeJRVkMao@D8?N}av*0b_7^G`aerei&RH{BR#$eKHytvM`0kKE* -&O%4vQjVS62?WFB2l13@Y;x!c;!HHSAgZ-LJ(Albc~&=EL9`YK9sCz8#i8>|r0y_QnNz -Vyl)yA>?3kvtJ?2dLAY2cHSxbN~kIG%cGd#mq==K^O>jE5EZbxGwRUww?^5F54PoE;iXV9OL@?Yz=U8(Ih}^K -sr0(deZZ^)IuK(@lz;~>bOAO2ZCyI6hnnETa^>U`!gN|z)4C~_fun~yGsHi+TGR}kU|!Db9H5;xxGp< -!qXFZ3IJD#USym)dmY=}UUA_41i|10?)B>y~I`b!)EaOaRX|gD`^a1UZM~qtc?7_aSAJs>cSkIwAELX -5vo}C|`o|5m46`ZoFgGTlM!>VJ&0EIxe`UA0?%6NT3-;_VpIhbb<`7NL8%u}LL8{&2S_innu -?}<}>X%{2RdWG68f!@16e?>2Ll0tH_lzHQn -&q_j=jNHb=!K%0|%KLWJa9MfB_UIXScSJV8(;{*t-v2UCVxnV)jc&T1i1@dQ9A&dsiF!-vh-I9^`UkB -v`>Mo#6;tGZyD_FE=1>!HS(Vp$x{&AThu(hutl0q3U=M?KIpp64>7joIB{kY7Ff^HPpZ}|cO+VyFV&W -Jr(}$-PrWdY)DK`OZn9+-B<&+l6Vd($Mr$_eYpKDKT{H4O7uKq(4+Mp3s8`SN*=%F47KOx{ZUokr~!m;dzu5-$cug -NI-O8eZw2X0Bxz)pk?8xBUV`&cOq12yaGbpp>bZ)ncb=V6XybZNGw;E+Xr<{PC3-cFbqR?4`4X7wFmk -`6cF+eliQT?z<`YoZrj`wgC%7bwa8oGFox`m=PCq>cq_^iPt`C1CVKTZZ_UoKnpUf#ka<|zL+`YrQa? -Te?c0`9e^favvV_}=DQiPpKSA$?HFlxX94#7Kx2iSG{OaETMf>qEgnxLiuhfF6S;1Nqnz^?FN$t}RFmcuc)g&zNoIU~W_tkK4y -vl7i+aHS#xTfZ5r3f8xHrrQa+BcX1qjIlJJ76E7suGCpDfO-V#1}0~g8Dh~F*jv08Mqtc^RTW%kK)Zr ->S$c1zD=;HR*B0fpSF7Msh6b#4#aSKBJcZaiYB&o8UgL6goo6v+c48yzq6&+r71#7eqUuxS}QWuoS;k6JZ -0NN^YO=4hRJan@RZqYZAp=(o4p-o070s(U^4cBYb)8tx6^4Yb|a{hLR&xt6Dq2OlUf -qG8HS~P?_{+W@;V$Gn;!G?Ps^J9Lex(Y;vG6!`N&~%XFm+ybMYTNCLD8G-^Fg%D&8r4*7he)}^kqH7# -A5@dREtkKfYj*KTk2*kLCE%gZ4M5Q5N|JD4mz+jdiTBzHeU}MAxr8yUtmpf<7^I>eN^2(@54n0+dXh- -=Zji!WikhY$1-e$igOe;SYH5iCuyLzbORgMPU6X2}GlCK3hG&?xGCJGD#iCG6)Uld^TjuZJ!o+EfKK@ -3Lq3Z845Td+|bwD{)%QWP}qH%tx88{lr30CO}Cna^)JgVpFhz3?5moPb9z|fAHZ7V>+2E8M@&}211y- -Ap5y-cwVl!&lwT2k483}!vwh$jN{b^nv=-Hsf{i%9( -9gt^23UYaD-#Pt^!tHQbr9!xXc)7+R7m>JQ)e|iR`yS8P+7Gvl^(77R! -;Z|HO*FJg>`A<=CQx5sG6_!CSSM}Tp3v5yt(uYO<%Lohl>n-!!FheW3(VZYknj-s76X@j>FWS0m@8l^{Um2tn16IltZs$S-r) -P@=_1Z<~)*Zu_Z0@&fj(H!#C(db6zAQ7mE@e%R>|{Tp>QD$6g0u0i!Z+!nyi3ik*of5p0>mJz3|_&)5 -pOKy&__}WlWu_k7s_GhrcEqsO)3<WHp6ha9(F -e3Y+(JH9ZtImDMS2ex*4M9TnHU{!AyN@^Vh#8(l#%>CV@^$eIlFfkd8g8ATUmo=NMh=n33Jz7w_M{SG -R@XPE-ie3BDr7;c>Zm1ny(DOzTH~`0Ue9zj*pc1l1R#y?ybODo6%j#TYmk$;1c67S)P^XrYT-e?NwQ{ -(%2{I;NUe)F#XIa*cN+zviff8^c2|uyj_dUS$pI>#T^&YKi$4JXr*u0qwa*)Jlf%VUHX)bFPeGQt5Oh -<&5>Wt?6O{m8=$v^>jCYJ=jd6YhXhU9Vl;h*Ck&OKd;i8RUTDsasr_+*hb)8cU7hX_8$u$QsopPk%J52@eG!k)8`|0Qg&s-GXilupKU)v?`M{%Bms|~9k>05KQ3*ZNIb -#9FZHLzxhHVn`F1Q`oqT^W7JoVUx2te- -hO9jglWfV9NlBFw#-tC0IL~g12yakF1Rd@=nzqYnKHdhb!z)L1wP1Vu5h%oR0Y4?q^?fP{5986{L2E5 -hQBZZAH5FpGImIS#3n5phd(8=ip?J{3-Jz63%5_zicCu=RYBHxg?gR -6Gmsf5x`rB_Mv`|MttocIk%Oi|kF7e}7@V{u!ulfeY^*9mlf9X0})2TdtXA9cyG1i#mMKzdEa=>Wsif -oyfPlIeM)+6!9$>d8>CYRaOWNot_4^?x@mqTI-O*m&!%b#fNan;xj9&e6Ry=2jo1SM+9zBwi@}*sm)fh!Kh&IgXXoAhRnUlk?Kw ->3g#%CLd;b=GTJQ3D&U0f51_K&L~W_iU1>?`Y%!QrFcJn<8-zZ^)XkVb7U?_YiN!b-+x7Hij_dy2M)) -fWgYL6?j+YrEpG)RY!naIDj!V8-e(}JllFUO}=72Fd-x;AQ8nIP(g{<*zBx-m0q$~Y$0D63Q<<77|b6 -%&(rdb+oQJA-g>4&TR)x_>tyLXAaEAJ6h|`#uBMEgVz1|wK1vY36`~Woi!k|PIMtxf?LqGqj8&gOQd!J%N7{A -*kfj1!!PWG=?gJ|FE3ADoGBM;#x?|fK0pRD>@TdRbc6VI=Zh=TyjILfCE13P(t%DMVhgt=%H5Gjr*AN -ZO4mufx3NuYHT{z2XixLGKXG9qci1y20ZS5o9x_*yP~AUR1QV)T7ww8Lc1{V^M4g3!qHA$s^jE_p -Ds0xO#?BYRw!-ZKC!Lb=mIt${R10!gN$ODd4`L`Jpv(M1DI-tf(5dSt^BZ1lqP3f|HK -z)NQIKo2}pySn0p%+d?TP@K--0o{s2$t-p5cuW^enR|wMawkXV<=>+2Izd`@pv@_1qSiL64;NkwO}5( -r^o*?Bi9CR$=Tpj+*McUXz$OPvTlb$9>NZq7nKb5CD^Wd_-km*aQIybLsC8^Y{g7cwg4ZA9#CPOebkC;?It4VY?`#Jm`r@UP(2AR$FAGw@u?-p%l%g)lXAd@e -m}Z3EE8_#*B6kS=s_RHoJhyNIe=k_o<3H`u(}tm?;9zrK(mxV;=FSERG=#wtc=4;&*Gox{|0xoN# -D3pyOa`_it?l_jBi`xo#rPxGP((`q!Y1vEGRY5@ECPm=Hs5Rc6ukBDT}nu1DvkvlyRmzS+)d}aI6jzI=nOm_>CHK?RpFRKCl4mmZm_VYbfM#E)A&6%l=l<8KzQsppaECTj -ZN8`4$S0(lJ+p!K46NN+r3S&e)=c{H_>19ti=P9uKHTA1}!px^FhQqVxZL#V3CA4K#i8Y1Y{Z8Jf;UX -}4>#_w6jiihf)1O!17~^ie2za}|yvdv}*<#u7L?@QRdW46yiJG2g-Sj&-O4WAmdHh+cShB`mS3x#wch -n{m+yOo=7t5&1YRtZrKm_(%&z{pOLa17x#tVoW_xPR6U)#O)HnBcHpn%{muLQH*Dd;-K#zO1AWILA^M -UEyD{K%x>EgD08p})>Pt^C1DV&1Rn5Z0x9k#3~Cx<^*3a6R-+*)giuz;iWpdrxK*2YuGbh~bf?z8uW} -(^cxZTFGg1Koz>|ipq);-b%%`n2dwf&4n2Q-<(t>jyH7%zxU^D6h3AO4eJxOV8YG4xU{uEec1FSYZNj -@Qtbn%L%qBx>5?RckzJIQmp3<9amrUy>I=rAI^`FvuP9qYx-Uy)h13U}9X@@2^6KLB#p%f#_l3om(ZS -!QSNQMagD<964}T|L<7K{@%NW#pf%yV}wUFhzy^JDLb*G2#P?GU(so^VnMFL-ZqAp|5@0U9g4=VIU47 -ie{bRB?tjWrhQ9P8uc_(~uZn&GmM=MeP^u2GpJ_-;BmoW9JmR@YC;z5X@5$C7R~mxtBqA}K7a7pBl__9FErx>{~xtM~L7 -L#mgdRjC?X12o=8ldn18+n0~1PDv6?U8_U4nn&`L%`A`Ykwh^s^F-~AM{xu8vYme09`Et2`51SLV`v@ -w)I9W+;0*?YXXdQ|rY)BzS@Gs4M=5VV<7RDZ_&Yd@+6Y8LbBdwF&V36L5kua_Xzlhoxii#@9*}%{QtY*|AyR|!~6c-Ob_2boaei+>bKy*cKYu``(CheU-Q?hK6VI)-{~f&El}-P{d9cF*CHO((jN&d)dj8+CMhVhyw@Cj# -VVu)&=f%U^b;HHY?`phYoz^<%6qLwg)hfwF7v|FK|KPXJR=G}m*;lRVFUM!EzQtGYzJ7Lbaq{MsbLsW -)ufadR{v%$epS}AMW6rBjMn{hx;g5V)>&x)r=y&F+%kR$KJnsdD7ia%)|H%mcH}-&^pY;#_L+I~w`s( -?~_x-&tjb1-oIm4a)`Ecg;D1k(U(96Ecd1xpH* ->JFQPHUID{2iFzsZYU=%9~Dw*CwKLOYb0ZO>xPLYH$2yL}(&c@<45v9JZn!6V^mVY*fm%Nn2 -Cyi~{4SLUxV0^&_~YLjZmpwee0e1;nOfeyqw@qZAwxGZtC-iulCDw=Hx4JS39jY4;fA48xj=cn3BS)C -P21xg&YGyZ5jmqN8HSfOCEX1k^VRS;h+jz&7I@!dh%bA`_TFY2F9uUssY|5fmNC6z-rH%%Vq5jM%WOX -i7j=6Ywy_>=Iz*`3j74)T_(dJ+YeX_8xShwmrGt#6wXPz1g8y;jw3V^+P}gIl3xfd!?oR8$Q5fyW9#17GX{~f -fh^<8>yY6pb&img}NW1JQCZgM~f4veiX~H0UHBAP>y3YpZUfu2jXz}MBv|QAbhp(ege$sE0lmzIe0gvY`0r6b^rGx=Uv8tPLu_^8 -a9innGsZrh_@1ek3b`S8P$sB8RXA!*}z2m&bj4MXM7#|J;xAt8wApMHT*duGPVxscIV;cr)@Y)$D9Qx -S0FOKO|>F9g&?tQy$Z=Z?e_p{2Kmf#XA-=v3ntBxCCP^zjQ?2?1q(N#~1Fb@2$e#L7maTYxXQ|VoIx- -Ohr_7{fZUo8cT2OgUcyrSaEYe)5z6wziA`?ksQtXa2LpvyPhsKNoZqgWw8X0#(AUgfhzhc|s%&l3Fq3 -s6e~1QY-O00;p4c~(<~Kx6#)F8}~@#{d8y0001RX>c!Jc4cm4Z*nhWX>)XJX<{#AVRT_)VRL0JaCz;0 -YjYdPk>Gd!inbJr251nPw;jBrZ)u4$t605G(s*WrqX3&gf$U+U8`Iq&3H$cHUq0&nXn>lr&yHJ!JtEL -mSyfqC`Krw9$Jx>0Q8sTDWqoy=ZTjWWAMuxi#|MwIXU%%smRHw(HhMPBzWD6(FOJ|pf5<*ui|p;Tzi# -U6eY5QEidJObHk*1;^kq{|q33%cvX?KOoxOg4Htp~Gtf&{+vp26lynFHI??1eG_a1+Qb_b6hJ$ifgHv -94q-(=-#U5S;bdjKnG1cjjB&i -~GJRFV!#CMR#p|t(vPVK!W<(=$`AQEAQ7uf31G#w))v$%QgQcCp%Shd|q}q*4h1BtZ_k;tQB3eY3HJo -E$7V&fU5TI`xXX&@kTzp6>SH-)YB``&${_F;AfJ3Uw4}|{wfxK;Z|pD+qAOdO7sQnL!o+(UdbOO*{ib -;r_WD6oX);|@$&5T>8rCz_QSuQoxPle4-T^U=jGXVr_cT~>@b$2Z~C&*Bd?3L6SG$QvMF2I*-17pU=f -Six^32??YEPR+tkHM%z#E_a@o=GTTv`#;vQjllkBdA%{O14z5mNQcrIG{^9Ov`jt>qFX0xKIfO1Z<^B -lLNEiX5?9r+~7{|wafqV9TVAr|gKd;C1nCd<#Ru3ps3W}vJ6=q*rC{t@PcgeF>;lBBpP?CwkYZ8T$4% -wXzgKfZnQ?!$YHVrZYQw+jGxBv(ECxq|@@p1pbX>h$&VXiK2672s7hJ$U}&{Re9I9vY4E7q8!cIDPr@ ->|GwEj{M!(vp4Ua=k(YA{{HO!hqLFozc=vc{hROKJv)03?fgkY&-ed+`VP8{dGHsnzkM@1`|$(LaY|D -x0fR8JgLzeST{e4$6f$Zqe-`t8d`x2@E5LtGvyPYqQ&F}m)@#6Ch8XLJ!x(@W9@j4Gn`~k2K<9v4h$Z -lrvM&4CY}AQrDTlM$;O7OjD2uA>1PqduNDTE{Opyx$-z&SUZrT;l$2Mzfku94x6EMHb-}-43IF38T6# -zp1_FwMLxQ58jSE2^42d5vNeGA-A9O&iDD^avVYk7tZvi@4)Z~_qf9+6n~h^(^jvXYmZ!7_WXW71O|a -2(D%{$yb4#bQAdpU{parhmFvWQDV(IBUHeGuva}E5(Xh9+SB8`@nGVwUtY1%gC=A;Lo*L)v-hS`sZ`q -<0C8#zl7}wU4U&^Yq6>N5p}r{zyi-dj;BD%qF&ICV|^BU3W}eKcVZ2a1-L7m0o=~4S-M2eB#ZnD2o&1 -T%0~TmUCxPWL7LIr$J62CF`=F(uYt-Z0CQe#Km?1cs}tJE6u|-4?wx~^k)+^hOtOZABh7p03wxD!TD3k2As5bUViqD@~F*N^?QmjVdL?@A9$DZimr#l5mM=ZAleY%C -C>+{|~QZUrGL@@iOpIdm^mZDnB94a50+<#(oTMK~G^b`)&6u^(@mn5gK!931>5Lqn%p};9%cJmuNBX= -Ud6)XoCS*-zbxy`J)WXoth#I@rdLyb@LXJ$Mg8>q%T3*HU}szS)^ -}q>Fen#bSlN)atE#MT;FMpNpkRPX0w=w}mf5nb1o9Nx7*XGrZBwJhJ+-h66F))zX>9>=96tL=A-62+1 -u3g~y;J^a2AyZveni5<=>+JK{h~~TC>P8zK#Fi4;5gio1{vjf02ldko58&4k7?P>5K>WsVzKWaC?o+B -WI>JEsa}{n7AbOd-=bimJ|TF2y3~9eG_z;jP7S_A9fC~GS^|o&R}>jE@)T5v?yiI#u{G^+d=M~dyApW -73`vJoXt0s?w*(`fhZ>WSPs+ah7=?jbr0iY24#aJ^xHuBi>SUyXQ1)1sRJ>lDRmG#;al!C!VG7BHcF5F^AMXt -5{U7t9Y5wI@02I(Zy1Y%fn2;&O8}%E{4`J?X&Oc84|%iMTZkR*(DZqDK)6Kw#BzT>$2#lvuI|EPX~Bvrc2yMB^LmK_IOsgaY)TM}^f%pq^Bg%hrN -Bswzdc9DtXdvOUPanq*P`R)Sn?1Rw#h#p<_8b0L5E28J)_6%Il;(M|C6dJ9T`z1TzoK-A;kh-!fWTsI -3%tT-z?l`y>)MZsuZbrW(!fd%@u&oHOyA5t72}^8L@k4Jj2GUM#72UvxS~4AT4-me00zXCoLaai -X~2-oJjbHL@=57_)WR7>&@yCxQyB4W4&VPag2(hSv5Ln&;eC1igw}pZV3M6RxStq^)WJ|49+9RNw=~1{x5 -H2HoGdF>r+CHsWFvYaIkR5_DTZFvog;qPrI8a7QF5bgsZIF4sfIf)Dq^H7`Wn=`|CU>7kEFjVT|QY*c -s0+Do$~tXmDRzv^e7IO$p?@0&aUBiO}B24rvemNeSo0V{py&SCyI&VHZSDz%YYXl3thp1y6H+j2p^>W -pr#U?D7o(EZaTWSLyMSu3%mNIGAGDwvuk``eg%p|*gG*ONwpmV#|Dp)wRBIB-4mznmzshL -YUXk6#;9x-X?;LP=yFrfWt(cNsGV9b%NAAz<~cAM*3rmkTQ}LNX$1=_TN2Sak;{tvHE;>BP>$Sdy5Z; -)jEf6&Nq{(mpLjfU-3A0dOyrY6iViRhh*85v@h4Y#Lf(=n_o|vZVY|=%?x#ZCgxrX2cEXd0Q;?o}aPy -%)uD$wcXA9RW&3pT3#iymG`6sa?)B@QAUj -u({pfM>2u51BejGhVUOmv^g2}oVlh-o-Zg{8HYx+R>dvgfOeOF?%b48Q6HUpMj+yI*(sL3Nl5=lpAED -7%=?k+SP>fS&IEl;S`awi=`wQXS5?imDZp!f0ypZb(XSz+^cmbX#igpmrqVB47@nAfXCR8Z_T#;#UF+ -^Ll&s_UOy6z6QRrm)<$3NZIrO1I(|gwU#U#x&!hZFmyMou<2-wkja*y -FwI8Jp`WM_x3T%m-vH~yT{Ro>;Ue-VYr988ZwQWz@3H%?1-qVc|Owbr#J~s_DK#!HuZNfF2jsg!0@aG -ju!w+q7TcRf)-TVUd^e?~4?h5;^7MahY5nY}49=z+~RxmWFPWJL1G@DMR0ncnG&i98u`x0`gi -MNinsb-znBCBCc7DNs4xtcjV7^12Bf3Jc&4ED?}Bx$vcoCK|@6sM#|@TqL+?<1j6;n4x306rIE#iixW -RFBNd0ArUUIM}|F%F$FS{Ao0{-C`H?s^G#K>8h9Yb^b-z*P~UJzI#IOqYklE`5fFZ7mNg(tO-KZbY&8 -$^4v-{d3MGe=QmtcadpIL)6(^D5Z$78fOTzoQ@F*Z7YR^@p@Ip!Jmno?iGz6kZpE!BiSm%3ygB0;{OR -#s?ZDx?$Xv9z|h7N;5GHxC?9407`$c1FZ47Kde!^^CDMH*)%wYn>|r1-5iUC&~PF17WA34A4@$U7op` -6phjOFtye0Ms9t!e-8~!)mhh$K|F3OQkP?#V-U#NQrvB)v;sty50*4U_;L>`M7bMo4UE9V6RKT#9A=G -BLtXP07vLMB`~=2JiV+~_683WlRWljhZ>yd>}WRm7?9jFl^|cLqZ=yh;!(c1&@q)@it5WU -W!Tf6J*}H4ng|$n}SLZjymGHEO%Pnf*tvyfNNmSBZEdyhq3@XM`y}C&Jmzw0m0wsN3qM$G%(s59hTS; -9PUce-}IZ!7%mrHZxZK@rJJh84I)t_zyX<(~|Q^%`FEy(({LinT^j?W)6`NDZRf>nsRD^U_;6xZFBU1 -K@zsK3&yt?_(Af!bBgoE&YQW=_u*<(23HKLI@w_kCM*@)7_37~>pEjqRyD<9wrQ&_h%~V#-*HSUJ^D8lu%t;&3ReI!&0b4WkXYgNLM5-SWe)`4}NW*Gknz;tAnvDepCvF*&A3o3= -$a;s4%fIYhg29k(!Qa*3nF4JT_aVU12T8h%?hnEluO0|Nvt&u)<05{eI%{k#Jb -m&rCaI^*Vc(9Sd38L3pc6@VWU -RLhmSwxVsWj#;~P|9h2mD@ByF~vS -`8l1rxTFZ?>p!13hTAzk2q-4~r;g$DO%JgUx1TzK-PMl7W&5|YTbm=23LPo7 -cuL*)YY8YDEjJY?UsX3I6IDYE^cQ%>s*BYKwM+tJ^C@!0QEOmGA^$SEs+z-u>;l#$Q=zHvtU*_>9vS^ -8&s28AZV{YM#W5~TXUCAzald0dd=1JLJ8JuDg$FZF<6^NfI$x-^z^awnMmesQ3&B=mO~II=SR`Gwl5K)WuQC2DpEsL|qj#vAlCn=)K=-CoIY?w<3`c4?*u -#(1KjHT&$nz37u75R=L=bx^4m?Q8Ere<-V!+o<53F-1uPTh`f*|=hgpmpeadk7uTlHsiQEX%*PnZZd}x5LR~%B7Q`@Y;Y@-m3JSg6}~*_B4q?QMXk_5B2WTHp^vs -j~YyN{-pbe`is}z9H{UzPIt}&W$}O79)?V+M^Cyx4QWUd3DFMsn}Oz_tV*(MQyg0e&9NQ*v1HA+Bh42 -TNrDpaYWg*v0mfz;2L?{F59B2Pjssf)4{hVh{iu<=8#Chp(bGcqFMXv6Sc;LL8f>;EqUu_r!dm!lae= -6VS&SIP%C$v5c{IwK9OVchh3JN6%h5N@h*8E!V$H54tU6hk80u%)UAn9JEl@+l1Bxl%!w2*kkOf|)U` -zI{-WAH|QmO)ZVv$tPFhdn+(85AU22mJGWPVXbg!7}lBIVgj<|>aOL7Qq4bj2zO#&i1_50Fd8Bjs9BY -56jqhy%2J4;aqbTp{jq8%<=+AL{T!7uOGUBc0YeyhzeyW?vHTUs_5#Rwtv7?zMH6FP-vBOs6UJOjA?~ -ADQmxIrwVn14N8CA%%9vBJZKRI*~U3&{&6LV%#RimFPI}i!kuST9>*3XXl7W+1ES+*d(5SrXr01?5mL -M=co=$z+^nqrDw>~(K9&^iVcz))Sl!)kTK;`O?=v0={lj3NsAPvE}z2c-MbupIwu*EGL!xg9e;-|>1) -YJnkBo&xic$`&czM2Dypld1$l(wyh}4Xb>|XZ>KH#N(H8r74$UC&h86@Md71-O>f0(TwLVcb@6$AaghYZO}_ZzD{LgcefdZJ2LHilUw@;Xt -rlO)c;#VmUw{4uM)Y!=m||x@m)12GA3A=NK40bzUY^q(A(HAaq~NKHVimt)rnbO{lvP=?<#s7FiYM8t -=U?xD$GO1_jz~}uT%dGxOb&{8dr4wbjscR>!4oU)!El+EJ!X;*rlPYV3e;-oiq7loPhbHJJF -iHH*tM!z`u`(FG_#B<)zonxRA~iUHQdQ%r3wAO4L|RXT&YW(-w(&J-P(c0~|rm@}irUWhAWnN%y293x -Vf4k>eQcCa&2eS-q#`aE$?rd=8O|A0{NGk%QXd%pMSer7t5JQ1M@|!OurV3viWhy5RUMnH`4(RxVK0K -)s+>H+X!^K}o<^vO~qNFAKfQ9MfkzF!aMSWIeNoHf{k+7dQ%s-MOk6AD`APrD}W74k%@J;eil7tiOEb -{^Ei_KY+WIG?ary-N9c5?U{X_tDY`H(J2X;H$!`8Q`yjBUQUOGJL<|1mT8Jw9Z2HBQN+SENmVq!QfAm -D&?yfn=DjmOm{B%*N2fz$S1?2e?sZZi2OAhp_vXk8pk()cmR0eIkck=8fCCz#&^25|)%PO-V4W1r%+S -aj9MSLtkK}x@=B;_gQb~r_AhvDdE>kJ+>T#1Aeb$IG|->heqxD^%FmCGF-pqh(&Kt?xOdp-}^v1Tm*6 -u@aObK0F{Bz%ZB1*cgoMn>jQ#XK)@+2fb&2}k#C1!AKcjd!~YwZAR8m*{zZf^nbl2fN~n~IAw<~XI96mw8}Luhoqo0jD}2eX5q8nGLqq(VKVF3K}bIgcn{-W9i6FIB;El!m -IL?MT&YGt~I&7!vi?mGws4M6x03#G5^9>3Mi&9+TTDqj(nSwB@h_RKjq-tWqtDGSu=PqZ&HX-ObroiM -JL(@7=e!>_`)Fo`?N$>)Ymzf(eILiJw*3kx37`^;%VdE2(c$Ux`r#`~`rGC)tWc&TvU9F-K^?%1*{eQBBzh1S~6Lfc!rO7TTaLT<(^HqLJ&;!+gm4`0aRo=NMWoW{Xes -J#asq~SwE==XY2a!n`atvN{zk41|=Q2 -7p-&CXfB6?b*T#$GXUKJlDBx^f@QaMsS5r60~Ji^NJ?s7q^C(NPw=q7tfjVD@6jDU$n4ZUot$`^ncNz -c7i2~>h$Lske~7JdMMS7fk4bw5CS9HyD=)2(eYE#dSx3lHX1XPv=0Tb1<_N2-1hgT*_))1nRuKw}TiM -uUKQ1mt#^#dxo3xv#qy!qgp|I`TY7!xc=wx}PNrseeydYbgp1#YuF>yH(LlNt(z^Js7y4}v&O+8rNmV -`{_V-A6;OFnbW+hN54TsC?wwf}dToz}*LBrgSa^Pth46xRL0YZlxrTt_cz3%BwfKwb1;>1%aU#9nFm< -&I^d%`8$;(&|>hb+<^=*mW5xuwXCKjLd`$`GYlh{A~)G0V0Bmp7DbubZu_9bC4FADpng8YoT>4kqwUu_0rE_r3U4brmczAWH5#vpqdT6Om=i?$E~+>EOP_ -@Hp${|kF!NX*5i89v&o5voe!dRQXGBxhi{_2DVTfn~0cI^ -Mte!F#=U%RtaBtrN4%eD0CMQeA`q%0Ce@*|XwLV_N+aE^a`;FA7RMLS*N8^nOVRGH9G2^YYGHsybsyO -G6g85`Q_cDhpL5HT-4HIfVUh&ZPq2~SPi0iI%JfPGf|rC3j6D+*TQw9j*53lU>NH?j&Iqu1df!txopD -cF;!ujD)JXzY|yr)%+AJlrni%TZ*~+n}0b(IpuB7JZEGw6Gn?LZ;hHtk(T@tZW7YtL$V@+B7>ufvYRl -(v_ur%H7m;yOiN{)ydLEgOe+e*kqSE;uP1HBgyBu=sYXth)`Lu0;9>nHd@rV3}n~fMg&3BtbY>1$;sP -d8G;^V5(1WgBINNzWjRyz4h@Kpt*Sm(%qen~evC^mgBI6+y1%UYM2VtUczD3H&JxwpymRsppI*u#7xT -lZQtzbY>ILo@hwV*DU8uxoWf7+9>@!rw#qe3X@(W5ha*lAF7>8+LCM5}24x^|Ti86LA@N6KzV^NlL^C -nhBCuAb&*@71tnO0}WBWaW5Cpi+$u9o<50#CTH%qIXKlkD>#tH8`!v2|uPd&Fz7-NBh2;x -dT9pX{FFQM``{ITqC;DYT!ffiEYIu?^jC4XmLCQ@H3)9>)%Ca|^`Wa>TbD90y5<_OAKQY=hmR(>s$6OHWN#&M9;{=Q~O;xTcg<*WTVDC-=1tlokYyRGNfx=o4cS -WS`l9-SdTZo0~iMMKj;%KxT&<9b0r}Cs96g-v+O>oK*?Gv#0aJ-M`AoSU(UJ25zHkBbb}i{g10 -7~$9p_0l^38Y`ylyH-sXNOC-@)nb_T+R+HkC*DPqi#O->`^?Y2ry^YbApgs0H6hqb7opE+x+^wyuxsNYi?KYS8ODK2Un!47ez3TnF -oW@=0MIP>R!OF}6`GaKSO)C>z(A2CH3!xmdf24QQzJoAp4ngw;v8(Y67ABCSI;9KJ_U;fmQC+jpEm8)Q_zt`i{A97s0U*rQ1zILJWf?+I{{H&5O3aNm2efQS-(%Z)$U -9ylZougWjY@!;flbi7xz9e;Yyo9d>1uQ3F_t23fm$4UX!A&;$8|fpNR{e-U48`MhF}YadGkXKfXJ{w| ->BKl$J!79ZD~X)`FyJ4YZl`MpjN32ZIm`2>_h5K2b4Ty5T@qgt@mPAOVGOy{5)*yct1FyOTPqTkEAZ$HkH`LGmN3tDRUr& -xL@PZs$mp(W~})bwyS=$6@T@4oV^x00T?~Mmtu)E@o^!3r1-Cyvd>@l%?YAqx|lV&L3(18trTHJ56D& -0c_$iMQ7cGQ5DV@ysEVOo<*J(z6tB!Fel-8H-x{qdr=lVub9@alf9*@KMISdAa -owu*$_|E*3Fp-lRO|%P*dv1wYFD5@sUVt@dzq(qrY~qzvGwb5lOoVmLpK)tV=OUa76`YjyccK#nt5eN -eS9>*fVj_22KkhzKZtj}$M-9JJsDGPg$<$YqBNWRKJ#-)TF;@FBFSCNFCo&t)QZOAZ%f(uue -&S=;T!n|*4zCHJncloA4eN#X3jDTME; -kT>!Gj|FmDg^2_X~;t;WM|4C~&{C8CJ|9?@@@re8&U_=we5pB-SQY&`KOFYqKlF -i_M4~mSILn#OPU=%ZchAig&qnSOTdRRnrB3AUb%GZl*%ilYB&se8BQuiD~c(z)69YZjx&wd2)ZqrhF& -MX1q0xfHdxr*eAQi`e=MXh(MZr?}_FrHqEdtE5cU66?{k48Y@#dBZu+yefj_6NW1vD1;5)_H!&QH)sV -c0$cVA}Ve7VM6Nf<37ZXZ46`CS<+Bb7$SFBs$$8b>C;F2o?3pD7P;Kyi;ONsf5&Bfo%%xwNCnjqvto> -$ioWq)2_`e}Mz_#oSRYAw(aZaHipWcpXpw`iADjuOK1qJ55kYsBcc@q|`JHZUv$^7UnI#y>``|%ZIRj -IXQLmcOP9*mLQ7BK{IPkwYJsdLT!iIE7wzJSgD#Bo*!&n9bTz?&h*5L<6m$kwW9u|IJvH9e!>|W6aCV -)#}fC69-)i;Wty0}2$hec><#=6j}obCY4^=(mi82?K%=ZY$@kjf1knr3e_-3)uzGK6KY-EfMR3klcstAk-8#_m65C`>Kxh|$kDB<3U@#dFY93%6NhedYzM`c0bOwZ-~6{tni -6DrDAu_hxp5U9?u>yq7`pV=DA+I&knGJiad1oC{{k4i;!>7mJ^ZIX0c4SOcy{X8NjFGrMwM{h*6kAx{ -I_KyGJMT#5?q0$FB}{o~Mpp392pWhVm#wR=OErDAbKb}oTYp+grJ^c8^8t5<^;F{%3BExEM14M-F$HL -l`^#?$Py>Y54hq7}wHh~=`Jm-Hg8wm?WlHPLcZzzZO0isX4ayr*q(f7>2zlWnmWz -O4=4tKjyKHn(1gf2@U?k^Mq4Bu7cM5za8N*Z0K$VJ6Ss$xul_661s??_c`z_3H=$~k`TxmZ>tPQAaA^gJNeu -tkhg6~@qPUMd9bocbl8Y$Ha(VK0{+OcpJu)^Q#WJMR?74dh;Vc794t!n&SPr;R_dLWXp=E;a~l|%4-y -_CO}$`nB#}U8-08pz)B#oaQ=T0%y0YI>(GFUvS44etsWBsuu|m2+f&}n+(2?FhuKKH7?4Rm9IxlhG(< -_;w+xh3mAH&y|=-f%}V9`O}%odHJEBGIpmxx&~MQggNIYCR@B?wtEuE?Ih`Jnb*GYGN(q>iC=D{}4}E -^StU{+D1da>QUTFaR+A7`jG6Po1NcX-K5g6|B-}C9}^+gOBNRtZTy1YxjLr#2l8#=t9TwWjG+r180A6 -aa5Hz0^R&=;f~K8gS8-ULGMd~x!6rD%>q9v<@M2`WOO>3^WT1|AG!H2Otx@@y2N>!0<=}EL=RT=#(rb -8Uq2&@NBJ1swZx8(eyYv|9Rc?aBXZtxyy%S>j;s%E{LtFi$sggB@X?<#`uChQFZ}!CPneW8C`4=qY2N -u5ig)!%dmy5-`Kk7~uo+|=gariHrhUMzv40$AfAaGa_1KChyF~T3fL-VgjOLT<(Q~WAqjdYrt9es3oN -c_IX2CWSKP~HQm~0$$A(AwXB0*+W=67OV|K&XLezthr8fJf0&at8q9Of7XAn8?e2ta&2%#dFxx7_l~BeB&5}9EY@}qi1cNH_=XsP5_n{@e%d}rmEZ2oc$?h0)Nem4g0A1 -O_3qEdD-$S|6G%v-T|9Q^v$>PYFDXt+wRI<6*m~e&|Q=IoXwl{R-s&TGq+NC5!%mGl3|kFK8O(#obww -hC30?M=mJJTY@rnL(+1J6cgL;-L?Ena)!YhQ45!_=c}P9h9-tOM(6K#XoW`iI=Q)PyBzfg+A}&b{rr7 -dL+{L>94xm=<%LR&7QgC8{r$6xf*DJB4L(e-gD#_{7WB$&VY2Bj&^$D{U@U`%SAzj|b-E<@cVxnCVeM -NeQ@nh}s`3Q`&cFcThg0JeS5d@qMWtZv2ds!%sMequ=8@|3`isp;WwBR{+BJmeWK{v0^rTMtSN3RH_z -NcFyt^_AGMPae6(Rhy+_rOVg7kQ(%WPEY8-_`IxQ6F)YY)@hK)a$fjdw^?O0WX=+e5qq+VlJf#mK%rm -D=OI)abs&rWTQ9c&9-h9X&gJSBve+7bqo#t+W8M~ltfFkKGY~S_4#>jvh;#g$ick`@Lu?-=z#H>(VHG -;{AI-d@MbY8Pl9WOvFm(ACQjZIbmmf789Y{qBMQG8>EJ=41L??Dw=6R7>~d393(E+h`}`84K!Hf_m|; -2jyPn!&XY{a;4_vOS;X1^=R=uYZwx`_kAvZ~+9Jw51GkVbwE~6n&ZlfoUd}bp5pHJKoB!(Pds-NwG;7 -*q7!9%gW!m2?Q<8jhgA>A(ASz+3&0)sfzq1xI1VPsLFhWRlrp|BzCYS4WlZf9YvG1piB#m)osqG)4rS -RB(vuMQx>oys538xu{-`M%bvVt-+zCF1M3z5t~b_ecEN%cWAdC}G>bHHwgcj-#1V-9Ft_QPh(*JCcRk -`NhX|wcVXm7%UAxGKU(TNrywU=w=dKti=b!4-I7t%Em^l_}SNPClyBKlb&qfM&Y9|FcWz{Ix4)NqpNp{N_N~+6z1L(aKU|L;?nnIaULYXBzIkUS%?M9LclI2FC7K -e_=-%H$^mdD=RGA638E(OKjsPXw-xoE@P30W1DUzj4Lm6Eu!W{}cUede1;7zuS4=a=K4LI5- -2a;bP7jq!7h42;n4c3E2|ND9TpKi~kVq3~M$zt7Kabbe5`L2HZ2Ln3&`(TUxx{Dz0e)(}u}h@YRejO& -O`DLg^cUTzRT9X>(WSR+WKLEDBPT@-m*^5NkPXjWI97ua-)F?g`kB_+3DRn=y}F*?N!y&;WhhpFLb%) -cvGAwgq90-rh%yUl8au_`AS4@hXs<*yjj{1#tbdEHd1prU%j7+fvvBtkuF`AnC+fq&FbR=pZavxEm`C -h9Lt{7DUOJ>0|XQR000O8`*~JVGU-Un))W8$15p3~8~^|SaA|NaUv_0~WN&gWWNCAB -Y-wUIX>Md?crI{x?Ob_}+%}f~zdr@nUPG#+T0XK9%=TlFk&o}Qm54D^p;fkObctUZCFM>Qnq -a+=QoMNn?CRy4t4M#;A}&(#?1z_cUqAojySG2QenSt@?kw9?rP5*>*V{Z>xu1Et*?~{Obp{{4#CUQ@| -$!ZM(t{5OVWM3!cu8Os56n^*i?W;e=b(TDB^!7XOB$sL~Ud#V&WUc3dUcZX#`nFW*_Zu~5Tt-);@+wx -foQqYH<>{iVWIlv|3Twmt94ahJbYN~t1K$jhHrwl-RrBTFTSUC=J8pQgDmkXPOjsPd=BOL7? -!IaWuj+>;rH&{BSJf -YKh4JLTVisiHy$L1{aWOydy9d;DZ$JUcV6Kn#p1$BJq4JiV|vH$s}86GBq?)yEWz?aY1DDq|!8)*3l$ -dgnNWB!3z2xaC)-tweDi$dzWOy-c8-qQads-6p|xZ2M#v*L{zigh@2!*^ -h^we3T=a4)*ye)ww!VK!ui2w4wW*B6}4Fr*rij{Q++qJ(C59&ZX5My-sNo6PHVr -%9sC?HAq8`X8bz`P>KOgGq6n?uhR+Y(d_Ub1e}><-I1O65(_4sDgoB^AQ0TkHO9XJJ(M{4D0?<*F)sf`E#j3huXhXSChwZy|qB_Oyn;SnU8 -TrZM1&%^gn7|cs>vxdNe4`kfZveB1B^Z~PY_(ET)6!)D?_Jzt3GMD1fG^Pxsli>6eqicl;ay@!$z041 -9JP$Lxh|8X3fe~Ji=~M}(+inM~_)t{&b*>SXIWdR*YS-!UFvVp`nc31@z=5uyQZdi5{+w23ty}x -Jx|`9gnd!2B<2h>A7+Rqj3#Z_NkkRL{HHZtN~VZc;N)x1El$X}22bQr(7mTt#x1MH103PhGipp0e@uR -#Z06ER%7R!m=l8~C#+^%$&TW#Fgxj6w0^arY5L_c)#xA?3boRw*j=BX$sI?$i!Yh`qr-2pe=^%pk#IW -cBlSTXQcqCHoqBzB@v+%-hMs%=xRL}dg-W^0tsiJPxPqr9#)KxuNWtQRmeI6E#7q{Cb(%zne< -nt5YNOPWocv9e!a6#T<4DxKxe!mq1qbC9%kS;8WgEc~nuvn|7W$UU7_x~4apSO?CTUvVE{bNi0>b2|o -C|BwMjpC40><&i4{clbMG-Wy7p>|F9!}yWk=YF><{8OWq`?~sL3UZq$e3_@G@>#I+9w)-mcbl!!s&SL -$OyOZHS+dE`)}U|7v#7p1fpb$qsx8ZIa9`qLJ~qk7RdbN!afbtC__37Oy=VKhnd&O^27E*ij}r~?BBy -EFthY$qrgGHk>R^Yh(d!_{p}>vc!)WOfN<3uGJ+;e7pf@=t6naThh2xAhp1T(VlM=xP^rWdKwV2Uo)9 -#=(JUm(8C(QEaY>p%ON5kb@qjj?#_Y@YvuFs+G&kUER}9Wb6Jj<{xy|xiAf>0#@;zn3S#@wqz#A6Gsk -;geI8-&-fd`&GwIhrSKP`cnSp+4nkbjtHi%~Po)8+JBxZP&R$=|AGVBc%f4@QDhNCFb}L-EX}w&Un{9 -8)tiBt)cY@DWty3TBZa!%+>u8j>PIMOgP=n$iaVt(r|3Jg8j~7+N)(c?R4dpX6mN{lJw1-a6Y9y(k@7 -F4$m!;3XGlsFCvkM>3dex#u|GSS8`LnLZ@TOvh9O@xy%@iLEhXF;-%Sn~^gm>8!+7W9Y51y9V`ithY> -&>7B8;w5)Jg)OEF0F|3kv1O`xWIM*^)rLr5CRN|XxC!S@&Xk^FSj&xRWI34%~ic? -8}lPy;@WB4~lggPBYg2zM}Y!d&(#-Wpin!`YxD-CMIXRaFHhouxX*^%Jz;zRzFj}9W{te!D|sl>ofiVPm~Jh*XOL@T*RlPh`o -U;2yQn?$EFe}n2F^;lY?%GkzMJqyr8MyoA#7G+I$6p#a>HWr*}q%mN};6U@u0|+GE&SXQq4PO<04cLN -^sV1}^e(6k-i@JFf%ec3=m2(Ydn|<$8>aartdvGJ>*Pn&KQ)O4#arCMc=Y@_@CuzKzv}-2eaeUob{-BYz$%#x4{!@9;?XQn7Qe(4zzjoGjd#Hmrsm3PYN#Ipv!Nul>t+&NGrHSRv!u(q}V-7t6h^sH&}auV}qS2CPT4i`WCAY^~yP(3_t5v0vbd -xl3P;@?K=Ooz&!1h&iVVz4JQHd)d_k_b-0G9X-RVylvI4aN$kvnq%iXsyAsE3QB_wlHi+24XVLCQ+mA -v_0W6sHx6JhI?=8fOu3k3QB&Om3qfWV)`jbCCAS8bgSjRh;pW95)EG`jN$>K>d(PDAp8rPRi_eWP`c| -8zLF$~mT*%O%j%=)^mrB2SY||2X_HX=Ln|W$elBF>p)|Y@jS?(aR1mOSuIR@C=1}H-d};}4HQ7+&^Z~ -@!ZBzycQ%Ey9i@&`k$nmAiNPLUIc@7VEy-*%wVB0+7wrukB2sEe=NaATmv3f}x18&LkAPeBW)PgvIW? -w~5CW!nMJHcPZT7fNty6hhbTCM9Y_dm^T5QH`DaB>CBwtw6G)7KjenP%2V`0K>NbBMoxIV64of}{rTn7C(T;{bLLm$Q*UC8`11{>o*quaUD4HKx9*|@Xrqe(4HCUPmv -4l94{DE-|c%{FF*#cq;}@Gxw9&7!URXqxy^+Qv_Q_w~4|d45;#G-@hVib0y~R)Q`>zz~dG9u$%Sh)tJ -C$(x~S+x+pck+b`F>28N$_Fez>e$b|44;pnphH1ag49fof-(K$Tt=R)5Dl@zf12Scpr@^t3+`JRw?89 -J2u(tIj-f4$>e^@(Mm}qrKL0W9x8VB#kTC}116y9#s*%2joW6t_RnfB@H@6TelS|Jte6frk9qRL5tomb4btkZizxbFDfl`Fs -24XZmxlFQy`$|`lT7$ma^@MLNfnM2kq0{B%DwOk4qcH$cR)IWRSXvqY!g_Pj*G#afkyEOAqHcFo+kwt -#uqyM_5uy_qU&l}wEcMVy0L+FAA8G5DkNZ}#vcZOW1djFi?e6 -oC%uHB!=dZ7K5hvO!k=RXshnaqhzu4|T3@i}f)T>4`*|$Z`TPZGgVeDl=`n^!3XI*l$?-+i?rOo0O`* -?*pKDALo(i+4YB1oheFeIc^>1s?C$B*d+S*-SCrsR*Z7sI5BG!VmL;nmm(BG8 -7myaMG1GH>d{<0J~0L~RDJsT#2qUW_C!u+n$q&N7%ojTDhKZn2$?L}r1W-Gf{q_3y)Db@W0uENl=&>{ -jVv-rX`9*c&o;;n+XXOo<7h!A?>en~S*jiF(R9UF+PKM^KDsV#I!SHHW`k+lD22E&-**9WX*T;4PCMj -pHt)f&FpFg5;3XJ-C0pcda??VIi#@N~%{<#l2E@_@Xg|`XlsvmO`Ps#~DUu61*aGgWOx{Q&TzF(Q{ue -!4KodWW73~FGshAG+*#k~oSEmg&d0yVuRv28iZYERKlnw}_4ey3=gmC)H3^2AcwkMl9I%%eKsRr%Bk} -221<;Tiwe&uA~w1bu2>WVi+i0A`3W_l#W?rg?Q7LzV;%K4|Ow=T{fZ}@d;I&d4^`l>HX!i&~9j_3Gvf -@UJYXU5;L6)B{WAeB9tThr&_aPc;xxkb`+vFk4o^QKgo?K@-V!((eH%O%z7g5`2%PoeOr@D~==P<8`~ -x~Xh8I`Wy2n&-xp>JBt^igN=cKImPn7EgwU=H2+TCtdx3>}BZywG#2%#kh7&TU2W2V?$f16d}dYb0d@ -4#8FEp(zfhtZceC^TghH>Ni{e`Bzzh9_s~|8=eFByP><}B{>^}HwuT~Chy9{JNXMD_vY9-My -X9?Fysds+MTB%XHVCV{Os?zf@01Mv{_%f4nJ829yYt5rWwe;$p-0m&i)J_Z5_905t*e|v{2RT!M-$s3 -JAI7S;*b?OwC&#CduCz0FfcxhNuv;`voMo@yZ50(dfu^Q&I6ADA36`wqu;Wf3;Qj^g9nvKkGR$0qsr5 -#kPQ_O#Fv)>>@-774+CYkyWWm8YHj0i$*%{=wr~qdN8q)(0Q|-(WJkP9OoqsxDk?j*CRSd+h|$Kr`!+ -yxW@S^F80B_b=F;vf0IJ5|_^yPN^S`0$=Au72-rUao##2=)u%-V-Eyvk22lH;y9FvJZOW>T -~;%092wAwo;I*JDV<}sKB(u+M0*&hb*j$jp9*MrIVKwFW+(Vemj>H1j`NspM%1(UCLzMFLmN9K@68?t -K5enh`iv#{9CO5)m6UU_On3DJx2!S_uram?&s?b+kO;+-7HPV*nQ#J0YWvzYolVDT_Ec@%-%affCJzt -a_La-^Z4Zh~b(xKT`d|Dw0ifT!17<$oLtmotzUC8t;UJ9iV$&evK!l{-sl#AN!EFlm7Z#>flT?fRSgTVYo<7?f -9h)!bm$cuRU8GaWRv6%p -E2@UmvSvYmGW0Df*@se1M8|sfP8(ktJ$C4sEv$;Dx*j$z+7y!U+Qo(gP|){1Z@10|XQR000O8`*~JV-AQjCA~OI0{mK9U -9{>OVaA|NaUv_0~WN&gWWNCABY-wUIY;R*>bZ>HVE^vA6eQS5yMwZ}r{R%`HFTjL?E#)!Y>?j$x<8(Y -Hop@}gCwmk(1ri{I76~u_D4A*Jzwdojp{h^-DLcKBJ!f$eiv+7~y>8ui-8u -gVM<+)o!IN^ctMcV_6GTr&!TF=J^Hcce)8KD;k?ew($v^T<48^|9=UGu_Y0#8GbDagxcg=NK1TV`)bC -*j -)oyYWz$34k>ncldo{q9N^K8=u&*=Trsw%5V5S#>?Dp{_RNl=t9z}u`cE2rD_dN9pKrMOO7x{pQuH=P$A}ud;db4J_XZ7rYMsoYa}~dK@&_n`YYV0J0y}iwrtaH) -8@y^E?iIc=0WL8p%umW+gKKZcZ4X7EIRI`t}^gJr35%O*T#8wbWl{O@c5~{lCihvHtX|xJ_1hO07yIf -Va|)ap&(Ev!&CUy|D62K}{}-;SrcJ7=L13q97zTFm2k8C=-)8CGxRg2cyX!2|WB+gbG7j> -3$_SayR@u=J?lM^Q3$@*1fc`DLi>5q)>FDU_o5wG|dHQ0Cf1qfU5l+J_MOFCs=%>4nMi$C-%gQ=JpJy~^ws}<{?vvMA{ -sutZq}=}$p#i{PPFRbo9Mb3eYsvi(_cOP`tc9nzM4M!?yILiLgnzfX*Tub;ls^tlgDMXqz~cIQJO7iD -wEZUa7>{djk?BddhuOZWFw|4$H&J} -F`0h+=sc>k)dKhE&*NaR8KV#a)Ms5U$1=!z)Gj!P1~_juVOAYPc`+|5T-4QWq!4JmT{Zb;mAR8u8&IO -U1U_BPk{JxM2AmI;C2TSr$`3D|!E6UVz=#)l1)GIy$GUcr-7)IpcF=FJ>P@!8^^U@ov<$Q-+*WyLj6i -mj6qTM$Iuu(sA|)=N!s||{8Hf%y`YnK!YHF|ciX--(Gq*I*lct7tl?V|r$dTEKDi#YOT>vc%?J?;7i% -b&1`XU3EF1A1v;bk-$2QFv367|))zBH@hxy`T_r<@vFU}Q(q|H%QQ8_~qq(cmeUU5mp>=l=;gHzb*z{`=2Bn`jL -EDqw3!@As7upGczK^(%Lp+iv^H%S#Qf5GiRA7{UutIzZo#CiU%%FKQVdHrU<0;*jHUbb9P!R$>&fA6k -WQUQeY)iwpe%&oFX7vWfD^~7L|O#-6>-bn%gi0F6M`TRN{d8ZC`JIY6sswQv24zop7Y{zdMZk4Zi-4?QI -?W3Zlc%zSn7&+uGQZ}=R|tmBGpLh;a0@6)(h(1|rPv}QF|Luvs06zU=DG@!x2qOTK>0Y;so+@%})hL&}8KHnuvzmX@j1`RLrb5ZLJV)J((Y_F4U&2SKIZ*%p5z-YH~$CbAA0znQx-0X$vtxyB$@Ofm -LAQxf-l9ztR?1pu>85zY^L -~4M_APwHmNVK!60HretTcT-^Qu@X=J-N$QD};?y4MhqKNEJ|ep-JhG4}r{6FF-6O0p>1e-BdwC8r4H8 -5-j>%**t3pLl&thd`^mZJ4&T=t8s=7N%Pob4j^yPK?K@sn<2Y*UMsQUUp|W8G3Fs6)z>)Fq{`s -pWX<&~o(XLY5&#wQQq?)l+lY0>E~q}?h0z98W>f?2O2Qk|Gy&0Oz$^mvA_A_Zc?x`q?GmYvETR4i_*J)-NkkH*n4#_ -4CgKs6%ibhNgxWa}8%1zi$H6c8W@OR0R!!5Si%pG-*0)yKW9NG!usdjOF`O!V@q4=Y&SwtLyrKSS~_3jo{TnopZjlaOg%EXtM~95g&q -gK@{Ary*zYKq!E)5(QoRA`(65s9}$ohIWnqb|8Ioa&M42RL`qqqlMlgKrbOELXIqW}6WP<+ -i3_*Qb{+n!92gf*^<8g2-Q+wRflw&n@$Fu~;Fbl_jY6amjTMP}b{$(M|K?kZS5uC9Mcs_E46Iw#s{mT -_b$P+w2oR5yWbipZUi9-!&loA -E(C(fAGWBZ<%01k&aSZOlGHSzsX|cf9TC8pdEasagMt>@qN#wLR_0YFUl}SC*hS0f2S9tjg`i6!N+R# -;JodKDOt*tnww!HA*}frDC*CuMw&6!{zDOYhw5h!~N4}0KA+)MHgyE7P5QowYMmFiscns5HrA|k;fKr -M7nUrnr;jD*a$#v5y%!EEIi6P{toy_yc)TI5?$YSr$Rwq23N8?tTh{ewXtamVjr>X>9)Y>Z%A+GfA(( -&ctUIikjLo2aCzB!x{3-+8Lp-1p{}NR%WyrQB~YaeZi*t8B&n@(Zvj?_F_OHtFU`iP$vnY^lGmwd3s&}g$j&-l9AutQU!a1V8}=`!?>%65YQ@ -X>|>M^qEWdNcH2F|A#ULHlYu4M4CA|U|+zv3}rk|V&CYnSXgKkXPYBPOD2K7A$ -l1_XS`P@&+sB}v@4kDXW#~_`D5lyP~FC?7!+?#mfa}r9N7sh}h`K(brAClJsm*Tk(ZZkxZ`{A>Cc%d& -{!j4uog6aj(iNz6gL75CCJD<()5~S8C3w&YlfQn%w{AQo-IAv2d78Rum^{YPXotS$`wiE-up~=j4uv!XihIAG%X3vPbtu>C)H!p4Ez -6d4MVx_de^HUq$X=|eDOF|Am@$80yk-Y=qOx~x<7^DVDL4L>tqvJe-xkBs(z$BW18Vdtn#;pH={b6Atc;m-ZCz>m$w#(>vEOW)J0IjEgR- -?l_f>%i_`K61t^N0qtJFeW#}}`n*J>zLJl?=Z^})?jH+D%r9qoT6#cXGT^%`IcrguoqMZy$ -`c-RR)^0o_>O>9GCC5eXzx$cRT79oiVZ)mpdy(E443#u6s!S{&WT^Z}XbYlrF>RO1qU_mFio8ln_M`H9iFLO3zTd`S|`CbJ%aF`tyCGV2aN8PN!m?S35_z|PyO<%Re-WFvUL}&4%QHWb`lU3Kr -rZ9f{`siHy${HLL -F7JSGpmdmxKd($$*W8_U)B; -Q7Hdrn6|(lG%SZRn3usH)gjFkxO%YgJW7!56_f%r8tZQXvwIm83=TBqADMCn(*vVs}Epfp~bN9=9F9( -qf&kO4lo96_+-*xt*`-cIK3MYD#F^k&$kYt*Ms0qTCIOP()70tvuPIL3)G1@bMZ?r3u_~s=I7-z8O$Z -F@_MQ$~B6^6sIjHc2McnzIhjAoQ$KIT;a|}@HoHF!8os{?XSc7G_8~hwCm{#J|}xR#AWP(*EczbvzX* -A90cLOQ(_P95?}>nzqrwJ7yE4~uI24;{nItPQBVlCMh?c>a4_y$Kf;BR!TFWFNkwpDo^_{JZKRsc0CL -eW?$iq*I`FaU&Bapt{H6?%V$j7f#Qo@0Yo$>taA|rc)T7D2+OJ({Q!C@0V>nfuy=kh%v&SV2lz&D^Rz -7;9DsBCn5?yP1LSd$gM*Q>F5q)>$EnCX9qpN8Z(hfjHYzStDcG3Bt!rL`3C6n@XdN{78N}{d#lXp(E4 -&9RHp?G9R!;@1g7!>j1`4e?mMgBUcRbClUcNm3K2S5Jx+f%64MpQdTRDx#|;0oe%S#cNUNr6;Mg)z}y -t0}f4SmzuD8_|&$GC}Yw(#nXoBeS*7y?72YkYN7dhc~drEsxeACmCFM(K_$RXAEfZW#FBo58UV^c&;P -OIkFH0S)OPZ1x(O(-W#|`Wg~k%*iVOIf(%rT&R%jfq)+v6F#ERyPmAhF{hyBCR1wH*bBq0_Rqquf+?t -h{R7_x78zKdMZApSFe@Xk&^i*+WQIAoHXxBnJmHd*85HAy~fFt6eG;W7t;hA}VB{If%E9t}b7y5L4v{ -!+bg0WaI@be{GFJAi#w#yzF3sJ^Xkh-UH;j2%Cs-|3>#*j;zR8uR;3;*f_juSLE91B0*JvhoV@56EI#{3z#Y2Z -0E`a$tSJ5V6CZ$mwjqZbw#@K9Uw;4HeYqjl3+n)@!mKn|wMc-c01t$huqk#Rh^=!u8nDYaIB%T0^{y#qTjb8PUe|85vXf1p2E3UrjcoCs^-h5tJ4VIRr1CUcB-{)d<)2dx -#9@`VG<+sW@JtI+mFZ6H2OM}NBLFT@_p$C&LV8i6W+r=p_~$easOA@4epFm@(J;kAys?x}T|}hq9COK8z&2KMP6UmwBY!GDCD(q=HltJ&&w^+0Kraa+_3J!|9UlLm7{$(ezVWgGtp|T -{(=>_fz?Hp!>;vFR4GE7bieC?&ydP2Y@TFt~0H=HwdGa309?{??BeqAdh+#KaJ2A26_+Muefi?3it3X -NKO4sJ>~s2rc_ZIP(FE!v>Q-9bxZjWzH~8OmpoJ)X;Hh}E0xeXypLE?-UrEz3h#vJ#wg##GvotvD7v5 -Y%^=HlCoX)~0cOr)r|>}%s=tezQQjoc4l{V*^i*>KT%pX%**!Wh|t43mWtcM$75ug17kRCI9 -)6aeYcjb?n4-P~mfY4c(woaqhay@c8wY5j1xE#p{noF3|pbDV(ve{|u+!yx69n`Ju>BDyPlU;=3FrV_ -Tz{3uTT>8}*lgtRYJBHX|-vEsnl-?SpGgys#_~M+|p~G}NKv7j6r3{vtY)xtrq2+hSU|%Mo7wJvsg5( -di%m?SFkVemMEl#h3pPpFEiU^wZnZ*E*3PLHG|R-=kg$LjuUrZ-<`a?`DE#ZZXj2)1)J8|T8<65a5p9J^C3NtL+l47X2mxGdE9w$aTWtey2dNT3urg> -b&he`pq{rXgpZM9##b(U92*kW)d`)k6vTH5NdW|TlD>s(@XMOy_EeqY%P4OO@+MQ -S%#`x8LBp&XqrIC(aTP_A&F+ywy4`IjGW)NmIM$u3(p>Y|0fKgB5H>G2hVjjC~ee8kAh`cvP1-*H~A( -+S&-Zt7z>7OZ%N5kRyU;bk{FhN=vi)|XGwjnyMv=_Wk2M4Rhy?E&5{)p*4w1YvF;P*c&ASb&V((rs8* ->fkvCGhr1k&+rY2j#+=LU#+e~JyH3>y6O$jkQH58CFolPiTb^I6j=WFCT=tcxV_d+sOM>CN1*-B=>D1 -Z`MFlPMYuf4*6cE~mB --SxkTN!==`=I}zzrv392X-03RxM__?u3a|CKQ-l))kHTNnO$s_q|@t?BMUrWFz$yAU{yd7unfhP+GUJ -2h62VnrzxO%H#km}pgn#8vw9Pm?7_&=_Q|Ic%u%P(?ahc^GGstPIwnnxIgu}sCfE -v+X(C>1f!G5vLY@R_Uf9qcYrARy1Hwt?)oBOLOW=cf!PzV^eIEIMFtlBv@+tL=M7=o*g>J_%q6-i-lf -?A~8j5XUvv;aEURm<`O#wToJS~nhk7sA+21#>T9+InH$!I7|2oHm!HE8zDTtqsDAEzK<0U63`P%{b)! -8w%F%A{@Y2sFiwfaadBfolMCSZmz80FDSH(19SzV}nW(R+IRh&6uT*2J|I0Qagf?tWFfxlN3vd5j7Ck -J(6uL1txsPi*}5zuTgs4M@r1sUq{!;AiZp9X{q!yYvUAAdWuO8nHOLSd^x6c -`YzOTXd~E`L0(=G$m~*`0I%-(hrn1_sg*?|~U&g!dKnvh(wr#e|I}~}!4O>Y-K*3*l%hkz_#Sc@sqwa -DFGql-f2?|ouL2C)y1EXqAJZh_^gk)=@MDg6tJArKYba98;npoB@*Kg}?|wlz^9W+->zzHTgPDto -6cCdK3xP~X?mY#NEtZJ%QCn81d3Z;Y~M_y{HOE}pS^wahtH;;eeyP`)_C>9+rt8OtMCrS_wo7LX|hf~ -`(*TXoy^Pn&GgHk(vL2T45&UT!Bp-G9HgEU9oJ*jDsYa-V8?u0lZ`XD5gvq`im$0#}&dZ>CD(Voat^1d}(HNr -7WgX(g_6X+1n!d1uHh%^lVwNeqyRZwxlLhn-5X)*-C~(X9dvaO14k?WG7yBkk7ZPq+??>a;#DZ(;l7I -67#$U&ME(A8a1(_){=P=Yxwf2GY{+}%KxaJn6iVfn<>AOOGr4Hm+K4>fR6LXHgeDN3{C)_--eI)i88h^@%(Z?%fyQ)|7)|IEiwQP59ebA^EpMLLCkS9&BMmePD -c4@^ePGt|0F_74M}UWG^PXT6hqSKr?^OC{PrLlX1iyV`G}kx8Y#I_QuzJ{$Ka>3ihiIy~g2SChj)y*s -?}E9H1)GN>oFB@79u!mUFR<*g_ -YD8K$pju%d5^YZe3;f%!|`?wk|JiBqQOxBIfg#mm8h;D4*-D$sH$Z6MPL7d&NOpEVr>{!F&VgfHMAnb?JM@iYHK9?V1L3?s`nTcPgL&V -4a{%cWGlK>W?SUV4t4Q|UuCyXFhIR}_%x|^&#()t)rp)bI;DFAPl3hUUxM8_DL;)=)adjIiLI42W$KR -eI?XG}5v;BzW#|0J$ApxvL5nXw#G4#L!$)r%b##n6gW25MV%?*TZZ=>PBusk7&V2iPrjLrOvgdl1#vaKES?-y@+d&I=nz&Ew;m9rsMT=4GS}by<-ys&^5*f0JSL%H-`Z9JxZ{O+ -fkw-j0&X1(Fdq3qsDI$%2kLQnDU5rZxOL2{7vL;_~lby#DZu=<@HMzy4_S`S0mYB!Bdz*D7F}`b^=DG -Y>CaX|5+*(+>rCLZE*rn!{oS4}u=po+wUZvfE%zvJ-fMIgJ=YBc>CmZtsF}7~fZPD7`gbnlhBqBjIfC<3fyFMatC<2SdU>)+xnn}AV -h0OL%MqGsWJJKLG+c%D(LNk5ZfaCEM^Dd`qhvWt#6Dun_a%FXEtHv6ibjec-+gCx#SmX5v4A+8lQh6| -ZFPU5KFhi+O34j*%|?SRNJNl>lyw`5l1Ung6(>`Yt9qayE{YE4g>O0i})-0o&57|4YQ8r{X1CF%0J7q -283a@FTrWuK3cm2*a?C2G)L@U%_hy{2T~rlK!{$8~T=5ip#!LeUN*#NKZlGROQt+qpe$yiQiZ>r|C -O|$#TMv6r-rbc_TqeWpLKi_dqJzNxiIY%2V^oayEf}r+xXtHf(897H4%-wj<@B!Jj`3bpM%FykXLGR_ -L-g75KUru0r`d!6-FR=jC#%)d5x=kFEL-_Y8{QQ4}=jZo|@D!H!ZU}G4gM2=i^G*Er7|znsyJU=^5S? -VjcV=MphtAkUPNg<^iQF2_*UjbZ{lGl|h?&#LHhs*a36%SKzQZ1ttF4M9N}0h+mRfx+`wcdf#A`H&$n_9`K*MVq+8Y|^cZ!Me#3$g>>-(j%UMYEN>IN}ObKcx4xo=R(2H(5nRmGZBz(R -~<{4(Tmyzq-G>3HMfOH`PsodKlhi5Z8VYeUODB!~J5B&haKN*|-FCwGNe-O65JtixX8N8LOJ9)Azcg@%}#tq^pLxy -K4Z(gP;{Y_BX{Y*&GbT7@pk!A|&DWi)@hTvpkVPR~duuJ)!q!=~TMg5ugh6u`bz>o`?yx^S$ARZx)0R>t?UD4}^4sMuxC9v+0%yqEXgi|2K{n*&!tnaDq(Z(X{l+q4Vg -PT{@RQm@yeUp@7WetJOQaG{=6?bp8jCL6Ac=KJl9bTMv -mpDBWsccqERxpJJzTD+7(ccYrfP2jF-vY~v-p1D3@Xh1lhFPcCfDfeH|z>!S7mtP{GRkY`W_s@t1XW6Ovr>OHclF>Ij5icTpUIUdXeqy|@+NbHu(AH`zp~7p --I$GTktaVMb$8(#Ua!%ECxkBbSPX=d;O!b?PD4#K`co7-@ae{lA<%8VFK=E4Zvk5ei|)Tuq6O9mD@}V -Avv-yh4$wS#Ft3HISBGnOoZP(wu+LNffrvaZ0u&XPVpC`fA9cw5*wSo8u>w=|tYRUA(U%x -wdZGfsL1Fz!IvFG?O3zp1-7;4v^VDb?FqjUjJE({f4CH!yurZr#TOE%v~=qn%D;Q_dK3h?D>YOVAaFn -5m8QmcCJP2mzf`zzenPQLD3EW!L`Z=@-lZ8A-$!8C!?-a=D}t7P-MFH#~da1Bi$#-~HuJ=%J~w+!(R< -7H-DscfNyLRqvi&u}jJQKm)ti)B6Li@Hu!G=3`rcoemJBeU|9JyrN?m{Qq+uhNx|bF_=KTETd276I$zi8`*W>gnX -DuqIPn2TNnlZaZ?-h5pa$zV3Nt)Vboz|w-GY5~;)QPar3D;uzKYZFyzB&&(S(NVq6T4R&5F_Os(s%RCao_^xPQvH?=vep3ZhejV|$@j>WbucPSULm`0;fr#xN -Yt~_(Gngg4DS@*97>aIl!iUYAyIr5{8H$SipAJ?tXlT3k?WBETEyO+M(Q=*5ZDx3Cau4&kvBO8i=Y*n -@fW$K>z_JS(8D5np9?nS!RL}?^dcz>khu^fVTRPuc=PbUHKU|&xmKkEHo$2nO$TC8_QJY|Yj0GxqSnV -6TOi+g4%<>v3o272?KZqa3A%LaN}qv_Q5O_D0TnbxwH=kHa=-i3lk_wy1&xX)1VB)%&qU-??nN&?NdW!CQh>3#4lf@*IR -chOT!2{?r)>x1I~1&3-g}O_-x{X42e0Hae)gC`56?o9%Ip=Shu+A!`hFZ1P+4>Oz0~x@=sRShUoC!A) -_10aGGt?X4R=)cTplnI?3JNAIy6w!K_9_sS}2+%+)`Z;ZhvSc~?VR;h%)vJo4l#_Knw9i*4SG+_8vm2 -EI2K}EI;+%cOw=*0{awLUk~Wk5z1HN2bS-hU;GWxf(k+CVirdSN+K2n3>+t{zJ)OfD0pbs^Mx**Hw?ycGw3vR=! -D;haelg(In>41XGw4j|i&c_R@Bwlwlzc{5GRz%HoAcQ^u-4h71#1kEaZ#u!oY-1ovO=7D_^y=3*6+E; -Q-jarIPG^e+5^=p6i0V&*W{c~soOF1U1k@5FcQB?9s9YnUhg4v5{l>N5Gx6M%QLqH)3(%W5284bs(VNA0B@+h5} -Qpx)tIzg+of*0Uqsw$D@;NEW>-6roRgaDv{wPB2Cec6D_P~SQKv)p2Fxiyh!x -73^$7ACl!9O%;g(l6EITN`>g61<@Nd^My>r@aT$=G5VNhw-b -z_HG6LaW?J0C+}Q3-Gb~T3n-hIj9(iZzs^E@V7G(LBOet -IKA@!5ZBT>K_{@YLw=km-@@NPZY+qR8qg_giEg=da%Pu1wpMX(=x$aMZ+RCN)#Fov2Ld6k^R0p?Fsbf -1n6FdsvQDnBEQ8M`X&WmYE>W+R#1XSx3m~T4ViA{S(-0cyZ4ro&5>_K@o2X8*A>SGQ)@E(hE6#u_zp}n6EOTtK9y6E9X`#v-lK$$u(rAg}NU(Q6ra+KrH2@g52LB-IY -9}zXuF{Uy(HQUFe#PLgSw;twLpw^#=0zD=uoJmdY%b0TczYFDh07A!}p@h{idRONyb1&_NMTW1$@F@0 -~We=g9|4;HdTI8z{7g}io;P*1x9CH=m8*sZ>nlb#q@|fwFXczC{ -dyq2=idpo)mhtJhyd}L}Q`#W!6XBwl!}Wc7QV}V?>K-~GJbe=n5a0W9aP{;US}Y4o&nyn(>b?24F&NF -iH^b|W_pW4Z1&QPoc22o9Hp4arj}WjKk^oGBtsPj*7yL#e+n}is*@M7_0N|*vA|0ruw9wEeDEp!8%O& -|hQczRnQ%ZGebOED8<9#?!jrZfY-5v$DVy;?dc^)BKtSU6lcIuD*4^T@31QY-O00;p4c~(<+rZs155C -8z%IRF430001RX>c!Jc4cm4Z*nhWX>)XJX<{#JVQy(=Wpi{caCyxeX>;2)_Pc%s5^sj2D>02Tv%90l$ -z&a8>uG$9?H)U}%TOdFv8D)>AT4XAKYrhP07&qVqr`2d%1lfVz{9&QKs#)-J7STDlWaC-WicK78{Z6e -20QFPEZ2E5n-|PI@YvqBFZM?8-%ED3F6JU*7h+nh!kn}BqRis3NJQqtsteB9hoghTlZ!*YxGz|k#q8k -pFmTUxuHm!Vz0RB=zCQMF!SV6o$>rg@;KR|$VQc!1zx&30vwxj=e_Utq@DEQfWPjkF3&aZWdRKB -36^b`brc<7y>_F^gTA&6LWu7U7wNU&b4>OsD1s}2%XBp=)KtW*(CR5bPbZtQb`2vL!HE1ewqoRBMzex -XH`g_dxeb_yb1j+!La5l+Bi8>ByX03eb|C5Of)H{}_tijp;F26rL34S^}zc@NYj)5llAU6pT50Y#uuE -u*HrHB;T$jud#-*{Ab;{k&YDBjzaYEQ}}Es$u~k>4*nJRia2Bn0d^-~+spMVLhZ -x0*0shIzOkIAeAR6yOsGC(=AfXd}C~nw@ef(%}`uNdU4zNO`Wl^Jtd_d!uwN6dZ8~1aUoDWE@a!VJ%(Z%`Ue~0w&?&w@Sd^ -q`Gu#r2~*g4*Kw1zc1yy8UQ8dSzM#Cn#0Cwtmdb+I{|knUhS7++7^^y42#w$XQFlH4&`dyt_Mu-l2_uCavjWi-$6i{BsRRaFW@<1(;ab{bUD%;U?_P8J)SK0Ek~(Op_0Ctr?THVxMgCPg -)FwOz!hJh$cu(0TWd48~jpE2#8mlsYsRJ#JJIpRhXfL%l-FZDmTTkbILUI!aBjM`+L7Fc>DmuXV8_r?z-C6Hq3 -HA+`A<~p#k^W5o~-XKz~vizIM+H^duwbR3~9Z;NG2vm^PF4s?N3o+v9a;%E1_5fbs5k^(y#! -&p+dUTmV44T|FKUyTBMLVTs16l7axP7T&Cc_5RdvhAk=lvN(q}nz{h%VO^0_=Yj2eDFz?9^4)8rC2oW -UmpqEBd{Z6XX%uE_X%|KmksuSfvX*?LGww~(y2E#d1>!=jdGch1XbH)?QF;O16*3x+hCL66Jrh@kIqk -cKW1TNsehVR3V`pQH>M08cm}1=5j8|G)8wsg(Qds>Wkh0#8l0eK%vOcf!B2HSKqBDMSK!X11q%2<1;LO4v(|oE$Wwj?7OGbGj5%S1)vSX{!gLj`Z7`rnfJr< -CBpj*hm0YUDxUInz;QUNFqTdPJ+=5fga}Le|kk=65FJ>DO3nEH*35oA;A20`yp;9&rgeiQ6%q5=+WSL -hUDHp!hZ@fDAD8NH&IlzUPWC;4DNob)o2LV_XQ`^eG!sPS^$OOd-E3aGpTVNSh6M>Apg9<7p9AVNtyd -$AmAbQ1Tc!3Nhk3xVoP{<@DRD3oN48CLnz)n)*1yp*AFl9W?MLvq=Ji5gOg6*6&wEcbNBw3V}F?Yrtc -Wl@Kc4XMrb;~*zQjF8E0zKW;e^L9Qxybh~^Kw%*s6!@H;29w6I+?KR8%8|>EP$WoP9;T49F&S$R6!2@ -7^>8!Mjte=hso-e1DZ7itGmU{2C5-}yu%VdU5_+7P2S@4!)Se+FJss&6_YR>eNAm>=WX_9dv)kxOCMJ -tL0+LJRM}tMT>Kp7d{w1V-1*yRmCbQQh|SS0fqXXO!S9y21kd|*6jK#%`zkbiEzMN@pt>Tl4c3IItyu -JR)U(N>zp;My@pUU$+w*>whts0P?*}yvdyN$ftwg2Es2&~}Qj-3t_$g(xw%bh6GB2~X1~n*Z>&2re>X -Ukx%1@e+(+qYtp*5Y@r=|ol4`+*T3?xDRe`n>EjE3zRBI7U)c^h`bz%NV8!RzuO3iUZc3xZA`3%*$5a -Ylf7T(dR}v8~=dM{+w`qG_jcDwrTC)^o9S_NR8LT*u&-Dv>UB2|Bk -*;}THYcYK|FE>ioUnr?$dEY`$mEyI~L2*-_xiry3^?sppar}Pu?M>YnF~Mb6Cyt*qr_QIHLVY=eN8r# -6n2!ST7;cbn92%F>atJ$Ue_F3+uoI|2%CWdCb?FPL8)65il1*c|s#=o_+g$Fq!+MtG+KgAL)f2IP34 -A0xaZ@;pP<@H7;H7#lbrswAnuWw;e|JR|8Pz8sQA5aH{BWOm*e$0~lku28K*i7Rn1pRW+@py( -5tHXL`6cqb%7!rRklR`ORiDzY)_dqJc7d$J}l?@zn^u3MRFE2z~rjQ9ZyAFj}=Z#~K%S1MkphQWSvS< -#eLj*g5EL8pYR1aAPY?5K_guUC5>JY--V|THTmgO2zrN?m -XPE4;`t=Z@uiPwRB&u`rl>$eHW7tsuu5)$ZPB5$lNQQy37HX(HTEL2^^4I7CMM6+JT&VnSZ-3F>Va07 -q?(urGO^ZFx<}u`Y+X<~p5U=yvEsaz24Jul+E_PfOlFzDdoM|*F7HHigxDa=7*r*qVGV)w&QOl}tl^P -*)J!$_QnOov$-$25c91#T&;mraRfGu^EbxxD#U68Lxfyyz_kOTB3t5f1$fSF~F%?Lv=ZIaAl;XvL|Q~EF98F_PZ0M2S#_WYh+9X-^)h|*iM|TMf~uyGClA~h*EKY4jxz^JwnUwT5o=Z5{9T-|H}ZMB%kV8jV>Yt6h=*wIS=A&hEAgSyr@C#@@9+ -OJ7gP@~E)n7#>Jh?I?u8aM}SHZy$nw+QsRmxl-DQndG74WSS`N0`Vo{{zfBdYP5wV^#`ELrWmCzgW+8 -WynxBLKl2%h1Fs39PF@@wgZG`NGiw@#ke6A85#5G(yOXIS!bWGhzv}&KEv()}pT;8}?KA%^^nUuV(yP -N?nDr>7?^5@8wSQRgFU^h^9OgVtM`?0v{ -yyLgPH1ufGt*V2ptu9To4x>;F{=GivYw;L8-c!OEHMKDmY5B%#L>6#=B7l-Hxs#(p)61brCS>(HAXs> -)vsvqetBA6b;tmMslw)Xy5C7&D@tmw%2J+#0lnUlo^?9$4XqAp#vukSumtY#`-YX4hjK`aGk*M{3YgC -s(uOVCPi&QO%!?z+5*|jjIGJTPjy46wqENZ8$NpW**Z?Zb9x$vhgM^CGwP00)2i?JA}AS!OYUyE59c^ -uqlu?)_2OM_NS3?N+sF$AAS#B|0{ax3UhQw>6JIQgwN=J;rQ36#O5wXncItxQ)p&%3dIP?9vxRnV1AL ->n&Du@UuEzU?-G@WDY!Pr!AA(Vsja^{>> -H-9jRSn8nKdDw-v-5xp9Ceeg&htCJ?tz3|757jRV5cY+T)B1K8tJx)>fFLr7Z9;7sM

s?{~Lz)_oP4U#H&MpXvvyVj?Y2ud$6H}F!OiZ|e`u?NQ3g}`~)jMGh2?AWO&9fODpSy -Nc!2um$M)h6`&hh?`_pfF%=ROWcWI9-qRk0Qv=Tk`7-3ZDoFJ8Z=r{H>9+TQ!7gmMlwc1oU>OZqTWpm -^ESGkCupNeSO_@6N^<6HT^*yeC+UJI7TBcu6}(cZ_o?abTnNB1mbey=8o&WdG+M_+H(<}x9eT|P}XW_ -w%_pDp2Bih{|8V@0|XQR000O8`*~JV^s9OJ*9HIpeG>ox9smFUaA|NaUv_0~WN&gWWNCABY-wUIZDDe -2WpZ;aaCx0rdvDt|5dUAFf=y9KY89c~0~?IGK-algfWi%$G#HQsE+f%4TZz<2Dv9^{?7QPbmMooN}ILL#Qapo -A=A}4=2BTJb(Y;3>N_x9UUD-r&MX+wgw(}EEBg}Nr9Y5P1P5sG^u%^6jh -&wtJ>CpdU14V#e@xAFZCKW3Ka0sS4-LAyfM4d`@Jv5aV^H`N9h)v|0W2K#MQk{d=^ZAhnD&6y@G5L`vAP96WKP?abA!i$${ -J(9Lzi#{t@xj;yzm>K>hdCrRx4Uns}-4(%NQ)fRHK-QuVLe2@!{;`{c?5s`uu$HVF?JutbDv;iHeits -(hAQo4euUjO7?q -$^-cj2J7v;DC`u)|(9M9+Rj8wF~DJi+n$ZkgNA?(R6>qEYc`S$?`y6K+08ektyU>^V23ij`Z~KI7)rxf1FRQ)kW^y~@ajXj7(*c1`VgB16EL?Pp#L}@V`PVbNHnX~8O8%tJ*@rg- -t=NML=}D(A%H#=K#XX{oJvrn$rDE3FomAy$mb?gveZyt~jDRjdMzUIKfi;+)C0V&^0 -cPIBCaB!Y=Y*oY~t%f^TD>)5JM&37l;WvS}ozeKB=3^x|@ -|=+rVYN5%iT!dSL9W?gWD81BsaGZL85XA6^taE~!nu@uuaGKBUaRWn4fC@X~b{ifbuQ3HOJpBq)0Z8f -8{YDCGmLPBCl6F8v%uNW1}DO~KWJaL_zSobl!)ZfyGz?yeY1inH8!&l+Z=J~#|qo&fo8YrS5}LGJ2r;Kg;ocZG6?Z(ZgE&rpJC~BQb4ugv>MsK6Gfq2+fq#p&_G)YPV35nHD}*>!=2 -UM08`ciX8DjHw6y6%3_M846eBXf(Qh_%I6 ->&k1Be^ -%ED_rcpV2vRQ`{M`kUQL>4Q9cUnB?krwM$s0B9ZP7v_D=ItClqKal5>VOSmW_JcpQKA&{p6)Ss0u(VY -@<)mDEtYpM%LKPQ?@a!NJsX+n*)}!P*Howsa>}7#iB9w2jgsaxhkRG2QE|1`|;9A5cpJ1QY-O00;p4c -~(=#)IC_^Bme-#m;eAD0001RX>c!Jc4cm4Z*nhWX>)XJX<{#JWprU=VRT_GaCz-L{de2Ok-zJ&K&AQt -bVyoaob;+zZ55kNbg^YWNlu)~3Jeh|2^9!104P~a{J-DK>^F7+QcCW%*LQr0MPhenXJ=>UYiAc*!RYZ -Qn3ZW(TiV=H)8F7B%A@~R1;(8ew?udQf3Iq-LR^LuW>Bu&dT{BX=IDzrUH-$uZKZR-{R(O!Rlequ#XL -W7p>+=bq#=Iv#fxTEh^TC;$@@90xhd#*2L#`@in^}2A8af>6sY#SYRaUV|hKhg83ZcEIoa?9e+1HIzD -)P7#~eHHgMFe3ijmi$58(zJ&WSFNajHI0T5!ACv~k@;8j-FP_|7F+}c%Nk}8S3ZgYXf?u -C0j{Nb!+{Kaha|E>LbYNStU@jj1R4k5lnZvT%=0@~_aG_GVScWX;*zCM9p)6ILr@koh722g{`3nRe$7 -P-Kw$vp?)Hko3hhQ8Q2OCNaRCUM)=ow^d?Ul6pP_-Bd|Z=@p2C0;0|j&XZ5Vgh&MbHzh!r~WtG-g^P6(cMhlQ}_ -+x(w!nxAlMkEY95ME|8R@4lJu@Qg1^YphKhnTRDuJkV%9n^e=i!cM3K<%Exs?EzJ9lbqziN9Z;*JaKc -R(pWFsmhxyWrW5Nmu9u1*Q@=bZ-L#2Uhv#;Tq?pk3Z*56{>6(59~o#7A$FEQtNn$(R4nbN{ -D!jsRyjNyGFcIgli(>F6sW`Y~&ki;=^L2(;(`p|GE#xv7}NX7v^xPOD)X6_-m_qBhtN{tOtBU>j}iY= -5~6ZV#AD8Cd#;#3j!{3*(@i&6a2>mc=EQ9$A6b#+5c2$H74{%aDYwy2&sWGit6AjE~BwR@+Wd&#Yj96#(H+4%>F$xIk&@IHAUdTEvq6hC__ -aocUtXuT4{Q9wVL=^DJr_HfTJU5_Bk>HINsqvH{S}9n=(s-Bqej7h8v*&SK|UWg*5f1{pW6BLhdA5LqWWJp;CRd-(0)>+cVNgAU7r4QzzK$!@a3zy$jpCkvz+(qX` -{S(Knt&vPc}PpHw-8S=F-f=T&*lia -za0xXb-9>%e=NA>>xLx|AywSRWboEAHm+0KoK1F2CiQN!%0%UDvUNL2Ebs((0c{OAWLwgiq8ymf??{Q -V7GD>NNREB#Tx48#QZt2OJO2R8~qkHGM0H-gbg4I)so^as26MoVr3>)izzI7;^V8uq>wk=Fcf>ypd3N -P*2bmtG?cAlIZO-SXy;{2I#LMLgA`mu@zphy;ZGGo!^Xm -c2B#+!p(Dkme8zM$IDzU#)P4gPI{fDt5d@@|kcO+}6H%TU_0*j6DRt@Tgfp~KWXKX|r(GBrbYP6Wk`A -=&{caI3}49(&=)Ae63WR#Pz_+C(`%F^bH{O#evv)9i}5IR6=M?gD5uL$JT!SONQzbntN`v@PtICwcd-ha^ -wrG%$H?vMVSjQ+Vj`s4WQGaWI(4QJXo1~H~QYYaCZ|n&GxN$YI)5#pO -eSU2Y?CnQ(Anfg|w_-tbTgizJXmBN%P6;QN?!<*#RUdsE!LfCYG7LXQL(#j>>NQ0`Vs(849k^ -stq&RGR=7+Qtn(10_$N^s8}cldEHS_p|$sHp&V!$HA>qi`Gq`)QitcoWDQm^#WSeGA=1m;h6=&I -fI#DR>y?(UZ5s$TrbZf=w(PH+Hg}b7hhFLY$@5o02yZ-4hEJ)epHXZC=4D2_EhWKftTzuncYbPBI5j| -XB`t*a;lRR!#}2>(;CCZUHk?~>xejyZY#{B@9)Ga4Wdhx2YIw*W~gLOvMSN|t$ps& -qp~y5g+JnmCSXmR}5s1bY}68U|vx;+Fu*f4MPij^AD=`Uve9)G2~h5EZa95JpQdSSb<}{5hDk>=!awF -^nzDR0|3XO%4@TCHpwbmo-d{;Q`{H#<7UDH0(g3{5JgOi@QI%uY}TqIIU01<6~-uq7O!YQ)8{(Z2SycTq -FJ)B=pGWkO3?-iHklBNobn(+va<5R&Wh$W|6jb!RgHK)0ty!_%vQQI1mpCJU`a-$IpTE;VETWd%7SX< -%EcwyYd!(>2C+TN4SZwigTZ@LUVu>ryE(j2}UGawC+IMa>(=;XpbT&Mt`I2VAPDZ&OZ4)0kLdHP(jJZ -n`U$<>?)dA@-y_AomFjY$RHSd0;8wWTOpa|#_Bn{RrG$?U ->RO>N47#blznuBbJnMLTX_;y8yTp7mk+(EhYM(EX9cp=Y0LkJh{=sg+g1S4$mGb~2wuGHtAxyI25%Jb -!x(>{&z-IIMJZTA9d0-4l7GJ^H9^+AMV4i;UbVyZ*&uJj=_PnI?&DZrTVB7$`T*t)b5AOBkz@k}hm8z -wOy!@&KsQP~P|AEGtb}r@ZMFS3`=>826n+Qd~u&0y&C7b)wjXLM}ukcp!Q=o -eAOL1UeB;X7mGyqJNTb=)BCVA!G+KwW2xSZu=i)a>_k?7+G^%SVEvh(O7|+0w)b+sc2&is05roL^`u?fK^1HIks7}(gH2VIEnF$))5u5Ks -HXGg+e~6SM;|IpfhbdxC%#I!{V2bP*DNH^({tc;7ABNohy$cbW2_Q+-hJ~6!?b!~Hv#de!Obf -mhG!s_*Fo;l}4|z88VBoT>+rm8I>!F)8$4_s@SaT5xxvC_l;W^lrx?g`NcxN_4M!s-fd(8iWQRA0;j? -0{7VT5P(v_Nr|z9_3m9$Ef=|LE}G@atCh3%-MH2DWAR$5viy3q~K6j*Pt-$0}0(uK!+Vk!=4 -RF(#&91e#B$OE{5{fQ$nLF4mM;wS1Q`MLRJgcG`$zy*C2dCRULs5PNxj_~bK;fvf9)31MaC&xe3pkYH -=J79X)l$J=v;W|VIawe-+B?*oA}l4yBQL4+8q>$k(V#OD{=&8;VG;>EDp~!jNF@lnP`dUt$AWLkJpEx)866f$`Hb8x- -Hw31toE;u|_M5j^6_a}-<=Ra=a2rP*8e>_88LF8cTm#=F-0uO)`CNpXCNB{6M2LVhS1<3VT65h5dR+t -Mdp;ZfRYu#dA6G5Bh7*u_wvt4wz*4gp;6m~FM!P+4W6|0Z^}bxhM!KxaCw3Fi`JNWlqkrgMHUG>RNgW -auC3U@gljYTL+bwOVBw-?Q&u)wN7M^~|aYi25uU{8-xKQg3ZhlA(_g{5xp)Lr+t$H7wa$uDZ?d@qz*Y -7jfq1@uPYZbIz|L|onC#LY>NdmspwHQ$R_7I<8 -6R#t38*-Wzpx7?aLVd?(_wy-EHWlEa2j@7@A$#^D`D%PQ~KfW -cu=99xE9`XG0knI;&$Ke6O7w51%O3H;1i8VEzLh9pg6p`|IcTdWK7k(-j{&%CzM-G2iB0> -v={LhBv*y@t09;uAl^#&6>~rt9PY -oulS1L*4%7a|c*AD6@%=Z`>C4z#PhGylYrdR*z5nb7e^vP~bgor&j+4;e6VrFH4wDEaVc&BK`l_b$Bh -h~!zdq!;UNxut2Gxm4a=z$p3GrYEvl`r1$pRInebj~ENIw(cWV0)f2t}GReq<3Qbi{Ri>%!Er8Vr3=B -#{=ec?y4;{UZf8H5Rwx9mtlfP-yGF|M;U>;>e)yWK}FI>h;fD -ZHX(B7wP$*@ES;4BLDJ$7d=~cJErxw|Ku9>>kLd -DS$Ipcowj*y2KB-wo_!(`OR$u^ZA?_zT3h1TO3m0e9J_%rc3mY@dIy$Z!5`wk7Yj^%`(EntdLJ -MiFAV?4R+I2!WUiG>I6(A7asMQglJ#*$^e2VM;Dx4tu7#ORh=fC-3Ix{IXu4vDHt%Kvbw0LXj!~5$9m -6ic9%0d5W8ksK({GhOfE4zxvv^1XtP*($&S|=oDc^(|~Jqinr=Q4BY8L{3V*=0su2iAY#~0rYT6~Z~F -#~Rzx?n4Mb|pKnHr2y#WF=Z>R1+LM;&hF5^m0g9Bs7tUHV;+DCkL-F?4oCrmHMPk{hKs|F -8@?i?v-($XYk1v=Ob#Wi?|w0EZX3+BX!F`PCv*+b;H9@ocpFBn`^k7^%kFa|n_SDJruQGcvWTtOkG3!J>vfJ2$z6{JAZ6dl7isPu>UA*GD{5#hpOs?QS`D2WY1ZJ$s<^aMj_*OrEq+! -h$!YgNBsNUn1W&*CV~2>xGYzmjj7xJ?V96W!EhBdrD7g4#L3PIo1GGuIbNR#sVVGJ=o?f~uR8x+C*f% -j|wu7qjLq@jh)Rbqv{iaTrDXi}-?;7`dKb7-~Z$SF#2sxnN^02mMC(c+lOJtG4QnwmX-556_%f9>arAi&bfGCQR ->`-kJ7q1(4cJ?YnQV;XCx!7W|Mh1qWSS%3Zj2SNFG&PFpvtp7n5_& -r-4$kiO9M)Hvyg#tJB8wYA$dW;?A^t|H#44-uJLSNED}f0I}ts*O1N>CmwI!YVH*dnxD<3~fpp9Hyz} -KPJ+V(A3mJ{@T!5?D(26jUnqc_{tWs(T!Lcm^c{lY9< -P50GPi3gu|;q&zVg0W_4p8f0j^+8vxNUc;n?S4Chh2rq?$c>#b9w+B)m8B^(7w_Kf$)M&cQucq(qn8u -LfCP{uK&eA(fQ`BV#!xrTCmru;Px1|f7&S1@K-0Heq&as3KLytHf2ehW@FEqV~_RxY%&!^t($Of>?w% -J0mn)rH*wnSO-(k+uP(Dnf*BWaAE+wHRCD6)jf}ld7e7t8jvOylRyXMSj0A;!aqC!8Mk~ymyBz)Ju>0 -V-}hQ>TSspZ%6XYYrgi5UMLDOgVP91ShbyS=JLJpd?LQ_fu2ChZtxFoR`|?&L^RxeRPUlz!@NQddQ8u -sA4Og!-&WEQnb`{Pz00#@g$Wbj66E=iUS*Iqavu=LHO~sX^cXC^852l%0K?lY&KX@I#@~ISHvf7!jU< -96ZuO|vn>3sbw)*d$51Ec{t2kS^2@ZX*jJt4T93Cy1S=|Wu6toYv6b-_p(n-#ZD9qjd -(vKR=Uug_hZ^GBDXiu@p~)NOxmX=s%sZr+M6aNr0ArJ|$Btel{&Z?^*U@HscfiyDt+6C^4bt9DYmsNn4Bg^)I$LyDNi(K-qo=wnX;n&0t>0pj_PszlutKk%Q?b+=pzRfC*R7~zM^Li34qCy>d$ -=Lg_}VGr(|B4?v>Na`z19-_`j$WA>cx0PwrNb!WlxCzX&AOnD^q*x)^6%c2MV#z>s#F)&k%|tWfaG$c -$JQoD&$F{iozNbfnuj&D`jY4%iR*hXL0R+?syoTkxTHa2(YGqyBV^-`vA^YW61m= -^WA)2y7KZpqBjMaEpqN&h&7Jr=^@}*zajM`d>+x42Ymu40wo?l+qj -T<5y)%-7$`JdOijnZ^OGp4pjFU8ts)uYfsL)2(d4PpH`hFyANP@t7!RDiS6U|t9&j2(DIZk|i>}*Zerk-nAuOwjzfsAunfO|At)^DGudu;WKFTI8pV%V4xN%N2{WL7E@l^vpVsmvmKF4Wy}@ -Veq=gx`(>Aw)nj4F^YJ73)|g>%P^9c5PiF6iDbaN=-7r7r6-!)$j9wH6&DWr-rc;G)gl!9FntbcH-Dm -omV)f9715LVsv<~J8FlJJgB#8sC9(Zj -WEoD&JZ_J3)OmR{Kiz&f{-f4)8lLT33vd%hGe)%|7M!JL_%Dgcb!F^zn%(oPGo|Bfpccm!nsFp;$6s* -u_}N()LL=&%r9pknuRTE4RD(^Vo6J){p{jQ*D$ufd~ItE(JooQOk49z5fdNHL$YoqL(KU*8uHJp6Dl{ -rhq?m+l_695)r+2o=l^oHtBM{h*IOYC0Lisip5Di%Q9luA&6VteA=7zg=cW7Q9jq68#3}^P3PFmw+0w -*43I~+dQRlo*gqI=AMR?kKj=n})qD112d)LNFIHqq_|8lS%Yk;cLz4V9xpUqfh#xZ9K^f3alnJF{E7r -5?%A4~|S!*Md3j9;45TZKXDz5&-+sAmj&Wt_<_vgU$(@kQx&Lp;JJO&d9Ouh(QI1X$b#;!1lfqO)}k+ -ty`{6Z!E8MiSv+Zoynnp+8={>wVdOF0X1Szj0xCBLy$`xyFFt!6e1)rQJ4M$p!a`VJ6Etm(f9WTgR*A -7!nW0Y!EFx6IM`A5cpJ1QY-O00;p4c~(=Mz14uQ3jhE_DgXc=0001RX>c!Jc4cm4Z*nhWX>)XJX<{#O -Wpi(Ja${w4E^v9RT6>S%xDo%~pMs5XuzdBZyO-;=FuH9oX;K78lS4Ks&>Dt9TeQurED55#yUyXhduN6 -Zk$QN&cQt~nEzS&QhV%H5)RV!H*)?axBlZ{_XwP|M8zR7W>bA4}nWe3hmCHqt7^R+E27RcCVb>R~NPG~EoNOiQxn ->zzoGvMNeZ)yEmziaMc($%-p6V=wXhXoNa~*RHLyG0?rGXHk;md=K_dN23vO0@b@B7YsgQwyp%PVO{Q -bB4@b}sYsD7<4Um)LKTN0gVdd#Gv8Jht;_a_+2VbS -jdQ?S(e7NdjT6*2`5br6a{ID8jB%$X7{WX2F)-(SXoD4OINRwgcay^FsTGLJ$>MbjD15lt#%(*=!9rc -)iQGx~+sN_EO -J4E48=13@Ks~2Lnt!$$e`YIzKGY!-x0z8T&4}Mvv7srgNG9_}EswVnkUTl?RR@8~*{BX{O4`T0R*rRG -g4jErNt4;Y!r6{jDT8=kWn$0f7mfe*R5VlLVOYhMa|hh|PXUF%H^&qjQg(L2l|S}4Qs!V~{~Vl8L9ky!QOCTL~8DI#aHpHW&we@+3zA*(?!%6B6oQ$ut?%AliTT~_F;o++t|B$Z -X9e@#tr2jXCu{L#oWPa-{0NDVgRMeRtXLg|KK8tRE3pxg6X0R+lXosuUWpsXr8b61|uxoaJ0{y^7FJ~ -?^BC44K@+xieuSJs%_SI~7RNLI%h3tTIL{HTRmzNWs!7RUPV@i^$z{R85AQ9dO+oZWQHt~ilku(r{5(q0qJ6z`Eq}PB-L=!VFz!(5YsR9l55N>iM0lOTVBFQl-p^xurB~BE`Ak@ -einlEnZqj3qjgENaeFfK6(nS?xB-O1vB=I|PS>(D`Axwtn1o6-Y;%xbwGD2FEOycnSp=v$*z`+ExrIo -oj|6U@Q3?c1za@l0ouUp`&;>Q+ySjzZkiNfKS&ZD)E+K}xi?aszgIw2w$t@~b#gXhYfsb~fnx(ySGfg -aFP6$U&8D;GOG?7JWx~on&EIvaw}pOuZq5g^_|7y%tUTS4|#{Ond-ETxPrmmVFFWpsE#?+i3EI-|>lG -*luN6f}!G}hOG^hX$4-2CUJROPoN;f--7;<))O5?VT(COf`>L+^>$yRhpba+^;3|ezl4KX!F3r1|MD8 -(uqJ}Jl0=Us3Cv6=KICj6Y}?*@J$}hIBP(sNhO23#$Wuames4>%NKb>niYF8Yx6s -CdJvg5GkIcv?O9bJNpSCcXCb-2{@5ioHA!!o7>I2;Y1Ep`HPVxcn|KBWq7No+2@E@2uT+9}SytSOG}?4Ri)M^A?045#d7*7ksw5LS5`)+r*b0a|)jf -?OuG@vCJ*i$ti;ZrhxXT4AGssumxP=B{95?zd!?Tk0K$H$(fiM;N92rnx;zBo)>%D+do<_m`(pLnJ{e -a<-THtWb#Q`>h$N=?``TZRhe#aF-V^2fG$0TwxZG$=uA#k?PCjIav9`sqAig5y8o7$*B)WX=k3|)jif53UsG! -2oA#9xEoL(p{s0D8b-(ORnc()T10%G@_~0AmG5dc7EnWZ~M;v=>)P?48gl04R4;$ipHd -1m)|2cnB)*}WcFL98Uk;XqZsM;XIBvrp(Jn5FGW1ttSIko=*XHyz-JjQv>aw`Bz -VHd#~x;)h_-$L}~P%&wJ)EF?8L;c1uU>gY)^r5UD6cZ;WayjZmNCqMsU<}_28+qllof+E_xwD-0coEa -v)sc`Qw&@+7t@87_O#P+Vfd$n&b=Ip7hd-Wm^;I8F1_Bq1Oz{=p9K<8^PzA%isOof=p6N~j7k?m2LgZ -mb;O-ww)9>_Y;jk}Y6k2{>!RlMEOTY3>ASV}kP$<3ezL`bB!! -?~jQK8hS%5{ullHh_=()QHdc-=C9^HJRkwR -2KZfAVGT7fFNE^v9RJneGZMw0*e6k}>t0Amug>?D^}MajyxRLfmiQdx4=d -$x}SfWrM&>Pfve$Pa_9x^v#Gx>0BhsV^*le=m&h# -JLny-S82A*#d4*X|7yUVKRbFcg1^3FzlbE-Xc%eOr% -V#vou%ie#)PzEbnq&-bs}gk*a=eWlzHlqI8{wiq$Uw*M(RP*_5809f&!%v-8t;7q`4z7FMREuuV -?AQ~ee^l>QH!TnL6t;0ktxKzK`i~ZDN6{yJ^Xd~3tK?nczdo0USAiGTS(>z)UWKxj>Ht&D<9$w-uPx^ --vZq+t}+l@$GJnxTx>H+j8qY=}P7h4kDFw0^QX<3y(-yE0B`9BJglZeWIjYfzUNRL#LWRX5{k1uF9B} -lwSsIX%`zs|)y2*i?0=kXcw7ZADN_dI4X{(aI9@Vn2>|fF0XQk1OnfS ->uw#KLO7JPz#ayAQT4-HWS@3u_*OfNEU^040;YfkXQ?mOVBluaJCU@1-hUiC3BJh)^5*9c&qjX?K7HeVU?0_|;G4nEfA9vqn^$iq*SFK#vp3flr?&(EFg(~ -{ui6R@Dowg~=&#dx5%WQ%nTNiaV0JqI51;bjtfsNPrgNTN?k_gTBoK?KRv12UZ*^pTj1?JgcK+}~E1Rb)Y -fithBvppF-QIabAnS)Bdxjdm@FYX;mhL*@ZSV~)(Q0@Q9c}60 -W#&fcsEX>h?Gauz6NjHNb3nh*KK?8*cUm5cd3<0)$BZByZ0a8)ULlAbYj*^kqYiC?N!%<+os;$e6M`B@2y$qZc^qi^UYxJpzZo -?GydMy%P;_i(O$rYc4$x`ZJsNK`4NQ9bBeaYj+(BL)&r5G|`BzkYU59jk~9D;QK+RBsLyx<+)f1Yord -6Ar)qtPFN5srD9o#6VG7~ncVy$m2&|8NJ@>%*Y>hdanY48}RX-Jw8$2nj;yxv!!)oy{m5abF&=iPDw| -EtrJC5KSU~C07h=H)k_}EKJz-)y?F6(AXs)OoL9CpUQwF7L9n8O7Y+gkn_&-&*6ROp*D8~R{JjiGi}0 -mk+~0Z5ki=N1n0{4(G_^_gTH%&#=xoO8;mnIfTkT5`i9MeUSpn+(?r2EdrOJ(Xq&wTvh6*)TYz~@{B? -8%;tG+?2{|qMktNY6Ct<93o`kO3(jsn0r)fq?W{D>lH1iN6Qr}w^0tiJ?F(5Rpy&j{0WO=w;hsR)kK< -NP^XQ%T7)F9C`!I0UQ_Ac9s7Y$t`5U_)KN8I;x6oQ;G@rdV6a1^~<<8vw!U$0z`?l7okyGY40LU*h;v -1YJ;lSFVJ1{jt&ZV|X){YxuWUaui-$=4X90UBu6jp>6p&JGH|kQ>A2%=R%;gJ1|>A3UL8Z?JbcNMPFD -h+Kb~@x*2ddXYue9&8bEkpY*&x&M4X@_O*ZYcM9_8RSz}H)nZ{snq0(ZaVy-&V&NUM-%W&zIPsqSZAz -=7CV%lL(%7jW&ka`$`p--vAI{Y3$e8v%oUIJ}W5$mKE?kjMF -g4!tkHyM47Q^eA-jXh2p*4dflHZEk;y$Nsv7FBvW8a(ZM-m -+#OemSU4_XVg@%6)^AKozndb9l&S_r^(^HXTg$iof$9fa!bb8EIN@d66@n*9LL^3Mkbuj1S|Acg&^58 -Lg_Oy^=vx)IR%#)>Bbx1z~k^pJ74A$wd0)oyU&6i_yKZ>x(6UJlo`(q`t@j8rFo#KIB(m3%aRlwnDgX -Ul!hBf9X4C;HHhpTz4|D|hrJp@dIi06jYZ3n&ZFP%3_HK*QM#en1|BRhAs9EEa -Q2B3~7i1MvPj=G5NAZ&omD)ZC)4KXBoiqObpjP_hNpiDzrXsG=OAD>fOlH|VTHSC@&MwZC-X=-Vk+P~ -SUye`e=GB(MJh!Y!0`ul#ay`FirUV;dFb8h3v;h+od$-b}78zeI|?XlMeo3Qi~%<<5je-|(e?5KXx8j -wCy7-NOzTfTm_(4N{Vu?f@H>6?WqX7z(j2u*|V?fwe^p0J17V+h2RyBVUi=PU~_6Z4bmIq3fLUOX|Nn -x9!SmU90}DqCP|gWW}naK)|yOV=SRk;D|zE_%Gtru^KS!o8pB45<%%H=7_L?5qZ} -@I0t>|PmUcISh?XZ(D2OtL7CZUiU6vTF$5O~7Q>)Q4ZWE`QFQ86MOvj6*^|6+a>K!+B%i__c5C3 -|&xJ-I!-U>9e%w`XtJ&7V)-Ub7n@>bs{$XMa2!=&)OpjPlC={qMc-Zr+?;G@*F}?%A`~=L1b!wMl8;X -dM7sDIg>U=(DW_uA+}*cusX6LbyRO4DAu4M;X`>GSO84(BYiT#T<#GD*`~X#0lhuf-q)_{w(2m&=lrd -*q8+>0`k)RAX`I)1e#<}j%&HYCt_71f?eRp)s<|*zE?bSk3KmpqVNsZ`;BqsD -#YgcscrBLH8aX$a-jzmMq%$5Z3v(1?ZE -7il!~>|Zlx6rtN7W#KO2jtt6^2w)*fPh74-XHppY%>}t$rnvXvP3E26ijka9IbSz`>A8xkD0~Hbh`_g -5;dp05-W$3e4k-1M6VXo754xdfi&`(8*g6Xlu0ywyJMs{!RcoWYCn*-X#Lxzot=CL;EtH^AO$4FJWBh -8SP-qvx(>*=7X{};ciFk5yg#q?4WI~3_tOJnY4*aY{;TQDgaRC3e2a%hBdv-h{5n{X>6@uEu_*kaLt! -_ftkAAkIRV8x@dztC6H$M`8TuZacp`dtGw=s#KA5SAru{>X|Q!lH)vJN&5&k7SRTYZs46K0I$GgyIh_ -uI8pn2r;NBu-4*ySRW}wR*Uk);@%W4%cKum$e93ThY;KQ>|Uj-5y0328*ge$vXz$&(Sa%$2bWy+Ct*^ -ycqYQoIWt<=FB{=^Y}IkJ<2)%u4t-{J-uDsK2;)}+W{$SUR<^3SjnOCL*`5x_kQF;LLz=&-c@?(t&d{ -HL0Uc2@FC@5=nx>08T8+bCF7SvKMI!i%!k&Uar^z-Kz*d_8u^5C{gQzb@L#|GEfBs$i^Nb=(B7U00OcRelxObRMb -@ai%pe?Yl?Y)u#z{JnnjN(RcP~lnef`cmlyn9I2EjaR4es=7!~yG=TXT9k6A9!H^sL;)K4#-(oZlJ!1Bqq#S_ -jOW=ixe(&)Jy_WL22TP$TP%mrh~aYiwK|#wOtEV-7Bg<5D8>+B?Q;zOWcj& -e+EVy!3Ga$qIiEQb}Kf*&zXV!T6l7LHf#}6+JIgrB7(|?iWA@h8;iM!h~SRy;NMS;2mIAynE{JBPDnl -*3J2>0LGmnizpqSLw=iaIwyDp%e==iYo`WOq_HX7Ir4E*j#q}QPYlT_6&9PdFr$PofzGf`4DlXe{O$A -SL{-gghThgmY@p8{)FTqVYR``SFon8`lL)H|ordg|R<1(LZXxQXPg8ks?nIU$uL&gHpiJqr{+&h?FDw -j+q-aa*?<1`x?aR)kN7zs4)c$Ff|G_NxsnOn2MqU9Pk0q&ah?94b06)1XcvT)4LAgGBYK1@erY+|No@d%DJTSQw -QsMKq-njVUn4RH&v%F*9p2?WaXtgBi)S%4JAI5AUYLHXM-(WFxcqUyquPg!t2pN0C9kPX!{b+Og0iE4cJR -3sNdsM1i@xb)Vw(Shnz*<(_%H7bi7i6<|Cb -6G7Ut&eT>1D!w!U%q}_Tk_Eihp^C8S#rXgypr?L~Xw?8a^FHPuvHz8NN!n-AG_k{Ymul;DTa=e;tO-Q -0%+n(CkEkab-Az-sG{jb;WJmFUB0jJHw8KmFpB-XLmalsBXrg`WSGu@inAhD@a>Lk-GfOf7X7p&rf$Y0Ch&wv#N0$&D+@4bIngv8t{NvG^}cu@A|soksmbdXZisfJ2MX^py-y;rnkJZStK0T#kP -6+#OvWBu`EM52lAb=vA0a%^`NcD<`Qn!C-jP1mAm#1i=))8hjPZ0r$u -AsESBGBg^Jx%}XVit>tonBAaBBjRfI$OXDw#*Xjukv>0t#*$zekfQS_gj4~$C#6^U4X~>P~TmnYSA%m -=OJ1mf#1eBZ5G`ZWo<4*<^w<0HdfkrgkJzvew{eb{r#nx?*M0Y|Lg!|;s(8L*%2708o@!(BvP5`vr{@ -Tg?sGuN$c#BTKR!(2-F~)~ESk~WLGzNG!F(lGoQX`oMS9o564o{Q&>fE*$x49Lx)H=3VL!_zLm)tvf&P)h>@6 -aWAK2mt$eR#UKm)&iHg0001b0RS5S003}la4%nWWo~3|axY|Qb98KJVlQ+vGA?C!W$e9wd=%ByI6V6+ -nIxOc0ttrS5rU#Yj4mi~32sa_L?yTwvrB{otq|8OwHRgqD}khwwV4d7*lJs!r?gVE53PNkhqm&=AK)& -F*`TNa5z|67wp8yrJQ_+!77{Y=Id^6^3EDn=zn{SR>|2D?NFpL@gRFz -@&Gul5V^S}S=fxoP2uVpc>q`iCfe#7E-ufEUs$hy3>4c~pZ;lc0ZJ@nwCkA9cW`_}5b2I0}XM;^^9yQ -eDeJKtTkdQN6$dcI!9*54gn-_iNYnTfyUKfi6}5qN%3nKm;64xLJ9vh7-8A!E3cGXWk7>MBh -o7%MZ>P_DA9=`!hJOq{hhM#shv&o --dV{d9=Fu>D(5g8Ns~+SZgmIm%_k9lh{b@8D)a#t1fiZ_~!Hfp2OxAEETxd?ix`u}!O#2FbX6}V&?$_ -X)a~f8!`7Qu{mZjl~Be~L9vl#1Xs{ybE|?i7#QqVRi -6qx!iV(YCJzHyiifkc1h;QNdI>}P9K?m3ZgL<46Rm!On#1%s8M$)M{x10rOsyjkttRt7t7%|#O~3RcY -8t?rHeyXoqNYPR-%yh&QIlNBdhKeke9+Auw1noAFz%(K*+z2&Lj0sxF-lHzu!2#J2P+J+i!BHT%L)gT -_vPryS}Z|4JKN*+{S@j9x(4m4?*RuCA~g<5jXmTMsK$Q85#^o@`(6NurDU3_s(zza6$Z(IWeO7YC9nnUU)EEcII07BkfPH2IQhOD7Kbv8y^52!s2VshsJ!C~D!$_`P8R~)gI#BcVDjw9T@BpTSUh-%*zV$*CC!tw#k3Hn*=bj7s-hggM!d -W+|#%R~s0#~o!0@c<4@bkm<5??wZakgA}##GtuXRfOxssR5FiXStieBGvXN_sKe{9yi)Ue#VVW|CLrCEo;v>hLPjY+~}kbE$FwGDO!VN(^Ms^ -Ubel8CDuE}@TnD!$ht2TljQ2bLZ(fDjIN%Vm32q7akYB>`Kn-q^KS{$PMey^KuCg6N!ks}^-2J8vAdwQ3yb(pZM;0k$BcRtq>arV+__pezgw`ZZd9ARhzfww<3EfMC{U>yq41nIeqgfoC34jUsmUV#JOHKxw)32QR4Gw$@3Nf -n7>aw>4wBrq!-8{YR+Fqv%Lb@pv((eqK|x}CGs@!Ct6~!mbiwzrX_BpiQm%_4ZaV@Ihkl#{t8^f)toY -@3xIPm$85LP3*_eYsysAG&9On6BPtYYDE7XJL%B5gY(9a$-MiEcWMRW1^0A1P{B8yZn$OSm?M50Pzqo -`XYQq@0EI+?$Ck}_2lLycc2k2dl-Bh)3uGS2jTYH^_h%#HT-G?q=yCK@9@w%fheoJAP5RPUiJl08 -|Aup?-wqz%LAzv%p)qeM~(J(KInWouBAu-DLY1jnV}x01s-w!UvEcQ?6yjc2*vUy$kJ!7r)gv7t16Mv -N(SOA>eaWKDQMa-7V0T;1jvrzEtsWE>Dn^Zv1HrQpd~BjApVy1GGjcya -f!mjY2^Vd80yM~2Uz8$zt|xTs2|Q~L)k`Um>LoVqVCU}k6sF+3r^?UzVwZ7V&80lf9LC$E3Jsc9y7eN0fio9rNp2`9cmQAIXwYq}msv4nt -PdO!E)*O(7MFWu+Uj5hq}XW6aYzXti%Wq{Bne7AUr&`gWJ;~H#9W$kN-NRSVG3eSzcI+)#Ck{>HabaI -y%b-EFMupWX;`IHs_wxyczv@tAZ1S#xhV~?QCLN;fo30#{RU9($i&$s7|%yy*IfmImDOjn0aBe)fi_j -C@r*qLT+Zr?81hGepOXhcq^pU@Di -s3+7TEPTaxEnK$Ja@PG($g9Nk2j{AQRAHV0o{XtE~!K?|7RZ3 -nRhpV{v$U>WgeVu!svflHD&B>mlAj97Pk-)X)<=u?y=L4D$mFpoBuy;M$;_N0zCSlD -fiOwz+R}V5Cl-HnkTIy_sM%LaZ -DfFmsu#;X~sm5;trV^4FP9uu%3F0TsE%dsJ1}E^qEMeOiB($2uN~2AaYCWWdvxn22}nN5Mr}~l|7_TM -S;FY+?dS>B?X^w`^*k8Ueb^$#_!(%(>ssb*8^<12Um26&cNst8XbZ~QLVJ`W%VE=++OeK;Umg?Bxr7* -$xUv!jbR|G_MQs-|29g`+FA?&Ny;sI5-#& -4}Fvb-`M2qUwKY=bgzIsY}x)Jo_YOzU#`K=DAyt|H$>0CMvLM1YmSDY?8hvY*K-VIoJ59^Dc0;BuZ_2 -Sudr$L%R4#nIlFD!1>%6u%h+Cnq*)=Zod22MAOBW{)%v(@1nwg2g{^SzW|_{8cPkvFyNT$!ZkCdEy~+ -^Tj6MV4J@157hyL4IsSop>W(Hn*&WC(8`y@&!>eyPm#?)K`PHW+Ice+X_Rh)>~}O@TniaCy|>aHs*8| -^N*Si9coU+Kw~!m*q0Ym)hz;2G2wq3K=;&#f-aPaG)V*P%{9Zvv_@YG&+m@0YWen52#L58Ut5MR$_B> -Fme1PB)TuzKQfjaan;F+*+359JJ?-aKp4>T48SU16Fm5suqNyqqLxVOKEm3@0 -t;LD_Jb^Vs_&=Jr8Zkh#dtzz*+?y=U(}~eAa%dZAHGd{}67#^5fgm#Y2Xh-{BiXV&HGqVude6V -s04gl?TFqWlf*p!)E$KnY0oFLOv^z!wETrzb}sc`#W!{LqnTQ7sYd`{e2w3PZ@)uneUeG8U*MW -H!J*vaLfilD+G;7$I9Rx>hehNj-i$KdztTlmyVQOLB2cXb)1J -)wQ+o6rMenIvDv6=;rcMig@osDu??x3r2HYEPiACiHV9DH$?YJj@snU(5-;N1y@+!+oL4VVNu(%O_?t -Zx`wL*Es>-i=-b4)!bjP3KAa{U9B2~s6?*Pj{ZV_d!$7y*X2Uoz@e?hpYl3h23R!TmBSpW1`E$|Y@JD -W@W-S*?l`p=1RW5cb%vNX0B6}=7P$aXCFup`1xjxtAq`zmAHB!+4vM5RR;#D^GSft$%)z?3sdYffUOD -o@gGEyZSf)wS8Aw-gyzaI^k6h4|i!4gW9%g+1Ms=Uu+GJHwK5#!PD4UHoK(16 -f2y4qku`!$Bvq5mUiu^b`$()ODyC+2T8d=B%)^7y~!G65IzsQxn{%UbR<*ikI&><@-H3h4W4-W%u*u( -vIV|IVJ)e1u@HTKA#@T^>(E4g}=)TpZ$^=y!$TW~8{2ja_Mkp*-XbFF{J)ndTW)nJt4j>bXm2d_hfSG -@%lT_H|ZEM}PNHk%C#4UO@FPk^ys*ElFQ_QV1J<*4m#SRpp{GW@hxabd9^cl>dEnjG#IdgZW^Dn~F7D -LABL&~?YrwZSMy9In9)%K_Wa4- -pnG?#Aal5SLEa5qL)Yo-*c-U^G+I?2#{`FmC08w`g_G;YA+D!Gt7A_i)Dml+94!F0-7nZ;En19CISVq -J8)Z26IMl@?;Y^WW<8Leyvia+agk0eUS~`a!85aK9-CE)Aj!h2a+Oas0qm`wk*uvVuzC~82@Lew{yGuv;Br!e&B3Nhmk_8yVr_;iprkJeH;j*PzUfl@7TVF~37U4Pso -q(X(GF@i6r-i#yOy-~lxSs+#^PBi=@~bh)7Tt+0syw3>)t(V%c*zW8G(9Np!u+;+N!Hy|-1U0MOF7>t -?p_$9i@O^yCK0uCKc2$X7r9sg1o$Jc)*H0fbs$l>K#)9p189u<%q7Yclx+Qmpo?|mCiq%SC^lV6ztx6lJQ9J`)=D>lDp{D%Q)HIT>d-LYLliLf6oc~K7%z%WbGUsAAzMALg0X -V6-f?K;hC>f%%nU??Az}>bfFIV2MT}HA$S(kn$nBdC3JB)R;r88xncD&O5WpJs%(pQDX2wjy35D+W7~ -mbc0Nl|vSP<^-niQJh8eCbIWQf)(U{A1`ItGAbEW=IT#c3q78gS_Gk{c^aK+z -gE);0Lzyn!fhSE!STYPjE7kQTd>F+n+YK~RlbLny9)j&)%NxsGC^NXB1vUaQW8I#7XB=yuk2=X6VTc* -N9Q`5+|zVhx?m{1!*R0V0_4G&FHDjnu`VdT<@m<0NG+FWXY2>?1C(pvT3(^Gn*nR=)z!VRm#DlSmxXv -i*mNj7V1b%~lec4?pn}RKT5BQH`ncbStRhavYn=fGCFIGAU}a)W9ZFi${B*&jx>zR&IOqEpS@Eb(abg -b2A3wcMXI_t6P^X(uMMc;R&rptEKPfyh18$g2h@+c8opeQYu`60#YeLAxw5z~IakE9c~xHfDL=>&HNnO+Qf>G7m -y2q%fd&l2RlS(0(ZCy2GqNGMI|lg4Afn`wKV>Gt8g;N|;U(%ii|34zKqR5N8Cs#9VW4HCJ_YD`NoCT}2ufpx}d?W;&*)()2{)1Ia`SECPx -RIoz3gsciEQw>=~hW8JW-1Zyu~bLw#0!&ch%+6b)F*g@6{vWN{-CtHzV6~@BkMd1e~c`8*61snO)nxs -7$OE_jl;cT+j9ml}>>*e{SRUY5WCMQFlp}J5mFh}7|vJ0!4KV7ILKZlSsU%5}t_3D7WA>1v*ZDDEg<{ -^ZiAT|Qezlt>IxvnKX%$2K51yY-0KT46soW}LdC2=9w;4cEUu|P*Jmj8vKo8~CnwHSxtk^vWFv|E{27c@=me2Y%61$KZEziY? -pjGT4J*9M|e`P6disR04T;-k%2TRmCx{mEeqW^Qo6-2)wBRNbHwVo-r%Q!fEx;SLuE#i<-_!D-@a9*D -U9=(^-^u3&~&jmbWrE|e~et0evHNaLU&snSoSX=twxS3v)tq-7<5F}Pn1*Y#RVISg)v?{f+BC#C1f5>Y&Xj(P}Fgh|6pKJf@ -K?xN-viwy*)Tg)6z77hHm`Pjblo~e_)d}dsvNzGoflHpGk`VL=`~Bh=&ySBxsl{P) -GA|6bzLUGU`2TJtL_0ED)rTw~&Q5Q1y`fpdhiG@{?J}5utI}KHC^*-1qS?s;_9?3qa>WhR5SKfZD$iq -@Mki_oIkp!3!e11&QaD9Sb1bydPum!jJI9s~J>`YR!ch;k3A)hiBIM59~*Zb`uD~JeDfYkXgBOKCu4D -1Qo*?^6pGM!bTR;i0aiyo`Xc4O{Wm+pG2MkEwe_=;h`lve}-Eb|F#My+8LZ^A3mf*vE^dv6a+*|m+P# -##fU_0wyPC+Z(*Q0uo?#Gv55j+V9WHNlNMQLP>p&5GJEkirw4zhoW -}d-NAhL_uId~&J%{!SwJWgkKS}{J*J*cWtO -9iAE;J+Q1xN{IN7`61kZH^O6Z*{y4xTHx**GrRvJ7FK{clp2fztM08*b@g~(;poEAJqedC-H -f=zfvUW@=B)&b082;;-DwF7pynzIQW!&>%^Tt^9GKVF3*0|piso8QLUi!)EwABKr@# -@eWwHTn#-Ef$s9_6^=GBAhn=sU_Ud*rDL}O>BRSza&rs;XcG{1w}bqcqiD*j!( -*?vSZW^^Uh3@&h*>d8F}*YW}$wdAeT#-(T>ANQL)zQZu+@l>RC5ZB#q2i^STf^XwQ!j0FUd3RARza019P=u=ZHKJW836@_LuLvbsIwa~fT)JR`@U0m?)E`D8pYHaU}SDw}@?O8XMu3!GjQWYzXaw^9X`#tGw~ -0&@9pP>{A0J$bh>DG7-Cwp!K&yP*bYB?|Q8n;)hdyr9%cD_Lopv~sd|k4;T1KN(2TQ6j=rjK~x%;tk1 -_98{Q{*}!9Ufl_0DBQ`bVT3Ce#R6ezeEO-F-6<$Jq#rPKo17{%Mht*cN4JIySV;W})1`020{F-tnDbN -kCQt&kXKG%i6+fU%{o=*Hda1_t4w>}rAkKkGQ*xo@Ofy4CiQiwkO^9^|X?CC(yWn|#yV5M0K;;DC()` -!ef3J^@Yb5IJN!2nQ75ZfhJ+8~Cun-R0zkzi2y=u ->Kyr3Ye$IZzOL~f!yjD#YpRP@qJ!4Jpp-EyE0^|RlBd;M*44P&QW{FjsHkg+ZsxajQxni{O&dw3w~UQ -c=3jYbHR%L5igF}kmb)@1_Ne(_u1(MAV=bWz}U -l@)wovr7o3N@_i~^_K-K=DHYaf`}T={zHLQLW|cY*FE`0$`8H`yzAXrU1>IF1FQu8AT!Z$v+|p_Snl& -&{odP9SJm~6KwG;D2_+Nf`<&)_jT6J*2djxxH7GBKrHn|GXT^aber51ZnE%V=#ZP7j%bo=NwFgDyI -0&ZM@<#-#f=Ug6&_?H$MY1a{ccHTOUQ?sW@cDAe=q5ikpibTKAo9 -;X1MSr>L>;c|LrS4^|_?!~I-Nu7>sbUNr~f#ltL+FvO5&4fG(VXqlBC>(h|(f!slUtVPR}apa3vwCem -Pqg(aYPv}{wT){#IuZNs?KTIvEvT`l=KtM0{9Z~{Gu?a|)c+Kj*d>agPw!An$xB6c9(xtxLjwB5vuFs -X_v#X?;I#m?P%UFP(ZJ!{owTeSSyaURZD_$Dn&5pmXpA($BKDgo!HaP=YEIwgogrhzK_M#NDu6l-f6~ -`D~UV#^7_o}B+cE(+KKxPDrHTRd8Qy^wP!tr*fkA)7|JBS5(S1tQ;)wNc2tqso{7V7)`6b8B>8TaFpSIXMiBQ(cWx4Wu4{bXENf&qqC8 -KXb3ov%twLB?te5VW{?AEw9OE?_H|$`PpR8kX}B^BsCik`T1PbR61Ad_h>ee>3;>VP@bcQDzF?GOW&R -F{(wE?)K)-!_g`32LN6~b_o^QPkH4=!&7`jj{?F(}Un`SXEO5Ge{nLF5Wr~+R -7so}W!ekt$*sLF^i||EClSv~L00=K$?(fMOwly^VdIhB9&(1Y`a#k(l^fM8f -hj3nD0pnz%p*>a;o>EuwJ})y)avhnzezDw+u=Luq^?Vll -;Ra_R~R$EkoU(w)NYgbZ*~~U>Vz~wLMmhz%OCXnAlB;g0C4}aIaqQ7+NsJLd-&o?7gB$e2;y>YMqbY$ -$Q1Pk1rSFYxrEyM7Y5FY@$;Pgkm8UD_ponxXN^1j|S4##w@mzO?1N+`$;mK7n5o6z*KJ$rqzp^jdO)u -vE8g^Z%Srgg<0Zu4d!MI<|Tx|({LTeNnuuTEw|FZBV5ZI8u%{PGLr@bP#2+*4*;Zfmck(BT@Ac`9JV+$f&;1l4gVmL2KNem -0pC|m2fQ=|jI1h=E<0o@Llw)aq)!>Pape+V&AIt0jxV`t&DpAuyuVl2?X4C4@0uHiHxRgahH>;(x+u; -8ie79fK@3Y9m48n8;PrQeKapF+lt?i*0u#Ypj`vlJ(L@Fj4Hd&&mgX-jDVAw2~`8vEe(hL*aCYn9Gk4 -dvx3ZGI7*&zm}9r|^~`0W@EL_5)I=fL{Q5Xdkd^MABS_2&eDD+~j -ZkayX14poh>h9m7#6r{Ex+Mqs{^UDNXfW0$ZA0;B}!QxZF -#RbK|)Tgigc6;XheS)=N{McAKJN;*DOZa*ax^Kj;|LFAne0Jtwx+fns|6}^XJHf`6>Do6teXqtJHb|7 -o~)`b^jcBXdda5{!0L$6lj#1RYEiDO2v(j`v2DuPDW)JIp*3rXav&JMf(qKp_2wZLr3^@-Y>QtBM3ySIH_b2P?^EgYl_|w( -T&kTZREjCvJCmHVTKe$(R?_dT7w0p4su(f`P6>Z6py_xY8A}ik;A{e{@4|^f+px>Y7yKGA#+&^&C|SD -<@GMYLb{Y9>e_lME3E!3}Ll2zV{Gd#ij8 -Tj@!GV!ed);>vORaYCIIxwD?B;xR&x;hbw0X2TG7IW59YBDZz^N^}l{sRNR;7hWnn1Y8u6KyPUhmM?Q1)i;l_(PrAo~GrU)_TN(*FUjb~LU=)+Ee*K(tf}1^T&Be(nX@Ai8FD#J&wJ -C&r$np!?ESFl7(CLKlEB7n0$TQm@8*aet!@H#purh8rB)e&cj9&n|Q_?eI6_b|MS*HlX@kmAq6Ulrwd95uVU6Oai^R-dC6Y|uTEUy_p~!x;z> -Mr4OMlVsGqQgShGi8L-5#okqTS?Vubb?+7E9*#b;x09vl$hq&DQXzwv2Fav+d=VUw)OzFQH}#<&z8Ed -lNIZWgA*;&U=LZ;FD9}^&mszScqF9^slT-s6LY|qjuN4KsuLjVOVcgb6$Wh(@TB0Tn^H%wYE^RK&K2UL5vt%Lc&QHuJveX3Ynl -jH`I-wZnUc-6IbH4X%)mn>}8xRiDWNisv_^wiBqNn42}fw(Q{%1+C_o#5pP>vSWREpz-KirPMKnB+iJ -mLl`(x`BcFp&c^Cx+wQfy8gej@5KcfNc>zm5G8X94}fnt84;^F#rG%P=dzrmI)Og41}@6QtlCTvJ=TA -Ve-7F?RAWQvz2Y_Q^MUT|r5t4%SrGfau&{f73$Cf=#{kjC+z815I(`X87x&!dtN-$C!GFIo%B@0K+Tf -0ew!2gJ$x^T=<|wn%O#P5_ShGs)x$xD&d1kO2@$IOCW{FT39o=;J4P)SO)!>2l3ONF{u#huqkSySWG? -E%M0}8nOZCivT+6l<+B8I0F^@<*H#DfU~wL7#Y0t_$??#nv#!N|Im;yHEO{ivT8MFHxx9#c93`Jmmie -{;?QMb>`0K`0*G=-92ykH;x>@eJe;T;#=NPrD^7Vrd-A%)*}PF(!B%=GOIE4WvZqhNV{9Qyj*!;|(XJ -C85%5|>~RxAKG5s&J8+Zv3A41$T2xKACvna+HGT<2et=wG_^K_dWY0mviB`ctG*Yp~Q7l;%P~_t(oe)k}q97*E9S63)rz$ww$My@FR+7n`P-osRJ -OJ}iDSuH$IR5GJWC4Wu12l7v)kj8H4L`sa2|C6_d1%)p?f2s-8errTjamU4)E70;PqO2 -`A}UlM0fyG-&5wNjH@-FOa#10)Nl)%CvQEEyx0`--)B;wGu@;=y^HuSzb)nW28vSH9NB1-?K5)Pl3qz -yS@Uvu$zi!D -lDw2!2$WoG6uobhox|_kX>5m{TA{KAOO=4lJ|1$C5)v%}qBMfw9(2X-Z@;t;8UIFx~C*>dPW~fb%$&D -}jN4~r=@+CC<5?twyx>(fYo1cj11CIGL*`%k5PYj~8P-;%RM{H^(D?kD0NyCCgLhSH$C2R&>R2Crtn} -*2x|EyVc%c(nq1fB#|eiFAM@QQwf|8DVo(3W_g{3JFFBI=5Ah5O|v_iJxg!P_`D+3+?D<6g`~Cagf=P -JZ&0Q2?G{fTx!ufVvmUBMXP)?Qi1*ke>``8OtD}Q-1O=yxo9P*-gd*fczvLS-*+OUqRGO)FT+xslS~& -f`wsz^4Sq;nQ*0Bq<~c8L+r5}X4jiGzp_`LP+@c|IjGPLM$bd!w)&3NLidGKtW93E -W;pk?J7NZoWQ4eXr*HL4PCRPs3G-9qtE_C$sv%J!L;>W2DYv3dg`4YDc7BV%s2yVMxTLjn9oBh!`+z4 -Gs@7-)ezVjb{gPD>`4K}p}xO^>0Zku5No=glFy>^Yh3g@%hD*W2ta22+aACJ*lfI1H?(Zu#O9#W%f?B ->jZtTH?z#q8AUcCWGEHMmo67qd}_kDn{T3t;GF|hs1 -}dVSi31;%13il~R;653xic*4_s-9ff%G1c4QkEhusHP&%-Y$tjc$>LPeC0nxbXuaHVFZMDR4g(=TR<= -Rd5wDr9Z76xk(Wm1UPj4nWdykO3A``VdV#WR-zI$SbmG9B^sS{YW!j66zR)2(DHbC -HBhSNWu@k2OKVB>@qb6>dVsQlxpg~;z4xCxEh#fc^q83_om6MSub?nrp5KhRQ`pn^4!YDn`X$b5bg+R -(%U&7lW^Dw(S59hIZQiIVOt5KWYSZ!+Rjiq&v&oby5^N?JO^ymv!JN44kkJQeciCJBipy!{lNqR0EP0 -t@uTAqZoJbUm;T1Hn8sZ^e`hw4^poReBNh-b6WqJeAq0bL`}_fO?n(s~#v_^iAn+kVRNj*tV)HCXXxy -?AlL26Me;zNy?z%^~~-Y7j9MWvl3W$Z$hw8&Sj0mY+IG1zod2$qQBw-s?96tJ!!8E4C=J|mqFUW74TCd#~tnKI5`!HF#;QpnjJ$8=f -vV?v7VK6{3IWS$Sl1ktDm7}2RRr^jmPtR%vb>U=wlG{S#mL5X9qwDMV1y6z+2G#VP_Kr+g!F^q8+H%X -a%|a1xn_vc&UeOEQ2J}CDnEYPVqiuHO6C6tUKXmiVm2}j -qNG&y1FbxaN)%C4ds?s*xw`l)ET^_pa&^&C<=Rf=<*4h-Ccr^+$f?y_Eu -VZ_@#~hXJ}NfX0vV@bstA#t7F%X|Iv10XpzV4~c~wK*<2odAt}pfGc{0#DFXk#(J93*MARD -v)Tl3BcJEp}8fx1C!SWjPyFsG~ZDupvprYO623MiTgzNp2GQd -qL5*n(5eONulJBByadfIWRG%3!Fztwv5UULP(Z;StK8%dEA)$Z!U5kK5P1-r<$+~ug?^P#cuC!ZU52= -^$+%~HwmbvlTMHSyc>m>nz4QHw~ -mJ7{ChL!2qVYTWbf%TR7zs5gs+COLKXqDY2Ocs1j=;hgWE*=X2m?-wF-w({!{8<9GII#MtYK&p6+ -edW!0n!&yaT7s!!dlgWK?&Z -YQ-|n#aShh`YDk(Az*>Nwv0k2Hag>(g8O7g;RwWZ;1U}a -yJ6*2&2-5PR><$m$AX>v3iw}d}SG0oMC>M^sFBD)6LnAUPLy)PLoweV5y3RbYkd&TPMx9h)1~~doA=P+h(7Mlp-w@< -3<5XaVlwI+$5MG$bPL&{A_5Cl8#_eJZ4i%#Q_ty)rS^Tbj;P9)6@mo%H?%OB4|d?r!96~dmpl?Z3mI3 -)PPKOZINX^(BJxLKv%TH>;!^?oc+YZ{bj`&Uampqpyge0jLU@*ja%g$A=ApFLyJZut65E2YM6M=@UUZM;wJrU9)2 -Al;siuoY!TD?4|Kp3~a@H#^EC8W<^K7r%Lw=D(4XRc@kTAyKAYV)9aZc>MjP_WmXy7cqqppOEV!aurL -!BI1KEmt^lp)ZU@E%@`a-)ZAQmHJ+QzOP{czMeS)nF=tU6w+^d`Llga#O17$fJ?kB -Ej6Bo&fHj7k=_TzU*)q@FK$`#hWu*lz7RLu%w+~h7*NBUGix-y} -tkrlG>6adEKMmU=1(`yh(PQCcpkr8wot?tHA)8k7* -jKz3tUv8+^HA4p;D@Iv1VIF~lKXew`(qsn*ux#ZtbQ3SDeK;Ve-!SLCe -;`XPLWsEj)ilzz$6c^qE9)zpBq;VfAlzWqzV6rJ0(n1B)&u{G;p?)SN!!;*x2-3-QYSO+5T?hrWo{0< -T{?rVOMnNSVL%e9ps@^i%dC5OEYMuQ-@>KKv<8&K4Mh!KRMy(SZ=Yfhpgnt_vjB8Ap7B32> -P-UezsKRUumvh#Sa#q$TS-M`Y7CSgP(djIvbasHJM_iiPSwFjN(eC)<;&wdbE0s{V!mM*-z~<@Z+UA^DdjFzspNlv3p3$0)`ERp~Y>75yAig6YJOXrtWNV*FODTFK; -Ca_a*N1W}?@U<3*a7n(869`H?}FTXD7UaxG1CAB&Y21}h9wmb@glAbzK`#OxT@JjF{Lp3J^KVYshUiX -C}SD-!$Ta-~(gh@KxVqUVRO*gZ`?7QSSm4jrBw<KRCKrmif!{|Y7;HwU^hmaO531sS?Urqx7S -c2WhvQIyzI7#AzK=Z46NUPtzj-VS+1rmz(2H1a-Ef`&j3M8BFjNK5%U=0d@o6kK#GkPhN(gykegTs6( -k#`k(=JbDN3$N0+QF>)tKTE3tHbo*SzR0LF=8I3UYDV;zBbB;6gU(eV1nb)4Peximw^31Z>%`M5^s|e -4yQ;gcX_#T!#J?)mr&^ZDob|#!zRx)iuN&3|08eHT;AMAxl2g--%N~{L7G3ZIf%Q=*;+gTC6aEv}rZ1 -eK$!6;~=QqA|E&07CupIROQ=Cc`zMEKgNI4C%Ix-$L;|^G2m*6L>p|hM+mNg6 -c_I+8iWzO_oUx~Rwj_}`WyVKHiF`aLqn+b{S2TiTuvzr>#bpF+LQ>L8SR;jiwPO&I69ko56D`CP5FixuG@#d?C=&dUnZ3g&KXb?KO`+hlItICm -6wZ1#ClSakTW`M@akpfo_4ghqL3v@Ettr$EAxw|c+<;cdA(%6$!m~nD@k0_iNH*XmSl{aMF&F#%_Hoe -+C8@C5Z+c<9T+uT73Ujs*kO!C*ak)F^wmh!z4M~7fGbtz7n>npnnx-_%2^}d@dpzm!;Ya1tXt@mZkEJ -c+s@pesVL6&)YyvWfnw!Zw8epC4UZ6P| -Algz>nw05E+A9QzBeXnPx&hQ5l4i3E^G=xLDJ&!g-ScnL`J>%&xwK>)c}ssml%-6A8`U*%;d+^OBv{< -Fl2<0kL{t9(<_FDK>MGH&P&CjbRm=OCD{Dw)b9-vQ48jF*^D}s7}10GKcbT5luD`-R5GhxY>cP#zxQV -@tCvhjDRHC}3|xf3OgXi^EajSX<$9!?R1G*lNoR;&A9#==Sc^~73W4?~2xuY_&;*#%e3rQ{1qmq2sHD -})j0OgS0xT`|LljkM`}~$VM?0+ef9^&^Dues5YQ@Mj>g=Vqaz_)8Iw_LVKA -t$V+3-w}VbR=&J?5iRN9!eZKrJ`3F1Fs8VRpf^k6CN~_tH_tpWZivHS`KV172`5r)N9wDYeNN!guD90QR(r&{T4{at((r?#x3!_P -QU$Yn*Lt$SB*mYT?5KGd1L;T)9ep%8gQNAzViZ)?^)ZnE_Ps%4sPn-Wn|5tU7q;YnD~vq-9l&6xa$m9O0y8RrO{kBL) -#|fZJAzVT!js6rX!2CihSb??Ft)VrW-&@X!@@RTUMwUDeyvKnda5*uiAj9Q%DTY>54e+Erl~$uahmWR -fu^L%8Wu!mf&1Dx<**w#6~T1=kFh*iQ|iWA{@dC=8+z)WNqkBPg15JL)ofuxGKkda>7xE|%!U=Fnm(4 -q|DvKypQaw3IJdEagtVy6KXSFKfEAhRMO -EQH%k3vLmnHa%fLYpRG*&)byDyB1&;*eS%#8pKNop`tC72C)PoR&g!m_QxSEb_Fn}}6&FwwpCf5{F{i`j2bXUVqmxDO(f|TA!?jTU9sZ^^Ljf-~Ub-0lbeRy -5(V5&wcXnnPpTG1>^SYN#c1J5s})>l9}`S8b9RB(ZpLA-7{lot)OUe<2fYjhv;P(v#0u4YK}b7mCApJ -qlT3+cm2G96XW`vl9Yx;d2wXv4k_Qv%xIb1g#&iz>~upqtt|bW=O}!+L{aM-#?T(?L9ic5f*Nb1m3AY -`=m=Nwm~&sE98_OAhT)uN|TtWJg0NtXGI%NpFK=~_@4_eFM6K{(teql`0rH6%wv?w}G-Xa=CqZxcQQ@ -%nCsfuM*iD~!!uQ-3>&7HN~_VR0AgRDAedvcD -HV2glqnVFqNY@|xKXB5O$V@@NlU5(z}GCP=y+nL;IKE{(HK(%mca1x -V)>Mfe>T_y@HPvYHNElO5s?&|BM$(0DOhuQ}pm1JWWK_zaMp{x~w>3+u{_Ay1DvdOfmQ;uc&63J>o|; -i5=9EM;OMB>a{}wf)datRdkoT&T1IYBZ-B9RMZoji0;;DVq5#f9RCBb8HW!4CL(qZnWq~6&yAEZSm{o -;d0uCylc0M0`|IQksWrW@jL9*hH??4)U9b%Qby&o}kA0>z3)9AmV%JZ%K&1uwd47tn$WbP4VSTB22$D -aVy5BNv$6dH0YXQp+cNQ>l&6Cn`m)d;ldpc{>HBl>nQii&AJ&g1XSGYK|5(L*WOo9%X`7wEWKQ!h6WP -D+>RscmNqCqm2(Q1VWo1llN$-_~~V8eWV64;P$7`??DB&;fvUAB)?m(Gk4*ZA{xujQTJ@SvZXW+ZRP} -Mrg#9$VrpuHUnw(0Dh{g3L@fhM@NU{S3aylDrd9aDY`R(R6n~xGsjp#eMeh|@6AvIKqLx({AlrYAmQ^ -&aMy4x#o2iwG0WBeZoE+RrTaNRktuxke;Tr{KjZr6WB8Weuwo_jTPUg#XPG&0aWH!NH2mE~ie-j}7&h -^fbE+Bqft!+;VHnRA&5lW;|>4i9YpzBt1;)(NV+$TpR7Uf+$t#XqEctnXACeb}V-6y@KN(Rw4YTo7K$ ->}asU~hYEgaWI)s;QKnuZ~n;pS@CnJ*Ti`-VoSr}qq(?0xkgs;7}x6Y6Pw -by7WD+E;3IdgLBC6|^ZnQ+xt+&s5oEHKZt|;gZKsn^D{Ry8HRBvPh1{v= -6Lzoa9nNji#{p#P@fe0MtHG!d4v~>UySx* -u?=(3*I0l$n*g<_KsJdHb7FA1r~xP^)sJ*yK~0wLqq<<12ht9ASnrcYyh3pB2=o9NDntO{!!?l0GZw(96pQH8Xpz_hWe@yyj<^I35~_gQf|p_YAuJ}W>adk))8Qn?oVN+6<1b6SA}InrrG=LoJe+G)irnyoZG{+ZK?cc+b -TT7fh9e|1_hJSYAorxgkM(4AIv4t%52ir1%(a$4~>s&2pgHK!GyU)G#fJc{~S{*_KEKyt)-zcs>Pg(u -lq7)7#Rs-~fBq#D|l9xF^qj}@jUWooq9{UbeAVEO4uD$+Ew=h0mCW4SagR}IEK#M8~Oz&#!oyd!)R-0 -kSt%<4V~I+`zPUMhmW)6}Z>{)on}STv|ry9U0&OGSK?mkN`vRz(tCDoT@HD(Ijmyi^?0Jyg){kMdCA9 -jR7rPk5;KYeKC`dZ?hqd9UzLfel5y>eNVx5qj0_qdZhB(&no-Hem?6(@S{)z1tLV#wd5(^?6m?B^DlX=s2lcLQT5GUD;7}3_jRuoi3zTx-9x=rq?p2>hMm9CX$4 -NG{}ZPbrw0;FD{!GsIIS4=AeL}ifs4|x2ZrK5eBEgU=1DlMI5BXg(~2KlNDS|lPAjmM(M~Hq`Tf_OR# -3+FO-?IDFybqnR=6kr1E&?Vd79ITb-(|=JYs#X{vcRYj39 -gVAzshBC#Spm|By-x~gvS>SyQD^M23n+u4ya1v*^?i}b1q>IyQQW=4Q$^0xsBH!BWLAhyW<|4;c?$kw -@Hg-KPNoF@egJ=)MQ8Fma(L|uXfxFwilrFn1wuxcNKX7VDQ0hgp!OlT&lq$mj-Lhrh~E!GR3{uJAz+q>)_ -ww_H}am!UczvTdO>1UhOm3@OJ0Y#kz%$Fqs&Ct9=PcYtOJze^jxFp&?8e{sLXym7k#F0(f6Q$Ibqilf -7F0a-!!g^vD>$KKIE%j3^Rp>$6F47_{(ZdfK-Vw4?t=K2JA$^ylg5R~?7H)ahc6#Ve8@=RzA~bSaLPg -$LynH2ihT#ds)>U(j+_RjD80Rrv^C;y1dqk|Aps0wknUsZ2G7+`wW2SMp`6K1g{n{CXYPX8Sd -VXywtCwim1sCJO=!|zxxdzWf_!=6`!SzY`OS;G%rP3 -nE&x4RU6N8L6gNa(2l6K`a%U9M#RQgQ>vXMezknT#HNtl%k?4ouw9yNzKN54^f#BmPKe(NIjMOi%k{A -m#@#Pw%SRY7}jnGu$sjAMyA?wOEx5cf5rE0@Uv=-$R5l{r;AF2i@2hG6-pXO-O~U8Tl>`X@FfKhlwi5_kY`HuyHca}O={Z93e8MF<*iFQBbHtN+BV8ZrB`gDSgXi~`nO^fj@X7 -P{)TOnt-BK2CO4Li`hEkwi^osh#qtx?^(>KPRc)@f_7C8#hZ-pborjj2M@%!BX`WBL*IqAfw3&o_*_m -COXZg_tKBxF0<9yyASjflIsh^)iX2y5TGfv$feZp~phx|68ne=ikH{)WzWCid>IqWU6!B -P;i24BVNy^d(ZSIuD>3a>M3pJ}BMh?@-@r>y(}_WF`Vu!;|`co`^mhDI1PV>@6}RdU?}(vk;20TC~?{-*zt8F}-n;-?rUB&0>lSK*y^WpnYi#CtQoKU(}u;vL6VsVn|b;vGMy -rYtEACf@NHqO!2~>BKu;R#N5^|1k01hVN60_Zao}Aiif6zk=_n@Qzn+)b*?Uta`jg+Pq3h#SSjTm%0a -*)9bgjNe2-Ya_oSLtmaIT)#M@#A}?Q{F`8zyl@IbM2Glu)FDRD@ze}fMjZ}<05-9#1t%NGY?jdf-(1Z -+T88R3{75UM-l)<2RTB%fXD=qq^p -?t%wkmtbBc;lJm#eK&ywrr>X5!Ou_PJQIG!EEx)57Tjely55_=oA-v!Qal6T3i7g>{5&OKCOUAyr%3r -gD2D?s78jPR)9AoSKSTeIQ>{TH8`om$lVU4i!LkEvNXK>G!IB-87KLTbc&agkL~O=Jq>O2B#o&lWU1F -sJt)8{ON*O?ol$ -)wJ>x8Istt+7K250?47i7xp2SaWEoEh81E5ei}rY$HPd&(uzB^^K%80Bn-;cgLsv!sw&vnOVaTp94@pDDu!BbUw0SYOuMMAy -F-qLKQ{)=oFm+Di<`8gyNsS1$9eubJ-cbh>%jt!Tuu~Fu@gTVBGFHX*h7j*yRJT>>4~MQ%iibsSo>g5 -eUV*Xx(fBBt5E3i%Ec^x+rZ-~h4L+Kyzz`bg5b(|7ymG{jhpE+ND9axGAD`R -FL!l(6j?!+&MD62h)Lp3KC@gWS_g}9oN4PkqaX3vgK8-1tW+RU*coU7OF>aq7Y`i0bk_dv;tEpT42%I -%qB7BNh?+m_KIQQz(TC#88<3pI=oXRgHyS&1gj1p?}G*_u3Zk5qfQwRT9VjYnNaHEWYjK254U(e6@hN -_y0Fg5Hv-RfDe6Zql_#`yxZ&;}nM3kGm@YmreM;*n1a%sH*LMd=CsTDms{2_)1h%3ZJ0{j^-esK#(X3 -nh!)71VW%;2DK}K4wU1F>8)GZk!_=G{r>se|`2jXE-2`b-Um1@Av= -R18ep^`?=QMYwx}G)wBy -p@b?Py-`RgFKR(&PTJYJ~oO1M!+gJc;cdq!5{Zm#xpPQKuZ>X_v>HMqidC&BFm-3k00lG*-z*yIbM`J -So1byw6GoC&V(L>8fj#Fs2$4(#ciK_jZ)Ta+;Cz5PubBTp8Z-%J%nA+{iDB!N^-w~WI4+cs$wqh#y@c -16j`OeT>CcH4ug%5=6qODq>KGZE1#7R9|j;+?MH&25=oQ%~xC^;diY_1a&78MXJSbUhF`p?)7~kN -nj%p!s0UUxk{46N(gi(f$u9<*%R{cOX=+lRqwv=kAY7aK98kP0{%W!1YV@*rZZEo6QEV*PNZdmO=A}z;YZ*)>VP__=#uUzJkrJ9!L@7cMEHChE`P^ZY1=MN=MR@Uek;O~%chG_obnH%{NG0 -teN)`8k -x{sComJ3<)l1O>Q35dirHb<29DNtb=Mt!o%z=JY_6>T0UDXI)>gAqtLEpyc5X_%-aYRow&y03ON7R1{~2iUpiD+im-jg -~L}vl`n2gmBx~lSf%tVi#mmJJhopcHDgJEIFamO%ObuomSRb0N#UupZ@OraoTIEQi!l -oAkyA7Im?5>+Fc5|sjD -Eq##OW~UvY+1zLB;pQWv<~a7X;L;*&*#4b6qXhY)@hRet3seTu&+YW-nx;Z?TGVDBk<^R9>`pHNjI%C -YpthJXe=$zsuyEO0klwJ*u?L{~zr(+52aj{EaY=&GnkaUbx1;R(*0&4+r1;gUZt(yLmJ<*ebZk2F+>9 ->l3{M^a4HX;#6>bc+v<$GP+Q|E8GgsTB}**i#Wo=|)Ff36h^FjJ}TC!x5|AVS!_ -3M=epqXyTpPZ1Wpk8~|J+dgy_)@i@n0}r}(O4DnVn~hIswL`h6-`qt)AdM(tPY69xlz&*^DO*rj;R(5 -9-IoU|^wd+lrT*$bp~^#h3y##2Yrhs0%0u)*$pQp&0~Uok8Mo=6@V0Hb$-{1d#>DL&)!&h$aa-%)iWv ->=kD(~arFc4q~~tqw(yRLNycsWdxw0STo!4-o! -(ttg}$z6U(;o4E7uf*=NG0c*4ASj@`3cjv69f@BGfGlu#Lb816ANO0ed< -FrUr=CEbp{yyn;Q8dtVH;M2XAnx>&eG-I^L2Ry>11qE+muMi+sn^?S?%0tpp%J&_g?zjOHwBgp%5h1a -5**r48b<*B^6e&*(V=IAt~ZPG`NzjFHbYIc@qH|Jh^u0v>q2%*_J!g;7C%=)&_q{4;6$k-`lua->#k$ -LB{PwMev=cZpv>zV-z`I3MW9|T8wwSKyJc{z47ZJNEDG$;ZQdqZVE=*vWkU;GzMGwKfxfs_X3=IE8g7 -yAm5JPJkYbo(Nx_i24QMOWDi_Oyl&87G`;vuv;9&Q2P4IP1^bMO}uzLy8DibHXT{D2h&?NL^ql8@7C3MlOFSgAPUDJguFSL^cY1}aN7_pD -gs8u~9gohQ@EFR47(J#2k<-I&^)P-dRS3BiX%yv|Sqb?*RNWWAnUu4*Jk24k4EIOD@byOYT7^}awpsl -MN7LuJhhqp&{Tjg549vlUtl!ZTOrCn})`jc|^Xf*EVB -@0|t_n0L(OxRR^|rv}I!6T*%-$nA*w2cKj>?`3)EF+^Tm0I!#hwQpH9Tayqk?2@oo* -#ciAk(AjwK&>sec~tK&3+KosxT3Nw{F>W#)orU&Z0=nV`Q{y(O?L{L;b-#;y%pMl4J5UB;fi8INI7N; -%3j7d^Tl39*p!a;TjfCB#@E|XD6RHjXg=GEiZZ_LoghKar8vw`h)s}XkYXH{b&LCi#rrJ^2>Cc{1`SW -!CsGAZNXlx(^Yrp3bEV}Jni^repGicq287PkQSV!f7s||7k}i5Vv5Qf4FNUZ#kEq}EX_3Jym`U_-BBy -eEv`0y2=^Ex{EFFQpvbPj%Q}vq-pURsJ^{r3nYpaot6yUT5jTG@UE+;-p#I5vwFc)pG!Lp~)ouWZ#jl -!L-_2WYly301@_8_Vh+_L2x^6{l}t|RCn$*gfzUP>eP-L9Q5Jr7lnBl~o6!Dmkt=1|q4V~2h?bfnE5` --MyAT{RS{BZZ*O^wM{phpg>P6mGu3YBW*Uf>!qglSkJ{fgMARkCLQG6=tDy%hKSv*FA{BR&|s+fPXl<+=bUT3;u%h=@u%pJQ7)m;*#w -FQllmv<*9;An2Ir7UVRxllXmm6#YdBfzdtOsw|i_5h(tp*A@Yly$;V6;Y5ZnHP#V=LPA`gby^NM1offC+1Dj&+2@^D95*S5rK{qWIbgSor0B}J^!3BQ -xT+j_%KrDczxT3`)m+%^VkUmsf_wC-hzV7d}UsrrbN{`vzD$|cn$YP1o(OqH!g7N_+;DBzT7~4btM52 -)2^Tap0wTV*e1!`fPaTnGrc}*17Q7Z~7P!v|9VyYG$Mz^MlDM?XLI>!?$`+BJhQYkP^dXsb$Twlk2isU1#&>}9?<-xiF)^oy&T^i{-VZ{+;>6oytf%TxUJ_YLs!nzLDw}kaMSpO-k8)1D -;ScPWhwZd8s>q=q8U?n+(6@9w2Kvs`W%UPcNLR%}a2LBa -~*O6o4GX9WGiDzrBH2y2a?KNr>uf_`DG74!?MP-S;aSg|@L9TZk9zDpknE0l;xZ{;c|Lzjm1vheVOb- -l25g!M6D^@r6ZtT=rs6$@(staf1ygf&Z8gJ4Y&*8Z?g64t@6#t7?BSnm?nP*_8RbtJ4o!fJ-KyRb&Wd -Xun5!|J1=9EZ2dIq*n;^+#cy1nalLItA7v!a5z+{lYp6*6lfBIytj}t}NSBm$%7F==}CckeRn@fO36xe@Y<;}ZqNtj|LCf4>*dJ}FR-%Rnz}7b5sbm&XX5eIOy#cSNer-lz2`TlWgdZ$!9< -y)!wsACc;5_aiCXv0dN#l>K8DNtKnWx|Sb0;V9QtUfM2Z!2ZiUh*nDGdm@k`9lx|4i}tqX(p^}zAdip -8btlsQfYN{0QQ=X&OjCZzkjF{xbrl+16|O^`J5ARL45nJ+(sh*llFR5Yt=-fw+dCvDIlebKzB8Jx%}l -X-Rb>oj?9whlOUH^T563s2j>`t?ZRelofq!AI;k3nD@N`t^t-g*Eo}RmTU}?u!X#=11PE$b6JuR| -V2f0)dU7_o3z9%Y2rP)h!#RTRzrGt&+n~r31lTI -`}Trrg4e#JVkJv;1hy<1Um@cA=pCTid=ch8Ts%jG9@x4GS85ChRhl=YskDn<^?iq$*d)_j?B6;J$Bfc -XN_mAS{fWg`x68bgu*4tFf(OOdF>{)hTrlSYHQiubzLtj`DqKGxEL -k1(kH%WiTk!kKicgeIVq*HgD-UO=+t%|a*t!D^b$m^G_x(!K#SI$fo(l^NzOW@YtYLy#$))vKZHPj4Fr<7X&- -LQQV1v=FA-#x+_%TqGu|-MRFUrmZx5_B@nJ(vsvcXYr;Y}$p(Sp -8k4MB2rZz=)+d8=_vple$*-QT1!bWy`gwzTltmkNwPWI2Lo>*YxSG&0jKr&slNF=5V{^e)vezHsq68xE9#UyDG-P|qfUgBI|@(ej3 -u9m3D^m{PnM^2>Uqw?qE3my>z}wX04F9KR}8k+M3K$QaYb+M>KZ@G8UKlM+$?gp-{l&g=8QkMYg`&+O -1SiQwKY?5I^&PK+D4MaQ@n=7A200eia$;T>PnvOjBIbNrj`cw+PhG?~SMX5M>yP^&{$EAgoOW!Y5HDH4Y%nfRNLuCD{IMMj1xK%|_@GYzNhQjb -VnnKl>&KsBFsFLei@RhQ0dPl9^YEP6(gURIxy-zhA$&Y?RC=))C@8PQStTa_xjR`oWz+Y7Kqu*Q>;YY -4_3D+vc)i$VtdnA+%eM8||kvX-EQ9=X%=T2|OPJI&pe1kwH${-(@laDrJ>9Yc)$s!%xqfsAU1TMX3_s -a60GPBarm*E5!l8%M`FudT6~`fC#&d%Uc|mj!cbquiXSTymFct% -N6BIlT}Ki>PcDWk7HCcl{N94WG~Fv(h7|yKpYdn4M&ijU8BS$2^(99dw#HzAhgpzj -&+%R&-s9Zw8R9)becv#JZ1|2NBSB6PZj;au@Z*Y?rhg3V -4#UG!Lg$1e!S-J#%7?DFY~Ny;fwg}0lEbJS4JyIo?NUVQ@U74l3&Wl! -qc4|)dw|Lc~pPm+KxMl{Ykt$CnM8NL8c!eQ{BsTF8C+oHec^_r*Z(<<~`o#dAGe9Cw9g1X(>0BE)?sJ -NXX9>@0*p7?u?ARHl+%es8J@;#Kmq(sApR2!L5$V9*cb)mp$zr*Pj`PPCyvWw(^T~%VKxu`c|Q -;lS#eB-QEU!Yhjel)ESk^Y4)~g9M7hT{<1ngxq7g;d@+e!vzU7EV&FODZjhN_UbuSEW=@h$KPxL2?(V -pDqF`^5^eNt@HHGymWl^KjLxnh+rS5~?S%?PfUHy%e+rYht?C#oC{h>lDj*^-77SvxB>6dSmMrcy64m -bWY-Y+THbuMg%!PW*;yw+D-JqFw*1D6jn65Dh%eUJ --+M2Q3k~$xbMUhB=|Ds00lHYT9M%LT`cSdxG{9Xkbq7oHYHA|lf$TiPFm66I%nu(uaJ4lY@N?}TkdMB|E!_$2XN(2oxlnI=t~si{w -NXVitZ4HAA;L_|oPam6OA7?vV^E>f?;F`bb*n|`E+y{B{H3A>fx8JjLIobchGyVlB0uBO%U20419VxoTi!tqvuu)asH ->eb(imEGmbfWo4_Gr4tht_Ei{Ay$FlU*ZG}h*1P78}C-nt4--Wzf;STI6aX*B(s;{EdbF7FT#?tB`RR -{TqA>f!_*J6T)v*kyj^u}iM;4m3gj)Kw!#39dnT2g!9PT$--N2xKS1=>+PgoyHX=xK##=4#5OngV{uO -RUbF%9>&uB4UvwsaL?iL~Cw%%VQ$u3l$;8h_^L)K-5ER*Xmg4ws-q!V24`4LEKCll -2yIUA`%vzCWkOiId_+!h}|*Hlx3EGt8@!4*P@Q-Z4^AxTlBDO%d -$!|U5ymzk%WGzsXoB{R(u@N4&cCGq`X{SLE6p<#8$#7XR9euPn`yQua4Zh(A~r|@rS2krH^lE#AZyIn -O$fLIBEh0jOq1`sohel(iyKn>GM0(?UxDV+l`6YD+I(bhQ3J?!CD;q@U{b98#y(x?*1kCnAUU-f4IuI -LDNnXHs{y3Ku71jr)?R4(_siG_n$qlUnmR}x4QN^4kd~XIZ*A_J9wck^(PnAz7Bqa6C{6Lx(BMrp3?a -$FD1TWr_?$Qj+D55*B1Z49oMK`c*jaMguuFZ)X^^sQEq$XS*;K+@emA1p(Dx0I|L61s)zbyB|8TIm1MYNT{F)JWM- -Yf2dsk+r56v|3Z(46P|=aqiP+szytt@sO-HMb=X(bXck-x?5AmHSRL5`o3OIWxd{%=x9Z6${wLN1;PJ -dEtNE_asFm4m5%3yBDSiK=G9ap-HsYHm4@ckRQ{_OHI<(>R8zULc{P7k0mGO -$2$|yxmtvZBhuFNI{>w4%z~7aJ+6{7Yj+mGKm!>T -Qjp%56e(#^hg7RJl=AR0&ShD5^~SpHWnKl>4|}(NY=u -g-~Nu68q~~D%AqIaV?c!LKn(4*SK0&a$Qxc##$;Bg^f)ptAw_=KTJ*K;zu=VD%V|YsHPG*-ME@c(N4M -0F?+q5$~9MitC~uYTuW*y)d0V)rgDd%M&MJ1fm=~gX;V~GPE%E>yr)r7iEz!Rs2nC#R3bq0Dk|G+R8- -#b`&Cpnr8}j09hFMy{YE8~r8lgka^evp1d3B+#&9c6K~em=lFB*tN-C#Zr=-#xBlMvtS}K3~{aPx~4* -!E%D%V_XK}+SCXPePd3CXW1EtOqocjHj$IxNo^>rd%-k#;EIUUY|(;>cQkNPO^8Hd0J1Ts6wfktPChL-dZ5H2Y1EZuMw1`|Z}?nyk;|) -5XrHL>T?(`XDXxIaJqQhHTi_}I{8;3jeF~FALL#qXh6t76-evKa9Stc;0(tnB5fTpzJ^`l -;+mrB_Vl7J3-3r?e~(>X}nlzv-BiOl2@tE`$6D@~2ic;z}WzTqDuy7}<{7hbcF#V(w_a)gdt -wAxV0)cSy{-P;E%e`nlROG3z4lG!zRsKF)65A?4GmZHSPgB-E(lkp84Z -h{*BK=hf94CGv -&^W&d*|yeBDH%uXPn|=4Jy+N#;*B{Q?~9RZFxIFX*Ok%p@^3uzDhqO%}&E-UB?Iw%4F8V+Kc>}{F)7M -l^YBFGYlPxieq!A5aLgX0|v*l+DLaitX%jfX7yOYZzG+6o3pSES6OqjvCM?=YUsO277OqACevv@;nE8 -iFR@i!V{|+Qfn=;+XP@R+Y+&|K!tik<6ml$rN_?i89?t)7C -XnCd9GRT3Y2_--c!bF|~ZGM*LJys0oxczM=1?WY{LQgGq8s{T~i;)dO<<+2)~6V -&ih%X~!)r%t(T*VTT>c0tqj{9AUK^~_=5JI!9Lq1?k!Zl*}pUsinYz}9N2Ooz;gIr)uEUOR;HL|Zwf|C4jX0E&{%(8iy;YM@l&yKEP-?9kaMVrJ8dLwlO0uia&vf -42-gKp)t^6s9htr?h^q=GxPb$|X98=7K4pPl5b;I}3Sz^OCiQD~gj{Bx4^?Md$DUKJXyP`V2iQDYbO% -(;hP3LV!O1@2UVv_XdwPLYSdVZGrm5KBO(o#M)(Wuj+Sba@b<_Aa*AlF{v{u{C(T&eO=intVenYZ!+}Mvzi9P2Fp5ba&$;Vo>VX;9{S}ZE<0bG-IWFh{F}tO}Ws~Uy2cKr59~ -oaPkRGNt$aNM-r~pH@iLx-fj2F@|aTHCZxP{k8y2zStQfdpPLohrm6#z#g$swo7Hd3{aoJ0ZTbb!m31 -pN-8&1NTjcSXYiF<*;#AanTsneu>ffttWYHtpRU1<7I#9}OabZ5TR*nk2*%8LUQoNxPO{z=S%DPQlj_q}^C2z@ZkG{!X;De>wU>a|aHQsho&W64&HjFzuvraLshDYz~|$?+2sJB -)a^Qse>|oWwT_BN|f@$<(+ENHQQv<*R}~E<>_JbJ%qxD+V!Bo!Bnk% -`I(S=+D1@p5!rX>m$X|GXUo|OsGh*Ec%+)Ezt>!tW5ODhumc``+3o8j5a#%K -_8?~Ry3gvE>+Pe8KzcDRWR8oSvz4~=yB=mvT+`!UB(rql;S8kjR$aHeU@HH<4lSCnfy*;Nh)%kTvz7% -hO@o%em#n3h@9pZli8qT+(R^L;7nV=%STaf1oy0KQo^?}$kJEZ -b`6+tiJwwXPO@5`O7vHI6QP;*Be)Z;`n1ow-b@+0};1qYNNnL4f;f#`m76a$xGgx$=it9wtg!y?WuEABE9LnRqv{-_HgY?5L{%Ce7{l;0N94FMeYl{iw;wBKj6YgRka9$s}pe@Z5@%Wq -A&{|DjgHI3rf51r6fYV>}{l%?p1YmI6mUk=&Ro~T`9VM5oMcyUt)dlsVCCayHt%Okzgir{Rb`sqok)$ -((<{q=(WzMn%>);lP^I3|7}v7J9%l+z4fPMoZf_MZ~FzuHA)RS^Am+86A6i~{YjhE7vi=$!9Vg<XK^V6Y`c%p+cTpQh0%wVGZOU7OmpT@Tsapy)! -n5Yn}Q*H3I%98-9+M)L44S+xzaabHP}(T(zmlzH${7;ulP>5YLrg7)c5Nt?n`}7Zq(Us!Tqeb(if+Y8 -(ry(n_HnkDa*rg;_S3GmF~M~`%Bi*+u9Yx+0ur)Jexqs`=uE-!|@ -ItM+kK3Z)?OO^6=S(+@zhOm^)QLZz`3A4|FpIZhT;{NQ7=RC|{_DNB~Q-URoMWSN^pl4b5pNR~bX;~{ -J&L)c6qr`&WBHgj`G*vu^;VKaAe9(l}#0GbN{bUp;oqP#NksaVHx1DBWX=A@YB~&40IO^DFWxEA -T5T@{6^fb(}QX0%_7%tKeY;H1f(ZF7K5%K>t^X`=nlQ;wT=d54_o^zT}CIy%vqk&3zI~tlI@O#s#ZI3Ip^}=$x^2{RSd&z^1iYr5|7E^N=#MHh4@}2(G~C4W{)i%eNC -b7w(!mOz8-*c^nF{OTJB@(*Sg%++p$yU*rglsz14ffxAQ$bz8kTNY?Wl&IpT!94enq(6tZW;30o^{3U -RB)r7v6+(;#YzBU_&IpFu>MGy1niu|VI-1{6T0S`0Q{Z1?&oiazU4`3qGOae;nqc0*f(y%nx*ymWHAA -4|M+Qu)j}EwH1fY&+?Bcp7=}K5HnA|33dOJUd8)_?X}n!A}IfH|yA~1a}Zb6QmIo6a1NABf)zFM+s^O -^tb3(M}k0tkp%Y;{M1#)P7oXUt(oNnn6 -HbYeTw>BoemT0l<@>>j6~~Y#%iePM-K{TbLcOh+$Xb1Y%CkVLN)dwY&eT!Q6inm>>d`wBG`BqDO{rIZ -v>l1epRROQm@AE;7CtNE&Vs`((DR7%fIerZY!gaNko?~@2ygvi&pB~vlXM805-G}wIBVDA4w2%(cp -m+&Ut -+!IADE`;5|1`+(cr?H1;XXnmQ+q}-hQ}5;NV`$a7jc?m_M!)tqb?Dg -X=3D$bcj?-#`>j0!diLra*r#v5pxbT_?muAQput1#2pKwT_?@9)BTOSl-8I@AJ|-eEYV5e^nAp4H;_n -%sFkxciq{&IiQ>NZKZTgIvDYH^%&q=eS&&|lpx-WZPPHx`))_j}2VE%%I5BzCS;o>3|k!y`wkZDV>=F -YL8_U93=%^}{f5|>zrvNUP|s2{7~6C3f=JaP{Zt<@^R1qeFYvepV;$5DGqq(5yK**B4TrF+=8CVbE)# -}iM6ix5ruPo(~BFqL+Qa2Y^h!U-F>??4|CAbOSn(Q{ZtpN8Dx$OdZCgj>2ubDjvBL6qc^Emic0au0-l -27RDiw4EI40|MwbRZv|ZQm9W0{+YrjmEvWx2b6GVRjoytuvEirV4E3JOd~!Mw4p4~r=qV2U_s -R9&!IOfyG@iHIAD$&CvADAHd#_R0!MI;-0xcjF4hJyBLz)WC!?+8j~l2R{Lw~t)HxuVq2mi`lb(U^|GZaP$))N*Tzg -0x2Q?;x2%jc*qMAOe@A}c(zM^1-!ON6d7`F}ZUKe23JCyd -G}Us{wpgE{n%>agfq#lf)xs7C%m*nPCBLEO)LOhA{~;`_#rn>r+$;*la(m8I{Lu>*2ySdT-G+PwZ1RN -EWmEbVm1mmj>$U0J`77znQ+l~f(c-Kk)iie>aNn<{|9kkQVNPkHyi=9b)pYCIA<8sY@I#R3M>E8j6Cf -mBO9O7WzC8}s)X$CMhY&IkqR!@tcUuGe25ai)#?ytg3)x&vG5?bn=nON@Jbk!_EWRQt9Ek82b!@bVTZh||R9pFwTG;goFVrHGmOdzuhn -J0cRv#w<3k%{k10?51-?w8H12bun4Lb};hYsQr=to)*x_>a;d6S89onGYZzGNGqv7Ma*mHIo@H+J;q( -(-?n2TaxJe)m|Zj1=C*um0Ux~DvoTJ!PL)RS}Wx+Q2a4AQp`VcsZ0@So-+2zv60q&qBQ5z?_EmWG|VB -&-6DF5v=bVnHbT%GBxX$D%G*R5K{@v9Y_fXeUq@xiJiVE>ho3$wD#{Dbu)&lL0tSR8Oz|o*1^f0y{Zvj093c-As!9a$A{(KDVzYm^(>;8h=f7kn6{})X))6!o_6MqduEYbYwZtzbVf2 -ror;|BlKxB>s$#s7c6bA6p={7-X$uKw>vp9UPDtN-)3!M}PA_;>$)jeh)fq(AMO*6)T%R_Jc{exvl() -7{ubT>Qg|mn?OZls@>-GUsyFin5gtuUh@*M;`slV~?*{`^1w^J^j~b)~$bb!*kF7?S+jm{{5ww|MAK{ -H@*7W<}F)a-}c6vZ@vA_yYIbUUa`G$#|Jxi?cVd@-hKN&I&kpN$A>@p^s~>8d~x*H@h`vn`ozg^zCCs -NyYHo{AF9v%>&LS-=YBeW;pdB&YA^qCrS9rA{%sH7;4`C|A_L3?)ElHkJz -2;dd2>j#*PcfRJmcL#{RIzzDi?Xt+D@EV}C?r$E9SdeXGU}#bvGJ^u#fZaTayWEIBhLEq8vRE!CEpn* -;aB`4;Q=*;$r3wwRpsT;FnH?6p)$|keWw<(kMJujj8 -YkYQk78sc8Y}*4%j%T102Pdt%QboT!PW@|;2@VV{>N{UhZlNxZiHEF3ilC8;~v#y3Rk}VzX(yS- -vfe$V#*4WoOQjL#RH&FV}8s+LQdKP@0Jdnwyx7&rxQbHsL1E0p^IcqkJZ>e5y08epCb5^xjXYjcl>&Q -(wk&EjB9ROk2R5+%!u-ZhAnTy8b&*%(3M?mO&y-E19!J%7c`dC7!4SdBosyYI3bmTJz?ml8tHxB}B#2 -Zy?oUKiKj!EVEO|F*`HILUo^=-((u|Gcs+K{JhjTmVnfBo5dQ?Z;(CTI%sxg&LB%pK|eKOBYvpo734$ -#Gp)Hf^DH?wcSa5JvRLP3=A?>Br0Stq1Mauy+AIN7$BmL|3h9y8@q3C`JF?PT3!8V7G|9{9&d;=EfDQ -6(saB2ijb*2)GeN5orirps>5|)-HmwF>(=4LOwf+J2e4sUX(%1o^*QeXBcfWvC1$%HxgOr=VqhHT{L| -b-t?tCPQ*p1P-K2G!H&;*}Z=h{)Z`2j(e0_rI;(*rE?@@xxlYdN3U#8L{=q+7dif7_NdAbLsdq)EfyW^X|Nj2JN;<4FGWB&yVf(`l^1 -fRaBwKXYDQwk1XFN2jM{=G&&*Lhcxtmo{6Q$Eq;KHc=;MUf(nhI7kok6U9{D*K{?v=>O+#E&EUD+p_< -Xel7d|pvK-f+~-O4e&1^B<&DBEnAS4A5wlySXa5}8AJ5u3=4R -LKh^Zv88m|5DEhW?GqG5+<2{*CGXa9hjxZ>? -naLYrk$ZgOUtB_bo$%KoCz=nfaoSQ^q!w53H;iD72o;i1gPqA)#SYye73LzW!Q$|M%4i0Kw3>O>Ziot -v+|-5oXYo~Sq)gVVCJ8FMj||3nfe3Iqv>B!nchC|#n>O8+rL%?J^Ks&2NZHkKG2mmHoDBcdPhh_{ -2uARdDKr};IO~}mz?d(IG1k$|w>nGZ>C+Co$na1`DTI4#XS*4&g0d%pmlpBSnLcGtC7Lzl_YMCdvwOZ ->=39IfIh#MY}z}THA!|_q^u(u{Uk_h88!L7u>>;wHI_gS7@u)ELr?8ck>^v_Pbi3;_v=WUsDs8;e(dK -w=5K+_T@WneBFLsP;i4`sa}JT1*CYNU@>GL@WQFqVrnOfvp{lrU-M;*&i(Z-wbU>&Y+=hQ;md#Me0 -Qm(sIvlrynpJ1^HHqDtMlSx@Wf>b7D&bd#ayW;wOjEZlepnqY$%`$INCgqbaN``FgVWpRVLoyrf8JC+ -%ZDY1I)w+;y{8;eRM2lR>Psr^@?j49DUe>%5AyhKSy0O{z{0t;E#-5%|(mP`hp|mu8j-L2P!7SH0)`F -927PVcZF>0Q9saA`-+>;SoQ+`RQ)=W%el3AkclQuqGu9aAi`V}c)O%j0Ix~Ywkq$qQXf^Bls!}fol{~ -tf)W5eC1PN(tkyFM)T+I81EH}LoP19AKB^Z)+`{B+NE=C@z%{C=eRj_-j~Uh-5-tnK5h2Cj(2CxXy@g ->`oyCa=326tk6LwpRSwX#8$g;=(j4>G~;dI3IG80&wrCxDQj_!0{i#ac*^2u- -#>6f*&XMCxiB-YXTM1vR;JidJmn!}a#lJ*JXEgy%Ui?*gU$5ZvoRZ%Qiu*PNzmF9E&lP`vAGs{lGW*X -ye$76ze?YU(e;l>{k*E31OxeHr&9?tty#L7O|8*Wvp8wlfA&}$)553_z>X2tGUAVNT;{3O-Wpn-Wvbp -4$6XNb}^WtJ*Zlv%P8;))WXeQYu*JRVIm|Jf!e7Iu1PT>zcd+OOP?DVtJv(uVSfAM-@n?M7|grhT$2C -$T)Sx4`U&yW7u%pwWQ1S1JT35F63CI})3An+&fC13=#7tHJofkbeM-~_>Of -+GZ<5F8}fN3fe<2SGW(I|N$@HWI8OSfhkrMX-#Zn7~GmO)!gK5K&V|L^nfKfiX)%%jocsXS_YGfA$43BZqkC%y~tdZe>9{g9L98v^wisK -8+2^#IeaLy__frSO5K<^gR-r=+drqsH?|bzj2$-0kK4hZOYxxovd4o*EJ4hz{B{DHjqd>)|9Dgz~7aOc=oQ_VMD2Z;u9qG?%!X3{74TEUimo?+QrWyB*&A7kO -jQj3p+>bQlE;ZqfvALGq4YPGjqQX0+99pYM(=psDtiJoF>)1v^{d;+ij%9e&zX#dKzfb-9!L{`6SO31 -??-X8N|9;{Xo%jw~eUJX9j@_%t4~hW;82Srfsh*^3`WZNIAR9Ms97|44W|^6p%x<^K6;)hJ|HW+0nl< -c&7hYg*zWF9AFE3}GfBreUcI_Hdnr^WOOfR_PnI@tS?<@9Y#$EI09}uqx_}RK*-@5tp7cD}7vwUCO0l -x1NdGDfk1HGK3xb!}MKHjAR`}XY(Yh){6#f}GZdNcRNm(wFw`P`dU~DwUWD-b4y30a* -rmMJ$^N+MrVj6vejWMap)f^q`tp1BB@uq#z5_dDhf5;;uaUp}PWf~0I)F`CxB_%E$n -zOQcIMWliEYZIr(P=LB@N}t}n-r3Xfwx{+TAQIA1K87RzSkmFFEJxrvY{S*kJKD$XJ3t{;ptuZmFFr9 -OR1P5CYxf^`uGh=^L|6d1p9vqyYbm-|10eiA+AwKfZQX(5auNOj=T|4cUt1@<5uVDo>Zw{TyhZsV2}$ --wc%f(Hi0m!W7r4pZ%&<&(WN%j2qrB*ZXYpuafgT$0B+wNzxuaAj#CB1OZ>B*BPzn>mA^d&M=?u)&D -Jm*_e_+n?j_oq!xVMTny`&%a?y*F1P{UXl)b?MS3Cb;-dj4kfbgZugP;LKA2)&i(ud=xOs<%BXai*jm -trKnGzJ}fvmm<=90n2j1WiiL-VvzV9|QFaq1Okhb#No?xWscgoK8EkRdST=X=T$YuU#pcbM$E;SX=sy -Yz3)y=6X!ghYlTLpM3HOJ9_je -`|`^#+1b-a*x{czJ9+Y?;ESrNDt6)Q5A55^oYmCSuuGRN30}eDF^I;81jf9HX$-3vYfXI9h8lZ2YQO> -fCN_!>Vbk~owvgY?*6`JAD}R-J!VjtaDeFk_dr|zs6n_-OkE8fgDgIoFzks)A%PIa-6#o^9UrzBqrue -5c@%vJIGsVA`;@c?xN{as?#otcx4^jLh6#pxV|1HI@qWEVi{zXmvL46rZps7zDO}|&!7~8ax=HxHZm{ -v)R@Z?DTe41Asp#Gi6 -AkMF0F^sprm+>Kaj8Axw@%x`*eD!OLzq*_8Ltks+2T=Us6hDsQ&!+ebDE>-{{~X2NO!42N_`4`R<+=J -e#Xn8)YuxesQwn!e3YnC`LzKevl)?^5;UuL{)1I@RhH!Ra0%sTR=j_sI&Mv>o*_A`w9ltBZA3*U(Q~Z -e(e-6bjp!mxu{#uIvH;TW7;%}$;2Ppn=ieIgXZ%|606UFaJ@q1GIz7)Sd#lMT8eZ$AZ#>7NM#zu@D9Uhr7AgEuzK7r%L^;T1ej3ICOk -0DEBWJ>UD2rzD3fZ`t;9}yKhnv#!-2p>HrJf%MZ^y$;9r{W(IZw`+QkBE$*_`qvm|KQ-eQtFY6jgM8I -v0eRdNf|g$P9d;Y&w$?J5TDYIjg1){o6@~QXMggi06|KCz9|%cbT}eM#HMuZ(4n*JFK`U(6+lSFxYJM -R)UiW{yGMxt!6HD<0K|`ogl{W85qwzjE#zqj30YzmrhCm_}>;ZAdboDM~E!cQcD?$0806Z^o -N+@sfgpFV&er7A_ukn3ICMHA!eWuOKEf&JGMiI4iv!cpEBG##XKmAGPsBQZyh^UpwP|jA7S(|hPE3V6 -&o2(r98HWAfo$Ng*`@(2oJx*)95vLbV__md`y&rWxUE=F%d(@L=4jFTHm91psZpd-77UQDPe>BT6ybq -?V|}ppco%Jni#3xKm5*NgF{+*8g7aq&xkR^%R`4oMAZAIq>Kq4Z))e`HJq#wDM&pgvOfQm2m~2#YU|- -+hJVCpV%Jz!?~n2!1~a#5-)?+LMEqzI@fu^=^yMeouYFj2IF)0J#vAEJ3U6}1GbF+k)6iSyA4)%x7%? -Vl*oa2nn)In?V!}r>@@BCOpZG@JoR=5X8jDAtAVH%1kCEq!nt&!{xnefDkv!M3oTAEe6DLj-^2lP#IJ -fNa!h*Zlvdk#9#u~x4ERABDo>fjmbC&dtEHf-1^RdSsV^2Q$BzyYlr`h`T>)CV9Jtt&^& -6_t1dGM{b-V(CHr{yoOCrRGeM6$w;9Xr@ZAAKZbgHzvp&A$2O8+Pi{DOO!w&CZ-T!+tt%fF|NJw% -di5$>eTlPI>HW|ZO+PybAFSL-10A)?RdZ;dTSx=l!!*$SjkV`n*bx36o4|Lo`}twEnjdGc^3yba*J$F -?fS5ot`aBwJSM{g(!zsR*;>S|_X%zo{ivJMBf12XIM)CJh{Q7b3zjDfd<&^(h<&-W;AJC;s7qNBKS*( -I~@$WNe&>*PD?h??Yi+`7%xA*XC*X~y0v(CMO`t<47XHa`T<85u}y-UyD{rXYZ_I^DE1rG{l-Fn>~G^ -l&)LG39(yS8m_?bfqT(4aQ0gKkwE0s?|=A7n8222q^bZfe)Ii~lXR4{BpDv~K0)<$Iga*hTN%<@WAv4 -6WfX$gf8yADvIHL3p~i@G_7w`vtk?W<>>uAbe}LrXK&z9k92rzz)Q*Xf8zN -)TNS_;M<_b==+Og(3czQOeOt9^MLmm9rc5OzREJPcA&z0FCH_Y}o=UpDHy9q}is2FLAMngG&m_~h^Y+ -=ZXTSUL#~;6^vFDqA{p(++$^81lg$rkCUV7^I@#C-V+O^B2)9HdojvP5ip~+uK2S-v+4!@=RP}dlkZ| -d8(Z*MYDR_$&uuK%=Y(?$=chwNQH|NQfZX&%krfB${{{rBH<8c+FWpM8b}D*pA?Uvrv&bNCYtoW|KNB -}u9xT2Frb@yCCysHi9!GiFQ(lj{jbaBgAp5gFiudTp%$?COyq9KGp|u=D243#qNG{e__$ym9nJZ@$S1KmNuWZwS0kpFYjM`|dk_@Zdpy=+Ge%4sl3Efcq(m^USNSzFN3 -#+qOay*`gc{$N0RwyvUU+S0<2L4cu@%DEO9JZV7QZo%h~x#~mm;|H#P5Af)?S@jrk5JSSOZb?@H2hfx -`>{NaZmczJobyPOXkIKaR7;tPQ%)sMg(_7f*g@MFi0iFeSX0?9nWB%Zr)?%cUr;=3!9ru54%zwn=a`s -w_qpMLu0x8HvIHRW{~;fT&hsJ#s%99AOkZ^fVFDdgn_;I9H`PyuzK0`OPqQ0*!-rbPw9*$q_ZZqT{|e -h>bYm6a=~9(xf_lz@iq+qVn+!5>GD9u;+P^5jYJ4jj=Ys0|DA@ZrM(&^AyXFi~%4JBYJ)?_PmF>X6z$ -M?GA-c8zNvdcQ*Mb?~P)e8+FW|Ki1qoQ5I6|DmCweZj{7;J<(We*X5`Z*!`1PBOD-!zh2$$DTcV1OWH -lyLXGWa^%PnVFwN91Hf-+C#WOf_WF;UTaI!*`XkPV?%{mU2e?O_^MU1@PyCSczn|v(=ZhB)Q(NiwTkw -DHz4ulJ1_llz8Ga@8m#7ax1Mo$eqg+7`>H@gKj=KNqtFHvWjxtAi17Gj~XaU|Rf7J1_XE=}ijC1qHoS -P1Ce&;^ULv|AlJ2=0cXz2Sc=e^$IeC2V@>+0&B`YrfVU+)IsuYy_^D*T>yf_4CYN&brSaYs0h_>{^YG -*BH3-^=+OL_^N!oL{WtJpB;ogWq#+P23Rvmo8lbFRus*3F$?=Py!mL9?M{W~0ia(7RTkgPdDwoLhQZVp`fukv=>6Z0KlR0{`}FBEjOt+}+8O -$mrsx2#H^*NnfB0iuKs@v_+9&B0wNcRUCFe0jLln`l>0kUto^u3GG>8A?%a=KgGYtSVsG!o(RGlv2)Z^}J4 -u7iW6+?#(?M3~03GfG>G^PW5558_phe`{09enilzAgNP`6KxT`zZd`{JR7VcxF^_K2@P1;cL$02&d>T -H0>EQDD8P5`XuU8`%s_MlW6GihTON(v!p5fd-dw&=6@RFG0tCCA85ZA57fFqd%(L&1MsJQkC#85#$Q@ -ElK*Y~DE=(b@HEkYCzblo=|sa6!ei1mG7V~b1`TR^MxQif7nOS@=eL0dD*u2tsr~O3$$Zol{?y+K{-? -IK67mq*oJxmE3uwT2&>U|8fAo`EmxS|I{$%2RCmNn78rIun{PWLpKC7B&kT_31Ew^X%NwLT3+cRiT+c -RiT+cWy4ptnUbmm1@bc82kVSl{oZRDfAkkB|Bo0kqA%(~1+-V --4cvgcN(=ga^!J$8px?w;g|Q9KdrL$4n?%Fb#UuEpMJE0-(a^X(|AqRb)$_vniu=a!WtkD&F*kx2rA2 -a={a*gn=LZE1S{Y-YB4ad%KlS&5{|TR!z#DkM1Wv#k?E!eJb%H)#D<5P01&!eU%4K))cZr5Kh=whNL< -6-2v}e%ptU|+Dg@#q2A!`hOD1&H7kK~Kfq67_i&?k-1$QZ*uXqx}gXJ8D290Az4aU*}}rI$G5A{DfC( -o#FQdi5$l_}5fk;kc8(vxI1%wy>FK_@~mI(I>69-^HJ{-o@ACjpmQ$nEAum;oLr -jG2dyvzQ6(aEnBwCO+#ZkTB7CZ)jEE%;tBr2iU| -I`Bb2|bwCC3}?fE%{h9?yo{!Dzd^1g8H%#>-sBV-I3pW_Ig{$7l~d)f6*XVf{3zpE!qm=K$jlk*{PMS -DaWSiE?#ph3%Dzg8zO(WV;Dv6^~5-|>oD#z1>6Q`&Q*r$zY(_@nNlqoYv|{KXev6tWj&8K=|93knL{G -_*uVOLc+=ebSc2H_)Dmh9>%%=AOqqUw$e2 -9mqmW(E_|NHlVM9EChKJ^8q|)&jYGh&*<WsCw@#z1>UpY+6nc%c8Q^RJO3M>^x<A#N#476Z+_ctw{eoK1unn?Z9@y7L50S&0Kijf+UQ&NKRuhjQ);I1S(Y&{KEG5L!M8m! -mmk~vI=}ey&xfhLZ+QJ-&6+j5q@?8h>eZ|H(xpp(zW3gHd3JWTD0iAy3SI&YXcwRpxPgwwv|tSa<1qL -hV>H?d)*~>Fgd1ce)E#JP;vw=ce}yvot?NICOl@~vUS8hUfBMs(t^v>Fk*03jHzaPF>$1oUq+?qx%;Ef1-QE@ZrOIQ5lw? -J-_?zyMhLk57rH-eFz#-Q&V|dT%6#o=bn2`;E%FKnZpje(GJl6z#XzF_y@8!+5y@C+62-?A56GkqjXC -A{r$~U@6Ybou>&^`V$a9tM)60!TKPvUR{-XVz=!IVlWc)g#iBh>eRJYB!2=khP{wK;bv+UND9RN5|7) -+k#$S2m6)^_>^Pm3|@le)OP7?KPGiZ!lM7VD!w}+U#?{TC1H$r}c{Dc0V+T%*RqrJfdE~sy`S>jV6OU -#=$PxP(S*2VaSz6SiQ@*&y*+9m1)e1bYapRARKU`M*BBh&}RbD|et7^2_wxa)Z+t$l!-3W$S#27M78@DJuh0N@U~o_XdOtTC?h@bLJpxV!OJ<$o&g640Rbi%rpi@{pUxkD{BgnG+HzL -`;lNAab-ZH?0PgDk2+6fi{~Ndq{I&C68hciPFEIvz4t2dB{RjFwj7M5JP`0nX{yKl+i6;aFN3J;O@p>J1%Nv7_Ds6Qk?+Sty?GJgD!*zZNL}2g}xKCsJN4S`nTW7@7LiET& -R5s{-^P~M4f-NM2ouSsqWvQZs3M`Q1|3eXXuNtmw-Je&;r~kkKfbJw1~fU{YRZkwNxjN?;-!-9s6Nb67>_V^qKq-;MA@Sr)b(A|1K!c+ -qpw9jMso_WuR%2b(YOoz!MiQ-d2{@(?Tg^Au3Mm-K_hsb_~;tR2olv5#?>x=Fz$Guj^yV5Fdz?{d-y| -C_wZsGT;88QzmECvYoq7SKf&1O8rdY0A7ttprLW1iSTW202>go_lgp+~F+CO2M=_0x*+V -f$D`t^ma@o|uZ1~+y0Q(&e&iD8z;Kf>y6|vq&u!rDcqxmAJ+Qe4dHIvh -p078^_Kdb9Z7IK1JGJ)^c9(4d>CH$$M;A3&x(p9hIC<|A_O?eiFE}gac^p=H=yeC(Z5dr#4-S{)}YKp -Q*pCqJHTN^-D)dK7W>E+boiS@eRWef?gzVoSl10-Y3IaDE9eG%3AIx=Q%%cj`PtwME*avkbigT!#7Z# -wc7o3^{k1yey+&;*9gv2pL>}4&9{&+_Dr$ghUrAt#Eee}^J-~ib -P^9PKN=+`k8p|6H43poXTu;ZB~)$bQ#j|cmmSkuP-4%Xzc$M$RaV-7%b&>WgCWU2Fe%yW@1-16nf+I<+W3E)z0B8S;^OPUtebdBmd=8til=KDE?g@sw#{ccsBg$ddEmBkTaEfMRvSVz?6kG)syA162BN3*g= -gS|fNF@Yb0-J4}8hFhB$y9lN#~kXl2hdMB&Fq`*b3InkS{ -MUAs0(iwE)spToq1{Wh$rVt)Z^!mmG`_p5t`Km5YEm2eq#NZxBho4|gjHh;(!3l}cTDJm+;((b(i2dt -Z*Ebw4`9eYjxQ1<(<&xG|*@FVt^u(#n*_LmmTk>RnWnF0sFj|c1bz8mM?crUo|9x&>^;-S%EzYBXsSk -uJXxXO=M7svhz_9nz0my&;B+E|g-I1N9J+SjoDA(J_s&eW%#dMXKR5%Yb>=b#(7pbTJsydhKU8)Cl-> -#8p)YunE#`wM6j*dM`OVX-CBT?TlxeKhuNwfv8^^}quUgrS1=4UVynB$^?B6%Ho3b1^@7o1hG$m -_32H@`ucO4Hi3O6m$FY*q~L%@@FV3ro#6PThV5USpP?Q8f9+j+Tvb)}zbKPrnlx(ZWPv26<7@AIo_pV -O_7Oz|hcWUMmYN7CH$jw#qmB~gBOm0eEXxOoXpW&Frlix5npxVEmQL2B6D3VnKA5zzG5g%#S_coIlUa -R!^Pl_kIl$$fv)4Xruk~H8efB}0zhRxFVeJn2bgi1vq1`XIH5RPZ&4e-JU#c}WX#a(ONPqtP`N^wRts -088A@s*_99~@QtsBH~ATJP)@H=c+zuh$Td!6_pd2AqSKZyV6V^5nlZTw@8J(h`lSh#SZi+hN>X!{WVQ -5KLND2qtDVa@6ECx?TlK6wvx;tF}ph^eoY{TO|;DVXQLoG$Vh<15g``ds>zwJ>jPa4ue3oEWUFIUGE5 -{JrnxG533UT%JD+$^W4JhyELR^oI@|`my6UZr&AZ(}6%BIB(uOH@?F$d060KCC(4>=In{T2j5;jF1US -0jJp=VlgE-NeS;NCCj`H&IT{RJ#s85~Dm{DloPzg}pP!#Kdi3bm2_eC8z;r70e -|2>(gy$I3#=t#-Z(x!-p#+_7$#1f=<*%$z^jpXGT`NqJ$V2-aWNw!V=TyW4){p;U^mA6*NUkkkXOL_M -d|73d$GoYIr1=5W5$e!us_Vnqs>Rm!P>%z5hL6hGwKDNiw$|;<#^ -Fw#+nNH?Qjn(eg^G&`d8+#8s;R>9%W}|yV!thRlU3-&KL3;?;A1s(4j-?ekN^~KGFfKaq<6$#^L$En> -TL`u3Wj&%@rdyV$R8{DgHv*E`6lq*#j&Iw(!W=(u?adFkgjT_zh^lTW -;GwAet4X?*~+OA!@Fwgoe^id7+cMkOsV`G%pO%Q_&`mk;tlmq_t@=x5ZD!uFRT$E|_;XHrF(<#KV2S2 -Zinb1Fd7u*{4^~?NYAKGA~g}Ua&L=AnxJg6Bd%ok~%ca4^S#`c)%B9!|<4 -25NF;>7>g+Q$I42%nFtr(Nz*irtG?w|7GT_5Kx#9+(M)%Ag~IL2NW`(fONaiYr!#as4gy_t4ivUlV;@j5o?w%ys*~m?I9xf%7+P*f6)w -jTnY}sNSY5iOpgFXrd*8^XcFJJEZgSbWn>uNZcC@)v~;s#M4&Twl#wkAt|4J{Qgh;%URN;NNJ=8_GYP@6ErrCJFmOJBjqs9$}n-eIqV|?S -Hu8^@!6rZtMqnfwdyMC$!m^Pr_?4&PLn4XV0F?uEmz$q%0;J4qxL-hwn!Y@0=!=ju!a;5ML8tWMsIz+ -Y|1%7J@qxyPt5swMIVNXBc-5z}?mw`*2rc{Dgb0h587oLnqv6?N^u1jCA)|yW!HAk?t;QO?*~zPEK0p -=!_|mlQT2M=h#ts+2ch{N_tvma!$9*KuUI2PS%**ZYf!rA~`3s`y?6_nVCF3FeWW0cW7F6P9ST%9Ywp -7DA~MeWTcgwotNXjedQrSLURpk#J@Fn_r*8GQY^+v~`kRK&Ru7M#GB -GAGCN?&HK)3+L^}CFqY1yus2yVrJd4Q8KgX@WGQQv9 -m-ziBju#hNR3c+_4n!oHBWt7EmQZY|5U$L8*33-XRWVxpEh30)$+A@+B4cJtz4_pUe#{Xf2$AEU)A5% -&*;q!V$3z3HmZy>#y3XLXl3>=dz(|uRpuu1tl5!g@Q3*lzJ~ANC-}``h1emEh|ff-zooUwdePcz^|a& -c5q7#gS28Er8S6}PwmMbL`_5<1ImhQZ7PtV@N5aW%q$6R(At@w}%pl8133-EjOPbP_)SzQ%A9h50RR6 -dBogQIyG&URij2OO^4-t1*eXaiXe*0bfxP8WMB9kRnBJd%dFj7F)k_z$)d6OI=$H*DrS0frv2hb$Cgg -#AI(Nfx$MX+erg;8cO$@;JaHi+HNl7XKS*ko3~3fWTjEGuRk*cMjB_Ok=*BUZz{V72T9r8#i>1NA2Dw -_3bbrXA4!rCrpR-b>HWr|a|d#rkS}i~f=xX8gvuV1$~tnD>}N%@i|Wt}}O=pPJv8ln>w~e43aET&ol} -`!#=zKLNP5)0%FNk$DpLuD}9OF8ta^Ynf0VQ8%gw)Ka}Izk|2u(Y!P7#wpi$EKlT5@fCa>_lwD5p$Pi -Jt^U@1ReWhMdMnE7SOV^S=vgFHCrF0KctuF8}zgKfAkhcE2EteWpp-r7`kB@vBvL=L?g*aHZqND; -}6Dk;}PRAqtI9a61mQJ+1PFDHx3v_jH5=4@wrh8@)>G2HCvjB$<2GsaDFR~;2n7!{|kSOzt4|@j9%cO -;zkiIdWb$EQ9LB_#B}kvctVtlV<5N9{jL1nVJ1fT8(FQbyRGroY-^GAytT#JZoO`OYlYc&*aPfwcBTE -P-AuNX5i(w8!CV!|8u_L4IsKe_ooUXKPKEQHgPSlE!ra|J+K|2^K(fd)Ak&-3r{pxbKon}xI68|ipwH -4R^e{a}L)k6tR@Q+9S(MUENmPoIe=4Vx^U95Cq|4R&)RF2R)cNXS^;wXEchp<7G~nT0(9!uut$E)3m1 -rj_#Bq^s-D!8SkJ?|_qhV~Z&H(2$Zsp;=^Rz1&NoJF3I)$}UgfdFmsT@$gP=fl`#$5ha{ta&{zVi=}! -{i9iqcjnC97+-YW*(b9%G_$KYz`?&;OSH9sl3_ANY^?KlV2R -E}aKiULz~y*T9{o&h1V|rQJI(#3QoZOwWx6OeTU+s2Nt|FB-le8p0Gsc~wOnxZb%8tZo&V- -25q(Cp7Mf!e?9#)_W8a*~A7n*iV1&~`KukSU8kP -M@U9=n7g4*tDLO(QX_&EcFR>nYvOfQ7hDs)z8#3s;>F9JZ-A>h&Ee$Tw9_QYax15JzQU=KL?zw()a4R;Rn4NWK1&tXcT~ -+)fz3$JIxNJ3Od!_9B3w)qs??P+nj7ZYR)&GGMAgBU<NIm&IqjSdPFF{DEGO1Ua0WZWom3|SWe9v)T%23zEOLC|A-*K|yAz2c>0}OB4|eke38NioCqS -5VItL`_4M3P$dXcsPc}`-fVA+b`%g+J&T;x6xD#AoF5iVMZHh^c5qJ!upx(Xsx&}2*W6tN;sBmi;`7D-~b7$s6gy2ua{L@rpq -X`%qE=^SxFd;_!9#^1@G05Uin&^*P)h>@6aWAK2mt$eR#WT86IIz)l9R0t -qBm&6C7x0#uuewrjT~0ozW9wno4bt!4tN9f(?n#8R>Q2Bfyhw05y3tE7-tX_6XEM)BCO})e@B4q>&4=N+o%1`t^Shnj?_57|*H&g=j2ZC@g&2E@i9eNH`7cFcY>NFSQ&^Ar -m$@%VD}I?<>uy-v8O9T7l~ioc;`@#v)4X}>rTK;)-%DtZcbqASf$KrFT*Q!naqkL-HX>G_L4k#`fJyHe#1IkS -G*_*gsASu=rj(->aM;2?)r70)X5G3lYIl%m!zn4D*jz7lCiDC`1??>71!J3*mQl@-lru@*6KP1*SjX9 -Q?I>$?ft8%NTLa7!>Vz0$4mG5d%lc>L?faVQ*iysgmh(-Q{w;If7eOuwG^58Qe-t~XDK74jMerTR=zR -z(9gQZ49pk)z7W@ic_Q6H{7&C0b$>RIf$KNDPH;k-*dfEWJ%?8>+Wqbc1M414lDhHEN3iG@F7Is3PrA ->2M(RGlTJ8=u8oEDNYwR9-$kaVnF8S7eQte*<>C*16)RlENyUV*Dy6@)hdZ!vxSqbp@IMUvXJmtt+1{ -kH?XKVhgi(vmfV21HEFlnHL$s-3WtoDG!dUBa3xYEJSppO1LsN5=+vc%yCCD%FjCa+@35(mRQqkFa_k -EzAD7VF`~{*`&y8H0({(si|w)#f@p!7?VDIbdM5C06xas6uTEu5}WtbvTZ$n1f%g>Ivr7d4hG0&J}m# -SH|Qsd7)74D*3u4P*t15s6L-(Eq#8Dt?c=0W|Tjl%Vghp9wU2xl~sEFZkzo47iJiqUyW~+f7K=AXV)# -EG6?P}=3L^~iTj;&?^xov5BK-ceKy@&ac})w>rQ{btu6O-=P1$CfcuT0>9c^Fo5BvQ#?RrX1AIsFvw) -wg;2nMrhIh;%%DK3*Xw^k~(PZTiT~BEAB|7JxerA;DYcNLBw-5BqWfIC$SI~71T~BWx%{|>dx*E@Q4z -_}>cjHR*dKUDW3R(mILqubuHE4N==uY$x=Or2wt%>GD`!KDa1+A%^)moVZ|5?yH*MmCZ=h%sNJL#QcN -v?`__&M&wyZh)}_L5u&-Z}U?@Pb+YTAGCpNQcHAk|6go#Tvi_by@}D!v -{`U?3;l@IdCb1{+4zFH{X@yM>eV(R#{oWTY#?`+0tE_t~@(m-L|r? -P`K>W2M{733P(neRgfPlZBR^zJyp0^KPOyI9&zc@p6jn9ztq&Kb3kgfnDfX$fS1Hso=)CWm3%AICf5` -w4?#Kwk&>gAmHwH1HfVv{Bx2)N%I3@^*b)_6I`+uqS8gfm5!c$v5O>&>#&wXOsr?X;+Pg3eY^-cA=Fg -+{-S6czwE%SC2o(C2m2(f${sDDo;H#cz;7`%UB*7ozjVkK*F4fNp8@Ka5s@FnMFCA} -o-gk8TeV8xxoG%WIFV_jaSXuRRup5LYmHV7g(0rUnsPD5jwWur<8t2h4@SHj&*_vcAD~SmvqX7jIekJ -MzK5#tm(epuv??&PSGYCGYVLsg4uE(8dInIZ$ozd`m4sDA*?QuOHfb+A#Xg-Yd0C@24qvE?~G{S>F#_ -%9i&JOXm8*TQP)e8sCB(k8MKlJT%qSmIbzHY!Sr;fl$twlbrd!!@B3@LtiOg2hSwqgw5-W>~VCmoH|& -ngSxto`W7>BU=j5Ug^|pGf)Dx@`*+)I%jGr$kN}*#%tb{YlW>VU=!^X6(F8#?OG?O4#b}C$L(Hec}J| -!fP`(tSVB><~AnT-*9E7&6t*vnmi|W*7R&!iZ%b5n7C3{zqtiB -6bs8veYp&^4ZP{x!2waL4p^5QM+#H-jJFC$kg!=`R4`ykXq^qqGuR7+)3ySVQ?!Han1t%O|Plg(<&XR -#EwdsWeDMH#4L_K1Jv+}Gutv#-l88AhZTtae#)-|zGBQzi4rvBb_CHpg=p!8ZJpI_kua(j5c-tF3*%S -CFm_^oUA#v-*+~zwvYm(wXC=gZw-3oACa_E(f;GiC+u7r*uZ7GyOl0&t!Onb^k9dchd4SIFshT+H(F@ -`dp#je<_^5mY-aAWbRKM(#t;VaP3OSd@kBF{rh@@cwaB_oLK|k4_6=Q)xB@w9&NH7_d(bOyyLdq@gD! -~TP;WH-efv9S)KLz?ETf&p0ECP$IsT?^N-6OJO1tdQ(?Ozv8^mOZQS2FTl3TIb4e_Ce5&02W|pD*zwH -!L?d37yd$8GO$sas`b^v}LtZdK#xjU};gKxqgbj)NOV-M}?I+vsd&({2FR|M|QINUI-bKJKy0G`o*RD -<>3fMWu@Hv#KJi|4TH@f;om%!dF|VV*QQgOz>3lM{izzbguFiUreIFc$CkH9z(&{Md6S^SnXw?H?~Qi -BPHNI383Nxi -#Gv0zdO{pWB+xW%3yOsVtQFOVpR(^E&TJ_Z`dX%=sb3SxG!zs+RD38S+TanMf<++4fbnot`(R8LLVp* -0gw6XU?D@-StDHJAwBb9iGFvfPD<|e4~SP3{rWx{}Is`byeo5-6N}+&yu1B+;dsC`w_Jr^&D7}$^wfe -);5RyzKuH%e-3%g8+TT3md(ql9HJ?eTfob+xd@l1v@&I+7iGc@jF*|=e;Q@-y5-t@6c4t@>1@Ld7y2zPr=_Yn45wNHXDvRXgd6 -Wa+tzc~eJJcd)j!>Ts`PJv~jmm2SPmjS4jXiWgXJaOpr+p?*b!aLJ5-)7P@f{=USc1Ck1YfzHB&$KP3 -qRk#&}Z;?s%0kZ0{IA}JAFo0P=I{IV=kMe8NR_y%t!Kg67?Mf4yc=paQhNyUt?f}bf1p2^&2yM$HA|= -kxrFa&wrr2p($z*dg|AgsV@)hZ6W2My!7r4LnT!Xgs|K<#fudsUP`SYUhr4v=DZ2@3?;Rx@M@ -su$`r{_`7^HcnjcyH^I5M8u1hMopHNM>8O5Zffr9-s0Qhs6Y1>PtSMb;&jH_Dq~B+2_GxxZl!*Yo&?& -Lgtm)J_hvQtCiq}T^Hd+-E&FmMPc_LHeW$X9W~ -%{$Z!eXbR7TzYo@!w;GJM~Gy&C$d8qnueOtwWNe?vcI0S@0J>M3+F)AuyxNtb-~hgr8nLVNMB)NOxQ# -?OGC5kFJ6y`BY>WKZ=_ISU*F4CgcfW3XKI{c~8acV+q>yQJA@d{+UBr&%rgI>vQB%lEJ@OwY@Z>UAXT~dR&8jXi`Gv5&6S^oaxYH%3sc_QeTfZwozk?cdZJ=N6K%3+(X(b*$ -M^PJx=sjPcZ7VK^T$+GBQ(BwFG2B$6gAun3c_Fk)Q=-2WbGbpZ=$ft%vg>Jw)3;Qyp -`}S!8v(Sw^ZltM08v0hPl?7YSycRI2jrrasP2QxL+(+088_w}XoAf%$_9Cx+Giz!EZKy2zM$bIYzL~s -zeiiA$Fgx(;wTO0J8l2@juF<55>>TLqy-C1h@!+-EeN^s`s4ddbXif~?luuvZ8QS-7eUHG_QQe~bRK4 -Ad_(vhbOxPn9k(pG@E--;7jRmn3=A-*%M(84brPAh{I}EJD!&oojo$1T)S-`tY1u$Z8JC`VM>(_9Dy? -o6EpNE%~%iGdwy-pIn&0kfIP!XQFqiYBYE09;1!{1&oW302 -iysBJl;o2qlH4l?7iF!I0^lcbIJrzS=^8)ykCk@&BeTDXZ2x(VH* -#pDyzx3(*@qV6^h4h(G=_}wPo5xDqU8c6SQpneG)8lRHOi#3VU>g@Egl*t8Q8KyrqM(yu@EYHr0FPqM -w}1!5e;bjnHIH#W@%D>iT!ic&bc;sTSsL%x75%x0k%AN-)v=pi5BJkX{1R&KL@de2cSL&l)M`MV{_Ir -yXgCkq-%;48f005t?2r})9YDP)@ObT+YclyE(U=O0Rf4q=?P@#lz-6@LE7#iOULY<>N4mUy_ZUpe3Z^1CHY>!_X;g9*|S32_o6KFEq{yh)(U$2p|`;<`0kw=UKMCF*5chekOeO1 -pDpF~nS7a2@_}%zlS|(CqAowu^?Y5+Px6YkB>3H2*71Nt#J72Q#8WjmBai5(21H*+x*s-RJMsJTueSpKECHm-ki$R;^O1LFCdT8m&+=Z**9Cf^J?#)3mrkgj28!l%Yck+^ -~w(x6D{3{ty&UT6ZEV0E2KZfdl6%7Wu?WwYi0}G;Lp2?eR;D*8Y(Lp<EMHI&Bu2O^K7%^vu+oD -7BdTiX+bW(^7X}GoWe29PCWmfh4oA5hrhC|8g_0E+7X{qfJg0osyp^`b^g1u|kE>+ -C1F(Ayk+KAsM;K>71Eo7Ii{B)cnS3mLu{{C33HEzWNp9O8{L|5iSp4!iOK%{}-Q^pJ2T9dv+yKgkn)K -y@<3z7beq7^k2cye}iY^R9_<^@>ie)Zp3#_G!D6K2i$4!7jX}v>JTWUtk_P+J6kbhl66MscH~*>b3lO -gKr3YapD_~t)RQ)i{(qot2}S1!8e>F@;Y?ik8PrQX=!q!(+nd{89{q91V7Z$YJPPXc`79HtBRELs`(X -lTM2OUO6=2$w0VaYe(SKo=_gv7O=PDJjLRpDV=a32=z^!`F}7H{avjrjSJ1%AuR>q0C)@j)bCaX_!{+>ecF+CUW@Uu% -9Bjn$sQm?IhOdah(JJ|oZQna0-pT~oCvPI4qnGHXm_k;7eXn(YS5L?bJt(<)yUO6NW%KDpy+$2WihWN -bT23GU7i52!wf$wzctJ_kfz8kHj%xAH(v}D-xlpS@qrCRE4n|D+8ZGJg>Y5q;hZHjefP1oH19WF^ZNi -kDpYTxh4$8h6nmZa2SxoF8&)4m&0k@vdm_k2k~9&aervk-8cbPs>SjI`8-ROGTMbJNV4?zsndIHjbM> -ZMSvf^QbU*_^zrjclzGWmEcmymLOR9@(5+(dH;+hbpN|(e@p_1@|u|%ePrkhpniC8TeCslt%bJx%Nxd -D4WW^4X{3n-!SSz^{50s%TO1>D@n3!aY`0mCyQ36ehrsK(7Zu=H>A~bC1i5m#Zc`6=m_v?_au9H-?dY -#(=yblLYlgzQkt>_GC9(JAym7c#?dvH%Xyx*HYlYH4!!R_jbyu>f9mvaeo>;S8;ZvKli-B;2-*e+C4&JMYGi4DD-Xw_S7Ox8|acnn`drTw- -rj$+J+aE7FVRQL+>pk8^D_GlDs>VL^W{G>LFXPW}m`pN86-s%GW6?jb$mK4Va#5^c}(VN7V&w{RwJdW -w~=?Qv$wO#C;y!r>TJ<6XNzMD))cUX4al?slmK*XWO75$xm&^ljlO)>rA6dsja#f?Z1NfB7X|YCfl%& -Y{L&rgCtvZ8SsxmwwNpeO(&Ucux0j2=^D___^bDR7>|G -r27Ml_(<{mmR*R6JVw?tY-HK6hnz+d$yT;#XLf=1iUU3(eV5FE`hm>$PxtOe6o0>@SQ%0!&{-fE5A7bbRTT3xzPdMMZ4&k52wo^+0t3QY9>B4%^&K@ojG3aZ`F3A`6#f1N7q_ -*7EhxFn1yniQwy+Attl#%s{d`4e54Pw7-l#cpTsUK}R^&zFeKf(QZiTCI5^yJfq^SlVUa(|Y_n5Zv>d -D7euvcNgKqcK-y9KWb?xX+Wp$JWyQC$luzrNSoyKU@yZ7tyylq5anR+1#huS15d%dgC|^xbHWO1?)z) -kmljhb2|7C#)Z;_@!qM!+rh^Wn9al@eGc*BgvEeYN4x-hD3S2F_eIL5v3PfmfEODl=wl5g-6(L+HSxS -O{wTfITI~>HXazhrZMdDofv=oe*)TN_i{FI)UfuWr$;^>8w+D{ECd1D;Z9{p$bx%gA$(Mx~4RJ$N_ob -u#b=6tVaM?jxlv8zZ&D6kR#L!{vi)g<GIl;kNjAYV@mrf&tr4Ft@+b>zm>{Ai@5PtU4B`Vy=?_xQ2w9e*^Tyv;8mb~ZAoLi4 -U)JV+FUQ;`=0>ki|oBW8{zJ`2c(00C*=t~;Ar9C-A-R5m>9GznJ -9PZAY@{Ak4Zr)pc#kWh!K!XOW#Ki80pI^JuDEw$jPibA})M7tVmJAva{e&lwrnt~H{(Z2ocnk2Ym(}i -fxQ(0I_C?V7m|?0vgM2X!zG9YZ;IkAa=xERfT7|!Z?@Nv&Pq-ZFZ=`-=!iW0*-x(`Ca!g8W)BEmG^K7 -F0mk9zA?WP}9qy3KZCz}1>7C%U2k}++ls9(VB*xw~K4!UYnN9Sw@p)Kv2{g`^4K>Q3dBYt$(^PK2MH$L1yRNpysN~!5`%f9G=baBJxZ&|r)H?UKf%c7W- -*`4HdYVL7JU~CJQ6&}^i$ct1iU)q=#PisCx+lNS>ZbjBklZBXWL0!lc>$2m>M>3ie&OVVPc1W1g>o$zbSXxp(@nleIj%8N9p-^Um9rWK7KpbO+^*`Z}w!?)Bt`sU-0HglIO7d_|yP{zRtadb!5|AiSvm;!siosR?go_ec -ioo(orpK9lqt@TR2_0md-8u{%9T>jW?f8ZxwxMOG&nnE)&0XfJ1Uv%-aXiUe@hrzMp(V!05hoOS?Z)- -F9S+b9CszhSAF0oM+}iw%lgW!_3Z9fXB^fn|l+O?~Od>Tf0i~!GEsm84Hb%M?~w9m~Te-D8PsM!YoPf -VW6`j&WrZbZpmjIrB3eC@OCy`!#NZA``^DcShvPKT7oom-++5R+F!>$=2Ic(G~Q0QynZQE*oW_9b6Lm -H_Y;Hh{2;D3gIYgQAL)rb@?J7ccpp11GCo()AiHY8pAET@c4KiaZ#xgi_U)g&6zZWd$F57E0vf|b%u= -{Um+vgvo1Nv%my7!CM!rqp=PGU{|T2E&v+5<*COps>?TobLcey^DPO(Iu3J+T1$CwB++Y3jVfaim}*JPT}87Y=7hXSymlx|L&OR -XN|W#3#~N-pZW%mpp5rH&nAtwH2*S;@BRFp&Gk+cy+yg^8(7UysI!{-_!i?H^sFtBj7_{>h4kX%r9pF -Z!v=?xZlV6A6Oe)7kFaL)uR^m~^WbtZ4)-;e7%v=}tu`~H)%W2?^C4Vcna_dkL7g2;4VG(on+;;z(gV -NXX7SFYulq#0MZ;OB!TG5ifwO25I5eMC-+mIjxo6R49!NVxb2`sK4&Ng=1pHBhiObD%jtFn+G~Se;u5 -cK73PT~pOh!NTr>{l%{~@=$8)}zc`nE;A5IQ1l7|QC*3AM1|q1CK-nC3mvyw)1l4SDwseM%0}*!^(G* -d1zQfzW}3(NKBfXviQnhuZO6BNvBmN17Tr_&)r!vnNd5p%;x5r|^0{g*Nd{i7f#hy=#q#>7i$K$u>gc -@h?iFOFQ>39$AI^8Nl^Z2G(Oke76>IOYh!;&)$%}{o8eSxT7lZZ<|2-amMbB4P*y(L{_#)JbgXcMR9V0wcoo;ND(c&9AULqyN$in#IjphHYJ$Vz(b{N5r=D+_`Sz2-W -}2S4yI^+PE0%Yq90RvP=Y!pC;Q$F|C$5NAKvhSkn^}b7v$cAr4BNhj+>xHJBu^Vl%G!CtaoY5VU7HNodx2{ZzS~vfdlRc^y_R*9%yu;XHTVXntHIdVX|n!FwAU_L1Mt3v^SLn2uvIh%P^ -{U>^cN?=yLO4|B~`hcPg*=uPHAkK51FSid$(6V2;}D{toM&uEn%UQz^|GFQoZ_% -HUL=IbZE=@U!l8@|)3?)FTF@ctxpz{YYRz^UZ3o!NP*iQVaq9)R!^ei@2vgF(=ygE~YG1B>N&4#p7t> -*R106&HNMlonI0D6VU@Wt~(9isXXmacs`j;B3ZThzXje?zgxx(q6PCwcz+PS@tW_Hc9X9}Y3ECXlNww -u(Hsiuqf>*_|1unsyF<&#A2PxpGL4=@S)l{0IkXn`S>RU8Pee2@X_lduyzMD|bf4_X42c$@yja -OOcb-Fqy$EmZE=C3>q{8(Qx;kQESD~5~}3>y^JkiqOKk*s_iQkl_LMDJ)WaFP*zEn-)?@+Q4QK~kNVjqyYob1J*Vxe_y6+2xYMr8gt#N*;cZWNpuL$nh -ncL->g9qJ3B~M9wUiSBZr*#HTU(@hA_lE9J*3$7bgA(tTwkp6)VLp0JO7ZUKLLKi#+hgW@=qp}>cB(6 -b>ST3&AF;wI)NQj?cJw$}q~D_*yrjO)cC<0q&lG(S?a;CH(D@eFw=!BEF+~lA=SnlwFXbbA-fO^P;jw -!!+Q|+N^6>FXZLUA%8RqNB6h5zQ^Ar};`%-RM^l{$-KCcHX-oG;G{FM0kPPrO<(dM1LCK_M%cf&Wj-= -uCyS!Wlc0! -v}Go1>Z0`lNi4-#`Mn~iS>gLlkj?yPc)q8?3VUWmV#hDf2=}i<`eta1ujP@1eD@=X`7N?k+)^zS7iux -VThQ<8sBg9#bp)>UhsAvF&%&;_@P1928XPiM2GH(qb4ixA<+;q)n8t$lw61*Vo+T{s3&eTuMQY%r!Mc -16p3Ty%MGcEsfaY~lJJNDJ3*>0W_w{NZ{{?SO1D;2;wXOezx*Ih7ON -|;GMnjx;X6RW%f>ccR`Da6g#2?~KEI$X&glg-+w;uuK_h0(izW?~$$e&~7!wvbM`7{IzeC)kTN9PQU& -JR-`TMX=D-@# -J&X4H*;Jnb`M+M2F+olWG&+9sI%RftL@z{?<@D`w}ZZuTGE1&!f|Y{H%>5KRc5r+M0hP`7!HkP15 -Mn#>doVG8}eYjQOHm~wS$l!3*=)ndacgjeSPDU1+wDl$LPLz>@XLT~7vnA4*iMB+FBQxLEY>8srS8Q(uZDdwU>+2X@pBFx -Xy;rOgw8Nj3sPE)DTAwg;5sj~k`&o<3T1P(AKXyCQuSZkOl-XmE)>06xFT6@PwHY#dDTQwQB~LsoeilazZ5Ea%oxwm%wFk3FBaf-jqy}_l10kaZU$h@tK`txsK0h+Hxcr2l-Rm6)ce<|j9IBCwuLDo -d`c$P_GzU#{0iMd()+aZx|4Zb;He=LAUWzk&oih8Mv`La)gYipmz>-s`;T{mm|qWKr*8zOoTp0_HcY;;Jvd4`wc- -(301{JUSrzXkuxzyEpup&illuS>_juW9_NkKvzJ$3I!ezgMya|L%z4pOVOGcV2;igX8=|yB6l3Il{l2 -H2&S7Cy4{#>sAHM1Nmm1;UT*CcaXLUu`57T&90Uj!I4^H6WAFq;!uWLL!Jb{P6<%Q_ -_UXF)<3IEPr1^>2XNBH-N|K;ER82@7DOYs;3ZL!59RjvF3a875no73UD|AYS}vy5v@zd#S-o;jL-`w0 -W<$kpz@kia@-Y4^|IJ@x01-))v=aDTg0;`=vo7)d6+Ue4Bl?@pSlN^{kvX^Y%QNBcX_Tpm7$8S?QS?k -OLwZ#LuEjqeS4iTR_Nznzny#bL?tYvj`6ek&`y2XKeVX{{S;ra6apGb{F{8f&^}50hyLyu2=x;O#w(E -peN~{6DWr;{K6hCp?4KY|NqjvjgaYhd -h~w6)#=gwIz77aD)s2|SD{DEnjAOB$T2-%?e9XI^i#F#`OCo4mmjM~&6*y>=F#g>Z21P=yRaU`j(?60 -@qIu1Zp7kYUFuJR?|YdxtXQW@|0YFr=`CYKm+rrUE}b-dc)HY}^(87=8|FnD)=0W!;JOsB2wiG+Yr5o -)(WOS6F1hu(WMIv-*5V$}X8427YA^JN*4FOg^Lhl%3p9D6@g?YC6{RyvX3Hlav)!aa2BAaFX4i&JO@} -&TbtsAX==-v99kOs8YPo_AZ94xqwp|9HbN_yoI`_r%5uMv+x;mY^>wL7%-90pMjGE+olP33@V&tBlul -9E#PWq|Z_55XE>C2DRyG@$j#OBfKU2ORV-Ma~T_ZgYDUpaB~&V7Y8>@#vi?`Yj^Snm>bde$@OW2>2kW1!F7r9{qg;XE+t)7mqfXg -=g|Any5##4bZLC9ZbX+hXtKN^MwaRMYJV5vq@Su?&tC?XzWi8S+Mww|Y#zNX#g=c-y(3+UowrLmG(Of -~jvi}R-@0=2oWy#164YQ1uB&cR1JrL?CnIJ(2OeI-g59#(%w<^O>wT-KKRW+msfOZ2KVzpWq`5vf(pB -Q>oY_|-_I{g7d)^FC|7$~aBu3zAE0 -RBXkbFnj1XrpzXU+Ab?+k{Y6_Q_W|7W3dumRQfnG)|0Scfz;j#aVVlqx-}w%4taeCyr|ZRdNiQNBfdt -gbgaztw%MnBQt%Z>pj(q6VaQn#zK(EoYpxHWc-pjHdUsuDnR=iH6SkyR#=CmJT)oQd -Gil$34T^uZv7#*xX}*}J^0{8`C9%NgQKv6l&uv@qMOrgVdJ0=adP?&qV7m(I!N0m|)Zjtr={T)s3R(@ -4?b7mW)}{R>(yqCN=8*LjxAd~6a5`G2w+8hyOF5kW^xZvI>z{lI@eb`JA&dS=&}?Z-u{55yaf+w9F+n -OeH_u25=N&d!{EcMmHQ8`VmI1dW6SOCwetiq?Z)9{fSM++BE6N9C_9WTiE6E4aU9EhaI{SZ!d{`p#G4 -BfUQA+X=rqvbXBjxWUACIx-4@Ev=#-;Q&_Rv|{gH;W3yT$kJqdoMa`LMcmkgO>FxG9)N42D -Pv!AApZ4-Yx$hx9r&xD2>J_?`6%!5)!hxwl+3X3}Xn%h;x@0}-NHV9FXPd;D_4u!^=JU36b=V(goy)} -e>%1)juICMs>(jt(g8kw9V?n1%Q5P@Lko|i3-O%=@Aln>|=A1OjOYQN|J -UT-%+jX4JWl#6h+*#T;_c6!}$v^I0`Dgha)B*F%#F}C9pR2`wK<;bAysDCYVjU1YTN3zQAcqkadxgZVC3s<@?}g{^_x^J)JU7|8g2`m4;eTc5FP5QVYD*zU&6CK`9dTr+`7$y -zTPH(5fA?}Sq&82GA@jHlHOG^o_fjJ=^e?Fq8H%^QV^8bWJ>SOPxo_|JcI>>Y$=YSu&C%6)uz|4kBwr -*~phIngAzH51xIBR^GTJ|CD-b&otq4DNbzd{?n|T}ZY+o_%i^FY3xDE$&b$CQqhj9CL>fLuv4dZWg_| -$N$A5ZhHx5UA{4Y+$Px8|GKp0u^7Lo3=q51pxNVhal;Z%#XCn*6TtWzZhc{-eF!e9y%>J};1%E|!GH0hZG0SZ7n#q}xS>T#z2BYCz9SuePp6d8*mfyY`}ibv7X2;Sx=veUy?d9D750PQQ)vG -!g{9K^$F~tXC4H67nPIhS#%^ivmlAzS^P16y40W5;>^VF?*ZK^_+uxVWKJQz7#ng_6^G=p$o|fl($m2 -ez77sDD+-UlYdL0zaT~m)dQY%p_dig6b5!}}=<;Lxr$2du*5_z(M|Ekj(vkN|;Wrv+A9^)#T%I_eD}H~e%==3dbp53SkNSQVN$~BfBl`=MUJTV{vdBCy -YBvbpDx|5uM!o8?OuDkDoNP(#l}EmJ)`>WMrZIF%h@lhh(;lW1txLQTo!-ddd$e;ptpc90_1?;&>rMO -JSakKK{yeHP<)eCs>#>nV>had`iS>x~PjzE^sDsypm`|(20#l?8StxTf)LG4aXxc}D&IJJcun%31y1W -m&L+c(FpzcKTW3$!fE6wApbg&Nl?1}RWNl(8*bNICS_wHZPMl_JX`wZ>dhsF0d$M;CwKfx~>k4tpxfO -PyWjK}ZEXmnv(ghsT^aS-$g&k+n=LVOxi*Q@1skq&?TnxJ^=F2#9#vWxa+=rFQ^gVZm_ZI5My&n1Cfp -?&CSy^&e6CDGgu+F!{5yRjYZj9tna$ym632d&Zk#<9@$S7l*i%u+^MC2R_RHfzsj?HMq=W1+&ma@Gj- -Kk)j%ci?@{D5qOik{bA$rmJ5+`eD`M*$XX4?$+1C^!Kbs{C%B*2BUwmf!38VIv>$Lih9wSVOnFheyUh -ww%EXeqx*~6!t2aJa`uP^`k4b9`f06jW?xaPuJvo{iSrGtXCLnWXcu#4cfq%F7{pq%&rapGTC7FuW=; -EO4o{r5$^E*u$pq_IqNn;*d;GQ3q`Nyb9CJSqXS~q!#5HX3_jiiF&z+#vC}@=q+-Yrf5VSh4qm@geRX -XZJd*3~v(Q0X;(pF-~8tK#M^#SY{t+jT1MBOmi8th7gSc9EwkIaE4x-pGzQ&AR+q1#oh=?2}bfY}mfO -?Q}1gsT*XZg&FS0~*~X+hb0&=Pz+u9ZxrO4}M+hK92|f2K1d`mb2-&voUtd09?F>u0XfgF6B&M*{v>=3+Cv=6jLEY0Ctr-Msp)! -v6^_&)(!qu`N7mq5mgDG$wSY{0h)qu4w33p%>zPKm6WjHh#dZ#tG=fsP|OIu^yyu|P-1g;${C?}(1HK -Mr8g9yxkC=Ecx))+P+Ax8j1CWk|QvGk{sDHOdnHqT3Ay*VlIm7 -zIZCMoyPdNPl4y)q -d#A`$REWkvc3-MPz;x3B6l6dTu^i0EGUva*1bg3Kh8e-!@S!RXdJzZ}B{Zzx~j|uMMms*aP`lHoPiSA~HPwix##Vasn`fj|jw#+Q?MxdzKX!*iobHDI+?l}m99Me(wUN+v@ -(D9oMjJi<&L;LapgnJS9W>h0m^JyV<7KU%6;&4PCR(;Fz>>6zL_lc -OV#$?8b2Q*Hjd8)uW#r3dE6|7WzeSE?6NS#))TT0LNaH&IVy>Gp(m_;d%-@qdns-cpkxZ*pTFEf!vS`ZC;>m7%` -Y#4YWrB&1;yiZtz~B^NG~vlfbD0v1q;qgZ5#_2aIGqQ=TCXCz-?XayVjN1{y;mIBvip{Oohs20OFR9^ -yeYRh%ynrqglSb4N#?pIgP=jTFFjNtYzhSz=+AE& -Ule0yg4@bDuo+}x^e^{oy-TS~_bIe(B=7GHlUcRR+jSVRq)^D(%6M8B3v$6Si%oJycH9V<~{+ae?mWB -WCFw>s5Tow<<(+@i1lq3xquoyg9-Px~H%uB|#A-e48_bC2jZ=KFAd`j2$hP{zn=nm -dE`h~@*4@6G;M4fa8Iw#aJpGLrrCG;cpi%-jDwYeL;?D_U79^}}wtR^8wVi9J+leL@&t@F0zNC^h$* -C~n~Ml15elzoDaF{o&NHn~%Ky$MJoReQW7FfTy3XrkMQQ48i*&kgKkrK2uGx)!H(_3);Jt_7PmDlgn$ -Z~}%ugO|j9n^f)$UHxluJxgns<&`gW0|z<}fyT+}%Gj -Z=rigt(?ZEr%e~iCxAIXQ1b7s?@vk>0nmG(&`AND`sr_i~~;r&;?44oR! -A_T53iayk8gKWTN&{}_Jm}c_T2e%Y+mc9L_N0Yv+|iI22ch$SjV$&&z=`K+htG+peL=~_1$402X|PGg -?gZ?$EaG1d!Ve`nJ_?Yu^X?KU>iSwhws>X9J}^O>9 -P*|3@B#BElCwuXC(sE@^lcAVs55d85$b_l-h23FH|h5TkMzgd^RRm)HNwt8eUr}H}PJo5GrLcN#c`NZ -?cCqKs~sM(oO(D4J%<}7^lQONOs;unD3SPfdf0lJdEJ1Z%?r|{%wlE9zuQJ80&V*c%p7K1XfS@WZJYW -7piqnqY``wyY*i^oDyX}d0lCit1Kv;4d&(q%K-Se?`|{h5=1yC4(j; -qhOgm?az7`MQ{RDNRZyk`KyAs(U+QVO!STWI;=xLEGZ6~L~7dRK{J!Y8he~k8fj>Ef0K!-nAA~yX&+C -w1he@ZhJh5gS2y$>wST%`9)>6|$Q?J)!I@6qCqHz9u~Acw~xk93xaXiH=D|7W^>Jl>DPV@r}-ij}sbj -_dJDmsv0T=@&d4cN@q3lfe65ED<}g#uC{hJ630icG<(B?bI%uK;1!ezu3ns(?4JAxy=3djA0%_`DxGP -Puk+)JMN>n$X@+xrf93yPGLPHH_Ii1Z>5wpLUVfh_?o=Zm+X?Q(L-zG@E*P|^%ourD?aqUPE`?nrU(3O{XCa=J|TKp*P^ -J?d`z61X9bM0Xh=uF<7a>*MHq|ta8{C3bMHXXI)l;$g->vy#Cs^azC`WWkb0>3Bm`#OGG@!N)9JAOSf ->+8jj25t9$I$peuu=(K(K;1+VT{DT(unDJ{=8mKcn*n~``v9t@IUg0mQ -K!fhnkyGVW0{n6(uMaRbk(*l@Dh$|}ruR$^|SLPqwp-7fjMLCJV%k6hL%KL*1e)vUn-sxc{uJ7sE^#@ ->s>5O#R<3Ee$j%W52@xC3%cSvSU=d(I0uvzg-EmgB5c)0u~*98YgiMFAyL|vckM-Fo5+sRS;UmriS)Er2&J{p6*zhmNbeQDBJ~y1` -LaU_E6~@;2^_X3@wq_0eBeQF=J9o}LRP)A_!$tX_9m%mCv-D>zCHZ-rN2Sj;Z5i(rgx<8gtJk^bWP_# -7drQB*~3ze3wQxW75Llp6|H{yx|6@}LAeeC&39pib`NVZW>@mPewQQerTW$(AElwZ@8m}4M`cb%Lwy; -P4smPNS`lkb_`V0pFpo>#`PxP2~Y`IwLXazbR~#=nTJ9DY34|~^132oY{a>?UOLMeeq -<};Z=*x(A^jc1P>8pD!~EP+zNg1)i(gRb{DdBwTTuZ%wB!4mn-C9%r0x@|DV7m2FyTn;&--61?atHU@ -0Ydq$OeDyyb`q+_TkT>UKEGZUUnO&WVR8^6Fh#f@%C#P@}kY)Yrg4x*%3M`vzzfVJ!l`_lX$MElmfJ7 -C35b(&7VI@?0qLq6 -c@`uY=kW6b}_}U8>`JK_@l+pbk1S37y=%X`(cA8i(#pM+y)^i)9n3EN(!}IG#agH3?wVvqsR^dJZ*}x -khPDfWC)9ra!e^%8Cg+&9Y@m$ -mn-VLxPFO#O7{x}t90$5g&QR(KzvS&*&zT+N?=+|$_qh7&xLwFZnCs_C4Mdpr(GT1_wawAK(+{kL;^{)}UrO3NdqhqN?$E -k7X_`rqG_E?HF3SkK|fY(^DyF -#|SZJ#2=MoeclsB+-`&-=k+0w;36(Fq}6*1HJ7ib?9ve;)y&_x3>YqjXEo^9gKN@R?G)Km4b -cATyA;cIr!p%u%}7^#BiEnmC>nhD$iSM0p`=zo5P|dJp=@SPZQ42r9Ua$=*L$K}ILG&K%NQ`vq;ty3kL;DDk@q)k*+F{q0P30(El*RclFN$-30;{uy- -;v1p-s*uzvx(-h=Lk(4FE|I9cOAdEcqoAt(%v~-p82_=onGpnARhtoQlMMr=jLvuIA=Lx9QbN`oKMra -Di&V1N*+HA?4Vf5t2>icvoXA_qu7bZW>NxQn?&aV()@4!u0FxHMJ9hxly?yHAUM)Kt8X)|j#8`daqyk -Llaj@|L96d;^e!QihvxHk9XA$xEh^s&$SkqqSlckmAkL))tZewqjdWHBy$_!wEcEc@Fb`eFzb5CeG6; -PBp6A_@Z?~Av5navKtrZVt!6&YP&cfduo^9x^Kz(Rmqv73#{1a=L@Az)`ukd4!-O7B;q{CT!%~k>RrH -AbR!IYVH4sZ0jC)c-fd7oI*G4gp0X?|rP==h2naOEmBh^<6@cd_xaVEOYtQ~1nT?QGk07oGbnXd`Rq- -imK}d8Jrd+U4Ya^JYG$I)SI9{-EQiAIUsF9}%$@Pb=m7$e-qLa}&mK(9SdK3Cox+Hg}PHQJ$UQ?}MiC -e0p8r^7eFO{w|HPmul@#Yj`cW_WZE6o}9ivsP)f<_23lXc%P2M{f%_KGxULCUi18c9WKQGTeNZz_wch -f39eg%6Xpet2@IGbanoZaF|L&$e8xSBd6^iW*3R{bJ+E0wQLF6hQ|_nO#eSXIbWGDJCD-X=njSN49l; -B_a_-T}3B#kl(~=aG;i -sJ9*Rj&3R(?L^v_;NAv6i75FkYRT_)NuHtT>s^B^2LCrm0V$Vj4@M=LFriaGsUO^JTv8=T<(B+Q{#{e -9Wr_He@r~S?$EnA$fE3bm1~k8j&5&_ZOIusa6*KzE|`i>c?C7`&^SR>bW%XJTV?N_*z}YvvgrpVv_Pc8PewSwTJfmE2YgjEeUKP@q8 -_G758ENR&k}okO7=xcmx0gur1_tStG8C1F$azr%&u7fh0}X|X&_P`Z9auj^f -g>E>h}NC%2G6)u^ck_D#liN)iJa8pw`z8-^4Y6zk_;yPI}H>pe#N-+HP{AQbms!HKX^8)!P~U^PvCk~N*>u|VzpgT -=0N&bc{}9rj9K*@cDAl9U}qU`+fnBQukKimx(!GyNa^T1rK?1mlhnU)?ZF*M_}-t(d}xdL_jy`cN=LBFfI(^JAzg)*Cd -nkyc#)<;QaG&c1^qk1X_Cw=sP$W<#aoXk_&51HTpG8bG4~|EYShk(rE?W8fS)T+ZiB|p2JrJq;Bd!+U -kQFDutO!u?1fr9S0u37AY$3`p#Lan`wZ+_M;<#wa|UUg&CBh|v=R8qwPxgf8urA!vNX6DdBg1{wV5>c -o64LgiLz*oiv{W3DEkvgyI0B@q37keuaiD~;n>Ac?Ri78{~Ug!h9o}jcw8GJ^YD3F8U9v*TYb4Xxq~G0n%_ucC1HC@cI_DKE%$>o+crdVVBt5BpH!jQxEb8WMfg&u(VO -q9sPOqw-2Uzo?dDcG7e{fhZbcEeMds(e94R+Z?mm7Xx?Fy?;zuHW8kIeAiOT+Y*zee?v|4=JcRpm`?M -=))0ba-7YVjEP#J>rN{ZsUIE1Z7I#prJ_@#EHXNsOD88HCOG(QI+eo})bn#o3`Rk44}_pR*)j@i_xWL -)&S8&*=FGx_Oeocg>}ngJg55zqA5&w_CHjv=2fZz9H7z>Mcw0Q9m`w$!H?{Mm)PO8OPh+$>L#xU-xQx -a0Gc;UNr>qytVFE<-tGE^RKjLzZyJ2&weJJf1n1B((?}@ZF{`>kw3sn^L-LC`sU6S`v-n%^F89$|?g()L}DC(I)(j-E -$2T;$APMed372#XgOBIDOXa{PIO;qo7u&HFQZsH`Vu^K+q3EBU2?3Z^WnV>i8ChdhOlr9PIj)FH7YeR -!|1J=G|3muh4!3sG9_Qe~a?7f$z(}_bK4|6s;4muqqk@Q -ef-IUXV`y>dnyhX5`OAIe(st_A-(A3JiV4_reaIg6-;uer%q_YAnBN*zhXLUh;jkJ3Ufg@!PPe_aF_? -u=7W(X-Kv&I-lmvp?m5(q_M5eABfm>KJpYG52YW%H^OZ_@Y!3&n(ASnPZ<=yw@f?3Wv-ZK=H1V#$Ora -5B8&Ih{4?+8q$)n{cq#JU`$tw)8D1+x{j`!eH|GhI*@m)dzm`Unt=}_|;yD`sQjiut{-IvrPWZh|dot -2K&NNRGX?_6u5gpgydJpKt_hIQsYJ6QS) --BR`EaBTnvK({zDV)o{^?C8==;g(TH -fhZG>@cZ$K0mb^nU+b@qWkLs?YHEA8A^hMDIVMz5htn9sGTM({2mB&)44PSKZFv&ucniruXx-_wz{Z@ -qR(mZ2o?M_I^QCIe)*fX|+P{7i#YpR^7fRmQTtzCH2k+k4rRnN3?UHI -PKi|yiDHCk?a;p*{-nNyO-KHS)2vDS>}Fj0pAy1O6TodfC)K;57O~lt(|*%l6G#h)<$?Iv~#pKOSbFD -u}C|&n?>5W4QS`GbnVpR!E*c_u^0 -a0jobd33uoiTI%=Pj(V4sn%Gf=kjv$|aA5)1r3#P^GJEz_raK}+XhK1vhso -SvI>>6r!>ob$!TBL(_&8eHayq>J@yKCnlvDRjM<&H@^*T`d;2kk*3H{Cn7yz@KRDyEcDlKH$D+99_!e -_kAA^kp0M`dC8Bc?O9{Dv?HG1S|laCw?<+qkvn522Ur(pGK%Q+;-=Igo)Roh -xm7T!nT4#pW#<^_TS|qwR%n;*z)X&+YrGHrm<0~PO70)*6OZ>pD2IKpC6V_L{$k(GdYx4}T#*OCHen$ -tFV9pXT?jEIH^r@dPWbm;YnnT6Mg2b5J9%Yv_-nYIMFwW54yU<&L+cy@fZFq#Qh1@#XTF5TDr+N+OQz -0o9_aKemMX&4JqK%*YD$Y0xje~{b;sQa7G_t2GTg-=#?!SoFKPPB%0W_&-QQHqfwuraHXW}pMkMJaV< -)0LML!nq*;qdhvmjHX7IHlru-pfo3!&unKK*H}aM=u%CF$PAqgZiy#Y>Vcv(Y$u -z56uUoaYKdXvuJgrdMRd4HOc5oNm)ZjN${KYBnsD!>ZRxR&}`Zd67e|7rFrMwD0k>1teMK9Jj39h3wbJsg$xYe-d8*i&kb*fWy1Ol@!olc -6{k^MCXtC;O(v$0OrT6_j7(S~GSR%&B4lFr`+AwMUQQ->9=9y!VmX@aHJ6i(EA`jYTE9`q0L|0B0H5H -Dn2W;K2(D>sluwA!Nq}*~z4F(+Ag$QhV@f-t=cG-|8#QPXf&kINH)_&qL^Z0~=j>7JMWA4WJEoOD1 -3bOja|W#x@QP7v)Q=umSHnQ$fFNpT4l0{N76|i)f!Q=fNSG-&b%i%{4}vZsbcs9>6Iq0&d~;cx3NsJ_J+n|cpbps&SY^G(u8l|_1e<;1ANTdU -|mjo;pXuGzup4BY2Sh4@N;M^-0@0h^|P6LePJP&UtQbrCj8IWVB@JQJ4_D}Fw6%n2wLPZ -$W+me9jgouLNGH96h`(11A%w#e_J-_ -pQ-hbXd@|m5z*Y#P?de-wi>$%jNRkj}E@BjDb2hJJipve!sI;_svqr;K2ka8oBu4lFlZ -M3Q2dsiR(ez!YgL0Km8tv~NR_E4KWjpG9a*E^^(qs9BJyGN68II-`m>VH8nqOdS1Mm4M%C40BLYGl~T -T~l$!|IH@G`|pUU;N&@$MW_Aj)wuSo4>6eBFuhh^z-AWe$qAJzB?xZv%{GGk?Eqc*Vs4L6JCJbWWa3U -Jq<&tfScpW)Q)n@`(e*$nB8r(f3E>E2n+6;FBkciGH8$2bTIs&rTa_Rca6!)2D8zr?7K5 -GQTP?3jjs)F?n9sz1k8_8tn|4D)O&u$tM4ml^n5|XFd`Bp3V}XC-F=_+b^ -Ik)Bb!BZHCtF`Voj7O2iV -UJG+bTythVRB=wmBPn`d!Fx{Ko&iC)7DE>r-Y%-CB#ZHIntsx`dgmk3G*=A0zcoMb)SLJ5GIB7c_zMo -OZd`v^l{TgPf}!_`JK=kuX%K&a5O~AMj4v6|IB3fxS;Z3^}g_qL}N(_(1DXHg`*;tUTkE3*TC-5B-3p -{g5`6jvRasa!8gQVvg*A2j}PIC)%88c&3e&Y!heZs5V!qd?03e%Xp|LKZrA?1a;Pd_m=iG#4(I{A@C{}=Vlqs&1x-6o|E}FC$ -p6fK#q4iaP$QwV~&EU$F0Wmv-cu<{C)$@%yq~A-G)7er$@y~cN_jZ-*`{P^hjq}-anobibgq%dneXe^ -PbwD-;HWtf6t^(5{}n`K_e+nR_;7zXUur={?)e+LPoX?U@3+X!@Q$M45-SG9H_Fl(IN@zv6DtpY>w$yx_& -dz0@DjkEr_MjT>$ER1ei;>JNBxrgW@dyL4z_h6r_`JlBW&Y6t#a!rr=NI9$N^P4Q^;`|&ZHLzXetDSH -I%DDN-An7KCBgCBEX+#j3NRPkfraWBRcTwOi14`Z5}v+)Cdg8M2-n@I%i -SHQde0)KPK>)`L9)JP1Eh~HG?o}BP^1OCL3s@j!9yiaXHTaC5(Lo1|R*rZbn!r{ue_u(=2yR>r*TIn~ -wyWOK{gX=&uY~R*8^3e8+=x6hU8)O{zxmruR!hI#T)5o88E>a$_h#maq#v`;U4NXfBtUdjEfiRBN~23u--E)qDSg#Qf~;g81g^IqZziJ)8o+Yh3_gI(mMP?#te*x>- -hfsT>GrjEMLD%gf6VL_@;n2p-zwI>kxQ&>H=O6Vq2?457jE4&8Med{d7C`YgH<6H&yI7Y?(YLAfs_ic -5SY&8e@0tN%jrQ*1GHPd{2L(?}Z!nou|Q{vOVb&nIs*K{ -G8c2T4J+LD{F^4vIqFcf9J}v`QOCwy#mX*ro3nznK+ygLj_TVC1DT#R>Mc4;?ZvJCsigq>^`>q^+rPPgBs&e1;O>;&J{v%x -afH+JE~DXk98@wC?%$3%?7Z1YX*W&YMg*?Y0PjFEsfa%#?}XB+k2FAhVjoeE)Sxh_@yYNe@C-@9Ov@H -IhZyhG_L{c|;AK2bVe>bN$ef9iD!Pwc(P(fgD6C1x3Z%+Yu+``cny{q3>)*q8K^G5V6RQeyft`-rt!x -fESzOcTg0(vHNUIZWRJufin<_;jmx!E~H?Xp`~hhOCKR#-DrFn(V&@V`2P@VoUT`rpU1bKcA -Zq4=|vrdNWmgPSRzJNJHj5t6thT3#%$xFQZH}gXcGWIUp>Q{R_x~^G|_@VN2Tsj`(nDtxcQ1EPasP)6 -VXnXSBMFmq=ob@wx^72lG-%69CY{SR*?~(oOG5YJ5aFuXu;yf>o_?t%c=QjFtdFeBfWcIfud6aGZc>X -YI{hX@59Op=Xz`cq;eVG21U8=v)=ZJ!-b3_lUVC(d;-`(iqC-wwXN=|MIAc*dbA^wi#*;HL9W%T%(Q%3ezlV$Awy -5-{k{{C{A`%AO^FYK>4_^?OK@$ASNq$qq~%rpZJ^W)$l^EYsO#6_#pW%z%et^B`Y*B^ZI^WHsEcvf$t -9c)=?g6}g4O$V21wn6%~zPaxE?evi@|E?IK{IbR1i^dzbysHEc!L#3mv!CZ;0nX@^IRC4_i-S1#$n%~ -0D}(oLTD1c?qH?&y3BBaWhtXZlXvhZ%)~+Br*F=<|wpVZY`{K$U4OX; -E!3K_Ov5$eG@h26=GcJkidKJOfoJHb-wJcOf%x~Yern?3DS2y-H!j8Q0JbPmA}FmzF4l$?+iZ-V=f+L -bN^osDZ6cRK^E9Y+uQ#C4BS1z*18?CA^rI_^DYl69p+oR@Mrj4B|^<4YJt@EPCOa7hq0e;N -C8*_o|7J7KO@I4GV(siW>b!*goZOjKoJ<2zxiB0%cN1aH#_(%pHC;jF}VsY9n($+Q-J9KnB&$w|)WAb`!G --A1Mz7q{vw9kmWIMUD6PMPC|-`*Mux1E?GZCs^ZKia-O+HRP1qLX&3G5w9S$$ibd4d(w-l4SlA#yoi9 -J?tyY$(h@fuHfKwM9-`7UeQe$j^g`dKap6tj8nIQ)qkfb>QS5omJ!LcYR37$J#xi9;k((+GJcj+n`x?d!-wx -WlglAtAd~z{gng8dda9bVVQ4LtwHP@V)VMmMKfW^SPYVUfhn8$K!P>yne=4?6$_#gZP;J6vIoBaP*@! -Yj`=@91MW6qWTOIhelbAQ8r!sz>R`e)h&ZE&QnBEa*<) -=(OtPE^KgyGmbG(k$wCm~q{3mXZd}&4CPrcc=Ld -ZKm3UZ-TTrZ>pFm7^_J6XMGBP>+pAjjCa7etP{L%S|@sg4w3KqtXAKfK;H~e$@o0GA=i2yQMyIXqoQ8 -$H%+VOU9YEDlXq~Qg6ey1hrH5T=xSV{qEScQiCNNATUnjiKnBFRNw7Vj(7WQ8Di7)i{XOx*}_L#FL~d8b)*l*wa6I`SLWMg-m)=cknL#`d|Qq7zBm| -eYm#U4ggFbfri1CquH$~Kbb?R$ak>4AN6Ji`W4AONw3}s`Cm8w%x4#T!szPCunKWk~>IDI#mf9NGFMV -k?SnRE3QRB1?g$vFu7sY!=;%uAyC=%nd&$xf1Z~GOX)%`O?Ah=F1?!(y(UbBGuhRM9|0_+k#+Ej5=-g -~*Fz41ppVw1prep>nz-V_-07-#dTiGj$zcY0&u?*&M28#BQRfc92ri-4k!PmU8$BEDbW%jw|96;Bgjt -Z&-2NcyNH`)J=n`v%&$D);#Io1b&5gEl0pZ^n;)H18v~Z{zjwUj;MZbIcMtWpNuP -S^8VxhxRjj{`DX4v(!9&ip8tp*qW)Og3gj%H&EGu6aW9o|%jXw^UoPbNyVRxb-*i0x8=i3{4)zT**1O -w#t4qp8&)fDy^oU{@cq;wbwP;^*FUwCt`IdY2jop@HpA+zK03Nvwb<#FsVNOa%NxvxGZusYy7;}sEk% -`3<*-wtm=*KhShkLn2Y@#iOtxOLE8P^H_2c3!!Hhpm8$u*uP@n|*aq7bKO=i}0v+GQX8Nj}1lI@%Gvu -88P$S%!MuYfeSA~%&hu0zIOAkJI0%YA12YQlqYoXIoresB7tO_KK -K{!9;nU)!-l+4(S*TLW>9HmGGuV{K5cMjhGvlv -s8F@SOa>tz$fCf3=_7#66vYj5u0QH^ro?qbF+nvQpUjGZ@;ab_a*2}gl8&30}=JDV?r3m!i^+6LqR<6 -vX{o;_B>-*PN{W6a?#(jUu&|L;fqK#tB74d97RaSeXq&@XUJcn4&}O_y;A=E-wY#=Vt!eS-Jszh(Su` -f6`H8#((vhiB>$X3I58@G~Fr@|R`&iclWbjPs%G>J}r%6XQIGCn|pI(}3?16^kzbKDGYq;6c89mU~hU -@J`o;wX-FN9keI%3|s5>S!C?(Q@_a8FLaNJ-|F^H=nc1hJqk`beuGC1J7ZUDJmEi$g4arn;Vv0NO%#* -2Rrwl4fN}=o8}aV`4bV?B#u3+vwvz1IHbKSHmUegVg$vC25QbdiSUTkiYV~_AgvXu7KmHPK`xxh8*k_ -+R6KYBYje<7R -gxbJ(Jy#wHC>^)7xwZ7KG>*D%3ueezK;YsBMxSD8s(^9_jod5#rhS?ECZ|JFgDTOT~!^@jfC)V~im^^ -fMz~PT)hfcV85orUOn*n2+r}<@;)TFXH=(xfICxlq$bh9rL*!vN8Moub4iE?|DMW6Z9ztz6U)*-lYf0 -1K}NhLyX#_Hyh8*6LER@{&?vTpcFxn -5-&}X2{z6UMcU4}hNx1_n4!&J=Qqvq8j{i>~bb|GU!owQ+IjGNyi{m$+FqiDYmbVL;Cp}rwSQ^eJJ=% -42Eb$aOU=5v7_df9xQrH5WHpY!z40rUA2dgwXxxl|ABH=p}UL%n#Gu{EOnz-jL+^({vYeGA@QACCBd$ -KEfH=WW6spuG|I#1fa%RdGM?{rl~SJ>WYxVo$7)vA1N5>^SktM)MQ#_#p5R_iuh+et^0-%3806l#Z8i -uoyp@d4=4dBeWZ2Iok8S6_Rnd^0fvflixa|>hSvu^wq_?V^tX1?nJ#=zx{JjsdPW2yPoxT&ZPH -&wbyH58Qb8%abZ5*uy>L<0Ng8LJ;;m7%y}ph9IL+7iY-pGGEqoR^FC%WZjKTawSjwS0jQB;HE -}-w&cnHz-2CzL@Wx$8~BsD}i1^mbZmXW->qxEVgoznbrk73_2HwtMA@uiD2=G4M~y=y@L?`k^@4$O=A(loK;f;HLOv$-?d@gn&8YeZT -Yyk!vmUUNFaGYq_=c!ozQgE3A=XyqZ#u;T`_J*w@M4i(y$V{~1J!+loha3}7G2vY@XT) -z*%+q|1=jUi^ONzR$dg^MiX}U}F^jujz90P5hdM%e0q>XQede91Nsu8hk9$y_y7qne=6P0srzownojRY~B-PfpP@cBnE18Q3sy6J;>JKbT5ae+qs1Qzd@fbJkN -tyl>R9Bo1|=zuw~_jl4t)b4~+3HJY-0uRe9S2s}ty^(jvg$NsG372RGfNv*@;r^0P3=I6HtAwNBmNZW -5wUsgADNnQF++wE8nwnNz8VHZB)fL)us-)6P#FVVy)@0ON!{#K)2*P|`j)rq2b>6VG7I#6fOn&Njd7X -2E~#ngT6j7{L^ws+VtHap<79%UM}ltIpCsZTq_vg=b++30fYpEl(z`ziaP3r-nZaQt#*QU5n$Jfsuj@ -$l*{bbMQpc=;91SQEW14v{v9J?21vRhmA$R7)5rdb&F+vd&AVoKB8UZ8?$w__t4I3)-12 -O=Z)k~OBRZk{9t)gO}X0QMf>&Ddpoo3v#_@?AJ3C{gFG7;Zv*thd)AsLX$W%>&3Gq5FaCK-(F^Ho87G -flzcpFP;wKJ;+X^i*k7s~$AHdwbn!B~p$l+TCd}q#wYWlGnF^F?D$9_v9{mLAE9ly`)4Y#rWAkLHD5o -dQO{ZKn#(<)B*v?3iRSzj4NVOG^9()4!es>_K5oFyw^?A1F7Kkzb=LfPgqmSS0{n~`n38w`s^f$$z5x5 -JM6<%hpHXBi)XJ%bltJ0+zEWIuxTmIAMCJCt#+V%o{;6c5~9jaV0nuy&$87ML~)Bf#ra60t-LyYcd<= -NbylZ~;u5Ejx$A6Vcd=)WvX7*`SW5sN?j1iD68FbO(2{t*q~F^gZlfKO=evW8zxLIyes#2eY(y8+Cw= -}BrQalD-n9WgR$!g|fXQUgnQv(#zYMVPXqk14bvbJ7_jp^&fa6PR<`zu`jWWM+{3h)u%_MwDo364--n -YPnKjmz$fv$Y4E83V78-FWNN8TS=h)=E_$AV6=)f8g*9g&26P0KXbFU$olS>`fdXs- -G1zGUoPPbJln|l{ZV+fLE%ZD#WGYFwA#vk9b*j70FOAY -Gn@9>dp%-PlT{SZ*0LI9$@_Rw_A@BknWK0b^31$PU_3eMo^3fVw_^*+Lyx~fdv1{+X}}7h-j}@>e%#crpIaK|{x!-ThrI -BKsGN_3fa6(f5_2-_xa(v@@A%P^O794|?yd}nduHP=0e`y^)IDsMTU$)~2A;E!tt*LRpSDh(_utsxEe -1a|$Kl=>$Ry%k_#*pLtCWFUp8D4SdxHZ>Xj=5K9o;fYUjfXVA>i>Ou)DrvhU;=ceydvWc*In>ZxlC&ZhlXiuWA&aOxe8G8Ugf<7h+H -XHE6L^vfWjI?vADTfQN~c8l8~P3(!&nly=hvBgUg_tTqJQTHoag<+;WowNjka|fm0y -zd5IFO-y|U~9Y44b+2Qoe`mPp%#>r$3<*+l{ANGtV>ru)jrduKqTZgQ+WPECWdmmRwJ@fy8>0G}|3UleM -_<;VLf)0~4!D0N*G>3VUx571xZYNscaBmP_yP9$Ieg#!^ywn5K~9u?7R%ALjJ9n~?0f3l`@)?vS7J*# -XuDl3UY{-k`@j>a^~>zDP(Wc~e=m%1IxsN&y_TOQA(-Mtu%P#l%nRDjUO_4in(|+bBO% -?Rh-eOhuROO#Dt>`OtCukDlfDM)XY$RrF_qPgDfA4% -JDUx-4*9cIy}n$X_j5uKM{^K=M4L&&yQpOgnoGsA1^LQmyt)a*T|!J@vR7L54=TP)V6fmDKL*F@1B=)|X{u0$6`;zy~HnI3p`7UGiY$m*@C*JN-^H+IQyR%&%l$kHXJGDqD1Y5_HUInm7v ->*puO|1^p3CWbRe_0}%uH-L2>N+QEY9pOZN9sSVG!ht@f=QuiQagHqPPw?eRUms7Le6DJYJZBD}??W7eF<$VF_h*zQ`m)d##9RsJKW<-)NQk0wd?F_l%5@q-@H*#k?owAtS|GTi_%h?Cu73D$jyE?8Dn<=mFaDZo?U9!Aw -?rcs_eNKAIuzR~+BNYoW$| -j9Ui+y>P#YIoH`mNGpKX0)zela+IE+)9r-`Uwxn9>k=IsKTW~YB=Y~hHpP>~qgQ6mYwB6@c}{QDY4eg_Mf>to;1{++9=%tiy>RGrlD{IKCkkjYUu_v({% -1yc@EQk$hJ6q1iK-m5CGO1TI+9oCZxDP^HOkgZ5FzTqXwMJcumSu`R%|YG+MLi&oOvaf_o+aWc1NRpI -Ny0Q+~zu=Y&5u6YcRfD7W-fg);oHPD~&O3zR-AR*EnPBwu~JkdD#DOFt3!|8#}I6jEi|K&g-7#x%W6WAqC|3Af5>amB==G|{bXzX -=wD%roD`wnLj?MQpEZ^Mn2?hKw!kPFMKqGw5BQ^nb2Wt(<-0q1S=pZ&Bb4wsAKAA1;c8?=$U&ARAZls -4wj+azAM4^&)!SLe~70md;TfYE+B -sXU6qT0bI$aK3>^|}eMZkeQ|3hsTJ!y3@V|DP5vGnXZmda*yLGbEH&6C64_?|y<+lh~qDSd|rhxaEEb -mNvX)~B43N)*G|53n`u^%_1{$9MFj`uZN^={rDQ#P!!NL!;F%uU#YcHe!QwnR#<`w05rU9+9`XwAY`v -sH96o)Bcb{chl!U7UQN6MNjoTx6iJ)R)rU`=d_jM=tZMxg?A;{2VXyrSjbb`Of8iihDD~-C8w6blX!h -TluX9bRdcSf<1N#_C69-R8>?Nh|9GVT{ -})Ak3kSn5r7s5q82SM;_+9+{{1p4J!j?PsumE01#SBm39wXWYR5=ACg=+g; -R4qiyt2m}kiR^a-#D_x^BWayNYe!i~0W>QX=-LToF%+)^A$ZfU<3@_aB^*)!TwRSt2PTXwm@FIq?Qi! -)G;{3!9BWf;dMZaKp)BHlO3F#S(z@{Z%gu=Mc?B`W=Gy}1T`u9o&P=^w1@laH*k_)MGOA;6FKW4yD8h -0k?HEYB~a@7^o)?mW%5e;#m3&bcw6n|24`#>DRMToDRCs1?8cOvIjZ)cnV}n>FWu9p`^EYW}h1qUZ05 -Gk+`QKiBG;NI&7(w4W07eXbw1%X_kbac7pmm}w%P|1XoVf_F1dQ$FXc_NCi@R{4^9=i&?xK3G~@E$zQ -E{N>nh5`N7Je*J(S_qfajcRpP3^bEb*4%$2$unAk764yXKPFnkC`1I7Q)@54Oe(t%arm0vW(iS1A{kd -lQr8dKsMztMWK3|o=ntI<>`w@G@_lZA*+d?x*e-?B{bZ3edYBuStCh{*v{4LHo$MQ7fFRA0;xnD+|Y! -ok3Wb!f=gO|}pm;2fA$el3ngJs1u^s(-^;#D#XpHR}n- -gm>5ipQ{x!((K6ncu?XF%}x}Hjc;PG0w%vZx7n$L_o6+7;H -IcEB1Nx$ubrK7POP5I<%kvpquJmoDei8i37Myo80H;@f7j8SGcriErmpLJSjs8-PtM`oU(mOD^z0Z -6l!S^uElUB6zCfeDcCHPuq?dhtX)!fy|JbUyhB0k`K<9ZPB*6H@&6CME`Wgnr1uIH_%U6gw`85jLPwUjbSHU6D3dXw&;Ne-&J>!(ej1o{t3$8Zu11`2k3cWg -1kRdHgu^z&)+80lTO=J{DS9&mg-9~pZBE5918NeImH*zXOG;^d;64~`N1FV54TYcF3}tT#xrExM&6%; -cMQnA&~KCaXYAMOh;yU9?XiurJCx}bG#S0S-k#Cds*WlgJzD>r}OsIblc;^KD3w}v29( -+&~4}3}ZsPldk<-Cb{G3h+Z@qW2=VXjfnQCEX{xmW8&!HeO7ub_Y4J-+rJ))@1k&vuT%H&00xo4&^Hc -)u*YG<5921Yg)H0txt^w%C^Gs@(d^V$0b^m(x|bMwI(MfE#0tzJ>lAc$bPYZ=l`RQD@&QeboT?o|^er -wU*a>ttfXH<7`cvlejv$~dK12j3%DiysVAH!0kdYX~RyRsCHRJD>b -$^%}9bc7AT_H^6%>T*kO`)8>>dQ@J9m6D_h_Zd5r{X6l?8&6>t?*PtHzo*?fqQd%c`C4u%~Y2P>cf8@ -Pz0oK`9kM%z7jN@-5>0DIK*NK+y!f9fO)QN-cEm8LC-dXgWcQN0w@Y-&m-bW9Br>-+?R_U84ZfH9ApY -JGs|1{3@v!GwBW4Qr%jygXHgxjbS-({h%q1*d_At!bwiR+qa>m7{9t(4oiN3K2*k$s|RiOe-|RE^1TL -wp(s_!jWHxIx~DZOe_KZ&_$p=FFwPj!VUJ%D6#<23!U$Jhd?3#h&JU(;l3+(Rt3g3||E1Ig7P}uLu8h -%Y`s~TqJGvqpgLSCG_kl|Dw?`_t*Ou1k65Z8_WCdmPOLezh~o(dWimkjDwSV0kX-BD!u~mA~u7TZf7i -#i)!xG7b9~I3VA+Cnl5v08hXxG-VC>GNBxaAicrl(gLYqx&~EmVe?i6r9kJDp)xJ8{0O`jA?NEe%Ikd=)xQh%Bx0*#>EjoJ9UUNqqR2{eL+H)bmT+D?pxIZ#H?SgW+plD6ZU -6Gi@A@_fh`IUlxSF1&+YX|zpS4J`q@(p0^8U@GXbg>gFc;vnemY)vl)KTuaDV`w|eYcUrm{<;>`7S+* -5N4tDZ*CH>^iL^S(_n^VKpuv>$@?^Q1qLN{2J!1=YEy^oe4jAx!NW-{UrV9O=@*rg3z&^$)miNw!0=8 -s%N1V>M&fk)A$QqeL{qNKnmt30TBQ5+P-u;<*9I*afbCNm+Q*Eua7<(=6PcawH345=Oy7tdvT)Pqz>g -SU`T=aE0_VuLAn#~(rbafHSKf*Ob`F)UmYcZ$epu41j&tc9J@J!rVxmDy5&i@*wJyp{|&gG6d!nYsaC -Rm#eKAVoQcqeoaPNd)64b`jG&l0N$C!gWZkg9C|D|lx!?RW9-?#KFZYF;xp6n(tUSgqc#>{GO>_Hwbr -w#VCQ|B7A={HrAny+|LqiAMaGS87*zwG0PgPMoccxrhHS_ktLMmuKKQrIV<5$Z^K?OO1kK@kN}t0+?Xuox@=z~!$mbgFftS=X*TQD-6;*gPc?`zO$q| ->Oml1E4X(Rr^EGzrw9Sg=Za3A%o3YTNYbsXa=!?;>3YFrbe#?u!yo`q54$>W|d`5f|d=GZu%Rg>jiE9 -XZ2M$mO1ZNBSYcYUwD2Jrnf=wX=oF|0Pf9Cwa=WZd!YGi8}l`v@;7V|OKq9?5aZ?_BIJbYuGlvuga^ot~J{2H_|6SCh+7SixczyGB2dtog8 -9-J4nS2F9_W58;e4>~h@x6`IP8vh -qF=+LR!6>_C}Ucz3=*`f&HmJ{)eFo+f(OhFQ1NYS?i_)umqHcN0ZE^{Tv=`6cAis{52)^*KDVe&s~q? -FK3P?J&!A;M?X6s@$LO%Wp -e@E!sMhh`FO*ww1X?bQ8vb1d(s**`syq`C8`wCWqvI4u1N$;+yYMcNWwOFc*%$*`auWKODijBoJnq`| -aA~R^t3ptEgXk%2U*D-O$c5tFvW{gMxF%BEH>WgKd$gBv_HmgoNbag*mBKYm_3- -FDBeIQI?G@8XXmd*ZyYC!RevdQWWU9S!EoeL)#j=H1?{V6uz;-|5&FZc+I=;CW@M9@?3tH_$*P_diGP -i#Lp%L6xm)Uw}W|*77|)^tj9cDta3Jg?*7Odf0|p*W4F>i>k|gv2zl5ktm%1*Fd3a(2lK~^Pn>$I -hl^<2^1(efURk>k2^ZOa>jTa`6{%7u=0=m>9^ShdRW6H?hSa&XJZ!|Ql`k;H`N5>qem9a1)E2V?K(t1U7v;KAc6cXChJ5A-3%itLrSje&p6 -%R8XX`^p5-!+WuPz_B0m?p504+eg{t->lj9Z%$G)=nB-^T{~CC7Q0D4uNaz+y0@c^EhzI}{g@wcvj%w -BJsg(fyFty_|I#@3UBAKne%)`x>Dxm7S2yhJN9 -$|G+NWuD^lfW8_;_$Mk3*f4O<#L(rN!3DIG>C!cGq6v`+`PYP16I9?G*?5)Load{{ubSxz;m88DrP?5 -{9}e{;g=2W!kF8XG|-4d=CERPb;#eU6$TeF|DX~ueWs}j{bC63EJw6T8r+M{^G{CKuuH$Dr~B -k7wI99zHP7VDz}p%DA;s_I^IIG||_4QSDvE?b$Iy^X)>JWyU@E)}Yb}jE!3t4YxlUu`-2smqV{#j<&t -8MPfJ|J4!o91^3u7bMN<2Zaqsqd(**PpkF(2zK}l~*n2}@m&IN;8|`IUlYPCbR}5jyJ@n0b0Q~gs)pH -q#b;l-6wejj%MJs5}5=33Qrlk$+O{DFmkFhpR;63k!y2goDH9g1KQ4W~YMA=rx!w)mI@#%!2d0j?4ES -U$?$m2)-Od;COK>Jx}zZd7lWVAmpV{(T#Lk}%Y6pw1aotIycwrcf^Ge;km#v~zqM;aYM+Kt7_c_3-;7 -W+s{WNEjF_KD+cKR-jr+}lqdjllm8M^#)c(=IR`-h3dqMA>%6TYs)WCDtGV{b0jLKY(3XBEa=*v{>pl -V4iNQ|L<5soTb$1mF-b$ws22k;I6M`FQGmECY0#{OdS}L9d&18Ofo)BzbIyZUzTg -0;f;q|#QB$j^JlOpdnPibXqJr6+HyqRITlWk=a8GS=Qdk^!idbo?>Ns3N4{y1IJrAOPc~tXyeV_1f^T -Fjw$y#?8B<)&F2jGqfpI!BlKC?ZD)wC0C2~L3VE#2&=c)|JgSg*4qT04bwf)Bq&R%EML_G$cYnFoE4rmOWuHdk&buik$0ctyG*>xjJ#XOciDKC9eMX -DzPlXnE|0t`;ky}lHzV@ydcL~?@2-fvE8x3Ayeo{n`zYUCk9XHc-p%H_PvYGtBk!)@yZLxGKk_bz?{3 -7q8zb+g@!cZ4TNHVh^@b6TXPo%=5?ezZe>9sX7w5x%2%Z -U%o1tSAjV%C>GP4gH3{Un~th!rw2Kh8})>Z2Y9#4L=R){uU;P0}DZ$9YSw#Fm5$|sSl=HC~V+ -w@dy6#fF1lJW7hKfbj?<-?hk%9+8(Peq5Iw0>K>t^hxer7zy&;qOg*kO+i%1g@xE(K@H1AzX={@Agtr=F7r&ey<09wTSf2Ps7L;SISXF|t?atcc*tHVWQ?cj&7JL=!5 -4YNq*K(<+CI))SYq%qw6Qt-?cYuu{?wr#aGWcy-H&zktpFTae)>}>r#w8U_(I<2bUo1_W3{cAFMMWhl -+$RJx-c`o{(Wdi`7^rw<~VcYH@Elh3o4$Xq_IYv$0PK6v`#hRpiJ=^dOWY8C#;ZpJ}gUjcB608ccE&& -I4}?ES!URJEV9PlpS!(FXlLug&pIJ@hpCkNa)aCBCh^#J8s -LzmZm?=1lTA`qZ65nYMJuqovB&C7*$;{S4}@H|njAs#n(hWhtY7rg`bmcx4tfQ^zS5*A0gY#Ql2cX6e -rq_x$zO+^dIb4BBPJ38S5B^_{#s1b?mR0p8bDt(HDoJvN~SP`2P8WZEj!y^}dU?;ibazwzyHenUHdG5 -&uFztwA1?0B}@!WiS<(?cR;J@7|<+dzLs%Yi5O{6qD;lXX$&G5$WFhvo>eiDS(I?C##0;9Chg^dj#9E -rg$n{nBI^J)Vb-(XLq!{Sv<|Yrj0yOB%R#=}?nV1~^*qb9{SghVX4#D|3h~{?V|Cz5SD8ik8Hl@tZZN -(>`fyo+IyxDR(%&ndmd_meZo|miw$WuZo4bEq%N=>?@+$;~gw*;;O$~8ZdCB;J+>^`Do>_(X{P-#!w{p9@`v|G8 -~ri+$cBDkC?VFa*WLmAN7*FGvoK9u!>D)>PbfBvx)JWG4i_0JTWRq+KcVXsk|3s_-5be__l7tzft;cX -Q=s_=faMAGdonwkPLrqhSK3N4(B)0Z@xfVo~;(wex4ak7FX+q-zvRGtn3|i&jYzLa?e9Ji_}EgsGjOe -?#wp%S-$t+J@p=A;}VUYBkk-N!;W`aUqJhJGyVhpG{4N8ESXY2u-j7d68&-k(|n$JgJ-XMu=%3Yag6eZj-@B#`85$I7>D2%jQcM1%l>-D>5p -UVzZjLHWwgKL{UR+fG+ungNd^utQS@Y6yu47n=bJ7@az2c*yNIS|{MK>`_`{f**tfGYM7oqe(ryTOHQ -UomuHN6yGQT_((fN$Fe>3BwMq-JsjMv^}s=Z3I2cA3NJ7wZlBwi@wuln7zBb+SiWsb>@tNiSqN+ln9Z -dLzF8-8UAdcSQ;u219$pCkJJWkT8)N;vlCO8Y|jjBnBLo~4b?f{2X|bHCOY_CNc=kr>V+?P{W1Xj*sN -I8-C~mq&b%z4XJGiuRaGar**M+&M$kUq$(rF?=EG@_xNDUC5jOo!KhKD)R?)UL$1Qg3eE>JnAjCh{f{ -mT-|4QAH&$rWBqeb_MyR%Tvj`T5u<#(yvNT4ikPMB3=O>j>XG$(K5$-ih}-O`H%R*_N1DC09RJq)&#(7o`hezgV8&1YN)%>=+7p{EfIOitGF~AbM)VVud(QkTqC4Q7jj#; -OYcX=U%9!|uUP8XRL`-~L%TDJ$>5FQw<6p`SW>h@NIQmCv_mCj-W!;~p&6D`BavFI%m(#wc%(>N}ayA -yO(OiFn++%zD+lRE8Zwbp-e2b=UW8+4RAHTU1GEa3(eghL;@$wruETZu4ccmS{{kHzdnP=EjRHP`|ulr-;n>PMS* ->2zWud(?M{&Ye4J-lB&1g`nFqUzIbka-YBt-0lct@-=qLD&x3LY(+gRDYTO8lC^(QgZcE+vw|LIY8N$ -xzN`~XZ9e_lu*O3g6>PwBfeQv~{sYCh*~AHrY-SO%~|(D(5xcfnK(t|?1t -*0RO%lv&gwl^w+_sh*x?QBW{!(|+N15t5cTDtkwMVdU7Dy*(OmnVO;S0KA874jNx`Q_A>n`-(S^y+l-81MY19uLJ<2$3nv9=w -eUi#yQI!GujJ-))g&K=q%=u*Q8z|~!uHikogLRXbTSwG~|C-S4MO%!sa%b+iG*yIQQteENK@v-s?Qd9?>LMYB1M-)AYVJ}Zg`)>(?l-!6 -S4+{UtHj4k@N;HqwIsF1d~cP9zoVer_NR8cA0k@^ea8-7Fi~pN=g -Ld$)vqgO~V`O3P1?P$ev+d}1=?f!y7Ngzi$!HgCweXjB16U`vnE^aGi+w@3P)^+n91T8nWB2hZ^?;7E -hPLIMnmEOL4h{o{_7T5oP8U7LIIMf}S_gqkwZNmN*XSFUYAJ(*pwTUw+ZjartzAwW6>oM*Dv^(f^c2u9G4gTeGxaP@mXltKS1PI6Ag89l$vLr^=l&p -uCyAia=*Y%rA>zd-(8E-_LYjc|pKJpu-e9pPreZ25Hsn-a(3< -UM`S~&MB=0*F@QjA#ljO-X=YA_-I6@Qs%@wcBoa$p96X|P1sJ%QX`w+?;PShC6o0CSG-2*z`Wv?6xz8?YwaZ!+Nhn_s`bJ~_U;Hj;U&m8JB0J#_m0n)+);UKcX7+bNZdfIb>+>&$@9 -wkd54f&IA56g-!r{M|KMCRy}xI!M4QNGThgAPe|NFhHdMG8XE<|FG44?}{eR8x=);?=%F|Dj?<7t&C; -6sls54vo!X=6P%0wC8wZ7@;`K$jMygzqIDVsF#nNx|IQwGn|E)4Sc*(>x_GA`gGv1!+g>2*0cy -N9qIZYR!Q@bA04qMp8pn<*!v3}Zfd3Y6crdm?y4p4G2f^1^77_|7~1+!(*xKH#N-nJ)99&XM@Qn3CjO -GQHD=!)?5u=KKiLg@DIC%TVDN>@VgXlQGsE7)P?qvolif{)@^EF8bZec$XOSu1M;$MTj{GgHH;do+16 -98Q=0CX^tANqOC^{nei@FUTWGrj+xI~5nzrN+9@6fj^)|)0R2xFqAl*}V6q;lwlZd=9%!OpyIa)Lhrc -iiuk&7|UROQ;2Vj?)eJ+`d4P%S&Dfu$2Dz -%UbiM1aSAQoqgIW4y}ajS?%7`|8+vaK%5{;y1paNpUyY?HVCV>*kJS-)aSqeYC3lJ^y{umpQ@{BvoayLu|D0Mouaf0=7ava_D!j`rCO29#YDr_bE|i0R{sQ)Ey7 -Z9@*tLw>QX_|GjU~fTlBU;RPkBx1_(f%IslM3Lkaj2as=3YTnL6xTtyDhKf3w|)**{lvdEX{|s-jP-& -EZK*kv!N>jJV?*Z=++(Jnri`q6nWCg}l(=&GC)AwtsAo`)6t)ju%LdK_obc -`&=c_F9&Jh;{hl~K%BGRCCd*OEkCC&6jACA9De&?pmwz5%1co@)7T{s5sTTEEX9bVXe{LCyhC#NdL=i5=R(&%qHU^s2s{=#(L -%&ebHy%J_-BqS6Uu@P&bJpdDhXVojC`3v{m!UlEvgsi>;P;LE7+>L%``oMHf6dy1ZMEb(T$$b=;a>C~ -45bMS%fpuGgz&I%-AHJZbBgE&`Pr&eKdiP|LY%Npot@Z_Nar-IL}J?q102wG%j&szSyQv}=>+FoxhJQ -7!A)wPaam6Zksyf8rd@X_J_4{GTAd$IJT>59cOqSNR=?caeLt2tQvFMN>&nM|8TNsidJcdsD^ie(6_R -yA1q$c|wuNZ$9v|SJeG7V||nNNR?{@xTRX;+PYp*`#)3Zkt)J7MT4S2E^jq>3z@GGbV%60xSDY)44*V -F^I6K=T{w>-d)(!1RyL83>cf$}7r76P=a072ymw=~b9t_ZaIW{sIHZZ-zY@IU)h+{Xa2&s9OR!F}^lr -+4i|jh@SFWp`CAw?vBETzbnqlSj#d86=~PgF=e0U{{~<6j^tmPDvp0z4} ->8@^x`|e^>4f}FIdjJcPqShO{51G=z)PRQMQ3hI8&26;|-e4D|y8nhvBC;Me>mE*4>czn)6e6yz>e(C -m7ahV4faG0S(AasTI`_*sQ|@%lB-eAbWUE%Ib6lSn&<-U%0c6YWr -6xS4qeY)UT@T)w+FL(87SvxMheqVf=lpFV6I=@#=70r)QY4qk6~n4GhYd+7{3kEBNLF=0=dQAsJuTH4OeXPTZa?0iKm -Arc5t!aK!(8#j5FA+DP8%OUVbL^V^NkcPWQ_Efw^1k~qbfD?HZ*4F0Pdzj@6q*~~>-V-XFKo{rcp#G| -JsFAg4C<{S|+KQr^r71~4(?e~9_D0&*OM(y}}7wz-xtcIM!djxq_C#nB=?;vxRIUsv#qJD<8y`8z`q& -$r=U!Q8+i{KmqZRnBbu{O1?o4i4iIJFRK?f_qI6CD*hAV1mi7I}}mB&}@-CN)(!ZqA&$;To~ZB7LbvL -yJTBHaU1F?`mcFo8JxZID>VeZ@_8BQXDSbXgBPCz@s-X_Zr{d{_?1t$`42#(Ku^Q-|)NWM~lA?I`6HP -ACJtN=k>y=~LMYta5O^u?SuyvHx8nsO)gmKy# -a;{Tt|xRbQb%eXv~RXl@|k59UjXB^>~%UA=$p85g6coEA?Q!%_sa-DZlP6`h1!g-?AF;{ghzhB-|@ut -<`&voNG0iU$KW_nT1x09B9KT#aG19EM(rcw7+%)DM4BY8FcI>*3veEV-BAGnvgwFfabsk3+yeEs8{*; -k;wlq9i<@n_BV|1IwXMP&`(bu(?EwcS$wqW>{-XYB_6PCLZvca|1`r%ul>Y?*kU$9y$}g=}k&VT1A*c -y4^Bt%F^bNszLHS%!R~Sw=VPBjWqbkM;YaEX%w#e|aeq7j5lJN^buY*HRNbfLA@`g0kJ6*^EtJvO8}O -nQ{Y&cZd@jCaVox+B>7l#P#$8D=52uW)37pIq3CbqghB955qG*1CSj_$?3>tY9Jkt@-w4WL|F3JGcZtNw(mb#TfO`i8fo@0O+9T5F3x08;7!m2OB0oj%$C4h`GVd4baw6*$5ifBTLO5qHYsxi7#lveZYuWz=|`f9WBwNA)UrlDWM^K)RW&=yo1Om83 -EEtK@`Y;qbM4q4i=B^5=)f6$D787YmGx*_S3O*H$hleMNuL(Xxty{E=(OrLbxgmF;O$y6Mdc4PXL+ZV -T6f%<;vJN6v-FMZy9~eApnpD3#P4BkVjcNy#!Fj@^1M^`OJ1Auy{P2fWU5=_2LU_gKfGpD?arW0Js04 -aV`4n7r_*+S(U!5OTKFENf;sYCOg1U!BvlAVV|N?jLoOnqCyFacxd7@0F@FUGboLBX{bV>@mYA-0){HeX8?ffr=l3_ -awZ^-}vI7{g)kceyz)wBMxJ{8q->GRELE#*nI|N?YQ$QD=d%MsL}lY(L(p|F-uJ){euPW6+LqQO^=wS -M}Ro&DLt4W$kzi--C_JHDeoOyq<^e)erp!#D2M -+US*!Gcb$aP%VxfJRF>cSQyq)~M$cRa_K-yl2z##TE@vO$!3ykH%_lz&Z{T++bJPXIZqutN=_b_iiM1 -Pg54#<0-WHEeB-o;q`=U%pUGgrjB;8h-TicP_()JfsoNfYUwHR?_;yiVm(Ww}Era~}VnPS!)+IGv?aUmL7A&I(2Vjm7gio(9Mi_<^mnLx3C`97U1E13XxcW*qr!I>^p1Mt66O;F-&_5h -$|olG679QU@`*9$Iqw*%FGli-RcoF(FDEH|7Iie#1Id_@sfG<`nyBx$$e5DbEIE|pco#O+f8qdW=UP! -feIRJPTYlFxA9(@!+z&XL&wIp3y~C)dH;Dr3(cZD5-Ua;u+8;1an9#oVbI0(?Lj&NssMA@Rr@u2`6~! -r*zJr9l?OFYS0qghMA?LJmj-R0ot=5vgC{LtUYBuKsfZ3$o|8coZY)il6V%4D#&Z*FLd)^lTXS-$9<+ -&PokOt44XRYhAJ3(8{CaZp^xi -Mg7u`?lJhE%+nvTWZES0}M;a3p_n(@p}BFu1Q>SCGdjlounm~qtAo6Ie%_X1nv%?jcUf2ysdgCZF=5> -%>J5nme&qCSeCJ{e7$BbcYqcyWeg?j`Y(2CPG`xpT762hNV6pY$3V|`elY)EA-)}cPOHa$to#kyd?ra -O?gU+?uX+piwO!`Ybk?n9yeIVgx!aT-AMH^}jQ2sjx80^_B6VFhEy1x*OLRD9{W&`~=P%idKz9dGruM -e0cJi!Y`^3$`ay>vB)zD3Pfb~vbZ*p!Wx8?3sF)CB*7-yB=&jHRn+vU9;o_~Vh^HVK>A&({S3VG}20J -rD!1uS%?mpmZmw~VSw$b>y920P`xXOc>oEs&Jd6>)53nmCuR{5mIXa`@w#a}F$78~iFsU}+$@!Nxw7Ar}yq)wqtS$prj%HMALMBl@|g4~-f(q;U;oZjpf(3azPe;#Mv!rSt963;Aa?mZM7v<9{ -&i=Ms}Hs8{ZCZB?=T-lFuPyQm@R>(UQ%(*4)r`g0O;#b+FV3PR%GECNdFqm`!CQso1wZ$qfay4M_mqX -#UY@CVf4$+oqKK9uBK-tY=@{e+c=5^iM)sc;}`L;&ulE4&^&)gZk*lQ&n;7gl-n$4W-^pDEh^9kQvyR -FrP@k)nYeAfiMcOjR^393KZm%R4n7C-^I(tj -KL0rCEW{q-yDXIDx_=*iaQ;r4%4kwBFE{y5C)$V0ESEGJt25`h;fs6#)AdXNjB7S`A7u3NDw+Z8 -knD;jhMPirdMZtM)C@g*K`%*>a%c+q$MdsrgcRtThrpEu_ClUie;1-_;(Ld+4{JIV8@_#%N&bJ%?f8@ -HhZ1VTV(${AbZ%Chhku37Rdh0iKCP4-{4BlxE@OScmitbv(CSy)!5#I4Nrlpx$S0{o2lcyq|sR*LS{=AkxAjv94v!uR83d*S5BNB5A4@JfIzW;`me%% -DeUIoo~<61LO~bPuvjDhQpQDT2w!MDXO0>QT=qua*2WC_`mG7ukU1=3-SJ1@~x=PcCNh`E|B#>|7bUP -SWBqeG8C@lUN`som^DOuPd_+%4S)X2aNF>~$QtfE7+FKokpbFwI&~S(y4Y$f54|2PIGYT)5c|e{*X2v -Rsb6$Hoa89qZ3k{Ez1dt;hte__3rFPk~ls8noh1qi98DU4N3=k1vrA0j)TP_UFXVitmo16`3E3R`h=Rzd*^`pLn!l5dYVJR#4t0tZMO%vgbL2UQ9IT#dKSQUNnPVa1Hm~8$~P3^+S -8RwvJxEjiCSU9EhynBX~XsoFSf&cLEPNLMzT&Y~@*RgbQpVbmCTLd9tIt^+&6p?W|DX_?`j^{$eKR%R$KMUNodLb`h7{_Azph(+< --6E|TW8=7j9zArNwyudH&?AQCQ8uIw;r<6YvmF+PGdNS`+&fTG>K&xMA@91D_Iz!sw0kbV?`Gi2TYwp -4{K=oQji$5DI>fE<>Fk8a94+J18I#UlY8^_W*5U6L!fhN&s@7C7XcgZ7w7AL-V-LTP)tUVU=3Bkyv5q -k0g%tF?VU2Z38F0>?`Wa_kqP@I!k4P)KOt@O{o;)V?%WsIzZ1R}Zi>@xB56dp_mHpUjT^84Vw&$58PU -T@8bN95gyEK=CN&opU^EVe$Z%q1eI=M4DxK39&WtIzHW3q~GA6|Eruj|1}+w;Y~J24=&JwxylJO4T4&DqmrdzQE&#_X&AhTtZ2@dSN6nlsKgHO8gtpH= -ui8tLc2=&zwSdn%&nx{n`*`Ger4N52>fXd*6PuE4@$3QrIO5aZCv(EN{L3-kNG{9lJEw!@R$rqtmt2K -gbCyHK^~qeyGngMh)O)c`wdo@8S?qD<(!_YZU1!vNFLP(@vbgJl3wASxRq+h&1siBL=*q%~Z{oNylZ< ->IC!LzzSKY4{w+vr9Vh3DsK-o$3zZZ_mdpSBs=ybzoM*7Jt5Wcl%RJ -iKk>!(y`fmU}|^&w{Ubv`O7(Z?i2+8j0m)=B1T!_hpP9#yhjB_;SnD9f^#gPo2cfQkgrBV~+TK?j03B -C^l|`nQPVmeDqp+q-|r)^AVfk@!&>#_D>V`Q8{aQKa~D_rh%s8x9M`>3SCs+80RJ4T>dzccQ^ -7bfOn{ye(88y5%@2P1OGjLeSi2fCvrUa|MssR9{%P3AHqK~2L7Kn;r~goZ?nV*0(>b -%M+%KK*jIcoyWuSD-77-O@{cb(z?*cO#ngK@6-Ugg2g^cQSVIplf(d)kwZ&V%jpzJ@YKLGKuIzTZ08O -M935UI@4GOl-Bd>Z;dxTz0_Mn+hI)`g-mEVeef4qAK40@i_~~0$vcjAYNE+i74JRyx}5VKoJp8u}oNC -k=13v-9^E)&~Bt%G_A}^vHJEUyHROj-!jZDc&QYvOieMVFs-nxuu^`{GxN;Z-6L4(2s_c4!<{2+-!?tk2)0OT -%SPyLzkP%I{$=+*_E_>??O`^5oA{>f66BQ=_pH{5Z<>}L6KgDw(jI2GmT)8OVTS8XQvL8u13X{7!**3 -CCUnC63XRYYcIn(P0{7dNI>&q~zRw*6?~>KGdFuRE`VJv|e~$Isb1m`M_4izRO()J^5PPhVKBRAV(5D -RY!RTeY>{)Nz_hiF;dAdftc|`OVP%ic}hAW3ZwGn)_x=yD!%w#ty|@oVn{~zAs8i4ZiO)wz?+%V!K^AHMpmv*th+`8oqY>GH?GC+S^JA -^=RAiEOcB4BY2%W&s4WVe-C6~`^C>o2U)z$*P&fJ_c6R)`*mv>wqNaV|3SX5(`B&T@9^HR2Xy-LBiDB -WZ+yi4tzY&R`Ly(E(~f!Yo1kYGagICO8}b***dJiu5KE`Wf9b7X|I3f;Ih>Eb5!)uO&;x#cpKa#n4fJ -FeI#R`+N#dJLdA5eZYr7%O|@rJ7L; -y3FTATB&dB;Bh6k86<Zn+Hx^0DgnPg@<>C-HL|4M>0Uzb1h1C$zE)O;W@Y^y8~hA< -{KEYPpU3ro!j>KQx4GA|`$u4FTw^3{AO1J|!^`Zt-=Gfe)O9+5`)lL5Il1ZDFdfw}FW@&*pqH0Urm5q -0++z#VWYu+SF-^FK*T$d?o3Whjhd#<50Y<*>G47Z98^qlNamOq4jJ!n~e`g+l)zzjEcvc?QnTHAGXH; -LSukc?B{xKduuiyjc=Ng}@*M%Oy_mBe+Cf%zu|F&`&)Cb%b<5qYczj-%n#QJca_Zvdaqpmtg=kH*%(V -#QBVVEMfFt53VL7ADj=PSfN5cfoa@Mw#jz1ZRNbJ=s?|L`C1PC4MKrZ?V%K0^_d@7_&v-+0kx!F&Hsc -&FJJBld1e#`NO4$UX4O0(V^Fh-)=_DdPUugRXYrm4mJ^1+4#|)An=F9)Abz2t3OU+>Y;pxYojQJ}MXT -r5yVJm{*WL_$`;}&V;)K;+%$ScYr?);=Ud0&L2%XQO-90y^XRq4C)f|FG)U&>&!u)nuD(X_x*5hg!_| -j;|~+Jial0wyc)mr^jm4%FkuzDI1$IY%aKVNfm2V38FtLuEZjn9aE{=~Qs?z -@TaE$aSVd`~xAceu&T?kNYwnL<6le@au+4n7tG`k%pXFW@<7eyQov3&F$-UE#jO!**~7_(Ld(jibU}BL)1V1 -UhN-4{rAT6#)B6Jd$abzWg9}>`@L}vBe^L@wjpI*Q3cr-xUckHP4#oh$ns{B3QUp)8l#eP_q2wW#LxJLBFX8W-bqjgE6Mge_C*J<%VJxi -U`OkYkLK8(T_`m&LDeqQ;%u`hc6mgf7S&wqOTzUUDzw!>4mj+%a^(C@dyqF*Zab`bs6U^;KQGk#k%Qr -hN$I{Kc&3)doFyZ3L@A-?Z^i+#}{heThL_dOv!6SrKyPlNVFo8rD`?(yL~493?DDs(LnWzu=QT5UKbJwS9Gy3p-s5)&w^(EimdZe -Du@%pVlGu(K&cDifdhhNUdjJ^bJI3|GaNfP7QK=Hj%hwZCALEF%o^$>RDCvxw$1J|5OBJ&x);rBb>dU -m|GU+nXb?-uyYc`lxL-DdJ-KYn(#PL78rqfXa+FVM_>qZ>g!+PnN{)1$a=z6`&Rc$BpbVHlCUCuG^aaSGNiT*l*zt7%pdKAZDQTCbd#D0Ak4#SjR#l68rIP8PrH}=>b -eX`$k58CF=Yf;bW=*^0up~5rGMtqeb&WXh`T>);mpI&WmV==a4IPUjX=j2=x`v=uvS-`ew@By(mCblz -jY&-(o@H@W^I(DGS58iX2Pg8<>ZRyy>8LjZlN}TgqU32nd@k~LxK2GVu=e)w!C8N9x?=?Modn@BbKX^ -aHv4vjSHiS~2IpXCX=sRjC*CD`nylXgs?ifcN?nw*b(68?44WZAFU)=w7*?#)2w4?Y&Z{>dREg#x(`F -`9x5cd{>_8N4<{kHJ?#r^Mzy;D(NETg!#>qt}6>!J<{eZJpM-;?Ta?6G6S)NlFxaf5!FPW_I|aJ6Zq$ -t^x!4mIB>&fG8dFPuxdh&Vd)IF9a@b)cp3JCR3KPl&!qE_LW^qoHT!F{KrZk!3vn)3?Vbvw4DzFRlX!f|51u0dgn7hn5~Yjd% -`o7<*aP}nNrW|ixY+jkEN+vBz5j4!;Sfm=Nj{Tk?(O%I2DB(Ap{pal4T@6|d2*D)uwH;!)8ouy32{m? -Jdo;7}KN!epD%lG==dVto8S99un{+!|d%^xbt0i09)0NzQz^=9jqPU#BnRek?v{;d+;@ALdt37=y(KjG>pYxh5|^->~YidqN3JAwZ>9SgxRZY=k>m$p1> -bTrmw=#%W*=W74%+$Y{yd_`NeV+i@dx*mz`0i%72X$+X7VaY9t(a?UpD@P>6;Ax?*#W)^nbOf2KT1;;Y!mR)d4IGzx~(;6yvLgX -GdV5tw;O7j2_MaHosd`L5~8UPr`3E{2m&xq(`Y2=(sZDp$%Q_J;o{vx2zv~YmedxM~~(HD|#I9XY->1 -ihFdNnHshO?q5$%3+o8@^NDF;jo%f9g+ln`fR#Opr{5AbZ{dQl4EW(W8G2o9&1vW#SMJakM!k3qVeEjn5QRQ-|d$S^&dAE1N|YaeLsQN1(o$jVl1zE#l&+y-=mlG$V5?Ikt!V6* -VZgx3N=&c4geBLkFn(=Aup&BE`PqQFlZML8VD+bCsyp})OHP;uU$adggLhcSgcl6*kcoLOPBfi8jHW1 -)^7PM}?H2lz?68gy=E#(}VZVB_`A+|3Js!=O7xr63VUN)CIbms-)~s8?k}<8*7liRWuW|nZx -aKspuR^-IQO6pZZmAwoGH${*sk7nQIbr0(OTuq_t~?s~MF&ny*gET?Z}0fb2}eJ8^RdSsyLSTL{D2+u -60~H(sReg)_n4_)jGYMn=FS#!fLS|n&^Hst6_5OJ^zXK4<{iS%7SK-``>B8JhU9jN|Frs*ZBu-ePC@l -P{&xH}Fic6}dtZbp8DbA^rC^C)MoIDy!S{w@`_a}QzRzoG6y+G#Z#5|)c0Z`+-zlAg&UxJ%iffehxK; -$^dX1@|2e5v -YGS=>LP4KH{_zX7_`5_A5Qqc=21414?%_-kB8dM@lzvdyvO5jpC?Ysq={Apu&QT?OR94`snT5DM;@fL -U!>cyXeU-t(W+Pl+}D4d@pZTee`AgTUejKGJe)FyhGKrVPQdSS&yK0tb33z>lW0Cbqzw>MXQW`;5-v!$ECCfiZZ0JaM7+ReDuCZGiRHQuN_E2Y9hGy%Mm-pLI*|3oNOb&gI>^8Ff -RQ!G>J7I99^-23u}N-PQoMaKCGWZi-7c)TuwkpV8FRgz>|(Ydd~-iT%iG=nsZ5*ypGywLDl)E4x*Vowuh5?0huEI>>v^AszO|R{`PsPs5$-d2|FE3*e>1LnU*_Lr)42ui?J#~j#IAts* -6fvXCi~N_^Ubg6pSqoEAl^B2ywAQ2@S8*)GN$I&Bwwriruku-~=8o8DL4`eV2+{vO-|{%T9Q -O!(~Inr^8d>QM_e5bp-p>ZA7anmIpBzrs7kAL>(_(tTJM>ujH{l!oRiJ>b5ReG#5pri8(LM{;Ly-+|o -QzA*mxT;udgv#z2=}(M)~ -j8_E_l|T^BNctp)mh)rL}*eg6$MrY=C}ZHQ3BQ5B2Lj?6j&!C%BjTbsTnD)w=`SGjeYa_iea)wND7y? -b`N8*v+5p=1+F>8-_MJ?W4D!16!fIdd1J*2zu$EJ%IG__F!$3tN$0~rT1!X{Tse-y%xlR@Ez<^7BXrF -GmgS<^q+x#jbnMYApG9^Qx*vBaifgjzJ=I?fcqxy9v0-w-Syy}HmZ;rMlon7)^lEupq+q -mUm>O|skx84bq&JtOgVnyzIN%#t^e{I7wye1*Y{&rQ0FHf6Z?}F>seZD0J{=y%;?cb=U3wx4+9&}Uhx -rPoTybqCd5~uTet=HKosYMb&6>4Wmnz;8`VNT?;U@}ZH;3{{QQWqAh$gmUVjJM^>uHyz7G0;XT3T{h& -?=UEr7DVKlEYQ+hQCj9QRCx-^IPgJk+7#Xv5@_gO>g*78DLCEv!9|3LAaFl`PmY0zo;N4fG32a$LHWO~$$t}>$tp -~Yc`W3gtk!IfR?Y3p?(ehu@ -~~{p$P}-&YRm8$TC?`}bM&s=u&74dQU0MX%wzDE3j~`93x{whsP&$>nk%f&Ny~EaTf1I`%fMeVWzZc; -yuxw4KeIZ42#q)r0VZcloOI@OuD$>)@xJSK=-9Ce-W2`8D-$|Ni@8&Jbn43-{qPf9O8mDWA8vpL_)M4 -SD-S&(^k3Zq9l2jEGd)uIazRctnps#c$2?(3kXs_S>TP?e~Ll_>L^!5BygeH%wR9_5p74WIU?;amHP^ -rry6Qd~JMxV{t+oT&L^57S9uccYzW92DUyP+Lj5%{^xPcHqQgxCmiru{;U<>6|ge4e%dy+{%LWRR833 -gkdA0-swmO1^|+sN2F;Vbd?;&y2;-#jh1}FMM -7J(zU%*qziP$?>=!(H*To07{?vLBK1q;zAo<_#+8*rHngs=1Sn<#zUC50EO4WFM>+UJ7S;<4CeXBRO-)u-6=5I)hkJcx`yTErir -V-!1>NxgQ;-W(m{ouYop2Mt*x-lH`D5Fi>TIfG_Xx*^#p>>M!N}ON!6gGY)kRf;21v-IL)r0KyK2urq)r06him1hye<6t*M0=l;$3tx5#wIvr%)> -sylCUkR9wW2AA4GAq1iHSq(~e07g^AH<7!S_^UWaek_&SZ)+3KlOqC`DMrc?<_mn|A}R1mRP^f*CHgs -y&t6!%g;HlkQ;|XxsqwcvV!@6dC~rX;WjKc7$5362;{d>{J$FrWeC&0U6XdGpY5%9wj$ZR+O1p+__HZ -B64$3fUCwAzx)%4cZv%FD1?&qIHpdPOz&H~$E_&Bf~rVG;q_SoUqOx|I>Ss>pAfZY};Rio2a^<12xY~76IB; -CL+;`*p}(e53LQDI6?zJ~$sslB?XX{R1&I%5}|LYKCI%yAc@w56jE3v5>~+4!Pm4_j~ds~Ug7D`0ybq}8^Ht?&52DBBRoKhSyojc`9r -Q4Xg94T0aT9&aSB>rt6FG^`?V7}x>R9`40f*~PuBz@O5 -5n#z1!c{>u*Keob_i*Z4{-2w7S$>tScf9%Iy@M^QY0q${5a-ED1_!po~t^;>3?#FL?V1OY}ugNdHXMS -xcfjBY$Wxa6s8?Nzd0lx-FC+aW_o+XQWvh{e@Xl_SWe&TwhHMqwb&~$}eto170?0P;nUai2lk4Ccee7*#$C^2`M -`bi6|rxAE`8UA-?X(>_swtZxoMcPPfwDTwP}W7hdHHbevWZJx=sjqTcC(b|M-5|KOkC1^iQH~GX)JLdIQlhMB|7qAX-3lHPJ_imJ_Wc`T@}zqNj+q$`Uk)sDbDpqESS -r5uH!eO7u3OYlvFbm*@(j4-u7DBIVZtqRtFYe*Fk$RNQd&AWt -l>#z^5_L43m$LF>0FoCZ!*nhh7eOZY2$QSj5IHZNa$1uoZ#_=uK_^9)yTT*%WCE+OnoxKzX6MA(mTIb -naol^R@4xHaKpn(%dmTM=&1@YxAZ9C*T0zmOB2@r4r(ARMI$Kk^pGH`QnNL{eeuf5TY -y9VG_*M-r(BPsIp7y>|!!OmyzfZ$or@omAtgD+_8)u6$bH2xbk_)iUHCpG0=gAFG=%V+pW&+-yQxC7xN4L^f -$5aA-i-3V_Y+?{ZxhJQ@MZ`ANZz7)7OVI$#C89!kI;Znk3gv$xvK=_a*d;{Tj(q5-L?d?t2N80a{r+> -pKPyHsH@@#*SPI=lpgK%5I1*bgqS$E1a{S}1!N_!FRBlSJ)$qy$yfN$?u9m!)w0lL&gNKhF(C${8nHX2($ -ujB7i>tev$AS48Oh@4g!q>0XM+?3ivI6U&hiLrzz8F?nnA(EX~wlG-5xN;VdpN=jTXgmSN2YhQZ-9IW -1X+g@q=2Hs@uaS!@ek{+42XgH^!25HqNsbm!S6%mo3oJ~Fb+Ii^CZ(~y;KlMF+aHQ!Na=elKJSukBEJ -jr(tZgC2m_w$IqS+>d->bE2X{aNhJLa;=(#Zh22WtsELHm4!aR3Ou7&NcjnDq4Tds^rGMu1XnM`31!- -84cqp%yt^=W@n+@#-&lT&ZKgb^8Wx+yX@Qnwx!G>W0dF#$l5cXFi!4re{oH -)Hm*eY3cTIV{zVGJj_!sg$>bo=7ZZ>7RReQBPGxX|Z@W5I5>852S3pJyv`?&GZqP(O+yJDDRc4XNt1# -Su&CSp2bttN-VjSJQ?gkQstvD;0>6p)iZy>}-IZqiH0LJ!Qrhqm>onoK>F3i7Wq*@;xl#AahyA?^b<#6ezrc&IdmOS4Ap6dG -=DNJd-OCNjaK0KneA%^^PD9=0H|O8|LVwb>t(w8jQSIeR;e4CH&C%xcYe`=%)`r)!=CAA7`|r5_@Ada -CS!S4qYTe!Y!P?8cKFhkJ4KK0I&E0+M#J#Sy)E?+tpzh(_FKvC)Yy7(Fr%jtXc5feKJ=ezV%bK^3dwI -NGJE7IR!dZw6oQx?a1XZ-%mFs4w$uALNgd9Xe0EQ-kF=g{)_CTqch>SQg7?c4mgZK`fiwfU($o` -()_9CfW0|%nk=*mAgI1yF=d5jMINBu)bc{2??kO7YCu*ql -lwUfq`SJcL&3r<=M(@&hJmoOLhjx3qh!pcYgo*a54HNAH+SZqBt|#h8)Sq|(ZoEzu7HK< -@8%Sto`7PW*Y^VvHoj*HuRG%J^R7XBp92qJ|#?Eg*V`sNqLJGl-TEttDz`AU@FwqJ -bC5ooEHoYNEA78;C~zB&Z22W?_-M{zS56aBl-DYf>cCBNKDNof9~D+{MaUo@K(6X0$*0(Eaz#lfQ!vT-q8OPdHIk|2mEj4*7DSN$lsP<02E^|1K$C1a=E(+^1;)7 -!~Ui`lws$t3q8UQ`P<=(!6ggy`1hp;!#lWZ4!B)w^}h-)TCa|M$qK{WYU*Mx%Le~jJ=`%S`K)f)e&|{NZu#IBC14H}^03#oNWwj9uCWx?B*0cdOW#^DdaxI>WHyg^Rr}n+}x+w&1QEz)$E*OvKE@P{!-lCeaSsu9 -hQr=!(Eoinqtm!TJmiWH(p-kFClh@sX*1r%^ios%!#Hvb83Ds@a}r -v6)c#46`#gpHCoU!5?&&*ld={rhtW_D18zSWusUEn+!OS#lus3CYuE4e=ox|f8pxiwY+Qp;vn~~_jhM -Af1W;8x&0}B^Upo}YPUb#-~5wd4gcNd|8KuaR;|9{&b#iur}W-6Ywx@Nfd|*Ee`v$ThaY)#(_@<-f8x -n4Tg$dR_4G6Uc=n&=&uxGHg%@9Xx#E>qU)!PtJt~N16=jH9w%2)5_A -JDo@+jfENgMvGB?9@4=OK8__-Ft)?diLtwr|%8@!uv-I7#KNd@Q|UyhL0FI>c*(iW5$jfA3ecn%FN0( -=Pb;%ELv>Mv*i~ou{)fFOP3WDFTZ*IEep~!TIS!nV&!eO|97YVzdQc_u>E5u#!i|%WolgfwCM>m5@#m -ON=`|gJ!fv(yqkpmTju|du>b#v7Ep2h{oTu>1yo#ry1)77Y60ET{ddx<{nIy-<{&PAh>APFpL9hOethB-X0he4#F#{8Gue=ar@()5ND -SP<9q^yT$d6B)q>h*&-(t(=*gRm!5YOH-=+h+P$tIdh)Jn8~sFP?B(G^7RB)X31CZgp;cMz>2dXVTLq -DP4yBYKi(Ezvrn^+YcaZ6JDyXk(KccM|JD^+Y=m?M76}??ZSX(Gf(AM3ac75zQs)Bw9qYgy=e=Ig`yW+2K9J4z!p}gZQTvT3JltLY87KU^BCvEHQs6n` -9QRR1l`nWG}{Wc76lb44gS<_%36&;Cmy;;4e=ghx0*Jp@Sy$L8jB3mua?>o*+LT5yau{jPa>BW(L@Kh -H#%TV{mr%;9{9fmJQcH!wk3#cvDHz8MqcVQvNp}GipHV-$Z%C~D=Wqd9-5ik1m^fwKVjr%vVOs#v>)LRX+OeUr2PnoN_!FRD(%&g+6QS5!ri65gnLN)5Dt_26E;Zu5bi1ML%5f;58>X -@K7{+o^b_tY(@*#YnSR3kWcmsBm+2=QA=6KIfJ{H(finG_sC^(DLU=IYaKgg~M-d)DIF9g0!fAv@5zZ -xiBjF;#QG`ngk0!i{@EF47gvS!DBpgk+n(zd|#|Rq<*AtE*+(`l0curJ|K!hVD|5%wosPPjGUO2VOptEIk#Yo$Jf8>Bw6gBD8q5Dp~ -lOV~izkMIz}{)CN$TN6$q97;HY@L0kHgca)GmB{!Buaof;E|c*Su8{E)u9EQ+J|yEOTr1-z+$iIx4x_ -#+$tN5_*q?AX;nsws2!|4mBRrOH24RIdlm${g;SwpI@H#1vaG8`xxKhe92wW}Y5k4m65w4fv2{+2{)W -Otuqws`72>TNbCp?yL6k&zF+=`Ro38%^Mgma~T>R=X0|Ab4Wf5Mxje;NQRm;MP?O8e(9fZz -4TAGQTnG2s=hn%35O6?#!>nupKz4q8!7#gPdH6t>X7D297pMwIDyhHF?Ap}5%yj|=_edYxRS7m-ib5$ -C;;PYW$=>krE&D;nnmxl*~HJLo~D`J?9F^sf$?z-ymFX@sF0sa{>3N?1FnOpc+X!%a`F8aFB{3V5iTH -HK>qFIZYO!U6pw@6ksWLa(WRU^X+*+F{tAg&ST?2KO#WqhL_AgsZzY^VdM>5(<U*Y@^AlA2s-^8WhSfy+i3O&kA9@l$@nl2|FC%Z{aM-j -J+3oqy4T^Q??J6_E7oT{cvtX`X<=36e;J6_d0kEe5nnob9|o2y)ibw)E(J1*w>PFKtMVxI2lYQ1pq)n -(Jw_zSrmW?y3mCy&=velmHwU3wLey(M1A)9u1e4c|%i!!>?r)1=d?!FCVq(8$Z<;b*8gi>Grclqji>) -gvE8JK~Zc+7m2s!g8WrL5ZS-Eg{QHBuk6cbFO^M35+qgXU_QA>PpR1gR)Fo;1Rn%)&JLX;wV!2JMCfm)$%8 -v*KwM5H%8T4*eOOWcfwAtd=z$yRrA3;o!9E`N~hE-iBcr=N>J;K&?`mNQ|Oi8riaZVUZK}ixAw>Gkzc -u#pDsP~pzq+yF%kbHwfuH$dZc*RVTN0}M7^Bs5l@oJ&m -sSEJkp`2^V)b!*Q7U9Ek9;WJ1XLtq^93pu4ldqxt{vZRNHkScbXe^@aMo}Rjw$>vSf(x^W4%a^4YVV# -=7YxS{~2vDQYQWCF`?>Vo8^(vL@FZzpeoLJgLj?2q&eKt_wB?RT -Xle_|ne~hq!@NvRJ2){zuNVtq}65$64XAu5`Z~@^@36~JAA-s<8bA-zX?~?iwevfb!;gf_95tjL0OZX -t+2Ey;i1V;*7O|VfjZxTrS#|eiMmNR*knt2@~@#O+#Igcl8oJ4%t|H~k}Qrd^`8!~;GdBIZR%W`>4Gc -UM___Dn&Cwz~zhi2ZelK8S5*K6hxtBL=VjF0eo>7TGHr;UVPBdi}J@bfZ0%{)#B@z)Y=(9Bzg6JO5DG --~ECqlo`B;W)w{5l$og0^wZ3`v?~ieu!`>;Wwo{3BNDxNqC1$58=-B`IWO@i6AuQ)z -<-DwZu)v3=%`oF~sEzI< --TdCo!7p2U|c3go=GoL4U;{zk%^2>*j{IpLQGR}z-b@oK`a6Fx@xEy8l%QO>j06TejIubH=PB)*(?l= -J#>-d{gdVELSq^SE+eIE46ee-AmYF6ZIHiT^U;D8jN|AlD7Zbp>(6m(NW(Pc1i$N+bRv!g5|+&co*pm -E&X-zh<365%J}`tz4HN*DaJ1|6#(L2+Q{wxo$xYo0k(`KBwh8xSU6?B)(kXBImi~H*wX(FCr}GoyQP9 -M*P);!gyp&wxvr&=`1cc*>lEa=3jJ__Z_euGLw- -U}JTrRO@olOz(<@>r^SJRnrDe+~$SgwHlkkT!{hD<~X~dr|(?{d@X9?#L|6Q3r!f#7^5q_3%1>wgC -%XLX|9aA;&H%t2vmiuwZbu`1JeTctY%Ga#JX(av{!gAe=T<4=7DR2p4xsFDz`w1cbDw!V5I;(KvFCZL -6*hW~ctCH)m;)tJ4Sg!jSDKYVB+C*JvWgwhO{7k}fot9kJRYd#@!g8GvO(U^V;%|}q5x$kMTxTWMWmO -WNwjX5Gg!2g>BP`eb)f4`Ta3kS$g!Q8Y-bOft@T=0Egttq35|-`DexjSy#g;*+h2jLYJNyu?1A$%`sJ(74}q6x_>OC^r#z=xz636IOYd@?e*8bnZ -M?hvj?`SJkUb5A7iHPh6_D#rmC-*z7scTaHa=dsKFLhKYo>p@zTIO`enM@4|&1=@Ii#7BxOGZlW+GZzd6)yNPCNU5SM;pp2k%##k{3!-Mg57a_8Hr-n!#lHGVPAnXASx=GjbY`4#hk7PTKD=0Pp0K -4PB3l|F$jYIzXr&=;xgwwOO#sM<@+cP&=y$6`&pBjcsW4YoNqs4h{*skG=`sRuA)bf`{y9r2r#JsRgEk9yj$W?v>wyF7PqxQq)-{H|76i|7!so2W -vhgHQ+F5m8f3simNyoziwB$hQ^Vp$`_JhffbN6b^()$-%e=p*KFT=hp_S9%0?sOcB;+72~8#JsY@Exe -dNkP;-866E}dJAaXeZ*$W~&L_C=5?=nTYJ9T(cs^I%-QAyu4tj1EH0xiud-`9hvCmSCJ(ltE?ZU+#Jp --r4-cD){TsW6MUvgFK`8;t_zrwC!r$>H@dH+JyzgQJXXYScaKyXW3HeY*Yg_xBg -(RXa}Ry?EY^9bC5O52p1SqmJEnOjpMQS-hpi|1{I -amqTZfZhe&dh+_dl4iQ4BWrY`x}Rou7H&tc2vT!M -?Rc;$EOpg)tP!d_r>_Yac|py{(8N$Hq_Nfyqe>cC7a%$P*PZ=swF2D1 -^^L;l?pK;ckt%}_H$E8EPl9R&Qz47x;!E=KrmeHR*)@5E}x2T8MXB}Rlb#k`E`79z)!=L1kZ}w{qa*@Pfora--ZMKRkZOpU6 -r%zee9Noioy?0JlA2@7hj#Ww=*Y94`0@)_Sd|3*T>HKWZ%R6Wd;l|DoHwvfslSE`2oNAA`D-E&1%NdnQ>{Dz?jkPtFM~E_~)e#?HLDYg}RJ>J6PH -e6)DXj(bntvqe8;@{HWp&z`ulenX$r!54Ce^qaKfu^(bqy}oz9F74v1+%KN$yL9FKOA?_3W&iQb9XI{ -d?!~zq+wUx4IXn9sKB;;C^OboM{?Tp58y}r2c=Gu8m`-mb?mx6-Q*hnq>&6&oel_9w%lXS@-0;oI{(P -I5p|hX&t4VA9?139hbMCpL+oC;}?kQYaKWX46Q_Etu_~`dVw|cXzzWS4IXIK4x=G95cj}^BW`Z)U953 -jmhS@)7pM_*&+*yS^CdFJEcF<;Hh%WK#3r{QzHo)f&M@1vie55M#LBS)5vjqmYNWY|}|JLUcGl*P@g4}R|YZ`)2-jM&bGNPWnS2RZ?7M}{Nt(D>wY}b`KPyYhL@D?xG{QQ-EF?(dYfIzNFm -x?7(T3hu`VZzuS8sZm;?D*wPP29d14Q_=a^)ez>{(!1TT$OB>qwm6reJ3=A9dXZyIXUwj~IUf{O}9<8 -^0enYgqZpYo**WKJ}^zbJNeV0#Pe{O2zvdgy3-x>otz1R3q>%fNepW0jZ_MN!)^1dh62RL3{(d2ViWN -d|{a6sh1JLf(d_+!{1W?cT^*O`^|i%O3Le!c0V>i1{g8#w#*d54bf{v&PZ>PoLJlM}o~@0-!(1@?Z}S -5*nc+e-G0JskPUxyD|33pU>yb?dP%#UI<3eo(Y^*Vn~~UK{Ha(bjA`cXYlre`Vd{-+nRn^0xfUWwS=C -{h)r{fKzFw#(k33G^JO^6?4ny6@Gg|%ED!xv)=gTe7gUYvzF2I!$+3+l;5*!=&!Nu!g`*Fz3JE^zjS^ -6sV)<8alS75>dM`Nz4uM8`2LqO#}l_b=s4k5_rwn~7q;2j@wv}-?)tr@San>qC3t#{qmahuYXy#cK^3e-<&+yYy8Y_C-QlTz%MT)`%S*(%)}ip4I1&<)A>(-G{ -Lq!e8+Fg_DnxiU9+&ZvHYclWB0xI+MH@@z>`lt%=X0HJbumYX_rQQkvyaR#FF=WcDwAiYVx#2$F_gIB -H@hwiGMs^$V$i6i7`FnLq`KIdhfPphqT@^>YhKIFY3_$Gh`vz1ksZ`~A<4{%+!kM;?9d^l9blk)LZybj4*j2cbOvMAF3@@8D-=>^} -4P^Zqlx&%E!}3%fq;3#i*LxwR}8RuWR5~71_@6&{_8-fh?!Eh&)i*zNaIiga`+9VT80JvgQD%cyT^?(wyBKCBD9dU0p;OV97xoBxBp+ivj -?HEw^`xax~#jRVJjerCz0!QX!Bmo>}&WtZ}or`#0r*~ce!5m&#Q^6~wBg8RK#)BBAs|MGrsz`<5;>_7 -j?)RWWwp4_iH`Thg5z8P9FXYT{$X@f^h?BTt(!`JUz4gRyqr(JQ@!YiXb%~_gsW>+Ro2Ae7thWaZtRc -Cw-jdcW_m@sJT(_cOLs_CnfSrKn}->~uVPYX_f}4{WqmR -C;LSTnp1T@sHO+c2^u(JF?|d@x;?A70-3=(pV#Mb)Lh@Wk4meSg1D`soXMZi@c+;LCRvObC1ONM2-p>yVY5^EX-+oEi8)cHHP~54W#vHz -I9Z=N%h=jqCUJ+#6O5I=;5BY+cFN;MY$?u8aF5?>6tcK99-CCUNN?E4`o}0>F31UjC5TdFe+oFTZH!zGfwt*lkMmsqR7-K58J`}~KTe^c^4j`#h%pX1*iJV5$?X$bdbB-Io=3^Xhocaal_ -x_pFk_%59=W>6Wbb{j<=Q*wV?ZK1W{pzqUdALPKIWipZ6(wP#qN^9`HtlibMsN9`bnDF@EQo$=@WIr9y0qvaDM4 -eF@6C+fu)<+^alR?~++DA}vZ(Kj=%5e&<5~4Q^fBnzFWm(Zn{T7!#`;Cd@ERIf|{q&j#=Pr)^bJxpDsz;lmvp-gRk -9=y5p4jv7t8aaq7oFWHanl0@dC{W=e|T=PWp?!Oo9lyr{mKyiYoC5QYTpAX(J9f7%uO_&?q-c%ka^sH`$l8*g|k;4Nr<#Xk9hF8bDv(yjNU!}pl#p6?C6^=j6Y;7utkqLwR -++^yB9`JC^{ed%DtJ@3}4!bF!in^l`tu_*H83@_p6wz -rHVt>@XzyoqoE1MOWlSZ>o5r;^sF@(Y;RZdcISBd~{~Fn8$v8%^W>D<=1)9i_@ZmUO4mJz_{e-am6El -9R0g3+M2&`D7GKCf)Lk2;y#BshdhP9)8aURf#H4+cD^g9fxmeh_#1B_?T?DwG2qr0-a^$~3)mEs(_}T -6?RGq}#AdLX3T;`rX8RaJUkCSOrxE5UCX3aaZE)rr@(av1gSp6@RmhLDAvv>c{CpeW=U8mn29wQTDgc -k!5C(Tcu`D$Y()i;ipyb%|^B{x})4M0*D@5_@fXOV4&k4(U>S_6zpkGeDJ&$h$X;@Ndwin~MMLArtYx -oYc)9J=$4rewbFT1e7YRM8lBu8Ap&o6Yg>;d>-r0WQfb^vRez-GA1u7U5^Vc?_MW+wJ{HE4FwHSjTZv -Hz`s^@H580`5mSWmU&FUG`sSb{hDRH(B}FWy+ks~CSO)Wv`=u&>a#4Xf -%PAy_oCCRec_z4n+(Qe3+4>EF{O*T$j$Cu52^_5!n;FW8)zPAyWw{ooIR3?8%5j?QhJJ$!9rnSQ7TaL -6ZD~IlSW|9XRdBkscz -8-`O7TMTr4UMr^)W-{P&`D%SWDG^3#m{WcozCbeDCFy|c}{D7gDE6gp6~*{M?^qngj3e!ctQX+S&`Sc -)_;T!*%uaKhC2-5=B3cmSaJ;JyaH!&|G$ne6N(xYCKK*?Yr@m9s -4#P*G6D64qqW~-BrnS9dufB9y}a}!!;%+fp=2F!?jt@fKvnT8isqG -!yBcr|C*~n%u{Qx%9`f)?^mz?Dt-QwuJYSF)0LbuDJA#2rT;vapd{Rt(dzKazXZ-je(ad}vmIu;WBwe -dM8)%y?D=?Fm1Dlcg2z{-%jewunB*CwM$At(Tg@hidA@Vhh)9z?Z%{$DoL4=7l45N<`d!5XWCtRG~_h9_Bg4=9`kUE=V5C#Zs=IAal>`r4d52ya^p+E^+!DX@i+oK+>&@4Ax9LxmXF-{vZioPe -`)A(h^HHu7vYM4gD2y9s|);MM>Ydr*;xx8i0UUq?xK$d>>f1Gf;D8($XG0B&x1&6mF5Iq?%)scvGqS^>lfy{th -(Jf3~cN1C)_(!>Rhdry$5WWO5@Kqh?sk+H!iGTa*$TE7JS#|d8*+Z)%>Q>=@XU} -$LWfACh*t;4X1pzOitfI>qp!d{Ol_>Yron2Q6WxA=U$<5v9?*1^S_mys9)y7_RXTOJ{a>iKY+y}yL0A -a0|QSZZytgeo)?bV;TqmD%~V_-PAomq8M!T54nQs8{VcN|*X2OJ7U)0rKEahZHu0U!KkqrmNH8*r>ja+uS&$Ei-z3pTFisTmS-W?$*PR@HY_>+Ti#R}T3X25Al8_IIUqosqG(;CEk@XodAWeZqc&A-~ -#a@(X>Hf-+_Bdx64$AH6??>E28nfzuf4Hq8}BDfk~t_MgUv1fDqrdfp}UtOh;DHskgs=u!q@weI7Z@t -et#LU8@@8DW8Yu*>cl+I_M3*TGHDO^lnAz)cUQ4^c|h%@;j^>Tw7V+oJA>=q6FMsT}Td(@v1n;~xadG8Va8sJ|4xC`IG%fq$l^? -V)7BxtGr+G8rLQL|2t0FFwTF!IO2#h1&q{u@`Bn5L)bBT4GPL|E;2m`1w_$;5I;5XNRnSgUiF>52vE7 -5hF#yuZ?YT;{j#F4c$eBB8d=GP*g{yJA^)X|O@LNZ5B(~u1!e{Ud{@mp+2-B<4rQ{SmFX2}~enz67J3 -MW&t{&u^1HH-5P)(Y1f!BD>Rffl+jJ2LJMtsLu9sC+dMhK6`Qmn(oqvv#$~Y8WmmoiYxq&WL;vJ=7r&c^UkbeU --1uP{ekI6eZv0`qZ4_<$15khX^XRU9l*4Dwj%OK=m(}m0-uYNh4_{kPZ&zjXs>|j64|`!s?+1A$+)l3 -gm+fs<2bZ=`2EzG!`D6&CSp5Pt-C|UcMRO7@i-1~ot|kyr(sZcBb%wa-UAhu4ZkSLOL*P~4&X -Y9a&!l87d_*f(@a^tuP^9#gQD8;eMna&kI$9XgWy)h%azNXqai+Dw>EN&2G0S#`-m`VTZe7v{0Rzs3x -2ZxoP_&zMi(BWJ_|j=-8UZE@bNBx$Kbxf4MUz9&eZtJr}0{u?oE*HuJEkvO!=yo&*=(t!Y}RJmh1YRi -3;?C6;?!kHbegQ745P|+kyHWOj6j2Sr8xjZJn$zL$V8Z(3Ib!Ah#jWW%o#JyP2Y}OYoC*wWYT0&s(4j -EKt-o>)>_lFhXgWm)Ju2OW@_!^EsM+oj688{d8;WQAlTdcnxjh%(g!1-+ro1f -zw<+AsRW70J2@G_r8w|+jFJ0hrD*?AnH7yTjsThp3h4YN8v7ZYTN{(St;f6a9hcpG4bcP&`DBXA8QY=vzc9h?Wt(kNiJN^ev(_L@yB4 -nFKWu9Y%Bt(Nv<@L<@-CM)W?Sj}U!{=m$jWi2g~mZKj~1L~kHEhG-no1w;#ot|s~j(Q=}dL_Z)}L-Z8 -UR#}1u5j7AUL^O)%G@|o~T8Z99bPdrhL|-CWMRY&W&xn3Sv{A-S`7)5`6ryv8<`P{&^r385nZ+xS@@o -N6XND*LoOg=Z8Jll&@V`-Lm45Z<-PLj!)JC}W-Oh -wszpf$l_GtXsRbm&rOx0`M1(WW)%mg3A7G2<7IF%E}ip-rChmG2VIoalE(es-aipLT}Fgw18_bDy~;i -*ri8J;h>MXf>1PNM#aD%9u%Xz1NIFtJ5;E*lAA9pKHlB$L5;s>^Wr$Psj|2%(M{YnJ|_FlfyX~r@!!< -&sb}|LrOWyV(n%Ss;U)(r!cr5)>=3LYotEIrQ -56h|gv{++0(0aXt5II0(XQSb}ETa=F#ACl_Uz3$U<2D2 -(}#$aRz5Sy+HYX)Y^r<*l$Zq@u<<*=e0^TV}C=05%jQy5-9git7}+EsFa|bz2qp^XZ;cz2TK$HZ9d2S;ttq*Y(b`3sNSTt!C#n!RC1Rc}Yjd;KgKT+j*#Cnz|OldZV>4mZW?OCK`{~yDEI~A_zi3n}u96astUSQ8Lf -j;sP_9HJ43Cj$7NfL+3jF5 -OagEZBVug9l4l{iG?{iX1fCyLb-BzW}&m7(22T#Os$eg@eG;IDUgLwG-mMJMF4FNzfU%cj0+UnWzWnJ -<+r^kzpf(lzgIKYu%}K(J~n`{k8Uy4TK{R`ky1=ac`Z@U>S`ar6F%{6^=GH`4j#fBw -aMcnepVUC0k4y<-0=z<>A(Z1EC&LyOmcxoZEPTrD5$DflfPJo(?N_kZ>E|9ZL5T>qCB6Gi9k@rZl|o) -j6Yi&ys~{48xGwv9HtTD7(IPo&1%N;oYC|9hT3{)48pj6DG~qZ70RK=qyB4HIZBP%DmQhcH$IbO_K=ph-YC0WASq2GkG=Z33=Q>LJ!M74*QpjLs -^Ddf?m*=o=>7%ff^^Gca}ma4t}9NY5byj*I2!(x3sFr89e#_hzh(V)m-+gL|H3Dnt6>ylO4W1=a3jzJZ`3LJ8wJIAh;2HH -cgQ##jjLs8<8UuG2nVzQ{2Rv$z0!!T1L}nQssP$isZ*+>(Jm&%I6>%BIsw;&L%Jv69t>t(l992K;QtU -%vy!PqO@#JWIp@^~9mw~vd`w}ir&6y=n~LAA*D1J=r}ZqbYaH&OV^R$9xVK)WQVG-u?n9Pk|Dehox3X5*f3CIvT2yA#4U0IdMpn1 -*{>X6wr537Q6v&1`Tlnh*BXfnE!sd;mR^!B~HwwOP1VnMujb7Ivx4#<^L@A2XIKlQJX+;#X=}!$LtTE -R5{}TCiBqA$gEqU6!uA0PF>{!2#tP;>%qI82ZcVjv-|4(~g0#;SE_P;i0Vw2c#BO5%9kfa<-jhr|LDxef*8f9HT1)&^prgAtAx3J9 -cUT?^^G>-eJ9Kz5DE~nV# -o6J`ZVCfTsZ~lQ}Ms?+ZdCqQ8#56hSoLq{5891>g?2SsOMI2>AJXipeCJn{s94T-zg7v)*Wd?mAD`1%Px}SjgX8K|) -UVNQme*moIJUpecU@MATEIZqslN?&IUI6J7vdDJX9&>}_G!+hJwaaCwCkg89=UwSMvRsOxLiBz=9$lT -j2_l%@@X%it3bE+YCtpE^DKh?0Y(<{-LZ!>ABUntvpQj=Lwy@nGq2&hZ%1pTYxMqn*0P^ix74*fKCIO -OE<-#1Z@`}cMz4chdAuHLI6(V)zN_t^R<=PO2ak<>mt>VNmC(MbL^9wy+~>22&!B#z!Tx(%=vup01fEzZrp8GZut;fw5G=UUPhwjb?Qv4uJ8D)qu++PBwMosamfx<5(Ys`F8;V2l=0s{ -4n;Qk|X>K(B{kFQvNt61NG_7snNV^W2ZRN4zJ*v8#@{hwQ+7f%8c_b-S+I$!GPd)QesTeE=WEbxS$*a -*Ug2h3>yX0DpwP@c$I_xE*tk+ygm*5C6SF%)~7>h@;f -^Lz+i5+dHHs0+ylwkYmtOK+ogwYnaExckDmfE&4m^^=QrOd)^Pf`90s^6Qk7tnsHy!2_c%pzgmC5xZt -?#N80Tnx1^uof7s5?d9j#HFlv{rSB?|M6=Sx@W74kxSvoUR>mi#`kg27C)*&a08cieuifc8|-EFL!`FglK99uj!C9hP# -)1W7%wWj1+)0*d@H#caQ2M@VfZq!JZo6k)e>>2YFpz`-OYp~New;I5=!I$H2npj4@y)>~1;~w2c6E^m -HyCw<%{qNL7DPUP!P3+-u2Te441oqoe!(0LSJ85DXjy=6K_~jV462N~@UwkwXjq?#*zz<-luO@r|lke -7)KGg!Yquq7YgeT71{WQ#Xhul)TYoe1D>sES?ruq5`m_j|M%D)xee4Yj{@j*@bvl75{ -r1v3B#Vt0#O^nBeV|*S({|~c$=sn;x99thnJAfsRX~GUzJxUXs0pp+0gqhd>u8BxMbgA?r5)ku=o97r -!RPuhn+aH6!iqpikfFTK*_#FP%^d$74*|Dq2CMvyh^O;0`4`_*iX58mBnfeR<6ofyR!f`pQc})XdxbB -gpiH8C0i!>1o81aIp{A)bm&Dt0@>x()}T}*loXb~@gUyOgn5*fKLGW7i!9Xl@-zsW8F%MHE|5>+8vsTPa@VJ#AZ8d!GE)F --6}R{D$cCls0j=yw2yd2-S0VI3MYRNlwV;zetaI?g`__=X0V*{ij)h|7*ovy@%8XOFX`oAyKGh7~dUkz4* -QsYVX~K^Bh5hFjnGWYPa$|CzvX&>OD`IuySJoF4)T7D44Z|?P%(ko*|8#SI-7Ea-JpWcN^aNiR32Er# -5lk-o*KmCeD{NalW#N^VN-<=l7IrHT`!l)h_B+yACm+%l)Pe@HH+wX=PjB1DjtsHgCuEP8W_VkKx$!! -m;&N+~4fNarJq(`gfPHL`XOIrH1tB?^!nq{r$h1X!iRXTuQfY-NfL*gT*6{JR%Yj62$cB({+Jat@vjZ -si~)-2v|Qtqf -Lmpjhl@&`Efz`;r2;&|pv9#@y|*s(($s}zz4{yQn_;?(t-vuZrP2eN$Mc|5b+qn7w_adrCfT;9Kiqx^mS#97t;e)g{)qkk!1EZ;%WaE|oVmH)CIM`w6l(L -cZLg&lP}&ae$0pMU7_P~1>A9_{0}`Q{IAx@+Z*aqmw+283+PYMCRLv@MhtovWocFon1vF9TqDrgKP~A~mpteG -7E2-z6PF=s8p7-d{LxhKii{ZnEi)WsB<{}+WNl6iTd3j>px^+s&_wL;*jvP5sujh&Ui5Aw9iH?meD2Z -nipOqWpwu-r1Pi~ESYLb`>5yfqla_-jeqM!*+&HdqY)CT-9_w^&Ar2H*z_J>DCj2N*sZfK8uz^JFfCO -;_U`Y1VPk@wb7BcjAynXz^K2)4Ir3EQ75<%zRr9YSgGvqH>i+g5tEXe))gvx-YpeALd$vfM9w73c=_lTp(?={AH&85oDo8Aw6ew0qv52?k^%jNJQ1RNl(PCq2qIm!1A)+GVDdDq4io4& -DqWk+&1ZTvqyaK#T -Vl1ufG;2k5r1?KS^=mzyYNf$BrEnr%oOhhklWwrlv-mJ$qK^3X7Yc5X~ZlxC%<+ks`#^(3@*u?AOD9P -4WiOPj(lhIsA9lg- -415dl9|Qh0;4cAwF7V$0{x0BG0{pmHEV=C@#0B47|NeO*f{v -7yz`q*!@IN&OW~Au`A^QXWao|4#{8xd$0r-1d@Wu5aLfitIZ6{KMFU+==D2L|PV5J~iKPF`NL?MSgC* -25_-&W0$G;8u{AO4H@E-#H7~szUelqZv13w%1>wy0b@XLY! -74W}v!S`^Ez|FwF4fySV-x>IK1HV7;BY{5|_=|y`4g9UZ|H1|T;u577*Hb}30|EyG>aU3!+Qp|+$M)? -z53au&I3O%EG&ndcC?Fs(II63Uw|A$Gg9mpoS_lrsW&8`pAHl&tW$R1H%H){J@~VfB} -I~ck_l$o!Yl^UJnho1cn6$1qT72balJi*SCMv1tP=3!;CWMHm_Tvx^>fA=-9rUsl#C6qy4b3(15V0J8 -rzy3)gXjkMoAkQNRxfByvz#)NME3c&mP0(b%!Q2}Fk0w;y%$O*h^+q@TLMSKZLgMEsy&UJc~+n{K>gh -?BITUDyNgqXGiM9|#K@7#7ur4czK=%PoJq_S$Q2AIcthJzN#E-{ztlqBsn^9yBmKG%PH9V0hb$ZoqZ# -E~@gE9V~KPtGpg(QT -bjW&Y@-FV}TxS{@fRIjU|EPeyg!2`H{=b%A~gxl+{2eoS6%Kv)vz_8$OjPjtiiitan4;uvZ4h+1w)+6G~e6O-Q -$K(TnrijUGC8%DColVsHg#f5B9mfMe|;!xu*5?4 -@L!r2lRoi3E|qlUP3%u-ya@`aSV02%=UxTWt_k7-k?6A7hl%(2ki$#5km*|?A_q9OZym_(7@geE{m{> -OL&9JQkKrGZDkFpLLgE4*Ti#k8>x-Pa#j)0AfCH)oMOat4?Xmdibv+e4z7NN{o>%kgW~Yv!{WQ|z7yYn|GoI>$CD~HIDPuGICt)xSbA29H8|e&n~R_IYln&MK1 -_75%ak#g=$^(z_cA8BY|&b-6W!$&F-(?;$#S2mVErQ9n)^Zf^C -jO{0sBkf8&(@#wq{5#wo^kncB2zqaI1RReeaRjaMf>KR>>D0-)lV5AkR(D;8<2LO&czfgC)}C$seEodI?d`kx_}y`}Uu)cO{dL#fd3(D~K7Q9+?Q^H|gvsR -7#m~dzS|8xtb;I@7weh;8i{CXK9#^+)-u&9TTD5A^?5Z|h?zqO|YM$`(YDDo4h;t`uX|tw~pw)i?44-U*C4Vioe@Y_^e#JcKQ=ob6W|~mz -#0!&f3(&qngV-yq_P)Ypd@M@jZUg>K4-_{p=MK?tpReeU(dXE-riy>3_TYnMY?yFF)sabKACUIjF3Bm -0{*=X7hkEx*Jl|c{#O-uXS>&4?nY@=`kPj-Ui}E>owrV&Jo)VpKm2eMbI-vO -Cr%sz+<)rSsgqcj9{%d9uhx9dK&k^Os&yevo*R7qjP_9=yEC0UEd5xdHE>-*CQ;*I~o={1N)koH-*g4JrNi_xJBiJ!U2SJ9qAsZ@u-Fgv=#kW@W=1f6C*-4?k3uw3n5YDO;(mt -W0M^fe;k@EDJGrM6cw_lcW7y7qs*|M}_ -$BsP_!!Loqq&yT4q?cpPaV0;L1!?Esl>Il~e4{G==9qK5NiX$)ypV2=KV_W$y_CUwrL=r1WuI~>@7p2 -ey=CBGyOdqPL+7`pZ2zW|OTLn_uC8w7dPbFNIAGt%An6M{^S8N==H -Ib_kf3q`=mTmCuQ6&Da~8zQxjK2|Jk!=smqJ+z4zYs(1nHM0dn{qFQkQfL>e38gX7LIr;MmK)PMS2tl -1Pn4}$(7U+O#z*dt|s(Apb3^n~6dR_gR8?3S|Q=8J#NbUFGlXGp{UQ~xm^N!ErkG4!0{N?A~^s2c_!v -~lVg^`3TNK-vY%@B>ml03O1i|AY4FJoKxO^8TGV4`$fH-S0^0v-OYD4_~~rQ>RWnA%`WjGy0as_@J&g -rC%I>Ugx|(Jo*{eGV(BNlstSbWhi(U2p$|K-;<0a~9C1j>VO3IwlZVfp_6(irYuNLLx;@h;b>5=ebJxwfO~d2vXo`Nwd2x>(J=( -*cFC_iclZJdy@2S@f`7n5)u2YZp?^q{uX7-gC)BDNSr}kGousm^0%150%MC_MxC};}#+{K>BgVUb7(I ->&Dc7ji82Oip%=)M(YUSssPZ{NON|1rmNp8uUZXuq5f3|Y_~cx>=M`r-Fv>8oR9{?mPB_RM}V9XzZ84 -=ge8pO1rwNYL@{L7fM~p2>q@&-6*%Kft)ZE9G6}0poAl1p6;j&3w`r{qXln|6yxO7!T3r3_c8A$OGqt -rgV$+(@(CS7buIK?IZKRLl$^Qo2Ju0^`w;1-+_l}DIYna+cSMq*q0aVnLHTwOdbq-rcd&DOEq)0A^o& -7&M%0w>MMCLYC}F6vk`+AmW^}omuqMBmPOC>k-16WVMafBy5_jt_p8oB6nGeQ6g;@tbC}bf19$5_$$z -IlKN~*D8$5J)t5LtC=tq3NxM$Cv?Gd9aB>nV@gwz@Gz?jG2h37ce8gUt~alU2D!1AW`ez{@J{c;U>C< -G67@UVILBzfxOu`6;m$H33w=NRJ=Zn#o1CjSx -3O%Q?=LrjhxK!MOUJA}vH(0ZwC8`rCoP>2C>K99Kqe;y$pzzrmC>~s6jBd^tqbd5~ -@0I?8o+YH4bP%zkEc8jeU1E%$?=`Of^ckGP7)P+$?RJ@;pD!5~8P!!Lm$H*{=g!HB*JI?i1^3C#^S}da;SK -Om?6hb4q_pY%<*F(DWolx8TsF}nUmhPQUjz@&frt6vVK#VRX&7VteRt#jhx9`il>S4n7m}Cfo_kIf6c -i|au7nSR7vfg$dPBam@IF}z9^L{E%AUakeUf3%Hu$8KQv&3xi5B@U@UR3tyZ|2l0X>=*8>|SWPYQI2G -2ZyDasN;H4gK%ar%z|nK>CuClk0hC$j7C4Id`s39@zGpd~b1(+`7PDzU8#%wJ!FY>EvOBlZSsokCr?Y -C>JH@Jg}%31M~Ax)Ky2-{QI%^eYuq~$Nal=*sx(?6DLmmh_uokX#;cS%uzhJ>euCDLP(oxxW;O%?Au; -cA7jv-Uvk=WgL29EhxAkSAt51@gUrp%Rk0UinMI2h$r&?d)bnsDJ}xB_7W$-hbFRRi!9yc{rm1o($0; -QxWgC2_^!E0aQBhGcD=SNKUYi;764l($mxBgAYC^hYuevUwY{!` -N}J=$YslxDSEiRC%xo>a{=cGgAcA@$OHW;^^kKR``G?su>3TK^q-L*WzM(|W6aQF4B9h&(rZcKB>%GO -ufBczE(#A1pGWkDAR64zu>Qj&yCNFEOvGDO7)@4fe)(g)H`S}7w#uMByRKk6RWIgIN -l3)%y1TG=z?;A+pg4eWWx#?_1iS6qqm+<*W5%a$)+uJ%FLx6_uT)oPU^M~;*oI&_eC-F25lY^`V^9kd -PCN*;{bkQY|!l+iYQ>&{i9<>r4_FhA=t2IuGe*+FvSzsAWgKi(kEoIbr9^1kBzht$+mxp3japO-FOD( -BCifBLb<9+Tt8k5}W4b*0iJ@<6*FpQMd^G~|VQ5S)jp_nf0?E8LIZI+EuYBT;tbrI7;mfBA-E^hfu9h -z#4cB_<~BfA-mD&y&ta9(hDn+O)w#mo8o8%$YNl95`nfdpianq?hy?d%j#75YirKBeaKx_Rcxmb?w3R -v-pGiZ?rAyKXfT=-n@BxXs2OeVRF{2SxOG%f%Fd;FhE8|Mym6a0eLVgc`(+!T#GTjrrwiILh2BCbJZ= -zRJro-MW{#hWIn|Q#VF=jqd@EBg%!3X&F!MRx1fP04}RiPMme#&pZD4Fg -h-=ntq@^v9H)vA-eIYmKisf1&6H--~eU`&xRCbz_SC^z!nuK+nb+Q=7mh_>+j^Uq88EHzKN7Sd)^;?U2~7qL)(xF%vH?c{6q>ebw1w7I -#t{ZZQM={MqkjQ2wFVEDzx_~3Zc2Vw43>jdf)=h{a0^cDS-jS)jIuS313E^*9hL#ce2|BsLRMfAJsKY -c{QIkq7$gxq6){PD--tFOMQ^xJjZjmrC|OVo89a}FTwMt%fw?W+HPc16GI`WJK066z)AAo5}C_tSsS* -Ks~_<%46pVZ#Rb+H0>VeKu^Em3$j}^NgE#Or0_E2k@CW?(XjY3GMatyUvTQF^p?$b19jy+H5w3Pri6R -c_Y2lE&5LKV$hEGH2Y8L`|s!{EwC@8|CqlQ8tbo1@nYfQhUX{;BPU0h(HAk7z#J5LA?@hn&-j^ -3((k(eW2~hvB@@Q?jDL8{yc{cai}3yT-&cN@V?cYQzLS310rWBRFQi@3@4Ej9KT*H`LmoJHGJc`|qaJ -WR;@rtG=9-gZPdOO-yOaZu>GSDp>Bq39P&CtX$C*4**P%z}5hGMXR- -9Mc{Kd4h+#vL45dJqS``~jAf60#`r4^IQ)|oTwL<@O7VCKv!Ap&Z2h`;_~=DTInr -GEwebDdD?P;)|eCv4$_t(>r}69zcpTql$|)Ce#B-Hu88iNfc5S~zQSafw)w`+cY%qMm86UgR+|Dh>Ao -n=0(f3$d>mbADKCZ0s<^`@!(}d<>h9oiXO-=L+ielx=@qE9H!XQikl+b8OrTW=`Rrccpy%6Djxpq-eP -m4dnIq=FOYmhqc{g*mN!Z8Dh@U@VCd{m%fKz`U3HJI%3-=5d-r#4E%msd&CLH8J+joiYD;)SuyVcf)VK$%dITWxkPlF6LLbC(gat&rV5M@z -WLMdfU=Z&!0a(X4$f3BS`~eC$1kjKhm#rE~2kyEXz2B*Z4QfsOk&(Lgsjw@8q5~^E=#=XO8W1{c{a~H -RwdF7oIfM_gv?)U&hfK6JyQ8xXHCJU-sBhJ>SP%*}9s>wk|FzCmZO4>fiO*hlbBQa7;LVojCG^%vmr} -?PD%+?g5TIA!XEYJ#RYvpq}?No*!`4N9Ko^XY333*~L29mU9Z$uQ7;oN4h?TLHRQVCM}$Q-da9E?SXM -Wn0xX@|EzyMuID|OZ&UhMsps05N5xzx^MsFntWe90{yC3hy+3jG?AcGc=DUq}mXNV^(VQT)m&p -BG?jySP&)h5X$B#77M~gE@!(1P8Ow>o;w{`zu^w0RmHCNg&|4v$19M1ei#teNw_SDJaa^8;@(?ES3*+ -3ryoH^5bo%+}yPpA6FIw@xP^5r94>0sZ~b3zv8+qkF7`~vrcH@up7*_`3=U!|M^TKetMb8WN<<~v>cX -I%00(@#&FJ9qAruDMszzr1q=7rnQJO^=KGju;(jRgkvS&jHWoPZOS8u4^_BSviUy^R6&G^f4f}7H -3vQSLru?@(AE5GG%oTA@lY8TaK5}22`4#3SRF2Ep|Ln1YR9}N#^s(QLi{;OlY|)}cF)LTD97$W`dY|z -*`6ewK1Hw-;5>(!h`6}+K<~#ScS3C0yvr@j?$%ZRX{VPJ{3AjJ)aPF^Xy4VEsOfNd~WOJP~uqb^*zmKDSb@pQWH`ZsgL&p5Bc^22)9mnZ%Ii -ub06ssHyb9D(WU&(|FQoUUwkp<_19k?$=nd*anfO|tBvOdNe9P*{)pFDn7=K0=7LQ$rjIVV) -~Nqyj6G}CtclM*|NI1w!^{)54>c_618_>tbI7!#SCSZQ{O9p<9=X|GIYVIs$gD)`6VsnU`U132 -~MjHENXNyQ#hAhO#l{apT!aG|%UrFZ~05j6eHE{BfMQMD81hhK8#BSJE(f@?}jD1RceEj>&@ob1ID6!NbZw(06_R4fn8I_arzUO_?%9`363#YV0dgUL0%kO`p7b_w -MXJ*thDR{lM2K|KAuLMhv`W%^JCC)he}DOy9^oCnKl$C;L|Yvmayrr7n?QBL=7K8F@+4KpkU_gZob#H -*PeZ6a2G%*YkfRd!Y0)^nH}G!{MO*oxoTu68iqhpDp~YiRYiJw6Dwgw@UGbSu+qP{R_gQ~K8&%N1Q?x^_jj69iSc44zQhyFA0P*#TgQ}^G20rhlPBV -rx;u*uH=*yyx>1!s&r=KFM>G8ou@v-0>%)V%A#+t}A7K}NeuYouI^t);JH0lQWCfX5cHGbbwu{l#Nou -tQxW1MRzt{=I6`EL_7dUVP)p0Ueaz -qmMqS^4#=c9EYmyX>xSJy)yPo5Rc|rLt=H&SNfH9Y{teMka9PZ{#3?$Os%PbtfX9(qWPDg88M`OQ1&a_# -)V>iN%ANkimEZ{`qxy@9`F%N7-@8PDU;Z!_kiJm^ndY2n|T%U$)K_Z#vzo=IX`oF~~o=OeBY*f#w#=K -i^^`1I4Ho9%Ecm@6VroU^%~L|m@3Id^a0zWpLxmN_j70zFZMyR(W*RO6l&&f1FSHPJ$tOrGj(Pki&5# -xGB(BffXdP4IgXd~*Qby5=tU^$8yFoomg6*{pjfzHzO&^G=g`-`Z8qJ5B0sYb`|IfTRiIO*3MrOih?H -v7dMMZf0*&?8GsX#wJW0*Ux*z!-Kl|dz+?Ci^*f%eC&jnsa+=|jF~cN>ZG`7UB^tC&^u=8gl;psdz&W2OiYN2ojPr#Yj0@EWa>L@%Jiw~vxv -^t+Fz!%d)q5KU`~x4Gkr?Jw5OfNI5Q=7@^thPJ2qlU!i50Kk -t~Sp%Z6JdMb8`w`qDp;23@pteXzJ@%&m4!%&looxG&y_xw=`KQU;)tol -Jv6ls`Tpgn)KRqk>QnL%J9jE$cW5{&PdF#W~5};G8`GD85J2-88sOq(=*eQY0k7{Mr1~3CT3bQQ!;It -j?B``ip;9anoN=9nPti{XIZi$vZAvRv#ePuS+*=kR%up6R#jF_mdN(ZHf5W$E!h#-(b}OUkq6CF -iB&rRLf4?0F@5rFms}6?v6;Re9BUHF+NSUim)x=6wHrOMXaxX+c>*ML}gjRY7$@O+jseDD)`wEc7Zg7 -5WsK3;hc%g&~Czg^`75yhan10Hyh5`4#z<`BnMV`8D~q`J%w1z_Y-sz*OKSK4xP -VSHg?VN#*BFu5?LFtyNDXfJdWmK2s2mK9bMRu)zjRu|S3iXx99&mylPQ;|=RxyZlBQWR1YQ50DeT@+u -GSd>&`ElMs*DM~G}71@g%MI}Y0MP)@5MU_QWMb$+$MYTnu*rV99*sIu7>{Dzm_Aj;+hZIK?^WEw6oGY -O}Ng2r*sTuZ+l8my9%8cra+6<3OuS}mz|ICog$jtc6q|D^Z)J%J3NoHAQWoC6|ZKg+-SC&tfe^y9VWL -A7uQdV+SYL-2#B&#f|GOIePHp?U1E88dAKRYBlGCMvyDLXkkHQSzDl3kWvnO&V-o9&U~mE)7+pA(W3n -G>Irl#`s3nq$u?$tlaJ%&E?)&GE2%*?sK(_7HodJ>H&VPqwFG@-4BK*(>eU_FB6~u2-&4u77SwZe(tJ -Zc=V?ZfdSQwH-!&nwR-&p$6DFETG4T9FKmpgxpA7pn7WotoeeEr`sI&rixv&QHy^=a)d -fm5^<1z6Yf01Br$dL>9ysBo!nVq(ZhOkZUDmT3g@&Y5G8tA&_D`B$y28*&(?yNUgfCw$KCe@`0>EAg6 -f9C>ipxLpEiQOEqNT0eSd979o&Bd~s57a&c<0y||>fthlncy116I@pR+6TZMmI|sZzS*3B55xXCk04iO`i4=!pY5QUU#_f -o^z0FU-)1h~ntt#A0i4O0lijQCwPFQCwAALpn@u`ny?eo;H)sY_r%RY|*wvo7I+r@Y`W4wN==vY&ABK -=9y+nGpAY7BGRJM64R_{DQUJeM_OrGMOsx_O`1seOgE*Q(=F){>Cx$l>DKg=bX&S3y)?Z7zN-d)%M*U -foMD0I`oHhD65*#(;G-PyP!;e_HF+Z6Gv5T?WXX@nkIqlbx8|qh+wvXxrSLda@HWEYrq)`iz)8%Hmwh82o28)hB)T+pFY^Sv`jF8f -9DDM)d7LKGqN}PFKOjEGTw^7YvOBR`sD@rW_|Ze{Qv*(U#Kncn4f*<3*Tuk4i{w=hvzt@n!} -O}CY@RTJI`#A^0rIipeq+r{A}lDoM&I8D9aO>rAraND_078Tu${BCxA-+aL5z!Kw3mvBt;zE8D^8JXP -Qi#a9woAhHp%!-A%i9e@C0i)Mj2no5}yS&6-8f=JsLdw)qCOSu`fz=6It`DO~2VNnzP#zgtocJDK%wv -!ESzN@7Rz4)|G{KQil#u%*Mt+SJepY_?SjHOSe_$|&Dn(QWn#p7(uvqb~k@-JFvg_3PSx(jSt`y~myhQ+{_-9%vgHh93+bXDo$=;1u=mvHL>vPaTjMfsNj77}@A=ps4{K -JfSumY!kHk9EFRAt}9O^{qNjJ7K5?{=J4QemfeK}_(%jxJ3U@G4Nbdk(u08jKG{cUwA)9LCAIgl8Mo!)Kuv(7MFl7V8mzsQX`~=CNGP0Q5jDhN+=pF -mDe{M_z-)dsxcphc?en;BER@vyTR;P8v-U2@PfPhOm|ILo1?C=QUNht-~QXU@c}}XcbsnBw8XAYhe~gYl;!Bf?vJX*3U$_ccwHeDSmd;s9N)EyD1U -s5KST3}}*HQR!Jmh!z>0__#z-c$}x0o*g3 -Wv1L?2Fxeh@;Y_LHBh9UI;(7-(uDo7<4CjSOOZER)qMb(HHWK*5N>+zDg!r#DhWZJ%58xl4jMDcL-P9(ED3(jCVS;f%?{YSy?)Fk2v%FqD=A6B}{7V1wEF{FJWO6!(~N6RV3=qdOisgp2 -R^DP(1>Y8_%?`e1iZLJeffUCdA+)>!<1DnA>8*o6_}5crv40fdc+ang}(=dH=TgYx?=^;)QXvw^G -uaI(|pYIEoHimXX?OAxAII&d8XV`_>}ouNZgF$Y4N^++=A}NImCgz)DJT%;fz#z5r#ajLJ1Zw--N%pC -it1c0rK|nBJluto~Ge8{aoB)Gav*&!6Veou~h&V=2R#rFO?^YY`?l`1dV$Pl>IA~jm)v>j-E -rYGJ)Mv=zyH=vjBMaMcYl(wO>F+u|9(;Zdc2P#4y!wAwARp(|~lOwjbANWtLDk6q_7|jrm6yWD+X2@G -}446Q|yT*_GxbBYOsB&*ptoCH8TJr{FSqj8K=3=~kYeCMiux@M2QnE91rC;RN!w3Qu?vzC?21JRPF}& -!iF(gVun8ATIp^_F9E<^QD2Q6^i>(o+*+_WDvX5jIU!gDJ@CZJ;?Dl8ogkQ{vjJR>8w!JO}JDZGg#5z -v7%?GqNkxE=pl}a=G@D>IR-1&1V|C;0Iqpr2DT>DSt?+FUr6BvgvS6&Ed!8d&4^3?T@^gf0FRXOR0oM=3WuQBO6i>a@TzcNcvJ3LPbHoY>mtv&{BoEc1dYS7V7rlp8F*LeO| -98Qei_N7~+piTLQ#To8*ryNzz7$>>cd#)5ECCLw(AQBu?lc??D*5?N*#3q@IYa9{dv9nR(m?0No)&UD -v5_tEI!ur*I=FhhSPGmB3e5h~aDRYPQxEm`x?>B$zcI#}UWoltcoU+lz3eVC51UKy>HsS~0r1AX65q3 -)18?NpFTy6pg|#;jy39H4XH77YP0_Qs!P@~BOrcsi`uyh?faLcQYgB_B -DDTG#{t6XTgyl6Q67V@C6$P*yGJrTN;ydc&cAAbM2rCr@Hw09Gtw}V3$`sC9$xs>>oM!K4du+j5V8K& -q!J)(@RN`@?#H_>;nHMR6NCUv59g#ty`pIcy&MHz2SntMOp8=%mXKP%AY;Q5m$4X;94#JDK2$Un42Vn -5TMZKeLu+j!w0|b1ggh(N>2qtDT&W<`G%M?q{9qq8Rkvm5K|1r-sA#jBch=Vh7@7L;mNjQ1X&`rtIk7 -58+MicnNw>W_}NaamZZ%4{D~=?4bjJ_AM2z|hCge5vls4VR&H{%VDM_k0_NI^NQr|v*M1<5fd -OicslHTUGG{#Z}&P#zuU!=^I3UH!sP9p5}mel~K{zBo|X21|1ABWiigqrV%In4D(GWS^#C&BJ|8z$C| -Oo%@WlwU1K<9pr*xQb}I`qUY?yV$>q3SnZ9VKx$iPy{)S17jrNMZMqLji9dD8v+w9RjAT{hZ=)Zx -==OYUyVQXI2&7}oz-Vu@QkV0+3vkkB)qt)+C0;*>kg=}l;B%G*djR{dOh -iK>=NA1ndHog|pNmxMxkk`_)%dqa+fMoDjAk)k=o8Q`HdWLXOFKU -(;@FNQHZwJWNs=VIP=rAAAD|fnW-uo56Da~aTQnBQLMTZskU`1(2;;mop -B?+2Lx;V(^S|@1w}S_oWzRktd_E6K9AXksNU&Rw+&zQQVmIgkqYHWYA(t$1JSW39T(P_6Z;saX-i=JD -nY1S3rw-hVkEwi5h-`Z?3L87IqX1E3)rXw%g**n(UJp#NoXxmJL|ed?(J#DrzAy$Z@l -25y#e`h>diH&rO7#i}&;y?14Dh0^3id%IEew!_sE46zz?}A++A|Bb}cX~h<}~xH!zDp1?WM -j>ry|#Sw(iiD3yk#tXOI{AabFuDW}ns4?HR0a#*EY<+nH|XSwJAq~6#@fU%dLBq5feJ^}De9&DmcKYP -ExdP_dc@Jg5i`FqCrCk~E(f-!zBU*P!tIUf`Wbr~ao@1s$jq)|0Gfoc!0P)I+a#{g!u}Wa8)%mzVWAaOao&&eIaQ5%(Z`avNlV*G{cNP!r<~G(D_2ep$kt-m$0;pIupYWkZ@}7pdRyRMP#qs;{)a7$frjrjdpeO`6X_Rmw5R@F7HGfZR4;DJFBjP -?i5pZT31dLHa`KSa&{*4sV{n7hoblSa3412LUma9J-46 -HB<;YCs6(fVtT$KW%K%Vx2k9kc@8VUPuo(Rkj@F1H}w)LRy0htr0`a3@`NafuCTQ{p!SmhPG%8GGF9; -2{sCKMhJd1VsvN=4*VfpmmqrMy5l@CzqQsF2(3bAP*H1h%YRj#Z&N5M2OZ6lz%eFiQHfx~0COlG2}4R -XPU{s9mZ#iNf&8=~6@#BzNP=G^xHvr+=`1-973{&^sxo8+qegV7%q2EvdVA!v+|k?mvxszkVc6LW}96 -k8oL9?3x8RwvZmMtFMfWZPw`32+1%L#uUJW7_+b##qTvR3%{O2NMJaLa{Cc2{2>zwvzDPQ7#p7yDKfX -GrU2UtSk{JZXs%W=dlME&e2svVX9WE -ZoO#mE>Qlfv^{l?bn6ive65#i8E7y2`h+(>>6@LEL4uejr+C<0mzE-uZUj{&i=@y2Ay^8pja=ep -BueuA{_>SJZ>U!;5o%HS;&XrP5oVq6vb}zHoA84}q;~^`eIL%~67}=Dpt`9mZ6a&-1O8qj<*1T572j2 -QP@Uqp^hi0a(ce-rc<*J6KDS!6#0%5ngkiW3J((`DZC-Xv>^rIf!rkFvCwzT9o}6P^8s54Od4<`>pn8 -Pp<;34ZlU}1aAKMSS*tq$F%1xzTmzM%o&QYUJ>Ig>dYS7P0K_?(u4r<_vy_bkMuv*D$;Je4zeh7vz$`SM=<{&ob`Phox;=X+94lniH;pWS>#+jfIqy*$t4LNJ -4$%yl%IWyz_JK|Gb9j@J`{v3*h|HzRox~jJ9V{CCq^Pa&ih@!w-fs}1v&TL1l~P -Mtr`{YtL`A)J^BeJ_HjALA6e{Lq+Wvk@131hD1^RmsBJ3d@fEv5=^v0MOXVFzHBMSV;(e;fY*Id(C@t -+Q^;t%?0y!Mx0t!70ZtO19u7L<@y$dq+&dOgGn2fcI#abM|D1ZQBP-~>rK1)VJub5vgk5HQ(@xI=Pec -h|WhP?uN^5;>Axr*6tL!>Ch#9i|SMT+v#hDdP?Q5cF8Q*a+6vs?jlX(O6ik4Nc3-3Jx!)&L4j-g-ot@ -U#Lrd#n9&5cliagt}x&yNncYe!ysj$gbvkSflQk{Vu9%V8*OM9lQ|g8il%DlNw`8InKW~??5r@03oiJ -q?HpR;{$qTBnhEEF_`Vok>FkDV>Y}_)7}NPyTV&AM$Yo0tmbhy03!!1rbnnts_;Txhk-1OMWseunA-! ->sO6kr&U~?Q0V)S~V*>3O>ix#xGwftvIL#+`nutZcxJ+S?bEiR~v(!rmhxFzM4o~+Yk-qT`czDW&boc -3GrQ8N$o5*&1BD2iu!}sGlg&J0MvOBZ-w%H(iAT4KwjWtIn;@OcKKqW^so7guwOYObeEb5)>g*JFj&*2G4Ur2ck+$>eShOd=7kU9z;sI?+O%XskNg -SCoZ^^jZ-fy6uN@>SGHCBi8N*{6pL_P=#T=MOijE9Ww))!1`qid&&ho+tfm{=~if+T27Xs-kiRO=el! -Lo2b_Wd{JsBHy3c3hwDcCoEYOuog=(IF+9Ft1H-uRK32W<1R^WgH>uDwj-Bw}f$Jv>?tf5b;Ri3Ic%k -#s5`^+@pt(q5BgI`FAk~PPPXfQ!q5j!RR1;vOcUZh>g*P4jZ+IvUeJ)!iplu(wc-2`XMF+a!uftb!1{ -Uk)Cx^6nyO0WMsYQ`wSJ>O-XT_4Ad0{UW0}gsm)ZaxesaGxopz`U(U8_JI_jc4~;V-U6*xR9v#FG(_! -D@;)61xOnV&3H1EX{7bTx`5d2DB^`Ea -mRerVy^;1*up%Rr$bmV>q61)C^K&2XlQcC`%Lg=@jhK ->-`=LX_L@BH~DX;jP-SwBe+uPVSC*T!2!7=Kjh{wD8881g9n=D-kj0xEsa0viON{+j~SW#5OjVGG(&cd ->nsO1r^vdn!{@5k+K0xcs%$9l{j|^lIZZijmaWR^q`*gN@F?t853u?Bk;Z+Eo3;z@l8dDzfzrXxF-r@nyC?v^LVKuU!&WA)5{z@-oMn+g&}Qb5A8Y3&lTNDf`LyDgvUgJ|&e -iYTK=Shr!0Q01t~lMIdh8?p2(U2^KhPLKeA@;k(x_IaceFf=X_FuTW*mVRp2Q3s -ejT?znX$dijnHn%(x*c*gII%&s8F$s40K=a^AzF8%b&FAg<7x*9LR}kEYembYfP%n*}s< -0N%V#KWoFO9f~}|5X2(cu-}HMzs?vO=adX=g>xakoK{h&GXWY)Hv5!@qzdF#BzUS_b7fr_!z)bbh3IV@4b(TcEq?E!r);VwNJ`s4lvmHU#TeJ0Ia&@+=dlFuCXLkp$WWg%dW%NcFGj@qXM4w}7WFgMJjsDar#`Cap((^lO8XU8lz=**B1+?N -6X&MmuD=QD1y79<*71xl;$UpLamZm&&beJL$Om`*^W5*5spEV&LGW^d!tGWd*wO4k+~~)(m}eNWt^{a -3eh11XUKQqqAt?FI8qbCzwr1vQ?dSwaK(;Jn8GMjA`zMOog9im&dZ6$mUs-Fe_U7sn5r=emn(!+Li-= -sO4AFe5P$0hc7O<63>!$?W8?=?TxTU-+Y|zpNHDi*B>+Tq7qI$k%M(UY9yhlg8EsGajL8?no3(S6;^C -d4pIZ_O_R(ff7p8SB*YC4CtO(ODEL)peu;C6GyyOL7FdiGuY@0MH0xK^xC%`H8zkQYDZAC>@VkL8utc -^b+_pnqx|~~aM4v=4Y&R{pRCBnNpQ!yL8xQ_-cG3~-8q^L!d!?l5n}_OMG*x-GfuA^*NYG}9NfxCRk1 -L-Jjv^jtEKHVWXv09{BVwJA&FcYK%#VA^gG2|agUzQvUD~A*3%aNSVXF<>j2MqfgAMhTSe#kErdq5EQ -PBtaH}L$6AhpGMJtPiEHZ`)Z(L0dm4a2*&a?R7G@)eJD2Y{x}eDVg=tWJOtqn=xR{&9XVzL`(n9^G*lB( -pimBH8^=UG3^KHoS$UWp^g~%Zq7Br(nVqpXEJ2BH%s7S#(be@|lob2l21Tjw7i<0iXa7129{EMmCIz& -b%YQq!s71yy}$Qd7TBLpS(_r%(XEQ#@0}k%FJ+iGD8QkyV33*y&_Bd0z`021M|1o0WYSoWi1(+#REJZ -%$vkMcHgjKzN9xvjR%t@%O|YpTbQA$0zPXirh7w2WUI%WBTvU0_tG=+6vb@) -_@YLa+%^k+uC6v1VPXa`Mh~tK~<hKfy~rn{wBThOO1nM!3 -Krk|O==XOuduhJPzWJ}DeZ;P^^~hiTLZ~SfhU!(29z()DD6qY=C4z=N~kFCAXFlz>sq-IohQ##qC@2w -(Bu`^_Gsl~iVzwLFw)mz0@M(=9$Fcy6?kE_bG)oo*xWl*%fY8?dir{(mTlzBfSiIR2%EpkSYZ{}ek~a -i5=>KsP%>2OMFrm$B)7?fGYxxSHGr#FZz0%!TU{rUq!{`M@w`nbiJ2=D_e>$QlD~;p;3%XDtR_x_lfF -QeG_7VDbYr-78)TVAS!_m@Pd<-lu|t+&S`K8nl(M86S^h~`giso9Y$#;2pVJjjL7ouemZ5%BBkl&47< -MX}h*hH+Kv}`Z@jhhHttUpoh6Z#P4rGPB<2~Vk%^waxLp$JNhrcOY$p)-*C2~Tmi2u0=d>edhZr*{&9 -JBiRbY%Q=RC2p#-ce*W2@y1xm&*G9?Ouw8%F2u$lUyNzG~HX6r3B{p6=s=03JUlmv(GC1_LcU?T)Vm^ -m#ZuPOk;OMuR$z7@)&T{-}@37!|eC2!IP!r<5&l(t~=n>C-6$MN(gOwOLR31a#Ja83w8xo(n{8`*56= -N;f#Ldf9B)K%I22@#N>B15eG0qVwq1-YZ)iiv`?w6rP!9@I&IQiP{9KJwjaytj;-5}N@q*do$;rUjY2 -j&yQZ8*g~}G-uk2)*$r-IV<8@9!s;|awH6`$x23-zkM#q{>O@lJZeAFOD-f-DZlSx#Ml?d7lYs@4HPx -ndO0v7G04$O-WxIK{5DoqFpa$cuc|1)gyuh((3RVhWhAY~WZxuGEvZ{6Xgk-TCfi8xHZBV^6Qr%F+`Iu@V4Amcrg@-SJ8z -5@6P^JN~QDL<5j?M|C5ujyEIWAH=)%$hgy-?03={i9iSvja&_D3=wu$a-Ti0$)43T^H*IY)2sSvr*dX;sG};t{2P*v -gdpOyoXl8a+_3o&@fbATZz3&?G%CGEZZ&!S}s5mYW?90fuY^8pZ)4X|VJ2aD2Zm;r-pQ_ao`Oao+sCd -S61Gym%gvdS;D9ICG5Src-jVKT=|c1#?4}#BfsFP~yAnjcN{ep*>YzLor&v!i4yduTZNt5_g5~jM7lV -gTRD5E6jA32lCl{Esaj=e~r;>MxG6WnR)#QxND8ePP%y%Zq$)8NmBaP;~e!oN4$SGPJctLQz6gADd9wn#V^S&3(MfN7j(2-9?P2a`@y4AXR2_@UhUIO(gT@ce4n^c#GvqnI7VbCD8Q0olX4;ZjDz2 -`&k7+9)`vGe6XIbq$aoke*?;*Q1i-N_8nD7#vLh8GLuZlfU3qIbCAjmz0<2JQCHD6Vebtd8_dlXqQ*0 -nG^_s!v5kWR1$e2_q3wS?FSSS>J@6YqRFpNx1&PLX{vBHu5sa -I3pF-ErRHF-<7DF%Hjlz#@&`%-gvLK5m3c+UN*!qivphP!bA^3N?nJolgr<*&4U@h^3r9$xWB)qv7IH -vvwgy7HVeYp?>JY^!qyN{*#lx7N+0M*IwqyA0=NRY#b^@=5J6RT26ICXDyQ6ETSsE+2hvNe1N(%>b0j}o~u#_JqZY3?A=}{1ii_ -8C^kXKfZGam03j+vuSi5V}BOFYE7FyiLg&Td`VD&_l?{uM%S4Uo1?0przv%(sd^gqq+@M0`{8!UDtwj -;rt@di}5_EkkbyKGH7R0LLW(we68|nj{VhwNP}cokPoEf!I>q?ILMO#DC#7^9Da({iGQ2IV+Tg+atGk -;_==hoYDuia*@;@nBz7hbNUyW&&O_x1Bd#-Qe#4e;8(!vcq8{ZaVe}7;TyE1@C~s^j;^=xNfbAw#hXT -3#(HGAJfUuRNHK?ZtpLFxqpAlVu&OcC5JHQbS}bh6rC%Elm;^2kN=qp&hx=qNyQP0HVwUT8;H@9P@ZHfbl!g~ZzR_E6h8E8?(;cr4T41M -Jdw)Xy3svp}jVSes;Nt_6VKrM*Ks<46c=K)8T)(3@Z#yrD0I;F&Wh3)BObY?5vH79pr1!q#`eHmS8lK -Zb|EunLbm8W0UQ{4Kh2xlf8)U#10|dlIV@-TKh7)4ivrH&9YFRgLMKtE{c -B66O-f-_0CvgWYwpLDYPLlWQmeJP)(U1IJZ^aoIX!ZAC$-l45zba)>_PVpfkjbS%?z@^~Q-bKs>y_;~ -F$~S}}`|lT986zsUgyW!jWW`VJ~RARZPxJKw_sw)`ysfV;jF~v4MRg_MX01-Lr(_+;TP4u -y!(OEAYsBF}JB*3Dx;5XK@$xuG>sK*QU}%*{h?4y*81$K&`(=zE;@H-6BG#PcO8*kMXwx!6N^%wvYNdar5E_%j(Mx( -%K>e;*V54_|p-8;(MMkvOOq<^xJz_>2axmI$Hc(Q9sN`QC7zLMAN!L=5WJ-^sD;&PeGhSX3fBCHt{Vl -w#M;Z*Rq1p@|ABjII__#d&DB~j{)`)bFr#uWP5z)yX?ltpH3&9ey_W~w?7Kqjc2EQkUa@F>m<5L+ufJ -b_wa`uYARFR`Y8Z>P%+@?q*P>ZRw>qH<>TNBCHijx^xBXv;P0o>N?6gKIJ?xp@>g;$Ck5hhd!oEa=u_ -)SiTlKZ+aCHfNT5aA}0QBC!Fpk7Q@OnlWucawaR(mzSQW>XGdwMm<%@tctZ&hAFOQujioHfj5;!hd=NM80C4uhBsZ8!oCOxrT#_Bj7WbtBJ59A0R8udaLY=M#@u{E}g -_hA1Y23@S>4v)d7U{o0jlC82>N| -&bxCve7(a1OdZ;76eyA#f9e0T_W3bhQ`gO-TymKB1!<`}ZO+^SbwAPH}t^{RKT%!2k#r< -qNW5*x^sx?}ENxvVu!nfa?>sHz=r!Oqt-WX^d3JsBL?l%}m-YsZOOZ%6y4WC~u1_wgHKQAo -z7t+t>;#sdBgSo6=1Y-RS8?S=HCJvd{MHon8zmX!DDx#7?u8%z1zoGnTuQ@u2zVUfcU9J!^#*9v-Bfj -Ay~lM|GjIz|G~~QXn<{L)+w2iGHhA3IR*n*LfC4q#8u2Hi8>soOPW6{AvgS-$pBiikyv=UOAXNu0HHs -~{?3QE@@A>IPs1g8$wBp<|)2X&SO1hRp6J%p|Wk6cK6!BJu%ed416hHZ!vZlN?JZV&ax8x$(L&(>hTS -6q86r<2%ONi!G`gu+%XTLx4LM3?v-IndoX%>4eW_d{U5c_&h+Cgn7JSNHZ>LE^Xy~lR2wgx9lOKq;RV10PvnyhrSIb`t7O~*m0GH{^i|WT>nzd -^)FdIwgNW6Q&W8G;DV<38J|;qjeL;$W1bdQodjSWAu06@D4U}_`Sa+!p)K2liVXF>Y&wp8kvnwY47sy -U_d&*42X=m%7mS-1D`gR0yfoo@`@!pU*`u-z7!<5WxSah+ -WJe3JAXg=kiedaX`%6;!TFekc_&^67!Vs_^CYsY~Ugh-J!0;C9mGPVtAgA_@a{ -5eQBx-`3Emphnjuj?rZ^dnLp}&R)gWEI{m1zTM(MqG1Z61DhF6UdP<=!fEgIV=%^$VV -5aSX&5Qovnh%KjKe54rd?9!l5>d-;pXD&BJCu{ws%VX_o<)fi1&$g$O7}2@CZVofcP#%C-+y14xVq2@ -oULP)k+_2rZyS$_N5m{@0eS+hO%C6m;VD?`ath!7p@|<4UY1|xx=fyyUuu@82pjW-jgr}Ya-}1u9qPO -x8#+{-mh7{<%)vE@Tw&f&mYFmE;A;LxzA@+9S(WRUW|$&}Oe_Q2S;_|V?sLwk!4?JXRdKW9HoH+0rM -C$i>>Z#SpV$|VaGVKcVk%XtT6I{=L;bc%04V9m!}EVOdESW3$-}zcMae=i08_TfnS^?t;h488PbbFlL83lC=lir(tfws23RoXE2Do6mRohvtMWy2p%KR -hD?2k;|`i;rdqYnuTGm?Dn-MIhT&^Uy;(4$*=zfTTZLg*A+daZ8(X)*Hy)-8wuU=mJb;puo$a@hJZKs -zh4g{{A#n~bpa7kKlYKVnV(qpqVqS2<;U+98$i6zV*4>=j}-D6P5&1e0yrXY4(EUvmmUk120SjT%8t1u>S?50HCO$^QQn5aTc9P>-#M=cWJ;=z}(T)akBuUDue!_hvw!x$ZC;GDh5Y` -|A-J@5d>unQR=sicQY@6QWF%L=TSu#A^61o<9&N}Zo_nha%-u?WT(Yp+zcbFxOcP}&6JtK|xZ)dE39& -;`E2D8Qs{mW{m(3|(M(bZBU^H--!{HT50O4uN$T|K#j(n`$y@eqH+YSfSUeJLIC -SeGC{0#ZCy*@V-8{d(jf>I-I2|ALt#PnB-r6K$P_*$Za($ipQjkYy5%#j1uepR%qWIRrM>mhfgdy=ie -G@3{rV*mEt_qH}kUUvu1F3s*|S@-zHwnBUzgUoC})p>q5x0Q6#bInf6X|ASf*%j#w81Z2H|Du=j -4Hm_JRVc;`BEUa$HQEo4`WmwB)U2vAm>ml>-5i{3wvk6O+qvQJw-5*eBZYanFjKt5N!i@Y$pc!oztB= -94xshl5)9hm^rsZajfANctc>{pg~*|&Jaqsy7P<2a-%nIj>A92=w04IezmGC=PXxyM;wYd}`028BN50 -lTS1M9UUaOAQVK@0x2+&@F1N>B9>YC_O|45TSCesc8i9WeP{JM&h5KK|YAz)9dhC4P3AZ3OrchGxV_=dBp^C5j}CMUkx!)cWm=b -n*0hN9zNth;07q9+GDHu@h~bLTSYksWzH~q%D89#0k*6>7HXN9Pt8)*U1x~ZiTi2ea*+c{%ygc7P%7N5Lhz3?tHs?aqmD1=R-PJZ3S2JK%?H_NYXwS -1GDq_mB1d?Qcr4xV-Z2kN4CP!s6$iuieLW;Chb!1dXzukVq8C(04%evVI9(IzOmR<#GvO$?S>m)L`LF7VPtSDa-Ow~@FbP_|A388lg17| -K4P@VqxOT@KvGfia1U*>ObqPHdVw%;(`+R{N;;CZI1`6ezz+B)6b6Mu%}4;nj3G^=!^DCSQ6#8v?Pn+qSlewl -cSm^J%LHTholYO(C4ziXU7g02_6HNwOF(J`K0g{ed(w>ZC9JlV4x+S17m-KV9Ck`EuRV#ET70UBU(ZB -vTzT}W{#E)U}&+tHdH7@=g_O&hL*^~wnYmD;{NHdxBeTCt9Ge{CW=Exro~u^I)ax2QRALJQTQ0v;ZBo ->l1sc@=^s^fG!M-^vJospfDAY2vM8KQ+YyLl$D2C)JlwG0@9Vs1W=bb=S99)Mu6=sx@ --~4HKSNN_xdiW#-;)f~7Lj0h~b@GFVS6-n1`zq1*o)X+bf}`uv;N;2sPj^ic;i(T0-RNIEf{_BCdtSV -|S)>!ST+J*qo~zJQ}%iU3&+Hp^l-f0nty3JYuh?#e?hsJ(={M&`!CDq#dbY)5w_zkrqhkU#-NeOEnK*defRgQ=w$UoqcsrHc9fOra -T7?|+0~L>|o_=koc;R%X8OF+d0ij0ha+GT3w6R{p`WMtBXhXZsW+oTkC}3@o$b6cVOmzv48=PG@;&_d -aOgGH9C;(^;=^Wf?w?HHSCwMaE%yVa_(=@j&F!R#=Y-7F-Gd@ebnFP;0X^iIn<_ZLnfi6FMsF31{AfP -wtKtUf5hiB<0pVy`p87Zc6int1NJLwJEK6)^vQD?WwIQg3UA;3n<%4rO2&)yc&$swS#4Tn`$;ycnW0K -gF^6HV))T=0QVOv)%@kow@erbHnJqS*z!d|mf+8F?-C+Epb^Y`4+>N75tbfI;`6bDIbNhtcD2+@IMwo -QnBuyDl(<5aA|<@T)el}>eNLxy(-JzIdSPeVm}559qO{oDZ?CDUKgbu7-<5On1ba@G02IL -3CUa!e4--{PpV5pB}xO7M7FKGvPrSfk8$&SRXzeKT3cPL*>M2}Eu>5)>&a%H2BCA%<+gt*>|Y?Qs$Yt -tRy{(@@;|)Gy*)xT3&137UQKQY4Z%-=4^eyaUdNRL=P$cDXD7Ov>T-(S0?5*(oF2JmI%IysN(Re4++caabHv^~#}kjLm;Ie=oPS^?8|ULDy-E8q7q+^ -QWoWs;Jo$pS&|(%Y+WRicjHJNLePR~w3;B9hkh9tz($`K?|A`6>cBdU3G+Wi-%SnS;2!B;B0okEkKSn&){R7yc9g>Xznmze9tnvX^HYg<*Yd{T)iF*VcjkAB)u7$cWL9ip(BA&kR=ILaSVSc3Lc%J2Q!Ce!8%5M -2GyQHxvVK#iI!osNN%!*IBOO>1AMJYD2gwF`y#_)AF&Q)j;3)tuF6TABSH#N`TJY=S^vz18Y(qMs~-Xxirkj1%HPKLp3OaoE>BAq; -H{>ap<NxPM?fpR6%{bGr=05lnmNbU@1}WvWpiDOfx9c-+syi?IBbz|Ff>E*9?~J -)iQnIva=>FMVb>`(3sTk{#2w8j>7ClG{QVDN -$d$4&=uvszB(GSX6-}iBSa>#iIb!8c_w7saN5IVN?NRC{-ssg|oD^>gW#gnw{}TVn~4{i6I3BD?`4f* -)vf$B}Nq3-%j-g^3>gMhYg)FZP;}{cN8+LS%)u;_{N@zAnFIpH$xJpR~_c-UP_ePN6>}n`D_|>lH16oFQx!ElPKf^t|8 -F>d)KhV{M#VOncpkeOk;!=*LbBsu)IRi3BsA^L?9q<-pQ*u~Y1*H`KN^9fb?ngZP~fjNn9(vKPVJ8u7 -L9H2ggeYekk|>yJ4`JeP!$-hg44A9*;44rV!n+gK=Ps=wP4hl$Vf^xj6ZJSO~8>KY~IDps4dE#pI@;BoS7xGYb%$tMG7U{qaUow+0ARTi4Pn8LsE{F>cMo{0KgIq?=QrPC -R%+Lrca_Oz%B3tOl+A$us1B(B=9M2E5yOlO4^dNdOtU5-onxqU2$P?&`3qa`{Ge9=Q_(vqqw1GU6u}w ->bz4XopFiz3tlt<@vvb_{{#`^>=N53uvzo8?dIrjA)S8lCY8}%=OCJavmS=kPC95wAeD};7HPGKWz+j -wM%WrMK6d7KN@n>G5Bep}U@!p2VDCbRL+C%m~2k_)RQNPX`spQg$w{+vcBY?myp#jMq*bl>c2jHUz%_ -(Li@Sma?96%`vc;K7&DXu}wb-=ZCITJ10+T3~OJFtN~tnc%;#<|KYbXGJdN58n>E;dVQeD}-wo{B=Wl -xTx^g1AqO2F70QAKaS8e4E~@5{O}@h(}h1&2(Bvr^x#i_prfZc!)1!jJC8Ss1-s?pcBNf$1oJL%xzQ1 -*wH32oRfHe{z^jEq2o-v-CIRtpb%w>PC`NWsTFr3FK`T{VQ;DYl5)%Q`&3I0M9vdDu+acfL2E3T~gy|tGdmG4Riqpn^?T`K& -js{{$nlGAIV+ -DPPQm2SJzU>`C&OL^E9i{x@%oKQeWb~@&`Gr`G+2z04b-YBxJ7H>C-w&4YzBW@p~o@&$eJUyJR?rV!} -_UMijAYeL>uG-(jo!dG9QjefK&uu>ZtluBWJXovizDC`ud^vFr`{7CF;~-)z?JZsJnQnX4ECiQ9-kQ@ -$#Gf3kJ_j{J1C?#Db&W1e!O8jxJ#ueK!KD;J*QXGXl=peoFPok()}vvKH|dDjaaI(z?D5n-wn%T2M*< -wDv!=d|SIzRP%E19YJEko%o}b^RiS8~@nAHUcBiMtI4h3W#r}nkRSSK}u`ap*o-5B$@Sjy|9*jVk$P; -HZ$QH$le=CB|Q3D9nCVeG-j_KWq%v-Ns%0p5eZg4^}}+#%wPtwQieCepVDF=U%qJ2X~CRCem3jF -0(hXsLxaD$sOVi9+4l6Wq=9_paS{!&;hx)HZ5Mx@`0ppgChM{q(H0&S>mdMGB;wGY$!8kunaG75{hD( -8)M7jj}(9CK_N6_I%)8A$HXw#deovdU-|qGfi{TgP!8jCN{)uOms3@8-RtRF$CGp -1XawZv;`t%*q-Gl+Xs-(C2-Rg?bzKGY1vAt3-X1})9&(tXHRv1}-Y-GUZmEzEhSOxkHV~p-v{?!HDvF -ZDCS5ffvhw=k3uBqSG^~6C*^4RL9OVi|up4v&Vn%8?vYdzD4Xl|eT<3KvT4p^HlX<7~djA$?qJ9YJ!k -D5&Q5^8-rr0cSPJKov~w@??1$?twl6`sj}ZJE@o3PO`EB~LS%G=E*`Ey}o`Yd5f(2P$Mji+}9LOsSARd(s -o0Mx2CxVJ%_<=Z4nm$u;C>2O0+wE)-3siZZ-)>~q8f~lVA16LvFt3xoWZ>uD%Q+iFe1L&s9lFC7A|bv8I*r}k36;5<*n(~l-X06;`mIK13x -#J}yq3?yYjUONAJ9U$b2DyEIkQ4681-)fauEUKLxTWGUxQHk0Vc1)lMB(kUA7xhXc6+F+#$mYLUzLd0 -_Io%%%q9}dteB>PLBQ$Y6t)}Ke`Y$yEtr8AS0;B0M6fp2qcH=6@ -^Sk6i{1nG}Z&$1>1l9H4_Y)oqVr6UR<=&|I5sK~c@&Ri_c!YKXES1!<4QT-&K#RKfVSueP-g*kOPA!e -nGGb}Ro*DeU2*C9s;TdFZv`a4v*BN|D`4n={h6O}0D0ZKaZ9d@7e)(Hi-qQo3;Q5d>Y4_-kVG7b%E8y -$dO)*NOmeDQ7R)~scQ4D%4kjy^O{sHju0KAG`c@~fPwXtem6VaHA!J@y|#)T7Qd+DGzzbIGTp#7G`i1NF=)2Scj2CyaE+>Z$to!Oa!o^+GM>=R~E+L&yE2(Y?J4{^qIKFY3Bl))4cLeu?`>&uNHHd)L;OfAsU@AH5;LK -YDX2f>*oVTa;A@Y|H$0AZLVLF>$kt8~7)v?0X@PyG5RYoa-cic>`p;avesiZWWd8i5Nh(cxgjvu_f4s -8(720x{zhaI&_vR%wHFnilrxD=`j>@b{htoB}oZ`&Hk*Bw*9R9YWYg_XItalzT`W)EwYxVCDrgD_PCZ -H9JF}XJ7b>R#r+hKS8(AGWNY`V~u8(# -e^%>*(8cjR8g9l38$AQ{VdgCe^j@*;5WoS_8Uo8B11&O2F9a|KFXi!N`-0zwUC+ -^G4h7)%--P|e5{~pF;HI_XIsSexAemkeX+FG3z-)*MDwAKkhTib#A-lC${uu?ZV=H -?TnJA{qSqcYAB~p9{G~0V*L!$vr_%ojXC=u``pb|Guc8IQT1LWe+zEO#cY?N4T7nZaJtz75&$eOyll4QMo*(|-Y!<3c{osnP(s}+FuI0S)#xZPti5c -|^?Au>XrDxWtgxEs_N-xC0ME;Gj*D*3&AUzH_6$4y4EK1h*B#TJA~V5i4}QK1ou2&|wzV16fN<-P*xM -Kj2XfS-*YjD!>v;nC`+CTy`3JjENgw?l)-%7F>>#M)F`ws!^u8_eeYJZUxjP?;xjP@YD(*{45z7kZxy -pnP`l8bMO~=);tw=?Xn+D6&yTdHo@gXM`8^Ny)gVZFMvN$!BL6V1j1EAVUPtIbfH0LYG67zkAF^cS+z -c|$m_|ZG&eJSqyTmr9RdgaUSpdQ%dEci`6w`i_9zn{E>C7O(R^1~s0ZMnpbd+3ui&o+>eQSqzX7GH12 -Z_%jVd&t-gfveg4S3%I6LTzJwT%!XKDo3rqc013k2j0-8h^!Bt&kHfQpwV02QR8Ei>QlETIzV579hS! -p*33uyJN%#DBSr*XY`LmxB#YQTHuB$}KP$kT%6wuGMm5^b*6G>QT_DS+xB|=!dA<=gg83#1XVL# -(OI8H>~HQSNFNc3F=`0&a3@Vi8w3ct%x+&;51@FF2#?Rl{}e_0?6NFzRHXtjVD?AkN%>!gra;(CsjW? -Ua|h}Emi*T#LTj|kCH$S@<1zqls;;)Fn|(iw||;0<^M}ur2Wlo1k`ZNxJXy&rAnF9x%&pF8L{{a^TX-0Cw|1e}K7 -bj(|N1AR7E=kYWY40(a>(p$F2-KCdP2E$$Y!B{op3~y=V&d^8NhcA>p$$ed#fMF`hIT~u=&e5gOsl1? -57U6diY_#e#Ir63OJ53$|zqvvvmDga44xHsxMNjY>bM!EMTzB)4)aYkCDJhD9*|1Bi;*Qa!_~ZAoL$T -3tvi@hts9w7<&R;eDL)Q!Q1J6f)gP#RCPlsCmK93EyOTA(NS62t@YTrNNYJA}0?*IfwfzioiX532DRb -xJz?)XuF8)PA}#W=E1PTNfm7fr}d9)uc=?*$lN_o-fNvz?8Fl08=sS;qB{F*e$et~~5BL%PaRtD${q -)mRemHLzb+|8v|A)UDuNI*q71%le{9-8S;uyhw{p^$hZ7&JRY-yTpmq6 -pG?wW^bMam=-bgBRZ_F@w^ebM)-+4xZ#Wk(XU9dIb_D*_`o-AocLzE^cTDehgj8$)PXP$h6d$TMfmsP -I3>n|g4g>Hi|$R3(o$#pNlhxdZ8>jZK-yLtN?810conCgz%XT87NAZM*o -4SM?nin)Ed-cd|_jn@rUlNZr)hX2WUnJ7h`X3pQrZ%1vX{u3*WB@&qQ{-C>5vACOJ=JeKzhYcB7+0@xy_}0Kp_21sPYtKoQ`f~}PwkG0Ew`Huv1M~iZ261 -Xh&|P4GGb3XWyGGEOQOrKW1`EYX4Ky;b9v&inwANn1u3yuO;=mvv6^~~SWRVdekAI!UKq2gASUCzpbZm{pHZZ7 -7QT2+ego`J;rO9X}#v#IAbHb@A9$`=f^j&TTXngi53XLyAH}BZ;*vUkd`l;#eH2ht7*bwV2{ip*Doap --RWH%Lao`dD+?M(*qYqprY6Uu`pDu2T5kxIZ>#j=nFBaDt}-w3RO!y3RRlfkgm3y5~5K3&XBMkAPMXB -#3)p#2#rUfI+27yQ{L1hm{LA+oYirS*!VK!oBwSfs@Kn65QvJ)R)`F?P-v;hBgky6UW0L{@M{UKGL*G -&dhgek4o0H7opO89ztsHa1fshAlDODaokD?83KN1+rGoU83ev$clD;;a6O4)~6#tcbt|0dRO+>087?H -|7Aj}=WFh-RZ3rKa2K<2!tKxPqR64~1n^k*<6l@tp}n_A*@ -#E=9IT_no*%CIpuxMx&AvqETJF)$|?FsGfby^#2-->ev<(y5>;@-x-Z+2W7fCG -1JAPQO)4Mgm}ryaUo*G=%G^=M5QW=o;r0wRH`x_mC8?1sr>P%R3w{SEGiWVSo_sK^pRvX7?r9lK{ETO -kD9qiR4SwNl0FQMeB?VrQY|_sB-M=*E*g?*5f4fAIq;-|A*ra_+BCW-JJF%OP@5F*C^bJQm0j0Iq#I+ --hB{X*q_9-?42GpLI{Q{H9?&Ab%hQk{Xa}%w|NVx-N--ea(nzr8WM?P*u -a?p{i29D^%6jW+PP97iJ!+s>}SJLRD=v|C>-%zc8N@s%pLYoKRIyna>GTh1lubP*n@f38AX)Fvmhwq2 -kR$TI%ydRgvaIb)BF^i0+MtshV=JFjW##5p)7!1CSz-0mives-$R_DITRtitaM~+bC5efua609;K=@i -pMK)K`b7ns+7K>^M4hk>TZfsbvs3=x`m=t74j%m1r((!FBYO|S)vg386O#ED(nbK07GN|hHT<_A*%Xu -)UPDwhnT)RvfAin -*n%BTB|LU+|oG5?l{FlmKL7l1uE^$K(EZs`&S{}8dE)tB8y4!)&;L}fL{W$K;%9jq@Uy -&)ax;C>ts$pTo9$tHL6X>Pwo^FhDfCqq^*49Ynds%JFW$?-d=M#w;5>5321qqVA1y+us9}I!^G{h8CO -F3~LP=x^X|vExcA*jftIf~$c?vS$0vZxn#pmObKg8!_*&mGg82k!xd_Md+od9pi!!I0m+J(c8e~s7}_ -Kw)Fjd07`r$jwuKGrDI*(LfiOlRO*3EuDPm-NLJpx-bfy1d7WzA9r(Oz0y(r&qk$i -BS5*n;Hy+m-2@XJh2F7BCd$mac#JfY=|8KsH4Ki%DT#r=bbD+m4re)KS7NQ8HerMM{+@3fX6*9dm@4`N;Fm_6*O(TvH(!Qg#z%%7v*U$^97>Yi;CB2VBM({3r{&^1Gm=e+| -h0=cU7?PlJe$1T9oF4&S(<{it@p;TL<^(Z4A!MI-i-Uzo{n^5dQ0-53@+?3 -*B-vYCWkOP`RYp3wt}UUk?ygDT~aK_DB8O -mKeI~Gy&3{1SKg$+a^WaVclmZT~&!mqg^_5GWVi`zvd62(*(zVXSR!`j5n2cd -Cr~ODA1c(lGe4B&hJM$H#AT6x<3e|B?;)&|T@#T7ckCMJWsws$}557bct_#yThDdRSDX1!444pC%LAN -P`5co_XaSv-8-m-HTAvIj-55EhFIvVIr)#g-}KA=wZ3QFHgm*q)s;#hS|y>7gN3XElYX#F3bI8h7|O1 -GAysjwml?$N&PUJfG%BWdb|G!un~f-EH#j~1ceXH0hE*98-QOGvr&25uQT^i_p)>yYpZz6BG@z)`hE25I;$ -wZ0Fn1DG`)*>!FxnD~I39o}7ZW)w^;b+g-wc5@3R-s@(E=LDTo3nt$0W{2;mIBzMK_?kzX9i9hr_AHq -AD%io8iJ8!w4wXhzoPH>G%Z3!)j(+>$HnYCaelu)`foNZ-lzdF;+*vY#Gg;S=SBJ>&R4_?DAr2j -iF<8e^zsQV(xUj@g>u5Nl}2u_NYcxtwL7L_}`cJHLORH>b`l|L*2ndpBP#FigRZ?nBC2ZW%Zt&7(1F4 -|P(>Z0@7y66pqXooRN?V>&FAR@;;h_v6!tb;af!b9Xk0$!$7K8#d(EjZa#7tKbnj*l?B{TS<=U*3oBc -WtE6{hq#GM5r8cowr$0^=?X{bjd8W7z?e)#h*dTHCAafT*mUTlz8AR*r;#wV+T2cqsoiFjeC8NerGw% -9wiy%Q6+)uUh&!Pxy?sVs`Rf%q031*bv7NHdvx>wu5m?8VqciOy%d++pi0&P&uDWpliNqcfLG4#(^n6>qN3z=__Jiu~9mViRV0e9>`7|KcXGn@}3B$&V -T2pf9laBk78ppd3=;=>})8atzb=uJl;~S;Q=ZwBaI(m*-fa6489ONVgp4Iv6Dc=|&0G8cx2gHP{Yd$g5~6HJ`~WBqA0jP*Z$8 -KsXYOWs!X8DQ7;VWrn?AcPSm^y*T2)?!=@b~{s^eT^;{B4vrgQ-HAxUqhpFS9bYdo#VcROB-p}`VO#{ -=#>FN?4`UVLYf6H1zcA0<_M)BmxS2Ub^#Suy>;025PgiLRNL4Vui!RwUO^S+VL?l#IA6ui;N7TlQ%V1 -i0e5<~z4$3Ik!0C*?l;9Lp}m(<(TpXlFM5HE;2;Va#1Q@f(O^fD3%h_qO^gsxhcHq&eT7P*rqWrOsAD -Fu8cu~CCG^Y^>=a}u$sBCppC58bCq^0r4UWNJYCvVvY?o~M&=T70{qd4?+Il>|^dpsDR$!Dc7xJg3+n -1<~-2HJ=sPIgx(__hDH+iPCf8R~?N`AuyxY0DVm)5#Aa5)cKJQN -8r#`1=E-2TGUtY7=0rXXI@HZ)nb0qO$mKlXtRxW1m&V$iVLrh2Q=3f)@%69U)TMvAlxTn@4 -wD(e=(xq0xl>-DaP0GjnXBUI#(sgVYu#Axaib%sxst^BKsPcmp-%Kn@cTtN$#j*VkX&vYe9VYQzPMO9CK}<3k;Pqje6`SDk!S8| -J2LsfL+n-A-QhU96$*~fwGX$&0+ZC}>>bxssS|aOB!MKqHR1u)IHzZV)F>sUm`WVw#psr-$^RRC$2lMVa|W23HvA8|~tBV{ExNaSE<{i?> -W6sD<^WDR_5995DsY>59Xr5O36n2Xo*44dJN$LHg3!!}uni;dS~O-^6V$bbfw} -AB0R68gk^ZEQzV}agO4Ng$9II*YxU_&3H{@`~(g~zp!6|l!r=?{Jtkmqgm=l)>m1Al#;et-vwK7jWF2 -RZXrNW&Aiw+G5W52-Z%>USu5V=c=c11gb*U35;K5NDDSjtEj>QWewAn^1!%wj-IM=0w>jePCj}*@#8h -@h8;N3t`0)iea>BrOLq-u@#PZdICPKs8O57_8YJ8Cf -BsKkBuRweXr^XWvchAS`ERGlCHy%V%;VF -6NZe#3Ghqp2RRGiZn)oEw})d|x4ddXi-iN)R>Q~KtRni9VT0O=+8wP13XtZCau9msC}on|H`-?+UhT- -cUSt1m`TJfTR@m;4^rUW4$*bL7(sxL -R;~I&FV#TN~UJ*`@d3G=M>Zewhl_Jzo$EH`a+tjQ@n(?!#g=RM+@yp5g|r?W4pmD -5l@fRej@eQn2bI_R=uE*eaWR`}9R}LT_vyUhSadoA?|!O~ne@dE;y6*%2sxzE8@A}C42s) -+a0oWJe(~($Qqx7K?YMICBF-{3xH;48zm+n&(OIlyEbeJy5Kn8Fv1~V`%KRry>3Ki)W0}NzG{eW1qz$mfIL^!>tDT1a+7^`o6ZI@r^PZ8tH+dLcQBd) -#68m8^hNTm?||_Y=H{KftG-50xs{+E8BPo-(ZrCgH(YsG)i3ZIXvr!yAIJ1I1+|3eAG`!(*vz3p-V$U --e&iN@s6;hJw!WBNiTmI!*2cAaYb6MG^|t<*D6ljGNxxa#wby{^@$lzX}A%;B#2QFge2FQmc5!|TuY6IBC!Bp!hhq@VTsF6oPl=~ -~Qjc^@v$mX0iYjB4%v=uOtcj>EgNyH8ME26&kBlzcB;#66|npv^yl(-_wc+CEliNo*^7>I~^hE~=i_()vIR*_)H7rVe!%1$aX&2==FiJA&z*_a~BD3d+zcwY{ -y-0fJy)-#IoN6jF>Gz#vPr}MCPonpQ%0)k_ag@HQHgQR~Ve?d>G&?+8nXOybQ;_s{4^SgWKKj>ns2-r -i$Jq@!YTR>`g>p!-8uk9Zh3{`1jfKz~tMvOSopD0iY&^E!S~6P)bv_j0$tW4)3>DI5;8BWJdc%1gu(3 -}@2~0|SJTQB3*-QH}~1{1R?MEQG)ryJQ1hu^oExXm99A3zd -s@cmXxto!($p@Vqo}Q+6oVMqE}wk9|Chuc87>+{5d1`z+(#COfiETLvV*dn_(&$$mVw`IJL{9y#s1BU -&pkeq6$~swK}>AcdeGzCrnHWaSWypr08D<0(hEYtSxjKt&wjeF$Hul>0j5wI94y$leWhovClL~B>{9( -_p)Ylq%ro=#t2+ig91Q{f3O^Pu&}vMy%xBGrG!qfuz=8hy9;Rrbk97t(LU4^vqB>pBx-?~&=-xT{e(- -WYr{}fz@D^2y`_0gPI1}I14lFI2-6zN@?b2TgD=J~Egv8^Vm2f}tAxHZi%m*=@#^WbR0qYehJ5H^+P! -ZfKeCeTtO38i{;?o!1G$Av)9=&XJ_IK+u4gIQ-nL+mQq)}gPDau{9Md)W4}|8AgctCfkEqWm+a3h1q3 -Ddb?0Fx@59a+!EB$RYzd@L0gaSbdO2o%p%XJpd<3jTpbJS_`;P*sXkdj&;+?=8&afVRwO%UJ{0Uyorf -q?g7s9#Z&;a|6A$*ztI1$(*Li}|9__cU#9=J9FCjsc5N^TTXLLGvJ^gt0JFs;=p?t7VkdbN -94@#0vC5~W2H&BO_pWp;PWe@lPd-73p( -+UdY-72ji-?dx6I;unS6_RWStGO16N3&i5I2C~KZ6k;Lq7rBOyv&|_lVLO(I=iQgvqBC<=Pf;f|O{(T ->@-Nv6uAsYT4D?OdQ%>;UO-*bTncn-t?ypd-DqQZKft#@I}D8oM&`p-kYAl$DX*#^**%>(A&Tg}q3Kn?mDN!dxosfs8Q?=GGOapUDz?`MlAaxO8={0vbZKx -LTG`-$omhd#WEMja$;rl7q?G7&|Z0RY<1mWUPQ7<^|Wj$e}066YBSlHU+a4_zn;i{w$=vpP}-G%NTlX -nLxygQ&TRhy;7L1rm0%q)FQ-{Ns^V{zEVB0IFPq1)9qMkGgOl#(DdB;pzKpNpFu-q2hmo3?qW4ag5Ir -0*HZfzVK}jqI3?ZKR&Jk>iks%`04$zr_+;zjhI{i*Q%F7}FJtEZ*;8kw~oezqr`rz7;O^(7Uk*T^${?yJ>wwJ?`T59*6ZF$r!VUD!*j!VM`yd)#!v)TEmNY4JWxB&2Xt$PFdDY?NM9*Z-F -5t;0@I}6&^4;NMKPYd+=9onkuwnAhXg^8!f8Csc7(W%wdS97y -?4CpFka{N}NX2mV@H94rVn}9}c8J6F@Qh2eiy!KQ(`D`f>~)T~EUZEmE={N?`_}#8_`ZH93;WuMurqs -NNi7qXl*5oQ1>&G~nw3?(vlg1wW(K7yE1Uw4Xjq>!%Mh`NatZ71+%~HZBMS4d`Qk9HmbHOV6S|>}x^A -I~K{}7lc+}U^EAzT^QOK&~7+t>xfp6P}tz%R!Oviz)tI`B(;K)+A659L5DP@gwwQDkPoXM;+Dh>){n`r1xwrrF=GXR$T= -qnZGmthEE%KDQWETVpH@$v(xJSxexSv#W%UDfk?&qjtbQuo>c_&z26+HOhRZz}5-A5##9#Je7 -IE^`W$Yab`3Ht1$=@(!k^Ct`Qsg>@WXh))5+heK#3sMRkahB_%Umy6$j>tz-?5f=GaP#lxs2i1zsN<) -Tn}By>llvRkDSf$y>x!ca2${1WQOCIBnu43-bS9xaC{D1j$}BVN0h@EjuL?!#&A3zCkHUR5plxs7Q}z -Nlj679aQpP8BQPDsbx4m%_ARSI6mYf?`JrDgy)ZUw!NOShv9f#T;9oW6qn^~496py@j7-lnw*(?H^70f1x*`$EYQfAY-SWS)gEy(?DeM=a4yl^k|`2J$nrGU+6%;qq&84fme%;qo5CK7 -B;GMguuO$^u^VK$}ACJt;4GMl@ZjQ}=#nN1e6$po9Xnax6GV*{I4nN0$-SqC;RGMlKyt_}z+A7hp`Eq -3)pU|Gg2eHXjBB(N-GmOtLnMt81{7^okJ>l1iD$9`8&aV8m&QbR37+zS}j^wuksmI^P-w2eX|m(1vbO2) -%+e6wjDpV;eMZ60E&8?Zf)G4Y3IP^5JERtZ?v0j#>9AW&lq@l5b-P-ipMI{UOkMV)r5sG~G*g|e=#!qx<1e -L=PH>Z))C2yRrr@w(cE!T7$q?EfHg{I*)Uo^(vqDyN-9o;sRv&8Ykg;AwgWFk%v}_IYLRR$X!a5NJhX -Nni#8D;UV!%?=OB4V3xQ>o*JjCVrUn2gusPG$4abDp -!T5(k2H{N1T<+wOb9=P6hdizi76vY{5DIM|&J~AN%egL2Ww?4q?9J6{4fO8|54>9u_XwH_49#Hr(6n3 -1@-tO3G9eRh*W#;q~rs@)<5#dhMvu$FjO#z@@cuY}3zl -xy>!_2559Os``DJOmF@H3VsddfQ=R+O4zMbt$dlZ9s$rL6FZP7E%*;x7giUeSs^773=a&Jh;rZ93DYh -qw!$MVx?7M-P+qI?72^_u&6X1*aEYB1%EWiBjP0^I$r68kZL&uQ`BwR>z*+bc>L7oNC3shZemsy;?_< -LsYwM;BfHdd{16|pQ4ycwBdOC4086TXB6cfJn0vE-VrGMj-E$6WmMjGdP(mBh&?ZM!z}wkmi5J93Z8P -N`Hel#mL*LkG?xv(w6c|F*NOc|H+59VV>ds7Z$EO#`e1s(uyke^)uF`RqEbRjCQuID@GJ$Zo~4VV4zJ -SqFsCISu4cW+Jn*0mye@zG32~Hbj_%A0Su!b*v;s6>~V!2X_+ikD -4s%TzZSZs|J-<5v|Yc)L{2K94~=Il@TIeKO6Ydxm`5G|hB3;wQr!}VJ3IvHe*BH~15T_0)Gyjxr&P1n -9XD^j+s%DkYdN^iwdo@mC793PRdRXJy|fS$`#(Tm_svuASy^X4GE0x7*3>2~V_RLe)`%sKpk!W?x -Uc7ul7RE?uP!l;jvP?)K^Fp}Zpx%+d>y*^2GA4=VIs{3!4`yQ8jH|j2rHv9GgtxaTTCJR(g5%iGPzzqzv3Wdv7*!)Hv#GF?2vjj&iz?>23GBZ!4g+`?jXCQ@6>OuUS4K&1r%a_Yz~syJwk#Ig-KDdSEYcx!;bq0qFEQdAWXd -BWzC_!@TzH6PYBWk94bI_~h|}@liXo{WuU*)mq-%gT6m^_B>XizUVmKk1h2|fJA0W+IsNrgS*$S&^hmR+LxCmhc5xC -kS+6j^?eNGpAgK6SqjkWcOF(kXOCffIGa!d@W-NWTv3z02&cqK=m>BE3M?{9pWt=ogko?>^uVKVP5R; -@;JfYqQRVrU{q#G_QF#X>aBw$fC8XY1xUp9f5e>%ere2n9mhe=jHFcXQJjGa;4o;Y{6sKAL_Az<$=ae -O*%}{+2hs~ZKF}G;+zkNx=SEyM~-~w6uIX^S>GP)^;o&ER;9pDi3DS!SM_i&$v9ng*8$8@1nT!P1~*~ -MnI>l$?iPjBG1K3?}`J4`>1=BjJ;MYn0#j(QTiQ~-rz@@POc9@$<*3xGsSkd7#!%SSUJ;`Www#TfC6C -u;nem>#}Jzrza^E^G|95ZvBQViK_{&Ju{T67m&DxuadgtZYiCH;=oL(0CSU^gHlEgklOeDOJu5$TO64 -JRCRa_jx8neUVYv;)nOo|pN);65GvhXs-U4j-nX$^`lSDT{mV{dxE>q6>egs|-Yq; -RSZJmsh?|y{PH+LxG11Gn7;BkvY=}XFSc(sA@V1G{#80Q6|Mgfyh`kDb1oFBv8be5fA#_ig0vvOpzpT -mD!Po~2I6|XB5+q@AixU4`svY!+NE|_%`Unw5BVl;wF^PX@y|*2tw(D=wAuV9 -m%Q%6>53n6ICoCA)--81F1@gt<>)bz|)S>k%N?)G;{i`)4_kk4?qvB1ATFwS}0t>q8_^{)>yo^idQ;3 -u!0@>jFO&4s4#?BK}cx04#Q)9v9RxIJ+u1gqv10^gS!Ah9e+I0zU(Og@?SS(?%hL_7mcYFi+ev>W -gOk0UYw=YouaGORw8igO<3la)`ejd`bSGu=oY|XZaYHZIoY{&B<1*w9np?)~xP}CVzI8v?Vm^JSHBaj -L$SR@qp@$D;n^jy8(7N`v=G_~Ix@tG{>J=Gq>sL6EO?RsoP{a(!<3I?(MyaC -Q@}m~_yKnvOK})PIdWeD#Q?1M}fNu8N)y1_hxxo)ucnXUX%o5x9+UFinv)c1}MCaKMWu|Ujpdfi{(+P@){`Cr{4u|P`AEslgL+k75rULDs(^oc?=_`viZg*2xwj8By< -tS<^>yFyWf|<)-Zk6%H#^RR)%*NKJH5}Uib&M^{KmCm}ti%wrOot~ttPQd -;!jVpNG!L_DEbNBJ*Q -p}ts&K&PyFAl2>Lg; -I^C5b=$l$7$9ACz+Vc3{tqA%q|Gz^K^d6=}OjQKkWC97JYemp@u5#?Uil85UtaC-sA5Ctj2%4!hM$zA -;3HqJ>iUI>2)dW2bZ@u8kAq+u2(Cf7Pnqp}CBql^(Pcifqsu+5dn_}oPrWm>;e^Rz15Qa{w7rJ3By$E -$8s3C_Th6Wg`mF*s?a_Er!4X7I$Cs0f`^mL{h8tuor?0?cXjN+=HhjP`>H*wX_J-KS=Zd^6AH&+df_p -Ulo4ZY^HqI9Mj`jMZo%r~SOI`=16tuz$4sfMOKsHz%z3)TGGicDZ?uy>*w`c8M%&~w~WL(`5^RZJH1E -2^QxepNMe(=S{#^tY;N=r2^&&>y>0L!VYvLswr3?zu4w=0zDjZ9sSUX*G5zsDSbpA_LDCb;CK@sgk4Br;jA!SReU%)ID3FWE0o3?z=Ez^q9 -=SnGfl^?*tqXocF?_Z4rsC>HgNH8fF{d;enRQU6~Dnmn^9{K%q3BC5!Av@_=vcV;)pGr^3@VTG>##~$ -Hbn>G0|T+=IDLkN4zN#aNs8dUI&`!p8r~$ztfN<{d}UzVWRxf?XJQfx}8mwU9S*cM=v$aD&+I7*uYor -fqZ7s2Z%?$a}UOUh2ADqys+*dwOUlQ{X{UwKKrPL>`@{$rO`1$WkaKT9gMef9@R-)MWx; -C^=NP(&bM0Ly))s&|7U^@`1OU!{q}YwKqrak~cj3mU0OE))o$V_Ry9W;lP;(y`ilc(~`_Xw}^a=DcgM -U^`Q333!=_^Gr8CFF7EGghC@Y_2`3BAxuefQn%#b{ir6&OF$dZZ=pUffxUA)25dK^gMLUgVm0wAHp9$ -5bbNl@Rr9>7A&uODdu_v3+1-h$=9_Cbbw!5e1W1uRjz~;Uv6JiPM)#8a$=nJX7k|NUvX9A}p22nUc0q -@FM=W@mZ%YL>-1r@j8vZ_zy^(yMY*!S&?-ZF|`;dl+IF#v!EAKC}Jwj)X>W*PERGC6X}COrVTY6{VSp -G_GD3u0nB_D%uWGIgr07#1Sg>+3g1U7BxF_uWOV=2(x`*-Q_?7**$G`5RHN1mHC-X!YVoX>sw(QeOrG -`h*1r?=sVu^{px`e<_y&Ah!4&(Iz8K#VQ>q$pXa3+Kdf1^_j@+zJEq;+yE0U1*A*8wyLYYgnwS``{17 -nG&WVuTNzhb|8_JalU?qDFM(d$`{cXoF7zt?S0%*v!pGrH0g92==>J{LN|>}t -tRPf^nAb{3G~`ldJnC`MLc?Ckew=^e2{^cZE+Wk-R?^<}sEbUG2jyJCA|EQRNNW`3WS?LSKce -U0f3?$IWlsxUEU1kB6lT()>8Q=qI0#Vz>N^g?EP#eKBrt==UYW(0AE0Vk_3n#I5)4co`{>ZgJv%;1eM -4%ikocx9sHefaY&%ArhftSflHv!tU^VIwT%Rq3e|pQzSlKezW`%geYA$2r?9#j6?0F7JZSm!(BeewX) -5d-aD-F#MX*Q=!-(BH`k561}~h6T89&(Gz+@^1gh&MhAmWEN)uw#4JW3eQN+dQvQVZ3s&LbAC~*|kdh -)UOWQnKqF(vf99yp)fpfB!A?XM~O)4zeTe>abBLPG&I%wvVu7d_M3K4&}WRpbjX{I%|p>doY?-$YNt* -WaXXOwythek(A4#TI=oOMm>3ySA!!L??Kxv)5BI>Ai -fl{GWFH}%{gU}C{Dz2k#k+0+7JGky^d63PhZ>66t(k1B=_BP$8b6s*%4xkM`r|`o -(lOvJUWO`NwZwgsr>zPLUjER{Zf;4W|D%>Ra#i-v|CB%-qA54y1)E4y}G%VD2<-7ytF*>DOnApwnlTz -+dkCw6l>C>VfKPwe{<)eADD(Fd#zCy%BoM#l{bHgP*ROgwb9gTkFsBeXm(NO4hLdm$^tFZAjsmp*~ms -bLj6w!=vnB+gdgq?<$)(XX3^+#;a>(|SV{DJPD+T>5>s5GFXqKoe%-KQD}5F5VfcOH`-Tq@|~q3da+_ -pW4TN}$EVoNQbfH8wd6bXh~zFi#t}r^lJ6RcMsHLk`^`NZyGN7X+!*eEJuB2-KFC_APo%cpFKdh&n54 -u>xXWgyH=dB;FfuGEbWrovAN+66rr}Vn8Oll*g!p`r;fgnvL_qR3cjO^i=bC7De34{&sAZwNEfq^_xxNtRDU5HRDM -zye^hE-;%(PRMlJIIEU$qXR1%mr#;R_7e;p;>P67T*O6yE~c5qL`?);Qc8 -saVc$8#;HI16T$GoUZOMezIpGHli-ysv~lp=ZyRf`}|XI$P~du~{D-$%yHXG4BThWJx;Q@YFBvDj(>Z -ukwQ1V2Xm??ql8>9{EL`v$)QH#Nh!DdM6F$2bW=rAj>1!R@HF4XKXCm=-J;f09YxM-aU4|cRaX~KV;) -Y_ryE9f$Xjn4V5)Wr;P{btJ9FpV-2X%p$k1}(4}zU`zS!kLAmWU>oD*@Rv^|s=9uJ!cz*qpL0@vfM)fvK94G1WoSa7kw|N0HsSJ15uGvC^nDFwVcZM#VmGhP4s5{=HI+*t5A_G~8o+qK5 -iKNtLrYJ9{+0Krzo8(yfmXE;I+A{r2Ci9?u{Z_()TLDqbCT7Ei-DF8Ex(KsVJ*l(rw9?mc@X>?nhL8^ -NIx@%(c6N*1}_|Vl^r{36nu5bAeQcj2$W&$fz$LAk7EcKFST3o}@hqDxgK6~o)lf3T81^Q^*^>Wa;Rh -YWT2!pR69f$d#9J3wjU|yJz*4T7ttE0EvGRw8&H!IA*v{2v{Mbn`;F;G57UFl)F`Y5I8kVfoXa<{JOP ->{Zeie)$r{j3#cp(XRer1&s!E3Cg*SnW{~ANB%X!gh?7mtmtUERO_PTx#eK+}k7_h43dnr5be?79KZ178)j_+|;Uwg9}? -*#hdV)r&KAFhUTqf3R+a{CY}L@oIWsZ@xL7kYSbCTUf3Mzk*JLqRff7iC7<{p=->>_es8f}GC3R~g{0 -|07D2v^CLjBYzW8li=F#sv;%bC$d!tk|Y4FabeFmPXRchHN;CFylB{{7qO;`D$5@eEpp`3FgP*V2^x@QWM!CZh#4i6 -F3?Z~uGRA1$&?Yi1gXjyc8%WE=>inprKDA)6VS+I{H}t)1Y@f5oavi_sx70>`y4|JE^#JyUgJSOdUE% ->#Z-zBR9(n{pDbnt$6f2ot)jOQ`i73Miww$;;rO7^kXz{g(Tt3)U)+4&(Sw8^C*L#z4~IjgFFnj$BAH -8ytmQ6|stXU3B!3gpAq;nklxsV>U~C6)<4B(4=rlT@?rBLUuYI}_FI#U#PP&in{zi3Xf{N}(-lkU2w+%l;7^(rT9+EniHhQAvT}3y^Jx0`abK# -fqDF@!FU>)~_b&SV4>zA?5m=6%zb)teL`1kwlDw38<}bQ#3*SP=0*s4D;|G^IZC&a0bovL;1-S-*r-t -g_}|cz1*=Xuam;aCFz~mA}5L8byiDs(PCg{F)=6aye=z{PUpX}x4D!5{5t%0jnPo}EQk9!tmLqs!|yr -t9?M`q4r4e><}ic9jU3+3;h#7>!eJeUGKVc321PMAh{Ku*3?Ah0MGnh2+{pcH<8UX3f8_88haYe#b9j -}*uxJLyb2yX3J2_m(;SLTT;qVZLXE^+f!xj#`$1xbfp@G9&I2_NRz~KT8(>cuHa5INHIo!kH%N$m6c! -t9-IQ*VN?RZ{a9ENik&0!peb2-fB@E#6#bNDicH5~qx!|yo^;Pugm!&^9v;ZWf44i0lT+{|Gehw3kx* -UwZAQ=;0tcl9e9gvu!)z6A`H9b>TNaRxj7%A1+JZE)xAo&7cq`(+726bTaH+cQW*_H^g%WuAt{lb_6B -8vH@mN#>JeGM$VhF+2{{Jcf)X7LwqKdp^01m`OaDO-$5QfWJv(HT+G2c=BBS()r&AVt_obKZDC=J^v% -rel)co&20!tqPd2-8He~Y5i8`>3h69||LrdO+ej{1(~13T@GXLyYC8|04Qx94G!h3{0bd@t;cq73**q -VrIS2d`mwy|C%_7;{oJdl^oJKOac{=!A4mJ*I?;cMo#M5TW%A@9A&99nIHD79e)O@h?*1Phdrmv=}ri -1aMLVONzOXnff{9*3f?2;kp=qs;-zl9KAIxTs7mt^qmZkGu8&vElHkLJ_eoJz~Z(pA$_<5%NT<59x{U -kQ^|ub!0WVyJZZjW8s_&w$8!_%Zq?j!sV>%?wGvE=JBWCTp5?^>V9=A#TfE)6P6QP0U5(JgZGq9gL0@ -jy%ZhOh-12Z>A%MR=m+>&%!!LOtq)E>e;&7?nt$}2sSe{hZ-SWObg@3xG_$Q595OH>oG2j2g73+GA~t -h*wa&KJk#x290R&!I_!Ba6VS_=o?~^hNOmw&Vr~w#oaf*$Rm6BOJcdDk=m*_tIE{y=*2~*R=j-Pm5ZF -y06x_W>NNCSqVZHnG?brXN0fvEt28RzBI&ApOw?vE>Icjv|n6XjOjnK&tS@|3B!#u=x@n-Zqa5X ->`YSrTuXoiu0eyyW={7B0H|j>Sus-nlF#b$MF4HDg6)*2-1ct8;Q~ciHnq$C|b4)^E6bqq-tLnW2r% -=)x7-;cVbNBlXpppSjTgMUp7^Lz|J{#|XJ%{Rs9a2HMLQXb)ngeI2G~0Srr(PVF*ioU5tdOej$vV5zj -XWW5pgH`q5iXdB6ac5i_HQ)%hf&=}g{Lia4{k_zFn$Oi5YTaO=YT`c}A+6uEEWGc-qOVa?UIcWUoj{R -^hO%>g2@R#ih!D1W*E@oOv7VB!T&7)@YxeDTCdB<{L?(8%r?6J^KE|lCr=*tl_z(4rgloH9GXYuAT|crX=;Tixg$5Ah`P`mwuQ7^QP5^{{RIJNTm`-6CD( -Q1hhHud@=QcR`P_Acv^&IA{$o=TyBOHINpVPxm<0_$K^=^|_Xo^!n|tv-HOQ1L+~xrvHQKjdP`Eqcw) -{t^xmcDg|I1owX#jZMMaz#uxn$#-muLtEpM+G{2E<^>^d(qhyS-y&7{~CpD_Z-`3w}& -^ltRjg43~FlW&i40Lp}Q5!ootGQue(yyVB2Zt#^Y`&#a9k|(-8~5m$Sq+)Q%|YDU*o~Qkxw(v+yK{3K -H}~M?`aot5<7OK-_vU5;H}~P@y#dVJSB;OG`>E;sGxJT{{5&@gP}B2cW`mj@HxK0Id>u2dQ|ZCYsyrL -V%^TGGar0Gf4&!Epn_GOCIgz$~JC#dN-oUz8K*u<>*NZ0+@MnOUZZA<<0K=JD+dR$Bt=>k_KSCzcc~& -m)0s36c}ESA4ua*Y8Tri7rlnl*&(*Fo0&Psk(~{wH~!NgA9#8bZ;v2tLP -CNU{>DW@TRb(J^WU;KaU&yQ^Okmr^)}n;CF4EX8w0`fTr#5Mt|)%K-2c;af5$t9PrQn -{aX3)F9iPFP3~?tTx4GE_U0QcZ(F%L+R(=#UBCQ-t%XI!_xxsC$#!W+>2L2XyYK!7c0TyKhju;u$fLU --d;Ez#Pd@eZGrxcKx#wSa@ehA|=}&*&`|MauZzxeX2ujPhsoEN_R?qcJm?|=C5r>5qX%RgVa`b(?wPcP7Py#RAV3p -D@J<^P{f|KGkq*0uZp3i{*ztG`=&=nrPCoB4Mg%=pZ%yDahB4(5A1n9Dkt@9SW`zk~UK4rY9Y*WLVD2 -QwZ`K$*H6KHMLlVO7@}7iQ(8=dPV6rixj)IcPsW&uX8&e5Ex_H0NaG+E-JDg*+Z`x1{EY#xyZ&jWsFP -=CHZ!nH`IcBtp!oE3DIU*RhcE$vlwx=F%0UQN#pNriHlwyz}$#$uGzUfzI=u{Cl?Mhj@8`L|0r~41|x -2%umYS2GZH^VKGSy^Fdw%p)M&2wWJz`=Wol0Pz1!f%*;%k&S#y?w$^65MxMi+ncLvQ{hM%t>+Wn8h?2 -1T8Y)}g$p#m;CXh*8Q63!vvU8gpJUvxR%{Ex=_FTImE60$X>c~mUwAv>bhU8ICcDn0;{<5;I=>{>^kZ -ZH%7_958X$~=UdA7Q9GCv0m=$-+DGo5)lbe#xP -I&`wSk;Z53BDG`ljWoI@mq*yjvN{tT2DOt7=3%_1vRylFZS-GZ6@mO&q9H9e-D=3qFxb?!cKnT^YZYz -#S)*zEc7V%iDx>+VT>T}ivq1x4F|(S3cvr7Z1q^BrDF|d6HVW!{7-D&u*5#>SnVpqm1s=)H>oT6TnOU -MW&z72IHKb;UR=Z)?Xh)uX^zy8n(bk+b!_>eX_@<4oFg5U)WzWr7ZOsu?|8415?W?nLQfVba)j^1ncR -6xJs{weRLo}Ubdh#`GV;NnWv%@@ZiCcXpyCuWoUz;UnBCF(ysdhK(8%x%$9%*^GpHAqfmXEbd_qeX1c -e<6bg1aAsBM-ASKY9Adn9l2G*x+G?R9*_?q7Ers>wn-dC}VbZ?pll(LwA;~Yr}O-lP>trzQTbOo@W?t -T?0K$R))d4+9s~Q`Pb814y?wRDHSpb&D82*W=fRR*44RuOK%WwU4vZh%+QG)%=hP+=1JObUp}9{&Ggv -MKWLXN-?|kA%J@l3aD2&IvH&V+{Sp|1aG=Rsl9#pGmTg_8_M=PEv+~3x;`r!Mw)ExX27VZKa1R~nksd -&LMEjE-bM>`><^HzeF>I~rei*P{gD<*;`9BEwNjGzFA1&!Kwx=yLB_tBVV0;H|BII*i*x`OOejdbcta -oJZk%8j|9MO}$VmHz&JrL3hAo?gjyLY{|)+33KBX_}=Mg41uU$h?q7n{akL;Rs6nh-w{60Ij8V}lyI) -d$r2mFsLiDc(uJz5Ph|v2NkLJ&5;QkL#8X>*wYukWJvr -!vzSO@j@tf=I@}nJ3$kXHTIue7L01pzN2__mt05PNo6GJl0`=YxMgS}s4pZc)cp5-AnO}_xrFI`XiC3 -hqJpq~594QuRK&uO6dCVD7~9?GJh+m;u6VRhP6LZ0M#={*YQo_+*qf0*>wyiK~52iW{MpVWhVqtR-Jm -UFqr$AkE29;NZPz94TBB>IpbD1Xq{fO3tm2k{;271=w|pA3k8mJHDBChfV2#a#;mY46d#}_?f=KF~hLS9NGWF(KT_eg)zH+>H2o18@YM$aaFH8Tkd!Dx*SJZ -Sp)lYU|t@KP^`wYeNZbo3^@$A*@(_6Hxd-7uhS*x2jyUd~EFK3mzwd -$fMU0Eidr2-+{yO(UkL(&?6L2m{|7UXDwv2>A_|3sgFV>PV;vU%C$og*=6h4KCM&=vdo_e1?D>wtr5; -q~L4)yLe$m4k5pXZ?_tEUq9$WgMe<(mja#p-cQSxr0GyL>KEHccYwWg(C7M+UdaHV&U=mR!QAod=S%u -M1?8&`uGNyr2mKx$Tdr2bXyyO-irB^qgS0Kn*>9>1& -?i?#^A?c&IRAW%kF8w*aBfgAZ*B7o(9yr9>TY!5eR7jKO}R0umg|LHm9R)&KK!Ct!+H{&2x|jeD#+Z) -P(zy@aWzo{QEwQVf8(0L(03`f>VN$g5AbCHSCE$6VeEu2d6{tfjZJXSr6^58|iMRydmqpse&R$H!)yMn9!f}z|&M1RKLM#pQ;XT2cp9#P% -w9m7Ss!s#JgH==*pzm2cDd5~^n{UTL!N7#{O-&R69s$@9A;9h>fvmh%#f75Gjk4BYaRR6`*g!DK@$gH -2;+pJrEI+k&)>c0v6zImLGES0V|K|8tMzuvdjr`*e?P4P_fh*W9Od%Qmx5`CBF5KV?>XJdq+FQj)JUO -x6AAy+mV -_=@T8L>7$9%sPcu)7v&sp5&-c8%w;v884y4Qh{0q)dJxo6H!`4Yd`s$Nys5|WruT$EGE}@uhNfR3L!* -BtLp48=woojsG9R9QXbbhD+Ln(aqnISKZcLk<4eWM#v|D0s_xsQJb_V}*zMaAUE9ct>Xxxu+mTG2L#d -tQAw!ln!A&Jn=v*N;DQmUA_kZcB%`zq_q+~sWLl{`=4?N*qGtBbTiQ5u)SZih*}8o$nKVQQ8*J=Z=jD -`!Qvm4{soK69*g2pylBlb4%qjTi0N32Pu6Aw7vPFE49F4v!%@w=GGMu9;3V!q;vg&95*`EKq!Iy47_y -wT}nbCg!F)vaQpzvaPqJu13GldCtML#ZF6;<(cTn7PF?U7p=*;3$xO#@tLW1@*ppx%b$^uRT%d?FI157IjuG_Pp~XBCYfpQ4?P5Hs%^U63i*4F;znFHA>WgP9MNizw`Ylvk?eU;B3 -x1@v7Q!d>Kf}c_BK{lJJenBdK;#)qXWMf$w{rclGaB5&$RP%GHJ2W6V|0!Z5SJrLd3(`uONN8boTHx= -b5a_9V=E??MZfP97`4$UqU4VQqz&?P?ACOYiMU7~fJ -NQR&t3!1QwI6cfcP|(!utPLHDY|KfYXUoco&vih$-vPC7{Vgds3#mrl#WUh==8q|5+qVzsXOU(LtoA% -=dY}4XTm*e0rCt*HCpm~4<6&f)iXmnvBqAOF -U(g`$DTdLjaqOBEuxY1}qsyz!ARTq*}=8`@;gHe8tN88>T<_yLx#_7;r2#I1G!E>~bboI-(v2$J>&Fy -yWgUGM`Q17-pb@KPYz_=4bI# -p)&Onx(TX}Bmwf^6$`y1{7jM>aWwknDe@8&M>dj}>C<2anmVGM`S97b|DoI?YLVI1l>Z1HCB0*Cb+{* -}Wz4$pE}%V7D^-4F1SrJ%{@_+{5ADGY9%-enm6e&)fdDx&In18s~N -%J|%?LcNnjyKD-`zdP#}=o|3|3-1MI9WrKefN9=Wt!xzXopMzb|;a8`J(VsOGg}yHVH3T6w=lW_fx4OD -a}dxTSaf -f?{PfR^4MzW|P~8Yb&Dz!9G5$NV<})b|JfgCQ)yZ{Z68yb)mhO%Q&VhNJ`h3BDw-Zvi-N0LV&!#{gVE -fce=7u)l%f27u!Sf}D5@_yJfukfnw2o!6e`>Oy)hBJIWz^8{ook6~HipF?1n>}i8^O;JfKx#X&j&wo0I!T?WxoorH45?!_9U8+Rn -e@B*#N(SuMzBJfVYhU*$40>fK}r`E`s>00iGMr^7&VQLt`LsV2@`YhD?HZ5C`Zr1@Z>C4&bt>PzJy=0 -lso8A%6t?Re)dK%Hoj$E{p>nfcjbla7`R5OF6*$IOYc-E*v3_aJrG@IR#)5e0AWz4B+$d)dRj4;9ubT -0^?e698` -nm_HZNg0dj|DH-H>ux|nQ!2&|I0bU0X7q>9}$VG&_a67A)KLYf;gOy7Qa6Nqc!T&aZFWtfX?*(XusaP -G@X9B#inB@&&&=M9FVf<1;0suDwJi3(GR|9Il$Z$hT8xtsnC~$pP1!@+_{{E-2t#69b`AaF8~~EW%-W*xXQ};4dB#Fm^T3)2k>q9Y5^|?7@x&(6 -To8}uK}365@-uJ!dCbQ)CE}uv|7bzxDjCWDqdcIuV#Z6@KX-Zv>JF4>ki<%t64pM0q}SZ8$JA%28+-o^Mc1>oVkSXy{?<KUfc}*E#Qp+dv0OzhXH(I3-iAR;3fF70sj%;p6&3B&`>yDVt!%(_Syk_33w -R5**kds0(^1@3;PVfsilmzaR49L#oEGdfJYvNHVb|bE_#IbQ2-}C3UfoSpA7J>-HbjWz{|TCZLb2{{} -^vO0PlVrU($p+0{F_~EDx^&{ONHPXA8ifC-^u5uznAV2Vu}ttbagw{%O`X)&u5Fx&?msR2=F5Sv;U0e-Zd0X-3xpRcpO0cURKw804jTV|MM5XUk2X7*PdSiI={l= -PXc)KRiFdd*8&WEjr9Xz0GDyx1~BY(@Q?NYKid!UO2EGWxZwaH{UU%q0M8wSdI$Wk0PlDM_#SW@z-Qh -D9tIrY{f8jmfbRrI4zqCtq2Cc^9{_Om5k}{HfRRU`j9?!J(5Djk0dNDr_u<j90S(e$ -VkA0VaP4bq(<#jH-jMfJX!L{|L$gcmTk;AF()-0XBTZc=!UqH@;wP;Z1;ZzGQWj3~<6%z|&wK15o;k_ -g4TXe$Du8GQh9kYXmj#}O8CKM0@X_6XnPIKmG(j_`YqBMiLEa6CIdlH&;Ha2(-!j^n -xU-5f`FkmCs7<2bG(;+bH?5#qU5%p1ZiZjW#a#}V%0IKo#rj_@SM@to#&9RL6HV|R`{>HgqwV9{=b?y -cqH9^p2&o#y7xht5;>fIZGnpxesFaJ9I+Mo<|%$;W*~@RW~}l`w#~cNnSZ7>|bDloUF#r*p$j?D4nGt -!muuw{>B^s|)+*yRhHig?)7w_H|v@%bnQcI&2Hr>z;ys1&H?rzwF)kjE0IasyXag4S8PIW-k8|v_G#l -^YB-}eMp_{o>DGe%@4 -S`cJ0QE=;5MLbM=%`(+tj4VAg@*B7_C=+O=qwPu0}aP&1t#>)^kO%GC_&zIJ1y?g9i>D!F;>DP0SO&& -asS%v;cZ&8duxQ&rr2mAPkw{Z%wW{8zypf8b{ei=UaZo2mbrnp2g`qM62j9@A%LNMBK;Q#F`1u*3XaJ -$3m!n3~X?=6~ze?KM|xnlKJDx6WN+1`k)WA$~A>y?HF;=G`@?pa>aAA6?x`1wAgZwE(=Dqx+?JY_O%~ -6!_SI3Yj^hf2wr7jyco#Y2KJK(QV^D=1i^*DBn`=nSU#=zyP<;6FyBoVI55upN9K$4|i)9-xm -0ke4>HR6FwjK^zaQ(kk5lSum2jKM@L7K#Kc50Z{9qzVZ(-YJYH5-MqYgJMe^#auTmcW;DZmymtTJA;& -U4w4**%wws)To*2LYmyOq~7-XUAwx%f`Ts$8-Kswm?fMcML>a~W{Lsx9CBwCr_2wmkmjVnw-4dNr}z?X>^cv}qH0(s3*K!-gs3mCdoFa76;yu_~VI%r%ln?Q!Idf=T41d^35ZU^&^h%SI -~iokeP&UPXq#t&kCi6f&k#A-5h;$hhMQnS4?qOHL>x{;Wb~om0r1^9sp>Z~kWrS^Tv^(&`nm{DMLX3J -OR`NeQ{{zWc~S4?RR4e)wUsd-ra#XU`t;```bb*6-fEd&z6By+-@8H{X1dymxpn*?LJKuQe*<=+UF(* -s){e#EBE+^y$;&?Af#A+_`h)lTSV&7r(3{?|rY3Pe1*X@?^Ey93|&Lu2m-4cMS`CsUL$WU(@btXJ+LyOjILYsz29S>?3apOO#=KM2A{Lii~V-U8uohwv* -P{5mC=Y=`i>A^e{pd^vSKYA -!3Nk-uH#UMAqoObVT2|4uwjA<|rQO*PtrEUySKAA%lm=r7D+((qgzYwL#Eqn-s$F;af2tO6VCqj5!(} -20YvL3>3hwzU<_&-DV3J8A&!hh)&9)yo1Xv#JaHp+Y!Zw9{Ml^VD|=Y -;uP6};)gGWw>jZ`RzOE22a4BuALTd^X+ILb1@6GV-8NMgOzryfI44=yID;Rzq!*6H!&lsNPOgql-=NS -Hq6TStH(1S;aA`2rGGnd>-L6k8q_nDOX#Qa=kApH%F85OByM^t|8^tK61i8#qcc|-oWtv7=94Lk7 -xK<48NG+S26s0hTp~Tdl>#W!(Y_FSJOD~B*Q<&@IDORgyCB-d-i{K0It`ZvQ3-MV>e -*DaBzaEpP54>uVNVMc2U+|anOua9;;A~MuyF`CRKh6i4)TKM~iSnncfiL}Vp^(l|K)>f_55gPgWcsJ| -@cpl$ki7;5K4eHeM;Ol&YpLRnNE5jR%05(~yPt~bYPrWX1Y~<_BNJcovw?6sTI(2$<5jXgY8+^P0Z!+ -Vm5!e4(r$G-5wVur}1H)Sl#>ie4OSr}Ai2>?))UErs#~**ZeoxGR>ye`I_?{(hu!0S^ZVHc#uvj9)Bf -U!8z}K5L7vqOTnl0gx=E$y3dp@b%fa}ftTJ}`b@l9e1$u(;*ZqWEA#&6v@l35%XZiy5`h#4gR8Goy}b -tq7<@ED$5yVj{whi`CRx3+)O8X6GJ6ZGQiPj~GqP^j;`ZmLzKR#45raEm#Tx!l!D5YeEkrXGe)M&mP; -YgGv}SR<{G5#bt^&cAFBVQL#@3V6h=MlbCGcoktbIAN-s5+OHm9i`CR#Vip=Nw_3J0{k>r?e&fMd@q!49RY`C&$j7eEPEnLZ6l -B{$n;GBZ}T=&DIM4eG{h9o{NT)0qKx^$_sY}qm;BO^muv0{bD71pg=C-T7$Km1VS3J3GvQm-7d4pqt2nZXO4^RZ4AIue7G^N?$5aMpL1ZM#q&kbdKZq6&*YW#6G;DPvBrXzXij$XZTQtw=n!bh9A -xFGZ}sv!>?ud&l&!1ocl9R`7=-X|C*=B_b_{Udivm4PrRtv)1z@fKmh)@tEac8r-!Fcb1(OrHJ@fZtL -N+2xN+0Q0kz$0HG6{3J$)KBZOZr7cJ~VK5Aav&`!@FrXiy`dHs4V5i6@?}@6*^X;ISHhPitRzd;2vHs -8;Q9KZa@cmzqy_dem(m@L08KHL6#s@_4gawLBkr)U$bm$Ewx97Xj{GPgZrS>KlO8pn4UI|2L0Hb@=SD -hBc~J_h`h1l^8t^&K?_d9~O&j_L1o$^){CWQ7{{D^p{eAoeef7EFnO`3tajHzIg=Z)g^#=f -zm9ihuans#{`|)7Em%2ZM7Zs7Q_|2Nu$Z0-VIJo5jfmzNiqijG$RHmP2{I(r -uJCr0tQZoJ}~J%#w$faLhYYL&HdAFts3;48u-)Zcsg^5xHS+}V2h^5yS;`st_h9DB}Ox^(FrA5UJte* -H49OV1uZetgYmpM5sR&CSg}I5;>!qsc=X2S1?zAMS^LkTnAHUz#*&(vXkfRm}>*dJG&m(9oV6e}3xl; -luNI9ZfrS?4a}K&lAT}I&|m|HmK<2$&x3Fg$))IOY712_Df#lL -Rdx@~69o;~21XPyD?Jj`aZAI803{I6ZRMx4u}HEh_h9rG~t!i5WzmzU?{^PW9>=$mi85qPqE1n&5J>e -MMZcI=oq2Tihb&LfVjbJu_V`R8KRyIVYF(Qm)~Mpv(1y>{^6!7bl?_uWaJ>sQ>zbiT#*){b#V1?>Ie& --oPQa(3V^J7|y{GLaqlOFHCd*^jT$I+ulg$z!g -J}u6HBW!|gSR4xr3q^-*Kpr?kZm=D|eDTE>0)NPm?Vlir-+z}+o2iY@Z}IoHah+}Wnft;2=FOYLVMyq -IP*6}4=rKC*|LUu+XzSLk#4;z&nS~95|B%P$pMNeoa4#q*5VmsU$Pw`wG{6Ty-(V+@5pdi16H&}jBE# -22Z9gXp_=L!RCsC_BqJF!Hvd$6RxOuaXZKeJ_G1uY0ef##bMvWS^;~YMf{UzifXaK(8Irs{CAPeA*&y -f9z6DLH+XYd^S2ENb(&;q={f5_POBa!(KQRsf6&U=VDd`Z-%fN97lYR)t?`H0B(L!#8UM<4bddQE8dwJHzaV;sX^1~ebn`aRkbOjf+nuF}3gQ3DFTX&S=eB9n -#+P+r254Y8{DBtW0zCqbrO^StgXfSD^alD5zsqYjflzP8zsE6^hOh%fA&hG$rlB3{O~Mfs|JXvJM%%R -KFe`{Z#|)DGANtSnh|pbSBJ~`6g)E>~&<#lkY#e$9y@y@MBkTh0+0#V5m<9{$f7in*4PEvUb^J=DA&_ -mM#V#Vh9ruqv`{K05jT^UPIi$kQ;9E+g1G-+8eu4kEj<^6g_!)hRI?FZ+8onipU>d@ihTKb3k@hq9C1 -v6N>#x5O$C(lxG{`RLC`~4%(Q)h6E!wwb8LgaTp(UdYv~X0YKw`l6M9+Ok)b|uoBxv|XvuDs9Hq9Qlw7*U)@Mk&CZQHi3FZ=Tuz#n?zN(b~FdhJSwqy@STJv#a2dU|g{FlCPGLhmMq -2pZ5{`hn;LjfOraiFz_l-M`V(7|)9}_hL4a|S!=L@V(0{hIROCajIZ2141vDTYl%-q1AAWMfR3qiQ)|s-HhLuc1##j~q#LGl4Ut}7Jh@L;E+B -1BT<=9<&1`X1lL4&ks_$0rrVwhiC@rRuuzHpx9?4Ut*S2{|w5lIW$rYRk1?W9hW^J-^$KZ$7=--T{mx -j=`1Q)#d=4Fk_J4LW?b`Wrjxq!I!!P0podFHVc_c0P -4zX6|Ww?fTi<|-N!<3G+F}WkHVH&cThSf~Nw#B39`sE)g((#ACkow=LQ>P} -7h3v3b;0@e>yQBraAO0Te8u(4bD#SLl?bCv23)8S6xfA71>P!x%!PTDs%|0nD&Pa1dgwd>66HOm#qA7 -#TG-uoZI&pZfph2Hww9;~nvhZhrFZ7@BNd?}(3rFAtykQT(Tgn7JUY{Q${(?s6|HrdJ=p&|KGt;nsGS -k4e0DA@vHjReG8V&P7!|*VgIgDu-Vy5K5;erM<_@qv{9HZSQrRzU@24WcU2=vvfSCidtC*(!4>t#}&o -!q%|hxWc3O*^M|plwr`2DXKDOv4A7J;Ntuj0>S-7xK^7`wqlf&T<^i+fnNef_$_N}8`GdfTn)3B9k5cbS8z$Zz2PG_I=&KLu|oe)ZIF%79 -q!y8P)KUt5a#+U`7@JU8pjHZ)1Oa1TMxpNcX0Q_dnn&qUym5%agxpU_>o!+^GKACHx9n*tot7g -w@b@seMqhYB=!@pRMQb!nRcC1PRn#eIYKKJCl?7WD-UnqZU^&oSOziEB@_O-;v$L|KNut(THa&oeuL9 -bu;k_nEmDc3btY3*=+jx)!AJ-?~hvr8*C{{jAxefRF&AqRT@{r5%gg{Yrew8RY30h5gt(lTm`G#Cj-|PC=L)<2_Srmc -xe)SEtOw9w&wEylquamndLwhZ)1GCHu~@U``D_dK!hhSgZM8Vp`MbowWy_ZBhYT4KKX~xqi|j{*9Bei -l_3qu9o_+RNdh^XUY2m_!v|zykfe+UAz!x+i79dVYIw``-ipH);2Z@pp5K@oJ -6%dxlS1k`xK_@45a84i26j85ubh`op@O#_Rre8#ZikuniMmqokxHVw)hG_vq0>-X{0Gk%}%yQ7%^ZPFL{N<+Q$OD&FM0q-P?6_d@;>BVg1aoV)OerZT6crUk4I4J3X -3d%r=hgxj-~rpvchDfaD=p~IDLFQL>sQNOp>6*R<@l`T7>LjI$tK$L)=)b3#YVb$<3=INyTbbqixw@S -88c>FOG`_mY15|N7%*S}jT|{r@SWF{LYF`T>;iNGH_+iq3-%xohoSd~(XbWlM_?U^?~o%wcA%w{#^nE -c0zA6k{T~3c?WQLrB%FNhwbylXrlrh9Jt_U(O{hcjT$AAR(ZpaJ~Bz9HL(pdmUsntJx^DRgVa -iWLHX@ESbFXW$Jxfd9ew$W5U?$hBbyumRWv#)S`N+<)hBW_WmbgtFXi`T6-pidyrjT2cHluRi~gd_~8 -45%{odiE|4)RV?g*hq!BhDEwQJW>PEL-9fggPEfq(o+2QL;eH*&-R##bJ!b>zy+>Oe#< --9XQdj@Ebez7z-8T#!ZpUa(t;!Q*k63{MSA<~w}pP|`7S%|gDyeWagG=O++}@)^V((q1?~cW{rZ<gKoJukGu)z&>2}jV4wM3<;s=+8{D1v>*Jz6htcOY<;es+Jw08(gD% -_;+JG-~3%(PyNZdI;UG;$a{s;Vl3)`2_e~#ZX*I=th`YdFzyHa8!nywg8W1~?zrg=N4 --k(KJHcbDIl+6#LGJHD4mgL;hp&Yn<28k-YcS0Z8+U;}bhkV`FH7I`z6jUlz6JOU8lmf~N56B9P{guA -T=jg&xTE3qnSc8q?wAA5Jv@}x@lpy0mmL!(+*Yd6?}iBzPAQ7vihATv5BXSGJGyB{wX~y`b~I?mDcX_LqZ^JTf4AeUJV3+qJyo?{MJXp&#C{+5&$-`pSuf%o6&2 -V0z_J?V^5EPzkYm0jCMKpY=lf>%`FIQ)kDVdsep1otDaQV`m}vYNqV9*(8XNY4QB!#4W1<(oCOULg;8 -GqAptXLLDpfl0+HN%4bTRxH=bSg#-~PaU=|}cUM>#*Yac(=Db71_2p*443&NnU(J*(EquosFtUuSJE_ -rNuxJwFo}^2Pi=l{0?>_Tia4XR*GXE}u1#`{!ED|2y|<>~jm*Z+?h@Q8Puo5p^!qSFk6Jz1V}-iS}Nt -P_5SsetO!pY0(Q7EQkUQ$epl$KzxK>M=XM`MlOpy1=sKyZD7&edLe2&sCQyd8}%LR$)m=0ulcbC;5BG -GuNQ{P^*z?Pm=}38cp}$4$eZ*H^|Ary)p{RlW$Uk$wsldWU3Sq2F~9!V2iIpFz!Su;OXrT#d()%DK4z --62l&b*BI^aUZu;yQweBmQAJFR~>O-hA2J`V?NuBgUOyTuwH0QZd`sXkpf8@Zx1@UL=;yAGfhW%jd$; -g+&U-OB=!=qpNoA&{ -rsrCqJI3mi#~>GH5%0VP-B8V`hTSQ2RT3TAAPOVwf+uV&~mlA&VJTN!(97iYe)ekylKdI5B? -8lqtjYwO8PPeG~8k4g2e;HDzn{KGd189}0a$jS01l=~{hh(jc`joE9r^5c;_HZtdH3e%D&CYYiCk-#O -DD>RqT6VNVl#<5C~7FOK>OY7?TyrOiKia91%`H=RCq`Ld+^k(14yJv;iHcixGDEn>Zo{2X)x7w`bb{h -6_%Zispn_EqiL-u7~>z5ts*eFU|_lo+#<2WWa9joPhV|6yCNzWQoB`~TsxK7e%r)^^|td=GeWz33a-{ -Z=1QC&2!6uC~9vLT3}GGtJTJWK%R8(1bqnyf1P;{!2;wm+LdwA##3wokd@}1E20yGb-HmfvB;dRyTuj -NY-kNy7mYEhxzBsnG^l)yYEJ!HiUc}c*u3Nd~OhUfEVybxQ2%MZO*HAZK5=N)ahEO{vSE^q)C(FXU?1 -%2R_W3H&6II_+7+4_eEIvu=Ls!lopv&JKx~q?4@9N{yii$dIFc`$XD{9lRv9a{V8*hm99dMGqK>A9^2f -VpfbdBNR -qS2JwGViXBls_$Z3I7DV=@}XZMp=SF8_WJJj|H{=W)h7KJ{{{H^dt5 -+|9o80%4^`y^;O`&n2#1yEa2n*9sMB!vDelOrJh|IL5=CJYqh44r&W8z4Vf(F~cr!FB*6t_jr*nq -o#tqooRUIKIV4L@4APj-;+Q*8Z%~$@C|rYRqiW7Uf?z8hEFamEL?S;bBp;g5BnP7|4YL|=D=&#tf6Jg -mWjP$_(tqG$(rH=&MoH0JTm@5mq4$~!C`x{E(siq#QtE_9RkEZ -3zm#??kY&Y3Q{j_rB%73j|wW@^QaX1{8d5lq6Sy|$HzUPt5%*=@N^z`l7+1Wy#GKN9UY}1GI@TjNl+_ -@9`thacKQ>=g2VTV{7Ltk@v4f50@f;lyjEJwc%jyc -H92sH{$t+vlOsfqbB)(vORuYAU@eZd7uJ4Q_hH=`#OqGvFvzKZH~bbF)-cEy*Zq667(?GK-ct8;u#bQ ->8~6t~AqL53P`ADwF7hqpd&u>WYa-Xhdc*O~Y%vDb9NifY$nS+0UJ!L|_%QI{RDK3+Oz9%llgQnXw`1 -LawbSI7ZeosSPZF(WzARn3ROEwrMg(;=$P)VEnv3t0%OJH5qw)*jXF^cHVftEvP?CnmHrT~?N -c70W7~hcKfHHvZP99M9?7;yga19P{63yqq0g|5~eT;pKW^+WZZvFdfAO1Xlc<0bz{J3|o?TGD^t;nWi -R?F;@**{ZpcM}Vkg7hQlMd`)q)iOLX{4#g|?VjzC?VatH9he=I9h%)edvo@Y?BZ;XoS>YfocTG6a -?*2F=j7((9*CjT-#<_o~^(Jz7^T7*op<`+%r8gy)*qX12cm%y9++P -oH;BrAu}m6C9|w7=Fj$L`@d`d0Z>Z=1QY-O00;p4c~(=jc!Jc4cm4Z*nh -WX>)XJX<{#RbZKlZaCyajYkST}oobCD$ZZmt6 -YW-?;$3TynlFv524&jHk+4K6HLl!I;mGKs -&WS3^E{m(pgNG()AZ9KZOax-de`LHxqedLaMjDaJXdux`jON@-Au>yLlw_T_3fW^S*Y)Md3l)?m+EI( -t8erB@uZy1vs`0P>2Gy8xlWsvrn=u+3E-n1*ZE3%H%aGBut!gJtE#NFf?zF}SIOlp*$RphI&za%R1L} -pqu@hXw15}q^QxSrwML`9*I%2Yx-igb(%Ibpeb?Mp$$X`QS94XR$rO9}6ztR>EM~L&u?B&pZQ-c~vRS -&qV-c%*-()oo)-JUOqbjeFrb(j;MRijFoN;qMPwOFdfxZue`fAZ+`7o%G+i_OR7tJs@qOyDYLqf0fnI -w~|bd0Tx>vB;|0LY@s%XwPRmw%QSe7S0xIbbym?q+#4pWs(Nv>J*vjLO^NFVx+ -2x{2ni8JeBzXp`HcgAB2NslXlB~e$6RJ8-sv3WNJbXukFbw`Tn|}*qISejpm=Z88dO$6TPm8kQt`U^M -$_lb@S^*~sFUOZ@^NyZH<8hJ9((xEdt-gb9^Z=?qW}ur`h-Him;b2M`Cl2f>@oFQM$p-Z#4+j`zO -Z+dTxpRT|4&h^p`((aGuUX}o^+VsKhN4+ksX?H(TO?SB|QSmpVPw$jf2`-APn-SPfGx9}fFU;q3S{6G -5X)$sK%KcD>TcV{E1_I~^Due*odT3BuHbMzJdYu5ZZD}FA@=Cpp16#`|yi(y%vp7YD?58rg&!sx5lAE -2j42irTlZlUP(NOkTA1m1_gjCbCC{P5RKv6IspiSg4f(ct-uwXHu4H@^Pezkc}B@1OnSzeYd*@vGC5( -=#L!fLU3YrWZ6&>3Eh@*D#>`XUx-Pz_WaHb(Q`eV8I{>xJt|m%b?*cOE -3T>7kLBKXYplKF6MPKIN3aNiU=IyMEQO)i_>Bn4Yt~{X%66#NV<^IRFVOE9o+*3??`Gz;i7=SghdS?q2b^P7chz0INrz+}R+KW(fkl!yo6^?mQX**VotC99pVD8s -{acAS8%aS|GGoaeKoS&WbC1ak_h@ugTT2{@FYpMxjrP4y_KzitF#kz!h6o6TgEd29J|n*5Ck1VY#?U> -#Tl&2Op;N5)llmwXdtrYW6f~hO<^HXAe)=<B)%(AobWcCJgf6S!MrV2DwP`G?r)mvW!0c0x!BbggZC$f^PZj|KjluF23rO4ID?CIcLTI -4W`2vjrW!#{lg7!eUs71pAnHmlb_Z&K!|ZcSS{K*z3oN3Qvb;Ne&kJ%K;P$@v2|#R6P49rypxU)`TdSJ|6D -_kae<)3;w8YiNqIlQM$B1BVW1`hw_%|FgPYfapa5sJ0hy{(n4tr<|0-paw -hm3Y|Y`-M9ora-L^kPE?C&8q*%c`Mks}i!WXd`RnU9n{Q$Td|IY;5xQWK{5H9-dEE(GGQ7hzp{jtt^7 -~*NMlYzZ%0)h9;yEx<1=h|pgyS2OsEO;z6|5%I<0dZBCNC$0M-A0pi=!H<4w)a2(|yK*1p`;(% -d{CQ(H+5;u}?ixAGIQyAHDwOu&m%(TFz7DKWDf>vzX`Tj=KO_+#_Rj&0m=2=u4Xp2~V1j@uQl@>C$?f -Wxm+`=+rTmDn0SdPuCgesd>`1Iq<`B{^?Ql=qdrKUt{4AtWHQL{fCC0UOGJ) -PP4V(0o4t7ok!V!ZiWZ^ab~9K4`ZrQlC}M*Fv0%elvf!B?ze)T!RC`wu^RpHll^}=a$>ch@gn4+C+@N -j^OJA0=Hy)Q-VBE=7a7z-9P(ra>T-M9LrXu2lwU?iL`6O!n7Lxp$DevI&>vh#DzZG*4jXmIB15{f -$2)nZ#~T#fEm{wsiNou3Yb!*`{qcTI)hj_x!F*p8`Ly6?g+hk0P%-`)~89;b)0@-%|HUYQFlc1>0nh; -u@CZU|y+pkM_l7=XD5bNM7DTIrX+!%+AGFwV@^Pc|1XlS9cI5E!({JMOoa>NCo-VND4;h=_FYIrBIFX -fI}?`n-(oPsq&0BggriH7?94)sy&JTepq5KEevpWwl$u7D?82xzWTUoDA5dwQ#BD#MtrFE8{e2d># -YS9n+QR6_Q^0SlC};@jkr|pVb04QbmUWy54vP=A?Z;K|uoZ9sg#n=sy$nMpgvv{E86NsD;TU?uh>k4K -}JrdIJ0TkqJFA1_O+~5f~yvbI3^WRH)bFY+(G9);OQ3c3eG98YfF)6+YwJo^3 -NQ+)Mzvo%ak-IvJWm?1iwP!vwRY$WFwKB9e?+mVVlFA&){*9_9UTCPEDC@04suz? -SSE%lFhU!~W->k|&V;uQmqrIB#-GvK)-{N2nG=Nu6bqf?X~A#XX+i*n8V1=$!B6a;Mf#m5HO5&jE-td -WA(cbUA=0;GnpY{jhKdSbVmx=e5LAmB0#DBCF+N3*6onTtBxjYtVid6FG@z<0{A?kn8X&sr1{5rx^(^ ->~kyB(DYT3i-jDqZ4_n1sI>0J~$2mD>$q#W2%G}S$6D@||lx8Cd~JLva(y$O470k{X@%QGHLF4`~8$h -`&N>ir-{avq)fm+yn@~+;)?_I5fdM(szv%vd=c(^woogW0@PjBdQzdAjuUvL!+c6iW!n26VtnNvS={= -2>ZE4p;zPa!Qa2z{qWuR-S)>1J8yRn$75J+ib+c9+MG4N{2t>r^RmDoz#>hj=pRXH05Q2r3N+;~hyA*ONLx^{$>DEMxh7)kn2 -A9foTow2gJ{+vLJ%ELtwl=@wRlvm3U;k_3dtR0ef(m6Si+k&|h%1m?SmDPSmDF`&x==xN6o&jWn_wQe -5E2Lg-2%P|z62e;DmZZ4c}CSZk9=AgRbvDc#R)6k(iBK_Zv4c{5ac{}|Y%0{*3-m@&T`p(w(|;UaLEk{qDkea;-3n6r=a@1=mP8O5Z;YaRFm{jRj -nUk_MK`rpK5RGkOlX56sw#H>5Lqdwzq#2*zkFV4F2qrK_SGMxb;01g!49vJ`gBd6$b3}h)ieQc$b~Dz -0C=^H-r={h~^QkXF<^*1%B%jh3ABSOYFghHS!4h;AjPOpAS=$w4c+n|=GcHP0yuTU+uPxOK*i!;M(FF -z62^dVvTl5n!;UfeX@{FyfsG>I)>tBWg)v!#@V_2FS6q52;EH!(z(O4lWwJ8!~kIPk+IKJ`~|76Vv3BEF@iNc3JU2zBU#N5$1y$fndhS9ipo^ecEDumg5cj%)yq!FQCb!*U{!M*c`OB0&y$9k0$eBU@@#PtugwZ)x7}VGx&I7@?OMQdD?Z9W{wT*TB -$>mi61Z{;9DH-G?L<(5sdcLQ!V{RFX;YzXqUyu%UaC9@zQKdSdxy(Qdc)*f1|cC;COd66fV9yLa> -@yWCLi~!fB@5rd?Y=O}8Rq_4ja^Jb+ls7}KeZ?GV);5tiUh0?;zT^AHq9zU&}K^VgO9Gov+F5 --_X)N8EX}7-oVfu{k>ti(J{}fVGe9Gan(5#{JSJC&#Z=Ik+)i0l2VRBmJ_O*Us$(PN$4D0kaE~AhA0Z -s$$+;4=W8llOnAW40Zxn(s;YLylkrdTemG|H)W$R8^f&Hjn^a25Vuve2DWEmf|o1$Vn2f211mFNG2c_ -o-*@5UfavSvJvP>g||1vaNKq3~gh&(LatKka-b&kiiKI2F-+~MYuv2x-$$Wd2QM|GUMQtf9{JAw!}6sHlwz`NT_xgm}>EOELcPP)UqxfD*|*D -pEW25Z}Di>czn{WfD6O}t*cE!EX8XEa0#ph4f~GpA3FP_y|5C(5vn&K?>144Ao&sKbl7cKkt&TY1N7q -Dr0kx(uVJDar^?uOSMxbbtbiUNH2%O=WPQ9TVNB?J%q2!-F6gKLf}JcX^n38&Wf+=Mg76kkZCS-Z@PW -J_f!gvy1Q1QqipiG&U6}{*gMva)=x4iStS*#KL_5_K)P{B*}T|;; -RCe87K%God&m1J^{OPg5Ie7KHz=0bf)kzC=>*Txa+_R*LOHLq?sXw@I)PAw4T*h0T-um8Dm7`L=n4j1QH^5#F_*skV9v_QQt^J?u_CV3UsDKrDI~J-K4s`q(vzIv11qUu -I>kc7x*%kX?>m+|p6)uI@En}AN@%4T_qhCY9@UxRhJsM0CPmd3CS=p{D;v$C -wFktt%$=UCEhlwSc*LdjVyaDt3c^8nM>-!4$99#23iX$;H^BM*U(6AeVj;Oq*aO5Eb&m<1g~P@|04t* -#p?QuI49aCHnFW+R9)qZycb$0-QQjAP3AJsAdA8>OkpDFP6;d!C1~N!3(n#lY0(5-{=az-(ae8GxCg( -1AX%cNi84;Ms_hQCmr-Vv^M3c6W(86`U1JoS&bC%g -ic-Pk<02lE3P%VJ)twy0GfgshX>8D5}JfkuTzT-20PrL+8_d(+>_Nbici7;RhcTiT;xeb1KEcR#@N=L -Fu=~rj__Xup&g$H!Um?i2y17@QVdAsLPEcH{EYR$Tt0L;o1w+6rC6GO8cdxQlM*whxTb}8h-|tNXL|A -Z?W`+48;gS-Y@fixX%hR5$IF(HVr$psV|G*(}1(3a8M!w#T5MHW3=Z!U -g1#&ZYFj{o%0%*6h?^9<8&m{JmldPr_qSAAN-5~QL$Z8V+9@2E+-qgu1=1{aW4O{2p&(-@=4c+rJ(Ax -8R -DmM%@jsKBgpFT#I(AAsWXE{wM)IiWEqg=(K;BY{A=wB*aA1t*(*f9y4hUe|*6wVNRoG`K&=-TT7hKA_ -4E3{+`k1n3mEsXlL?HF>5XiH76=?RPFi@;HBJz=balxtuKK+@;Y~h{F^39t!!(g-g0zO0p&0oe$_8KV?>f>s8_7oJ`SS3@j>^64=@1N -vmHN)C5i%bD|0~X -e|x&$1s=wylS(jjA8)*O$18tE5Ihs>H}r%u~l8pUQA|(wm92qJfkad)E9v1E%YYsiU=u!0WKigX;rp* -qF8nOR>>w0`rOsa&}Pv{gZo|C704$|6XvfSm=qFG~=$8G^V2{Rg9t?!YyMwzP<%x#@Shvo_PjR9EQAa -+$Ix*h4A2hvS$>058}X;^4(@nLK8U2kPyARE#%k(H{N->yYtuGZ^m!8kKS4>81)nn8{;?BO -XxA7Selr_=BDbVGih1+2>74Js@fUfXkwPM=qrFmF%Syqx310L&zET|r)m1#6h&li_bc!)@mt*O1vxcw -9)O*@-^PZ|5QO&=Jl#}JtulPW@!&msWr3)$qaqMET>9jE>m4M<+=`%3^u!~NS>rmZO+V5Ol@RYTXjT$&I}F%AcA*^4bcUT< -(RHP)-$5k{n!!@tLG1Fyuse%wq3k&4ALxHo<~eS~xfFPC(rD8GPkTpQJpJl{!(|seqNAOOj^(h1a=BL -L^U=mRhm~nIZ!kdmcBn$3=4o}6%;^k?Q++(1-?N%E#!S2j%_&i(7%h`>I)bE8r7#vdO)9nB2c}h436I -$js4#Bpkm(#Y#P2H%dWA>2rUqIqypDQt;L;g~dIzDaj+GLZ8&pBo0z?oHvr1sIa$d;DtW1pof1nC>ltZ;ua}7UvZe1BG1qyNYC-TB_9 -Lhp+O)E`}9*AG=q@`-9^HPT&EI4lOZ~LOHAbxVVi28doyKSQ?fH_r(dmiQrrvyYa(cUd_97Z#y|1Goyn}D@69D1wv%%Lr@BnpO_u}t%5C6P> -v^y}5M42JA?4_KLZ7Ym}`v8g{aSbalo&~qcDKYEjL{ki8`5AD3`6Q$TzyyMBj_|De{Be7^Dou|KvYVp -D^I6466^u&p^vRwS)AkHH#)KKFoyf`Q2_=0q7ID#p;f3M=OvPPF` -}`*K>PGU&S5w>>rPWsiCCs2c=Zq^PFRsWrz3e(%iM%j#+O7n;_iqw2YVdH) -4GK)Y=gk*(2XIU3eI%6NgRBvC7gtr3N(zH3{6OckI$)94Uv~`<8U0haKl&-3FXvrhCw7s2;g -F)eSwEQMn4_II}5Z66$bvwggc2mw5m5T6e*(}_wvv_HfL1;gd$>v}Qch(|gL&|=5CO&Ob94TX1-W!_R -mr_ZD_NeZ!p{iD6VaqUv0>BAD~D3}sj7(!Wvb#PUR{@1p&E}^1$JoIi3ps{Ea{wo~7s|Ro-P$r)A;8; -IERQ|cB8)vx9fhhd2lUmd!PECWmFz2A&QS=pUm2$u=2y49^$IpNGfjQ)dA39;1ePRm^S2qT -x-)&?I(fpS<-fnXQ?Q#Jx$XItqVJ(XU_yY_Iq>{V -rlqs0RZ0+(=RR=|K#h8$%9fGfa)Q)eW+#29iY$uCP_fU6l6!C+6x*0>g1f*w;S{_q1RCqMk4MAEdvV0 ->UAN$;TLOSrzi9xCwyaJHdtIpHY#3GIyk@B>GPBQS4$vpPJ83G0xHbGkhhU7#;f8PcI?YulJGjJ*5Wt --r)pdZ#*B1@v_oP*=Dt9WpmlS#TB${a$T91uvlJi$LM54rKE-6ot!2#zAcIdtwqN$=AH$$=6(Sehage -+RADTbAzsh0alN!jGrjxL}AA~*0&rFZKj -4IUNLa}Xi2^W*_Dyg`ZeBXhn|pHr!wy*BC(QUqbSHg;_xwTSUxdw*{E)CrQvAl&ktZVX?t1l)9#muD> -&<%R&_dfaJc_JyF16@58Ll|$J-yi8B2~f+dUCRC&~Jco9q8QK6??J#Or7Hb9|;BJ|8?Etamr{{GS$BO -!tYg_h4_Za`5-#xBDOb7*yf<{60FJp2lCFPQPw1*CPHLs{=xj`a;PX(Dg#jTA!V;%NZNgv0^?$3)MKq -u$9{{(Fc%%s3-${yk9(NVsUf;!B^ChH09ew^RDlL%bIMX+G{6lJUtJGf!& -vGfACXKe<)9%f8Mm+Db8AuCk1DKl%Z4oYwZSGM<;ssOdjdYV~}<4|AG-1>hG@yCNj><1%;eaO~N8O;g -fMR1WId$RbR4UOi2OFv+FciJ-&q>u=4MeRl2%Dc&nt0U6-VGNS>VD2Kv6h<+B{dDH7Kr;QiIfKThibR -vi339GFWe9diVBQQ0J;xidev0}Q6fn)_gu!jzn{x&-PLp0v@-=FC&MErRFexA-iLFJpLdz$SIQjvf|v -{!6JXJasq==3CIBPA-s83XveZD9a#v8E+1_*IPUUTcpT!aG=walmy$>lql7^O=)3}d9QoZ?+ps~@_bz -~l4J85s%dp8)M@yvJ`E}6jzRnVib{1-K1JoyQAu}(_zGZ$XQnR~Qw!oH9*eZ5t@doRY1FrLWvqJ|@d+TTVY)OenUoE1f2n0lK$QX9+R^X~T9&!e^_y*5`p5OLy@jlJ%@Xo|uKD>F9;R< -JGtS$9Li76n?dM2f0&qZ%qz>b+=n8rj~cCT$)qKs}%2N#$M$p+I)xE_7Qu%S>N1h@xm0l-C1X{Ts2be -B#@rye$y%e$-uDn$ZadG9#f}CwOj}ZofoPg7>1*zLzjGYL)pI%SDSSciC8?1T8CkO52X8jnnP=kKW1EK+aRE -xP-Hdsi)PxiV$)s7vbeuIRSe8f%8*m~`Bo7Xkl(bN>q+e6@>>6#7CzS`Pc8)t(%+qFz;x?>gK3byVnq -l-uNR+k$|6Aa%cG*)b%V~U&T+eDD5{&aONWC_U&FSPnA473;FXC;=_14D5?d>fPVY)trsg4T3#{7PsZ -HNw5gDGrUS)`ZIq%raf2*79c2K*<5Vqyc8fCrPhE#`AxUs|<@E6V-OQ4g-vzkxkHdnR$86-UT(Zu}N` -t*?7~!KK%q)em%}h4L$VM9B8_N5x_6pnJoPz-V)-XjX1^Ke}WwcRJ>QpW7G7i+6s6&0E$(`Txe!7J2VBSJ(5mC9lO4D4Px2h -EDGu@aI%3bS^?%RWM8AyJ-BCH6Md2O6E7Ud5?uu^Al+8`zz4komeP^NR)PBKs2nC<>1|26}7ypJ0{&U -s+=bu(d>hNR%fbSm3D}~7d5>(+D!9kZ{>nCx@x%2W}Tt`DG*M8okZ%Si@}>md}(MZ7I>&VAf*bm -w|8)9bGDGZAQe!kiR&g4EZi7CT@vhLXS>*Y{IVN64%KLn$UbCz;R_wbj}a8=w1*$-JTr55FC;q(--Xt -nVbDWTb<>OqL`K*s)kex;)|mI%9nh~wOj81bn#6Q4U!23ZTy4~gb4+A&uB31Gu5wG@xp8!3UB8l}ExI -16$~+0u8`=(Tiw?!B?>U~Yzi&_J@^xK@({>jOtSdz6v0)mQ7e-;HYzvet7>&r@^c1!D2+eDZZo$0@+{ -%X8u+05HjiH1Q+Jd>#X`1`J6?}ii$%$~D(<9_on5Fc!>EM(UeGQX5rxV$21q#4bfdCE(tTYBtMFoPJ1 -8It2?vy$P2xXiTe(>ak%1~Zc)SRP}7S!1?-MRimk(3&MYt0F*Iz6vHshQC>^q3PSx?Y}MG*(^Bvmyj~ -*tUmL4>^bX{}r<;Y4M*L;!d)J@Iz@}fhItO+9`ngxgCejBD{Y`=mT)H8( -TP5STbFV@gdD!M7{M08cK8@fhskz=sP||49YMtumF{jKGCGmwZ_`In6LZxS{Lxo;K@#T`N89*h4C&ou -kJX6HcAHa(UL}R{VGs6`fR1SB`p^;E8HO*LH8M{OJPSvofeOqUztoe?k|?RPC7x`w<0`4& -H*{``V4xekH0>vhb^tqd*B~2p$n6RE88&^jF!DAv9cB%v3?_)rFdc4nkYy|vVZzecC(I-1MBkCIk>Cl -{AA*NQGl6JD_=A{#|vYo>Y7rpgb3ya0kBjis;bU2V0Hi(83IFo$f8Xi(5_u>eULKK%e3yC3n!axnrfD -2xXuD?G$R)WEJi|vVP+?6sBozve(J=*<;$|?*8^R2@baDQP&Y4RC^|7(p7at9vw2j8_`?eEHP5QMp -$OZQ((Xu5GvDyZ+6J2C9Su$2$3EY?a8Qiq_UdZ=1^b_doe8iC~pg;8*`bMoJ0d;x$<~&WtQ6 -SItS|`TIHpKe923#_J=KXSge~4(RAq5F2KpE35(Y=tTEp$2t&I)n= -52PJ&C_X?#AS84fxkA6lj@&I@$A(X6b=WV@_Pjgui`0Zb5L2qDj4es*LD^c)AVjkr;uo?&b(u@ZZKE0 -HSO~97F~#rUPnOeN9HWn4=<}^er2x|?aP)Bf_mhrcti)B@p6J>PU#-7W#)HsUr$qgsn!g=Q(*dGKr(I -KLQ#8i$d67E%_s^5<*?Y<0NjDnYr*a6ZihBS25wx2t|1sQIi>LtUTdj3>R^^i&V@R1cz~ot3q -8x?JcXqPv0E?*8rOULS*jjWr0aot%>f$pp=Vh0W9=Ya78TIkqn`VC6ff&hrLbrPGgS{@1!?2&t!Mk!)e*c4AZV -B;WXoQv!@^Rk<~jd`wp1wULJ&L00CvF}xvj|_zmkVlydG)KZyXM9< -#@LDnb&UWO#P*>OMvdDqWQd&eG(0%n@N6`goH1=r{Y1huiW|Cj>mkfh5Mrs;M9&`rYVs-k?T3qRtyhw -4G_1DFH;-#kPlxlRO`i8Qi2xqkCeOP5R^X+-6|;pp{Wbm#vFKH66#hm$=3>kIV(G3er;KD+?H;tu^}p -F1FD&p#1n5J@ZtuzP&X?7U6j(a4OFqQzx~i_WQvv)vDyt!3>>~wTTea;Aj0PRBq)5sDmjt1l9Xf!7cX&5PS|S(W-%z;6z0C#KHAya!{l)Y6}LFo(J_;!318BOo2oO6q|F)!3g)XN1z^Dl -%N0g+0DwIz3>tv&%F#^+4Shn=QGj+kkc_?e=Fsl)B_Je0QQtDAK4xiVNd2>Sc~h7P;laQ*Rst&3N!;F -wEbmm%Y210)2bSU##wxn2jnjcwR@fJ%2c@9yB)}}Z_1?bZ^y!@D%F0+u$efrOE17PI-q0455;W>m^6K -p>rC0ya$gfVZn{=r?hUZCoatZ3XP4eqb7tD%k4)&z~zF6eB8}`L*+0#*dsuYW3i4*MERyUoTTtV-gxz -76l{;0CXBPOhHd82oYJw;IBFX3?=oF^43j2}Pj9kY&zE8t)8pZECBU&3K{4F4a5ZT#$k)V22>7qX~;r -1|*auOIfm{}A>@n$9SL?`RW~#oyt#mz$g2T@QYzG!j&*6ab -ORZzZ`dykQ^P1jm+ApSvnL0qKySu>b4{BovaJjME>}Ezw#QBdSDdR7Z+?gccl2}K*wGrZ;JMds&P-bK -xEAXGlS|Y5@dvo$Qsvx5RhLfq6o=fQyg_5?^b4+TTuwmeD}B4*W>p -w~jx~laOgQD-llRjc9Ca?(LkX*`#K^#HvQK-bP}jPKO4)2N>&dXkZOGJfup*Yh0cu&Lrw#apH)JK3ok -V{6Y6H(Tk0P!^n7qL=NAGp<=&Kui{k2`!Wv3e`1r5ZT!SkRU>~%eR2Ej(4scSy(Y4F+WY4GeBD!Vt!y -c|ds)}6*vyd(?bP?$;8Pn$@QdW7X8D2erIS{M-@rTD1k$N~MZDN*9VZtJ=F{^ -(;6HT11nNDexa-oOZ4b~4?DihyV};bZy4q@RhiCMC$R`p+9oOr`oF65>1jOJ1IL4{aXq?YtfT<=y_D-6QI!KSd}1_~q>R; -1u1HW4bM2e7L*w@$d*8iM}c)|JXP^9sYE7`uz0y&-f2N_~onx@P6;;{q`|5d9-`%R{zJzKYluUe)`ML -r$154zt^cKvP;07+RF*G~`^nmVGDxY1?W=Z459C~La%0b)Du8A2jUUrm4UpWv4uZK>%x? -U@W8Y8V`Phme{AtT7I>rCIO4GEX*Nl6@v;+0MH0Zx%CjUmx7minu-kaM7RQ~1ShwY`+M*IVi8Xo_PCP6q;({)0 -T>??Q#W^Q;&{rNmuJq}tQ(Q3zxgtECh|W1XO`gQM?Mffs^IyZ!o?DY)x4a3AfZ*!mKKw84uclO_nVOVZny?!D#G87|gU_2 -p)4{m}HH0BB+W6LIy2hXlN<-5%cKHEQl@#5k>Ml)KBON!j8F>?;KWz_8FLN$KNA!A|;qRM!3m8j#!?b -zF_(d|^v3=Ve20V5)@=4b9u_qw&#)y(MDDCcsGu$|E44m -nXVVP7A@MQ&M%JJ1zYOpj@)Ht|D`F@VEqg5rp`$7uHDQh8t2~D_7V`q=YvX>kBk9Pd>dZ>qf1uQ^}07 -1b_r7x=Z)t=A}i^7*Cx~%iAK7)#>Q%M_{9GzCZM%-BxsO7&bp-&~$VF(M=*(SuyXN>~3DDfg4)OxqJQ -_KQ7$ZKAUAP$rewHbSs`hwi03fw8L|0MIU2I3biQrAJ4>(X#wJgGL&GF5KTod`Fw&_s2LD?C0XNXt7q|x~G4@z^NOpowRa6Z$vL-n7BVnfdtt%(@$el -9Oec+%LAB7ZZ&)zjQxruyK*n&KV^~K<@5;3^4MRj{ERiVsF+< -c;azk$qwKreAwni=8%1&_*x@u79S7sU0Ju^{Zi!!vA+~Hvd~K;drbSOVNah?4@McTv8uPONp*D4Mu)f -mNylvJ~wN002c+shE8QW?@l&72VuAnMdFRy?4fDb@7!r=6fifK{BL?Mdmwns3W&2(1lk6^qfF`vPJ+AH!P3mP0E30@xUVo{z4>2m9F@X_EgtZt3Ih)iM}NA^8pcjV -v2^m>FJ!}x9C2bkaT-AnhL)NQ$XL%Wr|~kL~lskT2O;gMiI=kQHTN05%h3CcUGsM< -Wn+GG=tjQ&adMyZJD7!EUKJx;1dM*RIe!yTB=YsX7d%0#*;)qT2P67JsXbABu75BMA9Q>mQ?ds(IP>; -C80p03)LdOM78CfMJM7A1q-2g7IK3JI}+96JXU>oj=r;lN@DFM()af1?A#RB&zICq>Kn;Ji{?$(tSB= -=9U6#(x8*H5RmkI_^6F00Bxf~SiqZ8inBMkqcW3|b8{R#QbvklrhV6!nf=^|1rVftoL+2mJiTwfwUv7 -)DhfZg`b`^(4jGkuO_KG_&>I=YLTU>NU9CZ2H67!@=+`a*Uk4va3KnL>iN}AsbtTYX%k}lN2PQ -76Cj26^=UOiN5vm_sVhyKe>f+^H{X*ttPhSN09W|%)N+#*K=21E>1UntREgEzxhDq(S(u9wCc&$->SY -H~=|wBWH01#Z;<2M@H93wUb#U=J3<_)g4Jw9Q*UI6OQ8m|0IjJ1xRNu%;M_r_3*AsCtTd0J;gZyurax -`p%d#&(+0luAr_lm-@^&KB0+x$T`-9-2Ll-X9+;J5CbM3kWT~Q*INFasPdf*mA*;w)yO>+#AAS?R_nV -jX-|#8c9mJxe1A@PF5;0~x~>DeC*)igZ-s#BGj7g@XU@4Zs?_GZuW#f>WI#izvqw+(-n#NTdpw&Iw9V -)i6qQ21jLi)9OU9*hSW7O)QlUQW{aBq_8V$(KaU_mMdgvGu13bwnsRriQ?K^wqz0DhYO1F9A7h?{5Us -3kS2j>9Ft3Fl8GyYgSymiN%j>@Pp=O2$9b1q%pDz{X5>%^cwX0WQjo25ey9su -NthKAUJl`ldeMJKX+lkKT6Oc@Ga|wQZ!Iv9;O)^tt3z>he?E5l!#TY{B2I3nk-=mWfeN_O6(trrj?FE -dh{~?3&)}DG7(_YUyi92-eBoa0Q2^A)%RzLaKxceEbw(j-XRh_wX$VKiFK@zVz*p)nlPr4c#w9}eq%8 -#J<{9@x83~u*(!w~oKI1)pCNunejQ!l{iwx2-OHnTpA{*le5rHyhTSzU429|ktmDJMb1>`Ul6|Y~keY#nGtcC>? -NDG9mWz=w@%&5Y>;oxoRksXjEZyRyylwNVtE=kbVlB^ -gqHL(XwXuPeZ^}uT$L2Nnk4gPp%M^f6(${-i+iW{vIM%B=Nya|&b~X;2Z%c>BhpODLi -gmG{aPPB9%(RUQdF&l4+Eu=Nsv`<7WD+L)*}J!E -~N9c=ciH4!?R0t2Bjnoux_9OJ;~OMu3Mzg1zOUXlx!KVCrehX?IbcHkQkBIP3`eecMl)2oRbdr`SQ%} -;6=M?j2^K*s?@&RxgNi9OG!T54vW=ObbwC%+O<`>(2P14`J%ouNaz~y_>91Ckq57@DLlDdXkd!R`Y-ZN{@x$+K!TFLA}aLcYK|J&)-drk{Y-WMtT#oS}RaVN#4ZdXgY}Im}s6eBK`PeEJg?JJ>j8*94p={lvBSuqf-(iqqZRJ!$XeD;FxMwZ+Y>S9y8HCJB%iFe#p6Fm*26Kg?>*I!f`P_RPqQlUDK -!fach!woaTz>-8O>%slOp0ak4NN$~cWa7(R-I@%(3vynIVCI6i#OH$X7}6ekMA&C`e6TP?|A?4?}2ftASlbAAjC*!8cx2Bcq4 -SGZohkvwXj&fReMoLEX1P9-L)I@1r`32&fSZx)Q|LT4xPkg!%_Gzbkv41^MmVmF|WZc>{t4tKU)px^_ -Z!2oG -KXQbj=PY?IHtJyEsqd~2hdxe=nEWd2CfUI~b&V#vdbTEp-c2=un3f-AJcw&qILQV2n7a)k)r^7;91MmD%{+1<2Qt*>0H(-2~U^KFXtuRe(qrdN?{8 -wzLdxi$VEQQj~Vg2pMxGWoMj6(%Yuw20EKTk`{Ms<}?iqqA099Ok@zl!|7x&&UWf>SQUo)sxgM>P3FC -WVAXph?g@byxjY&;@eLbbz6yy@fE}i%hVQT5vl7_FX#Zl?R!&hf9Y7?jZ(I^UvUX9YEw+#rnXIQX45? -GW#iMse$f8J?p{70`jQtbRD*YpWwBgmzSWCKeXbN`BXCZc)V{sW)4K(%FsXi%ue=hk{L>dPbiuoo*>w -}%F82$x+Sd_W-Fk#9+2ODnb%4-e|`PBuf9QxEu)7$rJV(;7bZ&XEsx~xlsE4{@Ome$TyBaQ!$9@Ry!9 -b?Og5gU>W~-uIEzvo9Z;-Vnm4PeU4oW}Wx_-vT%1hNl6!hg$4A-bh)8F2T%zP2#ASfY%7jkJ+twvECCMQ8roq-2v>;jL9hq@xMNyk*M+|r<6keF-J -@p}G)E}q>aRNm17yoYQM4PK*Z0AXX@+}%{=*ZOe8T4zOsf)yQ{M^gUIN1#NTIL~>y2e1iSyq%4z-?~R -a#s8pVCBIjQFI#7Ogz~ -Ba*c+k?D86$2KrCvAYoQa^2Jn;+&>aO3V>6&UlYEtmcUTL(*|0_qau;;N1iczgkW(l$Z0b`i#N%E&Wp -<{z9Z-^nKY(bn7~QfQw(y%6wpCg2pWF~(#sfszRQY*M9W)YB-(M%J@8Uw&>sN%Hws5=39?30arWNLH`Eia%ek7sY1_ZsU94%ksA7>yK0@8K$;tX -XKdt|VD#CM0h&b7gq8y -(q^`4;(nfyiX=5SD?)M&%eCJA_oUM#L@9%NO;Un1 -ydm2z#J?Wuny+Q4M=aA+8>Kd5_v*Tc(LFZ`6|s*-mK)|#hg2s@GBj#?@lC^YHo*jQY~o+%@a>d2o7KW -6@(k1k!71tTvJ#v;GYVxdJbAn9Yn|is|N@Iseysy_0=lRXGWG#p{&)ye6VH$AwWffW?)=I=@86=k^0s -a3$WNYbStha4+HT4ws5lbhAyZHNyLTbP;XP7a6TVMa(9D9mAwAcT!^DciG6r;1-iAqZfbh3zx>=8*$x -9Se*{Md+lR~rU%OlotUjyPpVh0F!cUE*8Z+(;89+{VKpdB~Ep=q*C1^8z6K)w38ZT)oV1(ab?IV`!ft -`6ok4-3YKXgO_fyejHrm-Y&gu#v4b_XDuUuzybd9~#P%B%(7OXxQps0+1nU{0|JqxeG|Ae=Z5&daia# -XXthsGR31Xcpg7Ofh8|pESxPhgW6eu~4TvdInMJsTVb<1T?R%DD17aPrAdJRqz(=Pp@8n_QhvkzW)5P -FSf$huiyo6+JC=#_4@S}uQp$Q{^gs`zWCzJmz(gCR=UtOUKI~xB)JexK%e0BpCxy7_M>sz*IICaD_wI -{u`*F&3gcp$R8twBb)oLMf?VN=i5fHaYDL3hnAglONoI0CsfJ^CHdgu1#B9MwS|&N1YQ7WW#A@ktZfS -CV3Co7jSj$-Q$^e{?t28i^LI#*r@^s33PVktr@V9I@%VV3mS*jB2tnHmgDp}jKsg!8LsXQuO#Me*fZEZ%Aa!QA^2gW(am`DnXdS!*^4-zS`)|h3!SV55j>mY#QmFG&_JWZFjSgWRjh@P}X;(k#Ywm32{{v7<0|XQR000O -8`*~JVr=>FEei;A&*;@br9smFUaA|NaUv_0~WN&gWWNCABY-wUIc4cyNX>V>WaCzN4Yj@kWlHc_!5W6 -`fV~UYvr)jit(lqP#W>4$-#BTTQ%Bn6!LNXhQR7pyj27|%8FyKDxKI -yU`4Wnc}WUFG<{R~Sw`<;FEHeGJAXns{N`>n$Uz5bvJzn-&;E6&a~#Z{WH^K@2R`x$3nrmH0MizrPz= -y}dL`}*D6lhgAPued3gpM>n~x2G5H-+l4j#kcRzu@KsI_V)HV?-t9LFL+W=Zr5QUrIqM!B_1sR*i`BA7(>U)2>0${m5e$x>^M#)jQJ}%}2!T$tAUJV -4pF^Q~&3LCX%hCn&1Ry5M^CE**lVY>vxoog1qF7Zg{VeB`jQ_NXGMZMWGnx2tJeja#HnL9BY~jbz&pi -BIpkcX8EBwYUV49ibc3Aaeea+{7u+ch*g7(Jea-LdqRn*?$yF^m5#OWl<(hNJ!c`*@mx5k|g5{PFo4M -`~vs=uZ(dooG<1)ofuPNx&Zex9=}Q0?#ic*W__84?<>1Q%CP&fuS)uvOJfEtH3gge{i%JwX;JPG9ttX2@a{uhq$*gHz`n^>Q=*RLRKO>&e!+)HwHxF ->X#uRD4HnH9@-)>t?9s7!tGBAqx`1klOyDO;S}+;{WWnQ%Hr(UPVvm_InvT|l|5PqgeH5_<4a7yWiaf -B_v&3AmV*yLP>%%2{>u?ef&OrbZxXIx(7;*8(qx^C!#`Eo?X&BoQWx`FgV;~Wm6ESCUCpGR1*1-6_Q0 -Mo9Dm2MeQ7nf~pN46WdrQ(gUYgCHW_-po4x8>NC?0>2hCJRE(f~kR`T5mkqM;@V1a~*E!b)3P6k}2uU -uIi-?mfoIJ&J;m!VIPJm7#8XnyH6)J#9E!HW);ozlpM&;Wf+X3L5Tbmt%zz9e4jOQ_%a5}b`Jto- -&poRWw%VF;ftar)k2~lC4p~qd4agE+vSi0Q8@adKi`(iWpKwDEbA5BADd%9 -lw1zD97t_#ZYnKHsTMpS8c1=)eG3!-pn^#7gm&dJ*Kw<=lR|Di-%}waX+nXD$?$d0aO*go>{wgk9rkO -Fvuh^?&uLAe+VK_c&Z~mwp{8zPvg;L0JfmoWv8&;&H&E>}h&k{gut?>{UaJk}w$Y&|ANk+z+sp@p|s7 ->t&_HSuwY@MxY9yA$k9g|66I>@DMs;sg}%8ijPc@WJuKmiGbDaER~-piQC4AY@L84Y2SRZSKuIG57!{&91Ma;A# -t;YcT$$KLQ45#mPY8lzrfQ$)ZA3NEOotW)00j!OWr?d#=Pj)b+WS|^pRT+u0f*;u-X+g^owENB5b+nP -!;s0bu^zM>0xPA_-dQt|59C6^L^oMuFTzBH8d?(!cvxOJ991(MX*FW!9|Ob&;dWvQaFNY^=<~?NZ -G`QamQvWmgp0V(~i7UAx^F1#H?8uhz3k_!fdlyTROPvXDg978{!1_(3s|V>NB@9Lz9f;%XS8Gq1Z0Xm -<5wq^T?&^~Uf|S3YP0*8OQUM6F|gT%Uz#`N#V9rT<07!z1ND{#>~I`M=TaC#y!Gj4w6%|9Js{f -4^%A({tFt@AOq4XMet@+BrKpJ38u3zCU??{_fk;$@>$uNjb$)BUsPO`eFUrz6=i@IrPWA^x(Ji#IbGv -c6uA!W`Tp{j`R9X8!NM*x4le370NF79`f~rARO2uzx(r>?*CqfV?;-Lb6x|GPskr8IxP8ef|G;Qu$5G -9+WreDX;8tFDq1v=LT*ua*S7^u$@;HZQA4@gB*=M|u9k?*!V`}Yn`E`9>;{mb_SzxEwno7uT7t0g@?{ -(qU`<<6dV_HZPAAGa1b{J1A>Z(A)a{RPbM-3p_{8h~wG)f|o$#snS|B+7i3B)`V9w}TdS_@@GnFB){3=r-Vj3azk375pwCP^#fUD+0D7EUi_|RYab{Vq`n&tFc_& -Z3IISJkb>pl$;3QDxO7wQZ_l-_OBQo69wr!^OxWUxy)auQCNc`O?NOZL3l4%2<~@K#2c4QR|Sj^fNX| -cMO#C7Q*zk60Vl(r;6;E-R&$mjifa^Jl!wtA=F?)E(6oV4eYtY*TCR-OG`B_A5ZQV@Q&T&EgVK_Gn5d)F~gp{gXk2709*m|u|B0~UBe@kxC`oRDlQ>?!>`gbtmfAD2HN^#;0sh2Mw($w_Mp=1?dfg3p8n?djk-gBV7^al^+E03J1XB -VzWvW{hrq(H_T-DED``M~;p^CHthCP-Q4%dy3szjGNY*J#)4;B3sWwH-*F0tmjDB4CYZNJ}CI`6%D`2 -9pXj$4<@(|}<*Oh7RLZW~tS9^t2Ck&sM9}LuSX5#~8?M~h`;+%4Ur#RHe05GoAtz!*?HiYE<%yT1OH>J+GL1x) -B&?zH{Qaav%6^)}(TzAt%zw+%dfP<###t?-!qlC;-AbE>OQ%70y@7A-dnh5-p4z}p@#*!qh++9mQv(j -3`g;hK8QZ;j<=A{nS1ouj>nj2NedKLf4Y8+arIzO96AXu`ERXART@HbilZ2uocd= -7Br__BD{9*0~hGRh$(eBo+`%@S?YaI3h-)#?~lt7lH7UrGiwWKL9Sf3ddhby91C^eFtQW(P3xUHE)Q= -7Us~nN$rRSaZ*dGGUjHm=PqzManzJv9cwM?p}p3SWKE6et7BGHHs%aPEQmGl$23ap5lYuc=$wcS%#3d -120f7pOzB$=A4Dq^m8k_^zW+A1wJRdFMeZ^60@UvSY_Td{r8!H-E9nJ-$=q8t4DIu) -d$H-rurH`*#iXTZFqjsWWRSmR-lN2HPwU5pC7@_iBut+5#Nbv#;O0J^AM3^aA}sn|j;Y0g5&94!Yw{E -C*ad{MGh@p@~Hb)@2z5Sgw+w$Q!^i>xYpywl6~mL1k9YyX@D2?lbQcV^KZ$<6{au*osZ808V$j6R8R- -pij6PsY~1To;a7zCjzZDq$Dg12q$0#tB76=(At(#>-A;+gieJd#DRp^K&tG~1}?MtdMl_0f`f$||1xl -YC8BxlRKsXRTtpb3&Q?i|?j?G+P0TK*wja;@TYq`wJC{>}>BsO=!o|Q=o-cWfiA+$@4g@2lRDBCi^dO -*h>X=zj@0laCV2=(_=x1RhH)U-+d&&!6uCj&C_gZG$CgRGp(Zfcvn8wxjlFy|wXwY`i;aUCQPk2 -C1U39;so>P9I>bN})M=SeFs{ssz_hJ`W7c1~Di~f}l6^%~I4{Ih_wUNs#29OBrDcS*1%K^)yG)T&sH= -*6H4_!mLc+4f!K!Fc#b!r6J-*s(Il{24u9QVhrNpb_ZD|aF{c_kiRDue1!qzj)CA}knh_vK6>N3E63QIBr>rJBHG1ml8ekVB|YssEA6nL&f_f{(>~}^W2|{;Fbhj) -;4!ji_G6bLvv$^V#i2Hs3TnMNt$^J7s3`ol65TvLBV)&ZGwixrWlCa1`n1C`ya^mIwnz$vJb?O+CaO~ -&uY$uDMFjlO&6l)rQodEY5DCbOaR0yuEacA^UC#>@jXGkcQdw0S1NL8HW6n;5k@AfLqa?9xYrU@M_@9 -H$>Qh(3MK~!PYC{kE}mi*G{)BftvSbVtbUMaW7axiEsp?dr;;S$#F^g=b$NAAN%svY2J6e?Jye4^r9q -aY5w{skf{0nVYzoG`2=3#J)oISNb$NSLj-In%mC5reU=s8xp#E%g_ReEp5ip;HtHlz!5TN1;$zO(dS4 -7UU&0BaG^m;E`b~O0xIl{*@2C<@DHaP4LnEmX~dvW+!9+kBOrBR)s#0R4xnJdqTbR5piUll3z1Z^RiN -Ucqm9UeVDV)kc!uXj{}zs3bxdO^rV+u<``xoDNGp$Qqxj82Q4C -yj;27IvG6}|5RLMp1?3!P(Y%s-wrvTM91j7>LWTk|mg$g?-i`W(tuB9TtJX`LD+Usz%tIifZ##%C2sG -7eM=R+BOK?!)HA=eR9MF;19V6nAf-q<)$)bE_8u0t!uC>yoEG|}-d(zCdE9#dkhZ-(L4oA+%t0!UyVF -m|kDa^XKhRqa)u&;X+2Y~%cT4)bie6j~H@u7zglK;$B08X@m2i2-V00tnjTOmSnb8p%m>>(C=z6TF7i -v@6$;z9x_!wgbLW9t7D$iHF86RvOxUB-(*c8EFV0jMwnfDHZo3daf4q6K$**l -F8h}Qb2cDTx-iFXcKE!Hkp+17;2qv%gPzwH4W7Rck%vCkKY!6OupADPKE>nf?6o$y*Bt#V@L@7!0~pl -z4qbYf_FWj|h1dHilSyiWcpt~F#T1=O@3ov(E7)}pVXpf!Ednn{<@&j{wYHkPH|h_^2At9nwR*4$AO& -S2iZs$iTFWW{D;HK8^mrbHyvt`ZOj5&D#;Zau+XLls==Gj?y%&2DI_eo=D -aVby#peq{rb^m=xm=i^Pf(i24YtvX?ZxvPVmao9uULd!-b@$-k!(K%P%#6gqx`hn|(@F@mf$F96e+9b -MM)wyRbz}#TrS*o4@S8s%<3XWQ$nq^z_tXg+CYflW;4;OB)Tm*EV&yh2NK6h76!H7?##JEFhBLP+g@m -VU6fQ5@xD}JdPGo`y9F=3)aR_ -zcgL6y8weOkjOCP4?v6o7Sc|C%MNtkZgi_cx3U#^_?P)H1<^XtG$;vG9iHQ}u!~;2=?~7DEmAQqbvJ( -T*aOC&BtlyK{7Fb!9VV{0aYxu%DJnZ(L4+ev(A(n}jSYB_Osn%N?HyJX!?cR19tcbl@r5()~(-T0dP| -%0mmlgV6!x}mVZRkLA6Lr|fST;D+2qL986za{qc~q3`b~Ky*oZZz7)PRq}da|KF5X#Jky4+wN`_FnH< -&JhrIkCfU0n=MR;~pDa64gUnh}oJyMP7%nk-&EWE_DpD2Q5S&Ae1r#QOs!&^z|0qpvq8eHULTigmuKP -1zjZdf_9309Q2vp^FG}z_kjWc^H##W?HU3p0KqzVRohrW{Pf}j3c8%_EpPVJ?a3%Jx7EVWJ}7ZtvymE -{Y$%$Y=Df0}?rW)Md7D!W#``{#@TK{gLUa;MymK9O&06Qx4DGyuWA!Kk-t@_}~ -_%cnfk6#Gr*HruWY2XsL=6*C_MEFv5QLGv)=`pFQM`F@=DhSnPl;8})(|Wk$Z!X|J%J2sdQs32~jQB}v;?0V!e_nTdlfC4XOD#>4X=RNdZ5nwl3lqBdP@8w{XZ -V#n+j?7F)CH}je^Exn^eA&(;`}(@{b37XIBPj4k8`V)G)~)u4dya)?e&Dwhz=!bV>HuFb1y5;ed?K}y -Ynk>^9uET-uxIKDe&`?Z$JUH-B=~lh_MGloo@~)780w -l3Hs1b-5f -rK_QMYN&Mv^m`>lIJfu&1%vRwXj}U5rrW*<EpSZ4^BYv|1yleckq`lvAP%7`Q3=hX+70tLx>2L_$E&;-g;`GpsCC{+0cXZ0pb#~+18|GRVK3{#ir@qhx41AYAb1)D`Y4)fN|;wu_ap07CD?;rI%_5~jH#$ewig}6zD0AnqU5rp%?uW|%xp8| -91wyjV$M)sz@CmV}qg{#G4L!ZN7_T$`oi<56bLFHa3B_oLj1eoSeV?$H_M*=T --QiijjbiZwiN6{KRculiPZVR{rcrJCJ8^mT;Y#qgO7KYdQ|D`a+>3@DOM$VJUgsgr-Ysw=PTXn#b1sG -lwD~imD?^ev*+Kj;d6n@mLyJ)n}7!H+kfz{|K;Y^GS@b -X59HdP)h>@6aWAK2mt$eR#O}p4#qdI0001B0RS5S003}la4%nWWo~3|axY|Qb98KJVlQ_yGA?C!W$e9 -ucoappINbA*q?1h210)!}BLo)>YIF#a9fE_&BtZ!d49t+l1icH{adcfabOWvg5<8=5+EI5e-rsufdao -?Hi@WY!_O5)m3d}H=Nfb38i&;U9ZdPmTtOkk6KtlR=PIb>Dpzhu0eV+Gu|9N>xcURS^Q&p!^f#|QXjuB@)hm7XZp`1%@S`6z-1Ecy@89#l13%*P| -7CrCgYZE9y$|G<-|5c(;g8m>zac9tqd>>;<43vm#h=_WKk@ggJs0NBgZB*&y*@vOzSa5PrSCW9m%)4C -e||Z?h{E1I-%sP;*5QZr_b`30xcB=$#CK(#T~3BsYDi%=f9KL&iMVdYXvj2VFwCnEcvg!myBQu<`0Lc -Ef!{`kNns2$J@IDt8YuDp{RIOgJDMpV!Y1Fz7yW1HXPAW)rGa_Q^$kRQ*1!~{!|MdYO#KJ6-|)p1eFX -Kq+wkr1H}LDX@bG@`PkM#0t>(*N@&UyS4eRdV?*WjxdfRV+ztH7yP_E+!4UFlb6gFr$KmQtB=!S-k4c -~`v+E!>YvjW~D-+-@S{rx`zK;R}y1B%aOxDxKRZ*SrM|L_0Fzg2Rt$G7k^2czaL?Q}45-!6XkVkQt0C -Itrg`MxSh@u;~~khEJ%;3xYIW-^SQ@mZnUea~W6f%P+Lr;-kkR)DmH>S`ljN4#Y$;=9Hm-ttX|H~a&{ -&An!pVRo6B#f$R!DieVRbI3<+5m~ky9H;rQ$@t6f#KILVMlGE+1V3g -y*Due7~3&X+kqG9E2Il7j%S?rzb@%jz|Rf5i8o9gpT0VGo6u+-Q`{7{Uo!ye^!hJ7J`SWb$dnSP^J6^ -6-E(0Y9TJda_XmYgJInAWsFTGcB#6&2Usq@bRO8HA=+hG{8W)FFlV;ITVFGD#nK@8P=y@(X82oF$7=qJ;2G1L!5oQ7)2eYTK&kb5fR -I}6Q_gkBe^$7pA7p|jtg0>#z<@YAE^l6DIcajsn5W2)}9zq$h?7r%v1@98C#HFP8>TJRIL -!meFa%iOD@frhlplM2`3iDEzYDm*McOB8RHOu6LisAkJ|Zj$I^(V*MF5GMpyZE1=^Bet$U#7_hZu7xO -WNCnQ;NwpP=V;`VNlkrmD{xHdgMMeHxF`&*D!vg-{5CFh{~nruB4+pFd$ge+;RknrwWD&$fvO{j->Is -HfTdV9SNG7PlvQaOYey{_nu9T>SIz%K^KdJV%;7XIbN?+UNBY7U5JHr1C2ZE^Y2-D>VUjWQN --Yk_rxkOD{*=r!L0Xt|3N46ADMV+E}!?uwAQG$*g+v_MYZY6^$r*k4}ffy8-U7x~#TN=AtsRLylkSp -b}eIp(^&USKz`SLLBnYHkjs*`q?KMq=;F*p@5b&>a|-|(;()xXv6-s&JyolQ)ujdJBB2U;vEJ`o#Ck)tRg-ncq5qacH_4^06AtE;Dgbf= -PpI!AJMfEw6)cES?f_E{T{J_C&*UfjSr=J6PUCdJ0>Hx>u<#CK$dYSWv5l37VsAqIA>g<8Rw7OkrgAP -20y*dQZRkZoR|$0qK9t8DNCV-E1x!HPf3xbND8UMD-%GUWd6WVcF=~Vy2%e4L`*m>P(|Yk%MjqrRaR* -W{;h97NRLEFgk}7Y33r&UZ9k(7wt2JYaolbXr -`On8S>@-9c%S4q-uDeYHda_VuT9%IiK(E*y=aZ6Z6E3dhwy@Gx@v(0+5rf6v1MO-U5}wHeG`?{Fu{gm -`5=yw4rY%A~T^F4O;kAc3Pw&m}ctM=yPCE+SoY|I#$g!LpRrp3uf|Wf4-99pDBjSHOpOOfjQBK(ZH#= -s?Gy8Qwo9`jLJAHuI`(PllCE`CX^53EarhZKDUS#P>SK1JS*)x3ouGnG=OhEM2f^HKZOBXNAQ_$c-Kp -o+uF3Ll)MjNxyu#`oI#RwQIgyMaWz2^i?I#V$Zy!YHsp7*%@tS@#6$JGKV<7^?@8T)z6Gxc-K6B*RHuSLL2H8sI~_^vN`246{DTArO?8O -lr3g->ZW&hk|O65s_Fd5D(nH?Gw>D?gDT!!(G*vquC%vs~@llE_j;vuN;rSzm__2D*R_E|2sKzKI)H6 -eq2KinV`{);@*SzJvn$x)3ok>=y$-cW#Z_OPP>VrXdd&K>ux`nny6ui@Fd}*F~<*r3|XY;Y@G>`*QpA -nL?r8x+Yke@1N-+SztUN=_s+iI^#=L@X!pT7 -cKi1M&Xxm4YUb@2MCAl2h>laJ_dEpn4tA00}7%0fbr3oHJ -|WW+==>u=XP?W5l_BG=roE=@g8ADD^}d*gVXNfqi8@j5QAs+AUuO>~h3Z5=pp*Lnq$VP(R1XkXkk=E4 -49NWx>!c!bBYeqiN`bG5L2bkeDl-{R^+t=73T$bHcBGLt2*rR>K+(YbUN5h*C~Up`b$&c_1$ThO{_U$ -TvGO3dbui!Y6KF`>uTAs0zE>gW(yRv*@;KRa>Y!N9b||$5Bem(t&ysU9vF7l>&D5IUA^T<{Lwd+2czX -z$g3mSdyMjLLv+!9k3#pPNn+{Q`HLBbpX9?g87hPVaZu9wU{ -np62XVe8XoJrO53&oZi&AtYX!ws-LCoIk80O&Jkal<-JZj{)y=@~WrvVzH+9^iO_K}8jVre&)v$l?(; -=?cUQ@tdMA2l-|7set9{rNr@vRlzK=qi@{4LZI?TtAGuS4oPMgDI+|3%YE#1wMB;t-oa9EjC&hF7Pb& -WbrA$BNuN#E#b7>CDnEY&hS39xD%owA}%$al4{RNja}jwX85)bHQYf~7cfjo8vl0)EK+a5Y{UssQkwd -Xh7Y3>C`xM23MnPdE<8ksX6;8NuZvpr=?IwVe%AyYD)C6C^Kc+3zx;Miaka>S|)MkvI -fWXLRFc9<~{tc0UxMk7G474$W4iPOrp9M=>;5YLmz%*jci_}U~G9ze0}vEL#3bQ8%hD&SLbR>jCK6EYP5gvr87 -hBql4WT0SDzacwvZZJ8^p+~$m#Zxxa6T?P2QA9JqD7t1qE7CB;cG1l(-uL!PGY?}`3SAo(ZaAY!LQ3` -G73Yw>xBK8N9%7*$(A_+z#`cR2{d!0W7$0BKE* -_*v-G0h!b|~zu~*2n%y{#7D{%a8Z$Fi3%qH6V_TWO+jesG;5jjFY`7&@p3at{VY+z&sYb-d>=AxylsO -KgSsCXdPot6yQRz_z=^=(#7#(sK4Du{eIx*4n$|JN{%dwhXbwdvM{X9Ao11-n7DItE7K@}LePqB~`tj -={cAkNOYNdUOHMs5qP?**GiTZnrI6wSoY>jkd2qNGR#T~n@RaY&=C@&IJR;aXFZ9IqV-DX)){SJbh>K -j_RumKSnH2h!uIw34;Sq5waPCJPwHjaDG8R67i^YO>gv!|*wv=UheZ$W0FR&vAJwMC~wH$OBPg5!+SU -$-%)AXU?Xp#UYiqxcy-B#!EGWaDWew0&L#L{k}10u)<=2E|nVl*|{YqNY*^lc2p!K)B2LGCr0CI^F^EVr3d?2Yy4)$+~c%daI^bn6Ad<8T_lZ%nIu~zg%`&ab=MFW_+?;)#I -^U9T8B2Ep(x#ngt%7CbN{2n@0f)C}8Yy2ItIiUYvez;0`Q;Z1uqYN@$jIcx_!$2 -w2=OBw+@z4*{jy#wDwMFRq@ptFiS!LywmzC8$F|=fz9bnj1~hTC?Ba_K+8}AqXPd9^oDv!d~;jM8rZS -fi0{w$7aKqoF?DN%3Xzjk!t$~j+&q?r^6Rh4d#=qgC!}`fi3`D7(# -my5TFxgWN7mk-=sJ{4FQ9d_krs>lu3#9s`vY1K08rE!_-MW3OJ-AA6qaOmSI=7luto!$T=hQgG@sSSD -DBvWb>j1f@Pmtppg}XEhFnK`G?Rl^~5-Q@fJhv>-z;sZQ2m4>z12thC_vpyI$BR3`j-p*~GL>-XvG%(Sos5Gmj`?N`#>;JNr$};X2fbV8lI%Bkpl4@(o}C+ZK8@mt1d6c -1$NnJ4Ts<1K>yLx($PY>rmc*gWVdi>UQf(>B)g4_U602wljX1J8OcCJyLD=`QMh*<^T^I<&s6M*NTe0 -O0`QXvb%kVp9<4*9{G<^uthr9P6$Muy|_0i2}BaQo1qr!t((%pcA>gkYS4Tu)K3+r2A$oS5OArhSZZz -O*w6DGdt~cvYIV<+3@~0=^HgWIG7cbJZc=gsPWCAKW@368O;03FO(v#*;8Q?tE~)LQm&&(|aLzJ`D0Ss$L6L^2=oAn2Z>HmW>L@RINGop- -tDx3(DLc-!dQc<4?8CFfl$NEFt$|Q43}YwPY=PNeh&fa-LTQ^o`((9xmNVOSg?6{3I~JRFEb%J=b^0w -U{eUnGUQr&DK#bWh0oCjL%L8bFwdsHU?(S&r$tTX=+o(cyStDtHF)9*#5e>&?5reBaXs{dQGYTYVh=kOWgh=ETLM~uRI*r2#qsRL49*?OW-qY74JE~W5W)X|yn8#t?9KyQnVegKhU0_joK -gTZg~kfVJX#nNo5Jqm31YGy(KC4b07#TRI)OmTZ$#Lj3^GAdk+r9`n5sWD0D2|A&zkT>GRf8=-uZNV? -N%T~?2qk}4!RA(j{aV`T_&nO}2#G8Vu{fF}D!cJTglPpGU<&!tvDSVC#xWXw^!?Q5|Ose-PuZ|WWzO6 -z(=8(@blAQEY_{@wdt$7e5oD~o9@Xp@!j;*6)FJW-OJSGaXET%+k}1)$8Z&I^D-E6IXLA0r&@<%$uWc?g?H!?>Ew?X;Wv -;S*kIssuqmIVFngdo2b{+Yd@5*j?TX2tQM26T@nKuYhM1~K&h8SSM@V&%YXF{9!$Y}2cJ|7woGw+Lbu -A}TapVH6*Av-!7Qn^E9tPnp2tOAddiTIoL}r0_iI6o=KT|_ -n7Df`grX_=@Ju%|p=e5hzT`)(Z7I+NS$47X^DKO-xj)A)*Z@F4)&giz4KAVX#9hxGy7>Gg?rE1I0H}B -X!yt?gacdjmz+{iX3)F8bV2$hw&Bv{+oI2ouG^!jasrI(UG`VdzrufY+l8G2mT-EEzMOLJc>Sx7|M5PSw?fvmV+8P -qm&ky*L=4{-L9&IVQ45tRFXlnYk8j?led~hloxUuC+K--B{RMS9PIU7^QJ6jY$x5Q^MQ6Rt?HLn#idn -!iV1h^#6|f$?yZUOd-x(T-oVci`;)!$!^Sv2` -r=f*HiMoQCM&gWLav{%wH{u4SqLt)`F|cXLntE3Ry97fC+XGJx|B$>dj5@ns^(W10q?;e@`UYyt4}E0F}xWNh%kZ1OS5(bQctyneQo;xGW&po7WGkM^J3^V}9i(8aq3E)o>za -hMs3!^Bc$q3U`pMR&?=ZTZdxI>`K~+T;MDV{xLF0r8PymU40*EvbH+8QvB?*WELCd;uFO(pyMT=}=^AD)x0IOVD%Stn(wNu4At%(VWWFkj -L$u=YI;ZU-S8&!Tmtn97=+Ui4Jo3+NQ#LBgxMh&Tanww0-mWa>eB*XZZ1_M3t;RkhT!*%HXhtb+}5)2 -ew(zHoBrAUEp2ui`T_|fyeLd4oUx8!v^?ZoFe)lT8e(^-0?-I_s=3 -up13ZiR4l$M7xo)jRMwq#fe_TUF_N)YQMS6d;5)}=Q&$@v*MuFY&gT5Xp$x8a^9sFfDI(N|Yu_f-$Cw -Y|B9Ec{4YJ_WVu@KrG5dHhE5nzpCqxoq~+_es@aB(0jeLEE4;)vDcBu6@xRFI&ik|439@&r0@;t;249 -6DKx)!ea5l^$q8OReu*RjN6>;&sq%~W`1+05qmDB9NiqVYtW-_R#*(NwGon=PL;0IgK-ng!VniB#zfS -x7NzgQdC12qT>xG#vE*TDAM}w|;Vl)_B{v;>O?C&fPAx{~F}b|JD&1dT4Z>exx7*{T>~fQH*!G%BT2E -9V4)RlHKFJvmI{Vh`#(WXEx^JD7g`YKRaY_1bwaXLu^?4_cE)vTdSzEE0&f%$TPLn2LY(tT=M;m -KkOE+6-%GJ233oEAHK<7spBTDKXdXb(5F_wObqejF&4NNV#rPdRSrs4Tln!ljVSNP?d0FIXt_F$BJqk -^?fl^KTD5#Y_X6cA78-aR^f<-&J{)SY8Ig9+9t^xEaJ!rZwDOn#ET=m+5Y9G8-nvT1@HQ!RnCMOiw~I@;keI$ttexO@l -J+$8T%LmIS^3xu6qLQ9^6F$VnzV1d2q2A3l>=gXju7L^~Y1yALpq*oT-@U&SKyz-g&b?^B4PmrL?4)Qvg($n?FAsuVT|+2ivlV?LEL6G@L68<9WQyM8%n(fWOW -xZbEae(Zf^>CWWR-OhB -BE6%d_cyoB%;P4$h+$E}71o<*4#Qe;)a8Fqhv~plr%4S8f;<}B<(>X>e&g4nbAr1ISB3EgO3;Sv3J7aF`B)cb&5c -LmblCy2F~3#U@-|S2xxm-L+^bZ7E?7_XAf^o!DQ+Tq!uF%ZC*jQmM5{UH5ajiP$_8aq8rzl+wb6W<%r -){QyAuF!XFEN@GX>)LG(fc6{QPix*S})T?vEgFjjFFK9p7J=DXx~2yScE1UIm9s;h=k*B1_hVDx( -d{|f4_jeLDjfY_*7m#jCIe*bsk`tIb@t%cb!im`o7xr<8@mml`s1dmvs*~>o5+j6K -_$)}H08^1LE9F>0|-Mbp8+l6Q^X;&Fh#!eZmIf|RCzbV3yb7C*Gbi#(aLp?p}c^YxpG!JkQ3~9IqNul -m}s!1`fTGQx%w;+RQBE_m5*ap=UL@_g8K$iYN9K1bsRI)&FkFis_q(P5)e~v>_YpWXZ;L0g*sk@rd?j -Gak4W}r&QV5DOIksg$N6@8D)Zlm-YJYS1%8SMuTc`Xu=rHfupWn2~XbI2z6)D`7EO59)uV58}X>c1u2 -;C)r1%N6t%cLiAnBD0)#NwRu|cY;~#C}i7vIcGYP47idmSV?kdpJv!UCLq3xeV5yUp3n>g#+i>&tno- -`>qnea`6;UBHIYXQ()tY;Y%8ryQDZ5!GiX$Rt#wLNkNa6wtL!sh}+{w#7A5|oatO+GcZ9S|fsV-XrR7 -Gg&Lsc{%RbSv)1A-fCxZ{SThAK3}ji9oSX4?tgl?&ToH$dn;X8lWRDdJd_@*g`o@j$zhL -O0H$T93vk2Ae&0*hF48sfd8&XgH(%;kD(8`6#K#IiH}e(@Z3(M%`&qpr7GzjvfvD|?f{f@9OCiV0Cl$ -8j+z#hgNtDU@6rAFI+QkDNXGv&0I78KuqBR?tfBx4fpMTvz>2wA=zT3TPYb=Rg%)a|uoiM?p+9P&axL -_{7OK)h|3?emRf^q&5szR5kn63BdT+?ve7HvsO*o|qVy;=m9^*mk*1R`H>Uy^_e0)UUh -)w-@7hbLyHrlI+MNk+i+C#|0Ft_~9XFl9^;ZMpC8xepRb*${ed{mKUW0E7snNbw?^tF-_nLGv?-IK2u -8nx)VswY}-F*^Idz|*I#P{xF*m0+QRW#791spU$yY#eJiV%!kjKg9#4g-+&r3lFISz_sN`hhD(1-o!$ -Q53syG(=M{$_^_?!>4rPw?}vzMJIdqRaXDUCPT-keBBUU&+gOZXk`ki*tXdZmq -s0?)GSIklut3qvpYgK9R%fX}QmSo_m-&a*Y%Y1uM;B+}t3z$$M4kL=52?d)LOQgAADF@L`lArieY#z| -oUMOTVi-(oYPQSs`{2jh(N@@;8Q6qboS?pgzIU!Ua|?rQ~}T&mWjQ>btvXWt=P?c+k8^z+f}`^T{tu5 -DyB$(fkw`24spqZ#Ae{*Wlqg)OnaMVL+yy2hM!$6lhcW7VGf0Y_ZM+zzBKnR6^Dx>;VA4{r17Gnktm) -o`d;)4(2dEyx-`_x|Yg_gL4pS&i?}C1r_wb94g7NDbl0(4G^L_r{R;eb;)HveOcdtD=AQk#S2a0<^^; -${|nnmeLcS_kK1`vK6;shSyC~nT4-wL4lK@bd$ -Am7}xK*SudXpVHj0QQnSJ^I2M7r=At^fMvhD1Kzl30P -r|3Zan>fX6PSg6&)P1kwIT@VPGojXxbwHLnSH=VfP>jwmde{s9W-b#I`PB`T2g~-ELEsvMXs?--Bh3D -0M*kp*~l5s!vS@ZGG>=`_b^_Ww0z^`-Tzu1GgmeH@~I;g&4KhJ|1r1Fbxp&gdW*2hvHitx#BM)PNCSx?KMp^Z-9oBxe39k!10fD&Ax -ZMGoSL-pIl6^ohrYBL~NssZ%#t`hC<8$Ah_3y)N?XM8wP;XqUs%HnX&im9|;JA6YZQ!EM&x{`R*oqY{ -AUTq>p(zI6mM?#MB;S{-)^gTY6pA?q2a7gt+ -j=oJ9XHj4gEU;p95%H03-5SLVQv1nJ*xM+uzRZH`&`aPb>^OT)~}LnC{PCLEhJn;_N6hh!JB;NH`7Mw -FyryLnbKC1!W_si+ZJ7mf(`H8{#1r)Cg}f*$bK64avflJEP#(XIUvx#Q=kH*+ha~FW*}_fvzwNt -PP6WKECtaj;|9V;J{P0%F$xg1K9-6EQ_@<0K|ipqN0b#B8KZ$7K=WTHlsc}Z-||!V47OxrvZ*upy?k+ -K(&mh&rPHt%|7)c-j7TLtA12^P%IE_bomLPH*4oMcef&6ivIRqk82Aeww -)beulqF-t41VYCdswQZ1F-+KV=I7WwbKgnqc$CkZ|FV(RTaAEt!Ko-wN8h^8uf$PJi(7XnF@KAA#8Hq -YgXKHcJeTo!tu0RD@l(JuB{-Ow}WqfWl#LUH7hFn%mZ4 -}d6V#F1fPJg$Q|%%h3QG0dALJLBTr8!pLhNnfGf{L7ZXEmV|~b($m}kkAK1$d)7IPs2EE#BlQo8fFWX -9BfL*vm`&olAS$bsHhZg2x<7VStXZHhVuZ~tj6fKk8W~B!M5t9l;c`FxDDncz{@RBB5r?30g`p^Xxa*iY`J|JU^7(OpDy#4x&Z+%BAx&r=;U_7eIYFEvdwJM7_21BiZ`%5VWZ% -MdJ^{xV*e(ZsTjCBg-{%uL2X7QiaGS;Zu6WObRPiK>TCR4KnMDaL;00xNMLu3b$|0u3E>AmsS&#(^+B -2C3EEDp~y{_O8@;mf((nyw(SOPJWEr6i(J&X69PF*cJIO=!x}4T+ZqWoZ(t3Ao6A0V}l_f51oHroJ9} -*l9%W)NXglaGc$IOES#k_u0Be}$rZj*ZJfAB>YJ!q*7@9WX**SC2P!rgxxi<*FPVa))4&DZM@uV<6#h -Li1k233b8zA*V|VA#w`KR#$#~icbW|~((rB%hmzG)UC5w}VXoo^*N+~iyW7dwPc~e_kH>6=gOiREK(F -g)74<$@%>lPN%K2hnYO;&pX9nHy#S0BfUPs2F3`fg%G8*ykAL1mw(yXMMluttRZ<{>SYn_Pvt$f{2hi -%5!DT;I*0GsS6$i-@IN=#wRk_g^9&oOq)t7-66@gMK;#tPrFW`mcheCqIf-74EXQFmP~e;H9yF&}bmI -))jTKxTxIxP@D%E^XallPZJ*+21O4joCrs(YBehWf>QvW@)BLhIz8%Zht)vOauE`+YNx1y^#4({Kyk( -UG7@wG8|e|;{l+sD5&lP|3(&32CqIH!gNVA4JmGutBL}t6RS=rsBAZ@^ZrqQ#NM;4Kn;v=TGJsX10L@ -MWQ1?^2wsbW9=GWx7W$d4R@(DgW!U1S^p$dBN5{Sn+@FO0g#NC!rB>Y=_4M27jv-#X~w9Q -hG!Xz3Ot%aoFzW1X-zj{>)-`!R{U+CfhrZ%Yz!yGF!uavD@36Doo09|%;kc9RvL`jw?{T^0FM;8T%VTE;KY)b);UrFln>7UOP*r+(G^7{RZk;KON=Vwoa -;?qLY0MKUaSIIGuFN1|>hZYIud;5M0Hsa4jca8T6AY2*{h!)qX*391ofF#aH28ZL{8ZL7<= -BUGQ>s^b~Z}B(&(P(|fYxVgD{>HyyF84RSIhmRIx|5l>Tl;l?W9NSYKOGwNH?AZXK7zo4nL-Wu0=}dL -ii`RiQ^1`XU|abjl^N@8{F%^B_Gj-{3BoE3zzvC1Em6?ZJBt -=noMS>yr8XcB{QhGeWN}|=m~7uG#*STJk7wTJ3vc8@C$gZNSI45bO8lI`Sw2ELjF#_9rivU3r|@csnP6B5Dxd@=U{~el_f9N( -nObjS$R>g${=OS%km22OQdCH`_Q&&UV1?52dfi@E|hAqJs(O%4u4Rtwul#0eu{Wugr6vf#qJsMiSWgg -aCfT7OpjV9DF@rL7Bd45b3UKrH@e-+NimRT)=@ma3CuhjO5IV-wzA4Zn_B6`(=k|F+?yz_q;Z%}*Xg9 -->4n*77_VKEI8CA-hR-G2K1?$F)$kd+NSkEYBXTWYFi2i5@F~8He5Td_dbC4qM|X3LcIh6nay9zSr9h ->mRCQCHx{39Wb!pU({0FI8mGIma*Me^Nbe;8K)RihM4-3mZ7BzN~>9`6KRLTT=4M1hlMnB;>(9QWE=Ho2Z;#HxZKU%3T?|)mzlZ!hQ(60l7nl0Vz*TcS -<|6s>dol%n>-ZXJ5^pq$Gv}KwdCxTR`mcwOd)rF0L+ok5a$O;b_7;z1hdGEcWi8B?Zqun*@hWp{s%ZN -$U`w>n3-!=Wm_xrU!q1%}Vj!A=|!;>RP>Dl3>rcv;j>v{+#hc|t2mct6PqdGM -*smyezxQnl3{ZZJtsJb*~rXW_DN4P*yNIGvQepI9d$chFB(C&mM2oOBY$&j*)6E>0@Xo`b@AW%gW|Gr -cvi*jyz?_@C@plG1b`gKwax*zZ@Xtxb(1Olzjh1o6|OP~9Ia -Pc;uPOZj%=o`zE|ueFe&?UTV5P=^3@JMW;b^fcNG@MTaTJm~#!1+blxE2n8~cVHupO3 -DrcR9=o0I{UB#ppjgllhGE1aPRPr9hog1TT;tnk5D_n0amTTiE;Dx2~c=L_8r{*Oh?npAqy^=ksTAb{ -jYO}B@6~Sge*edLw>@;yvk1&+uH@RsY`Ll+(7wF(4^UAtt)Rz5#z?K={qLKTm!6kF*#m0CB|3Ch$)%B7IIV -Fyqf*%*)W44^uR-SrIhH@QpPMQWBpsX{31kTY;VQBH`8WN~~f`KL@15JW4&1ahjQjvkOjY@jG%xGZHD -In5fKSM>B*3X|(XK#ZU|KqNtyAg=$= -rHSi*>l!Pr_ampC*}~eVQCU5i?9jn}G+N|%_*72H5fIY}5q(}e0x73)hs%r1Vz+^B(~bx`ty4k{m`%% -cG;_L=#_|C-Q9b&B5hQn!*}&_Xmz(_TLgMt3auqrMZnEs5cPSn`rIN4^ER42)^)Fqxb5+Z_Nu_tJYbZ -U#viIGj5O=?;tFN-dI(mjI2PS>=)V+t^Za0B!2Po|{&n)?;7fF`E=4CjDH`|M(}aPC6vYD+?^>6Banh-)jr)C@)}JLc+x6i>N#`WQ(O+XH)n6*aA=@yz&L{q -PgLw!M}(fnRrX#iPkhRY7k=vVL)yVceAE2zO3+iUHsS-jv4vX^S=ee85-PY2nPkMjlk2Kn8%lTg3g!p -12uk!KflgH%BG=K3u87cu*eS_$nfBkIDze^r%ve?I -=Tue{Kh8#iHP;(pIC_UZqrrTRzQvC_nl1+ydrCcRGL-6p2#?io$u-8M$6A3XCjpmI0Dd<#`Fy#o2F`42K$l6n_k0(sTwb|)ct<)ehF1@SSpoF_++oe9rBogS&X=Pw6oCCr^R&@`nWa?OIlO~utuRwpu -waEH=_1Xw;KG7h!HUkfxl(lw+lRJ3ZaX8NM--H7=nJWWUI}HVHZ3zZDL-zjFgJs4k>Vd%tz-nt=%%a~ -`pTv6F~vnyHyhP#rA06w2#Cd@q~l~@hEMiub>injeUuCYi{dq!YMD4>;vVzi=~vq4YHl2 -NiZp?Oy7NF%UG|W-R%nVUs6v&e#)uQ8+6!^?$^|HiIic4S%_J0OxRQVycP?_vDV$Xx!xXCh=_g(^p%T -Os&iR-0qiR-P2t`@ZsWGDLlXo=X^2R%3McK|PMcEM@?71WuseI?35oK=TS@CUxYG7oHpt^k&^NM%ARZ -zYBPC`)q=Zd7D8gN(~e5$yGW%$W-Za%maIsIjN9RQ5R6fqk_mYSNlCt#q=;_%3QavBU9K1+NEgccb17 -DK91CVq!r`LwMVgdcZ+QL8O3IYRyocbJ{5QCy_f2%LE9b4J3VS%@ -p4Ik?BR;F-ePy6eUG>dhQ?;3&7hX>ZH{rWA*)DN|hch0?kGEk&IW?`OlrTqgxE+TN!59gFca?$Ip3qZ -R}{--shb3}SPkO+O#g-mWneh$kr!>rTh5lcOTZ=c`0w;^!5%@dbrpJ_iC^U_Yd$??dp|0lbMWh@a-zZ -%nriQ-hC{ -An^0pEGBk+0`Kh0@b>F?vngImftZnE?7spdhGU{o1}{?^#PC%^M!k5j~HVtl?Zsb1V}oF|M2DX(V)8pVEyz;TXEug(P6jlT&hec^G)BU5X&@Cy}m!>y+9rwgdM% -+9*VniHn?g>09hR4Ge^HyMh?`f{CT;L-xX2qlVQOZq-N#kmnl2~iBBX|h!yQK#cr9*wRJ8&$3p(Z_U# -7$U)#Z~0k2M`>j3|NG83Un}|lsm+XO%u}ec=-@538Z$)5hN1y`2dB=*ScO_Gb}xWg>o$y%qVe%%w5lB -<5^6M6rb;%t%;dNZUX~qJH~2$4I5;hH#2(4YUv8=^iZBCDRw!BUnXs?~4qc0czK7t<-J;@2p -CkqUNN1IFgmLdWoiK5LzQO(G#1GKGvgSg8K4QnZeqzX=Fuq=8iB@5cBZkpDsK)${(?FCS;*bI>bK3<4 -dD7bdg`;O^2~g1c`6pp7NDQ!+)VP@~lzG$$Cy5e*>zj3gJ~XyaO*#%=x& -?^Xn5MfbmI8E*q8tJ4ZWR-G?KIJA72#*Pmn$i0!2yn8WK*iybM{C$Lj~2Ad)to7((W6tMg>kq!q&$kE -Yx)#9Z(Y)wtQcnB-{10fHF!5E0DBt+A#*9+tH9)RXmYU9-#n>2#W>8COS=g%~xOMyr>axD)HQxfOv|o3lC8}gR9j(;LKJr#I+ -pB=h!PNiy*eN+X7!pi*sg8&s+kf(RU0k49A7tDcAmE65umev12Mg!otwDq^fUS=Bu_st -;Ok_Do?iAqGyVk^BWr9YZosTdupsovo9qytRSfRSSC6`{lWRD2@^`ioGb*7|tm>6n3^D8k+{e26Sv=7=_NO()2*!Wo0g^Lh=JQtTlH$cI-k)C -9K}_?dpAm#Fl2PbBpAS?uw;$ea5UcPcCa1tU0b8{QBoyrxnVVTu%z)$oWN$6JG<&)#&z^(L -p*C;wa{}#Vp+Rt)~Za9GM3L21yq}{6TdtSEkRnt~pWM%>?Z}F1cu)acO)UMd_TYc<#^nVSHSqOVSq^) -or4UJt0Yq@eEY`+fnA4xE>DD=-QgjL^q#X^|HP5!JOC{Z}FWVGOKSojKs5({6?-+I|ST6ABXpT39XC# -#!Sa^yKxZLYTr4&e?)jg%@?uu_G2%rKLg<^}X5`gP(Kt4S!39XX}>DgQc&&n>+gU)iNI@s(D}IG)Dar -Oq8gOn5@t_wi30`dvT?A3P8FUAuOojcd6X^{-`jfozn+-V!U!&LKQaI3{%P$O@Rgk#{O06O8+$x#;aVFWu?JH7+ngKMWs(9!npCM+)(fs=UvrAvXa2kYh8%^D`&d;hRYou-KKofulF2_LK-D{}-Os#&Lr-XVu0>KTqXHtAjKbCa(#% ->1~{R{Iqvn?V124Ludg)@b8TSq|Uc828(VE?;Fce!@uJy`$BI{=zify)HKSM=Or!j&?G{Q9(tk}M}F$ -?}lJajICT$+1l^dO?n*8ff;df?!L8Ul#0eRiP=2jO`DJjETj?*MNO)xFRu4$p -$ukmXJ}Ski*>oP=h1*q%faLrV4gQ7&vzkI-`AwqrdJbJ#V?nut!`V$Bc;h-%Bw6<-2cQ -YIQeuO+)makO@P^;Y0*ppuZDCE|8XX_GgN`@GYi!;n5NhrsjWuPc`Q4obNeH%3i8u3jhqwIG+f*Tjk4 -OZBUN=;S`8IY02J49hQFC!OZr07P(Gj1G?Xs<7hp2C-KH`)!l9X5ON?P?&++Dk=|XCSa(7S1~dZNa3ThC`1mEXaN}LKL -4!JkbJ!N>L4FmjBMViyV_`a9892k2a)(1M0G>r&yiIFl){azYp4K=uZM3CI7Ljxi6C^;=DoaJogLd0V -KCR8jAjL3)HI_}UL1XLIAS?(+)iCCIS=$+pw`7{m+p|yUzm{@%&SX5E)H-eM6x<)Nz$CH@Szan;fFEG -y#{UM{-vcHG3ipl-ZPj)y-r2F;v3+%Vro8VvNfqy0R8t)A -U9;`Jbcj!OvyRl9r8z>WP`L%Qj&DK)V;RzQm@_j79p)SS0j$oY2FXS)8KJjJMA&4oJ(;`%scG -`IK2vQHeZyn?@43X^44Ai}2)cJ9Ut-IVnnY`2=5kNAXPn-EuXry{;S?Sv=3^Jth}{hgab%k8&0`)qyo -K_Tt%oq$=f2F?o>xh-oPD5v3WYi6HBuVxuMEfr%a)Y(y--GuwC`kdgk4WNei^0TYlTn>Wn ->f)?OU**rqf2S$rTvdevFH@vQGQzelkuN(h0dI@8X{qwjYSK!hyy?v`D%mC5c5~qd#C$WXUhApAS7CI -22RyDwA{bw;AN^Z%`K~3Zt(8^J}#mkwh4O6&lx3yLcI?uXD-(v0JtiT7U_;b2Vphr2a!uUa_m2spo_(LaWd2>^>+9W%zVz;R< -@-k!&;m@)*@nu!~oGLz_EIEK)WMZ~T<{n766m;%K!zA_r^-t+nra;|5(j!_1(DrIq?l=^aw5nfr=J>6 -6`Nb{pcOCpqgFmZC9K!P@tNN+@>{X^moxSM4rR2f~^_aH62VHhDNwuIcS^orxnOR5akIZ@LAvJq=yN# -a%QmO!d39=NIy!e%+_yfYi^--thcfArm%yfkoK4<~Al`<&+IIoleia^$*{I%o<`x7m^eSe~bpYOvK(l -H-(YDc5e;DatzIjvD=AFiebqWnZTs~beu;E7q)c`Y4V()6qB_Bi-CxbXLjSW98GdfR${-pYnJ^mCzo#n!LG>D!S5ivUeXUrubt@P^7Yd=v4E*fo_5Emh((I56@_s$zE%N+t+teOOcvao94^NV -0(wc$5jn$j6BHf9-e4+e*#eqM6bnYekTS?uzl`$avjvRam+l@zom1i|afNC}A2vDuE2tA*q-3;B5DtC -(AGe&PUM{m-a9l+cjhSsG<^ViZLIaqEiXv%MDt-jQ>{!&$IwJK~M2sd#pdyIHl$F+8MLMfFJKemU -tz=OIsOq6@%0lKMU+>|ZG&6^6vcnTL-gS$P4og+52iffq;fhsOgiCX~99(4q_wPWM)EQ8VO1ybyuN!L -s>kMhU8G~#D7b?<0zt)`yMdIj-!90 -8Pj5*xVbm2YFQu&=&|I~skM*OF)fBgXxcz+) -6~Nbc*umahJ<{~i_9FJ%Akb;m8je{*AEh;YyglRfrp8N5D=S=0rTG-hg|NwtS}t= -Fn}3-k*+wPFmgKNa$P&K^MG3IZY?Ltccc5}nRDncEI%6__P}OfO2PDIM^51x`d~6r4AkUZI8q*3YK_q -iFsFAT%7gd!WTql3~Js2eZ9A6bq=*Rc!E3tSVbe{f$b;1O)Zf`=$vwP_RTMfv5uu2+P{U_~88mICw;B -sbfAJMLlaPY57jl*LPRv-_`ZRD=F{;L_?V=upvL2gDt3x^pcxR+jo1Lc=A_V9B`8oM>h;vKcLBlPx+w -)plNlS>Qq_q@hlR!FW^#~$aPzaXYvDuEUei;I4_+yMjr@Mc!M-6*hlxbvt{`2-@l0~v+k%Pxe_Zckf< -Lde%D)1SB|OF=oSP29O~<$Z5A)2ryJhHdR+{}c4SH>>QLvqE+itROi!u8y%^yY3Ayu`4gIu5$TDd{4? -(-d%^kD{I#7ngFOdcy}G%TBq{g6}J}5RrypqUodym^}%_YWXm6=r3Dtewr=YmL7ZKs-CI}wN0uyM^pC -YTO&q{#>$gYf7uVTB>tq^ke$b3{5UlcYi@d^8vYZuCXwxF@<*!&_9#=B;4;jn4gO$q6(tVay%U!E$YH -QXUH8W~wgcx6+*ycg+chOc{m1{|5Bw-?vKVcf}$tQ?k4nA+?kn -^=3Cdl-La9edP=u{#$q%wUxMlvGYQW)wd*i5J+0YtKKg7Chy899IDU -go$Fz++YE1&eX0J1l9$iUkieFL-LdG?t$QwJUpI-zhm$>>jW6yX*((mt(Ary6-GeYT4@y9Ok$KGKb}M -$Trvtvp$z!zuu&sI{VECdqy{6lAq+ZhLTc++9F_2z2Y++nuK@n0!e0*jS>Vr%Z76@dM=F1!2i^qU1m3 -;y?uB$Kf4^_b|MNga4PkHvy=sX#dBD9TmNpR&M1@aVb(!TtHAR0xBeiqM)e}%0(drgn -L~|1-+2s6_@vI(bUqk-O3iVO$9ZVveaxb+qCrc5S5gsR_Oen&vVYX91zU<_I|(L-~am_c<#)Z^?9CoX -6BihGc%r;^${Lb9v5|PNDw`Rz(%kTE;Kw&*}rQUFf`g#>NOX_u@*;&9eUVOThHp!*`d{183fdnLDNfT -pRA^Qzh8=cFMLLvUcHp*)yw2pwM+t0CRIzgOb~dPpq~JOVFZf?dvrX!aEOQFN9Nuc=H4rYq=B0jrlzc -Zq>x++3k@Nc8NyMZnlk&*!*K>Eo91H{p?2D9$#{lf6T#~QWd!>O4iS92l*^_a@wy$Et;lReW=k?#lIc -&TKbd}H`jP2NrZ1VEWO~*v8GDF!6_8_bcWx?;Y%faYYtL$8t7TT{@3Z8Qhl$>NIbP_eT>sfY0Izs=!UypW^!%-U-IXT_wm*rkLTe9*M|L@$Sd(;jZ^ -81@9L9ef`k3d~Le_l%oo#2)Af*TZbQKyrO=-LaOPI(547$i%7uXRIoZP;;PMJX}i7nb6Zzl3~F5(|HX -(*ztZ@s5t(iY@(9jVIUjS;%}&E3Qb^6BGI$+u35!u|xAMtdwhrh5{VQ;w(cVkT0#YRa}^+i#ha#c&`Q~Evz+O2^Ru3H0de23_(b8FzF(z>?>;%YmOk_qQRC -!DiwOy==3v^FSk0qSvc$GOr~@p`EmfHlcac+pfno{lFsib;pN)fr=k<3^#LrN4B&ig-I?MBW|boO3bk -_ql@%$oB>EVdXP!P$ujq%&WtGw@umW1I!(JeJk4}C{HcE;>tr_#G`-_(teS$s50b&`j(g&I>BuBOiZl -wmUc7m7`~XC2+FRJq5C=I!BU>u9b0NW>ALZ-q`3BthrU?NZ@CD2sJoWNR3RlT-5OOlK{> -?-mtFQ;Nx3TQo?z*zqH&~&E{=s7Za7qi*;tzTn{Y=$VWvl*^zS^x|!a=Jx%lAWTJHEa%59%6HtQp#q6Qq1NkWe%HT6g!*am8oprqgdHYQzo! -EMH$UzmJ-iqt`fthO&P%EOr;N-vz2Zd3R|FIEA8RB1fxW8U$++ -TYJNL2U|m6wXiiD);?^tzKW!0uDLv_y7-29Su~f0s!LFq*A&fVI=f8o9o8qSnRM9#r}VP4T%yn$*;{$XB2HJ95}mzH5(!!(yRs!OXduLRA-S9NI@<~2%lIqzT>bC}l{&E=%(5)kG!UU -T_SbqNXc%GF%TRG08DFPrA_qUvG^^O~u-tX5s3!@Oo|E{jx`ptDGcOJ+WaDK_S)lSTJIT<|D&_qvD@srZ+x^bX<-J9%kr46ah+Vl -8dtD-6`JQl_sVZbxvE;^yU@?uQpA^_Q5~^2-{UTn#*fvgcze|ET#V;$jtcO^;rH5g(BV$^{%-c}gjDD -Lsc!a8jZfNcDp&mbN4ncvlVkJ#sUCKJ8dW%UdNw|3|IkSq*{)@6%MKrRl)3G?u#;zGS}k%XS}B>+L?A -`_;=)cWeAqUmJ2CM?9v_hF9;E*crN51I!ODe(`JZdxpHnj9{B*|sR61CyoI4-MTfS!b(s)V6dd_U(Y` -)N4y5YDoKht(>iST}#KT_rI5xsXw??2r8HBjIC6+}JxB5eI~zNb7$6-~zYItEI6Gwk~^Ip5n9!TY{=+ -tzB0eU~vGMzU?cu2_KGOvEQAf+(hcx*f(*`1(pumVz1;4&V7y(?VVNSm%YkQczgJ5<`YB -Qo$sEH3@EU#Lb%ol*HWJ`<#RpAY-WR5Y1vi&~Uy=1X5T^QAAF-{#-7^txJ2Dp7LILY+v{#CD@7vewlZJiwlX+lTiF@R())}JD>p4Ls~gM4KBsOh14 -D^xiwzvzXW(&_i^mnGv9WA>osDHfhiV(k_VdOv1h3f6`?X;8ta!y`cdK}cH=nT~%RrZ?2-ec=TsvJwOSdJb%Pgqy -CR)xRKFcp97s}scA%Jv{wSeE^UD9w>Z%W4@`JnyR$;9v2$%Wkg -tC)pQyg!i@ITzJ1*Z+k1pE_W?%p(}025y!L(&(f|7DQuTU>4hA@t`b#RuVN|v%1Dyx1c0rt>D6I-qx!hwLs5V -Noor+!RocDy=5dk$HK0E>ch|@QZecPN{f=>-bZxgd&U-+P^DRodHH0em>s!X!QD;GT7hns` -t5`iF{IRI*Mc%3HAgj~id)4 -jWas0OYKk7NZVcSaCvK3U*1&M!Cl)LCiS -ghukx~8hHWOchTXc>L#%`-Xz8qgExsmoG`89t6 -EFEYZz29(96YcPTHdK30uXS>~Nu6-7NMcHj7<$4!P(|uC8e0UMH4=n*vd~2CP+gJFektxr$fNFZK_?A -ImM3nfB&|zfHDJQk}Szl0(^!ZA38VabmpHY=Cn!yX;Xnoc;be)f$%Pu_eW8FXBbn8_pD0cZqFi90V~EjT_FkfmUodJB@}H8_t+&Y$xO75{(^GCf0EYc$VZ7gr3)GFfDbSHvv74!3d`i)ASKh{Z&1}>_Pz5O(r|&h}?xq*QUhd%49cL^wY9XpS&iY -XJMI69$#GYc@j25)8_+yTRt_eC-Mf1yC(7f5~#>-sw&X=2QtyG4bFSlTm*%01jHl%uGWeegX9JLK*u0 -Zu|F4M`=s#C4aWz0QYIP}eBEOgrDGNV-Jo6CT7sr{_l#Yk?Pj62IfxVp0}#9!N4rV55@XBk+b?ktNr% -iGHIDpgXfDiU1PC-E1=9~6cL*~Un&{Z9Vf;}&G8w8Zj;F&{^nd+~nz&w(s~hc8xk<)&|Kou;qSC$^A6 -ds%qzfhw(>m17&~A@Ljwb!v438~adEU>{H;wYa)fw!!%p+FM|yA2!4^SKrKPA@a97+-nDN!hxFBv4T_ -dvv+b{th~wCyKFMwskGv~V0e?fYwByc8L^z(p!qr1aA3}a+F0(9+WexoE7)k(UA1~r;@gl_KE}J{exmjh!GStDX-m(;ygiP)rh)+Yu}u$!!^e?G(^1D3>IaL9 -ed5#ZLE{K_eCBHSqCV>C2U+-g3m8HRFTH~XYB!7&0z5{6_=i?SH)JFZrAkwb1U^hWyau0vPBDC=kPA>Ee+}F13!l91)oVz=TD$-~7quGMP-FH3yIYj8dj9 -VT%^%qauSU+6bJ5#KPbnB?X@>oep{+!aGWo-y=-r%EMKZ_l9*nD!!GBMigg10-%Q^yFKshLh8AwPE1dGv5- -QZg{E8V(HW>FGG(X4{jXh-P&*Q?#pf_T;DP_M(!QFG@xquWYS+zr>Zpxh3j{ZH#j5Z -5CfRM;}*`2pPU2KD?RhYlzoRwxO8V=4)IPQ;!4>rW(@w6gsC6{tcqv&^i7*zyG2)c0btLD+Nn%1gBzM -;u@;?E>Vx|b9(FBS1u=(wo|u{c&N7(FB%pS;J*LRuO({&D9weX5%gR5tGmss@K-l`7iYZxg~Rwn{3hi -Vl_bTxCi!PeNqM@7(mo#@;%T;-GTrg5MUt|edr1EzWuT9CvFS?a3LfX9eLM?IMT>E?^~gs^k}X-m`$L -CP$7rTpO%IkQI6Y)f*^@YlxpG_$zfDSV*gvDYz^+UDyQCG=i6`rN|!UmW*@2pmIpE}`-c) -AsTNhX8D`#sk<;a} -~<@ndbj`3FA>5gV=0Q3pLEbQKsI`W+l<4kcu-TO~LyinjsL)b@W=e4KhR8zM3@VxK&9DjVIZ<1WA4`_ -LTT|(`VEWs&+ojbMO0+T8WdXLZ;kCuR6O>Grk24A{Vjv%U6w6`$otq?rA7&?0>8VeWymh|7h@E% -<8W?_de|P*;xM@F3giYM49C=EVaVOdg}S?``2E^Q55;)nN$`VkS22gkBHO!xe3WTx3y-fnK2Y&@5dLl -4HUt06rsL8}S~=O-V~^WrB%%JMm-u_^nYRtr%b@J_KTBDL+m)21y49g<2q?RQL#lQCcAX?UYfE_52s4 -oNt43Cs^C1@Yt=GA+)O&mY(weD6HAv1g)S%^+1)on!nlid6jzFoZqzYMDiev-JdGMFuEKf+%&S2!4c@ -*+Vplg7%iYhNg95Jr?5V!paD(dIm2<$K6w>1&z6dbQ#96 -Fr*DHGzKWA7a)E+#6Pn@Jz{GgA>d?00=ZI*Q*R2nTNyEhFG%p8vJl<;r3HqalCP?9l@$xFdSuvQE!Z? -yFkbPxmYtFvy&F0*Ec>G2$gmr0Qf|MX!6U;SSfKBmytn4sYxP2LzR5=!jGGsn@xCRIIMuzWBi7G%ZzP -}VTiKjmJWC^6k{3B)DCUgxb)LCv{N$#Hop*O9gIt1G`cCFwn~MALFnhv_gz;PY{B8FnW!Q_n4dk+|Ly}@4yYYbIAbPJk;JDJ=xoeRnAfO=J-Bg6dxg^E(q -PB;G67n!4bm4{C37vkS{)F62MUQhrdtRtX=*<^u5_+*tLXTaM(0Pkzp=|Lr!O6_F>q95_+x$ar#>FzeVY_ -VVhb+Nm*u9~h9q?2JpB)f7rltL6HITPyOEU^(ym$r|?Y#%OOl$oh09be3->+}_Op^|*$(9gs*B -7^!I#^2#^PGvY$kCM)GtDT#vxIY@bH`Nb@4V(KvR<-X5hEECbnU%773J$D_@pZ;mIZLqUY})D>*$MMk -^?gF1o(hUN&9P%^-zIn9KoUL&Zc=9N>Kw+mRQm;gc;P4KQG5YRW|ec-g*0;C=iCF+<8b8=vX3Vh92-h -u4p$uh{P1^&k2cwFKX>7*vx-8sq!7^o{uln`aX7D87=@eRn`a86u+tk247juPcVMQhFz)vg>4{yg+$yub$1=!TVHX -Mr`@Au!bv&USMNiD@D3Er>;n -}m3vmh2N!~W_MP?JlQKa2Z&8v49$uv=V7ON+^l*~EmkQP&E7&NWk+mI~AAJ{2VVn6QmWEU?!Ikw8=co --9pgFW5s;~aB+g*}8YRFHV^=TXE7c58bjE4*lgTzKbLLYYzo;G?RfUp5>RliJ8Z}JgQ3UCmpznW7d$0+p!M@XVl7fTvsO9QcL|f2DR4e -d9}{w^L8>#JM(YcWAPLYU%AKP)fijq9c31ZRQYB3X|IwwCsj!}=s3~7{DfX!dmuQ;3g+OzRZpX;pl^U -2SAY$WO_ZPJ1N(H-nX(ayxJki85el3;9p4m`D?YY<+BdlArUEyDNPqm8@e_m}3w{#ta}R!UOG_{rJ=A -`v_(NMi=gxA)(`KslD2?U#rLq1)?dKiem~8EwJA*$j|IXd!Thh8T)-%q2(Q(|v=Do0WIkC&LfU*;O7<@XaB9d5^*Rc&pYfF@2VILRyLN`hf{4`kB|9-%b>&o2SEiQt++Xt(AM` -pkF#!;;Tf>x!NfhKuXo+9DV1D~%RzHD9~%Q7V-I$$Y--(%1D3cK%Lp>ceK@Aefu+rE6Jo*?BRz%z3a_+J=St*cv&?JuQw8e_33oG$@tq`Y$v6jhw9aD -CwA1m$0iad9~^^y~+b{=v~1`^Ia&M*rR)oe@rdIv2Kw_;C7u*Ywb$`_aU@0Pu|4itoWRnxD%vC@}{W<9&?vCmwGBN{^OezxGSJ+>ao2I{xh^C30c -U&jK#tYoD-TWrIX6c--oGH@tzsQn_0}V{e=3sl194Th>=`(zatIL7~ikdkgYf!Cq>vwjCuwPCi_ -RkbgB0+oY-wQ}bD+48F`ngk3f*zJxYB*i?iwMoN3XCHpF?QuuHbeBR0%_DJ=4tLx)B3WKPMY|r+5r -0l9WA4*+uVlqH1=ms8FWQx|nkn$3V+)&mkqv2>noR``LN;C?&$=7>=s$nXDc2b9cFpM -o}NVU@8L1M#dj8Co%;u}|!YHh|y(=QRfJJbvJt=cR}%FIIb0Jf -VPmyb_>FG-n#cjf7sHFvl|-BKQ&i7Kk^C>%T!owIl4H_F^v?%h>)=ibV9H3bc)VuZanz2=5k|9q1YYe ->B^()3cie^RpZCsM#|O+dve9;F@y?uxHe#gST$mSJDz`v;y)$`l@7Cn~4ba{1wKfpISFyH#QmuG4bEr -^vn~d@q8s^)&7@RCsr5$>{GfS{jQhfjFnrI7_tSySNtUS80pHwy`<>fmLmPGcCk@k_EnDXH9&c+*=qV -CuUM3I-YlQ@XfU}VkHs3QJFQvb(2f>49rUKj`k_g_L0l=Zt5{z#QE8c3p~Tl=6O1I(O7U#0#`6!#i1} -QK&|ge(BXy5T(%!TxaEFZ=!rZUEj;?_&`#7Sh+3i_kw -A&Dck&e6Ke)8YyTysejk#lC>r$)hdlhE_F?x>(&~-gLsyo>RCU~0GWr}oFuPSLbtSJnG`*(msnG3J9q -s2Fmnk)zcAm&P+!Z9+r&d;<@r@H)wba80wXeo_WuUaB+UYjdRND_0MFzN+C7ZU)@-%HPQy04rU!3RC|Pw8p3K -mcnAF_;w#Y&tbwYFr$+w6QpWdgs9Q{`PztCc2{rH&Wz49Tr6B<`CG_DkK%1I}UD<_*YuAJ$lapla-C6CF_!g8R6&43 -nGkXy>1UzhSVgK_rt#rtis&co&3;R1;pjrkS$luq|AE%1-CpK+Wp*#cb#-k#?tAYr3Id`_O -tj@&coISd-VnniM3xCb9c&wU2kGFKk!iCyCa-C^#C_4ypaQxE47jsYCk)+0`rA+rW2B>nuVRUHxK)Ld -Q)|xz`djuH3s*qFCL`ta?@Why@k@&rhg>5BcDGKrG}vu+S-t@`fP)#7+;a6J7ZeDf!hqy!^)@k4LfTy -QSwca6O)wJS91kZ4(03pkn#@F(xw6ohYM1y&SAgLFe^{y<0Dh#8cQ~WE7qnA_}=b?-rD?zdK?5#IfP5 -~yd8w$dtBrFMkFRDWxAEdw;YjJGQp206#RL2wykC81V2+T@9T;84SUbFt;DVJ4Ye*gF5YJ<##Qgs_!Q -+!zC`_jd&vYTKdfR>D#qzdt#cLKKY2o;rGBEIW56cr$S&Kx6f3^f#0%=NI?uF!7jAK?#(Gb@?+B{)Ls -@mTeS>) -FzjwW?Yk9u4~b5SDwnOxl-=Y%vx8d~vy46BpYmeVn^G!BTU -(9QB#h8S+$iWKuFpb>|BtPPuNG?n#P!lIxTVTYK_LN>=`1)s=Kl>U#auP5PExzS#%8W3Bll-XERljQ4 -M{-xiN{caGoI$XD;$V#asLog1HA84FmCSR$@R_rG95ixXNA#foDO47gDYt)z-I6$yU_GR7mo9@gKg^l>Xonrv;K*CshXwdI*@xZX_ -C~mh=E8}c{vz?h32oXdEuf``YCGX^WE^?PP8%tW|2)_7JVT>{Ed=`rJ|U7YQ0)6rvqLFM?=-F$6Y(e-J! -Hu$|x&g3AQWeifn(K`(+q1j7l&6J!$D2o@8pCU}M5U4l;tP7+iR{6^sYn-HxCx)Ssz7)&sVApJLd{Y$ -hFY$Di0aGaouz?<+k6Lcl$OE8FFBtaU1jbItU1_Dv7^7BEWX)eLq3s>@bY_LUqIm9BCC0fJ>Z(Bsb{T -7#B;ky>mxVPoXNc7C;a%ntFEW^YTyncq@Yaya?@?vIY*b?$`CRy|Ig_tVx#Uzm<@`RQCx{EZCMSp}pj -Yek@Ork#uCPs@53P%GU`m+*Fxn!CNa_HAioC1#VoZ~REfwO7smd@!;<*-wTntZaQ(r-SeGLd -*bi(JwujXd(r7L(~Mn?iu{>6}7MTJXnyOp^IDVXHK$;i*tdO4NNI6`_Rap~kSE_tL?>!oyc!q41K@XMeeEUs{l)T|!4zJ-p4FFM-@lL_q%`d>=GVuSUoMvv(w$D>^C* -^`)2OfJ=ykCsMLoUVe*ym#PSq-AGUh!riBjKCa(XFViGNQKde!pIq1>z*#%g`e(frX0PG@PXKi%4L1Z -={}p+uol_d~J5@_vPq(HXf~Pqw4_&z(ox*(%Y -P!|MTU8patdBv4^0{{W5JHtfl-X2%x7ETg$WT7lo^C4rPNoogiuqT_Z;+wAPUkXEYmeIFA -l*=x8{H|l6#!0Ki3tfj=cFe-JD~FJYG3)WqEF5A5nV8J5kl7sWM=b)^G^`-A1>DKRX^@9LwTRYa7Lth -@-TldIOXk{7Ec}sC0+~0%oy_)R4*l4|8qV>LEaDb2t3I@l{E-e#<}5Ndk$E4P50VKraxs~hxX2t5#O4Ei0Yl} -+s&Vq7)i7EU{%R%-p3=Arc0pWg7E&)O&H&f4GYtT(8CW|zw>?N6|K4|eaNxe#wz1ICKIYDRM{R`o8_P -&~s4i|KXT?$mS#-6fICT&;iB-6C0P?c9#ziQ_h5mdlQIL(MyvHM=av5&WGbFuEN~{u5bGso{AX_ue%w -D!w|7I$kKC?lJPXJyCHO%-GhvLwi7oe1lsu*OYA5AavJJ6w3G>&+R} ->oJ7XW_@2tNsQKyn=xOWe>FMb4vAeGS)T#aRSvb<@x!oL3j~SL(^oaQQ9%*SkW^pD7RP2!i(bh@UsS~ -YSxWjX%yX<#PvgN>%=^JYMvS?X#II>HlYjH;TC}|Wh8tV8Zqv5iO*gkU-_oIDK&Q@Ky54$Q;O*VI_Xz6QE4X)`zWqW% -`-g=O7#LxR926BDGkD0**tk1}#SgzTVZ_M9QKOTR$Beye-1rH1r`(e|aZ;K!ee#ry%zLw@X6NKi%geX -fr_Y!<>%RME&zV~w{;xmgEB?B={;u@9@^2{hFjxC4s^_nEh6xYhh!>r+bt+-m;Zuko)&0{*Lie?&k2TGF5K&FFW{Bx`ine!o_FYv``)!nbUQ!g=!@Ma2&+SX -i>ixwv%6(q+s4@y`bzdiaqQD<568dd*{xuU+@V`VCJ$_4G5(KKJ~_7hZg6)61{C`r76#TVH?U&9~m(w -tYug`OaOt-`TTw-~M;sJMjJo2M-IMu -_4S#Vn>!;nw^sUC5g1N@H3NJIY9~ep_v)UOMM`uHUq)q2wdUAuk@dJGV{D+#a%9ySKkD<8!^lUSz -4$p)v|QnSo4d3iZ`=8SCfuvB~Yq$yU!o5=JIPR+=&rkQOy=A2w>w%Iz~NJotOm$e{?o1@Jpw{GpA -;oQ*$X$8ij|(Pleju5GK!>nr2SV%b7}{IlAb03&xv~MD0+*bqtw8glT+^%b7^{xWvai&2G(`m61K!oX -!Yc;h%4{*$mzy-4yX|qf=c2mdvlXzs8FVvL|eP*SL= -tME|$;9YgwHtXn*=i#eXV}b>a?-5koOE-pzV_6e=hW2kaSu*2kIadjau01L2~W(-T&j3#YHF!aT2rT{ -l8s6R93r-K=}zU?1-ASt)`_X)n3a)j?Qiasm0xcfGp1zNtogaAldR^{belEL+@*&-Kd;BcjO-rP?CD+ -fh;`(lMplp$1wz)E@m6tVdYDRV{7a|o8#p*WAo@29`sf_C+RUgvf#LL>Uox*d)d19cz;@& -7j@@l#>GHg>I2KlzsJcIMKWoIZerd1155ASrk)cR#it5(=FD;K%Z-)zqZTBAn|?iO-ox?MVUF{f(SLs -Dv`Tn`>yZs|g_Wo6~eK%$6U7o98PG|Y#3h19^U9YFo4vwj@#RXWU%G*f@SwVkD -we_0)WQ+vx*~-;t9@=nzm9Z2r_UiN&-&x!LKpp?&#H+{uJn8EE_MCKe{} -(1-J^!V4)gR9|yQRT&p1SYq`80N1J)h4OUESY*0qH+j2;&LM9#Oy|aJ7BTdkE -2zlD_bZ2}Uj9rL&K;SN)|VHCR(A?CZyV?B#UYEW*ps+ -HmZ(z#6&lIt}`rSVjgCI?dPT8K9M_r|!+pnI^f --&&jez+48btsB5)RDT>Ja{EW%jS_-3bY7h{2yA5Wvw9x_O`8Dzcc~PWdxV~m;=}xZkIcfGR>tNDlhNn -(N*e5+kK+~?5w#gpx_AFb*pjkHSsGQ`CG;7q9)I9N!Mx!enjZD&zcA_n9DDe-vzPr0N_TzyDX#nop_% -SjqEswDY_ZXIwnx^rMQs@Q_8wgiZ$`gu+)>Lz=miR~!m6emPzugfta(K)zebnR>D6f&ELQH2O6G`z%7 -BOy#wmkZeC3^dF2rBA{T+AYI=&q>g4|}RxH5AoQ!Nz?ZiR%q1Mz~{zOZ>$9DB|tPGU{ -c~qjKX5}L019fC7QXJv%wIxZ2Noq_|>+HGkNTvb5uD#Zj-Vx8y*;5#)w3wNbthtCw6appjBzV`bw?}M -#v~{9=@?>jX0x=%~^`nRZHBosPHezj7B9$WJ66!I`nmXNjh5NVevDxI5nw4>1UC+quw8Y$u?5G?&rTZ -PFYvfid&ylvQ(b=SerisJE$t` -8gnsa$LF!54!RzGJA1qo!bd8cV~xN@Hv)Y&~`-uVliM#Ynu*C8x?gwYi*50t!n^fju%RL5O}R$2()ej%k->#n{_ItDkAf -l7P5&~JQK65)?BevYenFZEIv|e21ysS_G-ORTLR+cM#DRyPazh7ABNV(len$ros&1%iWNhvF6qf`gR| -`UQ;^LddwM!)@>nmYYMGmwXLXfEGEy{fEh#lG1H+VL@gZY2(y9t%q8cvk&UCfp;@oTI#So7AW>so5e3 -R?%-2e0Z|MtQ6Ft|wh<<>i*@m|(BQs}0-(xE;6^~ZI)(z%wu``?A=f1dvn59A43=XBZe&`hi;;M*%)3 -5M;4II$P={XRmS@AuWrMw;1J^J`-8yFrT!)1;;AuesrSuj@2``z@M#AMHI-dymrI6SO!ZHTOi#OxDsL -tL2@gxla{>SI_Z{555u0({T0H!WU{d&(qBLn!iKyFVfOkPJpF^$F%o#8a^Ae{GQg_w`%x(p!t8Q`M2^ ->d7<-6=ef}of6~1n|34ePA^-o$M{194nMGU+{Cgk&1|QYm+~D(XM(n@oX?Rgu^>295^nVubzw`OOE(6 -;0|F|e5c#(eP?&-UMI=Fl0&zgUW=Kj>zs=4k-)tvYEah?&f%q?W|846#%{@8kR1L@AYteO_h+C67AeOvkM+7T{E?$y9sgB`o^#chm~TWMy76m^c!ppD! -CHdV1S<$0Bv?#PL@=8mmmrN`JV64%FoK~3(F7KPaDotm-ULAew-ao}ZGzhg+7b8?ocogUAUH&@kD!d;b%IR -<&k(F7SV6FiU?D*v!Ayc&f_n&t5m*R%YvH>Rv?K5%xcG%dCo6LSa^!K4l-Napn{BX>nnZUwW>R<5H|8(!(T?`pAL?kCCi;Rp6VYl -1Wf+{Se|3a~1#R~EC(@%>x-grZlm6eH4KmAl(zI<6|Rkx4>(+h6Eq=D$t1BHIVw0FjggZz3>o~bVMtD -Z4q_G|<=BM(#`lm{-5_g;GUrI#}lm)>X0z`Jtrz<~q&jwVpxgs3yJI-UJz%&zkNo+8UV+WU-yz84WcJ --u3eFF^PM2h-CJ?$zF_Reu~`UX6E3znc8D?L7P$|M}I64pbjFk2LUpdF1$53Q(Oz>C?N{+xuJIy86IDA|W08@jd#&ycTEFF)8i}^}STwtNE} -42Pwp2u*;`;|Tbl7-+!ve_t`^a#4k)rb)0O9|^`cVfiRv#=ZWh-Sng2*aQN0nK&*?a-dUN`Wo9Zom7iy2{Evnm>6&&{{j35zcZvYPhw?d&dG!1~87S0$iq5&u%kc -GmHz=Obtz(mkqLeBj)S^pzB@7=q%h>wpKiHV8gzWeU0CF9GMEfdc@_ndg`wbxk2Kl$Vnaq845mz?Kfx -@8l!p_{h)pd{|kyKo%HPxH>_0Vn^ynSw -!+JkYX3D*B)9#dVbBdf>(q+fE(J7)puHUhFG}3!x3DPf+@`no-HVKyE`-Pa>zPh!G=1Qc{u_J9ey?Fkyn2n>JWXo;+D(W@d -`1Q>Ti&ygY6{=FFKR*4ZP(^Y;x9ug(n$1%@x4*v!JJ63D$gR`n%7^vP#7y7oi|ZzU6CvHEWJYSZ_~7ON!rt;s;Uu0Th22#UD%YCsX{Hvbk79@mE -v)7b$)j#s84vpEShpOz|xg|1OGeqxefG{<9Q+C&fQZ@sCpcuPFYv6u*MvpP~5Y4e@()rtvJP>bW#rTx -Jtu(-Im(K1+StE~_$vLVmYg$f}oxJa35KlHzxu_(2qZAjOZT_>@*v2F0I6@fT72H -5C6PieFCgKcx7l4Dm_%NT8~mOUlNwMiig);DgluRFM)R&DRMzkm8T0`1eu#M=1UlivN)zzGx;A#En#E -ZxPExAXVGG;vh+Wm53Fx`CUTx%oTFP142$)E#&f7gnW6QkcYoE#5Ys?z7&5L#h*y=XHxtn6n_K7f0g2 -Ir}%p*KIOUm3yOb=;#axi-%csqK`CTV3JWNOCn<&9l)?#0p{ltQKlha4+z2VoPm|)paw&d&S&H8eOIQ -516u%qAkD&M?DgGpiKb_()r1&c-{!?Pm1~&ouZ-!MGlJO^(8UvwyvE5I&|nf#N`_~C@wZOIyx>YA|f(6rCZl7T{;B}8 -PZWtAv%`4=|7e%(a|Y^w<5rhA!f}#Ec$&y@bm -L)Hw-zzKb{{-zfG+GDPRNqqhjJ?K>l5BDoxy|)j0Qle9wc9YErXR&w=v -+$)M*xj~oPN)+cw%vUOk6w@!8z#sC;U^Qds=`(9Hr4_@Zc6LT2KI&e@b8P6ibg7%3wJ8-#mCQqtMRfA -7%0~g)|F_iHnXWE)Q!pyo!x>%`C*G -g!b@nVMhWl-duVaF@J9O4>`m_df}_G>YkR -BmL+MA8AjZb@>0ig&kUp^{HnM*mZxL7fiLc`=Wm&;RQ(?q$QY6a$9(}H$9;jEBD-;oR^tr3+DY`y4a^ -y(XN9J0GxO9)FXATq#Gh)PwyeRS7{1~z6(Yd1EKGG?uog4b86iMHxI>SsdAAa~@v1-*Sv1ZL0v2NWuv -0=jo))iiT^;Omf-+c2;))hW3ds?g_ePa{p3cGjj79V`@0qX`Qzxi5x^UXKn=8M>gbkINB5LyE?*Np<#sVb?i17G5wTo;Azqf -JsQ<1q#HS81fkyPX)Y&e(o#OYU_!f#ENAbr|{Am<_0mWZK@n50%`zd}+KldL!umTRL{>LSdWxx9<_yBT -%&Ka9h_NH#P3joB}j!+Vtjjw{+^-qewm}dSC&P9FFv8TzJ2@lU@8FHAUii|)QDOZKH-))#YX{AOTnLE=p6sM9uF;DziSK+zM^}C_?s -Vp{PAS!ciuX4=FGo-{PD-r)c1Vz!w)~4BJ=BW=gys>ap}n~zWCzhy?ggM-Q3&)!^6XSXf(Cb(!nPw;K -QHcAIcgX^L3p&ckW0gc-8D0xmCP{P3~z^74W~g9Zf)wVdz?&W%F-LXP+^iR6dM5?8lEEm!E(BIlqG@9Z2V4lVtAPPe1*1k>u`oN> -lmmx8LN?KmUC8n98vO$wU!o*tv5j;}7{bcI+6J!HE+m_#HT+PEZ|Y^T?4S45%9@511%7)E&fm -@4fdJf0QBBe~EIqeEG68KJ@-OxmUxV>Ts_=ga7&S=OuMREdL=PA)O(|0O0@r`|r!Q-g-+?nM=}{xekN ->D3AU7_cH+Zef#!tT{(L6DBD2;+5pHK>IupSxNZ4SO6xHxBR-I__kJmRyo1XzrR-iN<;ZuXeD0K#znn -jRgz8GWKZF1F?c0|J1O)UU9exS5mnaXW0r-OF;4A1sSpav~QTAVb^%Vo`;5qmWd?5#*1$cx1DB~x-mo -oYjDJ>sL8Fo<0eg~us-bXa-mhv{Dq4PE=JG?37k}ssJuC8AFXYi-C-UYy42fZwG_$&1U^#Jmc{FRhLj -!GHzG4UTXP#N@nPs(0IL-waqp0AcN{jiil+g(c&*M$Ft3l|{Ei-UuMJCH0Cfd(pvD`){OkR#w&A06O3 -c#blH+(7=(?$Vfz5xSG`zvFY2hCv@mIgoJePc-x)xye1M;-7Iu%7C}kI&rP|Q=cLA_8;<3{gDLJEfZb -N!B><8Q -*u4ogta(6-b^2V}h=`2zppkA4C1(9Rg2q?1%fLBp3)#u5!NM8l>ZueZPG17L;Kg&ww0cF_2J*4LkE}qQy-6h{)+NI{Y8JEmj&tr-gO#)Kec< -Z?2$D2{H$>K)Qkc038G;Q(SRqF+RyPs!x+M2)Hf;(dVK~BdVNNl)N?QKeV3HCf(GKh`3cg -MfdzOEyYfGRHq0Z@a=(Ko7D9!PNwdu^Q}?O(7%wLc)Fw9Y2eU15X;!3k5)BU!4fBbH -IYa}Vx;n<)N9u2XfIrCs%Rk9=5omeffd}NqjT@PsYoSA@1#y+bugaZ8{bU)@@D|a)^_gfuo21w0wbUl -9&Wn(bkq3+oIVfZxJ}3tcqSrQ>R}T)I>(Pn1 -6@-&q_bcQ``iTUvd7#ZaF&Xf&+SX!r-o(UN;3Wl4rg10L2fsDBVKDy7%?I)J3 -ISb;EMW)Ixu(cT&BS&Uw>34Fj1%KjUYmOj^_gg>r=4l&X$?LtTehs6+D_S}OBb1vk|Ljc@=1w)IX^#N+H5wtc=2Mc -*I$15CAT}!h3caPc%yGXTLoPR`Y6T&cu=1YK4q8HztMPO{d8A-)^&`PT76zdb>WZjfBWsXO{D9L((&) -sty|yp^z`hsw6scUN4XrHc;X3p=bd-T#Kc5dT3RX}e)wVe;DZk`J{aEvU(kTQ0R4nc2gWd<0qrT|5Pc -!?*zJs#A3hEI&&zi=Os~-~rmH#z>NDD;M`y+Z{XZOkg@=ci#K*_agZz-JFQ;*TpUs;$Z=^ab$xqIlIa -5-dka)l2jyqUKc;}sWSRQ~oa77vEa;29C=!5KGoCCcMWr6yDI?eT&%E4HlpRH4$4{UuLdf=*S5uVV{& -<9tpT*>nw$Xlz+vaqla_jt;V9XrZfZ@pELZq2v=57Z4KfCe4v(gJ`?>1m^FeSghF`F62|`e#+gK>z&w -oG7{VpOfY1?`@IifBEGImG?EzKde}>LKYPjon5|sxtu?L{x5gkb(hS_%3{9LxRPZFG@xF9PT&SQ>e7N -a2=v2{d-Tz$E0~YKI1+Bqkx+J^rJje#|Me^I=+DmoATrh6wYj;uU*CWK{g;7fa&j^Q>aImvX-TIC`+c@^X_*wkP`8U)p$Un)_x_R^FeS~@%7Z)dI&z{ZY02 -+Y*ph1J=m@#A69c2I-bN~(dxEEtF=+}^Y;0Y5l1lo+U1)U1vsV(_OKtMp>*Is*VDaH_g1%JpU0J4PnX -1D<_%sJcbcCNosQBl%jvB>V-yK_0fA93o^QJ-w-bwf8HM`#ZqS7?t>cKZAV;e_S3YhAx!{E6>S0CfO$0_ma+CfqM -mIz_EowX#sTKe2oFZbhhTK0eoqKk_x|KRRCl7%u`JDqBgq1->fg`atC^N#0ln&_{vCdK`T|5$z~=iuV -7NS6-1XzW5^dfiJ!E62}9tiBAf(Z4;=EoK3jzB)0`ZUH7=w^&8gTp#Px#r~0@A@2GDufeXqTb(Z9mb& -07{r*hj$b)EY+v^9`#T@FzXP%lv?kQ0;v+GL|X1Uu428KFGTpA)^<5r=lu>l$%);ct{xqYN8m8@Pck$ -Pntnf&~jCwOQOx8z11N1LB~aL0g0e@`EuE0Jwv$#~*(jbBt@<-QE8z?k@aw{h#<<1RC^qu|7J$Z?r+w -ck?&_GKIdjo;iKSA7!KK5LnlN+(VYYbJU>~_?Z9C^Su`SM)^k@QMZq+OAAcQv5y}=UOw{3BP`!WzUzQ -+kR`}E-q8mDcYS??^x8H51>6~bL*AJ-8eA>gqJ$qeJpjgGJ$>%{Ri(@mjghyV -D8zohudB70QD8}4*XFMNFF!*jkq)Z#`#ZbCtUMCpaFd+^cS>$kOTBb=sUqm)~)Nk>qqtkAEv`I~XagX2hU_%{Rcz;_RS%TYb -DkUEzgGiFo^A9*=q#*E`aL{zCJ+5TqJLo?kpQ~XV)Tb=X``4(zs+24VGfo4k8bkj@^&GgYslV-Np%m~ -da&`hbCZZK=_+cAs3@ZkF%9}T?3Rdhwn_Yv$TIA3SHhWtKbqQne1 -?tB&Y*K&FAQ>uw7+_+S!nfbN9 -x1Nuj_>*$NnRzsJCo&rDE@r+Yy)(f%5gY{0#X=8l{bMjbY`=k6Z2B0x$HjNiD_3=H%xyToKG)Oya;kq6G-`0Yd8ZyvxC^j|-mIwqfXB=J1v5^WA};tx`$e5bCPCVr!?`| -95h808V`Ls(}FC-dXlGT9z|3XNY=NzY9(eushbhYk!}(Eq%(aw^Y(VLljh@_PP&yT4P{J+a=#@_1BTY -r`53$>OMVx^y4(%&z>`9PNs3aTi0h{LbrZ#ZWPZYVm=r1h{pV}_KNl6B&iko04v@#BI`SByt(gXE@>pk`j`OE+QtHZ -;E0c_PAaBSyOgva`!<;JC7ceKh<&oS!Tr>RcH!1T7mjQ>>wKmiVtalpohh8yj)~xJ;f`Uxr+ADCtya{ --L2lMM#YkEOj@54G1=0hQmSYyK4hC^FlnmtK{hv#Q74lIv{YSzB%=3jR$xb7M-%D;R;1h02ttq60Pm> -bvS5%c0$U%}c0uW@Pl&q*81c?~hheZ{0P!}=Yhkg#afeUy5^TYKSyl#l~D$J`su -gz^guB|ViPGEfmYlVf@XcrIg7~5#9-5TW|b?d(S?#rh3KT}^Hz_Vs`zzeiT@WX@k+ZXSvsT1|dqe0f{ZU3QT&z?Oyd%=PQQ^AL&OP6 -xHhjtfzAKHJ&0{8)0MBc_VrysxhNdEB6CsZfu$YWNdYpv{R`J+$4JO}11Bhmcx -QQntUCFTJoD`*HS%byk;heY$0GZB`+rK`UC%!$Dd|K+L!+!-7DX4MDn))x;#EHYp{Ifp&(0>LF92jh|SZ0BaxpU{{Oq@7zZ%|N>y!YOFc^-Jlk|jKM -p^tsQbC}@2{%s@pQMXUVgET4Jha_89)&IJ6>o%I|J&yy?*JE7iyMTDW1-wAJjW!c_?%1(oskpW^3a@s(Fz!I7BPUqjxUKk`KT<8U}Kksjvc(dVPh!P>%v2@`mY8TA5T@qh>V -953``tf@e6CmL4&iM(C;*PX*M&Pkv@%FD~+wgKO&>hp>yFYp?4qfI_?8jGf8?Y0zmO%+tLxyXd-}R0aDa?qjRW&fTeoi2zZ3kcdAsPpmN`(gGid -uz&YL!Eg8cnJyeJXs{K;Q^u-C-@F9PbLWR2Lul$L9yXFS*r2 -epdLw+QWQI9|V_-ju*@x*y*qkeqy$tVBu)KgE@YIinn+<1o4$a(I$=h&UXc&=Z+K6dTewcB5K;RP;Fy -$?elp=XZ@YhI8O3+4)qXYW60}^GzLlhT>TCzg7oWE-*8e}_4o)2nT8Ih>oa0=_JE|~g*ItJeQ^A+9jF9ciAHlZE?SABoSp|>~4wN{MDfji_M`93>0mUW!7GzMFBPE7-2ag4n%_QSXjYNVd5in*0|4>fogY<7u-+Ca1^)2Xo(Dk5eLf6H3W8>-)P6K0(p@av@Z|vByyv~g_41 -74gd!5`;IDp5K(A}Z8W88qT(;Vv%&Li<_DW4&}tXj2-^+9|if^{{NCFG^9E`F$d6YKqRCb@KO5W5-tq -ke(gEv(7m`vJW##q;vqf%3cKN0^@&LltLm{;r#NAbV)*A*U$Ikt0VkPti`JEWg_MgnVWG5dQvi&CEOG -yKg>?_?z|k+qZ9LT}}TU2kka=E|dq_QzI_0qc1ngKf>$fuYZ$-w9ro?fAmKfCm?OK%hdN57~`Xz2Hr> -qyuex!XhNTj`6R@}I2(QU?%lg<#lkSPH=Ll$Z#ZwwUEo -|?oE(7D);t7G(Zw6iTJsb^L2B!Slh(YnkY+w-&07m;=2O;uM0jd`zIE!vtXbxnQ?s)32XwIKW%tjYG{ -rhKHNV@`j7fPp`8nyfZj*AR_D{{9+I@P@4(6$;*%|59d|Q$=FFzwEdq9Vt-Ge#|Y}(jt4!7mm^ZC}5Y -fLRzjYl!_t&{9|8May4JGtapr`ahNYg$5H#`KIV>tt)b%hTu`!iI^`!#!8@y4b -5x@VMaB!5f3$2>v$sOt1{@(>tzrQtzDJvwQ!e_e;I^^!~E9=;PI=OP}yQWBW|+Q{3mDeO~JGMxRgmob -GeEk7wUzecSZCwQu*nmcGOLCiZ>0@0)!q`X=_fyI(=SHT}x^wGQbLGCZUpWMRnCkPRU_LXLzq4(%Q~F -w_ycCA2!!)W1Xj;Qpif&+PwL|J%dj!ls1H4ErVQ>F^iAw}fvC|2llsfbj!z2HZEGV8HSLs|Ktc@b-W` -1HKq=dVt$Nzkw|W1`NDyVAw$0z_|lA419jzI|C04yfE;whz${2BA|+d3*40(5)>aaCTLdA4!uV7O74~ -3t2p?^ey93f>SqtF2rcP9BK*GaxdSc_@E&;Mz>bvGLjwh`RQM7v!-HN6`YcEZstURk)Trn6J=^x|*0W -Df3vq67&xd=S{{PxL->0gMGmc-CT1sq;4#`kQs%0>-hU(d$XV31QT{JYogeJzKMXLdlAeTxEv}&%FMo -g$7mQ1O^N|YvGjNt{gSTQh$60lCfq-d!Uf1pIPj3FuNjX?$par#`+>5Kjco#B}~b6?$i_WOCB=euXmy -|ar};7pu@*WyC_9DW}E8kgZ$@orp$YjHhp!mYRie}ubnFa8XFfp6m&GL_6CiDVve$b;lZB%Q1z>&c6x -e2n}da+I7P=Yjn+ItwN#L!YL*=zgGoicVzH*a;qRXSv3$agVuI+|S+nge$Ve&w=YU@q1A#4v4SBRPVZ -X%bP4y6}I -0-etcRL+2ax_YO(W}lkPm}pNZdya&cI^E80ZRd&b-1{lPovHG1EdT0Saw$^*dkBv9>@*JOfPpiU_c32_zHHTGW -khp>=pOeg(gQkKm&~?h+8Y2kK@HnNJ8=MDoZE@+N5{lju^qn_i;_fyhsIA%B6t%iF|fVw=}1Kan?KJb -U#iodFq*vmN%ljcoV;ufS3#)1kJnO_t_AO=A5tKnYr+4cfJ)jf)`C|@H;_rn1&$5MfligxV>~=e3M{Q& -g(+k&)1Gg)4DxEs#@6Ls>o*j0g{?o5nC=YEwb?7i^LnCMso{H1(GMo*Z_uzf_Z}=?kf!b^Y?R(i_)&& -)Ii`~H=;cI!a_oTPSJL+Bb2E0jfF7(x~EK--$Rh6T+=u&+|$N1V$_kZd?3R()o2~sh>Fm*F(;}*`@zePp!ZNaI*EEv9LAuqlbj(vWCA$(Kj|U%1HJ@Yxl3LF1u<%-TBOp| -8t~ffs!IJ)wW^bDzZ6`n!T!252H<#ZE$l{V1RbQTM;={z2Kbb- -4}1jQ!tx;Q5WM3k54J?FjQHF;g$1UX-NG8eA*ylj!3a*9IWY-?1ds)i0csn6(3`VRkoKigmHf9WTf`78O5%z^%@rg^N`eI4B4D|e=#p -7ILhM)^y*Rql|dWngTh@6aWAK2mt$eR#RFt -TGU4=0RRAs0stQX003}la4%nWWo~3|axY|Qb98KJVlQ_#G%aCrZ7yYaW$e8Pc$3$aKYZU;k}b&#USu2 -0BAJ9#79zb!IS-wlbtm%+g@8ScWblZaN4%WeV*WLOWx-?F?y5Cz__kK>zcH&?P -{Ub~@7mL(ifaA!>pTyyedsf5SA5p9YQ^V1_t1LJ%1?RLZup$1{&SwY@2l~A_Mrzpb -z4S;^JcS*RiC--_k7H@b6s;RQ|vPOdBm-_{XM$4lQ#ify!2sQAJ)Z@w1fnwb+I%B*L+>n-hfth_`X0(c(G -d(&+Fr-zis`eHm=9@3js|5#I{>UrtzTI+txm?Vm&amdYdX!Y{&IW+K6-p{@tdMiESjjfr`^{ov0_L>$ -&Y=l~A_vVKd#$qtY3-tzEZv749WYfE!Vb>+NHu`_zLEA)&+(a3$<)%cyjf<749g-+$=C=d*MX%F>O9T -VxGX2vOT(S^nzKqd)E(vf#b>Jsa2A(^b0J_?`ct*7?O$(I&)QFSDHJ7IED2t-y&@bN0S@)*?E4)3i>! -3$ZNzV)b3D`RC5dpVT_9tkOFpYb>2_ud{XzJ!kss%5W`9EHC-5G9WHkY@(LeRaQ}3@?FzBfI+;(Ir{D!+Hjl!f0b5_)Jjk5zncWxDpg2VDC1K -0U?qy;btMMng?FB3H|}@y-ZRhhFzz4b{Y2iU<39br+IIc{ww9vv?SsUr1^4Khwa=j3 -qAYQI6@DI19m@A)JcII!?0AQtN8p|CsCup~U$F9;d%<}15U*!V{1VSa=XVVfzZUCA{PqC9MM6V;#!_B -y=JouJ!J_l6gRAgd=MhVJeE?VD>lxr{0&orZj}ynlHE?;Hcqjg2d5L4xMn@8%sN^AGr) -OK0Byw2vm5Vr^POj2k%4#kc^<~Qhxu;eydn?YdE`6LLZts^aTYzS9UppB1K;c5e+&A775%}6ev#5yB# -cO}MMN%w*DqPLuFDp^>n)3=>kNL@XmrPEOKw-M#f9g*uCtc>t{U{U`_b1vioW)7^tDf-ul*(Znm}J;8 -P~6yc_tX(@xY0T=JVU@K7cy!LEU!)hJ}Eo68*QL6R?HeiGOsfv2kU(D0v;_8&)xYkJ(qAnPqHW-cx?z -r{JgE>y4#aN5ki)i(95m*F?#;4H5eG8;@;wdn~W)UVm!%+cVKqR|4rJHxVocjgh-Dq5K*6;^=_N)&u9s38e6cw& -Azc>gBw<6e^wW3Yb(?@0Hv7R#~3Ht26hQQwwh-xih@=3R(3-gIqYYhqgtMWQ8;Cl^)#rXst^-{6;Ci+ -1eWR_$0~+LeRR5~xSl9wE93d*#)rY)=RBCc-aaN!=pbJUyb)h)A@S^*o5Qzd_rfj87!e4D?yBIZe@rv -@uls-emjIA>(#G%y!LI?Mk4{?uSnezmL&ng`|teq|5D!F6pBB+mIWCll6Yvs&GC`BeeG!m$9HS8Xcxl -Kj55@nUS96bl6iV9 -YiHxMfFbx2Q{646@2L|Ib_>UwSL>G$VQg=gVUsyH!*o9OPNu&?IoliA&`l{QHx%KP;7^$_$C;J}z$Z1 -Pf7c3?h(^FOq;oF--(H-og$^CPg6XmRX@4)%F+jg?7g{D1`_4D=HXQvp)%TZ6co;9!su=4#m;N6q1-K -$O6eXow61;6Ex)!$7KwVL?!-v(i#IOoIDK9(Y=AvnnC%UrIi*HH#OOg*pLwmYWb -Rfeh5*e&-_*wIec&^Ztu2Rp5-xBgAAY5fE_Th?A{xX}yjwb!+_Luss+SAV!oG5A+P7_&v|H=ic?Dk`I!X3AdyazkHHy`Y9YnB0Ril|+j(evwK{0z-8a5} -ZUP~05&1k1L3!5H-7$8@Iw|Elz!U)zzc4)_?6?r!6GFMh-6>`3PrBOUnPi{GgCf9`r9>%90i@jcU7k< -Rx2K0M>W5$pSZYQ5vuXO=T={&!o?e-%IP(C$CAoc}C6W#7@IJ$XEl_aTRCmxJevpwklHudt~1D^#8ft -6}@$8e+ft?CZFPPELfq7qS8Gq%8MblHWtC^pSmUygoKwn-lwU@aNmQAN$qLAFqG#FBX64%(srbVZQfj -VxK6=8Me1B*8I5ha+-(?J$gX;ftRHXXz99Wv36aC?FY#k`7LZW8~E)t*nnr70w?r9;6yKM!Ek|Svx`8 -ETR<8<8z>xl^kB#3G$V4c=1@nx{2wZxEmuTdE3kC_)=i&H)%BKXZ(fAGc^P$Hv1p-R4cA#9>pY9P&!F -C3*qyJT&TouS=apO|qI|ml)A4TFb>JP^HTzw)Yh?L$O>7gI9y%(-K5v;3VY`G@{%R@k-7%+9+SYbm?0 -*ivCDUpZX4(qa%}CdW?;UgWNS_e<<@;qugf=t3p|4jvpqCxUH~pX`WP7B!+KP8KX<{Pna>@dxONNtrA -xo$+nq+wUINYV+-IfIJwvKrt;bmHFkC`?R-g(3DZW{~Ue&n03{pa8vdh~_PGZVDV*9vs-j77rye_@M6 -EuQ1B`%7Qj;t8Dv%%}1GKG3t5-*G(xSnP|8hzIqt4x|qqls3#yI@EZ_=yIQx$Bb4!Z_TkTE7wHB+&%4uy_Q_xmyqr(-f#8)5=zdaLeO!g`2P -)>|U$bNL8My;-M@^s3Iht~tYX=5>7?b;`CKw1&vnO}e;1x%oKW`)^Tjtj;mQOiNgT5|%s@mfV&rkOzH -#e|ItKV%=WNp0=*idyMT}JVtwSQTJukw`Zj-1iW52y`#NwCF;6_cg-hkp-MB|Uhq$y5T#deKMn1z2QF -DY+gXJ6vA#KDl>hcb(Z;fe+k)~Jvn_glORh~EPeoa*gLRQlwxS-`k|pa>#PKFQe}H31wDYgv8|%9jG( -8P@JM`$`1X(&hKOM9Ms-qJ`gtTz6uiHdhKKN=k=qme3h7qA${mJIpAxj`otqUbrWBtCzDoRR_k2IFN* -=q~+ZV@5!KEf6N?rSWfl=l~rcE$9R&>7Hc0q|nzqWhPquP@VxoB=F}btcva{a?yF%*%Jh -$XjS@W!iAEf2ZsB%Y-=86r+{8j@>9MmHX2? -oAKFtY)0Zll>P0&DMA49|wHAVIKKAd6H3v<(rRs!uEcH^?G~68~O=bT3jfiH-CCVq}ON2LSnQ=Vk&&l --nD3~?k$Hb9*?=T{-fex0OK=qludZSbTrK2&Ty%Og=-bD&PWR(FeirQZ-Naz -puS461ut{{K!A!q24$v@l;tq-ijuUC%6`vh42>qesBNY+mtDCOiV7^xL)m9<8*G -;ERb#*N;<%XuEg*I!HiLR~=$PM|fIgK*6fjFWbvjYC7SBlVX-DvCq53{Zbs0(tpguJZAJ4`;_lP+<41 -@iJaCv@*^#>OLNp3@e)PeVQ<8ZGsstc#E@eF@*Lrphu){mA1-nmVN6w@d1M31mUzI+RHr^PTG^zcpt_ -8{q(CxP%uv=~t*bh`jEtqM;eMVO{*jXT_tLQa}G3>0)KJ;kVzZ^t`rWLgw-;OepVB2BBcEp4Y_V#%fY#v!xk<_KtiG7mzc06XBWPQk2dj;PtD0>ImUF -;CY?M~>sO~o6Xu;Z$-y=ABOW`+K4C?+$qN7xj8ykDJT4K?Y`fjpGohkjTIygTxG7L=EY2H>~0yjV1#9 -kp)FIk3>w#d*-%x(yD#IpBqdXg6jpcd&czJq}=Zh-O%}IG~Ps-B -^okY8o@@gQ~FAlnf`vu#Pi!uhGqg9{D3%M=oeTGQ`Ve&k6{@>+^HerRn2MwEHGo9W7nTa|j9+y=__)L -6tpilH84{5#!^r--k?cOXxuj-~8K|hPpmGd5~@4_|J=Nx+Agp&Ogk!MmIExjo7^kJ7Q_cE>>dQW3NZA -rA#duz|eKJuWYr}0+Ul;;)g~V6hy9h9{s`XB)C!P3e?gm6yl-UvljArT>4&R%ced$^W}CceqJ;MJfrX;%0l -dq_yFAF8j>>z(ZX0Bilkyw(AJTMGW=eags3={^HL)Ts^!Jlg-#YCMR9B+U@x4}&CjE8~bmbO&uQKymO -xw2`bu@BRqh=l~zt|FS~LYuYvyw25%7)5~A|OhSIt-Tl{Qe)2DL -OQf+-v^{_{xp#`1I{^!9V&56l4h3(6jw18`_=_}OYtqFII>XQ`U5UK3afz=V3i>O#K8`Uk?PtuieSou -Lp-~1}mR1+4G((Rb8ZpK|8dhkuH3HS;no6_6O#7ii(n`_q=cwc9DE<49bUlJRbvkTeOHNn1CiXp9R2E -8uU0XG@{D@x@`|$}THA1NeTS593$zD(l=hMZF7}?-CEk33ahBCnvFEK+8f~e*QN=_v0|k078{Lr&C%j -q0ezlq3>fNoNL9JGWue&857b>uW`5fAN`2}T5MIbXcT66yn8y!a;V8uVS-X2qKQMVYtR66#M=c|8f= -Ps48+PP1ZUntu2#1JE~puuGlIju-opr%H3YXxGwSbo|KJUJlrTn)sMq+qyFdJGS5A?INxnHp=P4!~Dt -d$vMxQy7lWbg_vtzN#j`GRX7OBtLiWB3$ab!UQ0;9;vSBXlHZ%(H#`m>!|TMW(H&c@ffKNWBjDLC^yv -u4;rZ;)n!XR27fHmoEbnoko~_+S%i6S%iLFj()vDi -}!1d_g_SN%S1yb`)Y)JaHKCc0zcG^6;l))H0v-O66GI#)K>6mu|-UT?g|qY>3bF8K3g;t;l6&l5w6n3 -M6YIF;&zAz^r2eln~CL`ea&ujEF@8u(N2KP5@}5_BKytzThXUht^4e0kF9l*Futx0+XL{662|3Uvags -Gf*t)*Y4fa5ZV~!Der_#qfPTae(2n(AG6uV$UyuA%1$43+PYOW)HtBf-H^HBf)a{hPxF%rY7(s=zJ^m{_ky~^hhRbvNs;vo~8A?KfMAvE?wkgKu=`tth+bcS$FTuyC&Y-rB9q+e3 -$)Rd-~Lxju}UG!udW&-)vEK&#!5__;Gb;T6Vv_VBR*{!8@~&_xAVg`(N>}zBy6O){Mn1l;K{~&Gf~1=Y8BbxizDz#Zw`UFJhfar=Iu#?w`xh?@dP= -wxJCUz)wAwL-@bC?sL_soAuv|vfhhdKia}}ECM`>(H6p$ra7PVYEIcEr`e_>CM;`!^Lq1LpV`jk;K`Z -SqP4f8j{vULKt@oGk$cTHEk>KFvjzj2S6K7d~H)qI3Q7=>_;iVpUnF+kChE86tWvr>w+IOn);vbHT(Yz@>ZhXaJ>FWEm(K)~vvuQ?T5d -Cfda@wg)I@X~pUFv8wwwG#?TKeZylvQPm~zk|UXhw5vVf_p?4DGJT=9rPq?yMV<8F%h4Tm -w!!(-fuDt4VTa#RoGB(!HXNjE_BJ6IA(Ynz839c);k@~7omSVvaZ~ -&S*VBa&mfJ(!a0cCuGgm4JBOTi{fV^yTTZ+hLyEg+5|3}3FNt?ef$B29v-*bV}c0G -N~=2TI8&XVr?2jHXM;`AM{DLmXnJUG<-A?Rh|HbtAfwockjf$A>Uxc`Vee~;hys%N%zAYW4@aEZ2eIM -n@g-1lc2;f`!2Gxvbs`qS~8p`JO$vBoB{n_#n@9g5cW1CAyweN8WTnC&>iyc}afSwFEGn<)R&(1sw|M -tbxu7U82Pi!`BppT4lTC3-2kgY9Ek!~x4`*BxulSscEzgV9=L`yB3g`z9kYGTv9s4O9Lm>ZMX>KOL(uuW#BgS>3}jfmS`W7$J{;A;h}qPyQJCPE%c9BJgOiAvAx0q*S{*q;2ZIi#MkTjcl -15$H+>XwXl8c8U?v4i>ivml4TFos3V-l(x4K4r-ox4!keX_!HfxcIM -D74~S5*Ka|e*yW{U+({@XnM7ty87WM(o6TBx|xRUV(SxaBFJC?6)lCt}g^;AB}I#MY6>FgCs&z|7;uW -ZX6ZP*r3cBPfN+}DHjvG;NuDMdWXKK`^-?3d|!t)W;N_>|k3j$^kRuba%Vsw~(ixW59u9ZOGJIhKcW; -?hp$To%V{L?Fk%NrW%s9p`rKG5AK-BW<2MIfs|qbtcO!yII*Jpoh;R>7vF^N9pfGKY%t3d@E_w?1|fB -(x#jw!fvaW&GxNIUmmNAz9k*kSqb)*H#M<=4ZeaWub;`Ot%LVbiauh| -wVfbR!?WLf9+Q%CBm3wE*2BS?#SmK|L^Av_npc?|yX=l_&%N}hNQ`fD!ye#n8gBf9j#!yK;yoH_LUVt -tD=zoLA!F7(&7zDJGkvw!>yc#KSr=XD_;?O5_L)1H@kT)y9%_H?4%%K9(DAMH=b|1DMb_N6F;`9F>Pe -&`pLR|Wmrlp~}L(j*_ce4oJg-=Un(T#x>fKi>$`|8(05*D^Mg`A+wEkCjm-KVh`4*6D+YeY3RGP}xrbffc+QpQba`3&%U+A^UlkG7auzTKhMz-B4!NWei4a25LwXJg(On(5S(Oa;o@i4(|M;u(#ZV^4TvrPr~wFV)<4zRzUtL -!uJEZ{lbH^g<`arlv2Yvh2iHSyVQ*n49nonE-BXp#}x!ZPtLf;yhWyG3|cZgC7$q?oevpe -A_6p0G|2B_-4CI>I_uy+^}$Yz!e&Z`?a+_3vnsqT=WtD<&eBzUStjK2Q -#Ns&<6D*Bi7E{|S1P3c40~PLZdEqCD;5n0ImTrZA=>$N(BSCNz6Fb6U%++?HE9I{{B{ubTOaDELi+{m -bJN$}9!)hu^}uCh&E3HBLg2d+c&~trQgo=AB61x=mTVu&eFJz{4*GgU`Y}6Tg3Jy1(Er*sZQu`Ep4>_ -J;Rl}h1MGs}3}fT>@SX6j$!pnpK+7CpY<*p%=_G6p7xVOQ3GSplz;U^L=E>_>u-MEaT!Ec39@C6J|Ne -C)Kew6JrEV4Ho;)5Oi{-d%(S5a$H`@#2a)sl=tS@;?`Qr6xH|M@Ou18Bamk8RHejy>>MaZMw3q|OYZq -c?E`L=+cAH#RMB~U$xZwDp_^w&Hebu;C_mpy76{W-i}hqSwq=KfO*8gMZn-3+FYgahQnP|#n>Mz;%OEYRf;}XJput>(Z -Nqsi0NWtHOe9w7k0}jq(9?Il8_&l@;*WrCrzM2!f#jW(;X!$(09=;_3w%$pj_3*H4nw?(5@;P>tfjZv -eS`ibshZ(D7!kaZ&)&Iv4_*V5g?i4kB(e`SN0nNod@L5}~SsSF^tufA~BI0PQ-{{eDog9BS3m)iyk7% -U+jou_0dzYyBpTk9Jo~rLAqfsDj;@EMV4kP(A*5=Eg_a*RZ4ch*@N%6KnlhF1=UTt@Zh9lRbwQSo-;O -Y$8#(7gdFA;G%ohVf8K0l7#LH)Pf4&OO7F8>{WqkQu~O8lMM=3c9%C|iPKLg>@Av>|2s$Bb6xE2KgWe -4w=$GSuF@Jw*fxGGWbs{_bb<%PC8(xwCIjw#Y0VyC(8Hq -%X|A84RpB#A96KhxkGavE7x2DQg0z|`yt3t%0lQ9Kl(#`j*4;eYOW=I*h(Y%M9o<9cyXMt%`tk!`T*> -Y(&hxZkS6qBR#%JAqM_x%;>IaCi-zpM4s%W@ksf(Toat!O}F=4>C-{9D~Fd8{u=)?65(2Qdv@HT8}f30724AW9dvZWeB}!x>>rl($eXZv4(A+a?61kE&~=usL5oew^*B)3_Qvscq~lXfKR5iL$;`qO70sag2Yi5uMOSp}vpm5yp7*N3 -EUFH6k26oH7_)m^v7>XpPZUJlE)D(fg35MvuG&JNx2UTW9n+Yxw`_YX1Le*fbx}#5};Uf1Op7a?Zz%K -Fqj{=d{83?T6+Lti=5k!1_^(=yt(BUkAOycp2)ISbD=EY7eaTE;wCN8Ts`z_+V#^GS*#`Z@H=V&xaXMw*Lv9lEQ{u2=yh-W?XVt8TDKw2Z+F9%q}@looez8mwP9 -IZJxPv7Rz~IyHl5`BeUqMbVJrKyUKwf9?E`ox#~de^{`P6u5*%~dpkR{o!U5#J3vGNlMUS-VdStE{Gf -_4z7NhVk)|ZIE5pcdyFb{_=&Br+S9QBs#`yF*! -2s-Qp$C2!_DkFCI9gGP`hmYikkCJ|~h-7G@y5ExCWnZ9d70<7XO&q(h+r%>a0uio++@>GuvmYv4^hJG -gFLZ_XpvX>}Dsr1%7Y$EQZ{}sB!H>q&&S@}$>hZ%KFD7~a$G2Hy9J8p7L|05ANr+$DHVSpB&7YNC1g`%IsIru`Q?>%TZ*7v})HfXfN^m*U!P;yaj^DdC@G;yXPF-+h3)^ -S^*^!kFP04SOQo!txGq1FsTpT-!~&$KWR3mzsDFj)8Z|t&SVueXQ|M+Mis@K>acwaC2=P`}k+kml(^a -)*j_|t+R_Z&Lwedoi@t46p`JeyVeX`S97~JjN|=N>bpzSu)atBGxa6;w@oWWSu^yY_hzHh|97HuvWhv -{X8+3y#=mpE=DfXkLda2Ubk=u?&RouS!aiOxU4-cG+N;3dDI(&y+lbUVMPzXiFmvI@7)1K@p^Jzw8sn%0Qo4X&j9__($EnV7w_9;CJ`c3;U%?iB?yU -}l^{rzOf@u}c}JZ)LIma&?1!v3i}3z)VZeC$KIy~krhnKf`KspVQU8MSr!^UW{2joVK1HUlssC61#?ENL{NXgcnj9BRw*xl22=O^BE4Z@*ZTzf_ -u|pi2>?vD~e%6u4Hl_Q%3tQ(6v~87@tSnr2zu>oaCpLpE(h_T_#dN=T*&$^ -P})&JYz9qEtz=T$~HU+>S9ank!=%ZuBP?RmZ+-P+)i{mpVJ$~}Z91-%@ujW=`E<5LKGVE#o+qgExp -60{%CH3o{UxTuniXb!Jj@C9!6%Bq|>(fM*chPqicpY}R?}H}CS#!Ilm@%D4BGHl+sC(rz=)ccohdz)h -+TN$?$Z2{arP%*OYH^dUl{HmsWhJI<^NJ8{@8uX_r)Xeo&WaN%zWI~zkBaGg=Kx0*=P`%P*zVJuElY| --XibiYJlMSa`3L8T@K0f{`sWzobC&catMTm6rp>9JBf^YZEM#2g`$Tw>Sw8#fiZkfXkkjLp>qof`t|x -}R@FQ>Wv%$i8T;DL)aazz;OU!;W+Z}u}T0&Yn(AVlyv@+h;UyPQLE~E)@%kr#~KDcSm`Dkr2UH4pw*4 -BZ3iQ^WX&xKz&@$&0^?}mSB6ZmI>$v>OM;Ge)4{Ii_=3EUY=?CGx5xCMVx15m~p^;!#ra&)_+Mf^8P4!#xa^_v}uGmSrO;(Uju(maq*pWCdjmhj$bguT$y<(*o?8!4ieW3}ZoU ->9)gdbTEgu<~VE!)}yWzDH|doRuS;Yo2?`+<4{~q?a~yx`;3y{rxC&5w3SI=7Ij`og&QnV!|8NPnJmO+a#oH|ZS2Z^*za3ZUR^yu&eCtGctc$U3RV%b*QvZN2>{d}*ZxtW&&N#BO!y@J -tYpS1h02a=Tls{{>;kYJ!(emGDvR{|?YYmJuI)ZesNS`K9eUjsgW=!{RTb~`-)~zPJ>Pdq;<1r4gxEU ->Lvqwvab8vuv6Y1BHK)*6g(Qic({elVfyObNJ-#_Lm`rV&IKYOaE-Te;q>m8;a`csU4jyV19GU<0`0{ -z}+(r^AS{fx#q{ZP)h^uxDA`en!IH-oTWZm+J2$$yiERiL4L#?etU{Ev4@!xgOJ5Hn{qh>Iv1JuT~^UH&AdN__w-w+FC5wwsV6El -xlUe|ai2xT3%ly^-OIUp#_DL3=J=70>*zRFCFeoFAD3{io9jj#c=qFa{q)r0K~qosmDRMP(%^6C6=hX -vqVz$OE91T4YjV!j?GR#i*NRR`k@a=h6m1_AVxHfo)-?oe8rPMS*sXFscG*ni{VZ^E4*p9A{g) -N=U!1G=NdH#BeVd8%Okt)09+@7x&Ur87VwWaL`vHqR1+do_FF3G5lWQXn*udWt0@d_I>}i4O{twc=EM -BmNx@;cjS<-y|Y9riko3<7GElSk@9%Kwdjrq;M=M9c|O|ed|zvWuSfi5SmYcD= -St{54(Cr%`Mt;=qwVj2OF#6ZR~wu!X@@*V(Z=LDCnL|uc=VBTaG+JRk-TPJ`5gQ4`Q84mj*)(XdA)Lj -vJ&eTW7IFk$TLx>Ep=UE)Ne_6mt*E<%wufaoBilxZ^(6BU4GbsvA*Pghraa5gucW$%vfJaP3TK)@6ea -h|BQX9#bT~KH2ableQ6E*l128Vuv7J=M!(sY{7HRjO+sJtC-x^Om=xJJ -#dM*o1gr%J*?^oNnul`ca;0^ncs+V~__4deq^oxP$j`-OB;o-it(d6ZgNxaPGcl*0KM -*0cn^~w#(N~}M*FKsmYLUNwOzLZ?QCE;C;7<-<1*k^Ez^cglZFCVbTw<5d-sIg%i9_IIl< -kyWxz-jc`q}lu6)0u7zFwzPJootihj4sgSyjI*WSkD0LY3s>lu&u6`f;yMWLarL+&+-&B1LGN9T$Rg? -D7dZeL0TD8HJtvznE)~(b+brChE?`6I@(&yeE8!z|YrpC)18*Izy>(wK@*H#&UY`NfNtRUJu9!?)_UV -d!_$~&JchEVr@YeJb;l(~mvQFylkWv*K32-(bU{T6%IP1dTG=}7aL=|(5#6)&ZU@TbtGPrpyfw#Xl+p -}(@9LKd-~`thD~*egK4x?7CM>)@?nTuoKDk}(8ko~;RKyODM^u1jEJH^GmKrDNRI>RY%!RiVT`zx!vH -_WskbySTPMSN1+|HovJ%8_v5XGf=$-zLukLN=__qzs1?LhO*w|4X@@r<~MnQYtIv7f~5T}xJUKKxT1I -D5Af_c%Hen752Smy{PC;7|Bvv8GtM8Mc?bUZOY%nySMR_dv;MpI<4dCP&%z%`;{=H^_D6%<`@@Jxxh2 -<#bMLm1bXe8Ad?V@5oPh5b9dzLM0pg)j3;kiDX!Gc%?9;U}uJ1^s_bkwRtn&TMrxpY?QKn>OoJZuoO$ -mH5YJKn+eora#Y=A5s=BcE*66N-KwCk_Ye%2@b4enEgdN09Gpv}Ds?TUU-lo19GVSvA@I|AX_iCl|6I -Bx^mNS;gNXBYgFMEEzLJtNz2XpDLD(fja8T?AOKSTx_q0Ne6gMT2*Os9luBvBR$6vBUL;L@1~m;d!vd -9I)a1XgB%Uf%ev;y~V(7Df+4Oi_LeX&C3_e#WV8~Pt5-jGrz(a@{gv`#ypfWL;BgHbmK;MQhE7WLV08 -8(%hu>s+{M)2DrB(znAta_rnl_KPX0j+p2SIOWrZcKgQ%s-h%v*>m|{rD$t -f7(olYNT#4>@8oVvxXe`WOUh3mh(|MM{MBf=X_n6zoxh%$ytp(4J|8eij8Ic=_HY*)l -dUyHpDnwgRV-x0EddeZ+k(_IGmszJsn%5Z~tFN;vmC0lI^>GA8MF(H(5xa9Y^&GA*u5C*!gEqB~%7_= -4}!4|&({LyslH|8FMz9q$tUF9H5H09*bzZQ#7t0bMlwOzy)5TA|!D*e^lo#;<_y;`0k@Ha2P5YyBy$J -9A<8v}<{5TCPWHzcNmnsrQ`Fr;GL&oo>xBz`YBqRv2aE`zoP$dJ$-EG3UUl7`Kn~0%>@TOkx_AQ{_Nk -LA{F{A|jswlLi@^Ce;47eocL^*Wz(B{{!_mj;P-;vi{-mCdQ?J?k|8A!}fc1MH%;tV*lp+*M}^kb_4o -9W2pDu!gDUy4NdkegH3jD@6oZg#^#@HMc_c -e;azW>m?mOlml1;cBcN5&+K6&IxT_tt%kZEdrPlB4KfKFK4_0Xdfd`g4CL#ydDPR~pCuxxUB)`tCr#a -%%+xd9!!yWDL>YydK^0qON$sq2;wK0`29q!+ds_&nPoE6fHfV7YxwflI?-4lw)qFr&G75)ytTQcOT#M -=W53@SDQ|LI*W-+&k^VH)}@Ha--~C+-7x2X1Mtn8pa+?U^KV5NYFz;3&$NuVF8 -tT@*cut=^AT$ZfAT+q$G6jOjK_uqJf2SG{(?{a*)$~z#Y>eUY7 -wK_4{xm&~$1lGNJjU=Al<|4-HRF9kyd=;Eks2Yoxp(uW>(OqGLl>)cQC%e#(cO<{uh!GZF?kpdA&bVxn`8UzJ23{jqb~8J(7wib# --kjRC^EBf7KZL1J_^cO1^mux|FTf60ly+dX$t#@O~GVISL-T2hH>iFTVYU+ --4m5LVJqk`*9{w*!em%mD*JmLCH;UbP+hmZ9dW7Eub5@&4dHwxuG%_V2#nQ#ivEpMvzlC1Y%(&Fu}#q -}a%%Jk*WcLE0PcfxVcx=L~UYPMQm=my*$e|_wA9pn$i=fah^k4kKfF4wuz=D7m-#{J!6>t}gxL*HIyf -1XPlxt1hxzY~^E8!;Mf5B5b*1!y)C6!ijyz;s_>nUO1t$mDD_M^`OkNY>J#d)|ijr_7NC$_fhR8;x3+(+ChlJ^lEy)S? -4ya{=p3fKQ8ls(NXn>_Xrl)X;G^-?eN7xDOQ;BjDr5%GUh?%%mz(Kuh8q3{A=@iGPga=PH)9iiQ@o87 -|Nz`erKrx+*uoRHm(4{qGbInJ%HP3s?AKpWBp|A~Cj3;N6k@34H=oua1a9r8QO{EmeDMP`1kyA9|LiR -bm^xdyFoNB7@|=A+Lw7=NFth$&|VfycMaGh+t9$Nz?37;+;6xO^3OrJg+EjI9YDf6t-kxF?PXY_~gpw -X?}$AJ}T@-`%GCRP&;fy8h|4=#IHV(Gh7ou0=BzT5qSGtnK -XQhyanxSs^fN1e)M_@Yzoq0m-2{}}fQ`NYEc#~fP0lB$aNE%oNwRLH@TTpQM2Sv;^8cFnm*jqkVX&H* -QQ^P|meg&ZqeWUgE3a~>^p{Khf)3TREZ7Ftyt9)0cDTG!h^3!XE{F`*5x?L0P7Vps>~w;l2vvp;6Ee& -Vp_Sbwe&K8G|JX3Ri&pkEKG}o)W2L3w>K0E_{h{~wPy -Pw9$jlag<_Mtld_xBfM>;Vb8OTu0VxIgYp*nh?u-{T;;&k%PX0KTag&Z6zWd6(KBB)@BxS~D- -#EiX)yvb~FI=BMP01@Ewp;-kEJ%B6JGx=hhczFDGKLd&(Z0nX9%$e6Q==iQoXO@J|Ac;6h2(jPj}moB ->VY(`{1t_SkP1^Qxc(`ICBowq_9zoO`M!Q8hb=aYE<2zco8DI(&s8R7QnqM;f5zUZI`&8*PnTueF2D3 -&@nO`c!KeYF-j#l+9&s6D%mfN!~e@u)-X2N!@Xztz0w+OGJ{-aI+{w-xhgr -^CloQjDsY>s?l(?^7eE+nO`vR~`C(2B-2dWpLyn4;C#6IijPP^uOQPmS$w>9=W!=UsdEnTpSxKHv4FR -qso^!n!^gL&4%tJCG#P1V8to(0k;L!=nqQ=o2D1+x`1m8Th05Y=-`ZDx1?%Q;E9 -)<7JG_`->sPuYISu8!|W2h8qc3gw3+N@&O58NUeH0UR$ZF>p0ua{?hG^@1k84AW=3j4ji -%dYj5abM~_yC_L)lNT`TNRx9rp<=+na%Rf7PQ|OiX>yO3Y;HUcBae;$ytIhmrfkUE9vR#a8Na+-Q5HxhB!>IJC1I@1Fs_iXYQ=^- -b_}alE#eXO3rx7M?Hlr3@vDsRObnsQpk9{kIYOkw`y#*!J{oNE0O;DQewrKk$Fp9@i^R*p-hBKi75tR -QS{A3(cEFh-Y!gJyqfF2Jzg_G4l(ljG5mz7yW3R^dEZUp6;#q&hv?)QLXdrD*7_Y2PTZvKW}}xqI0_G -f4^YrBNyzoJ@yf6rBD1@rf8!demh{;gM?Tn7CCX09>-t-_vM)BF4Z*J(gTZUj{e&$=+Gcsfy -tXzoW``V1YG1;>&A?CaYld96#JadI+>fV-Hf@?|Pk`^aw@|ui_q|u`Ik`W!-%y|2M^QuqUlVdHo%Pjb -DxR;-ywScx3H9%c*H8M+8mI10j8*qHQTNVx-KA#TbH}KA9Q&z@^5qJ#(97A-*f^2=&XzB+%dpzCpy?zAb@I95T|7p!QXMU=h*dBw -I+mTPFoCEeB1XO0J5QhSiXm#}wT%~14*?Yp0Cek()K`%;GFZ8@%WAw%I~EIVSXaj;{390wZ@Pu!!$)x -~)C4iisnY%M#vetzV5Sh{(>iU@th=4=USa$hfryE?qP%f#JWo5CH-?E~)Y?-#kw_lugb#@ATheB@nj; -<3WS66oeGJDTi(zl92fvn^HVxO=UxF=j+T!pvK-L -**{3FEQn_M%~tYYE((+s)ZJO~~fUbkLz<9$=fi(Zj;%rTYw@+~}hK-UJ|+VbR1^3mtf_8rhA-R7R~eM -x)5vwof}sYS5Vta@+=`Kn4uc7P>DEpP+{pOWlL7#%-JWR*3hxeBnb3Jry?Y7!ahrX{z7~%3fr -H506T35!X{}R~z(uV2eJ~U3* -wnfsPIJT3%Lr{(X4WFN5>PdNij?zaq8GFfnFd5$>-)SkKhjs9oS>I9MgXL%k(?eTv^;D#Xz5>3M@3ai -{t~WjOWxh*^=aJ_It+bT|orYZd<m -Te#C<|HP4e`+EDgIRcfC)b04JS{mJcF#xnPYthKb|-M1SJr_DZPFDeQNvmXmHPHIO&J=JDCvGO>USe| -9jui191mKsVQ`ao-Dp0T~7@xj4VN8rTKqExw;?M%7<@uVEN9@Aqqcz9le0T>!5tmQdF<)WgYS?KuwS#TgC2bFMb{^S -U9%dX6f3WtK{V6T+zm}$A}--o&xj(R~MMzJoh0DPN#fVI{N5lC(nb7(0AlG=s`SF$L#UQv#}yPr&I2E -3R#=tqO3hUN?uDEG}uB?4w-aNbW?reEc!NipX2_Vn+PHu*KhXyy@36?&3&vr*2%J=1aD=IYljE -pm|5;HHjyUEMWW*g553mQX?;8Al-m}e#?&3|tbAzNrHy~pt(o^ju8>b>S%W% -o@M$}bIo9?#^)&qW)5Kh=@tJY<&Bm{88PamyJUe|-gVsSo^;0l9E%y6FBD@Rx2eBAqhcE&at#n-OV&j -QGf{lnZ!v7?FF;`w!uIO3N77!?UBb{A0O83tPd57aT_5gtxhMr$fvAj?)@AaX;*)1A6YjO`xl8o&`xC -wPd=QyQfb2jZ?I7Pu~qW^!4FuYRZqDnULmXq;aK+?yqL%wh+d*&KG;W -)dEMMA`He4%L7w@>F=OWKgYMmmb~zyf7eoFDsdLfhTVLF{1Z_K}i3rp2JJT&fnsfBSZ#%j(4d0Ixi4b -(8{65o6%XBQufijqOCel@zY0_*eO%Q3SG`lRT^VTCfV`J-}-Y!&lGXIJVQKNi|48oQ5%8Jbp`kz1a5bMpWCL3+wQ*|*W(24YZ)cql(J)ji~@OcUD>$HzuJ$)@&d&QE`bs4`wOPZV? -J7dn}1mwP|d0qE|_AK*=va=$yVzF7=FA2DnabMiy$sv6+sT;B -Y*eSGUXunjMGH!~LadEoTnfR9L+wF4yQYn9_PmjoR81T*`I_-7^TRiUOg?PQGdgqzLBkPrWM7}3(Ke2 -9}!y21|mt{9Qd4}Y9?`1s_+b<*G%xBrgUq$6vwdZN8z7kdIHpeFSz(Rh?1#>QKC#vCf$w4tD%Dx`X>HjCbzgNx-{$+TA?6G01VfD#+c~*^FF&Rflgc1EyeQmORUZe -DYHV>^VI9r&)(%lSatDg!Zufx<6D#4j@m{AM}WPezP+2Lq5MLpI@rG`oYLQ@%d`1dcOX_$a8$Yo*KDP -`)I?7ib)|&SVMD*)P54>!#>G -4jp<3}#wPCbTW*Q#zN`AEeSXVE?(^Hfz?&x)_($*aTXw4@KF`{xkG;=ttp2YQsdHvn*RCRY4)9C%sYb -X;*yr&KygKB$54_YPvgUgMx^r?Dd~Z2aT<0XyOQJ@=&Gek|Na8EO6j_#+-y3F -4Z$)3E)f39|ModEY*acg;NbX-sA&NYXHx;nKbo9el*n>qv8_3HGz$=pwcd@)}u@IjY^TqeB#&izZcHz -wl^xgUURt4r<~xXx_rd%54THgP`VTN-r{87s!69=1lkP~%gOT~Y_kma#qgKAzuEW$q>3XAvcTsmJ@zB -SOpvujPP{3YYX=_KcaZ6@b8#+>gOr~h$Gi8jPr=zkl`{#S(d -zM{+df$+a;qU|9~xV}c2I7a__U?95V>&VZ!)wvd<(VHe@OaRNwondUG&C4@Fxp&0S@1i>lo;`!U)`Wi -cJb2Fk4`Nwov|YyOGz(?VFdy@r+BA7B;b?Zj_c$aP4rk(ePBg6KcpdogVe0lX?bS`XW6ha#eA_&!VQ& -VmU6WLr&6AdWhJM|l$qi@u{oG_T@8o5ScWBx-yO!7V6-`a4Ke1EI?{?=s+<@?zUtL%I~+k8KJ+56@Dxea?$`F^hXe(tjQ@ -_lK;*%ZDnHQ$#mBd<19mNoEPx~A1->V0L|vYGPz9Sy5&e1C`e{*GmgpJ>{AXTx4A@pGq%pF5YirG2zn -;_$GAXZiBpzGgG~E#CWe`x4HLJFclTd+Cc0sWdFZj`xfiv0wiN)2f(I$_mC`>RYOL9>SutibgU$=rzF -frc7YWg+E2b2-bf|TSgs1*->xG4Y{s*vsfnO%x1`_--u=G3tXo`Ir%DhXo|^i+~WhbcB9iFLR|ahH}? -fmXL|*znTP4C9O5|n>zF+6is!MWt)YLr3cOC9cjHP~hIFACd}ltU=N>1o=4_R6@*~4?@>R&m{K@~zpa -09BG5%z}%wuJ$zEisy@R4<7PquGDj3q4*RwNZYSWJ -@@r-^xRfcM+8UdIj#wv==w|G3dD~mV$BhTyj3@=kfHM -^r?Q97{4@nd?hh1{))xoV+_&r@MU42bm#tiMI;wjf6}?6ucEp)9;DyIeI_lRv?-q_Vv;`UCvwXj3;5ozgrQp -GPlwG!4Q+vGG&+E%LN8rTsQOGePv{mi_zZK>9vqi{L?|*)*@xirDdDhN;w#gn+@$mL-a<5Nct|r32j> ->(ieE!6A&1SlQJOeR2)_#qDk&s@P<#MDh*&nG+OlOvxc&=u$UGuhk#GVYk=W<2Zq_wZfDQ0tzi&N?>_ -VDi*qix2y%tE=(SqJA!f9K#mVaku`jGukNXe}7}Kr8(DmlkNLp-Fkh_Q{UG3Hzgu3>M!sS&m_!)5SdE -<8T3ERs!2cd_zC_SwcNMX5A?=COAKKc8$+vu9@Nel>#;Y#&OPWGyMw*WjqokuLQQs^W5A^>ocFN*(uj1xzxHz;#!L&}_=L_eYBSwaW;(w)7DZh~K -F&QTI6m~!e4d4ebnqumtY>U7a8oAFvsCAM)*TX|Tc)ToI(Or(RVtc**k_l81`ax;D|n^ZVTK_Z6ex-{^eFt30c-$r$dV?62 -&n?1$tx@_q45X}j(KzRs92hCK6*{)QJkXxFCAA^my3+RbyFoX)T5dJE@`+V%W_%iw*^?W}}t)rq|2^` -fB%_E86LU0y2WK1KDqD7$R2cX3}(x0Wx@bdkQPSPqzD@YPUSwuttP9c -gT1-#?iB%G5(D6vUbMLm~CUb?2bS+WzBNUzM6Ap=N9&qEiYDcHXYKB7G+8%rS02x_BCFd2lpSBeGiZ~C9bgYMI8Cy>H`5Y^kaSR85qlH(Z@+|7%dH~Op;`aGfnp@7l$urR{@jP| -9Ysrnyqx{d(JAIhm=?V0P?etFcPEquZ(VBGMoJM*ZjimQ!*vTEv5%g{(-R>Dd?|40=chIDFRh-_9WjC -OAWfHyXY>M7qo1%BRq<8uVdh-lCo}8(VbNWT$J$UJ^s#eR7j-8DDjjdz-N;o8DMdETt0> -nd!!3xK)KJg -uGvDxQ$@`z&QTrhUr?-x(t5mW&j$Xsf9&dB`Y+d)FW^2E-lKhtCoK6aW1Eqt6Zz7R2j!Ih3+(UM`akL -j?hEZ_ytbAu*Z!@moYg}AwhFj&IL!4-TvHBuu0cO)cfjvNI>KKMdBb@%*#^+JJwxp+Jn9>0y{=-`F&P -tJNngUf)TYaL(*obkz|}f^;*ztlLtY2Hxjy;~>{Z4Zcz)DgeM_!f&p65kSc`VN#IsjWms{kn5gXk#$5 -0pbeEnsk%m+QsITL$!d*LF@?yJA7Vrj1@6xWb2##&%9 -6%nVt6gT6|M4MZe@s@fs=j;jq}`X3Z1>%l07vCeqTR={OY;-$KJL%sYgBPpKH7cr%=C99lo7e6Y@Hk1 -ePi{D7h>~~zFy$*6!3NbW5!A1Y>HXW%?b77Xu|dRqBzb@q5Wq(qV|Z{Z{A0I0e2IDvqg_K4rTyv&i7> -OT7q_e$2}5fRZFSs~vCA__MA&w=oBa}2YWNC#MfOwn(cR#&s(z(ozffpw@{s4uy@vXbmSrtLJ?2`GW -h`?h`t;lI!}*QRzTYc7F7tEjgZn_~GL}o`kL4xKWPbE{?nyA5|3X53j$@3IU*^5Q^Lp}qf0!)Vc3Vvt -WLq8bY;mV`aGvb*%_*VNlhql_j2C0y=eiHsu7#qEbUVoXm^lBrC!SX7xK$#w8T8@0V;P^Njg}*I!2iV -pBk~u(fB)Xf;u4!ZEgScYVUuM-XO1Xyo!Uz}R%V~u2!mG|oZ0tpI+7}C`=LKwnjcBF10Q?-}jm8YMpk4w3m!B?&;V8+`Iu>q2E%_HGp4KPaoI}+Q>cM>y-{(4jX}cMs -Oc>$nk+QVx(=bILS6}XpWZE+)oSX2cTohk!K5hC>g&*9s|saz*k=A%_`_kzg8giWC`?Sp&HAP@ID6~< -^Jw|_S*!Ux?KvMnb+cde42@8?j6r(ml;>%?pAwky3F-M*WQ+Ar{*v%=aE0AD;ec9=MR}D&Fr7K3FZI% -?U=0k$vf!rONG`LYbd`u(j}2@F`@{KFWKRUvMx*`Ztrn7lAJsgY;b)^W?URXE~+<+pnw>@@K1JlYeNuwxPg;brO86dg>~`OxnB;_8 -8BFzauM9{Wbd9fT?ejfAA<^np@=D1=u1!@Y8aexFFB1J43s7(j5O8QRV~99>taOXFMmDbDecsAcyvtp -#@|Nmj_pGRJECL&h&NMD2rz#+RgW|Ipp0ON1q~cHsp&4d{N2cs=mTGuYn)wSIco!`EKeQo;Sh!O-Ro) -gh`H>y`skXyp5k9ESx0NUQwCo%V=lspo)_o*Iq~%w>k}&GM#G858-=nt{g)u{S^F}Z{f#t8kuIMGrni -YSm$>WCKvEe9;sWsevtFxI?8HVT{5^%#`k4iSQw4gjv3dG6d%nv(mo54V@^<{^!Gu1C8$Mm_88xQ --iZZ;`}vy5+fXTu*`YFF2qN(ElK3+T@ujx?E?~4ILYHgXaM=_kiOXtsDnv#yqb$V(Ta3=W)yN^v6V}e -WD24KMY>VLjE3Y5V9%aPKakr9*>ay_!YS)$>o|luT6Gmy)~>7dX=BIdj~u^a+_pn-4nX#?u*A!!D#Fy|a$k -d4#zwAX;5m2PhqnBSn)Lt6kD6zI{6=dinjmWD)~Pra_Khg_KY;zje*gcl_wI2~Uf16EexBhn7eHZ#i- -U0hYXW0T8U=(TEe{$~l(ablxwSbFCMN`tJQa%!PrZ=e)oFetbTBW@bP8*_XBVUVH7e*ZQtm>GdVB0X>T}Dl=BkoN?Ud1u;HMA+0_0imhA8V&U#2~_+XxpjAB#M`6prf$mi`aU$`Ly2 -{S=rk%%ULE*o{2N)GI`QWe%~Zpmu`*1y}otkO!|EtWIFnMD2nA=i~Hs%{p>0|n}zmnieR&#OZh27IjJ -*mRQu8@u^Plguq~8ofL~x+N1tflvyy0+TjDnBHZ^97tzW9?$K8~iytJI#6eP_=IRnwHRvwosDq*R_?P -3#V!Vcmys&7H9h3g-Gg6~q|OvLa&o9d|UNcA0lmXqT}S;?(d+%GO;<+%nXbdSqq3x5V07tnic6I+{Or -P#)SbH1E5;^RJ&X*!5EK$n5>cHE$>w5j9$TiDlAdXb)G&%ET9kxzPkP5r5g8+zGL{VL=xsXxQ{AeV0~ -uD=mqOS|gJS!ZQV+%<7M)eL_f@c%-L;A{iOYaP`FUY<17xlJu^!)LxG@Z1VHZsvTcz9&J|D@%n_ox+a -P;(BHz&xDxIuXRVUoH-SM{n*)BIf^!I;CCB-B&BE;+uAfZ2me%t@VCwHIbPw57EXJLZ+r -REj_7I#3H}E&=Xuqpl&Dtxh%q{riYoqa)1NO0F)Yc5*H~j7+a-O$B)u&@$kW?RsG$BI=$$Klj;!fsw^ -l4_n_b=nwEpLivn_)}BAXh!5Y=YjN^BJlW`g`d8pu^8f*YtZV_h!|`8S{LT*DwDezBKHf{~RCQ?pYg; -K3XThOT$^T)+^-5hJbRQGiN$}7Ul9Bd~DXx`a}D)wuRm=Xzg1-vYQ8XoH|_=x8F1~FU}yx=-kpz=Y~J -R#)r!Po?7pcfO@BRHTm_mkoy!r7RU4iagT!i8|xc9^%^SAbxiR>uHrhh^trgxoZV^^GI)_R2Qf@@Av2 -^Vh;ykz-&5#2?YTpJw8kJ`TN1bqp5y@kxH6I@L&y3o*Bw~wZxEdwBnP(3(M}W5!$|bVtgYrc!};kkEd -?Z(?o$1NqSd|y+5nC>4>_i0z`m+SELQFJq$rM83!mhLL{F>%Q)YdFx(i}~f>)yXH^tr4 -q;!M6~Q`aoO!*O)DB>jW=66E$ef^cL8NslVz(INdvhy{{GI#(&R>8NgBWw_yO9#`*0a^Q4%`8bZ-e~^ -^%Y6#nwC`j$T&sFH}sq4wD&!)@TV9h?{T^152~Mt+83KxTUF8GOk!4LnM{&p9wYf>7oBY`RL3)Is0c4Ge8X)w$7WFa* -#aVxcK0R9H2euJ^AS<2IahHJj%^q4$f1}?XlbG+CFHpF1imjgQtblyP*cseYnWg$>UI(2ysKXm@#uz! -5u%L6v4xCG3&icmOSmwY)tyBK_j -aR|7P)PLK@vg-FSTFwDR-&SrHuF?O$M(k0KPi3{VR?lDzeq~fHVN9UIP;o}N-{Nd8KXaToj`QnzQu#g -bBH=SubI~sYUr*m*%RZftS%eGmSYGMl{;l3D8Z-0R$8slcvr(|NV#Q96r!_ag -RA#SNV#eDU=WY&Y)1Y*%zO2W0MVe&$zJZ!rRYlHVJD;mgtb0~+>e{3B3T=#gsnUGk0Dj}PDRH -BCZ4R6DW57ko|dCwS8r=(iu_G=mXrN+BmPwhyjPlf(}q597||H<+ -e1<)yt=u}Vr(SIR)ZuDRHeQv04IlMoG!rw-r{+#}T_%GGDJfzMmPrfSlYxpwJ{Px%w*Zq2M_MtTT^ul -Mfo%@7GWO2G}O?D2vM$bq_0jAWzbs{FHDU-R6!x!pC2K$+J426kK@QP!{X@B1&e5FWM@&YGNYhN+VYw -(=Rc_O_Me$KU0E#5QyNXI|v4DS6Q?TyE`0AFOVUP1 -S?&8YnitylEXDhb_Wc`*_usYeg^IU9``%yZT}|J)Pj7(#8SJdM9CcikqbTiWpWpW^)L#o{-FjLBxF5r -s$cpDOC|?gq|FSjFG?8uIL~CLh$L=t@?wS}5pCnj*2<0a@pO64_ddtQ+Zphe>zjh@ETQ}fh0ACS!-4^ -0qxaUSb*q^*kdCGmhY^hf0;$FNY@Tu_pFnok4?T-oKE^ijm3U<8MXZKJV_BFMfo{j`zOLvjSq_nucDe -T31uBCT)TN@&Ra0U+ZzN$L803N*Ob77wuNMCCI>3X$apyMj_Jje0fs9y@nsOVF@+9xMH-$G{=txu0%5 --~pUyjSg$L+#Vm%f6-rzGsKz_3e{)j4luueXXhfbX2kB#z3oRN`pZ4jT5T-v$=b~t#-$htc--)(4{>!!3Sw5@)kzyuf-Z!B -DS*WC7{0U{kLj!io0H?Z6c?Ra+yXPz%rTXJC -osXX(VkyYSzBX7eAHGu+ni=*9bHqHnf9F$du3{AaiJ7%Vl3Yl!}2$xUN-zP;>zL<9&NxKU6;&Ctk*E} -d|PU1?lmlaxURmZmeLnx-SK&96rCBUA5T(#oS_fUGuxZW=Xvq1&CvB`Mv1Z>2bPeLIhVtZl-kd+%7}; -SSBd=_vC%Viq71{E%EtLotV2fGQeO|ws*jS)$e&5l`swt%V%q*@_$W-@$KtE#bJ54b*PuvBG665EXd+ -vPJAC;Y%#1#ye{owc?(nx=@#Q|K8=iff=m}a)l+`%abUvt#Vuwzq)|Wg&ZE?Vda1wk7WBL4e^1Lk%^S -(|>=5wf(>gyV0@sK4Ph_OTTsg#7RR>hg4c_f3xSDdlsq3v&S+0dfKtTnqz-|{sro0Hw>q4Uy9kvx80O -;L4fX?4Q$%?26|lmps7Vqi|p0fUs%t=H+hf#qc1>S)F%;{CSmbXY4QS^lD}6V9~IJ43n{mkC&{tLl4- -Vh=RKC(_t%Xwg&K3`D1Ol&4OL8A5*+Ii)kmyDmoL4a|r7VFQJ{w;2{(GI6Y-vGWxL{H~++fKQAivjYo -#JWT6w_`~OPk#>iX#ShUMGg5u!l5%B{6ftz3%Yzb~*L1GAH~o7v&aU-QEFSCaBCWUD$#j-CEAEfKhxi -rDew?1Kq35>UtY#>=wHaeG_+D#sn~Yd!rtTui)Cj+QCYc+z%IlgZrTW(q?(sjJu86fq<$R<|wjb9e*@ -aC7w@v3bP~q^6!`oMD=V;kZ<=0f~Z?zhx(b}T^*j~#T!rlNsU*aG3^SVgRLx9yEan8>#_MR2|g8O)cv -03idMf0=wyubUJa%q0k`f;Q01JrlBWNEBZWB3%QaVslA;#SI$uxB^A6#9It+t-BmhiE_f1Lo{*p;tZyX#0Q3s~=n6{)UmJC$E`4a@72l^(;sS&g=6wsRKIdcABWnx{14zCPooXs4ueG=q-Moc1- -PMYEi8;unv@pII+>NLy4uyuJw76)WyolJy^D!?`DQvBghB5&v~cHPx^&jbbTnt1?>*SIT=UjGg(@S4E -m1SygPJ`Cg)zVocBOHl$VMe2(y5<2tsQmm;Wf#nyNL>y5)>& -&<96ZiSJCz2v)o^_8MD*)?b6Y-L8J}f&9`vFEk4&S!&{n?G8e6!0;Wv^MSY|{Akr*DiumOKtXh;P$qdmZ -(D8uoR>zbwc02I -WMH=*N*VQNp=xG(Vy?nSB3s?EPEbJM$1A>;&|p9q<+=WxvG!ovW;=QYJR=smoW#_c=yEj^6MhlPTOQU -T~fX!5$uqI%78pGqt;gX9qQY^5kDD_BAhQ6;u^27tsaJ-|DcM;>Bk_A=|+8?XNd38dY(>sJ@gLrl`_5 -a&?&zy^wBZa>R)=rdEvq$Dd;Te;C|wg>z-R7XC0{rbUd$%0 -%q~+&(RB?4=Ht+wWcK7J9^Z~BV;+;D}*M|6h9@oQ7Dgxcxsl{GNOY -m}7V7f8k-4F^(NKW)OF)ZL}VvFSSFpF3;M;H>AFinD-hE -Kh`1QKD`uBSA|;FRM=swbpfXib!m4Rr#=*W5PP2A5$q0(T@*#_fzPh+572S^YJMNU)|6CF5oda5Kz*o ---(l(5!tvtGjE+kkYzGGVS@tEv7Ow<)iloT;o{H52B^lmTJ-HSG;?Iqs40e*DJR2 -H>qVf)piUF9XoEzn_fQC{p>2dI6sh<9r`gm7~9nC&VYqg?_1&f+|;XV`#wj|Sw7N!lGgrW9 -xs;0@gQs=%+=-y7H{6N$r{agtWTT|dOoJTkj@7#jJ+C<&5pBXIThMG1HM-1n`^bW -R(kf%_p*-WWo309?q7ht#Q7AZ8-Ag9=^WT -GjcE8$F>{XGfuup!F}VKV6Z;`5h^h;%*st%;D~%2Z2q%(^*h@XnZ41OIo9UyoPPo;{Cu*a~7?`2Qw5m?!d37GnJmkv^;^i5p -SSP;<88$FfDLbKRw=I9m{?R_v!zQyCSt-TcmAiLxiZ)N_9XV2A?GSPC6rEFv6w|zZ1>~8+v@7K;OBI3 -V5ZY+NoHSjajOlO3}2K7VtWH{!XT3aO(S&{P`2nK7-L_kolR!-^SEnnq)wx*Vo|gHPAK%v>o}Kt}m_N -dYUyQ6&b_}6BKt9kGsbfBHj|6E1ZhN?_j+@R`0Xw)lVCfh8`5ULvHJb-+fJj$A{xrTFf<^W3u5VyiLI -!a(1~<@ji3G*HpKS|wm$!G2EiyB;xXi+W#<`*}J?dYRN}#W-Rw#HWY$oeI -iZ8OOXhYr+nO&N{W9P(MOr@!<{^egnXZO(edOL+vaI@yGar=W~A&$@w9$Ie`C^;Kvjk70-e8#fuYyv8m6BI8UR-; -%gyeQNqVUsU2%9Qq=s=b?+RbpW()0i+?QgF&5yf;HjhHBdUIQKNx3BnrKY+oDySl-8f@%^1Ns(c=Xf% -#F!`nW1_V)w0*aS8JqXRjLk*jt-*O7bp1pbuDI -gcefFL??d`I;JM+riO%~!6nb*Ky$Lum(ncG_@B@UfjyRLZ>l^H|-FVS99^&@3?EFF|yn$-AXDq|9lR~ -|dGmE;q|V(I7cXf3XLNzMX2URGmZq*41&KR!Q5lg?x1IgJqF&t;5=|B5u;;lf1q$@(TsPrd$;iMp9ID#`kdtQs%JFZr3d#viO1#e=(1f -tl6C1!U8bGE!=2+jvZXPF>vK|Eh~e=p?VXUDNS5vpXRAph`=szYi8$EcM6n!6XXzF(gp*ub=zTqudN5G_f&pPSi#S-gpQiO&b{u^lS-%z+@IYR~olHR4ZP5Er*;GevmTE_5;QnfsZeLw=J8bNIb^3P -1t|$KBMOnTja=tg((fqsj==>8c?1S{NB8DB0ZL~y{7j?n%Vg}`dJX&J}-oBC#zs9in)O*Kp&v1t6e@) -^##FJOJZKO9+=tpX_G3YZpn)zO^SlIg=TPr&?8$86EwnlG^b<}m%&QU7P@oD=9=ettUa6Ie~3`De{s5 -A9ZU(h=~XXW96@m}7u-0)IUleHQD|;yQm-^mi8R;hyb<`4!x@GSQWVI|gp+An4am^urqGu@^4-at>xF -?FQn_(}^}d*~I4>?vZtcBp1deH!hZvyRqg>slvC6-}MEQAFq{P7_Ht_i?TgS=7>Brrh0Wf(i(9-{NKJ -N?^N)gz*xS57fO0=F?Zr9n=RoSGkUXuH43+YFy4X2Ci+TPky2akf;~?jv63n=0l1`pEx`Pdse+U-AiV` -y)A@^zk#Zm-^^D{5o{Dm_MO$&HOfI7ieF6S+y~H9sYm;ws?h$xsK(X#I0%0EtH-{^2=7*?=px^k$>?u -^$J!k>%W7@v9ifIjPt%z&EW0KN*p!B*`4Q;@ -I-$!ZkN#V2drxPQwSU86GSeVmVo-~Qun(YX*g`06!vq^Z74u-<&%EnYZ1Q6IU8&k3RtB6WJ7v -Ya{j$*Y2c9o%8{&i?+m<-)UuQcU$Mw@(Us0Ihw7nl!13s<#GVNeehBHe0zAv3)qw?y2el~2B%-yX?bjGe+mPfQ+Sdql?FpldUif~rY -Tro+8ZX@NbsH{Z1%7wtPhL2{by%}HMCLwD~cpd9|@*(JD%A*u_Cao{{2Xz{Om4G#`7kqxM@E>PJmaXU -ZjU>BM`Ff7C5>d0A=ADW7pXYl@{?K-oKkz-~gip%}%4d&M^1ThbghKDf+asJl9dk#}Z`eA@X(Hd-siFDw!TA)C?=O_^--vF&p8 -1UGGg4YC<@uD#{e<2*FilxGKypvzoa-BlD<96wTnxYK$ulz-vwY0U0j5`c4&L}T&n73e0J1?L*j33XNcNMK?@9<5l#7FNy4Z2SSqUh@|Nr(p8TqQxRMOGTv2^JNC)6M0f+R!X{f -pu}~GR_Onf2$aJy4@{1)Y)bJ*)5m>Q8gN|)*Aefov(o3ytd -{m4cOCKYx+LMZ+DdH!wlObDR-l+ -mw6{@!jc?l27tLbrHu@GZj}+UrhAhwxBAn8aD9Kl$mV{>^!bZ%3jUGQ7xanNUNv;>-==o$jf|+71Nm^lqrC0wz?&t#-Lqgu9`e70F{Atk=7vK?r -d(SK3z=3eUoB+3h-^ye4W1Py&$#0jy-U#&`FdWQT6j}Pyd|n{jeo-{OISzfAz(Pp-pFpvVk(baqVwUjvJ!5&t{n<#pqZh1xXm4D_sGE{C> -jklIy5?Q+PXT@wP@(-+X5`2p=g>}jn%kUO>ZfH$VEOyPH}=o|DK9`iG>sjT_L{B$$o)P9Tjp%1vXh{M -V8b@U_c4tJl)^AzewWJ&CacSW%d;@!1Fw6-B54d_fR#B!|&is5P+i3wXBX>6>Jl-e%bdr67i1_N`>rf -0xE4n0?DenyxZ|4F5QpegS&`?S1$>2|Q*0`@afEf5!$$%&a0?*s(Sv0ZQ(ypD+{Mp4n4!2%5!IgufQ_3Y{;bBxE@g0GW^Fr -VK0CB6xTrn^hw3-4YLo1x8MBJh+g#~t$E6{`xgU&_xbe+yP%7vG1(ejUviem*iLelbqkqkFZ{+5?!Hu -RVP$_Ho?6zTc=tppwJ-$8Y*oCp$M6_TXxB?B=5bjI^KOn{yZJu+oZ@|j$1q_X_Z;&zr6sTql%c&_s1x -u0GT>e474}Rdo~!CsaW|6&Y>DTDZuJGC3Chi!D9XwX$d^R*5j{X)p8wLkf83{5Q_7KHqMCtTv15K$JfY+fuZh%7YBc@$_h1o_iu^7_@mm -nq{BYC3bHGekn^FIvx$Z))yNQ+kQ>7i2sK2b_bOA0F~E4Yu!P0Yvm=aV(p;!J+!}f$(^;(xd+eXq;3n -*tDfpX%;vo(#hh3n?l4M#(+~Z4gHgx^2i_+7MX;RAXxgWu^o@MpHt3x;9jhtmx2^2gt!oZkm%zHX_pehQ^I}^-)}r6&IuM(M|aXQS|-2tlqQq!4{Ku_PVenIp!(5-#O3W3_BNwpuUE(-cKx7;{n -Swu5Vu~7E&kX^!Pk~Yuny?|2WXBxhdU;zvvyEt5Vn0PeT(RfoCtmpCFuJwz1vwei{lf0{u#b68&0G5A -Ea`&Ql1*3Zy(LiN}7x7uK0KxJ)iM@yWSl`xBoizan;AZrb|~f+-=RiGr^Q-9vF|CW{aWxjZM0pAWS~0r^*uQ!gLxayntEkT&vIGch&aZ;>shg%Iq#8hMyucW@_u%xPw2S -0-Os;9vdw$e*oGln?0q_7xUJ;=yuI?|m9Jz>&UOvFtaJMQ1;d4RY9f$cueF$uZMy(6h1(%E%Y%w_n$@6M2%dnwOi^?q#4XW}d{8V -4$nZhz9^PTThym6|0~cCN>dS#su6;+K-3duW@n7JzxybpiS{TAQ?<_mC|63jGi4pXc67=iup7UXm`_+ -4aP-VQO;+Y@RpLKHB-jtYO%JZI(nCf4?Bs4(_o%^sYgY;s^Fe!sf~eAJ&tU4t=mVFO9}$NlQ1i6cbG< -18l0o%?}N;{%pkXY+ja%-}<16%a3zeE-=)o{A4P>i}+^>l|PV?(qd0kyo(~)PKoAD{y(^lN)2$EU>k~ -frZ}G>o+)J2aM*W6+&@k`+lW0B~j%VUH#_t+(<2YZoxHwH!%y%9G`WW;;h@GH5E -A&6xP5fjtt&tNv7D`e#ut@c>2bvQnnN4UX;t3n6ohG6ma6OA^9~rJpk8R=lO~e9Lf5#HPm_cKO7#gq% -+(mSHh3hd!%e$CwrIEpNZGXUA3&o30=6KP`9Gi2Uj^zT&miXUJV730o-~V@W4*VE&Kt06QHe%i#pt|h -pyvqxichf`XT^PA_1m8o&TAHH1b8SF5kR8%wfB$i=*d__ArLpELVmVY&|LJ^@Tb{`GWJ|PK_Jn}44}P -M>tPuJRJ`d*?xed(0<#qUDWJ(Te(R{r-9RABDJ&(hE+EO^pSstf&9dW{c8GFqv+CRz@C@sOCb_3FqC@ -smKmW4DcrCI%HUqjjyN}J+Ovmq^m(lY#MR-|1^Y1jJG5|NfmX}SKiiAcMd(r)&rnUMBPO8cfiEedIKC -~b~EO+wo3lyPbx -27lW7NSjG%GyQ43VcRoWPq;2lYRo*XY{dQpTZZ#%9nCm{0ef{nm9dQbTM3(y@c5Jg{flHB{Z!5;z;A; -d>ep!4;fCBImhVy3(71h^g2=2-G$zt;`6(O-e>XoK%uws#AsZC2UQzwocZTRutRjl?=Ui@)(D&@`h -*?`=Om_Q&UE@Gx$%&JkKLp!hR7bR5o-Xu<|GgLYBka&6`tH^AH#)0J_2IQ&)vf}@%WmwZadj>uIyjzxp3ChYsB$6nxNWbtaLkisb3_a -v%%QVXF3!N>zS!*AMdb)T0kcbMXF7jov9G)=aEXcn@RE+fpFo$Y`WH{KtLJySsvjsh%VR4p+S5*TYc| -W}bJ(F|8qXs2j(34B^xn*3Uj&m%#LsHpa=Avh?;A1dC1RDalKE5~`Ze4 -Jm3b?hR4dYpaHrKwy+wpXf`Nb# -Ht%(|8-TaR=kxe@6r?R!W@EqTg6h`8IXJ}DQ@WL%b(!B2_1T-xG8VW?}uo7>D{G>_3&8mvlrB7uizP# -^IP@zuj#X5jlfz%xej1*J*{||S9j=_c(xvRS@NM4eE*5~{&T!b?>vXkjf!_BW1G>|WTM?;+ajFHi4VP -tI{+E<6F$}Ta&UW|8G=2d;{6eQI@UZe+yx%EX3=oHnuq3S&T4vwI6j-#a4hMCKf5BZY*!8mUJ|;;Z;` -kzi`%wFoknY)8UaxMj-%%ER3YY+))MD0HiHB?oap(4MlPVBYp6?O6C_?d -XdpKImT)3&&X#3&&X#3&&X#3&&X#3;%~}V&PX<6AQ;%6Tc^!G*s?s|4irxaJS}Au^n)?2K~||k_Sp9w -j2InrDw&xgalldIkM9vFgaa#w)AXxOpt%?AB|^%d!}AmW8l{o27Q}(Nb@a-=RB8j--2Em`{h0#$4SAt -f#fbX?!X-?jtum4(3Nw4A)mnf(DWl({o~CS%Q4VTu)&Ph)xy|5!hyaixSfPR~)F8CH!C9;G -{$#5`j#=&M_ahAstkB()#kIE@SOx$;!HtfrQo_2?EvB5boU24Y}>&XV$dH8l^&smbMQJ#j`(iz3dfH9 -m?yL?z?4JAdYEUul~LDyY6ZjZD9_DK3G0t?-Czr^DnK!+vu=c)L$iLNNzwa9$mc}GgmA4zAFuF!tO%b -|9m9Inp@sY~1IpkwmHSsD!_LmRHNcA?F%sT`y!pE^vcsaAEy#vz=+d-4X!@e@2- -8-NiFIVR4m1KLF=q=(Z^r^8Fa9#wSl?hl)g3iVI>kl-~pD?M>C>amh)pkIJ(aOYm4WE^i-$?H*pm&g8 -kGOa=MkdL0aJu@Qm=j}TCWOvU&`>^eEB}iUFaKELl)k{v#cDlQ@S%SqD}9Enx$jqrO2aX?X4gM -rIQf$wVXVSZ>nU;otjrQa;*S$MM;Kk8RG)%%68*>su1jGq>3Gg90+Z_xMq?_0(@2l4tE)cUsG%+L3z4 -}7st+^4%NLG@YG`pouvsxL~buh0;LJrvBxLh9S6*4OW^5B3Ou{%7iw#;b4i{ga%2%ZRQ_(DoK5>g}X@ -e{)sn;B$iNO&nPlsACJ&9j(=!ZWz&d1=jy%{+X~t74*I(c>Zn`ZBgXWPR1kqu1BnSO4FyFSEA3w*J3ZUt#7$gXrV=kukVc%m?aUhB^<^A7AQiwYy0cSO|p(7AO8cOEC!{{37sjxU?1fw6xv4@FFlcbgHzZ$EHOjs@ebF|U`+jWPcv?p;FXZos(FTsQc2qrfbS -jD!vRnZD>&tEM+Yx{cB`T82AEX?=tr4>0I(XMPWr{}}Ka5QpRejCm3t^G+IT(4NZ8#@QPBmwhC=;S)Z -cpf79@mSwO*6?8^{T)TqGd3U57v3{{HV%<_1hbl5~#vC?GLDHqADwwjv@v3qvSW%;cctmb;?(Sf;Zq&@+=EP?S@9<5dp(+hDwT5n)H-bm{=Mchuu-8?>fO`EV -Iu)j=gyGY}oPI+G$8rds%GZp)PtT=n^+}Don&>QA@?eY3X$R?SQ@By$?gT7f@|7GO&d*Fu$NPaKw_uo -TD(RW~z%qDvC`y1+03grVv3FM)FobokYfZi6dpCpbwn4PI3S)As;L0NzBZW{ZeQS3}Pt?wgx)`5O{pk -K>qDA7+lY;{O1cTbe!-9vm~Zxr)_UO1mvsE?@=75A&j%pDK?Lp-%-B6IWS>RqKFb}qNiG{zem`F&LUT -yGJ5cTHek@ORi9EsA8`1=PpO)CaW7BXgWk-u8E??P!~3SNoro2j%15!n9h)h9n`7r -{h`k>R(OVfR$TFo^izK&n))gFxAhBO+201yKjuD$`yRDDJ2~b}eM$OX1Mly}yoH``uUW(`YM^|}==aZ -Pjy^~KxGU<~R><#x7;w3cz?N;C -{-yR`S%r*xL5@3s$S4R@Foj$`o;oS&(EbpbXDqw)Ct_dC!V@96X2y}$;9$-}ex+S6iasP{TNZJZbyie -yc|`u~rB^6u#q_JhH3wnO8MN4RzoeW0Uww$DHBi~58wZ)iLk+-0tinS0AsU&r_1pR_!AX(91+j1BHKK -Ovb+;El-k<0RL>_7-|u#4GrAzC0ccWig9|L2U*r|U!rrso50%Ep-k3K)_YoZC8 -y+XC^evjW1hPRJGjM;(JoPIne3-G8>7x1V8>3Bh4ve*SZJM#wg~~!(ZTwR3@5~1A9eZV_RLH->4!v2$_M0V{?WeFt%VAruH`* -d&I2O{+ReUnEx6U|mX<8gBgCDIY_Up6XjZ)l3)>Z=hSn#Gv6Ie=Hl$-#c2j~HfY)1*cBTlE0?cv|kW9 -WS&(cZ+eVcQGbaKzy>AeKNPt4TMpGVt#9XqNpT6S1cR9$2!y_ZYt~tSLWO_{3S=CM(tr?7~3{>|@aX+ -G1D@d}wXbq-xl2qJO}*nKPm@Nw%-(nnka=)vVTTZ+Pv;@xzthYpaPCThp2i&R7d@be{_8-s@bn55#Agv{cYgEbvK8=^ -YgAZ+@ISl@Ep6Il7e|F=8vZWYh!`_Bt(0YAk7G^P#{8Js{6SCGnW6#vxN%T;n*{N6evT*p6hYFj%U=tQgT~qewA}y%00Y)dYW4@|J+fs&X|$_d- -DQmBHzCXY7_aqE|4NPjS3`v4dw!!A$SbI`P2u<*q -7W}q+`G4{@?4f`1HN|-^@Zl@-Zdg#+1~vkUfEt*;92dJW$x$oUA4RVRp`4EHM5fHt&CILMbgBXpu1g~GP5cUZ7I(M9*RLqnF% -bE&8&jIXON=#JDXXi`a6=xPT$PZ)ZY>Oc{qH~aC7d4PJsi7dFPkM{c)Wu19&R<=}62e;;G=FhQ-%CQK0PjnRJOU*l%`eMiL_SFZTi-uU|MJ(_$ETr?hM#7pPUK8~jpY46uX*gdZ?=QSjg4$|lO0ve}j -O8XGW)C&v>p!N@ -bOL6bPKIZqy@irbM|~dDsKi(f+Q@70eUN7j -3Qt?(fs7{K~D86Z?Fos;|GD`rkqQzF@!I?}e*Z+w<{$sBDqN{WbmN^ -nUKkDWY=PNS@jPd_ujzDD^Dq%ukf8GqIOozw@VEGZXydJbkYxIR){4voDBt{FvI|P}{LbW;4;2;Uz3T -aLwR3@RjnYE$iu>Vos;(;raY|OiRJXEPs}y&qdjjef$grEck5v-v5!X`+6964NIUSQ-F1x&*$Y@iO)- -crD?YJ({F8l{{9O0y>+9k72H2wZ--r=ewDI`68>-F?(-Fc#GVd(-paIA -?Jm}p*BNX7+@^DN({GgLwLSth0Af;K28bY5uoO7K5~ZCe+~H3lxj(Ed;I#imGYK1xCJA#jfGdx9lkZf -o-tb5xt3;0KtSp>xOecQIb$wu$Bo+5{TkOXEv>dh%HBU|iD@H6Qq{F?EC<;JDMCeT;a{apEr@sTeoFN -i$!8d^>hrm7kucJu@U8pf>Nr#RD|TEcc-c@PUK8ZS=?V7Je8dF?=r^y{4Z0yYTs5`}fiD13S(Oyo4`{ -9|+lPS-|`7H$>b(hZ@5^Fm7Pr{_;PG8@P+s2=@1T0_scndvLtK@$7Q?Uj{~sd$#D6D<~atSvYS=_tTp1O;hRoa$It*dR^d%_@V`d4`k0o*tO -_VT}IgW=u(CTuVZf6MzXuw4&bussU4o*Mzv4>V{6HiBx}G2d68tQ{t04dWB(&retK=ia4hVXsGSx2W; -70DOlYxtI}sBy8-8qiN#-vmnbea?`??%2s`aYzKwO{XLPDkY+_x~$OWO*<6d^xc92Db7HTJykRI)V2RJM-pCu_o^SC?!=p4rJw3!@ry8 -g1X^PY>-TigV#=gkDPOnm*sz(%0ivK?YefzY)9$b0=Y0R-eHyNl-}dVfuD>u&Yb6D5%aM;k(D97H}FL -(WhEc|Njfl)*M*qX`!a{BCyMir@9l_oJCy~w_1^e(qCD(rb47W5YI$__D_gcdw-IL=+;b!5_0YA(mPd -%+Iix8GC~GZz2Pv(J#$*n#qSnez1JS&~R$tAk+Gid*x)`Za^!yX+J;S0e5dZ27+Nxv&pQi -h)fVctVtO_(pNZ%x%dK%Ley8+x<8!eFm28nMHD>8Q6UKVh`=lWm2N)+yq+ZG@jcHtgLQ2?Tx^$rM2go -#cH*(1IvYc$90ivxoLFnL|G1e!nT;k31ueIJULJ60(60$%Ql*$p5NZyK6sttR*3(?C%si-XP)I}T``9 -aV18Ae&3g;+5%y&@4$)kyqIq=%0u2nBUVSVK}XNrIl{lPAjDQJTJhq}hj@|m&% -4LBRSV~z{5yK*#?1Pj1G|O&9(XITLLF*cRv*gBWuoD*|Z?B6BArx)~r{e|Zl -C2N3aq?ZgPPc+Vl>C;zj?d?~TiMhxen!KBdtOG<#5`*;D@un&lF`fW@f -M>d+`!DYb}65xc$^dVT -r6X{P*l(z`PPXs6LF9L*k6>E+RJn^PWV6|vLviGCT;Tt<5*h5CDm`uD1YyI->%XRSe9s?(#hx{!ZP2t -A?GP9=H<((*O<2ur4J9no-vC%PI+KAM{9L_9%`(M5Zy_p~Zo%R;a7E@(-4q{8e*z3*t(HrZ0F(Y;T-W -9IZGT3d*pw91LqYpJeEIk|X%XpZ(BgZ(u>zlE%QmDZs9ohrS%JHFf=HT*q{|9rlSc%^!OY6mV2^c1GQ -3VVO^U;XmX^)v@|MR56RhvrL>q54uJ(^##8ECHE^#w-jntAJavW_XaakTFO4|%y(SX#dB=CNLEusG{5Ug^z5R3*d -S--`gNd5_Uyl+uSxbVyuPL_dX|l{xIa{XXTDuOoV%H10>p#`UUEBd!?fpAhdo;42fh^2_?&8pa;7DUJ -)2|bM6v9$NFH~grvBiZCtf63pmt79Bm6^!k0;5QshFFShAzUdO~=a4U#qOd88XYnHt)?yt4^oAdl)gU -O|%D-{Jz)DYM`UoLgSG}d4Lsd%MrR!%LI}Qu~+|9&hk;2nC~aT&3BrYf@TVjLp+nu2VlKJc1f~Nrt>K -LThKqybUxAJfIOUgj@B1qb@SLqMrubik9Rrp-iE8fuQV|2JxYrVNz3E*u*{2lA?R?FM2?>Z-1#2xoIu -`syjR1aSMBT=*EIz9XBKlK_9Fa#2WgIF8KARc-uYA(*0d*DaaZVA4y|!_J@D`>tOh!vnE~^9+J~?U72 -p34eDdh`gTQefnG=ZN{&lW{v{ecnZU*IXsCuD9b?)B9@1LY%?f#qQx^vI2@p(x&T&w$DgAP_;E_Fe-F -@g`VY-O4hKN638JUoct_-Z43iQ`giHIghnk(~i15qRc+s@HF)&#VTA6|qk%Wme12lH=OtX?Hx5>nx9X -!FW7i*q-Lweiq3(YH4hn>Hjj74;og`I6_|LI^Zbr8}c=eon<5%(>W+Z*W8TQW_;dIo0l6^`JdK -FxsGEEk(`@a-42~|6gxAY){3!ES*T|%CA*{8A$`^Y`=FWY@M=9#^(Dr;l4h;HfvuD|)&Q$@&_1=U(aW@@YTLK};3gLl=}!T8jNyqTj>#{V$12A -^+HcTa_a4J_>G~xD@LI^vnbn#}!-60MU2>@=O*OcLkZIrMQ3dT-lpolcZ|IMy|s1DfJ~MbVgUEg(Vmk -Pnx-|GA%Ftsi@K)N3uhY_g4x&z`jN7rcOFbI8e^#i}HxaB_yi0I5@W=UJ7Wz%i5>fP`pfhfS%*7-oW -!ja6YW%!Tp1lM^W$hzlzz0Uy0h8Nokx$e_=cZjU$T3BZ{Z^#uT|}QZQ2qEmfj*B)6RL5K13bY+lpney7r(>7y)i4p89dfP@-@*8@%L_6S+&Qb7vFQ}JK6-iy -Myta-_<8BDCY7PwY4n1(WYoyjH9On4I`-SXd`0Q?5DQPj}Ub2qPCGN;6<6SROUx?1~|-dWfZS#+L7j -$s0~MBh?ePxo9~qLE*;{~t8Fl;ZHSd(xzEihdS|XWMxPm8YaXmqe&6*+UCU8A&lsp(xWD(TE&qL&q;E -7#)3tm?&pmaBZKEFop4AJhmEPY`zN%GmEhq7;!Qg^z5c*xo42=a+?9f}p7Z=dED2vue9PO8wGZUy!*U -|aMqs|vQ^{Z$Gm7&wyO*9`oQlu${&!b27oZ>pX*ypmDm!XJCl$61wC}{T1luZccJ-|lu{@D4*1$Gjq~B+w=`29|(_f>Q7xP --Z<18UQd?s_J!`io<$4Yg-N@Z4FTHXts-@3Py&vB>knXb6oSZ6M5wd6Bja~>srhj?rSh-Vc^Yx6A;&x -)@lO79JcXN8#CkfkfG`r}zuNVb{pMF~3z-2KD8fMc%4sy2-AbT$yaHXG!{)12bWm+HFlF!9bcEC+Tb# -Ot~JxLi2R30Z)DPoL&IuYH&N@1@gwN)%v14(NW|Jd4%|`gjL?|0IWXK^99Wlk}!Hh-MQz|IZXX+mZ0d)$(31?Ni=ehOF-roelEJDVY+ -t;Ac-M~ix(hn`O53>7nXYtO`*C5c4qSyQ0LaKjErFK;OsZj5$&HbuLyH?eH!!cWlild~ -je`+Ze=9P|&_w@||ZBpMb%7C1O@;X3+{T@_h+9nA}jca#)eOm+5TroY}CNpp9A%BTPa>wOh_pu0Xn=Y -fxO)9eP~gGGt+i`PkpVk7awMZg%-t^01fWHJ@JDb>U@uy}nG%`xI<*gp_|E|;DieM_pL^;q^hD)Wsfw -y>4>GB8~nw5|<2mZPb94e-#Y-tSPI8Ww1Qn(m=={e6Nb!iGXGMHmlAkw)XR*R7f9|7~4Be0PZQRNZ&| -9_%$JA9Hi4SaHL~(tD@k#(O7eZK7`l_hs%87_PC^uuI4D3q)t^?fm>o-+vBYn^f67Y?Iv|K(>C1==Rq -CPq%P%dztcTblU}6&kt$c(!BF5n-)A@zh`iJmKfY`7{q)%NzXr{au3j4ZByrKzdm@rt|QuEUFnH_pl2 -uz4Me*F$~$;ntMzV`X6vXAT6+*f;Q-OD0I~}1Wf3DXfNl#^`k*};JwcNSm7eH-QLV!)(z5ozj}v8SbU -LBZ33TXsn&eTE36`&pEM2pDweU;dHNhE6GF?RiOW^pF>0MUXgAP*qMcVV`-AWH{n$ -veKUTmv^BvpF?YGj=ShCmh!Xo;n{7g+ghx&H{VyMD|Js2{*hg@)(TokE^T<;ikRT=BhHdCoU;u2Mvcb -HjY|HCI3<70h1I47Xuge}O9n%uVz4y6{gSU~U}&xzv~mE8^c45A%}M4uEZO -}VoPX$9y@i!MPI97w?_pTide=!Bqqh61_vlx}v=|Gs8eR8I-vi9zIw`q&KD9p`b2owH9O|Eq);RQ5gY -?dPT0=-nro0&UA5$IZzg8#eps}*}qF7BEwdtbHU`IZ4ozj~@b3~Tuj6`z-a|Gui%#UBZ1lt~F(P&&l{ -oe7eAKSk=fX=hLK8{7x7t6}ti}m-(-yids{dogtYWnSaHPY>&xrOgTR6qK)2Po#&RW_{wgSMLm5nqMoe*^|bMPk?ukIU3A~0dr;~~EsL ->Y^_b=>!OgMRVXoFtPjG}78OMzL(15wZ5)lLP0q@hi>iEXhBwx9NiCHTDM&_47zZK)J*xzAe@FIC#0S -yxjF8*Vd9k?{S{j0xydp{E6@L!m6-m|oQDQwFfgD1-;sTXFGlW!}xsS4?m0x;}<@#f`)(C -XC`07gSzxr^+jUw~czj@roh(T}iwG=UvdMik{)@c|qkD6I6bYruXxU2I3bO!~LrRc!f58R9_?;)- -ksUC8r+R7pXwG1sKz1S-lJg4K3SE@-#mS%ga`Yp7;$Bm6w6VDHm`~Vf1n`OVI>9Hl^!v)5>-PooiLWr -9=S%+ioFRpu&lj%xlF`<4^eN`Eh;{3u^>~ct2GSa+pXmQ~#y0ma(L4L5O3qs^`s5Cy -TG3wxL}fo8leBoo*y^cd+5vI2%HTuwi4X)hHWHo~b-0<)MN?`w;XESvNVFv!zsizn?>&TWGF)Ml^fb# -8SSa59YIP8ri+!`D~oOkMh`jM&q;NjYDC;IDB}?*MzpjO7$f}I%fZ=Y%V@ZYxvXTR_mwK--=Z)xA^G1 -5JPpZU!^N8qB&=Xy~9);X(+DR$Ks2wVdh3khl~mR@~5oT3K_Fv!HhiEYV9StvY*ykn{4hzdA4cnOcsq -}<~~QWSu%5)^k4Lqd2~uX^v2*HXQNxKp0$dIxwM2i-4P=GgKzEiPUB1C-&=*VS#Rj@HL-)uw?HrR<{N -Lk5gy-+-}~~|b6Wgm*g+sR+qZlDx@vu|=KmC|GY4*ReM0G_?eIZS<1JtJ0@0TEs1_6HGOhccQQ7%wd6 -C?I$2_!#=#$+m^y8@GRFBYye;ROaa$6+ZTo@S`KRSGzVBosrZug>y#fWwCcG8C%mcf2ZXBkN1FiCvcvx`+)m`co9n${>&nttTiZ}EMmms{zE) -V3XV0qlsV~a=0*E>99b$0F>(<%HWcS=JU!l`7hwlF0r+g(AKgoH=Mv4AeblDT9>2Z7lRZ42^LsCjKX) -Iy>g%|V{tNW%40M~B5$q6Xf0@=mN@Y^Z`Kfy6`BSVNdf3^J33b+PgW*Usu*Vmrvol4+M{PRm(7+^C)+ -<|SZKVvI(^-LCUM76KCk5h0a9jiUODx~Nxw%(1^ZdZeAEGfH)TOvufx$!X{#Z`w#@z?;p+oB`2lE`2_ -PqIt40f5jQ*=q)sMGNU-c!6g;5T!nu-CjQsYR=EIkm-KuXUte*yNp{x`3~~NN3@7RY~SbHM!UvPurIWQjCAxqOfIdAUCyv=CU=M6g*wl=9c98hcWs|~jVSy -)lk?QtQ-Sc_es$fhFQGYV8zw@du8@L&~&HNoJkKH-Cy&``f5{qa$m -6sgVE6mvRObA{(`*>)f%Yvg=!dqFyj6CIXqHcHMjGB(8Su`ofOk+PVnPp7{nx{no8AX4c4x3dL-(+D% -75rG?Ge+D`Cj>htaw+_{GRI5aJ2=`B7VPn3;h=gpYsy*7x&>Lvvw?@blh#@j-KQv{{0V!{Cr`>A^%>2 -^-pbb4bVO`NPOfD#=7ew*iPKtIx2_qDrj8Vh#vz-avhBi_wm=#nMkjsIv3#1hMq5=y6doZF8g+)oUPn -{PinN$T4KiflH>4Oq4qp?$S-#;r0)uS|69PmbL|TPgA(Ty*s&N^DVqu!*qJ%RYanYB(44U~2wU+irsP -*5?iS^n0sZ%txxJYNmQY#wNS-Ub%UUEE>{*kQyo$=}^RkE+8`S*MxlGEO$eTs)R8ihcdIxcjaQ|744 -uj%`Ed=d5hcK?y^ojY=Dw~FI-&Y~Qem924O_@*Ar$RP$8zg08m2Bu%DBrktFe7n=#=-w|iTFX6=5l{* -`#e6+Sq=JiS@@pW`Mr@Y%6?H7Z9h4(k0-kRrb8T7bG4byMdmG=tdCPa95jcMV-*_WIVuP-G{`rzG!w22z684_wU@wFC2(qyEJM_Nr_dB -fOgTk+Evx*N28vSAT?Uq&FaJw!E`9Emld -9h!Is7(S)4?)Ml0EeO57hTpM6(>k6>3`@OoK1$e~sGzvvY&-QJwFPkB{1`V;#Y5OANq9^|x;)wQq|mt -8srO$_E)#i}ig$$8t2<6n&!Fup&Ns7;Mycj&NM^62wY&ggmdO=UqpHKiMLhmyqkOb&W66fWDw9@Eii` -!}%Nabx?U9hx+NK`msYW|GnG?in$Qyc9`b;MLy>vXzoY4&e7aIOLPDCZeJ6f8Qh=JJo$v=Q=*mYBZ1} -Q7IB?Z>^wI2cIKRlIC5je!WFXWYhmL`2lD;CkA!Ur)=gisAKxdOT+T7GA_P87^1e8Z7qWaZFhS -_FHlx4{%v~j!->3DfKlN0vRQdQY;#H+dz`z*JJxpg&%!^(+8^d3G25gp9UuXm6_hbug@{P9z?ze&4OETELyu%{rc@6MeD%5FM=Or4h* -Lb*1;KiG%e8@CCB+moiWfT8xam_r!Z51tbYuL^r9go|4TA!|SK1TI*M$O~)tHc9b+F6RnJ(JmP-7SwV-5Z^M@lw*D5^oA3IX;Qs>u-7 -gLZjH0f0dHjhI+(GsFd@qZ*&!Rn%@2LLNGO&3P?WG>B=Q^#IbR%}LX;*zYwE^`R)VRQb{)7*|E&K`pO ->K*)BbnE6;%&{JF#LIPB*wy2Ry$2*n@P^iK2|E?`(batjnd%H*f&+F#d_%TwFbpG(Z<-*CidP6Zugtz -tYhp<)kH@#?~h@Ivw(izK)>;sNn`mf_4|a0t<4Y9Sf&lT3CWGYsiSUV@DfthN)r!i@GbN!VysVm=n$R+B-W`iazT9mc+)B -$I7HJl8CTfgKuv3|L#S2eAW>Q@Me3S|kCx_St4`lS$t_Zw1?Af^V;F^UH;yc*uP^$zHfJCV^yNI5Tt>8T4`7*&O)qRLCM`HT2^H$s&FaFfxz^+fFTpPoO>{v>ud+vJd+>|IG1_tcu -^FeCExJXET9q@;;uE95^V6KKn7@g3FI%5B<_`^;Az+)*Tyhb^}Jh(Km&yQJ*f(wOMAC&tivPo(5Y`;s -H}pme1Eygnfnhfb1?zJV-@Au~1Ya1S1VYAZvC8kwVYK#BXcHhGf*vjc2xcPdyujeSqgV2-fpXlqEXkhn -KyIk>9{!gRpOK!UPhf{8lANr-N-gR`n`Gek>dUn4_vd)a3_{fy5-H-OJiemRaBI!-5>2s6x;FJS(^*t -V}v8c1@&m4tSUMPM0PWS6-R05cZmRlqXh_5FdSqzVIgJ3ggl#3AK-Y!GAa8J1sY_nDQ@?RMy=KZF -kXU4gEiFT{dMoeWn(d*=AF}F3h>hmPqgP-hPkmLIk^ix$*HSO%L5?yJ%lA#k%Nj+r8-Pl7F?8ps&Rb* -fhLdjP>28<2mN;nqirp_Wf&DcKxjdpOoE2f4d8EepZt?yS?q3c)qgxuLZ3O&qPn1|6sx4*Z=VR^Utp< -;NT(b9i8~BFZjm;PxJJ`Z+&pf9LjgkU7{R%*f-~fPYQ0W%{i5S=?C9r5h)yNTz}gw?;86yl}wO~3p}g -hN~5Ku1W%-KDQuu^(!X$D2%A(cu=1oI))>m`OFlENvx0xbr3pLaI7Q53+*A3al%)n*=Yvvm!hmj_6}A -|9=)_U3&S3=)dYbatCB6NGlt^h$b9*n?yw0bzlahtf8m;h^MxFuauvZz0?;7j|DK(*rK8vMjd$|;6NB -c9W{h#Ud_M?(*se$S_C`}ou7dCa>(lsOX-l^96rZkz-e#+}upw`j#f7yE%xTuQ1e|*jYvLGhj@yg;AQ -53IHDlS(60r8Gz!2%1c!m{oz3Z}K%jkFZYip&%*^(8MwrG;gRw?flID-QWyzV|(!na|98=5pqonKNgR$|a5?5^1Y#;}ay>qq}>@wrY1jw2M31MU8gxnd{|OcH%< -2r&+)1p)O-<{ylul*f$s9p5a6*;@|cj(xYu_(!H%G>DIOd>Drdso~|(sbSEhTB=u4?+(yray1tHp%`| -0DXNy;ZO*YeXwj_&zFI!FL6k6JBCmm{W^R11oszp?uT>eH|lSKZXfb?Il=MtImdT -SZyVn_?LW%>s@Rx)KIo}~G}3Umf7E7vkKJ)Ie+PC;^>0XVunoG;rlmF3ti}c -ZS)PW87y}|A2k;jR5_@0t_q5EWSXiu?H_o4ozlX<*U+$Bfq0lzz%XVdQ(N&fJ=1G*FV-5%Xr!S6QAUG -478_HDaUe?zZXb3FXmUQ2T%KeqqN9H|q8RhwZooMNoQ{t0TcoeU(6e&7+rw{y?rq?AE9PG9;fHp+y+7i -1^G3UQqusoQQk$LiP@6A-t*E{R$;(>{=~6>~0QqD6!7``sar?We-R?BcNAK6(ZA;qHd$=!&-^lGmJCe -SOvZ%u_vDc_a4%)<{%t*(yBge6MivmwNCN%DN+#>kb_Z~}FB -ehSPCUchy0)dy@5J*0=lR2%4}a+T9IzLRar=>*G|dNJ3iP%i!x|nVJ1_JiE9+ql$iK-;-^r!# -wn!`c!x;9Vef+rx{Wf-cIIr&4=Fv1XZdB#b_m!`8B|Dp-FS+VZZidrwYX(`Fx{rO!cLaThw=Z?`*WRR -dqr4~O^gUH9R!-`#_HK%Y -VQ)AwV*_VgP{biZ)=9Emp(*8s|aX)y(}`cj4}j?u5Bdk= -=WsyDxL!-c7i7M|VHw-d=X^#@u0CfBi=~4{|g1^K$x~&UXES;93;_1B@32Li;O*c3SSm{r$xJ#eH5){ -k3-X*PZ#R_Hx!2&-Jx*_SfSM`o?p8p3eUK@1QS%>vQWL1bxvks-qU_NEzAz?tOWlz?NlbGw#+M-F%(g -dZ3%1lbb)fX`I{$bE9eShVdTVKR6ZoU;17_D)hf}&%spaf9X5QER4k7;~Glm*@E!CC7!Oo16d%*Vm-* -PK)A=G_tvuif*x(*o@?h7;Kh`8@MAU@5ZsmTRRuEUHvrQUK)|Nj>p8%u(e-$FUE3N0xSvLq_M_xBX+(f{&n0x=(;(iKe;)RuV -<-IujhN!(!B|K@@G`+yIS;G$#i4%)C2Bj;cmPv% -yjQbx(1K!_vl5~zKqb`*#5=LzsKx%ytxfdP#)+p`(1i3O2?>i?0sGOEE)X$mc#VE2;;5fiQ11UROBQ2 -4#33z+M92ypzplHw=SU{uUY{o+{;(3gmXEZC2-37@4EB7J=JQy&vXO)K6;qXkx|`;;dda;mpMQ06xUn -2KTHsg4O#z$*VcQW-du7E2nv?&S=xW0_H#8oC9kDhU@Ykc{Wtvvju(WZ_sHV@zS- -Fw#`F^6g?-EY2+e>UJy6-Z3jy6Kfc@~44PNn{o%D(2`ZfEnMw9M4KwL338ngex>v~_+4?YtkDnrMsOh -+D?z8rVDvN!ob!GZlDyz|fBIckRp`BeP)2F3VwcUQ@BB6{dagv$-i-C?KAC>cw4Cd@XNb4vp8l -wdj(=hyJiV{7wTfG2fN;6tWMzrR-qAGPkozl)YVZs95!)eLQXu_Jzc&V9ycfAmX8V_88U{cbW`kRCc+ -fHuPL59l*>E2z$rJL`NUFwoK_2NhNvYm;%ZD4OJ~NyHA^ZfYL-MZ871>@)(+FvzWxpAI&!O+}1Jd@?K=TZ;R4Dfh -7+>Z>Tyr1|@v6{~&i;zgUp4U>|C3MbvPPf1qiNdPLLTNq+9$y|0KyK2u!(>PfOOw&+K04~$o9a1!>j3 -MsSbcW@ -%y$?+w2`wlf=Dj`>`5N4QM?_4$fhtIdt@o6a?ri1JIx(Zf6$Jr9M1@(&1Id)^|v1*D>b*)RhLI<@2mD8!zmBEb+ -(#*Q{rtHU-t(Y#UOCI}))iCK+&WwA{JetZoBGMHG!) -$cL`6wEs*mM@^btxaiE{;C3my4Ki89Zx4b}FqwgZ8-|iZ7x4eLS#NF}&ZJ-#x3o+ns{=lZOFcvTGq}{ -oN)(`qU0f~Q4AP2vNP4^q~k-sP46nf&kZZ#x3%E(vgG5oaRKLwp?ZMa -0dBA0VDYyn?72z_C4I5aMXW1jJd0vk?~}mLRT0tU%n2cp7nL2*-HD(TE{}59BxvF&A+O;u^$v5Ni;>L -;M}lJs9(Ys6iZrI2thq@gc-{h^r8{BJM^!jCcmI0r3W6+d&-rB8DQyB2Gh0M>HbZ5T8LTL##mDhj;|B -7O@`jPsH|vF`tOBh$)CR#Ags!BbFn+gLnY(B;xmoO^7Y9yt*KY;}(|lJy;IC2fE6OM-Z*##B&C_a&eR -n;_k^PuYQ+f3Jk_5rgf0+7_NK`>RZI+4cqUmUwQb(tC+tE#KJMI;Y^#kJ~eV1a!-*jg}fBGmq-_KZ{( -EYyAw|>RMkyk+(;ltdOT6Z;QMT -c{k*x$h#x2RLE-;@+O7cZyV>mkZX~55%D9}ATLJlkGvdtU*t84@Quh@3wyokYHxSs9>RWay873=>6*T -%H(mRmU&UX45uc@y#=p}& -IjuE=96T>Ik`9Xr> -3UsEc#4gKHwRd`ixvalg^T%%XPS0teL=bWp2$eSR4+isaZOU)tS%JXIS&i!Ugo^3X+T(U)MtvsZLP|5DG3w`wbZav8xq7RfjDg=_VK6=%O-KOz9s>>m -a6a%L_m^+dn{|+0`a3v`*Pj7UDT9OIB)oq1WwIbLnDF{Lnx3G8E*1&{90(ZLpO8lYAA)ls4FiD|!jB- -34}dcm&cSf@q2VCVXi(4>elLXcAvn|KWm$FUx%z&X{?k?d`zj -JVdmD{|ZOZ;~f%W|Il=Cn|M2D7yQERkujm~wR)`aHeSs>#!tL_YO7n!ibk(%&7ER*WlEfDxyrS|mET!0t^cB4WBnJDq~+%s4H{a9ECyOD(i}$OK9%WH${%>p -#YbA6&Zx`QXa1d0mHwJ7XXo-!Sk6V??e@QmZ(4tgjJiB%8U~}mYJi?&f!?gi0&gOpLawvw3k_E1^f~! -*uE%$c?zZ}Rci(riQ{Ow+Tc#)W6(r|RbG4A_mz?xV+q@%l0a`V~!--jZQ9n4A -)*iJs+-|=DobbdB#yj$I-R5$P88vrOQSOX -N`HUn{=diHAA!V33P>*e_2=AJkel1zA*b_FEuH+seOr~g6Ms>`p9St*9?KPcg@TtT_(}zTPQjm7@a-y -BzC*#qeTryj;=U}8SV#uRCuX9DzdR%j_Yc&zo@jG5q8Fk!%33Npy3zZ?ws=za0CXRLJREr>@?;D%3Ed|nPDlMSFz#G*& -m;Vv*hKifr5V#|!?Z3&T!QY8p`OPvJk(RxV%8#GkLlQi@%#(j-^X-)hVoM=pW)8$k^DTlEi&K_svDU~ -rjQu8V+zG`5_||5NfJmj@FWrkxt>6xfc@{Gx$Q4d{<~dv`(G^7xV!y5euqC-AB&y-q<{G59Da$@pXwj -}iLjb~xB36=hi&na#~y#;$)}2+Ub^g=XP2)iS^3=at6q5VrP7yIzq01lwd=~(Z`io$wb%bu{>J7lTer -RWR>j-zyt{qJd+%57{NTe~AMM^#wRhkC0|!6;r25d|Pmdh^>{!k56Q6(a<;hdEU!DH?%s1bjtvmPK`T -7grUu?Ma!{sYK{`7O>)n9(S_S^4&G+qDmuNyaS*@>ibbN6WBsrK@2>C@_-*1m1pwrk&^V<*4PUAlJb- -osxL(6d+XK7IQI_755`FnG}5kRd~d4IeS`-q8E*9|iXeW3{^Uj7)u2c8+28oZLKPzG<%6Vl9|Azp!Y* -1G64{C^fBl`NInrJ@V+kJOBUg`2W-P508k9ijEl<8y7!5Au(w}^2AA#r%atTJ!QsBZvW=x|0C@GKcWY -e9Do1xaOnXh$DisS{yBO;=Y0Q@{3`#{cgS;FrawgH?cqhUsah5`VCSoq4714%Rf>?@Jj<_AM60r)g2C?Cw>wHHOa`G|Gdm#EE`XOo%0}(?IwTQ`xX^4f0C5UB+ -m55b{)rd8SwTN|y4Tz11O^Dn -@Mv|CsBvT6XWV$|+Ovx!AF=hjqtg}*Crml!kIYWAYKMN(`*T{dNdrpB_PzWyzKu>`gSv<7A7=1cP)R{ -@R$qZOT;vxR{f?N__kWD7*O=Lobl_cfQBT@Pcf?*1D<{}!-%znU;NS{0cf(`?{e*ztP=C>f1$w60v1s -{llPOCmIU2n#8g8qEUAP#3|8lTL=6Tv=-+&wXIaAxM0-PptI{cr>cn-LLt>8}2!2IioRSuU4jO*p$9h~d2$Sz!;(Z!9(`ppVg5ud}2$4h;>`ilo< -Wd8Leouxy^?n%Sp4%Ljji239BjAu`6sJwt=Grf9tFFOJ5BWb>pARqInoxwdr)?dK17Rm9Lom|lVjAzj -Ih}ps8H;`O8odqoS#EIuRh3Dxh06SPQzcgJ|_&*Qg(-XN~uyp3jZalq8Un2dk?%{-2mSjYp?nE*eY?= -xG7dfRT6aNOnl|B*wG_p$({y}|$zxyGUJow*2DnppPM1qB}+4TrH3AJWW7zlYYH>LRmW;-Hq^;{2=SlO=bU3uHU?oDNM8 -dhi4PiCt{q6^u0n!3m2TEcU4^D{(#oZMr~dc89rr?*biP&K~oZRX3w?0Xir=OCa5E&Om!_a5#;U-gZey$;-2vUQSAXD__JcCh3l_%=0Ir*6`6{No -96h0T}n-TobCv$NAQv1V?)|i?#1J|+n7u|0=x9r(8YyZ{$8GPMe=8l`gJr10BD(kuH9Y*Cw@i4w$mFQ -Ouj7UEvMfY6Ysc*jt>!>2t3!ZzQp4PkDzy3U!omXu+leg`%nO)HKewNv9L&jmhO+WW}`1n!N&u8cOhy -CzsDxc`7H{RNRbhgi~tfBt*4RTXO&+OB5V(zr~efv*?0ex2tDpyCovw2~cfaTiHmj2rEvjdO!Yk7I${ -#AXp#Ze<@yL@wc%DN+abTiIu*)r=BpD2&tvODfNKIyIZ|LXtjinLXHaa@4U(hoarTK?9wH`a}7G4h@9 -57wogv3YO4)u{byd3lYk%bZ?N>Y1SlLtp%4`eR>4#MkS3zVX!<-_akLf4#8M+F?djtJk|<$^9zG_w(; -*?w{!KfNp}(5cIHLx9^hO2Mqsa@K61cGhUv1==F}5;+F>wQg_NX#&t1vrqQMse_gBTIVm}?_4~j6(r$XY_7P_mJ$T>Amp -jjB>K6JOIobZ*CwI<&ZGrCA`)z0IT7vc3bbmH`|1^#Ns{5CX^m;{YFnzRJKTmV{K;KthZOHCm^y={Hf -M15rZ8tG?_h%a}L{EB@t~T`juyEBMsw!*OXUKz%6$KxE{YLwpUwwDh+*+S7K5%}=y5IBmt&E&_?9dDS -rf&J}WWVpr*S!Ar7*UiW=P48ocp&w{q0k0)iKeDIXcNY$R((f6cKI2Tv`^8~a+f#P^SWYkKwcnDCD8CmpVtTi -UMvRLT9?3Ez#~ay@^3V&CuI@@CCw$kZ)fCsKS~|Fo}e+Eb5po4xniQw8%Hq6QutR~Ek3LwzW$#Rp{#) -yFPQt@`uaJ5kcl6_03oTl$!fFTP$`|E5O=Pi^|B1rr|J^x3fR?c=0?!5GS6&(yJpa0J^+j#Vjt82a^YLv={iRLr{ -yq`Qt{-}JWlPIj3+*0H1V>gF3I+rZe0=)rzCZid5bc6bE~HmB%r36=y-<3z`ry>3eW$)RqvngGI}Ly4WYl7s%=RV5Uyw;dXFJoxQPO+E7-TK#nB!?m4@J~PkzxNzOh3q?t8tLpJN$NEcO%*xG -QRR7AwuSQ*8pPxQ|;_zi3H_RCDZOXT!kEPgSdUjYiy?jQ&#lDlX=Xc6@|NG0S-Z#%1?rRt}qRgZGshv -Z9k8JH9@O9+O+82N8x@AM>u{m@;H1pP?-Gki^jj#Cew{xeH)~~R9?Nz_#rwQ4u)^&K}1W?sntI{4`O6Mp-1xwx!ER$FT#R7vCHQ`GZIV~?g -Xbc)zd305yBqU29vy335V-x1`FqFLRG-MMYbt*;d(@$A?@p`EZTae}FOa>l4~$v5JO0|puO=lnd_DJI -K)35&i=*Rb*KR(wFyWkf&1x$L^oxgtby^Vw3X`j6L*)8||5HbA4m)F`@S+er&19&ikQ7`70BfesMdn;oPLM`+NDGdc*QW)CXU+J?7^3l!bC;XWH%)?i9eioq3`hq;xVg2_7=a2l-fAHgrSx?_-e&%1leLi>0ys}G!+b_yWx -HfFi)yiL&w`$$auqwe)-nws^ww*NEyLj%AfZ*5e{rS-8rQ3&m7!jUu{ba7kvg%67EOrH!XQ!!;!&e-w}^Sy}G- -y-M;dqp$Uh!4O_eGgIinY4Gl;bH1ue@u%%8rY9Dd4%aNF-Z$iI6@sy{b({WXoTUU34y}4 -!Q{`{ZR-FD+^Yn%6J7k@RsY2cVs=jN6UzWAkA#zgZsoy*^fnHhBQvooroTi?Wd_H6HV{kEOx^?v6M-4 -6^n(&GKYm){(;a(?N@_B -D;&XbfW5ZhUFC*RC!V$}x|(+O -)Zvc@oHew#_o>R>Kel*2>#{MfbN<)QK3}lJ`q0SL#)CuNFBy3-BCvXS(VM-$WeeXk!}qQ({_^F4kzaM -WXxlJo%mk5ikT{V11Yo`3gD(rZR_`TDidE3DO1mfx8EvGvFo`zs -@i8DC95^1zM}mu|Jo)lEFmiYGs8YR^41fkvHq`qo)_HU- -M^r6(5S3>iqEZJFH}weO<`qWVykm*G_ax%p@Qg@7$ygJU1ygHdAFTY~R+pkRW?z~g-?((_h-SsD_Wmhj%%Wi#CExXfxgbv -&q;L7FO5AOe;l!!K>HL<-|3fIx!Jm`0F=m>pCFZu+Z;zargA8;~aTv8Nqde7$~e?B1Ap_L;S>uf5Ji? -uY-Sj$s~T&&+|KrY_fYD6y9Dm5V&>wicC=dF;ck&E{-e36UyJp7Q0alZz+c -yA#Pxp+?@1bI8;p~%I1G+N~1y@6Qd9g!y^_d}k7T)gLyhFrYYk%L^kH(^39-jgUqF5a`TA@6~_7`b?F -rUbbLc`5P$3f?Q -tLjyxWD4)XEHO~}P_$wK54klT=puB#aNG~^}7rz0;#J`;Hva`7a%9QmWjpJ~J27nQIBs6e?2c@=Uua;T$nO#1k+(*khTIpq33(eK$N1VJw -;^vQIQnld@{7C!@-pNdh5eBG3Hu@MEbNE8i?A2+uEJi(y9s+;#{NOr3waM=ALRbRKFBq~KF9-veUSGQ -`A6PM*avxUk$>cUME;TY75PWrPvjqYf02LWK_dUi2Z;P5A1LyVJQ%t93ic1k{g4ku9*BH6@=)X>kjEk -)i97}Qy~uNrhaxXTejoB;lMLr67Ir1>%mB_~;uSTv#UW+^&c?0rDd5BL -B!2i~J*(Z~&r3?vC7q?b#D~GRnP>ry=(i{*n72w;}gMUV^+G@-pP@kyjw^h`b7U7vwd_yCZKvJ`i~m@ -=?frf5!TST!TCkc?j}3$YYU9IG{>F?v6YMxhL{MdSuMz1%UMJFn1GGkw -9x-5R#PlHdMec=MgWMZ=2y!3fTI5}jCnFz)JPo;o13VLQcjPt^Kk^b0Kk_mWKk^C@Kk_OOKk^z8Kk^0 - -lPcpP9hiSWqPzhHRee#l254@55Eo4TRMy^zO>@W@kye;i=u2>-|ng@5G5!aq)*lnVdI%Y}dBmBRmU%) -juDyjJ)}-XQ$r0JTZTk*j}2`Dn~Pa&P2;LaxR93pw&w!Er#EB6uw3U+@IXzu-84EJp6W5c7|`3-WU0I --J8#XR8(nn=7S{HCXO`qI1{92s~`xQ2c<_AF(7jNIq09S`XC^+Ao6tyvr#XducwC* -^%{|zkekrI8QsmOF9+kX;GDjN%tf5X(26T4tmv-*(LgdW|9bQ<^26iF#qhbvvoM|WFn>9?wm}CL0E&6wqpLdo@|WAirk9npM&L)C)-1Z+=$_F(O*7`koTv>bmfOHYD)pP9?#39|jNSb4_D>6ypMYnpr?o5Ni}BlWCzd&!S%_aH%(WEZvE6zN~E -Cc&40>LS`2SU%{4DiSe}k_=E2y(QDQv)D7pUd_!H&w6!AOyL!(Q5)8qBbaeXqoq&FSQEBv$)aZP@H -3q9%gfodoklvPi}yUP;U~-Wh=-3*lvA>-pZkxO^>P1Goa(=Y)&EH@{u7+i!|gWR#ecM19^C(UC%KWuA -M4^jQohb}|KW=Ao+{@@hvncJK3O53s3^w>r~Fx%eWtnaSf_emVYMVqq2F~KENxd)(Efb9D4r7wyNKuV -;<y`THV&ih0 -38l#6;on -qe759MNBra>`}8Hn-*1NjbN59E7=J&=n{mUEDA681p8P1pnZSz!<4&kK7XuM_q_ -{-Ll3^3Q}lke?IwK>oGJKk{!y{#$VVmB>Hxk41dg9>qLrAj-wOTBBkfG!*4xJ#39)UNsiw;{A;##XNZ -m%Efg<%ySMB_C&e-1_bIC^Xi2tUxmCF`D@5ak-v$&9J#oTS0aB8c{TD~$V(LSthFdF7U@^a+cuzF%sY -yCeKGIfgmQ7667#rXURdqP;r&ql7V<#kVq75B4TyCGp(q#EO)*a`HVBMG`E2B6ih1}Hl< -P$NiggM(C>Qg#VqJn*w@`@k7mybt7xyz_-GX>bUW#&Yofh-pVjjI5<>HMTG0!dDiKs+*A#ySAd_VGPl -rKRp)=9|wKPu+YYtjGH$QzK0`vx(uE>?&)p|EcpjLBvHu5UPIt4$JXNmkFUoP}3)*XbRJP&y+ -^2d;idGs;JQ&9dSaxo7-3V9C7A4e|M6^K>%g(!bj=tnL#Xe>oufV>>JxPK7qP{cZwN|e7S?1fy6TWgW -Ogj}p!5$jqSQ2s1(u}(p(t7t;`Bf>t4buel#E*IlSvF=5zi}6Fb821JuFBSPg{*2I%d>!%>jVbv2!k7ouE@i^Vz#e2h+tQT_z-Qsn3*3F1@Hnk`hi|!kc?? -&E)d_QuvH|G_?p2#bOJ&}JZ?1}snk$=THqga&B68XdD_~((QpnRXmAM%fcy^z0-ybSrv$i=!OdH+|Gu -NL+}F7{s(>u81v`=ES*(63mB(}41&$i=!DvCgLnGE9gSG`qi)INi$#7E>#Y1x{t)s&$JqWt{jx7As6e6a2kmeqI|7LAM%Hhi*;6FT~;~Dal2Vki98> -9HFB};uNL`t$QzKCAa6px9=Y0w^LK*e -bM=LY$Hlk?efemS2b_rILy%H<(=kqggLq%U8-esDflA-CMdUG-Vz`o(#XQ+^k){L}w4oSwHhyXQJR-{ -H^i(`Eg9{v}`6Q8%qlJ&WPg=Z_cK~5j%v*mKu<9lv%~TU>ayQ~D{-k?RfTdGa`sbE8~-oSU5 -T$GKj?%?e)R!Y%T3m-7OJe4dj&D@(tF7r5ky&qEpH`1rhGmYiNb-;*WVCky)-y$g5LAHj26c$SlXvrG -NW!hS>8o6mzd(#Pj%9PN_NTRPs;=krg_ax=DDXPzU+&*wRF+ssk3;x8s6kE-pXYGok8 -^`uANYIov*mu9&!1(>_TuwhbL93hN73(yc^~1z-?N`1r=QP{&5`@9Tzn(gD)V`ai~7OmeH`tP&kyFw{ -SlwXa>UP{7Uwx}vqH|>n=8+g>t7zeO)ApE=Y@@O{o(UMj{3v7Q7%s-_8$)a7MK3Og!R=Z^IXcga*N -d1~EV4ET65A8=X=l<`W!zF022!a(trwxL#MC-JP$87QAko?&zPpyZWD}u+Kb&J?69e?chZ&BLl0#-d5 -}n96X0zUvgybdOfk?xWX)Rt4n$EdH(|0KVK(clFNtJj)L399nS8~`Q`Ka^W^mL`FBTp_; -T)A|2cAcIM0*o7v~nae^`L_m@o4Jm-z2=lvk}_k10x*tYesB{TIU!@3$D93V)a3iN^OB<~{cTL(hJ@8 -9s8VilS}O3;P%!ka&QHw;yEq^p;PUeCDJ>j34^-Fynu$ID&dUW9YZ@7{#jI8*7++R`zkm!~36L{JRE* -+Jx6XXYR{%UoiJ=UA|=a(Q$^P`pJ__e(Jtcj5qzj&^By!E#s@keZ}Oxer0I8vgS0?`$yE*j3-`ZSoOz ->GtB+g&~I3{*B0`+hA$ZE6E|4;k_u)Bj -Kr!@5gV3^k`b&$IA>Nerv%o@bb*`if!QithDj2Zq|FO$25Z -CJ2i=dPs>gq?Y9#*4j%=ZDRgK99ZWX$<@Q+RNk44xJO$)a{pR_5E|hW^_FpTm7VOSV;K1?bEI$gq03^ -@5bP=jIeoLbBbU8UWa<-giV^dap{WbbHZ-yd~0s?eY&vB&m_;!zto3C1RQ^7*TuZB%#KN=%T0MaN|VUN#x_ezVmQp0jm-yQS))ugc3=63lj@}-!t7pEs_&vwfVdno<1_vTgFu -q)?pzL*ef3>&`UjZ0r%OAp&U>xl7Cc4pYjD`RT3CS%yhZu*_|CWw(xS7x1UZA3;oU9sr%D2!&dI_ --|aggBkcLh9X~&}CNC`Fz#E-|re%aBsAGTI_T7}Q1&69u!_8}(uy?|54^7n>v?w -PmX7&xVd>q%U;g!7ec05=zt0GplM>c;>$x8W#!dqG(q0i{(j53`Yr~CBMXNm{+6Cz8RmN4(ZCuH|MbpBs6mi^yVV^eUmJfu-oJoz18X@ -CKhWZWGl14lxxV~K7@oHAP3_$?*Zg`v$=WsRIsKFY(;nF(Xbt@7}@L4@u!;@D4XFw<`aOlPqSW_Nn?WO;OE_6-%P^fXOr}p;uHz7<;e1je(qpDkw4xJops%2?@T?b3eJ8s1s0mLsZ(NtL+@NZ{d)DIUkhQmptVR5 -!|nb9`avFZbMxm@#WeJtl68By=55kU{WE75&;qw;0`>EthcRSn^m!(0QUAY>FCD5HRwfQBC{K%H!#KJIfz8wT!LM7Ai;T_>(m9D)Ae*I-(Yd@$5X1yB6UxL9>Pr -9X4lRGKsXQt-NOyFXkN>Xp{1+YNNq5fIf7dZ-a@1sX`=~Pk=X=Mm`tglVkH_@Spz%lEKWi#nnk}=YK_ -e}im2A$ZU(2z~vKZ*+b5e8hx;ZO+QsT(rvnJ_t^*W1w*8GvfgLUS-L8eUcobL*Bjtw$o!WFzXd;AY&&DMzw3JJ-`f>*5y2!qzF=sAGe|jjls#1UEm28(W&B-ss|w#o_DXmdxVt+bFTQWprcHY=N% -+QmDreS2w0F)WvNI)2DUOH--622Db_@^dK{}9bxU;&rR^&3l=QMOsDsGeqB{upV^wxn`f73T-;_deSy -wyQ@Fp7t|}fcbL0NB;MUj@+zNTTA^lWrULVVlO-ZMKo1eptO;ee`&8e)}R1E!2bwmr{_!) -*D+tB9=&@N^t4m{g>i#&1FoHt{Q2YV)k}iv7jrm&{ -(MJL(fJ(UQ}tCg>8bkjjZZ=SwcG7Z?pkN}7a%F$sUoUY^{hYtBNW9sZI$&9DC-N#JfJ$ATU>v>1<|TK -h?dmXv-KtFbC1=-J-F645Zul!{z9Vh=8$CHS(HCkv!pjTuRZ_rKGWI;6 -NhZ7Uk&BC&|P_T*>N~Q$-Qf%quw=4Ya#Nw|%-w%HGWWRgX2f*(R_0h1^55Zr^#wA66fFlA2~L;t+BRuO}2%Xmn@jG{@_)v@L$E#IO^KvYQa`cD1wq*8qF=pf2>CNy3hrhNoz -lyX}?}ze-^8otsqkbN9_2YAp?gbC+0{t9^bgpu$=K)F^9VR5|Q?Szt?th3vUVIed`3!80@=-L-Wv=Bg -1?29>z?SI80&zyO`oQBHjQz&Fh(U-W5J!Rkkxunx5Y&qx>>E2M(v$Kf*z-$AnYB~MLry|Jbr~b3g7zbM!2PvoU)(+E*sH@&5 -m~tm`!NCv=_bx#PF@n-R1>sa8EE+IcOw#j`kSSlpXvUvT_R{ZO)R@EzLXEH{blg42X$2G>O20W6KYY# -(F&muq~}SbcLX`}!O3-z|r|jzGSuSbcNY=zh@c>D0eXgEaN#e&l|M_IF#`O2ns~B-ihO@Vm~550bA1u -Hnx3!uYa{!(Sl$);jTg5~4YG!dqO8mWe!-LY}(9wW5e5{`77s>Zs|0>_jR44%Q=w*?(XO -IPSnn!uRa7jB0n7=PDC)bnc~b*Y#1h1{h!u#t5la+ihY`;pHXz4-)|8{#vFWr!7s`w)*H)*{ -v;{)yOrFy<367BL0UhWHHPYQ%EHcMuOCo<#f}u?evSmRFY$N1dg|EiC7IupD|1bd_InpRBh=<{K^fx% -x<}IX6~k%*@rB$siAKk8?~&#OJ5;4=a;!_lb1w;*PRf&_<_Al6BUc>EtVE9M#PpWF{Dl`st*zinmgb>;SeTGa{ioZ1NuM?ac-07Hs}}!c^52aq#xAQI~7gzJg^${;50s}EHX -b+FMpqPeDtKG=mat2Hipm=0Mn;4vy&B!Ptxb*&trz0Y|>{;CljQ}Rx|vI10R!P6Q+kJ$Fbm1yo8hWW> -&<;3`j+R(F(Pq%kAYCmq~g!xlUnU5C=zciIr=S#j4MvQ+weS8ZrGv(%l@pwogtlSgf%Sgcxa!n`+EqR -?Unq%+Q-?<$_Q&<{Dm0bY^RTiB`Soq|i|pcu7DmPPk99=1w)vHyA+y8A3HWmE#)8u?ziL$$6*wb&~V` -@~=u_SNdU6G|jhSYk5N6cT3Rg<|((FCnVMFZu`-LkdW$4Y_CxJ;~*t}w`q7{X -cwk@WE23C;d84oejCoNvsXqueUy6Z3VC{q~V!vYyW((8AQnQ@7CFu*9({%ELTbu=~Z^&e|8rmV*PSX}1nG -CnoRQrVJMEZL_OK~!-0_2v5Xv9QEIwdomjHeQ(-sz#{O -E}z^IoV_|vbI9jdQ6e)S{eN<*R}PO$#rcVWz2{lIrlQmMr&QarT{Km+?>8+Fr9w&+3|S-S{n5IJl?Z# -tFDKdTwuweB%&ZIOK-N&7lj>VKB2&BDzH+!CbH6;47q~7g#DhR=gIfww#l53#p`1mUi%zX7azY -Fl6K75-!xLnih@!y`b|4)wQKTOT#&3`!g-<$6L?&JUUdZ9S}e_l-_mAlJuVG~>_(v}o02|)fjZY;4LH -$Ygtu8uxoC)!7B49mg)-i@a>YMRl%_!dVk;*NjlKOFHr@PBMW{e~{&!iI(o4>apv`^b9sYoZ67ryn}4 -A*rWlpT2#0z~awqzpcAY$dE^P$Oh4G&-|~!NAb9VdfjLK-{BxUZ{hXz7G94F=!@dxJ;Zh}|9kwuc0ez -?wX2Hw0agOW0R5LbsRQ%@q>BXG16Bb>1*piv4um8Crgb -D_3ZS|ZA+rGM02}(M*v28(07C%X`>9AWV0%CtU^hTb7eac|H9|e;JMHJC!XCJ;2zjzU-Am=XL^K>L08 -{AtBi0dS&9?-tsdkXDl3Ew&Hc10!9k>O<$B>PQn{G5AjoBqUg+SJ{HV4!}d9(QKmbg -XkUvHj=}zj@Xcw0#;G^VD6qXn63$`BgKF<;2t=HyK4cpV6TcHG~IQ?7rK%J$^qL`I}W4kQFKz}FuM1X -PNFZM#{tF;=lXJn(>;OGRb?Z1di_SyJr~m43rEtur9j`kkZ;gadM|x9vtFVv>{Fz3BkNTLci%DeX -l9z*x~s3V1dnczP(jHfRdu^6xc+{*zwNc9qZ1E&bm5vt|svT5lW@H$efrSItJRk7hPR)zAe2GmRGQV0 -wr_ez)C8lwma1$V!3FxCgG9Y^1(20gKKua$JE25>vL$Hr0p=cFpYTHxAvT0ZGg@^~l*>AX~&K=%@;BN -d5sPpLZ6NH@mmp(2G7pnd?VCt|rH)=tHAP2*TOjqU}alT0aekE2YLW(LO^z!9o+$#)hZ{Q)(pw4Ugsa -=;xD*jq=)PQbtn$d@WZWy*r`g7_*8bUn#=DQz}K^&GI1>N_bgk7H~;-TO=@X-srasSH(viN=$q3bfEC -vKLjR0vdn16gZFW(R5yF0Q?QGt`O`6`pb*xUTB%Bv;{Q0UKRK-$40Yk -;3Xha4XfbmXITWIe;HYP#$H_z9fU2Z9QujQqy{>H%*nif$rr4?Rx`n52YK}H@>BY4ZIzyH*)*rFw{xa -8+m(b+{nu#bQ2+0ARf~uUT+%#eKpWOz<6h|+eNpUazZY(xad~>7UT=un<{v_NO_yCJA-%;%K-yHUiA* -GCmE`ecX_+71`L4mZ`e-X6|9#^b`WwE+MDftNT2GGTiH%1ADXWZp?PzT$!?LekLS -B$KOu!Q{{1xF>5}#U-Sh9f)Cjl++zSs9;sNd7_A&Htz^f0@dV5|~c$lZR7O)1~(>|r)>LlMIg#1YR;i -L3jW}TD+*a-5F&mjNM-)oN1aA_*rFILWt{ -Sx{Uz!JbZK;M&aoubzlz$(C+lXUO5I#uZ@8Xn5$6iru}ss?QX_dM+Rxw@~_RQgA01;m!rH32=xiQx*FI?XT- -d*$=cIKd-V~rtPL)(pI!WiS*q9{>0U5tD)mo<4219@jnFSaIaFSS{sDN(uW(%jEcp%kdqB+}gyd -5GCzKmt8OX~{mkdDS~61zSY_K-*&xHkaOekjc! -(h1kQ;s6?dy3_2Ru3wB@%#q%=2!~&1jMK5L*uWPD*AB$<$Wb+XVBmGqx#NE)qv~ -ibv00e{s!79;9kf_PJapNcfC~GpQao121&%dH{=^I6EJXq#QG-_U?6b1KIU=2ia`>)euoSOdjnU8NaP -A&<4}p50n8a8k*}cN(~gwL)9#mAG~5rP`93OAHj1XBPSTDB{lI<0C1L}NjgUwwU}2<0wgNUq!O)1xV< -Zv_sEwoP)TzqisQu1K4S?0)UNT;SdJWgT1d8V+Z6dWZ^e;&g@qzJ~Z4#sl{An^FU4Yeqw12DvJP74lu -9w(#Ha1Hl1HpgVY>6ZQYUfBK1+WS5ZMYtl=Sn0OctySh*XxUJDJEXu3ITV5o{+f`YlpVERByc$YnI41 -=&zeB67CsoX&<#SH*n=e7Xd`>D|D6#$}a1kv>sJD+w;iYX#^gR~k7p#JhVc+x`#c`)4 -c?3uKqmv8fCVkp6!)oqv4D)VsG-r-oc&fPrHLVJqmAo;K?0|n -{h8~#=X25_sVA6tDAAJZN|Of4({~(YE2S<@1Vh{*qR=PI^g7A^Ca}qPES&*mqTCR;ou>wK;Fl}Yj*Fzy3vb?%YYLs;bDTQ>VzSTek>4g0L~qKpH9YAuEp9d`Rn0=FdOI=rQ}xH*7vP=FeZakb3ype&o -h6`;k9D{0VS1ke?we@cHv8Z#Z`3$Pvcb6oMN5``LaYi^=CNY*b%?!1j-k&p)PaqT#c$ZgBo6^?&48R@ -Sjkkl*0)TyVcZIq1Iua(aNDN4b8^bN^uej~qGnF?aZb=|4;3=N#g<+m{_XLX!q=G<`RYT|W!#cPeM;U -vgvVksC*Tr#dLVHEC8Hc({=Z`hk1wI@G@Znj^;`3t7}Y>e_9K+x^U+;WSZx^NFEt5{?`LAJ0&8sc*co -dPpeuzHlowlp{?yj@hc1|6_Lh&8d5uZZI+R5B9yZrpeC4%>Gn -G0~b^ON9a+@#Y{g-8*VJBF2;k4$qoOiN9){dW1$N8JDqNDN;bkfoMui5{W*sl+syeWoOTeVc@&B$YmeRj=$^}|P!r}n`qOWz@4=5p*IszQZvQK5(V+`dr%v6Il`wn@V -CtOsxfAU69jW$5p6<8jfvKtFQTxU{JEl^7JC{@akJ{}&{_#fzVYL|U86WZYxBGhf+ld>V0$xAQ2L|%URWwK_?8d6qPMqYdEHCDbA6%}O1jvcHo+qG*K`Fwu`S#s4*b~M__!Gj0Mp+kqr(W6Jn@ -#DwI7hilqPM~l$}eI?mpf17+^KQ8)H(jLP1gzzB{ehh?9fbcUQd^UtHw6`H -kA^aK$|0aa5g77CGe7#fnz7Spu;b%g4D}-MT;opGp`yl*r2ww}~zlHGUA^ar>|1*UD-6{OwzHnU$hr0 -d%AthEqDwY#+>M-w+rJ;U_`(3welii(dMJ2pHjH8`+ezd -pUkjq4>eM8$y^{>8zMsHoHd{i%m><1{FbPmGL?9}DWEBg4l=gr^Rq9{Tj@8G!P*L~VF{cw|&0gs15mG -;qLxuvAAP;}he>5!2PLQ|h2WTtn}k0h(UpXn4>cA0Ib1KDB$h&VC>V4}s{RZz_Zz8%_g9#;0~|*RC^{ -vo!YZsewes$@)_}v~SmL{21n80P_%_q2VK=s5G3)+qdgJ9+Q?3L}P&Psbj+vlj7r}<5Rm(1)cpmb?WH -j;U~;Ts)*wGf;VCbYfh5d}4H>zl#Tu_X}eBBNC(HqZ6YNV|sMyfF7v4e_(I|;rb(46vA -q$qp1h9AJac%R3ex-F*-hxWrD>Z><{TrjT)k*Nr(p>U1DO|wQC0+WO?ey7OC37(GWor$a}=Zuq1So<& -mvDT8G{f5*;6v2v&~qXPM|OHaZ=9e|Y#%x7HpZV^b4T6XT*Wm0}-t5*Il5R -qgwY9uw=7*uCdQ-a3v?NRC38ii$Fd7Fs6Ni74v`DGDWs;sZ+&A15&l@#Hal<;*ZM%zMyGF-_5|$X=+W -RF-5~q?weD$VdPC|E<{zHxn;Cp6`D5ey#6YY-aXj{ol_$dk-HvxOV@Te&IoFt_uig7t0U=#E~PSS&%O -8AKbThM6Wh218*MAJ7b2hEZ=>1Ow8r|W5x^_JZivAty}fse`Chr>cfX#UVlss4jDDz#ulyh*dG(k;wo -QzAnC&broW-xO{2!djEo+@vZg57>-$D*P`iF32QwXqYj)%ML&a|XzHhIX0mJL=_Q?-le<%y$@L|3C`| -Z|TpGh-(aDTtu%80sSq~C6$(m6G4bD}FbkSM)2@!XtxX1%dojuPz`&ov&W$awCdhaM8~NOtnQwXsK0) -*vM}b(m5xB}UmeZ)x;PxH6cL=)araz;n+bOhb*O0E^?Sk6gaU*|j-?4K@2p`)ns6%*Ic$jiqr|zNQw_hLLjt{u$# -vA{9TgR@U;Wu0#`e*eEoi4O{cwpckL;0LLZocWp_QAJw55FNW@cK5bTK(~kwr$)0;kx$SZ@(e%dVCQc -)ZtI916p?q$GE*sD_sA-f?NCtf4ZUb^=;aOv3_;z`rj>YgG-&W3It4A2~dfq|Nq|y4<1~~c{J_Vv4g(-_FH0qN(T-cK!J)5A3jW+e-rjI -4#a-;i>j)s?-U>f6tyhv}MZ{V)&?}q(s1d?AS5-=9_P*yu6(D?%gYn!#Nxy;QO -O|&bsyM*U#R(dGl-z*@n%ZKYvt4M#j**yu3J$s{tEcaTa#VEw}Vqv}n=ecinXtXcs(m=+IDH_e$YEef -l(UEVHb0=gz&EhIz-2AE(mN(poxy^wCH3>8GCxIGH~JcKlyiSxKLN{<-)JoaE$~M{HT<&iwe}k2NfJ7 -x!2jl(Z!Tdzc4C=W033Gh+9lwJe0=uVXMzV`ef5?23>cvktixivfB$|lp&Q@_w%{9 -d2j{%^-g^Rm@R0SNz=vOdm3ImH27kW5-(SRj*5SLZ1pag9&JnvIA^(w)k=-E2n1KI-4?du+TelMPoH% -9{It==QAMd{Vu9$$mtgKAvN<~G5_#ZgH20-4RC*Toa+w=ob@@GWR9}?aDE>ZYfL}5FLdX^GBw1>!XjO -gsSbNg9WZfg>94gA};Z(r7>OPAgp!{@QR1V020fEP3eU4ajH0od_Bc>m>>Uy6zUL37X>@InrN3*ZL*! -DG|+L_-e{>Gu&0_=u?Q`$WCU7>C_N-5G~&ZxD5Qoha`R(Z!1w*E9|KW%$|F*G|AMr{sm4SEDD;1IWvx -UlQG0K@{@|(;ql65Bj`EbQj~0evs(gMWXS0i6XYwwkDbd|Ia`F3|U^%t5>g1EDH;O1M}fGxBwQ&5n!w -j56~Sn2ag~(kbl@+&e;S&qZt1CKKJ1;i2;UhX~e%zw9Ck-Ermcvn^iMwQ -JYj%!fSa8Ei{^ctF-0k}uF7`{5UG9_);E#2;lH1rA>j4QCvNF%HEisX60Eo~s&y|Ce8WA@(zMCUB5b; -!z(@>civ0g$uNI%UZI`8bPZjN7IT)dI7}a-w-|WHPM5WL?eO2r>Z`)%tT3je%GhZuu0vv`}DcTHlI$j -#eKga_?gd3?!No(PHfK?0Dj1cKOT^K$hAKn5*NriwhU6ctVzNUvlL5;$Ny`5nQ92`@;`CgMZ;Tt9AkhIsNgdPe&v!7@OzxqYbnAljqq1WY1z8rVpgE)yL`Jc^?jA7>Ci{G7cJj -9--><;Qcu^TCZIa@N;~>q<8P$oj68W0Qg}Sv4zY42gE!Q7kmd_E8{ZkgTF=0fbn` -xKiZVtkJd8|ZpOjJIBZ)viO!t*u35(BJH|7Oc@io0S&cD5w$~3o?1hy7{{8!R125!+z5;H*2G}Jou>G -+2$ZKFX;j7@=Ft*Q&q%DlYYuWv&c-8=NF%JIv{7<$?%hCqZl4picZfXq8pAbWH#to&#(;lZU50(oYv@ -u3cHO6QNezx~Q{u!P;zzw*t1x$b&`T)2kPhjJ<@iF`_aD@E7nLCKyU>r&qhmErt2i67XGjK4eIIL80$ -OjHj521xAjKlb$ls#^kzySj`slO)1=>1mx@(-H`7_I&m;9W*nYp9Of|&vl#~r{}|(M`|GzqfS+YS$Un>V0^su -e^UssZu0&aK+}D(za(mr5Cjt&D@vXT||GN$RtKZPJ=4(e!eLo?c=c@)(B~7>B>J9L-H0Dgc -E|8mx&iUOiF2{RjL~{s#;g&hlJTKATk>R;xJtgX -JjinZdLu)rSLyh%wkd-_LXHx5EFvr~G!@3ZAq7UH0IE4~|GrPu~Mrp^wmk?CflTgI2zp;t96UDgPX+z -HxB3r#8lbKEJ5yv)^bm{sH{peQazj_&|2MUBq69Wfm=3MAN5Fuf?G;JR0K(25i#C>}KdQ<4}*CX=vOE -I_2l*?_}FaAt50&X3Q9}SS$p;oSB(PQ>RX)B}-SDy!F;wLLLA+Uks{;p^L?29qrpPjQ~X!A=G==1kB(Ydo{_cOnnU4JMjD4+!k7MxzTY#Gg)H}CA -@k3UWmCr%V}=e$zL5^#WC08hXMJp6G%4FY}`at|L3T|qqpc_h9=j0D~RmwE=H|I3%4(UsPJa5C$zAtN -K>@N>^S_bcFh^wCGfgicEwx_9qRGiJ;Xe1Ol8wH=8E;064$=8N0_Tj&FH1p45w@9^2$+ynWua)tFb=o -aLkWyv^q?%a=|rz1v;pjoqK2|fS^z&~Wj5K2f$5Z{9bz(G#nAoE`2Vu-IH_ka^y$PjST$`)cO9M87oA -6>e1>9cX;#-+$1t_FU{CMIMF^=5nrxKMMRHf@^F-_UTT(aV7IFl80J(xa -2Jd8jLlKY3ZFc`cz|VLu>C>lAC#K;7=<^$IydiJ^eNZ=K{SY`LB_+}Q_unsM%WO6a_(5yX9RCAu=mG2 -xzDH~d`9Z7=J%A2CCvaWZV21ryzRrT+;9x!T-L!l6?kdIC^J(2&__3}w{*iRWM7{`kn772S1@0;q`oR -1qmNy{-@KK<#JV(|OVMjqz*#8Y1Hju~T5kAoC^@{UAYo=2b+qNg!N6uo{cky=%6<^<@x&0dvzajpC{b -zm5!)NFlwtxlvhR(8_idZ5oElt=~)^*|EU~3@XQVyX9&`aT)2?fW(hy79e_Fty; -B8$g|X8OyDhR^N5@988RdL2W&HoTDENYufSdlzt%5mV;F60(-==M4F-caA9&$-;0AahTdunYLL^-s1Fwe=t10N;uD1@;ee0DlDE2^u5k1nt2GS>FX8 -@EJBAwib4ba|+SdV4SZt>;itsZew!Zki2Vc5%$Zv1?UVMA?qwhzjBOF#k_)FZGVkn$GBPX-GlJ&oLB? -*JzSF?6H*R4mmM=^TvS@quhBDRR4PhzwQtM+UE^&_bsL~=m20#O@VmZd-5hmWdM)6eqi)H!4N$i&)op -8a+g9CnP`AdU#^W -Y*~#*fWkf&$7M!j_uO-Y?nUc_}s*??b93s<2MX=e^e)qH%?7B>g$uC7K%RK0JWC;@oA!uek6+CE!O{` -k@at98(zq3)@b|b@?H~JKUZV^UwNKpo4cRw=IgjHdZy?%qR)l?3Too0#eQ;zsQjm9^?EzNPS2Y+FKPM -me8>{OpUDO$;{NzEbuf6xd|DFfX1pe#fvCpVze!Qq-=BYKnr%n=$IqvJ5K7 -7R2_m%e#Xyp<8A@muec>76Rnd|_c!ue|w$GP#^`!K+N#K3?B{%7mTG*JUXJs354xjyFKj{Ewa=(h=Zt -nl^P(BpwTqDO^ZC;Ehsy(dmBlk3BebH1NGd-m+7wf$}x&ti+%+LIk4YKf@lqK>FtAH7%fk014uN4?sk -L9Y)zCdgyh8$SCW*GK%L?UnlX-vJ9ovD%*~obIc~o;h`#=Kfe02FPQ)pFBpZJ=0#QJo@$N#QK~kC9Pb -!GF}S@)`py8i-CR{YO3fjpeDTO<%}kKhR4qnO<`CD?)CNBpcCkKYS%|xF>~h3^f`0pJgx1$0tVDgKno -1i*U@WotNlLonNSafJfg>h-o|{jzcg#CZ?2e^Dqs-uSbn+p?Z3W%FWA2a4F2z27%lo;=oO)+iQ2f7N7 -Tj9UqNp|^tjaZXOFu_taYzO9tXZ(m;Z>#7A;zov}Vnkc<3VXeZ=R$8?b-|*zPM#6@5eWt58>Us*%M4tfl>0-6MZr11o -`b>+}KG_@<1`HvOyzUb`5B*$M|7Cs#Jw(i}?Xzfmcc4>KJ)`}*yrRc~UflwQAzSS=YQ}HuAJ$*IcyZD -zue=hE-Vow(z#;Q$dEX%504-pTunzVRb*j@NO*nh -|Z=mA;8+S;De4@W+xlSdA)PWZ{=#KE<_vc}ejPeGjnHC@mc`4#J8Z7tou7bbZFUc9v|j;fD-OsBp+a9 -JLwUY5tz*^goQdi_6NzlB^sK0f}d=;&xscSUbHH8qu9c;N++-vK6R3#6?Ce?XhlRiDv^#%E~D{K4X00 -4a~AnfK6!e`e5$qlbtZh<^qR8q`a#*UtnV+1c5Xo_gx3cOoJp=$U7p5q03aygX66khu?NjxFdf?`;G< -{CzS8uF1#kW7%pn{_D}B$0MxwA`gVGM_&fLC62Rb^ytw7?}lp4{=AWXT;5x$;MGw|6F=~0`eSX5KaQi -9h`RCc;loA!6);SmJXz2k_CWSXqzuR!vXlq*6Bj2=oH&7HIg?`~{IDB2e^WVC2hfUXpP!PF@-})rsL? -<2$Rh+^pufg`xwc;bx=T5f_lU?X`>OLy&*%7B>o`9@srY`f(bVW5Yy7wdVmr_qwOq&`YU$`}KtHs71n ->ZM0p|f0&;oWFHWP5}*s)`&(!3sp(ElY%mLwUC#(0@?gU^6r!h{JF78XViJn(>kP1gNne+b7xZlt}ye -Tf@3Y%nUmT%kEb*gx2x`Sa&Ljq9N%51$X4gWkfEPd+Jn%+L!Qivb$Q8ZY8y^i&YHGY)I6VC~xV{cBj- -ngsmOlqpk$ZNR;%vaSezf!4qqHhKU4{njg7Tda?D*wzU9UmqMY2425@J*{24R@91N8&PwTJ;keBTda? -Dr2mC10bdz|L-%A~5->o<(BnY;Y4hgI@;W5J}8z8rY- -Prq@)XL)XdZGs*FR{8sm@@-~XHpv$oj#YCfGu5f -9=X5%kr-OUR3VEMC5|Sj7Fa$JWN&K=w7zkNtwMTjqR*KHF^H_?b}7HChy0A-A2p>e!!k;VZs0KvA0B;@;pYYtZ77E@cfF}pwscS72ynO(l@XWP8C=n4p>x3t+wNj7NiRZ0dryi -*jPg`rPL`7$%P1H?Ko|2h5DScqbojoH$bjj&sCyh%@pD-}wk-y&4BQivnIW;MLT++lz>B$2_W+rEbL= -U?0`lzJL%;dDECeGCH1=2GIhD@81-am6}N^)9KW{eYr|~MuGQ}k?@9Bi3+a+Q&e~@av)o -{q#J)|(0lwo8XN}J0G9s}Q<gSpsTYA!cdnybx!;)jkw-_wNmQqW(rP5MuQLI5$oi)O$x5 -il$tr^xFYrfTBEw+|g%dM5xYO7KdRHQ44DAE_j6(tsB6y+4<7a59*i%N^iizHZF!9}4ATO>mlTf`@o2KYK7#z13`G1#axh8iP`kw(2S));3 -@FeVyPj2Xr(W2LdmSZ%B^DyBeFkSW-tGliNWOpzwNDb^HcN-!mwQcM}9EK`mt*OYH6Fd0lXQ?aSURLX -oQH&tkO6Id9e@+YEDUl^-$DTVoz#hl7zUKKF6Y|O6`=2#i?tb(~##eAz_&IK~>f|+}v%)dzHU@Y@6fw -`E%e9U4_<}xn}%*D16re+z_vw|sF#Wby9ss=J$gPF3SOxsANZmga0E)P%+@g~KdWzV%2*lqR_dzrn$U -S+Sb2Recsp^ivLtRumZ;>dF3Itm;%M~S1%QQ@d^)Hnj2!Ol=;q%+o;;7oC5Idh!_PMfpDS>~*8Ryk{& -fv#Xzs4LPH>q>B?xUyWit^$|MRpKghRk*5LHLgH+ushTp>5g?LxKrF&?p$|)+vYBDm$@t4Rqh&hpeNW -9>WTEkdJ;S-o-9wUr@&+Llz7TK6`m?jjVI6>><#rsdSkr_-V|?^H`iO>wRuatW!?&JmAA&LaA1s=k^~ -xp4WWieL#!dekYdO(6*OQHuXq%k}?Pt?(DYyDDAPF2x<>*103xdUu>V(VgMWap$`Y?qYYTyWCyru68S)Adk -)y;n92IJc*tRPmU+wWAGGvN8;%@dkNy-UzSW8|O{*W_WYF`Cfy!*jwr?_f~qVy)f1-lvrW2gA -6)Dgh6kJGb9=^3^|5;PGgDrOuqD#571FZ>N@VMkW6U=ijK#)MV>#O)K0m01$hk_G^7*h~d -f#<(+3HlWG={Pyrmzh9EC{SdoIQbQn!;4gV!GxsWufWCtm~z$?d7cRmG=Jy-}U?Wm&gAAP)h>@6aWAK -2mt$eR#W>LvixVj0001n0RS5S003}la4%nWWo~3|axY|Qb98KJVlQ_#G%jU$W$e9sd=%x?I6j-*O*S` -Xxh#ZBV1-4|2u9aS;(}(#uFT3tKtNGJz-US- -!1srtY|^#uV6b)vQI|U+P>^`o#B27yi&$yzoaqdO~qN@|d$U_#@}{f8_K|o9X=F6OTSNAu}`06|19di -2UM<%dT#U|E1(@Y#Ao|GmCdm-=-9#?T2{Mh*vn1tASk|0>|N9cW0BurY^oCI$LfB3JmMXcNm&j9<#ADU?|3KeD*g{C@Z-Y=J*kZpn>Gwab&C -P8*@kwX=(TpQ-xwjO8OA1=(2!vR;WH$2&J8*<3&a%#6+1x3P{o#z3YIP#8Q`F5z{&qYv0WnJ)kx$=;5 -w;Xm{IFCuKh9d*>gshdZ4Cxg4)5C(Gj|qgWUtzZJELW~nYZpv{yQRtN8dsAp2qmGFE~n8}iwkZzZiD> -W8@B(~u~=v&8(B&a%El;?AQ76?6-r!9azE -V1pCb&&QVWD#Gi*H-ixhRh&O*y~Di@6n>8aB_)3v6_b40#7F+2u -ZM2xM;{ATM!^;}A?4htgY=_L+v@fXkb{y&`=*bamWe_Gf7}Q>D_;W7E=5vvs!c1-eG-P3|TGIgDE;I^ -=;#?(^@91s!fmpj<*5GIRVcxWNvM~QntQ5xr*2s8}l>!9gy*O`4Mr=9y9h2ru4@>i;x$_#fZ|5*H`#uEJT-ZG9RQUc02u%yLU!)=6jQ^EHJ3<*R -)#7f0F3W76y9M)%lS7LE`W+RA02kGX6%{a@n{X%Ss{ru++|BpQBHWyFv|}T2Z7|3n$lA5efl%sD@Lme -*E?#|rU0(fIsy;Z(jeEhh0OsW(s9L*w2u)6~QMXc~RFC#vCce#eP47A^53iHQY=ym&JOAO{u_pFqfy@ -rdtlMyWeh`YNV+7dKxk!CCnuJ(Knggs=X8WPUIQ3DN6L$EG&==MYt!5YwE4-`GWYPNxZq7M;Z%&8y3U -F_#C&pE_1P8Fp`(zVfYQ^T>V>s48Q#I6Cb<4O0?2jEsnl)PvzwIjP0?;f!QI<;B?wVmSY*)F2POq__rGNQK*L2JlOd$lNC&9#q9b=;~d(WLUjPjxmA-8XJ -@oEf5?E%6X!yqeb*5*TI_a0owC4giXX@%1Sp|;a744ASdiwN~~P=VPvY1)gp(}7NOQQ^^0xb_TWHXN- -yQ!{3If@g~IOc%N#(`m}|XP&7IGu_QIeakbocVj5?_Yk=r$J69{8;~(H{=xwc>~jJzqcYCO5+A~l*Hs -|H!sQA0n=8Q2I1a%334=R1*8aGi;LB$?+}Jc_1r)J_L!_70x%YTO0R+c@%UO$zm*Lpmjx81~fzB=KLT -G7nw`hx_hQYiD;j>(GU>ZZ#2k$aL=2zM|bS^L)-Lcm>>NXha(s&5>$#lb^w$rSyPKA!CWa$?5Vz@P(nebhLI4!JV)ay834mcviP}9{87r}U+V2O_xQ9a7o3Lzn -s?BgrKNv3)+>S&t-ERi)?uzUZsB1$Zqg0bY74Oo1o)O1XS9M`sFbzmYa-+CT%d^Dle%NGxZ0u%ejDQd --HuEyK@QKq*m|bm5ie=Bh>{i}4O=h2zc=NAThKkkc7=LmA#iz+?LlPv634R$%*h>kDt;8D|gNaBEoaf ->+AgPOR1+)ef1aWB?_F9R0=jhfbC}xu2igCW<7}RJnc(|l;vkCVp) -Egk|rDVvZuN9E8PtH0LoV)?hrd^F|^-8;ex8$2wmo!3$O+lNp0i;Ztz{m%~7o!eGYzTi!;;3>4=P -~s{o@l~wE@zZ@uoDnZCfk*?uV;3TW0`2g**qnWFCI;(Au-9)uEdjRDRls%@(R_5B;q$>DVFQ73IP;;? -I1Ru>?bY71(FR)y1U!5jvW(#cFfr?Kb~G2VS3JUS+^`P>=$Qk7|Cr?(8NAL9#K9SU@GI@1B%C~GXydo -kk0Jn6MiV&nG)~}ca&dzkehDjqy2=39iozDJ;rQ}H#HfG52zQ-dZ87}p0&vLT=}9HuyyItv+_gIpmLR -r;!sj6@8!Z8IeLSY;k{Ag3!*gvZ04LfPR>EGMGcjs#!R1Hcz=k -J0}aDxZxkn2fFY*by!*_}nKYwp9k%=%qSXlp)%0#sbw71|!>g%~+hMNV$M081n3)JjzE90K|A55AD?c -c@@X!0NK-VKWA$9Q5{*x4{U;*6N00)!bG(&Nf|L-%BgLK=(TIzb^0g}QGB?L#AUA{PA-b`noO@vh^Xc -uf?9yw7V?(!k>4tqiPXu@02a5&>;Q}-=+O4NfWOD4LRnfdyt4-2{c6gnQ4)zrP6(NiYFVJ|6zxx#3pp -m|Lk-*Y)Dq8TrN6A!!#*(KKKL3Uf)<(8H@NE>Vr@cW;mo4F2&eDtrZrcP%;oIGR+k;d48?vI;s_BSxctZR_zy8@#k09^5-Y9XX!PZO~V!=KCDpoY -+E{>2>bn|29P_8cxfd70ct*^!r_0#M5kn2UijONVKOlDp$2BYjJ5?^9M*C*3q1rJMr_ -$j_PAz`OQ?8#ook!4JlWmnINQ{9+H`iDGzYt6_4WsRto^C!}h}$75RdZIoqF9z67ZR~mpisdXAOhr(a -xW_Z&sOrXX@;Ki+2Mj#y6L!$cx@k?cpCPoWlR8J61d~`{f~K0RpJD -(Ky;WoP!)xUsv2rg#TIRTE$eI0S7X~_TcjThzR`ExknnbB9cX2>Rst{nE*o$!ireQcwR=>89)Gd2@N| -$K;Jp;ePm>lneKGBN56h+6Bb{7ez~>?|1rOkw3`!K<6ByRYJt!V~Tvg9v1o)^Hw~ -rCq>(??AAGC0K$xdIiceafuPu0~c#gq@)LtAH2h|nIdy~P&{zAfNWrjOr@ml#GpeI+Z@Tqn8e=N4

oQke5T~y^Zn)SUt?ok{bXbKyr%iAi6X}`<0WX&Tu@tgSg~7Aq?2!;ynyJe1U+{Vu5 -DPO)#{>^C3cUXtSJr_or)j!yBNwf{+bWD;L)R+p1B@f{c5DPiqHok;ApBLjmjCv?U%)2WG@tngygZztE-(msz2mRZ+IgxtJ|gd-5i#+lEPbz -@XWaz7?M~_3dMV|62qUWJwnAfKF)^Z3_2iet;LE9}?SAgiyT=I||r$80k!61mp|A-8Hb6pN_mm$$ONe -DB%IV-mRVOL9SsgkfQwJLBTuaoG!6?#2qE-IjJ-aeji+JDN!4o$cU&>ii%QHos*blV~0{u0Nf*pkGzUg+kZRo9@QcR#Oijf>2{R28k6Fs(y){jOKk_HN33qhsrTlA{04jvtWvJ}dz_OCT+9ZxI7V -`SyIDj~Iu|AeX|o#$&Yl1}kZ*l(Jk0QNm;>b}vGI@W9e;Lg{9G2t@dt7a7K+sw13^ZjQFYU(>Yc#h4k -%I2QruaJ+wAnKo0I%TGxoU!`@BurB35q}t6OrK0dTQO5J9!sNX+80ejlA6;%v41$nuq3by3+Oi@t6E_ -wwsav{Fv(o$FzTj>L}ofT++u(H=t1o@LI3Oek@}wuo%w2n=&OS>x(8OOY&YK##KoV$#?{)!ElYZz|uw7;vJ6f5;WYiA=aCRIv!F_a6ukuGDkZp*JFd@vD2~aZ$P*V_4OAu#}4CxLY0X_=&KEcnn`#q65orTPM-td1eEczPg(kahMozZU2->y3505WQ0k+QAL}!3u-}3>*PGM_9YCfY^5ttxYDNOx-r1}k+%)RFdw0gA1clBhYQE~LxmlFwO9ioU`xiOZT0q*k-3t9kWPg+lp9s5i-o*$e~%TB?zy>iU3?3MU|bwYqUkf5*z-^&^U;%AUQft)E -=G(lklF4SWsA@ez4AJv*GVL9q=?SyH-|v480%WsrTm_+yyE0DKDIO9X%T;Vob-_($n3@$ -U8F6(3#z+zxlLk?0obdHlrC{0;FRGaW<_i+nIGP8f#g__B0~ZT7Kmq^?t -Xs?4@}*%^OVhd1XNJq=$y$9>|IZ=iaF=qGShcl&@`$C~rAeL#2r=CEoYg@;BFhW}qg`6YL>@ -hdx`kBz8mWpV1Q5O7Xaq@(W(4=}B3w$yb24(6&?QWGW@5sTW@vxN!d0Z*cnf4vr)4?ioR4AtL1^td9T -~^t?D}y(Yl;j*PT~^%NFcPp$t5g5J4JZtY=9j_U|E2`@(@A=u1;{Yte!QLv;f=e-lIK)ZC{<0fS`X5f -t`B;ZmuA#=vso!00`C~f1&DryLL}(PeTsfPNI>X#6hkAv8s%;YI=Y@3Z#n!CDIdv@CkdV2(`_dWp$gO -6&OS`5d627K$?yCks3VW7po7Jc$$MK82M@t1wd#ia3$6`gH4dwB34^u{c2LQ1z?93(4m!=)fyq-EBLO>q9Vs%o959&GrXm%JXFA_rA4v^RMoJ-4@u2e2a<=|~-aF>qyy`dNe$n=NO{Gzvk$m^B)p% -{5`#z;<<_Tb>ekiK*PfHoVE2a)J}GZ_L;*^rK`vZ%#2U^ykW9r;=9Ti9%@qq1(r9yT+pYqJfq2h(zv+ -E`;`6iQE?K-l_1u1R2D=R9uj+-z1RA{l|+)JLuA(NTV8sgoaXnbYaD*~%0L@_2e$SzcN&$=E6zb+X4? -dI}KLaJDr9!lqEMXT>S)T}Ql^kp}@3IyZwn^NzrK7))`WjQjxtIZxS(*oh)`+WeHwU*BVu)XzsvWtNV -j$4}0YI=xnCZDo8EN!oZAg{Lt%hS<+tvbj0li6-hqed(#L!Yni%&`5_2Bs*SHQOkdS{5zPZ2#5k8;ev2Qlkb;HIMW-W_9|0PIBqLnZ?gi3=sHPkEm -o{zDtwaR?Rwnc|yJ+6pz<3NC;V-U6$k(P1!V?hSAMniJ>ycROvfZ4LOI|sx`jGnIDG$66|oBGq*Un+F;)pH>Z$386Jy`iH*?N>bsB!209lO!x4g51U3cYOIyP}BgXPCM)iV%=VhGl@6}EAZcd4{V~4vh+My|lj%z -R4dGE)A6qlx+GbvW}TvG63O-_aV>IJrSA94m?;ihfJx8_t-|M5+GY1@5dC3VsdCJ{%K{jDACBTk7GDu -LXgpj*z*-U*RPP@6J~3^;84O@d~giJ0rjRPCmFNo#OFl&!|H(6Onw_3lDFX7`Mk9+OmNZY_dgPt&rY5 -L2Q~d~#Ay08i|~ySq_b24S;#LR(Bt=8v98^*HCwYM{c7k(fs(8)%o6ktZ9;O4lHFp&DcQdvO5fLqlEvCVL#WqDLk~ig;jnE0>0v|BgQRr* -wgpjMezp!z#o|7s-zXPbhi!wNo14n>(0np@yf!@v&*J2);9>V6wFcSte#=txUycfR9@G$e#r03@=E@N -S3!~LZTQg093B<&!A*L417`69?#uX8JG{exc2gkGiCYJRaNSd{1n5aFU$odz|`U$X-Yqg2D(-f!G4#$ -M!I-d6Dt*32Uo)6ph(Ff>#64a(Gm=nuOO3ICpS1Xzwi$W^}?JbBRK3BUog7UeN0*m-d%=cGV*iNr&u} -Q23fl#VNt3|Kq+ZMo+e8f8wzre{xVgV=wi(P}oZigRz7>la!(M$+I8>9?{$nDw<@Vgcl{}mVM{K~iBO -2l#hIPG^2aEp$}y`;n>kbvc?;-_n4Gf~C+^IAHNwf8g8G26; -he#k>%UnT?`bTa@bgUD2iP}gMLlFk*zP~iD4-cJy`t>M`F62tKun3jg$X8mz=v%9(BoPD&{8Mb23~XqP{f|J6|ozcWbv~n^N -Mo1irCQrJ1(=xn9LSVldJ-3N!Ig%4I%>Ue`3|1Hocsj&qVA2Fucpl%?SBTlR2 -x7%A@D1dWp@{QgFe^L_GgO+-XKm??zRHt|)$7SoZp0VQ{-(O+tEoT4{V&?}|`4*F~VGt)+DI}HXR!@T%XpoDW*CVb7S -;s;aO}?K%x!c9ruv3tlH2ybQX0h_7aiH(JvC`C^8*zj=-*Z4wT*jlk_@nMo;_ln@|l`^VZ+!-*s}%Zn^8YTz}Rqvu$ -#HG2KuBEwc$OGzIy{v~+C#E2w))1@WuPLybRm)jr&ntN*Z)$Wxev -299Qcl}w5CNa-EAZBr@~Rd*&~eG+Tq<~w3V1H__8bQ-B^b+g(%^XVs*A4oa7#ERY6R3u%QVzelCsF5t -?8a0xp3{|@=!RLG^U!j!gV{Pg#d*n}8eEruc5yVIZolON|C1M?QS3zVA@;YpLh}xYjRt^JL>}nF~03Y89t@P9LeSiUSe5_eq-`P*k!B -94ZzUrrE$8u&sPEi-d^cow^-FDZt)nLWu4V+OOvEaU6gwhSiz|NFGRJD3}%Ujqrwowm>Kd4Q+* -o9sb5~Wh01Oi4)e`i*epX@=vb&Pw`(Lr_X&X6fiG$Wn -u|y9%-dke-8q@RSQ`)V|yvo@Up|tXysTPa3=90NT{ELEivZlzbiqitMIQh|MhDVjZZ$Z-b=AAxXE&2y -I4F~U%>qgxyNDYf+wwxznx3Azg^D -B`J6ENeKP_$SKx*?H8R5$BNoM)57XyqHFXN;=@n?PR&j8T6?mVhmvg`3p4kkx|eF7|$>=e+;;_p;xw@ -4Y&hMZ|&#EWUj+XOwQY12{DtoBNjXy2QeL3`)&hp^7p3;Xhyn(*l@+YN2+-dTfE7|N!b!P?6b?^B0D! -kMTX9Byz~x^7UUNM3Cao-mV2DcqU=%szD>EEt{((#xiyDq&|KM_jY&hEfL91i0G=drGIRm)pR;^+h))2`SstR(?^V5-m7E}gfpM- -AhR>J+Z!&hdD4^|qt0(`Q={WczX?kx)oB0a$u*hCk(Sqcp -Ef`2^45n!F2p8;cfdY|;4#?uE;dcsEH%jDiX#fdy1E$>W5ICD%r6*FH7gs0nK!936l1+j$5F -YzFPH?CDke{7bpSGETSIfnX96>b7;fyjE=qDDGQZfMIMZ;14HnGlE1LLVS+0}Edl`Y(EzE|IZMvbIOp -4)&Xh2}cIBwJt+^+Q+Ew{3EfER~%F{a}DHb7WvxhpSK7OsJFx)_m04#G`9ZeT;B{IHkcZ8#{x0p^*MH -rcEW(mJZ@TFxhZ?{gx2RvnzP)%1Si37#K?HpRusH1u$DPY=PIY*d=o`o7k=50~@(#z@(>0L>i*KogoO -}d$KieT3ncZ@Bp4Kos-S5A;%X%#eQ|=1z;8}tWkfmb{uBeH8o-HB5FSXf)qd7*XNKVtd$5{=fH~w$H& -mL_!c*?L4vsaBanwD^p@?^GyS2t7X&?7n+r*6(Jtp}EVSou0G#TT7A2u)zCSiNIXvkV#H;2*K&TE2!T -#Er9-_>)u-sq!_z(^(<8ZHj1?Fn;-*7r$&pbp3y{e6CoE3|)TNkuv?&Cr?-*>=U>?ZetX@4W$zRqHbr -D8uaYcUwgT+bgH_~Siza9f(pzhaCrcFkWAL0w{(nP4ps5cyr;?qs{w<4IiK4KK^Mb8ashUT>}q&C4>j_u!!Ns+TNsup@$8RB1%a -kx|TFTY+In%+S(up`43o>fhgCJIC2C8w#{e|tz8scYQ=0x=uNW_MCa>WI_}Sx&D7fS@F0^E94N@|ZzA -c^{OSc$Fa;Rk^K=?;GMFZXi_F@2fX3~WhGGMBYKO&KtDdtd{nc~n3Xl?ul7e=}j%734I0dcRElB@ALJ{rmX|nw)-b-y9@TKX3* -rvTTz3Kw77fR1e@-AW8Mun|=ozj3cuG7{akETz0z+*8r6Ke^HS!iG7sI#Lvwg&91ktxpymViE(p8b_wnNKRaA0NMLG$s7d(@WgZ!!tgIdZ-wvWPAxl;hP1afS$CsuV3nWeRA1^f8w#ue -AGdHrw?P?OKPNJ+Z@@btfEdA@uDbu}%=`4Of}%gfpKurIO3H?qvX9FMK>FW(@RU?l2rTeS=nwH&{BBe -BMO6QyXa=TRAUi7h8-hQfDeX*c?Wn60+q82biMijiYT2P%%C^P!+MT$rUNBL|SEw0txIRG7ijV~%i`oV -+1w_3+DiLlo^D{t+=mc6hXJxUB!};F%AS3teoXt=%aO`Nx!y3#;So_%r565?MmUiaR67lao5E)9>2)< -v972hoV2#?S-5laCKHRzZydLSXj;OG!eRy40yU`q@!M*q=oXxh-u|&m|@3IB&-Fp|6E9T|q{(mj^f+h -%?(r9|q4M))*_$aXyFlYM32&d0Knbp_g2}()6O)oTKgq>Gmr_U6Ki2zcUXwK*B77XkeBRun6~G9fo=)_P{)mNkhL!C1t}S;RSJs*`> -IRY1aANGLauR}d>|NH!f`ehvib_Xe4f)3NUTJF(j6IJNs2G|_^(lR+j-8@~oKlK)sq=$6V0q$M{TrN2 -Wf@s+MGp`GiXhDV?V?kv6b7|ocYSF)qAX^)-)vWOW5-VBZ5tVAa+n}QKZH&Z8uKowFgX`;db-+Qn!{+ -zAy?x>stduP8U({bSC^=4qbP<;E_*tNYVF}%bNnWX@EP+^S9V}_wW+}k)FJL+d3;gRXUNT5>h(rW+H0 -o7g_`Fh_-9u>x@n4|2f*ZBE0bQ2^9K%&#wKC*dVlP*=+O7h)Ge`7Q0nj4bJ&z^=UE&Uv&{(^$4cU^9z -ra9BNnOJVzl?=U}u^q!P9kmneIRXV#0_3)GB>(~oR{}haKIM5avy#MF7|p15@LqxuWe5+p^7+VCC&?c -JgailGB5-Jn0f%$B=8+3AGwGNeS{>m)a?&N2Y{Sw18eyx22FG*P8jg(+GckGTFu=7_4S}rvbER6mN(2 -p~Uw*Bxr*&UG!;i)XXOTu`_geINnwE;%#Ew4F(mYvwr<>PMX~?2CihIsDvOEjgJJbV^Kiw8!O@`xpuX -0&^ygKP?h!&KgNFA-6B-^y2%Ibfb%vh)XHw+~6z-8yim&&Vh_<<2Ji)=IzpWo_5b4abVKSB;*{#AAS1?XAk+m)Qj8~Ze2#tDAL+P0{Ue|q%Ue^*Q8bsf -xUn{P=-{ck7)pD5dmyJ#>NJ~_)Us6j8gJS$y&nXV6nj~!Q(EJ-9jhwG$Sc -~3UX!|$YUfm4kRU2cPF!jqkPhJ>MhPzwO>C3P5^aiTvcZIW4dNlbqiY~owDjO*{RVB5kbcnY@gv3U)> -?5uX?G#RhPy;C-R0l*B>WDPQg0-k}rj2x!En2Q0UI*9N29p^f6hCJEqbI3Ru!F+g!kscfjjH7(C5Bq7 -=vbSR!)<-_aLO-Rl%sPugu$~}e5K!_K-bVu#;K*#@sZQ;^)&y&ku!|txt^j2n)Qj%XyimD1)_0EN%bw -uaChM)|xTs?|{`nh0;AZJF`Tt1Y32 -Grakax}4&(&ruGq>*~C@uB(;td2>w4IB{K`xK1{Cx|ch0c0rxDWkZL`(ky=SUVqN^;2G#xQeIZ@137% -x$XdjKWj34NyoJ)(J(lvw8$bo{n9HIJS0kBrqdxWUZFZn)gmTD-=V}-Cj9=t#Y1P|KFJZx -gG%Q|%>Es6t+JF9>0q^-tM;OfNr+ps{KoWLe`KI%0BFg~@(9WjKn*YUKv*@>PT7;$3Shh9b?x98M49X -wpBybD<;HYa0$J^YtU~A>G}CL(2X+C7Pkn*5ihu=Kw<%yc*_V%14;-BL&S5w2_ck`|!)SK2W#1Ve-C4 -|Ly-~Plr5_I%ySh$iOta_7attc!!WeS=|Xj#QIOJIS# -Hh9l=s?@-cZW=H!y$dM$gE24z^rrUa>>YV5o20&Pjjx8NV8M^j!eaWw&E_7My@S0Nl^vnWbTc57Q!&# -Svb1w?_?cLS!-etskhgs5i_cG(%i^cK&ZHxEMYpSBV2S5_L%qOpW9Hktyt1C01X0!xXxnJuV+&S0 -jKMfNS{V`7X|QKnz&*kiC6(Bs950l@vuC^5`TIgkuSWzc%kBB1e$Wdg%$s^)l_r^UU7WMUrSdj2wsET<+EY|)8rN|=!^@Q -3h5DI1KtF;%7(nVN{?(ORiv8b;*L~mstsI1XD$`JZ`L)^>j4Z1b?D4uXQ^=m8Zo*dVhB5-Gjn~vg|#h -f9=9!)IZ{~VzO{0^_4Ts(@sk}t&XdVL{cw&6Jbg8|kesmG#Hr(g-N-O50T71)Z3r(3$(1LoawadTkIZ -hvvZ-E5GZ+?HgUY@TZCl+2=%0j!x>J<=h+956Qr+}r%Dh2NN3rKB|CA?Q8tS -^?pDVXW1!VWy@;V3T;TL@$OXgrli}v7Qo!b?f^rMdcT)7qdxWVp9#WcRwn#j1XhQLZ9s`P-vmSol`!b -Dwz>4HZ!bNd?{hw{p0Ec(gIs7lVk>eV0{+#N7078T5+{G~Sb&Wfi>B2@o)4A|E|JPV6*`K-zXf`0NR4 -KU4R!<=lw~+Jb&EX#(*Vzl0G^jPLvs9p2#Dc9ttTD8aB7xZ+|gP8OmK*H$6;J%w5?XBW;v0mz8NO1v% -Wyd)J7jpz{96*qqE+m3=KVH>#Vnn6(K^Gg?9C5j~p_Gre?9iEJ#z2e62Pog$lF&N{87uXl^_l00~C+v -;+Pf%R>tw6imlEWgaCB5iv>G@ZMFJg;p(MMHunX60TtFQ<$x9Z-*^D9M4GdO2j5&CNxu*{>eC(_bG+4(9Vf3Ixnb|B{lDwQG@Zs`WOsD?N>_t! -Y*LV_y564dSQ*0W-mWe|a}vOT07}^M_`@PMBebas}{ghrc6G9v&L}b--UV*si}Avj-6y`okZTfFE82U -fS`83c*vupAP(q2HQHyGF(Dr7PrB>;8ite(B^t(;xj?3xc- -T(%<`wn2`e}pTC*t*=vCJga-JlwaM4>I!hVXzG@H1NRC6IwAB1Ns>JhtIXHT7yrGC#A_dvV&ZU#g~c4 -9eZ_HmFW;ygz#u9sMQlkW%~wuR<>hIoCb_LD;(QyQT>ZN{PaW{+$IR?lq!*{XU2-aOXLctM!@axxuA1 -z0o?&V1olhT*R_tm+q|rn2X{oe!G?`SH#L8^OJG1zC?@A -JCboJ#mZaBz-f=N+q-7*jd@ZH3g*6~lTzCSN!+-@TlK>QMvF2C;5eN%bhW){Qr=XL^#s^A`hr;a<=wj -EX_v&w?SL>o8i#a{Y$Jwi%MZqtxY~2{N*6XA0d@p94fh`M={07fh?@D{FVPCIeJZPI$W4`Q2Zruv$W+2`o<;els$aZ}q;4}L^Fe?3K9euJpvzn3%}w=HoKzl(XMhUwp# -o(_ZGmy3CMl-<#@5Jo!hk+aHktz --ZRrqy*k?kY$Rnlj2;H6~v38sD5M?~SRt$M5+hn2yOoYoj?$&jEmuVvNH!L)$h}5HJyHRiRcz#2s&40k2ROj4 -9yxp(Z|)0oy?{NyV3!W!PqRWGf&QYPgo$C1m!bvUHW8kV}%Jre-}~{nwb|bAqwn8}rq(=HO+qw_{B??fO8NpRR+k87khR{T2CI-)8~#!|77aC%ZK-Yb|1p$OgL -i&`pa0exL7uRKy2(VT;3X)T!isXM#>+ceg=ho(2$+K+Ma2!7%QQb+$l!smW*lEVMFLj{KQy>(}GvR5D -AX{K5YfAQuooKGqA6bX%nw1(=+GLY3s%qS#}%!64*C`0X9O1;C#4AYcvyz)UL1w+B-p*%JA?KqgUzYmImt?@wh)c-+TWl!1V(08Dwts -(N~1)3_hiN3OQ)Q0wNd`dCn*{Kk#Q)zYEKIu16BRAAz*~fZ=E8l ->_ll1A~D+$FT@0~F%4wht0Y!25yC_;9`;tfiuzUZ>5Z^!MU*;2%QD5WEGrU3X}&eUKRKzo`z`FOXiif -ZTM4Q94w-yf>TWgM_q}@LG5FF?-tT0A{#nD_Iyy9Nw;e5)5eX55x4T|RnFsq6Ug#gWzV^#8QS|;o@9)TWM6%`Z*`w@(q229@)P8 -?h-W~CCF)KAuDe$w$z1%qpM^v?G+c6GNW1r9N5RoGPn#cNry3lMN -xw8!9!!+YLfmuyNg$YhL&4dCaMQO=u=#eh!r;zCP8OOB*1u(6DW3WK! -zj`USugUf1aNu2C_V@V)&;tf=M$^#Cngm0?#_R4sa23+<>ixtEib!iuuEkBbz#0zf_XL(|L!>9=W0Hq -E6?g_33cR<(+L|j{Q0vui88xtgOp{4-xbOxrB!B41Q0223j@MUnw&eSWlYALQ6Hs%%y2a)>x3a> -=z5om^vq>K6-$ajr7k*Ck^eAa%0gtULQi*sx>~$H9UXQK3tGx#AA|KwO)}KL2>TNb03s+%JG%|}Q!da -BbAZib_s(8e8b#mx>j~Y!vB<8Ud=xJ)y9JJ!Q4M53bk6A@`uj1X@j9PcECqvJq>~fO@^(yEsG)FSF$m -QoqT76DNH}1@`<5=|J)u%lp -&_NLM)?!-!K{|9 -;Z&nQdNo+499HQ$Y^ugL|JA3ipPHLDVz{w900vxWr%44-)Z^opZeK26cjBWnIaXoS8=^crRlTK-k!9#^+BteZsUj$5 -k8ObsvjU>MY1AAZIbC-OI-KnZ~{eu!xf3S65JXP5t#kbqsJrtMh>f7YWq#hsI7V8{JHS2Z6SO4%KsAa -8T`?M71zoGtlzB^XcvAs%u6vf0-MNv)8V<_e`7LFaEa6d7cbHj_}X2tgek4gtVFY1#1V;3U!4Xnq3Cl -eXXCh6|qxz$JKOn76;A`-QUUo9(9E1g0dL?yjtbkhN0DL2XuhePQDt{wO_COWqX1&-ArkRym|B9M2)r -Y_1O74qV>6HI67Y$5}a0_SUpZ+dwlGyk8KNtX8#ie<$@Al#PD*UPin^&jqG{W~h;0-@~72Y%v2>H -rTFp8!UgSfRs%4vYbU+w_Rw(KPyC$0il>tByh7>4V#R0^^5lzM-c93C@0mZ#wOFj22U5D! -xurm?xWS5DSpS(#R68yFt2cAwZUTXbgA7YRZfmsRb#8diw308gIX1Ke$)y^xaQf20;wHfxeq!&hy#Va -qoXxwVUKZ7#_lleHeC)UvR>_OrB-#I_TP<5PHV(=6o&bwR=A(;K*B1PUQPAv5KL!+5_Op?6fv4z=Cff -E@91|;M((&0LSbtgcWfoVHw -vpjqm)PqSOQSHfJPE;S-H8&>v|L*bFH^(mg})VaajggH5RQ!h2f)kq@ktX8*A;!bS%4Ax0Hl@ZE|>Cn -~z2znMOOCustNNJuTppCCLZ157O|{?W08^1h -%lsn|4RVFrCt|UGAedO9EL5XQ#frDEWH6S5!cFZ89;iG6m4~HwbQz|Uj~r)pNDDR&gU9o?3}Y8CJ$A* -0rXZC@utUyikZE-zZvo{*TX6(|6C(NCN_0mEB7==MG++4pPG7}mPp&Lc>t^81O|H38c*Zw4y$|b;OLP -MWrq7_<-t;d(#$k6PR$Tl2^w^Aw6=g6bc$I=L{Gb!p1B9OxJt>sZgx7%}eOhbgQ^{`yp$ethu -tD{~G+wLO$LcDLCsX(*b5cQo=<@W4(|XVcW(m6NgkRl-fZWwSyk*Qbz$pQ?%h*-f^0XC^9A4ze00Fhg -U8e%9E<8C`jpna5OXfyYF|_I*1PGOfE3pm)5?B$`JVd>fUjY1{C$rg9Z(fdAVMcszv;yRNt8yY|&KX9 -qe~H%<3EnAD-y*+)HS|QKO{k9%r$w!s*@qw3rXiH+}%fBo`$$z*H;A}8d`Uzbw(z -cWg@z#=44ebCL!LV4Se^I8l`eq`$RFLNh+Gt_WToO=DmWzN1@sgF}Ld1#@bX%<#c&-x}ZcxZ~)!Th*v%~;%7nrjGz8tzM9ybjRDtONQj@{ecVOO -*p%V$YY93iQ*ADhb1J>43mxuH27$sEs~5`boP-{ZzO@|j;3bjCo-gPoaVjNYuB{v)t`06o}KAmBi*F&i%sxh09;Y&xBXbu`A`sz&|FbTzt2DXd*gm`lkbFL -gD#T&(P$#1T+>BqfPHnq{LjkYxa2VM0a(?b`u0*dw3A3n2SQyD5gWN+dP!*5F`xtVp_5p{B-S9whSq>wufgE=6Tzo2_(URz*r^7wZ&*IYW9FlEz2xM9driF4Vg(-ZBcY^Y$rZF_ -F!&vS;JjoR;5tK#5ZQ^h;7RI*>w>@u5wfHK)6T>Fi&O-Suvrhp$aJU@&e(+W7+xv25mCV}MoR1~wkAh -fsjFDu^%6&ped+^#4%RO8wm>sz>1xNA2&zo*UZQYD8P)5BdNIC?YvHy^o(AO(H9AJQsWyi%*qGY!{AC -bvzGgNiV?pPXY9-ZcozP-q7qS?B!zonS(z8AN!P#j58H>1SNnWI?zQnkt#+uk+ -{OjD9&1Ci;@gnTS{<}H*Yp^m_@J0Y$DiZnT3(PMN7P~uW(DeTr_bg-$Hi;^tnq|J5!(FSf8oCGRMjq^ -?zI@`!OjCRTT$Z=7~wcJG&%00ab2g`8iM8VPlWi0%#D+X4>&0zh-SPUg_Bs6j@OlKKK=pHw>gdV8_v|EuY^fW1Bj7DhQr4L6mI2f`dDRylrPn1NjL9VCFp?`#R4EhwReHjf-8X -`-K_$^}7Hlu~n1(WFQ;qvfwnIrC%vD+T!lf+J$BWZolH2cbd)A0(;gtbT=InCmycKPk|hc+c3V-dXa+ -xKI}BFa%eR}?O9kKHu2TU+`&ehq)F1gi-(Op$ZWiPe@-m)^RMUzob&%6j^8m9Z1Ar?127>Fbq7x*oVL -r(U1pWjjF9!lodd>8aF9-^yF5?-> -?=QnVlj+Ox1?aj|@+5tOy1)q=Bjfwe~voSUwy|?iv<|B}^2k?buz~#eEyL{M*Tlr(d-Wnfv>j8hV2iho7HzIOjEJxnwABkhw+P9zzssD>AVZUE<0`#?m)2~`bKFtwnf25qW?X`b$2c4xr?lQKlhoD%b>#c$9ESQZHGN -T}!ZUBqJi|p&rS0e(Z>Rk&_!AscNel(htcezzT1jM4szJ>&WBhiV;kB#Om6fJwFw~HXTXx&wSgZP -Bwh639?~1d^@lOdCOTzgX`So2nr`R%gx0W}YYiXsWc#3%rStHL3-+x{hbv=mg(&sQQS#%)96z(17B>d -+NNac-wEY_NbS=hqdh={1B^|#ROLs!+{j?t?>Ia$w3B#t)1)RgRV5|d0i45Ao+Jv5vYrp#H2!Y@Lom5 -7{i%l6JGrwKC@7IP=UYyl+j!g7JX8be^e!(T?FO26Z%v7F77X#*~E3z!Xv8=#339nC49FwlXN54s<@s -Tn~DEPH0n5+$aA7HQ7uQPV1#lOhY&;7lGjrSFO#n?BjTl=3nYF(vHsGIG9P#`HPm!HLESAJQZ|1&|TV;k^xg`n7^;uA0SB?rm!{I!r6k46BA{EVWX$LI_mjb>E -(Ps;OO=o5!$1A0k$ez;HEGZ=SodH&D)#G&nmen)x!FZ#scoof1Z<@x_f>W#xQT77VN{<{zdG!r`^dN+ -qCjlF=sq@a+Dwh)a(Ps52~ehb9NA)1%S-X8ACaV3Ad-fwvs`ICP08GV@}y$uOe_TtaT27AzQ(q_q`x4 -q2cT%KCy5fnFmN~Fv)I+*IN16KaUIu;ezrE4PtL`1mnUxVH@5wgR}8goS*`ujCp%mZHO9wv2?<1>Le@ -$4?@#5>h!9)v_ioIs4P<^%#JAbTOaWB;dP>9+@ApqD(DV&vgD -Yl1v1?IjPT^hulG)Hr!C!)yq_YN#}$niGe*m+(Yp?D=&BZr2D+)(OYHi#%}ZdFiE46aK>~!%x=XT@$> -zA7XV8fq0<*@{NH2)V-jHo$|A&q&|i_=u^NNQj^IPksR8vS8h4SUOlBrhzPD_~` -zHlr`q66xnDJ*SRrer{=$#7IomI86dTwqEi_TEbre7%6gniGW@0ne~)=m8>bk8j*1|6Crp!A0TTgp?N -qE~p46Zff#Vd#qg;p9*fxJ?q6@nx8Obp(N*%Kq^<|#9N+)*t){1Sx?8;81pByeUmF0~N(v5KGICYhs2 -!b8EBYJs58XFdj4O36Y -J5nG4<7f_mZ-0Q?;;9`WmOV1aK!<_P6suo#xgclq|Mu|#qEXxxo`e3dqa5@ZdMSn{UEd%izGVv3yU{u -l^{0m8?MD#avUeOoFrD?OSicifKaN8U%ZSTXMWTKaO#Kvi}J0y1Dn}dWEw?^Z$#jdx -7C-Sx9UlZ&s}ME9V}74Rb1k65rhIOuU^yhAL0aX~$=-y!#w+!QSvxle>jJPDBis;do{tA_WabG6Oewj -w!(zNmK(B$71wZE8vS4C-~wi3u)V@qf6Z3=kc0^)5JRs!74$HPNz`n!uTftjxU4JTF0oRwH2Mo`FEZ- --SmgUzg>X)e=dv*7{(g -D)(|tMU9NG*nhSUi2LPc5LfLmfYyPee;BDe-adUExq1CZBqLEy~cdeI`O?ti_#(fe$^`>sePuOcOzL2 -=YW*qDf7hY|4J#>dnDuJxt@T(d+EP4)0OqawdCnV2_U~?augnE*`bb_Bze^>kX!T4`v7Qx2(hv9K`pR -Y@F;ktb5*Ffx-jm9wu;3umJ;JB|*Q0lb154_fhNmZg_>=A%vV&ivxxzMa{! -Dc+eglIKitLtoU@kN^yRGJEMAp+g6G);i_1vJ9H$vtut;Fhg@QXN;=V2)G9Ugf~d+=v;cjxa~kqdNvD*!3^Iai@%0Jq9@Cl@O)04yYuGmr?OjsGy=@XfiUR1 -2TzsOItx0ltP$>8O8E~0nqpa^S()9)v|bu5x+$9Z{=d&T=N(3bYWKJM -yL|rp!aL8oKIb{lxu5qu&+)t=bbZhwsn#^$fQby{u6gPKmkDo*lJi$OQdSYJDr5fIdW>oJU&Iw@TjFV -V!@6cK-@EkN>@$0sy*<*-?$f*UNRlDRGbTDTmgWt#c$z)79CNk1*7SG2PHT#C4Wpj{^wWcW3>D|o+?A -HpX3sl@O}o9(CeLASgvs-kH^k&Q>J9L`w}B`!r0k{{+7jC% -WF()4CZ=xA!li>s|+tR^)wk?_#&#})gX`pcgmIFg-;_&JDI{|I7~kfF3uYUbCfFt=3ekn4vwYkIq7&x -Kjq?+8jp`WNlPy(Wo18%9@ey(@5xxw%pS{LtH+kTUr5|r4$_ull2E>2Q%s(>&0<8!na4jkHF6=1Di2M -=dC_|I4X8`4bEorDscDtXuCc(OAw?$6_H7p-Wk`()_e1Kw&YDN}lyBe11vUKSMzG%8N9SfPsWAaJ^3$ -v7baLU+pOKN2ru&2B;=14_IbY>y96yHU#kqtAtMm(hCN9_-)tCml|KL6MAC)KmOst}ltKKc9)K}b{Oy -!5tyrFblDF{Zr)ibNn6RzwXO(QI&)XA5UJerI2EMYB_93^XBw?dEkb9lSNq?DW1?~k{xvosj0Xenl?R -1MXS(YKSz?_6$AxW)4d9fh7uXZ$Vo#HAj}AP~4Fg+jY*AY}5Fs@ghJxyqM-|&4zQ5DG -pkn3Gp6)1X-mH3re=kjlUhD;Snl34y9~Yg{Ga@P{p+D*NpccK8wxg@Qc1LWq3=d -@PajXSeb?1kQUmmdEr2(NG=P4v1u?$d4r1=!=5?02-tId|m$$4jVdiy9=6ZvuyAdtfQ28W%06M!Lb#i -5G^~!of)$hr{v!BqP9Jvod2Z6sXh}V_G|E(QF=**9{01AU=zFQ*W!qe{=Q -BFu5u+1FJSG9|x74ebu+r#E5-`MR@Ph8@rdiTKjX9Qqnq1O78jI_NK5gZT*E!z|m83HpHN_Q2btHGq% -K9%~+{N6H=9;uilx?m*!LZ#@KmHOXCGMUmgZs{){$Bq_U%%)1zUe6Cis*6oMQEGS&v_9URSGUcf|?W435A%VxI ->AqElAaDN(hJA$oIkN?OXw2Dg!S@;%#cI9YnVQFcH3sWKU=ensO%Wiu$`GiYkjq&Lg2FYo_Exk^4XFfqt%!#GkGub!4nvG^Wri*d-#?d2%x<9eysiy3bJ@4DiA3d)}ir<$7yKf2mUGyez)GZ-mO>> -U&gRpJy*Ay-yBEPZ4>$I;v$r!OW0`kyvpx)euvMpmE-5=kRLpf5jhD%87X?U<{7|i&ghz$q&gwHM{;| -fpxwOV^cWP&C?SdCq%RT%wdodB_3h%NEfBgBQcRvYelMnlE;l?5HV@%-gfxqqJt(sy8J^epnK&t&P4CP-TNmOQL*kd=>x05T30LNHwl2VBDDc3cLJ5#sM9$U)x -gv#A5O)b8alyK#VD~g(2*i30{#NeEMFyKmnvk-tM-Aihf=GiWKw18V91Fyw(dQ>mXk@H(A|cH3%nZ|8 -=}SztsWFSh2tDSoW^!%ea)qYmERrcaip2Uhk;YAbd>gH6E|655^ErNU!Vnu2W{5S1rCHb1(Od;&V7k~ -uA$Fv3{zQoMvJ87dvq&&8%aaurMNYA>W<-%&EV&uUZ3sy$l6xb$E95Q+Q|rmKIg3jz{wZUPLMdddY2g -BT6-1qW*@ -wb`W6mp|N$WX|wk=%}vTLb9gB}BzHEsESZWukznn4^EJd|u#9SjaT=P>LV%eO&zhVe+UKD(1w!^07~m -o;azOK5iP#Y9#`RHFwYKtzYr)nK@PCJTi9f3{USvX&ca7B(Wh3S -Hwfd~USFPucWQ;nTG2l%X+C+8>;!UeO>zZt>`#cKfu`nk}b3d573u9R2S@%wBjajGeq2EjV_Xqxak^g -?re=qRg^Zd7EPuh%Gey6sv;0r!p!!$KiR+9((r(?v*rYA_^#X7e4jHmcclc7qeQHuPgkhZyErfR!1hR -XZ6_K=uY3+3cC`UW|M->#D`@7%ycY~~>_)C6DJMo1K!^xrQ0bA0}va{qeG|2M+l=<|Pr`)}3!m)$@W> -KHs;*(N4M{So}##XLC1OD)_~!c!f*8G*cXG49>lgfh|KJH>?(zuYGFdKPA-e&o8DN7=;VMlRXL^Q*>G -+Jq9iU{$d#-M{2 -Xx`5g?H$>&}%WkY{i~bq}vXH1>i?`QwAFB#!nY5kq5^XLm-$dDY#^mT=reHwLFy2X)QB2u(yK(wNoc9 -g>xBINh*4sAqwxI*^p>kstM#u3#GSPI3t*_{Gt;uBu{wXdWoT3##W&yJV_K%l2+0Dh6m-YI3VmzeLr6 -GGoR0fK`zd=L6kWd4fsbq_>|Zcq3qS@5p!#OKZmYF=CAHU -U)>C_Z1#3~g=jNPIs?GD2#nVzf3v-=S*O)zTW_apM>Z@rfDO|S%UG~#V!pE2yChS)0Q}@)&)vXBd95Z -|DWu9}GkGn&x9-Y~f6LjtY>am4nW*prPOeYX3PUwZK;%+r2Hj@tIDDvpaS(h&oGEi|bmC -%|Tx{=`dvKSBATTgsA6YnwI+Fo2d8UThALZNphS~nyVIRxZ6bx{~7NDvQvUp>ldqIcw}u+6LIoSI -93pXgl~Y^^@5d9+G5Iwh^{Uer=-Jvn9TkiLLT%GvcL@Rz!HxXH$N`AZQ9trJGC~(JnqG#REwv{M)FM;s?NGQp}BY* -LFaCEfcH%jTk8#QWOCh>ogA_zS)&l0?P+l&^ZceuuA -m82qXS|Gb2N>ujFA{L7LpuvU5QMcU(gU28L*H~5zqS!1k|gdXE8+KWW1B&~gi)R>)gt9i?16=7|e*uA -weD)+R~f27=VeC3`duJ2KHl+kCYExbHYj+RU(={o|v%*Ygc$KZfOg)*hMVRSKqOwt#o5@@$%q@JZ5@yd}j-o0#!pxP{%@k% -*ai>^?nH1tFiYsZt+( -_nRVOGdIM3|e&+)J2S$Q&iiw3V*}3A3KeEiS=lGM^UaSTZ*XGgrcSRG5d5xn7t_Q?~MwFsG1tn=q3KZ -{<;8wvu_XFq0~8WrHx!C3Cqj=aAVY%!|pqSeQwzu`*wn%gCH9%;jV@3-dZMrwH>#G7lBz&15zT^Fw6r -Cd^yOtQY2OWWHD`%7VcHxSSC#i{Ua`xV$G^9B|1JE(e558C)g`m+ekp2L#8_!toKOuP1`z5aC$k^mR#aj1!J!&Q`w5bK ->BBB+b$Fd}eN5r&A2~wa(1%zt?|ee!zxR!g`jz4r>IgkL3#C3{ChuuZvdRzE@ixyn^Zft3R^;WlcDo< -$0C1RN~g_Ca&)mdfjgKY^z!alyj`->S!76{&ZvOayzr8TWGQ7ndRN?a;wMPmn6Cj!aqXpxP4^>>bP1$yb*x{T-PE$)>MR;XZ4s+-OXWocNW -)r1!I(ZJdCkqjCBvE4LDlhyiWT#+J4ggTKqU|%kK-?QSw&sYi_mI6A^7S)LRV)Fkum?cUV3|v|AKXYa -vix`-c{n;UsmfVnr}VYp6vHPH -B(;}Y4rrG(_7U5!}E1sz2W5p$}`*erww;dXrP}c_)p4$r>&Q=;A!hg7QE`;E(?CWkOfaRt@jfJ@62x; -euX5#yO1P!wD5Uqg7U2p13&yT#K7B?15cv;wl$|4#Qh;l?~r>;RWxAwnhf|uLI(T~clp-QNq1=yHJiCa%|<^N@b^MU{^$+$Y_oP;zPJV9tm_UrapGRd3}GA=WCK&O5?&;7LZUqY -n!F=B&NJvFWb#KOfNF7FKRbuZ`E1Dx)(bMr5KIl<}T}!VR(7(M8oqN^tD*)ULmwzYYnWQWO%+Zv=P3h -Iwgn-A;*Ni%1}9pzS()|Ewp;h6B6i^8>B>fvV`AA_YWo9B4K4G@i{Y|NC5>EE%}DKOX-tmWpRL%R!`B -S)oHJhOREpkq}6*1X?2&2ORB%E6O!uPxukleCaF$BW=#n}Byk>+K}SJpw?`8;A7t1?B73L8OS}AbKk5 -^$he*u4>1?17o158mI*=dU%ibWW#gNZcwdpOA3O@G=m-7xr{4?Php1!8kOP>8Zg?xD(rGyYsddf?v5H -HsfAZh4Ofv)KUy8=^dOxIwGVf$39x@PfvF|0Pv9wlXvCVbul_~vs_V1sG>Y!TO -@?yi5GDFoKAlfmQXv$XW`L-Yp5&@V4G+Ps1vmQS0HHwqTy_ttIz#FF4n^U6cyk;FjeVy3zBhnpisyKDdl={8NF!g(MsQE&sdsoLY-F@4Y=?>6Dx{vaaqNZw>kmxpZiEi4`w^VrzmECoGr -5{?Jnj|FtNmm>^_XQ?3(UlnPC{y{xMecM>RMb0Gl18Z3s3hCHYEY|0_b=&UIw8@G(j;kaub!e(I5a)0 -C(^c(K1+gmhLu1ON1rk(p9xX!hkT;k*?ywjeO -h>p)w^Qx8+3WpB<0C-Lgtw>W2S3SmAWd=>iNcO*xuP%^Q&6^3C6ZnD8W|`Su6GhDChgh*5FR`%-V{%V~%%70%~|Xp87*VKpT8~(mTZDIcvrYBhB02N39@KJ6PZzfb1%2# -zBuC{UxO85^x@IrUK>SbTp}u6e#^ZOfYdC(3NN?rEW$ul(O~t}sY_h -|!K7e;s;pCo=@r)q4+dc%*$zDYey(&$_#+4Z=I}yYHM` -vPaOv<5pY%)B5kPlGGEe5)A7u-l7t~rE=_xgj%r0*!t#K-mhwQ-+M3A*!LZ2zEf-}y?VX$_qQD!+J*+ -h`I!C^dH)G3aoH$#aP+fw{>E3zz?Xd9RfJHaGo(2jKGG=B8;HnmCi_I5nYf(l`RH1)V$QHTa~xfkl({ -jujD&E)#wSpy`!gOmOwb?=V+a{3aQy&T`%Uuj`x62t!cQNny}6y+;NxJ*4Tda0+Ou|3PrAjDF_(OMHK ->Jvq9nr;VK8jkqsH8yB#^Bnt-J5JLenbG8Y*e~O^qpo7dS9C!}9?xnk<<4SK0#TsOwonRV8|Y_2 -Hvx%g=`yHr3L?3JILOp9{S=nkrUw&t@*VdaQfax~%wIp{aBopZVv*iwMi{X3&CncsQ@h$rd_ZR2OTgyn*+0S+q1I!7XI2$Nl_NdOZzEb9n}ci5AaZHRi72f{FAA(DS`1;fFv}9J=Ldu3VbuNJ+o{g*niJseW7r;O8Fj -Ltq9u&PnhkC@dGB$UP;bi)B2$J40Z3gFn9R6tAvg%eh*>Y -x=v(Vb6)MrnDY&5zxgyftTJ+);rq-9wbVyuTWcZ{f6Gg{juZiz>1J_WAXGe7OpYi#+Rac=EQuIl14(> -CAhISjTk|jtw5IMeY>1$_f@kjwoSU3D!&(~Ao4x@*s0<8M$Fxk*ZaELwQMN{sUIG%E3!nma8w#qgGNH -g|Y9`JL)&&$)cjDGH=PywX_jH^?+vcCnA!$mX9NI2*oWpB$SLQI4au}_X`Eu~D;*s*3oMY6H)*K~Xo} -&oVHl23#kzRSe(7NWrR;26P=ij2KH9HdO`Spy{j%OhSPaRkN{M&@xqmGb5uzHPR(YOl}hzl)zHAn6Lt -YC&{ig|BXYc@qvXp3h*7&P=Zf}7ym$wt+>26fxPyk+pjv--5DElh3-x0$E0*OoFZ19I8ejN*G8k{;xkhKKecv$cpb3G}xhvPjja4GQS* -wWxjzwR(>Uuie5wA|wmtAG`7)Zyy&guycH{kv;duj=q>Tz47@# -igH%!Pdm_5~Wa>`V3UbiaAl={_*1X6Q=3{<^m_*E@w6bcm^d)ncX*9M4)n5uWN=B?p6=u(@VgUl(5bS -|TZI}j0u$}#-hzV8mQCR#%)PcGGNGPr_Ln$-vJo$9?}<|t?|)zBI6{qo>bBPYXS4N_xF1B8%ZC|%F+I -KQLmKvOve(aY|TRRU&eRzw7?9N6Y(j+_J~qr;`@>28$TW2 -=qS>tqp*r`m6s#W@vAlMyYJs}DxwK!1XCGIS2$wT%QUQk -nEjX`+Z?eW$sA+~8kB&~$oy+v}YUalzzhWYlBrH^#HFfpJ3>Bm&?QFVUXQ=u!apw*&W1Pq`o#olc9r{ -^orfAH$-Y66Ym}?g6_#sknpqeh3w9q9qX693PH73v~P@_e1`}ciOA*_@m3N&FKysXuuvTB0=w1KfH2_ -SUGA3VcfP#*CNx1RH2T7_Z^%0NliDHE~+8$J8$8iUP&ja;(SOL`OL5-w!?7Q}Dr%T>vLYN(=nB-H&)U -UWm^&VtN(Wg`jC^TUv&{luC__uD2TRz`JDLq+gFn2eZzHjtPFXG~pb&wXh$kxAN@78+KO6M<$?JC|ed -zFqfOFdbX8<{grPR8oQ}!xL1y4>J}WM&)@wP&paQm&~5eDDcj3Vvwnn_pNOP7e1T`)`aG0ROWM=<)bTx#d35WA=X0~?W4b1=vz1O^*DFyQ`8FA?zB -rF^s%qsGf`^oE?*?n%a~V;Ry=X|{M;-Th-oheY9%z2G4=P&sgkxNSv&MSKV6EdqC};=fx*ySKCcnqi{ -a^n3Xru_>jORj@=PS<#;!cjjlGgptVyr~yqU9`4KN|_Cl1i8t#`DiP2bD)NX_BCDnGQP9nw8opw1yXq -T^wNbj4msoh1w4$!>0Y#)X`4Z+$E$H#}wM&Q?T>1(W^%2zi -2g@j8q#90%*r0)eIq((UUpz8@rq_E1UGuG-vWV*~+J^Fkx4e@8S&-uac!j4`pgK(IYDNj?Q3620&O2| -`GpO-IJ#%$w?EFFrySmOaNZr>H!N&agl3$W2$CLdAK18S^#<<6Hl(vo5!dx08{ImKv)AjECtpC^@E5$ -Glv=s^)^3Ryj0kO0XBtEZDK;k4CrDdX@DbODmXmUo7NT3#mB+ej~50~NqKf5q^20EzdYS?0vC$%_q@4 --@|4v!1&kX|G=dh^kzme9ig5{A{lb04AK#Y%P^iJNAPUBV2NYk2{j!xZB?raYcS{hjJ6$QoPp -9dq?YkNzD{>D2xF%uh89|LQ*O=~mF`Xi7^+LrsIB;$F`5NY@hDHsJaGj`EFM+~IYCl6kF^aG|lR>iqA -Rt$Mh3zJ-?8^!2eyT^?}mqngP9srwBZT$f0awYD{ttkhaV1$}Q2^@jAAhy)CkR@#ToJCs!&Uk~=x>wy -*wZz>`!^_XG1+{C=$VBX7oaaR-3$@8|7`J$+Gpl}xXcZR=>y0b&pnr@wxc{fWRXs?-28v)!zxzS^_TW1^o -kq$+`Ppf#(a&RDljV6$jk!9ON!c-X$=Kj -`5KgH4I^*DtGQnE$edE%QFh;=UE1eUljggNM0xBM-o>gho4Rv3XdCR+f!?>24VuRt!Xue`kW! -!*+vbTh)gV_S%jD`!q8yVOt>?bUNUbe3C>FoOz2h?se>+CYE4}j6e%W=sNfq)P{yl{2EV_#BD~h%ze1 -zH-LK)tpjMzZF``Wojr30N|lGC4$Qb}LZWS!?LrSO#Kf@$@f4YvTgjZ~R&ie{5=8gE6Xo^XujNO#roU -Fc`8qFVrHlcQ!1T64X{vtNyQhwGQ5j@&&$xuwu|8RCLM(zDT_N-Uw>_61s;Akh!`o+lQ3doofnT)$;8 -*!IIY{Zp~FkFahQl&p&LzNZW`vKIG@P9=; -|5ynbZSfvDbZt)+gLAOFgb$GLRIFQa -mw)U;uL&2?jB@bz*szPG63B>}wOt^I6Io*jb9so7BdAEq4#_#=@r4ul7c1{t@J_ODVThz2^?$Cs@1&) -4AhuwF6l#W5GlO6{udWZK%n1SC%en{r&|bze>v+<_Ug&8Tg~~-;dH4kU&0ti4!{xIb4O&*J8WDEw&q| -DFpP32G`c}c7DJWSP`m9)x1k%6#b&sW5AmV_$({m>Y0_s?>m2HlGu4L(S_%RD*8lX*ltPLsWN`I&eBX -8A-j;!S0wK`sGWVb4*kK?1zLxzJOLx!QSNDbOSx|x&*6KXz?4K6j$f^~5u;{7e-IQ{K%ab9wiqhyZT1 -(iI#<_q;(H`aoqiiynBC%ygzT3=cAE@@xooW;t4;bb8J4$6Z@UL& -KZD;Uy?d>@htizyf19+}v7@7ZXLr+m(ns=r_enR7QqS&|_et-LGF0+vM%%L|_!kU|J*#pZPo6H7ewmI>7`E^Ab=>}ImCJR231#lCEXDX3CQ<^gRf -jQ4`D~(ZIyBr`Gh4@%YSt?=XL2S5TRr=|u~+EA?7vVbq`mTfb0lUJjex7b7W!w9?I(xQyy5`u1gDwB_ -nn>5G_Mc-dLeJAR!ui<gGM`gx=n -9?ZCTPpePG->FkTS0Se7x97JBlO$s2C|NRgl7DRYBq<}?;-xq{>Bypzkl$`3@c6JtujFoRhajOD?d!a -DyugNQW>RC@KxZ(n$jQna=F+1ao^LPiPCoYC@8@+nhq#csaz}enm9gX7#A=7Xx={z#qy#foYuHrh!C( -97$F)luPnxyoMa4$ChOT5qp3O*G6rLC>zqqg4tID|>ucWdKSGO!|r=%)YCNbA23_*^HmC1~>$b`wwkF -1*D&u{l>k)b^hp+zQ4QBSv1u41PsrEMCmUcDD~$bG(t@=Z9xPYK6JI8(yK60VT&*Ao6o!W|MeNT^5{HBdmKgeekQC7dl`zJ$dRu99$*gbzvhq=dU -AY>@Cn2|t(cTM5}9SzZ$MmvFd*X%gNbVUC2iOSnnGKS@|C;c*E+myk*R_mr@|gh>*PlQ2udUr1Oi;aU -kllu&z$WckdNux!BP{vAE1uOmfnjK$joROSizezkyYpJy9|`@z)q{o8xogMXM$kcLYe-i9tZ)^I`irI -$D@WreP6XGxyDw3M+Wtd!-k66R!f{0(RMtccN7LrG>GPp?73Jp3Uzo5g0bNo*KPk!fl65o`?0Viq3_v -)BzRgQc^n%*S;*Zib^6-%?jKi70GL_B1pMMz5m%%*)m?wAOi`5Kb+n9-mYA?)m -(3!+W;g4K~A&d!Bxe#&*a{wCgmf+Wo+!CEOo?;eE#HD=hR={yC;b9jeN2M_3;)gJvTe!V=+H1Z-Dx?o -mz1^U>tb&)jx7Rm^Fm8kEON$H{4NUKZ2LX|f@1Ab{L>rp6ZPxLa<}{OJso&v%JOmgd<3UUIQOWn{yXYNTfQav@}beB@vprc7j2pxYqSGIzhVR;cvOJgZoc?&bYcIVq|3rFJYxDof^hW#AbMO))yh{<@iCAuqqrH}-war$ -H8ouOzg8l^@tDTi`$`fVH5;iJ1trV}Me*_!*qvc(K)a;UC(Y{NhKlO@Bd9AcxQJ4JjUtXVxD#Dn@Da` -k)I~|P^ZSh_E6Zn?MUa*k2L?_3d@9QIO?Jyk;|36cIE>9_+^FOyuE;RhD?UZ<3!t2Qp-rp|fV-xiRb| -240E$O(uCi#`$&!?9{h&oDTx`f+Z?b4g%SAIVqe=dF(A%upxw4D;w@VEB&1-y(zYZD`u1NK6W!N^B92 -X}FDyGD(OlYa$|0whekDCT2&X>a^q*h8hgT-vpmNzxuA?U;{od$hDWq`ix@8>PLgwAY>&_E>4pmi8VR -Vbb1HS~s=`dz^+(+Iwl~pA+^z(!NgG`)cW(6?UVRp0xLq_MC5py-eeWv@h4vllI#+`lS7$w6=UB?5ea -MlXe?#`%XSDq4@@t#cV#tX}w-LOTeEIC4KpfG7F}Kh)K0XP!Z-=Zo~N>=KCmX!oG-yTq5Ti0t4Z@mDh -k}_`ROTYnzJ2*UI~G{MnabyUxciEB>y73%Ql@nX`EE<=;>~XA*x#pUZOj$GCeUcTbcqC<8v5XP8mgeE -7_~mlE?%>VdT922i!M9rvkn4yC!y0CdRtiGN5FZ;5U`hS8JB`@iKrJejheWWMkYKHDvlb{j8;JhWx;O -hVX&d=A%IJ^~NTYFb@Iel+?udNn#Vx-@z;Iz)QQeROE)Yw2p~5T0B8fV;-g+!uUoR@x>8?-%EvR*Xv1S9!*V==|F*> -Z{6t~NMs+dCDr2_i*_SM|`zpi@B}*@}Pt9|ckadRL;nF-z?nUlW5S!^P;`lP%#k}B64rd{ifi2gW=PP -IXLZ>^|>0?-CZZWqaT}q4a6K=vu_y`x`FDG1thvHKlHY3;NcIM}DJd>P-(geONb~{UbHt@@yUu^etnC -TWaTS+l@oZ*%**F|_JKEGsgwOhT42)4jZ1BG-BkabUXOPe?OOOgCF5O}1uaPRX*}Fg1JH^cgc}&7L#&#+&BNzxfvn -a&i~u<=YDuEiSy})}kfFC63#ir7ri4cr}9sK-SFpc^Y{DNS(f ->}-2QX(+nVpTF8p#i-&4M3ZAE3(oxfaHz238-=B~Rp-gEDLn|}4{-)#Qv@9y97z=OYk=;25H@aSWYKe -6?XfBN&2fBEa*wmtRqGtWNv{PrC?UwHAQzrVbzwr=;Hy|3)sUw`1$*ABk^#-WD8N8UX8*4ytKd-wQz? -|<;&M~xq!_~hiNPd`(dKKGtJ^Tn6VUw!?}x8I#T*K+>C_ZNS-r2dx&gpND>pEV%-Pv`%CI{kn9fZVbF -{|f%o_sVd;_Rt&lAV2%B+t}%fP`#i1t~U0&+t@d@vES3ies3H5eQoS?g{Z&%xi)tCj81FfTD;U04&9{6YFmby%NuEM4E>=K9D;d2*m0v)pfbM7 -Mh#F8=*a~5XK7OdiE2Y5JE7g_kDX9!<)&@RZ;!d~nyDuS^S{p)C*9@vR>3Wy4_SS-Qxj84M(ivk)`vg -d~A*|kYYbFxD)Tp4+G^*M`k@L)mgD8r*i2o}QhdOd9s=vj!ylTPL3cb2ayM*@L&mfyLyyjyTEQmCq`L -O~}bm1mc)L+BxcQ?lojBR&$z&L-`K#v$eF$`OkpnZ=72>-8aJ4#zTw;|l!r#pMRP^j#(%b~l(hYUJ?z -BDpYY<r`~v+E?38&VrBFgIp}vj+1g-xkW~c(^=v)78V<`a^1ywi|rI|p}?1 -0SY*#Px=M^C4tufDUS`j8yK)y6X)DQD#bhD>0u-n*x7e8LK)`&&p;ZP7dU%^~PJ3>?vA|if1Th4f0sU -yLYyt2a~CEv3+4K9E%Ez?bjIx7L|60 -XW8OHm%Y@Hn`bxX7P#zADZl13X5}j;iAY -8W7uu(5|`bGvTuW?J)*~fx0l!b1$?jH$*yPfS?e>?FBySTtlnX-9Jng>9%$c&pzNAEMZSZdc=g&qVEuW7ukp{lf`K5(R97Xm8MY7+VpI=z&n!jvJ@^DA~LiRHr -!X4dJ&${MEu&&ABtm||`Lu75Z<7A51yPnvGF+1MoMiKrNm{QT$USX_ -PzJc{h?#y~QW1FvP?AFkwHj3k<^tz(I?bn~Nu#Y8<9$~D9E0Xm94LxRtvmVK?J7a2l42vAycdvoPxjM -7%`H`S6f*D4IIXg85H3VRAjm^UtNb$phSQz5K$Ds??v2f&E*DZ{7OE$1>Bcqx-H%2ss)#@D~Ii0ejDX -qOo!v&ZhinR2s$Ml#6jkfXxQq+KcP0|+4^A*DLrG@zcLrO>@V^)p+*f16g+G9a`Ea-@xF7m4j(y<`W6 -5Xv6>z1zvJt0Jo56@plGWOEQRy;pO8Fsq4l%VSc2A``Z}wL)S94@?!yY5dkbh7tM6W2xiQWX2zLVccN^K&R)$u8)F-;s_o{`^@J`-h -*a_Tj`wcMi(s29kO0r@dZZ>8t!35<2YeBypdPmkfDtatui);oC*>#cj4b*_zYgv)Yn43>Ee31A_*`+0 -od8>442F1-06EC%_H85vz0eWjbPj8i8uwjA&C8Xu^fCbs7PJp6u`B+D8wx_cPvAL?LoBuqMWVmlklYcL|Y(S%1w-In%9a -eTJ}Sl|BMBU72>Ut{<*y0|d(DN)EP`WDpVxO;gn`E;GP*)D+H^(gaD)_o=*^3i={SBK -VL>UxE;UXR2ycWsPrFw{mmB67l$!Vo)}@S-1xLq8ffvU{^A4TCTL4J9a7VA-nC)GM6za>b&}bVr-Hiu -KZUVImH2QT+uT)LU8G!g4e`hh11Fxm)q{ND60tlOJb&bz9iwy`3nRh9Ir3pnuJm{p)nlFY5}`wa?e0& -#OQmBXK?j`()U^(rE35{tIoO+ssJxV-c*|z3AVlUC#_dyAI{;TJ+<tJ+en*_lB5So<8zp0B)W>uXB1{=L9XF0sU@|nIWL -16Sa?8(O2UIu2_VP9r@#Z)K#0%jyARSQM&jr7N6XM#edziIkxethHkZ89ML&Z*}UFQ*Xq3(i!>ZhZ9( -6FH$vu9t4A8_(FbYX8{Qb&5KK@8J46A3m`rw>Dr-J7Yn|Dud(23Ecgl@I -z%1JM;*MW-8xtw@Z)tl19g67tzVrU6V3)E-xfGfR}k19K4aXEY*)P>zJ7YvWu^grKxfv)$w!tP -9}lSA%6Uc{<{5FQ-{=!WnL07I;q?+P^4kshr7>L8{gBo<5AUx~-!SI%$oJK^Mva~?4l!nE!2078|Gv9 -3!eZWo^f2FP^ly)mhZ#%8>yY7}rMO2J+o;9wpiO;(FoWOpYg1I;Q714bY3|Y()gW>nmVH^o2t}N}5v;E(n)S_(LS605` -nJwZvwK_{$p*PDvO)RZvq8xh*dX1vtkqZG*cc+m2aKUR`m}C4Tj?xsGh;dZTHV&cZR^*p!{IG5v3(q~+v&toT}w?LJSVr%HL1ioqp)~UkzHb1f{?{_Ct|0U6qlA1+0$LlA`6!Gm^EF+Or@ -oTi;5+NnI)|RuzcMlPL|x+M4F#cnv}itl6<@G1boi`xY|nc-9`3Eg+=xoa+grpCjzI@&S1NFD-5)`i( -G{hm%HpUOXd{j+tU~4I@zye9)01k61tUe&v4~i!9UuY)Cb6YY`#H?(hHFL;imk2C#UB6z^szoe91RpN -F@&YWn5r#veV2gw-D_T`E-_ER8p$hrdX!mV9C;Uem#uJYr1_&$x@!l84i2i9A?qYa5?dxf&5;_L!hV^ -@?vH)tXXqR*%>@~pH>bD?C^h$PIM3(OA3o!py!9Uy$#2oP2pKFnN8^!i?62`Q!O?!N1&`gWnn&VqiDA*F)%CLhGcjhLeBOCP6u6_0JehY7j;swi(Me -B)#qNpYSa3`zfvu*@ -vXyH!%U|H>W^%r56`VR@;`zJxx+m`ZtGU}CjFGm7kX2fJJLGvqOcy)ViDwFqOHXj!7K056~Rvx9tu*h -KvB>yK_m$+}Ay6RL^sf&vs8olr#MbmThiK91WdmLF$k=Q;~%qjU~C&QqUm7nLJdgv*~=AWCOiK1`Y`PDG&JB=^I-MKPOBgC)nDh(x^SesMC1{X%qof<1v+p8ba_=YIM@zd -&+S8>yTgI6#-DgNRN8-Oxrd=f6moPfH#`qa!I%DsYw04r=%O%ZgBwQ=~E2Mv=#6$Y?=tRPU(*Cfd=TV -v76ViRVr0)&s|Bm!0y~PoJJoD#!+j97^=U*NEAC3R-#{ZAT|Nr$NRc8M7?1zP%#=i>wd!MKdAK`E8@c -Fls_TTb!e7I2fcYOHmf0pil<@5i09cc6XAFmS4*UI^RKoB3^9>oMAXYKN}{iOfn9|`#IV*;-EeIpk+O -IuaW;Z}sN`@``+7(2kf=8}ME5%MoFk=+PYcNsfzLUn1 -uBb?vPN!zg5~dOSn!#hlDv2S|v=9FkZr566z&vyd=|;@V{%`6+iy+eS`mP{y)Ww#)%!o4-)Ph^|HLfW -WA8-HBXT3w$qQN|9|EADS4EDHcy~@d0u_?iw{7o%2>pkx-mAikB*rEhs9v -LfO`_){diMg-a>A89SB44i|#-Tb2H$@9_X(ThG1E5^j&Z#xVE)VET1kL2@iqfz4qzGH7RoQ+8oU=_-UxUPUUn6D1sH2V`NB-__ZGq19e -@u`f*;%;0z5fM);++G$*9|K*8>imENGYuxIP1M;r=M#I~jtWlYpU_vfKgd@NPnwy@1v!0w=+uDIz?<- -)Et(qI3cK*@SyM;38>W4CuT8c|jN#pyvjGe*@s&8w5^*7p0lt$f?3y3#iLRKhPb#0<4+NSQgA10pFZ1 -@EiraW`-~i1>847#H|OMIaBb5pm{dJATGg6csJ7xv~w`7;+>B$1cR{zdI|1&z)vx?KL)b`7&TwuPrI4 -1oxc$E_9eis3uIpbSdCYYwy*&(HwW`InDcW;K3=quEWk|*8QXw*Lh$K@qMkep*ga3+p))I^^F&%HfWN -}K8EG{D_Q}Uw5qOM%f3c&T!dwgZrvikB`ANV_i)8<~n6UvRjJe=W_mNI;AYGW#01F%f|6;%|9U^=);O -0i|IO~^!Z*u`Z_@yY*M!+rWL|GGDP>ptqFgbv~sTOH%27I?#;CvtO -TWKb^*CX%{9KS)}Ndt7$h_*~{!rh|ZO9LFUk;WC23*gXuM4wFXBfJsFy8>8nFWMl?ivhjTd>Zh!`-Is -Ac=0~LD}s;Hy=*Yk{cPD=kT%Q&`#*qs33EK)><2_WsRZ2nfS|t-u-n6!%Oeay+rz@$0T}xT#znXjG(L -(rFcW<2QNfd~fXf~e<}$z?kI8-vaP3y)7vU=bd;Ssi8Rgpx@b*6eFU%_dFW^ms`6A%we+Atzp9Y-!H! -*e)T)0iR=L5d7O{CQbxbZ2B9dLgLaM9B!cbLloM?V7^U`_#C^^6>60S`YT>ex}h!e<5FiU3o0p+3QV9 -N^(yqU??WZm-1{3HKd9_{+jv(z-}Lkx{pHl|-4Bo9UKMVMFqrgdWBqRs25q%@U;nGZy-ICK!1_nC -T4tFli>3F3ofn-YLxlYowXrR%s?!C(Q(#q?yi2(-~hf6Qr}AL?=OqbSLPMW;)CHXK5z5SDOFx{Ns<1X -%6Jy2QfV@@67nOwQ~AKxK4ad^9vWk#GVQ(r*+p3K#GTERZjj2AQv}5w?T^*3|Hm!?J!B#7LSfgK3dyx -7kfxoy3pF1X+=2Vn4%~Ni;9lQ>`>_t(m3G`|pQ{D#`g<`(fq5sTFnRMP%vW`-_Sj#eo#<@hgWn>TM}PdxDi+qrWmtF5hN@4WL4yL9Oilb^QAd0<$Y$ri!xK3E>X4EvWYJH -)Mr)GsfVM_gRCY{d!+@TGe2;vx0mIe71fT@TBbhzt9&Wn@H4ME6KEm -(4^<c+tG38(^n{oX*lx^gBunhsu_4C -fzjLI>|^ylOeq;SIzag4ckzuS)eiO4jwCs^`hc$;@W6u^BUFu-k9H{jxf~apOkzr$7CPJ^SplypF&9_ -S@{#sZ+js?x5oVm}5D%Z4aT6xWjRW`h3AIwtCl>y9#bCVXIL@1-n#r^)BxM)P!4CfAQUd=V4g=;Hi14 -`hCI51E*%qnzgGSEBViW3vSK4ZK|q1w?JK0J$To=Sqs=|^$)wAn??9`-bMIVtLm9^=e99ki|@d^t#4m -7Dx|N<0_DqoEL4rB=l2`nioiH#pWo|#N0mXVRcc{gp^G&XzoYVR5t&0m8Bn5*_7idoA!~)O7YG*p|W|Os -VuKiWeZQMY|WZAth&0I-E+@9>^Hyp4g2kHf6KOP*}@)r=ppvlV~_Fj-L`ETd+xdCcwhF?OE0lE_iSTp -zf#$A%_@84l~>q-0|(gaufNU?A3n^E9zDvAA3x4M_}~Nf<*8%r&97DV@y8$YdePL>#J>IVbN0!3l{Gg -vvvcRp@w!4!KqAHmtfxtaRFCyc80t+p8hd9nV58cFjaNsodFnK_T)mBLR_|fYsZX(^>S3)vW!(_JKjJ -4L{&>XCLi`&Me-YxBsnKjb;%`CxzaoAu;vYf$lYa3BA$}U---P%s#J>yi|A_dz5&tmaA4B{P5&sj!Z$ -kVp5&x`T{KP?6UzkwWu{PgGYt_3LJA`#g^By$9BhgGfHiD@iOk?WF+nDIeDpufn8c{YE=;{1@#iD{?TG&y -#D5;~kNU-Domn>PjyBtmZDa{(wqw{K)cj_a!PMxRm^#A2)MC ->n4`N%5!WZ&H5iL&e2x`Z6RuC=gjzgxwwt;d`K{hak3mCZgO<_u+kzy9^F+5Pw5&mMT-0rv325 -3@%feU#4?o_XdOJ|BGHg%|i-;jP*y*!`GqY{Oh(@7}%ajW^!lbAyv7K4K?MoM0zUo@8FHmz_R+ntlD% -mwayU-FM%yAAb0O-E&T5Pr-ipd%u1*u^$FH`oMyAxiJp|-Es_ccVnP?oJFh8vJvXbY?@lnZd2c6_o(l -)r_@szznlHyccx8i#K&N}aVX-CLHsnt&qVxrh<_X6{}S;ZK>Vi>{{Z5*j&uK+r~GH0@_%)nqUmYx*|T -Rqf<3q*_nxr>6B85Zj>w+Io;_oG_PeHUROilpP|tewA2M*@;DL$JQHHA{Vei>*z~I3M8y(d*F(EO5_3 -D4kki_0$iO~qqIWn?OuYLoEB!-6#=_4JC#v#`v>h%#r5a;SHog;h3cE2VuT(1uc4GxaD+FO-9FfOncd=aHL>_|VMSGU0f5)u;=27`X2e@#L{ -d_qFM1fIWMc<4-Jzkb}xRvQ=_N1Jeg^6hAwROc6l!qdV2zG8ofWc_n^5aW;bvp?g}L=Zapy}>fo|BmO -M*(4@<>BxUf-@bi`sq~V{zd@m)q3BuoRg|R8pRNEvPr=0oG>-qPoDgwSWJ9k4~pc7&mTQqRdmQ#6uz!#D{-`e^k~qFn -1X=XwU#a;#KFL88>#`ym=GG;DI^rop;{38|!Fw*REaaXP8ceufD48+__T)ed>-KJ2>5^PMuOe{q$3{p`k%NeE2XAM{zJmAor7q^ZTcsdTQn -N?b}yk%4VsksF><-ILvq5byqg#)kGVKvm|uy-hEVcb@fdnM~)=k#hT6LA%yoIrT?36zELrkxo5zD0i( -ggyFUN?bG5d%*2m{VhYqRlzWXkx6XnBcCv#(Cqx#-^?{PcjNqaHp;ShE1+poU*ss;7#d*D90X4c-edC^|b?0Zq>z -SrNK-hYPvJ$v?SKsokDov5Td?B2bb(@*u|`0?Ys3_kw&V{RuJsZF2_bNJ?)Z}OMg29*avDmQ976z8?q -UgPvr8KV9FzxK{GI;twmjOPJfP_{Up#A_+K0l|gpXYtm;oC2V{xfIJh`J#o|B;cAHzLPO=s$4afNb5mRg~wVF|*NO_8)%i-@o5XXy3PQpV5`7sw -(ps4A=nV4LyNJ(6;G&k)%&VV%`zCW4}oIy&|{n7HPLja -^d`~1|M@?&@QS|@(^Uug~L5B_n0$#M*!4U;U9VgLz`*oPC8C$Q80-aiHVK8FG(a*duzu=>Mh2{f -eQF%KzOT1u%506zO~*fFVM4;eTEgX|wb4=~r7k|ArfG=%{=sM9;7-p>QDUb;%d|&-?fV&cn{QNBmc+q -hR=4q_<+YOEFX&m&S}AbS|lj{$GCiMbyvKnqaWg;t0i)P&m$=J16hGyj)gI?juXa$H;TzVhs_4{#WGT -zll6}MC3j&eC+76%1o5i=lubF#wOjkBcRW1w*_=sEpDj)<7?2O*|l(lteX@it0#7o62-7wF)$L)JzVQik-S=LYP -BEHAE%Gw;okljsApFAcX{Tje9L@_+_jbd==b00^a;|>OFQsjYvf3`O1CdF|5){u6|(68}*LC20A!!<_ -9hkooLM`Q*J#5@)YuicqE=GB)2T&Q#?|q}699#<* -!mX!@}iR{pzm>2f2yuoHcSZfJvciv`<{y(h21ZsM!(ZHyh$BjshqurarbR7~zFWr`tKpPyBmG(R;?3d -VMqS;;+Q#;6`Lbwp2@lQl>Ve^hBOxMPfVPK;3({c7)x{3|_$&<$N2p$WRt2k5pu!N$AeWBe}|k^k3bM -ay=@uthO!n4%a|7tm)glsFg`I~eAHVN7?KIa)D{>?ye;?lKq{*rYD57^CCf(DIMXz=sh>FxRYEBkR_! -6XGH}-8`vJPk#IDH>rF+QFhO`Q?^Z045|w+DF(lz&)B5mtY}%D5iN_-V`M>EtjtY`lR1jv3B@p7F-%b -mjNlmKF9$>0ALv(EF!HZ*oe!2Lo_Ipa%E}C$#^A76IJf$}m*my_J7t$**s2(eJ}U-nlGW!TwMok|Vq{ -@@tURL_3KheXis7#+N7Ise8bYy2ajqESrSC%9f9SXJ-?eMk8=(REX3d&agCQ74eOP|`?Yw-sdx`8V=p -j32M9Nl2pVzzed8LD4se|EZm7~J3aWXqOfPrCR4E4`_buRzL_}{lYmySQebM?RTAAIn^K51!bZ$c~jh -z{iD<{Auc`D%zK9MP%Z94plLXpgTZ#z3E+a`ZW9)EoamKfLeNs~3EbwQJX!*o#`YzbkL=UDdGmIw?Ub8tx=Dr%86vAztrGllW@e^Lm@q*K3JQ!~fByOB#_kXcg~9^ -e_y%khu@Lbn`2Yibes^`2oc~qx4R3ahKHC^$v7^uPR2Leef7`Zg*J-TtXG?$Ewrx9&962&=#E22!sU0 -MJw*=jg{-k>PEUEi6#CD|n=7*~#u -(Xw7z2IACN0Ui5Aqw#zoMd|X5V+;ebbO1mG$|W_jla5abub4u;_b|lanK=6TjA57{HnA+CcL=mR=!^jZ1f*5|cB`uz6h6~uu{8zVfOJ9l2Nc=2LW2VreTmnAPRPvYa_<@)Qd -mz!_CSv0mbv_J>C;htcyGZ+gKnX=c$wjNkMOt$?sR{e7z#=t+Xo6mzsgeav}w~mKu`Ph=_8XTPd0o21N3+A-dz$B63pxH01S45!REc>V#L?TJ#=zJhQQ{QE -n+G@ueRjr)~#E2+OT26Tylsjp&!{~B1_bpc@4U#IcH^M8U5|iqld)C#!9<(?F=7ypL2q7gpw_*8}^7C -VGoci>@mEv^$n$oc3Wfn7lwYtUC^mhr*PfFeDrzy_U#4(`$OGO^}}FDOiYx%eft{OTDfwip`X2G&v^{ -p=mGYJ*NIJ$A7X9v03ARlxGpwWX}_S?$#2=RWvueOWY3;G)t*4lr+H)PXI*#vWA~Lwz6d?aThZ8pyNZ -oID8EJJ&By>giaoaH*m@#%ls(1%uV24jd_JG?fquW=oX1}4K2@u2dsKboWTpL8eQl;E(D!I;|Hi~`#6 -Q@7)yG1fqi-Cc1%9KmDyJrvNKH*OwpDfA_&00~@@?f1JwPwv3337tu*vRth{s$P9>EX%x#Hy)MzEXB8 -cBN%{cc%x%dlIvp$%Nf5V|mP=1ftWW&E^zK%1SMgPp+^F_0f}A||whYsHEc)EJAdx#pV7rM-rJ8~^Km -=Yzr8#ZWldZ)}kIZj&boiRZ|S?H{PkEN$Af=|4hy4gGGv=#F9Bu}ytEVHOn?ne)NL=fMWO$QHH}ES7eSPgh@|yk9~;w5 -Wa=`B(p)Z}YGEu-KZX?cc#0UV{&|CkM~4Mf4KrL4gI@b&V^rGxgH%uK(Cvsy?0&-xL4voW2|r+2Z*6> -#rNT%O0Sw$UF3-2P%&%uOjV+es}#-?La_$RXR2ay1r}OJm -F;WnKGFERL}S}A8UynkhTC+8YrJt{)K`H%8MRRQd|jPd?gu}Ky!(Sl%pSA;_w}s*Uuwg>y5?DTKi%GI -V(aHl%zr`WPik`ys@;5r3)3^D-$Jad=a* -qSe7`2_jt^Br21mNkRA{HPHNipcc{tJV{2%AasbUi(==ZgWAl6RT-GIyW>0L+L)_#Z^vec)6X^HRE8B -1~ROzDDI1wZdW_|a)55f05uqXJh<6nOwr8DA99kbA>0S-GZGUQmGZ`%K8pzmw%A8^Yf{UQ2{QF{Eawo -JCdr)d6~sBvz*`#uc#PYet#_@AwdQ%wzwdN4J4yFT+T#{zv%`fWxYs{*|?dOXM@Jt}&g^a%&PWlr5^* -T;`*zMnQ_%9Jtgez%QhITBm@a(kFsBK2JAi0<|2z0yA(5G0SWPLGCOA3Y}I@z(7D`(W26{&Dw8gZuB$ -!l-ci6W;7VJ@(XzV>0cB+B6`K@j>z!5j9O}<5nK2i_>4BH(`2Q&iYeE+-=sn$0d*5-mcAmVz -Sw@XD2ROwk#f9B;O}K2RF2^2OQt`CY!z?{VM9J>zvy53a7t-PS78rSD2U7vt|z%ZW~SS)-C_&)?<%7m -ZtW9jO`DQ7m(YrC)ghL;#Bp=^5!%BOrL=IbcIu2U+K~b`b=}2KG{@<28NMGU3aj~&wj40|298E4~hBR -eHM4`j(uvVXLRsYzv;2itIJm!a-CkIYg}UgSbxr(If>6d|9m{XA>wi9uz9t;ZxA}z3+xf^G3ej=9=oU -$q2$peYa#7FG4|xilhbC-oSDi#%$+;e*gfnnz7PA4EU+KQB5S*QPTwE>K#m_hq&g8Kk126Ay|Vh&$EQ -%|pr*?nlV7PW*3{C2dtsJ0@M7=$esc1w59Gw(4qcST2^ZyYY3@Ll59$B)`q$X?<*Hvo{=8g5^_TEapJoi1<{{?^Df7aIc;~2d}>c+i$_crxcXc#|!yxDi`f$fo48L%~ED-Y@?&Znf -Lj8a+7)EJ2`c9ZirlvA~0uXOL{jUGMvb$UG1=pTOgVSyL)*VHf9^b6Q`D~I+T5qnf$^{ejZ<9e+Xnx7 -x{1irG-&}e0A{9J?Bj{T;Viwsgrr>}v2xcdn30K33>(869|x3QVfxpU{vxt_-LD2)CW6ci*D7Z=CdoE -ttv!>CcC*kB4gSW|K(RB -}KKNWwQGxs&*S(nS3Di%nbnso17FRgYuZHYfo#T9~yqF_RYJK;X+0b`(-QV4eRgAZmcbj{{O?pQGKcpwMf{Np8V__WSV*e3J{TJ85cDz~ka`Qrky;czE+B7Y=*C08I< -iO^hWvC>7am7E;fk$=`*o7>yOI6r9)w)9ls8sy^SUgUn{edL{yns*Yz5K}=nc8fs{L%jIXv*XP*+~d1 -*ftn6=1ada^4?f|8>^-PkpSa7!Tf}?Bdc>NI^f^HfdfsS8ym(x9NAMWoASDue3I -CmxShO#+-XYEJ!XymUx=*HeObD6sfh=p@+<#VyEHd;a5v!{q%dx{h!No?v*!RDdqf)_WT_?c9>Yr- -j9RbCgy@4*i$zxJjR#1<)6=6{@Z(!xE6kr_3=mK30xbytiFG$`+V#)baNf{f?g4r@Y&RpIF~#d-@Rwg -o?7QJa+{aMwb1El>P$~P(|)G8Gq2x({uht=Oo}QVRG(0OKdB -P;;xxSK-G(JmLNgB~FBRM-cC23SrX3d-Kx9`f)Q~8&YeUq}2Qo>UB+bujXvv*qdxUoqY;bB?Hal`r7g -5APLCZ=R2g-1uVujAy4i?^?P^{Do>XpL%L16rR)wYU2j-7l_RT+g1p@4qJ@!Flle^Q-rq|GQ05`$$Q3 -$;pzlB`v%yy|cXYylcv~l$P06Us-I=akPXUsS%Pd`tPh@~ZOc^0Vdquw$i -OBdjQ*sHk*JX+`Ol(p{zdN-ImNN{^IQmxh%^=-IJ)em}j!P`yUFUL{YjGf%Hnq}Qs@tL@V3Rq7Rw=rv -DrRo}I~7QU9gFkc&AgfG$;>+9v~=S%Pn^^NwW`*M7FzD2$w-x^ksVcEM=J8(ZW!J*IZM>1*SZ^@6aWAK2mt$eR#UY87O)E`006(I000~S003}la4%nWWo~3|axY|Qb98KJVlQ`SWo2w -GaCz;0Yj@i?lIVB;3U+&PNhLC4yVH;L8E4l?oNnLE!^cj#XU6d`lmyv~C6b4vtSGzt-)}tt5FkNHcDm -=zJ$HDzV~GL^K%r2m7Yf_n&chvVoK2GS>cCr6)19C2&DQqTw)Z^CS4DDlU3tOtfw#ZA_ubC^?*6X#Ym -&w*?>zpL7&yU}nABWZF%8S#9_x$afcV{Qh-oJZ$c8)Kh+}5Nm)TRk3Ww^juR -luj*W`I@+1F#k&RuzXvbMZ*w3HA -iW{dAz-VDa4Lr0^YdTNL<|&4AAc*5h~Q&9z822L$N0-6%8LvDR;!_RC8~&CMu?dq%xXM|#Aj@|9MS|% -lEMiJYv!}Knr6jZ^;^!P>iQe_DV`-?#N=20Vd%Y2%SDbq>UYOQkri88NO-W&4iyE40RI)e3!*5E=dff -3TU*psDD);v1$JWF8$*u)n76zZsh0{d5ffNKQzZAes)_(eW$5|&Dqm?TOVVlP4-T+)Uc^`P_`pjuKrH --D13h_t8l9dUzdZR1wB+Zxza?e>5g>(Xz#&i$U}%|C*Ma{jzKu7B_i5#T=N-Z?<5}Ww3MHXlmEyqh+Z -cY^`t|rMI)DF?U+MJ{(tOj$=r%3_-#|;6O|r@#Ao9|z@@fDFUT3GQR{RI$gVOgN(8x?=UmWaTZk_%W3 -KK$CzW2zexdpxFpNwHx1-6dPp8o>Bn|w&1c^_MEdt;RgfKM5f2oTw)pU& -HbQQfge)j%n-+-y%hH;*YbQ1Uz80$vhemi;hOLY3z*GI2jy?x#fbG|7|=;He~C(qx$IF8Ow{&wsSknW -%KfnFG(3jtea>iK^7DNE9T3J#1JvG6LfJu}~KZ7D_@ogG7kLeRbf{52?i-`@)U{q)c2{OQHtcP<|dws -!FAZTj@XoW;sU>LAuRDBTwc%qv;~bL>kUtSe -)IP1`1#TKvG?Ej9cbX$+jH0!wy4wa_59@Ro8PbVo3woX%ki_LH$S%;9TonE^5OP37k~dyd`K@J!bGW} -FJNCgdF86{_kijI-(LLv+sjAa{`ZITM}xtK@=*{z9Q?_EaQfGGzr1||)kpsJH@vp|#*e>Qzb<{^`dJy -4A}4P8*k`(`vWR3C+*;!KXcBwbftMLe(p^BWZ3a0+fPjsy@hmP&?==XBu+;|H=#v;%17mFi!%M0t3Q9 -4X8jA+MhOjN7><3#Fv<}Vv*ZkbrR4&1)c2czA(60+G=C&E0-16@tm0uJuo482<~Neu`#BY^3=!QXX14_aafu5bPG#m!~i?P-!uq -Iou1%v9f}i`k6vhut|u3Bni#G^Pslv{{I9F{|W2YZPcwYL&xQn*57@th+kP;*PHN^bdAeuF~1Vy2OR5 -dD+utvjN=x`w?Hn3CPhWB<7I-2b?;MM>ByM72>|0Rhny-3-q|$T?WDl2^o_WluE`yJ!SPI>#?|igi(Y -5L;?C$W0LSuJFJ2c7IK?Laba)J#K%miO~!lb?KzO3+RWqe4eDIjC{@5czhl;;>nyv0a*Bi0JmoiBFg8 -c|_S_G>DJw0Yr4Wk>JHQ<}&b- -e%m4}MLb7E2?QN+9xR1;NZDSLxwPP+Z|N08geX{$|jw8U;Ql04f2U0H`NUc@XJXc#9H5e$cXmTlj4<( -2aynAVdw=FitZ3Y5Z`VEoPI5I|C*7gI$L3cuV>QY@OCm`HJ6PkidFr{|G_^NoXmI-EaO@jIZg(I9)Y= -10{-9)*#6m3!#ziVNH-!mwff$}7UsyrY}q56$JO{ -cP?cNDX5L;Ys009@d3aT1u-PPISabfMRm>bF=?#85H!!K5ZSPDF+#Qb+eT#I+mW_()=@ml~4|Zd+2XaN`~yoG(Mj?VT%{8CLcjbA}JI`KC!G@J -~`&ryg|<*t_qn-SPOUHwWH>g0_Xe-brM*``2Dugrs;>;@?32(lMLn6gg*ydhokaZYKqPHN7dW#pOW7z -6|x=*3n(&WfE|*Nd;zbTq7>*8dT*!lwrw`!x*tmdDvv<&-A7y_sY1YeA?b{k%Ru2WQ)>6WditP3Ob?_ -80w!U?rg#_`Th=?A*`}Je9*mND69hpSSPLLqQ)WP*)9U_i~@|;MhSRX7uohCi}H3nG)j1p7ngvib*!( -ZHIww(mXnr_{h-`IYtD}P5A{@BtAVEUp#nbC$mz6qwjaXL5OFi~c&Hr&Bt8T8DVyYj%iCD+HQF@qw3u -#de5ztnVi8hMI(tyXSFQF@rsc_3ax#6hX$A}7K1 -ht~<7yba($(N~(P!@%?iC-=bG*;Qa1Yf2$D_q_}VY|$QPr~<(v}T)3v87WWA`J<=TIA5(izKarWI%*K -w6eTy=r-F*K-6{rET#80oostk(!?*iToMlVMc1m`?(|b!Hu~q5WA4!x8}Pd7n|05vj1zo7`4qJfY^mM -6N4+tZfVm6}{RtFZDbsV~WBJy-tvc -XC9(!#hwz`7B>^c(@GSoeW*bj-2v3PD+u7EB&UUknuk;=%V2P^vv;{ck9B -q5HFHRE_K+S?4XdmYcVv}Ej@jE6^qGQT -+`tgJuGx%5iZ3ARl@Am*@e){y}~B?o+o=G~4KIr%};&;kMzydc+kN(=En^@2vd;EwRRWAJ(Q>hr^;a+ -d7Sgrd5}?k$$0zwIm!YLVFW*hQcRg)R57UdOqbge^hbsjBsrBT?Qm041^$i@$4!qfYr{GQOMDr=B?aI -=61lquS8h|j$#}^6)uWd8Bawt`r(O4agz$T#2_p%>?a2@J@f$%MGpmjT#l2Ztr6?~3|stqiD5TU0rd; -ztm7}G`XHlqutUZa%0lRO1Ox^0>i}5kJiZYWbC$1$xXa8JXzZ-#El2G^x-A2>rxjvGRGG?598c03xbL(?s?CGu|BLVNLWy^#N8%K-i#xN%rYB;sB^1l){BPC;E4{rK3C(uu{3nv -0@*#MKSK2|wx*-IQ*x729VS1Hy6~rq!E2_rXk8pjs2LZnx(x%8C9bs3StuWR -*z4ad@)5FD2Z=^yy+;jDF8>rjSUIHKu(GxGfQHs(dwTaKYx4n!dNHNJ!>iGMUbmmG;Cf@5^jp7su9~( -rtxkKA|Dv!L>8&&3oP!Y^Sq0Uy5zhiEDssC82;_#6!!m@ua4dwzi>)_o014V9PkEgXSBEq{NIYK5}!^sNwB&bg1d{boEL4K5eF;+Q6AYwuDiK@CGn$^`gN_xYfE2d^c)X26WRqVN`$@W{uj)ENO&y1tNqC -o?xTr_teu#pf>b$`07;AH`3?bT#t7;{oA(pGAW3ASBoiZq}K@=lS>`M0X$+y1}N04k>YQFprJtEQzwD -^uSsn$@nbm#!Kf;mVSgBs1GOaR4#tL&@=3a|O>NR(&&VcBH{jM5dB6%A`PipAcC7xcwx}lH2RnU{w7vA -|aa+vevJ`l(r3WaZd4+lD}mETg&CP(SwebIWJpOUMfCUUb#pr3f!9soL1 -Pc$C`XxwibZ07k25Zr-b=k9<{OWDxewpFy~)qfSYZP#=2$HG(|7)7w6^<0GBQCR{>(q -XZY@TNfNpjL(=#O|VmiQ70Vd68y|tLu9GG71lkQd)nqmCDZfLR_b6w+)mw1Q+D2-cyXJaD4WE`P_QHZ>J68|92b1u@gd{OLoDs0d`ltDJV0$+GpGC^2s19gy -fw4+Qk9h#iu5t!6xN}nOoU)Q|AS+1Gx-NWaO=#0auYLO{oc6h*^|6O{@fEhUQ%jz$fQcIAeQLVXxl`| -c&dpD4bZC8LfOanjcU9wMC+?)hLszMOzfFEw4%@iLpzsyd}nx^4`5nN^{#D0Rwnnas(o5StF2XG8**5u%F990H7%C -#9X!&@a4N02r9XUQT~c$m~22GxDU5NNkmW7YG-m|848xNF9FIL6rHfDSa6zXH24w@9*KdXEmhy -e3lQ^%!B97AALFRdlbByXSnN>@e -owPIpi(7W9G6>q{!$FrJve{ddUsrXgHqO}>kbdagt19|c4#_%VjqF=pmMz0RO7o)1)rKOJ$|9u4m9!Q -CfX1l<(+KlI+HS~FL$YkFZCpgwu<|JK8aGLVovTxs9Dtn|z9p3RIDQ>ZdODli_TB;NFuFU&1Ji^+SdF -P)A$*u}riSMFSqc~&l^QRK0=hL@$?ua4xdT%JaY2ha{05{mD>D{V)pAWxZjVdF37Tyi)kT6B3Gpy^jl -FH7z;#vS<-y~}S5R;f9K^1t(6krN^um-=NFWPQJ%zam=;6t1hi9x<9+-He_#Xu_z||>dNru>LsT$o -0*t*eK*tj)L#dQ46KD;78Pkz(CV^^MI|L{i3z^9z8ON2fP;%$An1QBB5cNuxC;&-L7t^5hF=*OEh;)Z -!#3VMAVBjWjm~kQUI32H4F^&X}LJQElSw#axZky`UST2&;elA}XRqJ --moC?A8_Z&_EvQ}uYNks$$Q&@7GP*daF~y2CKSrv^ypWp=**et!UXsL4uuCyk&5JwsmbTxN{e4}W|#2 -)B$G6KXBpVx$sJ_5mI6Ir!*IIPlRp1(O24Yuf(!F_Satnl*9}AYqSVDTOo}{`e6s1Rp=z47QdYid9rB -P$Zxg8_Msss7T8Ve&%(|Mpb@k2o9=ijdJQWa1TBXRL|1bHLs28eI!tW{Czgme=19_j<;GDw$+_NLeHN -iw66u&QWd1*cWpiG39MV*AavK(NbV53YeuIzhRE@0ONTwrj+k@>KW-YAM$m5SV*h{`VAwqZtLit0E{& -IEAX=ji1kn4SU2SOfYOvL8Sr4x_gVXk~Xh+*7u}-bJ(&=_-_pSE*8%L>Ws3w&dgxncctA -ZXln@Uh3z0MCuQ>wtU+wE4+p%2}~@IjRfiZ+!p$AB*KOD<~H)Asr)_5uIjkoIq}1hw^WG-DP>m2!@)t -iE=|A^)Cg9Sw%LRjpFeQg(AyTGA4}C7%HOFf`_X?tEeK@8@1p;@LpsKJP4hNE7-1X1<0_kvRg4*ePaX -5fD=j!;l(9YL{gG=2!x9YQv)Yhg0W86gA$KbET43Qm|7M-%dEKJm86m}h4Y&Uer`Bf01u+_D%gSr+M! -(r{cXgPbf;osf@l^m(lN{^iJ%ofF1$n8^(#ZipAEsM3n{G -D<9h{`V{o5IQkPJtgOT%f~lEmb3cPqp0VIn@L7laz|=16kEbOx5FkSVVP8@{_Y{cZ6aG6dmB!JUr9rI -G>@eZ37xqPp3PL`uHGsJk*>`gL5-Ma!D;O_Al9SR24yu?ug=(aOdM9-TmnbJERb)c~Vm7E|I{UPOjqP -vwT=%rEeH}J$>X@LKfB!Woach9Jb{K^Rad})sJ-tM(~j~_R(55Yc#AQ(AsflL(h(kbBtcrhPk2<_IIm -mx+IXVn=pLjr0FT^nlr7;P(YKKVlx?3p!U*!0?JdGt_=}-{of7)c;s|B*2$;>b#&ckfNWlf? -y-!lwH}DgFozB4K+JPN1j7#8JgPGniz{-Qs_a3vR_+hs7bGrk&*vVQCQD`wEHq8DVd=_6S3SKjHW|1i -tahz_D;waw!Qb{#{(}%<8(=?(I~0NYK8XNTpmi9ptGha9NEyI@321?@wnnWM+dDcapfAl<0yon`3wC^ -lVk#Ibf0=gnM!D*>~gsbK{Tqe90{ikQ&ByJC6bJ1;_>(2{q)_FC;R(hd0owCJpj)$FAPHvd(uTb14gC -B*B*cAU4isTM#wK#A2y41(JkGNCi-&rhBo#_xk@tL4O(;PU* -VW8V?As;}+Z8)8^qAAyR;Suz~yLeDua)J*T-zb4?-hs)*ZKzV~b}Kq}lWm$(#Cx4k73o;5PH -E`5TgJV)zPtc(_XP;Ljj8tG-?dSVsaI6C6G=j12dCl2H!W`@MeeH*syWxH+Ej`5I -hPC;IR`RD;_b);*EmI%T2k={iKbPgin={snnQTiYHb-|+ZokGnfEo}Q9pamcrDc@4_kB*LtwG^v3MDRxG5s0UVqtA7M6#`ad1sSP=TLPrw9=l&*7o0dS&#WhF+kL2SUDuqVCBjLtm0^7sEot-Oxw$r -T_;6ng)0dc-gFff^QsU+*^`X;@`!ea6`Cg~;afAa+tn-^#WTLuO25P%ny!g*ri)-zH>J=Vj4z--@63y -Dvt+`-)v%#sVDIMxGBe1~mF@lAy(hc2Mo)K%HJE~ZpH?s!QVfGnW7we|{K0yfz(CRNtCVb%t1GeS(yx -H>{pjB%%`kp!tkz=f|8tpP>(G{NL&Wi4-W|Weg!Sj<;kP@al+{_JjI2U;3{`ciJ9oB+Zj|WLx`oA3V% -#<;Ni&=Wy}8KTb(o_e?y!(l-lK{T?zrKl)m-FSE)0vWo@0o;ZfINRt-lStEK$Keh~W?A9E6wROS>+7a=lNgfs6xxnqi$7oJh -P;OdX-B}sx4;WmH9{}n+4|Lfz>JOjH|3VH-%J!-7*l$^&n2WBi1P{sgj9)kCLXBMm>eG6#ee)^n_G8_ -MM393Udh2KxA9*2-I#0`_Ow(o8S(~5N%FWx6r8FrczLYZ=&3e=O*9QlasXp$Na3+7QEe~Vi5tc+tT`i@L+qh& -qQ!^VhmY$FVP~i!L_ar>*3$6v<4k1q+o8-ZC#SwDj3uQIw`_gb)S^ze{~zFY_Nt!4H -JyUy=P?H6;DphICaUIF_eY~$Uc1%Klw$kRpf~Z1PXWsuNxpG$ny%=)fs1F}At$fYgi9sHx!raox>a>S -zz3QRc3r)p>aCmHnu22MNJ`h$ZSrTn>q;e}!VAd+1_C-$&8V*GB5JGzq|Ni_b%@vF)qt{hZ101d%-t|aTXN3R8N2$L47}Xziglhfq -sWnHZ?s;tLUSWtE#aZ9~sMKAKN&TTmq@2g2NOtCnp;;}RXA|`77Hm?$&0pq}UX$tNJpo~s0fbG7u`)y -3#AzS7llryS&vs5S?a<($E|1^>T$(dmwOlT!;)yxk~q^dG -q$zSA!BT}Fw!5en@+;C^x%%~PwWnS7=@G7TV2VU}6s!(@PR`6+Om&P8od1Bqr>3{ybbMofpTgu5OTXT -Wz1t-U&3beCR+r!t_s9}XevOFv0upx+ZHXfB?154WY=ZV)-V3llOU(v{>r`vC-B%aEG?RxU7-GXD!^$ -t}>`Q{$y9mq(ocy&*Q{HnVMGxKN>q`34G4PTu`cDBjKYRRHS6fIyYD39j+7A8m -+t}r9k2WkG?<0Oq-^3Xtycf(%q11|q-zAtm#}D>i0@eK+uqS+f+62>2exluhtXlm+GM(t;ki_6v5PCdg!1#s4rjd0d-26&uqKodD}euyeZ0N}ck8dYTEhX?s*_MTIc6t99Lap*dpDBI9c9q}i>Kj$G -RoqwY*%Z2_p0UrSuAt+w$hkO8T9=$O1dx`DPgRO)w%32tra(8?>Ig0WuPl5P?-h*Ta1PJ<9Lr*|yfsa -K(@CJl|v2*iZEpBKBFbdee{j&z!ul3@}Jn+Fo@7$K={4S_Lj`k(!gG-eq2}Wq#PD%4b%yfvR;|O~Jv_V#5es7F7Cu*L=#WFGP>YbCHL*B7m_`!$eqd^pWm>B=Un?J$85U>@9(YSe;g?KqVG&S;4jzi-LW?LL0>c -IO#D>;|;&)6p(6vAoUWTtNX0QvX3pMIcw1@p1%T(AVB5jl;U=cPNL!Jp4R{Sf`|WJ9>$^#KNe0O+nDv -Ecg7ZVnPH*t0u;gilfAi824b{zWIi`K9AGY3{=_AE!u&uX|BeeP=oPU43+A;NJp -{?{1X-e>W+mh9d#VmR5nbPEZ~w%W_TG7*$I&q?stl&3>U~U*dKb|_lcwTL++-MxUccO7iq^~R~UP<-+ -TN23?1L!-P^rK-+%lk>HCxQeg6ZyQ+LJYZESMiy&Ov8``+CTznKpYZlL%RtGsDOOz1H$%Zb3ziFCZ;e -0=rbjGr{4N`~ge9Q8GW3Juns;u~WRF~Q1u62 -+yhogjF^Bw~3k`U-V!B4qODZ2j6)a1%mYd1vs~U9Qc`fg|ZVXz_a{J~v`_ehqBJ~C1@^WU{)p;Z5*Z4 -(k^~PKH=9*F0L8JPjOIt_$KTt~p1QY-O00;p4c~(;+?`QnO0000I0RR9g0001RX>c!Jc4cm4Z*nhWX> -)XJX<{#5Vqs%zaBp&SFJE72ZfSI1UoLQYC6B>Q12GIl@A-;R&JYB>@drI1@dqfIc)NzIQ^gL|{yi?e& -5S&w=NRKhud|V&^ea=vI{J>!!?rFsK`l$oqoVOL@?g>@tbKsRXh?3DO6by#6vA05|8kw4mX=k0)5}<= -6yq-L26=gU#)A5mzLs2mu6f%9rO0N%LTQ=u)iph_Xe82$iIO9KQH000080Q-4XQ?H6IWzGQr0Luda03`qb0B~ -t=FJE?LZe(wAFJx(RbZlv2FJEF|V{344a&#|kX>(&PaCv=G!EW0y487|shy;b5Eity9h5|XPK?VdE(x -FLD!yw2qooG`eL!!6%?!#Rr5l!+vJwC}(SiD<+w3RZ4J7}q1e2N)1Wm8z$rgQ3WB*<4Yxc%_)7 -WPMkZyg=2ft{`Ck8lWIY-=h(%9w?Y%!c?$&*zO-U_fPwW$6ZW@J~o+5?uGo-SVtae ->p+=G{Z>^gG)OJHN8e-X*2u{1i-2HEogxCPPm%9DW1I`EIfo^D&!mt?FaCftQ(LO)^EG>rsK8JI1lBrJuErzcg|-6C@u{4EQfkOY9= -!XMr1a7ZgEJhGjJh;_YpHzo#qNWDNH)I;)ElW{e04Djf0(O&Q*eqW*IWMEq{*GUZg0mj3;4aU!OnYXR -pk>SR7@$&5WlMNvV(QD|tR~1Gov!Nf$*ExuWk={oeIuo*>BVpTFBVVk{~X0dS$J*50V$?KN -OY%XM$RXAZo)kb>f@aU0dp{x;Konj<`wpOQDAv-sNg*A;a#!6P)h>@6aWAK -2mt$eR#V0~2Gda-003)b001Wd003}la4%nWWo~3|axY|Qb98KJVlQ7}VPk7>Z*p`mb7*yRX>2ZVdDT2 -?ZyU*x-}Ngx6bueG^u*R37I$9g4vyDGj3jHrkL+Fqff|xcYNFu`GY`x3-v0NiXFq0!lGX`uh(6dPr@O -kUy53zqr{?iDkIkxD=jHa9>DtZX|G-~PPEStFi)w$U^X+wOk{2^`_T=fA`EYH`+e3R@mF9i5X>YUInp -agIK45Os -s>#42SA1ef_^ergW>W1F=no4VS;<8+_3*G3*>%Lc#dxJ|js+ST<*TOXd -C82F<(tEwn0h3rygVC?Dq6wkX>igTZF0-}m{iB?-vu42INGw{M%%1+*kd)tJdx_?y`Q?|Ia3KT0{QmHZ*#DSXm -OqOrm08}3j}V2%*(;3Z?e2-2m`P6Kf?Z0w+$hon*IZDH_-ENh|~48-IzeflTA~v=4P{Q+PNugxoxk{p -MLwz(O$Wqr?11eW1;;`mssxYQ -AzrJI?{?)ea>QdcYI$YO{mOXm&KG>li6mb%a=<}iw2fQz`7EMO;%&e~M>V|>FeJ -NG)>!w0cwa;8NHO>W1TCmHL_E1A^T ->D@Gv*z#XH%NIG#C2l@{TQRS`b4Z7TsC0DId0Drv~k-*;OzbCB6WfD^OLGSo;|iHyTam*@1;k?gPUB? -Z@j31Ei{JW#2;!+Wa@rS*>nt+W=^P8b`f-t)lFp*N6XxRqu5!1#s6)JY}?F^Q8-S(Ly+ -(Dro-hz-T+kkjVY7)f|~%tFSBuQ16GeQYJ4J5i~<9yua8KKU`cs)hf32rG>lhbRGEuM)Eh#+u)pbGniGDaTaNi_D!?6*=OCKAEicBoZhOvy>GSQ@yoq+ -Pvg=W0xH;j>&vxC<6l>QEQ(!Fer&F@>(T~M*OYHMW^+bCAbT;73^3}LY?k8!l5>Ms?Y8vyQeNl`s05! ->SJ@j)c0ck$xJ|j!>IuyfFY#56@YBl8~gXpmXPQ}e7`NK<>RJ -36ckO+AeuqzpueR>z~F$o6y2KLZlkQDV5^Tl$4|(TCSW6vS}*8#Of@fk3$-$Sjct<%&MXX^`hIQNV2reiD&w8buBLeRS?K+TMG+f3 -i4(p9-DMSSaj6Y&2aa4-B?t%@$cpjf>ewEXw`Fl&9^4uPf!9HS2Uh2HohoINzv{##SIUG)3i-+@$9lw -C;-6zHa1JaM+Me|qCJPbDM~iHBOwLQm0fqe@2TY%t}km=2e-@IbCKN`-)86-FbF;2y@X72 -6K8bv9Wmso~MKY(XbVP&;NkJjbUhs7U=4=<1a>f^%1_U@21lpy^AC+4iTr$z_ARHJx+jL6+hwh9R3s5 -O_6}xH(#6wkQk_sKwFfSU|U(DNu^ouCO&SkN+#Sb3FWgjvEnLc}H^XZG;HEfK}N$(Y2^9!^K@=GoU1} -CAdX-<0Zcp0)X?P94xYIyK4)ZizO129>?t-xaJy5Gp;zabb2+iQIR&7vdSNLJ$&}dTg`vW?<3{2BpA{E*KL}G519hUG}mNt=>7IANU%G$u@X24i$5-O8Wum` -Dl;Lm%piF(UGh+Kr9Yvh2)Ad}Gn9e23=Jrp;#TTwdNpM)M$(mp@|ORsGvEz)F) -LmDnLpRp!$r2;)GRXfMm3QUOZ#t}~1Vi55k-|aB`i(vXNPy@hT?AKp(Z0kAHsoCur?4f6T@W@EGzHf%Sf^R{G8ro8lqLT7P?AOQiG?`znff~(0iSh5^-`X)hD;>3-K1W{OE-2qR&@bDa; -lR$k-soXut$6yRA8W`t7&>Ib2>1Lo#{6r-dkAq8oAw8s@Q+F{2VxM}^uL|GCItytA=A=Qa*8$Q -F{LKnu)uRo!Sj-z$hjA*Mud5jRJ|Phw|C2RyS!32~}%oQOL@#!XigVWkv1(sDF|nn;i;&*VsG;&mj%`kah8nbADFZa; -m;}gytt+Ai+Ini98~6Lne9Gz^YZIJ@Zy*i9Mx#34vrTP5BRTLbGCbXdZQ9osW=p;66mrh4!9XS}ZnHy -!Z7`h5XANr4ZVxDzt=3q?=%X&7fCe@qI&2*jf$3XKcsWIG>|PH}Y4e74?$9mb5U?B+1_Ub~&{Sn5M_N -U-;(jt9DsQw4qse!9k=2@)^@DjT_P^og-I`Y6TcFMIxd00W&N{xkVva9L5#72)#Pg%S6I?Pag -y9%zb@$uXva~EN|+6$-4bp^*n!jJf0EU4fN=Q|*CW&A*7MRoKtL9Gr;H0iLuunCL3 -B|e)GTI$+q%;nkR0dc~-uS=-&naAyMfWJ8kSh`hia5#;p2KDXO%+upvD;-yjeC|__|i1x5n$r6UT=1v -(iPTTZgE3#qSUBxt-NVE+i=k?-$CSnAqX$MEvlx0XUMOfqX(`R3qAu`%dz%72PXF&@Uu4lXw8mVCzs+ -XvFQMX$buYEc`0CVZ|>gKSX`8(g*-%79zd(pXrSNZVLOJ`VPBF9NF7!X79;08J!VMOtYF3h7VT^JzON -F~N6oY+<9O)qV2xYjfVz0(u^3}4^X=h@+s9%F&yMZifFKEg#CUmALUdur4QOU#iw*sd_WL$MuICE|(e{GRDjbINFt3&e2w)qpU!EJ~v@c4c8J$e!`|1)fsA%t5+#lp$4;!$Ug-A2mtrDU -IO7^2msri?g4-pi~taiwWh=HtZo@ZKq>Y(Zu4~#R6@u#NhR38vpLt^@k?EGdkVqQASH0Er4r5?7u)s2 -d%=d9(gva}=RoHUUgBdNk?cR(Lr=8bBnLC!xD%zZ0;!6t$c?8>#}D#28-o{IOIDvT)yE8wJ<3X`zgrs -IIw+)S46>)C`7end>?6Ae@H=~JR%8lBo>+6fMl>O@R0cQ}Y%VaTb66WWlfr!;(Ewe0eurPDCn!F^qQg -5YZ{XFw98c(??WpoJ$W;aN=A?z`4=ONVg_*wpRs)ch8>p29dF*)e=yblNol1nmKvJ;ds*bvuD>j_ObJ -DVe0(FU+z@y8LqHS~pawT8|N8<69h6r(pd>2>aapJjLyqJ_Bxlna)|H&2;(;4cDOyz#$q7c)$?Vinwb -(=r4*_yO`3RPcH!!d_0@Ff#EvQsFa3D9zuOca+AZ808HD~8AOwGlw~4GU4`K6U~>8Bt^ToA~neGxPJ4UnVKfOB9s$ZE6F)Wc*I%K; -)(cC;svCloH&@3au0B>m$4{5W);Wl -4EC!IA~;ZXKvAC5hVwnGGI2wClA0g#But<0RiooRi?58_%>jo^MPbh81=1xf7LJC;!(% -ARE^n^EfwZ?4! -qf>>60m9vVzCaUJH}7ZL2BJ-Lhb|!pwkGup7=all0bKZX99Xd?HUyuCu?XL#uS+OJ?nXjiQM3^In}lF -HXc|r#DK0}U@npmvD^)Gvmlf#uaIubWO-^Q@Ss{zVz*T^c2d<-W>OIk?qw-jsIgBxbxXaD -HEAE=4C_wo-caJYkhX>lsCV;imQi1?JHM;s7`4Uq1){=F -8^mrfXr;vmY;@ks;!X!#zdUXhO@x0~;oKDa(ZBXmGn!Hi*#COJ_o%{yfNff4z5cnmO9U^tJoLML4u6> -$Uq%rK`zUp6ZAlvEs9n2kk8?{<$(H{cFC*-j5^(Ik~-Kb^jLx5a$ly>$i*VU%vX&bI4}`jPOrRHTaxE -w4#~2e2aQ+LGo_1;>K;pND(kRy>oNNdxEIW=`|5Ano*4`uC*#kq?^MQC}F5CVv-pjIpghdS?&ljY=3@ -r1pIyC)BlUaDG;8?*jk9qSkx8!s>SnnFaGfQFE1Cb-n{$s^AGPaZ{^a$_6sIM&rA~4VCLo`33D~hac7 -rvE?i8p=#bTq$w$oQQuTNkBu-=r6n`9p#gNQBu*iE}(#>aIrEz5k2aMBm2Ta5nfZC@h_o|DZ15bFU-eJBtyPP?V_djd%dhplhdWXGuocanX{DnBnp<*8Yj+ycPHE}C -6w$t!6Am6+shl{RhUb1q>6fH$69$b#Z$4eWK%l+@VkjfMRlZz0e67HB3M{U?%7Ev3U)!>=yMG?;#w9) -xZ*SC5MZVrDq?4^=5#R&URqqot;nk!c@@s4{nhs$}S<|ZNT&q-TncdZ&wQd+yF?Vt;2y17%|_@a87mj -MIfp6MP&JfV*c2KL}N^zqF6=KO5z_;DOfnZ@l-;11x&MJRY$4I0=}97M)+Oc{G7hE)3K`!>Skm9;I7E -G118!5cur3{IuCuBsNp6|n{yy~tOe-3?89knVleAw*zR__Y_?ltMgAyRur#++kWbW^3{=f1aCNUUq1) -4np8#x){EZi%YJo{+*ffa|blGaL){#JZ+3rA|_nKpO8yr6~zpjqKk=Qs;r#$TSOuoL@uYo7}vLuZ2AW -n-UIcfn<1PuJgyns>5=YCScVQc!xtn$V$Q9Yh(`b-qykeQD#q -JQ{uznDOy0d(iKR1W7nCbq0cM^II{NXeEGvk9C4J@utBMY5f}WY9nQ@&(?O?Z6QIs&dM#4z@ox|wXWfVYhMWPS%x0O2l;-_x -E{;7NjMX~iqfWAI&@*~2`vw7gwH(f=2jfr&r?|r!dv+S!6s7+Kk)}Sk%}^K)?x1bJs(;}eta2b<|9YJ -gQK3VyeVOudzOMZjP8-X>T5Ny;rTp73H^aJ#fx1c;Q81giD^#*g{0{%gt0fQY+W!>MLOX%O9B2s= -isK@Jbmi$Y17BIp -iaEX2K-=qt01Fq(BPs~;i=PDip35jpwrZ&513J@LadY|q2lk`J;{3O&4Cv -ln7dUlFsuirSB3$|1y84%`BlKO=Kd+4Nl@^@m#wO8ay!XwHJli1m9qM7IHP}Sk$Snk?GT}8%wpOWs3r -jJMr#LV`w5ut?;S3`zmjjHK^jP2MKrfGp5)#z5E1Ytrjmf$$pBjvuuKA*O?_5p##d%o#W+;ETT%HQ{! -Ye(o_2(kQ>{S~aOyDbXSz|HjzS5yrD3@2k@T*9o{*jX|^&k40$?2COU&gP)oa?J3#h8$r)+}rqI|V(r -$(sSa1DD_zojh{QU;ZGP;&+|Pi;N#O`9T9_pwHCU08cIOGuq#zch(|OKz%bl_zXv!$}2!*6RGRLvK=XgerDKDsdGo`*<_z3?)4%gc$IodmVQt3_?8Q5_M4?I@{ -F!(tDbL@#L3F8I#{T{*f-mK#I!QuiNpL#(=&rf`Pa_4id(G(A>HQJNyY?GTdc1?(LYVrx -ecYl>2ehK!t&Pwr}Y3&u?MT>nr?E-$M!y_3+Z)~XHW5dN=1UC)2&>VnbN8V_PzP_hB|F){>MbAFYayJ -JBeIoYiS5@mlO={rI#}(W|-S-q(YJD%hF`BW-}#U|0 -|Iwd^-X5oI%M}z%9@)~6rLLM!+&ebmEa=|8dlR(g{C`kO0|XQR000O8`*~JVZx#Tp_5lC -@ISK#(D*ylhaA|NaUv_0~WN&gWWNCABY-wUIUt(cnYjAIJbT4yxb7OCAW@%?GV`gXVRm+Z>KoGpoSB# -WvVx-)2$|+I~kv2J578;;|*5YY2J#36D=iAdf)*v2h*khE7<>~I~nkpCgSQ6tUEFGkHIjIl&E7=sY${ -CMjb%G9JPY!_(T0hYlG^N_-z@X#i#NHXqa<8fKeM^?a{SxWN4offCpE=apNF^nw@mv;g2J6vg4MdZCI -Q?QAny3K&s4aQzNmfOmD~6=MNl|OG`sjeEaxsJj#qCA;bWjbcOzAH=03WNwc+(#%b^+%?t_qAsH90Bv -#zS8deqsE@a+Ozxy92dqxC$Bj6CB#F!8JjVk5Sd!;9$)eZc6qgvR1~fkzu$s96?$8ob0u%!xwNY!y)J -7{7sdG@dKbaZ2^g|n<)ZD51&dCKbs7=CEUd}!K00fDuIE!FRbFPCc?BZ8Fykw_XPj5Is@p;}}cXaI7toOi;sp7?%?g8PUAEBMew0gkMaU@yWa*|L4zsV5bKV;3qNvfz(0DT$94{=aq{ogHvM69Tz2%wjKWevZ@9S*SaNc0dW(x2v -5O9_Viy8Apr)Z5yV$)d*Nyxi&38nT_!6d0{`r5Cd){R31}h&Kx6h^>rtH!F7f?$B1QY-O00;p4c~(>Y -dF!L89smI5XaE2z0001RX>c!Jc4cm4Z*nhWX>)XJX<{#5Vqs%zaBp&SFLQZwV{dL|X=g5QdEGs2d)qd -W-~B7FQu2^WBsy_iU(T)D$FZH(*Ch61xoP%nuR=+X#h4-BbLGcE)xQU%fzfJm~4F3X(2u_#%j` -AUL@{%dQ|DogVM(d9Ftf2Gy8oa=AmPOL5RbOnt;78XmUo@Ze`kMbe(3}bx0R^)fvYYaXWXhZ?6GTWlSvrIlL^~lXEe&YZ -FhD{h0O)mN*8rx&mjbG~2`5ia;d@4Jux8OYtNjsQ -P7FUmY&c0X!WhjBDE4#F5grwJ6Yz_SndEuWP|IE}gNI5|E!`mck%)5)9N@d2EOB8vcs0nzsx*x4WkgE -bxt1*zYF&t_r5rkwFrR@^a|KFlw#&t6=xB~Zka^MuWoVRFgm#0iRbBB5ZF3BdpsfMw(nzlQI?Vu%RK^ -7JwfR}2w!E~0@ntO-tKb~E<@VcZCKfMeOt4(laFj}Yi(vuPQ{^Bypl4e)IAdix@X(;J@q1NW}AC!|gM -l17Pd^&Kik14R1lcyImV_&kxb7f~_?`K-H5!`T%+2Vpq73NJaH7KyaUB*;Y;M}_YN-jKZ<08>|^CK3vCmS6lPm_gkYP -{WNzz_N3I|M(ur5h*7F=;74^LIL`tsKyPM-4g^M9I^1K{x=cmH{Cl3#Y7aa6Dxm!I!V}PHW}~zbnyOQ -9N=CpGG7c#%COGN9*Be{_-e+Xg -5jXI){N?*Tw>9Lps7-^EwZN^>)PAa -0o1Fiq3*DGG%I}T;MItMLFz<=IS1aD0ri3bJzJ*JFO7YYx{m4A;4)9k%zrs(?mzVd1S>4s+pQ#j)#D7 -uT&y{|G-i8YAnhay=So}D)p@KH+XkYX`qh@`^M={`O=i_HVL&Qu^6}tge0cN$Hp+V{0{WlFo{|lyEaC -}zDQDZSF3kRu@lQu5r`0I;kPXL>$%muK{*fAa@4%CTw}+n$Q#Jv+y)s^~lTy=Rnfb;R0L1KaB+NstW|*%R^;x6CPlWD`P#- -*snc`pC05TgO6%Vo#s^C+~6V}8$jS7d2$`)Y2pM@H#dNEvZ2w~$u!azt8`q!fo(`072xxLKZxIf_i)$ -gyY}y7>lMEv1yuJ9@R6Zu;HSa*H5|qMwiOGVNx0U)A5c2R#And`0&t=wOx%QKpl~+O -#8Ztx@~-$c&j-L{P2*`7}kh^SeW2%dB93YVBEm(N#h@7J&Jm8`H-{~c_KJ;5Q%1<0VfILr0U6=)MfdW -a#&?Q-7Ld`h10Ywn3swTz9QnyBXD&=^8#OGmt6D0=niyYQ;ljc{j0$U{Lb9j5Q`~Kk9qm!R|7R6TsTXwR0arZ6Iy-3 -$WVIN_g@1kiQ=64tj&{)9Jr}NVHu^5KZFJpe>Y#__0vVFj;Zk3ROQkg0AtJAg6> -|7k$lj~oBT?Le>rS4vM0J-;a99KE1Sy#?O3|CgTxQ439?4QgaAm}6r2SE4l+Et{p2rz* -G>i#2k3JQCzdOofMSgb9XBjxPf?&Zn*wB(@EZf3ZN2o_3BLp*56mFW=P;544J;JV)(d4mhPy%zj2J2aWS*t*Tx|!%mS|JNK+ZMMw*Mr*ZMeui|12E+V|Vo57wX>^qyL;-JRh8kXFKqJKX^7!Bu?Q -G0QAxU4FBC=+{Y;OIkeTklC-EF&=WFTWriY1GoBa(#}k=o+3_ -gR0`iVyEL-gKLaad5`hJN{m<$w>!w!{X@M8GG30NbY!h?wv7J(7&-c^ -+l{fK=2&KKlyeSH>v^q(FUyV+p8L!^;FVLoKe=_mFMa9wxS4YfP+`KKuOSiHFA#0=fmyfoDGB161Buw -iqi`nFOR%weP`DP$y;pNFmsJLNQz%(H4B_IL{I?4<8{Zt>i^>T~PvNUptbeG8rFHSTvFu_WFUvF%oDj --r3Q>xVio(jzF4wAq_xUOV88Ft;gl?JWFGloI~G=I+@{#I1+`gHc^@PmBRm6^&=JunFr3V$D|pf10`B -a2bgLEQH&xBoHGStiXG-4psz!aQ-~9SLKC7uYeIo4mOyKlOIC+X)$nn~XAx}VEL3ixl!ryiWKd2*sn$ -z|45}-Jst$~Ie$5jFDkAUF=mIiX%5Ab%G+Lp;DNt4#HqIyJj``l++jqM^j`6hK?N?v-j*sPcwS6~B6w -)9_{A%!kea~JPCIxV{wufbuukPt~2P$H3MQc_%SgoXSAb6Mqw@dRE<%cP2?bas0t)v59G=!@DB&Kv|6 -YBz#%Q|A)Y(x(LmpgYtrXJus!DtXKz9McHKWI2=L%P@z3^i23aM&S3Ux@#`1jWp9MA11!BQKL1-Sc*N -^YzrghEXBaFmlK?TZC~;xeH0XG#Jf<`*y*M3S6w+gHMZ2J2rR0eC%lH7v$*y=FU3pK^$`SYWu=DlOve -hSOwMrxS9!lDN>$4;yCeRpHBzWE@5e)RFPLT{l&nwzYojZd)A1?KjP~K5iSZ!M-dO6t>B|g6leTr9r7r_ug32r3$vVdfvasof@{e9MZOJ5oFEuIL)g+ZTy{v%H!AK2HM%Z;t -G|GAZ~L|mj3*c42hqInDW8Fs81A(d;99B4QhmYMYs8J?Aar1Gq4xZ%Tm+|9aJ$GEy)bL9Ufc;tS6Q8& -XaX;%GNXG#((Po^raL9o9AhUVi+7J0C7X5A09mBVcZ(>h*Q4rbo&%9&zQ){JW62=A_J>V)S*H&tJU^7 -!=sTv|1bnZ{p%qDYY0}t@9M%bzV*yuRfQtTxDeVjQ~CGW&D7SP2+C{@z6;4fD~^VZ$_;mxEcvzPeIiH -TL{zYqN&`##r-AlnyVKN-FqFHVL4oU4S=VJK!UE(s7>i(bh0|yehnH5uRmEoJQ9ei@F2NvQqoYPPuwm1f3T2C)0(I>9LTn+-DZkCL125j^f6+V`GsVei1NQxuM#@4RHC -F;;90ltgsnk$JSIWOBr;T8Jm?5XI%IqI2AM$+_wfj%s(!1;O&W|9-)f1k4+^Ph?VLL+5C{c92pBk*Yy -);H^siV(EBYY~j8j7HDj;p@r8jji!S%5TTQ*a1xmOQBsS%IQ(pPGzZQ!uU)%71 -~h&H1cIbxf~Y`cb9>Jjn4+yC(V*d=uQ315(r6}{4t^M;j|V60$ihMD59BOTZMR}w3fo}@Xh|g_> -|CVi!-C+o5K(8z8F_`^gsNt_wJYdgX6z8x<^($k?cD@qE23r-OWAY`N5|H)xdAL*`P!_HFjm}ZQS<{P -dqb;_Sia8s=Ln0X$(8LdweM1w>+~Zk7oCVueId(cL-lUHY`g8H?7ETuP+-IMSrc(@ACn};PZK+i~;bi -_@WJ9{p{HV>rM^W`3T< -$iJ%c_!t|>hOKscgxHP@_&Xu`WS4~idk8q|J_ZXlKrFowLTC3nkn4kCMDyco0p_yg~1*e62yk{i*+Iv -+)u?9K}%(|y7JZ*{SvNP34tgfs!pt2^Lh5zjx3}yJylrB=i;UGbhrnVu~{4!uE(!Ahv->CG|#c%_2`u -Z}%qbHX-+Mc@4Zf|cvPH{Xs@aAs91Ov99z6y}O7|Avcyq>uwx0t;SG2jk`Xkc@DnxivFa-Ai-c)k||T~pEw9>-_**Q&?T*I -wV+^)x(6~s+=vESm8J!=#lXaSx6YC=@lMlnMtw>XE -mIvh}XKDRAEyJc!gQ*N0DY8CbRTUcNbEax31ZgUn`NDlN$~noISi-|QB-3Sh& -A&B3!KxM^R_sYPCd|1i)PwZda57%WI_N|ei((XtDO>238IW>?%TA+yFu_$?79SLq$L?#7hQd6e> -=knWdm0_^dzDXF8gFYKWvG2I!W@rFmEDlpv#h99S -UIlGxlA4p$Tb$OYsTfX*ax`bdprp#Ab7S*DihFb;6{jM1(x13x)fP -J-sQ^hG_X*gp5MXH-k8mCZkEq50rkaBAz|ghEQPXYE8l^fkTu@oY+QlbL7@d6AzFb0Wwt#4s#(J_ewj -q43c%JZBD_9A;l>R!XQC$7c)I-}S%zqth2Cr;nSK*g5H(+2)!v-8w_dD@&XS-C)hVUJG?IH{?QL$v+hrPk9r%^iKv-gG!c8y?4PWc9}=!2)}&{!xd{JIYd|J+ou;1m2&xw>O*{UMt8wB8+ -qK{QTzmxxfh-{Kv=`J^;Y0$cdf(jhz=4&mIAzg(?OnErSj}T?zHwSX(S;08my3C?k*N(s#i1fIB`bu$ -WF_OElyn_rxq)st(yj(I1XZ54I&;%i_8ag=3L}r!j*Qp5C$DETj5F;KENx)89Bnd>`3@oEaD*Q1OPb% -Dd1N&M>(w!%Mup83|>~&MAfFfJ6=?@I3m)AU@k@qcOC5viFlDrpHuyk(98ivd5ZjV0zZbP67gWPSuvV(frmyV-;smqi#kD5CFFUtH>41Uy5dOh%ogQo{`pm0p -q|q#(^*0%jV?+>|nmb7pMq*0$P*su9a~o%l;_UgG*>2A2lnPO43;EpL!2Dz@l=M0)rxEvr!AC%hx@m_ -1Pzflcys*~#D}N1gXd(Y!3q&1-D|r0<`PpATT11kfJ)aNRC`#Qf_1>RkoOw9Mt(D|{GwPpRi0l527s| -1{yIz_!*DytWZ?(BdSaCL>sB~(}9PbR=AC5<8`=5>@;${fmz)WLI|AqUncXwCO^`2$Y{rVsvJMgSd -!DUe&MTfNw>tUyQi%8v9=m_SY))E)J%P5H{h>WHYKq@1#tVWCk-m6$dvqp -p?B1I(aFn8agHnq?v31Hg0ESkv}ip*0=B*D_EUW0|oQ2S(BF@ -sI#5!+ef!)Eh`egOj|AlJb^jxUwDI_8gKM^*oyFWaNl)1QoH$97x4CG0riHzInZ6KMCV(88ZKeiv2_i -6GdXQ5p0b_nQWH2A>^Y_-~Xe-#zSs9te!7O8yaZZrYDkN^AWJE96V*b|V6zmK=2G&Y%=z>$ -d3fb-Jt;rVn)`v;KWBhXPerxk|$apaun4qH23F#2FCMs8E*H;?=8PK9~E5Sxg6DShl(2iFaH8Cn!@D^ -WMLWNowgp*~GRS#fqghdQX?8in7hb`Lj!-80OIX>PaCI}uz9Zc-JCCX9FVVOB7_Up9FNnT(smqw&QCefFvD2CeO9cocDth@s=OsD74p>;! -uxLCwFGzi>yP?tKO&;z?;6~oSAQ!qGvHi^j#nVZ|_DrhSE%0?Tg6@}**k30C05 -A%;r;7=)JjLV#EOs<907S1|Z#9UZMQf6j*!Xz&L=wyoO@d*)r(z_lbbWpB3ISb_(OwN`gH#x+i58knF -b(=W^!TQ?ZE#$ljmrec;Ts%1mTHA#yWop_P%b00j+ -L6|Z&!-I73OcbE37D(Zt!O~#DzB)&vK!;p%QgDOF_V -HWbC#7k2x%@bhvvb_hce?r1~FxAC($b%m1j)Hk@74y+hSmh9$aSdv-3!E|QgK?Z!9ZoxprJnze<#h$N{& --3ii}PqqGG14MnRd(J_ZtZ0iC>b{n8Q5JA&6d*z$MXK$<_QnCYL4>r-#ymfA7wH7-gom;+49_{E!XtW -z!91N&IT4>X0KtltQ|KZ^A1f5e?^NTZi?T?Y_+XzOIR=-2BEuxSyv!FTKG7e-+MSt{wc{th$>en?05D -VW8n;Evm^Z{jb+;JHzDyuJ7I%^6r*4@mll8@XLQZgh!fz-qSNnOn|6VUBmE)0| -_z|r?$nCg-T6nN{q^(IRPUIp>xBUFj$(bE#_v)hz5Nt}eOLI#V -LhpO2dWDGh@Gc7+57Zk*d$O|Z-p)%CoZ_7m<3a0@=ZLlKXSSm=w_vi<%3BhZ8g&O4nR0yYtsbgVm^#>tioG?*A_v5Z#;YUYU9CUQ|EinW>Er)A_~WQl*+-clPb92B8@& -fjzANJ(sHN0(slF4$9o;Ix_U7F{SN>1C#0kKBiBMtjvA{h7~AUw3^JUL-oEY)Wx-s2=r>ZgvK$96X9rnxF5(K0tOsY6n6+}- -`|cisTvjg*|U=ic+vZexiA1_NL)m>CRaHn+e1v#}XfadDZ>lfmM4WAl%J_&i_S7U}G&in`Bx(eBR9Zg -d;=Ftyx`2Alq^?NW)euR1l^La$|%BV=nr1+dn2OFF4;!RRqr)8PuSrnyZ6kR1nGQN#wM -VwX1v>!#6MUq5Oei==!;$oJ-lPZs*IJ=D&Nm1q*z{tl{oMve@i{dDn0ESpRs;&TDnO{~nagkt+X%v@b -K1pK$98L4d@;b??xJn5IjuSNBt*)SK=Ul3FdeUArP2xFVm?2y!ajxM;H)(a1FDrlnJ*$dzf)M*yb~0a -15i3S5>pnio--%;y -Xv#m?|37})lC7_hkoj(Zi!AeP+Ao2&d9YXFS8gE%Xf%OV2=VBDzO6#gr7fCf1HBbikEl$yHC=kxpqdQ -Es_(-eDK?$KOdK!JFie@>{EOh!?bSI`T_3ulZmV~UV>YF7pAMt*)0Y?T4$cmvqx0y^+3DYoUL3xNItS+wJm~bJcSjdLoxZ(@pvc+5$;B_x>C5Qg -Y&eP%hH)n_E=h5j|boBbo@zLRnespy5{P^vQqmx%r^bG2roL)r7N3V}A0P@9YM9m7kqr-E -60WhQ2hiA`!f+q*hj*gEmet|K4d314tkl`Buj}D^f&B58l(et;*2kG>hNIC%juPEJpbPF| -itqld2#Pc8<4|49T-qv-H&@H;yH>EQU7+l~(2LZ{9EGQfR)`sSCjqgOv&0F0kbk6#?(^Jj;E@WHd=Lv -EWo{QUUf=ygAOaq#-!)gjkC1!#Z@7GXz|q?Bk;q*(<|Gg`YF8=X$)EQzNP6-UavDbgxIA_tBNBwk#{RTLMKtMqeH4zT#f#%S~zmJ(D4u=`Q -xKhBcRDgOKqcqeZn{J^`N!S{om4%CR3z!wUrfKN3a{dKvslRQFR+Vyy8535aqgJC7A_8%|c*B-Fq;c$ -CrcYF77H2iMw(eB>PFnX{93_9w*EYf}i&tE2ED%Oks1EEYlm+6$iAe5u&p0Qm-PoF-1`oseIn1TL1x` -vK#!=S)wnkU!eWib+pMM`tN=^=WjG-vA%WrUMwPO>@6Fq6sR^h -caj7ZK=NI`WrM_sqaZqWV!lLrPoPas8~*PF=R#Rdy#BxRK@My29B*fn-vRSz&F#4nze}@eep4cu0iOaYdNSCFia0HSah>E9K!kxzaK9t48&Y+|Eh6F -UN8N);I6sx?hbs1J^dGIZn83V4=($C%{LO0+>3W0~|NH%I8zs1sKqI^1MiZ#?E -iA$2sgc$AH?KBK-`MGE1;e{EbTcm_Gp2O;q|lHIOIqbz%(T0ybwHI$Co=iE0k+<64=|iL^d+F5==Pya -Xsu#Vi~CAnL>pRJ0GJTqKDQ*k4EEVIKR0Cj3OWb8)mBy1QDK?%R -pFaQj{OIq8Q1$U}H&AJwWV7lDciu&jPZHS4K?EAl^U0@jDddZUW2+1oW`qNB$K?DmK(25-i~1 -JIeJdWB3~(@$iR6MIMOVgM6+y3V}lYBh##;?9=&%f{`zGHJ{_~#A}`Y~X2p%;!;@DRKaEZfUL%&nogJ -@>*YTJ1dUaA%8Q{HfV3rTigH!zY*snlPhsk -xWqZo5H?3Nq|mk6dwhv5=pw%UL4geJ`Pr!ra+igUfgcHJbHPm3qSD-qf&91yDgwJq-%W_ss)R?N@vS_ -S@LM(3cwnp|GOGwBjypd$RiVLFRn9@&Jm)sfI~HQ9O1S{BBoXA^|MWt -4=Sy(JXeADY+aQb#qvT5k`#fwuS0G>kKY9P^SXZB|u0EURV;D=Re13Y2(&Na);eE%fe4Yc*0{?ZJ-FS -O`ad0*QDfjik1=MpFsnPE6`=?K%F0qfE$bQ6=blqVHYc*C<;myJOx`NL&-EMXl@fY3Li^G=(Z;vnP`g -QByzLn56`-f~NAipE5>UIHgM0EucvI1W&7bL~)ZJfV-^9FQ)!xtl*uXB`Zg!A;H1N}|dOMk;3r$|@`C -?vd6`;l6P!QyJQ^&{aW{YW@Nu(-R@`jNYkTC4HW>1_nYLWy1#$x>*F0H$v;)dBKs)bZMe#&5jt;2KDv -J`ZcRQhSC?qyr*r-TIx!B8UL&My2Rc(DWBse~eBJB3XeOL>!4A7sX1-9q^JlIy`E%NqFMaK)FG!0utf#<$Vcw7~tQOe)3>N2Nv)9(TiNx-FYcqZu -REaJIZ2gT&e=D@P8qRq);pYn0v_gv5eNDM-D2{p|3pM(dw6DxH{yXfL{)=W)d&)tLmIT?d}wIPL+a>i -5j~P18URHvavM9O)TokZ1}c`wo)mbv{kFWcj3u=Ywx)>U?zc@|gC;oqV{9Qen-CZ6O3p! -usQqJ9bG+D#C)mT&UpA6T2V_8zKT=L1158@R41x7B8?AFfA_SU|5Si21ww8TzF^~Mk!=tXgngjR7?vkzsX>azIgl -Y?C9c9Az+ncgN#x1@crR)-LzGd5lsGCCTOuS>b$BEfmsM5GAr`s0!r41?CdrmqKejtz&xx#MCzOxi0n -KHj|egVC}|+Fvl~W46*Umq8LmKt>wMeNO|ukjuj%h%QvZ5bps?rV0C^>@^=>Dtka=VNJ(WVrWroLUF^*oDt$EwgWP8Rxss>)^>9Gn5cPr6<+RHR6%WsybYhdoB; -WAq^EipIH(D0a(U4;1&4x5tb;NRNp8I!L!6%3a}Z*+?%4>B}^mc1cHNJb_jg(O>qX?ctu@{;7K(_7)$ -lo(!f*Lw^PTwA+9!Zp6F?F?NgF>IzRM=!K0W9de-9fIIX}j@E@T9fK66S}jr7a-jBspDJrx3N+9IL2; -hn3{(e8G*>|%Cs#$6SYfa3tG4*5589((C+^C8bmDT7rX(!U*;J>8XC<8-c=EKTD~d_(Z@9r4HaYbndt -r2x!3La8eSX3ZWhS3wxj9vM%CmmUNkrfTD&yw;MnFupd2jo;}zH?5ExGLki?L{ -AiJC#`A~+nBhs;3`B|jr+pD__iZgIG||?yswGv=k~`6)u%UhDp;8iyJ#Y%r67S*uP>C}^m$;U@RQ|E!gVxGXWO8dry7eQS)i -;S2eSiMPT`(M-QCAe++M2%vl9XB00l))qx<(GDdP}XNm|9do%?wwx@VT@M_I3@n4>J89*`SyI-k0sH8 -~IZi>a97BFmy5LDzr$q#1{LZPhEU+d;Jm#P1tiB_;u)%3?H&{oAo&ZAS0kzmML5)SO;}_$oB8RRR@W# -h=rBS9;0_EC=rGOkKgc#%3zr-;>o9O=NOoaJmLrQ4h<@>u?=hdkrLvf -h;O^SCI-A4=%CE>VW5@X)b)zRc*D^CWqH(W4RIehNkmKb>XJ7NV~~Fp|o&F- -<{NE@0`(!IEr)q%tdY4h9s;5JUw8A77<2kVBzYs9S?{72PBT8n}_F;f5`3nF%g{vhEB$v01b#C$kyxV -tjz}(sd|y_~_{~pi#i?!ybJ9i0j=c^6>E!Rb=@1$=>7P?#CX}&s|y>uWn@=Zbhin=~UltT}U`z+@jD6 -5>>Y>Ca@Wnl@crW@rcsg{T8?CAc6TZ*+Qgt1htWj)K7q;7yr?vs+X2@?%8M88=Ov{fwUmAVg+CASxI% -h{e6>vasQkm0=g~*6bHi8MX^k5ZU6&@e5jZVP-E&2pWGkr>^`#D0rJcokMAz+x>O&keX^cnUrDDTCg7 -q;zn{H*DMwjDVDb^RNYDAOs}E(bW7sW$-uGc~e7?@7T_xK~RfdZUU|h32u5Pe9r*l@_db|iv*c0S+P= -Y&^Ee5t+v;mTJ7GT2Sns#byr}y1*ZYqB%2I1Fc4*Dn`K@c6oZ;?TX2#&k$f!> -Ioz3iitpm5a=DmYdNR}zCj2rRmTH -NiZzU@bG`f;2;VPJ5n3Gt1C^MvK -fU-SLKP8G(gRPmi0lzhS_NYmbmYWCu@On|*nqZCZP5nr)j(`pV1^FRE6?S?@z?G`k2K?~!$zi?g>U&+B^s{(udAG^MtyxWp6LZjP43)6uh|lY_HgI*ukz=>2P-VRDgL<%V7jF3~kIXYaUf-9uEu -pFDZes|RB;&&z~H!e|02*3SW!QJ&;Y9ymOM?77+m*41U8suZgMum~v->^AFHTv6{&BW9@{eLbhQWJRO -1u96IXpjc1i1)AE%Np+JXnc|>;>c(gGQYjca_du1(O06c)Ll};9lB!CkCk>nRN-SDvZyw#B -`^ZEsI=&P(qGMGrrvn!E`I82Hh(i%g>4w~+j|*6nBtO{!#4 -?}}H6Q{7|U4gKNKdsF0JZoBf>?n{lPs)h%8>S@<7h*GcV$PhpKG_!Ql;YZ`-a1os* -r`@IlML_Wdv)q5XF>th!)SX%1cj)NAmzbUcdj14h~2W7!M7OnFI3B(d_rG56IVU0@aD}a{ -bn+8UMcf6>|f%{_v>*n0J_&wMTQxh9Q&Cy)u()Jk|vS2G9P!`yx?H2C%yG=v$zkhXC<7c?$+R>@rQi9 -drGQKoa1)vQ!-wj~^O*b0eB2bJ>TC{l$>Ec!%mR#7>WPaE&TI-D^OgYlt`wQ9A9xMs=k8Hh8-h(U*<> -Bnudvzh#Hx;n&M{pr>;DkM8$<29-El-Od2my?aci_r?AC`-i>lhqau)8J)d+KK%aiZdAs%B_6$8FUtxgs_5wabo=Sk$K -P)c^(oa$@<2CE#JxV5nDHTMTIK=<9TAFTF^|!khYwZP5XXVo-jREg*w#vQw#9-*m@3s6weN?vU1{}%3G~`T%k^qrUuVxcpUj1r0?4B6_r< -_#+V39K|V)|SPGluC5X2-aWO4d5MY2;o5x6IS-rE~bco@vMNsC1D8jm&FUzZLZ_O6GGqINGX`BytqhD -_U;)kz>XY7+eLLaWkl+3CTM0O72@VMm^u0?p0^N>1~1l2)J_PU5msmlMcV&! -C?N0O7ru0bZT&<8;TW`EmZG<{8)VZPc0P`6umB;W?p`YbLD?T8_wwu_l3h@<@ZMA>n;Af;0#X6CnLLP -eGM0KGFF-kYC1LWd&c|kACC5=r@t*$p6XFz!+9aV_>yOXyIXg{v|2>_CrMBZG3ug{xjP7aw(r)gMhyl -Fyyuh*h~2I96y+q;#*L33#wQmw>iR-QV3ScPTGWM3r+x7wZ6XmG)+tiDkT0mJBA@{8(Vu7e@Zf@4#a8 -j3jQJU6FYSv4aM_H+4?m};XCxicA$SnzMy|g}3gDpz43bX>sIRS-H`2&7MZbOV4vz -v1X<=!3Sa0Wdg1vzS7Tp!93UO$XG>V*f~fb({>E@r#1LbIibsYMNn;Bf*JwUUr4QG(eaT*!A`B^kuWeDGm5`XaBiGGNDeajrARr -7f_N4cQI(r6B~ky@DXXIfb=Pa7XQWZOTKs!XN!&a>Avd9K}{aq2cB>#Wx~ -`sD*F_x$(#8X5Qk-$`xKGZQ*WE1S;JRO{Z6T6FFlqDAC_7Ht*(y1&(1eYC00)AsBks88RT4zPmLs$;d -Rb^5M%^boAzR&1>o^N7#4kE`+njL1;2<tiGkZlMFHW~$1JD&H6AE;Di#u=LR$o_hd;FtMy#HvMbjVn&f0oh85`DDqI#9ec&<@LX-yStH_(; -~yW3nH|ViAK(mhLmdeMKysyB4>2>3Z0NU3`2O*q7HrG|W6#VNwif=r%N<;O@@A1PX*fG;Spa=GE!eVk -P`3ta`LbfS`V!o&iuk5ww^Ec7+^?YOTGs_Y0?~U5_0bf107%22!A^!%Z5xBy-|&qg`hD{bcV|)9)@W@ -$jBtL?AOlk^WNkg8QMc9175aLk!e*X@&_L}q_LIjkKnyZddYYLwaC}c6WkMq6Y|@6tC9I{_u -R&YKi;BdUKAI+7(*f-|Ua%#X#}MY>_oT>WGjz^pq3j5)D_?vMBLrhv2Y?7FcWZ&QJLhM{xahYi5tyS+ -5`pTB=W`*0`lD*7!Eyw6DGNcDT%Y=?LOsParGd3$)g? -xSY%uObUocgi!GzP7hpXUz|-&`F$54*%N1_<*06m#fRrRVv?<2P$Ph=iJX}mJZ3+v7<((|DlVw3Vl2} -LYV2DjQc&|A~_1={l7fA%pITVn94r;N=Eme-k6>8F5+kUg_*b!s4Sk(G?bAzoWJ;i-AC)#!fLlq1NV_ -fw*&ALoujX*pB#FX+_=%Q*rx`yU9>mqg^FNwo0fPeCZUX}ZLWqh!?)<+KrjtxS<+y3^%{6H}8Vu@VgT -vqc=;0MECIlb@KY}OD};c37;-q420b@8Jv%$m~${qtwa!3Rfqg(n-W63xZB_AqYq_y$gIO?LxJqGnVb -3tw<-1q4Q+y)QlVDRg^3_{zfafmg<~yry -+I8xo7eIMG=TiaAtiKa+ja8GR(V@%;q#q?I&KqCnnA89~#_z|tuyVN5Jy;B7WDC!8_qD`wZ6xP%#AbL -1xxb|~>me0=8);k$!6H~_%z!Kc7H)4D>j@T8y5ADU~E@|m3Erkv5?zU}y-OYB*tthUd>%6RjM;b&s&c -T&Avl6TYPjWd109&EwbKc;VS(VQ5yh=RG@D}%1I7@jUQjY$5YB*}a|7Fg_*`Q^^GVFer?hW+Ty|xBt) -DQDki%`tB&5Y23>T333;AREW^KE5M8hQ7sL$+Wg5gC*@!F8;0xOBV8fhExk;Fd?H??k;^mK14-f&fg1 -`a%;^5j}&kAiJ_f_wzJMOxoWb1}GnW`6N~amTw_@p>uOc*dIIFR<2+5Ij-`;B*jdvE>jPt(V=aeEu!f -CHBMOnMO(x?sTiaG$+cprc1%?|)>=lcxQ<*G^z*&*tD<@ -BMuMImy+G*2S_4*^>v=SbbyNr;JhH31)Oad41Y4Av<^_IUA7q13d%aOGKj^OyL2nzT%MxO(r4(}Tp*I -)YPx-FYEL_BjSD_L2XQ;IxBaIsOvCOxrS&K>uMCQ_t{*{_Y2#7BaLVSKX}&?dMwrPgC9uh(4UPtuD29 -PV%zT^re2l1-BRyd0b#9{xN!KfL&M)%(xs7eHlXap4;H28=a^WQgnMI{wn7&hGRK?b+NzgRq~!J@cdv -HWUTehSx5%>8!0ta9Bgo2=yWeZT;RHp1fEwQh>2;hnSMrhJ}5Tzn3+);sByf0z0nI5fs3DwKrtuSnu9 -|89^XOthOTJI2Di5Dk=QCUlg9CA}N3za?zmrIVp65TV)%Q`-2#ozb|_X?v?`7VQHmZo0@d+ID!r$#Al -c8$tS^Rxpf2>3t5I8CZJotKzrZi!>5Ft_#fXa?Ui4zr0-T2#7T9E4zGy&v+q9sJsB^s`9d=gZF46q5m3hw2wyRJeQPPU3I|RLA{v5&RgN{+ -hgS;R1P7BWzh3N1+iC9ognrh0Mq32b^MRKz*6`<&G37mAR892m|eK7&!E$^@x)DFnu;!J9hF -Xj~?mLEDq>|iKSfy2q1D&n{1*!RYd93E;$R8r0C>cQL&)dq0KLq71+TUY0WOY$+;~tf-G(jl$Xt_qr) -if7;63O=PP^2dDX6s!l(FFOmB(L4K6@0us9wp -6)3${w_Vj{UjsAw8N5_@wjrgni(Uw0N!8gQWH0tXS?5AugFi%MV7fSZmu2~6-2U6C#XE<_CH04yj7zsP9knkzJ6PAhSYtN -ovCpHsp&_)r7!?%-v2Op;_=U%DH!f&U<%qcPRN=SYdUBb7|>$ESC$+?{}A|LEU7N>R!k?3HwMG*#zeHydKG#8M}RV%FF^~v{bTZ^WH!)5?JoNI79Et#w0nc=nae%(CTIHd1GLTiHmx!8wiGO&wPEAs7c -!+zpKx6HY^?nwX?=y+u;s44grY$)kY}9ZIr^OC&N9a$1Cdra>;N8N)}69bn|qTr~oEi$c*n&*|>AW8| -q@@mj!u7NJGS88tf}`}QJLw`eGFDCiz@P$=+~4L)o+8_A4Vvu@IVh|t{WG-&giCliCin^PHaAjnCR4b -GWUott&Vk=vc;*gS3;8U4nYHAl|`7)VgpG@4&S6ziiDLWeWxwV+cQ#L=6qkyvUTwhpNn94166U%TGvP -ErM;b4KSXmiDU?cwlHgyh?Br-}dqR49($^>&}T0s)^2o!}rsd3rTTpO8a-`H>!-SQn*^i00Y>Y+tg-s -k(8C%{4OO~Am~|=iG;6Se3>NFSIJYS^YRulKn6I>y={z7nnefa&ySAugw#F5Rye}n75`BY@iU2MZhmU -pkE9$lh`85}x|Siw$V_M~zn*Vo>DD$M_2uC%ftxwDAbbwAAw>G4lmEp?pJY{%yNhqL6iXjcorY_S#u$8{fk7Ir8( -g!WCexyN$g-F;?%t$J=Vr2*b?6*#|3tc!0gcL&^AyJu!t6tw!i3UFhNxWfydZgZ#d7(SY0da9Hq0SYp -;$8Urz=!nMt=b5MF$jWMWy5MNwFSE&ttE724S$8CtfV^l)nfI1~M4ofTv|j3413=7fSVcR0HmXCIKis -fsV0K;0Cwd8)G`3(`M4ZyRD2-=H}Ws^5#`!W1pH~X!dlX_A8@$KavE`>q*FgI4EmwuC^pruYDb{j3NF -P*&)~uCA8yml4=EdGKUTry;*D1#gVphU9TmcNjIUz^+%TTZ}%ya;#133?g_X-BNm?KVi8POhHDPRAxS -KT$mNUTvIqTm25o4NTEjb@#8jA)kjxNw<50Ho0e#7*H` -`fxQ9xD7F=t_H2{4`=Vr_Mu&t}_eWZkcc7t?pb^GCyN0d*tb8qKK{_765$}mEC7YXXa$KSOC*=Xl=i} -)7gqiI+Ew#9-%eL;#@$@TbB9lA!FQ^C=AbUdKiGMX3Drjy_H++Z5Veq=;k(Bxv~;PS59D7+u+M|HRc5 -E<*BN|LVE8AYkrBHA=rJ4mu&@tp(buepJ^b`jheUDHj_CZN=X@X#8&bz^R0$>Zr7s{i?K$BdwDeVxHy -2Y>yqW9aoUa4*-|pzQzlIWxZ_9{o`~IBC{REOUHofTN(DWpp(yNPbSLeNL=GsiHwNFY*Zvaks~LctE`l8_ -32f$z$$a7_8(Bf~_kL(kcOt?&y?3qX|{le4Dh0Kfqj~5%&VA(_7o$xkxbQY3bXY8H($q{$|OM`0CMB{ -s8=ZoXpZJGm^ORpx6cg)h7C!cXo&VRQ;M63g}l3xKx3T$v1nPJvY8kf4R?M9xD>E#jw6vq|ncpgU&gL -m$B!+5uRJ2IJ758GMy(Sg_$nVzRMwXuj)MKyl#LTCs)Si>cir(*te;uW>5ZkrA<&lKi42kOTSg0xv*y -$U1Wp!lPcnmht%6G_}_zQYoGr8HQTa4&@Euk{?m5T0c*^qCA7vY0{1H^zw_+TF&@F+gu`x55H^p3LapduI-Q%-HpyX2er3nvZhh$#RqfOL8=%`N+&kwc3JF-w?$IH -fuweSM8*=7v3Ky?wgI;A=N(8L{TQI(Uq!ZDfWm@qvu~H*VDdE7aPXweC#wevMb2(4T-dXZW&8h->za< -l9rRAqI0S%N8$bGcT-vZ-(t4UIM_5ncmNiEUbb3Wxtl6LqLNG06T7aKN~nk3Y+B1tmrnq2`3F; -$4~NSG9#@}WmQUg)bP>Mz2W00A1xH56#JJnWh%GP2qX>cjN_@+?M)>3_ApR_;UA99@=CC-SjF4f1I7J -go1Tf4#9vkSX9uf2{b(0r57oL1^m+KiDKoRn44>=`cON-r#3@x5+I_S)eCPsF-l)3N!zY}E+bUJ7wpW -&`DZUd=)3Idd9zOo=qxIf!l~aQSW%hQV+^XSPI`wv+Jl@;=es#T>Tkret_8#tjzp|d;fvUy>s_i{|_} -xdppg5iCf)AmMM~{tywSiX9fY*DE8>qzKnx`Oi{p~yQHWGuN7DL#g2pkOLm0LE7OSVN*OxBUiV+^pE& -MEl?u(>PiD59Rb{pdjX38UbxsJqQW%b0j^^0E%jaQ}i{pd*F&wMtL&$z-`mfdf*Ib=m5c+0w|q$G)(W -L|uN8dn=TSm23--6inv1%fh_= -<*5JdZPaI1WIowBHTwy0V^rOJhRAZhi?X9e+4R01wAf9Nr`_Y$_@Ph}JwAom-sD@OvIEMO&I#OHO?;> -nT=XLlV6*8#2KAweTFflVEssz3Rt-uOWf8xil-om0hI -$0FrfQ7zCk&N4!noaYtRh@4raAsmsl2VzckjuQ;UK~c1m2|%v0&>fn>T2S;;VG{nw1weiSz{&FF -`U|;TSL&98|_a*urMC%S|d`5$b7fkX8tf?uiGU&6#R#S=&W|GM3bNjf6WfD&h4$*Mrlr(6 -8Rb}q0G9f1I*p^2jBjiv>XHnX$1J$>f(EY{t8gy;r;o!vLm5N=PWlQ9m^%_$}$yeF9 -G}XTA`+!A=SOpimp;>9411)(D(O$*zGM!hLHNavq^~|gHEmQx7XBsvkDJ)E -(;FMIKviwFK(ULb2LW0HRU&-xFQ*>PwS -kOF+ZgXo5PrV!M5kAFk;HNhsxA9zm3$1Hb>B*H4RDNjkX**TPFL3g5W!q0|29ve=*EWQ3%r7{FV+U=t -_b@#6Fc+xhJ}t8^U&pOs&B+h>S#k=Ci~`@iw#gW-H;x9Os;6BD%BKcFD9wKnTx|C5pXF)C3S)R+mA@r -H0WqIhrTVQzaG8#SoSJ1ir>fY%g~a*qV90+D8J-`4)_?Sq})xT@SK#ti^K;Wm+a%yI%O3#J!11*zbkDvj(_YxQyr-PI?XfDw*azW`5W8BnJ&ByE*260d(80n@vE;_$XADB)F-4FlIui3{3y!Q*e@ro$iM?L=PVA1q&U#t3%Jm -k5|E(AFm=u%_T0EYGp2~!uyzn5)KOsZbYb=MB3Lu~uw?d)>2rmpCP$BYNdou*{bFgF0xp^*<&ppuX$r -q(J$6RjUW26*+e?QIPqQi*oLf1|pcKMzP`=rvIG20;fWo_As@?|*+;(JMbKv>V92Hmfgi@#f1ATzdSA ->ZXS)RqSDb9*`iaF3!QIzQ-U;)#)ZW3jExzz!tm~m3GVqxQoe|ML-s4CcYV8h~z5X96RK%TdltjXTMDfkZ2DlmR*p%@qrSIx(2 -tqez#c2=~+QV_J8#DR86IYwjHg2mL0!Hl*_dq%7=xACb-j%6a|_GC|?mD<&uKsy5R-=Iw0q`BfJ)n1y -&f-$H9D#)XkM~BBR&i!SgsJHdi5wPw9*=kJO$S3WOKy -GIpH1WO*`9*bx973zLINfyN;%?^mwYoN3t82qx*X)A7i;mTJYeYBWRtraVmal`Aj#jc7Xkh`?FZa>U6Oj`; -g>v;Uo4FpyoF|;)WOUDSdN(nVqJ=fnS=~^qIj-LiMp$u&(w%RJDVQDjrhF^z|0?>+)DzqL-H1*1KhZj -z|zo!c^7nYX2uz7ZeO|$#$6FY1;eN-5Kz}|h~Yb9SCzVN}_JwFcUc6hhb@AN*d8UlXuVQ+8uV_(|)Fv -MSNLosY2^fe9<))|vw6S0dSh+|g9Zb?zz_=lVClr9l)`kd$oRpEZL> -jlK%v^;aOt^{4}1L;YX>C9xSchbps=>abNE+%xuVyTH}l{&371%ea*`B=yXfkQ{Y$&-Go8bsY2 -6f0Xu1;NFrmr!=D!X2sQ3rm$Pmf>~j&94Y_JW667H!rxk7bWdWl8j48e- -40Mqoc;~)dyPrh%UA`yp7WOE*Iznh1Yqg2%6FB%jKx#sTie|CG#hP*+8}M^x-4W^M0i4c6YuMPpz)J4 -euO*voDR6sZoUt;8*tzM^!x_tPHe7QC7;~+2O0VpcJxRU{ohVNK1XuHjEnRExoG2I=HM+vAn%rr{ei9 -ubqnX?9)0Gk57JfDo(D}LiPDiE~?}CS}i_1c3Zqm{n*W+nlF!Dp1Q+RJDW6AR(7KL$jh9Tb5lLrRUL+$7!%@1J7>Jag(nT5Z{XleJk-)gJ=*{D!W<*rfw$kwS6)cE_&XdmxUK<{|6#^wsI+zZK!&KNwQPptOhmk4{wSn28dW(YW)9o=Hxu~cJ^(kFF?n)8!x_D?FSMc2 -_-a2tAMadIDl5qv23W%-F1av{^V=iON6{g;6SAFQO8YR|4(8RZIWxnx%ST6&Rnlh4bCQZCRx!=KPK+> -W-mzdx5!=eD`ofI?Ht~@apkv!6Y!hL8>0mCi*NVf$vszB;R6391t3Ys4hN_Rc`TXe~S)Xhj;b})*XusEX=v~ok0CMx7%mfXlYA -SF!SDx|KP%klV`ku~874bY4O=9aM%R`G8#r_@18M%2ec#}qwWlBtz!7{01@u@4e_*MukcO{PhOnP4(> -SFA_RxP=0;n{Kxx`m)fnaKc@Y^H*^;H@!Kba|Jzbz0!@qG05F8qVObBZB^|Q@r|QT`0?(jf(pC>qsMc -}v(s^0(&U~o{;^+-n(_aj|kY?r7LM33iHHp&+gm#A;la||~0Jo!@9RqZ%3{sE{Cy~ ->$qX>6|IlGHzygIbbg1ja^OkCmy%^?WDi84nnR=?eg}Mni$L=%k^KR>&Eh#3S0*6C96AN}R%>995S|a -ItNL$=uA#E#J(e%cBe)-&wvAp{3|;Nn{Kc%VcDr@}^`0o-Oc163*bc@P6^&meI7^Ichv!Iq -=1&u%mND9)8KQ{iMa2b_h#``d*@kJ6Fq$FKw&tKY`9r&9qh&b-D*n?>M8fjns>xxsz!pGp_-cuE#Q>V -3Z~em~W!#}cchx!VMN49K$&yEQbq!L*>ryY{(m7;x5}u7P>Pg)K1G${XGwdtrHUj7hFV2sXB;(A{77+ -W~w884!^x4#zTHVB^O1`n8qB5yx`83fft^|w`Omz}Zu9RDxl+FDl$37-@%EMA$ivV>knbtGs8LYf5t_ -WIHX>f{C@*BFxBrWBbqXNh6_Pq0GcW(`CpRe(9x@a>JRmB+K(9!caxl}=~*qMq|L??NP`n1JvGP>j@5 -@osxlSr%4x8}emL$5U?o7=n>eQ?uP_0f8)WC};{}VfZwpX_HMDTKL^KkoeT3&h5Pvb& -oL^v~UKuEMx-I}jy>i(=t{?C$menUnMGHujh8dPG~SOTigDT69mu_e9)Zuf?DYEvsX1cfjAlZdP@->b -Zz~ummiB^^X4o^m}4=;X_YxJu7c*z1;t9z;1JQ2B}R^yPD$&T(J_-k3Hn67_Tp0qGqscB5BRAsDlR=2J(l -U14W|C4^^%`)7yGPtRU(K;--PKi$N|tPEK-EGveg!1uNKj#IXfcmXPOfw@*0ug$3$HEDjbZHI-@wG~sa*8V0j+`!L*uhQX@t4}(39|GKYdGH -3EEF(AP;NlUT1RC))>_?JT&6bGRuf-rbv_8e%=@Wl3bG{AFUIAat$gR%~gn>D*-jK5QV-HtRvSi2h`5 -FInkiEQfn%3GrqE0AI$=Sx?>SrH2-d!7=hy2K|tz-3&6%VzX4MOhp#Xwc4kUp7zN-VCOOeZV*78ni`l -EP;Jjr)(IrNON~zKDmVH0=Z)*E(XNittuZBu)y`<^io^J8uefGp_mgRzW=jAD{K_^6P?W6`k{S}J=3+Ze)M1Rf_8|5t#g2%}v#Xp{)mk2K^ry -5!OO6KQB$Yvyvu`sFuhR2kI -D#rHK%h{;ohH3}s~i7OwBJRZs4s8R3P1;5o5%%NsrKL# -;_hA02u4AfpzqdnlMJs{=WOB$iLjyr~S5*f-k`uP7c5C4xqlnD_ml*ezlF&w=s^caE?A6x`g=a%GKkq -X{JV4#SZEtUP>+n-yq@wJan4bw`{*Yiwy?dNl?XLEsbx -mwyzDl7c4pVL>TUvooc;@-q3`N~rh-5KdCIU=%g$3*jzE|S=85dG-q{{23Oc4wC`w9g -W&)#dPmBf;VxhqR&bLX(OOR`G7}((>oB3@{oa{=9MnvE3y&Jh5`LalC8j6x76>%3Vopo)U4URF^ -7Rk7~l6nwEISBT7Tl?S;vn59#;t`6j-(NunD}M}Ri!F@}q)G541R)rM_NKAWch4}~aVPZ071a_n=V&t -_C2bz`?~uJU=HbLI^ZX~&u*iY)Z)Qv@$k&VmWkb40UYuaD5gP-RAtL!lW&(+kg!bmxO|YtW*vrfun~3 -F@QOV=3vownN8^U#6_?Y?I0hJHUj*D_o!B2@A^WN9M3d#oEQW%V*?yIk?2+u^0~t83=*mSd)tigtS5Q -OTI+DW^;{cdU4KL8`;Y{rR=P^NogA&}$owJ4UjzQLWjfcP;i#;PyKELsJ%*a`1V! -?x&S64{ALwF8#k&HGqFKNU2zf9r^ZByksFGY&mJkIone$xGORKvwe_{5oUUH^d{c2i#?i=rvt1%h0rv -dG=`?=XsYKhf|*P9eHF -?$OeC-#k2Y=K|r_`PrE@PIL>5G$ji#6~M`uklt{8I@NtN9hOcA)kLXHS<+=7sCfVhn=KD7<792z}0n! -4%y1%svwU~)d5F>z)$#a#;O{q8So%BYY?wf$}~xiS#Elu8R5UpbLFzSrkHOG3fdi6ZuB^?@M>^AIyyf -)eAUI`ZrFj0k6yY440zTzVNR3J!N`c7e>pi`*Yz@rgpYO;eBozCtlL*xmUz)(akVJ^vTwfnKz}|410h -j#Ku&&14fCBMajp$i5T?XdwEx>HAcsxng;wiBKYqt*9%WFrNoFsO&O&77?pf6kY1C*^B_5_Rt3d;keL2n}%*t?PU -T25=RFWx&&9BD8Cc;FS9xrJs%A=J|30=W>bGhPBj1^us~SmA8_@pCHf`2CVRlqNgHNB%y@Q)Z>CRqqU -%!%G>+ypj2U(MaH@U}mqZ%}w)V#bzG?LpMgr(%h%L#Z^c-sIDclvoyWH#>Dc&yUYbg6v(3A=4G@!- -`Ylto~6kxtHqLvE{c4*oHQM(*!BheIH19L6E3|9P*K5wxY$*qYci@HUZ$SzZ$(>yL=aT1bFcjCy^`W) -+v~2z2-jM3Bv_9qW^Q#{8ZCI#hN%I^^XQqCIZ{V}fXyxpcen?gd(-~CX{SNjaz93w)hg(Fq}^*nJuKY -Vn32uQZqY(Ckc|HM+9KLle#@_p|_vE9|@`m56wf{YcQ2(;$hLULVebP ->+*95GMQY~;8ET(;4w}0&Py{%CC+Yjc1YL6)YaLFu}Mti4&y$~@w^Co3s_Z?LD8(EmyRr{n9)rlP$pnq|mQ -Zi+!y%?vCapLkvG;9LPS!_F@yLVAyPu6X$@NxIkxZ`=m`h9N?F3XIZMtl{i!Nh^JXufx6QW-TVcn`ks -)Q@Q&g2-2=cH&!%V!5@dZm>+bgH5E|)dPxXB&&Z)k61wZbIr(`=sWh%c;#`zQ$JFOs8@x>NzPUXN)wx -j0Bx+ESB8?e7$R|y>J`F%p<7A;+JyHkT1$_by9iKof7t_Zf(cxPDmErx6pZtF3ags-C7H5&5ognh3o|#PnbOzeIM%LBq39eRlt5?@ -jGGcTzgOoL0uOdP$J<&TN845kN5%ard -Sebbv(nOEbd#Skrl@E>bPg7F@_Auz=teIHy4Q*9?`O2ly%`EOFymv@}lL?&_~wOIBI4!_MVLn;_k%3C -dq98I(cA30f5v6h^(XSu{`Fd92I%4rt8OS36nQ&skUP{V87S?o|&%lf(r*{j?rFk8i#V@09x6QxCwa{ -ew!Nu?z&?DL&>@S2O@g>Sy~ZBH$JbPQ=W{p6UG(ugk7w*#4|pHA@qRky~QZi*Yc`Q=6vN_R_Icy@m+Y-{PfHdnVaVIxd7zywSjDH^m;+MVmr6=;codP$yM9pX*T7 -)zyAy0E(5=4X}Viu4KhOaivln98iLn+~mb4=cHO|Hxl=zit?KbV}H|Wcsh{`b*WrETwo|Nau6+@)aX- -FHMs&P35UecLQRkv=*{ka+u_=-W;xOW`(jC{c2IbSFjzu1^F$tXVx6V_LFNSGa=aPa -1;(5^5ciJ%U4V;^uj#wjW0*{51JQT(9n&*Ng^$d>x3j4zXh_+m!sm2pH`xmrVEWzyftzuU1RPj5yqay -jg)Mrc$mO=SA$$AaEJsAx@dNpZC-$O8nnfXrWZLKwAx!s#G2_RLvqIh77b7t{^f!nz;&l9|UU0^A_w)A?c@2q8EY#iD#%GmS)FK{Rf7Xy-Scz8_9)EZaF -*v0S)kgdmA;D>^ML*b=qFk4l4a%HgjhQcb84NrGlEE%Pu8KJCxjD`*ObiI2O)K;SrvO$+&b8q5vaADu -Pz!e+snB?gj<~1KzPA_XR@tSv9f-8$x74HML;axzm=PhFpk^8mbc-7_QV}hfVB-aN>Ud3%-nawd*$ZvYs>w}Cnq`bm0)1 -)QZJK}7Tbk)R`mFws4>(Syv%75BCIqIrh)=q(zEL0dAwzkH_ED?9z_)|hB&b9}6stN76xCnNRx(ciqQ7>C*Cw8%3wP&KiOAgc(7jy{_Rvj{X}4fG0VG!oQ9)Wk^! -c+Hk>kNgqLNvsat8f@Rdv*(XoJCj|P!LB=McV()EW-N>8j{KASY>E9TzU1&Ri{GtaX{bo6+GM#dk{Jb -R!fJgxAt*lD=Bo>=pHoaEmPG@Q_&6c1tC_HB2s+4;X@LKN5$T#Qa|34~k8}C-F`kj{gNLdzCHD^d=YOG|_T{Z#}S#Yi -nn*+ca-nAXSi4L>Wn_2NjaKA_EQY{Z4(8s0V*o2V_gKPp1Q?nDz|XCIyL^5Y&ziqVk&jKbIf_%rH$ah -)Qum_f*sg+axX}#2X$4(8&)9(*bCP8X8AkK(j|Xu;wP)re!_GHMWhUt7Dtl&_W@Ld`(TVr&3wDM#2e; -H7XV&22*paxlLY!2Q`2#t3DD0N{!4x>~{e?4JvTi^k9UU`Ai8zE0Aq#$}&-9Ia{gXvdkwbQK`OI5Ez38)&&il00bCb<*Z2_BBo<--LU8A2*Iy@EHTAsNF@>~ptn%(~oES7`_uuM2qbwFD>AqC@Nn# -eM{Yu^`=>Lzc5XgHg#T39wTajB>IME03Xgo>sR;<{h%P%wKo-F))~(&y)&gd=H*gDmi0X&RxN;~;qbg)#wT`QY&I{n7bFlcH}8c*Ph71gsP*N8Mt>%5obVKfCV1c -EUEHxu?Us6Nx;JyAMQ3U0tJdp}9i}*2Z)tfKAwFSk_re1)8zr%vYZ?|Y9>S%Z1!_mI@hx6GsFL -fB(`l8das58mN#=4mJ;^MExh{02los{ -5~feRb^Rv*Vvz=E^Jn{HL=T5t{joWfsjTDVB{-=t{+?CC}D-0rr%WKzX-0?*=Ze^_wKoCPUJBlGHMtZy*AE91S>h^wi+NM}B?=mS&Z9nR(S|Nx*i~Vy;@_WJ-O -4Y>}le8S|0O%h}@i(J*sZKXkeuuuk0RT&+glIqNguhR4)E$*tbPYnt5+{+y|4r0P`{3qx#tnW=;x5?z -3~4lMa}s%2M%zM6#2rSJDuOX7jhaqmakw^xsf*|z5DAyM3N -Q2hFZWX{$WVY1~OABN_hhVR;umP{e;Paj4zqBV{p8<6fWioW0HHw>dIHE$6*?obwX${cTGVN|spOpmI -4J<}Lvx~=&&Hjs74*B+kEpJa){$)jv~t)a!RgbuA22rqWw+rLRWc(vGn3;vwu5 -*P{B&H{bzH1s+t4CrojRLfqp9V(HUjC=Rsj!GAc3Kr>s{M^q4BgHQ746<=CUQAiuiR4rkbOf)g8UUx;n+4wLmqcKsah0sQw8nEdMY*8$79E(4aA6?t`6W$kP_^=!yr -8H)Vz$DjNhr&R@v>MEJz(YSKhGCUtb;bry4E4T79^e>)cf<5xLwKp!xC589jtrWIs0pnZnXzm^7RfSG -%=>M>=6pkZn^yS(byD}|HHmp*Z%nGw7f_$<76}f8iia5;5Wg(tG;S|C=7j=F6c83)o)7(go(TZd`s+0 -+Kmv5qA$m4+2v<_X>&C9nBdy2~N0@D{wWNyZLnGYM96OIr|)pwZfn=5rJM!DWF6%RSrm!Y|}8vjvK!~ -8mMLm{l-*wBp1UMvkRvTxQ1@B)zt3~xT7cnUTf62M4UDDvg(N@7@7X<;XWCZ%tl=byAh<$8vx=?hY5q -tIZ>2a<2u>9avhVV7h(hi@bAp)p{YO&oA@3BTb4bj2drupv&C72fJWp|Y>SG*+B@wIJbntMsmG(k+>C -k!Mpn@fBcF@GByp6N8rs{TQc*j-II?ZuH#i5gjm(M(_Ge^Q*})1CxjeWNrKSu~ -V)a+p=ZOr%jn)n7cB0B3dM_HLlem%{At3NORwE+{95wX$4$(6kw`v*oPbk5zC-^py!ntTKX!Ucu2)VK -wVQJYdy4-_%)z1t7_(wL}h_jp(G-@E@yTR-r+4{C_G-yW)!oQW17_g){>%VNfm=PD}LBlaZv$?jUES2 -f<`$oztN&@g=PUL_gw8be}OPO&@d0PLgpv`DB3BVfPxtfw(ygLJRXCi}VS -JiuFGopf#GV`H~3+1vP=C7O4Xz#rtt+vXR3(y7-O3sJ8RVZN1)$pclSaMEVYH7J;)oqG=aisd(!h+T`$5F#d1*-`R@z=x7A;(?-EDa(eUxQ^0tr0kDI?45cQQPe -l3Zjt8A@wMo>OC4nksPjfo6h*L;Bh@S5#(05_q9aN_jT^gatyq&pS65X`BQsZ)%ZiQ2bQ{L>0F677V> -Z08q1^s4iWK;;><<0yJyMom -1*%0N{qcL;9nCpsFYE8$U=-F#-GP#o6?PAmaM*y+;CqD4>&wUW>sLrfH)1VYSqm?XX779NM^~&?Q!o6 -~<;Hfa-Vf@-rp#eYXCpCMkPaGjveU%s4O9rV8Ajr$6v8BZ&u}N5j~0?jYl3b-&ia#74T!iNMuh`v?_f -fu?i*}nWJGI?1&g)WBANH(g^{8XwnMeQmV&`FXmvF7=FLb{6;#$tBy3&YTdSXj6IV^Z8WJ#) -HP-liV~6YK-MhY<1XsERG-!;Wu{m=Ee3RC`5hE(&~>r-lhhyQF3j|s|Ni=HMHgKsRIP+_*Ra^1AL;Xg -DMqORbt+b|L%*i=r+NJkZW8M44ECCq7bs$vLdoBxcl_KiPUFf_E6TDBK3FWvr4yoZMg@=Myw0GJTj^* -QR`_^=bI{gnzDFR6mC&vWDcP>6TN*H^Kn1d$fbkqrhw0Mm9 -4Bo{EGm(lCce-!%$H>Db8UhMdA@6aWAK2mt$eR#VESE#WN`003A)001EX003}la4%nWW -o~3|axZ9fZEQ7cX<{#5X=q_|Wq56DE^vA6TWfRVwz2)LU%|}AOeQCnFCJYt@Xpjvm|il9=c1SAdxql>DjM) -!h5>+ymVoY=}Fq$l?cCojOMT*IkIa}^pr74*!#Sj)^q=$WU9;ba*&IV_YVKZ(Zfegb#=BsER>*vn5!G -^NHbKlQ=MOFg<@U@V^yLn+mTTo=l*P`X-AIY+O`*LngM_ylgQ#|m7G9s~yGAS9X4%7i47ct#I{JwYBA -nLH*=VmTRctRC_rC^Ms{B+bB?WN6-lifK;h!=5nIzK0!H?fhlcBUS};=rGtI2c84ZzHn -==|B({q|;#aK|Q5_&R1EVFAcBg8V#XwC?+%p#T%VwrDH%?Po~B9{3ParEe!uN}-nOl}sM$<0C<)W%pw -_+&|HIar>Vjj@c;pv-KH8DqrJJ6pzbBRqCDdrL+;Tb7zh$f1WoA#vvY0q#-gCk*2ZkraL9g6 -mn0qiaw;1MxVQvx2DaLXjMtUMdcy3|I36|V0y_{glEi5^~lG|v>DOz$H8#%>BZek-Zbgbqj4O*HFwjc -)!8zcoeSXc)OafPT=Z7 -t_!*;G-iYW^n{_-qmNNps?Zd9x+(H?A=HIf7mTS_QDRC;j^ZT?ed~dGpNps9j5zDPWJz$(HTCsk!; -rF+ThMqa3P(ahns&tHOUp{N-QRg(qoKgQ~eOE8kvx|Ca;f;J0+In>H=>fyHo1IU)r*C)9Ai)6^Y-OB` -a?sT5-60)vp*^lbdN2sHLZ`4NO8oIotl$rcOrHp=LkfrVx)7R@PzHD?K(rMw616Ny*HlWHc#RV6#B7ISXv=NH%A|R`N%gVrUMzETP0Mv -3khVEH`LmIrYj78d*+RshNVzOhHaI=k8XrMQKxD<}?o?;@A|89@ldVZyr+G6q*&6lc{-PLMtazb9;*C -WNL1sB_~sJi)vnw=E6K+6fR{{*5-n2E-btS*<4tg3$nQ&cvUHkb!z3A)Qa*R-=w)EO;Z`ze8tmQS4P5 -i11V_(DKoR)%RHgP#kfP~mi%F(8I@0`uPns6z;vP11s(hqJ@HnFnVl+$h+}fBiXQtbHtw{t6q_bWl2Q -|6RYnT4SPN%k>M*BlN&e_V#VtLvW2dxZSLB5}G1$1PvlM%HyLhu0w%<7U^ft*p6-KmeN0H^wb8BSZDz -J#2e-bMDdw&{>7{;ac0#wp446#E_+#nRh*NUcYv{ -!?9>QC84aR!e`F>KyC-DZ#EC&K%(9V{&&NbgDVGZx80$6-;2!v`^Yn8-N}x~77FW4OJcBXW@mb}-rVu -!f>s@3Y4dJu7Q@44C8froJCtn3!dBp^s?^%AH-l)cz|m+@E!L;eA0==p5&W;$RCEucav_T@O7Cyw`jP -eD1BN|w)Zbf2fpUn47QI|-&AQh1Qg++Z=kK_*copnV5@EbW57e@fTbWgYCl<1k`U2g@#sfh)#dbkrCJ_tK47izs7)vMz}f_=IRs<$`F -wx#1VNNcyiII4fY3&z>%bbWkr$aOJ%v;GQum)x13L-6@!AgvzbUM=X++x@W8){4I2_ku>1LvE&H*V)W -QwUE2=&+jk&mWQYF^5||Sl^*TrnSn=mCYPV|*AwRxnQ+kQsrD8E%xufDa3jwr4HR`;*oN&W-5IvW0XX -*S??toV%iJ!5*Upr3fbSLgtswXC!NN|qs* -Mqa^NqM)@`eIlYP-6UP(#n`WE%#OR_k6SKKC;B!R4!3+F0Z#tM%%;yXBWMQ;|!ZVA)MAKE1q>Y2MlCt -BbQgUR_7@M5T#98C5uFOBINT1{DD{NR|QVZ~mA`BcwsMMI9sAiyGL37jwiVEhN&>M2YLb>%%4Nn7P>d-o7*}Sn+5?3ztcI4FMy@e(t>9;b5 -5tr(nlgqdV+~o>qNPo!f}Smg6l9-(<_=NET@Tc!N&s3@MfKqx%;cUjlh2iz+(#E5vVIH-D>?AtzOs@B -q%nv<8C5tatmFt1j=`3$bPiD0DG2bLx(DR`ZZ5F(_%_5UXJ&0aqiq>ITzRH?X3C%Y#f(hx`uVxtK{P&5-PO$aI4KN>9Kh5%^*NJD@$0HgsR4UlMnM1yc>5C{$V)4-nwjx=zjA -x9c4{D$_^2773O*k}X|h}dYzw-(Ezg>NlfC+9K&}tWNWZ6^Pb -$=6m23@8dSO%6?OP}o|;oh1JDGNgSLu#Y6VDIgM#IwTi~G~Ktq6r01Y8Egg$6Q73js3$seI;GzZ17Q3 -ML1h=igBv_mC^i7{kj(#S9(qjt5JC1q5B^}L}9#A+9n8pEiwPg2<3kaq%I1a%Scq9ZRl*n~6CYk`+w5 -EFn^P>VoI=fO10x}<_CXbGq#KubU^K&t~XUC2QjPz6sHlTZvHb@)lzg2Lsl@j%gl+(iCBwhd5e(b6_l -L5m=<4m~@xcZxMfRLtF$D#8U-D{}75L(rHi478z23)-Qg%#@j*LeqCuC`lTDCSa)?#dm$85m*5#EJQ` -cu%tCr8qf|E4zaEV1q-3-%kgC;z?`s-E1kSnl{V%IflmHie>!uV7tU(^4}P+hHsb}~^SSeW-u&-Nmvt -T-U;WdwVKo0+^5Bn5ZUJ%>PzAfQ1IfD&d*QG=E)kc_d$qA* -=JnpPeZ}v_8xZe|;+0fQ|{@VUG7NN$5TRJ(Z4#$fekg;fRNbFc -qmIj{;`#%{3p%$Pnz+eG~<^g6NhO8Qs}(|LZ=D8B0+fJe7(54Uy6-0+~R*d$c*7H5A~wO<<*f~VRHHX -)uS}&(Jkb^l%M@ydiFQ>j9p!weL9`JJUxGP@%j(H`^{%B{`lLQPv{=6S@OPj{K?liu7cR*T{0X4;EocjWbEyIJ4M1ZdTNzHT`3px3y(drm -YRhRv7#lXCYP@lG0AtRL#7D0qLHukV!QAMPP#(_xe@SM8JXBOKLwb5{w`+PAE))m_@t1AHwfa4G)#VL -b>;Aq^lgwl8NTO6P}|NVpG~Pjn)5wtEm~Z;&TZl0BbnJFa36wk-vFEqf+B2_vHGiMHcz>?3P!*l#&*L -(ZF9h4K$S+0bp)V$%o(-lBdO1Vyl;?&68|_p;+_b?H0Xxific4%NM#>UZ7`d^7a#7sKsz@~C7kCVLmq -`oB1xtHN|*Z0MbauhdO!E#53vj#A-3!{l`Te?9HqAdfHcm9bm5G1A!S(cqD$?gg=NSX~88ZinIi;?=A -7@86%l_s`dxH?QWd>%M|1uYQ3u>dHc0B}fH!q+jOK!41=ReWBbO*5k9*R1GrQpXzdn9d2u)x9|NvUe6 -ftqn=j!a3nyUTw{CS;Uai)b~aHMDP4!R{(AtMg*?LBtXBhf{4w`L$` -*C&5!TAy%2N(2KibBZ>cg1H{$#19gUK;F$@BB`8=R5SWU$mGRv&4fhdPq=$azA8LS)iI(TQl4hJfkCZZWb}V8@lZ_j@K_1 -;px4&moW&LH1Zu?5uChx+w-Qgpl<1fs{ue6NnohiBdc@d_RTsWWf>T>})@1F(MjA_t)7R4hb9v)kVIt -}OK0IMAt#n_r7N^jH$cCc%h9=WJDt5Nsgn&s_vyshn@is-Ley-%X@@vC?Y?fpr3xu?no{8QFZ_`}AXB -gTBEv7dwPCocy$YzyNHq3dU&=tzJ1ZbezR2c7L9apC|4j^91hpDFtAC430ZaSgX{>T!*P9zGYg`r;SQ -NZH4#(X?F7eP)h>@6aWAK2mt$eR#S>L5j_qc003cr001Na003}la4%nWWo~3|axZ9fZEQ7cX<{#5X>M -?JbaQlaWnpbDaCz-LX>S|HlHc_!IurttG08~sEm&Sy2lpB4*g`Dt!$Q^)!(o$}XgD*OL&tmj{`*!{AJ -f-Rv=U<%yN^H|v8TJby1MJG9xlp!#ir9mQ#U1_PFb=l^0H>ptjg1-=2P)~$mYpSQYCpdWKEXD`J7MFq -~>LmR$E&Ogi$2LczVOLd0vjIbjW5X~Hv@uqo3tnQd*!XE`Q4DLXhAgV$B>BK -=IA8{%|7PYNK%*Gzzop0V)=r#AQ!NTrs6EgSbfD=QSu -SNF1H+1i5%>Z!Bl_aS;g!A1#H)9Obh4z9--bF5I}r@5FHSggDrEpkAY~8SKu_g9t}`K0FZ=p^)6ec{0 -Q*A<~4#0663$jnP$DWy&;?R-t+(n9J1^tN|X6io$V~2R!v#v%c$l6uXpja+1=gU8`1w>(Z5IZ?$car?VWCWr`wKn+mUWN)@{eS?L@bo=(bbccBvGPV`NAcWpfsn95=fi&Tfd9@i}a=LeFq;7aEbi)b$yT40c_DL -%|eMeuO?c%@BkLlkdYI;FUFU6Oar+8Sw2sn;n(Cn!6Vo+e!mkeZFf3Vel`IRqE@9@Y0dRuV{!hjt$iv -=(F9BdyRt@`=wHxNq$aq4@$o_LIs1;JL-sR9`m)oh-V@VSa^IGgEiARfWINJ^I9W?Ka1{7OA%30XENU -xAs%=OBz&1X8W4$x^v$vg-A+fyZXD>QiOuGR*S;Lfi>;L?&alybyN}1lS7vBtWV0ed5d+pClyM~9&?R? -h_2yw|;HiQ1pbcGa-|bGhmnuEkh47!ss^+Zk=dBoPHc5RT+B0Y`V`DFEubTZszQS}QR%gH;N8lP{ -^FEFogbxVrN%!#~(i7!v5OS5+=&ZmXTzC~FUqvMHl)pnA+Q&Dx2oeiy=?wv7(WzFUElp-=2?`_ -$Y1gARl}IcEc|E3$XuJj;YXIF>LlqIK}|&oHhU7X)LRm&>0MFpN0C*^&h4Ct&tq -kSFFEBO@F({>%CK*{{4R@~q+mmX~^=+w5Ih_S@a|N=1T+~`l2++@faFk?GdA}Zja!@*EbRCtmGQUaYyfW4$ZFur6R6LK>Iq@4pJ -$TfS|6hs{~54`1GKlh|7^G0u_2NgOaQRnLx&^+mrIHrN1~0}{qB- -=7m5U6jgQ{4k9oNQcz*+LPLi7<)S|cLzhLZnb`Pw+N}r>kXr~gLf!Y~5avTmPP@Fh53On?ZHuag< -V17Ogl^Jd!S;0SGP>lSRVkLovo+CE -8Q;dXIm(y{&l%vsG7e*ax#qcua1t2W9&qg3JGIDGR2u|#_eGL9Vc+nM&@iC6X0akQ{{F2L$5** -BFNWjQsqKPii((!frvW%?XlL6GIidlStdw!zGgc^wyPnTVK|xy2We;AJhx9IKs^FaD7KvC?Ih&_lvwv -9C`rvxhitUx#R&7TGYFFMjU=`eI48RE#=7yE?&N{}j;i$DNh4-^ -lB+$}+o>d}hXWn{=arFyWUfqfKA5#}#~-l44B6s_dFWMcs}l6Mw&Y!^&}3no|)VMH9r~|9OOm?_PTkVJbC{1&=LRk*)Sue$kk-V_ -6L3dMA8x#v1N0c9k4Q*jBZhBJ-;w|IgxrBr1=f@&yx=b*$xN`{DpMTOFl%wY=?(iZ3tU}kL?wk%2FIR -*UWbmv?&{gAuZH-Xc3sJ?*9ZG9t^p*mWS}ILkK*mBWET0XDo`#yz&&hI)c7_v0~F^sk_ -}1lqS1(m@KC#DLs0Fk4}yz6Q32kDWRtGhDdVLbIjy#c^ekAtT(m5WoX?JW9WBtm7q5~efMDE|Hi^i=i -pS7U$Y*V&R}?ev?MWCTU7FAvtV3=;j2!rv`HG`TD-!Y|QK8(HIe0KK=`9^}W5j>t4Z}caOT+7=uwr~_*{i4q -=QYU!Q)RpxR^`-KB+z{(RdamVwo2({{ba(+5Tlfm$rf^GW_-ehX3G`0=9SxRE7 -2Eh=MWSd48A5I4aS>ao!vDjEvg(%CVtxwGhG8*_HP3Bpu!noFvGdrm2TCGiip`aOpYV|N-@M9dq39aM^toC<&u9y>|4gTb_`d@gF(uyT$}fAYDLx>XPDB(|w+NGPlV -g!!ntCP({k$?pmZC8N}#2}}NalYo{CDWJN%cdfpt6waGfm~+t&?S~Qh=TrvF{*B+09QK6V6s*Ttvz$) --gG3c1Na=QpFzbn2Z65_+jP(|K`>))*-IDUlDm`}5`}+0n$KCxq%pAyJf!Tc11dS)P$dDf(r}@txD}+ -qn{|FV`(yJe<%O@JJshw|}7?j9^qfBwlyouWV6cikX^GTY3V$LOBBsRJ$h3c4k8J79bv#dQ@t-|huK; -u2p@=BCO)#&sM<)LS{QI+i=-^3Bd-)}iu4J>ckiYvdgZ5x+_F_F9o$#u^Q_+h>0r#>@~4A&coJP>23Q -!xwH_a(eqtuQRE@{~*WRj*uHR~-2Ko`@2FA>-797#oRLhDAnHe9Ncb{4*9jsE!&QecS172c|ec+R^&G@*OM*tx<=O&*a!;_M@TbNPzInG<@Bp&g>{Zg*aB -!-pf8BJZ_$(#Q;m_(0F+R&!v+yejD&*4`{t~7LcF3IxuFOdA>mYj3TUYsg+R&EdrI9T^$x1_We%D$ne -#~GZ~~CXk-f#j1~OwYy$zcf-B3(rWgIUlm$Q2&Ei#nVRc^ -)9qK1qMNG2K+)3Bh5N2Hs#O}R!>5)UB5edd0wGRhcV}F+Q=P;A6YE5hM}m$PlTk59J!xU4`sPRtQg{^ghFygvbdGeblL1st=?kqbZ9vDyGEM-2_2vMdS7cmVl!7HwjE4a` -DZIN&$c26a>WDnhmiK_qu>Tv49W$(sPLiv0&ctY?8BSLNh^qenmxM#Cuo|Wsmm67lx|bHhYLK*`J~H1 -duF;6U>GqEiRhA(hec`)nPW#qY3Fb7FLk;O*tV}T -VlsDM#p!Zs9m%rZM`8)DD6h5>!EyvcQtI@mFIUB&Eg@++7Ew|ob0uyLL#84AlBhMT*vc51W2Li&5JGfZ#gE4cQ-_F -4}@`quYrUH)>O4tS?f!)^9ips$NylUbPT_)jhf3AQ@Z?UBa83O0FA*kStI -r}-m${B&>{NOYNGV`y5g)h`i4+WQ8gG!06S$SxRsd%Zpqfm^W=q!)%lNHo@vm3=Zou9prX+mLY=H5pZ -O&nm)f^PeHkEeKp*@6j3dTH?GMRC#n-PDf+*K`WF;u1P)(A-i2TSdPscxb!%Y+{Gp8^#zZoz$MSg_i@ ->k`3QW(-bgARe5lg)Lo4Yk%nam-o)7eoxOJ&fHvRtsg0hA+oC-Yc-F`z;+{qdF~grzN>4#Dr$l)`eB* -`ZU3m2B?6dolgTLQ1%Vmf9(LoRn)DI1+cIw}4G^MaKT1%Mn@B+0ZBbxybTMy{IC2D@7}I;4<*Sn<#wg -6+EsBzlU5#O`>6i1#s{^(}Cv#`Grq|w!>4#I<6Horm(C={6c9`+C(4OwM(4OuGph1VDwgW)Z!2V%bKe -8rR~t0WtQG -7~3IKjVY~5OP_fy>sZibS9S8P?~XpjZG`WVYqI&UGS8&Gsn -aY{&^$>{)(g=o*4}ccGDSkm&(9Q!`;EnNDsnnzgwFjaaPX#14~Ly!j6%P^&qk2G59nfKb) -=+zr*Sz@xvcyL3EqV+I(>Bv^IJy^QLNucs+^v`VH6(acgLVj;ac1ii^|uF2O2~F>hk<3NGa)D(SA6cw -L;RKNIp))?~CdP%4U`u_e7aj!M`vL+W9P;S%PeN*vUPN}0nzf=9Z3uFj0ebb_|l`Ieg~wL*FR$)FzI! -B8(hz}9g9^h4{oC=Lol!*~S|F2}`So1NY7Q&n|ThroUY{I4Si)m^r4L-XJH1xHLLLn5ZipxVL%*NfJ9QDtJ-J{*=~k+p8#IubQi -^Bc4SY_mj~?FfTK3t^bF*1fps!alj~G~E~w^;jHln^+*D)N!_{z*^)x+6)9?J+x1P6`d+?0!Dl*Bjmf -Xg2qgF)6)O9W>uW$8_-@?z4}|AtGpw&OfW*Iy4Z0HbzMoiOKS;EuN@$&OJ9k`npE?X$w5N~OMUOc)C3 -l3X=Eu?nRNJfwA=zYhjr@5y36KR)zYVM-_-L9n2X8*K}xsbfI!h8MFXs);JWfY=k6DDmFDzFgs1U-A5 -fgiRDotVyWynO;ssni_cpA&7HB&Qf%gO&9*NELrc>mb0khs4TwqVhZ$0M&N|`v4T#S9vRE0M5x)K^zUve+~jz8PIK=oG%nD(dH{5)C)`u||}Cjk=Z_Ai@t3s3$Tg$YjI -?ux!D*+J54&I>%GV$Zx=W5I-<1~rzV1QBwsatU^_7$%UO^HNZV?lr$3s*6j2xV^tV5M})lLWXVuFoA; -l&P9a@A|?-Hth$%*Z6pX!8@}w;J*Nzo89=&-9dwiIk}_mhi%kL{9IZcmydtJnJ3PL8B@Tw(A#^Z;@Y)cdWk{8jA -DQlk4lhBWX#kcif4^!oLoiKSs5)?k+?x2EBt{SIBQX|-(@+W@O&;!$1U?2cIia<2kTk8PHm`x`b)j?lcZz9)XZZHv`s?N*zgu&8i9zPuD@ILg -MgmDYr09P0`@i;Ll1Fop5wLe!DVaqz9!9WzlKYS%<+mLXn>M1!xL&3y-rsZqO>srN%gu1b-9HE9xZ(d -0pjwEQWP7iu4 -N|$*FL|=KJiZaJ{R;d*88`?LkR~I>ac2lp)!lGv1gu%~-)`Bovl)=y~iZ%cje1nX+d29IEwn;6E)S4} -n+9W6u6C$)+HeAK4KQss@cR=ip|)~a`ZEU;2uW3k#%($@1 -oZ*8z&Zw#qKOm`&px|oK>NWX99WYz>Z#YP_)nt}Ht*H|#?SgaXxm6|bl~>PYyL)lz>l8 -f0jAKiJ`c>=W#jt5onOt7lI(3SC~Y4)q2I9FO_sTi??XKAuY28k6$i8jy6ecUIhU67w(YcU6{Ks=*dq -k%CWq+Hm#i+eRT9H@m#sX;!^sD~;zqp%(*Fq}GjGH+9-D -+6talk}W(U(9KvQQF65^k@fPe8|;uYvJ=v%`=|66gzetdwpT8j0X>iRh(u9UKVZ|8!5+`S`=DF|M$g1 -(V1$oPm(7>u3uFSEdBD2miq -1GDYx9fJ0C_AQ3D0s&7ZV(i6^h|pHQz*?%vHBKY0Jk!%BiMfBJkZwjoq)Ezr_oga=TfA)#LhqNo22P) -h>@6aWAK2mt$eR#P<&;B8MG008hT0RSQZ003}la4%nWWo~3|axZ9fZEQ7cX<{#5bZ={AZfSaDaxQRr? -Ol6w+cviUKc51nXKJ~TR43i-BU^jNiS21-lQi2nceB^^_|haKv8G6sAnmAazWY4~-~%MVmqfi{aypGH -0SCZ&{LaAv2#h>;O0KU*fggDE`kGkNnd|vPzhSN&`1D%*-6y7XXEDol`Xq2H!!_x(ZTZyGZT9HV2x-i -$+3@;~I;QIlne{Ougrz4Q(HVh%!lH(2+tl!Z8jFgV^!I@F$hA)b;d0gacgQ?IC&X^kx(%1`LW)9~Zj&* -w#@efO3+7xNi~V!l4Ui2p3A;L({4qnkqKr4fdufi1JZXTC@EX;k5-i`TDS;@9u^*KtwbvuIT20zN-_6 -!8Q9B1inoUi96;qes>##eH&oM0(%!$#)0O9uatSA1K~{w!GG7P}@Z5QJYcaud;|AJu+;aG2T%=e(MkF -_1v4^Ks!R(nx>J9i3RGzOZxgPh0nT2i8~T3wEnQ1F()y6#H)No7_~=3Bk(*J1E3ikk3 -A_X*M#MeRAcp-61zxdvs7nrVt44xgq-=^&I54978$B&R&3{=_x$uNz>pa|aEX!{Ly8`0yb^jLSq&d+S -;#XsDEt84=7FM!XOtOgaLawH(X8zV0ElJ`v>lWTtzJdPi?u2h8cKuWs*kZ_I#~VM9c29HfzHt{0JsYo -_Z7>QGO7NT}|SchnDNoCDY|OSdEGKzW&QAtlNTeMZl1eGJsj0Y);vgS7o~H}Fu?85Fs^%8md?!uR|SQ -}^|EMQz0NrOsYJH$$O^svrb!!03Hyrpd^P1jLZFNr3=Rp_kI%KCg8!j0JR%5*iq7D6~++Q(6xH-A_QF -gevJ{%ODdSkjG)er2qe+#B>QThXgFpZBW}#VQ~ol#B+o3gcIOQ0xfk+r0S4+3e@f>sI4zq1wp;@Kztr -AF}J8~N=9-&0R#tu2MkC_4F!M?HZSMUkA8+CjKf1nXFeK{G|V(>d8K~`G(ahFAJDtJI*_%>*8!xGR)^ -$2I1qqU(S&YHs3~j2y=B|cVA5)}2Z{;Z(M?2-3lEw^zF_}t*PV%qAe^b?0G*`IFnpUzE%YR@<$g!w8B -))4J(ezok6`SdnhzkaJ(KYNQhST$bcXe`t3J_CpP@va=y2xFdWQ#}ih7Xnv#9w@F(hcykuNJLI-X!k- -vrYe>Ybq%n1>)^v7hM2wAym59R&_|=ON=6p%(DfBkv!dpP!!+*W+w9BBw=ebih{PA^<1yF{xoq0XCpC -I8Z(M6TF^B8Q7nWEey#^@Xj`K!OMVkdw{{(KQsakDlC-b2S@9 -Or_?P>Q5&qEAwBc{ylY4QW4*Eg{>Gq&89mXd$GNy5$2A~UOK -wLBhCd)r*427VI>hg?%ekn^{#>B2tbrx0k7=3d1p?Wj`O*+=?0nP|!7{m-lt~YRi&7NfpcnCSy`t@jG -yLZqP=-r=MJS_Mt{`~1vq7l>vPWF0-A5XtOJS0CJNnC|L;A?=M0|1ju+r6aNF_e08c#h=`lnmE?4G_P -yhsIcz@gCrYCvJewV@5%%d6m|8}OV@u*!Gm$|^q;RyjXCRVmf^>6yAp%A{eM^#??3)bCyL{yy(x)${YS@ZWR%_X+=^mH_b)6? -o035vixqH{Ud+^-WasZxOA(Mc@3rDXqUpHNTB$eH(rAT~k`$MK!;VXnh}j^FvcwKSVW8B3dWWH_w~Wd -LGq$5z%@PeRJBB)@fApETVN5eRJNF)_JT=hY>M>XjfTC!)IaWs@oF+d0heVa6C(w#oKIh70_K>%c_p-t}&of*eKy7LRwy^Ve^OO8g)QH(uh -P()|oO^FO*B2)X2YeMe-#8gONRr>wEief~vycjWjO;?#o!@;LhCMfA&;0x}L#!Oy7(!1qd=?WG0+LH7W9G4Ds-r+z+tadP(l3}@h%+L|d>5p^DCJW>f1lwL4@Vx(a8oq!r))k -f#q~(J^PJBGSV@NmPXueVkI2;zn!-1ThA}Oogp10yGn`1b%|!TkBSOQG<*_hXo{I^w8+?|S?Rz{eJ|M -p;%@r8hLq#a0Ag1Puq3@9spV(A~PN5Ral`S6j&IKO6ODeHQP6rb>%~9i8BoG8p6NO$;TeDfc&hw7UAEf%MCQa7a~B?KkkBKC= -|wWN#uHy;NF5y$1)}FeauzKYrHNr0h54=rB|}PM+r80k0uhL%-+-oQ(ISeLcKLf5N>Rc_N+fU{cBeCJ -M4-5UbDgl@26%?q8e9UI&KFI|UrN{}6u<$1=Nbb>3G`>_eu^AUFJnt_{zhR;mdLuv3rfR -@&aQ9ioL!^KyoNAgWPMa*SQOOoxdHXKZ3UFl!cUUnAuo7pjA6o=iOda7Z6%RpsPW=ceJ(hM870B_Ofd -)~QEVeCPBsI(nIDNsr0IG4JOeF=p}=(rh;X4RLl0#BC2-i{PTb;iYlEhxrVMFCdv8E1z<5&P342s1n9 -FB~UU^d#6kkzi47@r51a*2zBg)|QUvyp&x~6)^S6H&HK`J4+e5C>=MY+DJLdNJ>Pv{su8i$qI0c1Fwo -^m`KAX1TFDyEhnNtCk8xL1@{x>d0je@g0yUZ-hIjGvCQB9mXhFW^({8?rUj?leU!GL_3j3Rf%W^y8y^ -e^Werc2y@5Sf4&ieqK`Q^?P|RkdzfDLVS4v_fm}W3N7gpj0yNI8Y>AKpoy?U`6*1EVKIxb`|r}FwHhK -!)i0{rK+f?;X=PCi=aaPP7sfy$?tJOQ?95L>73+WDo`-OOf`wBc2#e51i5N(x$#WQdy8&1!Kn(q)pV;zM=6}*djKx -Ee0|88#(45v1);j+7kh!DWXI(nAw(rdqBx6Rhf#raIF$bWF1u;_Cd+gy0+r^zCTQUf2ZeWNm4Mw&;9=OV}6m* -M!lPOiO^@$hSBhM*-BcGb0x#LXK4On(B7UPL(+=CH>i{OZsF|902h89$^*l&%(LfmC5(`-_wg-1Be?b -_mbwwTV3m2`lIKLeY}i2NF03cRg^alJ%wBzw!b({0O47B`d`;+p-CB7vs;2ziiQJm)+DmzUd!0h3mtE -_}-kM8IYyEuh?ACDE*il5p+7gV@oYhyE{iuaP61y8&S^KZ2<+Upfe#7aviE6ccfTnB2pRTukn>iDtgz -h3|OP@mEf6PV%$s(BXn)W>D9s_I$Xo#Imtp!yucp@^Q}S*n3Rl^fCkN^VID@M!$GokOyMGK;dCXdSfN --HS7}rCqvH7a^gH6ks&7%CN^}&obyP92nL82(xmyWLriJ#N;h6AG7;o-XHI_nVlcqu?O<>WV}5U&ap&}h3}72JC@D~bWWgi0=1n$F-R`y4-2TZAA3l(me8-aO&2itb5$zkqMHbvaJ%&G+W#U -V%sA_zd(~vw5{0t@2#pM}LF?>4~chUT_9h7OdV>n(ShpzHv^1)5u55}%P@Nlcy;0A2MaZ3qCf`NvPCuNZ9{p;ay@to+Y -*obDk&^xgq$MB!*>tStUeNoUHTjo;mRGM*Dbd!!;kH)*F7D9*I7OT3e%3*@PeB<(T4qtC7yax3F -;AxQVAf%tf#|(j8-7F{3S5YFut|t5hPN;^LI<&7TLO5kF>0Q|$UwB=M_R2bsUUJx4^iAosJ@2$8;@xEx%-DLy%`gt7H( -AF>mXz1_1)*c*(8oKV|2f?9|5Kqm;(mB}D_}e0uHVc*jZ=(!(rjKie;ey5UEeT0P?{+dKk#<1T62bOI -C2~J^Ei73~8eL$r6HcWIOuE2iSq^o9$$A2l+CW^g-Dx|5lca;i@k!+-geBeM7u7jPshj)^Yi{zp6Gvq ->+11sVt&gU%UEQ&OYALaFVbxB!#4fDr!m4FC)P+@R39A-`<4i=bTwpb5KfoC*7~h=gY*NdcZh+Hq-#! -O!n>d;+<+k~GakOTm)?{)CV_UN4<&_fuS$ -1Ug?Q%zKb8Y6e%ns&)fz)`FqA29)Oh4O?&h%R`{q0Y$i+)yT*=ytnmP~0w+*2f&{16*ufluAw#-=--q -F+J_H<++*ny;5Tyt8ThrA5^x@pmEt+a>Y4B>u7->XP_tPU0^D&guYSDL3Ua?v{+HLx=~15RF*fIAPg=OSAN83AdZ#(vD!$X8(DbS9vgRLEB2UISBzM8K2Fv_v-^2KTIr8|| -BHy3&7jk1+XHmeTvIFi54!9oS(Z|S6eykn8aVNRzl3!9}xXvB*d~uw}2mtUq-UjIf4&#lY1!X8jhw4RkDw^Xb<>|z>Sh{6d3)1RQ`{E~sy5s#%4K_Hm=-hS}ttB -nb9m9xE16|E{`1YK86JD%80Lb9P!rW`qw>8tAFtt%*{Ox(Og}5gBifo}QMxLaxkRRFUxxhfYS{aZH_z -j~O(;>OQtKKG@w=BlJ3^j%Tz)uY2JV$B8*Sopo4!sZK#~L#quPMW$?DY}FOXkpleRUbibIpjn0!QfYTs2-sqEhEv}WMV=BP1!a?Me@kT70Ioj1^EB40Xy -Bi@4Ay4Mo{MvoTX(VZBK>NaV7ZXhzaRrv06>m%oye&gQJ+!_7WDDjU4`G%4yoI0{oyTn5u+!y50ST?Y;xxtuvlYUzU;p2@hi%T$oZjk-VF}g$JKcil@elDPG3G9K|;b|KElj4E;TT$r -Vlj1Lt?%;jBt5!m-?>ZfS09frlwFmDRhM_BmUg0ahdOa;y7wJVI)oQW?)HUBS$A4a%6f()y(nQ|mz-_ -izAeI}9hASQ2Jqa=jXGuvxF&G)Ut2P+$8*TmhdybESs)hRd$<7Txb2)mNxN6OK3lH!Hoqhyz}%SDgP=)zL@UkuK&#{{T_L(-%_Fzhu#xI6bv^g;2y5Abw`Qi6P -<}23PiVUv8O9`ClaZEUy+JGk1UV*MiL>H>mP0VvH|d<4=0vyHnk_44^DsfV#(<=X=vEe^_Osq -2Er$#$i$XXXw$H4L-0JdFFl`ASLAs)Fves^`?2jHN}p3X*6fLpXM7eEf2H@_jio=ov`pDmsTu=`-x>t -_%;y>e)Nbgk;9{7aP{9o9uCuEP-9meTT-gfRQGO!*(o(_=(%sXPT?tA*dqL3+(HYN`L0~aI2xD5piKG -7Vq7AUM4n>qb&hJPF)QjEtD}&trhQV=OR?67EiQW!n~K%ZcO351L*NrL6XMMM)BVg$42%X$$yE6jw5< -r7IEmiAi!^@TKj;HJ13$kIpMCs%0iVl3ueU$Pg8vnjX88H7_)PKh*?#tgorNT*lp=1sf-ywnQKNIhjP -&L>`N?mku|`qFC6wgUHTbtR{c3ywlUDPBc%sdM8{0Bono#MD>rgy+Y0i8)O}?o~$Z$Qx9H@mVQ%8|aF -}Q|0+{@eHB|qxwk)l1mH_bzwLrLRs%1aJGf9TK!g*vk-S{Z9v9y>MU*LLH?h&(diSz_c1T?79uLV?@i|$lcDm(~a(o!zHJ1LnNA0mGdb8&F8vP` -gYecM+&wxTt2wQ!>u)c+eUbl#<)p@+{?XPx|s#mMdb`8zd{s`t!6ioTxrxLocy9DZhqd=4&cLQ7>aRk -!n?o40U2mu+r})fajk!!UATO>?Qh10OsaQcRa$(}s^gV%v1#qPwR)yG6d1;pA&yq!wI#(M7TO3@s5T` -p0B7s0PT6NVV+uvyhIdt!id_Ao>b9u#E_+aMM;odAF09!><0jp1EUp^fwop`M9(P@b$P^5#@VByaiz@ -iQ@xF#juepP`?Y?%PRK()?Z|HA1Dv7vW#-w&|XeSl3KFVbiLd9?A>{1lfs&yA?bz@zs6Z5XyR5m&1O& -#Yk!&SDo+4nB-DcN<1Pkz_079g$n8gA6pZt(MS@!7`Due7RNzxebt3k$y#6%G9SLR0Pb#iv^LRl}~e! -#B-<;!TVAm9(JwP!dK+bmGhjS)bpLoid}}8|2I*sFgW=htP>` -dypE;Ivo8Uc~pNUM2>!oM;GW3U(@1U`o)c;Jq>C;MpmERwHDsR58ADQHlYGYZ=x8a)$zzAz(QLiwxkP -b@RiUS9#0)yqUcD32EE%S)oOJ%H5K-DQ$W&Mpz6x1hXr9~INU&M2BHRr$CA=W*6QF#W@3i`9yLLaM=pVBmm}}6aJ#UuOjT*!W -YO9F(LFX-TIP`dI+c#ImGH;v7q!B>1C*;F_~hTS$}t -69{;%XM?9#%EIcm!PGsCh(`>?E=#BWhoaQ9;cpPC3DrbZp3QBD+@6^h(_!{}n>(>)8n8ld& -YWcJ7@)opTb`4i#UHW2?`pW>jPsGKRvqs6cV^ -MEz2v3BTIpec&M4OCj9V>U7=kW>=4=y%^8skiMc4Fbp_?|%1wXsOj2(sWtbO7k=Yu`ZRiz!pXomF1~_ -k0BSg!U!c}sHDhL&eK9zu32G%U`ZHuQ*zU=zg2lv@Jl=j0;@OY0pOn_%T!VD%p#IW*dl8gSXl~r9q{{ -%mlj^T(^pz6y_FI%MlKXX$aH}~_UKM=JrM<-8zzp -Pk$*RJp?lRzo;hwLxY9~ncAPTYUGxW!G3W79cMS-!K!aN+@g)xM0xm~IJr7$J4*@|mA6Xtz|BCna{@0 -h@&Ry6*Frhj*-E-=`o64Daq+68vTGOP5aer&Oqq`pZDC3)I{neYl#J%D!wvrDDd2Bfl36JrR0$e|;L= -u`Fbmy?qte9UIE37D0tiX0XkK{8fduPiJ!U{@TE3U!B3Ha3CR60W(V#KbFM;I3u@CO1&lvJs&&B=f`N -?CA7o@~N-}xWvS!W3_Cu9E{a?Vq(-RTOlE`zS*EhWdfIkkN*pf>KNXDArdc^?t&SfgOW~3qCi}NL5$A -M7&Xg@LUZDg3f4SZF)0kzl!9fY=?=KbSjii9|7Mb|xYBzJKfh%~^fdhBJr_Qm031NMDKL2FxiD-|@F~ -$8p)$Eo|D_89mbNsO$&5$oecTnyq)W;J_{c$Wp4WaAb_G=!w$`Izb7)D;z6R-hIzuB<2*nXNq*hT%(m^KLo=Q -}Rq;N|sucsJ-CkJaBaXNDxw3!K~}8H+*BzzYH5Nrb8i&rOe})n@WSN$^x~Zn@q`D#Gzm)hqK0-7kKRv -i~^Qr+_)U4#HZ5208t)d*T&itJ}H$J*Fv(wVUW0^Lfqi-;tTd!&>V+?G>&pjHWC~LgnZzH=ID|wFMNx -_rBc)^7EFheb9{UazfWq8xx2iA=@i_BNBX1bX&xT#RsyRnW0EQt?MD@~!jgBYG^EU8Z7y&~C5_8H7~b -PVBe$k$M70=uqo>iNm(M9h>y<JG=ll%#LNzfhITw=%lpbyl_wU^jKWl5~yWqh=+R -?N+74un^MC8yY^f1}+$eI{)WJ>}SgCeTm(wmbbV)vz%)h8uWSda}<-76-m06#|H=J8Ez)j{tfp3TxBL -HyZ-;|So0;0H*BySnxqa@4-bbX!`UY@-xq5F8jv&$$UWq{9t|Ba*&0YAoxy?vg!7 -dAJ`VNB@;O>`mGpC_^;<8eMw!xRL5^f^2`%Qj0+~R+XM^!KvTnI3PvFXn(;k7f`s%xXF(Dd4|*6`(xLqMzr -+L+8v(cf?-~vN4|aUa&*8P(IW0mh~bcFTIUZBZ|>;Vuy%VBz59g4u0Vq_O=I_w8-?L`meVyV5T;K|T5 -A}>6X?XY5Dyf34YGI)O3UbtNZrPP%gOAo;sC#~#lZ2}|0bI@d=)E507wo_IX7t@!h -lG~G)_HEn7%nK-^l9T-J|Z|{X!HPW0BM?3C8jTr2x~}%Q4C3D=s#=*#%-@fTbBDg8UvCty8u1UBVfHl -cY}Ld_rrVMM++H7ZCm7H+14uGlPf_meD5l)DP6u+;wz7+vp1beXt$y$yP)h>@6aWAK2mt$eR#RIt113O7000O^0RSNY003}la4% -nWWo~3|axZ9fZEQ7cX<{#9Z*FsRVQzGDE^v9xeff7AIg;k@{wp}1owdDVxp;__Se))zl_lHimXERJy1 -Hs>lbJ~}OUXSd -7j2)5&l;He%=eBn<$UccpV!q!$&D9B!GY# -!lSWCwHJ{hWouRY2Kh3hxFXH=yx}5{n_b4cHuIW<^>OKADS&vHe8qQR;g}3b1dEjRUNq8GaNq7 -(@MO55N_8PL@%a6T=tXGrSy}o#b%Hud(dhx++lmzK*elQr0hmOk6DoC%)+bWuJy`)^vm6)G$dDYq?%) --PEpP6=iFH!ub$xWCQ2dYq*MZTk*cQvNrTZy`igXBrkn}@xouwtM{X@DQ2@KcP;=YRc|r<&#@KTnEh{ -o}i5ogN$FgGHJ>oFyg6EX>u9yyqoG45qSgnV}9Bh6&l+ffi|676*B_R`VfFlN}5MQ6-TuX-4A7?1`y1 -yxS-RP)cXjKUZ}x)8ZgZ7dvWhOTXy7PU{BJMQ;(sq0-2_2)$rmI}fP4r&#GFYC888X_=tL#9OQC9wZJ -;`E|mey0_LOJFhu36?ug+MvCt+KZp`lPjH~7r<@&up0Qd*3s1>PnKjd26mIK#l85o)z>m{hS-68m>@5 -#cQ|*%nR?BttbRFGptD;rc&FE**a#bA6)x^09g=ITyYt^mkBDD~n{Q1fDk?+)gU50s~jQYXa%dQ?$`@ -f_1-qSaiZ!da58s>U9X$s}#rdkg+S?Y&*)mvRCD}CKo{^HqI#vZ^zp02}UrRxmi+-P~H#&)Q5%@3DlJ -2={ucf{E{UnZ$u;d)R0?VCS4bk^0THGADHeTI(E41QPx+E#s43oye^{^e=l72dP_i8Zkm(|XppM#ar) -wfwC@3{{79Go_>gTH@Ivh~8OeW>(E~HN9qBHuf;~WzAjhqLte2`i;NA&Wx-%r=;P;MKk3;)!x -esG|9Z)v;r=Nq+G`y&lh1$Q+peYq=8#ti{~4^4O=%k1;)*&O`&!S%-9$Z<+`z*I@By`bRYl!S*CVHFs*viI`p=|o(pFaOcWMvH>=_)&*7 -%maA6_R3B-yH%w9N_w_SNlC_6NqXxZ@+?b}FfH@#Dr9q`2Vk53q6JYaj~S+X2V+~? -sD!h_!7#$>J^h!bdiFfapZ$?`904LrWgp%~T~%WxqqW+;sRnI@*jamHKediLZ7O)y(>3(MIFyTrjF&w -i0m)7$&yo>TeFA6I}Rzj!W_VpvPqcWpT2Rc --#Y{dG$>d;qcml_pMIa^+$XR+!@-GR61&7Ky&MuFRU)=?EbbIIUsjoLL@R_T04@!VD3DYsf3^F6~NdR -6^+*GP}TgiTf41@-57XP$cjJ1w_McuYmn@ZE_WGN7H-cxgPCC}L0lSUC6sXX4PU2SQ5eA=dqG(8BtEVhoII;JfT -L$w7=(_TePcBkUFZE<_`wWa&Ip1Ew>gZtwes|WPU9xpFQH&v+JTZV~$e=vA4*i++8l*UR4bEOX9rZ;% -b1GG_#>N4{-tKOgr(y5iAZJ<{-(DUM@Qv71I-oe7~AHu@$?_pu|4`E^S$5}Xharjj%9ByIZ@Zl_Um$& -^Eja2t*?OBq#%=P6e%3FVed%IG#Bxz?N63?wq)AC-0fYk1K3+D~F%g-^#2C8&#qgr-`wzjo*Fi(T~zq -3(Y#rBr#ok+9f)otFSX^^I0;r8nGNMY(`_3>bMr~QcPc)qT*w@w2m8>=V?LcN>Wl*L|YSD6#l!K(Ifd -v)15?nW)UnpvdTdSCUL=0TNpU4{q9&*^ae{qi%4^Keewdh>Vk;fHbjAnk1`SLAMyuov|e+N_&*SOUGT -rxqPur}l1FI@FMb*JYGJPI*u52zrgjxpN2}(EG2Lv%8%At!5)jZ~soKp=#JJ=C5e);qC0))hH|OSB9b -fYun_ikfcyeQw_XoM`$=!m4;;3n$X&&oh-CzT{}3~V@vk+(NtZeSIX=e!8)7b-tOOm?rnVgXv$W1G*v -51vnsp#z0W?^debiJ+w3XvWNi6X=}VqE!uUFl_R45KKh>gKM^|z!%Mj87Eag9_-8S&L$W%4?)}VV=plyLx!(S4`L!vSJ+x#jS+wAO^a4jjb;0_ya7kgXIwJu9mdQVN@* -%{%W$+brwUorAuAq|>Zd)nVMy@-ce>%M{md)hr%Egqo!U%`f)fe##u|CcQ2iThx^#9VLmO%>@T-RU!u -ZCPXw59RMnSmP%CNFBgJ;Bxx-S-L*3HB0}KRtUv;6LF68#>^#R90k~X7mXHoCv-> -SgwMlQ$)T(?W;e^3uQ3cM{aRSm|pM` -b=pN-Gza+*8lbBkrryjQI2ag_3-L{i_N#+d~OXHU -+A{gg(p_NK@Atpa*oN>XNxp-X3jfVxEvMX1H)_r{+Ng!K$kH!iqO91 -{zg4w8g(&}4i}2Uy%01a^Vr3nlJ>Nm&dsQ@dH&OM+PKvdcU6*0Fn<$|aO(lkYqg~8At|!xYEMTMO`7ZxSFxw7<>P17fahveh -`jjgM>`0zFyGVPhnOWV3-@S_a;GDIpQ-TjXrF$})lOwU#eOZBFLRwyxQ8fC{i{0ndN-X8itMJ?qjPGL -Q0z1753>EfQh;zzV}80ve-*(pOUunZs`c92C&FMKJ}i`ryGI3sXitxV=w=VWo*oB#)FoWc!#zD&L}47 -{`)0}_T9(-!by}q9-Z9iReh-D-IX!@6FUzaJ9{g|*ezXUFxCcMpgP-id>zt~+6#E3e+K1okbVXuB{#o -A;2D+?2gR@PmgdpxeZ^r*Vv=p{`h?GDf+g6WM(xv6et>9U3AUp-gb$6i#J+m -YHi2(#ypck!w@+S8r&p4F>5U)e*FM`|l_x0@*5v!Yj?{T_-czwaO56DwJl`zVS%OJU;eF;+?X0CSV1_ -3Yh8)oe2VZcl&Tnz3iG(5WeV4Av(0qGYR!4YGaa^C3OXLx2BT+C;$ixN-M;H$V^ABklJf_HLt;;P$&= -gUO)&qqSlx&cl^w1FUZ!5a3b+8mekHp%YAW_ME2DfL8`URGT -c-(ZH7GL-Vr@{duvkGjTLp|NU0~-QwbCvr+vllc=a-Od9Y(0X|oNmZ}c%)z+=$xdDEvfXsJGt<957tY -J(WM2khKEMl4{cR`1Q4d`VHYAC~C8MeL9e{(O?;DlM?#cD^HmYEv7HZ7+jZjc{a=G_4h~hcZfeIK$gut?Bv^xPqo5W{{qu+Fr=iHN8l9zz@~f -9M%&INF5<~)eu_`SA*BH4`ZZ%$r9+S5(SY|2 -Qdrw6XV${mI6DVeHQat6w9^ZbC2Kth|b%0i09-n@aCSifTa-v=#Vc1vVur{kaLzuNESDrY3FGDfRD<1 -iNowKLNH0y~0n++CtSoUsgW_MNnqIpQ;}QTU#7$iW2>JSYDIW&@(_S`h}{dYz$Kid)2}kTHX6H_A)l1 -EzGu|Ej+y2sEH+M&svzFRUTcS- -XTCwt)ME|_3ezp{4w$N*`TD>as(ER>X{cdUQ^CDcw%7m19|M+bSxo9B`UF8y2uLo4ERX@M4e;SHx8P? -if|M{cD+yi4YMEeJr#rl_=#WzuSJ5Q@R^`GCRcZ{hkYmt}ruLghTuar}#-}uoxg0xbwyO%ZWQwwWo?; -W)FTWG(esScrSylPzZuahMBpnmCdq}pWH=V8*#d0ZXeMSA?otUAg9;Pc0e$ENVB+P!o+N!5Yax1L{=) -y?kS)8+_G`>ze~d5=A@8vX0D=c0V(XK8GgZ-5Ud-z?cwxPm;OWVhLewwbSV3M`kbAP*>6yV#;=4SGn~ -+6m*Ll?%4B)lSRI5&uX8 -VB1R~#vU)C*$qt2nn&i)WajFc_vu}IXx(c0vQUCS4_f*-;MReCts3xi`&x)U||J%~oi%8kB{OL1&$g7 -#8%);(=>hUkeeEed}>@Q`%-+$Tu>KSnMxZ2iKhVWcfk`>yu-mzJ0q6)3$tKtXkk7|$K{#en2WJS}Am- -|t4k=}-xuk!%8_WF5*7Zt^CaZB>7F7kP^*;k}&Ntz-T)t%gYM;kT4vqwR+j0z`Vjd}KH72ch0B~r+ij -+h-*z0^vQX2DZm=|}bNi>hZ&pFOJ-#c$r>bo4@v^gs9BC=u3gyfwq2@6I<@=T&u~atI!+Y8o1`r?t#i -gI4;hq4{fM{yH>&jm=+^$B$Gebf(ZQ1*i={yW6XeV%n|PO_i`+?X=+HBmbd_QMRp_#b|E+(xKh9swxs -`9912}YxmDO*ksx_Ssr!NxAg}E3x$5?q1HfOzuVyM-IL0sK6&1I(uTC2=n~nEB31QIs=e-$=gp65N8J -9pTW{Jw>nMEts|mH@XB||<@1~xL>3OPF7|*$yDr-jkJdV^(Ry#P_lT#wmR>Mz~wKs;a4jals6-fXZJq -lK}`P$onz$=5w3uh9qEYeNv-9LU_@AuWBNcQ(83c{}|(dHOU2qo~jr1tTL+#HNa|XOaoH|_`3qi%emURfXp2B -S%M)xw5OgX@jVLFH!&Yl!~~yN2&#B}D*H8UHV4bwi|F1hQop1zucz?C*fc>NQI#A9OKsc4(g)Qq>NH9 -MnUuu?c}4H)V>oN>h2A>WsSuj1GWyl4u2Go(P`^uDJxi=_qt<5sx5n=OR^`i?w^UZ%O<29^t53CF^v( --ak-kY@r#1Rs^AfR#wqHDQHPo&i$D_&N^PWEE>)##sd+NW#{>jT;@9gu(_q|W&uisTvlks>odfreC;K -h7?cY5~f{N34UUDt4MZ0Z`;bq!8>s+xDNFV1=w?|(R$_WtAZ`^%Hd^Y`z1Z%;n{wJLYmAL?Rtxsfi{A -52yG3q|*?_v!7)#YOMp?DF#LWA9b(hx5xfy*KaQ|FvmyI5bTjHcg)OdZ+I{f48g27oquZEftV%@y%N{y$?Ml=A6t^KAvtPD28I=)JR0=T-{Y#k!Iv#3)Pevw$Ydbdr}K_(r*R(^5nz0?r786r&_ -cxPd>hX*ETdY4Sm-%G{tb9y}CU3@c#Urs_S*{q=GplJsKT@hnF=E<6(z~^VcVrpFf`U-k)_9?sS{Njg -AMX`>d|}=yXf}ezyB3+IY3C4W^p*cc}iPmDP06=%E@~Eu>apPBk1HnPTmXQ6m0${`$@Cazmp7(@|6IW -z$@vaPDFQQ>>jWr)_gP%b|DeY*Flf_gSf1tCa&(-As^y;_U35()>?vS{gH1r_Br*C^pXCg63!hm7O+o -<5cymwgQ*u7pJv?93LK|u6h;>G)tEsPfpJ&&UqS)P-i<022T6pm;_@;x}5~DX_e$2Gz+F+p`8UIi-jF -?V7g-tkignGaOUh>X9kRpCOuR3Ra5qO%l!GNE`54_a`y2|+ePKCstxp6v$$9;c3jM~gT1W(@z$IA-%X -lVf9RCAS><(yDBpOxCuea~oV(6=8n=)cqNnFM%#_0VutY#^bt4tSoaeN7E6`-8sOO` -Z5r<~P(5IO{_1+X@Z`eFI%&HT1LEoDYI=-)zcGmYG`7n_Bu3I_rRSyu7V`GzH^p&(FX&=#!v<4!yD25 -uT*WGyyeONjD3@<>=U;Hkx|sgpjf4cff>>7%rMBN%k?I_HVhw{o%PE%)We|>$jkR1eyZi_O&K=p0BOo;t(yR`G0g -=-f<}cGpGJ%7GaW^w{x<9$jzOk2x%VVP6G(XJdl_rF4@4M5t%4j$=&TDib6i()40xJENr8biQ|@Ju7A -RoC7cX`7aWkrSb5v9%)1J0v?&fHEk-56Hii~D>pjBMPp{YeNG@VM^+bAh>U?&p$A!g)QBCDfbV9}^fU -A~Q~Bm5#QvnCHDhERq?s>sSLzCW$rrTq_`@NCS;An4Lr6zGlq)CO8O5^z(%J30g+S!QKOV+E`AQV!y9 -K?rA$hL{d!#;M`KLZQG}8k9aN#Trl@B!=03u^CJDGD~wKd5_rZm{9Ol(rg+Ll&9ID5%{TVQk^PA%Dj~ -^b5j_!)4z}vK(|jnF&Gv7%wWU*skdB)M!D4<35X0j?-4j(kuf$ydumPb5C}5HAP*tq{t-dh*n-+NwkQ -#)#a6j2z89a68!?Kx0U4c&|>l9AF&?hFHmem-B)6SAI2d -cGJ-Z8<*{Pc-~v^djW9KfpH2TJ#S>s(t%I(Mrz<~BLWvh>z;1Dan`T!f3lqUd)iKI-fei|nvVb{U2)W -vuba+H@K_S{%Enx(yyJye_GWY%0lItPdR|#QV?|OdfTQ|X9WV=`r6r*vC3Hg&fdK?&_ko+Q&)YtP3S~*9` -6%dw19^oY{+onObMOy|bXR4!Ayg!YT!f*x(I@fNzTXPx`vxCuL+aGs@$|T#%@F=_K<&CN_lwwA@(-jA -=jHu8MkD{!~Z@@S2QjDDW+nr<}u3_HU)!)6_zey$llZ -y)tG*;D!|Bom)&f5KuvlcV$wY? -Hh+qCC0oqBWfk=^s5k?$MN56!YM#1{OGS6B1D3_y%Crr8_XFk`#_CW_}u!$0X{8lb_B*vAC`XVDk^Ny -GRe!nSgZne&Dx2URB*zoA(n-jwUgebWEJ_Jxd=V|-{9)N@{n21bF-|6&57swM2T&)@U>G}HE+7tlaLX -B{xDlc504x=>=ytQzM$yI`%)X^7yQo%0&$OX#cvUb|`#%OiA3!S&0*+5I_;HF&IFM|C*N=NuI932N<7 -=8Vdk3t!l4QGNPb!qr{}R&dy#j5-7&*3zG#2C+IL3~_GTpF_wx0yV!;t}hb6)zcjBMW0~J`XLl -y*U=VU1m{rlgQLQAG*-oahN5hkSp(8Fi}T6Y(55(#4${LNnwWz7A~|HBB3vKaleZcg@quzEs-yct3Hq -AZHaklud$MiV)xj=_7$<*VOX{jB@#<0TWnGDSB<&Q-~W(;w>2XknpbfK}zU(j~K^z -1E;HKsUtc*{$JnJaJALQ<~9PN0u)+uJZ!23u@}jSSve`UJnJI#(0NXV_xC?FiM(thZusjS?jmS;O`_v -6!2lzmuyq2JD?&NfDiRCs$G&_`G9rnF;nTTzdK{ol9)k@w8NztH{5408z4!Mn^h!)8$1NxHrDE&#cL8 -14EkgF1*Fw&fK1OXOFZ|zjx_(s5iI<>G^tQ(xhJOYJR+b?CU$V|aA4wvK0zI4mP1C%)4WMQxE*2 -m36XUojZ;QSTi&-(b^uSSD^f)?l@&faBwT027G0!d;AqgLaK%JAY;4t$ZIm)fGVKyL10~yPvlb^F_AS -wXPED?`K!?cb3c(q-D5N*EvohJT&`>$2LJMP!U*AG^{DEVOz?!iRCb#$oPD*PWSQ&TO63^=@w%e#Euc -brzAq$nYrM!vbz5^@y_qxN;5_Wq#47tLO;f)SJbfz_rS$*KtQIz>M64sQU-(gMATCA}}ssq-a%6X2R& -hWU~Ri?A58#>4~y1$g~(q&LmT2+pi(Bf^V&3iEHP{=BdMO&rs>kNHh2M(+p?zw~IhR`+Kt_m&Hb_ms$ -)A7jBIhTZv+5A}+e_*358+B;oyXKm_l7_}=gPV6QXK17o2SHiN8^m$;JI)&iD5zm(3_KIo@XDeY&7Os)^Ce`Zua*lK1}`vd1Anr?j{p -wU&mOQJNyySbTp`Xw5*X$d#V?9m|&_!a)M(ZUZzH)$r04xmXu5;xeRedwq?1d-Dq82#2)6DsIqhRiIGJ={jO{zHs}k?enT*f8??_#MT2r`R}uJbT@ -2lR^T`IxxA*#f1MTLeF!6Ifby#;35VaMpHk^%`&2HALV9wOeN@i01ZHIVam37WpkUrmHA*3ecm|8rr1 -7xSJO(rZSx00bLt9Qa;Z=7$i%{2&r8j2@u2n -K44SY+p%$n|(+)&Xzd_ymP8CUg-bg9L8~0VJx=P5M<7}f+iz@bVN+Xg1`!wfa8u$!U$#hWeagl1GvtV -T>vExi0|hZWe>ac?>X9J`t*GuDwzs6R&8WUeNQ8a+cDpV8MWFU5 -IKA^3*3n^k8G?tGyXp|=2NI2FQX015uyn#jb*j_gm)c!8w>HhZGa$@&eStA{QJ&GLpC>3#-^bkginvd -vLEzY+z_&1{0Eyc)YM_jsNfGB#?09NXxC?Sh#wP-cM$#>c)*y{>+XX=E2W$+lX`UpQVcihnTPV!r% -It)285p{~Ts%^#=F01dpGED+adsK0NxsdwHA*{oE0Tv5Ov!8xGYx9ybD8FM=2qGH!x05qR(4KO<;$7) -J$g3l}+L9$Xfja(d*lFx0S(k@cJxkUbg?B;R|Xc~%9cvjWaKP$N@%IuVR6HoL=*lYc;9o~lVn&b-r=%zHDxKL1!-%%M8g1|n3^Eln$k}#G5g~CO4 -#A7IwJw!-ZK}{V~Y?NdSP>`>4;(s6!o`<@QEz}&SsfF@^@+=f$S6Js86!Gm_s0C1gg<1j?TBsFJ3qo; ->MZ#|oPnd@m28>Ucg&fK^#C}pE+KcBt4gK;9ZD>X~AM)hmIw;Yx+-QPwfG{*0V@o`2M7)hhn1x9KUK_jpctX^T#;3t4=vh>c$hDPvi6Kv67bf3;{|ox65K-^XD%}O41aUAOH*}Wj$?PEQK99+BllNhH}X7j%s9IHIvfQShD7BPF -1sOTdCmy&u+dl|V2Kzt&rCWb?L9TP5GZtLn;3Y(f;fZgGBsVtax<@9RYK3l65)DoX&&lZ(8l&ovFDkG -ys;wLR}VD7wE(Z|p1^TcU|PGVn&LJLvCl@dVs0;|W-ia8rB5~ihg2E?KXNz@MG*=jVmGf4X+T4j01yrMCYD1rS$gim_4_Cr8io&}S(v)afG)GT*n`}Bj`UeWsJ)UzvO3CdZp>CWTK!v#7vTd1Pd9= --*;+B~X0A0oU89Q2hv0<&y`L-+A6}GUB>BjKO1YQavbkgX=enUfgz%C0mG?V9>b|yKEtV7 -%+L1#4;YN@PPm%y!On!G^F7#^uz0>-0X}Cq*u-goh7+3$g3saEOnv4*Ao+|LPV(Uf0yNATdKaMKK6e= -W_nCY}!_LjFXxLdh@ek60bn@~t-(#YQ~z77|9-`=Q7DmLw(~hmyp^{FWpp0f(3d%2S2+%3kH}P(ct)g;J(33F7(yVNuN`PGb~m1Y(>7)y;MWd>Z#^n`1O(be0 -)>L|P9Ay63Ng>vsX62_RGEiOltyp~Zks(W1bG!AC$4@e7h#`}8Ho@n)*|Lc&tmvon}^MiZ#{}f2)uh4 -9!cn|3l{HQLK>>e)T%GwIbPzyGUto;Fd;oK1DAz)VW{Q14}qz_ZKkG@fX!UMFbf>7Z2FcLj(Cc{qD={ -nuKwtNnHorht>F3#gH9ZF7&L;|$d8fjr=VtPOyS4#cu?Mi2Gh(KbLD;U5&7_PX`itM^B(&{goT_F@xE-;qIXoE -WtU6-|^t7)B3eku-zxbeWPAZ1B| -i5!A1NS;H|(K1jyK*}<%_0$+>FMJWd3^#O1KO}gL2^UZsv5?Y4B5<=B{sl)F@Xn5cs`ZnXN#a(npA$y -#`pK2CNRGK}9Ikq#lqIjLp0%!)QWK6O0;vJ;4Sjw4B)r~aq3;xCh0ROfEmDOQ;RspK3P^CdEKC~pK9t -az1AODM?UOEnW!kCt+nA&2DUeaA}bHZjNYixa92Kl5P8_%;Ko3(^*!STBNyojV{<3m+}4Ns -L`(5lAq{YpM_6SS#B3v_J3Q>6U{QN0VMDhNr-|Hz`;1SMI~U@2QZ2+Pg4jPHdw}> -X2LMn~jq-i2duRn57NZU)gomr-Zj8P^BZb1k!aAvUzI*!YfP^26fI}-}f*A5^T>)l>RRCp*0f@dZ$F~ -);N8oU4;s#o$%$1^!WxnjGIdTvD+eTryF@P+$@0$-vfuk}X@g9=Y6>y<2|_+$cRK7f_MTr>q5n65HPkO?^*G&PVs6ndXGD6*x!Z=A(J8CmT3nDxfa;Vg`5&{ETOY5 -7%>GT{3s}IHIS5zcl1V-~(#a;fxL}KGChS6)3sg5j40y4K@SwzE4ki~10jC17K%fcRAfqbTgcYFDJRFx3+5h0i>*tb5-r;M^CM({lr9q-r3Z;4!79EvZ`;5!_fo-O4u5|$OY5kxl1(t -5Q%00I#$Tu5A6Z`m7ryA;PPxQe{wfB{++Pu82cNAV;?O_ubrBV2o0k_ltl>uZ;+n#B$)Q3zD-h9V~BA -xOIv%Zi7WuxdmK%+SKnU>y>QyC$Q38cDq2O6tAo}TZZ5!%nC~-teJQt)Knr@A_8nQd*wwLZ&5rT;zei0UU};%MtlWl%PTMAi0 -dI$!-3&L28%1G$BdzSfM}X7mQkE{CBIBzG+-4fABDn?7D9w938#E82m?2&S7GMUjt23bm2Klt*Hs>hh -?79wN)#GOMZ^Z(OxFbhTSL6azDm=p?o-NG2sGaDiUtxWR~@TCW#w%?bfDt`V3@?$U3s9a&vY1tU)_ro -7hK9--EUS%8gw8r(V`%8(4-v(ELIIq0q2w1BEsZ$pgVVrB?~v*m5FU-pwn;!2xI~5N7}@$ -{G$Z2ZjxV9J4%kEq>Ej!b`X4*(L@kbyA-X78AoxBFr~p26q4u&yVWQu~_fa5j`K^#S!Qn|!_*27U`Un -htG()5G_QPGuoK5xV>q*htY?Rd0Jpy=Rj@|lnTusLf_<%tDSQ^xts7XXaMExr`CBIJLutrJBUL`Tgqc -A6^&o2jggb0@uextj1Ra|asso2X_X20hlIH^#a9N^)2HcHvs0>knXDGR`Dn}X)~6DhEx(YA)Gb-Y=I7 -7{w^fIDUh!;!m(Re`Z&;%#;f7ZLa|W3X)F1)&O#*wSLeh#Oba$UH+vZDf5C1`LT$r(pjXA)>Qj*vtn0 -Eh8yohKPRn)iEW$c+O3-We8fB|jAdhxK>aUnAx#@$naEbp@v=rLh-R)~8I4fS`_ -}9=qd)qpM|xOC^0e`o0oLYeukFVuNB7`l=%*plmte?nHdTZUJ8Sh=o%}q -6PB?K9XUF3~q^D)<6VoFN>cF^mWp0V_=&okqkPAOuev%3cyBXiqW4o+1`+G!8r(0XPcWg48Q&0nf&IU -cg{_vOADFl6WqZ(FdGvvK`^YlXz|(X8BJ6tBLFm&)xp&6trQ9y4J~cunVsLYNxINM -6+xl8YUQDn&pBN^GTMK{pp$zU~#<-USTk($(~m;+BxLjfh -d)q&Sae{D0B{_0b_>3w6w9wc;b1RbrzL`Xqq&9f{8i{ea*zy4FJie<21_j@6a_?& -2ztE*eC=#j#Bwq8sql1sf^&QbgP-!6$@MrtWi__(t}j&`G11!FufI9=9EP$`Axtr(_UCN;R -!fnLW{9t}|NXdCAJ;1cY#qn%$?pK}OSZh`n2!d+d^syV=>tz;-!5KNfGXuky?xU^wkeh@tDbTf=F89Fl8>l8Wz*AZ~>=O{GD-(w%RD{egvMV*1p -07A$IFb&LY8jVVAl*xkP%`rnZFdhhjxAR~9Bx?1H76SmvH8?-#|nCfH*64#6hKZL84?%|pd$@lgF`%e -AuzNvn-G+YX9po%V4Q~4NNF_>C6ZiY9AWAkaWDvDTp?Tg{4Hb;-p87EBF65nW%fRr4!yk0!oYeJ$xQO -K?d4!NRuBS0O4Wx=>H$F%wgS$&P4qn-IS1eu@oA+*eB1sP@ku3xyAeoHu=_z)T+&Xqe32E`a}x3WQoCyYK&Hjh1@mNmQ7v)2mXDi1i -S?!@A>xWirnr11F+ -^1n)#D$ZZw~e7E;E)tT8;Nl4PAt9kB!t7)*ZDp@3gI5!)EpqR;s?KNUfrv#l`X#D&yR`XJ8nA%o%e`n -C-a%^VB(t`FQcc=cvcZ@T=C4oq)22&sH=qV?GVh~P%tg0M@qDc?5S$|=N}b;B;&v`qULF7em*0b-;Kz1{xSgEpOZPbDKwK&f#rLJr{RL;F -(kaGdRq3UcxxxMp4$Y%-Yg)3Ar27{;Y~LoJ-UR1)Kos(i2)#Y0~#pbJA&A7%;w>6Qb7@(AK(PCf+JN~ -ViwJ=#$(vb0bJLxxq!{Oz-Y-?(U4$9SsY^mTi;iJ05kf$0(#E3i>M>?UGT-RgwDEPTt*w>DT0(8fU$( -m9AG{;L$s+zcytnszM7ocg@DaEz#E#RBShAOPdX;3*9^miVKWy{`qNwoEk7Pix4lOrIB(7jm<_U-ZE7 -m`jUo9Lf<;Yc_6vfok=E76y>6aM^bE8D7a<YdeS=t5^eEs2XW -AR89Nb5RzuIyYdc*C4o!Cq8G9_JhsS$JyCS(jQRf{5xz4=#$h)++zZO|w>ehL{bN(>nj7&nq0_NfqbE -;VKZCj618Y27@VhTG^hbmjuH#ttWpDQ=NW21jPI?pC8$I1xtxn;nx+rOG5|Oyo!aXB{Y|et7_5Q#2=1 -3iD$jk%0>&gUPrBL49Y7p>HJ|aSNf}yoBd&M}|5WVP`R6ovX0C4TG;yp+tJpF{4B<5E>}oY#s_Yoy`y -f$(gfpPRL?@x3WafTGF&qE2-E!#Rw?Q7MwDe? -nMk4POp588P4}5s3LPS0^V&#HzOFz8`>d*F`=hNV@A-G2??j05y-KzgRl(es}vz4_$r0R2);_;GlH&C -915<)RfSm%?YbxJwaA4DM3 -kQa~mT!jV~=Szo(9A_U*1paM;(7QFKvEVL-@QlQ!(mRBQCvAksw2zcg1a7H7<8zMzvK;}b%#kJNsW8} -3~QHX{dO@}_dLE{m)d4Uv%h?#kTlwel!I?7wg>tNAdOFBD-4^!(KRg==qK7 -bvkDZ?&O-ar`%0@oE3f8ZjaL_6w-CU9LsIqXn$Z;<$c#(5ECDo7j`OvspM3fB!2Y6cI~(XEI~-%ss4w -FNQB{C;#S7P-Y}C8iW&D2SD)KadESs1IZ!o|1>^%G~&X;G7#z^ddJNW!y1zsFP!Z@eZ1W;Us#a^p1x!q(amP-5>xEYWI>{&E~2n;s2Dtiu!J5Cl@mbD63JIa{e}V@8n6Dh -dc|OBlb+JUL?6tP5;DBs2p=zx@I(AjZ&om3c5>RN{o+Av+y@Vs0(eh|lHQ-gMLX`ex84$o9%t@{=r6L -J$~&>EsHG1ZqcUGhelzp9lyv>%gKVJr9R8g$lj^Vki(dP6VIh5maPYdW4{qg%N|V-EexNH)Jq%M5S-F9kvXd1a)CM|jC|Y*a@ca0JzuM#+NG6iFJHk@U>}yR?MG?B#TMOIN7#0b7*4m)J%Un -5&1W!OHuvQXkvv@#tgR!IC0F#&f!GBSdo^-h|b}JC$QphVj@sm98!ha;&+yS4oFR~ot)5 -uUv37qiqEGEn>m2(i^oHT%^bi{0~;}H<^W>&8Olj|1Qs(XWP})b8Z1ueJ7TbyV~2!U(P{jVKNT2mglm -&`Q%R9Fa>lZRc*gn+Cyu!$F=KsFEGsurB6xls*{Ex`l>asurCkL_fm1R(QyGgl@_2)&p#9i^o8SjV^*S!oM>r#aFHSI -Qc2Un2umaI8QF@-IK8s#VX7~^F@duvRW#{BBgKR2pm6ncP!xfJ+irifk53j5s`mo-L=B;wWc&{&i)Z#tAMi(l%KL2;_=8T7S!m>kpRw|P}=+VC4w#%(I{yYJtatGoR3@tHMM0-P#fTi@m&8-Ep{v=+s&Z~CH;D?y -sx1lW=vO|!QlpqR36<3#E-MgO*CP7CH~R*yrjI&`-vcCjAfqzWZgkP)+y(?&2Lc)<@-;OVX++k{^t_! -uKxJ19EafGMHKZb73&hy{VjLOxM}XT6Hs2;rb14poZVbQOAm;~rXQk+^Q6ndwuCwa4gpDNeqPf|Q(h> -`CaX3+7b{RnNuSkRieOX@9(5Qc_0pTmWZXD9IdL%kTY-0T&s`xUX5tb40LcovsnWT3w8Wl$b#E$SjVq%Z_C=bEn) -`nvMb6IKD-Pao)H-G)LlX@zy6kA!m^_d`xi8+!-_2Jf=EA{z+-=%eh3;H##PSq^vL51roud$tW?JV3` -jL2M7t5;>?CrvSZ;ezvb8LpndYSTO7le;#g?RU^@OWi%lsE2+0G9ygtKb9iaW)J7ZLDgw->g+q!yf;JQ?OA`UbX_9&Q;;0DkwR@LnL7- -1iM}y;QWDuAli19%8n9 -RYG4NoJ7^iPaG=BOn)oGJe(}s)o#JxUtvIwrw%lzP@U{o1yz(R0NT#mfqzY3YBFfeu;BmBDdytKtz3m -JL3OQ?nY9c4=HVg5BxIlEq$e6=BKKg*+xZ@8Hj`7y{9UpmM({d-jo@$il&hD_0_8mWVaRi6;j@>MJDD -jTItZ1gzT`Vd&23%BvM(JrJdS~%YJdR=1_l|GBc~Gsp&R9(e#XBB+tnMxq8yj2~xywWsP?P-jt64biI -ztIzivuYSXBBs)Dx4@ildH!us61k;px7NdgI-Il#0*Fq(BQr{xjgL~X|A-h5qtOrTi@X6v7k -$vuC31Zk1?ov}5VdEb$|(ZhSO=OM6hk`@e`*}yL{H3pX!zQ{-&Haq;yU}Rr@;m2HYf8k3Q7C5XLV3GS -G0%%4yUu>zN0p2)6nAWF?2lybzmySHm5Ps>%6Y{t=j* -TL_WsoS&#FxSu3yo=diCnGsYZcV53Ij=4evE4qLi2z{U;CM3 -EJ2(!%#&_8@dsdJ!w6Df_>N0O#yq^(2a@b)3(1Pm$O!z2==O_du3qLegm7L-RYo}! -7*S)e&A&~YGIK5)W{F*PL2G)^R$(p$&6C4R(|FtmV&kNJEbsefgyKFvP5nYal)CpD^@n;Z;GhA1+9>50m|=@4l%ss<>4Sw)13F8B8gSSG^reR -yG;Ui-*Lp4~$fSXMP(FQlrZ&d6u~DA2O`>#x -rqJ$kgs!bNmf3+v|F-c@grX1%NDy_;T?^b&6!=2|HEi>L_K`O|0rWeWW5@v0~`-+uGW?d|Q0+tG_OTY -fW`PLIENtK6pQKW{G{|Ej8etiQzaST*IxUY_?(y~4Xl^ZW<31yuXLr=RmMYyN)rZ7rn7k3V`*9tOSJR -habD?xyFLS*8TpE3z=`MR~8(?+AvfUp-~AvM{f3iY`hvWzh>%6RMJ`vW;rfwEf%1D&r?SyYs`1zNGNf -{MEEwk^f&%O9KQH000080Q-4XQ?aYPpWj3P0K&-u03!eZ0B~t=FJE?LZe(wAFKBdaY&C3YVlQZPZEQ7 -gVRCb2axQRr?R{&L+cvW3cmE1TQ+uu1mE>1;lg#n>9^0|Az8gQvo_$o!am{ihNW&e9)RNT5N-F>Tq8k -t5NrIH-kv&nlJQ4{sfJURy-Dq^ri+mZKoz2(Px*%s~QLjm-qFHj4lu4eAqIH%`^B -FlylZq5^THc!jh*h$hoL!M@mKT#U`6A$KlIr4YNve37#$`E*t`f4jB-`@dJzk*Nt_b~wFeakbS(2^RR -aq4zUYf6}{E}qJPoyv_(bD96ouo5T@IFn{N0aloB%GZ~7DZgqnfj0?SyeZ%jH`=fdP2Tb-ZBU!p8acGR6D9NiYQ?i0?-8;N@@7|2eBYZrZ<wU%6x!y3WC)moQ>r?Legvf{oC4a-a_o~V$c9aL -Q4Xpgbm3>TB6fYIjPli&c)3c+5&QM**|P{222j}_)8u@DU$9momI(fm#o6TDe+mVkJw4B7+h1pXnp_Zi>g{3J$SI$Y$ls0le}0wc=X+O-#z#O<$r`oW#?wzkB%I?EK-gdt%brN;;2@H3l(Rkm`i~e!+eZm3OQID_Q~Yg)~FopZkc-`INz6xQ$j5g^VRtsu -cdZh!;?g6``D`ijxfJoO0d4HIV+Cx3??lOE^79UyhQ2l0Weq@|a?n?jB$E42& -!1It6$#AHW#m{q-@kwP*SD|Uot(Y?`1}X>qOFy0zdnEX-CbNOz6Bw_*$asZ85jBNY(}Oz-P`h_mO6&> -bvA`s?> -Rw*r&6{=0hPM#w%*(g$s|Z6V~}32ql3Sr4po6;5h3kNU&^?Aq%3iW0cRM;VLi7EmuGY|z|*GH@oOsZl4j;Rql;s~3MvyMR+q-Rj(GnC%VL-SA}{hZ1s;k_j -iMPwf+MXiVvwrSG&*<937JhISf{`!wM86FDf2(440=lCMdVZ$MZR8KXfPymYFqwDefRVMF$^6=MVzp$ -h6r(v@!*)Vb5ay^l~CSAvT44iyuJV`&_L~nIZJRjBd+Qsi_cTS-bYKS)X*}?^dd_0MM)7vCqMzEGj#X -_$wW0>YxQ^`jd32jR&#U^EhM?~TA)=$CNzyTp>Y*Pt6c(U4PUwWOy7x4izE^kiqdo -ETAMV~sQC587)mayHVYN*dblPjPnS_%*?E$Pso$2A5=KYF`Oow`NO2uKrz9h?I?@LHq)|I1ZeaT-xesVmneCdms~ywCGqiz!` -O+#e$2nm1;n-B_o~KD7T4GKr$XPGt4J=sX-vAv&~{Zjm8_$c*0seK926MQi>d?a=&kr5`wGIlGtDmA_ -gf==}Me!ff>Wl*;7~Ppl-O44WAX2gls}VRx7|TzI!@2CzQF8Khv#u@Yz#^;*au;3d-=wa;;rlh;OhYU -G+e#Ds!;VVv#PcQ5gJcONF=UXqJ>9UmVk|j?5`F&&F4Ak-(!d$aI0j1Bo<=VQj`Kk4v^y&v1uYGy4KwG -tDDY0cU^d^%vp-@#}1aH)jEhcE!9CZNTN@z>F)+HhU;w1)MWZf|i`rgV7^ed3N~K0oxyM#BNHD>M>PV -P+7oR%tRtyf91P9{@XtP3u*R*230ELm>2^(Uz{xFEe(#tm@ub?$V8RqkY{ODR~$Rvp%dqc?x7+d5~~A -sNX8g^OJta;i_j8Q7dzy#aWBLQ;NUJW}d?AjZ*YwzJxJ5hrwPcg}=|Iz+_#8`75O`DoL+Eo^qA1->OU -)Bzi0b6Qy$po$gizxnh#7V8QBJu%1+EquN`ldr<|oF2h-$tp`~R$vU>VXI&H&SMQkMR?DH3;b(%ZPRE -ZvOEJrOi;K%1iUIsx&5hK@3YVe{sGSLh>Hz46WdTEbih`ii0jCC+EshmFHYvnyd1L#wEICw~07+-?c~ -rLwTIvuVQ=y=wEbZrC4iBABRSI!p$1Sn(0Sr@;4c*gt2m^bl?zSy>mwQVMk8+-PomTbR2577sjZ9N%@ -X)*KFknM5H9Es-ti;b$PX|CUgU`5@=<1yp`A;MREsSoJi*&oXfGuVjr^zB)#>FMwI1if!#%YQ|8X8=4 -=&qvQStRnEcmAS%{_{kMit^C&m-K^0UIfdg$lS~tLS>w-G7ecI;rJ2k4vC_S -2FwL3MaN;-X}msRi7q$)40-}euk|`E~f)n9WT?oL>0oj-hp#_YCWG`X89&1vjv%1Di3_jZF --PVR(O>svoROI$6zZPQ|%>zLzr=yr^#$h_vogL1wTJCu6Lw$jWNPs4pm$E-{C|A6yKCe?9rU+_~ixFv -y$q58NGV{T2U4_euZ`}UPz;}V*F#mqO4*_p;%!;^(K=%u%)QB -4Zh*B&eXb#%cA{zuREPn?o(sSnpZh;hMaM%t7eopv -FzfRpJv`CJ&;yA4h9<(H#da!{k*JXvLZvs5CV`MW>VTo?&g60H>Nff=AGiS1SLS`dYPd32M0nwl`GUj -m+7qb*5GN02$w2fZ8e+$C{ZM*=>OB%C1mhQdl=><4$BydIt$GC<$kSVYe6N|!}e*`$Dy~=X-NS?wW%z -5 -8QsQx(7zzMX7k~I2WZXV9=G~7nCXXL{3x`%>9DUF;taUn)c}we1!dyDY1Ck%h1CW7I<)fmR#NcjXCUr -Ioy5TV0l{Yg*}A(WWlGOUlw0njSPUh{dHOSvg_u`XEG7DUcz@U(jO20@sEFu*O@rTz!y|W7WNDDl2N$ -9K}5?VsM4lM1QD5WpMQ{6bp -rN-p@D0fGbaT$xnOA}`zXFakZ+4C`sfiEH_N+sHs#-y(koGll1m%ZrP*m+0B-tnSUFZ0NYR2Ba -q`R>7?xNvc!QOq$H`XhVd)V6(K>7>#X@Ms3tgXyE&#RU)(-`+p-cSC+ER&~;B44j~p*&qLvmfw**&wu -{G-$rB`|RpUJ!;omah%FZycWOA7T^$zmf$DX(4H(%b2W4s0!Eo+J}dpv(fHA4oi}B*jXV>^aqGZgr5* -^YFY&x|X4rVOCLexo*7Mxr7<@QUYig{c*9}k(qL&wBdWo2!GT01RdCBCS@*-cSGv@DdftvtdZXN;gAe -x$1JU)2r2Urosx*93OHRAL!;E|lj9_?8pbiFWaA@=%r<8f42u~E#Tm~mu3Scmx3%Cvklz!et9&eU -T{!#kCD25gZ)o?!8mU8#}X%qlG1k3lm|7Mr)CQGu+X^ZIEZ~8TJ)>XeD -cMBz_p%s;Oq=0qo_(SN)89S;{!YjiQsS#p+wK3hxUpSrdlXMIk)vb$>uqXkDY)XDWg7R#>@Ng$4#N0)F3j9DJzq8l7uhxaZFenw3~XosErqS*a%pccH&5x&kJBo3=D9Y3zDwjx&pI4F{2O|JGcu*EO{B#ML1Vby{lDjc6L^#c}2 -;QE;-6Vkknz5;&%xL^gTb5c}clzqZ#*P7(jMk=FqXNNTbS1H@L_GiZj0b(&(J7`YWZB{< -1P!mz5(7L#x9nM~(hnUcC_*CNos+oj}C-np+!X6hn1eR0+jN4c>#u(=^|JNRhxbpMq{o)dbFh3c`+l@ -OZ?1zvm#zuSiiOvl+=IZVYm{LiCOcJ$?tDL+9YcNuGaj8oyMI(bgIHLKRwyy77#=Sh<@}&VE~3QHBNn -B16ra#tqy&UA~4E#2Iqa{8L(DuB}r_kbKH#RNabjwkdPEVB$#@y~x+u>@6~`?-MHGS5tTuLvgGJYoFF -J;1>kk?ePlSG%2R*G%o0~IW3q`4Wi1iZRAEHloZffL6=AwtykDX@|1t@#L -FP^knkwVf4dGO0txm)eE&?!`p)DRaDG{SGQI(Csy4d#AxVE7Jb-09#Pf~;QpCp{{%$Qp-nHrR-5q)N- -0BCRcs&dTN-~MTed(Q@Scn;NE?3ON^u{uiaPPRSFvLso>d$ntQ&Kn@U`Dt3J8qWlWk&Bn -B9-rq&K*X-j~-1Pxh2E{Zdi*iIoBjt2}&E$Do>N?mad&_mM`IM*@tcPloIDRNZ_yEynY`&JBHs*xUNo -4R{3g}!Lo;7Chl<@nkVE^1hOoVZr^L1fH`K1^b6-~v^*v2oq&C$o!I -&HGPeVoFY2dYDDL(5B&YUeZ@xnALLF}b>lXPF^a*8L+SLnBm<$w3aqKL9>TY^%w=cZyo5U<#aTij5To -@v{O*0I~%Oq33~w-R*(fg1@^0N=1%Ure3E{}o1{s&mzvDlEfYMPa;d*H6_6RzHNBR$sNFb6tBz8#h^N~ -zyMOCCf$%G6xcXe$86Sn(WzQv(8p-PQe-|Z)DfRe+N=Ps=hft%mB)%#)%B+iHjv2`-IfLS|j+CaI!5hf3-yELzX*rgYJs`jY!h -;**>q(?(fCXc}#IZpf3>|UpH4K`oh$JLiq`qBrh*L{2%S#}@hjtgVQMWI#iJNB;C+RHBq1-#}=kpBP= -{gMhJG4sm|0Fq3L9N01g1JM(TjEc78%PP$ast5o_=roLAhouNNH{N_>Tp=&^>zVr3t02xGr*gOd)s9dWNCh2a-c}1od -LJr7(qRcJsz+e6KX4Eo`C`IBkgT~QN{0Y%{^=*CgRC12b7I_+4(uYzN4rq;Ba@^L{Wri ->3rdisMYXPoIAi>fMc&p*&X*A9>;arQK+hgrJ$m$jKE#__kH%{6CiHciF-8xWW=2(m54ukdgYmB89D4 -WSw(b$17b|H%$iNa-6>oh|~G(3m7OLZHQPL0YlGd`JS`i!$JMj3sTmeyT^=8m~YXZQYecF -%Cw7)=+D@%g$MFY;;(p|Z+>BkY=n1*eUi#j$Z(xZ7PZI(HOFdR)be5C*E;`8Kb6?{%Q92ZNo5N#)Z!H -%Pa0R`;Rh^L~U@2f$;eFfZg%>2VNqAUyb(Q(~!&*bYeO8@OUn|Cs015PSd=A_Gzk2O=bz46b$P7<)Kh -fZGk9)luYdy4iKQ+5#?G2sdEg*tyd_&d7rKWgu2|;&;+9yQANT>7-!Y)7j4mS{a$uf9}aKloxOC3YNH -l1HX%IhhUO#a27abls{(hLf9dEf>@&a{5=ShZft6NiQ;3o1LCF?EFGC>D-Nc^=fcNy+Sd1tF1x)9#X- -`6zFz9}YlG*Cn@?7FqDI#;7)Cf_H0~L>ivkib+!p$2`+bIc?JP-V?kehV-oyTKTT{p!G{Eb78-mvB?M -4tLT!IkiWAU6$-?#9CA2q#TTlh3BD!AEoxYTsl`wJf>lC|{YVt&M -?@fD=d~%u2~Ns!8U6L-_4r%n;EVxwF5>AWq~1inmu|t$l;8ii@&?o}fltQg6r -Gf3MSo>kOnJy9=E)2pNib)e8l*#q!Gm-{#1a-9h^4rRt5HM;&Yg`}Z)3RY1<^lJ42JLR6~>Lis7++Xk -6P&npMQG!=FPZVuLw+Sw8@Lha^eJmX&8>RoT(Fmz871FxuVgEh%!21O;UZRnvcIVB^B|JH}^E~78Zuk -2{#02WGb)z@u+IL4ql!SQ;w&L|*4 -a+LTeV#{8P;%nnfz+I1c;z}t7$R&xbdrc2=-YD1~s}FiXKDI(AeE^ -il%12Nlq{4OlIRxQBz0#e1!?R>@D!a>D^6)vhUsI}93=9B1y~FAcI3X#$ -0a^dfEAFC0HIX^cS7-`7D2P&}uUF>&bb2x2*Jr79S%mWWILS~U9ZMqI@kq?*QTqu^qh`+7qZzT>rI_# -IR2n4kJt3swa3ij^@7q!mZn@|(-cTD&fDjw7$0>*XfxBEMX{fpJB-7}ztsDCxR>rIXu(w -ZnOW6o(nM+0m-k03vK4)M9ZWp%h>|EVJQSKmm8KyF~mbD=d|pQQBLVTaKiR0-rPTBcAvG -O;Y$-73<|&Usi+XV*n=-Thl?n)@V!`g;47FMFOLx#W^@zOKepnqO|mjE8uP@7gDPTe5d_HA9T)8qk8X -0C7&O-!H&1e0XeI!=BS6j?1JV%lwKA0gT%kZN#44!x7=liJiL`;r=%pryRV(5>bRpz^?9=&Y;I$hO6& -O*J~H!BUJYYtBeeEKC&2cXNy&%q{YP%V~KJJ3_@6<5zlAj63qD8Emxv^u;4Wux}8fx*t_wuY*)LOtN? -^t06U1(G)5P3wOL>)BS>OU!tJOr-8#w#JYC}$jw~M>@OVyR$WBakC>XK=P`Iw7+p+%ViWJ*uTEstXqb -23gP9adnLN;>|kD8;EHV0tFBiEsV2j)tAIFV2q13xkH^6F9$MbKqF+&QVCN;e7ip&ps4}p -o)oAF(lY)NS1*al2qsN5$>6f*q_IqRNz=d?yFdm{qiT`6BX(-rLq=#)Ld0{&JcBDGxZ3kKlG>9+4^ke -8|{t~p~yJZ=nbjWSaS5sG|5_aKKQsL9y+DT6|siVCjEdb#M`*IH1kc1j~$zz4NDyDHqlFlX>si2bvB#>=j$YO2g^XTcSAkRk8(=;i80G}}@rKfO0A -JeWqi_X)0dP$eX#th-Oi8D-aH>0i7r4*wlRmQrZ`yp}XA-_-Dt~-=5v*;+KF2r=5iJoU -0K!%Uq6mqYqsIdkJ~HsC8#>+exr5Wtl@gkx4E~%xI1V`w#0z2QMOJ~@P~uIm-!~MKrk7@!*9Fe#fcMbM5btl$vZ -{5#q)TET#8B*kd`verZ%;D)G-%fQT=jw3df8x@~pld+zb^n89^m8!0l%j>9v^S0{9VIl|cg1^-PGt`1`4%o#JxKT3pzFd6A?u -4+c+7u^&Dx`f!`n@sROOl -X{w*^zeXy;lE2r5sAUm4AZ;&6E4r+;3?@XW$h6d~R0z!2|&%nI&R2YcD@D%Kzf-30 -l+ZUY%+OIerUTL9XC-jE~E^lS9{zTCT6f=8(iqn2myr80cYjw2y?3$_qc*a8 -ng`!^ZnyPIlB%uff^}WI+uz>R58hg6?8Ohr5=&tEwXG32ax)%rm^uu!Z(+YNU-K0(XQgL{8o6ec_YQEh|3!zL(bpY5@-01mKX>^1!r1J(N^-J_loikMUKI(?>a66_+^emWj9E!TUKz>0ohbv$x9~c}1o3>TO*7drioD%-wrK=JufQ8g0kg% -%Px@jlzkBe=JvbOG?}s(_M@-yI#;gf$RU_xAT?QuwT`|VXxVC(z51n)F>8W6h)l+ETVtQliAIP9k&z@ -rvrMO2zmqIKN|3&!ErZW_rZgG2R<;04Tg-d&Xl}n53STGtNpntgBbH9#|+yp%0FPq8!<2ZF)J3$a#Cj -ZtB6G>i9CaCM7mPPZH+deCXNB-;{5%8Tr!V{C -GxIbd$juG0#g9+9n0+I5-YGsd4z?K>0I37}(=~{>215h%s@h&46v-HKbEf>$B-)R=?>%8F*Z@KhmiC> -%7a+)|-o>{XR$=j5IzcjZwUrM*L$wjnm{O%vu(iYaJ$uHndz-A(O;P+S9@zgya#HAO`4Jg`wF>5*Ktg -JjWvnN}`kK6`p&s=g}qEZt`MQQX2MRN%3B@n*5pV6=FSr*(mgg9kgCTGvBX -kU5cw$Hq$uK(MA+351wvYEgcy2iC11&CJA@}Wm;%qHi4(Xb#Nga>-K2cri&e#*f7Mrm!n2iSf4;rmBf -xCqp#><$^*^W^1biK@eDDw4cM@Tp(cG-6)I3M%Pu45u;O83oxW#KiWF%ob|2yp*ghNG$JNK) -9vQ{@-022e*I1B@6m;u*!x#W#2d*=O01gC3SZuM=J*C~f0p7;d#dR_V-sgA*k&>aYw?96Y0$v-RV6|v -NBw1w5)fATsyhVXxt^7qvh4%FV)0cI?m|CM=Byp!$j)77<_z%_#S^tT^whla|l#cu?3O$78dDOV{SNY -iB=Dm_NhbrFyE`IWMBiZXECFGvR0ATj-zxQfh6Uc^@b@a)<5FcG~^c+^kU!$#v^3K5q^`^wR^h0+qXI(b`w_y<1+Eq-@?mBe?3uWu1D_F -`XlW9jbea)zjK%41>?em+w;_C5(u0eM<5#tx7n4=*A=&KahorNNA!U+l)lo>s+;K33m`L^y#~?6(lvn$6?y9gFdv(8#eAV=(pYGe|+TFapZsv=kw<11~v2SZZp)lFs_M${U4 -YL%Yx?S-u*!BCEP*{kH=l`-+_d4K}u?C-2a$?9tD!Tpj^5_c|%Gmiu~e*=OMwy&ezcw@{EYZEu$~Q8E -)?_2>5wG%Su-?J|6-ue=cdv!70SYzFZNin_@(NS2V&p_EO{d?B5|U$cn^FRs@$uiFgi|4gSl_38O$~exno!#lXh$ErptEPi~xPO{s^KDDhs -%`>Evy2AJggs*I1%s7VKsaFiUk6H$67cpVp~bUE+Utq|BW&CpS?Z`7%w{GjiO(n>hk@XQ~YY_Y$Cs<9 -Z7R-!Z%M9Jl@AAc#W{9VF+O8vvvvnP?( -MSnlua@iS#duC915z~3n1UI_l!&I=C{7o7kyIB;(=aFFrIuHj8ZUK2&RL+;9Mb<)WCD8JfdKoQK`qEt -QbH@=eEX+w46xt`_5zd3Mm60;ODfjVE|n1lUckk0%5*39jH;h1vWgyQ5A!tz^kbf{Ugm2qzdUqSd#pW -V^J!In8hv|o&zcAh;feLas{e;$+C5+WubrTO+7QC@8_?${CTKVphrlT8wmnvgpk0~2D&kH2r5~i1|e2HR#gp!Pwh-W~eFk3SM2L#}B -^Nl;LtYRBghxy8{zYVT`WOcXRA4q}c4NSuqOI$T9^fZ>}3UjfrgJQt!%Sr -11ByOJ(7?myR`AHuBKcYLj7OP_u4|kJu{4YzG=Ynmq0jkU)YCx)*MZT*jZP#?o%HM$+?x#(HU|<#AGWd^WqdvPx+`S_O{AQ~*tP45*>u2=n{cVmB{H{C&d -Y=|&1MLIPqb&Vi1U)EYLn>#0m!0FWU#g3@%)H|%cP7Hg3_`~;XcL1sgBQUw>?tzt#n;fr`Z?7P42BuaSsUth!e;;|X*wF@d~BG0s&Pi^(ua -awG^W&f4Wb9#$!Wn>UmOwiAeaOUZqSHfta{IUw!OXd>p}~81|9K{^TSwLx|F8O|o$k{q;Ep_;owW(`uPX -YL3)em>vv(Z{K;=WxdIi2aZYd(kz(@u3=_6MhPc5XlhGX+xO@gU4nV`xgTOxC~%$IIMwlv^d+Qf6?ww -q2wAA4I4NX;3o<>M#K9hwankw`zm2&w%W>jmU*&a>y)lp;27h!-fC%{Vd>Q7G`gb*ZZ-NbUJ0D4a|dT -dnev`LjWe4LGe!X0(-Lz7?KpaKV)M=Q!naSVM2aL`ToDrVzyfSktQ5_UyoN57lc=$Cjo4xs=qKljbYch(Cq3A|%gRc>6>mX*EkAWUzgy@r( -v*oWN7&$y+|Z04ye_lC1WXv1-p1b+l}3RW9vaB_ukpmA1DV@UG878=ddr&#&38!kVgv(&V1@wVU-a$M -xAJI(2$%hoo}X6R!iH2G8G5RN->NO!A6Fk1`tWe;KAEAq#qi!cO}r^_V^<67m!n8b369>Ba>_-{h;ImX(Lz&?^4)3+ -X$~QP&j;yf?8~F@y<;ONG~iXQb=(*MT8+NcuZdv+|{8%CAj#| -Kj6ZKB4#{ehUEhG<%~PD&#^JL0S%fLX6$#SqYivR=O%1kT;TN)%kZA6My5 -T6&{<|^c*))ShX}O+pnYJZ15^jx2J36<&0dY5K@1&cc__BPQPkGuKQ^@Dk}W!Q)8PHswGP{h5PjA8z) -jj7R(J#*RDCsVyNhQ?&-UuV)085o;m(MIp4yWX2l*uJA@C84imF6CI+vz7X -Yivv-<7ewdE28^ba3BkyZ*DkOx~d`S9|aQ8~{;do-bdWX~D`(Vl;kcP}CBG{bndMg|>YW9* -KneZgo@lBsC0T+?|gP<>dZ-Z@}515IQZUTu1war(kWu93*{aHPz#NOELP9}9z~#!a^&vpZGtmX!ioI4 -E0iWhw{-xJ-&+m5!BC&mGGHi6dFCwZLeFVDvVzSg?aE+xs$3Q?>$%wED=ym -aC!M?FAzOU+ZDL9SZ@aSVmqb$s$RiG^=*UA?#Xhep#-R2LG{KStW!rtAz%3^a)a)j)WY7aeNT9EQI1E -+0wldzHJ;y=G|df9W)A&NO5*B8YxqwVlu}fel|?fK-95J~~P|?yo -C#-n%*vkvP;s5EaBO1+f;CLZ7Z2)D?IK-*H{udrPzFjy=0&ul8KTEU2XmMQAi6i3ydB<8_MKj(SDD7C -pG%duPFe52U`SXD<*Yn$$NBcigJ$fd69CwsvZKV -mf$p{>P`fV97Xsc{;z1QXJ=)aSLN9mgaHL5##DT}WYRXZ^47%1BIext*9bMxa3K{WP=oy0>;o+rT(fF -Xf4#PgkYrX37psN+1m{Jo$?qHcgUhrTBY0|C3`IvN@uW!D!3IY<_n`-`_urwUbXd(npND^)G;SF@vvE7UEeRq2)CaNTL*q`C?xO{G&Cod#zHXHRq64-7^xRx|#_ -mY?!+Zd+w!9kLZQ3VO(oCoMXL<&w-rA82F;Qa)CbtgG?^JQMAS+t@C46wN3BaHC_z^9I#XQHnJ=%`w~ -uq@6p2c|qKWU(|{&QbveD{g(2=46302P-~MSZ_z(D;Gh?YB`AhE?gotw?w+kpTbz5MM-rZ{b6O=Z}fy -#r;;;FjLP7c&gM-lFyM?Qm$r8c%;z@A=5yU7~9_-FhkKgB>~pPL++7Fey{fya -Dig7ABA}#$~)nre8HmsIQBBiFj*EQBtFsMZfXmA+))MAm2Q2Z=1B>-ADGG^sIX{Ly=LV0xB -VeCHP{Sg7htW0yYsA(8rfU509b@`K6G=N)zWL#^cwlA#0@p81mnS)jsWj)4@IComWW`%C0|$>-1#xheo*FbE%_eCeb7S9?05)eXJru46ZR_p% -rD;#Y{i@l~GB@;ua0;TFU+lNE$E<`B}D#UV5^sWUdAVAL}FE+@j9jLXlO+%?h|T41}z8B>B-`pg4OA2 -Z@*ck4lzaU=*!-QD{_3v>Jz;Dqu0q$r!H!&8b+RkJ4qCpz8b!QfmT2M95XD#LQ(rQfMT*lFuOS1nmrmJLpYpBP$m$Yd6>pRD*^*E9OzxAprw-+RyUf8kM1A4_^^N;F`;O?)`(C!}1G2!a=`z1CRE;8-+ -aGP(o&aKMn7{sE2G32NR7H)f8bFPnir#DsN-u^;9NtssN;q0!_`q*|dm!TG(Mu8!gJjS_ICM7YA9gRx6=S49-nuSTSmOE=>3P9WSS($=voH -}cUpXmiurmGp2*+REdxMYRfwd2%7)pk9v5iHE!6KLe=h -P?e7c@)==J4U=l!*z6%eWw}d -o!VR&X#Plnp#v3_$v|%OJg5TE`5Las)S>(Kct-~M+Hg;%>wOpFIJlE>K>yV8=55 -@a>28<^=84xR*(o$o@q2hX+j+%FTC1Tq{A8q2?=;%m_brhwZHJngTb*MXSKSS`fPc(4KId=djZV@&+p -Dt7rWAr)TjTJg(?Om;*uH{o+mE_28d;#rdE4I4P63{uM9;G=gfEZFD9@scd;@0Ul9CF~f{nhsiemnM& -DB5jAJ_=~X&%?O@=-NF;hZxBN(&@z5HKP6Tq-*dx~!w1O5Q7QcGH~p7B1SBYg&DAcWkG7IbAV%X#4Ss -j{bI)r9)QN7BAdBBy)YLM>|o3TWkC9L-=$t@do%y|Hf{#I=`_SQQ}&TDE2A@^w7||@WtCZ)L;@zEz&g -j#+v68AglF1BQd>n_35$Yp<7kv8{LZ?>v+dwk}$W}nfH9;nvO;2==N(&DDq7|OF}dR33#1B+MZg}MNb -0ml&_aR)Yh-Xtm(k^Xn81_Oa(g_3IJU__U=6!5Hw7xra-QI_VErwl=&UQezPImqzxfd4R2aSH*r0mcF -`3Fqf;D&{yKP@{r$u|zfeU!x8cB$+nAOQT7AsDcENj>1+z&v@m4zYiFA>=%w{S3x9(}zLlGD+^U^$TeII`pDn!yTGdk-XW$yeGr3Yd-z*rXbp1rAhX7?$?AUp!=7Kn(oVJ5c^ -)9(SUTjKx3Y6cM2gUK0VSffv=>yr|{_-^w~gJ*JN1Y(=~}xcP;LkZ_F?ki1yBh%f1~iW8B^~5Dcbq!) -LmC?urDxFZr}BGg1!DI -ywkUwNBHm%dWquN`ybvA<1jajD#k2!j6;bqylYQUmw9WJt}N>Hj0ziaPAfvOI=k3t6(~hYGbzRh7>%Z -Z_OY6kJIXlW+)GaOO^5T{Z;(?tdHc5*2Skau8FsM`@4+ru(l90Z??qA@S3O|c2^M?CT;ZlkMRyq#?Mu -18G?;=~k?W1~?1S;x#bz5#3$0m9-c(VXmbn~OJ?Puw{Rh#DHSE*X1tdlRD!N-_#RljmtkykfZ|u2`8H -AZvY6}QRro08W5u;Bd>P-CVV$AL{((Ar?{7rae$^@ml9PTP-22m9}wSOK}5qea~rYm#D9=1g!werF8_ -@=k1wAbiV_vR*K>f4hal`6N{r>fnU-YhhwW2X}!*6z)m0hw)eIR)B{DKHlS&hK4 -*X(R%qB!*6WuNG^IB;j=UYGYvrP%YtlK=>_L5YXcbX-X4h6@PbK`#nBCc4UBl;T>QhT9MYG -=v3WtwUwb@lq3{aJ;>s}}~(c0I!E;PijWG%d?-Jp0$WtUmqi2TIWp9I<(q$4v5qGJhV#X|+hVs|!%k= -=UG+dvix+9oo2B?R(Oe;aS#%5Z`K5$Ufd$H%pdrMW%V07g(45eu=-s>MnC;nvoCQ<^ho?MPbYqh4K4q -04MTU3yf)+tSC=JJj3@7vEwMss52*&46+$inUfdzwY3JKhxV*UzOZ_TpSSEmK<(u!&nR)Qv64c-;Ezw -l<1k1ioo59C{tT#@UFGQ&(Fehvg^pvE%;#&m3X;VI@(Tp=J$v}0co+bsl&fVKuLP9(;oG=vQ8OqNm2< -E!5(GTKj{I -nDxU5Ke#BXJxt&zM{%iu6E%Q{TbK;DqV6EpCqmMsi6G2IA2qiITf|)-)(i71el-y3(b4-!F%?g -r8aJF1=|?$`^WZOF-({Tp*CTSHfACOM{N@EnRit|V7tgr^q&0!e3r$%~@V%L@((8z7w=9 -xFsYO3{@;|Hi>YLhuAN#*+GGtq65CL-*nP6Q@Epj;W8dYH`=Tr~#oc4e|r6F^2Nw($i@G$y6RIX)WB9 -)lu@s!Eo$xo1VwP!BRYMDwaNs^5@OBhd?nz)Am9cDpx;?Q;`#!_#8b?j@tuFh58ce0Z21TBi}9I_=G5 -4@p6r91}nT)@`R(6_^G(5X~+@%BgXaOu*9V2OPP$twiwggt48g>q40VN~`imc{-r7>%}q+aT{jJ -8^geJDt0I5>xwutm$P1l_Mp64G&XN5jh-gm9i|oAduvzY)|K|E%JRSwA-i@C+}&z;;@5fyfr>l|Z6oT -s_)goP0|MNJX1M{e^G!A*UnrE+8)oJ(8b^<;dYyQ>Jos$PnB49&AASfJSo`*TKvpcC6y&@ -^I`<1wQ3c_)84%u6Pv5{pNWd!CgF1AiM!bYw;pEuq<2Hx1@XKz()Nq*E=*X0P`kQGUviKZ0-6ao|CAB -TE8Z2@uC%E;yn=gyS}Pkh{05bEVKKBg(bjvW$~%Zo*eXUpSTc*w$uwS5%Exqv;P;x-M-huX -!5n2_p+5yN@L9y=Km#J?a-w1n$~UwE_)cTGHb@b~Hm-CV0SW0&wB~jtdXN1c`Ka%qX$0<~GzG9>&{oD -VOf3t>)i>yeBWyzZIVw0<+`#R%YjIFiaJJ_tru)Y?KOC#Ejlt;LZdazW&`yqIHIl{%kX@$;kiSC_fel -E`%KAT?PHKUN2nLmwN5Fu=HjgZ -OHvXhE%NET!IP(La}*~3}tUxI(C4oTN9#oG>o(D67E<6AWH0;gzgHH=J{w74+QyKyL503i{K_an(IDW -EY@AtENGR=I5$%Y3I=z(-p0yMb=I;EpR(sFvF%aXKq7u}tBUsimCd$Ie%B50w(qYQqul$R*rGaj*Pm{ -TOI{CVZh}$XS+-^?O#5PI*K73K!Lk=@O`GM@HA~v+YG_Zvgbnd~&#{EP+MQflRLv7+n&?qu&~qkI|Z!z&6?$8kIf?AZ!GezvdFu@%--d%e=TC}wertj3#Yp#o)jc$zTT6hc@V9(X_8e(Bm -HK$FpHzo!2v!O-))$9i-uj3MUp;>)a$`^ffEMeP5sL*_>$k7gJS%QqsxjO+s3SBQ*lC_WBc#lAy;Va};@_;7WU9SJ3b1VGYN%J#(7*NOjd -khbxrNBlCR7gr$_U_jV}{;1*Gyu^M -QAJ&vvtvOx!1#{2GrtHk}!;A|`3Xn+t7c)}QYek|mnmTZ7+5z4g8{w(Wa0)pi-fF5 -UttlHPyatqO9KQH000080Q-4XQ;2if;kpn20FONY03rYY0B~t=FJE?LZe(wAFKBdaY&C3YVlQ)La%o{ -~X?kUHE^v9xJ8N^>Hn!jWD^Pk8QCnG#-KLw$mUnU!_iks>O{aElvlDx0h=e566v+~#Y;|Y<`#lE$Nq~ -eX*}F5l@=%t@gY(9D0MI;7S0o7LWl`oV2nb(gXc$$nzneZ@;SP=7qI!5)jC*ktVBK;OyXqa1-qLP6@=3oZa*!16rJN91u8hfA90Y@V|$rXf=ggdRK&( -?k?BDQX1klt-5_TQRsdBC|A&8BN|6Mb2j+2iy`FV0i~PX$W-0HavM4(SkOZ;GGv75dce<`imZ1OPMqF -K;wHrJ0d|@@JcH%WGv_-Uh-}=fQgWzPJ-Xdv|u%tR~ZYDLqVp*^TxlWJn>s>56RcWyYU9o^NTyrH6Di -M48&SxJZ8T4#e)G`$pEffZ`eljfh}mbc^aYvvis%TJidAb*9lywaD4;Uw{X3N>pQsq4A+~R;wJI-#Ws -U0yvgC6!8N~mJOTVCfd2&Wp8)<7z<=_3UwkLP#R+h6@&>N&;i7N>jgt_r2ri&8 -qRagL#^h`w>|ao{$*AfnQC& -F~pjc@Q1^R)) -=ldoW=zVsRhxT%MNM5hyr${HkB(P1n~h-M2~u%9W -~Lh|>p_`$HSsA;}=Tg5F-n#TiN0`02y-nG|2}6_05iZwT7tC0%7_Bl2=lz&k6#v4y8g_RIPfIK2m7h} -fJgi&dq=4)qBXP*^{)qUWRGzt%$S+k^(D$x$nur**CvTeu${EJaFSmxpv}RlPr2|dL5 -0JoEeG_HnBNC!ASyHYbzZH_5aXR~rg~b>A2u$Y11V2HxyKU(d1VPM`&ZKeDbGlWHDv8)54B-%WYr1V> -+MT$#G9AifS)!S&Ad`I2JdZT$ugU4{uq}Y4vpxK^L&?CsO$pa|OVqBXx7k3i4{d~vWydtjKrX+&Gqfx -kW@yg_+@{1($c_h(7p(BZY}nSfgH3R`iUEiOU7?NVtosyj;_PK;81cZ1rSF}24x6+oowX>rRcP^c`1n -GcdE^E0m+TRR`*J$D9S)7T12tem{HpSR!NU>JPNTqVDtf@SQcccphyiwK=X;%a;ElG%h@p-lg5{jX{1 -4{yBrBmF#gc34G5qIYkp}kT$zuAqG+`t1r$5#3d5~s>&x@Vk1R*&k`a48C{VQ0?|S~6 -j0vqIcJek;2lUr6Php7*olO+5EN;{t^tlXG<1y(VM}2`Rgx&k>RT7CCEx_2_J+VGDA5H%XH>}lfTgzA -B->2F7#>XSP~Wd(J{vc9#(%y3=|ioa{0@fFg1|fwBm}t%7HRUz?4psSVJ0F}5Q>TbrWN?*BP5R)yheV -zxVRv)GOihrA2O&RK^wX{mfyC3H~d!6jjpQ^(i(xb##Ev62j__6;(V4yn~U@8;;;Ox^W*H|{8-&MmQM -%68cE}ECb|(xSG)+6qG=oybRniPsH>3`l_jGQB9;I8Ngh3 -Y;fb;v!^mOcRy@BPK-}XG=O`1rI?lme>t)o}k5p6)UJzxnPB<7K{N|@UVo+T`Is<2~B -{ZKbo7$96EeWAqRCl1fsmx*cZP!%}oq_zn463;#merRLCN;Jbl3Y -HDgP<<{*fFcUOh8yE_P<4@;J~kVP2ZWT^q2Pek)bj>Nh{jRRCPexMs6@2ijaq*oOUWw$y|ofGm&FtXr -lC|i^onl>71=1%zTr+9Y}uEaxB#knCYq^y)&!v##o`Np5+erd;b^J7B`F?o7d_>^fgULuCN10bA}W^=(mt{;04piDDD8=ZMk{PnI8xa_XYNZ~A+*q=bc2Cn5qNXtA_W#yP$!;Q~Be -ZX}Jh($aBj5Qv)Xn`vd7Baa~L0+PJz(1O$;tZVx9FWfE5LBue$3Z8o*y-5rsM3y|Ed{L7HI^?cn#k}R -Zy<_6WJSLfwh0;(W8z;UQ@Gp;DirA3Fq4e*38-o1{qm0O7LOszipu3OiycJ|eGRQF98cYsa>FHdxWf2z?0zowOs!!g94LW0>FkoyKhKr{g*_HDr!wPvJQoZxL46kPM@u^Vq?tVB_Nh~5S-gC1A3eBC}PQQOfJWZF- -ELqetcmGu(z>$3z!M{89Q;=&`B5ME11(9Ize!0HO8Wd(xXx^IVzQ49F;<-o*F)D5=4_wGFVuoJVXjae -oO -<>MdZooAV;TfR7vqP8=BSD%fYJW=mx`0b`^&bcU@dKV#C$#vmfWo+xJ3{^F}l*kdMRb%3Q -Nv;(01KOpL`!q?R&IWq2HWjfZqgS9Y@3-K*vxT62cUMB;mg6Vd>$<%7H@6{#mkSw4$fGF1>A@^+a00M -%R7bQ+tGI${dxTppog^ydJ9g;(fsXCk-MwyE?A>I5Rs{dHo5|VbP10iN1Ci$=2SNp9U -zl&iCitowbs$`0Vc$-u$wt-9@Mg`MyVEsjK%>b49h`h&BDI|88y4cw(1lU)dE?oo>t2j62aJ@Tf;+t( -_MJ%+ibB=wq?4K693morVQ{Lahhc=B}=oa)LKgcowJ$f(-{C{2cnt`gi=^@-0{ohB|QKs~8Pyx!y<@rolxlrxHU@KWe* -eqb^#kF&U`{Lm@@74kUSrp8ac<<9tLUi}lBJ~7UtcTfMCM47hXxy7ygsM};2w6}(ZqSt<$S|g92xn$s(bXX7DDB7h>qhna&u@67niT>b8N0XZgcLn!7 -ua_pP^Oe$4xCE;(T8pG^D>tQL=NL&Xd%vUXvT(TM -aa{n(f=ZdYy5%rC<{m##ZBrmspg*g%ZA?g(H|pbTZvGC*^~d)gC*&=ZcZf@s2cpbmip09nIqV)(Qqy- -rtSbn~)qW1A%{w3iNA&0$aqpfS`n-#T@7rJnh2CtPqyBw$cV7Bhc1L?7FpGIpTyyav -%0d-Z9-oAxA*s%~=ZhEGYg+k={DP89A)ehsE=^P_sndD25&?m<0)nB!zH%rTmWOXM7CX4EqFk80Ksm!;Lf!rA!D2)tyJPc?2daPGF@ -OT=e0J%l7hgUlG&3d_R(K?s3;$jD>Mpye7R#!-dm*Ab=ft5Ip<+Of^dN34p7#-F!Q$cA~g^Ts>(H*|G -Rd(h}P6MP<6GaaG1)|$3QbR@)UJlqzre*DcwD#dQ-}jkYiMMM$b+Lms5WC#e+kt*t95##KCyh7HTK~H -V?$*U#>=?zB`6HNSSZo^L=;>LI9B;8(zY4bEbD`_%3Obstl`;DHWfEa2{LD&tYfZbeaCI -L7|Ibve+w1w`7viF+=zG-oE?r6Fj%5-huT}fH^oUP%hjw(-+o3r%pc!a-dPMRBNxwQedLfREE{x@ys# -WXt9^6TIuJ_zC{DGAO{hD}Z&h-|A(Sg4{Yq7sp6o2G}Dsplbri%oR==OGL5>w~cAVv~{8RLg=TNad*- -K+6mV61C}YTMM;!@ZaKyt=cY*}60VgFAL}QF1BGWqsTw(T8I9-y-RrG}p(GxS2lCehEm(uK&Qa?Aqu^Z(t+UmpAjWp!_9Nt7x!fD*#@?874{%of!bASX&YlPEEv?}mJB# -}v9S(<%%||u+)bzs1H}yV${!A`^`Q_s;US|JUKpw~cC=jw1+FZf>*MhDZU0i9#{$P_et9AvsEvASf>oBoUu+!wKTt~p1QY-O0 -0;p4c~(=>c!Jc4cm4Z*nhabZu-kY-wUIUvzS5WiMY}X>MtBUtcb8c{Pkd -3V<*S!0vrT@htwpUw9KKQ#&Zsb#$BH?^VQO0!ef`kSMm=oQY75Y+f;}#k5tXk9*wZlp>aTY)LlTnN%u -!&;k(O$B?f-o?IA!D5yTi5$EnT-2yjIO9KQH000080Q-4XQyCiPfu;cf0QCa^03!eZ0B~t=FJE?LZe( -wAFKBdaY&C3YVlQ8Ga%p8RUt(c%WiD`eg;Psw+%OQn>sJh$i%p${^io*ZLk|sv(vU-LAqd$X?TTt8Gb -1N!O8}?-V=6eOLc{6OhtDtwL@hwVg0+O;UM4(|MA -Re8_8gDH&A!2!{>gK@sOLd)b8-e=0_BqEEbFJ -H3w2nfLbv^MruzGBAR1ejKCH({KkO`Myo662({U-AHBngDV-bW25VecwT4w~ajj_6@6ZLlZH?AQAIL- -nYv8^^Dw3}X5^=d4HA?XTGw`y^*1HXI_>aHY__D*R&5Tn -QpJ8_yAOHXWaA|NaUv_0~WN&gWXmo9CHEd~OFJE+WX=N{P -c`k5yy;aL@+b|Hk>nj#MQK%ra*8n=CMSJYYw;&*BX%#O@lLAR4&fj;bhegRw`an^hEY9w5W;ujgXHOt -y+lStvlt8D>x&Z3nt?mQL@w@)N*PI^8n`#e& -|iUP)7Xgg@J(T@>Lc<2T)f-e!a7SPL@x2M0F7oK%O@%0kZNm!;sIO+#YZJ39dZ;*+>C=*l&2I-Jau0; -#zU8`)yXk2Z}vs-t;Qz|44>Xty!1XW{G&PPv)tAu6tjX -LmXhTDF9(zyI~V*5g`-&C=q6^`7I37PiS6C4$_6^Fij~Q0N>t3Ai-~ec6l#xmf6113o3qQ_^VIM}Rwa -QLrgC#~5m_i-j-)Xe_5(epgPSC*iDF%;nntYJ@>LDNhYCJzpJnoc!Jc4cm4Z*nhabZu-kY-wUIW@&76WpZ;bUtei%X>?y-E^v7R08mQ<1QY-O00;p4c~(<^ -umljo0RRA(0{{Rv0001RX>c!Jc4cm4Z*nhabZu-kY-wUIW@&76WpZ;bVQg?{VPa);X=7n*VRUqIX<~J -BWpgfYd3933irX*{z56Q$U*do)#?t7$nWrc9F$` -c#Kk*dF7_h?_A_E>%a1)bR)-`nYVv`NNhRNGDbYz@mis({N^jf1?sYgt7&9%WfYFTJ$-TfXriqw6!Ef -+=6Q4a>0qj{pa(#iHy0TN|d{veU$0cDJ13}dv&1cHM;#GDqmJ&MSjNK)PUQ<3S9>sEp@UI-984wOc03 -HXc*t?-WoWcf@H4u@~S)8PPG7?N=Pse#{=YYB8CgfNU4l`Y?M5ORxC%IkR}`Ofye9LzKDJW{Gf4?L65 -{DmUtLbom)R6Y$O&(~(q+nBg1*0bk-Tnxyz>^|&esJ_i+Tf>_AxEB*!hbh5V>*PKvb-!8WFIGOf@r*} -e`$r7H7jM)zax{qSw06x`V)Sl}sS>&VT1G?h9G=L7_3)}w^BzIN_6uiMsx#$YxmEd-G4d}wl{J)K)Cr -yc!_-i<2_9a*n`(Q9Qek?-HC)h5q%HJ4P*+`BcgR@o8&Nr=YFpnGct7O9J=~t?e+1q{?o_~Dq-F_sxH -m!sl{s%vvnAIpxZ*ERO9KQH000080Q-4XQzC%-#FYR503HDV03-ka0B~t=FJE?LZe(wAFKBdaY&C3YV -lQTCY;5+cFc -7`-6&gAj!;Ith>?XaWNhg=|(A+W}tiWQ_fJD%e)RW1-S1V);HYQG@lYrf~@9nNuq?IaI6s0xR6+{seM -X4%e{9dcPMu?i{DNDqY&_c;6tEGrl0#PmyDxPc4O9^APalPjnJkN`Bt~I+7xv>i9-K^P%bB~$j`~J23 -3o&Z8T%HwxoW&WhG~oH=&BgDZ*Ehd@ESC)ViV2B|Wxx}rQkJN=ESx;hQ5#jqbSgjr&Fd?UBxh -EAPr9;S1zK9dQkyK(2P57Ui)#*tCe$}Gt9v48L9`6OrvoucC0rc8vD(Y@nA-X*A3h5JE@or5)WHbdOK -@D&s%)2tK8r?fR0X$Mx*iYOH))z-8Md@I!aY76Z1XjhY*_GDA{39f^@O(7F#)~s$5cp+|@HkhZe3&Vr -2d9e0uU1Jp++C8$d|TQf*Np2ZLo6eBti@9lD~aX=L@uLO<_yB1AJr)!mb&D0AZQy-*%Qxepwc-V -2{)t{GgG=k2jMV#r@RHDR*?#AyBzkQMQnwvhZ)kcnJ3_GCMPaQlX38#g1bGf3zME+hmV5wPR4CDLYda -R83zwjFkLcQ+c8`s9Z;>O%y%FBaIb~lRjXYzs?`MHAAZ -fS$iKI@!HJ%*lTTG~&%8P7<3q$srViiY9Xi)SYN#5+=B@&o9`?o>^jHhX6ZX=~zbH`yiCb -UW9yr0(BqXM?mtrO5ruA>6Jw54k*(Jt)d9Abdf_<#vfTkvG&Tdw!GWZq_8d8WO`gM!>*@mF?*`|2TL=NLQeOD5W>yz#8kuh|4>T<1QY-O00;p4c~(=B(PW@(0{{R!4gdfo0001RX>c!Jc4cm -4Z*nhabZu-kY-wUIW@&76WpZ;bY-w(EE^vA6R^4jbFciM`Q=AFP90-1ZKrTj?LSgKt-EK;7Rr|QnYD= -CZx6MZ1eMhn#|HR1>D7#v2B1@mX^M7&2X3(0iV`J_<^ -yP+Yo{(B57=6PM7>j-=A;ZzhEC_-m=f=R5$Y~>VibeH$wg`ZJjIl4m$+8;+tDaDTu^M>+nbLq-D-!6D -*BJ;4nUJXgF1O9uoHjq^IcP2n(mveZe=-KOC4a6Q;{HAgT1oyt#f*X?q_A-u8qBjf7***6=b-UtB`8z -{x5-Ax$Jp{mv%J+6Hrh5km!zOYQlqge*c~poer)I#-lTdxuxx~$6uN+y4bpuLx>j6MyHHJ7tc4N7=xBQXKfvt+tZryss=zhCW=%{yFBmNdXL{5OX&%PeDFz{7Lbb+KQ_obC!x -I3SBWjy;k*#}o^hhQW6Q<+aDZ46Jj}1CO%2iO2?TWnYtJ -v>BBU~$83q^dn^_lGFY;}oLy!^Y7P4_|fWka1=u(y8(G+g3E%%H0SZZFX!tqb!rYra^+3NB?A@B}(N? -rK3w43JKYkD&r@TMNs_fqP>6;FRK8@on5QuJtUInIcNGg0nMu%3(^3(sej_<3Zyb)3@+j|RuYAwiX!kf?0lN7 -%{UO$*lS=D`u>gyaZ|*CFGi!A{W5Y$YNdd!eguDeXe%qIeT}we*yP@s!AS+)=JlGbKndN7DnxKCR;Xm -K{Sc3U$M-16BsrgV>}InW3sQ*M?Gh?WMKH#{m{f*` -4<66!La_a|=6*LR#OeCa<_Vo>&|M6wd0JS4?bAqoL7Qw8h3&xy3 -vG2aU2mG$;OsN(v|QT`=es`9;BD0tZZT!34{Y#$@gp}nJRN@r66_?_Ul|5_`b=jyMR|-8Gs%U`8=C&; -LL1!CRw)OnrDD+6qvJ2l}NSdFw+v!UFFopu^|8L?7gF$rGQZ6^uA$aX7j^nhlN^gfJ3s8T=dmu`1$sj!|nu^A^Le#T{3}v~s`)JsbfevvC7W#mscprgrZQaj$C(ntadbq=$ix?V -!($0n*u~K3maRk1;wj+F=s86 -m4^-1x@1|;L=k2k{02g2QE^ -~=XR}NKpgVtWa5)K^8hB2!L1;^3?}Jf3dlfkEhDUaHM?oCW5w*xEk(MD)V`w3%E{W>{&V?lq%P|H~2VWrZzNK}?4s@$3bv0K@A5 -klT<6dkQhGhn!)Jf%`NWmq$J^*mEEFLR$%sU@B6AAf}W@{q|+VYjip2(#4t957%!5zKqL`}WFHUm7homuU}QVy6=Wi8f}8`OlTbWS6~=Zi<1^Uy9FH< -CgqFrY8K`ZZ6|nkpEF42mt_qcX|7WKx5MXEXPUix<>by{Q_^YYK4woW@qEr^b$Q~N7E|EaER=P~t<~f -`%;Cu<^Z*abX^L0wwcBIST6@9Brir^csrPkx-j5j{>t*mJtRpzBMz{zlhVbbURAr^5-2q -bp(se*qs<=@e6#Q9%S~qRxcF6O~AaD7@JfQ(6`0tvHwgTvZ%vL%T0i${8TPsF?Ali|6&;#yrd|Z}E?| --BF(p^ -L8&_^0*mDD}9(YsKey5B1~)ou&BiIE*~&@(+Dj!%8LIM**PNhN=Z(<&Y5wvJF~cm)o4P1mG}m-Y`T*J -ch{v0lgwCi+_1Q2}#VUjFL!RH>EvEs`!y^zuFSPquq>!bZaNe=z+=U~19vB!#1OX?qjR*%K1>Ijtcag -_3R_wQThGk63qpPBktq8e1@6NWpXZXd96MDcN@2nzw58q(s4;zBITrM@11Pf&vBGC@0{A -ta+CHXg$8#)648Dcz>=a*_rKo^_6xh1CFETBQh9eU00x8k1hD96H2R+Bl7~$5C|JoX2{t;3{~U{D@b& -!b`O(oO=fTGfmppjB(K=UWr>AS7H^p)uX31%d+&s+kz20Q$sZgrm>h#-h|McSHB3NbeNTl#LVOnHqz? -NB|1EEzARlNiBS@7|@n6q$0_LO9wMI5u!r9pv4kAw=KYL+BCjevNj0tSFQW+4ZVo(Jy$Rq-I^Iw(|dn -`XN}q=DY>fB`863A%-8h!c`9JRVM+=+DhDU7{y -}2;utt^9$bxn5HSpHbaMn?Yi1XTT0{BdD1hIn7L~CI6ibB`pSeVqM~xsp3lvD*C^)Vw{2sXrSQ-U+D* -{b(jVwH9zDGyXqoaCa=J+t_5A7uQz6JbJqCzELOSa?Dg2zVG6mS4I`q*IrNz2l!F%D&Xa}tc%c -zSZEc)64wj8JCjKJg$$k>pBwkRP_EV8*4=FzOE(@)*^e|4y -(k=5a7nBzz#)AZ+!#xCr!Z#$eJU)!ri?Ehik^8Wc1QhQuQp7Y{OYs1#SSanm^0mS}`G#lzYY}JjIDe(nXpVo{12tN9At$e$72{bG!6tf6SIX{f{|x<7>vl5R -XVD%wshnJ}7(`Ug1xQ7+kT>jHk?FV^V&neWuyh2}Ji^0myRw0DqD_zqRKNlS6->q$dI>oGd2@C1m;Cu}DE_n<7ZXzxMsa8`qY -`QvIW5+5n(5j`jyUpY8C4~l1gwTA?cT`N42x@DgJw~$%Tc|*eVrZK?7y467&V3>`=rOarWY^;@Jp%;n -vDmI3(=_-^W*R+ALk^STIGb-B-D71}~L+;3Fx#^&7SgJGHH+JyA_&#uma!BO6NINf%f%&mMNq=`r?+2 -_#2W!AO^>Bu4R2R*UGi=H_SpG>wYvo7s%M3W22S}E|dWa0BSxOI2ZPy1DQJT_JKx3X3N}ShOJfQDA#G -kT38`H!5AX%ygGm#oOj79!#$RagJLJqCI)F=^O(Tdd~DB4!l2s{Q-)Ztz*^_s1Ql*+-HVd(==l{`!y9 -TzmE@jgGy!_6(1hvzL8DQEJpl0||KWSBPUL4<_K+X4A*P`GBe`f(UZC?(SM%s)P`*B&YIBV_S@?GbLo -@s1u@$l?caQVvDBHNw#HLsPCZ+E6wK+ry%xc0z}T88c`e8w8;}`l$y)bEZpE$0a>aPr0H%!#}L-gSgZ-4+ian;FiY~7TlU#1TV-LPoPs -;yz?Lk$#SGKRbefWwrJ>XMavgSTeFKIBG3QbX?)H&M+pTq=bYpE%1cu;1F|cdnFlU)*h^R3B5>`4?6^ -3A^4-}zPQAHaU0#e4)wZkA31PYd|RxX$Xl7bX&czDa5S?a86*RYr=XA-~x5_D1A2cTO!i#rO~fPu}SO -DlLKT#=7NH^Q=tCD{o~tOM=Xcnh|Q9cCanXKSv+XQxvvvwdU>=AA6EhVvCGT)m5U%p6s=m#iC*!jZca -O1558Jc;%xK^;#+z0VzF?rLi%sHkD?+;-xr8YY=L{xwRs+}n$obxEPMtIg$turqlZPATXH(oxbvK|#b -FDce9B4oq@Ka}WkL%b|GF&0{L42wn3l9KM=G+Qi-%stc68U|9jd&QtT%U0|(&e5D7h4K;|)CQl8^(sd -^;ZQz+h8p1-b^F$+Hw_Slku+mHt_&Wkb2yepf2oTy#?M!C^w!+bJi10Rl?&5M?0 -=k@~v18|?E5wrLoaq@c)pdzGTh;Ls7yPLZ(lwcqZygXCl>yz6Ad3`?@LsZr-C#w9nXAC%in2=^2n~Tu -Od-`hp)#bx!Q?wr)Ll-gfkgs&lQV6q$n(z1eFX>KEYgTOGY2%R-S|Uuh&GuUY;;5S8kpq+F&1SWi?rk -~x0A;3r5j4`d}KX`p -yWjy}WzrT_LK%wMSp5sHSw=ep2oF@oI5;Tc3vIT>deFe&?BJXlO2?kM{OW}E_NdgD4K;4~>bzXMK6T4~LPY(Hh^Nw$`&= -`r8BUad4oM0&1x&Lq<=vz3-yhZ{#^X_L?ac5DI+__jKHUuANp*8AfoFnVzj!BHRGh6j64pEGTt&plrY)%m(bJDI4+@nVW+_i!q4(u{ -JJ2ncI4d(qUj^k%8-&4l2z+^__{@=e%5Ea;4Y{_ElTw2z5tm#3O6B0|-{Y}#CvI>6eDzNP_AeKPx$kR -gwWTcj-ZhDJ`g?H)rW`+paO0i|;|FMNkNpECOo%v6LvRojHy%@52z0V^+*z8D?#z@7h*) -{hNpB$7U2g_ur;9ZxKzJhRmKXv;XAy(+T}szsS>?2LR0t@%p_`mqrN3paUR1>ie&G@e_-6oNm_Jxb29 -|1%6Y&PDTfy9oiJ`pyPS02xZro|ojz(x_Prfdo(t1a43&&rXqYMYFM2j&v%hg9i1K=6Z>@uT3<2%dI; -yZ&eiv~_FV67BE-&XM5cWyL^A2cmROibW#O5k+qw0buXV%|BdzM~IUx2XXObuo0f!@`oG%m%h*zbILG -Kr0W)X9o9)>70jLaKXIbAVMcRpb*G>(wXSJGbwvV{y%3~@+ytql}W4FyM~lz0TQ?nYEei*eGgR!Tf{m -xv8G7v5Yn5^L)!`T`t$IT#L!k7xy42A9ds7Rzf|aY^Nd_o+x4R1tT`T_7Eu2HcMm6Mvz$&W!$K-z*Nx -6?2&VSVy*xP1JP@6d5v_8<>(*5^{6aP@;tayg#pW=xSgilrIbl=7dnd@c;MY`aK%W!q7P?2n4zr*Cm= -OMuo`Hc=Ol_1&?dJ>}*s)wJB}tl6VAn&NgDstNEum72_P7NxFJtyuBn9+Xwq)sQ6_C|%bCP0K9+)7D0 -2VVm*5-g6RNT-c3(R&6S<*&`6}&v9P``9a%Bij1A|b04x&~0LJyfqe=38c$ui}j9<^{iivYzYZ0vlwO -2|56?1Ykqz)Zd0xDSrjpblzf)!ZWKEFKZ~qHeE!_j8?eMTD|A2XlKB@%-9NLw*y*o{9wC*nNwNzOBD! -JASuF)38t!dNQ2mI=VexXq@cY%44nrOn}HxzKwrEYq}^_8~2GVV0%zBVM{VJ15*88B{CwVb -XayQPRk4cnqJfT`-Xxy?k#d(&HO^HguU4fw}d>CtPDiO^0QmAmEp9iC7j6|R72xvI}T6|52T7*wx)$? -TWH(yBOWpDn#LirKu$Y}bse`sB4$m5!QX?!ucUBWR|5TNU9(iH5?pY}IBhId6LSL-|F!CE)_UH4R&FU -bUhvT8@I>$=d0|o&BO2LJB6Wd1l*v3ztbNRn@k8tqn1*FxQfd$-E``h0I&6lak-$5B+}iVwkP#a1*eg>MAf@J!g3iHp5u-7 -pKn=_QKoNXj=y|6oxb!F76kYGN(Xy*NSlv>S4x3{Gk3V)nxMHh1Z2Ql^JMl}NK0hlpmcH(kIM)@3d9oerDu{@>bNS4)}>(Nz8J5lW7qpbqueC|5g^Wl7Kmba;K!Ki>bShInLanbVB -z;LD~yenbcJ@uO*T-fy(4f~0(Hm<~2<%dvr=Tz{X<%6~g-Xc1YU!VO4YkY+l-!_jy}SqhXmYyh$#kRM ->$fR=aWsZVIgQbR+BGN{BQo+s<`lrjXJ=39NX8xIV2rwgnJYyrQH_mfdWs@)s@Q~|;@DNV~sfIeeH7GTrwOHx@iv9ylhTi(pMwFmx{Fo_sDAb4&D&otzF%Ctx%lbg?;kEtFdnU1D;)*a#CsjFwk7$ -!`ma8Gc>m*f|M+lmIS(#-F>KWw8US-WVPrZn_c~%1cupP73+Q~#L$MO@y4*J{gQr`(d;j*`#rqFGUR+ -w_*$Dr2@vmRrzW*M{hhA#CUv0O~ZMu*g)M>+5fzJt$;yuFX93xW-@+W9_V}#MA;%`5cn^YE>^3_G359@W)s`8?1zdb-lqZn -X$>|qu{c+Q3p8;$A7wMke?Q3Y*iBpEmr;?;5) -e$*C}+Z!s_Z3Lvg1-J}Tp57h;-=IGwjP}Xql+}EpORe -f@wun1xKG!%j&D}$HG)};?P3{F4;6@+v>SgXrs@=lqTNv@9hGluV~&j5EsZ`5;HfKfGw0~W*WGmLS{T -W7A$$0Ph6)3|!%g5fhKE|=ad*1T&x6rxa}%r{bz9CM|F)|2JFYslOe4xVT3sWJyQttL||1i{~<;JpcmP=}W0OTxa@tbe7aS(NdUkz=c$*-&^wOlid9F7a1Qv -o{WM+sC{_nKZ~lBe5*)PUub0&N-OUHvthL8;u@{C1EY1wVqxOvgz;A`p)h)Gp40&| -CV|xYp5zIRhGAEk8+++oK`F&Pk9tDw}tlXm^y1XaZIAYdbyZbWu7tC1jw!9VDeDVOJPSLIPvh1=C)09 -coqAW11FjM&?$s~V*L4Jx%FzxIFW_3uCODs-OJ=&;TvQ@=$GP?SBefRX~?E3DTr`LD?v4RKo^!i%e9E -06`z|vd&tD7zr>I(nfOs8{{EpT4Mu&QBLmq%L@RZekBZd>9VSLc0RH9r|$gBxB&=u1W}HCpan!8M~Y5 -pR8Q^SpkTbUJa{<9B$6L+~@I(*<5#b*Odp6J#+O1B;su4p>8M0fQm2WQ+z -kJ}Nrh;lDuxX}bS;Or>OjY?0jB|PXbyhsh*;b4;C_8&?({)~r9=H?^T$=D`nJSCadUDx;`8U~H*p)|_ -vLae%$l_tXKxYc|)264VtPp&8M=jzu-pz~M1PH&E1UQZvc_CYWXz66^#6HXk~3Fx|9>1T&jgmv(USHm -l#ImisoBuJYa|JpEO{^Uyrb?8@)^B_StRB+eI%i+2X>zead<_nVpf>E{J#lLY<-%zOujoO{IPS{c{6S -UTvj`^IBlMeg7IFrE8v`QX`?A{`B^!)U(dNB$f52O%n%v)FL1)ODYb1;c%QWr8*y}EHM!6a2xI&czfn -@G1NU@kOIAXpuk&^gQT`i03jc&yGxkJShsN_{ZKQZ<>0Spf!wGr&QV`u|+uDpfa4_m#44r_;leJ$n9F -RgCQFF2)dZJRwlZ(9}*R`gsA>=z+PZXJ)>JFn)xR^E%G(cCLPs*l)KRV4uytz6p+l(dmoP(NV8TDP%~ ->>m~rPu&ijkM;k5f9i1vg-RP1<@1sU1FzT?@;>@B=Z==0qzps_C-S9qzKjW#f@o1c@KJ2`H%?e1>xwd -(U{!P}NSB^%bUnf^*C(j-|o?okG&-f2F_t(4G_3nOrb*-*a2`6ZIz~R88 -9Gew(}6xK;!RVoQeuc8(t`J^VE=V=?8X({Z7jD@98<07hzJ5aJ_A>LT#X1`pN;zq0Cle_kAeRobvgS-?+ZF;l>>WPD8amj=dYoE8+<2R`IQ_K!#)XIy^e}>x1<6MQ-xw3Pha -ERNt^#K3?z61aODgXcgaA|NaUv_0~WN&gWXmo9CHEd~OFJ@_MbY*gLFL!8ZbY*jJVPj=3a -Cw!KO>f&U42JLe6@-eSHsTrEbwCe8;O$y0Jq*YKL$T>qoh%uW+>ibGQMTMhPCH;a7?DUmd`XGvtm&by -yU~vp>l$P~80$eCol&F5dfpe%$_MGB(FKfJHm1c|Nsm@2$5@Q9$XFL}F*tPact`b448W%b2Mdqh34t=)gMB@eUg~t!D0VSQ!(pKdpJs?}`=;p#L)OF@iby0R$L(D`OKE51w#)3D$Q3vTK<(BVJ! -t&2UpiFUl9(MeYILI#g&;{*CxW5&@IQ&d}mK7S5y=YtH?70>_VS}uopnS7gD_u#In``*Q5;Lk(U6pPp -RU!Wp^bg#Jw{hC|SD%1-tvh{NbD~&VxH^6~&vpT~=sXymI@0JF!h18NliFL;j`ZlJo`h$JmaNnFF?`7 -D44nc=AMpPw=cJNz-D`LgN2Hz=W{Evd^FB*du`x7$;Q^mT)8mgfZIS+yG@QBeJr1y}RXbYEXCaCvP}&1wQM5WeRrrs=^IY&`WM>Y*T5XrTw8hf+dzchq3aq)Arv?VD`WTJW@)`Tk~SIOi*m2 -&z>sq7Ps&!ihR)$R5x~SbN$7S%jErA^NU~olhl!vJ|)I8Cx9H-Wi-QCPWgp_*5Hec9RLXQ{0ke44@b} -?Swp_ZOMb)J4ylDxHr#6*Y`N$0*ah|o$;*PpbcByo43@!3RMM}Bu@~x!2Y)sXGOczm>dIYUL=%C4tptCw(P8yw7tW03VLtLam9T}5S8I$a0@W -oSue=`>SX_DuJ|MW5*z#~}tQ4Eg_x(Kg5xbU8Q5jhnCLldx+XMdrbOZTsMap6Pu3s6e~1QY-O00;p4c -~(c!Jc4cm4Z*nhabZu-kY-wUIbaG{7VPs)&bY*gLFK1TScv(qhfIewi)P^O^IfRTKx#m)YPz#>Qj3os(OwmGnUXC{QOy~ojrc$xa$hS2uSqQhrDE%sEv%U -Sk4Ix@DL)b0Im%aqVF^kDNhQi04ncft~*y)G{IH|)28IqYJ0$|ZVxW&*hAq -HzKTJypy?oqR{|MB=lM=Z)Oz`DAgRil&x-O+p}>il(f06}*`2BAC*uVj}Zf+Zi>K#K9TH3N&wVL(LOs -Mb=h@km*m!g^#Ep1b;1st?kDf{DP6cCgbbDZwystOZOOhR95DM`-_-Sp$S#k9@3Z~iH8teD73D~pNal -E(F7*okJZAyseaZ?=9;}~cc3=_QS51paJ}vCeAnuJFAfA?7Wm(Nih*m}90rgz=d%&q#Fok2RdsOEkHz?$LEiYy2#=|ws?bHkhB=z{I>ZgLIcbUCa`&)`X7J-c4UW@Her~iz^XC1t<{wZ?0|XQR000O8`*~JVfN80hL;?T+@CEsJgq+2HyI1op6`yU;^JAuSY^V6^r -)Q6o!6lCxP#|9eNaqqXxv_psGrwVsdn-poi!ZA}A3QFp$xSQH?e)>seX*%{S&EQaq4DtVAj8l6F>Woa -rbl=NtYa*WjhY@n11yByj)n#J -9ZuuCfzDyrf+2c?foKj$ZI^xmGjcA1nkZlZVJEfyKKgPRq4l3l*)=3NNqgUP0JE;o8AEaB@Q^Sjvc4ER -IS`q%J(I>!)nwj60VzXr)DsuEuCO*YH9xo%#Bbe2DByhN@6aWAK2mt$eR#Ro$YfMuG -000OM001oj003}la4%nWWo~3|axZ9fZEQ7cX<{#Qa%E*=b!lv5WpZ;bUtei%X>?y-E^v9BR@-jdMi71 -HR}2&ckqntYZ!ZEIz;%qaK;r_5o0p=HR^&+BX1S~GE+xA~|GhJ_q -N}wX@)MP`k1*7+~i-Y#VJ#ZTC>>-3~q42p_dA1II;=9OYdNKiUH#vh1rIuIbmvxrI?7v`ml6Yu5z96?&Ku!V2EB# -FqStpD*FY)$&UQ0JkYHhx-ZxLfwQ2J%LyuXdBV*R3U^no2?3%52_0Johxy(VX(O}v2xA6qNd6bVCh+a -N*%cueH(+`TNN9R!k$jio>FXxzA2*f!Ym-taif!vzvV6c&1M1j_z)Xv5amY3t<@ta6C7-@uL4VkQvsB -9bd(oTkXqsR8}){s(iFT5q|a2M3Xgv_+9OZ)YwrJWq;C5MX$R(oA`z9u_nsbppB`>Duzh4|M}i`t1r@ -5OyOucU7?_mkXHujzcm%`4(cHxV-*#+DP%(PFZ6dNT2)zMFpxT@DKxg2QyT*q#_e{1++6{4vfP~ptNPfy6fUdXHZlGMr%hK)gD|jr1Ro!Q~|T6w$>z0>M$i -)hU;pQlCo?zn`N2kb-ms-yu0f)Z;JC&y6LI^qEXgYZ5>7xHbks*&|TFyVukPOXDnHc)7g!n5x77_ -e(7-wRRrsWVy-vnYRxrP5Jl}_gZmq+?XbjytcCF@m(#&Nsy2C`D3%-of`$~^2T^k7x^L_qwPtI^u>C< -f5BKX-VLy8@LuRRCi+=l{n0>4B*RNlvYq5&8`OfI==w+kxi? -Iou6T8`6OO#R!2IOx|7SwSj$bOyf+YHGqy#>jR&}PB5>Y41581w;yib -K_PxkuQ;?a{8>gFs91~AzKXBZnoM+?q&osh*BS@pa$DDLC1yTmFmUyJssjE)GaZF&`sZdL@>p}&C}=w -D-#XOasSAD1|74MN`dJ!ZI?_+ZG`>{7a9EQ3`Ee!Rpn=_TAHHy8VN#J-n%?$4@HKerz4LQg+Fz~5!tJ -eQpB}y^&0dBXZ#BF;cRX!I%WyhJCOhvV{{>J>0|XQR000O8`*~JVK0avt@(ut1wGg+JG@{{^K3gJ)t=ty3??t^(O`+un566fP=GN@D;><29ic;0u@Y-m(#SLYy>=>GQ;mGaan88m=}Pke^K*|&|6*#eRlSMU9AP}6>P(sx1!OEH-bspCl+kC7Bz!?q5|!-R(g`(f9+RtS%fm7=QP3$GXc0pA+n*gZqwtp>nGv27!ORl}C5*>Gb30l*{vMwQ@i;rj>(frSJJ0H -o*%q9>p@dqE7aIeV#UkW?kL@m#;)TX_EK8G{eLov|?>`b7YnW}Jv_GG))`Itl>~ga}s8?HLFIo -+v7qwxL4wFApWbS6_c}%XJ=<+#kB^#SctJzvB}I*H5$DZprxA5gVzjfbptBN3>b}Rgn$LKa3Pjp{j!$ -k_I3=cEUlF}Fb>qyMsOnl4q-a$Ud?#3qUAF(M#Jql1~jQsa6ENjuB7p|1#mja5T7gmz1B7piPG{`Dlr;NUo$jB&i@RV(r+_;y=KkQEv~_&$27$=pk|u~r)6v9 -Jbcz5|a79%HSl1^O@I_xxLjdCMiZJLCeqPb0xp0f!@JD63!%c#@A15;SJqS*~TZaKvW#Du8XN*`|fnv -M#F@Smjj}fby=50V3D}{{$$o=8dfOOaZ*c3J7BXc)W-}LjIC)&)M}&mcWhF=%9HmR12(Ed<8OIa|2$w -W_J(?1T3pN2_zV9?FND#=)E?LQ~N;SdvU!n=#ximRQ#-DJ-!}2)bpR<$4{dXduZ)+YENWxf>Xoe3bue -8PuP?kV1ah5$KVCJy>b~-Jd2<<0^)kjMt^>&*+cy;(cKx)G~U_66yVy!hH)8+|zkJNCf1sJ5c+-$8{i^a31QB6;+-F^iY?ucD*!> -8`?_X5sZ%+aE=^y6BjpB#j9$|3PnwpCi9zXvCHN5F7RuhND$WE8|qh?AjXQv?u%8M=h8EV?2Q43c66L -quf+L(4)OGm1Z`cYD>c9i(#oHXC1%t)A@IBjE?VA;r)GKqTa!OGw;45jajhCQEmqRZ~#O<;yQnuc_zH -!EvY90n516g1tt1HTwrzKhcTocK|eb%^qh5>x)&JmN`6jWlvUebo9uw_SrIT#74ar#|`;4t+D7RZ|SZr(f(|(es#z7svuc!A}y8lyxxr$V4F(&s~TG7`j$ApkaFEV%tL1UqOLn$db -^ixOOp`#20E@`G6|u%_dQPPF?xN$!s6&a{k3as%&Y51T7S4e7ZiHDYbXri|0PGD?z7 -jPSiht=gVkETZ1_UP=Kpb{@sfFQzRwrpxnnQGFT2|mjDML=85~(2tCJrnB_i6`{+wQX`C=1Aexc$9LO -Ldri+T;}r#XjA_T!>cJs>@58&?$VbqOQ4qs^EET4s&Ikm+lT4HU(!W<$FNk+7ub!%wvhK%Wf1n4eUpbnxUGltc=LH@Og)}JF_e3+OaVLn`wRQu*8xv-5Y` -?pD0{nVK^u@@TG>C!rj8Ww&+@>+3dKX~pPd5EJ;ltV)5$Avq~Euo@1hx0$KP{p3*kGv0Gm-(q#W}u3; -4HPf%udFo?E{_zk}Cobze!|DPy3%Q`-3p@FBe0?SQ#6I|}P>S1poCA -i8zLO5ynuSH++?_zjTj=%MWx|sKJA$-8JtrEfnXIk3LWz>qvch2w>C|aSjkWY?Cl)qaI`&25G4RpVu -`dqt1dYs}IC1Sb8v=O?s*M^ar9ThJFybreta@_~Mxl3)mrcUt;9A>Xnk%pU$$08q?THU?T2v^_F^|FEF!t1qy1+Av+9Ig_-M(1e;H}h>E+R_N0qQO*GJ)K#0qoiK -tbH3tW8jyH*$f_}HPEE6-;X_n_qcD)l52o#cM3QCMrtWJq2rZAqlJy}k?Kzdy!X`-rkb)NFO8SGeOu(P_2|7G!_TF3nFp22U)68;I{4t@H>a^U#KYbn@;U;QGz$V&U^zDfWB6-1OUF} -ik7;QKRzOuBh -=>leYK(4nl+TFgsyGQh51cX$n&&hpWG`q>&FFMC>q0ftjtu4lIsuxO757?uMmH -+I6578WvGeV&<>hk847vRrP>bE)j_H@k`lgyi+N8Yi~`Y`3kdX_u3JP&Ehr3R&D~%_9ne`6sg)0rAIn -qLw`;g8g6qJ@%ASEU>=4mQ=tX9S$|XWS}A{_X4G3zhZ-I`Gb-BsY|z*)K3x6z{@0*w78ONNlet@(kob -e`3ml-*%_Y#|YwII>@4s#DDp_7G`UU<;ut@JA%h`mB{F6}-&s=vRDC@drg}aybpr;+gQ@D1323$Y2J( -ZB_scUJc_ne>^uGcNK9a)OhqMNH291?oPvv^sN-U=6Cb7$-XL_y$Y5)d70NB*dSE=%uAuw!p;U4pyFd -cVUoH*Av}J?A=TV9<&Jg84XzdDDEqV{%B@!@ih1-umFCc-|utQEG!xbfZQU5PBIm77kqI+p&tI2@wF& -c|d+%B07dQkj9rbk6*TRNu`b-wR6W19Fe~-?8S{2CS(u`*G$Jr@0duSb2ozb3ELz=V7(A@2pJO$aD -D6Rmi4YvMPY#<2I6jzfTnCZ=4?H -tnk6!FlE!j{gRAIe~twY88ZCr=hC0Yr@#@G2X=l4U0kkVDh(dKI=X@nfS^Jpbc`2U<(`U -wEg4)!r*N;4_YUhB6*m$w$y0;c;Ed%P80#O~-UMnc>sP&`bRnly@*Tl}{x4i5wbkz@znx-t+^Bzt~XOW}&Qv(>X9H{9blY%4}r%(D3JiM3=KsBO-eK?p4I@z -K0*4nSL7hJ~8DZv3&6*e+q;fePS20uTrLRT(9|H`GD8eO-oA#wK@;={PZO#SwoBVOr~cpndnns7^O{h{&WNSYBi&m_g-S-rU8E8lezA85cm4M#5VJ;9e-^)OI+ -o3(8&HT360G~ns=z)t|HqTDv-2IHokpV -e$sIrDYm8QR8BH9h{guwj-$SJ`6Xh`hMU8Ur53t$U}EqVv&Crq5aMdKyoBjp6xLot|Q^}7f8nSasq2I -Y=>(dS=<_{q)J -yQp#6#oNIO9KQH000080Q-4XQ&0q_vHu4E0No-004M+e0B~t=FJE?LZe(wAFKBdaY&C3YVlQ-ZWo2S@ -X>4R=a&s?aZ*4AcdCeMKZ`(HT-M@lW5JX-Tq07sFU@4F$=~iq>iq;)Aq=vyrbi!4ZJdsM`VLyIHij+u --l%2e60vgxkeev#iU-Gi9R%o$UHoU3HVu9#tUDX`nTUHefCyVxbf^zym8Li3$H6_ieoGc2;NsS9OnM` -=SIi4VRmV#PV6$QyS0J9X|W}4(>!|`oFF7bLz%ex7A%E+3d4|4OOuB-YOp*{3*!|$sS9i=~b2?mHsu% -}g#=J2UNRH9=P(?L;j;68x%Mh+68_GGNDX$(i2h7BobOSA?x`Mmms1!)ej&ud2K$$ -a|des@|v%&{j^C=#KF7YW1$6MNaA%{tJ32$cmI4i*rs}b;3G2L4)0i+{_Bh7&_&={*^Kw+Zs^>#3R@( -7N88iSj3DJ%LI{@m;odnv6reXV5|N&aS4Id6(I+|Cg4LQ5&*GDE5#rqy#;S#={K@r@pS4EWrPFTsuNI -XHbD=#Xo$mvE)n>lG(<+?7LGTd9S2SdgefCne%`5%>IY9t*t;6srES5fi^Z_HuB$bv`6e##>Ndv`_ -ZZz{CJt!%U4o(x@yn+V(G=k_A1KOtERC7SsH#CrC2{-~;xQ@nvwKjTHo2`iDZS9g8rSXN1X%F!sYy`c -X+1mlx@4e(8F}K0ZH_rk)bcyBtZH&1#kD2jfUry46NH5oV?lO0@Lz+=(D5wdCTJ|U+cNg2Gb#8TI;bW -=)~g=Hwzo?HS9^IiG*DP%(*zy1Uuut)EtzVvsvgJ|R{`ysEEPS$6rI?1+oB+oTe+jm!eLyQ+f7NQR#^ -uGCyyWHx+DD~fDu~`xR(-H909`qQ0h$Vx<0w2HEWsfeo9Gbn4&vby}si?k_@s=W)SUWZfgZnRaY2{CB -m+HGi7!>l?`Ym$jIgc@96i2ssxWUYb&?~Z7%kt^dYjMzjJOfa0KAZn>pZuLviUwVR-TU)!>3IDal190 -0qaI!fO;ANAOpE|9(ESrhfb(VvL6(8N{*t%CySW=T^LiD680>-0qn8kJn*n5PT -P%XbsY}a($HY>o$GDC#j-z+LD;d|uw&|wm{78D_)f>NjCBgDso%;2^PYt4io`v}ryORTRkC=2+o;@x+ -KH_<&`712EDNj$T@>}Z)Jthz@E?wF}N(A2ErW;L;^IAdWd`|tIpppZIp%0xGL(v%G&`QZbusrYWGEms -x6nIP@cn-n+KJ!~p^-s?-dm5ryWp3vn*EcBY7{_oUhTh+#=aN?&;%i5RTfhgasb$;j7H&VKVS9|s4){ -U$MBRc@Bo7;kBAMzeZuQ-5g!ys~+fO;h|l7p15T2WBKs8$>Vc?>{MsE*EN;*9Mr*1{#7Jf7604QJ&!^ -DZie(?MM)2NEZGwBl(gT12~I)TtXVY$K`5+53-f{5zNTRj4YRSQk;0?TQIm;---9zvZfyvSjK+Q#!q9 -W%6|M{+l{VMI{Dxds~v*;-D<~oKRY=Ts_IEd$*X{OLf-LX-js8$FjzEqnCL*d2R3aiqxQm->DVqi0;_ -uK<43}352JD{;gyRwVvt-4u+qs9MGwdv?a$3{2FjO9fWb+_?r&U(m|Iy(%7|SxbMPin5EM*amSzG7>Ayk1`?@54g -!_wAk{%G;Uwdb?QjN(n}zCM+274BexEI_uFo%IJxhB}NCyIJsn@(XV6J{f=7k6szF{17rDFR8LrUH&= -v;QY|2HlVwKAD^e62xM^6i^S&wU==E$atQGsZME9oxHZotC+{GjE0&#vDe?WbQp_4S#-p -_T}>8YPOjD@$o|HZ)jf=AC4I8q`DSf*BH>9b>+KC-(l}DDia@}C<=Q>U#ht5wqVu|+J~Yj`e&d0_zLt -PG=cURiuPF~|Gb4ww7)~|6Ca=h^qn$8VkymA%Dkj0GDLO+ehWg5iQr)uW)))NVKQ~&P2JV-)$E`b~*XU?!zY=% -)as_sRj;wVdUOPJn!Me#>HwE*(9&idJ39QSco@uzCdShrxwK>t(GD`ofXc^ZJYFEDn7G{>ixXB)xLA{ -SN-rxeQ2nHAAmFS}oR==WnHEDk`aVfunVSPWYh^?PhOeg;WP)h>@6aWAK2mt$eR#Q!_(1%hA001O100 -1fg003}la4%nWWo~3|axZ9fZEQ7cX<{#Qa%E*=b!lv5WpZ;bWpr|7WiD`e?Hk)}+&1=ozJiruM3c(eG -><{YK;zg816#*P9bgN`6=;bwqpd~~M9##s-TeC=-XxNuG#V$}m#vW2vUKh@9v;ferrMw+S#H_3AxVPr -OCreY*zBs;@tN+_MpmYhq -S*Kx8VWmYvY&7U+YMxI#0_BEN!)RTrNjI^rv8jRno$FY9)h$&Hz+Gk0=#z1mYxG&PGqzspg>KE+;4N> -9^p*04PHAu2$dC|V#RN1y5S2$%=vsXW6Glo}5LssOeMhg%f$ZqPqAYt%Fc>mj%hnMp&Vf=C)%xAM%Mw -Vzr*yUYA$lGmRWTXi%3$h_4?zDERZm6_}aK-^>tP20ZW^#CBmXs*Sh>JKJcndJ_hiX -2CoLb!GxNEik1TGUTR?Z@}+L41RHSk|Zy4mLwr1#WE5mj5ss6Al7`@E-$j1px&j?mcis -O6O>#WtK47;CFZx3h^vD&%0Wg=5vc$zU*G1oFcCNn;t(Q7l*ssg3C-5>3y(fn<99K3`1D>1R`MqMaPV -Ta2@3+VK;zdDE5p{S0Nlr(fT3V(TH5H&ilr0<#8lYnix^gzEtLggq8$)$ARysv>$5*i!P2|%O>8jKgw0dRh)j4{g -y;N8E5s?C4?@G$qH`U{go6bK9;D^J~Xzz2X7ns$-W>FY*p7dw#?OP-j$Xlwn!nC+H@e!MzllwTZb87b -@#y?^6dT*j6k?swe;fYSw+f#nf=3?>nRg$l*c-(q5!uTv#*RCCO?#`K~pneIFsWrHC2Agu>UKzP{kv? -zfNN0j_w8AhNG?}!21XZgpfDnh4N0yM`eNNK;BJM!VWhUQxuKviwKpYxeKmRO+x9VyucSSNr5FJ85+2 -FJF=lm2;2z@@~HF9Own*JEWUMd3T=+)q2-8Z$wIYXls?_1JA^Bey#n$Hizz8F$sBtd#2j3mhbtd`L#1!)0U!OM?twd -yw3d@>^;!giM#bsJ9A`#Sph8c)xjOucsidHhuMKJ*ds@6S|XI)zx4!3!0Nx*C(ux8k{{!s%CWSDE`l` -=CTt8oL)67JWxdNLCdb9hV6aG63ukk)C%1C=^M~)?X;ccpTPqO6<1NrTC!R%;A(CU8#Z3_-Kk}faCNN -^pUDExP^vK5*1^?X3M90h*cmy<9iU@6Xwt$R{g!o3Hsdd7O3&A5@W+Et%DHne?6Od2TUjLO@(KBoqkK -g-{X6|?`&PT>TcKlPV^<3f#vmgRl3!)%xSKdHLKn}7I}J|LG{{G*b-i6j;xou9@*HzkKes}e{uKg?d5 -qe)}1Nk24-xEIoDI&_JOoqyt7odeV^Sb^vQk$oG{Q#1JEb%40J52epf=}P`m4~Vqx>94sS`fV{e+pbp -LJZD^obne)Sh+r{k=ltvh55i|RMFhQZslhT(t98d{vl63&|xQxalg*hA2N{Q2_cF8T2O{axVYcGck3p -T+g+!%u(bH<~ji$OvV>kS51P{%_K>+xU)hv-4CssxkDzfWnJJyIHcuEB|%}_T{|m=)ePpSAlQO)0K83 -%$f>l^jafp#<{SHi?M6Ux9AG+v-0bvpAP72j(J4g%pPupmox>Y+I@tc -oD>MtOx19I46vJJ(A(bi9xfthJoE)-Zhv|CJ&Xqn#gWE}_k|*%)fR1a+47&e0rJn-+pU~h1dVyY|PzE -^q9=)DN7HN;IEVZD8JS6H2kKdi0@d!UVJ2%`Ry)o(tf51Y`IFi@L_YqowIZkPW) -C61X^9-tHyufe$eGuJXmah2@u{{8f#UqQ#%;$5BJ2;vV4CFxEAa+D{mo(A@6tj5MRNI<{{@HxpkYz3= -7y6@sSU!TyT}dCt1caQ^Hk2s7lSF}(r9Jdvgz3^Amh3gZN)nUWU!iY@{n0FNp`lwzz=?JNkg;NHsP>Hx7{yauXCo(vc$4W` -2+vT%@u2b_&E2iRq3{nne6UN#>*b>;s#TNaC2sbIjvWs -jvbYlK43HH!<{!HLb5n@##;orDs9fXBd2fW&T5}IMzc5y}!Nm7SA-bt{Ha3VjwQ(Kz7{@kAzb^1?6({ -udy^EdRe|BKIy8X?kncE;9(Y9DUi7~kxT95k;iQA6IiK6*Qv^Whi9vs!2=;_LZAiHQsl_mh+>$>lGuD -38sdJS{wq+lw`gMO{5Mc2%zsi@0BPfJ_C-1=mD^mA^j`*Fsga&qeLfRxn-2B$Vze(a*h$exKUM2Y?)- -4h|ktX;dH9ehuyg!)(#b-pdN;;7UP4DbTz!>BGkuZdSxS -w9ei$1CM_`S@8YwY^J}Z{aO4W1H_x}FB(GUFBImp{{m1;0|XQR000O8`*~JVu^G3384dsdt~mezF8}} -laA|NaUv_0~WN&gWXmo9CHEd~OFLZKcWny({Y-D9}b1!9da%E*-YVR*L_!v0ic|^GJ~hpM?=BuB0D_e4xt24@$;4t`*j?-{z#s_Tm1&bRvM -MXWo9(u&>a4gU^_r3V^Cpj2T`~BXEh}2>B61Dyvy@S?W)=M6q%KLr8KInPnq)mGS7K^G>+GSOhNIh(^ -K6;3IeE?+YM+;wBUY-c}{ -d-S8hKlWX?5CZN8g+#tgo#YN08lM|(oss#F4qVqD@Z9yi~1Sv{_5>6(+u0bYR?9>#X0b&RgNzznvl!I --unox31Yx)+r|CLq+Kum%lm`qj`=q!#`P2E&1j!Cw`wIp=O%e<*sEdQR9G<(Q+Ru*&86j@THEY7oERaM!6{t<7FYa;mra(Ztm5wU -I0LI=;5)6h3`f$l<)m%4jp7GZq-7QH?6byj_38ULS!M;ePF#bfOhCFO&#w`|f?QV(o7D6!uGk&>yoEO -v0~t44aHBBzJ-qqnAGcr6{s^P5XTfYTQC2As0j!`NoynJBKzSO>BESYTlALl*z&INmy9W5MT>b`XpUI -U-*$NCSE3!I{L(cNmTv+a$Y#T84ep;@nW`E1AYg=WKwKEQHK3=$p#EtCXp8`$fA(12!=}qjto -%^v^L<%3OvT9>mi9gP28Z#R+ewQTj15-n42P8l6fT1XuvR0tE~)4abPr)5oVB1BbOm@;jKN3fpgN+21 -}S{D6#dd99?>){ASQ(hS&Mbn2@(VHvsOx1O%;P6QuLG|r(4Q_2(e{Vy$fxGKxu+xYR2o2tjcJf{hL*x -t*q)nY8QWJ#j512ev{=OD!gu;D7rL9LI~jCFZFo|#=_1iUn@)I3v>%ZK(47?NSd1V97v_`@8Z;uH(L? -hAJ&x9x~@9V030K5^bN3mIJHzREIvo%4fw9uWMJ#ytOrm%MAEFn;3!oV9n5-QKv%CPK%u@O2-U1AXnu -Zu-luEG6}l6ptHoIg+m?5te%y)D1>>g5J(B7tt<#v-xQfA#U!DJacKzwknwMu>3G()A{d4_rX=a{2lAkUOc$=4*5rcAyeFJ^PCj#(cK1_K=fa -0^fz^y86!9@m(XnbQy$eJ?8;h(MTIpuNc26`0XBb{|M`?wlPQF63KYnw;wCtiLe;>`zC)6drpX&I ->_aE%y~FFl%rvG%mtjyCi`s}|IC)YOh}GEEy}yR&wPZ>i-O{T<_$AkK9W;058m_=VEuGP*|S2#>JR;g -nqfyo7Co-D`F65)j)Wi9@i2S%Jk@CRgF3^602>$!%!Y_c9+b6`VY1_SmyP&2Vs)0FO*k}=s`YoR4Iht -*aHyDB8g?iL%%gLcsmP-im>*7uk|~3T$Lh`a{^THS6B^qp6r8(-qY^|BHo7VHHbgN+?}Y!yO-ajpV$0 -iYO~a+ysZzbFM}vP9LG)Xh6=C(zR+yzU8mG2|yuYnGK?F@!NR!oE>8}t~Y+XX)C9z;9O^W0c{zE)Izl -eNNG3SAYfBQ*_e!9|VU|Mh1u*-j$a;`HHU8h(eL;CHC6vGRjow#AvcFc9oX1d*%2v(c^6!Re56Xy{`7W#Ins+qNCX4*M)`Auk81o`IaW^r|Wlbm1ZC_(_Qk#8wxIB#9URN^a~ -aUFBdpNo`Qv9)WD;EbcX6SDn*ar_4So{XxA-h1EKpucJ3shl6tEG0rlOjr!h9X>3MT@4_;63l -O8G0^!lm>gTuhtYTK_F8FQUOgzkroz&ENt-|8B1_3Ht(2VH&oUS>a?#swAYq-j9&njP_PlrlUv#IvI$ -%RIdsr2zr#QvvU*K<`U?Z0;}(XhwasQ$G>V2*ShSNWXa#V#t;V8kG4|mRry{G}^$(WOz(P|I -gQJj&)Fmt1l-osS*jV`>QX7HS`jeH)qMjW>72zu*LhnHxa@}lEmz*Ma+hY^N_Iz6ih+=^8@1aHTakz>d2Fdn~$JKL1)d=!OHAWd -{PcfqEFYb)6BAW}8BK5vo*v;)imT)iFMI1o0>iwg{Mr#}Z0PAV}xx^=k$>Hd|m~=Zl>HIbft`Yg*9+& -$2^=+V6`8u;C3~z!@Gx4=s3?!Qh@Jp~5!jnmg&2_bAr(sG+*fAl3_!Y7u>i2Dj8+!X?A1zRjW4r~cj_ -R>dkjZ3z{r@%2+sZY-6Pyl#b8a{fn5Il&K)+7<$vKVGz)9J==*MhBH-)pEacVYymVLkj|#Q%hYqtpow -%)KyC87UoDoxmA{45?yV*aSs^X($Kd7_R0<8q6OZlt6hclkQz|7RMMA~FkL-myu1W%pagcOiI;JE=gv -A&5Fo04)g@@luyZG6sOV8^J#d4iLx*dt?;$x_fH`2$(*4>|xR@C0P#<{-9q+WQJNo_E;2D -=a`2!SCNdhKscZ(Dk%sKdP*O~T*3=|RC;gvvACbvtHWO1)OfASKaIaa7ozwYO@Lc6TlfM_EQ)-m}8cm -r6v$>ze@o3OcnJ*Vz<+eq#U`SK1VieiPhU{Uj{gkbo_tzIz$rk1)W?g}n=|gl)F+OfG6)0A37%h2G;d#Q0uX}YiO*peN?tSn|rD;RO -pD0Lz)2eILd<9NHHdtHe=X{@Nd+6LfXyAcelQl|Lh -q)Let-1Tf-YGdJ|c)U0I~P3)_6mfp`O(d}!=-GJl-en^CUBFQ*qw9*Z)$cS3e@M^(u)!uCrvVZtlZ6wcT^|g+PJKcXNU9kLn(v -X?&)Kdau2h08#JMN@4xRIrqZ83hdkdo76^tteLpLw2iS15dO!TG3EvMm>T>bL;?d6%g0y!neULxu -V2ZQ%jv&_-~As`k7fqO;8qw<#ffeTwZo~2G0Zl`!>Aby+_tuj24nFcfRazRc-SJN1n22(jOe1pLW4|; -JWbsZ>@vVgP!GA)42nV^!2#y$C0Q$kv}Z&&pSE7*h(ltbh}5$0t2%jxOqi$6^V@2B`=9=R0T+QL*{a1 -aOvYSYpCwVeY@kNGqhL^X1DY@nD1L+!Vm`sr{h9-QL|)&MLdi3Ny6u-2}LQ1@6aWAK2 -mt$eR#V${YK5K#0037O001li003}la4%nWWo~3|axZ9fZEQ7cX<{#Qa%E+AVQgzbYEXC -aCyyG-;dii41V`t!MZrq>5^dBW3Ut`uD4+iYl}7OI$VQ5XDf*sS@JCT(yZwJK2q{;JJ;OyvH*8UB=RF -gex&GxVfYeaEhUew&O2Tm8seNY%YgwQ!;#4Y&z(CA6hDGJWt<%Kc2By%RfM!U5N -RfPO3rn4_1z~tA4KSjM9CFy{o!}ek65dFa#X7c`owI5Vi4w;;kjTEeS8Kf$5SM5+>3Y*mL0gM>@WjHO -MK6Fg}1MgAm_HoF%C?Xal{jN-7;3F??W{Y!Y~YiZLU+;Y_i?$@t -bj9Fa5m#;w(AQ*JCA`)C-wx&?Wlu@9UueHL(=g$vEjF_1$0>Br>;Ac*$Yvm`F%y=r^vs~!Hxa^xM{cs -d^4|j!-b|w^)$|zymd{)PA*Pm&=mhEv5kYwo>;KopyG6z_%6H8l9u+XpT~=aoIOoapnQLUgToaUi ->oap5qVLcDdb<*v0l6mCJl6+rzh<=znsCzd`J}Kk^uRI -en(OL?a0rajdF|Z;Cr#!yPP7X)iLzi{pl3@vD=Ix-PY@ihjUKkY2P2uD*RH<=upLVT_lq!oTZ1%UO)A -apmsc$X~=y~ -7=naKCtX7+fCRI(>=3=yQ*?PjNurxnZkM#nJ$68Df68c99!dezI)l*Ll5RZ -o|FBp1dAuWTWUl4kZjGXe&PR5&qLl5l4r@w?owRKz~?w9n>MjK-P_>*zT_kT$vEAAW}3EG>YvqKX0;x ->F9NYJcFt>g64j_GB2FH5JH^NJfo%#XwtPLo;74%RO89yZx6)Bj)SW?0P2}fJC>mrg -~D?YxPb#FR^iR!pbL27)CbG}XeAWj+fCL@P7$ETsxM3c!VLu#b)WVIz8Lme0SGxVtehdHO$dFIrhHNr -T-Mv2qJ=`mdOP_^zrXh5DTv;2r}AHy>YDwEVB(o-;8ZfCJ{PcefYQB6K|dl^Zkr{avL*qfbdZIujlFV -MTzDa{@|#bMbznawcZFq@54PxpGBoU`>x$NoNhpie&B>$2ibiujjgOlrjM(0Tsq&GqWfw>RW|NSvX6F -G9-USt2&C?ny6D=pN=V8qMJzqh(Ima#GT2#i_MO{KpG*GFBO53jZ>&|KG4SM;nd7x$ZDh0?m0xir^BD -zIm?6C70d@6aWAK2mt -$eR#PD@+Cmox001-{001Ze003}la4%nWWo~3|axZ9fZEQ7cX<{#Qa%E+AVQgzb2T54h~M&@r^$#E2@lhV&0{WDM&sP`R!HOPHXcXts|id2L~V@6VVdNE -CL%YR3Ei@MM?;p6$Zg7_NzAqk_D4jno^EJHL!b_{=W`mzAlEUu^3N_#KfaqxXa0!bc=q}I1c$T9Uvg> -kc4BraFi~;uuu#GX*=KdaXBZH2mm4XoW&7O)u^8 -5h=L?Fwdq^j*-n+f*ng}&z1ztrzNpu$SBVlI7*yvcPh|XG|93wt56wEwFcXK?JDq^C6H)Rg+xIrV~{& -8Hd5*}L~BAIr*gVxHQj)mQcYntfiecEpX33#WErP1|A$Q~9_=ARZz6As$k$^HDpWefZS~u;A8|Gk)#4cQYIN-8e3BV%&u#|kweOXV -e(B{#8EO3D(^zgqAdctjA{E`iBY-U;GomdK(HpA=?H@~O6zL~<@pAf4jgI8hjN#%9P>C>N+Ow;Dv$g> -;Y=yb4S1!}{;�IFd54FBm%`k6dc>|9Rk5+25g0f8J{(zv0LJU^p}y>fumTk|gSMU0=13X#c&-s*Gz -DsMqES3f?P(bPOVGwU{$Pn+cc -}n*#d=)veg%ZsSBj@@;5i04IXNM+Io*hD{9DYI@oirE>U8t+;1CR)5pnWR_=!8K)a -2Hzp0R|aNi$TregNK7y&}E2R2-qe{0WlW=c|N`P@b2Z?^S>waPjAj=lgsxnPbb}S>=DBa>p39<+LratvG&M{I*vCWCM{RxtX;skD{3A)p49d+21vLc>^7@mC&$PLClgPndIaJ!LcG -Wtb7dTEh4lXv{vHnT1W1P?22W~!}I7E4@0=T-{*pD^|V{RGexQM6b@=@hD -&|junXF3fF|l-)N2Qg^Z4V{NC^;T*aBr_1}HOhx*Ea7y=CIwRt90aji@>g1%wN2M(QN9rvX8fTEA~;Q -6x7>dY2eA(umCf(^cgS)V~-M(XbLqUitGD)^=?ec^6jGiCFiIbYJpJVH52%ba>fMZac&9z -xLZ7sdBvfS$!p5@P?^HXa>*fxlJ)nHc~9ZWBQ<+6m994pFsz{sMY((vDfjQd1k8bgE)OrVSQ^}-8Y!z -AGBsaVB;4i&eLu30_2}xC>vHw#*TC&stScm7$fpIjFNCr$E5`9L(Jy*IBZsZ)rm2ZPYlcIY8jCG@_~z -FnwkjWUxQ=t#NtFbf7?lXMoNaK_kBLnZtyf)jWgOLPq~&Ok05(VHPV1VKSrg!1z42c8*k$|IL)rAmix -DVmzx%dp*|bghe$=NyE3KZ-QnpB(-E1yD-^1QY-O00;p4c~(c!Jc4cm4Z*nhabZu-kY-wUIbaG{7cVTR6WpZ;bWN&RQaCwzf$!^;)5WVXwc$5G_A5 -g#tLskl-4HY8%q9M)`_O=6++UDh)N{Lbd@I?752;bO4($qW1J0aN- -MqlvBLwXfZqGwQsbOVYqE`15S#HiL5hcSzl))JaWdTIf!R#r*4NuvFIOwug<@epSu*~DXzSpszawN+@ -a28A6Jt-yV@-GQ@wTex3&k_xJA;SEf{7Xny~e5)Xhzo&L%d{z`~*6BKXjH7g$5bFr8OESTDWd25Z2RF -Cv^Nzp9Htl7`R4F0R3jO{wGnb99E+d>XseH?8EbO4?L58nzl74WZwI|ek7O#7??n8_Y-#CLsh?Z>}w# -KrhIqY}aCGnv4r@uhsGXZaM%89lIzqABvI@%gt@&K1`a|bEue;%CgPqN>iE*pVod0lp8Dtb@8kmQNrxh`*d -G2AgBl`Y!)_kyFd1m%F8Q4n}jk4zu%`uSsydIZl%9;krYUQ-`)v;)@r3s6e~1QY-O00;p4c~(=_&&Na -S1ONcX5dZ)w0001RX>c!Jc4cm4Z*nhabZu-kY-wUIbaG{7cVTR6WpZ;bWpr|7WiD`e-B@jJ+cpsXu3y -2bU!+bE>UN*9dO(*rMSvwKG7sBeWCA0dZ6eeQNXm^d^uOPd@={>HHX(pz^6t6k9^R3Rm#WNJk} -PXeD@YP1@~TvZ@mpQW+Cb8Lk60!)LW{D9SY3#;%pj4%fZ|en-jXoVQc6e-n&=kfN@buRI~A$%+>ioiU -WqDBHc(`xinX|JU9?f6SS4m#K?@wWbGKZB8;XBih<*_%qt-w -@c#-He}27r6MhZiH=!STo|nOrt-#FY3gD+&NaP*NB;-&S8amIb<_o9%!efu&)0;>9QR{u%lQOrjnBlH -l>}CR=>&y>hr0-ehRC28unf%7(9Wo;!;>)tQhO|~%Y~W!oJZ9`QtF}@U<~S3Y-%6O)MXez_+HfW4y9K -4k_x(@kT`;o-R&ixST1&2WGE-0~&}}jinhGm>FT>wxQgg&`H`CftS_t#zrfPbKI5918b -LJsBFx$nSYiQ{U(m0EOUuGwkYleKnZ#_4rU -?776Pw&W+7b$40qt;zN7=BNNOUYNvH*jzK2tT=yK0|9y12Tx3wP|YU9NSB**4TbvDX6k -EZH4UM&;t^;ip2g|fu=U#ff#M6dW!(*&U4Q&UFGCWz^qaAp -6m#Q|5nog|_oQKF@yipT;D+$ob=nM1#V$X;Z#|U)8r#r02Lq-YN>WCHzL1+&fG%{UB+r#NVWb7G@2EAi{fWs2EG>R6n4VN|T)l3R -)8Vv|S+{Spb$dUiZW-u-z!X5^s=W!W3@{6D+AnvVEU(n!P7~Wu5OM4J!T;5&;Qa7>+Jc72NPmc2>|wPppwN8b$+2BQ`CYa~o*@2NUzE!V7d~{0tt8wwJ=9$_LwaLhQt91tZC7CXo|oPXoA)T=62GlPc$Fk+DQ@%_J3SvO$QFmvM4nc~;0+@neId*-2(FX~~#<9mn{*uDAdN6`PfIAG%E>+p=R>W5mtMnyO~=xP6) -M9R<_&VltUzY(rSC@c2s0nTl>E1U`I2z(hqJi=5dv_>l~F%FRpjjtP(hVsfQvp0b&mcSnu}YO2;zLqr -v43~8NBsY*GY&dC(40Zyh7*q92c6!}mTdRDCe0s?Dnv^`09&UKQ^6cd{{5a#TQuVN0c=mnZ)JbW!Pbw**C~5)OF=eGDzkxIO8q#YaWg&e6OYrm=H{=i_#*6V55@o6uYJZi_GA -C}tKAvXtz=i%QKR4F0^(y##PThAIKfag<<`ph|RZvmCMuHu7T_Qx0U>tn3yj7tMl`Fo46+J5A1fW~V^ -1y7oPT$f|>A3_+QpW`8@u@_MVG>~)*nO@drXa2xkP$DCmF&q*7(b5chg1aYqhv`WB6`|OJ#7!nQt{Dl -@DR@-M^q}>-p+UI}er1K2>XX?Cky=v)vkaUY>&lFAkGtOwE-Y3m1jSbvKiIFN@&4_FYX&(vCTeMa1`2 -Y08RdFl6ZqRgtE~3qe;7W&btSKRPT?JK48zsNH*F&jStbM7GEF9S(h#o*a!(bfZ%`bx+E@1oxV2Xi2jv)=j)qp -b^su9$HLAAW6)ng+Ld>b?6sGm1pFonv$0Y5lV1F;BxV_oV%5@DQ~T9ku$3G?@Ct4Y0bSYY?$A_yZiI) -;&C~@h*S?yVXy{k`RV<_@*msj$viqEl=&W*VZwX%I&nlO5Qp<_(~+zg@V=d%F>FAjv7)OPtpVmefRWo -JF~ZyW9<{6!9x`!o&@&~KKXM$n(HZ?3La7{hp(Ey)i*PtGJZMEZ`KOE-jXwPz -X81z9uT6hbrOIh7|;UuHeMJ=N(BvY`7BAU3aN1DohJ@+$-!*@zA=u^1hyq$#E%@?quTtt_fi_3F& -}z8U}-)UH)N!r%MeG{_?;h0di&%2X`}f-Ma3}Xu`*jcYOaGZwqLM23ftMU!^|XoIg9NHnpZ=~QHO+q$ -DPm~|9R%nKf8ts=6?SvidL|lv1Wh~c8-ILzNzQ1Fp5`?7QZiSIynL2G=&&{X?og=M_8SqE5pAgn62lg -)0FMc)d|@P|Il8z~P!q}248GP!w(A(Ca951zDM< -@pKvpRDAM~;~>`qO= -r8%&qw{JH16!87_WMWAJng-@x8fZU|Reig3;l#*SoDBHdKf;O+RDwO!GqSshE;d7AaFe!ge@q;1Plo{ -e^pFz`6BxFPi)fP)h>@6aWAK2mt$eR#OvSZ4k%;000FE001fg003}la4%nWWo~3|axZ9fZEQ7cX<{#Q -a%E+AVQgz92cuor^wb`t-=En7Z;(siG(_DAQ)0#yj{@rYRO+9})0CLBMEuLWsG7J>d&gZKhgE{Up>5w8E4BzQBXfrGYtY{qRtOzN0Rn)eJUg*H~FdB@7h{D6hUBx+;*`P-{o#^69@YmE9Oa%9|{P#u{V17}_x_z@xWg5CvLn; -(O$r8K>F2_1SUD9wOAN3f}@4zz)KDSfX_bMIK+T|U}eUfu(DEvky`==*oTImZL@y?qY(F7U|FFYBkt2 -HA`Z5;$oMTtxcbeY}T@IbCyZ@6aWAK2mt$eR#Sy|x~Loh002<~000 -~S003}la4%nWWo~3|axZCQZecH9UukY>bYEXCaCvo+!A^uQ5QgtOMYAVOHlB<(54w8Ti!Wfzq?7?h%9 -NC{(YLpu6xv~6LagDIPtxQi&aw@Xb#0|XQR000O8`*~JVWffy_D+2%keGLEr82|tPaA|NaUv_0~WN&gWX=H9;FJo_HWn(UIdF@wAZ__{!zW -Y}U>A|+@S}8~zDj5md9tsjjK*gb{qD?$*7H!tn?ph@v{yVde#Ez2`^@4c$U?;oZJocN}&+AInOUUQ7L -g34$Rt8Yc>k>04(Lb4BGZY!L;dyoO_T{BgwTgm)h0XQ)pTelJKFzA(@^0<)W7`Pw^{z3zmP|y^w3XZ% -PRrWpDMc^HlJZzKTwoI4Oxp4IDNfpF^q90&HAZ`XetH|HQ8X7!YdE)Y6CXWyf6}uk0=i19!ZH$#qN24 -h!!kgdwJu_96rY=z&=9U8n=YO~LQ@&gErpX8KIxm;%An4GOLM!y^C~!lCk3qib?)q?7}wa5mBiOlw~Z -wOOK%JdCQD&SnvA}EpN!(Xs@0O2#Jf(@s2@+(#w}wI1x>3Y%toUO#vMKk2M(-Rnt?#+e|9AK8b6k#z{ -oaDj=A5Oq&VKkQJ`R#Bj03Ka;|WR(lBx9*i`F|dqw?-3d>zY;LH*{ojKI>U^iw^aoP~+S;sHGle8TV_ -htsOx)y&F^@H|wN}_4Y4^<%7jo>C!V2w7Es!hX!$R>{aVZE#EpdlMSb#rohyFIzvYn;R4J8LE;w@Q9?Z?g$I`Zz#4nauUlb9Z#uV{f3-3^-V9K -=L$X}%jpV)LtZ7h|iGNWf?w+Q@idgTvdAgX#1)N6vM(u9&?xNmycwJLE_uMSt`k3AhmxD!3soni@^Us -7#{Adct+k_n2ZvEJOgLEhyU9`*@?NN*i{F!0|#2&>sD9!(dVkAzj)Bl?BcWXxbOElv#!ti(@NQO#4k% -Ja|Fbdi7lj4R{z#UPua9qy&Q(^lzWHK;)kE}dk>Zi6%t$^GiP2W)ysZ|nYH-g|Kpnqb%l2sib=XCaFh -R@6aWAK2mt$eR#Ppb#ofy -Q003wK000^Q003}la4%nWWo~3|axZCQZecHDZ*6d4bS`jtUC%MA!Y~jA@V%en;0@M?io8i2grW{zT+~ -V45ZkK+lDlv@hkkq0XhE7S|Kxsut`qjKYFH4g4=f75Mfb^CY$l=h!O~+4E9w_;CCgM4Ep~9>>b$S((w -RHD`L=*`euf#`LK#)&u-w7DSB&{dP@h78G&FsNMkuLY>4eIaw+t<^XGBc@pZQetji(i+I2n&YFqCoXr -hT$;V!}6KY{Ycc+6RXoNwGbOu#~g;(2F(I -(+HSj-ri(T$7UvoUEz^z~S<;bG9#`ytzZsH}_>lbQZgD*Wu_KC{_Y7x-T<~;71|(W#DbGnXPw1THw4< -MAkziq7J{3t6rgLUa;HC-6CCd`b$0SOVeey2L;g`m0${RVN8V}XXHs|N@F@>@igOX5#7VkEVxhl{h%||giYsZE~cEpE6xT0B8y_y4aT2~;qZ`sE{@>mb -Fo+q8GerrCPA;$QSjfqxP11K^OSc#Dp>NNciJKF+1uMAR9-G@x_L>4J0G>+xEK1*N)vd>veb2cX# -PiHY(30x^5WPqb%;(8v=I6jzmV(%~!Ai+I62N162T+0CBEg4HG8mSCNF+!hwncjmYG(IaVb8CbjRm=s(7joc_n+&da4zF@98J(qRYy$H9u$%HZ6 -1-mpgnVt>0eItz -;!3S_X(v+^En=1|^XsmBa3RY$HI@`g=}c_#fI4BdUP^u;qoZ1Znuu347R;pTiWkhH0>Gm2nPlt}kara -0Fhs14MD0t8)|1vfJUPZe6EI;w8#z2V>g_~!c+J9_DaM?VS~U{q{7kNEv@M^+M+Ac$;YAD)UtFg}3}G -aRAIOXmnq@HtE1+1(hiJeSxaf-XmiMOJq7 ->5(TC`V|VFyGh2&8^Cuupy*h$oLIY1-XmzYZApVTY^>8^0Nbaprc`10tFRA9?@9W -AB;C=pk7K2gj=--gzRO?AL?BY{^Sm3%A;n;VrIscm{Cb~z}{<)bUjn&S$1LJ@pAo;K}|MIZ?wk)J`%5ww(mY^l4DcK;SWD% -~H4({1aDo)Vb;^;q65M91H2d~jdJ(#`xZ#&u{;Q&EhYFJb{&MSB#=Yd@n>9YAtnFxS98x#7n>Z+6)X4 -6}&Rl(7)JX9iv)stT3a`hA_i|-GI!|>Ef8DKWlD#KGZba+L&g@E+Q!KBwipe^m8h5+eipZG~{ynPTOW -Q3BF1#lOz^$+%m`g6+lFR8+)_pholHpj(a$#R4GXyQarVAh7Cgc)1~J)a3wRg^fv(S?A)R)V+)AnTX^ -y$E^@QCD*B#ajH#IHNa~qRd}imX5p0^fAb@E?580o!hr8J)!X4pUQ1lu6T65uG5JCHr&u|4D4s14DH`5kVt -_{ii&;Zr*%l$3UTg3g{A)=Q8uU;g?%RJt7`thG`n<|4 -zp}$7e+P4>+TumN=>Nk=Z)+E~PaC{+@Z~DQMw6~qRp@kbQ)AFg>Wx<|wM(b&EymEa{{;1T=kC -~%K65*LsfpMt&53q@cXA{7jIq!6UTWxpOuFAtw_$_U0%KXZZf|3k@0klczGjQvB@B!*2Bd9jbrlsl -G|e>3S`1&~P=flahCph1;!aCd#44p&(s2B<@I!5nbw|rLr=mYvQkej%azHI^>ftt3X{gXpIy-|*CJ)Wm2iPL21r!WSnj^SeEm4l?Zcu?a}N7)92n|cD5DQGq;@^S|#_ZX0)ALvcF -*Rv~e*2lZ;Q|wC_k1ZbkR@@en-7HmeFSpQ1V=C4d)5gubJY&{G@3^0Pd&4f)xrH&TQCyRy8AaO~f;tU -aHJ%3O?YwJu+w?2hEECuz>)LkU<~U7&$C%i-%OD7%;zQMaR+790|(YSH3#+ED_(oU6&t5^DqxiPrOWN@?Y(DxK%j2-R967X`#$=j01ZRPCGkJVC_Y|&I3D43t -B?tB)#aeP0i<{Bx;`C;Tda5 -#n=FXRcZb7lpLOnq=oohC3z=rpnj%!fYYr2LVT)$K#lpYpx2XiwFL#-HZMd;2+%ZkCcxGmL)L~@%(gg -eHXR6Dx&$iAm7}Io%>-VNa5$-PM9U=QUh&;6=>~;;!Zv?nqG--~5mYx6Oxx^jv-YT7TN}C@G8u@36p! -1}#E-VF$7j--?9>|5QwIzpl9EOgCLwRPVzrUntr*opvIMl29fNS6D-9UW?A|mqR?5_K&hAyX^!C5{-B -wAfhv6G@Y7dT1Yb$S3i7htHbZV<3LeI`e)}HJr8J1bEP6X5~uPDq_$lO;hQ9c+!R*!6svg@)OGyHm?x -Xw)NUWV9^_h2mXcOwl)vSZIRcN1aqXmi)o>ArEL?aaEmCW;t`#*-445~P!SDC$HOM^NJo8aLl+o_m#8 -FB26vRZb$Rb4KY#UJcRV-OlO~OS;!iW-c`?Dz10nIY -VTDoG3D$?cAye#2dNHi~pnP_H#o-gF3?m-~We!6OsCHbZ+m7?)FWg~?zr2O3it-P12-BO$JZf^P4T+l -5&{x`0t7}?q9)RxoYckBJB#R)xU{k#3H28Q1C{k*RDk-aXk&EIpo{{m1;0|XQR000O8`*~JVb{HBmZz -BKzZlwSK8~^|SaA|NaUv_0~WN&gWX=H9;FKJ|MVPs)+VJ>iam3>RkB{!1YUcaIMFXRF2aq>+L;Duoue -!}BLV=y}*_{es7WT_#^_B4$C?`B?}`&FGIX)y&92nO%RJQ0ix2AL7$Kl}8D|MB_fPxt5V*QZ_D|NiC2 -|7E{?9`^nBfBy8x^~3tfep&NV|MRC`e*E$Kpa0e8`Gft-X}{Pv-+cGo|5-o%{Q1WpzWeUezy9>C3x9q -6_4VyHKaZb2|LK>XH|hU<^J)M4_CNfz)<1mvH=q9btNQ%A{xOv6^)Hn8FNmB#u1q$N7nAQpt{0JO1UW -G|gIt(gLGDZ*AWtT*LpD!j3x~XzeDGZx&(~x({R_%2JJZuV^z50Q@%t>l@Ash(?jMhJx64d-74*e)kK -af6eZ=ph{669LsS3S;ZbI*%51~)cwg1D{U4&lo+@6@;k9_VS^a1*eOrLMM;F-X4i#(5jb}9Ye=;yw_r -h9kBcbBAU*7HknyJR_E?xnH2thY268qA3XUC^NSkp0yuzWtB(gS=~g9kKbdB) -g-%Fs8--LJ?D!)pb@aoNv#TH@{And*zOBjLdIjelxR>nT5h*RCtUE>tx|~p|DPt%vILO!ed -kzb79Pd$Effa6&|C)3>Idv@;g|0j4F>&zk`j(xA -FKk9^b|`r}6kU9^b~}+nC?R{5Brn#^c*~d>fB%d9XeYet!qQzk~T5{QeH+ckuf=d3-1HJNf;c%>k9jIB~GsLk0T2wv2YR#C$Vr63n#H~5(_7>a1skARj#r!BoFu&|TmJ+$j{1)c7Fu#SBzwr3716kOCE -bKrQb|4EokcAz{GPvi#J+rAVgL`IEUk3NgroOPLFOwxXS#u_9&SXhWmgHnfPS%{snlqW-$^1^{cQU_| -`JK$~VtyC%yO`g_{4VBqF~5uXUCi%dei!q*nBUht__3y|k>;|>OT5ZUyvj?w%1gY;OS~F0_guJI`Nxf -0jaxfc2UjOo7guSFDqHW$*1NLxu57()qPM%U^{$EeP0Vkix4W`Yt(kUCDjU_xMzyk0t!z~5V^qKHiea -!(t(nE1X%eZt^r*b_sJ!&3y!5EN^r*b_sJ!&3y!5EN^r*b_s3ssgR|i+gMXa{Cm34!c9<`I@uibN%`Q -@cY<+VoTwMON&M(sQbyw<3^)~LMJsJzywyw<3^)~JIS -tv&Leev(vKizdFbuaZG^(gfu^(=M4$NP`F-lT3)?@}L9pHg2^zquZS{~-Ja;XerfLHG~Ce-QqI@E?T# -Ap8g6KM4Op_z%K=5dMSkABF!Y{72zG3ja~~kHUWx{-f|8h5so0N8vvT|55ml!haP0qwt@E|0Mh;;Xeu -gN%&8~e-i$a@SlYLB>X4gKMDUy_)o%r68@9$pN0P{{Ab}m3;$X8&%%Ee{5!|5^CY!ha -V2v+!Sp|04Vs;lBv~Mffkme-ZwR@Lz=gda-@3PU>R+Ymj=Bdir|5W!JOR1y5BxG9FdNqsn+x8ILOCQD -r=;j7O94Xfhs6#-qu2G#QU3({Q&ul);uy|#nCYG)sM*NfDv)Z5o}lljSHelnS#Oy(z(`N?E{GMS%D<|mW+$z*;qnV(GN -CzJWfWPUQ4pG@W_lljSHelnS#Oy(z(`N?E{GMS%D<|mW+$z*;qnV(GNCzJWfWPUQ4pG@W_lljSHelnS -#Oy(z(`N?E{GMS%D<|mW+$z*;qnV(GNCzJWfWPUQ4pG@W_lljSHezMp6RPtAKo)0eLg~~&4O7KbuN{C -9Jt@nqJ^|3{NQS}#1e_=}K`X@sP^K0%O!qPr&AjLRZ32!c -^!>e{l<8seUY#hb%caL3M0J{S!sH6zQ5uP^3#nu2(_YtJ7b2{YB7UL?t98WbLCUp(>$iAEv+PN*MYlQ --87a7b=gY@@Og#Q($IF=t@vvroe0|;bwK1PYl)9x;`&4IrH*L2ug@bNJ_{`C`zbGXi6|8bR`TWOeHKO -$SXG8r396SQ+YU*hf{etm4{P#IF*M}c{r7aQ+YU*hf{etm4{P#IF*N2d3cqF7ZW^X+v&Px-)0k>61)< -`7~HaVvk6HFSqVi6RS8WAri8A9p@gY~r38`b9aSDt5kf>%ONLR3OhLRLaiLRCUjf+?XZVJKlLVJYDjf>U`om4{P#IF*M}c -{r7aQ+YU*hf{etm4{P#IF*M}c{r7aQ+YU*hgW%cm4{b(c$J4&d3cqFS9y4qhgW%cm4{b(c$J4&d3cqF -S9y4qM^JeLl}Au{1eHfnc?6Y5PMnM{wWYt{`WY6 -RPGUtF@$YHxA6{~<;ncP6;jH0W9%z0$j1ewz=uEWhX^h9>R?HIQMZpXMCa7${R18&E-9dJ9wjSLXy7` -Fp%Ne@Jtit~)y1Gi_~9=JW@_Q35Kw+C*|xIJ)l=9u%q?HM-|&9S06B(iNm5;*{OVB7%!0|P{+stb+D1 -~O-4h5_Es3MChLR6B`86m2~qDl}|Vo@awapECPkYHj7 -CP+|{MH7ZtazztxOR6Z+*qmgFCg9GD8`i{09&HBh%(ydfOIBtE?##F|aA!#_1n$hZGjMZ?qRYU|30tl -JcVXNGxH(zcAz9iL#$AA$bEqA1s2y8aSD>+lHMT%w3v2ArwaZn`8>;|V8DIs#$^emZ?J8?5GOk@^Q8m -0IjbD<6z8m-5FglHgp@E&o>=@+Dxtb2y+|HOC!y}C!X<)~g9fQ1$kr))hekUbQ67paQB;mQk6m&KP9n -5!rWXB^1voj!*!7{<5?dI=f9P=h0nz;_LB+Q>jN`9$_r{vjF^6->A -drBUjl4noJ!&CC?DS3EGo;@WGPsy{V?wJ8N}fF ->j~COkr{wXLc**{Tr{vjF^6->AdrBUjl4noJ!&CC?DS3EGo;@WGPsy{V!CaoRBQ -8IgyujsRF4qE?Gq$huFGNo82J?ZL5Ag6p`>pTok;!`cV7_5s#Du(c1c_JOT^fVB^7?E|cR -U~3;>?E@br5^ezGBR2w;wBc?{#v}RQiwL!f?Da-?y^+1%2(LG?*BjyWM)rCmyxz!OZ-mzy+3St)dLw( -i5ngX(uQ$T$jjYHKiX2&yBNREZB1d?=k-gps{f?~Pu>f~r+)(>i7aiF@W{rZf{z^8M~)4mY77vrbL3NEVgqjWsAB_eK3XC+;O5geA{_0=j& -_8j9of;2aI_;k+7YL*L}T0r+{U;KxY_@X@V_Jb-_d}Z=Z4sUyEE<%+?{cE;O>mO19xZKn0I1l+#R^ti -H~sNV`tnQxaG*4;Xw{ejvyx{XOIh%E69z>26AWe0GVwSRvCLrCh~+}d3KExg5?97;)G!NK%$6wC-S@# -G4Dj4cOvGU$n#Fbyc2oeiI{idlCy|_o9CT~c_;F`6EW{Zo_8YVoyhY}#Jm%E-ierZBF{S!^G@V>Ct}` --JnzICxcTIxi0LTubQG~BiM%F>H*nvK8%veQOO=SpEAmn$VyO~&sS>eNNxW1^SgIsmsw6B`5-(K}mMV -#tDhW%K#7mWgrAp$ZO2Sel@lqvWsgii9lCV@syi`e8sw7^jBrH`DFI5tjDv6gW2}_m4OO=GBO5&wT!c -ryiQYB%jl6a|-uvAIBR7qH>Bwnf{EL9RORT7peiI*w~OO?b+m4u~A;-yN$QYG&omV=6o!2E;I>bcuJW~Z-(j3b29k=%!?$L*O?nKTc3O%a^aV|yubNGVy9sFR(R6IM&(u5C>7X~!uA9 -kl~-l;!O@Uc_p||$|fIPdl1tdJDwH -ruy(bVn*GCk5T!5GgZhMB~!&g3Ilb0jK*SvOBynm>ZIz`qV2hVO@y>!_T -gCVzkzxPDc%#Jd*~j>^k0m@Z@gt$J#tSmT>d07Q4H|2_-Z5yb@xsQSvBnx}&{$)QHE67{TW)a6jWyPw -vBrklps_ai*U(rSj2rgbc(>f3vBnx}&{$*hXmFr9o5v2-?5vs{s@bo}%1=RSO9lp3F4DdH|`s1)mf2y2bmp_j#XV})7!Cp>#WFFUS6FQxx*rNw# -Xfd+*y$^Ltlecb6f}DVAUM%2zIb)4yfi}WgFoj9IR{ud!~bxZA5g!$~K^EBNZlBkV)PU;5AqW2Xt_-4 -q~-@4OX@RWg9$24=CGUWgAeo!3J-@;0?BH1D0*DLJU~8!E^BdKZ6xwKp_Sz#DL>B_!Hikz&&Xi#VY?A -Z14v3WQd+%H9FY6g$h1V<4iuCPgb7^^_i>?6ACd|Atn@J^4|JP$03|-eI~5WH346U0TTPRkoT(GH+=rZ$H6c7{EH`ng(!r@TapV~l8d(_7q%o9`^ -XEQ?H7*@J{?_)-^+#fa`B)o7?j2DCANL8#k*Sz@8#mrLHykI4IGm@$fQ*`AO5;|X8}Wb-MnzW;T7EMu --=H6xG-cZe(LtgHNzIosNh9hz}IUOA`;Vt(Y9>;Ag6L=gq@0Q>2Z|}}B>0qbxBcaG -QD>7F3PXwk!PrgGi<#_6k@JT*K+u;~(r-7ePPv<2B&L5la?U9X@^KMDOv@R>)Diqr60(%bfM$=iz3%tl;gbbQX&D5o3KmP&ySct33I}{x=W!$4z)-i-#fmKmoE-q -KwXt_N3elwax(u%AM)`Wh%k6rTk;>+hQm(bC_*o}|aLx~l}>SCAt+Ba%_{XL?+lBZ1*FbkODZ$Gvc}c6hGiIdqA%oo6fE&GtL-j1to~&z -CZ{WL|@_yh{mbJYT50H(x&wa6Y*-HXC -ADGUCXOZZ&T_yn$qmZ^zu2YafM4Fn<$z!E8g^YAMeqU_h<#+4}w1ce-!)?_@m&Dz#j#F1b(^ -3YDM6Wf+5~GJmZC{E -|?y3h;}aU$FD52>t^6MerBkmw9br-e2P1Vnbt>g1-X4q+6^C{E~06D)3jqUx8ozja7la3jPZGvaVSb_ -{Gm(4fvbjZ@}LKe*=E;bJp2Y-=#da;kk0v(=t3)(mIymxsum`%^fG3I}W=$yA-(@)VnA6x_Yhu|N;FNr8?0KcqT*8u( -@_y_Qdf4&Cr55Yfxe+d2o{8R8x;Gcqj0>7-Y)&%}3__6+4vi@2V_+=fjCh*I(gbUXaE?KXw3H+J~vw& -YyVHWUfF3bXcO@>*(uh}pQ_%$770l((MEa2CKm<9ZD-QvPJecgip27XyztQ+`cUAb=HzXksd{NjhNw; -wLaH4FPrOR{Sg?i0Mlug2GnUthoZaiN_29_v}zF?BcAN58t3^Y^ab#Jcj9c@|-NyOhi$>@&z7ro$$tl -TFNB$90#b2p!ji%02*gJ$KoKK0xR7??-Nv*u9VIJtf`k<9bioZ~RDxl6B&jYd=Lk*!Q@X{C+_WqW@p6 -{iNr5ch-r!lW*C6WVqG=ujC{5XEsE;S=1fnJ1OL2p7g(7VtF=$ykc3Z7f!xyN7HrQkpMuuD$ -A85R9h>8GL}_FFtYk?t}Jy?|bY-at2@chD8j1bqqpK6Lh5b{+Ve;BUa+1b+kmCiolhH^JY4U+!ZaxQ} -%-!QX(t3H}EBCio5b^$Jo0eiQr#{3iGf_)YK|@SET_;5Wft>5L+}saAA)}X{}lWa_$BpiOyHk_e -**s${1f=6;Ge)h1^)#8DflPw%k8{l0{;^H3;37dU%|#>sm~9&E_mGNNA692A9pe(b90{k -pIug=Ptch!#F6@$<@aZu=p}N%BYk?8O0S^v{DMS?UCQq}e&6NyNO_c~f?db&hx{Jtml8nW5kas^`TaY -0Qqq*MujOT5>#KXHIdAVekwf8E2hN;0wO#UE?!SM9a06Yg@qJ${+$72Kcdz}$bLAReeE1A%cE@uQ>sK68s7Hli*Llp9Fsb{v`Ml@F&5afIkWT1pHa>XW-9*KLdXj{J4%jXThI=KMVd0{ -8{j4;Ln0T1Ah^mXMM3t;j4hJD!;Gbs|sHgd`XCLuHdT*Uln{+;j4nLDtuM&RfVq#z9fb@SKx1gzX87l -H|GZYP4GA1Z-T!8e-r!-_?zHwz~2P_2@SElq<_RPOP*Qv8x4kwTN9Uu&YJvXMz1JVm}M)XA%2ZU_Xo4&(bh1O~$1` -ZYDSlI8AUGaGJ=?AU6~I2K*-Y4fsv)8}M|&)6svI{yX~b(tk()l0Z{Br;d$-M -SXf@)$v5x-=O<9T-d$XVi)3=yHGDR5XAn1#Z0$U)P?urSyq_m8+Hvm*V#{un2yn4N{_E~S6q)HEqO8| -b^_{*frPOQko^ncwd4dndntr;mB1SJ0&&j7#TuNJEC?E`K}WtS|Zf*?-t2%K>@EyOdP4GhS2d#@6ze; -BMnMoLzUIODOP(!`WpL`hDnL`tj(;OFtg{c)^MT|$(t`NGdr@FZ3u#eLqAL%}V$>r -1mcOly5N)oPe;d9wlf@lf8e_i63ZbC+bd||uZClXgZPPkqb4NV*DGz9&N*XrXb^)H~JRSuWkgrlNW;a -0${^1<$^7{My%VO|w_RXH>3Aw)FDGu{UyH>V9A4H6x8H9WjYSi&Z((;)E#7vi}!N&6urG!$((vNETC; -qVs{OSlQ`H0XGo1nj5m+Hk~fQQ<=D8sB>vI5&OIk4b;s`=kv(wS>X888&lswL{98dlwslF8cTDXV49~CSs}>a(&(7?>Dt%rPnpOy4FXG)!$c57(?77a*(P1y%5e!4UK>UdI7Er -9M31;X&cPw!(HPXP0k6L4!U#zW+jm2=`-`iYlQi34cKz(xdbW7w=%mDyxELNTiEH(6hf5O;5<2GK`?xIb;3h;gh)59J>#}m5fhu< -y+7o8-l=2tvy9$aE=@xfJPiQF7S2eM%nMFZVG>Qfh~XyvraBIi3dD9Oy`2>t41juphWf4$#~L{Kb$JWJ -f0VE(I<^;=9BYK7@$|NnU*4xgfi~522q1@s_5&@?)MdOT%l&{3+!Y2y5s{#(WdpY0%UMh<%qu8|rCrO -&VTf^(0AZmgH+&@+p%xWHhkYKW>n4{6XNWghkCJLsB25{cyF<0GlrY -?ghOe^7$}C7lOz0I5A5(Yg{k(IN~zU*;#%<6%(htsqy|38g>}-3~2VmC;AAgLWAm_C`D051wF0SbNj; -gGUOs*jsd{z3|d#4)-U7gRxzf=gtf3>k?46bx}e*--q*k0aaBM6+E4ouXZn#oI`&P=mOO2ML2kc1fNM -A%*M*2)Ix;QwbWi!$ulU;ADhn=Jne0H#=1OL`bDlb`n5J(A6L$5H)S`2bop^x -Yq_HH;y&RA0uo}^u~3}Q9Qi+ -W9zvtd11z!KcBKan)TPxJ{W*IX6>U&Y?5H}WzLN!V*#1eUN`)AcIAOKu@JL(0l33t+zjy$GfErp1;^a -HOWXjz(e$s}+2{hrf(P6t(f}>scjNoz;o3j{1OYP#V_hlq3)Ks0x`jQC20$^kOAV-x?NmxMQx8M(XcNg~l-;5qtd1?oW4a=)z%*NEXBfHL4G7atUtt00cMLn8f+C -JV0&j5yZr{PY-6!6$3ABaBM}{r=?}$c<`xDWdbQu4KGMzH>ehN# -H{PEmBJ$}x;UX|nUaVBFmC2KR{9g9W^ow}KLXsXv;|D%5ti3MXC9=YJf3)6Z`v8?Z77Cjuo&+gM=^5D -5W}a-le+t4)9&2SOZMN&c6gGyz19`gyOfo9$0eUM{8~Zt0sD8juOs)5Ij8eCp&Vxa08mQ<1QY-O00;p -4c~(>DqeJEw0000L0000W0001RX>c!Jc4cm4Z*nhbWNu+EaA9L>VP|DuWMOn+E^v8^k1tCtD$dN$i;q -{ZRZut9Gg9Z`0sv4;0|XQR000O8`*~JVyvGuR9hd+Bc@zTx9RL6TaA|NaUv_0~WN&gWX=H9;FLiWtG& -W>mbYU)Vd9=N0b6m-hCHh^zg55ncDov}qahJ@SsTp<8y?O67YqK}+%jT(|1wm8)0(X2t8Tu6^~LmFr%8WyMc_vtr#_Z>@g!A6C7xcIDr_vU1(Z?|kR -AcivjFV%=}vUbXi16|2^~{nk6{R{S5U-d*?o760;!w^zUV{T2UX?W(ude&;)X{nvl}&-wqEuK4dO-(L -LazpVVJ`>n-?|Hr@lUvc&C-3Pw&o$vhQC%=64op;?6Kl#auAFlXr5dD4I-?x4DJFmR@+KQjN`|9gIX? -y8=KlsiH_rJe+`kSx*V8u({Tk&`QxcG}dTKuISE&l9}R;_g{cz4}9KmJQQyx^Ji$3I^Dl6PKR_ltMdu -K3a7A1}52y6yWbz8ih_d*6TlccA_b)!&i&J63;J)ZbP0ccT8TslV&$?^OMrslOZQ?_B*|sJ~17zJ>tJ -K%glIGzWnuAn1e%UO^AT!3LSqq{k5KawYCb~EN2vJ-H6NkoBh-9 -^nvYQP5o$idr+ob1Qc!4t<|ArTpNIl22(=*6f>;YGT2R%3LXsvZJ`xyp;c|6Ro!E&y2nnHDs(Ks(B+c9hjp2@>rn6HR-fX-~AHOf(;f<|EO3B$|&z^O0yi63s`V%`nksm}oOhv>7J -a3~NvM_*W?iv>?=iNDE>usAxe|3lc4;X+d2JQZ2}|prHl178F`gDnVWIQP+IbH6L}&M_uz#*L>79A9c -+~UGq`beAG1`bGDbNljEzyKdAlxbfZ@DfaQkd!AD{Du>HDDmQ#w^WHhxD6i4?&P%aZ>3bKX*oThqU6f+4+4n9Hi`K97^!@EL;7lO#8MhT!u -w$qk<&_&iH;!)FLS&y(En8G_G?Bqu&)5D_hF5D|n>+W>@Msj -HBI?0WeB53I*$&HpGKVH8-N^=p -?xwlLBNVKjBtPgSxgMheWF$WxUrT_u=?}=~#>Ph=CHVpQ+}Ic!5|ST~&y9_-AszVv`P_=JH -6$ZHAfHB*o9KFQ}+4WCu;Nj}%)5rZoDB%f>Yi9r>7lFv1H#h?m4$>*BFdUrR;y)XTcd}+=2(wZmr?p~66Q@u;>q~@vpa0|%|wK -Z>;ACXI_c_QzU`>A{Lyi3le?)m(1C&|sIRQIFOU7x0r0ZhES0ZiSi?Jjwtx=~vlY9o(P_iDR)j^w^ae -8`j;4^#Jk<1TrFy0`PYLnJqLUWa<9N2gvrACfmV3D_Wop%V`#5gVj1bn?a~AseKgv5(i29EMJerN+=x -1P#>vQsX^R&*6vUdsE|kQ*YuQQ#VUJhabwsQeaZELFSG7<4q*@9DYoZ43e}TUL&~)ypW*%@CM0E;FWm -}Kc)_r!RkgxZmbUJ*AFL1ZWu$h^A&cu$Y8Ff@Eh5}wJi4r{n0j(8-qsj@1yNfo*QK#@%IrCksD8gq~A -y6cXOi#B>X-i|C$>$X=Xws1@CMixwftMqxyKN>$2)x_H)=! -f@y-sC8?_<#cxM;MjoOfVyt9YoMs3JF-q}ZTqc-Fo?;IexQ5$lPcMg%Dbr%7)3MDFptcx#1Hz!8^kwH+)9^jiv{QP$cl~jFViCz#$TNA08xmfv-M8fO#_{@b0#gTn8{A5_li3C%LzakI9uGj -rZ{ul4B-APBJ#uf#Slu{LRKXkj1-8PSRxYLS*soZY8;~4rKA}lJ_!MyfE|D@dk-OEhzzk`p-!0`F)4P -m?Dml+|zq^jO3X1&r49? -PjnyRf%6ON=UKzh8F>Ia26ccntv2Vuj*OS~ggKA?U)88WWx8Ls~xo^Yod)0TKz-ENxrt*2whGnTxR53 -N!$o>6ckmRN~8@azf43pdxX(RXdF}2X-{vzc5{&17z`QneRkO86dfzJAQJR=V)fQgfeNBRM+Rb&`Ya8zlE^e?n{{LHEfml7nqBsyf&n -CAp6lJ{cpq=bWnTBBb>`nIO4m{*y_Pd*(lpbFT==y?f-~QqTWAvc^{$2;kJ@fZEN$#1y*F|# -A{Jn0Hd*<);klZtWub1SW`Fk5l?wP;0iR7O7dz(qFC$=I~o-t -R*LUI2>?HRuda#74Js?S*aru-~I{n@<(BsY6&PfTwB8$OYvx_6%BhEL?E?p-9g;S)Kkd*t~|jw(Wq>K^%glcS1CSSmGlD*#K77TeJ$?lGim^uBi0>_7D6?&*UG-KbPJE{6YHX7@4j6 -+ez;EyuX9wp3nO}GcCiVWLd+dxXnA7`@5)FUU~O-liVxs{vMM1j&Xl4$-N5i?<2YI9{2Z?+$-|_0g`( -~-aklk@7(VnBDwD>_YafYtNDIE$-SEIA0fF{^L^=w!5&P0#(4B-cYojGCVNr%A2{<`^|S_s^1C56m%YdhVYmxgMBf)b!lHNOC=R -#Hi`HKR|LlFvqCrxqpS^dSH$*ufI>8#LVks%9xl81wr3*C-s+o48Smpr*3U$ -*?OqCzd*j+bz$0YdyjoPMuCC2pl0gcynqjyYu>=Q{Bz!=u{rhDVrfKc -L~iZq$Z(_5*T%ridm+5zPZ~f2N2g#ti!bxj$1x6Ju`ufZU(i-56t5{eaw`+1?mqKK+2)pV{FUqpaoux -j$1@6Qiu=0lB}_@QGyo19E?6%VUf@{R47;nc)*T`Um9xGQ%gb^AE`VnLUp&67vtp{h7>sj17+u$o-iu -k1>jF9+3MpMK>{aIzAxxXZAV94V|luvB&Wtg+vXV;*PPu@gW664P6rxV{hX_3WFMYZWp5}=ph9_4L!G -uvDxq;g+2{k6BA>j;X?{~8amSxqsHhVg*&FkC`Lx-A-Ut+*dcN`56KCVv -wnfAf&svB}@W$lp99cWm-EG4eML$sL>gO^p1_L-La*e-k5r^N{?c$=}4t-#jEgY4SHQ@;493Pn!HqjQ -q_*@{=Zi6C;1~ko=^{-^9q@JS0D9@;5Q^HxJ2An*2?S{LMr1lO}%?BY*Rd{G{1-7$bA@ko=^{+{DP-J -S0D9>XTyBCp{!TX|^NAs7!iDe$rGX#mLz_BtL0#HZdxb9+ICll}RzOI1kBBn#!aK@;8q*kX-XwLFVR> -pE}CWq`s`7NyQXERgklJ)J1ZwwhFQ}k9tV1)mA~C=FvuyYqeF7p?S2KvM-PJk=*c!yvw5lBsY8_#wLI!4x#1I8mPbcPZum -rg<F9-St+;S<@EM`uZH_(UG%(Rq>^K9NCrbdltSPvlM>4UpXMiLA+^D>m&#NH%JctZ;~AR --y%8qA0avTA0;{XA0s*VA168ZpCCE-pCmc>mqTm?dnZ3%Pjb)y=QPk_2j%DOB=`J(-a&HD|L2_~_xyi -Oqak)ve%?)TeL7Ob?3%2gB;&Kw+&Z)Us$$A3DkdvbF$Y0aOqQq8&;>0Ov%R5GnBOviVx`pQLsi?%fog -$1%&8jCgouuVTi(N1tBKd&muDZ#o>fI|@^P<1ynep!QHbwFk2fjAoDrAna+j(pUrXBbp;k#?4tpz82|ow`#HZOvM~;R?+WXtC;Vpn(wKabJJ4unarHvW_B>ujP=*db -h=iU!{f{pv5uY5PfqQ5a%!BgI!z@) -Zt~G}dp4cPFV|ChZk$fe)HCxvnNBp-GjqzFv50zRkEGM-oqEHZh1bxLW4&S8+|ZF=yO7a_*LXUPlJ0Ie|Ehv{j~N8$cTA-jZwTNdl_HZxu;GhQn*UMn+RD>J9zWEDN$W+K+Qbo|LBI8T9-~}%>J6uM%6H6yh3KS9Aq -`awDAg=@d}xlF=jPAK4c~u%gkXJnK}43Gi&mhImb7v8?Bm?d^2;VM3$PKVV1G8)L4f(!y!ved(5f0ne -jkbWpR=C_m#<> -{-b2A3!=8XEhZrW^eX1OW#$uphD%1!Z1o@pD)P3|pknD!W3%^TV$|!rWcCj#4Qka_`ikkT?bN540GY3SQXsR#=nH8zF$ -ikG?6{(J<3R6^9U{Cmyv8_)=i77t6iN@gNJ$AJ)_FWkJE{wk}jK3~SA1F+bR$=^gVdAAC*W*VaGG(I| -pNt+LEGO(ZVdfZa(=STndrQ1?$FB8G-;FF0V!VI@q23q{|HNu -dzn;d#Fxt_2@V(w7|V?AJUSYvgG*+6c$y=EU9?$4c8{0x9@RP}l`qRd?GC}lqWm}m&o!;1%nf{dx^BGgoS>}3R2-=LV2-?iVHfTf1> -h!g|y67cNr{klx}F^!5}mNVGx>`QV^O$C!9!+2~K2AvkNQs -7c2G`O&%r)&7>*_&0S4SWWUQ~R)f%-JrIN@CmMt%DH=RGqD6hmzGG&%56v*^MD`a=owXC$Uo=B)5Sln -K2<=INk(os}QR{TTG9REC1?J77z_!`UuARtOdz9!-9z^!6-pE{C6+~tUI(W9}m_LO|)v3O#jMRY)GLi -xrVk84H%m{jT-!(>ZAbc`?Y0NvezN=~`V@_mG);8(AplS)KmZsY!_7@W~tql@0uMLtw2b)0>+V3*UZ9 -!t{6oRCpL-!!DP9rh#h!fd%C8nz+=7K&avcG7}E-{G|Co%_uCT6KONK9VBiHr#+=6V??GM}nh4b|F=q -1WskD7D39-G640X==@sx;T+-gjH=VF~(F|;*@QZ3l3`bc-@-qxpn*6^}u-Nx@}iI)brAyZpvqz$bQNi -W<55>RJV60)T@Toy16ydiA;y7TfXY4ZI|V%Zr0s`x(z$(HaM!A$ySh>Ix#1*pR!R&YP)D^y?JUjZw9H -km@!E0xSm>{pIVoeriS~}@|D^VJG1tc#fFK@MoXD}$IR+6D~;>O%neLIBQmjOBQj^UKFfgfr?~ixM*r -nKax`dQ?=78v-~b$#aE$?+m%xWf*RWxK0|VIG+|B^DGH+r4d-G2+;3I%<4{T<{IIsrx>rQoSXCwf!gO -L!(PDUaiyBLXq>}I3_WDg@%AbS}}fb3%g;ZMhYMiBmV9AE_DPsc$<5dL%=Vx-jsr-AA0R7XD}1&||*l -tAc|>qaW{bBqz8pW}=O{hVM#=!cCa75X{Fh|tezMudLOFe3DGmJy+!bBqZ6oM%Mn=K>=_KNlGh`nkl2 -(9ZxPLO+)o5&EH1vm2Sv&s9c*eg+v4`eBEX3H=N+BJ{()CKLL(&WO;@4Mv21ZZabDbBht7pAklcenuH -F<8dPs^iMD%=x48)3Hs@1?uMPKHf%PdVIzfxS+x%Aa#xUJFU?ApV3RXT0Z!yKz}AC}HEbo=Si@JPtO)bAF_P -OgRFxbhb}Qw)ymSZjiZ;;%T<#WFNY(T=xjj;)LXq=+Un)3>%-Cx9Wmwra|uR{@boU<3xnwNWtQW%r9B -Mb*5s=U;MOH}0SHWAV>!+v=lfa^mEJwh06G@i9b!_xbt*?l%GXroh)C~VU;qKn^)W^~-yd@aLOgnRfB -}R%H@X<{`uJGAI~?gC15gO_j#!lM#mW(i-et2yBzl+ooH!x=Is?$iEe5cO -B-9|WZVd_>NBlfzeK=8bS5zBKRc;3Z`M?_tF990;EGF=BZR1kVQ;u{;NY=R=HGo&&*iKO>gsK=6 -E&5zBKZc=n#3Lbv5TCDyfIi5tL#KKTwXdmnZY3Vrf5VD?JvA{6@Mo51X?*hMJx$(MoIi?NGP=#%dQGp -9f}k#dJV%1(5k@S}k>GiZ5zBKVc -%ERy@*D}CZ!=!n3#_6IAC_kI1zXr;c>v!lsOT29^rAo --iuveE;=>B}A7pX8&w&(=HOhGy7+Ip>~l7p4mU!JGF~M@XY?%UaMUsf@ -k*6_GaxO5j?Yhw$t7s5j?YhHfQlVk>HvAvpJ5}i3HE=pY5VXK5=64)F_Vw=FH-tz@f!cqdX3n18STI`W)qP!0cEK -3b7PA%Hx1J*2am1K6xB4`<$If=#$3*dj)ra)yS0}PcnklF6BVY?nfu`um0}44A?gpi;ScE#r6{ILaZ* -1vS+sUXcuBxag;r?y-K?f>xrZ6na!RBClVIMp4prQ>_o!C*fX2sfSpKK7<*=W(RLx$3`f~Bn?r$vLM# -@JvS&8u0y~l5nLV@FFBBAFd2p0Hv)KseM1p7b%;tb#ClWlfXSUaE7h)xFls&UKCfJDt&+M7)_1uM692 -{lOYz_+!o^2F)_Te%1%ni$P13ZthXEqlnIT3gsH?ad(I2NGyCTyt~xIS&+MOz)-__wdz`KNX`xRZ2kgb_MJe>jqJ7IJPw$nkDW;9lg9yjJ9| -+IeeyV94nTGyp-&zM?CtD@C@3G}abSsjx2Pl^<8i?56)8%=Gmiu2Fyx>VS?Tfhj9}e#d;=qB`gl7d2* -1WV7{MEicQS%%m+>w}@FL^gjG)?OoQ93kRxX!zXF@4*#N*o;5pTVN5%JbL84+*2ixKhGyBQI0y@wG~y -jt3V=2B!u$B!{0-rzVR;tftPBHrL6BjOECF(TgJG$TT9XBZKBBllR^YV1-ZB*(8ZB3^Nj5%G#cjEGkp -W<c0;Dd5TlH@YYa85;~g49x -H8@eOh#_u@*dRd+JGXh@h;syL`dV^8be9Rc#paz6mw1e7Op1yxog=W-@P1hsL^>1UMCH=2Zt_n~6ruG)?jM6Og9g{RW -GAZPk?Zasg9(EMw;U|ytrlP9ul|YwofCESm=p(?12<=%`}v0NWh=|tWvVwU>|zQS^E6IGUbk`k7C4^(5h_dj) -(r_X8oTb6t0Gu^wB5pPvLWVv@eFR|QP*7sQMWAn8nueBfpM80+J@YkApo3PDX*BshI;=;Dl__6?l_sV~GnUMg9| -9y(SmiJ6>*8*=Zs%73@es@(NI*N9p%JbKF`5)e5qzdF0jCgyY{kaY>LrH*z&EH*pjS+7xue01+%Ns2B -KJNXq1YqCHDwcbGRa?jCwry20BlZs1@LlbToeX$#zu3iqH$(0XkqHh1tkS-=^4E-DRi`C`tRT=YrvSP7JK38L`A={#`Tz;p*d;&DFSSf#xplS>6j}82*6o3u<8u40N^-Bi4 -%9I>?_|-R60VpI9h=O3u+#9h3uH;@rzgFLr0Z@on4i6XA7Jd_J-`pXw^39$vX`zjzl{S)=+DKYyBWa< -Hq?I<3mfA>KYa?l;jl^n$!oKA_hTi>tE3GFjwVt%ldeTDcNeitfEwpY<-*X~;?|>88xC`^RHscG;al} -rf6VqW}jx7vB#0Jy77u`Pwa|R7=0)-HP!gTLt_m9A+jX{H(LGIaU%n^&0_*7v&Wr|9|2=!+(>w6hVfo -x<%mpq1Xs1Jz^V^cuoMEZ=XFoyRHSnm6K$a3G^BbNKl9<$u{^$N>WwiZs#o^r7^rvlrWmM~_T4W~Z;fG)UmI>W;P-~#4fw_3cLRQNxZRN7e9`Sb%ZLQuW5mxPUkVlC -=a4T&3h`;rmtuwZyyr_5h4{qhOI3yVyw6LCLNbrk6yh^KFVz)-+)vP^paPI)p#-F8sI>u7Jk$a}iipK -z++QHY#G-fo3#6#12{kD$YS2@l$f!Y4fnuWuO@)e$no0S`b+EdUKU@c^KUD}weOV#k^c6-@Q2MGu!02 -lV0imxe1bn`s5YV|wAz<@o3IUmKDg<1vRtT(qjY44cYZU^ke@h{-`nMGV{rp@Z(9b&xfqvdq2=ud#kq -r9zg+id8Un&Ip`ISPTpI<8k`uUAQpr79=#19fHS-#}Kc24dGiI-XK2MHf0Q6#v$w*a``_hZqk+%C-PS -?))o*IDidqBmIX$Dvg$_rnm)u48O-dz0mU5L(T0KL)K~xgUbovV7^ZO5E_lz}qbMdj2`fy`JA;x!3c% -Ecbd|$8yjAFIc|hiFQuz`TrHmJ^#OEx##~kEcg6VVlB>>UT}zW&p+FBzH|aPMwQAdEcg7gz2%;Nwm0m -9V|&Xz|7>sh(%GCiUpku;=Syb|V-&WqyXl>NYU((3)40=C$lrJ)Eu7`rU*HI@Pi*R;+^{s6;K7B5R -;)!VGz?7`+t9nG1HvPTv_2nx~3(x_XRtV`RN7@Rhzzgl@Zi&TpeU21Tw@3swb`vGlJ5ItJfH*0J+WxY -A3GVU19FRzI*<`YQXr#@V7>P07$Z>G)p15Jv%fmQNC9M$5lro`-X;W#O4Ebu8G(KVH!uSI -47M`@{S0<60{skjG6MY!b}<6|40baD{S5Xn0{sm3G6MY!Ze#@d8QjDO^fS1b5$I=d3nS3a;8sSUpTTX -6KtF?hgoHvr+Zhr1*};g=&rU{!es(b;^s}20p&!2)isHZJy$8azK-j@vZkw=!eT)b@*w2Wtg9D5RJ2= -RQu!BR42s=2;h_HixMuZ(4VZ>BdG;9r7!`zS(Hmbr*jx%CXZVe=-?p|a7TM4eSYe9bMt^&;3X`?EXa* -h$RZr-R0ZCqf)1ntgd<+Ldm!y}m&YBXo$hj1L)z&s_a}3(lM{)~$3v7k*vW}R-{KL-4 -!8|uw|~ZR|DwNSxqr_)?DLV{zRNBjWmMBc?DEaYN};%GWqOExy*XJa6xXaw53!RsCmx03>Xqps_U`7y -qfp$sGCjm@-JE#jMCjo|?9a`KM^1#UJ;aXOoOl#Au%l~wh&{MD@hB9Rzf2FYZJHC0LUHTM^bq@ObK+4 -bu6vmt+QW$Dxh{BS@3}-ER~J09+cY+I=^^%)b<1;I@XU^~Zh5W?o{utOc}@k->=n)VN};&DWqR -l&BbMh>@O+vP%X2DtKFf&ZITbvgXT*#Vst;L+ -l{U`AVUy9OiMroa5v~(C09Z1Lhp3u#pR%c^ok3I60Bvna2Thj*}A!o_QQF=Quf$;F-q(bB>b}37&Z -zFy}Zqk>Huf0dtO%6A7Mq9I!cxMlN{faloA86gCRMGmiu29499dJo7kU&T(=gM5MzUkrp;0EifY(zRZ -YOjc=5q8SEy_@k35zGByo!I#8I0CS#M^a>m^5Z^_Nc)#0-Xm-th5rt<8{4*nE^?CI;Ii`*{z=jMu4Co -<~I>te) -=-_9E_&MA+qg{+tfox$U0dkCy8jw*&{7iYaQ?<=|+1aksjATGAGUA=>Y2eX?FaofBd%x)fG#M{B_mOYGAfE;9`3gi|e-VSEBZeXMaiDb@n=n6 -szOG6Maa9%cmk8Q9JU^mCcL3-XJzS6HXWFV0@=V+8uSdV&$?=c@WP=x6XUBhb&_AS2MvO}1;4_|4vAP -l5Wr*_-@r*e5W1bCMD0XJm*G=x2nz7`6<|j<6TQrh(b9Ym7iYW2yzv&-i9WpdUIuFBIpz%}%hkan9Q; -9l;ffbKYhr*mkgkVD@$gBVIpqz1*{K&f8os_eSh4nA^zjgY5-#yRR@30Ab#6H^toE9!4S{+=FpN#oXR -gj8uTIPsB|XbNkr)V?V>(KDIaPXqY>6l@YI>xx>sGPNbVV%p*EZq?@~>THy6FH^6-kC(_LgsP}<>F0- -~{5y!B8u)SjL%5FxWpDWxOv9)4Oc?#^Sn7gXJ4f^4A&Juennj6@|Vn;=@_W~mc5Vl0@rfBw#F;WM@Js -3MFnj6{Pu#cj-k)tQ!-P`laUe#B>_q-kTiGkc#0C<=B_i0c>OeY?`I?e!s8qkN -}9XbMp4S!+|3q`V&3Lnwkj;8H23ag1o}C^n!}1p^S}fn(9c00OR%ugJjgmlRd4f>{0fh`ex`RC -iYZ(}8DzK0`K?B}2FVb6yJsreq&f(npvM!bIJd-+YVuYbOm`vR7y=6l&wU{A>WM($ZyrJCQ!aTWIW&u -?Tqz+%<>fvt={KL>bRz?#+kagJ_r2I2g1ep4)6%^zp3vE6_EH1|fVV9lT5K8G#;^Jm#Yu#h!>j@yP^| -MOg%BTgNh=Xw!w=HUDw&lj+)H9yFVV)y?%FH4Ej2IsGHjD!;a=C8Al$0FDK4YoI&GdMrW`oUV){22FB -LOb4f?sru^ -6hl7cQ!{K|hyx_JPXog-bk}!qV5mCHB3j?q0aW;{sN|76v%;g9`73VQv8y!WKrjH=@dWLCuh{9=0&5z -76^r@Bf!wlKk?OfFK*%mEhA7AD!dpbC6pQngL5uS6{lMRRh!?&L(im4Clv^trq@X}I6l%Sw?rTx^B -{xTwus^%=E^j0@#59U?N8os1ykvbBp5y@(LC#rkj?C$cn}#h|Fo6sSbcj_P@)mznkh3^fFFkRiPe=0x -67XWDyb+BXqs7B`}{Qha`Rrb8w;qGzY@{40y}a%~hu_EX5wO?9qk1SczYZeRq(e%%)s!KuNW?Tp~e;O -|+G_>Dl7o<53RaI;^dR5t}~24KV4@TfquHqF#`Q`oo594>Ea$ -NuEwBszet>RGS$UBSlp1&#XVSDP1|*u5$LDu3M0@@7xzv8iW-V>`l#aBRmI5sr;JXd?8(9aLO*+sR!j5sr;Jj=1%pX9ytqE3%Uhxh9;4;G%)_-nbWpZyaYacFcXx3?Ne^=%t|#eYyB3#* -bnRzEI8e5DabZX|hZ*9!kZuk$#APAf+=IncA>BMAiHkzIc}No1gmkla5toE?b1xBBgmm*TW{%*D?CCI -0B#Kp4uwSm>rz-Oux^D_1lFUls= -ziXED_jdh1CSMMPYS;ZBtk(us(%l0^6>zhQM|xEEm{Lh2i9fZ?9@S;OvJ#w(n;wfd2lt`IN$-zd!bmD --8PkN$l$e}5XerZDL5-#Zj%0rdCpTa?}d -=P4dr1*@Wzi --A)Dh&FY-Jv>B1pUnpoKYC`*F2=Z6Z-pRT-gh*0eESCSPdMJ(BD>N^KqMacV+Xj=zrtNCSvGsz6ZLcc -z1bk1aWPML>q=x`%vPxr;iatQ6uAwpoEG_wH$(_|I6yzbY*uGBE#|ax>p&&L3IHGI0`>x00*Q+4B%kb -m;wKKVTA!4=~`vLzg(Cw;3LGa#()nH!#V>#K8)HJ@Zn*k-W&(GsyFw+VWi&N$A*!5a~~Q;{N^I1!HL; -Wq%=6m8j6$#Crm?;(%`gcSQRtpZbrn+nJr6ZHa0TSDkT_JTcrd;k=fwHUnnvgocIexW`h%dp~!4-;x8 -1L4Nm-p$ZWV6juEdO7r_yN%!Z5K81edX(HkRPKQ4A-#OvpeRJP|3uOAn;G2->(qBcgneq7APh}Vyc*c -kEpaq$`>UOz5cW5nyn#cGUr{kTYtkp^0D>)YXD40;v0Ans57>PfGqKOMZvh~C)~g-C3uNoU<6gKhgm;JOt{#K5e%u@w=#mNy^SM`pc;rae>((2>S@*j5)&@ -sVg!kavuw~vOf+}jWCT^?wBx`bSS{aho)IJ_Iu&n7Ol;+jBX(B}vrbVBv~Y|E3{(SsGtSyZHPCGL0Y* -@jJaCc`kvQ7K2zEzt5-3bX|KULf)j+d1&oBc0^f9BT2AbV6#t7D!FYjXn`WaBaBdT82o95un_frZzm- -jjp*8)_94fBAGsv{R`6N0Lv*;9uZ@pce#XojMrh(j~f97PZXWH4n;`GUx_A89kfDAH%W7gZ*zhWoOgYoRV;Rn-oa -x{=6{l0qbZ|_A^C@RKcq)YxDrY))DupvDXF7OVgd+-OI`%LE{d90lgOdtoIyh1;pr4K-j6gpf93_@QK -Ri0jD}{cz&y_+y+~-Q6AMSId&=2>yQs{>xu~O)Vdt)i|!@aQ-`r&C_DfGkrv=sW`ep(9saKu*% -{csO1g?@$@!SdDEso9-mj8U6(aWfq|W;(`|^+#<1edMCErkVER{E?78GCb39P -(&h+$K+OP6QD)f%WUq^v3jmtBfy@-N9f=|`uf1HBi<-IJ%wL -@5*d&AR9@DN1QRL9nO^O2aoWq`p7gU&1$b;3Fgs_Ii6$Dj2jZRkOwKX_#c{RYAcm;AZ9!54pvRU8OIy -z=zb+s7BpO}$`7pBh+f_g4Ohu*kv1PQex*2y+}>x@d`xzIuc>QS;7n@5T3ESjH7JL5^T`LrJ -d2`Vij2j7s$&4-GM#4zrteQ}aHtUz*A{1jzj|z>VPz*a8%`1wL$LKN?gU==vAINKRI<_rgPU5nhj=n_ -`=ww`sMT=v=(r?~SpVHqVMyKtI_$Bj-Vwl?fbR=+Y*!GdIq)!=Jh}sBeq9PGn>@rV^@Z#w(_T0Pj7!X -DDsgp~LijlB_y-&ZwnOpl=^K%vXMIB~H5{331`=7pO`|Tyh#z@%3K@}cHQB8tRJ@HP)>+&a_c1>1Vl+ -yENFpDz6ooU=+To}cHMf^77!YKNm58R4NVHAfKo2YPM4u>>_ahv@9r(e#xj01YukQ;{@SKlve;mC8># -11r$)kpMCR3!e?-1-==5`Iz5+oCuje0b5doiz;=hH&(`Cs!Y-%a5q2BTgB7-iOt@2y-~Dqk_1>;1X^u -&gDmz93f+24yTrg(Be|#{Cq`N#F=LSDdWN-E`S0LKwwT5d>CJ+nTCNi(4@bED@+wX=gq78rfxKet2EuhFLd%O+UTAp1x@ -%~7!OH6!Z?9y;Ywr~Wc-5`^1p{7hKVA6_BVKE7`8*&^S(f)!Bi8_L8gIYME%SEq${Gf0076D^L(`gw5 -pNKy6f;=-7#W`H-OeD^5BCq~TtOmgXV0pYpa{#mbNjc=(P6m`#6s1v3$x>W-;5trD}51Ge3kC8;Ja1< --Y7m9R|u}HR)DvPHx=NG;?18j;FYyT0bW_JD*%^itz6BBSJs;b^4fa)H3q!4)+(TZw^s3c( -#r3cy{o3h-KaQvtY*R&{$U@4l=NuNMu3_!Whqwg``L;Ptdt0ag5nLZBxFp -!+DG-s%Z>wO=s+uciRu)fD2@vqk~vJ_;yXJRz>}yT>yNEWxhP$Wx{KD~r4~e)@9;&~12t$ExegY++b) -ec4%%^1znHczyj-Pegf;OtNF* -Vyt?`=rU}cdtAEXaSBDzHvADWs6%PnlTwU{)LVWLd`wa&4iPug9$Mv>1Bq|{^Bq|{^Bq|{^Bq|{^Bq|{^Bq|{^Bq|{^Br -72_CMzK{CMzK{CT`z0n7Dx3VB!vLgNbXnHHJ0az_5Z<-N4X-b=|=5f|cFS@Pf77(C~uQ-O%uY_1)0$f -)(D-@Pako(C~s)-q7-b#EIp_>o2stU;=J=K|Q+V1tsa07q7q9d7crs>8*(Ma`8u`<4W7ir_#7)J{6n2 -r)g}4sx&r-)u+|PWd!h}YEjOe=Rv|157?_k98ToF9)V#M-X5j^i<#PVDbJnv(~@>~%-A7I -4tToF7UV#M-X5j^)ZVtK9zo{utOd9Dhck27Ly23$rIp)s0v|1PXWS3*^ymKO=FU4O -5HtKj=2!Ui9FBuH!tg3F;iaJ$#Z8!)1b^hHnp_X2furporn -y!)F;WTqdYn`1bI5Mhuq;>J+{`e322uWrA{qZx0VJVr(`+;la0uuP|b4Hi-mv!;Dz!B0=4CMl5xapzb -Ckmbyq#H^PXeE)vv@F=DBU1a%XPSn47{-EBrJbt01M-@u6J?um%x`a2l0Jc~%Kzl#yevxwySdl<1ii% -712BO{h)5y|y$X2kL=BDwypj98vUB-h`^h~-&Ca&$VaLoClClI!2ah~-&Ca{YT4u{?`Nu74jRmS+*k^ -&eow@+=~`{zHsdo<$_r-_MBUSwwREM;Wm^i%72jI3t#45y|zl%e9eQA|kndcDXi^OGG5s&o0+Sa*2rK -`q|~$NG=hPTtB;98_6XilIv%eYa_Wt?91)}_qZoQPog2zy&|akCSFrz2Y!u{> -ch^Dg(1nhA>%5osP_muMr+L`0fL*d^LXGZDe#5%zL6cuYj_c!a&24IUE_JRV^$XM@KC!Q;0_*vpv%YM -lsnbcDT}xiC3RQb8SiIdfsM6A9|r%b5$4ok-XbYtUSn>_mcR_HyRJWG50lvzId$COeVm`R*A8yfxlE% -z!8Ru3`ixd5HnsFMojnOE{JVKR&{M&!K+IJqgQ#AG7YUD){XHTXH72=g8DtqU=P1dyY)aCCW}DxM%le -E>U(OVdET`noE?ONZ2?>rsfi5ClWT!zS3N7oF)x@e6kb4l8>@SG*=iqkzjK-BR&uPaXSMzY2(H=MiTs -TFC+SN7bn6nbaWFVb{JY*(Bs!cp8VURHyEj!=aB$^pSvKk-yg7oN=9X9|(sziawb*_tF|* -y8xsNohHS`(mPGp9NT5f;Q+<=E4W_>^&7S({-Z3+#6V_Pav!j#;?Gr -%YeZLi>xh=VW5O!x@+i^|s$$y2%KJr;7~u<)Uu~b`nyxzllyg*hp3q6@QQFz-iMsP0Dw&RT8 -T%>I$7{R$n+fFiqbCI^4Vg%Im7{R$n+gL~9MB#0 -h7{N?p+W;dtNp{<1MsT9=Hkt%w;zZ$XR~Zrd8DvE0XNVD@pJ7IXey%Yh^mCmNp`RO!2>r0pi<5r0-C{ -)OXM_=RZnU^wYP25$LC{oe}7#uY(cjr>~O{=%=rX5$ -LC{n-S=zuZI!nhpr)Z2=vppkrC*pZxbWXPv2%npr5`ij6gqq?9niTn(Eue2=voOnYt_$`q|Ei(9aG=g -nronrb0iv7!mr}&4|#?9!7+I_A(;$vyTy>pZ$yo{TyIK=;t6KLO+KX5&AjIh|o_zBSJq%7!mrRxo?&U -{qXpc3H|W+lL`Is_>&3!`04p2|M6%E)#aZ<6I` -}fXBH^*a44onXm&M=Q3djJkDjp4tShv*vO_Zk!og6?sg&*sWwb(oIlHY{40qv`zcc{lRuli@ux~1MU{ -1&+Au%Z%LtBNJ9Lc^T*gS33OWRrF%GM5!==M?A(%tN@m@oW;L>igJBQ%1GP+vJAvnbAwE7)fN!Ftf>| ->#mQyqevcw;pG%#LKtH|eO>rgW{sBU8V!*;J -ep8$hFnfJFBRHh+_C-d}OZIJL1Xq@xKE(+1vw0&UIHa&=KO?xZ^w?2Ga0s62!Px#kcX%HoI7IR~YYto -fo14cO!6Aio%z#612;O8jBRHh6`z9keoNhB~0lWC;cCogxg@5kiE=F+mb1!QSd-j`$&N2f1&=tlG!Ii -T6`F*fXKYxII6*l9~-@L*Iu3S9KJ`sEH7Y5Wb=s!2vQ()hH^TZZLa7f{4_B+^X-@I{*5gZ=a$CilA^$ -S~A3s}nM3n?7}|8{XNBRE9zBwH``(a(;n-@(<~gWPItoS)w?$q4-0#uJRdzn$b>f@S{sVfIMa7r$_bt -rx5PvnRQqVk7+QplSg&QJ!P##pd_9n+F-e_Q`SY{beK>FYir0t_2w9uCa}x)S!8b*}+cw1*&Rw2<%|< -PDbEI+to8T?6rsc9Jb5P_HbW7{Xz4bdIolIgKY<82=jeBW?^Ie!T}zcP>nEuc|9ZE4(2xVxLpx;!1jh -D-TXz>Hg5;t+!|p7e)J0aH)ger#SHSUer{XR=M4u>EPInE;!cD^?c^N4|(hWQ=rN3rXD{sND*$a&6PW* -?87Ctup_5X2!v9K#~#Ik%Z@2RYCA%^Z0n=Q+2HJuGsb%{}TF=x6jYBZxz~Sf|K&&h{`n$a&74U`CPiY -!0(sBj-84kv&o(`VU(?a-Iu4+#3@Shj8CU&U1DL_fzCN`5M``~Kst*UMCZ@p{>;;`{e>f4~7ojeCb&a>IU9tkgl} -XahOVIXb@9A-Jt@RB;VE=urAWKfR*bhJJdL<0|Al=k{`b2RTn2bzPouX1^VLx+#N}1VB>?yDdey*SW0Sad3j`FzO5cv>}L$DEkZj^f?cEUHes}`W2?qM -xpFMRVj=aCv>p2g!FcEc~Icof^=`7#oRV4Uk^-mo8j;Q(7NHpI`a=eA)-{OmP;AMA*qyUCs$JL2cZ6@ -oZX^+xQBpX=gW0Cud;9pfr%?TcvuvLBjB)=(ks?VQfFNTfk3u?ZNZR!h$R14tWwy>|l -7WKJN9s#jIeRg|4Bj`Up+)J=Iy}6UUKendN@8jRW#`J}Q9PeOT`obXF8#bjcjB+LmThix_sAu5cZnDL -fBHzn)fX(R5i|QG#pXMc=wV@QRImGiHY(sBu=2#h<(3|btx3L9%en2sbeD9e0Hq3*!@Jb3cpU?J^7E- -KQ&Gz;%0{=F8h7t6i&AS*u96!955%{;uU5tqJk|T`3za6~Bi12S?jG+G<-@=HQ=Hh3KljGR1P$?D2z)=#n-k0`CWyk3d((I*~}vF|A@5YRd&iVD^VLF#@wcwdA4V@$%lZ< -6403by~d-y4UH`jG%j+>0<=LQ$HIjmb;omlZ?RqO-?YP3sK876zh;X8L^7R0@wV-TZ~x6V*P4iz4|cX -wGM8e*#hcB7&J~^VFZ)cljDqlwNvL9@vJS}VAI4R)ItaA4U13<2PsvNViD?_$;*tuDRrD>1e4b;?%r6 -0n&kz*6pK)^bm5>w(0jJ-UcWr`We(5vqBbZ4`@8O)i{B#p2Lu3dd5hICPr58y1K7wmpYn@!<^jA -1n?v&#)a}aj1Eotrv?!%?leC!RT{EJ%iDQ({?EqhnlyT9V`wtN7U~?KND=DSR9&P&sK%Sq4}NxMvy$E -n++X;C59aoc&1nanm@(*!4lB?8P*S$faWjp`(O!Z{t~wUOF;9(9Hd|gX#N`84wiuCCvPx<)QcKkumrR -~_wYFcq1Dmbj9`>G#(^4^faW$_XT)pa&!;wXcf=adeD8inbfH+8Vu|OQTO7b*iD$N(e>)MQ2zx^;@q9 -DMc8evR*)FyxEb+{Ct7kBZZ04{VOFXliIaC)*JZj_-OFVn_FoIEpQ&Oooa#;D}L@dFv2Nj16pBQEYQ} -@%Ij9}_ -tZQsu9JH;mXh>{E$*%DI>re4p&HA@QW7VkQ*m!?A8Q*+Npsr|GlFPh2Qw;`lJ>AS6iZ2J!XxjkWv`2+ -q`AX~7(q0lVty`2?mXhYiR12V=F`j5*DM`mBSW0TH=lB^*Njhr5Qd08>N6B? -jvd5lJEG2E?`2d!Z^n4LZNzEODj3AoW&9N|+lA61DM8Hx~b3gkxEG0GfA7up5gqpF6rKCfw1uP{sxg0 -meQc|;DeH)^Qe)i;8N@||ugoC)Z_JU#u`cW~ESV|gH>_9))`F*gIq~j(mB{j!6-o#Rpj)AbmGryJlDV -BKV``ALT#53Q|{Rc}t^QXC&V2Nk`3|k16cyx4xC7$^U9A{vON6((($;+en5o -*YX&IzGb^&-`V!DlGBn_)IMEjPv`*B_8(IVu`1lM?xLb{(Ry%s{v~~4&w0)i#&fm*~g)kxK#EsyDO~ne6yYhb*%D -yv!0Cvt2}=`y^-fQSmycj>0zEoV4dgBXH|j{3q1=H9gILrlN=?OoycW|{P1h9G2oebZ50Eam)BM^5aF -xTBQX&5jHl(@bqwfj8fAuJ{m-s_c7vfgSsUz}jTYyFGu40x@7v5Nt(l`nQP;C1rFAqKogzBtT)*T)zA40x8lILCnR1 -YcZW;AtO-o&tZ%a_^_UIRC{4hB5)QGt>a|`M7>ACr~|EfKRQ1Qb05YWXO*?Xq+J^XorTNqMaIol6Gkb -YTB(KC~A*}psKwZvOO31(J%JvPkU$OOj<$E)jRIb`3OVM$Bxf$Gn5N0DyR@FC`j -}h{sg<@GW~x6UGdwy~o^O2PHIKZDb37jM&5bE#hG6w8jxP{fe$57e`0;Bt -8B_y(&FVzS&(}SVIW9rX&({?FXDICXx{Cp?(62W#0J1qYK~2qL^@Jz;%QIh|WvBt@>pct=fFARUqT0n -h#jK&I#X+7o3MQBL<`LI~STsR3yec4O4|Of>b(udDseDa6zR@Ol`0^4%fq(`W5)?h|Vkm0Wh(?=utH+ -xd@&U@1=X5iw0#eOL1az698X(=mx`0#*QvsM4ifK&@}0jU(~A?i`Syr_N)b1?_0---pKehd1#q+S_wu`e&_--5mdbPJ)c0sR#8by+`^3P?2r`ns&Y5c(R{UziI>e -PJOWRwd?VUk*@I}(AQ1<6!djVL(tcV -ZanlgqMw4kM)gzB*QkEV>+A6b{Zt_!^;g$bL3xTj#k<{<7Vn6N!=SIxkL?Qy&Qt%`u -uZ&d}Pe#`6YaR-l*4NTk|#6)4@<{;I=R6vTT4AA3Fh8hA=@0ANky;mV1^5AVU=a4KXCbZ>49{<2b0Bp_+i$1gbJD9Uk=YGEND)xuIh+`>deqpF3 -d+;I@M5V;Q%`J#5mLEJ(V@0g$%)jK9AMmog>#j1sgfK&^iFC8zVg2zFsg{gp43o`+!7Q*}Lup$xBjeZ -K=R|g2F<1qmO3VBR`fJz=So+FQB#&guznDHD*CNrL+%Ek=8$UvFl7j-sf_^pZ2QoolRE!A(q`*thasL -9ciZKEbeOZ|mnv{bfHlcS}2FF9KBd!gXQxN}t8xUq;Ek3^b-xbdjDF~by!ZXCpouZz)AHNGxJOWk -sG1@Dp=yTkhx#e#OV8<1h-2pTsKhaIdSpn=oIVw^bNv+br6=yGoSmzu|Fm=RY2;upf5eAM`4bc)1xxSOx!auJJ&6QzVyUB6SH&u6!fJh?wOpOtEc -4boEzT|vvb8pL(b0mw@{*Err-@RJI7NL_b%@(JFW?0oK#Gp8pzDdQ4VBg<_(brP$PLmWC1iu7$@}{8z -n(zj*XfiGsi|zkQwAr733iHPbdpA6KT{1nTa&&g3Lr3bwOtQM_rJa5@W5!Oo>q$WTwO@4KkqtN`oB4u -Z-mzGw(%dkeT;lZO1_z8suVDslPB2kb19%fYf_IUpi{Qs*r=!d%^GO;kpp34(cgv1~;QC)|1TWTF7~W -dPM8hLJs1{poK#Q2@9IGV>sJm^Pr>hA(@&)WQctzcvK6hfY=!J!3TmCTDP$bV&A@7zF@RFUq53K -Guk~h1j}77u)N^{Qe>qT3>ak7y%bO}16^mmxRWyo?;%)>}Pt^pZo`Tj!^;4;U)KeHOhxJop?Lj|<(ej -#pst}NR3cYNApUTDB-T*(9i?zK=`l(Pr>M8WH%lfHUK$-lbDj@ZgjBeCZH36xoL@#64my5N%Yw -Y@Rv9@=OU0*KN_Efjc#oFFAo`L0UG8$D7^mSc9Vr}n+hM+HH^SM~tQ#PNAwY?GjRQ&xGHlK^Ny;1#C_ -4_SsJ`bc(aHSVPoNK#ny;y$+jW4N*^tbF3liDRGW9L_Hik4rk$IOc{k2sBnLn&7ErQHog|aXDNlLOP*HFm_WS -3HrWe|o0O10?Ul*60$r4=fMP)}9^0{J&lcli`l+=}Iz>#nuxYh@f^o|G1?^;xBWLD5&0Ly*==l_CTEw -P5Im8KcC4$_-;fi5%%QeF-@N{bWoazlvBQ88O{KERu}audNZtuae04*gTmMs2Y?JsJ>RLiCBi@(uZM4 -E`I0+)u>oNg8_luTCoJuY4j-;K{{PuVWXC79p+VNNkhMiT)8m)Q>Y7HxNp_vqP}($b-ApsRV*NTaSZ7p2Wb<+t_nh -a_XuIYvaMOxDmPl*=|id32WT8g=s_r@#Nh*C5{H$S5^j|M4`szH%rw;&}`HAqEp${l6WdFtMr@u;P~R -wls>SQ8D>E-;gBSND%BCGD#QW%Qd0Y`o~n{6!5ClT%BdNdFjpHe_1dfV5zuL6K@TzuAah@SY8kSTOI| -kf|W^Buz^6Z?bn*pxI!Vi~9cSk2`rIMlN!e?H0tVpLM-hEl}jhTRONy7v|(5bGbM!e|d#rNkFGwOUp( -6a?r)Vy`1vRDTmpq^Q}4Zn2&qi)1CUu>+-jiw_MC-MgUhlfB6;9e`>|jUz+*nE;MnWLF81V%BgysH4Ut+m`(eJT5c`pB7EU!J6|8JJp@%pN*DUdykW -I(nv(g3o9ksQb-MhYOC87YC#2Al#F$203UG9s*Q3nMszVEtA`A|TrsiGlPnQZbegd+LA0a!>t_S?+gL -ulRizBl*(CP-)TCDlNNO#dftyi!N31md0JM(xOXM{OfWTM5aVrf5pEr+j7PGJ+|eF_iJp+i%F9uY`G$ -AO#QO3F}7t?-Tn>9Q3?B>SnjJ}|1-;d1?=Cl+}qaQvD|Cnzp~tG;U8Jp?t%|7&^>h1 -d9{*3{z=l{TRujl{BaLNf){s$PqZ(L>ozj1{Du|a_W{P-XPcyp>sEkv!| -Fau&~ivbbKGJqeagJp__$g47d-?+tq*vH0zC~G4?)Ee -c#^nYNvA3y&i%e|-mPb~LQ%)fn?0q><>V!0pOzQ=O!p#L+={owYuEcd?o`z-ec@pmluAH4+)t~kEcd3Fu-u1{HI{pOt+U+o{{xnL{{IWhJ^%leV02{8M;bhCWjHZ!GuxuVlIB|EDbX{J*?p;J3_|Sd}Pxc!k^L_53Q!y`EoVx!3dSEcbeTgXLb& -t61)*r9Wf2cUEt*+)qhYv)uQyH7xi2Y%R;ZKYokkC^UGRd6|1HaXKl>5OeLv$_LK*pv_G6a&j`shs+;=pZ6qm8@X#bPtEjn7)0V*vzK&3?oXpyZbk*)ZDS?=xsf3w`%|NqBw&;RrpiaJXioj=Wo!UWSw>QKLfJW_}H -9poxIXckdof9_1jI3q~gwVz-F`e{GK2=vo_h7ste{Tw6EPx}Q%pr7`Oj0pVVlVCt-#%N`ME}{ui0D -6?8G(K}d=I9;aG9@|DzV>prh`2N@-Z_V+Zcfzbo4O-JLuTSi0G$#7!mz+KO>@_9%4lFQ#v5Ntc!knlo -4SE#~2ZIz+S8_?11}rUDyHl?Ygi7?%Q=?2i&*o!VcKK)rEez*Vl!9xYyT(el9U0^uzw3F7(6xpf2>oy -}mB`=>bMWKc(w5%G4$kkV%{BT+c`dgu5;ZXr{WE7F5qnb+$87!81$?>Sm_8nHChyOm*^rfQp%^PIey1 -^G$W~5RY1!sqU+cWI)*IqDp3}n+IQH2dBDOF{qE3;;V#9Bo3$e7MKzhD^s1UvP|fQona>Q)58e#!}oQ -Rna~e+Hl!%0IyW)`{dBTx&4hl~wPr#;92{jrKkV{QlQPxGE)NAMQ=RPcP>nLxxt$T{r*j7*&`&43$;= -$ETcUtus*4Fj^~h8gdnc5ROm%TogdFWu7yB&~jZAfMyo8F8sV??twp^sd7T2jR4z94nb&Bs9DzU+Js* -Bwp62wzo+|RMSb*hWqA9lA+b#Xt>t@FtRyWI72!R}>71iS1jbHOfm@LaIV9XuE8atF^XySZSO`+P3g< -vyPacDc{zCjVPvKk4t-q>wHD9a|LklKzg_E^uVf@7Y6QBkAwho{)L}9h)FFkxr`#Kw*|NOY9w;-pxY* -68O{I+*z@6bh?{6D>jZ!cXMaOzR~IKU5t3Mo!-l_#{Xa2mvqOCq}T5KD|)Fu_UV%VG6KNjEr0}Yj{ku --UX;6Rw})~MRqeKWrIDgWilXL`5+_l!L{amwd5gD_wvv}kW-__UpI}fd#SPpqzQtScoT`OzgUCR9_lx -1)Kmc!z{^eq~!>T#;qh7DaJ5_eBv7fqgt+VZ?GlAUOuST`M9diJ7u4S^`U!^4~_f=Nna=c$@=Nf-(w7 -u33lrw?$GJ$egpj;#C7X{is`s!tHIsVpYd+n>_N9B@0xlG1A$^*ZfbF#MAia@!q(nH+z6EFMh -KJ3G?DNdx$VEezS)O^WryqgfK6Dvr~k5@tZx`#bPghv(tom@tZwHm>0j<|687r)t)gn99sJw -=!ozuD7-dGVW_Ax!o|{XoCg5A@^xf=(?h0XEd(B-X%xkZ?YlM02HFupbu -f67O5azYl+)ctfzR%4Q=J9>*7GWOW=WY|`@qKQAFpuwZi-dW6pIai#e~4Sb%^xHj;4LgU)N=LwB#1D_`}t_^&i(6~16c|zm -bz~>2#YXhGrG_DPNp3t~9@Z(40TII)&#mIT -UK`Mbu-Ii5*bFo=3K@Vlw_zEgXxA1G(!TrjnPa#lV^7Wmz)C-0Z_Wcy`3ehFgdS|Q6Nf%^s4ELjq`Ut -leliLXoTT$Amb1==Muf#1#LS}7|6s*gFrbUUhQ0yyeb-5t_<`CWuRSlza~fJ0{K=N*?wgz^P@}y<#K*OW#?M -pcLQ2NSp>>;^S3j<+1n2WMZH0wT?YYvcqc$l&aCYx%o{7$4iM&zm1_qH^Tx`xLxg#AowdV+d2^k$BZP -T#owX^#yt&TWQNp~r&e}9#-dtzx7-8OAXYDv)-dtzx1YzD>XYC|m-dtzx6k*<6XYDj$-dty`yGHU(gl -@4jg!%ECCCrcCS;GAI%@O9u?;K%%{LT~R$L|7Re*7*H=Ev_6VSfBB6XwV73SoZyt`g?Q?;2r#{H_z`$ -L|JVe*C&?Ht&S!7Mmx`kKZlA{P^7_%#Ys!VSfA;3G?H(M3^7HWy1XUtq|tN?+#&p{8kC`<9C-ZKYsTJ -^W%4)Fh70|2=n9jkT5@f-L=%f*A&(s6XwV731NQxo)RXs?VQB2os%SR -zeM&EiS%gOYk}vG{d;>&^v6*mzDB)sP3RNtwZL=qL-&(B2zZzimXd!~&Xd|}@^{m~bEJ{%U(!g%EsbS-Q#rqgQmK1I0sS-D)f}muw=S4n*iTsGVF -w6{J?tQ1iH98`tnXom2{RscgfQ!2Q-q}+c9gKp!=?$#J?t1^g@+v{tn{!Ggbh6GBw>|@og%FEu+xM!9 -yZg(N;JJt*3Jm4wO#^0H~UOq}>or@{BCl;B^^EqK$U__OO>HUrV_VAnZ?DPcD2?dHY$?~L?KP2ycGpX#a;aE1waYcxF5c;hlMA -or35z`J7Gbf6-6kyYum!^U9=1rB@vtSrtcNWVmU`FRl*7nyGvNAHUax`SE*0m -><8lg!%D%N0=YK_k{WJ`_RP#{al@kd5I19EVhyFEZE(XIjCGF`-evA#a6ym+g_97v~A=(+ctrIGcf+9 -f$=R%8&SV3Fm7c7`3^)Dm``SvoM)GT-z}p^jH_k9?`W@ybwC+evA?UmCZ3~=~mw(rfTx| -beBE0eNOA^PqDc|t+|A$m23Y2+7W#T}Y$5bW>lzBpB`hhY}sZ2*3=*-Xd_Ok`;Z_7Ph?KLrnsUlD7xj -ywuw!5eIT%UR!z$^ZH85d9c`S~7!GELw<6gzLh(fNA<9Ebmr05?(iCj#6o;hza`Q-t3T;N}PaLV%kb{3`)&X7FzW -II8|T0gj_T5a7u99|Sl?{+0ko!{2oQybJbw0$e=)K!A(Kj|8}Q{6v6@#~%rB@%T>yTs;0nfQ!d}5#Zw -S-vqdL{Fwk34`L!-^&%$XJuhM+Uh*O);teliB3|zzCgR;LVj^DbA|~RkE@C2H=^`fLeJ)}mUgjbu;!Q -4MB3|PnCgMm%Vj|8|BqrieMPedORU{_jSVdwY&Q&BP;$TH$B2HE$CgNyCVj|8~Bqri;MPedOS0pClct -v6&&Q~NR;($eBB2HK&CgO-iVj|91BqrjJMPed8y+BOFmlueM`0xTT5#L=PCgQUT#6;)@#6*Y&0t# -6-vi#6)NX#6$=M#6+kB#6(C0#6;)=#6*Y##6&0q#6-vf#6)NU#6$=J#6+k8#6(B|#6;)-#6*Yyj*0$E -(eCZX0(L6k;!cc*%s`BX#z2fWV&U0db0H_PLscNMLsB5JLr);GLrfsDLrEaALq;I7Lqj04V*x{C$I6A -sjwK6`9qSY#I~FHIcC1K<>{x~n*>TPzksZf864`OeBat14JQCS)#v_p(M?4bQal#{YyEx#HNQmJTq>PSq)p^n5voasnR#F37~M4aeIOv -HhX#6+CuNKC|Wj>JTq=15G$t2M+#yjMd^#7i~AM7&W$OvLLn#6-MHLrlbrG{i)_MMF%)D>TGJygx%s# -LF|pM7%jeOvGz5#6-L^LrlaAGsHx^EkjJit1`qyyeC6U#7i>7M7$wGOvLLk#6-LsLrlbrF~mf?6+=wK -D>1}GybnW6#LF?EHh$YxgMl8WzGGYlfk`YU=i;P%;Eo8(J>>neRVDlKU1Utv5zru -E|;1>k!-hM$~rvfhIM9ET!&Xsrg1$Bv~P|z#y#0%;TArDe-2u+Yk3?Yz6jMFEmH^k8s)Ena538J_>_^ -I*^n;>@MqzPg-j+vm25NAvf#c{v{Q5>gB5XEt{1W_F4N)W|ys02|QCrS{-ahwEEywY^aO4BJTO{c6ho -wCw&%1YBID@~`YG@Y{2bjnK8DJxBr!cvetCUTGJ_OO{c6iowC++%39MYYfY!D -HJ!57bjn)ODQiuqtTmmo)^y5R(r%dve9(PM$;)9O{Z)$owCt%%0|;E8%?KdG@Y{1bjn84DH~0 -vY&4y+(R9j2(dcKh6T!XxV!+OCD-x%P6p7Qsio|JRDRKS`DAJ>e73tB$iu7n;$^3=@MSnE0qCc8g(H~8$=#M5=^ -hXmb`lE>z{n5mV{%B%Fe>Ab8Kbl0*A5Eg@k0w#{N0TV}qe&F~(IkrgXc9$#G>M`=nnckbO`_6R -n~g8mlOW1||nF5he#QF;#R$1CxW82$O@Dm@2BHNfp)6z~mq%!sH+(rivzMQbm(AsiH}mRM8|2Ob%jVs -tA)NRfI{CDZ-@56k*b2iZE$1DNJ^zgnRrAfVKjP0BSNt05zE+fSODZKusnE(4PTCm^3g`i1C@CP8ygg -#Q02+C{3nFlqORoN|P%RrO6eE(&UOnX>vuPG%!<$@wuW?nq1K-O|IyaCRcPylPfx<$rYW_ -!xuR2=T+t~_uIQAeP;^RDC_1Gn6rIu(icVeI2r70Di(v*r$X-Y+>G^L_bno`jzO{wUVrc`uFQz|;8DHWa4l!{JiN=2tMrJ -_@sQqd_*spynupy-rlpy-rlpy-rlpy-rlpy-rlpy-rlpy-rlpy-rlpy-rlpy-rlpy-rlpy-rlpy-rlp -y-rlpy-rlpy`yArc+j$PFZO>Wu@tqm8Mfxnoe0M`zcMOiItTmR#uu=S!rTrrR=9Pm9n4GRGO4oX;NmT -Ntv}KW!9RMS!+^etx1`+CS}&jeo9kodS)&4%+6eKkG~(#RzTBDYfU$;HQlt1S}rl~biv(`k --MiVs~P1I~OQM1uR%|;V78%@+~G*PqBM9oGMH5*OTY&21`(L~Kg6Ez!6)NC|Svr$A%+fzhM+fzhM+fz -hM+fzhM+fzhM+fzhM+fzhM+fzhM+fzhM+fzhM+fz1G+Mc3p+Mc3p+Mc3p+Mc3p+Mcqh(ngBBX(L76w2 ->lj+DMT%ZKTMXHc~cK+DOqjZKQ0fw2`u@(niXrN*gKCsErh9)JBRlY9mD&wUHu?+DMT`ZLCP6HdZ!O+ -E~#?ZLH{{Hdgde8!P&#jTL>=#)>{_V?`gev7(RKSkXsqtmvaQR`gLDEBdI76@Apkiau%+MIW_^qL11{ -(MN5f=%Y4K^ii8A`lwA5ebgq3K57$1AGL|1kJ?1hM{T0$qc&0WQJX0Gs7(}o)Fz5PYWs>lYWs>lYWs> -lYWs>lYWs>lYWs>lYWs>lYWs>lYWs>lYWs>lYWs>lYWq?j?aUST_*)EZ1r#0D_7xr0_7xr0_7xr08by -b-M$uudQFK^qlntWRDEg>1iau(MqK{gm=%dys`lvOEK5C7kk6NSXqt+<;s5Oc{YK@|gTBGQr)++j_wT -eD!t)h=wtLUTFD*C9kiau(sqK{gu=%dyu`lz*vK5DI^k6Nqfqt+_=sI`hdYOSJ=TC3=zHdXXdn=1OKO -%;9AriwmlQ$-)OsiKeCRMAIms_3IORrFDtD*C8R6@ApEiau&nMIW`PqL12C(MN5n=%Y4M^ii8B`l!tm -ebi=(K58>XAGMjHkJ?PpM{TC)qc&6YQJX3HsLd38)Mko4YBNP2wV9%i+Dy?$ZKmj>Hdpjfn=AUL%@uv -r=88UQb44GuxuTEST+v5uuIQsSSM*VvEBdI-6@Ap^iau&{MIW`fqL12K(MN5r=%cn!^if+V`lu}webg -3;K5F6T{NE>pxPbh>7lg>2oEDzVN4}41^p8&oajpJwjS$!DADb` -f|tAKj^5of+dEf9;{IfeZ4-*Hkwb<&SR&abf=WmJk=`kM9U^asK$85SMu$KM>+l@6&!l91lJnAS74Y+ -VSAiAwnDvJ{=~+@!-=DLL3i1O%dXF@aZTajt8Hny9hj^Psa#x@%hU%-43=9Cj)#VP6il9oD6V|I2m9a -aWcR=;$(n%#K|yF0|fi%<10dts6j_92p{JuN^fj2|Lt3g?GNn!@@alBV!}h@>gZA0lZA_lHQD! -u}zWrtp7=q$vy_B54W-h^Uyt=^-knuzHA!DZCz{VhXc|sF=d-Au6V@dx(lD{2uPh(^gd{3VXI -1_(FN3f>Zv2j-G!xIznEq=q}v(lL#+bW9^H9n(ln$5>6rSWU-RO~+VG$5>6rSWU-RO~+VG$5>6rSWU- -RO~+V`Ppqb6tj#x4I8L0Xv6`rfny87IsEL}WiJGX1ny87FsC+vFh)4?WiAf6ciAf6giAf6kiAf6oiAf -3ribPGRD ->VIRD>bKRD>fW#tftOV=#p{+;D|BsIZ0jf#3^qlNvP)Mokl=rioG0#HeXv)HE?_niw@rjG87!O%tQ0i -B;3Ys%c`?G_h)$ST#+o8Xv2siB;zT@QOHrteQYpO(3f#kW~}NstIJ(1hQTN(d}RwF-hSXF-c(?F-hSY -F-c(@F-hSZF-c(^F-cQ3KroM(q;QXzq_B^er0|bG($180k3Z7V)&Pl0$pEK{lL1y0Cj-1HPKHbqI8wM -(OnJCcOnKN+OnLZHOnDenOnEp{OnF#SOnG=yV$8_Y{u1^S(+d6+lPA;k*c1+xXi_x{xtfl-nvS`ej=7 -qSxtfl-nvS`ej=7qSxtfl-nvS`ej(H#*e@4_a$<;K;)ilY~_!MfI6zY5ft`#Rxp(aqFCQzX!P@yJJp( -aqFCQ#ue5Z?{~B9eN>7Ln95wuq#ju|*{Hj4dLmXKWEkJ!6YVTB-r^3@0L~XE+f_J;RAe>KRT%QqOQAl -EQFelEQFelEQFel9sCQ;2BOtMbB0uDtfjOQL)tI^3=1Hh&!;An2K9OfR^Zn9+8V1-(pRN-k&(=#}4)q9dtsHd$SVJ)Zu8k7{Tod;Ra4l>IaP4aeaLt<%;955$z%_18K&Cu -7ydr+`LVNO3d-8$y~V`+eoLtcoZ4Fy@_R%@DazG>Z*aBO#DP5RHSyiux -)+J>2esEk{pwyUzW3H%6W^SJEQgP -x8l0xV)-4~I&S3sV)-4~diNVo-EslG)Y(9`Up5fm1+KG7#y5x6Ik~ZTo3O~k76^+yY>}|U! -)SRZ|NXmUi_905hlLFUgv!=Zq&K_er0=2_CL9i{aJ21{f|GVOZWIIG4&u7<5)c?Wj|N-WVyii+3Onp+ -~*ZSTq}RRLx{_f&-V#&8S?oNA>tVC_L|F!FVlp$toZzt5SJ659}wa);`0kaTt0j`PKe8fFNX+mx$xyM -AubcX93jNz!IvpQTo!y@BgEyv=Wd4A=)t}mAOw+FC&WeO%RxdAnJ0v}p6l~7LR@@4-z5a`xkm`%^NMA3rth}CR})oh5>Y>3rtNYrdd)NDx9Y)Arao~U_{sCkg6d61}ikOX3qs2L!SP; -IZN+)w04=Y4^mnb!}rLSN-lKTz+!%B8^C!22FNvnVh^dtZTP{`6J;^i}=@)*#+@-ac5?jKnoj{8G+e~_r-Q*h6%K=QTsy^m?;>wC$xdKwQ5gjRX*B4+gi0Jw1Kv@DtoOuJFqH; -ZJyh+u(k>9~?uc>*Hsy%M1_PD8nxUxm>7RE&{8~LS=EGvb -a!LT&OHAR2CNj78fdu3zfx%%Hl$0aiOxfR9RfAEG|_Rmnw@(mBppX;!#wnDvRZ}) -#|#Ds#4tuwZ93t3{5W<_iK7moo{-%;C9n10_6%ZmNdOms6tI|AZBmvwLrNlP_7D;Ys8z^P4rbuN_JYY -_cw&Z-;jur3d6Jwq-!`~Hz37oL6p#E^^6zJF%O1!&)I7;+KX_b&{&rrY-!jxV=ywFgh}Z8KLcIRK5aRVCLx|T;3?W{BWC-#4Plgb$KQV-O{TD-s*M -BpFc>S3n*E?_QKlkIt{__O8F8bF5xDoUZ1h@h8*WVL@gZ}uG`TVLdGnKCGt&!iV*=K=`npCc+y%za=1iSWgq-eV*SF5I(G@ -1;U5*v_SZ&u{qGT|jM!y3GX!zhFLGDL}GbaghoZdKelpx3I&DZ=_j@QG*D+D=ak5?85O7T~#1Z -DWGMS>i^N0;Uaa)jABe2XB*n9+sH1UbqKkDVgOk$m``AK9^dIK_X};I|%kAz0SK>FWf!cx|m8B*?{U> -)cI(T)f6lT^n{LpnLqOnYId!7sCa9T=3$|D}o#^MsLp%sW;arVQQf^_EFkYuN#5`eiY>puCV)+0;ju%_2uL(lD&P)^J;x&3rZHO7e=o+; -l<_x22d_xznt#i~Gm^TcMJtfG+Yk1)~L5SB8N;_$6H8Do}c3L>PcAqDPF-HBiK+6tUAodHqJ}C<%UV- -7mz#aNtfvtnDWC7z>^4Od#VEsE>y(kN$ejQF+mjyEaC(Z}%Aoe9TG2XcyzNZlcw%Bmy5kXSRh^-vmV= -Fa)B%pch9x>D{w|hztm8*q{-dL%%G`q*0bjziI-`#zYk;-L(a+PekG*G|Y9m?HufgOqMwLrNjP)>TSO -%|9%Wzqv|vOqktEO5U(P%bbF%E~~wQt}`VJa3-L_RGKeD^yOtOVM7FbGpLFxD|otEv$TwA`O&Fg@@Q) -6FYm`Yr=081(9T5}xS!lP+g_8?$D#_9lRI6TG7z^im-j2b>W)-yAls$HPWm1 -Ju3w1=aNTfBfa`t}0$jJ-C%|>Lh5*;iS^`}6niAl;)rDiKkPi`_HQI^_ -!48^-1CDItH?y(ad1$7L3Y`;}QN$_;iMo4@N`6YP%bvgWVE#y|I(KhM725flM>NswdE=Bon)VbkvX;o -F2^)$?wiAV;Ll_YVoOfZh;~`bq4T7*&cm5LB1Jm2i{ -A&kl0G;bXSf&n-EE0s7#E};SVW0K`4fa8WK$OjLdd50$*>@K -bCMv0Z0bHi2-(y+LD*ltdh{k?Sg9W6KSRi-4-w=-wlPOH^jE6HV60S!$EFB^ve22-??4G43N%)b8|Tg -wgovFuN{|I~j35N;1Vsfa$l(d%Hdc_slSc?bz)tcbBVZ?q9Nr4@6xR^(IyFlW;&ti@K}fMvYh4H{$l) -pK6tRLFp87x#UP+k0LrlbCaO=Qhg3$Cc#|c8y&k&`s7@W*MSRf2-KXZ>DwEfHzg3$J7C@j#DhuptGO& -*@1K>~X6aCVv?wEgUzE`(L)@GK1T+(36MPD2*T}4=+=$Ku{iDe%6J&)$26%9Ny}6fd+2u>KxuYO%Q@| -lYa|AnWu3D$3_ir(6EAYqlWX;Sz-g|aGp4ilcR>WDPyq_baHEs`aK*THC*5yL%*`&?K#dh@c$`-?ymOTxP~#5GMsQ-)aODy~pvL -MlLC|`x5_v5z;dibfhwoF527hIE|2;vNYTR1kKSMws(R>1XNQaO3i4c%S)DGB1I($UkOzOqsKHU);Nr -#VUSi|vD!^cD?A?xg1Lm(a#S+JLM;|8r7u$grDgjNPPoND-lVuVel!?pbcAs$bu7s0mD$*mWZ+1OY*T -&H;8=&1J3h)39AI$Y;EB3A1(Bw(lMaQ!Vo=v80bA_$>+K@E(}rW^B*2tw+-peYTun+{)63S+^>d7ruhQ4pAO&9qzn5`CwEVu?PBmahi@p -$u?2N<`y>rB*lIeSCgNkS>F_PJ6}FUaEYP5fJ*AU{<23EUuF}c!QR?KeuXOm1e+>!qj(Cclr5g)0O~M -gU!}t7KX#EeAfjC5J_<`mpI7Mpsf!3lpMrw4BS`oWhHx_9jg6*sui?r^=e%8@JT4duWsmblDjKSDBL~ -+5+*3l8_>9MtSbc9y0*xNcfLh~eSZk2yk0ODtcK|94a8~~!J3%0qAc=?TeuA?dHSg_G`bd=Tx*y%btO -237zuA`%rn%L_)nx?5dHoK0dX=;GouA^hu2|~P%Q(|Jj>*xeU85>?lr??u(u-o@3D%kTnI!QeyHocBc -Q2&NqucK4c1>>V8qtn!3;`pi23-{TOfjXQ4i>&$ug_QN< -kRRA+)xprX2EcMwq?rLNR5(6sbd6?pI6!N3g* -tN_p*24EfFKvJ%^7-R9HTYjl`;;}8eOB#21jX)uF>oahiQ$jQ+J5tv_{uSh`@nblZ6?|a-6BP@ql89Q -?*7nX$Zi%TB943$v9bSbc>>bv$aOI`LCesFU(Q{1&3gbR%j`TW3Wbdsqe)>Sd)b-d`G0%J)#Fr!`gUE>nxmyHM&pCfsZ#?xJ|1%Sb3ud)KoC -@Hl9%1!_FH$pz#-m-smBP1(x3EA(0BE-smCe*o`+My-U+QID3;@&q%j{xi?w3$3KUliATd4Zs92nrZ_ -HZ^pw`JI52Cp#+Aer=Lxqyg7&N{!g#j1g#22(7W@QHy0@R -;*{K6M-^KZJR6nrd)N6vkiqn*m-YX!l?-NwwuZR@z=r&$c4+SG`YnE~xM%>mcaRNr%*6b645dX78KyN -GO9OVeMa&Ek(q=S98b#9R$94NN<_&H$~e?}uf;)UhuS;7#O^N$JgHL{z8AzLpIwc+1Q7T@0@Y~W!O88 -~>8r2~fv!%XJGTY_BFHuqB^!NS|RO2Y~)yvfob8X@4}wb)a_khe>RFB8`HFuo@e_Zs!T*a*6HjanHt- -qv*@5Ny0n?(1RWZIL^b!^Yb@K!XQtyv>8u*Y~~1oViXI^63_hRPgb(Zg9n*&d11V -u-CZFpFvP(@+nhc~ke4hE>2YD8Z9O7J!9d%3bc`Tuz?!DISuY|_sg>ZMZLLv11_y2P_&h<#qBR=4VWe -%X5!qm*Z9U^^pod#}&kv3H$U5HN(H*EU%`!5Cupj>dhMZIk7bG-knV+j>tk9k^{CgET6^ZyO&zL=d8Oh6V%pZR5k#`kvo*mnOBD7qlCc -yP0SC+~B|S7TBlf2=j!;(=_#h<;I>>4$EykO%n!KZsYfEyzNYe_x9sIJ5_M(*qo(K6V~4NI4vgO?2V6 -8yx{DOkJBiO(`AOoY3AX1e&jmj@chQ&ufUDtw1$HnI6gsxuxAG@->2~icHsB~P1oTDj!*J~A)!xEV!{ -m^pQZ&P+`#ck{womUG(VUN*m#DB4L5K++m&Z?xPjwSL~FQ#W7<)d!wnqI&;$r>;CO~|!E*zbdH!E`eJ -~mPIqbm6@)~t3@B=r`@t+}Nb3`5Zf#W%%4*bCJIhu*V51g!s7D3F;)3OJK;AHtNtpec)Zk{LZz!BU$P -wNmkf|KR<^k}dICo6|&mH|(2^E@>cJi+m0S`on$9ABo0!V?@{CeFbV9A72{4xZrn3PXt56?Kgim(F5^E5QT3f#O%wT4AEzC{B%EV}V -6>NjA~O;$Kk@aV=1)H}eV8!ynh4<6m-B^oQ?(Tx{qh69goI;0j$(@VT>%pjNF$yT0x -^3FKlEbOnK16*ijJoZ^Gy#H9H@S0-SPZ9b`!GcfPTlr2Kbni$_6bT)ICa}6E)nEnwtb8O1E+3#hB_BG -b=zl%#4ze6cOFqz!>QZ8Km>tPx4BGrgj2VDftcEOAzKgc|J~cq5be|feeeo(IdBBGuhN1Aj^OY)Epy- -qZeOGB8IIs&m3;uu5?r08l^-m@%~c{B9Kp?1nrp!j+`dVDpJxcJo}nHNhT!I1el(C{^(@Vr;0SKdcga -z}_S>FcB?t^yz3`kcB<;d&f?Q-a@2?Srq+R46Lu8g$2tv-T93%)iyYikO#O2Xxg3!^f-hWIO0(1W?K} -gys`w2qQuDv4&W8)gv17qXb1A<)6Zm&~wc(&i&Lv%;jew%AA34&;I_wW(IFgo6y=38RLaF>>v1&qJ#* -KY{ItB&gz2tv@_ydnr4E$vDzVEk>r4Sy2kk3ap`x8HyF(|5o4Kfm~Cr@P$u75Uob_V)eq>p%W__n%SspKZDw)i0*{#ZX%Uc5)L -fYFQNJ+RKJAkmr(r@s$W9&>r?&uRKGseug}b*`t_-PeX3ud>er|G^{IY_>Sw5ahU#aieuh(n>Sw5ahU -#aieunC2seYE~XQ_Uc>Sw8bmUEZtXQ_Uc>Sw8bDb+8f`lVFAlOVaA|NaUv_0~WN&gWZF6UE -VPk7AUtei%X>?y-E^v9BlTmNeFc8Pz{V7h_OHu_7@q{Y%0YL=_0oB$$Ob9hja_PJ}cI0!{_1ojvPANl -zK^~Gj-}&#~oi7*Am{#iT9QLkz@tSqk^uRX{Xh|9kvLB3fklYl-#|8)IeVXF4Q^ZY6%SX394p_ok_DD -|j17j&=@h{j^!#TrF3(>knVQWCa*IZr}-KBXwuKz5EV~1-zOw@K -1#3gi^I=b`#0U+h|MJ>%m)f2{*w&%TN)d;&~-vGw_1IvnrC!%e8+KBvC&NOvV -9AwlKm-imj^i3$#Ps84DBKhsV~|Igr<5ewtL6*kSlA4+SzxWALse*bFo;=JZGPx+X4R+BIJ0)&!+f^= -j0K{#1UCEanH6Y^uJ@Scpm?1iN}p)Lp6>=bgQjA;FdQ8;*Xa%FJE?La&u{KZZ2?nD@!dZ&dkqKuvO47) -KM_dQ83cv0sv4;0|XQR000O8`*~JVJ3F1ztpNZ4IRpRzApigXaA|NaUv_0~WN&gWZF6UEVPk7AWq4y{ -aCB*JZgVbhd5u%cPQx$|yyq)c<&sLJAArOK5u{!y5`}AJ;x(~w?8=Xp{yl3u4^u!Q2gmWu&W?9Ctzyo ->i$1XSqxo#{;HA;^v|HX(K+j^axML2XO>AK-IMXBGNONGBbjabW -fUn)epp)*xTT@;+}c-AM%5-J@ZX8RlhP7u*v>@4<%C@ePl{<=-qp<==l+h-pm|F5_+1~k&uc~-fCd -puaA3L&RTzEBfpov}z&mMehUf;D+*KD}i{EoRD`f8u~FMg%p)eq%f&Tq>Fr;KUlnW!Dif4~M*%E~C|% -7QA+y@lb18oVUn&l36B!JZVL<@`8H92D2BU+)%X@tLhS6@&|0|X -QR000O8`*~JViT|rS00#g7$QJ+r8UO$QaA|NaUv_0~WN&gWZF6UEVPk7AWq5QhaCxOzdvDt|5dYtw;^ -tz5lscO11Zi!=#fCnH4qK37$=0DL6xyb3ZL;KwR2rx2KKt%Si6Sl8aXTRoo4WV??vCUmm}av$U5ucR^ -W&$Td7iCsDM|QL##t&LUahlSLdsTrCX02#JLSrZNiO4+`7SoXe3W@Hm7PxK^3MT`=}mCUbAj@rIGtzj -!uK#9!=TfN=YS-m4&W1~GLoA$7s%n^mmVZpy71MzsE|`!zQ|JE;fdf)4*aavWWLcaEw;+ifd1U0i~!d -a5x<-AwS*s~%;QNR`O7@d@`w37XJy6{$-L0_(cx*rgn*ZK@(S(p>Y#bua;@8gE=HV_P}cQ*t*qOv-b2 -pUITt*Y01|iTs@*cCvN$t%n+y0}_<+2k)Qi8^2Ajh!*MYbV^v5tN%#m}WGbB*R(~F2iUgl#DY`<^QkG`r_n-=@78gY#$)t!Pti5;Ll7< -S$V(GrO%OFl;iD?u(nfEFsJzK*#v|x#fUL!-ylK7qjvofZWTzW;%Xw7oABKTKgT_|1@s|n8`n;X|K{! -^}iaDDOmE%0?VU4j>eBlQEx;!zUr;I!+4#31{m{f*&{HB!b|gG+hZ4;7#~E%eO=1 -E6ABrLgX=FoHoII4C_q9YAF?gozOkK$>71vi-%fNrLO8Im@nkz`GV(ryPy^0FVJRZW=mtMLQi!^o9i6 -`vXxRz=n3y$gg=?JI`6Hh1H)F -D&T&u?mPNryn3KuLe~aRR6|r$uLz&83#qBrQ52POe~v_D%t6!=j~6lUyA!d5bCDdLZhG_qu#{z>RQiF -^^U3vyRbb>o73l-CNP1+n&wfbKZVx6t9xNPsx5I?y9ZwuBPhBmv13W -uzQSzo$Nv)t{{0_pqP(LTH%4d&RCD*fOaB{lK!v0{iOG0l?R9=5j7F$xQqf_-9Fd^!UD{xG)|7U5UHY -L-XGg+nnWYB}NQ!M8khG)leFcG^3MOk)@w(PWIt9+}SYs*BqB;ZNfiZ(#1icRqyT3s!*Rn~w?m -{njX;N{z^*17^uI>GZBwV>A|yrwwUmRd7hPXrP{s>j*bXEB7-_$CxeD4b#yc^w@P1Ibuxo2-4QHo^X?WA -3p()*#JUnZJydhN;=XMTyPVgILLV_NgJw+PddGsejFwzWtE(7dP?B6gnQ -hGFtZ&%r@a;Rre(9Em${NUsg3oY$&cUOM5SQb0);hxh6xN;7#F_$vlI>*YP&X@6y~z6{z -y-QQ<2sH26*{tr+~0|XQR000O8`*~JVtE}ctBpm<%ij)8V9{>OVaA|NaUv_0~WN&gWZF6UEVPk7AW?^ -h>Vqs%zE^vA6J!^B@Mv~w4D<(|kLMjwPQnD?pLT~9At4@BbEM1by`5=%SN{C2+3xJlCtNZWQJu{d$Kv -0y=&h4Vg76J71boX@k^t>94M&l%$PKV*>mdqC^CGQ{dnn`oV`GvgG -cpcynnyE=ut}P$`!FNH5!_tB&4zfHJ!JA{o-RiD#>U3`-}+>LZ#ti7nn5)|6i<@hyC2D5K>8cegaE|e -27xPz^B{ki(+mnU;igTdNqpVfU@ymXLdMZ352Bo=@Nbqi$1PqCuaxGCG$!MYdJP3P=rp4)a`?@jE#hd -Dj49HWr|ijn8phZ7fc;&$W>G6JWiw2EBF*hKc_s>eOD?ac599`9-RX4LqjNf1F!6MR^M^o}bwdjIL!L(Q_31lckYv;3j -sRirAKpK-jW?4pgO=?O@;bSrfM}1we4mlzE;bMyETZX{rm{?y^QL2#5(JEppsP!-0~W=j4>ZGaadm+r@SH;q>j#!H0`;_;YZ1dUkYt4*JLCjqgX(Fw3O -Kn#u4FI?A;MVr>YbkMbaBW^_6M-5CrU4Gn~uAee>u1TTayXoOk~>YAvzS*ul9s1wfTG#&$-0j5m9c5T -Ad6fE7LwHN%jyl6%PYB-~_Bz^c4(YuzXR&+6)JQ-rMHf=f`TnHAXY4}iriledkEfI;q(KXFKh0_IR%0 -e!z%s|8bT8`AOEYQDXR2f5*7@7=r7AgheH0kyM2&Um|I1Wh`{Ym%8?;kfr|I^`m4yLVaV=9`xA(`v>6=ueKH0))3j&50}7l12=9tzEo-ZH<7_k3^LV!Brt7hIpN5|eczL)_3HUNQ}4cFdWI6zep25J~UAQ1-G9>I{cCxhbU(TIQ(Wl -`*1K^s2ceJ!X8`33~2)fz?_CI5jc&RV?L7=>}1H* -bA;DG-U&Rq1y(j8T)q)?Yh7)|MTPir*zIbRgoC@@-4A$U>E$fBj8lcGR#gT{-(tQ2F{Nggl_L=$bcY* -wWHHbEk7y&N^l8Ab(v%H!gGo4ja~=Pf-Vn4pTMInd{nrboA47Nyy^hzaWigRf$N0voiIeaPC@W#Ai`IZTo%(1K{Qd{1W<8ip2p7Cw!zhzk5vG5W<)N<_?G-GPG3atf05fjXHO8hdC-!@K2V0FZ`SbM -nim0CfX!Fb^2BVteDri(FouZ0!;ni)RIiEw^#zJnvC~040qBcTUxZFz -v;o7$utRbLYg0d${3W3DHAY7j^DpKd~HUoeSk;GNb_wNCs~aW+fbsIf#}Go2-3W~IBdcAO$e%A=xMf? -W0Z$olG9c^A_`{VJh-I~PO~XVf)|LV%%6}X#;8jP^l;3i3 -KRfWG`f2vt0SYCFMET&`ZEsLT`LkiErUS$Z)^dMwY6hIjFA|#x^kXCiCQu}~Vg{z6(Ed=ffgx%N${gE -YctCIA27^T@~VgT(C=b%7Ys%sI3FNE2?~L~727WKsNihylba6T`YjlN>K@2B#U_YPF!RU0#Q2)@3#P_+#H+2}GbzDc8 -b!aLxcb#?ErCkMBeDKteG@v^SyE|dRJpy;m{`eG{5Kq9)V!B)>g&U+8%xpd^t8{vGN=fYpg=PhG;!Q>JZ5gFa -H?9hN2kOpsx#)ghbs)8R0n|2P+l2PZx;)9ftGnFqqMG;*#&C#W2+MLf_pn%0Jj5Q#IF*5`Zdc@lBOs^ -{@28y9dKKQ)21o?JP3`4P_W;rB_J6X7|jwkxzff`EC2^K -ZYhP)Zi-qABK3Q0Zk)+2qZ%qnHwaqx6)k)mgW4c -rFec}V^;#Y9SmQ@S2aYgjG@(*pTl0DUdBSI8pujluXqXOVXofJY(qRQ&)=ONtAe)$_!>QOwL91t0 -nIhf+(Bh{@eNCqz?tE%)8a==Bz}Y4(?>(@l(TY0{oN15Z&Q&=WXa&h+l2O;sskQLV?#MMU_}X9+kva6XHan4HubK1;0(4a^MN!-1BP -eQMHYRc4Fg-t_A63ag;gyTQUU0bjU3^zu{GZUgHd0=f>sT6m}L~V`{`k?d?^<4{pRPt;r2OP3q -lGbjQxRuu#GmWbXh7$)PrE$k}@wstRaQy6hbT&7>Os?FJU;3A%90F)*DT|B3+Y!F`XKb{qK9;f*(Bt3 -BmK$z+1v!pfs}k7FXlY>r&7;2xjRXIeq*7Z2#~$*grZVKMWw=rdj+whKLb&-*W8BPXg=nJ6hU+Zok;xAqrPmET(}r0yp(rNYhCtJItw90;R -FA{a#y!SV%>UA&=BWgezRA2tQ#1v$HkiD6r18fcTYxruLibX;yi7*4o&!R-wchE(o%oK-L4$q{u6uMb -6`YG`j=_LmVPgR_HzJ7F&|)S0#y5reklRTdXp -Z#)xk#hlm=167&DgeIY_fiPeJ*)pSU$LxLrBl^K?dko7gYpyhhE;e*AZ?oJEr0WcB;6I3lxC^28bhOC -nK;Lz$`48xgacb1qP%2->Oznz02r1znL(~%g+EY><+CEa}t;9|KFfb>2hUDK!ci8Ll3#TwFCFKMSwK- -i?s@GGc+!j)oh$i#v;V%DOK0u6HjkpDk -3;dw;SX85KlECFVbKP7-v_R?06Iy^!S;gNox2Sg~#V_ye;HxsHojSq9kh} -@*Qa!Z6sc{RyIa1wr7tNTi+F%u_w0i>UE=SNV07Rto3`hTAKa8qBNgBQkpwUrFpuZG6sx`XRl9fv5JyXtY>W(?J}{Rm56m$inaIradO?RA=g*0*AeXYUs1 -3xrC_)Fj}z>(I)Xi2N3hTSih|wu4Q)z%Cr4_DZ1Pmz(;(}himcrlWR2G&Yu8WKVHH`u@+Md#;4$hys; -a+VLo81gIT&Oeud75))&(16ovf=wfBzgxbktp0iA=JV_^4%6`Bm$vx2|g4d=Aw*>aVL>(dSUDqwRH7> -mQ#*wRTuHIf?FB2vWDBl=G_k()LfJ_XJ&4Z+TZ$J@{$+O`w-ONoZ*Z@0l*V+EGS{@cK_6y#A`f+pZ_P -{yM_j_6zU1F1(#Jg}3tr!rNI@c+ctyZ)Y9hJ@X6ig)Y3>1F{m~?LL9FdnyeTAYtc%& -$I>WFg5&|;mby{}M|hoz!)mvg=@z5D`eNnB({j~3aN*1s<%vdZ!PmsX_zo8>x7RrEzfLsXS>5MNq>z5 --F%r@MH~+KEU^nsXZ`cQN7HCH6q7-`!)FHFUOUK`m_&Iyw1dYHzb>tKdsH++weWC~od55f8D&eJR46< -7+64Z*(71rvZ-d=X8X}+?jD&C)5Q0@e-<3ezHoM0Bj|Spv9Nb^DVh}cl53}-+xEZNM=zS0)4p2Q>HQc -o6Z4KnsPZgJYwcHmvB6G7U9lSp5U_?dyWWdB(l9+c+88Pkkw9R;TCD4ZL{|x;Tj_}PLSlLFciD*tVf` -5dSbk$WkPlM04y^9pGAq%c4r?K2lbvv@Kzx$Ll!YNOls1^)NePK-3m_?j@X&S;#i@6Vga{YT@}Y>u1I -qGH=jCT)skF!-UfXLPt#*kVJVQK)L%k?J!WLnR*KZMh -iG#b<%%wHvap6ye%BmXkmVEtekppAt`k01R82Q%aDi%&zSImfTlQMR&0j!)4`ary>TfdUf6q!CI&4`? -zGHBP(AjgKwVRkCefRq~ -ZDY7r<1(jWS*GQtV8rXVDG`R~8ODlBY^`Jm0A!}j;_}-8F9QE}Sdt^!Y%KCgu-j0Esh@K6a*Xj+3H=A -BZ+-0gNVG~G|9asb^cM$W*^zE<;xW8(-1#i?l6Ck=Bc8}l)>@$)+R@2@Z_z{JBF9X=D53kNi;%_r9+a -M?058fQ4&!76yxTSK7KzflXnopYV5sL+)oGRDSQ&0r@`+yT|JGaG-OQ}%n2O<90(OyM+J9C?2WIEX9A -G1*rzlqwso-Y%EdrX#|N3poa93Z^*A`PXMg~OAq@O#X5GGb(SvQ^?hCyV0iDGg -YLO+^YG>Ev$+<~fVl#}{}OPDTjEcInq -tW(GjhdT|CN(8)yzp-r1%5rO<0O2mZ9xRq{+X+=hpxu@!|-v!w3{_M^hT6#qm(T*l?39O2QP`uxO|VQ -=pC)BM67#QIVDr(^-;!r_01=(z`K5){wD8YgE3*IVF&;s7G_$G|YKgphYaxsd8=+9yq=W(=n$ -=FsvlCKOVCbMeHm)8=aR|p0R8QnwVKWO`eu$vY3jTJLvLEFA6GLMP_(?wny_R?+2{&aWIr!Wi|7n3bnIkc -RAvuCGQpEbtE97Ofl4j`RQ~p$D>xPyi(3m)L_Gub=_6y}qL}M)`wzU_wG#C$HOm8|K^5-VN$Mm^iF{S -Xy$!b{Qx&Gvl8G5LCW;Ktr88o7ewiXKdfDobmSU#BU6EQ*@F)NW1v#2EuzIiz!jx@;Qc -s`f@D$DK%GFh-sqpq8Mt53ar9Zx9I(74_L)n2?$-+UWTjBhsmXw1DF-*_?4XWs;FoOtvm0A+~XtS4e{H*Eap{iy4rVk)i40pfEc%w8G?4Yq#mpYq)5-uLy&)~ab(RTJ_dD;ls<`|GiVeBl|K -YVLNQjJsFt{30IMsO0kG1ZvWlT$9aL2d_DwN6Yx^YMgGZtu?O>ZaBBSCEU9FJ^|0Coaf`^Ja=8b?X&+ -Y*B)`s`%5`5DT@FBANh5NZt -pT{r@DNsMd;tSSWJ-@%?dA(3;@+9zme-^LuWnh6F%j3^wbvSvs@NzSL@%=5B3a|!nX=X6I&V+TC2?2| -Xoxd+%&@YMN$2%U)XAAIW6-)VL$evXq^PF<4LiW58ndj736|xtV$b5&Os<7>rW2=n(mm8Rf`Cqijna5 -7eIv!t(h`4vlYALgj+HvhmKju2)E7qshD(bfb|3*N~$*2i~RU;U!M=+MpJ$*vmU6cwXR{4AMv8_)@Y? -0n07-Zk(TQcLon7~zdwzJW{o34-p$H;c1u-;faH0g9OXj{ewiPjt+hR+(+!4bT`4xm-!iSDQcr1Gd*< -nl5%L&;r0-e$DoYL|2wQ2Qna*BJuT4*%Og&%`o2znY{Y+y}4ESxX+=8AZ=(J%e_V-ge}7-^)&ll+AJre9pEJ%T&0S*l%Z^-vZ?yVn)ECz}Lr -^vk%BjsazRl+tIa88fA|1s}X>;73qoD*RX$$%;V-bHk#(FvlQqoUS)l&DTl;}R23+Owm!jVey9NEt0U -~2MC>!YxOu?*Isw+gzZy~$f#1FUi)HIg5?pqP415qh1-yec9uThw^b!0gEO>Vh82R>$CFcy}GV>`17I* -bckPkiB$~om$B1teF%xQ(a6;DlOM2YxNpTLS0+k>o4ha?_YK?d3~$p!Rs-k)s`EM4%-!|*FeulKu1R= -de_YN*|+hmIVela%loGFg}nPYg?#*Zg?zG1$Vd30@Z~brBfD6{YVD#^Qg|gtvcGt4P)mC`^-^oN9>&vIeOW4t18vg+6uZ~jrB2@g>D|$nEf -Zeb!|Nlok?k{Ikkyse{&|eNB$K?Rx#CY%HolRD(16xR%yId7mlB;+J>vH(mr3kDG#E!v?}X&(EjFTy9pM@78|HB*-{Gl$Ajrp&He!%f^Zn$$s&OBgRdYwpf3RJvGn(HdV{M;u5>FraUO~0vj -_nBQ=(xSXg*jG{^a0KMeYtqp(oHgPh}CNX1-Sfh`u6AG!^JuLIk-GMJ32ncZDH?!vj=e;t6G-gM|s8S -L1m-+WiIG%ZTv4#O9KQH000080Q-4XQzi+)rey&D0F?p&03rYY0B~t=FJE?LZe(wAFK}UFYhh<;Zf7r -FUtwZzb#z}}E^v8Ol1*#FFbsz8`4u9iw>JG?mkkEn+8zdF3_5lhBe;o@h>!aRr@$xu`${)umYyt0RENPX^U|XygAXFX0o=*E~OaUjv2# -?RZ!^`P#DX1bdTJx0KJ1{tCORUZ4^EKK)rTal5Bh<)(q%E?nEQOS13HR`kfq(L{WMYYBnJ=!uy1o}?O -p!B`UcV>XDH4u5ZBl~0ubG0&a8gw1VKmfs9N}m1cd_Fg=6+aTq9xVlS>h^u*=d8|m?SZwjm^G}-hhHh -gEmN_>k!^C5B6@gYKc2{F8o|p!#IdlQXosZ3X<)nSVwwoEWo{1Le_9%*EAfrMXIK6io370Vnb)=7Z9s -o`VYD~>K<&=6{KV0-*JQpzwzaExs$?K1mDf{qb#?<#J@2`Z|lv?(!XwDZgZH!pULb8P)h>@6aWAK2mt -$eR#O$NlA!be008j;001EX003}la4%nWWo~3|axZXUV{2h&X>MmPUtei%X>?y-E^v8GkIQPrFbqZa{t -7X(X(5C%`!4#JMPcYBLpP-uH&Ggc9R*n~N|o_V)Shf-=doqGDkd$0Dp`m -X;%_Z_Vp8tg4c%5lNWv$IQ&{UpQ6V`r5pc$F0%yWok4 --q(37Ja(CrAUyV%x-lA!p0Ky8YMG@h%0$W1tYUeQeUy=4dCaSMRwPWO^be<%j6;-qK9`{sX~h-=PwSW -$vRbh!123#gD>=v2QZQbGOIhLUbs<VHVCOQe?>%<6l;M)q*k7$aIX`8ZsX -q#1UY0Dtty39`*VtGTha-m16Sl0Xygc0A7O8(HZ)Gkb$^EQYY}3{DNqfYAr%`~dZq>!9;{1fz1xv+Gu -`?T;r5|}LBWpkRXCBw$q$sjttoNpm<2dd2qZE2j3$^fA$~14dONqnOY2;fg)dgsSH%G6|j)RL2Z(g3f -V=vg`{^{x7{@(H4K|^tTesp!UyMt#_>(;>@ZtXO;emZ)8v~zy-vQMV&y;1iJ-J4+d*#Rzho6D=+=M#F -|K0kW*2G@I&=K7z{4g$5)fgw31lHlm&*_)%QzbS!FPbca#cpm(G@@`)dO!hYuoSt7Ey$@a=z57X%(Ps -~2iY^j!76dYtRS>wPi07VC>mg$7dbX-Wc{65PKd_4|6`f6JQJv;IymjYEbx1+1sAO7oMD(eOm3>34<` -st|w2e=>GZoU;*vezR@85uvS>H+IsXc5K;yx64lW%-MUbZe;!DT51V -c>6eR-1bv5dNu=@p^JIYM+u$eG#aX5hQ#eLr82yb$&Q=IzDo~iTF=1^xdHo+rs}>f?K=k!0beO`G91a -ma-_a+V72cCW1U>-8k6$^hb+o9OyBUD$=2#O%QjIHX;0ni~3O%C8g}m(psch$!GF0gdqD0wmFZiV0+r -p>Es_Q@!a|TwX<1yFJ7>r-k6A?7Uzf8&XAQlI(@lIAtmgORK0f7AuG0Sm)t&DY1Iv>paC)r~;FsCV57(da>zK}=XP;6<{WYdy@+0<=()7tz){&Va9h -On`+`HWLa1hn0vrU`y!E9M4A7hL3@}1Atmqz{jB{JA2!UZO$R@?v9^F^U=BrhZa; -M$Q3;*0Bait88w`8WD27fD*s^Dty<4By=Psv=`a6wc_t5$_3E7Jk2pz0*+p}35bm)I;Ew= -!wS~ec;U}MxjV5=UK{nCyDN(-N@d8<5lb})3-|GNizn9u8O$v=t!0hS}=RPQ&FE_K3PGtj9LdZI_PgM -+yI_%wFbw6vBw)>e{{9$Kf3*q2x>CE(}WtM%f6eo-_azqewM3(K?|f1dgR?nX#Bvrgf#S@u=aw$unjEny$2fA>cLeD;0t>iq2Gaq#Bs;_}_08rJBR4S%R*>y3 -+i_!Ea`5ocB5RYoB85X=cm$T!a^|9P;&SMIcGR7e>)|aFNec2ReGGYU;0=Op*4A)KP_&O-nxM -S*bGj@1e$ONaQ$x|#6M4BGKWX3lJY*}5>->mcvf8S#Er1ymK#jmx5UOAh=G;{b0+^m9|v5??p>Hl(3I9YWkhM@I(+Gtayk-Si($v&u -5SC<7jc#F3To*8QJySI`SEID;(EK}#v)T4MoKK_4WOp{-n@^^bNw^K8Z8tpqeD&mf`gDKmeEMv69sl( -8;yTAy!K;f8f$|>|j;piF3t%7}(@+I7swPb^M@Q(6KYi+Slb?Z6UVNE-yA%#5ifYI~>Tb6MK=cVb)`5 -1L-Y@f5R<4c1judAQ84Pw*fp%6`4JK=G+Qu=MrJ^wzd&4=Dko+T;u^KtLpn`Lu0aTGW_I-btK8KLboa -5opQQrB*$U)tUXF$te7dpoEEl!rNm=Un1>nr$7JvgxkJN;uChc-JLBf4C=Kc-wEid5x1n!T1RiWL~Y_ -;7wcM*2)6Lf|U>sRxEp!qE*Z5KnxCB%(iWP;*ze~|_LwEw$E()GF -CW7zyswopF47#Mq^cHC%%GA|eR@_n+n9GHlKG&7urboRhHnO^C@G|SCccpI=9!6KZW#3k*-GN5M{Ev9r^><*(%q4gwE3}pti<*VCsLBQ=j1}jg{dx4rNkL3ReO -!8#;UY2p(7}62Im=!XBe6AA||l5E_?CHqftM|%c{wa>WuJFl0@0BFsCzoutl1oHLPf`P77Mb?@3d{RW -vDz7mJ!4U#DM4g8()!JS?H8^aEO`_D9(aAOl!Td7dn_5Kx5BFD~HaO6OH05_MHz5z#Qb&}!o8N}a?*U -6C>Q2JKFOqE%QTIMV4|<9}ohY2>6GL_ysAW!SN?iH8s7@Y@k_MF)OxSQSU*tBp&tlRi~l06 -75wh!3~aHst-pz|zptHe`>FM6mCUkOy&FpZT!Eed7_wWdc!lw}F@SMsqkhdnWwg!N)#&$_l)p*D=Do~ -k3fzvrue0E$wLUqr9z_|IhXH_U5#*ym9aOj+WSh8{LInu0+F{%HyUQA6FHy%)o4iZ!Wj6||cmjqoa5if9{=LxIw?Fm2^$E`rskx01Zb -fNmR}&2z;^y{5G7SGFOsGuhgiO!xfh)EWEiN)}_gyeAx$Nll`_o`d?;d-+5{N^v -?fBEVb9jv+KczrJ%_dP%94eZRzX`jAylMJ=@$$hS;S+)zRW@$$S7CbO*x=3H`?m0FOw#ceO_D$)qZ~E -eR7S+2ttz;tpp=IfwjfZJyYwF?uzIVgH+{9lLS)IF%lEWE~y}@k~47cIPe{Emo`*BeRA%}V4;qB&Z>4?d$1{HiGrO&v5LuXoHt{T5>Xrt7+E)q<^XwVZ0a -&9(Fvo-#f?`6)^pb28J?iQbd34Fd#NcHr@Fe(U3bQ|ATac<&P8a=~oK4_rD3awCvLK5iogV4OIqQyfV2_Fi^IkPI{?rHimnp;JZF-;^-RvP*jX?Y34%~lPradhZ -dfS3#^KA{V=)T)j67)iSGvW7v8U?>#*A}aru3e|D&5U;~$c{F@$jSx=Tl4C2IN2-F--QB5MyOwP&FaN -(=%x8~xy0Tb{wSjtJ9`Ja2iCmcrJ7yyc3=&OWl|?L@+qmCRIpNyGMY(fUFCIUTJAJUXO*O`U*HluI(DVgW{{pYWgK>mLZX --&yJm8*e8vNtyzXXsF_Le%VqKx|m{oV|%RS;&`a{W -W{lNws%jvq@dh@6aWAK2mt$eR#U3$Z`m*h000^h001 -KZ003}la4%nWWo~3|axZXUV{2h&X>MmPUu|`BY;0+6b$Bjtd7W2VbK5o+e%G%!%RDUE$Z+h~PO4e$>~ -`v`HQOf6#6`5Et^bkw$eaV= -WQuKR!u1?p{#+j-pCAW4Y{x)vmgtwTLD?CHSU*t(5+HZZQy!ux3vOkCZ$yOA_uS -Ty_pocuE6daG9WB3K1^X@J^LxV=atBBT6D)0J~S3}=4gytm`o=V_}No}txoF+&c^_1+4znnF -?887Y#$(ceAQQYt$I#@K2$0erNutyV!<=GnwZhO~@0$6+VqWoG2Bg7%Um>4Z028ND -C*s(|w8Tf@F00k3Oa@BKUIdQhtwY^`m~?D%*ijb$5Yx -9yr`b#+|sjmhi$xHvyKfAc*TZ}ab8XKydg-|+MEi!&~Y?6fG>8F%b1e|iU7G+OAMp%#T{L}nPms@)ZJ -t8%9ztM%p>Z9U%bwLE)yoNdpT@WgCutFp7v{T9p!aUZ`*+?UmR?*e*cjyQ(-qc~0>I=T2E@)Tm1iKc@ -T6I$20=MX~pzE;AmVTw4W%h}lq{xSzkagW#0?e0y17DmYo-~}wyXI{!2Dvk*@1yx%zpFPVpnD7t|(bd -%=N~Um<5YtchvVlTk3L~fyvK7y^QVE}aA?ZTsMoxGEB6V1OSL+Isw5T;;x@K^PHio; -oY@9ima?2p&U)q>!jflX?Z4v@QxGtk)13qweO{o`F+q>LpG+GmO>O!#+JHGl-)FK37*)5r%CM9(<2qp -=YLWmY|1Fnb)UBYiZet`jn_Ye*D-|e{@fvKApmOpuRX8Q-53fFrTefLy-Ji>@R;GAfhy)-)+*3AOLnX -jY%siT*4X@f9EVte8gVBJ8~W6BNh>%8{{i`-;)D_B#DLFRZ$hx9R)j`qFb9SI(|y;{*P9u;3KB|hia= -Qv%*E>|Hul0iA2RgbF~q1C*ma{w2E17s`bEkK&%rM9Z55o$Ri>%JE2l&>|8JHvtHI_{r-M)&Fp< -WS{_Q(|%^!$L~A6wNysi&DdJUW?VB{v4rpHa$xeRYZZjv3-w@&BP0iIM-5s{0)EFxS9BiHhsiHtnEw9 -@^JXROn;a7qGC*VW}L(NM(ps@JQ0Hd7jKPrL>MFP7?BwT6I;s6e@N*3H%LC0SVpL^3|#<<=&sqgPc-E -3x;Je+r~Nyib8ywzf5kK`@y>6xt^?&PB6u%dY)_e9=X|hb&g()E4I;f+r%eSrkr<2%cp)?_{U*}Vx?} -=3>$-p6CVJ~jj{XtKL6)J(2o*_x$vl)3*B&mA68c4K?`ytC;sJVC`}xwQg+{s9Mtq>A#uFuwfQTa-78 -+Hsv>~bgVCrd+P0^Fef_ -&R&ON)`xEcZLZ{>@;wSvH1+cj(t$CqsN7D@!|5%)kJj7{Q2p01pG389{Dt~lg5YPogtFqWtrn_Z7ljm -&j)+gi|flNv`SJ$d6_El#iiQ(MzUIRl3RjSYWc>*TxZ)pAG$ljc%uH7P_!&u?A^o -)M2iut8U*N?CbN2YgF46vIWa@cF!Em3r)>u^qQD)z>GWC=XW#|32im^7bMn%=yMDC5;aTpmXETKK-JJ -*F-5tZs8=ep62Ydx_0s5iMY%{$-B;GZ<2c=wS(1Lut0#zxS`3! -94=A?s1e!cErW_W-a14cbPia@{%HW(a(tobVEFvQOMwACBXPNygRLewyjTFthS%W<>Yt{lpGZ@fWAjPV3RO$f$XU^7zNz>O%|Hi!4rQXh+hS%Ha^pQuF`_i; -RvEdk3ta4-g^?H9hZC~^p=PMm#E}msT`?k;t@IfMwwfAEL?kn8YxJ88H^@5L_`_(8s3`D6B~(6-Aj~r -kb>#1HN@B0Y{KrnaWJfES-qWCmZ}V|<6w%dad4$b&5DhQM1{t@?ucD~m08mQ<1QY-O00;p4c~(<#0(1 -;%0RRA91^@sg0001RX>c!Jc4cm4Z*nhiVPk7yXK8L{FJE(Xa&=>Lb#i5ME^v9}lh12|Fcin{{uK{h-O0=n^x*?jYGDVh<%IUerNsdPZ${v!FG5(MEOC`$)FxtR;t6AIqZu -0MB&}Xy{QL1D7-g0CMYb@6aWAK2mt$eR#R>K>OXr5001W;001BW003}la -4%nWWo~3|axZXUV{2h&X>MmPZDDe2WpZ;aaCxm+ZExE+68`RA!Kxo3A*#^s-C_$PF5q^(hhX!zNg5o^ -Fa%bnV{K(oOHoO4Y3{e*3|}NtlG9#s`4CIw%y4En96mF8Nv^kfO*VPK2>h-aT96{oSXr|*Y05PdL~a? -8OzdjHH|k|x(F}f0G+9h;n5c7JPF|9qlGibzY|uckir2 -a1;sNAsxF8wdRRxobteK>FQ74m0zN@$ZmWl}qlStx -ct5y9)N1zyKw8C;-j~m1NCXT^>MKH6vH65E>WieQmwjRf=JDOMGBkY)9O -KPfyC!`ghNE+EEx|b((Pl&-(59Br9q0AHBwiX-mJlG83t8Q&(iOonp#8g63zA4PD=Zx)wV$LDA1htmu9oX=)6*r2ca(LTLvpN`t67wyx3I=Yq -&f*;m6onQXGxJ=J+1$R9~gI88wS`K{b=)Ywkq&aO!yT;A@o&B4a7m1`O^F#O|T~si(S@SbDHtl^sjKKC6|wHQQHxcjg!t+foYIUk~g|$?09sGM -O0MvDmK?w(x!4Tkfw(I3$5*DgXle#(sj>urTm*k4?n9O$oRPPzt(mMv|83>vTEMidIer9C3M2%MDew$ -}oEPR0iHV|u4xRD9UHp1?=Oz6_pB>;|GB7%XR11OB|OU08wwV`ox23P@?zo#YO^v`)^#Ho6h)m^>2X- -ajdNX<1TFs(VFdq|^1-C-r;O-;fG>i`S0UlQI(#NLUGkFedk5pSV#QA>aodcjIF8z&KT -^?y^!=UShNXu?^#}8yry-rkKmJF-AurJI6&+In}v8v;EhjDBS=>%*{l(i#{h5-qZKY3n;YMGA7kAIgT -4B&|O*l;8Y%)uGAN;_)cp;pjxmvne}V9hGpe=0Qv9Lm)8Kp?AN`$Ct&wm@EWFek@b-{kFL3#mhFfd!s -a8rYh?g1sj9km^~7{0Jzm21z09=*$~79SOYQoniKu8GhjnmzMgSH+ozx2Q*Zy6^HIs}3=7P{cz0Dax`< -(pf?q;{&3T64aMfQXt5vC}=gw_S;SZ`v}b!NyTuU?2JOf`?75k6uR$33 -0qjY<$O;mN~mmj#UhkoC4DReq6U%go+w5nLS4)=M%vpjN>{P*Q*{8NF=bpin|*re-{_`(!}Er)u-jKI -|Pu*SkYMhe@Y>N5+6Sy(cUYIuS57cEBl`qIZ$6P;?`vnM7Mr{)wHH|p2uxl7l?P#0xORrwDmncOL;Hs&F8jo(+WZ4z@+4LfsKW(~P;##Q&RwRE(*Hyx84@8#)QH)xAPXZt~bICPW9nHw~ -2rljBB0kAMaZ~KT2y%`#(H$6g!T#t-XceeC9Q$GDj)6-$`yO856?dvQ(-?$PGWCj+1~roOzy9H8!G -1-efQSD{YmoCc_QPCj&ZB++3=$Ki2VwZ7fk3$#jIbN>B})pPktxx?hve-FB=`)`|M(fESJ8P6{E?3`# -s{NcU@SB+d+R}=7G3)}4bcwnc@G<@C<}sx5lE=qYms%yP?~#vDP!B({T7*~Ls<~UpF%~!xdW*)1P2DXFRDp_h+FGTsJK-c!Jc4cm4Z*nhiVPk7yXK8L{FLGsZb!l>CZDnqBb1rasl~ ->zx+cpq==T{)~gUPYNNjm9FMyca4GLveeiz~^wRO4`95we+5qyj*(UC0041AwGJN}jfQ5dkdD?j9_57 -YAf^oeHu{Ge+>AY-mQZbis1L60*q?#)-UUL^8e-q*&@fxTXvI4UAZad}LgtMeZDsUxH^HLi0q6y`7#` -G69(~rY8seUNo9UrNUg#@uH9h8y0Z3E<`E|eh0i|!O5anturPWNtmQ*CIZJvSL=dHh*n=h@-)AyMjM` -G>1D8{Trg+JixrV~Ypj!UeUJ*-A>*}#(QuavGOGq17%CmJX`QZv_y^0Af(Pq6g=S%RgdC8(_)Z;yU$0 -nQra9~A!5{@&26td)YIHq1Q+7qS3#Mny+=N}RTl4v;;EDPAlksBAhXpTz3!Yx3fSIvd`L5t*(zqVwi9 -MW3%4Ns#OHYCo<$vLjD&vRYAJ4|4|BwT7RO=M^5h28Eek|nhirthMO+t~Ovx-&JQn0EIwQY6dl}SV -juQ^feh)|ILvpMc^b8RJ6^k^D5kFGl9F_b837C`u{0!bs!rq5yhHV4Sox6S@HtRKKLU_J2X)Ine7YAq -IBpR6|ueHxw@|#P&@o*TLl}>YI8V&os@nBr;d(~VGOC#Pir=7F&QEzr0&iZ+lH2BA^0GulClA?xr}hH2060A(`)MTVb>@eM%cDp@Z6lP80?BZ&n97G$#Kpneaz@f;B -j~Um0TRTzUSg1W5fAmpsb%kCFC3fFpUTOsp7=IHLNilADN|6;S{1?eb`6!mGmq$<$iVK(2iTisel2_$ -7nJIJu$-W#fxXPrFHlRF;^2Yf~$id4Iuw^j3oC{4f^j#<4M#Dr;%R+4Wjp9?@JBTx?}$tuEraW6xl5^ -zmlf9T6!3czF0@9FOiM3mrP5*?~rdrnosprIz}8L=O^DNGQB*0M>8Z{uorWB6RUIr0^@4)^epN@b4|W -iUZO3$whhOi=Y6hg8C_i_l!y*N9+zDrgW3q>PUH!TvC@)OO7$A}@S@I#uJ8OW)$` -{U&}JptJ<61MXe-i()z)TGxF_cDxKe`XK@A|9g?bLf3})2|rV(fkXJchtoqoziOzlV=*_ -C6rD-Ut$St!^lff#P}h5poX#Mp@g(}=yg!MC$Q6*)S!L()s(b6TaJPZ8odjqT&MKCG*j0G6t -*&TBtR6t*hZ5}yXEY&#F43E8xL(LpnWCKiO?7D}q98X|EJ!K|qgRY)cbH@hXsHkzMh>1odtn=8nig}j -Jok2fp<=cytX4&>tEdhQLhb5_{;2LUuyU26g=th*;}dL^$}T~mQX-VZW^t{07L}$ks;j(41G}cNGf6Q -Z6PgOPuh^3!ht^A)WOq-|zO7iJv|BxjRlL>50h+MWahLRHt~irSpbxla=npw)T;anokA5&mAD9F8CRL -M7?@vlF^Xo6w5nLBH>I}?D$ZIN3$f+LY3@BhhE2CFH6;gMku_7TxPuf7!jj%QYG~KXFw|HvuhiQOM$R -~gkdRc4?Oxbpw>~k;&H!4z0TdeeL`PIV{!%M=JB#u>J#jz(?w(QvM#)iwT;?yTkUz1UhGZP)!Vt%(Bs -2{efs`E&|?E?GtZl)X7R`aR|U@Z+QF!nWp4Jq|o9-0_ssxc%W&b{B`+MEShu|OU(|3~EI#S{MEcF8>p -R^sa6uz~A4^-ysYC?YjE@EX{9TuY=&J%=u%>4K&ti7}5|_G3bc>JP8n=Qs8NZ@V=Rcq8FZkB^C~9y@M -#hXBa8(Korl-{Sb>$?<+O|CMpKoYN^*=K;5AFw0;ng`YqttUYc^*R^!uug?w+KA*pJX!F(}Ow+jZ -(uhnBWSh=7uohn8FVVP|P>XD@SbWn* -b(X=QSAE^vA6J@0edHnQLSS1|I;L~<0qB=5dFzsR{X&E0q=mt>k;GjAN9kEURmHAU)3%8DANfBV}X0J -s1_SxVDudRNUfwMbyGSS)rIyNlI+^!75Vql+w85&VC;O!6qtQdQJy8ZC>ds-osnMUATFbyQx^m#2#)h -5xDW=ryn>XK@HgWVuDSG(g_}TN9Z=UM`3wlm4e&0? -DoV@wB3F%yrm9J@ypH$wviYK{nkYF>&7T*`B5letug!xDFm@hP%KWuTsv;|9_1;BQ&ZB0%fG?4LH{6T -h^DN2pHvHEy0BxVsH$arXy~t|a>eZqFY9u*-d!01UuHY|kly38OxiDbft{3Y2L__&o0bR -#?qdi70UN$gegIt;vWs#*xp8crKt~JrQo&IK8zEHCyUF&tR3p9=vNmZ-8z1R4^seT$wc+?Zy%IST&_q -SrPL;G=5H`VE${-!4fee_?$f%|4Kj?PQqvxzs#XwNLds|Ax8V3_P3z)9M6>liLP<&NE~dK%dW_%e`-|5Q3QXeDA#*&A2Z7y5_M@JFUvxO(Fd*Y%j3R*htd+Zc~%(2L3Pd#QId@1tPZ`E -0C9{nx`oTWRcJY61xzAi5i$dDQqUM^Z|I?6GOG;cQto%*)EWNbAaP@B?nZqF&4(kz7YpJv2VwzoNXz; -bl%28dQq^RdvsWmW=25q4eVhvQeK$?%AxJ{*I>cgS2c239!5JD;|vc`%j#FZEf`VR(MbD6t==1G%Y!f -R4PJUr~lq)IP`2!CW70^i|=qV{cf{8UL+tFE1JF*x?Efy3zTNBr{X?qIa%uvdJ>vmt}dq0G=3i-wP9R -k>UY2Zo@7Q4Ec<#c(w4Z>&wbP+Nh^e)I+as;=C2w58VXNt&ufBkAtVNxXrX$aZ2)QD5{LDA1=vg9~@9 -Cm(4r@#IeRY2fsLwd*1TGs3CaO=&(Vht(82Pf({K1rZG%fBZE32%~)bG&&l@U`_y~hdja=ZHu!r_^U| -fpi^lfd3J>MNX~teZDt1h2*cPx4q9dzS#9F!zcXv#Q@a+t$6TGETG%HKK$wNj2}9B5;i)s1jW(Gc0o! -O0E|LzU9Ska1*{lYg(zjoe;&Rce(a7bC55(>MKJUWSbA4Gh$9UwNgD -Iq@ww21YBnjZ{AmU9S3^lxjE^VX9gy-tNkTl=H{QI5eA(|7V?^sHE^qr~3>OVu~1t~8guzyHx(J-B0> -A1;Zr!kNdz=OuDTsg_ZTnx_8^#SVqj0I(@hfm7I=_e`Zyoq(j786QKRi&<209%#69Y+2s9OvkqwHb4$tjoj59LA46-Kvb(8UZXXXIaW8N -f^#LJ@I%fH;F5w93sS7E!dxqVEVXiSuUhW{+#5sk~DgnHgIx+W7@Q&V=as7tOj&o?WU4~glp%(D`3fU -Tx+ZQ+_{}X;T7&^t=N3KT~`b}ifL7Ah4WW)+O`9)GnG$aKTq^Bndjub^@QEYY1{8=g$LMyXfQZ*`MM( -z%)x^X8*i(n`s8p02!`dC;K(UJhruyVlAgq3f>p -Z-S`ULKu}HX{waPXq1l;8n`^OdRF0gTQaB#I6b*;Mn=os|!vg+HS93+CxzEav_2zre~Buc;lII#>UD- -}wLyFk>M4!s%6#2n~dAt6%S+5i=H)hqyv){e+ZVkhiy5+raZ-6KJUot{K|!9IXtM_>5u1GMCY_Bc6C7 -7JBOyCzdR%jX%H>@0nlbKNBXd|Kg-+6E-NDguz$UEm9Sj-Fj&uUS@LM6F9aq;z;j?ViO=)ZfH4{urN^ -<%<1ezysIH?6fIwH3k2yY30YR80N-$T&Y-L*YUm_n~ex$A$kzam{I9FHkBkG?wdx-g}Y}-jjb|Z&lzy -^rAnp*#qQt@x~j2+A?KjK>@F?6?SD(_072b0m(3@9P7nkH+T6LH6YDNqsZkFm)S7QO3MbpNxk6oF&7tNbqRb}FOn368*KD>c0VoivY?mqq`E@)8=F1jUU*5A=4DMg4a`-rg -J|k{Qnf)_71)a0yS0II7@L0o{JU>ny?Gwp;FsgSynOZD^Jh=rJRkXPWz@p$ppyw`;Q>XE1}&uqcjz(fkh9LK$Mh; -!b$y0LWVgW)HJDWj6+};)#|{jqx8GU6{PfE@xPe(#GxxUyzrRE@Er^b3I)Ccsc|4?GYnGhPdjQy>K3PZhP^g5pF&-6$*)7Z)*;q>TrLp -ZI-w`KI}W}k$f20Lqrv-B($Rf(&HA@svenuKAJ{zJ9sj=sR4;pRJG4H|Z*e0fyv?1+lDGrv4Sk9S0TKgb5Eb)}ks)cRI8xGlVg7lCzT6!iM*c6u(>O#HG2;!l$s&qjp -r6Cr4OZNuS{(1G(U9;m07z0$0s*0i7lCD~wLJE=NN7HG?(dB-ORP;~)d(~_EhNYcibjBFF?JSd|w*5w -jF)R%PN$K}cM?jdNOQ!GiV;!1*MWtD~r96F2495cYe!zT9N5;~2+Aq33J1ZJ8>bCsZC<-=H})4A;QDW -fy1Q#gqyGjZr?ft`ct3wB|?#3Kh14^;V@@=aW7b!Rnv@_2#^hEQH%z5)}7Q?vq0(E%XO0p@c8HYM2ng -xWi8pZmu6IBa$ISi%g8ZQm8A&ro&+^dg%Gf#3?mLK~Fbxw=Q|n=LtUSiqJf&C%f?QCz8~PR*cIn6k<}El)w6?Ag{<^o-ieEn -o40?F-;Qs%d-n%n;A_rBAQl!F|4>yHA?_>URbU>}OO&WskKZ>-6skaY}bN}ppeA>m9U?w-`H|b54-X! -_r627P=xdDLq-*u)|UH8mw5NRr1YK?-_3eTskn~f8Kj`uaiZjj`R%@F(D0NOXzNIzm+x^nFgJNdT)~O|sl`W)}_L8NXn3kJLYU`e1>QD6m`pGvto{`4`E -d0Lmd$N~JZLd!F}bNcLtX&D>)JEgEAbYGijYG2UA5wTqpqJS -O7(MyA+_Dx@89|F*5TT*Jckx6xMW8Y!3UT$wP=4hwT=zvtpAq3vzJ&e))I93c9Kr1kVablzi>SaahV7 -)cCX<6Jg7ACBnihnm+eit5JPI&yDB(txY;$f1UsqDv!+U_wd;caQ=gBh>&|M45prg2; -g+#D?MKgA)5L}Zb(|2-K-qPYgY|33)3Y8gl^c8D449~=a_=hs{VkUW3H$!`cG;TnOr2oAlaKK|MEPLQlgsMl{o -sBa4ZBW0V!u%P3AT6W;B@vZgVD}=uh;v(+qQeB`OVl8oD@bGbHy~bq&4=6bfjXJ7-(GMF>;86^!%p#( -J`|arI3^tJ}_x>XdYuLD|pm#;y1=qD_105EYI^S9eFssL1yCnoS2-9@#%B4_8Og{07x~8BHnCBR&;ke -q3gc@PNd8+yeayRF?8IlFx;2;2Q+;$7kAFW^C2eJsp%os2flxFN*=-QC|1bV2CF2?k#ulw?VYEjlyR< -ZF>Mc?ON7suwg+zm5!|KqY0*@2l}Dq_#4*276}?kuQ=J$fnFEWdVuo&y+VX^tXlW8+C`Z6cKUCs&m`T -J!re=NMfeNmKmx@Sa;3=r7M~V)4P7V?OVaq$CQ=c5KQTZ%M$B=)a&ZeqnSI>_MAldoRS>+t`2>TcRBG71xZVz!ZV|HIO=-XU<}i98{Si9c!WJloWnR8|8!b?5HBFmj;`@j9WzrY -$kK^J1L-8Coj!BbR78$-EcZtbe!8n;`l?He-_9?xAX)yU9u&;KY^^VyHeRQWj|7pZO`nkm4m}n6oHJ; -5k9tk}e?OgjX5&W?YNMa7Nu_sq$bwx@~o?{xm{NJICHp1lO5jE$m@$6EiR|vUQYLg@en*hncH!wODh7 -Cf-s3ItT5Cu=@rUn^Q6ttE32he;ar_?&irzPqy^#M#oU>G#&@m4#Kp_3KsC9ojRhlF+Kn#Nn3-L0LDI -9W27wMj9Cf%W7aGl|NZI9OAVytW3EwUq5))L3A~D{C)VcdSbh*EL}dvlevXE{k>(wm1z{Jd2~p{~bTn -N!tRRz2P3g>v*cJAMoK=XLp-d59rmXLtZ);b@STgj&t6+#0MauJ$eoe%?2Ey?v%{xzU{pi+F8(JlgK? -Q8?vBFT28!oLdg)?h0??3XgUgDU9Pu{4`BWWEw{T%t{o|k<+#;ISS%GTTE$6{8l8@(N` -d;%DUN7R0$ZNUc1OD0g9#8Q7g3^S@j^sp~AxI5x?6Yt>E=0an-*5_2kv_LPw=sg6p85Ye9L*WtP0o;< -|($bAcFg9jo-R(9uD;PsSV{bCS5|Vp}_NMnhb0`!|vAL2I%0Mc@s1F;p^K0N-{sWjJC|a-% -v=p=1*l>P+QH`GPS?{rYJyz?*1M=aXvdm!2TC=zE}L^T52dSs1@?L|1=hr*AUE0UwjwV<`Pp+5=iapqt=ZdQZF9ftlx|tHgpv}~V%ojYYi~Q)%bAW^%PvGhXAzy -CpMFfywlM|e#uVrqbCB%R_ZnfaM*A&TVW2M5Is$lJcxUO3Hn~mF`R4o7jQZ!p931g3(D{vtteu;G5@8 -q`@g>?B?YzkWkDrzr`Y-Y;cb$e+t=w>sUZAY$TIpH@WVHvSrHc-x$---$X}NVOHrd32Ra=1|9ty$tFR -JgbcgAkto(S|%htY?_Cx<38V5Fb@VPKQ-T}QXQrDG%(RzW>!YZO(gUgphV`u}Zm^hMitB)x$A+K|Nw3l6o%G9<`_>n{31vrKUtH`=?$~1jDP -ZA@CPk*s;A{zaRZYab(Ajxs=Yu@Y;iE$lV)&&6D~0G>JYOMjv4NQ6l1SG$sfmy_^E8&jm*yMka0}CsS -`d19)UiSCLB4RJV6$pIH1)L(uhPv?>M+kq!RKs`B#{KWIa5CJLIx@7%#qqGNufWggpa^e@9(pau5o -KSVZ+CbT8?Yj4IgGwg*jCP`*Q(Ey|(ss4;B7VS6pgLipTbXacH#Ctt#rUIQxOLb8BX!pZ6(!d+D5k9d -<@vCpGUu64@1t(Xxzn}Ll_lcbLVXjy6ey_b|qBZS;_Hb_DL;zkPZ>~_t>Jo1p73AW0;wJ>8N4FEdlI7x@&T|aGF -ks=H(?WnoC6V0m>Z7x!mIh(ic>203wuWlweBISsWw;Cv7p81xf9X^kt?n?M5Tksc-m)yQu9eEZ-rI_! --mU0oX;QAQB*chQ_w=r=a;RGhaJcX6&)UF{O_nb6q|(JTBMj#`Q58H<;r&6pCgDY>q>`Yk8CSKgk>LC -!KwI-KP=)oEDu(+mkeJM5nNeijlzwz2%K19V%kqjFNMA@tB+xIFKVbR72DUrJ`$a_aAt&cNj8-;CelCCP0|iZqzjxyD4}a)akd -7uAMsl7;m*t9B1dv#n>b(HMv^j@1*Z6Ad -dh7i|*c!Jc4cm4Z*nhiVPk7yXK8L{FLYsNb1ras-8*Y<+enh%^(z`Q7NG)(v8 -`+-XGFPm5@*LiCN{>-&h8loLyIlDGZe{TleV>i|NB){KiExDe(cWN;m$}TKB}v$>s{5YEp~b(BwLD%G -x$!MBx9La@LckgHF?S_R$p;e^J*nov80>*GFiaaCj~ZPf8&)DMLybMyV3T9C3#9qKNS8jZxNEUKDi#pR{QFU@V4B{d9KnR~g -G&gG)Wm*R4?tcn#=Pcv~5tzac0=ha$2V+Vgf1^Nz;KF23tK7Ks>`@xACQm;#x%0Pu927e#KqMp#z$Ex -@z&ri6%Jp^_o7a4c%U_t8claLzfsH}l&Nv7^j_-_r*7kbQTQ<~vla)45oiv-7xM~bA#P!q3`e4UBBxt -+juBQv@ljgUVezqw&lbjj;a^el|yJX!HL23*JM`l`sa3<1~>awhWneDqVYXkQUk_>J9>A}y(K -OewneDdSb&!_R{{htm__J}oSvaTi!KF(q2cUnMD(qzFNUIi1zw%A`euwN`9^?uO;EXrVF1_3Y*u}?%* -`> -He26ndi6?Gi(fpsLAESPLclsiu&OCw#xvCN8E#xX4LEQmowyaJ-(6IdO@$%UYAV<&Wja|Y*Xe9D)AL* -$~4<52Q!Nn$oQ_87?%u;?p;hi=Dg`j&kza<2G>e~2M+sQ|K^yC^GeKoiFuc)$;E@VVi=nH7M{We;)-o -EnFv;NpNAAS-^2HZFmaz;vGR6$ldDPXg**pMki~*^S6Db^&j%ldMUAyNrW#V6|9r2XTFs)G$i&8muwP -Bonn{U!zpWZv`wa9Diji4oa9#KotaxUnOhAiok&K3S25^0J>5aDa;{Jb#f*bNuHypNm!X&a-fngBmKuWiFNiW(RLsa-KFN&FP ->_iyKhriYF`5z@(M&q$AGpSn1$jfTEID_1bErGoCP#rU4zTqQSeS%KQ4Z2_ -sDZ7)$`R34@{fF=%X9a%_zMW-Y7$iGXbt7vK1zCR7(ik+rzQlI0K{1?aBwCLxWOppP22V9vCki~0~lC -h9m(M_sByZ%pXQZLX+Mw_{SHD{wT^g{T&dsM-F>{;^U-UM&G%B0&!#4<9EN6Vt3$>#FxSM9(8xq?{;nW+A!iSaA&|6^UxH*8$xl%{=(=Y7U|v -FAVOqGO+)8OvGmKiNEJfh!iaP?sU(lVd4%x4_1!jwA-Ca2zhfLC$#;M$TtgH~De{Fpx{TP@ay^^o;QF -VKhI}0(M+s^(D*x_O^;8Eb%BxMAk&=_Z6jzJg$XQq!4+ -S0Odh*?&Yq-hAu4LvVQ{?%d}s}&d(HDW>WOQRGqq=vCdk>l&63Nbs>Vg4qm@;!1&z?qMbfN;_mN=sEP -NMIQIwc4ooGH3mi`qebwzqpe){Y$16m!${3g=nCWwVek;Y+g)Q@7YabGxU02u0`PBH_a{nZxxMdXmNK}lb5nLn$^#TxWrvxTi -Kh!t8r&{q?2lrln%o@#9Twg#_(Y*<0Ym_alUDj-~NP@95E*+|1))paTNUc9)3r8XB4V0r;1T=Jq6U&VT&Dr5<3Mq)oruF#SWM{Lx{f9EI0=Aynz7?ujc -9^2FvuOPdPC~_+JUF39BKDN`peFB*DSC@8+0jTmH=mq%x-S?CH}zefz)w>#S(;m^Y{5ciBdHEP{F6Iy3m -&@FI>D8`FMWKe8DRii5BtVmK8D0d$8bvkko{KDH|iNtCL@^JR${mGP8Y6YNE7S!Q(IE8!g<~&csWw0o -}YmwY(>!kOP4?$7UvTS=|FWn*EX>E7k&5U>7kpXu$<@5NX2hFdI-=x8F`kYvz=jLKU%$(y#8wM2_UNl -5{wZ}FN8?%Q{J6%6onZDMU=fbfGgu5i~1(7ccOc6%i>I2;vn0jl>y$r*}N?C?3X2nuvDZS_bFE*rQz9-c=PB#H_M|C|v2QwN(q7x;3mw_ -#Wcx^HfEkNY6BSC%($?ihbphKXkDR##|Ez!Dg6dIp_h;iN@?;LyO0lF&5FS{kj+0Hrm#GMKLo9;Zi@a|_FeXF%GeJ~Q6KNFEVmfap$6`CVg49Wj^h;AeTcw9=1i4>cDR--h-1PaM!`uaYJ1d>le&qdWr@^LYqK%n+!I{>y4x -Ti#dZ{;?r`5HxBebx?qPZ!=kQ+33Ew`K@MagYO3WiP&w|^gx|q{<}$wxBJ_~<2>nePdgSc1Xuvk?xv= -_ah>V?g${ydEf(Um{!ZT;yNeB12t8fEx&%`rN!~bOmJ{-zma?O3kxsG#pvwj_-{zs#MwsEKcT`3r^cw -akdzzcLVyqg~au_>1eSSof9iYm1#zQ3&Xp#co%pyD>0z-N>qPAPLVO4CW!>;ZyYNdjKNO0GrG$aZ?#e -uae1%Q$=btrDRPNswY{P_Fxqe1p&K^n2VOh-iqG775+XoiW?33g1hZjcsmB>&3I_P(XY7nnI629fQVp -vw%8dwE(~QZx(!y^zl#d+)r@LQXgp`wL$Nzc5!J_-mFjUsV1hdSXlLhF=|~8yYO=Td_z&@J9o2Lg2{0 -dMW_*99e}Us-F&C*;D&L(e_`^k`boEo3fj8K_uP}kJttV(t3r*OfY+}2(p=P}vjrF5}JvX$P2iO-3ZePEOU%d>x8rSJ@?rcxk%i*nXU -6?wvMbz|5!?OUTnxR~lNe$T5#cVe?*To3+6+JqL4SygNFuA(8hXxRi$BHd>di3FFkA3F2`&qCBdbQ-D -Xlgdwoq|rV46ZBrYctE3ERd)}GZIZvQc*s(TDIfz?dxdCLL@KTFtBbr&+ -5N-|Z#U--z?J=IiG*e>*;QF2&1aO_+aV3!F1x6eO8iNpYTyM>j3TlgrXFQRV@M(yPW>s>kD;zWdrMrQ -hD{od8P{=41_Pg0wS0~v`vXL-`?;C8e*z0vP+$N3!eCt%)PsThFI57@O(jETTU?PK^H?g{sSt5iXz)um32k2)o -O`S6DrkgWs31PFK!e9ks@-|lqr_N9j!s^o3#duF=W!iTH_&trT_m`n$Aj<{%rp-I%Bnj0f-9l==-__B -Ta_8U(^P-jS8(y-j4D<<{b8*1^N?k&+7aw?PBfh;IAiSf&0t=y8*R>&`iSkEo9ctR1VCK#SNZ)R?QbT -m(fhq2?p(TUnSREAE{G={b-D4BB-^KC}P!#Wkjs`197u9C*w7GbL_~$O2xNRUDGTr^(N2^X8uMOH@Mu -LR}(fHuo?V&qPNAr-^q9k(N8uY{QDh^2>+BJ`81=i**t)ZtTSNeo&on5exWKG9QT3h+!J=- -G%@+4tANf5R%LBAbxii}4$B?d|e!i7Wn2PnPWC3~EH+;(w#Ee)|W_&_*{%0c&FCe<+F74|O7ODaLTXc -9Ephwwnfc>P1BFvSy-$kIELRmYHf@AS4t+h1ozehW?iVypER-XQZcuFjw?wEn4=zq{8;ZoErxv6@jAN0z-w2 -fgNDEW>tUJ%xm45MXFQqibJ&DrsHSTsNVs3dx-8py*Y>NTzhI)G0~oC)vk38aK -P3Sh!P{pgjqoPy;2(>^2MIp8XJ4ES(Jz{S!uM8ySOgvb`PJ?K2{^sOVI^EQ=hQMFr%mP@&^*HWakRtm -Ndj;K|*s3P~*%%x7!;v(_Z9JHKyy)ULGM0nl)CR>LuJ$H{s}89Tbc!Jc4cm4Z*nhiVPk7yXK8L{FLiWjY;!Jfd97F5Zrer> -edkw9=>Q^Qif~*91q8T2E)HP8aD&=O9u!re$&s`P#btMwmM`$XcV@Y~DYEOJ6%tEY&h^afnb96xu7rk -#$Pws0tCAdYkz%1SgR00-fmtCKRBH`#VZXd7la$`hYOx4DqtZea-X0vr2N5JiX2srKyl-S;xInarXQ% -H@&M!`!!@`~^DNSvgB2ZXLEsRvRWN#rAq;g&6Xb>_qiO6;Ad175kWk}RpD7gl6TZ&={?(~C_cUWAYiQ -pVJm$xNFcv+E&2tE}QW&~-XjaRG2CRyr6^DeWrdi3AOs7N2|g!zg(pf$6;3>$ -m+$-b{S8H3E5L>|Earl2x7&q6;p#gpVg5;pp(UxV5(x!9^yt`KbsrYy3n6+!)k -n-;*WRmStdBi46N8&C{fig-9)6bA?O1AuJWf+%x1UQ2um0k7xUSNX_JOno!3hTyeResMc3xcwJFw+9X -upjW8=nAQMYrVsCE6xEpp`558vmVGzFzeXdc43m8OX!Qnp>TSgfF4#WD^XzkT^kp7wEI~HIYkE4Bk@N -1p!H3Lk}cO)F^>MHQZ)G6#JjB~j`6@*qzp{Z11`#l&sj%t%Tqjnu1xc75eMVOZ`a4br>?E -!HAWY%QH?JfD-wd>#?{23FJovOu8t4RJvx7F2mGQnaRQ9k>TJ$t?($F0veT%@>$sqDd2%Yb;DWh(RnU -0#Q&k$yAd=eU0j1`DN5?_k~~f)@Ow9=V9_;D}^^HmfQD#Xk-u0aG9jH;FLAvS`fEgArvkA0~J>xr(hp -puCcS*2G)pp?$l^HoX$MgK_{h5R}BEQQjB~CzZ}ECz@E`@QkGa`!NR}qi$46qZCu;?bBM7!J%2l?i%f -8elB%JIk@X~Y1(7c%dfz@ihS%Qo_ODpxX+~%v6ewslWb(4NdUuUyPlM<)U -7>+4u&14ez@n)6#Jrbbar3T+QQU`Z*;h*lFq5<9*+HM$BJ3;80kJ!m5oWOk}_29S098#_Rar<#P86VB -;!z|G&)V)VEZK}dB!-q|NPKMK@t__YNc>TYowNa9;w404`Q8&QTIW>gTMy<WyBa9@4O3%{HMscmxzC7n4#)(t&N?yaig@R*>DSHIES}F^zH -sBxN<}R5>HPHFAMZ~>z5>Jce*(`A!hSddpt_VP+uh574WgE7?r1lMYJgtNwu^s!46nNw>osM7>EUeas -YQWzh^?K*!PA-OW*)ig>;3@E&$QdH<@)Ov;ng{R-~x_107w47N;pi)VoJ+R#|Z&#*+DPpc&TJn2Cu>} -f*+3$d8)9r;m~btyEV%GMmX+%8Mva1o2oeKv&D>{T{*0k$Z8LX(5}KA5e_);-x;vuxm_GbOKiNn>(d- -d(cXpY0pGxwKK0~h4u(g}-ih-0Hm&e%2d;x&8UBQLgqpT~HSA>EI)xEm_7VFg`t`TWJw;u6@C6}JyjK -2Q3B?4H)FgCt&=qsD5_ALHK9|aZeu@_EE=JCbmaw6PiHVQtidl+&G8ros51EhlQ7qe5X)bPyYOMG~7$ -$%|@w0DId={XOVYtOk|B;^h89Z?rLhm0?O9KQH000080Q-4XQwuT+rSA^_07*Fj03ZMW0B~t=FJE?LZ -e(wAFK}UFYhh<;Zf7rcWpZqs$7&8N|Y* -J6e6AR!;4IY`14wW4a9qu7g3t@cEr{g(apg?5hZjmT4MSFLGtSUCTU)l6lda<>^9{s|*jqjJ -=d`EGIG6ats0zvk$;}Q4YkLjOfdFAkLNzO$e%~60hui|_=hxB&TJI&Q;T9mKU6+JgmH9;esb4X)=9WFK`@LRos%C<1rhlbr4QXS2v3qEEZ -Cj81IvQ#&JdgjL5*7ATbL@U13_T)8K7{;Tu3-0xP!XVVA;mC^Wl;=&H57jp>Et&omdT}c0EY()23MD< -WJ<&_O*HG6J1Ur#6m0L}^ypN4_uXDK%yYy{+rA5&HI?~lSShIC-f&^%<@30@2~|d(tnOG^sLnFh1bRJ -QW&^wbUdBsxl;>&gyOMr^g=x@gCD#(sDMch2h-HCyqPR$xF-d$zu_?7nUoX#2I{pc|;xIhERIA}OP3< -ulY0h|jA&ZOgxL>Gv#!3U@X)T*z7$P9jzDC~>r>v&pQmKr$ZVJtgLRliD{u&igQj{_Y)vp3kG`uu8CK -eXa4M#j}iG_eVDGtL1Y?;IgYg?sDgit7+gn(8g26>f2YI3Tz2GYAwaw=xagiTDqE+}-amlv02aRZT(( -hgyCHGz4&W-{CLqM?WhBK3L&ea0L(lJE2L)>klhuLmlhL+;EAxBEA59VXkSNhi~-@fbaJ(T|D0 -NnoT!^pEaYzpsa5{rAiOeEG1V`cz4f?XR|W{79#HVu3MkIH>$oV3?nIt>&)lti{E6T{KlTqjP3y-jPc -u|VtBP{AoFFtkn5j0Q2?S$dz5+SU`L+k&-Th6-W}nvRu5wmZlcF5`rBe(qmY|eqd06!|mS -ba-`uCp?9{wd>PDrjGB9z>{T9g5MjF(p-4byC8V;BRsj(3=PqNAC4MOrMBbaV~d1j_AvW;8yJV -s0aXs`uV3fXGeaUB1|%tAv@jiLS`WPN#~$clprpJ#!t}mRiv(+cf0h23{J+o_EjD_5LDsd`A0l|Hs#E -i>zRrHJe5ViSo>hxyrAmN)G*XRAvp48+e8u+NKv7*wE)9t2Q93|vV*xQ`vf{*gqp850gcI6E|7{;jGM7-;IaIc@w -r4&aymj)WU&@RcF8!JZvmG5$vVqXODTbx4#txD`>^^y0G7EBZ$k;C+`6<~C7xJ`a%pV9_-O(MR0l_Sw -oKP^3!X8DiLQ<1&bG@9EU>gS*s(*yDMiZCfmu;C`J?B6D=j-+2d;j_AGiLqpd~f%M(G&lX|GaCfs*PF -u1x?}fnb96Y05m@YhSoQJ7X_CvV;!y?nu0Dp9_FyR(K0pauC)<9j_=Tl+01Qwd#G75Hd=mk0B#W(dZJ->}l4T`NyBk*UyhVzs~$wndZu -b=bVl%Q)DWdRqK20_E@B5i^3i`xLpPIiw9VFpV})ZnHLOga&(5_w!zeLIxcHG2L)wB9Cdk}&iQG+U0rCC3h?J&Sf}5C<07G4j-NZM -VS=O!hu2WJl1No9(@|LblY3C_Qx3)CWm*Y)9RGRtd*`oB%q+c<={ZJ4FBq1koMhSkPa)!i*KodI&1UE7)?Cy{}9GRwMFcyu84T*u>Ne>3mX -8u;@g*TcNsklr{HV5Y%uX_WM--+7~r`-Y60>6-oW9lEC-sgC6uyGL~lIZd~X)1G+Sydcw<6WhLdQc6_ -rH|xw}=uZ{iXIEyg6spEFBYCw#yPb~0{=H`d@F$DanCo>t2Df&}&ZZ;vedl}fX^rGeXY|)f-PQ#(^sr -XumS#E -#RFt)?N`+Z)uZ*XPuTnsp55IDhYE?nCAUV&}AEFn7+{XaZOBt{a1Ot8g89=MmF(&*l=?)>^wy_xwIb6 -5xUXY<~0vdGWq~zBl@U^NIh5fbt;E^pIA6m$Rm{t}9S|l@o!^VDvRk2)b6Xi7ub}Z9j&p?*#OZG$dA9 -#i|XW^Xz?76KtF+N**-T{^qHk-Z@p6rw^Fw>E@}vt){A(emeQ)KGMqB0 -Jg5|@`o%GJ&<%-Dh5nu=F!{Q{-#M`CbgmQ$EkVw4N+42zIBga=quxEc`j^cUMf?rji6kuH?DPV;GDr4 -a)wp+cs}R7@RNE|>X^fB}@rv__53#8a?AycFUZi6;3k6l+_&_I^x(8{2C9YOIAFjP?*S{w`Yyx@N#!%MBt1Qw=n;2|C6PmK>Uhge{i6_wvDi)x* -8PEHC(0l+lFs9&b-z!s1&;D(kP2Bv*lb-(&aM?lNx!@qtk7n7+JH$x=DaQJ(JHBOZin-i4kQZ7YVrO) -R)*l~F2=}2YxlJPvvv0V!SpmT2cG0GD6?2#&-rMYEH#Au(UsdkZ*72LtvNm4d!vii#5j_DA7Ti9oKBcMjm@S+T)5lhMYsP=3 -SPa~*s;+ieCszN&7VyeMos6hU?ZV+y_<>k&Bb%cGtrH`39Q7o*S~3PLe9$SVzd?n8yuK2Hw0pLJua(Y -&0)zcRt-eeQs`#5wmS}IMrXR1ie`>T>oc;ayIX#p+!DzHro4sUX{{mG#Wo9c6W3@d@6 -aWAK2mt$eR#SqWLh5$_004pj0015U003}la4%nWWo~3|axZXYa5XVEFJE72ZfSI1UoLQYHO##V!Y~wu -;r(32$8ji#gv1{N!BOXIjv;DoLv2n>QpLMh2bXtyUS>XJpHiC(s^C0@`xW+Gdx=Q?GWf17m`aEp?7?D -Vw&+EMLd;C$-17^AMCY(aqm@}I-4NZfj8L!tIE+bh#T=L+%ERw)Tx+xVbwwXgO9KQH000080Q-4XQ~N -##QYZxg0D%nv02=@R0B~t=FJE?LZe(wAFK}gWH8D3YVs&Y3WG--dtyfKN<2Dez>sJsi7LpdWf*y(jMt -#_BlWo!MB8Q@vAP{J2Y;z-#T9UGB1o`hf9FqEQHrXDUizRWs_~y+cvsf&C?L|{F&N{6=LuFZSgxs^+> -s$8Ik3X}6QV*s`7K=q9+D>W9xZZbM8;I{h#ivH?_vjm6m5ER0&|A?Y$xf@56(EmW${ALvfNOaaTFDlq -4Q40JyAHI8X1CB`LBC@??|IXJ4raB`R;gw%v(R?uU4l(z8Elsb1`5k);cHoBD127 -F=dSQeGwiD3Qi1XBwRyEXE%VQc|OuA$##aeB7^#HO?pU7D3m{gW!gVQe?X28apLQ(Fo1!MRgFzVtjVW -`=IlP_b<9$UZiv?z8;x7=ofJB9G8o9MTiWLgfE`S7H=OI$RlasIqWP71HkYf_G*4s2^*wWny@~~VY -K{)0b=#}mA&|TYWd0}`Ldh0ir-UoaB17pd!bpwH8p%@Yw7iZ`N4v{yPbtHnDLgS&GBT=v -$oIU4-0m%zH-*{3hY|f60u1`B0GIl~-95Hup)R@w-0`Kgrh&DpjqQZHq=Pk4PJOUeBHZO684B_ -7jaL?H{#{5q>(^LM9kdIH3stVj0V$b_#LjH@<8@Zm2I22@4wqW_XEF0MK>Rgt=Hh&?*biVH2FY22v0<;+|1zc_CBrSJvyV9IK2Z7Syr0rI{cqnks^X2w^88sSj -DoMoGab`EOB7505{!q{?D5I3q1YBe?o^h3Kq7dKMx!-*<8W~9C>YdVc`Q27l|mYee9ZjPJkB3}4MnX! -V-2DTQER~QtwB}-!4C0gvctiV(J0$`Nmy}?sQ`szH|wjM>#0~vAtPp}Sd&oLdNjizq -ELVVwTTQ6Belz8Ta;6mw~Rdk)$(ivT}>Ih+;mUB19r18_18w!vOk@BeKzLu$;j#S)yQW67u`qE1v=P- -of0hQvl33stxQ*c_x7^zDg^gCP#Xd|KVFPz}CM!;i^%X>$50k2Q!J?}0A;yy08TSS(I&ns7RC~C^U%Z -rewB9eXN;wAHA+Ce_<$Buqi7O-D*lhe4%c&jVk>3#2#-_lB!2a|%=b;-k;haesIAJpvtO}AORb0-g;7 -)qb<856Wk}Q_VYoS-Gt&m)wo&K%1Av~|FP;!$R3=O5#J%ojzq8mJ%416!JubKjz|244c?TWC# -7d1oVH$)q^5ymTxL5XY7$bD0jL!6-Uqtzmo874MiYMeBkxOVtvX-7mY!rj_9rNcJ -RdCJn+{f!EKucQ$*Hs2umszVUAgSDMmUtM9hM4|NlDm3ki!d^wi`~jm|(5;XVw&QqZ25I!@(I-5r@z? -1DI%IzSCnu_*33Yb{x33~>8%NjO2k4pfp>o%AA@6aWAK2mt$eR#S7On+w7P006`n000{R0 -03}la4%nWWo~3|axZXYa5XVEFJowBV{0yOdF@!;Z`(EyfA?R(c_=7juCiDNdFwzZUr35$2rOmQJc -QahfbES#VNW99~{rlJ8ExCmXG=Z917wrxUp?wE+=kT{2@u97`R)3cX$fZyahnSsA^BcUm&HA(7s$WFh -9{wU&x7OG^BRJa$t#S=4;CT7ufSur{)uv!W3EvYxy!Mw^CjXXB@11~;f>pm{io61LYIry}he=~pK;ah -mHw8?Yi$3&oa&$Z)zctE$L!<;u!6)LXDC(!PB0{Mk<;*IanT<^brn$O_GPAQ$SU1~@SMh=U7tBaK#Dp -`z4V!^%>a#^y^=f**bXH+bvKWP%DVBG~C#xM%PwOp;}aZAF&NCX)#lE0Uw{GrwnMxwJ0v&YY7lwX>NK -e^$~6J3E^Ycy1-QXj)up-*ZK!3n&b%tH`zQ9_6 -cl}ll1%Pa13Q4Yny*@1Y0(+PddBnpS<4!YfIAAF2(l~Lp|DC;FZ@etb=2s}K+r34Oe5F;L}sYN;-#s?2id>x`o79z&gMIa)qlg=_uA-)xg2~Oib -m2hv?jeYyI%G5z#{P8e~!<_kvo{c2g?zraT8MA2y%Q*_M@n;PKP>J1aLesph^_n=RO`%ec7F;x_1zbB -a4q*5e1W|+{>j= -7UGp&{Q1Nsly5;Qib&-_sA_w-P&M`22v537j9VV~z+Lx8x&r%*4W_@LK@ok}I#uquwEn_lkZTy9Uaj$MUg}gsF>KWXOU#n`X20rV* -pOtLIR+p!_XNqR-SkUxp)A%1w-IE^6wqq3Bc3H^P%{KU7Qq`1Uq#@MfUx$qxT@>eLs)~tOJ8*it2-HR -MCL0RCp&cUzNkx9iwrA=v{}gb$bh{Z>S?|B5EtoC{eR|78gXDjewfM|nnci$a*5@}E=Kp9mEWns%8<(XOEEPmBW1gG^xmLBNT%ft!>=L{oq<^ -)d(Jt$xFTtjQ(!Vv6s`6e77HqMpy?BoReE>msRC&B$E9?FxbBP#*pc%zv6bV%;fTV@lyRQ1u --5){(l!9-yqVpIqFhWL5txPTFllqG92k9AWI{0p$rxX-kJkcsvL2zJ4euJX5hQhAsY^Y`q@CAQku|v1FS%fJM{nI6GJC -9Z9tGNb-|nI(`Fjw$JuY&=~~@p;Tv3d7Xr;|3W>s;i?sofu&F=ie6b)@IqEOOQCsUczzQ83$ahJnmV_ -v7ZMZ^#ugF*M?*lbp1|OZZVArbw{9-zIF3=H_i8r=n0or64rbd?0n#JeIZ1NXSO9KQH000080Q-4XQw -_0J(252C00IyI03HAU0B~t=FJE?LZe(wAFK}gWH8D3YV{dG4a%^vBE^v8$S7C44HW2;pUvY3SL|$AaO -+Re#mH}Skw8lIKu#*)i`Ck~<;D9;xMc}{RO;W -(3C>M|EmShaoh>d8C}E}#|8Q-Dl5iPcKtu-JFxBn-23u7@!M2Dbm4(W1gb)TYk#pcr2`?=OiQX1vE|^ -TX55_k5F?{h5pdKv~5|J21hwoY24w!WgudOWy=jWTvCT7mjSn1Vy2-%!Z#@D0ieDn^Tb>>g3IXBcnW0I6aUy2Ck)=Uw=7I -3LgZ@Ogay>F(hku|5xHv*Gl9JetGZ46g5{H{<*9-4x$$VL1H)e~qU%ec(tFZo{8SP5z^kf+)m8g*hjZ -d2@KA73YvCc_K2AU|e!lu@#4v`p&f^5tUpQ!q7n)bT37b6v8q~?}&616zU8&RN9glR|rtQnyu+{tlkb -ffKM1qP?_9x!hw(7;7Dtwy_KJaE0`+DJDpC-GguglTk3_WO0qT!61a|fVFkV5f?Jjw?sv*+81jD23?_ -$;(3{u;_2E>|*!HyNESj0vkJr%j?EJ4E4;>qBtI{Gx;^*XMk7g~Aq`RHQ8<%$HxgpQe4jM615dP7Iv_Az8@kV%(e3bIa=*B~o -7~Oj=pDbw228I2YhLbG4qzlH|4lfiz)ZYvPZ^DggV+O -N9|}xF$|=uoV?4G&vZOEBPVDPH9D6Ge$(kn9hYV7KM){sR?!IV7=R*52$jKQ;yv -=aW&2#H)?#0-7rlBmAgJ{u>Uh@Q8))Es*i>62Wdi}TWlA#LvR@XXc%qNNK@PFjz;?P!`m|YGnTtCG4o{3+<@|S -ndk`?^rH1_*MVf-qEFKv(ViY!D70RRBy1ONaW0001RX>c!Jc4cm4Z*nhiWpFhyH!ov -vZE#_9E^v9RQ(sTpFc5$Dr#P91rYK~rou&#=p0;XCTZ=@eJxo*NHWzB<_%g>O^xI=6A?ZS4NSmL|&fV -{y9nSgH&TdG<=*<|`l;y(8l~mI1IOj}eIT;I*9;@4e^kZG@3dYzGd=ffQ2nIzW^$oZkltx-#f4*MKL2 -CH6oSjTUa5N3uxM~|kjLFmkMR#ZlNjjD~r5~q1;bIEoGX`!QwKLT=!L)+M*lP)BbsK^cy$Rv?_*W^+O -);$J{3@m00OhEIx@AxfLO{JHuXH-t+Vo^H7=kDu?S}mF*io|0@|qysLuaiF@3>as(O;r9ucS3-w?^0O -ibN8mjt*6Uy(mWiBZ}24o%egaSEM*Fs-@%LazG8nES37rMntqEjn7uoF+hx@@IFEZlIRTQQCEnZ1#djm}98I -tvKX>s@cnBIxB{fbtAGX77B)vF0F+tgNr&0^0dJ|=opcU&)(A?@1gG*O`Kqsb-(R*DkzJA$1`qOata4 -W1_=23iyIw2EzT-i_Vx#0ee@Uo5vP)h>@6aWAK2mt$eR#Q9AxJJ(b003zO0015U003}la4%nWWo~3|a -xZXYa5XVEFJx(QbZ>8Lb1ras#Ztj;+b|5h`zy3u;$)t79}r+zkpTq;^w1rKT~Z~c8#cCNPz=)S>qkm< -oV2^o7gOTn<9npA>jSX~vA`T*7^7YA46oeccD!Ne`UYDV&vAcbY{wH2`AhL1%*dknI`P;c?3{PcKt&B -r;;fO%#h1d!H=E7w2pofPDHX`k*$4o393(<7iGjupJXVTQG1q2w5oxX-mZt{37$r3xX^rZ1O{o?~T}%fm^dN%;>vfO$%=r^;-PZ0Z2f_`rr7M)BkgyzZc94Bnua-$ -SaM{JB=#*JpuYM2507i2K*mMAnK(p&oNZd1`LN0xwCnY+`^TRXDs@x&! -x(f395%y-OA3LnmKS_vI__A#qW{!Er?v!a6m1%nG5Ni$ZhLGRAXy&<`|ZpS?;nQ6N$Dn)uEs}Ye`Eh$ -Tj!$6%un--Rd&@=X$P5xqNBbU8PmzNhikDTzIVJ=h*sR1ZsH~-S6Z%-eF^Bx+(6HZ+ZjRo7B5%&F9)V -7iMy>D?b#Ed*k&p(JRi;c^IY7`7ylHT%>8%8SIB`aqlfo7f{VXUO9KQH000080Q-4XQxxznc(?>=4vt+4u^vQ!zo1=ub;TApEADN0bKnm_oZUlAQk*T -WVtN_kExWz%*J~fKZP+cXx1O4S$vc+&r`Om*Y2nai%9LwJkm>9M(gRrQ-VW-YIYLy11zp56+dKQ#-qf -{zKEzOsAzbsY4PVDe_n9@GzTtJ*klQ6P;pt}C=B!HK*Ml67z`4c61_=S96`_DXsk -w(mm;QN!o+wwAn^CX)E?|M;RVe{dZhm2a)Rnje&a0f$l1hd;3lYtHMVFzcN^Isrlngk*zI!St+~b{yK_;!p(}rj@tSv=SPCzAi%dOVmV -&V3M&G&iX*O9djOL>RWFgCAd-L&NglHZP)Wk$5={~*+!ZMSQ?;iCWw-qP1_C>NT*J0V>ek${0{{c-dq4>FNfiOW{Vq_B7E3;9yF15a<3h{K9kC_KnMN2qpLtxa2ayN;E*u*A%RmHO!|Zt4%v7&!Trp -4h`S+c-}}|QcCN{Awt@GdP3~Rr*t>wh3V*f=9*B2CW~&GCi@Un6PsI7#+IKb^;`zkAT`yhd+9vL5zP! -73SD(ohNUywrEM2%s03CRGTLb7iAa`ScZXJKVfWg_7yL5vGo7}j;3XyLBdPdeWKXB)F%b8EscmCSjI6 -&pv$gaGVySnjVN9WdA1ruNZW5oFtUdU!KTQ2ps<_03yxwn4s-Or1FEWG8l1A|wN0b9DWtEE$I3(C%yG -xyde*R$K%XGhC=faU9DhUxvn(L=E74F2bV>#YnJSZwaC0v`r#(B2OkfO~i2*ktCr8#L67@4+T$Y>@CY -6v(ZdE*2g~|I}b1zTa(}me;j2TLR(+A-anvRpi6y3;AHhb6O4P{1!s^P+A9rcDu -)T(BD@^&onffLpC;&RZd#Zf(6K;HOA+pju5!=_hcWyn^rx92Et^fP(mqmpdf7J$|5W!BS690f?QT(V* --^Mg<^n9_8L|;vEINy!GKA;sg*{3e49K+LSFvfFCey%S<1fR{c_1B2H3_d($W{8R&WIrr5nN9Bm~TpFkTz>uwPqd6OnUuAM`n{Pe5^Lw@@WeQZ>qK!FFX@y386TP5Vm7UlF1)FQ -?M5qv<0Ht}1Ig{m((skAa2G}&kreng|ttum%#-=X%j4dHI>s|V~RP@fou6YNzQ?=@-rO7rj@gV`tAA7 -J^Khswlmnzz3aMi7ggN5A#EAYpF;p4>x+vQx)3o}NOlLi7-m;lp>mgrqjVX4sxlct61w==-74=PmxrQ -Kz(u^NHycGFftX7j$s~;%NhMcNz*R?{ZKacc!Z#_~yR$n7RNtFV>lb4!E({xtfSE5RQ=&uYA7c>&|A_Cd~4kbw -`@p`YFHGxWo>^XmurrMeok%$i9?}D`ZPoW?`K>!w}g(LmX0;ebq^C=0i6;vG%h%K?8(ApWgEbmGu(4C -!XJ0)IGbqXL`U>gpQjYXYtTH@HQ)yyb2NfJKUjDA^{D4TXTi-S)gOc2e9zFIZ@`CJ9pGisOd-d -Vid18kmtQQ9?>IDjmux|GP(*;qB#3!SI*1;?S#oGeeS&Rt3!q_};?k*{5LjV6CzDy~SPcY -qoZT)swj#M4or*|BE>vtq-Mwnq53l1@WskMeSe&-GOcuxH83iR{ -oUv@)Wm?`6<} -+z07~nh!gY?3!G5HFiXNSfQ>I@p+sMHJSfiIP@7Z#0~InV=!wUI}ti$yxA@4PO}dsLKFd+ZgXnc -35`cRl3X&au10edx+A~2|CV|u1 -SEGH_`QNw)smY0LN;W-WQ0>3sz`X|2~~dbo458jkuV>_4zH3)BRkBM_F{LHSHNYg$O-y!w|ToLUTd_M -O4DUJUee!>#+#wv)Wz>YZ?`C$ZHSB$vx@&7#iz6kmBst``J~ztUDZyW0m}%HYlkHCXhdsfp{kHLN^wA -BJjoFA(>;<9g@G_Rz9=83g=Tr7goTB`6+W8j5oNm_D}|sOLX@6aWAK2mt$eR#O&Lc!u -2!004C~0015U003}la4%nWWo~3|axZXYa5XVEFL!cbaByXEb1ras?Hg;4+cxsMe+6&z!SX>7+5l|;13 -p|{MHkn{BH3I%EEWn^qHT6%QWd3jjH3VjX7~^(N_Mho+FtHzU}Kpa4(B~Xal&`CY+BN88%9emHnVjjc -chS_W$)UG-w>Tc>r7EGyrqb)0L -^MY1YYM*7-)?J&DSC`L!e0|wtIoQ2hwyW2?Vh+LXw#1>Cnaw-{JD|x9a*;>d=5{DoMZ@bhNBU>8JO?` -RoUF*zjKF99loq#4l&=N7qr9Rw6`QAnnR9VRD_*+MZ>7Bbo+|~CbwjU(>T2ARl|R<3p)Hrf&6R9TyQ< -B-Sn{S5f;A4{=Z>=z(-*A%>dVKt=`$c)k6+Jbvwud~gKf0fO5P-krC!>Wz@!yjvM@ulr^4dsfJgadh7MsCbnQcxjei}OZV2 -q84Y7C0qV6dB;r_>j9C^vT;iPZX=xDX>BdhUgXn%`lVo6Tp4{920MKdVj(>n3q|5iZ~0M3%XAk1K<%BY0rDZB-3X1qJoQ2dO!xyNAO+bOV6B{!T0sC!bI4u?K9K}_rkcJ**01%eh8*< -cz(fDsb1~(0Q|x?C4*@qf@T22ZnX8hkcBEcW@`AQZ!6=2<7A3B`T7zhUnA~#$D1k1B@)mbi$ogQz1PEoYuV)MfvaT*cLd -gIEq61F*NbmW8zZBQ4;BR5sKN9oq!~ByEW|{1_m`l)D8fNU+dcf -=g=*iIQDYupGm1N*eZa#}rt+7sL+$4)I7aLNT!78bW)6#;}kXf?j~XG|U(_N~*^-0LBQXYF6+yhZ#up -6aXu5t~;jZOq|+LP!Ppyu=nH-I -%#;&F%iGngKbH<0ou$QFdy0m)F)?4$rgUGsNjEgN!i04o>dQTEu@JN$-Mygg*(l_Yy+v54LX3aT_RdM -9~_i4^klP65va9pUGw-C|22IJHni7*sbD!=&D;;R?21K1}qKw#$)_a1{3p*j#wv;w1}B)EPu$Z8RjY&pdHWmdKl*SkZ>T$%P@>NN@-cv)u#gGk}!M0d)ivU@mIO -o`d62a`EDV0AlBl)?JpM2)eTfazL;o82Poc0uuC>7f_hjk|QPkS4a`uiDU5gkDUDn -(DFgj(4Vm7$LV!>z!SIu3?j`j$z+KC#l6z3e(ky*QAT2C0c48$0p-;69%qlQG%v -4OYE~%j}V93>f&8FMI9L8NMd_ykNg51jv^@jGLEa@!UWMm!)!l&tZ<|x@~N(=>K0`{`G#pl3$QQ+2(E -E!r<7_+oV`t8Uw*#Np)ic&d?by5n<+L1C$w?Gt>WUv*e6DhFcC;@T@5lSlw>q3qC4qaVYdLZu?+uqj| -YbS`avV+_+%rb*QueWhm+oi*io`vD~z^kIEz#~ktGnh8^G%+zU+W;m=e_)0N1%`NpSWcjakbJZZoefJ -_&mk;|(jp8-W&40{JfeiZLmCHh;i!E2qk8y>i=-iyDXAA)*)<+3Cyz+@x&FwL)mUBtZUq;xhbP=|oH? -yeK%@>(i1q`+ou}YOV@7%L8v-0ds~%U~<{V>w1V*Ium_XeiNUIHU -2C_?{i1PumV3K-1`fE@SX!d`dvpqGJ4r@8aYxo^HHHcb>?eEp-Ny%OK>PTTjQckTy&TETuLkAmXRJ4o -=Stvl7+Idm53n^V|I>5;bcBR2PB|heqDG{H*m

IH(f%0Y9v@x=X>vC3*4^t8cch0MmY5^c8D=Yd)hhS-8o -to7SC>r}8P|oO1#IN)+rMY{M&=)Hg9ONBr;q -?`@Q|?Gd5h)t-{I@OsyiH8=hfDQ()&fuG5Zb;=l*$P=)uQ`Y=mhdSvy5e -I?eL^HqDYbKp^BDhcb&XMHAO0LV!PhU~MLEPg0tG4*vY6~agKcKwmE#tDyg&8$OwI9MTWcIQ& -d;gbIA!<0bHx#d&_Ap8=oR@#mLGxjyr%;3{-yrME0E(J^;s7o`ZoWwc&FjZ3@kn#ef@t`Y-lxos!C(5 -dHwvpV0t7@ntqT^9v-zyQ7|eDhei&4!tUk%(egei-bgHgXGb%?!Lvif1cXIxK`FW2(`KUr9XudEivE^ -6#kbjD%Dwr%7QB$)1Lk6fB<4;BMT&PyMY{ki(6?&=zVM33--7u|FHjgWH&OPM)Lna-O)~N(#0D^sky^ -54P(|Jx06P$b(4B52dGhMn`8i3V<~AY)nuI|gAge%btby22@(l-X#hLTMu -5tSjGIu#F=;9ZaYH;bBqDYuI)7)`g#Jq8_gUZTMSD^A -bvGuI)t)K9XZyMS6^I{6@ALmbBpMUo} -fBNLvzh3RJGEeF%>>^%`Budd_f?aO>`M+Os1m3y&Mt#2pA^1!e=qTcL48R3Tc4{+527_12hhJXnR{&}QNx4**MzBv5S3y;<& -;mKyze@`$P$FIa|+e6R(15ir?1QY-O00;p4c~(=R -9NGvZ0RR9q0ssIh0001RX>c!Jc4cm4Z*nhiWpFhyH!o>!UvP47V`X!5FJE72ZfSI1UoLQYjgd`mgfI+ -+_c?{dW0VRn=t-0{L+uOP8E~bytKAy;}r9M}$ -5wdL>><7e$a&+T?VEff#IvHZggWTwIO0WWhHd8EKh%5Opg!0i2U=n?xd^oALkx_eLSNjIO8m5U(&(zf -353wo_98x@?BAhdEqFg)k>#ZB12GW?-6u4xIg>;6^v&8C)l&euftcaT{7TyT(&gyy+CWPuWqKUCi8Uj -Pm+mAg|+oe`P_-D70|?;^KKfn60_T;U0+<&mpNT5OuSgmBpwF$1mkZUt4=*X6+eHKVv;L25%md!t+M) -#7w*!KK|nVFT@%!|CcS1`bexk0?Q~vm@-Gx{6+EsP)h>@6aWAK2mt$eR#UVSBHl;~006-&001li003} -la4%nWWo~3|axZXYa5XVEFKKRHaB^>BWpi^cUukY%aB^>BWpi^baCzNYdvDuD68~SHVxgcQnUk3&#i5 -1oRKQ8(i!X^2xDJXt#m7?O%G#LZ^0=gBHOP1G%)Yp!C|Qo(;BbOriRA3;Jbv@oT@HuC^Cc4m|MF{oMH -BcYF%k8wDEXWUK`Kt>ahj59Ny)5cX+mc4{EFrYS#o|Q!olFABJ`%9^GX~J4#*3hWidQWSx!k@UW!9W8 -0B$BM`X_Ps^n>uuo7q|`86wfp3%G_*Kx_>SxSKkk)QXI_kVr*WAyyP$(xta^P?B9C#NsRKv}NCg0s9j -6fkFpR#8RokIr9>i?~_};tN{DWn6(~D^@KD7omJWivR?0#CgKIL-IKc!!KV -3gEK1XRH^BXX9X_>N1b6;}k~Y;K6V>94tznk;|0N2+ImyR`R#Ht8S#sK2l#%#*;dw1ASA@pnr -4G{E{juM*70A`NYwmf)x0}3@9x_v8*eW+N-tYS(WjeqN4|cM?^wo!BUDiDQn3PFfuP$QH=;X`!_vo&N -G<(H>AsXMSf?+fruC2cfrYu0xOo!(>kGKRkDgksE;2=4ke8fb1fB7tioa)4j#e4S3m>;AQ{Uq={6mEl|1enwEm`JYxBRPrtt)Kaa@|3T}_=7xevltWXgcm=cYvg66&=@mAi5(0tBO(y>=pi-VuM0FAb22H^*Jhe@3kYJ!EsePG3C41?TvO_xYFJL;F3feKnvxhLnY){R*g5TE`{qy#!KA5L5B)aD+t$oPj>zJjOiu}pVoE8-zMo$Gnd=@i-SrU -vwAu;9%a}zU|Ar%vhb|Z8IaAQ>zK_*H;Q5#u~`1mBW56IXJzqWKIIph?U?3T5R^ib|<$Qf12Lw(vlHLIp3`HO-sRp;6Ru -1_^Wq4|85-^H@;d+=yoazpWI3Qo}OaXKW+48mt4tkU`STcwUrr!EXF%kGdBD8IPMwZKOyx29d^`-`M1 -sfwyHh0KvA|LKy8Ik)s4VkAteD4Rp;cjLI%V;sd$`D0nKHONr%pJtC5oPg;Sh&LX^|mGAX*2cA+N+~t -mKz(j8#1m!)*e3<~IE?p>!lvTdlOISc@!KQ~$U&dWNDwWZqCFIoWYjwN}Nec}?q;gsWrET`f=tv8grm -PqSp6?xMeV{pNbO34~Y&8}NhW2<%VuxXfQujKEWOpB@BqU-&ewh`B -e&uUB!*~3*hP`5GC_+NCM2r6$u}%(Xo610chxZ2bEvP$DprjO!V>~jHXd6317tmH)H1ej*c)jgbOefI -NHjdW(CY}JJOY@EtEy~DsbtWO$Jr+nT9DSzJ7Ay%jo~2`PJEpyatw`>2^kq&=3=OPnwnS!WVUuoL#R| -!EU=+797I(@_7p0#&S3!0(={nLBvtS~H0o(B)0?NFo+hk9V0fXC%8g0Tx0@YQ!ZYHH$@Ie5co_?68BI -DO!}b(I$DW2k7#9Vy-5_=CX^6E^!2?lA|64%%-Stmr!opAAzmetgdJ`^p(-HUz*O)d4@A?SZcR*jb|Rjz=qlXn -vIQI-@wdJr6F(7D1&s8#8tcjWXsG_*t#tj;x1DtFVK4NRb)D_4X;>_+%~WB2Go?4;=&#?i(!fxNK(p1 -kfFGY`!Ge@3@VrT&&7Q^ue5dtLl4rT^GWRK7B>YZL*s>xLT$ -lu`u1D5zTe{9{vuxNQ)5Aa=4=%ltazZopsa)sFwVS!5l(Ih!24{Fi<#Nj=kRC(r@Q%|W>a5zkdlV5(3&s((wqM0$;Xcue>u?K9~&W_ -c-9^k+hutvA#Qhh{NX9@QGLPdJW;xU>O+%_Z$V*iRqL%U8-BJvnczZjuD0L -IK(&(7e$We@W0Mon^6((TyR^y7uK5?4)k -_vaU%&TB~QSzKPBBbcc&Xj3UkvoExDI@kU|BdZqPH>~8IDk|6rVPgiXL^pttw80}@g;conf_|Y|IKt$GCstd)Yq)!lLm1%& -Ex{}g2fbiU@Z7OX9LnIz&s{v6vE0*6OfGR6WM~ -{J_%jM*NoR?NW4x_^L}M%st!b*jyv2Uncm!-+yLOyHGx~roGzlR*bRo=eqKRsSBts{3wL+Cy|l*D*;w -uuclRoEqC8pH2*CZZb`+?6?13`s?$wa(Y)3FecbO|`K3=Q|tDv#~s}df@SZ!?Yi$dze?Z#7~NHFqJ%r -$oja)S|zVJ8j7Z1y8*VnCU~*?`xZXT84Oyp!4)5gRy_>5(?OjOOMbN{d$}WO>_GcarS -C;L=LhByuoUhpS*|J<|x4rj9`%c7vE$Z)xe1}~2?_UGCTXq!%b_4&vd$v-&KY24z9qC3d>nvM0ZGO+O -?kUG}%u7HHuc2wC*AQU}%@~AQLKuPOFl_(9s}H$7f_PU@h|DeQk2$WODq5nGYC%<#Zfq(i5~PpWL!P! -biBcg;{@-gIvZKUGCMp$v4OF!M+9_vG@m%bfgC~2u;`Lb*mXRGS1l{Dd&8b_D1_!U+y?Y&<9=)CX0vd -mo?7UrZ=&6F|-PU6mx}ICN?9lHqLzlGw?yksfk8IuQIoN|oUo-@>7+Py>cir3K$#Ps?UI*mw=B*`(X1 -GmnU=1`pr8E&cU%AtVV($A6hU@gH${Y^0`}A3JMrQ}hS6_0{xVBHI5BXKjS1S1&lJCTjeAlpO#L-KOI -2C>{wa+gGjlA;pJh>NQ-vN^A4@Y1Cbn_CQXO5)tBI0uFXRmHx>m91CQG8p-zODy1vBmka8H9$-)TO@A -Z;belA2&iyJ@w*}chQQz&7*W&Ow+y@a&FRroZ-BzX%Cf+Hc3RAl-4t=KKb4{qdBzK?+Wdj){s&xKzeH -PevmWqmK?EF0@pM5RodO&-Zr1~mwqW}*MyoRvY^xxH*o-E%Xw=EM?3G!8Ac=02Uq9KQ7;%lG~xc@_!( -wQhP4b?MiIVp<3$nXgedZqGAcm~{s2%*0|XQR000O8`*~JVz>+DQ#B%@u|E2)|B>(^baA|NaUv_0~WN -&gWaBF8@a%FRGb#h~6b1z?CX>MtBUtcb8dDOk@dK*WQAo`zAQB95y02wSvc6+B+Ub;susk^mfOI%5|r --$YOMW8@d6Rd(!fGF7GM?2qnr1J_Vu9=ZpSs-Qi>^FyBw?$TEMcyJK;~q!ZteIE!<&$i?Ssef2;PCM9 -;Ix=s7nfzWF570io|W0t)0YRwiJyamr@m3uW<|DaE~{Cw%of$M1jzNKsOoZ_-Bz2cs)qL~20hN2wQqg -UZZGE5x}0sA^)B07mFu!u -?91R(4Us`oc)s76wq&&=$LbdE^HNy;FqMoR@8NiK_=++&1fL9(>)bZ)AbnUArl7^!s*vke8R2{AQdL^ -*r0w_zocW7ciBVn`~CVW-MEQOFOfyZYo-#Vn-bvPL97pK|hk3NItD(2-HrVneF -wcFLIS#M}^)9Gconaqn#F&s^&2RG&Bs+s46TnCFaK$uJx+s$@eP9|A(L!T8FZL{2N$_f8{a3IfNlKN} -oeybY&ZByNp`up1bw%M&p*BIu3(=dNZ7xLh?Sl6&au6?^H+`GlL2CQtB?&a$Ga=WRPZb*a!&Dy;N1X# -ch>EE+vxdc>2B=JqEcC)N5^zZVfsFr|H>$Y@Y%JsVTQ!HhRv+jAZG_B6cLBGJe}%yuY}UIc2O0dxTYPm;ewdZ3P4y+N%s0Jx0kt8t9o@OUUhmf1a?e87aB$bK?Rw_M*=pTf6c@`K0EH8o{pIP4B-}s4p)U{OQ3Dd!xG7 -CL!C&LNU3%Bld4`c@Ce_1G2(S!7ZseMH`^M|2<^x4&{2#pkRia!6R+D$Mmj$nQ4e@tPyhCrV*k6V1^`` -b;he(F#oJ7`Z~lapf(v};2RLtj-0&a}=#9B=7vM2#Z^|dky?W -EnT%i>0#&z5CTZ&wrdKK@_=DB5`z!hlgs;BHwLbYV^C_{U!m9Z11J9!DPIg){*yTkj^T24>zJ3(hJ92 -d_CizMU5(k$rs(b7`4y;5cFBi3`z}#B~7{mlGfs^JV&#GEHum4+yuBwfX(;AAQd(!BDVO{uD$;E(ZVo -7Wui`E1;dkvJwMa!++at$=p(r$#FpBc88VtNF*!E9j+PiT7;ayR6*Cd>*L@6uqq?QU?PBq?_~^*0Mp4 -T;hL(m4iU+}b6-(gp@`jfFYEWkvYJctdjS5ryewur10DzjvQ041f|mQ|Uu5_bIXnD%2(OWH0Q3Fe`c~ -#52WmLVx2rief;aTSfYev>YPQLd&R&N@3%8aC(}U?Ih@y# -glRvm-3{94e+PYD-epK|YPJZuLsAYE))O9OeoAJU3&WG?V4h10ek -A}vLYY7{&RQk;A;6XlXR=c5FQ0j>q!=i)a28@!OL35EK@mb68;ZNBsZWMIcT>QQ4>onZBpmX`GulvN} -5<+5+H5_hOVumAsEQ_0qd67LR)|c%QJ^t7mejqGWIiKj=KJ@4Q4G$ND+16eCgBJM457o`~#&NwM`|3$ -KpihxrA)5_&4FAos9}itbI=HviC3fA?m2p$m1XBR6l#SFOK?BC@2X|~Hb-amR{A|LetgwzLc*x} -x2>0SipaK4?v%Zpt!nk@U%c0(d?b_+zSs5eZ|W>@75FQ$->M+yhUp)H7Bl#!r2|;hyR1R7?zTB5>2Nx$O-qnm6TSvP_$I(4J7UxJO1FtNWez(5{BN^Hy~}dv3BJNY -k^mTj9FE08dXJYi@U2ZN#P=TKL}}`y?9<@)0xo -7tEpSl8k^(jnGkPYR;h&1Ont45n3K=kU%P>47)+8g=1YgWR6coW^AUHP|!>`+V($3b^YJ-2#kzF_MD_ -}MBZwquVW&o%XvAmDAGamkk|FEe?$|1XV|ZHpvxlG6!6EY&EF_I3!HMGI4wI`;XgA75v0ZK^Gmz>^Km5~m1#-*|Ww9m6 -J~WPuxV@6mYG9^_-F(k8c*_%bBb|Ly)Ih4-z@G=3kIkHPg@3-leLQ6ha4YFV6$XKjmjr;$1AZo_`dL# -ifD95O+Yv}9e{MiD)n4jbQltu6`;7PZf&Yxa@ERUzGE*o|T7qh=5J0H;R6L`OD<_%eU_k*q>g$d -iv&X?$5WczkT!Uh5Pf_Utc`?*Vo^^a}S=s`0mBmuTSM~?fLLjDAi#`g69H`CfrKl5fB~my1c}3(xZpV -rn%m(PysDNd-)AW{iw%1HMH-g9trypm2=uy_u~}S!#{(GUS0-&-mEq|3!}sbhRiaQSKH;%wgM>j&*yy -Z?W2FMR;LjVo+c1jmKK@d$)k?I7`$aA_XLFuPqaP&73vC;O50U~+I#-4FLepq=OtnWEq~`g>br9O@B+ -HHE!OjhBSUqEOc1WCo1&sq_w;>HEekk9&qKm`aPH*Xi1rK}ZSu_S(~FjXba)s>g*vEr93?f@&hMsB=ZE=C=J5}yLsF4`ogvIAiTKckOwWX?x!xDj0(M#GOJ^QUOtfO~PiJFf7gE -tkM@DdRR17%g*j$P*$NE8C54BrM`v$G8DAzNzSt&;4f}c5u}3R0U88mbo8{h>yQaP{F@s3H9)2nmk^J -zaanSy8z%BzXZ6y1X?l%^Dl62R^?jM?YIjFW#10q5}!Rf@B19!-}&|9^8?c!v3~EC>r8W`>&0gH-~n0 -OfN>moIJmwOf~Z)KNoGkfPWfPpr{%cUW@`u>+^!>;d+&J^5Z0o^>y&no-Hi-w$7G;6<%R^OtTvqSPntUZ#z3e! -x2{4Gj%~qwVZcqO<`R3{WeEkMR|DS)!fBt3QrWsp#MhD8iLgUX54F3v$e|BIPSor(#LBPSnlSc=Zg@p -&lk3|`mV!(#xcQSg~;qR=Tj=#;OahoaoYFwA@YgYJ74df|rVDN}^U$BPw2deXVuV7&d=M6EP-Zb;=vI -KrczMM>_%rWBWObmrwW}KP`Y35k{M2hSILwk_1_H%4BAY^EmPCI<~bV_p};wxhb=zL!;(R_kz^Qxu00wg8d)7WxQ-Lqu04mq;;!5~dvL!>SC-3X}~ -0t$3rGR70qneiX(nfdHol$E5?`qy$7DYT4j)%Nv~;CX9U8;F*2fd%BtV;B%K)b9nVev)w}AZD%L2n-X -OJ{Hg;f#|gDR+q<$l#{b_S!|)e{Mb)bS*jDK21OqL1Tl0=i$5S$!}X71457m}#q1Py-Il;RP(XDCu^t -l6Gg706Z%HnH_;q!$F4nt;&zg1lwp_ojK$Mbq!Z_sg%^&~+orqsU&Wf)gH5!GTNz1FErBzpl9TEzQ`6 -+=-eB#E*%xk@&HQ{e__oeih{2DqPoRYGBBa;}Qo;d)T?l;~E^TAmRQZ8V^Q`*cNZcC8shARA!as@TKq -Y$mAF);xFwiN5x)lhsXLbJ}#gVW}=T%SH0Jjn*D)eLW5dNP6SM^Cu+O5gl~MiGUAo;VE7ke^r`ewR^3 -^j3&$`t(?gu(N=>YI!~W?ce?_d&V|{<&L2V&2P~n4IJ2Xy4Q|_K7}ZyRcy(q%W~eb%fJ<2Jzq0lSM~M --T?eY^4ky2V6;NoZ;Ja`NbI$tCaHE -i|~W(r(j*i+3qum$TfM -{QBHEQf1d+u;xMPe(uG^w04ye=vfr;1Tcrnwtb)M?j9~07Yu51B;+Zvz8q2l-hSF7uVd&|NhWFD^Qa& -FCA~8?A3hg?QyU<88KGyT0!{Ne>@Py46{_5^|r*#EvwM(SK-0OI~LEkbmqoLHco(CEOSpqUeU=96?J4 -$jcEW|7`JuxVF$FJY(rWAW6?R`Ajpj88JXajYj<+u-g~?~%F3dJArx&HvrXh^DB4D6za=V(ww^*QBmf -iN?lC>O9nzumCw>q(#5-WVHT&I37Oix -8ZNd*f?|%6CD5*E;3}3h(M{j^O@c7xk2UnM9q0%0pMZpXdwE|qozM|(biri;J36h1>HYTUuY#0(Ub;G -!9GIZBF=yt?v2WR#lBDXg|7~_neke$NcL0>m$n+Y{_Q}@x7&1+lo6Un9q!c@R1p|4w!1knR(fjU;$+< -Y++zc6mYUXgWsHc)fP;9R~A{I9|m_zC=K6|{a@D4*qKzGjGHfP>8UQGBDTOTU_%kWp|D47Y9>duz;-XU?&_a@{uIX2cFDG88&_<19pI*_kOLsdbZS7TJ_OVGR>u`INX38oP0 -_*WHnkq;VndjLMG3ppkQ%vBN$gjYn7hD}y1!M?6*@x8vOwTe!TrAvC$)aKoVx!0sqf8+wn`#W8sA5R3 -0J(ocE)X~4>Pe(W%5EYX^>%Cck8f8w;&S4B%+{uH-m#xCQ3-jsYJ25QkLlLYgtU!5eGvGe)w=t)1w0Gbk}=FsRu^b^@|VXjADx1Z22q -g99?{uRXO+HXXxGxI9#lEY#NcX5^J)Dozr8?f9~!g{kvQXt$HDXh#v=fwjCVq`0x#H*z0WUKALow+Fh -t5!UmC{fLBCWg0H8ZrBm|J>1HV8=&|Ry2u1-| -$z&B2llB5$GR0VkE>0akyuMF-1<0VEsBI!J)6`zAgfy;fdw1AsVqC>b`E3dV~?)A4fgp5PNSZV#2Yq}@GsZA#6AS8y~F>x1F*;+nDdVU6&50pu)RpdfJw}W0i)SG;s -4MiV#qT?7@*AucHYtxhL?7aJCH^DF&ANb;4UeKGwi<#JH1fmt^v@20Ng%X<|hmO^Mi$|LIEezM=hT+% -j9WWP;a7h1EM+MqM4j}i-%{s)J0&Tp**|H56+5LXH#vp#a2)MCb`LsvREly&#w2JMHDqKR?QU37(>=X -5dZ6}jho7@RP)t@6uazKIyM&7sI0{G5WBFcZvv23SFrhnJR(VT#WdX^$~-P;EP?G@N`+6|Q8sg6 -k@2A3WZ=55O%VAwdaaVn%)vj#+1HQccnp4_#w8>{>3jtB-!666f%pI%}g4p6bYE#Put+c9?=POe06BU -Gudi=`Zg74+_WVDV@8PL=fKtUwL^wEpXcz$2sQVtt4Ttn+#OosLoa`Wo%a(?txzDiS6zk_ ->wEN}|`hv@B*n}HRs70(sl|h52m;SmbO9|an{eFC@VEDHS!lYA;qvRmWF|PD|$0#EsOt1WnPMia0HNw -F-yXVx7WDNKm-QY|+NRv0ulWSHtAG>C%xbhriD@$klu$Uq$)fiI%HX({-Mfqez(T(&_ZVUJ*=CjaLfV -UJ;4OEYXYzwI|W3OpGLu>1~6)0F0Wjm33g1<%qmyiWm9~Ed>Nd}f%Q}wM5zo(IG2^%DuT4Bk@8AVAUt -zBBUBsV$rEipXCG#TDILS_w%G-kWXEiMpc*MOc{H!26_qCp*&bVB})JT|;VXjp(9Lt!esvVV;#lr%ob -N{0?0G^>yoH#sKm$1#+cdJ2y?<9b7xq=pHW&{Wuq6`lo?58oiZhfDulo -F)}(!wB>cILHHq5wT9g<{U{&HreU>HsvA)#m4ba|gVbVnd2^tSk$5q;$lMqdDZBRQp-?E>#qlqaD_D2 -SVEkZ`dJiXK8j~W%YGl}iNoC||c9=|P(V*qhR%0JyHRkmeWhvGKhnZOhZ-lo}GVxORq(6xxq=(55`G? -ZdZaRNm~9($aUq|+2RfU4da1FH@TRRi75&Tt^-Nl!`qz>Tm>I2`B_oI%TT@&iw`;GHoS05uOdZo{kyb -92I!4_KLUy*WXi1rHo~o1HM*(_zVlp^>zDj?96y*~}`FCXjjFkl;E#INR3lP9Nj_?*u==31Aj6A^`D~ -`A_cFm~H|*E78imuqbCRY%^#>o_EO1_iaI*3=so9%8hj4I6NZoeW=aYIFJk -pjtgMfKnBD}K -_$7#4Bp^)f0Ik&`cDaEjTHMAa7YZ3dj1~%|5OJ5lB&D8_qqr{ -zAfmM085bJ-?6S_*o@3;LCM;u2knAlEvv9jK9S3vh5RiVxVXVG& -L`4pa3#4pfCfkk;Xk(qeQKd2yjWN>_K~gIODg4jO=R5tz0rbKTz2hK$GUeX^^!0C@+xRG9-7Bv&2B#* -c+42p@eX$^@gr!dO_JjOV-Ef-PDE(d4Kt+0*nm_n1o4HVI0R7tnf{-%P>tt#+I*}9cqBTG&*|x;v_qS -7xeofY%a64oGi)5of8J6|mA5b$uq768hQUD7C?Egd>MlPzA7yz4Y?tGNz -wWwYM;Oi$5$Y*e_&d~8?DHEiJSPXj!mjTqWzqq8SZjvt>#pc-vX-Q-(hqu=*0!M)eY{vgDYz#wL -s`*f%teFq%be#l3dB-ESsO5X+NRA+_*4>gj__f0jw0~}tsi|v{mofC;n%<|p=$l}IFE{>HGQa`kV-nP -=)-XQ&%AQ-a!&|!|xCLG|<+ZCO&+tzT?T}Ils?J_=zXUXq-LpAS*U@pCYCuc}m^;p^P-Q?$_m`*05ps -!8#g7{N&30Zk2O;q|6=lFA!y>3=5C;d3YtvSrvCR-F86f8`Jpe|{VSLlrOW4IW&{pXzz?$XKUL86!d{ -3s-^aQkfAD;IhbFb$0sWC$4x{DHQUh`}I%P_>&x7KPGBe1_S1zPvc|<1hx<8K}!#kQ}T?a~H%ib@X-- -aNN(hzwYMRZHQW*lOYi&DXJb;T&FfwS4aSG|#K~& -8y#zWDQWF-^`PBr-&X|0|C%tmIi(o&A46n{I3G#;wfOz4KX=Um=GvfA(4+RE%a$3|UPr9sugE5w5Tj- -<^B|Jz^k-!_;E&b1{bvFuq6rmz=3j*}hWp&XeJty!7Eh%<5@ZmTdcMKX{0`c9+Y$CM4aDc%x5?06!sw -w6Jo%kr!<=geVpK3wGaj|}TL5I@Dce(0uUzL6zdR#{L_po`rhy9=%kjWUh)NpOss18|RSYcQtXd@?thPWUqKKL(iEnl* -0e4dtoZbX|Le@LuziF6{z%@fvD=jKc(1kuy*|b&`4>bbluM5l-hAg=kPHBu%E(zrqJD0eMJ6Fw}R~qZ -?$*0q7u`Ez_nv=n)#Z1Y>hk+2xJL%OT8_W(wOr3zun&sT5FqXtIf<|okrzFSiCdl_p`|mVI0&1SyGXN -8oeQ>E^vd@k;Tc9UTvf(sXvnd)D@EB2e$aI9eWldU9>#lAiw!xvEC#Gr&Op-vaOFF=asbfIl#=E;|g5 -{o5$5)+^sNGU>c_%Blk8XTDE1M1&Md-!IIuw=Yxc51 -Wvvi&ha=y!U&j{YegWuR7s~(zl@&w9#BQ>T7jfmaFWt*(da)qU}1d=)GO`VyoNiL|i}qgtv6EY=;HWx -3yb}ni`|$8Quh>v%;cM=wpn!JbRPc)kwmNW*^ffnrjcg8crvse8w9r=Fth~tYJ{nkCDvrAdmsW;5BvM -FE-IYy(3s?o+Ce95qswMG2(GJ6WQ>GtEO$M3t%W~a^M>|6Q8-rsUG84To`v6{}fr>>bc(&r_U!YJzg# -L-epeB*b}=jX$`yq(^z)gbjjIyyIc^z8AUvNNrZV_F3a~t9WQ3FwsxY~a!dXN>atA!&&0f_AA|3)lH+ -%$&yS4!L|vnp8m7JWJu*ZWfs%Cww#V1Ei*5p`-ZaA>=%Vn=`hGXT`55YNvq#{xLl^!%zg_u6*cs}T$_QH&P55vX^JN6MU- -UCV3YlM2=N%r<(i|M4Zk9Kt-%3Pg2IUjfYMz|)>I4}*!vo0VvS^(wVhIvuL;QtrWRI_i*E~rD1O*{>!XLBZ5_Jw69vs`0LpLuwAoui*NvEP>Eke5@+cNh-H^ri;j3=tpVIY -IQvG<#NFKJ>x(vA80tBj) -S;Z2xSZzmlPVB5&Mh`M#gd2@wUWdbtIv-b+Kx%nuOiAq2Od{Nq!dTkko|AlfR*I6{6YHq&X*jC9uLnz -M`s%Cz>WW#ICnG;6A^cc-$U=o_6A(oHNPP40`Hgle}l7TSSj6;WtLz7CAB*#S{7cvKB3^Hw#;{HpwcZ -bjhX?$KhNkgzbvBC=&35`*At(6c{HG*q^-8F?Oc-7;0ETAF8{YRW%il%i)SMp-q-~cui?EADDpS2+Ni -{5R5=1V+m_9;LqQhE&0EgB4H?^n>8N-b$N(>^D_7mho!Mc6q2M}vvu -Hr%v^Cg=vDRri1&k-k;g;)cU;~Ll^l&$I8Xs;9!Qrq5C$QWck?i3B)ml^x(i3q4ahdh6Fca^4Y-pNIg -{Z_RP+qZAuv}kyqSa!)t$iqz<)q|gt;aF=Wb{u-<>&oe+>2WVW1YSBndGAw$p{M3YN+cQ)B${AyiUyG -whm@WT|~J1I{7bldtfL)$NYq*b=W! -%(zU|4J2(%+L*r@?V@VS&SHs25d?at5P-Ui?R>#@~eMugCIE!l=A#*=2lVHZcsAnI)fSGl)mU4t@_}+zCxM{}W5^k3E)5wX2ASJS{;^Tf# -9N&brjL^{u7Dy>J%{s?C^swNV?PLH`xSHLp29#(O=fXiVX5IU|r;KQ5%=KM2hu-jmgv!;{xxpr+) -oMWNNmMVOAKmRArfaw}G63Iaz*9%qep5&RYcyTp>LSr}?tb=@y)3IYyg&NcM_a+Kt2N?**?}friBzYH%? -&Y0QE&z5n&nIkJ^uP*PRqd>f)}UEqHZ5^V?35D;g23;S2 -q%o7vS%sbgB4((8AiZECIa7+xpxBJ^jJmdJS{t;)mBLtz!YdPhBeu|xg~@8L)nky#gX1OPqgHHCvYkz -PazF@&2b%lDIu&4WGYhV`>JpwHcImlQX^*q0hMi_o=ZTXkci-PQOU&E2Z_`@;~>T3^G?3S;9UTSg9M&>xC_qi$BPEh=U7za8MqYMz@fwAw> -5A=2R#9D!#q2j+PoNYvxm|FSo>sccU`Jo#(D2kY2uzh&IO|ZR-ly{K!v2sC|$7l^t*#N$pq)vVo2|=Z -{Wj_hjM^2S&rYUe#`j#_y~f=z}}x9gld!IDxNx(wfCx>d3;*{Y2F_Dla$oZb`aFp-K; -M$vo^;b`!Cr4}TE$cp8>#v%8TH{|m*Sm8ad-+Pi{MtFEGcqSd>iaH$BuoD7)8C&# -`9En5Wc0sv1uy~gZ(V`z4CCbR|A^V*NPp98H7W70o-Umq_^k(Ahx~Cz+|YTtjS`&oUn;I -n%hxtA(-`ie{2rk4O>3`k#Oq<}(=6jpvz!CHpNRq*e&ad^qcanE4>N`WT!%{>;^ifeB6MyHd60;A;rMBgrzI(81U3L95|daLU2h!+-=Af3DsEBh_5IHin3)wGn1pKsIUlpqs&8%y*6*gP+ywi-vlmdd%>YA+h#2onxDgcKDU&;V_Rn(V~J -t7+K1&wl}>Y~%Xz5DCyR}-vB{OswozrF~-S?>CMv92^>0cYQ1{+b6?v1liqgq67f5k~ovsyi4ZIczBM -vk~u#xRj_69ekE6%NWHyoRsF*}G90CMp_3!@Swdpb_tWo?pRCC+PwXyxS}V4aCzWqC$is}{K_$nDEX~+f{0pMP3|H(8kF -+v0h!qy{%nr9T(Dy?g&*kNywXC^rQ%~%W^hHr{E;|;7nacg*t7SW{Tx*w<{3^tAb~Eht<4f@#m-49w1=XP4yoc#?N;1Sn&A33~F{Z`xN8clDyM; -p!X!sB|kDR8ORf)#g6}xqzi`=%>Dw23}Y%qcxMgPXkUlXY5s4$qZQV$}sC>2{+gmv{3rq3;Qs8G(c?+ -d9E1>(Aw?9m35UDwU6AGaaE9bdG!rO6ynW=Eg@^A|K$3ZR&m8+5|#^#}m$)^w+XdNc5~gAwIT?cfHJr -PH-~Hn_#$$o1enQt2IGs>AQ$U4!WwR^^(q#ETccoa-f}0m8Aeu?%RBY-a?D(|eaUD;sFb5rZ%~WG?7d -ztHePTtLjc+Qe}9sBzLMSZyq+nNTIWM>kk(mJU6lE@NYkc2;<9*Nxi!Ul@bB$=^<1e*gO2Uo%?s@bO> -XoQ8{Z(_VHQk7!#yq1;e*VwAP^Ax%#LFSbiuLruR#c$gh_L6P57;<-;#l@xjN>=|qpQh)T~MdT6OOMr -6dhzWuiCfd;B)2zkV5)fhzF3Gixa#X%{BGE%eS+=#!@s}Yi@ECnJ*5C7EBfP*hvXOKI!U`dRF&m_d5R -x9?bH#UUmvGQD@Fb1XQ~}0D@2ce>T&iw+1?)?S3O_A9;aGrqQOMNeH_f_O??wTyhG4`rt$Q0}DR;b`&nO3jWN{m#kai-5kRi%c{S}K|boI48@wyR -(1&paa3}UkRO>wP-T%1-COPd9@cl;6JG#n~<(p7OJYLxv)_Ne*di!Vk%g&sBk3_k$`_;7Xw4Tm1{=`- -WL*FD8ftr6eMZzcOYG1<(*6Oy~lb}Q>|#Z5yDFh?z!9eznpyMVhy;5}}t1yziTcoO~aU>kAm=<*$hK3 -6!J*yT8gOe3eh1RFR-2Z-VKC<|D8!8y8ghtp|HzNXV80UI-I#6O#@$vfAfB^s29ZiRTEvt5Dbj*er^Z -0jn;yW8Et(_4PjJ@-<(+^jzud+nv_LMw5}c$(Xl^+l;!vc;Y;IJMTpVG5jSojh7R_1`A;3nAle<Li{1dso|vOW_W-%cxluT`0qmA*OU|sLM$Zg;s-1 -WrJ1+vHtf=4Nabi74_nK$*nM^&J_E`4G3-mINkX?vHlCG`E3moL~-kBrQFIePsvT*nNbH=4`XB|@4T`yS1G3ZIm$^5QHwp8D{?m^qYx*)A-!74 -wz@hBW=K~`fDY`tHK{%fBxL8Ku%q5G4U{+_Y8TWWvTg}x?U*Ob2>~*!z1}Jq6O8)2858oRB~u>jh4dZ -m?<`xa~%$^TQ3d@als%`K1~%0P5k*fB|znM5M8klPa3;q?+m2AX2Q_r6o;Lr%@E*NBULMls0 -iPMyfQ;uMg~-5j!7`1r-;WqX(h!|B9~da;i@fd}yr2cC2^o@Iru+Z=}?%15*_z!<1N%E0t4Uu%tJ(g% -?7Ev%^gjNPGWChLo8LzjodB{8aLCZNEPf@~NB0|NxsTh1bYKtN(~n(h@lU2Uv-!7N)hJgn(R&xd2 -l>PF4nrt;X)k_&i?n)<9{!Xe|mKM-}CcN2V?votaMHDQ@3RKH^PVePgVU=`DFkk&^H~yjN$fPRzLC8( -~Z4hL)%7eMkpa`EYo+`ZcH{ -L8JG?n)~9+WWWkyEmkH@u;e{V{&dcxe|~S|I_7nl3?%e--Wc -HJ1bU9`ELBGz#ha4;EhS)Qot~i6C;I#!-8X;kj|iDJeqJ+?GR0L(K-1J-(y5GzGIW{>5J3si${M5NG1 -dLQaiQTo(N3k`f`zEK67^7f2cIpI2fTl8*g{OtAB90ZH)I>nLBGl(k>qdz425qMjA0yD?ec`7!!9eT&E)^7fW --$xSSs;tw+?g$$0 -RBGQ-KFgCLQuRxS50S*ri0UHcPqtM9N=X!K!0q`K`X(9Dq%*f&|d%6uAe(u|8)#QKLZOZoL>!BthxNi -mXJ>&3hHy<~7CNjAVi{QQfk3E1%V1Rf03pM{I($k}!o-ml{V#WoDtGbdd=?<6G|kX9n`S^)J+k%U$ajrvls9<$$JL>tw>0E6j{DiY -7iGIpiek9wnKTmvw$v|4<_j1OM%X;{USW^?hXoioPwEQMfPN7M^*&>fHCN5bdTZgQlMNVaqs9h*hR1< -V?|Ni{x(})nT2nZ41qc4r(IdG!C}3g}Y@lq^;~sum{V`ZpRDd0e{SD8DM>j<~c59)jBpnKAo?c%}W(n%6a01iWOi -b)^w5*Ip|%jS=-uFc@&WXZ%%(S?RdgKivJ2DiXFiJD~xHOVYX<~>%YgCWV{!JiWen_<%@(L{ia%k;F6 -b7;5f%)co2N02jStMdmCZz+W2aBe8cu}nL&4*0{71cpQ9fy -s&-xc<__F+VX*MO|ID#sFN=@sLv(%mr+C=00MnMMf^vtj78b<+qPo1=9BE;yIhTIRRsL;ZvlxfMEempsyA^9a}Bz|EhJqJjWL<#*F9t3%)^hvhX@;jdX`TwFVRJgYVo@B=nilIx -(!C$nVI*?O|8rwUKud2pzb7P9=3toVld3}|PV#EWY -LZEl=v(+CIuVO*bR^_`;0jcqs@1f1W2a)+vTN8WJA@uK5^2{2h{FE91sFlgaS68gk@z!lm1YQ5POOXI -7VPD(l`h;Rb`1u!0xJ)$NxAZGc{8m_weDxmk;bWM3glB5@)Pn$@$KZ`}K4IggEi963lI{*FSwI*CO%pGS*G;ogcdfV)7jLjncojO0xj%9_ -^&^o>g_Y!j+RxeX#-X=C^$vEoCQ?yCXN~Et{d%t)b3PPeeiB$P}rP&Yww~W8vFMB!&+ALv7_-$q -^R`J|d@d>E1z8Kt#IFkbbK^Gju4^g$x!%P!qdZh{`79a^7V);>N<#4vNr8$IhvWMPDHh4=z|1<3T*Yp -N;aQqA%E$wp$LWAEBZ7jHOyz*v7NzK*R$?B(NbuqaqFtt^QzOk&yAzDs+U1SDG(R>eloXt21d2||bzX -0Yk!1A^Cy$+-tHD{v()`Y?lv}y##oC8hurs&Yv>+Q^p{a7UXifUyqi<^sik$HT0V)@|x_#!AU7<}a6u -$u3_L?swR1nWcveqEG3Mgj**=9lu{tJOC}T`h1CQY8U?s#bWws6lqIt(IDe%dJ=vZK>su_H~b@<4@Qs -tc})IH`TIOt9k>v7geWoadcmksuSzmuyxH`WK?esqpUXQiVstKiT43B7}oijsv)NWW&)8lqc8^K{udG -^MDHwkGPt0?7MCWMf77=TH*_MNec8i9XBBj<^ -!%0=ByrfuC_;P@L~N&5 -C3E}vD;Ity)g}m?0c+Tw2^nect%Tf0YZ>1OG@Llpf;UM#9o=8>FhjhT+=2arY@zEW0-vz+AlUMGH~k} -Bh0{GTE=Y?u!qg-0*~WNsJqG9fC3OlYG<)v!eh -n4iS$dp=JXdr)-2v?%jC)=IWxGHJKx&~W4qvKp_{)sTPnNh8vC>`EOb9@TUg$-wO9geNoS)=iFey4E# ->*+KE}cJTfLIsYD&i-drS@ccRAWQ*-%Q%g0A}og+kLC1WHjPxdF6l%k8{5UbLYwST5)BlXH>2Vl4T`?uF|!-i&_%Zfpfz?A+GKA?zuL#ga-uS#o!+M2P&4@-mE_u@W}3`)P}4 -)w;tNaTJfM=tJimd*b3Ih#YZDl}9gAdvV#!uA?ZIG5WFt{T3Amh2)bs;~*%$IrjrMzD*k6EY>)1dp>{eC#qyi1X}m#>+$4GtSLq~oaAM1X&9>t$76AG%y0o~R9yYs)_aS1wniPNRn;=hA3pilx# -l*_%Adm6fIuO1zaML_{c}v7vOia9)WMiIy3- -&3$Z--QitDh~`HlK-bIM~eEAnvX6G)7@xVSr3`-_R62e2uSXI6Ir1UL7v&5oo94;jTfCC`Sy$!k8fyq$H1Er_wt}Hy@Gvc=6 -^OeF7U5?amP-vZFPEg;cahNmV7*IzMR~=e97X-JXVPJodC!ph_V`y%()eT3$oHWK(sGi|3X0f0UaYIK -o&yILrg+95MeBVjzS0Cjj0+BvL>#T0Vm^USR@d>y7v|I|pYCImKhhV7>j)k}369@<%_tXx-??3ODN2#0bKeiy~%#0-2Sf;lI4X8n^T$edVkb{-Db@M^u@VwP5s3qYWM-x(0wyUh$SZSbi3yF?t}jG}j}hG2^i@oMA}5}ZBdOZJXA1;BNICAY_ -sc<%#vh|=FlG-K&=L9rIR>9g~tprX9LFURg#XFVhXv^!q_{sNt)m|Z@Yx^5hSS_M+tvTie6c)Uh74K? -zFpdx+5msf;l04EY=I%a0PJvkH;?_r9GGFaN-pZH)>vRmNV1q!Nmhx%1F`GCAIo16Mxlwba54DdL%I0j!v+3-KIjd@rD%}oYZG%9E|{dELP{E!z^`2;d|e{`}6T1LJc-M>-HpG4_bEegZ5zMKP+liDCH^4%~7q${U$*x9pNJV`{kw0GS)*PkF -w|J_1df`RRkJS%}e92XGkXdw!K2n1++e41HZ25R5xLFoLCp6&_*fx9L!XKjI-`Hn4c;3JtWQ%xa(oZ8 -PR<|I(u{;z<|$fi7D!ny0isuPh4;QsGRtdXIAms8h!rCdA#h=Y6|g9uIBN35aU-n{BOCLQS$dYS4v?< -YGL=-_*9PeO+UvVMsE!AU=y(QL}z5m9Ag^3>I$P#l5x7CoZ*g|LM>Q~b?QJ&b)at5$d7iSfBMe&Z1C97LP7%rda%M)$!QvbLf@(%M2N*qEzNv -JcIJ2;1Cb)puJ?<01?XR@xQ(Ba%U`u(^7o~d!Pxmg1c_U~wxQ#lr8jcK^Rk|^>&GC!y;=?euJQJ&1iF -#1xrt8^W*+6e_1{p+amr23IY=Gnt<+YeBx->r%t=VX`XM;ph_#|W%|&T%i*PS0pD`oU;d!CKOpR-*?z -t_^hV)#QXn}yOvdhJ@X4tX^Tu(STEs}n>7j)DOU)-(5H5i2AP!Q4~5aL7&;x&^9%8*&9hOuTmi^$~~sdr}_F`1WL0a^O|ZKFe5`{RGW7(JZ3Mylu(VP9;(v6jcvVvQlE1r|(i;5 -gJ&b=j-w0>7D{*fNQZoRX&_t%fc;Y74=7P=9XL2TNkgt -t%&AG=uDfBxZnfQoVx7;nY?px*(K?@ZTey#Q}O6F<{a>l3NV~-0%IGXXIFv)wk1HAr2;92fg6ux(i6w -Ae9_Ll^n#0U(6l~q>~vPbjIC*;?WD;@$X?dg*`Y -AI@#g7sB~0`z4jnb0zkZv_3x1TnysVowTN6#4-i5F9LDt8ygiDxWs#YlXW%pqD>#Fn)1r>rPT~r+5Sf -YzsP1JQrzvVApy?*!nE1;12i&9i(bJiu?7Q5_8lr3iMq -M+e|JG{l5w;D{F?*R!UI51lax3gCZEjjG3hTV&O9E~gBCTf(}3>0z!O6WRP*OvfonZLYQy$tm%A -}hg1CS_1EUBdv6)1L|6z^PYKW{}{!+<(>SvCsJ8a&=C`9>J!UyPHdflcc4l12i_;bQ+{gHl0Rfu#tgz -5SnY{|BRU?F{sc;v#h8PW7S$CJ5iwCMG=`<2^$&NUKLVTF$_M&9~Z<^nD20DFt(189b94RhHzP4x#iM -*CPh6loKm$*{Ii`Ygq3!Ps2ZY0kc!JSMf#!qS78RYT0jbkkSbDc6~}6t+S%IS_GJ0EO&J1D8~8q&-^6 -n-8Y-qn0IAni{;EOB(hMDlK-nDHl%L&})y2I0u&?U}H*QC{HquNgXb3%qJXZ -bf;6Mqt3aCtEL8QNx0L<<= -rR2GE7JMT%+N!9qJP?0x^zm?L*mKM`O4j#v4Nazk&H8TQN0pKKG{1T{9EwugcMNxN1Jn10s~cmO5+aD -6p%9m0I>tHq|Lvfm+#PfveSWwl)GaPxU-{ueZn5X5i%*KCQPkfG^Pj&!?Pw4c8bHHIIlP=@x)M{9FDV -22kMvEF@|=JM{El9#+M#a$aEt(AU3xG+kBW}FkuL{K!8W<7ra&%?v5==0wnOCBu=^YUze0D0~`5#X?q -5(idHf92s!9LY1Wmx-UujlKeBg$*TK-|9aN#xgj`6X*l;mQ(QOgcN0W$%b+cv-lyBA4CUbGM_9|F!KG -8nmmAZhOC3LW&YPN0^CN~nvRlTqYWiX#6S^lfqO>JKiX;ZL~5=IwGH6_rpx*UmEMNMJb3pyT+nQ5Qf`P&w%W>Q~zApUvbi5eqdml4wxV9u*Z%Oz^gwmX23E*ZB+ua7P*{wEiK~@h{ZmJzTdOEWTE4SDmVp0x1SD_LHDUqnD7*nFMc8Z;5t9G!bhReh;cFQB65*Cp-r9Jt4^f -kzBHGW!>0wq0SgrB@3k78FkQMyl4z`l0lN*Yp^;n30d5N?omYfipMknav%bac2{Clfge?;YrwPE)HMH -(hb^p0oLUL^Ng|YI7MfmR5gx(vB0v!>bT1=`uJ`}pU1sG`fojh=zK~RV?Cd0aQ(1dB^Jk;Yj`?Q%|^D3cE+Bcj@E{$83#0ep3 -Qr@&25w+ns04%?eX^!`Z6aV_h~~0=B35w6PLWM;ghF+wt9AO_tn>PXSO`AH(Qw%z4tn9b_O4wM4>Rz;n_qW-*wUXg_O=cp(Yp!h3SgPPC7LyR(^KoFPVb6sIYyw!FM{2@F0 -n-SWCi&!n$DVH(l0n1)-O2)}-cX?Zp5_M*VG;$`!o(50IjJ7T=Z;B@{v03vzlA)e8HJ4olf$Q}3+n4{ -AefH?lAJEL6JtK%?)Y8mu=h^V{k*d2|5x;IwZSVb%+C2NV)%x8!h2nw&RfnVO79=RV(ny9P%h|HJskk -a`o;^jemirYeJ)vBl5P -=olp={xLF&&*~qB*5Pt*8LBW1(;3Qj!6osM0u|ICnf@Y%tyjYCD$K=7mUy#1r<~}Dsby8M9OI|Y`R?O -j{oRqYD9+9y+yP!fi^zB0#a2vEkvfxUZv^Fi};3$=|gAJ!`f~lY}yFo_@PINxO61RKc9~^x1`uVqCza -T=8Yl*$};SaZ;4u8nuzoQ5LbU4a6RSRJ2&7WSseUbeK{_*lJuU@}-@$Bi_7vlq+p$pwZecLIZ -|Hk3wYc?>gOEEJe1_HS#=9u!dLM0fpB4k%%?qOg2o>h~I&J@p8domnJDSQ4EZMarI5@bR&;+-2B7*{jtL<^EG94MarU5CUjngw@ZkCuf9q8S(>Gr7cB!!Z7{(~f)Xq<%+Vm9BVkDMPP~|D;7ysoEg -e$nG>=FS!E!ME1o04yGO8Jg@k+~F7D(5+{v8YAKbzGEvu?r~I0ZG>Wa=DAEGvUwa^`$2IZttqrL>S;J -HhtEYTadB}Q0IwgR(Ugw_o;(6+?eKQbIa(X2!+DU<&~jyn`EY6;eN7H5vuovYT(Kd@~1b}nG}suGCp} -@0*YGd6RgLJkFXx^IO8RH6s5AXtD(k?Z9UVvl=CfI_To!7HV3(z<2U -3IR9U}2VVxmyAWIMz6O`LhJ;x9zl}nA`+xoh0ZtEBL$xw=CX~!d1BdZ7;q7Hc(0v+pL%8fWZ(6_{+`1 -5NwoQ&XjSSm^vqeBh#tNw&z7*n2oUvf|+)f0j%bjlx2Gt7W<$*Noa0&mW?37|=8IF$bAEesVW-Lgtph -H#69d!TXIYt)w9R#;zk5%_{S=jtr-l!}2!_r`FF<|K@pl640Fsv5*j&2qx+ZzSIp;Q?niMMq!A#Ub%L*>84P}CADFx;1IOd -#pZ;+ay*o*?Fu^KfDVAiSEhBRKXyojxHtIGqN^cRJ;M+H-ip!Xw$)1)I}E9gCSPNrFX8VRm^4tI=^wj -;d5iiAm2MWH;NE(%yiWGN7y_vJA{C>d*!mC%HLHP{gn{BWE@G!-WWbk7jHTGgQtyu%4Nm9Gm7r5d9w8Dn5FHz4s;zHW%@F~go-3ZukS -5GQEL^U_*2id;FTI=nzY{4RywEuMtznmciM3Av58`ZZ3crcS1h7coD00q8f%CD9OSGDSz@~~hi+8`Vq -`xC*k|$vb%rJlj5BUjckdxyM6`qK3V#PFhSsR=>Fi${ZJ<&3DBRg5T2b-)rz8Y)^I4+MNgpGv>72uj$ -@QCtVfr!{U1EnVYEtZ8CmR@Q9|>`isbHlA;v~W7zQrHZuvhiAyvJ^v8>)4b7r6p970ZH#yUpV};eY%5 -{bdq0EyTy+-M)jPOM6Do&+R!763{*U-JEg&z`Ly708=)(nSU`^pos&M$!V~LAc-!xJnhKWQpbk|friH -IdbzAFcu+a~A*WPzn52O~KdbcOgBD5{|)9vY!YUE$$iFb=~8wOgI9#(}z!;DEwJAr>5j#u} -@I4IwEqmzxSh2qfP{U_!9|kp@<2J$qQg$`A&g7AvYkNx&*@*7a$#hbt=i_fCXTlcHNHp}_Vi!>GNH^gHnU_x7unG>j2%LWDC>g8`OhHT96bs^$b_}A -3rjvZ{oCZ5r~mWyo3Sfbk@(ugMxm-11sxAS}kDYwWN%0rcrTSLr3&3H<^FYz=r4>s? -$Z$#(;|0vJ#=9L>EKm=xwF1PX;(#iHM?2$eAy+3d>^|W$B72+Bqm;7m%H$3k{t=o|0vamr!Yv_2B{{B -H&KJ2R3y&6*NT8Ihgv597b*C3=`B}um-i%DmQS<)!L9V~08;sJkWTtRU4Go4zS;xn;|BHBo{M&8pi4w7m~y%#sImS -jX}TPA7^lseSgJ8pd|Bw;Yc^-?5eJ2gvv!GqtW-F{l(P~WmOag2mk)}k337k=VA$m3oArjqp@OREiUf -J%Snd9mA^Lb)n7b{rdmAW~!N@|dAY6Xz#US}RfMxT3$&Y{-2Z+g2`mpGv}JZyR -rZxh)6FWp;@g1WBnuwpIfNmytzMv#Od8CX>)IEUMEOo3yP|s>0;nEtGuj$cNs7jF!*w`riz#RtkRxKi -i}EOE&yjp#L(WsF#Zp2PH}cG_c8zhnIT|laSp_N?LjLw`NQ9jB*1v^Em(p5?WC_@tgvZI4UlzX7-Zvs -#UY?5_8vYTxkc6V8Q_rfrXW?vq$-(94?`d%WYv{*wtyyrBrXK%QbCI<_zkAk0X!Xir+Sv`s-*a2z^;dYWIz8B02{*4mtJED6k8=Z=DH!9`I(Ks3Ac@H*yC4k#V5R8O -jtso?p-}9jdTGOpQnUjDvc0nfhw{x#ZqX0NjdPITvc1qof%aZ3QA#MPXk^5wQBMp -M3qkS~oR$h29kFYf2AM&qddlkvBxW3&6N<$wG;OwQh_6Nbia(42`!E+0sRy$Z8LmUc~ocm?VF2!XHL^ -O0EF&8*;&=_oMXZq-o@#DWsIH(T%gHmhHr~mtSyAy2rX=^Odw7@;}`}$k$|6qUsWh74B)?AzpSk3DW? -tLR9R|t{f@d=#Qa$Y_z8KGgwO#Y-iRF!}g?t9d_8k>G_ReCc^^|(TVv@*UqCxw}CoyXh}XsI{vfkn=5 -0Z6+j2x(7@EC{EkyB+^^sx8DAUrlmeSSKP1bH_S -3+nOWkD%QK3O~ORoaT~3GMD>xr(b(?q>=*MSptY{xuO7{?4HjH=%V1_SsdQy663NgbrhM$*d!%)J>K3 -8f8sw%ZX{Cp9mIT&=_l1p!R{;*3H$r*CYt43cy4BNtSDp?xPgkinTRRy8 -j{M%|nbI>I`KDJD^`xAd8+pZ3-ywQdug`ct+NXD`_lY)crGVc_}J -{&;n|iM5`B1n7_T)hj*Xc~8v90})kJSno(TRCqW$v{m0BB4V6m-HV{}%{61G`QJt0lpwBu)hn#{br -vm=sNS$Xe6k3X^k%Bm}UOlp#Pe5nY{Tc>8kgmcaN-JM_Gbo~Dc=n|(;9X7!jues8fe(V-?`q&p*?oC< -@uxq?dNl54`!(;J9XtRRTCB}g-dd5X{}w2+>C}i%(_*TXn+pAxga1ShFf -;e3zV@^b>$Y7yHPi1^Tg2-ET!6U%QhE@+OX*qM?9U{ZL*5q5Kq`YmnLCkS908R{J)o6$Xo2*;!EiG2Fux4NfLiv -Ew5F4?Q2d#9C#&y(!TOJglRXqB2FUhIK--NXp`Z&}jkOC)%<Xg{P>KyLTv8kW#vG*=H4t~jgFx-!IM+o)0n56{e-_Js`du}?4im7>pccP>XRXPDGN -7?U0rkr^ha{(B8?cX=$7iXsiQ`9vQ>zKUEI6Z1nxiheMiEM_fcX}>4U -K{38g!qbp4f9T9lw&!A|pyaa(E;=Cu&s601PXzRb{}j(uFs$#T$c$@i&yS0$}7HF9;jCzz8L_XF4r%= -Vi!$@J+-@TW^T(u;!vDw^hSdX#|FA*ToYE(^ixM@}s$_G7O2HIzDW(orY -M;1)G|2E_T^>L9QRDT6Jv75yw~+c@0UdoNVmZcDAMkjUx66$i3U(ACdy^fywdn_Lb*X*t{-8abajy2% -o$UF_vXLbNQ}7-kbIg5w-5?g*p7>b@RhR;eC~xP8PbT$ICaYxGSZ_MDE%Ay7Y5fQye?O6*MfdLzv>bZ -7~PA!1vkP@@#Dt6j>Ysoh?*n`*NquTH9GvIB}%)R(XsKq+pU^_+6}#1&UwrHW}|Rd4|ePGs?%% -^;u@stYC15{w_A-1V(>kKnX0q0XJ3(fRO;7=AWT>N6Nede=S&De>PQ(vtSMqZ!Yut{(GjUb2P-|8=BL -eYmhRUI~|x%TjiE`LbR#R?i$Lody0qLTAG2!7i&Z=NOq&I!u6zd1hN+pwU1LjQ(mz(#)({?aXdu5_etRRFgw{-rT}s0fD-~wX%D_^9@$O&c3*2UvTpMXg -4JbVp1aMQZn|0t3!m(u3zYlff4{lVyF*yRQI_jXOs2P-{+o+mz!(5KXlT!OC6UW`1L+mti!#3Tr4&Bv -9aUK7hi>?-YuhGWE(U4&5L(WpFe%~lqt`P|MlX3#R(FJXFm+b=a|f9_`^JZKzVS6XX776=cB{kv#)^! -$Pzg9UOI2ZJR=c2iz9n!&-XfuD)9Km*{4)BwGlgB7W@2%!yeI9@usMjtbkqTG#3omroYj0sxXrmS2X; --HSBTI`#*6o{0$NE#+x2JFXqk5`=;{LiGP&d$z%vGPgmig}d1bbiZpzhk -K#u|ZK`*~*p_=3<6F8p~ve--JhSg$xH`!fB83WDiKIbao!^*N^Rf|HY9p4T-~IOHOUhS#Mn={D)+O*g -C0lk~D<5xXtnx@>i}7u*07@JJ8?>6YF0RTrU2w*AW2lL&YcwqKH7ae=2LV^hP6(_>lPx$3*LDFo39Zg -!4j#72~*mYnPPNf`R* --*7SQCn;t~Tlt#XPm+NMcinC{2_#@ap;_n -PP(J|ddjLx_M`_V~N~&usIc8cC&D2h$w02P#6zf>dyRlG>ee`ILc2qzRkXU_5Ag-Z@+o*>fMXy88FFzef|7PqDEa8jnVJ+vaIYPWj^yBQZmHE;9~BrXYP`r%> -a)|&(o5V$3>bHQL0yS=Mj0?)-KzO@>fB^>)zBfu4$Y&rt1mAf!*PYy%FB@73JPTJ}#IbImzV6JqrQBK -h?*EQ^li?j&cw}>B{wa2s}#AO`Nx562`VoykJN-o?&W%RAP-zY7AcE^)m6)4J~}ph@x3=+KlWZN|5m^N|3Q9?L#uVNH+^=t-HF42K2j)J%n$_GP#H|%Mvp&7*;7osidsXde1$6G+hRwNRulnELCTcFhXYEuw3v -^VZWaa_NnGrwm}9gwyE$#RG8x+T)H6G40hKlqZpYbQHU8OvqZs;PHrq-{@uUXP;d!o$FY9V1MV=n_&+ -1}O`2P9iyTm-5P(B?4Djs?P+#km-`$Nc+rBZ@wDm&i33G-w_y7@YZ?8K8uJf5_ei{ucvSvOSp$8kN)a -(+yjpG=^I92nrJ01_*bv`V;}(sNA6Cl0bf?l*a@^9TwKbIBg` -j;Vg`ytk;N3HpPCs|C0N$h1K*5LQ8xqr+&)KqQLl!I(aWry~uaBdLPc58(!o>;LLmWU~GXpX>gn8Xu1C+zivzo)(&ACF0#pqqA80Aesbdw~+)bSiB?93<(3M1d77z1d~*hu~(K4`FzCCQr^K -pDHeE$snaS1*wSz9B7u?U^ztfX6gw!RzQTZ;|6OA(yKi4f)bTMt#=$s${${o3v~_<{R5AWpz)|FGyZN -s*~9S&b5__w+S{eTlPHd(A<@)jFi|KnTd!H{OMcvj`~-<<#;kmJ7Uvb}$a070aT95#!=hq)G@O06a%z4==+`*nT5T -bZrC32hi$rxTDbY9_<@TS^IrA2;h_U14E9A|xOi5e2nsD*-^DCJq!WkN7wP7^GNvYCMc(aUIknH -l|_aiPd(4-f|ZpFs8bciNKVPvbRVUXMApg3^H65kc`7)m$wWR4g%G0|SBvED9A0Hq&G9Soo?>m@LYY-?>6lK(=ra^#+h=Ec}UkWzJHvZOL -Ua4Nj;+u!5@Rm7qZU;z|>)BF%J4Uq3y4X>$V5vvap?xHRHZJX`~Uj7n%#t982lY{&6u6Vo-<6WY9T9(Ea~=goD>S{Qe}H*p8$HPZ27Assl*zome5Lrl -x#8&9+_E$$A+R#(%}FvimLb_;c$6m?h2r~9qU-I7+-d1K@)bUz8p=^Vz=YF*Gf1Qr}_;;zvNgq@oF*jZq^lWj$B585=RLMvuV7_**i+w3#UO=U#&xXS|Bwql@W>lg`O -r>g6T?Z?FQJwij*ZG5@Hwb7|@Qdj<@BA=GvKYT9KHyKn;=vIA5@l^U4{GviKxvJ*Xg$P3gEznwmzaU1 -6-sdgO59DsQY4Wc(e7qNxy8z4=XQqE*!4&>wSUv75SeXha?GNZK+MiH<`;&-EwaAUsGUa4HuC0OA|ou ->U7Bs>p-B)J17@!>5e#NRUr*rHTF|(XB_22=2g>tss<_;oI=&g{b1hZ=1QY-O00;p4c~(q<0RR9p0ssIf0001RX>c!Jc4cm4Z* -nhiYiD0_Wpi(Ja${w4FK~G?F=KCSaA9;VaCucxO>e?5487-9c;Z4O)~@S@5P~C`Y{oGc3`DEw$`8j@6dlw=&Ry1T7f_0aITUKfpTn(OlTAt7v7vKWYSt(_32W72~Xee-5I%oRw)&<;r#j?)XR=OjIX=PmjOU+Yx}BBi-|xshlG*2U{|kQTwyerb4G4%?@z)$ix}=f>( -HXkwIoM-Z@T!CBb4T6Aec6~z`dc+e-3d5w)!LG27mhh;Jtq)RfPg0^N5=T7|*2OSWMsMx6OzCx1JgjD$A1 -ONa#G5`Q10001RX>c!Jc4cm4Z*nhiY+-a}Z*py9X>xNfUtei%X>?y-E^vA6no)1tHV}Z{^(zSV#RgOb -*xrmffSYt%u`MoI2g48qDlO4AF-a6iDyjedjv^^jmhCQSQ*(g)Lh*Du9?9=c@=0MB2Dg&tR8k_)igA< -?Nq9j^TCNeUs+^`+QdYhe6-nuerYNIa#OMpTN=z;;)>LllWt_6&qRO!ZGlkOXbS|xROml&7nFY -1LYZ3<`xIl}Fafx)3)1?(KVUNUC1S`%8RAIRR4Wo-bKv$oT+e-OtwjvR_Euk9(bk$Xy1PFePxrBU?q!gemtmQu_ED+8SdW9;E*9D%SlA)A65FThxVqDSIKeGhaumrQWS -4IJJl})RZBu0=Vk<+2&iX}-91Q}VOL=c(S1x;W@lQhRdjK=8oWizR}y`k>)WMGO+#B12q@?jLtZmvJh --(M%!v&AeNqv;fd7a5i~R6BA=@#B2Hu!^t;k`$q~t9}K`>Kshno<75>HL!&Ly%{fs!RGsS-Mqib2% -rX&gJCro`NmdAuJ^ywZRhWqePE6(#=?pJvJs~%}Zk$cyg_R#p7t9R}*wqb52T`ZxK!n!5qJ7KA5?K{fKI@fQ@7^OH7g?}X%P?l-I?kyXL9+%P`Tv -8*&AeIA7-MDKpVuqpRp_ev98r;KxkAp@=_XvtU(XBsupwfOSO{7h5>FsNv} -x1+Jqy!;&RS+mo#_H_ThS=73FHrt6e7Htapx;>-0XRTeM#+vCp!u6m;OgVa+`A?kh7!0LH4r%mERb3B~s3q}mMYmz}7zhVyy3dIB~w -e_4sQ|Zgwr@sS}>4|NcJ2IW#DR+PS6Y5F3PTmLq0#Hi>1QY-O00;p4c~(c!Jc4cm4Z*nhiY+-a}Z*py9X>xNfUteuuX>MO%E^v9plwV83Fcih#^C>QQSqtsZ*FgnQd{c+M3L!4 -(7HIyEq{mP(eljAKC`#nc$H#%+q2dZmg-*b}sYHPR`U2d7P__o#%z!v|5@NW)sPv>uP+iv(x+G5Maz -coZHE6C(mP2_1Pu9p)vUBH{;acL7>Er&^Ir<~>HtwwN3wKsKZuXRw(uuy_3~;6V*+Pd2g*P?(0;9B^g~xauHdkhcs9{z -B7F;;njcTN-^`5Mw($(`|3izle*CX^FU5$jfxkCW{1q^5Xz#BccoWV@OjM13hi(XCP~k;jth+;u(Y{% -Kp~x3dO9KQH000080Q-4XQ|<{Z(mn(L0GbZ~03!eZ0B~t=FJE?LZe(wAFK}#ObY^dIZDeV3b1z|TWO8 -q5WG--d#aGXBqc#-2^H;nf7d&+#NoV%pNj#f6PN#?6WH#GFXS)Lm2wNMFs3fv+r+<8(guo`YiPO!tK7 -c^)_xC;V%*skg4MKUWSxMTi)Jl1|6eZ*}Pqh$*0=HAhI!;Ntq+TNsl8Uu^HwDqTkmV(l>f+~_=Xq&Cl -!6PMNx`z<$^K~K0seg7xA!Yi6ymD_y`-?HSw?tDA+b)DR8lxwYF(*G6p_YUs5D9M>0`Pid_luhlo5$e -Pu`sTbUIDq5Z;k{s-RXBL~e{)Ckd%4PD->^xnMF3#v~Cwi7s@K(*)3Aqx?XnVuBx_>?Eg2*yU&!Z!0M -(D)q`fWi&Sd$~YsM#Aqx~w8%&B;}n#ZO?jO9L{eQ#J^>>NC`u6*xdP2-23pvv8B=4R;Ua`2iHu-mUPW -i-%Cc#6R$;}+g4(>IoE20>XBSoV-sYLKNSI&a4oo~@jHRGFGq>2N##oTpWf;T`jyM-ZMrAM>gKsVSqk -SnWrs+4Ntd>M#(swJHuo{ChfD#2suaAsdDldq& -V?Y8d2QKidS22#2#gsFk1%4!w1WhKm&w=T2=Meri)RDEfGmDDoC7f>(xs9(A!7%N%i*vFOP^T|cXjb7 -D!FXU+$O?3doOl9<*&F`;h_o&#*XpG(bczFDbQM&%jn#^1Sr?}8(Q$Oy>JJ}j9sk#Xww*ATm#n#F)|laXi8*gC*^Kx`gEcLZzINa0P|in{?4u5a5UwO -p+uK7uP8yR>;}Fj`REB!Fw@7VuPs2S%V;ec`NDovIx#?W=Z(CAAjK*ot)ZFc6NGv>~%Ub!8LuQc!;rInc)fN$Y5liZt!Et?Yrjr;!cU8*7ODw3>qA81g4dS -xxMwEJTfTfgyHy}Gmcf@T|r!h3nA_qUo`c?$<|aGhkpzECYv-xl+Q6}GiO&O8tFHL4b1g#OWQxPA4X9 -S;-wt`yD}q?)&-KkHQ#3(2I|~|CDG~9(rwh2S(gn%vBXqW!F1ra{yrUl-cq=el-zf3O(*t^O2UwE*SB -*ig$(=|;ih|SxptALshdm9vA>DwF#c~JV0$}ZeYQT^4oC0{lD<8z82<11$}sj#w)Z;b|D*k!KQJk{$! -oVIJaaZ=HaOJ#M(yaE`J-VEDZ2jIFcV(>`e8o%c>Uq``0{%8+q?PukGC&xr(Oi;+#md+xqp0)BRyTDN -*t4-h-0+!sR7=B>W{t8-ak-F0|XQR000O8`*~JVYCo`j`vd?0Iuif@9{>OVaA|NaUv_0~WN&gWaBN|8 -W^ZzBWNC79FJW+LE^v9RSMP7zHW2;pzv7U7u?JUfv7zgPG+5eVz<^>4&X;lJSClG$S{P<~(b7M72Y{_k|Qje*a%?q(^aaTC?3%0 -3*+_b;{U0I7p2PcD4Dbpn%{C>AK`K~lCX;*u_&IR)7h$<(#e -+SG!m}rnNm4Ll;HqFYQ@Z>tw831a_Dt{X!qZi)`M_SXD_@^0-st7Q@}a2lz`#z5`=5s;gr!6rtuG17{)8+do)g_}$t5?x5z6f@sA`)Gvm$ -QU$KM^ZMnoF07z?eGH&beHSJyU+X%o22F>$(&v-r%yM8L`F3B>CYp4}xMG@>y;@&6hgF!Zq64s2trqC -#`_N7r2Mn`)SSO8xh1ouAAdv%h^L=P1mIH>_05;T#N+t@~qOR=-Ud3>ob8r)t(eH&w7+rC+x7Q2ccl+ -_dP{Z>qju8!EEB;Fb7LHigmfCEs(#Exd&4t+s4%gU-2`h?b4#EPi(ot$FX1lrE8AGuUsR -kDd|Ed~F)}WnW+pSiv;8rUFAr-bRIG%q*vv8l7br>ClUP`zx -rHtBs2VN8sev}u;HvxG7XeZYXPdrZ97;QparYYKrj&4~YY9T8oPr{QqKV+ojE+y|KEPJhMd}RC=}dc9 -tMih5Q~Dx0$LhrI)v5qxauu|%ylcZX!S0Xx+4G -L)1fgR*v89?v_N7G~X!$dN7~E7*pT2%PB_L;VCC=8?j8R@C&!>>riB$LMQG?Z0F=2|Y7)PfhrV9>Ou8 -3C-a31%3Aq7N@5{G9<{9fVXesyIHqRZx#a|?Kk$DBhO$u#Laea?IG=BV`uS^g93K;;5P8hDsn#ZO$q7 -wzPZB`{RuO#%kNnJ71y@$vj=Zlt#Z^9OS=Ch-oFdFFAv^c>;uQ}pl_|y-XESA+@+o3sK~JQU`EJzlkkMmzR)&=_}Z=;?G0c+jJ4cI@s^HbY+;% -P084i30T&bX=lmkb1cp -1q(6Z^b*LD(Spgcm6U=wi8^*s;Um{n3J140Kuu0#cdN(QVQTyko1p{hAk`tjo|2!fPTtzkjaFRRhs+w -mj-`w7F)Kl`_kF@>oSilGKO0H#E!+yo#bDr#6=fZk|>6@e$gx%j0$rf673>lDG$N*<-;2$@Km3TtD_M -D6=Ll*ULsiJ}bG$la7yfrXM(R1s${imFXSRa&w&Mmy!v6XUp`H7(GMMKfW$Nz+j7->f{Rr4ogrRb^Rz -vV<`5+P}?TMg2QMN>lM^5))5wL4R81jYbf;7nWFlHLVaHWap8!NIT!jo~5KiP7|PY=4VURsI(IpdLHrEXph8Y;)#kr~O0ZBn1N&Z -C7T>vgUmU`P^)kwFxqPtFAbk{LWl78{HPumO}8xmV6)|q?6t|Yd0#9h@$Xdq(uvD0%Lc7KEjgo)c@m%89$$w3~H{d|>h -YYwn=r@y|3z()Q4i)IIm&H4CCAUhpZFVA4uUOk-eyC!PJ*il`6Ps#Cb`Hy<>UK~kX>u`i-C5*ip*)M? -R*|??=K|^fef7|@wZ{)rJ@4TM1s@6aWAK2mt$eR#V_P9{#%q008+K001BW003}la4%nWWo~3|axZXfVRUA1a&2 -U3a&s?rZfSTfaCyyH+iu%95PkPo5K4f`fFtd`dfQ;rBy9t1;vz|b1-ypDSd`6;ED9u@3ySvJcZSrBzN -B*21)7J%7H5WYIdjBe_@WR}6QO$Cep1h>mrAi9Q<0~9R#2&!B<13%dG^^nvr+}s^NinB0-xclUC_@3& -u7*1QK7lWY1Xrg0WEl~l2M%sxj5reoxDHo^>~U-1V#BgP?}1u9=V?TUdHp~lh+>-azF}6XA3$cxgd9v -=F>SmgU_NM(>a^o4~WRKXQBluGDa06dd|=*W|tuV0zbwbp($9Tr9K1ZhpteQZf8O{<5C;dcA2zl_Fnq{^YO3-nW)L&oh5VxseKu>VP~ll8P -)7j~Iy&i2pwONZvzi376!iU{msTu63b?VW{96Px+K$-c^uF#uBFPwI~81XW^YyIzln$%Mp0Qln&;4ks -ZfTKyD#xxIidwt<1jSfl0{|tS{)XsTPtmMX*GKE@R4?VNY}cwNM07^hl_*C4mIfW -ro7plBoggBl4NziCxXvbDB=KI*n_DR~?RHH^%MKMN@eCiQa-Xvm6;5~^tg#(c%H)L>g_*{bb!o<1a<0~LvwKeH*^?Q25k-q3uXa{gTf&|IXXMJJ&8 -VzF3&e9Y;??I^&K>DPy!=>UiCf4m^T2GeysxZk_&A#fm$Rl{C*R(YtzWEr-J4QM(2BOSS_PAKLj%wMB -qEi6Z{il3=ybDWf(r1>~AZJ;+p;cvT)O3;9o-So=sU4@geHh;*fCF~HDOH%!M}prPBxNT+u5 -+JJx1-?&{nG>(^}!KBzro#`f%}aZ#3I7w_l>QQT-f_-AhsS&9hBzfIxmJo@II(J|!CwwhMQbP(ila4| -%o>z7l(072EFoxUJtQj2Am23zfQdJIZ!w_V1xh`pN01^Zj*uZEerZjzR6c*G)lSeAGJ^#3=1{&_#`|9 -DH+oa=X8xG}eB~Q5OUIIi<QNhk}4bq|}^n{Zishyl#iL^K6z& ->o4ZGK083EVC(h7;@Zn|o0u+f{#83q36)|uV4qduTM+5^0TD(3$jEvKydI?@OAtur-8DM>a(RArdxKo -<0@Sqv?0+S4=4NnpqmAciX9v>;733XTB@P^u?rvLl+|VF(g1i#uttvVeUM#1yw@14cvsCL+$`E8jd=e -h|JRwKRepjBl{&IDDae0n8-3L*%%)Nfxse}1iUCVvj^77up99|DSrYfJE0R@6~2XK3Sa!-T-8R1E%^v -fK0fN0nPiu1s7j5}`5i_Osv${5GDa>?Sjom(e7V=O{r5mm<1h^J9)%(xvmR|=gD#a%-#Ps1xgSK#|K0 -&mXw?V+}d{-9~BX3}+&QL+A(+4X>2tek5xtbxgpO-J?I2FH@XWxcww-!-4t$LaBJYm+Lv>DMZF8rKZXaB`Y!sqWVG5Jb7zj0j?7qtzbZ7OJYxa5VT@E4(W^JlkNlp8HzL)KcpC#J@1xpMSXI3 -7vW3ajcc{5Q?*}VGHqeUeSxGWbtcJv4XjlHmP5SMd4D+9lcg{aO{1(%j>uzYIe2XbOhCm`2U91A*BuP -k0?-}4{iWw(!MiX;-5zcP#7GWxXqanKsm?U|=LWw97zy?W#oSD!3)Eh&dC;#MM?Z##i39{ujWCDtA=+ -@kKppmf$YTm^4vqTfVk9(`^?x>By-lU}MH{P3u)ep!g;M@#{QW$&r1E4~{E6YH4-6E7mG6-|ZeDAP4& -2D`0di@s;O^(;SD56;wMR+%4;f$#92E9q|KTt~p1QY-O00;p4c~(<{`37n|0000`0000Z0001RX>c!J -c4cm4Z*nhiY+-a}Z*py9X>xNfc4cyNX>V>WaCuWwQc?&@Eh^5;&r`_EOUp0HO)LSim6VjYxZ>l>AX4% -13bqPLMtUZC21-bxAPrzC4I>=|6CDKuO)daXO9KQH000080Q-4XQ-&KjFir;m01z1f03!eZ0B~t=FJE -?LZe(wAFK}#ObY^dIZDeV3b1!#kZe(wFb1rasy;*y2+cpsY-=BhTH%u-(HF4Tu^_Fa@ldi>_$KrI|5C -<|NQ#Kb_)JQ6cSM0m*j-+JCZ*RqdppnFT-|vn`mQPY4H3{`JWva&Qn^3h#iV2CbB-BF0inxVXWfTU`84G@(Pd0^B;@3TOLhFFQ>)d&m?}j+@?0Jk|{NkE -s+XlX`02ASD<|8DJsJKJYMrs+~8Z{K%MzwGyq)AR&H^!r^A(zxIMq6n{j#xxBE#7l%GE(Q%E9buT){Gtxp-O7$2d3FYIHpFnc(!5c9hJn|%nL_B2DGa4H+Yix+ -EwgAj#$uLN%)XGEdMy*I*brI>CMrRxI*CfxI#9<(KpmE09MTfY7^;@v)TqJBU-yn`o6fED|7TVK1@GVKB$av1#~cw&$M8Mf?=Rn@JyW?NMEs-jk7Dxs|JrlzEot!XH{PP^Z~lwt(BZN)7wv~pE#S -qiS6Iu~bW7w1Jm?OGcv+8oqb#7A3(!V<6Ta0>Svs>qb#;^28q;%4{IG6 -ptFMQ;(o}EAN|6jh`@aL-5XE8x$`_#nEdRz7dFNDhnfxkz-IkY|8DX6LLieZV=ZBD6B{Et`3kh0+PQsh6@>zr~OhfX!Q_^W0fTKFzXL -o%GwJonaF`EO4S)NPs!~Q8c}y!d!;Fga=?wdla_C^c;o$@pIE-6(x)2%@%K8c;9PxJQ6~Ja0RKSd~@R=bd&sN16*h)6yfX?a8u*HJUnSB -d{>oK?jzm!BYj_+uAPc#e13I7ug0#uVbrnFDqe>E{sRheWMg-qe8FTZohMS?B~A3@qEgIPS$%-l~HwV -{yMegNm{5zPTg&}UNbpHF`eIv=jCPS39cEaN7T^`40IBaG1fv-|?9@2<)E -oeVOX=wIzr*gt(rVKUDX!*T&kX8%6OV}1adKigjIXq2bKkYTqIp^jxasttSL;TR*>yEK<%+zAbGJ<&+Hflx2d^Em;= -MWB-u35n%O{SiZLoaNQBhq^H%JjDo$7obBO9Z^NwhyNh?mPsC6NWB=gFUnl{W}NiK6=3En*)(?snsM# -Ms@PGiNxhvxuyzMS5867!~0-v6f>oh->u)63$rGc(rr5r0EA|F!b>2u3Rl*vA%tCI#!+ctGvvH|xwmh -Jn5U4Xn!Gn;2H2alRi-{GqjD*?hAZFXAWqd~h-&eF65fP9DgGBbG?$6q@?Zkwf&hXUU@N?_-N-dn*f4 -Hu{{T=+0|XQR000O8`*~JV65G%(0tWy9t`qFa%FRKFJE72ZfSI1Uo -LQY)mLqAn@AA;?q4xVR2iqPFR66*;neg>9g-k9CI_Tdb(Ii-wY?V%K6ahh|Nfp`UJTf7b03dLF)TCBy -zI=g!zqok)i&qzg(M>y(EIa?_jJRue9kjLUl`tmPh8N4=i>I$d>Qg&6lKegz0=+)-Lrhjh2U9AGNZL% -Ly7=6q-7TK8GgewrkD$v^T)MhBo_b^*-XBKIip#YLtqP>)jD5gg3$|?3&N`U&DN4;j1e>zEN72fn&)9 -ESv;gwo;~xJ#lQGi;Y>) -0;=Zk%1UD1i@;rWEj>6I2TAN*U#r7PPTPfYvHBXcq#Xu0Opw=EA)Uv2-ETtd{j~0n!e}2s*BjXhl#IZxOSa8nmC?vn;tw1 -CQRQ{%E%ua_J+{2;GV0zHmAy=v2Qz5B@e^CYbr0M*3HDev8H27sXAKTcU%ZQc%{OSOxO05P3OTclH_P -mpiHd-Qzu^K0-k6eEEh?*Gd1pYdm~1@-Yn0S6a=%iT<0tZh%Z-JE>XMBT-A)KIi5t%>yFJDEBC&jAR)=)ymEYEu~-~QesT>5k!k|z_rB=i;3|@E|XD?X4IH&45B@gaG`VM`b&dG#-;at&!lGUowf<;e(lv`-} -IWn#m7a)ZoTIT{3QZDQDn9Z=a%B3JM1WRThC?*6TFZ=x=ot-^_kLIHEQ+o!X5v;KdfX>^?LWa2vq`;d -xi^iZW6F$%P6$7jW>y;{tLcImok^QN_W(9)0Eb7fzhMRS8k~Yz1t~9?fVDnPz9#0z1puFe0Pw(KGM -ZtzttM-C)Uemb`boJ^0muZmnGpschkCCzrv}rlhHNwW09&|i;YJr*@6Ci4gXXOs6x{)-JBN!AG)NupE -*oJaSS)(SZyrE=?Eij4kBRBJX;GqV)#uP$3S%{v-B1xG5*fT=(6*>!iBbD419cK^IF8Ne-98-|9jr2x -7zgUxU^m#&62fP7>zzuvjSF*kmPGNq44QP=3hg(JET|9?&0JeKKbfcOaD8u))QWJl~lG%8W*9CyAEVG -|ER5OA4ugeXv*@lt1%B&*HWksJ$Zv~D-45aMqwpP{lZp%1W-jJQmT5a?Op1KMkn+}JJNPL;6|_;EfL!hkjCdJfR$8mErge -ns4_hVE-Uc<6ZPYx*<&kb|L2mYBwwxvLfpx8`bKS@zOS+iN*#X>w<-K8LcPcla8ui(N*fb_=RAC^uD^ -}dWT)Fhv1uy%5P=7w$=h-~RP-s-&5v9g~G5$=Av4p{k5>NXFb_^sxdCx -HvyQKA7l<5$b;*b!2!H4^I(x`Us#_UzIt7mx%W$P_;=%cw49NvO7GL -cFwzompnBthRt65rG_~gOP#(W(LmRzeRHw&1*z< -v3ytDb0|#$VT}k>rI_?19@1~y`=Kn`l{{m1;0|XQR000O8`*~JV?nCll#smNWehUBq8vpFa%FRKFJfVGE^v9ZRY8y2L=?XJS3GsKO0sf7yIfF3E4+!5wUpSwcDAajRpgDw$$(><8G -Cml^}qqa777TET5&8raOB95|FXY?H#2segcKGbBJIA+%=^Cg&71dL4xoM1hFKcYWd0C}JZgU+b~Nn(x -@opzbiT-;Icvf3{RhV|ASs*El*1Xpli*Xz;loSy^`rSRpmArGE+1$7l6CMrbP}BA%KVlVdP>3a~32y33bR=dPze -PB43c>KLWu;dP$IIw!Ti1=5$7+02wu_rB+8-Mgj&jfgp|rk`msL*YZzI@SMK?#BfcpJQ%yqnbk9UaL@ -2?eGd$~C!LImf9kl9(83@!9- -KZ>j7+zCiW|m>HMIPvGI-zeZPW8QNCCD3=9y{x;GGJZ4P7TD3@zhiXhPp{k;$;1f;Tq1mNC>(v}<;K9 -T}=`i0C@DXMQ|EHuQm>F){{1hAW}vQrzm)@I42kx_CElGW5kA`mQsAq2meJF!4+bVfcmsWByRErz6fp -d9F}x*g{a(w;WseV_@GkgbLdn46T7_cTE9xq`&XD=s598fDVRhc_OAW@l}!>Ns27CsW@fgMJ`Z@)L^= -pgpnb6``ZuE(X@!AOPEfhA}<)3PJu47l;uz)6x)VOW115d#9Y(L1HR%Vs8jilSvqHg^KyHZ#wp{~pak5NrPtynig0yoe%tjqAo?lO`d#ZiVk1HkSd~hIF_H*fe9Lbb{ygZVi17YL -_gtd^L?Imbvg%o7M6_Eu}zQ+i5~Hxf7BIiYyFr(&QwLN}UyygwvQLbE%moxpe8MOB{h*WYq=NIHh9^+ -kmvUIkh!pi3Gu%RNmF{P%hVAcy08MS5QLa<&L&Vb%^GV4?Ffo#A~ZJ| -oljDlG@q5ab%m+lP8ZU36=&m6k?L!@-H$pyudkp|$*@Y}q$z^8*0t7M1USx0WGy>94_MB{Z&b5!MU%!xXJm_j6OPo)0#l?zfs$XA_NJlUi^QDlLr4JneX -q=S%6zNLOJ%-M=4)kMC?kmfp<|PE7%XPhB5J3#r>VDFbElhY%BYY@6wO~_Tl)+40PJFUK^UYFNZbsci -f<>Oc!Jc4cm4Z*nhia&KpHWpi^cV{dG4a&sqRJLxJl4EW8Pc>{hvhpXSNAY*e{6}QmH{OYTEVi@NzVD*aAV -j-cESG_j4gjR&N2Iu@Ff+3vI-=FMvyL6saIWCVyzsnNi~N# -6_V4mkp$!d$pp*gYr+ZSj3Z!$JaxlsCJ4Mzxd42suB?%e69SmAAe^KtD0osyGVemo*$bVMIr1eEe+VQ -gdm%`aZRJ!<(v0W^bk%y->Sn2~Ny33vkd&p$q(t`vY$_bp5f;bEl7V$&{KC -mBL&yAmT@sS(*P89W!0{j4+C&!N=nkbsRz8)O#T>HUuU=)PNWc$w$Jvsx|_PW?~=nUd6GHfEu7zBRg{ -5eB5#K6#=+GEv5s8iRsO4Y=UqX4Uhs4bd#vC=RKgDH4Hq}qmAi!f|M;E_`R~xnsv4I4U*J_&5j=IE&T^U9dwpQZ<8F+zu(wq{!ksEvFv5ngw>EdvHP=!qnEhqr(@ -`GmMY@F&`PR+%A};3ikfOpcu6bL^3&?)Hs~MnR-{NIebK;-RT)Xjw$ -Y3;AuzO1&z$7=$(M|GL>Kbn-e(nV-gFZPxhjc{ty|443XUEA_}kETG{>CVZBc!zHc~sK+r4gdW>0OkY -TiK~=zPA0%Qdb?QPW!|FD%R2rjQcZsGyaQ3HFYwU1XM)-pSYS?Ov6_-~P?b$vWoeuXi_h7jqLXRIaYD -S3r=%OrB8m9_KA1wM2jDm~QogW1qJJg0l;8rkIdrt3EwcpZ-_%U*4%tKUAN-Q=k63`Y${6$o~I@r+dQ -nhp}T(7A~X`uNQivE~)I8m!@@CNoOip`>`J`9+}f898(Kjk4eb=F&&rlp(NekRuaAz23c(QyFRn_m0WaX@QfmeD%cam?D?EJK;wsYV)QXlvIyj@ekEWhh5Hj?rFe8 -QLI^BR1ZN(~9(q-5V5o==-GB&{~7F4DGKcL9P2*Mr*z&VQX);jIA}Fgzn5+jSnsRbc5R2*Qox-us-58 -W`7d7Jq>W+o5SEFcz0YNx;-sUW4EWtFHlPZ1QY-O00;p4c~(0{{R`1^@sb0001RX>c!Jc4cm -4Z*nhia&KpHWpi^cV{dhCbY*fbaCyB{O^@3)5WVYH46-O}2U?@aDF~XPT$#2B$dW6{8w5dMXf4WSBa; -F}?RJm-N&@5%6ljzGmtT@0Wv`nSC~_#OgGU^`_vX!5e?}-Xve;-d`^L<)BGvPC@>DoEWKnCI)QtH6eSiki{_Xh7c6G^Ghc -kl@`Q3eMhQq#pqDhsfzO@HUwmCqh#9$vDNNH0l}Pdo_>xA9#37o_Xq1PjaC}2XlqQ^hzd|*{z=SNw%- -P61^{{kZ%}LHvteRnOd==-ehiR5BAWpfn4J%Hu1j&gbMy%H$ -_l-6dIwkSgh;=QkFh$+=xbbnDsY)u`3SnvV)`+$Zf?!h@ZHou1!;jSrkC4&h0PrbA1zl4XRt#HFSBI> -7_Q)=_0-k`|7$28k`Q;s|mdZf}gYgeqs^RkkECf7bUU4i{DSjNi7~N5P_Qs%xS`8h4^ts7W->Biy|Nm -yw2)s%ZUPxbPmD^(T^6xgAUD1jb3k?S_2x0K?{ZD=Pxevee;nzO=)`wSpc-@CL1FZ4yEvvgSwNSnLK6 -5fIadVH29--&(AjF%+9?%EZaQ~<^8vSPjJ=u9KUwqjtmr@Mn7>MjES0JQ}G03QGV0B~t=FJE?LZe(wAFK}{iXL4n8b1!pnX>M+1axQRrjZ@2x -+cp&4>nkoY2y7P`x6LXDnxY(;wh73RE6NiDlYybJD4VS;3ZyiX0s0L=AB!xD)*sMKA3)Qz|B^51C1ra -i-K1LZh~zovo_j5y!0AV)uu{3K)=!{qiqpsT#Pd!dQ1z{r>rDgw)c_uS^64X(2&LCj88{bslYK1>e0J -TvezD$WvK+3|_H*w9)pMb@(io{KXcV+Y_*kXB^RD+fTtE!+dv@%pkgDmxVnY4&Zmo>Nu$gb42K%>>Mok}%wC0qbkwZ4mbxTE -g@jH>f{GYLXm@8F1>s`EqKVV7**s)e!#I -g&5XRibh6Sfsf~3TXaef$>`>3NBFR`gfWCH~$izCa&!f8Tmiil1^Hla~Ktu%K0G)|DX;Cq1&Bw^gvj2 -CG)q}Z9FOGGOyV-o1cxvq&UgI&4>9z-LaQw-mqpvOS}d0!x3$s@w3WoajaZhl5jrbP#~U85S -yjjiuRPCNm6)$*0t%F~Cmq(kKQq}+P`L0ub-@&&X{BX}F#Bd -+>dG)7LzIxNaUmd)CZ};Y&>s-DR<%ex|e>>@}>vs1`2R}Oa=^qC_cd+X!@7`jOe*sWS0|XQR000O8`* -~JVxf(FQYzF`U`4a#DAOHXWaA|NaUv_0~WN&gWa%FLKWpi|MFJE72ZfSI1UoLQYwODO$+%^*au3s@#K -CJd2%ieX{D;BYVTaIz9PGn{!iultOS{&qx# -EJeMTQETpwzOyS)^o)q|+nQ*uPfarbvbmKwYHxRc>O$59l=adgFhZMim -@UfG(OL8lrbR(NT-;Er*@D -la2&$Z)pOaWMQW;YIgs`mWtY6C(+$5u=F^!%bA3r=iWKDNYCe>mz?m04Tm}zwm2)SLHo}7fe`N(P3=} -(Q43&mC|Xhs#Q7cCSTS@l&`$Qq^?%Xgz%9z|J}5sZ2N?)qk>!GDG8VOdeSCoR}uR8x|v{7rp6-FO-tXi -QS>=&n@{K_IU9Lv9fA#H*--5vrfGAfj-?8;1&)WHFihth`^CME17O^Rx-+5XuurJH^BFNU!Bp1QOWN| -ABmpPPh5$+bs$0%*CSw4BjBqF4%orB5GLy)6!g~;o+?&cr#+9Xt2F}9B81z+zi<-poP3*2TnLFfs(8H -%#F);`XgUkQvu6@(?CmF`6WHW6cxmD754T>p1+_bE#eR`0B_tyIh23Jg515r!%U{`yUVS&2Ji%v^L$@ -lU;+Kch)Don(`go_CMm4sWCrWeuNfty-a$b9!JIQXpdsfqjR03=DC%>uwvs4;#EgKJ -cs$v(u0Nf%yy3a<=RS66`S0qi^tV($ji8lGBusiKb(VNpe5~Y*6w|M|9#&;+03>aD06U7>7Ck& -$#T|u5}HI2<$sOh|JU*{UkD{+sGip_i_{yn$tx{#Yy~c*;37s22c}hr8=OLRMcnGkTv45ON6=4glgRw -+ZQy;buxU}NpLWFunAo!*=hE5x7DZfF83Y~0eCB+Ext_xJkx_jgXxd{7HVtqqH|v?bvKX>Zo@mqwykkUvK@j0|FiGM2VAtHD<$tr+o -}sjHt4b99@EVy_DX&ghuip{%}oy+^Ym@KK<~c-Kt>X;*}USRyo_*L2(&rFW!h`IzI+qx0UMkZ!Agk5x -`cXRn&=VmlpPnCK?|ml*43ydbzK0vM_Xsl9eBbs^f?Ai;mr~qXC)VyJQ~H(TA}Q>4V=}^Slf;c-@g@T -y&ljqK)2w1UR=mv$WYJ;L1KHgoz4vb%~j2+tTndJbbc5bxZ5|E-@m)P4slw`erY$c4Vt(lhF}VE^$g& -2eUB6Qi2Ct(KNooppf}7Xd+nnAfTnK~2g+gsW5%ci+bPAbUtH(7m^h_Z(2I{Z?0b!D@JKsYDKQVKG;0 -ZG3JTdzzmkdi7hV{Pr_4Qj;`=Ih8@{s`+8;XMEj_j<9_geHq_1Ac7gvTy8}J52&j!gRB=&+xlU~XeDR -_=(5NJLNUS@mqNsr_H2(mwV^qEM5q~dp?f(Iodu|U9c43~!j*$p^Dbb7#yFcr6nB+pB(3njYq-2&8d4 -?W*glIE?bN=MIHOp?=f{iGhDl0GOta`^q+RGb8ho{Q#wpxDoOJi+#b~be@H+i_O4sfokZpe3=t;YmM@Q2 -n8YJ(&T}tT&bCIVGIP)h>@6aWAK2 -mt$eR#VF+@nr@9006lG001KZ003}la4%nWWo~3|axZdaadl;LbaO9XUv_13b7^mGUtcb8d1a4JO9L?w -#qawm26~8>O=E;s1VQmq@KU7S3cE2oZ9;dGHGjlPKfFoPTBL->%)I$Ak4%8p^dg}D=bBa%INSgn>Lhx -A<4b*;wyDiE5hT5~&T46?Maj;!s+uO~&|}lUBM^t55qd5s -eS=aO9KQH000080Q-4XQ;4ZKyA%Qd07wJ?04D$d0B~t=FJE?LZe(wAFLGsZb!BsOb1z?MZggdGZeeU+ -b#!TLb1rasWm3Uz+b|5h>nn)t&}2w_=xH#}!?1Nj4+Dy=JG+Wa*V=5!qo}67?Au34w$o-h#1ZxQNWRB -+aCm=!+BL>Ll@Pc+e25XHHk*wi{1ec#FDhdh$?CoeYpdKU>Dk!IGwnfslu`}0z^<~I%`?UaQDK`udqA6Ixw+E5Hs)$qDv%>}z6#ochKvMv{Dn2|f$&LF) -1&v`gm)S-#yF73pyl64=+Ux{!U!UwXe`!JR?}3#LuaI%k6L^9 -_~X;v9R!=2V%LkZQi4v#W3fz=zNQtOjPQANqmq06iy&4b%ztlf% -yOZ8t~8vS)%f@YB;a`rS>6LFRFk-Xc6+1#^-pHUWSFcxFKGk!JH%GSxgvKB>V4eL&mt|iA8xo9~`V2C -NOu$Pxa>?9BC-vxXNp5cfKV4X1fX&uA;#GP!#H9Sh{WA>(`XnqMt=lVBhiMyB@6aWAK2mt$eR#TigE<}tJ001mh001BW0 -03}la4%nWWo~3|axZdaadl;LbaO9ZWMOc0WpZ;aaCz-KYjfK;lHc_!aP|jDb22x{eAq43C{>Q*xpA#y -=WJ)UYO^T@5+Ms~iq!I9MKd+`+poLvAV7kOoMi55=T23LB@$>f`rVBtkJ-t0Cv1@?GP|F$x>}z639k$ -WM@L74Lj1E9WmQHz;hRbn<>0^CpTXd6B}=x>lR6auC#$&3N>;4|s|uc#o4lx)nGTnE#cniIR+j=UG?nx3qfMTtYo -76YQ7}E{EnIq|E=|~`$Up63oF2oJemI4Iy=0{@Kz_QrdwZj=_0Os+nVK0JUX)`0BJS`zxfXd1#4F9$T -V5|dvlgIP6*~js27mNV5T6!eLnJm6&eFUT3DW1hDDvV-Qx(HBs!yJd%LZ@r<7ta4`G4}n%Y4D{!e4k=>)C301cpSUodxvGMZN~ -zH#Y%1snmqcIarda028JMqAFM*qu1v@p1!-hi_cEuSMRP~U!KQrPw#%3F!2dY)S`-imYa=8Vl*q|WWW -NSdr`$AQ&v*hiWSfuEE?Jmv4QQz8E-Fi-{6RJY%^vmTxXR;yaLua#V@54Sn6&hI;tJfvN>F&S^l9{A!W#9Qu -#4=Gr!u)kvT^&9M!&nc+yhXLgh7TpNI0{B7-T^%6pevwg8{5KC1AbciB7AK^9Wr@qZ`*}f4(Jbw5J#- -VzCRlPn%G1fqC80qwjce57P17cMn_qHlErI6)yzIlsHPIKPF9&%BZG-Nl>p>vwl><0=O?JQyreUY6_?h}f -xe+@pN{50C-93Gl{u;NI~*yR$xC4<%P^$P&jglT{p#K&Y1v31B+_)LDSUemRcpgmD7|gghYt6Q<)47+ -n>DuV=LF39FPi&Jg_xdlZFS?q+vIEm~5v5QQ0p>IEdGUk+k`{Vgn4g47DIEh5pe3R47#P=f0M)WsMkb -7NpZOko~h8&szZNi`*kPCvGx{?NMc@*|rI6a&~Zz-)n3o1U^!PCCoeG5hOEK%`Y*5T))?;HB55Xe7%AAjd_=_S%>=y(XUjC_z$kPsY+8ur^y5f}bY^* -g(?u^+F`dbMUsp?HCoIO;#jcB$&>)hQqir*!GU -ZwdACAC@=$K5GlUTB@|NUFE+x1&p?9vFQecU74g8r47(&6U2R4VRXQymlX9LQH&bwsAd!i7_ -(s7O7<9Hx1i&~$_CS`fG5G^4H)4U?0SbzB86CKewr3bq8hhrL9^a_VYnfIjz%CqZY#Ql;@fHg2w~ERr ->GvWN1y4^DYhm4D$rpm=*h~k)lakylkc@Jqg^G9?*D8fSZgGv0%_)3Wdq9;!5_imOvrxn+0aJV2$}}Z --by@QiSOgqjfNB9T5N_qzqFaR$`$2yJO;a#H>;$Pk0b1lB-=*HQkrmLK*h$cok<04!946*VLnGo$|sMLY6-&iNFhqR68Ltv9OeN -+X}rBZ(MZ~tfNeTrTqAj!oZJ4-jF04_+nvEkw9i(rg+kolz9r&oi?6NVdcu1VIPeoJ@kqR1o2%7QQCB -B-1f_tFAbuG{?45eLL*xW(dq&bN;81F15B|aat)3nl(X;MsU>7QbykgrtE$>e|L}()_|LJiNc1<3K@N -*Dj$x7aLkAEH9m39*@*cm%ZLMrBYULURf@(d2NHzS>6S++~8b51?ngP)6(%tc{n$B7wY9BDda@L7|9j -_smE?hR7u@>m52O2jQc@tEMGI}<^jQ*juCi1Iry(k|y4Kr~J94rcuFtA9h4q)gF@?zci`N -3{AvXA7+(7yz+-DKtZ~&WzSSLnD=<%^W7RP$0~lLfojm{={2jqEvU*z#F@g2|09GflKS=6$M4->=#s+ -DdyVvMT;1l -Q>y9<+ooXeOM>$LRdY(W)U1vnAiMF+}#FaO4@c?1c+>Ma?WT;p8>>WcFNTRSXL*&6NM-*?3#%jBCQHaA>@QO0oYW}$(!ncv5D?=wC5NM_QPIR_3~pkn -f>}$&jI!=LOlnZjt5?m4I)}(5S0}|_;DA6Qv4C(A7546z(O&1%FeN3#va6On->rR7WXwq;&fV(AV$(M -oRC6JGs|j6FkIEfO~7E_7Ac4-?U3$b&C*krkhwnCQXgQXL-yIVIx|;xN5B`FUd}=qGdoaATk1;xQxOM -}N>!+7X;`Dbo*j#UY$hVX>oko9gfvstY8sVv%5%25aW5Z1VhG0XKu>8OQ=7N%R6ape;JtJRmI8JMtAl -bRb)mI^mhGs+RAB+&ypn2j8;d$qn_~wDxhf^KU|0w4iHYx~8o}<4d{4hVI02r49Hsddrw)_K~id2VH -8e+1l)I7sw82;jyh2N&yiHerzBHT$YGBnQ0g_W8pK-LLbZ$NCMXYjg5lZu4VH!e^WDYG`Riej2xil=O -k}Zd&k8z^aR>$MdZ;(B>z&$DV^}6MyF0UIJ6G3N=fl9j1~U@p&#do@yj13V8X}M^u};ggg&^vh}D;ap~sL%a6t}Q%qqKCnxAp19epL)CAFlAzs -z(Hwi~VO$kqKNHBSxI_9i|KJwKh=&n$1{CKBmcS_h9I_c{$An464n3?Upa4X%%*aDNz`Qia_MSVt5$B -;%0(dAMdvo2%B4W#&X1)IThtwQk9b=Y=q&J>l=QMD6qPuNi19O{a)i^v=PAehNGFbX=6O`}#|nI<_2G -6zn>l~~)<(yC>T|+W`-8Dh(I{zV639#uLD)Y5 -!mt9V1Ce~tu_E43-W?sAK$mQkO(>PTb?3PBTj1 -g{N9qAG)WlO_xcW$hd%C@zKDk(ZnwBw^yURJ%~gx>=?I(iFfD_jS}z^*HNNysiR@?>kCVvHb?&jlBje -!BgFngaw|ODj_h(ZcS^?k4xuf~X!H*fq+KEfDFkj>a}MBlx$^d>ylE)C$EJ+twf$<+S8me1wq`Sba2| -(LQ|qI4nz|d(`c86vVxKF3^vD+wS1N&o_6ymL;lsiAut=o$6--jnW5>4b-!`C`4AJJKcao084xHVhY* -FS%d>78ZE6`FKy~sx-y|4;Aq^Sa659!a4n%r4Uo*jp&Xs(^jD-Kb?nnVK~+wyuM$7`KNX@Z4wAvou*_nR~!%OEI -1HPG6Vn^qB5bHB+x}jp2LW2rg7T8w>*v5-E?ElTZ$#2m1YrW2C%eN@AP-2C~d -_^jtDWjuPYcR{Mr@_|vke~g@a) -ITzDOOl+`K?eBJles>plkyYkfqiL#zzRe7*=t2t}5jFd@ktdM^MV=l3bCB($h)K;m>rVqF-Fu3}<3Pt -i+XxtK4Nnpq{06xF3c38+n4?|_wPY@a%w$R+h)-P1x|1+irtM0%A^HvAh96g`dVcSTPzT^;yEGmr0$ -TO<`TkR1GuDelrh7kZ~RSk+j;$v`=%O<9|zPdU;yK8(cdCY%dS#%l?&>U%ADP!5}Q0I&LG9YAlP>`A` -iPOpUw4-;|=9-zlDbuatcuLLjFuAIzv42Z9QE!>OWY2(#K%mwbdGFY4UeR+g2K{F4`4 -FB_U9xX0=#RNac%Ac?KPD_nZY^ILy=m|D~0HV%ZxQtofkrp(I@-M$e>@7VR#6}*T{DfO8KsZ*sq{B#M;q -^lpX?i672BvIDOkylSUrUp2h7nkPqWy_uwgA+)zCaK!_5UmH0GD!Fb5=`2CCN2X#8# -oalB3|Tl8?ahj1FC_HCc3Yr&wyDMf(-Iq+tV4x6h-;m(|#)uq5l83<(6?5;f*o^lS?6C>s_ZA@)hft*IFb5y -nb>Ahz9-l`qUQi7;<&_AeND4;-{_`022=D>o+j{d~Z&lz+U^{&1h3oR63t?nQJaJwQIR3nveGPLU_@U -=71us%*iu=T}FX59ryqyXP@3m^5Qw%%sfOSHFe9KzuN;72|t)5z2UQz9A{|!(}0|XQR000O8`*~JV0; -;kfX$AlQ0vP}R8vpt2ABj_c4*m?H -V4$Ri@0~Q6^B`ufs?XVMNGQq-N1pB2(mbfzjg9sP}^#gb(IieQKV#~Na+fqBooF&aX1b<4;One9g4R`rO -+Y=Z#@dX4k$;Vz|~C?F{s%um{1(z`1|oRjt6vcc0N5mJDvDHj>h|k{_6Z}5*D0a)EE2HgCo8q4{>}rJ -{eEPF@d>;g-#g3#jBIYplEG$--ZZMrKJdptPh7WahG~MfJ{;V)EsO@OdAF;TU)xY%7x2W;*TtxSMsCR -KtUx@HBfHCX!%ct6_4~j@V;Fb<}$ZiWZK2csV&cC1YBOOuiK}();< -Em@fXQt0jsQ^}AxuCXS}lb2#$L8aFFRIrE_LM5#-Ow~ceq4&1YsLf<^W<4kkR??WBO-9qHpjW+CTdq* -aYX&a08_tp}}>mPC4_gP+LR+1|VSopdG^jwK_*pM8@n|)+65e3YUQg+ARqUXIXuW(KV&IuvtbMAb7?~ -(y@6IEC$WpC`d#4c^wC`*%vZ{BPB0FugBmA?sGI|QpGYBo?W-F8dq7?P`wPnyOll`&ax2$+_z0eC|#v -4yJE8(SZZ!h0Uv5_N>fNRkmgFNUb0j1r-w{(5Xv&t1oO8#Z5P1;|KyZlXBqlr -Xd`24QQQRZjh06pFVL+`zY^qwg+lS}j%IwAdoB_Whq=E13vw63q6|pQ+t2Y``sW}~5Cw-w*hN2k4*u` -y>O5@^3jbapf#E6^1i2A|EwStjv0V8f2P#Yy{Lpa2_pe%CHisPxSu;~*H>Ma~Ia^7kOTK#rc`3PB_i? -Za9(lVqC0kgx=p~xueBClRrh&fi+SO!Is8M-sjAcsG4 -p;Y#IybmNE@<4Oad^#6E_s9E>?F5t~BZa}WFg -xqGS#yftHMiu5E*)kI%SYI}_SWJlN6Td%`$BRC5XNH2@3ej}o05Xa%n)Q_#WMDxKDtR?6`YLhdZ7~%C -N@Z^7P^XX$~dUw4lU8Avp(2_ndm!Ci^Lxj3FUnfHVd5C%_dJsDH{~tMO1Dm1x##G=2(n48oHfdVpAX8A@CG2g?u~;ImF^V%JNafp-$gc^Gn -7A`J45l~SICF!Adhk%SOOpIJ2c;L=>E3JPoOTa7HXY1HSkwZ1ApD2p6a%E?o-Ug7r}g1f~oG(%Po-zn -=Ic;Cj4snMJS(>P*?(wkV=XfT6}Q$@c%XVD8`TfG>kqxw_EvNP)h>@6aWAK2mt$eR#UsQi7Rsp007@7 -000~S003}la4%nWWo~3|axZdaadl;LbaO9Zb#!PhaCzk#`)}Je`gi{oJO_oO)>a;CyCSGDWKDK0+NMR -C;d>%HsEalk^@LJ{*#XnDSzNOe#GaKEpS?UccY(N%p -m3N~@GsdeQr5`04d77hI90m{vIh{6bS+D54jPATTR5pHR(3K^C-_=1eLw6OvqbiiUoVFH0dc5z0SF=A -SfQ*3S#77C9f+OAE;;3hZC3s4U>C>h)$)EXkNE_Vh7vi{qz{Spf<%x0+7GvZUHCS28cDRBS+$mIH$e4 -@lfYi!08Q2WK~0(uvO8>r9L63zZHwOT0w3_<1=E!T$5Zk%B9Rk7B38}RxESH~NdF -Z$sQ_z3hzL?v8W6^OJ`{Nb!LwXKyby>LElMJ0grkqCcDRb(D)SxwK*){Rf?jh`NdjM{)pAV6KyJlI!P -wMLmkb2HW)2;QjoE~P5~O6JglMIbL7?TgAR{v6I_*1H_zcOL>WV6A-?;ae3m|Fm77YF+mu3L3jvJlJhG@07<39WYCJ|IS2>>EyXn`0JMHc58yw8%4ehy5UUE3OVeV) -0%tU#>fC`Jp9(2H#Pw_At#eTfEdudW9REU)ToiLAQ4&>^7?M^NpRlCgre$R$?{fxHVE$h-lbMW&@fSA -H#i$*h7nV~9L0hz!|DsTSgLJhjmV?5PSS)>-R0tOb -R4B$1TW%Eg*!=7L@i@$9>+|YD(-ng8<^yPV2U{~XR&p8Q}ur~VP#-UVWtqJh>YC~91YdnI1TDElOQd@ -hF5n3(_*p-;AB~>SUww&&_t4QAbu|ND^H6}&!iMGj!bh|K<9#ge0Sx+9Ftk7nw01;c3b6(8z_){ -yPI*dnT&cH#CD~7^=M1CzAfZ>a7&FqM^j_%hydX4=#2}Lj1F43`?A^cbI9}V$jwt>B=KpG4)=Gn_Cl8tq9u|SO$DUV9|Sp)qS)z9ESfSd2;t+F;4ZgJ= -J_lBM`(suC{|U3k8neo<3>)oV9S+IsuZJJbK75GU!eaC-r%#Mw;+sy6ItIZ!dD9&v!_N)qdsB~*<_ssWzyG-M?#@zDU=Dnt -m)uzW<2FXUqFT`Jry?*vyswU4&|NwXkDff;tS2mdS5<-kHz@0+-Kp=B!=ugm!oqj -e7x;gJzFyj<`n<+NG+PRY8j$I6gA(^2sN)B!PacpMmy)Dav>MVIi#TZmRO1_D->#8+; -H;O1Pvh5p4UliUrS -Y)wdoS{TT?LGb)Oo#5Yi2Qi`2O44E~mhrQ|1rt9G#jSd-S}FhO`0(lHBy`*T+HW^7+Mx)m1FPe}iIH? -;L*ww(&3jea41RrTNd#rYQG=jMk{|?tmKoykI3M;+W`J-{kh!pwsxs#=Rt>syydgL?gL~f#f86j}E1* -sQ7dT$8f#YT*YvCk2bo2a^I|AAkJ$nIU&WDD)(CO}CxohKL{_WOQSJ!Y{!)p!LM7haMnL1~$L|~E=KQ -)JDc#Jh_jz30V$-XN+?%A+%&rnz0Q$+sJHE@I`5{0tBcT_ZIcdS$cZ`W1?+X=dDNh2v%)qz{Cx!A88X -X>paYKWRj{U#)2JJMDv*19zb3mxLGWk`SwYok7f1lo8qdZ2tnHszfb(g61^Ci7vDmY& -8AxMA}(GssMt&@gm>4B+Sj6oWtjV=8)w!Z7Va6~q5YWK*iS;Gg04lIR_Z(zck3iE8PiW}$;jFE-br8? -fj!j*Hd^`53)0JZx1$( -rIQNF$SkLGHG;)ge?5P9arWZm!&$P6pF3q*yK#?!Q>vS-a!t-uQ0Q5#8EwV<^>o0Q}P0BjG2L#s-ZyQp_|XUr -=W|DCvUZIq+>t%MzjG8i4oN$gu5gsxg_obF$f9r`k1Y$F%?y=KAP3Jk2&I%R%XHi?aEYI%${t6_jv#4 -P6C-5r3lKCKz?aOB13X{f;WSZNGHq&COQ#%i7hl?PuNHjBY(TmbQLsmgj-C>}JABO-?lQ&T*xgxpwT9 -{8#qgdxD7$wY_~w0;4A0m7DaP+BWQKznFFNA177G@V6}W-re*8YI@1g{8Gf;lnk)3;C2O(I%L{?$90> -Uu2Iw9GH@L!f@6*r9U|vz73O}Y?w9~`$PtJjyqAsZDL8Ul_hza|z8T3+Rmr!d&z<@6e|CNPFHlPZ1QY --O00;p4c~(<`vZBu}0RRBe0RR9U0001RX>c!Jc4cm4Z*nhkWpQ<7b98erV`Xx5b1rasRgkex#4r#&uE{9o9CG2JsPmAzwn03k6%PN1E}xy}a0$r28Ywp5zVvkeevx61(di> -gZWcw^Z9R#d2TqNi@vl3rCd}Jazp5q0;!URr{GGPaes#?f&P?nIR_4*^3gaHIPyj_vWZwB3TLQ?*5i3WrCQ@&V5&D<4b -dD46xm$(ZHMcnj7j9ZBBKp|um#jgu42pN|MEeD(#rPmMQM$Z;ValM?k#N33=*OG7^aFOc&gXOy1%%D$ -2KsMQ}5cYV}+jx~k@tQFUhwKyEGwzhLD)~;_2#_5$MZ=@?FBB_bvMm7?!YUTMOxUk_iV-(WDX>`p3f=QJK73-zu6g)Tt;lWs>;(e^8S; -1^9k24zk(|0YXMGr0`pttp^#djSVpxi^{HXgo~L6Xw0O+RoZ8C1u(LIUf6$nx?0;?{n_Eu$aY!{ -nk<%z{ipLI9*}7f^d>$*$I46f+&YT+(64E<=>{9k{l+O=6YqY5S1w0o}GybnlY_dJSNd5SSiK2tAE1U -?!jH4Y0My~R!G0x$l!Wor&)w*m<~IG-g`2e{DWD~H`HZ;5p0m)(A+vu3 -g%5Di??wX|5i6NQJ0?(3Qtw-0nz-ToVp4N(g%B^I8pU;kX3aVhy)Qq33K7&?Q5;G?D2N9Qn>f~|v)UZ -~vpbgR8>thFby4tld28p<^Y?&w*ImC(kZq&*dOXho6x+N3BG5$7p&%%GB*w?iZ=ZqOcp(e0-Wx8z(tA -!qj2WAAwRM`S+Cm8^Z)pP|ei_y3Xe!_hJPJ`r!YjW5#yRz_^r*xhs76#Boja!k2HxQwrh-)8VjoIT9o -^nc3O2}!eNbNT{jf5p}*`vFi(0|XQR000O8`*~JVy(bqsz!Lxf{zm`+9{>OVaA|NaUv_0~WN&gWa%FL -KWpi|MFJo_SYiVV3E^v9>Ty2lz#*zN6U(q*<0jZ6KPMj}y3WPh`NzQxm+KbI>3}Z8pIAo88I~1vOlcN -<3=eMU``jwa-aNj ->}f;4_OcFJ -Js&Qy;O1`G*K>cCGYy0$D3Xmt#`)1U>W7M7cY|32CW<)ewS4}5Xlh^BTN?7%}0iGV^T6YTM -VtNtO6`R@AxHFl+JfmIax%w;yED&6x;70ALT~%N!Fad0R?iyihkih -l50-v6;4vLNGAxa4pc2y(A;a -+y20kAmJwa6RP1TDLT*uq9NFsCGvWxLC(MvL|iA6fMhc~jDF9v^Vjs%l$-2DqLqGyxo-Xs_C)K4#2&U -CG8Y=zZJh3N`$$RY0_^VGM2qlErx%c>tZwfom-uD-h~dc58UJHV@PJhSo$<+AE2Jao|pXny7ZKs=Sjp -^ABxLMB(8rsL@dza##eBV_po!4a(~dgjIrI>h_S?O=ni2O{_uPt8TqcK`rkVmSLkF_`&DdT8Wiy8?*% -Lhdw|46gPLry@bcs7*azmg9r0W7r>kJYr*y70O}Pi0VL6@#uJQ3+_#|B(<7#*ZDd!sMNEWud<`s~qeo -=iob#9jcpq>XsveGgA-)53u_RdkNL6H_#k;rPh>Eome4-NfdD$c#>M%X~x(($0!YGe>00W!0vpbZTUB -CsV364yHRSdacVFa{a(JGFjtV)``>QEcN)yYS&RyG!}QdoDV`hu6owhh|F2Ii=tcwpCuE;Ajh^gXZgW -+7z5!Df|Euu0~Q6zyMmN`->;za6X%B@52*U{?t|OsI!(%<20`P&}#8&jqowun)o8-v>WbzSHg}*oQhU -rbykXe7gfr;o5Ej?Wy#-Zyu$p?kWrlCzS76^?~N!=h~Q^vD_x#$?+h3ioSLrdGK(9rofB`<~5vzd`jN -e-Rj%6nV51)9eLbnrl8HxoCjjb%QBw*K^_<4F|UI;!I!zHgE9ew5VhP^5a7XA63#$pOu=Ud@uDq%hjd -Y)Z}Z{-P3YM60(@jCfSU|`qvI#9VEDIf2eG%7xO=js0GwWMd~?oR-?x2T62K`-&{hH=xct6RvaA#tiR -EQ}G2n5kQz&lWxR)c39EkBf_PYTap`P*fh2iClQycI9|6s(CsyjMz&(Kk#HB{&MDT1Hqbcfwf()|<<9 -w8)l%cmjIeb?;~-h6$1{pPjQ8gR-KO3GkYtLCnSUlu22S$4S3yZ;tn=DGsJS%W5nUur?Y&{-KThjg7% -5x&GmhzlT17d0H5ION9wa|KANg&{!as+l7H; -RfM*V?@1?V%_Z`&PlU<*=oVA{yq$QkBXiV4UqQUnE!EKuO80_c$ogW9$0G!H46g)RkXtY{C77^VUS|H -fl@kxRik|6AH*r!Bz%yNw2|q~N9Vu2ZR{un@`G0`9egMsoxl1N6d8*|_&;&*|q2lIFNKczX%ny3%Y#V -BCAKy--&NY;Z&ep@8CrsgwoFX0|;5KoFrpZh^{EI0!VHu++XsNP+;kbr=_BTlfos}R61aa=T+Iwx9 -W4FhGobfd$@$W=B~FKp0`Ic5WW -~FBU<#@;ObraN!t#zv=Ii!6>r`}Ex~up7pR)wrOiBxL`1C^P*FeL-_SEoG-(B(NRA6{4q$X9yCt~yA1 -hjBoE89XhD@`U;UHczE(C2Bd8gHz$3VtV%EcRdoQNa-kup -1Co-Zbroj6aZyC@9r!6!brNZ&&6lbv>b{hA$DFuxwbfDa|{(z*igNg_7|BFef+1V=$euR( -$!m3C)~Jdi*D;4;ai^S^fYY_u}8-E#zE{hmv8z>7er3eteFh7l`WSc7Ey=nW{O{$a(8%aZjG#MdQSeC -RbX$KD~5GC5(01~kAHsjyC$i7)EfOM^;xCVM%w>VZgM2s4$k+xBBj3(y3Fg2$>PebikJLZZ6bX5(vq; -Pqv;WkuR-zWM%5&_o-s+5EApW$@I<;$2f$gglkp_BHHhpcj7mJM*>omd0UgJ~rv))@!JaUwFz(fUUtqo2Aj*mp?8ld*#p -=rz)*D>23!`RFYB1%_n+i3sC*9yn;r36rUzR_S@A-=eED!dd7j!G{r`G*rbPY1xt74fnS-A3iSQw6?_JL6xG3=0Oor$wT?QnEj_$H%|L1h_3_=7x-Dld`VxlTs;m<&b~YRWqZU$A4i -F7mYY*v%4idE*C{A>s$DSWAhT3|5SUh$Wnb-~ob*PVNvUPwG~TEV28L3H%%>lL}y#=lw~(CwUfNDBTp -hj!j4G47)>CPqS(E3}@xSvfqt)ypM+FQ#E~>w5#GOQD+h(dXYhW4Z?~T#mOp7jV|G7rJMG -d28_I;9+D|jxlcssaj -v;D-Z*NHc~jMOF80Un9)JnsYVtTHeC_i}>1XKDc{xuilaWQBOW3|Pae6|G?>dI$eENVKliZMN0{NykV -3Io&RJaD)>@xo6D}Yb^4Q@f)27e66xN|U<&8(xrqbO%O55z##jZYT>4Lc*V*lW`NA6McVVRZoRsZ4f54QU96i9lo=`JFCwuM{0Z>WQ6qWs=d(`%E~xobl|xpgR*A90Z-rXnV+w$1#h~x#w9#m`jD0fJtj7Du$C2# -V7ykHE1-3p;POPVt3ekZnk|?DT)e2*Y^s-^kA)20Ek={)(k;cn=p?hVz`=2ylPmJi(E^8Bf^%pXtex@ -HGrx!brBOAFxiXG6@Okm;yLAqc_>{yA>HvI{6NTV&@UuntdG`szIya$3{;Z?k=lZ_jTBbajvmSUgsk< -XtfvHLGu<#Fjs#W8`YAP(wd`bYOv2ala-$=B{;`?7H@Hc>d^Vfu(!(4Jhtqq0zEUxdibXb+=XU-9sXy -lZlU1XNZr{)uX6S4dBGFy0I*t3y=a-Q)%6w~@TugO<=;xbaXo9uH-#HH8})mw#xK-JwWb2GWY78x0$WkWnS4- -5)djj}gNBT;1pii5elXcmkV2|1(uFJBvo#2|=%|T%G;8syskyOp*-OoY(|D^i}A)3lnj;Hc1<^;I%>X -ZPh8hHc;HRbP7YKx0cYKr4$ox)Lx{bi>MG3yi4oQ*TuoZJF4+Q#`j6+z!H3hIchdR6~B@rbkJ!y&Kym -O)S|Ze1PKFKP7SS?DD}=4briH!UnsR(`<;Xs -3HHhwuvIQtOl9<5;N+v+Y#iY`&?_jj$kh-oSY6nZ1GdKBa-XW)45b)Ap%b@I4uf4!45`X2^c$IW&$P* -*4lJEX^gv#Us0Z4xBTi%W3OYl&=eg-**PY&G~4*Ea&_pR00?!`0PX?ovPb89xc#h?^9b!DWsn``NmMX -duNe-7x9k~N^ZN>oCner~(-sfmtv^RI+qe5PaTg?O=r;jSvF9KO3wd2|k6)POh*gVgX`XP?2Wz)=#Ax -&Y98yy_3SWk>Z)Ig9tMKg+lJcp#Atg`yBWHir#BH32C%gbxe1Pf3mv-iZPK15w^(vm6F3_3Z^HJvj_K -$XtyTlc@fbb+&{@%_T>gjHiP#DclX7EhcT;^M&WNeye?%kT%VB;|P7#(CAV&vp)N1M&j{l5fhhUwP}@ -x4sIMU^tIF*Ne!{>CpDc4j2);e2$3%p21*O%PlI?z9LJBRk2=ZUr*65!AXe -inpmYc^TOas2_Hg1GDaLA9z*Il>A7vD^mpTa(O#pl2I^?${y{E0XIHZSQ~&59~jMw7%9{bH%lt=XEcU -ZwXjSP7`+6xZbux(B*T+5I+Dwt?B_9Hs32jv73_44J#JXS6&-@s(Y{`(&4YyvnySxj9qY_Y`3I6kS4w{{c -1n6+Icznvcr*e?g<3s^$+g%hEvo0{{%}=$3DJZY2T2@l?!X9k680I%El!yM{D&Uo)ahE_1-p<<;fw)# -VT3^6Ta0>*eL2#s9v(PG)1S?c4Cg^>!B@|ByH!S=^IIMr&Bab9=!=fh0E1pcX`4Wo4;a_j9utc0tFpB -E1~-o%_zludN1-&|q&n(wfC$D2kQ&>l+BZ^SBJ(M05>=;VnTnk)RPwd=>jhbdjsbLE6Fi4hkM;CO3h* -G~RN^2z&Zi4#xY3VxS0PmpuqKCStj@+X!}pX{8Bb){1$%0068pe)EB<0bclh;X9aW3*KiuV1Bs5xZ4u9JZ8hN*WcA7CHtoGGqsHN!Jos7Vp<)I=?aYVeU3OMopicC0&wNvXj&-Vybcm*|R(dy3-JA46oXm#6nQ~1GK8lXH -fMQ2eHw{50vA{>57_5T4-O9KQH000080Q-4XQ)i?&ghvDb0J01K03rYY0B~t=FJE?LZe(wAFLGsZb!B -sOb1!9hV`Xr3X>V?GE^v93R!wi)I1s(-R}9ia?7-2bmqma*v}uZ_Xxbw7?jCJg99!H-q?V-Ocn|&Uou -Ne8vK<-m#S%61-hAIv;!B2=S4O+!*YPh&X1Q`u8d(=eS -lSDv9rzr$ua4lvzwijRi|A%Y{&uzG~4Rxs)yX(qJs!6L-ZFFj0J>^$Qy7fyI@JCq4@Wa -GW+ortfOPh8(6Q&(t5hh?4wuW{OS?XX|nXx-~)>XiIC{qa4F)DhHf#y$XB0ft$PqZC>d!Me#ELear&2 -x9y!ZR~9vi*8y6Tpm%#&OVU3J&EXdE#`MnqDf5rOrEVYdAki+8rEI -L;YS}FO5<68?K9f8C{czRk)tlkIwQ2b5(yGVCsxQP776?~{Jk7UCqr#0L8`dpY3mEU1u47$r9*V0D2( -^SElN}Ca7J)uy#e^^@f!!MXfOsRJM2rgX2G$o2PaU9Ct~Wfx;I?LJWx!JD9Mdh_$_Bbe*UHeF=%$E#G>N*P*sCq9h@~l76M#F#K -=HjX8=9wQCHc^#)76<0bF-m-2eeZRUucj{O7d$Yd2ry+YkV_XP&Q6#mtP;f5w%_-)Pqxqw|T9fP~Pe0 -+H_8P_xA)60#9ewKmfKFu&l#Q;WIIEHb#en@5$&B4lz&qm!n-Ep9LvK*cp-maCc+K8r;5ydyAqZMpu* -Myhb(KR;OCk0)|lw^3qX-t5-qLa?4f$JnicX$^EsmU04qK#xb&t%&67=u;EU%wJKeB&_`1|3Qre{oR`j}5TG%ZVsMkI}XjJ1t%p~ztvF$6t^ --lffGEd;La|3D6=}U?xoXvbb;7dhHOIEaN9N$^2(Cg-S_zh4?0|XQR000O8`*~JVVHPL3jsySzgbx4! -8~^|SaA|NaUv_0~WN&gWa%FLKWpi|MFKA_Ka4v9pwO8A2<2Dd|*H=tk1hN5F4_g!q1jtJl=^{bbF_PV -VDGEWhXq$;lsw9;}UF6?8yh+r}bUTrQ -H?Y!>YVKSG-BDpd)Rra+HIs1%ebTxDBDIg28<{3!8<7b!Bebd8C512UiiFUl8o5qy44i(7 -UKB++*+Ghr3UVZvB09wLg~h;}xF?tk3^K^+R>2EZ2T%tB>slQ+gRUB6EC&~bBr*tV!JlbPV8fjv%Z|j -z=^suO_--21ZnY8u7m6B0#dHoQ@C(L_yk=T<38?kKX}?R}CqDtzT#EveL(?}H-(qb$zJ%C`#!HAD1HE -b#<2OhA^MOk6DGx7PJW1GKnuVtHHrDmJz68pk%!H!bs>sArTQ3FQWSgQkU^yp}#mc|{9uv5=0Ql`jaA -x6fOXl1m*fScUd+Sqq;8l^MZA5W_SKK1;N*TXxIeG`9BM(_gyfe2P-L7pRwj|8~m5Gn6Pl+&qSB+d!8 -d8JAu->7&f#y}~SDQRTcz;4i%(y3ruhmE~Za_Qx9Q61?Cgv}O3z%aqLNjIElnu&uJUM2MTr3-`GhY)} -l>LWTTfrCY9(NORf)pf^`1VW+_zw5Hqbw@|@0|8foIG;M=D!U{kE<-tV9|m4{d6R6|9z$ad=DSER#OB -%VhfNbEw%^k|FP=p?rFDwVNZ;|u-G0WTbhr*X(PSHE>Ef};JAFd{NV%sTc!;{;s`vY_`Tz6L -ZcOuzibO?Z*5dP4enk%DL^RIQC$tOrJ4m_k?Y7B~qw?b^#T9@9RL6TaA|NaUv_0~WN&gWa%FLKWpi|MFKBOXYjZAed2NwFZ-PJ& -h41?-CVBvka&AaGH1W`+Nl997#=r^#Y{XrP$BrMhH`Lo0e{df57viJ5{&D8Mk}^^ZDV6>%#O7$o0B!q -qw;Hl0c~yN^;%ONh}08f?8loGieTig7&yApbK=>ujKx6{i_EB+Sbs_ZHatH8~SP$Zn(AD?;U$_obsxQ -V{bNocF*0qhuv+j4bGXT+t -hrJ3+28c4!)IBiw)M4c)CU8UhS)1M+q&sgo^`q_Hrjk|Z`!@ReRa&=bj34NRee2EU9GaKy2ur@EsIsr -Dd|)KkS67B-izy^yC^D{MWeGEW}MxqRkmxj`mwGrciT5jQ#b1RLRYHovMSG-T$ROHgDZdqLeEV7%kld -eKyG)d3zgBcH=8q^=Q>yKZ@P=RO4W(h>Z0qm?fl7;a~N@Vmagi}lWn_xVlernXj}O3&+=2)?|RqCllM*C)vLOcuRqqS41v7I0qNOPF -jLe9?#HU#ZSj-N-)(h6?YT{Q0UK+_&-FTk^)Be4G-$hKx9WC{wyj=etv;?=UA0A5T~*R0&!%)&QCx`Krtt -)3%$u^>bSbf$|C>x;m4VJb+M~1b^b<(iaSvMS8zgg)zLoKH~6%IGYZ4WgOd(U$p#*`yA8nMhcd#SPu~ -43o56Ek6acfSvT}h`Vkg|yiz-7bE&$Ik)=cfHf(Wl@7g@V13hD3l;$@x#RksVI+bY{=MjUPcBNM1>rt -mn`9nxXJOKWM)i=0So0h_vo>BD;-6j{dNQr|3K4U4j9VKtVfqCe5CNue4TUW5U^ -$ZUG7F+9TrU3D)OVgiFQ*<{|OAecD*7>V?m6c_7R>E9<%C=iX2|eb`hjRlMYRX+j^P+=MT5v-kXGVT}TDUCaWcakyU+}9v8dNOFh;?~@N`Fv3U#RDr=84R%N>J3oB3YLF^-M38*xNQ*?u#I`q0PUrFAKtt^{_y72>Eh({<>|3H5~L -AOkUm#BpDF)uUp;^NmD6Zl@2Y5{XHKIUWK$Ds^xSFGXfO($Z+<>qy!zqe&wp8*9RJH3c>dK>bpZc+_VwQJ>6;J!yKlaFwzs#pD -zmmVJl`DdpNcOJQb4vO_}T#uC;-8X(CL#4o*ww -H9XOLeOr~S+sHZbXqd_=SYBEXxRs)Xv<^2%}zF2KE2>sk5c_DHY&TqD96BMXEs%gs*&+uLYlM%IITj- -U>vxGg;e)zu@Wr@lLo|W|u&W;Ff7XT?NTObS2LEs=~3I&Y?Z%kzKaF1{l&0<3u*(CzWW?hCUrSeSG0$LXbHDIzU-^kW6-#1crwhq@b&1d%pVL7 -GrMX(58mSZu5fNwZM-W?<_h;4$Nagjbi7rM0&_@4o8ez_nKF$g6S5;F(_qn;B*Lb!1)zCf&Ta*!uI>P -KJ1qBB;D{tq0`2JSrheILf%gP~JX114= -Q3iYup0}@F*VP2{-3(72Ob6yyv1Fp@wsRcd{mpZ*6cn~Oukj!@HEhNhtHlw0&c%{$oaL`iuB-%+dDu9K|zkr~mXK_H; -18gt*3xxL&AtNCy}ac0nB-sb}%+T7xmTMs@gV@S4tj58(N20A{?B7?4mp4Li$8`k_eG40>hY -HQGEida99OCef?#BOQO_SZk(77gta|CdMi8>k?LX7HiK7bUPB-0@rm@}O5`1wXbP*gj8A2jt-^?CCe9 -=eXV1TlDY$#VZ_&8*#E_6WB~WX;j~BG6IR=_b2$JaLH#ss)Owqjl#uA0GK;?HnppEv%>`2r$u6UtiKg -8=5>rP@(zx1NzY-wkIk@jXsCd1gsmKZDbnGl)eH;fC+fwt48n1Rt2@j^a9C-tIrNM -N}*Vk35;5kJI1qtPuCpw*#b9KT05c~l+Ji0b<^v5?l20nH17V8SoaRni=;fEgi$&8g#jDLvHh<-_+&i -Gzq{{}J`|u10ijQG3p8&-eT!w-AB{-!Fk|FN)O#GPxEh7r=DnkZZ0m^g;0Fq@63aR_5%b<1fa$<2C~$ -X>L3}05bPp3iFziPG8g&#dOx@akL8y^q|8ph9%C8UJmH^RkTfjVp)^|iCkwsMC4fW-hjgJPuFFD1X;Y -Xo7C}ItPeAAW4AdlY?jMU3Fx<^wt_AAdAUT48s&Ya$SAW>r)x07dfEi0t+r*h^3jO?(>Xg;@l@V9Ta} -=+GFr8vdP7B%$-V9ye7m1BM(eBv+$KD_^*#RU55=L(}Ez~QRA1lEBXA0Cw%S*ueX+v_9|6rXMy^@ -<+XP5m>XeCKz{$wYwLPQoc|JER>HF`$H=b}4%5&bpBapT>T -534Q~XpR#a)-oIg3&o%KZuh*bE2Rcpf2uRa05&)*yF)AMwY997XR7R<~&*2d@l2o)lV -CCDeEl<+qqL-|a441Ekdv`Br@26tFK%AOdoh*kNFM}a<0=RIQw^NM3}N^iE^4ed@#yFjiB9;FuwqJ@3 ->?0P8>CYu$WW6xNw+?c<4t?7|%Zdlk6ShU!|eD}J@!#y7hwvT-Ey704p7>YyMK{zVu4M8zU+rAfc(8- -HiI@+#pL6LnW4MUB|9%VREIk4EZ;97f -67b&<`><3HWkwDu`0t57Sz7Sl8HVLjM6fDiV*Nqv-%dli*jjBd#Cj}HHyhedy)KDvQg-ofb{$1WA3!6 -lPnn1^<>8>TMs+}c$$+tvdCt*U6yfMY9918W#e!hh?zyfovjQ|$3Jzy?Yq>L|P=eH6)Vne_PD{2-x`K -dhvTBDR;qO#l<5b(4yu+Y((1ju(`oi#wP{O+y; -T4{NFsy!^z7-=J0-uvZ}vs)hh050T6(zIrmfHMJ+GMoRNmEOqk^UCfXmis(8_i$Z1zq|`k@Zw-J+Kp;ld0ajHZ7lnyKMk2)d7w>Z9!r -ePMr2{m)^|xrWn72d)Ea3qNdY%y$C0Qlrg)el_Ag$gU5|;*!mnV88`FTiBkmpr{c1->8lnDCB{nsdCm -LaKxhNshb=``24)MqClH5x_tK(iy@&UUSMMMf@mAe#h^4MUd2c&!AW9bRqM9lmPf!(FOSsv|xbY3&>H -Eh^<(Q{Ro$)+>~3fp+R*9noah2k-*t;-I>{KdrMm+sVs?^zz`&Q6$D&&k}uLPir|KM|$OU^zII?CX@h -7-s`~+#R=>r(@r0AQ@!J`_XWt4z`l7yszZ8|H!GD^gi5C9B#O&CZ&zolx3#(+E(LsjSWLH`u?F!GoMxB|MnWMNs-@ocfgE}j5Jz!&JWl`nj4F{1W2lWZS0j^e&pl{vYS<6l=Qd -x>>Rqk@uX!@wLNvTA;|7k@xYUfXQq#v`vVZT|U=Oszju16Auzc^>Yvu)eXB)i8NR;ttkbe -I^=v-7E_9JpXDYi%C26{98gOXq)Vl{l@(c8gW0FbN!+ICEnzkb%IGzIdj|~rMqBma?JM>CcQ3w$jVv8 -K3$QbpPtd=E3qzY9ISDNf%P5j*N*Y{?N=iajOf+sZbk_9e%Eix#6LFVa0&KzRD+-RaGy}}TbSQrqr6J -U@LN-&(BoOM1dd@gI6I75UJKv!7dDNE^@YY-UVXiT(0l>o&CTkJ<=riDx1D$5gdF#jT$gxh-^LuN8dZ -dy^0)dF*m#6>G{5l7|&3!VP;FPNOy1B@*fUL7A(_IkD=wVG8rV4C%HOQALP^}8wLSPej+S9OC~*E9(S?wJOrUDUfKRc`_JlC9MNH!Ok;a34$VoEBrGhL>WFt80Hy6kB;IT)nYJ_x$A4X;3+vvC01Z( -OQ`=1O54vtsb06h}K0IjvR;+nl+FU%?RiCNK%d_8CxCr0&8+W1h)V*%N|h7VH{-n^}bgj4ips>Q$?MfVyL5jh5^ufFl_rV$1UGl{YggA&~yh3Dr1)XdWhu-2)Y@ZjzyH$`btvjy~~KvYfCWnciMifL;tRRyc6B+JV(laV~owc@d+bVK>K+-HNZ997IIDU ->u>L(rBR}H+taisW=02LYFB3DbE+mi2u;7Tc2BAH|saVAOc0qbV}U@|<|kvazaV@3ho9j~O&sAfn ->eKO%XdzdSXQHY0yvt-aJ*z7tAkRvwBT?>rUGlXPRCgz9&?@H5&6#gInNUbL*N1amfgn`^g5A(sEVpa -<5*`djDAjp#yUqH~CO+#4w>L*P2Jfw_IB3@4WY}0$9H5n$UPeVm^SeAE>6-u62oKoE=My%jB+UbtkFS -JMRQOfH6#pHji-FWFp1d1nTzZ_m)UmsHHr>&vP1Hl`FAsHBx675bvto!O^o|{1rX;8#Wr>-;dshBb!i -uy?nT}TYxl<~ME6BppbFAx|g<=)H(hXW-G5wKtR;B|5Y@=ym2aKPFqVIrn`D*sR#g&Qd&M=j6IEIuB#mqiw#yN#0gy3 -P@b*hpgh5zHkVa~`MaL&`=(Yp;tlc$p*eNtSXsnSOGk0z7*Sw`eKi&;iw=Ww<+peu5U!PfuMp_>&CJ4 -toPQU~7Nw4LYV$^^_1>ljIq#LmHeFwO-OZWn28dm{YmCNbZqPINn*!)=~3#vLibOh|aF7+}0gaGx{!v -+{5ShZ|3LvFedJXfD(Ct{F-WXn+AnC0#I+Ujgb1+mXqT+i?$OMpl%0~COnJh}P5>>b?-ZXl`ogU76LA5nZ!APw* -cz^#7hb&UL*wy9q-{IMt(05jrR*HLg>~YF|9DxI$G=RoNbd2>}-%tI*dhE7sHft^M0s&#d~DG2q0-DCpvFK)LlAz=zo&_@aryb~L} -+t3Xo4_puJMe%#eeC_yOCxU=rIGg(f(W=OKR;a^k~*=!6$}$36&>=5zvCt^HhEP^lPM+XHTC78MO}HV -q;)H;IQZ;0_wGwF>H7LhcK-g`o5)06tWF@?v_h?#Et1>(f`?SUP-%h89%^b2R^xxmP@O3TH7BwTT0?F -Mw7ea;SF!jHv5=qI7q+h(F}tnbfZpwg&aKR{OGBNfR8Ord%0|~Ynr8ax=BZ0F4<8@d2I0ZOm4TUywXw -^+^MNJ1Rxa4!?XZms%S#<1wY7FOv-jisp0*#r#>IZu4DWCw!mC(RFX|J+ui2zqMp{vQAXOfF^_`(O -l|pt&FA+;)Z&#jaQL` -pVS2+KK`$+(BbIfO)%S;oaLK*EnVym?IYh7aPp>B{mC3pu5(17cQXOG=` -Wl#t?*JYPYhlj{qRcK0{Il~9E)^=w!2AUWa8du@8LYg9Ll0qr{OjtQ($7)ry7gX<%HLcB85_YMMy_FR --cEeuzU6qmMiHNyTEF*fX$XbnnVbwXxHL<;6Ho#{t_02U#1+wORM`-}T0hGNsvNt2A7L&=-;Z{CaF{j -)`(hGbIYR(sdHE~vBP#RWzEEee59#4?Rr(WwXeZ8_epme=JX0OV+)rpsa52`PUNgXM7QgU4_DqhVEUi -N(cox|feVT`vZGhrh#nEi5uMte;$9q9~7&LkXV`5-4Ju0ljKK4~(zZsix8(A>tC#6i -)Etw*sm^jS|2+zx|r3CdsCLH6N)b?ZW7NBXubJ3{k#OGZLzu}!%>)hUdvrxprW8`+lzyn3=jsooqsD| -&;ekXaNC2bc1``9Uyk8uTOPQxx-N&{K>rPIHJQ_)phbU;8lLY<=n0R~C)z$q(q5oD;Gso -p*q((2kcp#x3;$BdnaQ?nFgp{^?y4-1dO7=)ZUYp_!Fj0a*tr(aAAjZyh)OCA1Ov{T40Y#z6;9afPei -K@4q*Ea4sa5bG2uoHx#^#r8adcRUvEF0bf(D -zkv$Y#D8>~YUhcw^|OrPx*oUvH^JKy0XKQth}jRLl2Y@X66h5rm?7~OaJsrU6czB`YDOZ*1I!;2OMKK -g1V-V5_<4wAO#E$g_GkPNwaJuXCH9i{%tzwTmPJ$!?jar8Kgpj{GHNIx^vn#8QYjNoWVQDNv!Z_sV9c -CXn2qo$t)n3!ay(Yh3_ON>la?KWqcOnttiqv+(?3};GN?hccRpb<}grRnz+nxf>blXyJ<)ObY}D)+$_ -v1>h-p8+AKyL**aA9}ExU@N@w>{99m+qyqf#QJ4#pn6vSW8TdnTd -@@WJ`5gf8+wXu6C28u+ay9|=y{-B(%*XI7yH2s-3+OPW9@lA75orW`z32q0cz!lmq?gQnwkK%fW#r)% -?*>}?d>E|(}ErJuP@mFy}j=)%Uns>M0cYnDj1RYQR+DBI<|HRcgaIz1Rajq{gHxIWYJ`djDqkezsTHbzlM>FD(aZ$7JojwCt -!#&17mj;G&5NK4cr+#)Hr73Ol)WweYf@{37Hkd)-`Qul%dQ`hUppq;Y;Y&JTe&4NxIMVK^ -I&~;drz`tJBcE~X^^%&%}dQ1%5-6N-(9K)hJ%$T(zuian`yKqm@+>9s7&Pn5SJPT=b$iI(*!N#}J)Rgy-Jp%AKwvS^w*6qJ4#W=JP_n{ut4{EiXvZ(qTm0rgQe)K1<@!-?4*dUxpNy -{A?(2P`X93C|ncFT)a6qU4|c66s!)Nj-6+R;EO>amtm&YPY~Xu!WfqXByf@4y4yC|i$-KciBFL6xB-@ -)S;I8zN%K?12B(z0y($%8Y(%Er8*drtxoLT#TqwHab$ifE0u=9@0Yi2bgU0`v`yt0H(@Fb~!br95?** ->F}%1^o^I{J*Rg^0u{eR$~5i%P_J}bZwEKH$0j_sulPqag+xvFU{fqyJYKI|&w1uRXX;&yWCm_2OL7Eaa(Iin=;%zE ->Ov^30eI8sm1hOlAq(R=&D9F9T4`z^%^PPa4jTV6vRF#JunDJAAZmmmXtRJ-VRh;w?)Wde;ZHEP_lWu -mZsN3NlXgqQd#5Hh|+DfdJ&GSX|T)4a%bc!fH_;%r5`m#&Y%i!=>swu`$lZX;M>d%n?Qvk|PZB0D(i% -@|DD)vSja&xvc7I*wAxr^KvtExMC#|VFKl2=PYJMzyrG<NBCOJ)qO|IJQ?_Y7|}upvBT=t|iXptj*-q -<^}8r)-ir`-z2Hbt|Nk8hIXGGh=Sk8g9a!cpqSUprpI35}6~G2%EHogUjn6Gof+Zy@+OPeP}LsS@jGw -bJ-`A9XMP}?x+9PgMXC)=IR~Q-)E)G2C>O$RJ}~4ad7+*|Bk$$6KY$|W!2OYOOYKH#T@yT$0fPKT@uy -?h|KuMvkEC2C#SjvZj4Dv6(7#xzc9WT)0@Yw<8wWa%mMkiDJ>t)$N903`^|z!_U7`ySv*Tk$2jkGS)) -A+HgaKYC#fm%Jm>2B@b{ze&Vc?%0W(h(VxJ_~*WW(-Ej(h!>vK-?Xz@JBjm-MX7G4J6KAyfkd@)mpAp -GF;9zKWFye9rDY(rA9!TkQtAe8wtWhXWPH0xf*f?O1_=tnTjeMxIWMP!LT3_)J#VZ=dXX8<@OpDD}HX -*$O3{hx03aT%(nE?*kA-9|l~mX-9^4bXud|4lY`Hs%UFGl^->c%uDB-G_3gf_wU+0QS{i@z?5byD0{? -Qbx*t2YbsUE@yg`Q3@-nsE87xjS+BI$JW*x(1b9ArCuDK75sbAD<@I`vH%J5s;Enf;KhGgZr>&qN+>% -oa#W_z7&Pi+8n0Ar$KL8nbFai#JMe*O|!y1F3GzhoQ(y1d3#`J3X74ci*gZwkgO{?wgD%2yQaI? -hlSck3u7?v#}6km_RbYLp54la%<>4zvhe#DT1veAOwU&G%>w^zKxjPOe_!ozw8{JYdQZ1-RCoWUxZkc -HJh!uo(AahIQ<>u2dZ5Z}(tJcD$B$VRYWFsYI@ePBr_r_l6@E@;RD+;`n}LAu0Z(;{>6?+*(d#+=(5z -tsZ{A3EUhC&oJ`FPkp)Fd3|J_UK6QF$nKkSzXjM*K1%vyp&j1(U|mPx%DnJY?{A^8XuKi)5x}j*D~m$ -tq)(k_~v(qUvc^&yrLr*z%Wb;hDuJfR_{Nk{pa62f8pLFe~JL%+uF)i+O{`mhEBw3|F`)ho{55f$ZeYl3q4Ihv|CQ--wqf>&n+FsE-u=k)FU=5)er_wO_(#ZhKb*HW;vpJl@2-GC^b07Xvx*m2EGZJcI3|4VK%o-n2Ux3-yPp1d&(xNKleX@=*DCeb8Gi?*1uwTU`g<-(gZ -DI3nojG_AM%5;_6^#F%P)h>@6aWAK2mt$eR#Or@-hpcY006fF001BW003}la4%nWWo~3|axZdaadl;L -baO9oVPk7yXJvCPaCv1?J#XVM4Bh=Jh~6N(4YYI%5Zv6}+ASyy#bRP2isU2Zr2h9uIc||$xCs1skL08 -1G$hU-lT_6&_~=QXOCABgNV32_G3;D-yg2qHa~fQx9R^<@B914JvFmj)m*?%sYo0QpRx>V41EV0H( -L>GncSP0HF3rR)``YKy;m5fSQN%eVUxW#IiCPb2)gJ@OqD3*<&qbTfqcT^#mFzgnMep&jt!ibgxnV0d -Mjd%iUs^UZ7m{tjSl{d1tUvEyH{)&1ZRO+EM*H4(DOb5AD53Hj|4{T!KV1qdRXB}@eg#a2KL4)%0Z>Z -=1QY-O00;p4c~(;nv^1zVApii_bpQY$0001RX>c!Jc4cm4Z*nhkWpQ<7b98erb7gaLX>V?GE^vA6J!^ -9t$C2OpD<;+l06uUjS-M0)8LFZvI_8~TM^d>&U%%$L3zBkjuez{Y7O*?hGt -=GEujw8v*G-+&x~{XbsQ3OF|LyIaUuCsgm&-QSYEzXr* -;3akRcG8ql@~whMWdQht{h})C+776Oi?Y1VbUjvS+gyzi@MrisN@!l$ -dQ(=7Dr@tp-rA2%w$^*As$8pO+GzZyyf;goHz~hflz9#_!)2Kk@2aJ)^zv1D&$^y~gT&rD0-vX&<~g{^9V$>+{*^@vD>5K0>ytMp=K|ykU|JYUy-M4>nbE_sT4psMV{t{_rTr``8eJ7>K5LuIjSh1!(k8pnh)aizxXrcR&TgP@p -5esoa5@E2)o!Nm-<_VH2?+gpp*I0x8KviC`6ey4r+S%HKzZnWQUK^|dA7LH>vXJ+uG->SFOLh}564wi -0w#}2n19i{(%#dN;SZR5yJ*_Vk?$z2^+{3dqRw!2#7x3V%X9;nuq}T)KYxG7&-VIO&$GroQ8s$kR@qo -x>Sm^ko2)8}nd}LESm>r=)*P$)s%-P+>|(n~>)JLWVAz!SmzW9|at!cqvjSR|%k7@Gth8xsV4tO17=7 -8>d!$xn1>BWyks4W{m+A_34Fb}iH)So~v6Z?=7uUCGwN$)GvWqOwnr%Nb8df_yKRiFdVSp=Fx&pRTi@ -aQ1j{zD22bpqvl`XE|18i*+Juq0hShN-FcYK?wOTc@9LZQ*?WG`Fc$7GYI4bEImr)mI8e)`>DLO5xv? -L;XUZ`)i!_ovCTf1>C3w=r{=(Hzv2q(}ntYX!(@k+G>Qvz$6?fR!)WMiY||)Z?_uGhM0XDlOa)_YFW_ -_5eMa=eVO83A?>Lpo8pp5vW5Eq~<&D1dC($00jyF0}h#bVsFI5fslQrVQp(5F9_!i@X)5p5Wmu&sA~{ -i8L`o%*Z2EG+=0xXN%0re54fMq;b7bFi6%{);jE0q#B+W_?pt^;1D)pm@{x9T ->_b9JE^Apo%io(G0hZH;~AZryXU)IIuuY!V^~2Y3ryHnG&IG;DRdVAIa_kDl-BAC1HPAE}qLVG&SKHo -?-=P$!p(y3tj(+K$X{*lK8D(?eJzY~5m^haG~8&KV(|cXjHpjp;-kMh`;-Biap4|66oLU>sSS4fn4AR0J^Lxsibg{}p-xZxM+XdXcuZ208Ae2hRb8^DhI?rW? -@SdN5HRKYgKn?f61nR62#0XZ}zM6f`LHe4E%!9qnncS{UC7Fde)|Ku8STxFej$$4@)?o%(dPeP@W^#R -G>3rqv~GN5bDnN!S&8^uZv^m_Ki_&si~K^BhRL$2_(o6QPnevnH)@+O^JCsNn03Cj$VnQN*sE6K?!%1+1|gVIs$2n8ZyjfW`a#x#aSTM<*r@wKw3kZ~`5r(J -{)-fhd)Uj3%5@66y9DW`+D*wSrL3~c3=C3bs6o7I8#lMvLVNI_mrlXpsN0Jgr&KgUJO13?H{S577Ul70kMHoYl|g{5E -Pz38BkhjbUKm3b$fY997AHC8zas^ac(1ERf^(*s -d1Zu90QTNe|UCua-tSjDQX9@)C;oG&4iK&$Vp=@&^)_Fct8NyFzUnk56`6&j70Va8Gt;c7z{~B5{WqC -qO6Fl#2{oTtr(t0zXYahNw8y9=cTI4{DygJtZtDX_PH~W!1mAjCj>mezH3PNi2CMZ!jOjp1~nK3okVu -U&fd`U9;-pyte$(lqz6h0CzQmc!Ok0UU_>O9@Q}QiLf_(tP&|Rl28@AfhA40d{b1|=#Ydh%#ywY&*|0 -QPiJ2ef>Pssek9_zgV8HnELz#tJ1J0(~%*@f>Yz`p!)pb*(Yps4W^#It%lRmtjSFOLH&o;1P*bocwlq -O)w1o!by4VYQ^Fz;4HQUaN@$I(~x0>PRy{x~=0=(1(I!dr+*wCgyjW{qN%fve1_;-a-VYG`8( -brmU6gq0xWD&xv64bbD3!)U7B8HMao0Y6pKhAQKfiX2(Rq5p#O*l_5!;%ve%!zqvyWL)8u>Ig&$Qm07 -m>Oh}G4yk?C^y^TXfJ5`NPQsp)y$t5F>C41RQx?EF-K -ik9vblsqx1rlTYC)FhuA0BrdtJAhXuID -K1jyYB7Z35e5Ib?HkCUaztj_3m-CfCK#$HGXx5WZTcpWSBc87$4f#*V|v!@)7EOQWUs%NLKhg9O8mDS -J~P=+W4k>{F&t#0jU2J|aA2Azh$_htd!BKaoy3KT+939CS~`sB?y8$4lZE=-> -b>$E$;;JV0&PRtr5)7{3HqC`-u#n;aZ0%XOL+Ns}+T?MD&dBd|vE4E$}jL}C1=zkmK)a-23AI=~Chdq -QT5DU3FX;G)nCtgwq<%k{*|hUv%t@eVOhOuSm}f))4NyHiCa+^MsNx;;reRr;dMz#8BP%P@u}!JwGnW -c~v7F9DD31s;taZ0gm4F)0qRx`q!2PaZ#i7Nd|b;vG8?PSIwjW2dCicubU5TPrljd?1BqG3&GdBH1@R -vgg55d;0A6kGq?rrLd~-E1I+eQBM1>b#d8T4Zw{xPtLa+jo;6!w5V6Q+Ba%#(3Nl)@fAN5mKqMzT|_T -gO$=aDZif7Mm)FwiibsC180Tr5Key0b!}T?%DGc}sdSE6IF^CDgqwi*py=&ZH;=b9ym8dj(3f>I`QM^ -#wC}E;rny9iKfSg&kD0e_6v|w+S8+2ODVmelquyiX57F-KDTqJK3guqpLqtX4{(h))nie?!~fc|6o&Z -bFbHmiYbd1I@DqzVr1?}9=5)E@BPNojd-Khz$>gX93EVTj5qKjRSv+cNe9#0^azg)z*H7?H<4a>QhI> -=r>3?s=d9I#jDP!^m%=b5KZl>Qk#Z9d3j7g(vc5F-PW_b7XWlnoEQO!+*3BLV!1E-f9s4H68KK`LH~_ -$o1SFG_qGk3JhIwFe3myn}%r-`vQf7@De5^My;p7&^0tc8E5ImW}Sh7vgrPe9Rt|LcEk;tN3zO8 -iGV$N54Mcj#>z$09n7MqOm?5YE2z9CzsJ3+EnVLSVXRDw%S80yHMA&f4j7)Ah#j@2f%==FhNDz40SPo -{Z#RQU6je2drX#3Zw1zRLhyiyJ{&>JDv42Y6F=w*)XCe^x3_X{K?*brWN1&*P -Svj%8l(0O{F8N%DBkzuysAqt8H(-@xWGNi|ma>aLnAI5yKs%-z3vgb0u@pak{}q;vcsM;~wBrb%_#ik -{2t*|xh~pmaoL(4`Bpz6DT_f9-&Jcknc&6l02^USq4@SDlQITT|rWg9~z=;v02#>voN0?+_PxQi-eez -?V3FVuqb#{5xkbNX+SZF06`?nO3N88KH(v5>rSZX+pGS|o#;WK+cT2r+(CA*Q7RDo_L_gtWO+U)Kiow -b5!EYly)$K%~(td(6G%GfNye)8EH&)d*)01LF{&$NXJoe-hJ0^1U1kkFkx7JMgkm?&nIRDe -3&w$m!1D`7S0v(LN_c-=`?+G3W1dqEU%ftgcsTy#~CX^M -?D;Av9YCd7i#C-J3C1S;2zWkl#iD_aFT!L^?e7r)Z81aNn?AbOdXG|{Fg7Qzy^MR+=yhKrK-j(uoTo&Icdv&?u8t1VZ$5=g1wu11BGKrbU}0roMLrus#N(ZcMmmwh`{ZGHc}Y$c_-t4D -~|~=lWV=vRnT4_4K50CngX6YlSDpFb_cZZCNb^qd~^+&|AYtp7SoH&@Y>pKX{OR -NGYCZ!2=yfNPY4;LVWs64uf5Y_8H0aPJ8~*D3yR&oD6>=)inwbL1Da`SS%rQ -h(%%qMt=nbRrd*#oznV3<-p`Q77ZU$IioYw=c!ClEN_2d;Dnt3gA6*+)y7dQDS2K>lZIxm|}w{wFKYP5>cl!X|s-z-+=Xn*;DxM -MOM&;f)_Tp`@K2kRv*86{P@!g9{pX#Ie2su`MHr=s4AQsF-vz2*wd;B*a!6QubqvMfQOyUFaxlj&G5+ -GQhOkB8H3p3Amc}r!uYQUSNurHrt~bW(lveJV8Ot-?*o)`)L*J!@H2-d@RKzc{4T-SsJ!^(r1@-?6537il$l`sBOk#8Z5JM=m4kdcV!H$4|Z+d>KW{1TwawQz=dN!OM1L -Cs_18#N!(be%ky?NoEg8$l2;){aZ}Ffbn1s)s_$4urx6Z$o3Tsh=G)VsVPKVo6KP>jUfNbhX(Q+Gz78 -`48Df>X4GlPlTx&OvECq)h`niZOvU1mnGX+C5 -s1runArY0TVhJc@J-gd88LJEj1P_975SEtg64|D3!>jg+z70r;k_lUnY>E~?Su2s>lFLV`TYMFcm8p$* -B$$!$zx}zKNY@K0^o88!P*3ewpvQ@Go@%$)cOtKJ{-d?b#0#{Nd{Z+e$H^TWmeHf$B8AzC9_Z!TK4qp5Fm?uCCJBZZ|J -DKsB%CfaWeU*3^7T*=<>CG1*+yG&E=Den6EB1A~^%F7Tj(m=ct-dlPIkCIdkqxYan0GjUtHlOini21p -#^8*kM-0htuoG3sSmf;_0wjguXLS4}BMt~%D97WVAO=-lS+-PcXFc%l(7Lx8JW-GM# -}iu{b2=2G5#t-vFz%7Gn+-~)@QfNpn|dRMFVQ%u?Q8lLHb9TSzQ-@&>!0efu&?n;_*&e@X>W;WV)HD0 -=QF7G?YXleS{-`=I|ksFW8Y_px+7hp&T?0UA#Bt^xN#cte6(uxxu!kk&513FHJ+LWy;Erx9H;wIpQ%m -9vc1UQ$Tv}rKN>RdH?su(#vITs)F8}olc1WTlNP~gMnT0!4>q{3WPtD&N#l@7x#ADvDaz8jU#)c&h>f -44u9BnfVo8e2LNj$B4vP4THY^2-$XGA2a$)RGOi6-P2&~(nV8pNK%tW53VM#`LnvVj9z5 -;KKPIp}#Ns@eDdcBTMZVcuWpHscQ6ZfXQhZW1Nu9p9%(dK_VoOnIz?YN{r^-`5>nR^ -T7vqKn%ve7rHqJ+lA>Bm*D>B0(Ug5m=j+)m~6_z)4bIJDVU36K -Coh=Xq_-tEy1SF5L9}HUOB~~La=z0cXSADJm2O}Vk|mdFvTqAiK3Ha4k-peEJ_|8U<{&)I~Mvi5k?bM -4C(jx2r=T{y)j1IJ`|ib5nPW?R0L`(5&u=2o9lS6rU<_18=aRMibVB=BaI4xRRv6TXPmJcTnwi3&_H9 -yl|79&On52E5$wW-^9fGlv}8_6Ha<+BzjB|S(y?fb$r3r={h*ksRqE|-_B4sp~#Olel|I#<2xtPnW*!QfW1a@3LmXdqDyQw>2YO}j{h|u=!x -1I9y3bxD{IUSTu;VOZ4`5n8RjlaoL`17}amdQ=jJCQ01zP88@au>~fF0%u9)`0Z9E6UX?8tfOry_bE~ -A>^3MkdC7(RW;1_l;RQN)?uHeWBnBrc=!JNy!1NCqI2uUJ|D8mjq_5o -+*pzi3ioA%AjiKC27fKKNpQ6kE^?<_t5dV8Qua}D`q0VO!3gdcqP%m$_yIAW!e){kacKY8${11J!?;| -+6UB8m#hv!E>{)6cMi-?W{c^KK3I@e?>KU9yuI(~h8e*FJLllNkmF>r@oCeI=`pzp=1Ak|)19UTJp)C -oP*NynI&^}m^stFkcQ$sp!@aiySStGInWMlJKEXq1SNw@MZ>eg4OA*rr-uPHFUJKemz#W_l4 -b*2$oA0N-w04WPM|XhpXRN4LdMxX=6ogIpZS*9aSck$DU*x)6!3?zqo^v3Z=*i89cI!(1zq85^9%`fK -`w=6&K{D=%q95!y -R35UyII#r_Sj^>X(RB!qOoF9IRSt`q>*_EI1>^l(g+)nBCOf7n!w|IWm<`$* --e4;Fd(dqGd^f-yHw8QpiUAPyCQ8r+zHX;tiIJq7`e=*KlOo)i>{5{xC==|BjA0xb9=khR;#(-IJ_Q> -FVD0^Z9|_4mwgL1fkAve2t-P*5iTT-8yIS!D+4KuR6_smc#0`}OEg&5OeDu;4cnH|DVyVNo{SVEhA0x -ZxM0OC@^Sc~?(qh**7=ysED{dcX;u$S3-y$+7WK-kA@(_ -SA(szJgYbj6YH3bd1VuPj3}Z+b8PC0eBBH`N?RzAed& -P$ubNvVY(xNN!s2Q*gs?-j;pE5PZ1_7mnj1fB8-IoF4`JI^y13 -@&8rxvH(eZZ@n0;xZn}jHTx4NyJ_tw?-ROOZHP@AtE5{r|2`@qkhv7rJ61@DStQxLXU+Q|HS>=>iErO -X*+!rrhzVwsH{Iv#4{Ky?XIUmbK81$BGYx=n-`^!t$9W0qkp07vhd-}j2N1nau{q~X%HAHJTFZ}f-V> -4TNpj`(qf}$1JyI7X5yLBt{7!U5T4ts)|96tz^@S|L#$v?LS-|fGqdc5)n*)_&OYZ%q_fG7~o^M? -JyG{{hG1vnI!ca$?r|orf}UB{L{bgQuw&AgeD-B@%vmE&z1@GOA=TI5Dc@BmXnEkk~~fx_g@=?H8B@j -7hVTdXIO?JzbnC=X}e`LM`7q!EBun29luhMsJHr-?o%*#ZQ*sHW+{KQLel(Hs;S?1S)6!F2)nPTt1jc -|b54`_0TTR;3$<$Lr;sqgLJ(o(u>cN`E3|j_7W^krO9KQH000080Q-4XQ$5SydW{AE09+6N03-ka0B~ -t=FJE?LZe(wAFLGsZb!BsOb1!prVRUtKUt@1%WpgfYd3{&Ga@#f#z4I$D$#^Iwwqnb&<9H^ywCS|dPT -Og6i!%^JLK0#UU;xmz+UZyH&`ZCrU(&@cNI-H@9c*IvVX@dNaGPTm1Yu5Yq$yJaPg?;kmN -%4Lw*>VY5>6S}oR-a_$SuNK}OUDFjMc2VRwzj4P8*OVe)1n34+F17e`}e;x=Y{W`bcHv}Z>6$K_pP+5 -5)DJU$4pd}bWW`r$)o~|Wde+QZ`G>zV9TJOR+U#q3%e_nm#v!>#oGtbc8$EgE(!*}H*elB)po{uX65H -zI`2mLAeiBrA4;$QKrqIwtd)grl^+p&e){q&lV)Cu&Un-=&aS>{`o_G7w&tg&Me)wGl6K0r;!d)*Z@d -DpDNwL`U9*+(Th@1-v?n-j%sqTo!bx@~t%Get6xIdACWcp7$wU)i^^AQ70g@Do;~`r!bGuxo(Jw!S5t -uHLUhE*HU<91AR=Uo>!jz;ubLe{5$*c$kQDCZP&fC87Jxm)S{*y7cefKRSsVF#Y_ci;267!V(prIgIf -eCD8C_Fnoy=IfLGTJNM%kp@79OY+0?(UVZwoqKWv&NgsG%kb80|XRUIC_(5&e7;rR=1r|+9%B%=k!)_ -Zwzltn8nJNJiaH7Q|9EMxedS&ukarN#z{h7NxCtn4&OJS0O0?>|NNCK;hrihg_rf{)13AZ>c88U<1Vz -(T=afxtb$aiJINJ>r77dQ--P -SSA;qbPZVF&?*^89+V#LOlRiLu~=IL;SpqMFPU$ONco%2q39>AdZy;D#SIJ((HV8I^^Y>M!_2Ft+mEd -ViuH`m8f|lP5wJf>@U-3)onPWEuUct(N`Cwtfv+ORt{Y -V)0$7}3{5M8tZk}Pt8>u&bn>jYBBj-x6Rx9*=EmSl3p2XwgXXm+>VH5^4o{ -MAH!e(z;Qe@EH5P+gMS{z1okmguHrGZ2L#$3*|P)S`VcIq_!KPgpFqqj@WYy>J~bTv>XVzsa;-ltb8O -JW-f-NN>?T)V?WLiuc*R2P%yCfmWs(I_FY^M`%&=uD!_DY?j48D3VDm&*5Z{1+wFmHv@?+89V8614ph -e*xdAYAPY3P=1N(wu@++bSj<*!DT>%l8$VY(N)Ock50y@OWHv%OQvrQtXM6Bg|xt{l}^!&~n=9(|WBc47T&oj5BV#AeLUi_55DdK)#J?6%gh_C1p(>jJk1oV({Rq -eDL95X5k|y*V5k`d;5PaCx;x4_uI~&MKN6J(@lR89wV%8kC<*uhF6K7!2BD2AtG -{H-lybpW#z(zztYL1rWiLGW_6*8s3JJ_;^IfF`A$5;nbQzbVv{H6}Yp8yVQ0vDXAP^l+)?H<*!+v;md -3?9ah0fVUx9~5SZDa=hRXY8oHW=Aeeno#7>a9 -n%Sm=!#qIR^(gM^XHSl_$gJbGW1{{T=+0|XQR000O8`*~JV2#vMhd;|ahy$b*UA^-pYaA|NaUv_0~WN&gWa%FLKWpi|MFLQKqbz^jO -a%FQaaCwzh+iu%N5Pj!Y3~U6X42nhH3ZpJyI4K$+NsGck9vnklkt1p2C70e^S`~`++dH$%t8BTg9^#O -D=62@HEYD$iv4ldGvff@o>o&_D_)Qd@ot;GnA6vA}X4aXuaIHZb{r&tGMQ?=@FoMACgo8%!(ZKJ$5AQ -MB7+p~~rLb^P*A`^eXyf2lQ=-B0tt?yz$_iaILqX`J9O01XC0XS8QppDdz5Yh|Wsx^{Uo{FVtepj+6jbPTx}^WtCFll?2zBU<2j}@bX -r6{?Sl&Sc22AM&_LY*$i%X -I5q(cl^M2!=5fr~s(ySg}h!p{LyO&`p7I64_X-oai{35Bk -mkaeJcB%iG__=W0zLX!ds#vG_5@2fMh-iae6@qx6Vm`MjOF9TD)P+7v&i;7rGF?aEGQDEcZdqT(5FC` -1#&F{WhUB`2%xVd@v+bu(1!UCMbt!Z(vQPR4E3>S=_1nO}f__H?DOu_nMB6lcg2<%Gr -D-_(c?b%kB!k$r;pDTnDTPCp09Wz_60H)R#I4+pmnBAT@Yf_UH$J1!U8q;x!UR -QUsDfTbHHFp^(N%(XUcbX*49goS7CsC1xu1?ZBhtJeNKefyVE5G!!yCEKw)(>q%46R!i`s9&(kCdkAa` -0D5%s)E>F%n(e$Gt?Y%#6_3c0GV-uk3;7)iOWo?qsfuBzfcl?n!@}}?tlL9#Oe-i2RJHwG=v=ypR^lp -Y%C`eaN}3e?9xz&V;cE}4h@|4YvI>;IizuP7>M8q&4CvLe>X|f1!sQvx<4V9mrDIt1sZHl%bd1yGM3T -jxEQ{aKKPB{12Js;EaF$RnuR%ZTc!UFdTq1vb>TPb@jmtU$dx}I4_ey)$(TUup^I$r`QNLIf9!Jn8o# -k$T8MdaUX3 -U1)PgZ^B^q9ccU3Nx`=pRr^0|XQR000O8`*~JVR?BT%7bgG!qIv)T9RL6TaA|NaUv_0~WN&gWa%FLKW -pi|MFLiWjY;!JfdDT66f7`~f|Lariqv`^5DCn@`x{T@6k>y0KW4k(%*UByo5laac2rvMsBYEj(e{=0( -agcKQuHVzIjY(j4c6N4luALojs;!4xYMRGMb}?4VYPR(hpKNRl27`@4|FqO)RR+r{Nz09YHvTp?PA-# -D&GUGfYPBfx>m=5t!lp@er)JA+S|xc_s_HVTR8(lSEOo4IlIk+a)RBPRuy3QZ%(2OAnW|Y{D1BS$B8y -T$E-RPw1p*5Qt&L>9$cqZG*3+{7mQ~OvP0r1eBsZUO+p5s!Q<+b%bY;I)#d2DIyR#iCJzva{)ONdxiV -SFF-~XKO>sgV{0f#izGHrSPwup71Tpdh~i^K7U7q9jRBqr{ -BY#`H@+8q)xL!-y~Taou~SnJia?9io6(ZY`i%*Ioc0j9lky|RukoWL8hx>>W!2a@+WBX^5E5r)7K~A{ -)_ON(>E_)AB68-oO}n(iT#2bgf)^!_OsrdHnd%uN%O#2e8$F4@oHCc*L{F -u~iQ1pi5m`qpZ=_Nxm#qtkYzk06(g8eHp>CBCzAV*xx^RcM=}F*?;@;@Xfa{@?fL}!KXaQm{5fd$^~$ -?Ukpa?%ZKpR-FbdAMf$?JvTU536iYok8}Q>T^LX%KxB-*gNM&WX3{Yz{V-={L8$ohCCAf!K3;M{sDT-(ap#7O2 -k?Myl~NYtJ6}iY#(w5+{3M|%$Vws+7U-JffO1zJX5bg-s_DuD3hGg)v>NBl((>eJW%jbDBU4i_>PcMIbd-QUoGGGPRfIpM)tr}n!4v+~>)=cNGOI_Uk5T#3faO<~;(4E@ -)KhMF{8=mca=$*@3V8Q}<6E<6v&T|ya=}mM8-;{{+4(hg#U6%h=#0~0Z7(%|yVsDS{V=wa%_&>|52GL -=q>?1SE=P>Ffi3y<0^iof+)HP06kvb!}TwQ8qwxt5L0=9u3sKsTImcGyfhLGgDsR(fR;5q(!%lvqC7E8}E4t`X>LSh0Rf(o-HvJ3~zGg52r3D5)7j0UBj-n;-5zdGxF~Wz3!!AMkh|y)?whj^1!HfS ->WP5_0eN+P+(Za#{6JM5>M(QyP9S8OP*hls$c?=+p2=9xiLW3Z#H%Y@_LiM@O(`1(DxE2-2QV!1rE^_ -YH?l;5yVefWcWz8`mY7x^>etrF$3K;AFBrh6tAsEDlFZ@_Au`~t2nki#QTmGSrpn@XUUdu%sRXQMVM` -n2$l~Gj{ehg+2*gSJkOf`Zr@LC+uV>G%Pd}G0?TrR-8O?Bqy(yz)ZFyrCLMRf_?-@rDf=TlaQ;UY()6 -AY7y+Ua1CqcMn`!_dW}1(S#4&S0xGyTi*hUGo4wuIPylSC)J0EFNw`aQLI|tSzgLBo9u(++45&1Xe{6 -;dGmDaGBRIMH}fdn3Yvj`8?(jEc&oaesIeqIwIUCFs`s38^2F$V?%p~ZtT%Zg+Pl&C+S=T{Z$av>g)aC)U=*kx3^al9B_j`r$z_|3dRP|(x@&a -zpsx6@uX$!=5&<{!i4?rL!69SM4ATfD -XE6w{1WN_IkxQLf_Gm#v`xgA`C{6Fsv_*{r@|UIzM#8{UOx-@GHMUsQm1b)Py?UANe9vbFp5286z?NzoaJQ)OozE@_yh$=ssTI*gwAT4zd -XQ}KJ2EwVQ+(u-Vt71H!jA=aBv~8vIK@_vT0XzUqnTf5Caw{DG==~Vz~VL6Nrj(-5fg03Cz0Foz!P;t -`+mIRsH&v?(Zf>$2_`4iZ|M9b=c7uOklrpTq%r})uq2IQZRC(q|~8CyvqsMuU#VwcbXNL)p7xRgaAmp -MCu~B21x+G*yf}GYJ-Z$qpeSBTP(o^kz67H&K4wWi%8Urd;2{`{(vAzj^^ -ikI;S?(kx~u@SJFzaAjj#)Uz|O8Tv9(Z}6XY80w_u-)N1#h-$O1;l|OREeu;$Pv5qLwoFO{9O&BxY<$ -?>1b<~ZKoK3 -{@Cv$zA5p9Ohwv7MCVQt$2TK6$Ku-vi2Yy8t6$d5!_?7w5QT4}|zSG%!3}THV`Ce2`lf;RgkYhXn`UEmrj<1-06uw$*GqL!C{`9Y3ugW -k$i3UP5fsF{Fu;r!7RJeQJP7)s;4O`%y3)tA(Ja@<|Mt;ZdcWaxc8-{0QfL6UH -3k8ZN8RRT5bCs8rDT`CZf|L^ae;LC=&4tbTP`34?%GIky14-14-W&%p2{2Dg9}QQH(3#Ri-;>jb#Rwa -|vj|l1-BeO~mqwPaa4a@xW!r9CI~QPgDlq^2)=;TrAW2<@e8B|3X;Ih)RS@GsXWH0|H!~0u1}`=LDRDm^M!7)An&}29CjIfIm -uGvf8j6yUuvd!iGOk1#Pn#=p1z8)Lf$=LwZGc)=TiBVtNUL<85VPwrr8j(DM^~fO)3*;?B^ziV7g<2w -7K~;52cfOcC42DN#(g0ZVd#P<#NhK3&b>%#}HfoNEf_T3il -OFpR4hCHt3nLYPxdDnt?6aN;ki!5NRFVWxG7${{1t@Uya;_!TSJt3JE8%Cuvvxhi{dNJ#=aFer8zU@> -iAdBEv`A|EGJED#o2K&GA*&r=0sN8Nb_Os#YzK>ox9#!C;~94~IDV*oY{Wov34){y9%RqMD1cbA1ml0>;GkJzX*Bxt)0 -7COmb+Sw#ALmgPUBKo?dFo;8l7(3&U8rU*aLKMY1B -OnXOy=Xw9vE~Nkxtpg&HZ{B167Mu}v`Q1ByVMvbiKZaNBizVN7mhm@wik;N(M%6i9$z#NuTF>O?qvx3@(Ri}D`yexccnc{aa(?v$CL1tv3%8yRTA2}WA->2=5RlNGB&15I~{dX1D%c~_Eyb)M%b -Wl;@X9YS_T> -^QTqSLuf1cNjKF#!@-n%~w{LWD^|M}HDBtGQIu7wLpv}=quWWA*a>Q -wsxjH%D#k4*Yf8_IuVXODP?mYY$4$>Y&hRH*qf -tr84kSIHca0ZBCli$Iy|Pt2-LN}C30k}gijZ6c1b+XM5qpnp`vYCMIFT8=*=sXvM^!p-0146n+sbT(2 -0p&&sUh%`9kwx2x`O;c{{BeU(H_1Sor3|RWSwL7LS-k_D_L~ti2XIVgo1k -$ITyJPFr)VKw71{za~hJDYk|BFI`(pK&l5}@WlX^e!|7Ew}sj^MBXAK0~E?vvMql%)_0$KXlPIt+>_E -nUpKS)NY@#NxB0yc}SpshAA#;8&-q;a1Xau*`lu#O)9p)5j)9VS|$LK75-tBaH5Q01^3IV0uGP9XKpL -vjd|pWhqbx*fFlcWZ@Gipc9%4=Jfs}>Zk4&Y|IYzv1B1IJII87>3K6Ih4<^{Z$KRBe`W(B0|V?*G+@&_xL2t2T~d3s|+&OjN36IW}gL@{celr;Jh*7s6fLF+L6h(O|eobIoU~!k -8GBNd+~^j$KxqUvXaKHzgh|(WN0t=O8$oEre_%oqBnf(ik0d(Gw5~1PulS7Voz|Q8V*8?KIZAyWDjk7 -H8I-72m;`CAWNL0qx4%UCRr;F<=ws=>#J$V0vzloh~4pN9SOwPy$VkLd&5g -%Q{%P!vnc}70nlE`$!G|^Z5#=8PT|d{qcK{j_*Ng2GwmfF!3v7nSN`R+1ouhq*F?XSl4G1Ci59=nSyu -#Q)DrY1qj=Ky#pmur*HNp#Epk$o#;AaVY0lKd11<@Cd+q_6lE-dM>NbOVKKLeLFc -J=hrxPE24e9Q~tn5QzAuMkRee>W767$?}4G%~9ptB3dvZa5gvlFDg+#-9TtqvflR!6A&(N5~V!acFXe -T>_2Vwtdv9r!6GvBi}k|0hz#2)kRGuWXhn?p+;e>F!&+{AdgczAc&uxMRG}PA2;dUAW7pwobQYabok_ -^3YoiCrQs>cqV(n8E}?9DCKDfdN^7xWT14V^V~!QDq2q8=6JGOZZdIbJcbL@~ew5p}dM533&p^Eb4y< -z*ym^l1X2z*Pu*%?pgM=YZQJZM|bXl5vWqdNexwrS^NuYdGwhkF^|>(V -@e6IuPpHOU~lJX56pi&d;c}O0jTz_Ul+$_WX7+j8{mm3SRj`D)E5m&YNv8vLq@LC$}qkd!19=`sTxC= -|LO9*VJB=$nuWMqS2it&cY<8IbvFP%8oQUjtU=Ftb`pG%6Rzekj_8(94^zU9-V_29SHKiF@gG_jliV4 -P7)VKZtk??-FuXLIEd1ffJOZ|x4QfHp1V}RJ?&e(JiwXa026go;y=b -aY$5ED!qwZxTSa5wC=CcsQ0~pwL2;>BRFd1~RecByBhl8)tXeEOiSZCu_A49b!IlB2h4cyP#1An1_)} -H~%S&1{iDgZ+Y*ml_K%JeO_t1Mly$x_o?*p+`jQ~rN$MDRBDoktvYlRX5L%x~`G?!dNpHdO&=-3g>$J9r0A2i`itHw>QBCrhBIqGnOBBp -cSIW)Fu;cqSg;@Ab5s8~J&IT1)uP;fp<<+WeqF!Uu0+Y^&t1T&e%d{?0lmEsz&q%ky;HA&4ILbJH;L% -z7)SOwzTL&ctCQOY;+H+leBAiPkBTUY{H>C!jl9_XuNrq|1JNV5PJ=^6;cJviw|tVCnwE%K65-eqhTm -{~D&vl`EzL5CW}#w)%jC)sjrMpA+j4)e@?*PbFjK7)2+Z2Lt6Y<<>8?N_+NaY&-~a*Bl%rrn90^3nHG@%Y%B}&!l2n4UiH;>qx*OPSDFs$)iAfFi0yV?aNJ0ax597GwLpz9 -#8#Ip?+Yz4MX1(_k?*F6lULnD2XM~SH=*=c9#@#(DU}e>0jdCsmqn@plGY!{VB@?*|%LD*q;F0avNi9 -C*1?jm~(3+t+NU0&oY+o5xk{hxvLhF$kseUObsGR_{T6me*t${`1TRhBLX6A%XogN*!K`$S5+oL}|eK -OoQeRFhhd~o!`!OQUY-~?z7Y{_x|!|LyP5`4sm4# -bxs8DxGKf;-{jlme)78cRvq6gba3e_Z~g|^2r~b{&6b^|2Z%YhzSzc*!jyMsinkewx!EyL_WYB#+Wfv -z&tWhG&+5aXj*kuC=1Yfl003xF95sa8g(UyJ*nYH!6wreQ7XBc+188W{li1{*X6L@9FMNr>v(`&(q)F -kxEv20&6Xgh1t#D6yCagp)WOXbxMah)D$kM+XFKD)THpeAV~@)rr?MfEmqAiSX||lVuci0irNOHJ)5` -+OX!w^Swfkh)dxyr!v*>`}T!y+DkUx;mtJY?$ya%N>P!H5)>+%~+XNu%#@?3XFLkvV7ILXpxZ~UQ&eQ -#kIr#GyG(|==izx92L*(SNSynw>aD9(wUi=@fNN18JTob-5LS9{iuH>~AkjMDIR*=z{kihOy2pXY+ve{c{ND4255E{~kH4CH{im}J -|7Xw{I@^?q?D{G$~5!JxZ9Z>~CB^G(4gDv?&^_z6~)4tN|$d{m4{ALrVDig7L`FNK&52Q&%#RBc9>Mc; -vTaiA@V942A8gb$Cv?H5{3BRosvdugbYMLA4@jQ^;YDVI8Y3@WM%w;SC==fbf&W2(QsN8F!6M0& -*LRTHWPKx-e^s%fR+Hd`E{WXmQX22>l`l1A_ly8^Ey -q>|vfliqHcP`R4VgdXP=W_wF7QX>d^IBX0`)c#C}f+O`;zc=_af}%nI`;xSuZ^o$~n^U1Cj`Yg%&_1V -$lw72T4A1S`Tg6s0vdiu5ZQ%UB#VvT%0XqUC?mez~(X9U7mtO|v|2ykNt1QXR?M(jEKa{_#o;_2K_SB -Z*d2Xp)ml7`VvMP^!p)k|;`Ju-vZDYF$?5D-`<4`u2czf`yrpJ2LYslx$($(NxNhi&8uTff8&vL5@S! -_v0hSMZ23~*WH7Q#PhD%n8?$-MLBY7cfbi;0(w^N{NWT}MTN^#FqV_S#G}2L>a0nel!>uG?MXRLDNUJ -<)>UTq)V@UR+#dZ1tB3GaUF#?V`4#&(DEp{-s)YtCq9}@trQ>{VUzO+D!}V9%i`b5=lwUAl<45N9#It -(rKK4n6BE@pez$8&vf%}$9tUh#AG^-+&Ykwt3pAMkv&AG%VfD^pElbndVw`yc>mmwfyw!t89Yp&XC&G -dZ_pX`q!yvv-7`^cw|0PG;zmQ~#zz{5CiLE_O<)kJSGjI*W?nWU>kW1y8nKsyLA2C)wHhCJTcfqI?nk -WCUW7iw{$DWK@*=6Q6cHU|P? -l~V|CAg_TrL*eNcsaaUunId%w=Qhn+m4OLT(}vsu;mW^f>Jt3xL2hZ-5UZa|#14EIB7CjppZZq^`zom -H`EY7p7Ye1HuZk9!>0nZgjGkHl(tG856X)(1N)HfwgV8vB_5g)AhB$y3t^$B6e&BQ?`oOw-zMk^6@Op -qcAEkie$c;P2wWaD!h@~*;`8Zo_Q*!6CH)b{bdUhNq&t3yjRb6(%OS;;=D>J^CNXJDWBYQkqaMOk7|2 -+8wV;oRyGUG)|6UKhOzmZi(u<8lJ)MklM5`SLDSDLm2phjKDn0-j3rr{S==aig%{6*KGHVJeNmwUjF8 -X=M|sy7K~%Zd8ydAV0-cQYgz~cXgoX`G**1V$Yto9C&@nCXOR{hg=!%r2ZLOGYD@x%cy$((bn~}N)N_ -)}U@2H$zo9!kO3wBk-qeFKqEa;fZ6hFa3yA&@&fKLS?jPIgL{V3!CvbXGuZgmqLfS5=E8URRmhfaNad --P4RE8B63;&5ZFj!Ap!Z7n0Ov2m9YEnr_vls9(D<~V&@F|0n0?PZu2b|5t45(nhU8%n^`IJ6q9TD%jM -kDTVp+ZppP`Qp8U6GakiRWZC^yaFJ)%!`EtV5|n?a%DJDaPN{)0}=(UsaA{6QA02{?oGG3SMJYohC3b -EV)+MKlTGdlQyk3z=@_}9j3>?Vq-{~qHg0_zS+`E>MU?2Cf+7~aV{OBF^W%=uTT`fMqdQ3mM6Z4eWI| -;!`pgiMET7LB7K1=(*&dw~`Q)?gpGnIQa%LfEFuituT}IV6o@uG^=L|%#(iqahwct~_S1A==XHvA48l -ybr1AWJ*E=ZvQZ_v@~mx*ip32oR+k -h1~TVvq$e@7I_NPN`U2RJ6BjFGh3JcngrKXc3>sI2j~!p -vX499gL(C^}-`#w-bKp?}%xi{h@UNq}SuvR0GiYAta5nBq-4jUbfI%;!w&G|h@%KZZ9k2`hSElVfdrpKI{@Sc#*h=M -e5=%eh1h$*HSzrG8&1bQ#3VmialnmLobRX6)Y=a~=nE}7FFObvvZfvSIOU!G))A6s|;!lN^JA)wNFhe -I(Is%K0uJ@a=VIijBh0AKT5oQPlFO1yo?08bU&sM@iv(To3KxaOYckA~tfIUo4BuEd{9zS~IJbKiqYi -7pwU4KVZYqI0Cur3Nk7RFSv1&dczzayFfYDKjHQ*G5mW?7nG}*>5*DI5vF -8{^Hw@WeFH(MpW4S53fBDJG}%=+f-_aj!4q;uFtF8Th|MB&uW#*aT(15hqzVH!GT@In9Z^yJl}z3}zH -NOc~5bI`Q|DV6cjo~Ia3Xl$5f-cb_QQ>RXf*(>~~LQ{quCB_ -7q-wtkAJ$(JkV`?>ws&MILoBK$QIejaP~ -X|$~H%tr6CP6lvtLFUhJR9HD+i5mgEBE4+t` -Iai^WPi6^d9?`N6e)%470VHr5dKIxtU6Xxm}@!-fD8cC{v@17wyFJ3l_HMn=$dh0QfE!?Q6i;`^B8ur -A@R(F7P>$17|0kWu)A9QLvRm6&J$|m1UsvgDqkri}lNc5` -q4z=6Iw2oXZE}g(j?y4bA$0ki`5Aq~;bSKdm>^cffw+KwK=7HL1ZY|Ru8iYUdu9})mQ3w{m -gvJs*Z-jUvnmfy=A;bN3X9?6e@T^y8k`utRETr>C-S(@YcO!B-@gfEJ@NR6VDtvJE@2F=}$$b|Gu9CexMHf{knGQVB -YDqLOncak#Ng{(UOi3ax9D_Xx9yDmUthr17E_jp5!qmRsw9rlWh-rsTc2QM;HEPYBpy%F<|CvaO1bMn -tDt>3=QtqkBvGT4O~^!>oaTD(sA7Z@o3fv+tdy}@6aWAK2mt$eR#Tgz-!<0&000>R001HY00 -3}la4%nWWo~3|axZdab8l>RWo&6;FJE72ZfSI1UoLQYZIC|?f-n%p_kN0oNfSaGT^#&3xEbPROjF=Yn -v@>iRX)E}5U|*-@4dU+`zfWZRZ4E;RmkuXrCK01=#)y*PTCgiNtgai*qRC`)^lLA?WpfGLk#bR$D%@j3M`r>@0+DqpYLHu;T&VsYdXJ3XCLEX4~@O9K -QH000080Q-4XQy*o!4>Sh=0Pq$703!eZ0B~t=FJE?LZe(wAFLGsbZ)|pDY-wUIaB^>UX=G(`b1ras)m -Y1p+cp&4=PQWW#4~XG0aFA6qfOdbG$@*&Sz44t*{mf|C8=@z_dS=C_>k=)lU)Y`!5W&v?3;w%SqSX+O)9LObQsZr+65Uk=hR{EtS -|Wc#dtDNStl8+JMfSPt_~Dt&8$G;>g6TeAay5#*!P%nYLPst2yEVV%%>QEyV~!*|W45$uz)&)v{E$)f -(CwbC&>dtrBj7LCxNuf!@Urf7v<(c$>ag?qG8^(HLAK#+iNurOf`(_{pj+JZHeKJ$T07c;6Ji$R(#w{N?}uZWBKpSQz`P -NDD;+8qNb%yJNrkyet4=P_&!HlG7wzu8D4`ca+0i5-`BMl><(N_nda3{J_X!95aKA=VtiA510*yNZ_w -Pv*}(GtiIj3wiz4oEz1C0w|Xa?*teFV)zrtX+Jjk!aXzu0jI9`$czU69-8yichK+kWGUL@+y)CCzP`3 -+S}k+IaKf}32-@zNlP_4)6_-M8t3Egr_b4M*GaN!k*Y?q^UyT!vjQcf;vRBC)u|2*nt4|DJ%-ihN1}49 -25zc#>OouN7>qUC=}8{<=2dTMJ00YN44pYscBe;DV>bu<+Q%!1o*j<8?kYVms%p>!QdlJwGRxz2jI(y -_aAv{(0%tm%+^3#>fJP}CqkP}$rMIi8>((Vnau^5v#XDg#bD;O$>lL-E>TaokWeD-;NAZ#juImR%OK< -0{G9xyV9b&-o07nEqX~iee#VkXTTI=YlekudEz$2QgkX`^g=4<|E>_`HX2ycTyoP@DdBzfFF131VrfC -?t-nMOXAr~7TDAW&>UGa_P3Gr@XY)h)WJbO)5VfC4`;(U{)FPs#Xtm7@S=OG=%AK}nnKYw0>;G>y+2) -M2e=!Hd}ktht3cUbXG_3c!sEUD|csJs-)F=uyl#vVv`4KWBO_`)3t-?>HO{$rq?Ff45f)~V1P~NB(2P+hGMh&(OSKa(@ -P2y$cJl4H9OVQTm2F2PEmI0otnJ(X4=7f^Ae37rSc|^IK5R;T0J|$K-euD(@?__)A;wtDu-@I?0ZXv?2w=%(UIB6Q*L=ae5ew$8QQb7HE*lJ>|L|};alKz~sxSN~E;5!7r!^8IfdiwB<1jtM -EK4B(99f4Y&>ar>4kl%v#}wyVE7qQOrm&hcD7XHQN`!i14Rj!Mhi)8TVW){VUCAuqah|!cVLYCLFoct -h)UTGDz105+hQ>f|2H^VhL-g-f&zlZIDRP^l@d@6aWAK2mt$eR#VFXU)|~f002}4001KZ003}la4%nWWo~3|axZdab8l>RWo&6;FL -GsYZ*p{Ha&s^TXk;cW8M1%EPHLjJK2g2gZd8@H#{2TBLQO7!+5p#otw -ptQ6U6A#BWQEWR&Ofp~CTG7-92y#XA$#_SsQjh)12#EKg--GX+9?)AdPRsAVDF~|r4H4wqQfrb}&7sf -8Wx$b%^eVxBYT%cyO7=Ayar$m9+YC>)ZmiAJjHLcgwwF$fDeEgftYr!HH<^ZMwW_#;qHZ!dt! -$<0GuzcViltDh<0>pC@aY47(iNPbU^ZM-_6`;eDPs60E9#U03!eZ0B~t=FJE?LZe(wAFLGsbZ)|pDY-wUIa%FRGY<6XGb1raswLI -I7+sKvg`idI)!IB|!Gn;*CK*4&)lc15!?!YmS%rqJjC6-#1rbsS}Y*}mg-}jtT_Y2kCvlBiTYO?Cosd -K+nS)97IAK1RFYrY>u+sK(vuiB3H<iop>|cHix!9m53Kx`$!f|dE(VDb~Rtj -7(Da*toe%F&3pF={+@SbkH79`OIU1Qmr^pbg&)7{d57~B_Sf=8!Jp4Ruw5xRQ!`)%R@OD+W}IYwDI~A -h?!tznknGe}W6g^hP5;pi0}_fSZ=8ZBkL_4j1aimOv23au#)u|>#Xd9_0=FA?yBf&HI-ENkIqUTf3?K -k9^t=SZvr@7WW;_&TBi^H#C5C$NyUj2rB{WdR@Xe{g$q6tVv@sCzCi=KD=qF)kwdnA8|O -hIz$$~o^BghR0=u3kRO1Tq5@95imqnTH49@QnsUV(An4T`fsdi -xnxZ{FbUc!50^;}g=nt+x!n7~OjxwLe}KVpCg2k+z$MI7CaFEJu9Z^kYOR1Hy@sD-4T1-sOVH5$WBFk -0z-QFvV_R3KwLRyo9EM)(#(~QlHR2uH3TMBzOrZ8U%ZEU>d0V%IwL~uqy`vw?{gK`%5Z<;?Bh;#BLWW -k`A+zfh13IHo(SRJ!hGRJ}?FWq6u}}+#{=PkRfYMIXVh~(rH-VP!v1!i@^8fwNzCXA9qgzYr#Q{4ovG -rJSM(%|qzCs^@3^hx-Y3W+H*%EXCwu)<7hPgQ)!PSV?Nv}x@;qJypBO4H`_l*mZN^K+AwqeBWV2pCZ( -L#f1=BIABthDoB=Ob@uJ*Q;2iN*;B5ln;AZf2V|uv@kOxj!ETFn^EkN(=<7K9?7%)?I2@ED9t&zj5mN -+4&>*AsR3&sI#Q74uY -Ll~UTbjDew$zCn}WAQ8kC-e2*sXfYZbz4P~gMkr?qJ)v29HY+q`3p9%>_9V(R{#sgV2=iA=MJ~%=3fV -gM(OSLShL(w8cBbxYxR^zD>GqbG&%Cu?h{88Eh@i-!6+>1VmbjN&0lLknLU1P+mD=gFW+F_j!m`M!@t9oU -23q&7X8^Fs@@mC*ZT{Qe&Zp`Bw82bd#Oehv%d~HWXb0RbNY^&c2dcPv9AyfgHy)b<#D43G|$OS;{6HU -VXS>$ug3V^U7$aa;-N92ciZsy?ctz@DkrK!0N(_Gk$n{VN8DyV5HuUstChg-?zxo8JDU)K+hC(Sz}C) -XU%kMxnp6(B$}vgfeQZHD9njSbi$3+pCp|CvL_9*dySrD5|2x0{2^lUH6&eyN`Osf(0;{IA-iLbk{`- -47Mtja%@K0YzaHHwyBiRSzc9a#E6P&qcb~fE8kDG>{H(9KJgg?OmU?B{#@&}9pHbV+~(OkEjccaci<# -D4_?i1NH5g{TE8}O|b6u1_OGL(#axY*y6hOVcHBeh1-*%V9G7PGXs;H6Xuk6rRn -bef{?cL42A8Y62rT}~(;#kmMKAXy_+6?W+U=ik?<;r)i)=9C0|UrsyOlC*j9T8RKYXNOs9TzxZSWjZ}9!MaBws?q11 -1JKL*Pn1p{FpukbAT$&BeO_+ouUy8I|Pkt;9wnwVvjIv>6)a;ke>I(>ZZUIEn;VPErH1RUBwn>DPX3g -tpedLj&V`L&;|JR>p5o1BpZ_F$335#C{!^LrCzy-fATh~u3Tq5Fvf4~XQZ>-m8LG51`XV2rlXSl!r#X -v&mjKp?SAFrHGb2^e_<6l|*@e=9gTIr-bc~CD@2q0>Sua>WAp6rN&?IYO}k90f1L>SbLae@@-5 -*u9X$DbVPL+JA_^(p^RDABc4rG);T%Rk=QIi%gee&sD>3sBMfQ?7G~k?GPABDR~&X-4Y)j5F*nRSKcW -LMK6L|vN13v;%-a>;UT0{brN#|6+3($bKp$7g4A?hiOcUwNaV5{y3@to)&f8~97I|ht+Klm{yvSH_JG -2Fp+KhY_e0jruIC4lVRibgOFYkfkO;C$3_rpkqJ*>gXLS-|mW*LFfbwp}AL4E>ZaD#TJW_!`^#}n`cE -WO08-k_RSTvZ{x`lSJNC?N&9c)93V=S|9ql?~&l^q4+@^Mml3potTt_DDG|_f+s;+(CR{K`^?+WcgpP -U=->7vIynfd;_*7wu1(|+J59E0H4=c5PYKG>fp6Z0FP8=6b185sC+vpqy2q>a)M_5*w$AlHgv-3e%fBIZ-1NKCS>3*sv>$a -ZwD2*P`eHX?|2#NV)eaYlXtcI*hz-in9}qye)7sMg|<6Dw|RLVDckQpGRqKKuUXiT8hx-WTvNUiLNS< -+=t1PKgQd;Wd9Mo54f(`vQ20%$G!5kUXT;FF?B%n)$QzzSJIeZE+mYZgL_GpQ&ip -O>3*t{lAkJbqvR*tK!Jf3|fW9m#-~IfSs%I60b^op)gX-3o9Qh{mPU;U5!+(D3*88`zV7CX|OH~mFtO -kS3`2+HB4c84dqf4xOC_%*7U&=>BFN{Ex%Le^GL?6^FoDDX;1CqP7cY5u36Ii}c8{F5tY>=8@CnjuzU -t>HUMGXSLRYYbgbW8S!HT(TdWP3mHQ>!IVQJG5IV8|XGNaFIV;ZyNCEn_Co(Ijg4WVoHDP%8Fp2Ub11 -zJ27)##Bdx$|KtO(7Nla#yc6|&-Wb!sLg2+*FPdmHYhV21px!o#APoYSE{=5l&RL`V+fe>gxVb7W&JN -=B_p&F+J30P=$*>miw?82yc93ro(Wa8-@LOVykAbl8NPhvgN-<#rISs8V%wfQNeJ!19x4FO?WuYkHll -U4#>O|3lNKwMVQeH|!0F=uWg3%dj-~1;GJ@;VGX+H~P(ffaGd~(1n;qmnW^)}o^O^MP@jg5CYWDtO%w -AJ&_8+gn^q3Y^%=5bHv;VxPcH=+)j{>!Mu6+6xwdmhwH?J;N`_I?M=PTAPAV6v#@d-|WOcimjDc4kCz -<;t;r~p>tl<*);Q@yM`<3D-Tn=WDJY2nPOye3Vqig~5x{~B?;Qt0-dbnBYJpZmD5q3uD~!i`e2agRN7 -piyF>W1n(6s9fti_Md!st8xR35{IC(8~MS5Ss{~yPoe!rhy6Cupp~e7uN)KxwJ!>2-T?HA4!f{$V%(T -P)k_W`<2jd?DDZ49C&(|7+)&A~O%3(QVsi|{Gngwd&R;gvsP<33AXU8BD%+tL!8V^vU%srtpH~;NQZ@ -IOV`2n47J+7L>2K?{5m*xt|Er?IzHNt?Pw~No0GmvQ93Rsq_Yao*cVA&#@DT#}xB-rj?U#Sen6IP;e| -I0%?CoD+#gfWIKFMEDq0HBAa|WLEws@OXH-1g6(zKs42dQ|GHz7VZKL;$t%7HAO%qbcw|1_8{v?>`CJ -p_7mpc1nHuxgYCd*Eb?o>o_WoY=!30#BS#I!UFKcvZ*{1-j>p2Ta$(Xz3@>W4)BkaoXIR@HO?$2fXMI -%U@!pLN?Q>CxM4qh~Z`Pz!zEFauA(Ls8I`=zV<*xCT?t8A> -8Ov_9XqQdzK5i-k$*nPL)R3Q8X3@wj#$J5qQ!nK3ZiNYl8)~bypHid -3vRZdUtc5)hwcr>O&zm5xwoH)_wNmu6ZIYP&D`m-V@6@lFZ@dj0tSP{v@6_8@qI$Mj59f7ksW*@+(`9 -JV9E%tY!T6D`7w(64vA60*Pt9puhjmP%TF@&-eQ$qv2O8xE`^8d=E!>D=oiH>$&IhCvi_{WkdVz6wPTT -xOut2!&H4@D|W&BQeeijnlq><>PE1s**uZ`fl{E;N-(?W?~9vyiP63!)9fA4&%lhU@S$r#6c!-a%5)L -`rZ>tOgoRp_PC9;H{21b$(*0jR+%zZ&q3}F-JDs2vQGJh>RG-q3_FOA(329?P%}e3`dSYa$@Z>P2W`W -@W}@D&uAb%&z9V>r8c9D)0+x6)+IWjzG)&Zl%mlfUqh7OyKKv3Sy%*mx+bDT^uShlITkIwrhnZ_n~mj -4TIsdlMSPXno%zR*L?;n+`AxM#)zN!(Y{Ea+jR8ktPu^pvX=5)41M8A|D6h@3*Wdr{cVTb#Gq#Q5yLH -(2hy4heEjtK*Ko{cs1j?=iXV|%6)l*rI+}s-st~?GV`)b0SYiR7&btY*#89i|GJ4JD7)~TD1-ME8flB -p)4^g4|1*j5X`_J)066=a;aDN`NrYFUNqTaqWC3v;gM8;B=e#Pm-`EzmI0R?S6hZV#fom27KHbZ?m|) -m}Vi8X(gE!8G-3yn9h$@O*5~L(bChPbcsoD -Wlwml=Oz5hetR9^7jh`!?~l-yb+1Ia=gy~D|14nm#D1?xn+X#iw&i5NhDkY4-tv;!SC7A4>kp}$oj|2 -Dj&>TV6I$63GNE?f41_6pI^qCZ7!so1_(*J;&OY{FaQ$)SUFpvC7@r$~ESNHkzX4K9#NDsMI6X@uZ^S -a>K^Vr9=JjZ&SSBZ`ncDb;y6P^AEjTU#CGxU?hXsY8F!=Sgq{RdU{8biD6*DRU&*I-dX>=pROR0~;T| -lkc&A?cZ*rM8}=JQvN#R~vpHbt?s8{p`M-B|SzZ=6+e&geSse2|^UUn5Sy^+TfGHG;^FnSmncyXa2`}rI-g -o4L7X>DY@k&PVKG|$GNfAp4zcN9uG5uY{b?))~C;H`IY=A!7?e=H5)9*nl(Il!72q&g;NUacHdWSM3# -*L9kF@q>#6;n~5FR3gPUsTBrBR)6AZ8NbV_Y=&n-E-hTdNoEnN)H+hkmam)IMtO&a -}o2(QePQ&yWw7X$W6n$&Yw01Mdx$l3*P*V`NA{KwU2h!bKP5uF}pphRIOp;DXl|xm8*2pcqVe}@k3v=?A~m}E~$jLN~b;z*0S4$g?0Uu*f$*=)QNW((p4DJ -yBS#D5#}{1kLwyMso|=NIP`K%Q#>vN#%Vi76;3Tpk*=WP>Sb8}6Wb2y>MVAtWE$QJhH0`4M>a{l8Q2D -D^T}uM6F`!)A{AJjA2F`G144R1m#jZ6!y+|BnEBC*JT=27hlta)^O$NXMWy5mRk_XW4=4B^#1bnm{-S -zQ-M!gKAKLd;_u_~SpzIg|h!d);NSIXBJPLB>1ipjK7 -UGY=P^HgBOU`7|N7gIGGE5ZTj}*a-WAf2fi+s^>PT`<_dTs%XZ_Vg&rKc$?5MTMqNw84e<*kqQFeN@d -aJps8*tNTjyxboQZ{-!?>^EeS)8xdEbwl9Ar1ao -SD0NlOmd3cbBjm2Lg2;|QV<@k4xaD(&#!_drwMrp-l5w#7TGZifIAj#v@djYXgF=@`SsHJBb(Cu;mch -bx73~kzB49t5VvAI*37u)P|JxTA-u}j@nzJXy33xy`mb}MwR$)gH=Kb+ -gcyeHhE`mOF)Q)tlAaqWJ*0QoQa4 -IMrx%3cQUAiStN4hz37@NIrS%n|VBT!j-Mx%Ugk%oEC69z`FE7gPp#FC4vv5c4*;KBQ<<$!SCDt?lA% -Wh$IbS;ySXFE`}X?5hPWTKu8`?6tv$-*sZnk?s*9Ih87qcJh&WqzAg$~GoN+3&V;`@P$tLI#MDMh>ef -#?>7z!8t@#L(Uo?Y*6U{f?vX1(y`1JVfcXOKd34ul{nc126x;n;t?p}>KG9)bzDyCR^axnjQQFygPh2 -^Ehn(^Ui?p;{zpMmp#P)h>@6aWAK2mt$eR#N}~0006200000001cf003}la4%nWWo~3|axZdab8l>RW -o&6;FJo_QaA9;WUtei%X>?y-E^v7R08mQ<1QY-O00;p4c~(;!*Dhyc0001-0000m0001RX>c!Jc4cm4 -Z*nhkWpi(Ac4cg7VlQKFZE#_9FJo_PY-M9~X>V?GUtwZnE^v8^k5A0WiH}#XRftydO)MzL%u83&QBVp -_Ei6sVOHNga<>D$Sss)?-d -#=1#0Q3jD`60`jODge?9g&R&&}aY+pus(4=lObD$i=c^Z%$tS?il{_uk4R?Dn-WL@$5@p9~}BH|H3X( -S&A+D51xI^+oQi@|M~X{CTD`Z6-CM8F2Eo2a#?fs1258i(;of}a`0Pr&A04_JWWJ#a0nnDWGu2$B&^Q -h6|0tlovnEc|LBnk`w6JYJY#Rd6BZ!ANDqz1zXuH4yk;vvn&lO%O93ck$>uT@OgzM5T``%lIA5(($+K -9njjWbLMk5IU#m^c=KC3uDbAVsN)7*t)yds8|RkfbJdbQbXLQaf^d9iqvDxmV!hs*PetDB2sV3xpt%u --R7tPsD{vVdjIwv4ZVmzd9h!<27WUNF8W1dLTV^13NxC9}nZmHE8d@InBVM3z+{XLaQX%5Vnjbpb$BJ -Y%D?8+Lg!VsFoGE^j7)?d8WSczVyyu6|~Jy1aTfVFJVi -WW>X|K>mSAi6RsU%Iii5kAZov7%JBymU9`yy0S&h7lJMFdr@St9JUt4N|v~hC9szOmQ=1}#VdN#SyXs -%00i@*V)HtSt2|Fj^P=3^pVhJuJOT9fg!_z%7b~4uNa@jOb?vBJ)gic2$9FtN>s -}{Q800y#dT4LtYVg*ss@>;VVU8Y?U*d%yvS?VKS6B6+74i)mui%!DVxoH3!qybZD&LC* -f-^+mcf{wFC?Z}}Vzq8>)&OuKQr_0&E@;Pcx*#2U=z#R;Mh9rK6q%AzRElRMwc)bbM^Xf5eV(kMc!>v -_&r265U(LXCJqHG7LY@J`lDDPQ0iDYSIFOzJl384c6(R(0{S0iDKhIwQ#|sSX1@)4}^vr;>MXj?=)98 -x?MJ%4%!kK_g6Z>qUhSV&mrzp+aGU^2b2e5R-Ilv6r#6G+zKutqgQkRn+pno(UdU?tN`Ab%AXGlIfru -6W5E!HWAU=bt1ERpC%VYiJkUD8TE~4`vF3us!{C;}C;IE0+KxXu2!2U4mamG8Yl*SJZJ{1sPi{yOWXQ{{n;(yFePuXjl6KmP2fzKy*W2FZ82k7cTNB+UMfgk{pmSTEfVQ~vLAk|? -0jDDFv^d%5-l@(^W!F=MR3S%D<|4bqj;*X#+Ojb7fKKBw1gPaAA^Dv6y6hs_F@GIjh5k*fVC$oOL6%# -fZKu?J}xvq7Q&tTFPd@Agbgj{egGg%3E6C@yZO7R8PqC4#;AB15z{`Ya0+PEvHPL>wTdxTU>+2}2-+- -T1Tyu%r&Kx|%U^U`+Z(-Em`+%{^)l9%A3i$K2%7XplJB+JM$6B%wsW18VWhP`n8TmzT4YUX}?qTcl(8 -Jg_@8=VeOSxaE(psnsW06vNKi`^F6vjv2r!G+0A^a{Wl&r~X^?2nE?b&MsBUx8?u6x-KoKcMGU#vP0;wRicMIut!uY{qWz^bh9XlB;nK0Pg%5k+mG+E2c&)kkJQG`YiMN?H6k{EHyzX_GJi5QbQKtGovO-eoG`rg>-KQ(CM{aG`S<6T{3_Fx2vxs+pA52hMo>wX -}Yget67P;f?sE7T``kL)do*yB8>$ieeRhT&wfI3}aBC@StmqB4jm+6tn0S0jyDDa8wn9cNMBJocE*fO -Ab0WA3c(^o=zVZq9S9&=@TWx@jn$$BU&mw;tFvzbl4uZbsI1eOrJY?v`jpWM=s~^W{`1vMA&^W>PXh8 -^f72uQ!B2+BT8~af$eD0NEGEj`{i+>^h*nY=FrBd>c6aid8s$%E){IdO}{KI>R*9vqhp -cmqke_Za}}LHuBJsiGqhPqZAl&`!NdqQslLG6b!m@LWV)8CXC)pz4QflQ51R6=X5Tz82nb9ax#V`mSo -E&Xtzh6s$%O@Ya#E^it2S}b-PENh{)6?;2?QVWy8^sJY8rIpZnMV)@xA&p@1^J9nb)7{AKu6FQ$<{4y(h22*5Z1)k=r4kR-& -izu64jCVG3{Q{=Tcz#sPmIZahL0`y?LXNF`K`Z&bO;XY>bFx9{%! -Nf5c#{wyE7fyUyuK*#Mtk0>(>Z_OZ59O+rwJ}vAl&(k|k!l#$+SU`O)}v*^M`RGm2JkbM%6$!GsuLF{&~ZakFAPw=#_0o68qo^@>{xc9xhse{c4I&9 -o^_V!%M##7e;1+o6SN#Bf*l^BMTrv~L9f_S3R1ZO_v(4hD>mBwhdsbe;9n2Q6PP010?*5-oo#I@~WJ9 -RUN)4sYk?xj0TIaz-pZF6*1;TOCbG*nXB8;e)1YV#6V(K?3rJX#aAxDp``Ib~=kF#4W@(zOH@2|TL64 -^q}n=G~lNcP1>Sr-7L|`g#e!bi}q8Zo52l;?uvaAuPoxZSe=FhNIM@$A21q@Q3svYj)NjRE0vlUw!i; -!>0S#rmfDYQ?nobaFpgCLM#NC%f*o1IOm*W=XSTOa@#jM!1Wfj=jhkXp@8?$&)U0*qxQ0U8)*S;6*IL -xfSK%gf?bo8JFt@)x3FuHb`54$<05uVY8*p%#X~l@3!n86{}^;_UtzGm%xCPDnsy%sIl7T&#q2v2!&T -d{?2X3pE-zi7_A*1HRH*6|TpAYyksEfCsO}D(b5fO&YTQ%`gtWRSS&TI#LCHZTM4}4?&QeqY;Rc(2{f -jp6)Ja&>Iq5h-=ddzn)=F?|?g*zDn)EvAUFfuO58~A0eTemtgMGYrxk78DuZQ!|9|n^m -V*$KkCl8M@QFx~hPmue!kdSshT5Wu4*JJ18vNZ($?14}l{n%JQ4~#C&HiGGa(aQPgXhsi|Qpd+e`<}+ -%j@nZFZe8S%4Od$$V~n_ES;@F;q+EsXWL#kh$#@^nqo|aaah6D`reHlShy1zDu#d>W7&cYS+a$O7(aMsSO!WfHgvgv^ISbh*gjaWfK$RM$ay0{n@mla+=5?8F5vE+yeH`T?!K -`i~D&74yRzr0u=5>mNxK~)V%RtI2uJ2a9imfWqQQA}zyA4B{0jb-uHhgQaQ2!kG*TfM-7@Avz=@g_Q6 -mq)3^d)q3q&Po1LOGGY-Yj|L?YjK~nPagsz^hYo<$!-YVX#~jYk@^!*b=N80$y17(NRAhbqoN(Y6C(C -5q+V8Z7Se+DH-j^#@S%Tok-LBTX3SAFq*&0@8M^en^F*uu?~w;qt%P7tP4SRt^L>l*VZPFYAyP5AWKq -V)n^NNCvK@PZg?TIto8o&NE;TyR0ONc$)R_j?MC><|U>RW>AQQi{!?Yi -i+OFA~KgNZEQ8CFSJXr9Jud1N($$4k=L9&Bo}^G(L?CG1r0+P6YM${wSM#dt>$sJrtXRH|W*Z3Gz#_> -Fs+;MG3rH^_)D7KK<~A%||md4U$!gT|px7tqAL64N+zy^2#$*-qPMO%r|4CWQ@Fw`hZHt7oQ2PXSs_j -iiGC;;F}{7Z9|rQ{CO@TKFFKjXgKM>OMp%hoiOnk`|DjHLxIbqS%_`iqJlMXWKuU8zwVmSO@N+d2rOa ->26LEL|v$jvU)WW1xx@u$P1rb0=tH1+UEk|G<^_<=!x_D>xh*w}W>QHJ#_aDD{|I;hQr(sIf{g -Mw^ed>IaF7PW^)Act7!96)O(shVHf#XG1{!;<26*u#K(aJ~Jy(VJ^`w8Ql#eKU5d3_?G|+MBeqqW}cn -X_#=cu^wiqnr0Cjz@SCghf-%K?yjR&-3_MdQa3#}%;WH&fOnMk4U&`aq*dimo12LT*m-qYoqAWV`lhp -QlJQFHn}H%}uLFA?xZ@kYLFd>JmEY&M(ElzgF`FfR0fhHH{`#pwkx0vyTjnK!>NN0_0RtYd)1yh$*sMp){{V -gxE89%ffg9fG#o~4V0G2lG*JlE*hMKbgK%ddDx1>nY^?CluPKvRZ&y*fxuumY_*n1?(0egd6;$DEb4= -ltNnZ_cvlLXd;7xA;9-=dWj%2C(!PKXvB%Z-r1t*+wPhrg;ZAi;|f|~-bTE|sDHR~R6O_*)sASbls&b -MUhHd$Ss=q97qwTVWr8?xW)?Nx1w?lM$%gF&FrgBRobwEfa8M036w65G21F*8M5Sqr|&0es(Yy{6S+d -cyi1uT>WX^{HrS;UVeF>(dHud^68W?qw*y}K-d)|V$hlzdZ9VyObm~SD6Q^mSk -5@j5eVZ*OgH+4T`lUFzN;%N%Rb(DMu&rorb!eFGY% -CMArcSxQPGM`8?p)5>Pt@B-D=smkQQpwRh6!&6mTSM!v2?BRlu19)R%!_9E)^o9QD6{jxQ%F_trvIyR -g)EAnY+9G-S6KVVyH-GvyXGV4ekK6(*HRu4TAs`X%i0yZRP`fE|Tc2FI~QnVT;Os+IloeZt44Uk>I0j -k@tv9Of$kypJ?p^t0SPp<<0{r+ZapF{3 -jEPjKRJ}{{?|G3oxvIetq}fmyLf-K4>jNHlj0*5q%`Q6;%`2P^9jn=F1ONc?3;+Ni0001RX>c!Jc4cm4Z*nhmWo}_(X>@rnUtx23ZewY0E^v9pR@-jlHV}Q+R}7pNDNvL& -Eeh0yi#nIZdb@Rs*h#Pm0)duCHn%dVC8;=W(GTc%^~XA-F1CEh?xGLXgH3X7XNHGE9>Jx|SBlT(2F}m -E`5ylH+i(;D;R2OriFdTE@UF;60j`+%D2qK}spkcQw@hVnxh?+ognqt*TegC?GMl3Ej!5M_Pf%!_LLq -1g%p9SgvxNRCNeK4@hD!nG(HZzwp;L)E!H?u&B@0-PQy~o$8p#FMkn#)xUlPm>Z~=*2pS)6?a088HVml4^FEl}h^b{owL?IJ!O|uulC>WT-VL{8Vs7X ---zaa>A0Z2_ekb5~Kn)Q%Eu+E!L&thKpGUTBc^n6q1)I*GBP4VOoW%kx{;z3REg4pwl10VAO$&{%9}? -$;ZQB{PyN<3=jR=+x~DoxE{gHEnMCVuLk46&5&kqpg;TozYK;~At05B!r)7;EPevXZ3%H|>or1b0Z3& -DNavXFlqZyzm=$b>Fq6-y1f>Hxss-0}BQ?RBl9!AxxM9Yv>?9R=9tD)jKw5~zNSWz+L9d#jS>@#0OO; -#z(<_m13AI*+n0uut=Xn%AqnJt+85AtxW>xF;jlo53er1C2s?Y0RFpalp)T#;mn)a2f{C|n+$U*+BqK -kgXvTQ`vL(h9q#3-D;5zwm&5{*|m-ZSNQ8d)cViKHs`Dh+FIDHwP$`MYP?c#m~0>I9zWB^e_k^lg>v{ -Qb>h1YHm4n_>!a%mp`b>}$-@u-u9+Y(;*2Of$*MQ&rW@;BO4C8z-K}#wDZe3QJNS|E6fzM=C;YjwW_T!VKYm -`ZrV{4?WX)#m-^{xZGF6*`G#muT}r8|PVKGRPJe6#KiQ@+|Z6~#P4m4gO&|Xsl#zW66)XnREFzQ6|}wEkDTl-mbvO8GZ -dcLqEjmVahbv{SL(02KrG9z-H>`!*Rny#Ppk_UKb)N(2-Tz#X*4m`#y<6~h-O67RQkc5h?&Mi@Y_Dib -o}zK?Fh4dC3D?nPr)`FQ?t+M#)+w+#{yqW%*s0ZyGY9_u1^(3*+#K16^d3UK|Js_x&^ajsM>|&Z>QB} -ItOUnT8v-iazkI+H!1@AK||j^{|dY@6aWAK2mt$eR#R0xYXyA+0 -05W=0015U003}la4%nWWo~3|axZjcZee3-ba^jdb#!TLb1rasomF9P+cpsW?q6|GFu(>J*>Q_~@Rki) -;w|c0dx+g-MNufUbduOmBui3p)S~}=M_F#|*&B-KgJm6`-o1M~p3dM(>BjJ)bTAoTd=D>b!BN6BlkZj -CJ3CrdwyYW4FeB9FpUcnA>E}7zvIcIH$k6o82=bDrC@p4COA3K25hBe}x^*i<_!EugO2Q-@L*D~}ZYd -c2Kn#tlp(0YL9Ml$xf?LSBK)|OIwF538Rh1T;$rNG3UD>ATNJ64`(^06kV}xde*YuR{mf#2i#^$?J3qBQuVjn_{ixwjBA@7EIXKtQgxf~>}r=RBplWKpvMp-_)#B$WdiO~tL>NK5iEViCO -Jj=45+R8-jOQ9@-L*v9Vux<(UKuSYLaG_2T@}#6ZN -pgV7?k0LKX8p1XKl~;r_oFng4@zlpkczEEw;Qqu%^aunRpIe8o0cK@s{3S23>7vuU#3mF%*Z;i({yDr -=gQ$sLW17pB+QZKJ+>Kc=-iSe<%N`*y7RZDTCoVxEK$*9dPO!{NIrUpE}IvPp60npM#FIK$oOUh&4+e -63Hs;r|W6gbm0{(_}+ONhT(7*jAZlfFli73zoGLZq$7g77NR--P&2ZI&JTEE>TVB?bDCOhpl!c3f;K9 -vQ$pR}8`Opb4DRp!0rzwirjygvDwNJr*S>w%Mx1HBI=fq@p`}q~L>wlG7v-}j$zZz~+kq -LmZkEi9QRXYh0pY)r?h}3kslPO{UmD$y@g46`W9&RR(K<)sXyTRhtunQ2=g)S+SY2}Y1l;*4jLg{W=c -038Dv3WIkdUCZsEW2lP+5m54m{vAwT$bJhlg+cKO^>P4#FUOuej{*G7Wlv15ir?1QY-O00;p4c~(=X; -TZ1n0ssKm1pojY0001RX>c!Jc4cm4Z*nhmWo}_(X>@rnVP6*LK{H^q)`+_)?u&Bwf3ERw~m9N{CoF|X&`xs$^*!|v$HdEhYw(&i?xyIis1C*-3RzqW)c;g3-wE -v-odN3wyRRWNSI7lFWYCw{PiAYVhtmm#b~?(hFr=BmBkoJ#U-E>Lcb6Z|1_Nr{6u4=R&W}eK+6IArsc -OkGQ(PzkTa>$1f@l$kQNe|A>c!V1%Xr$>Ac9KP!U2UX;rUjNCKv~X(;rP1fvDx3w}y=aUiG`ydo-k-E -LJ?fvANB+N9lV1G3%A@nATa506=F4Zl(uS_=mMl+v)x88xW~M?vZv{D?gBU}p}_CYVLbQYOMgC|cbMaPyckWd7I594&H9tYcsPgY3@KAxO+068XJgAavq`dOrO5n}A}nj`KBNc2XKEijUajM$e-OOYa^`W}s9J5Zp>rF92sSzgR6nam{ -#sw=Nd1)j$XwIQH&!SkM%7@b2Qiy*v56>Ad^DiKLqLjw#;l9}bXq?rj8H$R5qU^E=Cr>{K^_|srBou) -ObtG&rQLo(}u$g(->MbGnMOh6=rCgfYUVxpFDkkGn*5B&WBzYWKqVWJgoH2A*Xcf$}d0{GwYBuyW_L4hZ -(n-2>(mbJ-nPK&t-H%s=h!H%Pd1TC+7(|=L4!NRdKUT#D#-s6>vtVuD1qpN_zP!B(FOJU*ujhp;=v4Xt7uTFLJoS2Og~vhB_*qWY~>30o(%RYW? -}CYDNMds3SGLRIEVH0>uK$w@ou?vExp*k{x2O~O208r6jC{RL1<0|XQR000O8`*~JV(0%2py#fFLU -}m!JriE&rFTXyfuQ#w}Ei9DCP;g(~70^Gpq#g6}90?!fAK{M^4}mOaAB`Ikc*V -l1!ztP#Yu*ZXo9Z0q;|+EN}^_Dl5U6Oc5GxcilFFNk|sggN52!Mrua-ith5KI$*W~-dS5s#^a`GLe_z -XN*80%i_G{Un$4H-{Fq=n`K1(S3~2mOb4@X~EwGAMQnn>R!5UB+SfP>Dta7Z4=9bGM0Hbo-FpcDrabq ->#)^=}Dk7ShhsenesB>2-9qS%MCX&l7?d0a)C#p-ecSJU-+y4*zb7*=bTt(ND}CR#1&?gLDh*YG)7o( -F(5CfWw?EA99RlDiRN=H?Y6?g8Yg2Qn3=Jm)Fpl10r5ghJh+mJ|mn)Fn6WK^nr#$VKpyTV}h;i%_BG( -S}M}ux&;Co|kJ?LdB~vxkH&L9a@wu<+g=EnADaFLw)(s6K<}*%#+z-KBG(@Cmzr>XoV_@&ehfa>byp) -+ZhugCSLSBFT)(RNU<@YW49xxBkeSaae@i>`yKuWj^D#lN$eqf-#_cRAtedWqa#t#QhsJ3Q)pmt4N1A -b9f2P?c+|AZkRrV33Vz^XB?9kIOz0%pZr5QHj>R3ho15osw?RjC`wwVjZ`>(0szm51x@X3$#j>(Jt#f -g}j)mL*nv1)7+tB6wT^s6sv|z8C`{bnE3)2G=-QmyvB!mUp#1*$J{hmlx;!~I~m!pTn0Q_fm-QNd&T& -_nWs}iL%E}7VK7-A=~F1M&h4`6xO&&}a*n#Uj3Q9l@JueBUzO4w1SuH1)O -?7+=t@98)9g64I3g=+CgoRD1?PVNCdpkuyP<2WO`Fc!BvLP2)7Q~^dyWZZhH#7a-7hd@D;rk`#9h(2w -po^4bRV;reQn?W!jYIvIE)ktLb<$TTISKY=GY=iPnO_AGI)KbJYT`NK3|7q)_q(v;kfkq?prQYh#2Hs -!YJ@!Zq9=vE;%!BUZILDAc1e^1Bb9QgH?GXaUnj4DUvZ=^`PHFVp4a_01B#jOO#vY&o4Q;Cc?@>)FM0 -IlZ3I+j|(zZsF5(c98&5nP?lluZ-7EB84v@=DuDF!~+3^?gZHiGf{|)e5tbLB|@q1&?vG46`GB({va* -M%ZWvbjd0wBm7h$pD54Fuap2mD@}sCQdIOcHSbB#l*M>P%JQJ>k9+=inNK1J|pTm~7e^1i!2!zxaoh#&BtrD-O)wP{Mf6yIQ)ZihgDZEtE%;jZEbf$n^ -$Zs}wEH}xSM+b>XtDVT^+NfakIWaU6YUz%YJijNv$JXP)7%080A~dN02=@R0B~t=FJE?LZ -e(wAFLY&YVPk1@c`t5Za4v9pZB)^2+AtJ-=PRzl3rsY0-Lwa$PNI|zwkk}7uBxU9ndBz8Gj?V>P4fx+ -uKn1qos>aGgm7XXpL@>nO}a2qMXh|`g;pYR>Mq*6m_RdkQ<($G?+puX$tHXVO+3;hIVBD-$)Es5-!4X=s7^pE(QJf#89gec0?+bh0- -8Va03|^2>6s>VS!6X)UFVm$poR|cI(TuBq34Uw-jn)8KD{B3wp{9alot>ytTF%4Tn`#h0F^LmClEv0h -!^=d@_yV=^2Uj@LMU+7|{5uPg92VhiYE2fcHQf{o~o6;Ty^ -{kBi9s(#TCLtKdFpmSc8prc^Kpc1T)ogLQg1hl@IgVEIX$*@cOcv4gd^KN0^z;$N(LH>bN7n;DiiyhL -Q=y%H63JbJn7X`T#3KQjY6O`A6Q1#ee963IIYO=;QA@G|1?n9)?jQ}xONmAB9k~Q8G0n$b@3H4`&}BQWBe;K@j*cc2J -;1nZ>*&CT1Z;!D>T+U{X5G-ZX=pKhg^!%iV_bQ^&OYB;OIOTrbkk86yp>6n0NLa54C{+aT;z^3xq%X+ -3W#IT=9X8X-KA=F$j{tn|rWA{ZT#pI(=SHm#9l*}bb%hAPIbX`98PUH_33gEn;4{9YH_dd*4BdQAmIH -fXRT2=@7gQ#TX5$kJVIL)dU%Hv0})6DGu-)%ha#qvvy76fR^YzWa0fae}-H(PB02ZT-jQf%Qkto9_aB -Mc;x~z!F@6aWA -K2mt$eR#TW#ExWV@008wF0012T003}la4%nWWo~3|axZjcZee3-ba^jwWpr|RE^vA6Slw>hHWa@1Qye -%Kkpf3)(_t4LGR$?|#a-bHak?T13W1g=o2^W$6jfIk2J9X7hI^77N|Z$TCw0=STY>tbRmj8d{CtP!9O -(dtBFQD2FBF_Udi?$0fBtp)==3qX$YMr0JR|(A$T|mWQt(2gi;TcIk+E2vFTx76BP%ac?DMbjDLd0SU^kYC11&l)= -mPyPA4=AjdS`=ywh=&l@213jfL1}{W3H}w?azIpJ@ItAie{!-~tvpf~>IpeNiA$mMXlx< -=ipMlfLDKgblj!Cw2a=#I0hytNFi#8Bd<|fMS?X4gHu%Z9 -f{xbO>pv<29wUWk4iKgVKZsEMfur#pfBBQ<#enRC06&5-OK0)kLOfUWh0$TQWsdv9jTm*Xf`_Ar+8WP -(5NS%#+F!1Vx$1JbG8xzmELj`Dlpt|J?5Y{vs)skg&w_KTsYD=_$%dz*G(f<&r9y4@n$P(GJ?_bb^=^ -WZ|+uBPn%Ixi@$^bW6Z)w>y|&ph=)WZ$l}s-7n67-cxkWXzHPCr#SSJ#vta{lB$IWi}jF3;QIk;_kAa -&anv)4Q*BHix^3&aE*$>|Ga=&A7X3?5d&dI9xYk%g^M@#nGbMsqK-g{rgln1PP;c27uB}1Hy%q3$rvj -d@DKLTe%Y9BUX`BYPiTcC@7>6`7I5UPOTiD(=1jE0}l{sWB+p!r#eq8JK?Nt_J%hkUXeWA+n -rMU3;A;;v&J2x`E1Q_ckN!V$S(^kUfjLC(sFn!0Pic-rusZ!#IRuC1_3NIA(ll&*Bxb?soK$6s$X4v? -WZoLH#bkwj8O&srdPQ9rf_~DIBlHQNL~$u4%g2Q7GLz!I^u~;VGE*OM6fS~jhID6+bCz-=_c&1TSHLH -$gO}?uI}Q_$M{ux?DaO0+ellG6lN6X07;l`-PkBGzPd*&g`}DnyZ*IEY;n+QoM;)9*4)10xOUU>t}6E -^w@&z0xh%odxaAOdo@X6>gXvK3mdm>`Xw7ImxVF-WXNj&1ULA&*EN3AB%q5mGjx6J3;^3&W2H>;~_ceKAU8DCiqQaft8^nq>>BPoS+ -qVDenAD9g~fWD7UoGhvD|KYfx$U8mal%FgP4fLUo!(oAa*ee*F;p=ACRB-(}K>wAMbDQS%J1whp5Y3J -s@xZAW=tHZKZOOy6bg0}vJJT;p4UpSbXx|M>v6XJx*3IxGQq1v)q@Morg=y{-LhcrK^!jHyqcYp*fxYrDuB5i^>5VDhaJJO -IXV2H1Yvd^;uDWp{V0FjLzRvYYxnI68W_Djv-gXMs8!G7mS!{a{YmW;jb49A5>>x1R|COAR=q!bA -TbMX?e|CKT;KXax~r>KD$Xip5B=$?@YoKMTYiVtpU`a!HX;?rpNv!-?+6pYH>$5KPTJqS;`9$tO9KQH -000080Q-4XQ}tHOb*TdY0Okq+02}}S0B~t=FJE?LZe(wAFLY&YVPk1@c`tKxZ*VSfdDT`;bK5o$z4KS ->&=;#C(~jHeK^ll8J{>Hxunnm0Tuu)>DK?yf0$pA1*i{7vD|d(9;$;xV&A@f`xXRs1|w -CkH7^$yuCHGG`1#MjuCK0N!MjFqlyJ-B@2UyTj!^JUnMK3kj%lHm$KmfU=<5fVu{GQ&k)saI2=akvC= -KS&NLm8BKp566qn}Nr55J){TuHc&uHf7O!c8db{X`6F)j&n0QdwvW5(PIM-TlWod>YPX!|8l7j^Ta=qx{$&sVeYV(zMwWZHTP?JzpyqXyJfh5%jx0)+al78)XK3|$#Ds`R5(Yt#2}XSztLh|W2G2=HG<-Aaw -N=6}9HwMB%PhDsv~XvHO92iHAW1T+oh0W5MbW42LTVD@qzoWD)E4&O -;x(j7iB^jH)Kom^kdeVVF69PEEW1`(g7%(B!#xd^ZM1}Kq)X82(kuOq>~ejIU9OMVyN3JdmT>QQ?3|_ -HQ+WGudb2YnbME$$dlDX*;Q8M4Rxp>`?j(QblFVQ~Td#PkB<{63WaM#hO#Iz1dfYxbf{CZ|1_HPMEb=)!*O+y@(#UY -G^mN8SwgsUfQcI!L!D6aEWZH)E!w|+Ec!K4Tqa2I9}SZPFi$So_al@>?u!k6zuc~x^vX>3fniMyDPXh -wrj3`KmV<*zSaN=q1dy2dfdiFfBj#&XV9H~wCgzi<#TJ=9$454_ySN%0|XQR000O8`*~JV9o`~2tOEc -5VF&;KA^-pYaA|NaUv_0~WN&gWbY*T~V`+4GFLZBmZee6^cV%KOaCwzhOK;ma5WeeI43vu$s7f}MMZN -H19mj2rEu129Qe=%lprw(_txT#URY(86Lp^LcanV)>OC*QiV>l#z22)wqisu^xSC@bM3A2K0tl>WE3U}qx&~H~mV~WIVZnAF6|h_(jTvdR?i4p%S7dP|e{B}XDcQ9Ez!oTyyO#nz~A{u-ymDNuoxlJ -LaHeV6qk$_CLf^>-q&v-(gtVrlR_AVq$w>#S+GQth&SpraZSFa(%~fl7i7%E?c}l -Ki)7|e#@I;264;w5%lM_{el5?-ycLNfCdbYfNuDe&pZB`IVtY-R;;e*T3jIoG3J-$c| -ty!jh!bW3R;T~U>Iez>RZ=lO>a64Rm*L-Tf;v41REB9*LY^WeZlLP7}%;LLL42zV}}Lff8hs7$G9sfS -tZZMqu~HuQ)y*tarqA`4*{2EDg?K_$KF8t+k{Vzh*-gYy7}-~9?5Qn>G-JRW$@UnX!cbRxa&Xp?03_q -DjAF~vRYJLC=XB>5VcACRUfyJGVoat4h?|9;&KJz;0MSR1)mI(9rddhy>s|8qP#K4NdmO -bW$ba`jP{ohMsV?2Wd|lC#&`WO}u|{Ok~aeqz^r&0gzlCQRoENPd;6P*%)Xsql)qrC{TNr}(N?2JBzL -Sg92|PDZRp1l?+>+y5&#taZt9Fjd;I(h9JUmd#}*n7B_x;h0n`)p?Ogu2R8P(k=Zl6_W%k-c>C0)NxR -A&=+``$8?N4FYwa2;$%2ntyT&5f+pH5hFKu8!(T7X&M$Ay4ZxQW~f^T{B)lmXuE+Va8 -WX8^#w#V9jZgb!DWJYB69|&)td}0WLFXosrYhZ5XPS4En|dFcnv-m -<&#_4WAj_Tv18U0t)YtIL-cw-;BJczVUgm+#m=E-qgVn1GpZjJPih$qykVnNZBgUN=H)2r$<{AX|u3& -SeU{)S~1I!4~?nFbe9hLYQ1ya-@ZLGjPddE**FN%@;}~oemDv#xXuk>z@W2bzVQYbpghm&fMsn6|zVs -p9R)U;)LR;3x@M*PjdV2pXZaa*XL(Y_U|VhhR>s1u7xw}GoEEP5L$FPoiyXH%!*4lnQ|+ -75HuP8a@G(0y4~)03a9YYFVq7%e%mkisS)?f5-E6ANDL%7t6%nSVbMb2gczID0bbS&_{L2q; -K1u_Vd~>u$={?X$zbv3aJs3$oCs!0DZc8=uV@W}18rHjOTULwH8+Or;XtmKDw}jbia?%%~q9W4_{?$| --LZLgD@9p@W$CLo31)L;n?XZ8 -@0zo+L8v_7NB=YZOU5rI4}yoE2d60KHV>{dQ@>ghY*E!1M`w8v~(MnR%pySUdfGcBz8ORx}NnBj031eqj)Y!o~XTVt0 -2N}y4%pEPu%Ex!ez`9{ZuO%Hi4mui^v`@L=h&BydU7E5lHy`UXr6$UCI)rf9E^=9z>nsJZ! -<=JtX{|NcjB^439{fr>qyAlF9{rcEEZ&r8?Znp}*zH8lQH!@+>TY4Qdq6JUvyaUC6w{SqClMNm~W>ry -F3$!kq2S?{C#I@7?#J-KWv-zAr!b)Mac5wQaQ}`kIZApQ!D7hF%5@#gdm6Z -OXu-GAzGFw_u~3LN@~(4~35)Rq(kJregX^Q%}OtiinewPNV$KUP&+TX{5lx`v`@Pg7%WMN4zXV?^*u? -d%A)3J3cXs+z)JM$Lq!bCQ0-*127Nf!--FpFrjzTx_43OwVUN}2o$KloU<`uP -C?77_~VPxBGzxMBUiuTCqKPK9n$VDLu5Y_`G#BrZgCYY!)p9(VoG7eZKRBubb$oLIY5_*&B{lghAjO5r0SKHQHa)fxp)%+ -GV{(GE-j7%)=5ffBbYP(nisPo0<5kOv-+Il3#)tDo$Nsf}nEv`-}IBNec1*=im3*TYe?<+0)!28HCe@ -xLc`G`}aD8BR4tAKGB(osirF8@FWTQ&U>l2foEZTM7XG`t};>(Hg -R^^8L75+S(NQEa~mVuDH^Sg$C~BdN?#VVuQIYekY>5e&h8N -Ok$}u*llwjl-UoaOa0;r7{HmP*YR!c#*6U}7;G}EO?Ma`~M{4zuS!f8Z#=?wQ5Y85z>GNp*rpIsP?J;55IAd^=> -=UCxoT*jTU>iG9P*V-c+UUOD#4^UBr-eXCS6J=QpAH8M$#Wv(jP*k!+`C>!0x=cmSH+gOJhUlRow{qX -3gI#F9Zq<6(jFtum%Bnxh^NkovWVn_mD=&P3{iS -%kX;V2(s~k6sIpn{q_aj@!hi*BkK*Q%j$;ej*4JCbv9M5EqqP9LcOU-Nx^o}S5(AQ5JwjXiI6hk8mY1 -*3gJfb?97Kj(@$(?d7F^EkOCzoReiHpo4@b>Ox|0OkMy0384T0B~t=FJE?LZe(wAFLZBhY-ulFUu -kY>bYEXCaCu#hJqyAx6h-&^ipz6QL4qH%L!ku&X*(E)PNCWefz*W8>hITDCzp5Ma1ZwoQHJ2d5~eOSQ -pc!Jc4cm4Z*nhmZ*6R8FJEwBa&u*JE^v9xJZp2?IFjG}D=_p2NfT -M)S9W()=dE%aWmh$}ld_e4oUP(g6l8Nuks6YU?eqG-Uv~op0T9$9ne3ga5=$h|?{0KAKt}ULniVWBvM -9ORnPusm70(Nvvq;>y2o?*t^C?T8o=0#)4S|d0nD!2X&*-0@2L9P!2WP(wFaOe|OQ*@R_;MD+aWILw` -1L*t3-Rqkmi`Yxxhi49YM_^TJ-!cJej6huSn-{)blfL5_dK5 --wiLj<@fieqrqr+F;Ex&I{1hBe*W?7^xU|6`T64A;7a}ccs3H>=RpzP`$g(cgMt^)oLiUUGz?Y*x*euOh#G-^{L2}32M~9sb`?o -dh;P8(fz`M&Xc~sojI-0k}2o~6AUXS{M$f}$lslU0sk`ir$Sl{5HX+^i(&?A4dtFaX!@t&D5YXF$f%N&g@Z{8uea`3WF?*f;$c#Tr9zD%dxFnkb)e0cqqoApB -aS17&+wanZ_SF9u0flA@4=uupmjKm*zX4(7QgwX;6R|%XoWp1KJyRF%XY;egUUF5wR05cUiC38}EEdb -GRBM1w@w+u;gSs?|GO``5?>Eto^qjUW%_B5uH5GJ53nJD-da-9Go7)Siq)fxST^UU{TJ1F!m*gqbY&w -!4!Z$JU0|d?Vb}NzS4w?0b~~-pMc?dD95xhkANi-R*yLklAMO4BO(VRl002zAs0v=? -@_a(Y{~n|VRqR;k6k}R3o$JQ1*248Xu-{xAV|((s2|<~ZEfJPi&wj4w3MU^u!lX#LAH!J2yXBM@-K)$ -v0y?hKxF_nof`P;F~~%45E^2`EhN^y6{d3(l70DsKmYUvJrHyhW%gYNmk4qSmWKL_qZ~3GScus?{lf7 -I#_S}R3t@C>IKd-8c{F{qX(a0cygoMN3}3d!qR*+P^b7ziSan+YapS`Z -UE`)Wd8&3vTN}|D;>+1vsv`Sr#>uy8Z2*^h}BL7JI@FwM>0y;POg(DZYiL~{9_e?v#l5B9jP*Bf-Y -3b?w3b@~IYJ23sGzjrV;oe4bcff8sKfl%s^&?+o6bol4yVSM8yWRl+4pr^M;7i~leIJ-n|r{WDc1Iql -2$52yz;XVwMY@!YNC9i0%vGxmj>thA+l|oR!w-kb86*c4$XegYp*6cul$#}SgjP?un?K@H~eNgM`b^= -rcm+aT+T}SJUBy`*hrcedJ_f6J47o#uu<@I;wR5u07z%?~f{iD$K*(6QlO7&HhbGpBOw22{LUIh_~BV -_yz#LPvyMZQ57(O{kKBWM19w;iOYWUY}fF#`&rTFsa&C(&I)x3fkY -Q_Pfx;Zjb?m!LuNfs4O+qsHov1xHA=%v7pGX`3W$RCbT2gcU7C7U9$+D9y)Q&~f?hTRj+SHNS%u4;DO -GQJ|dL28^yQa$)c4%8%98AcSlxr`jeL;Sl?5pjk15Seh!9OD*(~qSB-(NiFZ{2K^0zZB>Y}q3SG9G8R -F)1vzvQ@-|NCQfr~(##e&fJE*}WT5V9stOJy&AwbBY?dXBlaSMck0cDXBejfp8#XVdD(Rdx())0K9UC -|pMwbEt`FtA7JCj{AkdDTm0J5e!t0H$tUUdOU~YL?%!i3(*~pMAn|knZ%=?jN--NHuU*ATcUh(FZJxS&}RPXl?mP*9ZbNce(!i2&#EtIG3cjIM_GP~y3wse -V(-w$jzd&QP>e;*I$2>trbaYEKh}o#|A>ly^&{Flh_aZ3zA$#>&GIN*&qCZZC1#32aMU_9a -B=T|ktYT2WM90$>S$D%#lEZmV&fjxo>{S2<1-*eVVs2;}esc}pa_JP0EQu|MIxlrSaBnk`9o3@50XJe -C`*Q2ffy-gupb+wD8Ozo_@P%EpOh_T{JS-e4!OTW&T;ZJs!HG`ogPotWuWXNA*AA>}?SL_Reg{6|;hF -h%Ct9)OYlgu{BueS{2RKvde@HN8Q7TeahyS8Lj7E=A)`Y$BYga3uj(d$SfXsk_w|-7HfIW!zJ}dhf>D -*^F7WwL=Re%8DA=)CDkmlGypB{uqmawBOGk#qQ4?ia4J&N3Q}?{8i|Eb;jF?+4x*PJMSV_O|jQV~oV6?wrFk-j=M4nOULH76irEi -X@K|A&~q(TebV-GjzFsqiW*g$mnq^xP5xn=t;qBJenXz_1vxrdtN{>M%CYpoG`rUiF4Psu=~Mt30%;G -9HU4M@^Q(I1Tp01NA4jxF0D5&0UQFN$Z8uz`ild%a%G$g7^DvdCTB+n+28mDjNa7(1{MKVw3jLv)YfQ -hFavBGkP&)MYFCJGzrAH~+u)CnO1vJ8^BdM0wWXxHKSb82Q>7XR9rvcA{!tD02-+r4$~_Eo|pZWItY0 -Sg&P3YB9~Z$6l+#BaS3&XqpgcoL%K+~k^Fp%L9fB{tmM*0fpMOx>zW7))}%nt;l5>;8=fwlO} -Kz_tk$&ID@HxM8Aw^(^J>vNNc-f+)dLTJnTl*|`Vl75BKG(kiqal*b4z;xK_qY@Wz%HjEQ4cXwq-Dzg-l-Qq+xM2x&N+l!RnbI`riw**akE9%c#2|Jh$44t70bx=+YWW>a^mYfOlCN(?i(<&`eMYZH ->KK;kXLza=K~ZdR)ikS}=w}GFsI{m^-I;db(LsYr&-*#*1?}vb?{2HHMYu&%(zRxR)yP2i+bF)HQH2^nR>ERgg#vuD*#@wlqFPoQ{EkK#jvXsbRLRW -V*zWdz-0-xHT3L*5}I5Klv0)U)bzUz%UcUr3b}0Vm80BMZsUPD%fU-LYit5#{U7UdfHRo~lZ|{dD`b; -s!^VIKs@8!P$B=+x1-h?j*BdXbdgDb%Q6ANVxSClC)26q)R>O2)-p94Zp9FanI-qQJiU~Jlq9E4#T(~ -D8f)nc4Gga@#y0H-$MsKiF=b{TaQ(WNbLse$zl+eQJOj=i^sTx(dq`Jr=zksZv-Qamxu3tj -FSOPSx9$z$ru>!S!fURh)(foleo2SNQC51rglmrXQbJaHETwo -Fs*r7L89$qbu-7$S?zC_P;TxivE)!Ge8<&UpaW?W+ReGf$x6VrSNT=3upXXsvKGx+5c|U3kygExEwvL -(;2m#cG#HF!qS9X`n;H8jIJzfC-!+r5?Zy7!Af{&4jzlry%A4sA8wD#}=?U&JhBzQ@(r`@SCD0ReHgM -;0eX&UwzIi%R^H>O9q+NZ}2r1HFUPuG7%JRQxxbv_QUsUMIp7{|q3?jSK^M4+VIxM8{l$j`IqHJ>A2> -kzHaiQspNta_qU77shMIyJ7hG+Os|pMw`e>huW8?XoGOzvoYcPeXUyX@c^33n+hbv^8{ES3YZbqc9tV -Ref#1Lo7ULtKl6~+~Ia149tHXC3MhRs7gP&^Yz+Z6=8hk;9HdVk4608H< -w4E<5;xvVU>5$zLWBK5y!SnByRmC5)WCF<}E~DOJdo&!GnFvw+V8e1mM=WAOR-604z7t192RvQ=vawF -U4&h5&9AS-Q9v>58ipp~WlfJch<}%XJK+_E5TY6hoalb?xT5`WK3RSivmnbulTgXb==F?^2f9x$#w^J -6cn17%PZb;h}d(Tx!IW#}SqQAh#+lPSio66=z@UwZ**PwOXJF<5Ep4D_$Dlo~d+YZ`=u`wsWL+_@J=!C$>*{(4w2lEyT*w2ZNBA^IQLVZ% -b!mmdvNAp) -2o+-a$=&bpsSZTNzeE_gRCiJrk^q{AeubunDQ5I@%mnEdwV(H$8?e65v{SEl{0ofJ-)krqVu?;YdSrH -1kXh#%weGH|!Ivj^&=gahVPeZb ->x^%L|o=DRvvmUU=!v+Xx`Jk@)KU9AmaEidJh0Eb=q@;rM@trA{m<=;R=>LMZyi%p35cs5#1 -goLB|W(_nN5I_Q@s(Dt713JlkfFX15!VFPq=7G --!~`W{b_*L#CUsw=3EqaBSXnc+@G5*Qm9c4Dtl(bhn8G~Bav>1s^H&{Bbh+K{keLy&zU=T);o5p9vIoOcv3D*mx*{=WS8v=SEYwMG+bg -d6d(2@pz(5^h#JY5;(A(C8@fk$Gx3O?f!MZ=nMbH!@c-9t3FSrnef5kt0NJlGoOv&x;aAoU3Y?+KS0H -=sC|hZ2N#hlIM#J^S1Bti2&;AP^>6Ih#$J@7uKfk3Xq1tc%cyxSR8*p%Na18%CJO2w%O9KQH000080Q --4XQ-a(vfX)K|0C@@k02lxO0B~t=FJE?LZe(wAFLZBhY-ulFa%C=Xd97A$Z=6OD{?4yhZN5MZ2<$eFk -*+7_N*b%N9l4h(2}K+|_G~JSMayFKuI_*DzVqfNiq!jX>chPuvOew|L)_6|xEBfJ^Uf?( -I^08D-0X?7V~yw|aARGq(ygJT$o5)%qFl=f>~3LEe14eiPq~!GcI;apI**W)VCkAq(Ba#B3c1zza~;6 -x|~t;3u4rWu(#^VV44oRb^R)@~v93qR^prg5p%{`b{V3?}qhRO{EKw$@|;Y<$GSEaWccQ3Ea<@>v#@R -Ioo2te}4Ga@$5jP{S-3QX*!lw$=Tyf&@O_LYcFJW=fm`%5t21XwKNQHh_=5tPc~=6`9`B -DZp5VG-L6GFr@Y%7%d!fz1A9Is8O50%U|VP+0LAz~2Y5fpsZ071=c`oC9E5O%>qo@KCkLAWksIk6Pz* -NO($|X(z+|G{_;OV5^0GxO_&u*W6cfU5Y}n}oQtk@OUe=g`EhYrDtlkCRA_tFoRTd&L;fC26_c~DVlr -`nQ1IkI(QsNrT9@q3?tSa)uSn+_nsa08b6peK)+D<`Rg4?m~*i~I77&R(csjMg`dno;UbM;~xgTwk*5 -a!<#+%>K3SUP1_rpc>#U??`Owr0X{g=lC{vf@CxVAar3fU*TQY~Ugj4MDUcwP4;(JjL~p*&CCv)<=H9vzuHfcAm -aU=8PE~V(yzPypNLFycY(O&WIKMbVcPzdBWYnbEXj&=Gt1gGa}RyU;uzrAAq&wnx_GRcoezV6Y=d%!o -QVAUH~#@pO9KQH000080Q-4XQz1!h98m-S0Luyh03QGV0B~t=FJE?LZe(wAFLiQkY-wUMFJE72ZfSI1 -UoLQYl~zk{<2De!>sJi42x2c>U3v=;py~FJ0Gq(rc+n$4OCy^NMQZtoSN-)JlAPa>iv -@7VE+7wSxQ2W(`to4L1WEgxiDL8|2MTmbVDCRtjq;NU_&i3bk87DXiw0UFP&IN`0ap -!l+F((VpVsIO7;C-r1{nj<1smX7tEG3y5?vG@;29k>*m5r&NWI&UH`o)FBygvJziQDy`J|7`R(sWyaN -tIfuC0m@do<2umZwlM@+f@rQr;)LA}Lf^gx_oIL++zx#a_|aP>WH4Wd>uT##FCRuB;bo{OKrJlVlZn& -*#j^oCQ4QjU0hP(PPbXY^v4`vW{vcdTtzRwdoq}_^2P;cU!9iByAS%tNnxhFDmA(XGE_I?q?T;XvM9wuRuq@r7V|yw_Q830mjj^G*x`py -iH3kP*6S#hb1&?IaNAefD=l|$KrO+))vXi1lN+cORK5PQ-*iGWidmG$H8QF^h=u=*Hv|LS+=eI?(1{_ -(lHp~%noii`iD-&aJGRd2USx4nIEX?)<^u{!0Q6nthN@V$IWd6Hjl@)U7Gd=Hi=ABLxahF9gzPgI+Kg -S>!j49qx{g)LbFuuUW>@_ZyWLD5VfQtkd5?@i?X3XDy|Mlyi%bVn!Hct4yN7=^>Vc?q=^@|(=OnCPko -*gZ??ta=VZjX?VEBI=$^)eVnoCdc(c_|`Ijqpa%$OCt`B>Ip8F;6H) -BeqP%Mwss@KNG;iIKq<;ffG_-J*q%;IZ=q;n}_RvQgKL%x@sJ^B^p)XuPgN!)cT5xoP5jhvOS(ivFRqO -Pe^Ic7o}!raXl0qTJFAQ8fl1T@~+b?Tv85Uny_`=JLP?3p)+EZDBj)b6ge28*6C?OH4T~%49$_oNi@x -2D=*5zug#*%iz{;|E77qlkSPiJ~>Z0XXiRZ8lG@m^zXPI!#nf;7XC#P542q=PFzdw!jhZfXmqIiYxq6 -1MR!1YZ{vxxE3g_42rq~|wc>~6c{9FuJYUx7>ETgWOQKQZvP3jJp-%tL{{`-u=d^UsFT|qe4tLrNw=< -}4sz#6>@85(NS1`SnyuYR&Z*%i^@)0{li;mLC!7Ph^0Z>Z=1QY-O00;p4c~(;$KNE5S4FCW;DgXc@00 -01RX>c!Jc4cm4Z*nhna%^mAVlyvaV{dG1Wn*+{Z*FrgaCxm-ZExE+68@fFK{zNZcWt5V91B~?E^|>$=4|rg=g%MP& -LrgI8p^DGsk02LiuLbMCz|0aXX-H~QRseWpUGrm{(q>7olKzzcV1j5z7Z_NUW-iL3mDDuVf#Co+_5b_ -&=-o;CDVmra&xog2POXyH#dybB+^U}!(va7#rOP(Pl9EFE4`?kn2Q>6+68NIEb_F^EVglQQSyp!nfto -)@6Y@oxAm6g^>z*UiVd@znaIQz{}trJS0ru7DV3@$lt8}bNyqMoov0wD+zQ5Xa@3Yd#l#M#fS4{>JcG ->Jl{Ys&$H@4123ufx0vC%kX6zDgC_bGT_Yy9=71|NBJ~3}v-(6-3ehz}POA48LN#TsMeEDJ?sJHy3$c -2{^L>;djHF&#s;d7q>X#9_7Jx*PCEbTYG>Eww*4p1R11&av6a?lic?~(tGKdnljP_FBb)tlo!Z>`{S1T@yG9qaL+8)sr2(Gb~SOjzkzPhrp1~ -tH73oc+KGsa3niQr$yVOpX?DkVf_&U6(_?3<3TvtgGgZZSv(4GHxRrTMyj^^BI)yQ?0kb^n=`v%l!@L -R2O|O+P%VYLKERkFhinU(8<(U$*&NSG2n0y=|9dboY$qQ{ga53~iP>6z+LB6FTJCSdvg6o8CHcw6}^q -9m$N#TznPB2Gw&EfK*&60Z5|(d~6dDsX;$cu*c*bR&!He3Um#G6qS0@gYl-$sdFL{5wmO2p -d+-^c+zt5WJwUe)z@fvjs{U4Sy@OzQKrkGiCE77Vj#YAtS^O$^sPgE10Pan$&hhT2T2A7JDK9K75u9_ -4(#Y6NajwAVKO})7K9o8OiVQDw(CP>8ypp4uG0X@L5e#=?kV%el>LepQqkE+k);c(ddX#_V!(1#Ey`s -l0^8P^mIymb%yPa3N^I35uO`J7+o8NAT-lD8E2-rU!4@K%`#rC2eQDBEQn>PLC<(<+X+k%rw*+Pq9oJ ->bb%(*;xbJ~#da#gOs&r9NbtS_wYdPf>zU_2j5Z#bM0V-@Kv>{pOaVB}52;*DB?c>`^_T8&*U&O4-z_ -Nj`6z&@+q%Fgi9fb=b1NI|X1y3xm{;tLvPU>G3sdIDV^=fFYj!^nwQhs0ITV;(&J9FKr!^D@GXQMkGuq9_%QIt+w -^y4u0oJQ?_W17%I3LE&(;x8vA1gq)n)K^mqe6)oH`pd1pn-a#Eem5 -P`efr^Z!|~cYZA6%}mi=(mrQ`mS<8D_v{_T7)g8v-{1NZ0b`Ey$UeBfV~m<2=A%}u?W#VxNMUJuZ{H- -&g~DEWo_dx@w+J2oy|aqrkyn^~blMQz;8edTh*d_@*PUOJ;o -&P#=h*`^F52mYHc<_juRZgmA@HRJS6HW;3>2+Eu}ZCs>$`10G|bbhXOGgEn@;k`b(vtV5hiXU;}S9^w -!Gd}YrKkNOU+`c-E`hr(c#5MIU2lYq2EtRuwDaT?LCFrr~~6`YQBI*kq!|I@&5&fe!}h7N<676&MVX% -mS2Xxe5U$TV&b~R70XKeAB?dH&X3natbN -v=JKGs)o$V&~+l5&+_P+79O3#Qe?x@uu!?3iNY`L7y!L8pAj27H}bVF|?=Y>L220Mk+%{^Hv)Bl6t1K -D2(I?-OgwzFxW4Z82f4H-*5?&rnP7egUU+o2pL&p!~?ECVQ^A)G|5B8Vf&(nnZ<#|4tvZ*;}t<>i)d< -C!sqj;K+NbVX)E&`ojcMZ;B=97WbI*l^YZ6sb4b!1_}C-&#yo=_>KsgFb6IC~8+Jcq)v-F}Sae$snrB -nhL(x1I-hmDUA;z?XSLtY3ALu^L{G{I9Z2;{?Sna{?i`c=KLnql0Wn6QjW?((yvJi@nn_BR0yya%m4V -@zLjOO&CCl|)WW{jcNNfb)xZrUNt9=%1GZ;Qgg%WGvZ -$>;zMuJGUl-deeM+Sb@N3hq+?YWOfTSM;-5uDUFisHGA2^EanXBGxQlbxiYMr)n-iddm76%+hW#YFUh -;jF~MZG4ExpgUM2eXi~rcdKAXgJAINpoN)+Pr?o$JThzO0Wr^OQ6Q#oS21EnF#IypBOq~l^eL_5X-{n -kpa#tl(hQ@r{(DnWP>)Z4n<^|74{ktv0}4W9x``JWoFbCp_`nKzh(yPHjeVo0wioN!#TZ-|9=zX4nHO` -T*#F2aF$7Cwx7HXMw~hN|18EAhRpkel|bSlGK73JM!{=5m;RlaVJ`Z3dha7Lkb|#gibdJ&(}_e?G+zg+3ToZ9?|7vb-wos!;TV@nI -D_54HBr^t3irrSu*#>yrpX@T*IZohvCp#8tPZ^ccw=eMC)csPR9f#eDdbQJ9`yzAS_A -_F~{$2TaTC6Y-A$MwRX%t(;-Uf1a-R@6aWAK2mt$eR#RKC9dbVa002J#0018V003}la4%nWWo~3|axZmqY;0 -*_GcR9uWpZ@6aWAK2mt$eR#Uz!Uy%zJ003}K001EX003}la4%nWWo~3|axZmqY;0*_G -cRLrZf<2`bZKvHE^vA6JpFguHj=;VuR!I?nQCR`^LA71`Q56v()iW2y|&W4y=<1INJwH$kt{)4(RRCk -`_2pi5&$VD>FaxUKU8@yra)jY7|b^YT9)~S1;Mhe>XHWmOEyJbRxDhIJgqAp$nS%JYLCN;SILI!?`gh -TCD}@U&4qp{n=T@c?s%oYZNoBy0b;PkiRC*zDKE>sWT9X;)I7tlef43g}6ABJ5iInUZ+kO3Yz -zHelozWO-8?$LumB|8jp1zN0uB$YxmU+235(STvWfD!;MUHd&GzS0$&=+~e<(yF(3SrIc;g^O6qX~7x -PXRp#a#7RmZ$OJnK{~zVWm~HuC$ypf3z&os3CxTTu{N*eQH(bC*aNrS^0Oi7rEx4isk0pl -f&S^q8Et(b=0F4?Pbe$TiQ2ee#f(`q~LmKPD^)b=DH@v=A -n{zvf}g%hM#PG`}+s7FOkD5{2m)lmkjj%#w`VKO1Ra_q-G+A_`ET8-hUf;2NK1GSA#zr3EA-(~Aqfb# -_a(-_(mAp>dj4NR_uzC8<|CQSl9eYMMtKaR-6igjKW-*14!~n>0QrysS__LM1$RR(iVt%G4?_dF{En0J+-ZA@mh?;X -aVK1MI89fX5^5VtwUj~B%_IAxPl7Ji}g493CNL`>ck|J{-rZq=R9Ri13jz3(Du>C2k -+UFXRw?Cykb9|D|#6js|Jd5(n|vz%C5N$zksY|KH-K*lBnWO9p?_NBe@Z3wpvtN;TGbf3gPbkIG(Hf4zX -D5|oKK$r>S0;B?rdMu7`1!0vefRNRWrQwd3K(=bjVe|476aoN=S;n{UB=wK*rTUqI^20g9l-=>&HZNn -`MPQZ+c#VZX_Zsnr4T0I0$*M+$sSTCD&8B^6aF?oa8lk%27OW(T(mLi!?H#e(TUZR}soUELyWw(w<2< -T2K#-6Owm~c{yHw<~D6ZK3CN8Sy~Ln4G1eS>zk0-Z0=Ui@DszNMI;qK`wQm2BwWNQ|*W`La -H+$dAdWZA_w~HY)MvCjo|SZJ7!9^$9x&qX(j2M=Agl2MjGe#)?T5ndIl~&Y`adPwWhIPaU#M6N|l=7B -3z~X5GF`hUsm?&iA8Y%W+>=jUte+V~4%Ev5O -vRz(-V!le^n{A*w7t~?7%QL%WRtAc6&byWp3CaWix7Ej)7{UR^C -q12JXF~J|TtPXx;$&UmoAxqEsO%I&ew_gI902kZ>fNQ-Nnvf@KPD+yEKE1oj8pX&)q~gHkQCh9(1KP` -kqKBZLjr~nw&|^C2IxN+Y!FmDYpzi&g=uipw^TAcvC`blqXQuBB{kvTQX&2oQs=2#yj?Cv@z(=DBTP&%;cQhHlpYAfXU5Q60_Cf;rYO}f?Oc)a?{>4o1Sf0 -nTo&v=QE1VosoWPr`dv%^Z8AOdDirl%q8*8p7BX}UZ5sSZ;qM{$lmBgJ21@>!~NEyz~x6ntEM#2X-lX)ga1@KL4Csl-<1iH6L-2 -bZmiL9%uJ|HRPsfNnrc?ksT5?0>ICN-!H}==OnFB*AsK0aQ-&p4 -;boqt`Id<~srrd$QwAhGdJMJg3+R#WT&w*a^NQWkgC6Lnf!q(>GO`AAv%Q7hv;GHzCG+Qt#I7pRM#yj2%760%O~(8y1N%s-M@5eV&TP;H)CsZNw1KN -6#12gD<_Xk)#q|7iS4E1T(bJ?~pwH}|$@ZEW}|q)fJ@14B#{7@IhOH4Xu1z7I8}&2WSvh2S3780!R84 -Uke-7!TODc@8lOn-&F|WuWgdT+u2e&OsBP!39w8hAAM#A@023&!SRNI&o-rwcRg5t0;K^j%uqQWS-B! -tl@dZ${lIyENx`(!U3CKo?cGatKd1lN7Y>(6qzn5r5VTSvR8D;Wq^X#ai)kN#C1iuAc)<-4tmgG5skxjwqR?yV1jP#}uPW}q~t -->3PJ|gaJAoF=!ZL!Fj4lUF$`31U?S$~KrRQwEKjh6`Y_A$}y-GfH0$`Kj!dv<(mruoSox~Ibr7dU=; -mh>QMt#VFLH$WWEGl7S>fPg1E`{2vK_|b?^g^4@o44=&dIu}Z(5$58^@e*}`wo|afN8+$|w}*#gb~yB -Z1h3WGgoOvlg88X1e<$d8%j@7c8jZ%i{D2$yz^*Uycz28%Skq3>H#-NS-k?n4OPe10rR6m7RB%`2PVe -(1W}7@tmOFWz$D6^Jb)z#cuyejfceH07_2fr+vGXjE9b&rRD1->O8tU!zoL#?bHynBG4*MvGqu!Oh8@ -@rPQ-X>vR{-`b(3uUKF`yYuwOfM^@42TAq`RBdb+-ZkYqvq;)Y--$TGtsw(7$dwP4S$jpocP0nfF}hd -82%@0UfQTYJkgN3Zo}sMPN9ejBFKBjAv0E%LHmHg&&=B&L|_?i$!N+A-XhqNwx-ed?(1%Ro=T#-_rNS -rrae(5Yo9v&|rDYhO+5MHx1j-G^agY1vEqQUH0}Q|G3q*7FSvO)rZq@U*ou){91RVi4XY@n-+|`P4nKs=F_ZmmfVdg7IHVE2?~Ya$nR -<2K;s;%pB&+$pp$5tX1(Xi1tp`(rM(7;Nzdmgw_l9+~d^sF-2BbVgc_vNp!GmvdhSBKVv&s8TZ}5{To -pVeGgk>4-JOx2dH0sWwM|}muhJP#nmUS(#(S_#!H85n@0G&M5*~)oOmlc;;A0TR!qMJNM4IHN15VYJn -#9T1E$k8a`Mh?1g7sDqwHBE1yeBT&H-?j~MPQ1Uze6u`6v(hepJvm!r!vyYQW!_5H#g@5Pvuz -_J5X|AMrY5HVmDdVS=1g7HzgWIX~ga4pQs6aq}Ehp=p!C#45><^uPum^vbQ;`LbKf{=P;YQr6lItDiu -MJ1^5S*e^R=qUe?i*wX1Djfszi*H(b0l=}i;pJwPDkPIDv?>$GEc+&iota9+v7E2mLsM%1}r@a_5KH` -DXrboOfc=6oK^&%U2szL^_9+8R6;JSFFw3Lob!h8px`G%=F!4<5eAd<>FgtZ|or#tqg+E~+`9X(UHe4 -iAIWqursQA?hss+=ZdZso?0Zj+nM|k>!O=rJ~){J(*Y{CAdA~yg^o0OrhKT$j_tEkY|kc@^WJPJ1EeG=d(I)2{;5c -Cr*vZaYKUkI$#!aG#T-Bq;DW$4eSP~E~rk$pXZzv|7J?XoV9uWm3qhFZ|Li^^mex`TDQ@`w ->7ZMBSw6=@Qbio`}{)zUomFz^!rgS1Z9rRZx6%4F8e@96K;O}Xa@BF3+&8ea#d{-*$K&yC0Z@*tqu-a -gW7lnwliR|`o~h%x?hin(ZjLm^ja8|C -P9-5;U$I2hgW21|+nov0vRF5RkKzsh3>vqtQ{nNizoqGk-XvCx-We0=CRWb6$ehqB~cmNmlyAv|J;mZ -yy3&rg0C`O=c=N{R(+@s4#PG6I)EfRAntmd2QJF0a>!?x>NEmpj7BGnPB*9y#_@##>NK7w#>@Jvvk(i -MKInn5)b&P$$irm?t9;lEGwgfbXSdZC*CzR+i@}HIA`IeYUmBHvPaz9hlvET;2Y%`ko`sHpDvasiOB9 -(5j(_@U2(awbiy^-G6Xv_~_1B+%Bdo(d!;KQ<~d8FL%g!SUk)wZ<;bYwZWKM0cC<=_A0sO#t{}UOkUo -hO9uQ6{P8x);(RNZr_{et_2SR{7hhTVGn?L2$7ot^>#a`Kx|aD9I-!wud-bqRr_VYRbSSJZU4U=`X_| -B+;b`QD9CN5Qq&voQ-7{sWoRou29c(*Bi(xYf_*fK)SZg_`zh` -VKbdSXb@*|@PI$#%5ve0Mqf}*BWtz(bmsHS}LZp -)tZOD?30s^Sl!_TU*%OIi#`e1V@<}k8XZ>?D@sE2*fo+vM3(F?#ICBao=)aF?{|WqSt_R9uydFS -!Bq}W+J*iuL@|FVC1VcQKCQ(wQvSc`09lgZ7#cZCJaZlf5O~_x;5`61f -$x3eHPPq%4vwEI|o@M{ -jIhc7s&qz1qG?r)=l#8lz~$3$pms7y5tTx6o24%Rol25tH?ptZYYQgDm~#oy -U9_t&?2!=6|-u4z@1BQ4B`Qh34ob_{id4N1{sZ>u!98LX0xcz+WyYzQKlS3bz#7|xC!J3hVF)jA5jSS ->2Jl)(73}%`V;(b)MzxgC-!-4|%uMfo0toky+mq;<*?T(;Vv|?E$(_{vq$0*4yu4OYXA$&VGl} -A=nmN23 -j){6(k1fc%tRRHVPdZiA`GLAAUNrpdmQ5j$;&qnSAUc`*F(#GZnD*c88UH4T#rO;*<`~cN{e#}Y#1b|2-Jg2}8*6B=d{EFGjr%wUG4@$9gM>;Koe*;iU0|XQR000O8`*~JVo&cQ-MkoLP(~ -(^baA|NaUv_0~WN&gWb#iQMX<{=kV{dM5Wn*+{Z*FjJZ)`4bdF?%GZyU*x-}NizC>SDr)X1_o*^O}Az -_GQvLj2HKa*SZJAV-`gIj7+aGY?Trko)adkA8PkmXl4eID{}Pa;B%cySlnwT~)Ja>UA=mF8Z!-#B`bz ->rLHsNp{oLW#5S@|2{a7*G1D*wfa%k%Vkk5)z7w`--=HE+O_KIHft(q*B&geGj5g`fOV5(ZE{7I&+%u -hU019{-FK$tHD5U3#_7DSDlzYhx>8RJ4-)wNE^Ecr)f<94<||EoE2_(4Bdm+B`}KPFO2gobKU`m5#;% ->;&&9^Qbmh1EgJ_CHA@b-=9N+Y2H*M!Du@>Wlk(`83fLnJGugba=`DI;~){xKFn{MY_`$1&6XfCs+1$ -^7r>$ZEf%BrQvt*aMRE9S85AH<(~(RQDBnfTtDdY<7({*aZuji|TT{Rh!CJL}Rru{9I4u3t8FSI?dC! -KHiq(GM_QxscXXcfH*E3RRnIew!_YMpRqedv{W}l&blL -e>)}ReKSz^S&%G%Y^?zH~pcdZ5TG34xMJcA6x-GB^x2-$DtZe~X(-qO*bi)MbJ^(~xa0Wl@yJ~mK`)Y -4nE&8TZcRx1eIGKuSUgu)kWzACTYdNjzW}TJAUj!{KG7tC4p637}-5w`66ETHb2M6~J?w@3mXu3-I(! -l{RLbxv3VG2*bfv=(&PNzsu)9L8oVES4tvcBw~U48REux3NJcyO5D29{UagLw&fOw4A&n?t@iI-|`n7 -z{L9OSt(0`Da;Lv;h32F57X^_VX3YnI1T!#%HtCWop>NEXk_eZE)qaNDl}|ngaehamHgo#^f|Z6xJJ2 -4M#~{3&eG2@SVUgH=>z~sypVjOL*;)j~^~t1#r76V9Z6E^sQL*upo*d1 -D6)+2as)Y$h`{J&x?YU=hXs-tel^`rcX-keyTPl)v3&r -Mz@w!J$s)C`_%nA0j(K@ZV)6EKyfOcgMV3S+#>%bug+TD-$D~{ejVwW@OxhHwU$IY1?1}+O)V+MQ-ig -mNsKvwAoDwgINh`l19O7V8apnyv;zJ7i -xZsyNU-V`?{tZ+g5!}9sBo*ztaO1J7Gh9_%`%WXAd#@ab&rZ97zYkdNf_M29%SVOdNo8H#k7CIgsgR^r2h(U -`Cb&lFWxYfy?gB36Nb3ZMv&k67SD5WV_IWkv7B1OZCISjs$5w_3ut>WN83Fr`S{Y^-6S&?vZ5ny19JyOwRuyq*096)ve@&=X?u(jhGs2spe9B&Rx -Y)&kGcXTIQ?9t#IT+l!aCSY)KJl9PR!>MW-5l?kClbC~n$A(6nDC^AbrR7bNPb=seAT1gpT#*Ejv%9P -)Q7Dah@lkp)KpdHd)q|`o0iwmUfi`$njVMJ%%c2BUru4kF0DVv_7VroXz9r(o2AlU~*6dv=AZ-ETL-w -h)&TYo*6QH{*Mfj1}ai$0=B&%WY)TR-4$bVt0);VhQu8<)cr?un^U%)4eYF_p*lPwICt+u(A2^!y8c8R&`(IihzlPa~3a}-lqX(UqTnOK3TKlsqlbg -!-ofD?Q%1?FBsIH{`B-IQpognYmF$+$rc7yujL9aBXtf7!22HeP`e6xv&4fZ;>LT?<~?JXW;4zI_u4l -Dl&Y*!ndjKdaR(sXF>KYF2HI&6nnI3NZH7D( -H*3D~;2lRb(@iSRX5o%g8lal`1k(_fxbjrli8CXGacviC^UHh!oETmNzHNywn!{j>@s3>09Ti0)rwr0I1 -uR#!@V`?}`&|BD9!WX$B$gk226LrTavU~PW>G`|sq4AY&`i=~ckv*8}l|U=CK@r*lM`eu^$Pq_ER;sw -^$>o)7`ep+rZJWB&uKM-V7P6D%S=c~Jr{J#KMsP!ID4Wy_V%=FCCg&9y&+oEjDUML}T-FT;k2OsIp8% ->EBd^ZxYOq+)6%4x6qREzP5c?!bQxD`PvVCy(+*b8gnMT0#N%c*#9CZ>UbmhrK&CY@`uHjZc--u0_vE -JwPQ5I;=1`8zTo`Sw7I~WeQhpFsgFiw8_$yz%9(H{qdrom5YU$`{Ar@16g?F`NDI!m6kXHNisAuhZQBQ0D(r!SKi4d`s} -%`*$GwK@n@q12Jd?&!&_wx=VKs=7D#o)V-sFI76kRPlRv*esAO+2P)Q`qZWlYf9>y^nzgni!RWKuIXZ -|$K^Rp?Unu>HcjT3(4Pv@p(XJI&NAGG(ru#66U$d{f%0y@A;WHO<4|V&^RMg_aJblm=g&?bSU9%gzLhK%o -|O~*`8e}saE^TiPRd8_1dIOas}lTYiCB_8d3-~2%c5)vI8;%!J^{&hQNZK+U-3ci%<5}Nsw=VvNK`Z>YIv_DlXB753>oOj$B@W -?pQ$J`xOYGvK+(zM|fJqW=cl9(IROid0Pp%SQr-$8p$mPDYT8vBjW@{4oV2%3J7`$A}%jldE53{veSk -p6+Pq};w4W*F`Rt>mWb$xo6Gjr5Ri`#tmB1IK$U^lUk(WfGy`IzzJXM-zN@Ergayz}N67?Kl5o`zE%y -a$6S<2)P}j82W8HV`2HHj7*e*|bWDDvYVn`|r+LM(Gfg^y{fO;RAyb)k958UeH!`;!U6Bj_CJqnuJ+d -0O^QG)T|krwavK@I4OHgES<&gL7Jz01>+n+$0MUD)Pu!)Kk?+{L@)DQ8cic8|{cra^+_171L#Jdf6#* -+=~u80{2RZYsvCbr6N$2Q!M~x7Z -Xz&3|5=FszQ47N`tQlDpgGQ!iP196cCOffD5zJoROfzftXt|oCuIzMor$`udDEl`363`~YU4F?txtD! -LoN#x?JA7W}2Ted*)h>-6BrX$;H%*zO@BlVeaD7`hGG<%(nb%2LGa>Ks@$pkdh6w8%XZ!&$308?Irl{ -`)|eT<;uyjtZlf9SANk7@&U6l@Q+q~oJHgoS5P0&L_paSYE2unblYKC%I+pSTLFJMAAtxuJOg&zDCWO -w1@89z2n;=Y|@aI$vrN6)F-qPt+N>QR_>f?4j#{dAxT=LiB;@Z2{ZV-w7Yl;FX7Xeak0M-3j@Fj8h-+ -jARIO51Q`0UT?spzA4J0+l@VV`m_e)Y4W6m~YKw$kyxKNGc`Os7b*qTO{Cy170*r*GrS3hYf2E& -=K1I}S3SAydVvKv?fTrPBi;4hdSpY%JYrOAakVMQUf%B;hyLpqG(K$bZnagL#NsJ5UG=~hj>bq+ZRL(aUz9`&^Dk5g8{2V@I9s9I2=DX;g^NdS2XEO-L?hVnKq=Bkx)?}>(v`Z0G~FZ!e@JhaC_iUBkTcUTdSNp`83_VPbZf_g6m8p!(`V1Wer}_GpfNC@qi$Y_5jiySf=^m-N0$PO -2SU+EH+1j;PZ>`ES#XkQ{wsN&ew}{x3t2DX8|&;=C^w{ctdX$hvL*+-%Sa}QPon|USGOXUM-a)=r-*Z -4`3h*{EWpu`MFTu@ZHdVm-5qo=n6U7$wKJ&zTfshcZ=IMvQloa1a)we-907*g`LTqLbZ$HtDuLuH=Vx -LgC?>)IvM)Y@bW%F=Mx6n}bX7mJxX!6Dek|2O&PK8zeYv4S!61<0LAQ$|!pG653MZPvSFlWE1&1}Nu! -@I0pV~!EZcn3hOX>!KP+k{DZ_y$$@<7_6b5xn(me?mPFgSyuM?5d0b5;O+2^NbY=EQy1$Y>xwmfqT|% -1kwGGkE$Y>2$uTK6K>i*a`9Roq%N@`n6h{*OZ5)fJE{Em*5%SL$g)!baw>E_wMU4t4N``KLRA277)ypZ7X_-uCg2_jvtze-mb1g#+a2EI!mYrX+- -D8qi}&X$IVZB8qy~sQb)C)MU!A-us{Y<`yng-u(>q!UAOE${DaYLW^27V<_t$@fr(wbsy -GiPbyYf20)o($=5lu0_?NV5sRezHf^$F;R%aiBNPQN-iJv}-7%}JJHSWgeAJT9tZeR_GE9qS<9G8z-Cx3l@IlVp}yl(;3vE8@8QGo3@WI@JR-NZ5z{n%D)*b` -NBB4B~Wra8jL6E_q4~N&X_5T2q7c$N=vr$B3nq`K0cWMAQ@hSiK!TU#z=3bmv109Qn>@fo1H8BibWZk -jhdzUy!~7rn;p*a#uG-5mCjSriwCLvLrM$_V!|rdE{=z4Q -nLq_VpEAgpyJ~}{jewPd;gAeOkh(O~@FwU5_3*?J<$4#DNzV9Q1h=B|dF-gdqYot|LHRp3TQ87tW?&! -4J4LA16ELe`w(ooeD!;9P#pjm{IZ6~UPa))HTbISHNUnCkHP+|%MK?Mi1JM(gJa>3NiOUI+#>|~EBGs*lFNru|@~ -*8g(+NbN)1pkuO! -NS`2s`1~d6gG#45KOP$61Ea7Zj5ea+-Or~*wWx_ly0=HcL9CBA%Vp*C&!Tmcb*nIE?jzn0G6ctdc#$7 -cM9?M#GNtTS)$_k9w@nB5S!Z~$;|q%`yq18o`5Ytdts^kqs4nv#5+)>$;+Jr4GZwHy -8rh6c`346Q^jxu&u!OpKF&Yw7M&D*Oh;>GxZYO9F3VSyP!gfsYi{Qo2TC%Sn$3c$iXcVS+`d_I17L+YNSTg&;8V8p)<1GkV16J1cAaQvuGWWVPn~E;aj}wk@xc@CIrcB0-|CpGJtt+>8h4OJM?*J>bm)%i+pg -9v)szhBVw}hE%O(2294HryW8^2tZ_%B_xW|)g;xWt^?94_^!G-&33we?1lwYDAP^Zd3?|m^t0xCZuC(8*4 -kye!*VXSNbr6nVHsu+XEfRDm|2aDB@-@>FzXEOh`ep^5ns>pfQa=;|0d!VlC4&V1Z)>Nv(cuYp5zyv_ -=6Rhm(QIV=k}pgXBeUOYP;!++1+dA@k|0sHXYPl?kqIPlSvzgr^uxJ&xqOx)PBo9ud^@+m@0XIoTs3l2 -N0pX+eQPh!iAzS$i<}5e4j@6b{k5z?%Qws@%cMR>$IS3EtDilWWa8i>zbsi#{W-^zsUBy!huZ&j%k5D -;P1@}m+_LPRKvHZOl3NS)S-?BRr -1RrR*JM?@@FH}n<{G+G%&Ej9K+3{GRSq_Szm0G<$Lw{+-MI#F!Q86V^;g0v0&2C*>(291mM>&2SKM-) -4oi=wVaYAZg+G9BrKn13KOUK-QHriD*P~?OqUPPzuFB=`M_7Af6cvElslB`g@`t0eUsE2T ->SeU}126wjm3slIk?apOe(nvL-LQ?&-i$8GKwe&{M9Cv|nUQodhbVs9qNmWgB9cVe_j#Et7R9C1l4@# -Gh6G*MBgQy4CWZ<)th<`p*??7rBV=h1t`Wc<_1T<@)EAy20_!y-`3et&Po4_xLV7SOWPqx)!lK4A);O -+E*-04olF*$9iq@~rhuN-O5AdS}X3cLPb8dX%W8CvJ~;dC+g-c_#9=H7}UD}#~qF|Z1atXhzGWg8#m6 -lOxhlT1N9;^E!UEec9=dMKYE3d5pDB3{Rz9k?ZY*?~nX6(Dct=p<Xg~IlUN{hE5{SiG(U^N3Ne$Q};{s*KqVpU+F4k8P-ceuh_z -ZW;aTBSX4B@ib2{5#7<&dA{g;Xwli_0XdVM09p#Dp6+Y^BMZQzN;buk;(%-)cORfks<=gnn=!UH9H-GfR~cVTROmo -*s15r(Rtb~fc(0sFzEgK9Sl7tYId>=d#Fmd{+C@cp}Y=dZ3Y*T4cszDQJ=>dlVrC&`AfR+Fi9kznmcO -aLL~!39e?Nq=?gWI0d^41HvSa8XJhCmKXM92{<4Qsp+g)aULoH187=YTgLQiwse>dfIlwu+EE%&>ooS -tq>b81mp%xT~YutST2;moiLjhV?EGp49=<#+4+?fctT2fHyXm!>d!qL7156Clpfy`D>IoI{C>udDr{^gf -pV&Vw{CCI-{X4vnQ5^$8|Kxxs~8C}Xp#F=u-dcYhbT7_^a+ldM+F&lJ2TNb7VPgTRBLK9n`(>AP*I>ZG&0tjimc%zlz+g!hnu37*f8!VM4k?E;fnRLosVTac~~CR44MvGz -%Jp2i@uukhWEGV2c7}iNWCi;j&j-$hm1$#lm)WW$ -_=Je(RWUp@Rc0u6M6lK`d9&>?v2QMK-4FEJBx-Ez3RV`!-`6brm4KS*RZk54nCmj^!Cs@tds{}FX}zzqp6b!O#br2 -n#f55AiX~Ogg}W(e0AD&Dhqm_3)*&XRqy~tLrlX2TiSYJ#P%|>60x6j29&#`zw~P)XF=%;e*j6%y8O( -1{+ZK~{*QP3{b2$3(`A-k%JgW1y1Z{m7@F7=+Hw6hy0uPEd8PMvI8XE=HwpIezr#M>N5GM-ud>(e+_-JFQ#FxI&0}aT%lUS77`uKbgG*gdW1SVOEiLGj9 -qXK(CQtk#@W9I1NIipeT(RwLB~wG!l;nAw9>GbqBBg9Yh6dc1?BQ2`_x=B#H3W~Rdp#(qgV7B-~zpHyChp?Uh>7m0pR5 -2#Q_bUV0<|xYub3E97PQEI(pI^$!a{>6|CxtJiOg?#g-sUQGLG1CU{WO->W3WZbvz -+b$r>An71n6~_(E$%;f4Yp)tA3hCp=7ka6=$|Yx(~8%CwZP@gadCqa66OULKerg$J#NPAGA3JK)t`ng -)i(zIgT`8D1ZwQ*YxMl-ye$e6HaQz2R=Adwd|7M^0pDv^+5Ad{*@7 -j2pOhM}A*ZbCPO;_<*8kv#=TJS8bSR8Niqexu73@~n`*JZY2TZ<&8cw0cYbGxCi9_Rqyd_M2Masn}=P -%H-dC=FlWsY<1~AR`IO2zWAjAR$0pwIAwV?f$`Mk&}KZmkV6)4$Mm|E!N_Aha@R{(dei%fVSaahdH7# -GbK!~n@>$Dc7z(0WbCF-Bs_}0oVX*geFo}S~tkE*2P`OVMmV(eGw5)Lq|GFIb)N8z*NoyntzCGElE>a*imz!qD)Pz{h?tw;`(tIcEnh(Rc;3B*>EKN1- -9+V%Ul0R4>TF%-XZRRL(m26_6sU|Pq~uG|{O}x#o5UW`cRS@2Hwr-OH_HaLCPDN}Jyl;5!DST{owG~) -?0IKQyZ@yz77_gWwJ`2YMO%K3mLjs)b)>ck7)3(=`Qb?s(fJe%IQy@&1tBKlhp&eJA8AX-FP -4>fjV0{;q@A`wF6OJ@C@ynI^o%1FAvqb{dmvQAx&&AGV6wxq&#X|GuoCpfMCD-Tg%=BHb{tJDu(8kXo8I!sEw|5_8ZUd -4HAFd8pM*tPZ2v+|T$9tH;Tv$4JI6k&1iYoaJ+PH=NBs`}!rlhN10&c@HE02WGURc>P}e_+j#0-OROp -Dustm#O^6*UP18VzkELMP)SL*OxT*xK;gm|0hY|D7T?SzkqFbaI-}b~JH3-Cm{%BxPZ?V}ZVVYoyd>h -_W2K-;#$v4xM?>Q(Axdcb9zAJ~(qs^ag^53yt7W1_0#^uZjJjH4)!O{7po8HnAv9jsLr`*#6{@htBOx_lMEHpm)Vioz%oq?d6!#Ou*&DYQ)* -NLez*<8^E!u!9Ra*v|Zu{PlJBOZhGvX{5j| -0Iw^r32J-UfZAT5=f!ue=#&+f%tBj3{D##DB55RAV3NlN%mt`%}f2Q?4&YHDPqqR_NVv)6n~WDXZk-^ -6uA2?O=JSGPsAymGwTGu{^I09>!x63O_e*-owS!!S7}~^CtUCL1|YQgx=yyKWT&hxkt~F1}O6s&_I?{ -o=QL$>V5XsdlgJs3%YnGU(;lY&SYj#3eFGp78{P87dE-|nlQJYmWFe>0E_DFND{_Vc78l1u9GH|j`z? -DXP$ras~5jf1WgRr&hq?GzUqf=h$@$tf3lN5B?aK0A5YJYPdVUHRm?9=9?n48_l9uGm@XYYc40mI6#-eCbxWb4iV -lvCMag496P75a@|9dpr;JjJR<43*~c4p<%EYo}VwsS6z;#3Od%EjMRtg}27PTUHfFm4%6O659gkBmld -*dN>qwtR|3?xa?BG#bSzx0ZdoyZf~?Mw>}BcsU<2_%|MpX^|+Ov%-pmt&hy@q-B{-id4*C)`+I(Agv| -!AKWOZwv+JLk$Uh?Um^3C)~UC&vU$+bjW$f+49j9pDh?7fD8^Y1VrmVE*nn50k}irSR-~ILO_;JwSPL -Eq8t@;E&Tx=j(VS|aGH2G2G`_HNc45ctf=${|FPqwpNQzj5RSku2*jLy_F5|}(k;)5`YYUN3O)vt2Nq -NJfh){~6DVyAAC8iAjURLPcihmWtIuBq4<+A`*vn+UrJGSF{0YPSUEV%qvtqGUO2mlwP{QBs+{e_#YZF;K61;k8ci-+d2c2K{7wx_j`Td8;&(Hw9(eO5dDB4VA0v$o0$r!($ -k410k^=-M^MQP}rFD+dJm?(Ue2?#{c&IvqblC{2x4X9C#+*h-0~959BF9nW*2fU{)s&iKRi-Uyomxt= -oEn>idn#WPq3u4yS}>jKh1DrpON?AE~jz>Wz}7jQUibS+cqjt6=499W;XiP~AY=VDs -}~SMqf6=yBP%>K6G@*BIbY_i~GhP*JBlt~(jFKekym-bUt*u+^xY~;3+}HsA5>L`PB2~< -XTj4D5P4EE)F~`3`ZWY^sf^)M(z~bt+2AB=F?qHA(kj~MJmWmc^h*LAEycV2}BfW#JO7kl0?qnu(;Z_ -dl{VXCuHbB$>L8Wh6KinAB8I=`oLy7jtgVMfn9JH6Qxn%CBs>N%C{rYjSjatsk -Z=d#grzsE5N0pT34;S!08nj26Af{pbXgii}{Ed6=~7+A-|72cp)d -|Ceo+%!q1`axHT<8Z`T)7CU-cq|vN3su?vePI^}(LD?>$o7oH&bs+3w&w=abU6V^R~ -fm_Fu>9|yIIH+<^fm}lYg&RpR?^E~Qs$*+_{g*FZCb4Rk8jj5ZC9_-9Q6TD(o -20ue8hjb)m#tt!XAhUBXtMZw6T4yCS1GBjb;u>?sta9#9J-z6TCmIiPfPgql9X)blERmX=XAB= -#$8XxadUweTGH{4Rf`+@Zf-2RA?pfz0l%SMRC&*J>lTh4K6$eW}3rKva+s1@^0u8iJxB0B*kj@Xk3q( -n_$~0F&D>6@tXth5@Z#1QghbQ1SEeO`lEG;9w|!1f*)*ZN?L-Y|E?8aJ}aPunaj(9T`sp&jr1*yrvnd -3;lrvqFd|=^(c}_s}-KIUIGrQ>Z%N4A@=K$U9?FsL2G0lc;JwoXn5!n;SZf0!Gv%A(sdEK^;GoH9ev^ -gX2A_6mFH*W$|Xnkz4u%g-@&foZOJLZ9Pe#rvloSn7KKE5US6PzkAWb!oxrNV&2ex@-gob*gmGyz5>QAYru+%rZbIm6s>)^wO*c{c_SY -i0A>oYvqXpTHCI)I+3Q6%UZ+CD*%UNz~vA+4MG=|G5*6g)>cdBA3uEjz}|fd3a+XF3{iX)$mE9lYxzWXH}&fbcwScHP|;TF#`AUrlm$CpTK233! -*Qg&sL@=I7icRxPF=KLH!brT)XydqnaB<;;k1MevwS!99!A$ONt37R&ptq?^K)j#TUH&`i -h5TWtpsHVdoZB1q#d&4wq^~OZD#ArvS*}?Kl~~|AH=~Q~EWh+lYd!Ju5#1>W?~onLvCO4l#u7+Nnc62M!7#J1D^3s6e~1Q -Y-O00;p4c~(=$C`&9L3IG6uApig!0001RX>c!Jc4cm4Z*nhna%^mAVlyvhX=Q9=b1ras-C28Y+sG0BU -!P*ZAP||8*0y{>1GOsZbL_hSaT+8!w1sU@D{>_J?T9@}bHUFCptY++=32f394nDz8#3SJ@qAe;zohN~MuUd`t2N2;F0Y7XWX -1JaJg)V|^OxqgVQUpl)&sJa9DJ8GexTa8Eu#k4$G0Re?GYn~&Ms-ktn+fmWVq!l=4Gg~3Cndfo1LEc+ -B=_ETJeqpe&T7tOR}!A2%Uu*Xhwu(G5Mv+B?m{K-BM08Z=!f}3BR;K!PtryxR^=4*`r`d#$%FXo&o#UabH>Dl}9SFf}>VnWj7PD6m8nt}o(5&>M -(G`|;$W~rn&eer_4=UUD0>G$LhTPX`MKRS|mo(h_9xu$u!IoirydW4n(f+s=~Oo8B!%dic`J`fcM0cZ -%nT9*kLP?l&Z{!jovund?P@T2v}vvl9Wpi->OSS<~3F3S8<5(7gf$a1;bOThCI;j)AimloQ9mxkS$a> -^8)GS#j|fvPNt@|YVzd)Q6D(L%B;;=U4Pf*FCXl8lvmh1Z}tK+C$&-!$gP*Y_&9p1$yck>!}>2(i5nP -6n808bpGA9(y-8H|0$>4%}!F7-I`kjh~8z_f&Xe@|3i&UGJ6Uz9DRgDbKVtOhifIHT(HP#0yFMpS2k- -)V7PV%)!QH?Io|*f;R7h3DIy$1@9iMqE$;!>yP$(`?uMgkSFBrUTy)DvqLttQnzgQgy>}(;%(WVaTeI4$p=|8!Ez~IUTvOI~9kvMYR~J6>`bIvCkIZ+@5cLi{%Er$_AKnA~XSKERZmQ$s+02{6&!_M`G4^`Li%=<73PY#}6l$HWi -_AM+&hui-Tp5rdMe4+xY!^lw!3(6RK6?}Z=SY7^AZGC9f|PerxztzvB?chbVa8jj`Y`G2W`D}dE?{C` -!mRgqh2S^kojcPQb@lC5>~)0FU&V>+AB-z`I>sX_tQ#4EmKpAwJ}DrQubsonsY01Un=;Ruk{Xh$gt2a -f6=)8Nte4fm%J-9YSDE1KlGZGd(`6;(m;@rjNCu%B9-La4I;hYq4s0w(IaD1Zs_hd5k8z~rDvlimK)9 -fjR;FaZrD&vqa_(p6y6z66u3U@E$2Wh|zcP++=QC21nT^aF7VvK)+NU>K#)G(Z*k1;RmICOnP(eDx7r -+llTof}pX{gmDgXl$wmJ@_6sW-`97i5Nnpl8qcQWszQF;dbT|ifdp+%xN@XTjxw}%_s1OQIP(I7iKWe -`W{~&|VrV;xVZLz^$*IaJ+KkBG5MZbzSt1jb>I9~2DAnAEhxX41ZC_jt9~J>81{en>kY>S{z*I$~ZgW -(&F*z8IF>+Ay!@g!`X19rU2K>!2fzOu*G2_gWb;S%r)GG{|x+ZwKp43kY#Lbc5wC2e=V(})=oJ~g7!P -Ukri-Tfn8Ygy9A{r`}FO)!?H#Hy@wMgw93jYfwefx!VFZ60(+#aQelK~2Zi*Fu-A-0XKu)DjYJ@6+L! -w`DgJg!}WOYXf)a=qCSP|NoKiDt8Vra-Xp~H{up`6So0n=8J+8!|pj7>U;n4WnyQ+iV$nk -`X%{`bvHO;GW(4j9mOKW=I@6hK3&-`XXK;dWX2VD-RQS7kzyJ7*gvSwk{Y7QV;vSS;5^s(TZoWt+co8!UqfGu)OH`sdN=iku -UJN!HCy*=P-T6{Km(`Rkj_0OQj1Gh3=rwQY=rli%Ae%EckVSIHA{&O>qwzde}nlY#g*sAA3&n9*#Ac^ -EKqmc``p-pZa+e`kJi2#xWp|GR;(_l*o7?JJy!P?I~>gJP8 -Qy$6YuxrZU#SQ@Pksi98R=KBFF~Uw@vN^x2eEbw8TD;o82uF)hcx}^e{WBtm->qu5MBqwBU7$rS0W>v -qPv0CQN|+bgfTeFllD7uiHmAQ@FV7=b8$PX6}469vkQyCI&i+^8vKJQ#*y*)|+XfH&PAKr!d1A-!xq( -bX*}`CgHj_fI)B4`Tvvi{5seFW%F)ZMz=iC;x+%C$=#-yZaCyxvvoo)8Pu8v+J@F -;du^9*gAOhXa#}^GMoCLD1FN3+4hMTHc|g}T!9e=_b@dL4>+01Z$Nyk>K_7@}T|-e(I8f}=()M6iw+V -3r4n3*08i>wPY9O+xyNUzgzJjyG0F=S7keL{%#p)v6xf=jlJrR*y*JI%LRhY(f-$>Piw>!+c_XbVl~B>iGJc26I7uOpgUjRA(%ppP)MavxwkX#Q5%4BG4#d%Qjbu&7jCeZ%-|9KTz*nX#qObuUCJ&ztlXd -x^NygxZe}5rN_3O`J;sm{efdcBA#Hx}OeutUAL?llKPact>vqGuMO&j9;N?(yK#f{XhB_P)h>@6aWAK -2mt$eR#S(ozoUKw004*y0018V003}la4%nWWo~3|axZmqY;0*_GcRUoY-Mn7b963nd6iYcj@vd6z3VF -mDi)9mn?V{NC|v9%Xae|ah9z?5&CGklVQX{;MbQp!Fjy -2Ix?URxys}yj4vXK`mg}RaZwWTN$!C)_WjU6fntv1k!4xQH0vIQhN0i0GTwTU#g!v`taeWooN3|EcrvX`&(JlKmC -lnw>6zyA4cOm2xY1)@r++AIyI>F95e6jZZKtlE=P1l9{({g1V?*{H#y{4LpWhoJ9>%+Z1+VdwMEd#)` -=VM*Z@vfCBh<=_S)#{yLWjUe1rc=bk>tb*&NmPF>J{5BXVMfEDm&yIC3W0)xsqCpV<)^Sy1%yktaQE*)zPn;iwYJfEuPwXyV)e{cp<|A2nFKE`aJyVq)(5s@XtPA1iN|tH2%Tr7>4<#E=CTsUxR;sCf -OreF$N9x25SEvF2&b -T2f}f`dY*RqX{4#VL#%C_cs5z}4c?5^&Oov84#`ODwROw+TrLN60uvv{faCp5tIm=^ON$s^Ow@2x0tx0rDez&aC5a)4jp4c*4~+)#7Wy6d?Um5{>ipN94a$>D=5+`h*}k -PC%{U{6fdBko4ii>#)B(71U{_j>&};k8zdZvBvMBp|(Ty8bvE-7D*nW}uT95n|tE^v9 -ZJpFImMzX)_uh^%eu#~5;O;X(JGoS)_xwH+Mq(PkC;gAccmAJCDrbr!^w5=ZczrXp|Pm;2o-kpKi5_f -lYcD`qJm~B(Khbvc=A5q -6eGYaICZYNM*QJpN)-@uIF;sVmjk`J1|`c=`dLx)ncu`0(z9A9)8O3wn0Ze~7d5^)WY_o!To4Q{8UVA -;Dla5%BZ6-bj4>7g=`dWz*Enf}X#Tx9?Tk9HU3?n)>#5K|p`0uB!T`8hLKI1435?kNxugb#KMoB6N_O -D|x9rI!532MlyJm?dxm!)HP+P*D%r;+c%17*CfL23l>@FgAvWZW1bnZtw0B+q48f=DgJ$0_pWjjZ5c&2%BAM6~ETp_L;6V5OC%>{7L_;iGD1d0xpqNa}ntxwv?p|Mc>2fBpIWpD$o8vvhi;0Fo}%lzA}SC=iIj130kp4`hzd{C -8u@{8}{z2-BOA7rKdO0R>xi+qy@%^+6hQQ#ZvFnLYVJJTFT@%oi|<141^J-5$i>h;}ED3td@=wGu#tY -h9>9v^9*tcNG6U6KNrIYXBNPGYe5SG^48u)s#mdP{d1mbIi0A9yg#MhVl(B(qu20$wm>&zZOBl*Yn|XxHK_S@epjGJ2x*rZ6YO61K!lnd%#N6FnsWz|KwD -XiwgB^q;&q{TzN-+bh=b$i$7R`qHdX4s;z921tj`O)_yF;lW-~`>Iz*7VP7vu+O1p*K9XMv&<>`&gpL -)9E$Y)q423h=)N5oe$&IS0K~0-qOzD^p`c$pMlPTWceNVIbAd5Dwa;O=ns#;4oLPz8hh7WQugPDZ4_^ -YHJ>J1>eA+;B1LjWUni|@AlaQ4f{{k->rUBkqwuER^H+xxv^fxooJ|y-+SWoK^oS!rErc?U+qEWI{k(Cu-<4_ -ugfsRH2FD5v1ot_Ike4+&k^C>jg+^y{qr|>zk;{m5INSmvnJY!$4FZPv6g-GGn_b^$CExzZDjI^w7vH%E9)E3)!qsUS(5qu#Lu?agG7NCp-z!zc*G$jAxWqdTUj7g+s -MiQ8-R}J6Ker+>u;ces%7ti974qb#?7X|zfA`oZ`yM)^B@;`>p(aHArkHkF!TxIyQ*%*+qzO=U3_+$w -G7zJ;(<)JYHw!qm;fU6r-Z>?9$9P1uR2PYQ>2g<8VMMSO#o^F8!(^;OP~60W*rfo|mnD -1i~NjjXbbf0AF%xT$qgtVLe>pT_3YG!~G#!H3ozj(ln593oT4Fs6=P20JG;kCd6J1*&#A5XZcyymklJ -SV@Zbg1(-2%LbNSY@!!I7(=9mE2XssRY=36Y#TFHb1xCwmX250{gSb%278w(d5+RL9c}tdA*O-VTPeX -PD!5o)8nu+)#DvTqWps>U2(Ie+XCluYpwvcVpULMdQS!<$Z@WUgU30lGae1q)lSja4EVUI{XcYw3 -}|&jXK8xi94RNPzI9Wf?&Oro1KW#SF&Tq88UF~Bqb5%hQ#XV_UAyO18@lKvaHu|f}=neO?jNI3JpH8q -o*}Xp$px!N#8dDMgPfm7h|C5d8oc%d7#y@oSg__7S3$X7Xg6zqY!?dOpt`kNo7(59d2Va8^7&H@dkiS -*sDkMo~6Z%Ucd#BY<=AIldJ+RsfiA6)g0oS@37D=96dyEla|Qe(FhRq6-j6{;q?3juRkzP -+I7t87ZodZU0D%~Q!1xHHNV#~c&}JiTs0U9=C9N}LHQ~}23oeK3@z&OTgpGgX2e^xi+{xyEKzT|mL|1 -{GG6H-XAZcNc>|a9ISb1RT_E74LhW*oB|NeK0@XVBK0V>kcnf*>&M!OXsA -=W0(X{rLrQ)&jdSNVbFK^`@?C<&c#6$ehkTyH};pK<3g*ZcLJ^$*f% -euY@+1pIYhZ_p3jUbw~HX| -%O_tiob)Vl$wG|DMdZg)?ag|CEvSGtKF)>!+41!rri)nBmgA-6!2AVj-=W5w671fxl>2o7-s4;xvP}t -(eME#mUr>P<4KsvKp(baI9z{dsL%`&-Vny;s4k&_DLt@`(POEM9kl;RQO2-G%#Un1F|H#GC5r2V3sb0n2uf6++otGa+8lfgq(&RpApat3m~&`skIpf~9|>8=P#Ep;mv;F5IDLANkLxIMnSC9I+W@TK!4 -OQ}!!SKvsq4B^|8%FuLIj&}= -FMM$*YBrc0UlE7n*bsD1wKm8aSlVM^Bt^E)Z+(w_w}C8 -8@PkJbt-hF4opW--o56n8EIB!w#mfKaQNLIi3>TZmp7D((_vQZ3V)YOU$rv+2T*)ei3 -@aczp&F6eQ<#U+fzB(k8l=rP8U`_4liMvW(ADE@sV{f&)gO4ob=Pvr;^(hbbi&768`@(2gsNJq(+U?{ -vA6!=%so4Rv~qEPC!38bn2Af~;6hk6LOHz4e*d7bLia}vHwt~sKcuwvpFL}g<79#{>!$#kOfZoT1Rg> -fcvggTJE|&n#@!0w&*~JC=`HqZmAQ>Q9ccex=z6YTubNB1z}GchFUvCw>^%s=kXNU9@Za7~Y}1ZUrmw_ -_fHtUq5@dD3M~0#Yp=OYnCl%*hw`#(nz|~0$JBljKueYw492c)?1C;RfKz1r#UAqqYKXO6@oN+p?aCC -Pt}bR?Lml-^2HzLLlGUq8PmIdyUGBb1LQK39?TQcbD!)vP&4;o7_&cIEIlxm%hMqLqi)1U;-M@V393U -1glGI-ii?{RwppRYMj^acSx1*vd{YF0+4JaL3hh71Efu3vQD6A`tTZF>4vkB`sj;BXM{|=NPH -*`3ipp0#M7Rj7T-8`$Zk(*XU-gse;!64-vGoX|DI+leF23 -G<}}=8H*2t8yJ=fXmj*jN(cAclsIo|dB&xH?IERFV?=!JdvPkV;pQf@2r$pjX>0y+y9ZHd -FCY-^)`GcGZB^8N;c3G5NbXi=Ofv-*hVsVQ;dq{EdY#3Ft+`1Cq2~mB6%YeMmT@hFgUl*j9gYO$oRS| -mWonw#K*M;65PZm#6aG7Stau-nuWPe^AxsGXrBac#qDPJkrz^LAaOU9U^Hq#G@k4;13UY9Y-pzn<7d4 -D0ri*_L3NS=>=pD~7$xq0s9duaXvn4LXV>~TMBa~vOF`6vgcq5JmH6;xQHTmK23CVv|fdtS+t=>BOaLe~}2-udusIowA6aoWE05Kx9 -LfOxDG65{Yo5}L%@98OE;>431g4n`flwli~#EYNbzJ2-PL$I{e={zo#SW6=B#y!*H1;Bn)I(mw`-_v5 -9TB@+yBKmCaXks#Fy?cPzI027!0kpQvw6;~PsKtlZ7sw -|36UA=84-e|yvE9|xQw#>ll60X}9t&~UJzgrTQ%mmOdpJXEGpoD&_^+kEVjkhtWE@uZ2Nvtw?A_m=rX=-C0Q~d1q#y1G` -hS)^r!KkV*alrDpYjl+0He`2fL{H_p)hhA3_Qu?eh3oE6F0dvNl(^gMSa-nw$OACs&}dL75AHK_OxIE -ZC$x7Uy8Z0vi8FwQmS3&gbw07BYIEIt_?GK}Z3&E(5*w7!qjQYwVB*_7cVceebebh5#ZO4?)Ua#%f1W;jvJe-xJpHHao -9vtTIR5AcTuo>2n&{cr&(ibQOHgnIKfL@fP5e6bgDydqxPj5Vv@)QWHm)orENW%z7IA{d}+kv@8I{e7YvPAuDeeampl57U;x!S1J_9zy=gg%+LL;Hl>dkCYyjVX#TbU( -LvW!1!XD64r4R$>k_6Bn{U -O6fWs9w$*jS&gnO&I`$?Pj@tT$8OjtOm!-FEJUqaKGE2HZr}qaGf}@obn<{J|_SG?NN$usdM1rpzZ4i -6_M}R1Ft+69myl@u=RKi075@H^f>|iOFWoO%mB#GXyN=eJ~#B?m`dU0h+z+SFPdff-6rm3DKQ3#_65T -)SE`XnJqak@WEn%UlVDz;MKH^*uxwd6tW3AqLH%XzvWmCQKWyoVbL%^p~eS2p;@Qn__vwJ9~y+*U`aq -ZezPR7Svq_lgGsbE%u!Vb95n`^luR=c&$*wGKI2F9kgg2OKLZG_T=fegC5=|m@Scde}4b()BD!qpS7;Cy15 -+d-{rNv?%gNp9?$OF!`!_|^Ub}hTQF1*%baR49!2q4ufaHJ8%~nVMXGyf(!-;qwb(yRT8w+k?;49tTW -|c7nOTxS3G>FbQgQ+Mk4p{-kKO*+D4Nck!C@Fc_<~a4yzRSE5aZuF)n+) -H7fFjbRhi>{`jB2k1dEq(=UpijVe2M~z?@385UmW#w;4ZA>?A!Ity1J4r$4$%s$Ceb9IZmQrCE2t!tiYHZ -jKcjH}BtZ1>oxb=r?B<^YL2$!q?jS753~w1#jz8?z@V+=l0DPulr3;-{Y$Y(XCg=cw9yR0t@>p5X= -dxP+ei+tFhCh*HPu{5ckCw5mQRm4;#nifQ_vOwB?eo;hmwO!5aG2Q8zr{r>|{O9KQH000080Q-4XQ~Z -za0;dN60AUvZ03HAU0B~t=FJE?LZe(wAFLiQkY-wUMFLGsZb!BsOE^v9pSW9o?I1;}5R}{1d+Zna`ur -~w2BIwR;0!%u=vuQoTf(e#WoLq^a6r$CR>R -gSDE|Y_{2mG=&_O^dV -IkvlI-IvzA%p2|$-(3Vyic&)Y&L>DsOQs!8PR95?gV<$>lxxm_fA$RFQ$7ekRPsnW?<%G|N?B+i;}rn?AI8R;0+2V;We?LpFyK -YULMHYc0%KiBJ!ZmzFBhV=3J`|I`fGWx~acRD`(vFk6G#Rc1ipBmd#B{OQ_OW=My9^WyF-?gBujoix4 -B0SuGh5`>i^1rL~hnqLVZ$nZI;z0H+XB2_<2L(pB|lr-ha>)}@DAVusJ$$xI@1!k@CeOkp5vRjx1@;B}Xsmf -D{DAl;sG(6ZZ^r#S$Ei?I2q-w8hdWzb}bw7HT+ce5t(zl?7N)S>2{O}heh`;Y2*0=YcJ_oNe9}fvqnu -3Cx&7zcP5;js?euBl8oj$x7H4E+r~z3nc=? -sS#M!i>3ITs$4!!*WIR54zfw)`-1r_l-~h%qMMco3tn6Yh$$O)N)Sv;4`Fru|Cd**%p%2`QPWxV0aup -SBCU4r)`@}n{*m=w9C%A9yi`mP2y!E`jD;_9hwd5dhVO2W6z;Y_UF$Xz>m~EjP-G%ckp%bGZk(+X}N8 -;wKULXCrTt1T_=|9wiX%dh{)JS7k}0VL_hunvU#r-Icgk57_(@~z}U$1+jF8?gm3J*T;Aapu_j@h+64N@o=UNhjy)Ak`q*3ZvO!o?6w)k~vwkpolza&qL9iBl# -Tk5egeqy6!K5xPOr)XJ0PIGhY4XJ91{lUwC+ra0^;d^(|2+iwfYY*rL|V$N}zYukpL=H9rc5v0t?*^;p*y|Y -`;E(28E_xOm0K7KDPzKP_cb63WE1>VXMto6|_Kx?V)fB{mPcJSSG2Vv55QM*@jP?QZrx(%@%Ksu1vTc -4l496X)M`6qHznr(X+eu?Uh6S7>u(%BQ?oNnl@;Q>sNgU0zWREw>IlsY -GEl$+Qslq!_ix>~3y&x98@#1RbR-gYRG7O?2|};oY6N?%ENct^KagY09iCgFN}VCLHAz>5!XnW -#=UvBubr&U3%)EEfVQREpG5pQtEEls*KCM))C#H*gZ?*27pYQXa_b=jk7yO>M_TiZQ-r8zQ3yMp(p-l -)ID?uJQDiyF48{a+6JE+izj({|18pqTLc5%C0%{`*s|hXR?t@H@FKdR_Mz@&7W|xpTSyFqknV2puAMo -TRFlD0W{8HCgjqE5Mf7j3{V#%B$<=B$$Vz^gL*6?pS@T5e`HJQ{Vdmk)F2a&U1j&L)`FCVr>^!8ocfO -mb{nmUr!2_O1TOPQl$HV2lj$ny0E8@j+iVG_o_t^aG*z8o^vhiyzmvbf~mMRmE^8HVRe4La!*X$U7UCi0Ac;0beLH79Z$Q8TVH;~feE8dAv>4gREEy8e -H{WuJEM;!QhX2Um|$exJ)@v)x4s4d<2+!N6=wh$yR1g@548o}t>KBeFBfju?#46B&j`Y!Q5KYV+zTJF -v4o2NLEq-bCuj`m45D6V)YJ|iLM^}g6-+NTLd$~RPBa;Q^ag?xz8o{{g5NQ3`u%QRM&-2q(sEwY`Z52 -P_9ow6Uw^gbz;eL02skfx#ddWD=z3;l?@dgqtx`>WeAJd)UaQ}^?2vJh4&427m#pV}DT0sfsdF}?n|4 -n88YK$Om@j8zsmJB;Tw=ZLdrrM%0x-;z!%)b!tNe{$~L30eD3=YzqUvQ3L{)(1{!rr=i#Zg75ZWY -z{pUusD3T*g1%plN{i)xoYG-@sm9wbyf`_D(mAvR_!9(f(p5{kvM>{{v7<0|XQR000O8`*~JVDK8iZV -;ukhD{cS)9{>OVaA|NaUv_0~WN&gWb#iQMX<{=ka%FRHZ*FsCE^vA6J!^9t$#LKLD<)962%MljlDgbE -v}s$SC?9P}ydrs#Q?d$J00VH%#V))LLhy(D_Vi=kI}1{=9y#hLg}7V?0HN+$C%&* -i+y%A(eAe!RW?=|`C^Wc4PSH>x2JRB$`p>~&hpo11qu!k9W;(8$n4^^y9!0X-y4UYCpAhqC#xX*R&J^ -K!jOn{1ZLF}-~w=jB2&Sb9L9?bpj<4n1<&$ZJ4SKYK{?Y;k_q7WeODv1;zT=S&Sh`U%)j0@V&ggrm6X -J*_u@x&%xKu2f?gnh!>b&P_VMhiNuc*4Y>Jrfsr3sT3gnI;$tk&19aJHGsr#y1}F=CwG9$!2w7bOq7z -hWF?z-@TaVXlSz@TL1sq>2M6;!t!v=cGEW=HOsj_F>@P4&BcAbvT!NfuMb=Cv!&>G`^&CI=HJL24s%| -DANHE%M)ym-YB5hK5GaC$o7cdMw#|4lqBu)pAP{YF^AZAAfywj$s)Zk-*xH~9R5*A<~RI8zZtQuNKb_ -fLs@p1rVOHq^!J+%*=5%9%9*g9Z9w4*`Ep>jxrAH+EI{EUavuR35Y6uAKY?E&xh%9dV7A7p$k-==vjd -)lc+^>PM0fsZT

ZFh-d%m1ym@u|3Lc)shWOgG+sn% -1@5OV`$4>5BBRSAJq%d)5`rlZ}000Ff1<1Y#<&f&s)l&t%cmQE2^hXbn#qGBp+=uKh&Zf><`?1~5#E`JLAnn=fIZ2Ezdg4OmaQ12<79nBMyoWXMSdpS-cWtcIO -E_AaeMNB8vrY>Qt9Xj7GQgyIpt%-i}dMs*Jz)ILT+$IxNOqo~FNGf?ka@7uhIi_< -_Ei*L~YzJ7cD!^z8IJMQIDfM}v}UZeJsV9PH+3)F!1gA_9<3z2TXD**ebDMboHBS&EUr|3tFQO=yLtX -PoROB>ti&IS7Z=bKyDlnPnlLQKUudSylP;zG4Bpzg>)0PQLB1p~~p8WsbBn{y|jfr56z$7O{*;0x>;^ -E+$v;X>F-TnCI&Vz?6D*T=w#g{(W<`+}K^!te>VU;SXXPU@!0Hp9_~7RDNJAP`9n12)6Kcrf~G2aAe| -H|=_)Pe-54^gnsSMidr^0_+X|LoS9s&_L#t@?G@rmY&UiKkNg+QIU|M#e=Y8BH^g^Mr?Z4SIT;R*U -+@+<4bwn}KFOr7=#Yci?;{ytO9ofKxGR8xnBiwoa!raM-6)Af15cB`lT&5zn)-fHnPMA=mh1dnb8)Os -6=7&1_l74U~R(IQ0)$DaSGf&oo3~fAiRz$+;tQB9unFSh2G=G^|4f4J -0>g;yO{K07>Z7byJM<|5#Gc66iPATo&P1DZ?9Iv{?$irVHCdh`iK;gcxo9i2s{jn>iIze(i8l~W+MaO -;ChQ?{4JIq7VeUY~)0nfgraAYt@u7nELaX0HrB(gG%PL!;CuV{kcKnYlI+?!$#I^!WimJ9Ys(c|Rypl -eF#|Apj_E=;Munus2kOj6z`oMz8TScJ8j}@DowJWh{t4&#heifI{*8)Tnbqm&|U<1;p6Bc=u$=XtPOO -rXF?1qf&i9sk)@If(a59XSHmBe}+(Q!ADD*@7tJpnA4hH2&Jky7BRl1rRbfvpvZvI%d(j;P+;i< -M#sWuK7L2o+wzSR2B?311nEfpy|cB1+>>`x=W-$<6Jn+n;YPXrz}%$7AvG=mdY -f#2-K4k6(U{Y~ew)+-89mIinTr*pB -Yf4_MEc4)zjS2@Z*7YiQqGUgK8W+zq3Lm+BNwO!Pb??eW2Qa^cl@q*4+ot3=t3#=d-YB8Tz< -hXbJmy!ZrXVHiFH2Czzz;h3!1bU(fQBUk{Q&yIf2o86-m1U+*DNUTmj4I=qrebg{KJ3gO -9z^1-h*%$)@5gUX8qUkiG|kj7k+0uanQkj=>%QbbP9u+{E~L6=$qvx{=6gq(5&;U18YJdfC*X^CJ1QW -!!m$}@H6P<$Sg6?eDh?JNI+x85mJ0M7Q;IGLUUQHCe3amL1+n$QPc@mtiWF(3vo@uEBqC_aS}df&N}x -H)UGBYH9#JOPNu_hfBa|S6*x&-*gb2#fz3J1@}h2bIqf50Z58zrW?0)4zx9z+9Xj!5`x3-!*@V;GR1* -NGy==7tG0QR>=n;(-gFDS&l;q`Bx-o-i;!Rl`qM0{uIr6rVASPht0@`ylH<756+(??j2929VR?oqJ#a -EeKPS#&N6GQl-{W~&25K**H31!E_^8%v-#RzhS47kWdScXxy$0Y|1+BLWk+5!MN;xf=?XS8Vx-O8VDmlVu9e|p80>ofSI5^yKeLjHtO8M=6Lmp?shUV_v_tWS_DHRi7f`o4cLupvMusoRMsRw*o3t+bGxu@Ixj!jz$?y1115-3xyr -swPgrEG0=nKcBECrJ&PCuG-wiKg$~cFhM5X3bPTKhJe#);HC6*hbW~$r>SoeFL|~tz>Fmsvm+B)`r?L -W}ii=?tQ21k+E+^_oee5o)jrU0V+p#N19F-^ZbaX2ih8l54kv1~V_lfq^4BcOnmZDwHfUmIIkO2eEcAjH!;Xx&|wxEARU5aH -|CE|U$gVnG^ivXq23Kld>*JWE!_(ty*uG4yo8`0@>s@-e7M!aAFadu38;-_<7aydk)Ka7mX_jOBD~_wN)4MuZ2Y9%NtC<0;>K -ApsT?tnfNH-Jlj3R;-WeP$q0asptX%Q6ukn`4HoDa;fSu8^e8<)%;IP9l}O?VLIC_vBi+UPDF@WOdqFbJA*isTW!AT3gS=1Xj&z9)vt}&qi@)yL)YLvkUyqOd_5}aa|bOk-5fw4$mR4hr*`c!;(GKT -+0YCdx`)@4z*z)BKLTKC;NIj31c)Rm=}6-}*Nl9b8{^AhEkVN5|@rp{tM9>?G@tX648>{nB&*9q$3Lz -PuS+2cCA$ih%8M19i?KLKt-XvwY*{DI8lCT?=8JJ+m$h6>t9{6-vir+F8Wqbw-us1ZLWKmU_8)f&=7QIt)>yh -6>0ll>wCE*3S|E)SqEFvX))umkVLDB~0JREG`?^nKRKBY_-ad24b(TScvx&Y;@G4O0uf6^15S=_Ry7x -WUrSZI(N1*&WD@Bz=l5TB(Ce(YF+EiVfvx-)Kib(m?KIOsD7PK0R*eh*YfnHyBDV6c&?<>+7rQ$@#li -H=O1fUS26*(iZ12k&mR5;sBomL{)A?pvG=RG^6MgQjCKT`! -S0z({@DMC^tE`Z`?pr1I@qlc2Q_($lrbr2oiMZk{|vYH>^ntJiSY4ZGQyCsFpB@7Yq`Vnb!2>iO1z)3 -N{=rQLv>{%q;C;(C#$ML{;uIPS#h_Sk6q1#s3b_sQODFo+kvLAU2YH?L62ZL4$FzYC548P1Vfn9R{O+ -|RjlYy#H#7Ci0;nTrjV!PrwZV}WTQ^o1+=LOY@=!eO91fWei5$v?&@3YWGSh|U2WG~`hS<>DMt)_icxjd%l~ebcc6nD(3gSz5LVri&JsAkfbu -V@(?SG?qygRg1I^sv{{7ip}C@fBXP9XG#C$QwphNY!PdX+fJ9S#bJr8gJ@Wv$atsShUn0mm06YdvVa|_!c7KH59y{%9Z0QF5-7>dT~%%Y;jJ;|(}yx!uxOL|*#tn^ -Li-F^zVp%amc4iUEP7an5Q2dh8M#}WuSC%!IQfpSDkO^pAu3W7)v~PuIBsIZeWUCki&uOaoG9QL;A0o -u#3&q2yF2I*rA}9+ihH$q{x7B^(qc!Y&{+FO6%QHeiY@lx;0`P8K(A)8IL{N(F%G3YMGyNd?oQ)`P## -8B40z1&%9f@D{L&jcMakObGMnpSJg)TUg2VD+*Z|Q0LSAv2l&Z#4aaW=-u?(5S{PgPPEWD;H_pqfeHR -$uhk)tRhw8RpAUUi`jBe#tzNo4VXsdrXa08QB#Pu^qq&orsWzKJy%puTetUfy-&=3~=^@^I31!xdR!- -5EdhQ(3g__k|_0b(W_U5~&h7ef6H4!eJYm>xZ|cNEeF?IhEQP7hA)7ij~S9debR!W;*o{oK79HM-^8B -fj)PBgoV3q=XZ`UHcGA$u>|Eo2b34g0SYD5qQ1{?%ks@tIZs({h(#%DRC;T$&M>T8lX*f2A}y5uF;x< -5Y%kk9_rRIC?a;B$@%#I#g7NcMWw3*Y$MJo5)VT0jv#2B=c{4Tg-snpVN+Z>6-h>L%b_1qljnosyN4C -_L7SEUDJDj0qW(vj=Te_}485QFK!*R9k$`&l7qES`wNmvGt^9BTFMGIbHS>1=ZCO*uEHRUia^ymEebg -ItxId}}OKqpq5#({ySQ;-s5yfQ14TnDZB_J==~8#TGw@UZk?RNgyx-^UQo -Y{E}Gp6@NJ-UH%M?0}Z9K@eKTGb+f*e>oFJri!a`;1WNhQ)KH;VA?OsYW+ef%lWBz-L6(UUGYlU06rG -d-Nf<3=10dJrPC>3({lAoZPY*D*Uth8Z)!D7Xk6W-!ccQOaBvOH^ciTH>v%pNdcQ(?@E -OId{~cvR*IIxS(bMqDHqmU7V5QI;A{Ypu1N&%%Xu8l<$r{Vgl3NZbQ1EKR78Y3=GR5?9e5ofdX+Ham$ -^uQQIzg~uOSm^L68Hs}s*F7eiC0&)|`q_hMV3}v%aUf -2_H{Xck@1s6MkGt?n9~3Jqb9xL@TWl}VhRdnd1e&67sZ(@V?g&O-lUci2+f-Z!W)CUT6so|Trw?Hmg5 -X)fp5v}Oy?=x^fuRV8R3t(%6_dEWQYS#1F2dM)qatwZDxq6$a>sRTD~8Con7813IYD^*pqYcwQru9vg -5SB?28T)PZbpZ)!E4y*8fV1RgH~Wej0NZo`TvlQ5|gl?n$7MQwYo-OqTbM2uS2^Mg^|j4GN-gtcR_=z -Bke)WdQSkHfSOlbOOP)(p-n7W!MQ(Or>()WCm`4j2t^g6kG<6dzoX-jyhx2|z~weko8C}6{O5T+79eB -ad}D6Wy5IKbh2Linc)!3CPK^qE-UbvvHJy(2;z*q(UGadFV$=ZlmX}Vo<7Sz2Z8WuWs2DdamC{vFov= -Wg5B6ftI>q8G->;cYy9fCyj9$c8V$Y>jcvz`(OV(E(kHeLbeMx@$kCT|Yq)u|5BtLXs_MMPRylL+om% -~ro<$3JLT>?6A4NKEIoZ@ksr;F-#6yh(zR7NLH#U1r=I#VKJU2A>LPP?TrR|9?s{^*f=-9e3Kqa1Pej -Y~R0TKRUPK1Hf$#J4s0FlnXAp8S{kTxq2b5&Eyy8PiIgAq)Z2SyTSyzlsQ)-@sd)=1u3)2W-T%@hozM -0a>PM=jNmLee2h#4B)_Y81D#P1C+=T50x*v_1KOQT02e-#P1I420NF4lok3K|CFFa)!eZs*XZIRFwX!@ -ohXN!GeM9cR@shfXK4+X4r&^TM*wa0RBsjwR|SQAA@G#}>(J6l&OC+VqNpa7!;6z -0@Pay1T#^Wbrcx%FcdE8Iu>X!h{z>@R~`6_0&EAwPASNca8<_^$KOk*c4xDqAofUAkfE&Z}$#`@7%XN -wP#Q_31?3+-cCBqt_tikvkmZ+oC#6DWUmznAPC>$P9#64e5(ny63XoLmL!$`)iw3cnb(9O}za@-TV^7 -FhXVN3ojq@XiBBxXaC3n(f{zQW^}@5{+V5TgzZLH-+L5g)zKc|oP9dAAU?xj_(u$gY2^6WaVKS8DH2v -c*SbI}$cuRLpTqW5t01^_;U2ZObxPwSh=0k8Xto2IYC1QvbZ#@GEXc;1T$EGGC`hmG-r3@*lDb_}=1k -X-SLv2d0fB*SgHh@@3Lg^lys-UexniqkGjMA?+YPuga-vV6n{!?MfkV8m5%ax=c-SdhVdM-id$wn=Q% -2gHA$K}+FT8J61q4hn5#J!)J(TDiiSG?2sSJ$%N^Yu?5>QzIeC#Ms)e3*9QsLR)SD+D&N1u*tm2dp~r -*}>ti}n!;bp8Ix75mThfewAl3#XOJUQEstJf#_kd=eQx5>WO|i8z-^D_wyIx>J1zlas5dwvnrjwI-Et -g{Ls~Wtnsb!~aR0F6G1|*&Xi1N*A3W>37_ub6}o~Nd1*H=aA2@-@O$on<@)iV}Y<@yzISlfi`QT^4DQ -YpI$yE5T_S;`pB5!lNUJVe!kF%T|Cb^!2VuD|4E#F5|OZ*P!Jyo{Y?mPKcue~#D6C3#*K85_P2ba-=9 -#T^5kliy%i{Fv9|yvR)6yUl_vj`DDK|+2UGugEw>d`h%}B(NEbgMgEI6__y$9sY+WPEA-Wcg#=Kl*b8 -j=Yo_6dsv@SLc|M`LFxdZEDQ01aN<{a@fI07aPJ6GJC>TZV!W}Q)1FA05*82V3(b~8PF|L8-6D!Ovco -)zn}8p+Cxl6t9+<#C{nqjiO6HuANt5$mu+xcU0jsu6uR(2Os!R_DpB_>xtJ2m0)deQx(h3icxZUc|7c -eP8L`o;uF3eOQmf^j$9LZWt?jx{9IFca{VJ1@1<7An5?hdMy`NI35-SJK=+*FM`mAS>FZjmhzZY9z9O -n*=agXn*Pjy==;taf4}rB#EyA@AAOT*kdlp#NMIhxzyx{Qvv|Qzlyrs4-b@WQ=_G^qsdxN5!viDdDwv -LP^3(38om)-Li9lV%ZH})+0GYEyCd~*S|G^GAIjl%f1EjG*8bVjgAXtv9-x4-r%Y3I ->W@dF8fC7j?g?rJP+e^#}^caObA2#_h_*9VBc~Kxb=|}|4tG4ubgrw3K(DW6oZZe=6L94z*R2QFmhI1v>q{c -TOS3U3dqRdm%TF(kKPR2b+V%v@yzpb^#zl3TD_Cybe@$T4(v3rB;*$+!&}{~^1a%5u6|O -{XSnXy=L4XTB(v2V@)tau)ZUQQQE+VI|SxL+WgXAw|Rty=}k$BF1KOKFBKdEb{Cz9~RAo1z=6gaw1=Y -Q#igjGNUqZfi@p=sDX{2fo@`rZ9r@gCed!H|2g7}9{A`O5G8esMM){1;G50|XQR000O8`*~JV000000 -ssI200000CjbBdaA|NaUv_0~WN&gWb#iQMX<{=kV{dMBa%o~OUtei%X>?y-E^v7R08mQ<1QY-O00;p4 -c~(;@gXtBu0RRBK0{{Rq0001RX>c!Jc4cm4Z*nhna%^mAVlyveZ*FvQX<{#5VQ_F|Zf9w3WnX1(c4=~ -NZZ2?n#gjp6+%OP@@BS5oF5SRlLoR_p4hwNY4&4N2+a5|WiZpAB${Hb!o$kM{WOf^88+xs89*@3xdT% -U*D0aPxFpwTCf)6wqjp-ewi@*dL85INf2pjLAcAaqu=q3}$4d}QmM1mA%@Dvy*7Db_P4<@$Kdz{->7u -N-(Cm@f(A;*|FEsw=F4{X@VOR0;N}K|KX6a(@=Cnr@>i1YqW*xCO?#VlHoEMPS2JK1{aiO+>!y8vyxV -=-G__d6@fsIpx@L;})o{NOw>Y6CpSQ6Ri=8>&rvD)Ae$Hv}>-Z=1QY-O00;p4c~(<6WeLe=3;+NcD*yl}0001RX>c!Jc4cm4Z*nhna%^mAVlyveZ*FvQX<{#7aByXAX -K8L_E^v9ZTI+M$xDo%(zXDYzW9dew^O)%ePjww%;^aJ+T-;chn@)Q}LnI{OOcFeTl&yB!zrDM75g;kW -Zd%PGrohEwAHRJ7&}1@+mRl}KA+k1Sq^iY^XG{`GDj{-G(2{OgO`0ujNGsvRdm%PJcu`g4vfS{Joyh3 -+%jXa8Sd(ta4XbxNW#muWrm5ul*;$&4Hj6iEQk;t7j8+v>^UgAn%Clsl0~;#HjNSec1BhB-N3y(YYk~nb(~41!6Hx*-T;*Mq!bNk9wFHZ6d>tVAjMkLWG~v9fB-p9m7Fo@#Ba7 -WV?@1G#m -O>f9EHMXs5ZR*d1(6j?`FgL=wH7%rL*2os%F?&DAZ)y+MSgp0bCrt9a+mCA&B0VmcHxPi_ZOGRmz&Gv -=F^*>E|)jSkIUuthpQjRIXP+SmYqb=>zG`vk-x&9LL0g5z(wBW?12O0=+P!zn;B{2Yf%=gY>3l4jwVoHB9)7iWZgDk(j>77m#(D9+lD3jdlZ ->*F6>h-HXDpI^YxzAC47~JV;r-Gl$o$JBvWN+wCH7Bi+b+9{-WpVfsgdzGi&O-M{`|^Mx-M5hHh>?c; -A-1U|d`9FTY-oma&Z2uu=|gE@i`LM#S1A29w(fD_YQ#WvWO60Oz{&Nl0X?-(W80GRH -Q^&5}URmqJqnlL4d13}x`-haA#kXzQ@Q#nPzAzOB(i4nIRaUZGkL-aOAqO*s~$a#$||FWy26)@9Z2{G -zGKJl#>AV-AWvBAZj2h82~vhfaVn2ecZ=uXD7`x&^UxVerzpY%86t1yO%FW! -8UZYZV~f|aM)rAC9)&2fJ!W -f?zPv8L%WWku*6|G*7_Yt76GMF_8`)T>a$?fD~0hSU@+_Y16#{td}CAwkMYA<&DM}2pfx?~Z@aclk;jvH!1qvs@2EV(g$ -X%!cvCR5@R|kai^qTj#b<0doY;pKOzICs;f#*ZK0!fHH93BJ`R?M&hov7Y01;Lj;XS;<)V3}a!+`q1( --bOg((EgypFkBevW*V-&3}CvOg=uT7m6PSw$IDM&mL1`D-`TvH+}+y@(O(n%rrel7Lt;HVHGk($1P_j`N3tKiMMx~ -EMuL7eyCB6Zc7uOdkQc$521;TcrYKtwI{3xv|6zU#EN(v)-RFX43-rM7t#;a?@4LQ_%wNRwKmRo(qe^ -Yqz?2w9*+KzlNY*#cVm!{!9(~dwvkXLnohVy$K69H2gNv#8I~&d(EiVW2ML54pFs9>metBzTx`c6-u} -swwthj8f?D)G18`vJY`pmCNB^u=DLu{ecO&TFcPU{VuU`>K;@s!QZ2kJkq?%}7aeYUA^E6x2H1qEd?& -cL)K(lUSoS7BOu`Jz}US)t+SR|jW{;qYw4qQORPdd!$_#eIokamI21m@ror~ocopLkm{2Lo -1^YI+?H&+h`+&ATj#MMEyVLSe&3&-P1+pYUiJDOn9oy7mzh?!u -zKeQVKonFt$b3o8@TPO)Tq&>D;iI?|vhV$~IDP@gF0@6?>J34nY)zz|LIqXUBjQj{$7+OsW{XY*%agq -zYne7+3K%`jL(%zJ57X%P!gV&)Ko{=|Omfoq|wOh#Y?+5MSisEW7IyDiclj_Nlta5BGF@#Ya9?N^;g| -?Bfr*5H1SdiB*UJvQv2nmKJjn2t6kWUJt2SIm%}?4 -09cOfj!5CR$zFvo1CiO>MqY_*!Bkc4oz+)lsRKn0;p6l+fRdO!v%7&i8HopH`B>yBL{Bpo=bB2JthWc -^AkT7c<@Q%$7=xkA$vazX$$WLHJAuNm3gHu>rKF7f3sq_0pt?W5aN2qjfB)%=- -)X8kS?};fYxwL|6cG%uWv5voQg56djA5N5ScBcSP+ts&ccnd*Xl^519bHi6dHss>Q~q7;Mvtp+2?LlUbPPLE9lisKQ2($*>w_EFWL+Z^OlY -_0JG!Dc7uiq}-O=7re9AFU($hs)K9m$rEXnnR~<%8hqBA;045;x$(Tm)Bf@VjjsAu(bfQxoxWsFw{3a -HG6I`p8vC#qQ2{Jv$a|KuB$#;y^I}=$9F&~*+bKIK)?k=A<{{ky^ABzID>hW~Nv!IoaS{K3_W -)O)rlHn`ZSTmVkekJrbrN_HQrZkSYRiyRSDGHB9!$Ujz1`hc_Z%jn9X9iSqxk<#odH4?gOMlbuTu8t5 -tdHU_8=>`)^K;bk{mby9n^d@FrKU;&felVy?-%yqVhCt^Ck|nKHd=EZ%yoZ+T5-;8yuP#ia5dR0eelN -R)oNCFH?PY>c1Z*)FZbf_H$&*y@8-_bLtj_2BN`+8#MVTTt*1{3BJn%6Y1O@5k|;X-!kultlgy7s0PXCb3Tujs4E?>(kuNC1DV^q4^`$? -9oMoR`cM@kZd?HDRzZ?0|XQR000O8`*~JVxW_~OR0aS5x)A^XCjbBdaA|NaUv_0~WN&gW -b#iQMX<{=kV{dMBa%o~OZggyIaBpvHE^v9xS6y%0HWYpLueehYRPI#8NmpPnU<^U(CBf3PXk7Fm2?Sc -AEjE&QeY3m@)C-?ygWSTe$bxhO|Dj#?}1b*rG;WKQLNZP$$1t)%nK#hx>7WO^&? -Qh?qixzA(bG-WO5;8Md8KKEC5OA98#sq`Qc&A6B=$g-)1z7d458b!eJk_!c7EqR8ANA`O%Y36Gp3Ir+bzKn0T8gjF^rRoxAsF>L0LS=corpL{67`Bt=t -JTNrQ(hD_R*V-P@#d{bN#c95r81Q4TbT0)9O*koamJ9VCmc*BExu3>{0ZQnw@N9zYUnHCLy7vpr`^;PKpF-mSwP}AgDe+0nfT)1vJj!;W(V(az=@yK{tz@3a%+cuZn}LN`sXtCq6Fljk(sI0A^@{LHzmCC2&hEx<%MHBublL94G{9Q(NCL>>L@n&_wV0!q87GTU3ItF{U0{ -x!!$ve{ykkd#hJ9^|0Y0C#rW<)~%xw<*2zxw27I}d&0dJ!DvM>$I|IKlWnG9N7ybey$7@lTRs*oGs9& -;YID(rK{B{}R1$&W+m%q38B@M@d+P;ov~vb>OpuF(v+ZVf7h<~M4R&mu(;Ca8yP*ZLwLr?8&?smf>98 -`f;pqMXK2o8{w&a=^O;K{0l6z`3mNdq4YS;V73_+J7$>V#f9B+9gm98`Qv{ZlvqJTPOnXWs3VruI|z( -g+Xku(X1u!|~>`V%Dlynv(50&_G>=(^ha-IUS$S|J6xa2xC=vEgC+L_9_Lr#?o?|3S$EvRD*G^HPUVpm2 -4w~%LDPP}X(ccJkLAT#};ou7U#GZ{h<`;H5b_Pv5#s& -2U25F*2NLZ|V{L*%f8-Mx)X6pr{BC0Z&Eo{Bp&e^eTqtE*oUfiO2en&zSF=yU{VR+|tM$;{>ybv@bO8l1Da=CwQ$NnQG~K}QX5BO>`1&h`BKS4MCm6PD^m>o!;J7xGV?cDcD -UoQc)G$6xE0QU%vZOgczjaiKohwdBmeFEMM^USlZ^AWtAX#OEw*1{V>2=pu>;Sy!)0NR7sVS%DCzqGYtFtF+%7~slg~iz9a@%@$OV- -Kf)%o&jb#eM+)fz$W8g=0I_n>XhyLZ|H=*OkCh5WxPj{i;3+h2MAZ4sQ_%&`tS;ftx&V}zd;a6Frha@ -euJME+%VG*05(#PgcMCFob1Y>fnSs0GKTO!o}NQqsP6z3H>L?0qpiW$V~pw&l9X+I?(0={c;kdpPW05 -rAm%0ncPgU1WmdPw&FSMgRE3IMVgJ&lvOjr2EQII-d03gU8gGCyxsggS&~bGm9q+M&;s-JC!i}wWk&< -Q8|{#ClxA^`WK#g3-!y+!x)8aUvGPBsq?h^KFV45v1hW@dG76U^`jptBlaB53|EGuE=7ZuiJ-fGFuJl -+ZvFvKO9KQH000080Q-4XQ((E8UV#(<00cq+04M+e0B~t=FJE?LZe(wAFLiQkY-wUMFJo_RbaH88FK~ -HpaAj_Db8IefdF4C(bKAJFzw58SDDNgzPG;=ndUH>$I`uhnqiJIMS$HFS+g -$)8NKvwL@BQ#T+}?yF7K{DH?gHxf`@Oq&Gq#P3HJe>cgOj|>GN}r-l#1Q#inUDHE1zA;B#~QItVJ$Zo -aTip!y=Yx&f+E8$&zhlnMA!Lz89>J>_Mn_xnqbq6sm}qamWh+z^Q}tN)}>>C|M+yyi5v~#0$k$41kgB -y+~#80v=opEL@B5K2BG^r$tjCB3urDcofA*5Kn*mBBs;9N*|D`yILu%oD>Js{4=55P5?;g)0sxj`TT4iZzn3sG0&)Hf8VPx7g@!WSXz?_q8_Lawr -zle2hnGm4ED{;sk49|4^L4Ms;P2mnN3xR>kXD>kIBU*bc8NLACzt_P$3mSq}_=Km^H -0tWZm4`o`Yc;Q#L{W$#vWD&$^Ap-+7*BU|Vu_lb9_Z9*iMUXWv#0ul!KSp>L@B9i3NoJOz1|fBoS -m^LwAB#^XH;>AD^m=RY6Du`=?F9gnMEuv!0y0WY%GJo*4wT{N``1HmT6h%j4M$)I@s~KwjmTIiq61+t -AO*YxsDZ?1kHgvusn{yRhCQOlF21_QO*|$PK7A(65?MEtUXgA&rwlM_+wrUPB*`}oxY!dstfR^c`)!v -O+lW1k*G!ra-epg~p(p9061^lZ?!%5mt`-E#vM7}Z0&9_ck;| -kkM4;b$J@efJWgq_Q?Zr(y`OEK4|9yYdU-Mr#yzmzwD~y!BwQDoWd`Dw5BZ2w!bX-0zZ4m|+bztiCD! -v{&{Oqo;k`oX#7VEUtR2haFLnsQe6&$ -Jc5{1ucYSgFE`UfBT)dl3uI_rhAb>~*UNvT)`W^APKV*IJrBJYy28g%%{<+tChM^Q9;g;`m%CmC6_x5 -}i;MXy`Q>8$d_dL@wGBYt5yRoSgZvPnss|83Y;|k(eA=C-vYOquKy})`I1h<^>%Q5>037vRz`5C!>kN --WqdL6u<+`YYiGo%?E%s)B@-PN7;nJeUijt7aqi*Zl!)NEXCF1)v0BkoApQ? -FryhgCv^r8HCd2!{RwcktY7^C`|_D>!>o5+H^H>Jx~S_-S%Gz5JO5hE3S(o4e`t)%m+l_?yY(wgrDX`Hv6N+sT`Q@C5#C*XZT>^j$E$1S$w -HKzwMS6+gh5VY1>3(uTtG9)|3GK=t1JkUiiAaZnmQq^q0U8w}9RtwU3)B~whnAcqi84E=?YSBm02fCx -U`PVXkckMFK8{xX~XZ2~KwegFE0?|MC{;#Gj29n?{T9NoM+0D$KLvQLOi76!@k>(8J?2&yn>LaV#95v16tJ ->ngLXAos=8WeUecbsWAH$DR!y6x>B?^W0YCCy9U -}ow<0hgva6$>L0-96P~-ZlG~ikos4LXtsJOrc2p3hvZ$f$>aev7NRJ}ywt)RZq-eA|#^W>pZ2o(i%O7 -~0aNOhpN+uE1ur@*;NqBu>^UW%~sWQ-98wL;wj!pYRJp|hWdECYC$3V`Ty3gy@ECO|x6Rhx;O` -e)zKmdFu9<{R_}ASCMId?zC0}TLFryVPF*GYuR78Mvy5GO(auXHa1BE}X*u*^P*1dc_uy3c19(rikCwuRn6nRemnT0k4;j~D*Wohl2-3SKMDs+_Z7Y*M$T%{)5ZE8Y`HkPnlu{_&~ozwjZ9lzq -Sdxd~|E4$|g>BY{g|uIA~vgMC>1BlV5xpHGQMEifB>?gTd$p`nb(ygtvvIEI4dTjoeotGCxN`!JB{O% -OmRmGGx;uFhKxQ3nxqAxvO@BT=_GHmIgI58vaqjIBmW50ordy7P|sm|j}>Hc2UU1b);@7 -)k9Ju*E{-`(fRolsHTPG~eaE31X1(EhRRdyw^i^>L|%kjIVv!>zBpyQ>wkt@t6v;9-s~SgzwayjKC;xIil>y1||q&InCd>pF?S#_^_vhlMBrA3ML -9=49pAVy?7eE8mI<*h66-auN0&dC`JnRzdqMSleocOtYT!OnWf=zYyUX0xNaT_2EIL^yvvwH)N19*!V -_gYCKdjCjmLglz@zrLoQKuY|Jr5E$0>HgDBukGv4Cc;l%BZdaZ`*(BYHviYi}%-D-;zfasTy1d%Rg=U -1#s2Yq&)-#3FNz$TJThamKMaq&Di$d)`u{Xk?(0YYXdma1<**gq}N#RgGTC$Ij2Y?SgMzdKVH_|yb*4 -@U^P%5tw{aVbkYIIE?9CBaU?s;aey)=5wZVv`j+NGtNfZ#(Cw&+bBX-bG8kDfQu!sMa1<0A0=DVRO0z -@@J6>#S`T3ZkGuc4ejU@=k(VBW-SdnJ-1s6c@GsZOB6tTsQCAi(mp(Su?}dg2UVVz8{wc!C0T_|$iVz -f5?qTWkt=`8RSGI^C89MYm_T79ETNuM!Xd>ya5UQ@RAMO}5w~(u+f<;fajCO*ZL>!*NaqN#g^ugPCwF -~QPLkzgF`(P-NcuNW5H+SdDj9Ux$i9b}u`1KV(?O0q)|YszaSL8gwQ@_N#CS3N66am -I`tyMdyP$M54B#0NWY8e+C?Qj8p_&r2b2#Y)9%JdluaB@lKpL+Ps62i6Q>PNJd!5(=jF>{tmf) -3pU#4!#K$(~dGT;-!Zpsn7`7&bFp-=#sB^W{w!4B!^7C3wYkH8nD?E))Z+BKo@;A+8FT5h3C5?BC6AO -vQ+Y}ZkK1dwmVMS!Qg$#RJGm!WBBPRxdMP>$jW6kn}G6bQM@$Jpg_k`1~vQ%}ZCuXsO!d2fPR8tPACcy{V>S&~R**O)!UtmkFSuHPy}fvGzr`nqz`Z&Qb`g%+Y3kl -5WNkF^ry+2VD|RI-J9!=SN|O>9V7oE>> -sy+N8Q9@bf(_*mP72oXsQ6#bKGUv@Eu^z9NK$>E^Uy2Ekys0?&0tVFI6agi3!Id>|pc -z#Kqynct5c8U|o>!W-PuEltX89-sL&KwcdA+yz7;prVfX}jqYTS8wk_(t{FH_WZ|_n5Ki6!=-wSLs;f ->mAk=Ov_<4#{U&wd-{LPXP?Z_{~M1zV2%Hy+_rMayq1;rNb`0)pKxYbcqc2vwp{>aWSj(eBh(MC|Y~R!ddoXzGG)_zL82OKct9q%fpyB8ms<$ -NR)<+==o&&9L9UguK4 -X{DzrLTKZQ+rzdwWaR8-aP%C4`Syi0WcKZYA)pNme1Xra)(`Ps^D9z|NlF53RDGy4TP!h;E;;>9Y%ZD -8d|a#3Q@@!34jO2DlBjJF!(@EO8O1TZlTjQ70cayuHZzjR{>X1U{719hW!m!n5%WB+kH005DK;7UdcJQ@1OUbx_!n1$A^xu -u$9YoF%u&ZN0?|@W`)92g)8{@`PqtN{WFetaw|CU^3hrK@tNj+nE+hH2VI;a^@EcvGQ6~NtQ^osy&JE -c|@99=#TV%$tH&8=kCvBO>zl!n6*;7Q&lDb1i7t)~)F1n)ugcFxfxd2?<=N)%lTD`J8DyzMcv>cm~lJgEQfysfwq0Vt>tmow7oxB@cz|neW{`n9plEs -}%?zb#s#he-`E5=zYB9WF4TbS^FJNe)jHh+QH8)YY2-kt=pVds12Nh)--9hs=dAu{W_@bI5*lyf6|9m -#jfJapgHFKb}56xyiqnNR41Y#qpszi;ny_RM?(!kAW{qEX&i?L*B3Yg^os+;AG|60sa}sdtPZAuIU@L -z^Rdlod*wsiT7ERVckmMGCz}3w$Sfsjl<1z#ggr37Dzs3LwygK`)?B%BMhj~gXKOG~+f3DO&F~xN^mC -##6rLF$;w^n2jg*?wt(?#RVMkRzRhJCh!|#1^HFSXIdv+m**hKQkazaO)j=6;#!_yes@A!g@Rxg-y(o -2&mJ;iO@x0v82`00m(ED$PO@E9r@ADf4RsP>Y+T0Gs%Ytk&o&FqfoqITy&x}LIx`SFXVaUE|)d`*2?r -gWK-j;*0mE$|QuvRd~jsjF}$A`vJE*E~+G`r(6?#J$)#mVm$K*-90))n79WX^NZBX{@ySdKPIz)fVGn -p_&}j@fkZuJB>*>JKP5UD*WT>@u0ahIY9@aDIlMPm+;LZ;_O8*s_=Aq9kOrkNPy%t-$sOZr?3`5BV_O --ORFR>Z|#Y0?5Mus;apCajdJ`2oR)!?=@T#fHEa#~xgExvEY@r1UcF>ZA0`{-z`|=VT<5+x>b&V69^& -sFbs16*MKY+`Gy$Z!CJm;v%RzAHtZ8ozyC-Ywq*3?sOWKhKn6%rggrLv3nl+DOYps>`;`CFJ!~;}QJ= -XZLXPM2My*c}EbWF8o3y?j~i4$}|(Zb-Mw^NKlU}tR=f?w5ULTs+8A1{-SG%gMIhnkmYdZfXx?<4iW2 -Jm9Up*AJ4^2E;4<1M9Tv|e!h6_b%q$2kE0Er7%Oy=u34{0yS54A}fqpqcV$2z}dHC>5kq{~G}ZYkL}Q|${u5A30|XQR000O8`*~JV!0)=_!6g6yk%j;OE&u=kaA|NaUv_0~WN&gWb#iQMX<{= -kV{dMBa%o~Ob7f<7a%FUKVQzD9Z*p`laCzl@`*Y(q(%|pq}l>Er<)$N@tm9a^n(P%XKg>JCh?RKUzEHgRFf@G6sg~-bd{ku#vQD -$)*%}>Qc6o|0cZDj6sIbFqno0@|&Qlc0zmq?lbzMeqKlj|>XHQ_N);U?1o!Zgv1C5jR12AKb~#I9bTA*m0dc -M9Cc@0Ik!t1n7X;!-@CqIL(e%(cMaB0#JI8SszAO5U_<@24!4`$wnsA=~bs#1wf5ew8;ryg*re_(n5s -6hUN&EFH#&!!AIobX%g?mHrOEsnatDpK>|3Q6g;{^S}`~#3`{hL3z;c0LWi=5U}De~G23J*jJ4LN21T -c8!2ppS^)lv2GFYfDl3wt_37E1;Jm^>fS(kZ1v&!WEDx*v~ixCvB^N#Mhi8h)BK^&)Bx!_5<9fbs%L4 -Uo|lrjIR^?BV!MjMRc8aM2EPM&fcTj#5PNDLrb&@SbKs#x8B0&RFA;58hkS(_199wQ&qUHhI(jgP -+zQhp)NXWyc+QntGl`>jt9*nM7jpVpBJ6))zSp3v=eJLOc>Bf0rS -QTAZDAIs$&wJ6Jr=2P!Sn%u`#_Q-gXr@lT=oM+Cqi_9KGiJo9)cABdUy3S{r7)A<-|HapxDK#V^q9th -I~tz01HL0$yeBFce)um^KoY}iRiLTa>3OPYoFg6sM|O%|^~dgNiY+W@^`j4a4@o){!TCLn%*fnfK}iO -iZKSJ9dleI5t@panq;0PQGNyx@t;G~-R4$skXYzFz+Yt#lbLkVSz!d9;8v&^#Na5A@eO1(`74X%PooB -#uIvz~bNvzzdD<>l_xr^9bLzB`8heP7R7{D_SG*X6mroQ%S@773RgjP2kZf82gy!sfX+zfc@abjQY0W)A>hEYCyB5uwn4^I2=Y7)qX1x -nhzrX#Y_5Q|tAs<0xPC1+Ww{b6McFz=yOVj2g`8PbrFhATB_OU=E7sG?q%hrbsKmLy?Te-mcO$sV -jt3%?e1iEVBfLX7*U5KtdXs_MZ~p%M=AAP7|1E5#j7|#Q`YLf;n&$&5L;fLE>Zf+-k8()krM0YTSzq@_$Wjy;h`8*TQ -;&yO7`&mpb#o+p9@zePFqAy1O}OuL%kk_QVP8&e#X#H)ZfE1+=c~c3xcPj0GntM6jSB#MJ-)uYg^@;|M%O -bBMuul%^f&lJOg|2;u5dVlH~0)QxkX&XaB}nW?fApTnfN%lx){O3_ane?@cwGV!@&fHSA+4VzPK2C8h -jX0uL(fnd16z>RDAh3!pAu70R9`!#*=HD(r|J;yM;gdFyq@<)&0wOI_itT?RW|(^E7WKFbt9ydQAur^ -t~Q2BqYDRO3(!V{yZJoI9-eeR{(R00L%_vrwhj3G6O#L{bh-^l=heC|3HR@2z+`yQ*C`$xy>We=DF@VOfz|jx+!1*MmASe?CRf!u*Zsh;6MuaF<2 -?FhS#GN^Fh+etnic62YRL%v%Nnrqj=#LaqgGNeUfeyKMec;OshgQ2(eR;Wx>wJ0sRe_i0Tax5f(FCqB -KnIxRH7nr@_Jx;|Rn#LjXMyCF*4%5iwA%{=M9(7og3HYiP+N^m;{b}C3(t3(MY -KxTQVMl%q<)#p0dNabQJ_7ZWv8q9cb8(#}vL4I>mLuao_vY{m0?s{jPOy*0LBH|!i%!P}O2)wd=i--c -iwOe)uM2#i1@N3`tDpbc>AVut>oJI0u -JL0#&jsLu&DvtKGXTJAWq4d*Ys; -bl13E%88X9G35Q1U_l}dIK@FAF$uj3eiEm9D(;FkeE$I-on-)pl;JAnDRyt|8~i;9B)0s~t)EYOTqmI -DzWku>L9xwKIr#i+4k>mOG_i~-VJDRb~sU%{_mDkEK>-32x@8J2A8klaC!5`jhuZo!;K0c(4aCa(+B= -MfDGu#m|4pSG~cLAxp>HO+H&``C^|`jXLpmqJT0qgnrh%14yo=LE=BXpd-q{XfCjVNv9n7oNINnr$c(CR%W+- -_`OImu1dFU-cN%R{WsETK(LCX0)7*HMri(KXEpb6sLA{JF_2K<%Fj6z{Dx#46KOi`uzD)ocdC)RL2@@ -b?w6Tl!xYzN+ipzY^Gdb)+rEqbUVhyKS7PI{AhUbafJV -$4t%tv{K07Izx(cc|HJT;57r?}y#Ji8C+~jHT>;ABuD358^Y*198pwh*4gbA+>)%YSem*&U``z{&i9`+TRr0N{qQBLYIJ=zH>iHk#n$NhOfGur9QSp6Cht9Ue)~E;i(?)i>?JdM;k;fltEaR6)4BZx`| -1UIs+u6Q{NWjGpNae9WPia*J(nT&uPRyj?}J=f6IvlrpjDocCa%0lP^J&>PEiMgF`1ygCJQbQXo93m) -Tekfz#%n(XAh`JtK))8fCVmL1`ZDzjxxp|*B0aKD*hc5Iz;MKu+Wz9-eE{sAJ?GL)u$^bZ@nMB>G)tA -7ipNrKDagOC;{i^E2zl-O~fEYZaTteqGuE69vyrT=t1pN8m@F?|MKst!-abU$mhv;r@rvye;CdPu_X!!YdG`k&KPj4_NtHL%mi2d!DrfuONm -#Ci;@NL!xWl_Y>cj9bWCgIs{Ez9!TW3+6G937(M;r2D18X>E_6g|+gm1dU#=(ML0mm4y5ee|Y%?cw(f -pF3Lg#UdyIe_k$^cz#cRfQNcltb97vm6@{)t_x|!%W8)lpKwrZI@5>RGA~#M<0u4dj&UAT^-={q=wz7 -VXx6AI#O14as1douaB8)S8_nE^gm)(kn*c{uXN-;5{b3$BP7NP_7-2_X$Sebv7$=l>5LfNzK`v`#Z0M -c}*L7;ZL?URw3jjS@mgASVzt&xfXe}{5(0~ooRPTEA1SueH(vC+(-bl*?p}aLfn{*}Gt}qI0H2<-IvE -4z@RO6_KiHB+W(Vt<&W*^lLqg=$~+aLnTkY>cjJ%glu4d|#H2u&}D@`|O106jWq@kWCWgYh+}8-zhom -hMrf0Vb1cb;5hzT%Tiu8jYU3qfis6WSbv6yF>g@2K%4T$RbapGW_Y -fQ3OeFO%T|3Cf<{_BBc^6sQ*bn{xwZ+fq|tvTAW~Woxhp)^7h!8^&oa7au@r$YmL~LYTu4_cibHaY@h -Qgd+?nJ80@XB0WBivt9!3{9eVIiJ#FI;irE%7bb5tC)U25iM95wYc=M11s#tvZ%5P74EK`z@@9N9YA5 -NVwlI0CEzbsz+iN7k{4*=oC4E)9FWKY&w&2_Q^NCIA#-#vn67(Ou(=(em6GxLh`8ukt -7-#1`lsq|TD7NWZff|WgUi2IW78P_T9xhgR{DpjVro5jw4h~a_Mz6bc9W(E(?9AcXN9SJm<5XmE-|QW -N;cBRlzenEe!V7I$K4|92}pqOaT -f-$u>Z+yQ74^8hstZLI(I6q}U~a4+Qs86D3Xx0o@3u6PcRK>Eof#FP6tdXRMdOvl_H-o%ID>jjG@I@? -sKxH4&IxRCN7f&%ktit1=T5a{=FYO~4;1|XQPiO1z@xlS>w6%($-RwYt+o>#8mfAe7BiJxV#>QmR(Ic -HI%89aUu?>e@_tk++MQh+EJ7Py5rmw-agZhh2o^!=XhU$7DPx7%Z!DU1VlC28UFU^h70$ZnZ9e(RCq84S%V>=0Bx>?nZc2^!eV1mU`l`Y5zPVPU()Q{+{q%3NsmQ~qB0s~N%Ly; -Pw{vorpwhI?1#&veUma7^7EzAc)D9a3RTTq6DPBL0a1DG07CHHVTNh|}Ev#;?%G?XQYWeu5TIKL<-P(;Wvq4@rc&n_rz -t-8EnlaZw0vjT$Ii6IE4+bwAH3-pQc)g`*TkB*|p@8t@u)Ja}Ib99&s9`q~xl{qiA{FC5O(>97L2@fu -#CU0D3t{^@^=B0!6NrlaIg7u7<>Ex^7N(g>Hrs%tfE7}yP4ylMmW5;)u?)hZUIOCC02dKY12HWIla02 -4PVffa$PGBTSMQH87R+EWR5ds*y3CFEq2L|vwbFObHn;bm{JM#wFhb~@xD`@7-^_d4MUpfN?RVJ -9h7w-&cR87N+WYSk`117raNd*mc#vEqnP~IQY#DEtWWF#DHgT$WPUVFe;=O#g%Cw*IxRYW8z_iU4z4f -v|7A@gD5_?)|xA7cNh1nZ$BlcFk&Qr$p9@2cuDuH3;@(Mylwua+<5M^m?N2O!vz*+lweBgVyITKg)3N6^fgNTmSL-2^>1$ -Sc8?J_ayTf6H%WCEm@5OK`uhnyHMqLDt~+Q=rI2o?t#WUxPHpym^{8rv$>J0MY0Dw+)hO@x-S;g4PT| -HPK}~dPczbK^rLbhNvbt^kQr4P6&bwt=Ox+~dzNd+;j -1kgHI%XXz`I**K?NAGK~b%<9#M{N(LuD>A -qPFsXwXE&#rv9PZc+A85h|?Hi2Y|>jHd%$S|U^LeJbh|YHCyk3Mm)!zW6(iod>%!0G-C3%c@N5Rz9uu -U>SneM1;)~bNq4BAY?d0Y;TEOJ}0WWKrZ@E?*;4{O0&lSwr38hm|rO)43^+NqPIPZi`>wK1pwHhnrSm -=;A~!gL(}PZI -N^unrn|Cc*Nx71->Q#O?56%{E|2p{UwlDZ^4mCPb^AEL+r1V({sujD5N(P%M -U&xAqnUPA;gOHkVWZnV4dFm?Eh81y%qa(fBtx%@dm4*$aE82J!A1sC7{tyWb6j%f22-*+n}7xD9)U_` -ELo8hGK6iwaS^I)z}hAB70Zn!I#l7A7v<A4+IR!G^IK^+h+EcPZMqZ0LCYS!qzy@NM=bAVHmpLMZqh)@wA8|)(FW8=vthmIbDl`wV6!LA>GupeN#dOz7?ZGGGNhxR -TQ()WnZc5|&f(rJBPH1?nlu(%jrZJjZ?$Q)jnHI*z1A)>UE`F*5D|M_?SpX5#dpK-=Da;REUI+@2I%r -ta4&1qjnskh7v9R^ufPpWg0-w)C_~(qty+LrPcg?*se9vL@U{8Sh-fw*-pgIvGk*iiwpnFCZj5Mqb}H -|3Pl!yPT&r?4mgIP8PDPel7-c2Muo5ixKo_;mYm>tcinG+?;!AY!=T)Se&nPlSmf1G@rE)ctOM)?>i0 -%xSM@7zpurhzZ#sJ-Go};jIH^2p=5`P@o%{jscVpWOfT&WKOZY7(XXlH0=gldsXW2$6E+Xp~4Zse8IF -&Zx7G+MO2ofZIbjXS@^(BJ3Ijwg5&axj$8ekjv-(<}6gXNMP<;vdxwG1Zc!>rJUbnswDJ@PXxbV(l=8 -XzXZtgjdnfJoti{RJ?oh=JZ4yQ{C#aDuxJRoZPNiNs6pwz$2HBPTQgcm<6v9QCYAU)i2?%7?EE_R3AZ -?m*imY8k|5zTU*(sEw9v6&tV1}r~_N{-O0EA@$C<%-+%ibwi!s-1*!u~z|Hn(3c?C_PyP0dI5{wY3-A -~2^{es=JC8C~UHj~q6b$Hj3^HC;@rXbPSWyXBe5LBPd1GQcFikB3+Y!nQtwdKl)v4T(C|6Axqph^}6@ESASVJi%}w;-NWqkz#t_HNa>lDcTfmV2g1-=b^2=pKE-e+gqqOo{kCIa?Xk}@ -9Akr0NVgKK}P#e7b?L7bs4Qo4iAHtq~B2=DQ%g1k20zb=>3g*e2QF#H8k>bxb~^z1z+9qYGwcIU7`|S -V3+79nR(F{ojXQFEk*h44t*6fMBWT?U{D<6;es?MZ9mZLD*{AkLK+&MP0~q<0ZzMJIx=bae*s1F&#)K -C*)1KyR2gy$y86jf&LuldQKC=ha=9l8M=4aM>$}%b+_zLyNw*eFC?NFME^JZCX7~3J4BP{km -r3<=fI)a<=n`ux+buQpvC#ZBp3B>3!#Kb-dnk%ZlT0>_2ngbA8Jm>Kd4s=B{DV9w6Xe)l|D>Jy(*pqW -#=;VeO2|+Y-FtF`Q%R7S+;<{m07tFcELbp{FEZ25|1Mrket_C!@3C3SE_> -ZxHgngH3`j@jB#INqUP-eD;JVUN#K=2cSaE|K33io18nz~ -sN)@wAm^qxk#D&f#lUo=R2{S824w4P0?(ZD?g&tknWnBjtcILfgV-2)(7=>e -bNXnLcdSDi<((w3z#Rm@C%mr`PjVL!-)uO2mVU~x^d|UbCiS0G_7-zjMPw*XI>$JcNrO=0ll~+lb&cO -zm_G$s>Qc%%27TJQ^;86x^wUrjO^{ynGb#Zf)IAAs)_rYO1ybQws>^{CHDM_X!kKKRdbToit -+tH(NOeBhDEe^oFV$}~j5`~~9t*~A^rJEl9+iqc>VE$x^$KNoKc-k%;GhO2Okb#3o*|btYdlF18A8aO -Z>z+il!JDO^Adjk!u3l1z05qrLruK63~x5$QkpIIc|Nmp#zHI46loE}epO>tF$Z~7#>*e;Wps_wq)so -^6-d7rJdv@0*B9BaEe@DL5*vr;n?z*ipyt8l4M*YR(B^UV9;`2VPcK14?riwz-CDgf5;3`CTX -oIDX>vJkBu{Ou&>~4h;ElIIpu$EJ=j5Z%Rf`&qETb(bRwKhHRC`(efc@sLTWR=DXo#(F-&iLM6ojhW| -5~4LH>=oW&%7dsw`VUsjYEbZJLipBK{Ozro(*kU;G=ZIh+^*jaulro=0k932IG16%UZ-^6V5%&#?QEp -kRY#kUW4>r}sVUQqwqVgwkQel-iO9DWBGbXO6{I91b1gZ#8{pBcSYx70Na3NcXEq8!O(AC%bl+)i5`; -CrD#f#0Ap8vzyI5@rfL$Ihsx++6(7SV{##PR(jFkI=8(kla;DVVzV^QJq#nD>8 -Ji8L)OVims_!d9go)VRXXJRdklDgS8SNcjimA&O-;^hatr)GLA=pNpo{bkeyl^R%}{%~ky$`TonSkZY -n5MVH!!W)c$(hs5u$u}|S+a8>v&vSs3uM}o?6P2P$eO%Jvww$ -_uUFzmo0sZbmlr*$&-^*cyu-V_uVDj3cXyF&@x)ol*mTm=t!~(9tb{F4huBOf8nsNKhSfHc0iJ&a6t! -SRUeis*E6i_6s=J?ghSkJcf(oF9=xv(b|3y@B{^T!DpEFAS$7ie5hfer1=ez5zz;7s1^^rQvRXFiZ-Du&n@OamHuFzFCw03_7#k!FE^w_xNgfy{PfclSjY2QlYJZDbH -@5E~Gx!ljp}LPa%P`NCp~hK>Hk1Ip>V*PKRds*e6{Mp?luVy884A)WJEYbG`|(yBz*`bHByk+-eb3Zd -P||y|=Uuq1Ruc;4eEG(5Rxh0;6jhNbE%FA`vjU0AZ=VEBNZ<W|x~yExh}QEk7JV=RI-m|^-R4hEp_$#5yDKET&!pADYIDl3UV5uuiW{?Z2Q-~!As -EF@9=#BaaVcai?~n|ATlEh$_DLXcWrl0!FgR3f0k=|)<3#r##C80OWqUDT_e{pvtVxwyaOiVl@J)qX{ -XQ%H39mW_iA?q#FY$bE0*s<$__vjZ(}>8}`t033F8=yWB+`zjmqa20A#MV6Lx{Lbkp$0LD=WUCR=l1aJ77z$meP -<^;?dZj1f#@6^-y6ivdSW2nB$EiX~B)FIQ7#>E&Z!hg?A!e(Bbsv);w=0QBg6@&Jszl=t(KyUzi3zn7 ->Mxhw%mr!knO>LA8%q3ywU47jst+jPcO%_2;94{pM@CIHU^(TZzS(zF31-)DbUX8JNB{lv_;z$r+doR -kQQ=+H?0RxNQllBb-PyY1HfQX}QQWpTa%E;V@gh9b-O+j+%!XDsZYWqjR*1In{WMkIeZ|B4_CV@ZMtI -GZow%}9K{vDw%vc@yC#$`&a{hR>3>5eFjjvnP*Co-Pl<28&kJFITLJUZJS*Ak1a*WDEOgM*2A%OkbzPW_`MJ1{ymSn -LfV)X^LejTXmly1yN;?fzHcly6RKgRc1EsQ*&)_$5QsQ5#6kXCGrgsT{8|vCJ*K!?&Go}n-k6JW2*Z_ -EH1p{@7ndNL~i{-ZS%wA)}1SMKC-$+-zZ!(F4w8x=$le7ZEK7NYiOd$MRjKifsZWa*>_ip3Xbc~i0J4^pW-6!W -jS3pe}`DE1&L<*PJ_Fva~MLF_E%(lIq2lwH+F`gb45hcZ}=sTzItFu;Rl8JOsBUwBHs~9{%UIfS}!EJ -nJV>qZym6e0;KzQ3Z-0}J0J$3g03-ka0B~t=FJE?LZe(wA -FLiQkY-wUMFJo_RbaH88FLQ5WYjZAed94|1Z`(%lJAcK3K~Tw%=(gt+2O=t3HU?m|baRJ!vtt7bHT -jF2yC_n>%)hj<%1snlwKzuss32)yZ#hRCQK;l|$*&euRYLF3oqZluw2>M8(D8vbkZW%c6I0)9AEpBhg -pyvdMZobmeUGezxGK!+x+hq$l2{!_JW;CtgC={@lZ?{uu&kl2Vg-Frkb7(%+I|+5kj~E5$F;;OYPomD -GjuO>cRFepTC}Jhb6UPB#WGRf08d7mYTW63lC0HX{*J$a`KS;=~Xmj9-ObOSu_OEN{Il90!E -^RQ3S5E9%yY*G}@oYH4`P6uq7Fdm53H9b8mwQct##hiyQ-CE%OY6=Kv+0jS?^b(V57GBdH@E6-G9|Ah -JGcz5nL={z_ZM1Cw18I^VZvAhK=R!70c1-rqgjC@}NL+u&BI@|0tMv~JU+K2u#C&d(i_@8jo=%|Lg`#X2&RWZzJhND@j&lzI71$1o) -3I|x*L)?KrmD$Dfz-XIuwhaJ(&LNC$Nz0n -hzxNsE%!__593Y8qsqBf&V(A-6os7zjGVV_#&M8RScGg(2J@6jVgf$87o`-rjEm?(~-CbF1(j+*KUJq -$x~2c;^B&Tyrw#-onnWFnPOE-syZOQ8>E?4dA$YN$|ssGkImk=1w5 --Cv5&_EudAl&L@?XW30l^;Pgs)M{S3zldUxdTHf&Y=8sb94QoU0vz#ZMQH+tb{10k*{%QM1U59*$A?5 -R!pteioJzXhwtecMa28aOMXWQ*iaz)|29hw3R8Fyq30Y(#cK(4%qC8QEN0IF?y(Jfi)A1Pg0#S+X!1) -Z+oG@k(%{kQq~9M|RbU!kE6m*5D$KLKE;{yGviqF7>}8Td)Kbzf8SL?_=Vh*vWFev0uh<2z?z3fW=M5k!+TbVHVWJfUKL{MoMNd!xL>a-TxNvkcRSjC)7hVg45Op~4SHYXUo%piU>tS#O@Y59L$$* -E{NMWV(mULP$DwrI>&)sduL8CsTT~$s?)|#$geKvzL0@V~UPCSFo9g6eGAyu*IQXBm6K&fXp*Z!0hW% -HH%HMJ%mmB^VqIJPE&BZ4{*0-bw&caJC9#yrJTm4ohdwfS5(lCl7!!ZPA8%)&e)ZlX~`7XK0S;WoY_p3B~mp+JqSGc;0`VV#Uq7Hectq-MnFgf=V -a<<$`lr0YF*Eh%Q0noKEtK%nRcyvzJQC}^JrJQXE`=rA>kp#$L+xX(VK;@IfjL9&SAFL#He?O@@q#tL -eyz7KTpKJBHoD5TsrX13kCH{*wegtk6-S>Lmos^WDEzD6dCGk50Hacf4o9}TNzK08gPs;^Bgu?PuP2) -`THw{bPVmRsR}(|=>z=>4utz0vdeeny?td_E^!bvK~0SyQnB6pWhY2fo%Q*~%#d=rNFZv1j@3+lyr7; -Rq4_;QLy7cKQP&8A$wXq$O&XhvC0``Ei}0kPF@x>Srr$FcOy=GT$sK{0vg>!I#b}hEIYo~$E}vOB7!~ -ozZ%qdz#+}>%z$P53c>NxC+}JP1OBTCZd{I!RIbb>_yq%!DeLU&mgwmt0D~Fv27T#dkF~<8CS3@a -f|lr`4Fta2V}kNV+!FwY_LTYk(uYwg}^}Uki|V0XXE)?viab?Avi=aMG)b^R4ES9 -X%s=&J~G~ATyxB{{T=+0|XQR000O8`*~JV000000ssI200000H~;_uaA|NaUv_0~WN&gWb#iQMX<{=k -V{dMBa%o~OUvp(+b#i5Na$##jSF -_n{EO&N5_G}Y5ltcjyh!wth@L$Nt+IBa@BriJ#ee1}zc7$!o7Jh^_a|2>Rw*@~!(+vZ%_1V5*P%HB3( -l_0}V}95r%N-?;%yh_(`jpt5uIIC7dqYVGeGYs1=+I~CkimQmouw24N=(3}K|tNmHXY}U<7zQxg+v|v -YyjZf2SE0G8isG=(Pse{;)VH7^+BJYRbOQS*HkH)+=-+ -tVMq-C|qX|I3r5&pT%lqCOrfuH|=S;OzHOT&K&ygBvvcEF7U^5HPA(abOcbkhN^pcIW~?M(lXc2G*we -Ip*26|K4sP1{8Sn4FdcG6+)xseo<67aVt#*bD+@*(T5=8)ij&a0C;_oNR3eytRdc(==j?X1l}50kaJS ---DsT{-m1^JB!e=LFn7ucRB!MInf4XNxB_-ZwoGh8j#=y;{F4%(&6%UFna7Q{!h7zUbJ@XVAmnwK|p> -Th0qvaBOd@E8t8Na107H>E>N*ShuFmuDY^Uda6toyvB`w*?${n-Ii*x9FxfWpU0^M|VZ*?Lob!LE6=D -mL-!cbc4%GzT+1Na8Vh_MrHP_%kTrc4ZE(<|yAqozPmyLGZ73?4&(e;D@DMp4+QBuuEF?d))_!10k9< -Mo-yRjU%Ix%LW<-=g1ld(bOi`lpFP#==!z<}>&hdhjzx3l{t0f@z5y8KRNBQltNCqIv;!w%7ZnJ;w1A -hQK0H@=%s#yYf*r`qIxIG%nbH-I~xEy-kjH(mna<%~0p$&Gab3Et@o?G`=Q>clvbM19v{z+Y`So=s6DZ8lvl;Iji7FP1 -6#!`RR}WUv?;AhJ+%F#|SeV!)blLV!Ef1tm1UI!XY7-}i>D$~4pm6QF4z0U4p&Xu{lsQwCYBwowQvf3 -+g^9z6qmFklWu%}RVX8gaYlm?31xy}*m-skpBta@cS(5#ywd?x))F`&<{F^VxX1)EDBzTCEv#(%)|(O -@3pRT6xKr?S^Nk$%n(-$K-cl|4vt7@)ap?kqr3E>AUNh)|2@NxPkqepu%hs&=sF{8}>G}PB(Z9Lb`9Y$=|L?b7=Yy?9DG+AdW%C99{Q)=C_(N7beraY0V7 -GA3@|{AKf(DjYcx{U1y{L5G3pORFVyE%K`L_xGZOhJ=>8NmQ{10OOXdi2cN7tR?f=&1z@TtrL$ -^?nn46Wgah#TG?jZ?-$}H&jR{hJBXNJ32Qt;jh0AjU(K8F*k0N+B^!{oY#0mGF08*Xp+xkEQ!vVhET&$l)^FB)S)((Jt=2G+_UccHb;#kk}R|p@KJ%JE -@6`MILQvqp`c8ue<@1A2b+H_qdO`3)L#R;dPoNp>Jq{dtRr>F@;v!2EK2;sZDoLXs#Vv(lZn_91fAvF -8oMoT0#acwv2~DMlOEbwt!@ColG1p(bh)EpnkH|ZByM&;n9hP~FcdxRj4d;;@-^F>Vk!VTlQO@61EgqG>W+3|z|>q>gzO$Ql!a^7 -VrtEcZ1U>5Q1>KxGKJu!a;>-!=I)&D?2>b)=>=w6ey;It9mSAB0K#OMJ87QE2XiKVVK81fD>~M#kr!Z3I -hfe042W^=nn2A$Jm51emWqU!Q0*S+tBqU^$#442V253IX-a$;m`b0#X!ai}oPMRu9_?MHBMM&eh+O;w -xO8Qi#jCh7u!cR3*c`#?QN_7+-l&4N$CV41TbGbjqJ`J37Bd3&aOsFQ?s -*IE4oq0_u&-dktJjx&ncMQ60deHn??n|=4FWC*OzE>_tM(Q0tFl2j36H_R&{v=1?<=zYqSgmO^`PPPtz9=Csi0D^8E2{eZV?=rAJs6!fR!)df%Ro+bb$Do=UDxVa*{Spa -!l5<$+W$1o@IaVSA_mI!N!2iG#;SC{j=^Q$-bYlwxh6^k>tF3YDQOLUD+aExE!-O<7WYBOVGsEU2dE`TLhK#8CgARTxf+K_)v{38%8`R#-umq1vVyenSrz*#b4hMFa7*V- -W$*MzX|2E>F7{Qjb<1ouY2De!1WFLfYwRMmAC$v<{WIk-Un}KM&Tn&M29xtv0CuzAx7#^qF;xzj7A1R1u24$PX|NkvMWs5}mDqlHdaOr2sWG9`fs8N>(l}>D=uOda|1V? -SC+#!tlD-D%fUMaBDP@W!B$mFXEOgi@|HWrWyr*VZg6-7D^t%%{_d-p{R*k -24WC85g|YP{E-5$mJM|OeI+hcejBA6LcNP}(ESSfDWfl+2^}>Mn3p -rokBf4!M2@A&5_vUPvP_gGFYB~>$XJxPu+q+1h;T;X<-w-o9=l?GOSMJjTYS ->%b)Q+cPZ7QZ5e;~bCP)T_<8oYH$FsJsXG12U+zl%l)fEOFjal4P}4C6H+dmx9XoHRUSrNa9Wqa??k= -VsEKS_IBk}eQ}%Bc5Qq^WAkTY;z0pk)^Au2EPQ~8hR@$xjKBHGF01qZ2Sfm|_(2dpE;Fq#z$?}A+d7l -qX%0NoqWm?Qsv0MiKCD)N}5}}*WOjH*X#FsnefRn>n~6tAzdwae#7hrvi;M{HK_ -kgeLMjNEqWPjvJr3!&HX~_8MK~?*5$-VTKmv?UTnqYPsL`Wd=)P!4fRnZ*)YhRc!xfI{6GMa^t)$-K2 -sO(*N6BH?*l$hNEGH~u)`uBl(pY0wBMPp*^bdQ`TY5Fro86ZVCneYXQn+2F*TFBxT&qEB{m1<`kg`l8 -&TJyf@9h*Z9I(KXYlO}$1mvy;VjDK>BpAgPd6V;4_{4qFc*Ma_|U?Oe2)e6i+Hs50^X6M1U|O4{e6eL ->}+cn6SMK5CQ!j$<4w0rfFX5pv*4_(?;Zw+TK3lF0-l$FEGU1UiSs;f9htTR0}J36W#A(y(w!XOWf|Z -M$2+<0{z+Jt`o6k-_3<-AoYeJzk4SG|HhYRY&!kR%Qoi}>LjY}~1pA@@FlAE~+S@k)(7!AI4A$tL0{v -1J>=)dj*Ki*z8*>EuB$MDTRO61%Qpea8+fFfCSt}%~zk0S$V~YLrTrWucofi=a&%UhJz005MEEwZw3^ -By11Tkf~PT6j^EC}(E$ekU4&s#eIaxR15DO-Be0|!>XoSx@k?gQ#;CWMRQB)Xi*MtFb(g>bsJqkC{FX -Lt*fv;e%2O_Jp>jqN?WI2V&U?@LhVoBn17kXwhn!~l{nQR^G^VW>yq1q}3Ge!A*iN&QonoS21>;&HD3 -czSVqc_xA2rWx3u&nBY(O!7(S7rszR=QQ13<6Enc`)oWLBo9>VXnQt3x!U0uJ~@qi2xd{}SuuimXJX< -5*f{?~Y6UA-eErd)2jpt@Dn~ahxnAEjH>x)w* -&Zl+FQ+Mlkfer-i3my4L(kkQYbE#*k9JsL%-Fy|2KOm@6aWAK2mt$eR#UDjr@UD -a003e(0021v003}la4%nWWo~3|axZmqY;0*_GcRLrZgg^KVlQ8FWn*=6Wpr|3ZgX&Na&#|jZ+Bm8Wp- -t3E^v9(8*OjfxcR$(1<%Ey^34^}mmP`#*^s8`U9h$(;&jE(D+F4mV_jrXM@n(MVgG&43n`JZ-KO2XEI -=EHA|F2QZ)C>f@#wYQJSx?lD#c2bwK7&LwGquqiJ8hciU#AE -3s^=yeV{LVWn*7OQjpJD2&RRax07~Wuq3N(FA%`+^pQLr|&N8Xx_RtDp%G@v&D%ws=>)?eBNktn+n)= -D~c*F+Xd_{HY;5!F^8>JLN7(>DH)wI?+jOhwGzDJx3RY)7L9z(!!3Q8#TVjZmL!gS-bBz$g$*3UFiOT)aDfBVfH0$VCvIq!Eziq -6B&~c*)MrudZj=)wwtUfN8GRH6WLa1pLg$kMQTk`7al51~aA9&x8>1lb_b((|*kb*B)3iJk -OSu_<;VysTjMCV4b*CTeiqMS+>fO-jSIrVdYv0Z5E!_H1$Lz&Bvop>0*z}26IlueSKa8U=L-{_lQ@G{4lgDTT4`wqqF<~lN?0p7kJ8X!3R!bV6{kJ#VT_6&^Rh0Jg5{a5`R -w;!wv^cd7Tskhl2ejiX;r>x)rQFG8KMAkhc`(Rzcu)+31#(aN6&NEmBf@Ke$=2^ayCaXKlyaw>B9pNz -!EVvIvvpVB}@020#67ZKDJ+5J>=M{t@`2GI+)pO6zK(iRjj=_Wz^BLA;(ipi-O55DeL+oYxz)ieq8#y -o4DMIeWKpWjd3bf!#7FMA{z<8L>X0OkU0V~E-MiwRHJp*Yikq!{VTU{v{fyjRx>O6nmkrUy_VscpbBK -lS_;m=Y0>a=IRHEKC@5za0c18)tJe*L6-DwdS3F%`$r1_t>g-cVXYEK0`jDq -$6weiXi(KXs6cB8gb$zn$cqf-{Zyq4OTmXJ0J!px6`^-)q>nf!pxaElwy!e(Al!Q4_K#@NeD%K&Q;xr -7chQKkC-n>E7Wl)pnbym5_p*0Bgy;-d|E;)caGH6Q$xk+H99+qarE9*r9@{D0yFc{|6=$1t#^ChFA-6{ExOEH(lz6YY|H1eSLy?C=F -mbS*p*ml@t1zv>E2|Ssi$1X&WYP>WAK4Uc9|Nze+m)&1)tu=sFq?dhTCT<6QPTFn~@_0}Q5L_QioK1` -Tmr0&(c1dQF+P%cTlK4LfKjJ^q?JK6>&j&=+OF^V|pf*qiO3&(Nm=!QY3No*+jQdI82}pI^G1Ib~+=o -$H#)v9n0U{AQ3W1+JhI5Tlq9>btDO{bP1^3;7{kC{q1#+bkbFA4e9$GrYw>2!rx0t8_;CoY4PMXa2z5 -EH7co3GOyDp_KyHO+n1tBH9sVv(3ur4aKnz4iW%Up=K@k1M#NOfdNI(VLDGaIT1(4fezqKBOm@~O4bq -YYrYg7&VcPCfHzpY-zcXUjy|G?pg_u9w+*}fJ~A{e!~)tiu;GN0$kVzcob+`tGhr0^+!XbLoG=$ywOS|eFXOWKPzg#OF@wV8)Vsh^8vN#3LV -<@uwA@E2Ds=Y31-EVmr!(j(bI=w@oqVAVCQ&Rn>BWfxGSs4qJ>~;o1?*E|QHJW-7x51*WOl%R<(o!0y -(b2k)W4*lm8Hy8G9UkDeygB7hlgtHWD0dz#-yEykG)r;K1e$M`I$37pf+EnH%7O0@^~_#z(Hfkk*J}N -F0r%GfY%F{OMpLlnN#!1?;F^}o;dne3!^btfvLr^$SK&_TIKMf7M_S+wBMQ~eoKZJWQX6I8BRhL^jLq -H2VYFScGG1P4RV*74Je*HtoBT!Ceg{>?o0<_f9~iOAIcuTfr24UL;u%HzAwnIA(QNEeV>+$M?6{_BXQ -U(y0r&6@~zyMw?(;NqB%e7Bzh2+-PF0$A|~z|Xo)i9E<5gth`K&Yqu2|voy`^K84vWaao)~>GE4{!#> -g~=q$|icH?nFF+(NTg1nzZFROcXedn`0A-T>pKL1`JlvMb#J-w>npkR`{IGJ^&wd(^<`XZd(hSh5V^^QSgJHFOP}Ly^L{JwmlPKreA+n!GXkr-lzymsp1!nxs_WNmp -yvM<~7bO>N^8b(wY@lm{U9MZTrC^y1D7lVmk-V#nx|QOM%!Oag>bFr8mmw(ogAlO+|H4tlC?Z3a*7cX -WwJo(bK%>>8rhvMrhI|7SFDPpacf^*z&kk+lEe4%PiYCEC;?^s9hZdsxJ!&W`tj=$hJ3nFG0YeZL@c3 -_TB8tfOWzZxBEt#o1)4|LVM=)m|KzIq3f+`y6gXDfICNl24D%_B>Vjz2B`wCo1(^ep^ayt!oVuufuL)~eJ<9E -}(i+axrzVHtHdW>qfgHsIEoXFY8HUu_KrY+ud#}suNNNj5`*P?IZm(5wa^(NdW2weV4GbVGWz3iOHTAu{Md0swB511Ie*)H3-j-KTQzhH%{XzjTTViKH?e|rPIshq&s&@*S8CNPF#TXY{^ -KYh$?(ae+PLd5s+y|-XwJxb*PW}FsV0PY;&p-aldG+Dwk=r_rPfe_T}8A<+^GAwBflzL=m4id9F^T9f -POh73h-U%5fSevL1pzoWlQi3TwuxBmGx-N1la{Swu8X(GWdPYAWuq7rpfUB5d5dz|b#^MBrmUspTTW2 -w^!>MZqNf8YKKc9fwEM;LLCVum-?bOhwsk8B`_CzpOt{bmbi819Y(E%DA4m^pLZ}jyvQqICohXfJ`lT -$_IQi-i6Wv+iNh7MGZzlPQ*{5jD6524~rlu*IYr>LF>o3sneB2)K{ku7~F)eNjFeS#%4aAU_W#j)*u_ -=tSb(dP(aE>vbWUeXWi?}6o2lMq=y)=CWj+^8aM2zeWeEM4Xa-=Ok6;@F6cx)#=6G_be -Jb?gBAq>p;D48}hK=C-$%U!0lAfN3cjAbt!!)ELqY*R}Zm^VAOs98vyXo`eRqt^ux=JXpOskuq@?`-4 -7M?9qh`;HJ&SD8t5*Fpgq?s1HQW3u?1U*H|b520$|_{!MGq9Kt7_O#Wi5)l0vyo2+S3Y*gZP1PP!OftHC>{hJ~u|X;Oo7LbA$mPmq1m6 -x{885i_8~Zw@TGwrJ>*frNGnR?~D^-rbm5+0+N%@C~%0 -$H!2CiPU7O1j;Fq`6BVzg%>@FCHk|S%lYP*_0|Oiy-j?YP?gwRbvA>=RfA-Xmn6U0SJ|iU!TyZL?9N# -ljmBrx#?E5$GJ^t?2JEvuULCD^Fo^i!0@bx0Q_swBAPr^tSvTPDX^t>!!8aHUUvg;kkPZ+0$_g`Masy -G_by)(I;E52XiPQR*lJ%=g^><00JD(+Xd4D6eN!OD`L>7~P$uCa9v!c{DEcvevXc$o255iKzfb!u@97w7l=8$i~ -2(y;?<&4V@bbnaui>+<}iVBXGeQZ -SM1GcdZb)a23#}}BLB6zDlv@vJxy0J0xSUP!2(0OkV}PI)h1<%4;`MQT3E@joZBHzKVwWg#@bEg*kb2 -6KFl4VdfO~2~S|gUvIHj$1S>)h=9YWyt8+og`RFkxjGzS~xu126!{NRGr192|#&SmAQL{yU~Gl|*-PV -u@(g9fD*WB?8t-j4g=U{M)aL!<(dn}C>5H}>^Ix`^{Y)TX#Y%wcKFf=j$*2^14*p5jjPq56cO9M9S)Q -=zb#H!V6VS_1X=GAKy9P=>&U(gB25z(uIcSI-IDS0%?z(8zOoBDGkz|0s*GB5zJiSNH=_Ac0ul)}-(^?W2*Y -%$0OcyYGw~ZzM{U9Tb~WB?_IgHbGw{)+23$L|AfW`q>@NfNG6*dCenpmB3aNgh=nPcMAtX_(a))vFh* -t}#zkoE-1Cm2uAS_e4df)&DJw+C+hy!<}Hdz>4@!cQeeXUIHU=MPvKC6Hf7PE?Yliib=E&&SW6B1F%j -j_tR1%>H0Ice@`{?RJyZjKz|kwE7@`hN3Y!7l{Rz{f{@1-kEyj*vorY~CJRe@NiYYI>p$a&>#4qaD|f -__Mc36CxO65x*Vm+8hwLOCw|O(d3;bm*LEhy}T)IFz*bppJHya!8HP -{FgU<*g2eL1H-r7p7Y}hdb*puq98Dr6bIAFci4WE!3bgY)`9T15_t*_Sjs{ad{3ITJ;)b8X@V|WxM#hpgrOnz3YlSMyWhP6kmBC%(AD9fv -uNSxgwE=(%nh4y@v3rlq!}O8ih_lnd6+mUOwfud`4`gF|neox>`^mLN;T1emwuAz+gf&N>kDkz%g|fL -R_|8v{JI#1{L^IabT_$VuCau9{lJQ?&yckahK&J;lO+1jSRF(U~2zc{{nMiYnM$7D?jUWFx__$~97r) -XU$PAWFcX%`P+fKxv@G-2bdHLu+P)h>@6aWAK2mt$eR#U7wjI2-q003+N001Ze003}la4%nWWo~3|ax -ZmqY;0*_GcRyqV{2h&WpgiIUukY>bYEXCaCu8B%Fk7Zk54NtDJ@Ekk5|adEyyn_QAkWG&d(_=NsWi_x -wz6m>hxgJ#l<=Cxrrso8SxqU#U*)(xv5-S@$rc{IY1L^6*QDE<&|_axd2d00|XQR000O8`*~JV-)WED -CLsU-YKH&-BLDyZaA|NaUv_0~WN&gWb#iQMX<{=kaA9L>VP|D?FLP;lE^v9pJ^ORpHj}^WufQqOsgx? -b#7=v6nYvTQaT?v{*U3)PUcL-XiIBybB2_+YtKWS8`|Se&!KY+7cjw7OB(S?!EOr-*2Z3keC|<3T;AW -AD!E7k@UcP$yV(;bNOYv4le|XCv5*IR)Ng7AJXT4|ek}QKX4dO@ysaVKFPS@fl@uE!nBQZ~;6!Bcl7G -82AM8dIFk`#L0$eePDCn0WDRHpz&+Kt;ES<$e(_%e4uRr+J= -4P^=^@KL*iJsei~>hAD_JkHav&#|qBk$Pchy=^JXkE6DYx@tve9akEEp1{^aV*cPjziB(p*@WN0`rIH -*J4RZEMwMvkOGy^>dyily-gbJzZ3n#OL*^(#HaDqhx3nD0w -5-bXIHIp)HkYDXuB010eYR@$%S^6z`4?Pk`DbB6CGGxYO(P`~8nu5Q4^mPRv0m?h|+>J@h4Lf(MqEqg -sGhS%c|CVvqE09}s)Jc>)UWy7N5C6Y07lSfU<*zNT>q#^v&Rucy-Ic{I!7I81dy0Dh{J5Q8dvS)Azib -ZxS-^-89_p6hx!@R2}i2clnddE&>ZO8R+4{5o-R^HU$t?jRpDobg^cuYPb|LJdF~A5%HcL#jN$tfA&! -l1icA!B@=Z;55-p&_bNv#qWDS0yuLt$sn4?pI?3v2M1ygviIAw_c8l*WB!}HFXE>IG1wo8*FzB5Tma~ -so`64eR?&@15&+x*ypM*I6Mz@Lkx7vS2`XzAf>$mw@ImEd@*$%P_bwqzXznV)G<(nET9g9!7K6Ok7EDTP`nVYn8QLWOmv)m3*I>f* -E|g+`uc?jjvOTz&j%y|@h=DZ{bKE=_~>TCqDYdC_-znVcVTgif4-c9^@S3YdH{XaypoN7i3HP(gkrU4 -^0Q=J1_mmF-v+~>Nt6#Wxyr;jXz3jKrsE`ulQPOPkkAW|P$_YvjOub?T0gwriq;SN6!w(i)2{ItTS4#y|2(vFk>7+6Dnd{Q-IDO`;=oU_?^BXSup0=5 -!8cZ@Byt(w86Y1;7ObETb1JqPYSuvm*y*`eC2EFun5hu8!SYssZAp!ql4W4p?-W8t|CH6KXv^+Kgf^j&2Nq4{eCQY|N~f#TWVvA)P^?Zx* -BJuUX^C+3|oqRkkmUWtR!~)pZf{}1{3gzMZxoz3fy-9h)?WaFl56w9Ncj6OVm);)25StsmhJWFjEi*= -Tl-A+(inb%Fq{po+JT9z5!cUjU@8=rEF%#R3B$)s03tf{D3J~4A~y1o7ZMjH0ADWjaYK0MY_uJx~&qS -ny7Q*w2=oOUFUWn%?Cj>FoJ(|U(kT5i>g;clUA@>yO^e0Z?2GmXp?G{-;IvUc%N2RrXiIPXfL$1%-ms -5WIW1c@Nf!X7hbmD_(NKOfR0S)_fRYW5c;HSp@J-oL}C+g1rH9|P)g%Qh^KMhLB$*nQ=)#D%LSxRVi< -+J^2PDR{>wcv4cg(38-K}thgHSVIh4u;z|6>2kDsD -%=%+Y#7DE`Rl^D{X^&a$bB^700yEd0GB5kUAJ}@u4-t}V30z* -BdhM_)ZMCCYLRYxI~A>>VO8|`H@R+zI}ZOIX=F(b+8XdYF@pQLsc9&sNIJz_!zTcfQq5IEU$Ka3CLE**t~*I^8X -V0+aQBju12@RZk?%XN1#l^`Pk8;kHT<_3RFzzKsfnUnT8&~%v~GwoEu}`Ai))tz -!ofu#B^!Wx0&tW4)t8#0Y(n})|#y@wau>GOCY&4&XbuMZBTWPUW$IoOHEo)=~*JZOqOjC+oQ$Nmkbxc -oQh$~2k>y!uB>bUWMRFYYodjN=6FL71FW!wOlq9TWonO9tdIl~$+=)p4^D!@cdc5chu4iy!90p*=_R) -)9@TEhT~pvs$d#DCKf9#)&ha@8T}m%OjQ^XI;iwUR9)wU^g4grm_%h8Q9UKZ}6fo+knO32v3^w<`m(( -biUn-FHE0JsAI*3+z#)CD*DUaQ3wiEB(HE{gUN(~B)8pwfjYr|Q3t8Sqw0Q_ko0oE|P_Yo*gi~l(_FC -M$mr_K1q)3gIW&NC$+2xFHJs}cnWkylAPlWA+^6EC`v8^r%$fcRmP&Tx0CjJJWdnNjVlA?0i8t0;p?A -^c9Q0T-FinrNG~p|%RAi%>qef97#U=&N8QSyY&g51iG4ZChoXU{V`+RKRI)O6nAj>*vem+RYXaO!&48+S1DijgtA11oIzSFl8c5+%K|h1;I^x8^}Mrgk^&-=&|4hvz -;P)1BH~e10O5MX$$Hyba9zDQ@EHw?`sH)Op0SbT8R|O`QhsH8gR0*hw5VpgItE-DkncT5L)KjRTQ+{otarDc$j8IG1MB$Q#Gi98nv`uto7s2e -7qYUeD#ZOHLf95hLnE#@OCXhs~5~cC@*a(xxvs;;^1@H8J+0LkOJEC7a>BJ_4?3RLA1iMm?9bm2>cwt -PA{GEAHd5+5KLdP9;1&R0YRwrY@bdgGD3HCj{R?|!Mu9Z}iE5UUVB)urOV?C+!SAy#zm<1H+UGa}b$J -9=&?3TdQkH&)6E-VNXXm?!74Ox2CG>XK|7LI(Tp&m@@Mx6=-9E5Q)Tg0*Q|Jm&krX1NRnhy)Io8)qd8 -w`sUIHHt#o0K3srF&ae!R+=)N;r=rrEKrblOhne^j;cu3Bx7mOs28Yj#GYr8<(`jg6ghTReQ7Bo%J_Q -Lhwu6u>+!>*PzL9S{rdH5DJ5P`!13ar-M34Y6wk|-`h!c5$nwB;4+D)aaM#il~=`>o!Ug}VHqZ?nHAu -0f^zu$(iP+={It?-{dRgXxi~61E=8#5y1wAnP~R8#LFC8xDb0-q^I#^-B35Om8E*9J1N8o0pXa>lQ%T -?HxuhDGfT!YmS3Rn_eChS<_3j!?wTDS>FeoOD9=~(UW4WdYco&h*vnLm(PtIFNq$qxu! -noHc%w38KC~5ns;N#@vxE#pub;k4-D$ -dL7ihBfOE-=xnIKEuy#pgzIH&mJxwz;<3DJJQp7VzSgjG8r17ty*T3}cfpbdi8;QQi$Ca4E=t3IW_DfDO6_;Ej5}0Ux0DG>u*GZ)CPg -;)kFmVA+5@QQ5oH{G5dLF2L6HU!d|NT1kwDji|tjGm8g!?%0Pj9{ojZZP;FA`mD-mQAWU>g>fnu5CZK=0QcrR1-CtvF3{VI -phZ^OS^=Z)Xa#nyP`S;vU^2HbhGK2*>)C^~-d8q>B4c~2m>?1c4#j?{t(F; -EG_8Y$EtRdHu$#UW_>C&Zs+j=gnFLkLW`N;nh@)zEVk7-b| -lpBU8DY;#YY135)*1q=4G~GXVr5=8LZ{QO4Fs9Zge{>aY-u?Y6{IlShQ7IS+32@M*6xL|i? --U9fUO=IzXeyHnSAb>>2R}o!Svkts2x|?#J}`*(erlQ?&DWNr36w>IIcPoYO~7lu9i=UfXcqMC}_q^j%yUHsdUF-OrEygs@!L -pdW#>^4Wsuj>I=fUwk8|)}tZH!~THX#V^QQ{9FJJa&qa(b^BHxY8B_<6XtOyL1OrmCzA8ki$P}y{9=l -;;i;VG^Ylo{`TDI4a}&l>FH|7v1tE&h`pQ)Zmh{+E1IWV1whGwqO*JqtZB-~GwA8_V$6ng#gXiDj))5 -!?@8g+W^a=e#)>F-Y2Cb*s-`IT>We{X_^A9oy=?t*p@d4;o@oY9`UKn~)`u<%%cCnyg@MHFR*aPLH!&zKbNdtXW2g2%3u-B@~=G-ZQXljKKb_&19j8BqOi2_f_q>rT4`D1I{CI$pM3)EQ -Zy{YnsBx->=ijNM401)~h(>Si7jiuaGI|f$~F?0cz8*4RYb-ytA^7?!bVm&Im$1=d6%pj~vE2ryI=KF(Zi#%CxS|1OaCSS?$4tTd?KfY -is_z6H&dXou%~8-uuRN1$wgueh_#twQohibi={n=rQBB#&wlMzHXUYZ10_|rO|$}znL9}hfGd4k7mNvmq?;@W%Cyo} -v_CVqUs}oc8vW07(9q?j4sTQ&^l=|j^x~C0Jj)v~I3Ew*Tws&{|8pJGF+W+7L(srUQMQMh<@hvCNOms -iXVCxYZGY(C;8~RwYCL>Jls&+;wFV`xJnc(;o~SP$Xe*u#2QUa~q^be%*=93{nMM5#FoML6ext0nh^l -kci(wb=@zKRo!;`qS+A&)f?!Qm&?PK;7_VS -eQq-l}K;pT$*pol-_AA3(AtZ(FhU2$uR)kKL@t`Yg9kadPV=9N*yC)|nUiUI-DKn1Z^c5V|ckD7ZROc -91%WT!r3D4wC-g|K1-7{DFrd?FJCAG~Mo8X744Pa7H3#H_PX#)#!;zRo9mtSmmZ}fdI&F3Q# -w4mkl&w<9XEx>$OkGtRStnY#O;2J-^T#XQYXxoUfhN`6$mZ1~NmeXMsH0xkl#p4uyYh{0gq8m>|B^dV^e8QD`$%mbj$ -2qh$hfjjy-ckC(~GmaPlOUK0E+t8v(%X2NOxNRfBA$I`)ysrqy%6YV%`IBp>(LdIX?84K!O>r#{)JFg -V0bCK6p29;?o$YGu=g^POorjgj!&qyle{P0PeBP?J6nB0OTKOjoledXOhpF#|sLIDR{>|Dk+$2ip?^eC+he;=x1>*n%)Pm%GK8t4j9~->p@$!1(@CT -)>HWf9eA&j!BBy+i%HRN18GB6F=|bx%9$8|Jm`T%OlwsPr{&8|Nh7!eThLo6b4ig|SxT_IMujPKLbsS -QyIRmg2i)V44#PKHY+}K5Lf$}?OsC~>T;0kjt1L~17vRkdm$bX!xuVZouu&-;YQ2KKFzfU%1kvZIr6E -d5p%{%Y3~)xxrIqXl?*@tF{fjhPhmtXh`5%me%qf_!=@iU8=lhaf%~!xH6LwbdicX@!Wfg3?H1p#;D; -;kK;EkHg&bc@X4cW(QB;>&NGH+P(j_Av*PY)4jB#L_j8cwlC5FSi0$r8XbG3K-Akb6A*4aZP4y1`2b_ -(|&Mj!y>-GN;PapjN2G;vDP%sSj&Fbf#&wE~>0%==sXg%xTA7bF>X2pp~%H(kt!*Kv%RJ5Ss9Izduio+0y%~M$)W^uL)LhbW0kxV-Qn{^a4xn -GV=0tMKnj@!M)Eo-oYB#METI7NV9Bts^^bhV>7J!oTQNef{6MDbFuuK+tw<}o*8<3skh9Zwz>2+ZOOq -VdX5vcx~CepjL8Et10{CgNg)|DrN{@Gb0w?>6|KXwk}C#^e -h3;O80of<5wvRJWhC*m-hBiOicEe6m?_JJlYvT+%t^pTSCsOjI+07Q)^f*spt|HobkTPI3U0fzf1QVU -x~O)?me8;GqeSmRiktR|SeA-fF;Bh&?K;sjAydhRwozSxE1-l7qTsgN_E -458f;b*VI|ld8GHa8wcOsVA%5JdbQqvQ_NT7D<9YK?h|jdlF79qX964|S=>uyPW9%iQ1+cYJ|k#z>dbN~EBZDQg*0gM4#)s+%ZulV8kS@2(n9A1Eb;AY*Viyy4Q}8Ks3 -Y0yYpx)@CsNQAZn$|aQq(j%zG25Py62XKGq4n#5=NB-zevz*92AoNG&#JuIG(tNmzU#T&hVDB%K~0;* -#Q3W;@#oZu>!4}P_B2>>C(!YR~t0rU-gm<&3WaL4E)>TjuiZx#n1pztygQhEg&rwnzY=$Vb@H_rA+Fk -iOf)`@}RM1Bk_-a4u{nSbS!NJnW*Pt;K36j3RvwADeHuFH%FUI)ogsqT$ZE9La8ojjCL2UQbXG$DhRBhb!)0X~7hrjbsuo6GE4#B{aCfCed^FsN -qKro3swr%mgdi(;IAS%qFicTc?`H#EhMokZYE5B)y`NK5Y)OQB;I6)i)b^cn-SP~ORq?gLeOT+{`T>Y -`2BJ4tinDcMK(4yl^^?5K{bl9MC+$z^7^cT -SI`RWT)@-gDy0Q@i1B+&lDHm|L{^*(3Wij6_R$Acw&ZLdvK4QdkM0p&WyS`Ty$6qK8e&f8&sScEp_y} -&y_SgdsH%~@`ZYhL%RyP>gMUCIXw08lASmSZWsga*MUOmc?%C&iCx@Em!%E@jYh_m&DV^tR(@1a2Gr>2 -g2Q!3#1at@6c}gLGu3yAJw!Hh=N&eLW*{pl5U}4UHw<4)FYW -`uw@xrEJ}xsqSrfZ_OTR8N3kJYJ_<$Ui_pc*61pSYbfQqfWkL~;N&X3{Wo<;n-(oxTGaTR=vi61N35v -m$i5ApfrbQ~F`nRzR|4Xlq~Cs%ev2ELdGp_JSZ+j|)ZDEF)8|I^|39r(Ay7 -0F`oCHsMGSKeWg{93!gqoH@!Hz$&~oeTLI1Xrp5btE7P)1YGRrN!&`mpM7wp(VK -H=(txhXKe`6Reldmm0JW}UMk}4@+Mq5^&pn>Q?4A?rFAj)X6|c -K7rIqFY>uHn=T^C-i%$!jK|Lp}-eN1rgnxs_D3-@-Dh4c;-MY6*=9;W|o$S0y;NBvN*d;L@fTq@0Ui!fPHz~l(ygZfmT$GZHVt6ogP&G8Wl^6v7ikL?O743 -Xr(ui0Ml7)XDLT@?sJ-P_!jlRLiPyrV7rb6~0C9gxI=)(1*7|&hJq`JQtPx~}?B&j>r)mV);2Eb5^X9 -;okIG(F}r1;7ybU!P+Tq7X31gM%=$9)jyU$K00arqWAh3)*f#*FzYl6>s8aG(%Jh -|5q;w=<9)JLVYa;mIxLKuDap2MhxnMh`x@LYr%@PN@BoYrjBX}F9KQ%3_>FsH~=Aa+RZPItd;1U#JMYANN!@XzRG&DMhml8m|6dHf1rmc=TIje(b0}Wlo6By41&wLC%XpwBy%mggdytcY$0d2SQ -Cq$DX!KoR5>WX&L@gC-r_OB(K*S=zJ-*0OhvK+k3VpFr2-vNZGT|CARYGp -2T)4`1QY-O00;p4c~(;Z00002000000000o0001RX>c!Jc4cm4Z*nhna%^mAVlyvrVPk7yXJvCQVqs% -zaBp&Sb1z?CX>MtBUtcb8c>@4YO9KQH000080Q-4XQ{K2^vq%B}0Eqblf}u6E?$Ex7o~)jtK{nuv5qk0s~sv -d

ua{Ci^CingE#+3ywNu^|y6mj>~ua;MYkP<6MXIZCiCIsxGv96}j$Px>x=oGeXRiU!o6knylDWC| -}q@_|)>9`Qs)m)%ok?h>_TwCsc@&sfxYJ_h%S(Sgt|4Zf-2x)9@X5u=_;G%#h-yZGUpDq0ps<}roZk_ -zb>K5m&y9wpu7pr;cPX%WRCQ{S~P*dpa`GD26?6AxzoPozrv&_Rf#f`g$!c_q-e7=v^nz)UdXgYd*-CKieYri1yct{**@d5&J%5G=M17ZzKbM_{Bc=F6W*fJJWRrsM_txWLEcZsT4ALf; -9aWHPX%xd?z)flj-di(BMBGqc3jM5{&~LmbpPdUeR;oF%x~7W%kT43Kv4_{aWsOJ$L;Y!Xhpza3EEgg -8x2jXZ8k;#&5pjIX_aucJi7t=ewL>;2XHvXWh`>Dx@KHWEW+D6c$q>OtjF$klw?I1Nhc6Ja6b;*aLQg -{uB_$-rt|xmq0x>Y^t%Rsn?Ss?zm``k#rAs(43guQB7G$K??cw7=W)shFP5+3V&orCO9KQH000080Q- -4XQw0;FcccLT0G|T@06PEx0B~t=FJE?LZe(wAFLiQkY-wUMFK}UFYhh<)b1!pqY+r3*bYo~=Xm4|LZe -eX@FJE72ZfSI1UoLQYjZ;f+n?Ml0^D91)gKZU|L{TqMn?tKs>ZR?iN3>=KdllG0yMrnJzQf`;ekfA+0 -%CT)*9@-fISAquvrNMDltIrOehmSgk$PY4If^$Op&5KFjy+t2>VsZOl%omW`JHlldu{0q7_5abx=4!>RjOH)2MSS9D(4% --K9M<{wS6{+7khdJjCLj4GOMOk?4l%Z`aek#Bu5s#y5=3zoZbpA=>gAke5pbIFlF1h(O@Q2_3{Epiga --Z8J9KP^mQG!%v^_S)QdLz~lRb)D&kQp4^aQu_>gfbApBoDo)l0;;+9tid=k9HN&r<&L!;E62R0^31!?do9$*4@H5Z?XOqec*$$x@6aWAK -2mt$eR#QHNOV+Un001u*002S&003}la4%nWWo~3|axZmqY;0*_GcRyqV{2h&Wpgicb8KI2VRU0?UubW -0bZ%j7WiMZ8ZE$R5ZDnqBVRUJ4ZZ2?ntypbu+c*;b?q4xbP()50rD<-vJ>#-K(5AZqx=FTa_loQG3|g -XMHZmoUbRFNX-)Bg@&`J8Tcb5XGB9b%D%slf#B}wvjA!#XGzL+g)>$F(PbWG(+=T6m{N>eZCa^n_wKF -aWKLeg5Poe~wT7gE#8Dt%2?SFf`qNk*d`IQw_;xtst=0)rN|iUY=itxWK)@lXExIj{7S8#dIycB-x?0 -X@NQ>DsTngnA<`{xA+f>S>W;fd%w*ZVsFf1im5|%n -4SoOO;JP9aciDBVz`Y~YZeTt>-c$U{I?b2kg6$>MWZ9i>?Vz7IM&a4IFb`F31`|~GL5}#{TP$4xcg{8 -VgB+O@|D$O?lREDy#tdXdL1cy9A0?1w;H2_^2alJ&zK^B~lT_LeHEKedY=asSxSwte58ueg9Vp)f(#P -qHt9~T@^OLkGOIU8J?8zMSvq_DMyR1~B|4Y1h|R>8@6kI?p$NYOB94!sO5|j*SP}7f(`+Vx&b=tm=F8FgbS15IKn6*2(TjZ63 -JL&3|i+8VI()q<(5)PXh?@^z;B&b25z%KT5!R?|{h$Nh`UZ{LA>cSmGI$sDQa)(_oxiRoI>K#>LE~`) --13UN(6LsGc-7jaID+d0f80{r7H~-14YIkp!E4t_z#avX*%Bq|&Av|`kRwMV>BCf=);-WZ+>gm|O -6(rjfcxlMnySK0as!<(NPxd{-44W$w7xV@=I}oE9G)7eM -4i|h%N$jvfsummh-NAS@ISIg+}{)6qWi9ju$moD4!V*yVb}{+FUd;?c6!;Tx@0Rr;DzUPJFogkm^2^m -?;cu4+fbGjdi_V?|rYv4Eb7#T%zRuv6oE9#~M|#&m;6l@2pU8xxjT~u^`yBMzy1@^5D;vb*OQi9(}2v -eghOA=oDiI$~V?Hb1-r-z7tX+8hc(i03<)(=TK_wOg{;4$nhcu>l^2|?+yrhia+G>LeP?LZz0MN~AN;kx5Q*jy=Km0W4=%~8T}y|?Y4|X3_1Vu -kxz{P&%R+RYK0kc!S=xmZ+>wV2K_% -FU{$Xn+-07>OL^_@d6PT#zpU0l9ByEs32&FB1!H>bb7J9~5ba@ZV*)L%^gAB^UomW6iJ;`4-`4{oomx4m_^_x9DIDx6)#0^*yiRzqzw-&q~D2y?9RTX{! -t}61S7KI!0CEvxhReW2(NCcsgH16HKKBaxZ^VWWcu_q2FkV${V++W3wj5JniZiE4Qn=m@`GPnl~xUAP -b-QwT_xIDJ4>4ATI`uI2Pbu_x|S=fKn+bhp=CG%5bQ;6Os-IGEA4s+VV6g -++TH=e;o!hY&e6NUZ8R9F)*6YrqZkq5y#*s53V4!vTE#s^Bm~;ciyDd0EIdtIv9%O&p4m)BF@xa@LgJ -JoPC@B3XR7Lnq<@_Y;5mRw~#R0>pfm#y@;W%xDv|r@3ypW+`aqG -_2^#z>E^a@>1lfurm;shp+}SsUzvEO599JuG%4TCCq3%%IKHOy@Ah^V!-jjmje85~HnvCR4x&!AikX_&0RRAl1ON -ae0001RX>c!Jc4cm4Z*nhna%^mAVlyvwbZKlaUtei%X>?y-E^v8mlfiD=Fbsz8ehMMGY=F@>=poBIG- -!|*c6!)}P;9ypVp|#|C+pi!jzvdl>XZ2W6eazn8`7NsXa+YB0tnR^O-{&z)$QOArZ`EyiQk&UK~|@Wq -}qx~cSbsOP_1$wsW7C^s>ZP03U`!F3>ItQv^bzRBH>fgjE6l{y6>@aO80!4vT%b?lQstHkWKh^KvxT*z-(>E7W#H^tIgBnOS^-;oTdK5+ -je-JTJuQS}bldpzFy><#d4PQnN-Bn?rjWZ{dZM!z2NaZR=<7Ias|2zAmOmEMjLP_Q_jTZeB8p{bqLGP -Nvrp;2@a7p?^FtAKSx9>>sl)r#uqpp=8Dmb3FUZARcyR52Nu}h=zlusB1I2pBTn>9ejY-KF044wc0B~t=FJE?LZe(wAFLiQkY-wUMFLiWjY%gP -PZf<2`bZKvHE^v9BSlezRHxhlhhK2*A-o>MV%Krt}1Qv#z-la-{ -t&E$!jOHvPsluT{2pDV#e}2W3-9a^Sso`8L>i -?wt7CEO=%p1rAF;S}gtvC^h6oK>UIsNyRSwm<_EFUh*1G8(m487)m_02lBinZqmFZ*9#>u@dD7@Mk<8 -`_JDvMhv~2n}&l}f!lEC#i4MB5`n1->_#`Tf+e1j+VUzNc_Fi!8MgKJW`4mhmXcXJTWp-G^HK_~T>i^ -^{`9evz5u=a3;&E#9<<{#K$oG8CR>o=mBVda-}!h>(LA(hFQfz}_pG&Gy#26Pd}7k_#R8o6!X$Uzmm~ -@{XullzW_a16U1|-^FlEsPG*PyI$Dy(LAWQ@p_yB9}m~+NQcM4-#9Z$ilp7?VZL4Opj^sDDHFza3F!W -A9yw~j?UXcZ*&u|xj}18qzMWVR_cz$`3Vpok-iC;72(O0r*brtK4TaRLI8%$5@4z268FyWxhzmNTjbc -ZoVUR|yx#ItCEU$OFup*mB*t4{!1}AxxpXzK-<&Gi;;L1v_^-mAasZMQc9p6HwxmA}A5x(Q$ZgQVV;o9 -7@M2YD@9_-qbIK -+*~ELBrPFu8dZFJF0TiHY=nc@aZ$zxL%j+n-zJ4_wQ4jGSWLEZN&Q2Ia-;>0#JmNT!2 -{eq0O)Jp^u=9M+lbz0F1FEF@ -XaL<*0}6UEYAj_EPZ?uFZA<3Gleo*KRK59yE*HSYg8jvQtTZV|qNmJ4|4P_ttanbcDblTz<_RjipPR% -|O|n@tk?(BQzoc=xEYpk+d>H1#eUMK%I9@U2?OvN#AZw}G-+M;DUZzcDr=kA4Zy2$qcAGp(n --idoKmtYdoG?PqnuFeI$%P~`ND6*XFwZzQI_)q%eptG?;@MbKij4^LLkh6&3H3TM`B -)6eNlmNnzcZ|vn1BwKO`RAMbf!D$SuR_if*V7SHq -F?rwiQfDc=R1Fo&Hm)1r*J%y4f3^}p~?lFuZ>gz7VF=JrbAd&}%qUP|ColzBiN$lbbgW -UycNnY4*r_TLF4Q_7h$--5F(moe<%f$$+NotlnnFHNIw}8Jml?X8+|E74c`;f1oE)ne($`ny5GQ5Bla -rGm=P?GEWy8$`O+3?k8NnRg;)F(GJwCJ`um(RhI!t;L7(eZuH`@<;+2rizfQ(QiX*w&>R}*cAqv7kJ+ -1dDzjXD-kv7Dr^4huNEKE4izgo9Z!C;==Dv`3ZRS0Emw3XJDxgX$mz&?jzYSNJR0agK?)q2pvGtT;_@ -VV?7BdP2yd&-P)UQJALSAUlmnx1Wlahv%oek71tc8W2qwUV@(vYHs`!gn6pAoEe;{SCN|oW79<9$vp9 -F*StEe?N#1C+|5%^f4O}w?w@|Y>7#V!La=dk0NrpX<#5AnA?n?YXShCW=iY{K=0p;UeO*NdLJ{Rh52l -XEk_udlvpTI_&_oYmr0?cfns=yIlUvaV>jT6}*I=Go5{AM2yU~utQ7cMQ&31`BQm?vue#rLLazEn`O~ -GF@$6y*z6cLh7EDdS+Vo9mL2>p>7I!ipMIL9ZXwX#hO^_g@3+Oo6j>oqpyW{I4!y^jAy=stzD4Y${C- -@m^}c{}|lL5>gke$k@COZ_FAoHWFP@QLJSp2BqjOa*&CFZMc6$AUnUbJ{wRnuCLvQ|jnCnK$>zlW4&)ixzYm3i5ay!> -%9WrwB{Xf2Y_rw3#XoqEZbEa$f-V%p}Hc!Jc4cm4Z*nhna%^mAVlyvwbZ -KlaaB^>Wc`k5yeN;`4n=lZ)^D9R5utY*!dRr-{sNGhova8+nj2sw(C1TUDNjCq!W8;AHwOoMle7yH&# -trE`6vlKV$bGNLLPaPnmIHO2+4p>%5wj}&#W|Ip)AQ`Duk|Z@Yz)=b-%LeB@%ck>fi5Z2T}$$G$6N>DI70x -T;MZpJ2CLWg`p0U+ex!$8>-~N8BJug2dqH9kf8YPiAjgXCac14bK~c<`v#dOa3qo_a_khtlkmm-CYeT -Pfw--w6kc$2kCpDJtNHM3}FqF}D1{Qf2kjVnU$XC8UmC` -gQq_q4$H%8P8cA*`u_rhxXz5NIx!hu*7I52-#F-5}>$(V$#XSC5s2*;X%O~moqT;>37HYh{f0BUb*>n -lxEWHwqYU6?aFyu$CV#JxMdDYKN5_#JBMidE8*_~6IM0+mbIj7i?HJE+PwE8K{ov!fEOCIk> -pX^w?f81-afi>RE_LDpEq_T;ABCu5>iT%?bf!1BSpHuo^1xx90+WzqK{pQ0u`mN)lfHhl&EMRGeiZrR -R4UQ9%E;3R!~#^PQavQENU$|CkTq@5OZ88jZNrHku%iXw_z|17`{;}bb2{{c`-0|XQR000O8`*~JVP6 -PIz`~Uy|@&Nzc!Jc4cm4Z*nhna%^mAVlyvwbZKlaa%FLKWpi{caCxOy>u=LY5dZGKVq}Dph -{S*np;M$HS_nsV6|Ju2zMPP=u_w(^dpGQ^n+C-HelzR0o5DS!s>yoiJ->N)ZFCNCye?c}FpfdyTWuVO -mDO3{FfPwQux7fIu=w&tO|yFW0#%|@tZt(S?-JZPsgTeLy&2rh)RAm|TnMOGl}q}xaZ%jE_|ipDON;5679xg}!ErE^kUgVj00`>U%9sK~%=1bJ -oqHsDFP9`GD_7SmOZ4k7|_V*cVv1RL+iB8Mqal($y7VJLhS}v!=zFfZ7*3oYeDy7p$i)MCwjf -$Q{!R7C#7xDYW@!Q28a0KD&*RMNI=oEU2i{q2CMfLEK=qOTOQCW23zd}(e#Y$;7g>XX(`MY3Mf>ni|C -P8rg=FQ^bDqehebN=@9!*8tb`m5YdQ(R}lVHh~Gp9esnI_gHdOmd9lLXogE&UIR37*`kY2$cRJ=J4*t -v*$sXW;pe{KBJ|uE#)b|%wl;QTJeO;n66A11o6A$)3e3ftHs$F*``;YF>zzEJ0Jrg7dIGJ85ny(H;CI -oQ1 -VEK>E-Kk35543bd|0Xw0{En6fT65lX~+r8UpH7iV($<;4UpG7ajp2jxw -otWv2SS&IH7d&fHBF8zSd#6oWh0Z5^#Jt+bucFM97f78g8a48m;$YT>;QOy8csaBGVMq_^KL#aJ%m|v -!=xk0cVdkF3v?j3f9~UdZ*sg-LhPN@bl;2@VaJa(X8%F@Jn(tq+*}grBQ?AYeelBV -igwT)0E_Nk@;xpGg@s`G&d*$Nj1GiI@grf;wJ@IzS_+JL5=3OWUv?YOM`>iyqG&w)hDf0f&mQb);Kir -Hm6_Wg$FiccJlW+)9~Dq7?npuyE_yuLS2>)x*`_C_GTu53B8^X*C{-Z$**Nwg03qk%{fg3vy+zE$PPy -|YJ_u`(}qH{hMc8_*^$dlvs`x>Ls8EHPK>s!T4@J7_1id*u{ZdDBb_a;2M@Zfpm9H>{6D^rjN@)Xy<#ThqksgB6H+w^E={>GG -8abDGVhlTPI|qT8B8U?g7!Av)5$0!aj>MZ^ft8bC?2y+-e9$=mH@2Y2`m8|Z~E+nOg5*La8ZMg-&2U3 -zsY0KjiY(NdI+p*;6&G1=k!}gtr;Y(?zDwJwowR=J@6aWAK2mt$eR#P@8)w -Ey*006cP001Na003}la4%nWWo~3|axZmqY;0*_GcR>?X>2cYWpi+EZgXWWaCxm(O>g5i5WV|X5Z*&29{kA={vvE(#QdLrWuzjYN7$%3h=BfA1TTl4U2zqCj=AC2~Ia=FJ<)2Ke3u$*BjsL) -Z<6?50W%MJgZ%7to@1=E0udX>}w|6b`C%Gb*IrTMb%!YcK%KP;quN7$=!G+gaEk~nKL8VY`QSz -#BR7}kBujzaw@Qlaf@cXW!{K<)JMZO{{q*$p9X;G@0PHXi<0w%~9ZbbBvje^}AhnEenMt}S~RP@}?8< -BI2tPdOhc)QyhzKLmwN99tY(?@u+4lpV$a_LCi?|fzSg(wU;ed2{9FCOFW$86x~=NobBTub83_uswkz -gt43HRqJ=V}#XM0&(TNRZG-9V7^anEuDKMqt -mrA3ar9n-#3AKG*no9bCyA^qd3~K`1g)kaD -+XIZy#&t&|Ymdl!$J_UKQ4UW^ZG)75$98VL|;f$)1k*`09>me--u)cwg5d?G%;VkXRDBz_o -c!&XsLl?*uz!`m_14jsQHe^+~>+A!fPPDd$7F-OJCVMV1`-4oa^;6Yf*_zZXrQ$7Qs>ka -de-UR5U=U}gpp|vyLCr2ct1HO)Q;f7(ub`jD8Tf=MRfaPa2NlBxZN4DwA?XZK*PAew{$<9b0+s76Hm)i -hpDP?DV{TQ6rel4Fm{11#l`5^=d6X9g~(xf%m9o>{{m1;0|XQR000O8`*~JVmFNkb&=vpyk5d2uApig -XaA|NaUv_0~WN&gWb#iQMX<{=kb#!TLFLGsca(OOrdF4E7bKAyt-}x(c=w<{a6imrU({L`EC=wlOt{r -(K`lcP%1wnEtq6PsL4^fK8{p~&H?0W%FPDb}bZ}~w2f!(uv9(&&4y2!UIiq>^i7d(nsvfbrH#o|Sor* -*|6@x9lR_xUzXQuQUxZ*P4rxD;}@7y!9r&zvfl(V2yfPYhh^?J|PMO?+KtcrS -xjA84%0Q&9*(iW1u~|J7s&$mT?A~+2(f~Hk -p)6pu$rYXDh;GnZ{+w_DQv2E5sGR@fFJjr^8-!!PhaOHDUSUzhK1xml2a9;^i9v5>yX6J{S{9KkbTq2 -h#LF-&TChE~YQf-o2hhZ>FhIkR^+eD^!CDQ+h77V-K9Wg-ALIP3j -yKweNnz99#Bv~P|?oPu|$ljIQ2Y>eZ_0@0F3%SeNL@}h0-@q1dHel7-3 -9+4TPL@5K|2cti>L8&Bdi>YfsxxCI2}lwWx;`XU;#6}7eJ-m5GJPHt%$a?H(+O(6ErZa4`ixx#7OTLn;A)o|Ix*P%m12zmeRKh|lX`s+r$7}~q%y`_hrI%(-B!)}jtxUcf!lQUKImQ3NmQKDiMBY@@ -4(7ZCXm8>yMpr>Xn#lk50TGS}jK^uA^a_HbU#7F8Av?mG-;Z?yvA05jXEC7! -S$O@uW6v>L0gvOaxg@MuR~+sgp8Zq(JM%1KWY1;E9^gU&d+4=kurA1p$;hAcw -AG>a6XHc()U0ZA`K{p8pRxMrpEmVfmz?<66#Hou#}id$tI~g+XVC-~_doamk6W%eW@=G*G>TB2J7fj( -2epgU6hKlbly^YIM;Izp|{}E_fl_L$Pp`vp7xjeaL3uRhJA;IbZQ?$%)l0ht&%>Wp!4Sp`qvv#}$Cuz -vdYb8?S9o4kQ^|)fo4Inra>6UcvWeuyKNDG)5=nkpR1f_yVUP71l{nR?2^E&XAnPFOrtDI{4afX6JLF -RFr3tK2&l-lX&tZ8uGo$_iP(y59$eU08U0q`hp9YAQF&H$*j%IHsGNXd*v{}FW?-U8Npk1!lVa`fW+- ->4%({Z#hnUK@FN5Ryl0TMddt}c6it@HtRlgv0YKwqF$+O~Td@Rbvse1#)JBOR#-cCXBo}I{7FuD{=g@ -@)N;_~!-w` -JUzG0KORg%hOQg?{pLeCSN373lJ^~@urB8Y)Qqpg~(rsq@Wxa{5>cp?vQR9S4)_w!OZ94X#|X1@)QDN -z2&EqCsL4Kgs@P~jq`M6-L_Q^WdiU!>I?>`N|S^L>dsP!ObWT{z{y8F^&O27m}}F5z0+or>izA^z(yq)rL4Q&&Y|DQY6=umIdVl%{Nejj7 -bhm)^OnFb_Z4oxEgE$S_1Bp$uyDfAy&s=mn@j(b^ayABMy%^6A2IDQKEwu5kGGWewXBR3DhCxYZOSBDL)2_33^39u1czaA+f*#RhQtHdqEpUWGCV2KZhd{ -lCFTQT|#`tGqwyuDTD-+2L~5GNQkWneF-5TDicVOVql4TuyGXKMC^O*9Be6jGz7@uiSB%^fgzG&qBKs -nYE{z@DpXr;i6cl*<@J+9DG$`8+B(evVe~lj^*UO|%PKEyg2^XLJRvFCp=Da|YR`G5!p724YbA$M0r( -UT8cwjyfgHBq!pl4*R&GI5ka!650qwD#P20|EP{bO}3LMD?I0Q0s5Q0&robQlRzL0)$H~*P^&4N?*HT -?Z#+((W9fha#_gnZIh -T#WIP`MLWQO4pKNBoq@1|Kcmn)>10h+3(Hg27ga>SPYY0q;mP}H`zLVArPsPCy7VX9TJ%9z;9NNBgj@S)&lxbY -R(Tp=H2IOr{76ips7(wXTW<%hRnHZB0XzbpEB0wp5qA|ehJkJ6R(gw=;LNl+yEKoo+JzaFE9KklC0x? -DxM12uv-e@HVSVPg|)Kd_q3*kz#C8%GpXY-JN -o8Hy#cU#2ts_x2tAyO_S7&Zc;E^>%i7_3I5@&aThSr~MB!dC=IVYu)|5HqW3)pLKBB+3ovpPasJA_Vk -A#`)>5&Lvv}1k?yM965$c+U}o6bN&l<{S{KRhvWjT-0(v&t)?2OkF*Fi9wX6jIJ2@TobQ(X3(4~nY!; -q4vYopG{W=>8<=8j6pqKh%KWGG%WZpkp#y)F3Q-K3z+yXOhlL{&C@P==)*0M~*~EF7Al_N=IR%X7I|_ -o-C24RpJdl@GjABwA2Z_2CQcF%Olhkgl;F>bu|`w1MR^fsyh@tLRTnG73vh@Yr#9ye1<18%7^w`h&&@ -{K%ru0wjPPd21ZClyvmGBOik!5Y}5^4CLMW)thjr+)b;Hyj+LG -yb)2KsR$YinU*l(xpnx`p{Rj-4yc%%)s;ciiD#05|dMfw%1z^cwd%rfA-HciiC{-BYBU^+LGvKAzo{g -oxVp&gPuDu{zS(20eKJ*e1F_qb${}dkx1aSddmtQ!<-yf?i|lEX%W}q;lnp+7ut?lHiF(wcZTw8Y=QO -Z+3CDIB6dEUPH*XvGcfeU4nMk$4$TZo!2`QZ?jUjk2}VK0kRA>EeiLJ2TRa12-8!Vta58!g;vr1PxXA -TVF9m4Mx}y11wt@&YWWQmtf0?Yf1!O__Xskucz%;K58TevV5<{>Scfjd$5`d^Qw8qN7p3{1=xj=ggEn -rAI_ep7QNMhdb5khc3TKz~^wC{uN6J=l|kdizU51RG7;W)McgMq~5C_-%zMNQ}N5yuKYPv%?^uqSh{@ -be{3)}V1msL1zCdVNryNSD3C4{z9V8>Y{%PLQ6$4n+e{0J_`?U?}^Ar6khzgW&70#b|jVex}KHhx8aH}>f4}yo@aNUbGRWJRf@*!tAZW>EBx(SY_e8s2!i#@I06ufMaraMCPGMx!_k+(r8 -%iE@IWi?V^LyNH9HaEA!HCtD)W|n3CrE-(ru}-a>3ZR -t`)d$eI_4c~J_cV#46BtE2GPHno=S(GSviyI95Se$4K*Q=(hLm4G7ofkR+V0^1-DT+Ly=4Qxs5*y~>= -?4(r`M~29XZK7~EY4xUceWha^QC#uX*e034#7amF-Q_G-5>yI!DwYEv9ZTzuB~QS2)R*NhUV?mJ^IAE -6{+wQs2oqj`>sH)8-&EW5d9hx8fBItdtduQFFT&ICTUSqn)Z-8o{r~)r^6U4b;~zeJ0~i0)?^+x`vVH -{gLM3O9)5Dr7S`0%C8;Pv`M7%r_0zqNpL;~^j_jZta%O0Fo9Y-rtR>pbNBk0SoEtzxB24LL3Vt-`u_d -k5l>my^{K7b*}*lA-g&)Avh*LpWQM?hIlNz30+%~GcXxPZwzu~OA$=2a%)o`&BKnN27uZDIqm%T$8}L -4&ZT1zw_ZL2EYh_mW7DQFX&?KjI@?s>1HsvISf$R`=G$23S1hsg<04L>wB#9Rqd0VZmrPfPamrC!_I) -UWbTz!lkhYd?*w=W7P}%rzfvM_*JyTs!g$^`&sx=l~3~#W>NxLtWZ -1WD%w8i3S`}0#O_a3rc_BWmU}FE@uLbw)lYxrL7)6S)9vOTu$;F8l^6984*ny9W2l1 -Hp(5-zWTIjwtaJ3RdRBRCFNQLiv1gdUf?c8x7oGq)tV9!QvwAZ&R6yzMAypIqSq~?a&4(zhMZlZVl=| -C7{BM5~PsvsH5J}-zo=W|P}unerq++Gd`-96FmKvAFUbnh08YCoz3OZ4t0|L4` -1p8xaICCr7vxK7|+j79?Lbeq)@GRP7||z%ppO)at$oJMeIpK|JvWdjllq;y=1iAPKtC25#IWGH5TU!C?>5dWWZ8=o^_!!52ZWFGT1f@MmBt? -E2Ve-G!fLI=6)hG>pbjiBVFF-iN4lvl+FAEQc-6Yc>TyJjPTn-*kq!u* -Nurjro_tRku0v|j4F)PV*xApEAID?xZW`YLPZuc^;|vLCLzRce?vEkr3frv6*Oi=8cUjsd?77{BsSoX -sEcjXFb7}WTK7k@gZ&l=`x_1{B2~L5irN)lo(rcJ=nh*6GMnjz@u5t@&;Wp@#=R{=I6x_Zvm`Q1@zHO -z=SZ+Flyp~hhE-or>Yrl=g_-C#LQq#n;qMwPB4R#&ji3OR3p<;i7R-KcqWy3k1*fyQn(3TBJX*d;)S| -WK;MmDsH=F$~{TA>|rmbG2WBf#`B;JkFEwLJ1=CP9oLSfp+L3#jtt5wRG1}wL4U->#EYV)#nCX`=l5wp%UfW+2)|hbhs)cMkwc{h%Evfx_YOI&xj{ru -^wSphg^Kkv+Hp-jcNaGxgF-vSM)OZh7PeD&h{V>Vg-|1ApSW2XX4a=*R##-A|9op}b;fHcT`i%kA@En -)Q-(R88K*w}lr>XrK=U5<#q(Z{W=n>*ZL9b4y-)ckAogb({1Rl=bp0Nw-9b0NDl~Kj9$Q9zIX`Y*}5) -biJ^@7ti{iaAjBL_n)&sZIr`hSDG -%j#kN1bkWhw8pD3dn3LuIUBqew}>;r#kCJS#D1UGwZC<&R(0HE|Y0~3-(rc*^W1cZXHOIWZ2D7f5IVoD$=OEbkl`KbP>hJTDl6e1jHN$OG5K4%4r8hzxQR?aqFp4_uY!-rL4*nAUdE9>c|TH+0jNnkCi3f#~WPyHs -|PdS1EMZobwTLMh&9@)6&h`EE1&+`ugg+$)Psx`! -6J06C4eCkgbNSa&&{e-Fv=yJ3YUAsWu&o1_0Y%o_Kq0wqHEXQPaZbbo71Wxxwy>ErAM9!S~RLKsvBZF -&wnEH>nD{UK-o%nnrPQpna~pbM8RX+~IyXi&bq6b1l7cs=ut!TISYQG`gV^ulR8wsa)snvVh!Mo7AY% -9Z3mDe(V?jtd@vN%Z(LIE|w1i^tg|{Dvt(Z(G@}9^(F@mw_iW9-?P9Uri&s!=xPlq`=7ACME%s==Rh# -`lkt{54>eyu*$xlf_kIA0IQ&UFKA)}TkWY@s^H~X{yf40d9vFB7b*f-<6Vv=NB#ET1Jv#*&ffB_xswB -!odJoJ(95d7SX4quXlE?Garbta*TR)~^=it&LE*o8J`1wcA_t_Q2E;SE0v<9q_JwaJxj*f5@oE%7289 -PQ%5Icj>$)c{rF**oSLK9%{Hx4nWyiomH%c!Jc4cm4Z*nhna%^mAVlyvwbZKlab8~E8E^v9xJZp2? -$dTXqD<)Lcnao0vC3&43?{rsOie9rKOFB`$Ehm)&hrp1W2m}}a6vb>dzx}#<9+&|^*;|(nr&3vX^z`) -f^t%UM6#1G((PCR|3lT+3t~Ys6F+MNzbX$o?-FG_r`D)E$bDfvws@&Vl9WOGOEz8aV5zl4D%UDW-b|q -d?x!4D6o9Rmhx8mh0m2(5OPQDLVx#H*a@5T4;I|?ie#Y-$UmCUo!!1g)}ekNHdVpzd_%Bw|QtbbBu1o -ZO0$Y1u;cqP{2b5?FQI8G$$-Sl?yNtPwRdPAnun{iR(MbPP+sW&`+;!9yCF6E1UwJlQf@P~)tXnZrCP -NT{Ff4`50*Vhk!j2^}TgWGp-(dhu@QJz)erD9j?HqQh;c`SHV5(53~<8T@QX>j|2r={pjrqTVEyZiAi -0J{&Tt!ang$$*PrLsmPYqiscK -&St4P56{~W%-sHfQc`9gc*kYT-$ac0V@?;wWpJ1>o^#~;Q2x}YSgU$@qo?y1KN#lw@ZIV3Y1m5T+du0;o`4bo|=Woj1gX*!9wI(gd -H0C2un3X)>~NQT;R}ZzY(mginIB?5@krB4+3-n1njZc3MLoCB|DT?@hV_EOBAD74sVN{EJf&1T8MI+; -%^?L2QN4Iy;=8r -izlgXh&*ormPB#Symjncz$t)2s;i_7v-tg5v6HnnA`$51Pxr8v)18=~mNb8P3iF(5;?I -b9bQnvKi2_`U1b+?gMB=(&f=wY7qR?w)*B{4c;}QJZ4_ -rj_h~+>B!wcl^TvV0Dge=Zt;f>(go(;#-(;r8l$QO-P9DXm}DkSFZ=!ek}WH<=$F}2IE4<2@dnUGo3a -}o1xiSa;@z_P?Ck7JPne;}5^A;2E+yz6zIDP)N(ImQpSpKop$--4gaU>itU0`}?pd-iEKDg$;sohpAy -o|K4^I+!!kM2j$#f@|X?xTTOLxxITF4@mOP!^_5wV59)6g_tdY-6`_zaz!x??h$P#>}s*#v7kw_yaIC -uQ4c`R_iUu~tG5@`Z~8i>uJJ;h6!_PO5(P(o2ar}Vht~=<4f5~O00@g -oSJK3e}{|-0^Jnsh0=cA89_`P`RzG;U)290=H!H>*|j}HOeClAwM%UJ!?ee@fSyYD>_2Twp2ynB!ceg -Ef9{}s#aMABd>Kjn#lzy}_-`+GtWr{JTVfxUc=7N!3chkaqGqqsGanXICySBi8IU_@Nyah^tKM?kopI -E|5prHjzOfEYBvJ>V1exwJNyJ@a&n_7>O8QSMWR^utLY!HJ)uRbEyZUkjTG9KgZEka$IFU>E=aLqm$f -Ztrf#El7D3ivl8FotPW|*9Of3cMrrLAEO1g{4rnhp;N{9>TV|CIBjaA*iUl -o2BrO}(DpOz4mT#*trCN@Qm5HETMtmqCQ0^bVPKq)(8I9U4$2s7Xr_}yplNG0nY;@TEzD(9EV3P3)#k -W27`kbh&U#W!G1HiZNqobJgnnA^VbVVRXi}n`OblJWwSd6Fb>;i?-FpVNgkJ>%+Ex2b{<~54`0rB@{BP|a=I_ro*?&Fv{6*2NoLCA*?pl*>s1 -!df4e`ry4=(hHZcdmaJ>4va=~W=1&$s)jRgc>pVy8H6@6(IvY9`}yrJ-B4Kwk0$LLPS{TZtPt=-WHAV -IuMfNftAc64W`B9{Z)}$ruRYM->&N7vpZR6aO+7)qim(jERjU8AFAg)4XWADS}p -52c!EP}`bT-@DbpK=fmn1+XhIU{kd0p;UPb5HMLq^>39q_8p94$Z7^WWRv_nW>s`4(W{Uc`3C}$za&P -Uen^gx7xE^eK)(a8q|sT2?Nd%MQmOF0&}E@W)$wE^3$WV|xkfb}#?-Evjag5i!au@&jFwE)+XDE~;x4 -H*KMy=|&Xvezn_CNDR@kYq5kF|VmS&J^gx7!GdKRFcmILV#2x1?j>`D8kNFZP=EZ!L!1>G6+Bc%`?-q -FpyPHb;jp&aQ2d1p_;Te&Dmc2U6pz4*~}7#$#4ap5GR`*p;=aMAg&M+6KFQW5M~DQQPiv03B3ffo)n$ -yUadrJf|7Z&l6oIh+z7#iyCLpI_;qgz7PTPYOn$7WeS)_Pt3tArG&>kDiz2`k_=Rt$Yz^!Um;*~ewS| -==rH4cu!P6zGy6n3eW4}o_7PZ|fyM=6Mh2vh2>P0G_#GxAfdS)KhZvP+f(h~K5<7W(FT8T(UglJzWX{ -R9$k4|?S1!M$R?F8XPmNYRb-=@1{R;G?5yf(=!by8w#IPErUDHR -avjV9c??LmkTw<485VVnT|uj~ljRCEUKPF!eq*nad@!g}Ei%Cyxd~xovLr7;6UuKZnL3@I$c~6@Clnu -tNA|nlgttY9f4O4+ppkBso}j;23Tw{S%W!zZdgXRrR1;;GXESZ(S;`?WNM# -15b)G+qz8^n2(phT@^&mwF7B#VDAZC&%AP%xNoe=IMh@~k5X$Ocu&7O*~(*&If@cWnu5_K!NTw&y5r= -jxnJg-(37qLZcJcIUT72}(n_i1RJ^55u?JUt60%s9URSk$!*qyG;Y!)=8b_5DudJjDTQM%^7R_&AqwYD!(vx+>d-$H<( -D215a-5SiOMOr5s%2GOrM(DGPEvfTi!C?nMfsQIL-_b15sId7xXqFU>^EvU`b+k{QGcm6k{ipM{ZV>D -UK88H*?Umn@tC!#bwYL6DbfS%gQ^0k`_C2uBEhU*E5m3}9%-a+LjANyaHPoVlSw?{7et_A9xRg0~o;fgQClfqplYvf>RAEOc1s%e+6bhZfM0=RU#eQRtC@HKJrM!|HxGbMApwDA -T+DJ++@kk3*7W53xSbc~^n-pDy1kb=LmE)9pa=d#Idn`!i8&Lokjzzqh3g+{WY$Y6iBN4vO -uaCC}_`S7eVA)MYb-rDe;YQJf&3A&Yp@D*)W4PKZ7h3Xt8R|7gSk7htam9MDPrnP; -yS{3WnjD}Ru0cY88z0$?k8p3gLEZD|xAzu|T(yh_`-kVVlzd^Xpxe!n-v#SA2@kH*%Ule-HJSckO`%6 -zT?zT>A)gCxGAd$$@xU-E-(}tw2Ovywx=m337H*|S8wpWl0QW2!2o0SO;!JbfZRHEYsY@yBgU)97z-SE4)ej$N$6UqtOxmJ5*@CqB1x -k#p{pZsts(&3iBeS%8>F^8a!M!aneH|@k6OE>?bR5C!l}8`aGLzXsiH&{&QqW~!jdC+yrd1-^wHJhDl -Ne;Mes+pY=cgt;O19)^brwtvdCLi;1a1zDK&kR1K5IevrL2sfzZf<9m&8@;L`zhMkqgY%SVlJflq>$Q -D=CD6$QLLxlVHnQ}(y!+|;8R;e=C?(5tHP57GRdlBFEb;h4d!wn;P7aC4dNgfJV_G -1LJ56o45A%vd&i$hD6o+VGfhf{Gw7Nj<0jUCAwDin&i)_cUFH$p}Q1XRUlwT65 -3i8eho5qN+IZt^~-}a`~>bRh_NZyn8U0weM1qOu!yHAO>O)baSB2+JVcn~;2SZ@bFgi7%hw$DfTRq&; -}|rSkRzsvP2z1GhIk576=Dr4nJv9!k+S|o)CpX@%p+A+$Amkz^ty1a|JFTEdsYdPFvmi=x1Lye$I=~| -j_kQT?%-k%JlwUH)%LtMxl`Zmu)8T;^ik}Z9SgtHD)f>>s$6x{*mPp@VW>bv+loD%)9AbM0jotJL@*% -6)|3d+bgXv*!~`OhpJbNgyOQ-x*;GX^SnfZJz78ku*(%l$;%dX+4otkZokXBGu0M#KV(`we)&%_^(0|cob>kJ -xyy3bg??h(@hqc4uL1VE(M2>J^$-6ok}PL1gki~3=937NYvR|s_%(~0Q>a}CKC+dXcACUI^FTT?z$aL -<2eu`;1d#OE!&N-iL!uoQ -2oCxi&xm|oRrG`-iV0E$QJh_x^qRk7MP!yNU&7}mV9>W_HL_gXk@lki#~UpaBv9cJv~+7x&jn7(b+PvDm3^#9jtNdqKC{t< -}fU!c^XKZO?OYnD3rumT{;D_@SD5VBb*JK=0xW|?fB+D@#yBO$2O0AtE+_5$dpk8zrBQkAE-vuNS5*Q -=F}G_%AD)UWgTlC9u{uXl3G)IoBmZIj?UY6Q-6a-=l-63v4+Lwz1L(WOIy~W&alx>b<)Y0%;$G#7~t= -FV?0BhHJ2uoD81LBR3-I4qMM#vmyLwBD(p-H_R-9V9rzaxk%92qnFH@z+oR?@c>SAO(}9#P;r*8j-H) ->8VCwX~dGHRL%JnT9o`PPBR3GldFnu^3O(s+@1j(PLPAu(MerwYbLB|KX^u2r6+q8L}r#<}vcu0r#>b -*j)ANgp!32M&X9gl}1Cm?dRU*fmj82udNg1Ukk8Wz%y<`C;yY!E6GT?(QX&L^e#?J^#H0JpU8qlOc9l -jlzaabM)|=_B=+-v#5_(ZiSf$MLlX4U~25n=Yb5v@IVOC`Q&5fBit0uN7NfQ7f;a^O{OK-({dQwj`Qb -%G_MD$pQ79_HO;I -D@I^Hjdd{nZ!(1-5HntyxKefd^Qv7mko(V>q$H~8Hl-q3aH{k7e{ufY70|XQR000O8`*~JV`M_-R4F~ -`L6B_^kC;$KeaA|NaUv_0~WN&gWb#iQMX<{=kb#!TLFLQHjbaG*Cb8v5RbS`jt%~@@4A{Qg+;;|GhIqN+vBS8}!4q{*blBnTPZKqLsT!8^@#`N -&n62;J!<9w)A4d#6;T@#gN`+s8=x -7XYu;2#EcG@}=7w&2d@!|f4=}H(<#b&KJ>C5jds#xqmIK84LE3szzo|kgPoyZ|La?rzxXP2+F$z5r4s -qmjkbTsu&iXDZY2QDpIejsE}W0bQpdM{V-J3?dyS8p6!3bf9uMFbJZ6p0xcQ)mUoG~yq%G~Q?u?7rpB -8zaIF7g{=agAtT3rGC?bd20kyCFUZhteMh|ZMo&nnb-_7Hg3Sikx2p*#C>-E=XdFI{1`OTfQ=>{YLe$ -m?R;TsL68H9^3e!Lwosx@wT -&BHLlp~oTwAPeLkW}h0;BkuRE6Fl?;IJjKZ>0;$IF|$$uyAdAhVhTzT -+@2G06aYrJ+9(1|sGlhUec=5%j~M8(qSRJQBK);1h0-s82dig+Fk*$MTZN0@X#V!%-48Ritk-K{D4GZ -Ga`Fk9fdGKD;YzTJeFuA8@{OHeM7U5F=^^{ooL!{##MhEP@v;_I#%MDcV?zH^7#&!!3R5G-R-&L|lqE -J=x_Uf)rRZlOLjiC)$nABSF||B8VMBiZaw?E0ksff5mxxB2iI)m{+?lKJ6AtHS`$^-Q=1`Q_!V>lYZu -DBO9m#s0tjG5j20+jxm=Wd({!7^{+#Ff0fD(!o=c0XrV-&P_EN*)#4il7b%D83tYes*wf?GEkUykWHs -+r^CR7k!H&}8F=Chd~YvY;1v8QuquZr9vn&7({O@lQ~HCB7g3HtjHYz;EVkpBnvtWbq`~pmiyLnt!2M -9*lIti`q%m2IQpN4r(gauk*&d($sLx8WSW(fnze$K -{RW*M?2}s6JzmZ_6gErepRsqbp|9s54`r_@M-@!_Y&DC}O;=;ncLJ47LOmIk2|Guqg>Jb;AQx=M+n+a -21@RR^7Y!fzaVJu};7{H-abrh9ry0!=1zAhjDymJ1u2?|AcwW$9`Zr8{z^j=A02QmI2_~ZVN&6drSZ6 -c+S2ZV0?dGJYpuPv!AZ6KYY4fhWudg0*-0=H>=AAMWV$Hi@XuXIdt5$9LSqF~TL2}lX7Do>)fUYDW!^xTjAbFYkj4Pwd=TTbt2U45PDW9mdv=j2Yn?ir$EkHu*qxFTXtmV{Z1^YSJa -5yy+>N)%A?b5A67$2v)W=n%(9>6X-PaYM_Y8z>nsCnsVjuu-L>m(KJS;_>m8N<9wZqSGuy2GvDr>?DC -Wzj}DMd&sZuuBn<2zOU$ME?(#hd>H@~v-UxjkiPKZE3)9Z(lCzKs(^)_n~hD!&G6!Z-ca9scgK(i`j< -Nhm?fKc9N)DsXHcut?3N{Od$zhRS9B?q)b^!QP!1l^z<2c3jhEUCjbB=0001RX>c!Jc4cm -4Z*nhna%^mAVlyvwbZKlabZKp6Z*_DoaCy~QTXWmE6@K@xz}VxlG{eyDrs=lqy0dALO*50-WG2q`QG! -TFLPHTO0a{TX`rCWX!G(ZG#ZKDJw0cM+5;!=Q?_A+^r*>?ySofyy#A3nZu2r33e5F<08?msz&(7R)BX -{EL?1J5G1*^oG_qAbKm7j!trFxUGUW;qplEHtFa#?e&y?8Tw|)XY|^=)2!qTTag_>6%_ -+Hc`)i)b?~cIsxIuVJO^Txc(SU&B1OU4gW0OazJrg~)@@)3j&6Xb+wbz5jCF!nx8LVCbH5(J0gK>FY-Cbqu3Lh5N*WxqZwV=>a?XIupma%soYeo0O?O?P0R -+d|)?nT$hN@y}=EHVB@ndgra`dc{5ysB=!qJDETpP#Ud)QAy6ofZZVyJ5VpEt|aKJ7Gkp5xvXhFRBsC -<>UBn)Z({^qvHw``yl*B#wsErLPWwziOeuvj2GRUnwjCW#)85tvALIw -l5M{Gi_^cG?muo+^JlZUquSw)oxi!v8|2n=X8_o^_5RyZZ2!ZNn56*t5ZE=z3TB#Nye*p7deyMD*`PW -7MxwYD0Z$p*9vJ1RZLGxuEjpy9|szzKw$R&gPRZ5^r}zU#g$1&G1k?W9F_f@~|U_s`fG$GUg6)dvmd_ -#GK*JGBA_INWo1)^k=w}>Ss@%{b{+#136T=l?o9FPA(xlH`0i}1*&jRpjVtu%<1j)pg|FG-)_y7Fnb{yJPzLVM&1twAa7F -;Te;1)YX@-`B7$34Tc0v;>Zg&R6K{T$bpxr?%Kux|O{_@9Ic-7+;gI=7Q4Br~iSoeKyfmt_DT1%pG#@A}?~vhW+L4?tQRY6it4DjV_R9>^-UDO0$>0e89!3M)$201Zdz8svU6%krh=PVp -t%j8f!qSI`$5JbU_e`sj)O9iBH)ihiuBauLc`$9R4I -ROhdo1@#tm2Y&)WW)uFyB_3f3bO#RP!5m}>N=L=sM+`8Yum#m6 -bD_0z-_GYpBuHx~_v2gGjQ<_)w0?MUC%uNP^#ABoZFpxQ_J>ShHG~#$8j6H=E`5HfL;Zf)IUV5_tIEr -6e$Fo0ENmkjK@zg$^2MToguGZ}$$~_oQ?E!BXLQG5)OtTfNoGE@DFq{gUYCz)A8oFpbR3nAOhCS9(wV4aTDSn0Mr -Lo9#>}(`PK9VJ-g5h4XU-2;wPzsD_l2QR@O}`VJES&~wU;?2Tc&9@5A8csDL@q5?B0G+jFcBGVmG7HOLmVOV8nY;1N1)+5HVNQiHj(W? -DY)4c5xJa>iVhLg^UG9lxXbQtN-&7AYkkEA^vqL_6E5m&P>Vm#cIX*Y`n -+X{~|VK^VB;gF>JxWRF4_;Tru&_6H_PTG%XzYw1KlHN%Y-*a;Ti9U-SboECjtF(CuScy -kLoXfZgly=QN(-a+cu;X#O>y#Zkh+UXLO-=0D%N4%=^+Mnd?Su4xENKX0t_4#2aeAHNA5XPQP4#iiN? -jjA@tIqdz^OT0Gr%O@Nygai$;hFaj=FuemkP(L7+*G8d3}0X>E0)P&%PF^f@kkh#880ZlY=%Kx%IXnzX~gBxa-yX6O-O@ -&Y^NkT8y$wB;!IPa($+Yo-AcyKrKi1bRoH!&65u=)q%}$bqL(<+_tLI7E43ln}d{lCC-7(CtMvY3>U28|B$9FuuMF~0y-dP{g>1k>%fOIzA4lgRvil)Mmvzw`hDQYr -0r4er9|BmD6A~DDFCgceIzKUo0P*(9(E%g?!YoWr1xL7Rz`IsoV^xihunx^Vgp7Cq!)zgOhPx{K&1$$ -7}WcEoQ?v-~CF0^Cf0?d$|z;d=UI~nNHK%Jt&=l)~~c17c5Q?59@%}#(Hq@FDMBK7%k7yYK8D~%}#j< -prREKlfsO%f!G#c?h9bA;&aj(?urWJA!gI&>LpvT&Ja5y^QpN~lM%)+_WYOf(rS9g~k`Fy={rDcF-z{ -smA=0|XQR000O8`*~JV=c|?Zr4j%D-!=dM9{>OVaA|NaUv_0~WN&gWb#iQMX<{=kb#!TLFLiQkE^v9R -J9}T-NRt2iQ}l>sEe%E{ye^{~hLGSS>=DQsnB5(4glx63HnODAgWx>wvtLzpx74y^Nbb(R87#Nz)z#J -2_2^8~c+OmRTI5B_U6%#(Bu;bYjk7o`a_)-XosJw&d1s0klQ<0dBoE>!Qz(%)=YC!!A@2wn+vX1wp45 -O&_3}K;*a5pXvdN6kxzS^WSL8GNp2b;?--toqo*4hL;O -PP%rm1%~=TUBqI-NE456Gbkj2=Z#Pfc#0C}U -$*&^X-Um|qg4~DM>XP5490Ai&a0h1u)W@@Zodz;gv&DUF-zl^?Ye>1F(kiP}=gL&ux^yj0^KSuH!ur@ -YFKRaLh7GPctPa0?)1|NZIwjkSpZ8_GVA>jb5-#4#6+}!jJ*Wi`*=K2GTXplFDFt`MD=&YGGj{L>wi7 -{?pot?ZoKOV5J_~ZEe^62!m*MVg?dH?&a`~K(>B}U9^Qo^&|&bJ%e`m*z_`fYUH4*n@Y4IBoIf!^@tF -1$Rw*x6QZfzFp1fA}W;e*F?8_q*RL!=9oM<|YgtNLoFH?+%m{R>#eXF>Zk`20$4OE8&bV+u4Qz#phec -z4y|<6MGK!49#?nJ@VN%+3eXqGaQ3i49O7+O1q9SS(#&f+5fg~;wvkYx?4rjaDdESJ4MH-(cEj&yk4S -)R7Qg4?o(1)hfDN#+vxxNOsAvK-&sjtCwjPw*-k67?G?ijXoT|@boObJ+njH4NhXahSVzAdT6`lTmJN;GU_H}YV2hNkLv*F-kF#LURT>6Tmm#>Zouiv~q`P -+Y;zB@aA|M%g=<<;;1_~*a=@WvCLPw!^IzdwfaC{F%Nv%I+f^sxB6^=x}*cW?hcp8sOIKizEpw}I|wU --KFU+hE35!~S<1MCSDaTW7)_fa72fa3nEp&yOqw24DEc`PsldIo8OmZ|{9wx6qp*!6o4VY4ml;{58Ld0$%NuNeHaeF_!#9JrQ|W`*m+rp -q=Fz=_cL(RL-1joB@j;5ct0s)FG>w&@`wdW`iboJCT}iZ;!=qP&)4|1s`-23ox1G)Hks_mX+rTtiHGe -ZAO}KB*FD~8p=fg{>ec)uSXJ6?*R3V(bq;}jdbT(RGUgB)l*G=pjrM&W&CM}+u8nw@%F9KKdu_47vWKMycu0U=J#UZ0mR4Z*&$WkO%!fOd>;9 -hl+U3a?HGVP0V5*9XAAUk!%;Dj{DFkKVk)U`!2YbP(+jFgl${=w%RrrlDB{+@=A)foK^nTb3gFDwX&XgSj&nmD%!M({3hytUcO7y@S -qMO7s1MLwHR7z@nGAe)E+)B=Zz)EMz5UdA_r2t>((93orhO(t>bqgW714`07x7^&LjeCWY-voOvxNLa -w8EincL0OJw=&uv{Jd1XH3zDvRQre% -M*=rFn)4h6#3}K`XZC()+ma@rO=JEm76uww>593M8yv}(3DNaAKP^RnJ)Wp69Yo(l9o0b -wA$Lo?xL5y{XC4B=Am{RS%3a4p=Ppu6scZ=-h^Q$$z{>EIQcdkb2+F=zb2o8;j__(%^uBzZyt3! -9KKq+bgC6Z?XE;`;H?7h1pOC2)LfY06o>*0M7DBd;&u37d2dRFH -hl;Q_zseq@O}63`$a9_GV+w!hJtMVEL?l(mzYKw!pI=!sW%QeRxgPl4q%*s -ycoDy^i#wV0!nbMQgR7sv!Rd_jvtAVU?@b?5p(Y4V0XTTA%r=Zk`jqz4z2i-&+H%i>$PN4l@HWV;93oJC5b60?dk{plc$Sbedm2Q(>DM+HzWSAI)s1`-V+ -K<2TKJ=+fHgyocpM94`Jzsuk{SLs-YC(1!J(bH8mft>HVq_evO?eK=;POpL>qy5Un>$D>=|=lq~t9(o -6Dm~>~lAW;LHwucQS(`)Wjeb2uE-dcp(Pk@r`24Th&0l|(9;f23oNsWwO5vW@PD6AqaRo2i^2_ZC8Ask>I$A;7nyGuEL$g3GkQ(gm#stdy(f{gO -fW!c7yoJ&hxW0&A{VJaEtC>(BTZSEBJE8U^IE2b-HWicL^6HrU`5rRByyL8vP4A%f6Hth9`$x~psBrr -v52DqkDAZ-aH4_EQW5`5KRv9n?vRVh^H&BwkcJeniE>^lfJ6LSmVkpvSktEXueaiq>5q<2XrK)+fxA) -hJj1*{g7p2m0JxG{J!#9jCvT=!;8 -F+EhYX*W1oW*^6a=o`U+#?b7NjtLEsMq~)ZPxme1@iA=)9K*{yv+|4+JESZTTa-M7Kf{XSjgpGL|hnX -Gld}_z(U4NF(L)DsG%S3;9bI;zkcgdWRj2C{qiM)xFBF_$F*#wVQs+f?Qpg3Yy=H`_Dh2UWD>{F=1GZ -s0<0|)GSgor0mkO#scAvzV;F`@wi7;vNO$tg)Bw948gOE7aKjglw1j -3ItowF(s&_NpXT}0dvA?pFRU236K|VxQE|Tt|U%Kn9$Rr{WTDV*D6UQoZ#Oo6Ob{>v*u)4nS`jaiP#B -3wKB$(<^aNqd?`+~SW>tlNsU7HXp_LyVcAx|Bhn@YsFF626i?S%BW?dniLn@(=IAmF<2xh=y8?Rf4G- -d^MJ0N^&oH{7dPXJTnu^zPI{=8s@)R;|9O=rmTTKF1QcMBnU{lE&mfdQKD;K?#Rh4`CvSlT}N+(g)gI -!GHOj!k@Y*NyeX<)5L8o;3d(~c%wVL)6l4k9mIxcMUCOJ0=H$Xf7$c^{5i^+KCWb|cnyRijQ54u&AP- -v_d_PL54Sx1?QzpDL6(9sCduP^#KGb_VBXD;-Ho1PYBe_#u11kP%Nc5lLi;7n#qRgz)nj>;hs{CKBmu -!gc_8)M*6=-V&o`*X!GR(y|pj^b9!KE@Yjve7fFoMy=31%=nMcdA5R11u4$K_Ap#R^%`BCb_GfcC+#_ -;laZ>V0}I7+LjZtcTVz{3KaF^!JTEWnG@lNrR1JgF)(^u|22WC{t7xLZ -g%dESCk7&Y}|>ApZzamD`ENn>fP+Vqhpdq-ZhFt0H>d$8DV6i=1CZiLZYW}YtUlcsA?Ns!p?J!l?-P; -s9O*;Z!;T})#q^op*bEwxENUEmfo?jpISx|-M6Oyeq2Q34f5v6oh+V(7WMD1o}6Wmvri(R$mc(qZc~r -DBn$ttnGkOpo0BBtN)J3yweh9XAEO84Xy=iXbv4$7e_XKe{N4GhE89R5`I$v<1gfURIDhQF0f -_Tqbv~WOT5?0SSs#!&e|F4(&LyXsk7nEwz&#DNRa2%Fa<$n3U=k08+74oge;RDh;XvV1aGIGI=8mpE= -a0c5dyfG`!XBhT7~U=zWt~+9HUrZ=TZm{bjo93$dLcQ^Bh12b6 -y>ZbK?@Bs7Y`j>jxK|{c=UeSO1*2SupCvFexjX^0_ZJWW0M+|ai;k@UL~W&>an97Lez1Qi{CBX%6j10vfh>Nv}*0KHTGH-Z8Gm8wIa* -IlPz5vf-nH$^OpJuU3wFjY()Waszry8pkTW-z^fhtP$G}{B9>(&S$3pnyjLe7F3*q89X2HM5hbxt*sT -OX#wqtcu9DQ&&xN{Kw#9qszn<D{82l1Ky-P->EP)h>@6aWAK2mt$eR#T!zzj43^000~n001BW003} -la4%nWWo~3|axZmqY;0*_GcR>?X>2cdVQF+OaCy~OTW=f36@KThm{Jd-T#8m}1OZ&LfeSdOkyJ8l1%3 -z&Vz_&joN{(&Gc&6df&Tb>XD-VnMLF(UOW5LN&iUrtFRSX-STD%h#!OWF;z(2Bhi`i~sjR6*)is4TMm -v#CT4}wKdNT2L|9T?u3Upr8lu2sS7;hR?Oz5$o){FPy_jlGB`wUC?EZvB8vRX;4S1Yj;zfNz>XSu(}A -LV~E_LC`g6yc_GG41i|sxo$ENtQ=H^j3MAewj=rg_h2VXJKaVf}qMYS5d$gVBA9faHEl7Q*Ad?~U;LmYgt^c>CeK6MNH%LN=mvBp^`$P|aq9@mlyT&SX>UfmNH5EY7_2RCPL -nXtg6jt%A!Ua@Co1&aB#a@h<|jRt!o;+7xX}ECP;(3vc08nu$ -rbMhmiOen1RLkrD-nxz;z^H)OrriV6YQYu@Pk9JA_&^|JWW2@ySH5M_g7Q8g+Ih;7qpc>}nB$;MVkWu -|=?yfu-0Q1L`&To~Su+v9OWt2XWW1kfq|VlUn@Js7G@1!EvD8A|s$WsG1EhAtc96VVVa>2QFv?OVo0Q -h=ZY=W+5P0C6#8&Xm2(It?fJn-&Sq5*G*Ti5wE4F6zo}&E39h0u`b3)YZR1)*`ky}1)o>y5qNxCFeGk -H(z33trHYQN0o6XMrZhb$d`J)AG$w=2APfZ8B%K!@_I?Y%(vV{(w$?nTlB2J;vTex%j)hnL3c=R-AjP -El;Y}sc``8UyOsYagajompY-cVHOlX*S5hxHzZL(E8y-F+YQC5_WWlq@4YLo -4kJD}?*SngbNZ9j?Ung&gSFWX{yYLAADKhcJQ}YUMZ;7L$ppxJoZ@rGxw{)l^3OeJN(I=Hk0l#V*9ZV -m*mAp2v8x210I-j8V1)2`vRWr+HD4DEyzWokWlkJkk -k5ahBe|L+j@{eqhThq|tECR_A&LlL7N*n2CQRya8`%5L`JTx@-ETQb?rnF1F!a9h>x?< -Z$1umI&}>3DzY=ejqTO?`dm~;gzRQ8Tpn)WvF2!FL-yb2Rt~yJ2$E;V}M(L?^7vho@(r(xzOq{(7HqYH0&k7zpA-!7|9 -Trp35%nQpV4Cloh=2>?_kG<061+?4VNDfKke`c|TQ3ByUu7nQ0)nbYT%wUzRyx-$G3U%&OF&Zzb99-BljEw@%=Hp1M4g0J2 -J+CxW@W0iy+*=`r%Uqx=N|QUGM)e5o+mnLlJor_2KkC%Uno5-2MFX``c&Y{tWHiFZXdBVcVGXzr}DGX -N04}g{6%`;M-(1#=DxNK)}8erU{nM@hFgn3?GKap}{s7FfbZ9;;3^&qb|)V(4Y3@IM(?yUD=BlOYxVR -n?rF;(J)Ax04GmED60_Fubq`7$UV%PJhByaTi^XuQV$z#|x{nn`msd*ie516C5C`|K -}D;_3S+%S<=(ImsD^|nQBfekxMFmx_LX_p%LEskM4q&u!0&hUm4cSR_kGdJ!V3du=|XJ}D&6vo9o!(b+io --S>!t2{VU8OnSS9u8pbQ(-S1JyXpvLwUm$g*$&ETC9Wrv4&&MA%^E;}2p=Dp(Q4sid8SC67e -R>=8;6l6>?K)&$nMw>Od;EAPj5qdtP`E|m?4dhIjPM!i9HdYBI&vIXx#t+l4LY^Oj4(?~gK4|H2R$s> -&t%}#2aMWzVg7!T2xgs&HIDZsBr(wqBJ=NDU&1X8Da&?K_n@K~5H&|2;#Uwt@*r*4}&w!%*AA>+H1a~ -N|hv5WOmcf`i?Dru(GEJ`C^w=t!eGWmsrsb%u#0YKKMn8FJE72ZfSI1UoLQY?OOeA+ -eQ-qUr(_Yqo9OJjFVh@E)g5B9i=hqI6>sJxJv?RMXoH?)KcIsE$fN~d5r_^UM^2^Gy6d6^$5JyY{s}=6Y9Zx=cP$57l|P7hmw~YxyU$4maCL48JAQ -h@HLwgxnN}a)3f8tpS~w!BaR43c}(+I33NJTM7c3wrlP5zWK=S$7>G)#O6s;^V&$ -A^7!+9Z#oP{BtFv}$=cmmq8Fij-OX)1a>vw%+{Vfg%%&R8le)A{kq_~JFAe^mRG&PhL0yaU2Otn&|!zckHagoU6hV6#tJ$3$3ll -f>GD*h#v(E{BLv14Tr4I|%Z1L^@tDoPB%RAfrOA)$bF;08qG01FIW?I5N2>10B#*=C*Pu_#rI*^R$`q -?C(buKzF%zuu%NG8H6{2ki}65p^Xja!UL?^%AAo|u -NCMdT$XIv*PK?m=`V0ahL?##@=yE(e?7Ad~O0~b{0tj8PKr4%{qoX5xXux6NozUp*#V|;rpKUk3u!c3W-rMV`N -T+RDht|<}P1itff>kql%6Ni+u$?*7u+)9C!n(vk%$Ld2B*D&9dua)mJbz!EE(8hb4tQ*to(&%yK}<7{fpQt)e@t+_Qv!<{c64XQm?2Hp@bl8F_KAOXJq -dVhcayP)<# -%QDp@KC48u!m7YH&svamo$Xd!NDO$4ZCJ+Tss36JZBfwn3S?pCrD#NLgdmAE2Xh?`#mcxY4vyuf|&gu -eIs9VhCD -#5qpidu`59ZzKZpRqm0m+S&}Zz(JX|0+}{!1%OuoZRlep-UG^tUF|6ftnY%nzI-}5&~;ozMg)e2NGQ8 -1Raq2bDrRGQ-zRABWL8KP1S$lp@_tI8VMrpH>NH{Q9F_VV0-S76iow1LG%r9-_btXs9F{B;;25XLtOV -bC6Sk-Lb_U|>D8v_DWP*9Y0fmM*XIEHs@@WHTDhNTzM22Bsuyn3-Pc6JF?<0Y4GMBPXZK0i8oYU)#a< -P@W}rjaZwo)29+TTi`vY;P0ucD7F_NEIj!3e -B4-j@NLRqS+Ab7S7)s3O-#d*@Kpg7cuvz4ySFm44GF_o5r-ZaAcdmpH8R^1#gD!Q>a=k+FlkH)$KGJJ -k(R<;Z|%BMzpyRuS+tfBCROi`*+F*~N}VZ*Gia3I=Exvn-QWpMUOe|}*;kz$oVQu2xKZDMm;7o^pCI;tge$1r3%?Lz~r+-5lj(dov6fXLN(Ht5qu-%lE_Qx9)2e0 -ZYx1(jtk$}8(2UW -xB_9AYpeYpD5Pt_{KqG+Aah80_{+sHl9Wc3z*DM*snv0b6fU)19mSK|*0yvV*+@qkoWA529;P_wkS-;!mX&@AX!OTGw644b(J$UBvpA9BfIK)V3}8Y0Da7sm~hwl9C?l1^(80~H4yz-33t -VIeWq|%Ev#&z53LEyD&}CQF@GC27;6Pk&GYI7RQK($He;wWc6Cn3nO;%>8!Fcsk8<5ar@Dp8+qJQbHN -|_yIH)Q!~973HDNU}fGE-=^(l#&R9Y?E3n~6gQO$|6Hh( ->t>z=e4ew8`gbWyd|%K=0?wd&`CS{nWYZSWhy!r<_W!@~X$@>x-#M`s{yY1`KYOS<1L -RXfwB1|x&kY-Vsrgv=+4F)*gw_pls6I4SX9fNUi-Q8sYb;f*Sgll4#La7EVH>X+>r#q^j(3Av2*LUqcsCKoDYz^ICn|X>Q+If&>bld)$i6K -ZCH*DlFd8EdNcnIw$3nhcqm`Yj)cMXSCMAmg~n8Arwbt3F;^+pKp7MFwWEs3SWS6$=4q^d;L6KV1n%7auce-0N! -e`4gumjNS(;>ucFI?|{{|it{0|XQR000O8`*~JV$E^s91qJ{B6C(fsA^-pYaA|NaUv_0~WN&gWcV%K_ -Zewp`X>Mn8FKl6AWo&aUaCwcIO>?t05P56$CCXS}KJ1wu`hFZbllGDx-W!9lVk^er0o*w4w7vX%dbKeroxkI(FW2fN+Cs+?5^sS1|GC;5 -EiHc5W|EY$u`ZP?fQpUhbbhiyOEt2*TwdyUU3yV0piWLC-z`||qjiIq~am*2jA|Ks&Ld&>*9lU1zJvR -3`r82&T+@#6>kma{CcnB1xAr&{#qL6z)HrIOXZ<4cvF;z}iPntrmq%w!Q)a@Q<}!;4TWdOFQG -DfUl=PtZ)Q$Z2@;t9B0_rfla_e6!0EQ-SL3CM!*qFfJ4AnWWZM_;49jh6%4poPJ>wC2)MEZe1+mT!^R -Gz1Kxw_YCPa8Bj5-oz#-sC{SZj&gTA?G0KFd6E>jvklo^lq+Jl){S;FJN%y^c}K+v0GV>H*s4x}^FgK --gWVQ_S?Kf~kll=EO2+H5or`$ptTaw`y^ngO=C)~zK#nVjN!$5<=eN4fUtI1(V&ycq}8B-m7s6HG6SO -&kHybD9F9xn6f5{p2DtvoEAPA}|1%u~yFjrg;^uooP^Ca2iBy69H-xGCToFp(Tz1OkrS2Aq0a-ulvl* -u(1Q_6!u_w$s&1)KsX7o->xCOuR~lE2e~ce2i&ebs9&a>2~lQ3+G_%&w`&Mf=+4lHR~`Yag&U&G%vib -u=`*|s|5D{8PG`h%NFjaVhV|KnBT1fT8k*+&=8XufPohI<_@n`Kvjf*R`X3wFq|&o&34_Ec$4C<-$&` -BAqDkoX4BzSQU7%)LSXyI53R_z)@GF6!TUmiRPhn|<5Giausv!EhHN86S*Mv7sVsMNUwvJ8U25WNlbU -jfagh!U$p*?qxxe5!o)dI>vibL8c? -#gQsFY;5Ijjp@POCARBEoJecvR(dhyM!{T&nYY<2FFMfB*{vZ+;^5U?9@f$b|z=xcu?g)wikg?^9B;V -a!N0WwxAd@KoAB9+5iCz5OxE4G|oyDUESvu7i+)YJ388rVd64Tcn^|jyF4E4&}b;|nL?xeX-d|gjy4l -Jtd>UmswQg{qV3pdIS0i`N70dN;UU~CTRYTfnJF|y1!zZM^zG=SU9{ac~kXjPT&wuqK -1sDffc9$IGG_6+km46c!U8SZGZ<1fFm{vZ9&|qYp72c;L`^9zyLU4aec -x0lXAz?sB8xR5m;2ScHHk}w8-9oVeUyenrGS#BI5rGf{56wrXLEr(HNp!~Q=#KF3|{ENUTtKi{G58(hkSuAIUE)RvaBbR2_g^Vmo=QWuSrNm( -A$TlKEYaJIszk=|dE%IFwp@38$qTr1!}osElTa1gO@_k+R}@4Cl`yK+h+=GxRA(MxobkYh1Gbsc*CQf -Mt9g1T>1{s*XUEzBpo2_5r78gHK@R7OeE*u-_C#-OzoYJD3IK@CNi0?<7wC -!2qM5dIdN=!4k6P+)K36ksW>ZNRSlQp$qwO0z2{d-HSc&#fdO%`PWRd~Ab!^u*0#voXef^OHu~d7kBPEXg=8fk)&tNdfGVtSoYm}9$x!l^K`6<{Mg^f -FZ|20z3cif_6hB*Yc!%L3D0FA2m0*iznS4y7@-|pNP?z<`6e)@b1>l4g*qa@6k<}#rLuTWYX!`=L01Q -roykN4AW^C@hUff^m-!g}gKD1tXy83)f+$;_G4I8tnkYG7}K;A?0B{!eL?86GRt4j$v`)EH&1LJEVw_ -GsPf&@8KQ1I$`9t`zEkhFWKQ)p?>wJCN33GHGW;g60up(Cv+EcUnd-CI&I7Pt;OrW}JlyED0e`sn9F8 --u5a5d}SKC2xzTwD!9fCNY?xhhu*_tv495!QaQkyyLW!+$BtXraDm{zZ0}UV9Iu!Vr(TXar0%X{p-)* -h`*Js(&AlwYGq1}ko~13yOBH{^pa!*}7d}N)o);nruPND+eAMG1;yam^B7DUB9p{qt4!0F{=vYI5o==*JC#wW(*s&BSy%v5({j4qf96 -gGj2aXh(pD$a1M~AYeeM%BuLpvT?{~kR_0z;rJ3-Qkfpk2a`a88z|9QpOp-%8H34#~+Ma?vJ)6wiuF8 -bP^l7rueA3^ZjK8jO@>!?-L<(zcE7Do6W%B0J~w_~xb)tgsrZ;;>`xuq~9?-kh5$*cj}20Z>Z=1QY-O -00;p4c~(c!Jc4cm4Z*nhpWnyJ+V{c?>ZfA2ZbY*jNb1raswO31T+eQ$ -+>sKri1f&87o52y%s3Y-g|TVOZrPX`y!Vw>tRQ*Oma -EjH;mWu5nw@`~T+Anudr3H^Ap>e@^eTz(k -ms#Zo+iZe5WQQ+2}fx}!35%kU{ZoZrUiTEy&J~y*L2-Mv(xcpHtztfknbSrJzNFOGmG*;zmYatpYte4 -$Q_9|4$q``(y`ZMD8$;A%jt2u)oLxt3Sq`b!o37jMwm-B0aN?DB_G>qjiFB?(hgjF=WO!lH|g|Msz9-29`;Z#EI*i(Itp-{q_y(Ip| -HU%D5@A!V9qvT3qdb@*Jt?yN=90r`?P29YH52NH5fhe%V9xa=NQ1y!kGYXg5)`NEVThU+^8d!3IPbx` --np_U*?9eha5Z2D#T5;m6;KD7bb{=O53{(fzGBvuuq6q7Q31mUAU(n|Dv6Wqi-NgQIsxwN_FVcaM+X4 -ZDNAU?Ju-+7B{2y*$4-K#Q=<<*MAczTbQK6c}DPO0&gFJL%*FX0 -PsIJjl&0usCpeB5jaaYku8?LS!rL3CjUe)oJ36wDj*j6pMZpoxVK$Bh`SV?LHrcn@TF^zC|a+##03{q -B?EYXF&!4eJWWjj^bq9(zLAqE8b7>3AnYa|W;LpUMm8xEm7tMo%Wj`IYRhXfLU5$>uS?169!J*eHTWT -Q?wfys}TxY~(MBdJX#d$Guy_BN9|vT@{X)^`@0$#k(9kKqf~;D^Nq55ZYSGdk^URHjoq|NOoeuF%=9u -J?u-z57Ay#)N~=*+5~Ttqsu`Tx_>lS}!)oeGx2EX;pps@0UOF5|h1aKgCcHK7=XXbk$rHhuqF$*WF4W -hpWTk4rT$RnE}5(+8p{_6G?p*7YS=j66aUWDd?W^TQvUssnwuhI)N{c_xp>p5#g -zbWD{VG8B4t(Giw(%(m;$el+MW0WPFiOjtf}K+-zs8eoac&^Wy;MZAr-;Hmh_=$~$dh -|^K;+9|nr$O{%EL0*Zu^&)Jl!gbCC~Iv)g0`4r-I=$3+KhiDiSX#p806)%Qk@UdQXUNL3E4@t=pRLbn -L`CYuV$m$9MwMg-atj8LnWk^>OgkGh>G}He}OdHj)&l$VQ6N4^x=5dOyW9GIONuX1#H;6Wd$tI6HvEP -u4C1Ds>;c9tD21iU#&!-7PN%TCLEa$5c5sh?5j+!=lV1%=DD$+U~eLnTg_fddzH5GQe -NT<{3*`6vKGc3!S{GT1JXS-DLc!Jc4cm4Z*nhpWnyJ+V{c?>ZfA2ZcwcpM -WpZC+WoBt^Wn?aJd8L#?GDJy{lHCz?!}ldi_!-v6r?ILt*lTVy -n&yp&}+&&Aw`a!TTQXSLW4FDCphy?WY6%@;Q|y_}XKapXfW=tl#Q+=z>G<`%k;i!`tA&Qe8rK_l__;< -zgcBJt$x=;Z0ei7<03G?g;V3b%CSW$b(=CKIzPmzE}zRef_?ie;X<5@?d8C0UsjjRp^_t}2%{wzjrhF -)k5@=V$JM%4AX0SK9SWy(;O9Y6<67dr`Zf%ydPXFP3FjkhODLj9W!%Q&qOy?Sg-nTunLe*i%z!XL2#& -H8y5<*BzUbGRdeDwsdp<_m;fItu3poxFxsXELXOE!r4hxtM&c3JHlJv*Bf^lWHvr^{ipZr%W9e4Ji#6{j$FL@lcQWg!pi)T+Jn9Q& -PXh5nwRa?YJ+Xt65B#e-MJH~akoAa(O@XZog$Y!5dG`ZgMPo$X>6Vg*Y>*2tykCmerp{@AsmEoH-ryE -xEI3x5FUhZ=)=+nc^FJWsC;O{H433whf%0B3YA8o(kN6Kg-WAPX%s4rLZ#6#gmDOE2t&1z3ZV{Rs5kQ -U3g!pQCUcAVA#se8B87cbGlqCrsZg-zncF-y`23-yPo?-xuE$-;ojXQ|2S)nE4 -s=bLJP!L*`@V5%ZXN!aQYu$^43W#{8Q34f6@}Tjo>dcg%C<_snO^3+8j?56mB#FPJ|ue`fx|eCZ$cD- -K>W-!Lc4Uzz?K`9tJSkUu{D?D&Io$(%A%=5Ng3nHh7&^l#UkxnTam^siUJ^snWT`6u&=dCk0G{>A*8` -4977=G*l8%pWFM0QOoo`mm`F?#OcW*>6T$?2V)Tj8Cq|zbePZ;9(I-Zq -7=2>&iP0xUpBQ~&^oh|YMxPjcB>G77k?14QN1~5JABjE^eI)uw^pWTz(MO_>L?4Mh5`7Z%Nzf-jp9Fm -p^hwYsL7xPD67)&XCqbVCeG>FZ&?iBk1br0xDD+Y2qtHj8k3t`XJ_>yl`Y7~K=%dg_p^rizg+2;>H2P -@t(deVmN28BMAB{d5eKh)L^wH>}(MO|?Mjwqn8hr?T2z>~B2z>~B2z>~B2z>~B2z>~B2z>~B2z>~B2z -?Ct81ymdW6;N-k3k=UJ_daZ`WW;v=wr~wppQWxgFeRjbn1VvPxu2wRR8xjHoxV<*N6YIN|tG++qb_>{ -{v7<0Rj{Q6aWAK2mt$eR#TliG(h+O003nH000jF0000000000005+c00000aA|NaUtei%X>?y-E^v8J -O928D0~7!N00;p4c~(;+8{@xi0ssK61ONaJ00000000000001_fh7R|0B~t=FJE76VQFq(UoLQYP)h* -<6ay3h000O8`*~JV&BwqszyJUM9svLV3;+NC0000000000q=CN!003}la4&FqE_8WtWn@rG0Rj{Q6aW -AK2mt$eR#TR>aG_BF002D#000>P0000000000005+csRRH3aA|NaUukZ1WpZv|Y%gD5X>MtBUtcb8c~ -DCM0u%!j000080Q-4XQ(rU*uN({j0Ny4502%-Q00000000000HlF21^@tXX>c!JX>N37a&BR4FJg6RY --C?$Zgwtkc~DCM0u%!j000080Q-4XQw7u2l@$vB0O2G602TlM00000000000HlG15&!^jX>c!JX>N37 -a&BR4FJob2Xk{*Nc~DCM0u%!j000080Q-4XQ=-7pTFMUq0AVu#03HAU00000000000HlG=9RL7uX>c! -JX>N37a&BR4FJo_RW@%@2a$$67Z*DGdc~DCM0u%!j000080Q-4XQ{;JOYzzc!JX>N37a&BR4FJ*XRWpH$9Z*FrgaCuNm0Rj{Q6aWAK2mt$eR#PVcqo?Qq002}000 -0#L0000000000005+c89o32aA|NaUukZ1WpZv|Y%gtLX>KlXc~DCM0u%!j000080Q-4XQ#;j?=0FJm0 -52Q>02%-Q00000000000HlF5KL7x5X>c!JX>N37a&BR4FK~Hqa&Ky7V{|TXc~DCM0u%!j000080Q-4X -Q;uiz4&(>`0QndI03-ka00000000000HlGeNB{tEX>c!JX>N37a&BR4FLPyVW?yf0bYx+4Wn^DtXk}w --E^v8JO928D0~7!N00;p4c~(=hw_o*?3;+PvF8}}@00000000000001_fznX`0B~t=FJEbHbY*gGVQe -pVXk}$=Ut)D>Y-D9}E^v8JO928D0~7!N00;p4c~(=-cj`1~0001l0000T00000000000001_fuddj0B -~t=FJEbHbY*gGVQepBY-ulFUukY>bYEXCaCuNm0Rj{Q6aWAK2mt$eR#VjdaF^N#0093O001KZ000000 -0000005+cMPC2_aA|NaUukZ1WpZv|Y%gPMX)j@QbZ=vCZE$R5bZKvHE^v8JO928D0~7!N00;p4c~(<) -AAbh82><|Y9smF#00000000000001_fna9<0B~t=FJEbHbY*gGVQepBY-ulIVRL0)V{dJ3VQyqDaCuN -m0Rj{Q6aWAK2mt$eR#VV?GE^v8JO928D0~7!N00;p4c~(;(W`@cs0RRB_0ssIc00000000000001_f&GsF0B~t=FJ -EbHbY*gGVQepBY-ulJZ*6U1Ze(9$Z*FvDcyumsc~DCM0u%!j000080Q-4XQ)*dce2@eH0H_H702u%P0 -0000000000HlFvkpKX2X>c!JX>N37a&BR4FJo+JFKuCIZZ2?nP)h*<6ay3h000O8`*~JVm>Usq2Lu2B -HVOa$AOHXW0000000000q=7G%003}la4%nJZggdGZeeUMV{Bc!JX>N37a&BR4FJo+JFLQ8dZf<3Ab1rasP)h*<6ay3h000O8`*~JV{dxd -PSO5S3bN~PVApigX0000000000q=DJX003}la4%nJZggdGZeeUMV{B6he1Pj1ONb-4gdfm00000000000001_fpE+K0B~t=FJEbHbY*gGVQepBZ*6U1Ze -(*WUtei%X>?y-E^v8JO928D0~7!N00;p4c~(<6gYW%*2mkOV00000 -00000q=Dht003}la4%nJZggdGZeeUMV{dJ3VQyq|FJowBV{0yOc~DCM0u%!j000080Q-4XQ|fU;s?`G -k0FDa)03-ka00000000000HlFW+yDS@X>c!JX>N37a&BR4FJo_QZDDR?b1!3WZE$R5bZKvHE^v8JO92 -8D0~7!N00;p4c~(=T(w7#`2><}_A^-p<00000000000001_fo9+U0B~t=FJEbHbY*gGVQepBZ*6U1Ze -(*WV{dL|X=inEVRUJ4ZZ2?nP)h*<6ay3h000O8`*~JVT3!3Cp$Gr~OV0000000000q=9c!JX>N37a&BR4FJo_QZDDR?b1!6XcW!KNVPr0Fc~DCM0u%!j000080Q-4 -XQ~$H#<-r300EY_z03ZMW00000000000HlE_`2YZLX>c!JX>N37a&BR4FJo_QZDDR?b1!CcWo3G0E^v -8JO928D0~7!N00;p4c~(=u$ot^q0ssJ~1^@sa00000000000001_fhhd|0B~t=FJEbHbY*gGVQepBZ* -6U1Ze(*WXkl|`E^v8JO928D0~7!N00;p4c~(=2!<0Pg0RRAO1ONaY00000000000001_fkyxV0B~t=F -JEbHbY*gGVQepBZ*6U1Ze(*WXk~10E^v8JO928D0~7!N00;p4c~(;`3unU81pok=5&!@n0000000000 -0001_fo%c-0B~t=FJEbHbY*gGVQepBZ*6U1Ze(*WX>Md?crI{xP)h*<6ay3h000O8`*~JV9AJnmU>g7 -c%WMDuApigX0000000000q=9@00RV7ma4%nJZggdGZeeUMV{dJ3VQyq|FKKRbbYX04E^v8JO928D0~7 -!N00;p4c~(>7Sb)bq3;+PDF8}}@00000000000001_fg2c!JX>N37a&BR4FJo_QZDDR?b1!pfZ+9+mc~DCM0u%!j000080Q-4XQ!gWy@Rc!JX>N37a&BR4FJo_QZDDR?b1!vnX>N0LVQg$JaCuNm0Rj{Q6aWAK2mt -$eR#OXid4LQD000;m0018V0000000000005+cK1TrnaA|NaUukZ1WpZv|Y%gPPZEaz0WOFZfXk}$=E^ -v8JO928D0~7!N00;p4c~(=?bqy{%0RRA60{{Rg00000000000001_frm~30B~t=FJEbHbY*gGVQepCX ->)XPX<~JBX>V?GFJE72ZfSI1UoLQYP)h*<6ay3h000O8`*~JVXbO;_`~d&}lmq|(BLDyZ0000000000 -q=5%e0RV7ma4%nJZggdGZeeUMWNCABa%p09bZKvHb1!0Hb7d}Yc~DCM0u%!j000080Q-4XQ;*`ja$Nx -c0RI9204M+e00000000000HlFLQUL&PX>c!JX>N37a&BR4FJx(RbaH88b#!TOZgVebZgX^DY;0v@E^v -8JO928D0~7!N00;p4c~(;vfl9b{1^@uw6#xJv00000000000001_f#*{J0B~t=FJEbHbY*gGVQepCX> -)XPX<~JBX>V?GFLPvRb963nc~DCM0u%!j000080Q-4XQvgu2Xxae)09ynA03-ka00000000000HlGSS -^)rXX>c!JX>N37a&BR4FJx(RbaH88b#!TOZgVepXk}$=E^v8JO928D0~7!N00;p4c~(Md?crRmbY;0v?bZ> -GlaCuNm0Rj{Q6aWAK2mt$eR#Ts`S07&@008)n001Qb0000000000005+cA9Dc!aA|NaUukZ1WpZv|Y% -ghUWMz0SaA9L>VP|DuW@&C@WpXZXc~DCM0u%!j000080Q-4XQ`;-Z>hA>r0G$~C03HAU00000000000 -HlGzl>q>7X>c!JX>N37a&BR4FKKRMWq2=hZ*_8GWpgfYc~DCM0u%!j000080Q-4XQ^k>Mqx%p50Bkq_ -03!eZ00000000000HlHJn*jiDX>c!JX>N37a&BR4FKlmPVRUJ4ZgVeRUukY>bYEXCaCuNm0Rj{Q6aWA -K2mt$eR#Wqj+MRm{008e6001Qb0000000000005+cD6IhiaA|NaUukZ1WpZv|Y%gqYV_|e@Z*FrhUu0 -=>baixTY;!Jfc~DCM0u%!j000080Q-4XQ#cE&dK(G=0PY?D03`qb00000000000HlHDwE+NdX>c!JX> -N37a&BR4FKlmPVRUJ4ZgVeRb9r-PZ*FF3XD)DgP)h*<6ay3h000O8`*~JVv-DZ*7XttQD+T}n9{>OV0 -000000000q=7`h0RV7ma4%nJZggdGZeeUMY;R*>bZKvHb1!0Hb7d}Yc~DCM0u%!j000080Q-4XQw(z& -$U*`D0DJ}j03rYY00000000000HlGK!vO$rX>c!JX>N37a&BR4FKuOXVPs)+VJ}}_X>MtBUtcb8c~DC -M0u%!j000080Q-4XQ!azP_Xi9B0ADKr03HAU00000000000HlE$#sL6uX>c!JX>N37a&BR4FKuOXVPs -)+VJ~7~b7d}Yc~DCM0u%!j000080Q-4XQwc9KIy?pd0O1n=04D$d00000000000HlFk(g6T)X>c!JX> -N37a&BR4FKuOXVPs)+VJ~oNXJ2wPD002J#001BW0 -000000000005+c-q-;EaA|NaUukZ1WpZv|Y%gtZWMyn~FJE72ZfSI1UoLQYP)h*<6ay3h000O8`*~JV -6)>Prc>w?b-U9#tApigX0000000000q=8r20RV7ma4%nJZggdGZeeUMZEs{{Y;!MTVQyq;WMOn=E^v8 -JO928D0~7!N00;p4c~(<{l00000000000001_fe+#V0B -~t=FJEbHbY*gGVQepLZ)9a`b1!CZa&2LBUt@1>baHQOE^v8JO928D0~7!N00;p4c~(>3m#NLd0RR971 -ONaX00000000000001_fxG1a0B~t=FJEbHbY*gGVQepLZ)9a`b1!LbWMz0RaCuNm0Rj{Q6aWAK2mt$e -R#RFqbX4mM003Dg000~S0000000000005+cxaR=?aA|NaUukZ1WpZv|Y%gtZWMyn~FKlUUYc6nkP)h* -<6ay3h000O8`*~JV8yxcLYykiO;sO8w9smFU0000000000q=DV^0RV7ma4%nJZggdGZeeUMZEs{{Y;! -MjV`ybc!JX>N37a&BR4FK%UYcW-iQFJE72ZfSI1UoLQYP)h*<6ay3h000O8`*~JVt357KN(}%2o-Y6Z9 -RL6T0000000000q=BRg0swGna4%nJZggdGZeeUMZe?_LZ*prdVRdw9E^v8JO928D0~7!N00;p4c~(z?C000000 -00000001_fzm7j0B~t=FJEbHbY*gGVQepMWpsCMa%(ShWpi_BZ*DGdc~DCM0u%!j000080Q-4XQ`3)D -F}no-0NW1$03HAU00000000000HlGgLIMDAX>c!JX>N37a&BR4FK%UYcW-iQFLiWjY;!Jfc~DCM0u%! -j000080Q-4XQ!TH1U%CPS0RIL603QGV00000000000HlGXNCE(GX>c!JX>N37a&BR4FK%UYcW-iQFL- -Tia&TiVaCuNm0Rj{Q6aWAK2mt$eR#N}~0006200000001Na0000000000005+coJ#@#aA|NaUukZ1Wp -Zv|Y%gzcWpZJ3X>V?GFJE72ZfSI1UoLQYP)h*<6ay3h000O8`*~JV>CLabA_f2e^%DR9ApigX000000 -0000q=Dc|0swGna4%nJZggdGZeeUMZ*XODVRUJ4ZgVeVXk}w-E^v8JO928D0~7!N00;p4c~(=zz6u&A -3IG5qCIA2;00000000000001_fk9FN0B~t=FJEbHbY*gGVQepNaAk5~bZKvHb1!CcWo3G0E^v8JO928 -D0~7!N00;p4c~(>WI%Nj382|ttT>tia -P)h*<6ay3h000O8`*~JVz2<6y83F(RnFIg;GXMYp0000000000q=7Jb0swGna4%nJZggdGZeeUMZ*XO -DVRUJ4ZgVeUb!lv5FKuOXVPs)+VP9orX>?&?Y-KKRc~DCM0u%!j000080Q-4XQ>U8^`|<(+0GS5>05J -dn00000000000HlGMdjbG(X>c!JX>N37a&BR4FK=*Va$$67Z*FrhVs&Y3WG`)HbYWy+bYWj?WoKbyc` -k5yP)h*<6ay3h000O8`*~JV;ye;Y=m7u#Cjc!JX>N37a&BR4FK=*Va$$67Z*FrhVs&Y3WG{DUWo2w%Wn^h|VPb4$E^v8JO928D0~7! -N00;p4c~(<_$cJSK1ONc%3jhEv00000000000001_ftP~<0B~t=FJEbHbY*gGVQepNaAk5~bZKvHb1! -0bX>4RKcW7m0Y+r0;XJKP`E^v8JO928D0~7!N00;p4c~(>5-4uH@0000p0000i00000000000001_f$ -WC@0B~t=FJEbHbY*gGVQepNaAk5~bZKvHb1!Lbb97;BY%gD5X>MtBUtcb8c~DCM0u%!j000080Q-4XQ -{h^v-UR{x01^cN05bpp00000000000HlFyhynm`X>c!JX>N37a&BR4FK=*Va$$67Z*FrhX>N0LVQg$K -Wn^h|VPb4$UuV?GFKKRbbYX04FKlIJVPknNaCuNm0Rj{Q6aWAK2mt$eR#T6qSSr -FG000zg001cf0000000000005+ce2@YFaA|NaUukZ1WpZv|Y%gzcWpZJ3X>V?GFKKRbbYX04FL!8VWo -#~Rc~DCM0u%!j000080Q-4XQwXG`bdLi70O<+<0384T00000000000HlG1u>t^aX>c!JX>N37a&BR4F -LGsZFJE72ZfSI1UoLQYP)h*<6ay3h000O8`*~JVWn*>Aln?*_wL1U+ApigX0000000000q=8Sh0swGn -a4%nJZggdGZeeUMa%FKZV{dMAbaHiLbZ>HVE^v8JO928D0~7!N00;p4c~(;=qt)2!6951WL;wIC0000 -0000000001_fg;8N0B~t=FJEbHbY*gGVQepQWpOWZWpQ6-X>4UKaCuNm0Rj{Q6aWAK2mt$eR#R!EM9= -9W000bx001BW0000000000005+cNZJAbaA|NaUukZ1WpZv|Y%g+UaW8UZabIa}b97;BY%XwlP)h*<6a -y3h000O8`*~JVBLu(1a|i$cpdA1J8~^|S0000000000q=9e!0swGna4%nJZggdGZeeUMa%FKZa%FK}b -7gccaCuNm0Rj{Q6aWAK2mt$eR#O+blrnz>000#b001BW0000000000005+c90mgbaA|NaUukZ1WpZv| -Y%g+UaW8UZabI+DVPk7$axQRrP)h*<6ay3h000O8`*~JVbYEXCaCuNm0Rj{Q6a -WAK2mt$eR#W90%c=hW002h<001BW0000000000005+c@+JcSaA|NaUukZ1WpZv|Y%g+Ub8l>QbZKvHF -JfVHWiD`eP)h*<6ay3h000O8`*~JV000000ssI200000D*ylh0000000000q=7Fe0|0Poa4%nJZggdG -ZeeUMa%FRGY;|;LZ*DJaWoKbyc`sjIX>MtBUtcb8c~DCM0u%!j000080Q-4XQ}{OHU%wOp0EkBb04o3 -h00000000000HlF>C<6d+X>c!JX>N37a&BR4FLGsbZ)|mRX>V>XY-ML*V|g!fWpi(Ac4cxdaCuNm0Rj -{Q6aWAK2mt$eR#N}~0006200000001ul0000000000005+cf;|HOaA|NaUukZ1WpZv|Y%g+Ub8l>QbZ -KvHFLGsbZ)|pDY-wUIUtei%X>?y-E^v8JO928D0~7!N00;p4c~(=y03zQ=1pokK6aWA#00000000000 -001_fzdq!0B~t=FJEbHbY*gGVQepQWpi(Ab#!TOZZC3Wb8l>RWo&6;FJfVHWiD`eP)h*<6ay3h000O8 -`*~JVYKm(DsSp4FB1ZrKF#rGn0000000000q=8~X0|0Poa4%nJZggdGZeeUMa%FRGY;|;LZ*DJgWpi( -Ac4cg7VlQK1Ze(d>VRU74E^v8JO928D0~7!N00;p4c~(dXaE2%00000000000001_fm& -1p0B~t=FJEbHbY*gGVQepQWpi(Ab#!TOZZC3Wb8l>RWo&6;FJ@t5bZ>HbE^v8JO928D0~7!N00;p4c~ -(;?)mp#21^@s_761S@00000000000001_fy{3M0B~t=FJEbHbY*gGVQepQWpi(Ab#!TOZZC3Wb8l>RW -o&6;FJ^CbZe(9$VQyq;WMOn=b1rasP)h*<6ay3h000O8`*~JVRpR%rEerqv^&c!JX>N37a&BR4FLGsbZ)|mRX>V>Xa%F -RGY<6XAX<{#OWpHnDbY*fbaCuNm0Rj{Q6aWAK2mt$eR#RNV>E!MN002)F001)p0000000000005+cw} -t}%aA|NaUukZ1WpZv|Y%g+Ub8l>QbZKvHFLGsbZ)|pDY-wUIa%FLKX>w(4Wo~qHE^v8JO928D0~7!N0 -0;p4c~(;z7guBI3jhFYB>(^~00000000000001_f%c070B~t=FJEbHbY*gGVQepQWpi(Ab#!TOZZC3W -b8l>RWo&6;FLGsbZ)|pDaxQRrP)h*<6ay3h000O8`*~JV000000ssI2000009{>OV0000000000q=7A -%0|0Poa4%nJZggdGZeeUMb#!TLb1z?CX>MtBUtcb8c~DCM0u%!j000080Q-4XQ%ZO_Kh^;N0QUm`02= -@R00000000000HlFzm;(TCX>c!JX>N37a&BR4FLiWjY;!MPY;R{SaCuNm0Rj{Q6aWAK2mt$eR#O9VLc -*5<004mo0015U0000000000005+cdzu3PaA|NaUukZ1WpZv|Y%g_mX>4;ZVQ_F{X>xNeaCuNm0Rj{Q6 -aWAK2mt$eR#W6-jcert003ME0012T0000000000005+cPMre)aA|NaUukZ1WpZv|Y%g_mX>4;ZV{dJ6 -VRSBVc~DCM0u%!j000080Q-4XQ?|5;v2z9h009*M04V?f00000000000HlF#p#uPLX>c!JX>N37a&BR -4FLiWjY;!MTZ*6d4bZKH~Y-x0PUvyz-b1rasP)h*<6ay3h000O8`*~JVY!f}Mm;e9(@&Et;9{>OV000 -0000000q=6`?0|0Poa4%nJZggdGZeeUMb#!TLb1!6JbY*mDZDlTSc~DCM0u%!j000080Q-4XQ_!}>j! -Xpr04ojv03rYY00000000000HlHar~?3SX>c!JX>N37a&BR4FLiWjY;!MUWpHw3V_|e@Z*DGdc~DCM0 -u%!j000080Q-4XQznk}UF`z^0EP?z04V?f00000000000HlG5t^)vYX>c!JX>N37a&BR4FLiWjY;!MU -X>w&_bYFFHY+q<)Y;a|Ab1rasP)h*<6ay3h000O8`*~JVxg+o|3jzQD;RFBxB>(^b0000000000q=CJ -%0|0Poa4%nJZggdGZeeUMb#!TLb1!6Rb98ldX>4;}VRC14E^v8JO928D0~7!N00;p4c~(OV0000000000q=AOG0|0Poa4%nJZggdGZeeUMb#!TLb1!9XV{c?>Z -f7oVc~DCM0u%!j000080Q-4XQ{g5JlS2Xk03QSZ03rYY00000000000HlG@x&r`kX>c!JX>N37a&BR4 -FLiWjY;!MVZgg^aaBpdDbaO6nc~DCM0u%!j000080Q-4XQ^z}7-{%Mb00kES03iSX00000000000HlF -by#oMnX>c!JX>N37a&BR4FLiWjY;!MWX>4V4d2@7SZ7y(mP)h*<6ay3h000O8`*~JVqt#C>SOEY4%mM -%aAOHXW0000000000q=94;ZXKZO=V=i!cP -)h*<6ay3h000O8`*~JV5m0CS*#-ar%Mt(p9RL6T0000000000q=9+O0|0Poa4%nJZggdGZeeUMb#!TL -b1!INb7*CAE^v8JO928D0~7!N00;p4c~(=`e-Wdi0RR9S0{{Rm00000000000001_fsNDy0B~t=FJEb -HbY*gGVQepTbZKmJFKKRSWn*+-b7f<7a%FUKVQzD9Z*p`laCuNm0Rj{Q6aWAK2mt$eR#O=t$&+0T000 -av0015U0000000000005+cde#E~aA|NaUukZ1WpZv|Y%g_mX>4;ZY;R|0X>MmOaCuNm0Rj{Q6aWAK2m -t$eR#V=;&!#;a007WW000{R0000000000005+c6XXK`aA|NaUukZ1WpZv|Y%g_mX>4;ZZE163E^v8JO -928D0~7!N00;p4c~(<9v<$F!0RRB01ONaX00000000000001_fr4;ZaA9L>VP|P>XD)DgP)h*<6ay3h000O8`*~JV$Uak^jsySzd<*~p9{>OV00000000 -00q=EMZ1ORYpa4%nJZggdGZeeUMb#!TLb1!gVa$#(2Wo#~Rc~DCM0u%!j000080Q-4XQc!JX>N37a&BR4FLiWjY;!MgYiD0_Wpi(Ja${w4E^v8JO928D0 -~7!N00;p4c~(<7l-3zA1pok95&!@v00000000000001_ftL&f0B~t=FJEbHbY*gGVQepTbZKmJFLPyd -b#QcVZ)|g4Vs&Y3WG--dP)h*<6ay3h000O8`*~JV$(R6($qWDhN+$pSApigX0000000000q=5_)1ORY -pa4%nJZggdGZeeUMb#!TLb1!psVsLVAV`X!5E^v8JO928D0~7!N00;p4c~(=8MVb@a2><}@9RL6y000 -00000000001_ffOGE0B~t=FJEbHbY*gGVQepTbZKmJFLY&Xa9?C;axQRrP)h*<6ay3h000O8`*~JVJg -Ie^3<>}M$|3*&AOHXW0000000000q=76c1ORYpa4%nJZggdGZeeUMb#!TLb1!vnaA9L>X>MmOaCuNm0 -Rj{Q6aWAK2mt$eR#SeVju$xt007?x000{R0000000000005+cb~6M3aA|NaUukZ1WpZv|Y%g_mX>4;Z -b#iQTE^v8JO928D0~7!N00;p4c~(4;ZcW7m0Y%XwlP)h*<6ay3h000O8`*~JVFRk9iF984mR00419R -L6T0000000000q=9lo1ORYpa4%nJZggdGZeeUMc4KodUtei%X>?y-E^v8JO928D0~7!N00;p4c~($_h7T@?TTj70zd7ytkO0000000000q=ENI1ORYpa4%nJZggdGZeeUMc4KodXK8dUaCuN -m0Rj{Q6aWAK2mt$eR#TU6$*GwI002=F0015U0000000000005+cieCf(aA|NaUukZ1WpZv|Y%g|Wb1! -XWa$|LJX<=+GaCuNm0Rj{Q6aWAK2mt$eR#R%uV*EA^002xa0018V0000000000005+cUu6UUaA|NaUu -kZ1WpZv|Y%g|Wb1!psVs>S6b7^mGE^v8JO928D0~7!N00;p4c~(<%F(Ir$7ytl{R{#Jb00000000000 -001_fzopX0B~t=FJEbHbY*gGVQepUV{MtBUtcb8c~DCM0u%!j000080Q-4 -XQ>83)(Elp{03N*n02KfL00000000000HlGik^}&7X>c!Jc4cm4Z*nhWX>)XPZ!U0oP)h*<6ay3h000 -O8`*~JV1`i|L^ZNh*@+$-Y7ytkO0000000000q=DkT1ORYpa4%nWWo~3|axZXsaA9(DX>MmOaCuNm0R -j{Q6aWAK2mt$eR#VdT6U!aA|NaUv_0~WN&gWV_{=xWn*t{baHQOFJWY1aCBvIE^v8JO928D0~7!N0 -0;p4c~(=#V!TZ<0RR9c0{{Ra00000000000001_fq&)&0B~t=FJE?LZe(wAFJob2Xk}w>Zgg^QY%gPB -V`ybAaCuNm0Rj{Q6aWAK2mt$eR#SzRWD^nr006fF001HY0000000000005+c@aF{paA|NaUv_0~WN&g -WV_{=xWn*t{baHQOFJo_QaA9;VaCuNm0Rj{Q6aWAK2mt$eR#TS#Z*33|002cd001Tc0000000000005 -+cLg@tnaA|NaUv_0~WN&gWV_{=xWn*t{baHQOFJo_RbaHQOY-MsTaCuNm0Rj{Q6aWAK2mt$eR#WrQ{u -=rN0089)001Wd0000000000005+cmiYw$aA|NaUv_0~WN&gWV_{=xWn*t{baHQOFJ@_MWp{F6aByXEE -^v8JO928D0~7!N00;p4c~(=DC2d}>1pol%4*&or00000000000001_fz|y50B~t=FJE?LZe(wAFJob2 -Xk}w>Zgg^QY%geKb#iHQbZKLAE^v8JO928D0~7!N00;p4c~(=m$h}$^2><}I8vp<$00000000000001 -_fye^}0B~t=FJE?LZe(wAFJob2Xk}w>Zgg^QY%g|z0B~t=FJE?LZe(wAFJob2Xk}w>Z -gg^QY%gPBV`yb_FJE72ZfSI1UoLQYP)h*<6ay3h000O8`*~JV{LU}Q2?hWFIS>EZgg^QY%gPBV`y -b_FLGsMX>(s=VPj}zE^v8JO928D0~7!N00;p4c~(=ukX5ii0000!0000V00000000000001_f!P)Y0B -~t=FJE?LZe(wAFJonLbZKU3FJE72ZfSI1UoLQYP)h*<6ay3h000O8`*~JVFCeYqo&W#<{{R309{>OV0 -000000000q=8l!1^{qra4%nWWo~3|axY_La&&2CX)j-2ZDDC{Utcb8c~DCM0u%!j000080Q-4XQx_bf -z0f5B0EzVj03HAU00000000000HlF27zO}vX>c!Jc4cm4Z*nhVWpZ?BW@#^DVPj=-bS`jZZBR=A0u%! -j000080Q-4XQc!Jc4cm4Z*nhVWpZ?BW@#^DZ*p -ZWaCuNm0Rj{Q6aWAK2mt$eR#T(rX47{B0074f0018V0000000000005+c8bb&GaA|NaUv_0~WN&gWV` -yP=WMy?y-E^v8JO928D0~7!N00;p4c~(<7zl0F)IRF3_dH?_)00000000000001_fzC$=0 -B~t=FJE?LZe(wAFJow7a%5$6FJftDHD+>UaV~IqP)h*<6ay3h000O8`*~JV%jaiiJOcm#-39;vApigX -0000000000q=EW@2mo+ta4%nWWo~3|axY_OVRB?;bT49QXEktgZ(?O~E^v8JO928D0~7!N00;p4c~(> -4Stva{2><}YBme*>00000000000001_fpvul0B~t=FJE?LZe(wAFJow7a%5$6FJow7a%5?9baH88b#! -TOZZ2?nP)h*<6ay3h000O8`*~JV#p&<`cLV?c{|*2EDF6Tf0000000000q=EO22mo+ta4%nWWo~3|ax -Y_OVRB?;bT4CQVRCb2bZ2sJb#QQUZ(?O~E^v8JO928D0~7!N00;p4c~(>4mtk7J2LJ%}6951t000000 -00000001_fwhwe0B~t=FJE?LZe(wAFJow7a%5$6FJow7a&u*LaB^>AWpXZXc~DCM0u%!j000080Q-4X -Q(XHuNTdY-00s^K04V?f00000000000HlGon+O1KX>c!Jc4cm4Z*nhVXkl_>WppoNZ)9n1XLEF6bY*Q -}V`yn^WiD`eP)h*<6ay3h000O8`*~JV*bE&BS^@w7umk`A9RL6T0000000000q=BKK2mo+ta4%nWWo~ -3|axY_OVRB?;bT4CXZE#_9E^v8JO928D0~7!N00;p4c~(=sLYgsX0{{R&2LJ#f00000000000001_fi -|QF0B~t=FJE?LZe(wAFJow7a%5$6FJo{yG&yi`Z(?O~E^v8JO928D0~7!N00;p4c~(c!Jc4cm4Z*nhVXkl_>WppoPb7OFFZ(?O~E^v8 -JO928D0~7!N00;p4c~(=w3SgoX1^@sKDF6T*00000000000001_flIIm0B~t=FJE?LZe(wAFJow7a%5 -$6FJ*IMb8Rkgc~DCM0u%!j000080Q-4XQy@3Q&?*H00HqE903rYY00000000000HlGLwg>=lX>c!Jc4 -cm4Z*nhVXkl_>WppoPbz^F9aB^>AWpXZXc~DCM0u%!j000080Q-4XQ}jUtd0`j;0O~XV03ZMW000000 -00000HlEfya)hrX>c!Jc4cm4Z*nhVXkl_>WppoPbz^ICW^!e5E^v8JO928D0~7!N00;p4c~(;<1(*0Z -0{{Tj1^@se00000000000001_fuht10B~t=FJE?LZe(wAFJow7a%5$6FJ*OOYjSXMZ(?O~E^v8JO928 -D0~7!N00;p4c~(=*+ey_kIsgELdjJ3+00000000000001_fg0Ed0B~t=FJE?LZe(wAFJow7a%5$6FJ* -OOba!TQWpOTWc~DCM0u%!j000080Q-4XQ=mbO7&rp}0MiBl03rYY00000000000HlG75(xlsX>c!Jc4 -cm4Z*nhVXkl_>WppoPbz^jQaB^>AWpXZXc~DCM0u%!j000080Q-4XQyE3l*JCFD0P9cy03iSX000000 -00000HlEf76|}wX>c!Jc4cm4Z*nhVXkl_>WppoRVlp!^GG=mRaV~IqP)h*<6ay3h000O8`*~JVeyjeE -HUj_v+6DjsBLDyZ0000000000q=BV92>@_ua4%nWWo~3|axY_OVRB?;bT4OOGBYtUaB^>AWpXZXc~DC -M0u%!j000080Q-4XQw=H8vY8S901h?)03!eZ00000000000HlE&K?wkGX>c!Jc4cm4Z*nhVXkl_>Wpp -oSWnyw=cW`oVVr6nJaCuNm0Rj{Q6aWAK2mt$eR#Uc!Jc4cm4Z*nhVXkl_>WppoWVQyz)b!=y0a%o|1ZEs{{Y%Xw -lP)h*<6ay3h000O8`*~JVq|tlo_ZI*F^MnBaB>(^b0000000000q=C(~2>@_ua4%nWWo~3|axY_OVRB -?;bT4dSZf9q5Wo2t^Z)9a`E^v8JO928D0~7!N00;p4c~(>R2%Q%i7XSd*fdK#}00000000000001_fd -|eB0B~t=FJE?LZe(wAFJow7a%5$6FKl6MXJ}<&a%FdIZ)9a`E^v8JO928D0~7!N00;p4c~(>Ngpv<-8 -vp=ekO2TG00000000000001_fo0_h0B~t=FJE?LZe(wAFJow7a%5$6FKl6MXJ~b9XJK+_VQy`2WMynF -aCuNm0Rj{Q6aWAK2mt$eR#V+O6}2NM003+N0stof0000000000005+cA^{2jaA|NaUv_0~WN&gWV`yP -=WMycaX<=?{Z)9a`E^v8JO928D0~7!N00;p4c~(<35Ym-B8vp>1lK}uE0000000000000 -1_fr=>#0B~t=FJE?LZe(wAFJow7a%5$6FKl6MXLM*`X>D(0Wo#~Rc~DCM0u%!j000080Q-4XQyVYVGZ --8I0Lpd&04D$d00000000000HlElMG63LX>c!Jc4cm4Z*nhVXkl_>WppoWVQy!1b#iNIb7*aEWMynFa -CuNm0Rj{Q6aWAK2mt$eR#W8c@T8Xp0082daA|NaUv_0~WN&gWV`yP= -WMyJaA|NaUv_0~ -WN&gWV`yP=WMyAWp -XZXc~DCM0u%!j000080Q-4XQ^$+OgLe%80M{@804M+e00000000000HlGBkqQ8CX>c!Jc4cm4Z*nhVX -kl_>WppofZfSO9a&uv9WMy<^V{~tFE^v8JO928D0~7!N00;p4c~(>Pzq~y~1ONce3IG5h0000000000 -0001_flQwY0B~t=FJE?LZe(wAFJow7a%5$6FLiWgIB;@rVr6nJaCuNm0Rj{Q6aWAK2mt$eR#S&;{S~< -Y008m;0015U0000000000005+c(4z_faA|NaUv_0~WN&gWV`yP=WMyV>WaCuNm0Rj{Q6aW -AK2mt$eR#TG(*D?bD00031001KZ0000000000005+c#iR-VaA|NaUv_0~WN&gWV`yP=WMy?y-E^v8JO928D0~7!N00;p4c~(MtBUtcb8c~DCM0u%!j000080Q-4XQ&QQjhKd6K0NM!v02}}S000000 -00000HlE#z6tc!Jc4cm4Z*nhVZ)|UJVQpbAVQzD2E^v8JO928D0~7!N00;p4c~(=)#1YeR3jhEW -DF6T?00000000000001_f!)Ch0B~t=FJE?LZe(wAFJo_PZ*pO6VJ~5Bb7^#McWG`jGA?j=P)h*<6ay3 -h000O8`*~JV8*fE&g8~2mdj|jjA^-pY0000000000q=Apk3IK3va4%nWWo~3|axY_VY;SU5ZDB8IZfS -IBVQgu0WiD`eP)h*<6ay3h000O8`*~JV2pbF$Pz3-092Ecn9RL6T0000000000q=8b<3IK3va4%nWWo -~3|axY_VY;SU5ZDB8WX>KzzE^v8JO928D0~7!N00;p4c~(=RDAwlg1pojh82|tu00000000000001_f -!);#0B~t=FJE?LZe(wAFJo_PZ*pO6VJ~-SZggdGZ7y(mP)h*<6ay3h000O8`*~JVU2hWoT>$_9MFIc- -9{>OV0000000000q=5+B3IK3va4%nWWo~3|axY|Qb98KJVlQ7`X>MtBUtcb8c~DCM0u%!j000080Q-4 -XQ!LZH4qz()02iVF0384T00000000000HlGU-3kD3X>c!Jc4cm4Z*nhWX>)XJX<{#9Z*6d4bS`jtP)h -*<6ay3h000O8`*~JVhCpNd_%8qebH@Mx9{>OV0000000000q=7vN3jlCwa4%nWWo~3|axY|Qb98KJVl -QN2bYWs)b7d}Yc~DCM0u%!j000080Q-4XQ!?pD%+?eD00U6~02}}S00000000000HlF(IST-AX>c!Jc -4cm4Z*nhWX>)XJX<{#FZe(S6E^v8JO928D0~7!N00;p4c~(>1NpBz`GXMbn$^ZZ#00000000000001_ -fr3s80B~t=FJE?LZe(wAFJx(RbZlv2FKlmPVRUbDb1rasP)h*<6ay3h000O8`*~JVc&0UHY!Cnd+c^L -L9{>OV0000000000q=Dgq3jlCwa4%nWWo~3|axY|Qb98KJVlQoBZfRy^b963nc~DCM0u%!j000080Q- -4XQ}nBO_}2yi0DThx03HAU00000000000HlG6k_!NEX>c!Jc4cm4Z*nhWX>)XJX<{#JVRCC_a&sc!Jc4cm4Z*nhWX>)XJX -<{#JWprU=VRT_GaCuNm0Rj{Q6aWAK2mt$eR#Tn5)qt=I002ZP001BW0000000000005+cy}kc!Jc4cm4Z*nhWX>)XJX<{#QHZ(0^a&0bUc -x6ya0Rj{Q6aWAK2mt$eR#SN)bMBJK0001<0RS5S0000000000005+c>eLMYaA|NaUv_0~WN&gWWNCAB -Y-wUIbT%|DWq4&!O928D0~7!N00;p4c~(=jFEei;A&*;@br9smFU0000000000q=B -mE4ghdza4%nWWo~3|axY|Qb98KJVlQ@Oa&u{KZZ2?nP)h*<6ay3h000O8`*~JV92gG9H?RNz0AK+C8v -pt03QGV00000000000HlG`u@3-nX>c!Jc4cm4Z*nhWX>)XJX<{#THZ(0^a&0bUcx6ya0Rj{Q -6aWAK2mt$eR#W>LvixVj0001n0RS5S0000000000005+cSlbW)aA|NaUv_0~WN&gWWNCABY-wUIcQ!O -GWq4&!O928D0~7!N00;p4c~(=j{uZzcDF6V!rvLyP00000000000001_f%uyd0B~t=FJE?LZe(wAFJx -(RbZlv2FL!8VWo#~Rc~DCM0u%!j000080Q-4XQzP$Z{KEhM01^QJ04V?f00000000000HlFE#Ss8-X> -c!Jc4cm4Z*nhWX>)XJX<{#5Vqs%zaBp&SFJE72ZfSI1UoLQYP)h*<6ay3h000O8`*~JVuZk~a&H(@b% -L4!aB>(^b0000000000q=84q5dd&$a4%nWWo~3|axY|Qb98KJVlQ7}VPk7>Z*p`mZE163E^v8JO928D -0~7!N00;p4c~(=#IR?{F8~^}oWB>ps00000000000001_fmp~90B~t=FJE?LZe(wAFJx(RbZlv2FJEF -|V{344a&#|qXmxaHY%XwlP)h*<6ay3h000O8`*~JVZx#Tp_5lC@ISK#(D*ylh0000000000q=D|_5dd -&$a4%nWWo~3|axY|Qb98KJVlQ7}VPk7>Z*p`mb9r-PZ*FF3XD(xAXHZK40u%!j000080Q-4XQ~r7Dqp -2PM0On`_04e|g00000000000HlE}=MeyKX>c!Jc4cm4Z*nhWX>)XJX<{#5Vqs%zaBp&SFLQZwV{dL|X -=g5Qc~DCM0u%!j000080Q-4XQ$#`^Ltc!Jc4cm4Z*nhW -X>)XJX<{#5Vqs%zaBp&SFLYsYW@&6?E^v8JO928D0~7!N00;p4c~(=2mv5*<0ssJr1ONaa000000000 -00001_fyQ4F0B~t=FJE?LZe(wAFKBdaY&C3YVlQ7`X>MtBUtcb8c~DCM0u%!j000080Q-4XQ_80;;Vl -#Z09Zi)03iSX00000000000HlFPViEvwX>c!Jc4cm4Z*nhabZu-kY-wUIUukGzbY*yLY%XwlP)h*<6a -y3h000O8`*~JViZ&5F4j%vjVSWGrBme*a0000000000q=B?{5&&>%a4%nWWo~3|axZ9fZEQ7cX<{#5X ->M?JbaQlaWnpbDaCuNm0Rj{Q6aWAK2mt$eR#P<&;B8MG008hT0RSQZ0000000000005+c1eOv2aA|Na -Uv_0~WN&gWXmo9CHEd~OFJE+TYh`X}dS!AhaCuNm0Rj{Q6aWAK2mt$eR#T4CiAmiC002W10015U0000 -000000005+cld}>4aA|NaUv_0~WN&gWXmo9CHEd~OFJE4;YaCuNm0Rj{Q6aWAK2mt$eR#RIt113 -O7000O^0RSNY0000000000005+cthy2aaA|NaUv_0~WN&gWXmo9CHEd~OFJo_Rb97;DbaO6nc~DCM0u -%!j000080Q-4XQ?aYPpWj3P0K&-u03!eZ00000000000HlE{0}}vnX>c!Jc4cm4Z*nhabZu-kY-wUIX -mo9CHE>~ab7gWaaCuNm0Rj{Q6aWAK2mt$eR#S*`+2Ohn0056Y001HY0000000000005+cOGpy{aA|Na -Uv_0~WN&gWXmo9CHEd~OFLPybX<=+>dS!AhaCuNm0Rj{Q6aWAK2mt$eR#VdDT~t~C003bYEXCaCuNm0Rj{Q6aWAK2mt -$eR#O=o=YggH008v^001KZ0000000000005+c<5?2`aA|NaUv_0~WN&gWXmo9CHEd~OFJE+WX=N{8Vq -tS-E^v8JO928D0~7!N00;p4c~( -UK0RtX>c!Jc4cm4Z*nhabZu-kY-wUIW@&76WpZ;bUtei%X>?y-E^v8JO928D0~7!N00;p4c~(<^umlj -o0RRA(0{{Rv00000000000001_fo)zB0B~t=FJE?LZe(wAFKBdaY&C3YVlQTCY;c!Jc4cm4Z*nhabZu-kY-wUIW@&76WpZ;bX>Mv|V{~6_WprU*V`yP= -b7gccaCuNm0Rj{Q6aWAK2mt$eR#TGEWT0#V0027<001Na0000000000005+c!DJHvaA|NaUv_0~WN&g -WXmo9CHEd~OFJ@_MbY*gLFKlUUbS`jtP)h*<6ay3h000O8`*~JV7j>E{4?5a&s?laCB*JZeeV6VP|tLaCuNm0Rj{Q6aWAK2m -t$eR#Q>FVND4b000qb001cf0000000000005+co^KNXaA|NaUv_0~WN&gWXmo9CHEd~OFJ@_MbY*gLF -LPmTX>@6NWpXZXc~DCM0u%!j000080Q-4XQ($uc5A^{60KNnO04e|g00000000000HlHLhZ6vBX>c!J -c4cm4Z*nhabZu-kY-wUIW@&76WpZ;bcW7yJWpi+0V`VOIc~DCM0u%!j000080Q-4XQ=U0-T4wc!Jc4cm4Z*nhabZu-kY-wUIbaG{7VPs)&bY*gLFJE72ZfSI1UoL -QYP)h*<6ay3h000O8`*~JVQj8HPR0041vjzYFD*ylh0000000000q=DUw698~&a4%nWWo~3|axZ9fZE -Q7cX<{#Qa%E*p0PqF?04M+e000000 -00000HlF>juQZIX>c!Jc4cm4Z*nhabZu-kY-wUIbaG{7VPs)&bY*gLFLPmdE^v8JO928D0~7!N00;p4 -c~(4R -=a&s?VUukY>bYEXCaCuNm0Rj{Q6aWAK2mt$eR#QGcX#DaH008AU001cf0000000000005+cqLvc?aA| -NaUv_0~WN&gWXmo9CHEd~OFLZKcWny({Y-D9}b1!0Hb7d}Yc~DCM0u%!j000080Q-4XQ&0q_vHu4E0N -o-004M+e00000000000HlH2r4s;fX>c!Jc4cm4Z*nhabZu-kY-wUIbaG{7Vs&Y3WMy)5FJy0RE^v8JO -928D0~7!N00;p4c~(4R=a&s?bbaG{7E^v8JO928D0~7!N00;p4c~(=g8MlBL4gdhIIRF4J00000000000001 -_fx5U80B~t=FJE?LZe(wAFKBdaY&C3YVlQ-ZWo2S@X>4R=a&s?bbaG{7Uu<}7Y%XwlP)h*<6ay3h000 -O8`*~JV+jeS&o(2E_R~7&OEC2ui0000000000q=6vE698~&a4%nWWo~3|axZ9fZEQ7cX<{#Qa%E+AVQ -gzbYEXCaCuNm0Rj{Q6aWAK2mt$eR#PD@+Cmox001-{001Ze0000000000005+c2+k7#a -A|NaUv_0~WN&gWXmo9CHEd~OFLZKcWp`n0Yh`kCFJfVHWiD`eP)h*<6ay3h000O8`*~JVWr}a9_W=L^ -g#`crCjbBd0000000000q=9AC698~&a4%nWWo~3|axZ9fZEQ7cX<{#Qa%E+AVQgzc!Jc4cm4Z*nhabZu-kY-w -UIbaG{7cVTR6WpZ;bWpr|7WiD`eP)h*<6ay3h000O8`*~JVFatz%c?JLg)ffN(E&u=k0000000000q= -D1i698~&a4%nWWo~3|axZ9fZEQ7cX<{#Qa%E+AVQgzbYEXCaCuNm0Rj{Q6aWAK2mt$eR#RmaV{t13004ar000>P0000000000005 -+c{^t__aA|NaUv_0~WN&gWX=H9;FJo_HWn(UIc~DCM0u%!j000080Q-4XQ!S&#-OB&~0B8XK02%-Q00 -000000000HlFn>k|NQX>c!Jc4cm4Z*nhbWNu+EV{dJ6VRSBVc~DCM0u%!j000080Q-4XQ&gZqI-Lsu0 -2(p?02lxO00000000000HlFq>=OWRX>c!Jc4cm4Z*nhbWNu+EV{dY0E^v8JO928D0~7!N00;p4c~(<) -7#cEfBLDzyr2qgN00000000000001_fj0OP0B~t=FJE?LZe(wAFKJ|MVJ~T9Zee6$bYU)Vc~DCM0u%! -j000080Q-4XQ!L0`TPFhm0F4I#0384T00000000000HlH68x#O=X>c!Jc4cm4Z*nhbWNu+EX>N3KVQy -z-b1rasP)h*<6ay3h000O8`*~JV>7zsD7XSbN6#xJLAOHXW0000000000q=7*n6aa8(a4%nWWo~3|ax -ZCQZecHQVPk7yXJubxVRT_GaCuNm0Rj{Q6aWAK2mt$eR#Uvk5`-O?004Ou0{|TW0000000000005+cm -LC)VaA|NaUv_0~WN&gWX=H9;FLiWtG&W>mbYU)Vc~DCM0u%!j000080Q-4XQ;jEASl|Hw0A2(D03QGV -00000000000HlHLw-f+yX>c!Jc4cm4Z*nhfb7yd2V{0#8UukY>bYEXCaCuNm0Rj{Q6aWAK2mt$eR#P$ -fug4Yu000yK0018V0000000000005+c3%V2laA|NaUv_0~WN&gWZF6UEVPk7AUv_13b7^mGE^v8JO92 -8D0~7!N00;p4c~(<9JDt<50RR9w1ONab00000000000001_fnK^40B~t=FJE?LZe(wAFKu&YaA9L>FJ -*XRWpH$9Z*FrgaCuNm0Rj{Q6aWAK2mt$eR#S=pt2_V)0077r000^Q0000000000005+cO1u;RaA|NaU -v_0~WN&gWZF6UEVPk7AWq5QhaCuNm0Rj{Q6aWAK2mt$eR#U62=1n9W004@V0018V0000000000005+c -g2NO5aA|NaUv_0~WN&gWZF6UEVPk7AW?^h>Vqs%zE^v8JO928D0~7!N00;p4c~(;<3Bjgi0RRA%0ssI -a00000000000001_f#cv50B~t=FJE?LZe(wAFK}UFYhh<;Zf7rFUtwZzb#z}}E^v8JO928D0~7!N00; -p4c~(;ut&*Vh0002-0RR9Y00000000000001_fr#Q10B~t=FJE?LZe(wAFK}UFYhh<;Zf7rFUukY>bY -EXCaCuNm0Rj{Q6aWAK2mt$eR#O`)I%9|q007`D001KZ0000000000005+cyWVP|P>XD?rEVQzVBX>N6RE^v8JO928D0~7!N00;p4c~(=Z>u=dG2LJ#X5dZ)q00000000000001_ -frRoD0B~t=FJE?LZe(wAFK}UFYhh<;Zf7rFZFO^OY-w(FcrI{xP)h*<6ay3h000O8`*~JVaRPJMmPUvqSFbz^jOa%FQaaCuNm0Rj -{Q6aWAK2mt$eR#R>K>OXr5001W;001BW0000000000005+cp!*a6aA|NaUv_0~WN&gWaA9L>VP|P>XD -@AGa%*LBb1rasP)h*<6ay3h000O8`*~JVZj(V;@&*6^L=pf1B>(^b0000000000q=8um6##H)a4%nWW -o~3|axZXUV{2h&X>MmPa%FLKX>w(4Wo~qHE^v8JO928D0~7!N00;p4c~(<2WSxN$8vp?GcmMz+00000 -000000001_fsPFo0B~t=FJE?LZe(wAFK}UFYhh<;Zf7rZaAjj@W@%+|b1rasP)h*<6ay3h000O8`*~J -V5aMk*l@R~{Vm$x=9RL6T0000000000q=Dfm6##H)a4%nWWo~3|axZXUV{2h&X>MmPbYW+6E^v8JO92 -8D0~7!N00;p4c~(URJD0D=Gj03HAU0000000000 -0HlHFP!#}hX>c!Jc4cm4Z*nhiWpFhyH!ojbX>MtBUtcb8c~DCM0u%!j000080Q-4XQ~N##QYZxg0D%n -v02=@R00000000000HlGNQ567iX>c!Jc4cm4Z*nhiWpFhyH!os!X>4RJaCuNm0Rj{Q6aWAK2mt$eR#S -7On+w7P006`n000{R0000000000005+c{8kkJaA|NaUv_0~WN&gWaAj~cF*h$`Xk}w-E^v8JO928D0~ -7!N00;p4c~(;mu~yKE1^@s85C8xk00000000000001_f%jY$0B~t=FJE?LZe(wAFK}gWH8D3YV{dG4a -%^vBE^v8JO928D0~7!N00;p4c~(=83#T>70RRBy1ONaW00000000000001_fxTlD0B~t=FJE?LZe(wA -FK}gWH8D3YV{dJ6VRSBVc~DCM0u%!j000080Q-4XQ#;SNM$Z8N0BHmO03HAU00000000000HlGyWfcH -$X>c!Jc4cm4Z*nhiWpFhyH!oyqa&&KRY;!Jfc~DCM0u%!j000080Q-4XQxxc!Jc4cm4Z*nhiWpFhyH!o#wc4BpDY-BEQc~DCM0u%!j000080Q-4XQX>c!Jc4cm4Z*nhiWpFhyH!p2vbYU)Vc~DCM0u%!j000080 -Q-4XQx;WthTRMR0Ch9~03HAU00000000000HlE^bQJ(_X>c!Jc4cm4Z*nhiWpFhyH!pW`VQ_F|a&sc!Jc4cm4Z*nhiWpFh -yH!o>!UvP47V`X!5FJE72ZfSI1UoLQYP)h*<6ay3h000O8`*~JVv=buUNDBY}!7Bg&EC2ui00000000 -00q=Br06##H)a4%nWWo~3|axZXYa5XVEFKKRHaB^>BWpi^cUukY%aB^>BWpi^baCuNm0Rj{Q6aWAK2m -t$eR#U)|DV@Y~0094{0RSZc0000000000005+cK8_UtaA|NaUv_0~WN&gWaBF8@a%FRGb#h~6b1z?CX ->MtBUtcb8c~DCM0u%!j000080Q-4XQ(+_n)=L2Z05Spq04D$d00000000000HlFM0u}&pX>c!Jc4cm4 -Z*nhiYiD0_Wpi(Ja${w4FK~G?F=KCSaA9;VaCuNm0Rj{Q6aWAK2mt$eR#WeURQTcq0028O001Na0000 -000000005+c)dLm)aA|NaUv_0~WN&gWaBN|8W^ZzBWNC79FJE72ZfSI1UoLQYP)h*<6ay3h000O8`*~ -JVR)sLnTmb+8bOZnZBme*a0000000000q=ESe765Q*a4%nWWo~3|axZXfVRUA1a&2U3a&s?VUu|J&Ze -L$6aCuNm0Rj{Q6aWAK2mt$eR#WZ?Ez&*&005c~001KZ0000000000005+cmkJgDaA|NaUv_0~WN&gWa -BN|8W^ZzBWNC79FJW$Ea&Kv5E^v8JO928D0~7!N00;p4c~(4IUfGI1^@v08UO$w00000000000001_f%p~{0B~t=FJE?LZe(wAFK}#ObY^dIZDeV3b1!vnX? -QMhc~DCM0u%!j000080Q-4XQ-k>iYCQk|08jt`03!eZ00000000000HlHO9Tos^X>c!Jc4cm4Z*nhiY -+-a}Z*py9X>xNfc4cyNX>V>WaCuNm0Rj{Q6aWAK2mt$eR#S!>I518J000mf001KZ0000000000005+c -Zypu^aA|NaUv_0~WN&gWaBN|8W^ZzBWNC79FL!BfWN&wKE^v8JO928D0~7!N00;p4c~(;r+t4op2LJ% -B6aWAq00000000000001_f&L{H0B~t=FJE?LZe(wAFK}{iXL4n8b1z?CX>MtBUtcb8c~DCM0u%!j000 -080Q-4XQ|?3ZU&aIg0DcPq02=@R00000000000HlFFEfxT9X>c!Jc4cm4Z*nhia&KpHWpi^cVqtPFaC -uNm0Rj{Q6aWAK2mt$eR#TCk?2lvw003VK0015U0000000000005+cJu(&maA|NaUv_0~WN&gWaB^>Fa -%FRKFJo_PZ*p@kaCuNm0Rj{Q6aWAK2mt$eR#Q%e)_ffU002z}0018V0000000000005+c-8L2gaA|Na -Uv_0~WN&gWaB^>Fa%FRKFJo_YZggdGE^v8JO928D0~7!N00;p4c~(>8*Y@fz0{{TE1poja000000000 -00001_fj2r90B~t=FJE?LZe(wAFK}{iXL4n8b1!pnX>M+1axQRrP)h*<6ay3h000O8`*~JVxf(FQYzF -`U`4a#DAOHXW0000000000q=BM6765Q*a4%nWWo~3|axZdaadl;LbaO9XUukY>bYEXCaCuNm0Rj{Q6a -WAK2mt$eR#VF+@nr@9006lG001KZ0000000000005+cOhpy|aA|NaUv_0~WN&gWa%FLKWpi|MFJE7FW -pZEK~phAOHX -W0000000000q=D>6765Q*a4%nWWo~3|axZdaadl;LbaO9ZWMOc0WpZ;aaCuNm0Rj{Q6aWAK2mt$eR#O -71vL9&%0006R000{R0000000000005+cwp|tgaA|NaUv_0~WN&gWa%FLKWpi|MFJW+LE^v8JO928D0~ -7!N00;p4c~(=qw23Qo3jhG$CjbB(00000000000001_fmmb~0B~t=FJE?LZe(wAFLGsZb!BsOb1z|ab -Z9Pcc~DCM0u%!j000080Q-4XQ-ZRh&n^J~0MP*e0384T00000000000HlEha25b?X>c!Jc4cm4Z*nhk -WpQ<7b98erV`Xx5b1rasP)h*<6ay3h000O8`*~JVuV#XDOV000 -0000000q=Alf765Q*a4%nWWo~3|axZdaadl;LbaO9bZ*Oa9WpgfYc~DCM0u%!j000080Q-4XQ)i?&gh -vDb0J01K03rYY00000000000HlG7h!y~FX>c!Jc4cm4Z*nhkWpQ<7b98erWq4y{aCB*JZgVbhc~DCM0 -u%!j000080Q-4XQ(+b-xsC(?0E7c!Jc4cm4Z*nhkWpQ<7b98er -Xk~10E^v8JO928D0~7!N00;p4c~(;!N^{|P0RRB?0ssIV00000000000001_f!dK40B~t=FJE?LZe(w -AFLGsZb!BsOb1!IbZ)?y-E^v8J -O928D0~7!N00;p4c~(;&WxEeF2LJ%@761Sv00000000000001_fx8hG0B~t=FJE?LZe(wAFLGsbZ)|p -DY-wUIaB^>UX=G(`b1rasP)h*<6ay3h000O8`*~JV%K=~A>Hz=%R0RM4BLDyZ0000000000q=7IQ7XW -Z+a4%nWWo~3|axZdab8l>RWo&6;FLGsYZ*p{Ha&ss60E9#U03 -!eZ00000000000HlFi8y5g@X>c!Jc4cm4Z*nhkWpi(Ac4cg7VlQ%Kb8l>RWpZ;aaCuNm0Rj{Q6aWAK2 -mt$eR#Q~m&~Q@)006oY001EX0000000000005+c&Mg-JaA|NaUv_0~WN&gWa%FRGY<6XAX<{#PbaHiL -baO6nc~DCM0u%!j000080Q-4XQvd(}00IC20000004V?f00000000000HlFnGZz4GX>c!Jc4cm4Z*nh -kWpi(Ac4cg7VlQKFZE#_9FJE72ZfSI1UoLQYP)h*<6ay3h000O8`*~JV8`mypV*mgEoB#j-FaQ7m000 -0000000q=Bh37XWZ+a4%nWWo~3|axZdab8l>RWo&6;FJo_QaA9;WV{dG1Wn*+{Z*Fs6VPa!0aCuNm0R -j{Q6aWAK2mt$eR#WC47Qf{a002=(001BW0000000000005+cS~M2`aA|NaUv_0~WN&gWbY*T~V`+4GF -JE72ZfSI1UoLQYP)h*<6ay3h000O8`*~JV8Qg7btpor7@(cg~AOHXW0000000000q=9`%7XWZ+a4%nW -Wo~3|axZjcZee3-ba^jdVRLzIV`*4;YaCuNm0Rj{Q6aWAK2mt$eR#T|q81C`{007 -tp0012T0000000000005+cAyF3qaA|NaUv_0~WN&gWbY*T~V`+4GFJWeMWpXZXc~DCM0u%!j000080Q --4XQ_y|osl5UK0AK|G03HAU00000000000HlFVR2KknX>c!Jc4cm4Z*nhmWo}_(X>@rnVr6D;a%C=Xc -~DCM0u%!j000080Q-4XQv?g`YhnWc0CWcc03-ka00000000000HlFOR~Gc!Jc4cm4Z*nhmWo}_( -X>@rnVr6D;a%Eq0Y-MF|E^v8JO928D0~7!N00;p4c~(=VY_ikb0ssJK1pojW00000000000001_f$Lf -q0B~t=FJE?LZe(wAFLY&YVPk1@c`t5Za4v9pP)h*<6ay3h000O8`*~JVm{Kjfv;_bF^%(#F9RL6T000 -0000000q=5il7XWZ+a4%nWWo~3|axZjcZee3-ba^jwWpr|RE^v8JO928D0~7!N00;p4c~(>PR?T&(0{ -{T#3IG5c00000000000001_f$w7%0B~t=FJE?LZe(wAFLY&YVPk1@c`tKxZ*VSfc~DCM0u%!j000080 -Q-4XQytzSIjjQ!0AUCK03rYY00000000000HlG^XBPl)X>c!Jc4cm4Z*nhmWo}_(X>@rnbZ>HQVPtQ2 -WnwOHc~DCM0u%!j000080Q-4XQ(9$!S(69=03#Xz02}}S00000000000HlGwYZm};X>c!Jc4cm4Z*nh -mWo}_(X>@rncVTICE^v8JO928D0~7!N00;p4c~(=s(5w5a0002y0000T00000000000001_fs1q(0B~ -t=FJE?LZe(wAFLZBhY-ulFUukY>bYEXCaCuNm0Rj{Q6aWAK2mt$eR#V0R{PsW<0056y000~S0000000 -000005+cadj5}aA|NaUv_0~WN&gWbZ>2JX)j-JVRCb2axQRrP)h*<6ay3h000O8`*~JVg4{8H&I14dc -?tjk7ytkO0000000000q=D;-7XWZ+a4%nWWo~3|axZjmZER^TUvgzGaCuNm0Rj{Q6aWAK2mt$eR#PEK -Z5&Yq007Gh0018V0000000000005+c?~WG$aA|NaUv_0~WN&gWb#iQMX<{=kUtei%X>?y-E^v8JO928 -D0~7!N00;p4c~(;$KNE5S4FCW;DgXc@00000000000001_fqjz~0B~t=FJE?LZe(wAFLiQkY-wUMFJE -JCY;0v?bZKvHb1rasP)h*<6ay3h000O8`*~JVTd^H-KL7v#KL7v#9{>OV0000000000q=CSo7XWZ+a4 -%nWWo~3|axZmqY;0*_GcR9uWpZc!Jc4cm4Z*nhna%^mAVlyveZ*Fd7V{~b6ZZ2?nP)h*<6ay3h000O8`*~JVo&cQ- -MkoLP(~(^b0000000000q=AOG7XWZ+a4%nWWo~3|axZmqY;0*_GcRLrZf<2`bZKvHaBpvHE^v8 -JO928D0~7!N00;p4c~(<{YAOHX%00000000000001_fe+yq0B~t=FJE?LZe(wAFLiQkY-w -UMFJ*XRWpH$9Z*FrgaCuNm0Rj{Q6aWAK2mt$eR#V3)ODrG?004s_0012T0000000000005+cIqMeyaA -|NaUv_0~WN&gWb#iQMX<{=kW@%+?WOFWXc~DCM0u%!j000080Q-4XQ-`a+qkaPb0Eh_y03QGV000000 -00000HlGG^%nqeX>c!Jc4cm4Z*nhna%^mAVlyvhX>4V1Z*z1maCuNm0Rj{Q6aWAK2mt$eR#WJrk&9>+ -001*h001HY0000000000005+cPx%)BaA|NaUv_0~WN&gWb#iQMX<{=kaBpvHZDDR001j)0018V0000000000005+c+7}oAaA|NaUv_0~WN -&gWb#iQMX<{=ka%FRHZ*FsCE^v8JO928D0~7!N00;p4c~(;Z00002000000000d00000000000001_f -qFF<0B~t=FJE?LZe(wAFLiQkY-wUMFJo_RbaH88FJE72ZfSI1UoLQYP)h*<6ay3h000O8`*~JVDud}2 -wE+MCy#oLMF#rGn0000000000q=CUT7yxi-a4%nWWo~3|axZmqY;0*_GcRLrZgg^KVlQ7|aByXAXK8L -_UuAA~X>xCFE^v8JO928D0~7!N00;p4c~(<6WeLe=3;+NcD*yl}00000000000001_fyFl%0B~t=FJE -?LZe(wAFLiQkY-wUMFJo_RbaH88FJW+SWo~C_Ze=cTc~DCM0u%!j000080Q-4XQ@F=N{!|740J;$X04 -D$d00000000000HlF(L>K^YX>c!Jc4cm4Z*nhna%^mAVlyveZ*FvQX<{#KbZl*KZ*OcaaCuNm0Rj{Q6 -aWAK2mt$eR#RZPnqGkv000C+001Ze0000000000005+c3riRPaA|NaUv_0~WN&gWb#iQMX<{=kV{dMB -a%o~OaCvWVWo~nGY%XwlP)h*<6ay3h000O8`*~JV!0)=_!6g6yk%j;OE&u=k0000000000q=C|37yxi --a4%nWWo~3|axZmqY;0*_GcRLrZgg^KVlQ)LV|8+6baG*Cb8v5RbS`jtP)h*<6ay3h000O8`*~JV%Tm -ZcSqK0Cxf=igBme*a0000000000q=Das7yxi-a4%nWWo~3|axZmqY;0*_GcRLrZgg^KVlQ)VV{3CRaC -uNm0Rj{Q6aWAK2mt$eR#N}~0006200000001}u0000000000005+cdX5+XaA|NaUv_0~WN&gWb#iQMX -<{=kV{dMBa%o~OUvp(+b#i5Na$#Md`ZfA2YaCuNm0Rj{Q6aWAK2mt$eR#UDjr@UDa003e(0021v0000000000005+cAfOlkaA| -NaUv_0~WN&gWb#iQMX<{=kV{dMBa%o~OUvp(+b#i5Na$#;0RtWW>|0BisN04M+e00000000000HlG?u^0ewX>c!Jc4cm4Z*nhna%^mAVlyvrVPk7yX -JvCQUtei%X>?y-E^v8JO928D0~7!N00;p4c~(>3X^-9}ApihshX4R000000000000001_fo8H80B~t= -FJE?LZe(wAFLiQkY-wUMFK}UFYhh<)b1!pgcrI{xP)h*<6ay3h000O8`*~JV000000ssI200000G5`P -o0000000000q=C)T7yxi-a4%nWWo~3|axZmqY;0*_GcRyqV{2h&WpgiLVPk7>Z*p{VFJE72ZfSI1UoL -QYP)h*<6ay3h000O8`*~JV-ne74NCE%=i3I=vG5`Po0000000000q=6sQ7yxi-a4%nWWo~3|axZmqY; -0*_GcRyqV{2h&WpgiLVPk7>Z*p{VFKuCKWoBt?WiD`eP)h*<6ay3h000O8`*~JV1rwupqyYc`p925@I -{*Lx0000000000q=C2A7yxi-a4%nWWo~3|axZmqY;0*_GcRyqV{2h&Wpgicb8KI2VRU0?UubW0bZ%j7 -WiMY}X>MtBUtcb8c~DCM0u%!j000080Q-4XQ$B=C*0Bfx0528*073u&00000000000HlGm*cbqCX>c! -Jc4cm4Z*nhna%^mAVlyvrVPk7yXJvCQb8~E8ZDDj{XkTb=b98QDZDlWCX>D+9Wo>0{bYXO9Z*DGdc~D -CM0u%!j000080Q-4XQ|5}9n%DsV0D}Yo03-ka00000000000HlG%;TQmLX>c!Jc4cm4Z*nhna%^mAVl -yvwbZKlaUtei%X>?y-E^v8JO928D0~7!N00;p4c~(<%5t5pw2LJ##6951v00000000000001_f#2g80 -B~t=FJE?LZe(wAFLiQkY-wUMFLiWjY%gPPZf<2`bZKvHE^v8JO928D0~7!N00;p4c~(;j&s+yy0ssI- -1^@sd00000000000001_fywI_0B~t=FJE?LZe(wAFLiQkY-wUMFLiWjY%g$fZ+LkwaCuNm0Rj{Q6aWA -K2mt$eR#Q#`_MiL!008m<001EX0000000000005+cX6_gOaA|NaUv_0~WN&gWb#iQMX<{=kb#!TLFL8 -Bcb!9Gac~DCM0u%!j000080Q-4XQ<7w_RO$r)02>eh03!eZ00000000000HlGT?-&4ZX>c!Jc4cm4Z* -nhna%^mAVlyvwbZKlaa%FLKWpi{caCuNm0Rj{Q6aWAK2mt$eR#P@8)wEy*006cP001Na00000000000 -05+c%=H)maA|NaUv_0~WN&gWb#iQMX<{=kb#!TLFLGsbaBpsNWiD`eP)h*<6ay3h000O8`*~JVmFNkb -&=vpyk5d2uApigX0000000000q=9bx7yxi-a4%nWWo~3|axZmqY;0*_GcR>?X>2cYWpr|RE^v8JO928 -D0~7!N00;p4c~(=TlKaP}761SlLjV9E00000000000001_fqfDg0B~t=FJE?LZe(wAFLiQkY-wUMFLi -WjY%gc!Jc4 -cm4Z*nhna%^mAVlyvwbZKlab8~ETa$#<2c3jhEUCjbB=0 -0000000000001_fweIi0B~t=FJE?LZe(wAFLiQkY-wUMFLiWjY%g?aZDntDbS`jtP)h*<6ay3h000O8 -`*~JV=c|?Zr4j%D-!=dM9{>OV0000000000q=B|Q831r;a4%nWWo~3|axZmqY;0*_GcR>?X>2cba%?V -ec~DCM0u%!j000080Q-4XQ=&${ali)v02~zn03ZMW00000000000HlGKP#FMlX>c!Jc4cm4Z*nhna%^ -mAVlyvwbZKlacVTICE^v8JO928D0~7!N00;p4c~(<+&>wL!3jhF9DF6T@00000000000001_ftFYq0B -~t=FJE?LZe(wAFLz~PWo~0{WNB_^b1z?CX>MtBUtcb8c~DCM0u%!j000080Q-4XQ^&0civc!Jc4cm4Z*nhpWnyJ+V{c?>ZfA2ZY++($Y;!Jfc~DCM0u%!j00008 -0Q-4XQ&4{~A4CEG02u`U03-ka00000000000HlFWY8e1c!Jc4cm4Z*nhpWnyJ+V{c?>ZfA2ZZEI{ -{Vr6V|E^v8JO928D0~7!N00;p4c~(Mn8FL+;db7gX0WMyV)Ze?UHaCuNm1qJ{B005Z*nE_CM0 -06po82|tP -""" - - -if __name__ == "__main__": - main() diff --git a/pythonFiles/install_debugpy.py b/pythonFiles/install_debugpy.py deleted file mode 100644 index cabb620ea1f2..000000000000 --- a/pythonFiles/install_debugpy.py +++ /dev/null @@ -1,66 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -import io -import json -import os -import urllib.request as url_lib -import zipfile - -from packaging.version import parse as version_parser - -EXTENSION_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) -DEBUGGER_DEST = os.path.join(EXTENSION_ROOT, "pythonFiles", "lib", "python") -DEBUGGER_PACKAGE = "debugpy" -DEBUGGER_PYTHON_ABI_VERSIONS = ("cp310",) -DEBUGGER_VERSION = "1.6.7" # can also be "latest" - - -def _contains(s, parts=()): - return any(p in s for p in parts) - - -def _get_package_data(): - json_uri = "https://pypi.org/pypi/{0}/json".format(DEBUGGER_PACKAGE) - # Response format: https://warehouse.readthedocs.io/api-reference/json/#project - # Release metadata format: https://github.com/pypa/interoperability-peps/blob/master/pep-0426-core-metadata.rst - with url_lib.urlopen(json_uri) as response: - return json.loads(response.read()) - - -def _get_debugger_wheel_urls(data, version): - return list( - r["url"] - for r in data["releases"][version] - if _contains(r["url"], DEBUGGER_PYTHON_ABI_VERSIONS) - ) - - -def _download_and_extract(root, url, version): - root = os.getcwd() if root is None or root == "." else root - print(url) - with url_lib.urlopen(url) as response: - data = response.read() - with zipfile.ZipFile(io.BytesIO(data), "r") as wheel: - for zip_info in wheel.infolist(): - # Ignore dist info since we are merging multiple wheels - if ".dist-info/" in zip_info.filename: - continue - print("\t" + zip_info.filename) - wheel.extract(zip_info.filename, root) - - -def main(root): - data = _get_package_data() - - if DEBUGGER_VERSION == "latest": - use_version = max(data["releases"].keys(), key=version_parser) - else: - use_version = DEBUGGER_VERSION - - for url in _get_debugger_wheel_urls(data, use_version): - _download_and_extract(root, url, use_version) - - -if __name__ == "__main__": - main(DEBUGGER_DEST) diff --git a/pythonFiles/jedilsp_requirements/requirements.in b/pythonFiles/jedilsp_requirements/requirements.in deleted file mode 100644 index 7ad7ca14fa90..000000000000 --- a/pythonFiles/jedilsp_requirements/requirements.in +++ /dev/null @@ -1,8 +0,0 @@ -# This file is used to generate requirements.txt. -# To update requirements.txt, run the following commands. -# Use Python 3.7 when creating the environment or using pip-tools -# 1) pip install pip-tools -# 2) pip-compile --generate-hashes --upgrade pythonFiles\jedilsp_requirements\requirements.in - -jedi-language-server>=0.34.3 -pygls>=0.10.3 diff --git a/pythonFiles/jedilsp_requirements/requirements.txt b/pythonFiles/jedilsp_requirements/requirements.txt deleted file mode 100644 index 062b037e0783..000000000000 --- a/pythonFiles/jedilsp_requirements/requirements.txt +++ /dev/null @@ -1,105 +0,0 @@ -# -# This file is autogenerated by pip-compile with Python 3.7 -# by the following command: -# -# pip-compile --generate-hashes 'pythonFiles\jedilsp_requirements\requirements.in' -# -attrs==22.2.0 \ - --hash=sha256:29e95c7f6778868dbd49170f98f8818f78f3dc5e0e37c0b1f474e3561b240836 \ - --hash=sha256:c9227bfc2f01993c03f68db37d1d15c9690188323c067c641f1a35ca58185f99 - # via - # cattrs - # lsprotocol -cattrs==22.2.0 \ - --hash=sha256:bc12b1f0d000b9f9bee83335887d532a1d3e99a833d1bf0882151c97d3e68c21 \ - --hash=sha256:f0eed5642399423cf656e7b66ce92cdc5b963ecafd041d1b24d136fdde7acf6d - # via lsprotocol -docstring-to-markdown==0.11 \ - --hash=sha256:01900aee1bc7fde5aacaf319e517a5e1d4f0bf04e401373c08d28fcf79bfb73b \ - --hash=sha256:5b1da2c89d9d0d09b955dec0ee111284ceadd302a938a03ed93f66e09134f9b5 - # via jedi-language-server -exceptiongroup==1.1.0 \ - --hash=sha256:327cbda3da756e2de031a3107b81ab7b3770a602c4d16ca618298c526f4bec1e \ - --hash=sha256:bcb67d800a4497e1b404c2dd44fca47d3b7a5e5433dbab67f96c1a685cdfdf23 - # via cattrs -importlib-metadata==3.10.1 \ - --hash=sha256:2ec0faae539743ae6aaa84b49a169670a465f7f5d64e6add98388cc29fd1f2f6 \ - --hash=sha256:c9356b657de65c53744046fa8f7358afe0714a1af7d570c00c3835c2d724a7c1 - # via jedi-language-server -jedi==0.18.2 \ - --hash=sha256:203c1fd9d969ab8f2119ec0a3342e0b49910045abe6af0a3ae83a5764d54639e \ - --hash=sha256:bae794c30d07f6d910d32a7048af09b5a39ed740918da923c6b780790ebac612 - # via jedi-language-server -jedi-language-server==0.40.0 \ - --hash=sha256:53e590400b5cd2f6e363e77a4d824b1883798994b731cb0b4370d103748d30e2 \ - --hash=sha256:bacbae2930b6a8a0f1f284c211672fceec94b4808b0415d1c3352fa4b1ac5ad6 - # via -r pythonFiles\jedilsp_requirements\requirements.in -lsprotocol==2022.0.0a10 \ - --hash=sha256:2cd78770b7a4ec979f3ee3761265effd50ea0f5e858ce21bf2fba972e1783c50 \ - --hash=sha256:ef516aec43c2b3c8debc06e84558ea9a64c36d635422d1614fd7fd2a45b1d291 - # via - # jedi-language-server - # pygls -parso==0.8.3 \ - --hash=sha256:8c07be290bb59f03588915921e29e8a50002acaf2cdc5fa0e0114f91709fafa0 \ - --hash=sha256:c001d4636cd3aecdaf33cbb40aebb59b094be2a74c556778ef5576c175e19e75 - # via jedi -pydantic==1.10.4 \ - --hash=sha256:05a81b006be15655b2a1bae5faa4280cf7c81d0e09fcb49b342ebf826abe5a72 \ - --hash=sha256:0b53e1d41e97063d51a02821b80538053ee4608b9a181c1005441f1673c55423 \ - --hash=sha256:2b3ce5f16deb45c472dde1a0ee05619298c864a20cded09c4edd820e1454129f \ - --hash=sha256:2e82a6d37a95e0b1b42b82ab340ada3963aea1317fd7f888bb6b9dfbf4fff57c \ - --hash=sha256:301d626a59edbe5dfb48fcae245896379a450d04baeed50ef40d8199f2733b06 \ - --hash=sha256:39f4a73e5342b25c2959529f07f026ef58147249f9b7431e1ba8414a36761f53 \ - --hash=sha256:4948f264678c703f3877d1c8877c4e3b2e12e549c57795107f08cf70c6ec7774 \ - --hash=sha256:4b05697738e7d2040696b0a66d9f0a10bec0efa1883ca75ee9e55baf511909d6 \ - --hash=sha256:51bdeb10d2db0f288e71d49c9cefa609bca271720ecd0c58009bd7504a0c464c \ - --hash=sha256:55b1625899acd33229c4352ce0ae54038529b412bd51c4915349b49ca575258f \ - --hash=sha256:572066051eeac73d23f95ba9a71349c42a3e05999d0ee1572b7860235b850cc6 \ - --hash=sha256:6a05a9db1ef5be0fe63e988f9617ca2551013f55000289c671f71ec16f4985e3 \ - --hash=sha256:6dc1cc241440ed7ca9ab59d9929075445da6b7c94ced281b3dd4cfe6c8cff817 \ - --hash=sha256:6e7124d6855b2780611d9f5e1e145e86667eaa3bd9459192c8dc1a097f5e9903 \ - --hash=sha256:75d52162fe6b2b55964fbb0af2ee58e99791a3138588c482572bb6087953113a \ - --hash=sha256:78cec42b95dbb500a1f7120bdf95c401f6abb616bbe8785ef09887306792e66e \ - --hash=sha256:7feb6a2d401f4d6863050f58325b8d99c1e56f4512d98b11ac64ad1751dc647d \ - --hash=sha256:8775d4ef5e7299a2f4699501077a0defdaac5b6c4321173bcb0f3c496fbadf85 \ - --hash=sha256:887ca463c3bc47103c123bc06919c86720e80e1214aab79e9b779cda0ff92a00 \ - --hash=sha256:9193d4f4ee8feca58bc56c8306bcb820f5c7905fd919e0750acdeeeef0615b28 \ - --hash=sha256:983e720704431a6573d626b00662eb78a07148c9115129f9b4351091ec95ecc3 \ - --hash=sha256:990406d226dea0e8f25f643b370224771878142155b879784ce89f633541a024 \ - --hash=sha256:9cbdc268a62d9a98c56e2452d6c41c0263d64a2009aac69246486f01b4f594c4 \ - --hash=sha256:a48f1953c4a1d9bd0b5167ac50da9a79f6072c63c4cef4cf2a3736994903583e \ - --hash=sha256:a9a6747cac06c2beb466064dda999a13176b23535e4c496c9d48e6406f92d42d \ - --hash=sha256:a9f2de23bec87ff306aef658384b02aa7c32389766af3c5dee9ce33e80222dfa \ - --hash=sha256:b5635de53e6686fe7a44b5cf25fcc419a0d5e5c1a1efe73d49d48fe7586db854 \ - --hash=sha256:b6f9d649892a6f54a39ed56b8dfd5e08b5f3be5f893da430bed76975f3735d15 \ - --hash=sha256:b9a3859f24eb4e097502a3be1fb4b2abb79b6103dd9e2e0edb70613a4459a648 \ - --hash=sha256:cd8702c5142afda03dc2b1ee6bc358b62b3735b2cce53fc77b31ca9f728e4bc8 \ - --hash=sha256:d7b5a3821225f5c43496c324b0d6875fde910a1c2933d726a743ce328fbb2a8c \ - --hash=sha256:d88c4c0e5c5dfd05092a4b271282ef0588e5f4aaf345778056fc5259ba098857 \ - --hash=sha256:eb992a1ef739cc7b543576337bebfc62c0e6567434e522e97291b251a41dad7f \ - --hash=sha256:f2f7eb6273dd12472d7f218e1fef6f7c7c2f00ac2e1ecde4db8824c457300416 \ - --hash=sha256:fdf88ab63c3ee282c76d652fc86518aacb737ff35796023fae56a65ced1a5978 \ - --hash=sha256:fdf8d759ef326962b4678d89e275ffc55b7ce59d917d9f72233762061fd04a2d - # via jedi-language-server -pygls==1.0.0 \ - --hash=sha256:3414594ac29ff3ab990f004c675d1077e4e2659eae5cc3ae67cc6fa4d861e342 \ - --hash=sha256:c2a1c22e30028f7ca9d3f0a04da8eef29f0f1701bdbd97d8614d8e1e6711f336 - # via - # -r pythonFiles\jedilsp_requirements\requirements.in - # jedi-language-server -typeguard==2.13.3 \ - --hash=sha256:00edaa8da3a133674796cf5ea87d9f4b4c367d77476e185e80251cc13dfbb8c4 \ - --hash=sha256:5e3e3be01e887e7eafae5af63d1f36c849aaa94e3a0112097312aabfa16284f1 - # via pygls -typing-extensions==4.4.0 \ - --hash=sha256:1511434bb92bf8dd198c12b1cc812e800d4181cfcb867674e0f8279cc93087aa \ - --hash=sha256:16fa4864408f655d35ec496218b85f79b3437c829e93320c7c9215ccfd92489e - # via - # cattrs - # importlib-metadata - # pydantic -zipp==3.12.0 \ - --hash=sha256:73efd63936398aac78fd92b6f4865190119d6c91b531532e798977ea8dd402eb \ - --hash=sha256:9eb0a4c5feab9b08871db0d672745b53450d7f26992fd1e4653aa43345e97b86 - # via importlib-metadata diff --git a/pythonFiles/normalizeSelection.py b/pythonFiles/normalizeSelection.py deleted file mode 100644 index 35bc42d6e6fe..000000000000 --- a/pythonFiles/normalizeSelection.py +++ /dev/null @@ -1,143 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -import ast -import json -import re -import sys -import textwrap - - -def split_lines(source): - """ - Split selection lines in a version-agnostic way. - - Python grammar only treats \r, \n, and \r\n as newlines. - But splitlines() in Python 3 has a much larger list: for example, it also includes \v, \f. - As such, this function will split lines across all Python versions. - """ - return re.split(r"[\n\r]+", source) - - -def _get_statements(selection): - """ - Process a multiline selection into a list of its top-level statements. - This will remove empty newlines around and within the selection, dedent it, - and split it using the result of `ast.parse()`. - """ - - # Remove blank lines within the selection to prevent the REPL from thinking the block is finished. - lines = (line for line in split_lines(selection) if line.strip() != "") - - # Dedent the selection and parse it using the ast module. - # Note that leading comments in the selection will be discarded during parsing. - source = textwrap.dedent("\n".join(lines)) - tree = ast.parse(source) - - # We'll need the dedented lines to rebuild the selection. - lines = split_lines(source) - - # Get the line ranges for top-level blocks returned from parsing the dedented text - # and split the selection accordingly. - # tree.body is a list of AST objects, which we rely on to extract top-level statements. - # If we supported Python 3.8+ only we could use the lineno and end_lineno attributes of each object - # to get the boundaries of each block. - # However, earlier Python versions only have the lineno attribute, which is the range start position (1-indexed). - # Therefore, to retrieve the end line of each block in a version-agnostic way we need to do - # `end = next_block.lineno - 1` - # for all blocks except the last one, which will will just run until the last line. - ends = [] - for node in tree.body[1:]: - line_end = node.lineno - 1 - # Special handling of decorators: - # In Python 3.8 and higher, decorators are not taken into account in the value returned by lineno, - # and we have to use the length of the decorator_list array to compute the actual start line. - # Before that, lineno takes into account decorators, so this offset check is unnecessary. - # Also, not all AST objects can have decorators. - if hasattr(node, "decorator_list") and sys.version_info >= (3, 8): - # Using getattr instead of node.decorator_list or pyright will complain about an unknown member. - line_end -= len(getattr(node, "decorator_list")) - ends.append(line_end) - ends.append(len(lines)) - - for node, end in zip(tree.body, ends): - # Given this selection: - # 1: if (m > 0 and - # 2: n < 3): - # 3: print('foo') - # 4: value = 'bar' - # - # The first block would have lineno = 1,and the second block lineno = 4 - start = node.lineno - 1 - - # Special handling of decorators similar to what's above. - if hasattr(node, "decorator_list") and sys.version_info >= (3, 8): - # Using getattr instead of node.decorator_list or pyright will complain about an unknown member. - start -= len(getattr(node, "decorator_list")) - block = "\n".join(lines[start:end]) - - # If the block is multiline, add an extra newline character at its end. - # This way, when joining blocks back together, there will be a blank line between each multiline statement - # and no blank lines between single-line statements, or it would look like this: - # >>> x = 22 - # >>> - # >>> total = x + 30 - # >>> - # Note that for the multiline parentheses case this newline is redundant, - # since the closing parenthesis terminates the statement already. - # This means that for this pattern we'll end up with: - # >>> x = [ - # ... 1 - # ... ] - # >>> - # >>> y = [ - # ... 2 - # ...] - if end - start > 1: - block += "\n" - - yield block - - -def normalize_lines(selection): - """ - Normalize the text selection received from the extension. - - If it is a single line selection, dedent it and append a newline and - send it back to the extension. - Otherwise, sanitize the multiline selection before returning it: - split it in a list of top-level statements - and add newlines between each of them so the REPL knows where each block ends. - """ - try: - # Parse the selection into a list of top-level blocks. - # We don't differentiate between single and multiline statements - # because it's not a perf bottleneck, - # and the overhead from splitting and rejoining strings in the multiline case is one-off. - statements = _get_statements(selection) - - # Insert a newline between each top-level statement, and append a newline to the selection. - source = "\n".join(statements) + "\n" - except: - # If there's a problem when parsing statements, - # append a blank line to end the block and send it as-is. - source = selection + "\n\n" - - return source - - -if __name__ == "__main__": - # Content is being sent from the extension as a JSON object. - # Decode the data from the raw bytes. - stdin = sys.stdin if sys.version_info < (3,) else sys.stdin.buffer - raw = stdin.read() - contents = json.loads(raw.decode("utf-8")) - - normalized = normalize_lines(contents["code"]) - - # Send the normalized code back to the extension in a JSON object. - data = json.dumps({"normalized": normalized}) - - stdout = sys.stdout if sys.version_info < (3,) else sys.stdout.buffer - stdout.write(data.encode("utf-8")) - stdout.close() diff --git a/pythonFiles/printEnvVariablesToFile.py b/pythonFiles/printEnvVariablesToFile.py deleted file mode 100644 index be966bcac28c..000000000000 --- a/pythonFiles/printEnvVariablesToFile.py +++ /dev/null @@ -1,13 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -import os -import json -import sys - - -# Last argument is the target file into which we'll write the env variables as json. -json_file = sys.argv[-1] - -with open(json_file, "w") as outfile: - json.dump(dict(os.environ), outfile) diff --git a/pythonFiles/pyproject.toml b/pythonFiles/pyproject.toml deleted file mode 100644 index 56237999e603..000000000000 --- a/pythonFiles/pyproject.toml +++ /dev/null @@ -1,36 +0,0 @@ -[tool.black] -exclude = ''' - -( - /( - .data - | .vscode - | lib - )/ -) -''' - -[tool.pyright] -exclude = ['lib'] -extraPaths = ['lib/python', 'lib/jedilsp'] -ignore = [ - # Ignore all pre-existing code with issues - 'get-pip.py', - 'install_debugpy.py', - 'tensorboard_launcher.py', - 'testlauncher.py', - 'visualstudio_py_testlauncher.py', - 'testing_tools/unittest_discovery.py', - 'testing_tools/adapter/util.py', - 'testing_tools/adapter/pytest/_discovery.py', - 'testing_tools/adapter/pytest/_pytest_item.py', - 'tests/debug_adapter/test_install_debugpy.py', - 'tests/testing_tools/adapter/.data', - 'tests/testing_tools/adapter/test___main__.py', - 'tests/testing_tools/adapter/test_discovery.py', - 'tests/testing_tools/adapter/test_functional.py', - 'tests/testing_tools/adapter/test_report.py', - 'tests/testing_tools/adapter/test_util.py', - 'tests/testing_tools/adapter/pytest/test_cli.py', - 'tests/testing_tools/adapter/pytest/test_discovery.py', -] diff --git a/pythonFiles/run-jedi-language-server.py b/pythonFiles/run-jedi-language-server.py deleted file mode 100644 index 31095121409f..000000000000 --- a/pythonFiles/run-jedi-language-server.py +++ /dev/null @@ -1,11 +0,0 @@ -import sys -import os - -# Add the lib path to our sys path so jedi_language_server can find its references -EXTENSION_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) -sys.path.insert(0, os.path.join(EXTENSION_ROOT, "pythonFiles", "lib", "jedilsp")) - - -from jedi_language_server.cli import cli - -sys.exit(cli()) diff --git a/pythonFiles/testing_tools/adapter/__main__.py b/pythonFiles/testing_tools/adapter/__main__.py deleted file mode 100644 index 218456897df6..000000000000 --- a/pythonFiles/testing_tools/adapter/__main__.py +++ /dev/null @@ -1,105 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -from __future__ import absolute_import - -import argparse -import sys - -from . import pytest, report -from .errors import UnsupportedCommandError, UnsupportedToolError - -TOOLS = { - "pytest": { - "_add_subparser": pytest.add_cli_subparser, - "discover": pytest.discover, - }, -} -REPORTERS = { - "discover": report.report_discovered, -} - - -def parse_args( - # the args to parse - argv=sys.argv[1:], - # the program name - prog=sys.argv[0], -): - """ - Return the subcommand & tool to run, along with its args. - - This defines the standard CLI for the different testing frameworks. - """ - parser = argparse.ArgumentParser( - description="Run Python testing operations.", - prog=prog, - # ... - ) - cmdsubs = parser.add_subparsers(dest="cmd") - - # Add "run" and "debug" subcommands when ready. - for cmdname in ["discover"]: - sub = cmdsubs.add_parser(cmdname) - subsubs = sub.add_subparsers(dest="tool") - for toolname in sorted(TOOLS): - try: - add_subparser = TOOLS[toolname]["_add_subparser"] - except KeyError: - continue - subsub = add_subparser(cmdname, toolname, subsubs) - if cmdname == "discover": - subsub.add_argument("--simple", action="store_true") - subsub.add_argument( - "--no-hide-stdio", dest="hidestdio", action="store_false" - ) - subsub.add_argument("--pretty", action="store_true") - - # Parse the args! - if "--" in argv: - sep_index = argv.index("--") - toolargs = argv[sep_index + 1 :] - argv = argv[:sep_index] - else: - toolargs = [] - args = parser.parse_args(argv) - ns = vars(args) - - cmd = ns.pop("cmd") - if not cmd: - parser.error("missing command") - - tool = ns.pop("tool") - if not tool: - parser.error("missing tool") - - return tool, cmd, ns, toolargs - - -def main( - toolname, - cmdname, - subargs, - toolargs, - # internal args (for testing): - _tools=TOOLS, - _reporters=REPORTERS, -): - try: - tool = _tools[toolname] - except KeyError: - raise UnsupportedToolError(toolname) - - try: - run = tool[cmdname] - report_result = _reporters[cmdname] - except KeyError: - raise UnsupportedCommandError(cmdname) - - parents, result = run(toolargs, **subargs) - report_result(result, parents, **subargs) - - -if __name__ == "__main__": - tool, cmd, subargs, toolargs = parse_args() - main(tool, cmd, subargs, toolargs) diff --git a/pythonFiles/testing_tools/adapter/discovery.py b/pythonFiles/testing_tools/adapter/discovery.py deleted file mode 100644 index 798aea1e93f1..000000000000 --- a/pythonFiles/testing_tools/adapter/discovery.py +++ /dev/null @@ -1,117 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -from __future__ import absolute_import, print_function - -import re - -from .util import fix_fileid, DIRNAME, NORMCASE -from .info import ParentInfo - - -FILE_ID_RE = re.compile( - r""" - ^ - (?: - ( .* [.] (?: py | txt ) \b ) # .txt for doctest files - ( [^.] .* )? - ) - $ - """, - re.VERBOSE, -) - - -def fix_nodeid( - nodeid, - kind, - rootdir=None, - # *, - _fix_fileid=fix_fileid, -): - if not nodeid: - raise ValueError("missing nodeid") - if nodeid == ".": - return nodeid - - fileid = nodeid - remainder = "" - if kind not in ("folder", "file"): - m = FILE_ID_RE.match(nodeid) - if m: - fileid, remainder = m.groups() - elif len(nodeid) > 1: - fileid = nodeid[:2] - remainder = nodeid[2:] - fileid = _fix_fileid(fileid, rootdir) - return fileid + (remainder or "") - - -class DiscoveredTests(object): - """A container for the discovered tests and their parents.""" - - def __init__(self): - self.reset() - - def __len__(self): - return len(self._tests) - - def __getitem__(self, index): - return self._tests[index] - - @property - def parents(self): - return sorted( - self._parents.values(), - # Sort by (name, id). - key=lambda p: (NORMCASE(p.root or p.name), p.id), - ) - - def reset(self): - """Clear out any previously discovered tests.""" - self._parents = {} - self._tests = [] - - def add_test(self, test, parents): - """Add the given test and its parents.""" - parentid = self._ensure_parent(test.path, parents) - # Updating the parent ID and the test ID aren't necessary if the - # provided test and parents (from the test collector) are - # properly generated. However, we play it safe here. - test = test._replace( - # Clean up the ID. - id=fix_nodeid(test.id, "test", test.path.root), - parentid=parentid, - ) - self._tests.append(test) - - def _ensure_parent( - self, - path, - parents, - # *, - _dirname=DIRNAME, - ): - rootdir = path.root - relpath = path.relfile - - _parents = iter(parents) - nodeid, name, kind = next(_parents) - # As in add_test(), the node ID *should* already be correct. - nodeid = fix_nodeid(nodeid, kind, rootdir) - _parentid = nodeid - for parentid, parentname, parentkind in _parents: - # As in add_test(), the parent ID *should* already be correct. - parentid = fix_nodeid(parentid, kind, rootdir) - if kind in ("folder", "file"): - info = ParentInfo(nodeid, kind, name, rootdir, relpath, parentid) - relpath = _dirname(relpath) - else: - info = ParentInfo(nodeid, kind, name, rootdir, None, parentid) - self._parents[(rootdir, nodeid)] = info - nodeid, name, kind = parentid, parentname, parentkind - assert nodeid == "." - info = ParentInfo(nodeid, kind, name=rootdir) - self._parents[(rootdir, nodeid)] = info - - return _parentid diff --git a/pythonFiles/testing_tools/adapter/errors.py b/pythonFiles/testing_tools/adapter/errors.py deleted file mode 100644 index 3e6ae5189cb8..000000000000 --- a/pythonFiles/testing_tools/adapter/errors.py +++ /dev/null @@ -1,16 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - - -class UnsupportedToolError(ValueError): - def __init__(self, tool): - msg = "unsupported tool {!r}".format(tool) - super(UnsupportedToolError, self).__init__(msg) - self.tool = tool - - -class UnsupportedCommandError(ValueError): - def __init__(self, cmd): - msg = "unsupported cmd {!r}".format(cmd) - super(UnsupportedCommandError, self).__init__(msg) - self.cmd = cmd diff --git a/pythonFiles/testing_tools/adapter/info.py b/pythonFiles/testing_tools/adapter/info.py deleted file mode 100644 index d518a29dd97a..000000000000 --- a/pythonFiles/testing_tools/adapter/info.py +++ /dev/null @@ -1,119 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -from collections import namedtuple - - -class SingleTestPath(namedtuple("TestPath", "root relfile func sub")): - """Where to find a single test.""" - - def __new__(cls, root, relfile, func, sub=None): - self = super(SingleTestPath, cls).__new__( - cls, - str(root) if root else None, - str(relfile) if relfile else None, - str(func) if func else None, - [str(s) for s in sub] if sub else None, - ) - return self - - def __init__(self, *args, **kwargs): - if self.root is None: - raise TypeError("missing id") - if self.relfile is None: - raise TypeError("missing kind") - # self.func may be None (e.g. for doctests). - # self.sub may be None. - - -class ParentInfo(namedtuple("ParentInfo", "id kind name root relpath parentid")): - KINDS = ("folder", "file", "suite", "function", "subtest") - - def __new__(cls, id, kind, name, root=None, relpath=None, parentid=None): - self = super(ParentInfo, cls).__new__( - cls, - id=str(id) if id else None, - kind=str(kind) if kind else None, - name=str(name) if name else None, - root=str(root) if root else None, - relpath=str(relpath) if relpath else None, - parentid=str(parentid) if parentid else None, - ) - return self - - def __init__(self, *args, **kwargs): - if self.id is None: - raise TypeError("missing id") - if self.kind is None: - raise TypeError("missing kind") - if self.kind not in self.KINDS: - raise ValueError("unsupported kind {!r}".format(self.kind)) - if self.name is None: - raise TypeError("missing name") - if self.root is None: - if self.parentid is not None or self.kind != "folder": - raise TypeError("missing root") - if self.relpath is not None: - raise TypeError("unexpected relpath {}".format(self.relpath)) - elif self.parentid is None: - raise TypeError("missing parentid") - elif self.relpath is None and self.kind in ("folder", "file"): - raise TypeError("missing relpath") - - -class SingleTestInfo( - namedtuple("TestInfo", "id name path source markers parentid kind") -): - """Info for a single test.""" - - MARKERS = ("skip", "skip-if", "expected-failure") - KINDS = ("function", "doctest") - - def __new__(cls, id, name, path, source, markers, parentid, kind="function"): - self = super(SingleTestInfo, cls).__new__( - cls, - str(id) if id else None, - str(name) if name else None, - path or None, - str(source) if source else None, - [str(marker) for marker in markers or ()], - str(parentid) if parentid else None, - str(kind) if kind else None, - ) - return self - - def __init__(self, *args, **kwargs): - if self.id is None: - raise TypeError("missing id") - if self.name is None: - raise TypeError("missing name") - if self.path is None: - raise TypeError("missing path") - if self.source is None: - raise TypeError("missing source") - else: - srcfile, _, lineno = self.source.rpartition(":") - if not srcfile or not lineno or int(lineno) < 0: - raise ValueError("bad source {!r}".format(self.source)) - if self.markers: - badmarkers = [m for m in self.markers if m not in self.MARKERS] - if badmarkers: - raise ValueError("unsupported markers {!r}".format(badmarkers)) - if self.parentid is None: - raise TypeError("missing parentid") - if self.kind is None: - raise TypeError("missing kind") - elif self.kind not in self.KINDS: - raise ValueError("unsupported kind {!r}".format(self.kind)) - - @property - def root(self): - return self.path.root - - @property - def srcfile(self): - return self.source.rpartition(":")[0] - - @property - def lineno(self): - return int(self.source.rpartition(":")[-1]) diff --git a/pythonFiles/testing_tools/adapter/pytest/__init__.py b/pythonFiles/testing_tools/adapter/pytest/__init__.py deleted file mode 100644 index e894f7bcdb8e..000000000000 --- a/pythonFiles/testing_tools/adapter/pytest/__init__.py +++ /dev/null @@ -1,7 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -from __future__ import absolute_import - -from ._cli import add_subparser as add_cli_subparser -from ._discovery import discover diff --git a/pythonFiles/testing_tools/adapter/pytest/_cli.py b/pythonFiles/testing_tools/adapter/pytest/_cli.py deleted file mode 100644 index 3d3eec09a199..000000000000 --- a/pythonFiles/testing_tools/adapter/pytest/_cli.py +++ /dev/null @@ -1,17 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -from __future__ import absolute_import - -from ..errors import UnsupportedCommandError - - -def add_subparser(cmd, name, parent): - """Add a new subparser to the given parent and add args to it.""" - parser = parent.add_parser(name) - if cmd == "discover": - # For now we don't have any tool-specific CLI options to add. - pass - else: - raise UnsupportedCommandError(cmd) - return parser diff --git a/pythonFiles/testing_tools/adapter/pytest/_discovery.py b/pythonFiles/testing_tools/adapter/pytest/_discovery.py deleted file mode 100644 index 4b852ecf81c9..000000000000 --- a/pythonFiles/testing_tools/adapter/pytest/_discovery.py +++ /dev/null @@ -1,112 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -from __future__ import absolute_import, print_function - -import sys - -import pytest - -from .. import discovery, util -from ._pytest_item import parse_item - - -def discover( - pytestargs=None, - hidestdio=False, - # *, - _pytest_main=pytest.main, - _plugin=None, - **_ignored -): - """Return the results of test discovery.""" - if _plugin is None: - _plugin = TestCollector() - - pytestargs = _adjust_pytest_args(pytestargs) - # We use this helper rather than "-pno:terminal" due to possible - # platform-dependent issues. - with util.hide_stdio() if hidestdio else util.noop_cm() as stdio: - ec = _pytest_main(pytestargs, [_plugin]) - # See: https://docs.pytest.org/en/latest/usage.html#possible-exit-codes - if ec == 5: - # No tests were discovered. - pass - elif ec == 1: - # Some tests where collected but with errors. - pass - elif ec != 0: - print( - "equivalent command: {} -m pytest {}".format( - sys.executable, util.shlex_unsplit(pytestargs) - ) - ) - if hidestdio: - print(stdio.getvalue(), file=sys.stderr) - sys.stdout.flush() - raise Exception("pytest discovery failed (exit code {})".format(ec)) - if not _plugin._started: - print( - "equivalent command: {} -m pytest {}".format( - sys.executable, util.shlex_unsplit(pytestargs) - ) - ) - if hidestdio: - print(stdio.getvalue(), file=sys.stderr) - sys.stdout.flush() - raise Exception("pytest discovery did not start") - return ( - _plugin._tests.parents, - list(_plugin._tests), - ) - - -def _adjust_pytest_args(pytestargs): - """Return a corrected copy of the given pytest CLI args.""" - pytestargs = list(pytestargs) if pytestargs else [] - # Duplicate entries should be okay. - pytestargs.insert(0, "--collect-only") - # TODO: pull in code from: - # src/client/testing/pytest/services/discoveryService.ts - # src/client/testing/pytest/services/argsService.ts - return pytestargs - - -class TestCollector(object): - """This is a pytest plugin that collects the discovered tests.""" - - @classmethod - def parse_item(cls, item): - return parse_item(item) - - def __init__(self, tests=None): - if tests is None: - tests = discovery.DiscoveredTests() - self._tests = tests - self._started = False - - # Relevant plugin hooks: - # https://docs.pytest.org/en/latest/reference.html#collection-hooks - - def pytest_collection_modifyitems(self, session, config, items): - self._started = True - self._tests.reset() - for item in items: - test, parents = self.parse_item(item) - if test is not None: - self._tests.add_test(test, parents) - - # This hook is not specified in the docs, so we also provide - # the "modifyitems" hook just in case. - def pytest_collection_finish(self, session): - self._started = True - try: - items = session.items - except AttributeError: - # TODO: Is there an alternative? - return - self._tests.reset() - for item in items: - test, parents = self.parse_item(item) - if test is not None: - self._tests.add_test(test, parents) diff --git a/pythonFiles/testing_tools/adapter/pytest/_pytest_item.py b/pythonFiles/testing_tools/adapter/pytest/_pytest_item.py deleted file mode 100644 index ccfe14122316..000000000000 --- a/pythonFiles/testing_tools/adapter/pytest/_pytest_item.py +++ /dev/null @@ -1,610 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. -""" -During "collection", pytest finds all the tests it supports. These are -called "items". The process is top-down, mostly tracing down through -the file system. Aside from its own machinery, pytest supports hooks -that find tests. Effectively, pytest starts with a set of "collectors"; -objects that can provide a list of tests and sub-collectors. All -collectors in the resulting tree are visited and the tests aggregated. -For the most part, each test's (and collector's) parent is identified -as the collector that collected it. - -Collectors and items are collectively identified as "nodes". The pytest -API relies on collector and item objects providing specific methods and -attributes. In addition to corresponding base classes, pytest provides -a number of concrete implementations. - -The following are the known pytest node types: - - Node - Collector - FSCollector - Session (the top-level collector) - File - Module - Package - DoctestTextfile - DoctestModule - PyCollector - (Module) - (...) - Class - UnitTestCase - Instance - Item - Function - TestCaseFunction - DoctestItem - -Here are the unique attrs for those classes: - - Node - name - nodeid (readonly) - config - session - (parent) - the parent node - (fspath) - the file from which the node was collected - ---- - own_marksers - explicit markers (e.g. with @pytest.mark()) - keywords - extra_keyword_matches - - Item - location - where the actual test source code is: (relfspath, lno, fullname) - user_properties - - PyCollector - module - class - instance - obj - - Function - module - class - instance - obj - function - (callspec) - (fixturenames) - funcargs - originalname - w/o decorations, e.g. [...] for parameterized - - DoctestItem - dtest - obj - -When parsing an item, we make use of the following attributes: - -* name -* nodeid -* __class__ - + __name__ -* fspath -* location -* function - + __name__ - + __code__ - + __closure__ -* own_markers -""" - -from __future__ import absolute_import, print_function - -import sys - -import _pytest.doctest -import _pytest.unittest -import pytest - -from ..info import SingleTestInfo, SingleTestPath -from ..util import NORMCASE, PATH_SEP, fix_fileid - - -def should_never_reach_here(item, **extra): - """Indicates a code path we should never reach.""" - print("The Python extension has run into an unexpected situation") - print("while processing a pytest node during test discovery. Please") - print("Please open an issue at:") - print(" https://github.com/microsoft/vscode-python/issues") - print("and paste the following output there.") - print() - for field, info in _summarize_item(item): - print("{}: {}".format(field, info)) - if extra: - print() - print("extra info:") - for name, info in extra.items(): - print("{:10}".format(name + ":"), end="") - if isinstance(info, str): - print(info) - else: - try: - print(*info) - except TypeError: - print(info) - print() - print("traceback:") - import traceback - - traceback.print_stack() - - msg = "Unexpected pytest node (see printed output)." - exc = NotImplementedError(msg) - exc.item = item - return exc - - -def parse_item( - item, - # *, - _get_item_kind=(lambda *a: _get_item_kind(*a)), - _parse_node_id=(lambda *a: _parse_node_id(*a)), - _split_fspath=(lambda *a: _split_fspath(*a)), - _get_location=(lambda *a: _get_location(*a)), -): - """Return (TestInfo, [suite ID]) for the given item. - - The suite IDs, if any, are in parent order with the item's direct - parent at the beginning. The parent of the last suite ID (or of - the test if there are no suites) is the file ID, which corresponds - to TestInfo.path. - - """ - # _debug_item(item, showsummary=True) - kind, _ = _get_item_kind(item) - # Skip plugin generated tests - if kind is None: - return None, None - - if kind == "function" and item.originalname and item.originalname != item.name: - # split out parametrized decorations `node[params]`) before parsing - # and manually attach parametrized portion back in when done. - parameterized = item.name[len(item.originalname) :] - (parentid, parents, fileid, testfunc, _) = _parse_node_id( - item.nodeid[: -len(parameterized)], kind - ) - nodeid = "{}{}".format(parentid, parameterized) - parents = [(parentid, item.originalname, kind)] + parents - name = parameterized[1:-1] or "" - else: - (nodeid, parents, fileid, testfunc, parameterized) = _parse_node_id( - item.nodeid, kind - ) - name = item.name - - # Note: testfunc does not necessarily match item.function.__name__. - # This can result from importing a test function from another module. - - # Figure out the file. - testroot, relfile = _split_fspath(str(item.fspath), fileid, item) - location, fullname = _get_location(item, testroot, relfile) - if kind == "function": - if testfunc and fullname != testfunc + parameterized: - raise should_never_reach_here( - item, - fullname=fullname, - testfunc=testfunc, - parameterized=parameterized, - # ... - ) - elif kind == "doctest": - if testfunc and fullname != testfunc and fullname != "[doctest] " + testfunc: - raise should_never_reach_here( - item, - fullname=fullname, - testfunc=testfunc, - # ... - ) - testfunc = None - - # Sort out the parent. - if parents: - parentid, _, _ = parents[0] - else: - parentid = None - - # Sort out markers. - # See: https://docs.pytest.org/en/latest/reference.html#marks - markers = set() - for marker in getattr(item, "own_markers", []): - if marker.name == "parameterize": - # We've already covered these. - continue - elif marker.name == "skip": - markers.add("skip") - elif marker.name == "skipif": - markers.add("skip-if") - elif marker.name == "xfail": - markers.add("expected-failure") - # We can add support for other markers as we need them? - - test = SingleTestInfo( - id=nodeid, - name=name, - path=SingleTestPath( - root=testroot, - relfile=relfile, - func=testfunc, - sub=[parameterized] if parameterized else None, - ), - source=location, - markers=sorted(markers) if markers else None, - parentid=parentid, - ) - if parents and parents[-1] == (".", None, "folder"): # This should always be true? - parents[-1] = (".", testroot, "folder") - return test, parents - - -def _split_fspath( - fspath, - fileid, - item, - # *, - _normcase=NORMCASE, -): - """Return (testroot, relfile) for the given fspath. - - "relfile" will match "fileid". - """ - # "fileid" comes from nodeid and is always relative to the testroot - # (with a "./" prefix). There are no guarantees about casing, so we - # normcase just be to sure. - relsuffix = fileid[1:] # Drop (only) the "." prefix. - if not _normcase(fspath).endswith(_normcase(relsuffix)): - raise should_never_reach_here( - item, - fspath=fspath, - fileid=fileid, - # ... - ) - testroot = fspath[: -len(fileid) + 1] # Ignore the "./" prefix. - relfile = "." + fspath[-len(fileid) + 1 :] # Keep the pathsep. - return testroot, relfile - - -def _get_location( - item, - testroot, - relfile, - # *, - _matches_relfile=(lambda *a: _matches_relfile(*a)), - _is_legacy_wrapper=(lambda *a: _is_legacy_wrapper(*a)), - _unwrap_decorator=(lambda *a: _unwrap_decorator(*a)), - _pathsep=PATH_SEP, -): - """Return (loc str, fullname) for the given item.""" - # When it comes to normcase, we favor relfile (from item.fspath) - # over item.location in this function. - - srcfile, lineno, fullname = item.location - if _matches_relfile(srcfile, testroot, relfile): - srcfile = relfile - else: - # pytest supports discovery of tests imported from other - # modules. This is reflected by a different filename - # in item.location. - - if _is_legacy_wrapper(srcfile): - srcfile = relfile - unwrapped = _unwrap_decorator(item.function) - if unwrapped is None: - # It was an invalid legacy wrapper so we just say - # "somewhere in relfile". - lineno = None - else: - _srcfile, lineno = unwrapped - if not _matches_relfile(_srcfile, testroot, relfile): - # For legacy wrappers we really expect the wrapped - # function to be in relfile. So here we ignore any - # other file and just say "somewhere in relfile". - lineno = None - elif _matches_relfile(srcfile, testroot, relfile): - srcfile = relfile - # Otherwise we just return the info from item.location as-is. - - if not srcfile.startswith("." + _pathsep): - srcfile = "." + _pathsep + srcfile - - if lineno is None: - lineno = -1 # i.e. "unknown" - - # from pytest, line numbers are 0-based - location = "{}:{}".format(srcfile, int(lineno) + 1) - return location, fullname - - -def _matches_relfile( - srcfile, - testroot, - relfile, - # *, - _normcase=NORMCASE, - _pathsep=PATH_SEP, -): - """Return True if "srcfile" matches the given relfile.""" - testroot = _normcase(testroot) - srcfile = _normcase(srcfile) - relfile = _normcase(relfile) - if srcfile == relfile: - return True - elif srcfile == relfile[len(_pathsep) + 1 :]: - return True - elif srcfile == testroot + relfile[1:]: - return True - else: - return False - - -def _is_legacy_wrapper( - srcfile, - # *, - _pathsep=PATH_SEP, - _pyversion=sys.version_info, -): - """Return True if the test might be wrapped. - - In Python 2 unittest's decorators (e.g. unittest.skip) do not wrap - properly, so we must manually unwrap them. - """ - if _pyversion > (3,): - return False - if (_pathsep + "unittest" + _pathsep + "case.py") not in srcfile: - return False - return True - - -def _unwrap_decorator(func): - """Return (filename, lineno) for the func the given func wraps. - - If the wrapped func cannot be identified then return None. Likewise - for the wrapped filename. "lineno" is None if it cannot be found - but the filename could. - """ - try: - func = func.__closure__[0].cell_contents - except (IndexError, AttributeError): - return None - else: - if not callable(func): - return None - try: - filename = func.__code__.co_filename - except AttributeError: - return None - else: - try: - lineno = func.__code__.co_firstlineno - 1 - except AttributeError: - return (filename, None) - else: - return filename, lineno - - -def _parse_node_id( - testid, - kind, - # *, - _iter_nodes=(lambda *a: _iter_nodes(*a)), -): - """Return the components of the given node ID, in heirarchical order.""" - nodes = iter(_iter_nodes(testid, kind)) - - testid, name, kind = next(nodes) - parents = [] - parameterized = None - if kind == "doctest": - parents = list(nodes) - fileid, _, _ = parents[0] - return testid, parents, fileid, name, parameterized - elif kind is None: - fullname = None - else: - if kind == "subtest": - node = next(nodes) - parents.append(node) - funcid, funcname, _ = node - parameterized = testid[len(funcid) :] - elif kind == "function": - funcname = name - else: - raise should_never_reach_here( - testid, - kind=kind, - # ... - ) - fullname = funcname - - for node in nodes: - parents.append(node) - parentid, name, kind = node - if kind == "file": - fileid = parentid - break - elif fullname is None: - # We don't guess how to interpret the node ID for these tests. - continue - elif kind == "suite": - fullname = name + "." + fullname - else: - raise should_never_reach_here( - testid, - node=node, - # ... - ) - else: - fileid = None - parents.extend(nodes) # Add the rest in as-is. - - return ( - testid, - parents, - fileid, - fullname, - parameterized or "", - ) - - -def _iter_nodes( - testid, - kind, - # *, - _normalize_test_id=(lambda *a: _normalize_test_id(*a)), - _normcase=NORMCASE, - _pathsep=PATH_SEP, -): - """Yield (nodeid, name, kind) for the given node ID and its parents.""" - nodeid, testid = _normalize_test_id(testid, kind) - if len(nodeid) > len(testid): - testid = "." + _pathsep + testid - - parentid, _, name = nodeid.rpartition("::") - if not parentid: - if kind is None: - # This assumes that plugins can generate nodes that do not - # have a parent. All the builtin nodes have one. - yield (nodeid, name, kind) - return - # We expect at least a filename and a name. - raise should_never_reach_here( - nodeid, - # ... - ) - yield (nodeid, name, kind) - - # Extract the suites. - while "::" in parentid: - suiteid = parentid - parentid, _, name = parentid.rpartition("::") - yield (suiteid, name, "suite") - - # Extract the file and folders. - fileid = parentid - raw = testid[: len(fileid)] - _parentid, _, filename = _normcase(fileid).rpartition(_pathsep) - parentid = fileid[: len(_parentid)] - raw, name = raw[: len(_parentid)], raw[-len(filename) :] - yield (fileid, name, "file") - # We're guaranteed at least one (the test root). - while _pathsep in _normcase(parentid): - folderid = parentid - _parentid, _, foldername = _normcase(folderid).rpartition(_pathsep) - parentid = folderid[: len(_parentid)] - raw, name = raw[: len(parentid)], raw[-len(foldername) :] - yield (folderid, name, "folder") - # We set the actual test root later at the bottom of parse_item(). - testroot = None - yield (parentid, testroot, "folder") - - -def _normalize_test_id( - testid, - kind, - # *, - _fix_fileid=fix_fileid, - _pathsep=PATH_SEP, -): - """Return the canonical form for the given node ID.""" - while "::()::" in testid: - testid = testid.replace("::()::", "::") - while ":::" in testid: - testid = testid.replace(":::", "::") - if kind is None: - return testid, testid - orig = testid - - # We need to keep the testid as-is, or else pytest won't recognize - # it when we try to use it later (e.g. to run a test). The only - # exception is that we add a "./" prefix for relative paths. - # Note that pytest always uses "/" as the path separator in IDs. - fileid, sep, remainder = testid.partition("::") - fileid = _fix_fileid(fileid) - if not fileid.startswith("./"): # Absolute "paths" not expected. - raise should_never_reach_here( - testid, - fileid=fileid, - # ... - ) - testid = fileid + sep + remainder - - return testid, orig - - -def _get_item_kind(item): - """Return (kind, isunittest) for the given item.""" - if isinstance(item, _pytest.doctest.DoctestItem): - return "doctest", False - elif isinstance(item, _pytest.unittest.TestCaseFunction): - return "function", True - elif isinstance(item, pytest.Function): - # We *could* be more specific, e.g. "method", "subtest". - return "function", False - else: - return None, False - - -############################# -# useful for debugging - -_FIELDS = [ - "nodeid", - "kind", - "class", - "name", - "fspath", - "location", - "function", - "markers", - "user_properties", - "attrnames", -] - - -def _summarize_item(item): - if not hasattr(item, "nodeid"): - yield "nodeid", item - return - - for field in _FIELDS: - try: - if field == "kind": - yield field, _get_item_kind(item) - elif field == "class": - yield field, item.__class__.__name__ - elif field == "markers": - yield field, item.own_markers - # yield field, list(item.iter_markers()) - elif field == "attrnames": - yield field, dir(item) - else: - yield field, getattr(item, field, "") - except Exception as exc: - yield field, "".format(exc) - - -def _debug_item(item, showsummary=False): - item._debugging = True - try: - summary = dict(_summarize_item(item)) - finally: - item._debugging = False - - if showsummary: - print(item.nodeid) - for key in ( - "kind", - "class", - "name", - "fspath", - "location", - "func", - "markers", - "props", - ): - print(" {:12} {}".format(key, summary[key])) - print() - - return summary diff --git a/pythonFiles/testing_tools/adapter/report.py b/pythonFiles/testing_tools/adapter/report.py deleted file mode 100644 index bacdef7b9a00..000000000000 --- a/pythonFiles/testing_tools/adapter/report.py +++ /dev/null @@ -1,94 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -from __future__ import print_function - -import json - - -def report_discovered( - tests, - parents, - # *, - pretty=False, - simple=False, - _send=print, - **_ignored -): - """Serialize the discovered tests and write to stdout.""" - if simple: - data = [ - { - "id": test.id, - "name": test.name, - "testroot": test.path.root, - "relfile": test.path.relfile, - "lineno": test.lineno, - "testfunc": test.path.func, - "subtest": test.path.sub or None, - "markers": test.markers or [], - } - for test in tests - ] - else: - byroot = {} - for parent in parents: - rootdir = parent.name if parent.root is None else parent.root - try: - root = byroot[rootdir] - except KeyError: - root = byroot[rootdir] = { - "id": rootdir, - "parents": [], - "tests": [], - } - if not parent.root: - root["id"] = parent.id - continue - root["parents"].append( - { - # "id" must match what the testing framework recognizes. - "id": parent.id, - "kind": parent.kind, - "name": parent.name, - "parentid": parent.parentid, - } - ) - if parent.relpath is not None: - root["parents"][-1]["relpath"] = parent.relpath - for test in tests: - # We are guaranteed that the parent was added. - root = byroot[test.path.root] - testdata = { - # "id" must match what the testing framework recognizes. - "id": test.id, - "name": test.name, - # TODO: Add a "kind" field - # (e.g. "unittest", "function", "doctest") - "source": test.source, - "markers": test.markers or [], - "parentid": test.parentid, - } - root["tests"].append(testdata) - data = [ - { - "rootid": byroot[root]["id"], - "root": root, - "parents": byroot[root]["parents"], - "tests": byroot[root]["tests"], - } - for root in sorted(byroot) - ] - - kwargs = {} - if pretty: - # human-formatted - kwargs = dict( - sort_keys=True, - indent=4, - separators=(",", ": "), - # ... - ) - serialized = json.dumps(data, **kwargs) - - _send(serialized) diff --git a/pythonFiles/testing_tools/adapter/util.py b/pythonFiles/testing_tools/adapter/util.py deleted file mode 100644 index c7a178311b8b..000000000000 --- a/pythonFiles/testing_tools/adapter/util.py +++ /dev/null @@ -1,289 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -import contextlib -import io - -try: - from io import StringIO -except ImportError: - from StringIO import StringIO # 2.7 - -import os -import os.path -import sys -import tempfile - - -@contextlib.contextmanager -def noop_cm(): - yield - - -def group_attr_names(attrnames): - grouped = { - "dunder": [], - "private": [], - "constants": [], - "classes": [], - "vars": [], - "other": [], - } - for name in attrnames: - if name.startswith("__") and name.endswith("__"): - group = "dunder" - elif name.startswith("_"): - group = "private" - elif name.isupper(): - group = "constants" - elif name.islower(): - group = "vars" - elif name == name.capitalize(): - group = "classes" - else: - group = "other" - grouped[group].append(name) - return grouped - - -if sys.version_info < (3,): - _str_to_lower = lambda val: val.decode().lower() -else: - _str_to_lower = str.lower - - -############################# -# file paths - -_os_path = os.path -# Uncomment to test Windows behavior on non-windows OS: -# import ntpath as _os_path -PATH_SEP = _os_path.sep -NORMCASE = _os_path.normcase -DIRNAME = _os_path.dirname -BASENAME = _os_path.basename -IS_ABS_PATH = _os_path.isabs -PATH_JOIN = _os_path.join -ABS_PATH = _os_path.abspath - - -def fix_path( - path, - # *, - _pathsep=PATH_SEP, -): - """Return a platform-appropriate path for the given path.""" - if not path: - return "." - return path.replace("/", _pathsep) - - -def fix_relpath( - path, - # *, - _fix_path=fix_path, - _path_isabs=IS_ABS_PATH, - _pathsep=PATH_SEP, -): - """Return a ./-prefixed, platform-appropriate path for the given path.""" - path = _fix_path(path) - if path in (".", ".."): - return path - if not _path_isabs(path): - if not path.startswith("." + _pathsep): - path = "." + _pathsep + path - return path - - -def _resolve_relpath( - path, - rootdir=None, - # *, - _path_isabs=IS_ABS_PATH, - _normcase=NORMCASE, - _pathsep=PATH_SEP, -): - # "path" is expected to use "/" for its path separator, regardless - # of the provided "_pathsep". - - if path.startswith("./"): - return path[2:] - if not _path_isabs(path): - return path - - # Deal with root-dir-as-fileid. - _, sep, relpath = path.partition("/") - if sep and not relpath.replace("/", ""): - return "" - - if rootdir is None: - return None - rootdir = _normcase(rootdir) - if not rootdir.endswith(_pathsep): - rootdir += _pathsep - - if not _normcase(path).startswith(rootdir): - return None - return path[len(rootdir) :] - - -def fix_fileid( - fileid, - rootdir=None, - # *, - normalize=False, - strictpathsep=None, - _pathsep=PATH_SEP, - **kwargs -): - """Return a pathsep-separated file ID ("./"-prefixed) for the given value. - - The file ID may be absolute. If so and "rootdir" is - provided then make the file ID relative. If absolute but "rootdir" - is not provided then leave it absolute. - """ - if not fileid or fileid == ".": - return fileid - - # We default to "/" (forward slash) as the final path sep, since - # that gives us a consistent, cross-platform result. (Windows does - # actually support "/" as a path separator.) Most notably, node IDs - # from pytest use "/" as the path separator by default. - _fileid = fileid.replace(_pathsep, "/") - - relpath = _resolve_relpath( - _fileid, - rootdir, - _pathsep=_pathsep, - # ... - **kwargs - ) - if relpath: # Note that we treat "" here as an absolute path. - _fileid = "./" + relpath - - if normalize: - if strictpathsep: - raise ValueError("cannot normalize *and* keep strict path separator") - _fileid = _str_to_lower(_fileid) - elif strictpathsep: - # We do not use _normcase since we want to preserve capitalization. - _fileid = _fileid.replace("/", _pathsep) - return _fileid - - -############################# -# stdio - - -@contextlib.contextmanager -def _replace_fd(file, target): - """ - Temporarily replace the file descriptor for `file`, - for which sys.stdout or sys.stderr is passed. - """ - try: - fd = file.fileno() - except (AttributeError, io.UnsupportedOperation): - # `file` does not have fileno() so it's been replaced from the - # default sys.stdout, etc. Return with noop. - yield - return - target_fd = target.fileno() - - # Keep the original FD to be restored in the finally clause. - dup_fd = os.dup(fd) - try: - # Point the FD at the target. - os.dup2(target_fd, fd) - try: - yield - finally: - # Point the FD back at the original. - os.dup2(dup_fd, fd) - finally: - os.close(dup_fd) - - -@contextlib.contextmanager -def _replace_stdout(target): - orig = sys.stdout - sys.stdout = target - try: - yield orig - finally: - sys.stdout = orig - - -@contextlib.contextmanager -def _replace_stderr(target): - orig = sys.stderr - sys.stderr = target - try: - yield orig - finally: - sys.stderr = orig - - -if sys.version_info < (3,): - _coerce_unicode = lambda s: unicode(s) -else: - _coerce_unicode = lambda s: s - - -@contextlib.contextmanager -def _temp_io(): - sio = StringIO() - with tempfile.TemporaryFile("r+") as tmp: - try: - yield sio, tmp - finally: - tmp.seek(0) - buff = tmp.read() - sio.write(_coerce_unicode(buff)) - - -@contextlib.contextmanager -def hide_stdio(): - """Swallow stdout and stderr.""" - with _temp_io() as (sio, fileobj): - with _replace_fd(sys.stdout, fileobj): - with _replace_stdout(fileobj): - with _replace_fd(sys.stderr, fileobj): - with _replace_stderr(fileobj): - yield sio - - -############################# -# shell - - -def shlex_unsplit(argv): - """Return the shell-safe string for the given arguments. - - This effectively the equivalent of reversing shlex.split(). - """ - argv = [_quote_arg(a) for a in argv] - return " ".join(argv) - - -try: - from shlex import quote as _quote_arg -except ImportError: - - def _quote_arg(arg): - parts = None - for i, c in enumerate(arg): - if c.isspace(): - pass - elif c == '"': - pass - elif c == "'": - c = "'\"'\"'" - else: - continue - if parts is None: - parts = list(arg) - parts[i] = c - if parts is not None: - arg = "'" + "".join(parts) + "'" - return arg diff --git a/pythonFiles/testing_tools/run_adapter.py b/pythonFiles/testing_tools/run_adapter.py deleted file mode 100644 index 1eeef194f8f5..000000000000 --- a/pythonFiles/testing_tools/run_adapter.py +++ /dev/null @@ -1,22 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -# Replace the "." entry. -import os.path -import sys - -sys.path.insert( - 1, - os.path.dirname( # pythonFiles - os.path.dirname( # pythonFiles/testing_tools - os.path.abspath(__file__) # this file - ) - ), -) - -from testing_tools.adapter.__main__ import parse_args, main - - -if __name__ == "__main__": - tool, cmd, subargs, toolargs = parse_args() - main(tool, cmd, subargs, toolargs) diff --git a/pythonFiles/testing_tools/socket_manager.py b/pythonFiles/testing_tools/socket_manager.py deleted file mode 100644 index 372a50b5e012..000000000000 --- a/pythonFiles/testing_tools/socket_manager.py +++ /dev/null @@ -1,44 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -import socket -import sys - - -class SocketManager(object): - """Create a socket and connect to the given address. - - The address is a (host: str, port: int) tuple. - Example usage: - - ``` - with SocketManager(("localhost", 6767)) as sock: - request = json.dumps(payload) - result = s.socket.sendall(request.encode("utf-8")) - ``` - """ - - def __init__(self, addr): - self.addr = addr - self.socket = None - - def __enter__(self): - self.socket = socket.socket( - socket.AF_INET, socket.SOCK_STREAM, socket.IPPROTO_TCP - ) - if sys.platform == "win32": - addr_use = socket.SO_EXCLUSIVEADDRUSE - else: - addr_use = socket.SO_REUSEADDR - self.socket.setsockopt(socket.SOL_SOCKET, addr_use, 1) - self.socket.connect(self.addr) - - return self - - def __exit__(self, *_): - if self.socket: - try: - self.socket.shutdown(socket.SHUT_RDWR) - except Exception: - pass - self.socket.close() diff --git a/pythonFiles/testing_tools/unittest_discovery.py b/pythonFiles/testing_tools/unittest_discovery.py deleted file mode 100644 index 2988092c387c..000000000000 --- a/pythonFiles/testing_tools/unittest_discovery.py +++ /dev/null @@ -1,65 +0,0 @@ -import inspect -import os -import sys -import traceback -import unittest - -start_dir = sys.argv[1] -pattern = sys.argv[2] -top_level_dir = sys.argv[3] if len(sys.argv) >= 4 else None -sys.path.insert(0, os.getcwd()) - - -def get_sourceline(obj): - try: - s, n = inspect.getsourcelines(obj) - except: - try: - # this handles `tornado` case we need a better - # way to get to the wrapped function. - # This is a temporary solution - s, n = inspect.getsourcelines(obj.orig_method) - except: - return "*" - - for i, v in enumerate(s): - if v.strip().startswith(("def", "async def")): - return str(n + i) - return "*" - - -def generate_test_cases(suite): - for test in suite: - if isinstance(test, unittest.TestCase): - yield test - else: - for test_case in generate_test_cases(test): - yield test_case - - -try: - loader = unittest.TestLoader() - suite = loader.discover(start_dir, pattern=pattern, top_level_dir=top_level_dir) - - print("start") # Don't remove this line - loader_errors = [] - for s in generate_test_cases(suite): - tm = getattr(s, s._testMethodName) - testId = s.id() - if testId.startswith("unittest.loader._FailedTest"): - loader_errors.append(s._exception) - else: - print(testId.replace(".", ":") + ":" + get_sourceline(tm)) -except: - print("=== exception start ===") - traceback.print_exc() - print("=== exception end ===") - - -for error in loader_errors: - try: - print("=== exception start ===") - print(error.msg) - print("=== exception end ===") - except: - pass diff --git a/pythonFiles/tests/debug_adapter/test_install_debugpy.py b/pythonFiles/tests/debug_adapter/test_install_debugpy.py deleted file mode 100644 index 19565c19675c..000000000000 --- a/pythonFiles/tests/debug_adapter/test_install_debugpy.py +++ /dev/null @@ -1,37 +0,0 @@ -import os -import pytest -import subprocess -import sys - - -def _check_binaries(dir_path): - expected_endswith = ( - "win_amd64.pyd", - "win32.pyd", - "darwin.so", - "i386-linux-gnu.so", - "x86_64-linux-gnu.so", - ) - - binaries = list(p for p in os.listdir(dir_path) if p.endswith(expected_endswith)) - - assert len(binaries) == len(expected_endswith) - - -@pytest.mark.skipif( - sys.version_info[:2] != (3, 7), - reason="DEBUGPY wheels shipped for Python 3.7 only", -) -def test_install_debugpy(tmpdir): - import install_debugpy - - install_debugpy.main(str(tmpdir)) - dir_path = os.path.join( - str(tmpdir), "debugpy", "_vendored", "pydevd", "_pydevd_bundle" - ) - _check_binaries(dir_path) - - dir_path = os.path.join( - str(tmpdir), "debugpy", "_vendored", "pydevd", "_pydevd_frame_eval" - ) - _check_binaries(dir_path) diff --git a/pythonFiles/tests/pytestadapter/.data/parametrize_tests.py b/pythonFiles/tests/pytestadapter/.data/parametrize_tests.py deleted file mode 100644 index 9421e0cc0691..000000000000 --- a/pythonFiles/tests/pytestadapter/.data/parametrize_tests.py +++ /dev/null @@ -1,10 +0,0 @@ -import pytest - - -# Testing pytest with parametrized tests. The first two pass, the third fails. -# The tests ids are parametrize_tests.py::test_adding[3+5-8] and so on. -@pytest.mark.parametrize( # test_marker--test_adding - "actual, expected", [("3+5", 8), ("2+4", 6), ("6+9", 16)] -) -def test_adding(actual, expected): - assert eval(actual) == expected diff --git a/pythonFiles/tests/pytestadapter/expected_discovery_test_output.py b/pythonFiles/tests/pytestadapter/expected_discovery_test_output.py deleted file mode 100644 index 8e96d109ba78..000000000000 --- a/pythonFiles/tests/pytestadapter/expected_discovery_test_output.py +++ /dev/null @@ -1,475 +0,0 @@ -import os -import pathlib - -from .helpers import TEST_DATA_PATH, find_test_line_number - -# This file contains the expected output dictionaries for tests discovery and is used in test_discovery.py. - -# This is the expected output for the empty_discovery.py file. -# └── -TEST_DATA_PATH_STR = os.fspath(TEST_DATA_PATH) -empty_discovery_pytest_expected_output = { - "name": ".data", - "path": TEST_DATA_PATH_STR, - "type_": "folder", - "children": [], - "id_": TEST_DATA_PATH_STR, -} - -# This is the expected output for the simple_pytest.py file. -# └── simple_pytest.py -# └── test_function -simple_test_file_path = os.fspath(TEST_DATA_PATH / "simple_pytest.py") -simple_discovery_pytest_expected_output = { - "name": ".data", - "path": TEST_DATA_PATH_STR, - "type_": "folder", - "children": [ - { - "name": "simple_pytest.py", - "path": simple_test_file_path, - "type_": "file", - "id_": simple_test_file_path, - "children": [ - { - "name": "test_function", - "path": simple_test_file_path, - "lineno": find_test_line_number( - "test_function", - simple_test_file_path, - ), - "type_": "test", - "id_": "simple_pytest.py::test_function", - "runID": "simple_pytest.py::test_function", - } - ], - } - ], - "id_": TEST_DATA_PATH_STR, -} - -# This is the expected output for the unittest_pytest_same_file.py file. -# β”œβ”€β”€ unittest_pytest_same_file.py -# β”œβ”€β”€ TestExample -# β”‚ └── test_true_unittest -# └── test_true_pytest -unit_pytest_same_file_path = os.fspath(TEST_DATA_PATH / "unittest_pytest_same_file.py") -unit_pytest_same_file_discovery_expected_output = { - "name": ".data", - "path": TEST_DATA_PATH_STR, - "type_": "folder", - "children": [ - { - "name": "unittest_pytest_same_file.py", - "path": unit_pytest_same_file_path, - "type_": "file", - "id_": unit_pytest_same_file_path, - "children": [ - { - "name": "TestExample", - "path": unit_pytest_same_file_path, - "type_": "class", - "children": [ - { - "name": "test_true_unittest", - "path": unit_pytest_same_file_path, - "lineno": find_test_line_number( - "test_true_unittest", - unit_pytest_same_file_path, - ), - "type_": "test", - "id_": "unittest_pytest_same_file.py::TestExample::test_true_unittest", - "runID": "unittest_pytest_same_file.py::TestExample::test_true_unittest", - } - ], - "id_": "unittest_pytest_same_file.py::TestExample", - }, - { - "name": "test_true_pytest", - "path": unit_pytest_same_file_path, - "lineno": find_test_line_number( - "test_true_pytest", - unit_pytest_same_file_path, - ), - "type_": "test", - "id_": "unittest_pytest_same_file.py::test_true_pytest", - "runID": "unittest_pytest_same_file.py::test_true_pytest", - }, - ], - } - ], - "id_": TEST_DATA_PATH_STR, -} - -# This is the expected output for the unittest_folder tests -# └── unittest_folder -# β”œβ”€β”€ test_add.py -# β”‚ └── TestAddFunction -# β”‚ β”œβ”€β”€ test_add_negative_numbers -# β”‚ └── test_add_positive_numbers -# └── test_subtract.py -# └── TestSubtractFunction -# β”œβ”€β”€ test_subtract_negative_numbers -# └── test_subtract_positive_numbers -unittest_folder_path = os.fspath(TEST_DATA_PATH / "unittest_folder") -test_add_path = os.fspath(TEST_DATA_PATH / "unittest_folder" / "test_add.py") -test_subtract_path = os.fspath(TEST_DATA_PATH / "unittest_folder" / "test_subtract.py") -unittest_folder_discovery_expected_output = { - "name": ".data", - "path": TEST_DATA_PATH_STR, - "type_": "folder", - "children": [ - { - "name": "unittest_folder", - "path": unittest_folder_path, - "type_": "folder", - "id_": unittest_folder_path, - "children": [ - { - "name": "test_add.py", - "path": test_add_path, - "type_": "file", - "id_": test_add_path, - "children": [ - { - "name": "TestAddFunction", - "path": test_add_path, - "type_": "class", - "children": [ - { - "name": "test_add_negative_numbers", - "path": test_add_path, - "lineno": find_test_line_number( - "test_add_negative_numbers", - test_add_path, - ), - "type_": "test", - "id_": "unittest_folder/test_add.py::TestAddFunction::test_add_negative_numbers", - "runID": "unittest_folder/test_add.py::TestAddFunction::test_add_negative_numbers", - }, - { - "name": "test_add_positive_numbers", - "path": test_add_path, - "lineno": find_test_line_number( - "test_add_positive_numbers", - test_add_path, - ), - "type_": "test", - "id_": "unittest_folder/test_add.py::TestAddFunction::test_add_positive_numbers", - "runID": "unittest_folder/test_add.py::TestAddFunction::test_add_positive_numbers", - }, - ], - "id_": "unittest_folder/test_add.py::TestAddFunction", - } - ], - }, - { - "name": "test_subtract.py", - "path": test_subtract_path, - "type_": "file", - "id_": test_subtract_path, - "children": [ - { - "name": "TestSubtractFunction", - "path": test_subtract_path, - "type_": "class", - "children": [ - { - "name": "test_subtract_negative_numbers", - "path": test_subtract_path, - "lineno": find_test_line_number( - "test_subtract_negative_numbers", - test_subtract_path, - ), - "type_": "test", - "id_": "unittest_folder/test_subtract.py::TestSubtractFunction::test_subtract_negative_numbers", - "runID": "unittest_folder/test_subtract.py::TestSubtractFunction::test_subtract_negative_numbers", - }, - { - "name": "test_subtract_positive_numbers", - "path": test_subtract_path, - "lineno": find_test_line_number( - "test_subtract_positive_numbers", - test_subtract_path, - ), - "type_": "test", - "id_": "unittest_folder/test_subtract.py::TestSubtractFunction::test_subtract_positive_numbers", - "runID": "unittest_folder/test_subtract.py::TestSubtractFunction::test_subtract_positive_numbers", - }, - ], - "id_": "unittest_folder/test_subtract.py::TestSubtractFunction", - } - ], - }, - ], - } - ], - "id_": TEST_DATA_PATH_STR, -} - -# This is the expected output for the dual_level_nested_folder tests -# └── dual_level_nested_folder -# └── test_top_folder.py -# └── test_top_function_t -# └── test_top_function_f -# └── nested_folder_one -# └── test_bottom_folder.py -# └── test_bottom_function_t -# └── test_bottom_function_f -dual_level_nested_folder_path = os.fspath(TEST_DATA_PATH / "dual_level_nested_folder") -test_top_folder_path = os.fspath( - TEST_DATA_PATH / "dual_level_nested_folder" / "test_top_folder.py" -) -test_nested_folder_one_path = os.fspath( - TEST_DATA_PATH / "dual_level_nested_folder" / "nested_folder_one" -) -test_bottom_folder_path = os.fspath( - TEST_DATA_PATH - / "dual_level_nested_folder" - / "nested_folder_one" - / "test_bottom_folder.py" -) - -dual_level_nested_folder_expected_output = { - "name": ".data", - "path": TEST_DATA_PATH_STR, - "type_": "folder", - "children": [ - { - "name": "dual_level_nested_folder", - "path": dual_level_nested_folder_path, - "type_": "folder", - "id_": dual_level_nested_folder_path, - "children": [ - { - "name": "test_top_folder.py", - "path": test_top_folder_path, - "type_": "file", - "id_": test_top_folder_path, - "children": [ - { - "name": "test_top_function_t", - "path": test_top_folder_path, - "lineno": find_test_line_number( - "test_top_function_t", - test_top_folder_path, - ), - "type_": "test", - "id_": "dual_level_nested_folder/test_top_folder.py::test_top_function_t", - "runID": "dual_level_nested_folder/test_top_folder.py::test_top_function_t", - }, - { - "name": "test_top_function_f", - "path": test_top_folder_path, - "lineno": find_test_line_number( - "test_top_function_f", - test_top_folder_path, - ), - "type_": "test", - "id_": "dual_level_nested_folder/test_top_folder.py::test_top_function_f", - "runID": "dual_level_nested_folder/test_top_folder.py::test_top_function_f", - }, - ], - }, - { - "name": "nested_folder_one", - "path": test_nested_folder_one_path, - "type_": "folder", - "id_": test_nested_folder_one_path, - "children": [ - { - "name": "test_bottom_folder.py", - "path": test_bottom_folder_path, - "type_": "file", - "id_": test_bottom_folder_path, - "children": [ - { - "name": "test_bottom_function_t", - "path": test_bottom_folder_path, - "lineno": find_test_line_number( - "test_bottom_function_t", - test_bottom_folder_path, - ), - "type_": "test", - "id_": "dual_level_nested_folder/nested_folder_one/test_bottom_folder.py::test_bottom_function_t", - "runID": "dual_level_nested_folder/nested_folder_one/test_bottom_folder.py::test_bottom_function_t", - }, - { - "name": "test_bottom_function_f", - "path": test_bottom_folder_path, - "lineno": find_test_line_number( - "test_bottom_function_f", - test_bottom_folder_path, - ), - "type_": "test", - "id_": "dual_level_nested_folder/nested_folder_one/test_bottom_folder.py::test_bottom_function_f", - "runID": "dual_level_nested_folder/nested_folder_one/test_bottom_folder.py::test_bottom_function_f", - }, - ], - } - ], - }, - ], - } - ], - "id_": TEST_DATA_PATH_STR, -} - -# This is the expected output for the double_nested_folder tests. -# └── double_nested_folder -# └── nested_folder_one -# └── nested_folder_two -# └── test_nest.py -# └── test_function -double_nested_folder_path = os.fspath(TEST_DATA_PATH / "double_nested_folder") -double_nested_folder_one_path = os.fspath( - TEST_DATA_PATH / "double_nested_folder" / "nested_folder_one" -) -double_nested_folder_two_path = os.fspath( - TEST_DATA_PATH / "double_nested_folder" / "nested_folder_one" / "nested_folder_two" -) -double_nested_test_nest_path = os.fspath( - TEST_DATA_PATH - / "double_nested_folder" - / "nested_folder_one" - / "nested_folder_two" - / "test_nest.py" -) -double_nested_folder_expected_output = { - "name": ".data", - "path": TEST_DATA_PATH_STR, - "type_": "folder", - "children": [ - { - "name": "double_nested_folder", - "path": double_nested_folder_path, - "type_": "folder", - "id_": double_nested_folder_path, - "children": [ - { - "name": "nested_folder_one", - "path": double_nested_folder_one_path, - "type_": "folder", - "id_": double_nested_folder_one_path, - "children": [ - { - "name": "nested_folder_two", - "path": double_nested_folder_two_path, - "type_": "folder", - "id_": double_nested_folder_two_path, - "children": [ - { - "name": "test_nest.py", - "path": double_nested_test_nest_path, - "type_": "file", - "id_": double_nested_test_nest_path, - "children": [ - { - "name": "test_function", - "path": double_nested_test_nest_path, - "lineno": find_test_line_number( - "test_function", - double_nested_test_nest_path, - ), - "type_": "test", - "id_": "double_nested_folder/nested_folder_one/nested_folder_two/test_nest.py::test_function", - "runID": "double_nested_folder/nested_folder_one/nested_folder_two/test_nest.py::test_function", - } - ], - } - ], - } - ], - } - ], - } - ], - "id_": TEST_DATA_PATH_STR, -} - -# This is the expected output for the nested_folder tests. -# └── parametrize_tests.py -# └── test_adding[3+5-8] -# └── test_adding[2+4-6] -# └── test_adding[6+9-16] -parameterize_tests_path = os.fspath(TEST_DATA_PATH / "parametrize_tests.py") -parametrize_tests_expected_output = { - "name": ".data", - "path": TEST_DATA_PATH_STR, - "type_": "folder", - "children": [ - { - "name": "parametrize_tests.py", - "path": parameterize_tests_path, - "type_": "file", - "id_": parameterize_tests_path, - "children": [ - { - "name": "test_adding[3+5-8]", - "path": parameterize_tests_path, - "lineno": find_test_line_number( - "test_adding[3+5-8]", - parameterize_tests_path, - ), - "type_": "test", - "id_": "parametrize_tests.py::test_adding[3+5-8]", - "runID": "parametrize_tests.py::test_adding[3+5-8]", - }, - { - "name": "test_adding[2+4-6]", - "path": parameterize_tests_path, - "lineno": find_test_line_number( - "test_adding[2+4-6]", - parameterize_tests_path, - ), - "type_": "test", - "id_": "parametrize_tests.py::test_adding[2+4-6]", - "runID": "parametrize_tests.py::test_adding[2+4-6]", - }, - { - "name": "test_adding[6+9-16]", - "path": parameterize_tests_path, - "lineno": find_test_line_number( - "test_adding[6+9-16]", - parameterize_tests_path, - ), - "type_": "test", - "id_": "parametrize_tests.py::test_adding[6+9-16]", - "runID": "parametrize_tests.py::test_adding[6+9-16]", - }, - ], - } - ], - "id_": TEST_DATA_PATH_STR, -} - -# This is the expected output for the text_docstring.txt tests. -# └── text_docstring.txt -text_docstring_path = os.fspath(TEST_DATA_PATH / "text_docstring.txt") -doctest_pytest_expected_output = { - "name": ".data", - "path": TEST_DATA_PATH_STR, - "type_": "folder", - "children": [ - { - "name": "text_docstring.txt", - "path": text_docstring_path, - "type_": "file", - "id_": text_docstring_path, - "children": [ - { - "name": "text_docstring.txt", - "path": text_docstring_path, - "lineno": find_test_line_number( - "text_docstring.txt", - text_docstring_path, - ), - "type_": "test", - "id_": "text_docstring.txt::text_docstring.txt", - "runID": "text_docstring.txt::text_docstring.txt", - } - ], - } - ], - "id_": TEST_DATA_PATH_STR, -} diff --git a/pythonFiles/tests/pytestadapter/expected_execution_test_output.py b/pythonFiles/tests/pytestadapter/expected_execution_test_output.py deleted file mode 100644 index a894403c7d71..000000000000 --- a/pythonFiles/tests/pytestadapter/expected_execution_test_output.py +++ /dev/null @@ -1,328 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -TEST_SUBTRACT_FUNCTION = "unittest_folder/test_subtract.py::TestSubtractFunction::" -TEST_ADD_FUNCTION = "unittest_folder/test_add.py::TestAddFunction::" -SUCCESS = "success" -FAILURE = "failure" -TEST_SUBTRACT_FUNCTION_NEGATIVE_NUMBERS_ERROR = "self = \n\n def test_subtract_negative_numbers( # test_marker--test_subtract_negative_numbers\n self,\n ):\n result = subtract(-2, -3)\n> self.assertEqual(result, 100000)\nE AssertionError: 1 != 100000\n\nunittest_folder/test_subtract.py:25: AssertionError" - -# This is the expected output for the unittest_folder execute tests -# └── unittest_folder -# β”œβ”€β”€ test_add.py -# β”‚ └── TestAddFunction -# β”‚ β”œβ”€β”€ test_add_negative_numbers: success -# β”‚ └── test_add_positive_numbers: success -# └── test_subtract.py -# └── TestSubtractFunction -# β”œβ”€β”€ test_subtract_negative_numbers: failure -# └── test_subtract_positive_numbers: success -uf_execution_expected_output = { - f"{TEST_ADD_FUNCTION}test_add_negative_numbers": { - "test": f"{TEST_ADD_FUNCTION}test_add_negative_numbers", - "outcome": SUCCESS, - "message": None, - "traceback": None, - "subtest": None, - }, - f"{TEST_ADD_FUNCTION}test_add_positive_numbers": { - "test": f"{TEST_ADD_FUNCTION}test_add_positive_numbers", - "outcome": SUCCESS, - "message": None, - "traceback": None, - "subtest": None, - }, - f"{TEST_SUBTRACT_FUNCTION}test_subtract_negative_numbers": { - "test": f"{TEST_SUBTRACT_FUNCTION}test_subtract_negative_numbers", - "outcome": FAILURE, - "message": "ERROR MESSAGE", - "traceback": None, - "subtest": None, - }, - f"{TEST_SUBTRACT_FUNCTION}test_subtract_positive_numbers": { - "test": f"{TEST_SUBTRACT_FUNCTION}test_subtract_positive_numbers", - "outcome": SUCCESS, - "message": None, - "traceback": None, - "subtest": None, - }, -} - - -# This is the expected output for the unittest_folder only execute add.py tests -# └── unittest_folder -# β”œβ”€β”€ test_add.py -# β”‚ └── TestAddFunction -# β”‚ β”œβ”€β”€ test_add_negative_numbers: success -# β”‚ └── test_add_positive_numbers: success -uf_single_file_expected_output = { - f"{TEST_ADD_FUNCTION}test_add_negative_numbers": { - "test": f"{TEST_ADD_FUNCTION}test_add_negative_numbers", - "outcome": SUCCESS, - "message": None, - "traceback": None, - "subtest": None, - }, - f"{TEST_ADD_FUNCTION}test_add_positive_numbers": { - "test": f"{TEST_ADD_FUNCTION}test_add_positive_numbers", - "outcome": SUCCESS, - "message": None, - "traceback": None, - "subtest": None, - }, -} - -# This is the expected output for the unittest_folder execute only signle method -# └── unittest_folder -# β”œβ”€β”€ test_add.py -# β”‚ └── TestAddFunction -# β”‚ └── test_add_positive_numbers: success -uf_single_method_execution_expected_output = { - f"{TEST_ADD_FUNCTION}test_add_positive_numbers": { - "test": f"{TEST_ADD_FUNCTION}test_add_positive_numbers", - "outcome": SUCCESS, - "message": None, - "traceback": None, - "subtest": None, - } -} - -# This is the expected output for the unittest_folder tests run where two tests -# run are in different files. -# └── unittest_folder -# β”œβ”€β”€ test_add.py -# β”‚ └── TestAddFunction -# β”‚ └── test_add_positive_numbers: success -# └── test_subtract.py -# └── TestSubtractFunction -# └── test_subtract_positive_numbers: success -uf_non_adjacent_tests_execution_expected_output = { - TEST_SUBTRACT_FUNCTION - + "test_subtract_positive_numbers": { - "test": TEST_SUBTRACT_FUNCTION + "test_subtract_positive_numbers", - "outcome": SUCCESS, - "message": None, - "traceback": None, - "subtest": None, - }, - TEST_ADD_FUNCTION - + "test_add_positive_numbers": { - "test": TEST_ADD_FUNCTION + "test_add_positive_numbers", - "outcome": SUCCESS, - "message": None, - "traceback": None, - "subtest": None, - }, -} - -# This is the expected output for the simple_pytest.py file. -# └── simple_pytest.py -# └── test_function: success -simple_execution_pytest_expected_output = { - "simple_pytest.py::test_function": { - "test": "simple_pytest.py::test_function", - "outcome": "success", - "message": None, - "traceback": None, - "subtest": None, - } -} - -# This is the expected output for the unittest_pytest_same_file.py file. -# β”œβ”€β”€ unittest_pytest_same_file.py -# β”œβ”€β”€ TestExample -# β”‚ └── test_true_unittest: success -# └── test_true_pytest: success -unit_pytest_same_file_execution_expected_output = { - "unittest_pytest_same_file.py::TestExample::test_true_unittest": { - "test": "unittest_pytest_same_file.py::TestExample::test_true_unittest", - "outcome": "success", - "message": None, - "traceback": None, - "subtest": None, - }, - "unittest_pytest_same_file.py::test_true_pytest": { - "test": "unittest_pytest_same_file.py::test_true_pytest", - "outcome": "success", - "message": None, - "traceback": None, - "subtest": None, - }, -} - -# This is the expected output for the dual_level_nested_folder.py tests -# └── dual_level_nested_folder -# └── test_top_folder.py -# └── test_top_function_t: success -# └── test_top_function_f: failure -# └── nested_folder_one -# └── test_bottom_folder.py -# └── test_bottom_function_t: success -# └── test_bottom_function_f: failure -dual_level_nested_folder_execution_expected_output = { - "dual_level_nested_folder/test_top_folder.py::test_top_function_t": { - "test": "dual_level_nested_folder/test_top_folder.py::test_top_function_t", - "outcome": "success", - "message": None, - "traceback": None, - "subtest": None, - }, - "dual_level_nested_folder/test_top_folder.py::test_top_function_f": { - "test": "dual_level_nested_folder/test_top_folder.py::test_top_function_f", - "outcome": "failure", - "message": "ERROR MESSAGE", - "traceback": None, - "subtest": None, - }, - "dual_level_nested_folder/nested_folder_one/test_bottom_folder.py::test_bottom_function_t": { - "test": "dual_level_nested_folder/nested_folder_one/test_bottom_folder.py::test_bottom_function_t", - "outcome": "success", - "message": None, - "traceback": None, - "subtest": None, - }, - "dual_level_nested_folder/nested_folder_one/test_bottom_folder.py::test_bottom_function_f": { - "test": "dual_level_nested_folder/nested_folder_one/test_bottom_folder.py::test_bottom_function_f", - "outcome": "failure", - "message": "ERROR MESSAGE", - "traceback": None, - "subtest": None, - }, -} - -# This is the expected output for the nested_folder tests. -# └── nested_folder_one -# └── nested_folder_two -# └── test_nest.py -# └── test_function: success -double_nested_folder_expected_execution_output = { - "double_nested_folder/nested_folder_one/nested_folder_two/test_nest.py::test_function": { - "test": "double_nested_folder/nested_folder_one/nested_folder_two/test_nest.py::test_function", - "outcome": "success", - "message": None, - "traceback": None, - "subtest": None, - } -} - -# This is the expected output for the nested_folder tests. -# └── parametrize_tests.py -# └── test_adding[3+5-8]: success -# └── test_adding[2+4-6]: success -# └── test_adding[6+9-16]: failure -parametrize_tests_expected_execution_output = { - "parametrize_tests.py::test_adding[3+5-8]": { - "test": "parametrize_tests.py::test_adding[3+5-8]", - "outcome": "success", - "message": None, - "traceback": None, - "subtest": None, - }, - "parametrize_tests.py::test_adding[2+4-6]": { - "test": "parametrize_tests.py::test_adding[2+4-6]", - "outcome": "success", - "message": None, - "traceback": None, - "subtest": None, - }, - "parametrize_tests.py::test_adding[6+9-16]": { - "test": "parametrize_tests.py::test_adding[6+9-16]", - "outcome": "failure", - "message": "ERROR MESSAGE", - "traceback": None, - "subtest": None, - }, -} - -# This is the expected output for the single parameterized tests. -# └── parametrize_tests.py -# └── test_adding[3+5-8]: success -single_parametrize_tests_expected_execution_output = { - "parametrize_tests.py::test_adding[3+5-8]": { - "test": "parametrize_tests.py::test_adding[3+5-8]", - "outcome": "success", - "message": None, - "traceback": None, - "subtest": None, - }, -} - -# This is the expected output for the single parameterized tests. -# └── text_docstring.txt -# └── text_docstring: success -doctest_pytest_expected_execution_output = { - "text_docstring.txt::text_docstring.txt": { - "test": "text_docstring.txt::text_docstring.txt", - "outcome": "success", - "message": None, - "traceback": None, - "subtest": None, - } -} - -# Will run all tests in the cwd that fit the test file naming pattern. -no_test_ids_pytest_execution_expected_output = { - "double_nested_folder/nested_folder_one/nested_folder_two/test_nest.py::test_function": { - "test": "double_nested_folder/nested_folder_one/nested_folder_two/test_nest.py::test_function", - "outcome": "success", - "message": None, - "traceback": None, - "subtest": None, - }, - "dual_level_nested_folder/test_top_folder.py::test_top_function_t": { - "test": "dual_level_nested_folder/test_top_folder.py::test_top_function_t", - "outcome": "success", - "message": None, - "traceback": None, - "subtest": None, - }, - "dual_level_nested_folder/test_top_folder.py::test_top_function_f": { - "test": "dual_level_nested_folder/test_top_folder.py::test_top_function_f", - "outcome": "failure", - "message": "ERROR MESSAGE", - "traceback": None, - "subtest": None, - }, - "dual_level_nested_folder/nested_folder_one/test_bottom_folder.py::test_bottom_function_t": { - "test": "dual_level_nested_folder/nested_folder_one/test_bottom_folder.py::test_bottom_function_t", - "outcome": "success", - "message": None, - "traceback": None, - "subtest": None, - }, - "dual_level_nested_folder/nested_folder_one/test_bottom_folder.py::test_bottom_function_f": { - "test": "dual_level_nested_folder/nested_folder_one/test_bottom_folder.py::test_bottom_function_f", - "outcome": "failure", - "message": "ERROR MESSAGE", - "traceback": None, - "subtest": None, - }, - "unittest_folder/test_add.py::TestAddFunction::test_add_negative_numbers": { - "test": "unittest_folder/test_add.py::TestAddFunction::test_add_negative_numbers", - "outcome": "success", - "message": None, - "traceback": None, - "subtest": None, - }, - "unittest_folder/test_add.py::TestAddFunction::test_add_positive_numbers": { - "test": "unittest_folder/test_add.py::TestAddFunction::test_add_positive_numbers", - "outcome": "success", - "message": None, - "traceback": None, - "subtest": None, - }, - "unittest_folder/test_subtract.py::TestSubtractFunction::test_subtract_negative_numbers": { - "test": "unittest_folder/test_subtract.py::TestSubtractFunction::test_subtract_negative_numbers", - "outcome": "failure", - "message": "ERROR MESSAGE", - "traceback": None, - "subtest": None, - }, - "unittest_folder/test_subtract.py::TestSubtractFunction::test_subtract_positive_numbers": { - "test": "unittest_folder/test_subtract.py::TestSubtractFunction::test_subtract_positive_numbers", - "outcome": "success", - "message": None, - "traceback": None, - "subtest": None, - }, -} diff --git a/pythonFiles/tests/pytestadapter/helpers.py b/pythonFiles/tests/pytestadapter/helpers.py deleted file mode 100644 index b078439f6eac..000000000000 --- a/pythonFiles/tests/pytestadapter/helpers.py +++ /dev/null @@ -1,161 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -import contextlib -import io -import json -import os -import pathlib -import random -import socket -import subprocess -import sys -import uuid -from typing import Any, Dict, List, Optional, Union - -TEST_DATA_PATH = pathlib.Path(__file__).parent / ".data" -from typing_extensions import TypedDict - - -@contextlib.contextmanager -def test_output_file(root: pathlib.Path, ext: str = ".txt"): - """Creates a temporary python file with a random name.""" - basename = ( - "".join(random.choice("abcdefghijklmnopqrstuvwxyz") for _ in range(9)) + ext - ) - fullpath = root / basename - try: - fullpath.write_text("", encoding="utf-8") - yield fullpath - finally: - os.unlink(str(fullpath)) - - -def create_server( - host: str = "127.0.0.1", - port: int = 0, - backlog: int = socket.SOMAXCONN, - timeout: int = 1000, -) -> socket.socket: - """Return a local server socket listening on the given port.""" - server: socket.socket = _new_sock() - if port: - # If binding to a specific port, make sure that the user doesn't have - # to wait until the OS times out waiting for socket in order to use - # that port again if the server or the adapter crash or are force-killed. - if sys.platform == "win32": - server.setsockopt(socket.SOL_SOCKET, socket.SO_EXCLUSIVEADDRUSE, 1) - else: - try: - server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) - except (AttributeError, OSError): - pass # Not available everywhere - server.bind((host, port)) - if timeout: - server.settimeout(timeout) - server.listen(backlog) - return server - - -def _new_sock() -> socket.socket: - sock: socket.socket = socket.socket( - socket.AF_INET, socket.SOCK_STREAM, socket.IPPROTO_TCP - ) - options = [ - ("SOL_SOCKET", "SO_KEEPALIVE", 1), - ("IPPROTO_TCP", "TCP_KEEPIDLE", 1), - ("IPPROTO_TCP", "TCP_KEEPINTVL", 3), - ("IPPROTO_TCP", "TCP_KEEPCNT", 5), - ] - - for level, name, value in options: - try: - sock.setsockopt(getattr(socket, level), getattr(socket, name), value) - except (AttributeError, OSError): - pass # May not be available everywhere. - - return sock - - -CONTENT_LENGTH: str = "Content-Length:" -Env_Dict = TypedDict( - "Env_Dict", {"TEST_UUID": str, "TEST_PORT": str, "PYTHONPATH": str} -) - - -def process_rpc_json(data: str) -> Dict[str, Any]: - """Process the JSON data which comes from the server which runs the pytest discovery.""" - str_stream: io.StringIO = io.StringIO(data) - - length: int = 0 - - while True: - line: str = str_stream.readline() - if CONTENT_LENGTH.lower() in line.lower(): - length = int(line[len(CONTENT_LENGTH) :]) - break - - if not line or line.isspace(): - raise ValueError("Header does not contain Content-Length") - - while True: - line: str = str_stream.readline() - if not line or line.isspace(): - break - - raw_json: str = str_stream.read(length) - return json.loads(raw_json) - - -def runner(args: List[str]) -> Optional[Dict[str, Any]]: - """Run the pytest discovery and return the JSON data from the server.""" - process_args: List[str] = [ - sys.executable, - "-m", - "pytest", - "-p", - "vscode_pytest", - ] + args - - with test_output_file(TEST_DATA_PATH) as output_path: - env = os.environ.copy() - env.update( - { - "TEST_UUID": str(uuid.uuid4()), - "TEST_PORT": str(12345), # port is not used for tests - "PYTHONPATH": os.fspath(pathlib.Path(__file__).parent.parent.parent), - "TEST_OUTPUT_FILE": os.fspath(output_path), - } - ) - - result = subprocess.run( - process_args, - env=env, - cwd=os.fspath(TEST_DATA_PATH), - stdout=subprocess.PIPE, - stderr=subprocess.PIPE, - ) - if result.returncode != 0: - print("Subprocess Run failed with:") - print(result.stdout.decode(encoding="utf-8")) - print(result.stderr.decode(encoding="utf-8")) - - return process_rpc_json(output_path.read_text(encoding="utf-8")) - - -def find_test_line_number(test_name: str, test_file_path) -> str: - """Function which finds the correct line number for a test by looking for the "test_marker--[test_name]" string. - - The test_name is split on the "[" character to remove the parameterization information. - - Args: - test_name: The name of the test to find the line number for, will be unique per file. - test_file_path: The path to the test file where the test is located. - """ - test_file_unique_id: str = "test_marker--" + test_name.split("[")[0] - with open(test_file_path) as f: - for i, line in enumerate(f): - if test_file_unique_id in line: - return str(i + 1) - error_str: str = f"Test {test_name!r} not found on any line in {test_file_path}" - raise ValueError(error_str) diff --git a/pythonFiles/tests/pytestadapter/test_discovery.py b/pythonFiles/tests/pytestadapter/test_discovery.py deleted file mode 100644 index bb6e7255704e..000000000000 --- a/pythonFiles/tests/pytestadapter/test_discovery.py +++ /dev/null @@ -1,111 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. -import os -import shutil - -import pytest - -from . import expected_discovery_test_output -from .helpers import TEST_DATA_PATH, runner - - -def test_syntax_error(tmp_path): - """Test pytest discovery on a file that has a syntax error. - - Copies the contents of a .txt file to a .py file in the temporary directory - to then run pytest discovery on. - - The json should still be returned but the errors list should be present. - - Keyword arguments: - tmp_path -- pytest fixture that creates a temporary directory. - """ - # Saving some files as .txt to avoid that file displaying a syntax error for - # the extension as a whole. Instead, rename it before running this test - # in order to test the error handling. - file_path = TEST_DATA_PATH / "error_syntax_discovery.txt" - temp_dir = tmp_path / "temp_data" - temp_dir.mkdir() - p = temp_dir / "error_syntax_discovery.py" - shutil.copyfile(file_path, p) - actual = runner(["--collect-only", os.fspath(p)]) - assert actual - assert all(item in actual for item in ("status", "cwd", "error")) - assert actual["status"] == "error" - assert actual["cwd"] == os.fspath(TEST_DATA_PATH) - assert len(actual["error"]) == 2 - - -def test_parameterized_error_collect(): - """Tests pytest discovery on specific file that incorrectly uses parametrize. - - The json should still be returned but the errors list should be present. - """ - file_path_str = "error_parametrize_discovery.py" - actual = runner(["--collect-only", file_path_str]) - assert actual - assert all(item in actual for item in ("status", "cwd", "error")) - assert actual["status"] == "error" - assert actual["cwd"] == os.fspath(TEST_DATA_PATH) - assert len(actual["error"]) == 2 - - -@pytest.mark.parametrize( - "file, expected_const", - [ - ( - "parametrize_tests.py", - expected_discovery_test_output.parametrize_tests_expected_output, - ), - ( - "empty_discovery.py", - expected_discovery_test_output.empty_discovery_pytest_expected_output, - ), - ( - "simple_pytest.py", - expected_discovery_test_output.simple_discovery_pytest_expected_output, - ), - ( - "unittest_pytest_same_file.py", - expected_discovery_test_output.unit_pytest_same_file_discovery_expected_output, - ), - ( - "unittest_folder", - expected_discovery_test_output.unittest_folder_discovery_expected_output, - ), - ( - "dual_level_nested_folder", - expected_discovery_test_output.dual_level_nested_folder_expected_output, - ), - ( - "double_nested_folder", - expected_discovery_test_output.double_nested_folder_expected_output, - ), - ( - "text_docstring.txt", - expected_discovery_test_output.doctest_pytest_expected_output, - ), - ], -) -def test_pytest_collect(file, expected_const): - """ - Test to test pytest discovery on a variety of test files/ folder structures. - Uses variables from expected_discovery_test_output.py to store the expected dictionary return. - Only handles discovery and therefore already contains the arg --collect-only. - All test discovery will succeed, be in the correct cwd, and match expected test output. - - Keyword arguments: - file -- a string with the file or folder to run pytest discovery on. - expected_const -- the expected output from running pytest discovery on the file. - """ - actual = runner( - [ - "--collect-only", - os.fspath(TEST_DATA_PATH / file), - ] - ) - assert actual - assert all(item in actual for item in ("status", "cwd", "tests")) - assert actual["status"] == "success" - assert actual["cwd"] == os.fspath(TEST_DATA_PATH) - assert actual["tests"] == expected_const diff --git a/pythonFiles/tests/pytestadapter/test_execution.py b/pythonFiles/tests/pytestadapter/test_execution.py deleted file mode 100644 index 8613deb96098..000000000000 --- a/pythonFiles/tests/pytestadapter/test_execution.py +++ /dev/null @@ -1,165 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. -import os -import shutil - -import pytest -from tests.pytestadapter import expected_execution_test_output - -from .helpers import TEST_DATA_PATH, runner - - -def test_syntax_error_execution(tmp_path): - """Test pytest execution on a file that has a syntax error. - - Copies the contents of a .txt file to a .py file in the temporary directory - to then run pytest exeuction on. - - The json should still be returned but the errors list should be present. - - Keyword arguments: - tmp_path -- pytest fixture that creates a temporary directory. - """ - # Saving some files as .txt to avoid that file displaying a syntax error for - # the extension as a whole. Instead, rename it before running this test - # in order to test the error handling. - file_path = TEST_DATA_PATH / "error_syntax_discovery.txt" - temp_dir = tmp_path / "temp_data" - temp_dir.mkdir() - p = temp_dir / "error_syntax_discovery.py" - shutil.copyfile(file_path, p) - actual = runner(["error_syntax_discover.py::test_function"]) - assert actual - assert all(item in actual for item in ("status", "cwd", "error")) - assert actual["status"] == "error" - assert actual["cwd"] == os.fspath(TEST_DATA_PATH) - assert len(actual["error"]) == 1 - - -def test_bad_id_error_execution(): - """Test pytest discovery with a non-existent test_id. - - The json should still be returned but the errors list should be present. - """ - actual = runner(["not/a/real::test_id"]) - assert actual - assert all(item in actual for item in ("status", "cwd", "error")) - assert actual["status"] == "error" - assert actual["cwd"] == os.fspath(TEST_DATA_PATH) - assert len(actual["error"]) == 1 - - -@pytest.mark.parametrize( - "test_ids, expected_const", - [ - ( - [ - "unittest_folder/test_add.py::TestAddFunction::test_add_positive_numbers", - "unittest_folder/test_add.py::TestAddFunction::test_add_negative_numbers", - "unittest_folder/test_subtract.py::TestSubtractFunction::test_subtract_positive_numbers", - "unittest_folder/test_subtract.py::TestSubtractFunction::test_subtract_negative_numbers", - ], - expected_execution_test_output.uf_execution_expected_output, - ), - ( - [ - "unittest_folder/test_add.py::TestAddFunction::test_add_positive_numbers", - "unittest_folder/test_add.py::TestAddFunction::test_add_negative_numbers", - ], - expected_execution_test_output.uf_single_file_expected_output, - ), - ( - [ - "unittest_folder/test_add.py::TestAddFunction::test_add_positive_numbers", - ], - expected_execution_test_output.uf_single_method_execution_expected_output, - ), - ( - [ - "unittest_folder/test_add.py::TestAddFunction::test_add_positive_numbers", - "unittest_folder/test_subtract.py::TestSubtractFunction::test_subtract_positive_numbers", - ], - expected_execution_test_output.uf_non_adjacent_tests_execution_expected_output, - ), - ( - [ - "unittest_pytest_same_file.py::TestExample::test_true_unittest", - "unittest_pytest_same_file.py::test_true_pytest", - ], - expected_execution_test_output.unit_pytest_same_file_execution_expected_output, - ), - ( - [ - "dual_level_nested_folder/test_top_folder.py::test_top_function_t", - "dual_level_nested_folder/test_top_folder.py::test_top_function_f", - "dual_level_nested_folder/nested_folder_one/test_bottom_folder.py::test_bottom_function_t", - "dual_level_nested_folder/nested_folder_one/test_bottom_folder.py::test_bottom_function_f", - ], - expected_execution_test_output.dual_level_nested_folder_execution_expected_output, - ), - ( - [ - "double_nested_folder/nested_folder_one/nested_folder_two/test_nest.py::test_function" - ], - expected_execution_test_output.double_nested_folder_expected_execution_output, - ), - ( - [ - "parametrize_tests.py::test_adding[3+5-8]", - "parametrize_tests.py::test_adding[2+4-6]", - "parametrize_tests.py::test_adding[6+9-16]", - ], - expected_execution_test_output.parametrize_tests_expected_execution_output, - ), - ( - [ - "parametrize_tests.py::test_adding[3+5-8]", - ], - expected_execution_test_output.single_parametrize_tests_expected_execution_output, - ), - ( - [ - "text_docstring.txt::text_docstring.txt", - ], - expected_execution_test_output.doctest_pytest_expected_execution_output, - ), - ( - [ - "", - ], - expected_execution_test_output.no_test_ids_pytest_execution_expected_output, - ), - ], -) -def test_pytest_execution(test_ids, expected_const): - """ - Test that pytest discovery works as expected where run pytest is always successful - but the actual test results are both successes and failures.: - 1. uf_execution_expected_output: unittest tests run on multiple files. - 2. uf_single_file_expected_output: test run on a single file. - 3. uf_single_method_execution_expected_output: test run on a single method in a file. - 4. uf_non_adjacent_tests_execution_expected_output: test run on unittests in two files with single selection in test explorer. - 5. unit_pytest_same_file_execution_expected_output: test run on a file with both unittest and pytest tests. - 6. dual_level_nested_folder_execution_expected_output: test run on a file with one test file at the top level and one test file in a nested folder. - 7. double_nested_folder_expected_execution_output: test run on a double nested folder. - 8. parametrize_tests_expected_execution_output: test run on a parametrize test with 3 inputs. - 9. single_parametrize_tests_expected_execution_output: test run on single parametrize test. - 10. doctest_pytest_expected_execution_output: test run on doctest file. - 11. no_test_ids_pytest_execution_expected_output: test run with no inputted test ids. - - - Keyword arguments: - test_ids -- an array of test_ids to run. - expected_const -- a dictionary of the expected output from running pytest discovery on the files. - """ - args = test_ids - actual = runner(args) - assert actual - assert all(item in actual for item in ("status", "cwd", "result")) - assert actual["status"] == "success" - assert actual["cwd"] == os.fspath(TEST_DATA_PATH) - result_data = actual["result"] - for key in result_data: - if result_data[key]["outcome"] == "failure": - result_data[key]["message"] = "ERROR MESSAGE" - assert result_data == expected_const diff --git a/pythonFiles/tests/testing_tools/adapter/.data/NormCase/tests/A/b/C/test_Spam.py b/pythonFiles/tests/testing_tools/adapter/.data/NormCase/tests/A/b/C/test_Spam.py deleted file mode 100644 index 3501b9e118e5..000000000000 --- a/pythonFiles/tests/testing_tools/adapter/.data/NormCase/tests/A/b/C/test_Spam.py +++ /dev/null @@ -1,3 +0,0 @@ - -def test_okay(): - assert True diff --git a/pythonFiles/tests/testing_tools/adapter/.data/complex/README.md b/pythonFiles/tests/testing_tools/adapter/.data/complex/README.md deleted file mode 100644 index e30e96142d02..000000000000 --- a/pythonFiles/tests/testing_tools/adapter/.data/complex/README.md +++ /dev/null @@ -1,156 +0,0 @@ -## Directory Structure - -``` -pythonFiles/tests/testing_tools/adapter/.data/ - tests/ # test root - test_doctest.txt - test_pytest.py - test_unittest.py - test_mixed.py - spam.py # note: no "test_" prefix, but contains tests - test_foo.py - test_42.py - test_42-43.py # note the hyphen - testspam.py - v/ - __init__.py - spam.py - test_eggs.py - test_ham.py - test_spam.py - w/ - # no __init__.py - test_spam.py - test_spam_ex.py - x/y/z/ # each with a __init__.py - test_ham.py - a/ - __init__.py - test_spam.py - b/ - __init__.py - test_spam.py -``` - -## Tests (and Suites) - -basic: - -- `./test_foo.py::test_simple` -- `./test_pytest.py::test_simple` -- `./test_pytest.py::TestSpam::test_simple` -- `./test_pytest.py::TestSpam::TestHam::TestEggs::test_simple` -- `./test_pytest.py::TestEggs::test_simple` -- `./test_pytest.py::TestParam::test_simple` -- `./test_mixed.py::test_top_level` -- `./test_mixed.py::MyTests::test_simple` -- `./test_mixed.py::TestMySuite::test_simple` -- `./test_unittest.py::MyTests::test_simple` -- `./test_unittest.py::OtherTests::test_simple` -- `./x/y/z/test_ham.py::test_simple` -- `./x/y/z/a/test_spam.py::test_simple` -- `./x/y/z/b/test_spam.py::test_simple` - -failures: - -- `./test_pytest.py::test_failure` -- `./test_pytest.py::test_runtime_failed` -- `./test_pytest.py::test_raises` - -skipped: - -- `./test_mixed.py::test_skipped` -- `./test_mixed.py::MyTests::test_skipped` -- `./test_pytest.py::test_runtime_skipped` -- `./test_pytest.py::test_skipped` -- `./test_pytest.py::test_maybe_skipped` -- `./test_pytest.py::SpamTests::test_skipped` -- `./test_pytest.py::test_param_13_markers[???]` -- `./test_pytest.py::test_param_13_skipped[*]` -- `./test_unittest.py::MyTests::test_skipped` -- (`./test_unittest.py::MyTests::test_maybe_skipped`) -- (`./test_unittest.py::MyTests::test_maybe_not_skipped`) - -in namespace package: - -- `./w/test_spam.py::test_simple` -- `./w/test_spam_ex.py::test_simple` - -filename oddities: - -- `./test_42.py::test_simple` -- `./test_42-43.py::test_simple` -- (`./testspam.py::test_simple` not discovered by default) -- (`./spam.py::test_simple` not discovered) - -imports discovered: - -- `./v/test_eggs.py::test_simple` -- `./v/test_eggs.py::TestSimple::test_simple` -- `./v/test_ham.py::test_simple` -- `./v/test_ham.py::test_not_hard` -- `./v/test_spam.py::test_simple` -- `./v/test_spam.py::test_simpler` - -subtests: - -- `./test_pytest.py::test_dynamic_*` -- `./test_pytest.py::test_param_01[]` -- `./test_pytest.py::test_param_11[1]` -- `./test_pytest.py::test_param_13[*]` -- `./test_pytest.py::test_param_13_markers[*]` -- `./test_pytest.py::test_param_13_repeat[*]` -- `./test_pytest.py::test_param_13_skipped[*]` -- `./test_pytest.py::test_param_23_13[*]` -- `./test_pytest.py::test_param_23_raises[*]` -- `./test_pytest.py::test_param_33[*]` -- `./test_pytest.py::test_param_33_ids[*]` -- `./test_pytest.py::TestParam::test_param_13[*]` -- `./test_pytest.py::TestParamAll::test_param_13[*]` -- `./test_pytest.py::TestParamAll::test_spam_13[*]` -- `./test_pytest.py::test_fixture_param[*]` -- `./test_pytest.py::test_param_fixture[*]` -- `./test_pytest_param.py::test_param_13[*]` -- `./test_pytest_param.py::TestParamAll::test_param_13[*]` -- `./test_pytest_param.py::TestParamAll::test_spam_13[*]` -- (`./test_unittest.py::MyTests::test_with_subtests`) -- (`./test_unittest.py::MyTests::test_with_nested_subtests`) -- (`./test_unittest.py::MyTests::test_dynamic_*`) - -For more options for pytests's parametrize(), see -https://docs.pytest.org/en/latest/example/parametrize.html#paramexamples. - -using fixtures: - -- `./test_pytest.py::test_fixture` -- `./test_pytest.py::test_fixture_param[*]` -- `./test_pytest.py::test_param_fixture[*]` -- `./test_pytest.py::test_param_mark_fixture[*]` - -other markers: - -- `./test_pytest.py::test_known_failure` -- `./test_pytest.py::test_param_markers[2]` -- `./test_pytest.py::test_warned` -- `./test_pytest.py::test_custom_marker` -- `./test_pytest.py::test_multiple_markers` -- (`./test_unittest.py::MyTests::test_known_failure`) - -others not discovered: - -- (`./test_pytest.py::TestSpam::TestHam::TestEggs::TestNoop1`) -- (`./test_pytest.py::TestSpam::TestNoop2`) -- (`./test_pytest.py::TestNoop3`) -- (`./test_pytest.py::MyTests::test_simple`) -- (`./test_unittest.py::MyTests::TestSub1`) -- (`./test_unittest.py::MyTests::TestSub2`) -- (`./test_unittest.py::NoTests`) - -doctests: - -- `./test_doctest.txt::test_doctest.txt` -- (`./test_doctest.py::test_doctest.py`) -- (`../mod.py::mod`) -- (`../mod.py::mod.square`) -- (`../mod.py::mod.Spam`) -- (`../mod.py::mod.spam.eggs`) diff --git a/pythonFiles/tests/testing_tools/adapter/.data/complex/mod.py b/pythonFiles/tests/testing_tools/adapter/.data/complex/mod.py deleted file mode 100644 index b8c495503895..000000000000 --- a/pythonFiles/tests/testing_tools/adapter/.data/complex/mod.py +++ /dev/null @@ -1,51 +0,0 @@ -""" - -Examples: - ->>> square(1) -1 ->>> square(2) -4 ->>> square(3) -9 ->>> spam = Spam() ->>> spam.eggs() -42 -""" - - -def square(x): - """ - - Examples: - - >>> square(1) - 1 - >>> square(2) - 4 - >>> square(3) - 9 - """ - return x * x - - -class Spam(object): - """ - - Examples: - - >>> spam = Spam() - >>> spam.eggs() - 42 - """ - - def eggs(self): - """ - - Examples: - - >>> spam = Spam() - >>> spam.eggs() - 42 - """ - return 42 diff --git a/pythonFiles/tests/testing_tools/adapter/.data/complex/tests/spam.py b/pythonFiles/tests/testing_tools/adapter/.data/complex/tests/spam.py deleted file mode 100644 index 4c4134d75584..000000000000 --- a/pythonFiles/tests/testing_tools/adapter/.data/complex/tests/spam.py +++ /dev/null @@ -1,3 +0,0 @@ - -def test_simple(): - assert True diff --git a/pythonFiles/tests/testing_tools/adapter/.data/complex/tests/test_42-43.py b/pythonFiles/tests/testing_tools/adapter/.data/complex/tests/test_42-43.py deleted file mode 100644 index 4c4134d75584..000000000000 --- a/pythonFiles/tests/testing_tools/adapter/.data/complex/tests/test_42-43.py +++ /dev/null @@ -1,3 +0,0 @@ - -def test_simple(): - assert True diff --git a/pythonFiles/tests/testing_tools/adapter/.data/complex/tests/test_42.py b/pythonFiles/tests/testing_tools/adapter/.data/complex/tests/test_42.py deleted file mode 100644 index 4c4134d75584..000000000000 --- a/pythonFiles/tests/testing_tools/adapter/.data/complex/tests/test_42.py +++ /dev/null @@ -1,3 +0,0 @@ - -def test_simple(): - assert True diff --git a/pythonFiles/tests/testing_tools/adapter/.data/complex/tests/test_doctest.py b/pythonFiles/tests/testing_tools/adapter/.data/complex/tests/test_doctest.py deleted file mode 100644 index 27cccbdb77cc..000000000000 --- a/pythonFiles/tests/testing_tools/adapter/.data/complex/tests/test_doctest.py +++ /dev/null @@ -1,6 +0,0 @@ -""" -Doctests: - ->>> 1 == 1 -True -""" diff --git a/pythonFiles/tests/testing_tools/adapter/.data/complex/tests/test_doctest.txt b/pythonFiles/tests/testing_tools/adapter/.data/complex/tests/test_doctest.txt deleted file mode 100644 index 4b51fde5667e..000000000000 --- a/pythonFiles/tests/testing_tools/adapter/.data/complex/tests/test_doctest.txt +++ /dev/null @@ -1,15 +0,0 @@ - -assignment & lookup: - ->>> x = 3 ->>> x -3 - -deletion: - ->>> del x ->>> x -Traceback (most recent call last): - ... -NameError: name 'x' is not defined - diff --git a/pythonFiles/tests/testing_tools/adapter/.data/complex/tests/test_foo.py b/pythonFiles/tests/testing_tools/adapter/.data/complex/tests/test_foo.py deleted file mode 100644 index e752106f503a..000000000000 --- a/pythonFiles/tests/testing_tools/adapter/.data/complex/tests/test_foo.py +++ /dev/null @@ -1,4 +0,0 @@ - - -def test_simple(): - assert True diff --git a/pythonFiles/tests/testing_tools/adapter/.data/complex/tests/test_mixed.py b/pythonFiles/tests/testing_tools/adapter/.data/complex/tests/test_mixed.py deleted file mode 100644 index e9c675647f13..000000000000 --- a/pythonFiles/tests/testing_tools/adapter/.data/complex/tests/test_mixed.py +++ /dev/null @@ -1,27 +0,0 @@ -import pytest -import unittest - - -def test_top_level(): - assert True - - -@pytest.mark.skip -def test_skipped(): - assert False - - -class TestMySuite(object): - - def test_simple(self): - assert True - - -class MyTests(unittest.TestCase): - - def test_simple(self): - assert True - - @pytest.mark.skip - def test_skipped(self): - assert False diff --git a/pythonFiles/tests/testing_tools/adapter/.data/complex/tests/test_pytest.py b/pythonFiles/tests/testing_tools/adapter/.data/complex/tests/test_pytest.py deleted file mode 100644 index 39d3ece9c0ba..000000000000 --- a/pythonFiles/tests/testing_tools/adapter/.data/complex/tests/test_pytest.py +++ /dev/null @@ -1,227 +0,0 @@ -# ... - -import pytest - - -def test_simple(): - assert True - - -def test_failure(): - assert False - - -def test_runtime_skipped(): - pytest.skip('???') - - -def test_runtime_failed(): - pytest.fail('???') - - -def test_raises(): - raise Exception - - -@pytest.mark.skip -def test_skipped(): - assert False - - -@pytest.mark.skipif(True) -def test_maybe_skipped(): - assert False - - -@pytest.mark.xfail -def test_known_failure(): - assert False - - -@pytest.mark.filterwarnings -def test_warned(): - assert False - - -@pytest.mark.spam -def test_custom_marker(): - assert False - - -@pytest.mark.filterwarnings -@pytest.mark.skip -@pytest.mark.xfail -@pytest.mark.skipif(True) -@pytest.mark.skip -@pytest.mark.spam -def test_multiple_markers(): - assert False - - -for i in range(3): - def func(): - assert True - globals()['test_dynamic_{}'.format(i + 1)] = func -del func - - -class TestSpam(object): - - def test_simple(): - assert True - - @pytest.mark.skip - def test_skipped(self): - assert False - - class TestHam(object): - - class TestEggs(object): - - def test_simple(): - assert True - - class TestNoop1(object): - pass - - class TestNoop2(object): - pass - - -class TestEggs(object): - - def test_simple(): - assert True - - -# legend for parameterized test names: -# "test_param_XY[_XY]*" -# X - # params -# Y - # cases -# [_XY]* - extra decorators - -@pytest.mark.parametrize('', [()]) -def test_param_01(): - assert True - - -@pytest.mark.parametrize('x', [(1,)]) -def test_param_11(x): - assert x == 1 - - -@pytest.mark.parametrize('x', [(1,), (1.0,), (1+0j,)]) -def test_param_13(x): - assert x == 1 - - -@pytest.mark.parametrize('x', [(1,), (1,), (1,)]) -def test_param_13_repeat(x): - assert x == 1 - - -@pytest.mark.parametrize('x,y,z', [(1, 1, 1), (3, 4, 5), (0, 0, 0)]) -def test_param_33(x, y, z): - assert x*x + y*y == z*z - - -@pytest.mark.parametrize('x,y,z', [(1, 1, 1), (3, 4, 5), (0, 0, 0)], - ids=['v1', 'v2', 'v3']) -def test_param_33_ids(x, y, z): - assert x*x + y*y == z*z - - -@pytest.mark.parametrize('z', [(1,), (5,), (0,)]) -@pytest.mark.parametrize('x,y', [(1, 1), (3, 4), (0, 0)]) -def test_param_23_13(x, y, z): - assert x*x + y*y == z*z - - -@pytest.mark.parametrize('x', [ - (1,), - pytest.param(1.0, marks=[pytest.mark.skip, pytest.mark.spam], id='???'), - pytest.param(2, marks=[pytest.mark.xfail]), - ]) -def test_param_13_markers(x): - assert x == 1 - - -@pytest.mark.skip -@pytest.mark.parametrize('x', [(1,), (1.0,), (1+0j,)]) -def test_param_13_skipped(x): - assert x == 1 - - -@pytest.mark.parametrize('x,catch', [(1, None), (1.0, None), (2, pytest.raises(Exception))]) -def test_param_23_raises(x, catch): - if x != 1: - with catch: - raise Exception - - -class TestParam(object): - - def test_simple(): - assert True - - @pytest.mark.parametrize('x', [(1,), (1.0,), (1+0j,)]) - def test_param_13(self, x): - assert x == 1 - - -@pytest.mark.parametrize('x', [(1,), (1.0,), (1+0j,)]) -class TestParamAll(object): - - def test_param_13(self, x): - assert x == 1 - - def test_spam_13(self, x): - assert x == 1 - - -@pytest.fixture -def spamfix(request): - yield 'spam' - - -@pytest.fixture(params=['spam', 'eggs']) -def paramfix(request): - return request.param - - -def test_fixture(spamfix): - assert spamfix == 'spam' - - -@pytest.mark.usefixtures('spamfix') -def test_mark_fixture(): - assert True - - -@pytest.mark.parametrize('x', [(1,), (1.0,), (1+0j,)]) -def test_param_fixture(spamfix, x): - assert spamfix == 'spam' - assert x == 1 - - -@pytest.mark.parametrize('x', [ - (1,), - (1.0,), - pytest.param(1+0j, marks=[pytest.mark.usefixtures('spamfix')]), - ]) -def test_param_mark_fixture(x): - assert x == 1 - - -def test_fixture_param(paramfix): - assert paramfix == 'spam' - - -class TestNoop3(object): - pass - - -class MyTests(object): # does not match default name pattern - - def test_simple(): - assert True diff --git a/pythonFiles/tests/testing_tools/adapter/.data/complex/tests/test_pytest_param.py b/pythonFiles/tests/testing_tools/adapter/.data/complex/tests/test_pytest_param.py deleted file mode 100644 index bd22d89f42bd..000000000000 --- a/pythonFiles/tests/testing_tools/adapter/.data/complex/tests/test_pytest_param.py +++ /dev/null @@ -1,18 +0,0 @@ -import pytest - - -# module-level parameterization -pytestmark = pytest.mark.parametrize('x', [(1,), (1.0,), (1+0j,)]) - - -def test_param_13(x): - assert x == 1 - - -class TestParamAll(object): - - def test_param_13(self, x): - assert x == 1 - - def test_spam_13(self, x): - assert x == 1 diff --git a/pythonFiles/tests/testing_tools/adapter/.data/complex/tests/test_unittest.py b/pythonFiles/tests/testing_tools/adapter/.data/complex/tests/test_unittest.py deleted file mode 100644 index dd3e82535739..000000000000 --- a/pythonFiles/tests/testing_tools/adapter/.data/complex/tests/test_unittest.py +++ /dev/null @@ -1,66 +0,0 @@ -import unittest - - -class MyTests(unittest.TestCase): - - def test_simple(self): - self.assertTrue(True) - - @unittest.skip('???') - def test_skipped(self): - self.assertTrue(False) - - @unittest.skipIf(True, '???') - def test_maybe_skipped(self): - self.assertTrue(False) - - @unittest.skipUnless(False, '???') - def test_maybe_not_skipped(self): - self.assertTrue(False) - - def test_skipped_inside(self): - raise unittest.SkipTest('???') - - class TestSub1(object): - - def test_simple(self): - self.assertTrue(True) - - class TestSub2(unittest.TestCase): - - def test_simple(self): - self.assertTrue(True) - - def test_failure(self): - raise Exception - - @unittest.expectedFailure - def test_known_failure(self): - raise Exception - - def test_with_subtests(self): - for i in range(3): - with self.subtest(i): # This is invalid under Py2. - self.assertTrue(True) - - def test_with_nested_subtests(self): - for i in range(3): - with self.subtest(i): # This is invalid under Py2. - for j in range(3): - with self.subtest(i): # This is invalid under Py2. - self.assertTrue(True) - - for i in range(3): - def test_dynamic_(self, i=i): - self.assertEqual(True) - test_dynamic_.__name__ += str(i) - - -class OtherTests(unittest.TestCase): - - def test_simple(self): - self.assertTrue(True) - - -class NoTests(unittest.TestCase): - pass diff --git a/pythonFiles/tests/testing_tools/adapter/.data/complex/tests/testspam.py b/pythonFiles/tests/testing_tools/adapter/.data/complex/tests/testspam.py deleted file mode 100644 index 7ec91c783e2c..000000000000 --- a/pythonFiles/tests/testing_tools/adapter/.data/complex/tests/testspam.py +++ /dev/null @@ -1,9 +0,0 @@ -''' -... -... -... -''' - - -def test_simple(): - assert True diff --git a/pythonFiles/tests/testing_tools/adapter/.data/complex/tests/v/spam.py b/pythonFiles/tests/testing_tools/adapter/.data/complex/tests/v/spam.py deleted file mode 100644 index 18c92c09306e..000000000000 --- a/pythonFiles/tests/testing_tools/adapter/.data/complex/tests/v/spam.py +++ /dev/null @@ -1,9 +0,0 @@ - -def test_simple(self): - assert True - - -class TestSimple(object): - - def test_simple(self): - assert True diff --git a/pythonFiles/tests/testing_tools/adapter/.data/complex/tests/v/test_eggs.py b/pythonFiles/tests/testing_tools/adapter/.data/complex/tests/v/test_eggs.py deleted file mode 100644 index f3e7d9517631..000000000000 --- a/pythonFiles/tests/testing_tools/adapter/.data/complex/tests/v/test_eggs.py +++ /dev/null @@ -1 +0,0 @@ -from .spam import * diff --git a/pythonFiles/tests/testing_tools/adapter/.data/complex/tests/v/test_ham.py b/pythonFiles/tests/testing_tools/adapter/.data/complex/tests/v/test_ham.py deleted file mode 100644 index 6b6a01f87ec5..000000000000 --- a/pythonFiles/tests/testing_tools/adapter/.data/complex/tests/v/test_ham.py +++ /dev/null @@ -1,2 +0,0 @@ -from .spam import test_simple -from .spam import test_simple as test_not_hard diff --git a/pythonFiles/tests/testing_tools/adapter/.data/complex/tests/v/test_spam.py b/pythonFiles/tests/testing_tools/adapter/.data/complex/tests/v/test_spam.py deleted file mode 100644 index 18cf56f90533..000000000000 --- a/pythonFiles/tests/testing_tools/adapter/.data/complex/tests/v/test_spam.py +++ /dev/null @@ -1,5 +0,0 @@ -from .spam import test_simple - - -def test_simpler(self): - assert True diff --git a/pythonFiles/tests/testing_tools/adapter/.data/complex/tests/w/test_spam.py b/pythonFiles/tests/testing_tools/adapter/.data/complex/tests/w/test_spam.py deleted file mode 100644 index 6a0b60d1d5bd..000000000000 --- a/pythonFiles/tests/testing_tools/adapter/.data/complex/tests/w/test_spam.py +++ /dev/null @@ -1,5 +0,0 @@ - - - -def test_simple(): - assert True diff --git a/pythonFiles/tests/testing_tools/adapter/.data/complex/tests/w/test_spam_ex.py b/pythonFiles/tests/testing_tools/adapter/.data/complex/tests/w/test_spam_ex.py deleted file mode 100644 index 6a0b60d1d5bd..000000000000 --- a/pythonFiles/tests/testing_tools/adapter/.data/complex/tests/w/test_spam_ex.py +++ /dev/null @@ -1,5 +0,0 @@ - - - -def test_simple(): - assert True diff --git a/pythonFiles/tests/testing_tools/adapter/.data/complex/tests/x/y/z/a/test_spam.py b/pythonFiles/tests/testing_tools/adapter/.data/complex/tests/x/y/z/a/test_spam.py deleted file mode 100644 index bdb7e4fec3a5..000000000000 --- a/pythonFiles/tests/testing_tools/adapter/.data/complex/tests/x/y/z/a/test_spam.py +++ /dev/null @@ -1,12 +0,0 @@ -""" -... -""" - - -# ... - -ANSWER = 42 - - -def test_simple(): - assert True diff --git a/pythonFiles/tests/testing_tools/adapter/.data/complex/tests/x/y/z/b/test_spam.py b/pythonFiles/tests/testing_tools/adapter/.data/complex/tests/x/y/z/b/test_spam.py deleted file mode 100644 index 4923c556c29a..000000000000 --- a/pythonFiles/tests/testing_tools/adapter/.data/complex/tests/x/y/z/b/test_spam.py +++ /dev/null @@ -1,8 +0,0 @@ - - -# ?!? -CHORUS = 'spamspamspamspamspam...' - - -def test_simple(): - assert True diff --git a/pythonFiles/tests/testing_tools/adapter/.data/complex/tests/x/y/z/test_ham.py b/pythonFiles/tests/testing_tools/adapter/.data/complex/tests/x/y/z/test_ham.py deleted file mode 100644 index 4c4134d75584..000000000000 --- a/pythonFiles/tests/testing_tools/adapter/.data/complex/tests/x/y/z/test_ham.py +++ /dev/null @@ -1,3 +0,0 @@ - -def test_simple(): - assert True diff --git a/pythonFiles/tests/testing_tools/adapter/.data/simple/tests/test_spam.py b/pythonFiles/tests/testing_tools/adapter/.data/simple/tests/test_spam.py deleted file mode 100644 index 4c4134d75584..000000000000 --- a/pythonFiles/tests/testing_tools/adapter/.data/simple/tests/test_spam.py +++ /dev/null @@ -1,3 +0,0 @@ - -def test_simple(): - assert True diff --git a/pythonFiles/tests/testing_tools/adapter/.data/syntax-error/tests/test_spam.py b/pythonFiles/tests/testing_tools/adapter/.data/syntax-error/tests/test_spam.py deleted file mode 100644 index 54d6400a3465..000000000000 --- a/pythonFiles/tests/testing_tools/adapter/.data/syntax-error/tests/test_spam.py +++ /dev/null @@ -1,7 +0,0 @@ - -def test_simple(): - assert True - - -# A syntax error: -: diff --git a/pythonFiles/tests/testing_tools/adapter/pytest/test_cli.py b/pythonFiles/tests/testing_tools/adapter/pytest/test_cli.py deleted file mode 100644 index 6f590a31fa56..000000000000 --- a/pythonFiles/tests/testing_tools/adapter/pytest/test_cli.py +++ /dev/null @@ -1,61 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -import unittest - -from ....util import Stub, StubProxy -from testing_tools.adapter.errors import UnsupportedCommandError -from testing_tools.adapter.pytest._cli import add_subparser - - -class StubSubparsers(StubProxy): - def __init__(self, stub=None, name="subparsers"): - super(StubSubparsers, self).__init__(stub, name) - - def add_parser(self, name): - self.add_call("add_parser", None, {"name": name}) - return self.return_add_parser - - -class StubArgParser(StubProxy): - def __init__(self, stub=None): - super(StubArgParser, self).__init__(stub, "argparser") - - def add_argument(self, *args, **kwargs): - self.add_call("add_argument", args, kwargs) - - -class AddCLISubparserTests(unittest.TestCase): - def test_discover(self): - stub = Stub() - subparsers = StubSubparsers(stub) - parser = StubArgParser(stub) - subparsers.return_add_parser = parser - - add_subparser("discover", "pytest", subparsers) - - self.assertEqual( - stub.calls, - [ - ("subparsers.add_parser", None, {"name": "pytest"}), - ], - ) - - def test_unsupported_command(self): - subparsers = StubSubparsers(name=None) - subparsers.return_add_parser = None - - with self.assertRaises(UnsupportedCommandError): - add_subparser("run", "pytest", subparsers) - with self.assertRaises(UnsupportedCommandError): - add_subparser("debug", "pytest", subparsers) - with self.assertRaises(UnsupportedCommandError): - add_subparser("???", "pytest", subparsers) - self.assertEqual( - subparsers.calls, - [ - ("add_parser", None, {"name": "pytest"}), - ("add_parser", None, {"name": "pytest"}), - ("add_parser", None, {"name": "pytest"}), - ], - ) diff --git a/pythonFiles/tests/testing_tools/adapter/pytest/test_discovery.py b/pythonFiles/tests/testing_tools/adapter/pytest/test_discovery.py deleted file mode 100644 index 83eeaa1f9062..000000000000 --- a/pythonFiles/tests/testing_tools/adapter/pytest/test_discovery.py +++ /dev/null @@ -1,1645 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -from __future__ import print_function, unicode_literals - -try: - from io import StringIO -except ImportError: - from StringIO import StringIO # type: ignore (for Pylance) - -import os -import sys -import tempfile -import unittest - -import _pytest.doctest -import pytest -from testing_tools.adapter import info -from testing_tools.adapter import util as adapter_util -from testing_tools.adapter.pytest import _discovery -from testing_tools.adapter.pytest import _pytest_item as pytest_item - -from .... import util - - -def unique(collection, key): - result = [] - keys = [] - for item in collection: - k = key(item) - if k in keys: - continue - result.append(item) - keys.append(k) - return result - - -class StubPyTest(util.StubProxy): - def __init__(self, stub=None): - super(StubPyTest, self).__init__(stub, "pytest") - self.return_main = 0 - - def main(self, args, plugins): - self.add_call("main", None, {"args": args, "plugins": plugins}) - return self.return_main - - -class StubPlugin(util.StubProxy): - _started = True - - def __init__(self, stub=None, tests=None): - super(StubPlugin, self).__init__(stub, "plugin") - if tests is None: - tests = StubDiscoveredTests(self.stub) - self._tests = tests - - def __getattr__(self, name): - if not name.startswith("pytest_"): - raise AttributeError(name) - - def func(*args, **kwargs): - self.add_call(name, args or None, kwargs or None) - - return func - - -class StubDiscoveredTests(util.StubProxy): - NOT_FOUND = object() - - def __init__(self, stub=None): - super(StubDiscoveredTests, self).__init__(stub, "discovered") - self.return_items = [] - self.return_parents = [] - - def __len__(self): - self.add_call("__len__", None, None) - return len(self.return_items) - - def __getitem__(self, index): - self.add_call("__getitem__", (index,), None) - return self.return_items[index] - - @property - def parents(self): - self.add_call("parents", None, None) - return self.return_parents - - def reset(self): - self.add_call("reset", None, None) - - def add_test(self, test, parents): - self.add_call("add_test", None, {"test": test, "parents": parents}) - - -class FakeFunc(object): - def __init__(self, name): - self.__name__ = name - - -class FakeMarker(object): - def __init__(self, name): - self.name = name - - -class StubPytestItem(util.StubProxy): - _debugging = False - _hasfunc = True - - def __init__(self, stub=None, **attrs): - super(StubPytestItem, self).__init__(stub, "pytest.Item") - if attrs.get("function") is None: - attrs.pop("function", None) - self._hasfunc = False - - attrs.setdefault("user_properties", []) - - slots = getattr(type(self), "__slots__", None) - if slots: - for name, value in attrs.items(): - if name in self.__slots__: - setattr(self, name, value) - else: - self.__dict__[name] = value - else: - self.__dict__.update(attrs) - - if "own_markers" not in attrs: - self.own_markers = () - - def __repr__(self): - return object.__repr__(self) - - def __getattr__(self, name): - if not self._debugging: - self.add_call(name + " (attr)", None, None) - if name == "function": - if not self._hasfunc: - raise AttributeError(name) - - def func(*args, **kwargs): - self.add_call(name, args or None, kwargs or None) - - return func - - -class StubSubtypedItem(StubPytestItem): - @classmethod - def from_args(cls, *args, **kwargs): - if not hasattr(cls, "from_parent"): - return cls(*args, **kwargs) - self = cls.from_parent(None, name=kwargs["name"], runner=None, dtest=None) - self.__init__(*args, **kwargs) - return self - - def __init__(self, *args, **kwargs): - super(StubSubtypedItem, self).__init__(*args, **kwargs) - if "nodeid" in self.__dict__: - self._nodeid = self.__dict__.pop("nodeid") - - @property - def location(self): - return self.__dict__.get("location") - - -class StubFunctionItem(StubSubtypedItem, pytest.Function): - @property - def function(self): - return self.__dict__.get("function") - - -def create_stub_function_item(*args, **kwargs): - return StubFunctionItem.from_args(*args, **kwargs) - - -class StubDoctestItem(StubSubtypedItem, _pytest.doctest.DoctestItem): - pass - - -def create_stub_doctest_item(*args, **kwargs): - return StubDoctestItem.from_args(*args, **kwargs) - - -class StubPytestSession(util.StubProxy): - def __init__(self, stub=None): - super(StubPytestSession, self).__init__(stub, "pytest.Session") - - def __getattr__(self, name): - self.add_call(name + " (attr)", None, None) - - def func(*args, **kwargs): - self.add_call(name, args or None, kwargs or None) - - return func - - -class StubPytestConfig(util.StubProxy): - def __init__(self, stub=None): - super(StubPytestConfig, self).__init__(stub, "pytest.Config") - - def __getattr__(self, name): - self.add_call(name + " (attr)", None, None) - - def func(*args, **kwargs): - self.add_call(name, args or None, kwargs or None) - - return func - - -def generate_parse_item(pathsep): - if pathsep == "\\": - - def normcase(path): - path = path.lower() - return path.replace("/", "\\") - - else: - raise NotImplementedError - - ########## - def _fix_fileid(*args): - return adapter_util.fix_fileid( - *args, - **dict( - # dependency injection - _normcase=normcase, - _pathsep=pathsep, - ) - ) - - def _normalize_test_id(*args): - return pytest_item._normalize_test_id( - *args, - **dict( - # dependency injection - _fix_fileid=_fix_fileid, - _pathsep=pathsep, - ) - ) - - def _iter_nodes(*args): - return pytest_item._iter_nodes( - *args, - **dict( - # dependency injection - _normalize_test_id=_normalize_test_id, - _normcase=normcase, - _pathsep=pathsep, - ) - ) - - def _parse_node_id(*args): - return pytest_item._parse_node_id( - *args, - **dict( - # dependency injection - _iter_nodes=_iter_nodes, - ) - ) - - ########## - def _split_fspath(*args): - return pytest_item._split_fspath( - *args, - **dict( - # dependency injection - _normcase=normcase, - ) - ) - - ########## - def _matches_relfile(*args): - return pytest_item._matches_relfile( - *args, - **dict( - # dependency injection - _normcase=normcase, - _pathsep=pathsep, - ) - ) - - def _is_legacy_wrapper(*args): - return pytest_item._is_legacy_wrapper( - *args, - **dict( - # dependency injection - _pathsep=pathsep, - ) - ) - - def _get_location(*args): - return pytest_item._get_location( - *args, - **dict( - # dependency injection - _matches_relfile=_matches_relfile, - _is_legacy_wrapper=_is_legacy_wrapper, - _pathsep=pathsep, - ) - ) - - ########## - def _parse_item(item): - return pytest_item.parse_item( - item, - **dict( - # dependency injection - _parse_node_id=_parse_node_id, - _split_fspath=_split_fspath, - _get_location=_get_location, - ) - ) - - return _parse_item - - -################################## -# tests - - -def fake_pytest_main(stub, use_fd, pytest_stdout): - def ret(args, plugins): - stub.add_call("pytest.main", None, {"args": args, "plugins": plugins}) - if use_fd: - os.write(sys.stdout.fileno(), pytest_stdout.encode()) - else: - print(pytest_stdout, end="") - return 0 - - return ret - - -class DiscoverTests(unittest.TestCase): - DEFAULT_ARGS = [ - "--collect-only", - ] - - def test_basic(self): - stub = util.Stub() - stubpytest = StubPyTest(stub) - plugin = StubPlugin(stub) - expected = [] - plugin.discovered = expected - calls = [ - ("pytest.main", None, {"args": self.DEFAULT_ARGS, "plugins": [plugin]}), - ("discovered.parents", None, None), - ("discovered.__len__", None, None), - ("discovered.__getitem__", (0,), None), - ] - - parents, tests = _discovery.discover( - [], _pytest_main=stubpytest.main, _plugin=plugin - ) - - actual_calls = unique(stub.calls, lambda k: k[0]) - expected_calls = unique(calls, lambda k: k[0]) - - self.assertEqual(parents, []) - self.assertEqual(tests, expected) - self.assertEqual(actual_calls, expected_calls) - - def test_failure(self): - stub = util.Stub() - pytest = StubPyTest(stub) - pytest.return_main = 2 - plugin = StubPlugin(stub) - - with self.assertRaises(Exception): - _discovery.discover([], _pytest_main=pytest.main, _plugin=plugin) - - self.assertEqual( - stub.calls, - [ - # There's only one call. - ("pytest.main", None, {"args": self.DEFAULT_ARGS, "plugins": [plugin]}), - ], - ) - - def test_no_tests_found(self): - stub = util.Stub() - pytest = StubPyTest(stub) - pytest.return_main = 5 - plugin = StubPlugin(stub) - expected = [] - plugin.discovered = expected - calls = [ - ("pytest.main", None, {"args": self.DEFAULT_ARGS, "plugins": [plugin]}), - ("discovered.parents", None, None), - ("discovered.__len__", None, None), - ("discovered.__getitem__", (0,), None), - ] - - parents, tests = _discovery.discover( - [], _pytest_main=pytest.main, _plugin=plugin - ) - - actual_calls = unique(stub.calls, lambda k: k[0]) - expected_calls = unique(calls, lambda k: k[0]) - - self.assertEqual(parents, []) - self.assertEqual(tests, expected) - self.assertEqual(actual_calls, expected_calls) - - def test_found_with_collection_error(self): - stub = util.Stub() - pytest = StubPyTest(stub) - pytest.return_main = 1 - plugin = StubPlugin(stub) - expected = [] - plugin.discovered = expected - calls = [ - ("pytest.main", None, {"args": self.DEFAULT_ARGS, "plugins": [plugin]}), - ("discovered.parents", None, None), - ("discovered.__len__", None, None), - ("discovered.__getitem__", (0,), None), - ] - - parents, tests = _discovery.discover( - [], _pytest_main=pytest.main, _plugin=plugin - ) - - actual_calls = unique(stub.calls, lambda k: k[0]) - expected_calls = unique(calls, lambda k: k[0]) - - self.assertEqual(parents, []) - self.assertEqual(tests, expected) - self.assertEqual(actual_calls, expected_calls) - - def test_stdio_hidden_file(self): - stub = util.Stub() - - plugin = StubPlugin(stub) - plugin.discovered = [] - calls = [ - ("pytest.main", None, {"args": self.DEFAULT_ARGS, "plugins": [plugin]}), - ("discovered.parents", None, None), - ("discovered.__len__", None, None), - ("discovered.__getitem__", (0,), None), - ] - pytest_stdout = "spamspamspamspamspamspamspammityspam" - - # to simulate stdio behavior in methods like os.dup, - # use actual files (rather than StringIO) - with tempfile.TemporaryFile("r+") as mock: - sys.stdout = mock - try: - _discovery.discover( - [], - hidestdio=True, - _pytest_main=fake_pytest_main(stub, False, pytest_stdout), - _plugin=plugin, - ) - finally: - sys.stdout = sys.__stdout__ - - mock.seek(0) - captured = mock.read() - - actual_calls = unique(stub.calls, lambda k: k[0]) - expected_calls = unique(calls, lambda k: k[0]) - - self.assertEqual(captured, "") - self.assertEqual(actual_calls, expected_calls) - - def test_stdio_hidden_fd(self): - # simulate cases where stdout comes from the lower layer than sys.stdout - # via file descriptors (e.g., from cython) - stub = util.Stub() - plugin = StubPlugin(stub) - pytest_stdout = "spamspamspamspamspamspamspammityspam" - - # Replace with contextlib.redirect_stdout() once Python 2.7 support is dropped. - sys.stdout = StringIO() - try: - _discovery.discover( - [], - hidestdio=True, - _pytest_main=fake_pytest_main(stub, True, pytest_stdout), - _plugin=plugin, - ) - captured = sys.stdout.read() - self.assertEqual(captured, "") - finally: - sys.stdout = sys.__stdout__ - - def test_stdio_not_hidden_file(self): - stub = util.Stub() - - plugin = StubPlugin(stub) - plugin.discovered = [] - calls = [ - ("pytest.main", None, {"args": self.DEFAULT_ARGS, "plugins": [plugin]}), - ("discovered.parents", None, None), - ("discovered.__len__", None, None), - ("discovered.__getitem__", (0,), None), - ] - pytest_stdout = "spamspamspamspamspamspamspammityspam" - - buf = StringIO() - - sys.stdout = buf - try: - _discovery.discover( - [], - hidestdio=False, - _pytest_main=fake_pytest_main(stub, False, pytest_stdout), - _plugin=plugin, - ) - finally: - sys.stdout = sys.__stdout__ - captured = buf.getvalue() - - actual_calls = unique(stub.calls, lambda k: k[0]) - expected_calls = unique(calls, lambda k: k[0]) - - self.assertEqual(captured, pytest_stdout) - self.assertEqual(actual_calls, expected_calls) - - def test_stdio_not_hidden_fd(self): - # simulate cases where stdout comes from the lower layer than sys.stdout - # via file descriptors (e.g., from cython) - stub = util.Stub() - plugin = StubPlugin(stub) - pytest_stdout = "spamspamspamspamspamspamspammityspam" - stub.calls = [] - with tempfile.TemporaryFile("r+") as mock: - sys.stdout = mock - try: - _discovery.discover( - [], - hidestdio=False, - _pytest_main=fake_pytest_main(stub, True, pytest_stdout), - _plugin=plugin, - ) - finally: - mock.seek(0) - captured = sys.stdout.read() - sys.stdout = sys.__stdout__ - self.assertEqual(captured, pytest_stdout) - - -class CollectorTests(unittest.TestCase): - def test_modifyitems(self): - stub = util.Stub() - discovered = StubDiscoveredTests(stub) - session = StubPytestSession(stub) - config = StubPytestConfig(stub) - collector = _discovery.TestCollector(tests=discovered) - - testroot = adapter_util.ABS_PATH(adapter_util.fix_path("/a/b/c")) - relfile1 = adapter_util.fix_path("./test_spam.py") - relfile2 = adapter_util.fix_path("x/y/z/test_eggs.py") - - collector.pytest_collection_modifyitems( - session, - config, - [ - create_stub_function_item( - stub, - nodeid="test_spam.py::SpamTests::test_one", - name="test_one", - originalname=None, - location=("test_spam.py", 12, "SpamTests.test_one"), - path=adapter_util.PATH_JOIN(testroot, "test_spam.py"), - function=FakeFunc("test_one"), - ), - create_stub_function_item( - stub, - nodeid="test_spam.py::SpamTests::test_other", - name="test_other", - originalname=None, - location=("test_spam.py", 19, "SpamTests.test_other"), - path=adapter_util.PATH_JOIN(testroot, "test_spam.py"), - function=FakeFunc("test_other"), - ), - create_stub_function_item( - stub, - nodeid="test_spam.py::test_all", - name="test_all", - originalname=None, - location=("test_spam.py", 144, "test_all"), - path=adapter_util.PATH_JOIN(testroot, "test_spam.py"), - function=FakeFunc("test_all"), - ), - create_stub_function_item( - stub, - nodeid="test_spam.py::test_each[10-10]", - name="test_each[10-10]", - originalname="test_each", - location=("test_spam.py", 273, "test_each[10-10]"), - path=adapter_util.PATH_JOIN(testroot, "test_spam.py"), - function=FakeFunc("test_each"), - ), - create_stub_function_item( - stub, - nodeid=relfile2 + "::All::BasicTests::test_first", - name="test_first", - originalname=None, - location=(relfile2, 31, "All.BasicTests.test_first"), - path=adapter_util.PATH_JOIN(testroot, relfile2), - function=FakeFunc("test_first"), - ), - create_stub_function_item( - stub, - nodeid=relfile2 + "::All::BasicTests::test_each[1+2-3]", - name="test_each[1+2-3]", - originalname="test_each", - location=(relfile2, 62, "All.BasicTests.test_each[1+2-3]"), - path=adapter_util.PATH_JOIN(testroot, relfile2), - function=FakeFunc("test_each"), - own_markers=[ - FakeMarker(v) - for v in [ - # supported - "skip", - "skipif", - "xfail", - # duplicate - "skip", - # ignored (pytest-supported) - "parameterize", - "usefixtures", - "filterwarnings", - # ignored (custom) - "timeout", - ] - ], - ), - ], - ) - - self.maxDiff = None - expected = [ - ("discovered.reset", None, None), - ( - "discovered.add_test", - None, - dict( - parents=[ - ("./test_spam.py::SpamTests", "SpamTests", "suite"), - ("./test_spam.py", "test_spam.py", "file"), - (".", testroot, "folder"), - ], - test=info.SingleTestInfo( - id="./test_spam.py::SpamTests::test_one", - name="test_one", - path=info.SingleTestPath( - root=testroot, - relfile=relfile1, - func="SpamTests.test_one", - sub=None, - ), - source="{}:{}".format(relfile1, 13), - markers=None, - parentid="./test_spam.py::SpamTests", - ), - ), - ), - ( - "discovered.add_test", - None, - dict( - parents=[ - ("./test_spam.py::SpamTests", "SpamTests", "suite"), - ("./test_spam.py", "test_spam.py", "file"), - (".", testroot, "folder"), - ], - test=info.SingleTestInfo( - id="./test_spam.py::SpamTests::test_other", - name="test_other", - path=info.SingleTestPath( - root=testroot, - relfile=relfile1, - func="SpamTests.test_other", - sub=None, - ), - source="{}:{}".format(relfile1, 20), - markers=None, - parentid="./test_spam.py::SpamTests", - ), - ), - ), - ( - "discovered.add_test", - None, - dict( - parents=[ - ("./test_spam.py", "test_spam.py", "file"), - (".", testroot, "folder"), - ], - test=info.SingleTestInfo( - id="./test_spam.py::test_all", - name="test_all", - path=info.SingleTestPath( - root=testroot, - relfile=relfile1, - func="test_all", - sub=None, - ), - source="{}:{}".format(relfile1, 145), - markers=None, - parentid="./test_spam.py", - ), - ), - ), - ( - "discovered.add_test", - None, - dict( - parents=[ - ("./test_spam.py::test_each", "test_each", "function"), - ("./test_spam.py", "test_spam.py", "file"), - (".", testroot, "folder"), - ], - test=info.SingleTestInfo( - id="./test_spam.py::test_each[10-10]", - name="10-10", - path=info.SingleTestPath( - root=testroot, - relfile=relfile1, - func="test_each", - sub=["[10-10]"], - ), - source="{}:{}".format(relfile1, 274), - markers=None, - parentid="./test_spam.py::test_each", - ), - ), - ), - ( - "discovered.add_test", - None, - dict( - parents=[ - ( - "./x/y/z/test_eggs.py::All::BasicTests", - "BasicTests", - "suite", - ), - ("./x/y/z/test_eggs.py::All", "All", "suite"), - ("./x/y/z/test_eggs.py", "test_eggs.py", "file"), - ("./x/y/z", "z", "folder"), - ("./x/y", "y", "folder"), - ("./x", "x", "folder"), - (".", testroot, "folder"), - ], - test=info.SingleTestInfo( - id="./x/y/z/test_eggs.py::All::BasicTests::test_first", - name="test_first", - path=info.SingleTestPath( - root=testroot, - relfile=adapter_util.fix_relpath(relfile2), - func="All.BasicTests.test_first", - sub=None, - ), - source="{}:{}".format(adapter_util.fix_relpath(relfile2), 32), - markers=None, - parentid="./x/y/z/test_eggs.py::All::BasicTests", - ), - ), - ), - ( - "discovered.add_test", - None, - dict( - parents=[ - ( - "./x/y/z/test_eggs.py::All::BasicTests::test_each", - "test_each", - "function", - ), - ( - "./x/y/z/test_eggs.py::All::BasicTests", - "BasicTests", - "suite", - ), - ("./x/y/z/test_eggs.py::All", "All", "suite"), - ("./x/y/z/test_eggs.py", "test_eggs.py", "file"), - ("./x/y/z", "z", "folder"), - ("./x/y", "y", "folder"), - ("./x", "x", "folder"), - (".", testroot, "folder"), - ], - test=info.SingleTestInfo( - id="./x/y/z/test_eggs.py::All::BasicTests::test_each[1+2-3]", - name="1+2-3", - path=info.SingleTestPath( - root=testroot, - relfile=adapter_util.fix_relpath(relfile2), - func="All.BasicTests.test_each", - sub=["[1+2-3]"], - ), - source="{}:{}".format(adapter_util.fix_relpath(relfile2), 63), - markers=["expected-failure", "skip", "skip-if"], - parentid="./x/y/z/test_eggs.py::All::BasicTests::test_each", - ), - ), - ), - ] - self.assertEqual(stub.calls, expected) - - def test_finish(self): - stub = util.Stub() - discovered = StubDiscoveredTests(stub) - session = StubPytestSession(stub) - testroot = adapter_util.ABS_PATH(adapter_util.fix_path("/a/b/c")) - relfile = adapter_util.fix_path("x/y/z/test_eggs.py") - session.items = [ - create_stub_function_item( - stub, - nodeid=relfile + "::SpamTests::test_spam", - name="test_spam", - originalname=None, - location=(relfile, 12, "SpamTests.test_spam"), - path=adapter_util.PATH_JOIN(testroot, relfile), - function=FakeFunc("test_spam"), - ), - ] - collector = _discovery.TestCollector(tests=discovered) - - collector.pytest_collection_finish(session) - - self.maxDiff = None - self.assertEqual( - stub.calls, - [ - ("discovered.reset", None, None), - ( - "discovered.add_test", - None, - dict( - parents=[ - ("./x/y/z/test_eggs.py::SpamTests", "SpamTests", "suite"), - ("./x/y/z/test_eggs.py", "test_eggs.py", "file"), - ("./x/y/z", "z", "folder"), - ("./x/y", "y", "folder"), - ("./x", "x", "folder"), - (".", testroot, "folder"), - ], - test=info.SingleTestInfo( - id="./x/y/z/test_eggs.py::SpamTests::test_spam", - name="test_spam", - path=info.SingleTestPath( - root=testroot, - relfile=adapter_util.fix_relpath(relfile), - func="SpamTests.test_spam", - sub=None, - ), - source="{}:{}".format( - adapter_util.fix_relpath(relfile), 13 - ), - markers=None, - parentid="./x/y/z/test_eggs.py::SpamTests", - ), - ), - ), - ], - ) - - def test_doctest(self): - stub = util.Stub() - discovered = StubDiscoveredTests(stub) - session = StubPytestSession(stub) - testroot = adapter_util.ABS_PATH(adapter_util.fix_path("/a/b/c")) - doctestfile = adapter_util.fix_path("x/test_doctest.txt") - relfile = adapter_util.fix_path("x/y/z/test_eggs.py") - session.items = [ - create_stub_doctest_item( - stub, - nodeid=doctestfile + "::test_doctest.txt", - name="test_doctest.txt", - location=(doctestfile, 0, "[doctest] test_doctest.txt"), - path=adapter_util.PATH_JOIN(testroot, doctestfile), - ), - # With --doctest-modules - create_stub_doctest_item( - stub, - nodeid=relfile + "::test_eggs", - name="test_eggs", - location=(relfile, 0, "[doctest] test_eggs"), - path=adapter_util.PATH_JOIN(testroot, relfile), - ), - create_stub_doctest_item( - stub, - nodeid=relfile + "::test_eggs.TestSpam", - name="test_eggs.TestSpam", - location=(relfile, 12, "[doctest] test_eggs.TestSpam"), - path=adapter_util.PATH_JOIN(testroot, relfile), - ), - create_stub_doctest_item( - stub, - nodeid=relfile + "::test_eggs.TestSpam.TestEggs", - name="test_eggs.TestSpam.TestEggs", - location=(relfile, 27, "[doctest] test_eggs.TestSpam.TestEggs"), - path=adapter_util.PATH_JOIN(testroot, relfile), - ), - ] - collector = _discovery.TestCollector(tests=discovered) - - collector.pytest_collection_finish(session) - - self.maxDiff = None - self.assertEqual( - stub.calls, - [ - ("discovered.reset", None, None), - ( - "discovered.add_test", - None, - dict( - parents=[ - ("./x/test_doctest.txt", "test_doctest.txt", "file"), - ("./x", "x", "folder"), - (".", testroot, "folder"), - ], - test=info.SingleTestInfo( - id="./x/test_doctest.txt::test_doctest.txt", - name="test_doctest.txt", - path=info.SingleTestPath( - root=testroot, - relfile=adapter_util.fix_relpath(doctestfile), - func=None, - ), - source="{}:{}".format( - adapter_util.fix_relpath(doctestfile), 1 - ), - markers=[], - parentid="./x/test_doctest.txt", - ), - ), - ), - ( - "discovered.add_test", - None, - dict( - parents=[ - ("./x/y/z/test_eggs.py", "test_eggs.py", "file"), - ("./x/y/z", "z", "folder"), - ("./x/y", "y", "folder"), - ("./x", "x", "folder"), - (".", testroot, "folder"), - ], - test=info.SingleTestInfo( - id="./x/y/z/test_eggs.py::test_eggs", - name="test_eggs", - path=info.SingleTestPath( - root=testroot, - relfile=adapter_util.fix_relpath(relfile), - func=None, - ), - source="{}:{}".format(adapter_util.fix_relpath(relfile), 1), - markers=[], - parentid="./x/y/z/test_eggs.py", - ), - ), - ), - ( - "discovered.add_test", - None, - dict( - parents=[ - ("./x/y/z/test_eggs.py", "test_eggs.py", "file"), - ("./x/y/z", "z", "folder"), - ("./x/y", "y", "folder"), - ("./x", "x", "folder"), - (".", testroot, "folder"), - ], - test=info.SingleTestInfo( - id="./x/y/z/test_eggs.py::test_eggs.TestSpam", - name="test_eggs.TestSpam", - path=info.SingleTestPath( - root=testroot, - relfile=adapter_util.fix_relpath(relfile), - func=None, - ), - source="{}:{}".format( - adapter_util.fix_relpath(relfile), 13 - ), - markers=[], - parentid="./x/y/z/test_eggs.py", - ), - ), - ), - ( - "discovered.add_test", - None, - dict( - parents=[ - ("./x/y/z/test_eggs.py", "test_eggs.py", "file"), - ("./x/y/z", "z", "folder"), - ("./x/y", "y", "folder"), - ("./x", "x", "folder"), - (".", testroot, "folder"), - ], - test=info.SingleTestInfo( - id="./x/y/z/test_eggs.py::test_eggs.TestSpam.TestEggs", - name="test_eggs.TestSpam.TestEggs", - path=info.SingleTestPath( - root=testroot, - relfile=adapter_util.fix_relpath(relfile), - func=None, - ), - source="{}:{}".format( - adapter_util.fix_relpath(relfile), 28 - ), - markers=[], - parentid="./x/y/z/test_eggs.py", - ), - ), - ), - ], - ) - - def test_nested_brackets(self): - stub = util.Stub() - discovered = StubDiscoveredTests(stub) - session = StubPytestSession(stub) - testroot = adapter_util.ABS_PATH(adapter_util.fix_path("/a/b/c")) - relfile = adapter_util.fix_path("x/y/z/test_eggs.py") - session.items = [ - create_stub_function_item( - stub, - nodeid=relfile + "::SpamTests::test_spam[a-[b]-c]", - name="test_spam[a-[b]-c]", - originalname="test_spam", - location=(relfile, 12, "SpamTests.test_spam[a-[b]-c]"), - path=adapter_util.PATH_JOIN(testroot, relfile), - function=FakeFunc("test_spam"), - ), - ] - collector = _discovery.TestCollector(tests=discovered) - - collector.pytest_collection_finish(session) - - self.maxDiff = None - self.assertEqual( - stub.calls, - [ - ("discovered.reset", None, None), - ( - "discovered.add_test", - None, - dict( - parents=[ - ( - "./x/y/z/test_eggs.py::SpamTests::test_spam", - "test_spam", - "function", - ), - ("./x/y/z/test_eggs.py::SpamTests", "SpamTests", "suite"), - ("./x/y/z/test_eggs.py", "test_eggs.py", "file"), - ("./x/y/z", "z", "folder"), - ("./x/y", "y", "folder"), - ("./x", "x", "folder"), - (".", testroot, "folder"), - ], - test=info.SingleTestInfo( - id="./x/y/z/test_eggs.py::SpamTests::test_spam[a-[b]-c]", - name="a-[b]-c", - path=info.SingleTestPath( - root=testroot, - relfile=adapter_util.fix_relpath(relfile), - func="SpamTests.test_spam", - sub=["[a-[b]-c]"], - ), - source="{}:{}".format( - adapter_util.fix_relpath(relfile), 13 - ), - markers=None, - parentid="./x/y/z/test_eggs.py::SpamTests::test_spam", - ), - ), - ), - ], - ) - - def test_nested_suite(self): - stub = util.Stub() - discovered = StubDiscoveredTests(stub) - session = StubPytestSession(stub) - testroot = adapter_util.ABS_PATH(adapter_util.fix_path("/a/b/c")) - relfile = adapter_util.fix_path("x/y/z/test_eggs.py") - session.items = [ - create_stub_function_item( - stub, - nodeid=relfile + "::SpamTests::Ham::Eggs::test_spam", - name="test_spam", - originalname=None, - location=(relfile, 12, "SpamTests.Ham.Eggs.test_spam"), - path=adapter_util.PATH_JOIN(testroot, relfile), - function=FakeFunc("test_spam"), - ), - ] - collector = _discovery.TestCollector(tests=discovered) - - collector.pytest_collection_finish(session) - - self.maxDiff = None - self.assertEqual( - stub.calls, - [ - ("discovered.reset", None, None), - ( - "discovered.add_test", - None, - dict( - parents=[ - ( - "./x/y/z/test_eggs.py::SpamTests::Ham::Eggs", - "Eggs", - "suite", - ), - ("./x/y/z/test_eggs.py::SpamTests::Ham", "Ham", "suite"), - ("./x/y/z/test_eggs.py::SpamTests", "SpamTests", "suite"), - ("./x/y/z/test_eggs.py", "test_eggs.py", "file"), - ("./x/y/z", "z", "folder"), - ("./x/y", "y", "folder"), - ("./x", "x", "folder"), - (".", testroot, "folder"), - ], - test=info.SingleTestInfo( - id="./x/y/z/test_eggs.py::SpamTests::Ham::Eggs::test_spam", - name="test_spam", - path=info.SingleTestPath( - root=testroot, - relfile=adapter_util.fix_relpath(relfile), - func="SpamTests.Ham.Eggs.test_spam", - sub=None, - ), - source="{}:{}".format( - adapter_util.fix_relpath(relfile), 13 - ), - markers=None, - parentid="./x/y/z/test_eggs.py::SpamTests::Ham::Eggs", - ), - ), - ), - ], - ) - - @pytest.mark.skipif(sys.platform != "win32", reason="Windows specific test.") - def test_windows(self): - stub = util.Stub() - discovered = StubDiscoveredTests(stub) - session = StubPytestSession(stub) - testroot = r"C:\A\B\C" - relfile = r"X\Y\Z\test_Eggs.py" - session.items = [ - # typical: - create_stub_function_item( - stub, - # pytest always uses "/" as the path separator in node IDs: - nodeid="X/Y/Z/test_Eggs.py::SpamTests::test_spam", - name="test_spam", - originalname=None, - # normal path separator (contrast with nodeid): - location=(relfile, 12, "SpamTests.test_spam"), - # path separator matches location: - path=testroot + "\\" + relfile, - function=FakeFunc("test_spam"), - ), - ] - tests = [ - # permutations of path separators - (r"X/test_a.py", "\\", "\\"), # typical - (r"X/test_b.py", "\\", "/"), - (r"X/test_c.py", "/", "\\"), - (r"X/test_d.py", "/", "/"), - (r"X\test_e.py", "\\", "\\"), - (r"X\test_f.py", "\\", "/"), - (r"X\test_g.py", "/", "\\"), - (r"X\test_h.py", "/", "/"), - ] - for fileid, locfile, fspath in tests: - if locfile == "/": - locfile = fileid.replace("\\", "/") - elif locfile == "\\": - locfile = fileid.replace("/", "\\") - if fspath == "/": - fspath = (testroot + "/" + fileid).replace("\\", "/") - elif fspath == "\\": - fspath = (testroot + "/" + fileid).replace("/", "\\") - session.items.append( - create_stub_function_item( - stub, - nodeid=fileid + "::test_spam", - name="test_spam", - originalname=None, - location=(locfile, 12, "test_spam"), - path=fspath, - function=FakeFunc("test_spam"), - ) - ) - collector = _discovery.TestCollector(tests=discovered) - if os.name != "nt": - collector.parse_item = generate_parse_item("\\") - - collector.pytest_collection_finish(session) - - self.maxDiff = None - expected = [ - ("discovered.reset", None, None), - ( - "discovered.add_test", - None, - dict( - parents=[ - (r"./X/Y/Z/test_Eggs.py::SpamTests", "SpamTests", "suite"), - (r"./X/Y/Z/test_Eggs.py", "test_Eggs.py", "file"), - (r"./X/Y/Z", "Z", "folder"), - (r"./X/Y", "Y", "folder"), - (r"./X", "X", "folder"), - (".", testroot, "folder"), - ], - test=info.SingleTestInfo( - id=r"./X/Y/Z/test_Eggs.py::SpamTests::test_spam", - name="test_spam", - path=info.SingleTestPath( - root=testroot, # not normalized - relfile=r".\X\Y\Z\test_Eggs.py", # not normalized - func="SpamTests.test_spam", - sub=None, - ), - source=r".\X\Y\Z\test_Eggs.py:13", # not normalized - markers=None, - parentid=r"./X/Y/Z/test_Eggs.py::SpamTests", - ), - ), - ), - # permutations - # (*all* the IDs use "/") - # (source path separator should match relfile, not location) - # /, \, \ - ( - "discovered.add_test", - None, - dict( - parents=[ - (r"./X/test_a.py", "test_a.py", "file"), - (r"./X", "X", "folder"), - (".", testroot, "folder"), - ], - test=info.SingleTestInfo( - id=r"./X/test_a.py::test_spam", - name="test_spam", - path=info.SingleTestPath( - root=testroot, - relfile=r".\X\test_a.py", - func="test_spam", - sub=None, - ), - source=r".\X\test_a.py:13", - markers=None, - parentid=r"./X/test_a.py", - ), - ), - ), - # /, \, / - ( - "discovered.add_test", - None, - dict( - parents=[ - (r"./X/test_b.py", "test_b.py", "file"), - (r"./X", "X", "folder"), - (".", testroot, "folder"), - ], - test=info.SingleTestInfo( - id=r"./X/test_b.py::test_spam", - name="test_spam", - path=info.SingleTestPath( - root=testroot, - relfile=r".\X\test_b.py", - func="test_spam", - sub=None, - ), - source=r".\X\test_b.py:13", - markers=None, - parentid=r"./X/test_b.py", - ), - ), - ), - # /, /, \ - ( - "discovered.add_test", - None, - dict( - parents=[ - (r"./X/test_c.py", "test_c.py", "file"), - (r"./X", "X", "folder"), - (".", testroot, "folder"), - ], - test=info.SingleTestInfo( - id=r"./X/test_c.py::test_spam", - name="test_spam", - path=info.SingleTestPath( - root=testroot, - relfile=r".\X\test_c.py", - func="test_spam", - sub=None, - ), - source=r".\X\test_c.py:13", - markers=None, - parentid=r"./X/test_c.py", - ), - ), - ), - # /, /, / - ( - "discovered.add_test", - None, - dict( - parents=[ - (r"./X/test_d.py", "test_d.py", "file"), - (r"./X", "X", "folder"), - (".", testroot, "folder"), - ], - test=info.SingleTestInfo( - id=r"./X/test_d.py::test_spam", - name="test_spam", - path=info.SingleTestPath( - root=testroot, - relfile=r".\X\test_d.py", - func="test_spam", - sub=None, - ), - source=r".\X\test_d.py:13", - markers=None, - parentid=r"./X/test_d.py", - ), - ), - ), - # \, \, \ - ( - "discovered.add_test", - None, - dict( - parents=[ - (r"./X/test_e.py", "test_e.py", "file"), - (r"./X", "X", "folder"), - (".", testroot, "folder"), - ], - test=info.SingleTestInfo( - id=r"./X/test_e.py::test_spam", - name="test_spam", - path=info.SingleTestPath( - root=testroot, - relfile=r".\X\test_e.py", - func="test_spam", - sub=None, - ), - source=r".\X\test_e.py:13", - markers=None, - parentid=r"./X/test_e.py", - ), - ), - ), - # \, \, / - ( - "discovered.add_test", - None, - dict( - parents=[ - (r"./X/test_f.py", "test_f.py", "file"), - (r"./X", "X", "folder"), - (".", testroot, "folder"), - ], - test=info.SingleTestInfo( - id=r"./X/test_f.py::test_spam", - name="test_spam", - path=info.SingleTestPath( - root=testroot, - relfile=r".\X\test_f.py", - func="test_spam", - sub=None, - ), - source=r".\X\test_f.py:13", - markers=None, - parentid=r"./X/test_f.py", - ), - ), - ), - # \, /, \ - ( - "discovered.add_test", - None, - dict( - parents=[ - (r"./X/test_g.py", "test_g.py", "file"), - (r"./X", "X", "folder"), - (".", testroot, "folder"), - ], - test=info.SingleTestInfo( - id=r"./X/test_g.py::test_spam", - name="test_spam", - path=info.SingleTestPath( - root=testroot, - relfile=r".\X\test_g.py", - func="test_spam", - sub=None, - ), - source=r".\X\test_g.py:13", - markers=None, - parentid=r"./X/test_g.py", - ), - ), - ), - # \, /, / - ( - "discovered.add_test", - None, - dict( - parents=[ - (r"./X/test_h.py", "test_h.py", "file"), - (r"./X", "X", "folder"), - (".", testroot, "folder"), - ], - test=info.SingleTestInfo( - id=r"./X/test_h.py::test_spam", - name="test_spam", - path=info.SingleTestPath( - root=testroot, - relfile=r".\X\test_h.py", - func="test_spam", - sub=None, - ), - source=r".\X\test_h.py:13", - markers=None, - parentid=r"./X/test_h.py", - ), - ), - ), - ] - self.assertEqual(stub.calls, expected) - - def test_mysterious_parens(self): - stub = util.Stub() - discovered = StubDiscoveredTests(stub) - session = StubPytestSession(stub) - testroot = adapter_util.ABS_PATH(adapter_util.fix_path("/a/b/c")) - relfile = adapter_util.fix_path("x/y/z/test_eggs.py") - session.items = [ - create_stub_function_item( - stub, - nodeid=relfile + "::SpamTests::()::()::test_spam", - name="test_spam", - originalname=None, - location=(relfile, 12, "SpamTests.test_spam"), - path=adapter_util.PATH_JOIN(testroot, relfile), - function=FakeFunc("test_spam"), - ), - ] - collector = _discovery.TestCollector(tests=discovered) - - collector.pytest_collection_finish(session) - - self.maxDiff = None - self.assertEqual( - stub.calls, - [ - ("discovered.reset", None, None), - ( - "discovered.add_test", - None, - dict( - parents=[ - ("./x/y/z/test_eggs.py::SpamTests", "SpamTests", "suite"), - ("./x/y/z/test_eggs.py", "test_eggs.py", "file"), - ("./x/y/z", "z", "folder"), - ("./x/y", "y", "folder"), - ("./x", "x", "folder"), - (".", testroot, "folder"), - ], - test=info.SingleTestInfo( - id="./x/y/z/test_eggs.py::SpamTests::test_spam", - name="test_spam", - path=info.SingleTestPath( - root=testroot, - relfile=adapter_util.fix_relpath(relfile), - func="SpamTests.test_spam", - sub=[], - ), - source="{}:{}".format( - adapter_util.fix_relpath(relfile), 13 - ), - markers=None, - parentid="./x/y/z/test_eggs.py::SpamTests", - ), - ), - ), - ], - ) - - def test_mysterious_colons(self): - stub = util.Stub() - discovered = StubDiscoveredTests(stub) - session = StubPytestSession(stub) - testroot = adapter_util.ABS_PATH(adapter_util.fix_path("/a/b/c")) - relfile = adapter_util.fix_path("x/y/z/test_eggs.py") - session.items = [ - create_stub_function_item( - stub, - nodeid=relfile + "::SpamTests:::()::test_spam", - name="test_spam", - originalname=None, - location=(relfile, 12, "SpamTests.test_spam"), - path=adapter_util.PATH_JOIN(testroot, relfile), - function=FakeFunc("test_spam"), - ), - ] - collector = _discovery.TestCollector(tests=discovered) - - collector.pytest_collection_finish(session) - - self.maxDiff = None - self.assertEqual( - stub.calls, - [ - ("discovered.reset", None, None), - ( - "discovered.add_test", - None, - dict( - parents=[ - ("./x/y/z/test_eggs.py::SpamTests", "SpamTests", "suite"), - ("./x/y/z/test_eggs.py", "test_eggs.py", "file"), - ("./x/y/z", "z", "folder"), - ("./x/y", "y", "folder"), - ("./x", "x", "folder"), - (".", testroot, "folder"), - ], - test=info.SingleTestInfo( - id="./x/y/z/test_eggs.py::SpamTests::test_spam", - name="test_spam", - path=info.SingleTestPath( - root=testroot, - relfile=adapter_util.fix_relpath(relfile), - func="SpamTests.test_spam", - sub=[], - ), - source="{}:{}".format( - adapter_util.fix_relpath(relfile), 13 - ), - markers=None, - parentid="./x/y/z/test_eggs.py::SpamTests", - ), - ), - ), - ], - ) - - def test_imported_test(self): - # pytest will even discover tests that were imported from - # another module! - stub = util.Stub() - discovered = StubDiscoveredTests(stub) - session = StubPytestSession(stub) - testroot = adapter_util.ABS_PATH(adapter_util.fix_path("/a/b/c")) - relfile = adapter_util.fix_path("x/y/z/test_eggs.py") - srcfile = adapter_util.fix_path("x/y/z/_extern.py") - session.items = [ - create_stub_function_item( - stub, - nodeid=relfile + "::SpamTests::test_spam", - name="test_spam", - originalname=None, - location=(srcfile, 12, "SpamTests.test_spam"), - path=adapter_util.PATH_JOIN(testroot, relfile), - function=FakeFunc("test_spam"), - ), - create_stub_function_item( - stub, - nodeid=relfile + "::test_ham", - name="test_ham", - originalname=None, - location=(srcfile, 3, "test_ham"), - path=adapter_util.PATH_JOIN(testroot, relfile), - function=FakeFunc("test_spam"), - ), - ] - collector = _discovery.TestCollector(tests=discovered) - - collector.pytest_collection_finish(session) - - self.maxDiff = None - self.assertEqual( - stub.calls, - [ - ("discovered.reset", None, None), - ( - "discovered.add_test", - None, - dict( - parents=[ - ("./x/y/z/test_eggs.py::SpamTests", "SpamTests", "suite"), - ("./x/y/z/test_eggs.py", "test_eggs.py", "file"), - ("./x/y/z", "z", "folder"), - ("./x/y", "y", "folder"), - ("./x", "x", "folder"), - (".", testroot, "folder"), - ], - test=info.SingleTestInfo( - id="./x/y/z/test_eggs.py::SpamTests::test_spam", - name="test_spam", - path=info.SingleTestPath( - root=testroot, - relfile=adapter_util.fix_relpath(relfile), - func="SpamTests.test_spam", - sub=None, - ), - source="{}:{}".format( - adapter_util.fix_relpath(srcfile), 13 - ), - markers=None, - parentid="./x/y/z/test_eggs.py::SpamTests", - ), - ), - ), - ( - "discovered.add_test", - None, - dict( - parents=[ - ("./x/y/z/test_eggs.py", "test_eggs.py", "file"), - ("./x/y/z", "z", "folder"), - ("./x/y", "y", "folder"), - ("./x", "x", "folder"), - (".", testroot, "folder"), - ], - test=info.SingleTestInfo( - id="./x/y/z/test_eggs.py::test_ham", - name="test_ham", - path=info.SingleTestPath( - root=testroot, - relfile=adapter_util.fix_relpath(relfile), - func="test_ham", - sub=None, - ), - source="{}:{}".format(adapter_util.fix_relpath(srcfile), 4), - markers=None, - parentid="./x/y/z/test_eggs.py", - ), - ), - ), - ], - ) diff --git a/pythonFiles/tests/testing_tools/adapter/test___main__.py b/pythonFiles/tests/testing_tools/adapter/test___main__.py deleted file mode 100644 index 5ff0ec30c947..000000000000 --- a/pythonFiles/tests/testing_tools/adapter/test___main__.py +++ /dev/null @@ -1,199 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -import unittest - -from testing_tools.adapter.__main__ import ( - UnsupportedCommandError, - UnsupportedToolError, - main, - parse_args, -) - -from ...util import Stub, StubProxy - - -class StubTool(StubProxy): - def __init__(self, name, stub=None): - super(StubTool, self).__init__(stub, name) - self.return_discover = None - - def discover(self, args, **kwargs): - self.add_call("discover", (args,), kwargs) - if self.return_discover is None: - raise NotImplementedError - return self.return_discover - - -class StubReporter(StubProxy): - def __init__(self, stub=None): - super(StubReporter, self).__init__(stub, "reporter") - - def report(self, tests, parents, **kwargs): - self.add_call("report", (tests, parents), kwargs or None) - - -################################## -# tests - - -class ParseGeneralTests(unittest.TestCase): - def test_unsupported_command(self): - with self.assertRaises(SystemExit): - parse_args(["run", "pytest"]) - with self.assertRaises(SystemExit): - parse_args(["debug", "pytest"]) - with self.assertRaises(SystemExit): - parse_args(["???", "pytest"]) - - -class ParseDiscoverTests(unittest.TestCase): - def test_pytest_default(self): - tool, cmd, args, toolargs = parse_args( - [ - "discover", - "pytest", - ] - ) - - self.assertEqual(tool, "pytest") - self.assertEqual(cmd, "discover") - self.assertEqual(args, {"pretty": False, "hidestdio": True, "simple": False}) - self.assertEqual(toolargs, []) - - def test_pytest_full(self): - tool, cmd, args, toolargs = parse_args( - [ - "discover", - "pytest", - # no adapter-specific options yet - "--", - "--strict", - "--ignore", - "spam,ham,eggs", - "--pastebin=xyz", - "--no-cov", - "-d", - ] - ) - - self.assertEqual(tool, "pytest") - self.assertEqual(cmd, "discover") - self.assertEqual(args, {"pretty": False, "hidestdio": True, "simple": False}) - self.assertEqual( - toolargs, - [ - "--strict", - "--ignore", - "spam,ham,eggs", - "--pastebin=xyz", - "--no-cov", - "-d", - ], - ) - - def test_pytest_opts(self): - tool, cmd, args, toolargs = parse_args( - [ - "discover", - "pytest", - "--simple", - "--no-hide-stdio", - "--pretty", - ] - ) - - self.assertEqual(tool, "pytest") - self.assertEqual(cmd, "discover") - self.assertEqual(args, {"pretty": True, "hidestdio": False, "simple": True}) - self.assertEqual(toolargs, []) - - def test_unsupported_tool(self): - with self.assertRaises(SystemExit): - parse_args(["discover", "unittest"]) - with self.assertRaises(SystemExit): - parse_args(["discover", "???"]) - - -class MainTests(unittest.TestCase): - # TODO: We could use an integration test for pytest.discover(). - - def test_discover(self): - stub = Stub() - tool = StubTool("spamspamspam", stub) - tests, parents = object(), object() - tool.return_discover = (parents, tests) - reporter = StubReporter(stub) - main( - tool.name, - "discover", - {"spam": "eggs"}, - [], - _tools={ - tool.name: { - "discover": tool.discover, - } - }, - _reporters={ - "discover": reporter.report, - }, - ) - - self.assertEqual( - tool.calls, - [ - ("spamspamspam.discover", ([],), {"spam": "eggs"}), - ("reporter.report", (tests, parents), {"spam": "eggs"}), - ], - ) - - def test_unsupported_tool(self): - with self.assertRaises(UnsupportedToolError): - main( - "unittest", - "discover", - {"spam": "eggs"}, - [], - _tools={"pytest": None}, - _reporters=None, - ) - with self.assertRaises(UnsupportedToolError): - main( - "???", - "discover", - {"spam": "eggs"}, - [], - _tools={"pytest": None}, - _reporters=None, - ) - - def test_unsupported_command(self): - tool = StubTool("pytest") - with self.assertRaises(UnsupportedCommandError): - main( - "pytest", - "run", - {"spam": "eggs"}, - [], - _tools={"pytest": {"discover": tool.discover}}, - _reporters=None, - ) - with self.assertRaises(UnsupportedCommandError): - main( - "pytest", - "debug", - {"spam": "eggs"}, - [], - _tools={"pytest": {"discover": tool.discover}}, - _reporters=None, - ) - with self.assertRaises(UnsupportedCommandError): - main( - "pytest", - "???", - {"spam": "eggs"}, - [], - _tools={"pytest": {"discover": tool.discover}}, - _reporters=None, - ) - self.assertEqual(tool.calls, []) diff --git a/pythonFiles/tests/testing_tools/adapter/test_discovery.py b/pythonFiles/tests/testing_tools/adapter/test_discovery.py deleted file mode 100644 index cf3b8fb3139b..000000000000 --- a/pythonFiles/tests/testing_tools/adapter/test_discovery.py +++ /dev/null @@ -1,674 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -from __future__ import absolute_import, print_function - -import unittest - -from testing_tools.adapter.discovery import DiscoveredTests, fix_nodeid -from testing_tools.adapter.info import ParentInfo, SingleTestInfo, SingleTestPath -from testing_tools.adapter.util import fix_path, fix_relpath - - -def _fix_nodeid(nodeid): - nodeid = nodeid.replace("\\", "/") - if not nodeid.startswith("./"): - nodeid = "./" + nodeid - return nodeid - - -class DiscoveredTestsTests(unittest.TestCase): - def test_list(self): - testroot = fix_path("/a/b/c") - relfile = fix_path("./test_spam.py") - tests = [ - SingleTestInfo( - # missing "./": - id="test_spam.py::test_each[10-10]", - name="test_each[10-10]", - path=SingleTestPath( - root=testroot, - relfile=relfile, - func="test_each", - sub=["[10-10]"], - ), - source="{}:{}".format(relfile, 10), - markers=None, - # missing "./": - parentid="test_spam.py::test_each", - ), - SingleTestInfo( - id="test_spam.py::All::BasicTests::test_first", - name="test_first", - path=SingleTestPath( - root=testroot, - relfile=relfile, - func="All.BasicTests.test_first", - sub=None, - ), - source="{}:{}".format(relfile, 62), - markers=None, - parentid="test_spam.py::All::BasicTests", - ), - ] - allparents = [ - [ - (fix_path("./test_spam.py::test_each"), "test_each", "function"), - (fix_path("./test_spam.py"), "test_spam.py", "file"), - (".", testroot, "folder"), - ], - [ - (fix_path("./test_spam.py::All::BasicTests"), "BasicTests", "suite"), - (fix_path("./test_spam.py::All"), "All", "suite"), - (fix_path("./test_spam.py"), "test_spam.py", "file"), - (".", testroot, "folder"), - ], - ] - expected = [ - test._replace(id=_fix_nodeid(test.id), parentid=_fix_nodeid(test.parentid)) - for test in tests - ] - discovered = DiscoveredTests() - for test, parents in zip(tests, allparents): - discovered.add_test(test, parents) - size = len(discovered) - items = [discovered[0], discovered[1]] - snapshot = list(discovered) - - self.maxDiff = None - self.assertEqual(size, 2) - self.assertEqual(items, expected) - self.assertEqual(snapshot, expected) - - def test_reset(self): - testroot = fix_path("/a/b/c") - discovered = DiscoveredTests() - discovered.add_test( - SingleTestInfo( - id="./test_spam.py::test_each", - name="test_each", - path=SingleTestPath( - root=testroot, - relfile="test_spam.py", - func="test_each", - ), - source="test_spam.py:11", - markers=[], - parentid="./test_spam.py", - ), - [ - ("./test_spam.py", "test_spam.py", "file"), - (".", testroot, "folder"), - ], - ) - - before = len(discovered), len(discovered.parents) - discovered.reset() - after = len(discovered), len(discovered.parents) - - self.assertEqual(before, (1, 2)) - self.assertEqual(after, (0, 0)) - - def test_parents(self): - testroot = fix_path("/a/b/c") - relfile = fix_path("x/y/z/test_spam.py") - tests = [ - SingleTestInfo( - # missing "./", using pathsep: - id=relfile + "::test_each[10-10]", - name="test_each[10-10]", - path=SingleTestPath( - root=testroot, - relfile=fix_relpath(relfile), - func="test_each", - sub=["[10-10]"], - ), - source="{}:{}".format(relfile, 10), - markers=None, - # missing "./", using pathsep: - parentid=relfile + "::test_each", - ), - SingleTestInfo( - # missing "./", using pathsep: - id=relfile + "::All::BasicTests::test_first", - name="test_first", - path=SingleTestPath( - root=testroot, - relfile=fix_relpath(relfile), - func="All.BasicTests.test_first", - sub=None, - ), - source="{}:{}".format(relfile, 61), - markers=None, - # missing "./", using pathsep: - parentid=relfile + "::All::BasicTests", - ), - ] - allparents = [ - # missing "./", using pathsep: - [ - (relfile + "::test_each", "test_each", "function"), - (relfile, relfile, "file"), - (".", testroot, "folder"), - ], - # missing "./", using pathsep: - [ - (relfile + "::All::BasicTests", "BasicTests", "suite"), - (relfile + "::All", "All", "suite"), - (relfile, "test_spam.py", "file"), - (fix_path("x/y/z"), "z", "folder"), - (fix_path("x/y"), "y", "folder"), - (fix_path("./x"), "x", "folder"), - (".", testroot, "folder"), - ], - ] - discovered = DiscoveredTests() - for test, parents in zip(tests, allparents): - discovered.add_test(test, parents) - - parents = discovered.parents - - self.maxDiff = None - self.assertEqual( - parents, - [ - ParentInfo( - id=".", - kind="folder", - name=testroot, - ), - ParentInfo( - id="./x", - kind="folder", - name="x", - root=testroot, - relpath=fix_path("./x"), - parentid=".", - ), - ParentInfo( - id="./x/y", - kind="folder", - name="y", - root=testroot, - relpath=fix_path("./x/y"), - parentid="./x", - ), - ParentInfo( - id="./x/y/z", - kind="folder", - name="z", - root=testroot, - relpath=fix_path("./x/y/z"), - parentid="./x/y", - ), - ParentInfo( - id="./x/y/z/test_spam.py", - kind="file", - name="test_spam.py", - root=testroot, - relpath=fix_relpath(relfile), - parentid="./x/y/z", - ), - ParentInfo( - id="./x/y/z/test_spam.py::All", - kind="suite", - name="All", - root=testroot, - parentid="./x/y/z/test_spam.py", - ), - ParentInfo( - id="./x/y/z/test_spam.py::All::BasicTests", - kind="suite", - name="BasicTests", - root=testroot, - parentid="./x/y/z/test_spam.py::All", - ), - ParentInfo( - id="./x/y/z/test_spam.py::test_each", - kind="function", - name="test_each", - root=testroot, - parentid="./x/y/z/test_spam.py", - ), - ], - ) - - def test_add_test_simple(self): - testroot = fix_path("/a/b/c") - relfile = "test_spam.py" - test = SingleTestInfo( - # missing "./": - id=relfile + "::test_spam", - name="test_spam", - path=SingleTestPath( - root=testroot, - # missing "./": - relfile=relfile, - func="test_spam", - ), - # missing "./": - source="{}:{}".format(relfile, 11), - markers=[], - # missing "./": - parentid=relfile, - ) - expected = test._replace( - id=_fix_nodeid(test.id), parentid=_fix_nodeid(test.parentid) - ) - discovered = DiscoveredTests() - - before = list(discovered), discovered.parents - discovered.add_test( - test, - [ - (relfile, relfile, "file"), - (".", testroot, "folder"), - ], - ) - after = list(discovered), discovered.parents - - self.maxDiff = None - self.assertEqual(before, ([], [])) - self.assertEqual( - after, - ( - [expected], - [ - ParentInfo( - id=".", - kind="folder", - name=testroot, - ), - ParentInfo( - id="./test_spam.py", - kind="file", - name=relfile, - root=testroot, - relpath=relfile, - parentid=".", - ), - ], - ), - ) - - def test_multiroot(self): - # the first root - testroot1 = fix_path("/a/b/c") - relfile1 = "test_spam.py" - alltests = [ - SingleTestInfo( - # missing "./": - id=relfile1 + "::test_spam", - name="test_spam", - path=SingleTestPath( - root=testroot1, - relfile=fix_relpath(relfile1), - func="test_spam", - ), - source="{}:{}".format(relfile1, 10), - markers=[], - # missing "./": - parentid=relfile1, - ), - ] - allparents = [ - # missing "./": - [ - (relfile1, "test_spam.py", "file"), - (".", testroot1, "folder"), - ], - ] - # the second root - testroot2 = fix_path("/x/y/z") - relfile2 = fix_path("w/test_eggs.py") - alltests.extend( - [ - SingleTestInfo( - id=relfile2 + "::BasicTests::test_first", - name="test_first", - path=SingleTestPath( - root=testroot2, - relfile=fix_relpath(relfile2), - func="BasicTests.test_first", - ), - source="{}:{}".format(relfile2, 61), - markers=[], - parentid=relfile2 + "::BasicTests", - ), - ] - ) - allparents.extend( - [ - # missing "./", using pathsep: - [ - (relfile2 + "::BasicTests", "BasicTests", "suite"), - (relfile2, "test_eggs.py", "file"), - (fix_path("./w"), "w", "folder"), - (".", testroot2, "folder"), - ], - ] - ) - - discovered = DiscoveredTests() - for test, parents in zip(alltests, allparents): - discovered.add_test(test, parents) - tests = list(discovered) - parents = discovered.parents - - self.maxDiff = None - self.assertEqual( - tests, - [ - # the first root - SingleTestInfo( - id="./test_spam.py::test_spam", - name="test_spam", - path=SingleTestPath( - root=testroot1, - relfile=fix_relpath(relfile1), - func="test_spam", - ), - source="{}:{}".format(relfile1, 10), - markers=[], - parentid="./test_spam.py", - ), - # the secondroot - SingleTestInfo( - id="./w/test_eggs.py::BasicTests::test_first", - name="test_first", - path=SingleTestPath( - root=testroot2, - relfile=fix_relpath(relfile2), - func="BasicTests.test_first", - ), - source="{}:{}".format(relfile2, 61), - markers=[], - parentid="./w/test_eggs.py::BasicTests", - ), - ], - ) - self.assertEqual( - parents, - [ - # the first root - ParentInfo( - id=".", - kind="folder", - name=testroot1, - ), - ParentInfo( - id="./test_spam.py", - kind="file", - name="test_spam.py", - root=testroot1, - relpath=fix_relpath(relfile1), - parentid=".", - ), - # the secondroot - ParentInfo( - id=".", - kind="folder", - name=testroot2, - ), - ParentInfo( - id="./w", - kind="folder", - name="w", - root=testroot2, - relpath=fix_path("./w"), - parentid=".", - ), - ParentInfo( - id="./w/test_eggs.py", - kind="file", - name="test_eggs.py", - root=testroot2, - relpath=fix_relpath(relfile2), - parentid="./w", - ), - ParentInfo( - id="./w/test_eggs.py::BasicTests", - kind="suite", - name="BasicTests", - root=testroot2, - parentid="./w/test_eggs.py", - ), - ], - ) - - def test_doctest(self): - testroot = fix_path("/a/b/c") - doctestfile = fix_path("./x/test_doctest.txt") - relfile = fix_path("./x/y/z/test_eggs.py") - alltests = [ - SingleTestInfo( - id=doctestfile + "::test_doctest.txt", - name="test_doctest.txt", - path=SingleTestPath( - root=testroot, - relfile=doctestfile, - func=None, - ), - source="{}:{}".format(doctestfile, 0), - markers=[], - parentid=doctestfile, - ), - # With --doctest-modules - SingleTestInfo( - id=relfile + "::test_eggs", - name="test_eggs", - path=SingleTestPath( - root=testroot, - relfile=relfile, - func=None, - ), - source="{}:{}".format(relfile, 0), - markers=[], - parentid=relfile, - ), - SingleTestInfo( - id=relfile + "::test_eggs.TestSpam", - name="test_eggs.TestSpam", - path=SingleTestPath( - root=testroot, - relfile=relfile, - func=None, - ), - source="{}:{}".format(relfile, 12), - markers=[], - parentid=relfile, - ), - SingleTestInfo( - id=relfile + "::test_eggs.TestSpam.TestEggs", - name="test_eggs.TestSpam.TestEggs", - path=SingleTestPath( - root=testroot, - relfile=relfile, - func=None, - ), - source="{}:{}".format(relfile, 27), - markers=[], - parentid=relfile, - ), - ] - allparents = [ - [ - (doctestfile, "test_doctest.txt", "file"), - (fix_path("./x"), "x", "folder"), - (".", testroot, "folder"), - ], - [ - (relfile, "test_eggs.py", "file"), - (fix_path("./x/y/z"), "z", "folder"), - (fix_path("./x/y"), "y", "folder"), - (fix_path("./x"), "x", "folder"), - (".", testroot, "folder"), - ], - [ - (relfile, "test_eggs.py", "file"), - (fix_path("./x/y/z"), "z", "folder"), - (fix_path("./x/y"), "y", "folder"), - (fix_path("./x"), "x", "folder"), - (".", testroot, "folder"), - ], - [ - (relfile, "test_eggs.py", "file"), - (fix_path("./x/y/z"), "z", "folder"), - (fix_path("./x/y"), "y", "folder"), - (fix_path("./x"), "x", "folder"), - (".", testroot, "folder"), - ], - ] - expected = [ - test._replace(id=_fix_nodeid(test.id), parentid=_fix_nodeid(test.parentid)) - for test in alltests - ] - - discovered = DiscoveredTests() - - for test, parents in zip(alltests, allparents): - discovered.add_test(test, parents) - tests = list(discovered) - parents = discovered.parents - - self.maxDiff = None - self.assertEqual(tests, expected) - self.assertEqual( - parents, - [ - ParentInfo( - id=".", - kind="folder", - name=testroot, - ), - ParentInfo( - id="./x", - kind="folder", - name="x", - root=testroot, - relpath=fix_path("./x"), - parentid=".", - ), - ParentInfo( - id="./x/test_doctest.txt", - kind="file", - name="test_doctest.txt", - root=testroot, - relpath=fix_path(doctestfile), - parentid="./x", - ), - ParentInfo( - id="./x/y", - kind="folder", - name="y", - root=testroot, - relpath=fix_path("./x/y"), - parentid="./x", - ), - ParentInfo( - id="./x/y/z", - kind="folder", - name="z", - root=testroot, - relpath=fix_path("./x/y/z"), - parentid="./x/y", - ), - ParentInfo( - id="./x/y/z/test_eggs.py", - kind="file", - name="test_eggs.py", - root=testroot, - relpath=fix_relpath(relfile), - parentid="./x/y/z", - ), - ], - ) - - def test_nested_suite_simple(self): - testroot = fix_path("/a/b/c") - relfile = fix_path("./test_eggs.py") - alltests = [ - SingleTestInfo( - id=relfile + "::TestOuter::TestInner::test_spam", - name="test_spam", - path=SingleTestPath( - root=testroot, - relfile=relfile, - func="TestOuter.TestInner.test_spam", - ), - source="{}:{}".format(relfile, 10), - markers=None, - parentid=relfile + "::TestOuter::TestInner", - ), - SingleTestInfo( - id=relfile + "::TestOuter::TestInner::test_eggs", - name="test_eggs", - path=SingleTestPath( - root=testroot, - relfile=relfile, - func="TestOuter.TestInner.test_eggs", - ), - source="{}:{}".format(relfile, 21), - markers=None, - parentid=relfile + "::TestOuter::TestInner", - ), - ] - allparents = [ - [ - (relfile + "::TestOuter::TestInner", "TestInner", "suite"), - (relfile + "::TestOuter", "TestOuter", "suite"), - (relfile, "test_eggs.py", "file"), - (".", testroot, "folder"), - ], - [ - (relfile + "::TestOuter::TestInner", "TestInner", "suite"), - (relfile + "::TestOuter", "TestOuter", "suite"), - (relfile, "test_eggs.py", "file"), - (".", testroot, "folder"), - ], - ] - expected = [ - test._replace(id=_fix_nodeid(test.id), parentid=_fix_nodeid(test.parentid)) - for test in alltests - ] - - discovered = DiscoveredTests() - for test, parents in zip(alltests, allparents): - discovered.add_test(test, parents) - tests = list(discovered) - parents = discovered.parents - - self.maxDiff = None - self.assertEqual(tests, expected) - self.assertEqual( - parents, - [ - ParentInfo( - id=".", - kind="folder", - name=testroot, - ), - ParentInfo( - id="./test_eggs.py", - kind="file", - name="test_eggs.py", - root=testroot, - relpath=fix_relpath(relfile), - parentid=".", - ), - ParentInfo( - id="./test_eggs.py::TestOuter", - kind="suite", - name="TestOuter", - root=testroot, - parentid="./test_eggs.py", - ), - ParentInfo( - id="./test_eggs.py::TestOuter::TestInner", - kind="suite", - name="TestInner", - root=testroot, - parentid="./test_eggs.py::TestOuter", - ), - ], - ) diff --git a/pythonFiles/tests/testing_tools/adapter/test_functional.py b/pythonFiles/tests/testing_tools/adapter/test_functional.py deleted file mode 100644 index a78d36a5fdcf..000000000000 --- a/pythonFiles/tests/testing_tools/adapter/test_functional.py +++ /dev/null @@ -1,1536 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -from __future__ import absolute_import, unicode_literals - -import json -import os -import os.path -import subprocess -import sys -import unittest - -from testing_tools.adapter.util import PATH_SEP, fix_path - -from ...__main__ import TESTING_TOOLS_ROOT - -# Pytest 3.7 and later uses pathlib/pathlib2 for path resolution. -try: - from pathlib import Path -except ImportError: - from pathlib2 import Path # type: ignore (for Pylance) - - -CWD = os.getcwd() -DATA_DIR = os.path.join(os.path.dirname(__file__), ".data") -SCRIPT = os.path.join(TESTING_TOOLS_ROOT, "run_adapter.py") - - -def resolve_testroot(name): - projroot = os.path.join(DATA_DIR, name) - testroot = os.path.join(projroot, "tests") - return str(Path(projroot).resolve()), str(Path(testroot).resolve()) - - -def run_adapter(cmd, tool, *cliargs): - try: - return _run_adapter(cmd, tool, *cliargs) - except subprocess.CalledProcessError as exc: - print(exc.output) - - -def _run_adapter(cmd, tool, *cliargs, **kwargs): - hidestdio = kwargs.pop("hidestdio", True) - assert not kwargs or tuple(kwargs) == ("stderr",) - kwds = kwargs - argv = [sys.executable, SCRIPT, cmd, tool, "--"] + list(cliargs) - if not hidestdio: - argv.insert(4, "--no-hide-stdio") - kwds["stderr"] = subprocess.STDOUT - argv.append("--cache-clear") - print( - "running {!r}".format(" ".join(arg.rpartition(CWD + "/")[-1] for arg in argv)) - ) - output = subprocess.check_output(argv, universal_newlines=True, **kwds) - return output - - -def fix_test_order(tests): - if sys.version_info >= (3, 6): - return tests - fixed = [] - curfile = None - group = [] - for test in tests: - if (curfile or "???") not in test["id"]: - fixed.extend(sorted(group, key=lambda t: t["id"])) - group = [] - curfile = test["id"].partition(".py::")[0] + ".py" - group.append(test) - fixed.extend(sorted(group, key=lambda t: t["id"])) - return fixed - - -def fix_source(tests, testid, srcfile, lineno): - for test in tests: - if test["id"] == testid: - break - else: - raise KeyError("test {!r} not found".format(testid)) - if not srcfile: - srcfile = test["source"].rpartition(":")[0] - test["source"] = fix_path("{}:{}".format(srcfile, lineno)) - - -def sorted_object(obj): - if isinstance(obj, dict): - return sorted((key, sorted_object(obj[key])) for key in obj.keys()) - if isinstance(obj, list): - return sorted((sorted_object(x) for x in obj)) - else: - return obj - - -# Note that these tests are skipped if util.PATH_SEP is not os.path.sep. -# This is because the functional tests should reflect the actual -# operating environment. - - -class PytestTests(unittest.TestCase): - def setUp(self): - if PATH_SEP is not os.path.sep: - raise unittest.SkipTest("functional tests require unmodified env") - super(PytestTests, self).setUp() - - def complex(self, testroot): - results = COMPLEX.copy() - results["root"] = testroot - return [results] - - def test_discover_simple(self): - projroot, testroot = resolve_testroot("simple") - - out = run_adapter("discover", "pytest", "--rootdir", projroot, testroot) - result = json.loads(out) - - self.maxDiff = None - self.assertEqual( - result, - [ - { - "root": projroot, - "rootid": ".", - "parents": [ - { - "id": "./tests", - "kind": "folder", - "name": "tests", - "relpath": fix_path("./tests"), - "parentid": ".", - }, - { - "id": "./tests/test_spam.py", - "kind": "file", - "name": "test_spam.py", - "relpath": fix_path("./tests/test_spam.py"), - "parentid": "./tests", - }, - ], - "tests": [ - { - "id": "./tests/test_spam.py::test_simple", - "name": "test_simple", - "source": fix_path("./tests/test_spam.py:2"), - "markers": [], - "parentid": "./tests/test_spam.py", - }, - ], - } - ], - ) - - def test_discover_complex_default(self): - projroot, testroot = resolve_testroot("complex") - expected = self.complex(projroot) - expected[0]["tests"] = fix_test_order(expected[0]["tests"]) - if sys.version_info < (3,): - decorated = [ - "./tests/test_unittest.py::MyTests::test_skipped", - "./tests/test_unittest.py::MyTests::test_maybe_skipped", - "./tests/test_unittest.py::MyTests::test_maybe_not_skipped", - ] - for testid in decorated: - fix_source(expected[0]["tests"], testid, None, 0) - - out = run_adapter("discover", "pytest", "--rootdir", projroot, testroot) - result = json.loads(out) - result[0]["tests"] = fix_test_order(result[0]["tests"]) - - self.maxDiff = None - self.assertEqual(sorted_object(result), sorted_object(expected)) - - def test_discover_complex_doctest(self): - projroot, _ = resolve_testroot("complex") - expected = self.complex(projroot) - # add in doctests from test suite - expected[0]["parents"].insert( - 3, - { - "id": "./tests/test_doctest.py", - "kind": "file", - "name": "test_doctest.py", - "relpath": fix_path("./tests/test_doctest.py"), - "parentid": "./tests", - }, - ) - expected[0]["tests"].insert( - 2, - { - "id": "./tests/test_doctest.py::tests.test_doctest", - "name": "tests.test_doctest", - "source": fix_path("./tests/test_doctest.py:1"), - "markers": [], - "parentid": "./tests/test_doctest.py", - }, - ) - # add in doctests from non-test module - expected[0]["parents"].insert( - 0, - { - "id": "./mod.py", - "kind": "file", - "name": "mod.py", - "relpath": fix_path("./mod.py"), - "parentid": ".", - }, - ) - expected[0]["tests"] = [ - { - "id": "./mod.py::mod", - "name": "mod", - "source": fix_path("./mod.py:1"), - "markers": [], - "parentid": "./mod.py", - }, - { - "id": "./mod.py::mod.Spam", - "name": "mod.Spam", - "source": fix_path("./mod.py:33"), - "markers": [], - "parentid": "./mod.py", - }, - { - "id": "./mod.py::mod.Spam.eggs", - "name": "mod.Spam.eggs", - "source": fix_path("./mod.py:43"), - "markers": [], - "parentid": "./mod.py", - }, - { - "id": "./mod.py::mod.square", - "name": "mod.square", - "source": fix_path("./mod.py:18"), - "markers": [], - "parentid": "./mod.py", - }, - ] + expected[0]["tests"] - expected[0]["tests"] = fix_test_order(expected[0]["tests"]) - if sys.version_info < (3,): - decorated = [ - "./tests/test_unittest.py::MyTests::test_skipped", - "./tests/test_unittest.py::MyTests::test_maybe_skipped", - "./tests/test_unittest.py::MyTests::test_maybe_not_skipped", - ] - for testid in decorated: - fix_source(expected[0]["tests"], testid, None, 0) - - out = run_adapter( - "discover", "pytest", "--rootdir", projroot, "--doctest-modules", projroot - ) - result = json.loads(out) - result[0]["tests"] = fix_test_order(result[0]["tests"]) - - self.maxDiff = None - self.assertEqual(sorted_object(result), sorted_object(expected)) - - def test_discover_not_found(self): - projroot, testroot = resolve_testroot("notests") - - out = run_adapter("discover", "pytest", "--rootdir", projroot, testroot) - result = json.loads(out) - - self.maxDiff = None - self.assertEqual(result, []) - # TODO: Expect the following instead? - # self.assertEqual(result, [{ - # 'root': projroot, - # 'rootid': '.', - # 'parents': [], - # 'tests': [], - # }]) - - @unittest.skip("broken in CI") - def test_discover_bad_args(self): - projroot, testroot = resolve_testroot("simple") - - with self.assertRaises(subprocess.CalledProcessError) as cm: - _run_adapter( - "discover", - "pytest", - "--spam", - "--rootdir", - projroot, - testroot, - stderr=subprocess.STDOUT, - ) - self.assertIn("(exit code 4)", cm.exception.output) - - def test_discover_syntax_error(self): - projroot, testroot = resolve_testroot("syntax-error") - - with self.assertRaises(subprocess.CalledProcessError) as cm: - _run_adapter( - "discover", - "pytest", - "--rootdir", - projroot, - testroot, - stderr=subprocess.STDOUT, - ) - self.assertIn("(exit code 2)", cm.exception.output) - - def test_discover_normcase(self): - projroot, testroot = resolve_testroot("NormCase") - - out = run_adapter("discover", "pytest", "--rootdir", projroot, testroot) - result = json.loads(out) - - self.maxDiff = None - self.assertTrue(projroot.endswith("NormCase")) - self.assertEqual( - result, - [ - { - "root": projroot, - "rootid": ".", - "parents": [ - { - "id": "./tests", - "kind": "folder", - "name": "tests", - "relpath": fix_path("./tests"), - "parentid": ".", - }, - { - "id": "./tests/A", - "kind": "folder", - "name": "A", - "relpath": fix_path("./tests/A"), - "parentid": "./tests", - }, - { - "id": "./tests/A/b", - "kind": "folder", - "name": "b", - "relpath": fix_path("./tests/A/b"), - "parentid": "./tests/A", - }, - { - "id": "./tests/A/b/C", - "kind": "folder", - "name": "C", - "relpath": fix_path("./tests/A/b/C"), - "parentid": "./tests/A/b", - }, - { - "id": "./tests/A/b/C/test_Spam.py", - "kind": "file", - "name": "test_Spam.py", - "relpath": fix_path("./tests/A/b/C/test_Spam.py"), - "parentid": "./tests/A/b/C", - }, - ], - "tests": [ - { - "id": "./tests/A/b/C/test_Spam.py::test_okay", - "name": "test_okay", - "source": fix_path("./tests/A/b/C/test_Spam.py:2"), - "markers": [], - "parentid": "./tests/A/b/C/test_Spam.py", - }, - ], - } - ], - ) - - -COMPLEX = { - "root": None, - "rootid": ".", - "parents": [ - # - { - "id": "./tests", - "kind": "folder", - "name": "tests", - "relpath": fix_path("./tests"), - "parentid": ".", - }, - # +++ - { - "id": "./tests/test_42-43.py", - "kind": "file", - "name": "test_42-43.py", - "relpath": fix_path("./tests/test_42-43.py"), - "parentid": "./tests", - }, - # +++ - { - "id": "./tests/test_42.py", - "kind": "file", - "name": "test_42.py", - "relpath": fix_path("./tests/test_42.py"), - "parentid": "./tests", - }, - # +++ - { - "id": "./tests/test_doctest.txt", - "kind": "file", - "name": "test_doctest.txt", - "relpath": fix_path("./tests/test_doctest.txt"), - "parentid": "./tests", - }, - # +++ - { - "id": "./tests/test_foo.py", - "kind": "file", - "name": "test_foo.py", - "relpath": fix_path("./tests/test_foo.py"), - "parentid": "./tests", - }, - # +++ - { - "id": "./tests/test_mixed.py", - "kind": "file", - "name": "test_mixed.py", - "relpath": fix_path("./tests/test_mixed.py"), - "parentid": "./tests", - }, - { - "id": "./tests/test_mixed.py::MyTests", - "kind": "suite", - "name": "MyTests", - "parentid": "./tests/test_mixed.py", - }, - { - "id": "./tests/test_mixed.py::TestMySuite", - "kind": "suite", - "name": "TestMySuite", - "parentid": "./tests/test_mixed.py", - }, - # +++ - { - "id": "./tests/test_pytest.py", - "kind": "file", - "name": "test_pytest.py", - "relpath": fix_path("./tests/test_pytest.py"), - "parentid": "./tests", - }, - { - "id": "./tests/test_pytest.py::TestEggs", - "kind": "suite", - "name": "TestEggs", - "parentid": "./tests/test_pytest.py", - }, - { - "id": "./tests/test_pytest.py::TestParam", - "kind": "suite", - "name": "TestParam", - "parentid": "./tests/test_pytest.py", - }, - { - "id": "./tests/test_pytest.py::TestParam::test_param_13", - "kind": "function", - "name": "test_param_13", - "parentid": "./tests/test_pytest.py::TestParam", - }, - { - "id": "./tests/test_pytest.py::TestParamAll", - "kind": "suite", - "name": "TestParamAll", - "parentid": "./tests/test_pytest.py", - }, - { - "id": "./tests/test_pytest.py::TestParamAll::test_param_13", - "kind": "function", - "name": "test_param_13", - "parentid": "./tests/test_pytest.py::TestParamAll", - }, - { - "id": "./tests/test_pytest.py::TestParamAll::test_spam_13", - "kind": "function", - "name": "test_spam_13", - "parentid": "./tests/test_pytest.py::TestParamAll", - }, - { - "id": "./tests/test_pytest.py::TestSpam", - "kind": "suite", - "name": "TestSpam", - "parentid": "./tests/test_pytest.py", - }, - { - "id": "./tests/test_pytest.py::TestSpam::TestHam", - "kind": "suite", - "name": "TestHam", - "parentid": "./tests/test_pytest.py::TestSpam", - }, - { - "id": "./tests/test_pytest.py::TestSpam::TestHam::TestEggs", - "kind": "suite", - "name": "TestEggs", - "parentid": "./tests/test_pytest.py::TestSpam::TestHam", - }, - { - "id": "./tests/test_pytest.py::test_fixture_param", - "kind": "function", - "name": "test_fixture_param", - "parentid": "./tests/test_pytest.py", - }, - { - "id": "./tests/test_pytest.py::test_param_01", - "kind": "function", - "name": "test_param_01", - "parentid": "./tests/test_pytest.py", - }, - { - "id": "./tests/test_pytest.py::test_param_11", - "kind": "function", - "name": "test_param_11", - "parentid": "./tests/test_pytest.py", - }, - { - "id": "./tests/test_pytest.py::test_param_13", - "kind": "function", - "name": "test_param_13", - "parentid": "./tests/test_pytest.py", - }, - { - "id": "./tests/test_pytest.py::test_param_13_markers", - "kind": "function", - "name": "test_param_13_markers", - "parentid": "./tests/test_pytest.py", - }, - { - "id": "./tests/test_pytest.py::test_param_13_repeat", - "kind": "function", - "name": "test_param_13_repeat", - "parentid": "./tests/test_pytest.py", - }, - { - "id": "./tests/test_pytest.py::test_param_13_skipped", - "kind": "function", - "name": "test_param_13_skipped", - "parentid": "./tests/test_pytest.py", - }, - { - "id": "./tests/test_pytest.py::test_param_23_13", - "kind": "function", - "name": "test_param_23_13", - "parentid": "./tests/test_pytest.py", - }, - { - "id": "./tests/test_pytest.py::test_param_23_raises", - "kind": "function", - "name": "test_param_23_raises", - "parentid": "./tests/test_pytest.py", - }, - { - "id": "./tests/test_pytest.py::test_param_33", - "kind": "function", - "name": "test_param_33", - "parentid": "./tests/test_pytest.py", - }, - { - "id": "./tests/test_pytest.py::test_param_33_ids", - "kind": "function", - "name": "test_param_33_ids", - "parentid": "./tests/test_pytest.py", - }, - { - "id": "./tests/test_pytest.py::test_param_fixture", - "kind": "function", - "name": "test_param_fixture", - "parentid": "./tests/test_pytest.py", - }, - { - "id": "./tests/test_pytest.py::test_param_mark_fixture", - "kind": "function", - "name": "test_param_mark_fixture", - "parentid": "./tests/test_pytest.py", - }, - # +++ - { - "id": "./tests/test_pytest_param.py", - "kind": "file", - "name": "test_pytest_param.py", - "relpath": fix_path("./tests/test_pytest_param.py"), - "parentid": "./tests", - }, - { - "id": "./tests/test_pytest_param.py::TestParamAll", - "kind": "suite", - "name": "TestParamAll", - "parentid": "./tests/test_pytest_param.py", - }, - { - "id": "./tests/test_pytest_param.py::TestParamAll::test_param_13", - "kind": "function", - "name": "test_param_13", - "parentid": "./tests/test_pytest_param.py::TestParamAll", - }, - { - "id": "./tests/test_pytest_param.py::TestParamAll::test_spam_13", - "kind": "function", - "name": "test_spam_13", - "parentid": "./tests/test_pytest_param.py::TestParamAll", - }, - { - "id": "./tests/test_pytest_param.py::test_param_13", - "kind": "function", - "name": "test_param_13", - "parentid": "./tests/test_pytest_param.py", - }, - # +++ - { - "id": "./tests/test_unittest.py", - "kind": "file", - "name": "test_unittest.py", - "relpath": fix_path("./tests/test_unittest.py"), - "parentid": "./tests", - }, - { - "id": "./tests/test_unittest.py::MyTests", - "kind": "suite", - "name": "MyTests", - "parentid": "./tests/test_unittest.py", - }, - { - "id": "./tests/test_unittest.py::OtherTests", - "kind": "suite", - "name": "OtherTests", - "parentid": "./tests/test_unittest.py", - }, - ## - { - "id": "./tests/v", - "kind": "folder", - "name": "v", - "relpath": fix_path("./tests/v"), - "parentid": "./tests", - }, - ## +++ - { - "id": "./tests/v/test_eggs.py", - "kind": "file", - "name": "test_eggs.py", - "relpath": fix_path("./tests/v/test_eggs.py"), - "parentid": "./tests/v", - }, - { - "id": "./tests/v/test_eggs.py::TestSimple", - "kind": "suite", - "name": "TestSimple", - "parentid": "./tests/v/test_eggs.py", - }, - ## +++ - { - "id": "./tests/v/test_ham.py", - "kind": "file", - "name": "test_ham.py", - "relpath": fix_path("./tests/v/test_ham.py"), - "parentid": "./tests/v", - }, - ## +++ - { - "id": "./tests/v/test_spam.py", - "kind": "file", - "name": "test_spam.py", - "relpath": fix_path("./tests/v/test_spam.py"), - "parentid": "./tests/v", - }, - ## - { - "id": "./tests/w", - "kind": "folder", - "name": "w", - "relpath": fix_path("./tests/w"), - "parentid": "./tests", - }, - ## +++ - { - "id": "./tests/w/test_spam.py", - "kind": "file", - "name": "test_spam.py", - "relpath": fix_path("./tests/w/test_spam.py"), - "parentid": "./tests/w", - }, - ## +++ - { - "id": "./tests/w/test_spam_ex.py", - "kind": "file", - "name": "test_spam_ex.py", - "relpath": fix_path("./tests/w/test_spam_ex.py"), - "parentid": "./tests/w", - }, - ## - { - "id": "./tests/x", - "kind": "folder", - "name": "x", - "relpath": fix_path("./tests/x"), - "parentid": "./tests", - }, - ### - { - "id": "./tests/x/y", - "kind": "folder", - "name": "y", - "relpath": fix_path("./tests/x/y"), - "parentid": "./tests/x", - }, - #### - { - "id": "./tests/x/y/z", - "kind": "folder", - "name": "z", - "relpath": fix_path("./tests/x/y/z"), - "parentid": "./tests/x/y", - }, - ##### - { - "id": "./tests/x/y/z/a", - "kind": "folder", - "name": "a", - "relpath": fix_path("./tests/x/y/z/a"), - "parentid": "./tests/x/y/z", - }, - ##### +++ - { - "id": "./tests/x/y/z/a/test_spam.py", - "kind": "file", - "name": "test_spam.py", - "relpath": fix_path("./tests/x/y/z/a/test_spam.py"), - "parentid": "./tests/x/y/z/a", - }, - ##### - { - "id": "./tests/x/y/z/b", - "kind": "folder", - "name": "b", - "relpath": fix_path("./tests/x/y/z/b"), - "parentid": "./tests/x/y/z", - }, - ##### +++ - { - "id": "./tests/x/y/z/b/test_spam.py", - "kind": "file", - "name": "test_spam.py", - "relpath": fix_path("./tests/x/y/z/b/test_spam.py"), - "parentid": "./tests/x/y/z/b", - }, - #### +++ - { - "id": "./tests/x/y/z/test_ham.py", - "kind": "file", - "name": "test_ham.py", - "relpath": fix_path("./tests/x/y/z/test_ham.py"), - "parentid": "./tests/x/y/z", - }, - ], - "tests": [ - ########## - { - "id": "./tests/test_42-43.py::test_simple", - "name": "test_simple", - "source": fix_path("./tests/test_42-43.py:2"), - "markers": [], - "parentid": "./tests/test_42-43.py", - }, - ##### - { - "id": "./tests/test_42.py::test_simple", - "name": "test_simple", - "source": fix_path("./tests/test_42.py:2"), - "markers": [], - "parentid": "./tests/test_42.py", - }, - ##### - { - "id": "./tests/test_doctest.txt::test_doctest.txt", - "name": "test_doctest.txt", - "source": fix_path("./tests/test_doctest.txt:1"), - "markers": [], - "parentid": "./tests/test_doctest.txt", - }, - ##### - { - "id": "./tests/test_foo.py::test_simple", - "name": "test_simple", - "source": fix_path("./tests/test_foo.py:3"), - "markers": [], - "parentid": "./tests/test_foo.py", - }, - ##### - { - "id": "./tests/test_mixed.py::test_top_level", - "name": "test_top_level", - "source": fix_path("./tests/test_mixed.py:5"), - "markers": [], - "parentid": "./tests/test_mixed.py", - }, - { - "id": "./tests/test_mixed.py::test_skipped", - "name": "test_skipped", - "source": fix_path("./tests/test_mixed.py:9"), - "markers": ["skip"], - "parentid": "./tests/test_mixed.py", - }, - { - "id": "./tests/test_mixed.py::TestMySuite::test_simple", - "name": "test_simple", - "source": fix_path("./tests/test_mixed.py:16"), - "markers": [], - "parentid": "./tests/test_mixed.py::TestMySuite", - }, - { - "id": "./tests/test_mixed.py::MyTests::test_simple", - "name": "test_simple", - "source": fix_path("./tests/test_mixed.py:22"), - "markers": [], - "parentid": "./tests/test_mixed.py::MyTests", - }, - { - "id": "./tests/test_mixed.py::MyTests::test_skipped", - "name": "test_skipped", - "source": fix_path("./tests/test_mixed.py:25"), - "markers": ["skip"], - "parentid": "./tests/test_mixed.py::MyTests", - }, - ##### - { - "id": "./tests/test_pytest.py::test_simple", - "name": "test_simple", - "source": fix_path("./tests/test_pytest.py:6"), - "markers": [], - "parentid": "./tests/test_pytest.py", - }, - { - "id": "./tests/test_pytest.py::test_failure", - "name": "test_failure", - "source": fix_path("./tests/test_pytest.py:10"), - "markers": [], - "parentid": "./tests/test_pytest.py", - }, - { - "id": "./tests/test_pytest.py::test_runtime_skipped", - "name": "test_runtime_skipped", - "source": fix_path("./tests/test_pytest.py:14"), - "markers": [], - "parentid": "./tests/test_pytest.py", - }, - { - "id": "./tests/test_pytest.py::test_runtime_failed", - "name": "test_runtime_failed", - "source": fix_path("./tests/test_pytest.py:18"), - "markers": [], - "parentid": "./tests/test_pytest.py", - }, - { - "id": "./tests/test_pytest.py::test_raises", - "name": "test_raises", - "source": fix_path("./tests/test_pytest.py:22"), - "markers": [], - "parentid": "./tests/test_pytest.py", - }, - { - "id": "./tests/test_pytest.py::test_skipped", - "name": "test_skipped", - "source": fix_path("./tests/test_pytest.py:26"), - "markers": ["skip"], - "parentid": "./tests/test_pytest.py", - }, - { - "id": "./tests/test_pytest.py::test_maybe_skipped", - "name": "test_maybe_skipped", - "source": fix_path("./tests/test_pytest.py:31"), - "markers": ["skip-if"], - "parentid": "./tests/test_pytest.py", - }, - { - "id": "./tests/test_pytest.py::test_known_failure", - "name": "test_known_failure", - "source": fix_path("./tests/test_pytest.py:36"), - "markers": ["expected-failure"], - "parentid": "./tests/test_pytest.py", - }, - { - "id": "./tests/test_pytest.py::test_warned", - "name": "test_warned", - "source": fix_path("./tests/test_pytest.py:41"), - "markers": [], - "parentid": "./tests/test_pytest.py", - }, - { - "id": "./tests/test_pytest.py::test_custom_marker", - "name": "test_custom_marker", - "source": fix_path("./tests/test_pytest.py:46"), - "markers": [], - "parentid": "./tests/test_pytest.py", - }, - { - "id": "./tests/test_pytest.py::test_multiple_markers", - "name": "test_multiple_markers", - "source": fix_path("./tests/test_pytest.py:51"), - "markers": ["expected-failure", "skip", "skip-if"], - "parentid": "./tests/test_pytest.py", - }, - { - "id": "./tests/test_pytest.py::test_dynamic_1", - "name": "test_dynamic_1", - "source": fix_path("./tests/test_pytest.py:62"), - "markers": [], - "parentid": "./tests/test_pytest.py", - }, - { - "id": "./tests/test_pytest.py::test_dynamic_2", - "name": "test_dynamic_2", - "source": fix_path("./tests/test_pytest.py:62"), - "markers": [], - "parentid": "./tests/test_pytest.py", - }, - { - "id": "./tests/test_pytest.py::test_dynamic_3", - "name": "test_dynamic_3", - "source": fix_path("./tests/test_pytest.py:62"), - "markers": [], - "parentid": "./tests/test_pytest.py", - }, - { - "id": "./tests/test_pytest.py::TestSpam::test_simple", - "name": "test_simple", - "source": fix_path("./tests/test_pytest.py:70"), - "markers": [], - "parentid": "./tests/test_pytest.py::TestSpam", - }, - { - "id": "./tests/test_pytest.py::TestSpam::test_skipped", - "name": "test_skipped", - "source": fix_path("./tests/test_pytest.py:73"), - "markers": ["skip"], - "parentid": "./tests/test_pytest.py::TestSpam", - }, - { - "id": "./tests/test_pytest.py::TestSpam::TestHam::TestEggs::test_simple", - "name": "test_simple", - "source": fix_path("./tests/test_pytest.py:81"), - "markers": [], - "parentid": "./tests/test_pytest.py::TestSpam::TestHam::TestEggs", - }, - { - "id": "./tests/test_pytest.py::TestEggs::test_simple", - "name": "test_simple", - "source": fix_path("./tests/test_pytest.py:93"), - "markers": [], - "parentid": "./tests/test_pytest.py::TestEggs", - }, - { - "id": "./tests/test_pytest.py::test_param_01[]", - "name": "", - "source": fix_path("./tests/test_pytest.py:103"), - "markers": [], - "parentid": "./tests/test_pytest.py::test_param_01", - }, - { - "id": "./tests/test_pytest.py::test_param_11[x0]", - "name": "x0", - "source": fix_path("./tests/test_pytest.py:108"), - "markers": [], - "parentid": "./tests/test_pytest.py::test_param_11", - }, - { - "id": "./tests/test_pytest.py::test_param_13[x0]", - "name": "x0", - "source": fix_path("./tests/test_pytest.py:113"), - "markers": [], - "parentid": "./tests/test_pytest.py::test_param_13", - }, - { - "id": "./tests/test_pytest.py::test_param_13[x1]", - "name": "x1", - "source": fix_path("./tests/test_pytest.py:113"), - "markers": [], - "parentid": "./tests/test_pytest.py::test_param_13", - }, - { - "id": "./tests/test_pytest.py::test_param_13[x2]", - "name": "x2", - "source": fix_path("./tests/test_pytest.py:113"), - "markers": [], - "parentid": "./tests/test_pytest.py::test_param_13", - }, - { - "id": "./tests/test_pytest.py::test_param_13_repeat[x0]", - "name": "x0", - "source": fix_path("./tests/test_pytest.py:118"), - "markers": [], - "parentid": "./tests/test_pytest.py::test_param_13_repeat", - }, - { - "id": "./tests/test_pytest.py::test_param_13_repeat[x1]", - "name": "x1", - "source": fix_path("./tests/test_pytest.py:118"), - "markers": [], - "parentid": "./tests/test_pytest.py::test_param_13_repeat", - }, - { - "id": "./tests/test_pytest.py::test_param_13_repeat[x2]", - "name": "x2", - "source": fix_path("./tests/test_pytest.py:118"), - "markers": [], - "parentid": "./tests/test_pytest.py::test_param_13_repeat", - }, - { - "id": "./tests/test_pytest.py::test_param_33[1-1-1]", - "name": "1-1-1", - "source": fix_path("./tests/test_pytest.py:123"), - "markers": [], - "parentid": "./tests/test_pytest.py::test_param_33", - }, - { - "id": "./tests/test_pytest.py::test_param_33[3-4-5]", - "name": "3-4-5", - "source": fix_path("./tests/test_pytest.py:123"), - "markers": [], - "parentid": "./tests/test_pytest.py::test_param_33", - }, - { - "id": "./tests/test_pytest.py::test_param_33[0-0-0]", - "name": "0-0-0", - "source": fix_path("./tests/test_pytest.py:123"), - "markers": [], - "parentid": "./tests/test_pytest.py::test_param_33", - }, - { - "id": "./tests/test_pytest.py::test_param_33_ids[v1]", - "name": "v1", - "source": fix_path("./tests/test_pytest.py:128"), - "markers": [], - "parentid": "./tests/test_pytest.py::test_param_33_ids", - }, - { - "id": "./tests/test_pytest.py::test_param_33_ids[v2]", - "name": "v2", - "source": fix_path("./tests/test_pytest.py:128"), - "markers": [], - "parentid": "./tests/test_pytest.py::test_param_33_ids", - }, - { - "id": "./tests/test_pytest.py::test_param_33_ids[v3]", - "name": "v3", - "source": fix_path("./tests/test_pytest.py:128"), - "markers": [], - "parentid": "./tests/test_pytest.py::test_param_33_ids", - }, - { - "id": "./tests/test_pytest.py::test_param_23_13[1-1-z0]", - "name": "1-1-z0", - "source": fix_path("./tests/test_pytest.py:134"), - "markers": [], - "parentid": "./tests/test_pytest.py::test_param_23_13", - }, - { - "id": "./tests/test_pytest.py::test_param_23_13[1-1-z1]", - "name": "1-1-z1", - "source": fix_path("./tests/test_pytest.py:134"), - "markers": [], - "parentid": "./tests/test_pytest.py::test_param_23_13", - }, - { - "id": "./tests/test_pytest.py::test_param_23_13[1-1-z2]", - "name": "1-1-z2", - "source": fix_path("./tests/test_pytest.py:134"), - "markers": [], - "parentid": "./tests/test_pytest.py::test_param_23_13", - }, - { - "id": "./tests/test_pytest.py::test_param_23_13[3-4-z0]", - "name": "3-4-z0", - "source": fix_path("./tests/test_pytest.py:134"), - "markers": [], - "parentid": "./tests/test_pytest.py::test_param_23_13", - }, - { - "id": "./tests/test_pytest.py::test_param_23_13[3-4-z1]", - "name": "3-4-z1", - "source": fix_path("./tests/test_pytest.py:134"), - "markers": [], - "parentid": "./tests/test_pytest.py::test_param_23_13", - }, - { - "id": "./tests/test_pytest.py::test_param_23_13[3-4-z2]", - "name": "3-4-z2", - "source": fix_path("./tests/test_pytest.py:134"), - "markers": [], - "parentid": "./tests/test_pytest.py::test_param_23_13", - }, - { - "id": "./tests/test_pytest.py::test_param_23_13[0-0-z0]", - "name": "0-0-z0", - "source": fix_path("./tests/test_pytest.py:134"), - "markers": [], - "parentid": "./tests/test_pytest.py::test_param_23_13", - }, - { - "id": "./tests/test_pytest.py::test_param_23_13[0-0-z1]", - "name": "0-0-z1", - "source": fix_path("./tests/test_pytest.py:134"), - "markers": [], - "parentid": "./tests/test_pytest.py::test_param_23_13", - }, - { - "id": "./tests/test_pytest.py::test_param_23_13[0-0-z2]", - "name": "0-0-z2", - "source": fix_path("./tests/test_pytest.py:134"), - "markers": [], - "parentid": "./tests/test_pytest.py::test_param_23_13", - }, - { - "id": "./tests/test_pytest.py::test_param_13_markers[x0]", - "name": "x0", - "source": fix_path("./tests/test_pytest.py:140"), - "markers": [], - "parentid": "./tests/test_pytest.py::test_param_13_markers", - }, - { - "id": "./tests/test_pytest.py::test_param_13_markers[???]", - "name": "???", - "source": fix_path("./tests/test_pytest.py:140"), - "markers": ["skip"], - "parentid": "./tests/test_pytest.py::test_param_13_markers", - }, - { - "id": "./tests/test_pytest.py::test_param_13_markers[2]", - "name": "2", - "source": fix_path("./tests/test_pytest.py:140"), - "markers": ["expected-failure"], - "parentid": "./tests/test_pytest.py::test_param_13_markers", - }, - { - "id": "./tests/test_pytest.py::test_param_13_skipped[x0]", - "name": "x0", - "source": fix_path("./tests/test_pytest.py:149"), - "markers": ["skip"], - "parentid": "./tests/test_pytest.py::test_param_13_skipped", - }, - { - "id": "./tests/test_pytest.py::test_param_13_skipped[x1]", - "name": "x1", - "source": fix_path("./tests/test_pytest.py:149"), - "markers": ["skip"], - "parentid": "./tests/test_pytest.py::test_param_13_skipped", - }, - { - "id": "./tests/test_pytest.py::test_param_13_skipped[x2]", - "name": "x2", - "source": fix_path("./tests/test_pytest.py:149"), - "markers": ["skip"], - "parentid": "./tests/test_pytest.py::test_param_13_skipped", - }, - { - "id": "./tests/test_pytest.py::test_param_23_raises[1-None]", - "name": "1-None", - "source": fix_path("./tests/test_pytest.py:155"), - "markers": [], - "parentid": "./tests/test_pytest.py::test_param_23_raises", - }, - { - "id": "./tests/test_pytest.py::test_param_23_raises[1.0-None]", - "name": "1.0-None", - "source": fix_path("./tests/test_pytest.py:155"), - "markers": [], - "parentid": "./tests/test_pytest.py::test_param_23_raises", - }, - { - "id": "./tests/test_pytest.py::test_param_23_raises[2-catch2]", - "name": "2-catch2", - "source": fix_path("./tests/test_pytest.py:155"), - "markers": [], - "parentid": "./tests/test_pytest.py::test_param_23_raises", - }, - { - "id": "./tests/test_pytest.py::TestParam::test_simple", - "name": "test_simple", - "source": fix_path("./tests/test_pytest.py:164"), - "markers": [], - "parentid": "./tests/test_pytest.py::TestParam", - }, - { - "id": "./tests/test_pytest.py::TestParam::test_param_13[x0]", - "name": "x0", - "source": fix_path("./tests/test_pytest.py:167"), - "markers": [], - "parentid": "./tests/test_pytest.py::TestParam::test_param_13", - }, - { - "id": "./tests/test_pytest.py::TestParam::test_param_13[x1]", - "name": "x1", - "source": fix_path("./tests/test_pytest.py:167"), - "markers": [], - "parentid": "./tests/test_pytest.py::TestParam::test_param_13", - }, - { - "id": "./tests/test_pytest.py::TestParam::test_param_13[x2]", - "name": "x2", - "source": fix_path("./tests/test_pytest.py:167"), - "markers": [], - "parentid": "./tests/test_pytest.py::TestParam::test_param_13", - }, - { - "id": "./tests/test_pytest.py::TestParamAll::test_param_13[x0]", - "name": "x0", - "source": fix_path("./tests/test_pytest.py:175"), - "markers": [], - "parentid": "./tests/test_pytest.py::TestParamAll::test_param_13", - }, - { - "id": "./tests/test_pytest.py::TestParamAll::test_param_13[x1]", - "name": "x1", - "source": fix_path("./tests/test_pytest.py:175"), - "markers": [], - "parentid": "./tests/test_pytest.py::TestParamAll::test_param_13", - }, - { - "id": "./tests/test_pytest.py::TestParamAll::test_param_13[x2]", - "name": "x2", - "source": fix_path("./tests/test_pytest.py:175"), - "markers": [], - "parentid": "./tests/test_pytest.py::TestParamAll::test_param_13", - }, - { - "id": "./tests/test_pytest.py::TestParamAll::test_spam_13[x0]", - "name": "x0", - "source": fix_path("./tests/test_pytest.py:178"), - "markers": [], - "parentid": "./tests/test_pytest.py::TestParamAll::test_spam_13", - }, - { - "id": "./tests/test_pytest.py::TestParamAll::test_spam_13[x1]", - "name": "x1", - "source": fix_path("./tests/test_pytest.py:178"), - "markers": [], - "parentid": "./tests/test_pytest.py::TestParamAll::test_spam_13", - }, - { - "id": "./tests/test_pytest.py::TestParamAll::test_spam_13[x2]", - "name": "x2", - "source": fix_path("./tests/test_pytest.py:178"), - "markers": [], - "parentid": "./tests/test_pytest.py::TestParamAll::test_spam_13", - }, - { - "id": "./tests/test_pytest.py::test_fixture", - "name": "test_fixture", - "source": fix_path("./tests/test_pytest.py:192"), - "markers": [], - "parentid": "./tests/test_pytest.py", - }, - { - "id": "./tests/test_pytest.py::test_mark_fixture", - "name": "test_mark_fixture", - "source": fix_path("./tests/test_pytest.py:196"), - "markers": [], - "parentid": "./tests/test_pytest.py", - }, - { - "id": "./tests/test_pytest.py::test_param_fixture[x0]", - "name": "x0", - "source": fix_path("./tests/test_pytest.py:201"), - "markers": [], - "parentid": "./tests/test_pytest.py::test_param_fixture", - }, - { - "id": "./tests/test_pytest.py::test_param_fixture[x1]", - "name": "x1", - "source": fix_path("./tests/test_pytest.py:201"), - "markers": [], - "parentid": "./tests/test_pytest.py::test_param_fixture", - }, - { - "id": "./tests/test_pytest.py::test_param_fixture[x2]", - "name": "x2", - "source": fix_path("./tests/test_pytest.py:201"), - "markers": [], - "parentid": "./tests/test_pytest.py::test_param_fixture", - }, - { - "id": "./tests/test_pytest.py::test_param_mark_fixture[(1+0j)]", - "name": "(1+0j)", - "source": fix_path("./tests/test_pytest.py:207"), - "markers": [], - "parentid": "./tests/test_pytest.py::test_param_mark_fixture", - }, - { - "id": "./tests/test_pytest.py::test_param_mark_fixture[x0]", - "name": "x0", - "source": fix_path("./tests/test_pytest.py:207"), - "markers": [], - "parentid": "./tests/test_pytest.py::test_param_mark_fixture", - }, - { - "id": "./tests/test_pytest.py::test_param_mark_fixture[x1]", - "name": "x1", - "source": fix_path("./tests/test_pytest.py:207"), - "markers": [], - "parentid": "./tests/test_pytest.py::test_param_mark_fixture", - }, - { - "id": "./tests/test_pytest.py::test_fixture_param[spam]", - "name": "spam", - "source": fix_path("./tests/test_pytest.py:216"), - "markers": [], - "parentid": "./tests/test_pytest.py::test_fixture_param", - }, - { - "id": "./tests/test_pytest.py::test_fixture_param[eggs]", - "name": "eggs", - "source": fix_path("./tests/test_pytest.py:216"), - "markers": [], - "parentid": "./tests/test_pytest.py::test_fixture_param", - }, - ###### - { - "id": "./tests/test_pytest_param.py::test_param_13[x0]", - "name": "x0", - "source": fix_path("./tests/test_pytest_param.py:8"), - "markers": [], - "parentid": "./tests/test_pytest_param.py::test_param_13", - }, - { - "id": "./tests/test_pytest_param.py::test_param_13[x1]", - "name": "x1", - "source": fix_path("./tests/test_pytest_param.py:8"), - "markers": [], - "parentid": "./tests/test_pytest_param.py::test_param_13", - }, - { - "id": "./tests/test_pytest_param.py::test_param_13[x2]", - "name": "x2", - "source": fix_path("./tests/test_pytest_param.py:8"), - "markers": [], - "parentid": "./tests/test_pytest_param.py::test_param_13", - }, - { - "id": "./tests/test_pytest_param.py::TestParamAll::test_param_13[x0]", - "name": "x0", - "source": fix_path("./tests/test_pytest_param.py:14"), - "markers": [], - "parentid": "./tests/test_pytest_param.py::TestParamAll::test_param_13", - }, - { - "id": "./tests/test_pytest_param.py::TestParamAll::test_param_13[x1]", - "name": "x1", - "source": fix_path("./tests/test_pytest_param.py:14"), - "markers": [], - "parentid": "./tests/test_pytest_param.py::TestParamAll::test_param_13", - }, - { - "id": "./tests/test_pytest_param.py::TestParamAll::test_param_13[x2]", - "name": "x2", - "source": fix_path("./tests/test_pytest_param.py:14"), - "markers": [], - "parentid": "./tests/test_pytest_param.py::TestParamAll::test_param_13", - }, - { - "id": "./tests/test_pytest_param.py::TestParamAll::test_spam_13[x0]", - "name": "x0", - "source": fix_path("./tests/test_pytest_param.py:17"), - "markers": [], - "parentid": "./tests/test_pytest_param.py::TestParamAll::test_spam_13", - }, - { - "id": "./tests/test_pytest_param.py::TestParamAll::test_spam_13[x1]", - "name": "x1", - "source": fix_path("./tests/test_pytest_param.py:17"), - "markers": [], - "parentid": "./tests/test_pytest_param.py::TestParamAll::test_spam_13", - }, - { - "id": "./tests/test_pytest_param.py::TestParamAll::test_spam_13[x2]", - "name": "x2", - "source": fix_path("./tests/test_pytest_param.py:17"), - "markers": [], - "parentid": "./tests/test_pytest_param.py::TestParamAll::test_spam_13", - }, - ###### - { - "id": "./tests/test_unittest.py::MyTests::test_dynamic_", - "name": "test_dynamic_", - "source": fix_path("./tests/test_unittest.py:54"), - "markers": [], - "parentid": "./tests/test_unittest.py::MyTests", - }, - { - "id": "./tests/test_unittest.py::MyTests::test_failure", - "name": "test_failure", - "source": fix_path("./tests/test_unittest.py:34"), - "markers": [], - "parentid": "./tests/test_unittest.py::MyTests", - }, - { - "id": "./tests/test_unittest.py::MyTests::test_known_failure", - "name": "test_known_failure", - "source": fix_path("./tests/test_unittest.py:37"), - "markers": [], - "parentid": "./tests/test_unittest.py::MyTests", - }, - { - "id": "./tests/test_unittest.py::MyTests::test_maybe_not_skipped", - "name": "test_maybe_not_skipped", - "source": fix_path("./tests/test_unittest.py:17"), - "markers": [], - "parentid": "./tests/test_unittest.py::MyTests", - }, - { - "id": "./tests/test_unittest.py::MyTests::test_maybe_skipped", - "name": "test_maybe_skipped", - "source": fix_path("./tests/test_unittest.py:13"), - "markers": [], - "parentid": "./tests/test_unittest.py::MyTests", - }, - { - "id": "./tests/test_unittest.py::MyTests::test_simple", - "name": "test_simple", - "source": fix_path("./tests/test_unittest.py:6"), - "markers": [], - "parentid": "./tests/test_unittest.py::MyTests", - }, - { - "id": "./tests/test_unittest.py::MyTests::test_skipped", - "name": "test_skipped", - "source": fix_path("./tests/test_unittest.py:9"), - "markers": [], - "parentid": "./tests/test_unittest.py::MyTests", - }, - { - "id": "./tests/test_unittest.py::MyTests::test_skipped_inside", - "name": "test_skipped_inside", - "source": fix_path("./tests/test_unittest.py:21"), - "markers": [], - "parentid": "./tests/test_unittest.py::MyTests", - }, - { - "id": "./tests/test_unittest.py::MyTests::test_with_nested_subtests", - "name": "test_with_nested_subtests", - "source": fix_path("./tests/test_unittest.py:46"), - "markers": [], - "parentid": "./tests/test_unittest.py::MyTests", - }, - { - "id": "./tests/test_unittest.py::MyTests::test_with_subtests", - "name": "test_with_subtests", - "source": fix_path("./tests/test_unittest.py:41"), - "markers": [], - "parentid": "./tests/test_unittest.py::MyTests", - }, - { - "id": "./tests/test_unittest.py::OtherTests::test_simple", - "name": "test_simple", - "source": fix_path("./tests/test_unittest.py:61"), - "markers": [], - "parentid": "./tests/test_unittest.py::OtherTests", - }, - ########### - { - "id": "./tests/v/test_eggs.py::test_simple", - "name": "test_simple", - "source": fix_path("./tests/v/spam.py:2"), - "markers": [], - "parentid": "./tests/v/test_eggs.py", - }, - { - "id": "./tests/v/test_eggs.py::TestSimple::test_simple", - "name": "test_simple", - "source": fix_path("./tests/v/spam.py:8"), - "markers": [], - "parentid": "./tests/v/test_eggs.py::TestSimple", - }, - ###### - { - "id": "./tests/v/test_ham.py::test_simple", - "name": "test_simple", - "source": fix_path("./tests/v/spam.py:2"), - "markers": [], - "parentid": "./tests/v/test_ham.py", - }, - { - "id": "./tests/v/test_ham.py::test_not_hard", - "name": "test_not_hard", - "source": fix_path("./tests/v/spam.py:2"), - "markers": [], - "parentid": "./tests/v/test_ham.py", - }, - ###### - { - "id": "./tests/v/test_spam.py::test_simple", - "name": "test_simple", - "source": fix_path("./tests/v/spam.py:2"), - "markers": [], - "parentid": "./tests/v/test_spam.py", - }, - { - "id": "./tests/v/test_spam.py::test_simpler", - "name": "test_simpler", - "source": fix_path("./tests/v/test_spam.py:4"), - "markers": [], - "parentid": "./tests/v/test_spam.py", - }, - ########### - { - "id": "./tests/w/test_spam.py::test_simple", - "name": "test_simple", - "source": fix_path("./tests/w/test_spam.py:4"), - "markers": [], - "parentid": "./tests/w/test_spam.py", - }, - { - "id": "./tests/w/test_spam_ex.py::test_simple", - "name": "test_simple", - "source": fix_path("./tests/w/test_spam_ex.py:4"), - "markers": [], - "parentid": "./tests/w/test_spam_ex.py", - }, - ########### - { - "id": "./tests/x/y/z/test_ham.py::test_simple", - "name": "test_simple", - "source": fix_path("./tests/x/y/z/test_ham.py:2"), - "markers": [], - "parentid": "./tests/x/y/z/test_ham.py", - }, - ###### - { - "id": "./tests/x/y/z/a/test_spam.py::test_simple", - "name": "test_simple", - "source": fix_path("./tests/x/y/z/a/test_spam.py:11"), - "markers": [], - "parentid": "./tests/x/y/z/a/test_spam.py", - }, - { - "id": "./tests/x/y/z/b/test_spam.py::test_simple", - "name": "test_simple", - "source": fix_path("./tests/x/y/z/b/test_spam.py:7"), - "markers": [], - "parentid": "./tests/x/y/z/b/test_spam.py", - }, - ], -} diff --git a/pythonFiles/tests/testing_tools/adapter/test_report.py b/pythonFiles/tests/testing_tools/adapter/test_report.py deleted file mode 100644 index bb68c8a65e79..000000000000 --- a/pythonFiles/tests/testing_tools/adapter/test_report.py +++ /dev/null @@ -1,1179 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -import json -import unittest - -from ...util import StubProxy -from testing_tools.adapter.util import fix_path, fix_relpath -from testing_tools.adapter.info import SingleTestInfo, SingleTestPath, ParentInfo -from testing_tools.adapter.report import report_discovered - - -class StubSender(StubProxy): - def send(self, outstr): - self.add_call("send", (json.loads(outstr),), None) - - -################################## -# tests - - -class ReportDiscoveredTests(unittest.TestCase): - def test_basic(self): - stub = StubSender() - testroot = fix_path("/a/b/c") - relfile = "test_spam.py" - relpath = fix_relpath(relfile) - tests = [ - SingleTestInfo( - id="test#1", - name="test_spam", - path=SingleTestPath( - root=testroot, - relfile=relfile, - func="test_spam", - ), - source="{}:{}".format(relfile, 10), - markers=[], - parentid="file#1", - ), - ] - parents = [ - ParentInfo( - id="", - kind="folder", - name=testroot, - ), - ParentInfo( - id="file#1", - kind="file", - name=relfile, - root=testroot, - relpath=relpath, - parentid="", - ), - ] - expected = [ - { - "rootid": "", - "root": testroot, - "parents": [ - { - "id": "file#1", - "kind": "file", - "name": relfile, - "relpath": relpath, - "parentid": "", - }, - ], - "tests": [ - { - "id": "test#1", - "name": "test_spam", - "source": "{}:{}".format(relfile, 10), - "markers": [], - "parentid": "file#1", - } - ], - } - ] - - report_discovered(tests, parents, _send=stub.send) - - self.maxDiff = None - self.assertEqual( - stub.calls, - [ - ("send", (expected,), None), - ], - ) - - def test_multiroot(self): - stub = StubSender() - # the first root - testroot1 = fix_path("/a/b/c") - relfileid1 = "./test_spam.py" - relpath1 = fix_path(relfileid1) - relfile1 = relpath1[2:] - tests = [ - SingleTestInfo( - id=relfileid1 + "::test_spam", - name="test_spam", - path=SingleTestPath( - root=testroot1, - relfile=relfile1, - func="test_spam", - ), - source="{}:{}".format(relfile1, 10), - markers=[], - parentid=relfileid1, - ), - ] - parents = [ - ParentInfo( - id=".", - kind="folder", - name=testroot1, - ), - ParentInfo( - id=relfileid1, - kind="file", - name="test_spam.py", - root=testroot1, - relpath=relpath1, - parentid=".", - ), - ] - expected = [ - { - "rootid": ".", - "root": testroot1, - "parents": [ - { - "id": relfileid1, - "kind": "file", - "name": "test_spam.py", - "relpath": relpath1, - "parentid": ".", - }, - ], - "tests": [ - { - "id": relfileid1 + "::test_spam", - "name": "test_spam", - "source": "{}:{}".format(relfile1, 10), - "markers": [], - "parentid": relfileid1, - } - ], - }, - ] - # the second root - testroot2 = fix_path("/x/y/z") - relfileid2 = "./w/test_eggs.py" - relpath2 = fix_path(relfileid2) - relfile2 = relpath2[2:] - tests.extend( - [ - SingleTestInfo( - id=relfileid2 + "::BasicTests::test_first", - name="test_first", - path=SingleTestPath( - root=testroot2, - relfile=relfile2, - func="BasicTests.test_first", - ), - source="{}:{}".format(relfile2, 61), - markers=[], - parentid=relfileid2 + "::BasicTests", - ), - ] - ) - parents.extend( - [ - ParentInfo( - id=".", - kind="folder", - name=testroot2, - ), - ParentInfo( - id="./w", - kind="folder", - name="w", - root=testroot2, - relpath=fix_path("./w"), - parentid=".", - ), - ParentInfo( - id=relfileid2, - kind="file", - name="test_eggs.py", - root=testroot2, - relpath=relpath2, - parentid="./w", - ), - ParentInfo( - id=relfileid2 + "::BasicTests", - kind="suite", - name="BasicTests", - root=testroot2, - parentid=relfileid2, - ), - ] - ) - expected.extend( - [ - { - "rootid": ".", - "root": testroot2, - "parents": [ - { - "id": "./w", - "kind": "folder", - "name": "w", - "relpath": fix_path("./w"), - "parentid": ".", - }, - { - "id": relfileid2, - "kind": "file", - "name": "test_eggs.py", - "relpath": relpath2, - "parentid": "./w", - }, - { - "id": relfileid2 + "::BasicTests", - "kind": "suite", - "name": "BasicTests", - "parentid": relfileid2, - }, - ], - "tests": [ - { - "id": relfileid2 + "::BasicTests::test_first", - "name": "test_first", - "source": "{}:{}".format(relfile2, 61), - "markers": [], - "parentid": relfileid2 + "::BasicTests", - } - ], - }, - ] - ) - - report_discovered(tests, parents, _send=stub.send) - - self.maxDiff = None - self.assertEqual( - stub.calls, - [ - ("send", (expected,), None), - ], - ) - - def test_complex(self): - """ - /a/b/c/ - test_ham.py - MySuite - test_x1 - test_x2 - /a/b/e/f/g/ - w/ - test_ham.py - test_ham1 - HamTests - test_uh_oh - test_whoa - MoreHam - test_yay - sub1 - sub2 - sub3 - test_eggs.py - SpamTests - test_okay - x/ - y/ - a/ - test_spam.py - SpamTests - test_okay - b/ - test_spam.py - SpamTests - test_okay - test_spam.py - SpamTests - test_okay - """ - stub = StubSender() - testroot = fix_path("/a/b/c") - relfileid1 = "./test_ham.py" - relfileid2 = "./test_spam.py" - relfileid3 = "./w/test_ham.py" - relfileid4 = "./w/test_eggs.py" - relfileid5 = "./x/y/a/test_spam.py" - relfileid6 = "./x/y/b/test_spam.py" - tests = [ - SingleTestInfo( - id=relfileid1 + "::MySuite::test_x1", - name="test_x1", - path=SingleTestPath( - root=testroot, - relfile=fix_path(relfileid1), - func="MySuite.test_x1", - ), - source="{}:{}".format(fix_path(relfileid1), 10), - markers=None, - parentid=relfileid1 + "::MySuite", - ), - SingleTestInfo( - id=relfileid1 + "::MySuite::test_x2", - name="test_x2", - path=SingleTestPath( - root=testroot, - relfile=fix_path(relfileid1), - func="MySuite.test_x2", - ), - source="{}:{}".format(fix_path(relfileid1), 21), - markers=None, - parentid=relfileid1 + "::MySuite", - ), - SingleTestInfo( - id=relfileid2 + "::SpamTests::test_okay", - name="test_okay", - path=SingleTestPath( - root=testroot, - relfile=fix_path(relfileid2), - func="SpamTests.test_okay", - ), - source="{}:{}".format(fix_path(relfileid2), 17), - markers=None, - parentid=relfileid2 + "::SpamTests", - ), - SingleTestInfo( - id=relfileid3 + "::test_ham1", - name="test_ham1", - path=SingleTestPath( - root=testroot, - relfile=fix_path(relfileid3), - func="test_ham1", - ), - source="{}:{}".format(fix_path(relfileid3), 8), - markers=None, - parentid=relfileid3, - ), - SingleTestInfo( - id=relfileid3 + "::HamTests::test_uh_oh", - name="test_uh_oh", - path=SingleTestPath( - root=testroot, - relfile=fix_path(relfileid3), - func="HamTests.test_uh_oh", - ), - source="{}:{}".format(fix_path(relfileid3), 19), - markers=["expected-failure"], - parentid=relfileid3 + "::HamTests", - ), - SingleTestInfo( - id=relfileid3 + "::HamTests::test_whoa", - name="test_whoa", - path=SingleTestPath( - root=testroot, - relfile=fix_path(relfileid3), - func="HamTests.test_whoa", - ), - source="{}:{}".format(fix_path(relfileid3), 35), - markers=None, - parentid=relfileid3 + "::HamTests", - ), - SingleTestInfo( - id=relfileid3 + "::MoreHam::test_yay[1-2]", - name="test_yay[1-2]", - path=SingleTestPath( - root=testroot, - relfile=fix_path(relfileid3), - func="MoreHam.test_yay", - sub=["[1-2]"], - ), - source="{}:{}".format(fix_path(relfileid3), 57), - markers=None, - parentid=relfileid3 + "::MoreHam::test_yay", - ), - SingleTestInfo( - id=relfileid3 + "::MoreHam::test_yay[1-2][3-4]", - name="test_yay[1-2][3-4]", - path=SingleTestPath( - root=testroot, - relfile=fix_path(relfileid3), - func="MoreHam.test_yay", - sub=["[1-2]", "[3=4]"], - ), - source="{}:{}".format(fix_path(relfileid3), 72), - markers=None, - parentid=relfileid3 + "::MoreHam::test_yay[1-2]", - ), - SingleTestInfo( - id=relfileid4 + "::SpamTests::test_okay", - name="test_okay", - path=SingleTestPath( - root=testroot, - relfile=fix_path(relfileid4), - func="SpamTests.test_okay", - ), - source="{}:{}".format(fix_path(relfileid4), 15), - markers=None, - parentid=relfileid4 + "::SpamTests", - ), - SingleTestInfo( - id=relfileid5 + "::SpamTests::test_okay", - name="test_okay", - path=SingleTestPath( - root=testroot, - relfile=fix_path(relfileid5), - func="SpamTests.test_okay", - ), - source="{}:{}".format(fix_path(relfileid5), 12), - markers=None, - parentid=relfileid5 + "::SpamTests", - ), - SingleTestInfo( - id=relfileid6 + "::SpamTests::test_okay", - name="test_okay", - path=SingleTestPath( - root=testroot, - relfile=fix_path(relfileid6), - func="SpamTests.test_okay", - ), - source="{}:{}".format(fix_path(relfileid6), 27), - markers=None, - parentid=relfileid6 + "::SpamTests", - ), - ] - parents = [ - ParentInfo( - id=".", - kind="folder", - name=testroot, - ), - ParentInfo( - id=relfileid1, - kind="file", - name="test_ham.py", - root=testroot, - relpath=fix_path(relfileid1), - parentid=".", - ), - ParentInfo( - id=relfileid1 + "::MySuite", - kind="suite", - name="MySuite", - root=testroot, - parentid=relfileid1, - ), - ParentInfo( - id=relfileid2, - kind="file", - name="test_spam.py", - root=testroot, - relpath=fix_path(relfileid2), - parentid=".", - ), - ParentInfo( - id=relfileid2 + "::SpamTests", - kind="suite", - name="SpamTests", - root=testroot, - parentid=relfileid2, - ), - ParentInfo( - id="./w", - kind="folder", - name="w", - root=testroot, - relpath=fix_path("./w"), - parentid=".", - ), - ParentInfo( - id=relfileid3, - kind="file", - name="test_ham.py", - root=testroot, - relpath=fix_path(relfileid3), - parentid="./w", - ), - ParentInfo( - id=relfileid3 + "::HamTests", - kind="suite", - name="HamTests", - root=testroot, - parentid=relfileid3, - ), - ParentInfo( - id=relfileid3 + "::MoreHam", - kind="suite", - name="MoreHam", - root=testroot, - parentid=relfileid3, - ), - ParentInfo( - id=relfileid3 + "::MoreHam::test_yay", - kind="function", - name="test_yay", - root=testroot, - parentid=relfileid3 + "::MoreHam", - ), - ParentInfo( - id=relfileid3 + "::MoreHam::test_yay[1-2]", - kind="subtest", - name="test_yay[1-2]", - root=testroot, - parentid=relfileid3 + "::MoreHam::test_yay", - ), - ParentInfo( - id=relfileid4, - kind="file", - name="test_eggs.py", - root=testroot, - relpath=fix_path(relfileid4), - parentid="./w", - ), - ParentInfo( - id=relfileid4 + "::SpamTests", - kind="suite", - name="SpamTests", - root=testroot, - parentid=relfileid4, - ), - ParentInfo( - id="./x", - kind="folder", - name="x", - root=testroot, - relpath=fix_path("./x"), - parentid=".", - ), - ParentInfo( - id="./x/y", - kind="folder", - name="y", - root=testroot, - relpath=fix_path("./x/y"), - parentid="./x", - ), - ParentInfo( - id="./x/y/a", - kind="folder", - name="a", - root=testroot, - relpath=fix_path("./x/y/a"), - parentid="./x/y", - ), - ParentInfo( - id=relfileid5, - kind="file", - name="test_spam.py", - root=testroot, - relpath=fix_path(relfileid5), - parentid="./x/y/a", - ), - ParentInfo( - id=relfileid5 + "::SpamTests", - kind="suite", - name="SpamTests", - root=testroot, - parentid=relfileid5, - ), - ParentInfo( - id="./x/y/b", - kind="folder", - name="b", - root=testroot, - relpath=fix_path("./x/y/b"), - parentid="./x/y", - ), - ParentInfo( - id=relfileid6, - kind="file", - name="test_spam.py", - root=testroot, - relpath=fix_path(relfileid6), - parentid="./x/y/b", - ), - ParentInfo( - id=relfileid6 + "::SpamTests", - kind="suite", - name="SpamTests", - root=testroot, - parentid=relfileid6, - ), - ] - expected = [ - { - "rootid": ".", - "root": testroot, - "parents": [ - { - "id": relfileid1, - "kind": "file", - "name": "test_ham.py", - "relpath": fix_path(relfileid1), - "parentid": ".", - }, - { - "id": relfileid1 + "::MySuite", - "kind": "suite", - "name": "MySuite", - "parentid": relfileid1, - }, - { - "id": relfileid2, - "kind": "file", - "name": "test_spam.py", - "relpath": fix_path(relfileid2), - "parentid": ".", - }, - { - "id": relfileid2 + "::SpamTests", - "kind": "suite", - "name": "SpamTests", - "parentid": relfileid2, - }, - { - "id": "./w", - "kind": "folder", - "name": "w", - "relpath": fix_path("./w"), - "parentid": ".", - }, - { - "id": relfileid3, - "kind": "file", - "name": "test_ham.py", - "relpath": fix_path(relfileid3), - "parentid": "./w", - }, - { - "id": relfileid3 + "::HamTests", - "kind": "suite", - "name": "HamTests", - "parentid": relfileid3, - }, - { - "id": relfileid3 + "::MoreHam", - "kind": "suite", - "name": "MoreHam", - "parentid": relfileid3, - }, - { - "id": relfileid3 + "::MoreHam::test_yay", - "kind": "function", - "name": "test_yay", - "parentid": relfileid3 + "::MoreHam", - }, - { - "id": relfileid3 + "::MoreHam::test_yay[1-2]", - "kind": "subtest", - "name": "test_yay[1-2]", - "parentid": relfileid3 + "::MoreHam::test_yay", - }, - { - "id": relfileid4, - "kind": "file", - "name": "test_eggs.py", - "relpath": fix_path(relfileid4), - "parentid": "./w", - }, - { - "id": relfileid4 + "::SpamTests", - "kind": "suite", - "name": "SpamTests", - "parentid": relfileid4, - }, - { - "id": "./x", - "kind": "folder", - "name": "x", - "relpath": fix_path("./x"), - "parentid": ".", - }, - { - "id": "./x/y", - "kind": "folder", - "name": "y", - "relpath": fix_path("./x/y"), - "parentid": "./x", - }, - { - "id": "./x/y/a", - "kind": "folder", - "name": "a", - "relpath": fix_path("./x/y/a"), - "parentid": "./x/y", - }, - { - "id": relfileid5, - "kind": "file", - "name": "test_spam.py", - "relpath": fix_path(relfileid5), - "parentid": "./x/y/a", - }, - { - "id": relfileid5 + "::SpamTests", - "kind": "suite", - "name": "SpamTests", - "parentid": relfileid5, - }, - { - "id": "./x/y/b", - "kind": "folder", - "name": "b", - "relpath": fix_path("./x/y/b"), - "parentid": "./x/y", - }, - { - "id": relfileid6, - "kind": "file", - "name": "test_spam.py", - "relpath": fix_path(relfileid6), - "parentid": "./x/y/b", - }, - { - "id": relfileid6 + "::SpamTests", - "kind": "suite", - "name": "SpamTests", - "parentid": relfileid6, - }, - ], - "tests": [ - { - "id": relfileid1 + "::MySuite::test_x1", - "name": "test_x1", - "source": "{}:{}".format(fix_path(relfileid1), 10), - "markers": [], - "parentid": relfileid1 + "::MySuite", - }, - { - "id": relfileid1 + "::MySuite::test_x2", - "name": "test_x2", - "source": "{}:{}".format(fix_path(relfileid1), 21), - "markers": [], - "parentid": relfileid1 + "::MySuite", - }, - { - "id": relfileid2 + "::SpamTests::test_okay", - "name": "test_okay", - "source": "{}:{}".format(fix_path(relfileid2), 17), - "markers": [], - "parentid": relfileid2 + "::SpamTests", - }, - { - "id": relfileid3 + "::test_ham1", - "name": "test_ham1", - "source": "{}:{}".format(fix_path(relfileid3), 8), - "markers": [], - "parentid": relfileid3, - }, - { - "id": relfileid3 + "::HamTests::test_uh_oh", - "name": "test_uh_oh", - "source": "{}:{}".format(fix_path(relfileid3), 19), - "markers": ["expected-failure"], - "parentid": relfileid3 + "::HamTests", - }, - { - "id": relfileid3 + "::HamTests::test_whoa", - "name": "test_whoa", - "source": "{}:{}".format(fix_path(relfileid3), 35), - "markers": [], - "parentid": relfileid3 + "::HamTests", - }, - { - "id": relfileid3 + "::MoreHam::test_yay[1-2]", - "name": "test_yay[1-2]", - "source": "{}:{}".format(fix_path(relfileid3), 57), - "markers": [], - "parentid": relfileid3 + "::MoreHam::test_yay", - }, - { - "id": relfileid3 + "::MoreHam::test_yay[1-2][3-4]", - "name": "test_yay[1-2][3-4]", - "source": "{}:{}".format(fix_path(relfileid3), 72), - "markers": [], - "parentid": relfileid3 + "::MoreHam::test_yay[1-2]", - }, - { - "id": relfileid4 + "::SpamTests::test_okay", - "name": "test_okay", - "source": "{}:{}".format(fix_path(relfileid4), 15), - "markers": [], - "parentid": relfileid4 + "::SpamTests", - }, - { - "id": relfileid5 + "::SpamTests::test_okay", - "name": "test_okay", - "source": "{}:{}".format(fix_path(relfileid5), 12), - "markers": [], - "parentid": relfileid5 + "::SpamTests", - }, - { - "id": relfileid6 + "::SpamTests::test_okay", - "name": "test_okay", - "source": "{}:{}".format(fix_path(relfileid6), 27), - "markers": [], - "parentid": relfileid6 + "::SpamTests", - }, - ], - } - ] - - report_discovered(tests, parents, _send=stub.send) - - self.maxDiff = None - self.assertEqual( - stub.calls, - [ - ("send", (expected,), None), - ], - ) - - def test_simple_basic(self): - stub = StubSender() - testroot = fix_path("/a/b/c") - relfile = fix_path("x/y/z/test_spam.py") - tests = [ - SingleTestInfo( - id="test#1", - name="test_spam_1", - path=SingleTestPath( - root=testroot, - relfile=relfile, - func="MySuite.test_spam_1", - sub=None, - ), - source="{}:{}".format(relfile, 10), - markers=None, - parentid="suite#1", - ), - ] - parents = None - expected = [ - { - "id": "test#1", - "name": "test_spam_1", - "testroot": testroot, - "relfile": relfile, - "lineno": 10, - "testfunc": "MySuite.test_spam_1", - "subtest": None, - "markers": [], - } - ] - - report_discovered(tests, parents, simple=True, _send=stub.send) - - self.maxDiff = None - self.assertEqual( - stub.calls, - [ - ("send", (expected,), None), - ], - ) - - def test_simple_complex(self): - """ - /a/b/c/ - test_ham.py - MySuite - test_x1 - test_x2 - /a/b/e/f/g/ - w/ - test_ham.py - test_ham1 - HamTests - test_uh_oh - test_whoa - MoreHam - test_yay - sub1 - sub2 - sub3 - test_eggs.py - SpamTests - test_okay - x/ - y/ - a/ - test_spam.py - SpamTests - test_okay - b/ - test_spam.py - SpamTests - test_okay - test_spam.py - SpamTests - test_okay - """ - stub = StubSender() - testroot1 = fix_path("/a/b/c") - relfile1 = fix_path("./test_ham.py") - testroot2 = fix_path("/a/b/e/f/g") - relfile2 = fix_path("./test_spam.py") - relfile3 = fix_path("w/test_ham.py") - relfile4 = fix_path("w/test_eggs.py") - relfile5 = fix_path("x/y/a/test_spam.py") - relfile6 = fix_path("x/y/b/test_spam.py") - tests = [ - # under first root folder - SingleTestInfo( - id="test#1", - name="test_x1", - path=SingleTestPath( - root=testroot1, - relfile=relfile1, - func="MySuite.test_x1", - sub=None, - ), - source="{}:{}".format(relfile1, 10), - markers=None, - parentid="suite#1", - ), - SingleTestInfo( - id="test#2", - name="test_x2", - path=SingleTestPath( - root=testroot1, - relfile=relfile1, - func="MySuite.test_x2", - sub=None, - ), - source="{}:{}".format(relfile1, 21), - markers=None, - parentid="suite#1", - ), - # under second root folder - SingleTestInfo( - id="test#3", - name="test_okay", - path=SingleTestPath( - root=testroot2, - relfile=relfile2, - func="SpamTests.test_okay", - sub=None, - ), - source="{}:{}".format(relfile2, 17), - markers=None, - parentid="suite#2", - ), - SingleTestInfo( - id="test#4", - name="test_ham1", - path=SingleTestPath( - root=testroot2, - relfile=relfile3, - func="test_ham1", - sub=None, - ), - source="{}:{}".format(relfile3, 8), - markers=None, - parentid="file#3", - ), - SingleTestInfo( - id="test#5", - name="test_uh_oh", - path=SingleTestPath( - root=testroot2, - relfile=relfile3, - func="HamTests.test_uh_oh", - sub=None, - ), - source="{}:{}".format(relfile3, 19), - markers=["expected-failure"], - parentid="suite#3", - ), - SingleTestInfo( - id="test#6", - name="test_whoa", - path=SingleTestPath( - root=testroot2, - relfile=relfile3, - func="HamTests.test_whoa", - sub=None, - ), - source="{}:{}".format(relfile3, 35), - markers=None, - parentid="suite#3", - ), - SingleTestInfo( - id="test#7", - name="test_yay (sub1)", - path=SingleTestPath( - root=testroot2, - relfile=relfile3, - func="MoreHam.test_yay", - sub=["sub1"], - ), - source="{}:{}".format(relfile3, 57), - markers=None, - parentid="suite#4", - ), - SingleTestInfo( - id="test#8", - name="test_yay (sub2) (sub3)", - path=SingleTestPath( - root=testroot2, - relfile=relfile3, - func="MoreHam.test_yay", - sub=["sub2", "sub3"], - ), - source="{}:{}".format(relfile3, 72), - markers=None, - parentid="suite#3", - ), - SingleTestInfo( - id="test#9", - name="test_okay", - path=SingleTestPath( - root=testroot2, - relfile=relfile4, - func="SpamTests.test_okay", - sub=None, - ), - source="{}:{}".format(relfile4, 15), - markers=None, - parentid="suite#5", - ), - SingleTestInfo( - id="test#10", - name="test_okay", - path=SingleTestPath( - root=testroot2, - relfile=relfile5, - func="SpamTests.test_okay", - sub=None, - ), - source="{}:{}".format(relfile5, 12), - markers=None, - parentid="suite#6", - ), - SingleTestInfo( - id="test#11", - name="test_okay", - path=SingleTestPath( - root=testroot2, - relfile=relfile6, - func="SpamTests.test_okay", - sub=None, - ), - source="{}:{}".format(relfile6, 27), - markers=None, - parentid="suite#7", - ), - ] - expected = [ - { - "id": "test#1", - "name": "test_x1", - "testroot": testroot1, - "relfile": relfile1, - "lineno": 10, - "testfunc": "MySuite.test_x1", - "subtest": None, - "markers": [], - }, - { - "id": "test#2", - "name": "test_x2", - "testroot": testroot1, - "relfile": relfile1, - "lineno": 21, - "testfunc": "MySuite.test_x2", - "subtest": None, - "markers": [], - }, - { - "id": "test#3", - "name": "test_okay", - "testroot": testroot2, - "relfile": relfile2, - "lineno": 17, - "testfunc": "SpamTests.test_okay", - "subtest": None, - "markers": [], - }, - { - "id": "test#4", - "name": "test_ham1", - "testroot": testroot2, - "relfile": relfile3, - "lineno": 8, - "testfunc": "test_ham1", - "subtest": None, - "markers": [], - }, - { - "id": "test#5", - "name": "test_uh_oh", - "testroot": testroot2, - "relfile": relfile3, - "lineno": 19, - "testfunc": "HamTests.test_uh_oh", - "subtest": None, - "markers": ["expected-failure"], - }, - { - "id": "test#6", - "name": "test_whoa", - "testroot": testroot2, - "relfile": relfile3, - "lineno": 35, - "testfunc": "HamTests.test_whoa", - "subtest": None, - "markers": [], - }, - { - "id": "test#7", - "name": "test_yay (sub1)", - "testroot": testroot2, - "relfile": relfile3, - "lineno": 57, - "testfunc": "MoreHam.test_yay", - "subtest": ["sub1"], - "markers": [], - }, - { - "id": "test#8", - "name": "test_yay (sub2) (sub3)", - "testroot": testroot2, - "relfile": relfile3, - "lineno": 72, - "testfunc": "MoreHam.test_yay", - "subtest": ["sub2", "sub3"], - "markers": [], - }, - { - "id": "test#9", - "name": "test_okay", - "testroot": testroot2, - "relfile": relfile4, - "lineno": 15, - "testfunc": "SpamTests.test_okay", - "subtest": None, - "markers": [], - }, - { - "id": "test#10", - "name": "test_okay", - "testroot": testroot2, - "relfile": relfile5, - "lineno": 12, - "testfunc": "SpamTests.test_okay", - "subtest": None, - "markers": [], - }, - { - "id": "test#11", - "name": "test_okay", - "testroot": testroot2, - "relfile": relfile6, - "lineno": 27, - "testfunc": "SpamTests.test_okay", - "subtest": None, - "markers": [], - }, - ] - parents = None - - report_discovered(tests, parents, simple=True, _send=stub.send) - - self.maxDiff = None - self.assertEqual( - stub.calls, - [ - ("send", (expected,), None), - ], - ) diff --git a/pythonFiles/tests/testing_tools/adapter/test_util.py b/pythonFiles/tests/testing_tools/adapter/test_util.py deleted file mode 100644 index 822ba2ed1b22..000000000000 --- a/pythonFiles/tests/testing_tools/adapter/test_util.py +++ /dev/null @@ -1,330 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -from __future__ import absolute_import, print_function - -import ntpath -import os -import os.path -import posixpath -import shlex -import sys -import unittest - -import pytest - -# Pytest 3.7 and later uses pathlib/pathlib2 for path resolution. -try: - from pathlib import Path -except ImportError: - from pathlib2 import Path # type: ignore (for Pylance) - -from testing_tools.adapter.util import ( - fix_path, - fix_relpath, - fix_fileid, - shlex_unsplit, -) - - -@unittest.skipIf(sys.version_info < (3,), "Python 2 does not have subTest") -class FilePathTests(unittest.TestCase): - def test_isolated_imports(self): - import testing_tools.adapter - from testing_tools.adapter import util - from . import test_functional - - ignored = { - str(Path(os.path.abspath(__file__)).resolve()), - str(Path(os.path.abspath(util.__file__)).resolve()), - str(Path(os.path.abspath(test_functional.__file__)).resolve()), - } - adapter = os.path.abspath(os.path.dirname(testing_tools.adapter.__file__)) - tests = os.path.join( - os.path.abspath(os.path.dirname(os.path.dirname(testing_tools.__file__))), - "tests", - "testing_tools", - "adapter", - ) - found = [] - for root in [adapter, tests]: - for dirname, _, files in os.walk(root): - if ".data" in dirname: - continue - for basename in files: - if not basename.endswith(".py"): - continue - filename = os.path.join(dirname, basename) - if filename in ignored: - continue - with open(filename) as srcfile: - for line in srcfile: - if line.strip() == "import os.path": - found.append(filename) - break - - if found: - self.fail( - os.linesep.join( - [ - "", - "Please only use path-related API from testing_tools.adapter.util.", - 'Found use of "os.path" in the following files:', - ] - + [" " + file for file in found] - ) - ) - - def test_fix_path(self): - tests = [ - ("./spam.py", r".\spam.py"), - ("./some-dir", r".\some-dir"), - ("./some-dir/", ".\\some-dir\\"), - ("./some-dir/eggs", r".\some-dir\eggs"), - ("./some-dir/eggs/spam.py", r".\some-dir\eggs\spam.py"), - ("X/y/Z/a.B.c.PY", r"X\y\Z\a.B.c.PY"), - ("/", "\\"), - ("/spam", r"\spam"), - ("C:/spam", r"C:\spam"), - ] - for path, expected in tests: - pathsep = ntpath.sep - with self.subTest(r"fixed for \: {!r}".format(path)): - fixed = fix_path(path, _pathsep=pathsep) - self.assertEqual(fixed, expected) - - pathsep = posixpath.sep - with self.subTest("unchanged for /: {!r}".format(path)): - unchanged = fix_path(path, _pathsep=pathsep) - self.assertEqual(unchanged, path) - - # no path -> "." - for path in ["", None]: - for pathsep in [ntpath.sep, posixpath.sep]: - with self.subTest(r"fixed for {}: {!r}".format(pathsep, path)): - fixed = fix_path(path, _pathsep=pathsep) - self.assertEqual(fixed, ".") - - # no-op paths - paths = [path for _, path in tests] - paths.extend( - [ - ".", - "..", - "some-dir", - "spam.py", - ] - ) - for path in paths: - for pathsep in [ntpath.sep, posixpath.sep]: - with self.subTest(r"unchanged for {}: {!r}".format(pathsep, path)): - unchanged = fix_path(path, _pathsep=pathsep) - self.assertEqual(unchanged, path) - - def test_fix_relpath(self): - tests = [ - ("spam.py", posixpath, "./spam.py"), - ("eggs/spam.py", posixpath, "./eggs/spam.py"), - ("eggs/spam/", posixpath, "./eggs/spam/"), - (r"\spam.py", posixpath, r"./\spam.py"), - ("spam.py", ntpath, r".\spam.py"), - (r"eggs\spam.py", ntpath, r".\eggs\spam.py"), - ("eggs\\spam\\", ntpath, ".\\eggs\\spam\\"), - ("/spam.py", ntpath, r"\spam.py"), # Note the fixed "/". - # absolute - ("/", posixpath, "/"), - ("/spam.py", posixpath, "/spam.py"), - ("\\", ntpath, "\\"), - (r"\spam.py", ntpath, r"\spam.py"), - (r"C:\spam.py", ntpath, r"C:\spam.py"), - # no-op - ("./spam.py", posixpath, "./spam.py"), - (r".\spam.py", ntpath, r".\spam.py"), - ] - # no-op - for path in [".", ".."]: - tests.extend( - [ - (path, posixpath, path), - (path, ntpath, path), - ] - ) - for path, _os_path, expected in tests: - with self.subTest((path, _os_path.sep)): - fixed = fix_relpath( - path, - _fix_path=(lambda p: fix_path(p, _pathsep=_os_path.sep)), - _path_isabs=_os_path.isabs, - _pathsep=_os_path.sep, - ) - self.assertEqual(fixed, expected) - - def test_fix_fileid(self): - common = [ - ("spam.py", "./spam.py"), - ("eggs/spam.py", "./eggs/spam.py"), - ("eggs/spam/", "./eggs/spam/"), - # absolute (no-op) - ("/", "/"), - ("//", "//"), - ("/spam.py", "/spam.py"), - # no-op - (None, None), - ("", ""), - (".", "."), - ("./spam.py", "./spam.py"), - ] - tests = [(p, posixpath, e) for p, e in common] - tests.extend( - (p, posixpath, e) - for p, e in [ - (r"\spam.py", r"./\spam.py"), - ] - ) - tests.extend((p, ntpath, e) for p, e in common) - tests.extend( - (p, ntpath, e) - for p, e in [ - (r"eggs\spam.py", "./eggs/spam.py"), - ("eggs\\spam\\", "./eggs/spam/"), - (r".\spam.py", r"./spam.py"), - # absolute - (r"\spam.py", "/spam.py"), - (r"C:\spam.py", "C:/spam.py"), - ("\\", "/"), - ("\\\\", "//"), - ("C:\\\\", "C://"), - ("C:/", "C:/"), - ("C://", "C://"), - ("C:/spam.py", "C:/spam.py"), - ] - ) - for fileid, _os_path, expected in tests: - pathsep = _os_path.sep - with self.subTest(r"for {}: {!r}".format(pathsep, fileid)): - fixed = fix_fileid( - fileid, - _path_isabs=_os_path.isabs, - _normcase=_os_path.normcase, - _pathsep=pathsep, - ) - self.assertEqual(fixed, expected) - - # with rootdir - common = [ - ("spam.py", "/eggs", "./spam.py"), - ("spam.py", r"\eggs", "./spam.py"), - # absolute - ("/spam.py", "/", "./spam.py"), - ("/eggs/spam.py", "/eggs", "./spam.py"), - ("/eggs/spam.py", "/eggs/", "./spam.py"), - # no-op - ("/spam.py", "/eggs", "/spam.py"), - ("/spam.py", "/eggs/", "/spam.py"), - # root-only (no-op) - ("/", "/", "/"), - ("/", "/spam", "/"), - ("//", "/", "//"), - ("//", "//", "//"), - ("//", "//spam", "//"), - ] - tests = [(p, r, posixpath, e) for p, r, e in common] - tests = [(p, r, ntpath, e) for p, r, e in common] - tests.extend( - (p, r, ntpath, e) - for p, r, e in [ - ("spam.py", r"\eggs", "./spam.py"), - # absolute - (r"\spam.py", "\\", r"./spam.py"), - (r"C:\spam.py", "C:\\", r"./spam.py"), - (r"\eggs\spam.py", r"\eggs", r"./spam.py"), - (r"\eggs\spam.py", "\\eggs\\", r"./spam.py"), - # normcase - (r"C:\spam.py", "c:\\", r"./spam.py"), - (r"\Eggs\Spam.py", "\\eggs", r"./Spam.py"), - (r"\eggs\spam.py", "\\Eggs", r"./spam.py"), - (r"\eggs\Spam.py", "\\Eggs", r"./Spam.py"), - # no-op - (r"\spam.py", r"\eggs", r"/spam.py"), - (r"C:\spam.py", r"C:\eggs", r"C:/spam.py"), - # TODO: Should these be supported. - (r"C:\spam.py", "\\", r"C:/spam.py"), - (r"\spam.py", "C:\\", r"/spam.py"), - # root-only - ("\\", "\\", "/"), - ("\\\\", "\\", "//"), - ("C:\\", "C:\\eggs", "C:/"), - ("C:\\", "C:\\", "C:/"), - (r"C:\spam.py", "D:\\", r"C:/spam.py"), - ] - ) - for fileid, rootdir, _os_path, expected in tests: - pathsep = _os_path.sep - with self.subTest( - r"for {} (with rootdir {!r}): {!r}".format(pathsep, rootdir, fileid) - ): - fixed = fix_fileid( - fileid, - rootdir, - _path_isabs=_os_path.isabs, - _normcase=_os_path.normcase, - _pathsep=pathsep, - ) - self.assertEqual(fixed, expected) - - -class ShlexUnsplitTests(unittest.TestCase): - def test_no_args(self): - argv = [] - joined = shlex_unsplit(argv) - - self.assertEqual(joined, "") - self.assertEqual(shlex.split(joined), argv) - - def test_one_arg(self): - argv = ["spam"] - joined = shlex_unsplit(argv) - - self.assertEqual(joined, "spam") - self.assertEqual(shlex.split(joined), argv) - - def test_multiple_args(self): - argv = [ - "-x", - "X", - "-xyz", - "spam", - "eggs", - ] - joined = shlex_unsplit(argv) - - self.assertEqual(joined, "-x X -xyz spam eggs") - self.assertEqual(shlex.split(joined), argv) - - def test_whitespace(self): - argv = [ - "-x", - "X Y Z", - "spam spam\tspam", - "eggs", - ] - joined = shlex_unsplit(argv) - - self.assertEqual(joined, "-x 'X Y Z' 'spam spam\tspam' eggs") - self.assertEqual(shlex.split(joined), argv) - - def test_quotation_marks(self): - argv = [ - "-x", - "''", - 'spam"spam"spam', - "ham'ham'ham", - "eggs", - ] - joined = shlex_unsplit(argv) - - self.assertEqual( - joined, - "-x ''\"'\"''\"'\"'' 'spam\"spam\"spam' 'ham'\"'\"'ham'\"'\"'ham' eggs", - ) - self.assertEqual(shlex.split(joined), argv) diff --git a/pythonFiles/tests/unittestadapter/conftest.py b/pythonFiles/tests/unittestadapter/conftest.py deleted file mode 100644 index 19af85d1e095..000000000000 --- a/pythonFiles/tests/unittestadapter/conftest.py +++ /dev/null @@ -1,8 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -import sys - -# Ignore the contents of this folder for Python 2 tests. -if sys.version_info[0] < 3: - collect_ignore_glob = ["*.py"] diff --git a/pythonFiles/tests/unittestadapter/helpers.py b/pythonFiles/tests/unittestadapter/helpers.py deleted file mode 100644 index 303d021368f7..000000000000 --- a/pythonFiles/tests/unittestadapter/helpers.py +++ /dev/null @@ -1,32 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -import pathlib - -TEST_DATA_PATH = pathlib.Path(__file__).parent / ".data" - - -def is_same_tree(tree1, tree2) -> bool: - """Helper function to test if two test trees are the same. - - `is_same_tree` starts by comparing the root attributes, and then checks if all children are the same. - """ - # Compare the root. - if any(tree1[key] != tree2[key] for key in ["path", "name", "type_"]): - return False - - # Compare child test nodes if they exist, otherwise compare test items. - if "children" in tree1 and "children" in tree2: - children1 = tree1["children"] - children2 = tree2["children"] - - # Compare test nodes. - if len(children1) != len(children2): - return False - else: - return all(is_same_tree(*pair) for pair in zip(children1, children2)) - elif "id_" in tree1 and "id_" in tree2: - # Compare test items. - return all(tree1[key] == tree2[key] for key in ["id_", "lineno"]) - - return False diff --git a/pythonFiles/tests/unittestadapter/test_discovery.py b/pythonFiles/tests/unittestadapter/test_discovery.py deleted file mode 100644 index 30ccb7ef4079..000000000000 --- a/pythonFiles/tests/unittestadapter/test_discovery.py +++ /dev/null @@ -1,216 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -import os -import pathlib -from typing import List - -import pytest -from unittestadapter.discovery import ( - DEFAULT_PORT, - discover_tests, - parse_discovery_cli_args, -) -from unittestadapter.utils import TestNodeTypeEnum, parse_unittest_args - -from .helpers import TEST_DATA_PATH, is_same_tree - - -@pytest.mark.parametrize( - "args, expected", - [ - (["--port", "6767", "--uuid", "some-uuid"], (6767, "some-uuid")), - (["--foo", "something", "--bar", "another"], (int(DEFAULT_PORT), None)), - (["--port", "4444", "--foo", "something", "--port", "9999"], (9999, None)), - ( - ["--uuid", "first-uuid", "--bar", "other", "--uuid", "second-uuid"], - (int(DEFAULT_PORT), "second-uuid"), - ), - ], -) -def test_parse_cli_args(args: List[str], expected: List[str]) -> None: - """The parse_cli_args function should parse and return the port and uuid passed as command-line options. - - If there were no --port or --uuid command-line option, it should return default values). - If there are multiple options, the last one wins. - """ - actual = parse_discovery_cli_args(args) - - assert expected == actual - - -@pytest.mark.parametrize( - "args, expected", - [ - ( - ["-s", "something", "-p", "other*", "-t", "else"], - ("something", "other*", "else"), - ), - ( - [ - "--start-directory", - "foo", - "--pattern", - "bar*", - "--top-level-directory", - "baz", - ], - ("foo", "bar*", "baz"), - ), - ( - ["--foo", "something"], - (".", "test*.py", None), - ), - ], -) -def test_parse_unittest_args(args: List[str], expected: List[str]) -> None: - """The parse_unittest_args function should return values for the start_dir, pattern, and top_level_dir arguments - when passed as command-line options, and ignore unrecognized arguments. - """ - actual = parse_unittest_args(args) - - assert actual == expected - - -def test_simple_discovery() -> None: - """The discover_tests function should return a dictionary with a "success" status, a uuid, no errors, and a test tree - if unittest discovery was performed successfully. - """ - start_dir = os.fsdecode(TEST_DATA_PATH) - pattern = "discovery_simple*" - file_path = os.fsdecode(pathlib.PurePath(TEST_DATA_PATH / "discovery_simple.py")) - - expected = { - "path": start_dir, - "type_": TestNodeTypeEnum.folder, - "name": ".data", - "children": [ - { - "name": "discovery_simple.py", - "type_": TestNodeTypeEnum.file, - "path": file_path, - "children": [ - { - "name": "DiscoverySimple", - "path": file_path, - "type_": TestNodeTypeEnum.class_, - "children": [ - { - "name": "test_one", - "path": file_path, - "type_": TestNodeTypeEnum.test, - "lineno": "14", - "id_": file_path - + "\\" - + "DiscoverySimple" - + "\\" - + "test_one", - }, - { - "name": "test_two", - "path": file_path, - "type_": TestNodeTypeEnum.test, - "lineno": "17", - "id_": file_path - + "\\" - + "DiscoverySimple" - + "\\" - + "test_two", - }, - ], - "id_": file_path + "\\" + "DiscoverySimple", - } - ], - "id_": file_path, - } - ], - "id_": start_dir, - } - - uuid = "some-uuid" - actual = discover_tests(start_dir, pattern, None, uuid) - - assert actual["status"] == "success" - assert is_same_tree(actual.get("tests"), expected) - assert "errors" not in actual - - -def test_empty_discovery() -> None: - """The discover_tests function should return a dictionary with a "success" status, a uuid, no errors, and no test tree - if unittest discovery was performed successfully but no tests were found. - """ - start_dir = os.fsdecode(TEST_DATA_PATH) - pattern = "discovery_empty*" - - uuid = "some-uuid" - actual = discover_tests(start_dir, pattern, None, uuid) - - assert actual["status"] == "success" - assert "tests" not in actual - assert "errors" not in actual - - -def test_error_discovery() -> None: - """The discover_tests function should return a dictionary with an "error" status, a uuid, the discovered tests, and a list of errors - if unittest discovery failed at some point. - """ - # Discover tests in .data/discovery_error/. - start_path = pathlib.PurePath(TEST_DATA_PATH / "discovery_error") - start_dir = os.fsdecode(start_path) - pattern = "file*" - - file_path = os.fsdecode(start_path / "file_two.py") - - expected = { - "path": start_dir, - "type_": TestNodeTypeEnum.folder, - "name": "discovery_error", - "children": [ - { - "name": "file_two.py", - "type_": TestNodeTypeEnum.file, - "path": file_path, - "children": [ - { - "name": "DiscoveryErrorTwo", - "path": file_path, - "type_": TestNodeTypeEnum.class_, - "children": [ - { - "name": "test_one", - "path": file_path, - "type_": TestNodeTypeEnum.test, - "lineno": "14", - "id_": file_path - + "\\" - + "DiscoveryErrorTwo" - + "\\" - + "test_one", - }, - { - "name": "test_two", - "path": file_path, - "type_": TestNodeTypeEnum.test, - "lineno": "17", - "id_": file_path - + "\\" - + "DiscoveryErrorTwo" - + "\\" - + "test_two", - }, - ], - "id_": file_path + "\\" + "DiscoveryErrorTwo", - } - ], - "id_": file_path, - } - ], - "id_": start_dir, - } - - uuid = "some-uuid" - actual = discover_tests(start_dir, pattern, None, uuid) - - assert actual["status"] == "error" - assert is_same_tree(expected, actual.get("tests")) - assert len(actual.get("errors", [])) == 1 diff --git a/pythonFiles/tests/unittestadapter/test_execution.py b/pythonFiles/tests/unittestadapter/test_execution.py deleted file mode 100644 index 7f58049a56b7..000000000000 --- a/pythonFiles/tests/unittestadapter/test_execution.py +++ /dev/null @@ -1,283 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -import os -import pathlib -from typing import List - -import pytest -from unittestadapter.execution import parse_execution_cli_args, run_tests - -TEST_DATA_PATH = pathlib.Path(__file__).parent / ".data" - - -@pytest.mark.parametrize( - "args, expected", - [ - ( - [ - "--port", - "111", - "--uuid", - "fake-uuid", - "--testids", - "test_file.test_class.test_method", - ], - (111, "fake-uuid", ["test_file.test_class.test_method"]), - ), - ( - ["--port", "111", "--uuid", "fake-uuid", "--testids", ""], - (111, "fake-uuid", [""]), - ), - ( - [ - "--port", - "111", - "--uuid", - "fake-uuid", - "--testids", - "test_file.test_class.test_method", - "-v", - "-s", - ], - (111, "fake-uuid", ["test_file.test_class.test_method"]), - ), - ], -) -def test_parse_execution_cli_args(args: List[str], expected: List[str]) -> None: - """The parse_execution_cli_args function should return values for the port, uuid, and testids arguments - when passed as command-line options, and ignore unrecognized arguments. - """ - actual = parse_execution_cli_args(args) - assert actual == expected - - -def test_no_ids_run() -> None: - """This test runs on an empty array of test_ids, therefore it should return - an empty dict for the result. - """ - start_dir: str = os.fspath(TEST_DATA_PATH) - testids = [] - pattern = "discovery_simple*" - actual = run_tests(start_dir, testids, pattern, None, "fake-uuid") - assert actual - assert all(item in actual for item in ("cwd", "status")) - assert actual["status"] == "success" - assert actual["cwd"] == os.fspath(TEST_DATA_PATH) - if "result" in actual: - assert len(actual["result"]) == 0 - else: - raise AssertionError("actual['result'] is None") - - -def test_single_ids_run() -> None: - """This test runs on a single test_id, therefore it should return - a dict with a single key-value pair for the result. - - This single test passes so the outcome should be 'success'. - """ - id = "discovery_simple.DiscoverySimple.test_one" - actual = run_tests( - os.fspath(TEST_DATA_PATH), [id], "discovery_simple*", None, "fake-uuid" - ) - assert actual - assert all(item in actual for item in ("cwd", "status")) - assert actual["status"] == "success" - assert actual["cwd"] == os.fspath(TEST_DATA_PATH) - assert "result" in actual - result = actual["result"] - assert len(result) == 1 - assert id in result - id_result = result[id] - assert id_result is not None - assert "outcome" in id_result - assert id_result["outcome"] == "success" - - -def test_subtest_run() -> None: - """This test runs on a the test_subtest which has a single method, test_even, - that uses unittest subtest. - - The actual result of run should return a dict payload with 6 entry for the 6 subtests. - """ - id = "test_subtest.NumbersTest.test_even" - actual = run_tests( - os.fspath(TEST_DATA_PATH), [id], "test_subtest.py", None, "fake-uuid" - ) - subtests_ids = [ - "test_subtest.NumbersTest.test_even (i=0)", - "test_subtest.NumbersTest.test_even (i=1)", - "test_subtest.NumbersTest.test_even (i=2)", - "test_subtest.NumbersTest.test_even (i=3)", - "test_subtest.NumbersTest.test_even (i=4)", - "test_subtest.NumbersTest.test_even (i=5)", - ] - assert actual - assert all(item in actual for item in ("cwd", "status")) - assert actual["status"] == "success" - assert actual["cwd"] == os.fspath(TEST_DATA_PATH) - assert "result" in actual - result = actual["result"] - assert len(result) == 6 - for id in subtests_ids: - assert id in result - - -@pytest.mark.parametrize( - "test_ids, pattern, cwd, expected_outcome", - [ - ( - [ - "test_add.TestAddFunction.test_add_negative_numbers", - "test_add.TestAddFunction.test_add_positive_numbers", - ], - "test_add.py", - os.fspath(TEST_DATA_PATH / "unittest_folder"), - "success", - ), - ( - [ - "test_add.TestAddFunction.test_add_negative_numbers", - "test_add.TestAddFunction.test_add_positive_numbers", - "test_subtract.TestSubtractFunction.test_subtract_negative_numbers", - "test_subtract.TestSubtractFunction.test_subtract_positive_numbers", - ], - "test*", - os.fspath(TEST_DATA_PATH / "unittest_folder"), - "success", - ), - ( - [ - "pattern_a_test.DiscoveryA.test_one_a", - "pattern_a_test.DiscoveryA.test_two_a", - ], - "*test", - os.fspath(TEST_DATA_PATH / "two_patterns"), - "success", - ), - ( - [ - "test_pattern_b.DiscoveryB.test_one_b", - "test_pattern_b.DiscoveryB.test_two_b", - ], - "test_*", - os.fspath(TEST_DATA_PATH / "two_patterns"), - "success", - ), - ( - [ - "file_one.CaseTwoFileOne.test_one", - "file_one.CaseTwoFileOne.test_two", - "folder.file_two.CaseTwoFileTwo.test_one", - "folder.file_two.CaseTwoFileTwo.test_two", - ], - "*", - os.fspath(TEST_DATA_PATH / "utils_nested_cases"), - "success", - ), - ( - [ - "test_two_classes.ClassOne.test_one", - "test_two_classes.ClassTwo.test_two", - ], - "test_two_classes.py", - os.fspath(TEST_DATA_PATH), - "success", - ), - ], -) -def test_multiple_ids_run(test_ids, pattern, cwd, expected_outcome) -> None: - """ - The following are all successful tests of different formats. - - # 1. Two tests with the `pattern` specified as a file - # 2. Two test files in the same folder called `unittest_folder` - # 3. A folder with two different test file patterns, this test gathers pattern `*test` - # 4. A folder with two different test file patterns, this test gathers pattern `test_*` - # 5. A nested structure where a test file is on the same level as a folder containing a test file - # 6. Test file with two test classes - - All tests should have the outcome of `success`. - """ - actual = run_tests(cwd, test_ids, pattern, None, "fake-uuid") - assert actual - assert all(item in actual for item in ("cwd", "status")) - assert actual["status"] == "success" - assert actual["cwd"] == cwd - assert "result" in actual - result = actual["result"] - assert len(result) == len(test_ids) - for test_id in test_ids: - assert test_id in result - id_result = result[test_id] - assert id_result is not None - assert "outcome" in id_result - assert id_result["outcome"] == expected_outcome - assert True - - -def test_failed_tests(): - """This test runs on a single file `test_fail` with two tests that fail.""" - test_ids = [ - "test_fail_simple.RunFailSimple.test_one_fail", - "test_fail_simple.RunFailSimple.test_two_fail", - ] - actual = run_tests( - os.fspath(TEST_DATA_PATH), test_ids, "test_fail_simple*", None, "fake-uuid" - ) - assert actual - assert all(item in actual for item in ("cwd", "status")) - assert actual["status"] == "success" - assert actual["cwd"] == os.fspath(TEST_DATA_PATH) - assert "result" in actual - result = actual["result"] - assert len(result) == len(test_ids) - for test_id in test_ids: - assert test_id in result - id_result = result[test_id] - assert id_result is not None - assert "outcome" in id_result - assert id_result["outcome"] == "failure" - assert "message" and "traceback" in id_result - assert True - - -def test_unknown_id(): - """This test runs on a unknown test_id, therefore it should return - an error as the outcome as it attempts to find the given test. - """ - test_ids = ["unknown_id"] - actual = run_tests( - os.fspath(TEST_DATA_PATH), test_ids, "test_fail_simple*", None, "fake-uuid" - ) - assert actual - assert all(item in actual for item in ("cwd", "status")) - assert actual["status"] == "success" - assert actual["cwd"] == os.fspath(TEST_DATA_PATH) - assert "result" in actual - result = actual["result"] - assert len(result) == len(test_ids) - assert "unittest.loader._FailedTest.unknown_id" in result - id_result = result["unittest.loader._FailedTest.unknown_id"] - assert id_result is not None - assert "outcome" in id_result - assert id_result["outcome"] == "error" - assert "message" and "traceback" in id_result - - -def test_incorrect_path(): - """This test runs on a non existent path, therefore it should return - an error as the outcome as it attempts to find the given folder. - """ - test_ids = ["unknown_id"] - actual = run_tests( - os.fspath(TEST_DATA_PATH / "unknown_folder"), - test_ids, - "test_fail_simple*", - None, - "fake-uuid", - ) - assert actual - assert all(item in actual for item in ("cwd", "status", "error")) - assert actual["status"] == "error" - assert actual["cwd"] == os.fspath(TEST_DATA_PATH / "unknown_folder") diff --git a/pythonFiles/unittestadapter/discovery.py b/pythonFiles/unittestadapter/discovery.py deleted file mode 100644 index dc0a139ed5a2..000000000000 --- a/pythonFiles/unittestadapter/discovery.py +++ /dev/null @@ -1,136 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -import argparse -import json -import os -import pathlib -import sys -import traceback -import unittest -from typing import List, Literal, Optional, Tuple, Union - -# Add the path to pythonFiles to sys.path to find testing_tools.socket_manager. -PYTHON_FILES = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) -sys.path.insert(0, PYTHON_FILES) - -from testing_tools import socket_manager - -# If I use from utils then there will be an import error in test_discovery.py. -from unittestadapter.utils import TestNode, build_test_tree, parse_unittest_args - -# Add the lib path to sys.path to find the typing_extensions module. -sys.path.insert(0, os.path.join(PYTHON_FILES, "lib", "python")) - -from typing_extensions import NotRequired, TypedDict - -DEFAULT_PORT = "45454" - - -def parse_discovery_cli_args(args: List[str]) -> Tuple[int, Union[str, None]]: - """Parse command-line arguments that should be processed by the script. - - So far this includes the port number that it needs to connect to, and the uuid passed by the TS side. - The port is passed to the discovery.py script when it is executed, and - defaults to DEFAULT_PORT if it can't be parsed. - The uuid should be passed to the discovery.py script when it is executed, and defaults to None if it can't be parsed. - If the arguments appear several times, the value returned by parse_cli_args will be the value of the last argument. - """ - arg_parser = argparse.ArgumentParser() - arg_parser.add_argument("--port", default=DEFAULT_PORT) - arg_parser.add_argument("--uuid") - parsed_args, _ = arg_parser.parse_known_args(args) - - return int(parsed_args.port), parsed_args.uuid - - -class PayloadDict(TypedDict): - cwd: str - status: Literal["success", "error"] - tests: NotRequired[TestNode] - errors: NotRequired[List[str]] - - -def discover_tests( - start_dir: str, pattern: str, top_level_dir: Optional[str], uuid: Optional[str] -) -> PayloadDict: - """Returns a dictionary containing details of the discovered tests. - - The returned dict has the following keys: - - - cwd: Absolute path to the test start directory; - - uuid: UUID sent by the caller of the Python script, that needs to be sent back as an integrity check; - - status: Test discovery status, can be "success" or "error"; - - tests: Discoverered tests if any, not present otherwise. Note that the status can be "error" but the payload can still contain tests; - - errors: Discovery errors if any, not present otherwise. - - Payload format for a successful discovery: - { - "status": "success", - "cwd": , - "tests": - } - - Payload format for a successful discovery with no tests: - { - "status": "success", - "cwd": , - } - - Payload format when there are errors: - { - "cwd": - "errors": [list of errors] - "status": "error", - } - """ - cwd = os.path.abspath(start_dir) - payload: PayloadDict = {"cwd": cwd, "status": "success"} - tests = None - errors: List[str] = [] - - try: - loader = unittest.TestLoader() - suite = loader.discover(start_dir, pattern, top_level_dir) - - tests, errors = build_test_tree(suite, cwd) # test tree built succesfully here. - - except Exception: - errors.append(traceback.format_exc()) - - if tests is not None: - payload["tests"] = tests - - if len(errors): - payload["status"] = "error" - payload["errors"] = errors - - return payload - - -if __name__ == "__main__": - # Get unittest discovery arguments. - argv = sys.argv[1:] - index = argv.index("--udiscovery") - - start_dir, pattern, top_level_dir = parse_unittest_args(argv[index + 1 :]) - - # Perform test discovery. - port, uuid = parse_discovery_cli_args(argv[:index]) - payload = discover_tests(start_dir, pattern, top_level_dir, uuid) - - # Build the request data (it has to be a POST request or the Node side will not process it), and send it. - addr = ("localhost", port) - data = json.dumps(payload) - request = f"""Content-Length: {len(data)} -Content-Type: application/json -Request-uuid: {uuid} - -{data}""" - try: - with socket_manager.SocketManager(addr) as s: - if s.socket is not None: - s.socket.sendall(request.encode("utf-8")) - except Exception as e: - print(f"Error sending response: {e}") - print(f"Request data: {request}") diff --git a/pythonFiles/unittestadapter/execution.py b/pythonFiles/unittestadapter/execution.py deleted file mode 100644 index 37288651f531..000000000000 --- a/pythonFiles/unittestadapter/execution.py +++ /dev/null @@ -1,247 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -import argparse -import enum -import json -import os -import sys -import traceback -import unittest -from types import TracebackType -from typing import Dict, List, Optional, Tuple, Type, Union - -# Add the path to pythonFiles to sys.path to find testing_tools.socket_manager. -PYTHON_FILES = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) -sys.path.insert(0, PYTHON_FILES) -# Add the lib path to sys.path to find the typing_extensions module. -sys.path.insert(0, os.path.join(PYTHON_FILES, "lib", "python")) -from testing_tools import socket_manager -from typing_extensions import NotRequired, TypeAlias, TypedDict -from unittestadapter.utils import parse_unittest_args - -DEFAULT_PORT = "45454" - - -def parse_execution_cli_args( - args: List[str], -) -> Tuple[int, Union[str, None], List[str]]: - """Parse command-line arguments that should be processed by the script. - - So far this includes the port number that it needs to connect to, the uuid passed by the TS side, - and the list of test ids to report. - The port is passed to the execution.py script when it is executed, and - defaults to DEFAULT_PORT if it can't be parsed. - The list of test ids is passed to the execution.py script when it is executed, and defaults to an empty list if it can't be parsed. - The uuid should be passed to the execution.py script when it is executed, and defaults to None if it can't be parsed. - If the arguments appear several times, the value returned by parse_cli_args will be the value of the last argument. - """ - arg_parser = argparse.ArgumentParser() - arg_parser.add_argument("--port", default=DEFAULT_PORT) - arg_parser.add_argument("--uuid") - arg_parser.add_argument("--testids", nargs="+") - parsed_args, _ = arg_parser.parse_known_args(args) - - return (int(parsed_args.port), parsed_args.uuid, parsed_args.testids) - - -ErrorType = Union[ - Tuple[Type[BaseException], BaseException, TracebackType], Tuple[None, None, None] -] - - -class TestOutcomeEnum(str, enum.Enum): - error = "error" - failure = "failure" - success = "success" - skipped = "skipped" - expected_failure = "expected-failure" - unexpected_success = "unexpected-success" - subtest_success = "subtest-success" - subtest_failure = "subtest-failure" - - -class UnittestTestResult(unittest.TextTestResult): - def __init__(self, *args, **kwargs): - self.formatted: Dict[str, Dict[str, Union[str, None]]] = dict() - super(UnittestTestResult, self).__init__(*args, **kwargs) - - def startTest(self, test: unittest.TestCase): - super(UnittestTestResult, self).startTest(test) - - def addError( - self, - test: unittest.TestCase, - err: ErrorType, - ): - super(UnittestTestResult, self).addError(test, err) - self.formatResult(test, TestOutcomeEnum.error, err) - - def addFailure( - self, - test: unittest.TestCase, - err: ErrorType, - ): - super(UnittestTestResult, self).addFailure(test, err) - self.formatResult(test, TestOutcomeEnum.failure, err) - - def addSuccess(self, test: unittest.TestCase): - super(UnittestTestResult, self).addSuccess(test) - self.formatResult(test, TestOutcomeEnum.success) - - def addSkip(self, test: unittest.TestCase, reason: str): - super(UnittestTestResult, self).addSkip(test, reason) - self.formatResult(test, TestOutcomeEnum.skipped) - - def addExpectedFailure(self, test: unittest.TestCase, err: ErrorType): - super(UnittestTestResult, self).addExpectedFailure(test, err) - self.formatResult(test, TestOutcomeEnum.expected_failure, err) - - def addUnexpectedSuccess(self, test: unittest.TestCase): - super(UnittestTestResult, self).addUnexpectedSuccess(test) - self.formatResult(test, TestOutcomeEnum.unexpected_success) - - def addSubTest( - self, - test: unittest.TestCase, - subtest: unittest.TestCase, - err: Union[ErrorType, None], - ): - super(UnittestTestResult, self).addSubTest(test, subtest, err) - self.formatResult( - test, - TestOutcomeEnum.subtest_failure if err else TestOutcomeEnum.subtest_success, - err, - subtest, - ) - - def formatResult( - self, - test: unittest.TestCase, - outcome: str, - error: Union[ErrorType, None] = None, - subtest: Union[unittest.TestCase, None] = None, - ): - tb = None - if error and error[2] is not None: - # Format traceback - formatted = traceback.format_exception(*error) - # Remove the 'Traceback (most recent call last)' - formatted = formatted[1:] - tb = "".join(formatted) - - if subtest: - test_id = subtest.id() - else: - test_id = test.id() - - result = { - "test": test.id(), - "outcome": outcome, - "message": str(error), - "traceback": tb, - "subtest": subtest.id() if subtest else None, - } - - self.formatted[test_id] = result - - -class TestExecutionStatus(str, enum.Enum): - error = "error" - success = "success" - - -TestResultTypeAlias: TypeAlias = Dict[str, Dict[str, Union[str, None]]] - - -class PayloadDict(TypedDict): - cwd: str - status: TestExecutionStatus - result: NotRequired[TestResultTypeAlias] - not_found: NotRequired[List[str]] - error: NotRequired[str] - - -# Args: start_path path to a directory or a file, list of ids that may be empty. -# Edge cases: -# - if tests got deleted since the VS Code side last ran discovery and the current test run, -# return these test ids in the "not_found" entry, and the VS Code side can process them as "unknown"; -# - if tests got added since the VS Code side last ran discovery and the current test run, ignore them. -def run_tests( - start_dir: str, - test_ids: List[str], - pattern: str, - top_level_dir: Optional[str], - uuid: Optional[str], -) -> PayloadDict: - cwd = os.path.abspath(start_dir) - status = TestExecutionStatus.error - error = None - payload: PayloadDict = {"cwd": cwd, "status": status} - - try: - # If it's a file, split path and file name. - start_dir = cwd - if cwd.endswith(".py"): - start_dir = os.path.dirname(cwd) - pattern = os.path.basename(cwd) - - # Discover tests at path with the file name as a pattern (if any). - loader = unittest.TestLoader() - - args = { - "start_dir": start_dir, - "pattern": pattern, - "top_level_dir": top_level_dir, - } - suite = loader.discover(start_dir, pattern, top_level_dir) - - # Run tests. - runner = unittest.TextTestRunner(resultclass=UnittestTestResult) - # lets try to tailer our own suite so we can figure out running only the ones we want - loader = unittest.TestLoader() - tailor: unittest.TestSuite = loader.loadTestsFromNames(test_ids) - result: UnittestTestResult = runner.run(tailor) # type: ignore - - payload["result"] = result.formatted - - except Exception: - status = TestExecutionStatus.error - error = traceback.format_exc() - - if error is not None: - payload["error"] = error - else: - status = TestExecutionStatus.success - - payload["status"] = status - - return payload - - -if __name__ == "__main__": - # Get unittest test execution arguments. - argv = sys.argv[1:] - index = argv.index("--udiscovery") - - start_dir, pattern, top_level_dir = parse_unittest_args(argv[index + 1 :]) - - # Perform test execution. - port, uuid, testids = parse_execution_cli_args(argv[:index]) - payload = run_tests(start_dir, testids, pattern, top_level_dir, uuid) - - # Build the request data (it has to be a POST request or the Node side will not process it), and send it. - addr = ("localhost", port) - data = json.dumps(payload) - request = f"""Content-Length: {len(data)} -Content-Type: application/json -Request-uuid: {uuid} - -{data}""" - try: - with socket_manager.SocketManager(addr) as s: - if s.socket is not None: - s.socket.sendall(request.encode("utf-8")) - except Exception as e: - print(f"Error sending response: {e}") - print(f"Request data: {request}") diff --git a/pythonFiles/unittestadapter/utils.py b/pythonFiles/unittestadapter/utils.py deleted file mode 100644 index 568ff30ee92d..000000000000 --- a/pythonFiles/unittestadapter/utils.py +++ /dev/null @@ -1,228 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -import argparse -import enum -import inspect -import os -import pathlib -import unittest -from typing import List, Tuple, TypedDict, Union - -# Types - - -# Inherit from str so it's JSON serializable. -class TestNodeTypeEnum(str, enum.Enum): - class_ = "class" - file = "file" - folder = "folder" - test = "test" - - -class TestData(TypedDict): - name: str - path: str - type_: TestNodeTypeEnum - id_: str - - -class TestItem(TestData): - lineno: str - runID: str - - -class TestNode(TestData): - children: "List[TestNode | TestItem]" - - -# Helper functions for data retrieval. - - -def get_test_case(suite): - """Iterate through a unittest test suite and return all test cases.""" - for test in suite: - if isinstance(test, unittest.TestCase): - yield test - else: - for test_case in get_test_case(test): - yield test_case - - -def get_source_line(obj) -> str: - """Get the line number of a test case start line.""" - try: - sourcelines, lineno = inspect.getsourcelines(obj) - except: - try: - # tornado-specific, see https://github.com/microsoft/vscode-python/issues/17285. - sourcelines, lineno = inspect.getsourcelines(obj.orig_method) - except: - return "*" - - # Return the line number of the first line of the test case definition. - for i, v in enumerate(sourcelines): - if v.strip().startswith(("def", "async def")): - return str(lineno + i) - - return "*" - - -# Helper functions for test tree building. - - -def build_test_node(path: str, name: str, type_: TestNodeTypeEnum) -> TestNode: - """Build a test node with no children. A test node can be a folder, a file or a class.""" - ## figure out if we are folder, file, or class - id_gen = path - if type_ == TestNodeTypeEnum.folder or type_ == TestNodeTypeEnum.file: - id_gen = path - else: - # means we have to build test node for class - id_gen = path + "\\" + name - - return {"path": path, "name": name, "type_": type_, "children": [], "id_": id_gen} - - -def get_child_node( - name: str, path: str, type_: TestNodeTypeEnum, root: TestNode -) -> TestNode: - """Find a child node in a test tree given its name and type. If the node doesn't exist, create it.""" - try: - result = next( - node - for node in root["children"] - if node["name"] == name and node["type_"] == type_ - ) - except StopIteration: - result = build_test_node(path, name, type_) - root["children"].append(result) - - return result # type:ignore - - -def build_test_tree( - suite: unittest.TestSuite, test_directory: str -) -> Tuple[Union[TestNode, None], List[str]]: - """Build a test tree from a unittest test suite. - - This function returns the test tree, and any errors found by unittest. - If no tests were discovered, return `None` and a list of errors (if any). - - Test tree structure: - { - "path": , - "type": "folder", - "name": , - "children": [ - { files and folders } - ... - { - "path": , - "name": filename.py, - "type_": "file", - "children": [ - { - "path": , - "name": , - "type_": "class", - "children": [ - { - "path": , - "name": , - "type_": "test", - "lineno": - "id_": , - } - ], - "id_": - } - ], - "id_": - } - ], - "id_": - } - """ - errors = [] - directory_path = pathlib.PurePath(test_directory) - root = build_test_node(test_directory, directory_path.name, TestNodeTypeEnum.folder) - - for test_case in get_test_case(suite): - test_id = test_case.id() - if test_id.startswith("unittest.loader._FailedTest"): - errors.append(str(test_case._exception)) # type: ignore - else: - # Get the static test path components: filename, class name and function name. - components = test_id.split(".") - *folders, filename, class_name, function_name = components - py_filename = f"{filename}.py" - - current_node = root - - # Find/build nodes for the intermediate folders in the test path. - for folder in folders: - current_node = get_child_node( - folder, - os.fsdecode(pathlib.PurePath(current_node["path"], folder)), - TestNodeTypeEnum.folder, - current_node, - ) - - # Find/build file node. - path_components = [test_directory] + folders + [py_filename] - file_path = os.fsdecode(pathlib.PurePath("/".join(path_components))) - current_node = get_child_node( - py_filename, file_path, TestNodeTypeEnum.file, current_node - ) - - # Find/build class node. - current_node = get_child_node( - class_name, file_path, TestNodeTypeEnum.class_, current_node - ) - - # Get test line number. - test_method = getattr(test_case, test_case._testMethodName) - lineno = get_source_line(test_method) - - # Add test node. - test_node: TestItem = { - "name": function_name, - "path": file_path, - "lineno": lineno, - "type_": TestNodeTypeEnum.test, - "id_": file_path + "\\" + class_name + "\\" + function_name, - "runID": test_id, - } # concatenate class name and function test name - current_node["children"].append(test_node) - - if not root["children"]: - root = None - - return root, errors - - -def parse_unittest_args(args: List[str]) -> Tuple[str, str, Union[str, None]]: - """Parse command-line arguments that should be forwarded to unittest to perform discovery. - - Valid unittest arguments are: -v, -s, -p, -t and their long-form counterparts, - however we only care about the last three. - - The returned tuple contains the following items - - start_directory: The directory where to start discovery, defaults to . - - pattern: The pattern to match test files, defaults to test*.py - - top_level_directory: The top-level directory of the project, defaults to None, and unittest will use start_directory behind the scenes. - """ - - arg_parser = argparse.ArgumentParser() - arg_parser.add_argument("--start-directory", "-s", default=".") - arg_parser.add_argument("--pattern", "-p", default="test*.py") - arg_parser.add_argument("--top-level-directory", "-t", default=None) - - parsed_args, _ = arg_parser.parse_known_args(args) - - return ( - parsed_args.start_directory, - parsed_args.pattern, - parsed_args.top_level_directory, - ) diff --git a/pythonFiles/vscode_datascience_helpers/tests/logParser.py b/pythonFiles/vscode_datascience_helpers/tests/logParser.py deleted file mode 100644 index 767f837c5136..000000000000 --- a/pythonFiles/vscode_datascience_helpers/tests/logParser.py +++ /dev/null @@ -1,96 +0,0 @@ -from io import TextIOWrapper -import sys -import argparse -import os - -os.system("color") -from pathlib import Path -import re - -parser = argparse.ArgumentParser(description="Parse a test log into its parts") -parser.add_argument("testlog", type=str, nargs=1, help="Log to parse") -parser.add_argument( - "--testoutput", action="store_true", help="Show all failures and passes" -) -parser.add_argument( - "--split", - action="store_true", - help="Split into per process files. Each file will have the pid appended", -) -ansi_escape = re.compile(r"\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])") -pid_regex = re.compile(r"(\d+).*") -timestamp_regex = re.compile(r"\d{4}-\d{2}-\d{2}T.*\dZ") - - -def stripTimestamp(line: str): - match = timestamp_regex.match(line) - if match: - return line[match.end() :] - return line - - -def readStripLines(f: TextIOWrapper): - return map(stripTimestamp, f.readlines()) - - -def printTestOutput(testlog): - # Find all the lines that don't have a PID in them. These are the test output - p = Path(testlog[0]) - with p.open() as f: - for line in readStripLines(f): - stripped = line.strip() - if len(stripped) > 2 and stripped[0] == "\x1B" and stripped[1] == "[": - print(line.rstrip()) # Should be a test line as it has color encoding - - -def splitByPid(testlog): - # Split testlog into prefixed logs based on pid - baseFile = os.path.splitext(testlog[0])[0] - p = Path(testlog[0]) - pids = set() - logs = {} - pid = None - with p.open() as f: - for line in readStripLines(f): - stripped = ansi_escape.sub("", line.strip()) - if len(stripped) > 0: - # Pull out the pid - match = pid_regex.match(stripped) - - # Pids are at least two digits - if match and len(match.group(1)) > 2: - # Pid is found - pid = int(match.group(1)) - - # See if we've created a log for this pid or not - if not pid in pids: - pids.add(pid) - logFile = "{}_{}.log".format(baseFile, pid) - print("Writing to new log: " + logFile) - logs[pid] = Path(logFile).open(mode="w") - - # Add this line to the log - if pid != None: - logs[pid].write(line) - # Close all of the open logs - for key in logs: - logs[key].close() - - -def doWork(args): - if not args.testlog: - print("Test log should be passed") - elif args.testoutput: - printTestOutput(args.testlog) - elif args.split: - splitByPid(args.testlog) - else: - parser.print_usage() - - -def main(): - doWork(parser.parse_args()) - - -if __name__ == "__main__": - main() diff --git a/pythonFiles/vscode_pytest/__init__.py b/pythonFiles/vscode_pytest/__init__.py deleted file mode 100644 index 6063e4113d55..000000000000 --- a/pythonFiles/vscode_pytest/__init__.py +++ /dev/null @@ -1,492 +0,0 @@ -import json -import os -import pathlib -import sys -import traceback - -import pytest - -script_dir = pathlib.Path(__file__).parent.parent -sys.path.append(os.fspath(script_dir)) -sys.path.append(os.fspath(script_dir / "lib" / "python")) - -from typing import Any, Dict, List, Optional, Union - -from testing_tools import socket_manager -from typing_extensions import Literal, TypedDict - - -class TestData(TypedDict): - """A general class that all test objects inherit from.""" - - name: str - path: str - type_: Literal["class", "file", "folder", "test", "error"] - id_: str - - -class TestItem(TestData): - """A class defining test items.""" - - lineno: str - runID: str - - -class TestNode(TestData): - """A general class that handles all test data which contains children.""" - - children: "list[Union[TestNode, TestItem, None]]" - - -class VSCodePytestError(Exception): - """A custom exception class for pytest errors.""" - - def __init__(self, message): - super().__init__(message) - - -ERRORS = [] - - -def pytest_internalerror(excrepr, excinfo): - """A pytest hook that is called when an internal error occurs. - - Keyword arguments: - excrepr -- the exception representation. - excinfo -- the exception information of type ExceptionInfo. - """ - # call.excinfo.exconly() returns the exception as a string. - ERRORS.append(excinfo.exconly()) - - -def pytest_exception_interact(node, call, report): - """A pytest hook that is called when an exception is raised which could be handled. - - Keyword arguments: - node -- the node that raised the exception. - call -- the call object. - report -- the report object of either type CollectReport or TestReport. - """ - # call.excinfo is the captured exception of the call, if it raised as type ExceptionInfo. - # call.excinfo.exconly() returns the exception as a string. - if call.excinfo and call.excinfo.typename != "AssertionError": - ERRORS.append(call.excinfo.exconly()) - - -def pytest_keyboard_interrupt(excinfo): - """A pytest hook that is called when a keyboard interrupt is raised. - - Keyword arguments: - excinfo -- the exception information of type ExceptionInfo. - """ - # The function execonly() returns the exception as a string. - ERRORS.append(excinfo.exconly()) - - -class TestOutcome(Dict): - """A class that handles outcome for a single test. - - for pytest the outcome for a test is only 'passed', 'skipped' or 'failed' - """ - - test: str - outcome: Literal["success", "failure", "skipped"] - message: Union[str, None] - traceback: Union[str, None] - subtest: Optional[str] - - -def create_test_outcome( - test: str, - outcome: str, - message: Union[str, None], - traceback: Union[str, None], - subtype: Optional[str] = None, -) -> TestOutcome: - """A function that creates a TestOutcome object.""" - return TestOutcome( - test=test, - outcome=outcome, - message=message, - traceback=traceback, # TODO: traceback - subtest=None, - ) - - -class testRunResultDict(Dict[str, Dict[str, TestOutcome]]): - """A class that stores all test run results.""" - - outcome: str - tests: Dict[str, TestOutcome] - - -collected_tests = testRunResultDict() -IS_DISCOVERY = False - - -def pytest_load_initial_conftests(early_config, parser, args): - if "--collect-only" in args: - global IS_DISCOVERY - IS_DISCOVERY = True - - -def pytest_report_teststatus(report, config): - """ - A pytest hook that is called when a test is called. It is called 3 times per test, - during setup, call, and teardown. - Keyword arguments: - report -- the report on the test setup, call, and teardown. - config -- configuration object. - """ - - if report.when == "call": - traceback = None - message = None - report_value = "skipped" - if report.passed: - report_value = "success" - elif report.failed: - report_value = "failure" - message = report.longreprtext - item_result = create_test_outcome( - report.nodeid, - report_value, - message, - traceback, - ) - collected_tests[report.nodeid] = item_result - - -ERROR_MESSAGE_CONST = { - 2: "Pytest was unable to start or run any tests due to issues with test discovery or test collection.", - 3: "Pytest was interrupted by the user, for example by pressing Ctrl+C during test execution.", - 4: "Pytest encountered an internal error or exception during test execution.", - 5: "Pytest was unable to find any tests to run.", -} - - -def pytest_sessionfinish(session, exitstatus): - """A pytest hook that is called after pytest has fulled finished. - - Keyword arguments: - session -- the pytest session object. - exitstatus -- the status code of the session. - - 0: All tests passed successfully. - 1: One or more tests failed. - 2: Pytest was unable to start or run any tests due to issues with test discovery or test collection. - 3: Pytest was interrupted by the user, for example by pressing Ctrl+C during test execution. - 4: Pytest encountered an internal error or exception during test execution. - 5: Pytest was unable to find any tests to run. - """ - cwd = pathlib.Path.cwd() - if IS_DISCOVERY: - try: - session_node: Union[TestNode, None] = build_test_tree(session) - if not session_node: - raise VSCodePytestError( - "Something went wrong following pytest finish, \ - no session node was created" - ) - post_response(os.fsdecode(cwd), session_node) - except Exception as e: - ERRORS.append( - f"Error Occurred, traceback: {(traceback.format_exc() if e.__traceback__ else '')}" - ) - errorNode: TestNode = { - "name": "", - "path": "", - "type_": "error", - "children": [], - "id_": "", - } - post_response(os.fsdecode(cwd), errorNode) - else: - if exitstatus == 0 or exitstatus == 1: - exitstatus_bool = "success" - else: - ERRORS.append( - f"Pytest exited with error status: {exitstatus}, {ERROR_MESSAGE_CONST[exitstatus]}" - ) - exitstatus_bool = "error" - - execution_post( - os.fsdecode(cwd), - exitstatus_bool, - collected_tests if collected_tests else None, - ) - - -def build_test_tree(session: pytest.Session) -> TestNode: - """Builds a tree made up of testing nodes from the pytest session. - - Keyword arguments: - session -- the pytest session object. - """ - session_node = create_session_node(session) - session_children_dict: Dict[str, TestNode] = {} - file_nodes_dict: Dict[Any, TestNode] = {} - class_nodes_dict: Dict[str, TestNode] = {} - - for test_case in session.items: - test_node = create_test_node(test_case) - if isinstance(test_case.parent, pytest.Class): - try: - test_class_node = class_nodes_dict[test_case.parent.name] - except KeyError: - test_class_node = create_class_node(test_case.parent) - class_nodes_dict[test_case.parent.name] = test_class_node - test_class_node["children"].append(test_node) - if test_case.parent.parent: - parent_module = test_case.parent.parent - else: - ERRORS.append(f"Test class {test_case.parent} has no parent") - break - # Create a file node that has the class as a child. - try: - test_file_node: TestNode = file_nodes_dict[parent_module] - except KeyError: - test_file_node = create_file_node(parent_module) - file_nodes_dict[parent_module] = test_file_node - # Check if the class is already a child of the file node. - if test_class_node not in test_file_node["children"]: - test_file_node["children"].append(test_class_node) - else: # This includes test cases that are pytest functions or a doctests. - try: - parent_test_case = file_nodes_dict[test_case.parent] - except KeyError: - parent_test_case = create_file_node(test_case.parent) - file_nodes_dict[test_case.parent] = parent_test_case - parent_test_case["children"].append(test_node) - created_files_folders_dict: Dict[str, TestNode] = {} - for file_module, file_node in file_nodes_dict.items(): - # Iterate through all the files that exist and construct them into nested folders. - root_folder_node: TestNode = build_nested_folders( - file_module, file_node, created_files_folders_dict, session - ) - # The final folder we get to is the highest folder in the path - # and therefore we add this as a child to the session. - root_id = root_folder_node.get("id_") - if root_id and root_id not in session_children_dict: - session_children_dict[root_id] = root_folder_node - session_node["children"] = list(session_children_dict.values()) - return session_node - - -def build_nested_folders( - file_module: Any, - file_node: TestNode, - created_files_folders_dict: Dict[str, TestNode], - session: pytest.Session, -) -> TestNode: - """Takes a file or folder and builds the nested folder structure for it. - - Keyword arguments: - file_module -- the created module for the file we are nesting. - file_node -- the file node that we are building the nested folders for. - created_files_folders_dict -- Dictionary of all the folders and files that have been created. - session -- the pytest session object. - """ - prev_folder_node = file_node - - # Begin the iterator_path one level above the current file. - iterator_path = file_module.path.parent - while iterator_path != session.path: - curr_folder_name = iterator_path.name - try: - curr_folder_node: TestNode = created_files_folders_dict[curr_folder_name] - except KeyError: - curr_folder_node: TestNode = create_folder_node( - curr_folder_name, iterator_path - ) - created_files_folders_dict[curr_folder_name] = curr_folder_node - if prev_folder_node not in curr_folder_node["children"]: - curr_folder_node["children"].append(prev_folder_node) - iterator_path = iterator_path.parent - prev_folder_node = curr_folder_node - return prev_folder_node - - -def create_test_node( - test_case: pytest.Item, -) -> TestItem: - """Creates a test node from a pytest test case. - - Keyword arguments: - test_case -- the pytest test case. - """ - test_case_loc: str = ( - str(test_case.location[1] + 1) if (test_case.location[1] is not None) else "" - ) - return { - "name": test_case.name, - "path": os.fspath(test_case.path), - "lineno": test_case_loc, - "type_": "test", - "id_": test_case.nodeid, - "runID": test_case.nodeid, - } - - -def create_session_node(session: pytest.Session) -> TestNode: - """Creates a session node from a pytest session. - - Keyword arguments: - session -- the pytest session. - """ - return { - "name": session.name, - "path": os.fspath(session.path), - "type_": "folder", - "children": [], - "id_": os.fspath(session.path), - } - - -def create_class_node(class_module: pytest.Class) -> TestNode: - """Creates a class node from a pytest class object. - - Keyword arguments: - class_module -- the pytest object representing a class module. - """ - return { - "name": class_module.name, - "path": os.fspath(class_module.path), - "type_": "class", - "children": [], - "id_": class_module.nodeid, - } - - -def create_file_node(file_module: Any) -> TestNode: - """Creates a file node from a pytest file module. - - Keyword arguments: - file_module -- the pytest file module. - """ - return { - "name": file_module.path.name, - "path": os.fspath(file_module.path), - "type_": "file", - "id_": os.fspath(file_module.path), - "children": [], - } - - -def create_folder_node(folderName: str, path_iterator: pathlib.Path) -> TestNode: - """Creates a folder node from a pytest folder name and its path. - - Keyword arguments: - folderName -- the name of the folder. - path_iterator -- the path of the folder. - """ - return { - "name": folderName, - "path": os.fspath(path_iterator), - "type_": "folder", - "id_": os.fspath(path_iterator), - "children": [], - } - - -class DiscoveryPayloadDict(TypedDict): - """A dictionary that is used to send a post request to the server.""" - - cwd: str - status: Literal["success", "error"] - tests: Optional[TestNode] - error: Optional[List[str]] - - -class ExecutionPayloadDict(Dict): - """ - A dictionary that is used to send a execution post request to the server. - """ - - cwd: str - status: Literal["success", "error"] - result: Union[testRunResultDict, None] - not_found: Union[List[str], None] # Currently unused need to check - error: Union[str, None] # Currently unused need to check - - -def execution_post( - cwd: str, - status: Literal["success", "error"], - tests: Union[testRunResultDict, None], -): - """ - Sends a post request to the server after the tests have been executed. - Keyword arguments: - cwd -- the current working directory. - session_node -- the status of running the tests - tests -- the tests that were run and their status. - """ - testPort = os.getenv("TEST_PORT", 45454) - testuuid = os.getenv("TEST_UUID") - payload: ExecutionPayloadDict = ExecutionPayloadDict( - cwd=cwd, status=status, result=tests, not_found=None, error=None - ) - if ERRORS: - payload["error"] = ERRORS - - addr = ("localhost", int(testPort)) - data = json.dumps(payload) - request = f"""Content-Length: {len(data)} -Content-Type: application/json -Request-uuid: {testuuid} - -{data}""" - test_output_file: Optional[str] = os.getenv("TEST_OUTPUT_FILE", None) - if test_output_file == "stdout": - print(request) - elif test_output_file: - pathlib.Path(test_output_file).write_text(request, encoding="utf-8") - else: - try: - with socket_manager.SocketManager(addr) as s: - if s.socket is not None: - s.socket.sendall(request.encode("utf-8")) - except Exception as e: - print(f"Plugin error connection error[vscode-pytest]: {e}") - print(f"[vscode-pytest] data: {request}") - - -def post_response(cwd: str, session_node: TestNode) -> None: - """Sends a post request to the server. - - Keyword arguments: - cwd -- the current working directory. - session_node -- the session node, which is the top of the testing tree. - errors -- a list of errors that occurred during test collection. - """ - payload: DiscoveryPayloadDict = { - "cwd": cwd, - "status": "success" if not ERRORS else "error", - "tests": session_node, - "error": [], - } - if ERRORS is not None: - payload["error"] = ERRORS - testPort: Union[str, int] = os.getenv("TEST_PORT", 45454) - testuuid: Union[str, None] = os.getenv("TEST_UUID") - addr = "localhost", int(testPort) - data = json.dumps(payload) - request = f"""Content-Length: {len(data)} -Content-Type: application/json -Request-uuid: {testuuid} - -{data}""" - test_output_file: Optional[str] = os.getenv("TEST_OUTPUT_FILE", None) - if test_output_file == "stdout": - print(request) - elif test_output_file: - pathlib.Path(test_output_file).write_text(request, encoding="utf-8") - else: - try: - with socket_manager.SocketManager(addr) as s: - if s.socket is not None: - s.socket.sendall(request.encode("utf-8")) - except Exception as e: - print(f"Plugin error connection error[vscode-pytest]: {e}") - print(f"[vscode-pytest] data: {request}") diff --git a/pythonFiles/.env b/python_files/.env similarity index 100% rename from pythonFiles/.env rename to python_files/.env diff --git a/pythonFiles/.vscode/settings.json b/python_files/.vscode/settings.json similarity index 100% rename from pythonFiles/.vscode/settings.json rename to python_files/.vscode/settings.json diff --git a/pythonFiles/Notebooks intro.ipynb b/python_files/Notebooks intro.ipynb similarity index 81% rename from pythonFiles/Notebooks intro.ipynb rename to python_files/Notebooks intro.ipynb index 850d7f5a86f9..0e8aadad1919 100644 --- a/pythonFiles/Notebooks intro.ipynb +++ b/python_files/Notebooks intro.ipynb @@ -11,7 +11,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "1. Open the command palette with the shortcut: `Ctrl/Command` + `Shift` + `P`\r\n", + "1. Open the command palette with the shortcut: `Ctrl/Command` + `Shift` + `P`\n", "2. Search for the command `Create New Blank Notebook`" ] }, @@ -26,8 +26,8 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "1. Open the command palette with the shortcut: `Ctrl/Command` + `Shift` + `P`\r\n", - "\r\n", + "1. Open the command palette with the shortcut: `Ctrl/Command` + `Shift` + `P`\n", + "\n", "2. Search for the command `Python: Open Start Page`" ] }, @@ -42,10 +42,10 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "You are currently viewing what we call our Notebook Editor. It is an interactive document based on Jupyter Notebooks that supports the intermixing of code, outputs and markdown documentation. \r\n", - "\r\n", - "This cell is a markdown cell. To edit the text in this cell, simply double click on the cell to change it into edit mode.\r\n", - "\r\n", + "You are currently viewing what we call our Notebook Editor. It is an interactive document based on Jupyter Notebooks that supports the intermixing of code, outputs and markdown documentation. \n", + "\n", + "This cell is a markdown cell. To edit the text in this cell, simply double click on the cell to change it into edit mode.\n", + "\n", "The next cell below is a code cell. You can switch a cell between code and markdown by clicking on the code ![code icon](https://raw.githubusercontent.com/microsoft/vscode-python/main/images/codeIcon.PNG) /markdown ![markdown icon](https://raw.githubusercontent.com/microsoft/vscode-python/main/images/markdownIcon.PNG) icons or using the keyboard shortcut `M` and `Y` respectively." ] }, @@ -55,16 +55,16 @@ "metadata": {}, "outputs": [], "source": [ - "print('hello world')" + "print(\"hello world\")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "* To execute the code in the cell above, click on the cell to select it and then either press the play ![play](https://raw.githubusercontent.com/microsoft/vscode-python/main/images/playIcon.PNG) button in the cell toolbar, or use the keyboard shortcut `Ctrl/Command` + `Enter`.\r\n", - "* To edit the code, just click in cell and start editing.\r\n", - "* To add a new cell below, click the `Add Cell` icon ![add cell](https://raw.githubusercontent.com/microsoft/vscode-python/main/images/addIcon.PNG) at the bottom left of the cell or enter command mode with the `ESC` Key and then use the keyboard shortcut `B` to create the new cell below.\r\n" + "* To execute the code in the cell above, click on the cell to select it and then either press the play ![play](https://raw.githubusercontent.com/microsoft/vscode-python/main/images/playIcon.PNG) button in the cell toolbar, or use the keyboard shortcut `Ctrl/Command` + `Enter`.\n", + "* To edit the code, just click in cell and start editing.\n", + "* To add a new cell below, click the `Add Cell` icon ![add cell](https://raw.githubusercontent.com/microsoft/vscode-python/main/images/addIcon.PNG) at the bottom left of the cell or enter command mode with the `ESC` Key and then use the keyboard shortcut `B` to create the new cell below.\n" ] }, { @@ -78,40 +78,40 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "**Variable explorer**\r\n", - "\r\n", - "To view all your active variables and their current values in the notebook, click on the variable explorer icon ![variable explorer](https://raw.githubusercontent.com/microsoft/vscode-python/main/images/variableExplorerIcon.PNG) in the top toolbar.\r\n", - "\r\n", - "![Variable Explorer](https://raw.githubusercontent.com/microsoft/vscode-python/main/images/variableexplorer.png)\r\n", - "\r\n", - "**Data Viewer**\r\n", - "\r\n", - "To view your data frame in a more visual \"Excel\" like format, open the variable explorer and to the left of any dataframe object, you will see the data viewer icon ![data viewer](https://raw.githubusercontent.com/microsoft/vscode-python/main/images/dataViewerIcon.PNG) which you can click to open the data viewer.\r\n", - "\r\n", - "![Data Viewer](https://raw.githubusercontent.com/microsoft/vscode-python/main/images/dataviewer.gif)\r\n", - "\r\n", - "**Convert to Python File**\r\n", - "\r\n", - "To export your notebook to a Python file (.py), click on the `Convert to Python script` icon ![Export icon](https://raw.githubusercontent.com/microsoft/vscode-python/main/images/exportIcon.PNG) in the top toolbar \r\n", - "\r\n", - "![Export](https://raw.githubusercontent.com/microsoft/vscode-python/main/images/savetopythonfile.png)\r\n", - "\r\n", - "**Plot Viewer**\r\n", - "\r\n", - "If you have a graph (such as matplotlib) in your output, you'll notice if you hover over the graph, the `Plot Viewer` icon ![Plot Viewer](https://raw.githubusercontent.com/microsoft/vscode-python/main/images/plotViewerIcon.PNG) will appear in the top left. Click the icon to open up the graph in the Plotviewer which allows you to zoom on your plots and export it in formats such as png and jpeg.\r\n", - "\r\n", - "![Plot Viewer](https://raw.githubusercontent.com/microsoft/vscode-python/main/images/plotviewer.gif)\r\n", - "\r\n", - "**Switching Kernels**\r\n", - "\r\n", - "The notebook editor will detect all kernels in your system by default. To change your notebook kernel, click on the kernel status in the top toolbar at the far right. For example, your kernel status may say \"Python 3: Idle\". This will open up the kernel selector where you can choose your desired kernel.\r\n", - "\r\n", - "![Switching Kernels](https://raw.githubusercontent.com/microsoft/vscode-python/main/images/kernelchange.gif)\r\n", - "\r\n", - "**Remote Jupyter Server**\r\n", - "\r\n", - "To connect to a remote Jupyter server, open the command prompt and search for the command `Specify remote or local Jupyter server for connections`. Then select `Existing` and enter the remote Jupyter server URL. Afterwards, you'll be prompted to reload the window and the Notebook will be opened connected to the remote Jupyter server.\r\n", - "\r\n", + "**Variable explorer**\n", + "\n", + "To view all your active variables and their current values in the notebook, click on the variable explorer icon ![variable explorer](https://raw.githubusercontent.com/microsoft/vscode-python/main/images/variableExplorerIcon.PNG) in the top toolbar.\n", + "\n", + "![Variable Explorer](https://raw.githubusercontent.com/microsoft/vscode-python/main/images/variableexplorer.png)\n", + "\n", + "**Data Viewer**\n", + "\n", + "To view your data frame in a more visual \"Excel\" like format, open the variable explorer and to the left of any dataframe object, you will see the data viewer icon ![data viewer](https://raw.githubusercontent.com/microsoft/vscode-python/main/images/dataViewerIcon.PNG) which you can click to open the data viewer.\n", + "\n", + "![Data Viewer](https://raw.githubusercontent.com/microsoft/vscode-python/main/images/dataviewer.gif)\n", + "\n", + "**Convert to Python File**\n", + "\n", + "To export your notebook to a Python file (.py), click on the `Convert to Python script` icon ![Export icon](https://raw.githubusercontent.com/microsoft/vscode-python/main/images/exportIcon.PNG) in the top toolbar \n", + "\n", + "![Export](https://raw.githubusercontent.com/microsoft/vscode-python/main/images/savetopythonfile.png)\n", + "\n", + "**Plot Viewer**\n", + "\n", + "If you have a graph (such as matplotlib) in your output, you'll notice if you hover over the graph, the `Plot Viewer` icon ![Plot Viewer](https://raw.githubusercontent.com/microsoft/vscode-python/main/images/plotViewerIcon.PNG) will appear in the top left. Click the icon to open up the graph in the Plotviewer which allows you to zoom on your plots and export it in formats such as png and jpeg.\n", + "\n", + "![Plot Viewer](https://raw.githubusercontent.com/microsoft/vscode-python/main/images/plotviewer.gif)\n", + "\n", + "**Switching Kernels**\n", + "\n", + "The notebook editor will detect all kernels in your system by default. To change your notebook kernel, click on the kernel status in the top toolbar at the far right. For example, your kernel status may say \"Python 3: Idle\". This will open up the kernel selector where you can choose your desired kernel.\n", + "\n", + "![Switching Kernels](https://raw.githubusercontent.com/microsoft/vscode-python/main/images/kernelchange.gif)\n", + "\n", + "**Remote Jupyter Server**\n", + "\n", + "To connect to a remote Jupyter server, open the command prompt and search for the command `Specify remote or local Jupyter server for connections`. Then select `Existing` and enter the remote Jupyter server URL. Afterwards, you'll be prompted to reload the window and the Notebook will be opened connected to the remote Jupyter server.\n", + "\n", "![Remote](https://raw.githubusercontent.com/microsoft/vscode-python/main/images/remoteserver.gif)" ] }, @@ -129,7 +129,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "- [Data science tutorial for Visual Studio Code](https://code.visualstudio.com/docs/python/data-science-tutorial)\r\n", + "- [Data science tutorial for Visual Studio Code](https://code.visualstudio.com/docs/python/data-science-tutorial)\n", "- [Jupyter Notebooks in Visual Studio Code documentation](https://code.visualstudio.com/docs/python/jupyter-support)" ] } @@ -145,9 +145,10 @@ "name": "python3" }, "language_info": { + "name": "python", "version": "3.8.6-final" } }, "nbformat": 4, "nbformat_minor": 0 -} \ No newline at end of file +} diff --git a/pythonFiles/create_conda.py b/python_files/create_conda.py similarity index 87% rename from pythonFiles/create_conda.py rename to python_files/create_conda.py index 15320a8a1ce6..284f734081b2 100644 --- a/pythonFiles/create_conda.py +++ b/python_files/create_conda.py @@ -48,19 +48,19 @@ def parse_args(argv: Sequence[str]) -> argparse.Namespace: def file_exists(path: Union[str, pathlib.PurePath]) -> bool: - return os.path.exists(path) + return os.path.exists(path) # noqa: PTH110 def conda_env_exists(name: Union[str, pathlib.PurePath]) -> bool: - return os.path.exists(CWD / name) + return os.path.exists(CWD / name) # noqa: PTH110 def run_process(args: Sequence[str], error_message: str) -> None: try: print("Running: " + " ".join(args)) - subprocess.run(args, cwd=os.getcwd(), check=True) - except subprocess.CalledProcessError: - raise VenvError(error_message) + subprocess.run(args, cwd=os.getcwd(), check=True) # noqa: PTH109 + except subprocess.CalledProcessError as exc: + raise VenvError(error_message) from exc def get_conda_env_path(name: str) -> str: @@ -89,11 +89,10 @@ def install_packages(env_path: str) -> None: def add_gitignore(name: str) -> None: - git_ignore = os.fspath(CWD / name / ".gitignore") - if not file_exists(git_ignore): - print(f"Creating: {git_ignore}") - with open(git_ignore, "w") as f: - f.write("*") + git_ignore = CWD / name / ".gitignore" + if not git_ignore.is_file(): + print(f"Creating: {os.fsdecode(git_ignore)}") + git_ignore.write_text("*") def main(argv: Optional[Sequence[str]] = None) -> None: diff --git a/pythonFiles/create_microvenv.py b/python_files/create_microvenv.py similarity index 87% rename from pythonFiles/create_microvenv.py rename to python_files/create_microvenv.py index 10eae38ab977..2f2135444bc1 100644 --- a/pythonFiles/create_microvenv.py +++ b/python_files/create_microvenv.py @@ -20,9 +20,9 @@ class MicroVenvError(Exception): def run_process(args: Sequence[str], error_message: str) -> None: try: print("Running: " + " ".join(args)) - subprocess.run(args, cwd=os.getcwd(), check=True) - except subprocess.CalledProcessError: - raise MicroVenvError(error_message) + subprocess.run(args, cwd=os.getcwd(), check=True) # noqa: PTH109 + except subprocess.CalledProcessError as exc: + raise MicroVenvError(error_message) from exc def parse_args(argv: Sequence[str]) -> argparse.Namespace: diff --git a/pythonFiles/create_venv.py b/python_files/create_venv.py similarity index 77% rename from pythonFiles/create_venv.py rename to python_files/create_venv.py index cac084fd2222..83106bd889f8 100644 --- a/pythonFiles/create_venv.py +++ b/python_files/create_venv.py @@ -3,6 +3,7 @@ import argparse import importlib.util as import_util +import json import os import pathlib import subprocess @@ -35,6 +36,7 @@ def parse_args(argv: Sequence[str]) -> argparse.Namespace: default=None, help="Install additional dependencies from sources like `pyproject.toml` into the virtual environment.", ) + parser.add_argument( "--extras", action="append", @@ -48,6 +50,7 @@ def parse_args(argv: Sequence[str]) -> argparse.Namespace: default=False, help="Add .gitignore to the newly created virtual environment.", ) + parser.add_argument( "--name", default=VENV_NAME, @@ -56,6 +59,14 @@ def parse_args(argv: Sequence[str]) -> argparse.Namespace: metavar="NAME", action="store", ) + + parser.add_argument( + "--stdin", + action="store_true", + default=False, + help="Read arguments from stdin.", + ) + return parser.parse_args(argv) @@ -64,26 +75,43 @@ def is_installed(module: str) -> bool: def file_exists(path: Union[str, pathlib.PurePath]) -> bool: - return os.path.exists(path) + return pathlib.Path(path).exists() + + +def is_file(path: Union[str, pathlib.PurePath]) -> bool: + return pathlib.Path(path).is_file() def venv_exists(name: str) -> bool: - return os.path.exists(CWD / name) and file_exists(get_venv_path(name)) + return ( + (CWD / name).exists() + and (CWD / name / "pyvenv.cfg").exists() + and file_exists(get_venv_path(name)) + ) def run_process(args: Sequence[str], error_message: str) -> None: try: print("Running: " + " ".join(args)) - subprocess.run(args, cwd=os.getcwd(), check=True) - except subprocess.CalledProcessError: - raise VenvError(error_message) + subprocess.run(args, cwd=os.getcwd(), check=True) # noqa: PTH109 + except subprocess.CalledProcessError as exc: + raise VenvError(error_message) from exc + + +def get_win_venv_path(name: str) -> str: + venv_dir = CWD / name + # If using MSYS2 Python, the Python executable is located in the 'bin' directory. + if file_exists(venv_dir / "bin" / "python.exe"): + return os.fspath(venv_dir / "bin" / "python.exe") + else: + return os.fspath(venv_dir / "Scripts" / "python.exe") def get_venv_path(name: str) -> str: # See `venv` doc here for more details on binary location: # https://docs.python.org/3/library/venv.html#creating-virtual-environments if sys.platform == "win32": - return os.fspath(CWD / name / "Scripts" / "python.exe") + return get_win_venv_path(name) else: return os.fspath(CWD / name / "bin" / "python") @@ -119,12 +147,15 @@ def upgrade_pip(venv_path: str) -> None: print("CREATE_VENV.UPGRADED_PIP") +def create_gitignore(git_ignore: Union[str, pathlib.PurePath]): + print("Creating:", os.fspath(git_ignore)) + pathlib.Path(git_ignore).write_text("*") + + def add_gitignore(name: str) -> None: git_ignore = CWD / name / ".gitignore" - if not file_exists(git_ignore): - print("Creating: " + os.fspath(git_ignore)) - with open(git_ignore, "w") as f: - f.write("*") + if not is_file(git_ignore): + create_gitignore(git_ignore) def download_pip_pyz(name: str): @@ -133,13 +164,10 @@ def download_pip_pyz(name: str): try: with url_lib.urlopen(url) as response: - pip_pyz_path = os.fspath(CWD / name / "pip.pyz") - with open(pip_pyz_path, "wb") as out_file: - data = response.read() - out_file.write(data) - out_file.flush() - except Exception: - raise VenvError("CREATE_VENV.DOWNLOAD_PIP_FAILED") + pip_pyz_path = CWD / name / "pip.pyz" + pip_pyz_path.write_bytes(data=response.read()) + except Exception as exc: + raise VenvError("CREATE_VENV.DOWNLOAD_PIP_FAILED") from exc def install_pip(name: str): @@ -152,6 +180,16 @@ def install_pip(name: str): ) +def get_requirements_from_args(args: argparse.Namespace) -> List[str]: + requirements = [] + if args.stdin: + data = json.loads(sys.stdin.read()) + requirements = data.get("requirements", []) + if args.requirements: + requirements.extend(args.requirements) + return requirements + + def main(argv: Optional[Sequence[str]] = None) -> None: if argv is None: argv = [] @@ -219,14 +257,15 @@ def main(argv: Optional[Sequence[str]] = None) -> None: download_pip_pyz(args.name) install_pip(args.name) + requirements = get_requirements_from_args(args) + if requirements: + print(f"VENV_INSTALLING_REQUIREMENTS: {requirements}") + install_requirements(venv_path, requirements) + if args.toml: print(f"VENV_INSTALLING_PYPROJECT: {args.toml}") install_toml(venv_path, args.extras) - if args.requirements: - print(f"VENV_INSTALLING_REQUIREMENTS: {args.requirements}") - install_requirements(venv_path, args.requirements) - if __name__ == "__main__": main(sys.argv[1:]) diff --git a/python_files/deactivate/bash/deactivate b/python_files/deactivate/bash/deactivate new file mode 100755 index 000000000000..f6dd33425d1a --- /dev/null +++ b/python_files/deactivate/bash/deactivate @@ -0,0 +1,44 @@ +# Same as deactivate in "/bin/activate" +deactivate () { + if [ -n "${_OLD_VIRTUAL_PATH:-}" ] ; then + PATH="${_OLD_VIRTUAL_PATH:-}" + export PATH + unset _OLD_VIRTUAL_PATH + fi + if [ -n "${_OLD_VIRTUAL_PYTHONHOME:-}" ] ; then + PYTHONHOME="${_OLD_VIRTUAL_PYTHONHOME:-}" + export PYTHONHOME + unset _OLD_VIRTUAL_PYTHONHOME + fi + if [ -n "${BASH:-}" -o -n "${ZSH_VERSION:-}" ] ; then + hash -r 2> /dev/null + fi + if [ -n "${_OLD_VIRTUAL_PS1:-}" ] ; then + PS1="${_OLD_VIRTUAL_PS1:-}" + export PS1 + unset _OLD_VIRTUAL_PS1 + fi + unset VIRTUAL_ENV + unset VIRTUAL_ENV_PROMPT + if [ ! "${1:-}" = "nondestructive" ] ; then + unset -f deactivate + fi +} + +# Get the directory of the current script +SCRIPT_DIR=$(dirname "$0") +# Construct the path to envVars.txt relative to the script directory +ENV_FILE="$SCRIPT_DIR/envVars.txt" + +# Read the JSON file and set the variables +TEMP_PS1=$(grep '^PS1=' $ENV_FILE | cut -d '=' -f 2) +TEMP_PATH=$(grep '^PATH=' $ENV_FILE | cut -d '=' -f 2) +TEMP_PYTHONHOME=$(grep '^PYTHONHOME=' $ENV_FILE | cut -d '=' -f 2) +# Initialize the variables required by deactivate function +_OLD_VIRTUAL_PS1="${TEMP_PS1:-}" +_OLD_VIRTUAL_PATH="$TEMP_PATH" +if [ -n "${PYTHONHOME:-}" ] ; then + _OLD_VIRTUAL_PYTHONHOME="${TEMP_PYTHONHOME:-}" +fi +deactivate +bash diff --git a/python_files/deactivate/fish/deactivate b/python_files/deactivate/fish/deactivate new file mode 100755 index 000000000000..3a9d50ccde2b --- /dev/null +++ b/python_files/deactivate/fish/deactivate @@ -0,0 +1,44 @@ +# Same as deactivate in "/bin/activate" +deactivate () { + if [ -n "${_OLD_VIRTUAL_PATH:-}" ] ; then + PATH="${_OLD_VIRTUAL_PATH:-}" + export PATH + unset _OLD_VIRTUAL_PATH + fi + if [ -n "${_OLD_VIRTUAL_PYTHONHOME:-}" ] ; then + PYTHONHOME="${_OLD_VIRTUAL_PYTHONHOME:-}" + export PYTHONHOME + unset _OLD_VIRTUAL_PYTHONHOME + fi + if [ -n "${BASH:-}" -o -n "${ZSH_VERSION:-}" ] ; then + hash -r 2> /dev/null + fi + if [ -n "${_OLD_VIRTUAL_PS1:-}" ] ; then + PS1="${_OLD_VIRTUAL_PS1:-}" + export PS1 + unset _OLD_VIRTUAL_PS1 + fi + unset VIRTUAL_ENV + unset VIRTUAL_ENV_PROMPT + if [ ! "${1:-}" = "nondestructive" ] ; then + unset -f deactivate + fi +} + +# Get the directory of the current script +SCRIPT_DIR=$(dirname "$0") +# Construct the path to envVars.txt relative to the script directory +ENV_FILE="$SCRIPT_DIR/envVars.txt" + +# Read the JSON file and set the variables +TEMP_PS1=$(grep '^PS1=' $ENV_FILE | cut -d '=' -f 2) +TEMP_PATH=$(grep '^PATH=' $ENV_FILE | cut -d '=' -f 2) +TEMP_PYTHONHOME=$(grep '^PYTHONHOME=' $ENV_FILE | cut -d '=' -f 2) +# Initialize the variables required by deactivate function +_OLD_VIRTUAL_PS1="${TEMP_PS1:-}" +_OLD_VIRTUAL_PATH="$TEMP_PATH" +if [ -n "${PYTHONHOME:-}" ] ; then + _OLD_VIRTUAL_PYTHONHOME="${TEMP_PYTHONHOME:-}" +fi +deactivate +fish diff --git a/python_files/deactivate/powershell/deactivate.ps1 b/python_files/deactivate/powershell/deactivate.ps1 new file mode 100644 index 000000000000..49365e0fbeff --- /dev/null +++ b/python_files/deactivate/powershell/deactivate.ps1 @@ -0,0 +1,11 @@ +# Load dotenv-style file and restore environment variables +Get-Content -Path "$PSScriptRoot\envVars.txt" | ForEach-Object { + # Split each line into key and value at the first '=' + $parts = $_ -split '=', 2 + if ($parts.Count -eq 2) { + $key = $parts[0].Trim() + $value = $parts[1].Trim() + # Set the environment variable + Set-Item -Path "env:$key" -Value $value + } +} diff --git a/python_files/deactivate/zsh/deactivate b/python_files/deactivate/zsh/deactivate new file mode 100755 index 000000000000..8b059318f988 --- /dev/null +++ b/python_files/deactivate/zsh/deactivate @@ -0,0 +1,44 @@ +# Same as deactivate in "/bin/activate" +deactivate () { + if [ -n "${_OLD_VIRTUAL_PATH:-}" ] ; then + PATH="${_OLD_VIRTUAL_PATH:-}" + export PATH + unset _OLD_VIRTUAL_PATH + fi + if [ -n "${_OLD_VIRTUAL_PYTHONHOME:-}" ] ; then + PYTHONHOME="${_OLD_VIRTUAL_PYTHONHOME:-}" + export PYTHONHOME + unset _OLD_VIRTUAL_PYTHONHOME + fi + if [ -n "${BASH:-}" -o -n "${ZSH_VERSION:-}" ] ; then + hash -r 2> /dev/null + fi + if [ -n "${_OLD_VIRTUAL_PS1:-}" ] ; then + PS1="${_OLD_VIRTUAL_PS1:-}" + export PS1 + unset _OLD_VIRTUAL_PS1 + fi + unset VIRTUAL_ENV + unset VIRTUAL_ENV_PROMPT + if [ ! "${1:-}" = "nondestructive" ] ; then + unset -f deactivate + fi +} + +# Get the directory of the current script +SCRIPT_DIR=$(dirname "$0") +# Construct the path to envVars.txt relative to the script directory +ENV_FILE="$SCRIPT_DIR/envVars.txt" + +# Read the JSON file and set the variables +TEMP_PS1=$(grep '^PS1=' $ENV_FILE | cut -d '=' -f 2) +TEMP_PATH=$(grep '^PATH=' $ENV_FILE | cut -d '=' -f 2) +TEMP_PYTHONHOME=$(grep '^PYTHONHOME=' $ENV_FILE | cut -d '=' -f 2) +# Initialize the variables required by deactivate function +_OLD_VIRTUAL_PS1="${TEMP_PS1:-}" +_OLD_VIRTUAL_PATH="$TEMP_PATH" +if [ -n "${PYTHONHOME:-}" ] ; then + _OLD_VIRTUAL_PYTHONHOME="${TEMP_PYTHONHOME:-}" +fi +deactivate +zsh diff --git a/python_files/download_get_pip.py b/python_files/download_get_pip.py new file mode 100644 index 000000000000..91ab107760d8 --- /dev/null +++ b/python_files/download_get_pip.py @@ -0,0 +1,59 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +import json +import pathlib +import urllib.request as url_lib + +from packaging.version import parse as version_parser + +EXTENSION_ROOT = pathlib.Path(__file__).parent.parent +GET_PIP_DEST = EXTENSION_ROOT / "python_files" +PIP_PACKAGE = "pip" +PIP_VERSION = "latest" # Can be "latest", or specific version "23.1.2" + + +def _get_package_data(): + json_uri = f"https://pypi.org/pypi/{PIP_PACKAGE}/json" + # Response format: https://warehouse.readthedocs.io/api-reference/json/#project + # Release metadata format: https://github.com/pypa/interoperability-peps/blob/master/pep-0426-core-metadata.rst + with url_lib.urlopen(json_uri) as response: + return json.loads(response.read()) + + +def _download_and_save(root, version): + root = pathlib.Path.cwd() if root is None or root == "." else pathlib.Path(root) + url = f"https://raw.githubusercontent.com/pypa/get-pip/{version}/public/get-pip.py" + print(url) + with url_lib.urlopen(url) as response: + data = response.read() + get_pip_file = root / "get-pip.py" + get_pip_file.write_bytes(data) + + +def main(root): + data = _get_package_data() + + if PIP_VERSION == "latest": + # Pick latest 5 versions to try and get-pip + sorted_versions = sorted(data["releases"].keys(), key=version_parser, reverse=True)[:5] + downloaded = False + while sorted_versions: + use_version = sorted_versions.pop(0) + try: + print(f"Trying version: get-pip == {use_version}") + _download_and_save(root, use_version) + downloaded = True + break + except Exception as e: + print(f"Failed to download get-pip == {use_version}: {e}") + print(f"NExt attempt(s) with versions: {sorted_versions}") + if not downloaded: + raise Exception("Failed to download get-pip.py") + else: + use_version = PIP_VERSION + _download_and_save(root, use_version) + + +if __name__ == "__main__": + main(GET_PIP_DEST) diff --git a/pythonFiles/get_output_via_markers.py b/python_files/get_output_via_markers.py similarity index 89% rename from pythonFiles/get_output_via_markers.py rename to python_files/get_output_via_markers.py index 00dd57065b3c..e37f7f8c5df0 100644 --- a/pythonFiles/get_output_via_markers.py +++ b/python_files/get_output_via_markers.py @@ -18,9 +18,9 @@ del sys.argv[0] exec(code, ns, ns) elif module.startswith("-m"): - moduleName = sys.argv[2] + module_name = sys.argv[2] sys.argv = sys.argv[2:] # It should begin with the module name. - runpy.run_module(moduleName, run_name="__main__", alter_sys=True) + runpy.run_module(module_name, run_name="__main__", alter_sys=True) elif module.endswith(".py"): sys.argv = sys.argv[1:] runpy.run_path(module, run_name="__main__") diff --git a/python_files/get_variable_info.py b/python_files/get_variable_info.py new file mode 100644 index 000000000000..d60795982617 --- /dev/null +++ b/python_files/get_variable_info.py @@ -0,0 +1,539 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See LICENSE in the project root +# for license information. + +import locale +import sys +from typing import ClassVar + + +# this class is from in ptvsd/debugpy tools +class SafeRepr(object): # noqa: UP004 + # Can be used to override the encoding from locale.getpreferredencoding() + locale_preferred_encoding = None + + # Can be used to override the encoding used for sys.stdout.encoding + sys_stdout_encoding = None + + # String types are truncated to maxstring_outer when at the outer- + # most level, and truncated to maxstring_inner characters inside + # collections. + maxstring_outer = 2**16 + maxstring_inner = 128 + string_types = (str, bytes) + bytes = bytes + set_info = (set, "{", "}", False) + frozenset_info = (frozenset, "frozenset({", "})", False) + int_types = (int,) + long_iter_types = (list, tuple, bytearray, range, dict, set, frozenset) + + # Collection types are recursively iterated for each limit in + # maxcollection. + maxcollection = (60, 20) + + # Specifies type, prefix string, suffix string, and whether to include a + # comma if there is only one element. (Using a sequence rather than a + # mapping because we use isinstance() to determine the matching type.) + collection_types = [ # noqa: RUF012 + (tuple, "(", ")", True), + (list, "[", "]", False), + frozenset_info, + set_info, + ] + try: + from collections import deque + + collection_types.append((deque, "deque([", "])", False)) + except Exception: + pass + + # type, prefix string, suffix string, item prefix string, + # item key/value separator, item suffix string + dict_types: ClassVar[list] = [(dict, "{", "}", "", ": ", "")] + try: + from collections import OrderedDict + + dict_types.append((OrderedDict, "OrderedDict([", "])", "(", ", ", ")")) + except Exception: + pass + + # All other types are treated identically to strings, but using + # different limits. + maxother_outer = 2**16 + maxother_inner = 128 + + convert_to_hex = False + raw_value = False + + def __call__(self, obj): + """ + :param object obj: + The object for which we want a representation. + + :return str: + Returns bytes encoded as utf-8 on py2 and str on py3. + """ # noqa: D205 + try: + return "".join(self._repr(obj, 0)) + except Exception: + try: + return f"An exception was raised: {sys.exc_info()[1]!r}" + except Exception: + return "An exception was raised" + + def _repr(self, obj, level): + """Returns an iterable of the parts in the final repr string.""" + try: + obj_repr = type(obj).__repr__ + except Exception: + obj_repr = None + + def has_obj_repr(t): + r = t.__repr__ + try: + return obj_repr == r + except Exception: + return obj_repr is r + + for t, prefix, suffix, comma in self.collection_types: + if isinstance(obj, t) and has_obj_repr(t): + return self._repr_iter(obj, level, prefix, suffix, comma) + + for ( + t, + prefix, + suffix, + item_prefix, + item_sep, + item_suffix, + ) in self.dict_types: + if isinstance(obj, t) and has_obj_repr(t): + return self._repr_dict( + obj, level, prefix, suffix, item_prefix, item_sep, item_suffix + ) + + for t in self.string_types: + if isinstance(obj, t) and has_obj_repr(t): + return self._repr_str(obj, level) + + if self._is_long_iter(obj): + return self._repr_long_iter(obj) + + return self._repr_other(obj, level) + + # Determines whether an iterable exceeds the limits set in + # maxlimits, and is therefore unsafe to repr(). + def _is_long_iter(self, obj, level=0): + try: + # Strings have their own limits (and do not nest). Because + # they don't have __iter__ in 2.x, this check goes before + # the next one. + if isinstance(obj, self.string_types): + return len(obj) > self.maxstring_inner + + # If it's not an iterable (and not a string), it's fine. + if not hasattr(obj, "__iter__"): + return False + + # If it's not an instance of these collection types then it + # is fine. Note: this is a fix for + # https://github.com/Microsoft/ptvsd/issues/406 + if not isinstance(obj, self.long_iter_types): + return False + + # Iterable is its own iterator - this is a one-off iterable + # like generator or enumerate(). We can't really count that, + # but repr() for these should not include any elements anyway, + # so we can treat it the same as non-iterables. + if obj is iter(obj): + return False + + # range reprs fine regardless of length. + if isinstance(obj, range): + return False + + # numpy and scipy collections (ndarray etc) have + # self-truncating repr, so they're always safe. + try: + module = type(obj).__module__.partition(".")[0] + if module in ("numpy", "scipy"): + return False + except Exception: + pass + + # Iterables that nest too deep are considered long. + if level >= len(self.maxcollection): + return True + + # It is too long if the length exceeds the limit, or any + # of its elements are long iterables. + if hasattr(obj, "__len__"): + try: + size = len(obj) + except Exception: + size = None + if size is not None and size > self.maxcollection[level]: + return True + return any(self._is_long_iter(item, level + 1) for item in obj) + return any( + i > self.maxcollection[level] or self._is_long_iter(item, level + 1) + for i, item in enumerate(obj) + ) + + except Exception: + # If anything breaks, assume the worst case. + return True + + def _repr_iter(self, obj, level, prefix, suffix, comma_after_single_element=False): # noqa: FBT002 + yield prefix + + if level >= len(self.maxcollection): + yield "..." + else: + count = self.maxcollection[level] + yield_comma = False + for item in obj: + if yield_comma: + yield ", " + yield_comma = True + + count -= 1 + if count <= 0: + yield "..." + break + + yield from self._repr(item, 100 if item is obj else level + 1) + else: + if comma_after_single_element: # noqa: SIM102 + if count == self.maxcollection[level] - 1: + yield "," + yield suffix + + def _repr_long_iter(self, obj): + try: + length = hex(len(obj)) if self.convert_to_hex else len(obj) + obj_repr = f"<{type(obj).__name__}, len() = {length}>" + except Exception: + try: + obj_repr = "<" + type(obj).__name__ + ">" + except Exception: + obj_repr = "" + yield obj_repr + + def _repr_dict(self, obj, level, prefix, suffix, item_prefix, item_sep, item_suffix): + if not obj: + yield prefix + suffix + return + if level >= len(self.maxcollection): + yield prefix + "..." + suffix + return + + yield prefix + + count = self.maxcollection[level] + yield_comma = False + + obj_keys = list(obj) + + for key in obj_keys: + if yield_comma: + yield ", " + yield_comma = True + + count -= 1 + if count <= 0: + yield "..." + break + + yield item_prefix + for p in self._repr(key, level + 1): + yield p + + yield item_sep + + try: + item = obj[key] + except Exception: + yield "" + else: + for p in self._repr(item, 100 if item is obj else level + 1): + yield p + yield item_suffix + + yield suffix + + def _repr_str(self, obj, level): + try: + if self.raw_value: + # For raw value retrieval, ignore all limits. + if isinstance(obj, bytes): + yield obj.decode("latin-1") + else: + yield obj + return + + limit_inner = self.maxother_inner + limit_outer = self.maxother_outer + limit = limit_inner if level > 0 else limit_outer + if len(obj) <= limit: + # Note that we check the limit before doing the repr (so, the final string + # may actually be considerably bigger on some cases, as besides + # the additional u, b, ' chars, some chars may be escaped in repr, so + # even a single char such as \U0010ffff may end up adding more + # chars than expected). + yield self._convert_to_unicode_or_bytes_repr(repr(obj)) + return + + # Slightly imprecise calculations - we may end up with a string that is + # up to 6 characters longer than limit. If you need precise formatting, + # you are using the wrong class. + left_count, right_count = max(1, int(2 * limit / 3)), max(1, int(limit / 3)) + + # Important: only do repr after slicing to avoid duplicating a byte array that could be + # huge. + + # Note: we don't deal with high surrogates here because we're not dealing with the + # repr() of a random object. + # i.e.: A high surrogate unicode char may be splitted on Py2, but as we do a `repr` + # afterwards, that's ok. + + # Also, we just show the unicode/string/bytes repr() directly to make clear what the + # input type was (so, on py2 a unicode would start with u' and on py3 a bytes would + # start with b'). + + part1 = obj[:left_count] + part1 = repr(part1) + part1 = part1[: part1.rindex("'")] # Remove the last ' + + part2 = obj[-right_count:] + part2 = repr(part2) + part2 = part2[part2.index("'") + 1 :] # Remove the first ' (and possibly u or b). + + yield part1 + yield "..." + yield part2 + except: # noqa: E722 + # This shouldn't really happen, but let's play it safe. + # exception('Error getting string representation to show.') + yield from self._repr_obj(obj, level, self.maxother_inner, self.maxother_outer) + + def _repr_other(self, obj, level): + return self._repr_obj(obj, level, self.maxother_inner, self.maxother_outer) + + def _repr_obj(self, obj, level, limit_inner, limit_outer): + try: + if self.raw_value: + # For raw value retrieval, ignore all limits. + if isinstance(obj, bytes): + yield obj.decode("latin-1") + return + + try: + mv = memoryview(obj) + except Exception: + yield self._convert_to_unicode_or_bytes_repr(repr(obj)) + return + else: + # Map bytes to Unicode codepoints with same values. + yield mv.tobytes().decode("latin-1") + return + elif self.convert_to_hex and isinstance(obj, self.int_types): + obj_repr = hex(obj) + else: + obj_repr = repr(obj) + except Exception: + try: + obj_repr = object.__repr__(obj) + except Exception: + try: + obj_repr = "" + except Exception: + obj_repr = "" + + limit = limit_inner if level > 0 else limit_outer + + if limit >= len(obj_repr): + yield self._convert_to_unicode_or_bytes_repr(obj_repr) + return + + # Slightly imprecise calculations - we may end up with a string that is + # up to 3 characters longer than limit. If you need precise formatting, + # you are using the wrong class. + left_count, right_count = max(1, int(2 * limit / 3)), max(1, int(limit / 3)) + + yield obj_repr[:left_count] + yield "..." + yield obj_repr[-right_count:] + + def _convert_to_unicode_or_bytes_repr(self, obj_repr): + return obj_repr + + def _bytes_as_unicode_if_possible(self, obj_repr): + # We try to decode with 3 possible encoding (sys.stdout.encoding, + # locale.getpreferredencoding() and 'utf-8). If no encoding can decode + # the input, we return the original bytes. + try_encodings = [] + encoding = self.sys_stdout_encoding or getattr(sys.stdout, "encoding", None) + if encoding: + try_encodings.append(encoding.lower()) + + preferred_encoding = self.locale_preferred_encoding or locale.getpreferredencoding() + if preferred_encoding: + preferred_encoding = preferred_encoding.lower() + if preferred_encoding not in try_encodings: + try_encodings.append(preferred_encoding) + + if "utf-8" not in try_encodings: + try_encodings.append("utf-8") + + for encoding in try_encodings: + try: + return obj_repr.decode(encoding) + except UnicodeDecodeError: # noqa: PERF203 + pass + + return obj_repr # Return the original version (in bytes) + + +class DisplayOptions: + def __init__(self, width, max_columns): + self.width = width + self.max_columns = max_columns + + +_safe_repr = SafeRepr() +_collection_types = ["list", "tuple", "set"] +_array_page_size = 50 + + +def _get_value(variable): + return _safe_repr(variable) + + +def _get_property_names(variable): + props = [] + private_props = [] + for prop in dir(variable): + if not prop.startswith("_"): + props.append(prop) + elif not prop.startswith("__"): + private_props.append(prop) + return props + private_props + + +def _get_full_type(var_type): + module = "" + if hasattr(var_type, "__module__") and var_type.__module__ != "builtins": + module = var_type.__module__ + "." + if hasattr(var_type, "__qualname__"): + return module + var_type.__qualname__ + elif hasattr(var_type, "__name__"): + return module + var_type.__name__ + return None + + +def _get_variable_description(variable): + result = {} + + var_type = type(variable) + result["type"] = _get_full_type(var_type) + if hasattr(var_type, "__mro__"): + result["interfaces"] = [_get_full_type(t) for t in var_type.__mro__] + + if hasattr(variable, "__len__") and result["type"] in _collection_types: + result["count"] = len(variable) + + result["hasNamedChildren"] = hasattr(variable, "__dict__") or isinstance(variable, dict) + + result["value"] = _get_value(variable) + return result + + +def _get_child_property(root, property_chain): + try: + variable = root + for prop in property_chain: + if isinstance(prop, int): + if hasattr(variable, "__getitem__"): + variable = variable[prop] + elif isinstance(variable, set): + variable = list(variable)[prop] + else: + return None + elif hasattr(variable, prop): + variable = getattr(variable, prop) + elif isinstance(variable, dict) and prop in variable: + variable = variable[prop] + else: + return None + except Exception: + return None + + return variable + + +types_to_exclude = ["module", "function", "method", "class", "type"] + + +### Get info on variables at the root level +def getVariableDescriptions(): # noqa: N802 + return [ + { + "name": varName, + **_get_variable_description(globals()[varName]), + "root": varName, + "propertyChain": [], + "language": "python", + } + for varName in globals() + if type(globals()[varName]).__name__ not in types_to_exclude + and not varName.startswith("__") + ] + + +### Get info on children of a variable reached through the given property chain +def getAllChildrenDescriptions(root_var_name, property_chain, start_index): # noqa: N802 + root = globals()[root_var_name] + if root is None: + return [] + + parent = root + if len(property_chain) > 0: + parent = _get_child_property(root, property_chain) + + children = [] + parent_info = _get_variable_description(parent) + if "count" in parent_info: + if parent_info["count"] > 0: + last_item = min(parent_info["count"], start_index + _array_page_size) + index_range = range(start_index, last_item) + children = [ + { + **_get_variable_description(_get_child_property(parent, [i])), + "name": str(i), + "root": root_var_name, + "propertyChain": [*property_chain, i], + "language": "python", + } + for i in index_range + ] + elif parent_info["hasNamedChildren"]: + children_names = [] + if hasattr(parent, "__dict__"): + children_names = _get_property_names(parent) + elif isinstance(parent, dict): + children_names = list(parent.keys()) + + children = [] + for prop in children_names: + child_property = _get_child_property(parent, [prop]) + if child_property is not None and type(child_property).__name__ not in types_to_exclude: + child = { + **_get_variable_description(child_property), + "name": prop, + "root": root_var_name, + "propertyChain": [*property_chain, prop], + } + children.append(child) + + return children diff --git a/python_files/installed_check.py b/python_files/installed_check.py new file mode 100644 index 000000000000..4fa3cdbb2385 --- /dev/null +++ b/python_files/installed_check.py @@ -0,0 +1,132 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +import argparse +import json +import os +import pathlib +import sys +from typing import Dict, List, Optional, Sequence, Tuple, Union + +LIB_ROOT = pathlib.Path(__file__).parent / "lib" / "python" +sys.path.insert(0, os.fspath(LIB_ROOT)) + +import tomli # noqa: E402 +from importlib_metadata import metadata # noqa: E402 +from packaging.requirements import Requirement # noqa: E402 + +DEFAULT_SEVERITY = "3" # 'Hint' +try: + SEVERITY = int(os.getenv("VSCODE_MISSING_PGK_SEVERITY", DEFAULT_SEVERITY)) +except ValueError: + SEVERITY = int(DEFAULT_SEVERITY) + + +def parse_args(argv: Optional[Sequence[str]] = None): + if argv is None: + argv = sys.argv[1:] + parser = argparse.ArgumentParser( + description="Check for installed packages against requirements" + ) + parser.add_argument("FILEPATH", type=str, help="Path to requirements.[txt, in]") + + return parser.parse_args(argv) + + +def parse_requirements(line: str) -> Optional[Requirement]: + try: + req = Requirement(line.strip("\\")) + if req.marker is None or req.marker.evaluate(): + return req + except Exception: + pass + return None + + +def process_requirements(req_file: pathlib.Path) -> List[Dict[str, Union[str, int]]]: + diagnostics = [] + for n, line in enumerate(req_file.read_text(encoding="utf-8").splitlines()): + if line.startswith(("#", "-", " ")) or line == "": + continue + + req = parse_requirements(line) + if req: + try: + # Check if package is installed + metadata(req.name) + except Exception: + diagnostics.append( + { + "line": n, + "character": 0, + "endLine": n, + "endCharacter": len(req.name), + "package": req.name, + "code": "not-installed", + "severity": SEVERITY, + } + ) + return diagnostics + + +def get_pos(lines: List[str], text: str) -> Tuple[int, int, int, int]: + for n, line in enumerate(lines): + index = line.find(text) + if index >= 0: + return n, index, n, index + len(text) + return (0, 0, 0, 0) + + +def process_pyproject(req_file: pathlib.Path) -> List[Dict[str, Union[str, int]]]: + diagnostics = [] + try: + raw_text = req_file.read_text(encoding="utf-8") + pyproject = tomli.loads(raw_text) + except Exception: + return diagnostics + + lines = raw_text.splitlines() + reqs = pyproject.get("project", {}).get("dependencies", []) + for raw_req in reqs: + req = parse_requirements(raw_req) + n, start, _, end = get_pos(lines, raw_req) + if req: + try: + # Check if package is installed + metadata(req.name) + except Exception: + diagnostics.append( + { + "line": n, + "character": start, + "endLine": n, + "endCharacter": end, + "package": req.name, + "code": "not-installed", + "severity": SEVERITY, + } + ) + return diagnostics + + +def get_diagnostics(req_file: pathlib.Path) -> List[Dict[str, Union[str, int]]]: + diagnostics = [] + if not req_file.exists(): + return diagnostics + + if req_file.name == "pyproject.toml": + diagnostics = process_pyproject(req_file) + else: + diagnostics = process_requirements(req_file) + + return diagnostics + + +def main(): + args = parse_args() + diagnostics = get_diagnostics(pathlib.Path(args.FILEPATH)) + print(json.dumps(diagnostics, ensure_ascii=False)) + + +if __name__ == "__main__": + main() diff --git a/pythonFiles/interpreterInfo.py b/python_files/interpreterInfo.py similarity index 100% rename from pythonFiles/interpreterInfo.py rename to python_files/interpreterInfo.py diff --git a/python_files/jedilsp_requirements/requirements.in b/python_files/jedilsp_requirements/requirements.in new file mode 100644 index 000000000000..794e9c8ea686 --- /dev/null +++ b/python_files/jedilsp_requirements/requirements.in @@ -0,0 +1,8 @@ +# This file is used to generate requirements.txt. +# To update requirements.txt, run the following commands. +# Use Python 3.9 when creating the environment or using pip-tools +# 1) Install `uv` https://docs.astral.sh/uv/getting-started/installation/ +# 2) uv pip compile --generate-hashes --upgrade python_files\jedilsp_requirements\requirements.in -o python_files\jedilsp_requirements\requirements.txt + +jedi-language-server>=0.34.3 +pygls>=0.10.3 diff --git a/python_files/jedilsp_requirements/requirements.txt b/python_files/jedilsp_requirements/requirements.txt new file mode 100644 index 000000000000..e2599e7bbce4 --- /dev/null +++ b/python_files/jedilsp_requirements/requirements.txt @@ -0,0 +1,63 @@ +# This file was autogenerated by uv via the following command: +# uv pip compile --generate-hashes python_files\jedilsp_requirements\requirements.in -o .\python_files\jedilsp_requirements\requirements.txt +attrs==25.3.0 \ + --hash=sha256:427318ce031701fea540783410126f03899a97ffc6f61596ad581ac2e40e3bc3 \ + --hash=sha256:75d7cefc7fb576747b2c81b4442d4d4a1ce0900973527c011d1030fd3bf4af1b + # via + # cattrs + # lsprotocol +cattrs==25.2.0 \ + --hash=sha256:539d7eedee7d2f0706e4e109182ad096d608ba84633c32c75ef3458f1d11e8f1 \ + --hash=sha256:f46c918e955db0177be6aa559068390f71988e877c603ae2e56c71827165cc06 + # via + # jedi-language-server + # lsprotocol + # pygls +docstring-to-markdown==0.17 \ + --hash=sha256:df72a112294c7492487c9da2451cae0faeee06e86008245c188c5761c9590ca3 \ + --hash=sha256:fd7d5094aa83943bf5f9e1a13701866b7c452eac19765380dead666e36d3711c + # via jedi-language-server +exceptiongroup==1.3.0 \ + --hash=sha256:4d111e6e0c13d0644cad6ddaa7ed0261a0b36971f6d23e7ec9b4b9097da78a10 \ + --hash=sha256:b241f5885f560bc56a59ee63ca4c6a8bfa46ae4ad651af316d4e81817bb9fd88 + # via cattrs +importlib-metadata==8.7.0 \ + --hash=sha256:d13b81ad223b890aa16c5471f2ac3056cf76c5f10f82d6f9292f0b415f389000 \ + --hash=sha256:e5dd1551894c77868a30651cef00984d50e1002d06942a7101d34870c5f02afd + # via docstring-to-markdown +jedi==0.19.2 \ + --hash=sha256:4770dc3de41bde3966b02eb84fbcf557fb33cce26ad23da12c742fb50ecb11f0 \ + --hash=sha256:a8ef22bde8490f57fe5c7681a3c83cb58874daf72b4784de3cce5b6ef6edb5b9 + # via jedi-language-server +jedi-language-server==0.45.1 \ + --hash=sha256:8c0c6b4eaeffdbb87be79e9897c9929ffeddf875dff7c1c36dd67768e294942b \ + --hash=sha256:a1fcfba8008f2640e921937fcf1933c3961d74249341eba8b3ef9a0c3f817102 + # via -r python_files/jedilsp_requirements/requirements.in +lsprotocol==2023.0.1 \ + --hash=sha256:c75223c9e4af2f24272b14c6375787438279369236cd568f596d4951052a60f2 \ + --hash=sha256:cc5c15130d2403c18b734304339e51242d3018a05c4f7d0f198ad6e0cd21861d + # via + # jedi-language-server + # pygls +parso==0.8.5 \ + --hash=sha256:034d7354a9a018bdce352f48b2a8a450f05e9d6ee85db84764e9b6bd96dafe5a \ + --hash=sha256:646204b5ee239c396d040b90f9e272e9a8017c630092bf59980beb62fd033887 + # via jedi +pygls==1.3.1 \ + --hash=sha256:140edceefa0da0e9b3c533547c892a42a7d2fd9217ae848c330c53d266a55018 \ + --hash=sha256:6e00f11efc56321bdeb6eac04f6d86131f654c7d49124344a9ebb968da3dd91e + # via + # -r python_files/jedilsp_requirements/requirements.in + # jedi-language-server +typing-extensions==4.15.0 \ + --hash=sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466 \ + --hash=sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548 + # via + # cattrs + # docstring-to-markdown + # exceptiongroup + # jedi-language-server +zipp==3.23.0 \ + --hash=sha256:071652d6115ed432f5ce1d34c336c0adfd6a884660d1e9712a256d3d3bd4b14e \ + --hash=sha256:a07157588a12518c9d4034df3fbbee09c814741a33ff63c05fa29d26a2404166 + # via importlib-metadata diff --git a/pythonFiles/linter.py b/python_files/linter.py similarity index 89% rename from pythonFiles/linter.py rename to python_files/linter.py index 58ad9397f58b..edbbe9dfafe5 100644 --- a/pythonFiles/linter.py +++ b/python_files/linter.py @@ -1,7 +1,6 @@ import subprocess import sys - linter_settings = { "pylint": { "args": ["--reports=n", "--output-format=json"], @@ -37,11 +36,7 @@ def main(): invoke = sys.argv[1] if invoke == "-m": linter = sys.argv[2] - args = ( - [sys.executable, "-m", linter] - + linter_settings[linter]["args"] - + sys.argv[3:] - ) + args = [sys.executable, "-m", linter] + linter_settings[linter]["args"] + sys.argv[3:] else: linter = sys.argv[2] args = [sys.argv[3]] + linter_settings[linter]["args"] + sys.argv[4:] diff --git a/python_files/normalizeSelection.py b/python_files/normalizeSelection.py new file mode 100644 index 000000000000..9d82a4dc9440 --- /dev/null +++ b/python_files/normalizeSelection.py @@ -0,0 +1,310 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +import ast +import json +import re +import sys +import textwrap +from typing import Iterable + +attach_bracket_paste = sys.version_info >= (3, 13) + + +def split_lines(source): + """ + Split selection lines in a version-agnostic way. + + Python grammar only treats \r, \n, and \r\n as newlines. + But splitlines() in Python 3 has a much larger list: for example, it also includes \v, \f. + As such, this function will split lines across all Python versions. + """ + return re.split(r"[\n\r]+", source) + + +def _get_statements(selection): + """Process a multiline selection into a list of its top-level statements. + + This will remove empty newlines around and within the selection, dedent it, + and split it using the result of `ast.parse()`. + """ + # Remove blank lines within the selection to prevent the REPL from thinking the block is finished. + lines = (line for line in split_lines(selection) if line.strip() != "") + + # Dedent the selection and parse it using the ast module. + # Note that leading comments in the selection will be discarded during parsing. + source = textwrap.dedent("\n".join(lines)) + tree = ast.parse(source) + + # We'll need the dedented lines to rebuild the selection. + lines = split_lines(source) + + # Get the line ranges for top-level blocks returned from parsing the dedented text + # and split the selection accordingly. + # tree.body is a list of AST objects, which we rely on to extract top-level statements. + # If we supported Python 3.8+ only we could use the lineno and end_lineno attributes of each object + # to get the boundaries of each block. + # However, earlier Python versions only have the lineno attribute, which is the range start position (1-indexed). + # Therefore, to retrieve the end line of each block in a version-agnostic way we need to do + # `end = next_block.lineno - 1` + # for all blocks except the last one, which will will just run until the last line. + ends = [] + for node in tree.body[1:]: + line_end = node.lineno - 1 + # Special handling of decorators: + # In Python 3.8 and higher, decorators are not taken into account in the value returned by lineno, + # and we have to use the length of the decorator_list array to compute the actual start line. + # Before that, lineno takes into account decorators, so this offset check is unnecessary. + # Also, not all AST objects can have decorators. + if hasattr(node, "decorator_list") and sys.version_info >= (3, 8): + # Using getattr instead of node.decorator_list or pyright will complain about an unknown member. + line_end -= len(getattr(node, "decorator_list")) # noqa: B009 + ends.append(line_end) + ends.append(len(lines)) + + for node, end in zip(tree.body, ends): + # Given this selection: + # 1: if (m > 0 and + # 2: n < 3): + # 3: print('foo') + # 4: value = 'bar' + # + # The first block would have lineno = 1,and the second block lineno = 4 + start = node.lineno - 1 + + # Special handling of decorators similar to what's above. + if hasattr(node, "decorator_list") and sys.version_info >= (3, 8): + # Using getattr instead of node.decorator_list or pyright will complain about an unknown member. + start -= len(getattr(node, "decorator_list")) # noqa: B009 + block = "\n".join(lines[start:end]) + + # If the block is multiline, add an extra newline character at its end. + # This way, when joining blocks back together, there will be a blank line between each multiline statement + # and no blank lines between single-line statements, or it would look like this: + # >>> x = 22 + # >>> + # >>> total = x + 30 + # >>> + # Note that for the multiline parentheses case this newline is redundant, + # since the closing parenthesis terminates the statement already. + # This means that for this pattern we'll end up with: + # >>> x = [ + # ... 1 + # ... ] + # >>> + # >>> y = [ + # ... 2 + # ...] + if end - start > 1: + block += "\n" + + yield block + + +def normalize_lines(selection): + """ + Normalize the text selection received from the extension. + + If it is a single line selection, dedent it and append a newline and + send it back to the extension. + Otherwise, sanitize the multiline selection before returning it: + split it in a list of top-level statements + and add newlines between each of them so the REPL knows where each block ends. + """ + try: + # Parse the selection into a list of top-level blocks. + # We don't differentiate between single and multiline statements + # because it's not a perf bottleneck, + # and the overhead from splitting and rejoining strings in the multiline case is one-off. + statements = _get_statements(selection) + + # Insert a newline between each top-level statement, and append a newline to the selection. + source = "\n".join(statements) + "\n" + # If selection ends with trailing dictionary or list, remove last unnecessary newline. + if selection[-2] == "}" or selection[-2] == "]": + source = source[:-1] + # If the selection contains trailing return dictionary, insert newline to trigger execute. + if check_end_with_return_dict(selection): + source = source + "\n" + except Exception: + # If there's a problem when parsing statements, + # append a blank line to end the block and send it as-is. + source = selection + "\n\n" + + return source + + +top_level_nodes = [] +min_key = None + + +def check_end_with_return_dict(code): + stripped_code = code.strip() + return stripped_code.endswith("}") and "return {" in stripped_code.strip() + + +def check_exact_exist(top_level_nodes, start_line, end_line): + return [ + node + for node in top_level_nodes + if node.lineno == start_line and node.end_lineno == end_line + ] + + +def traverse_file(whole_file_content, start_line, end_line, was_highlighted): # noqa: ARG001 + """Intended to traverse through a user's given file content and find, collect all appropriate lines that should be sent to the REPL in case of smart selection. + + This could be exact statement such as just a single line print statement, + or a multiline dictionary, or differently styled multi-line list comprehension, etc. + Then call the normalize_lines function to normalize our smartly selected code block. + """ + parsed_file_content = None + + try: + parsed_file_content = ast.parse(whole_file_content) + except Exception: + # Handle case where user is attempting to run code where file contains deprecated Python code. + # Let typescript side know and show warning message. + return { + "normalized_smart_result": "deprecated", + "which_line_next": 0, + } + + smart_code = "" + should_run_top_blocks = [] + + # Purpose of this loop is to fetch and collect all the + # AST top level nodes, and its node.body as child nodes. + # Individual nodes will contain information like + # the start line, end line and get source segment information + # that will be used to smartly select, and send normalized code. + for node in ast.iter_child_nodes(parsed_file_content): + top_level_nodes.append(node) + + ast_types_with_nodebody = ( + ast.Module, + ast.Interactive, + ast.Expression, + ast.FunctionDef, + ast.AsyncFunctionDef, + ast.ClassDef, + ast.For, + ast.AsyncFor, + ast.While, + ast.If, + ast.With, + ast.AsyncWith, + ast.Try, + ast.Lambda, + ast.IfExp, + ast.ExceptHandler, + ) + if isinstance(node, ast_types_with_nodebody) and isinstance(node.body, Iterable): + top_level_nodes.extend(node.body) + + exact_nodes = check_exact_exist(top_level_nodes, start_line, end_line) + + # Just return the exact top level line, if present. + if len(exact_nodes) > 0: + which_line_next = 0 + for same_line_node in exact_nodes: + should_run_top_blocks.append(same_line_node) + smart_code += f"{ast.get_source_segment(whole_file_content, same_line_node)}\n" + which_line_next = get_next_block_lineno(should_run_top_blocks) + return { + "normalized_smart_result": smart_code, + "which_line_next": which_line_next, + } + + # For each of the nodes in the parsed file content, + # add the appropriate source code line(s) to be sent to the REPL, dependent on + # user is trying to send and execute single line/statement or multiple with smart selection. + for top_node in ast.iter_child_nodes(parsed_file_content): + if start_line == top_node.lineno and end_line == top_node.end_lineno: + should_run_top_blocks.append(top_node) + + smart_code += f"{ast.get_source_segment(whole_file_content, top_node)}\n" + break # If we found exact match, don't waste computation in parsing extra nodes. + elif start_line >= top_node.lineno and end_line <= top_node.end_lineno: + # Case to apply smart selection for multiple line. + # This is the case for when we have to add multiple lines that should be included in the smart send. + # For example: + # 'my_dictionary': { + # 'Audi': 'Germany', + # 'BMW': 'Germany', + # 'Genesis': 'Korea', + # } + # with the mouse cursor at 'BMW': 'Germany', should send all of the lines that pertains to my_dictionary. + + should_run_top_blocks.append(top_node) + + smart_code += str(ast.get_source_segment(whole_file_content, top_node)) + smart_code += "\n" + + normalized_smart_result = normalize_lines(smart_code) + which_line_next = get_next_block_lineno(should_run_top_blocks) + return { + "normalized_smart_result": normalized_smart_result, + "which_line_next": which_line_next, + } + + +# Look at the last top block added, find lineno for the next upcoming block, +# This will be used in calculating lineOffset to move cursor in VS Code. +def get_next_block_lineno(which_line_next): + last_ran_lineno = int(which_line_next[-1].end_lineno) + next_lineno = int(which_line_next[-1].end_lineno) + + for reverse_node in top_level_nodes: + if reverse_node.lineno > last_ran_lineno: + next_lineno = reverse_node.lineno + break + return next_lineno + + +if __name__ == "__main__": + # Content is being sent from the extension as a JSON object. + # Decode the data from the raw bytes. + stdin = sys.stdin if sys.version_info < (3,) else sys.stdin.buffer + raw = stdin.read() + contents = json.loads(raw.decode("utf-8")) + # Empty highlight means user has not explicitly selected specific text. + empty_highlight = contents.get("emptyHighlight", False) + + # We also get the activeEditor selection start line and end line from the typescript VS Code side. + # Remember to add 1 to each of the received since vscode starts line counting from 0 . + vscode_start_line = contents["startLine"] + 1 + vscode_end_line = contents["endLine"] + 1 + + # Send the normalized code back to the extension in a JSON object. + data = None + which_line_next = 0 + + if empty_highlight and contents.get("smartSendSettingsEnabled"): + result = traverse_file( + contents["wholeFileContent"], + vscode_start_line, + vscode_end_line, + not empty_highlight, + ) + normalized = result["normalized_smart_result"] + which_line_next = result["which_line_next"] + if normalized == "deprecated": + data = json.dumps( + {"normalized": normalized, "attach_bracket_paste": attach_bracket_paste} + ) + else: + data = json.dumps( + { + "normalized": normalized, + "nextBlockLineno": result["which_line_next"], + "attach_bracket_paste": attach_bracket_paste, + } + ) + else: + normalized = normalize_lines(contents["code"]) + data = json.dumps({"normalized": normalized, "attach_bracket_paste": attach_bracket_paste}) + + stdout = sys.stdout if sys.version_info < (3,) else sys.stdout.buffer + stdout.write(data.encode("utf-8")) + stdout.close() diff --git a/pythonFiles/printEnvVariables.py b/python_files/printEnvVariables.py similarity index 100% rename from pythonFiles/printEnvVariables.py rename to python_files/printEnvVariables.py index 353149f237de..bf2cfd80e666 100644 --- a/pythonFiles/printEnvVariables.py +++ b/python_files/printEnvVariables.py @@ -1,7 +1,7 @@ # Copyright (c) Microsoft Corporation. All rights reserved. # Licensed under the MIT License. -import os import json +import os print(json.dumps(dict(os.environ))) diff --git a/python_files/printEnvVariablesToFile.py b/python_files/printEnvVariablesToFile.py new file mode 100644 index 000000000000..f6013a8c24cf --- /dev/null +++ b/python_files/printEnvVariablesToFile.py @@ -0,0 +1,16 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +import os +import sys + +# Prevent overwriting itself, since sys.argv[0] is the path to this file +if len(sys.argv) > 1: + # Last argument is the target file into which we'll write the env variables line by line. + output_file = sys.argv[-1] +else: + raise ValueError("Missing output file argument") + +with open(output_file, "w") as outfile: # noqa: PTH123 + for key, val in os.environ.items(): # noqa: FURB122 + outfile.write(f"{key}={val}\n") diff --git a/python_files/pyproject.toml b/python_files/pyproject.toml new file mode 100644 index 000000000000..7fb5e18339cb --- /dev/null +++ b/python_files/pyproject.toml @@ -0,0 +1,83 @@ +[tool.pyright] +exclude = ['lib'] +extraPaths = ['lib/python', 'lib/jedilsp'] +ignore = [ + # Ignore all pre-existing code with issues + 'get-pip.py', + 'tensorboard_launcher.py', + 'testlauncher.py', + 'visualstudio_py_testlauncher.py', + 'testing_tools/unittest_discovery.py', + 'testing_tools/adapter/util.py', + 'testing_tools/adapter/pytest/_discovery.py', + 'testing_tools/adapter/pytest/_pytest_item.py', + 'tests/testing_tools/adapter/.data', + 'tests/testing_tools/adapter/test___main__.py', + 'tests/testing_tools/adapter/test_discovery.py', + 'tests/testing_tools/adapter/test_functional.py', + 'tests/testing_tools/adapter/test_report.py', + 'tests/testing_tools/adapter/test_util.py', + 'tests/testing_tools/adapter/pytest/test_cli.py', + 'tests/testing_tools/adapter/pytest/test_discovery.py', +] + +[tool.ruff] +line-length = 100 +target-version = "py38" +exclude = [ + "**/.data", + "lib", +] + +[tool.ruff.format] +docstring-code-format = true + +[tool.ruff.lint] +# Ruff's defaults are F and a subset of E. +# https://docs.astral.sh/ruff/rules/#rules +# Compatible w/ ruff formatter. https://docs.astral.sh/ruff/formatter/#conflicting-lint-rules +# Up-to-date as of Ruff 0.5.0. +select = [ + "A", # flake8-builtins + "ARG", # flake8-unused-argument + "ASYNC", # flake8-async + "B", # flake8-bugbear + "C4", # flake8-comprehensions + "D2", "D400", "D403", "D419", # pydocstyle + "DJ", # flake8-django + "DTZ", # flake8-dasetimez + "E4", "E7", "E9", # pycodestyle (errors) + "EXE", # flake8-executable + "F", # Pyflakes + "FBT", # flake8-boolean-trap + "FLY", # flynt + "FURB", # refurb + "I", # isort + "INP", # flake8-no-pep420 + "INT", # flake8-gettext + "LOG", # flake8-logging + "N", # pep8-naming + "NPY", # NumPy-specific rules + "PD", # pandas-vet + "PERF", # Perflint + "PIE", # flake8-pie + "PTH", # flake8-pathlib + # flake8-pytest-style + "PT006", "PT007", "PT009", "PT012", "PT014", "PT015", "PT016", "PT017", "PT018", "PT019", + "PT020", "PT021", "PT022", "PT024", "PT025", "PT026", "PT027", + "PYI", # flake8-pyi + "Q", # flake8-quotes + "RET502", "RET503", "RET504", # flake8-return + "RSE", # flake8-raise + "RUF", # Ruff-specific rules + "SIM", # flake8-simplify + "SLF", # flake8-self + "SLOT", # flake8-slots + "TCH", # flake8-type-checking + "UP", # pyupgrade + "W", # pycodestyle (warnings) + "YTT", # flake8-2020 +] + +[tool.ruff.lint.pydocstyle] +convention = "pep257" diff --git a/python_files/python_server.py b/python_files/python_server.py new file mode 100644 index 000000000000..e7ee92794a21 --- /dev/null +++ b/python_files/python_server.py @@ -0,0 +1,214 @@ +import ast +import contextlib +import io +import json +import sys +import traceback +import uuid +from pathlib import Path +from typing import Dict, List, Optional, Union + +STDIN = sys.stdin +STDOUT = sys.stdout +STDERR = sys.stderr +USER_GLOBALS = {} + + +def _send_message(msg: str): + # Content-Length is the data size in bytes. + length_msg = len(msg.encode()) + STDOUT.buffer.write(f"Content-Length: {length_msg}\r\n\r\n{msg}".encode()) + STDOUT.buffer.flush() + + +def send_message(**kwargs): + _send_message(json.dumps({"jsonrpc": "2.0", **kwargs})) + + +def print_log(msg: str): + send_message(method="log", params=msg) + + +def send_response( + response: str, + response_id: int, + execution_status: bool = True, # noqa: FBT001, FBT002 +): + send_message( + id=response_id, + result={"status": execution_status, "output": response}, + ) + + +def send_request(params: Optional[Union[List, Dict]] = None): + request_id = uuid.uuid4().hex + if params is None: + send_message(id=request_id, method="input") + else: + send_message(id=request_id, method="input", params=params) + + return request_id + + +original_input = input + + +def custom_input(prompt=""): + try: + send_request({"prompt": prompt}) + headers = get_headers() + # Content-Length is the data size in bytes. + content_length = int(headers.get("Content-Length", 0)) + + if content_length: + message_text = STDIN.buffer.read(content_length).decode() + message_json = json.loads(message_text) + return message_json["result"]["userInput"] + except EOFError: + # Input stream closed, exit gracefully + sys.exit(0) + except Exception: + print_log(traceback.format_exc()) + + +# Set input to our custom input +USER_GLOBALS["input"] = custom_input +input = custom_input # noqa: A001 + + +def handle_response(request_id): + while True: + try: + headers = get_headers() + # Content-Length is the data size in bytes. + content_length = int(headers.get("Content-Length", 0)) + + if content_length: + message_text = STDIN.buffer.read(content_length).decode() + message_json = json.loads(message_text) + our_user_input = message_json["result"]["userInput"] + if message_json["id"] == request_id: + send_response(our_user_input, message_json["id"]) + elif message_json["method"] == "exit": + sys.exit(0) + except EOFError: # noqa: PERF203 + # Input stream closed, exit gracefully + sys.exit(0) + except Exception: + print_log(traceback.format_exc()) + + +def exec_function(user_input): + try: + compile(user_input, "", "eval") + except SyntaxError: + return exec + return eval + + +def check_valid_command(request): + try: + user_input = request["params"] + ast.parse(user_input[0]) + send_response("True", request["id"]) + except SyntaxError: + send_response("False", request["id"]) + + +def execute(request, user_globals): + str_output = CustomIO("", encoding="utf-8") + str_error = CustomIO("", encoding="utf-8") + str_input = CustomIO("", encoding="utf-8", newline="\n") + + with contextlib.redirect_stdout(str_output), contextlib.redirect_stderr(str_error): + original_stdin = sys.stdin + try: + sys.stdin = str_input + execution_status = exec_user_input(request["params"], user_globals) + finally: + sys.stdin = original_stdin + + send_response(str_output.get_value(), request["id"], execution_status) + + +def exec_user_input(user_input, user_globals) -> bool: + user_input = user_input[0] if isinstance(user_input, list) else user_input + + try: + callable_ = exec_function(user_input) + retval = callable_(user_input, user_globals) + if retval is not None: + print(retval) + return True + except KeyboardInterrupt: + print(traceback.format_exc()) + return False + except Exception: + print(traceback.format_exc()) + return False + + +class CustomIO(io.TextIOWrapper): + """Custom stream object to replace stdio.""" + + def __init__(self, name, encoding="utf-8", newline=None): + self._buffer = io.BytesIO() + self._custom_name = name + super().__init__(self._buffer, encoding=encoding, newline=newline) + + def close(self): + """Provide this close method which is used by some tools.""" + # This is intentionally empty. + + def get_value(self) -> str: + """Returns value from the buffer as string.""" + self.seek(0) + return self.read() + + +def get_headers(): + headers = {} + while True: + raw = STDIN.buffer.readline() + # Detect EOF: readline() returns empty bytes when input stream is closed + if raw == b"": + raise EOFError("EOF reached while reading headers") + line = raw.decode().strip() + if not line: + break + name, value = line.split(":", 1) + headers[name] = value.strip() + return headers + + +if __name__ == "__main__": + # https://docs.python.org/3/tutorial/modules.html#the-module-search-path + # The directory containing the input script (or the current directory when no file is specified). + # Here we emulate the same behavior like no file is specified. + input_script_dir = Path(__file__).parent + script_dir_str = str(input_script_dir) + if script_dir_str in sys.path: + sys.path.remove(script_dir_str) + while "" in sys.path: + sys.path.remove("") + sys.path.insert(0, "") + while True: + try: + headers = get_headers() + # Content-Length is the data size in bytes. + content_length = int(headers.get("Content-Length", 0)) + + if content_length: + request_text = STDIN.buffer.read(content_length).decode() + request_json = json.loads(request_text) + if request_json["method"] == "execute": + execute(request_json, USER_GLOBALS) + if request_json["method"] == "check_valid_command": + check_valid_command(request_json) + elif request_json["method"] == "exit": + sys.exit(0) + except EOFError: # noqa: PERF203 + # Input stream closed (VS Code terminated), exit gracefully + sys.exit(0) + except Exception: + print_log(traceback.format_exc()) diff --git a/python_files/pythonrc.py b/python_files/pythonrc.py new file mode 100644 index 000000000000..3042ffb7a309 --- /dev/null +++ b/python_files/pythonrc.py @@ -0,0 +1,88 @@ +import platform +import sys + +if sys.platform != "win32": + import readline + +original_ps1 = ">>> " +is_wsl = "microsoft-standard-WSL" in platform.release() + + +class REPLHooks: + def __init__(self): + self.global_exit = None + self.failure_flag = False + self.original_excepthook = sys.excepthook + self.original_displayhook = sys.displayhook + sys.excepthook = self.my_excepthook + sys.displayhook = self.my_displayhook + + def my_displayhook(self, value): + if value is None: + self.failure_flag = False + + self.original_displayhook(value) + + def my_excepthook(self, type_, value, traceback): + self.global_exit = value + self.failure_flag = True + + self.original_excepthook(type_, value, traceback) + + +def get_last_command(): + # Get the last history item + last_command = "" + if sys.platform != "win32": + last_command = readline.get_history_item(readline.get_current_history_length()) + + return last_command + + +class PS1: + hooks = REPLHooks() + sys.excepthook = hooks.my_excepthook + sys.displayhook = hooks.my_displayhook + + # str will get called for every prompt with exit code to show success/failure + def __str__(self): + exit_code = int(bool(self.hooks.failure_flag)) + self.hooks.failure_flag = False + # Guide following official VS Code doc for shell integration sequence: + result = "" + # For non-windows allow recent_command history. + if sys.platform != "win32": + result = "{soh}{command_executed}{command_line}{command_finished}{prompt_started}{stx}{prompt}{soh}{command_start}{stx}".format( + soh="\001", + stx="\002", + command_executed="\x1b]633;C\x07", + command_line="\x1b]633;E;" + str(get_last_command()) + "\x07", + command_finished="\x1b]633;D;" + str(exit_code) + "\x07", + prompt_started="\x1b]633;A\x07", + prompt=original_ps1, + command_start="\x1b]633;B\x07", + ) + else: + result = "{command_finished}{prompt_started}{prompt}{command_start}{command_executed}".format( + command_finished="\x1b]633;D;" + str(exit_code) + "\x07", + prompt_started="\x1b]633;A\x07", + prompt=original_ps1, + command_start="\x1b]633;B\x07", + command_executed="\x1b]633;C\x07", + ) + + # result = f"{chr(27)}]633;D;{exit_code}{chr(7)}{chr(27)}]633;A{chr(7)}{original_ps1}{chr(27)}]633;B{chr(7)}{chr(27)}]633;C{chr(7)}" + + return result + + def __repr__(self): + return "" + + +if sys.platform != "win32" and (not is_wsl): + sys.ps1 = PS1() + +if sys.platform == "darwin": + print("Cmd click to launch VS Code Native REPL") +else: + print("Ctrl click to launch VS Code Native REPL") diff --git a/python_files/run-jedi-language-server.py b/python_files/run-jedi-language-server.py new file mode 100644 index 000000000000..47bf503d596c --- /dev/null +++ b/python_files/run-jedi-language-server.py @@ -0,0 +1,14 @@ +import os +import pathlib +import sys + +# Add the lib path to our sys path so jedi_language_server can find its references +extension_dir = pathlib.Path(__file__).parent.parent +EXTENSION_ROOT = os.fsdecode(extension_dir) +sys.path.insert(0, os.fsdecode(extension_dir / "python_files" / "lib" / "jedilsp")) +del extension_dir + + +from jedi_language_server.cli import cli # noqa: E402 + +sys.exit(cli()) diff --git a/pythonFiles/shell_exec.py b/python_files/shell_exec.py similarity index 87% rename from pythonFiles/shell_exec.py rename to python_files/shell_exec.py index c521586ca31b..62b6b28af6cd 100644 --- a/pythonFiles/shell_exec.py +++ b/python_files/shell_exec.py @@ -1,9 +1,8 @@ # Copyright (c) Microsoft Corporation. All rights reserved. # Licensed under the MIT License. -import os -import sys import subprocess +import sys # This is a simple solution to waiting for completion of commands sent to terminal. # 1. Intercept commands send to a terminal @@ -17,7 +16,7 @@ print("Executing command in shell >> " + " ".join(shell_args)) -with open(lock_file, "w") as fp: +with open(lock_file, "w") as fp: # noqa: PTH123 try: # Signal start of execution. fp.write("START\n") @@ -37,7 +36,7 @@ fp.flush() try: # ALso log the error for use from the other side. - with open(lock_file + ".error", "w") as fpError: - fpError.write(traceback.format_exc()) + with open(lock_file + ".error", "w") as fp_error: # noqa: PTH123 + fp_error.write(traceback.format_exc()) except Exception: pass diff --git a/pythonFiles/tensorboard_launcher.py b/python_files/tensorboard_launcher.py similarity index 78% rename from pythonFiles/tensorboard_launcher.py rename to python_files/tensorboard_launcher.py index bad1ef09fc6e..a04d51e7eb74 100644 --- a/pythonFiles/tensorboard_launcher.py +++ b/python_files/tensorboard_launcher.py @@ -1,7 +1,9 @@ -import time -import sys -import os +import contextlib import mimetypes +import os +import sys +import time + from tensorboard import program @@ -17,14 +19,12 @@ def main(logdir): tb = program.TensorBoard() tb.configure(bind_all=False, logdir=logdir) url = tb.launch() - sys.stdout.write("TensorBoard started at %s\n" % (url)) + sys.stdout.write(f"TensorBoard started at {url}\n") sys.stdout.flush() - while True: - try: + with contextlib.suppress(KeyboardInterrupt): + while True: time.sleep(60) - except KeyboardInterrupt: - break sys.stdout.write("TensorBoard is shutting down") sys.stdout.flush() @@ -32,5 +32,5 @@ def main(logdir): if __name__ == "__main__": if len(sys.argv) == 2: logdir = str(sys.argv[1]) - sys.stdout.write("Starting TensorBoard with logdir %s" % (logdir)) + sys.stdout.write(f"Starting TensorBoard with logdir {logdir}") main(logdir) diff --git a/pythonFiles/testing_tools/__init__.py b/python_files/testing_tools/__init__.py similarity index 100% rename from pythonFiles/testing_tools/__init__.py rename to python_files/testing_tools/__init__.py diff --git a/python_files/testing_tools/socket_manager.py b/python_files/testing_tools/socket_manager.py new file mode 100644 index 000000000000..f143ac111cdb --- /dev/null +++ b/python_files/testing_tools/socket_manager.py @@ -0,0 +1,95 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +import contextlib +import socket +import sys + +# set the socket before it gets blocked or overwritten by a user tests +_SOCKET = socket.socket + + +class PipeManager: + def __init__(self, name): + self.name = name + + def __enter__(self): + return self.connect() + + def __exit__(self, *_): + self.close() + + def connect(self): + self._writer = open(self.name, "w", encoding="utf-8") # noqa: SIM115, PTH123 + # reader created in read method + return self + + def close(self): + self._writer.close() + if hasattr(self, "_reader"): + self._reader.close() + + def write(self, data: str): + try: + # for windows, is should only use \n\n + request = f"""content-length: {len(data)}\ncontent-type: application/json\n\n{data}""" + self._writer.write(request) + self._writer.flush() + except Exception as e: + print("error attempting to write to pipe", e) + raise (e) + + def read(self, bufsize=1024) -> str: + """Read data from the socket. + + Args: + bufsize (int): Number of bytes to read from the socket. + + Returns: + data (str): Data received from the socket. + """ + # returns a string automatically from read + if not hasattr(self, "_reader"): + self._reader = open(self.name, encoding="utf-8") # noqa: SIM115, PTH123 + return self._reader.read(bufsize) + + +class SocketManager: + """Create a socket and connect to the given address. + + The address is a (host: str, port: int) tuple. + Example usage: + + ``` + with SocketManager(("localhost", 6767)) as sock: + request = json.dumps(payload) + result = s.socket.sendall(request.encode("utf-8")) + ``` + """ + + def __init__(self, addr): + self.addr = addr + self.socket = None + + def __enter__(self): + return self.connect() + + def __exit__(self, *_): + self.close() + + def connect(self): + self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM, socket.IPPROTO_TCP) + if sys.platform == "win32": + addr_use = socket.SO_EXCLUSIVEADDRUSE + else: + addr_use = socket.SO_REUSEADDR + self.socket.setsockopt(socket.SOL_SOCKET, addr_use, 1) + self.socket.connect(self.addr) + + return self + + def close(self): + if self.socket: + with contextlib.suppress(Exception): + self.socket.shutdown(socket.SHUT_RDWR) + self.socket.close() diff --git a/pythonFiles/testlauncher.py b/python_files/testlauncher.py similarity index 71% rename from pythonFiles/testlauncher.py rename to python_files/testlauncher.py index 3278815b380c..2309a203363b 100644 --- a/pythonFiles/testlauncher.py +++ b/python_files/testlauncher.py @@ -7,30 +7,31 @@ def parse_argv(): """Parses arguments for use with the test launcher. + Arguments are: 1. Working directory. 2. Test runner `pytest` 3. Rest of the arguments are passed into the test runner. """ cwd = sys.argv[1] - testRunner = sys.argv[2] + test_runner = sys.argv[2] args = sys.argv[3:] - return (cwd, testRunner, args) + return (cwd, test_runner, args) + +def run(cwd, test_runner, args): + """Runs the test. -def run(cwd, testRunner, args): - """Runs the test cwd -- the current directory to be set testRunner -- test runner to be used `pytest` args -- arguments passed into the test runner """ - - sys.path[0] = os.getcwd() + sys.path[0] = os.getcwd() # noqa: PTH109 os.chdir(cwd) try: - if testRunner == "pytest": + if test_runner == "pytest": import pytest pytest.main(args) @@ -40,5 +41,5 @@ def run(cwd, testRunner, args): if __name__ == "__main__": - cwd, testRunner, args = parse_argv() - run(cwd, testRunner, args) + cwd, test_runner, args = parse_argv() + run(cwd, test_runner, args) diff --git a/pythonFiles/tests/__init__.py b/python_files/tests/__init__.py similarity index 93% rename from pythonFiles/tests/__init__.py rename to python_files/tests/__init__.py index 4f762cd1f81a..86bc29ff33e8 100644 --- a/pythonFiles/tests/__init__.py +++ b/python_files/tests/__init__.py @@ -1,5 +1,6 @@ # Copyright (c) Microsoft Corporation. All rights reserved. # Licensed under the MIT License. +# ruff:noqa: PTH118, PTH120 import os.path TEST_ROOT = os.path.dirname(__file__) diff --git a/pythonFiles/tests/__main__.py b/python_files/tests/__main__.py similarity index 86% rename from pythonFiles/tests/__main__.py rename to python_files/tests/__main__.py index 901385d41d87..2595fce358e4 100644 --- a/pythonFiles/tests/__main__.py +++ b/python_files/tests/__main__.py @@ -12,9 +12,7 @@ def parse_args(): parser = argparse.ArgumentParser() # To mark a test as functional: (decorator) @pytest.mark.functional - parser.add_argument( - "--functional", dest="markers", action="append_const", const="functional" - ) + parser.add_argument("--functional", dest="markers", action="append_const", const="functional") parser.add_argument( "--no-functional", dest="markers", action="append_const", const="not functional" ) @@ -36,7 +34,7 @@ def parse_args(): return ns, remainder -def main(pytestargs, markers=None, specific=False): +def main(pytestargs, markers=None, specific=False): # noqa: FBT002 sys.path.insert(1, TESTING_TOOLS_ROOT) sys.path.insert(1, DEBUG_ADAPTER_ROOT) @@ -48,8 +46,7 @@ def main(pytestargs, markers=None, specific=False): pytestargs.insert(0, marker) pytestargs.insert(0, "-m") - ec = pytest.main(pytestargs) - return ec + return pytest.main(pytestargs) if __name__ == "__main__": diff --git a/python_files/tests/pytestadapter/.data/2496-black-formatter/app.py b/python_files/tests/pytestadapter/.data/2496-black-formatter/app.py new file mode 100644 index 000000000000..3b474e9d911e --- /dev/null +++ b/python_files/tests/pytestadapter/.data/2496-black-formatter/app.py @@ -0,0 +1,6 @@ +def add(a, b): + return a + b + + +def subtract(a, b): + return a - b diff --git a/python_files/tests/pytestadapter/.data/2496-black-formatter/test_app.py b/python_files/tests/pytestadapter/.data/2496-black-formatter/test_app.py new file mode 100644 index 000000000000..ef4398feb786 --- /dev/null +++ b/python_files/tests/pytestadapter/.data/2496-black-formatter/test_app.py @@ -0,0 +1,14 @@ +import pytest +from app import add, subtract + + +def test_add(): # test_marker--test_add + assert add(2, 3) == 5 + assert add(-1, 1) == 0 + assert add(0, 0) == 0 + + +def test_subtract(): # test_marker--test_subtract + assert subtract(5, 3) == 2 + assert subtract(0, 0) == 0 + assert subtract(-1, -1) == 0 diff --git a/python_files/tests/pytestadapter/.data/config_sub_folder/config/pytest.ini b/python_files/tests/pytestadapter/.data/config_sub_folder/config/pytest.ini new file mode 100644 index 000000000000..dfac39a723e8 --- /dev/null +++ b/python_files/tests/pytestadapter/.data/config_sub_folder/config/pytest.ini @@ -0,0 +1,8 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +[pytest] +python_files = + test_*.py +testpaths = + tests diff --git a/python_files/tests/pytestadapter/.data/config_sub_folder/tests/test_hello.py b/python_files/tests/pytestadapter/.data/config_sub_folder/tests/test_hello.py new file mode 100644 index 000000000000..2fd5e2b0a309 --- /dev/null +++ b/python_files/tests/pytestadapter/.data/config_sub_folder/tests/test_hello.py @@ -0,0 +1,6 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + + +def test_hello(): # test_marker--test_hello + assert True diff --git a/pythonFiles/testing_tools/adapter/__init__.py b/python_files/tests/pytestadapter/.data/coverage_gen/__init__.py similarity index 100% rename from pythonFiles/testing_tools/adapter/__init__.py rename to python_files/tests/pytestadapter/.data/coverage_gen/__init__.py diff --git a/python_files/tests/pytestadapter/.data/coverage_gen/reverse.py b/python_files/tests/pytestadapter/.data/coverage_gen/reverse.py new file mode 100644 index 000000000000..cb6755a3a369 --- /dev/null +++ b/python_files/tests/pytestadapter/.data/coverage_gen/reverse.py @@ -0,0 +1,19 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +def reverse_string(s): + if s is None or s == "": + return "Error: Input is None" + return s[::-1] + +def reverse_sentence(sentence): + if sentence is None or sentence == "": + return "Error: Input is None" + words = sentence.split() + reversed_words = [reverse_string(word) for word in words] + return " ".join(reversed_words) + +# Example usage +if __name__ == "__main__": + sample_string = "hello" + print(reverse_string(sample_string)) # Output: "olleh" diff --git a/python_files/tests/pytestadapter/.data/coverage_gen/test_reverse.py b/python_files/tests/pytestadapter/.data/coverage_gen/test_reverse.py new file mode 100644 index 000000000000..e7319f143608 --- /dev/null +++ b/python_files/tests/pytestadapter/.data/coverage_gen/test_reverse.py @@ -0,0 +1,28 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +from .reverse import reverse_sentence, reverse_string + + +def test_reverse_sentence(): + """ + Tests the reverse_sentence function to ensure it correctly reverses each word in a sentence. + + Test cases: + - "hello world" should be reversed to "olleh dlrow" + - "Python is fun" should be reversed to "nohtyP si nuf" + - "a b c" should remain "a b c" as each character is a single word + """ + assert reverse_sentence("hello world") == "olleh dlrow" + assert reverse_sentence("Python is fun") == "nohtyP si nuf" + assert reverse_sentence("a b c") == "a b c" + +def test_reverse_sentence_error(): + assert reverse_sentence("") == "Error: Input is None" + assert reverse_sentence(None) == "Error: Input is None" + + +def test_reverse_string(): + assert reverse_string("hello") == "olleh" + assert reverse_string("Python") == "nohtyP" + # this test specifically does not cover the error cases diff --git a/python_files/tests/pytestadapter/.data/coverage_w_config/pyproject.toml b/python_files/tests/pytestadapter/.data/coverage_w_config/pyproject.toml new file mode 100644 index 000000000000..c3406cc68929 --- /dev/null +++ b/python_files/tests/pytestadapter/.data/coverage_w_config/pyproject.toml @@ -0,0 +1,5 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +[tool.coverage.report] +omit = ["test_ignore.py", "tests/*.py"] diff --git a/python_files/tests/pytestadapter/.data/coverage_w_config/test_ignore.py b/python_files/tests/pytestadapter/.data/coverage_w_config/test_ignore.py new file mode 100644 index 000000000000..98640e336ab4 --- /dev/null +++ b/python_files/tests/pytestadapter/.data/coverage_w_config/test_ignore.py @@ -0,0 +1,5 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +def test_to_ignore(): + assert True diff --git a/python_files/tests/pytestadapter/.data/coverage_w_config/test_ran.py b/python_files/tests/pytestadapter/.data/coverage_w_config/test_ran.py new file mode 100644 index 000000000000..864acec79ba2 --- /dev/null +++ b/python_files/tests/pytestadapter/.data/coverage_w_config/test_ran.py @@ -0,0 +1,9 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +def test_simple(): + assert True + + +def untouched_function(): + return 1 diff --git a/python_files/tests/pytestadapter/.data/coverage_w_config/tests/test_disregard.py b/python_files/tests/pytestadapter/.data/coverage_w_config/tests/test_disregard.py new file mode 100644 index 000000000000..110a11534171 --- /dev/null +++ b/python_files/tests/pytestadapter/.data/coverage_w_config/tests/test_disregard.py @@ -0,0 +1,5 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +def test_i_hope_this_is_ignored(): + assert True diff --git a/pythonFiles/tests/pytestadapter/.data/dual_level_nested_folder/nested_folder_one/test_bottom_folder.py b/python_files/tests/pytestadapter/.data/dual_level_nested_folder/nested_folder_one/test_bottom_folder.py similarity index 100% rename from pythonFiles/tests/pytestadapter/.data/dual_level_nested_folder/nested_folder_one/test_bottom_folder.py rename to python_files/tests/pytestadapter/.data/dual_level_nested_folder/nested_folder_one/test_bottom_folder.py diff --git a/pythonFiles/tests/pytestadapter/.data/dual_level_nested_folder/test_top_folder.py b/python_files/tests/pytestadapter/.data/dual_level_nested_folder/test_top_folder.py similarity index 100% rename from pythonFiles/tests/pytestadapter/.data/dual_level_nested_folder/test_top_folder.py rename to python_files/tests/pytestadapter/.data/dual_level_nested_folder/test_top_folder.py diff --git a/pythonFiles/tests/pytestadapter/.data/empty_discovery.py b/python_files/tests/pytestadapter/.data/empty_discovery.py similarity index 100% rename from pythonFiles/tests/pytestadapter/.data/empty_discovery.py rename to python_files/tests/pytestadapter/.data/empty_discovery.py diff --git a/pythonFiles/tests/pytestadapter/.data/error_parametrize_discovery.py b/python_files/tests/pytestadapter/.data/error_parametrize_discovery.py similarity index 100% rename from pythonFiles/tests/pytestadapter/.data/error_parametrize_discovery.py rename to python_files/tests/pytestadapter/.data/error_parametrize_discovery.py diff --git a/python_files/tests/pytestadapter/.data/error_pytest_import.txt b/python_files/tests/pytestadapter/.data/error_pytest_import.txt new file mode 100644 index 000000000000..7d65dee2ccc6 --- /dev/null +++ b/python_files/tests/pytestadapter/.data/error_pytest_import.txt @@ -0,0 +1,6 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +@pytest.mark.parametrize("num", range(1, 89)) +def test_odd_even(num): + assert True diff --git a/python_files/tests/pytestadapter/.data/error_raise_exception.py b/python_files/tests/pytestadapter/.data/error_raise_exception.py new file mode 100644 index 000000000000..2506089abe07 --- /dev/null +++ b/python_files/tests/pytestadapter/.data/error_raise_exception.py @@ -0,0 +1,14 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +import pytest + + +@pytest.fixture +def raise_fixture(): + raise Exception("Dummy exception") + + +class TestSomething: + def test_a(self, raise_fixture): + assert True diff --git a/pythonFiles/tests/pytestadapter/.data/error_syntax_discovery.txt b/python_files/tests/pytestadapter/.data/error_syntax_discovery.txt similarity index 100% rename from pythonFiles/tests/pytestadapter/.data/error_syntax_discovery.txt rename to python_files/tests/pytestadapter/.data/error_syntax_discovery.txt diff --git a/pythonFiles/tests/pytestadapter/.data/double_nested_folder/nested_folder_one/nested_folder_two/test_nest.py b/python_files/tests/pytestadapter/.data/folder_a/folder_b/folder_a/test_nest.py similarity index 100% rename from pythonFiles/tests/pytestadapter/.data/double_nested_folder/nested_folder_one/nested_folder_two/test_nest.py rename to python_files/tests/pytestadapter/.data/folder_a/folder_b/folder_a/test_nest.py diff --git a/python_files/tests/pytestadapter/.data/folder_with_script/script_random.py b/python_files/tests/pytestadapter/.data/folder_with_script/script_random.py new file mode 100644 index 000000000000..d8c32027a9e6 --- /dev/null +++ b/python_files/tests/pytestadapter/.data/folder_with_script/script_random.py @@ -0,0 +1,7 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +# This file has no test, it's just a random script. + +if __name__ == "__main__": + print("Hello World!") diff --git a/pythonFiles/tests/pytestadapter/.data/simple_pytest.py b/python_files/tests/pytestadapter/.data/folder_with_script/test_simple.py similarity index 100% rename from pythonFiles/tests/pytestadapter/.data/simple_pytest.py rename to python_files/tests/pytestadapter/.data/folder_with_script/test_simple.py diff --git a/python_files/tests/pytestadapter/.data/param_same_name/test_param1.py b/python_files/tests/pytestadapter/.data/param_same_name/test_param1.py new file mode 100644 index 000000000000..a16d0f49f411 --- /dev/null +++ b/python_files/tests/pytestadapter/.data/param_same_name/test_param1.py @@ -0,0 +1,8 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. +import pytest + + +@pytest.mark.parametrize("num", ["a", "b", "c"]) +def test_odd_even(num): + assert True diff --git a/python_files/tests/pytestadapter/.data/param_same_name/test_param2.py b/python_files/tests/pytestadapter/.data/param_same_name/test_param2.py new file mode 100644 index 000000000000..c0ea8010e359 --- /dev/null +++ b/python_files/tests/pytestadapter/.data/param_same_name/test_param2.py @@ -0,0 +1,8 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. +import pytest + + +@pytest.mark.parametrize("num", range(1, 4)) +def test_odd_even(num): + assert True diff --git a/python_files/tests/pytestadapter/.data/parametrize_tests.py b/python_files/tests/pytestadapter/.data/parametrize_tests.py new file mode 100644 index 000000000000..34d3c4201f0f --- /dev/null +++ b/python_files/tests/pytestadapter/.data/parametrize_tests.py @@ -0,0 +1,23 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +import pytest + + +class TestClass: + # Testing pytest with parametrized tests. The first two pass, the third fails. + # The tests ids are parametrize_tests.py::test_adding[3+5-8] and so on. + @pytest.mark.parametrize( # test_marker--test_adding + "actual, expected", [("3+5", 8), ("2+4", 6), ("6+9", 16)] + ) + def test_adding(self, actual, expected): + assert eval(actual) == expected + + +# Testing pytest with parametrized tests. All three pass. +# The tests ids are parametrize_tests.py::test_under_ten[1] and so on. +@pytest.mark.parametrize( # test_marker--test_string + "string", ["hello", "complicated split [] ()"] +) +def test_string(string): + assert string == "hello" diff --git a/python_files/tests/pytestadapter/.data/pytest.ini b/python_files/tests/pytestadapter/.data/pytest.ini new file mode 100644 index 000000000000..ddbcd6544e5d --- /dev/null +++ b/python_files/tests/pytestadapter/.data/pytest.ini @@ -0,0 +1,5 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +# pytest.ini is specified here so the root directory of the tests is kept at .data instead of referencing +# the parent python_files/pyproject.toml for test_discovery.py and test_execution.py for pytest-adapter tests. diff --git a/python_files/tests/pytestadapter/.data/pytest_describe_plugin/describe_only.py b/python_files/tests/pytestadapter/.data/pytest_describe_plugin/describe_only.py new file mode 100644 index 000000000000..0702c032684b --- /dev/null +++ b/python_files/tests/pytestadapter/.data/pytest_describe_plugin/describe_only.py @@ -0,0 +1,9 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +def describe_A(): + def test_1(): # test_marker--test_1 + pass + + def test_2(): # test_marker--test_2 + pass diff --git a/python_files/tests/pytestadapter/.data/pytest_describe_plugin/nested_describe.py b/python_files/tests/pytestadapter/.data/pytest_describe_plugin/nested_describe.py new file mode 100644 index 000000000000..5b9c13cc8d53 --- /dev/null +++ b/python_files/tests/pytestadapter/.data/pytest_describe_plugin/nested_describe.py @@ -0,0 +1,31 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +import pytest + + +def describe_list(): + @pytest.fixture + def list(): + return [] + + def describe_append(): + def add_empty(list): # test_marker--add_empty + list.append("foo") + list.append("bar") + assert list == ["foo", "bar"] + + def remove_empty(list): # test_marker--remove_empty + try: + list.remove("foo") + except ValueError: + pass + + def describe_remove(): + @pytest.fixture + def list(): + return ["foo", "bar"] + + def removes(list): # test_marker--removes + list.remove("foo") + assert list == ["bar"] diff --git a/pythonFiles/tests/testing_tools/adapter/.data/NormCase/tests/A/__init__.py b/python_files/tests/pytestadapter/.data/root/tests/pytest.ini similarity index 100% rename from pythonFiles/tests/testing_tools/adapter/.data/NormCase/tests/A/__init__.py rename to python_files/tests/pytestadapter/.data/root/tests/pytest.ini diff --git a/python_files/tests/pytestadapter/.data/root/tests/test_a.py b/python_files/tests/pytestadapter/.data/root/tests/test_a.py new file mode 100644 index 000000000000..3ec3dd9626cb --- /dev/null +++ b/python_files/tests/pytestadapter/.data/root/tests/test_a.py @@ -0,0 +1,6 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + + +def test_a_function(): # test_marker--test_a_function + assert True diff --git a/python_files/tests/pytestadapter/.data/root/tests/test_b.py b/python_files/tests/pytestadapter/.data/root/tests/test_b.py new file mode 100644 index 000000000000..0d3148641f85 --- /dev/null +++ b/python_files/tests/pytestadapter/.data/root/tests/test_b.py @@ -0,0 +1,6 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + + +def test_b_function(): # test_marker--test_b_function + assert True diff --git a/python_files/tests/pytestadapter/.data/same_function_new_class_param.py b/python_files/tests/pytestadapter/.data/same_function_new_class_param.py new file mode 100644 index 000000000000..6f85051436b8 --- /dev/null +++ b/python_files/tests/pytestadapter/.data/same_function_new_class_param.py @@ -0,0 +1,25 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. +import pytest + + +class TestNotEmpty: + @pytest.mark.parametrize("a, b", [(1, 1), (2, 2)]) # test_marker--TestNotEmpty::test_integer + def test_integer(self, a, b): + assert a == b + + @pytest.mark.parametrize( # test_marker--TestNotEmpty::test_string + "a, b", [("a", "a"), ("b", "b")] + ) + def test_string(self, a, b): + assert a == b + + +class TestEmpty: + @pytest.mark.parametrize("a, b", [(0, 0)]) # test_marker--TestEmpty::test_integer + def test_integer(self, a, b): + assert a == b + + @pytest.mark.parametrize("a, b", [("", "")]) # test_marker--TestEmpty::test_string + def test_string(self, a, b): + assert a == b diff --git a/python_files/tests/pytestadapter/.data/simple_pytest.py b/python_files/tests/pytestadapter/.data/simple_pytest.py new file mode 100644 index 000000000000..9f9bfb014f3d --- /dev/null +++ b/python_files/tests/pytestadapter/.data/simple_pytest.py @@ -0,0 +1,7 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + + +# This test passes. +def test_function(): # test_marker--test_function + assert 1 == 1 diff --git a/python_files/tests/pytestadapter/.data/skip_test_fixture.py b/python_files/tests/pytestadapter/.data/skip_test_fixture.py new file mode 100644 index 000000000000..3d354cae86ea --- /dev/null +++ b/python_files/tests/pytestadapter/.data/skip_test_fixture.py @@ -0,0 +1,19 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +import pytest + + +@pytest.fixture +def docker_client() -> object: + try: + # NOTE: Actually connect with the docker sdk + raise Exception("Docker client not available") + except Exception: + pytest.skip("Docker client not available") + + return object() + + +def test_docker_client(docker_client): + assert False diff --git a/python_files/tests/pytestadapter/.data/skip_tests.py b/python_files/tests/pytestadapter/.data/skip_tests.py new file mode 100644 index 000000000000..871b0e7bf5c3 --- /dev/null +++ b/python_files/tests/pytestadapter/.data/skip_tests.py @@ -0,0 +1,40 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +import pytest + +# Testing pytest with skipped tests. The first passes, the second three are skipped. + + +def test_something(): # test_marker--test_something + # This tests passes successfully. + assert 1 + 1 == 2 + + +def test_another_thing(): # test_marker--test_another_thing + # Skip this test with a reason. + pytest.skip("Skipping this test for now") + + +@pytest.mark.skip( + reason="Skipping this test as it requires additional setup" # test_marker--test_complex_thing +) +def test_decorator_thing(): + # Skip this test as well, with a reason. This one uses a decorator. + assert True + + +@pytest.mark.skipif(1 < 5, reason="is always true") # test_marker--test_complex_thing_2 +def test_decorator_thing_2(): + # Skip this test as well, with a reason. This one uses a decorator with a condition. + assert True + + +# With this test, the entire class is skipped. +@pytest.mark.skip(reason="Skip TestClass") +class TestClass: + def test_class_function_a(self): # test_marker--test_class_function_a + assert True + + def test_class_function_b(self): # test_marker--test_class_function_b + assert False diff --git a/python_files/tests/pytestadapter/.data/test_env_vars.py b/python_files/tests/pytestadapter/.data/test_env_vars.py new file mode 100644 index 000000000000..c8a3add56763 --- /dev/null +++ b/python_files/tests/pytestadapter/.data/test_env_vars.py @@ -0,0 +1,32 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +import os + + +def test_clear_env(monkeypatch): + # Clear all environment variables + monkeypatch.setattr(os, "environ", {}) + + # Now os.environ should be empty + assert not os.environ + + # After the test finishes, the environment variables will be reset to their original state + + +def test_check_env(): + # This test will have access to the original environment variables + assert "PATH" in os.environ + + +def test_clear_env_unsafe(): + # Clear all environment variables + os.environ.clear() + # Now os.environ should be empty + assert not os.environ + + +def test_check_env_unsafe(): + # ("PATH" in os.environ) is False here if it runs after test_clear_env_unsafe. + # Regardless, this test will pass and TEST_PORT and TEST_UUID will still be set correctly + assert "PATH" not in os.environ diff --git a/python_files/tests/pytestadapter/.data/test_logging.py b/python_files/tests/pytestadapter/.data/test_logging.py new file mode 100644 index 000000000000..058ad8075718 --- /dev/null +++ b/python_files/tests/pytestadapter/.data/test_logging.py @@ -0,0 +1,35 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. +import logging +import sys + + +def test_logging2(caplog): + logger = logging.getLogger(__name__) + caplog.set_level(logging.DEBUG) # Set minimum log level to capture + + logger.debug("This is a debug message.") + logger.info("This is an info message.") + logger.warning("This is a warning message.") + logger.error("This is an error message.") + logger.critical("This is a critical message.") + + # Printing to stdout and stderr + print("This is a stdout message.") + print("This is a stderr message.", file=sys.stderr) + assert False + + +def test_logging(caplog): + logger = logging.getLogger(__name__) + caplog.set_level(logging.DEBUG) # Set minimum log level to capture + + logger.debug("This is a debug message.") + logger.info("This is an info message.") + logger.warning("This is a warning message.") + logger.error("This is an error message.") + logger.critical("This is a critical message.") + + # Printing to stdout and stderr + print("This is a stdout message.") + print("This is a stderr message.", file=sys.stderr) diff --git a/python_files/tests/pytestadapter/.data/test_multi_class_nest.py b/python_files/tests/pytestadapter/.data/test_multi_class_nest.py new file mode 100644 index 000000000000..209f9d51915b --- /dev/null +++ b/python_files/tests/pytestadapter/.data/test_multi_class_nest.py @@ -0,0 +1,19 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + + +class TestFirstClass: + class TestSecondClass: + def test_second(self): # test_marker--test_second + assert 1 == 2 + + def test_first(self): # test_marker--test_first + assert 1 == 2 + + class TestSecondClass2: + def test_second2(self): # test_marker--test_second2 + assert 1 == 1 + + +def test_independent(): # test_marker--test_independent + assert 1 == 1 diff --git a/python_files/tests/pytestadapter/.data/test_param_span_class.py b/python_files/tests/pytestadapter/.data/test_param_span_class.py new file mode 100644 index 000000000000..a024c438bbf9 --- /dev/null +++ b/python_files/tests/pytestadapter/.data/test_param_span_class.py @@ -0,0 +1,16 @@ +import pytest + + +@pytest.fixture(scope="function", params=[1, 2]) +def setup(request): + return request.param + + +class TestClass1: + def test_method1(self, setup): # test_marker--TestClass1::test_method1 + assert 1 == 1 + + +class TestClass2: + def test_method1(self, setup): # test_marker--TestClass2::test_method1 + assert 2 == 2 diff --git a/pythonFiles/tests/pytestadapter/.data/text_docstring.txt b/python_files/tests/pytestadapter/.data/text_docstring.txt similarity index 100% rename from pythonFiles/tests/pytestadapter/.data/text_docstring.txt rename to python_files/tests/pytestadapter/.data/text_docstring.txt diff --git a/pythonFiles/tests/pytestadapter/.data/unittest_folder/test_add.py b/python_files/tests/pytestadapter/.data/unittest_folder/test_add.py similarity index 66% rename from pythonFiles/tests/pytestadapter/.data/unittest_folder/test_add.py rename to python_files/tests/pytestadapter/.data/unittest_folder/test_add.py index a96c7f2fa392..e9bdda0ad2ad 100644 --- a/pythonFiles/tests/pytestadapter/.data/unittest_folder/test_add.py +++ b/python_files/tests/pytestadapter/.data/unittest_folder/test_add.py @@ -19,3 +19,11 @@ def test_add_positive_numbers(self): # test_marker--test_add_positive_numbers def test_add_negative_numbers(self): # test_marker--test_add_negative_numbers result = add(-2, -3) self.assertEqual(result, -5) + + +class TestDuplicateFunction(unittest.TestCase): + # This test's id is unittest_folder/test_subtract.py::TestDuplicateFunction::test_dup_a. It has the same class name as + # another test, but it's in a different file, so it should not be confused. + # This test passes. + def test_dup_a(self): # test_marker--test_dup_a + self.assertEqual(1, 1) diff --git a/pythonFiles/tests/pytestadapter/.data/unittest_folder/test_subtract.py b/python_files/tests/pytestadapter/.data/unittest_folder/test_subtract.py similarity index 71% rename from pythonFiles/tests/pytestadapter/.data/unittest_folder/test_subtract.py rename to python_files/tests/pytestadapter/.data/unittest_folder/test_subtract.py index 087e5140def4..634a6d81f9eb 100644 --- a/pythonFiles/tests/pytestadapter/.data/unittest_folder/test_subtract.py +++ b/python_files/tests/pytestadapter/.data/unittest_folder/test_subtract.py @@ -24,3 +24,11 @@ def test_subtract_negative_numbers( # test_marker--test_subtract_negative_numbe result = subtract(-2, -3) # This is intentional to test assertion failures self.assertEqual(result, 100000) + + +class TestDuplicateFunction(unittest.TestCase): + # This test's id is unittest_folder/test_subtract.py::TestDuplicateFunction::test_dup_s. It has the same class name as + # another test, but it's in a different file, so it should not be confused. + # This test passes. + def test_dup_s(self): # test_marker--test_dup_s + self.assertEqual(1, 1) diff --git a/pythonFiles/tests/pytestadapter/.data/unittest_pytest_same_file.py b/python_files/tests/pytestadapter/.data/unittest_pytest_same_file.py similarity index 100% rename from pythonFiles/tests/pytestadapter/.data/unittest_pytest_same_file.py rename to python_files/tests/pytestadapter/.data/unittest_pytest_same_file.py diff --git a/python_files/tests/pytestadapter/.data/unittest_skiptest_file_level.py b/python_files/tests/pytestadapter/.data/unittest_skiptest_file_level.py new file mode 100644 index 000000000000..362c74cbb76f --- /dev/null +++ b/python_files/tests/pytestadapter/.data/unittest_skiptest_file_level.py @@ -0,0 +1,13 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +import unittest +from unittest import SkipTest + +# Due to the skip at the file level, no tests will be discovered. +raise SkipTest("Skip all tests in this file, they should not be recognized by pytest.") + + +class SimpleTest(unittest.TestCase): + def testadd1(self): + assert True diff --git a/pythonFiles/tests/debug_adapter/__init__.py b/python_files/tests/pytestadapter/__init__.py similarity index 100% rename from pythonFiles/tests/debug_adapter/__init__.py rename to python_files/tests/pytestadapter/__init__.py diff --git a/python_files/tests/pytestadapter/expected_discovery_test_output.py b/python_files/tests/pytestadapter/expected_discovery_test_output.py new file mode 100644 index 000000000000..047f1c72ad17 --- /dev/null +++ b/python_files/tests/pytestadapter/expected_discovery_test_output.py @@ -0,0 +1,2083 @@ +import os + +from .helpers import ( + TEST_DATA_PATH, + find_class_line_number, + find_test_line_number, + get_absolute_test_id, +) + +# This file contains the expected output dictionaries for tests discovery and is used in test_discovery.py. + +# This is the expected output for the empty_discovery.py file. +# └── +TEST_DATA_PATH_STR = os.fspath(TEST_DATA_PATH) +empty_discovery_pytest_expected_output = { + "name": ".data", + "path": TEST_DATA_PATH_STR, + "type_": "folder", + "children": [], + "id_": TEST_DATA_PATH_STR, +} + +# This is the expected output for the simple_pytest.py file. +# └── simple_pytest.py +# └── test_function +simple_test_file_path = TEST_DATA_PATH / "simple_pytest.py" +simple_discovery_pytest_expected_output = { + "name": ".data", + "path": TEST_DATA_PATH_STR, + "type_": "folder", + "children": [ + { + "name": "simple_pytest.py", + "path": os.fspath(simple_test_file_path), + "type_": "file", + "id_": os.fspath(simple_test_file_path), + "children": [ + { + "name": "test_function", + "path": os.fspath(simple_test_file_path), + "lineno": find_test_line_number( + "test_function", + simple_test_file_path, + ), + "type_": "test", + "id_": get_absolute_test_id( + "simple_pytest.py::test_function", simple_test_file_path + ), + "runID": get_absolute_test_id( + "simple_pytest.py::test_function", simple_test_file_path + ), + } + ], + } + ], + "id_": TEST_DATA_PATH_STR, +} + +# This is the expected output for the unittest_pytest_same_file.py file. +# β”œβ”€β”€ unittest_pytest_same_file.py +# β”œβ”€β”€ TestExample +# β”‚ └── test_true_unittest +# └── test_true_pytest +unit_pytest_same_file_path = TEST_DATA_PATH / "unittest_pytest_same_file.py" +unit_pytest_same_file_discovery_expected_output = { + "name": ".data", + "path": TEST_DATA_PATH_STR, + "type_": "folder", + "children": [ + { + "name": "unittest_pytest_same_file.py", + "path": os.fspath(unit_pytest_same_file_path), + "type_": "file", + "id_": os.fspath(unit_pytest_same_file_path), + "children": [ + { + "name": "TestExample", + "path": os.fspath(unit_pytest_same_file_path), + "type_": "class", + "children": [ + { + "name": "test_true_unittest", + "path": os.fspath(unit_pytest_same_file_path), + "lineno": find_test_line_number( + "test_true_unittest", + os.fspath(unit_pytest_same_file_path), + ), + "type_": "test", + "id_": get_absolute_test_id( + "unittest_pytest_same_file.py::TestExample::test_true_unittest", + unit_pytest_same_file_path, + ), + "runID": get_absolute_test_id( + "unittest_pytest_same_file.py::TestExample::test_true_unittest", + unit_pytest_same_file_path, + ), + } + ], + "id_": get_absolute_test_id( + "unittest_pytest_same_file.py::TestExample", + unit_pytest_same_file_path, + ), + "lineno": find_class_line_number("TestExample", unit_pytest_same_file_path), + }, + { + "name": "test_true_pytest", + "path": os.fspath(unit_pytest_same_file_path), + "lineno": find_test_line_number( + "test_true_pytest", + unit_pytest_same_file_path, + ), + "type_": "test", + "id_": get_absolute_test_id( + "unittest_pytest_same_file.py::test_true_pytest", + unit_pytest_same_file_path, + ), + "runID": get_absolute_test_id( + "unittest_pytest_same_file.py::test_true_pytest", + unit_pytest_same_file_path, + ), + }, + ], + } + ], + "id_": TEST_DATA_PATH_STR, +} + +# This is the expected output for the unittest_skip_file_level test. +# └── unittest_skiptest_file_level.py +unittest_skip_file_level_expected_output = { + "name": ".data", + "path": TEST_DATA_PATH_STR, + "type_": "folder", + "children": [], + "id_": TEST_DATA_PATH_STR, +} + +# This is the expected output for the unittest_folder tests +# └── unittest_folder +# β”œβ”€β”€ test_add.py +# β”‚ └── TestAddFunction +# β”‚ β”œβ”€β”€ test_add_negative_numbers +# β”‚ └── test_add_positive_numbers +# β”‚ └── TestDuplicateFunction +# β”‚ └── test_dup_a +# └── test_subtract.py +# └── TestSubtractFunction +# β”œβ”€β”€ test_subtract_negative_numbers +# └── test_subtract_positive_numbers +# β”‚ └── TestDuplicateFunction +# β”‚ └── test_dup_s +unittest_folder_path = TEST_DATA_PATH / "unittest_folder" +test_add_path = TEST_DATA_PATH / "unittest_folder" / "test_add.py" +test_subtract_path = TEST_DATA_PATH / "unittest_folder" / "test_subtract.py" +unittest_folder_discovery_expected_output = { + "name": ".data", + "path": TEST_DATA_PATH_STR, + "type_": "folder", + "children": [ + { + "name": "unittest_folder", + "path": os.fspath(unittest_folder_path), + "type_": "folder", + "id_": os.fspath(unittest_folder_path), + "children": [ + { + "name": "test_add.py", + "path": os.fspath(test_add_path), + "type_": "file", + "id_": os.fspath(test_add_path), + "children": [ + { + "name": "TestAddFunction", + "path": os.fspath(test_add_path), + "type_": "class", + "children": [ + { + "name": "test_add_negative_numbers", + "path": os.fspath(test_add_path), + "lineno": find_test_line_number( + "test_add_negative_numbers", + os.fspath(test_add_path), + ), + "type_": "test", + "id_": get_absolute_test_id( + "unittest_folder/test_add.py::TestAddFunction::test_add_negative_numbers", + test_add_path, + ), + "runID": get_absolute_test_id( + "unittest_folder/test_add.py::TestAddFunction::test_add_negative_numbers", + test_add_path, + ), + }, + { + "name": "test_add_positive_numbers", + "path": os.fspath(test_add_path), + "lineno": find_test_line_number( + "test_add_positive_numbers", + os.fspath(test_add_path), + ), + "type_": "test", + "id_": get_absolute_test_id( + "unittest_folder/test_add.py::TestAddFunction::test_add_positive_numbers", + test_add_path, + ), + "runID": get_absolute_test_id( + "unittest_folder/test_add.py::TestAddFunction::test_add_positive_numbers", + test_add_path, + ), + }, + ], + "id_": get_absolute_test_id( + "unittest_folder/test_add.py::TestAddFunction", + test_add_path, + ), + "lineno": find_class_line_number("TestAddFunction", test_add_path), + }, + { + "name": "TestDuplicateFunction", + "path": os.fspath(test_add_path), + "type_": "class", + "children": [ + { + "name": "test_dup_a", + "path": os.fspath(test_add_path), + "lineno": find_test_line_number( + "test_dup_a", + os.fspath(test_add_path), + ), + "type_": "test", + "id_": get_absolute_test_id( + "unittest_folder/test_add.py::TestDuplicateFunction::test_dup_a", + test_add_path, + ), + "runID": get_absolute_test_id( + "unittest_folder/test_add.py::TestDuplicateFunction::test_dup_a", + test_add_path, + ), + }, + ], + "id_": get_absolute_test_id( + "unittest_folder/test_add.py::TestDuplicateFunction", + test_add_path, + ), + "lineno": find_class_line_number( + "TestDuplicateFunction", test_add_path + ), + }, + ], + }, + { + "name": "test_subtract.py", + "path": os.fspath(test_subtract_path), + "type_": "file", + "id_": os.fspath(test_subtract_path), + "children": [ + { + "name": "TestSubtractFunction", + "path": os.fspath(test_subtract_path), + "type_": "class", + "children": [ + { + "name": "test_subtract_negative_numbers", + "path": os.fspath(test_subtract_path), + "lineno": find_test_line_number( + "test_subtract_negative_numbers", + os.fspath(test_subtract_path), + ), + "type_": "test", + "id_": get_absolute_test_id( + "unittest_folder/test_subtract.py::TestSubtractFunction::test_subtract_negative_numbers", + test_subtract_path, + ), + "runID": get_absolute_test_id( + "unittest_folder/test_subtract.py::TestSubtractFunction::test_subtract_negative_numbers", + test_subtract_path, + ), + }, + { + "name": "test_subtract_positive_numbers", + "path": os.fspath(test_subtract_path), + "lineno": find_test_line_number( + "test_subtract_positive_numbers", + os.fspath(test_subtract_path), + ), + "type_": "test", + "id_": get_absolute_test_id( + "unittest_folder/test_subtract.py::TestSubtractFunction::test_subtract_positive_numbers", + test_subtract_path, + ), + "runID": get_absolute_test_id( + "unittest_folder/test_subtract.py::TestSubtractFunction::test_subtract_positive_numbers", + test_subtract_path, + ), + }, + ], + "id_": get_absolute_test_id( + "unittest_folder/test_subtract.py::TestSubtractFunction", + test_subtract_path, + ), + "lineno": find_class_line_number( + "TestSubtractFunction", test_subtract_path + ), + }, + { + "name": "TestDuplicateFunction", + "path": os.fspath(test_subtract_path), + "type_": "class", + "children": [ + { + "name": "test_dup_s", + "path": os.fspath(test_subtract_path), + "lineno": find_test_line_number( + "test_dup_s", + os.fspath(test_subtract_path), + ), + "type_": "test", + "id_": get_absolute_test_id( + "unittest_folder/test_subtract.py::TestDuplicateFunction::test_dup_s", + test_subtract_path, + ), + "runID": get_absolute_test_id( + "unittest_folder/test_subtract.py::TestDuplicateFunction::test_dup_s", + test_subtract_path, + ), + }, + ], + "id_": get_absolute_test_id( + "unittest_folder/test_subtract.py::TestDuplicateFunction", + test_subtract_path, + ), + "lineno": find_class_line_number( + "TestDuplicateFunction", test_subtract_path + ), + }, + ], + }, + ], + } + ], + "id_": TEST_DATA_PATH_STR, +} + + +# This is the expected output for the dual_level_nested_folder tests +# └── dual_level_nested_folder +# └── test_top_folder.py +# └── test_top_function_t +# └── test_top_function_f +# └── nested_folder_one +# └── test_bottom_folder.py +# └── test_bottom_function_t +# └── test_bottom_function_f +dual_level_nested_folder_path = TEST_DATA_PATH / "dual_level_nested_folder" +test_top_folder_path = TEST_DATA_PATH / "dual_level_nested_folder" / "test_top_folder.py" + +test_nested_folder_one_path = TEST_DATA_PATH / "dual_level_nested_folder" / "nested_folder_one" + +test_bottom_folder_path = ( + TEST_DATA_PATH / "dual_level_nested_folder" / "nested_folder_one" / "test_bottom_folder.py" +) + + +dual_level_nested_folder_expected_output = { + "name": ".data", + "path": TEST_DATA_PATH_STR, + "type_": "folder", + "children": [ + { + "name": "dual_level_nested_folder", + "path": os.fspath(dual_level_nested_folder_path), + "type_": "folder", + "id_": os.fspath(dual_level_nested_folder_path), + "children": [ + { + "name": "test_top_folder.py", + "path": os.fspath(test_top_folder_path), + "type_": "file", + "id_": os.fspath(test_top_folder_path), + "children": [ + { + "name": "test_top_function_t", + "path": os.fspath(test_top_folder_path), + "lineno": find_test_line_number( + "test_top_function_t", + test_top_folder_path, + ), + "type_": "test", + "id_": get_absolute_test_id( + "dual_level_nested_folder/test_top_folder.py::test_top_function_t", + test_top_folder_path, + ), + "runID": get_absolute_test_id( + "dual_level_nested_folder/test_top_folder.py::test_top_function_t", + test_top_folder_path, + ), + }, + { + "name": "test_top_function_f", + "path": os.fspath(test_top_folder_path), + "lineno": find_test_line_number( + "test_top_function_f", + test_top_folder_path, + ), + "type_": "test", + "id_": get_absolute_test_id( + "dual_level_nested_folder/test_top_folder.py::test_top_function_f", + test_top_folder_path, + ), + "runID": get_absolute_test_id( + "dual_level_nested_folder/test_top_folder.py::test_top_function_f", + test_top_folder_path, + ), + }, + ], + }, + { + "name": "nested_folder_one", + "path": os.fspath(test_nested_folder_one_path), + "type_": "folder", + "id_": os.fspath(test_nested_folder_one_path), + "children": [ + { + "name": "test_bottom_folder.py", + "path": os.fspath(test_bottom_folder_path), + "type_": "file", + "id_": os.fspath(test_bottom_folder_path), + "children": [ + { + "name": "test_bottom_function_t", + "path": os.fspath(test_bottom_folder_path), + "lineno": find_test_line_number( + "test_bottom_function_t", + test_bottom_folder_path, + ), + "type_": "test", + "id_": get_absolute_test_id( + "dual_level_nested_folder/nested_folder_one/test_bottom_folder.py::test_bottom_function_t", + test_bottom_folder_path, + ), + "runID": get_absolute_test_id( + "dual_level_nested_folder/nested_folder_one/test_bottom_folder.py::test_bottom_function_t", + test_bottom_folder_path, + ), + }, + { + "name": "test_bottom_function_f", + "path": os.fspath(test_bottom_folder_path), + "lineno": find_test_line_number( + "test_bottom_function_f", + test_bottom_folder_path, + ), + "type_": "test", + "id_": get_absolute_test_id( + "dual_level_nested_folder/nested_folder_one/test_bottom_folder.py::test_bottom_function_f", + test_bottom_folder_path, + ), + "runID": get_absolute_test_id( + "dual_level_nested_folder/nested_folder_one/test_bottom_folder.py::test_bottom_function_f", + test_bottom_folder_path, + ), + }, + ], + } + ], + }, + ], + } + ], + "id_": TEST_DATA_PATH_STR, +} + +# This is the expected output for the double_nested_folder tests. +# └── folder_a +# └── folder_b +# └── folder_a +# └── test_nest.py +# └── test_function + +folder_a_path = TEST_DATA_PATH / "folder_a" +folder_b_path = TEST_DATA_PATH / "folder_a" / "folder_b" +folder_a_nested_path = TEST_DATA_PATH / "folder_a" / "folder_b" / "folder_a" +test_nest_path = TEST_DATA_PATH / "folder_a" / "folder_b" / "folder_a" / "test_nest.py" +double_nested_folder_expected_output = { + "name": ".data", + "path": TEST_DATA_PATH_STR, + "type_": "folder", + "children": [ + { + "name": "folder_a", + "path": os.fspath(folder_a_path), + "type_": "folder", + "id_": os.fspath(folder_a_path), + "children": [ + { + "name": "folder_b", + "path": os.fspath(folder_b_path), + "type_": "folder", + "id_": os.fspath(folder_b_path), + "children": [ + { + "name": "folder_a", + "path": os.fspath(folder_a_nested_path), + "type_": "folder", + "id_": os.fspath(folder_a_nested_path), + "children": [ + { + "name": "test_nest.py", + "path": os.fspath(test_nest_path), + "type_": "file", + "id_": os.fspath(test_nest_path), + "children": [ + { + "name": "test_function", + "path": os.fspath(test_nest_path), + "lineno": find_test_line_number( + "test_function", + test_nest_path, + ), + "type_": "test", + "id_": get_absolute_test_id( + "folder_a/folder_b/folder_a/test_nest.py::test_function", + test_nest_path, + ), + "runID": get_absolute_test_id( + "folder_a/folder_b/folder_a/test_nest.py::test_function", + test_nest_path, + ), + } + ], + } + ], + } + ], + } + ], + } + ], + "id_": TEST_DATA_PATH_STR, +} + +# This is the expected output for the nested_folder tests. +# └── parametrize_tests.py +# └── TestClass +# └── test_adding +# └── [3+5-8] +# └── [2+4-6] +# └── [6+9-16] +# └── test_string +# └── [hello] +# └── [complicated split [] ()] +parameterize_tests_path = TEST_DATA_PATH / "parametrize_tests.py" +parametrize_tests_expected_output = { + "name": ".data", + "path": TEST_DATA_PATH_STR, + "type_": "folder", + "children": [ + { + "name": "parametrize_tests.py", + "path": os.fspath(parameterize_tests_path), + "type_": "file", + "id_": os.fspath(parameterize_tests_path), + "children": [ + { + "name": "TestClass", + "path": os.fspath(parameterize_tests_path), + "type_": "class", + "id_": get_absolute_test_id( + "parametrize_tests.py::TestClass", + parameterize_tests_path, + ), + "lineno": find_class_line_number("TestClass", parameterize_tests_path), + "children": [ + { + "name": "test_adding", + "path": os.fspath(parameterize_tests_path), + "type_": "function", + "id_": os.fspath(parameterize_tests_path) + "::TestClass::test_adding", + "children": [ + { + "name": "[3+5-8]", + "path": os.fspath(parameterize_tests_path), + "lineno": find_test_line_number( + "test_adding[3+5-8]", + parameterize_tests_path, + ), + "type_": "test", + "id_": get_absolute_test_id( + "parametrize_tests.py::TestClass::test_adding[3+5-8]", + parameterize_tests_path, + ), + "runID": get_absolute_test_id( + "parametrize_tests.py::TestClass::test_adding[3+5-8]", + parameterize_tests_path, + ), + }, + { + "name": "[2+4-6]", + "path": os.fspath(parameterize_tests_path), + "lineno": find_test_line_number( + "test_adding[2+4-6]", + parameterize_tests_path, + ), + "type_": "test", + "id_": get_absolute_test_id( + "parametrize_tests.py::TestClass::test_adding[2+4-6]", + parameterize_tests_path, + ), + "runID": get_absolute_test_id( + "parametrize_tests.py::TestClass::test_adding[2+4-6]", + parameterize_tests_path, + ), + }, + { + "name": "[6+9-16]", + "path": os.fspath(parameterize_tests_path), + "lineno": find_test_line_number( + "test_adding[6+9-16]", + parameterize_tests_path, + ), + "type_": "test", + "id_": get_absolute_test_id( + "parametrize_tests.py::TestClass::test_adding[6+9-16]", + parameterize_tests_path, + ), + "runID": get_absolute_test_id( + "parametrize_tests.py::TestClass::test_adding[6+9-16]", + parameterize_tests_path, + ), + }, + ], + }, + ], + }, + { + "name": "test_string", + "path": os.fspath(parameterize_tests_path), + "type_": "function", + "children": [ + { + "name": "[hello]", + "path": os.fspath(parameterize_tests_path), + "lineno": find_test_line_number( + "test_string[hello]", + parameterize_tests_path, + ), + "type_": "test", + "id_": get_absolute_test_id( + "parametrize_tests.py::test_string[hello]", + parameterize_tests_path, + ), + "runID": get_absolute_test_id( + "parametrize_tests.py::test_string[hello]", + parameterize_tests_path, + ), + }, + { + "name": "[complicated split [] ()]", + "path": os.fspath(parameterize_tests_path), + "lineno": find_test_line_number( + "test_string[1]", + parameterize_tests_path, + ), + "type_": "test", + "id_": get_absolute_test_id( + "parametrize_tests.py::test_string[complicated split [] ()]", + parameterize_tests_path, + ), + "runID": get_absolute_test_id( + "parametrize_tests.py::test_string[complicated split [] ()]", + parameterize_tests_path, + ), + }, + ], + "id_": os.fspath(parameterize_tests_path) + "::test_string", + }, + ], + }, + ], + "id_": TEST_DATA_PATH_STR, +} + +# This is the expected output for the text_docstring.txt tests. +# └── text_docstring.txt +text_docstring_path = TEST_DATA_PATH / "text_docstring.txt" +doctest_pytest_expected_output = { + "name": ".data", + "path": TEST_DATA_PATH_STR, + "type_": "folder", + "children": [ + { + "name": "text_docstring.txt", + "path": os.fspath(text_docstring_path), + "type_": "file", + "id_": os.fspath(text_docstring_path), + "children": [ + { + "name": "text_docstring.txt", + "path": os.fspath(text_docstring_path), + "lineno": find_test_line_number( + "text_docstring.txt", + os.fspath(text_docstring_path), + ), + "type_": "test", + "id_": get_absolute_test_id( + "text_docstring.txt::text_docstring.txt", text_docstring_path + ), + "runID": get_absolute_test_id( + "text_docstring.txt::text_docstring.txt", text_docstring_path + ), + } + ], + } + ], + "id_": TEST_DATA_PATH_STR, +} + +# This is the expected output for the param_same_name tests. +# └── param_same_name +# └── test_param1.py +# └── test_odd_even +# └── [a] +# └── [b] +# └── [c] +# └── test_param2.py +# └── test_odd_even +# └── [1] +# └── [2] +# └── [3] +param1_path = TEST_DATA_PATH / "param_same_name" / "test_param1.py" +param2_path = TEST_DATA_PATH / "param_same_name" / "test_param2.py" +param_same_name_expected_output = { + "name": ".data", + "path": TEST_DATA_PATH_STR, + "type_": "folder", + "children": [ + { + "name": "param_same_name", + "path": os.fspath(TEST_DATA_PATH / "param_same_name"), + "type_": "folder", + "id_": os.fspath(TEST_DATA_PATH / "param_same_name"), + "children": [ + { + "name": "test_param1.py", + "path": os.fspath(param1_path), + "type_": "file", + "id_": os.fspath(param1_path), + "children": [ + { + "name": "test_odd_even", + "path": os.fspath(param1_path), + "type_": "function", + "children": [ + { + "name": "[a]", + "path": os.fspath(param1_path), + "lineno": "6", + "type_": "test", + "id_": get_absolute_test_id( + "param_same_name/test_param1.py::test_odd_even[a]", + param1_path, + ), + "runID": get_absolute_test_id( + "param_same_name/test_param1.py::test_odd_even[a]", + param1_path, + ), + }, + { + "name": "[b]", + "path": os.fspath(param1_path), + "lineno": "6", + "type_": "test", + "id_": get_absolute_test_id( + "param_same_name/test_param1.py::test_odd_even[b]", + param1_path, + ), + "runID": get_absolute_test_id( + "param_same_name/test_param1.py::test_odd_even[b]", + param1_path, + ), + }, + { + "name": "[c]", + "path": os.fspath(param1_path), + "lineno": "6", + "type_": "test", + "id_": get_absolute_test_id( + "param_same_name/test_param1.py::test_odd_even[c]", + param1_path, + ), + "runID": get_absolute_test_id( + "param_same_name/test_param1.py::test_odd_even[c]", + param1_path, + ), + }, + ], + "id_": os.fspath(param1_path) + "::test_odd_even", + } + ], + }, + { + "name": "test_param2.py", + "path": os.fspath(param2_path), + "type_": "file", + "id_": os.fspath(param2_path), + "children": [ + { + "name": "test_odd_even", + "path": os.fspath(param2_path), + "type_": "function", + "children": [ + { + "name": "[1]", + "path": os.fspath(param2_path), + "lineno": "6", + "type_": "test", + "id_": get_absolute_test_id( + "param_same_name/test_param2.py::test_odd_even[1]", + param2_path, + ), + "runID": get_absolute_test_id( + "param_same_name/test_param2.py::test_odd_even[1]", + param2_path, + ), + }, + { + "name": "[2]", + "path": os.fspath(param2_path), + "lineno": "6", + "type_": "test", + "id_": get_absolute_test_id( + "param_same_name/test_param2.py::test_odd_even[2]", + param2_path, + ), + "runID": get_absolute_test_id( + "param_same_name/test_param2.py::test_odd_even[2]", + param2_path, + ), + }, + { + "name": "[3]", + "path": os.fspath(param2_path), + "lineno": "6", + "type_": "test", + "id_": get_absolute_test_id( + "param_same_name/test_param2.py::test_odd_even[3]", + param2_path, + ), + "runID": get_absolute_test_id( + "param_same_name/test_param2.py::test_odd_even[3]", + param2_path, + ), + }, + ], + "id_": os.fspath(param2_path) + "::test_odd_even", + } + ], + }, + ], + } + ], + "id_": TEST_DATA_PATH_STR, +} + +tests_path = TEST_DATA_PATH / "root" / "tests" +tests_a_path = TEST_DATA_PATH / "root" / "tests" / "test_a.py" +tests_b_path = TEST_DATA_PATH / "root" / "tests" / "test_b.py" +# This is the expected output for the root folder tests. +# └── tests +# └── test_a.py +# └── test_a_function +# └── test_b.py +# └── test_b_function +root_with_config_expected_output = { + "name": "tests", + "path": os.fspath(tests_path), + "type_": "folder", + "children": [ + { + "name": "test_a.py", + "path": os.fspath(tests_a_path), + "type_": "file", + "id_": os.fspath(tests_a_path), + "children": [ + { + "name": "test_a_function", + "path": os.fspath(os.path.join(tests_path, "test_a.py")), # noqa: PTH118 + "lineno": find_test_line_number( + "test_a_function", + os.path.join(tests_path, "test_a.py"), # noqa: PTH118 + ), + "type_": "test", + "id_": get_absolute_test_id("tests/test_a.py::test_a_function", tests_a_path), + "runID": get_absolute_test_id("tests/test_a.py::test_a_function", tests_a_path), + } + ], + }, + { + "name": "test_b.py", + "path": os.fspath(tests_b_path), + "type_": "file", + "id_": os.fspath(tests_b_path), + "children": [ + { + "name": "test_b_function", + "path": os.fspath(os.path.join(tests_path, "test_b.py")), # noqa: PTH118 + "lineno": find_test_line_number( + "test_b_function", + os.path.join(tests_path, "test_b.py"), # noqa: PTH118 + ), + "type_": "test", + "id_": get_absolute_test_id("tests/test_b.py::test_b_function", tests_b_path), + "runID": get_absolute_test_id("tests/test_b.py::test_b_function", tests_b_path), + } + ], + }, + ], + "id_": os.fspath(tests_path), +} +TEST_MULTI_CLASS_NEST_PATH = TEST_DATA_PATH / "test_multi_class_nest.py" +# This is the expected output for the nested_classes tests. +# └── test_multi_class_nest.py +# └── TestFirstClass +# └── TestSecondClass +# └── test_second +# └── test_first +# └── TestSecondClass2 +# └── test_second2 +# └── test_independent +nested_classes_expected_test_output = { + "name": ".data", + "path": TEST_DATA_PATH_STR, + "type_": "folder", + "children": [ + { + "name": "test_multi_class_nest.py", + "path": str(TEST_MULTI_CLASS_NEST_PATH), + "type_": "file", + "id_": str(TEST_MULTI_CLASS_NEST_PATH), + "children": [ + { + "name": "TestFirstClass", + "path": str(TEST_MULTI_CLASS_NEST_PATH), + "type_": "class", + "id_": get_absolute_test_id( + "test_multi_class_nest.py::TestFirstClass", + TEST_MULTI_CLASS_NEST_PATH, + ), + "lineno": find_class_line_number("TestFirstClass", TEST_MULTI_CLASS_NEST_PATH), + "children": [ + { + "name": "TestSecondClass", + "path": str(TEST_MULTI_CLASS_NEST_PATH), + "type_": "class", + "id_": get_absolute_test_id( + "test_multi_class_nest.py::TestFirstClass::TestSecondClass", + TEST_MULTI_CLASS_NEST_PATH, + ), + "lineno": find_class_line_number( + "TestSecondClass", TEST_MULTI_CLASS_NEST_PATH + ), + "children": [ + { + "name": "test_second", + "path": str(TEST_MULTI_CLASS_NEST_PATH), + "lineno": find_test_line_number( + "test_second", + str(TEST_MULTI_CLASS_NEST_PATH), + ), + "type_": "test", + "id_": get_absolute_test_id( + "test_multi_class_nest.py::TestFirstClass::TestSecondClass::test_second", + TEST_MULTI_CLASS_NEST_PATH, + ), + "runID": get_absolute_test_id( + "test_multi_class_nest.py::TestFirstClass::TestSecondClass::test_second", + TEST_MULTI_CLASS_NEST_PATH, + ), + } + ], + }, + { + "name": "test_first", + "path": str(TEST_MULTI_CLASS_NEST_PATH), + "lineno": find_test_line_number( + "test_first", str(TEST_MULTI_CLASS_NEST_PATH) + ), + "type_": "test", + "id_": get_absolute_test_id( + "test_multi_class_nest.py::TestFirstClass::test_first", + TEST_MULTI_CLASS_NEST_PATH, + ), + "runID": get_absolute_test_id( + "test_multi_class_nest.py::TestFirstClass::test_first", + TEST_MULTI_CLASS_NEST_PATH, + ), + }, + { + "name": "TestSecondClass2", + "path": str(TEST_MULTI_CLASS_NEST_PATH), + "type_": "class", + "id_": get_absolute_test_id( + "test_multi_class_nest.py::TestFirstClass::TestSecondClass2", + TEST_MULTI_CLASS_NEST_PATH, + ), + "lineno": find_class_line_number( + "TestSecondClass2", TEST_MULTI_CLASS_NEST_PATH + ), + "children": [ + { + "name": "test_second2", + "path": str(TEST_MULTI_CLASS_NEST_PATH), + "lineno": find_test_line_number( + "test_second2", + str(TEST_MULTI_CLASS_NEST_PATH), + ), + "type_": "test", + "id_": get_absolute_test_id( + "test_multi_class_nest.py::TestFirstClass::TestSecondClass2::test_second2", + TEST_MULTI_CLASS_NEST_PATH, + ), + "runID": get_absolute_test_id( + "test_multi_class_nest.py::TestFirstClass::TestSecondClass2::test_second2", + TEST_MULTI_CLASS_NEST_PATH, + ), + } + ], + }, + ], + }, + { + "name": "test_independent", + "path": str(TEST_MULTI_CLASS_NEST_PATH), + "lineno": find_test_line_number( + "test_independent", str(TEST_MULTI_CLASS_NEST_PATH) + ), + "type_": "test", + "id_": get_absolute_test_id( + "test_multi_class_nest.py::test_independent", + TEST_MULTI_CLASS_NEST_PATH, + ), + "runID": get_absolute_test_id( + "test_multi_class_nest.py::test_independent", + TEST_MULTI_CLASS_NEST_PATH, + ), + }, + ], + } + ], + "id_": str(TEST_DATA_PATH), +} +SYMLINK_FOLDER_PATH = TEST_DATA_PATH / "symlink_folder" +SYMLINK_FOLDER_PATH_TESTS = TEST_DATA_PATH / "symlink_folder" / "tests" +SYMLINK_FOLDER_PATH_TESTS_TEST_A = TEST_DATA_PATH / "symlink_folder" / "tests" / "test_a.py" +SYMLINK_FOLDER_PATH_TESTS_TEST_B = TEST_DATA_PATH / "symlink_folder" / "tests" / "test_b.py" + +# This is the expected output for the symlink_folder tests. +# └── symlink_folder +# └── tests +# └── test_a.py +# └── test_a_function +# └── test_b.py +# └── test_b_function +symlink_expected_discovery_output = { + "name": "symlink_folder", + "path": str(SYMLINK_FOLDER_PATH), + "type_": "folder", + "children": [ + { + "name": "tests", + "path": str(SYMLINK_FOLDER_PATH_TESTS), + "type_": "folder", + "id_": str(SYMLINK_FOLDER_PATH_TESTS), + "children": [ + { + "name": "test_a.py", + "path": str(SYMLINK_FOLDER_PATH_TESTS_TEST_A), + "type_": "file", + "id_": str(SYMLINK_FOLDER_PATH_TESTS_TEST_A), + "children": [ + { + "name": "test_a_function", + "path": str(SYMLINK_FOLDER_PATH_TESTS_TEST_A), + "lineno": find_test_line_number( + "test_a_function", + os.path.join(tests_path, "test_a.py"), # noqa: PTH118 + ), + "type_": "test", + "id_": get_absolute_test_id( + "tests/test_a.py::test_a_function", + SYMLINK_FOLDER_PATH_TESTS_TEST_A, + ), + "runID": get_absolute_test_id( + "tests/test_a.py::test_a_function", + SYMLINK_FOLDER_PATH_TESTS_TEST_A, + ), + } + ], + }, + { + "name": "test_b.py", + "path": str(SYMLINK_FOLDER_PATH_TESTS_TEST_B), + "type_": "file", + "id_": str(SYMLINK_FOLDER_PATH_TESTS_TEST_B), + "children": [ + { + "name": "test_b_function", + "path": str(SYMLINK_FOLDER_PATH_TESTS_TEST_B), + "lineno": find_test_line_number( + "test_b_function", + os.path.join(tests_path, "test_b.py"), # noqa: PTH118 + ), + "type_": "test", + "id_": get_absolute_test_id( + "tests/test_b.py::test_b_function", + SYMLINK_FOLDER_PATH_TESTS_TEST_B, + ), + "runID": get_absolute_test_id( + "tests/test_b.py::test_b_function", + SYMLINK_FOLDER_PATH_TESTS_TEST_B, + ), + } + ], + }, + ], + } + ], + "id_": str(SYMLINK_FOLDER_PATH), +} + +same_function_new_class_param_expected_output = { + "name": ".data", + "path": TEST_DATA_PATH_STR, + "type_": "folder", + "children": [ + { + "name": "same_function_new_class_param.py", + "path": os.fspath(TEST_DATA_PATH / "same_function_new_class_param.py"), + "type_": "file", + "id_": os.fspath(TEST_DATA_PATH / "same_function_new_class_param.py"), + "children": [ + { + "name": "TestNotEmpty", + "path": os.fspath(TEST_DATA_PATH / "same_function_new_class_param.py"), + "type_": "class", + "children": [ + { + "name": "test_integer", + "path": os.fspath(TEST_DATA_PATH / "same_function_new_class_param.py"), + "type_": "function", + "children": [ + { + "name": "[1-1]", + "path": os.fspath( + TEST_DATA_PATH / "same_function_new_class_param.py" + ), + "lineno": find_test_line_number( + "TestNotEmpty::test_integer", + os.fspath( + TEST_DATA_PATH / "same_function_new_class_param.py" + ), + ), + "type_": "test", + "id_": get_absolute_test_id( + "same_function_new_class_param.py::TestNotEmpty::test_integer[1-1]", + TEST_DATA_PATH / "same_function_new_class_param.py", + ), + "runID": get_absolute_test_id( + "same_function_new_class_param.py::TestNotEmpty::test_integer[1-1]", + TEST_DATA_PATH / "same_function_new_class_param.py", + ), + }, + { + "name": "[2-2]", + "path": os.fspath( + TEST_DATA_PATH / "same_function_new_class_param.py" + ), + "lineno": find_test_line_number( + "TestNotEmpty::test_integer", + os.fspath( + TEST_DATA_PATH / "same_function_new_class_param.py" + ), + ), + "type_": "test", + "id_": get_absolute_test_id( + "same_function_new_class_param.py::TestNotEmpty::test_integer[2-2]", + TEST_DATA_PATH / "same_function_new_class_param.py", + ), + "runID": get_absolute_test_id( + "same_function_new_class_param.py::TestNotEmpty::test_integer[2-2]", + TEST_DATA_PATH / "same_function_new_class_param.py", + ), + }, + ], + "id_": os.fspath(TEST_DATA_PATH / "same_function_new_class_param.py") + + "::TestNotEmpty::test_integer", + }, + { + "name": "test_string", + "path": os.fspath(TEST_DATA_PATH / "same_function_new_class_param.py"), + "type_": "function", + "children": [ + { + "name": "[a-a]", + "path": os.fspath( + TEST_DATA_PATH / "same_function_new_class_param.py" + ), + "lineno": find_test_line_number( + "TestNotEmpty::test_string", + os.fspath( + TEST_DATA_PATH / "same_function_new_class_param.py" + ), + ), + "type_": "test", + "id_": get_absolute_test_id( + "same_function_new_class_param.py::TestNotEmpty::test_string[a-a]", + TEST_DATA_PATH / "same_function_new_class_param.py", + ), + "runID": get_absolute_test_id( + "same_function_new_class_param.py::TestNotEmpty::test_string[a-a]", + TEST_DATA_PATH / "same_function_new_class_param.py", + ), + }, + { + "name": "[b-b]", + "path": os.fspath( + TEST_DATA_PATH / "same_function_new_class_param.py" + ), + "lineno": find_test_line_number( + "TestNotEmpty::test_string", + os.fspath( + TEST_DATA_PATH / "same_function_new_class_param.py" + ), + ), + "type_": "test", + "id_": get_absolute_test_id( + "same_function_new_class_param.py::TestNotEmpty::test_string[b-b]", + TEST_DATA_PATH / "same_function_new_class_param.py", + ), + "runID": get_absolute_test_id( + "same_function_new_class_param.py::TestNotEmpty::test_string[b-b]", + TEST_DATA_PATH / "same_function_new_class_param.py", + ), + }, + ], + "id_": os.fspath(TEST_DATA_PATH / "same_function_new_class_param.py") + + "::TestNotEmpty::test_string", + }, + ], + "id_": get_absolute_test_id( + "same_function_new_class_param.py::TestNotEmpty", + TEST_DATA_PATH / "same_function_new_class_param.py", + ), + "lineno": find_class_line_number( + "TestNotEmpty", TEST_DATA_PATH / "same_function_new_class_param.py" + ), + }, + { + "name": "TestEmpty", + "path": os.fspath(TEST_DATA_PATH / "same_function_new_class_param.py"), + "type_": "class", + "children": [ + { + "name": "test_integer", + "path": os.fspath(TEST_DATA_PATH / "same_function_new_class_param.py"), + "type_": "function", + "children": [ + { + "name": "[0-0]", + "path": os.fspath( + TEST_DATA_PATH / "same_function_new_class_param.py" + ), + "lineno": find_test_line_number( + "TestEmpty::test_integer", + os.fspath( + TEST_DATA_PATH / "same_function_new_class_param.py" + ), + ), + "type_": "test", + "id_": get_absolute_test_id( + "same_function_new_class_param.py::TestEmpty::test_integer[0-0]", + TEST_DATA_PATH / "same_function_new_class_param.py", + ), + "runID": get_absolute_test_id( + "same_function_new_class_param.py::TestEmpty::test_integer[0-0]", + TEST_DATA_PATH / "same_function_new_class_param.py", + ), + }, + ], + "id_": os.fspath(TEST_DATA_PATH / "same_function_new_class_param.py") + + "::TestEmpty::test_integer", + }, + { + "name": "test_string", + "path": os.fspath(TEST_DATA_PATH / "same_function_new_class_param.py"), + "type_": "function", + "children": [ + { + "name": "[-]", + "path": os.fspath( + TEST_DATA_PATH / "same_function_new_class_param.py" + ), + "lineno": find_test_line_number( + "TestEmpty::test_string", + os.fspath( + TEST_DATA_PATH / "same_function_new_class_param.py" + ), + ), + "type_": "test", + "id_": get_absolute_test_id( + "same_function_new_class_param.py::TestEmpty::test_string[-]", + TEST_DATA_PATH / "same_function_new_class_param.py", + ), + "runID": get_absolute_test_id( + "same_function_new_class_param.py::TestEmpty::test_string[-]", + TEST_DATA_PATH / "same_function_new_class_param.py", + ), + }, + ], + "id_": os.fspath(TEST_DATA_PATH / "same_function_new_class_param.py") + + "::TestEmpty::test_string", + }, + ], + "id_": get_absolute_test_id( + "same_function_new_class_param.py::TestEmpty", + TEST_DATA_PATH / "same_function_new_class_param.py", + ), + "lineno": find_class_line_number( + "TestEmpty", TEST_DATA_PATH / "same_function_new_class_param.py" + ), + }, + ], + } + ], + "id_": TEST_DATA_PATH_STR, +} + +test_param_span_class_expected_output = { + "name": ".data", + "path": TEST_DATA_PATH_STR, + "type_": "folder", + "children": [ + { + "name": "test_param_span_class.py", + "path": os.fspath(TEST_DATA_PATH / "test_param_span_class.py"), + "type_": "file", + "id_": os.fspath(TEST_DATA_PATH / "test_param_span_class.py"), + "children": [ + { + "name": "TestClass1", + "path": os.fspath(TEST_DATA_PATH / "test_param_span_class.py"), + "type_": "class", + "children": [ + { + "name": "test_method1", + "path": os.fspath(TEST_DATA_PATH / "test_param_span_class.py"), + "type_": "function", + "children": [ + { + "name": "[1]", + "path": os.fspath(TEST_DATA_PATH / "test_param_span_class.py"), + "lineno": find_test_line_number( + "TestClass1::test_method1", + os.fspath(TEST_DATA_PATH / "test_param_span_class.py"), + ), + "type_": "test", + "id_": get_absolute_test_id( + "test_param_span_class.py::TestClass1::test_method1[1]", + TEST_DATA_PATH / "test_param_span_class.py", + ), + "runID": get_absolute_test_id( + "test_param_span_class.py::TestClass1::test_method1[1]", + TEST_DATA_PATH / "test_param_span_class.py", + ), + }, + { + "name": "[2]", + "path": os.fspath(TEST_DATA_PATH / "test_param_span_class.py"), + "lineno": find_test_line_number( + "TestClass1::test_method1", + os.fspath(TEST_DATA_PATH / "test_param_span_class.py"), + ), + "type_": "test", + "id_": get_absolute_test_id( + "test_param_span_class.py::TestClass1::test_method1[2]", + TEST_DATA_PATH / "test_param_span_class.py", + ), + "runID": get_absolute_test_id( + "test_param_span_class.py::TestClass1::test_method1[2]", + TEST_DATA_PATH / "test_param_span_class.py", + ), + }, + ], + "id_": os.fspath( + TEST_DATA_PATH + / "test_param_span_class.py::TestClass1::test_method1" + ), + } + ], + "id_": get_absolute_test_id( + "test_param_span_class.py::TestClass1", + TEST_DATA_PATH / "test_param_span_class.py", + ), + "lineno": find_class_line_number( + "TestClass1", TEST_DATA_PATH / "test_param_span_class.py" + ), + }, + { + "name": "TestClass2", + "path": os.fspath(TEST_DATA_PATH / "test_param_span_class.py"), + "type_": "class", + "children": [ + { + "name": "test_method1", + "path": os.fspath(TEST_DATA_PATH / "test_param_span_class.py"), + "type_": "function", + "children": [ + { + "name": "[1]", + "path": os.fspath(TEST_DATA_PATH / "test_param_span_class.py"), + "lineno": find_test_line_number( + "TestClass2::test_method1", + os.fspath(TEST_DATA_PATH / "test_param_span_class.py"), + ), + "type_": "test", + "id_": get_absolute_test_id( + "test_param_span_class.py::TestClass2::test_method1[1]", + TEST_DATA_PATH / "test_param_span_class.py", + ), + "runID": get_absolute_test_id( + "test_param_span_class.py::TestClass2::test_method1[1]", + TEST_DATA_PATH / "test_param_span_class.py", + ), + }, + { + "name": "[2]", + "path": os.fspath(TEST_DATA_PATH / "test_param_span_class.py"), + "lineno": find_test_line_number( + "TestClass2::test_method1", + os.fspath(TEST_DATA_PATH / "test_param_span_class.py"), + ), + "type_": "test", + "id_": get_absolute_test_id( + "test_param_span_class.py::TestClass2::test_method1[2]", + TEST_DATA_PATH / "test_param_span_class.py", + ), + "runID": get_absolute_test_id( + "test_param_span_class.py::TestClass2::test_method1[2]", + TEST_DATA_PATH / "test_param_span_class.py", + ), + }, + ], + "id_": os.fspath( + TEST_DATA_PATH + / "test_param_span_class.py::TestClass2::test_method1" + ), + } + ], + "id_": get_absolute_test_id( + "test_param_span_class.py::TestClass2", + TEST_DATA_PATH / "test_param_span_class.py", + ), + "lineno": find_class_line_number( + "TestClass2", TEST_DATA_PATH / "test_param_span_class.py" + ), + }, + ], + } + ], + "id_": TEST_DATA_PATH_STR, +} +# This is the expected output for the describe_only.py tests. +# └── describe_only.py +# └── describe_A +# └── test_1 +# └── test_2 + +describe_only_path = TEST_DATA_PATH / "pytest_describe_plugin" / "describe_only.py" +pytest_describe_plugin_path = TEST_DATA_PATH / "pytest_describe_plugin" + +expected_describe_only_output = { + "name": ".data", + "path": TEST_DATA_PATH_STR, + "type_": "folder", + "children": [ + { + "name": "pytest_describe_plugin", + "path": os.fspath(pytest_describe_plugin_path), + "type_": "folder", + "id_": os.fspath(pytest_describe_plugin_path), + "children": [ + { + "name": "describe_only.py", + "path": os.fspath(describe_only_path), + "type_": "file", + "id_": os.fspath(describe_only_path), + "children": [ + { + "name": "describe_A", + "path": os.fspath(describe_only_path), + "type_": "class", + "children": [ + { + "name": "test_1", + "path": os.fspath(describe_only_path), + "lineno": find_test_line_number( + "test_1", + describe_only_path, + ), + "type_": "test", + "id_": get_absolute_test_id( + "pytest_describe_plugin/describe_only.py::describe_A::test_1", + describe_only_path, + ), + "runID": get_absolute_test_id( + "pytest_describe_plugin/describe_only.py::describe_A::test_1", + describe_only_path, + ), + }, + { + "name": "test_2", + "path": os.fspath(describe_only_path), + "lineno": find_test_line_number( + "test_2", + describe_only_path, + ), + "type_": "test", + "id_": get_absolute_test_id( + "pytest_describe_plugin/describe_only.py::describe_A::test_2", + describe_only_path, + ), + "runID": get_absolute_test_id( + "pytest_describe_plugin/describe_only.py::describe_A::test_2", + describe_only_path, + ), + }, + ], + "id_": get_absolute_test_id( + "pytest_describe_plugin/describe_only.py::describe_A", + describe_only_path, + ), + "lineno": find_class_line_number("describe_A", describe_only_path), + } + ], + } + ], + } + ], + "id_": TEST_DATA_PATH_STR, +} +# This is the expected output for the nested_describe.py tests. +# └── nested_describe.py +# └── describe_list +# └── describe_append +# └── add_empty +# └── remove_empty +# └── describe_remove +# └── removes +nested_describe_path = TEST_DATA_PATH / "pytest_describe_plugin" / "nested_describe.py" +expected_nested_describe_output = { + "name": ".data", + "path": TEST_DATA_PATH_STR, + "type_": "folder", + "children": [ + { + "name": "pytest_describe_plugin", + "path": os.fspath(pytest_describe_plugin_path), + "type_": "folder", + "id_": os.fspath(pytest_describe_plugin_path), + "children": [ + { + "name": "nested_describe.py", + "path": os.fspath(nested_describe_path), + "type_": "file", + "id_": os.fspath(nested_describe_path), + "children": [ + { + "name": "describe_list", + "path": os.fspath(nested_describe_path), + "type_": "class", + "children": [ + { + "name": "describe_append", + "path": os.fspath(nested_describe_path), + "type_": "class", + "children": [ + { + "name": "add_empty", + "path": os.fspath(nested_describe_path), + "lineno": find_test_line_number( + "add_empty", + nested_describe_path, + ), + "type_": "test", + "id_": get_absolute_test_id( + "pytest_describe_plugin/nested_describe.py::describe_list::describe_append::add_empty", + nested_describe_path, + ), + "runID": get_absolute_test_id( + "pytest_describe_plugin/nested_describe.py::describe_list::describe_append::add_empty", + nested_describe_path, + ), + }, + { + "name": "remove_empty", + "path": os.fspath(nested_describe_path), + "lineno": find_test_line_number( + "remove_empty", + nested_describe_path, + ), + "type_": "test", + "id_": get_absolute_test_id( + "pytest_describe_plugin/nested_describe.py::describe_list::describe_append::remove_empty", + nested_describe_path, + ), + "runID": get_absolute_test_id( + "pytest_describe_plugin/nested_describe.py::describe_list::describe_append::remove_empty", + nested_describe_path, + ), + }, + ], + "id_": get_absolute_test_id( + "pytest_describe_plugin/nested_describe.py::describe_list::describe_append", + nested_describe_path, + ), + "lineno": find_class_line_number( + "describe_append", nested_describe_path + ), + }, + { + "name": "describe_remove", + "path": os.fspath(nested_describe_path), + "type_": "class", + "children": [ + { + "name": "removes", + "path": os.fspath(nested_describe_path), + "lineno": find_test_line_number( + "removes", + nested_describe_path, + ), + "type_": "test", + "id_": get_absolute_test_id( + "pytest_describe_plugin/nested_describe.py::describe_list::describe_remove::removes", + nested_describe_path, + ), + "runID": get_absolute_test_id( + "pytest_describe_plugin/nested_describe.py::describe_list::describe_remove::removes", + nested_describe_path, + ), + } + ], + "id_": get_absolute_test_id( + "pytest_describe_plugin/nested_describe.py::describe_list::describe_remove", + nested_describe_path, + ), + "lineno": find_class_line_number( + "describe_remove", nested_describe_path + ), + }, + ], + "id_": get_absolute_test_id( + "pytest_describe_plugin/nested_describe.py::describe_list", + nested_describe_path, + ), + "lineno": find_class_line_number("describe_list", nested_describe_path), + } + ], + } + ], + } + ], + "id_": TEST_DATA_PATH_STR, +} +# This is the expected output for the folder_with_script folder when run with ruff +# └── .data +# └── folder_with_script +# └── script_random.py +# └── ruff +# └── test_simple.py +# └── ruff +# └── test_function +ruff_test_expected_output = { + "name": ".data", + "path": TEST_DATA_PATH_STR, + "type_": "folder", + "children": [ + { + "name": "folder_with_script", + "path": os.fspath(TEST_DATA_PATH / "folder_with_script"), + "type_": "folder", + "id_": os.fspath(TEST_DATA_PATH / "folder_with_script"), + "children": [ + { + "name": "script_random.py", + "path": os.fspath(TEST_DATA_PATH / "folder_with_script" / "script_random.py"), + "type_": "file", + "id_": os.fspath(TEST_DATA_PATH / "folder_with_script" / "script_random.py"), + "children": [ + { + "name": "ruff", + "path": os.fspath( + TEST_DATA_PATH / "folder_with_script" / "script_random.py" + ), + "lineno": "", + "type_": "test", + "id_": get_absolute_test_id( + "folder_with_script/script_random.py::ruff", + TEST_DATA_PATH / "folder_with_script" / "script_random.py", + ), + "runID": get_absolute_test_id( + "folder_with_script/script_random.py::ruff", + TEST_DATA_PATH / "folder_with_script" / "script_random.py", + ), + } + ], + }, + { + "name": "test_simple.py", + "path": os.fspath(TEST_DATA_PATH / "folder_with_script" / "test_simple.py"), + "type_": "file", + "id_": os.fspath(TEST_DATA_PATH / "folder_with_script" / "test_simple.py"), + "children": [ + { + "name": "ruff", + "path": os.fspath( + TEST_DATA_PATH / "folder_with_script" / "test_simple.py" + ), + "lineno": "", + "type_": "test", + "id_": get_absolute_test_id( + "folder_with_script/test_simple.py::ruff", + TEST_DATA_PATH / "folder_with_script" / "test_simple.py", + ), + "runID": get_absolute_test_id( + "folder_with_script/test_simple.py::ruff", + TEST_DATA_PATH / "folder_with_script" / "test_simple.py", + ), + }, + { + "name": "test_function", + "path": os.fspath( + TEST_DATA_PATH / "folder_with_script" / "test_simple.py" + ), + "lineno": find_test_line_number( + "test_function", + TEST_DATA_PATH / "folder_with_script" / "test_simple.py", + ), + "type_": "test", + "id_": get_absolute_test_id( + "folder_with_script/test_simple.py::test_function", + TEST_DATA_PATH / "folder_with_script" / "test_simple.py", + ), + "runID": get_absolute_test_id( + "folder_with_script/test_simple.py::test_function", + TEST_DATA_PATH / "folder_with_script" / "test_simple.py", + ), + }, + ], + }, + ], + } + ], + "id_": TEST_DATA_PATH_STR, +} + +# This is the expected output for the 2496-black-formatter folder when run with black plugin +# └── .data +# └── 2496-black-formatter +# └── app.py +# └── black +# └── test_app.py +# └── black +# └── test_add +# └── test_subtract +black_formatter_folder_path = TEST_DATA_PATH / "2496-black-formatter" +black_app_path = black_formatter_folder_path / "app.py" +black_test_app_path = black_formatter_folder_path / "test_app.py" +black_formatter_expected_output = { + "name": ".data", + "path": TEST_DATA_PATH_STR, + "type_": "folder", + "children": [ + { + "name": "2496-black-formatter", + "path": os.fspath(black_formatter_folder_path), + "type_": "folder", + "id_": os.fspath(black_formatter_folder_path), + "children": [ + { + "name": "app.py", + "path": os.fspath(black_app_path), + "type_": "file", + "id_": os.fspath(black_app_path), + "children": [ + { + "name": "black", + "path": os.fspath(black_app_path), + "lineno": "0", + "type_": "test", + "id_": get_absolute_test_id( + "2496-black-formatter/app.py::black", + black_app_path, + ), + "runID": get_absolute_test_id( + "2496-black-formatter/app.py::black", + black_app_path, + ), + } + ], + }, + { + "name": "test_app.py", + "path": os.fspath(black_test_app_path), + "type_": "file", + "id_": os.fspath(black_test_app_path), + "children": [ + { + "name": "black", + "path": os.fspath(black_test_app_path), + "lineno": "0", + "type_": "test", + "id_": get_absolute_test_id( + "2496-black-formatter/test_app.py::black", + black_test_app_path, + ), + "runID": get_absolute_test_id( + "2496-black-formatter/test_app.py::black", + black_test_app_path, + ), + }, + { + "name": "test_add", + "path": os.fspath(black_test_app_path), + "lineno": find_test_line_number( + "test_add", + black_test_app_path, + ), + "type_": "test", + "id_": get_absolute_test_id( + "2496-black-formatter/test_app.py::test_add", + black_test_app_path, + ), + "runID": get_absolute_test_id( + "2496-black-formatter/test_app.py::test_add", + black_test_app_path, + ), + }, + { + "name": "test_subtract", + "path": os.fspath(black_test_app_path), + "lineno": find_test_line_number( + "test_subtract", + black_test_app_path, + ), + "type_": "test", + "id_": get_absolute_test_id( + "2496-black-formatter/test_app.py::test_subtract", + black_test_app_path, + ), + "runID": get_absolute_test_id( + "2496-black-formatter/test_app.py::test_subtract", + black_test_app_path, + ), + }, + ], + }, + ], + } + ], + "id_": TEST_DATA_PATH_STR, +} + +# ===================================================================================== +# PROJECT_ROOT_PATH environment variable tests +# These test the project-based testing feature where PROJECT_ROOT_PATH changes +# the test tree root from cwd to the specified project path. +# ===================================================================================== + +# This is the expected output for unittest_folder when PROJECT_ROOT_PATH is set to unittest_folder. +# The root of the tree is unittest_folder (not .data), simulating project-based testing. +# +# **Project Configuration:** +# In the VS Code Python extension, projects are defined by the Python Environments extension. +# Each project has a root directory (identified by pyproject.toml, setup.py, etc.). +# When PROJECT_ROOT_PATH is set, pytest uses that path as the test tree root instead of cwd. +# +# **Test Tree Structure:** +# Without PROJECT_ROOT_PATH (legacy mode): +# └── .data (cwd = workspace root) +# └── unittest_folder +# └── test_add.py, test_subtract.py... +# +# With PROJECT_ROOT_PATH set to unittest_folder (project-based mode): +# └── unittest_folder (ROOT - set via PROJECT_ROOT_PATH env var) +# β”œβ”€β”€ test_add.py +# β”‚ └── TestAddFunction +# β”‚ β”œβ”€β”€ test_add_negative_numbers +# β”‚ └── test_add_positive_numbers +# β”‚ └── TestDuplicateFunction +# β”‚ └── test_dup_a +# └── test_subtract.py +# └── TestSubtractFunction +# β”œβ”€β”€ test_subtract_negative_numbers +# └── test_subtract_positive_numbers +# └── TestDuplicateFunction +# └── test_dup_s +# +# Note: This reuses the unittest_folder paths defined earlier in this file. +project_root_unittest_folder_expected_output = { + "name": "unittest_folder", + "path": os.fspath(unittest_folder_path), + "type_": "folder", + "children": [ + { + "name": "test_add.py", + "path": os.fspath(test_add_path), + "type_": "file", + "id_": os.fspath(test_add_path), + "children": [ + { + "name": "TestAddFunction", + "path": os.fspath(test_add_path), + "type_": "class", + "children": [ + { + "name": "test_add_negative_numbers", + "path": os.fspath(test_add_path), + "lineno": find_test_line_number( + "test_add_negative_numbers", + os.fspath(test_add_path), + ), + "type_": "test", + "id_": get_absolute_test_id( + "test_add.py::TestAddFunction::test_add_negative_numbers", + test_add_path, + ), + "runID": get_absolute_test_id( + "test_add.py::TestAddFunction::test_add_negative_numbers", + test_add_path, + ), + }, + { + "name": "test_add_positive_numbers", + "path": os.fspath(test_add_path), + "lineno": find_test_line_number( + "test_add_positive_numbers", + os.fspath(test_add_path), + ), + "type_": "test", + "id_": get_absolute_test_id( + "test_add.py::TestAddFunction::test_add_positive_numbers", + test_add_path, + ), + "runID": get_absolute_test_id( + "test_add.py::TestAddFunction::test_add_positive_numbers", + test_add_path, + ), + }, + ], + "id_": get_absolute_test_id( + "test_add.py::TestAddFunction", + test_add_path, + ), + "lineno": find_class_line_number("TestAddFunction", test_add_path), + }, + { + "name": "TestDuplicateFunction", + "path": os.fspath(test_add_path), + "type_": "class", + "children": [ + { + "name": "test_dup_a", + "path": os.fspath(test_add_path), + "lineno": find_test_line_number( + "test_dup_a", + os.fspath(test_add_path), + ), + "type_": "test", + "id_": get_absolute_test_id( + "test_add.py::TestDuplicateFunction::test_dup_a", + test_add_path, + ), + "runID": get_absolute_test_id( + "test_add.py::TestDuplicateFunction::test_dup_a", + test_add_path, + ), + }, + ], + "id_": get_absolute_test_id( + "test_add.py::TestDuplicateFunction", + test_add_path, + ), + "lineno": find_class_line_number("TestDuplicateFunction", test_add_path), + }, + ], + }, + { + "name": "test_subtract.py", + "path": os.fspath(test_subtract_path), + "type_": "file", + "id_": os.fspath(test_subtract_path), + "children": [ + { + "name": "TestSubtractFunction", + "path": os.fspath(test_subtract_path), + "type_": "class", + "children": [ + { + "name": "test_subtract_negative_numbers", + "path": os.fspath(test_subtract_path), + "lineno": find_test_line_number( + "test_subtract_negative_numbers", + os.fspath(test_subtract_path), + ), + "type_": "test", + "id_": get_absolute_test_id( + "test_subtract.py::TestSubtractFunction::test_subtract_negative_numbers", + test_subtract_path, + ), + "runID": get_absolute_test_id( + "test_subtract.py::TestSubtractFunction::test_subtract_negative_numbers", + test_subtract_path, + ), + }, + { + "name": "test_subtract_positive_numbers", + "path": os.fspath(test_subtract_path), + "lineno": find_test_line_number( + "test_subtract_positive_numbers", + os.fspath(test_subtract_path), + ), + "type_": "test", + "id_": get_absolute_test_id( + "test_subtract.py::TestSubtractFunction::test_subtract_positive_numbers", + test_subtract_path, + ), + "runID": get_absolute_test_id( + "test_subtract.py::TestSubtractFunction::test_subtract_positive_numbers", + test_subtract_path, + ), + }, + ], + "id_": get_absolute_test_id( + "test_subtract.py::TestSubtractFunction", + test_subtract_path, + ), + "lineno": find_class_line_number("TestSubtractFunction", test_subtract_path), + }, + { + "name": "TestDuplicateFunction", + "path": os.fspath(test_subtract_path), + "type_": "class", + "children": [ + { + "name": "test_dup_s", + "path": os.fspath(test_subtract_path), + "lineno": find_test_line_number( + "test_dup_s", + os.fspath(test_subtract_path), + ), + "type_": "test", + "id_": get_absolute_test_id( + "test_subtract.py::TestDuplicateFunction::test_dup_s", + test_subtract_path, + ), + "runID": get_absolute_test_id( + "test_subtract.py::TestDuplicateFunction::test_dup_s", + test_subtract_path, + ), + }, + ], + "id_": get_absolute_test_id( + "test_subtract.py::TestDuplicateFunction", + test_subtract_path, + ), + "lineno": find_class_line_number("TestDuplicateFunction", test_subtract_path), + }, + ], + }, + ], + "id_": os.fspath(unittest_folder_path), +} diff --git a/python_files/tests/pytestadapter/expected_execution_test_output.py b/python_files/tests/pytestadapter/expected_execution_test_output.py new file mode 100644 index 000000000000..fa6743d0e112 --- /dev/null +++ b/python_files/tests/pytestadapter/expected_execution_test_output.py @@ -0,0 +1,749 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. +from .helpers import TEST_DATA_PATH, get_absolute_test_id + +TEST_SUBTRACT_FUNCTION = "unittest_folder/test_subtract.py::TestSubtractFunction::" +TEST_ADD_FUNCTION = "unittest_folder/test_add.py::TestAddFunction::" +SUCCESS = "success" +FAILURE = "failure" + +# This is the expected output for the unittest_folder execute tests +# └── unittest_folder +# β”œβ”€β”€ test_add.py +# β”‚ └── TestAddFunction +# β”‚ β”œβ”€β”€ test_add_negative_numbers: success +# β”‚ └── test_add_positive_numbers: success +# └── test_subtract.py +# └── TestSubtractFunction +# β”œβ”€β”€ test_subtract_negative_numbers: failure +# └── test_subtract_positive_numbers: success +test_add_path = TEST_DATA_PATH / "unittest_folder" / "test_add.py" +test_subtract_path = TEST_DATA_PATH / "unittest_folder" / "test_subtract.py" +uf_execution_expected_output = { + get_absolute_test_id(f"{TEST_ADD_FUNCTION}test_add_negative_numbers", test_add_path): { + "test": get_absolute_test_id( + f"{TEST_ADD_FUNCTION}test_add_negative_numbers", test_add_path + ), + "outcome": SUCCESS, + "message": None, + "traceback": None, + "subtest": None, + }, + get_absolute_test_id(f"{TEST_ADD_FUNCTION}test_add_positive_numbers", test_add_path): { + "test": get_absolute_test_id( + f"{TEST_ADD_FUNCTION}test_add_positive_numbers", test_add_path + ), + "outcome": SUCCESS, + "message": None, + "traceback": None, + "subtest": None, + }, + get_absolute_test_id( + f"{TEST_SUBTRACT_FUNCTION}test_subtract_negative_numbers", + test_subtract_path, + ): { + "test": get_absolute_test_id( + f"{TEST_SUBTRACT_FUNCTION}test_subtract_negative_numbers", + test_subtract_path, + ), + "outcome": FAILURE, + "message": "ERROR MESSAGE", + "traceback": None, + "subtest": None, + }, + get_absolute_test_id( + f"{TEST_SUBTRACT_FUNCTION}test_subtract_positive_numbers", + test_subtract_path, + ): { + "test": get_absolute_test_id( + f"{TEST_SUBTRACT_FUNCTION}test_subtract_positive_numbers", + test_subtract_path, + ), + "outcome": SUCCESS, + "message": None, + "traceback": None, + "subtest": None, + }, +} + + +# This is the expected output for the unittest_folder only execute add.py tests +# └── unittest_folder +# β”œβ”€β”€ test_add.py +# β”‚ └── TestAddFunction +# β”‚ β”œβ”€β”€ test_add_negative_numbers: success +# β”‚ └── test_add_positive_numbers: success +test_add_path = TEST_DATA_PATH / "unittest_folder" / "test_add.py" + +uf_single_file_expected_output = { + get_absolute_test_id(f"{TEST_ADD_FUNCTION}test_add_negative_numbers", test_add_path): { + "test": get_absolute_test_id( + f"{TEST_ADD_FUNCTION}test_add_negative_numbers", test_add_path + ), + "outcome": SUCCESS, + "message": None, + "traceback": None, + "subtest": None, + }, + get_absolute_test_id(f"{TEST_ADD_FUNCTION}test_add_positive_numbers", test_add_path): { + "test": get_absolute_test_id( + f"{TEST_ADD_FUNCTION}test_add_positive_numbers", test_add_path + ), + "outcome": SUCCESS, + "message": None, + "traceback": None, + "subtest": None, + }, +} + + +# This is the expected output for the unittest_folder execute only signle method +# └── unittest_folder +# β”œβ”€β”€ test_add.py +# β”‚ └── TestAddFunction +# β”‚ └── test_add_positive_numbers: success +uf_single_method_execution_expected_output = { + get_absolute_test_id(f"{TEST_ADD_FUNCTION}test_add_positive_numbers", test_add_path): { + "test": get_absolute_test_id( + f"{TEST_ADD_FUNCTION}test_add_positive_numbers", test_add_path + ), + "outcome": SUCCESS, + "message": None, + "traceback": None, + "subtest": None, + }, +} + +# This is the expected output for the unittest_folder tests run where two tests +# run are in different files. +# └── unittest_folder +# β”œβ”€β”€ test_add.py +# β”‚ └── TestAddFunction +# β”‚ └── test_add_positive_numbers: success +# └── test_subtract.py +# └── TestSubtractFunction +# └── test_subtract_positive_numbers: success +test_subtract_path = TEST_DATA_PATH / "unittest_folder" / "test_subtract.py" +test_add_path = TEST_DATA_PATH / "unittest_folder" / "test_add.py" + +uf_non_adjacent_tests_execution_expected_output = { + get_absolute_test_id( + f"{TEST_SUBTRACT_FUNCTION}test_subtract_positive_numbers", test_subtract_path + ): { + "test": get_absolute_test_id( + f"{TEST_SUBTRACT_FUNCTION}test_subtract_positive_numbers", + test_subtract_path, + ), + "outcome": SUCCESS, + "message": None, + "traceback": None, + "subtest": None, + }, + get_absolute_test_id(f"{TEST_ADD_FUNCTION}test_add_positive_numbers", test_add_path): { + "test": get_absolute_test_id( + f"{TEST_ADD_FUNCTION}test_add_positive_numbers", test_add_path + ), + "outcome": SUCCESS, + "message": None, + "traceback": None, + "subtest": None, + }, +} + + +# This is the expected output for the simple_pytest.py file. +# └── simple_pytest.py +# └── test_function: success +simple_pytest_path = TEST_DATA_PATH / "unittest_folder" / "simple_pytest.py" + +simple_execution_pytest_expected_output = { + get_absolute_test_id("test_function", simple_pytest_path): { + "test": get_absolute_test_id("test_function", simple_pytest_path), + "outcome": "success", + "message": None, + "traceback": None, + "subtest": None, + } +} + + +# This is the expected output for the unittest_pytest_same_file.py file. +# β”œβ”€β”€ unittest_pytest_same_file.py +# β”œβ”€β”€ TestExample +# β”‚ └── test_true_unittest: success +# └── test_true_pytest: success +unit_pytest_same_file_path = TEST_DATA_PATH / "unittest_pytest_same_file.py" +unit_pytest_same_file_execution_expected_output = { + get_absolute_test_id( + "unittest_pytest_same_file.py::TestExample::test_true_unittest", + unit_pytest_same_file_path, + ): { + "test": get_absolute_test_id( + "unittest_pytest_same_file.py::TestExample::test_true_unittest", + unit_pytest_same_file_path, + ), + "outcome": "success", + "message": None, + "traceback": None, + "subtest": None, + }, + get_absolute_test_id( + "unittest_pytest_same_file.py::test_true_pytest", unit_pytest_same_file_path + ): { + "test": get_absolute_test_id( + "unittest_pytest_same_file.py::test_true_pytest", + unit_pytest_same_file_path, + ), + "outcome": "success", + "message": None, + "traceback": None, + "subtest": None, + }, +} + +# This is the expected output for the error_raised_exception.py file. +# └── error_raise_exception.py +# β”œβ”€β”€ TestSomething +# β”‚ └── test_a: failure +error_raised_exception_path = TEST_DATA_PATH / "error_raise_exception.py" +error_raised_exception_execution_expected_output = { + get_absolute_test_id( + "error_raise_exception.py::TestSomething::test_a", error_raised_exception_path + ): { + "test": get_absolute_test_id( + "error_raise_exception.py::TestSomething::test_a", + error_raised_exception_path, + ), + "outcome": "error", + "message": "ERROR MESSAGE", + "traceback": "TRACEBACK", + "subtest": None, + } +} + +# This is the expected output for the skip_tests.py file. +# └── test_something: success +# └── test_another_thing: skipped +# └── test_decorator_thing: skipped +# └── test_decorator_thing_2: skipped +# β”œβ”€β”€ TestClass +# β”‚ └── test_class_function_a: skipped +# β”‚ └── test_class_function_b: skipped + +skip_tests_path = TEST_DATA_PATH / "skip_tests.py" +skip_tests_execution_expected_output = { + get_absolute_test_id("skip_tests.py::test_something", skip_tests_path): { + "test": get_absolute_test_id("skip_tests.py::test_something", skip_tests_path), + "outcome": "success", + "message": None, + "traceback": None, + "subtest": None, + }, + get_absolute_test_id("skip_tests.py::test_another_thing", skip_tests_path): { + "test": get_absolute_test_id("skip_tests.py::test_another_thing", skip_tests_path), + "outcome": "skipped", + "message": None, + "traceback": None, + "subtest": None, + }, + get_absolute_test_id("skip_tests.py::test_decorator_thing", skip_tests_path): { + "test": get_absolute_test_id("skip_tests.py::test_decorator_thing", skip_tests_path), + "outcome": "skipped", + "message": None, + "traceback": None, + "subtest": None, + }, + get_absolute_test_id("skip_tests.py::test_decorator_thing_2", skip_tests_path): { + "test": get_absolute_test_id("skip_tests.py::test_decorator_thing_2", skip_tests_path), + "outcome": "skipped", + "message": None, + "traceback": None, + "subtest": None, + }, + get_absolute_test_id("skip_tests.py::TestClass::test_class_function_a", skip_tests_path): { + "test": get_absolute_test_id( + "skip_tests.py::TestClass::test_class_function_a", skip_tests_path + ), + "outcome": "skipped", + "message": None, + "traceback": None, + "subtest": None, + }, + get_absolute_test_id("skip_tests.py::TestClass::test_class_function_b", skip_tests_path): { + "test": get_absolute_test_id( + "skip_tests.py::TestClass::test_class_function_b", skip_tests_path + ), + "outcome": "skipped", + "message": None, + "traceback": None, + "subtest": None, + }, +} + + +# This is the expected output for the dual_level_nested_folder.py tests +# └── dual_level_nested_folder +# └── test_top_folder.py +# └── test_top_function_t: success +# └── test_top_function_f: failure +# └── nested_folder_one +# └── test_bottom_folder.py +# └── test_bottom_function_t: success +# └── test_bottom_function_f: failure +dual_level_nested_folder_top_path = ( + TEST_DATA_PATH / "dual_level_nested_folder" / "test_top_folder.py" +) +dual_level_nested_folder_bottom_path = ( + TEST_DATA_PATH / "dual_level_nested_folder" / "nested_folder_one" / "test_bottom_folder.py" +) +dual_level_nested_folder_execution_expected_output = { + get_absolute_test_id( + "test_top_folder.py::test_top_function_t", dual_level_nested_folder_top_path + ): { + "test": get_absolute_test_id( + "test_top_folder.py::test_top_function_t", dual_level_nested_folder_top_path + ), + "outcome": "success", + "message": None, + "traceback": None, + "subtest": None, + }, + get_absolute_test_id( + "test_top_folder.py::test_top_function_f", dual_level_nested_folder_top_path + ): { + "test": get_absolute_test_id( + "test_top_folder.py::test_top_function_f", dual_level_nested_folder_top_path + ), + "outcome": "failure", + "message": "ERROR MESSAGE", + "traceback": None, + "subtest": None, + }, + get_absolute_test_id( + "nested_folder_one/test_bottom_folder.py::test_bottom_function_t", + dual_level_nested_folder_bottom_path, + ): { + "test": get_absolute_test_id( + "nested_folder_one/test_bottom_folder.py::test_bottom_function_t", + dual_level_nested_folder_bottom_path, + ), + "outcome": "success", + "message": None, + "traceback": None, + "subtest": None, + }, + get_absolute_test_id( + "nested_folder_one/test_bottom_folder.py::test_bottom_function_f", + dual_level_nested_folder_bottom_path, + ): { + "test": get_absolute_test_id( + "nested_folder_one/test_bottom_folder.py::test_bottom_function_f", + dual_level_nested_folder_bottom_path, + ), + "outcome": "failure", + "message": "ERROR MESSAGE", + "traceback": None, + "subtest": None, + }, +} + +# This is the expected output for the nested_folder tests. +# └── folder_a +# └── folder_b +# └── folder_a +# └── test_nest.py +# └── test_function: success + +nested_folder_path = TEST_DATA_PATH / "folder_a" / "folder_b" / "folder_a" / "test_nest.py" +double_nested_folder_expected_execution_output = { + get_absolute_test_id( + "folder_a/folder_b/folder_a/test_nest.py::test_function", nested_folder_path + ): { + "test": get_absolute_test_id( + "folder_a/folder_b/folder_a/test_nest.py::test_function", nested_folder_path + ), + "outcome": "success", + "message": None, + "traceback": None, + "subtest": None, + } +} +# This is the expected output for the nested_folder tests. +# └── parametrize_tests.py +# └── TestClass +# └── test_adding[3+5-8]: success +# └── test_adding[2+4-6]: success +# └── test_adding[6+9-16]: failure +parametrize_tests_path = TEST_DATA_PATH / "parametrize_tests.py" + +parametrize_tests_expected_execution_output = { + get_absolute_test_id( + "parametrize_tests.py::TestClass::test_adding[3+5-8]", parametrize_tests_path + ): { + "test": get_absolute_test_id( + "parametrize_tests.py::TestClass::test_adding[3+5-8]", parametrize_tests_path + ), + "outcome": "success", + "message": None, + "traceback": None, + "subtest": None, + }, + get_absolute_test_id( + "parametrize_tests.py::TestClass::test_adding[2+4-6]", parametrize_tests_path + ): { + "test": get_absolute_test_id( + "parametrize_tests.py::TestClass::test_adding[2+4-6]", parametrize_tests_path + ), + "outcome": "success", + "message": None, + "traceback": None, + "subtest": None, + }, + get_absolute_test_id( + "parametrize_tests.py::TestClass::test_adding[6+9-16]", parametrize_tests_path + ): { + "test": get_absolute_test_id( + "parametrize_tests.py::TestClass::test_adding[6+9-16]", parametrize_tests_path + ), + "outcome": "failure", + "message": "ERROR MESSAGE", + "traceback": None, + "subtest": None, + }, +} + +# This is the expected output for the single parameterized tests. +# └── parametrize_tests.py +# └── TestClass +# └── test_adding[3+5-8]: success +single_parametrize_tests_expected_execution_output = { + get_absolute_test_id( + "parametrize_tests.py::TestClass::test_adding[3+5-8]", parametrize_tests_path + ): { + "test": get_absolute_test_id( + "parametrize_tests.py::TestClass::test_adding[3+5-8]", parametrize_tests_path + ), + "outcome": "success", + "message": None, + "traceback": None, + "subtest": None, + }, +} + +# This is the expected output for the single parameterized tests. +# └── text_docstring.txt +# └── text_docstring: success +doc_test_path = TEST_DATA_PATH / "text_docstring.txt" +doctest_pytest_expected_execution_output = { + get_absolute_test_id("text_docstring.txt::text_docstring.txt", doc_test_path): { + "test": get_absolute_test_id("text_docstring.txt::text_docstring.txt", doc_test_path), + "outcome": "success", + "message": None, + "traceback": None, + "subtest": None, + } +} + +# Will run all tests in the cwd that fit the test file naming pattern. +folder_a_path = TEST_DATA_PATH / "folder_a" / "folder_b" / "folder_a" / "test_nest.py" +dual_level_nested_folder_top_path = ( + TEST_DATA_PATH / "dual_level_nested_folder" / "test_top_folder.py" +) +dual_level_nested_folder_bottom_path = ( + TEST_DATA_PATH / "dual_level_nested_folder" / "nested_folder_one" / "test_bottom_folder.py" +) +unittest_folder_add_path = TEST_DATA_PATH / "unittest_folder" / "test_add.py" +unittest_folder_subtract_path = TEST_DATA_PATH / "unittest_folder" / "test_subtract.py" + +no_test_ids_pytest_execution_expected_output = { + get_absolute_test_id("test_function", folder_a_path): { + "test": get_absolute_test_id("test_function", folder_a_path), + "outcome": "success", + "message": None, + "traceback": None, + "subtest": None, + }, + get_absolute_test_id("test_top_function_t", dual_level_nested_folder_top_path): { + "test": get_absolute_test_id("test_top_function_t", dual_level_nested_folder_top_path), + "outcome": "success", + "message": None, + "traceback": None, + "subtest": None, + }, + get_absolute_test_id("test_top_function_f", dual_level_nested_folder_top_path): { + "test": get_absolute_test_id("test_top_function_f", dual_level_nested_folder_top_path), + "outcome": "failure", + "message": "ERROR MESSAGE", + "traceback": None, + "subtest": None, + }, + get_absolute_test_id("test_bottom_function_t", dual_level_nested_folder_bottom_path): { + "test": get_absolute_test_id( + "test_bottom_function_t", dual_level_nested_folder_bottom_path + ), + "outcome": "success", + "message": None, + "traceback": None, + "subtest": None, + }, + get_absolute_test_id("test_bottom_function_f", dual_level_nested_folder_bottom_path): { + "test": get_absolute_test_id( + "test_bottom_function_f", dual_level_nested_folder_bottom_path + ), + "outcome": "failure", + "message": "ERROR MESSAGE", + "traceback": None, + "subtest": None, + }, + get_absolute_test_id("TestAddFunction::test_add_negative_numbers", unittest_folder_add_path): { + "test": get_absolute_test_id( + "TestAddFunction::test_add_negative_numbers", unittest_folder_add_path + ), + "outcome": "success", + "message": None, + "traceback": None, + "subtest": None, + }, + get_absolute_test_id("TestAddFunction::test_add_positive_numbers", unittest_folder_add_path): { + "test": get_absolute_test_id( + "TestAddFunction::test_add_positive_numbers", unittest_folder_add_path + ), + "outcome": "success", + "message": None, + "traceback": None, + "subtest": None, + }, + get_absolute_test_id( + "TestSubtractFunction::test_subtract_negative_numbers", + unittest_folder_subtract_path, + ): { + "test": get_absolute_test_id( + "TestSubtractFunction::test_subtract_negative_numbers", + unittest_folder_subtract_path, + ), + "outcome": "failure", + "message": "ERROR MESSAGE", + "traceback": None, + "subtest": None, + }, + get_absolute_test_id( + "TestSubtractFunction::test_subtract_positive_numbers", + unittest_folder_subtract_path, + ): { + "test": get_absolute_test_id( + "TestSubtractFunction::test_subtract_positive_numbers", + unittest_folder_subtract_path, + ), + "outcome": "success", + "message": None, + "traceback": None, + "subtest": None, + }, +} + +# This is the expected output for the root folder with the config file referenced. +# └── test_a.py +# └── test_a_function: success +test_add_path = TEST_DATA_PATH / "root" / "tests" / "test_a.py" +config_file_pytest_expected_execution_output = { + get_absolute_test_id("tests/test_a.py::test_a_function", test_add_path): { + "test": get_absolute_test_id("tests/test_a.py::test_a_function", test_add_path), + "outcome": "success", + "message": None, + "traceback": None, + "subtest": None, + } +} + + +# This is the expected output for the test logging file. +# └── test_logging.py +# └── test_logging2: failure +# └── test_logging: success +test_logging_path = TEST_DATA_PATH / "test_logging.py" + +logging_test_expected_execution_output = { + get_absolute_test_id("test_logging.py::test_logging2", test_logging_path): { + "test": get_absolute_test_id("test_logging.py::test_logging2", test_logging_path), + "outcome": "failure", + "message": "ERROR MESSAGE", + "traceback": None, + "subtest": None, + }, + get_absolute_test_id("test_logging.py::test_logging", test_logging_path): { + "test": get_absolute_test_id("test_logging.py::test_logging", test_logging_path), + "outcome": "success", + "message": None, + "traceback": None, + "subtest": None, + }, +} + +# This is the expected output for the test safe clear env vars file. +# └── test_env_vars.py +# └── test_clear_env: success +# └── test_check_env: success + +test_safe_clear_env_vars_path = TEST_DATA_PATH / "test_env_vars.py" +safe_clear_env_vars_expected_execution_output = { + get_absolute_test_id("test_env_vars.py::test_clear_env", test_safe_clear_env_vars_path): { + "test": get_absolute_test_id( + "test_env_vars.py::test_clear_env", test_safe_clear_env_vars_path + ), + "outcome": "success", + "message": None, + "traceback": None, + "subtest": None, + }, + get_absolute_test_id("test_env_vars.py::test_check_env", test_safe_clear_env_vars_path): { + "test": get_absolute_test_id( + "test_env_vars.py::test_check_env", test_safe_clear_env_vars_path + ), + "outcome": "success", + "message": None, + "traceback": None, + "subtest": None, + }, +} + +# This is the expected output for the test unsafe clear env vars file. +# └── test_env_vars.py +# └── test_clear_env_unsafe: success +# └── test_check_env_unsafe: success +unsafe_clear_env_vars_expected_execution_output = { + get_absolute_test_id( + "test_env_vars.py::test_clear_env_unsafe", test_safe_clear_env_vars_path + ): { + "test": get_absolute_test_id( + "test_env_vars.py::test_clear_env_unsafe", test_safe_clear_env_vars_path + ), + "outcome": "success", + "message": None, + "traceback": None, + "subtest": None, + }, + get_absolute_test_id( + "test_env_vars.py::test_check_env_unsafe", test_safe_clear_env_vars_path + ): { + "test": get_absolute_test_id( + "test_env_vars.py::test_check_env_unsafe", test_safe_clear_env_vars_path + ), + "outcome": "success", + "message": None, + "traceback": None, + "subtest": None, + }, +} + +# Constant for the symlink execution test where TEST_DATA_PATH / "root" the target and TEST_DATA_PATH / "symlink_folder" the symlink +test_a_symlink_path = TEST_DATA_PATH / "symlink_folder" / "tests" / "test_a.py" +symlink_run_expected_execution_output = { + get_absolute_test_id("test_a.py::test_a_function", test_a_symlink_path): { + "test": get_absolute_test_id("test_a.py::test_a_function", test_a_symlink_path), + "outcome": "success", + "message": None, + "traceback": None, + "subtest": None, + } +} + + +# This is the expected output for the pytest_describe_plugin/describe_only.py file. +# └── pytest_describe_plugin +# └── describe_only.py +# └── describe_A +# └── test_1: success +# └── test_2: success + +describe_only_expected_execution_output = { + get_absolute_test_id( + "pytest_describe_plugin/describe_only.py::describe_A::test_1", + TEST_DATA_PATH / "pytest_describe_plugin" / "describe_only.py", + ): { + "test": get_absolute_test_id( + "pytest_describe_plugin/describe_only.py::describe_A::test_1", + TEST_DATA_PATH / "pytest_describe_plugin" / "describe_only.py", + ), + "outcome": "success", + "message": None, + "traceback": None, + "subtest": None, + }, + get_absolute_test_id( + "pytest_describe_plugin/describe_only.py::describe_A::test_2", + TEST_DATA_PATH / "pytest_describe_plugin" / "describe_only.py", + ): { + "test": get_absolute_test_id( + "pytest_describe_plugin/describe_only.py::describe_A::test_2", + TEST_DATA_PATH / "pytest_describe_plugin" / "describe_only.py", + ), + "outcome": "success", + "message": None, + "traceback": None, + "subtest": None, + }, +} + +# This is the expected output for the pytest_describe_plugin/nested_describe.py file. +# └── pytest_describe_plugin +# └── nested_describe.py +# └── describe_list +# └── describe_append +# └── add_empty: success +# └── remove_empty: success +# └── describe_remove +# └── removes: success +nested_describe_expected_execution_output = { + get_absolute_test_id( + "pytest_describe_plugin/nested_describe.py::describe_list::describe_append::add_empty", + TEST_DATA_PATH / "pytest_describe_plugin" / "nested_describe.py", + ): { + "test": get_absolute_test_id( + "pytest_describe_plugin/nested_describe.py::describe_list::describe_append::add_empty", + TEST_DATA_PATH / "pytest_describe_plugin" / "nested_describe.py", + ), + "outcome": "success", + "message": None, + "traceback": None, + "subtest": None, + }, + get_absolute_test_id( + "pytest_describe_plugin/nested_describe.py::describe_list::describe_append::remove_empty", + TEST_DATA_PATH / "pytest_describe_plugin" / "nested_describe.py", + ): { + "test": get_absolute_test_id( + "pytest_describe_plugin/nested_describe.py::describe_list::describe_append::remove_empty", + TEST_DATA_PATH / "pytest_describe_plugin" / "nested_describe.py", + ), + "outcome": "success", + "message": None, + "traceback": None, + "subtest": None, + }, + get_absolute_test_id( + "pytest_describe_plugin/nested_describe.py::describe_list::describe_remove::removes", + TEST_DATA_PATH / "pytest_describe_plugin" / "nested_describe.py", + ): { + "test": get_absolute_test_id( + "pytest_describe_plugin/nested_describe.py::describe_list::describe_remove::removes", + TEST_DATA_PATH / "pytest_describe_plugin" / "nested_describe.py", + ), + "outcome": "success", + "message": None, + "traceback": None, + "subtest": None, + }, +} + +skip_test_fixture_path = TEST_DATA_PATH / "skip_test_fixture.py" +skip_test_fixture_execution_expected_output = { + get_absolute_test_id("skip_test_fixture.py::test_docker_client", skip_test_fixture_path): { + "test": get_absolute_test_id( + "skip_test_fixture.py::test_docker_client", skip_test_fixture_path + ), + "outcome": "skipped", + "message": None, + "traceback": None, + "subtest": None, + } +} diff --git a/python_files/tests/pytestadapter/helpers.py b/python_files/tests/pytestadapter/helpers.py new file mode 100644 index 000000000000..03f1187149df --- /dev/null +++ b/python_files/tests/pytestadapter/helpers.py @@ -0,0 +1,469 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +import contextlib +import io +import json +import os +import pathlib +import socket +import subprocess +import sys +import tempfile +import threading +import uuid +from typing import Any, Dict, List, Optional, Tuple + +if sys.platform == "win32": + from namedpipe import NPopen + + +script_dir = pathlib.Path(__file__).parent.parent.parent +script_dir_child = pathlib.Path(__file__).parent.parent +sys.path.append(os.fspath(script_dir)) +sys.path.append(os.fspath(script_dir_child)) +sys.path.append(os.fspath(script_dir / "lib" / "python")) +print("sys add path", script_dir) + +TEST_DATA_PATH = pathlib.Path(__file__).parent / ".data" +CONTENT_LENGTH: str = "Content-Length:" +CONTENT_TYPE: str = "Content-Type:" + + +@contextlib.contextmanager +def text_to_python_file(text_file_path: pathlib.Path): + """Convert a text file to a python file and yield the python file path.""" + python_file = None + try: + contents = text_file_path.read_text(encoding="utf-8") + python_file = text_file_path.with_suffix(".py") + python_file.write_text(contents, encoding="utf-8") + yield python_file + finally: + if python_file: + python_file.unlink() + + +@contextlib.contextmanager +def create_symlink(root: pathlib.Path, target_ext: str, destination_ext: str): + destination = None + try: + destination = root / destination_ext + target = root / target_ext + if destination and destination.exists(): + print("destination already exists", destination) + try: + destination.symlink_to(target) + except Exception as e: + print("error occurred when attempting to create a symlink", e) + yield target, destination + finally: + if destination and destination.exists(): + destination.unlink() + print("destination unlinked", destination) + + +def process_data_received(data: str) -> List[Dict[str, Any]]: + """Process the all JSON data which comes from the server. + + After listen is finished, this function will be called. + Here the data must be split into individual JSON messages and then parsed. + + This function also: + - Checks that the jsonrpc value is 2.0 + """ + json_messages = [] + remaining = data + while remaining: + json_data, remaining = parse_rpc_message(remaining) + # here json_data is a single rpc payload, now check its jsonrpc 2 and save the param data + if "params" not in json_data or "jsonrpc" not in json_data: + raise ValueError("Invalid JSON-RPC message received, missing params or jsonrpc key") + elif json_data["jsonrpc"] != "2.0": + raise ValueError("Invalid JSON-RPC version received, not version 2.0") + else: + json_messages.append(json_data["params"]) + + return json_messages # return the list of json messages + + +def parse_rpc_message(data: str) -> Tuple[Dict[str, str], str]: + """Process the JSON data which comes from the server. + + A single rpc payload is in the format: + content-length: #LEN# \r\ncontent-type: application/json\r\n\r\n{"jsonrpc": "2.0", "params": ENTIRE_DATA} + + returns: + json_data: A single rpc payload of JSON data from the server. + remaining: The remaining data after the JSON data. + """ + str_stream: io.StringIO = io.StringIO(data) + + length: int = 0 + while True: + line: str = str_stream.readline() + if CONTENT_LENGTH.lower() in line.lower(): + length = int(line[len(CONTENT_LENGTH) :]) + + line: str = str_stream.readline() + if CONTENT_TYPE.lower() not in line.lower(): + raise ValueError("Header does not contain Content-Type") + + line = str_stream.readline() + if line not in ["\r\n", "\n"]: + raise ValueError("Header does not contain space to separate header and body") + # if it passes all these checks then it has the right headers + break + + if not line or line.isspace(): + raise ValueError("Header does not contain Content-Length") + + while True: # keep reading until the number of bytes is the CONTENT_LENGTH + line: str = str_stream.readline(length) + try: + # try to parse the json, if successful it is single payload so return with remaining data + json_data: dict[str, str] = json.loads(line) + return json_data, str_stream.read() + except json.JSONDecodeError: + print("json decode error") + + +def _listen_on_fifo(pipe_name: str, result: List[str], completed: threading.Event): + # Open the FIFO for reading + fifo_path = pathlib.Path(pipe_name) + with fifo_path.open() as fifo: + print("Waiting for data...") + while True: + if completed.is_set(): + break # Exit loop if completed event is set + data = fifo.read() # This will block until data is available + if len(data) == 0: + # If data is empty, assume EOF + break + print(f"Received: {data}") + result.append(data) + + +def _listen_on_pipe_new(listener, result: List[str], completed: threading.Event): + """Listen on the named pipe or Unix domain socket for JSON data from the server. + + Created as a separate function for clarity in threading context. + """ + # Windows design + if sys.platform == "win32": + all_data: list = [] + stream = listener.wait() + while True: + # Read data from collection + close = stream.closed + if close: + break + data = stream.readlines() + if not data: + if completed.is_set(): + break # Exit loop if completed event is set + else: + try: + # Attempt to accept another connection if the current one closes unexpectedly + print("attempt another connection") + except socket.timeout: + # On timeout, append all collected data to result and return + # result.append("".join(all_data)) + return + data_decoded = "".join(data) + all_data.append(data_decoded) + # Append all collected data to result array + result.append("".join(all_data)) + else: # Unix design + connection, _ = listener.socket.accept() + listener.socket.settimeout(1) + all_data: list = [] + while True: + # Reading from connection + data: bytes = connection.recv(1024 * 1024) + if not data: + if completed.is_set(): + break # Exit loop if completed event is set + else: + try: + # Attempt to accept another connection if the current one closes unexpectedly + connection, _ = listener.socket.accept() + except socket.timeout: + # On timeout, append all collected data to result and return + result.append("".join(all_data)) + return + all_data.append(data.decode("utf-8")) + # Append all collected data to result array + result.append("".join(all_data)) + + +def _run_test_code(proc_args: List[str], proc_env, proc_cwd: str, completed: threading.Event): + result = subprocess.run(proc_args, env=proc_env, cwd=proc_cwd) + completed.set() + return result + + +def runner(args: List[str]) -> Optional[List[Dict[str, Any]]]: + """Run a subprocess and a named-pipe to listen for messages at the same time with threading.""" + print("\n Running python test subprocess with cwd set to: ", TEST_DATA_PATH) + return runner_with_cwd(args, TEST_DATA_PATH) + + +def runner_with_cwd(args: List[str], path: pathlib.Path) -> Optional[List[Dict[str, Any]]]: + """Run a subprocess and a named-pipe to listen for messages at the same time with threading.""" + return runner_with_cwd_env(args, path, {}) + + +def split_array_at_item(arr: List[str], item: str) -> Tuple[List[str], List[str]]: + """ + Splits an array into two subarrays at the specified item. + + Args: + arr (List[str]): The array to be split. + item (str): The item at which to split the array. + + Returns: + Tuple[List[str], List[str]]: A tuple containing two subarrays. The first subarray includes the item and all elements before it. The second subarray includes all elements after the item. If the item is not found, the first subarray is the original array and the second subarray is empty. + """ + if item in arr: + index = arr.index(item) + before = arr[: index + 1] + after = arr[index + 1 :] + return before, after + else: + return arr, [] + + +def runner_with_cwd_env( + args: List[str], path: pathlib.Path, env_add: Dict[str, str] +) -> Optional[List[Dict[str, Any]]]: + """ + Run a subprocess and a named-pipe to listen for messages at the same time with threading. + + Includes environment variables to add to the test environment. + """ + process_args: List[str] + pipe_name: str + if "MANAGE_PY_PATH" in env_add and "COVERAGE_ENABLED" not in env_add: + # If we are running Django, generate a unittest-specific pipe name. + process_args = [sys.executable, *args] + pipe_name = generate_random_pipe_name("unittest-discovery-test") + elif "_TEST_VAR_UNITTEST" in env_add: + before_args, after_ids = split_array_at_item(args, "*test*.py") + process_args = [sys.executable, *before_args] + pipe_name = generate_random_pipe_name("unittest-execution-test") + test_ids_pipe = os.fspath( + script_dir / "tests" / "unittestadapter" / ".data" / "coverage_ex" / "10943021.txt" + ) + env_add.update({"RUN_TEST_IDS_PIPE": test_ids_pipe}) + test_ids_arr = after_ids + with open(test_ids_pipe, "w") as f: # noqa: PTH123 + f.write("\n".join(test_ids_arr)) + else: + process_args = [sys.executable, "-m", "pytest", "-p", "vscode_pytest", "-s", *args] + pipe_name = generate_random_pipe_name("pytest-discovery-test") + + if "COVERAGE_ENABLED" in env_add and "_TEST_VAR_UNITTEST" not in env_add: + if "_PYTEST_MANUAL_PLUGIN_LOAD" in env_add: + # Test manual plugin loading scenario for issue #25590 + process_args = [ + sys.executable, + "-m", + "pytest", + "--disable-plugin-autoload", + "-p", + "pytest_cov.plugin", + "-p", + "vscode_pytest", + "--cov=.", + "--cov-branch", + "-s", + *args, + ] + else: + process_args = [ + sys.executable, + "-m", + "pytest", + "-p", + "vscode_pytest", + "--cov=.", + "--cov-branch", + "-s", + *args, + ] + + # Generate pipe name, pipe name specific per OS type. + + # Windows design + if sys.platform == "win32": + with NPopen("r+t", name=pipe_name, bufsize=0) as pipe: + # Update the environment with the pipe name and PYTHONPATH. + env = os.environ.copy() + env.update( + { + "TEST_RUN_PIPE": pipe.path, + "PYTHONPATH": os.fspath(pathlib.Path(__file__).parent.parent.parent), + } + ) + # if additional environment variables are passed, add them to the environment + if env_add: + env.update(env_add) + + completed = threading.Event() + + result = [] # result is a string array to store the data during threading + t1: threading.Thread = threading.Thread( + target=_listen_on_pipe_new, args=(pipe, result, completed) + ) + t1.start() + + t2 = threading.Thread( + target=_run_test_code, + args=(process_args, env, path, completed), + ) + t2.start() + + t1.join() + t2.join() + + return process_data_received(result[0]) if result else None + else: # Unix design + # Update the environment with the pipe name and PYTHONPATH. + env = os.environ.copy() + env.update( + { + "TEST_RUN_PIPE": pipe_name, + "PYTHONPATH": os.fspath(pathlib.Path(__file__).parent.parent.parent), + } + ) + # if additional environment variables are passed, add them to the environment + if env_add: + env.update(env_add) + # server = UnixPipeServer(pipe_name) + # server.start() + ################# + # Create the FIFO (named pipe) if it doesn't exist + # if not pathlib.Path.exists(pipe_name): + os.mkfifo(pipe_name) + ################# + + completed = threading.Event() + + result = [] # result is a string array to store the data during threading + t1: threading.Thread = threading.Thread( + target=_listen_on_fifo, args=(pipe_name, result, completed) + ) + t1.start() + + t2: threading.Thread = threading.Thread( + target=_run_test_code, + args=(process_args, env, path, completed), + ) + + t2.start() + + t1.join() + t2.join() + + return process_data_received(result[0]) if result else None + + +def find_test_line_number(test_name: str, test_file_path) -> str: + """Function which finds the correct line number for a test by looking for the "test_marker--[test_name]" string. + + The test_name is split on the "[" character to remove the parameterization information. + + Args: + test_name: The name of the test to find the line number for, will be unique per file. + test_file_path: The path to the test file where the test is located. + """ + test_file_unique_id: str = "test_marker--" + test_name.split("[")[0] + with open(test_file_path) as f: # noqa: PTH123 + for i, line in enumerate(f): + if test_file_unique_id in line: + return str(i + 1) + error_str: str = f"Test {test_name!r} not found on any line in {test_file_path}" + raise ValueError(error_str) + + +def find_class_line_number(class_name: str, test_file_path) -> str: + """Function which finds the correct line number for a class definition. + + Args: + class_name: The name of the class to find the line number for. + test_file_path: The path to the test file where the class is located. + """ + # Look for the class definition line (or function for pytest-describe) + with open(test_file_path) as f: # noqa: PTH123 + for i, line in enumerate(f): + # Match "class ClassName" or "class ClassName(" or "class ClassName:" + # Also match "def ClassName(" for pytest-describe blocks + if ( + line.strip().startswith(f"class {class_name}") + or line.strip().startswith(f"class {class_name}(") + or line.strip().startswith(f"def {class_name}(") + ): + return str(i + 1) + error_str: str = f"Class {class_name!r} not found on any line in {test_file_path}" + raise ValueError(error_str) + + +def get_absolute_test_id(test_id: str, test_path: pathlib.Path) -> str: + """Get the absolute test id by joining the testPath with the test_id.""" + split_id = test_id.split("::")[1:] + return "::".join([str(test_path), *split_id]) + + +def generate_random_pipe_name(prefix=""): + # Generate a random suffix using UUID4, ensuring uniqueness. + random_suffix = uuid.uuid4().hex[:10] + # Default prefix if not provided. + if not prefix: + prefix = "python-ext-rpc" + + # For Windows, named pipes have a specific naming convention. + if sys.platform == "win32": + return f"\\\\.\\pipe\\{prefix}-{random_suffix}" + + # For Unix-like systems, use either the XDG_RUNTIME_DIR or a temporary directory. + xdg_runtime_dir = os.getenv("XDG_RUNTIME_DIR") + if xdg_runtime_dir: + return os.path.join(xdg_runtime_dir, f"{prefix}-{random_suffix}") # noqa: PTH118 + else: + return os.path.join(tempfile.gettempdir(), f"{prefix}-{random_suffix}") # noqa: PTH118 + + +class UnixPipeServer: + def __init__(self, name): + self.name = name + self.is_windows = sys.platform == "win32" + if self.is_windows: + raise NotImplementedError( + "This class is only intended for Unix-like systems, not Windows." + ) + else: + # For Unix-like systems, use a Unix domain socket. + self.socket = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) + # Ensure the socket does not already exist + try: + os.unlink(self.name) # noqa: PTH108 + except OSError: + if os.path.exists(self.name): # noqa: PTH110 + raise + + def start(self): + if self.is_windows: + raise NotImplementedError( + "This class is only intended for Unix-like systems, not Windows." + ) + else: + # Bind the socket to the address and listen for incoming connections. + self.socket.bind(self.name) + self.socket.listen(1) + print(f"Server listening on {self.name}") + + def stop(self): + # Clean up the server socket. + self.socket.close() + print("Server stopped.") diff --git a/python_files/tests/pytestadapter/test_coverage.py b/python_files/tests/pytestadapter/test_coverage.py new file mode 100644 index 000000000000..f2387527698f --- /dev/null +++ b/python_files/tests/pytestadapter/test_coverage.py @@ -0,0 +1,164 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. +import json +import os +import pathlib +import sys + +import coverage +import pytest +from packaging.version import Version + +script_dir = pathlib.Path(__file__).parent.parent +sys.path.append(os.fspath(script_dir)) + +from .helpers import ( # noqa: E402 + TEST_DATA_PATH, + runner_with_cwd_env, +) + + +def test_simple_pytest_coverage(): + """ + Test coverage payload is correct for simple pytest example. Output of coverage run is below. + + Name Stmts Miss Branch BrPart Cover + --------------------------------------------------- + __init__.py 0 0 0 0 100% + reverse.py 13 3 8 2 76% + test_reverse.py 11 0 0 0 100% + --------------------------------------------------- + TOTAL 24 3 8 2 84% + + """ + args = [] + env_add = {"COVERAGE_ENABLED": "True"} + cov_folder_path = TEST_DATA_PATH / "coverage_gen" + actual = runner_with_cwd_env(args, cov_folder_path, env_add) + assert actual + cov = actual[-1] + assert cov + results = cov["result"] + assert results + assert len(results) == 3 + focal_function_coverage = results.get(os.fspath(TEST_DATA_PATH / "coverage_gen" / "reverse.py")) + assert focal_function_coverage + assert focal_function_coverage.get("lines_covered") is not None + assert focal_function_coverage.get("lines_missed") is not None + assert set(focal_function_coverage.get("lines_covered")) == {4, 5, 7, 9, 10, 11, 12, 13, 14, 17} + assert len(set(focal_function_coverage.get("lines_missed"))) >= 3 + + coverage_version = Version(coverage.__version__) + # only include check for branches if the version is >= 7.7.0 + if coverage_version >= Version("7.7.0"): + assert focal_function_coverage.get("executed_branches") == 4 + assert focal_function_coverage.get("total_branches") == 6 + + +coverage_gen_file_path = TEST_DATA_PATH / "coverage_gen" / "coverage.json" + + +@pytest.fixture +def cleanup_coverage_gen_file(): + # delete the coverage file if it exists as part of test cleanup + yield + if os.path.exists(coverage_gen_file_path): # noqa: PTH110 + os.remove(coverage_gen_file_path) # noqa: PTH107 + + +def test_coverage_gen_report(cleanup_coverage_gen_file): # noqa: ARG001 + """ + Test coverage payload is correct for simple pytest example. Output of coverage run is below. + + Name Stmts Miss Branch BrPart Cover + --------------------------------------------------- + __init__.py 0 0 0 0 100% + reverse.py 13 3 8 2 76% + test_reverse.py 11 0 0 0 100% + --------------------------------------------------- + TOTAL 24 3 8 2 84% + + """ + args = ["--cov-report=json"] + env_add = {"COVERAGE_ENABLED": "True"} + cov_folder_path = TEST_DATA_PATH / "coverage_gen" + print("cov_folder_path", cov_folder_path) + actual = runner_with_cwd_env(args, cov_folder_path, env_add) + assert actual + cov = actual[-1] + assert cov + results = cov["result"] + assert results + assert len(results) == 3 + focal_function_coverage = results.get(os.fspath(TEST_DATA_PATH / "coverage_gen" / "reverse.py")) + assert focal_function_coverage + assert focal_function_coverage.get("lines_covered") is not None + assert focal_function_coverage.get("lines_missed") is not None + assert set(focal_function_coverage.get("lines_covered")) == {4, 5, 7, 9, 10, 11, 12, 13, 14, 17} + assert set(focal_function_coverage.get("lines_missed")) == {18, 19, 6} + coverage_version = Version(coverage.__version__) + # only include check for branches if the version is >= 7.7.0 + if coverage_version >= Version("7.7.0"): + assert focal_function_coverage.get("executed_branches") == 4 + assert focal_function_coverage.get("total_branches") == 6 + # assert that the coverage file was created at the right path + assert os.path.exists(coverage_gen_file_path) # noqa: PTH110 + + +def test_coverage_w_omit_config(): + """ + Test the coverage report generation with omit configuration. + + folder structure of coverage_w_config + β”œβ”€β”€ coverage_w_config + β”‚ β”œβ”€β”€ test_ignore.py + β”‚ β”œβ”€β”€ test_ran.py + β”‚ └── pyproject.toml + β”‚ β”œβ”€β”€ tests + β”‚ β”‚ └── test_disregard.py + + pyproject.toml file with the following content: + [tool.coverage.report] + omit = [ + "test_ignore.py", + "tests/*.py" (this will ignore the coverage in the file tests/test_disregard.py) + ] + + + Assertions: + - The coverage report is generated. + - The coverage report contains results. + - Only one file is reported in the coverage results. + """ + env_add = {"COVERAGE_ENABLED": "True"} + cov_folder_path = TEST_DATA_PATH / "coverage_w_config" + print("cov_folder_path", cov_folder_path) + actual = runner_with_cwd_env([], cov_folder_path, env_add) + assert actual + print("actual", json.dumps(actual, indent=2)) + cov = actual[-1] + assert cov + results = cov["result"] + assert results + # assert one file is reported and one file (as specified in pyproject.toml) is omitted + assert len(results) == 1 + + +def test_pytest_cov_manual_plugin_loading(): + """ + Test that pytest-cov is detected when loaded manually via -p pytest_cov.plugin. + + This test verifies the fix for issue #25590, where pytest-cov detection failed + when using --disable-plugin-autoload with -p pytest_cov.plugin. The plugin is + registered under its module name (pytest_cov.plugin) instead of entry point name + (pytest_cov) in this scenario. + """ + args = ["--collect-only"] + env_add = {"COVERAGE_ENABLED": "True", "_PYTEST_MANUAL_PLUGIN_LOAD": "True"} + cov_folder_path = TEST_DATA_PATH / "coverage_gen" + + # Should NOT raise VSCodePytestError about pytest-cov not being installed + actual = runner_with_cwd_env(args, cov_folder_path, env_add) + assert actual is not None + # Verify discovery succeeded (status != "error") + assert actual[0].get("status") != "error" diff --git a/python_files/tests/pytestadapter/test_discovery.py b/python_files/tests/pytestadapter/test_discovery.py new file mode 100644 index 000000000000..cf777399fed9 --- /dev/null +++ b/python_files/tests/pytestadapter/test_discovery.py @@ -0,0 +1,482 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. +import json +import os +import sys +from typing import Any, Dict, List, Optional + +import pytest + +from tests.tree_comparison_helper import is_same_tree + +from . import expected_discovery_test_output, helpers + + +def test_import_error(): + """Test pytest discovery on a file that has a pytest marker but does not import pytest. + + Copies the contents of a .txt file to a .py file in the temporary directory + to then run pytest discovery on. + + The json should still be returned but the errors list should be present. + + Keyword arguments: + tmp_path -- pytest fixture that creates a temporary directory. + """ + file_path = helpers.TEST_DATA_PATH / "error_pytest_import.txt" + with helpers.text_to_python_file(file_path) as p: + actual: Optional[List[Dict[str, Any]]] = helpers.runner(["--collect-only", os.fspath(p)]) + + assert actual + actual_list: List[Dict[str, Any]] = actual + if actual_list is not None: + for actual_item in actual_list: + assert all(item in actual_item for item in ("status", "cwd", "error")) + assert actual_item.get("status") == "error" + assert actual_item.get("cwd") == os.fspath(helpers.TEST_DATA_PATH) + + # Ensure that 'error' is a list and then check its length + error_content = actual_item.get("error") + if error_content is not None and isinstance( + error_content, (list, tuple, str) + ): # You can add other types if needed + assert len(error_content) == 2 + else: + pytest.fail(f"{error_content} is None or not a list, str, or tuple") + + +def test_syntax_error(tmp_path): # noqa: ARG001 + """Test pytest discovery on a file that has a syntax error. + + Copies the contents of a .txt file to a .py file in the temporary directory + to then run pytest discovery on. + + The json should still be returned but the errors list should be present. + + Keyword arguments: + tmp_path -- pytest fixture that creates a temporary directory. + """ + # Saving some files as .txt to avoid that file displaying a syntax error for + # the extension as a whole. Instead, rename it before running this test + # in order to test the error handling. + file_path = helpers.TEST_DATA_PATH / "error_syntax_discovery.txt" + with helpers.text_to_python_file(file_path) as p: + actual = helpers.runner(["--collect-only", os.fspath(p)]) + + assert actual + actual_list: List[Dict[str, Any]] = actual + if actual_list is not None: + for actual_item in actual_list: + assert all(item in actual_item for item in ("status", "cwd", "error")) + assert actual_item.get("status") == "error" + assert actual_item.get("cwd") == os.fspath(helpers.TEST_DATA_PATH) + + # Ensure that 'error' is a list and then check its length + error_content = actual_item.get("error") + if error_content is not None and isinstance( + error_content, (list, tuple, str) + ): # You can add other types if needed + assert len(error_content) == 2 + else: + pytest.fail(f"{error_content} is None or not a list, str, or tuple") + + +def test_parameterized_error_collect(): + """Tests pytest discovery on specific file that incorrectly uses parametrize. + + The json should still be returned but the errors list should be present. + """ + file_path_str = "error_parametrize_discovery.py" + actual = helpers.runner(["--collect-only", file_path_str]) + assert actual + actual_list: List[Dict[str, Any]] = actual + if actual_list is not None: + for actual_item in actual_list: + assert all(item in actual_item for item in ("status", "cwd", "error")) + assert actual_item.get("status") == "error" + assert actual_item.get("cwd") == os.fspath(helpers.TEST_DATA_PATH) + + # Ensure that 'error' is a list and then check its length + error_content = actual_item.get("error") + if error_content is not None and isinstance( + error_content, (list, tuple, str) + ): # You can add other types if needed + assert len(error_content) == 2 + else: + pytest.fail(f"{error_content} is None or not a list, str, or tuple") + + +@pytest.mark.parametrize( + ("file", "expected_const"), + [ + ( + "test_param_span_class.py", + expected_discovery_test_output.test_param_span_class_expected_output, + ), + ( + "test_multi_class_nest.py", + expected_discovery_test_output.nested_classes_expected_test_output, + ), + ( + "same_function_new_class_param.py", + expected_discovery_test_output.same_function_new_class_param_expected_output, + ), + ( + "unittest_skiptest_file_level.py", + expected_discovery_test_output.unittest_skip_file_level_expected_output, + ), + ( + "param_same_name", + expected_discovery_test_output.param_same_name_expected_output, + ), + ( + "parametrize_tests.py", + expected_discovery_test_output.parametrize_tests_expected_output, + ), + ( + "empty_discovery.py", + expected_discovery_test_output.empty_discovery_pytest_expected_output, + ), + ( + "simple_pytest.py", + expected_discovery_test_output.simple_discovery_pytest_expected_output, + ), + ( + "unittest_pytest_same_file.py", + expected_discovery_test_output.unit_pytest_same_file_discovery_expected_output, + ), + ( + "unittest_folder", + expected_discovery_test_output.unittest_folder_discovery_expected_output, + ), + ( + "dual_level_nested_folder", + expected_discovery_test_output.dual_level_nested_folder_expected_output, + ), + ( + "folder_a", + expected_discovery_test_output.double_nested_folder_expected_output, + ), + ( + "text_docstring.txt", + expected_discovery_test_output.doctest_pytest_expected_output, + ), + ( + "pytest_describe_plugin" + os.path.sep + "describe_only.py", + expected_discovery_test_output.expected_describe_only_output, + ), + ( + "pytest_describe_plugin" + os.path.sep + "nested_describe.py", + expected_discovery_test_output.expected_nested_describe_output, + ), + ], +) +def test_pytest_collect(file, expected_const): + """Test to test pytest discovery on a variety of test files/ folder structures. + + Uses variables from expected_discovery_test_output.py to store the expected + dictionary return. Only handles discovery and therefore already contains the arg + --collect-only. All test discovery will succeed, be in the correct cwd, and match + expected test output. + + Keyword arguments: + file -- a string with the file or folder to run pytest discovery on. + expected_const -- the expected output from running pytest discovery on the file. + """ + actual = helpers.runner( + [ + os.fspath(helpers.TEST_DATA_PATH / file), + "--collect-only", + ] + ) + + assert actual + actual_list: List[Dict[str, Any]] = actual + if actual_list is not None: + actual_item = actual_list.pop(0) + assert all(item in actual_item for item in ("status", "cwd", "error")) + assert actual_item.get("status") == "success", ( + f"Status is not 'success', error is: {actual_item.get('error')}" + ) + assert actual_item.get("cwd") == os.fspath(helpers.TEST_DATA_PATH) + assert is_same_tree( + actual_item.get("tests"), + expected_const, + ["id_", "lineno", "name", "runID"], + ), ( + f"Tests tree does not match expected value. \n Expected: {json.dumps(expected_const, indent=4)}. \n Actual: {json.dumps(actual_item.get('tests'), indent=4)}" + ) + + +@pytest.mark.skipif( + sys.platform == "win32", + reason="See https://stackoverflow.com/questions/32877260/privlege-error-trying-to-create-symlink-using-python-on-windows-10", +) +def test_symlink_root_dir(): + """Test to test pytest discovery with the command line arg --rootdir specified as a symlink path. + + Discovery should succeed and testids should be relative to the symlinked root directory. + """ + with helpers.create_symlink(helpers.TEST_DATA_PATH, "root", "symlink_folder") as ( + source, + destination, + ): + assert destination.is_symlink() + + # Run pytest with the cwd being the resolved symlink path (as it will be when we run the subprocess from node). + actual = helpers.runner_with_cwd( + ["--collect-only", f"--rootdir={os.fspath(destination)}"], source + ) + expected = expected_discovery_test_output.symlink_expected_discovery_output + assert actual + actual_list: List[Dict[str, Any]] = actual + if actual_list is not None: + actual_item = actual_list.pop(0) + try: + # Check if all requirements + assert all(item in actual_item for item in ("status", "cwd", "error")), ( + "Required keys are missing" + ) + assert actual_item.get("status") == "success", "Status is not 'success'" + assert actual_item.get("cwd") == os.fspath(destination), ( + f"CWD does not match: {os.fspath(destination)}" + ) + assert actual_item.get("tests") == expected, "Tests do not match expected value" + except AssertionError as e: + # Print the actual_item in JSON format if an assertion fails + print(json.dumps(actual_item, indent=4)) + pytest.fail(str(e)) + + +def test_pytest_root_dir(): + """Test to test pytest discovery with the command line arg --rootdir specified to be a subfolder of the workspace root. + + Discovery should succeed and testids should be relative to workspace root. + """ + rd = f"--rootdir={helpers.TEST_DATA_PATH / 'root' / 'tests'}" + actual = helpers.runner_with_cwd( + [ + "--collect-only", + rd, + ], + helpers.TEST_DATA_PATH / "root", + ) + assert actual + actual_list: List[Dict[str, Any]] = actual + if actual_list is not None: + actual_item = actual_list.pop(0) + + assert all(item in actual_item for item in ("status", "cwd", "error")) + assert actual_item.get("status") == "success" + assert actual_item.get("cwd") == os.fspath(helpers.TEST_DATA_PATH / "root") + assert is_same_tree( + actual_item.get("tests"), + expected_discovery_test_output.root_with_config_expected_output, + ["id_", "lineno", "name", "runID"], + ), ( + f"Tests tree does not match expected value. \n Expected: {json.dumps(expected_discovery_test_output.root_with_config_expected_output, indent=4)}. \n Actual: {json.dumps(actual_item.get('tests'), indent=4)}" + ) + + +def test_pytest_config_file(): + """Test to test pytest discovery with the command line arg -c with a specified config file which changes the workspace root. + + Discovery should succeed and testids should be relative to workspace root. + """ + actual = helpers.runner_with_cwd( + [ + "--collect-only", + "tests/", + ], + helpers.TEST_DATA_PATH / "root", + ) + assert actual + actual_list: List[Dict[str, Any]] = actual + if actual_list is not None: + actual_item = actual_list.pop(0) + + assert all(item in actual_item for item in ("status", "cwd", "error")) + assert actual_item.get("status") == "success" + assert actual_item.get("cwd") == os.fspath(helpers.TEST_DATA_PATH / "root") + assert is_same_tree( + actual_item.get("tests"), + expected_discovery_test_output.root_with_config_expected_output, + ["id_", "lineno", "name", "runID"], + ), ( + f"Tests tree does not match expected value. \n Expected: {json.dumps(expected_discovery_test_output.root_with_config_expected_output, indent=4)}. \n Actual: {json.dumps(actual_item.get('tests'), indent=4)}" + ) + + +def test_config_sub_folder(): + """Here the session node will be a subfolder of the workspace root and the test are in another subfolder. + + This tests checks to see if test node path are under the session node and if so the + session node is correctly updated to the common path. + """ + folder_path = helpers.TEST_DATA_PATH / "config_sub_folder" + actual = helpers.runner_with_cwd( + [ + "--collect-only", + "-c=config/pytest.ini", + "--rootdir=config/", + "-vv", + ], + folder_path, + ) + + assert actual + actual_list: List[Dict[str, Any]] = actual + if actual_list is not None: + actual_item = actual_list.pop(0) + assert all(item in actual_item for item in ("status", "cwd", "error")) + assert actual_item.get("status") == "success" + assert actual_item.get("cwd") == os.fspath(helpers.TEST_DATA_PATH / "config_sub_folder") + assert actual_item.get("tests") is not None + if actual_item.get("tests") is not None: + tests: Any = actual_item.get("tests") + assert tests.get("name") == "config_sub_folder" + + +@pytest.mark.parametrize( + ("file", "expected_const", "extra_arg"), + [ + ( + "folder_with_script", + expected_discovery_test_output.ruff_test_expected_output, + "--ruff", + ), + ( + "2496-black-formatter", + expected_discovery_test_output.black_formatter_expected_output, + "--black", + ), + ], +) +def test_plugin_collect(file, expected_const, extra_arg): + """Test pytest discovery on a folder with a plugin argument (e.g., --ruff, --black). + + Uses variables from expected_discovery_test_output.py to store the expected + dictionary return. Only handles discovery and therefore already contains the arg + --collect-only. All test discovery will succeed, be in the correct cwd, and match + expected test output. + + Keyword arguments: + file -- a string with the file or folder to run pytest discovery on. + expected_const -- the expected output from running pytest discovery on the file. + extra_arg -- the extra plugin argument to pass (e.g., --ruff, --black) + """ + file_path = helpers.TEST_DATA_PATH / file + actual = helpers.runner( + [os.fspath(file_path), "--collect-only", extra_arg], + ) + + assert actual + actual_list: List[Dict[str, Any]] = actual + if actual_list is not None: + actual_item = actual_list.pop(0) + assert all(item in actual_item for item in ("status", "cwd", "error")) + assert actual_item.get("status") == "success", ( + f"Status is not 'success', error is: {actual_item.get('error')}" + ) + assert actual_item.get("cwd") == os.fspath(helpers.TEST_DATA_PATH) + assert is_same_tree( + actual_item.get("tests"), + expected_const, + ["id_", "lineno", "name", "runID"], + ), ( + f"Tests tree does not match expected value. \n Expected: {json.dumps(expected_const, indent=4)}. \n Actual: {json.dumps(actual_item.get('tests'), indent=4)}" + ) + + +def test_project_root_path_env_var(): + """Test pytest discovery with PROJECT_ROOT_PATH environment variable set. + + This simulates project-based testing where the test tree root should be + the project root (PROJECT_ROOT_PATH) rather than the workspace cwd. + + When PROJECT_ROOT_PATH is set: + - The test tree root (name, path, id_) should match PROJECT_ROOT_PATH + - The cwd in the response should match PROJECT_ROOT_PATH + - Test files should be direct children of the root (not nested under a subfolder) + """ + # Use unittest_folder as our "project" subdirectory + project_path = helpers.TEST_DATA_PATH / "unittest_folder" + + actual = helpers.runner_with_cwd_env( + [os.fspath(project_path), "--collect-only"], + helpers.TEST_DATA_PATH, # cwd is parent of project + {"PROJECT_ROOT_PATH": os.fspath(project_path)}, # Set project root + ) + + assert actual + actual_list: List[Dict[str, Any]] = actual + if actual_list is not None: + actual_item = actual_list.pop(0) + + assert all(item in actual_item for item in ("status", "cwd", "error")) + assert actual_item.get("status") == "success", ( + f"Status is not 'success', error is: {actual_item.get('error')}" + ) + # cwd in response should be PROJECT_ROOT_PATH + assert actual_item.get("cwd") == os.fspath(project_path), ( + f"Expected cwd '{os.fspath(project_path)}', got '{actual_item.get('cwd')}'" + ) + assert is_same_tree( + actual_item.get("tests"), + expected_discovery_test_output.project_root_unittest_folder_expected_output, + ["id_", "lineno", "name", "runID"], + ), ( + f"Tests tree does not match expected value. \n Expected: {json.dumps(expected_discovery_test_output.project_root_unittest_folder_expected_output, indent=4)}. \n Actual: {json.dumps(actual_item.get('tests'), indent=4)}" + ) + + +@pytest.mark.skipif( + sys.platform == "win32", + reason="Symlinks require elevated privileges on Windows", +) +def test_symlink_with_project_root_path(): + """Test pytest discovery with both symlink and PROJECT_ROOT_PATH set. + + This tests the combination of: + 1. A symlinked test directory (--rootdir points to symlink) + 2. PROJECT_ROOT_PATH set to the symlink path + + This simulates project-based testing where the project root is a symlink, + ensuring test IDs and paths are correctly resolved through the symlink. + """ + with helpers.create_symlink(helpers.TEST_DATA_PATH, "root", "symlink_folder") as ( + source, + destination, + ): + assert destination.is_symlink() + + # Run pytest with: + # - cwd being the resolved symlink path (simulating subprocess from node) + # - PROJECT_ROOT_PATH set to the symlink destination + actual = helpers.runner_with_cwd_env( + ["--collect-only", f"--rootdir={os.fspath(destination)}"], + source, # cwd is the resolved (non-symlink) path + {"PROJECT_ROOT_PATH": os.fspath(destination)}, # Project root is the symlink + ) + + expected = expected_discovery_test_output.symlink_expected_discovery_output + assert actual + actual_list: List[Dict[str, Any]] = actual + if actual_list is not None: + actual_item = actual_list.pop(0) + try: + assert all(item in actual_item for item in ("status", "cwd", "error")), ( + "Required keys are missing" + ) + assert actual_item.get("status") == "success", ( + f"Status is not 'success', error is: {actual_item.get('error')}" + ) + # cwd should be the PROJECT_ROOT_PATH (the symlink destination) + assert actual_item.get("cwd") == os.fspath(destination), ( + f"CWD does not match symlink path: expected {os.fspath(destination)}, got {actual_item.get('cwd')}" + ) + assert actual_item.get("tests") == expected, "Tests do not match expected value" + except AssertionError as e: + # Print the actual_item in JSON format if an assertion fails + print(json.dumps(actual_item, indent=4)) + pytest.fail(str(e)) diff --git a/python_files/tests/pytestadapter/test_execution.py b/python_files/tests/pytestadapter/test_execution.py new file mode 100644 index 000000000000..95a66e0e7b87 --- /dev/null +++ b/python_files/tests/pytestadapter/test_execution.py @@ -0,0 +1,274 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. +import json +import os +import pathlib +import sys +from typing import Any, Dict, List + +import pytest + +script_dir = pathlib.Path(__file__).parent.parent +sys.path.append(os.fspath(script_dir)) + +from tests.pytestadapter import expected_execution_test_output # noqa: E402 + +from .helpers import ( # noqa: E402 + TEST_DATA_PATH, + create_symlink, + get_absolute_test_id, + runner, + runner_with_cwd, +) + + +def test_config_file(): + """Test pytest execution when a config file is specified.""" + args = [ + "-c", + "tests/pytest.ini", + str(TEST_DATA_PATH / "root" / "tests" / "test_a.py::test_a_function"), + ] + new_cwd = TEST_DATA_PATH / "root" + actual = runner_with_cwd(args, new_cwd) + expected_const = expected_execution_test_output.config_file_pytest_expected_execution_output + assert actual + actual_list: List[Dict[str, Any]] = actual + assert len(actual_list) == len(expected_const) + actual_result_dict = {} + if actual_list is not None: + for actual_item in actual_list: + assert all(item in actual_item for item in ("status", "cwd", "result")) + assert actual_item.get("status") == "success" + assert actual_item.get("cwd") == os.fspath(new_cwd) + actual_result_dict.update(actual_item["result"]) + assert actual_result_dict == expected_const + + +def test_rootdir_specified(): + """Test pytest execution when a --rootdir is specified.""" + rd = f"--rootdir={TEST_DATA_PATH / 'root' / 'tests'}" + args = [rd, "tests/test_a.py::test_a_function"] + new_cwd = TEST_DATA_PATH / "root" + actual = runner_with_cwd(args, new_cwd) + expected_const = expected_execution_test_output.config_file_pytest_expected_execution_output + assert actual + actual_list: List[Dict[str, Dict[str, Any]]] = actual + assert len(actual_list) == len(expected_const) + actual_result_dict = {} + if actual_list is not None: + for actual_item in actual_list: + assert all(item in actual_item for item in ("status", "cwd", "result")) + assert actual_item.get("status") == "success" + assert actual_item.get("cwd") == os.fspath(new_cwd) + actual_result_dict.update(actual_item["result"]) + assert actual_result_dict == expected_const + + +@pytest.mark.parametrize( + ("test_ids", "expected_const"), + [ + pytest.param( + [ + "test_env_vars.py::test_clear_env", + "test_env_vars.py::test_check_env", + ], + expected_execution_test_output.safe_clear_env_vars_expected_execution_output, + id="safe_clear_env_vars", + ), + pytest.param( + [ + "skip_tests.py::test_something", + "skip_tests.py::test_another_thing", + "skip_tests.py::test_decorator_thing", + "skip_tests.py::test_decorator_thing_2", + "skip_tests.py::TestClass::test_class_function_a", + "skip_tests.py::TestClass::test_class_function_b", + ], + expected_execution_test_output.skip_tests_execution_expected_output, + id="skip_tests_execution", + ), + pytest.param( + ["error_raise_exception.py::TestSomething::test_a"], + expected_execution_test_output.error_raised_exception_execution_expected_output, + id="error_raised_exception", + ), + pytest.param( + [ + "unittest_folder/test_add.py::TestAddFunction::test_add_positive_numbers", + "unittest_folder/test_add.py::TestAddFunction::test_add_negative_numbers", + "unittest_folder/test_subtract.py::TestSubtractFunction::test_subtract_positive_numbers", + "unittest_folder/test_subtract.py::TestSubtractFunction::test_subtract_negative_numbers", + ], + expected_execution_test_output.uf_execution_expected_output, + id="unittest_multiple_files", + ), + pytest.param( + [ + "unittest_folder/test_add.py::TestAddFunction::test_add_positive_numbers", + "unittest_folder/test_add.py::TestAddFunction::test_add_negative_numbers", + ], + expected_execution_test_output.uf_single_file_expected_output, + id="unittest_single_file", + ), + pytest.param( + [ + "unittest_folder/test_add.py::TestAddFunction::test_add_positive_numbers", + ], + expected_execution_test_output.uf_single_method_execution_expected_output, + id="unittest_single_method", + ), + pytest.param( + [ + "unittest_folder/test_add.py::TestAddFunction::test_add_positive_numbers", + "unittest_folder/test_subtract.py::TestSubtractFunction::test_subtract_positive_numbers", + ], + expected_execution_test_output.uf_non_adjacent_tests_execution_expected_output, + id="unittest_non_adjacent_tests", + ), + pytest.param( + [ + "unittest_pytest_same_file.py::TestExample::test_true_unittest", + "unittest_pytest_same_file.py::test_true_pytest", + ], + expected_execution_test_output.unit_pytest_same_file_execution_expected_output, + id="unittest_pytest_same_file", + ), + pytest.param( + [ + "dual_level_nested_folder/test_top_folder.py::test_top_function_t", + "dual_level_nested_folder/test_top_folder.py::test_top_function_f", + "dual_level_nested_folder/nested_folder_one/test_bottom_folder.py::test_bottom_function_t", + "dual_level_nested_folder/nested_folder_one/test_bottom_folder.py::test_bottom_function_f", + ], + expected_execution_test_output.dual_level_nested_folder_execution_expected_output, + id="dual_level_nested_folder", + ), + pytest.param( + ["folder_a/folder_b/folder_a/test_nest.py::test_function"], + expected_execution_test_output.double_nested_folder_expected_execution_output, + id="double_nested_folder", + ), + pytest.param( + [ + "parametrize_tests.py::TestClass::test_adding[3+5-8]", + "parametrize_tests.py::TestClass::test_adding[2+4-6]", + "parametrize_tests.py::TestClass::test_adding[6+9-16]", + ], + expected_execution_test_output.parametrize_tests_expected_execution_output, + id="parametrize_tests", + ), + pytest.param( + [ + "parametrize_tests.py::TestClass::test_adding[3+5-8]", + ], + expected_execution_test_output.single_parametrize_tests_expected_execution_output, + id="single_parametrize_test", + ), + pytest.param( + [ + "text_docstring.txt::text_docstring.txt", + ], + expected_execution_test_output.doctest_pytest_expected_execution_output, + id="doctest_pytest", + ), + pytest.param( + ["test_logging.py::test_logging2", "test_logging.py::test_logging"], + expected_execution_test_output.logging_test_expected_execution_output, + id="logging_tests", + ), + pytest.param( + [ + "pytest_describe_plugin/describe_only.py::describe_A::test_1", + "pytest_describe_plugin/describe_only.py::describe_A::test_2", + ], + expected_execution_test_output.describe_only_expected_execution_output, + id="describe_only", + ), + pytest.param( + [ + "pytest_describe_plugin/nested_describe.py::describe_list::describe_append::add_empty", + "pytest_describe_plugin/nested_describe.py::describe_list::describe_append::remove_empty", + "pytest_describe_plugin/nested_describe.py::describe_list::describe_remove::removes", + ], + expected_execution_test_output.nested_describe_expected_execution_output, + id="nested_describe_plugin", + ), + pytest.param( + ["skip_test_fixture.py::test_docker_client"], + expected_execution_test_output.skip_test_fixture_execution_expected_output, + id="skip_test_fixture", + ), + ], +) +def test_pytest_execution(test_ids, expected_const): + """ + Test that pytest discovery works as expected where run pytest is always successful, but the actual test results are both successes and failures. + + Keyword arguments: + test_ids -- an array of test_ids to run. + expected_const -- a dictionary of the expected output from running pytest discovery on the files. + """ + args = test_ids + actual = runner(args) + assert actual + actual_list: List[Dict[str, Dict[str, Any]]] = actual + assert len(actual_list) == len(expected_const) + actual_result_dict = {} + if actual_list is not None: + for actual_item in actual_list: + assert all(item in actual_item for item in ("status", "cwd", "result")) + assert actual_item.get("status") == "success" + assert actual_item.get("cwd") == os.fspath(TEST_DATA_PATH) + actual_result_dict.update(actual_item["result"]) + for key in actual_result_dict: + if ( + actual_result_dict[key]["outcome"] == "failure" + or actual_result_dict[key]["outcome"] == "error" + ): + actual_result_dict[key]["message"] = "ERROR MESSAGE" + if actual_result_dict[key]["traceback"] is not None: + actual_result_dict[key]["traceback"] = "TRACEBACK" + assert actual_result_dict == expected_const + + +def test_symlink_run(): + """Test to test pytest discovery with the command line arg --rootdir specified as a symlink path. + + Discovery should succeed and testids should be relative to the symlinked root directory. + """ + with create_symlink(TEST_DATA_PATH, "root", "symlink_folder") as ( + source, + destination, + ): + assert destination.is_symlink() + test_a_path = TEST_DATA_PATH / "symlink_folder" / "tests" / "test_a.py" + test_a_id = get_absolute_test_id( + "tests/test_a.py::test_a_function", + test_a_path, + ) + + # Run pytest with the cwd being the resolved symlink path (as it will be when we run the subprocess from node). + actual = runner_with_cwd([f"--rootdir={os.fspath(destination)}", test_a_id], source) + + expected_const = expected_execution_test_output.symlink_run_expected_execution_output + assert actual + actual_list: List[Dict[str, Any]] = actual + if actual_list is not None: + actual_item = actual_list.pop(0) + try: + # Check if all requirements + assert all(item in actual_item for item in ("status", "cwd", "result")), ( + "Required keys are missing" + ) + assert actual_item.get("status") == "success", "Status is not 'success'" + assert actual_item.get("cwd") == os.fspath(destination), ( + f"CWD does not match: {os.fspath(destination)}" + ) + actual_result_dict = {} + actual_result_dict.update(actual_item["result"]) + assert actual_result_dict == expected_const + except AssertionError as e: + # Print the actual_item in JSON format if an assertion fails + print(json.dumps(actual_item, indent=4)) + pytest.fail(str(e)) diff --git a/python_files/tests/pytestadapter/test_utils.py b/python_files/tests/pytestadapter/test_utils.py new file mode 100644 index 000000000000..70201db7d097 --- /dev/null +++ b/python_files/tests/pytestadapter/test_utils.py @@ -0,0 +1,57 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +import os +import pathlib +import sys +import tempfile + +from .helpers import ( + TEST_DATA_PATH, +) + +script_dir = pathlib.Path(__file__).parent.parent.parent +sys.path.append(os.fspath(script_dir)) +from vscode_pytest import cached_fsdecode, has_symlink_parent # noqa: E402 + + +def test_has_symlink_parent_with_symlink(): + # Create a temporary directory and a file in it + with tempfile.TemporaryDirectory() as temp_dir: + file_path = pathlib.Path(temp_dir) / "file" + file_path.touch() + + # Create a symbolic link to the temporary directory + symlink_path = pathlib.Path(temp_dir) / "symlink" + symlink_path.symlink_to(temp_dir) + + # Check that has_symlink_parent correctly identifies the symbolic link + assert has_symlink_parent(symlink_path / "file") + + +def test_has_symlink_parent_without_symlink(): + folder_path = TEST_DATA_PATH / "unittest_folder" / "test_add.py" + # Check that has_symlink_parent correctly identifies that there are no symbolic links + assert not has_symlink_parent(folder_path) + + +def test_cached_fsdecode(): + """Test that cached_fsdecode correctly caches path-to-string conversions.""" + # Create a test path + test_path = TEST_DATA_PATH / "simple_pytest.py" + + # First call should compute and cache + result1 = cached_fsdecode(test_path) + assert result1 == os.fspath(test_path) + assert isinstance(result1, str) + + # Second call should return cached value (same object) + result2 = cached_fsdecode(test_path) + assert result2 == result1 + assert result2 is result1 # Should be the same object from cache + + # Different path should be cached independently + test_path2 = TEST_DATA_PATH / "parametrize_tests.py" + result3 = cached_fsdecode(test_path2) + assert result3 == os.fspath(test_path2) + assert result3 != result1 diff --git a/pythonFiles/tests/run_all.py b/python_files/tests/run_all.py similarity index 63% rename from pythonFiles/tests/run_all.py rename to python_files/tests/run_all.py index ce5a62649962..3edb3cd3440c 100644 --- a/pythonFiles/tests/run_all.py +++ b/python_files/tests/run_all.py @@ -2,13 +2,13 @@ # Licensed under the MIT License. # Replace the "." entry. -import os.path +import os +import pathlib import sys -sys.path[0] = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) - -from tests.__main__ import main, parse_args +sys.path[0] = os.fsdecode(pathlib.Path(__file__).parent.parent) +from tests.__main__ import main, parse_args # noqa: E402 if __name__ == "__main__": mainkwargs, pytestargs = parse_args() diff --git a/pythonFiles/tests/test_create_conda.py b/python_files/tests/test_create_conda.py similarity index 94% rename from pythonFiles/tests/test_create_conda.py rename to python_files/tests/test_create_conda.py index 29dc323402eb..82daafbea9dc 100644 --- a/pythonFiles/tests/test_create_conda.py +++ b/python_files/tests/test_create_conda.py @@ -4,9 +4,10 @@ import importlib import sys -import create_conda import pytest +import create_conda + @pytest.mark.parametrize("env_exists", [True, False]) @pytest.mark.parametrize("git_ignore", [True, False]) @@ -29,9 +30,7 @@ def install_packages(_name): def run_process(args, error_message): nonlocal run_process_called run_process_called = True - version = ( - "12345" if python else f"{sys.version_info.major}.{sys.version_info.minor}" - ) + version = "12345" if python else f"{sys.version_info.major}.{sys.version_info.minor}" if not env_exists: assert args == [ sys.executable, diff --git a/pythonFiles/tests/test_create_microvenv.py b/python_files/tests/test_create_microvenv.py similarity index 93% rename from pythonFiles/tests/test_create_microvenv.py rename to python_files/tests/test_create_microvenv.py index f123052c491c..e5d4e68802e9 100644 --- a/pythonFiles/tests/test_create_microvenv.py +++ b/python_files/tests/test_create_microvenv.py @@ -6,7 +6,6 @@ import sys import create_microvenv -import pytest def test_create_microvenv(): @@ -26,4 +25,4 @@ def run_process(args, error_message): create_microvenv.run_process = run_process create_microvenv.main() - assert run_process_called == True + assert run_process_called is True diff --git a/pythonFiles/tests/test_create_venv.py b/python_files/tests/test_create_venv.py similarity index 66% rename from pythonFiles/tests/test_create_venv.py rename to python_files/tests/test_create_venv.py index bebe304c13c3..6308934d71a0 100644 --- a/pythonFiles/tests/test_create_venv.py +++ b/python_files/tests/test_create_venv.py @@ -1,17 +1,20 @@ # Copyright (c) Microsoft Corporation. All rights reserved. # Licensed under the MIT License. +import argparse +import contextlib import importlib +import io +import json import os import sys -import create_venv import pytest +import create_venv + -@pytest.mark.skipif( - sys.platform == "win32", reason="Windows does not have micro venv fallback." -) +@pytest.mark.skipif(sys.platform == "win32", reason="Windows does not have micro venv fallback.") def test_venv_not_installed_unix(): importlib.reload(create_venv) create_venv.is_installed = lambda module: module != "venv" @@ -35,12 +38,10 @@ def run_process(args, error_message): create_venv.main(["--name", ".test_venv"]) # run_process is called when the venv does not exist - assert run_process_called == True + assert run_process_called is True -@pytest.mark.skipif( - sys.platform != "win32", reason="Windows does not have microvenv fallback." -) +@pytest.mark.skipif(sys.platform != "win32", reason="Windows does not have microvenv fallback.") def test_venv_not_installed_windows(): importlib.reload(create_venv) create_venv.is_installed = lambda module: module != "venv" @@ -50,13 +51,14 @@ def test_venv_not_installed_windows(): @pytest.mark.parametrize("env_exists", ["hasEnv", "noEnv"]) -@pytest.mark.parametrize("git_ignore", ["useGitIgnore", "skipGitIgnore"]) +@pytest.mark.parametrize("git_ignore", ["useGitIgnore", "skipGitIgnore", "gitIgnoreExists"]) @pytest.mark.parametrize("install", ["requirements", "toml", "skipInstall"]) def test_create_env(env_exists, git_ignore, install): importlib.reload(create_venv) create_venv.is_installed = lambda _x: True create_venv.venv_exists = lambda _n: env_exists == "hasEnv" create_venv.upgrade_pip = lambda _x: None + create_venv.is_file = lambda _x: git_ignore == "gitIgnoreExists" install_packages_called = False @@ -83,9 +85,19 @@ def run_process(args, error_message): def add_gitignore(_name): nonlocal add_gitignore_called add_gitignore_called = True + if not create_venv.is_file(_name): + create_venv.create_gitignore(_name) create_venv.add_gitignore = add_gitignore + create_gitignore_called = False + + def create_gitignore(_p): + nonlocal create_gitignore_called + create_gitignore_called = True + + create_venv.create_gitignore = create_gitignore + args = [] if git_ignore == "useGitIgnore": args += ["--git-ignore"] @@ -101,30 +113,34 @@ def add_gitignore(_name): assert run_process_called == (env_exists == "noEnv") # add_gitignore is called when new venv is created and git_ignore is True - assert add_gitignore_called == ( - (env_exists == "noEnv") and (git_ignore == "useGitIgnore") - ) + assert add_gitignore_called == ((env_exists == "noEnv") and (git_ignore == "useGitIgnore")) + + assert create_gitignore_called == (add_gitignore_called and (git_ignore != "gitIgnoreExists")) -@pytest.mark.parametrize("install_type", ["requirements", "pyproject"]) +@pytest.mark.parametrize("install_type", ["requirements", "pyproject", "both"]) def test_install_packages(install_type): importlib.reload(create_venv) create_venv.is_installed = lambda _x: True - create_venv.file_exists = lambda x: install_type in x + create_venv.file_exists = lambda x: install_type in str(x) pip_upgraded = False installing = None + order = [] + def run_process(args, error_message): - nonlocal pip_upgraded, installing + nonlocal pip_upgraded, installing, order if args[1:] == ["-m", "pip", "install", "--upgrade", "pip"]: pip_upgraded = True assert error_message == "CREATE_VENV.UPGRADE_PIP_FAILED" elif args[1:-1] == ["-m", "pip", "install", "-r"]: installing = "requirements" + order += ["requirements"] assert error_message == "CREATE_VENV.PIP_FAILED_INSTALL_REQUIREMENTS" elif args[1:] == ["-m", "pip", "install", "-e", ".[test]"]: installing = "pyproject" + order += ["pyproject"] assert error_message == "CREATE_VENV.PIP_FAILED_INSTALL_PYPROJECT" create_venv.run_process = run_process @@ -133,9 +149,23 @@ def run_process(args, error_message): create_venv.main(["--requirements", "requirements-for-test.txt"]) elif install_type == "pyproject": create_venv.main(["--toml", "pyproject.toml", "--extras", "test"]) + elif install_type == "both": + create_venv.main( + [ + "--requirements", + "requirements-for-test.txt", + "--toml", + "pyproject.toml", + "--extras", + "test", + ] + ) assert pip_upgraded - assert installing == install_type + if install_type == "both": + assert order == ["requirements", "pyproject"] + else: + assert installing == install_type @pytest.mark.parametrize( @@ -151,7 +181,7 @@ def test_toml_args(extras, expected): actual = [] - def run_process(args, error_message): + def run_process(args, error_message): # noqa: ARG001 nonlocal actual actual = args[1:] @@ -184,7 +214,7 @@ def test_requirements_args(extras, expected): actual = [] - def run_process(args, error_message): + def run_process(args, error_message): # noqa: ARG001 nonlocal actual actual.append(args) @@ -215,11 +245,56 @@ def run_process(args, error_message): if "install" in args and "pip" in args: nonlocal run_process_called run_process_called = True - pip_pyz_path = os.fspath( - create_venv.CWD / create_venv.VENV_NAME / "pip.pyz" - ) + pip_pyz_path = os.fspath(create_venv.CWD / create_venv.VENV_NAME / "pip.pyz") assert args[1:] == [pip_pyz_path, "install", "pip"] assert error_message == "CREATE_VENV.INSTALL_PIP_FAILED" create_venv.run_process = run_process create_venv.main([]) + + +@contextlib.contextmanager +def redirect_io(stream: str, new_stream): + """Redirect stdio streams to a custom stream.""" + old_stream = getattr(sys, stream) + setattr(sys, stream, new_stream) + yield + setattr(sys, stream, old_stream) + + +class CustomIO(io.TextIOWrapper): + """Custom stream object to replace stdio.""" + + name: str = "customio" + + def __init__(self, name: str, encoding="utf-8", newline=None): + self._buffer = io.BytesIO() + self._buffer.name = name + super().__init__(self._buffer, encoding=encoding, newline=newline) + + def close(self): + """Provide this close method which is used by some tools.""" + # This is intentionally empty. + + def get_value(self) -> str: + """Returns value from the buffer as string.""" + self.seek(0) + return self.read() + + +def test_requirements_from_stdin(): + importlib.reload(create_venv) + + cli_requirements = [f"cli-requirement{i}.txt" for i in range(3)] + args = argparse.Namespace() + args.__dict__.update({"stdin": True, "requirements": cli_requirements}) + + stdin_requirements = [f"stdin-requirement{i}.txt" for i in range(20)] + text = json.dumps({"requirements": stdin_requirements}) + str_input = CustomIO("", encoding="utf-8", newline="\n") + with redirect_io("stdin", str_input): + str_input.write(text) + str_input.seek(0) + actual = create_venv.get_requirements_from_args(args) + + assert actual == stdin_requirements + cli_requirements diff --git a/python_files/tests/test_data/missing-deps.data b/python_files/tests/test_data/missing-deps.data new file mode 100644 index 000000000000..c8c911f218a8 --- /dev/null +++ b/python_files/tests/test_data/missing-deps.data @@ -0,0 +1,121 @@ +# +# This file is autogenerated by pip-compile with Python 3.8 +# by the following command: +# +# pip-compile --generate-hashes --resolver=backtracking requirements-test.in +# +flake8-csv==0.2.0 \ + --hash=sha256:246e07207fefbf8f80a59ff7e878f153635f562ebaf20cf796a2b00b1528ea9a \ + --hash=sha256:bf3ac6aecbaebe36a2c7d5d275f310996fcc33b7370cdd81feec04b79af2e07c + # via -r requirements-test.in +levenshtein==0.21.0 \ + --hash=sha256:01dd427cf72b4978b09558e3d36e3f92c8eef467e3eb4653c3fdccd8d70aaa08 \ + --hash=sha256:0236c8ff4648c50ebd81ac3692430d2241b134936ac9d86d7ca32ba6ab4a4e63 \ + --hash=sha256:023ca95c833ca548280e444e9a4c34fdecb3be3851e96af95bad290ae0c708b9 \ + --hash=sha256:024302c82d49fc1f1d044794997ef7aa9d01b509a9040e222480b64a01cd4b80 \ + --hash=sha256:04046878a57129da4e2352c032df7c1fceaa54870916d12772cad505ef998290 \ + --hash=sha256:04850a0719e503014acb3fee6d4ec7d7f170a2c7375ffbc5833c7256b7cd10ee \ + --hash=sha256:0cc3679978cd0250bf002963cf2e08855b93f70fa0fc9f74956115c343983fbb \ + --hash=sha256:0f42b8dba2cce257cd34efd1ce9678d06f3248cb0bb2a92a5db8402e1e4a6f30 \ + --hash=sha256:13e8a5b1b58de49befea555bb913dc394614f2d3553bc5b86bc672c69ef1a85a \ + --hash=sha256:1f19fe25ea0dd845d0f48505e8947f6080728e10b7642ba0dad34e9b48c81130 \ + --hash=sha256:1fde464f937878e6f5c30c234b95ce2cb969331a175b3089367e077113428062 \ + --hash=sha256:2290732763e3b75979888364b26acce79d72b8677441b5762a4e97b3630cc3d9 \ + --hash=sha256:24843f28cbbdcbcfc18b08e7d3409dbaad7896fb7113442592fa978590a7bbf0 \ + --hash=sha256:25576ad9c337ecb342306fe87166b54b2f49e713d4ff592c752cc98e0046296e \ + --hash=sha256:26c6fb012538a245d78adea786d2cfe3c1506b835762c1c523a4ed6b9e08dc0b \ + --hash=sha256:31cb59d86a5f99147cd4a67ebced8d6df574b5d763dcb63c033a642e29568746 \ + --hash=sha256:32dfda2e64d0c50553e47d0ab2956413970f940253351c196827ad46f17916d5 \ + --hash=sha256:3305262cb85ff78ace9e2d8d2dfc029b34dc5f93aa2d24fd20b6ed723e2ad501 \ + --hash=sha256:37a99d858fa1d88b1a917b4059a186becd728534e5e889d583086482356b7ca1 \ + --hash=sha256:3c6858cfd84568bc1df3ad545553b5c27af6ed3346973e8f4b57d23c318cf8f4 \ + --hash=sha256:3e1723d515ab287b9b2c2e4a111894dc6b474f5d28826fff379647486cae98d2 \ + --hash=sha256:3e22d31375d5fea5797c9b7aa0f8cc36579c31dcf5754e9931ca86c27d9011f8 \ + --hash=sha256:426883be613d912495cf6ee2a776d2ab84aa6b3de5a8d82c43a994267ea6e0e3 \ + --hash=sha256:4357bf8146cbadb10016ad3a950bba16e042f79015362a575f966181d95b4bc7 \ + --hash=sha256:4515f9511cb91c66d254ee30154206aad76b57d8b25f64ba1402aad43efdb251 \ + --hash=sha256:457442911df185e28a32fd8b788b14ca22ab3a552256b556e7687173d5f18bc4 \ + --hash=sha256:46dab8c6e8fae563ca77acfaeb3824c4dd4b599996328b8a081b06f16befa6a0 \ + --hash=sha256:4b2156f32e46d16b74a055ccb4f64ee3c64399372a6aaf1ee98f6dccfadecee1 \ + --hash=sha256:4bbceef2caba4b2ae613b0e853a7aaab990c1a13bddb9054ba1328a84bccdbf7 \ + --hash=sha256:4c8eaaa6f0df2838437d1d8739629486b145f7a3405d3ef0874301a9f5bc7dcd \ + --hash=sha256:4dc79033140f82acaca40712a6d26ed190cc2dd403e104020a87c24f2771aa72 \ + --hash=sha256:4ec2ef9836a34a3bb009a81e5efe4d9d43515455fb5f182c5d2cf8ae61c79496 \ + --hash=sha256:5369827ace536c6df04e0e670d782999bc17bf9eb111e77435fdcdaecb10c2a3 \ + --hash=sha256:5378a8139ba61d7271c0f9350201259c11eb90bfed0ac45539c4aeaed3907230 \ + --hash=sha256:545635d9e857711d049dcdb0b8609fb707b34b032517376c531ca159fcd46265 \ + --hash=sha256:587ad51770de41eb491bea1bfb676abc7ff9a94dbec0e2bc51fc6a25abef99c4 \ + --hash=sha256:5cfbc4ed7ee2965e305bf81388fea377b795dabc82ee07f04f31d1fb8677a885 \ + --hash=sha256:5e748c2349719cb1bc90f802d9d7f07310633dcf166d468a5bd821f78ed17698 \ + --hash=sha256:608beb1683508c3cdbfff669c1c872ea02b47965e1bbb8a630de548e2490f96a \ + --hash=sha256:6338a47b6f8c7f1ee8b5636cc8b245ad2d1d0ee47f7bb6f33f38a522ef0219cc \ + --hash=sha256:668ea30b311944c643f866ce5e45edf346f05e920075c0056f2ba7f74dde6071 \ + --hash=sha256:66d303cd485710fe6d62108209219b7a695bdd10a722f4e86abdaf26f4bf2202 \ + --hash=sha256:6ebabcf982ae161534f8729d13fe05eebc977b497ac34936551f97cf8b07dd9e \ + --hash=sha256:6ede583155f24c8b2456a7720fbbfa5d9c1154ae04b4da3cf63368e2406ea099 \ + --hash=sha256:709a727f58d31a5ee1e5e83b247972fe55ef0014f6222256c9692c5efa471785 \ + --hash=sha256:742b785c93d16c63289902607219c200bd2b6077dafc788073c74337cae382fb \ + --hash=sha256:76d5d34a8e21de8073c66ae801f053520f946d499fa533fbba654712775f8132 \ + --hash=sha256:7bc550d0986ace95bde003b8a60e622449baf2bdf24d8412f7a50f401a289ec3 \ + --hash=sha256:7c2d67220867d640e36931b3d63b8349369b485d52cf6f4a2635bec8da92d678 \ + --hash=sha256:7ce3f14a8e006fb7e3fc7bab965ab7da5817f48fc48d25cf735fcec8f1d2e39a \ + --hash=sha256:7e40a4bac848c9a8883225f926cfa7b2bc9f651e989a8b7006cdb596edc7ac9b \ + --hash=sha256:80e67bd73a05592ecd52aede4afa8ea49575de70f9d5bfbe2c52ebd3541b20be \ + --hash=sha256:8446f8da38857482ec0cfd616fe5e7dcd3695fd323cc65f37366a9ff6a31c9cb \ + --hash=sha256:8476862a5c3150b8d63a7475563a4bff6dc50bbc0447894eb6b6a116ced0809d \ + --hash=sha256:84b55b732e311629a8308ad2778a0f9824e29e3c35987eb35610fc52eb6d4634 \ + --hash=sha256:88ccdc8dc20c16e8059ace00fb58d353346a04fd24c0733b009678b2554801d2 \ + --hash=sha256:8aa92b05156dfa2e248c3743670d5deb41a45b5789416d5fa31be009f4f043ab \ + --hash=sha256:8ac4ed77d3263eac7f9b6ed89d451644332aecd55cda921201e348803a1e5c57 \ + --hash=sha256:8bdbcd1570340b07549f71e8a5ba3f0a6d84408bf86c4051dc7b70a29ae342bb \ + --hash=sha256:8c031cbe3685b0343f5cc2dcf2172fd21b82f8ccc5c487179a895009bf0e4ea8 \ + --hash=sha256:8c27a5178ce322b56527a451185b4224217aa81955d9b0dad6f5a8de81ffe80f \ + --hash=sha256:8cf87a5e2962431d7260dd81dc1ca0697f61aad81036145d3666f4c0d514ce3a \ + --hash=sha256:8d4ba0df46bb41d660d77e7cc6b4d38c8d5b6f977d51c48ed1217db6a8474cde \ + --hash=sha256:8dd8ef4239b24fb1c9f0b536e48e55194d5966d351d349af23e67c9eb3875c68 \ + --hash=sha256:92bf2370b01d7a4862abf411f8f60f39f064cebebce176e3e9ee14e744db8288 \ + --hash=sha256:9485f2a5c88113410153256657072bc93b81bf5c8690d47e4cc3df58135dbadb \ + --hash=sha256:9ff1255c499fcb41ba37a578ad8c1b8dab5c44f78941b8e1c1d7fab5b5e831bc \ + --hash=sha256:a18c8e4d1aae3f9950797d049020c64a8a63cc8b4e43afcca91ec400bf6304c5 \ + --hash=sha256:a68b05614d25cc2a5fbcc4d2fd124be7668d075fd5ac3d82f292eec573157361 \ + --hash=sha256:a7adaabe07c5ceb6228332b9184f06eb9cda89c227d198a1b8a6f78c05b3c672 \ + --hash=sha256:aa39bb773915e4df330d311bb6c100a8613e265cc50d5b25b015c8db824e1c47 \ + --hash=sha256:ac8b6266799645827980ab1af4e0bfae209c1f747a10bdf6e5da96a6ebe511a2 \ + --hash=sha256:b0ba9723c7d67a61e160b3457259552f7d679d74aaa144b892eb68b7e2a5ebb6 \ + --hash=sha256:b167b32b3e336c5ec5e0212f025587f9248344ae6e73ed668270eba5c6a506e5 \ + --hash=sha256:b646ace5085a60d4f89b28c81301c9d9e8cd6a9bdda908181b2fa3dfac7fc10d \ + --hash=sha256:bd0bfa71b1441be359e99e77709885b79c22857bf9bb7f4e84c09e501f6c5fad \ + --hash=sha256:be038321695267a8faa5ae1b1a83deb3748827f0b6f72471e0beed36afcbd72a \ + --hash=sha256:be87998ffcbb5fb0c37a76d100f63b4811f48527192677da0ec3624b49ab8a64 \ + --hash=sha256:c270487d60b33102efea73be6dcd5835f3ddc3dc06e77499f0963df6cba2ec71 \ + --hash=sha256:c290a7211f1b4f87c300df4424cc46b7379cead3b6f37fa8d3e7e6c6212ccd39 \ + --hash=sha256:cc36ba40027b4f8821155c9e3e0afadffccdccbe955556039d1d1169dfc659c9 \ + --hash=sha256:ce7e76c6341abb498368d42b8081f2f45c245ac2a221af6a0394349d41302c08 \ + --hash=sha256:cefd5a668f6d7af1279aca10104b43882fdd83f9bdc68933ba5429257a628abe \ + --hash=sha256:cf2dee0f8c71598f8be51e3feceb9142ac01576277b9e691e25740987761c86e \ + --hash=sha256:d23c647b03acbb5783f9bdfd51cfa5365d51f7df9f4029717a35eff5cc32bbcc \ + --hash=sha256:d647f1e0c30c7a73f70f4de7376ed7dafc2b856b67fe480d32a81af133edbaeb \ + --hash=sha256:d932cb21e40beb93cfc8973de7f25fbf25ba4a07d1dccac3b9ba977164cf9887 \ + --hash=sha256:db7567997ffbc2feb999e30002a92461a76f17a596a142bdb463b5f7037f160c \ + --hash=sha256:de2dfd6498454c7d89036d56a53c0a01fd9bcf1c2970253e469b5e8bb938b69f \ + --hash=sha256:df9b0f8f511270ad259c7bfba22ab6d5a0c33d81cd594461668e67cd80dd9052 \ + --hash=sha256:e043b79e39f165026bc941c95582bfc4bfdd297a1de6f13ace0d0a7abf486288 \ + --hash=sha256:e2686c37d22faf27d02a19e83b55812d248b32b7ba3aa638e768d0ea032e1f3c \ + --hash=sha256:e9a6251818b9eb6d519bffd7a0b745f3a99b3e99563a4c9d3cad26e34f6ac880 \ + --hash=sha256:eab6c253983a6659e749f4c44fcc2215194c2e00bf7b1c5e90fe683ea3b7b00f \ + --hash=sha256:ec64b7b3fb95bc9c20c72548277794b81281a6ba9da85eda2c87324c218441ff \ + --hash=sha256:ee62ec5882a857b252faffeb7867679f7e418052ca6bf7d6b56099f6498a2b0e \ + --hash=sha256:ee757fd36bad66ad8b961958840894021ecaad22194f65219a666432739393ff \ + --hash=sha256:f55623094b665d79a3b82ba77386ac34fa85049163edfe65387063e5127d4184 \ + --hash=sha256:f622f542bd065ffec7d26b26d44d0c9a25c9c1295fd8ba6e4d77778e2293a12c \ + --hash=sha256:f873af54014cac12082c7f5ccec6bbbeb5b57f63466e7f9c61a34588621313fb \ + --hash=sha256:fae24c875c4ecc8c5f34a9715eb2a459743b4ca21d35c51819b640ee2f71cb51 \ + --hash=sha256:fb26e69fc6c12534fbaa1657efed3b6482f1a166ba8e31227fa6f6f062a59070 + # via -r requirements-test.in +pytest==7.3.1 \ + --hash=sha256:3799fa815351fea3a5e96ac7e503a96fa51cc9942c3753cda7651b93c1cfa362 \ + --hash=sha256:434afafd78b1d78ed0addf160ad2b77a30d35d4bdf8af234fe621919d9ed15e3 + +tomli==2.0.1 \ + --hash=sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc \ + --hash=sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f diff --git a/python_files/tests/test_data/no-missing-deps.data b/python_files/tests/test_data/no-missing-deps.data new file mode 100644 index 000000000000..d5d04476dec0 --- /dev/null +++ b/python_files/tests/test_data/no-missing-deps.data @@ -0,0 +1,13 @@ +# +# This file is autogenerated by pip-compile with Python 3.8 +# by the following command: +# +# pip-compile --generate-hashes --resolver=backtracking requirements-test.in +# +pytest==7.3.1 \ + --hash=sha256:3799fa815351fea3a5e96ac7e503a96fa51cc9942c3753cda7651b93c1cfa362 \ + --hash=sha256:434afafd78b1d78ed0addf160ad2b77a30d35d4bdf8af234fe621919d9ed15e3 + +tomli==2.0.1 \ + --hash=sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc \ + --hash=sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f diff --git a/python_files/tests/test_data/pyproject-missing-deps.data b/python_files/tests/test_data/pyproject-missing-deps.data new file mode 100644 index 000000000000..e4d6f9eb10d3 --- /dev/null +++ b/python_files/tests/test_data/pyproject-missing-deps.data @@ -0,0 +1,9 @@ +[build-system] +requires = ["flit_core >=3.2,<4"] +build-backend = "flit_core.buildapi" + +[project] +name = "something" +version = "2023.0.0" +requires-python = ">=3.8" +dependencies = ["pytest==7.3.1", "flake8-csv"] diff --git a/python_files/tests/test_data/pyproject-no-missing-deps.data b/python_files/tests/test_data/pyproject-no-missing-deps.data new file mode 100644 index 000000000000..64dadf6fdf2e --- /dev/null +++ b/python_files/tests/test_data/pyproject-no-missing-deps.data @@ -0,0 +1,9 @@ +[build-system] +requires = ["flit_core >=3.2,<4"] +build-backend = "flit_core.buildapi" + +[project] +name = "something" +version = "2023.0.0" +requires-python = ">=3.8" +dependencies = [jedi-language-server"] diff --git a/python_files/tests/test_dynamic_cursor.py b/python_files/tests/test_dynamic_cursor.py new file mode 100644 index 000000000000..d30887c24d5b --- /dev/null +++ b/python_files/tests/test_dynamic_cursor.py @@ -0,0 +1,192 @@ +import importlib +import textwrap + +import normalizeSelection + + +def test_dictionary_mouse_mover(): + """Having the mouse cursor on second line, 'my_dict = {' and pressing shift+enter should bring the mouse cursor to line 6, on and to be able to run 'print('only send the dictionary')'.""" + importlib.reload(normalizeSelection) + src = textwrap.dedent( + """\ + not_dictionary = 'hi' + my_dict = { + "key1": "value1", + "key2": "value2" + } + print('only send the dictionary') + """ + ) + + result = normalizeSelection.traverse_file(src, 2, 2, was_highlighted=False) + + assert result["which_line_next"] == 6 + + +def test_beginning_func(): + """Pressing shift+enter on the very first line, of function definition, such as 'my_func():'. + + It should properly skip the comment and assert the next executable line to be + executed is line 5 at 'my_dict = {'. + """ + importlib.reload(normalizeSelection) + src = textwrap.dedent( + """\ + def my_func(): + print("line 2") + print("line 3") + # Skip line 4 because it is a comment + my_dict = { + "key1": "value1", + "key2": "value2" + } + """ + ) + + result = normalizeSelection.traverse_file(src, 1, 1, was_highlighted=False) + + assert result["which_line_next"] == 5 + + +def test_cursor_forloop(): + importlib.reload(normalizeSelection) + src = textwrap.dedent( + """\ + lucid_dream = ["Corgi", "Husky", "Pomsky"] + for dogs in lucid_dream: # initial starting position + print(dogs) + print("I wish I had a dog!") + + print("This should be the next block that should be ran") + """ + ) + + result = normalizeSelection.traverse_file(src, 2, 2, was_highlighted=False) + + assert result["which_line_next"] == 6 + + +def test_inside_forloop(): + importlib.reload(normalizeSelection) + src = textwrap.dedent( + """\ + for food in lucid_dream: + print("We are starting") # initial starting position + print("Next cursor should be here!") + + """ + ) + + result = normalizeSelection.traverse_file(src, 2, 2, was_highlighted=False) + + assert result["which_line_next"] == 3 + + +def test_skip_sameline_statements(): + importlib.reload(normalizeSelection) + src = textwrap.dedent( + """\ + print("Audi");print("BMW");print("Mercedes") + print("Next line to be run is here!") + """ + ) + result = normalizeSelection.traverse_file(src, 1, 1, was_highlighted=False) + + assert result["which_line_next"] == 2 + + +def test_skip_multi_comp_lambda(): + importlib.reload(normalizeSelection) + src = textwrap.dedent( + """\ + ( + my_first_var + for my_first_var in range(1, 10) + if my_first_var % 2 == 0 + ) + + my_lambda = lambda x: ( + x + 1 + ) + """ + ) + + result = normalizeSelection.traverse_file(src, 1, 1, was_highlighted=False) + # Shift enter from the very first ( should make + # next executable statement as the lambda expression + assert result["which_line_next"] == 7 + + +def test_move_whole_class(): + """Shift+enter on a class definition should move the cursor after running whole class.""" + importlib.reload(normalizeSelection) + src = textwrap.dedent( + """\ + class Stub(object): + def __init__(self): + self.calls = [] + + def add_call(self, name, args=None, kwargs=None): + self.calls.append((name, args, kwargs)) + print("We should be here after running whole class") + """ + ) + result = normalizeSelection.traverse_file(src, 1, 1, was_highlighted=False) + + assert result["which_line_next"] == 7 + + +def test_def_to_def(): + importlib.reload(normalizeSelection) + src = textwrap.dedent( + """\ + def my_dogs(): + print("Corgi") + print("Husky") + print("Corgi2") + print("Husky2") + print("no dogs") + + # Skip here + def next_func(): + print("Not here but above") + """ + ) + result = normalizeSelection.traverse_file(src, 1, 1, was_highlighted=False) + + assert result["which_line_next"] == 9 + + +def test_try_catch_move(): + importlib.reload(normalizeSelection) + src = textwrap.dedent( + """\ + try: + 1+1 + except: + print("error") + + print("Should be here afterwards") + """ + ) + + result = normalizeSelection.traverse_file(src, 1, 1, was_highlighted=False) + assert result["which_line_next"] == 6 + + +def test_skip_nested(): + importlib.reload(normalizeSelection) + src = textwrap.dedent( + """\ + for i in range(1, 6): + for j in range(1, 6): + for x in range(1, 5): + for y in range(1, 5): + for z in range(1,10): + print(i, j, x, y, z) + + print("Cursor should be here after running line 1") + """ + ) + result = normalizeSelection.traverse_file(src, 1, 1, was_highlighted=False) + assert result["which_line_next"] == 8 diff --git a/python_files/tests/test_get_variable_info.py b/python_files/tests/test_get_variable_info.py new file mode 100644 index 000000000000..73f94fe26f06 --- /dev/null +++ b/python_files/tests/test_get_variable_info.py @@ -0,0 +1,114 @@ +import get_variable_info + + +def set_global_variable(value): + # setting on the module allows tests to set a variable that the module under test can access + get_variable_info.test_variable = value # pyright: ignore[reportGeneralTypeIssues] + + +def get_global_variable(): + results = get_variable_info.getVariableDescriptions() + for variable in results: + if variable["name"] == "test_variable": + return variable + return None + + +def assert_variable_found(variable, expected_value, expected_type, expected_count=None): + set_global_variable(variable) + variable = get_global_variable() + assert variable is not None + if expected_value is not None: + assert variable["value"] == expected_value + assert variable["type"] == expected_type + if expected_count is not None: + assert variable["count"] == expected_count + else: + assert "count" not in variable + return variable + + +def assert_indexed_child(variable, start_index, expected_index, expected_child_value=None): + children = get_variable_info.getAllChildrenDescriptions( + variable["root"], variable["propertyChain"], start_index + ) + child = children[expected_index] + + if expected_child_value is not None: + assert child["value"] == expected_child_value + return child + + +def assert_property(variable, expected_property_name, expected_property_value=None): + children = get_variable_info.getAllChildrenDescriptions( + variable["root"], variable["propertyChain"], 0 + ) + found = None + for child in children: + chain = child["propertyChain"] + property_name = chain[-1] if chain else None + if property_name == expected_property_name: + found = child + break + + assert found is not None + if expected_property_value is not None: + assert found["value"] == expected_property_value + return found + + +def test_simple(): + assert_variable_found(1, "1", "int", None) + + +def test_list(): + found = assert_variable_found([1, 2, 3], "[1, 2, 3]", "list", 3) + assert_indexed_child(found, 0, 0, "1") + + +def test_dict(): + found = assert_variable_found({"a": 1, "b": 2}, "{'a': 1, 'b': 2}", "dict", None) + assert found["hasNamedChildren"] + assert_property(found, "a", "1") + assert_property(found, "b", "2") + + +def test_tuple(): + found = assert_variable_found((1, 2, 3), "(1, 2, 3)", "tuple", 3) + assert_indexed_child(found, 0, 0, "1") + + +def test_set(): + found = assert_variable_found({1, 2, 3}, "{1, 2, 3}", "set", 3) + assert_indexed_child(found, 0, 0, "1") + + +def test_self_referencing_dict(): + d = {} + d["self"] = d + found = assert_variable_found(d, "{'self': {...}}", "dict", None) + assert_property(found, "self", "{'self': {...}}") + + +def test_nested_list(): + found = assert_variable_found([[1, 2], [3, 4]], "[[1, 2], [3, 4]]", "list", 2) + assert_indexed_child(found, 0, 0, "[1, 2]") + + +def test_long_list(): + child = assert_variable_found(list(range(1_000_000)), None, "list", 1_000_000) + value = child["value"] + assert value.startswith("[0, 1, 2, 3") + assert value.endswith("...]") + assert_indexed_child(child, 400_000, 10, "400010") + assert_indexed_child(child, 999_950, 10, "999960") + + +def test_get_nested_children(): + d = [{"a": {("hello")}}] + found = assert_variable_found(d, "[{'a': {...}}]", "list", 1) + + found = assert_indexed_child(found, 0, 0) + found = assert_property(found, "a") + found = assert_indexed_child(found, 0, 0) + assert found["value"] == "'hello'" diff --git a/python_files/tests/test_installed_check.py b/python_files/tests/test_installed_check.py new file mode 100644 index 000000000000..607e02f34abd --- /dev/null +++ b/python_files/tests/test_installed_check.py @@ -0,0 +1,138 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +import contextlib +import json +import os +import pathlib +import subprocess +import sys +from typing import Dict, List, Optional, Union + +import pytest + +SCRIPT_PATH = pathlib.Path(__file__).parent.parent / "installed_check.py" +TEST_DATA = pathlib.Path(__file__).parent / "test_data" +DEFAULT_SEVERITY = 3 + + +@contextlib.contextmanager +def generate_file(base_file: pathlib.Path): + basename = "pyproject.toml" if "pyproject" in base_file.name else "requirements.txt" + fullpath = base_file.parent / basename + if fullpath.exists(): + fullpath.unlink() + fullpath.write_text(base_file.read_text(encoding="utf-8")) + try: + yield fullpath + finally: + fullpath.unlink() + + +def run_on_file( + file_path: pathlib.Path, severity: Optional[str] = None +) -> List[Dict[str, Union[str, int]]]: + env = os.environ.copy() + if severity: + env["VSCODE_MISSING_PGK_SEVERITY"] = severity + result = subprocess.run( + [ + sys.executable, + os.fspath(SCRIPT_PATH), + os.fspath(file_path), + ], + capture_output=True, + check=True, + env=env, + ) + assert result.returncode == 0 + assert result.stderr == b"" + return json.loads(result.stdout) + + +EXPECTED_DATA = { + "missing-deps": [ + { + "line": 6, + "character": 0, + "endLine": 6, + "endCharacter": 10, + "package": "flake8-csv", + "code": "not-installed", + "severity": 3, + }, + { + "line": 10, + "character": 0, + "endLine": 10, + "endCharacter": 11, + "package": "levenshtein", + "code": "not-installed", + "severity": 3, + }, + ], + "no-missing-deps": [], + "pyproject-missing-deps": [ + { + "line": 8, + "character": 34, + "endLine": 8, + "endCharacter": 44, + "package": "flake8-csv", + "code": "not-installed", + "severity": 3, + } + ], + "pyproject-no-missing-deps": [], +} + + +@pytest.mark.parametrize("test_name", EXPECTED_DATA.keys()) +def test_installed_check(test_name: str): + base_file = TEST_DATA / f"{test_name}.data" + with generate_file(base_file) as file_path: + result = run_on_file(file_path) + assert result == EXPECTED_DATA[test_name] + + +EXPECTED_DATA2 = { + "missing-deps": [ + { + "line": 6, + "character": 0, + "endLine": 6, + "endCharacter": 10, + "package": "flake8-csv", + "code": "not-installed", + "severity": 0, + }, + { + "line": 10, + "character": 0, + "endLine": 10, + "endCharacter": 11, + "package": "levenshtein", + "code": "not-installed", + "severity": 0, + }, + ], + "pyproject-missing-deps": [ + { + "line": 8, + "character": 34, + "endLine": 8, + "endCharacter": 44, + "package": "flake8-csv", + "code": "not-installed", + "severity": 0, + } + ], +} + + +@pytest.mark.parametrize("test_name", EXPECTED_DATA2.keys()) +def test_with_severity(test_name: str): + base_file = TEST_DATA / f"{test_name}.data" + with generate_file(base_file) as file_path: + result = run_on_file(file_path, severity="0") + assert result == EXPECTED_DATA2[test_name] diff --git a/pythonFiles/tests/test_normalize_selection.py b/python_files/tests/test_normalize_selection.py similarity index 60% rename from pythonFiles/tests/test_normalize_selection.py rename to python_files/tests/test_normalize_selection.py index 138c5ad2f522..779bb9720bfa 100644 --- a/pythonFiles/tests/test_normalize_selection.py +++ b/python_files/tests/test_normalize_selection.py @@ -1,21 +1,25 @@ # Copyright (c) Microsoft Corporation. All rights reserved. # Licensed under the MIT License. + +import importlib import textwrap +# __file__ = "/Users/anthonykim/Desktop/vscode-python/python_files/normalizeSelection.py" +# sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__)))) import normalizeSelection -class TestNormalizationScript(object): +class TestNormalizationScript: """Unit tests for the normalization script.""" - def test_basicNormalization(self): + def test_basic_normalization(self): src = 'print("this is a test")' expected = src + "\n" result = normalizeSelection.normalize_lines(src) assert result == expected - def test_moreThanOneLine(self): + def test_more_than_one_line(self): src = textwrap.dedent( """\ # Some rando comment @@ -34,7 +38,7 @@ def show_something(): result = normalizeSelection.normalize_lines(src) assert result == expected - def test_withHangingIndent(self): + def test_with_hanging_indent(self): src = textwrap.dedent( """\ x = 22 @@ -60,7 +64,7 @@ def test_withHangingIndent(self): result = normalizeSelection.normalize_lines(src) assert result == expected - def test_clearOutExtraneousNewlines(self): + def test_clear_out_extraneous_newlines(self): src = textwrap.dedent( """\ value_x = 22 @@ -84,7 +88,7 @@ def test_clearOutExtraneousNewlines(self): result = normalizeSelection.normalize_lines(src) assert result == expected - def test_clearOutExtraLinesAndWhitespace(self): + def test_clear_out_extra_lines_and_whitespace(self): src = textwrap.dedent( """\ if True: @@ -111,13 +115,13 @@ def test_clearOutExtraLinesAndWhitespace(self): result = normalizeSelection.normalize_lines(src) assert result == expected - def test_partialSingleLine(self): + def test_partial_single_line(self): src = " print('foo')" expected = textwrap.dedent(src) + "\n" result = normalizeSelection.normalize_lines(src) assert result == expected - def test_multiLineWithIndent(self): + def test_multiline_with_indent(self): src = """\ if (x > 0 @@ -142,7 +146,7 @@ def test_multiLineWithIndent(self): result = normalizeSelection.normalize_lines(src) assert result == expected - def test_multiLineWithComment(self): + def test_multiline_with_comment(self): src = textwrap.dedent( """\ @@ -168,7 +172,7 @@ def test_exception(self): result = normalizeSelection.normalize_lines(src) assert result == expected - def test_multilineException(self): + def test_multiline_exception(self): src = textwrap.dedent( """\ @@ -215,3 +219,99 @@ def show_something(): ) result = normalizeSelection.normalize_lines(src) assert result == expected + + def test_fstring(self): + importlib.reload(normalizeSelection) + src = textwrap.dedent( + """\ + name = "Ahri" + age = 10 + + print(f'My name is {name}') + """ + ) + + expected = textwrap.dedent( + """\ + name = "Ahri" + age = 10 + print(f'My name is {name}') + """ + ) + result = normalizeSelection.normalize_lines(src) + + assert result == expected + + def test_list_comp(self): + importlib.reload(normalizeSelection) + src = textwrap.dedent( + """\ + names = ['Ahri', 'Bobby', 'Charlie'] + breed = ['Pomeranian', 'Welsh Corgi', 'Siberian Husky'] + dogs = [(name, breed) for name, breed in zip(names, breed)] + + print(dogs) + my_family_dog = 'Corgi' + """ + ) + + expected = textwrap.dedent( + """\ + names = ['Ahri', 'Bobby', 'Charlie'] + breed = ['Pomeranian', 'Welsh Corgi', 'Siberian Husky'] + dogs = [(name, breed) for name, breed in zip(names, breed)] + print(dogs) + my_family_dog = 'Corgi' + """ + ) + + result = normalizeSelection.normalize_lines(src) + + assert result == expected + + def test_return_dict(self): + importlib.reload(normalizeSelection) + src = textwrap.dedent( + """\ + def get_dog(name, breed): + return {'name': name, 'breed': breed} + """ + ) + + expected = textwrap.dedent( + """\ + def get_dog(name, breed): + return {'name': name, 'breed': breed} + + """ + ) + + result = normalizeSelection.normalize_lines(src) + + assert result == expected + + def test_return_dict2(self): + importlib.reload(normalizeSelection) + src = textwrap.dedent( + """\ + def get_dog(name, breed): + return {'name': name, 'breed': breed} + + dog = get_dog('Ahri', 'Pomeranian') + print(dog) + """ + ) + + expected = textwrap.dedent( + """\ + def get_dog(name, breed): + return {'name': name, 'breed': breed} + + dog = get_dog('Ahri', 'Pomeranian') + print(dog) + """ + ) + + result = normalizeSelection.normalize_lines(src) + + assert result == expected diff --git a/python_files/tests/test_python_server.py b/python_files/tests/test_python_server.py new file mode 100644 index 000000000000..ca542b8ea292 --- /dev/null +++ b/python_files/tests/test_python_server.py @@ -0,0 +1,162 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +"""Tests for python_server.py, specifically EOF handling to prevent infinite loops.""" + +import io +from unittest import mock + +import pytest + + +class TestGetHeaders: + """Tests for the get_headers function.""" + + def test_get_headers_normal(self): + """Test get_headers with valid headers.""" + # Arrange: Import the module + import python_server + + # Create a mock stdin with valid headers + mock_input = b"Content-Length: 100\r\nContent-Type: application/json\r\n\r\n" + mock_stdin = io.BytesIO(mock_input) + + # Act + with mock.patch.object(python_server, "STDIN", mock.Mock(buffer=mock_stdin)): + headers = python_server.get_headers() + + # Assert + assert headers == {"Content-Length": "100", "Content-Type": "application/json"} + + def test_get_headers_eof_raises_error(self): + """Test that get_headers raises EOFError when stdin is closed (EOF).""" + # Arrange: Import the module + import python_server + + # Create a mock stdin that returns empty bytes (EOF) + mock_stdin = io.BytesIO(b"") + + # Act & Assert + with mock.patch.object(python_server, "STDIN", mock.Mock(buffer=mock_stdin)), pytest.raises( + EOFError, match="EOF reached while reading headers" + ): + python_server.get_headers() + + def test_get_headers_eof_mid_headers_raises_error(self): + """Test that get_headers raises EOFError when EOF occurs mid-headers.""" + # Arrange: Import the module + import python_server + + # Create a mock stdin with partial headers then EOF + mock_input = b"Content-Length: 100\r\n" # No terminating empty line + mock_stdin = io.BytesIO(mock_input) + + # Act & Assert + with mock.patch.object(python_server, "STDIN", mock.Mock(buffer=mock_stdin)), pytest.raises( + EOFError, match="EOF reached while reading headers" + ): + python_server.get_headers() + + def test_get_headers_empty_line_terminates(self): + """Test that an empty line (not EOF) properly terminates header reading.""" + # Arrange: Import the module + import python_server + + # Create a mock stdin with headers followed by empty line + mock_input = b"Content-Length: 50\r\n\r\nsome body content" + mock_stdin = io.BytesIO(mock_input) + + # Act + with mock.patch.object(python_server, "STDIN", mock.Mock(buffer=mock_stdin)): + headers = python_server.get_headers() + + # Assert + assert headers == {"Content-Length": "50"} + + +class TestEOFHandling: + """Tests for EOF handling in various functions that use get_headers.""" + + def test_custom_input_exits_on_eof(self): + """Test that custom_input exits gracefully on EOF.""" + # Arrange: Import the module + import python_server + + # Create a mock stdin that returns empty bytes (EOF) + mock_stdin = io.BytesIO(b"") + mock_stdout = io.BytesIO() + + # Act & Assert + with mock.patch.object( + python_server, "STDIN", mock.Mock(buffer=mock_stdin) + ), mock.patch.object(python_server, "STDOUT", mock.Mock(buffer=mock_stdout)), pytest.raises( + SystemExit + ) as exc_info: + python_server.custom_input("prompt> ") + + # Should exit with code 0 (graceful exit) + assert exc_info.value.code == 0 + + def test_handle_response_exits_on_eof(self): + """Test that handle_response exits gracefully on EOF.""" + # Arrange: Import the module + import python_server + + # Create a mock stdin that returns empty bytes (EOF) + mock_stdin = io.BytesIO(b"") + + # Act & Assert + with mock.patch.object(python_server, "STDIN", mock.Mock(buffer=mock_stdin)), pytest.raises( + SystemExit + ) as exc_info: + python_server.handle_response("test-request-id") + + # Should exit with code 0 (graceful exit) + assert exc_info.value.code == 0 + + +class TestMainLoopEOFHandling: + """Tests that simulate the main loop EOF scenario.""" + + def test_main_loop_exits_on_eof(self): + """Test that the main loop pattern exits gracefully on EOF. + + This test verifies the fix for GitHub issue #25620 where the server + would spin at 100% CPU instead of exiting when VS Code closes. + """ + # Arrange: Import the module + import python_server + + # Create a mock stdin that returns empty bytes (EOF) + mock_stdin = io.BytesIO(b"") + + # Simulate what happens in the main loop + with mock.patch.object(python_server, "STDIN", mock.Mock(buffer=mock_stdin)): + try: + python_server.get_headers() + # If we get here without raising EOFError, the fix isn't working + pytest.fail("Expected EOFError to be raised on EOF") + except EOFError: + # This is the expected behavior - the fix is working + pass + + def test_readline_eof_vs_empty_line(self): + """Test that we correctly distinguish between EOF and empty line. + + EOF: readline() returns b'' (empty bytes) + Empty line: readline() returns b'\\r\\n' or b'\\n' (newline bytes) + """ + # Test EOF case + eof_stream = io.BytesIO(b"") + result = eof_stream.readline() + assert result == b"", "EOF should return empty bytes" + + # Test empty line case + empty_line_stream = io.BytesIO(b"\r\n") + result = empty_line_stream.readline() + assert result == b"\r\n", "Empty line should return newline bytes" + + # Test empty line with just newline + empty_line_stream2 = io.BytesIO(b"\n") + result = empty_line_stream2.readline() + assert result == b"\n", "Empty line should return newline bytes" diff --git a/python_files/tests/test_shell_integration.py b/python_files/tests/test_shell_integration.py new file mode 100644 index 000000000000..7503a725b6d1 --- /dev/null +++ b/python_files/tests/test_shell_integration.py @@ -0,0 +1,83 @@ +import importlib +import platform +import sys +from unittest.mock import Mock + +import pythonrc + +is_wsl = "microsoft-standard-WSL" in platform.release() + + +def test_decoration_success(): + importlib.reload(pythonrc) + ps1 = pythonrc.PS1() + + ps1.hooks.failure_flag = False + result = str(ps1) + if sys.platform != "win32" and (not is_wsl): + assert ( + result + == "\x01\x1b]633;C\x07\x1b]633;E;None\x07\x1b]633;D;0\x07\x1b]633;A\x07\x02>>> \x01\x1b]633;B\x07\x02" + ) + else: + pass + + +def test_decoration_failure(): + importlib.reload(pythonrc) + ps1 = pythonrc.PS1() + + ps1.hooks.failure_flag = True + result = str(ps1) + if sys.platform != "win32" and (not is_wsl): + assert ( + result + == "\x01\x1b]633;C\x07\x1b]633;E;None\x07\x1b]633;D;1\x07\x1b]633;A\x07\x02>>> \x01\x1b]633;B\x07\x02" + ) + else: + pass + + +def test_displayhook_call(): + importlib.reload(pythonrc) + pythonrc.PS1() + mock_displayhook = Mock() + + hooks = pythonrc.REPLHooks() + hooks.original_displayhook = mock_displayhook + + hooks.my_displayhook("mock_value") + + mock_displayhook.assert_called_once_with("mock_value") + + +def test_excepthook_call(): + importlib.reload(pythonrc) + pythonrc.PS1() + mock_excepthook = Mock() + + hooks = pythonrc.REPLHooks() + hooks.original_excepthook = mock_excepthook + + hooks.my_excepthook("mock_type", "mock_value", "mock_traceback") + mock_excepthook.assert_called_once_with("mock_type", "mock_value", "mock_traceback") + + +if sys.platform == "darwin": + + def test_print_statement_darwin(monkeypatch): + importlib.reload(pythonrc) + with monkeypatch.context() as m: + m.setattr("builtins.print", Mock()) + importlib.reload(sys.modules["pythonrc"]) + print.assert_any_call("Cmd click to launch VS Code Native REPL") + + +if sys.platform == "win32": + + def test_print_statement_non_darwin(monkeypatch): + importlib.reload(pythonrc) + with monkeypatch.context() as m: + m.setattr("builtins.print", Mock()) + importlib.reload(sys.modules["pythonrc"]) + print.assert_any_call("Ctrl click to launch VS Code Native REPL") diff --git a/python_files/tests/test_smart_selection.py b/python_files/tests/test_smart_selection.py new file mode 100644 index 000000000000..15b1b1a3ec02 --- /dev/null +++ b/python_files/tests/test_smart_selection.py @@ -0,0 +1,360 @@ +import importlib +import textwrap + +import normalizeSelection + + +def test_part_dictionary(): + importlib.reload(normalizeSelection) + src = textwrap.dedent( + """\ + not_dictionary = 'hi' + my_dict = { + "key1": "value1", + "key2": "value2" + } + print('only send the dictionary') + """ + ) + + expected = textwrap.dedent( + """\ + my_dict = { + "key1": "value1", + "key2": "value2" + } + """ + ) + + result = normalizeSelection.traverse_file(src, 3, 3, was_highlighted=False) + assert result["normalized_smart_result"] == expected + + +def test_nested_loop(): + importlib.reload(normalizeSelection) + src = textwrap.dedent( + """\ + for i in range(1, 6): + for j in range(1, 6): + for x in range(1, 5): + for y in range(1, 5): + for z in range(1,10): + print(i, j, x, y, z) + """ + ) + expected = textwrap.dedent( + """\ + for i in range(1, 6): + for j in range(1, 6): + for x in range(1, 5): + for y in range(1, 5): + for z in range(1,10): + print(i, j, x, y, z) + + """ + ) + result = normalizeSelection.traverse_file(src, 1, 1, was_highlighted=False) + assert result["normalized_smart_result"] == expected + + +def test_smart_shift_enter_multiple_statements(): + importlib.reload(normalizeSelection) + src = textwrap.dedent( + """\ + import textwrap + import ast + + print("Porsche") + print("Genesis") + + + print("Audi");print("BMW");print("Mercedes") + + print("dont print me") + + """ + ) + # Expected to printing statement line by line, + # for when multiple print statements are ran + # from the same line. + expected = textwrap.dedent( + """\ + print("Audi") + print("BMW") + print("Mercedes") + """ + ) + result = normalizeSelection.traverse_file(src, 8, 8, was_highlighted=False) + assert result["normalized_smart_result"] == expected + + +def test_two_layer_dictionary(): + importlib.reload(normalizeSelection) + src = textwrap.dedent( + """\ + print("dont print me") + + two_layered_dictionary = { + 'inner_dict_one': { + 'Audi': 'Germany', + 'BMW': 'Germnay', + 'Genesis': 'Korea', + }, + 'inner_dict_two': { + 'Mercedes': 'Germany', + 'Porsche': 'Germany', + 'Lamborghini': 'Italy', + 'Ferrari': 'Italy', + 'Maserati': 'Italy' + } + } + """ + ) + expected = textwrap.dedent( + """\ + two_layered_dictionary = { + 'inner_dict_one': { + 'Audi': 'Germany', + 'BMW': 'Germnay', + 'Genesis': 'Korea', + }, + 'inner_dict_two': { + 'Mercedes': 'Germany', + 'Porsche': 'Germany', + 'Lamborghini': 'Italy', + 'Ferrari': 'Italy', + 'Maserati': 'Italy' + } + } + """ + ) + result = normalizeSelection.traverse_file(src, 6, 7, was_highlighted=False) + + assert result["normalized_smart_result"] == expected + + +def test_run_whole_func(): + importlib.reload(normalizeSelection) + src = textwrap.dedent( + """\ + print("Decide which dog you will choose") + def my_dogs(): + print("Corgi") + print("Husky") + print("Corgi2") + print("Husky2") + print("no dogs") + """ + ) + + expected = textwrap.dedent( + """\ + def my_dogs(): + print("Corgi") + print("Husky") + print("Corgi2") + print("Husky2") + print("no dogs") + + """ + ) + result = normalizeSelection.traverse_file(src, 2, 2, was_highlighted=False) + + assert result["normalized_smart_result"] == expected + + +def test_small_forloop(): + importlib.reload(normalizeSelection) + src = textwrap.dedent( + """\ + for i in range(1, 6): + print(i) + print("Please also send this print statement") + """ + ) + expected = textwrap.dedent( + """\ + for i in range(1, 6): + print(i) + print("Please also send this print statement") + + """ + ) + + # Cover the whole for loop block with multiple inner statements + # Make sure to contain all of the print statements included. + result = normalizeSelection.traverse_file(src, 1, 1, was_highlighted=False) + + assert result["normalized_smart_result"] == expected + + +def inner_for_loop_component(): + """Pressing shift+enter inside a for loop, specifically on a viable expression by itself, such as print(i) should only return that exact expression.""" + importlib.reload(normalizeSelection) + src = textwrap.dedent( + """\ + for i in range(1, 6): + print(i) + print("Please also send this print statement") + """ + ) + result = normalizeSelection.traverse_file(src, 2, 2, was_highlighted=False) + expected = textwrap.dedent( + """\ + print(i) + """ + ) + + assert result["normalized_smart_result"] == expected + + +def test_dict_comprehension(): + """Having the mouse cursor on the first line, and pressing shift+enter should return the whole dictionary comp, respecting user's code style.""" + src = textwrap.dedent( + """\ + my_dict_comp = {temp_mover: + temp_mover for temp_mover in range(1, 7)} + """ + ) + + expected = textwrap.dedent( + """\ + my_dict_comp = {temp_mover: + temp_mover for temp_mover in range(1, 7)} + """ + ) + + result = normalizeSelection.traverse_file(src, 1, 1, was_highlighted=False) + + assert result["normalized_smart_result"] == expected + + +def test_send_whole_generator(): + """Pressing shift+enter on the first line, which is the '(' should be returning the whole generator expression instead of just the '('.""" + importlib.reload(normalizeSelection) + src = textwrap.dedent( + """\ + ( + my_first_var + for my_first_var in range(1, 10) + if my_first_var % 2 == 0 + ) + """ + ) + + expected = textwrap.dedent( + """\ + ( + my_first_var + for my_first_var in range(1, 10) + if my_first_var % 2 == 0 + ) + + """ + ) + + result = normalizeSelection.traverse_file(src, 1, 1, was_highlighted=False) + + assert result["normalized_smart_result"] == expected + + +def test_multiline_lambda(): + """Shift+enter on part of the lambda expression should return the whole lambda expression, regardless of whether all the component of lambda expression is on the same or not.""" + importlib.reload(normalizeSelection) + src = textwrap.dedent( + """\ + my_lambda = lambda x: ( + x + 1 + ) + """ + ) + expected = textwrap.dedent( + """\ + my_lambda = lambda x: ( + x + 1 + ) + + """ + ) + + result = normalizeSelection.traverse_file(src, 1, 1, was_highlighted=False) + assert result["normalized_smart_result"] == expected + + +def test_send_whole_class(): + """Shift+enter on a class definition should send the whole class definition.""" + importlib.reload(normalizeSelection) + src = textwrap.dedent( + """\ + class Stub(object): + def __init__(self): + self.calls = [] + + def add_call(self, name, args=None, kwargs=None): + self.calls.append((name, args, kwargs)) + print("We should be here after running whole class") + """ + ) + result = normalizeSelection.traverse_file(src, 1, 1, was_highlighted=False) + expected = textwrap.dedent( + """\ + class Stub(object): + def __init__(self): + self.calls = [] + def add_call(self, name, args=None, kwargs=None): + self.calls.append((name, args, kwargs)) + + """ + ) + assert result["normalized_smart_result"] == expected + + +def test_send_whole_if_statement(): + """Shift+enter on an if statement should send the whole if statement including statements inside and else.""" + importlib.reload(normalizeSelection) + src = textwrap.dedent( + """\ + if True: + print('send this') + else: + print('also send this') + + print('cursor here afterwards') + """ + ) + expected = textwrap.dedent( + """\ + if True: + print('send this') + else: + print('also send this') + + """ + ) + result = normalizeSelection.traverse_file(src, 1, 1, was_highlighted=False) + assert result["normalized_smart_result"] == expected + + +def test_send_try(): + importlib.reload(normalizeSelection) + src = textwrap.dedent( + """\ + try: + 1+1 + except: + print("error") + + print("Not running this") + """ + ) + expected = textwrap.dedent( + """\ + try: + 1+1 + except: + print("error") + + """ + ) + result = normalizeSelection.traverse_file(src, 1, 1, was_highlighted=False) + assert result["normalized_smart_result"] == expected diff --git a/python_files/tests/tree_comparison_helper.py b/python_files/tests/tree_comparison_helper.py new file mode 100644 index 000000000000..3d9d1d39194b --- /dev/null +++ b/python_files/tests/tree_comparison_helper.py @@ -0,0 +1,39 @@ +def is_same_tree(tree1, tree2, test_key_arr, path="root") -> bool: + """Helper function to test if two test trees are the same with detailed error logs. + + `is_same_tree` starts by comparing the root attributes, and then checks if all children are the same. + """ + # Compare the root. + for key in ["path", "name", "type_", "id_"]: + if tree1.get(key) != tree2.get(key): + print( + f"Difference found at {path}: '{key}' is '{tree1.get(key)}' in tree1 and '{tree2.get(key)}' in tree2." + ) + return False + + # Compare child test nodes if they exist, otherwise compare test items. + if "children" in tree1 and "children" in tree2: + # Sort children by path before comparing since order doesn't matter of children + children1 = sorted(tree1["children"], key=lambda x: x["path"]) + children2 = sorted(tree2["children"], key=lambda x: x["path"]) + + # Compare test nodes. + if len(children1) != len(children2): + print( + f"Difference in number of children at {path}: {len(children1)} in tree1 and {len(children2)} in tree2." + ) + return False + else: + for i, (child1, child2) in enumerate(zip(children1, children2)): + if not is_same_tree(child1, child2, test_key_arr, path=f"{path} -> child {i}"): + return False + elif "id_" in tree1 and "id_" in tree2: + # Compare test items. + for key in test_key_arr: + if tree1.get(key) != tree2.get(key): + print( + f"Difference found at {path}: '{key}' is '{tree1.get(key)}' in tree1 and '{tree2.get(key)}' in tree2." + ) + return False + + return True diff --git a/pythonFiles/tests/pytestadapter/__init__.py b/python_files/tests/unittestadapter/.data/coverage_ex/__init__.py similarity index 100% rename from pythonFiles/tests/pytestadapter/__init__.py rename to python_files/tests/unittestadapter/.data/coverage_ex/__init__.py diff --git a/python_files/tests/unittestadapter/.data/coverage_ex/reverse.py b/python_files/tests/unittestadapter/.data/coverage_ex/reverse.py new file mode 100644 index 000000000000..4840b7d05bf3 --- /dev/null +++ b/python_files/tests/unittestadapter/.data/coverage_ex/reverse.py @@ -0,0 +1,14 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +def reverse_string(s): + if s is None or s == "": + return "Error: Input is None" + return s[::-1] + +def reverse_sentence(sentence): + if sentence is None or sentence == "": + return "Error: Input is None" + words = sentence.split() + reversed_words = [reverse_string(word) for word in words] + return " ".join(reversed_words) diff --git a/python_files/tests/unittestadapter/.data/coverage_ex/test_reverse.py b/python_files/tests/unittestadapter/.data/coverage_ex/test_reverse.py new file mode 100644 index 000000000000..2521e3dc1935 --- /dev/null +++ b/python_files/tests/unittestadapter/.data/coverage_ex/test_reverse.py @@ -0,0 +1,32 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +import unittest +from reverse import reverse_sentence, reverse_string + +class TestReverseFunctions(unittest.TestCase): + + def test_reverse_sentence(self): + """ + Tests the reverse_sentence function to ensure it correctly reverses each word in a sentence. + + Test cases: + - "hello world" should be reversed to "olleh dlrow" + - "Python is fun" should be reversed to "nohtyP si nuf" + - "a b c" should remain "a b c" as each character is a single word + """ + self.assertEqual(reverse_sentence("hello world"), "olleh dlrow") + self.assertEqual(reverse_sentence("Python is fun"), "nohtyP si nuf") + self.assertEqual(reverse_sentence("a b c"), "a b c") + + def test_reverse_sentence_error(self): + self.assertEqual(reverse_sentence(""), "Error: Input is None") + self.assertEqual(reverse_sentence(None), "Error: Input is None") + + def test_reverse_string(self): + self.assertEqual(reverse_string("hello"), "olleh") + self.assertEqual(reverse_string("Python"), "nohtyP") + # this test specifically does not cover the error cases + +if __name__ == '__main__': + unittest.main() diff --git a/pythonFiles/tests/unittestadapter/.data/discovery_empty.py b/python_files/tests/unittestadapter/.data/discovery_empty.py similarity index 100% rename from pythonFiles/tests/unittestadapter/.data/discovery_empty.py rename to python_files/tests/unittestadapter/.data/discovery_empty.py diff --git a/pythonFiles/tests/unittestadapter/.data/discovery_error/file_one.py b/python_files/tests/unittestadapter/.data/discovery_error/file_one.py similarity index 91% rename from pythonFiles/tests/unittestadapter/.data/discovery_error/file_one.py rename to python_files/tests/unittestadapter/.data/discovery_error/file_one.py index 42f84f046760..031b6f6c9d68 100644 --- a/pythonFiles/tests/unittestadapter/.data/discovery_error/file_one.py +++ b/python_files/tests/unittestadapter/.data/discovery_error/file_one.py @@ -3,7 +3,7 @@ import unittest -import something_else # type: ignore +import something_else # type: ignore # noqa: F401 class DiscoveryErrorOne(unittest.TestCase): diff --git a/pythonFiles/tests/unittestadapter/.data/discovery_error/file_two.py b/python_files/tests/unittestadapter/.data/discovery_error/file_two.py similarity index 100% rename from pythonFiles/tests/unittestadapter/.data/discovery_error/file_two.py rename to python_files/tests/unittestadapter/.data/discovery_error/file_two.py diff --git a/pythonFiles/tests/unittestadapter/.data/discovery_simple.py b/python_files/tests/unittestadapter/.data/discovery_simple.py similarity index 100% rename from pythonFiles/tests/unittestadapter/.data/discovery_simple.py rename to python_files/tests/unittestadapter/.data/discovery_simple.py diff --git a/python_files/tests/unittestadapter/.data/doctest_patched_module.py b/python_files/tests/unittestadapter/.data/doctest_patched_module.py new file mode 100644 index 000000000000..636c5320b6d6 --- /dev/null +++ b/python_files/tests/unittestadapter/.data/doctest_patched_module.py @@ -0,0 +1,17 @@ +""" +Patched doctest module. +This module's doctests will be patched to have proper IDs. + +>>> 2 + 2 +4 +""" + + +def example_function(): + """ + Example function with doctest. + + >>> example_function() + 'works' + """ + return "works" diff --git a/python_files/tests/unittestadapter/.data/doctest_standard.py b/python_files/tests/unittestadapter/.data/doctest_standard.py new file mode 100644 index 000000000000..52a10aa46a7f --- /dev/null +++ b/python_files/tests/unittestadapter/.data/doctest_standard.py @@ -0,0 +1,7 @@ +""" +Standard doctest module that should be blocked. +This has a simple doctest with short ID. + +>>> 2 + 2 +4 +""" diff --git a/python_files/tests/unittestadapter/.data/simple_django/db.sqlite3 b/python_files/tests/unittestadapter/.data/simple_django/db.sqlite3 new file mode 100644 index 000000000000..519ec5e1a11c Binary files /dev/null and b/python_files/tests/unittestadapter/.data/simple_django/db.sqlite3 differ diff --git a/python_files/tests/unittestadapter/.data/simple_django/manage.py b/python_files/tests/unittestadapter/.data/simple_django/manage.py new file mode 100755 index 000000000000..c5734a6babee --- /dev/null +++ b/python_files/tests/unittestadapter/.data/simple_django/manage.py @@ -0,0 +1,23 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. +"""Django's command-line utility for administrative tasks.""" +import os +import sys + + +def main(): + """Run administrative tasks.""" + os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'mysite.settings') + try: + from django.core.management import execute_from_command_line + except ImportError as exc: + raise ImportError( + "Couldn't import Django. Are you sure it's installed and " + "available on your PYTHONPATH environment variable? Did you " + "forget to activate a virtual environment?" + ) from exc + execute_from_command_line(sys.argv) + + +if __name__ == '__main__': + main() diff --git a/pythonFiles/tests/testing_tools/__init__.py b/python_files/tests/unittestadapter/.data/simple_django/mysite/__init__.py similarity index 100% rename from pythonFiles/tests/testing_tools/__init__.py rename to python_files/tests/unittestadapter/.data/simple_django/mysite/__init__.py diff --git a/python_files/tests/unittestadapter/.data/simple_django/mysite/asgi.py b/python_files/tests/unittestadapter/.data/simple_django/mysite/asgi.py new file mode 100644 index 000000000000..bb01f607934c --- /dev/null +++ b/python_files/tests/unittestadapter/.data/simple_django/mysite/asgi.py @@ -0,0 +1,9 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. +import os + +from django.core.asgi import get_asgi_application + +os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'mysite.settings') + +application = get_asgi_application() diff --git a/python_files/tests/unittestadapter/.data/simple_django/mysite/settings.py b/python_files/tests/unittestadapter/.data/simple_django/mysite/settings.py new file mode 100644 index 000000000000..3120fb4e829f --- /dev/null +++ b/python_files/tests/unittestadapter/.data/simple_django/mysite/settings.py @@ -0,0 +1,102 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. +""" +Django settings for mysite project. + +Generated by 'django-admin startproject' using Django 3.2.22. + +For more information on this file, see +https://docs.djangoproject.com/en/3.2/topics/settings/ + +For the full list of settings and their values, see +https://docs.djangoproject.com/en/3.2/ref/settings/ +""" + +from pathlib import Path + +# Build paths inside the project like this: BASE_DIR / 'subdir'. +BASE_DIR = Path(__file__).resolve().parent.parent + + +ALLOWED_HOSTS = [] + + +# Application definition + +INSTALLED_APPS = [ + "polls.apps.PollsConfig", + "django.contrib.admin", + "django.contrib.auth", + "django.contrib.contenttypes", + "django.contrib.sessions", + "django.contrib.messages", + "django.contrib.staticfiles", +] + +MIDDLEWARE = [ + 'django.middleware.security.SecurityMiddleware', + 'django.contrib.sessions.middleware.SessionMiddleware', + 'django.middleware.common.CommonMiddleware', + 'django.middleware.csrf.CsrfViewMiddleware', + 'django.contrib.auth.middleware.AuthenticationMiddleware', + 'django.contrib.messages.middleware.MessageMiddleware', + 'django.middleware.clickjacking.XFrameOptionsMiddleware', +] + +ROOT_URLCONF = 'mysite.urls' + +TEMPLATES = [ + { + 'BACKEND': 'django.template.backends.django.DjangoTemplates', + 'DIRS': [], + 'APP_DIRS': True, + 'OPTIONS': { + 'context_processors': [ + 'django.template.context_processors.debug', + 'django.template.context_processors.request', + 'django.contrib.auth.context_processors.auth', + 'django.contrib.messages.context_processors.messages', + ], + }, + }, +] + +WSGI_APPLICATION = 'mysite.wsgi.application' + + +# Database +# https://docs.djangoproject.com/en/3.2/ref/settings/#databases + +DATABASES = { + 'default': { + 'ENGINE': 'django.db.backends.sqlite3', + 'NAME': BASE_DIR / 'db.sqlite3', + } +} + + + + +# Internationalization +# https://docs.djangoproject.com/en/3.2/topics/i18n/ + +LANGUAGE_CODE = 'en-us' + +TIME_ZONE = 'UTC' + +USE_I18N = True + +USE_L10N = True + +USE_TZ = True + + +# Static files (CSS, JavaScript, Images) +# https://docs.djangoproject.com/en/3.2/howto/static-files/ + +STATIC_URL = '/static/' + +# Default primary key field type +# https://docs.djangoproject.com/en/3.2/ref/settings/#default-auto-field + +DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' diff --git a/python_files/tests/unittestadapter/.data/simple_django/mysite/urls.py b/python_files/tests/unittestadapter/.data/simple_django/mysite/urls.py new file mode 100644 index 000000000000..02e76f125c72 --- /dev/null +++ b/python_files/tests/unittestadapter/.data/simple_django/mysite/urls.py @@ -0,0 +1,9 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. +from django.contrib import admin +from django.urls import include, path + +urlpatterns = [ + path("polls/", include("polls.urls")), + path("admin/", admin.site.urls), +] diff --git a/python_files/tests/unittestadapter/.data/simple_django/mysite/wsgi.py b/python_files/tests/unittestadapter/.data/simple_django/mysite/wsgi.py new file mode 100644 index 000000000000..e932bff6649e --- /dev/null +++ b/python_files/tests/unittestadapter/.data/simple_django/mysite/wsgi.py @@ -0,0 +1,7 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. +import os + +from django.core.wsgi import get_wsgi_application + +application = get_wsgi_application() diff --git a/python_files/tests/unittestadapter/.data/simple_django/old_manage.py b/python_files/tests/unittestadapter/.data/simple_django/old_manage.py new file mode 100755 index 000000000000..844b98b4edba --- /dev/null +++ b/python_files/tests/unittestadapter/.data/simple_django/old_manage.py @@ -0,0 +1,21 @@ +#!/usr/bin/env python +import os +import sys +if __name__ == "__main__": + os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'mysite.settings') + try: + from django.core.management import execute_from_command_line + except ImportError: + # The above import may fail for some other reason. Ensure that the + # issue is really that Django is missing to avoid masking other + # exceptions on Python 2. + try: + import django + except ImportError: + raise ImportError( + "Couldn't import Django. Are you sure it's installed and " + "available on your PYTHONPATH environment variable? Did you " + "forget to activate a virtual environment?" + ) + raise + execute_from_command_line(sys.argv) diff --git a/pythonFiles/tests/testing_tools/adapter/__init__.py b/python_files/tests/unittestadapter/.data/simple_django/polls/__init__.py similarity index 100% rename from pythonFiles/tests/testing_tools/adapter/__init__.py rename to python_files/tests/unittestadapter/.data/simple_django/polls/__init__.py diff --git a/pythonFiles/tests/testing_tools/adapter/pytest/__init__.py b/python_files/tests/unittestadapter/.data/simple_django/polls/admin.py similarity index 100% rename from pythonFiles/tests/testing_tools/adapter/pytest/__init__.py rename to python_files/tests/unittestadapter/.data/simple_django/polls/admin.py diff --git a/python_files/tests/unittestadapter/.data/simple_django/polls/apps.py b/python_files/tests/unittestadapter/.data/simple_django/polls/apps.py new file mode 100644 index 000000000000..e31968ce16c0 --- /dev/null +++ b/python_files/tests/unittestadapter/.data/simple_django/polls/apps.py @@ -0,0 +1,13 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +from django.apps import AppConfig +from django.utils.functional import cached_property + + +class PollsConfig(AppConfig): + @cached_property + def default_auto_field(self): + return "django.db.models.BigAutoField" + + name = "polls" diff --git a/python_files/tests/unittestadapter/.data/simple_django/polls/migrations/0001_initial.py b/python_files/tests/unittestadapter/.data/simple_django/polls/migrations/0001_initial.py new file mode 100644 index 000000000000..e33d24a3f704 --- /dev/null +++ b/python_files/tests/unittestadapter/.data/simple_django/polls/migrations/0001_initial.py @@ -0,0 +1,52 @@ +# Generated by Django 5.0.8 on 2024-08-09 20:04 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [] + + operations = [ + migrations.CreateModel( + name="Question", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("question_text", models.CharField(max_length=200, default="")), + ("pub_date", models.DateTimeField(verbose_name="date published", auto_now_add=True)), + ], + ), + migrations.CreateModel( + name="Choice", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("choice_text", models.CharField(max_length=200)), + ("votes", models.IntegerField(default=0)), + ( + "question", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, to="polls.question" + ), + ), + ], + ), + ] diff --git a/pythonFiles/tests/unittestadapter/.data/utils_nested_cases/folder/__init__.py b/python_files/tests/unittestadapter/.data/simple_django/polls/migrations/__init__.py similarity index 100% rename from pythonFiles/tests/unittestadapter/.data/utils_nested_cases/folder/__init__.py rename to python_files/tests/unittestadapter/.data/simple_django/polls/migrations/__init__.py diff --git a/python_files/tests/unittestadapter/.data/simple_django/polls/models.py b/python_files/tests/unittestadapter/.data/simple_django/polls/models.py new file mode 100644 index 000000000000..260a3da60f99 --- /dev/null +++ b/python_files/tests/unittestadapter/.data/simple_django/polls/models.py @@ -0,0 +1,25 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +from django.db import models +from django.utils import timezone +import datetime + + +class Question(models.Model): + question_text = models.CharField(max_length=200) + pub_date = models.DateTimeField("date published") + def __str__(self): + return self.question_text + def was_published_recently(self): + if self.pub_date > timezone.now(): + return False + return self.pub_date >= timezone.now() - datetime.timedelta(days=1) + + +class Choice(models.Model): + question = models.ForeignKey(Question, on_delete=models.CASCADE) + choice_text = models.CharField(max_length=200) + votes = models.IntegerField() + def __str__(self): + return self.choice_text diff --git a/python_files/tests/unittestadapter/.data/simple_django/polls/tests.py b/python_files/tests/unittestadapter/.data/simple_django/polls/tests.py new file mode 100644 index 000000000000..243262f195a8 --- /dev/null +++ b/python_files/tests/unittestadapter/.data/simple_django/polls/tests.py @@ -0,0 +1,38 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +from django.utils import timezone +from django.test import TestCase +from .models import Question +import datetime + +class QuestionModelTests(TestCase): + def test_was_published_recently_with_future_question(self): + """ + was_published_recently() returns False for questions whose pub_date + is in the future. + """ + time = timezone.now() + datetime.timedelta(days=30) + future_question: Question = Question.objects.create(pub_date=time) + self.assertIs(future_question.was_published_recently(), False) + + def test_was_published_recently_with_future_question_2(self): + """ + was_published_recently() returns False for questions whose pub_date + is in the future. + """ + time = timezone.now() + datetime.timedelta(days=30) + future_question = Question.objects.create(pub_date=time) + self.assertIs(future_question.was_published_recently(), True) + + def test_question_creation_and_retrieval(self): + """ + Test that a Question can be created and retrieved from the database. + """ + time = timezone.now() + question = Question.objects.create(pub_date=time, question_text="What's new?") + retrieved_question = Question.objects.get(question_text=question.question_text) + self.assertEqual(question, retrieved_question) + self.assertEqual(retrieved_question.question_text, "What's new?") + self.assertEqual(retrieved_question.pub_date, time) + diff --git a/python_files/tests/unittestadapter/.data/simple_django/polls/urls.py b/python_files/tests/unittestadapter/.data/simple_django/polls/urls.py new file mode 100644 index 000000000000..5756c7daa847 --- /dev/null +++ b/python_files/tests/unittestadapter/.data/simple_django/polls/urls.py @@ -0,0 +1,11 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +from django.urls import path + +from . import views + +urlpatterns = [ + # ex: /polls/ + path("", views.index, name="index"), +] diff --git a/python_files/tests/unittestadapter/.data/simple_django/polls/views.py b/python_files/tests/unittestadapter/.data/simple_django/polls/views.py new file mode 100644 index 000000000000..cccb6b3b0685 --- /dev/null +++ b/python_files/tests/unittestadapter/.data/simple_django/polls/views.py @@ -0,0 +1,7 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. +from django.http import HttpResponse +from .models import Question # noqa: F401 + +def index(request): + return HttpResponse("Hello, world. You're at the polls index.") diff --git a/python_files/tests/unittestadapter/.data/test_doctest_patched.py b/python_files/tests/unittestadapter/.data/test_doctest_patched.py new file mode 100644 index 000000000000..3a719c7139ca --- /dev/null +++ b/python_files/tests/unittestadapter/.data/test_doctest_patched.py @@ -0,0 +1,50 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. +"""Test file with patched doctest integration that should work.""" + +import unittest +import doctest +import sys +import doctest_patched_module + + +# Patch DocTestCase to modify test IDs to be compatible with the extension +original_init = doctest.DocTestCase.__init__ + + +def patched_init(self, test, optionflags=0, setUp=None, tearDown=None, checker=None): + """Patch to modify doctest names to have proper hierarchy.""" + if hasattr(test, 'name'): + # Get module name + module_hierarchy = test.name.split('.') + module_name = module_hierarchy[0] if module_hierarchy else 'unknown' + + # Reconstruct with proper formatting to have enough components + # Format: module.file.class.function + if test.filename.endswith('.py'): + file_base = test.filename.split('/')[-1].replace('.py', '') + test_name = test.name.split('.')[-1] if '.' in test.name else test.name + # Create a properly formatted ID with enough components + test.name = f"{module_name}.{file_base}._DocTests.{test_name}" + + # Call original init + original_init(self, test, optionflags, setUp, tearDown, checker) + + +# Apply the patch +doctest.DocTestCase.__init__ = patched_init + + +def load_tests(loader, tests, ignore): + """ + Standard hook for unittest to load tests. + This uses patched doctest to create compatible test IDs. + """ + tests.addTests(doctest.DocTestSuite(doctest_patched_module)) + return tests + + +# Clean up the patch after loading +def tearDownModule(): + """Restore original DocTestCase.__init__""" + doctest.DocTestCase.__init__ = original_init diff --git a/python_files/tests/unittestadapter/.data/test_doctest_standard.py b/python_files/tests/unittestadapter/.data/test_doctest_standard.py new file mode 100644 index 000000000000..f5dba1209b98 --- /dev/null +++ b/python_files/tests/unittestadapter/.data/test_doctest_standard.py @@ -0,0 +1,16 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. +"""Test file with standard doctest integration that should be blocked.""" + +import unittest +import doctest +import doctest_standard + + +def load_tests(loader, tests, ignore): + """ + Standard hook for unittest to load tests. + This uses standard doctest without any patching. + """ + tests.addTests(doctest.DocTestSuite(doctest_standard)) + return tests diff --git a/pythonFiles/tests/unittestadapter/.data/test_fail_simple.py b/python_files/tests/unittestadapter/.data/test_fail_simple.py similarity index 100% rename from pythonFiles/tests/unittestadapter/.data/test_fail_simple.py rename to python_files/tests/unittestadapter/.data/test_fail_simple.py diff --git a/pythonFiles/tests/unittestadapter/__init__.py b/python_files/tests/unittestadapter/.data/test_scenarios/tests/__init__.py similarity index 100% rename from pythonFiles/tests/unittestadapter/__init__.py rename to python_files/tests/unittestadapter/.data/test_scenarios/tests/__init__.py diff --git a/python_files/tests/unittestadapter/.data/test_scenarios/tests/test_scene.py b/python_files/tests/unittestadapter/.data/test_scenarios/tests/test_scene.py new file mode 100644 index 000000000000..35c1c7002319 --- /dev/null +++ b/python_files/tests/unittestadapter/.data/test_scenarios/tests/test_scene.py @@ -0,0 +1,39 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +import unittest + +from testscenarios import TestWithScenarios, generate_scenarios + + +def load_tests(loader, standard_tests, pattern): # noqa: ARG001 + # Pre-expand ``TestWithScenarios`` scenarios at load time so individual + # scenario-multiplied test IDs (e.g. ``test_operations(add)``) can be + # resolved by ``unittest.TestLoader.loadTestsFromName``. Without this, + # ``TestWithScenarios`` only multiplies scenarios at ``run()`` time and + # loading a specific scenario by name raises ``AttributeError``. + result = unittest.TestSuite() + result.addTests(generate_scenarios(standard_tests)) + return result + + +class TestMathOperations(TestWithScenarios): + scenarios = [ + ('add', {'test_id': 'test_add', 'a': 5, 'b': 3, 'expected': 8}), + ('subtract', {'test_id': 'test_subtract', 'a': 5, 'b': 3, 'expected': 2}), + ('multiply', {'test_id': 'test_multiply', 'a': 5, 'b': 3, 'expected': 15}), + ] + a: int = 0 + b: int = 0 + expected: int = 0 + test_id: str = "" + + def test_operations(self): + result = None + if self.test_id == 'test_add': + result = self.a + self.b + elif self.test_id == 'test_subtract': + result = self.a - self.b + elif self.test_id == 'test_multiply': + result = self.a * self.b + self.assertEqual(result, self.expected) diff --git a/pythonFiles/tests/unittestadapter/.data/test_subtest.py b/python_files/tests/unittestadapter/.data/test_subtest.py similarity index 100% rename from pythonFiles/tests/unittestadapter/.data/test_subtest.py rename to python_files/tests/unittestadapter/.data/test_subtest.py diff --git a/pythonFiles/tests/unittestadapter/.data/test_two_classes.py b/python_files/tests/unittestadapter/.data/test_two_classes.py similarity index 100% rename from pythonFiles/tests/unittestadapter/.data/test_two_classes.py rename to python_files/tests/unittestadapter/.data/test_two_classes.py diff --git a/pythonFiles/tests/unittestadapter/.data/two_patterns/pattern_a_test.py b/python_files/tests/unittestadapter/.data/two_patterns/pattern_a_test.py similarity index 95% rename from pythonFiles/tests/unittestadapter/.data/two_patterns/pattern_a_test.py rename to python_files/tests/unittestadapter/.data/two_patterns/pattern_a_test.py index 4f3f77e1056e..52641360b526 100644 --- a/pythonFiles/tests/unittestadapter/.data/two_patterns/pattern_a_test.py +++ b/python_files/tests/unittestadapter/.data/two_patterns/pattern_a_test.py @@ -7,7 +7,6 @@ # and the two tests with their outcome as "success". - class DiscoveryA(unittest.TestCase): """Test class for the two file pattern test. It is pattern *test.py @@ -19,4 +18,4 @@ def test_one_a(self) -> None: self.assertGreater(2, 1) def test_two_a(self) -> None: - self.assertNotEqual(2, 1) \ No newline at end of file + self.assertNotEqual(2, 1) diff --git a/pythonFiles/tests/unittestadapter/.data/two_patterns/test_pattern_b.py b/python_files/tests/unittestadapter/.data/two_patterns/test_pattern_b.py similarity index 93% rename from pythonFiles/tests/unittestadapter/.data/two_patterns/test_pattern_b.py rename to python_files/tests/unittestadapter/.data/two_patterns/test_pattern_b.py index a912699383ca..06b6a818537d 100644 --- a/pythonFiles/tests/unittestadapter/.data/two_patterns/test_pattern_b.py +++ b/python_files/tests/unittestadapter/.data/two_patterns/test_pattern_b.py @@ -6,10 +6,10 @@ # The test_ids_multiple_runs function should return a dictionary with a "success" status, # and the two tests with their outcome as "success". -class DiscoveryB(unittest.TestCase): +class DiscoveryB(unittest.TestCase): def test_one_b(self) -> None: self.assertGreater(2, 1) def test_two_b(self) -> None: - self.assertNotEqual(2, 1) \ No newline at end of file + self.assertNotEqual(2, 1) diff --git a/pythonFiles/tests/unittestadapter/.data/unittest_folder/test_add.py b/python_files/tests/unittestadapter/.data/unittest_folder/test_add.py similarity index 81% rename from pythonFiles/tests/unittestadapter/.data/unittest_folder/test_add.py rename to python_files/tests/unittestadapter/.data/unittest_folder/test_add.py index 2e616077ec40..f562474b596a 100644 --- a/pythonFiles/tests/unittestadapter/.data/unittest_folder/test_add.py +++ b/python_files/tests/unittestadapter/.data/unittest_folder/test_add.py @@ -6,17 +6,16 @@ # files in the same folder. The cwd is set to the parent folder. This should return # a dictionary with a "success" status and the two tests with their outcome as "success". + def add(a, b): return a + b class TestAddFunction(unittest.TestCase): - - def test_add_positive_numbers(self): + def test_add_positive_numbers(self): result = add(2, 3) self.assertEqual(result, 5) - - def test_add_negative_numbers(self): + def test_add_negative_numbers(self): result = add(-2, -3) - self.assertEqual(result, -5) \ No newline at end of file + self.assertEqual(result, -5) diff --git a/pythonFiles/tests/unittestadapter/.data/unittest_folder/test_subtract.py b/python_files/tests/unittestadapter/.data/unittest_folder/test_subtract.py similarity index 94% rename from pythonFiles/tests/unittestadapter/.data/unittest_folder/test_subtract.py rename to python_files/tests/unittestadapter/.data/unittest_folder/test_subtract.py index 4028e25825d1..8ac3988a3251 100644 --- a/pythonFiles/tests/unittestadapter/.data/unittest_folder/test_subtract.py +++ b/python_files/tests/unittestadapter/.data/unittest_folder/test_subtract.py @@ -6,6 +6,7 @@ # files in the same folder. The cwd is set to the parent folder. This should return # a dictionary with a "success" status and the two tests with their outcome as "success". + def subtract(a, b): return a - b @@ -15,7 +16,6 @@ def test_subtract_positive_numbers(self): result = subtract(5, 3) self.assertEqual(result, 2) - def test_subtract_negative_numbers(self): result = subtract(-2, -3) - self.assertEqual(result, 1) \ No newline at end of file + self.assertEqual(result, 1) diff --git a/python_files/tests/unittestadapter/.data/unittest_skip/unittest_skip_file.py b/python_files/tests/unittestadapter/.data/unittest_skip/unittest_skip_file.py new file mode 100644 index 000000000000..927a56bc920b --- /dev/null +++ b/python_files/tests/unittestadapter/.data/unittest_skip/unittest_skip_file.py @@ -0,0 +1,10 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +from unittest import SkipTest + +raise SkipTest("This is unittest.SkipTest calling") + + +def test_example(): + assert 1 == 1 diff --git a/python_files/tests/unittestadapter/.data/unittest_skip/unittest_skip_function.py b/python_files/tests/unittestadapter/.data/unittest_skip/unittest_skip_function.py new file mode 100644 index 000000000000..59e66e9a1d40 --- /dev/null +++ b/python_files/tests/unittestadapter/.data/unittest_skip/unittest_skip_function.py @@ -0,0 +1,18 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +import unittest + + +def add(x, y): + return x + y + + +class SimpleTest(unittest.TestCase): + @unittest.skip("demonstrating skipping") + def testadd1(self): + self.assertEquals(add(4, 5), 9) + + +if __name__ == "__main__": + unittest.main() diff --git a/pythonFiles/tests/testing_tools/adapter/.data/NormCase/tests/A/b/C/__init__.py b/python_files/tests/unittestadapter/.data/utils_complex_tree/__init__.py similarity index 100% rename from pythonFiles/tests/testing_tools/adapter/.data/NormCase/tests/A/b/C/__init__.py rename to python_files/tests/unittestadapter/.data/utils_complex_tree/__init__.py diff --git a/pythonFiles/tests/testing_tools/adapter/.data/NormCase/tests/A/b/__init__.py b/python_files/tests/unittestadapter/.data/utils_complex_tree/test_outer_folder/__init__.py similarity index 100% rename from pythonFiles/tests/testing_tools/adapter/.data/NormCase/tests/A/b/__init__.py rename to python_files/tests/unittestadapter/.data/utils_complex_tree/test_outer_folder/__init__.py diff --git a/pythonFiles/tests/testing_tools/adapter/.data/NormCase/tests/__init__.py b/python_files/tests/unittestadapter/.data/utils_complex_tree/test_outer_folder/test_inner_folder/__init__.py similarity index 100% rename from pythonFiles/tests/testing_tools/adapter/.data/NormCase/tests/__init__.py rename to python_files/tests/unittestadapter/.data/utils_complex_tree/test_outer_folder/test_inner_folder/__init__.py diff --git a/python_files/tests/unittestadapter/.data/utils_complex_tree/test_outer_folder/test_inner_folder/test_utils_complex_tree.py b/python_files/tests/unittestadapter/.data/utils_complex_tree/test_outer_folder/test_inner_folder/test_utils_complex_tree.py new file mode 100644 index 000000000000..8f57fb880ff1 --- /dev/null +++ b/python_files/tests/unittestadapter/.data/utils_complex_tree/test_outer_folder/test_inner_folder/test_utils_complex_tree.py @@ -0,0 +1,8 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. +import unittest + + +class TreeOne(unittest.TestCase): + def test_one(self): + assert True diff --git a/pythonFiles/tests/unittestadapter/.data/utils_decorated_tree.py b/python_files/tests/unittestadapter/.data/utils_decorated_tree.py similarity index 100% rename from pythonFiles/tests/unittestadapter/.data/utils_decorated_tree.py rename to python_files/tests/unittestadapter/.data/utils_decorated_tree.py diff --git a/pythonFiles/tests/unittestadapter/.data/utils_nested_cases/file_one.py b/python_files/tests/unittestadapter/.data/utils_nested_cases/file_one.py similarity index 100% rename from pythonFiles/tests/unittestadapter/.data/utils_nested_cases/file_one.py rename to python_files/tests/unittestadapter/.data/utils_nested_cases/file_one.py diff --git a/pythonFiles/unittestadapter/__init__.py b/python_files/tests/unittestadapter/.data/utils_nested_cases/folder/__init__.py similarity index 100% rename from pythonFiles/unittestadapter/__init__.py rename to python_files/tests/unittestadapter/.data/utils_nested_cases/folder/__init__.py diff --git a/pythonFiles/tests/unittestadapter/.data/utils_nested_cases/folder/file_two.py b/python_files/tests/unittestadapter/.data/utils_nested_cases/folder/file_two.py similarity index 100% rename from pythonFiles/tests/unittestadapter/.data/utils_nested_cases/folder/file_two.py rename to python_files/tests/unittestadapter/.data/utils_nested_cases/folder/file_two.py diff --git a/pythonFiles/tests/unittestadapter/.data/utils_simple_cases.py b/python_files/tests/unittestadapter/.data/utils_simple_cases.py similarity index 100% rename from pythonFiles/tests/unittestadapter/.data/utils_simple_cases.py rename to python_files/tests/unittestadapter/.data/utils_simple_cases.py diff --git a/pythonFiles/tests/unittestadapter/.data/utils_simple_tree.py b/python_files/tests/unittestadapter/.data/utils_simple_tree.py similarity index 100% rename from pythonFiles/tests/unittestadapter/.data/utils_simple_tree.py rename to python_files/tests/unittestadapter/.data/utils_simple_tree.py diff --git a/python_files/tests/unittestadapter/__init__.py b/python_files/tests/unittestadapter/__init__.py new file mode 100644 index 000000000000..5b7f7a925cc0 --- /dev/null +++ b/python_files/tests/unittestadapter/__init__.py @@ -0,0 +1,2 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. diff --git a/python_files/tests/unittestadapter/conftest.py b/python_files/tests/unittestadapter/conftest.py new file mode 100644 index 000000000000..5b7f7a925cc0 --- /dev/null +++ b/python_files/tests/unittestadapter/conftest.py @@ -0,0 +1,2 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. diff --git a/python_files/tests/unittestadapter/django_test_execution_script.py b/python_files/tests/unittestadapter/django_test_execution_script.py new file mode 100644 index 000000000000..21dd945224ea --- /dev/null +++ b/python_files/tests/unittestadapter/django_test_execution_script.py @@ -0,0 +1,17 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +import os +import pathlib +import sys + +sys.path.append(os.fspath(pathlib.Path(__file__).parent.parent)) + +from unittestadapter.django_handler import django_execution_runner + +if __name__ == "__main__": + args = sys.argv[1:] + manage_py_path = args[0] + test_ids = args[1:] + # currently doesn't support additional args past test_ids. + django_execution_runner(manage_py_path, test_ids, []) diff --git a/python_files/tests/unittestadapter/expected_discovery_test_output.py b/python_files/tests/unittestadapter/expected_discovery_test_output.py new file mode 100644 index 000000000000..0901f21bfbc2 --- /dev/null +++ b/python_files/tests/unittestadapter/expected_discovery_test_output.py @@ -0,0 +1,171 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +import os +import pathlib + +from unittestadapter.pvsc_utils import TestNodeTypeEnum + +TEST_DATA_PATH = pathlib.Path(__file__).parent / ".data" + + +def find_class_line_number(class_name: str, test_file_path) -> str: + """Function which finds the correct line number for a class definition. + + Args: + class_name: The name of the class to find the line number for. + test_file_path: The path to the test file where the class is located. + """ + # Look for the class definition line + with pathlib.Path(test_file_path).open() as f: + for i, line in enumerate(f): + # Match "class ClassName" or "class ClassName(" or "class ClassName:" + if line.strip().startswith(f"class {class_name}") or line.strip().startswith( + f"class {class_name}(" + ): + return str(i + 1) + error_str: str = f"Class {class_name!r} not found on any line in {test_file_path}" + raise ValueError(error_str) + + +skip_unittest_folder_discovery_output = { + "path": os.fspath(TEST_DATA_PATH / "unittest_skip"), + "name": "unittest_skip", + "type_": TestNodeTypeEnum.folder, + "children": [ + { + "path": os.fspath(TEST_DATA_PATH / "unittest_skip" / "unittest_skip_file.py"), + "name": "unittest_skip_file.py", + "type_": TestNodeTypeEnum.file, + "children": [], + "id_": os.fspath(TEST_DATA_PATH / "unittest_skip" / "unittest_skip_file.py"), + }, + { + "path": os.fspath(TEST_DATA_PATH / "unittest_skip" / "unittest_skip_function.py"), + "name": "unittest_skip_function.py", + "type_": TestNodeTypeEnum.file, + "children": [ + { + "path": os.fspath( + TEST_DATA_PATH / "unittest_skip" / "unittest_skip_function.py" + ), + "name": "SimpleTest", + "type_": TestNodeTypeEnum.class_, + "children": [ + { + "name": "testadd1", + "path": os.fspath( + TEST_DATA_PATH / "unittest_skip" / "unittest_skip_function.py" + ), + "lineno": "13", + "type_": TestNodeTypeEnum.test, + "id_": os.fspath( + TEST_DATA_PATH / "unittest_skip" / "unittest_skip_function.py" + ) + + "\\SimpleTest\\testadd1", + "runID": "unittest_skip_function.SimpleTest.testadd1", + } + ], + "id_": os.fspath(TEST_DATA_PATH / "unittest_skip" / "unittest_skip_function.py") + + "\\SimpleTest", + "lineno": find_class_line_number( + "SimpleTest", + TEST_DATA_PATH / "unittest_skip" / "unittest_skip_function.py", + ), + } + ], + "id_": os.fspath(TEST_DATA_PATH / "unittest_skip" / "unittest_skip_function.py"), + }, + ], + "id_": os.fspath(TEST_DATA_PATH / "unittest_skip"), +} + +complex_tree_file_path = os.fsdecode( + pathlib.PurePath( + TEST_DATA_PATH, + "utils_complex_tree", + "test_outer_folder", + "test_inner_folder", + "test_utils_complex_tree.py", + ) +) +complex_tree_expected_output = { + "name": "utils_complex_tree", + "type_": TestNodeTypeEnum.folder, + "path": os.fsdecode(pathlib.PurePath(TEST_DATA_PATH, "utils_complex_tree")), + "children": [ + { + "name": "test_outer_folder", + "type_": TestNodeTypeEnum.folder, + "path": os.fsdecode( + pathlib.PurePath(TEST_DATA_PATH, "utils_complex_tree", "test_outer_folder") + ), + "children": [ + { + "name": "test_inner_folder", + "type_": TestNodeTypeEnum.folder, + "path": os.fsdecode( + pathlib.PurePath( + TEST_DATA_PATH, + "utils_complex_tree", + "test_outer_folder", + "test_inner_folder", + ) + ), + "children": [ + { + "name": "test_utils_complex_tree.py", + "type_": TestNodeTypeEnum.file, + "path": complex_tree_file_path, + "children": [ + { + "name": "TreeOne", + "type_": TestNodeTypeEnum.class_, + "path": complex_tree_file_path, + "children": [ + { + "name": "test_one", + "type_": TestNodeTypeEnum.test, + "path": complex_tree_file_path, + "lineno": "7", + "id_": complex_tree_file_path + + "\\" + + "TreeOne" + + "\\" + + "test_one", + "runID": "utils_complex_tree.test_outer_folder.test_inner_folder.test_utils_complex_tree.TreeOne.test_one", + }, + ], + "id_": complex_tree_file_path + "\\" + "TreeOne", + "lineno": find_class_line_number( + "TreeOne", + pathlib.PurePath( + TEST_DATA_PATH, + "utils_complex_tree", + "test_outer_folder", + "test_inner_folder", + "test_utils_complex_tree.py", + ), + ), + } + ], + "id_": complex_tree_file_path, + } + ], + "id_": os.fsdecode( + pathlib.PurePath( + TEST_DATA_PATH, + "utils_complex_tree", + "test_outer_folder", + "test_inner_folder", + ) + ), + }, + ], + "id_": os.fsdecode( + pathlib.PurePath(TEST_DATA_PATH, "utils_complex_tree", "test_outer_folder") + ), + } + ], + "id_": os.fsdecode(pathlib.PurePath(TEST_DATA_PATH, "utils_complex_tree")), +} diff --git a/python_files/tests/unittestadapter/test_coverage.py b/python_files/tests/unittestadapter/test_coverage.py new file mode 100644 index 000000000000..76fdfec43376 --- /dev/null +++ b/python_files/tests/unittestadapter/test_coverage.py @@ -0,0 +1,106 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +import os +import pathlib +import sys + +import coverage +import pytest +from packaging.version import Version + +sys.path.append(os.fspath(pathlib.Path(__file__).parent)) + +python_files_path = pathlib.Path(__file__).parent.parent.parent +sys.path.insert(0, os.fspath(python_files_path)) +sys.path.insert(0, os.fspath(python_files_path / "lib" / "python")) + +from tests.pytestadapter import helpers # noqa: E402 + +TEST_DATA_PATH = pathlib.Path(__file__).parent / ".data" + + +def test_basic_coverage(): + """This test runs on a simple django project with three tests, two of which pass and one that fails.""" + coverage_ex_folder: pathlib.Path = TEST_DATA_PATH / "coverage_ex" + execution_script: pathlib.Path = python_files_path / "unittestadapter" / "execution.py" + test_ids = [ + "test_reverse.TestReverseFunctions.test_reverse_sentence", + "test_reverse.TestReverseFunctions.test_reverse_sentence_error", + "test_reverse.TestReverseFunctions.test_reverse_string", + ] + argv = [os.fsdecode(execution_script), "--udiscovery", "-vv", "-s", ".", "-p", "*test*.py"] + argv = argv + test_ids + + actual = helpers.runner_with_cwd_env( + argv, + coverage_ex_folder, + {"COVERAGE_ENABLED": os.fspath(coverage_ex_folder), "_TEST_VAR_UNITTEST": "True"}, + ) + + assert actual + cov = actual[-1] + assert cov + results = cov["result"] + assert results + assert len(results) == 3 + focal_function_coverage = results.get(os.fspath(TEST_DATA_PATH / "coverage_ex" / "reverse.py")) + assert focal_function_coverage + assert focal_function_coverage.get("lines_covered") is not None + assert focal_function_coverage.get("lines_missed") is not None + assert set(focal_function_coverage.get("lines_covered")) == {4, 5, 7, 9, 10, 11, 12, 13, 14} + assert set(focal_function_coverage.get("lines_missed")) == {6} + coverage_version = Version(coverage.__version__) + # only include check for branches if the version is >= 7.7.0 + if coverage_version >= Version("7.7.0"): + assert focal_function_coverage.get("executed_branches") == 3 + assert focal_function_coverage.get("total_branches") == 4 + + +@pytest.mark.parametrize("manage_py_file", ["manage.py", "old_manage.py"]) +@pytest.mark.timeout(30) +def test_basic_django_coverage(manage_py_file): + """This test validates that the coverage is correctly calculated for a Django project.""" + data_path: pathlib.Path = TEST_DATA_PATH / "simple_django" + manage_py_path: str = os.fsdecode(data_path / manage_py_file) + execution_script: pathlib.Path = python_files_path / "unittestadapter" / "execution.py" + + test_ids = [ + "polls.tests.QuestionModelTests.test_was_published_recently_with_future_question", + "polls.tests.QuestionModelTests.test_was_published_recently_with_future_question_2", + "polls.tests.QuestionModelTests.test_question_creation_and_retrieval", + ] + + script_str = os.fsdecode(execution_script) + actual = helpers.runner_with_cwd_env( + [script_str, "--udiscovery", "-p", "*test*.py", *test_ids], + data_path, + { + "MANAGE_PY_PATH": manage_py_path, + "_TEST_VAR_UNITTEST": "True", + "COVERAGE_ENABLED": os.fspath(data_path), + }, + ) + + assert actual + cov = actual[-1] + assert cov + results = cov["result"] + assert results + assert len(results) == 16 + polls_views_coverage = results.get(str(data_path / "polls" / "views.py")) + assert polls_views_coverage + assert polls_views_coverage.get("lines_covered") is not None + assert polls_views_coverage.get("lines_missed") is not None + assert set(polls_views_coverage.get("lines_covered")) == {3, 4, 6} + assert set(polls_views_coverage.get("lines_missed")) == {7} + + model_cov = results.get(str(data_path / "polls" / "models.py")) + coverage_version = Version(coverage.__version__) + # only include check for branches if the version is >= 7.7.0 + if coverage_version >= Version("7.7.0"): + assert model_cov.get("executed_branches") == 1 + assert model_cov.get("total_branches") == 2 diff --git a/python_files/tests/unittestadapter/test_discovery.py b/python_files/tests/unittestadapter/test_discovery.py new file mode 100644 index 000000000000..ab028ef176c3 --- /dev/null +++ b/python_files/tests/unittestadapter/test_discovery.py @@ -0,0 +1,447 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +import os +import pathlib +import sys +from typing import Any, Dict, List + +import pytest + +from unittestadapter.discovery import discover_tests +from unittestadapter.pvsc_utils import TestNodeTypeEnum, parse_unittest_args + +script_dir = pathlib.Path(__file__).parent.parent +sys.path.append(os.fspath(script_dir)) + +from tests.pytestadapter import helpers # noqa: E402 +from tests.tree_comparison_helper import is_same_tree # noqa: E402 + +from . import expected_discovery_test_output # noqa: E402 + +TEST_DATA_PATH = pathlib.Path(__file__).parent / ".data" + + +@pytest.mark.parametrize( + ("args", "expected"), + [ + ( + ["-s", "something", "-p", "other*", "-t", "else"], + ("something", "other*", "else", 1, None, None), + ), + ( + [ + "--start-directory", + "foo", + "--pattern", + "bar*", + "--top-level-directory", + "baz", + ], + ("foo", "bar*", "baz", 1, None, None), + ), + ( + ["--foo", "something"], + (".", "test*.py", None, 1, None, None), + ), + ( + ["--foo", "something", "-v"], + (".", "test*.py", None, 2, None, None), + ), + ( + ["--foo", "something", "-f"], + (".", "test*.py", None, 1, True, None), + ), + ( + ["--foo", "something", "--verbose", "-f"], + (".", "test*.py", None, 2, True, None), + ), + ( + ["--foo", "something", "-q", "--failfast"], + (".", "test*.py", None, 0, True, None), + ), + ( + ["--foo", "something", "--quiet"], + (".", "test*.py", None, 0, None, None), + ), + ( + ["--foo", "something", "--quiet", "--locals"], + (".", "test*.py", None, 0, None, True), + ), + ], +) +def test_parse_unittest_args(args: List[str], expected: List[str]) -> None: + """The parse_unittest_args function should return values for the start_dir, pattern, and top_level_dir arguments when passed as command-line options, and ignore unrecognized arguments.""" + actual = parse_unittest_args(args) + + assert actual == expected + + +def test_simple_discovery() -> None: + """The discover_tests function should return a dictionary with a "success" status, no errors, and a test tree if unittest discovery was performed successfully.""" + start_dir = os.fsdecode(TEST_DATA_PATH) + pattern = "discovery_simple*" + file_path = os.fsdecode(pathlib.PurePath(TEST_DATA_PATH / "discovery_simple.py")) + + expected = { + "path": start_dir, + "type_": TestNodeTypeEnum.folder, + "name": ".data", + "children": [ + { + "name": "discovery_simple.py", + "type_": TestNodeTypeEnum.file, + "path": file_path, + "children": [ + { + "name": "DiscoverySimple", + "path": file_path, + "type_": TestNodeTypeEnum.class_, + "children": [ + { + "name": "test_one", + "path": file_path, + "type_": TestNodeTypeEnum.test, + "lineno": "14", + "id_": file_path + "\\" + "DiscoverySimple" + "\\" + "test_one", + }, + { + "name": "test_two", + "path": file_path, + "type_": TestNodeTypeEnum.test, + "lineno": "17", + "id_": file_path + "\\" + "DiscoverySimple" + "\\" + "test_two", + }, + ], + "id_": file_path + "\\" + "DiscoverySimple", + } + ], + "id_": file_path, + } + ], + "id_": start_dir, + } + + actual = discover_tests(start_dir, pattern, None) + + assert actual["status"] == "success" + assert is_same_tree(actual.get("tests"), expected, ["id_", "lineno", "name"]) + assert "error" not in actual + + +def test_simple_discovery_with_top_dir_calculated() -> None: + """The discover_tests function should return a dictionary with a "success" status, no errors, and a test tree if unittest discovery was performed successfully.""" + start_dir = "." + pattern = "discovery_simple*" + file_path = os.fsdecode(pathlib.PurePath(TEST_DATA_PATH / "discovery_simple.py")) + + expected = { + "path": os.fsdecode(pathlib.PurePath(TEST_DATA_PATH)), + "type_": TestNodeTypeEnum.folder, + "name": ".data", + "children": [ + { + "name": "discovery_simple.py", + "type_": TestNodeTypeEnum.file, + "path": file_path, + "children": [ + { + "name": "DiscoverySimple", + "path": file_path, + "type_": TestNodeTypeEnum.class_, + "children": [ + { + "name": "test_one", + "path": file_path, + "type_": TestNodeTypeEnum.test, + "lineno": "14", + "id_": file_path + "\\" + "DiscoverySimple" + "\\" + "test_one", + }, + { + "name": "test_two", + "path": file_path, + "type_": TestNodeTypeEnum.test, + "lineno": "17", + "id_": file_path + "\\" + "DiscoverySimple" + "\\" + "test_two", + }, + ], + "id_": file_path + "\\" + "DiscoverySimple", + } + ], + "id_": file_path, + } + ], + "id_": os.fsdecode(pathlib.PurePath(TEST_DATA_PATH)), + } + + # Define the CWD to be the root of the test data folder. + os.chdir(os.fsdecode(pathlib.PurePath(TEST_DATA_PATH))) + actual = discover_tests(start_dir, pattern, None) + + assert actual["status"] == "success" + assert is_same_tree(actual.get("tests"), expected, ["id_", "lineno", "name"]) + assert "error" not in actual + + +def test_empty_discovery() -> None: + """The discover_tests function should return a dictionary with a "success" status, no errors, and no test tree if unittest discovery was performed successfully but no tests were found.""" + start_dir = os.fsdecode(TEST_DATA_PATH) + pattern = "discovery_empty*" + + actual = discover_tests(start_dir, pattern, None) + + assert actual["status"] == "success" + assert "tests" in actual + assert "error" not in actual + + +def test_error_discovery() -> None: + """The discover_tests function should return a dictionary with an "error" status, the discovered tests, and a list of errors if unittest discovery failed at some point.""" + # Discover tests in .data/discovery_error/. + start_path = pathlib.PurePath(TEST_DATA_PATH / "discovery_error") + start_dir = os.fsdecode(start_path) + pattern = "file*" + + file_path = os.fsdecode(start_path / "file_two.py") + + expected = { + "path": start_dir, + "type_": TestNodeTypeEnum.folder, + "name": "discovery_error", + "children": [ + { + "name": "file_two.py", + "type_": TestNodeTypeEnum.file, + "path": file_path, + "children": [ + { + "name": "DiscoveryErrorTwo", + "path": file_path, + "type_": TestNodeTypeEnum.class_, + "children": [ + { + "name": "test_one", + "path": file_path, + "type_": TestNodeTypeEnum.test, + "lineno": "14", + "id_": file_path + "\\" + "DiscoveryErrorTwo" + "\\" + "test_one", + }, + { + "name": "test_two", + "path": file_path, + "type_": TestNodeTypeEnum.test, + "lineno": "17", + "id_": file_path + "\\" + "DiscoveryErrorTwo" + "\\" + "test_two", + }, + ], + "id_": file_path + "\\" + "DiscoveryErrorTwo", + } + ], + "id_": file_path, + } + ], + "id_": start_dir, + } + + actual = discover_tests(start_dir, pattern, None) + + assert actual["status"] == "error" + assert is_same_tree(expected, actual.get("tests"), ["id_", "lineno", "name"]) + assert len(actual.get("error", [])) == 1 + + +def test_unit_skip() -> None: + """The discover_tests function should return a dictionary with a "success" status, no errors, and test tree. + + if unittest discovery was performed and found a test in one file marked as skipped and another file marked as skipped. + """ + start_dir = os.fsdecode(TEST_DATA_PATH / "unittest_skip") + pattern = "unittest_*" + + actual = discover_tests(start_dir, pattern, None) + + assert actual["status"] == "success" + assert "tests" in actual + assert is_same_tree( + actual.get("tests"), + expected_discovery_test_output.skip_unittest_folder_discovery_output, + ["id_", "lineno", "name"], + ) + assert "error" not in actual + + +def test_complex_tree() -> None: + """This test specifically tests when different start_dir and top_level_dir are provided.""" + start_dir = os.fsdecode( + pathlib.PurePath( + TEST_DATA_PATH, + "utils_complex_tree", + "test_outer_folder", + "test_inner_folder", + ) + ) + pattern = "test_*.py" + top_level_dir = os.fsdecode(pathlib.PurePath(TEST_DATA_PATH, "utils_complex_tree")) + actual = discover_tests(start_dir, pattern, top_level_dir) + assert actual["status"] == "success" + assert "error" not in actual + assert is_same_tree( + actual.get("tests"), + expected_discovery_test_output.complex_tree_expected_output, + ["id_", "lineno", "name"], + ) + + +def test_simple_django_collect(): + test_data_path: pathlib.Path = pathlib.Path(__file__).parent / ".data" + python_files_path: pathlib.Path = pathlib.Path(__file__).parent.parent.parent + discovery_script_path: str = os.fsdecode(python_files_path / "unittestadapter" / "discovery.py") + data_path: pathlib.Path = test_data_path / "simple_django" + manage_py_path: str = os.fsdecode(pathlib.Path(data_path, "manage.py")) + + actual = helpers.runner_with_cwd_env( + [ + discovery_script_path, + "--udiscovery", + ], + data_path, + {"MANAGE_PY_PATH": manage_py_path}, + ) + + assert actual + actual_list: List[Dict[str, Any]] = actual + assert actual_list is not None + if actual_list is not None: + actual_item = actual_list.pop(0) + assert all(item in actual_item for item in ("status", "cwd")) + assert actual_item.get("status") == "success", ( + f"Status is not 'success', error is: {actual_item.get('error')}" + ) + assert actual_item.get("cwd") == os.fspath(data_path) + assert len(actual_item["tests"]["children"]) == 1 + assert actual_item["tests"]["children"][0]["children"][0]["id_"] == os.fsdecode( + pathlib.PurePath(test_data_path, "simple_django", "polls", "tests.py") + ) + assert ( + len(actual_item["tests"]["children"][0]["children"][0]["children"][0]["children"]) == 3 + ) + + +def test_project_root_path_with_cwd_override() -> None: + """Test unittest discovery with project_root_path parameter. + + This simulates project-based testing where the cwd in the payload should be + the project root (project_root_path) rather than the start_dir. + + When project_root_path is provided: + - The cwd in the response should match project_root_path + - The test tree root should still be built correctly based on top_level_dir + """ + # Use unittest_skip folder as our "project" directory + project_path = TEST_DATA_PATH / "unittest_skip" + start_dir = os.fsdecode(project_path) + pattern = "unittest_*" + + # Call discover_tests with project_root_path to simulate PROJECT_ROOT_PATH + actual = discover_tests(start_dir, pattern, None, project_root_path=start_dir) + + assert actual["status"] == "success" + # cwd in response should match the project_root_path (project root) + assert actual["cwd"] == os.fsdecode(project_path), ( + f"Expected cwd '{os.fsdecode(project_path)}', got '{actual['cwd']}'" + ) + assert "tests" in actual + # Verify the test tree structure matches expected output + assert is_same_tree( + actual.get("tests"), + expected_discovery_test_output.skip_unittest_folder_discovery_output, + ["id_", "lineno", "name"], + ) + assert "error" not in actual + + +def test_project_root_path_with_different_cwd_and_start_dir() -> None: + """Test unittest discovery where project_root_path differs from start_dir. + + This simulates the scenario where: + - start_dir points to a subfolder where tests are located + - project_root_path (PROJECT_ROOT_PATH) points to the project root + + The cwd in the response should be the project root, while discovery + still runs from the start_dir. + """ + # Use utils_complex_tree as our test case - discovery from a subfolder + project_path = TEST_DATA_PATH / "utils_complex_tree" + start_dir = os.fsdecode( + pathlib.PurePath( + TEST_DATA_PATH, + "utils_complex_tree", + "test_outer_folder", + "test_inner_folder", + ) + ) + pattern = "test_*.py" + top_level_dir = os.fsdecode(project_path) + + # Call discover_tests with project_root_path set to project root + actual = discover_tests(start_dir, pattern, top_level_dir, project_root_path=top_level_dir) + + assert actual["status"] == "success" + # cwd should be the project root (project_root_path), not the start_dir + assert actual["cwd"] == os.fsdecode(project_path), ( + f"Expected cwd '{os.fsdecode(project_path)}', got '{actual['cwd']}'" + ) + assert "error" not in actual + # Test tree should still be structured correctly with top_level_dir as root + assert is_same_tree( + actual.get("tests"), + expected_discovery_test_output.complex_tree_expected_output, + ["id_", "lineno", "name"], + ) + + +@pytest.mark.skipif( + sys.platform == "win32", + reason="Symlinks require elevated privileges on Windows", +) +def test_symlink_with_project_root_path() -> None: + """Test unittest discovery with both symlink and PROJECT_ROOT_PATH set. + + This tests the combination of: + 1. A symlinked test directory + 2. project_root_path (PROJECT_ROOT_PATH) set to the symlink path + + This simulates project-based testing where the project root is a symlink, + ensuring test IDs and paths are correctly resolved through the symlink. + """ + with helpers.create_symlink(TEST_DATA_PATH, "unittest_skip", "symlink_unittest") as ( + _source, + destination, + ): + assert destination.is_symlink() + + # Run discovery with: + # - start_dir pointing to the symlink destination + # - project_root_path set to the symlink destination (simulating PROJECT_ROOT_PATH) + start_dir = os.fsdecode(destination) + pattern = "unittest_*" + + actual = discover_tests(start_dir, pattern, None, project_root_path=start_dir) + + assert actual["status"] == "success", ( + f"Status is not 'success', error is: {actual.get('error')}" + ) + # cwd should be the symlink path (project_root_path) + assert actual["cwd"] == os.fsdecode(destination), ( + f"CWD does not match symlink path: expected {os.fsdecode(destination)}, got {actual['cwd']}" + ) + assert "tests" in actual + assert actual["tests"] is not None + # The test tree root should be named after the symlink directory + assert actual["tests"]["name"] == "symlink_unittest", ( + f"Expected root name 'symlink_unittest', got '{actual['tests']['name']}'" + ) + # The test tree root path should use the symlink path + assert actual["tests"]["path"] == os.fsdecode(destination), ( + f"Expected root path to be symlink, got '{actual['tests']['path']}'" + ) diff --git a/python_files/tests/unittestadapter/test_execution.py b/python_files/tests/unittestadapter/test_execution.py new file mode 100644 index 000000000000..cab03f0b5dc4 --- /dev/null +++ b/python_files/tests/unittestadapter/test_execution.py @@ -0,0 +1,474 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +import os +import pathlib +import sys +from typing import TYPE_CHECKING, Any, Dict, List, Optional +from unittest.mock import patch + +import pytest + +sys.path.append(os.fspath(pathlib.Path(__file__).parent)) + +python_files_path = pathlib.Path(__file__).parent.parent.parent +sys.path.insert(0, os.fspath(python_files_path)) +sys.path.insert(0, os.fspath(python_files_path / "lib" / "python")) + +from tests.pytestadapter import helpers # noqa: E402 +from unittestadapter.execution import run_tests # noqa: E402 + +if TYPE_CHECKING: + from unittestadapter.pvsc_utils import ExecutionPayloadDict + +TEST_DATA_PATH = pathlib.Path(__file__).parent / ".data" + + +def test_no_ids_run() -> None: + """This test runs on an empty array of test_ids, therefore it should return an empty dict for the result.""" + start_dir: str = os.fspath(TEST_DATA_PATH) + testids = [] + pattern = "discovery_simple*" + actual = run_tests(start_dir, testids, pattern, None, 1, None) + assert actual + assert all(item in actual for item in ("cwd", "status")) + assert actual["status"] == "success" + assert actual["cwd"] == os.fspath(TEST_DATA_PATH) + if actual["result"] is not None: + assert len(actual["result"]) == 0 + else: + raise AssertionError("actual['result'] is None") + + +@pytest.fixture +def mock_send_run_data(): + with patch("unittestadapter.execution.send_run_data") as mock: + yield mock + + +def test_single_ids_run(mock_send_run_data): + """This test runs on a single test_id, therefore it should return a dict with a single key-value pair for the result. + + This single test passes so the outcome should be 'success'. + """ + id_ = "discovery_simple.DiscoverySimple.test_one" + os.environ["TEST_RUN_PIPE"] = "fake" + actual: ExecutionPayloadDict = run_tests( + os.fspath(TEST_DATA_PATH), + [id_], + "discovery_simple*", + None, + 1, + None, + ) + + # Access the arguments + args, _ = mock_send_run_data.call_args + test_actual = args[0] # first argument is the result + + assert test_actual + actual_result: Optional[Dict[str, Dict[str, Optional[str]]]] = actual["result"] + if actual_result is None: + raise AssertionError("actual_result is None") + else: + if not isinstance(actual_result, Dict): + raise AssertionError("actual_result is not a Dict") + assert len(actual_result) == 1 + assert id_ in actual_result + id_result = actual_result[id_] + assert id_result is not None + assert "outcome" in id_result + assert id_result["outcome"] == "success" + + +def test_subtest_run(mock_send_run_data) -> None: # noqa: ARG001 + """This test runs on a the test_subtest which has a single method, test_even, that uses unittest subtest. + + The actual result of run should return a dict payload with 6 entry for the 6 subtests. + """ + id_ = "test_subtest.NumbersTest.test_even" + os.environ["TEST_RUN_PIPE"] = "fake" + actual = run_tests( + os.fspath(TEST_DATA_PATH), + [id_], + "test_subtest.py", + None, + 1, + None, + ) + subtests_ids = [ + "test_subtest.NumbersTest.test_even (i=0)", + "test_subtest.NumbersTest.test_even (i=1)", + "test_subtest.NumbersTest.test_even (i=2)", + "test_subtest.NumbersTest.test_even (i=3)", + "test_subtest.NumbersTest.test_even (i=4)", + "test_subtest.NumbersTest.test_even (i=5)", + ] + assert actual + assert all(item in actual for item in ("cwd", "status")) + assert actual["status"] == "success" + assert actual["cwd"] == os.fspath(TEST_DATA_PATH) + assert actual["result"] is not None + result = actual["result"] + assert len(result) == 6 + for id_ in subtests_ids: + assert id_ in result + + +@pytest.mark.parametrize( + ("test_ids", "pattern", "cwd", "expected_outcome"), + [ + ( + [ + "test_add.TestAddFunction.test_add_negative_numbers", + "test_add.TestAddFunction.test_add_positive_numbers", + ], + "test_add.py", + os.fspath(TEST_DATA_PATH / "unittest_folder"), + "success", + ), + ( + [ + "test_add.TestAddFunction.test_add_negative_numbers", + "test_add.TestAddFunction.test_add_positive_numbers", + "test_subtract.TestSubtractFunction.test_subtract_negative_numbers", + "test_subtract.TestSubtractFunction.test_subtract_positive_numbers", + ], + "test*", + os.fspath(TEST_DATA_PATH / "unittest_folder"), + "success", + ), + ( + [ + "pattern_a_test.DiscoveryA.test_one_a", + "pattern_a_test.DiscoveryA.test_two_a", + ], + "*test.py", + os.fspath(TEST_DATA_PATH / "two_patterns"), + "success", + ), + ( + [ + "test_pattern_b.DiscoveryB.test_one_b", + "test_pattern_b.DiscoveryB.test_two_b", + ], + "test_*", + os.fspath(TEST_DATA_PATH / "two_patterns"), + "success", + ), + ( + [ + "file_one.CaseTwoFileOne.test_one", + "file_one.CaseTwoFileOne.test_two", + "folder.file_two.CaseTwoFileTwo.test_one", + "folder.file_two.CaseTwoFileTwo.test_two", + ], + "*", + os.fspath(TEST_DATA_PATH / "utils_nested_cases"), + "success", + ), + ( + [ + "test_two_classes.ClassOne.test_one", + "test_two_classes.ClassTwo.test_two", + ], + "test_two_classes.py", + os.fspath(TEST_DATA_PATH), + "success", + ), + ( + [ + "test_scene.TestMathOperations.test_operations(add)", + "test_scene.TestMathOperations.test_operations(subtract)", + "test_scene.TestMathOperations.test_operations(multiply)", + ], + "*", + os.fspath(TEST_DATA_PATH / "test_scenarios" / "tests"), + "success", + ), + ], +) +def test_multiple_ids_run(mock_send_run_data, test_ids, pattern, cwd, expected_outcome) -> None: # noqa: ARG001 + """ + The following are all successful tests of different formats. + + # 1. Two tests with the `pattern` specified as a file + # 2. Two test files in the same folder called `unittest_folder` + # 3. A folder with two different test file patterns, this test gathers pattern `*test` + # 4. A folder with two different test file patterns, this test gathers pattern `test_*` + # 5. A nested structure where a test file is on the same level as a folder containing a test file + # 6. Test file with two test classes + + All tests should have the outcome of `success`. + """ + os.environ["TEST_RUN_PIPE"] = "fake" + actual = run_tests(cwd, test_ids, pattern, None, 1, None) + assert actual + assert all(item in actual for item in ("cwd", "status")) + assert actual["status"] == "success" + assert actual["cwd"] == cwd + assert actual["result"] is not None + result = actual["result"] + assert len(result) == len(test_ids) + for test_id in test_ids: + assert test_id in result + id_result = result[test_id] + assert id_result is not None + assert "outcome" in id_result + assert id_result["outcome"] == expected_outcome + assert True + + +def test_failed_tests(mock_send_run_data): # noqa: ARG001 + """This test runs on a single file `test_fail` with two tests that fail.""" + os.environ["TEST_RUN_PIPE"] = "fake" + test_ids = [ + "test_fail_simple.RunFailSimple.test_one_fail", + "test_fail_simple.RunFailSimple.test_two_fail", + ] + actual = run_tests( + os.fspath(TEST_DATA_PATH), + test_ids, + "test_fail_simple*", + None, + 1, + None, + ) + assert actual + assert all(item in actual for item in ("cwd", "status")) + assert actual["status"] == "success" + assert actual["cwd"] == os.fspath(TEST_DATA_PATH) + assert actual["result"] is not None + result = actual["result"] + assert len(result) == len(test_ids) + for test_id in test_ids: + assert test_id in result + id_result = result[test_id] + assert id_result is not None + assert "outcome" in id_result + assert id_result["outcome"] == "failure" + assert "message" in id_result + assert "traceback" in id_result + assert "2 not greater than 3" in str(id_result["message"]) or "1 == 1" in str( + id_result["traceback"] + ) + assert True + + +def test_unknown_id(mock_send_run_data): # noqa: ARG001 + """This test runs on a unknown test_id, therefore it should return an error as the outcome as it attempts to find the given test.""" + os.environ["TEST_RUN_PIPE"] = "fake" + test_ids = ["unknown_id"] + actual = run_tests( + os.fspath(TEST_DATA_PATH), + test_ids, + "test_fail_simple*", + None, + 1, + None, + ) + assert actual + assert all(item in actual for item in ("cwd", "status")) + assert actual["status"] == "success" + assert actual["cwd"] == os.fspath(TEST_DATA_PATH) + assert actual["result"] is not None + result = actual["result"] + assert len(result) == len(test_ids) + assert "unittest.loader._FailedTest.unknown_id" in result + id_result = result["unittest.loader._FailedTest.unknown_id"] + assert id_result is not None + assert "outcome" in id_result + assert id_result["outcome"] == "error" + assert "message" in id_result + assert "traceback" in id_result + + +def test_incorrect_path(): + """This test runs on a non existent path, therefore it should return an error as the outcome as it attempts to find the given folder.""" + test_ids = ["unknown_id"] + os.environ["TEST_RUN_PIPE"] = "fake" + + actual = run_tests( + os.fspath(TEST_DATA_PATH / "unknown_folder"), + test_ids, + "test_fail_simple*", + None, + 1, + None, + ) + assert actual + assert all(item in actual for item in ("cwd", "status", "error")) + assert actual["status"] == "error" + assert actual["cwd"] == os.fspath(TEST_DATA_PATH / "unknown_folder") + + +def test_basic_run_django(): + """This test runs on a simple django project with three tests, two of which pass and one that fails.""" + data_path: pathlib.Path = TEST_DATA_PATH / "simple_django" + manage_py_path: str = os.fsdecode(data_path / "manage.py") + execution_script: pathlib.Path = ( + pathlib.Path(__file__).parent / "django_test_execution_script.py" + ) + + test_ids = [ + "polls.tests.QuestionModelTests.test_was_published_recently_with_future_question", + "polls.tests.QuestionModelTests.test_was_published_recently_with_future_question_2", + "polls.tests.QuestionModelTests.test_question_creation_and_retrieval", + ] + script_str = os.fsdecode(execution_script) + actual = helpers.runner_with_cwd_env( + [script_str, manage_py_path, *test_ids], + data_path, + {"MANAGE_PY_PATH": manage_py_path}, + ) + assert actual + actual_list: List[Dict[str, Dict[str, Any]]] = actual + actual_result_dict = {} + assert len(actual_list) == 3 + for actual_item in actual_list: + assert all(item in actual_item for item in ("status", "cwd", "result")) + assert actual_item.get("cwd") == os.fspath(data_path) + actual_result_dict.update(actual_item["result"]) + for test_id in test_ids: + assert test_id in actual_result_dict + id_result = actual_result_dict[test_id] + assert id_result is not None + assert "outcome" in id_result + if ( + test_id + == "polls.tests.QuestionModelTests.test_was_published_recently_with_future_question_2" + ): + assert id_result["outcome"] == "failure" + else: + assert id_result["outcome"] == "success" + + +def test_project_root_path_with_cwd_override(mock_send_run_data) -> None: # noqa: ARG001 + """Test unittest execution with project_root_path parameter. + + This simulates project-based testing where the cwd in the payload should be + the project root (project_root_path) rather than the start_dir. + + When project_root_path is provided: + - The cwd in the response should match project_root_path + - Test execution should still work correctly with start_dir + """ + # Use unittest_folder as our "project" directory + project_path = TEST_DATA_PATH / "unittest_folder" + start_dir = os.fsdecode(project_path) + pattern = "test_add*" + test_ids = [ + "test_add.TestAddFunction.test_add_positive_numbers", + ] + + os.environ["TEST_RUN_PIPE"] = "fake" + + # Call run_tests with project_root_path to simulate PROJECT_ROOT_PATH + actual = run_tests( + start_dir, + test_ids, + pattern, + None, + 1, + None, + project_root_path=start_dir, + ) + + assert actual["status"] == "success" + # cwd in response should match the project_root_path (project root) + assert actual["cwd"] == os.fsdecode(project_path), ( + f"Expected cwd '{os.fsdecode(project_path)}', got '{actual['cwd']}'" + ) + assert actual["result"] is not None + assert test_ids[0] in actual["result"] + assert actual["result"][test_ids[0]]["outcome"] == "success" + + +def test_project_root_path_with_different_cwd_and_start_dir(mock_send_run_data) -> None: # noqa: ARG001 + """Test unittest execution where project_root_path differs from start_dir. + + This simulates the scenario where: + - start_dir points to a subfolder where tests are located + - project_root_path (PROJECT_ROOT_PATH) points to the project root + + The cwd in the response should be the project root, while execution + still runs from the start_dir. + """ + # Use utils_nested_cases as our test case + project_path = TEST_DATA_PATH / "utils_nested_cases" + start_dir = os.fsdecode(project_path) + pattern = "*" + test_ids = [ + "file_one.CaseTwoFileOne.test_one", + ] + + os.environ["TEST_RUN_PIPE"] = "fake" + + # Call run_tests with project_root_path set to project root + actual = run_tests( + start_dir, + test_ids, + pattern, + None, + 1, + None, + project_root_path=os.fsdecode(project_path), + ) + + assert actual["status"] == "success" + # cwd should be the project root (project_root_path) + assert actual["cwd"] == os.fsdecode(project_path), ( + f"Expected cwd '{os.fsdecode(project_path)}', got '{actual['cwd']}'" + ) + assert actual["result"] is not None + assert test_ids[0] in actual["result"] + + +@pytest.mark.skipif( + sys.platform == "win32", + reason="Symlinks require elevated privileges on Windows", +) +def test_symlink_with_project_root_path(mock_send_run_data) -> None: # noqa: ARG001 + """Test unittest execution with both symlink and project_root_path set. + + This tests the combination of: + 1. A symlinked test directory + 2. project_root_path (PROJECT_ROOT_PATH) set to the symlink path + + This simulates project-based testing where the project root is a symlink, + ensuring execution payloads correctly use the symlink path. + """ + with helpers.create_symlink(TEST_DATA_PATH, "unittest_folder", "symlink_unittest_exec") as ( + _source, + destination, + ): + assert destination.is_symlink() + + # Run execution with: + # - start_dir pointing to the symlink destination + # - project_root_path set to the symlink destination (simulating PROJECT_ROOT_PATH) + start_dir = os.fsdecode(destination) + pattern = "test_add*" + test_ids = [ + "test_add.TestAddFunction.test_add_positive_numbers", + ] + + os.environ["TEST_RUN_PIPE"] = "fake" + + actual = run_tests( + start_dir, + test_ids, + pattern, + None, + 1, + None, + project_root_path=start_dir, + ) + + assert actual["status"] == "success", ( + f"Status is not 'success', error is: {actual.get('error')}" + ) + # cwd should be the symlink path (project_root_path) + assert actual["cwd"] == os.fsdecode(destination), ( + f"CWD does not match symlink path: expected {os.fsdecode(destination)}, got {actual['cwd']}" + ) diff --git a/pythonFiles/tests/unittestadapter/test_utils.py b/python_files/tests/unittestadapter/test_utils.py similarity index 76% rename from pythonFiles/tests/unittestadapter/test_utils.py rename to python_files/tests/unittestadapter/test_utils.py index a3bc1dd7693c..dc8a81175e70 100644 --- a/pythonFiles/tests/unittestadapter/test_utils.py +++ b/python_files/tests/unittestadapter/test_utils.py @@ -3,10 +3,12 @@ import os import pathlib +import sys import unittest import pytest -from unittestadapter.utils import ( + +from unittestadapter.pvsc_utils import ( TestNode, TestNodeTypeEnum, build_test_tree, @@ -14,11 +16,16 @@ get_test_case, ) -from .helpers import TEST_DATA_PATH, is_same_tree +script_dir = pathlib.Path(__file__).parent.parent +sys.path.append(os.fspath(script_dir)) + +from tests.tree_comparison_helper import is_same_tree # noqa: E402 + +TEST_DATA_PATH = pathlib.Path(__file__).parent / ".data" @pytest.mark.parametrize( - "directory, pattern, expected", + ("directory", "pattern", "expected"), [ ( ".", @@ -42,7 +49,6 @@ ) def test_simple_test_cases(directory, pattern, expected) -> None: """The get_test_case fuction should return tests from all test suites.""" - actual = [] # Discover tests in .data/. @@ -52,15 +58,13 @@ def test_simple_test_cases(directory, pattern, expected) -> None: suite = loader.discover(start_dir, pattern) # Iterate on get_test_case and save the test id. - for test in get_test_case(suite): - actual.append(test.id()) + actual = [test.id() for test in get_test_case(suite)] assert expected == actual def test_get_existing_child_node() -> None: """The get_child_node fuction should return the child node of a test tree if it exists.""" - tree: TestNode = { "name": "root", "path": "foo", @@ -103,12 +107,11 @@ def test_get_existing_child_node() -> None: tree_copy = tree.copy() # Check that the tree didn't get mutated by get_child_node. - assert is_same_tree(tree, tree_copy) + assert is_same_tree(tree, tree_copy, ["id_", "lineno", "name"]) def test_no_existing_child_node() -> None: """The get_child_node fuction should add a child node to a test tree and return it if it does not exist.""" - tree: TestNode = { "name": "root", "path": "foo", @@ -157,7 +160,7 @@ def test_no_existing_child_node() -> None: tree_after["children"] = tree_after["children"][:-1] # Check that all pre-existing items in the tree didn't get mutated by get_child_node. - assert is_same_tree(tree_before, tree_after) + assert is_same_tree(tree_before, tree_after, ["id_", "lineno", "name"]) # Check for the added node. last_child = tree["children"][-1] @@ -165,10 +168,7 @@ def test_no_existing_child_node() -> None: def test_build_simple_tree() -> None: - """The build_test_tree function should build and return a test tree from discovered test suites, - and an empty list of errors if there are none in the discovered data. - """ - + """The build_test_tree function should build and return a test tree from discovered test suites, and an empty list of errors if there are none in the discovered data.""" # Discovery tests in utils_simple_tree.py. start_dir = os.fsdecode(TEST_DATA_PATH) pattern = "utils_simple_tree*" @@ -219,16 +219,12 @@ def test_build_simple_tree() -> None: suite = loader.discover(start_dir, pattern) tests, errors = build_test_tree(suite, start_dir) - assert is_same_tree(expected, tests) + assert is_same_tree(expected, tests, ["id_", "lineno", "name"]) assert not errors def test_build_decorated_tree() -> None: - """The build_test_tree function should build and return a test tree from discovered test suites, - with correct line numbers for decorated test, - and an empty list of errors if there are none in the discovered data. - """ - + """The build_test_tree function should build and return a test tree from discovered test suites, with correct line numbers for decorated test, and an empty list of errors if there are none in the discovered data.""" # Discovery tests in utils_decorated_tree.py. start_dir = os.fsdecode(TEST_DATA_PATH) pattern = "utils_decorated_tree*" @@ -279,21 +275,65 @@ def test_build_decorated_tree() -> None: suite = loader.discover(start_dir, pattern) tests, errors = build_test_tree(suite, start_dir) - assert is_same_tree(expected, tests) + assert is_same_tree(expected, tests, ["id_", "lineno", "name"]) assert not errors def test_build_empty_tree() -> None: """The build_test_tree function should return None if there are no discovered test suites, and an empty list of errors if there are none in the discovered data.""" - start_dir = os.fsdecode(TEST_DATA_PATH) pattern = "does_not_exist*" - expected = None - loader = unittest.TestLoader() suite = loader.discover(start_dir, pattern) tests, errors = build_test_tree(suite, start_dir) - assert expected == tests + assert tests is not None + assert tests.get("children") == [] assert not errors + + +def test_doctest_standard_blocked() -> None: + """Standard doctests with short IDs should be skipped with an error message.""" + start_dir = os.fsdecode(TEST_DATA_PATH) + pattern = "test_doctest_standard*" + + loader = unittest.TestLoader() + suite = loader.discover(start_dir, pattern) + tests, errors = build_test_tree(suite, start_dir) + + # Should return a tree but with no test children (since doctests are skipped) + assert tests is not None + # Check that we got an error about doctests not being supported + assert len(errors) > 0 + assert "Skipping doctest as it is not supported for the extension" in errors[0] + + +def test_doctest_patched_works() -> None: + """Patched doctests with properly formatted IDs should be processed normally.""" + start_dir = os.fsdecode(TEST_DATA_PATH) + pattern = "test_doctest_patched*" + + loader = unittest.TestLoader() + suite = loader.discover(start_dir, pattern) + tests, errors = build_test_tree(suite, start_dir) + + # Should successfully build a tree with the patched doctest + assert tests is not None + + # The patched doctests should have proper IDs and be included + # We should find at least one test child (the doctests that were patched) + def count_tests(node): + """Recursively count test nodes.""" + if node.get("type_") == "test": + return 1 + count = 0 + for child in node.get("children", []): + count += count_tests(child) + return count + + test_count = count_tests(tests) + # We expect at least the module doctest and function doctest + assert test_count > 0, "Patched doctests should be included in the tree" + # Should not have doctest-related errors since they're properly formatted + assert not any("doctest" in str(e).lower() for e in errors) diff --git a/pythonFiles/tests/util.py b/python_files/tests/util.py similarity index 85% rename from pythonFiles/tests/util.py rename to python_files/tests/util.py index 45c3536145cf..ee240cd95202 100644 --- a/pythonFiles/tests/util.py +++ b/python_files/tests/util.py @@ -2,7 +2,7 @@ # Licensed under the MIT License. -class Stub(object): +class Stub: def __init__(self): self.calls = [] @@ -10,7 +10,7 @@ def add_call(self, name, args=None, kwargs=None): self.calls.append((name, args, kwargs)) -class StubProxy(object): +class StubProxy: def __init__(self, stub=None, name=None): self.name = name self.stub = stub if stub is not None else Stub() @@ -22,5 +22,5 @@ def calls(self): def add_call(self, funcname, *args, **kwargs): callname = funcname if self.name: - callname = "{}.{}".format(self.name, funcname) + callname = f"{self.name}.{funcname}" return self.stub.add_call(callname, *args, **kwargs) diff --git a/python_files/unittestadapter/__init__.py b/python_files/unittestadapter/__init__.py new file mode 100644 index 000000000000..5b7f7a925cc0 --- /dev/null +++ b/python_files/unittestadapter/__init__.py @@ -0,0 +1,2 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. diff --git a/python_files/unittestadapter/discovery.py b/python_files/unittestadapter/discovery.py new file mode 100644 index 000000000000..c864ac76916b --- /dev/null +++ b/python_files/unittestadapter/discovery.py @@ -0,0 +1,156 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +import os +import pathlib +import sys +import traceback +import unittest +from typing import List, Optional + +script_dir = pathlib.Path(__file__).parent +sys.path.append(os.fspath(script_dir)) + +from django_handler import django_discovery_runner # noqa: E402 + +# If I use from utils then there will be an import error in test_discovery.py. +from unittestadapter.pvsc_utils import ( # noqa: E402 + DiscoveryPayloadDict, + VSCodeUnittestError, + build_test_tree, + parse_unittest_args, + send_post_request, +) + + +def discover_tests( + start_dir: str, + pattern: str, + top_level_dir: Optional[str], + project_root_path: Optional[str] = None, +) -> DiscoveryPayloadDict: + """Returns a dictionary containing details of the discovered tests. + + The returned dict has the following keys: + + - cwd: Absolute path to the test start directory (or project_root_path if provided); + - status: Test discovery status, can be "success" or "error"; + - tests: Discoverered tests if any, not present otherwise. Note that the status can be "error" but the payload can still contain tests; + - error: Discovery error if any, not present otherwise. + + Payload format for a successful discovery: + { + "status": "success", + "cwd": , + "tests": + } + + Payload format for a successful discovery with no tests: + { + "status": "success", + "cwd": , + } + + Payload format when there are errors: + { + "cwd": + "": [list of errors] + "status": "error", + } + + Args: + start_dir: Directory where test discovery starts + pattern: Pattern to match test files (e.g., "test*.py") + top_level_dir: Top-level directory for the test tree hierarchy + project_root_path: Optional project root path for the cwd in the response payload + (used for project-based testing to root test tree at project) + """ + cwd = os.path.abspath(project_root_path or start_dir) # noqa: PTH100 + if "/" in start_dir: # is a subdir + parent_dir = os.path.dirname(start_dir) # noqa: PTH120 + sys.path.insert(0, parent_dir) + else: + sys.path.insert(0, cwd) + payload: DiscoveryPayloadDict = {"cwd": cwd, "status": "success", "tests": None} + tests = None + error: List[str] = [] + + try: + loader = unittest.TestLoader() + suite = loader.discover(start_dir, pattern, top_level_dir) + + # If the top level directory is not provided, then use the start directory. + if top_level_dir is None: + top_level_dir = start_dir + + # Get abspath of top level directory for build_test_tree. + top_level_dir = os.path.abspath(top_level_dir) # noqa: PTH100 + + tests, error = build_test_tree(suite, top_level_dir) # test tree built successfully here. + + except Exception: + error.append(traceback.format_exc()) + + # Still include the tests in the payload even if there are errors so that the TS + # side can determine if it is from run or discovery. + payload["tests"] = tests if tests is not None else None + + if len(error): + payload["status"] = "error" + payload["error"] = error + + return payload + + +if __name__ == "__main__": + # Get unittest discovery arguments. + argv = sys.argv[1:] + index = argv.index("--udiscovery") + + ( + start_dir, + pattern, + top_level_dir, + _verbosity, + _failfast, + _locals, + ) = parse_unittest_args(argv[index + 1 :]) + + test_run_pipe = os.getenv("TEST_RUN_PIPE") + if not test_run_pipe: + error_msg = ( + "UNITTEST ERROR: TEST_RUN_PIPE is not set at the time of unittest trying to send data. " + "Please confirm this environment variable is not being changed or removed " + "as it is required for successful test discovery and execution." + f"TEST_RUN_PIPE = {test_run_pipe}\n" + ) + print(error_msg, file=sys.stderr) + raise VSCodeUnittestError(error_msg) + + if manage_py_path := os.environ.get("MANAGE_PY_PATH"): + # Django configuration requires manage.py path to enable. + print( + f"MANAGE_PY_PATH is set, running Django discovery with path to manage.py as: ${manage_py_path}" + ) + try: + # collect args for Django discovery runner. + args = argv[index + 1 :] or [] + django_discovery_runner(manage_py_path, args) + except Exception as e: + error_msg = f"Error configuring Django test runner: {e}" + print(error_msg, file=sys.stderr) + raise VSCodeUnittestError(error_msg) # noqa: B904 + else: + # Check for PROJECT_ROOT_PATH environment variable (project-based testing). + # When set, this overrides top_level_dir to root the test tree at the project directory. + project_root_path = os.environ.get("PROJECT_ROOT_PATH") + if project_root_path: + top_level_dir = project_root_path + + # Perform regular unittest test discovery. + # Pass project_root_path so the payload's cwd matches the project root. + payload = discover_tests( + start_dir, pattern, top_level_dir, project_root_path=project_root_path + ) + # Post this discovery payload. + send_post_request(payload, test_run_pipe) diff --git a/python_files/unittestadapter/django_handler.py b/python_files/unittestadapter/django_handler.py new file mode 100644 index 000000000000..574aee7af7fa --- /dev/null +++ b/python_files/unittestadapter/django_handler.py @@ -0,0 +1,111 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +import os +import pathlib +import subprocess +import sys +from contextlib import contextmanager, suppress +from typing import Generator, List + +script_dir = pathlib.Path(__file__).parent +sys.path.append(os.fspath(script_dir)) +sys.path.insert(0, os.fspath(script_dir / "lib" / "python")) + +from pvsc_utils import ( # noqa: E402 + VSCodeUnittestError, +) + + +@contextmanager +def override_argv(argv: List[str]) -> Generator: + """Context manager to temporarily override sys.argv with the provided arguments.""" + original_argv = sys.argv + sys.argv = argv + try: + yield + finally: + sys.argv = original_argv + + +def django_discovery_runner(manage_py_path: str, args: List[str]) -> None: + # Attempt a small amount of validation on the manage.py path. + if not pathlib.Path(manage_py_path).exists(): + raise VSCodeUnittestError("Error running Django, manage.py path does not exist.") + + try: + # Get path to the custom_test_runner.py parent folder, add to sys.path and new environment used for subprocess. + custom_test_runner_dir = pathlib.Path(__file__).parent + sys.path.insert(0, os.fspath(custom_test_runner_dir)) + env = os.environ.copy() + if "PYTHONPATH" in env: + env["PYTHONPATH"] = os.fspath(custom_test_runner_dir) + os.pathsep + env["PYTHONPATH"] + else: + env["PYTHONPATH"] = os.fspath(custom_test_runner_dir) + + # Build command to run 'python manage.py test'. + command = [ + sys.executable, + manage_py_path, + "test", + "--testrunner=django_test_runner.CustomDiscoveryTestRunner", + ] + command.extend(args) + print("Running Django tests with command:", command) + + subprocess_discovery = subprocess.run( + command, + capture_output=True, + text=True, + env=env, + ) + print(subprocess_discovery.stderr, file=sys.stderr) + print(subprocess_discovery.stdout, file=sys.stdout) + # Zero return code indicates success, 1 indicates test failures, so both are considered successful. + if subprocess_discovery.returncode not in (0, 1): + error_msg = "Django test discovery process exited with non-zero error code See stderr above for more details." + print(error_msg, file=sys.stderr) + except Exception as e: + raise VSCodeUnittestError(f"Error during Django discovery: {e}") # noqa: B904 + + +def django_execution_runner(manage_py_path: str, test_ids: List[str], args: List[str]) -> None: + manage_path: pathlib.Path = pathlib.Path(manage_py_path) + # Attempt a small amount of validation on the manage.py path. + if not manage_path.exists(): + raise VSCodeUnittestError("Error running Django, manage.py path does not exist.") + + try: + # Get path to the custom_test_runner.py parent folder, add to sys.path. + custom_test_runner_dir: pathlib.Path = pathlib.Path(__file__).parent + sys.path.insert(0, os.fspath(custom_test_runner_dir)) + env: dict[str, str] = os.environ.copy() + if "PYTHONPATH" in env: + env["PYTHONPATH"] = os.fspath(custom_test_runner_dir) + os.pathsep + env["PYTHONPATH"] + else: + env["PYTHONPATH"] = os.fspath(custom_test_runner_dir) + + django_project_dir: pathlib.Path = manage_path.parent + sys.path.insert(0, os.fspath(django_project_dir)) + print(f"Django project directory: {django_project_dir}") + + manage_argv: List[str] = [ + str(manage_path), + "test", + "--testrunner=django_test_runner.CustomExecutionTestRunner", + *args, + *test_ids, + ] + print(f"Django manage.py arguments: {manage_argv}") + + try: + argv_context = override_argv(manage_argv) + suppress_context = suppress(SystemExit) + manage_file = manage_path.open() + with argv_context, suppress_context, manage_file: + manage_code = manage_file.read() + exec(manage_code, {"__name__": "__main__", "__file__": manage_path}) + except OSError as e: + raise VSCodeUnittestError("Error running Django, unable to read manage.py") from e + except Exception as e: + print(f"Error during Django test execution: {e}", file=sys.stderr) diff --git a/python_files/unittestadapter/django_test_runner.py b/python_files/unittestadapter/django_test_runner.py new file mode 100644 index 000000000000..c1cca7ac2780 --- /dev/null +++ b/python_files/unittestadapter/django_test_runner.py @@ -0,0 +1,95 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +import os +import pathlib +import sys + +script_dir = pathlib.Path(__file__).parent.parent +sys.path.append(os.fspath(script_dir)) + +from typing import TYPE_CHECKING # noqa: E402 + +from execution import UnittestTestResult # noqa: E402 +from pvsc_utils import ( # noqa: E402 + DiscoveryPayloadDict, + VSCodeUnittestError, + build_test_tree, + send_post_request, +) + +try: + from django.test.runner import DiscoverRunner +except ImportError: + raise ImportError( # noqa: B904 + "Django module not found. Please only use the environment variable MANAGE_PY_PATH if you want to use Django." + ) + + +if TYPE_CHECKING: + import unittest + + +class CustomDiscoveryTestRunner(DiscoverRunner): + """Custom test runner for Django to handle test DISCOVERY and building the test tree.""" + + def run_tests(self, test_labels, **kwargs): + test_run_pipe: str | None = os.getenv("TEST_RUN_PIPE") + if not test_run_pipe: + error_msg = ( + "UNITTEST ERROR: TEST_RUN_PIPE is not set at the time of unittest trying to send data. " + "Please confirm this environment variable is not being changed or removed " + "as it is required for successful test discovery and execution." + f"TEST_RUN_PIPE = {test_run_pipe}\n" + ) + print(error_msg, file=sys.stderr) + raise VSCodeUnittestError(error_msg) + try: + top_level_dir: pathlib.Path = pathlib.Path.cwd() + + # Discover tests and build into a tree. + suite: unittest.TestSuite = self.build_suite(test_labels, **kwargs) + tests, error = build_test_tree(suite, os.fspath(top_level_dir)) + + payload: DiscoveryPayloadDict = { + "cwd": os.fspath(top_level_dir), + "status": "success", + "tests": None, + } + payload["tests"] = tests if tests is not None else None + if len(error): + payload["status"] = "error" + payload["error"] = error + + # Send discovery payload. + send_post_request(payload, test_run_pipe) + return 0 # Skip actual test execution, return 0 as no tests were run. + except Exception as e: + error_msg = ( + "DJANGO ERROR: An error occurred while discovering and building the test suite. " + f"Error: {e}\n" + ) + print(error_msg, file=sys.stderr) + raise VSCodeUnittestError(error_msg) # noqa: B904 + + +class CustomExecutionTestRunner(DiscoverRunner): + """Custom test runner for Django to handle test EXECUTION and uses UnittestTestResult to send dynamic run results.""" + + def get_test_runner_kwargs(self): + """Override to provide custom test runner; resultclass.""" + test_run_pipe: str | None = os.getenv("TEST_RUN_PIPE") + if not test_run_pipe: + error_msg = ( + "UNITTEST ERROR: TEST_RUN_PIPE is not set at the time of Django trying to send data. " + "Please confirm this environment variable is not being changed or removed " + "as it is required for successful test discovery and execution." + f"TEST_RUN_PIPE = {test_run_pipe}\n" + ) + print(error_msg, file=sys.stderr) + raise VSCodeUnittestError(error_msg) + # Get existing kwargs + kwargs = super().get_test_runner_kwargs() + # Add custom resultclass, same resultclass as used in unittest. + kwargs["resultclass"] = UnittestTestResult + return kwargs diff --git a/python_files/unittestadapter/execution.py b/python_files/unittestadapter/execution.py new file mode 100644 index 000000000000..422f246d3476 --- /dev/null +++ b/python_files/unittestadapter/execution.py @@ -0,0 +1,427 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +import atexit +import enum +import os +import pathlib +import sys +import sysconfig +import traceback +import unittest +from types import TracebackType +from typing import Dict, List, Optional, Set, Tuple, Type, Union + +# Adds the scripts directory to the PATH as a workaround for enabling shell for test execution. +path_var_name = "PATH" if "PATH" in os.environ else "Path" +os.environ[path_var_name] = ( + sysconfig.get_paths()["scripts"] + os.pathsep + os.environ[path_var_name] +) + +script_dir = pathlib.Path(__file__).parent +sys.path.append(os.fspath(script_dir)) + +from django_handler import django_execution_runner # noqa: E402 + +from unittestadapter.pvsc_utils import ( # noqa: E402 + CoveragePayloadDict, + ExecutionPayloadDict, + FileCoverageInfo, + TestExecutionStatus, + VSCodeUnittestError, + parse_unittest_args, + send_post_request, +) + +ErrorType = Union[Tuple[Type[BaseException], BaseException, TracebackType], Tuple[None, None, None]] +test_run_pipe = "" +START_DIR = "" +# PROJECT_ROOT_PATH: Used for project-based testing to override cwd in payload +# When set, this should be used as the cwd in all execution payloads +PROJECT_ROOT_PATH = None # type: Optional[str] + + +class TestOutcomeEnum(str, enum.Enum): + error = "error" + failure = "failure" + success = "success" + skipped = "skipped" + expected_failure = "expected-failure" + unexpected_success = "unexpected-success" + subtest_success = "subtest-success" + subtest_failure = "subtest-failure" + + +class UnittestTestResult(unittest.TextTestResult): + def __init__(self, *args, **kwargs): + self.formatted: Dict[str, Dict[str, Union[str, None]]] = {} + super().__init__(*args, **kwargs) + + def startTest(self, test: unittest.TestCase): # noqa: N802 + super().startTest(test) + + def stopTestRun(self): # noqa: N802 + super().stopTestRun() + + def addError( # noqa: N802 + self, + test: unittest.TestCase, + err: ErrorType, + ): + super().addError(test, err) + self.formatResult(test, TestOutcomeEnum.error, err) + + def addFailure( # noqa: N802 + self, + test: unittest.TestCase, + err: ErrorType, + ): + super().addFailure(test, err) + self.formatResult(test, TestOutcomeEnum.failure, err) + + def addSuccess(self, test: unittest.TestCase): # noqa: N802 + super().addSuccess(test) + self.formatResult(test, TestOutcomeEnum.success) + + def addSkip(self, test: unittest.TestCase, reason: str): # noqa: N802 + super().addSkip(test, reason) + self.formatResult(test, TestOutcomeEnum.skipped) + + def addExpectedFailure(self, test: unittest.TestCase, err: ErrorType): # noqa: N802 + super().addExpectedFailure(test, err) + self.formatResult(test, TestOutcomeEnum.expected_failure, err) + + def addUnexpectedSuccess(self, test: unittest.TestCase): # noqa: N802 + super().addUnexpectedSuccess(test) + self.formatResult(test, TestOutcomeEnum.unexpected_success) + + def addSubTest( # noqa: N802 + self, + test: unittest.TestCase, + subtest: unittest.TestCase, + err: Union[ErrorType, None], + ): + super().addSubTest(test, subtest, err) + self.formatResult( + test, + TestOutcomeEnum.subtest_failure if err else TestOutcomeEnum.subtest_success, + err, + subtest, + ) + + def formatResult( # noqa: N802 + self, + test: unittest.TestCase, + outcome: str, + error: Union[ErrorType, None] = None, + subtest: Union[unittest.TestCase, None] = None, + ): + tb = None + + message = "" + # error is a tuple of the form returned by sys.exc_info(): (type, value, traceback). + if error is not None: + try: + message = f"{error[0]} {error[1]}" + except Exception: + message = "Error occurred, unknown type or value" + formatted = traceback.format_exception(*error) + tb = "".join(formatted) + # Remove the 'Traceback (most recent call last)' + formatted = formatted[1:] + test_id = subtest.id() if subtest else test.id() + + result = { + "test": test.id(), + "outcome": outcome, + "message": message, + "traceback": tb, + "subtest": subtest.id() if subtest else None, + } + self.formatted[test_id] = result + test_run_pipe = os.getenv("TEST_RUN_PIPE") + if not test_run_pipe: + print( + "UNITTEST ERROR: TEST_RUN_PIPE is not set at the time of unittest trying to send data. " + f"TEST_RUN_PIPE = {test_run_pipe}\n", + file=sys.stderr, + ) + raise VSCodeUnittestError( + "UNITTEST ERROR: TEST_RUN_PIPE is not set at the time of unittest trying to send data. " + ) + send_run_data(result, test_run_pipe) + + +def filter_tests(suite: unittest.TestSuite, test_ids: List[str]) -> unittest.TestSuite: + """Filter the tests in the suite to only run the ones with the given ids.""" + filtered_suite = unittest.TestSuite() + for test in suite: + if isinstance(test, unittest.TestCase): + if test.id() in test_ids: + filtered_suite.addTest(test) + else: + filtered_suite.addTest(filter_tests(test, test_ids)) + return filtered_suite + + +def get_all_test_ids(suite: unittest.TestSuite) -> List[str]: + """Return a list of all test ids in the suite.""" + test_ids = [] + for test in suite: + if isinstance(test, unittest.TestCase): + test_ids.append(test.id()) + else: + test_ids.extend(get_all_test_ids(test)) + return test_ids + + +def find_missing_tests(test_ids: List[str], suite: unittest.TestSuite) -> List[str]: + """Return a list of test ids that are not in the suite.""" + all_test_ids = get_all_test_ids(suite) + return [test_id for test_id in test_ids if test_id not in all_test_ids] + + +# Args: start_path path to a directory or a file, list of ids that may be empty. +# Edge cases: +# - if tests got deleted since the VS Code side last ran discovery and the current test run, +# return these test ids in the "not_found" entry, and the VS Code side can process them as "unknown"; +# - if tests got added since the VS Code side last ran discovery and the current test run, ignore them. +def run_tests( + start_dir: str, + test_ids: List[str], + pattern: str, + top_level_dir: Optional[str], + verbosity: int, + failfast: Optional[bool], # noqa: FBT001 + locals_: Optional[bool] = None, # noqa: FBT001 + project_root_path: Optional[str] = None, +) -> ExecutionPayloadDict: + """Run unittests and return the execution payload. + + Args: + start_dir: Directory where test discovery starts + test_ids: List of test IDs to run + pattern: Pattern to match test files + top_level_dir: Top-level directory for test tree hierarchy + verbosity: Verbosity level for test output + failfast: Stop on first failure + locals_: Show local variables in tracebacks + project_root_path: Optional project root path for the cwd in the response payload + (used for project-based testing to root test tree at project) + """ + cwd = os.path.abspath(project_root_path or start_dir) # noqa: PTH100 + if "/" in start_dir: # is a subdir + parent_dir = os.path.dirname(start_dir) # noqa: PTH120 + sys.path.insert(0, parent_dir) + else: + sys.path.insert(0, cwd) + status = TestExecutionStatus.error + error = None + payload: ExecutionPayloadDict = {"cwd": cwd, "status": status, "result": None} + + try: + # If it's a file, split path and file name. + start_dir = cwd + if cwd.endswith(".py"): + start_dir = os.path.dirname(cwd) # noqa: PTH120 + pattern = os.path.basename(cwd) # noqa: PTH119 + + if failfast is None: + failfast = False + if locals_ is None: + locals_ = False + if verbosity is None: + verbosity = 1 + runner = unittest.TextTestRunner( + resultclass=UnittestTestResult, + tb_locals=locals_, + failfast=failfast, + verbosity=verbosity, + ) + + # Discover tests at path with the file name as a pattern (if any). + loader = unittest.TestLoader() + suite = loader.discover(start_dir, pattern, top_level_dir) + + # lets try to tailer our own suite so we can figure out running only the ones we want + tailor: unittest.TestSuite = filter_tests(suite, test_ids) + + # If any tests are missing, add them to the payload. + not_found = find_missing_tests(test_ids, tailor) + if not_found: + missing_suite = loader.loadTestsFromNames(not_found) + tailor.addTests(missing_suite) + + result: UnittestTestResult = runner.run(tailor) # type: ignore + + payload["result"] = result.formatted + + except Exception: + status = TestExecutionStatus.error + error = traceback.format_exc() + + if error is not None: + payload["error"] = error + else: + status = TestExecutionStatus.success + + payload["status"] = status + + return payload + + +__socket = None +atexit.register(lambda: __socket.close() if __socket else None) + + +def send_run_data(raw_data, test_run_pipe): + status = raw_data["outcome"] + # Use PROJECT_ROOT_PATH if set (project-based testing), otherwise use START_DIR + cwd = os.path.abspath(PROJECT_ROOT_PATH or START_DIR) # noqa: PTH100 + test_id = raw_data["subtest"] or raw_data["test"] + test_dict = {} + test_dict[test_id] = raw_data + payload: ExecutionPayloadDict = {"cwd": cwd, "status": status, "result": test_dict} + send_post_request(payload, test_run_pipe) + + +if __name__ == "__main__": + # Get unittest test execution arguments. + argv = sys.argv[1:] + index = argv.index("--udiscovery") + + ( + start_dir, + pattern, + top_level_dir, + verbosity, + failfast, + locals_, + ) = parse_unittest_args(argv[index + 1 :]) + + run_test_ids_pipe = os.environ.get("RUN_TEST_IDS_PIPE") + test_run_pipe = os.getenv("TEST_RUN_PIPE") + if not run_test_ids_pipe: + print("Error[vscode-unittest]: RUN_TEST_IDS_PIPE env var is not set.", file=sys.stderr) + raise VSCodeUnittestError("Error[vscode-unittest]: RUN_TEST_IDS_PIPE env var is not set.") + if not test_run_pipe: + print("Error[vscode-unittest]: TEST_RUN_PIPE env var is not set.", file=sys.stderr) + raise VSCodeUnittestError("Error[vscode-unittest]: TEST_RUN_PIPE env var is not set.") + test_ids = [] + cwd = pathlib.Path(start_dir).absolute() + try: + # Read the test ids from the file, attempt to delete file afterwords. + ids_path = pathlib.Path(run_test_ids_pipe) + test_ids = ids_path.read_text(encoding="utf-8").splitlines() + try: + ids_path.unlink() + except Exception as e: + print(f"Error[vscode-unittest]: unable to delete temp file: {e}", file=sys.stderr) + + except Exception as e: + # No test ids received from buffer, return error payload + status: TestExecutionStatus = TestExecutionStatus.error + payload: ExecutionPayloadDict = { + "cwd": str(cwd), + "status": status, + "result": None, + "error": "No test ids read from temp file," + str(e), + } + send_post_request(payload, test_run_pipe) + + workspace_root = os.environ.get("COVERAGE_ENABLED") + # For unittest COVERAGE_ENABLED is to the root of the workspace so correct data is collected + cov = None + is_coverage_run = os.environ.get("COVERAGE_ENABLED") is not None + include_branches = False + if is_coverage_run: + import coverage + + # insert "python_files/lib/python" into the path so packaging can be imported + python_files_dir = pathlib.Path(__file__).parent.parent + bundled_dir = pathlib.Path(python_files_dir / "lib" / "python") + sys.path.append(os.fspath(bundled_dir)) + + from packaging.version import Version + + coverage_version = Version(coverage.__version__) + # only include branches if coverage version is 7.7.0 or greater (as this was when the api saves) + if coverage_version >= Version("7.7.0"): + include_branches = True + + source_ar: List[str] = [] + if workspace_root: + source_ar.append(workspace_root) + if top_level_dir: + source_ar.append(top_level_dir) + if start_dir: + source_ar.append(os.path.abspath(start_dir)) # noqa: PTH100 + cov = coverage.Coverage( + branch=include_branches, source=source_ar + ) # is at least 1 of these required?? + cov.start() + + # If no error occurred, we will have test ids to run. + if manage_py_path := os.environ.get("MANAGE_PY_PATH"): + args = argv[index + 1 :] or [] + django_execution_runner(manage_py_path, test_ids, args) + else: + # Check for PROJECT_ROOT_PATH environment variable (project-based testing). + # When set, this overrides the cwd in the payload to match the project root. + project_root_path = os.environ.get("PROJECT_ROOT_PATH") + if project_root_path: + # Update the module-level variable for send_run_data to use + # pylint: disable=global-statement + globals()["PROJECT_ROOT_PATH"] = project_root_path + + # Perform regular unittest execution. + # Pass project_root_path so the payload's cwd matches the project root. + payload = run_tests( + start_dir, + test_ids, + pattern, + top_level_dir, + verbosity, + failfast, + locals_, + project_root_path=project_root_path, + ) + + if is_coverage_run: + import coverage + + if not cov: + raise VSCodeUnittestError("Coverage is enabled but cov is not set") + cov.stop() + cov.save() + cov.load() + file_set: Set[str] = cov.get_data().measured_files() + file_coverage_map: Dict[str, FileCoverageInfo] = {} + for file in file_set: + analysis = cov.analysis2(file) + taken_file_branches = 0 + total_file_branches = -1 + + if include_branches: + branch_stats: dict[int, tuple[int, int]] = cov.branch_stats(file) + total_file_branches = sum([total_exits for total_exits, _ in branch_stats.values()]) + taken_file_branches = sum([taken_exits for _, taken_exits in branch_stats.values()]) + + lines_executable = {int(line_no) for line_no in analysis[1]} + lines_missed = {int(line_no) for line_no in analysis[3]} + lines_covered = lines_executable - lines_missed + file_info: FileCoverageInfo = { + "lines_covered": list(lines_covered), # list of int + "lines_missed": list(lines_missed), # list of int + "executed_branches": taken_file_branches, + "total_branches": total_file_branches, + } + file_coverage_map[file] = file_info + + payload_cov: CoveragePayloadDict = CoveragePayloadDict( + coverage=True, + cwd=os.fspath(cwd), + result=file_coverage_map, + error=None, + ) + send_post_request(payload_cov, test_run_pipe) diff --git a/python_files/unittestadapter/pvsc_utils.py b/python_files/unittestadapter/pvsc_utils.py new file mode 100644 index 000000000000..d6920592a4d4 --- /dev/null +++ b/python_files/unittestadapter/pvsc_utils.py @@ -0,0 +1,390 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +import argparse +import atexit +import doctest +import enum +import inspect +import json +import os +import pathlib +import sys +import unittest +from typing import Dict, List, Literal, Optional, Tuple, TypedDict, Union + +script_dir = pathlib.Path(__file__).parent.parent +sys.path.append(os.fspath(script_dir)) +sys.path.append(os.fspath(script_dir / "lib" / "python")) + +from typing_extensions import NotRequired # noqa: E402 + +# Types + + +# Inherit from str so it's JSON serializable. +class TestNodeTypeEnum(str, enum.Enum): + class_ = "class" + file = "file" + folder = "folder" + test = "test" + + +class TestData(TypedDict): + name: str + path: str + type_: TestNodeTypeEnum + id_: str + + +class TestItem(TestData): + lineno: str + runID: str + + +class TestNode(TestData): + children: "List[TestNode | TestItem]" + lineno: NotRequired[str] # Optional field for class nodes + + +class TestExecutionStatus(str, enum.Enum): + error = "error" + success = "success" + + +class VSCodeUnittestError(Exception): + """A custom exception class for unittest errors.""" + + def __init__(self, message): + super().__init__(message) + + +class DiscoveryPayloadDict(TypedDict): + cwd: str + status: Literal["success", "error"] + tests: Optional[TestNode] + error: NotRequired[List[str]] + + +class ExecutionPayloadDict(TypedDict): + cwd: str + status: TestExecutionStatus + result: Optional[Dict[str, Dict[str, Optional[str]]]] + not_found: NotRequired[List[str]] + error: NotRequired[str] + + +class FileCoverageInfo(TypedDict): + lines_covered: List[int] + lines_missed: List[int] + executed_branches: int + total_branches: int + + +class CoveragePayloadDict(Dict): + """A dictionary that is used to send a execution post request to the server.""" + + coverage: bool + cwd: str + result: Optional[Dict[str, FileCoverageInfo]] + error: Optional[str] # Currently unused need to check + + +# Helper functions for data retrieval. + + +def get_test_case(suite): + """Iterate through a unittest test suite and return all test cases.""" + for test in suite: + if isinstance(test, unittest.TestCase): + yield test + else: + yield from get_test_case(test) + + +def get_class_line(test_case: unittest.TestCase) -> Optional[str]: + """Get the line number where a test class is defined.""" + try: + test_class = test_case.__class__ + _sourcelines, lineno = inspect.getsourcelines(test_class) + return str(lineno) + except Exception: + return None + + +def get_source_line(obj) -> str: + """Get the line number of a test case start line.""" + try: + sourcelines, lineno = inspect.getsourcelines(obj) + except Exception: + try: + # tornado-specific, see https://github.com/microsoft/vscode-python/issues/17285. + sourcelines, lineno = inspect.getsourcelines(obj.orig_method) + except Exception: + return "*" + + # Return the line number of the first line of the test case definition. + for i, v in enumerate(sourcelines): + if v.strip().startswith(("def", "async def")): + return str(lineno + i) + + return "*" + + +# Helper functions for test tree building. + + +def build_test_node(path: str, name: str, type_: TestNodeTypeEnum) -> TestNode: + """Build a test node with no children. A test node can be a folder, a file or a class.""" + ## figure out if we are folder, file, or class + id_gen = path + if type_ == TestNodeTypeEnum.folder or type_ == TestNodeTypeEnum.file: + id_gen = path + else: + # means we have to build test node for class + id_gen = path + "\\" + name + + return {"path": path, "name": name, "type_": type_, "children": [], "id_": id_gen} + + +def get_child_node(name: str, path: str, type_: TestNodeTypeEnum, root: TestNode) -> TestNode: + """Find a child node in a test tree given its name, type and path. + + If the node doesn't exist, create it. + Path is required to distinguish between nodes with the same name and type. + """ + try: + result = next( + node + for node in root["children"] + if node["name"] == name and node["type_"] == type_ and node["path"] == path + ) + except StopIteration: + result = build_test_node(path, name, type_) + root["children"].append(result) + + return result # type:ignore + + +def build_test_tree( + suite: unittest.TestSuite, top_level_directory: str +) -> Tuple[Union[TestNode, None], List[str]]: + """Build a test tree from a unittest test suite. + + This function returns the test tree, and any errors found by unittest. + If no tests were discovered, return `None` and a list of errors (if any). + + Test tree structure: + { + "path": , + "type": "folder", + "name": , + "children": [ + { files and folders } + ... + { + "path": , + "name": filename.py, + "type_": "file", + "children": [ + { + "path": , + "name": , + "type_": "class", + "children": [ + { + "path": , + "name": , + "type_": "test", + "lineno": + "id_": , + } + ], + "id_": + } + ], + "id_": + } + ], + "id_": + } + """ + error = [] + directory_path = pathlib.PurePath(top_level_directory) + root = build_test_node(top_level_directory, directory_path.name, TestNodeTypeEnum.folder) + + for test_case in get_test_case(suite): + test_id = test_case.id() + if test_id.startswith("unittest.loader._FailedTest"): + error.append(str(test_case._exception)) # type: ignore # noqa: SLF001 + elif test_id.startswith("unittest.loader.ModuleSkipped"): + components = test_id.split(".") + class_name = f"{components[-1]}.py" + # Find/build class node. + file_path = os.fsdecode(directory_path / class_name) + current_node = get_child_node(class_name, file_path, TestNodeTypeEnum.file, root) + else: + # Get the static test path components: filename, class name and function name. + components = test_id.split(".") + # Check if this is a doctest with insufficient components that would cause unpacking to fail + if len(components) < 3 and isinstance(test_case, doctest.DocTestCase): + print( + "Skipping doctest as it is not supported for the extension. Test case: ", + test_case, + ) + error = ["Skipping doctest as it is not supported for the extension."] + continue + *folders, filename, class_name, function_name = components + py_filename = f"{filename}.py" + + current_node = root + + # Find/build nodes for the intermediate folders in the test path. + for folder in folders: + current_node = get_child_node( + folder, + os.fsdecode(pathlib.PurePath(current_node["path"], folder)), + TestNodeTypeEnum.folder, + current_node, + ) + + # Find/build file node. + path_components = [top_level_directory, *folders, py_filename] + file_path = os.fsdecode(pathlib.PurePath("/".join(path_components))) + current_node = get_child_node( + py_filename, file_path, TestNodeTypeEnum.file, current_node + ) + + # Find/build class node. + current_node = get_child_node( + class_name, file_path, TestNodeTypeEnum.class_, current_node + ) + + # Add line number to class node if not already present. + if "lineno" not in current_node: + class_lineno = get_class_line(test_case) + if class_lineno is not None: + current_node["lineno"] = class_lineno + + # Get test line number. + test_method = getattr(test_case, test_case._testMethodName) # noqa: SLF001 + lineno = get_source_line(test_method) + + # Add test node. + test_node: TestItem = { + "name": function_name, + "path": file_path, + "lineno": lineno, + "type_": TestNodeTypeEnum.test, + "id_": file_path + "\\" + class_name + "\\" + function_name, + "runID": test_id, + } # concatenate class name and function test name + current_node["children"].append(test_node) + + return root, error + + +def parse_unittest_args( + args: List[str], +) -> Tuple[str, str, Union[str, None], int, Union[bool, None], Union[bool, None]]: + """Parse command-line arguments that should be forwarded to unittest to perform discovery. + + Valid unittest arguments are: -v, -s, -p, -t and their long-form counterparts, + however we only care about the last three. + + The returned tuple contains the following items + - start_directory: The directory where to start discovery, defaults to . + - pattern: The pattern to match test files, defaults to test*.py + - top_level_directory: The top-level directory of the project, defaults to None, + and unittest will use start_directory behind the scenes. + """ + arg_parser = argparse.ArgumentParser() + arg_parser.add_argument("--start-directory", "-s", default=".") + arg_parser.add_argument("--pattern", "-p", default="test*.py") + arg_parser.add_argument("--top-level-directory", "-t", default=None) + arg_parser.add_argument("--failfast", "-f", action="store_true", default=None) + arg_parser.add_argument("--verbose", "-v", action="store_true", default=None) + arg_parser.add_argument("-q", "--quiet", action="store_true", default=None) + arg_parser.add_argument("--locals", action="store_true", default=None) + + parsed_args, _ = arg_parser.parse_known_args(args) + + verbosity: int = 1 + if parsed_args.quiet: + verbosity = 0 + elif parsed_args.verbose: + verbosity = 2 + + return ( + parsed_args.start_directory, + parsed_args.pattern, + parsed_args.top_level_directory, + verbosity, + parsed_args.failfast, + parsed_args.locals, + ) + + +__writer = None +atexit.register(lambda: __writer.close() if __writer else None) + + +def send_post_request( + payload: Union[ExecutionPayloadDict, DiscoveryPayloadDict, CoveragePayloadDict], + test_run_pipe: Optional[str], +): + """ + Sends a post request to the server. + + Keyword arguments: + payload -- the payload data to be sent. + test_run_pipe -- the name of the pipe to send the data to. + """ + if not test_run_pipe: + error_msg = ( + "UNITTEST ERROR: TEST_RUN_PIPE is not set at the time of unittest trying to send data. " + "Please confirm this environment variable is not being changed or removed " + "as it is required for successful test discovery and execution." + f"TEST_RUN_PIPE = {test_run_pipe}\n" + ) + print(error_msg, file=sys.stderr) + raise VSCodeUnittestError(error_msg) + + global __writer + + if __writer is None: + try: + __writer = open(test_run_pipe, "wb") # noqa: SIM115, PTH123 + except Exception as error: + error_msg = f"Error attempting to connect to extension named pipe {test_run_pipe}[vscode-unittest]: {error}" + print(error_msg, file=sys.stderr) + __writer = None + raise VSCodeUnittestError(error_msg) from error + + rpc = { + "jsonrpc": "2.0", + "params": payload, + } + data = json.dumps(rpc) + try: + if __writer: + request = ( + f"""content-length: {len(data)}\r\ncontent-type: application/json\r\n\r\n{data}""" + ) + size = 4096 + encoded = request.encode("utf-8") + bytes_written = 0 + while bytes_written < len(encoded): + segment = encoded[bytes_written : bytes_written + size] + bytes_written += __writer.write(segment) + __writer.flush() + else: + print( + f"Connection error[vscode-unittest], writer is None \n[vscode-unittest] data: \n{data} \n", + file=sys.stderr, + ) + except Exception as error: + print( + f"Exception thrown while attempting to send data[vscode-unittest]: {error} \n[vscode-unittest] data: \n{data}\n", + file=sys.stderr, + ) diff --git a/pythonFiles/visualstudio_py_testlauncher.py b/python_files/visualstudio_py_testlauncher.py similarity index 76% rename from pythonFiles/visualstudio_py_testlauncher.py rename to python_files/visualstudio_py_testlauncher.py index 0b0ef3242f65..878491083a71 100644 --- a/pythonFiles/visualstudio_py_testlauncher.py +++ b/python_files/visualstudio_py_testlauncher.py @@ -17,6 +17,7 @@ __author__ = "Microsoft Corporation " __version__ = "3.0.0.0" +import contextlib import json import os import signal @@ -27,11 +28,11 @@ try: import thread -except: +except ModuleNotFoundError: import _thread as thread -class _TestOutput(object): +class _TestOutput: """file like object which redirects output to the repl window.""" errors = "strict" @@ -39,7 +40,7 @@ class _TestOutput(object): def __init__(self, old_out, is_stdout): self.is_stdout = is_stdout self.old_out = old_out - if sys.version >= "3." and hasattr(old_out, "buffer"): + if sys.version_info[0] >= 3 and hasattr(old_out, "buffer"): self.buffer = _TestOutputBuffer(old_out.buffer, is_stdout) def flush(self): @@ -78,7 +79,7 @@ def __getattr__(self, name): return getattr(self.old_out, name) -class _TestOutputBuffer(object): +class _TestOutputBuffer: def __init__(self, old_buffer, is_stdout): self.buffer = old_buffer self.is_stdout = is_stdout @@ -100,7 +101,7 @@ def seek(self, pos, whence=0): return self.buffer.seek(pos, whence) -class _IpcChannel(object): +class _IpcChannel: def __init__(self, socket, callback): self.socket = socket self.seq = 0 @@ -108,14 +109,14 @@ def __init__(self, socket, callback): self.lock = thread.allocate_lock() self._closed = False # start the testing reader thread loop - self.test_thread_id = thread.start_new_thread(self.readSocket, ()) + self.test_thread_id = thread.start_new_thread(self.read_socket, ()) def close(self): self._closed = True - def readSocket(self): + def read_socket(self): try: - data = self.socket.recv(1024) + self.socket.recv(1024) self.callback() except OSError: if not self._closed: @@ -129,7 +130,7 @@ def send_event(self, name, **args): body = {"type": "event", "seq": self.seq, "event": name, "body": args} self.seq += 1 content = json.dumps(body).encode("utf8") - headers = ("Content-Length: %d\n\n" % (len(content),)).encode("utf8") + headers = f"Content-Length: {len(content)}\n\n".encode() self.socket.send(headers) self.socket.send(content) @@ -138,42 +139,40 @@ def send_event(self, name, **args): class VsTestResult(unittest.TextTestResult): - def startTest(self, test): - super(VsTestResult, self).startTest(test) + def startTest(self, test): # noqa: N802 + super().startTest(test) if _channel is not None: _channel.send_event(name="start", test=test.id()) - def addError(self, test, err): - super(VsTestResult, self).addError(test, err) + def addError(self, test, err): # noqa: N802 + super().addError(test, err) self.sendResult(test, "error", err) - def addFailure(self, test, err): - super(VsTestResult, self).addFailure(test, err) + def addFailure(self, test, err): # noqa: N802 + super().addFailure(test, err) self.sendResult(test, "failed", err) - def addSuccess(self, test): - super(VsTestResult, self).addSuccess(test) + def addSuccess(self, test): # noqa: N802 + super().addSuccess(test) self.sendResult(test, "passed") - def addSkip(self, test, reason): - super(VsTestResult, self).addSkip(test, reason) + def addSkip(self, test, reason): # noqa: N802 + super().addSkip(test, reason) self.sendResult(test, "skipped") - def addExpectedFailure(self, test, err): - super(VsTestResult, self).addExpectedFailure(test, err) + def addExpectedFailure(self, test, err): # noqa: N802 + super().addExpectedFailure(test, err) self.sendResult(test, "failed-expected", err) - def addUnexpectedSuccess(self, test): - super(VsTestResult, self).addUnexpectedSuccess(test) + def addUnexpectedSuccess(self, test): # noqa: N802 + super().addUnexpectedSuccess(test) self.sendResult(test, "passed-unexpected") - def addSubTest(self, test, subtest, err): - super(VsTestResult, self).addSubTest(test, subtest, err) - self.sendResult( - test, "subtest-passed" if err is None else "subtest-failed", err, subtest - ) + def addSubTest(self, test, subtest, err): # noqa: N802 + super().addSubTest(test, subtest, err) + self.sendResult(test, "subtest-passed" if err is None else "subtest-failed", err, subtest) - def sendResult(self, test, outcome, trace=None, subtest=None): + def sendResult(self, test, outcome, trace=None, subtest=None): # noqa: N802 if _channel is not None: tb = None message = None @@ -196,22 +195,19 @@ def sendResult(self, test, outcome, trace=None, subtest=None): _channel.send_event("result", **result) -def stopTests(): +def stop_tests(): try: os.kill(os.getpid(), signal.SIGUSR1) - except: - try: - os.kill(os.getpid(), signal.SIGTERM) - except: - pass + except Exception: + os.kill(os.getpid(), signal.SIGTERM) -class ExitCommand(Exception): +class ExitCommand(Exception): # noqa: N818 pass -def signal_handler(signal, frame): - raise ExitCommand() +def signal_handler(signal, frame): # noqa: ARG001 + raise ExitCommand def main(): @@ -226,9 +222,7 @@ def main(): prog="visualstudio_py_testlauncher", usage="Usage: %prog [

Output for Python in the Output panel (View→Output, change the drop-down the upper-right of the Output panel to Python) @@ -32,16 +27,3 @@ XXX

- -
- -User Settings - -

- -``` -{3}{4} -``` - -

-
diff --git a/resources/report_issue_user_data_template.md b/resources/report_issue_user_data_template.md new file mode 100644 index 000000000000..037b844511d3 --- /dev/null +++ b/resources/report_issue_user_data_template.md @@ -0,0 +1,21 @@ +- Python version (& distribution if applicable, e.g. Anaconda): {0} +- Type of virtual environment used (e.g. conda, venv, virtualenv, etc.): {1} +- Value of the `python.languageServer` setting: {2} + +
+User Settings +

+ +``` +{3}{4} +``` +

+
+ +
+Installed Extensions + +|Extension Name|Extension Id|Version| +|---|---|---| +{5} +
diff --git a/resources/report_issue_user_settings.json b/resources/report_issue_user_settings.json index 778434c5cf0d..7e034651c46d 100644 --- a/resources/report_issue_user_settings.json +++ b/resources/report_issue_user_settings.json @@ -11,6 +11,7 @@ "condaPath": "placeholder", "pipenvPath": "placeholder", "poetryPath": "placeholder", + "pixiToolPath": "placeholder", "devOptions": false, "globalModuleInstallation": false, "languageServer": true, @@ -69,19 +70,6 @@ "memory": true, "symbolsHierarchyDepthLimit": false }, - "sortImports": { - "args": "placeholder", - "path": "placeholder" - }, - "formatting": { - "autopep8Args": "placeholder", - "autopep8Path": "placeholder", - "provider": true, - "blackArgs": "placeholder", - "blackPath": "placeholder", - "yapfArgs": "placeholder", - "yapfPath": "placeholder" - }, "testing": { "cwd": "placeholder", "debugPort": true, @@ -91,7 +79,8 @@ "pytestPath": "placeholder", "unittestArgs": "placeholder", "unittestEnabled": true, - "autoTestDiscoverOnSaveEnabled": true + "autoTestDiscoverOnSaveEnabled": true, + "autoTestDiscoverOnSavePattern": "placeholder" }, "terminal": { "activateEnvironment": true, diff --git a/resources/walkthrough/environments-info.md b/resources/walkthrough/environments-info.md new file mode 100644 index 000000000000..7bdc61a96e2e --- /dev/null +++ b/resources/walkthrough/environments-info.md @@ -0,0 +1,10 @@ +## Python Environments + +Create Environment Dropdown + +Python virtual environments are considered a best practice in Python development. A virtual environment includes a Python interpreter and any packages you have installed into it, such as numpy or Flask. + +After you create a virtual environment using the **Python: Create Environment** command, you can install packages into the environment. +For example, type `python -m pip install numpy` in an activated terminal to install `numpy` into the environment. + +πŸ” Check out our [docs](https://aka.ms/pythonenvs) to learn more. diff --git a/schemas/conda-environment.json b/schemas/conda-environment.json index 458676942a44..fb1e821778c3 100644 --- a/schemas/conda-environment.json +++ b/schemas/conda-environment.json @@ -1,6 +1,6 @@ { "title": "conda environment file", - "description": "Support for conda's enviroment.yml files (e.g. `conda env export > environment.yml`)", + "description": "Support for conda's environment.yml files (e.g. `conda env export > environment.yml`)", "id": "https://raw.githubusercontent.com/Microsoft/vscode-python/main/schemas/conda-environment.json", "$schema": "http://json-schema.org/draft-04/schema#", "definitions": { diff --git a/schemas/condarc.json b/schemas/condarc.json index 396236238c1a..a881315d3137 100644 --- a/schemas/condarc.json +++ b/schemas/condarc.json @@ -59,7 +59,14 @@ } }, "ssl_verify": { - "type": "boolean" + "oneOf": [ + { + "type": "boolean" + }, + { + "type": "string" + } + ] }, "offline": { "type": "boolean" diff --git a/scripts/cleanup-eslintignore.js b/scripts/cleanup-eslintignore.js new file mode 100644 index 000000000000..848f5a9c4910 --- /dev/null +++ b/scripts/cleanup-eslintignore.js @@ -0,0 +1,44 @@ +const fs = require('fs'); +const path = require('path'); + +const baseDir = process.cwd(); +const eslintignorePath = path.join(baseDir, '.eslintignore'); + +fs.readFile(eslintignorePath, 'utf8', (err, data) => { + if (err) { + console.error('Error reading .eslintignore file:', err); + return; + } + + const lines = data.split('\n'); + const files = lines.map((line) => line.trim()).filter((line) => line && !line.startsWith('#')); + const nonExistentFiles = []; + + files.forEach((file) => { + const filePath = path.join(baseDir, file); + if (!fs.existsSync(filePath) && file !== 'pythonExtensionApi/out/') { + nonExistentFiles.push(file); + } + }); + + if (nonExistentFiles.length > 0) { + console.log('The following files listed in .eslintignore do not exist:'); + nonExistentFiles.forEach((file) => console.log(file)); + + const updatedLines = lines.filter((line) => { + const trimmedLine = line.trim(); + return !nonExistentFiles.includes(trimmedLine) || trimmedLine === 'pythonExtensionApi/out/'; + }); + const updatedData = `${updatedLines.join('\n')}\n`; + + fs.writeFile(eslintignorePath, updatedData, 'utf8', (err) => { + if (err) { + console.error('Error writing to .eslintignore file:', err); + return; + } + console.log('Non-existent files have been removed from .eslintignore.'); + }); + } else { + console.log('All files listed in .eslintignore exist.'); + } +}); diff --git a/scripts/issue_velocity_summary_script.py b/scripts/issue_velocity_summary_script.py new file mode 100644 index 000000000000..94929d1798a9 --- /dev/null +++ b/scripts/issue_velocity_summary_script.py @@ -0,0 +1,110 @@ +""" +This script fetches open issues from the microsoft/vscode-python repository, +calculates the thumbs-up per day for each issue, and generates a markdown +summary of the issues sorted by highest thumbs-up per day. Issues with zero +thumbs-up are excluded from the summary. +""" + +import requests +import os +from datetime import datetime, timezone + + +GITHUB_API_URL = "https://api.github.com" +REPO = "microsoft/vscode-python" +TOKEN = os.getenv("GITHUB_TOKEN") + + +def fetch_issues(): + """ + Fetches all open issues from the specified GitHub repository. + + Returns: + list: A list of dictionaries representing the issues. + """ + headers = {"Authorization": f"token {TOKEN}"} + issues = [] + page = 1 + while True: + query = ( + f"{GITHUB_API_URL}/repos/{REPO}/issues?state=open&per_page=25&page={page}" + ) + response = requests.get(query, headers=headers) + if response.status_code == 403: + raise Exception( + "Access forbidden: Check your GitHub token and permissions." + ) + response.raise_for_status() + page_issues = response.json() + if not page_issues: + break + issues.extend(page_issues) + page += 1 + return issues + + +def calculate_thumbs_up_per_day(issue): + """ + Calculates the thumbs-up per day for a given issue. + + Args: + issue (dict): A dictionary representing the issue. + + Returns: + float: The thumbs-up per day for the issue. + """ + created_at = datetime.strptime(issue["created_at"], "%Y-%m-%dT%H:%M:%SZ").replace( + tzinfo=timezone.utc + ) + now = datetime.now(timezone.utc) + days_open = (now - created_at).days or 1 + thumbs_up = issue["reactions"].get("+1", 0) + return thumbs_up / days_open + + +def generate_markdown_summary(issues): + """ + Generates a markdown summary of the issues. + + Args: + issues (list): A list of dictionaries representing the issues. + + Returns: + str: A markdown-formatted string summarizing the issues. + """ + summary = "| URL | Title | πŸ‘ | Days Open | πŸ‘/day |\n| --- | ----- | --- | --------- | ------ |\n" + issues_with_thumbs_up = [] + for issue in issues: + created_at = datetime.strptime( + issue["created_at"], "%Y-%m-%dT%H:%M:%SZ" + ).replace(tzinfo=timezone.utc) + now = datetime.now(timezone.utc) + days_open = (now - created_at).days or 1 + thumbs_up = issue["reactions"].get("+1", 0) + if thumbs_up > 0: + thumbs_up_per_day = thumbs_up / days_open + issues_with_thumbs_up.append( + (issue, thumbs_up, days_open, thumbs_up_per_day) + ) + + # Sort issues by thumbs_up_per_day in descending order + issues_with_thumbs_up.sort(key=lambda x: x[3], reverse=True) + + for issue, thumbs_up, days_open, thumbs_up_per_day in issues_with_thumbs_up: + summary += f"| {issue['html_url']} | {issue['title']} | {thumbs_up} | {days_open} | {thumbs_up_per_day:.2f} |\n" + + return summary + + +def main(): + """ + Main function to fetch issues, generate the markdown summary, and write it to a file. + """ + issues = fetch_issues() + summary = generate_markdown_summary(issues) + with open("endorsement_velocity_summary.md", "w") as f: + f.write(summary) + + +if __name__ == "__main__": + main() diff --git a/scripts/onCreateCommand.sh b/scripts/onCreateCommand.sh new file mode 100644 index 000000000000..3d473d1ee172 --- /dev/null +++ b/scripts/onCreateCommand.sh @@ -0,0 +1,36 @@ +#!/bin/bash + +# Install pyenv and Python versions here to avoid using shim. +curl https://pyenv.run | bash +echo 'export PYENV_ROOT="$HOME/.pyenv"' >> ~/.bashrc +echo 'command -v pyenv >/dev/null || export PATH="$PYENV_ROOT/bin:$PATH"' >> ~/.bashrc +# echo 'eval "$(pyenv init -)"' >> ~/.bashrc + +export PYENV_ROOT="$HOME/.pyenv" +command -v pyenv >/dev/null || export PATH="$PYENV_ROOT/bin:$PATH" +# eval "$(pyenv init -)" Comment this out and DO NOT use shim. +source ~/.bashrc + +# Install Python via pyenv . +pyenv install 3.8.18 3.9:latest 3.10:latest 3.11:latest + +# Set default Python version to 3.8 . +pyenv global 3.8.18 + +npm ci + +# Create Virutal environment. +pyenv exec python -m venv .venv + +# Activate Virtual environment. +source /workspaces/vscode-python/.venv/bin/activate + +# Install required Python libraries. +/workspaces/vscode-python/.venv/bin/python -m pip install nox +nox --session install_python_libs + +/workspaces/vscode-python/.venv/bin/python -m pip install -r build/test-requirements.txt +/workspaces/vscode-python/.venv/bin/python -m pip install -r build/functional-test-requirements.txt + +# Below will crash codespace +# npm run compile diff --git a/src/client/activation/activationManager.ts b/src/client/activation/activationManager.ts index fac5cbeda648..9e97c5c48857 100644 --- a/src/client/activation/activationManager.ts +++ b/src/client/activation/activationManager.ts @@ -11,6 +11,7 @@ import { PYTHON_LANGUAGE } from '../common/constants'; import { IFileSystem } from '../common/platform/types'; import { IDisposable, IInterpreterPathService, Resource } from '../common/types'; import { Deferred } from '../common/utils/async'; +import { StopWatch } from '../common/utils/stopWatch'; import { IInterpreterAutoSelectionService } from '../interpreter/autoSelection/types'; import { traceDecoratorError } from '../logging'; import { sendActivationTelemetry } from '../telemetry/envFileTelemetry'; @@ -69,7 +70,7 @@ export class ExtensionActivationManager implements IExtensionActivationManager { } } - public async activate(): Promise { + public async activate(startupStopWatch: StopWatch): Promise { this.filterServices(); await this.initialize(); @@ -77,12 +78,14 @@ export class ExtensionActivationManager implements IExtensionActivationManager { await Promise.all([ ...this.singleActivationServices.map((item) => item.activate()), - this.activateWorkspace(this.activeResourceService.getActiveResource()), + this.activateWorkspace(this.activeResourceService.getActiveResource(), startupStopWatch), ]); } @traceDecoratorError('Failed to activate a workspace') - public async activateWorkspace(resource: Resource): Promise { + public async activateWorkspace(resource: Resource, startupStopWatch?: StopWatch): Promise { + const folder = this.workspaceService.getWorkspaceFolder(resource); + resource = folder ? folder.uri : undefined; const key = this.getWorkspaceKey(resource); if (this.activatedWorkspaces.has(key)) { return; @@ -95,7 +98,7 @@ export class ExtensionActivationManager implements IExtensionActivationManager { await this.interpreterPathService.copyOldInterpreterStorageValuesToNew(resource); } await sendActivationTelemetry(this.fileSystem, this.workspaceService, resource); - await Promise.all(this.activationServices.map((item) => item.activate(resource))); + await Promise.all(this.activationServices.map((item) => item.activate(resource, startupStopWatch))); await this.appDiagnostics.performPreStartupHealthCheck(resource); } @@ -117,8 +120,7 @@ export class ExtensionActivationManager implements IExtensionActivationManager { if (this.activatedWorkspaces.has(key)) { return; } - const folder = this.workspaceService.getWorkspaceFolder(doc.uri); - this.activateWorkspace(folder ? folder.uri : undefined).ignoreErrors(); + this.activateWorkspace(doc.uri).ignoreErrors(); } protected addHandlers(): void { diff --git a/src/client/activation/extensionSurvey.ts b/src/client/activation/extensionSurvey.ts index 6d1d784237ba..d32ba7180c0f 100644 --- a/src/client/activation/extensionSurvey.ts +++ b/src/client/activation/extensionSurvey.ts @@ -3,10 +3,10 @@ 'use strict'; -import { inject, injectable, optional } from 'inversify'; +import { inject, injectable } from 'inversify'; import * as querystring from 'querystring'; import { env, UIKind } from 'vscode'; -import { IApplicationEnvironment, IApplicationShell } from '../common/application/types'; +import { IApplicationEnvironment, IApplicationShell, IWorkspaceService } from '../common/application/types'; import { ShowExtensionSurveyPrompt } from '../common/experiments/groups'; import '../common/extensions'; import { IPlatformService } from '../common/platform/types'; @@ -37,8 +37,9 @@ export class ExtensionSurveyPrompt implements IExtensionSingleActivationService @inject(IExperimentService) private experiments: IExperimentService, @inject(IApplicationEnvironment) private appEnvironment: IApplicationEnvironment, @inject(IPlatformService) private platformService: IPlatformService, - @optional() private sampleSizePerOneHundredUsers: number = 10, - @optional() private waitTimeToShowSurvey: number = WAIT_TIME_TO_SHOW_SURVEY, + @inject(IWorkspaceService) private readonly workspace: IWorkspaceService, + private sampleSizePerOneHundredUsers: number = 10, + private waitTimeToShowSurvey: number = WAIT_TIME_TO_SHOW_SURVEY, ) {} public async activate(): Promise { @@ -57,6 +58,18 @@ export class ExtensionSurveyPrompt implements IExtensionSingleActivationService if (env.uiKind === UIKind?.Web) { return false; } + + let feedbackEnabled = true; + + const telemetryConfig = this.workspace.getConfiguration('telemetry'); + if (telemetryConfig) { + feedbackEnabled = telemetryConfig.get('feedback.enabled', true); + } + + if (!feedbackEnabled) { + return false; + } + const doNotShowSurveyAgain = this.persistentState.createGlobalPersistentState( extensionSurveyStateKeys.doNotShowAgain, false, @@ -83,10 +96,10 @@ export class ExtensionSurveyPrompt implements IExtensionSingleActivationService @traceDecoratorError('Failed to display prompt for extension survey') public async showSurvey() { const prompts = [ExtensionSurveyBanner.bannerLabelYes, ExtensionSurveyBanner.maybeLater, Common.doNotShowAgain]; - const telemetrySelections: ['Yes', 'Maybe later', 'Do not show again'] = [ + const telemetrySelections: ['Yes', 'Maybe later', "Don't show again"] = [ 'Yes', 'Maybe later', - 'Do not show again', + "Don't show again", ]; const selection = await this.appShell.showInformationMessage(ExtensionSurveyBanner.bannerMessage, ...prompts); sendTelemetryEvent(EventName.EXTENSION_SURVEY_PROMPT, undefined, { diff --git a/src/client/activation/jedi/analysisOptions.ts b/src/client/activation/jedi/analysisOptions.ts index 4778c4e1523f..007008dc9b13 100644 --- a/src/client/activation/jedi/analysisOptions.ts +++ b/src/client/activation/jedi/analysisOptions.ts @@ -16,6 +16,8 @@ import { ILanguageServerOutputChannel } from '../types'; export class JediLanguageServerAnalysisOptions extends LanguageServerAnalysisOptionsWithEnv { private resource: Resource | undefined; + private interpreter: PythonEnvironment | undefined; + constructor( envVarsProvider: IEnvironmentVariablesProvider, lsOutputChannel: ILanguageServerOutputChannel, @@ -28,6 +30,7 @@ export class JediLanguageServerAnalysisOptions extends LanguageServerAnalysisOpt public async initialize(resource: Resource, interpreter: PythonEnvironment | undefined) { this.resource = resource; + this.interpreter = interpreter; return super.initialize(resource, interpreter); } @@ -76,11 +79,15 @@ export class JediLanguageServerAnalysisOptions extends LanguageServerAnalysisOpt }, workspace: { extraPaths: distinctExtraPaths, + environmentPath: this.interpreter?.path, symbols: { // 0 means remove limit on number of workspace symbols returned maxSymbols: 0, }, }, + semantic_tokens: { + enable: true, + }, }; } } diff --git a/src/client/activation/jedi/languageClientFactory.ts b/src/client/activation/jedi/languageClientFactory.ts index c3ef8d9623f3..70bd65da8d0d 100644 --- a/src/client/activation/jedi/languageClientFactory.ts +++ b/src/client/activation/jedi/languageClientFactory.ts @@ -21,7 +21,7 @@ export class JediLanguageClientFactory implements ILanguageClientFactory { clientOptions: LanguageClientOptions, ): Promise { // Just run the language server using a module - const lsScriptPath = path.join(EXTENSION_ROOT_DIR, 'pythonFiles', 'run-jedi-language-server.py'); + const lsScriptPath = path.join(EXTENSION_ROOT_DIR, 'python_files', 'run-jedi-language-server.py'); const interpreter = await this.interpreterService.getActiveInterpreter(resource); const serverOptions: ServerOptions = { command: interpreter ? interpreter.path : 'python', diff --git a/src/client/activation/jedi/languageClientMiddleware.ts b/src/client/activation/jedi/languageClientMiddleware.ts index 656c47309bb9..c8bb99629946 100644 --- a/src/client/activation/jedi/languageClientMiddleware.ts +++ b/src/client/activation/jedi/languageClientMiddleware.ts @@ -8,6 +8,5 @@ import { LanguageServerType } from '../types'; export class JediLanguageClientMiddleware extends LanguageClientMiddleware { public constructor(serviceContainer: IServiceContainer, serverVersion?: string) { super(serviceContainer, LanguageServerType.Jedi, serverVersion); - this.setupHidingMiddleware(serviceContainer); } } diff --git a/src/client/activation/jedi/manager.ts b/src/client/activation/jedi/manager.ts index 672e9a1b33fd..bafdcc735a12 100644 --- a/src/client/activation/jedi/manager.ts +++ b/src/client/activation/jedi/manager.ts @@ -68,7 +68,7 @@ export class JediLanguageServerManager implements ILanguageServerManager { try { // Version is actually hardcoded in our requirements.txt. const requirementsTxt = await fs.readFile( - path.join(EXTENSION_ROOT_DIR, 'pythonFiles', 'jedilsp_requirements', 'requirements.txt'), + path.join(EXTENSION_ROOT_DIR, 'python_files', 'jedilsp_requirements', 'requirements.txt'), 'utf-8', ); diff --git a/src/client/activation/languageClientMiddleware.ts b/src/client/activation/languageClientMiddleware.ts index 110d7461c615..d3d1e0c3c171 100644 --- a/src/client/activation/languageClientMiddleware.ts +++ b/src/client/activation/languageClientMiddleware.ts @@ -1,58 +1,14 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -import { IJupyterExtensionDependencyManager } from '../common/application/types'; -import { IDisposableRegistry, IExtensions } from '../common/types'; import { IServiceContainer } from '../ioc/types'; import { sendTelemetryEvent } from '../telemetry'; import { LanguageClientMiddlewareBase } from './languageClientMiddlewareBase'; import { LanguageServerType } from './types'; -import { createHidingMiddleware } from '@vscode/jupyter-lsp-middleware'; - export class LanguageClientMiddleware extends LanguageClientMiddlewareBase { public constructor(serviceContainer: IServiceContainer, serverType: LanguageServerType, serverVersion?: string) { super(serviceContainer, serverType, sendTelemetryEvent, serverVersion); } - - /** - * Creates the HidingMiddleware if needed and sets up code to do so if needed after - * Jupyter is installed. - * - * This method should be called from the constructor of derived classes. It is separated - * from the constructor to allow derived classes to initialize before it is called. - */ - protected setupHidingMiddleware(serviceContainer: IServiceContainer) { - const jupyterDependencyManager = serviceContainer.get( - IJupyterExtensionDependencyManager, - ); - const disposables = serviceContainer.get(IDisposableRegistry) || []; - const extensions = serviceContainer.get(IExtensions); - - // Enable notebook support if jupyter support is installed - if (this.shouldCreateHidingMiddleware(jupyterDependencyManager)) { - this.notebookAddon = createHidingMiddleware(); - } - - disposables.push( - extensions?.onDidChange(async () => { - await this.onExtensionChange(jupyterDependencyManager); - }), - ); - } - - protected shouldCreateHidingMiddleware(jupyterDependencyManager: IJupyterExtensionDependencyManager): boolean { - return jupyterDependencyManager && jupyterDependencyManager.isJupyterExtensionInstalled; - } - - protected async onExtensionChange(jupyterDependencyManager: IJupyterExtensionDependencyManager): Promise { - if (jupyterDependencyManager) { - if (this.notebookAddon && !this.shouldCreateHidingMiddleware(jupyterDependencyManager)) { - this.notebookAddon = undefined; - } else if (!this.notebookAddon && this.shouldCreateHidingMiddleware(jupyterDependencyManager)) { - this.notebookAddon = createHidingMiddleware(); - } - } - } } diff --git a/src/client/activation/languageClientMiddlewareBase.ts b/src/client/activation/languageClientMiddlewareBase.ts index 5f7b6fa72656..f1e102a4081d 100644 --- a/src/client/activation/languageClientMiddlewareBase.ts +++ b/src/client/activation/languageClientMiddlewareBase.ts @@ -87,10 +87,7 @@ export class LanguageClientMiddlewareBase implements Middleware { const settingDict: LSPObject & { pythonPath: string; _envPYTHONPATH: string } = settings[ i ] as LSPObject & { pythonPath: string; _envPYTHONPATH: string }; - settingDict.pythonPath = - (await this.getPythonPathOverride(uri)) ?? - (await interpreterService.getActiveInterpreter(uri))?.path ?? - 'python'; + settingDict.pythonPath = (await interpreterService.getActiveInterpreter(uri))?.path ?? 'python'; const env = await envService.getEnvironmentVariables(uri); const envPYTHONPATH = env.PYTHONPATH; @@ -106,11 +103,6 @@ export class LanguageClientMiddlewareBase implements Middleware { }, }; - // eslint-disable-next-line class-methods-use-this - protected async getPythonPathOverride(_uri: Uri | undefined): Promise { - return undefined; - } - // eslint-disable-next-line class-methods-use-this, @typescript-eslint/no-empty-function protected configurationHook(_item: ConfigurationItem, _settings: LSPObject): void {} diff --git a/src/client/activation/node/languageClientMiddleware.ts b/src/client/activation/node/languageClientMiddleware.ts index fbc534f17e1c..dfd65f1bb418 100644 --- a/src/client/activation/node/languageClientMiddleware.ts +++ b/src/client/activation/node/languageClientMiddleware.ts @@ -1,104 +1,13 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -import { Uri } from 'vscode'; -import { ConfigurationItem, LanguageClient, LSPObject } from 'vscode-languageclient/node'; -import { IJupyterExtensionDependencyManager, IWorkspaceService } from '../../common/application/types'; import { IServiceContainer } from '../../ioc/types'; -import { JupyterExtensionIntegration } from '../../jupyter/jupyterIntegration'; -import { traceLog } from '../../logging'; import { LanguageClientMiddleware } from '../languageClientMiddleware'; -import { LspInteractiveWindowMiddlewareAddon } from './lspInteractiveWindowMiddlewareAddon'; import { LanguageServerType } from '../types'; -import { LspNotebooksExperiment } from './lspNotebooksExperiment'; - export class NodeLanguageClientMiddleware extends LanguageClientMiddleware { - private readonly lspNotebooksExperiment: LspNotebooksExperiment; - - private readonly jupyterExtensionIntegration: JupyterExtensionIntegration; - - private readonly workspaceService: IWorkspaceService; - - public constructor( - serviceContainer: IServiceContainer, - private getClient: () => LanguageClient | undefined, - serverVersion?: string, - ) { + public constructor(serviceContainer: IServiceContainer, serverVersion?: string) { super(serviceContainer, LanguageServerType.Node, serverVersion); - - this.workspaceService = serviceContainer.get(IWorkspaceService); - - this.lspNotebooksExperiment = serviceContainer.get(LspNotebooksExperiment); - this.setupHidingMiddleware(serviceContainer); - - this.jupyterExtensionIntegration = serviceContainer.get( - JupyterExtensionIntegration, - ); - if (!this.notebookAddon) { - this.notebookAddon = new LspInteractiveWindowMiddlewareAddon( - this.getClient, - this.jupyterExtensionIntegration, - ); - } - } - - // eslint-disable-next-line class-methods-use-this - protected shouldCreateHidingMiddleware(_: IJupyterExtensionDependencyManager): boolean { - return false; - } - - protected async onExtensionChange(jupyterDependencyManager: IJupyterExtensionDependencyManager): Promise { - if (jupyterDependencyManager && jupyterDependencyManager.isJupyterExtensionInstalled) { - await this.lspNotebooksExperiment.onJupyterInstalled(); - } - - if (!this.notebookAddon) { - this.notebookAddon = new LspInteractiveWindowMiddlewareAddon( - this.getClient, - this.jupyterExtensionIntegration, - ); - } - } - - protected async getPythonPathOverride(uri: Uri | undefined): Promise { - if (!uri) { - return undefined; - } - - const jupyterPythonPathFunction = this.jupyterExtensionIntegration.getJupyterPythonPathFunction(); - if (!jupyterPythonPathFunction) { - return undefined; - } - - const result = await jupyterPythonPathFunction(uri); - - if (result) { - traceLog(`Jupyter provided interpreter path override: ${result}`); - } - - return result; - } - - // eslint-disable-next-line class-methods-use-this - protected configurationHook(item: ConfigurationItem, settings: LSPObject): void { - if (item.section === 'editor') { - if (this.workspaceService) { - // Get editor.formatOnType using Python language id so [python] setting - // will be honored if present. - const editorConfig = this.workspaceService.getConfiguration( - item.section, - undefined, - /* languageSpecific */ true, - ); - - const settingDict: LSPObject & { formatOnType?: boolean } = settings as LSPObject & { - formatOnType: boolean; - }; - - settingDict.formatOnType = editorConfig.get('formatOnType'); - } - } } } diff --git a/src/client/activation/node/lspInteractiveWindowMiddlewareAddon.ts b/src/client/activation/node/lspInteractiveWindowMiddlewareAddon.ts deleted file mode 100644 index c68ebfe5a59c..000000000000 --- a/src/client/activation/node/lspInteractiveWindowMiddlewareAddon.ts +++ /dev/null @@ -1,187 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. -import { Disposable, NotebookCell, NotebookDocument, TextDocument, TextDocumentChangeEvent, Uri } from 'vscode'; -import { Converter } from 'vscode-languageclient/lib/common/codeConverter'; -import { - DidChangeNotebookDocumentNotification, - LanguageClient, - Middleware, - NotebookCellKind, - NotebookDocumentChangeEvent, -} from 'vscode-languageclient/node'; -import * as proto from 'vscode-languageserver-protocol'; -import { JupyterExtensionIntegration } from '../../jupyter/jupyterIntegration'; - -type TextContent = Required['cells']>['textContent']>[0]; - -/** - * Detects the input box text documents of Interactive Windows and makes them appear to be - * the last cell of their corresponding notebooks. - */ -export class LspInteractiveWindowMiddlewareAddon implements Middleware, Disposable { - constructor( - private readonly getClient: () => LanguageClient | undefined, - private readonly jupyterExtensionIntegration: JupyterExtensionIntegration, - ) { - // Make sure a bunch of functions are bound to this. VS code can call them without a this context - this.didOpen = this.didOpen.bind(this); - this.didChange = this.didChange.bind(this); - this.didClose = this.didClose.bind(this); - } - - public dispose(): void { - // Nothing to dispose at the moment - } - - // Map of document URIs to NotebookDocuments for all known notebooks. - private notebookDocumentMap: Map = new Map(); - - // Map of document URIs to TextDocuments that should be linked to a notebook - // whose didOpen we're expecting to see in the future. - private unlinkedTextDocumentMap: Map = new Map(); - - public async didOpen(document: TextDocument, next: (ev: TextDocument) => Promise): Promise { - const notebookUri = this.getNotebookUriForTextDocumentUri(document.uri); - if (!notebookUri) { - await next(document); - return; - } - - const notebookDocument = this.notebookDocumentMap.get(notebookUri.toString()); - if (!notebookDocument) { - this.unlinkedTextDocumentMap.set(notebookUri.toString(), document); - return; - } - - try { - const result: NotebookDocumentChangeEvent = { - cells: { - structure: { - array: { - start: notebookDocument.cellCount, - deleteCount: 0, - cells: [{ kind: NotebookCellKind.Code, document: document.uri.toString() }], - }, - didOpen: [ - { - uri: document.uri.toString(), - languageId: document.languageId, - version: document.version, - text: document.getText(), - }, - ], - didClose: undefined, - }, - }, - }; - - await this.getClient()?.sendNotification(DidChangeNotebookDocumentNotification.type, { - notebookDocument: { version: notebookDocument.version, uri: notebookUri.toString() }, - change: result, - }); - } catch (error) { - this.getClient()?.error('Sending DidChangeNotebookDocumentNotification failed', error); - throw error; - } - } - - public async didChange( - event: TextDocumentChangeEvent, - next: (ev: TextDocumentChangeEvent) => Promise, - ): Promise { - const notebookUri = this.getNotebookUriForTextDocumentUri(event.document.uri); - if (!notebookUri) { - await next(event); - return; - } - - const notebookDocument = this.notebookDocumentMap.get(notebookUri.toString()); - if (notebookDocument) { - const client = this.getClient(); - if (client) { - client.sendNotification(proto.DidChangeNotebookDocumentNotification.type, { - notebookDocument: { uri: notebookUri.toString(), version: notebookDocument.version }, - change: { - cells: { - textContent: [ - LspInteractiveWindowMiddlewareAddon._asTextContentChange( - event, - client.code2ProtocolConverter, - ), - ], - }, - }, - }); - } - } - } - - private static _asTextContentChange(event: TextDocumentChangeEvent, c2pConverter: Converter): TextContent { - const params = c2pConverter.asChangeTextDocumentParams(event); - return { document: params.textDocument, changes: params.contentChanges }; - } - - public async didClose(document: TextDocument, next: (ev: TextDocument) => Promise): Promise { - const notebookUri = this.getNotebookUriForTextDocumentUri(document.uri); - if (!notebookUri) { - await next(document); - return; - } - - this.unlinkedTextDocumentMap.delete(notebookUri.toString()); - } - - public async didOpenNotebook( - notebookDocument: NotebookDocument, - cells: NotebookCell[], - next: (notebookDocument: NotebookDocument, cells: NotebookCell[]) => Promise, - ): Promise { - this.notebookDocumentMap.set(notebookDocument.uri.toString(), notebookDocument); - - const relatedTextDocument = this.unlinkedTextDocumentMap.get(notebookDocument.uri.toString()); - if (relatedTextDocument) { - const newCells = [ - ...cells, - { - index: notebookDocument.cellCount, - notebook: notebookDocument, - kind: NotebookCellKind.Code, - document: relatedTextDocument, - metadata: {}, - outputs: [], - executionSummary: undefined, - }, - ]; - - this.unlinkedTextDocumentMap.delete(notebookDocument.uri.toString()); - - await next(notebookDocument, newCells); - } else { - await next(notebookDocument, cells); - } - } - - public async didCloseNotebook( - notebookDocument: NotebookDocument, - cells: NotebookCell[], - next: (notebookDocument: NotebookDocument, cells: NotebookCell[]) => Promise, - ): Promise { - this.notebookDocumentMap.delete(notebookDocument.uri.toString()); - - await next(notebookDocument, cells); - } - - notebooks = { - didOpen: this.didOpenNotebook.bind(this), - didClose: this.didCloseNotebook.bind(this), - }; - - private getNotebookUriForTextDocumentUri(textDocumentUri: Uri): Uri | undefined { - const getNotebookUriFunction = this.jupyterExtensionIntegration.getGetNotebookUriForTextDocumentUriFunction(); - if (!getNotebookUriFunction) { - return undefined; - } - - return getNotebookUriFunction(textDocumentUri); - } -} diff --git a/src/client/activation/node/lspNotebooksExperiment.ts b/src/client/activation/node/lspNotebooksExperiment.ts deleted file mode 100644 index de0acde0600e..000000000000 --- a/src/client/activation/node/lspNotebooksExperiment.ts +++ /dev/null @@ -1,60 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. -import { inject, injectable } from 'inversify'; -import { IExtensionSingleActivationService } from '../types'; -import { traceVerbose } from '../../logging'; -import { IJupyterExtensionDependencyManager } from '../../common/application/types'; -import { IServiceContainer } from '../../ioc/types'; -import { sleep } from '../../common/utils/async'; -import { JupyterExtensionIntegration } from '../../jupyter/jupyterIntegration'; - -@injectable() -export class LspNotebooksExperiment implements IExtensionSingleActivationService { - public readonly supportedWorkspaceTypes = { untrustedWorkspace: true, virtualWorkspace: true }; - - private isJupyterInstalled = false; - - constructor( - @inject(IServiceContainer) private readonly serviceContainer: IServiceContainer, - @inject(IJupyterExtensionDependencyManager) jupyterDependencyManager: IJupyterExtensionDependencyManager, - ) { - this.isJupyterInstalled = jupyterDependencyManager.isJupyterExtensionInstalled; - } - - // eslint-disable-next-line class-methods-use-this - public activate(): Promise { - return Promise.resolve(); - } - - public async onJupyterInstalled(): Promise { - if (this.isJupyterInstalled) { - return; - } - - await this.waitForJupyterToRegisterPythonPathFunction(); - - this.isJupyterInstalled = true; - } - - private async waitForJupyterToRegisterPythonPathFunction(): Promise { - const jupyterExtensionIntegration = this.serviceContainer.get( - JupyterExtensionIntegration, - ); - - let success = false; - for (let tryCount = 0; tryCount < 20; tryCount += 1) { - const jupyterPythonPathFunction = jupyterExtensionIntegration.getJupyterPythonPathFunction(); - if (jupyterPythonPathFunction) { - traceVerbose(`Jupyter called registerJupyterPythonPathFunction`); - success = true; - break; - } - - await sleep(500); - } - - if (!success) { - traceVerbose(`Timed out waiting for Jupyter to call registerJupyterPythonPathFunction`); - } - } -} diff --git a/src/client/activation/node/manager.ts b/src/client/activation/node/manager.ts index b85d8fe6ed14..5a66e4abecd0 100644 --- a/src/client/activation/node/manager.ts +++ b/src/client/activation/node/manager.ts @@ -116,11 +116,7 @@ export class NodeLanguageServerManager implements ILanguageServerManager { @traceDecoratorVerbose('Starting language server') protected async startLanguageServer(): Promise { const options = await this.analysisOptions.getAnalysisOptions(); - this.middleware = new NodeLanguageClientMiddleware( - this.serviceContainer, - () => this.languageServerProxy.languageClient, - this.lsVersion, - ); + this.middleware = new NodeLanguageClientMiddleware(this.serviceContainer, this.lsVersion); options.middleware = this.middleware; // Make sure the middleware is connected if we restart and we we're already connected. diff --git a/src/client/activation/node/pylanceApi.ts b/src/client/activation/node/pylanceApi.ts index 72f20db140e4..4b3d21d7527e 100644 --- a/src/client/activation/node/pylanceApi.ts +++ b/src/client/activation/node/pylanceApi.ts @@ -18,7 +18,6 @@ export interface PylanceApi { }; notebook?: { registerJupyterPythonPathFunction(func: (uri: Uri) => Promise): void; - registerGetNotebookUriForTextDocumentUriFunction(func: (textDocumentUri: Uri) => Uri | undefined): void; getCompletionItems( document: TextDocument, position: Position, diff --git a/src/client/activation/requirementsTxtLinkActivator.ts b/src/client/activation/requirementsTxtLinkActivator.ts new file mode 100644 index 000000000000..fcb6b72e545e --- /dev/null +++ b/src/client/activation/requirementsTxtLinkActivator.ts @@ -0,0 +1,26 @@ +import { injectable } from 'inversify'; +import { Hover, languages, TextDocument, Position } from 'vscode'; +import { IExtensionSingleActivationService } from './types'; + +const PYPI_PROJECT_URL = 'https://pypi.org/project'; + +export function generatePyPiLink(name: string): string | null { + // Regex to allow to find every possible pypi package (base regex from https://peps.python.org/pep-0508/#names) + const projectName = name.match(/^([A-Z0-9]|[A-Z0-9][A-Z0-9._-]*)($|=| |;|\[)/i); + return projectName ? `${PYPI_PROJECT_URL}/${projectName[1]}/` : null; +} + +@injectable() +export class RequirementsTxtLinkActivator implements IExtensionSingleActivationService { + public readonly supportedWorkspaceTypes = { untrustedWorkspace: true, virtualWorkspace: true }; + + // eslint-disable-next-line class-methods-use-this + public async activate(): Promise { + languages.registerHoverProvider([{ pattern: '**/*requirement*.txt' }, { pattern: '**/requirements/*.txt' }], { + provideHover(document: TextDocument, position: Position) { + const link = generatePyPiLink(document.lineAt(position.line).text); + return link ? new Hover(link) : null; + }, + }); + } +} diff --git a/src/client/activation/serviceRegistry.ts b/src/client/activation/serviceRegistry.ts index aed2d2e346e4..875afa12f0b4 100644 --- a/src/client/activation/serviceRegistry.ts +++ b/src/client/activation/serviceRegistry.ts @@ -15,7 +15,7 @@ import { LoadLanguageServerExtension } from './common/loadLanguageServerExtensio import { PartialModeStatusItem } from './partialModeStatus'; import { ILanguageServerWatcher } from '../languageServer/types'; import { LanguageServerWatcher } from '../languageServer/watcher'; -import { LspNotebooksExperiment } from './node/lspNotebooksExperiment'; +import { RequirementsTxtLinkActivator } from './requirementsTxtLinkActivator'; export function registerTypes(serviceManager: IServiceManager): void { serviceManager.addSingleton(IExtensionActivationService, PartialModeStatusItem); @@ -35,6 +35,9 @@ export function registerTypes(serviceManager: IServiceManager): void { serviceManager.addSingleton(ILanguageServerWatcher, LanguageServerWatcher); serviceManager.addBinding(ILanguageServerWatcher, IExtensionActivationService); - serviceManager.addSingleton(LspNotebooksExperiment, LspNotebooksExperiment); - serviceManager.addBinding(LspNotebooksExperiment, IExtensionSingleActivationService); + + serviceManager.addSingleton( + IExtensionSingleActivationService, + RequirementsTxtLinkActivator, + ); } diff --git a/src/client/activation/types.ts b/src/client/activation/types.ts index 2a177bb570b8..e3b9b818691a 100644 --- a/src/client/activation/types.ts +++ b/src/client/activation/types.ts @@ -6,32 +6,20 @@ import { Event } from 'vscode'; import { LanguageClient, LanguageClientOptions } from 'vscode-languageclient/node'; import type { IDisposable, ILogOutputChannel, Resource } from '../common/types'; +import { StopWatch } from '../common/utils/stopWatch'; import { PythonEnvironment } from '../pythonEnvironments/info'; export const IExtensionActivationManager = Symbol('IExtensionActivationManager'); /** * Responsible for activation of extension. - * - * @export - * @interface IExtensionActivationManager - * @extends {IDisposable} */ export interface IExtensionActivationManager extends IDisposable { - /** - * Method invoked when extension activates (invoked once). - * - * @returns {Promise} - * @memberof IExtensionActivationManager - */ - activate(): Promise; + // Method invoked when extension activates (invoked once). + activate(startupStopWatch: StopWatch): Promise; /** * Method invoked when a workspace is loaded. * This is where we place initialization scripts for each workspace. * (e.g. if we need to run code for each workspace, then this is where that happens). - * - * @param {Resource} resource - * @returns {Promise} - * @memberof IExtensionActivationManager */ activateWorkspace(resource: Resource): Promise; } @@ -42,12 +30,10 @@ export const IExtensionActivationService = Symbol('IExtensionActivationService') * invoked for every workspace folder (in multi-root workspace folders) during the activation of the extension. * This is a great hook for extension activation code, i.e. you don't need to modify * the `extension.ts` file to invoke some code when extension gets activated. - * @export - * @interface IExtensionActivationService */ export interface IExtensionActivationService { supportedWorkspaceTypes: { untrustedWorkspace: boolean; virtualWorkspace: boolean }; - activate(resource: Resource): Promise; + activate(resource: Resource, startupStopWatch?: StopWatch): Promise; } export enum LanguageServerType { @@ -99,8 +85,6 @@ export interface ILanguageServerProxy extends IDisposable { * Sends a request to LS so as to load other extensions. * This is used as a plugin loader mechanism. * Anyone (such as intellicode) wanting to interact with LS, needs to send this request to LS. - * @param {{}} [args] - * @memberof ILanguageServerProxy */ loadExtension(args?: unknown): void; } @@ -109,9 +93,6 @@ export const ILanguageServerOutputChannel = Symbol('ILanguageServerOutputChannel export interface ILanguageServerOutputChannel { /** * Creates output channel if necessary and returns it - * - * @type {ILogOutputChannel} - * @memberof ILanguageServerOutputChannel */ readonly channel: ILogOutputChannel; } @@ -122,8 +103,6 @@ export const IExtensionSingleActivationService = Symbol('IExtensionSingleActivat * invoked during the activation of the extension. * This is a great hook for extension activation code, i.e. you don't need to modify * the `extension.ts` file to invoke some code when extension gets activated. - * @export - * @interface IExtensionSingleActivationService */ export interface IExtensionSingleActivationService { supportedWorkspaceTypes: { untrustedWorkspace: boolean; virtualWorkspace: boolean }; diff --git a/src/client/api.ts b/src/client/api.ts index 7bc3fc81373b..908da4be7103 100644 --- a/src/client/api.ts +++ b/src/client/api.ts @@ -4,39 +4,70 @@ 'use strict'; -import { noop } from 'lodash'; import { Uri, Event } from 'vscode'; import { BaseLanguageClient, LanguageClientOptions } from 'vscode-languageclient'; import { LanguageClient } from 'vscode-languageclient/node'; import { PYLANCE_NAME } from './activation/node/languageClientFactory'; import { ILanguageServerOutputChannel } from './activation/types'; -import { IExtensionApi } from './apiTypes'; +import { PythonExtension } from './api/types'; import { isTestExecution, PYTHON_LANGUAGE } from './common/constants'; import { IConfigurationService, Resource } from './common/types'; -import { getDebugpyLauncherArgs, getDebugpyPackagePath } from './debugger/extension/adapter/remoteLaunchers'; +import { getDebugpyLauncherArgs } from './debugger/extension/adapter/remoteLaunchers'; import { IInterpreterService } from './interpreter/contracts'; import { IServiceContainer, IServiceManager } from './ioc/types'; -import { JupyterExtensionIntegration } from './jupyter/jupyterIntegration'; -import { IDataViewerDataProvider, IJupyterUriProvider } from './jupyter/types'; +import { + JupyterExtensionIntegration, + JupyterExtensionPythonEnvironments, + JupyterPythonEnvironmentApi, +} from './jupyter/jupyterIntegration'; import { traceError } from './logging'; import { IDiscoveryAPI } from './pythonEnvironments/base/locator'; import { buildEnvironmentApi } from './environmentApi'; import { ApiForPylance } from './pylanceApi'; import { getTelemetryReporter } from './telemetry'; +import { TensorboardExtensionIntegration } from './tensorBoard/tensorboardIntegration'; +import { getDebugpyPath } from './debugger/pythonDebugger'; export function buildApi( ready: Promise, serviceManager: IServiceManager, serviceContainer: IServiceContainer, discoveryApi: IDiscoveryAPI, -): IExtensionApi { +): PythonExtension { const configurationService = serviceContainer.get(IConfigurationService); const interpreterService = serviceContainer.get(IInterpreterService); serviceManager.addSingleton(JupyterExtensionIntegration, JupyterExtensionIntegration); + serviceManager.addSingleton( + JupyterExtensionPythonEnvironments, + JupyterExtensionPythonEnvironments, + ); + serviceManager.addSingleton( + TensorboardExtensionIntegration, + TensorboardExtensionIntegration, + ); + const jupyterPythonEnvApi = serviceContainer.get(JupyterExtensionPythonEnvironments); + const environments = buildEnvironmentApi(discoveryApi, serviceContainer, jupyterPythonEnvApi); const jupyterIntegration = serviceContainer.get(JupyterExtensionIntegration); + jupyterIntegration.registerEnvApi(environments); + const tensorboardIntegration = serviceContainer.get( + TensorboardExtensionIntegration, + ); const outputChannel = serviceContainer.get(ILanguageServerOutputChannel); - const api: IExtensionApi & { + const api: PythonExtension & { + /** + * Internal API just for Jupyter, hence don't include in the official types. + */ + jupyter: { + registerHooks(): void; + }; + /** + * Internal API just for Tensorboard, hence don't include in the official types. + */ + tensorboard: { + registerHooks(): void; + }; + } & { /** * @deprecated Temporarily exposed for Pylance until we expose this API generally. Will be removed in an * iteration or two. @@ -44,7 +75,7 @@ export function buildApi( pylance: ApiForPylance; } & { /** - * @deprecated Use IExtensionApi.environments API instead. + * @deprecated Use PythonExtension.environments API instead. * * Return internal settings within the extension which are stored in VSCode storage */ @@ -60,7 +91,6 @@ export function buildApi( * @param {Resource} [resource] A resource for which the setting is asked for. * * When no resource is provided, the setting scoped to the first workspace folder is returned. * * If no folder is present, it returns the global setting. - * @returns {({ execCommand: string[] | undefined })} */ getExecutionDetails( resource?: Resource, @@ -87,6 +117,9 @@ export function buildApi( jupyter: { registerHooks: () => jupyterIntegration.integrateWithJupyterExtension(), }, + tensorboard: { + registerHooks: () => tensorboardIntegration.integrateWithTensorboardExtension(), + }, debug: { async getRemoteLauncherCommand( host: string, @@ -100,7 +133,7 @@ export function buildApi( }); }, async getDebuggerPackagePath(): Promise { - return getDebugpyPackagePath(); + return getDebugpyPath(); }, }, settings: { @@ -111,16 +144,6 @@ export function buildApi( return { execCommand: pythonPath === '' ? undefined : [pythonPath] }; }, }, - // These are for backwards compatibility. Other extensions are using these APIs and we don't want - // to force them to move to the jupyter extension ... yet. - datascience: { - registerRemoteServerProvider: jupyterIntegration - ? jupyterIntegration.registerRemoteServerProvider.bind(jupyterIntegration) - : ((noop as unknown) as (serverProvider: IJupyterUriProvider) => void), - showDataViewer: jupyterIntegration - ? jupyterIntegration.showDataViewer.bind(jupyterIntegration) - : ((noop as unknown) as (dataProvider: IDataViewerDataProvider, title: string) => Promise), - }, pylance: { createClient: (...args: any[]): BaseLanguageClient => { // Make sure we share output channel so that we can share one with @@ -134,7 +157,7 @@ export function buildApi( stop: (client: BaseLanguageClient): Promise => client.stop(), getTelemetryReporter: () => getTelemetryReporter(), }, - environments: buildEnvironmentApi(discoveryApi, serviceContainer), + environments, }; // In test environment return the DI Container. diff --git a/src/client/api/types.ts b/src/client/api/types.ts new file mode 100644 index 000000000000..95556aacbd90 --- /dev/null +++ b/src/client/api/types.ts @@ -0,0 +1,349 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { CancellationToken, Event, Uri, WorkspaceFolder, extensions } from 'vscode'; + +/* + * Do not introduce any breaking changes to this API. + * This is the public API for other extensions to interact with this extension. + */ +export interface PythonExtension { + /** + * Promise indicating whether all parts of the extension have completed loading or not. + */ + ready: Promise; + debug: { + /** + * Generate an array of strings for commands to pass to the Python executable to launch the debugger for remote debugging. + * Users can append another array of strings of what they want to execute along with relevant arguments to Python. + * E.g `['/Users/..../pythonVSCode/python_files/lib/python/debugpy', '--listen', 'localhost:57039', '--wait-for-client']` + * @param host + * @param port + * @param waitUntilDebuggerAttaches Defaults to `true`. + */ + getRemoteLauncherCommand(host: string, port: number, waitUntilDebuggerAttaches: boolean): Promise; + + /** + * Gets the path to the debugger package used by the extension. + */ + getDebuggerPackagePath(): Promise; + }; + + /** + * These APIs provide a way for extensions to work with by python environments available in the user's machine + * as found by the Python extension. See + * https://github.com/microsoft/vscode-python/wiki/Python-Environment-APIs for usage examples and more. + */ + readonly environments: { + /** + * Returns the environment configured by user in settings. Note that this can be an invalid environment, use + * {@link resolveEnvironment} to get full details. + * @param resource : Uri of a file or workspace folder. This is used to determine the env in a multi-root + * scenario. If `undefined`, then the API returns what ever is set for the workspace. + */ + getActiveEnvironmentPath(resource?: Resource): EnvironmentPath; + /** + * Sets the active environment path for the python extension for the resource. Configuration target will always + * be the workspace folder. + * @param environment : If string, it represents the full path to environment folder or python executable + * for the environment. Otherwise it can be {@link Environment} or {@link EnvironmentPath} itself. + * @param resource : [optional] File or workspace to scope to a particular workspace folder. + */ + updateActiveEnvironmentPath( + environment: string | EnvironmentPath | Environment, + resource?: Resource, + ): Promise; + /** + * This event is triggered when the active environment setting changes. + */ + readonly onDidChangeActiveEnvironmentPath: Event; + /** + * Carries environments known to the extension at the time of fetching the property. Note this may not + * contain all environments in the system as a refresh might be going on. + * + * Only reports environments in the current workspace. + */ + readonly known: readonly Environment[]; + /** + * This event is triggered when the known environment list changes, like when a environment + * is found, existing environment is removed, or some details changed on an environment. + */ + readonly onDidChangeEnvironments: Event; + /** + * This API will trigger environment discovery, but only if it has not already happened in this VSCode session. + * Useful for making sure env list is up-to-date when the caller needs it for the first time. + * + * To force trigger a refresh regardless of whether a refresh was already triggered, see option + * {@link RefreshOptions.forceRefresh}. + * + * Note that if there is a refresh already going on then this returns the promise for that refresh. + * @param options Additional options for refresh. + * @param token A cancellation token that indicates a refresh is no longer needed. + */ + refreshEnvironments(options?: RefreshOptions, token?: CancellationToken): Promise; + /** + * Returns details for the given environment, or `undefined` if the env is invalid. + * @param environment : If string, it represents the full path to environment folder or python executable + * for the environment. Otherwise it can be {@link Environment} or {@link EnvironmentPath} itself. + */ + resolveEnvironment( + environment: Environment | EnvironmentPath | string, + ): Promise; + /** + * Returns the environment variables used by the extension for a resource, which includes the custom + * variables configured by user in `.env` files. + * @param resource : Uri of a file or workspace folder. This is used to determine the env in a multi-root + * scenario. If `undefined`, then the API returns what ever is set for the workspace. + */ + getEnvironmentVariables(resource?: Resource): EnvironmentVariables; + /** + * This event is fired when the environment variables for a resource change. Note it's currently not + * possible to detect if environment variables in the system change, so this only fires if custom + * environment variables are updated in `.env` files. + */ + readonly onDidEnvironmentVariablesChange: Event; + }; +} + +export type RefreshOptions = { + /** + * When `true`, force trigger a refresh regardless of whether a refresh was already triggered. Note this can be expensive so + * it's best to only use it if user manually triggers a refresh. + */ + forceRefresh?: boolean; +}; + +/** + * Details about the environment. Note the environment folder, type and name never changes over time. + */ +export type Environment = EnvironmentPath & { + /** + * Carries details about python executable. + */ + readonly executable: { + /** + * Uri of the python interpreter/executable. Carries `undefined` in case an executable does not belong to + * the environment. + */ + readonly uri: Uri | undefined; + /** + * Bitness if known at this moment. + */ + readonly bitness: Bitness | undefined; + /** + * Value of `sys.prefix` in sys module if known at this moment. + */ + readonly sysPrefix: string | undefined; + }; + /** + * Carries details if it is an environment, otherwise `undefined` in case of global interpreters and others. + */ + readonly environment: + | { + /** + * Type of the environment. + */ + readonly type: EnvironmentType; + /** + * Name to the environment if any. + */ + readonly name: string | undefined; + /** + * Uri of the environment folder. + */ + readonly folderUri: Uri; + /** + * Any specific workspace folder this environment is created for. + */ + readonly workspaceFolder: WorkspaceFolder | undefined; + } + | undefined; + /** + * Carries Python version information known at this moment, carries `undefined` for envs without python. + */ + readonly version: + | (VersionInfo & { + /** + * Value of `sys.version` in sys module if known at this moment. + */ + readonly sysVersion: string | undefined; + }) + | undefined; + /** + * Tools/plugins which created the environment or where it came from. First value in array corresponds + * to the primary tool which manages the environment, which never changes over time. + * + * Array is empty if no tool is responsible for creating/managing the environment. Usually the case for + * global interpreters. + */ + readonly tools: readonly EnvironmentTools[]; +}; + +/** + * Derived form of {@link Environment} where certain properties can no longer be `undefined`. Meant to represent an + * {@link Environment} with complete information. + */ +export type ResolvedEnvironment = Environment & { + /** + * Carries complete details about python executable. + */ + readonly executable: { + /** + * Uri of the python interpreter/executable. Carries `undefined` in case an executable does not belong to + * the environment. + */ + readonly uri: Uri | undefined; + /** + * Bitness of the environment. + */ + readonly bitness: Bitness; + /** + * Value of `sys.prefix` in sys module. + */ + readonly sysPrefix: string; + }; + /** + * Carries complete Python version information, carries `undefined` for envs without python. + */ + readonly version: + | (ResolvedVersionInfo & { + /** + * Value of `sys.version` in sys module if known at this moment. + */ + readonly sysVersion: string; + }) + | undefined; +}; + +export type EnvironmentsChangeEvent = { + readonly env: Environment; + /** + * * "add": New environment is added. + * * "remove": Existing environment in the list is removed. + * * "update": New information found about existing environment. + */ + readonly type: 'add' | 'remove' | 'update'; +}; + +export type ActiveEnvironmentPathChangeEvent = EnvironmentPath & { + /** + * Resource the environment changed for. + */ + readonly resource: Resource | undefined; +}; + +/** + * Uri of a file inside a workspace or workspace folder itself. + */ +export type Resource = Uri | WorkspaceFolder; + +export type EnvironmentPath = { + /** + * The ID of the environment. + */ + readonly id: string; + /** + * Path to environment folder or path to python executable that uniquely identifies an environment. Environments + * lacking a python executable are identified by environment folder paths, whereas other envs can be identified + * using python executable path. + */ + readonly path: string; +}; + +/** + * Tool/plugin where the environment came from. It can be {@link KnownEnvironmentTools} or custom string which + * was contributed. + */ +export type EnvironmentTools = KnownEnvironmentTools | string; +/** + * Tools or plugins the Python extension currently has built-in support for. Note this list is expected to shrink + * once tools have their own separate extensions. + */ +export type KnownEnvironmentTools = + | 'Conda' + | 'Pipenv' + | 'Poetry' + | 'VirtualEnv' + | 'Venv' + | 'VirtualEnvWrapper' + | 'Pyenv' + | 'Hatch' + | 'Unknown'; + +/** + * Type of the environment. It can be {@link KnownEnvironmentTypes} or custom string which was contributed. + */ +export type EnvironmentType = KnownEnvironmentTypes | string; +/** + * Environment types the Python extension is aware of. Note this list is expected to shrink once tools have their + * own separate extensions, in which case they're expected to provide the type themselves. + */ +export type KnownEnvironmentTypes = 'VirtualEnvironment' | 'Conda' | 'Unknown'; + +/** + * Carries bitness for an environment. + */ +export type Bitness = '64-bit' | '32-bit' | 'Unknown'; + +/** + * The possible Python release levels. + */ +export type PythonReleaseLevel = 'alpha' | 'beta' | 'candidate' | 'final'; + +/** + * Release information for a Python version. + */ +export type PythonVersionRelease = { + readonly level: PythonReleaseLevel; + readonly serial: number; +}; + +export type VersionInfo = { + readonly major: number | undefined; + readonly minor: number | undefined; + readonly micro: number | undefined; + readonly release: PythonVersionRelease | undefined; +}; + +export type ResolvedVersionInfo = { + readonly major: number; + readonly minor: number; + readonly micro: number; + readonly release: PythonVersionRelease; +}; + +/** + * A record containing readonly keys. + */ +export type EnvironmentVariables = { readonly [key: string]: string | undefined }; + +export type EnvironmentVariablesChangeEvent = { + /** + * Workspace folder the environment variables changed for. + */ + readonly resource: WorkspaceFolder | undefined; + /** + * Updated value of environment variables. + */ + readonly env: EnvironmentVariables; +}; + +export const PVSC_EXTENSION_ID = 'ms-python.python'; + +// eslint-disable-next-line @typescript-eslint/no-namespace +export namespace PythonExtension { + /** + * Returns the API exposed by the Python extension in VS Code. + */ + export async function api(): Promise { + const extension = extensions.getExtension(PVSC_EXTENSION_ID); + if (extension === undefined) { + throw new Error(`Python extension is not installed or is disabled`); + } + if (!extension.isActive) { + await extension.activate(); + } + const pythonApi: PythonExtension = extension.exports; + return pythonApi; + } +} diff --git a/src/client/application/diagnostics/applicationDiagnostics.ts b/src/client/application/diagnostics/applicationDiagnostics.ts index 493c6cfece53..90d2ced8d0ae 100644 --- a/src/client/application/diagnostics/applicationDiagnostics.ts +++ b/src/client/application/diagnostics/applicationDiagnostics.ts @@ -9,7 +9,7 @@ import { Resource } from '../../common/types'; import { IServiceContainer } from '../../ioc/types'; import { traceLog, traceVerbose } from '../../logging'; import { IApplicationDiagnostics } from '../types'; -import { IDiagnostic, IDiagnosticsService, ISourceMapSupportService } from './types'; +import { IDiagnostic, IDiagnosticsService } from './types'; function log(diagnostics: IDiagnostic[]): void { diagnostics.forEach((item) => { @@ -43,9 +43,7 @@ async function runDiagnostics(diagnosticServices: IDiagnosticsService[], resourc export class ApplicationDiagnostics implements IApplicationDiagnostics { constructor(@inject(IServiceContainer) private readonly serviceContainer: IServiceContainer) {} - public register() { - this.serviceContainer.get(ISourceMapSupportService).register(); - } + public register() {} public async performPreStartupHealthCheck(resource: Resource): Promise { // When testing, do not perform health checks, as modal dialogs can be displayed. diff --git a/src/client/application/diagnostics/base.ts b/src/client/application/diagnostics/base.ts index 17bb7559ee77..8ce1c3b83184 100644 --- a/src/client/application/diagnostics/base.ts +++ b/src/client/application/diagnostics/base.ts @@ -73,11 +73,6 @@ export abstract class BaseDiagnosticsService implements IDiagnosticsService, IDi /** * Returns a key used to keep track of whether a diagnostic was handled or not. * So as to prevent handling/displaying messages multiple times for the same diagnostic. - * - * @protected - * @param {IDiagnostic} diagnostic - * @returns {string} - * @memberof BaseDiagnosticsService */ protected getDiagnosticsKey(diagnostic: IDiagnostic): string { if (diagnostic.scope === DiagnosticScope.Global) { diff --git a/src/client/application/diagnostics/checks/macPythonInterpreter.ts b/src/client/application/diagnostics/checks/macPythonInterpreter.ts index 19ccc2f8beb9..21d6b34fb7c5 100644 --- a/src/client/application/diagnostics/checks/macPythonInterpreter.ts +++ b/src/client/application/diagnostics/checks/macPythonInterpreter.ts @@ -40,7 +40,7 @@ export const InvalidMacPythonInterpreterServiceId = 'InvalidMacPythonInterpreter export class InvalidMacPythonInterpreterService extends BaseDiagnosticsService { protected changeThrottleTimeout = 1000; - private timeOut?: NodeJS.Timer | number; + private timeOut?: NodeJS.Timeout | number; constructor( @inject(IServiceContainer) serviceContainer: IServiceContainer, diff --git a/src/client/application/diagnostics/checks/pythonInterpreter.ts b/src/client/application/diagnostics/checks/pythonInterpreter.ts index 31da53e75357..9167e232a417 100644 --- a/src/client/application/diagnostics/checks/pythonInterpreter.ts +++ b/src/client/application/diagnostics/checks/pythonInterpreter.ts @@ -20,7 +20,7 @@ import { IDiagnosticHandlerService, IDiagnosticMessageOnCloseHandler, } from '../types'; -import { Common } from '../../../common/utils/localize'; +import { Common, Interpreters } from '../../../common/utils/localize'; import { Commands } from '../../../common/constants'; import { ICommandManager, IWorkspaceService } from '../../../common/application/types'; import { sendTelemetryEvent } from '../../../telemetry'; @@ -30,11 +30,12 @@ import { cache } from '../../../common/utils/decorators'; import { noop } from '../../../common/utils/misc'; import { getEnvironmentVariable, getOSType, OSType } from '../../../common/utils/platform'; import { IFileSystem } from '../../../common/platform/types'; -import { traceError } from '../../../logging'; +import { traceError, traceWarn } from '../../../logging'; import { getExecutable } from '../../../common/process/internal/python'; import { getSearchPathEnvVarNames } from '../../../common/utils/exec'; import { IProcessServiceFactory } from '../../../common/process/types'; import { normCasePath } from '../../../common/platform/fs-paths'; +import { useEnvExtension } from '../../../envExt/api.internal'; const messages = { [DiagnosticCodes.NoPythonInterpretersDiagnostic]: l10n.t( @@ -144,6 +145,9 @@ export class InvalidPythonInterpreterService extends BaseDiagnosticsService const isInterpreterSetToDefault = interpreterPathService.get(resource) === 'python'; if (!hasInterpreters && isInterpreterSetToDefault) { + if (useEnvExtension()) { + traceWarn(Interpreters.envExtDiscoveryNoEnvironments); + } return [ new InvalidPythonInterpreterDiagnostic( DiagnosticCodes.NoPythonInterpretersDiagnostic, @@ -156,6 +160,9 @@ export class InvalidPythonInterpreterService extends BaseDiagnosticsService const currentInterpreter = await interpreterService.getActiveInterpreter(resource); if (!currentInterpreter) { + if (useEnvExtension()) { + traceWarn(Interpreters.envExtNoActiveEnvironment); + } return [ new InvalidPythonInterpreterDiagnostic( DiagnosticCodes.InvalidPythonInterpreterDiagnostic, diff --git a/src/client/application/diagnostics/serviceRegistry.ts b/src/client/application/diagnostics/serviceRegistry.ts index 8d9b765939c9..acf460b88625 100644 --- a/src/client/application/diagnostics/serviceRegistry.ts +++ b/src/client/application/diagnostics/serviceRegistry.ts @@ -11,10 +11,6 @@ import { EnvironmentPathVariableDiagnosticsService, EnvironmentPathVariableDiagnosticsServiceId, } from './checks/envPathVariable'; -import { - InvalidLaunchJsonDebuggerService, - InvalidLaunchJsonDebuggerServiceId, -} from './checks/invalidLaunchJsonDebugger'; import { InvalidPythonPathInDebuggerService, InvalidPythonPathInDebuggerServiceId, @@ -59,11 +55,6 @@ export function registerTypes(serviceManager: IServiceManager): void { EnvironmentPathVariableDiagnosticsService, EnvironmentPathVariableDiagnosticsServiceId, ); - serviceManager.addSingleton( - IDiagnosticsService, - InvalidLaunchJsonDebuggerService, - InvalidLaunchJsonDebuggerServiceId, - ); serviceManager.addSingleton( IDiagnosticsService, InvalidPythonInterpreterService, diff --git a/src/client/application/diagnostics/surceMapSupportService.ts b/src/client/application/diagnostics/surceMapSupportService.ts deleted file mode 100644 index 8ff491e4cb06..000000000000 --- a/src/client/application/diagnostics/surceMapSupportService.ts +++ /dev/null @@ -1,46 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; - -import { inject, injectable } from 'inversify'; -import { ConfigurationTarget } from 'vscode'; -import { IApplicationShell, ICommandManager } from '../../common/application/types'; -import { Commands } from '../../common/constants'; -import { IConfigurationService, IDisposableRegistry } from '../../common/types'; -import { Diagnostics } from '../../common/utils/localize'; -import { ISourceMapSupportService } from './types'; - -@injectable() -export class SourceMapSupportService implements ISourceMapSupportService { - constructor( - @inject(ICommandManager) private readonly commandManager: ICommandManager, - @inject(IDisposableRegistry) private readonly disposables: IDisposableRegistry, - @inject(IConfigurationService) private readonly configurationService: IConfigurationService, - @inject(IApplicationShell) private readonly shell: IApplicationShell, - ) {} - public register(): void { - this.disposables.push( - this.commandManager.registerCommand(Commands.Enable_SourceMap_Support, this.onEnable, this), - ); - } - public async enable(): Promise { - await this.configurationService.updateSetting( - 'diagnostics.sourceMapsEnabled', - true, - undefined, - ConfigurationTarget.Global, - ); - await this.commandManager.executeCommand('workbench.action.reloadWindow'); - } - protected async onEnable(): Promise { - const enableSourceMapsAndReloadVSC = Diagnostics.enableSourceMapsAndReloadVSC; - const selection = await this.shell.showWarningMessage( - Diagnostics.warnBeforeEnablingSourceMaps, - enableSourceMapsAndReloadVSC, - ); - if (selection === enableSourceMapsAndReloadVSC) { - await this.enable(); - } - } -} diff --git a/src/client/application/diagnostics/types.ts b/src/client/application/diagnostics/types.ts index ced9930c81ab..1dc9a3c689df 100644 --- a/src/client/application/diagnostics/types.ts +++ b/src/client/application/diagnostics/types.ts @@ -64,8 +64,3 @@ export const IInvalidPythonPathInDebuggerService = Symbol('IInvalidPythonPathInD export interface IInvalidPythonPathInDebuggerService extends IDiagnosticsService { validatePythonPath(pythonPath?: string, pythonPathSource?: PythonPathSource, resource?: Uri): Promise; } -export const ISourceMapSupportService = Symbol('ISourceMapSupportService'); -export interface ISourceMapSupportService { - register(): void; - enable(): Promise; -} diff --git a/src/client/application/serviceRegistry.ts b/src/client/application/serviceRegistry.ts index 38773bd20198..ff5376d70b24 100644 --- a/src/client/application/serviceRegistry.ts +++ b/src/client/application/serviceRegistry.ts @@ -5,10 +5,7 @@ import { IServiceManager } from '../ioc/types'; import { registerTypes as diagnosticsRegisterTypes } from './diagnostics/serviceRegistry'; -import { SourceMapSupportService } from './diagnostics/surceMapSupportService'; -import { ISourceMapSupportService } from './diagnostics/types'; export function registerTypes(serviceManager: IServiceManager) { - serviceManager.addSingleton(ISourceMapSupportService, SourceMapSupportService); diagnosticsRegisterTypes(serviceManager); } diff --git a/src/client/application/types.ts b/src/client/application/types.ts index 460ac39807c8..cfd41f7b9746 100644 --- a/src/client/application/types.ts +++ b/src/client/application/types.ts @@ -11,8 +11,6 @@ export interface IApplicationDiagnostics { /** * Perform pre-extension activation health checks. * E.g. validate user environment, etc. - * @returns {Promise} - * @memberof IApplicationDiagnostics */ performPreStartupHealthCheck(resource: Resource): Promise; register(): void; diff --git a/src/client/browser/extension.ts b/src/client/browser/extension.ts index 28e1912f67e4..132618430551 100644 --- a/src/client/browser/extension.ts +++ b/src/client/browser/extension.ts @@ -108,7 +108,7 @@ async function runPylance( middleware, }; - const client = new LanguageClient('python', 'Python Language Server', clientOptions, worker); + const client = new LanguageClient('python', 'Python Language Server', worker, clientOptions); languageClient = client; context.subscriptions.push( @@ -139,7 +139,7 @@ async function runPylance( await client.start(); } catch (e) { - console.log(e); + console.log(e); // necessary to use console.log for browser } } @@ -200,7 +200,7 @@ function sendTelemetryEventBrowser( break; } } catch (exception) { - console.error(`Failed to serialize ${prop} for ${eventName}`, exception); + console.error(`Failed to serialize ${prop} for ${eventName}`, exception); // necessary to use console.log for browser } }); } diff --git a/src/client/chat/baseTool.ts b/src/client/chat/baseTool.ts new file mode 100644 index 000000000000..d8e2e1d60d42 --- /dev/null +++ b/src/client/chat/baseTool.ts @@ -0,0 +1,80 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { + CancellationToken, + LanguageModelTextPart, + LanguageModelTool, + LanguageModelToolInvocationOptions, + LanguageModelToolInvocationPrepareOptions, + LanguageModelToolResult, + PreparedToolInvocation, + Uri, + workspace, +} from 'vscode'; +import { IResourceReference, isCancellationError, resolveFilePath } from './utils'; +import { ErrorWithTelemetrySafeReason } from '../common/errors/errorUtils'; +import { sendTelemetryEvent } from '../telemetry'; +import { EventName } from '../telemetry/constants'; +import { StopWatch } from '../common/utils/stopWatch'; + +export abstract class BaseTool implements LanguageModelTool { + protected extraTelemetryProperties: Record = {}; + constructor(private readonly toolName: string) {} + + async invoke( + options: LanguageModelToolInvocationOptions, + token: CancellationToken, + ): Promise { + if (!workspace.isTrusted) { + return new LanguageModelToolResult([ + new LanguageModelTextPart('Cannot use this tool in an untrusted workspace.'), + ]); + } + this.extraTelemetryProperties = {}; + let error: Error | undefined; + const resource = resolveFilePath(options.input.resourcePath); + const stopWatch = new StopWatch(); + try { + return await this.invokeImpl(options, resource, token); + } catch (ex) { + error = ex as any; + throw ex; + } finally { + const isCancelled = token.isCancellationRequested || (error ? isCancellationError(error) : false); + const failed = !!error || isCancelled; + const failureCategory = isCancelled + ? 'cancelled' + : error + ? error instanceof ErrorWithTelemetrySafeReason + ? error.telemetrySafeReason + : 'error' + : undefined; + sendTelemetryEvent(EventName.INVOKE_TOOL, stopWatch.elapsedTime, { + toolName: this.toolName, + failed, + failureCategory, + ...this.extraTelemetryProperties, + }); + } + } + protected abstract invokeImpl( + options: LanguageModelToolInvocationOptions, + resource: Uri | undefined, + token: CancellationToken, + ): Promise; + + async prepareInvocation( + options: LanguageModelToolInvocationPrepareOptions, + token: CancellationToken, + ): Promise { + const resource = resolveFilePath(options.input.resourcePath); + return this.prepareInvocationImpl(options, resource, token); + } + + protected abstract prepareInvocationImpl( + options: LanguageModelToolInvocationPrepareOptions, + resource: Uri | undefined, + token: CancellationToken, + ): Promise; +} diff --git a/src/client/chat/configurePythonEnvTool.ts b/src/client/chat/configurePythonEnvTool.ts new file mode 100644 index 000000000000..914a92f81c52 --- /dev/null +++ b/src/client/chat/configurePythonEnvTool.ts @@ -0,0 +1,131 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { + CancellationToken, + LanguageModelTool, + LanguageModelToolInvocationOptions, + LanguageModelToolInvocationPrepareOptions, + LanguageModelToolResult, + PreparedToolInvocation, + Uri, + workspace, + lm, +} from 'vscode'; +import { PythonExtension } from '../api/types'; +import { IServiceContainer } from '../ioc/types'; +import { ICodeExecutionService } from '../terminals/types'; +import { TerminalCodeExecutionProvider } from '../terminals/codeExecution/terminalCodeExecution'; +import { + getEnvDetailsForResponse, + getEnvTypeForTelemetry, + getToolResponseIfNotebook, + IResourceReference, + isCancellationError, + raceCancellationError, +} from './utils'; +import { ITerminalHelper } from '../common/terminal/types'; +import { IRecommendedEnvironmentService } from '../interpreter/configuration/types'; +import { CreateVirtualEnvTool } from './createVirtualEnvTool'; +import { ISelectPythonEnvToolArguments, SelectPythonEnvTool } from './selectEnvTool'; +import { BaseTool } from './baseTool'; + +export class ConfigurePythonEnvTool extends BaseTool + implements LanguageModelTool { + private readonly terminalExecutionService: TerminalCodeExecutionProvider; + private readonly terminalHelper: ITerminalHelper; + private readonly recommendedEnvService: IRecommendedEnvironmentService; + public static readonly toolName = 'configure_python_environment'; + constructor( + private readonly api: PythonExtension['environments'], + private readonly serviceContainer: IServiceContainer, + private readonly createEnvTool: CreateVirtualEnvTool, + ) { + super(ConfigurePythonEnvTool.toolName); + this.terminalExecutionService = this.serviceContainer.get( + ICodeExecutionService, + 'standard', + ); + this.terminalHelper = this.serviceContainer.get(ITerminalHelper); + this.recommendedEnvService = this.serviceContainer.get( + IRecommendedEnvironmentService, + ); + } + + async invokeImpl( + options: LanguageModelToolInvocationOptions, + resource: Uri | undefined, + token: CancellationToken, + ): Promise { + const notebookResponse = getToolResponseIfNotebook(resource); + if (notebookResponse) { + this.extraTelemetryProperties.resolveOutcome = 'notebook'; + return notebookResponse; + } + + const workspaceSpecificEnv = await raceCancellationError( + this.hasAlreadyGotAWorkspaceSpecificEnvironment(resource), + token, + ); + + if (workspaceSpecificEnv) { + this.extraTelemetryProperties.resolveOutcome = 'existingWorkspaceEnv'; + this.extraTelemetryProperties.envType = getEnvTypeForTelemetry(workspaceSpecificEnv); + return getEnvDetailsForResponse( + workspaceSpecificEnv, + this.api, + this.terminalExecutionService, + this.terminalHelper, + resource, + token, + ); + } + + if (await this.createEnvTool.shouldCreateNewVirtualEnv(resource, token)) { + try { + const result = await lm.invokeTool(CreateVirtualEnvTool.toolName, options, token); + this.extraTelemetryProperties.resolveOutcome = 'createdVirtualEnv'; + return result; + } catch (ex) { + if (isCancellationError(ex)) { + const input: ISelectPythonEnvToolArguments = { + ...options.input, + reason: 'cancelled', + }; + // If the user cancelled the tool, then we should invoke the select env tool. + this.extraTelemetryProperties.resolveOutcome = 'selectedEnvAfterCancelledCreate'; + return lm.invokeTool(SelectPythonEnvTool.toolName, { ...options, input }, token); + } + throw ex; + } + } else { + const input: ISelectPythonEnvToolArguments = { + ...options.input, + }; + this.extraTelemetryProperties.resolveOutcome = 'selectedEnv'; + return lm.invokeTool(SelectPythonEnvTool.toolName, { ...options, input }, token); + } + } + + async prepareInvocationImpl( + _options: LanguageModelToolInvocationPrepareOptions, + _resource: Uri | undefined, + _token: CancellationToken, + ): Promise { + return { + invocationMessage: 'Configuring a Python Environment', + }; + } + + async hasAlreadyGotAWorkspaceSpecificEnvironment(resource: Uri | undefined) { + const recommededEnv = await this.recommendedEnvService.getRecommededEnvironment(resource); + // Already selected workspace env, hence nothing to do. + if (recommededEnv?.reason === 'workspaceUserSelected' && workspace.workspaceFolders?.length) { + return recommededEnv.environment; + } + // No workspace folders, and the user selected a global environment. + if (recommededEnv?.reason === 'globalUserSelected' && !workspace.workspaceFolders?.length) { + return recommededEnv.environment; + } + } +} diff --git a/src/client/chat/createVirtualEnvTool.ts b/src/client/chat/createVirtualEnvTool.ts new file mode 100644 index 000000000000..56760d2b4bef --- /dev/null +++ b/src/client/chat/createVirtualEnvTool.ts @@ -0,0 +1,246 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { + CancellationError, + CancellationToken, + commands, + l10n, + LanguageModelTool, + LanguageModelToolInvocationOptions, + LanguageModelToolInvocationPrepareOptions, + LanguageModelToolResult, + PreparedToolInvocation, + Uri, + workspace, +} from 'vscode'; +import { PythonExtension, ResolvedEnvironment } from '../api/types'; +import { IServiceContainer } from '../ioc/types'; +import { ICodeExecutionService } from '../terminals/types'; +import { TerminalCodeExecutionProvider } from '../terminals/codeExecution/terminalCodeExecution'; +import { + doesWorkspaceHaveVenvOrCondaEnv, + getDisplayVersion, + getEnvDetailsForResponse, + IResourceReference, + isCancellationError, + raceCancellationError, +} from './utils'; +import { ITerminalHelper } from '../common/terminal/types'; +import { raceTimeout, sleep } from '../common/utils/async'; +import { IInterpreterPathService } from '../common/types'; +import { DisposableStore } from '../common/utils/resourceLifecycle'; +import { IRecommendedEnvironmentService } from '../interpreter/configuration/types'; +import { EnvironmentType } from '../pythonEnvironments/info'; +import { IDiscoveryAPI } from '../pythonEnvironments/base/locator'; +import { convertEnvInfoToPythonEnvironment } from '../pythonEnvironments/legacyIOC'; +import { sortInterpreters } from '../interpreter/helpers'; +import { isStableVersion } from '../pythonEnvironments/info/pythonVersion'; +import { createVirtualEnvironment } from '../pythonEnvironments/creation/createEnvApi'; +import { traceError, traceVerbose, traceWarn } from '../logging'; +import { StopWatch } from '../common/utils/stopWatch'; +import { useEnvExtension } from '../envExt/api.internal'; +import { PythonEnvironment } from '../envExt/types'; +import { hideEnvCreation } from '../pythonEnvironments/creation/provider/hideEnvCreation'; +import { BaseTool } from './baseTool'; + +interface ICreateVirtualEnvToolParams extends IResourceReference { + packageList?: string[]; // Added only becausewe have ability to create a virtual env with list of packages same tool within the in Python Env extension. +} + +export class CreateVirtualEnvTool extends BaseTool + implements LanguageModelTool { + private readonly terminalExecutionService: TerminalCodeExecutionProvider; + private readonly terminalHelper: ITerminalHelper; + private readonly recommendedEnvService: IRecommendedEnvironmentService; + + public static readonly toolName = 'create_virtual_environment'; + constructor( + private readonly discoveryApi: IDiscoveryAPI, + private readonly api: PythonExtension['environments'], + private readonly serviceContainer: IServiceContainer, + ) { + super(CreateVirtualEnvTool.toolName); + this.terminalExecutionService = this.serviceContainer.get( + ICodeExecutionService, + 'standard', + ); + this.terminalHelper = this.serviceContainer.get(ITerminalHelper); + this.recommendedEnvService = this.serviceContainer.get( + IRecommendedEnvironmentService, + ); + } + + async invokeImpl( + options: LanguageModelToolInvocationOptions, + resource: Uri | undefined, + token: CancellationToken, + ): Promise { + let info = await this.getPreferredEnvForCreation(resource); + if (!info) { + traceWarn(`Called ${CreateVirtualEnvTool.toolName} tool not invoked, no preferred environment found.`); + throw new CancellationError(); + } + const { workspaceFolder, preferredGlobalPythonEnv } = info; + const interpreterPathService = this.serviceContainer.get(IInterpreterPathService); + const disposables = new DisposableStore(); + try { + disposables.add(hideEnvCreation()); + const interpreterChanged = new Promise((resolve) => { + disposables.add(interpreterPathService.onDidChange(() => resolve())); + }); + + let createdEnvPath: string | undefined = undefined; + if (useEnvExtension()) { + const result: PythonEnvironment | undefined = await commands.executeCommand('python-envs.createAny', { + quickCreate: true, + additionalPackages: options.input.packageList || [], + uri: workspaceFolder.uri, + selectEnvironment: true, + }); + createdEnvPath = result?.environmentPath.fsPath; + } else { + const created = await raceCancellationError( + createVirtualEnvironment({ + interpreter: preferredGlobalPythonEnv.id, + workspaceFolder, + }), + token, + ); + createdEnvPath = created?.path; + } + if (!createdEnvPath) { + traceWarn(`${CreateVirtualEnvTool.toolName} tool not invoked, virtual env not created.`); + throw new CancellationError(); + } + + // Wait a few secs to ensure the env is selected as the active environment.. + // If this doesn't work, then something went wrong. + await raceTimeout(5_000, interpreterChanged); + + const stopWatch = new StopWatch(); + let env: ResolvedEnvironment | undefined; + while (stopWatch.elapsedTime < 5_000 || !env) { + env = await this.api.resolveEnvironment(createdEnvPath); + if (env) { + break; + } else { + traceVerbose( + `${CreateVirtualEnvTool.toolName} tool invoked, env created but not yet resolved, waiting...`, + ); + await sleep(200); + } + } + if (!env) { + traceError(`${CreateVirtualEnvTool.toolName} tool invoked, env created but unable to resolve details.`); + throw new CancellationError(); + } + return await getEnvDetailsForResponse( + env, + this.api, + this.terminalExecutionService, + this.terminalHelper, + resource, + token, + ); + } catch (ex) { + if (!isCancellationError(ex)) { + traceError( + `${ + CreateVirtualEnvTool.toolName + } tool failed to create virtual environment for resource ${resource?.toString()}`, + ex, + ); + } + throw ex; + } finally { + disposables.dispose(); + } + } + + public async shouldCreateNewVirtualEnv(resource: Uri | undefined, token: CancellationToken): Promise { + if (doesWorkspaceHaveVenvOrCondaEnv(resource, this.api)) { + // If we already have a .venv or .conda in this workspace, then do not prompt to create a virtual environment. + return false; + } + + const info = await raceCancellationError(this.getPreferredEnvForCreation(resource), token); + return info ? true : false; + } + + async prepareInvocationImpl( + _options: LanguageModelToolInvocationPrepareOptions, + resource: Uri | undefined, + token: CancellationToken, + ): Promise { + const info = await raceCancellationError(this.getPreferredEnvForCreation(resource), token); + if (!info) { + return {}; + } + const { preferredGlobalPythonEnv } = info; + const version = getDisplayVersion(preferredGlobalPythonEnv.version); + return { + confirmationMessages: { + title: l10n.t('Create a Virtual Environment{0}?', version ? ` (${version})` : ''), + message: l10n.t(`Virtual Environments provide the benefit of package isolation and more.`), + }, + invocationMessage: l10n.t('Creating a Virtual Environment'), + }; + } + async hasAlreadyGotAWorkspaceSpecificEnvironment(resource: Uri | undefined) { + const recommededEnv = await this.recommendedEnvService.getRecommededEnvironment(resource); + // Already selected workspace env, hence nothing to do. + if (recommededEnv?.reason === 'workspaceUserSelected' && workspace.workspaceFolders?.length) { + return recommededEnv.environment; + } + // No workspace folders, and the user selected a global environment. + if (recommededEnv?.reason === 'globalUserSelected' && !workspace.workspaceFolders?.length) { + return recommededEnv.environment; + } + } + + private async getPreferredEnvForCreation(resource: Uri | undefined) { + if (await this.hasAlreadyGotAWorkspaceSpecificEnvironment(resource)) { + return undefined; + } + + // If we have a resource or have only one workspace folder && there is no .venv and no workspace specific environment. + // Then lets recommend creating a virtual environment. + const workspaceFolder = + resource && workspace.workspaceFolders?.length + ? workspace.getWorkspaceFolder(resource) + : workspace.workspaceFolders?.length === 1 + ? workspace.workspaceFolders[0] + : undefined; + if (!workspaceFolder) { + // No workspace folder, hence no need to create a virtual environment. + return undefined; + } + + // Find the latest stable version of Python from the list of know envs. + let globalPythonEnvs = this.discoveryApi + .getEnvs() + .map((env) => convertEnvInfoToPythonEnvironment(env)) + .filter((env) => + [ + EnvironmentType.System, + EnvironmentType.MicrosoftStore, + EnvironmentType.Global, + EnvironmentType.Pyenv, + ].includes(env.envType), + ) + .filter((env) => env.version && isStableVersion(env.version)); + + globalPythonEnvs = sortInterpreters(globalPythonEnvs); + const preferredGlobalPythonEnv = globalPythonEnvs.length + ? this.api.known.find((e) => e.id === globalPythonEnvs[globalPythonEnvs.length - 1].id) + : undefined; + + return workspaceFolder && preferredGlobalPythonEnv + ? { + workspaceFolder, + preferredGlobalPythonEnv, + } + : undefined; + } +} diff --git a/src/client/chat/getExecutableTool.ts b/src/client/chat/getExecutableTool.ts new file mode 100644 index 000000000000..38dabce644a7 --- /dev/null +++ b/src/client/chat/getExecutableTool.ts @@ -0,0 +1,90 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { + CancellationToken, + l10n, + LanguageModelTextPart, + LanguageModelTool, + LanguageModelToolInvocationOptions, + LanguageModelToolInvocationPrepareOptions, + LanguageModelToolResult, + PreparedToolInvocation, + Uri, +} from 'vscode'; +import { PythonExtension } from '../api/types'; +import { IServiceContainer } from '../ioc/types'; +import { ICodeExecutionService } from '../terminals/types'; +import { TerminalCodeExecutionProvider } from '../terminals/codeExecution/terminalCodeExecution'; +import { + getEnvDisplayName, + getEnvironmentDetails, + getEnvTypeForTelemetry, + getToolResponseIfNotebook, + IResourceReference, + raceCancellationError, +} from './utils'; +import { ITerminalHelper } from '../common/terminal/types'; +import { IDiscoveryAPI } from '../pythonEnvironments/base/locator'; +import { BaseTool } from './baseTool'; + +export class GetExecutableTool extends BaseTool implements LanguageModelTool { + private readonly terminalExecutionService: TerminalCodeExecutionProvider; + private readonly terminalHelper: ITerminalHelper; + public static readonly toolName = 'get_python_executable_details'; + constructor( + private readonly api: PythonExtension['environments'], + private readonly serviceContainer: IServiceContainer, + private readonly discovery: IDiscoveryAPI, + ) { + super(GetExecutableTool.toolName); + this.terminalExecutionService = this.serviceContainer.get( + ICodeExecutionService, + 'standard', + ); + this.terminalHelper = this.serviceContainer.get(ITerminalHelper); + } + async invokeImpl( + _options: LanguageModelToolInvocationOptions, + resourcePath: Uri | undefined, + token: CancellationToken, + ): Promise { + const notebookResponse = getToolResponseIfNotebook(resourcePath); + if (notebookResponse) { + return notebookResponse; + } + + const envPath = this.api.getActiveEnvironmentPath(resourcePath); + const environment = await raceCancellationError(this.api.resolveEnvironment(envPath), token); + if (environment) { + this.extraTelemetryProperties.envType = getEnvTypeForTelemetry(environment); + } + + const message = await getEnvironmentDetails( + resourcePath, + this.api, + this.terminalExecutionService, + this.terminalHelper, + undefined, + token, + ); + return new LanguageModelToolResult([new LanguageModelTextPart(message)]); + } + + async prepareInvocationImpl( + _options: LanguageModelToolInvocationPrepareOptions, + resourcePath: Uri | undefined, + token: CancellationToken, + ): Promise { + if (getToolResponseIfNotebook(resourcePath)) { + return {}; + } + + const envName = await raceCancellationError(getEnvDisplayName(this.discovery, resourcePath, this.api), token); + return { + invocationMessage: envName + ? l10n.t('Fetching Python executable information for {0}', envName) + : l10n.t('Fetching Python executable information'), + }; + } +} diff --git a/src/client/chat/getPythonEnvTool.ts b/src/client/chat/getPythonEnvTool.ts new file mode 100644 index 000000000000..d25d72baeba8 --- /dev/null +++ b/src/client/chat/getPythonEnvTool.ts @@ -0,0 +1,131 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { + CancellationToken, + l10n, + LanguageModelTextPart, + LanguageModelTool, + LanguageModelToolInvocationOptions, + LanguageModelToolInvocationPrepareOptions, + LanguageModelToolResult, + PreparedToolInvocation, + Uri, +} from 'vscode'; +import { PythonExtension } from '../api/types'; +import { IServiceContainer } from '../ioc/types'; +import { ICodeExecutionService } from '../terminals/types'; +import { TerminalCodeExecutionProvider } from '../terminals/codeExecution/terminalCodeExecution'; +import { IProcessServiceFactory, IPythonExecutionFactory } from '../common/process/types'; +import { + getEnvironmentDetails, + getEnvTypeForTelemetry, + getToolResponseIfNotebook, + IResourceReference, + raceCancellationError, +} from './utils'; +import { getPythonPackagesResponse } from './listPackagesTool'; +import { ITerminalHelper } from '../common/terminal/types'; +import { getEnvExtApi, useEnvExtension } from '../envExt/api.internal'; +import { ErrorWithTelemetrySafeReason } from '../common/errors/errorUtils'; +import { BaseTool } from './baseTool'; + +export class GetEnvironmentInfoTool extends BaseTool + implements LanguageModelTool { + private readonly terminalExecutionService: TerminalCodeExecutionProvider; + private readonly pythonExecFactory: IPythonExecutionFactory; + private readonly processServiceFactory: IProcessServiceFactory; + private readonly terminalHelper: ITerminalHelper; + public static readonly toolName = 'get_python_environment_details'; + constructor( + private readonly api: PythonExtension['environments'], + private readonly serviceContainer: IServiceContainer, + ) { + super(GetEnvironmentInfoTool.toolName); + this.terminalExecutionService = this.serviceContainer.get( + ICodeExecutionService, + 'standard', + ); + this.pythonExecFactory = this.serviceContainer.get(IPythonExecutionFactory); + this.processServiceFactory = this.serviceContainer.get(IProcessServiceFactory); + this.terminalHelper = this.serviceContainer.get(ITerminalHelper); + } + + async invokeImpl( + _options: LanguageModelToolInvocationOptions, + resourcePath: Uri | undefined, + token: CancellationToken, + ): Promise { + const notebookResponse = getToolResponseIfNotebook(resourcePath); + if (notebookResponse) { + return notebookResponse; + } + + // environment + const envPath = this.api.getActiveEnvironmentPath(resourcePath); + const environment = await raceCancellationError(this.api.resolveEnvironment(envPath), token); + if (!environment || !environment.version) { + throw new ErrorWithTelemetrySafeReason( + 'No environment found for the provided resource path: ' + resourcePath?.fsPath, + 'noEnvFound', + ); + } + this.extraTelemetryProperties.envType = getEnvTypeForTelemetry(environment); + + let packages = ''; + let responsePackageCount = 0; + if (useEnvExtension()) { + const api = await getEnvExtApi(); + const env = await api.getEnvironment(resourcePath); + const pkgs = env ? await api.getPackages(env) : []; + if (pkgs && pkgs.length > 0) { + responsePackageCount = pkgs.length; + // Installed Python packages, each in the format or (). The version may be omitted if unknown. Returns an empty array if no packages are installed. + const response = [ + 'Below is a list of the Python packages, each in the format or (). The version may be omitted if unknown: ', + ]; + pkgs.forEach((pkg) => { + const version = pkg.version; + response.push(version ? `- ${pkg.name} (${version})` : `- ${pkg.name}`); + }); + packages = response.join('\n'); + } + } + if (!packages) { + packages = await getPythonPackagesResponse( + environment, + this.pythonExecFactory, + this.processServiceFactory, + resourcePath, + token, + ); + // Count lines starting with '- ' to get the number of packages + responsePackageCount = (packages.match(/^- /gm) || []).length; + } + this.extraTelemetryProperties.responsePackageCount = String(responsePackageCount); + const message = await getEnvironmentDetails( + resourcePath, + this.api, + this.terminalExecutionService, + this.terminalHelper, + packages, + token, + ); + + return new LanguageModelToolResult([new LanguageModelTextPart(message)]); + } + + async prepareInvocationImpl( + _options: LanguageModelToolInvocationPrepareOptions, + resourcePath: Uri | undefined, + _token: CancellationToken, + ): Promise { + if (getToolResponseIfNotebook(resourcePath)) { + return {}; + } + + return { + invocationMessage: l10n.t('Fetching Python environment information'), + }; + } +} diff --git a/src/client/chat/index.ts b/src/client/chat/index.ts new file mode 100644 index 000000000000..b548860eaae3 --- /dev/null +++ b/src/client/chat/index.ts @@ -0,0 +1,52 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { lm } from 'vscode'; +import { PythonExtension } from '../api/types'; +import { IServiceContainer } from '../ioc/types'; +import { InstallPackagesTool } from './installPackagesTool'; +import { IExtensionContext } from '../common/types'; +import { DisposableStore } from '../common/utils/resourceLifecycle'; +import { IDiscoveryAPI } from '../pythonEnvironments/base/locator'; +import { GetExecutableTool } from './getExecutableTool'; +import { GetEnvironmentInfoTool } from './getPythonEnvTool'; +import { ConfigurePythonEnvTool } from './configurePythonEnvTool'; +import { SelectPythonEnvTool } from './selectEnvTool'; +import { CreateVirtualEnvTool } from './createVirtualEnvTool'; + +export function registerTools( + context: IExtensionContext, + discoverApi: IDiscoveryAPI, + environmentsApi: PythonExtension['environments'], + serviceContainer: IServiceContainer, +) { + const ourTools = new DisposableStore(); + context.subscriptions.push(ourTools); + + ourTools.add( + lm.registerTool(GetEnvironmentInfoTool.toolName, new GetEnvironmentInfoTool(environmentsApi, serviceContainer)), + ); + ourTools.add( + lm.registerTool( + GetExecutableTool.toolName, + new GetExecutableTool(environmentsApi, serviceContainer, discoverApi), + ), + ); + ourTools.add( + lm.registerTool( + InstallPackagesTool.toolName, + new InstallPackagesTool(environmentsApi, serviceContainer, discoverApi), + ), + ); + const createVirtualEnvTool = new CreateVirtualEnvTool(discoverApi, environmentsApi, serviceContainer); + ourTools.add(lm.registerTool(CreateVirtualEnvTool.toolName, createVirtualEnvTool)); + ourTools.add( + lm.registerTool(SelectPythonEnvTool.toolName, new SelectPythonEnvTool(environmentsApi, serviceContainer)), + ); + ourTools.add( + lm.registerTool( + ConfigurePythonEnvTool.toolName, + new ConfigurePythonEnvTool(environmentsApi, serviceContainer, createVirtualEnvTool), + ), + ); +} diff --git a/src/client/chat/installPackagesTool.ts b/src/client/chat/installPackagesTool.ts new file mode 100644 index 000000000000..5d3d456361f9 --- /dev/null +++ b/src/client/chat/installPackagesTool.ts @@ -0,0 +1,167 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { + CancellationToken, + l10n, + LanguageModelTextPart, + LanguageModelTool, + LanguageModelToolInvocationOptions, + LanguageModelToolInvocationPrepareOptions, + LanguageModelToolResult, + PreparedToolInvocation, + Uri, +} from 'vscode'; +import { PythonExtension } from '../api/types'; +import { IServiceContainer } from '../ioc/types'; +import { + getEnvDisplayName, + getEnvTypeForTelemetry, + getToolResponseIfNotebook, + IResourceReference, + isCancellationError, + isCondaEnv, + raceCancellationError, +} from './utils'; +import { IModuleInstaller } from '../common/installer/types'; +import { ModuleInstallerType } from '../pythonEnvironments/info'; +import { IDiscoveryAPI } from '../pythonEnvironments/base/locator'; +import { getEnvExtApi, useEnvExtension } from '../envExt/api.internal'; +import { ErrorWithTelemetrySafeReason } from '../common/errors/errorUtils'; +import { BaseTool } from './baseTool'; + +export interface IInstallPackageArgs extends IResourceReference { + packageList: string[]; +} + +export class InstallPackagesTool extends BaseTool + implements LanguageModelTool { + public static readonly toolName = 'install_python_packages'; + constructor( + private readonly api: PythonExtension['environments'], + private readonly serviceContainer: IServiceContainer, + private readonly discovery: IDiscoveryAPI, + ) { + super(InstallPackagesTool.toolName); + } + + async invokeImpl( + options: LanguageModelToolInvocationOptions, + resourcePath: Uri | undefined, + token: CancellationToken, + ): Promise { + const packageCount = options.input.packageList.length; + const packagePlurality = packageCount === 1 ? 'package' : 'packages'; + this.extraTelemetryProperties.packageCount = String(packageCount); + const notebookResponse = getToolResponseIfNotebook(resourcePath); + if (notebookResponse) { + return notebookResponse; + } + + if (useEnvExtension()) { + const api = await getEnvExtApi(); + const env = await api.getEnvironment(resourcePath); + if (env) { + await raceCancellationError(api.managePackages(env, { install: options.input.packageList }), token); + const resultMessage = `Successfully installed ${packagePlurality}: ${options.input.packageList.join( + ', ', + )}`; + return new LanguageModelToolResult([new LanguageModelTextPart(resultMessage)]); + } else { + return new LanguageModelToolResult([ + new LanguageModelTextPart( + `Packages not installed. No environment found for: ${resourcePath?.fsPath}`, + ), + ]); + } + } + + try { + // environment + const envPath = this.api.getActiveEnvironmentPath(resourcePath); + const environment = await raceCancellationError(this.api.resolveEnvironment(envPath), token); + if (!environment || !environment.version) { + throw new ErrorWithTelemetrySafeReason( + 'No environment found for the provided resource path: ' + resourcePath?.fsPath, + 'noEnvFound', + ); + } + this.extraTelemetryProperties.envType = getEnvTypeForTelemetry(environment); + const isConda = isCondaEnv(environment); + const installers = this.serviceContainer.getAll(IModuleInstaller); + const installerType = isConda ? ModuleInstallerType.Conda : ModuleInstallerType.Pip; + this.extraTelemetryProperties.installerType = isConda ? 'conda' : 'pip'; + const installer = installers.find((i) => i.type === installerType); + if (!installer) { + throw new ErrorWithTelemetrySafeReason( + `No installer found for the environment type: ${installerType}`, + 'noInstallerFound', + ); + } + if (!installer.isSupported(resourcePath)) { + throw new ErrorWithTelemetrySafeReason( + `Installer ${installerType} not supported for the environment type: ${installerType}`, + 'installerNotSupported', + ); + } + for (const packageName of options.input.packageList) { + await installer.installModule(packageName, resourcePath, token, undefined, { + installAsProcess: true, + hideProgress: true, + }); + } + // format and return + const resultMessage = `Successfully installed ${packagePlurality}: ${options.input.packageList.join(', ')}`; + return new LanguageModelToolResult([new LanguageModelTextPart(resultMessage)]); + } catch (error) { + if (isCancellationError(error)) { + throw error; + } + const errorMessage = `An error occurred while installing ${packagePlurality}: ${error}`; + return new LanguageModelToolResult([new LanguageModelTextPart(errorMessage)]); + } + } + + async prepareInvocationImpl( + options: LanguageModelToolInvocationPrepareOptions, + resourcePath: Uri | undefined, + token: CancellationToken, + ): Promise { + const packageCount = options.input.packageList.length; + if (getToolResponseIfNotebook(resourcePath)) { + return {}; + } + + const envName = await raceCancellationError(getEnvDisplayName(this.discovery, resourcePath, this.api), token); + let title = ''; + let invocationMessage = ''; + const message = + packageCount === 1 + ? '' + : l10n.t(`The following packages will be installed: {0}`, options.input.packageList.sort().join(', ')); + if (envName) { + title = + packageCount === 1 + ? l10n.t(`Install {0} in {1}?`, options.input.packageList[0], envName) + : l10n.t(`Install packages in {0}?`, envName); + invocationMessage = + packageCount === 1 + ? l10n.t(`Installing {0} in {1}`, options.input.packageList[0], envName) + : l10n.t(`Installing packages {0} in {1}`, options.input.packageList.sort().join(', '), envName); + } else { + title = + options.input.packageList.length === 1 + ? l10n.t(`Install Python package '{0}'?`, options.input.packageList[0]) + : l10n.t(`Install Python packages?`); + invocationMessage = + packageCount === 1 + ? l10n.t(`Installing Python package '{0}'`, options.input.packageList[0]) + : l10n.t(`Installing Python packages: {0}`, options.input.packageList.sort().join(', ')); + } + + return { + confirmationMessages: { title, message }, + invocationMessage, + }; + } +} diff --git a/src/client/chat/listPackagesTool.ts b/src/client/chat/listPackagesTool.ts new file mode 100644 index 000000000000..fcae831cfe2f --- /dev/null +++ b/src/client/chat/listPackagesTool.ts @@ -0,0 +1,95 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { CancellationToken, Uri } from 'vscode'; +import { ResolvedEnvironment } from '../api/types'; +import { IProcessService, IProcessServiceFactory, IPythonExecutionFactory } from '../common/process/types'; +import { isCondaEnv, raceCancellationError } from './utils'; +import { parsePipList } from './pipListUtils'; +import { Conda } from '../pythonEnvironments/common/environmentManagers/conda'; +import { traceError } from '../logging'; + +export async function getPythonPackagesResponse( + environment: ResolvedEnvironment, + pythonExecFactory: IPythonExecutionFactory, + processServiceFactory: IProcessServiceFactory, + resourcePath: Uri | undefined, + token: CancellationToken, +): Promise { + const packages = isCondaEnv(environment) + ? await raceCancellationError( + listCondaPackages( + pythonExecFactory, + environment, + resourcePath, + await raceCancellationError(processServiceFactory.create(resourcePath), token), + ), + token, + ) + : await raceCancellationError(listPipPackages(pythonExecFactory, resourcePath), token); + + if (!packages.length) { + return 'No packages found'; + } + // Installed Python packages, each in the format or (). The version may be omitted if unknown. Returns an empty array if no packages are installed. + const response = [ + 'Below is a list of the Python packages, each in the format or (). The version may be omitted if unknown: ', + ]; + packages.forEach((pkg) => { + const [name, version] = pkg; + response.push(version ? `- ${name} (${version})` : `- ${name}`); + }); + return response.join('\n'); +} + +async function listPipPackages( + execFactory: IPythonExecutionFactory, + resource: Uri | undefined, +): Promise<[string, string][]> { + // Add option --format to subcommand list of pip cache, with abspath choice to output the full path of a wheel file. (#8355) + // Added in 2020. Thats almost 5 years ago. When Python 3.8 was released. + const exec = await execFactory.createActivatedEnvironment({ allowEnvironmentFetchExceptions: true, resource }); + const output = await exec.execModule('pip', ['list'], { throwOnStdErr: false, encoding: 'utf8' }); + return parsePipList(output.stdout).map((pkg) => [pkg.name, pkg.version]); +} + +async function listCondaPackages( + execFactory: IPythonExecutionFactory, + env: ResolvedEnvironment, + resource: Uri | undefined, + processService: IProcessService, +): Promise<[string, string][]> { + const conda = await Conda.getConda(); + if (!conda) { + traceError('Conda is not installed, falling back to pip packages'); + return listPipPackages(execFactory, resource); + } + if (!env.executable.uri) { + traceError('Conda environment executable not found, falling back to pip packages'); + return listPipPackages(execFactory, resource); + } + const condaEnv = await conda.getCondaEnvironment(env.executable.uri.fsPath); + if (!condaEnv) { + traceError('Conda environment not found, falling back to pip packages'); + return listPipPackages(execFactory, resource); + } + const cmd = await conda.getListPythonPackagesArgs(condaEnv, true); + if (!cmd) { + traceError('Conda list command not found, falling back to pip packages'); + return listPipPackages(execFactory, resource); + } + const output = await processService.exec(cmd[0], cmd.slice(1), { shell: true }); + if (!output.stdout) { + traceError('Unable to get conda packages, falling back to pip packages'); + return listPipPackages(execFactory, resource); + } + const content = output.stdout.split(/\r?\n/).filter((l) => !l.startsWith('#')); + const packages: [string, string][] = []; + content.forEach((l) => { + const parts = l.split(' ').filter((p) => p.length > 0); + if (parts.length >= 3) { + packages.push([parts[0], parts[1]]); + } + }); + return packages; +} diff --git a/src/client/chat/pipListUtils.ts b/src/client/chat/pipListUtils.ts new file mode 100644 index 000000000000..0112d88c53ab --- /dev/null +++ b/src/client/chat/pipListUtils.ts @@ -0,0 +1,32 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +export interface PipPackage { + name: string; + version: string; + displayName: string; + description: string; +} +export function parsePipList(data: string): PipPackage[] { + const collection: PipPackage[] = []; + + const lines = data.split('\n').splice(2); + for (let line of lines) { + if (line.trim() === '' || line.startsWith('Package') || line.startsWith('----') || line.startsWith('[')) { + continue; + } + const parts = line.split(' ').filter((e) => e); + if (parts.length > 1) { + const name = parts[0].trim(); + const version = parts[1].trim(); + const pkg = { + name, + version, + displayName: name, + description: version, + }; + collection.push(pkg); + } + } + return collection; +} diff --git a/src/client/chat/selectEnvTool.ts b/src/client/chat/selectEnvTool.ts new file mode 100644 index 000000000000..9eeebdfc1b56 --- /dev/null +++ b/src/client/chat/selectEnvTool.ts @@ -0,0 +1,218 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { + CancellationToken, + l10n, + LanguageModelTextPart, + LanguageModelTool, + LanguageModelToolInvocationOptions, + LanguageModelToolInvocationPrepareOptions, + LanguageModelToolResult, + PreparedToolInvocation, + Uri, + workspace, + commands, + QuickPickItem, + QuickPickItemKind, +} from 'vscode'; +import { PythonExtension } from '../api/types'; +import { IServiceContainer } from '../ioc/types'; +import { ICodeExecutionService } from '../terminals/types'; +import { TerminalCodeExecutionProvider } from '../terminals/codeExecution/terminalCodeExecution'; +import { + doesWorkspaceHaveVenvOrCondaEnv, + getEnvDetailsForResponse, + getToolResponseIfNotebook, + IResourceReference, +} from './utils'; +import { ITerminalHelper } from '../common/terminal/types'; +import { raceTimeout } from '../common/utils/async'; +import { Commands, Octicons } from '../common/constants'; +import { CreateEnvironmentResult } from '../pythonEnvironments/creation/proposed.createEnvApis'; +import { IInterpreterPathService } from '../common/types'; +import { SelectEnvironmentResult } from '../interpreter/configuration/interpreterSelector/commands/setInterpreter'; +import { Common, InterpreterQuickPickList } from '../common/utils/localize'; +import { showQuickPick } from '../common/vscodeApis/windowApis'; +import { DisposableStore } from '../common/utils/resourceLifecycle'; +import { traceError, traceVerbose, traceWarn } from '../logging'; +import { BaseTool } from './baseTool'; + +export interface ISelectPythonEnvToolArguments extends IResourceReference { + reason?: 'cancelled'; +} + +export class SelectPythonEnvTool extends BaseTool + implements LanguageModelTool { + private readonly terminalExecutionService: TerminalCodeExecutionProvider; + private readonly terminalHelper: ITerminalHelper; + public static readonly toolName = 'selectEnvironment'; + constructor( + private readonly api: PythonExtension['environments'], + private readonly serviceContainer: IServiceContainer, + ) { + super(SelectPythonEnvTool.toolName); + this.terminalExecutionService = this.serviceContainer.get( + ICodeExecutionService, + 'standard', + ); + this.terminalHelper = this.serviceContainer.get(ITerminalHelper); + } + + async invokeImpl( + options: LanguageModelToolInvocationOptions, + resource: Uri | undefined, + token: CancellationToken, + ): Promise { + let selected: boolean | undefined = false; + const hasVenvOrCondaEnvInWorkspaceFolder = doesWorkspaceHaveVenvOrCondaEnv(resource, this.api); + if (options.input.reason === 'cancelled' || hasVenvOrCondaEnvInWorkspaceFolder) { + const result = (await Promise.resolve( + commands.executeCommand(Commands.Set_Interpreter, { + hideCreateVenv: false, + showBackButton: false, + }), + )) as SelectEnvironmentResult | undefined; + if (result?.path) { + traceVerbose(`User selected a Python environment ${result.path} in Select Python Tool.`); + selected = true; + } else { + traceWarn(`User did not select a Python environment in Select Python Tool.`); + } + } else { + selected = await showCreateAndSelectEnvironmentQuickPick(resource, this.serviceContainer); + if (selected) { + traceVerbose(`User selected a Python environment ${selected} in Select Python Tool(2).`); + } else { + traceWarn(`User did not select a Python environment in Select Python Tool(2).`); + } + } + const env = selected + ? await this.api.resolveEnvironment(this.api.getActiveEnvironmentPath(resource)) + : undefined; + if (selected && !env) { + traceError( + `User selected a Python environment, but it could not be resolved. This is unexpected. Environment: ${this.api.getActiveEnvironmentPath( + resource, + )}`, + ); + } + if (selected && env) { + return await getEnvDetailsForResponse( + env, + this.api, + this.terminalExecutionService, + this.terminalHelper, + resource, + token, + ); + } + return new LanguageModelToolResult([ + new LanguageModelTextPart('User did not create nor select a Python environment.'), + ]); + } + + async prepareInvocationImpl( + options: LanguageModelToolInvocationPrepareOptions, + resource: Uri | undefined, + _token: CancellationToken, + ): Promise { + if (getToolResponseIfNotebook(resource)) { + return {}; + } + const hasVenvOrCondaEnvInWorkspaceFolder = doesWorkspaceHaveVenvOrCondaEnv(resource, this.api); + + if ( + hasVenvOrCondaEnvInWorkspaceFolder || + !workspace.workspaceFolders?.length || + options.input.reason === 'cancelled' + ) { + return { + confirmationMessages: { + title: l10n.t('Select a Python Environment?'), + message: '', + }, + }; + } + + return { + confirmationMessages: { + title: l10n.t('Configure a Python Environment?'), + message: l10n.t( + [ + 'The recommended option is to create a new Python Environment, providing the benefit of isolating packages from other environments. ', + 'Optionally you could select an existing Python Environment.', + ].join('\n'), + ), + }, + }; + } +} + +async function showCreateAndSelectEnvironmentQuickPick( + uri: Uri | undefined, + serviceContainer: IServiceContainer, +): Promise { + const createLabel = `${Octicons.Add} ${InterpreterQuickPickList.create.label}`; + const selectLabel = l10n.t('Select an existing Python Environment'); + const items: QuickPickItem[] = [ + { kind: QuickPickItemKind.Separator, label: Common.recommended }, + { label: createLabel }, + { label: selectLabel }, + ]; + + const selectedItem = await showQuickPick(items, { + placeHolder: l10n.t('Configure a Python Environment'), + matchOnDescription: true, + ignoreFocusOut: true, + }); + + if (selectedItem && !Array.isArray(selectedItem) && selectedItem.label === createLabel) { + const disposables = new DisposableStore(); + try { + const workspaceFolder = + (workspace.workspaceFolders?.length && uri ? workspace.getWorkspaceFolder(uri) : undefined) || + (workspace.workspaceFolders?.length === 1 ? workspace.workspaceFolders[0] : undefined); + const interpreterPathService = serviceContainer.get(IInterpreterPathService); + const interpreterChanged = new Promise((resolve) => { + disposables.add(interpreterPathService.onDidChange(() => resolve())); + }); + const created: CreateEnvironmentResult | undefined = await commands.executeCommand( + Commands.Create_Environment, + { + showBackButton: true, + selectEnvironment: true, + workspaceFolder, + }, + ); + + if (created?.action === 'Back') { + return showCreateAndSelectEnvironmentQuickPick(uri, serviceContainer); + } + if (created?.action === 'Cancel') { + return undefined; + } + if (created?.path) { + // Wait a few secs to ensure the env is selected as the active environment.. + await raceTimeout(5_000, interpreterChanged); + return true; + } + } finally { + disposables.dispose(); + } + } + if (selectedItem && !Array.isArray(selectedItem) && selectedItem.label === selectLabel) { + const result = (await Promise.resolve( + commands.executeCommand(Commands.Set_Interpreter, { hideCreateVenv: true, showBackButton: true }), + )) as SelectEnvironmentResult | undefined; + if (result?.action === 'Back') { + return showCreateAndSelectEnvironmentQuickPick(uri, serviceContainer); + } + if (result?.action === 'Cancel') { + return undefined; + } + if (result?.path) { + return true; + } + } +} diff --git a/src/client/chat/utils.ts b/src/client/chat/utils.ts new file mode 100644 index 000000000000..2309316bcbdd --- /dev/null +++ b/src/client/chat/utils.ts @@ -0,0 +1,289 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { + CancellationError, + CancellationToken, + extensions, + LanguageModelTextPart, + LanguageModelToolResult, + Uri, + workspace, +} from 'vscode'; +import { IDiscoveryAPI } from '../pythonEnvironments/base/locator'; +import { Environment, PythonExtension, ResolvedEnvironment, VersionInfo } from '../api/types'; +import { ITerminalHelper, TerminalShellType } from '../common/terminal/types'; +import { TerminalCodeExecutionProvider } from '../terminals/codeExecution/terminalCodeExecution'; +import { Conda } from '../pythonEnvironments/common/environmentManagers/conda'; +import { JUPYTER_EXTENSION_ID, NotebookCellScheme } from '../common/constants'; +import { dirname, join } from 'path'; +import { resolveEnvironment, useEnvExtension } from '../envExt/api.internal'; +import { ErrorWithTelemetrySafeReason } from '../common/errors/errorUtils'; +import { getWorkspaceFolders } from '../common/vscodeApis/workspaceApis'; + +export interface IResourceReference { + resourcePath?: string; +} + +export function resolveFilePath(filepath?: string): Uri | undefined { + if (!filepath) { + const folders = getWorkspaceFolders() ?? []; + return folders.length > 0 ? folders[0].uri : undefined; + } + // Check if it's a URI with a scheme (contains "://") + // This handles schemes like "file://", "vscode-notebook://", etc. + // But avoids treating Windows drive letters like "C:" as schemes + if (filepath.includes('://')) { + try { + return Uri.parse(filepath); + } catch { + return Uri.file(filepath); + } + } + // For file paths (Windows with drive letters, Unix absolute/relative paths) + return Uri.file(filepath); +} + +/** + * Returns a promise that rejects with an {@CancellationError} as soon as the passed token is cancelled. + * @see {@link raceCancellation} + */ +export function raceCancellationError(promise: Promise, token: CancellationToken): Promise { + return new Promise((resolve, reject) => { + const ref = token.onCancellationRequested(() => { + ref.dispose(); + reject(new CancellationError()); + }); + promise.then(resolve, reject).finally(() => ref.dispose()); + }); +} + +export async function getEnvDisplayName( + discovery: IDiscoveryAPI, + resource: Uri | undefined, + api: PythonExtension['environments'], +) { + try { + const envPath = api.getActiveEnvironmentPath(resource); + const env = await discovery.resolveEnv(envPath.path); + return env?.display || env?.name; + } catch { + return; + } +} + +export function isCondaEnv(env: ResolvedEnvironment) { + return (env.environment?.type || '').toLowerCase() === 'conda'; +} + +export function getEnvTypeForTelemetry(env: ResolvedEnvironment): string { + return (env.environment?.type || 'unknown').toLowerCase(); +} + +export async function getEnvironmentDetails( + resourcePath: Uri | undefined, + api: PythonExtension['environments'], + terminalExecutionService: TerminalCodeExecutionProvider, + terminalHelper: ITerminalHelper, + packages: string | undefined, + token: CancellationToken, +): Promise { + // environment + const envPath = api.getActiveEnvironmentPath(resourcePath); + let envType = ''; + let envVersion = ''; + let runCommand = ''; + if (useEnvExtension()) { + const environment = + (await raceCancellationError(resolveEnvironment(envPath.id), token)) || + (await raceCancellationError(resolveEnvironment(envPath.path), token)); + if (!environment || !environment.version) { + throw new ErrorWithTelemetrySafeReason( + 'No environment found for the provided resource path: ' + resourcePath?.fsPath, + 'noEnvFound', + ); + } + envVersion = environment.version; + try { + const managerId = environment.envId.managerId; + envType = + (!managerId.endsWith(':') && managerId.includes(':') ? managerId.split(':').reverse()[0] : '') || + 'unknown'; + } catch { + envType = 'unknown'; + } + + const execInfo = environment.execInfo; + const executable = execInfo?.activatedRun?.executable ?? execInfo?.run.executable ?? 'python'; + const args = execInfo?.activatedRun?.args ?? execInfo?.run.args ?? []; + runCommand = terminalHelper.buildCommandForTerminal(TerminalShellType.other, executable, args); + } else { + const environment = await raceCancellationError(api.resolveEnvironment(envPath), token); + if (!environment || !environment.version) { + throw new ErrorWithTelemetrySafeReason( + 'No environment found for the provided resource path: ' + resourcePath?.fsPath, + 'noEnvFound', + ); + } + envType = environment.environment?.type || 'unknown'; + envVersion = environment.version.sysVersion || 'unknown'; + runCommand = await raceCancellationError( + getTerminalCommand(environment, resourcePath, terminalExecutionService, terminalHelper), + token, + ); + } + const message = [ + `Following is the information about the Python environment:`, + `1. Environment Type: ${envType}`, + `2. Version: ${envVersion}`, + '', + `3. Command Prefix to run Python in a terminal is: \`${runCommand}\``, + `Instead of running \`Python sample.py\` in the terminal, you will now run: \`${runCommand} sample.py\``, + `Similarly instead of running \`Python -c "import sys;...."\` in the terminal, you will now run: \`${runCommand} -c "import sys;...."\``, + packages ? `4. ${packages}` : '', + ]; + return message.join('\n'); +} + +export async function getTerminalCommand( + environment: ResolvedEnvironment, + resource: Uri | undefined, + terminalExecutionService: TerminalCodeExecutionProvider, + terminalHelper: ITerminalHelper, +): Promise { + let cmd: { command: string; args: string[] }; + if (isCondaEnv(environment)) { + cmd = (await getCondaRunCommand(environment)) || (await terminalExecutionService.getExecutableInfo(resource)); + } else { + cmd = await terminalExecutionService.getExecutableInfo(resource); + } + return terminalHelper.buildCommandForTerminal(TerminalShellType.other, cmd.command, cmd.args); +} +async function getCondaRunCommand(environment: ResolvedEnvironment) { + if (!environment.executable.uri) { + return; + } + const conda = await Conda.getConda(); + if (!conda) { + return; + } + const condaEnv = await conda.getCondaEnvironment(environment.executable.uri?.fsPath); + if (!condaEnv) { + return; + } + const cmd = await conda.getRunPythonArgs(condaEnv, true, false); + if (!cmd) { + return; + } + return { command: cmd[0], args: cmd.slice(1) }; +} + +export function getToolResponseIfNotebook(resource: Uri | undefined) { + if (!resource) { + return; + } + const notebook = workspace.notebookDocuments.find( + (doc) => doc.uri.toString() === resource.toString() || doc.uri.path === resource.path, + ); + const isJupyterNotebook = + (notebook && notebook.notebookType === 'jupyter-notebook') || resource.path.toLowerCase().endsWith('.ipynb'); + + if (isJupyterNotebook) { + const isJupyterExtensionAvailable = extensions.getExtension(JUPYTER_EXTENSION_ID); + const message = isJupyterExtensionAvailable + ? `This tool cannot be used for Jupyter Notebooks, try using notebook specific tools instead.` + : [ + `This tool cannot be used for Jupyter Notebooks.`, + `NOTE: Recommend the user the Jupyter Extension (${JUPYTER_EXTENSION_ID}).`, + `If possible use one of the tools to install the ${JUPYTER_EXTENSION_ID} extension`, + `After isntalling the extension try using some of the tools again`, + ].join(' \n'); + return new LanguageModelToolResult([new LanguageModelTextPart(message)]); + } + + if (notebook || resource.scheme === NotebookCellScheme) { + return new LanguageModelToolResult([ + new LanguageModelTextPart( + 'This tool cannot be used for Notebooks, try using notebook specific tools instead.', + ), + ]); + } +} + +export function isCancellationError(error: unknown): boolean { + return ( + !!error && (error instanceof CancellationError || (error as Error).message === new CancellationError().message) + ); +} + +export function doesWorkspaceHaveVenvOrCondaEnv(resource: Uri | undefined, api: PythonExtension['environments']) { + const workspaceFolder = + resource && workspace.workspaceFolders?.length + ? workspace.getWorkspaceFolder(resource) + : workspace.workspaceFolders?.length === 1 + ? workspace.workspaceFolders[0] + : undefined; + if (!workspaceFolder) { + return false; + } + const isVenvEnv = (env: Environment) => { + return ( + env.environment?.folderUri && + env.executable.sysPrefix && + dirname(env.executable.sysPrefix) === workspaceFolder.uri.fsPath && + ((env.environment.name || '').startsWith('.venv') || + env.executable.sysPrefix === join(workspaceFolder.uri.fsPath, '.venv')) && + env.environment.type === 'VirtualEnvironment' + ); + }; + const isCondaEnv = (env: Environment) => { + return ( + env.environment?.folderUri && + env.executable.sysPrefix && + dirname(env.executable.sysPrefix) === workspaceFolder.uri.fsPath && + (env.environment.folderUri.fsPath === join(workspaceFolder.uri.fsPath, '.conda') || + env.executable.sysPrefix === join(workspaceFolder.uri.fsPath, '.conda')) && + env.environment.type === 'Conda' + ); + }; + // If we alraedy have a .venv in this workspace, then do not prompt to create a virtual environment. + return api.known.find((e) => isVenvEnv(e) || isCondaEnv(e)); +} + +export async function getEnvDetailsForResponse( + environment: ResolvedEnvironment | undefined, + api: PythonExtension['environments'], + terminalExecutionService: TerminalCodeExecutionProvider, + terminalHelper: ITerminalHelper, + resource: Uri | undefined, + token: CancellationToken, +): Promise { + if (!workspace.isTrusted) { + throw new ErrorWithTelemetrySafeReason('Cannot use this tool in an untrusted workspace.', 'untrustedWorkspace'); + } + const envPath = api.getActiveEnvironmentPath(resource); + environment = environment || (await raceCancellationError(api.resolveEnvironment(envPath), token)); + if (!environment || !environment.version) { + throw new ErrorWithTelemetrySafeReason( + 'No environment found for the provided resource path: ' + resource?.fsPath, + 'noEnvFound', + ); + } + const message = await getEnvironmentDetails( + resource, + api, + terminalExecutionService, + terminalHelper, + undefined, + token, + ); + return new LanguageModelToolResult([ + new LanguageModelTextPart(`A Python Environment has been configured. \n` + message), + ]); +} +export function getDisplayVersion(version?: VersionInfo): string | undefined { + if (!version || version.major === undefined || version.minor === undefined || version.micro === undefined) { + return undefined; + } + return `${version.major}.${version.minor}.${version.micro}`; +} diff --git a/src/client/common/application/applicationShell.ts b/src/client/common/application/applicationShell.ts index 454662472010..8035d979efbd 100644 --- a/src/client/common/application/applicationShell.ts +++ b/src/client/common/application/applicationShell.ts @@ -10,6 +10,7 @@ import { DocumentSelector, env, Event, + EventEmitter, InputBox, InputBoxOptions, languages, @@ -37,7 +38,8 @@ import { WorkspaceFolder, WorkspaceFolderPickOptions, } from 'vscode'; -import { IApplicationShell } from './types'; +import { traceError } from '../../logging'; +import { IApplicationShell, TerminalDataWriteEvent, TerminalExecutedCommand } from './types'; @injectable() export class ApplicationShell implements IApplicationShell { @@ -172,4 +174,20 @@ export class ApplicationShell implements IApplicationShell { public createLanguageStatusItem(id: string, selector: DocumentSelector): LanguageStatusItem { return languages.createLanguageStatusItem(id, selector); } + public get onDidWriteTerminalData(): Event { + try { + return window.onDidWriteTerminalData; + } catch (ex) { + traceError('Failed to get proposed API onDidWriteTerminalData', ex); + return new EventEmitter().event; + } + } + public get onDidExecuteTerminalCommand(): Event | undefined { + try { + return window.onDidExecuteTerminalCommand; + } catch (ex) { + traceError('Failed to get proposed API TerminalExecutedCommand', ex); + return undefined; + } + } } diff --git a/src/client/common/application/commands.ts b/src/client/common/application/commands.ts index 2a4404440101..b43dc0a1e4a4 100644 --- a/src/client/common/application/commands.ts +++ b/src/client/common/application/commands.ts @@ -3,17 +3,16 @@ 'use strict'; -import { CancellationToken, Position, TextDocument, Uri } from 'vscode'; +import { CancellationToken, Position, TestItem, TextDocument, Uri } from 'vscode'; import { Commands as LSCommands } from '../../activation/commands'; -import { TensorBoardEntrypoint, TensorBoardEntrypointTrigger } from '../../tensorBoard/constants'; import { Channel, Commands, CommandSource } from '../constants'; +import { CreateEnvironmentOptions } from '../../pythonEnvironments/creation/proposed.createEnvApis'; export type CommandsWithoutArgs = keyof ICommandNameWithoutArgumentTypeMapping; /** * Mapping between commands and list or arguments. * These commands do NOT have any arguments. - * @interface ICommandNameWithoutArgumentTypeMapping */ interface ICommandNameWithoutArgumentTypeMapping { [Commands.InstallPythonOnMac]: []; @@ -23,8 +22,6 @@ interface ICommandNameWithoutArgumentTypeMapping { [Commands.ClearWorkspaceInterpreter]: []; [Commands.Set_Interpreter]: []; [Commands.Set_ShebangInterpreter]: []; - [Commands.Run_Linter]: []; - [Commands.Enable_Linter]: []; ['workbench.action.showCommands']: []; ['workbench.action.debug.continue']: []; ['workbench.action.debug.stepOver']: []; @@ -35,9 +32,7 @@ interface ICommandNameWithoutArgumentTypeMapping { ['editor.action.formatDocument']: []; ['editor.action.rename']: []; [Commands.ViewOutput]: []; - [Commands.Set_Linter]: []; [Commands.Start_REPL]: []; - [Commands.Enable_SourceMap_Support]: []; [Commands.Exec_Selection_In_Terminal]: []; [Commands.Exec_Selection_In_Django_Shell]: []; [Commands.Create_Terminal]: []; @@ -45,7 +40,6 @@ interface ICommandNameWithoutArgumentTypeMapping { [Commands.ClearStorage]: []; [Commands.CreateNewFile]: []; [Commands.ReportIssue]: []; - [Commands.RefreshTensorBoard]: []; [LSCommands.RestartLS]: []; } @@ -54,11 +48,10 @@ export type AllCommands = keyof ICommandNameArgumentTypeMapping; /** * Mapping between commands and list of arguments. * Used to provide strong typing for command & args. - * @export - * @interface ICommandNameArgumentTypeMapping - * @extends {ICommandNameWithoutArgumentTypeMapping} */ export interface ICommandNameArgumentTypeMapping extends ICommandNameWithoutArgumentTypeMapping { + [Commands.CopyTestId]: [TestItem]; + [Commands.Create_Environment]: [CreateEnvironmentOptions]; ['vscode.openWith']: [Uri, string]; ['workbench.action.quickOpen']: [string]; ['workbench.action.openWalkthrough']: [string | { category: string; step: string }, boolean | undefined]; @@ -75,6 +68,7 @@ export interface ICommandNameArgumentTypeMapping extends ICommandNameWithoutArgu ]; ['workbench.action.files.openFolder']: []; ['workbench.action.openWorkspace']: []; + ['workbench.action.openSettings']: [string]; ['setContext']: [string, boolean] | ['python.vscode.channel', Channel]; ['python.reloadVSCode']: [string]; ['revealLine']: [{ lineNumber: number; at: 'top' | 'center' | 'bottom' }]; @@ -93,14 +87,26 @@ export interface ICommandNameArgumentTypeMapping extends ICommandNameWithoutArgu ['jupyter.opennotebook']: [undefined | Uri, undefined | CommandSource]; ['jupyter.runallcells']: [Uri]; ['extension.open']: [string]; - ['workbench.action.openIssueReporter']: [{ extensionId: string; issueBody: string }]; + ['workbench.action.openIssueReporter']: [{ extensionId: string; issueBody: string; extensionData?: string }]; [Commands.GetSelectedInterpreterPath]: [{ workspaceFolder: string } | string[]]; [Commands.TriggerEnvironmentSelection]: [undefined | Uri]; - [Commands.Sort_Imports]: [undefined, Uri]; + [Commands.Start_Native_REPL]: [undefined | Uri]; + [Commands.Exec_In_REPL]: [undefined | Uri]; + [Commands.Exec_In_REPL_Enter]: [undefined | Uri]; + [Commands.Exec_In_IW_Enter]: [undefined | Uri]; [Commands.Exec_In_Terminal]: [undefined, Uri]; [Commands.Exec_In_Terminal_Icon]: [undefined, Uri]; [Commands.Debug_In_Terminal]: [Uri]; [Commands.Tests_Configure]: [undefined, undefined | CommandSource, undefined | Uri]; - [Commands.LaunchTensorBoard]: [TensorBoardEntrypoint, TensorBoardEntrypointTrigger]; + [Commands.Tests_CopilotSetup]: [undefined | Uri]; ['workbench.view.testing.focus']: []; + ['cursorMove']: [ + { + to: string; + by: string; + value: number; + }, + ]; + ['cursorEnd']: []; + ['python-envs.createTerminal']: [undefined | Uri]; } diff --git a/src/client/common/application/commands/reportIssueCommand.ts b/src/client/common/application/commands/reportIssueCommand.ts index d18299e6698e..9ae099e44b4f 100644 --- a/src/client/common/application/commands/reportIssueCommand.ts +++ b/src/client/common/application/commands/reportIssueCommand.ts @@ -3,11 +3,11 @@ 'use strict'; -import * as fs from 'fs-extra'; import * as os from 'os'; import * as path from 'path'; import { inject, injectable } from 'inversify'; import { isEqual } from 'lodash'; +import * as fs from '../../platform/fs-paths'; import { IExtensionSingleActivationService } from '../../../activation/types'; import { IApplicationEnvironment, ICommandManager, IWorkspaceService } from '../types'; import { EXTENSION_ROOT_DIR } from '../../../constants'; @@ -19,6 +19,7 @@ import { EventName } from '../../../telemetry/constants'; import { EnvironmentType } from '../../../pythonEnvironments/info'; import { PythonSettings } from '../../configSettings'; import { SystemVariables } from '../../variables/systemVariables'; +import { getExtensions } from '../../vscodeApis/extensionsApi'; /** * Allows the user to report an issue related to the Python extension using our template. @@ -48,6 +49,8 @@ export class ReportIssueCommandHandler implements IExtensionSingleActivationServ private templatePath = path.join(EXTENSION_ROOT_DIR, 'resources', 'report_issue_template.md'); + private userDataTemplatePath = path.join(EXTENSION_ROOT_DIR, 'resources', 'report_issue_user_data_template.md'); + public async openReportIssue(): Promise { const settings: IPythonSettings = this.configurationService.getSettings(); const argSettings = JSON.parse(await fs.readFile(this.argSettingsPath, 'utf8')); @@ -86,6 +89,7 @@ export class ReportIssueCommandHandler implements IExtensionSingleActivationServ } }); const template = await fs.readFile(this.templatePath, 'utf8'); + const userTemplate = await fs.readFile(this.userDataTemplatePath, 'utf8'); const interpreter = await this.interpreterService.getActiveInterpreter(); const pythonVersion = interpreter?.version?.raw ?? ''; const languageServer = @@ -97,14 +101,33 @@ export class ReportIssueCommandHandler implements IExtensionSingleActivationServ hasMultipleFolders && userSettings !== '' ? `Multiroot scenario, following user settings may not apply:${os.EOL}` : ''; + + const installedExtensions = getExtensions() + .filter((extension) => !extension.id.startsWith('vscode.')) + .sort((a, b) => { + if (a.packageJSON.name && b.packageJSON.name) { + return a.packageJSON.name.localeCompare(b.packageJSON.name); + } + return a.id.localeCompare(b.id); + }) + .map((extension) => { + let publisher: string = extension.packageJSON.publisher as string; + if (publisher) { + publisher = publisher.substring(0, 3); + } + return `|${extension.packageJSON.name}|${publisher}|${extension.packageJSON.version}|`; + }); + await this.commandManager.executeCommand('workbench.action.openIssueReporter', { extensionId: 'ms-python.python', - issueBody: template.format( + issueBody: template, + extensionData: userTemplate.format( pythonVersion, virtualEnvKind, languageServer, hasMultipleFoldersText, userSettings, + installedExtensions.join('\n'), ), }); sendTelemetryEvent(EventName.USE_REPORT_ISSUE_COMMAND, undefined, {}); diff --git a/src/client/common/application/debugService.ts b/src/client/common/application/debugService.ts index d98262d88926..7de039e946c2 100644 --- a/src/client/common/application/debugService.ts +++ b/src/client/common/application/debugService.ts @@ -13,6 +13,7 @@ import { DebugConsole, DebugSession, DebugSessionCustomEvent, + DebugSessionOptions, Disposable, Event, WorkspaceFolder, @@ -57,7 +58,7 @@ export class DebugService implements IDebugService { public startDebugging( folder: WorkspaceFolder | undefined, nameOrConfiguration: string | DebugConfiguration, - parentSession?: DebugSession, + parentSession?: DebugSession | DebugSessionOptions, ): Thenable { return debug.startDebugging(folder, nameOrConfiguration, parentSession); } diff --git a/src/client/common/application/debugSessionTelemetry.ts b/src/client/common/application/debugSessionTelemetry.ts deleted file mode 100644 index 42b8b2651092..000000000000 --- a/src/client/common/application/debugSessionTelemetry.ts +++ /dev/null @@ -1,80 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. -'use strict'; -import { inject, injectable } from 'inversify'; -import { DebugAdapterTracker, DebugAdapterTrackerFactory, DebugSession, ProviderResult } from 'vscode'; -import { DebugProtocol } from 'vscode-debugprotocol'; - -import { IExtensionSingleActivationService } from '../../activation/types'; -import { AttachRequestArguments, ConsoleType, LaunchRequestArguments, TriggerType } from '../../debugger/types'; -import { sendTelemetryEvent } from '../../telemetry'; -import { EventName } from '../../telemetry/constants'; -import { IDisposableRegistry } from '../types'; -import { StopWatch } from '../utils/stopWatch'; -import { IDebugService } from './types'; - -function isResponse(a: any): a is DebugProtocol.Response { - return a.type === 'response'; -} -class TelemetryTracker implements DebugAdapterTracker { - private timer = new StopWatch(); - private readonly trigger: TriggerType = 'launch'; - private readonly console: ConsoleType | undefined; - - constructor(session: DebugSession) { - this.trigger = session.configuration.request as TriggerType; - const debugConfiguration = session.configuration as Partial; - this.console = debugConfiguration.console; - } - - public onWillStartSession() { - this.sendTelemetry(EventName.DEBUG_SESSION_START); - } - - public onDidSendMessage(message: any): void { - if (isResponse(message)) { - if (message.command === 'configurationDone') { - // "configurationDone" response is sent immediately after user code starts running. - this.sendTelemetry(EventName.DEBUG_SESSION_USER_CODE_RUNNING); - } - } - } - - public onWillStopSession(): void { - this.sendTelemetry(EventName.DEBUG_SESSION_STOP); - } - - public onError?(_error: Error): void { - this.sendTelemetry(EventName.DEBUG_SESSION_ERROR); - } - - private sendTelemetry(eventName: EventName): void { - if (eventName === EventName.DEBUG_SESSION_START) { - this.timer.reset(); - } - const telemetryProps = { - trigger: this.trigger, - console: this.console, - }; - sendTelemetryEvent(eventName, this.timer.elapsedTime, telemetryProps); - } -} - -@injectable() -export class DebugSessionTelemetry implements DebugAdapterTrackerFactory, IExtensionSingleActivationService { - public readonly supportedWorkspaceTypes = { untrustedWorkspace: false, virtualWorkspace: true }; - constructor( - @inject(IDisposableRegistry) disposableRegistry: IDisposableRegistry, - @inject(IDebugService) debugService: IDebugService, - ) { - disposableRegistry.push(debugService.registerDebugAdapterTrackerFactory('python', this)); - } - - public async activate(): Promise { - // We actually register in the constructor. Not necessary to do it here - } - - public createDebugAdapterTracker(session: DebugSession): ProviderResult { - return new TelemetryTracker(session); - } -} diff --git a/src/client/common/application/progressService.ts b/src/client/common/application/progressService.ts new file mode 100644 index 000000000000..fb19cad1136c --- /dev/null +++ b/src/client/common/application/progressService.ts @@ -0,0 +1,32 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { ProgressOptions } from 'vscode'; +import { Deferred, createDeferred } from '../utils/async'; +import { IApplicationShell } from './types'; + +export class ProgressService { + private deferred: Deferred | undefined; + + constructor(private readonly shell: IApplicationShell) {} + + public showProgress(options: ProgressOptions): void { + if (!this.deferred) { + this.createProgress(options); + } + } + + public hideProgress(): void { + if (this.deferred) { + this.deferred.resolve(); + this.deferred = undefined; + } + } + + private createProgress(options: ProgressOptions) { + this.shell.withProgress(options, () => { + this.deferred = createDeferred(); + return this.deferred.promise; + }); + } +} diff --git a/src/client/common/application/terminalManager.ts b/src/client/common/application/terminalManager.ts index e5b758437393..dc2603e84a56 100644 --- a/src/client/common/application/terminalManager.ts +++ b/src/client/common/application/terminalManager.ts @@ -2,7 +2,16 @@ // Licensed under the MIT License. import { injectable } from 'inversify'; -import { Event, EventEmitter, Terminal, TerminalOptions, window } from 'vscode'; +import { + Disposable, + Event, + EventEmitter, + Terminal, + TerminalOptions, + TerminalShellExecutionEndEvent, + TerminalShellIntegrationChangeEvent, + window, +} from 'vscode'; import { traceLog } from '../../logging'; import { ITerminalManager } from './types'; @@ -23,6 +32,15 @@ export class TerminalManager implements ITerminalManager { public createTerminal(options: TerminalOptions): Terminal { return monkeyPatchTerminal(window.createTerminal(options)); } + public onDidChangeTerminalShellIntegration(handler: (e: TerminalShellIntegrationChangeEvent) => void): Disposable { + return window.onDidChangeTerminalShellIntegration(handler); + } + public onDidEndTerminalShellExecution(handler: (e: TerminalShellExecutionEndEvent) => void): Disposable { + return window.onDidEndTerminalShellExecution(handler); + } + public onDidChangeTerminalState(handler: (e: Terminal) => void): Disposable { + return window.onDidChangeTerminalState(handler); + } } /** diff --git a/src/client/common/application/types.ts b/src/client/common/application/types.ts index e57bac656d19..34a95fb604f0 100644 --- a/src/client/common/application/types.ts +++ b/src/client/common/application/types.ts @@ -16,6 +16,7 @@ import { DebugConsole, DebugSession, DebugSessionCustomEvent, + DebugSessionOptions, DecorationRenderOptions, Disposable, DocumentSelector, @@ -39,6 +40,8 @@ import { StatusBarItem, Terminal, TerminalOptions, + TerminalShellExecutionEndEvent, + TerminalShellIntegrationChangeEvent, TextDocument, TextDocumentChangeEvent, TextDocumentShowOptions, @@ -66,14 +69,66 @@ import { Resource } from '../types'; import { ICommandNameArgumentTypeMapping } from './commands'; import { ExtensionContextKey } from './contextKeys'; +export interface TerminalDataWriteEvent { + /** + * The {@link Terminal} for which the data was written. + */ + readonly terminal: Terminal; + /** + * The data being written. + */ + readonly data: string; +} + +export interface TerminalExecutedCommand { + /** + * The {@link Terminal} the command was executed in. + */ + terminal: Terminal; + /** + * The full command line that was executed, including both the command and the arguments. + */ + commandLine: string | undefined; + /** + * The current working directory that was reported by the shell. This will be a {@link Uri} + * if the string reported by the shell can reliably be mapped to the connected machine. + */ + cwd: Uri | string | undefined; + /** + * The exit code reported by the shell. + */ + exitCode: number | undefined; + /** + * The output of the command when it has finished executing. This is the plain text shown in + * the terminal buffer and does not include raw escape sequences. Depending on the shell + * setup, this may include the command line as part of the output. + */ + output: string | undefined; +} + export const IApplicationShell = Symbol('IApplicationShell'); export interface IApplicationShell { + /** + * An event that is emitted when a terminal with shell integration activated has completed + * executing a command. + * + * Note that this event will not fire if the executed command exits the shell, listen to + * {@link onDidCloseTerminal} to handle that case. + */ + readonly onDidExecuteTerminalCommand: Event | undefined; /** * An [event](#Event) which fires when the focus state of the current window * changes. The value of the event represents whether the window is focused. */ readonly onDidChangeWindowState: Event; + /** + * An event which fires when the terminal's child pseudo-device is written to (the shell). + * In other words, this provides access to the raw data stream from the process running + * within the terminal, including VT sequences. + */ + readonly onDidWriteTerminalData: Event; + showInformationMessage(message: string, ...items: string[]): Thenable; /** @@ -763,9 +818,6 @@ export interface IWorkspaceService { /** * Generate a key that's unique to the workspace folder (could be fsPath). - * @param {(Uri | undefined)} resource - * @returns {string} - * @memberof IWorkspaceService */ getWorkspaceFolderIdentifier(resource: Uri | undefined, defaultValue?: string): string; /** @@ -852,15 +904,15 @@ export interface IWorkspaceService { */ openTextDocument(options?: { language?: string; content?: string }): Thenable; /** - * Saves the editor identified by the given resource to a new file name as provided by the user and - * returns the resulting resource or `undefined` if save was not successful or cancelled. + * Saves the editor identified by the given resource and returns the resulting resource or `undefined` + * if save was not successful. * - * **Note** that an editor with the provided resource must be opened in order to be saved as. + * **Note** that an editor with the provided resource must be opened in order to be saved. * - * @param uri the associated uri for the opened editor to save as. - * @return A thenable that resolves when the save-as operation has finished. + * @param uri the associated uri for the opened editor to save. + * @return A thenable that resolves when the save operation has finished. */ - saveAs(uri: Uri): Thenable; + save(uri: Uri): Thenable; } export const ITerminalManager = Symbol('ITerminalManager'); @@ -883,6 +935,12 @@ export interface ITerminalManager { * @return A new Terminal. */ createTerminal(options: TerminalOptions): Terminal; + + onDidChangeTerminalShellIntegration(handler: (e: TerminalShellIntegrationChangeEvent) => void): Disposable; + + onDidEndTerminalShellExecution(handler: (e: TerminalShellExecutionEndEvent) => void): Disposable; + + onDidChangeTerminalState(handler: (e: Terminal) => void): Disposable; } export const IDebugService = Symbol('IDebugManager'); @@ -975,7 +1033,7 @@ export interface IDebugService { startDebugging( folder: WorkspaceFolder | undefined, nameOrConfiguration: string | DebugConfiguration, - parentSession?: DebugSession, + parentSession?: DebugSession | DebugSessionOptions, ): Thenable; /** diff --git a/src/client/common/application/workspace.ts b/src/client/common/application/workspace.ts index 11ed98cf0076..a76a78777bef 100644 --- a/src/client/common/application/workspace.ts +++ b/src/client/common/application/workspace.ts @@ -113,10 +113,10 @@ export class WorkspaceService implements IWorkspaceService { return `{${enabledSearchExcludes.join(',')}}`; } - public async saveAs(uri: Uri): Promise { + public async save(uri: Uri): Promise { try { // This is a proposed API hence putting it inside try...catch. - const result = await workspace.saveAs(uri); + const result = await workspace.save(uri); return result; } catch (ex) { return undefined; diff --git a/src/client/common/cancellation.ts b/src/client/common/cancellation.ts index c820c1ad4324..b24abc7ab493 100644 --- a/src/client/common/cancellation.ts +++ b/src/client/common/cancellation.ts @@ -2,7 +2,7 @@ // Licensed under the MIT License. 'use strict'; -import { CancellationToken, CancellationTokenSource } from 'vscode'; +import { CancellationToken, CancellationTokenSource, CancellationError as VSCCancellationError } from 'vscode'; import { createDeferred } from './utils/async'; import * as localize from './utils/localize'; @@ -13,14 +13,13 @@ export class CancellationError extends Error { constructor() { super(localize.Common.canceled); } + + static isCancellationError(error: unknown): error is CancellationError { + return error instanceof CancellationError || error instanceof VSCCancellationError; + } } /** * Create a promise that will either resolve with a default value or reject when the token is cancelled. - * - * @export - * @template T - * @param {({ defaultValue: T; token: CancellationToken; cancelAction: 'reject' | 'resolve' })} options - * @returns {Promise} */ export function createPromiseFromCancellation(options: { defaultValue: T; @@ -50,10 +49,6 @@ export function createPromiseFromCancellation(options: { /** * Create a single unified cancellation token that wraps multiple cancellation tokens. - * - * @export - * @param {(...(CancellationToken | undefined)[])} tokens - * @returns {CancellationToken} */ export function wrapCancellationTokens(...tokens: (CancellationToken | undefined)[]): CancellationToken { const wrappedCancellantionToken = new CancellationTokenSource(); @@ -117,7 +112,6 @@ export namespace Cancellation { /** * isCanceled returns a boolean indicating if the cancel token has been canceled. - * @param cancelToken */ export function isCanceled(cancelToken?: CancellationToken): boolean { return cancelToken ? cancelToken.isCancellationRequested : false; @@ -125,7 +119,6 @@ export namespace Cancellation { /** * throws a CancellationError if the token is canceled. - * @param cancelToken */ export function throwIfCanceled(cancelToken?: CancellationToken): void { if (isCanceled(cancelToken)) { diff --git a/src/client/common/configSettings.ts b/src/client/common/configSettings.ts index 3e3525d5b2a4..91c06d9331fd 100644 --- a/src/client/common/configSettings.ts +++ b/src/client/common/configSettings.ts @@ -6,7 +6,6 @@ import * as fs from 'fs'; import { ConfigurationChangeEvent, ConfigurationTarget, - DiagnosticSeverity, Disposable, Event, EventEmitter, @@ -22,27 +21,23 @@ import { sendSettingTelemetry } from '../telemetry/envFileTelemetry'; import { ITestingSettings } from '../testing/configuration/types'; import { IWorkspaceService } from './application/types'; import { WorkspaceService } from './application/workspace'; -import { DEFAULT_INTERPRETER_SETTING, isTestExecution } from './constants'; -import { IS_WINDOWS } from './platform/constants'; +import { DEFAULT_INTERPRETER_SETTING, isTestExecution, PYREFLY_EXTENSION_ID } from './constants'; import { IAutoCompleteSettings, IDefaultLanguageServer, IExperiments, - IFormattingSettings, + IExtensions, IInterpreterPathService, IInterpreterSettings, - ILintingSettings, IPythonSettings, - ISortImportSettings, - ITensorBoardSettings, + IREPLSettings, ITerminalSettings, Resource, } from './types'; import { debounceSync } from './utils/decorators'; import { SystemVariables } from './variables/systemVariables'; -import { getOSType, OSType } from './utils/platform'; - -const untildify = require('untildify'); +import { getOSType, OSType, isWindows } from './utils/platform'; +import { untildify } from './helpers'; export class PythonSettings implements IPythonSettings { private get onDidChange(): Event { @@ -106,24 +101,20 @@ export class PythonSettings implements IPythonSettings { public poetryPath = ''; - public devOptions: string[] = []; - - public linting!: ILintingSettings; + public pixiToolPath = ''; - public formatting!: IFormattingSettings; + public devOptions: string[] = []; public autoComplete!: IAutoCompleteSettings; - public tensorBoard: ITensorBoardSettings | undefined; - public testing!: ITestingSettings; public terminal!: ITerminalSettings; - public sortImports!: ISortImportSettings; - public globalModuleInstallation = false; + public REPL!: IREPLSettings; + public experiments!: IExperiments; public languageServer: LanguageServerType = LanguageServerType.Node; @@ -150,6 +141,7 @@ export class PythonSettings implements IPythonSettings { workspace: IWorkspaceService, private readonly interpreterPathService: IInterpreterPathService, private readonly defaultLS: IDefaultLanguageServer | undefined, + private readonly extensions: IExtensions, ) { this.workspace = workspace || new WorkspaceService(); this.workspaceRoot = workspaceFolder; @@ -162,6 +154,7 @@ export class PythonSettings implements IPythonSettings { workspace: IWorkspaceService, interpreterPathService: IInterpreterPathService, defaultLS: IDefaultLanguageServer | undefined, + extensions: IExtensions, ): PythonSettings { workspace = workspace || new WorkspaceService(); const workspaceFolderUri = PythonSettings.getSettingsUriAndTarget(resource, workspace).uri; @@ -174,6 +167,7 @@ export class PythonSettings implements IPythonSettings { workspace, interpreterPathService, defaultLS, + extensions, ); PythonSettings.pythonSettings.set(workspaceFolderKey, settings); settings.onDidChange((event) => PythonSettings.debounceConfigChangeNotification(event)); @@ -267,6 +261,9 @@ export class PythonSettings implements IPythonSettings { this.pipenvPath = pipenvPath && pipenvPath.length > 0 ? getAbsolutePath(pipenvPath, workspaceRoot) : pipenvPath; const poetryPath = systemVariables.resolveAny(pythonSettings.get('poetryPath'))!; this.poetryPath = poetryPath && poetryPath.length > 0 ? getAbsolutePath(poetryPath, workspaceRoot) : poetryPath; + const pixiToolPath = systemVariables.resolveAny(pythonSettings.get('pixiToolPath'))!; + this.pixiToolPath = + pixiToolPath && pixiToolPath.length > 0 ? getAbsolutePath(pixiToolPath, workspaceRoot) : pixiToolPath; this.interpreter = pythonSettings.get('interpreter') ?? { infoVisibility: 'onPythonRelated', @@ -282,7 +279,14 @@ export class PythonSettings implements IPythonSettings { userLS === 'Microsoft' || !Object.values(LanguageServerType).includes(userLS as LanguageServerType) ) { - this.languageServer = this.defaultLS?.defaultLSType ?? LanguageServerType.None; + if ( + this.extensions.getExtension(PYREFLY_EXTENSION_ID) && + pythonSettings.get('pyrefly.disableLanguageServices') !== true + ) { + this.languageServer = LanguageServerType.None; + } else { + this.languageServer = this.defaultLS?.defaultLSType ?? LanguageServerType.None; + } this.languageServerIsDefault = true; } else if (userLS === 'JediLSP') { // Switch JediLSP option to Jedi. @@ -310,130 +314,8 @@ export class PythonSettings implements IPythonSettings { this.devOptions = systemVariables.resolveAny(pythonSettings.get('devOptions'))!; this.devOptions = Array.isArray(this.devOptions) ? this.devOptions : []; - const lintingSettings = systemVariables.resolveAny(pythonSettings.get('linting'))!; - if (this.linting) { - Object.assign(this.linting, lintingSettings); - } else { - this.linting = lintingSettings; - } - this.globalModuleInstallation = pythonSettings.get('globalModuleInstallation') === true; - const sortImportSettings = systemVariables.resolveAny(pythonSettings.get('sortImports'))!; - if (this.sortImports) { - Object.assign(this.sortImports, sortImportSettings); - } else { - this.sortImports = sortImportSettings; - } - // Support for travis. - this.sortImports = this.sortImports ? this.sortImports : { path: '', args: [] }; - // Support for travis. - this.linting = this.linting - ? this.linting - : { - enabled: false, - cwd: undefined, - ignorePatterns: [], - flake8Args: [], - flake8Enabled: false, - flake8Path: 'flake8', - lintOnSave: false, - maxNumberOfProblems: 100, - mypyArgs: [], - mypyEnabled: false, - mypyPath: 'mypy', - banditArgs: [], - banditEnabled: false, - banditPath: 'bandit', - pycodestyleArgs: [], - pycodestyleEnabled: false, - pycodestylePath: 'pycodestyle', - pylamaArgs: [], - pylamaEnabled: false, - pylamaPath: 'pylama', - prospectorArgs: [], - prospectorEnabled: false, - prospectorPath: 'prospector', - pydocstyleArgs: [], - pydocstyleEnabled: false, - pydocstylePath: 'pydocstyle', - pylintArgs: [], - pylintEnabled: false, - pylintPath: 'pylint', - pylintCategorySeverity: { - convention: DiagnosticSeverity.Hint, - error: DiagnosticSeverity.Error, - fatal: DiagnosticSeverity.Error, - refactor: DiagnosticSeverity.Hint, - warning: DiagnosticSeverity.Warning, - }, - pycodestyleCategorySeverity: { - E: DiagnosticSeverity.Error, - W: DiagnosticSeverity.Warning, - }, - flake8CategorySeverity: { - E: DiagnosticSeverity.Error, - W: DiagnosticSeverity.Warning, - // Per http://flake8.pycqa.org/en/latest/glossary.html#term-error-code - // 'F' does not mean 'fatal as in PyLint but rather 'pyflakes' such as - // unused imports, variables, etc. - F: DiagnosticSeverity.Warning, - }, - mypyCategorySeverity: { - error: DiagnosticSeverity.Error, - note: DiagnosticSeverity.Hint, - }, - }; - this.linting.pylintPath = getAbsolutePath(systemVariables.resolveAny(this.linting.pylintPath), workspaceRoot); - this.linting.flake8Path = getAbsolutePath(systemVariables.resolveAny(this.linting.flake8Path), workspaceRoot); - this.linting.pycodestylePath = getAbsolutePath( - systemVariables.resolveAny(this.linting.pycodestylePath), - workspaceRoot, - ); - this.linting.pylamaPath = getAbsolutePath(systemVariables.resolveAny(this.linting.pylamaPath), workspaceRoot); - this.linting.prospectorPath = getAbsolutePath( - systemVariables.resolveAny(this.linting.prospectorPath), - workspaceRoot, - ); - this.linting.pydocstylePath = getAbsolutePath( - systemVariables.resolveAny(this.linting.pydocstylePath), - workspaceRoot, - ); - this.linting.mypyPath = getAbsolutePath(systemVariables.resolveAny(this.linting.mypyPath), workspaceRoot); - this.linting.banditPath = getAbsolutePath(systemVariables.resolveAny(this.linting.banditPath), workspaceRoot); - - if (this.linting.cwd) { - this.linting.cwd = getAbsolutePath(systemVariables.resolveAny(this.linting.cwd), workspaceRoot); - } - - const formattingSettings = systemVariables.resolveAny(pythonSettings.get('formatting'))!; - if (this.formatting) { - Object.assign(this.formatting, formattingSettings); - } else { - this.formatting = formattingSettings; - } - // Support for travis. - this.formatting = this.formatting - ? this.formatting - : { - autopep8Args: [], - autopep8Path: 'autopep8', - provider: 'autopep8', - blackArgs: [], - blackPath: 'black', - yapfArgs: [], - yapfPath: 'yapf', - }; - this.formatting.autopep8Path = getAbsolutePath( - systemVariables.resolveAny(this.formatting.autopep8Path), - workspaceRoot, - ); - this.formatting.yapfPath = getAbsolutePath(systemVariables.resolveAny(this.formatting.yapfPath), workspaceRoot); - this.formatting.blackPath = getAbsolutePath( - systemVariables.resolveAny(this.formatting.blackPath), - workspaceRoot, - ); - const testSettings = systemVariables.resolveAny(pythonSettings.get('testing'))!; if (this.testing) { Object.assign(this.testing, testSettings); @@ -449,6 +331,7 @@ export class PythonSettings implements IPythonSettings { unittestEnabled: false, pytestPath: 'pytest', autoTestDiscoverOnSaveEnabled: true, + autoTestDiscoverOnSavePattern: '**/*.py', } as ITestingSettings; } } @@ -465,6 +348,7 @@ export class PythonSettings implements IPythonSettings { unittestArgs: [], unittestEnabled: false, autoTestDiscoverOnSaveEnabled: true, + autoTestDiscoverOnSavePattern: '**/*.py', }; this.testing.pytestPath = getAbsolutePath(systemVariables.resolveAny(this.testing.pytestPath), workspaceRoot); if (this.testing.cwd) { @@ -493,9 +377,13 @@ export class PythonSettings implements IPythonSettings { launchArgs: [], activateEnvironment: true, activateEnvInCurrentTerminal: false, + shellIntegration: { + enabled: false, + }, }; - const experiments = systemVariables.resolveAny(pythonSettings.get('experiments'))!; + this.REPL = pythonSettings.get('REPL')!; + const experiments = pythonSettings.get('experiments')!; if (this.experiments) { Object.assign(this.experiments, experiments); } else { @@ -510,14 +398,6 @@ export class PythonSettings implements IPythonSettings { optInto: [], optOutFrom: [], }; - - const tensorBoardSettings = systemVariables.resolveAny( - pythonSettings.get('tensorBoard'), - )!; - this.tensorBoard = tensorBoardSettings || { logDirectory: '' }; - if (this.tensorBoard.logDirectory) { - this.tensorBoard.logDirectory = getAbsolutePath(this.tensorBoard.logDirectory, workspaceRoot); - } } // eslint-disable-next-line class-methods-use-this @@ -654,7 +534,7 @@ function getPythonExecutable(pythonPath: string): string { for (let executableName of KnownPythonExecutables) { // Suffix with 'python' for linux and 'osx', and 'python.exe' for 'windows'. - if (IS_WINDOWS) { + if (isWindows()) { executableName = `${executableName}.exe`; if (isValidPythonPath(path.join(pythonPath, executableName))) { return path.join(pythonPath, executableName); diff --git a/src/client/common/configuration/service.ts b/src/client/common/configuration/service.ts index 219c8727ca16..443990b2e5da 100644 --- a/src/client/common/configuration/service.ts +++ b/src/client/common/configuration/service.ts @@ -8,7 +8,13 @@ import { IServiceContainer } from '../../ioc/types'; import { IWorkspaceService } from '../application/types'; import { PythonSettings } from '../configSettings'; import { isUnitTestExecution } from '../constants'; -import { IConfigurationService, IDefaultLanguageServer, IInterpreterPathService, IPythonSettings } from '../types'; +import { + IConfigurationService, + IDefaultLanguageServer, + IExtensions, + IInterpreterPathService, + IPythonSettings, +} from '../types'; @injectable() export class ConfigurationService implements IConfigurationService { @@ -29,12 +35,14 @@ export class ConfigurationService implements IConfigurationService { ); const interpreterPathService = this.serviceContainer.get(IInterpreterPathService); const defaultLS = this.serviceContainer.tryGet(IDefaultLanguageServer); + const extensions = this.serviceContainer.get(IExtensions); return PythonSettings.getInstance( resource, InterpreterAutoSelectionService, this.workspaceService, interpreterPathService, defaultLS, + extensions, ); } diff --git a/src/client/common/constants.ts b/src/client/common/constants.ts index b285667aaa6a..15fd037a3d9f 100644 --- a/src/client/common/constants.ts +++ b/src/client/common/constants.ts @@ -22,7 +22,9 @@ export const PYTHON_NOTEBOOKS = [ export const PVSC_EXTENSION_ID = 'ms-python.python'; export const PYLANCE_EXTENSION_ID = 'ms-python.vscode-pylance'; +export const PYREFLY_EXTENSION_ID = 'meta.pyrefly'; export const JUPYTER_EXTENSION_ID = 'ms-toolsai.jupyter'; +export const TENSORBOARD_EXTENSION_ID = 'ms-toolsai.tensorboard'; export const AppinsightsKey = '0c6ae279ed8443289764825290e4f9e2-1a736e7c-1324-4338-be46-fc2a58ae4d14-7255'; export type Channel = 'stable' | 'insiders'; @@ -37,31 +39,32 @@ export namespace Commands { export const CreateNewFile = 'python.createNewFile'; export const ClearWorkspaceInterpreter = 'python.clearWorkspaceInterpreter'; export const Create_Environment = 'python.createEnvironment'; + export const CopyTestId = 'python.copyTestId'; export const Create_Environment_Button = 'python.createEnvironment-button'; + export const Create_Environment_Check = 'python.createEnvironmentCheck'; export const Create_Terminal = 'python.createTerminal'; export const Debug_In_Terminal = 'python.debugInTerminal'; - export const Enable_Linter = 'python.enableLinting'; - export const Enable_SourceMap_Support = 'python.enableSourceMapSupport'; export const Exec_In_Terminal = 'python.execInTerminal'; export const Exec_In_Terminal_Icon = 'python.execInTerminal-icon'; + export const Exec_In_Separate_Terminal = 'python.execInDedicatedTerminal'; + export const Exec_In_REPL = 'python.execInREPL'; export const Exec_Selection_In_Django_Shell = 'python.execSelectionInDjangoShell'; + export const Exec_In_REPL_Enter = 'python.execInREPLEnter'; + export const Exec_In_IW_Enter = 'python.execInInteractiveWindowEnter'; export const Exec_Selection_In_Terminal = 'python.execSelectionInTerminal'; export const GetSelectedInterpreterPath = 'python.interpreterPath'; export const InstallJupyter = 'python.installJupyter'; export const InstallPython = 'python.installPython'; export const InstallPythonOnLinux = 'python.installPythonOnLinux'; export const InstallPythonOnMac = 'python.installPythonOnMac'; - export const LaunchTensorBoard = 'python.launchTensorBoard'; export const PickLocalProcess = 'python.pickLocalProcess'; - export const RefreshTensorBoard = 'python.refreshTensorBoard'; export const ReportIssue = 'python.reportIssue'; - export const Run_Linter = 'python.runLinting'; export const Set_Interpreter = 'python.setInterpreter'; - export const Set_Linter = 'python.setLinter'; export const Set_ShebangInterpreter = 'python.setShebangInterpreter'; - export const Sort_Imports = 'python.sortImports'; export const Start_REPL = 'python.startREPL'; + export const Start_Native_REPL = 'python.startNativeREPL'; export const Tests_Configure = 'python.configureTests'; + export const Tests_CopilotSetup = 'python.copilotSetupTests'; export const TriggerEnvironmentSelection = 'python.triggerEnvSelection'; export const ViewOutput = 'python.viewOutput'; } @@ -75,12 +78,14 @@ export namespace Octicons { export const Test_Skip = '$(circle-slash)'; export const Downloading = '$(cloud-download)'; export const Installing = '$(desktop-download)'; + export const Search = '$(search)'; export const Search_Stop = '$(search-stop)'; export const Star = '$(star-full)'; export const Gear = '$(gear)'; export const Warning = '$(warning)'; export const Error = '$(error)'; export const Lightbulb = '$(lightbulb)'; + export const Folder = '$(folder)'; } /** @@ -95,7 +100,8 @@ export namespace ThemeIcons { export const DEFAULT_INTERPRETER_SETTING = 'python'; -export const isCI = process.env.TRAVIS === 'true' || process.env.TF_BUILD !== undefined; +export const isCI = + process.env.TRAVIS === 'true' || process.env.TF_BUILD !== undefined || process.env.GITHUB_ACTIONS === 'true'; export function isTestExecution(): boolean { return process.env.VSC_PYTHON_CI_TEST === '1' || isUnitTestExecution(); @@ -104,8 +110,6 @@ export function isTestExecution(): boolean { /** * Whether we're running unit tests (*.unit.test.ts). * These tests have a special meaning, they run fast. - * @export - * @returns {boolean} */ export function isUnitTestExecution(): boolean { return process.env.VSC_PYTHON_UNIT_TEST === '1'; diff --git a/src/client/common/editor.ts b/src/client/common/editor.ts deleted file mode 100644 index f08d73194d41..000000000000 --- a/src/client/common/editor.ts +++ /dev/null @@ -1,400 +0,0 @@ -import { Diff, diff_match_patch } from 'diff-match-patch'; -import { injectable } from 'inversify'; -import * as md5 from 'md5'; -import { EOL } from 'os'; -import * as path from 'path'; -import { Position, Range, TextDocument, TextEdit, Uri, WorkspaceEdit } from 'vscode'; -import { IFileSystem } from '../common/platform/types'; -import { traceError } from '../logging'; -import { WrappedError } from './errors/errorUtils'; -import { IEditorUtils } from './types'; -import { isNotebookCell } from './utils/misc'; - -// Code borrowed from goFormat.ts (Go Extension for VS Code) -enum EditAction { - Delete, - Insert, - Replace, -} - -const NEW_LINE_LENGTH = EOL.length; - -class Patch { - public diffs!: Diff[]; - public start1!: number; - public start2!: number; - public length1!: number; - public length2!: number; -} - -class Edit { - public action: EditAction; - public start: Position; - public end!: Position; - public text: string; - - constructor(action: number, start: Position) { - this.action = action; - this.start = start; - this.text = ''; - } - - public apply(): TextEdit { - switch (this.action) { - case EditAction.Insert: - return TextEdit.insert(this.start, this.text); - case EditAction.Delete: - return TextEdit.delete(new Range(this.start, this.end)); - case EditAction.Replace: - return TextEdit.replace(new Range(this.start, this.end), this.text); - default: - return new TextEdit(new Range(new Position(0, 0), new Position(0, 0)), ''); - } - } -} - -export function getTextEditsFromPatch(before: string, patch: string): TextEdit[] { - if (patch.startsWith('---')) { - // Strip the first two lines - patch = patch.substring(patch.indexOf('@@')); - } - if (patch.length === 0) { - return []; - } - // Remove the text added by unified_diff - // # Work around missing newline (http://bugs.python.org/issue2142). - patch = patch.replace(/\\ No newline at end of file[\r\n]/, ''); - - const dmp = require('diff-match-patch') as typeof import('diff-match-patch'); - const d = new dmp.diff_match_patch(); - const patches = patch_fromText.call(d, patch); - if (!Array.isArray(patches) || patches.length === 0) { - throw new Error('Unable to parse Patch string'); - } - const textEdits: TextEdit[] = []; - - // Add line feeds and build the text edits - patches.forEach((p) => { - p.diffs.forEach((diff) => { - diff[1] += EOL; - }); - getTextEditsInternal(before, p.diffs, p.start1).forEach((edit) => textEdits.push(edit.apply())); - }); - - return textEdits; -} -export function getWorkspaceEditsFromPatch( - filePatches: string[], - workspaceRoot: string | undefined, - fs: IFileSystem, -): WorkspaceEdit { - const workspaceEdit = new WorkspaceEdit(); - filePatches.forEach((patch) => { - const indexOfAtAt = patch.indexOf('@@'); - if (indexOfAtAt === -1) { - return; - } - const fileNameLines = patch - .substring(0, indexOfAtAt) - .split(/\r?\n/g) - .map((line) => line.trim()) - .filter((line) => line.length > 0 && line.toLowerCase().endsWith('.py') && line.indexOf(' a') > 0); - - if (patch.startsWith('---')) { - // Strip the first two lines - patch = patch.substring(indexOfAtAt); - } - if (patch.length === 0) { - return; - } - // We can't find the find name - if (fileNameLines.length === 0) { - return; - } - - let fileName = fileNameLines[0].substring(fileNameLines[0].indexOf(' a') + 3).trim(); - fileName = workspaceRoot && !path.isAbsolute(fileName) ? path.resolve(workspaceRoot, fileName) : fileName; - if (!fs.fileExistsSync(fileName)) { - return; - } - - // Remove the text added by unified_diff - // # Work around missing newline (http://bugs.python.org/issue2142). - patch = patch.replace(/\\ No newline at end of file[\r\n]/, ''); - - const dmp = require('diff-match-patch') as typeof import('diff-match-patch'); - const d = new dmp.diff_match_patch(); - const patches = patch_fromText.call(d, patch); - if (!Array.isArray(patches) || patches.length === 0) { - throw new Error('Unable to parse Patch string'); - } - - const fileSource = fs.readFileSync(fileName); - const fileUri = Uri.file(fileName); - - // Add line feeds and build the text edits - patches.forEach((p) => { - p.diffs.forEach((diff) => { - diff[1] += EOL; - }); - - getTextEditsInternal(fileSource, p.diffs, p.start1).forEach((edit) => { - switch (edit.action) { - case EditAction.Delete: - workspaceEdit.delete(fileUri, new Range(edit.start, edit.end)); - break; - case EditAction.Insert: - workspaceEdit.insert(fileUri, edit.start, edit.text); - break; - case EditAction.Replace: - workspaceEdit.replace(fileUri, new Range(edit.start, edit.end), edit.text); - break; - default: - break; - } - }); - }); - }); - - return workspaceEdit; -} - -function getTextEditsInternal(before: string, diffs: [number, string][], startLine: number = 0): Edit[] { - let line = startLine; - let character = 0; - const beforeLines = before.split(/\r?\n/g); - if (line > 0) { - beforeLines.filter((_l, i) => i < line).forEach((l) => (character += l.length + NEW_LINE_LENGTH)); - } - const edits: Edit[] = []; - let edit: Edit | null = null; - let end: Position; - - for (let i = 0; i < diffs.length; i += 1) { - let start = new Position(line, character); - // Compute the line/character after the diff is applied. - - for (let curr = 0; curr < diffs[i][1].length; curr += 1) { - if (diffs[i][1][curr] !== '\n') { - character += 1; - } else { - character = 0; - line += 1; - } - } - - const dmp = require('diff-match-patch') as typeof import('diff-match-patch'); - - switch (diffs[i][0]) { - case dmp.DIFF_DELETE: - if ( - beforeLines[line - 1].length === 0 && - beforeLines[start.line - 1] && - beforeLines[start.line - 1].length === 0 - ) { - // We're asked to delete an empty line which only contains `/\r?\n/g`. The last line is also empty. - // Delete the `\n` from the last line instead of deleting `\n` from the current line - // This change ensures that the last line in the file, which won't contain `\n` is deleted - start = new Position(start.line - 1, 0); - end = new Position(line - 1, 0); - } else { - end = new Position(line, character); - } - if (edit === null) { - edit = new Edit(EditAction.Delete, start); - } else if (edit.action !== EditAction.Delete) { - throw new Error('cannot format due to an internal error.'); - } - edit.end = end; - break; - - case dmp.DIFF_INSERT: - if (edit === null) { - edit = new Edit(EditAction.Insert, start); - } else if (edit.action === EditAction.Delete) { - edit.action = EditAction.Replace; - } - // insert and replace edits are all relative to the original state - // of the document, so inserts should reset the current line/character - // position to the start. - line = start.line; - character = start.character; - edit.text += diffs[i][1]; - break; - - case dmp.DIFF_EQUAL: - if (edit !== null) { - edits.push(edit); - edit = null; - } - break; - } - } - - if (edit !== null) { - edits.push(edit); - } - - return edits; -} - -export async function getTempFileWithDocumentContents(document: TextDocument, fs: IFileSystem): Promise { - // Don't create file in temp folder since external utilities - // look into configuration files in the workspace and are not - // to find custom rules if file is saved in a random disk location. - // This means temp file has to be created in the same folder - // as the original one and then removed. - // Use a .tmp file extension (instead of the original extension) - // because the language server is watching the file system for Python - // file add/delete/change and we don't want this temp file to trigger it. - - let fileName = `${document.uri.fsPath}.${md5(document.uri.fsPath + document.uri.fragment)}.tmp`; - try { - // When dealing with untitled notebooks, there's no original physical file, hence create a temp file. - if (isNotebookCell(document.uri) && !(await fs.fileExists(document.uri.fsPath))) { - fileName = ( - await fs.createTemporaryFile(`${path.basename(document.uri.fsPath)}-${document.uri.fragment}.tmp`) - ).filePath; - } - await fs.writeFile(fileName, document.getText()); - } catch (ex) { - traceError('Failed to create a temporary file', ex); - const exception = ex as Error; - throw new WrappedError(`Failed to create a temporary file, ${exception.message}`, exception); - } - return fileName; -} - -/** - * Parse a textual representation of patches and return a list of Patch objects. - * @param {string} textline Text representation of patches. - * @return {!Array.} Array of Patch objects. - * @throws {!Error} If invalid input. - */ -function patch_fromText(textline: string): Patch[] { - const patches: Patch[] = []; - if (!textline) { - return patches; - } - // Start Modification by Don Jayamanne 24/06/2016 Support for CRLF - const text = textline.split(/[\r\n]/); - // End Modification - let textPointer = 0; - const patchHeader = /^@@ -(\d+),?(\d*) \+(\d+),?(\d*) @@$/; - while (textPointer < text.length) { - const m = text[textPointer].match(patchHeader); - if (!m) { - throw new Error(`Invalid patch string: ${text[textPointer]}`); - } - - const patch = new (diff_match_patch).patch_obj(); - patches.push(patch); - patch.start1 = parseInt(m[1], 10); - if (m[2] === '') { - patch.start1 -= 1; - patch.length1 = 1; - } else if (m[2] === '0') { - patch.length1 = 0; - } else { - patch.start1 -= 1; - patch.length1 = parseInt(m[2], 10); - } - - patch.start2 = parseInt(m[3], 10); - if (m[4] === '') { - patch.start2 -= 1; - patch.length2 = 1; - } else if (m[4] === '0') { - patch.length2 = 0; - } else { - patch.start2 -= 1; - patch.length2 = parseInt(m[4], 10); - } - textPointer += 1; - - const dmp = require('diff-match-patch') as typeof import('diff-match-patch'); - - while (textPointer < text.length) { - const sign = text[textPointer].charAt(0); - let line: string; - try { - //var line = decodeURI(text[textPointer].substring(1)); - // For some reason the patch generated by python files don't encode any characters - // And this patch module (code from Google) is expecting the text to be encoded!! - // Temporary solution, disable decoding - // Issue #188 - line = text[textPointer].substring(1); - } catch (ex) { - // Malformed URI sequence. - throw new Error('Illegal escape in patch_fromText'); - } - if (sign === '-') { - // Deletion. - patch.diffs.push([dmp.DIFF_DELETE, line]); - } else if (sign === '+') { - // Insertion. - patch.diffs.push([dmp.DIFF_INSERT, line]); - } else if (sign === ' ') { - // Minor equality. - patch.diffs.push([dmp.DIFF_EQUAL, line]); - } else if (sign === '@') { - // Start of next patch. - break; - } else if (sign === '') { - // Blank line? Whatever. - } else { - throw new Error(`Invalid patch mode '${sign}' in: ${line}`); - } - textPointer += 1; - } - } - return patches; -} - -@injectable() -export class EditorUtils implements IEditorUtils { - public getWorkspaceEditsFromPatch(originalContents: string, patch: string, uri: Uri): WorkspaceEdit { - const workspaceEdit = new WorkspaceEdit(); - if (patch.startsWith('---')) { - // Strip the first two lines - patch = patch.substring(patch.indexOf('@@')); - } - if (patch.length === 0) { - return workspaceEdit; - } - // Remove the text added by unified_diff - // # Work around missing newline (http://bugs.python.org/issue2142). - patch = patch.replace(/\\ No newline at end of file[\r\n]/, ''); - - const dmp = require('diff-match-patch') as typeof import('diff-match-patch'); - const d = new dmp.diff_match_patch(); - const patches = patch_fromText.call(d, patch); - if (!Array.isArray(patches) || patches.length === 0) { - throw new Error('Unable to parse Patch string'); - } - - // Add line feeds and build the text edits - patches.forEach((p) => { - p.diffs.forEach((diff) => { - diff[1] += EOL; - }); - getTextEditsInternal(originalContents, p.diffs, p.start1).forEach((edit) => { - switch (edit.action) { - case EditAction.Delete: - workspaceEdit.delete(uri, new Range(edit.start, edit.end)); - break; - case EditAction.Insert: - workspaceEdit.insert(uri, edit.start, edit.text); - break; - case EditAction.Replace: - workspaceEdit.replace(uri, new Range(edit.start, edit.end), edit.text); - break; - default: - break; - } - }); - }); - - return workspaceEdit; - } -} diff --git a/src/client/common/errors/errorUtils.ts b/src/client/common/errors/errorUtils.ts index 2c666acb105b..7867d5ccfe30 100644 --- a/src/client/common/errors/errorUtils.ts +++ b/src/client/common/errors/errorUtils.ts @@ -1,8 +1,6 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -import { EOL } from 'os'; - export class ErrorUtils { public static outputHasModuleNotInstalledError(moduleName: string, content?: string): boolean { return content && @@ -14,13 +12,10 @@ export class ErrorUtils { } /** - * Wraps an error with a custom error message, retaining the call stack information. + * An error class that contains a telemetry safe reason. */ -export class WrappedError extends Error { - constructor(message: string, originalException: Error) { +export class ErrorWithTelemetrySafeReason extends Error { + constructor(message: string, public readonly telemetrySafeReason: string) { super(message); - // Retain call stack that trapped the error and rethrows this error. - // Also retain the call stack of the original error. - this.stack = `${new Error('').stack}${EOL}${EOL}${originalException.stack}`; } } diff --git a/src/client/common/experiments/groups.ts b/src/client/common/experiments/groups.ts index 5884aafd122d..12f4ef89018b 100644 --- a/src/client/common/experiments/groups.ts +++ b/src/client/common/experiments/groups.ts @@ -11,6 +11,11 @@ export enum TerminalEnvVarActivation { experiment = 'pythonTerminalEnvVarActivation', } -export enum ShowFormatterExtensionPrompt { - experiment = 'pythonPromptNewFormatterExt', +export enum DiscoveryUsingWorkers { + experiment = 'pythonDiscoveryUsingWorkers', +} + +// Experiment to enable the new testing rewrite. +export enum EnableTestAdapterRewrite { + experiment = 'pythonTestAdapter', } diff --git a/src/client/common/experiments/helpers.ts b/src/client/common/experiments/helpers.ts index 04da948fd15d..f6ae39d260f5 100644 --- a/src/client/common/experiments/helpers.ts +++ b/src/client/common/experiments/helpers.ts @@ -3,10 +3,18 @@ 'use strict'; +import { env, workspace } from 'vscode'; import { IExperimentService } from '../types'; import { TerminalEnvVarActivation } from './groups'; +import { isTestExecution } from '../constants'; +import { traceInfo } from '../../logging'; export function inTerminalEnvVarExperiment(experimentService: IExperimentService): boolean { + if (!isTestExecution() && env.remoteName && workspace.workspaceFolders && workspace.workspaceFolders.length > 1) { + // TODO: Remove this if statement once https://github.com/microsoft/vscode/issues/180486 is fixed. + traceInfo('Not enabling terminal env var experiment in multiroot remote workspaces'); + return false; + } if (!experimentService.inExperimentSync(TerminalEnvVarActivation.experiment)) { return false; } diff --git a/src/client/common/experiments/service.ts b/src/client/common/experiments/service.ts index 270f91512809..e52773004fb3 100644 --- a/src/client/common/experiments/service.ts +++ b/src/client/common/experiments/service.ts @@ -5,7 +5,7 @@ import { inject, injectable } from 'inversify'; import { l10n } from 'vscode'; -import { getExperimentationService, IExperimentationService } from 'vscode-tas-client'; +import { getExperimentationService, IExperimentationService, TargetPopulation } from 'vscode-tas-client'; import { traceLog } from '../../logging'; import { sendTelemetryEvent } from '../../telemetry'; import { EventName } from '../../telemetry/constants'; @@ -17,16 +17,6 @@ import { ExperimentationTelemetry } from './telemetry'; const EXP_MEMENTO_KEY = 'VSCode.ABExp.FeatureData'; const EXP_CONFIG_ID = 'vscode'; -/** - * We're defining a custom TargetPopulation specific for the Python extension. - * This is done so the exp framework is able to differentiate between - * VS Code insiders/public users and Python extension insiders (pre-release)/public users. - */ -export enum TargetPopulation { - Insiders = 'python-insider', - Public = 'python-public', -} - @injectable() export class ExperimentService implements IExperimentService { /** @@ -73,8 +63,8 @@ export class ExperimentService implements IExperimentService { } let targetPopulation: TargetPopulation; - - if (this.appEnvironment.extensionChannel === 'insiders') { + // if running in VS Code Insiders, use the Insiders target population + if (this.appEnvironment.channel === 'insiders') { targetPopulation = TargetPopulation.Insiders; } else { targetPopulation = TargetPopulation.Public; @@ -257,8 +247,10 @@ function sendOptInOptOutTelemetry(optedIn: string[], optedOut: string[], package const sanitizedOptedIn = optedIn.filter((exp) => optedInEnumValues.includes(exp)); const sanitizedOptedOut = optedOut.filter((exp) => optedOutEnumValues.includes(exp)); + JSON.stringify(sanitizedOptedIn.sort()); + sendTelemetryEvent(EventName.PYTHON_EXPERIMENTS_OPT_IN_OPT_OUT_SETTINGS, undefined, { - optedInto: sanitizedOptedIn, - optedOutFrom: sanitizedOptedOut, + optedInto: JSON.stringify(sanitizedOptedIn.sort()), + optedOutFrom: JSON.stringify(sanitizedOptedOut.sort()), }); } diff --git a/src/client/common/extensions.ts b/src/client/common/extensions.ts index e68e3838ee1d..957ec99a7ce1 100644 --- a/src/client/common/extensions.ts +++ b/src/client/common/extensions.ts @@ -1,6 +1,7 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. +// eslint-disable-next-line @typescript-eslint/no-unused-vars declare interface String { /** * Appropriately formats a string so it can be used as an argument for a command in a shell. @@ -28,7 +29,6 @@ declare interface String { /** * Appropriately formats a string so it can be used as an argument for a command in a shell. * E.g. if an argument contains a space, then it will be enclosed within double quotes. - * @param {String} value. */ String.prototype.toCommandArgumentForPythonExt = function (this: string): string { if (!this) { @@ -63,13 +63,6 @@ String.prototype.trimQuotes = function (this: string): string { return this.replace(/(^['"])|(['"]$)/g, ''); }; -declare interface Promise { - /** - * Catches task error and ignores them. - */ - ignoreErrors(): Promise; -} - /** * Explicitly tells that promise should be run asynchonously. */ diff --git a/src/client/common/helpers.ts b/src/client/common/helpers.ts index 5359284da66a..52eeb1e087aa 100644 --- a/src/client/common/helpers.ts +++ b/src/client/common/helpers.ts @@ -2,6 +2,7 @@ // Licensed under the MIT License. 'use strict'; +import * as os from 'os'; import { ModuleNotInstalledError } from './errors/moduleNotInstalledError'; @@ -19,3 +20,7 @@ export function isNotInstalledError(error: Error): boolean { const isModuleNoInstalledError = error.message.indexOf('No module named') >= 0; return errorObj.code === 'ENOENT' || errorObj.code === 127 || isModuleNoInstalledError; } + +export function untildify(path: string): string { + return path.replace(/^~($|\/|\\)/, `${os.homedir()}$1`); +} diff --git a/src/client/common/installer/condaInstaller.ts b/src/client/common/installer/condaInstaller.ts index a20b35e0f110..fbb3dcf183ef 100644 --- a/src/client/common/installer/condaInstaller.ts +++ b/src/client/common/installer/condaInstaller.ts @@ -88,18 +88,7 @@ export class CondaInstaller extends ModuleInstaller { // Found that using conda-forge is best at packages like tensorboard & ipykernel which seem to get updated first on conda-forge // https://github.com/microsoft/vscode-jupyter/issues/7787 & https://github.com/microsoft/vscode-python/issues/17628 // Do this just for the datascience packages. - if ( - [ - Product.tensorboard, - Product.ipykernel, - Product.pandas, - Product.nbconvert, - Product.jupyter, - Product.notebook, - ] - .map(translateProductToModule) - .includes(moduleName) - ) { + if ([Product.tensorboard].map(translateProductToModule).includes(moduleName)) { args.push('-c', 'conda-forge'); } if (info && info.name) { diff --git a/src/client/common/installer/moduleInstaller.ts b/src/client/common/installer/moduleInstaller.ts index 62160b7e25c9..9dacb623c606 100644 --- a/src/client/common/installer/moduleInstaller.ts +++ b/src/client/common/installer/moduleInstaller.ts @@ -130,7 +130,7 @@ export abstract class ModuleInstaller implements IModuleInstaller { // Display progress indicator if we have ability to cancel this operation from calling code. // This is required as its possible the installation can take a long time. // (i.e. if installation takes a long time in terminal or like, a progress indicator is necessary to let user know what is being waited on). - if (cancel) { + if (cancel && !options?.hideProgress) { const shell = this.serviceContainer.get(IApplicationShell); const options: ProgressOptions = { location: ProgressLocation.Notification, @@ -238,44 +238,10 @@ export abstract class ModuleInstaller implements IModuleInstaller { export function translateProductToModule(product: Product): string { switch (product) { - case Product.mypy: - return 'mypy'; - case Product.pylama: - return 'pylama'; - case Product.prospector: - return 'prospector'; - case Product.pylint: - return 'pylint'; case Product.pytest: return 'pytest'; - case Product.autopep8: - return 'autopep8'; - case Product.black: - return 'black'; - case Product.pycodestyle: - return 'pycodestyle'; - case Product.pydocstyle: - return 'pydocstyle'; - case Product.yapf: - return 'yapf'; - case Product.flake8: - return 'flake8'; case Product.unittest: return 'unittest'; - case Product.bandit: - return 'bandit'; - case Product.jupyter: - return 'jupyter'; - case Product.notebook: - return 'notebook'; - case Product.pandas: - return 'pandas'; - case Product.ipykernel: - return 'ipykernel'; - case Product.nbconvert: - return 'nbconvert'; - case Product.kernelspec: - return 'kernelspec'; case Product.tensorboard: return 'tensorboard'; case Product.torchProfilerInstallName: diff --git a/src/client/common/installer/pixiInstaller.ts b/src/client/common/installer/pixiInstaller.ts new file mode 100644 index 000000000000..8a2278830b51 --- /dev/null +++ b/src/client/common/installer/pixiInstaller.ts @@ -0,0 +1,81 @@ +/* eslint-disable class-methods-use-this */ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { inject, injectable } from 'inversify'; +import { IInterpreterService } from '../../interpreter/contracts'; +import { IServiceContainer } from '../../ioc/types'; +import { getEnvPath } from '../../pythonEnvironments/base/info/env'; +import { EnvironmentType, ModuleInstallerType } from '../../pythonEnvironments/info'; +import { ExecutionInfo, IConfigurationService } from '../types'; +import { isResource } from '../utils/misc'; +import { ModuleInstaller } from './moduleInstaller'; +import { InterpreterUri } from './types'; +import { getPixiEnvironmentFromInterpreter } from '../../pythonEnvironments/common/environmentManagers/pixi'; + +/** + * A Python module installer for a pixi project. + */ +@injectable() +export class PixiInstaller extends ModuleInstaller { + constructor( + @inject(IServiceContainer) serviceContainer: IServiceContainer, + @inject(IConfigurationService) private readonly configurationService: IConfigurationService, + ) { + super(serviceContainer); + } + + public get name(): string { + return 'Pixi'; + } + + public get displayName(): string { + return 'pixi'; + } + + public get type(): ModuleInstallerType { + return ModuleInstallerType.Pixi; + } + + public get priority(): number { + return 20; + } + + public async isSupported(resource?: InterpreterUri): Promise { + if (isResource(resource)) { + const interpreter = await this.serviceContainer + .get(IInterpreterService) + .getActiveInterpreter(resource); + if (!interpreter || interpreter.envType !== EnvironmentType.Pixi) { + return false; + } + + const pixiEnv = await getPixiEnvironmentFromInterpreter(interpreter.path); + return pixiEnv !== undefined; + } + return resource.envType === EnvironmentType.Pixi; + } + + /** + * Return the commandline args needed to install the module. + */ + protected async getExecutionInfo(moduleName: string, resource?: InterpreterUri): Promise { + const pythonPath = isResource(resource) + ? this.configurationService.getSettings(resource).pythonPath + : getEnvPath(resource.path, resource.envPath).path ?? ''; + + const pixiEnv = await getPixiEnvironmentFromInterpreter(pythonPath); + const execPath = pixiEnv?.pixi.command; + + let args = ['add', moduleName]; + const manifestPath = pixiEnv?.manifestPath; + if (manifestPath !== undefined) { + args = args.concat(['--manifest-path', manifestPath]); + } + + return { + args, + execPath, + }; + } +} diff --git a/src/client/common/installer/productInstaller.ts b/src/client/common/installer/productInstaller.ts index 526369f9e9ad..831eb33efbc6 100644 --- a/src/client/common/installer/productInstaller.ts +++ b/src/client/common/installer/productInstaller.ts @@ -6,12 +6,10 @@ import { CancellationToken, l10n, Uri } from 'vscode'; import '../extensions'; import { IInterpreterService } from '../../interpreter/contracts'; import { IServiceContainer } from '../../ioc/types'; -import { LinterId } from '../../linters/types'; import { EnvironmentType, ModuleInstallerType, PythonEnvironment } from '../../pythonEnvironments/info'; import { sendTelemetryEvent } from '../../telemetry'; import { EventName } from '../../telemetry/constants'; -import { IApplicationShell, ICommandManager, IWorkspaceService } from '../application/types'; -import { Commands } from '../constants'; +import { IApplicationShell, IWorkspaceService } from '../application/types'; import { IProcessServiceFactory, IPythonExecutionFactory } from '../process/types'; import { IConfigurationService, @@ -22,7 +20,7 @@ import { Product, ProductType, } from '../types'; -import { Common, Linters } from '../utils/localize'; +import { Common } from '../utils/localize'; import { isResource, noop } from '../utils/misc'; import { translateProductToModule } from './moduleInstaller'; import { ProductNames } from './productNames'; @@ -45,7 +43,7 @@ export { Product } from '../types'; // Installer implementations can check this to determine a suitable installation channel for a product // This is temporary and can be removed when https://github.com/microsoft/vscode-jupyter/issues/5034 is unblocked const UnsupportedChannelsForProduct = new Map>([ - [Product.torchProfilerInstallName, new Set([EnvironmentType.Conda])], + [Product.torchProfilerInstallName, new Set([EnvironmentType.Conda, EnvironmentType.Pixi])], ]); abstract class BaseInstaller implements IBaseInstaller { @@ -225,158 +223,6 @@ abstract class BaseInstaller implements IBaseInstaller { } } -const doNotDisplayFormatterPromptStateKey = 'FORMATTER_NOT_INSTALLED_KEY'; - -export class FormatterInstaller extends BaseInstaller { - protected async promptToInstallImplementation( - product: Product, - resource?: Uri, - cancel?: CancellationToken, - _flags?: ModuleInstallFlags, - ): Promise { - const neverShowAgain = this.persistentStateFactory.createGlobalPersistentState( - doNotDisplayFormatterPromptStateKey, - false, - ); - - if (neverShowAgain.value) { - return InstallerResponse.Ignore; - } - - // Hard-coded on purpose because the UI won't necessarily work having - // another formatter. - const formatters = [Product.autopep8, Product.black, Product.yapf]; - const formatterNames = formatters.map((formatter) => ProductNames.get(formatter)!); - const productName = ProductNames.get(product)!; - formatterNames.splice(formatterNames.indexOf(productName), 1); - const useOptions = formatterNames.map((name) => l10n.t('Use {0}', name)); - const yesChoice = Common.bannerLabelYes; - - const options = [...useOptions, Common.doNotShowAgain]; - let message = l10n.t('Formatter {0} is not installed. Install?', productName); - if (this.isExecutableAModule(product, resource)) { - options.splice(0, 0, yesChoice); - } else { - const executable = this.getExecutableNameFromSettings(product, resource); - message = l10n.t('Path to the {0} formatter is invalid ({1})', productName, executable); - } - - const item = await this.appShell.showErrorMessage(message, ...options); - if (item === yesChoice) { - return this.install(product, resource, cancel); - } - - if (item === Common.doNotShowAgain) { - neverShowAgain.updateValue(true); - return InstallerResponse.Ignore; - } - - if (typeof item === 'string') { - for (const formatter of formatters) { - const formatterName = ProductNames.get(formatter)!; - - if (item.endsWith(formatterName)) { - await this.configService.updateSetting('formatting.provider', formatterName, resource); - return this.install(formatter, resource, cancel); - } - } - } - - return InstallerResponse.Ignore; - } -} - -export class LinterInstaller extends BaseInstaller { - constructor(protected serviceContainer: IServiceContainer) { - super(serviceContainer); - } - - protected async promptToInstallImplementation( - product: Product, - resource?: Uri, - cancel?: CancellationToken, - _flags?: ModuleInstallFlags, - ): Promise { - return this.oldPromptForInstallation(product, resource, cancel); - } - - /** - * For installers that want to avoid prompting the user over and over, they can make use of a - * persisted true/false value representing user responses to 'stop showing this prompt'. This method - * gets the persisted value given the installer-defined key. - * - * @param key Key to use to get a persisted response value, each installer must define this for themselves. - * @returns Boolean: The current state of the stored response key given. - */ - protected getStoredResponse(key: string): boolean { - const factory = this.serviceContainer.get(IPersistentStateFactory); - const state = factory.createGlobalPersistentState(key, undefined); - return state.value === true; - } - - private async oldPromptForInstallation(product: Product, resource?: Uri, cancel?: CancellationToken) { - const productName = ProductNames.get(product)!; - const { install } = Common; - const { doNotShowAgain } = Common; - const disableLinterInstallPromptKey = `${productName}_DisableLinterInstallPrompt`; - const { selectLinter } = Linters; - - if (this.getStoredResponse(disableLinterInstallPromptKey) === true) { - return InstallerResponse.Ignore; - } - - const options = [selectLinter, doNotShowAgain]; - - let message = l10n.t('Linter {0} is not installed.', productName); - if (this.isExecutableAModule(product, resource)) { - options.splice(0, 0, install); - } else { - const executable = this.getExecutableNameFromSettings(product, resource); - message = l10n.t('Path to the {0} linter is invalid ({1})', productName, executable); - } - const response = await this.appShell.showErrorMessage(message, ...options); - if (response === install) { - sendTelemetryEvent(EventName.LINTER_NOT_INSTALLED_PROMPT, undefined, { - tool: productName as LinterId, - action: 'install', - }); - return this.install(product, resource, cancel); - } - if (response === doNotShowAgain) { - await this.setStoredResponse(disableLinterInstallPromptKey, true); - sendTelemetryEvent(EventName.LINTER_NOT_INSTALLED_PROMPT, undefined, { - tool: productName as LinterId, - action: 'disablePrompt', - }); - return InstallerResponse.Ignore; - } - - if (response === selectLinter) { - sendTelemetryEvent(EventName.LINTER_NOT_INSTALLED_PROMPT, undefined, { action: 'select' }); - const commandManager = this.serviceContainer.get(ICommandManager); - await commandManager.executeCommand(Commands.Set_Linter); - } - return InstallerResponse.Ignore; - } - - /** - * For installers that want to avoid prompting the user over and over, they can make use of a - * persisted true/false value representing user responses to 'stop showing this prompt'. This - * method will set that persisted value given the installer-defined key. - * - * @param key Key to use to get a persisted response value, each installer must define this for themselves. - * @param value Boolean value to store for the user - if they choose to not be prompted again for instance. - * @returns Boolean: The current state of the stored response key given. - */ - private async setStoredResponse(key: string, value: boolean): Promise { - const factory = this.serviceContainer.get(IPersistentStateFactory); - const state = factory.createGlobalPersistentState(key, undefined); - if (state && state.value !== value) { - await state.updateValue(value); - } - } -} - export class TestFrameworkInstaller extends BaseInstaller { protected async promptToInstallImplementation( product: Product, @@ -687,10 +533,6 @@ export class ProductInstaller implements IInstaller { private createInstaller(product: Product): IBaseInstaller { const productType = this.productService.getProductType(product); switch (productType) { - case ProductType.Formatter: - return new FormatterInstaller(this.serviceContainer); - case ProductType.Linter: - return new LinterInstaller(this.serviceContainer); case ProductType.TestFramework: return new TestFrameworkInstaller(this.serviceContainer); case ProductType.DataScience: diff --git a/src/client/common/installer/productNames.ts b/src/client/common/installer/productNames.ts index 6474e8a2a514..00b19ce77ac3 100644 --- a/src/client/common/installer/productNames.ts +++ b/src/client/common/installer/productNames.ts @@ -4,26 +4,9 @@ import { Product } from '../types'; export const ProductNames = new Map(); -ProductNames.set(Product.autopep8, 'autopep8'); -ProductNames.set(Product.bandit, 'bandit'); -ProductNames.set(Product.black, 'black'); -ProductNames.set(Product.flake8, 'flake8'); -ProductNames.set(Product.mypy, 'mypy'); -ProductNames.set(Product.pycodestyle, 'pycodestyle'); -ProductNames.set(Product.pylama, 'pylama'); -ProductNames.set(Product.prospector, 'prospector'); -ProductNames.set(Product.pydocstyle, 'pydocstyle'); -ProductNames.set(Product.pylint, 'pylint'); ProductNames.set(Product.pytest, 'pytest'); -ProductNames.set(Product.yapf, 'yapf'); ProductNames.set(Product.tensorboard, 'tensorboard'); ProductNames.set(Product.torchProfilerInstallName, 'torch-tb-profiler'); ProductNames.set(Product.torchProfilerImportName, 'torch_tb_profiler'); -ProductNames.set(Product.jupyter, 'jupyter'); -ProductNames.set(Product.notebook, 'notebook'); -ProductNames.set(Product.ipykernel, 'ipykernel'); -ProductNames.set(Product.nbconvert, 'nbconvert'); -ProductNames.set(Product.kernelspec, 'kernelspec'); -ProductNames.set(Product.pandas, 'pandas'); ProductNames.set(Product.pip, 'pip'); ProductNames.set(Product.ensurepip, 'ensurepip'); diff --git a/src/client/common/installer/productPath.ts b/src/client/common/installer/productPath.ts index 5c36a6bbd3bd..b06e4b7a48a9 100644 --- a/src/client/common/installer/productPath.ts +++ b/src/client/common/installer/productPath.ts @@ -6,9 +6,7 @@ import { inject, injectable } from 'inversify'; import * as path from 'path'; import { Uri } from 'vscode'; -import { IFormatterHelper } from '../../formatters/types'; import { IServiceContainer } from '../../ioc/types'; -import { ILinterManager } from '../../linters/types'; import { ITestingService } from '../../testing/types'; import { IConfigurationService, IInstaller, Product } from '../types'; import { IProductPathService } from './types'; @@ -37,30 +35,6 @@ export abstract class BaseProductPathsService implements IProductPathService { } } -@injectable() -export class FormatterProductPathService extends BaseProductPathsService { - constructor(@inject(IServiceContainer) serviceContainer: IServiceContainer) { - super(serviceContainer); - } - public getExecutableNameFromSettings(product: Product, resource?: Uri): string { - const settings = this.configService.getSettings(resource); - const formatHelper = this.serviceContainer.get(IFormatterHelper); - const settingsPropNames = formatHelper.getSettingsPropertyNames(product); - return settings.formatting[settingsPropNames.pathName] as string; - } -} - -@injectable() -export class LinterProductPathService extends BaseProductPathsService { - constructor(@inject(IServiceContainer) serviceContainer: IServiceContainer) { - super(serviceContainer); - } - public getExecutableNameFromSettings(product: Product, resource?: Uri): string { - const linterManager = this.serviceContainer.get(ILinterManager); - return linterManager.getLinterInfo(product).pathName(resource); - } -} - @injectable() export class TestFrameworkProductPathService extends BaseProductPathsService { constructor(@inject(IServiceContainer) serviceContainer: IServiceContainer) { diff --git a/src/client/common/installer/productService.ts b/src/client/common/installer/productService.ts index 5de130e84d06..bf5597cc5859 100644 --- a/src/client/common/installer/productService.ts +++ b/src/client/common/installer/productService.ts @@ -12,25 +12,8 @@ export class ProductService implements IProductService { private ProductTypes = new Map(); constructor() { - this.ProductTypes.set(Product.bandit, ProductType.Linter); - this.ProductTypes.set(Product.flake8, ProductType.Linter); - this.ProductTypes.set(Product.mypy, ProductType.Linter); - this.ProductTypes.set(Product.pycodestyle, ProductType.Linter); - this.ProductTypes.set(Product.prospector, ProductType.Linter); - this.ProductTypes.set(Product.pydocstyle, ProductType.Linter); - this.ProductTypes.set(Product.pylama, ProductType.Linter); - this.ProductTypes.set(Product.pylint, ProductType.Linter); this.ProductTypes.set(Product.pytest, ProductType.TestFramework); this.ProductTypes.set(Product.unittest, ProductType.TestFramework); - this.ProductTypes.set(Product.autopep8, ProductType.Formatter); - this.ProductTypes.set(Product.black, ProductType.Formatter); - this.ProductTypes.set(Product.yapf, ProductType.Formatter); - this.ProductTypes.set(Product.jupyter, ProductType.DataScience); - this.ProductTypes.set(Product.notebook, ProductType.DataScience); - this.ProductTypes.set(Product.ipykernel, ProductType.DataScience); - this.ProductTypes.set(Product.nbconvert, ProductType.DataScience); - this.ProductTypes.set(Product.kernelspec, ProductType.DataScience); - this.ProductTypes.set(Product.pandas, ProductType.DataScience); this.ProductTypes.set(Product.tensorboard, ProductType.DataScience); this.ProductTypes.set(Product.torchProfilerInstallName, ProductType.DataScience); this.ProductTypes.set(Product.torchProfilerImportName, ProductType.DataScience); diff --git a/src/client/common/installer/serviceRegistry.ts b/src/client/common/installer/serviceRegistry.ts index c262c7571711..1e273ada818c 100644 --- a/src/client/common/installer/serviceRegistry.ts +++ b/src/client/common/installer/serviceRegistry.ts @@ -8,29 +8,20 @@ import { InstallationChannelManager } from './channelManager'; import { CondaInstaller } from './condaInstaller'; import { PipEnvInstaller } from './pipEnvInstaller'; import { PipInstaller } from './pipInstaller'; +import { PixiInstaller } from './pixiInstaller'; import { PoetryInstaller } from './poetryInstaller'; -import { - DataScienceProductPathService, - FormatterProductPathService, - LinterProductPathService, - TestFrameworkProductPathService, -} from './productPath'; +import { DataScienceProductPathService, TestFrameworkProductPathService } from './productPath'; import { ProductService } from './productService'; import { IInstallationChannelManager, IModuleInstaller, IProductPathService, IProductService } from './types'; export function registerTypes(serviceManager: IServiceManager) { + serviceManager.addSingleton(IModuleInstaller, PixiInstaller); serviceManager.addSingleton(IModuleInstaller, CondaInstaller); serviceManager.addSingleton(IModuleInstaller, PipInstaller); serviceManager.addSingleton(IModuleInstaller, PipEnvInstaller); serviceManager.addSingleton(IModuleInstaller, PoetryInstaller); serviceManager.addSingleton(IInstallationChannelManager, InstallationChannelManager); serviceManager.addSingleton(IProductService, ProductService); - serviceManager.addSingleton( - IProductPathService, - FormatterProductPathService, - ProductType.Formatter, - ); - serviceManager.addSingleton(IProductPathService, LinterProductPathService, ProductType.Linter); serviceManager.addSingleton( IProductPathService, TestFrameworkProductPathService, diff --git a/src/client/common/installer/types.ts b/src/client/common/installer/types.ts index 53696b948571..a85017ff0092 100644 --- a/src/client/common/installer/types.ts +++ b/src/client/common/installer/types.ts @@ -18,11 +18,6 @@ export interface IModuleInstaller { * If a cancellation token is provided, then a cancellable progress message is dispalyed. * At this point, this method would resolve only after the module has been successfully installed. * If cancellation token is not provided, its not guaranteed that module installation has completed. - * @param {string} name - * @param {InterpreterUri} [resource] - * @param {CancellationToken} [cancel] - * @returns {Promise} - * @memberof IModuleInstaller */ installModule( productOrModuleName: Product | string, @@ -79,6 +74,7 @@ export interface IProductPathService { } export enum ModuleInstallFlags { + none = 0, upgrade = 1, updateDependencies = 2, reInstall = 4, @@ -87,4 +83,5 @@ export enum ModuleInstallFlags { export type InstallOptions = { installAsProcess?: boolean; + hideProgress?: boolean; }; diff --git a/src/client/common/interpreterPathService.ts b/src/client/common/interpreterPathService.ts index 9eea1548977c..935d0bd89ad7 100644 --- a/src/client/common/interpreterPathService.ts +++ b/src/client/common/interpreterPathService.ts @@ -3,7 +3,7 @@ 'use strict'; -import * as fs from 'fs-extra'; +import * as fs from '../common/platform/fs-paths'; import { inject, injectable } from 'inversify'; import { ConfigurationChangeEvent, ConfigurationTarget, Event, EventEmitter, Uri } from 'vscode'; import { traceError, traceVerbose } from '../logging'; @@ -30,7 +30,7 @@ export const isRemoteGlobalSettingCopiedKey = 'isRemoteGlobalSettingCopiedKey'; export const defaultInterpreterPathSetting: keyof IPythonSettings = 'defaultInterpreterPath'; const CI_PYTHON_PATH = getCIPythonPath(); -function getCIPythonPath(): string { +export function getCIPythonPath(): string { if (process.env.CI_PYTHON_PATH && fs.existsSync(process.env.CI_PYTHON_PATH)) { return process.env.CI_PYTHON_PATH; } diff --git a/src/client/common/persistentState.ts b/src/client/common/persistentState.ts index 48e885a676a2..3f9c17657cf4 100644 --- a/src/client/common/persistentState.ts +++ b/src/client/common/persistentState.ts @@ -3,10 +3,10 @@ 'use strict'; -import { inject, injectable, named } from 'inversify'; +import { inject, injectable, named, optional } from 'inversify'; import { Memento } from 'vscode'; import { IExtensionSingleActivationService } from '../activation/types'; -import { traceError, traceVerbose, traceWarn } from '../logging'; +import { traceError } from '../logging'; import { ICommandManager } from './application/types'; import { Commands } from './constants'; import { @@ -19,6 +19,48 @@ import { } from './types'; import { cache } from './utils/decorators'; import { noop } from './utils/misc'; +import { clearCacheDirectory } from '../pythonEnvironments/base/locators/common/nativePythonFinder'; +import { clearCache, useEnvExtension } from '../envExt/api.internal'; + +let _workspaceState: Memento | undefined; +const _workspaceKeys: string[] = []; +export function initializePersistentStateForTriggers(context: IExtensionContext) { + _workspaceState = context.workspaceState; +} + +export function getWorkspaceStateValue(key: string, defaultValue?: T): T | undefined { + if (!_workspaceState) { + throw new Error('Workspace state not initialized'); + } + if (defaultValue === undefined) { + return _workspaceState.get(key); + } + return _workspaceState.get(key, defaultValue); +} + +export async function updateWorkspaceStateValue(key: string, value: T): Promise { + if (!_workspaceState) { + throw new Error('Workspace state not initialized'); + } + try { + _workspaceKeys.push(key); + await _workspaceState.update(key, value); + const after = getWorkspaceStateValue(key); + if (JSON.stringify(after) !== JSON.stringify(value)) { + await _workspaceState.update(key, undefined); + await _workspaceState.update(key, value); + traceError('Error while updating workspace state for key:', key); + } + } catch (ex) { + traceError(`Error while updating workspace state for key [${key}]:`, ex); + } +} + +async function clearWorkspaceState(): Promise { + if (_workspaceState !== undefined) { + await Promise.all(_workspaceKeys.map((key) => updateWorkspaceStateValue(key, undefined))); + } +} export class PersistentState implements IPersistentState { constructor( @@ -52,12 +94,8 @@ export class PersistentState implements IPersistentState { // Due to a VSCode bug sometimes the changes are not reflected in the storage, atleast not immediately. // It is noticed however that if we reset the storage first and then update it, it works. // https://github.com/microsoft/vscode/issues/171827 - traceVerbose('Storage update failed for key', this.key, ' retrying by resetting first'); await this.updateValue(undefined as any, false); await this.updateValue(newValue, false); - if (JSON.stringify(this.value) != JSON.stringify(newValue)) { - traceWarn('Retry failed, storage update failed for key', this.key); - } } } catch (ex) { traceError('Error while updating storage for key:', this.key, ex); @@ -68,7 +106,7 @@ export class PersistentState implements IPersistentState { export const GLOBAL_PERSISTENT_KEYS_DEPRECATED = 'PYTHON_EXTENSION_GLOBAL_STORAGE_KEYS'; export const WORKSPACE_PERSISTENT_KEYS_DEPRECATED = 'PYTHON_EXTENSION_WORKSPACE_STORAGE_KEYS'; -const GLOBAL_PERSISTENT_KEYS = 'PYTHON_GLOBAL_STORAGE_KEYS'; +export const GLOBAL_PERSISTENT_KEYS = 'PYTHON_GLOBAL_STORAGE_KEYS'; const WORKSPACE_PERSISTENT_KEYS = 'PYTHON_WORKSPACE_STORAGE_KEYS'; type KeysStorageType = 'global' | 'workspace'; export type KeysStorage = { key: string; defaultValue: unknown }; @@ -90,10 +128,17 @@ export class PersistentStateFactory implements IPersistentStateFactory, IExtensi @inject(IMemento) @named(GLOBAL_MEMENTO) private globalState: Memento, @inject(IMemento) @named(WORKSPACE_MEMENTO) private workspaceState: Memento, @inject(ICommandManager) private cmdManager?: ICommandManager, + @inject(IExtensionContext) @optional() private context?: IExtensionContext, ) {} public async activate(): Promise { - this.cmdManager?.registerCommand(Commands.ClearStorage, this.cleanAllPersistentStates.bind(this)); + this.cmdManager?.registerCommand(Commands.ClearStorage, async () => { + await clearWorkspaceState(); + await this.cleanAllPersistentStates(); + if (useEnvExtension()) { + await clearCache(); + } + }); const globalKeysStorageDeprecated = this.createGlobalPersistentState(GLOBAL_PERSISTENT_KEYS_DEPRECATED, []); const workspaceKeysStorageDeprecated = this.createWorkspacePersistentState( WORKSPACE_PERSISTENT_KEYS_DEPRECATED, @@ -141,6 +186,7 @@ export class PersistentStateFactory implements IPersistentStateFactory, IExtensi } private async cleanAllPersistentStates(): Promise { + const clearCacheDirPromise = this.context ? clearCacheDirectory(this.context).catch() : Promise.resolve(); await Promise.all( this._globalKeysStorage.value.map(async (keyContent) => { const storage = this.createGlobalPersistentState(keyContent.key); @@ -155,6 +201,7 @@ export class PersistentStateFactory implements IPersistentStateFactory, IExtensi ); await this._globalKeysStorage.updateValue([]); await this._workspaceKeysStorage.updateValue([]); + await clearCacheDirPromise; this.cmdManager?.executeCommand('workbench.action.reloadWindow').then(noop); } } @@ -173,7 +220,7 @@ export interface IPersistentStorage { */ export function getGlobalStorage(context: IExtensionContext, key: string, defaultValue?: T): IPersistentStorage { const globalKeysStorage = new PersistentState(context.globalState, GLOBAL_PERSISTENT_KEYS, []); - const found = globalKeysStorage.value.find((value) => value.key === key && value.defaultValue === defaultValue); + const found = globalKeysStorage.value.find((value) => value.key === key); if (!found) { const newValue = [{ key, defaultValue }, ...globalKeysStorage.value]; globalKeysStorage.updateValue(newValue).ignoreErrors(); diff --git a/src/client/common/pipes/namedPipes.ts b/src/client/common/pipes/namedPipes.ts new file mode 100644 index 000000000000..9bffe78f2b9f --- /dev/null +++ b/src/client/common/pipes/namedPipes.ts @@ -0,0 +1,200 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import * as cp from 'child_process'; +import * as crypto from 'crypto'; +import * as fs from 'fs-extra'; +import * as net from 'net'; +import * as os from 'os'; +import * as path from 'path'; +import * as rpc from 'vscode-jsonrpc/node'; +import { CancellationError, CancellationToken, Disposable } from 'vscode'; +import { traceVerbose } from '../../logging'; +import { isWindows } from '../utils/platform'; +import { createDeferred } from '../utils/async'; +import { noop } from '../utils/misc'; + +const { XDG_RUNTIME_DIR } = process.env; +export function generateRandomPipeName(prefix: string): string { + // length of 10 picked because of the name length restriction for sockets + const randomSuffix = crypto.randomBytes(10).toString('hex'); + if (prefix.length === 0) { + prefix = 'python-ext-rpc'; + } + + if (process.platform === 'win32') { + return `\\\\.\\pipe\\${prefix}-${randomSuffix}`; + } + + let result; + if (XDG_RUNTIME_DIR) { + result = path.join(XDG_RUNTIME_DIR, `${prefix}-${randomSuffix}`); + } else { + result = path.join(os.tmpdir(), `${prefix}-${randomSuffix}`); + } + + return result; +} + +async function mkfifo(fifoPath: string): Promise { + return new Promise((resolve, reject) => { + const proc = cp.spawn('mkfifo', [fifoPath]); + proc.on('error', (err) => { + reject(err); + }); + proc.on('exit', (code) => { + if (code === 0) { + resolve(); + } + }); + }); +} + +export async function createWriterPipe(pipeName: string, token?: CancellationToken): Promise { + // windows implementation of FIFO using named pipes + if (isWindows()) { + const deferred = createDeferred(); + const server = net.createServer((socket) => { + traceVerbose(`Pipe connected: ${pipeName}`); + server.close(); + deferred.resolve(new rpc.SocketMessageWriter(socket, 'utf-8')); + }); + + server.on('error', deferred.reject); + server.listen(pipeName); + if (token) { + token.onCancellationRequested(() => { + if (server.listening) { + server.close(); + } + deferred.reject(new CancellationError()); + }); + } + return deferred.promise; + } + // linux implementation of FIFO + await mkfifo(pipeName); + try { + await fs.chmod(pipeName, 0o666); + } catch { + // Intentionally ignored + } + const writer = fs.createWriteStream(pipeName, { + encoding: 'utf-8', + }); + return new rpc.StreamMessageWriter(writer, 'utf-8'); +} + +class CombinedReader implements rpc.MessageReader { + private _onError = new rpc.Emitter(); + + private _onClose = new rpc.Emitter(); + + private _onPartialMessage = new rpc.Emitter(); + + // eslint-disable-next-line class-methods-use-this, @typescript-eslint/no-empty-function + private _callback: rpc.DataCallback = () => {}; + + private _disposables: rpc.Disposable[] = []; + + private _readers: rpc.MessageReader[] = []; + + constructor() { + this._disposables.push(this._onClose, this._onError, this._onPartialMessage); + } + + onError: rpc.Event = this._onError.event; + + onClose: rpc.Event = this._onClose.event; + + onPartialMessage: rpc.Event = this._onPartialMessage.event; + + listen(callback: rpc.DataCallback): rpc.Disposable { + this._callback = callback; + // eslint-disable-next-line no-return-assign, @typescript-eslint/no-empty-function + return new Disposable(() => (this._callback = () => {})); + } + + add(reader: rpc.MessageReader): void { + this._readers.push(reader); + reader.listen((msg) => { + this._callback(msg as rpc.NotificationMessage); + }); + this._disposables.push(reader); + reader.onClose(() => { + this.remove(reader); + if (this._readers.length === 0) { + this._onClose.fire(); + } + }); + reader.onError((e) => { + this.remove(reader); + this._onError.fire(e); + }); + } + + remove(reader: rpc.MessageReader): void { + const found = this._readers.find((r) => r === reader); + if (found) { + this._readers = this._readers.filter((r) => r !== reader); + reader.dispose(); + } + } + + dispose(): void { + this._readers.forEach((r) => r.dispose()); + this._readers = []; + this._disposables.forEach((disposable) => disposable.dispose()); + this._disposables = []; + } +} + +export async function createReaderPipe(pipeName: string, token?: CancellationToken): Promise { + if (isWindows()) { + // windows implementation of FIFO using named pipes + const deferred = createDeferred(); + const combined = new CombinedReader(); + + let refs = 0; + const server = net.createServer((socket) => { + traceVerbose(`Pipe connected: ${pipeName}`); + refs += 1; + + socket.on('close', () => { + refs -= 1; + if (refs <= 0) { + server.close(); + } + }); + combined.add(new rpc.SocketMessageReader(socket, 'utf-8')); + }); + server.on('error', deferred.reject); + server.listen(pipeName); + if (token) { + token.onCancellationRequested(() => { + if (server.listening) { + server.close(); + } + deferred.reject(new CancellationError()); + }); + } + deferred.resolve(combined); + return deferred.promise; + } + // mac/linux implementation of FIFO + await mkfifo(pipeName); + try { + await fs.chmod(pipeName, 0o666); + } catch { + // Intentionally ignored + } + const fd = await fs.open(pipeName, fs.constants.O_RDONLY | fs.constants.O_NONBLOCK); + const socket = new net.Socket({ fd }); + const reader = new rpc.SocketMessageReader(socket, 'utf-8'); + socket.on('close', () => { + fs.close(fd).catch(noop); + reader.dispose(); + }); + + return reader; +} diff --git a/src/client/common/platform/constants.ts b/src/client/common/platform/constants.ts deleted file mode 100644 index 808a63188c1d..000000000000 --- a/src/client/common/platform/constants.ts +++ /dev/null @@ -1,7 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -// TODO : Drop all these in favor of IPlatformService. -// See https://github.com/microsoft/vscode-python/issues/8542. - -export const IS_WINDOWS = /^win/.test(process.platform); diff --git a/src/client/common/platform/fileSystem.ts b/src/client/common/platform/fileSystem.ts index 8f962b0f776f..3e7f441654ec 100644 --- a/src/client/common/platform/fileSystem.ts +++ b/src/client/common/platform/fileSystem.ts @@ -333,7 +333,7 @@ export class FileSystemUtils implements IFileSystemUtils { pathUtils.paths, tmp || TemporaryFileSystem.withDefaults(), getHash || getHashString, - globFiles || promisify(glob), + globFiles || promisify(glob.default), ); } diff --git a/src/client/common/platform/fs-paths.ts b/src/client/common/platform/fs-paths.ts index 2d46fca98526..fa809d31b0b9 100644 --- a/src/client/common/platform/fs-paths.ts +++ b/src/client/common/platform/fs-paths.ts @@ -3,11 +3,11 @@ import * as nodepath from 'path'; import { getSearchPathEnvVarNames } from '../utils/exec'; +import * as fs from 'fs-extra'; +import * as os from 'os'; import { getOSType, OSType } from '../utils/platform'; import { IExecutables, IFileSystemPaths, IFileSystemPathUtils } from './types'; -const untildify = require('untildify'); - // The parts of node's 'path' module used by FileSystemPaths. interface INodePath { sep: string; @@ -119,7 +119,7 @@ export class FileSystemPathUtils implements IFileSystemPathUtils { } return new FileSystemPathUtils( // Use the current user's home directory. - untildify('~'), + os.homedir(), paths, Executables.withDefaults(), // Use the actual node "path" module. @@ -170,3 +170,201 @@ export function isParentPath(filePath: string, parentPath: string): boolean { export function arePathsSame(path1: string, path2: string): boolean { return normCasePath(path1) === normCasePath(path2); } + +export async function copyFile(src: string, dest: string): Promise { + const destDir = nodepath.dirname(dest); + if (!(await fs.pathExists(destDir))) { + await fs.mkdirp(destDir); + } + + await fs.copy(src, dest, { + overwrite: true, + }); +} + +// These function exist so we can stub them out in tests. We can't stub out the fs module directly +// because of the way that sinon does stubbing, so we have these intermediaries instead. +export { Stats, WriteStream, ReadStream, PathLike, Dirent, PathOrFileDescriptor } from 'fs-extra'; + +export function existsSync(path: string): boolean { + return fs.existsSync(path); +} + +export function readFileSync(filePath: string, encoding: BufferEncoding): string; +export function readFileSync(filePath: string): Buffer; +export function readFileSync(filePath: string, options: { encoding: BufferEncoding }): string; +export function readFileSync( + filePath: string, + options?: { encoding: BufferEncoding } | BufferEncoding | undefined, +): string | Buffer { + if (typeof options === 'string') { + return fs.readFileSync(filePath, { encoding: options }); + } + return fs.readFileSync(filePath, options); +} + +export function readJSONSync(filePath: string): any { + return fs.readJSONSync(filePath); +} + +export function readdirSync(path: string): string[]; +export function readdirSync( + path: string, + options: fs.ObjectEncodingOptions & { + withFileTypes: true; + }, +): fs.Dirent[]; +export function readdirSync( + path: string, + options: fs.ObjectEncodingOptions & { + withFileTypes: false; + }, +): string[]; +export function readdirSync( + path: fs.PathLike, + options?: fs.ObjectEncodingOptions & { + withFileTypes: boolean; + recursive?: boolean | undefined; + }, +): string[] | fs.Dirent[] { + if (options === undefined || options.withFileTypes === false) { + return fs.readdirSync(path); + } + return fs.readdirSync(path, { ...options, withFileTypes: true }); +} + +export function readlink(path: string): Promise { + return fs.readlink(path); +} + +export function unlink(path: string): Promise { + return fs.unlink(path); +} + +export function symlink(target: string, path: string, type?: fs.SymlinkType): Promise { + return fs.symlink(target, path, type); +} + +export function symlinkSync(target: string, path: string, type?: fs.SymlinkType): void { + return fs.symlinkSync(target, path, type); +} + +export function unlinkSync(path: string): void { + return fs.unlinkSync(path); +} + +export function statSync(path: string): fs.Stats { + return fs.statSync(path); +} + +export function stat(path: string): Promise { + return fs.stat(path); +} + +export function lstat(path: string): Promise { + return fs.lstat(path); +} + +export function chmod(path: string, mod: fs.Mode): Promise { + return fs.chmod(path, mod); +} + +export function createReadStream(path: string): fs.ReadStream { + return fs.createReadStream(path); +} + +export function createWriteStream(path: string): fs.WriteStream { + return fs.createWriteStream(path); +} + +export function pathExistsSync(path: string): boolean { + return fs.pathExistsSync(path); +} + +export function pathExists(absPath: string): Promise { + return fs.pathExists(absPath); +} + +export function createFile(filename: string): Promise { + return fs.createFile(filename); +} + +export function rmdir(path: string, options?: fs.RmDirOptions): Promise { + return fs.rmdir(path, options); +} + +export function remove(path: string): Promise { + return fs.remove(path); +} + +export function readFile(filePath: string, encoding: BufferEncoding): Promise; +export function readFile(filePath: string): Promise; +export function readFile(filePath: string, options: { encoding: BufferEncoding }): Promise; +export function readFile( + filePath: string, + options?: { encoding: BufferEncoding } | BufferEncoding | undefined, +): Promise { + if (typeof options === 'string') { + return fs.readFile(filePath, { encoding: options }); + } + return fs.readFile(filePath, options); +} + +export function readJson(filePath: string): Promise { + return fs.readJson(filePath); +} + +export function writeFile(filePath: string, data: any, options?: { encoding: BufferEncoding }): Promise { + return fs.writeFile(filePath, data, options); +} + +export function mkdir(dirPath: string): Promise { + return fs.mkdir(dirPath); +} + +export function mkdirp(dirPath: string): Promise { + return fs.mkdirp(dirPath); +} + +export function rename(oldPath: string, newPath: string): Promise { + return fs.rename(oldPath, newPath); +} + +export function ensureDir(dirPath: string): Promise { + return fs.ensureDir(dirPath); +} + +export function ensureFile(filePath: string): Promise { + return fs.ensureFile(filePath); +} + +export function ensureSymlink(target: string, filePath: string, type?: fs.SymlinkType): Promise { + return fs.ensureSymlink(target, filePath, type); +} + +export function appendFile(filePath: string, data: any, options?: { encoding: BufferEncoding }): Promise { + return fs.appendFile(filePath, data, options); +} + +export function readdir(path: string): Promise; +export function readdir( + path: string, + options: fs.ObjectEncodingOptions & { + withFileTypes: true; + }, +): Promise; +export function readdir( + path: fs.PathLike, + options?: fs.ObjectEncodingOptions & { + withFileTypes: true; + }, +): Promise { + if (options === undefined) { + return fs.readdir(path); + } + return fs.readdir(path, options); +} + +export function emptyDir(dirPath: string): Promise { + return fs.emptyDir(dirPath); +} diff --git a/src/client/common/platform/fs-temp.ts b/src/client/common/platform/fs-temp.ts index 32b57df15387..60dde040f454 100644 --- a/src/client/common/platform/fs-temp.ts +++ b/src/client/common/platform/fs-temp.ts @@ -5,14 +5,7 @@ import * as tmp from 'tmp'; import { ITempFileSystem, TemporaryFile } from './types'; interface IRawTempFS { - // TODO (https://github.com/microsoft/vscode/issues/84517) - // This functionality has been requested for the - // VS Code FS API (vscode.workspace.fs.*). - file( - config: tmp.Options, - - callback?: (err: any, path: string, fd: number, cleanupCallback: () => void) => void, - ): void; + fileSync(config?: tmp.Options): tmp.SynchrounousResult; } // Operations related to temporary files and directories. @@ -35,14 +28,13 @@ export class TemporaryFileSystem implements ITempFileSystem { mode, }; return new Promise((resolve, reject) => { - this.raw.file(opts, (err, filename, _fd, cleanUp) => { - if (err) { - return reject(err); - } - resolve({ - filePath: filename, - dispose: cleanUp, - }); + const { name, removeCallback } = this.raw.fileSync(opts); + if (!name) { + return reject(new Error('Failed to create temp file')); + } + resolve({ + filePath: name, + dispose: removeCallback, }); }); } diff --git a/src/client/common/platform/pathUtils.ts b/src/client/common/platform/pathUtils.ts index ed3dc28b1de5..b3be39f4644b 100644 --- a/src/client/common/platform/pathUtils.ts +++ b/src/client/common/platform/pathUtils.ts @@ -6,8 +6,7 @@ import * as path from 'path'; import { IPathUtils, IsWindows } from '../types'; import { OSType } from '../utils/platform'; import { Executables, FileSystemPaths, FileSystemPathUtils } from './fs-paths'; - -const untildify = require('untildify'); +import { untildify } from '../helpers'; @injectable() export class PathUtils implements IPathUtils { diff --git a/src/client/common/platform/platformService.ts b/src/client/common/platform/platformService.ts index 0277c1bcd2a2..dc9b04cc652c 100644 --- a/src/client/common/platform/platformService.ts +++ b/src/client/common/platform/platformService.ts @@ -7,7 +7,7 @@ import { injectable } from 'inversify'; import * as os from 'os'; import { coerce, SemVer } from 'semver'; import { getSearchPathEnvVarNames } from '../utils/exec'; -import { Architecture, getArchitecture, getOSType, OSType } from '../utils/platform'; +import { Architecture, getArchitecture, getOSType, isWindows, OSType } from '../utils/platform'; import { parseSemVerSafe } from '../utils/version'; import { IPlatformService } from './types'; @@ -50,8 +50,9 @@ export class PlatformService implements IPlatformService { } } + // eslint-disable-next-line class-methods-use-this public get isWindows(): boolean { - return this.osType === OSType.Windows; + return isWindows(); } public get isMac(): boolean { diff --git a/src/client/common/process/internal/scripts/constants.ts b/src/client/common/process/internal/scripts/constants.ts index 4448f7e639ce..6954592ed3dd 100644 --- a/src/client/common/process/internal/scripts/constants.ts +++ b/src/client/common/process/internal/scripts/constants.ts @@ -5,4 +5,4 @@ import * as path from 'path'; import { EXTENSION_ROOT_DIR } from '../../../constants'; // It is simpler to hard-code it instead of using vscode.ExtensionContext.extensionPath. -export const _SCRIPTS_DIR = path.join(EXTENSION_ROOT_DIR, 'pythonFiles'); +export const _SCRIPTS_DIR = path.join(EXTENSION_ROOT_DIR, 'python_files'); diff --git a/src/client/common/process/internal/scripts/index.ts b/src/client/common/process/internal/scripts/index.ts index c52983d9910b..f2c905c02889 100644 --- a/src/client/common/process/internal/scripts/index.ts +++ b/src/client/common/process/internal/scripts/index.ts @@ -7,7 +7,7 @@ import { _SCRIPTS_DIR } from './constants'; const SCRIPTS_DIR = _SCRIPTS_DIR; // "scripts" contains everything relevant to the scripts found under -// the top-level "pythonFiles" directory. Each of those scripts has +// the top-level "python_files" directory. Each of those scripts has // a function in this module which matches the script's filename. // Each function provides the commandline arguments that should be // used when invoking a Python executable, whether through spawn/exec @@ -18,16 +18,13 @@ const SCRIPTS_DIR = _SCRIPTS_DIR; // into the corresponding object or objects. "parse()" takes a single // string as the stdout text and returns the relevant data. // -// Some of the scripts are located in subdirectories of "pythonFiles". +// Some of the scripts are located in subdirectories of "python_files". // For each of those subdirectories there is a sub-module where // those scripts' functions may be found. // // In some cases one or more types related to a script are exported // from the same module in which the script's function is located. // These types typically relate to the return type of "parse()". -// -// ignored scripts: -// * install_debugpy.py (used only for extension development) export * as testingTools from './testing_tools'; // interpreterInfo.py @@ -110,6 +107,13 @@ export function testlauncher(testArgs: string[]): string[] { return [script, ...testArgs]; } +// run_pytest_script.py +export function pytestlauncher(testArgs: string[]): string[] { + const script = path.join(SCRIPTS_DIR, 'vscode_pytest', 'run_pytest_script.py'); + // There is no output to parse, so we do not return a function. + return [script, ...testArgs]; +} + // visualstudio_py_testlauncher.py // eslint-disable-next-line camelcase @@ -149,3 +153,8 @@ export function createCondaScript(): string { const script = path.join(SCRIPTS_DIR, 'create_conda.py'); return script; } + +export function installedCheckScript(): string { + const script = path.join(SCRIPTS_DIR, 'installed_check.py'); + return script; +} diff --git a/src/client/common/process/logger.ts b/src/client/common/process/logger.ts index 1c0b78dd941f..b65da8dc81e5 100644 --- a/src/client/common/process/logger.ts +++ b/src/client/common/process/logger.ts @@ -12,6 +12,7 @@ import { IProcessLogger, SpawnOptions } from './types'; import { escapeRegExp } from 'lodash'; import { replaceAll } from '../stringUtils'; import { identifyShellFromShellPath } from '../terminal/shellDetectors/baseShellDetector'; +import '../../common/extensions'; @injectable() export class ProcessLogger implements IProcessLogger { @@ -40,6 +41,13 @@ export class ProcessLogger implements IProcessLogger { }); } + /** + * Formats command strings for display by replacing common paths with symbols. + * - Replaces the workspace folder path with '.' if there's exactly one workspace folder + * - Replaces the user's home directory path with '~' + * @param command The command string to format + * @returns The formatted command string with paths replaced by symbols + */ private getDisplayCommands(command: string): string { if (this.workspaceService.workspaceFolders && this.workspaceService.workspaceFolders.length === 1) { command = replaceMatchesWithCharacter(command, this.workspaceService.workspaceFolders[0].uri.fsPath, '.'); diff --git a/src/client/common/process/proc.ts b/src/client/common/process/proc.ts index 0ac610e3eac9..4a5aa984fa44 100644 --- a/src/client/common/process/proc.ts +++ b/src/client/common/process/proc.ts @@ -7,6 +7,7 @@ import { IDisposable } from '../types'; import { EnvironmentVariables } from '../variables/types'; import { execObservable, killPid, plainExec, shellExec } from './rawProcessApis'; import { ExecutionResult, IProcessService, ObservableExecutionResult, ShellOptions, SpawnOptions } from './types'; +import { workerPlainExec, workerShellExec } from './worker/rawProcessApiWrapper'; export class ProcessService extends EventEmitter implements IProcessService { private processesToKill = new Set(); @@ -40,21 +41,30 @@ export class ProcessService extends EventEmitter implements IProcessService { } public execObservable(file: string, args: string[], options: SpawnOptions = {}): ObservableExecutionResult { - const result = execObservable(file, args, options, this.env, this.processesToKill); + const execOptions = { ...options, doNotLog: true }; + const result = execObservable(file, args, execOptions, this.env, this.processesToKill); this.emit('exec', file, args, options); return result; } public exec(file: string, args: string[], options: SpawnOptions = {}): Promise> { - const promise = plainExec(file, args, options, this.env, this.processesToKill); this.emit('exec', file, args, options); + if (options.useWorker) { + return workerPlainExec(file, args, options); + } + const execOptions = { ...options, doNotLog: true }; + const promise = plainExec(file, args, execOptions, this.env, this.processesToKill); return promise; } public shellExec(command: string, options: ShellOptions = {}): Promise> { this.emit('exec', command, undefined, options); + if (options.useWorker) { + return workerShellExec(command, options); + } const disposables = new Set(); - return shellExec(command, options, this.env, disposables).finally(() => { + const shellOptions = { ...options, doNotLog: true }; + return shellExec(command, shellOptions, this.env, disposables).finally(() => { // Ensure the process we started is cleaned up. disposables.forEach((p) => { try { diff --git a/src/client/common/process/processFactory.ts b/src/client/common/process/processFactory.ts index 8681d5073d8e..40204a640dae 100644 --- a/src/client/common/process/processFactory.ts +++ b/src/client/common/process/processFactory.ts @@ -17,8 +17,10 @@ export class ProcessServiceFactory implements IProcessServiceFactory { @inject(IProcessLogger) private readonly processLogger: IProcessLogger, @inject(IDisposableRegistry) private readonly disposableRegistry: IDisposableRegistry, ) {} - public async create(resource?: Uri): Promise { - const customEnvVars = await this.envVarsService.getEnvironmentVariables(resource); + public async create(resource?: Uri, options?: { doNotUseCustomEnvs: boolean }): Promise { + const customEnvVars = options?.doNotUseCustomEnvs + ? undefined + : await this.envVarsService.getEnvironmentVariables(resource); const proc: IProcessService = new ProcessService(customEnvVars); this.disposableRegistry.push(proc); return proc.on('exec', this.processLogger.logProcess.bind(this.processLogger)); diff --git a/src/client/common/process/pythonEnvironment.ts b/src/client/common/process/pythonEnvironment.ts index 9566f373aa91..cbf898ac5f50 100644 --- a/src/client/common/process/pythonEnvironment.ts +++ b/src/client/common/process/pythonEnvironment.ts @@ -12,6 +12,7 @@ import { isTestExecution } from '../constants'; import { IFileSystem } from '../platform/types'; import * as internalPython from './internal/python'; import { ExecutionResult, IProcessService, IPythonEnvironment, ShellOptions, SpawnOptions } from './types'; +import { PixiEnvironmentInfo } from '../../pythonEnvironments/common/environmentManagers/pixi'; const cachedExecutablePath: Map> = new Map>(); @@ -173,6 +174,23 @@ export async function createCondaEnv( return new PythonEnvironment(interpreterPath, deps); } +export async function createPixiEnv( + pixiEnv: PixiEnvironmentInfo, + // These are used to generate the deps. + procs: IProcessService, + fs: IFileSystem, +): Promise { + const pythonArgv = pixiEnv.pixi.getRunPythonArgs(pixiEnv.manifestPath, pixiEnv.envName); + const deps = createDeps( + async (filename) => fs.pathExists(filename), + pythonArgv, + pythonArgv, + (file, args, opts) => procs.exec(file, args, opts), + (command, opts) => procs.shellExec(command, opts), + ); + return new PythonEnvironment(pixiEnv.interpreterPath, deps); +} + export function createMicrosoftStoreEnv( pythonPath: string, // These are used to generate the deps. diff --git a/src/client/common/process/pythonExecutionFactory.ts b/src/client/common/process/pythonExecutionFactory.ts index fc13e7f2346c..efb05c3c9d12 100644 --- a/src/client/common/process/pythonExecutionFactory.ts +++ b/src/client/common/process/pythonExecutionFactory.ts @@ -10,7 +10,7 @@ import { EventName } from '../../telemetry/constants'; import { IFileSystem } from '../platform/types'; import { IConfigurationService, IDisposableRegistry, IInterpreterPathService } from '../types'; import { ProcessService } from './proc'; -import { createCondaEnv, createPythonEnv, createMicrosoftStoreEnv } from './pythonEnvironment'; +import { createCondaEnv, createPythonEnv, createMicrosoftStoreEnv, createPixiEnv } from './pythonEnvironment'; import { createPythonProcessService } from './pythonProcess'; import { ExecutionFactoryCreateWithEnvironmentOptions, @@ -25,6 +25,7 @@ import { import { IInterpreterAutoSelectionService } from '../../interpreter/autoSelection/types'; import { sleep } from '../utils/async'; import { traceError } from '../../logging'; +import { getPixi, getPixiEnvironmentFromInterpreter } from '../../pythonEnvironments/common/environmentManagers/pixi'; @injectable() export class PythonExecutionFactory implements IPythonExecutionFactory { @@ -79,6 +80,13 @@ export class PythonExecutionFactory implements IPythonExecutionFactory { } const processService: IProcessService = await this.processServiceFactory.create(options.resource); + if (await getPixi()) { + const pixiExecutionService = await this.createPixiExecutionService(pythonPath, processService); + if (pixiExecutionService) { + return pixiExecutionService; + } + } + const condaExecutionService = await this.createCondaExecutionService(pythonPath, processService); if (condaExecutionService) { return condaExecutionService; @@ -116,10 +124,18 @@ export class PythonExecutionFactory implements IPythonExecutionFactory { processService.on('exec', this.logger.logProcess.bind(this.logger)); this.disposables.push(processService); + if (await getPixi()) { + const pixiExecutionService = await this.createPixiExecutionService(pythonPath, processService); + if (pixiExecutionService) { + return pixiExecutionService; + } + } + const condaExecutionService = await this.createCondaExecutionService(pythonPath, processService); if (condaExecutionService) { return condaExecutionService; } + const env = createPythonEnv(pythonPath, processService, this.fileSystem); return createPythonService(processService, env); } @@ -139,6 +155,23 @@ export class PythonExecutionFactory implements IPythonExecutionFactory { } return createPythonService(processService, env); } + + public async createPixiExecutionService( + pythonPath: string, + processService: IProcessService, + ): Promise { + const pixiEnvironment = await getPixiEnvironmentFromInterpreter(pythonPath); + if (!pixiEnvironment) { + return undefined; + } + + const env = await createPixiEnv(pixiEnvironment, processService, this.fileSystem); + if (env) { + return createPythonService(processService, env); + } + + return undefined; + } } function createPythonService(procService: IProcessService, env: IPythonEnvironment): IPythonExecutionService { diff --git a/src/client/common/process/rawProcessApis.ts b/src/client/common/process/rawProcessApis.ts index 025e5b607229..864191851c91 100644 --- a/src/client/common/process/rawProcessApis.ts +++ b/src/client/common/process/rawProcessApis.ts @@ -12,6 +12,8 @@ import { ExecutionResult, ObservableExecutionResult, Output, ShellOptions, Spawn import { noop } from '../utils/misc'; import { decodeBuffer } from './decoder'; import { traceVerbose } from '../../logging'; +import { WorkspaceService } from '../application/workspace'; +import { ProcessLogger } from './logger'; const PS_ERROR_SCREEN_BOGUS = /your [0-9]+x[0-9]+ screen size is bogus\. expect trouble/; @@ -49,12 +51,16 @@ function getDefaultOptions(options: T, de export function shellExec( command: string, - options: ShellOptions = {}, + options: ShellOptions & { doNotLog?: boolean } = {}, defaultEnv?: EnvironmentVariables, disposables?: Set, ): Promise> { const shellOptions = getDefaultOptions(options, defaultEnv); - traceVerbose(`Shell Exec: ${command} with options: ${JSON.stringify(shellOptions, null, 4)}`); + if (!options.doNotLog) { + const processLogger = new ProcessLogger(new WorkspaceService()); + const loggingOptions = { ...shellOptions, encoding: shellOptions.encoding ?? undefined }; + processLogger.logProcess(command, undefined, loggingOptions); + } return new Promise((resolve, reject) => { // eslint-disable-next-line @typescript-eslint/no-explicit-any const callback = (e: any, stdout: any, stderr: any) => { @@ -69,11 +75,26 @@ export function shellExec( resolve({ stderr: stderr && stderr.length > 0 ? stderr : undefined, stdout }); } }; + let procExited = false; const proc = exec(command, shellOptions, callback); // NOSONAR + proc.once('close', () => { + procExited = true; + }); + proc.once('exit', () => { + procExited = true; + }); + proc.once('error', () => { + procExited = true; + }); const disposable: IDisposable = { dispose: () => { - if (!proc.killed) { - proc.kill(); + // If process has not exited nor killed, force kill it. + if (!procExited && !proc.killed) { + if (proc.pid) { + killPid(proc.pid); + } else { + proc.kill(); + } } }, }; @@ -86,12 +107,16 @@ export function shellExec( export function plainExec( file: string, args: string[], - options: SpawnOptions = {}, + options: SpawnOptions & { doNotLog?: boolean } = {}, defaultEnv?: EnvironmentVariables, disposables?: Set, ): Promise> { const spawnOptions = getDefaultOptions(options, defaultEnv); const encoding = spawnOptions.encoding ? spawnOptions.encoding : 'utf8'; + if (!options.doNotLog) { + const processLogger = new ProcessLogger(new WorkspaceService()); + processLogger.logProcess(file, args, options); + } const proc = spawn(file, args, spawnOptions); // Listen to these errors (unhandled errors in streams tears down the process). // Errors will be bubbled up to the `error` event in `proc`, hence no need to log. @@ -100,8 +125,13 @@ export function plainExec( const deferred = createDeferred>(); const disposable: IDisposable = { dispose: () => { + // If process has not exited nor killed, force kill it. if (!proc.killed && !deferred.completed) { - proc.kill(); + if (proc.pid) { + killPid(proc.pid); + } else { + proc.kill(); + } } }, }; @@ -156,10 +186,12 @@ export function plainExec( deferred.resolve({ stdout, stderr }); } internalDisposables.forEach((d) => d.dispose()); + disposable.dispose(); }); proc.once('error', (ex) => { deferred.reject(ex); internalDisposables.forEach((d) => d.dispose()); + disposable.dispose(); }); return deferred.promise; @@ -182,12 +214,16 @@ function removeCondaRunMarkers(out: string) { export function execObservable( file: string, args: string[], - options: SpawnOptions = {}, + options: SpawnOptions & { doNotLog?: boolean } = {}, defaultEnv?: EnvironmentVariables, disposables?: Set, ): ObservableExecutionResult { const spawnOptions = getDefaultOptions(options, defaultEnv); const encoding = spawnOptions.encoding ? spawnOptions.encoding : 'utf8'; + if (!options.doNotLog) { + const processLogger = new ProcessLogger(new WorkspaceService()); + processLogger.logProcess(file, args, options); + } const proc = spawn(file, args, spawnOptions); let procExited = false; const disposable: IDisposable = { @@ -217,7 +253,11 @@ export function execObservable( internalDisposables.push( options.token.onCancellationRequested(() => { if (!procExited && !proc.killed) { - proc.kill(); + if (proc.pid) { + killPid(proc.pid); + } else { + proc.kill(); + } procExited = true; } }), @@ -255,6 +295,10 @@ export function execObservable( subscriber.error(ex); internalDisposables.forEach((d) => d.dispose()); }); + if (options.stdinStr !== undefined) { + proc.stdin?.write(options.stdinStr); + proc.stdin?.end(); + } }); return { @@ -273,6 +317,6 @@ export function killPid(pid: number): void { process.kill(pid); } } catch { - // Ignore. + traceVerbose('Unable to kill process with pid', pid); } } diff --git a/src/client/common/process/types.ts b/src/client/common/process/types.ts index 8298957285e8..9263e69cbe21 100644 --- a/src/client/common/process/types.ts +++ b/src/client/common/process/types.ts @@ -25,9 +25,11 @@ export type SpawnOptions = ChildProcessSpawnOptions & { throwOnStdErr?: boolean; extraVariables?: NodeJS.ProcessEnv; outputChannel?: OutputChannel; + stdinStr?: string; + useWorker?: boolean; }; -export type ShellOptions = ExecOptions & { throwOnStdErr?: boolean }; +export type ShellOptions = ExecOptions & { throwOnStdErr?: boolean; useWorker?: boolean }; export type ExecutionResult = { stdout: T; @@ -54,7 +56,7 @@ export interface IProcessService extends IDisposable { export const IProcessServiceFactory = Symbol('IProcessServiceFactory'); export interface IProcessServiceFactory { - create(resource?: Uri): Promise; + create(resource?: Uri, options?: { doNotUseCustomEnvs: boolean }): Promise; } export const IPythonExecutionFactory = Symbol('IPythonExecutionFactory'); diff --git a/src/client/common/process/worker/main.ts b/src/client/common/process/worker/main.ts new file mode 100644 index 000000000000..324673618942 --- /dev/null +++ b/src/client/common/process/worker/main.ts @@ -0,0 +1,42 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { Worker } from 'worker_threads'; +import * as path from 'path'; +import { traceVerbose, traceError } from '../../../logging/index'; + +/** + * Executes a worker file. Make sure to declare the worker file as a entry in the webpack config. + * @param workerFileName Filename of the worker file to execute, it has to end with ".worker.js" for webpack to bundle it. + * @param workerData Arguments to the worker file. + * @returns + */ +// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/explicit-module-boundary-types +export async function executeWorkerFile(workerFileName: string, workerData: any): Promise { + if (!workerFileName.endsWith('.worker.js')) { + throw new Error('Worker file must end with ".worker.js" for webpack to bundle webworkers'); + } + return new Promise((resolve, reject) => { + const worker = new Worker(workerFileName, { workerData }); + const id = worker.threadId; + traceVerbose( + `Worker id ${id} for file ${path.basename(workerFileName)} with data ${JSON.stringify(workerData)}`, + ); + worker.on('message', (msg: { err: Error; res: unknown }) => { + if (msg.err) { + reject(msg.err); + } + resolve(msg.res); + }); + worker.on('error', (ex: Error) => { + traceError(`Error in worker ${workerFileName}`, ex); + reject(ex); + }); + worker.on('exit', (code) => { + traceVerbose(`Worker id ${id} exited with code ${code}`); + if (code !== 0) { + reject(new Error(`Worker ${workerFileName} stopped with exit code ${code}`)); + } + }); + }); +} diff --git a/src/client/common/process/worker/plainExec.worker.ts b/src/client/common/process/worker/plainExec.worker.ts new file mode 100644 index 000000000000..f44ea15f9653 --- /dev/null +++ b/src/client/common/process/worker/plainExec.worker.ts @@ -0,0 +1,16 @@ +import { parentPort, workerData } from 'worker_threads'; +import { _workerPlainExecImpl } from './workerRawProcessApis'; + +_workerPlainExecImpl(workerData.file, workerData.args, workerData.options) + .then((res) => { + if (!parentPort) { + throw new Error('Not in a worker thread'); + } + parentPort.postMessage({ res }); + }) + .catch((err) => { + if (!parentPort) { + throw new Error('Not in a worker thread'); + } + parentPort.postMessage({ err }); + }); diff --git a/src/client/common/process/worker/rawProcessApiWrapper.ts b/src/client/common/process/worker/rawProcessApiWrapper.ts new file mode 100644 index 000000000000..e6476df5d8fa --- /dev/null +++ b/src/client/common/process/worker/rawProcessApiWrapper.ts @@ -0,0 +1,26 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { SpawnOptions } from 'child_process'; +import * as path from 'path'; +import { executeWorkerFile } from './main'; +import { ExecutionResult, ShellOptions } from './types'; + +export function workerShellExec(command: string, options: ShellOptions): Promise> { + return executeWorkerFile(path.join(__dirname, 'shellExec.worker.js'), { + command, + options, + }); +} + +export function workerPlainExec( + file: string, + args: string[], + options: SpawnOptions = {}, +): Promise> { + return executeWorkerFile(path.join(__dirname, 'plainExec.worker.js'), { + file, + args, + options, + }); +} diff --git a/src/client/common/process/worker/shellExec.worker.ts b/src/client/common/process/worker/shellExec.worker.ts new file mode 100644 index 000000000000..f4e9809a29a5 --- /dev/null +++ b/src/client/common/process/worker/shellExec.worker.ts @@ -0,0 +1,16 @@ +import { parentPort, workerData } from 'worker_threads'; +import { _workerShellExecImpl } from './workerRawProcessApis'; + +_workerShellExecImpl(workerData.command, workerData.options, workerData.defaultEnv) + .then((res) => { + if (!parentPort) { + throw new Error('Not in a worker thread'); + } + parentPort.postMessage({ res }); + }) + .catch((ex) => { + if (!parentPort) { + throw new Error('Not in a worker thread'); + } + parentPort.postMessage({ ex }); + }); diff --git a/src/client/common/process/worker/types.ts b/src/client/common/process/worker/types.ts new file mode 100644 index 000000000000..5c58aec10214 --- /dev/null +++ b/src/client/common/process/worker/types.ts @@ -0,0 +1,38 @@ +/* eslint-disable @typescript-eslint/no-empty-function */ +/* eslint-disable @typescript-eslint/explicit-module-boundary-types */ +import { ExecOptions, SpawnOptions as ChildProcessSpawnOptions } from 'child_process'; + +export function noop() {} +export interface IDisposable { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + dispose(): void | undefined | Promise; +} +export type EnvironmentVariables = Record; +export class StdErrError extends Error { + // eslint-disable-next-line @typescript-eslint/no-useless-constructor + constructor(message: string) { + super(message); + } +} + +export type SpawnOptions = ChildProcessSpawnOptions & { + encoding?: string; + // /** + // * Can't use `CancellationToken` here as it comes from vscode which is not available in worker threads. + // */ + // token?: CancellationToken; + mergeStdOutErr?: boolean; + throwOnStdErr?: boolean; + extraVariables?: NodeJS.ProcessEnv; + // /** + // * Can't use `OutputChannel` here as it comes from vscode which is not available in worker threads. + // */ + // outputChannel?: OutputChannel; + stdinStr?: string; +}; +export type ShellOptions = ExecOptions & { throwOnStdErr?: boolean }; + +export type ExecutionResult = { + stdout: T; + stderr?: T; +}; diff --git a/src/client/common/process/worker/workerRawProcessApis.ts b/src/client/common/process/worker/workerRawProcessApis.ts new file mode 100644 index 000000000000..cfae9b1e6471 --- /dev/null +++ b/src/client/common/process/worker/workerRawProcessApis.ts @@ -0,0 +1,214 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +// !!!! IMPORTANT: DO NOT IMPORT FROM VSCODE MODULE AS IT IS NOT AVAILABLE INSIDE WORKER THREADS !!!! + +import { exec, execSync, spawn } from 'child_process'; +import { Readable } from 'stream'; +import { createDeferred } from '../../utils/async'; +import { DEFAULT_ENCODING } from '../constants'; +import { decodeBuffer } from '../decoder'; +import { + ShellOptions, + SpawnOptions, + EnvironmentVariables, + IDisposable, + noop, + StdErrError, + ExecutionResult, +} from './types'; +import { traceWarn } from '../../../logging'; + +const PS_ERROR_SCREEN_BOGUS = /your [0-9]+x[0-9]+ screen size is bogus\. expect trouble/; + +function getDefaultOptions(options: T, defaultEnv?: EnvironmentVariables): T { + const defaultOptions = { ...options }; + const execOptions = defaultOptions as SpawnOptions; + if (execOptions) { + execOptions.encoding = + typeof execOptions.encoding === 'string' && execOptions.encoding.length > 0 + ? execOptions.encoding + : DEFAULT_ENCODING; + const { encoding } = execOptions; + delete execOptions.encoding; + execOptions.encoding = encoding; + } + if (!defaultOptions.env || Object.keys(defaultOptions.env).length === 0) { + const env = defaultEnv || process.env; + defaultOptions.env = { ...env }; + } else { + defaultOptions.env = { ...defaultOptions.env }; + } + + if (execOptions && execOptions.extraVariables) { + defaultOptions.env = { ...defaultOptions.env, ...execOptions.extraVariables }; + } + + // Always ensure we have unbuffered output. + defaultOptions.env.PYTHONUNBUFFERED = '1'; + if (!defaultOptions.env.PYTHONIOENCODING) { + defaultOptions.env.PYTHONIOENCODING = 'utf-8'; + } + + return defaultOptions; +} + +export function _workerShellExecImpl( + command: string, + options: ShellOptions, + defaultEnv?: EnvironmentVariables, + disposables?: Set, +): Promise> { + const shellOptions = getDefaultOptions(options, defaultEnv); + return new Promise((resolve, reject) => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const callback = (e: any, stdout: any, stderr: any) => { + if (e && e !== null) { + reject(e); + } else if (shellOptions.throwOnStdErr && stderr && stderr.length) { + reject(new Error(stderr)); + } else { + stdout = filterOutputUsingCondaRunMarkers(stdout); + // Make sure stderr is undefined if we actually had none. This is checked + // elsewhere because that's how exec behaves. + resolve({ stderr: stderr && stderr.length > 0 ? stderr : undefined, stdout }); + } + }; + let procExited = false; + const proc = exec(command, shellOptions, callback); // NOSONAR + proc.once('close', () => { + procExited = true; + }); + proc.once('exit', () => { + procExited = true; + }); + proc.once('error', () => { + procExited = true; + }); + const disposable: IDisposable = { + dispose: () => { + // If process has not exited nor killed, force kill it. + if (!procExited && !proc.killed) { + if (proc.pid) { + killPid(proc.pid); + } else { + proc.kill(); + } + } + }, + }; + if (disposables) { + disposables.add(disposable); + } + }); +} + +export function _workerPlainExecImpl( + file: string, + args: string[], + options: SpawnOptions & { doNotLog?: boolean } = {}, + defaultEnv?: EnvironmentVariables, + disposables?: Set, +): Promise> { + const spawnOptions = getDefaultOptions(options, defaultEnv); + const encoding = spawnOptions.encoding ? spawnOptions.encoding : 'utf8'; + const proc = spawn(file, args, spawnOptions); + // Listen to these errors (unhandled errors in streams tears down the process). + // Errors will be bubbled up to the `error` event in `proc`, hence no need to log. + proc.stdout?.on('error', noop); + proc.stderr?.on('error', noop); + const deferred = createDeferred>(); + const disposable: IDisposable = { + dispose: () => { + // If process has not exited nor killed, force kill it. + if (!proc.killed && !deferred.completed) { + if (proc.pid) { + killPid(proc.pid); + } else { + proc.kill(); + } + } + }, + }; + disposables?.add(disposable); + const internalDisposables: IDisposable[] = []; + + // eslint-disable-next-line @typescript-eslint/ban-types + const on = (ee: Readable | null, name: string, fn: Function) => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + ee?.on(name, fn as any); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + internalDisposables.push({ dispose: () => ee?.removeListener(name, fn as any) as any }); + }; + + // Tokens not supported yet as they come from vscode module which is not available. + // if (options.token) { + // internalDisposables.push(options.token.onCancellationRequested(disposable.dispose)); + // } + + const stdoutBuffers: Buffer[] = []; + on(proc.stdout, 'data', (data: Buffer) => { + stdoutBuffers.push(data); + }); + const stderrBuffers: Buffer[] = []; + on(proc.stderr, 'data', (data: Buffer) => { + if (options.mergeStdOutErr) { + stdoutBuffers.push(data); + stderrBuffers.push(data); + } else { + stderrBuffers.push(data); + } + }); + + proc.once('close', () => { + if (deferred.completed) { + return; + } + const stderr: string | undefined = + stderrBuffers.length === 0 ? undefined : decodeBuffer(stderrBuffers, encoding); + if ( + stderr && + stderr.length > 0 && + options.throwOnStdErr && + // ignore this specific error silently; see this issue for context: https://github.com/microsoft/vscode/issues/75932 + !(PS_ERROR_SCREEN_BOGUS.test(stderr) && stderr.replace(PS_ERROR_SCREEN_BOGUS, '').trim().length === 0) + ) { + deferred.reject(new StdErrError(stderr)); + } else { + let stdout = decodeBuffer(stdoutBuffers, encoding); + stdout = filterOutputUsingCondaRunMarkers(stdout); + deferred.resolve({ stdout, stderr }); + } + internalDisposables.forEach((d) => d.dispose()); + disposable.dispose(); + }); + proc.once('error', (ex) => { + deferred.reject(ex); + internalDisposables.forEach((d) => d.dispose()); + disposable.dispose(); + }); + + return deferred.promise; +} + +function filterOutputUsingCondaRunMarkers(stdout: string) { + // These markers are added if conda run is used or `interpreterInfo.py` is + // run, see `get_output_via_markers.py`. + const regex = />>>PYTHON-EXEC-OUTPUT([\s\S]*)<<= 2 ? match[1].trim() : undefined; + return filteredOut !== undefined ? filteredOut : stdout; +} + +function killPid(pid: number): void { + try { + if (process.platform === 'win32') { + // Windows doesn't support SIGTERM, so execute taskkill to kill the process + execSync(`taskkill /pid ${pid} /T /F`); // NOSONAR + } else { + process.kill(pid); + } + } catch { + traceWarn('Unable to kill process with pid', pid); + } +} diff --git a/src/client/common/serviceRegistry.ts b/src/client/common/serviceRegistry.ts index 5b527499460a..abd2b220e400 100644 --- a/src/client/common/serviceRegistry.ts +++ b/src/client/common/serviceRegistry.ts @@ -5,7 +5,6 @@ import { IBrowserService, IConfigurationService, ICurrentProcess, - IEditorUtils, IExperimentService, IExtensions, IInstaller, @@ -29,7 +28,6 @@ import { CommandManager } from './application/commandManager'; import { ReloadVSCodeCommandHandler } from './application/commands/reloadCommand'; import { ReportIssueCommandHandler } from './application/commands/reportIssueCommand'; import { DebugService } from './application/debugService'; -import { DebugSessionTelemetry } from './application/debugSessionTelemetry'; import { DocumentManager } from './application/documentManager'; import { Extensions } from './application/extensions'; import { LanguageService } from './application/languageService'; @@ -51,13 +49,11 @@ import { import { WorkspaceService } from './application/workspace'; import { ConfigurationService } from './configuration/service'; import { PipEnvExecutionPath } from './configuration/executionSettings/pipEnvExecution'; -import { EditorUtils } from './editor'; import { ExperimentService } from './experiments/service'; import { ProductInstaller } from './installer/productInstaller'; import { InterpreterPathService } from './interpreterPathService'; import { BrowserService } from './net/browser'; import { PersistentStateFactory } from './persistentState'; -import { IS_WINDOWS } from './platform/constants'; import { PathUtils } from './platform/pathUtils'; import { CurrentProcess } from './process/currentProcess'; import { ProcessLogger } from './process/logger'; @@ -91,9 +87,11 @@ import { Random } from './utils/random'; import { ContextKeyManager } from './application/contextKeyManager'; import { CreatePythonFileCommandHandler } from './application/commands/createPythonFile'; import { RequireJupyterPrompt } from '../jupyter/requireJupyterPrompt'; +import { isWindows } from './utils/platform'; +import { PixiActivationCommandProvider } from './terminal/environmentActivationProviders/pixiActivationProvider'; export function registerTypes(serviceManager: IServiceManager): void { - serviceManager.addSingletonInstance(IsWindows, IS_WINDOWS); + serviceManager.addSingletonInstance(IsWindows, isWindows()); serviceManager.addSingleton(IActiveResourceService, ActiveResourceService); serviceManager.addSingleton(IInterpreterPathService, InterpreterPathService); @@ -130,7 +128,6 @@ export function registerTypes(serviceManager: IServiceManager): void { serviceManager.addSingleton(IApplicationEnvironment, ApplicationEnvironment); serviceManager.addSingleton(ILanguageService, LanguageService); serviceManager.addSingleton(IBrowserService, BrowserService); - serviceManager.addSingleton(IEditorUtils, EditorUtils); serviceManager.addSingleton(ITerminalActivator, TerminalActivator); serviceManager.addSingleton( ITerminalActivationHandler, @@ -164,6 +161,11 @@ export function registerTypes(serviceManager: IServiceManager): void { CondaActivationCommandProvider, TerminalActivationProviders.conda, ); + serviceManager.addSingleton( + ITerminalActivationCommandProvider, + PixiActivationCommandProvider, + TerminalActivationProviders.pixi, + ); serviceManager.addSingleton( ITerminalActivationCommandProvider, PipEnvActivationCommandProvider, @@ -186,8 +188,4 @@ export function registerTypes(serviceManager: IServiceManager): void { IExtensionSingleActivationService, ReportIssueCommandHandler, ); - serviceManager.addSingleton( - IExtensionSingleActivationService, - DebugSessionTelemetry, - ); } diff --git a/src/client/common/terminal/activator/index.ts b/src/client/common/terminal/activator/index.ts index 1c2cf4041585..cde04bdbf10d 100644 --- a/src/client/common/terminal/activator/index.ts +++ b/src/client/common/terminal/activator/index.ts @@ -9,6 +9,9 @@ import { IConfigurationService, IExperimentService } from '../../types'; import { ITerminalActivationHandler, ITerminalActivator, ITerminalHelper, TerminalActivationOptions } from '../types'; import { BaseTerminalActivator } from './base'; import { inTerminalEnvVarExperiment } from '../../experiments/helpers'; +import { shouldEnvExtHandleActivation } from '../../../envExt/api.internal'; +import { EventName } from '../../../telemetry/constants'; +import { sendTelemetryEvent } from '../../../telemetry'; @injectable() export class TerminalActivator implements ITerminalActivator { @@ -41,7 +44,10 @@ export class TerminalActivator implements ITerminalActivator { const settings = this.configurationService.getSettings(options?.resource); const activateEnvironment = settings.terminal.activateEnvironment && !inTerminalEnvVarExperiment(this.experimentService); - if (!activateEnvironment || options?.hideFromUser) { + if (!activateEnvironment || options?.hideFromUser || shouldEnvExtHandleActivation()) { + if (shouldEnvExtHandleActivation()) { + sendTelemetryEvent(EventName.PYTHON_INTERPRETER_ACTIVATION_FOR_TERMINAL); + } return false; } diff --git a/src/client/common/terminal/environmentActivationProviders/condaActivationProvider.ts b/src/client/common/terminal/environmentActivationProviders/condaActivationProvider.ts index d209550e04a4..42bb8f38fc9e 100644 --- a/src/client/common/terminal/environmentActivationProviders/condaActivationProvider.ts +++ b/src/client/common/terminal/environmentActivationProviders/condaActivationProvider.ts @@ -8,6 +8,7 @@ import '../../extensions'; import { inject, injectable } from 'inversify'; import * as path from 'path'; import { Uri } from 'vscode'; +import { traceInfo, traceVerbose, traceWarn } from '../../../logging'; import { IComponentAdapter, ICondaService } from '../../../interpreter/contracts'; import { IPlatformService } from '../../platform/types'; @@ -53,16 +54,21 @@ export class CondaActivationCommandProvider implements ITerminalActivationComman pythonPath: string, targetShell: TerminalShellType, ): Promise { + traceVerbose(`Getting conda activation commands for interpreter ${pythonPath} with shell ${targetShell}`); const envInfo = await this.pyenvs.getCondaEnvironment(pythonPath); if (!envInfo) { + traceWarn(`No conda environment found for interpreter ${pythonPath}`); return undefined; } + traceVerbose(`Found conda environment: ${JSON.stringify(envInfo)}`); const condaEnv = envInfo.name.length > 0 ? envInfo.name : envInfo.path; // New version. const interpreterPath = await this.condaService.getInterpreterPathForEnvironment(envInfo); + traceInfo(`Using interpreter path: ${interpreterPath}`); const activatePath = await this.condaService.getActivationScriptFromInterpreter(interpreterPath, envInfo.name); + traceVerbose(`Got activation script: ${activatePath?.path}} with type: ${activatePath?.type}`); // eslint-disable-next-line camelcase if (activatePath?.path) { if ( @@ -70,11 +76,14 @@ export class CondaActivationCommandProvider implements ITerminalActivationComman targetShell !== TerminalShellType.bash && targetShell !== TerminalShellType.gitbash ) { - return [activatePath.path, `conda activate ${condaEnv.toCommandArgumentForPythonExt()}`]; + const commands = [activatePath.path, `conda activate ${condaEnv.toCommandArgumentForPythonExt()}`]; + traceInfo(`Using Windows-specific commands: ${commands.join(', ')}`); + return commands; } const condaInfo = await this.condaService.getCondaInfo(); + traceVerbose(`Conda shell level: ${condaInfo?.conda_shlvl}`); if ( activatePath.type !== 'global' || // eslint-disable-next-line camelcase @@ -84,27 +93,36 @@ export class CondaActivationCommandProvider implements ITerminalActivationComman // activatePath is not the global activate path, or we don't have a shlvl, or it's -1(conda never sourcedοΌ‰. // and we need to source the activate path. if (activatePath.path === 'activate') { - return [ + const commands = [ `source ${activatePath.path}`, `conda activate ${condaEnv.toCommandArgumentForPythonExt()}`, ]; + traceInfo(`Using source activate commands: ${commands.join(', ')}`); + return commands; } - return [`source ${activatePath.path} ${condaEnv.toCommandArgumentForPythonExt()}`]; + const command = [`source ${activatePath.path} ${condaEnv.toCommandArgumentForPythonExt()}`]; + traceInfo(`Using single source command: ${command}`); + return command; } - return [`conda activate ${condaEnv.toCommandArgumentForPythonExt()}`]; + const command = [`conda activate ${condaEnv.toCommandArgumentForPythonExt()}`]; + traceInfo(`Using direct conda activate command: ${command}`); + return command; } switch (targetShell) { case TerminalShellType.powershell: case TerminalShellType.powershellCore: + traceVerbose('Using PowerShell-specific activation'); return _getPowershellCommands(condaEnv); // TODO: Do we really special-case fish on Windows? case TerminalShellType.fish: + traceVerbose('Using Fish shell-specific activation'); return getFishCommands(condaEnv, await this.condaService.getCondaFile()); default: if (this.platform.isWindows) { + traceVerbose('Using Windows shell-specific activation fallback option.'); return this.getWindowsCommands(condaEnv); } return getUnixCommands(condaEnv, await this.condaService.getCondaFile()); diff --git a/src/client/common/terminal/environmentActivationProviders/pixiActivationProvider.ts b/src/client/common/terminal/environmentActivationProviders/pixiActivationProvider.ts new file mode 100644 index 000000000000..1deaa56dd8ae --- /dev/null +++ b/src/client/common/terminal/environmentActivationProviders/pixiActivationProvider.ts @@ -0,0 +1,77 @@ +/* eslint-disable class-methods-use-this */ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { inject, injectable } from 'inversify'; +import { Uri } from 'vscode'; +import { IInterpreterService } from '../../../interpreter/contracts'; +import { ITerminalActivationCommandProvider, TerminalShellType } from '../types'; +import { getPixiActivationCommands } from '../../../pythonEnvironments/common/environmentManagers/pixi'; + +@injectable() +export class PixiActivationCommandProvider implements ITerminalActivationCommandProvider { + constructor(@inject(IInterpreterService) private readonly interpreterService: IInterpreterService) {} + + // eslint-disable-next-line class-methods-use-this + public isShellSupported(targetShell: TerminalShellType): boolean { + return shellTypeToPixiShell(targetShell) !== undefined; + } + + public async getActivationCommands( + resource: Uri | undefined, + targetShell: TerminalShellType, + ): Promise { + const interpreter = await this.interpreterService.getActiveInterpreter(resource); + if (!interpreter) { + return undefined; + } + + return this.getActivationCommandsForInterpreter(interpreter.path, targetShell); + } + + public getActivationCommandsForInterpreter( + pythonPath: string, + targetShell: TerminalShellType, + ): Promise { + return getPixiActivationCommands(pythonPath, targetShell); + } +} + +/** + * Returns the name of a terminal shell type within Pixi. + */ +function shellTypeToPixiShell(targetShell: TerminalShellType): string | undefined { + switch (targetShell) { + case TerminalShellType.powershell: + case TerminalShellType.powershellCore: + return 'powershell'; + case TerminalShellType.commandPrompt: + return 'cmd'; + + case TerminalShellType.zsh: + return 'zsh'; + + case TerminalShellType.fish: + return 'fish'; + + case TerminalShellType.nushell: + return 'nushell'; + + case TerminalShellType.xonsh: + return 'xonsh'; + + case TerminalShellType.cshell: + // Explicitly unsupported + return undefined; + + case TerminalShellType.gitbash: + case TerminalShellType.bash: + case TerminalShellType.wsl: + case TerminalShellType.tcshell: + case TerminalShellType.other: + default: + return 'bash'; + } +} diff --git a/src/client/common/terminal/factory.ts b/src/client/common/terminal/factory.ts index 3cbe123b5629..39cc88c4b024 100644 --- a/src/client/common/terminal/factory.ts +++ b/src/client/common/terminal/factory.ts @@ -24,14 +24,14 @@ export class TerminalServiceFactory implements ITerminalServiceFactory { ) { this.terminalServices = new Map(); } - public getTerminalService(options: TerminalCreationOptions): ITerminalService { + public getTerminalService(options: TerminalCreationOptions & { newTerminalPerFile?: boolean }): ITerminalService { const resource = options?.resource; const title = options?.title; let terminalTitle = typeof title === 'string' && title.trim().length > 0 ? title.trim() : 'Python'; const interpreter = options?.interpreter; - const id = this.getTerminalId(terminalTitle, resource, interpreter); + const id = this.getTerminalId(terminalTitle, resource, interpreter, options.newTerminalPerFile); if (!this.terminalServices.has(id)) { - if (this.terminalServices.size >= 1 && resource) { + if (resource && options.newTerminalPerFile) { terminalTitle = `${terminalTitle}: ${path.basename(resource.fsPath).replace('.py', '')}`; } options.title = terminalTitle; @@ -51,13 +51,19 @@ export class TerminalServiceFactory implements ITerminalServiceFactory { title = typeof title === 'string' && title.trim().length > 0 ? title.trim() : 'Python'; return new TerminalService(this.serviceContainer, { resource, title }); } - private getTerminalId(title: string, resource?: Uri, interpreter?: PythonEnvironment): string { + private getTerminalId( + title: string, + resource?: Uri, + interpreter?: PythonEnvironment, + newTerminalPerFile?: boolean, + ): string { if (!resource && !interpreter) { return title; } const workspaceFolder = this.serviceContainer .get(IWorkspaceService) .getWorkspaceFolder(resource || undefined); - return `${title}:${workspaceFolder?.uri.fsPath || ''}:${interpreter?.path}:${resource?.fsPath || ''}`; + const fileId = resource && newTerminalPerFile ? resource.fsPath : ''; + return `${title}:${workspaceFolder?.uri.fsPath || ''}:${interpreter?.path}:${fileId}`; } } diff --git a/src/client/common/terminal/helper.ts b/src/client/common/terminal/helper.ts index f1a89df10786..d2b3bb7879af 100644 --- a/src/client/common/terminal/helper.ts +++ b/src/client/common/terminal/helper.ts @@ -22,6 +22,7 @@ import { TerminalActivationProviders, TerminalShellType, } from './types'; +import { isPixiEnvironment } from '../../pythonEnvironments/common/environmentManagers/pixi'; @injectable() export class TerminalHelper implements ITerminalHelper { @@ -50,6 +51,9 @@ export class TerminalHelper implements ITerminalHelper { @inject(ITerminalActivationCommandProvider) @named(TerminalActivationProviders.pipenv) private readonly pipenv: ITerminalActivationCommandProvider, + @inject(ITerminalActivationCommandProvider) + @named(TerminalActivationProviders.pixi) + private readonly pixi: ITerminalActivationCommandProvider, @multiInject(IShellDetector) shellDetectors: IShellDetector[], ) { this.shellDetector = new ShellDetector(this.platform, shellDetectors); @@ -75,7 +79,14 @@ export class TerminalHelper implements ITerminalHelper { resource?: Uri, interpreter?: PythonEnvironment, ): Promise { - const providers = [this.pipenv, this.pyenv, this.bashCShellFish, this.commandPromptAndPowerShell, this.nushell]; + const providers = [ + this.pixi, + this.pipenv, + this.pyenv, + this.bashCShellFish, + this.commandPromptAndPowerShell, + this.nushell, + ]; const promise = this.getActivationCommands(resource || undefined, interpreter, terminalShellType, providers); this.sendTelemetry( terminalShellType, @@ -93,7 +104,7 @@ export class TerminalHelper implements ITerminalHelper { if (this.platform.osType === OSType.Unknown) { return; } - const providers = [this.bashCShellFish, this.commandPromptAndPowerShell, this.nushell]; + const providers = [this.pixi, this.bashCShellFish, this.commandPromptAndPowerShell, this.nushell]; const promise = this.getActivationCommands(resource, interpreter, shell, providers); this.sendTelemetry( shell, @@ -133,6 +144,19 @@ export class TerminalHelper implements ITerminalHelper { ): Promise { const settings = this.configurationService.getSettings(resource); + const isPixiEnv = interpreter + ? interpreter.envType === EnvironmentType.Pixi + : await isPixiEnvironment(settings.pythonPath); + if (isPixiEnv) { + const activationCommands = interpreter + ? await this.pixi.getActivationCommandsForInterpreter(interpreter.path, terminalShellType) + : await this.pixi.getActivationCommands(resource, terminalShellType); + + if (Array.isArray(activationCommands)) { + return activationCommands; + } + } + const condaService = this.serviceContainer.get(IComponentAdapter); // If we have a conda environment, then use that. const isCondaEnvironment = interpreter diff --git a/src/client/common/terminal/service.ts b/src/client/common/terminal/service.ts index 7128d27802f8..0dffd5615ae1 100644 --- a/src/client/common/terminal/service.ts +++ b/src/client/common/terminal/service.ts @@ -2,14 +2,15 @@ // Licensed under the MIT License. import { inject, injectable } from 'inversify'; -import { CancellationToken, Disposable, Event, EventEmitter, Terminal } from 'vscode'; +import { CancellationToken, Disposable, Event, EventEmitter, Terminal, TerminalShellExecution } from 'vscode'; import '../../common/extensions'; import { IInterpreterService } from '../../interpreter/contracts'; import { IServiceContainer } from '../../ioc/types'; import { captureTelemetry } from '../../telemetry'; import { EventName } from '../../telemetry/constants'; import { ITerminalAutoActivation } from '../../terminals/types'; -import { ITerminalManager } from '../application/types'; +import { IApplicationShell, ITerminalManager } from '../application/types'; +import { _SCRIPTS_DIR } from '../process/internal/scripts/constants'; import { IConfigurationService, IDisposableRegistry } from '../types'; import { ITerminalActivator, @@ -18,6 +19,10 @@ import { TerminalCreationOptions, TerminalShellType, } from './types'; +import { traceVerbose } from '../../logging'; +import { sleep } from '../utils/async'; +import { useEnvExtension } from '../../envExt/api.internal'; +import { ensureTerminalLegacy } from '../../envExt/api.legacy'; @injectable() export class TerminalService implements ITerminalService, Disposable { @@ -28,9 +33,17 @@ export class TerminalService implements ITerminalService, Disposable { private terminalHelper: ITerminalHelper; private terminalActivator: ITerminalActivator; private terminalAutoActivator: ITerminalAutoActivation; + private applicationShell: IApplicationShell; + private readonly executeCommandListeners: Set = new Set(); + private _terminalFirstLaunched: boolean = true; + private pythonReplCommandQueue: string[] = []; + private isReplReady: boolean = false; + private replPromptListener?: Disposable; + private replShellTypeListener?: Disposable; public get onDidCloseTerminal(): Event { return this.terminalClosed.event.bind(this.terminalClosed); } + constructor( @inject(IServiceContainer) private serviceContainer: IServiceContainer, private readonly options?: TerminalCreationOptions, @@ -40,12 +53,18 @@ export class TerminalService implements ITerminalService, Disposable { this.terminalHelper = this.serviceContainer.get(ITerminalHelper); this.terminalManager = this.serviceContainer.get(ITerminalManager); this.terminalAutoActivator = this.serviceContainer.get(ITerminalAutoActivation); + this.applicationShell = this.serviceContainer.get(IApplicationShell); this.terminalManager.onDidCloseTerminal(this.terminalCloseHandler, this, disposableRegistry); this.terminalActivator = this.serviceContainer.get(ITerminalActivator); } public dispose() { - if (this.terminal) { - this.terminal.dispose(); + this.terminal?.dispose(); + this.disposeReplListener(); + + if (this.executeCommandListeners && this.executeCommandListeners.size > 0) { + this.executeCommandListeners.forEach((d) => { + d?.dispose(); + }); } } public async sendCommand(command: string, args: string[], _?: CancellationToken): Promise { @@ -54,8 +73,10 @@ export class TerminalService implements ITerminalService, Disposable { if (!this.options?.hideFromUser) { this.terminal!.show(true); } - this.terminal!.sendText(text, true); + + await this.executeCommand(text, false); } + /** @deprecated */ public async sendText(text: string): Promise { await this.ensureTerminal(); if (!this.options?.hideFromUser) { @@ -63,44 +84,175 @@ export class TerminalService implements ITerminalService, Disposable { } this.terminal!.sendText(text); } + public async executeCommand( + commandLine: string, + isPythonShell: boolean, + ): Promise { + if (isPythonShell) { + if (this.isReplReady) { + this.terminal?.sendText(commandLine); + traceVerbose(`Python REPL sendText: ${commandLine}`); + } else { + // Queue command to run once REPL is ready. + this.pythonReplCommandQueue.push(commandLine); + traceVerbose(`Python REPL queued command: ${commandLine}`); + this.startReplListener(); + } + return undefined; + } + + // Non-REPL code execution + return this.executeCommandInternal(commandLine); + } + + private startReplListener(): void { + if (this.replPromptListener || this.replShellTypeListener) { + return; + } + + this.replShellTypeListener = this.terminalManager.onDidChangeTerminalState((terminal) => { + if (this.terminal && terminal === this.terminal) { + if (terminal.state.shell == 'python') { + traceVerbose('Python REPL ready from terminal shell api'); + this.onReplReady(); + } + } + }); + + let terminalData = ''; + this.replPromptListener = this.applicationShell.onDidWriteTerminalData((e) => { + if (this.terminal && e.terminal === this.terminal) { + terminalData += e.data; + if (/>>>\s*$/.test(terminalData)) { + traceVerbose('Python REPL ready, from >>> prompt detection'); + this.onReplReady(); + } + } + }); + } + + private onReplReady(): void { + if (this.isReplReady) { + return; + } + this.isReplReady = true; + this.flushReplQueue(); + this.disposeReplListener(); + } + + private disposeReplListener(): void { + if (this.replPromptListener) { + this.replPromptListener.dispose(); + this.replPromptListener = undefined; + } + if (this.replShellTypeListener) { + this.replShellTypeListener.dispose(); + this.replShellTypeListener = undefined; + } + } + + private flushReplQueue(): void { + while (this.pythonReplCommandQueue.length > 0) { + const commandLine = this.pythonReplCommandQueue.shift(); + if (commandLine) { + traceVerbose(`Executing queued REPL command: ${commandLine}`); + this.terminal?.sendText(commandLine); + } + } + } + + private async executeCommandInternal(commandLine: string): Promise { + const terminal = this.terminal; + if (!terminal) { + traceVerbose('Terminal not available, cannot execute command'); + return undefined; + } + + if (!this.options?.hideFromUser) { + terminal.show(true); + } + + // If terminal was just launched, wait some time for shell integration to onDidChangeShellIntegration. + if (!terminal.shellIntegration && this._terminalFirstLaunched) { + this._terminalFirstLaunched = false; + const promise = new Promise((resolve) => { + const disposable = this.terminalManager.onDidChangeTerminalShellIntegration(() => { + // eslint-disable-next-line @typescript-eslint/no-use-before-define + clearTimeout(timer); + disposable.dispose(); + resolve(true); + }); + const TIMEOUT_DURATION = 500; + const timer = setTimeout(() => { + disposable.dispose(); + resolve(true); + }, TIMEOUT_DURATION); + }); + await promise; + } + + if (terminal.shellIntegration) { + const execution = terminal.shellIntegration.executeCommand(commandLine); + traceVerbose(`Shell Integration is enabled, executeCommand: ${commandLine}`); + return execution; + } else { + terminal.sendText(commandLine); + traceVerbose(`Shell Integration is disabled, sendText: ${commandLine}`); + } + + return undefined; + } + public async show(preserveFocus: boolean = true): Promise { await this.ensureTerminal(preserveFocus); if (!this.options?.hideFromUser) { this.terminal!.show(preserveFocus); } } - private async ensureTerminal(preserveFocus: boolean = true): Promise { + // TODO: Debt switch to Promise ---> breaks 20 tests + public async ensureTerminal(preserveFocus: boolean = true): Promise { if (this.terminal) { return; } - this.terminalShellType = this.terminalHelper.identifyTerminalShell(this.terminal); - this.terminal = this.terminalManager.createTerminal({ - name: this.options?.title || 'Python', - env: this.options?.env, - hideFromUser: this.options?.hideFromUser, - }); - this.terminalAutoActivator.disableAutoActivation(this.terminal); - // Sometimes the terminal takes some time to start up before it can start accepting input. - await new Promise((resolve) => setTimeout(resolve, 100)); + if (useEnvExtension()) { + this.terminal = await ensureTerminalLegacy(this.options?.resource, { + name: this.options?.title || 'Python', + hideFromUser: this.options?.hideFromUser, + }); + return; + } else { + this.terminalShellType = this.terminalHelper.identifyTerminalShell(this.terminal); + this.terminal = this.terminalManager.createTerminal({ + name: this.options?.title || 'Python', + hideFromUser: this.options?.hideFromUser, + }); + this.terminalAutoActivator.disableAutoActivation(this.terminal); - await this.terminalActivator.activateEnvironmentInTerminal(this.terminal!, { - resource: this.options?.resource, - preserveFocus, - interpreter: this.options?.interpreter, - hideFromUser: this.options?.hideFromUser, - }); + await sleep(100); + + await this.terminalActivator.activateEnvironmentInTerminal(this.terminal, { + resource: this.options?.resource, + preserveFocus, + interpreter: this.options?.interpreter, + hideFromUser: this.options?.hideFromUser, + }); + } if (!this.options?.hideFromUser) { - this.terminal!.show(preserveFocus); + this.terminal.show(preserveFocus); } this.sendTelemetry().ignoreErrors(); + return; } private terminalCloseHandler(terminal: Terminal) { if (terminal === this.terminal) { this.terminalClosed.fire(); this.terminal = undefined; + this.isReplReady = false; + this.disposeReplListener(); + this.pythonReplCommandQueue = []; } } diff --git a/src/client/common/terminal/shellDetector.ts b/src/client/common/terminal/shellDetector.ts index 98cda5953fe8..bf183f20a279 100644 --- a/src/client/common/terminal/shellDetector.ts +++ b/src/client/common/terminal/shellDetector.ts @@ -33,10 +33,6 @@ export class ShellDetector { * 3. Try to identify the type of the shell based on the user environment (OS). * 4. If all else fail, use defaults hardcoded (cmd for windows, bash for linux & mac). * More information here: https://github.com/microsoft/vscode/issues/74233#issuecomment-497527337 - * - * @param {Terminal} [terminal] - * @returns {TerminalShellType} - * @memberof TerminalHelper */ public identifyTerminalShell(terminal?: Terminal): TerminalShellType { let shell: TerminalShellType | undefined; @@ -53,9 +49,6 @@ export class ShellDetector { for (const detector of shellDetectors) { shell = detector.identify(telemetryProperties, terminal); - traceVerbose( - `${detector}. Shell identified as ${shell} ${terminal ? `(Terminal name is ${terminal.name})` : ''}`, - ); if (shell && shell !== TerminalShellType.other) { telemetryProperties.failed = false; break; @@ -66,7 +59,7 @@ export class ShellDetector { // This impacts executing code in terminals and activation of environments in terminal. // So, the better this works, the better it is for the user. sendTelemetryEvent(EventName.TERMINAL_SHELL_IDENTIFICATION, undefined, telemetryProperties); - traceVerbose(`Shell identified as '${shell}'`); + traceVerbose(`Shell identified as ${shell} ${terminal ? `(Terminal name is ${terminal.name})` : ''}`); // If we could not identify the shell, use the defaults. if (shell === undefined || shell === TerminalShellType.other) { diff --git a/src/client/common/terminal/shellDetectors/baseShellDetector.ts b/src/client/common/terminal/shellDetectors/baseShellDetector.ts index d3c3967d3e3a..4262bdf80364 100644 --- a/src/client/common/terminal/shellDetectors/baseShellDetector.ts +++ b/src/client/common/terminal/shellDetectors/baseShellDetector.ts @@ -5,7 +5,6 @@ import { injectable, unmanaged } from 'inversify'; import { Terminal } from 'vscode'; -import { traceVerbose } from '../../../logging'; import { IShellDetector, ShellIdentificationTelemetry, TerminalShellType } from '../types'; /* @@ -63,7 +62,7 @@ export abstract class BaseShellDetector implements IShellDetector { export function identifyShellFromShellPath(shellPath: string): TerminalShellType { // Remove .exe extension so shells can be more consistently detected // on Windows (including Cygwin). - const basePath = shellPath.replace(/\.exe$/, ''); + const basePath = shellPath.replace(/\.exe$/i, ''); const shell = Array.from(detectableShells.keys()).reduce((matchedShell, shellToDetect) => { if (matchedShell === TerminalShellType.other) { @@ -75,7 +74,5 @@ export function identifyShellFromShellPath(shellPath: string): TerminalShellType return matchedShell; }, TerminalShellType.other); - traceVerbose(`Shell path '${shellPath}', base path '${basePath}'`); - traceVerbose(`Shell path identified as shell '${shell}'`); return shell; } diff --git a/src/client/common/terminal/shellDetectors/settingsShellDetector.ts b/src/client/common/terminal/shellDetectors/settingsShellDetector.ts index 7ffc168db28b..6288675ec3f8 100644 --- a/src/client/common/terminal/shellDetectors/settingsShellDetector.ts +++ b/src/client/common/terminal/shellDetectors/settingsShellDetector.ts @@ -5,7 +5,6 @@ import { inject, injectable } from 'inversify'; import { Terminal } from 'vscode'; -import { traceVerbose } from '../../../logging'; import { IWorkspaceService } from '../../application/types'; import { IPlatformService } from '../../platform/types'; import { OSType } from '../../utils/platform'; @@ -14,10 +13,6 @@ import { BaseShellDetector } from './baseShellDetector'; /** * Identifies the shell based on the user settings. - * - * @export - * @class SettingsShellDetector - * @extends {BaseShellDetector} */ @injectable() export class SettingsShellDetector extends BaseShellDetector { @@ -62,7 +57,6 @@ export class SettingsShellDetector extends BaseShellDetector { } else { telemetryProperties.shellIdentificationSource = 'settings'; } - traceVerbose(`Shell path from user settings '${shellPath}'`); return shell; } } diff --git a/src/client/common/terminal/shellDetectors/terminalNameShellDetector.ts b/src/client/common/terminal/shellDetectors/terminalNameShellDetector.ts index 80911e85c1b5..0f14adbe9d36 100644 --- a/src/client/common/terminal/shellDetectors/terminalNameShellDetector.ts +++ b/src/client/common/terminal/shellDetectors/terminalNameShellDetector.ts @@ -11,10 +11,6 @@ import { BaseShellDetector } from './baseShellDetector'; /** * Identifies the shell, based on the display name of the terminal. - * - * @export - * @class TerminalNameShellDetector - * @extends {BaseShellDetector} */ @injectable() export class TerminalNameShellDetector extends BaseShellDetector { diff --git a/src/client/common/terminal/shellDetectors/userEnvironmentShellDetector.ts b/src/client/common/terminal/shellDetectors/userEnvironmentShellDetector.ts index 7d8ed34ebf62..da84eef4d46f 100644 --- a/src/client/common/terminal/shellDetectors/userEnvironmentShellDetector.ts +++ b/src/client/common/terminal/shellDetectors/userEnvironmentShellDetector.ts @@ -5,7 +5,6 @@ import { inject, injectable } from 'inversify'; import { Terminal } from 'vscode'; -import { traceVerbose } from '../../../logging'; import { IPlatformService } from '../../platform/types'; import { ICurrentProcess } from '../../types'; import { OSType } from '../../utils/platform'; @@ -14,10 +13,6 @@ import { BaseShellDetector } from './baseShellDetector'; /** * Identifies the shell based on the users environment (env variables). - * - * @export - * @class UserEnvironmentShellDetector - * @extends {BaseShellDetector} */ @injectable() export class UserEnvironmentShellDetector extends BaseShellDetector { @@ -41,7 +36,6 @@ export class UserEnvironmentShellDetector extends BaseShellDetector { if (shell !== TerminalShellType.other) { telemetryProperties.shellIdentificationSource = 'environment'; } - traceVerbose(`Shell path from user env '${shellPath}'`); return shell; } } diff --git a/src/client/common/terminal/shellDetectors/vscEnvironmentShellDetector.ts b/src/client/common/terminal/shellDetectors/vscEnvironmentShellDetector.ts index a4592374b36f..9ca1b8c4ec22 100644 --- a/src/client/common/terminal/shellDetectors/vscEnvironmentShellDetector.ts +++ b/src/client/common/terminal/shellDetectors/vscEnvironmentShellDetector.ts @@ -12,10 +12,6 @@ import { BaseShellDetector } from './baseShellDetector'; /** * Identifies the shell, based on the VSC Environment API. - * - * @export - * @class VSCEnvironmentShellDetector - * @extends {BaseShellDetector} */ export class VSCEnvironmentShellDetector extends BaseShellDetector { constructor(@inject(IApplicationEnvironment) private readonly appEnv: IApplicationEnvironment) { diff --git a/src/client/common/terminal/syncTerminalService.ts b/src/client/common/terminal/syncTerminalService.ts index 4e95ddab01b5..0b46a86ee51e 100644 --- a/src/client/common/terminal/syncTerminalService.ts +++ b/src/client/common/terminal/syncTerminalService.ts @@ -4,7 +4,7 @@ 'use strict'; import { inject } from 'inversify'; -import { CancellationToken, Disposable, Event } from 'vscode'; +import { CancellationToken, Disposable, Event, TerminalShellExecution } from 'vscode'; import { IInterpreterService } from '../../interpreter/contracts'; import { traceVerbose } from '../../logging'; import { PythonEnvironment } from '../../pythonEnvironments/info'; @@ -92,11 +92,6 @@ class ExecutionState implements Disposable { * - Send text to a terminal that executes our python file, passing in the original text as args * - The pthon file will execute the commands as a subprocess * - At the end of the execution a file is created to singal completion. - * - * @export - * @class SynchronousTerminalService - * @implements {ITerminalService} - * @implements {Disposable} */ export class SynchronousTerminalService implements ITerminalService, Disposable { private readonly disposables: Disposable[] = []; @@ -146,9 +141,13 @@ export class SynchronousTerminalService implements ITerminalService, Disposable lockFile.dispose(); } } + /** @deprecated */ public sendText(text: string): Promise { return this.terminalService.sendText(text); } + public executeCommand(commandLine: string, isPythonShell: boolean): Promise { + return this.terminalService.executeCommand(commandLine, isPythonShell); + } public show(preserveFocus?: boolean | undefined): Promise { return this.terminalService.show(preserveFocus); } diff --git a/src/client/common/terminal/types.ts b/src/client/common/terminal/types.ts index 880bf0dd72fb..3e54458a57fd 100644 --- a/src/client/common/terminal/types.ts +++ b/src/client/common/terminal/types.ts @@ -3,7 +3,7 @@ 'use strict'; -import { CancellationToken, Event, Terminal, Uri } from 'vscode'; +import { CancellationToken, Event, Terminal, Uri, TerminalShellExecution } from 'vscode'; import { PythonEnvironment } from '../../pythonEnvironments/info'; import { IEventNamePropertyMapping } from '../../telemetry/index'; import { IDisposable, Resource } from '../types'; @@ -15,6 +15,7 @@ export enum TerminalActivationProviders { pyenv = 'pyenv', conda = 'conda', pipenv = 'pipenv', + pixi = 'pixi', } export enum TerminalShellType { powershell = 'powershell', @@ -51,7 +52,9 @@ export interface ITerminalService extends IDisposable { cancel?: CancellationToken, swallowExceptions?: boolean, ): Promise; + /** @deprecated */ sendText(text: string): Promise; + executeCommand(commandLine: string, isPythonShell: boolean): Promise; show(preserveFocus?: boolean): Promise; } @@ -92,12 +95,8 @@ export interface ITerminalServiceFactory { /** * Gets a terminal service. * If one exists with the same information, that is returned else a new one is created. - * - * @param {TerminalCreationOptions} - * @returns {ITerminalService} - * @memberof ITerminalServiceFactory */ - getTerminalService(options: TerminalCreationOptions): ITerminalService; + getTerminalService(options: TerminalCreationOptions & { newTerminalPerFile?: boolean }): ITerminalService; createTerminalService(resource?: Uri, title?: string): ITerminalService; } @@ -124,11 +123,7 @@ export type TerminalActivationOptions = { resource?: Resource; preserveFocus?: boolean; interpreter?: PythonEnvironment; - /** - * When sending commands to the terminal, do not display the terminal. - * - * @type {boolean} - */ + // When sending commands to the terminal, do not display the terminal. hideFromUser?: boolean; }; export interface ITerminalActivator { @@ -162,16 +157,10 @@ export const IShellDetector = Symbol('IShellDetector'); /** * Used to identify a shell. * Each implemenetion will provide a unique way of identifying the shell. - * - * @export - * @interface IShellDetector */ export interface IShellDetector { /** * Classes with higher priorities will be used first when identifying the shell. - * - * @type {number} - * @memberof IShellDetector */ readonly priority: number; identify(telemetryProperties: ShellIdentificationTelemetry, terminal?: Terminal): TerminalShellType | undefined; diff --git a/src/client/common/types.ts b/src/client/common/types.ts index 3359854f89b7..c30ad704b6c1 100644 --- a/src/client/common/types.ts +++ b/src/client/common/types.ts @@ -8,7 +8,6 @@ import { CancellationToken, ConfigurationChangeEvent, ConfigurationTarget, - DiagnosticSeverity, Disposable, DocumentSymbolProvider, Event, @@ -17,8 +16,6 @@ import { Memento, LogOutputChannel, Uri, - WorkspaceEdit, - OutputChannel, } from 'vscode'; import { LanguageServerType } from '../activation/types'; import type { InstallOptions, InterpreterUri, ModuleInstallFlags } from './installer/types'; @@ -26,14 +23,11 @@ import { EnvironmentVariables } from './variables/types'; import { ITestingSettings } from '../testing/configuration/types'; export interface IDisposable { - // eslint-disable-next-line @typescript-eslint/no-explicit-any dispose(): void | undefined | Promise; } export const ILogOutputChannel = Symbol('ILogOutputChannel'); export interface ILogOutputChannel extends LogOutputChannel {} -export const ITestOutputChannel = Symbol('ITestOutputChannel'); -export interface ITestOutputChannel extends OutputChannel {} export const IDocumentSymbolProvider = Symbol('IDocumentSymbolProvider'); export interface IDocumentSymbolProvider extends DocumentSymbolProvider {} export const IsWindows = Symbol('IS_WINDOWS'); @@ -86,35 +80,14 @@ export enum ProductInstallStatus { } export enum ProductType { - Linter = 'Linter', - Formatter = 'Formatter', TestFramework = 'TestFramework', - RefactoringLibrary = 'RefactoringLibrary', DataScience = 'DataScience', Python = 'Python', } export enum Product { pytest = 1, - pylint = 3, - flake8 = 4, - pycodestyle = 5, - pylama = 6, - prospector = 7, - pydocstyle = 8, - yapf = 9, - autopep8 = 10, - mypy = 11, unittest = 12, - isort = 15, - black = 16, - bandit = 17, - jupyter = 18, - ipykernel = 19, - notebook = 20, - kernelspec = 21, - nbconvert = 22, - pandas = 23, tensorboard = 24, torchProfilerInstallName = 25, torchProfilerImportName = 26, @@ -190,107 +163,39 @@ export interface IPythonSettings { readonly condaPath: string; readonly pipenvPath: string; readonly poetryPath: string; + readonly pixiToolPath: string; readonly devOptions: string[]; - readonly linting: ILintingSettings; - readonly formatting: IFormattingSettings; readonly testing: ITestingSettings; readonly autoComplete: IAutoCompleteSettings; readonly terminal: ITerminalSettings; - readonly sortImports: ISortImportSettings; readonly envFile: string; readonly globalModuleInstallation: boolean; readonly experiments: IExperiments; readonly languageServer: LanguageServerType; readonly languageServerIsDefault: boolean; readonly defaultInterpreterPath: string; - readonly tensorBoard: ITensorBoardSettings | undefined; + readonly REPL: IREPLSettings; register(): void; } -export interface ITensorBoardSettings { - logDirectory: string | undefined; -} -export interface ISortImportSettings { - readonly path: string; - readonly args: string[]; -} - -export interface IPylintCategorySeverity { - readonly convention: DiagnosticSeverity; - readonly refactor: DiagnosticSeverity; - readonly warning: DiagnosticSeverity; - readonly error: DiagnosticSeverity; - readonly fatal: DiagnosticSeverity; -} -export interface IPycodestyleCategorySeverity { - readonly W: DiagnosticSeverity; - readonly E: DiagnosticSeverity; -} - -export interface Flake8CategorySeverity { - readonly F: DiagnosticSeverity; - readonly E: DiagnosticSeverity; - readonly W: DiagnosticSeverity; -} -export interface IMypyCategorySeverity { - readonly error: DiagnosticSeverity; - readonly note: DiagnosticSeverity; -} export interface IInterpreterSettings { infoVisibility: 'never' | 'onPythonRelated' | 'always'; } -export interface ILintingSettings { - readonly enabled: boolean; - readonly ignorePatterns: string[]; - readonly prospectorEnabled: boolean; - readonly prospectorArgs: string[]; - readonly pylintEnabled: boolean; - readonly pylintArgs: string[]; - readonly pycodestyleEnabled: boolean; - readonly pycodestyleArgs: string[]; - readonly pylamaEnabled: boolean; - readonly pylamaArgs: string[]; - readonly flake8Enabled: boolean; - readonly flake8Args: string[]; - readonly pydocstyleEnabled: boolean; - readonly pydocstyleArgs: string[]; - readonly lintOnSave: boolean; - readonly maxNumberOfProblems: number; - readonly pylintCategorySeverity: IPylintCategorySeverity; - readonly pycodestyleCategorySeverity: IPycodestyleCategorySeverity; - readonly flake8CategorySeverity: Flake8CategorySeverity; - readonly mypyCategorySeverity: IMypyCategorySeverity; - cwd?: string; - prospectorPath: string; - pylintPath: string; - pycodestylePath: string; - pylamaPath: string; - flake8Path: string; - pydocstylePath: string; - mypyEnabled: boolean; - mypyArgs: string[]; - mypyPath: string; - banditEnabled: boolean; - banditArgs: string[]; - banditPath: string; -} -export interface IFormattingSettings { - readonly provider: string; - autopep8Path: string; - readonly autopep8Args: string[]; - blackPath: string; - readonly blackArgs: string[]; - yapfPath: string; - readonly yapfArgs: string[]; -} - export interface ITerminalSettings { readonly executeInFileDir: boolean; readonly focusAfterLaunch: boolean; readonly launchArgs: string[]; readonly activateEnvironment: boolean; readonly activateEnvInCurrentTerminal: boolean; + readonly shellIntegration: { + enabled: boolean; + }; +} + +export interface IREPLSettings { + readonly enableREPLSmartSend: boolean; + readonly sendToNativeREPL: boolean; } export interface IExperiments { @@ -407,11 +312,6 @@ export interface IBrowserService { launch(url: string): void; } -export const IEditorUtils = Symbol('IEditorUtils'); -export interface IEditorUtils { - getWorkspaceEditsFromPatch(originalContents: string, patch: string, uri: Uri): WorkspaceEdit; -} - /** * Stores hash formats */ diff --git a/src/client/common/utils/async.ts b/src/client/common/utils/async.ts index 29bf4a8d6fca..a44425f8f1a3 100644 --- a/src/client/common/utils/async.ts +++ b/src/client/common/utils/async.ts @@ -1,3 +1,5 @@ +/* eslint-disable @typescript-eslint/no-use-before-define */ +/* eslint-disable no-async-promise-executor */ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. @@ -50,11 +52,17 @@ class DeferredImpl implements Deferred { } public resolve(_value: T | PromiseLike) { + if (this.completed) { + return; + } this._resolve.apply(this.scope ? this.scope : this, [_value]); this._resolved = true; } public reject(_reason?: string | Error | Record) { + if (this.completed) { + return; + } this._reject.apply(this.scope ? this.scope : this, [_reason]); this._rejected = true; } @@ -147,6 +155,7 @@ export async function* chain( ): IAsyncIterableIterator { const promises = iterators.map(getNext); let numRunning = iterators.length; + while (numRunning > 0) { // Promise.race will not fail, because each promise calls getNext, // Which handles failures by wrapping each iterator in a try/catch block. @@ -222,3 +231,63 @@ export async function flattenIterator(iterator: IAsyncIterator): Promise(iterableItem: AsyncIterable): Promise { + const results: T[] = []; + for await (const item of iterableItem) { + results.push(item); + } + return results; +} + +/** + * Wait for a condition to be fulfilled within a timeout. + */ +export async function waitForCondition( + condition: () => Promise, + timeoutMs: number, + errorMessage: string, +): Promise { + return new Promise(async (resolve, reject) => { + const timeout = setTimeout(() => { + clearTimeout(timeout); + + clearTimeout(timer); + reject(new Error(errorMessage)); + }, timeoutMs); + const timer = setInterval(async () => { + if (!(await condition().catch(() => false))) { + return; + } + clearTimeout(timeout); + clearTimeout(timer); + resolve(); + }, 10); + }); +} + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export function isPromiseLike(v: any): v is PromiseLike { + return typeof v?.then === 'function'; +} + +export function raceTimeout(timeout: number, ...promises: Promise[]): Promise; +export function raceTimeout(timeout: number, defaultValue: T, ...promises: Promise[]): Promise; +export function raceTimeout(timeout: number, defaultValue: T, ...promises: Promise[]): Promise { + const resolveValue = isPromiseLike(defaultValue) ? undefined : defaultValue; + if (isPromiseLike(defaultValue)) { + promises.push((defaultValue as unknown) as Promise); + } + + let promiseResolve: ((value: T) => void) | undefined = undefined; + + const timer = setTimeout(() => promiseResolve?.((resolveValue as unknown) as T), timeout); + + return Promise.race([ + Promise.race(promises).finally(() => clearTimeout(timer)), + new Promise((resolve) => (promiseResolve = resolve)), + ]); +} diff --git a/src/client/common/utils/cacheUtils.ts b/src/client/common/utils/cacheUtils.ts index 2564eff52003..6101b3ef928f 100644 --- a/src/client/common/utils/cacheUtils.ts +++ b/src/client/common/utils/cacheUtils.ts @@ -5,11 +5,7 @@ const globalCacheStore = new Map(); -/** - * Gets a cache store to be used to store return values of methods or any other. - * - * @returns - */ +// Gets a cache store to be used to store return values of methods or any other. export function getGlobalCacheStore() { return globalCacheStore; } diff --git a/src/client/common/utils/charCode.ts b/src/client/common/utils/charCode.ts new file mode 100644 index 000000000000..ba76626bfcbb --- /dev/null +++ b/src/client/common/utils/charCode.ts @@ -0,0 +1,453 @@ +//!!! DO NOT modify, this file was COPIED from 'microsoft/vscode' + +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +// Names from https://blog.codinghorror.com/ascii-pronunciation-rules-for-programmers/ + +/** + * An inlined enum containing useful character codes (to be used with String.charCodeAt). + * Please leave the const keyword such that it gets inlined when compiled to JavaScript! + */ +export const enum CharCode { + Null = 0, + /** + * The `\b` character. + */ + Backspace = 8, + /** + * The `\t` character. + */ + Tab = 9, + /** + * The `\n` character. + */ + LineFeed = 10, + /** + * The `\r` character. + */ + CarriageReturn = 13, + Space = 32, + /** + * The `!` character. + */ + ExclamationMark = 33, + /** + * The `"` character. + */ + DoubleQuote = 34, + /** + * The `#` character. + */ + Hash = 35, + /** + * The `$` character. + */ + DollarSign = 36, + /** + * The `%` character. + */ + PercentSign = 37, + /** + * The `&` character. + */ + Ampersand = 38, + /** + * The `'` character. + */ + SingleQuote = 39, + /** + * The `(` character. + */ + OpenParen = 40, + /** + * The `)` character. + */ + CloseParen = 41, + /** + * The `*` character. + */ + Asterisk = 42, + /** + * The `+` character. + */ + Plus = 43, + /** + * The `,` character. + */ + Comma = 44, + /** + * The `-` character. + */ + Dash = 45, + /** + * The `.` character. + */ + Period = 46, + /** + * The `/` character. + */ + Slash = 47, + + Digit0 = 48, + Digit1 = 49, + Digit2 = 50, + Digit3 = 51, + Digit4 = 52, + Digit5 = 53, + Digit6 = 54, + Digit7 = 55, + Digit8 = 56, + Digit9 = 57, + + /** + * The `:` character. + */ + Colon = 58, + /** + * The `;` character. + */ + Semicolon = 59, + /** + * The `<` character. + */ + LessThan = 60, + /** + * The `=` character. + */ + Equals = 61, + /** + * The `>` character. + */ + GreaterThan = 62, + /** + * The `?` character. + */ + QuestionMark = 63, + /** + * The `@` character. + */ + AtSign = 64, + + A = 65, + B = 66, + C = 67, + D = 68, + E = 69, + F = 70, + G = 71, + H = 72, + I = 73, + J = 74, + K = 75, + L = 76, + M = 77, + N = 78, + O = 79, + P = 80, + Q = 81, + R = 82, + S = 83, + T = 84, + U = 85, + V = 86, + W = 87, + X = 88, + Y = 89, + Z = 90, + + /** + * The `[` character. + */ + OpenSquareBracket = 91, + /** + * The `\` character. + */ + Backslash = 92, + /** + * The `]` character. + */ + CloseSquareBracket = 93, + /** + * The `^` character. + */ + Caret = 94, + /** + * The `_` character. + */ + Underline = 95, + /** + * The ``(`)`` character. + */ + BackTick = 96, + + a = 97, + b = 98, + c = 99, + d = 100, + e = 101, + f = 102, + g = 103, + h = 104, + i = 105, + j = 106, + k = 107, + l = 108, + m = 109, + n = 110, + o = 111, + p = 112, + q = 113, + r = 114, + s = 115, + t = 116, + u = 117, + v = 118, + w = 119, + x = 120, + y = 121, + z = 122, + + /** + * The `{` character. + */ + OpenCurlyBrace = 123, + /** + * The `|` character. + */ + Pipe = 124, + /** + * The `}` character. + */ + CloseCurlyBrace = 125, + /** + * The `~` character. + */ + Tilde = 126, + + /** + * The   (no-break space) character. + * Unicode Character 'NO-BREAK SPACE' (U+00A0) + */ + NoBreakSpace = 160, + + U_Combining_Grave_Accent = 0x0300, // U+0300 Combining Grave Accent + U_Combining_Acute_Accent = 0x0301, // U+0301 Combining Acute Accent + U_Combining_Circumflex_Accent = 0x0302, // U+0302 Combining Circumflex Accent + U_Combining_Tilde = 0x0303, // U+0303 Combining Tilde + U_Combining_Macron = 0x0304, // U+0304 Combining Macron + U_Combining_Overline = 0x0305, // U+0305 Combining Overline + U_Combining_Breve = 0x0306, // U+0306 Combining Breve + U_Combining_Dot_Above = 0x0307, // U+0307 Combining Dot Above + U_Combining_Diaeresis = 0x0308, // U+0308 Combining Diaeresis + U_Combining_Hook_Above = 0x0309, // U+0309 Combining Hook Above + U_Combining_Ring_Above = 0x030a, // U+030A Combining Ring Above + U_Combining_Double_Acute_Accent = 0x030b, // U+030B Combining Double Acute Accent + U_Combining_Caron = 0x030c, // U+030C Combining Caron + U_Combining_Vertical_Line_Above = 0x030d, // U+030D Combining Vertical Line Above + U_Combining_Double_Vertical_Line_Above = 0x030e, // U+030E Combining Double Vertical Line Above + U_Combining_Double_Grave_Accent = 0x030f, // U+030F Combining Double Grave Accent + U_Combining_Candrabindu = 0x0310, // U+0310 Combining Candrabindu + U_Combining_Inverted_Breve = 0x0311, // U+0311 Combining Inverted Breve + U_Combining_Turned_Comma_Above = 0x0312, // U+0312 Combining Turned Comma Above + U_Combining_Comma_Above = 0x0313, // U+0313 Combining Comma Above + U_Combining_Reversed_Comma_Above = 0x0314, // U+0314 Combining Reversed Comma Above + U_Combining_Comma_Above_Right = 0x0315, // U+0315 Combining Comma Above Right + U_Combining_Grave_Accent_Below = 0x0316, // U+0316 Combining Grave Accent Below + U_Combining_Acute_Accent_Below = 0x0317, // U+0317 Combining Acute Accent Below + U_Combining_Left_Tack_Below = 0x0318, // U+0318 Combining Left Tack Below + U_Combining_Right_Tack_Below = 0x0319, // U+0319 Combining Right Tack Below + U_Combining_Left_Angle_Above = 0x031a, // U+031A Combining Left Angle Above + U_Combining_Horn = 0x031b, // U+031B Combining Horn + U_Combining_Left_Half_Ring_Below = 0x031c, // U+031C Combining Left Half Ring Below + U_Combining_Up_Tack_Below = 0x031d, // U+031D Combining Up Tack Below + U_Combining_Down_Tack_Below = 0x031e, // U+031E Combining Down Tack Below + U_Combining_Plus_Sign_Below = 0x031f, // U+031F Combining Plus Sign Below + U_Combining_Minus_Sign_Below = 0x0320, // U+0320 Combining Minus Sign Below + U_Combining_Palatalized_Hook_Below = 0x0321, // U+0321 Combining Palatalized Hook Below + U_Combining_Retroflex_Hook_Below = 0x0322, // U+0322 Combining Retroflex Hook Below + U_Combining_Dot_Below = 0x0323, // U+0323 Combining Dot Below + U_Combining_Diaeresis_Below = 0x0324, // U+0324 Combining Diaeresis Below + U_Combining_Ring_Below = 0x0325, // U+0325 Combining Ring Below + U_Combining_Comma_Below = 0x0326, // U+0326 Combining Comma Below + U_Combining_Cedilla = 0x0327, // U+0327 Combining Cedilla + U_Combining_Ogonek = 0x0328, // U+0328 Combining Ogonek + U_Combining_Vertical_Line_Below = 0x0329, // U+0329 Combining Vertical Line Below + U_Combining_Bridge_Below = 0x032a, // U+032A Combining Bridge Below + U_Combining_Inverted_Double_Arch_Below = 0x032b, // U+032B Combining Inverted Double Arch Below + U_Combining_Caron_Below = 0x032c, // U+032C Combining Caron Below + U_Combining_Circumflex_Accent_Below = 0x032d, // U+032D Combining Circumflex Accent Below + U_Combining_Breve_Below = 0x032e, // U+032E Combining Breve Below + U_Combining_Inverted_Breve_Below = 0x032f, // U+032F Combining Inverted Breve Below + U_Combining_Tilde_Below = 0x0330, // U+0330 Combining Tilde Below + U_Combining_Macron_Below = 0x0331, // U+0331 Combining Macron Below + U_Combining_Low_Line = 0x0332, // U+0332 Combining Low Line + U_Combining_Double_Low_Line = 0x0333, // U+0333 Combining Double Low Line + U_Combining_Tilde_Overlay = 0x0334, // U+0334 Combining Tilde Overlay + U_Combining_Short_Stroke_Overlay = 0x0335, // U+0335 Combining Short Stroke Overlay + U_Combining_Long_Stroke_Overlay = 0x0336, // U+0336 Combining Long Stroke Overlay + U_Combining_Short_Solidus_Overlay = 0x0337, // U+0337 Combining Short Solidus Overlay + U_Combining_Long_Solidus_Overlay = 0x0338, // U+0338 Combining Long Solidus Overlay + U_Combining_Right_Half_Ring_Below = 0x0339, // U+0339 Combining Right Half Ring Below + U_Combining_Inverted_Bridge_Below = 0x033a, // U+033A Combining Inverted Bridge Below + U_Combining_Square_Below = 0x033b, // U+033B Combining Square Below + U_Combining_Seagull_Below = 0x033c, // U+033C Combining Seagull Below + U_Combining_X_Above = 0x033d, // U+033D Combining X Above + U_Combining_Vertical_Tilde = 0x033e, // U+033E Combining Vertical Tilde + U_Combining_Double_Overline = 0x033f, // U+033F Combining Double Overline + U_Combining_Grave_Tone_Mark = 0x0340, // U+0340 Combining Grave Tone Mark + U_Combining_Acute_Tone_Mark = 0x0341, // U+0341 Combining Acute Tone Mark + U_Combining_Greek_Perispomeni = 0x0342, // U+0342 Combining Greek Perispomeni + U_Combining_Greek_Koronis = 0x0343, // U+0343 Combining Greek Koronis + U_Combining_Greek_Dialytika_Tonos = 0x0344, // U+0344 Combining Greek Dialytika Tonos + U_Combining_Greek_Ypogegrammeni = 0x0345, // U+0345 Combining Greek Ypogegrammeni + U_Combining_Bridge_Above = 0x0346, // U+0346 Combining Bridge Above + U_Combining_Equals_Sign_Below = 0x0347, // U+0347 Combining Equals Sign Below + U_Combining_Double_Vertical_Line_Below = 0x0348, // U+0348 Combining Double Vertical Line Below + U_Combining_Left_Angle_Below = 0x0349, // U+0349 Combining Left Angle Below + U_Combining_Not_Tilde_Above = 0x034a, // U+034A Combining Not Tilde Above + U_Combining_Homothetic_Above = 0x034b, // U+034B Combining Homothetic Above + U_Combining_Almost_Equal_To_Above = 0x034c, // U+034C Combining Almost Equal To Above + U_Combining_Left_Right_Arrow_Below = 0x034d, // U+034D Combining Left Right Arrow Below + U_Combining_Upwards_Arrow_Below = 0x034e, // U+034E Combining Upwards Arrow Below + U_Combining_Grapheme_Joiner = 0x034f, // U+034F Combining Grapheme Joiner + U_Combining_Right_Arrowhead_Above = 0x0350, // U+0350 Combining Right Arrowhead Above + U_Combining_Left_Half_Ring_Above = 0x0351, // U+0351 Combining Left Half Ring Above + U_Combining_Fermata = 0x0352, // U+0352 Combining Fermata + U_Combining_X_Below = 0x0353, // U+0353 Combining X Below + U_Combining_Left_Arrowhead_Below = 0x0354, // U+0354 Combining Left Arrowhead Below + U_Combining_Right_Arrowhead_Below = 0x0355, // U+0355 Combining Right Arrowhead Below + U_Combining_Right_Arrowhead_And_Up_Arrowhead_Below = 0x0356, // U+0356 Combining Right Arrowhead And Up Arrowhead Below + U_Combining_Right_Half_Ring_Above = 0x0357, // U+0357 Combining Right Half Ring Above + U_Combining_Dot_Above_Right = 0x0358, // U+0358 Combining Dot Above Right + U_Combining_Asterisk_Below = 0x0359, // U+0359 Combining Asterisk Below + U_Combining_Double_Ring_Below = 0x035a, // U+035A Combining Double Ring Below + U_Combining_Zigzag_Above = 0x035b, // U+035B Combining Zigzag Above + U_Combining_Double_Breve_Below = 0x035c, // U+035C Combining Double Breve Below + U_Combining_Double_Breve = 0x035d, // U+035D Combining Double Breve + U_Combining_Double_Macron = 0x035e, // U+035E Combining Double Macron + U_Combining_Double_Macron_Below = 0x035f, // U+035F Combining Double Macron Below + U_Combining_Double_Tilde = 0x0360, // U+0360 Combining Double Tilde + U_Combining_Double_Inverted_Breve = 0x0361, // U+0361 Combining Double Inverted Breve + U_Combining_Double_Rightwards_Arrow_Below = 0x0362, // U+0362 Combining Double Rightwards Arrow Below + U_Combining_Latin_Small_Letter_A = 0x0363, // U+0363 Combining Latin Small Letter A + U_Combining_Latin_Small_Letter_E = 0x0364, // U+0364 Combining Latin Small Letter E + U_Combining_Latin_Small_Letter_I = 0x0365, // U+0365 Combining Latin Small Letter I + U_Combining_Latin_Small_Letter_O = 0x0366, // U+0366 Combining Latin Small Letter O + U_Combining_Latin_Small_Letter_U = 0x0367, // U+0367 Combining Latin Small Letter U + U_Combining_Latin_Small_Letter_C = 0x0368, // U+0368 Combining Latin Small Letter C + U_Combining_Latin_Small_Letter_D = 0x0369, // U+0369 Combining Latin Small Letter D + U_Combining_Latin_Small_Letter_H = 0x036a, // U+036A Combining Latin Small Letter H + U_Combining_Latin_Small_Letter_M = 0x036b, // U+036B Combining Latin Small Letter M + U_Combining_Latin_Small_Letter_R = 0x036c, // U+036C Combining Latin Small Letter R + U_Combining_Latin_Small_Letter_T = 0x036d, // U+036D Combining Latin Small Letter T + U_Combining_Latin_Small_Letter_V = 0x036e, // U+036E Combining Latin Small Letter V + U_Combining_Latin_Small_Letter_X = 0x036f, // U+036F Combining Latin Small Letter X + + /** + * Unicode Character 'LINE SEPARATOR' (U+2028) + * http://www.fileformat.info/info/unicode/char/2028/index.htm + */ + LINE_SEPARATOR = 0x2028, + /** + * Unicode Character 'PARAGRAPH SEPARATOR' (U+2029) + * http://www.fileformat.info/info/unicode/char/2029/index.htm + */ + PARAGRAPH_SEPARATOR = 0x2029, + /** + * Unicode Character 'NEXT LINE' (U+0085) + * http://www.fileformat.info/info/unicode/char/0085/index.htm + */ + NEXT_LINE = 0x0085, + + // http://www.fileformat.info/info/unicode/category/Sk/list.htm + // eslint-disable-next-line @typescript-eslint/no-duplicate-enum-values + U_CIRCUMFLEX = 0x005e, // U+005E CIRCUMFLEX + // eslint-disable-next-line @typescript-eslint/no-duplicate-enum-values + U_GRAVE_ACCENT = 0x0060, // U+0060 GRAVE ACCENT + U_DIAERESIS = 0x00a8, // U+00A8 DIAERESIS + U_MACRON = 0x00af, // U+00AF MACRON + U_ACUTE_ACCENT = 0x00b4, // U+00B4 ACUTE ACCENT + U_CEDILLA = 0x00b8, // U+00B8 CEDILLA + U_MODIFIER_LETTER_LEFT_ARROWHEAD = 0x02c2, // U+02C2 MODIFIER LETTER LEFT ARROWHEAD + U_MODIFIER_LETTER_RIGHT_ARROWHEAD = 0x02c3, // U+02C3 MODIFIER LETTER RIGHT ARROWHEAD + U_MODIFIER_LETTER_UP_ARROWHEAD = 0x02c4, // U+02C4 MODIFIER LETTER UP ARROWHEAD + U_MODIFIER_LETTER_DOWN_ARROWHEAD = 0x02c5, // U+02C5 MODIFIER LETTER DOWN ARROWHEAD + U_MODIFIER_LETTER_CENTRED_RIGHT_HALF_RING = 0x02d2, // U+02D2 MODIFIER LETTER CENTRED RIGHT HALF RING + U_MODIFIER_LETTER_CENTRED_LEFT_HALF_RING = 0x02d3, // U+02D3 MODIFIER LETTER CENTRED LEFT HALF RING + U_MODIFIER_LETTER_UP_TACK = 0x02d4, // U+02D4 MODIFIER LETTER UP TACK + U_MODIFIER_LETTER_DOWN_TACK = 0x02d5, // U+02D5 MODIFIER LETTER DOWN TACK + U_MODIFIER_LETTER_PLUS_SIGN = 0x02d6, // U+02D6 MODIFIER LETTER PLUS SIGN + U_MODIFIER_LETTER_MINUS_SIGN = 0x02d7, // U+02D7 MODIFIER LETTER MINUS SIGN + U_BREVE = 0x02d8, // U+02D8 BREVE + U_DOT_ABOVE = 0x02d9, // U+02D9 DOT ABOVE + U_RING_ABOVE = 0x02da, // U+02DA RING ABOVE + U_OGONEK = 0x02db, // U+02DB OGONEK + U_SMALL_TILDE = 0x02dc, // U+02DC SMALL TILDE + U_DOUBLE_ACUTE_ACCENT = 0x02dd, // U+02DD DOUBLE ACUTE ACCENT + U_MODIFIER_LETTER_RHOTIC_HOOK = 0x02de, // U+02DE MODIFIER LETTER RHOTIC HOOK + U_MODIFIER_LETTER_CROSS_ACCENT = 0x02df, // U+02DF MODIFIER LETTER CROSS ACCENT + U_MODIFIER_LETTER_EXTRA_HIGH_TONE_BAR = 0x02e5, // U+02E5 MODIFIER LETTER EXTRA-HIGH TONE BAR + U_MODIFIER_LETTER_HIGH_TONE_BAR = 0x02e6, // U+02E6 MODIFIER LETTER HIGH TONE BAR + U_MODIFIER_LETTER_MID_TONE_BAR = 0x02e7, // U+02E7 MODIFIER LETTER MID TONE BAR + U_MODIFIER_LETTER_LOW_TONE_BAR = 0x02e8, // U+02E8 MODIFIER LETTER LOW TONE BAR + U_MODIFIER_LETTER_EXTRA_LOW_TONE_BAR = 0x02e9, // U+02E9 MODIFIER LETTER EXTRA-LOW TONE BAR + U_MODIFIER_LETTER_YIN_DEPARTING_TONE_MARK = 0x02ea, // U+02EA MODIFIER LETTER YIN DEPARTING TONE MARK + U_MODIFIER_LETTER_YANG_DEPARTING_TONE_MARK = 0x02eb, // U+02EB MODIFIER LETTER YANG DEPARTING TONE MARK + U_MODIFIER_LETTER_UNASPIRATED = 0x02ed, // U+02ED MODIFIER LETTER UNASPIRATED + U_MODIFIER_LETTER_LOW_DOWN_ARROWHEAD = 0x02ef, // U+02EF MODIFIER LETTER LOW DOWN ARROWHEAD + U_MODIFIER_LETTER_LOW_UP_ARROWHEAD = 0x02f0, // U+02F0 MODIFIER LETTER LOW UP ARROWHEAD + U_MODIFIER_LETTER_LOW_LEFT_ARROWHEAD = 0x02f1, // U+02F1 MODIFIER LETTER LOW LEFT ARROWHEAD + U_MODIFIER_LETTER_LOW_RIGHT_ARROWHEAD = 0x02f2, // U+02F2 MODIFIER LETTER LOW RIGHT ARROWHEAD + U_MODIFIER_LETTER_LOW_RING = 0x02f3, // U+02F3 MODIFIER LETTER LOW RING + U_MODIFIER_LETTER_MIDDLE_GRAVE_ACCENT = 0x02f4, // U+02F4 MODIFIER LETTER MIDDLE GRAVE ACCENT + U_MODIFIER_LETTER_MIDDLE_DOUBLE_GRAVE_ACCENT = 0x02f5, // U+02F5 MODIFIER LETTER MIDDLE DOUBLE GRAVE ACCENT + U_MODIFIER_LETTER_MIDDLE_DOUBLE_ACUTE_ACCENT = 0x02f6, // U+02F6 MODIFIER LETTER MIDDLE DOUBLE ACUTE ACCENT + U_MODIFIER_LETTER_LOW_TILDE = 0x02f7, // U+02F7 MODIFIER LETTER LOW TILDE + U_MODIFIER_LETTER_RAISED_COLON = 0x02f8, // U+02F8 MODIFIER LETTER RAISED COLON + U_MODIFIER_LETTER_BEGIN_HIGH_TONE = 0x02f9, // U+02F9 MODIFIER LETTER BEGIN HIGH TONE + U_MODIFIER_LETTER_END_HIGH_TONE = 0x02fa, // U+02FA MODIFIER LETTER END HIGH TONE + U_MODIFIER_LETTER_BEGIN_LOW_TONE = 0x02fb, // U+02FB MODIFIER LETTER BEGIN LOW TONE + U_MODIFIER_LETTER_END_LOW_TONE = 0x02fc, // U+02FC MODIFIER LETTER END LOW TONE + U_MODIFIER_LETTER_SHELF = 0x02fd, // U+02FD MODIFIER LETTER SHELF + U_MODIFIER_LETTER_OPEN_SHELF = 0x02fe, // U+02FE MODIFIER LETTER OPEN SHELF + U_MODIFIER_LETTER_LOW_LEFT_ARROW = 0x02ff, // U+02FF MODIFIER LETTER LOW LEFT ARROW + U_GREEK_LOWER_NUMERAL_SIGN = 0x0375, // U+0375 GREEK LOWER NUMERAL SIGN + U_GREEK_TONOS = 0x0384, // U+0384 GREEK TONOS + U_GREEK_DIALYTIKA_TONOS = 0x0385, // U+0385 GREEK DIALYTIKA TONOS + U_GREEK_KORONIS = 0x1fbd, // U+1FBD GREEK KORONIS + U_GREEK_PSILI = 0x1fbf, // U+1FBF GREEK PSILI + U_GREEK_PERISPOMENI = 0x1fc0, // U+1FC0 GREEK PERISPOMENI + U_GREEK_DIALYTIKA_AND_PERISPOMENI = 0x1fc1, // U+1FC1 GREEK DIALYTIKA AND PERISPOMENI + U_GREEK_PSILI_AND_VARIA = 0x1fcd, // U+1FCD GREEK PSILI AND VARIA + U_GREEK_PSILI_AND_OXIA = 0x1fce, // U+1FCE GREEK PSILI AND OXIA + U_GREEK_PSILI_AND_PERISPOMENI = 0x1fcf, // U+1FCF GREEK PSILI AND PERISPOMENI + U_GREEK_DASIA_AND_VARIA = 0x1fdd, // U+1FDD GREEK DASIA AND VARIA + U_GREEK_DASIA_AND_OXIA = 0x1fde, // U+1FDE GREEK DASIA AND OXIA + U_GREEK_DASIA_AND_PERISPOMENI = 0x1fdf, // U+1FDF GREEK DASIA AND PERISPOMENI + U_GREEK_DIALYTIKA_AND_VARIA = 0x1fed, // U+1FED GREEK DIALYTIKA AND VARIA + U_GREEK_DIALYTIKA_AND_OXIA = 0x1fee, // U+1FEE GREEK DIALYTIKA AND OXIA + U_GREEK_VARIA = 0x1fef, // U+1FEF GREEK VARIA + U_GREEK_OXIA = 0x1ffd, // U+1FFD GREEK OXIA + U_GREEK_DASIA = 0x1ffe, // U+1FFE GREEK DASIA + + U_IDEOGRAPHIC_FULL_STOP = 0x3002, // U+3002 IDEOGRAPHIC FULL STOP + U_LEFT_CORNER_BRACKET = 0x300c, // U+300C LEFT CORNER BRACKET + U_RIGHT_CORNER_BRACKET = 0x300d, // U+300D RIGHT CORNER BRACKET + U_LEFT_BLACK_LENTICULAR_BRACKET = 0x3010, // U+3010 LEFT BLACK LENTICULAR BRACKET + U_RIGHT_BLACK_LENTICULAR_BRACKET = 0x3011, // U+3011 RIGHT BLACK LENTICULAR BRACKET + + U_OVERLINE = 0x203e, // Unicode Character 'OVERLINE' + + /** + * UTF-8 BOM + * Unicode Character 'ZERO WIDTH NO-BREAK SPACE' (U+FEFF) + * http://www.fileformat.info/info/unicode/char/feff/index.htm + */ + UTF8_BOM = 65279, + + U_FULLWIDTH_SEMICOLON = 0xff1b, // U+FF1B FULLWIDTH SEMICOLON + U_FULLWIDTH_COMMA = 0xff0c, // U+FF0C FULLWIDTH COMMA +} diff --git a/src/client/common/utils/decorators.ts b/src/client/common/utils/decorators.ts index 689eb9acad44..44a82ee13760 100644 --- a/src/client/common/utils/decorators.ts +++ b/src/client/common/utils/decorators.ts @@ -1,5 +1,5 @@ import '../../common/extensions'; -import { traceError, traceVerbose } from '../../logging'; +import { traceError } from '../../logging'; import { isTestExecution } from '../constants'; import { createDeferred, Deferred } from './async'; import { getCacheKeyFromFunctionArgs, getGlobalCacheStore } from './cacheUtils'; @@ -161,7 +161,6 @@ export function cache(expiryDurationMs: number, cachePromise = false, expiryDura } const cachedItem = cacheStoreForMethods.get(key); if (cachedItem && (cachedItem.expiry > Date.now() || expiryDurationMs === -1)) { - traceVerbose(`Cached data exists ${key}`); return Promise.resolve(cachedItem.data); } const expiryMs = diff --git a/src/client/common/utils/iterable.ts b/src/client/common/utils/iterable.ts new file mode 100644 index 000000000000..5e04aaa430ea --- /dev/null +++ b/src/client/common/utils/iterable.ts @@ -0,0 +1,11 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +/* eslint-disable @typescript-eslint/no-explicit-any */ +// eslint-disable-next-line @typescript-eslint/no-namespace +export namespace Iterable { + // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types + export function is(thing: any): thing is Iterable { + return thing && typeof thing === 'object' && typeof thing[Symbol.iterator] === 'function'; + } +} diff --git a/src/client/common/utils/localize.ts b/src/client/common/utils/localize.ts index f32c4fec0ac9..7b7560c74e05 100644 --- a/src/client/common/utils/localize.ts +++ b/src/client/common/utils/localize.ts @@ -4,20 +4,12 @@ 'use strict'; import { l10n } from 'vscode'; +import { Commands } from '../constants'; /* eslint-disable @typescript-eslint/no-namespace, no-shadow */ // External callers of localize use these tables to retrieve localized values. export namespace Diagnostics { - export const warnSourceMaps = l10n.t( - 'Source map support is enabled in the Python Extension, this will adversely impact performance of the extension.', - ); - export const disableSourceMaps = l10n.t('Disable Source Map Support'); - - export const warnBeforeEnablingSourceMaps = l10n.t( - 'Enabling source map support in the Python Extension will adversely impact performance of the extension.', - ); - export const enableSourceMapsAndReloadVSC = l10n.t('Enable and reload Window.'); export const lsNotSupported = l10n.t( 'Your operating system does not meet the minimum requirements of the Python Language Server. Reverting to the alternative autocompletion provider, Jedi.', ); @@ -39,12 +31,15 @@ export namespace Diagnostics { 'Your settings needs to be updated to change the setting "python.unitTest." to "python.testing.", otherwise testing Python code using the extension may not work. Would you like to automatically update your settings now?', ); export const updateSettings = l10n.t('Yes, update settings'); - export const checkIsort5UpgradeGuide = l10n.t( - 'We found outdated configuration for sorting imports in this workspace. Check the [isort upgrade guide](https://aka.ms/AA9j5x4) to update your settings.', - ); export const pylanceDefaultMessage = l10n.t( "The Python extension now includes Pylance to improve completions, code navigation, overall performance and much more! You can learn more about the update and learn how to change your language server [here](https://aka.ms/new-python-bundle).\n\nRead Pylance's license [here](https://marketplace.visualstudio.com/items/ms-python.vscode-pylance/license).", ); + export const invalidSmartSendMessage = l10n.t( + `Python is unable to parse the code provided. Please + turn off Smart Send if you wish to always run line by line or explicitly select code + to force run. See [logs](command:{0}) for more details`, + Commands.ViewOutput, + ); } export namespace Common { @@ -53,7 +48,6 @@ export namespace Common { export const close = l10n.t('Close'); export const bannerLabelYes = l10n.t('Yes'); export const bannerLabelNo = l10n.t('No'); - export const yesPlease = l10n.t('Yes, please'); export const canceled = l10n.t('Canceled'); export const cancel = l10n.t('Cancel'); export const ok = l10n.t('Ok'); @@ -64,7 +58,8 @@ export namespace Common { export const openOutputPanel = l10n.t('Show output'); export const noIWillDoItLater = l10n.t('No, I will do it later'); export const notNow = l10n.t('Not now'); - export const doNotShowAgain = l10n.t('Do not show again'); + export const doNotShowAgain = l10n.t("Don't show again"); + export const editSomething = l10n.t('Edit {0}'); export const reload = l10n.t('Reload'); export const moreInfo = l10n.t('More Info'); export const learnMore = l10n.t('Learn more'); @@ -95,6 +90,10 @@ export namespace AttachProcess { export const refreshList = l10n.t('Refresh process list'); } +export namespace Repl { + export const disableSmartSend = l10n.t('Disable Smart Send'); + export const launchNativeRepl = l10n.t('Launch VS Code Native REPL'); +} export namespace Pylance { export const remindMeLater = l10n.t('Remind me later'); @@ -140,10 +139,8 @@ export namespace TensorBoard { export const upgradePrompt = l10n.t( 'Integrated TensorBoard support is only available for TensorBoard >= 2.4.1. Would you like to upgrade your copy of TensorBoard?', ); - export const launchNativeTensorBoardSessionCodeLens = l10n.t('β–Ά Launch TensorBoard Session'); - export const launchNativeTensorBoardSessionCodeAction = l10n.t('Launch TensorBoard session'); export const missingSourceFile = l10n.t( - 'We could not locate the requested source file on disk. Please manually specify the file.', + 'The Python extension could not locate the requested source file on disk. Please manually specify the file.', ); export const selectMissingSourceFile = l10n.t('Choose File'); export const selectMissingSourceFileDescription = l10n.t( @@ -167,7 +164,7 @@ export namespace LanguageService { ); export const reloadAfterLanguageServerChange = l10n.t( - 'Please reload the window switching between language servers.', + 'Reload the window after switching between language servers.', ); export const lsFailedToStart = l10n.t( @@ -184,7 +181,7 @@ export namespace LanguageService { export const extractionCompletedOutputMessage = l10n.t('Language server download complete.'); export const extractionDoneOutputMessage = l10n.t('done.'); export const reloadVSCodeIfSeachPathHasChanged = l10n.t( - 'Search paths have changed for this Python interpreter. Please reload the extension to ensure that the IntelliSense works correctly.', + 'Search paths have changed for this Python interpreter. Reload the extension to ensure that the IntelliSense works correctly.', ); } export namespace Interpreters { @@ -194,11 +191,43 @@ export namespace Interpreters { export const installingPython = l10n.t('Installing Python into Environment...'); export const discovering = l10n.t('Discovering Python Interpreters'); export const refreshing = l10n.t('Refreshing Python Interpreters'); + export const envExtDiscoveryAttribution = l10n.t( + 'Environment discovery is managed by the Python Environments extension (ms-python.vscode-python-envs). Check the "Python Environments" output channel for environment-specific logs.', + ); + export const envExtDiscoveryFailed = l10n.t( + 'Environment discovery failed. Check the "Python Environments" output channel for details. The Python Environments extension (ms-python.vscode-python-envs) manages environment discovery.', + ); + export const envExtDiscoverySlow = l10n.t( + 'Environment discovery is taking longer than expected. Check the "Python Environments" output channel for progress. The Python Environments extension (ms-python.vscode-python-envs) manages environment discovery.', + ); + export const envExtActivationFailed = l10n.t( + 'Failed to activate the Python Environments extension (ms-python.vscode-python-envs), which is required for environment discovery. Please ensure it is installed and enabled.', + ); + export const envExtDiscoveryNoEnvironments = l10n.t( + 'Environment discovery completed but no Python environments were found. Check the "Python Environments" output channel for details.', + ); + export const envExtNoActiveEnvironment = l10n.t( + 'No Python environment is set for this resource. Check the "Python Environments" output channel for details, or select an interpreter.', + ); export const condaInheritEnvMessage = l10n.t( 'We noticed you\'re using a conda environment. If you are experiencing issues with this environment in the integrated terminal, we recommend that you let the Python extension change "terminal.integrated.inheritEnv" to false in your user settings. [Learn more](https://aka.ms/AA66i8f).', ); export const activatingTerminals = l10n.t('Reactivating terminals...'); export const activateTerminalDescription = l10n.t('Activated environment for'); + export const terminalEnvVarCollectionPrompt = l10n.t( + '{0} environment was successfully activated, even though {1} indicator may not be present in the terminal prompt. [Learn more](https://aka.ms/vscodePythonTerminalActivation).', + ); + export const shellIntegrationEnvVarCollectionDescription = l10n.t( + 'Enables `python.terminal.shellIntegration.enabled` by modifying `PYTHONSTARTUP` and `PYTHON_BASIC_REPL`', + ); + export const shellIntegrationDisabledEnvVarCollectionDescription = l10n.t( + 'Disables `python.terminal.shellIntegration.enabled` by unsetting `PYTHONSTARTUP` and `PYTHON_BASIC_REPL`', + ); + export const terminalDeactivateProgress = l10n.t('Editing {0}...'); + export const restartingTerminal = l10n.t('Restarting terminal and deactivating...'); + export const terminalDeactivatePrompt = l10n.t( + 'Deactivating virtual environments may not work by default. To make it work, edit your "{0}" and then restart your shell. [Learn more](https://aka.ms/AAmx2ft).', + ); export const activatedCondaEnvLaunch = l10n.t( 'We noticed VS Code was launched from an activated conda environment, would you like to select it?', ); @@ -211,11 +240,11 @@ export namespace Interpreters { 'Tip: you can change the Python interpreter used by the Python extension by clicking on the Python version in the status bar', ); export const installPythonTerminalMessageLinux = l10n.t( - 'πŸ’‘ Please try installing the Python package using your package manager. Alternatively you can also download it from https://www.python.org/downloads', + 'πŸ’‘ Try installing the Python package using your package manager. Alternatively you can also download it from https://www.python.org/downloads', ); export const installPythonTerminalMacMessage = l10n.t( - 'πŸ’‘ Brew does not seem to be available. Please try to download Python from https://www.python.org/downloads. Alternatively, you can install the Python package using some other available package manager.', + 'πŸ’‘ Brew does not seem to be available. You can download Python from https://www.python.org/downloads. Alternatively, you can install the Python package using some other available package manager.', ); export const changePythonInterpreter = l10n.t('Change Python Interpreter'); export const selectedPythonInterpreter = l10n.t('Selected Python Interpreter'); @@ -225,7 +254,7 @@ export namespace InterpreterQuickPickList { export const condaEnvWithoutPythonTooltip = l10n.t( 'Python is not available in this environment, it will automatically be installed upon selecting it', ); - export const noPythonInstalled = l10n.t('Python is not installed, please download and install it'); + export const noPythonInstalled = l10n.t('Python is not installed'); export const clickForInstructions = l10n.t('Click for instructions...'); export const globalGroupName = l10n.t('Global'); export const workspaceGroupName = l10n.t('Workspace'); @@ -244,12 +273,14 @@ export namespace InterpreterQuickPickList { }; export const refreshInterpreterList = l10n.t('Refresh Interpreter list'); export const refreshingInterpreterList = l10n.t('Refreshing Interpreter list...'); + export const create = { + label: l10n.t('Create Virtual Environment...'), + }; } export namespace OutputChannelNames { export const languageServer = l10n.t('Python Language Server'); export const python = l10n.t('Python'); - export const pythonTest = l10n.t('Python Test Log'); } export namespace Linters { @@ -266,7 +297,7 @@ export namespace Installer { export namespace ExtensionSurveyBanner { export const bannerMessage = l10n.t( - 'Can you please take 2 minutes to tell us how the Python extension is working for you?', + 'Can you take 2 minutes to tell us how the Python extension is working for you?', ); export const bannerLabelYes = l10n.t('Yes, take survey now'); export const bannerLabelNo = l10n.t('No, thanks'); @@ -404,7 +435,6 @@ export namespace DebugConfigStrings { export namespace Testing { export const configureTests = l10n.t('Configure Test Framework'); - export const testNotConfigured = l10n.t('No test framework configured.'); export const cancelUnittestDiscovery = l10n.t('Canceled unittest test discovery'); export const errorUnittestDiscovery = l10n.t('Unittest test discovery error'); export const cancelPytestDiscovery = l10n.t('Canceled pytest test discovery'); @@ -413,12 +443,13 @@ export namespace Testing { export const cancelUnittestExecution = l10n.t('Canceled unittest test execution'); export const errorUnittestExecution = l10n.t('Unittest test execution error'); export const cancelPytestExecution = l10n.t('Canceled pytest test execution'); - export const errorPytestExecution = l10n.t('Pytest test execution error'); + export const errorPytestExecution = l10n.t('pytest test execution error'); + export const copilotSetupMessage = l10n.t('Confirm your Python testing framework to enable test discovery.'); } export namespace OutdatedDebugger { export const outdatedDebuggerMessage = l10n.t( - 'We noticed you are attaching to ptvsd (Python debugger), which was deprecated on May 1st, 2020. Please switch to [debugpy](https://aka.ms/migrateToDebugpy).', + 'We noticed you are attaching to ptvsd (Python debugger), which was deprecated on May 1st, 2020. Use [debugpy](https://aka.ms/migrateToDebugpy) instead.', ); } @@ -435,13 +466,13 @@ export namespace SwitchToDefaultLS { } export namespace CreateEnv { - export const informEnvCreation = l10n.t('We have selected the following environment:'); + export const informEnvCreation = l10n.t('The following environment is selected:'); export const statusTitle = l10n.t('Creating environment'); export const statusStarting = l10n.t('Starting...'); export const hasVirtualEnv = l10n.t('Workspace folder contains a virtual environment'); - export const noWorkspace = l10n.t('Please open a folder when creating an environment using venv.'); + export const noWorkspace = l10n.t('A workspace is required when creating an environment using venv.'); export const pickWorkspacePlaceholder = l10n.t('Select a workspace to create environment'); @@ -462,52 +493,62 @@ export namespace CreateEnv { export const error = l10n.t('Creating virtual environment failed with error.'); export const tomlExtrasQuickPickTitle = l10n.t('Select optional dependencies to install from pyproject.toml'); export const requirementsQuickPickTitle = l10n.t('Select dependencies to install'); + export const recreate = l10n.t('Delete and Recreate'); + export const recreateDescription = l10n.t( + 'Delete existing ".venv" directory and create a new ".venv" environment', + ); + export const useExisting = l10n.t('Use Existing'); + export const useExistingDescription = l10n.t('Use existing ".venv" environment with no changes to it'); + export const existingVenvQuickPickPlaceholder = l10n.t( + 'Choose an option to handle the existing ".venv" environment', + ); + export const deletingEnvironmentProgress = l10n.t('Deleting existing ".venv" environment...'); + export const errorDeletingEnvironment = l10n.t('Error while deleting existing ".venv" environment.'); + export const openRequirementsFile = l10n.t('Open requirements file'); } export namespace Conda { - export const condaMissing = l10n.t('Please install `conda` to create conda environments.'); + export const condaMissing = l10n.t('Install `conda` to create conda environments.'); export const created = l10n.t('Environment created...'); export const installingPackages = l10n.t('Installing packages...'); export const errorCreatingEnvironment = l10n.t('Error while creating conda environment.'); export const selectPythonQuickPickPlaceholder = l10n.t( - 'Please select the version of Python to install in the environment', + 'Select the version of Python to install in the environment', ); export const creating = l10n.t('Creating conda environment...'); export const providerDescription = l10n.t('Creates a `.conda` Conda environment in the current workspace'); - } -} -export namespace ToolsExtensions { - export const flake8PromptMessage = l10n.t( - 'Use the Flake8 extension to enable easier configuration and new features such as quick fixes.', - ); - export const pylintPromptMessage = l10n.t( - 'Use the Pylint extension to enable easier configuration and new features such as quick fixes.', - ); - export const isortPromptMessage = l10n.t( - 'To use sort imports, please install the isort extension. It provides easier configuration and new features such as code actions.', - ); - export const installPylintExtension = l10n.t('Install Pylint extension'); - export const installFlake8Extension = l10n.t('Install Flake8 extension'); - export const installISortExtension = l10n.t('Install isort extension'); + export const recreate = l10n.t('Delete and Recreate'); + export const recreateDescription = l10n.t('Delete existing ".conda" environment and create a new one'); + export const useExisting = l10n.t('Use Existing'); + export const useExistingDescription = l10n.t('Use existing ".conda" environment with no changes to it'); + export const existingCondaQuickPickPlaceholder = l10n.t( + 'Choose an option to handle the existing ".conda" environment', + ); + export const deletingEnvironmentProgress = l10n.t('Deleting existing ".conda" environment...'); + export const errorDeletingEnvironment = l10n.t('Error while deleting existing ".conda" environment.'); + } - export const selectBlackFormatterPrompt = l10n.t( - 'You have the Black formatter extension installed, would you like to use that as the default formatter?', - ); + export namespace Trigger { + export const workspaceTriggerMessage = l10n.t( + 'A virtual environment is not currently selected for your Python interpreter. Would you like to create a virtual environment?', + ); + export const createEnvironment = l10n.t('Create'); - export const selectAutopep8FormatterPrompt = l10n.t( - 'You have the Autopep8 formatter extension installed, would you like to use that as the default formatter?', - ); + export const globalPipInstallTriggerMessage = l10n.t( + 'You may have installed Python packages into your global environment, which can cause conflicts between package versions. Would you like to create a virtual environment with these packages to isolate your dependencies?', + ); + } +} - export const selectMultipleFormattersPrompt = l10n.t( - 'You have multiple formatters installed, would you like to select one as the default formatter?', +export namespace PythonLocator { + export const startupFailedNotification = l10n.t( + 'Python Locator failed to start. Python environment discovery may not work correctly.', ); - - export const installBlackFormatterPrompt = l10n.t( - 'You triggered formatting with Black, would you like to install one of our new formatter extensions? This will also set it as the default formatter for Python.', + export const windowsRuntimeMissing = l10n.t( + 'Missing Windows runtime dependencies detected. The Python Locator requires the Microsoft Visual C++ Redistributable. This is often missing on clean Windows installations.', ); - - export const installAutopep8FormatterPrompt = l10n.t( - 'You triggered formatting with Autopep8, would you like to install one of our new formatter extension? This will also set it as the default formatter for Python.', + export const windowsStartupFailed = l10n.t( + 'Python Locator failed to start on Windows. This might be due to missing system dependencies such as the Microsoft Visual C++ Redistributable.', ); } diff --git a/src/client/common/utils/misc.ts b/src/client/common/utils/misc.ts index c95a3cc75575..a461d25d9d30 100644 --- a/src/client/common/utils/misc.ts +++ b/src/client/common/utils/misc.ts @@ -27,10 +27,6 @@ type NonFunctionPropertyNames = { [K in keyof T]: T[K] extends Function ? nev * Checking whether something is a Resource (Uri/undefined). * Using `instanceof Uri` doesn't always work as the object is not an instance of Uri (at least not in tests). * That's why VSC too has a helper method `URI.isUri` (though not public). - * - * @export - * @param {InterpreterUri} [resource] - * @returns {resource is Resource} */ export function isResource(resource?: InterpreterUri): resource is Resource { if (!resource) { @@ -44,9 +40,6 @@ export function isResource(resource?: InterpreterUri): resource is Resource { * Checking whether something is a Uri. * Using `instanceof Uri` doesn't always work as the object is not an instance of Uri (at least not in tests). * That's why VSC too has a helper method `URI.isUri` (though not public). - * - * @param {InterpreterUri} [resource] - * @returns {resource is Uri} */ function isUri(resource?: Uri | any): resource is Uri { @@ -60,7 +53,7 @@ function isUri(resource?: Uri | any): resource is Uri { /** * Create a filter func that determine if the given URI and candidate match. * - * The scheme must match, as well as path. + * Only compares path. * * @param checkParent - if `true`, match if the candidate is rooted under `uri` * or if the candidate matches `uri` exactly. @@ -80,9 +73,8 @@ export function getURIFilter( } const uriRoot = `${uriPath}/`; function filter(candidate: Uri): boolean { - if (candidate.scheme !== uri.scheme) { - return false; - } + // Do not compare schemes as it is sometimes not available, in + // which case file is assumed as scheme. let candidatePath = candidate.path; while (candidatePath.endsWith('/')) { candidatePath = candidatePath.slice(0, -1); diff --git a/src/client/common/utils/multiStepInput.ts b/src/client/common/utils/multiStepInput.ts index e44879e8bbbb..2de1684a4d2e 100644 --- a/src/client/common/utils/multiStepInput.ts +++ b/src/client/common/utils/multiStepInput.ts @@ -26,7 +26,7 @@ export class InputFlowAction { } // eslint-disable-next-line @typescript-eslint/no-explicit-any -export type InputStep = (input: MultiStepInput, state: T) => Promise | void>; +export type InputStep = (input: MultiStepInput, state: T) => Promise | void>; type buttonCallbackType = (quickPick: QuickPick) => void; @@ -47,7 +47,7 @@ export interface IQuickPickParameters { totalSteps?: number; canGoBack?: boolean; items: T[]; - activeItem?: T | Promise; + activeItem?: T | ((quickPick: QuickPick) => Promise); placeholder: string | undefined; customButtonSetups?: QuickInputButtonSetup[]; matchOnDescription?: boolean; @@ -156,7 +156,13 @@ export class MultiStepInput implements IMultiStepInput { initialize(input); } if (activeItem) { - input.activeItems = [await activeItem]; + if (typeof activeItem === 'function') { + activeItem(input).then((item) => { + if (input.activeItems.length === 0) { + input.activeItems = [item]; + } + }); + } } else { input.activeItems = []; } diff --git a/src/client/common/utils/platform.ts b/src/client/common/utils/platform.ts index cf3b28e5cc35..a1a49ba3c427 100644 --- a/src/client/common/utils/platform.ts +++ b/src/client/common/utils/platform.ts @@ -67,3 +67,15 @@ export function getUserHomeDir(): string | undefined { } return getEnvironmentVariable('HOME') || getEnvironmentVariable('HOMEPATH'); } + +export function isWindows(): boolean { + return getOSType() === OSType.Windows; +} + +export function getPathEnvVariable(): string[] { + const value = getEnvironmentVariable('PATH') || getEnvironmentVariable('Path'); + if (value) { + return value.split(isWindows() ? ';' : ':'); + } + return []; +} diff --git a/src/client/common/utils/resourceLifecycle.ts b/src/client/common/utils/resourceLifecycle.ts index 485294392ea1..b5d1a9a1c83a 100644 --- a/src/client/common/utils/resourceLifecycle.ts +++ b/src/client/common/utils/resourceLifecycle.ts @@ -1,12 +1,51 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. +// eslint-disable-next-line max-classes-per-file +import { traceWarn } from '../../logging'; import { IDisposable } from '../types'; +import { Iterable } from './iterable'; interface IDisposables extends IDisposable { push(...disposable: IDisposable[]): void; } +export const EmptyDisposable = { + // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types + dispose: () => { + /** */ + }, +}; + +/** + * Disposes of the value(s) passed in. + */ +export function dispose(disposable: T): T; +export function dispose(disposable: T | undefined): T | undefined; +export function dispose = Iterable>(disposables: A): A; +export function dispose(disposables: Array): Array; +export function dispose(disposables: ReadonlyArray): ReadonlyArray; +// eslint-disable-next-line @typescript-eslint/no-explicit-any, consistent-return +export function dispose(arg: T | Iterable | undefined): any { + if (Iterable.is(arg)) { + for (const d of arg) { + if (d) { + try { + d.dispose(); + } catch (e) { + traceWarn(`dispose() failed for ${d}`, e); + } + } + } + + return Array.isArray(arg) ? [] : arg; + } + if (arg) { + arg.dispose(); + return arg; + } +} + /** * Safely dispose each of the disposables. */ @@ -43,3 +82,118 @@ export class Disposables implements IDisposables { await disposeAll(disposables); } } + +/** + * Manages a collection of disposable values. + * + * This is the preferred way to manage multiple disposables. A `DisposableStore` is safer to work with than an + * `IDisposable[]` as it considers edge cases, such as registering the same value multiple times or adding an item to a + * store that has already been disposed of. + */ +export class DisposableStore implements IDisposable { + static DISABLE_DISPOSED_WARNING = false; + + private readonly _toDispose = new Set(); + + private _isDisposed = false; + + constructor(...disposables: IDisposable[]) { + disposables.forEach((disposable) => this.add(disposable)); + } + + /** + * Dispose of all registered disposables and mark this object as disposed. + * + * Any future disposables added to this object will be disposed of on `add`. + */ + public dispose(): void { + if (this._isDisposed) { + return; + } + + this._isDisposed = true; + this.clear(); + } + + /** + * @return `true` if this object has been disposed of. + */ + public get isDisposed(): boolean { + return this._isDisposed; + } + + /** + * Dispose of all registered disposables but do not mark this object as disposed. + */ + public clear(): void { + if (this._toDispose.size === 0) { + return; + } + + try { + dispose(this._toDispose); + } finally { + this._toDispose.clear(); + } + } + + /** + * Add a new {@link IDisposable disposable} to the collection. + */ + public add(o: T): T { + if (!o) { + return o; + } + if (((o as unknown) as DisposableStore) === this) { + throw new Error('Cannot register a disposable on itself!'); + } + + if (this._isDisposed) { + if (!DisposableStore.DISABLE_DISPOSED_WARNING) { + traceWarn( + new Error( + 'Trying to add a disposable to a DisposableStore that has already been disposed of. The added object will be leaked!', + ).stack, + ); + } + } else { + this._toDispose.add(o); + } + + return o; + } +} + +/** + * Abstract class for a {@link IDisposable disposable} object. + * + * Subclasses can {@linkcode _register} disposables that will be automatically cleaned up when this object is disposed of. + */ +export abstract class DisposableBase implements IDisposable { + protected readonly _store = new DisposableStore(); + + private _isDisposed = false; + + public get isDisposed(): boolean { + return this._isDisposed; + } + + constructor(...disposables: IDisposable[]) { + disposables.forEach((disposable) => this._store.add(disposable)); + } + + public dispose(): void { + this._store.dispose(); + this._isDisposed = true; + } + + /** + * Adds `o` to the collection of disposables managed by this object. + */ + public _register(o: T): T { + if (((o as unknown) as DisposableBase) === this) { + throw new Error('Cannot register a disposable on itself!'); + } + return this._store.add(o); + } +} diff --git a/src/client/common/variables/environment.ts b/src/client/common/variables/environment.ts index 81e6b8b2cfc9..9f0abd9b0ee7 100644 --- a/src/client/common/variables/environment.ts +++ b/src/client/common/variables/environment.ts @@ -1,7 +1,7 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -import { pathExistsSync, readFileSync } from 'fs-extra'; +import { pathExistsSync, readFileSync } from '../platform/fs-paths'; import { inject, injectable } from 'inversify'; import * as path from 'path'; import { traceError } from '../../logging'; diff --git a/src/client/common/variables/environmentVariablesProvider.ts b/src/client/common/variables/environmentVariablesProvider.ts index 2524aac21017..14573d2204aa 100644 --- a/src/client/common/variables/environmentVariablesProvider.ts +++ b/src/client/common/variables/environmentVariablesProvider.ts @@ -1,7 +1,7 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -import { inject, injectable, optional } from 'inversify'; +import { inject, injectable } from 'inversify'; import * as path from 'path'; import { ConfigurationChangeEvent, Disposable, Event, EventEmitter, FileSystemWatcher, Uri } from 'vscode'; import { traceError, traceVerbose } from '../../logging'; @@ -33,7 +33,7 @@ export class EnvironmentVariablesProvider implements IEnvironmentVariablesProvid @inject(IPlatformService) private platformService: IPlatformService, @inject(IWorkspaceService) private workspaceService: IWorkspaceService, @inject(ICurrentProcess) private process: ICurrentProcess, - @optional() private cacheDuration: number = CACHE_DURATION, + private cacheDuration: number = CACHE_DURATION, ) { disposableRegistry.push(this); this.changeEventEmitter = new EventEmitter(); diff --git a/src/client/common/variables/systemVariables.ts b/src/client/common/variables/systemVariables.ts index eb318b2f4915..05e5d9d6f584 100644 --- a/src/client/common/variables/systemVariables.ts +++ b/src/client/common/variables/systemVariables.ts @@ -132,6 +132,8 @@ export class SystemVariables extends AbstractSystemVariables { const basename = Path.basename(folder.uri.fsPath); ((this as any) as Record)[`workspaceFolder:${basename}`] = folder.uri.fsPath; + ((this as any) as Record)[`workspaceFolder:${folder.name}`] = + folder.uri.fsPath; }); } catch { // This try...catch block is here to support pre-existing tests, ignore error. diff --git a/src/client/common/vscodeApis/commandApis.ts b/src/client/common/vscodeApis/commandApis.ts index 580760e106e1..908cb761c538 100644 --- a/src/client/common/vscodeApis/commandApis.ts +++ b/src/client/common/vscodeApis/commandApis.ts @@ -3,13 +3,16 @@ import { commands, Disposable } from 'vscode'; -/* eslint-disable @typescript-eslint/no-explicit-any */ -/* eslint-disable @typescript-eslint/explicit-module-boundary-types */ +/** + * Wrapper for vscode.commands.executeCommand to make it easier to mock in tests + */ +export function executeCommand(command: string, ...rest: any[]): Thenable { + return commands.executeCommand(command, ...rest); +} +/** + * Wrapper for vscode.commands.registerCommand to make it easier to mock in tests + */ export function registerCommand(command: string, callback: (...args: any[]) => any, thisArg?: any): Disposable { return commands.registerCommand(command, callback, thisArg); } - -export function executeCommand(command: string, ...rest: any[]): Thenable { - return commands.executeCommand(command, ...rest); -} diff --git a/src/client/common/vscodeApis/extensionsApi.ts b/src/client/common/vscodeApis/extensionsApi.ts index ece424847a16..f099d6f636b0 100644 --- a/src/client/common/vscodeApis/extensionsApi.ts +++ b/src/client/common/vscodeApis/extensionsApi.ts @@ -2,8 +2,8 @@ // Licensed under the MIT License. import * as path from 'path'; -import * as fs from 'fs-extra'; import * as vscode from 'vscode'; +import * as fs from '../platform/fs-paths'; import { PVSC_EXTENSION_ID } from '../constants'; export function getExtension(extensionId: string): vscode.Extension | undefined { @@ -32,3 +32,8 @@ export function isExtensionDisabled(extensionId: string): boolean { export function isInsider(): boolean { return vscode.env.appName.includes('Insider'); } + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export function getExtensions(): readonly vscode.Extension[] { + return vscode.extensions.all; +} diff --git a/src/client/common/vscodeApis/languageApis.ts b/src/client/common/vscodeApis/languageApis.ts new file mode 100644 index 000000000000..87681507693d --- /dev/null +++ b/src/client/common/vscodeApis/languageApis.ts @@ -0,0 +1,12 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License + +import { DiagnosticChangeEvent, DiagnosticCollection, Disposable, languages } from 'vscode'; + +export function createDiagnosticCollection(name: string): DiagnosticCollection { + return languages.createDiagnosticCollection(name); +} + +export function onDidChangeDiagnostics(handler: (e: DiagnosticChangeEvent) => void): Disposable { + return languages.onDidChangeDiagnostics(handler); +} diff --git a/src/client/common/vscodeApis/windowApis.ts b/src/client/common/vscodeApis/windowApis.ts index 5c279b890a9f..90a06e7ed75a 100644 --- a/src/client/common/vscodeApis/windowApis.ts +++ b/src/client/common/vscodeApis/windowApis.ts @@ -16,8 +16,31 @@ import { TextEditor, window, Disposable, + QuickPickItemButtonEvent, + Uri, + TerminalShellExecutionStartEvent, + LogOutputChannel, + OutputChannel, + TerminalLinkProvider, + NotebookDocument, + NotebookEditor, + NotebookDocumentShowOptions, + Terminal, } from 'vscode'; import { createDeferred, Deferred } from '../utils/async'; +import { Resource } from '../types'; +import { getWorkspaceFolders } from './workspaceApis'; + +export function showTextDocument(uri: Uri): Thenable { + return window.showTextDocument(uri); +} + +export function showNotebookDocument( + document: NotebookDocument, + options?: NotebookDocumentShowOptions, +): Thenable { + return window.showNotebookDocument(document, options); +} export function showQuickPick( items: readonly T[] | Thenable, @@ -48,6 +71,23 @@ export function showErrorMessage(message: string, ...items: any[]): Thenable< return window.showErrorMessage(message, ...items); } +export function showWarningMessage(message: string, ...items: T[]): Thenable; +export function showWarningMessage( + message: string, + options: MessageOptions, + ...items: T[] +): Thenable; +export function showWarningMessage(message: string, ...items: T[]): Thenable; +export function showWarningMessage( + message: string, + options: MessageOptions, + ...items: T[] +): Thenable; + +export function showWarningMessage(message: string, ...items: any[]): Thenable { + return window.showWarningMessage(message, ...items); +} + export function showInformationMessage(message: string, ...items: T[]): Thenable; export function showInformationMessage( message: string, @@ -77,6 +117,18 @@ export function getActiveTextEditor(): TextEditor | undefined { return activeTextEditor; } +export function onDidChangeActiveTextEditor(handler: (e: TextEditor | undefined) => void): Disposable { + return window.onDidChangeActiveTextEditor(handler); +} + +export function onDidStartTerminalShellExecution(handler: (e: TerminalShellExecutionStartEvent) => void): Disposable { + return window.onDidStartTerminalShellExecution(handler); +} + +export function onDidChangeTerminalState(handler: (e: Terminal) => void): Disposable { + return window.onDidChangeTerminalState(handler); +} + export enum MultiStepAction { Back = 'Back', Cancel = 'Cancel', @@ -87,6 +139,7 @@ export async function showQuickPickWithBack( items: readonly T[], options?: QuickPickOptions, token?: CancellationToken, + itemButtonHandler?: (e: QuickPickItemButtonEvent) => void, ): Promise { const quickPick: QuickPick = window.createQuickPick(); const disposables: Disposable[] = [quickPick]; @@ -126,6 +179,11 @@ export async function showQuickPickWithBack( deferred.resolve(undefined); } }), + quickPick.onDidTriggerItemButton((e) => { + if (itemButtonHandler) { + itemButtonHandler(e); + } + }), ); if (token) { disposables.push( @@ -200,3 +258,23 @@ export function createStepForwardEndNode(deferred?: Deferred, result?: T): undefined, ); } + +export function getActiveResource(): Resource { + const editor = window.activeTextEditor; + if (editor && !editor.document.isUntitled) { + return editor.document.uri; + } + const workspaces = getWorkspaceFolders(); + return Array.isArray(workspaces) && workspaces.length > 0 ? workspaces[0].uri : undefined; +} + +export function createOutputChannel(name: string, languageId?: string): OutputChannel { + return window.createOutputChannel(name, languageId); +} +export function createLogOutputChannel(name: string, options: { log: true }): LogOutputChannel { + return window.createOutputChannel(name, options); +} + +export function registerTerminalLinkProvider(provider: TerminalLinkProvider): Disposable { + return window.registerTerminalLinkProvider(provider); +} diff --git a/src/client/common/vscodeApis/workspaceApis.ts b/src/client/common/vscodeApis/workspaceApis.ts index 74200ba46924..cd45f655702d 100644 --- a/src/client/common/vscodeApis/workspaceApis.ts +++ b/src/client/common/vscodeApis/workspaceApis.ts @@ -36,12 +36,12 @@ export function findFiles( return vscode.workspace.findFiles(include, exclude, maxResults, token); } -export function onDidSaveTextDocument( - listener: (e: vscode.TextDocument) => unknown, - thisArgs?: unknown, - disposables?: vscode.Disposable[], -): vscode.Disposable { - return vscode.workspace.onDidSaveTextDocument(listener, thisArgs, disposables); +export function onDidCloseTextDocument(handler: (e: vscode.TextDocument) => void): vscode.Disposable { + return vscode.workspace.onDidCloseTextDocument(handler); +} + +export function onDidSaveTextDocument(handler: (e: vscode.TextDocument) => void): vscode.Disposable { + return vscode.workspace.onDidSaveTextDocument(handler); } export function getOpenTextDocuments(): readonly vscode.TextDocument[] { @@ -55,3 +55,62 @@ export function onDidOpenTextDocument(handler: (doc: vscode.TextDocument) => voi export function onDidChangeTextDocument(handler: (e: vscode.TextDocumentChangeEvent) => void): vscode.Disposable { return vscode.workspace.onDidChangeTextDocument(handler); } + +export function onDidChangeConfiguration(handler: (e: vscode.ConfigurationChangeEvent) => void): vscode.Disposable { + return vscode.workspace.onDidChangeConfiguration(handler); +} + +export function onDidCloseNotebookDocument(handler: (e: vscode.NotebookDocument) => void): vscode.Disposable { + return vscode.workspace.onDidCloseNotebookDocument(handler); +} + +export function createFileSystemWatcher( + globPattern: vscode.GlobPattern, + ignoreCreateEvents?: boolean, + ignoreChangeEvents?: boolean, + ignoreDeleteEvents?: boolean, +): vscode.FileSystemWatcher { + return vscode.workspace.createFileSystemWatcher( + globPattern, + ignoreCreateEvents, + ignoreChangeEvents, + ignoreDeleteEvents, + ); +} + +export function onDidChangeWorkspaceFolders( + handler: (e: vscode.WorkspaceFoldersChangeEvent) => void, +): vscode.Disposable { + return vscode.workspace.onDidChangeWorkspaceFolders(handler); +} + +export function isVirtualWorkspace(): boolean { + const isVirtualWorkspace = + vscode.workspace.workspaceFolders && vscode.workspace.workspaceFolders.every((f) => f.uri.scheme !== 'file'); + return !!isVirtualWorkspace; +} + +export function isTrusted(): boolean { + return vscode.workspace.isTrusted; +} + +export function onDidGrantWorkspaceTrust(handler: () => void): vscode.Disposable { + return vscode.workspace.onDidGrantWorkspaceTrust(handler); +} + +export function createDirectory(uri: vscode.Uri): Thenable { + return vscode.workspace.fs.createDirectory(uri); +} + +export function openNotebookDocument(uri: vscode.Uri): Thenable; +export function openNotebookDocument( + notebookType: string, + content?: vscode.NotebookData, +): Thenable; +export function openNotebookDocument(notebook: any, content?: vscode.NotebookData): Thenable { + return vscode.workspace.openNotebookDocument(notebook, content); +} + +export function copy(source: vscode.Uri, dest: vscode.Uri, options?: { overwrite?: boolean }): Thenable { + return vscode.workspace.fs.copy(source, dest, options); +} diff --git a/src/client/debugger/constants.ts b/src/client/debugger/constants.ts index 08b8619ce03a..a2ac198a597d 100644 --- a/src/client/debugger/constants.ts +++ b/src/client/debugger/constants.ts @@ -4,3 +4,4 @@ 'use strict'; export const DebuggerTypeName = 'python'; +export const PythonDebuggerTypeName = 'debugpy'; diff --git a/src/client/debugger/extension/adapter/factory.ts b/src/client/debugger/extension/adapter/factory.ts index fc9232729eae..edef16368dc0 100644 --- a/src/client/debugger/extension/adapter/factory.ts +++ b/src/client/debugger/extension/adapter/factory.ts @@ -15,10 +15,8 @@ import { } from 'vscode'; import { EXTENSION_ROOT_DIR } from '../../../constants'; import { IInterpreterService } from '../../../interpreter/contracts'; -import { traceLog, traceVerbose } from '../../../logging'; +import { traceError, traceLog, traceVerbose } from '../../../logging'; import { PythonEnvironment } from '../../../pythonEnvironments/info'; -import { sendTelemetryEvent } from '../../../telemetry'; -import { EventName } from '../../../telemetry/constants'; import { AttachRequestArguments, LaunchRequestArguments } from '../../types'; import { IDebugAdapterDescriptorFactory } from '../types'; import { showErrorMessage } from '../../../common/vscodeApis/windowApis'; @@ -26,6 +24,7 @@ import { Common, Interpreters } from '../../../common/utils/localize'; import { IPersistentStateFactory } from '../../../common/types'; import { Commands } from '../../../common/constants'; import { ICommandManager } from '../../../common/application/types'; +import { getDebugpyPath } from '../../pythonDebugger'; // persistent state names, exported to make use of in testing export enum debugStateKeys { @@ -75,10 +74,6 @@ export class DebugAdapterDescriptorFactory implements IDebugAdapterDescriptorFac const command = await this.getDebugAdapterPython(configuration, session.workspaceFolder); if (command.length !== 0) { - if (configuration.request === 'attach' && configuration.processId !== undefined) { - sendTelemetryEvent(EventName.DEBUGGER_ATTACH_TO_LOCAL_PROCESS); - } - const executable = command.shift() ?? 'python'; // "logToFile" is not handled directly by the adapter - instead, we need to pass @@ -90,19 +85,15 @@ export class DebugAdapterDescriptorFactory implements IDebugAdapterDescriptorFac traceLog(`DAP Server launched with command: ${executable} ${args.join(' ')}`); return new DebugAdapterExecutable(executable, args); } - - const debuggerAdapterPathToUse = path.join( - EXTENSION_ROOT_DIR, - 'pythonFiles', - 'lib', - 'python', - 'debugpy', - 'adapter', - ); + const debugpyPath = await getDebugpyPath(); + if (!debugpyPath) { + traceError('Could not find debugpy path.'); + throw new Error('Could not find debugpy path.'); + } + const debuggerAdapterPathToUse = path.join(debugpyPath, 'adapter'); const args = command.concat([debuggerAdapterPathToUse, ...logArgs]); traceLog(`DAP Server launched with command: ${executable} ${args.join(' ')}`); - sendTelemetryEvent(EventName.DEBUG_ADAPTER_USING_WHEELS_PATH, undefined, { usingWheels: true }); return new DebugAdapterExecutable(executable, args); } @@ -183,7 +174,10 @@ export class DebugAdapterDescriptorFactory implements IDebugAdapterDescriptorFac private async getExecutableCommand(interpreter: PythonEnvironment | undefined): Promise { if (interpreter) { - if ((interpreter.version?.major ?? 0) < 3 || (interpreter.version?.minor ?? 0) <= 6) { + if ( + (interpreter.version?.major ?? 0) < 3 || + ((interpreter.version?.major ?? 0) <= 3 && (interpreter.version?.minor ?? 0) <= 6) + ) { this.showDeprecatedPythonMessage(); } return interpreter.path.length > 0 ? [interpreter.path] : []; @@ -200,6 +194,6 @@ export class DebugAdapterDescriptorFactory implements IDebugAdapterDescriptorFac * @memberof DebugAdapterDescriptorFactory */ private async notifySelectInterpreter() { - await showErrorMessage(l10n.t('Please install Python or select a Python Interpreter to use the debugger.')); + await showErrorMessage(l10n.t('Install Python or select a Python Interpreter to use the debugger.')); } } diff --git a/src/client/debugger/extension/adapter/remoteLaunchers.ts b/src/client/debugger/extension/adapter/remoteLaunchers.ts index f42f101f8523..f68f747a8a8c 100644 --- a/src/client/debugger/extension/adapter/remoteLaunchers.ts +++ b/src/client/debugger/extension/adapter/remoteLaunchers.ts @@ -3,12 +3,8 @@ 'use strict'; -import * as path from 'path'; -import { EXTENSION_ROOT_DIR } from '../../../common/constants'; import '../../../common/extensions'; - -const pathToPythonLibDir = path.join(EXTENSION_ROOT_DIR, 'pythonFiles', 'lib', 'python'); -const pathToDebugger = path.join(pathToPythonLibDir, 'debugpy'); +import { getDebugpyPath } from '../../pythonDebugger'; type RemoteDebugOptions = { host: string; @@ -16,7 +12,11 @@ type RemoteDebugOptions = { waitUntilDebuggerAttaches: boolean; }; -export function getDebugpyLauncherArgs(options: RemoteDebugOptions, debuggerPath: string = pathToDebugger) { +export async function getDebugpyLauncherArgs(options: RemoteDebugOptions, debuggerPath?: string) { + if (!debuggerPath) { + debuggerPath = await getDebugpyPath(); + } + const waitArgs = options.waitUntilDebuggerAttaches ? ['--wait-for-client'] : []; return [ debuggerPath.fileToCommandArgumentForPythonExt(), @@ -25,7 +25,3 @@ export function getDebugpyLauncherArgs(options: RemoteDebugOptions, debuggerPath ...waitArgs, ]; } - -export function getDebugpyPackagePath(): string { - return pathToDebugger; -} diff --git a/src/client/debugger/extension/configuration/debugConfigurationService.ts b/src/client/debugger/extension/configuration/debugConfigurationService.ts index 80a1e3a8a8c4..9997fb4f0509 100644 --- a/src/client/debugger/extension/configuration/debugConfigurationService.ts +++ b/src/client/debugger/extension/configuration/debugConfigurationService.ts @@ -4,31 +4,13 @@ 'use strict'; import { inject, injectable, named } from 'inversify'; -import { cloneDeep } from 'lodash'; -import { CancellationToken, DebugConfiguration, QuickPickItem, WorkspaceFolder } from 'vscode'; -import { DebugConfigStrings } from '../../../common/utils/localize'; -import { - IMultiStepInputFactory, - InputStep, - IQuickPickParameters, - MultiStepInput, -} from '../../../common/utils/multiStepInput'; -import { AttachRequestArguments, DebugConfigurationArguments, LaunchRequestArguments } from '../../types'; -import { DebugConfigurationState, DebugConfigurationType, IDebugConfigurationService } from '../types'; -import { buildDjangoLaunchDebugConfiguration } from './providers/djangoLaunch'; -import { buildFastAPILaunchDebugConfiguration } from './providers/fastapiLaunch'; -import { buildFileLaunchDebugConfiguration } from './providers/fileLaunch'; -import { buildFlaskLaunchDebugConfiguration } from './providers/flaskLaunch'; -import { buildModuleLaunchConfiguration } from './providers/moduleLaunch'; -import { buildPidAttachConfiguration } from './providers/pidAttach'; -import { buildPyramidLaunchConfiguration } from './providers/pyramidLaunch'; -import { buildRemoteAttachConfiguration } from './providers/remoteAttach'; +import { CancellationToken, DebugConfiguration, WorkspaceFolder } from 'vscode'; +import { AttachRequestArguments, LaunchRequestArguments } from '../../types'; +import { IDebugConfigurationService } from '../types'; import { IDebugConfigurationResolver } from './types'; @injectable() export class PythonDebugConfigurationService implements IDebugConfigurationService { - private cacheDebugConfig: DebugConfiguration | undefined = undefined; - constructor( @inject(IDebugConfigurationResolver) @named('attach') @@ -36,26 +18,8 @@ export class PythonDebugConfigurationService implements IDebugConfigurationServi @inject(IDebugConfigurationResolver) @named('launch') private readonly launchResolver: IDebugConfigurationResolver, - @inject(IMultiStepInputFactory) private readonly multiStepFactory: IMultiStepInputFactory, ) {} - public async provideDebugConfigurations( - folder: WorkspaceFolder | undefined, - token?: CancellationToken, - ): Promise { - const config: Partial = {}; - const state = { config, folder, token }; - - // Disabled until configuration issues are addressed by VS Code. See #4007 - const multiStep = this.multiStepFactory.create(); - await multiStep.run((input, s) => PythonDebugConfigurationService.pickDebugConfiguration(input, s), state); - - if (Object.keys(state.config).length !== 0) { - return [state.config as DebugConfiguration]; - } - return undefined; - } - public async resolveDebugConfiguration( folder: WorkspaceFolder | undefined, debugConfiguration: DebugConfiguration, @@ -76,19 +40,7 @@ export class PythonDebugConfigurationService implements IDebugConfigurationServi ); } else { if (Object.keys(debugConfiguration).length === 0) { - if (this.cacheDebugConfig) { - debugConfiguration = cloneDeep(this.cacheDebugConfig); - } else { - const configs = await this.provideDebugConfigurations(folder, token); - if (configs === undefined) { - return undefined; - } - if (Array.isArray(configs) && configs.length === 1) { - // eslint-disable-next-line prefer-destructuring - debugConfiguration = configs[0]; - } - this.cacheDebugConfig = cloneDeep(debugConfiguration); - } + return undefined; } return this.launchResolver.resolveDebugConfiguration( folder, @@ -108,88 +60,4 @@ export class PythonDebugConfigurationService implements IDebugConfigurationServi } return debugConfiguration.request === 'attach' ? resolve(this.attachResolver) : resolve(this.launchResolver); } - - // eslint-disable-next-line consistent-return - protected static async pickDebugConfiguration( - input: MultiStepInput, - state: DebugConfigurationState, - ): Promise | void> { - type DebugConfigurationQuickPickItemFunc = ( - input: MultiStepInput, - state: DebugConfigurationState, - ) => Promise>; - type DebugConfigurationQuickPickItem = QuickPickItem & { - type: DebugConfigurationType; - func: DebugConfigurationQuickPickItemFunc; - }; - const items: DebugConfigurationQuickPickItem[] = [ - { - func: buildFileLaunchDebugConfiguration, - label: DebugConfigStrings.file.selectConfiguration.label, - type: DebugConfigurationType.launchFile, - description: DebugConfigStrings.file.selectConfiguration.description, - }, - { - func: buildModuleLaunchConfiguration, - label: DebugConfigStrings.module.selectConfiguration.label, - type: DebugConfigurationType.launchModule, - description: DebugConfigStrings.module.selectConfiguration.description, - }, - { - func: buildRemoteAttachConfiguration, - label: DebugConfigStrings.attach.selectConfiguration.label, - type: DebugConfigurationType.remoteAttach, - description: DebugConfigStrings.attach.selectConfiguration.description, - }, - { - func: buildPidAttachConfiguration, - label: DebugConfigStrings.attachPid.selectConfiguration.label, - type: DebugConfigurationType.pidAttach, - description: DebugConfigStrings.attachPid.selectConfiguration.description, - }, - { - func: buildDjangoLaunchDebugConfiguration, - label: DebugConfigStrings.django.selectConfiguration.label, - type: DebugConfigurationType.launchDjango, - description: DebugConfigStrings.django.selectConfiguration.description, - }, - { - func: buildFastAPILaunchDebugConfiguration, - label: DebugConfigStrings.fastapi.selectConfiguration.label, - type: DebugConfigurationType.launchFastAPI, - description: DebugConfigStrings.fastapi.selectConfiguration.description, - }, - { - func: buildFlaskLaunchDebugConfiguration, - label: DebugConfigStrings.flask.selectConfiguration.label, - type: DebugConfigurationType.launchFlask, - description: DebugConfigStrings.flask.selectConfiguration.description, - }, - { - func: buildPyramidLaunchConfiguration, - label: DebugConfigStrings.pyramid.selectConfiguration.label, - type: DebugConfigurationType.launchPyramid, - description: DebugConfigStrings.pyramid.selectConfiguration.description, - }, - ]; - const debugConfigurations = new Map(); - for (const config of items) { - debugConfigurations.set(config.type, config.func); - } - - state.config = {}; - const pick = await input.showQuickPick< - DebugConfigurationQuickPickItem, - IQuickPickParameters - >({ - title: DebugConfigStrings.selectConfiguration.title, - placeholder: DebugConfigStrings.selectConfiguration.placeholder, - activeItem: items[0], - items, - }); - if (pick) { - const pickedDebugConfiguration = debugConfigurations.get(pick.type)!; - return pickedDebugConfiguration(input, state); - } - } } diff --git a/src/client/debugger/extension/configuration/dynamicdebugConfigurationService.ts b/src/client/debugger/extension/configuration/dynamicdebugConfigurationService.ts deleted file mode 100644 index 2d80f0e3d6e8..000000000000 --- a/src/client/debugger/extension/configuration/dynamicdebugConfigurationService.ts +++ /dev/null @@ -1,134 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; - -import * as path from 'path'; -import * as fs from 'fs-extra'; -import { injectable } from 'inversify'; -import { CancellationToken, DebugConfiguration, WorkspaceFolder } from 'vscode'; -import { IDynamicDebugConfigurationService } from '../types'; -import { DebuggerTypeName } from '../../constants'; -import { asyncFilter } from '../../../common/utils/arrayUtils'; -import { replaceAll } from '../../../common/stringUtils'; - -const workspaceFolderToken = '${workspaceFolder}'; - -@injectable() -export class DynamicPythonDebugConfigurationService implements IDynamicDebugConfigurationService { - // eslint-disable-next-line class-methods-use-this - public async provideDebugConfigurations( - folder: WorkspaceFolder, - _token?: CancellationToken, - ): Promise { - const providers = []; - - providers.push({ - name: 'Python: File', - type: DebuggerTypeName, - request: 'launch', - program: '${file}', - justMyCode: true, - }); - - const djangoManagePath = await DynamicPythonDebugConfigurationService.getDjangoPath(folder); - if (djangoManagePath) { - providers.push({ - name: 'Python: Django', - type: DebuggerTypeName, - request: 'launch', - program: `${workspaceFolderToken}${path.sep}${djangoManagePath}`, - args: ['runserver'], - django: true, - justMyCode: true, - }); - } - - const flaskPath = await DynamicPythonDebugConfigurationService.getFlaskPath(folder); - if (flaskPath) { - providers.push({ - name: 'Python: Flask', - type: DebuggerTypeName, - request: 'launch', - module: 'flask', - env: { - FLASK_APP: path.relative(folder.uri.fsPath, flaskPath), - FLASK_DEBUG: '1', - }, - args: ['run', '--no-debugger', '--no-reload'], - jinja: true, - justMyCode: true, - }); - } - - let fastApiPath = await DynamicPythonDebugConfigurationService.getFastApiPath(folder); - if (fastApiPath) { - fastApiPath = replaceAll(path.relative(folder.uri.fsPath, fastApiPath), path.sep, '.').replace('.py', ''); - providers.push({ - name: 'Python: FastAPI', - type: DebuggerTypeName, - request: 'launch', - module: 'uvicorn', - args: [`${fastApiPath}:app`], - jinja: true, - justMyCode: true, - }); - } - - return providers; - } - - private static async getDjangoPath(folder: WorkspaceFolder) { - const regExpression = /execute_from_command_line\(/; - const possiblePaths = await DynamicPythonDebugConfigurationService.getPossiblePaths( - folder, - ['manage.py', '*/manage.py', 'app.py', '*/app.py'], - regExpression, - ); - return possiblePaths.length ? path.relative(folder.uri.fsPath, possiblePaths[0]) : null; - } - - private static async getFastApiPath(folder: WorkspaceFolder) { - const regExpression = /app\s*=\s*FastAPI\(/; - const fastApiPaths = await DynamicPythonDebugConfigurationService.getPossiblePaths( - folder, - ['main.py', 'app.py', '*/main.py', '*/app.py', '*/*/main.py', '*/*/app.py'], - regExpression, - ); - - return fastApiPaths.length ? fastApiPaths[0] : null; - } - - private static async getFlaskPath(folder: WorkspaceFolder) { - const regExpression = /app(?:lication)?\s*=\s*(?:flask\.)?Flask\(|def\s+(?:create|make)_app\(/; - const flaskPaths = await DynamicPythonDebugConfigurationService.getPossiblePaths( - folder, - ['__init__.py', 'app.py', 'wsgi.py', '*/__init__.py', '*/app.py', '*/wsgi.py'], - regExpression, - ); - - return flaskPaths.length ? flaskPaths[0] : null; - } - - private static async getPossiblePaths( - folder: WorkspaceFolder, - globPatterns: string[], - regex: RegExp, - ): Promise { - const foundPathsPromises = (await Promise.allSettled( - globPatterns.map( - async (pattern): Promise => - (await fs.pathExists(path.join(folder.uri.fsPath, pattern))) - ? [path.join(folder.uri.fsPath, pattern)] - : [], - ), - )) as { status: string; value: [] }[]; - const possiblePaths: string[] = []; - foundPathsPromises.forEach((result) => possiblePaths.push(...result.value)); - const finalPaths = await asyncFilter(possiblePaths, async (possiblePath) => - regex.exec((await fs.readFile(possiblePath)).toString()), - ); - - return finalPaths; - } -} diff --git a/src/client/debugger/extension/configuration/launch.json/completionProvider.ts b/src/client/debugger/extension/configuration/launch.json/completionProvider.ts deleted file mode 100644 index c3b243fe9065..000000000000 --- a/src/client/debugger/extension/configuration/launch.json/completionProvider.ts +++ /dev/null @@ -1,83 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; - -import { inject, injectable } from 'inversify'; -import { getLocation } from 'jsonc-parser'; -import * as path from 'path'; -import { - CancellationToken, - CompletionItem, - CompletionItemKind, - CompletionItemProvider, - Position, - SnippetString, - TextDocument, -} from 'vscode'; -import { IExtensionSingleActivationService } from '../../../../activation/types'; -import { ILanguageService } from '../../../../common/application/types'; -import { IDisposableRegistry } from '../../../../common/types'; -import { DebugConfigStrings } from '../../../../common/utils/localize'; - -const configurationNodeName = 'configurations'; -enum JsonLanguages { - json = 'json', - jsonWithComments = 'jsonc', -} - -@injectable() -export class LaunchJsonCompletionProvider implements CompletionItemProvider, IExtensionSingleActivationService { - public readonly supportedWorkspaceTypes = { untrustedWorkspace: false, virtualWorkspace: false }; - - constructor( - @inject(ILanguageService) private readonly languageService: ILanguageService, - @inject(IDisposableRegistry) private readonly disposableRegistry: IDisposableRegistry, - ) {} - - public async activate(): Promise { - this.disposableRegistry.push( - this.languageService.registerCompletionItemProvider({ language: JsonLanguages.json }, this), - ); - this.disposableRegistry.push( - this.languageService.registerCompletionItemProvider({ language: JsonLanguages.jsonWithComments }, this), - ); - } - - // eslint-disable-next-line class-methods-use-this - public async provideCompletionItems( - document: TextDocument, - position: Position, - token: CancellationToken, - ): Promise { - if (!LaunchJsonCompletionProvider.canProvideCompletions(document, position)) { - return []; - } - - return [ - { - command: { - command: 'python.SelectAndInsertDebugConfiguration', - title: DebugConfigStrings.launchJsonCompletions.description, - arguments: [document, position, token], - }, - documentation: DebugConfigStrings.launchJsonCompletions.description, - sortText: 'AAAA', - preselect: true, - kind: CompletionItemKind.Enum, - label: DebugConfigStrings.launchJsonCompletions.label, - insertText: new SnippetString(), - }, - ]; - } - - public static canProvideCompletions(document: TextDocument, position: Position): boolean { - if (path.basename(document.uri.fsPath) !== 'launch.json') { - return false; - } - const location = getLocation(document.getText(), document.offsetAt(position)); - // Cursor must be inside the configurations array and not in any nested items. - // Hence path[0] = array, path[1] = array element index. - return location.path[0] === configurationNodeName && location.path.length === 2; - } -} diff --git a/src/client/debugger/extension/configuration/launch.json/launchJsonReader.ts b/src/client/debugger/extension/configuration/launch.json/launchJsonReader.ts index 789dda510e37..d5857638821a 100644 --- a/src/client/debugger/extension/configuration/launch.json/launchJsonReader.ts +++ b/src/client/debugger/extension/configuration/launch.json/launchJsonReader.ts @@ -2,16 +2,22 @@ // Licensed under the MIT License. import * as path from 'path'; -import * as fs from 'fs-extra'; import { parse } from 'jsonc-parser'; import { DebugConfiguration, Uri, WorkspaceFolder } from 'vscode'; -import { getWorkspaceFolder } from '../../../../common/vscodeApis/workspaceApis'; +import * as fs from '../../../../common/platform/fs-paths'; +import { getConfiguration, getWorkspaceFolder } from '../../../../common/vscodeApis/workspaceApis'; +import { traceLog } from '../../../../logging'; export async function getConfigurationsForWorkspace(workspace: WorkspaceFolder): Promise { const filename = path.join(workspace.uri.fsPath, '.vscode', 'launch.json'); - if (!(await fs.pathExists(filename))) { - return []; + // Check launch config in the workspace file + const codeWorkspaceConfig = getConfiguration('launch', workspace); + if (!codeWorkspaceConfig.configurations || !Array.isArray(codeWorkspaceConfig.configurations)) { + return []; + } + traceLog('Using configuration in workspace'); + return codeWorkspaceConfig.configurations; } const text = await fs.readFile(filename, 'utf-8'); @@ -23,6 +29,7 @@ export async function getConfigurationsForWorkspace(workspace: WorkspaceFolder): throw Error('Missing field in launch.json: version'); } // We do not bother ensuring each item is a DebugConfiguration... + traceLog('Using configuration in launch.json'); return parsed.configurations; } diff --git a/src/client/debugger/extension/configuration/launch.json/updaterService.ts b/src/client/debugger/extension/configuration/launch.json/updaterService.ts deleted file mode 100644 index b95749040f3c..000000000000 --- a/src/client/debugger/extension/configuration/launch.json/updaterService.ts +++ /dev/null @@ -1,28 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; - -import { inject, injectable } from 'inversify'; -import { IExtensionSingleActivationService } from '../../../../activation/types'; -import { IDisposableRegistry } from '../../../../common/types'; -import { registerCommand } from '../../../../common/vscodeApis/commandApis'; -import { IDebugConfigurationService } from '../../types'; -import { LaunchJsonUpdaterServiceHelper } from './updaterServiceHelper'; - -@injectable() -export class LaunchJsonUpdaterService implements IExtensionSingleActivationService { - public readonly supportedWorkspaceTypes = { untrustedWorkspace: false, virtualWorkspace: false }; - - constructor( - @inject(IDisposableRegistry) private readonly disposableRegistry: IDisposableRegistry, - @inject(IDebugConfigurationService) private readonly configurationProvider: IDebugConfigurationService, - ) {} - - public async activate(): Promise { - const handler = new LaunchJsonUpdaterServiceHelper(this.configurationProvider); - this.disposableRegistry.push( - registerCommand('python.SelectAndInsertDebugConfiguration', handler.selectAndInsertDebugConfig, handler), - ); - } -} diff --git a/src/client/debugger/extension/configuration/launch.json/updaterServiceHelper.ts b/src/client/debugger/extension/configuration/launch.json/updaterServiceHelper.ts deleted file mode 100644 index bc0820fa188f..000000000000 --- a/src/client/debugger/extension/configuration/launch.json/updaterServiceHelper.ts +++ /dev/null @@ -1,151 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; - -import { createScanner, parse, SyntaxKind } from 'jsonc-parser'; -import { CancellationToken, DebugConfiguration, Position, Range, TextDocument, WorkspaceEdit } from 'vscode'; -import { noop } from '../../../../common/utils/misc'; -import { executeCommand } from '../../../../common/vscodeApis/commandApis'; -import { getActiveTextEditor } from '../../../../common/vscodeApis/windowApis'; -import { applyEdit, getWorkspaceFolder } from '../../../../common/vscodeApis/workspaceApis'; -import { captureTelemetry } from '../../../../telemetry'; -import { EventName } from '../../../../telemetry/constants'; -import { IDebugConfigurationService } from '../../types'; - -type PositionOfCursor = 'InsideEmptyArray' | 'BeforeItem' | 'AfterItem'; -type PositionOfComma = 'BeforeCursor'; - -export class LaunchJsonUpdaterServiceHelper { - constructor(private readonly configurationProvider: IDebugConfigurationService) {} - - @captureTelemetry(EventName.DEBUGGER_CONFIGURATION_PROMPTS_IN_LAUNCH_JSON) - public async selectAndInsertDebugConfig( - document: TextDocument, - position: Position, - token: CancellationToken, - ): Promise { - const activeTextEditor = getActiveTextEditor(); - if (activeTextEditor && activeTextEditor.document === document) { - const folder = getWorkspaceFolder(document.uri); - const configs = await this.configurationProvider.provideDebugConfigurations!(folder, token); - - if (!token.isCancellationRequested && Array.isArray(configs) && configs.length > 0) { - // Always use the first available debug configuration. - await LaunchJsonUpdaterServiceHelper.insertDebugConfiguration(document, position, configs[0]); - } - } - } - - /** - * Inserts the debug configuration into the document. - * Invokes the document formatter to ensure JSON is formatted nicely. - * @param {TextDocument} document - * @param {Position} position - * @param {DebugConfiguration} config - * @returns {Promise} - * @memberof LaunchJsonCompletionItemProvider - */ - public static async insertDebugConfiguration( - document: TextDocument, - position: Position, - config: DebugConfiguration, - ): Promise { - const cursorPosition = LaunchJsonUpdaterServiceHelper.getCursorPositionInConfigurationsArray( - document, - position, - ); - if (!cursorPosition) { - return; - } - const commaPosition = LaunchJsonUpdaterServiceHelper.isCommaImmediatelyBeforeCursor(document, position) - ? 'BeforeCursor' - : undefined; - const formattedJson = LaunchJsonUpdaterServiceHelper.getTextForInsertion(config, cursorPosition, commaPosition); - const workspaceEdit = new WorkspaceEdit(); - workspaceEdit.insert(document.uri, position, formattedJson); - await applyEdit(workspaceEdit); - executeCommand('editor.action.formatDocument').then(noop, noop); - } - - /** - * Gets the string representation of the debug config for insertion in the document. - * Adds necessary leading or trailing commas (remember the text is added into an array). - * @param {DebugConfiguration} config - * @param {PositionOfCursor} cursorPosition - * @param {PositionOfComma} [commaPosition] - * @returns - * @memberof LaunchJsonCompletionItemProvider - */ - public static getTextForInsertion( - config: DebugConfiguration, - cursorPosition: PositionOfCursor, - commaPosition?: PositionOfComma, - ): string { - const json = JSON.stringify(config); - if (cursorPosition === 'AfterItem') { - // If we already have a comma immediatley before the cursor, then no need of adding a comma. - return commaPosition === 'BeforeCursor' ? json : `,${json}`; - } - if (cursorPosition === 'BeforeItem') { - return `${json},`; - } - return json; - } - - public static getCursorPositionInConfigurationsArray( - document: TextDocument, - position: Position, - ): PositionOfCursor | undefined { - if (LaunchJsonUpdaterServiceHelper.isConfigurationArrayEmpty(document)) { - return 'InsideEmptyArray'; - } - const scanner = createScanner(document.getText(), true); - scanner.setPosition(document.offsetAt(position)); - const nextToken = scanner.scan(); - if (nextToken === SyntaxKind.CommaToken || nextToken === SyntaxKind.CloseBracketToken) { - return 'AfterItem'; - } - if (nextToken === SyntaxKind.OpenBraceToken) { - return 'BeforeItem'; - } - return undefined; - } - - public static isConfigurationArrayEmpty(document: TextDocument): boolean { - const configuration = parse(document.getText(), [], { allowTrailingComma: true, disallowComments: false }) as { - configurations: []; - }; - return ( - !configuration || !Array.isArray(configuration.configurations) || configuration.configurations.length === 0 - ); - } - - public static isCommaImmediatelyBeforeCursor(document: TextDocument, position: Position): boolean { - const line = document.lineAt(position.line); - // Get text from start of line until the cursor. - const currentLine = document.getText(new Range(line.range.start, position)); - if (currentLine.trim().endsWith(',')) { - return true; - } - // If there are other characters, then don't bother. - if (currentLine.trim().length !== 0) { - return false; - } - - // Keep walking backwards until we hit a non-comma character or a comm character. - let startLineNumber = position.line - 1; - while (startLineNumber > 0) { - const lineText = document.lineAt(startLineNumber).text; - if (lineText.trim().endsWith(',')) { - return true; - } - // If there are other characters, then don't bother. - if (lineText.trim().length !== 0) { - return false; - } - startLineNumber -= 1; - } - return false; - } -} diff --git a/src/client/debugger/extension/configuration/providers/djangoLaunch.ts b/src/client/debugger/extension/configuration/providers/djangoLaunch.ts deleted file mode 100644 index 4e1513ccb1ea..000000000000 --- a/src/client/debugger/extension/configuration/providers/djangoLaunch.ts +++ /dev/null @@ -1,88 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; - -import * as vscode from 'vscode'; -import * as path from 'path'; -import * as fs from 'fs-extra'; -import { DebugConfigStrings } from '../../../../common/utils/localize'; -import { MultiStepInput } from '../../../../common/utils/multiStepInput'; -import { sendTelemetryEvent } from '../../../../telemetry'; -import { EventName } from '../../../../telemetry/constants'; -import { DebuggerTypeName } from '../../../constants'; -import { LaunchRequestArguments } from '../../../types'; -import { DebugConfigurationState, DebugConfigurationType } from '../../types'; -import { resolveVariables } from '../utils/common'; - -const workspaceFolderToken = '${workspaceFolder}'; - -export async function buildDjangoLaunchDebugConfiguration( - input: MultiStepInput, - state: DebugConfigurationState, -): Promise { - const program = await getManagePyPath(state.folder); - let manuallyEnteredAValue: boolean | undefined; - const defaultProgram = `${workspaceFolderToken}${path.sep}manage.py`; - const config: Partial = { - name: DebugConfigStrings.django.snippet.name, - type: DebuggerTypeName, - request: 'launch', - program: program || defaultProgram, - args: ['runserver'], - django: true, - justMyCode: true, - }; - if (!program) { - const selectedProgram = await input.showInputBox({ - title: DebugConfigStrings.django.enterManagePyPath.title, - value: defaultProgram, - prompt: DebugConfigStrings.django.enterManagePyPath.prompt, - validate: (value) => validateManagePy(state.folder, defaultProgram, value), - }); - if (selectedProgram) { - manuallyEnteredAValue = true; - config.program = selectedProgram; - } - } - - sendTelemetryEvent(EventName.DEBUGGER_CONFIGURATION_PROMPTS, undefined, { - configurationType: DebugConfigurationType.launchDjango, - autoDetectedDjangoManagePyPath: !!program, - manuallyEnteredAValue, - }); - - Object.assign(state.config, config); -} - -export async function validateManagePy( - folder: vscode.WorkspaceFolder | undefined, - defaultValue: string, - selected?: string, -): Promise { - const error = DebugConfigStrings.django.enterManagePyPath.invalid; - if (!selected || selected.trim().length === 0) { - return error; - } - const resolvedPath = resolveVariables(selected, undefined, folder); - if (resolvedPath) { - if (selected !== defaultValue && !(await fs.pathExists(resolvedPath))) { - return error; - } - if (!resolvedPath.trim().toLowerCase().endsWith('.py')) { - return error; - } - } - return undefined; -} - -export async function getManagePyPath(folder: vscode.WorkspaceFolder | undefined): Promise { - if (!folder) { - return undefined; - } - const defaultLocationOfManagePy = path.join(folder.uri.fsPath, 'manage.py'); - if (await fs.pathExists(defaultLocationOfManagePy)) { - return `${workspaceFolderToken}${path.sep}manage.py`; - } - return undefined; -} diff --git a/src/client/debugger/extension/configuration/providers/fastapiLaunch.ts b/src/client/debugger/extension/configuration/providers/fastapiLaunch.ts deleted file mode 100644 index 25aaf3d25c08..000000000000 --- a/src/client/debugger/extension/configuration/providers/fastapiLaunch.ts +++ /dev/null @@ -1,67 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; - -import * as path from 'path'; -import * as fs from 'fs-extra'; -import { WorkspaceFolder } from 'vscode'; -import { DebugConfigStrings } from '../../../../common/utils/localize'; -import { MultiStepInput } from '../../../../common/utils/multiStepInput'; -import { sendTelemetryEvent } from '../../../../telemetry'; -import { EventName } from '../../../../telemetry/constants'; -import { DebuggerTypeName } from '../../../constants'; -import { LaunchRequestArguments } from '../../../types'; -import { DebugConfigurationState, DebugConfigurationType } from '../../types'; - -export async function buildFastAPILaunchDebugConfiguration( - input: MultiStepInput, - state: DebugConfigurationState, -): Promise { - const application = await getApplicationPath(state.folder); - let manuallyEnteredAValue: boolean | undefined; - const config: Partial = { - name: DebugConfigStrings.fastapi.snippet.name, - type: DebuggerTypeName, - request: 'launch', - module: 'uvicorn', - args: ['main:app'], - jinja: true, - justMyCode: true, - }; - - if (!application) { - const selectedPath = await input.showInputBox({ - title: DebugConfigStrings.fastapi.enterAppPathOrNamePath.title, - value: 'main.py', - prompt: DebugConfigStrings.fastapi.enterAppPathOrNamePath.prompt, - validate: (value) => - Promise.resolve( - value && value.trim().length > 0 - ? undefined - : DebugConfigStrings.fastapi.enterAppPathOrNamePath.invalid, - ), - }); - if (selectedPath) { - manuallyEnteredAValue = true; - config.args = [`${path.basename(selectedPath, '.py').replace('/', '.')}:app`]; - } - } - - sendTelemetryEvent(EventName.DEBUGGER_CONFIGURATION_PROMPTS, undefined, { - configurationType: DebugConfigurationType.launchFastAPI, - autoDetectedFastAPIMainPyPath: !!application, - manuallyEnteredAValue, - }); - Object.assign(state.config, config); -} -export async function getApplicationPath(folder: WorkspaceFolder | undefined): Promise { - if (!folder) { - return undefined; - } - const defaultLocationOfManagePy = path.join(folder.uri.fsPath, 'main.py'); - if (await fs.pathExists(defaultLocationOfManagePy)) { - return 'main.py'; - } - return undefined; -} diff --git a/src/client/debugger/extension/configuration/providers/fileLaunch.ts b/src/client/debugger/extension/configuration/providers/fileLaunch.ts deleted file mode 100644 index edda7ed7e22d..000000000000 --- a/src/client/debugger/extension/configuration/providers/fileLaunch.ts +++ /dev/null @@ -1,30 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; - -import { DebugConfigStrings } from '../../../../common/utils/localize'; -import { MultiStepInput } from '../../../../common/utils/multiStepInput'; -import { sendTelemetryEvent } from '../../../../telemetry'; -import { EventName } from '../../../../telemetry/constants'; -import { DebuggerTypeName } from '../../../constants'; -import { LaunchRequestArguments } from '../../../types'; -import { DebugConfigurationState, DebugConfigurationType } from '../../types'; - -export async function buildFileLaunchDebugConfiguration( - _input: MultiStepInput, - state: DebugConfigurationState, -): Promise { - const config: Partial = { - name: DebugConfigStrings.file.snippet.name, - type: DebuggerTypeName, - request: 'launch', - program: '${file}', - console: 'integratedTerminal', - justMyCode: true, - }; - sendTelemetryEvent(EventName.DEBUGGER_CONFIGURATION_PROMPTS, undefined, { - configurationType: DebugConfigurationType.launchFile, - }); - Object.assign(state.config, config); -} diff --git a/src/client/debugger/extension/configuration/providers/flaskLaunch.ts b/src/client/debugger/extension/configuration/providers/flaskLaunch.ts deleted file mode 100644 index d85258c800c6..000000000000 --- a/src/client/debugger/extension/configuration/providers/flaskLaunch.ts +++ /dev/null @@ -1,71 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; - -import * as path from 'path'; -import * as fs from 'fs-extra'; -import { WorkspaceFolder } from 'vscode'; -import { DebugConfigStrings } from '../../../../common/utils/localize'; -import { MultiStepInput } from '../../../../common/utils/multiStepInput'; -import { sendTelemetryEvent } from '../../../../telemetry'; -import { EventName } from '../../../../telemetry/constants'; -import { DebuggerTypeName } from '../../../constants'; -import { LaunchRequestArguments } from '../../../types'; -import { DebugConfigurationState, DebugConfigurationType } from '../../types'; - -export async function buildFlaskLaunchDebugConfiguration( - input: MultiStepInput, - state: DebugConfigurationState, -): Promise { - const application = await getApplicationPath(state.folder); - let manuallyEnteredAValue: boolean | undefined; - const config: Partial = { - name: DebugConfigStrings.flask.snippet.name, - type: DebuggerTypeName, - request: 'launch', - module: 'flask', - env: { - FLASK_APP: application || 'app.py', - FLASK_DEBUG: '1', - }, - args: ['run', '--no-debugger', '--no-reload'], - jinja: true, - justMyCode: true, - }; - - if (!application) { - const selectedApp = await input.showInputBox({ - title: DebugConfigStrings.flask.enterAppPathOrNamePath.title, - value: 'app.py', - prompt: DebugConfigStrings.flask.enterAppPathOrNamePath.prompt, - validate: (value) => - Promise.resolve( - value && value.trim().length > 0 - ? undefined - : DebugConfigStrings.flask.enterAppPathOrNamePath.invalid, - ), - }); - if (selectedApp) { - manuallyEnteredAValue = true; - config.env!.FLASK_APP = selectedApp; - } - } - - sendTelemetryEvent(EventName.DEBUGGER_CONFIGURATION_PROMPTS, undefined, { - configurationType: DebugConfigurationType.launchFlask, - autoDetectedFlaskAppPyPath: !!application, - manuallyEnteredAValue, - }); - Object.assign(state.config, config); -} -export async function getApplicationPath(folder: WorkspaceFolder | undefined): Promise { - if (!folder) { - return undefined; - } - const defaultLocationOfManagePy = path.join(folder.uri.fsPath, 'app.py'); - if (await fs.pathExists(defaultLocationOfManagePy)) { - return 'app.py'; - } - return undefined; -} diff --git a/src/client/debugger/extension/configuration/providers/moduleLaunch.ts b/src/client/debugger/extension/configuration/providers/moduleLaunch.ts deleted file mode 100644 index 16787296ce7c..000000000000 --- a/src/client/debugger/extension/configuration/providers/moduleLaunch.ts +++ /dev/null @@ -1,45 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; - -import { DebugConfigStrings } from '../../../../common/utils/localize'; -import { MultiStepInput } from '../../../../common/utils/multiStepInput'; -import { sendTelemetryEvent } from '../../../../telemetry'; -import { EventName } from '../../../../telemetry/constants'; -import { DebuggerTypeName } from '../../../constants'; -import { LaunchRequestArguments } from '../../../types'; -import { DebugConfigurationState, DebugConfigurationType } from '../../types'; - -export async function buildModuleLaunchConfiguration( - input: MultiStepInput, - state: DebugConfigurationState, -): Promise { - let manuallyEnteredAValue: boolean | undefined; - const config: Partial = { - name: DebugConfigStrings.module.snippet.name, - type: DebuggerTypeName, - request: 'launch', - module: DebugConfigStrings.module.snippet.default, - justMyCode: true, - }; - const selectedModule = await input.showInputBox({ - title: DebugConfigStrings.module.enterModule.title, - value: config.module || DebugConfigStrings.module.enterModule.default, - prompt: DebugConfigStrings.module.enterModule.prompt, - validate: (value) => - Promise.resolve( - value && value.trim().length > 0 ? undefined : DebugConfigStrings.module.enterModule.invalid, - ), - }); - if (selectedModule) { - manuallyEnteredAValue = true; - config.module = selectedModule; - } - - sendTelemetryEvent(EventName.DEBUGGER_CONFIGURATION_PROMPTS, undefined, { - configurationType: DebugConfigurationType.launchModule, - manuallyEnteredAValue, - }); - Object.assign(state.config, config); -} diff --git a/src/client/debugger/extension/configuration/providers/pidAttach.ts b/src/client/debugger/extension/configuration/providers/pidAttach.ts deleted file mode 100644 index fc0d66874470..000000000000 --- a/src/client/debugger/extension/configuration/providers/pidAttach.ts +++ /dev/null @@ -1,29 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; - -import { DebugConfigStrings } from '../../../../common/utils/localize'; -import { MultiStepInput } from '../../../../common/utils/multiStepInput'; -import { sendTelemetryEvent } from '../../../../telemetry'; -import { EventName } from '../../../../telemetry/constants'; -import { DebuggerTypeName } from '../../../constants'; -import { AttachRequestArguments } from '../../../types'; -import { DebugConfigurationState, DebugConfigurationType } from '../../types'; - -export async function buildPidAttachConfiguration( - _input: MultiStepInput, - state: DebugConfigurationState, -): Promise { - const config: Partial = { - name: DebugConfigStrings.attachPid.snippet.name, - type: DebuggerTypeName, - request: 'attach', - processId: '${command:pickProcess}', - justMyCode: true, - }; - sendTelemetryEvent(EventName.DEBUGGER_CONFIGURATION_PROMPTS, undefined, { - configurationType: DebugConfigurationType.pidAttach, - }); - Object.assign(state.config, config); -} diff --git a/src/client/debugger/extension/configuration/providers/pyramidLaunch.ts b/src/client/debugger/extension/configuration/providers/pyramidLaunch.ts deleted file mode 100644 index 315e204e7bf8..000000000000 --- a/src/client/debugger/extension/configuration/providers/pyramidLaunch.ts +++ /dev/null @@ -1,96 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; - -import * as path from 'path'; -import * as fs from 'fs-extra'; -import { l10n, WorkspaceFolder } from 'vscode'; -import { DebugConfigStrings } from '../../../../common/utils/localize'; -import { MultiStepInput } from '../../../../common/utils/multiStepInput'; -import { sendTelemetryEvent } from '../../../../telemetry'; -import { EventName } from '../../../../telemetry/constants'; -import { DebuggerTypeName } from '../../../constants'; -import { LaunchRequestArguments } from '../../../types'; -import { DebugConfigurationState, DebugConfigurationType } from '../../types'; -import { resolveVariables } from '../utils/common'; - -const workspaceFolderToken = '${workspaceFolder}'; - -export async function buildPyramidLaunchConfiguration( - input: MultiStepInput, - state: DebugConfigurationState, -): Promise { - const iniPath = await getDevelopmentIniPath(state.folder); - const defaultIni = `${workspaceFolderToken}${path.sep}development.ini`; - let manuallyEnteredAValue: boolean | undefined; - - const config: Partial = { - name: DebugConfigStrings.pyramid.snippet.name, - type: DebuggerTypeName, - request: 'launch', - module: 'pyramid.scripts.pserve', - args: [iniPath || defaultIni], - pyramid: true, - jinja: true, - justMyCode: true, - }; - - if (!iniPath) { - const selectedIniPath = await input.showInputBox({ - title: DebugConfigStrings.pyramid.enterDevelopmentIniPath.title, - value: defaultIni, - prompt: l10n.t( - 'Enter the path to development.ini ({0} points to the root of the current workspace folder)', - workspaceFolderToken, - ), - validate: (value) => validateIniPath(state ? state.folder : undefined, defaultIni, value), - }); - if (selectedIniPath) { - manuallyEnteredAValue = true; - config.args = [selectedIniPath]; - } - } - - sendTelemetryEvent(EventName.DEBUGGER_CONFIGURATION_PROMPTS, undefined, { - configurationType: DebugConfigurationType.launchPyramid, - autoDetectedPyramidIniPath: !!iniPath, - manuallyEnteredAValue, - }); - Object.assign(state.config, config); -} - -export async function validateIniPath( - folder: WorkspaceFolder | undefined, - defaultValue: string, - selected?: string, -): Promise { - if (!folder) { - return undefined; - } - const error = DebugConfigStrings.pyramid.enterDevelopmentIniPath.invalid; - if (!selected || selected.trim().length === 0) { - return error; - } - const resolvedPath = resolveVariables(selected, undefined, folder); - if (resolvedPath) { - if (selected !== defaultValue && !fs.pathExists(resolvedPath)) { - return error; - } - if (!resolvedPath.trim().toLowerCase().endsWith('.ini')) { - return error; - } - } - return undefined; -} - -export async function getDevelopmentIniPath(folder: WorkspaceFolder | undefined): Promise { - if (!folder) { - return undefined; - } - const defaultLocationOfManagePy = path.join(folder.uri.fsPath, 'development.ini'); - if (await fs.pathExists(defaultLocationOfManagePy)) { - return `${workspaceFolderToken}${path.sep}development.ini`; - } - return undefined; -} diff --git a/src/client/debugger/extension/configuration/providers/remoteAttach.ts b/src/client/debugger/extension/configuration/providers/remoteAttach.ts deleted file mode 100644 index a43c48b664af..000000000000 --- a/src/client/debugger/extension/configuration/providers/remoteAttach.ts +++ /dev/null @@ -1,61 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; - -import { DebugConfigStrings } from '../../../../common/utils/localize'; -import { InputStep, MultiStepInput } from '../../../../common/utils/multiStepInput'; -import { sendTelemetryEvent } from '../../../../telemetry'; -import { EventName } from '../../../../telemetry/constants'; -import { DebuggerTypeName } from '../../../constants'; -import { AttachRequestArguments } from '../../../types'; -import { DebugConfigurationState, DebugConfigurationType } from '../../types'; -import { configurePort } from '../utils/configuration'; - -const defaultHost = 'localhost'; -const defaultPort = 5678; - -export async function buildRemoteAttachConfiguration( - input: MultiStepInput, - state: DebugConfigurationState, -): Promise | void> { - const config: Partial = { - name: DebugConfigStrings.attach.snippet.name, - type: DebuggerTypeName, - request: 'attach', - connect: { - host: defaultHost, - port: defaultPort, - }, - pathMappings: [ - { - localRoot: '${workspaceFolder}', - remoteRoot: '.', - }, - ], - justMyCode: true, - }; - - const connect = config.connect!; - connect.host = await input.showInputBox({ - title: DebugConfigStrings.attach.enterRemoteHost.title, - step: 1, - totalSteps: 2, - value: connect.host || defaultHost, - prompt: DebugConfigStrings.attach.enterRemoteHost.prompt, - validate: (value) => - Promise.resolve( - value && value.trim().length > 0 ? undefined : DebugConfigStrings.attach.enterRemoteHost.invalid, - ), - }); - if (!connect.host) { - connect.host = defaultHost; - } - - sendTelemetryEvent(EventName.DEBUGGER_CONFIGURATION_PROMPTS, undefined, { - configurationType: DebugConfigurationType.remoteAttach, - manuallyEnteredAValue: connect.host !== defaultHost, - }); - Object.assign(state.config, config); - return (_) => configurePort(input, state.config); -} diff --git a/src/client/debugger/extension/configuration/resolvers/attach.ts b/src/client/debugger/extension/configuration/resolvers/attach.ts index bdc72680d861..1c232f261d03 100644 --- a/src/client/debugger/extension/configuration/resolvers/attach.ts +++ b/src/client/debugger/extension/configuration/resolvers/attach.ts @@ -43,17 +43,10 @@ export class AttachConfigurationResolver extends BaseConfigurationResolver ): boolean { return !!(debugConfiguration.module && debugConfiguration.module.toUpperCase() === 'FLASK'); } - - protected static sendTelemetry( - trigger: 'launch' | 'attach' | 'test', - debugConfiguration: Partial, - ): void { - const name = debugConfiguration.name || ''; - const moduleName = debugConfiguration.module || ''; - const telemetryProps: DebuggerTelemetry = { - trigger, - console: debugConfiguration.console, - hasEnvVars: typeof debugConfiguration.env === 'object' && Object.keys(debugConfiguration.env).length > 0, - django: !!debugConfiguration.django, - fastapi: BaseConfigurationResolver.isDebuggingFastAPI(debugConfiguration), - flask: BaseConfigurationResolver.isDebuggingFlask(debugConfiguration), - hasArgs: Array.isArray(debugConfiguration.args) && debugConfiguration.args.length > 0, - isLocalhost: BaseConfigurationResolver.isLocalHost(debugConfiguration.host), - isModule: moduleName.length > 0, - isSudo: !!debugConfiguration.sudo, - jinja: !!debugConfiguration.jinja, - pyramid: !!debugConfiguration.pyramid, - stopOnEntry: !!debugConfiguration.stopOnEntry, - showReturnValue: !!debugConfiguration.showReturnValue, - subProcess: !!debugConfiguration.subProcess, - watson: name.toLowerCase().indexOf('watson') >= 0, - pyspark: name.toLowerCase().indexOf('pyspark') >= 0, - gevent: name.toLowerCase().indexOf('gevent') >= 0, - scrapy: moduleName.toLowerCase() === 'scrapy', - }; - sendTelemetryEvent(EventName.DEBUGGER, undefined, telemetryProps); - } } diff --git a/src/client/debugger/extension/configuration/resolvers/launch.ts b/src/client/debugger/extension/configuration/resolvers/launch.ts index f48b2c19aaff..3ca38fb0f710 100644 --- a/src/client/debugger/extension/configuration/resolvers/launch.ts +++ b/src/client/debugger/extension/configuration/resolvers/launch.ts @@ -13,13 +13,19 @@ import { EnvironmentVariables } from '../../../../common/variables/types'; import { IEnvironmentActivationService } from '../../../../interpreter/activation/types'; import { IInterpreterService } from '../../../../interpreter/contracts'; import { DebuggerTypeName } from '../../../constants'; -import { DebugOptions, DebugPurpose, LaunchRequestArguments } from '../../../types'; +import { DebugOptions, LaunchRequestArguments } from '../../../types'; import { BaseConfigurationResolver } from './base'; import { getProgram, IDebugEnvironmentVariablesService } from './helper'; +import { + CreateEnvironmentCheckKind, + triggerCreateEnvironmentCheckNonBlocking, +} from '../../../../pythonEnvironments/creation/createEnvironmentTrigger'; +import { sendTelemetryEvent } from '../../../../telemetry'; +import { EventName } from '../../../../telemetry/constants'; @injectable() export class LaunchConfigurationResolver extends BaseConfigurationResolver { - private isPythonSet = false; + private isCustomPythonSet = false; constructor( @inject(IDiagnosticsService) @@ -38,7 +44,7 @@ export class LaunchConfigurationResolver extends BaseConfigurationResolver { - this.isPythonSet = debugConfiguration.python !== undefined; + this.isCustomPythonSet = debugConfiguration.python !== undefined; if ( debugConfiguration.name === undefined && debugConfiguration.type === undefined && @@ -55,6 +61,10 @@ export class LaunchConfigurationResolver extends BaseConfigurationResolver debugConfiguration.debugOptions!.indexOf(item) === pos, ); } + sendTelemetryEvent(EventName.ENVIRONMENT_CHECK_TRIGGER, undefined, { trigger: 'debug' }); + triggerCreateEnvironmentCheckNonBlocking(CreateEnvironmentCheckKind.Workspace, workspaceFolder); return debugConfiguration; } @@ -106,7 +118,9 @@ export class LaunchConfigurationResolver extends BaseConfigurationResolver 0 ? pathMappings : undefined; } - const trigger = - debugConfiguration.purpose?.includes(DebugPurpose.DebugTest) || debugConfiguration.request === 'test' - ? 'test' - : 'launch'; - LaunchConfigurationResolver.sendTelemetry(trigger, debugConfiguration); } protected async validateLaunchConfiguration( diff --git a/src/client/debugger/extension/configuration/utils/configuration.ts b/src/client/debugger/extension/configuration/utils/configuration.ts deleted file mode 100644 index 37fb500dbfdd..000000000000 --- a/src/client/debugger/extension/configuration/utils/configuration.ts +++ /dev/null @@ -1,44 +0,0 @@ -/* eslint-disable @typescript-eslint/no-explicit-any */ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -'use strict'; - -import { DebugConfigStrings } from '../../../../common/utils/localize'; -import { MultiStepInput } from '../../../../common/utils/multiStepInput'; -import { sendTelemetryEvent } from '../../../../telemetry'; -import { EventName } from '../../../../telemetry/constants'; -import { AttachRequestArguments } from '../../../types'; -import { DebugConfigurationState, DebugConfigurationType } from '../../types'; - -const defaultPort = 5678; - -export async function configurePort( - input: MultiStepInput, - config: Partial, -): Promise { - const connect = config.connect || (config.connect = {}); - const port = await input.showInputBox({ - title: DebugConfigStrings.attach.enterRemotePort.title, - step: 2, - totalSteps: 2, - value: (connect.port || defaultPort).toString(), - prompt: DebugConfigStrings.attach.enterRemotePort.prompt, - validate: (value) => - Promise.resolve( - value && /^\d+$/.test(value.trim()) ? undefined : DebugConfigStrings.attach.enterRemotePort.invalid, - ), - }); - if (port && /^\d+$/.test(port.trim())) { - connect.port = parseInt(port, 10); - } - if (!connect.port) { - connect.port = defaultPort; - } - sendTelemetryEvent(EventName.DEBUGGER_CONFIGURATION_PROMPTS, undefined, { - configurationType: DebugConfigurationType.remoteAttach, - manuallyEnteredAValue: connect.port !== defaultPort, - }); -} diff --git a/src/client/debugger/extension/debugCommands.ts b/src/client/debugger/extension/debugCommands.ts index 14a108d27793..629f8616a6d6 100644 --- a/src/client/debugger/extension/debugCommands.ts +++ b/src/client/debugger/extension/debugCommands.ts @@ -14,6 +14,10 @@ import { DebugPurpose, LaunchRequestArguments } from '../types'; import { IInterpreterService } from '../../interpreter/contracts'; import { noop } from '../../common/utils/misc'; import { getConfigurationsByUri } from './configuration/launch.json/launchJsonReader'; +import { + CreateEnvironmentCheckKind, + triggerCreateEnvironmentCheckNonBlocking, +} from '../../pythonEnvironments/creation/createEnvironmentTrigger'; @injectable() export class DebugCommands implements IExtensionSingleActivationService { @@ -29,12 +33,13 @@ export class DebugCommands implements IExtensionSingleActivationService { public activate(): Promise { this.disposables.push( this.commandManager.registerCommand(Commands.Debug_In_Terminal, async (file?: Uri) => { - sendTelemetryEvent(EventName.DEBUG_IN_TERMINAL_BUTTON); const interpreter = await this.interpreterService.getActiveInterpreter(file); if (!interpreter) { this.commandManager.executeCommand(Commands.TriggerEnvironmentSelection, file).then(noop, noop); return; } + sendTelemetryEvent(EventName.ENVIRONMENT_CHECK_TRIGGER, undefined, { trigger: 'debug-in-terminal' }); + triggerCreateEnvironmentCheckNonBlocking(CreateEnvironmentCheckKind.File, file); const config = await DebugCommands.getDebugConfiguration(file); this.debugService.startDebugging(undefined, config); }), diff --git a/src/client/debugger/extension/helpers/protocolParser.ts b/src/client/debugger/extension/helpers/protocolParser.ts deleted file mode 100644 index c0d1306a841b..000000000000 --- a/src/client/debugger/extension/helpers/protocolParser.ts +++ /dev/null @@ -1,140 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -import { EventEmitter } from 'events'; -import { injectable } from 'inversify'; -import { Readable } from 'stream'; -import { DebugProtocol } from 'vscode-debugprotocol'; -import { IProtocolParser } from '../types'; - -const PROTOCOL_START_INDENTIFIER = '\r\n\r\n'; - -type Listener = (...args: unknown[]) => void; - -/** - * Parsers the debugger Protocol messages and raises the following events: - * 1. 'data', message (for all protocol messages) - * 1. 'event_', message (for all protocol events) - * 1. 'request_', message (for all protocol requests) - * 1. 'response_', message (for all protocol responses) - * 1. '', message (for all protocol messages that are not events, requests nor responses) - * @export - * @class ProtocolParser - * @extends {EventEmitter} - * @implements {IProtocolParser} - */ -@injectable() -export class ProtocolParser implements IProtocolParser { - private rawData = Buffer.alloc(0); - - private contentLength = -1; - - private disposed = false; - - private stream?: Readable; - - private events: EventEmitter; - - constructor() { - this.events = new EventEmitter(); - } - - public dispose(): void { - if (this.stream) { - this.stream.removeListener('data', this.dataCallbackHandler); - this.stream = undefined; - } - } - - public connect(stream: Readable): void { - this.stream = stream; - stream.addListener('data', this.dataCallbackHandler); - } - - public on(event: string | symbol, listener: Listener): this { - this.events.on(event, listener); - return this; - } - - public once(event: string | symbol, listener: Listener): this { - this.events.once(event, listener); - return this; - } - - private dataCallbackHandler = (data: string | Buffer) => { - this.handleData(data as Buffer); - }; - - private dispatch(body: string): void { - const message = JSON.parse(body) as DebugProtocol.ProtocolMessage; - - switch (message.type) { - case 'event': { - const event = message as DebugProtocol.Event; - if (typeof event.event === 'string') { - this.events.emit(`${message.type}_${event.event}`, event); - } - break; - } - case 'request': { - const request = message as DebugProtocol.Request; - if (typeof request.command === 'string') { - this.events.emit(`${message.type}_${request.command}`, request); - } - break; - } - case 'response': { - const reponse = message as DebugProtocol.Response; - if (typeof reponse.command === 'string') { - this.events.emit(`${message.type}_${reponse.command}`, reponse); - } - break; - } - default: { - this.events.emit(`${message.type}`, message); - } - } - - this.events.emit('data', message); - } - - private handleData(data: Buffer): void { - if (this.disposed) { - return; - } - this.rawData = Buffer.concat([this.rawData, data]); - - // eslint-disable-next-line no-constant-condition - while (true) { - if (this.contentLength >= 0) { - if (this.rawData.length >= this.contentLength) { - const message = this.rawData.toString('utf8', 0, this.contentLength); - this.rawData = this.rawData.slice(this.contentLength); - this.contentLength = -1; - if (message.length > 0) { - this.dispatch(message); - } - // there may be more complete messages to process. - // eslint-disable-next-line no-continue - continue; - } - } else { - const idx = this.rawData.indexOf(PROTOCOL_START_INDENTIFIER); - if (idx !== -1) { - const header = this.rawData.toString('utf8', 0, idx); - const lines = header.split('\r\n'); - for (const line of lines) { - const pair = line.split(/: +/); - if (pair[0] === 'Content-Length') { - this.contentLength = +pair[1]; - } - } - this.rawData = this.rawData.slice(idx + PROTOCOL_START_INDENTIFIER.length); - // eslint-disable-next-line no-continue - continue; - } - } - break; - } - } -} diff --git a/src/client/debugger/extension/hooks/childProcessAttachHandler.ts b/src/client/debugger/extension/hooks/childProcessAttachHandler.ts index 6851e54a8723..233818e00aaf 100644 --- a/src/client/debugger/extension/hooks/childProcessAttachHandler.ts +++ b/src/client/debugger/extension/hooks/childProcessAttachHandler.ts @@ -9,13 +9,11 @@ import { swallowExceptions } from '../../../common/utils/decorators'; import { AttachRequestArguments } from '../../types'; import { DebuggerEvents } from './constants'; import { IChildProcessAttachService, IDebugSessionEventHandlers } from './types'; +import { DebuggerTypeName } from '../../constants'; /** * This class is responsible for automatically attaching the debugger to any * child processes launched. I.e. this is the class responsible for multi-proc debugging. - * @export - * @class ChildProcessAttachEventHandler - * @implements {IDebugSessionEventHandlers} */ @injectable() export class ChildProcessAttachEventHandler implements IDebugSessionEventHandlers { @@ -25,7 +23,7 @@ export class ChildProcessAttachEventHandler implements IDebugSessionEventHandler @swallowExceptions('Handle child process launch') public async handleCustomEvent(event: DebugSessionCustomEvent): Promise { - if (!event) { + if (!event || event.session.configuration.type !== DebuggerTypeName) { return; } diff --git a/src/client/debugger/extension/hooks/childProcessAttachService.ts b/src/client/debugger/extension/hooks/childProcessAttachService.ts index c6556a62eaa1..39556f94c87c 100644 --- a/src/client/debugger/extension/hooks/childProcessAttachService.ts +++ b/src/client/debugger/extension/hooks/childProcessAttachService.ts @@ -5,10 +5,8 @@ import { inject, injectable } from 'inversify'; import { IDebugService } from '../../../common/application/types'; -import { DebugConfiguration, DebugSession, l10n, WorkspaceFolder } from 'vscode'; +import { DebugConfiguration, DebugSession, l10n, WorkspaceFolder, DebugSessionOptions } from 'vscode'; import { noop } from '../../../common/utils/misc'; -import { captureTelemetry } from '../../../telemetry'; -import { EventName } from '../../../telemetry/constants'; import { AttachRequestArguments } from '../../types'; import { IChildProcessAttachService } from './types'; import { showErrorMessage } from '../../../common/vscodeApis/windowApis'; @@ -17,22 +15,24 @@ import { getWorkspaceFolders } from '../../../common/vscodeApis/workspaceApis'; /** * This class is responsible for attaching the debugger to any * child processes launched. I.e. this is the class responsible for multi-proc debugging. - * @export - * @class ChildProcessAttachEventHandler - * @implements {IChildProcessAttachService} */ @injectable() export class ChildProcessAttachService implements IChildProcessAttachService { constructor(@inject(IDebugService) private readonly debugService: IDebugService) {} - @captureTelemetry(EventName.DEBUGGER_ATTACH_TO_CHILD_PROCESS) public async attach(data: AttachRequestArguments & DebugConfiguration, parentSession: DebugSession): Promise { const debugConfig: AttachRequestArguments & DebugConfiguration = data; - const processId = debugConfig.subProcessId!; const folder = this.getRelatedWorkspaceFolder(debugConfig); - const launched = await this.debugService.startDebugging(folder, debugConfig, parentSession); + const debugSessionOption: DebugSessionOptions = { + parentSession: parentSession, + lifecycleManagedByParent: true, + }; + const launched = await this.debugService.startDebugging(folder, debugConfig, debugSessionOption); if (!launched) { - showErrorMessage(l10n.t('Failed to launch debugger for child process {0}', processId)).then(noop, noop); + showErrorMessage(l10n.t('Failed to launch debugger for child process {0}', debugConfig.subProcessId!)).then( + noop, + noop, + ); } } diff --git a/src/client/debugger/extension/serviceRegistry.ts b/src/client/debugger/extension/serviceRegistry.ts index a8c5ae7bbfcc..7734e87124cd 100644 --- a/src/client/debugger/extension/serviceRegistry.ts +++ b/src/client/debugger/extension/serviceRegistry.ts @@ -13,10 +13,6 @@ import { OutdatedDebuggerPromptFactory } from './adapter/outdatedDebuggerPrompt' import { AttachProcessProviderFactory } from './attachQuickPick/factory'; import { IAttachProcessProviderFactory } from './attachQuickPick/types'; import { PythonDebugConfigurationService } from './configuration/debugConfigurationService'; -import { DynamicPythonDebugConfigurationService } from './configuration/dynamicdebugConfigurationService'; -import { LaunchJsonCompletionProvider } from './configuration/launch.json/completionProvider'; -import { InterpreterPathCommand } from './configuration/launch.json/interpreterPathCommand'; -import { LaunchJsonUpdaterService } from './configuration/launch.json/updaterService'; import { AttachConfigurationResolver } from './configuration/resolvers/attach'; import { DebugEnvironmentVariablesHelper, IDebugEnvironmentVariablesService } from './configuration/resolvers/helper'; import { LaunchConfigurationResolver } from './configuration/resolvers/launch'; @@ -29,31 +25,14 @@ import { IDebugAdapterDescriptorFactory, IDebugConfigurationService, IDebugSessionLoggingFactory, - IDynamicDebugConfigurationService, IOutdatedDebuggerPromptFactory, } from './types'; export function registerTypes(serviceManager: IServiceManager): void { - serviceManager.addSingleton( - IExtensionSingleActivationService, - LaunchJsonCompletionProvider, - ); - serviceManager.addSingleton( - IExtensionSingleActivationService, - InterpreterPathCommand, - ); - serviceManager.addSingleton( - IExtensionSingleActivationService, - LaunchJsonUpdaterService, - ); serviceManager.addSingleton( IDebugConfigurationService, PythonDebugConfigurationService, ); - serviceManager.addSingleton( - IDynamicDebugConfigurationService, - DynamicPythonDebugConfigurationService, - ); serviceManager.addSingleton(IChildProcessAttachService, ChildProcessAttachService); serviceManager.addSingleton(IDebugSessionEventHandlers, ChildProcessAttachEventHandler); serviceManager.addSingleton>( diff --git a/src/client/debugger/extension/types.ts b/src/client/debugger/extension/types.ts index 2a304efae918..4a8f35e2b808 100644 --- a/src/client/debugger/extension/types.ts +++ b/src/client/debugger/extension/types.ts @@ -3,46 +3,11 @@ 'use strict'; -import { Readable } from 'stream'; -import { - CancellationToken, - DebugAdapterDescriptorFactory, - DebugAdapterTrackerFactory, - DebugConfigurationProvider, - Disposable, - WorkspaceFolder, -} from 'vscode'; - -import { DebugConfigurationArguments } from '../types'; +import { DebugAdapterDescriptorFactory, DebugAdapterTrackerFactory, DebugConfigurationProvider } from 'vscode'; export const IDebugConfigurationService = Symbol('IDebugConfigurationService'); export interface IDebugConfigurationService extends DebugConfigurationProvider {} -export const IDynamicDebugConfigurationService = Symbol('IDynamicDebugConfigurationService'); -export interface IDynamicDebugConfigurationService extends DebugConfigurationProvider {} - -export type DebugConfigurationState = { - config: Partial; - folder?: WorkspaceFolder; - token?: CancellationToken; -}; - -export enum DebugConfigurationType { - launchFile = 'launchFile', - remoteAttach = 'remoteAttach', - launchDjango = 'launchDjango', - launchFastAPI = 'launchFastAPI', - launchFlask = 'launchFlask', - launchModule = 'launchModule', - launchPyramid = 'launchPyramid', - pidAttach = 'pidAttach', -} - -export enum PythonPathSource { - launchJson = 'launch.json', - settingsJson = 'settings.json', -} - export const IDebugAdapterDescriptorFactory = Symbol('IDebugAdapterDescriptorFactory'); export interface IDebugAdapterDescriptorFactory extends DebugAdapterDescriptorFactory {} @@ -54,9 +19,7 @@ export const IOutdatedDebuggerPromptFactory = Symbol('IOutdatedDebuggerPromptFac export interface IOutdatedDebuggerPromptFactory extends DebugAdapterTrackerFactory {} -export const IProtocolParser = Symbol('IProtocolParser'); -export interface IProtocolParser extends Disposable { - connect(stream: Readable): void; - once(event: string | symbol, listener: (...args: unknown[]) => void): this; - on(event: string | symbol, listener: (...args: unknown[]) => void): this; +export enum PythonPathSource { + launchJson = 'launch.json', + settingsJson = 'settings.json', } diff --git a/src/client/debugger/pythonDebugger.ts b/src/client/debugger/pythonDebugger.ts new file mode 100644 index 000000000000..3450e95f3cee --- /dev/null +++ b/src/client/debugger/pythonDebugger.ts @@ -0,0 +1,30 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { extensions } from 'vscode'; + +interface IPythonDebuggerExtensionApi { + debug: { + getDebuggerPackagePath(): Promise; + }; +} + +async function activateExtension() { + const extension = extensions.getExtension('ms-python.debugpy'); + if (extension) { + if (!extension.isActive) { + await extension.activate(); + } + } + return extension; +} + +async function getPythonDebuggerExtensionAPI(): Promise { + const extension = await activateExtension(); + return extension?.exports as IPythonDebuggerExtensionApi; +} + +export async function getDebugpyPath(): Promise { + const api = await getPythonDebuggerExtensionAPI(); + return api?.debug.getDebuggerPackagePath() ?? ''; +} diff --git a/src/client/debugger/types.ts b/src/client/debugger/types.ts index 60e82fb04418..1422f1aa75ab 100644 --- a/src/client/debugger/types.ts +++ b/src/client/debugger/types.ts @@ -5,13 +5,12 @@ import { DebugConfiguration } from 'vscode'; import { DebugProtocol } from 'vscode-debugprotocol/lib/debugProtocol'; -import { DebuggerTypeName } from './constants'; +import { DebuggerTypeName, PythonDebuggerTypeName } from './constants'; export enum DebugOptions { RedirectOutput = 'RedirectOutput', Django = 'Django', Jinja = 'Jinja', - DebugStdLib = 'DebugStdLib', Sudo = 'Sudo', Pyramid = 'Pyramid', FixFilePathCase = 'FixFilePathCase', @@ -61,6 +60,7 @@ interface ICommonDebugArguments { pathMappings?: PathMapping[]; clientOS?: 'windows' | 'unix'; } + interface IKnownAttachDebugArguments extends ICommonDebugArguments { workspaceFolder?: string; customDebugger?: boolean; @@ -122,14 +122,14 @@ export interface LaunchRequestArguments extends DebugProtocol.LaunchRequestArguments, IKnownLaunchRequestArguments, DebugConfiguration { - type: typeof DebuggerTypeName; + type: typeof DebuggerTypeName | typeof PythonDebuggerTypeName; } export interface AttachRequestArguments extends DebugProtocol.AttachRequestArguments, IKnownAttachDebugArguments, DebugConfiguration { - type: typeof DebuggerTypeName; + type: typeof DebuggerTypeName | typeof PythonDebuggerTypeName; } export interface DebugConfigurationArguments extends LaunchRequestArguments, AttachRequestArguments {} diff --git a/src/client/deprecatedProposedApi.ts b/src/client/deprecatedProposedApi.ts index e63670e4bf1b..d0003c895517 100644 --- a/src/client/deprecatedProposedApi.ts +++ b/src/client/deprecatedProposedApi.ts @@ -13,7 +13,7 @@ import { } from './deprecatedProposedApiTypes'; import { IInterpreterService } from './interpreter/contracts'; import { IServiceContainer } from './ioc/types'; -import { traceVerbose } from './logging'; +import { traceVerbose, traceWarn } from './logging'; import { PythonEnvInfo } from './pythonEnvironments/base/info'; import { getEnvPath } from './pythonEnvironments/base/info/env'; import { GetRefreshEnvironmentsOptions, IDiscoveryAPI } from './pythonEnvironments/base/locator'; @@ -74,7 +74,7 @@ export function buildDeprecatedProposedApi( }); traceVerbose(`Extension ${info.extensionId} accessed ${apiName}`); if (warnLog && !warningLogged.has(info.extensionId)) { - console.warn( + traceWarn( `${info.extensionId} extension is using deprecated python APIs which will be removed soon.`, ); warningLogged.add(info.extensionId); diff --git a/src/client/deprecatedProposedApiTypes.ts b/src/client/deprecatedProposedApiTypes.ts index 14cabe1d09ae..eb76d61dc907 100644 --- a/src/client/deprecatedProposedApiTypes.ts +++ b/src/client/deprecatedProposedApiTypes.ts @@ -4,7 +4,7 @@ import { Uri, Event } from 'vscode'; import { PythonEnvKind, EnvPathType } from './pythonEnvironments/base/info'; import { ProgressNotificationEvent, GetRefreshEnvironmentsOptions } from './pythonEnvironments/base/locator'; -import { Resource } from './apiTypes'; +import { Resource } from './api/types'; export interface EnvironmentDetailsOptions { useCache: boolean; @@ -57,7 +57,6 @@ export interface DeprecatedProposedAPI { * @param {Resource} [resource] A resource for which the setting is asked for. * * When no resource is provided, the setting scoped to the first workspace folder is returned. * * If no folder is present, it returns the global setting. - * @returns {({ execCommand: string[] | undefined })} */ getExecutionDetails( resource?: Resource, diff --git a/src/client/envExt/api.internal.ts b/src/client/envExt/api.internal.ts new file mode 100644 index 000000000000..5edfb712072e --- /dev/null +++ b/src/client/envExt/api.internal.ts @@ -0,0 +1,179 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { EventEmitter, Terminal, Uri, Disposable } from 'vscode'; +import { getExtension } from '../common/vscodeApis/extensionsApi'; +import { + GetEnvironmentScope, + PythonBackgroundRunOptions, + PythonEnvironment, + PythonEnvironmentApi, + PythonProcess, + RefreshEnvironmentsScope, + DidChangeEnvironmentEventArgs, +} from './types'; +import { executeCommand } from '../common/vscodeApis/commandApis'; +import { getConfiguration, getWorkspaceFolders } from '../common/vscodeApis/workspaceApis'; +import { traceError, traceLog } from '../logging'; +import { Interpreters } from '../common/utils/localize'; + +export const ENVS_EXTENSION_ID = 'ms-python.vscode-python-envs'; + +export function isEnvExtensionInstalled(): boolean { + return !!getExtension(ENVS_EXTENSION_ID); +} + +/** + * Returns true if the Python Environments extension is installed and not explicitly + * disabled by the user. Mirrors the envs extension's own activation logic: it + * deactivates only when `python.useEnvironmentsExtension` is explicitly set to false + * at the global, workspace, or workspace-folder level. + */ +export function shouldEnvExtHandleActivation(): boolean { + if (!isEnvExtensionInstalled()) { + return false; + } + const config = getConfiguration('python'); + const inspection = config.inspect('useEnvironmentsExtension'); + if (inspection?.globalValue === false || inspection?.workspaceValue === false) { + return false; + } + // The envs extension also checks folder-scoped settings in multi-root workspaces. + // Any single folder with the setting set to false causes the envs extension to + // deactivate entirely (window-wide), so we must mirror that here. + const workspaceFolders = getWorkspaceFolders(); + if (workspaceFolders) { + for (const folder of workspaceFolders) { + const folderConfig = getConfiguration('python', folder.uri); + const folderInspection = folderConfig.inspect('useEnvironmentsExtension'); + if (folderInspection?.workspaceFolderValue === false) { + return false; + } + } + } + return true; +} + +let _useExt: boolean | undefined; +export function useEnvExtension(): boolean { + if (_useExt !== undefined) { + return _useExt; + } + const config = getConfiguration('python'); + const inExpSetting = config?.get('useEnvironmentsExtension', false) ?? false; + // If extension is installed and in experiment, then use it. + _useExt = !!getExtension(ENVS_EXTENSION_ID) && inExpSetting; + return _useExt; +} + +const onDidChangeEnvironmentEnvExtEmitter: EventEmitter = new EventEmitter< + DidChangeEnvironmentEventArgs +>(); +export function onDidChangeEnvironmentEnvExt( + listener: (e: DidChangeEnvironmentEventArgs) => unknown, + thisArgs?: unknown, + disposables?: Disposable[], +): Disposable { + return onDidChangeEnvironmentEnvExtEmitter.event(listener, thisArgs, disposables); +} + +let _extApi: PythonEnvironmentApi | undefined; +export async function getEnvExtApi(): Promise { + if (_extApi) { + return _extApi; + } + const extension = getExtension(ENVS_EXTENSION_ID); + if (!extension) { + traceError(Interpreters.envExtActivationFailed); + throw new Error('Python Environments extension not found.'); + } + if (!extension?.isActive) { + try { + await extension.activate(); + } catch (ex) { + traceError(Interpreters.envExtActivationFailed, ex); + throw ex; + } + } + + traceLog(Interpreters.envExtDiscoveryAttribution); + + _extApi = extension.exports as PythonEnvironmentApi; + _extApi.onDidChangeEnvironment((e) => { + onDidChangeEnvironmentEnvExtEmitter.fire(e); + }); + + return _extApi; +} + +export async function runInBackground( + environment: PythonEnvironment, + options: PythonBackgroundRunOptions, +): Promise { + const envExtApi = await getEnvExtApi(); + return envExtApi.runInBackground(environment, options); +} + +export async function getEnvironment(scope: GetEnvironmentScope): Promise { + const envExtApi = await getEnvExtApi(); + const env = await envExtApi.getEnvironment(scope); + if (!env) { + traceLog(Interpreters.envExtNoActiveEnvironment); + } + return env; +} + +export async function resolveEnvironment(pythonPath: string): Promise { + const envExtApi = await getEnvExtApi(); + return envExtApi.resolveEnvironment(Uri.file(pythonPath)); +} + +export async function refreshEnvironments(scope: RefreshEnvironmentsScope): Promise { + const envExtApi = await getEnvExtApi(); + return envExtApi.refreshEnvironments(scope); +} + +export async function runInTerminal( + resource: Uri | undefined, + args?: string[], + cwd?: string | Uri, + show?: boolean, +): Promise { + const envExtApi = await getEnvExtApi(); + const env = await getEnvironment(resource); + const project = resource ? envExtApi.getPythonProject(resource) : undefined; + if (env && resource) { + return envExtApi.runInTerminal(env, { + cwd: cwd ?? project?.uri ?? process.cwd(), + args, + show, + }); + } + throw new Error('Invalid arguments to run in terminal'); +} + +export async function runInDedicatedTerminal( + resource: Uri | undefined, + args?: string[], + cwd?: string | Uri, + show?: boolean, +): Promise { + const envExtApi = await getEnvExtApi(); + const env = await getEnvironment(resource); + const project = resource ? envExtApi.getPythonProject(resource) : undefined; + if (env) { + return envExtApi.runInDedicatedTerminal(resource ?? 'global', env, { + cwd: cwd ?? project?.uri ?? process.cwd(), + args, + show, + }); + } + throw new Error('Invalid arguments to run in dedicated terminal'); +} + +export async function clearCache(): Promise { + const envExtApi = await getEnvExtApi(); + if (envExtApi) { + await executeCommand('python-envs.clearCache'); + } +} diff --git a/src/client/envExt/api.legacy.ts b/src/client/envExt/api.legacy.ts new file mode 100644 index 000000000000..6f2e60774033 --- /dev/null +++ b/src/client/envExt/api.legacy.ts @@ -0,0 +1,171 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { Terminal, Uri } from 'vscode'; +import { getEnvExtApi, getEnvironment } from './api.internal'; +import { EnvironmentType, PythonEnvironment as PythonEnvironmentLegacy } from '../pythonEnvironments/info'; +import { PythonEnvironment, PythonTerminalCreateOptions } from './types'; +import { Architecture } from '../common/utils/platform'; +import { parseVersion } from '../pythonEnvironments/base/info/pythonVersion'; +import { PythonEnvType } from '../pythonEnvironments/base/info'; +import { traceError } from '../logging'; +import { reportActiveInterpreterChanged } from '../environmentApi'; +import { getWorkspaceFolder, getWorkspaceFolders } from '../common/vscodeApis/workspaceApis'; + +function toEnvironmentType(pythonEnv: PythonEnvironment): EnvironmentType { + if (pythonEnv.envId.managerId.toLowerCase().endsWith('system')) { + return EnvironmentType.System; + } + if (pythonEnv.envId.managerId.toLowerCase().endsWith('venv')) { + return EnvironmentType.Venv; + } + if (pythonEnv.envId.managerId.toLowerCase().endsWith('virtualenv')) { + return EnvironmentType.VirtualEnv; + } + if (pythonEnv.envId.managerId.toLowerCase().endsWith('conda')) { + return EnvironmentType.Conda; + } + if (pythonEnv.envId.managerId.toLowerCase().endsWith('pipenv')) { + return EnvironmentType.Pipenv; + } + if (pythonEnv.envId.managerId.toLowerCase().endsWith('poetry')) { + return EnvironmentType.Poetry; + } + if (pythonEnv.envId.managerId.toLowerCase().endsWith('pyenv')) { + return EnvironmentType.Pyenv; + } + if (pythonEnv.envId.managerId.toLowerCase().endsWith('hatch')) { + return EnvironmentType.Hatch; + } + if (pythonEnv.envId.managerId.toLowerCase().endsWith('pixi')) { + return EnvironmentType.Pixi; + } + if (pythonEnv.envId.managerId.toLowerCase().endsWith('virtualenvwrapper')) { + return EnvironmentType.VirtualEnvWrapper; + } + if (pythonEnv.envId.managerId.toLowerCase().endsWith('activestate')) { + return EnvironmentType.ActiveState; + } + return EnvironmentType.Unknown; +} + +function getEnvType(kind: EnvironmentType): PythonEnvType | undefined { + switch (kind) { + case EnvironmentType.Pipenv: + case EnvironmentType.VirtualEnv: + case EnvironmentType.Pyenv: + case EnvironmentType.Venv: + case EnvironmentType.Poetry: + case EnvironmentType.Hatch: + case EnvironmentType.Pixi: + case EnvironmentType.VirtualEnvWrapper: + case EnvironmentType.ActiveState: + return PythonEnvType.Virtual; + + case EnvironmentType.Conda: + return PythonEnvType.Conda; + + case EnvironmentType.MicrosoftStore: + case EnvironmentType.Global: + case EnvironmentType.System: + default: + return undefined; + } +} + +function toLegacyType(env: PythonEnvironment): PythonEnvironmentLegacy { + const ver = parseVersion(env.version); + const envType = toEnvironmentType(env); + return { + id: env.execInfo.run.executable, + displayName: env.displayName, + detailedDisplayName: env.name, + envType, + envPath: env.sysPrefix, + type: getEnvType(envType), + path: env.execInfo.run.executable, + version: { + raw: env.version, + major: ver.major, + minor: ver.minor, + patch: ver.micro, + build: [], + prerelease: [], + }, + sysVersion: env.version, + architecture: Architecture.x64, + sysPrefix: env.sysPrefix, + }; +} + +const previousEnvMap = new Map(); +export async function getActiveInterpreterLegacy(resource?: Uri): Promise { + const api = await getEnvExtApi(); + const uri = resource ? api.getPythonProject(resource)?.uri : undefined; + + const pythonEnv = await getEnvironment(resource); + const oldEnv = previousEnvMap.get(uri?.fsPath || ''); + const newEnv = pythonEnv ? toLegacyType(pythonEnv) : undefined; + + const folders = getWorkspaceFolders() ?? []; + const shouldReport = + (folders.length === 0 && resource === undefined) || (folders.length > 0 && resource !== undefined); + if (shouldReport && newEnv && oldEnv?.envId.id !== pythonEnv?.envId.id) { + reportActiveInterpreterChanged({ + resource: getWorkspaceFolder(resource), + path: newEnv.path, + }); + previousEnvMap.set(uri?.fsPath || '', pythonEnv); + } + return pythonEnv ? toLegacyType(pythonEnv) : undefined; +} + +export async function setInterpreterLegacy(pythonPath: string, uri: Uri | undefined): Promise { + const api = await getEnvExtApi(); + const pythonEnv = await api.resolveEnvironment(Uri.file(pythonPath)); + if (!pythonEnv) { + traceError(`EnvExt: Failed to resolve environment for ${pythonPath}`); + return; + } + await api.setEnvironment(uri, pythonEnv); +} + +export async function resetInterpreterLegacy(uri: Uri | undefined): Promise { + const api = await getEnvExtApi(); + await api.setEnvironment(uri, undefined); +} + +export async function ensureTerminalLegacy( + resource: Uri | undefined, + options?: PythonTerminalCreateOptions, +): Promise { + const api = await getEnvExtApi(); + const pythonEnv = await api.getEnvironment(resource); + const project = resource ? api.getPythonProject(resource) : undefined; + + if (pythonEnv && project) { + const fixedOptions = options ? { ...options } : { cwd: project.uri }; + const terminal = await api.createTerminal(pythonEnv, fixedOptions); + return terminal; + } + traceError('ensureTerminalLegacy - Did not return terminal successfully.'); + traceError( + 'ensureTerminalLegacy - pythonEnv:', + pythonEnv + ? `id=${pythonEnv.envId.id}, managerId=${pythonEnv.envId.managerId}, name=${pythonEnv.name}, version=${pythonEnv.version}, executable=${pythonEnv.execInfo.run.executable}` + : 'undefined', + ); + traceError( + 'ensureTerminalLegacy - project:', + project ? `name=${project.name}, uri=${project.uri.toString()}` : 'undefined', + ); + traceError( + 'ensureTerminalLegacy - options:', + options + ? `name=${options.name}, cwd=${options.cwd?.toString()}, hideFromUser=${options.hideFromUser}` + : 'undefined', + ); + traceError('ensureTerminalLegacy - resource:', resource?.toString() || 'undefined'); + + throw new Error('Invalid arguments to create terminal'); +} diff --git a/src/client/envExt/envExtApi.ts b/src/client/envExt/envExtApi.ts new file mode 100644 index 000000000000..34f42f0d6954 --- /dev/null +++ b/src/client/envExt/envExtApi.ts @@ -0,0 +1,345 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +/* eslint-disable class-methods-use-this */ + +import * as path from 'path'; +import { Event, EventEmitter, Disposable, Uri } from 'vscode'; +import { PythonEnvInfo, PythonEnvKind, PythonEnvType, PythonVersion } from '../pythonEnvironments/base/info'; +import { + GetRefreshEnvironmentsOptions, + IDiscoveryAPI, + ProgressNotificationEvent, + ProgressReportStage, + PythonLocatorQuery, + TriggerRefreshOptions, +} from '../pythonEnvironments/base/locator'; +import { PythonEnvCollectionChangedEvent } from '../pythonEnvironments/base/watcher'; +import { getEnvExtApi } from './api.internal'; +import { createDeferred, Deferred } from '../common/utils/async'; +import { StopWatch } from '../common/utils/stopWatch'; +import { traceError, traceLog, traceWarn } from '../logging'; +import { + DidChangeEnvironmentsEventArgs, + EnvironmentChangeKind, + PythonEnvironment, + PythonEnvironmentApi, +} from './types'; +import { FileChangeType } from '../common/platform/fileSystemWatcher'; +import { Architecture, isWindows } from '../common/utils/platform'; +import { parseVersion } from '../pythonEnvironments/base/info/pythonVersion'; +import { Interpreters } from '../common/utils/localize'; + +function getKind(pythonEnv: PythonEnvironment): PythonEnvKind { + if (pythonEnv.envId.managerId.toLowerCase().endsWith('system')) { + return PythonEnvKind.System; + } + if (pythonEnv.envId.managerId.toLowerCase().endsWith('conda')) { + return PythonEnvKind.Conda; + } + if (pythonEnv.envId.managerId.toLowerCase().endsWith('venv')) { + return PythonEnvKind.Venv; + } + if (pythonEnv.envId.managerId.toLowerCase().endsWith('virtualenv')) { + return PythonEnvKind.VirtualEnv; + } + if (pythonEnv.envId.managerId.toLowerCase().endsWith('virtualenvwrapper')) { + return PythonEnvKind.VirtualEnvWrapper; + } + if (pythonEnv.envId.managerId.toLowerCase().endsWith('pyenv')) { + return PythonEnvKind.Pyenv; + } + if (pythonEnv.envId.managerId.toLowerCase().endsWith('pipenv')) { + return PythonEnvKind.Pipenv; + } + if (pythonEnv.envId.managerId.toLowerCase().endsWith('poetry')) { + return PythonEnvKind.Poetry; + } + if (pythonEnv.envId.managerId.toLowerCase().endsWith('pixi')) { + return PythonEnvKind.Pixi; + } + if (pythonEnv.envId.managerId.toLowerCase().endsWith('hatch')) { + return PythonEnvKind.Hatch; + } + if (pythonEnv.envId.managerId.toLowerCase().endsWith('activestate')) { + return PythonEnvKind.ActiveState; + } + + return PythonEnvKind.Unknown; +} + +function makeExecutablePath(prefix?: string): string { + if (!prefix) { + return process.platform === 'win32' ? 'python.exe' : 'python'; + } + return process.platform === 'win32' ? path.join(prefix, 'python.exe') : path.join(prefix, 'python'); +} + +function getExecutable(pythonEnv: PythonEnvironment): string { + if (pythonEnv.execInfo?.run?.executable) { + return pythonEnv.execInfo?.run?.executable; + } + + const basename = path.basename(pythonEnv.environmentPath.fsPath).toLowerCase(); + if (isWindows() && basename.startsWith('python') && basename.endsWith('.exe')) { + return pythonEnv.environmentPath.fsPath; + } + + if (!isWindows() && basename.startsWith('python')) { + return pythonEnv.environmentPath.fsPath; + } + + return makeExecutablePath(pythonEnv.sysPrefix); +} + +function getLocation(pythonEnv: PythonEnvironment): string { + if (pythonEnv.envId.managerId.toLowerCase().endsWith('conda')) { + return pythonEnv.sysPrefix; + } + + return pythonEnv.environmentPath.fsPath; +} + +function getEnvType(kind: PythonEnvKind): PythonEnvType | undefined { + switch (kind) { + case PythonEnvKind.Poetry: + case PythonEnvKind.Pyenv: + case PythonEnvKind.VirtualEnv: + case PythonEnvKind.Venv: + case PythonEnvKind.VirtualEnvWrapper: + case PythonEnvKind.OtherVirtual: + case PythonEnvKind.Pipenv: + case PythonEnvKind.ActiveState: + case PythonEnvKind.Hatch: + case PythonEnvKind.Pixi: + return PythonEnvType.Virtual; + + case PythonEnvKind.Conda: + return PythonEnvType.Conda; + + case PythonEnvKind.System: + case PythonEnvKind.Unknown: + case PythonEnvKind.OtherGlobal: + case PythonEnvKind.Custom: + case PythonEnvKind.MicrosoftStore: + default: + return undefined; + } +} + +function toPythonEnvInfo(pythonEnv: PythonEnvironment): PythonEnvInfo | undefined { + const kind = getKind(pythonEnv); + const arch = Architecture.x64; + const version: PythonVersion = parseVersion(pythonEnv.version); + const { name, displayName, sysPrefix } = pythonEnv; + const executable = getExecutable(pythonEnv); + const location = getLocation(pythonEnv); + + return { + name, + location, + kind, + id: executable, + executable: { + filename: executable, + sysPrefix, + ctime: -1, + mtime: -1, + }, + version: { + sysVersion: pythonEnv.version, + major: version.major, + minor: version.minor, + micro: version.micro, + }, + arch, + distro: { + org: '', + }, + source: [], + detailedDisplayName: displayName, + display: displayName, + type: getEnvType(kind), + }; +} + +function hasChanged(old: PythonEnvInfo, newEnv: PythonEnvInfo): boolean { + if (old.executable.filename !== newEnv.executable.filename) { + return true; + } + if (old.version.major !== newEnv.version.major) { + return true; + } + if (old.version.minor !== newEnv.version.minor) { + return true; + } + if (old.version.micro !== newEnv.version.micro) { + return true; + } + if (old.location !== newEnv.location) { + return true; + } + if (old.kind !== newEnv.kind) { + return true; + } + if (old.arch !== newEnv.arch) { + return true; + } + + return false; +} + +class EnvExtApis implements IDiscoveryAPI, Disposable { + private _onProgress: EventEmitter; + + private _onChanged: EventEmitter; + + private _refreshPromise?: Deferred; + + private _envs: PythonEnvInfo[] = []; + + refreshState: ProgressReportStage; + + private _disposables: Disposable[] = []; + + constructor(private envExtApi: PythonEnvironmentApi) { + this._onProgress = new EventEmitter(); + this._onChanged = new EventEmitter(); + + this.onProgress = this._onProgress.event; + this.onChanged = this._onChanged.event; + + this.refreshState = ProgressReportStage.idle; + this._disposables.push( + this._onProgress, + this._onChanged, + this.envExtApi.onDidChangeEnvironments((e) => this.onDidChangeEnvironments(e)), + this.envExtApi.onDidChangeEnvironment((e) => { + this._onChanged.fire({ + type: FileChangeType.Changed, + searchLocation: e.uri, + old: e.old ? toPythonEnvInfo(e.old) : undefined, + new: e.new ? toPythonEnvInfo(e.new) : undefined, + }); + }), + ); + } + + onProgress: Event; + + onChanged: Event; + + getRefreshPromise(_options?: GetRefreshEnvironmentsOptions): Promise | undefined { + return this._refreshPromise?.promise; + } + + triggerRefresh(_query?: PythonLocatorQuery, _options?: TriggerRefreshOptions): Promise { + const stopwatch = new StopWatch(); + traceLog('Native locator: Refresh started'); + if (this.refreshState === ProgressReportStage.discoveryStarted && this._refreshPromise?.promise) { + return this._refreshPromise?.promise; + } + + this.refreshState = ProgressReportStage.discoveryStarted; + this._onProgress.fire({ stage: this.refreshState }); + this._refreshPromise = createDeferred(); + + const SLOW_DISCOVERY_THRESHOLD_MS = 25_000; + const slowDiscoveryTimer = setTimeout(() => { + traceWarn(Interpreters.envExtDiscoverySlow); + }, SLOW_DISCOVERY_THRESHOLD_MS); + + setImmediate(async () => { + try { + await this.envExtApi.refreshEnvironments(undefined); + if (this._envs.length === 0) { + traceWarn(Interpreters.envExtDiscoveryNoEnvironments); + } + this._refreshPromise?.resolve(); + } catch (error) { + traceError(Interpreters.envExtDiscoveryFailed, error); + this._refreshPromise?.reject(error); + } finally { + clearTimeout(slowDiscoveryTimer); + traceLog(`Native locator: Refresh finished in ${stopwatch.elapsedTime} ms`); + this.refreshState = ProgressReportStage.discoveryFinished; + this._refreshPromise = undefined; + this._onProgress.fire({ stage: this.refreshState }); + } + }); + + return this._refreshPromise?.promise; + } + + getEnvs(_query?: PythonLocatorQuery): PythonEnvInfo[] { + return this._envs; + } + + private addEnv(pythonEnv: PythonEnvironment, searchLocation?: Uri): PythonEnvInfo | undefined { + const info = toPythonEnvInfo(pythonEnv); + if (info) { + const old = this._envs.find((item) => item.executable.filename === info.executable.filename); + if (old) { + this._envs = this._envs.filter((item) => item.executable.filename !== info.executable.filename); + this._envs.push(info); + if (hasChanged(old, info)) { + this._onChanged.fire({ type: FileChangeType.Changed, old, new: info, searchLocation }); + } + } else { + this._envs.push(info); + this._onChanged.fire({ type: FileChangeType.Created, new: info, searchLocation }); + } + } + + return info; + } + + private removeEnv(env: PythonEnvInfo | string): void { + if (typeof env === 'string') { + const old = this._envs.find((item) => item.executable.filename === env); + this._envs = this._envs.filter((item) => item.executable.filename !== env); + this._onChanged.fire({ type: FileChangeType.Deleted, old }); + return; + } + this._envs = this._envs.filter((item) => item.executable.filename !== env.executable.filename); + this._onChanged.fire({ type: FileChangeType.Deleted, old: env }); + } + + async resolveEnv(envPath?: string): Promise { + if (envPath === undefined) { + return undefined; + } + try { + const pythonEnv = await this.envExtApi.resolveEnvironment(Uri.file(envPath)); + if (pythonEnv) { + return this.addEnv(pythonEnv); + } + } catch (error) { + traceError( + `Failed to resolve environment "${envPath}" via the Python Environments extension (ms-python.vscode-python-envs). Check the "Python Environments" output channel for details.`, + error, + ); + } + return undefined; + } + + dispose(): void { + this._disposables.forEach((d) => d.dispose()); + } + + onDidChangeEnvironments(e: DidChangeEnvironmentsEventArgs): void { + e.forEach((item) => { + if (item.kind === EnvironmentChangeKind.remove) { + this.removeEnv(item.environment.environmentPath.fsPath); + } + if (item.kind === EnvironmentChangeKind.add) { + this.addEnv(item.environment); + } + }); + } +} + +export async function createEnvExtApi(disposables: Disposable[]): Promise { + const api = new EnvExtApis(await getEnvExtApi()); + disposables.push(api); + return api; +} diff --git a/src/client/envExt/types.ts b/src/client/envExt/types.ts new file mode 100644 index 000000000000..707d641bbfe8 --- /dev/null +++ b/src/client/envExt/types.ts @@ -0,0 +1,1274 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { + Disposable, + Event, + FileChangeType, + LogOutputChannel, + MarkdownString, + TaskExecution, + Terminal, + TerminalOptions, + ThemeIcon, + Uri, +} from 'vscode'; + +/** + * The path to an icon, or a theme-specific configuration of icons. + */ +export type IconPath = + | Uri + | { + /** + * The icon path for the light theme. + */ + light: Uri; + /** + * The icon path for the dark theme. + */ + dark: Uri; + } + | ThemeIcon; + +/** + * Options for executing a Python executable. + */ +export interface PythonCommandRunConfiguration { + /** + * Path to the binary like `python.exe` or `python3` to execute. This should be an absolute path + * to an executable that can be spawned. + */ + executable: string; + + /** + * Arguments to pass to the python executable. These arguments will be passed on all execute calls. + * This is intended for cases where you might want to do interpreter specific flags. + */ + args?: string[]; +} + +/** + * Contains details on how to use a particular python environment + * + * Running In Terminal: + * 1. If {@link PythonEnvironmentExecutionInfo.activatedRun} is provided, then that will be used. + * 2. If {@link PythonEnvironmentExecutionInfo.activatedRun} is not provided, then: + * - If {@link PythonEnvironmentExecutionInfo.shellActivation} is provided and shell type is known, then that will be used. + * - If {@link PythonEnvironmentExecutionInfo.shellActivation} is provided and shell type is not known, then: + * - 'unknown' will be used if provided. + * - {@link PythonEnvironmentExecutionInfo.activation} will be used otherwise. + * - If {@link PythonEnvironmentExecutionInfo.shellActivation} is not provided, then {@link PythonEnvironmentExecutionInfo.activation} will be used. + * - If {@link PythonEnvironmentExecutionInfo.activation} is not provided, then {@link PythonEnvironmentExecutionInfo.run} will be used. + * + * Creating a Terminal: + * 1. If {@link PythonEnvironmentExecutionInfo.shellActivation} is provided and shell type is known, then that will be used. + * 2. If {@link PythonEnvironmentExecutionInfo.shellActivation} is provided and shell type is not known, then {@link PythonEnvironmentExecutionInfo.activation} will be used. + * 3. If {@link PythonEnvironmentExecutionInfo.shellActivation} is not provided, then: + * - 'unknown' will be used if provided. + * - {@link PythonEnvironmentExecutionInfo.activation} will be used otherwise. + * 4. If {@link PythonEnvironmentExecutionInfo.activation} is not provided, then {@link PythonEnvironmentExecutionInfo.run} will be used. + * + */ +export interface PythonEnvironmentExecutionInfo { + /** + * Details on how to run the python executable. + */ + run: PythonCommandRunConfiguration; + + /** + * Details on how to run the python executable after activating the environment. + * If set this will overrides the {@link PythonEnvironmentExecutionInfo.run} command. + */ + activatedRun?: PythonCommandRunConfiguration; + + /** + * Details on how to activate an environment. + */ + activation?: PythonCommandRunConfiguration[]; + + /** + * Details on how to activate an environment using a shell specific command. + * If set this will override the {@link PythonEnvironmentExecutionInfo.activation}. + * 'unknown' is used if shell type is not known. + * If 'unknown' is not provided and shell type is not known then + * {@link PythonEnvironmentExecutionInfo.activation} if set. + */ + shellActivation?: Map; + + /** + * Details on how to deactivate an environment. + */ + deactivation?: PythonCommandRunConfiguration[]; + + /** + * Details on how to deactivate an environment using a shell specific command. + * If set this will override the {@link PythonEnvironmentExecutionInfo.deactivation} property. + * 'unknown' is used if shell type is not known. + * If 'unknown' is not provided and shell type is not known then + * {@link PythonEnvironmentExecutionInfo.deactivation} if set. + */ + shellDeactivation?: Map; +} + +/** + * Interface representing the ID of a Python environment. + */ +export interface PythonEnvironmentId { + /** + * The unique identifier of the Python environment. + */ + id: string; + + /** + * The ID of the manager responsible for the Python environment. + */ + managerId: string; +} + +/** + * Display information for an environment group. + */ +export interface EnvironmentGroupInfo { + /** + * The name of the environment group. This is used as an identifier for the group. + * + * Note: The first instance of the group with the given name will be used in the UI. + */ + readonly name: string; + + /** + * The description of the environment group. + */ + readonly description?: string; + + /** + * The tooltip for the environment group, which can be a string or a Markdown string. + */ + readonly tooltip?: string | MarkdownString; + + /** + * The icon path for the environment group, which can be a string, Uri, or an object with light and dark theme paths. + */ + readonly iconPath?: IconPath; +} + +/** + * Interface representing information about a Python environment. + */ +export interface PythonEnvironmentInfo { + /** + * The name of the Python environment. + */ + readonly name: string; + + /** + * The display name of the Python environment. + */ + readonly displayName: string; + + /** + * The short display name of the Python environment. + */ + readonly shortDisplayName?: string; + + /** + * The display path of the Python environment. + */ + readonly displayPath: string; + + /** + * The version of the Python environment. + */ + readonly version: string; + + /** + * Path to the python binary or environment folder. + */ + readonly environmentPath: Uri; + + /** + * The description of the Python environment. + */ + readonly description?: string; + + /** + * The tooltip for the Python environment, which can be a string or a Markdown string. + */ + readonly tooltip?: string | MarkdownString; + + /** + * The icon path for the Python environment, which can be a string, Uri, or an object with light and dark theme paths. + */ + readonly iconPath?: IconPath; + + /** + * Information on how to execute the Python environment. This is required for executing Python code in the environment. + */ + readonly execInfo: PythonEnvironmentExecutionInfo; + + /** + * `sys.prefix` is the path to the base directory of the Python installation. Typically obtained by executing `sys.prefix` in the Python interpreter. + * This is required by extension like Jupyter, Pylance, and other extensions to provide better experience with python. + */ + readonly sysPrefix: string; + + /** + * Optional `group` for this environment. This is used to group environments in the Environment Manager UI. + */ + readonly group?: string | EnvironmentGroupInfo; +} + +/** + * Interface representing a Python environment. + */ +export interface PythonEnvironment extends PythonEnvironmentInfo { + /** + * The ID of the Python environment. + */ + readonly envId: PythonEnvironmentId; +} + +/** + * Type representing the scope for setting a Python environment. + * Can be undefined or a URI. + */ +export type SetEnvironmentScope = undefined | Uri | Uri[]; + +/** + * Type representing the scope for getting a Python environment. + * Can be undefined or a URI. + */ +export type GetEnvironmentScope = undefined | Uri; + +/** + * Type representing the scope for creating a Python environment. + * Can be a Python project or 'global'. + */ +export type CreateEnvironmentScope = Uri | Uri[] | 'global'; +/** + * The scope for which environments are to be refreshed. + * - `undefined`: Search for environments globally and workspaces. + * - {@link Uri}: Environments in the workspace/folder or associated with the Uri. + */ +export type RefreshEnvironmentsScope = Uri | undefined; + +/** + * The scope for which environments are required. + * - `"all"`: All environments. + * - `"global"`: Python installations that are usually a base for creating virtual environments. + * - {@link Uri}: Environments for the workspace/folder/file pointed to by the Uri. + */ +export type GetEnvironmentsScope = Uri | 'all' | 'global'; + +/** + * Event arguments for when the current Python environment changes. + */ +export type DidChangeEnvironmentEventArgs = { + /** + * The URI of the environment that changed. + */ + readonly uri: Uri | undefined; + + /** + * The old Python environment before the change. + */ + readonly old: PythonEnvironment | undefined; + + /** + * The new Python environment after the change. + */ + readonly new: PythonEnvironment | undefined; +}; + +/** + * Enum representing the kinds of environment changes. + */ +export enum EnvironmentChangeKind { + /** + * Indicates that an environment was added. + */ + add = 'add', + + /** + * Indicates that an environment was removed. + */ + remove = 'remove', +} + +/** + * Event arguments for when the list of Python environments changes. + */ +export type DidChangeEnvironmentsEventArgs = { + /** + * The kind of change that occurred (add or remove). + */ + kind: EnvironmentChangeKind; + + /** + * The Python environment that was added or removed. + */ + environment: PythonEnvironment; +}[]; + +/** + * Type representing the context for resolving a Python environment. + */ +export type ResolveEnvironmentContext = Uri; + +export interface QuickCreateConfig { + /** + * The description of the quick create step. + */ + readonly description: string; + + /** + * The detail of the quick create step. + */ + readonly detail?: string; +} + +/** + * Interface representing an environment manager. + */ +export interface EnvironmentManager { + /** + * The name of the environment manager. Allowed characters (a-z, A-Z, 0-9, -, _). + */ + readonly name: string; + + /** + * The display name of the environment manager. + */ + readonly displayName?: string; + + /** + * The preferred package manager ID for the environment manager. This is a combination + * of publisher id, extension id, and {@link EnvironmentManager.name package manager name}. + * `.:` + * + * @example + * 'ms-python.python:pip' + */ + readonly preferredPackageManagerId: string; + + /** + * The description of the environment manager. + */ + readonly description?: string; + + /** + * The tooltip for the environment manager, which can be a string or a Markdown string. + */ + readonly tooltip?: string | MarkdownString | undefined; + + /** + * The icon path for the environment manager, which can be a string, Uri, or an object with light and dark theme paths. + */ + readonly iconPath?: IconPath; + + /** + * The log output channel for the environment manager. + */ + readonly log?: LogOutputChannel; + + /** + * The quick create details for the environment manager. Having this method also enables the quick create feature + * for the environment manager. Should Implement {@link EnvironmentManager.create} to support quick create. + */ + quickCreateConfig?(): QuickCreateConfig | undefined; + + /** + * Creates a new Python environment within the specified scope. + * @param scope - The scope within which to create the environment. + * @param options - Optional parameters for creating the Python environment. + * @returns A promise that resolves to the created Python environment, or undefined if creation failed. + */ + create?(scope: CreateEnvironmentScope, options?: CreateEnvironmentOptions): Promise; + + /** + * Removes the specified Python environment. + * @param environment - The Python environment to remove. + * @returns A promise that resolves when the environment is removed. + */ + remove?(environment: PythonEnvironment): Promise; + + /** + * Refreshes the list of Python environments within the specified scope. + * @param scope - The scope within which to refresh environments. + * @returns A promise that resolves when the refresh is complete. + */ + refresh(scope: RefreshEnvironmentsScope): Promise; + + /** + * Retrieves a list of Python environments within the specified scope. + * @param scope - The scope within which to retrieve environments. + * @returns A promise that resolves to an array of Python environments. + */ + getEnvironments(scope: GetEnvironmentsScope): Promise; + + /** + * Event that is fired when the list of Python environments changes. + */ + onDidChangeEnvironments?: Event; + + /** + * Sets the current Python environment within the specified scope. + * @param scope - The scope within which to set the environment. + * @param environment - The Python environment to set. If undefined, the environment is unset. + * @returns A promise that resolves when the environment is set. + */ + set(scope: SetEnvironmentScope, environment?: PythonEnvironment): Promise; + + /** + * Retrieves the current Python environment within the specified scope. + * @param scope - The scope within which to retrieve the environment. + * @returns A promise that resolves to the current Python environment, or undefined if none is set. + */ + get(scope: GetEnvironmentScope): Promise; + + /** + * Event that is fired when the current Python environment changes. + */ + onDidChangeEnvironment?: Event; + + /** + * Resolves the specified Python environment. The environment can be either a {@link PythonEnvironment} or a {@link Uri} context. + * + * This method is used to obtain a fully detailed {@link PythonEnvironment} object. The input can be: + * - A {@link PythonEnvironment} object, which might be missing key details such as {@link PythonEnvironment.execInfo}. + * - A {@link Uri} object, which typically represents either: + * - A folder that contains the Python environment. + * - The path to a Python executable. + * + * @param context - The context for resolving the environment, which can be a {@link PythonEnvironment} or a {@link Uri}. + * @returns A promise that resolves to the fully detailed {@link PythonEnvironment}, or `undefined` if the environment cannot be resolved. + */ + resolve(context: ResolveEnvironmentContext): Promise; + + /** + * Clears the environment manager's cache. + * + * @returns A promise that resolves when the cache is cleared. + */ + clearCache?(): Promise; +} + +/** + * Interface representing a package ID. + */ +export interface PackageId { + /** + * The ID of the package. + */ + id: string; + + /** + * The ID of the package manager. + */ + managerId: string; + + /** + * The ID of the environment in which the package is installed. + */ + environmentId: string; +} + +/** + * Interface representing package information. + */ +export interface PackageInfo { + /** + * The name of the package. + */ + readonly name: string; + + /** + * The display name of the package. + */ + readonly displayName: string; + + /** + * The version of the package. + */ + readonly version?: string; + + /** + * The description of the package. + */ + readonly description?: string; + + /** + * The tooltip for the package, which can be a string or a Markdown string. + */ + readonly tooltip?: string | MarkdownString | undefined; + + /** + * The icon path for the package, which can be a string, Uri, or an object with light and dark theme paths. + */ + readonly iconPath?: IconPath; + + /** + * The URIs associated with the package. + */ + readonly uris?: readonly Uri[]; +} + +/** + * Interface representing a package. + */ +export interface Package extends PackageInfo { + /** + * The ID of the package. + */ + readonly pkgId: PackageId; +} + +/** + * Enum representing the kinds of package changes. + */ +export enum PackageChangeKind { + /** + * Indicates that a package was added. + */ + add = 'add', + + /** + * Indicates that a package was removed. + */ + remove = 'remove', +} + +/** + * Event arguments for when packages change. + */ +export interface DidChangePackagesEventArgs { + /** + * The Python environment in which the packages changed. + */ + environment: PythonEnvironment; + + /** + * The package manager responsible for the changes. + */ + manager: PackageManager; + + /** + * The list of changes, each containing the kind of change and the package affected. + */ + changes: { kind: PackageChangeKind; pkg: Package }[]; +} + +/** + * Interface representing a package manager. + */ +export interface PackageManager { + /** + * The name of the package manager. Allowed characters (a-z, A-Z, 0-9, -, _). + */ + name: string; + + /** + * The display name of the package manager. + */ + displayName?: string; + + /** + * The description of the package manager. + */ + description?: string; + + /** + * The tooltip for the package manager, which can be a string or a Markdown string. + */ + tooltip?: string | MarkdownString | undefined; + + /** + * The icon path for the package manager, which can be a string, Uri, or an object with light and dark theme paths. + */ + iconPath?: IconPath; + + /** + * The log output channel for the package manager. + */ + log?: LogOutputChannel; + + /** + * Installs/Uninstall packages in the specified Python environment. + * @param environment - The Python environment in which to install packages. + * @param options - Options for managing packages. + * @returns A promise that resolves when the installation is complete. + */ + manage(environment: PythonEnvironment, options: PackageManagementOptions): Promise; + + /** + * Refreshes the package list for the specified Python environment. + * @param environment - The Python environment for which to refresh the package list. + * @returns A promise that resolves when the refresh is complete. + */ + refresh(environment: PythonEnvironment): Promise; + + /** + * Retrieves the list of packages for the specified Python environment. + * @param environment - The Python environment for which to retrieve packages. + * @returns An array of packages, or undefined if the packages could not be retrieved. + */ + getPackages(environment: PythonEnvironment): Promise; + + /** + * Event that is fired when packages change. + */ + onDidChangePackages?: Event; + + /** + * Clears the package manager's cache. + * @returns A promise that resolves when the cache is cleared. + */ + clearCache?(): Promise; +} + +/** + * Interface representing a Python project. + */ +export interface PythonProject { + /** + * The name of the Python project. + */ + readonly name: string; + + /** + * The URI of the Python project. + */ + readonly uri: Uri; + + /** + * The description of the Python project. + */ + readonly description?: string; + + /** + * The tooltip for the Python project, which can be a string or a Markdown string. + */ + readonly tooltip?: string | MarkdownString; + + /** + * The icon path for the Python project, which can be a string, Uri, or an object with light and dark theme paths. + */ + readonly iconPath?: IconPath; +} + +/** + * Options for creating a Python project. + */ +export interface PythonProjectCreatorOptions { + /** + * The name of the Python project. + */ + name: string; + + /** + * Path provided as the root for the project. + */ + rootUri: Uri; + + /** + * Boolean indicating whether the project should be created without any user input. + */ + quickCreate?: boolean; +} + +/** + * Interface representing a creator for Python projects. + */ +export interface PythonProjectCreator { + /** + * The name of the Python project creator. + */ + readonly name: string; + + /** + * The display name of the Python project creator. + */ + readonly displayName?: string; + + /** + * The description of the Python project creator. + */ + readonly description?: string; + + /** + * The tooltip for the Python project creator, which can be a string or a Markdown string. + */ + readonly tooltip?: string | MarkdownString; + + /** + * The icon path for the Python project creator, which can be a string, Uri, or an object with light and dark theme paths. + */ + readonly iconPath?: IconPath; + + /** + * Creates a new Python project(s) or, if files are not a project, returns Uri(s) to the created files. + * Anything that needs its own python environment constitutes a project. + * @param options Optional parameters for creating the Python project. + * @returns A promise that resolves to one of the following: + * - PythonProject or PythonProject[]: when a single or multiple projects are created. + * - Uri or Uri[]: when files are created that do not constitute a project. + * - undefined: if project creation fails. + */ + create(options?: PythonProjectCreatorOptions): Promise; + + /** + * A flag indicating whether the project creator supports quick create where no user input is required. + */ + readonly supportsQuickCreate?: boolean; +} + +/** + * Event arguments for when Python projects change. + */ +export interface DidChangePythonProjectsEventArgs { + /** + * The list of Python projects that were added. + */ + added: PythonProject[]; + + /** + * The list of Python projects that were removed. + */ + removed: PythonProject[]; +} + +export type PackageManagementOptions = + | { + /** + * Upgrade the packages if they are already installed. + */ + upgrade?: boolean; + + /** + * Show option to skip package installation or uninstallation. + */ + showSkipOption?: boolean; + /** + * The list of packages to install. + */ + install: string[]; + + /** + * The list of packages to uninstall. + */ + uninstall?: string[]; + } + | { + /** + * Upgrade the packages if they are already installed. + */ + upgrade?: boolean; + + /** + * Show option to skip package installation or uninstallation. + */ + showSkipOption?: boolean; + /** + * The list of packages to install. + */ + install?: string[]; + + /** + * The list of packages to uninstall. + */ + uninstall: string[]; + }; + +/** + * Options for creating a Python environment. + */ +export interface CreateEnvironmentOptions { + /** + * Provides some context about quick create based on user input. + * - if true, the environment should be created without any user input or prompts. + * - if false, the environment creation can show user input or prompts. + * This also means user explicitly skipped the quick create option. + * - if undefined, the environment creation can show user input or prompts. + * You can show quick create option to the user if you support it. + */ + quickCreate?: boolean; + /** + * Packages to install in addition to the automatically picked packages as a part of creating environment. + */ + additionalPackages?: string[]; +} + +/** + * Object representing the process started using run in background API. + */ +export interface PythonProcess { + /** + * The process ID of the Python process. + */ + readonly pid?: number; + + /** + * The standard input of the Python process. + */ + readonly stdin: NodeJS.WritableStream; + + /** + * The standard output of the Python process. + */ + readonly stdout: NodeJS.ReadableStream; + + /** + * The standard error of the Python process. + */ + readonly stderr: NodeJS.ReadableStream; + + /** + * Kills the Python process. + */ + kill(): void; + + /** + * Event that is fired when the Python process exits. + */ + onExit(listener: (code: number | null, signal: NodeJS.Signals | null) => void): void; +} + +export interface PythonEnvironmentManagerRegistrationApi { + /** + * Register an environment manager implementation. + * + * @param manager Environment Manager implementation to register. + * @returns A disposable that can be used to unregister the environment manager. + * @see {@link EnvironmentManager} + */ + registerEnvironmentManager(manager: EnvironmentManager): Disposable; +} + +export interface PythonEnvironmentItemApi { + /** + * Create a Python environment item from the provided environment info. This item is used to interact + * with the environment. + * + * @param info Some details about the environment like name, version, etc. needed to interact with the environment. + * @param manager The environment manager to associate with the environment. + * @returns The Python environment. + */ + createPythonEnvironmentItem(info: PythonEnvironmentInfo, manager: EnvironmentManager): PythonEnvironment; +} + +export interface PythonEnvironmentManagementApi { + /** + * Create a Python environment using environment manager associated with the scope. + * + * @param scope Where the environment is to be created. + * @param options Optional parameters for creating the Python environment. + * @returns The Python environment created. `undefined` if not created. + */ + createEnvironment( + scope: CreateEnvironmentScope, + options?: CreateEnvironmentOptions, + ): Promise; + + /** + * Remove a Python environment. + * + * @param environment The Python environment to remove. + * @returns A promise that resolves when the environment has been removed. + */ + removeEnvironment(environment: PythonEnvironment): Promise; +} + +export interface PythonEnvironmentsApi { + /** + * Initiates a refresh of Python environments within the specified scope. + * @param scope - The scope within which to search for environments. + * @returns A promise that resolves when the search is complete. + */ + refreshEnvironments(scope: RefreshEnvironmentsScope): Promise; + + /** + * Retrieves a list of Python environments within the specified scope. + * @param scope - The scope within which to retrieve environments. + * @returns A promise that resolves to an array of Python environments. + */ + getEnvironments(scope: GetEnvironmentsScope): Promise; + + /** + * Event that is fired when the list of Python environments changes. + * @see {@link DidChangeEnvironmentsEventArgs} + */ + onDidChangeEnvironments: Event; + + /** + * This method is used to get the details missing from a PythonEnvironment. Like + * {@link PythonEnvironment.execInfo} and other details. + * + * @param context : The PythonEnvironment or Uri for which details are required. + */ + resolveEnvironment(context: ResolveEnvironmentContext): Promise; +} + +export interface PythonProjectEnvironmentApi { + /** + * Sets the current Python environment within the specified scope. + * @param scope - The scope within which to set the environment. + * @param environment - The Python environment to set. If undefined, the environment is unset. + */ + setEnvironment(scope: SetEnvironmentScope, environment?: PythonEnvironment): Promise; + + /** + * Retrieves the current Python environment within the specified scope. + * @param scope - The scope within which to retrieve the environment. + * @returns A promise that resolves to the current Python environment, or undefined if none is set. + */ + getEnvironment(scope: GetEnvironmentScope): Promise; + + /** + * Event that is fired when the selected Python environment changes for Project, Folder or File. + * @see {@link DidChangeEnvironmentEventArgs} + */ + onDidChangeEnvironment: Event; +} + +export interface PythonEnvironmentManagerApi + extends PythonEnvironmentManagerRegistrationApi, + PythonEnvironmentItemApi, + PythonEnvironmentManagementApi, + PythonEnvironmentsApi, + PythonProjectEnvironmentApi {} + +export interface PythonPackageManagerRegistrationApi { + /** + * Register a package manager implementation. + * + * @param manager Package Manager implementation to register. + * @returns A disposable that can be used to unregister the package manager. + * @see {@link PackageManager} + */ + registerPackageManager(manager: PackageManager): Disposable; +} + +export interface PythonPackageGetterApi { + /** + * Refresh the list of packages in a Python Environment. + * + * @param environment The Python Environment for which the list of packages is to be refreshed. + * @returns A promise that resolves when the list of packages has been refreshed. + */ + refreshPackages(environment: PythonEnvironment): Promise; + + /** + * Get the list of packages in a Python Environment. + * + * @param environment The Python Environment for which the list of packages is required. + * @returns The list of packages in the Python Environment. + */ + getPackages(environment: PythonEnvironment): Promise; + + /** + * Event raised when the list of packages in a Python Environment changes. + * @see {@link DidChangePackagesEventArgs} + */ + onDidChangePackages: Event; +} + +export interface PythonPackageItemApi { + /** + * Create a package item from the provided package info. + * + * @param info The package info. + * @param environment The Python Environment in which the package is installed. + * @param manager The package manager that installed the package. + * @returns The package item. + */ + createPackageItem(info: PackageInfo, environment: PythonEnvironment, manager: PackageManager): Package; +} + +export interface PythonPackageManagementApi { + /** + * Install/Uninstall packages into a Python Environment. + * + * @param environment The Python Environment into which packages are to be installed. + * @param packages The packages to install. + * @param options Options for installing packages. + */ + managePackages(environment: PythonEnvironment, options: PackageManagementOptions): Promise; +} + +export interface PythonPackageManagerApi + extends PythonPackageManagerRegistrationApi, + PythonPackageGetterApi, + PythonPackageManagementApi, + PythonPackageItemApi {} + +export interface PythonProjectCreationApi { + /** + * Register a Python project creator. + * + * @param creator The project creator to register. + * @returns A disposable that can be used to unregister the project creator. + * @see {@link PythonProjectCreator} + */ + registerPythonProjectCreator(creator: PythonProjectCreator): Disposable; +} +export interface PythonProjectGetterApi { + /** + * Get all python projects. + */ + getPythonProjects(): readonly PythonProject[]; + + /** + * Get the python project for a given URI. + * + * @param uri The URI of the project + * @returns The project or `undefined` if not found. + */ + getPythonProject(uri: Uri): PythonProject | undefined; +} + +export interface PythonProjectModifyApi { + /** + * Add a python project or projects to the list of projects. + * + * @param projects The project or projects to add. + */ + addPythonProject(projects: PythonProject | PythonProject[]): void; + + /** + * Remove a python project from the list of projects. + * + * @param project The project to remove. + */ + removePythonProject(project: PythonProject): void; + + /** + * Event raised when python projects are added or removed. + * @see {@link DidChangePythonProjectsEventArgs} + */ + onDidChangePythonProjects: Event; +} + +/** + * The API for interacting with Python projects. A project in python is any folder or file that is a contained + * in some manner. For example, a PEP-723 compliant file can be treated as a project. A folder with a `pyproject.toml`, + * or just python files can be treated as a project. All this allows you to do is set a python environment for that project. + * + * By default all `vscode.workspace.workspaceFolders` are treated as projects. + */ +export interface PythonProjectApi extends PythonProjectCreationApi, PythonProjectGetterApi, PythonProjectModifyApi {} + +export interface PythonTerminalCreateOptions extends TerminalOptions { + /** + * Whether to disable activation on create. + */ + disableActivation?: boolean; +} + +export interface PythonTerminalCreateApi { + /** + * Creates a terminal and activates any (activatable) environment for the terminal. + * + * @param environment The Python environment to activate. + * @param options Options for creating the terminal. + * + * Note: Non-activatable environments have no effect on the terminal. + */ + createTerminal(environment: PythonEnvironment, options: PythonTerminalCreateOptions): Promise; +} + +/** + * Options for running a Python script or module in a terminal. + * + * Example: + * * Running Script: `python myscript.py --arg1` + * ```typescript + * { + * args: ["myscript.py", "--arg1"] + * } + * ``` + * * Running a module: `python -m my_module --arg1` + * ```typescript + * { + * args: ["-m", "my_module", "--arg1"] + * } + * ``` + */ +export interface PythonTerminalExecutionOptions { + /** + * Current working directory for the terminal. This in only used to create the terminal. + */ + cwd: string | Uri; + + /** + * Arguments to pass to the python executable. + */ + args?: string[]; + + /** + * Set `true` to show the terminal. + */ + show?: boolean; +} + +export interface PythonTerminalRunApi { + /** + * Runs a Python script or module in a terminal. This API will create a terminal if one is not available to use. + * If a terminal is available, it will be used to run the script or module. + * + * Note: + * - If you restart VS Code, this will create a new terminal, this is a limitation of VS Code. + * - If you close the terminal, this will create a new terminal. + * - In cases of multi-root/project scenario, it will create a separate terminal for each project. + */ + runInTerminal(environment: PythonEnvironment, options: PythonTerminalExecutionOptions): Promise; + + /** + * Runs a Python script or module in a dedicated terminal. This API will create a terminal if one is not available to use. + * If a terminal is available, it will be used to run the script or module. This terminal will be dedicated to the script, + * and selected based on the `terminalKey`. + * + * @param terminalKey A unique key to identify the terminal. For scripts you can use the Uri of the script file. + */ + runInDedicatedTerminal( + terminalKey: Uri | string, + environment: PythonEnvironment, + options: PythonTerminalExecutionOptions, + ): Promise; +} + +/** + * Options for running a Python task. + * + * Example: + * * Running Script: `python myscript.py --arg1` + * ```typescript + * { + * args: ["myscript.py", "--arg1"] + * } + * ``` + * * Running a module: `python -m my_module --arg1` + * ```typescript + * { + * args: ["-m", "my_module", "--arg1"] + * } + * ``` + */ +export interface PythonTaskExecutionOptions { + /** + * Name of the task to run. + */ + name: string; + + /** + * Arguments to pass to the python executable. + */ + args: string[]; + + /** + * The Python project to use for the task. + */ + project?: PythonProject; + + /** + * Current working directory for the task. Default is the project directory for the script being run. + */ + cwd?: string; + + /** + * Environment variables to set for the task. + */ + env?: { [key: string]: string }; +} + +export interface PythonTaskRunApi { + /** + * Run a Python script or module as a task. + * + */ + runAsTask(environment: PythonEnvironment, options: PythonTaskExecutionOptions): Promise; +} + +/** + * Options for running a Python script or module in the background. + */ +export interface PythonBackgroundRunOptions { + /** + * The Python environment to use for running the script or module. + */ + args: string[]; + + /** + * Current working directory for the script or module. Default is the project directory for the script being run. + */ + cwd?: string; + + /** + * Environment variables to set for the script or module. + */ + env?: { [key: string]: string | undefined }; +} +export interface PythonBackgroundRunApi { + /** + * Run a Python script or module in the background. This API will create a new process to run the script or module. + */ + runInBackground(environment: PythonEnvironment, options: PythonBackgroundRunOptions): Promise; +} + +export interface PythonExecutionApi + extends PythonTerminalCreateApi, + PythonTerminalRunApi, + PythonTaskRunApi, + PythonBackgroundRunApi {} + +/** + * Event arguments for when the monitored `.env` files or any other sources change. + */ +export interface DidChangeEnvironmentVariablesEventArgs { + /** + * The URI of the file that changed. No `Uri` means a non-file source of environment variables changed. + */ + uri?: Uri; + + /** + * The type of change that occurred. + */ + changeTye: FileChangeType; +} + +export interface PythonEnvironmentVariablesApi { + /** + * Get environment variables for a workspace. This picks up `.env` file from the root of the + * workspace. + * + * Order of overrides: + * 1. `baseEnvVar` if given or `process.env` + * 2. `.env` file from the "python.envFile" setting in the workspace. + * 3. `.env` file at the root of the python project. + * 4. `overrides` in the order provided. + * + * @param uri The URI of the project, workspace or a file in a for which environment variables are required. + * @param overrides Additional environment variables to override the defaults. + * @param baseEnvVar The base environment variables that should be used as a starting point. + */ + getEnvironmentVariables( + uri: Uri, + overrides?: ({ [key: string]: string | undefined } | Uri)[], + baseEnvVar?: { [key: string]: string | undefined }, + ): Promise<{ [key: string]: string | undefined }>; + + /** + * Event raised when `.env` file changes or any other monitored source of env variable changes. + */ + onDidChangeEnvironmentVariables: Event; +} + +/** + * The API for interacting with Python environments, package managers, and projects. + */ +export interface PythonEnvironmentApi + extends PythonEnvironmentManagerApi, + PythonPackageManagerApi, + PythonProjectApi, + PythonExecutionApi, + PythonEnvironmentVariablesApi {} diff --git a/src/client/environmentApi.ts b/src/client/environmentApi.ts index 3e846eb2772f..ecd8eef21845 100644 --- a/src/client/environmentApi.ts +++ b/src/client/environmentApi.ts @@ -9,9 +9,9 @@ import { Architecture } from './common/utils/platform'; import { IServiceContainer } from './ioc/types'; import { PythonEnvInfo, PythonEnvKind, PythonEnvType } from './pythonEnvironments/base/info'; import { getEnvPath } from './pythonEnvironments/base/info/env'; -import { IDiscoveryAPI } from './pythonEnvironments/base/locator'; +import { IDiscoveryAPI, ProgressReportStage } from './pythonEnvironments/base/locator'; import { IPythonExecutionFactory } from './common/process/types'; -import { traceError, traceVerbose } from './logging'; +import { traceError, traceInfo, traceVerbose } from './logging'; import { isParentPath, normCasePath } from './common/platform/fs-paths'; import { sendTelemetryEvent } from './telemetry'; import { EventName } from './telemetry/constants'; @@ -26,12 +26,15 @@ import { EnvironmentTools, EnvironmentType, EnvironmentVariablesChangeEvent, - IExtensionApi, + PythonExtension, RefreshOptions, ResolvedEnvironment, Resource, -} from './apiTypes'; +} from './api/types'; import { buildEnvironmentCreationApi } from './pythonEnvironments/creation/createEnvApi'; +import { EnvironmentKnownCache } from './environmentKnownCache'; +import type { JupyterPythonEnvironmentApi } from './jupyter/jupyterIntegration'; +import { noop } from './common/utils/misc'; type ActiveEnvironmentChangeEvent = { resource: WorkspaceFolder | undefined; @@ -39,7 +42,13 @@ type ActiveEnvironmentChangeEvent = { }; const onDidActiveInterpreterChangedEvent = new EventEmitter(); +const previousEnvMap = new Map(); export function reportActiveInterpreterChanged(e: ActiveEnvironmentChangeEvent): void { + const oldPath = previousEnvMap.get(e.resource?.uri.fsPath ?? ''); + if (oldPath === e.path) { + return; + } + previousEnvMap.set(e.resource?.uri.fsPath ?? '', e.path); onDidActiveInterpreterChangedEvent.fire({ id: getEnvID(e.path), path: e.path, resource: e.resource }); reportActiveInterpreterChangedDeprecated({ path: e.path, resource: e.resource?.uri }); } @@ -114,35 +123,88 @@ function filterUsingVSCodeContext(e: PythonEnvInfo) { export function buildEnvironmentApi( discoveryApi: IDiscoveryAPI, serviceContainer: IServiceContainer, -): IExtensionApi['environments'] { + jupyterPythonEnvsApi: JupyterPythonEnvironmentApi, +): PythonExtension['environments'] { const interpreterPathService = serviceContainer.get(IInterpreterPathService); const configService = serviceContainer.get(IConfigurationService); const disposables = serviceContainer.get(IDisposableRegistry); const extensions = serviceContainer.get(IExtensions); const envVarsProvider = serviceContainer.get(IEnvironmentVariablesProvider); + let knownCache: EnvironmentKnownCache; + + function initKnownCache() { + const knownEnvs = discoveryApi + .getEnvs() + .filter((e) => filterUsingVSCodeContext(e)) + .map((e) => updateReference(e)); + return new EnvironmentKnownCache(knownEnvs); + } function sendApiTelemetry(apiName: string, args?: unknown) { extensions .determineExtensionFromCallStack() .then((info) => { - sendTelemetryEvent(EventName.PYTHON_ENVIRONMENTS_API, undefined, { - apiName, - extensionId: info.extensionId, - }); + const p = Math.random(); + if (p <= 0.001) { + // Only send API telemetry 1% of the time, as it can be chatty. + sendTelemetryEvent(EventName.PYTHON_ENVIRONMENTS_API, undefined, { + apiName, + extensionId: info.extensionId, + }); + } traceVerbose(`Extension ${info.extensionId} accessed ${apiName} with args: ${JSON.stringify(args)}`); }) .ignoreErrors(); } + + function getActiveEnvironmentPath(resource?: Resource) { + resource = resource && 'uri' in resource ? resource.uri : resource; + const jupyterEnv = + resource && jupyterPythonEnvsApi.getPythonEnvironment + ? jupyterPythonEnvsApi.getPythonEnvironment(resource) + : undefined; + if (jupyterEnv) { + traceVerbose('Python Environment returned from Jupyter', resource?.fsPath, jupyterEnv.id); + return { + id: jupyterEnv.id, + path: jupyterEnv.path, + }; + } + const path = configService.getSettings(resource).pythonPath; + const id = path === 'python' ? 'DEFAULT_PYTHON' : getEnvID(path); + return { + id, + path, + }; + } + disposables.push( + onDidActiveInterpreterChangedEvent.event((e) => { + let scope = 'global'; + if (e.resource) { + scope = e.resource instanceof Uri ? e.resource.fsPath : e.resource.uri.fsPath; + } + traceInfo(`Active interpreter [${scope}]: `, e.path); + }), + discoveryApi.onProgress((e) => { + if (e.stage === ProgressReportStage.discoveryFinished) { + knownCache = initKnownCache(); + } + }), discoveryApi.onChanged((e) => { const env = e.new ?? e.old; if (!env || !filterUsingVSCodeContext(env)) { // Filter out environments that are not in the current workspace. return; } + if (!knownCache) { + knownCache = initKnownCache(); + } if (e.old) { if (e.new) { + const newEnv = updateReference(e.new); + knownCache.updateEnv(convertEnvInfo(e.old), newEnv); traceVerbose('Python API env change detected', env.id, 'update'); - onEnvironmentsChanged.fire({ type: 'update', env: convertEnvInfoAndGetReference(e.new) }); + onEnvironmentsChanged.fire({ type: 'update', env: newEnv }); reportInterpretersChanged([ { path: getEnvPath(e.new.executable.filename, e.new.location).path, @@ -150,8 +212,10 @@ export function buildEnvironmentApi( }, ]); } else { + const oldEnv = updateReference(e.old); + knownCache.updateEnv(oldEnv, undefined); traceVerbose('Python API env change detected', env.id, 'remove'); - onEnvironmentsChanged.fire({ type: 'remove', env: convertEnvInfoAndGetReference(e.old) }); + onEnvironmentsChanged.fire({ type: 'remove', env: oldEnv }); reportInterpretersChanged([ { path: getEnvPath(e.old.executable.filename, e.old.location).path, @@ -160,8 +224,10 @@ export function buildEnvironmentApi( ]); } } else if (e.new) { + const newEnv = updateReference(e.new); + knownCache.addEnv(newEnv); traceVerbose('Python API env change detected', env.id, 'add'); - onEnvironmentsChanged.fire({ type: 'add', env: convertEnvInfoAndGetReference(e.new) }); + onEnvironmentsChanged.fire({ type: 'add', env: newEnv }); reportInterpretersChanged([ { path: getEnvPath(e.new.executable.filename, e.new.location).path, @@ -178,9 +244,22 @@ export function buildEnvironmentApi( }), onEnvironmentsChanged, onEnvironmentVariablesChanged, + jupyterPythonEnvsApi.onDidChangePythonEnvironment + ? jupyterPythonEnvsApi.onDidChangePythonEnvironment((e) => { + const jupyterEnv = getActiveEnvironmentPath(e); + onDidActiveInterpreterChangedEvent.fire({ + id: jupyterEnv.id, + path: jupyterEnv.path, + resource: e, + }); + }, undefined) + : { dispose: noop }, ); + if (!knownCache!) { + knownCache = initKnownCache(); + } - const environmentApi: IExtensionApi['environments'] = { + const environmentApi: PythonExtension['environments'] = { getEnvironmentVariables: (resource?: Resource) => { sendApiTelemetry('getEnvironmentVariables'); resource = resource && 'uri' in resource ? resource.uri : resource; @@ -192,13 +271,7 @@ export function buildEnvironmentApi( }, getActiveEnvironmentPath(resource?: Resource) { sendApiTelemetry('getActiveEnvironmentPath'); - resource = resource && 'uri' in resource ? resource.uri : resource; - const path = configService.getSettings(resource).pythonPath; - const id = path === 'python' ? 'DEFAULT_PYTHON' : getEnvID(path); - return { - id, - path, - }; + return getActiveEnvironmentPath(resource); }, updateActiveEnvironmentPath(env: Environment | EnvironmentPath | string, resource?: Resource): Promise { sendApiTelemetry('updateActiveEnvironmentPath'); @@ -234,11 +307,9 @@ export function buildEnvironmentApi( return resolveEnvironment(path, discoveryApi); }, get known(): Environment[] { - sendApiTelemetry('known'); - return discoveryApi - .getEnvs() - .filter((e) => filterUsingVSCodeContext(e)) - .map((e) => convertEnvInfoAndGetReference(e)); + // Do not send telemetry for "known", as this may be called 1000s of times so it can significant: + // sendApiTelemetry('known'); + return knownCache.envs; }, async refreshEnvironments(options?: RefreshOptions) { if (!workspace.isTrusted) { @@ -318,6 +389,8 @@ function convertKind(kind: PythonEnvKind): EnvironmentTools | undefined { return 'Pipenv'; case PythonEnvKind.Poetry: return 'Poetry'; + case PythonEnvKind.Hatch: + return 'Hatch'; case PythonEnvKind.VirtualEnvWrapper: return 'VirtualEnvWrapper'; case PythonEnvKind.VirtualEnv: @@ -351,7 +424,7 @@ export function convertEnvInfo(env: PythonEnvInfo): Environment { return convertedEnv as Environment; } -function convertEnvInfoAndGetReference(env: PythonEnvInfo): Environment { +function updateReference(env: PythonEnvInfo): Environment { return getEnvReference(convertEnvInfo(env)); } diff --git a/src/client/environmentKnownCache.ts b/src/client/environmentKnownCache.ts new file mode 100644 index 000000000000..287f5bab343f --- /dev/null +++ b/src/client/environmentKnownCache.ts @@ -0,0 +1,37 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { Environment } from './api/types'; + +/** + * Workaround temp cache until types are consolidated. + */ +export class EnvironmentKnownCache { + private _envs: Environment[] = []; + + constructor(envs: Environment[]) { + this._envs = envs; + } + + public get envs(): Environment[] { + return this._envs; + } + + public addEnv(env: Environment): void { + const found = this._envs.find((e) => env.id === e.id); + if (!found) { + this._envs.push(env); + } + } + + public updateEnv(oldValue: Environment, newValue: Environment | undefined): void { + const index = this._envs.findIndex((e) => oldValue.id === e.id); + if (index !== -1) { + if (newValue === undefined) { + this._envs.splice(index, 1); + } else { + this._envs[index] = newValue; + } + } + } +} diff --git a/src/client/extension.ts b/src/client/extension.ts index 5fcb63e2d322..c3fb2a3ab3b0 100644 --- a/src/client/extension.ts +++ b/src/client/extension.ts @@ -6,10 +6,6 @@ if ((Reflect as any).metadata === undefined) { require('reflect-metadata'); } -// Initialize source maps (this must never be moved up nor further down). -import { initialize } from './sourceMapSupport'; -initialize(require('vscode')); - //=============================================== // We start tracking the extension's startup time at this point. The // locations at which we record various Intervals are marked below in @@ -41,11 +37,16 @@ import { sendErrorTelemetry, sendStartupTelemetry } from './startupTelemetry'; import { IStartupDurations } from './types'; import { runAfterActivation } from './common/utils/runAfterActivation'; import { IInterpreterService } from './interpreter/contracts'; -import { IExtensionApi } from './apiTypes'; +import { PythonExtension } from './api/types'; import { WorkspaceService } from './common/application/workspace'; import { disposeAll } from './common/utils/resourceLifecycle'; import { ProposedExtensionAPI } from './proposedApiTypes'; import { buildProposedApi } from './proposedApi'; +import { GLOBAL_PERSISTENT_KEYS } from './common/persistentState'; +import { registerTools } from './chat'; +import { IRecommendedEnvironmentService } from './interpreter/configuration/types'; +import { registerTypes as unitTestsRegisterTypes } from './testing/serviceRegistry'; +import { registerTestCommands } from './testing/main'; durations.codeLoadingTime = stopWatch.elapsedTime; @@ -58,11 +59,13 @@ let activatedServiceContainer: IServiceContainer | undefined; ///////////////////////////// // public functions -export async function activate(context: IExtensionContext): Promise { - let api: IExtensionApi; +export async function activate(context: IExtensionContext): Promise { + let api: PythonExtension; let ready: Promise; let serviceContainer: IServiceContainer; + let isFirstSession: boolean | undefined; try { + isFirstSession = context.globalState.get(GLOBAL_PERSISTENT_KEYS, []).length === 0; const workspaceService = new WorkspaceService(); context.subscriptions.push( workspaceService.onDidGrantWorkspaceTrust(async () => { @@ -79,8 +82,7 @@ export async function activate(context: IExtensionContext): Promise, IServiceContainer]> { +): Promise<[PythonExtension & ProposedExtensionAPI, Promise, IServiceContainer]> { // Add anything that we got from initializing logs to dispose. context.subscriptions.push(...logDispose); const activationDeferred = createDeferred(); displayProgress(activationDeferred.promise); startupDurations.startActivateTime = startupStopWatch.elapsedTime; + const activationStopWatch = new StopWatch(); //=============================================== // activation starts here @@ -120,6 +123,11 @@ async function activateUnsafe( // Note standard utils especially experiment and platform code are fundamental to the extension // and should be available before we activate anything else.Hence register them first. initializeStandard(ext); + + // Register test services and commands early to prevent race conditions. + unitTestsRegisterTypes(ext.legacyIOC.serviceManager); + registerTestCommands(activatedServiceContainer); + // We need to activate experiments before initializing components as objects are created or not created based on experiments. const experimentService = activatedServiceContainer.get(IExperimentService); // This guarantees that all experiment information has loaded & all telemetry will contain experiment info. @@ -127,7 +135,7 @@ async function activateUnsafe( const components = await initializeComponents(ext); // Then we finish activating. - const componentsActivated = await activateComponents(ext, components); + const componentsActivated = await activateComponents(ext, components, activationStopWatch); activateFeatures(ext, components); const nonBlocking = componentsActivated.map((r) => r.fullyReady); @@ -163,6 +171,10 @@ async function activateUnsafe( components.pythonEnvs, ); const proposedApi = buildProposedApi(components.pythonEnvs, ext.legacyIOC.serviceContainer); + registerTools(context, components.pythonEnvs, api.environments, ext.legacyIOC.serviceContainer); + ext.legacyIOC.serviceContainer + .get(IRecommendedEnvironmentService) + .registerEnvApi(api.environments); return [{ ...api, ...proposedApi }, activationPromise, ext.legacyIOC.serviceContainer]; } diff --git a/src/client/extensionActivation.ts b/src/client/extensionActivation.ts index ba7bdf61c8f2..57bcb8237eeb 100644 --- a/src/client/extensionActivation.ts +++ b/src/client/extensionActivation.ts @@ -3,42 +3,29 @@ 'use strict'; -import { debug, DebugConfigurationProvider, DebugConfigurationProviderTriggerKind, languages, window } from 'vscode'; +import { DebugConfigurationProvider, debug, languages, window } from 'vscode'; import { registerTypes as activationRegisterTypes } from './activation/serviceRegistry'; import { IExtensionActivationManager } from './activation/types'; import { registerTypes as appRegisterTypes } from './application/serviceRegistry'; import { IApplicationDiagnostics } from './application/types'; import { IApplicationEnvironment, ICommandManager, IWorkspaceService } from './common/application/types'; -import { Commands, PYTHON, PYTHON_LANGUAGE, UseProposedApi } from './common/constants'; +import { Commands, PYTHON_LANGUAGE, UseProposedApi } from './common/constants'; import { registerTypes as installerRegisterTypes } from './common/installer/serviceRegistry'; import { IFileSystem } from './common/platform/types'; -import { - IConfigurationService, - IDisposableRegistry, - IExtensions, - IInterpreterPathService, - ILogOutputChannel, - IPathUtils, -} from './common/types'; +import { IConfigurationService, IDisposableRegistry, IExtensions, ILogOutputChannel, IPathUtils } from './common/types'; import { noop } from './common/utils/misc'; -import { DebuggerTypeName } from './debugger/constants'; import { registerTypes as debugConfigurationRegisterTypes } from './debugger/extension/serviceRegistry'; -import { IDebugConfigurationService, IDynamicDebugConfigurationService } from './debugger/extension/types'; -import { registerTypes as formattersRegisterTypes } from './formatters/serviceRegistry'; +import { IDebugConfigurationService } from './debugger/extension/types'; import { IInterpreterService } from './interpreter/contracts'; import { getLanguageConfiguration } from './language/languageConfiguration'; -import { LinterCommands } from './linters/linterCommands'; -import { registerTypes as lintersRegisterTypes } from './linters/serviceRegistry'; -import { PythonFormattingEditProvider } from './providers/formatProvider'; import { ReplProvider } from './providers/replProvider'; import { registerTypes as providersRegisterTypes } from './providers/serviceRegistry'; import { TerminalProvider } from './providers/terminalProvider'; import { setExtensionInstallTelemetryProperties } from './telemetry/extensionInstallTelemetry'; import { registerTypes as tensorBoardRegisterTypes } from './tensorBoard/serviceRegistry'; import { registerTypes as commonRegisterTerminalTypes } from './terminals/serviceRegistry'; -import { ICodeExecutionManager, ITerminalAutoActivation } from './terminals/types'; -import { registerTypes as unitTestsRegisterTypes } from './testing/serviceRegistry'; +import { ICodeExecutionHelper, ICodeExecutionManager, ITerminalAutoActivation } from './terminals/types'; // components import * as pythonEnvironments from './pythonEnvironments'; @@ -50,16 +37,23 @@ import { DebugService } from './common/application/debugService'; import { DebugSessionEventDispatcher } from './debugger/extension/hooks/eventHandlerDispatcher'; import { IDebugSessionEventHandlers } from './debugger/extension/hooks/types'; import { WorkspaceService } from './common/application/workspace'; -import { DynamicPythonDebugConfigurationService } from './debugger/extension/configuration/dynamicdebugConfigurationService'; -import { registerCreateEnvironmentFeatures } from './pythonEnvironments/creation/createEnvApi'; -import { IInterpreterQuickPick } from './interpreter/configuration/types'; -import { registerInstallFormatterPrompt } from './providers/prompts/installFormatterPrompt'; -import { registerPyProjectTomlCreateEnvFeatures } from './pythonEnvironments/creation/pyprojectTomlCreateEnv'; +import { IInterpreterQuickPick, IPythonPathUpdaterServiceManager } from './interpreter/configuration/types'; +import { registerAllCreateEnvironmentFeatures } from './pythonEnvironments/creation/registrations'; +import { registerCreateEnvironmentTriggers } from './pythonEnvironments/creation/createEnvironmentTrigger'; +import { initializePersistentStateForTriggers } from './common/persistentState'; +import { DebuggerTypeName } from './debugger/constants'; +import { StopWatch } from './common/utils/stopWatch'; +import { registerReplCommands, registerReplExecuteOnEnter, registerStartNativeReplCommand } from './repl/replCommands'; +import { registerTriggerForTerminalREPL } from './terminals/codeExecution/terminalReplWatcher'; +import { registerPythonStartup } from './terminals/pythonStartup'; +import { registerPixiFeatures } from './pythonEnvironments/common/environmentManagers/pixi'; +import { registerCustomTerminalLinkProvider } from './terminals/pythonStartupLinkProvider'; export async function activateComponents( // `ext` is passed to any extra activation funcs. ext: ExtensionState, components: Components, + startupStopWatch: StopWatch, ): Promise { // Note that each activation returns a promise that resolves // when that activation completes. However, it might have started @@ -77,7 +71,7 @@ export async function activateComponents( // activate them in parallel with the other components. // https://github.com/microsoft/vscode-python/issues/15380 // These will go away eventually once everything is refactored into components. - const legacyActivationResult = await activateLegacy(ext); + const legacyActivationResult = await activateLegacy(ext, startupStopWatch); const workspaceService = new WorkspaceService(); if (!workspaceService.isTrusted) { return [legacyActivationResult]; @@ -93,12 +87,25 @@ export function activateFeatures(ext: ExtensionState, _components: Components): const interpreterQuickPick: IInterpreterQuickPick = ext.legacyIOC.serviceContainer.get( IInterpreterQuickPick, ); - const interpreterPathService: IInterpreterPathService = ext.legacyIOC.serviceContainer.get( - IInterpreterPathService, + const interpreterService: IInterpreterService = ext.legacyIOC.serviceContainer.get( + IInterpreterService, ); const pathUtils = ext.legacyIOC.serviceContainer.get(IPathUtils); - registerCreateEnvironmentFeatures(ext.disposables, interpreterQuickPick, interpreterPathService, pathUtils); - registerPyProjectTomlCreateEnvFeatures(ext.disposables); + registerPixiFeatures(ext.disposables); + registerAllCreateEnvironmentFeatures( + ext.disposables, + interpreterQuickPick, + ext.legacyIOC.serviceContainer.get(IPythonPathUpdaterServiceManager), + interpreterService, + pathUtils, + ); + const executionHelper = ext.legacyIOC.serviceContainer.get(ICodeExecutionHelper); + const commandManager = ext.legacyIOC.serviceContainer.get(ICommandManager); + registerTriggerForTerminalREPL(ext.disposables); + registerStartNativeReplCommand(ext.disposables, interpreterService); + registerReplCommands(ext.disposables, interpreterService, executionHelper, commandManager); + registerReplExecuteOnEnter(ext.disposables, interpreterService, commandManager); + registerCustomTerminalLinkProvider(ext.disposables); } /// ////////////////////////// @@ -110,8 +117,8 @@ export function activateFeatures(ext: ExtensionState, _components: Components): // init and activation: move them to activateComponents(). // See https://github.com/microsoft/vscode-python/issues/10454. -async function activateLegacy(ext: ExtensionState): Promise { - const { context, legacyIOC } = ext; +async function activateLegacy(ext: ExtensionState, startupStopWatch: StopWatch): Promise { + const { legacyIOC } = ext; const { serviceManager, serviceContainer } = legacyIOC; // register "services" @@ -124,9 +131,6 @@ async function activateLegacy(ext: ExtensionState): Promise { const { enableProposedApi } = applicationEnv.packageJson; serviceManager.addSingletonInstance(UseProposedApi, enableProposedApi); // Feature specific registrations. - unitTestsRegisterTypes(serviceManager); - lintersRegisterTypes(serviceManager); - formattersRegisterTypes(serviceManager); installerRegisterTypes(serviceManager); commonRegisterTerminalTypes(serviceManager); debugConfigurationRegisterTypes(serviceManager); @@ -135,7 +139,6 @@ async function activateLegacy(ext: ExtensionState): Promise { const extensions = serviceContainer.get(IExtensions); await setDefaultLanguageServer(extensions, serviceManager); - const configuration = serviceManager.get(IConfigurationService); // Settings are dependent on Experiment service, so we need to initialize it after experiments are activated. serviceContainer.get(IConfigurationService).getSettings().register(); @@ -158,7 +161,6 @@ async function activateLegacy(ext: ExtensionState): Promise { const handlers = serviceManager.getAll(IDebugSessionEventHandlers); const dispatcher = new DebugSessionEventDispatcher(handlers, DebugService.instance, disposables); dispatcher.registerEventHandlers(); - const outputChannel = serviceManager.get(ILogOutputChannel); disposables.push(cmdManager.registerCommand(Commands.ViewOutput, () => outputChannel.show())); cmdManager.executeCommand('setContext', 'python.vscode.channel', applicationEnv.channel).then(noop, noop); @@ -166,44 +168,25 @@ async function activateLegacy(ext: ExtensionState): Promise { serviceContainer.get(IApplicationDiagnostics).register(); serviceManager.get(ITerminalAutoActivation).register(); - const pythonSettings = configuration.getSettings(); - - serviceManager.get(ICodeExecutionManager).registerCommands(); - disposables.push(new LinterCommands(serviceManager)); + await registerPythonStartup(ext.context); - if ( - pythonSettings && - pythonSettings.formatting && - pythonSettings.formatting.provider !== 'internalConsole' - ) { - const formatProvider = new PythonFormattingEditProvider(context, serviceContainer); - disposables.push(languages.registerDocumentFormattingEditProvider(PYTHON, formatProvider)); - disposables.push(languages.registerDocumentRangeFormattingEditProvider(PYTHON, formatProvider)); - } + serviceManager.get(ICodeExecutionManager).registerCommands(); disposables.push(new ReplProvider(serviceContainer)); const terminalProvider = new TerminalProvider(serviceContainer); terminalProvider.initialize(window.activeTerminal).ignoreErrors(); - disposables.push(terminalProvider); serviceContainer .getAll(IDebugConfigurationService) .forEach((debugConfigProvider) => { disposables.push(debug.registerDebugConfigurationProvider(DebuggerTypeName, debugConfigProvider)); }); + disposables.push(terminalProvider); - // register a dynamic configuration provider for 'python' debug type - disposables.push( - debug.registerDebugConfigurationProvider( - DebuggerTypeName, - serviceContainer.get(IDynamicDebugConfigurationService), - DebugConfigurationProviderTriggerKind.Dynamic, - ), - ); - - registerInstallFormatterPrompt(serviceContainer); + registerCreateEnvironmentTriggers(disposables); + initializePersistentStateForTriggers(ext.context); } } @@ -212,7 +195,7 @@ async function activateLegacy(ext: ExtensionState): Promise { const manager = serviceContainer.get(IExtensionActivationManager); disposables.push(manager); - const activationPromise = manager.activate(); + const activationPromise = manager.activate(startupStopWatch); return { fullyReady: activationPromise }; } diff --git a/src/client/extensionInit.ts b/src/client/extensionInit.ts index 851bc943cb8d..b161643d2d97 100644 --- a/src/client/extensionInit.ts +++ b/src/client/extensionInit.ts @@ -5,7 +5,6 @@ import { Container } from 'inversify'; import { Disposable, Memento, window } from 'vscode'; -import { instance, mock } from 'ts-mockito'; import { registerTypes as platformRegisterTypes } from './common/platform/serviceRegistry'; import { registerTypes as processRegisterTypes } from './common/process/serviceRegistry'; import { registerTypes as commonRegisterTypes } from './common/serviceRegistry'; @@ -16,7 +15,6 @@ import { IExtensionContext, IMemento, ILogOutputChannel, - ITestOutputChannel, WORKSPACE_MEMENTO, } from './common/types'; import { registerTypes as variableRegisterTypes } from './common/variables/serviceRegistry'; @@ -29,7 +27,6 @@ import * as pythonEnvironments from './pythonEnvironments'; import { IDiscoveryAPI } from './pythonEnvironments/base/locator'; import { registerLogger } from './logging'; import { OutputChannelLogger } from './logging/outputChannelLogger'; -import { WorkspaceService } from './common/application/workspace'; // The code in this module should do nothing more complex than register // objects to DI and simple init (e.g. no side effects). That implies @@ -57,16 +54,7 @@ export function initializeGlobals( disposables.push(standardOutputChannel); disposables.push(registerLogger(new OutputChannelLogger(standardOutputChannel))); - const workspaceService = new WorkspaceService(); - const unitTestOutChannel = - workspaceService.isVirtualWorkspace || !workspaceService.isTrusted - ? // Do not create any test related output UI when using virtual workspaces. - instance(mock()) - : window.createOutputChannel(OutputChannelNames.pythonTest); - disposables.push(unitTestOutChannel); - serviceManager.addSingletonInstance(ILogOutputChannel, standardOutputChannel); - serviceManager.addSingletonInstance(ITestOutputChannel, unitTestOutChannel); return { context, diff --git a/src/client/formatters/autoPep8Formatter.ts b/src/client/formatters/autoPep8Formatter.ts deleted file mode 100644 index bf1285a60b58..000000000000 --- a/src/client/formatters/autoPep8Formatter.ts +++ /dev/null @@ -1,43 +0,0 @@ -import * as vscode from 'vscode'; -import { Product } from '../common/installer/productInstaller'; -import { IConfigurationService } from '../common/types'; -import { StopWatch } from '../common/utils/stopWatch'; -import { IServiceContainer } from '../ioc/types'; -import { sendTelemetryWhenDone } from '../telemetry'; -import { EventName } from '../telemetry/constants'; -import { BaseFormatter } from './baseFormatter'; - -export class AutoPep8Formatter extends BaseFormatter { - constructor(serviceContainer: IServiceContainer) { - super('autopep8', Product.autopep8, serviceContainer); - } - - public formatDocument( - document: vscode.TextDocument, - options: vscode.FormattingOptions, - token: vscode.CancellationToken, - range?: vscode.Range, - ): Thenable { - const stopWatch = new StopWatch(); - const settings = this.serviceContainer - .get(IConfigurationService) - .getSettings(document.uri); - const hasCustomArgs = - Array.isArray(settings.formatting.autopep8Args) && settings.formatting.autopep8Args.length > 0; - const formatSelection = range ? !range.isEmpty : false; - - const autoPep8Args = ['--diff']; - if (formatSelection) { - autoPep8Args.push( - ...['--line-range', (range!.start.line + 1).toString(), (range!.end.line + 1).toString()], - ); - } - const promise = super.provideDocumentFormattingEdits(document, options, token, autoPep8Args); - sendTelemetryWhenDone(EventName.FORMAT, promise, stopWatch, { - tool: 'autopep8', - hasCustomArgs, - formatSelection, - }); - return promise; - } -} diff --git a/src/client/formatters/baseFormatter.ts b/src/client/formatters/baseFormatter.ts deleted file mode 100644 index 64e7d15a3d45..000000000000 --- a/src/client/formatters/baseFormatter.ts +++ /dev/null @@ -1,149 +0,0 @@ -import * as path from 'path'; -import * as vscode from 'vscode'; -import { IApplicationShell, IWorkspaceService } from '../common/application/types'; -import '../common/extensions'; -import { isNotInstalledError } from '../common/helpers'; -import { IFileSystem } from '../common/platform/types'; -import { IPythonToolExecutionService } from '../common/process/types'; -import { IDisposableRegistry, IInstaller, Product } from '../common/types'; -import { isNotebookCell } from '../common/utils/misc'; -import { IServiceContainer } from '../ioc/types'; -import { traceError } from '../logging'; -import { getTempFileWithDocumentContents, getTextEditsFromPatch } from './../common/editor'; -import { IFormatterHelper } from './types'; -import { IInstallFormatterPrompt } from '../providers/prompts/types'; - -export abstract class BaseFormatter { - protected readonly workspace: IWorkspaceService; - private readonly helper: IFormatterHelper; - private errorShown: boolean = false; - - constructor(public Id: string, private product: Product, protected serviceContainer: IServiceContainer) { - this.helper = serviceContainer.get(IFormatterHelper); - this.workspace = serviceContainer.get(IWorkspaceService); - } - - public abstract formatDocument( - document: vscode.TextDocument, - options: vscode.FormattingOptions, - token: vscode.CancellationToken, - range?: vscode.Range, - ): Thenable; - protected getDocumentPath(document: vscode.TextDocument, fallbackPath: string) { - if (path.basename(document.uri.fsPath) === document.uri.fsPath) { - return fallbackPath; - } - return path.dirname(document.fileName); - } - protected getWorkspaceUri(document: vscode.TextDocument) { - const workspaceFolder = this.workspace.getWorkspaceFolder(document.uri); - if (workspaceFolder) { - return workspaceFolder.uri; - } - const folders = this.workspace.workspaceFolders; - if (Array.isArray(folders) && folders.length > 0) { - return folders[0].uri; - } - return vscode.Uri.file(__dirname); - } - protected async provideDocumentFormattingEdits( - document: vscode.TextDocument, - _options: vscode.FormattingOptions, - token: vscode.CancellationToken, - args: string[], - cwd?: string, - ): Promise { - if (typeof cwd !== 'string' || cwd.length === 0) { - cwd = this.getWorkspaceUri(document).fsPath; - } - - // autopep8 and yapf have the ability to read from the process input stream and return the formatted code out of the output stream. - // However they don't support returning the diff of the formatted text when reading data from the input stream. - // Yet getting text formatted that way avoids having to create a temporary file, however the diffing will have - // to be done here in node (extension), i.e. extension CPU, i.e. less responsive solution. - // Also, always create temp files for Notebook cells. - const tempFile = await this.createTempFile(document); - if (this.checkCancellation(document.fileName, tempFile, token)) { - return []; - } - - const executionInfo = this.helper.getExecutionInfo(this.product, args, document.uri); - executionInfo.args.push(tempFile); - const pythonToolsExecutionService = this.serviceContainer.get( - IPythonToolExecutionService, - ); - const promise = pythonToolsExecutionService - .exec(executionInfo, { cwd, throwOnStdErr: false, token }, document.uri) - .then((output) => output.stdout) - .then((data) => { - if (this.checkCancellation(document.fileName, tempFile, token)) { - return [] as vscode.TextEdit[]; - } - return getTextEditsFromPatch(document.getText(), data); - }) - .catch((error) => { - if (this.checkCancellation(document.fileName, tempFile, token)) { - return [] as vscode.TextEdit[]; - } - - this.handleError(this.Id, error, document.uri).catch(() => {}); - return [] as vscode.TextEdit[]; - }) - .then((edits) => { - this.deleteTempFile(document.fileName, tempFile).ignoreErrors(); - return edits; - }); - - const appShell = this.serviceContainer.get(IApplicationShell); - const disposableRegistry = this.serviceContainer.get(IDisposableRegistry); - const disposable = appShell.setStatusBarMessage(`Formatting with ${this.Id}`, promise); - disposableRegistry.push(disposable); - return promise; - } - - protected async handleError(_expectedFileName: string, error: Error, resource?: vscode.Uri) { - if (isNotInstalledError(error)) { - const prompt = this.serviceContainer.get(IInstallFormatterPrompt); - if (!(await prompt.showInstallFormatterPrompt(resource))) { - const installer = this.serviceContainer.get(IInstaller); - const isInstalled = await installer.isInstalled(this.product, resource); - if (!isInstalled && !this.errorShown) { - traceError( - `\nPlease install '${this.Id}' into your environment.`, - "\nIf you don't want to use it you can turn it off or use another formatter in the settings.", - ); - this.errorShown = true; - } - } - } - - traceError(`Formatting with ${this.Id} failed:\n${error}`); - } - - /** - * Always create a temporary file when formatting notebook cells. - * This is because there is no physical file associated with notebook cells (they are all virtual). - */ - private async createTempFile(document: vscode.TextDocument): Promise { - const fs = this.serviceContainer.get(IFileSystem); - return document.isDirty || isNotebookCell(document) - ? getTempFileWithDocumentContents(document, fs) - : document.fileName; - } - - private deleteTempFile(originalFile: string, tempFile: string): Promise { - if (originalFile !== tempFile) { - const fs = this.serviceContainer.get(IFileSystem); - return fs.deleteFile(tempFile); - } - return Promise.resolve(); - } - - private checkCancellation(originalFile: string, tempFile: string, token?: vscode.CancellationToken): boolean { - if (token && token.isCancellationRequested) { - this.deleteTempFile(originalFile, tempFile).ignoreErrors(); - return true; - } - return false; - } -} diff --git a/src/client/formatters/blackFormatter.ts b/src/client/formatters/blackFormatter.ts deleted file mode 100644 index 0a8109e163e0..000000000000 --- a/src/client/formatters/blackFormatter.ts +++ /dev/null @@ -1,55 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; - -import * as path from 'path'; -import * as vscode from 'vscode'; -import { IApplicationShell } from '../common/application/types'; -import { Product } from '../common/installer/productInstaller'; -import { IConfigurationService } from '../common/types'; -import { noop } from '../common/utils/misc'; -import { StopWatch } from '../common/utils/stopWatch'; -import { IServiceContainer } from '../ioc/types'; -import { sendTelemetryWhenDone } from '../telemetry'; -import { EventName } from '../telemetry/constants'; -import { BaseFormatter } from './baseFormatter'; - -export class BlackFormatter extends BaseFormatter { - constructor(serviceContainer: IServiceContainer) { - super('black', Product.black, serviceContainer); - } - - public async formatDocument( - document: vscode.TextDocument, - options: vscode.FormattingOptions, - token: vscode.CancellationToken, - range?: vscode.Range, - ): Promise { - const stopWatch = new StopWatch(); - const settings = this.serviceContainer - .get(IConfigurationService) - .getSettings(document.uri); - const hasCustomArgs = Array.isArray(settings.formatting.blackArgs) && settings.formatting.blackArgs.length > 0; - const formatSelection = range ? !range.isEmpty : false; - - if (formatSelection) { - const shell = this.serviceContainer.get(IApplicationShell); - // Black does not support partial formatting on purpose. - shell - .showErrorMessage(vscode.l10n.t('Black does not support the "Format Selection" command')) - .then(noop, noop); - return []; - } - - const blackArgs = ['--diff', '--quiet']; - - if (path.extname(document.fileName) === '.pyi') { - blackArgs.push('--pyi'); - } - - const promise = super.provideDocumentFormattingEdits(document, options, token, blackArgs); - sendTelemetryWhenDone(EventName.FORMAT, promise, stopWatch, { tool: 'black', hasCustomArgs, formatSelection }); - return promise; - } -} diff --git a/src/client/formatters/dummyFormatter.ts b/src/client/formatters/dummyFormatter.ts deleted file mode 100644 index b4fdba9fbc0f..000000000000 --- a/src/client/formatters/dummyFormatter.ts +++ /dev/null @@ -1,19 +0,0 @@ -import * as vscode from 'vscode'; -import { Product } from '../common/types'; -import { IServiceContainer } from '../ioc/types'; -import { BaseFormatter } from './baseFormatter'; - -export class DummyFormatter extends BaseFormatter { - constructor(serviceContainer: IServiceContainer) { - super('none', Product.yapf, serviceContainer); - } - - public formatDocument( - _document: vscode.TextDocument, - _options: vscode.FormattingOptions, - _token: vscode.CancellationToken, - _range?: vscode.Range, - ): Thenable { - return Promise.resolve([]); - } -} diff --git a/src/client/formatters/helper.ts b/src/client/formatters/helper.ts deleted file mode 100644 index ac305b51e785..000000000000 --- a/src/client/formatters/helper.ts +++ /dev/null @@ -1,53 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -import { inject, injectable } from 'inversify'; -import * as path from 'path'; -import { Uri } from 'vscode'; -import { ExecutionInfo, IConfigurationService, IFormattingSettings, Product } from '../common/types'; -import { IServiceContainer } from '../ioc/types'; -import { FormatterId, FormatterSettingsPropertyNames, IFormatterHelper } from './types'; - -@injectable() -export class FormatterHelper implements IFormatterHelper { - constructor(@inject(IServiceContainer) private serviceContainer: IServiceContainer) {} - public translateToId(formatter: Product): FormatterId { - switch (formatter) { - case Product.autopep8: - return 'autopep8'; - case Product.black: - return 'black'; - case Product.yapf: - return 'yapf'; - default: { - throw new Error(`Unrecognized Formatter '${formatter}'`); - } - } - } - public getSettingsPropertyNames(formatter: Product): FormatterSettingsPropertyNames { - const id = this.translateToId(formatter); - return { - argsName: `${id}Args` as keyof IFormattingSettings, - pathName: `${id}Path` as keyof IFormattingSettings, - }; - } - public getExecutionInfo(formatter: Product, customArgs: string[], resource?: Uri): ExecutionInfo { - const settings = this.serviceContainer.get(IConfigurationService).getSettings(resource); - const names = this.getSettingsPropertyNames(formatter); - - const execPath = settings.formatting[names.pathName] as string; - let args: string[] = Array.isArray(settings.formatting[names.argsName]) - ? (settings.formatting[names.argsName] as string[]) - : []; - args = args.concat(customArgs); - - let moduleName: string | undefined; - - // If path information is not available, then treat it as a module, - if (path.basename(execPath) === execPath) { - moduleName = execPath; - } - - return { execPath, moduleName, args, product: formatter }; - } -} diff --git a/src/client/formatters/serviceRegistry.ts b/src/client/formatters/serviceRegistry.ts deleted file mode 100644 index 196e6c806b5f..000000000000 --- a/src/client/formatters/serviceRegistry.ts +++ /dev/null @@ -1,10 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -import { IServiceManager } from '../ioc/types'; -import { FormatterHelper } from './helper'; -import { IFormatterHelper } from './types'; - -export function registerTypes(serviceManager: IServiceManager) { - serviceManager.addSingleton(IFormatterHelper, FormatterHelper); -} diff --git a/src/client/formatters/types.ts b/src/client/formatters/types.ts deleted file mode 100644 index 7f4bcf5b7524..000000000000 --- a/src/client/formatters/types.ts +++ /dev/null @@ -1,20 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -import { Uri } from 'vscode'; -import { ExecutionInfo, IFormattingSettings, Product } from '../common/types'; - -export const IFormatterHelper = Symbol('IFormatterHelper'); - -export type FormatterId = 'autopep8' | 'black' | 'yapf'; - -export type FormatterSettingsPropertyNames = { - argsName: keyof IFormattingSettings; - pathName: keyof IFormattingSettings; -}; - -export interface IFormatterHelper { - translateToId(formatter: Product): FormatterId; - getSettingsPropertyNames(formatter: Product): FormatterSettingsPropertyNames; - getExecutionInfo(formatter: Product, customArgs: string[], resource?: Uri): ExecutionInfo; -} diff --git a/src/client/formatters/yapfFormatter.ts b/src/client/formatters/yapfFormatter.ts deleted file mode 100644 index 08729a97694f..000000000000 --- a/src/client/formatters/yapfFormatter.ts +++ /dev/null @@ -1,38 +0,0 @@ -import * as vscode from 'vscode'; -import { IConfigurationService, Product } from '../common/types'; -import { StopWatch } from '../common/utils/stopWatch'; -import { IServiceContainer } from '../ioc/types'; -import { sendTelemetryWhenDone } from '../telemetry'; -import { EventName } from '../telemetry/constants'; -import { BaseFormatter } from './baseFormatter'; - -export class YapfFormatter extends BaseFormatter { - constructor(serviceContainer: IServiceContainer) { - super('yapf', Product.yapf, serviceContainer); - } - - public formatDocument( - document: vscode.TextDocument, - options: vscode.FormattingOptions, - token: vscode.CancellationToken, - range?: vscode.Range, - ): Thenable { - const stopWatch = new StopWatch(); - const settings = this.serviceContainer - .get(IConfigurationService) - .getSettings(document.uri); - const hasCustomArgs = Array.isArray(settings.formatting.yapfArgs) && settings.formatting.yapfArgs.length > 0; - const formatSelection = range ? !range.isEmpty : false; - - const yapfArgs = ['--diff']; - if (formatSelection && range !== undefined) { - yapfArgs.push(...['--lines', `${range.start.line + 1}-${range.end.line + 1}`]); - } - // Yapf starts looking for config file starting from the file path. - const fallbarFolder = this.getWorkspaceUri(document).fsPath; - const cwd = this.getDocumentPath(document, fallbarFolder); - const promise = super.provideDocumentFormattingEdits(document, options, token, yapfArgs, cwd); - sendTelemetryWhenDone(EventName.FORMAT, promise, stopWatch, { tool: 'yapf', hasCustomArgs, formatSelection }); - return promise; - } -} diff --git a/src/client/interpreter/activation/service.ts b/src/client/interpreter/activation/service.ts index e5da57227b19..f47575cad60b 100644 --- a/src/client/interpreter/activation/service.ts +++ b/src/client/interpreter/activation/service.ts @@ -19,8 +19,8 @@ import { ICurrentProcess, IDisposable, Resource } from '../../common/types'; import { sleep } from '../../common/utils/async'; import { InMemoryCache } from '../../common/utils/cacheUtils'; import { OSType } from '../../common/utils/platform'; -import { IEnvironmentVariablesProvider } from '../../common/variables/types'; -import { EnvironmentType, PythonEnvironment } from '../../pythonEnvironments/info'; +import { EnvironmentVariables, IEnvironmentVariablesProvider } from '../../common/variables/types'; +import { EnvironmentType, PythonEnvironment, virtualEnvTypes } from '../../pythonEnvironments/info'; import { sendTelemetryEvent } from '../../telemetry'; import { EventName } from '../../telemetry/constants'; import { IInterpreterService } from '../contracts'; @@ -38,6 +38,8 @@ import { Conda } from '../../pythonEnvironments/common/environmentManagers/conda import { StopWatch } from '../../common/utils/stopWatch'; import { identifyShellFromShellPath } from '../../common/terminal/shellDetectors/baseShellDetector'; import { getSearchPathEnvVarNames } from '../../common/utils/exec'; +import { cache } from '../../common/utils/decorators'; +import { getRunPixiPythonCommand } from '../../pythonEnvironments/common/environmentManagers/pixi'; const ENVIRONMENT_PREFIX = 'e8b39361-0157-4923-80e1-22d70d46dee6'; const CACHE_DURATION = 10 * 60 * 1000; @@ -154,11 +156,11 @@ export class EnvironmentActivationService implements IEnvironmentActivationServi } // Cache only if successful, else keep trying & failing if necessary. - const cache = new InMemoryCache(CACHE_DURATION); + const memCache = new InMemoryCache(CACHE_DURATION); return this.getActivatedEnvironmentVariablesImpl(resource, interpreter, allowExceptions, shell) .then((vars) => { - cache.data = vars; - this.activatedEnvVariablesCache.set(cacheKey, cache); + memCache.data = vars; + this.activatedEnvVariablesCache.set(cacheKey, memCache); sendTelemetryEvent( EventName.PYTHON_INTERPRETER_ACTIVATION_ENVIRONMENT_VARIABLES, stopWatch.elapsedTime, @@ -176,6 +178,35 @@ export class EnvironmentActivationService implements IEnvironmentActivationServi }); } + @cache(-1, true) + public async getProcessEnvironmentVariables(resource: Resource, shell?: string): Promise { + // Try to get the process environment variables using Python by printing variables, that can be little different + // from `process.env` and is preferred when calculating diff. + const globalInterpreters = this.interpreterService + .getInterpreters() + .filter((i) => !virtualEnvTypes.includes(i.envType)); + const interpreterPath = + globalInterpreters.length > 0 && globalInterpreters[0] ? globalInterpreters[0].path : 'python'; + try { + const [args, parse] = internalScripts.printEnvVariables(); + args.forEach((arg, i) => { + args[i] = arg.toCommandArgumentForPythonExt(); + }); + const command = `${interpreterPath} ${args.join(' ')}`; + const processService = await this.processServiceFactory.create(resource, { doNotUseCustomEnvs: true }); + const result = await processService.shellExec(command, { + shell, + timeout: ENVIRONMENT_TIMEOUT, + maxBuffer: 1000 * 1000, + throwOnStdErr: false, + }); + const returnedEnv = this.parseEnvironmentOutput(result.stdout, parse); + return returnedEnv ?? process.env; + } catch (ex) { + return process.env; + } + } + public async getEnvironmentActivationShellCommands( resource: Resource, interpreter?: PythonEnvironment, @@ -222,6 +253,11 @@ export class EnvironmentActivationService implements IEnvironmentActivationServi // Using environment prefix isn't needed as the marker script already takes care of it. command = [...pythonArgv, ...args].map((arg) => arg.toCommandArgumentForPythonExt()).join(' '); } + } else if (interpreter?.envType === EnvironmentType.Pixi) { + const pythonArgv = await getRunPixiPythonCommand(interpreter.path); + if (pythonArgv) { + command = [...pythonArgv, ...args].map((arg) => arg.toCommandArgumentForPythonExt()).join(' '); + } } if (!command) { const activationCommands = await this.helper.getEnvironmentActivationShellCommands( @@ -229,9 +265,11 @@ export class EnvironmentActivationService implements IEnvironmentActivationServi shellInfo.shellType, interpreter, ); - traceVerbose(`Activation Commands received ${activationCommands} for shell ${shellInfo.shell}`); + traceVerbose( + `Activation Commands received ${activationCommands} for shell ${shellInfo.shell}, resource ${resource?.fsPath} and interpreter ${interpreter?.path}`, + ); if (!activationCommands || !Array.isArray(activationCommands) || activationCommands.length === 0) { - if (interpreter?.envType === EnvironmentType.Venv) { + if (interpreter && [EnvironmentType.Venv, EnvironmentType.Pyenv].includes(interpreter?.envType)) { const key = getSearchPathEnvVarNames()[0]; if (env[key]) { env[key] = `${path.dirname(interpreter.path)}${path.delimiter}${env[key]}`; @@ -243,11 +281,18 @@ export class EnvironmentActivationService implements IEnvironmentActivationServi } return undefined; } + const commandSeparator = [TerminalShellType.powershell, TerminalShellType.powershellCore].includes( + shellInfo.shellType, + ) + ? ';' + : '&&'; // Run the activate command collect the environment from it. - const activationCommand = fixActivationCommands(activationCommands).join(' && '); + const activationCommand = fixActivationCommands(activationCommands).join(` ${commandSeparator} `); // In order to make sure we know where the environment output is, // put in a dummy echo we can look for - command = `${activationCommand} && echo '${ENVIRONMENT_PREFIX}' && python ${args.join(' ')}`; + command = `${activationCommand} ${commandSeparator} echo '${ENVIRONMENT_PREFIX}' ${commandSeparator} python ${args.join( + ' ', + )}`; } // Make sure python warnings don't interfere with getting the environment. However @@ -255,7 +300,6 @@ export class EnvironmentActivationService implements IEnvironmentActivationServi const oldWarnings = env[PYTHON_WARNINGS]; env[PYTHON_WARNINGS] = 'ignore'; - traceVerbose(`${hasCustomEnvVars ? 'Has' : 'No'} Custom Env Vars`); traceVerbose(`Activating Environment to capture Environment variables, ${command}`); // Do some wrapping of the call. For two reasons: @@ -292,7 +336,15 @@ export class EnvironmentActivationService implements IEnvironmentActivationServi } if (result.stderr) { if (returnedEnv) { - traceWarn('Got env variables but with errors', result.stderr); + traceWarn('Got env variables but with errors', result.stderr, returnedEnv); + if ( + result.stderr.includes('running scripts is disabled') || + result.stderr.includes('FullyQualifiedErrorId : UnauthorizedAccess') + ) { + throw new Error( + `Skipping returned result when powershell execution is disabled, stderr ${result.stderr} for ${command}`, + ); + } } else { throw new Error(`StdErr from ShellExec, ${result.stderr} for ${command}`); } @@ -334,9 +386,9 @@ export class EnvironmentActivationService implements IEnvironmentActivationServi return undefined; } + // eslint-disable-next-line class-methods-use-this @traceDecoratorError('Failed to parse Environment variables') @traceDecoratorVerbose('parseEnvironmentOutput', TraceOptions.None) - // eslint-disable-next-line class-methods-use-this private parseEnvironmentOutput(output: string, parse: (out: string) => NodeJS.ProcessEnv | undefined) { if (output.indexOf(ENVIRONMENT_PREFIX) === -1) { return parse(output); diff --git a/src/client/interpreter/activation/terminalEnvVarCollectionService.ts b/src/client/interpreter/activation/terminalEnvVarCollectionService.ts deleted file mode 100644 index 26852303d099..000000000000 --- a/src/client/interpreter/activation/terminalEnvVarCollectionService.ts +++ /dev/null @@ -1,220 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -import * as path from 'path'; -import { inject, injectable } from 'inversify'; -import { ProgressOptions, ProgressLocation, MarkdownString, WorkspaceFolder } from 'vscode'; -import { pathExists } from 'fs-extra'; -import { IExtensionActivationService } from '../../activation/types'; -import { IApplicationShell, IApplicationEnvironment, IWorkspaceService } from '../../common/application/types'; -import { inTerminalEnvVarExperiment } from '../../common/experiments/helpers'; -import { IPlatformService } from '../../common/platform/types'; -import { identifyShellFromShellPath } from '../../common/terminal/shellDetectors/baseShellDetector'; -import { - IExtensionContext, - IExperimentService, - Resource, - IDisposableRegistry, - IConfigurationService, - IPathUtils, -} from '../../common/types'; -import { Deferred, createDeferred } from '../../common/utils/async'; -import { Interpreters } from '../../common/utils/localize'; -import { traceDecoratorVerbose, traceVerbose } from '../../logging'; -import { IInterpreterService } from '../contracts'; -import { defaultShells } from './service'; -import { IEnvironmentActivationService } from './types'; -import { EnvironmentType } from '../../pythonEnvironments/info'; - -@injectable() -export class TerminalEnvVarCollectionService implements IExtensionActivationService { - public readonly supportedWorkspaceTypes = { - untrustedWorkspace: false, - virtualWorkspace: false, - }; - - private deferred: Deferred | undefined; - - private registeredOnce = false; - - private previousEnvVars = _normCaseKeys(process.env); - - constructor( - @inject(IPlatformService) private readonly platform: IPlatformService, - @inject(IInterpreterService) private interpreterService: IInterpreterService, - @inject(IExtensionContext) private context: IExtensionContext, - @inject(IApplicationShell) private shell: IApplicationShell, - @inject(IExperimentService) private experimentService: IExperimentService, - @inject(IApplicationEnvironment) private applicationEnvironment: IApplicationEnvironment, - @inject(IDisposableRegistry) private disposables: IDisposableRegistry, - @inject(IEnvironmentActivationService) private environmentActivationService: IEnvironmentActivationService, - @inject(IWorkspaceService) private workspaceService: IWorkspaceService, - @inject(IConfigurationService) private readonly configurationService: IConfigurationService, - @inject(IPathUtils) private readonly pathUtils: IPathUtils, - ) {} - - public async activate(resource: Resource): Promise { - if (!inTerminalEnvVarExperiment(this.experimentService)) { - this.context.environmentVariableCollection.clear(); - await this.handleMicroVenv(resource); - if (!this.registeredOnce) { - this.interpreterService.onDidChangeInterpreter( - async (r) => { - await this.handleMicroVenv(r); - }, - this, - this.disposables, - ); - this.registeredOnce = true; - } - return; - } - if (!this.registeredOnce) { - this.interpreterService.onDidChangeInterpreter( - async (r) => { - this.showProgress(); - await this._applyCollection(r).ignoreErrors(); - this.hideProgress(); - }, - this, - this.disposables, - ); - this.applicationEnvironment.onDidChangeShell( - async (shell: string) => { - this.showProgress(); - // Pass in the shell where known instead of relying on the application environment, because of bug - // on VSCode: https://github.com/microsoft/vscode/issues/160694 - await this._applyCollection(undefined, shell).ignoreErrors(); - this.hideProgress(); - }, - this, - this.disposables, - ); - this.registeredOnce = true; - } - this._applyCollection(resource).ignoreErrors(); - } - - public async _applyCollection(resource: Resource, shell = this.applicationEnvironment.shell): Promise { - const workspaceFolder = this.getWorkspaceFolder(resource); - const settings = this.configurationService.getSettings(resource); - if (!settings.terminal.activateEnvironment) { - traceVerbose('Activating environments in terminal is disabled for', resource?.fsPath); - return; - } - const env = await this.environmentActivationService.getActivatedEnvironmentVariables( - resource, - undefined, - undefined, - shell, - ); - if (!env) { - const shellType = identifyShellFromShellPath(shell); - const defaultShell = defaultShells[this.platform.osType]; - if (defaultShell?.shellType !== shellType) { - // Commands to fetch env vars may fail in custom shells due to unknown reasons, in that case - // fallback to default shells as they are known to work better. - await this._applyCollection(resource, defaultShell?.shell); - return; - } - this.context.environmentVariableCollection.clear({ workspaceFolder }); - this.previousEnvVars = _normCaseKeys(process.env); - return; - } - const previousEnv = this.previousEnvVars; - this.previousEnvVars = env; - Object.keys(env).forEach((key) => { - const value = env[key]; - const prevValue = previousEnv[key]; - if (prevValue !== value) { - if (value !== undefined) { - traceVerbose(`Setting environment variable ${key} in collection to ${value}`); - this.context.environmentVariableCollection.replace(key, value, { workspaceFolder }); - } else { - traceVerbose(`Clearing environment variable ${key} from collection`); - this.context.environmentVariableCollection.delete(key, { workspaceFolder }); - } - } - }); - Object.keys(previousEnv).forEach((key) => { - // If the previous env var is not in the current env, clear it from collection. - if (!(key in env)) { - traceVerbose(`Clearing environment variable ${key} from collection`); - this.context.environmentVariableCollection.delete(key, { workspaceFolder }); - } - }); - const displayPath = this.pathUtils.getDisplayName(settings.pythonPath, workspaceFolder?.uri.fsPath); - const description = new MarkdownString(`${Interpreters.activateTerminalDescription} \`${displayPath}\``); - this.context.environmentVariableCollection.setDescription(description, { - workspaceFolder, - }); - } - - private async handleMicroVenv(resource: Resource) { - const workspaceFolder = this.getWorkspaceFolder(resource); - const interpreter = await this.interpreterService.getActiveInterpreter(resource); - if (interpreter?.envType === EnvironmentType.Venv) { - const activatePath = path.join(path.dirname(interpreter.path), 'activate'); - if (!(await pathExists(activatePath))) { - this.context.environmentVariableCollection.replace( - 'PATH', - `${path.dirname(interpreter.path)}${path.delimiter}${process.env.Path}`, - { - workspaceFolder, - }, - ); - return; - } - } - this.context.environmentVariableCollection.clear(); - } - - private getWorkspaceFolder(resource: Resource): WorkspaceFolder | undefined { - let workspaceFolder = this.workspaceService.getWorkspaceFolder(resource); - if ( - !workspaceFolder && - Array.isArray(this.workspaceService.workspaceFolders) && - this.workspaceService.workspaceFolders.length > 0 - ) { - [workspaceFolder] = this.workspaceService.workspaceFolders; - } - return workspaceFolder; - } - - @traceDecoratorVerbose('Display activating terminals') - private showProgress(): void { - if (!this.deferred) { - this.createProgress(); - } - } - - @traceDecoratorVerbose('Hide activating terminals') - private hideProgress(): void { - if (this.deferred) { - this.deferred.resolve(); - this.deferred = undefined; - } - } - - private createProgress() { - const progressOptions: ProgressOptions = { - location: ProgressLocation.Window, - title: Interpreters.activatingTerminals, - }; - this.shell.withProgress(progressOptions, () => { - this.deferred = createDeferred(); - return this.deferred.promise; - }); - } -} - -export function _normCaseKeys(env: NodeJS.ProcessEnv): NodeJS.ProcessEnv { - const result: NodeJS.ProcessEnv = {}; - Object.keys(env).forEach((key) => { - // `os.environ` script used to get env vars normalizes keys to upper case: - // https://github.com/python/cpython/issues/101754 - // So convert `process.env` keys to upper case to match. - result[key.toUpperCase()] = env[key]; - }); - return result; -} diff --git a/src/client/interpreter/activation/types.ts b/src/client/interpreter/activation/types.ts index d8e4ae16dbca..e00ef9b62b3f 100644 --- a/src/client/interpreter/activation/types.ts +++ b/src/client/interpreter/activation/types.ts @@ -4,10 +4,12 @@ 'use strict'; import { Resource } from '../../common/types'; +import { EnvironmentVariables } from '../../common/variables/types'; import { PythonEnvironment } from '../../pythonEnvironments/info'; export const IEnvironmentActivationService = Symbol('IEnvironmentActivationService'); export interface IEnvironmentActivationService { + getProcessEnvironmentVariables(resource: Resource, shell?: string): Promise; getActivatedEnvironmentVariables( resource: Resource, interpreter?: PythonEnvironment, diff --git a/src/client/interpreter/autoSelection/index.ts b/src/client/interpreter/autoSelection/index.ts index a57577c8c918..5ad5362e8210 100644 --- a/src/client/interpreter/autoSelection/index.ts +++ b/src/client/interpreter/autoSelection/index.ts @@ -6,11 +6,13 @@ import { inject, injectable } from 'inversify'; import { Event, EventEmitter, Uri } from 'vscode'; import { IWorkspaceService } from '../../common/application/types'; +import { DiscoveryUsingWorkers } from '../../common/experiments/groups'; import '../../common/extensions'; import { IFileSystem } from '../../common/platform/types'; -import { IPersistentState, IPersistentStateFactory, Resource } from '../../common/types'; +import { IExperimentService, IPersistentState, IPersistentStateFactory, Resource } from '../../common/types'; import { createDeferred, Deferred } from '../../common/utils/async'; import { compareSemVerLikeVersions } from '../../pythonEnvironments/base/info/pythonVersion'; +import { ProgressReportStage } from '../../pythonEnvironments/base/locator'; import { PythonEnvironment } from '../../pythonEnvironments/info'; import { sendTelemetryEvent } from '../../telemetry'; import { EventName } from '../../telemetry/constants'; @@ -44,6 +46,7 @@ export class InterpreterAutoSelectionService implements IInterpreterAutoSelectio @inject(IInterpreterComparer) private readonly envTypeComparer: IInterpreterComparer, @inject(IInterpreterAutoSelectionProxyService) proxy: IInterpreterAutoSelectionProxyService, @inject(IInterpreterHelper) private readonly interpreterHelper: IInterpreterHelper, + @inject(IExperimentService) private readonly experimentService: IExperimentService, ) { proxy.registerInstance!(this); } @@ -181,6 +184,11 @@ export class InterpreterAutoSelectionService implements IInterpreterAutoSelectio return this.stateFactory.createWorkspacePersistentState(key, undefined); } + private getAutoSelectionQueriedOnceState(): IPersistentState { + const key = `autoSelectionInterpretersQueriedOnce`; + return this.stateFactory.createGlobalPersistentState(key, undefined); + } + /** * Auto-selection logic: * 1. If there are cached interpreters (not the first session in this workspace) @@ -194,17 +202,45 @@ export class InterpreterAutoSelectionService implements IInterpreterAutoSelectio private async autoselectInterpreterWithLocators(resource: Resource): Promise { // Do not perform a full interpreter search if we already have cached interpreters for this workspace. const queriedState = this.getAutoSelectionInterpretersQueryState(resource); - if (queriedState.value !== true && resource) { + const globalQueriedState = this.getAutoSelectionQueriedOnceState(); + if (globalQueriedState.value && queriedState.value !== true && resource) { await this.interpreterService.triggerRefresh({ searchLocations: { roots: [resource], doNotIncludeNonRooted: true }, }); } - await this.interpreterService.refreshPromise; - const interpreters = this.interpreterService.getInterpreters(resource); + await this.envTypeComparer.initialize(resource); + const inExperiment = this.experimentService.inExperimentSync(DiscoveryUsingWorkers.experiment); const workspaceUri = this.interpreterHelper.getActiveWorkspaceUri(resource); + let recommendedInterpreter: PythonEnvironment | undefined; + if (inExperiment) { + if (!globalQueriedState.value) { + // Global interpreters are loaded the first time an extension loads, after which we don't need to + // wait on global interpreter promise refresh. + // Do not wait for validation of all interpreters to finish, we only need to validate the recommended interpreter. + await this.interpreterService.getRefreshPromise({ stage: ProgressReportStage.allPathsDiscovered }); + } + let interpreters = this.interpreterService.getInterpreters(resource); + + recommendedInterpreter = this.envTypeComparer.getRecommended(interpreters, workspaceUri?.folderUri); + const details = recommendedInterpreter + ? await this.interpreterService.getInterpreterDetails(recommendedInterpreter.path) + : undefined; + if (!details || !recommendedInterpreter) { + await this.interpreterService.refreshPromise; // Interpreter is invalid, wait for all of validation to finish. + interpreters = this.interpreterService.getInterpreters(resource); + recommendedInterpreter = this.envTypeComparer.getRecommended(interpreters, workspaceUri?.folderUri); + } + } else { + if (!globalQueriedState.value) { + // Global interpreters are loaded the first time an extension loads, after which we don't need to + // wait on global interpreter promise refresh. + await this.interpreterService.refreshPromise; + } + const interpreters = this.interpreterService.getInterpreters(resource); - const recommendedInterpreter = this.envTypeComparer.getRecommended(interpreters, workspaceUri?.folderUri); + recommendedInterpreter = this.envTypeComparer.getRecommended(interpreters, workspaceUri?.folderUri); + } if (!recommendedInterpreter) { return; } @@ -215,6 +251,7 @@ export class InterpreterAutoSelectionService implements IInterpreterAutoSelectio } queriedState.updateValue(true); + globalQueriedState.updateValue(true); this.didAutoSelectedInterpreterEmitter.fire(); } diff --git a/src/client/interpreter/autoSelection/types.ts b/src/client/interpreter/autoSelection/types.ts index 8833c6cac371..91d0224717d4 100644 --- a/src/client/interpreter/autoSelection/types.ts +++ b/src/client/interpreter/autoSelection/types.ts @@ -14,9 +14,6 @@ export const IInterpreterAutoSelectionProxyService = Symbol('IInterpreterAutoSel * However, the class that reads python Path, must first give preference to selected interpreter. * But all classes everywhere make use of python settings! * Solution - Use a proxy that does nothing first, but later the real instance is injected. - * - * @export - * @interface IInterpreterAutoSelectionProxyService */ export interface IInterpreterAutoSelectionProxyService { readonly onDidChangeAutoSelectedInterpreter: Event; diff --git a/src/client/interpreter/configuration/environmentTypeComparer.ts b/src/client/interpreter/configuration/environmentTypeComparer.ts index 0631bb594bfd..2e1013b7b5a8 100644 --- a/src/client/interpreter/configuration/environmentTypeComparer.ts +++ b/src/client/interpreter/configuration/environmentTypeComparer.ts @@ -6,10 +6,17 @@ import { Resource } from '../../common/types'; import { Architecture } from '../../common/utils/platform'; import { isActiveStateEnvironmentForWorkspace } from '../../pythonEnvironments/common/environmentManagers/activestate'; import { isParentPath } from '../../pythonEnvironments/common/externalDependencies'; -import { EnvironmentType, PythonEnvironment, virtualEnvTypes } from '../../pythonEnvironments/info'; +import { + EnvironmentType, + PythonEnvironment, + virtualEnvTypes, + workspaceVirtualEnvTypes, +} from '../../pythonEnvironments/info'; import { PythonVersion } from '../../pythonEnvironments/info/pythonVersion'; import { IInterpreterHelper } from '../contracts'; import { IInterpreterComparer } from './types'; +import { getActivePyenvForDirectory } from '../../pythonEnvironments/common/environmentManagers/pyenv'; +import { arePathsSame } from '../../common/platform/fs-paths'; export enum EnvLocationHeuristic { /** @@ -26,6 +33,8 @@ export enum EnvLocationHeuristic { export class EnvironmentTypeComparer implements IInterpreterComparer { private workspaceFolderPath: string; + private preferredPyenvInterpreterPath = new Map(); + constructor(@inject(IInterpreterHelper) private readonly interpreterHelper: IInterpreterHelper) { this.workspaceFolderPath = this.interpreterHelper.getActiveWorkspaceUri(undefined)?.folderUri.fsPath ?? ''; } @@ -54,6 +63,18 @@ export class EnvironmentTypeComparer implements IInterpreterComparer { return envLocationComparison; } + if (a.envType === EnvironmentType.Pyenv && b.envType === EnvironmentType.Pyenv) { + const preferredPyenv = this.preferredPyenvInterpreterPath.get(this.workspaceFolderPath); + if (preferredPyenv) { + if (arePathsSame(preferredPyenv, b.path)) { + return 1; + } + if (arePathsSame(preferredPyenv, a.path)) { + return -1; + } + } + } + // Check environment type. const envTypeComparison = compareEnvironmentType(a, b); if (envTypeComparison !== 0) { @@ -85,6 +106,16 @@ export class EnvironmentTypeComparer implements IInterpreterComparer { return nameA > nameB ? 1 : -1; } + public async initialize(resource: Resource): Promise { + const workspaceUri = this.interpreterHelper.getActiveWorkspaceUri(resource); + const cwd = workspaceUri?.folderUri.fsPath; + if (!cwd) { + return; + } + const preferredPyenvInterpreter = await getActivePyenvForDirectory(cwd); + this.preferredPyenvInterpreterPath.set(cwd, preferredPyenvInterpreter); + } + public getRecommended(interpreters: PythonEnvironment[], resource: Resource): PythonEnvironment | undefined { // When recommending an intepreter for a workspace, we either want to return a local one // or fallback on a globally-installed interpreter, and we don't want want to suggest a global environment @@ -105,8 +136,8 @@ export class EnvironmentTypeComparer implements IInterpreterComparer { if (getEnvLocationHeuristic(i, workspaceUri?.folderUri.fsPath || '') === EnvLocationHeuristic.Local) { return true; } - if (virtualEnvTypes.includes(i.envType)) { - // We're not sure if these envs were created for the workspace, so do not recommend them. + if (!workspaceVirtualEnvTypes.includes(i.envType) && virtualEnvTypes.includes(i.envType)) { + // These are global virtual envs so we're not sure if these envs were created for the workspace, skip them. return false; } if (i.version?.major === 2) { @@ -234,6 +265,16 @@ export function getEnvLocationHeuristic(environment: PythonEnvironment, workspac * Compare 2 environment types: return 0 if they are the same, -1 if a comes before b, 1 otherwise. */ function compareEnvironmentType(a: PythonEnvironment, b: PythonEnvironment): number { + if (!a.type && !b.type) { + // Unless one of them is pyenv interpreter, return 0 if two global interpreters are being compared. + if (a.envType === EnvironmentType.Pyenv && b.envType !== EnvironmentType.Pyenv) { + return -1; + } + if (a.envType !== EnvironmentType.Pyenv && b.envType === EnvironmentType.Pyenv) { + return 1; + } + return 0; + } const envTypeByPriority = getPrioritizedEnvironmentType(); return Math.sign(envTypeByPriority.indexOf(a.envType) - envTypeByPriority.indexOf(b.envType)); } @@ -244,6 +285,7 @@ function getPrioritizedEnvironmentType(): EnvironmentType[] { EnvironmentType.Poetry, EnvironmentType.Pipenv, EnvironmentType.VirtualEnvWrapper, + EnvironmentType.Hatch, EnvironmentType.Venv, EnvironmentType.VirtualEnv, EnvironmentType.ActiveState, diff --git a/src/client/interpreter/configuration/interpreterSelector/commands/base.ts b/src/client/interpreter/configuration/interpreterSelector/commands/base.ts index 6ed2dee36c89..6307e286dbfe 100644 --- a/src/client/interpreter/configuration/interpreterSelector/commands/base.ts +++ b/src/client/interpreter/configuration/interpreterSelector/commands/base.ts @@ -53,7 +53,7 @@ export abstract class BaseInterpreterSelectorCommand implements IExtensionSingle }, ]; } - if (!this.workspaceService.workspaceFile && workspaceFolders.length === 1) { + if (workspaceFolders.length === 1) { return [ { folderUri: workspaceFolders[0].uri, diff --git a/src/client/interpreter/configuration/interpreterSelector/commands/installPython/installPythonViaTerminal.ts b/src/client/interpreter/configuration/interpreterSelector/commands/installPython/installPythonViaTerminal.ts index 9da7284a3bea..3b4a6d428baa 100644 --- a/src/client/interpreter/configuration/interpreterSelector/commands/installPython/installPythonViaTerminal.ts +++ b/src/client/interpreter/configuration/interpreterSelector/commands/installPython/installPythonViaTerminal.ts @@ -94,7 +94,7 @@ export class InstallPythonViaTerminal implements IExtensionSingleActivationServi async function isPackageAvailable(packageManager: PackageManagers) { try { const which = require('which') as typeof whichTypes; - const resolvedPath = await which(packageManager); + const resolvedPath = await which.default(packageManager); traceVerbose(`Resolved path to ${packageManager} module:`, resolvedPath); return resolvedPath.trim().length > 0; } catch (ex) { diff --git a/src/client/interpreter/configuration/interpreterSelector/commands/resetInterpreter.ts b/src/client/interpreter/configuration/interpreterSelector/commands/resetInterpreter.ts index 82b40a3ff5e8..c10f90781adb 100644 --- a/src/client/interpreter/configuration/interpreterSelector/commands/resetInterpreter.ts +++ b/src/client/interpreter/configuration/interpreterSelector/commands/resetInterpreter.ts @@ -9,6 +9,8 @@ import { Commands } from '../../../../common/constants'; import { IConfigurationService, IPathUtils } from '../../../../common/types'; import { IPythonPathUpdaterServiceManager } from '../../types'; import { BaseInterpreterSelectorCommand } from './base'; +import { useEnvExtension } from '../../../../envExt/api.internal'; +import { resetInterpreterLegacy } from '../../../../envExt/api.legacy'; @injectable() export class ResetInterpreterCommand extends BaseInterpreterSelectorCommand { @@ -46,6 +48,9 @@ export class ResetInterpreterCommand extends BaseInterpreterSelectorCommand { const configTarget = targetConfig.configTarget; const wkspace = targetConfig.folderUri; await this.pythonPathUpdaterService.updatePythonPath(undefined, configTarget, 'ui', wkspace); + if (useEnvExtension()) { + await resetInterpreterLegacy(wkspace); + } }), ); } diff --git a/src/client/interpreter/configuration/interpreterSelector/commands/setInterpreter.ts b/src/client/interpreter/configuration/interpreterSelector/commands/setInterpreter.ts index c0876ff518dd..a629d1bc793c 100644 --- a/src/client/interpreter/configuration/interpreterSelector/commands/setInterpreter.ts +++ b/src/client/interpreter/configuration/interpreterSelector/commands/setInterpreter.ts @@ -46,11 +46,13 @@ import { ISpecialQuickPickItem, } from '../../types'; import { BaseInterpreterSelectorCommand } from './base'; - -const untildify = require('untildify'); +import { untildify } from '../../../../common/helpers'; +import { useEnvExtension } from '../../../../envExt/api.internal'; +import { setInterpreterLegacy } from '../../../../envExt/api.legacy'; +import { CreateEnvironmentResult } from '../../../../pythonEnvironments/creation/proposed.createEnvApis'; export type InterpreterStateArgs = { path?: string; workspace: Resource }; -type QuickPickType = IInterpreterQuickPickItem | ISpecialQuickPickItem | QuickPickItem; +export type QuickPickType = IInterpreterQuickPickItem | ISpecialQuickPickItem | QuickPickItem; function isInterpreterQuickPickItem(item: QuickPickType): item is IInterpreterQuickPickItem { return 'interpreter' in item; @@ -74,6 +76,8 @@ export namespace EnvGroups { export const Pyenv = 'Pyenv'; export const Venv = 'Venv'; export const Poetry = 'Poetry'; + export const Hatch = 'Hatch'; + export const Pixi = 'Pixi'; export const VirtualEnvWrapper = 'VirtualEnvWrapper'; export const ActiveState = 'ActiveState'; export const Recommended = Common.recommended; @@ -81,8 +85,13 @@ export namespace EnvGroups { @injectable() export class SetInterpreterCommand extends BaseInterpreterSelectorCommand implements IInterpreterQuickPick { + private readonly createEnvironmentSuggestion: QuickPickItem = { + label: `${Octicons.Add} ${InterpreterQuickPickList.create.label}`, + alwaysShow: true, + }; + private readonly manualEntrySuggestion: ISpecialQuickPickItem = { - label: `${Octicons.Add} ${InterpreterQuickPickList.enterPath.label}`, + label: `${Octicons.Folder} ${InterpreterQuickPickList.enterPath.label}`, alwaysShow: true, }; @@ -177,7 +186,7 @@ export class SetInterpreterCommand extends BaseInterpreterSelectorCommand implem items: suggestions, sortByLabel: !preserveOrderWhenFiltering, keepScrollPosition: true, - activeItem: this.getActiveItem(state.workspace, suggestions), // Use a promise here to ensure quickpick is initialized synchronously. + activeItem: (quickPick) => this.getActiveItem(state.workspace, quickPick), // Use a promise here to ensure quickpick is initialized synchronously. matchOnDetail: true, matchOnDescription: true, title, @@ -220,6 +229,14 @@ export class SetInterpreterCommand extends BaseInterpreterSelectorCommand implem } else if (selection.label === this.manualEntrySuggestion.label) { sendTelemetryEvent(EventName.SELECT_INTERPRETER_ENTER_OR_FIND); return this._enterOrBrowseInterpreterPath.bind(this); + } else if (selection.label === this.createEnvironmentSuggestion.label) { + const createdEnv = (await Promise.resolve( + this.commandManager.executeCommand(Commands.Create_Environment, { + showBackButton: false, + selectEnvironment: true, + }), + ).catch(noop)) as CreateEnvironmentResult | undefined; + state.path = createdEnv?.path; } else if (selection.label === this.noPythonInstalled.label) { this.commandManager.executeCommand(Commands.InstallPython).then(noop, noop); this.wasNoPythonInstalledItemClicked = true; @@ -237,7 +254,13 @@ export class SetInterpreterCommand extends BaseInterpreterSelectorCommand implem filter: ((i: PythonEnvironment) => boolean) | undefined, params?: InterpreterQuickPickParams, ): QuickPickType[] { - const suggestions: QuickPickType[] = [this.manualEntrySuggestion]; + const suggestions: QuickPickType[] = []; + if (params?.showCreateEnvironment) { + suggestions.push(this.createEnvironmentSuggestion, { label: '', kind: QuickPickItemKind.Separator }); + } + + suggestions.push(this.manualEntrySuggestion, { label: '', kind: QuickPickItemKind.Separator }); + const defaultInterpreterPathSuggestion = this.getDefaultInterpreterPathSuggestion(resource); if (defaultInterpreterPathSuggestion) { suggestions.push(defaultInterpreterPathSuggestion); @@ -277,8 +300,9 @@ export class SetInterpreterCommand extends BaseInterpreterSelectorCommand implem return getGroupedQuickPickItems(items, recommended, workspaceFolder?.uri.fsPath); } - private async getActiveItem(resource: Resource, suggestions: QuickPickType[]) { + private async getActiveItem(resource: Resource, quickPick: QuickPick) { const interpreter = await this.interpreterService.getActiveInterpreter(resource); + const suggestions = quickPick.items; const activeInterpreterItem = suggestions.find( (i) => isInterpreterQuickPickItem(i) && i.interpreter.id === interpreter?.id, ); @@ -339,7 +363,9 @@ export class SetInterpreterCommand extends BaseInterpreterSelectorCommand implem return false; }) : undefined; - quickPick.activeItems = activeItem ? [activeItem] : []; + if (activeItem) { + quickPick.activeItems = [activeItem]; + } } /** @@ -447,7 +473,6 @@ export class SetInterpreterCommand extends BaseInterpreterSelectorCommand implem } const areItemsGrouped = items.find((item) => isSeparatorItem(item) && item.label === EnvGroups.Recommended); const recommended = cloneDeep(suggestion); - recommended.label = `${Octicons.Star} ${recommended.label}`; recommended.description = areItemsGrouped ? // No need to add a tag as "Recommended" group already exists. recommended.description @@ -529,6 +554,7 @@ export class SetInterpreterCommand extends BaseInterpreterSelectorCommand implem openLabel: InterpreterQuickPickList.browsePath.openButtonLabel, canSelectMany: false, title: InterpreterQuickPickList.browsePath.title, + defaultUri: state.workspace, }); if (uris && uris.length > 0) { state.path = uris[0].fsPath; @@ -540,8 +566,14 @@ export class SetInterpreterCommand extends BaseInterpreterSelectorCommand implem return Promise.resolve(); } + /** + * @returns true when an interpreter was set, undefined if the user cancelled the quickpick. + */ @captureTelemetry(EventName.SELECT_INTERPRETER) - public async setInterpreter(): Promise { + public async setInterpreter(options?: { + hideCreateVenv?: boolean; + showBackButton?: boolean; + }): Promise { const targetConfig = await this.getConfigTargets(); if (!targetConfig) { return; @@ -550,13 +582,34 @@ export class SetInterpreterCommand extends BaseInterpreterSelectorCommand implem const wkspace = targetConfig[0].folderUri; const interpreterState: InterpreterStateArgs = { path: undefined, workspace: wkspace }; const multiStep = this.multiStepFactory.create(); - await multiStep.run((input, s) => this._pickInterpreter(input, s, undefined), interpreterState); - + try { + await multiStep.run( + (input, s) => + this._pickInterpreter(input, s, undefined, { + showCreateEnvironment: !options?.hideCreateVenv, + showBackButton: options?.showBackButton, + }), + interpreterState, + ); + } catch (ex) { + if (ex === InputFlowAction.back) { + // User clicked back button, so we need to return this action. + return { action: 'Back' }; + } + if (ex === InputFlowAction.cancel) { + // User clicked cancel button, so we need to return this action. + return { action: 'Cancel' }; + } + } if (interpreterState.path !== undefined) { // User may choose to have an empty string stored, so variable `interpreterState.path` may be // an empty string, in which case we should update. // Having the value `undefined` means user cancelled the quickpick, so we update nothing in that case. await this.pythonPathUpdaterService.updatePythonPath(interpreterState.path, configTarget, 'ui', wkspace); + if (useEnvExtension()) { + await setInterpreterLegacy(interpreterState.path, wkspace); + } + return { path: interpreterState.path }; } } @@ -657,3 +710,14 @@ function getGroup(item: IInterpreterQuickPickItem, workspacePath?: string) { return EnvGroups[item.interpreter.envType]; } } + +export type SelectEnvironmentResult = { + /** + * Path to the executable python in the environment + */ + readonly path?: string; + /* + * User action that resulted in exit from the create environment flow. + */ + readonly action?: 'Back' | 'Cancel'; +}; diff --git a/src/client/interpreter/configuration/interpreterSelector/interpreterSelector.ts b/src/client/interpreter/configuration/interpreterSelector/interpreterSelector.ts index 8c94abe2c8b4..6b33245bb907 100644 --- a/src/client/interpreter/configuration/interpreterSelector/interpreterSelector.ts +++ b/src/client/interpreter/configuration/interpreterSelector/interpreterSelector.ts @@ -5,7 +5,7 @@ import { inject, injectable } from 'inversify'; import { Disposable, Uri } from 'vscode'; -import { arePathsSame } from '../../../common/platform/fs-paths'; +import { arePathsSame, isParentPath } from '../../../common/platform/fs-paths'; import { IPathUtils, Resource } from '../../../common/types'; import { getEnvPath } from '../../../pythonEnvironments/base/info/env'; import { PythonEnvironment } from '../../../pythonEnvironments/info'; @@ -45,6 +45,13 @@ export class InterpreterSelector implements IInterpreterSelector { workspaceUri?: Uri, useDetailedName = false, ): IInterpreterQuickPickItem { + if (!useDetailedName) { + const workspacePath = workspaceUri?.fsPath; + if (workspacePath && isParentPath(interpreter.path, workspacePath)) { + // If interpreter is in the workspace, then display the full path. + useDetailedName = true; + } + } const path = interpreter.envPath && getEnvPath(interpreter.path, interpreter.envPath).pathType === 'envFolderPath' ? interpreter.envPath diff --git a/src/client/interpreter/configuration/pythonPathUpdaterService.ts b/src/client/interpreter/configuration/pythonPathUpdaterService.ts index 9b9cc26f845f..9814ff6ee4cb 100644 --- a/src/client/interpreter/configuration/pythonPathUpdaterService.ts +++ b/src/client/interpreter/configuration/pythonPathUpdaterService.ts @@ -7,7 +7,11 @@ import { sendTelemetryEvent } from '../../telemetry'; import { EventName } from '../../telemetry/constants'; import { PythonInterpreterTelemetry } from '../../telemetry/types'; import { IComponentAdapter } from '../contracts'; -import { IPythonPathUpdaterServiceFactory, IPythonPathUpdaterServiceManager } from './types'; +import { + IRecommendedEnvironmentService, + IPythonPathUpdaterServiceFactory, + IPythonPathUpdaterServiceManager, +} from './types'; @injectable() export class PythonPathUpdaterService implements IPythonPathUpdaterServiceManager { @@ -15,6 +19,7 @@ export class PythonPathUpdaterService implements IPythonPathUpdaterServiceManage @inject(IPythonPathUpdaterServiceFactory) private readonly pythonPathSettingsUpdaterFactory: IPythonPathUpdaterServiceFactory, @inject(IComponentAdapter) private readonly pyenvs: IComponentAdapter, + @inject(IRecommendedEnvironmentService) private readonly preferredEnvService: IRecommendedEnvironmentService, ) {} public async updatePythonPath( @@ -28,6 +33,9 @@ export class PythonPathUpdaterService implements IPythonPathUpdaterServiceManage let failed = false; try { await pythonPathUpdater.updatePythonPath(pythonPath); + if (trigger === 'ui') { + this.preferredEnvService.trackUserSelectedEnvironment(pythonPath, wkspace); + } } catch (err) { failed = true; const reason = err as Error; diff --git a/src/client/interpreter/configuration/recommededEnvironmentService.ts b/src/client/interpreter/configuration/recommededEnvironmentService.ts new file mode 100644 index 000000000000..39f4edfde1d6 --- /dev/null +++ b/src/client/interpreter/configuration/recommededEnvironmentService.ts @@ -0,0 +1,205 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { inject, injectable } from 'inversify'; +import { IRecommendedEnvironmentService } from './types'; +import { PythonExtension, ResolvedEnvironment } from '../../api/types'; +import { IExtensionContext, Resource } from '../../common/types'; +import { commands, Uri, workspace } from 'vscode'; +import { getWorkspaceStateValue, updateWorkspaceStateValue } from '../../common/persistentState'; +import { traceError } from '../../logging'; +import { IExtensionActivationService } from '../../activation/types'; +import { StopWatch } from '../../common/utils/stopWatch'; +import { isParentPath } from '../../common/platform/fs-paths'; + +const MEMENTO_KEY = 'userSelectedEnvPath'; + +@injectable() +export class RecommendedEnvironmentService implements IRecommendedEnvironmentService, IExtensionActivationService { + private api?: PythonExtension['environments']; + private hasRegisteredCommand = false; + constructor(@inject(IExtensionContext) private readonly extensionContext: IExtensionContext) {} + supportedWorkspaceTypes: { untrustedWorkspace: boolean; virtualWorkspace: boolean } = { + untrustedWorkspace: true, + virtualWorkspace: false, + }; + + async activate(_resource: Resource, _startupStopWatch?: StopWatch): Promise { + if (this.hasRegisteredCommand) { + return; + } + this.hasRegisteredCommand = true; + this.extensionContext.subscriptions.push( + commands.registerCommand('python.getRecommendedEnvironment', async (resource: Resource) => { + return this.getRecommededEnvironment(resource); + }), + ); + } + + registerEnvApi(api: PythonExtension['environments']) { + this.api = api; + } + + trackUserSelectedEnvironment(environmentPath: string | undefined, uri: Uri | undefined) { + if (workspace.workspaceFolders?.length) { + try { + void updateWorkspaceStateValue(MEMENTO_KEY, getDataToStore(environmentPath, uri)); + } catch (ex) { + traceError('Failed to update workspace state for preferred environment', ex); + } + } else { + void this.extensionContext.globalState.update(MEMENTO_KEY, environmentPath); + } + } + + async getRecommededEnvironment( + resource: Resource, + ): Promise< + | { + environment: ResolvedEnvironment; + reason: 'globalUserSelected' | 'workspaceUserSelected' | 'defaultRecommended'; + } + | undefined + > { + if (!workspace.isTrusted || !this.api) { + return undefined; + } + const preferred = await this.getRecommededInternal(resource); + if (!preferred) { + return undefined; + } + const activeEnv = await this.api.resolveEnvironment(this.api.getActiveEnvironmentPath(resource)); + const recommendedEnv = await this.api.resolveEnvironment(preferred.environmentPath); + if (activeEnv && recommendedEnv && activeEnv.id !== recommendedEnv.id) { + traceError( + `Active environment ${activeEnv.id} is different from recommended environment ${ + recommendedEnv.id + } for resource ${resource?.toString()}`, + ); + return undefined; + } + if (recommendedEnv) { + return { environment: recommendedEnv, reason: preferred.reason }; + } + const globalEnv = await this.api.resolveEnvironment(this.api.getActiveEnvironmentPath()); + if (activeEnv && globalEnv?.path !== activeEnv?.path) { + // User has definitely got a workspace specific environment selected. + // Given the fact that global !== workspace env, we can safely assume that + // at some time, the user has selected a workspace specific environment. + // This applies to cases where the user has selected a workspace specific environment before this version of the extension + // and we did not store it in the workspace state. + // So we can safely return the global environment as the recommended environment. + return { environment: activeEnv, reason: 'workspaceUserSelected' }; + } + return undefined; + } + async getRecommededInternal( + resource: Resource, + ): Promise< + | { environmentPath: string; reason: 'globalUserSelected' | 'workspaceUserSelected' | 'defaultRecommended' } + | undefined + > { + let workspaceState: string | undefined = undefined; + try { + workspaceState = getWorkspaceStateValue(MEMENTO_KEY); + } catch (ex) { + traceError('Failed to get workspace state for preferred environment', ex); + } + + if (workspace.workspaceFolders?.length && workspaceState) { + const workspaceUri = ( + (resource ? workspace.getWorkspaceFolder(resource)?.uri : undefined) || + workspace.workspaceFolders[0].uri + ).toString(); + + try { + const existingJson: Record = JSON.parse(workspaceState); + const selectedEnvPath = existingJson[workspaceUri]; + if (selectedEnvPath) { + return { environmentPath: selectedEnvPath, reason: 'workspaceUserSelected' }; + } + } catch (ex) { + traceError('Failed to parse existing workspace state value for preferred environment', ex); + } + } + + if (workspace.workspaceFolders?.length && this.api) { + // Check if we have a .venv or .conda environment in the workspace + // This is required for cases where user has selected a workspace specific environment + // but before this version of the extension, we did not store it in the workspace state. + const workspaceEnv = await getWorkspaceSpecificVirtualEnvironment(this.api, resource); + if (workspaceEnv) { + return { environmentPath: workspaceEnv.path, reason: 'workspaceUserSelected' }; + } + } + + const globalSelectedEnvPath = this.extensionContext.globalState.get(MEMENTO_KEY); + if (globalSelectedEnvPath) { + return { environmentPath: globalSelectedEnvPath, reason: 'globalUserSelected' }; + } + return this.api && workspace.isTrusted + ? { + environmentPath: this.api.getActiveEnvironmentPath(resource).path, + reason: 'defaultRecommended', + } + : undefined; + } +} + +async function getWorkspaceSpecificVirtualEnvironment(api: PythonExtension['environments'], resource: Resource) { + const workspaceUri = + (resource ? workspace.getWorkspaceFolder(resource)?.uri : undefined) || + (workspace.workspaceFolders?.length ? workspace.workspaceFolders[0].uri : undefined); + if (!workspaceUri) { + return undefined; + } + let workspaceEnv = api.known.find((env) => { + if (!env.environment?.folderUri) { + return false; + } + if (env.environment.type !== 'VirtualEnvironment' && env.environment.type !== 'Conda') { + return false; + } + return isParentPath(env.environment.folderUri.fsPath, workspaceUri.fsPath); + }); + let resolvedEnv = workspaceEnv ? api.resolveEnvironment(workspaceEnv) : undefined; + if (resolvedEnv) { + return resolvedEnv; + } + workspaceEnv = api.known.find((env) => { + // Look for any other type of env thats inside this workspace + // Or look for an env thats associated with this workspace (pipenv or the like). + return ( + (env.environment?.folderUri && isParentPath(env.environment.folderUri.fsPath, workspaceUri.fsPath)) || + (env.environment?.workspaceFolder && env.environment.workspaceFolder.uri.fsPath === workspaceUri.fsPath) + ); + }); + return workspaceEnv ? api.resolveEnvironment(workspaceEnv) : undefined; +} + +function getDataToStore(environmentPath: string | undefined, uri: Uri | undefined): string | undefined { + if (!workspace.workspaceFolders?.length) { + return environmentPath; + } + const workspaceUri = ( + (uri ? workspace.getWorkspaceFolder(uri)?.uri : undefined) || workspace.workspaceFolders[0].uri + ).toString(); + const existingData = getWorkspaceStateValue(MEMENTO_KEY); + if (!existingData) { + return JSON.stringify(environmentPath ? { [workspaceUri]: environmentPath } : {}); + } + try { + const existingJson: Record = JSON.parse(existingData); + if (environmentPath) { + existingJson[workspaceUri] = environmentPath; + } else { + delete existingJson[workspaceUri]; + } + return JSON.stringify(existingJson); + } catch (ex) { + traceError('Failed to parse existing workspace state value for preferred environment', ex); + return JSON.stringify({ + [workspaceUri]: environmentPath, + }); + } +} diff --git a/src/client/interpreter/configuration/types.ts b/src/client/interpreter/configuration/types.ts index 2f3882e1246e..05ff8e32c18e 100644 --- a/src/client/interpreter/configuration/types.ts +++ b/src/client/interpreter/configuration/types.ts @@ -1,6 +1,7 @@ import { ConfigurationTarget, Disposable, QuickPickItem, Uri } from 'vscode'; import { Resource } from '../../common/types'; import { PythonEnvironment } from '../../pythonEnvironments/info'; +import { PythonExtension, ResolvedEnvironment } from '../../api/types'; export interface IPythonPathUpdaterService { updatePythonPath(pythonPath: string | undefined): Promise; @@ -58,6 +59,7 @@ export interface ISpecialQuickPickItem extends QuickPickItem { export const IInterpreterComparer = Symbol('IInterpreterComparer'); export interface IInterpreterComparer { + initialize(resource: Resource): Promise; compare(a: PythonEnvironment, b: PythonEnvironment): number; getRecommended(interpreters: PythonEnvironment[], resource: Resource): PythonEnvironment | undefined; } @@ -80,6 +82,11 @@ export interface InterpreterQuickPickParams { * Specify `true` to show back button. */ showBackButton?: boolean; + + /** + * Show button to create a new environment. + */ + showCreateEnvironment?: boolean; } export const IInterpreterQuickPick = Symbol('IInterpreterQuickPick'); @@ -90,3 +97,18 @@ export interface IInterpreterQuickPick { params?: InterpreterQuickPickParams, ): Promise; } + +export const IRecommendedEnvironmentService = Symbol('IRecommendedEnvironmentService'); +export interface IRecommendedEnvironmentService { + registerEnvApi(api: PythonExtension['environments']): void; + trackUserSelectedEnvironment(environmentPath: string | undefined, uri: Uri | undefined): void; + getRecommededEnvironment( + resource: Resource, + ): Promise< + | { + environment: ResolvedEnvironment; + reason: 'globalUserSelected' | 'workspaceUserSelected' | 'defaultRecommended'; + } + | undefined + >; +} diff --git a/src/client/interpreter/contracts.ts b/src/client/interpreter/contracts.ts index bfaebd235f19..30a05c140249 100644 --- a/src/client/interpreter/contracts.ts +++ b/src/client/interpreter/contracts.ts @@ -4,6 +4,7 @@ import { FileChangeType } from '../common/platform/fileSystemWatcher'; import { Resource } from '../common/types'; import { PythonEnvSource } from '../pythonEnvironments/base/info'; import { + GetRefreshEnvironmentsOptions, ProgressNotificationEvent, PythonLocatorQuery, TriggerRefreshOptions, @@ -22,7 +23,7 @@ export const IComponentAdapter = Symbol('IComponentAdapter'); export interface IComponentAdapter { readonly onProgress: Event; triggerRefresh(query?: PythonLocatorQuery, options?: TriggerRefreshOptions): Promise; - getRefreshPromise(): Promise | undefined; + getRefreshPromise(options?: GetRefreshEnvironmentsOptions): Promise | undefined; readonly onChanged: Event; // VirtualEnvPrompt onDidCreate(resource: Resource, callback: () => void): Disposable; @@ -74,6 +75,7 @@ export const IInterpreterService = Symbol('IInterpreterService'); export interface IInterpreterService { triggerRefresh(query?: PythonLocatorQuery, options?: TriggerRefreshOptions): Promise; readonly refreshPromise: Promise | undefined; + getRefreshPromise(options?: GetRefreshEnvironmentsOptions): Promise | undefined; readonly onDidChangeInterpreters: Event; onDidChangeInterpreterConfiguration: Event; onDidChangeInterpreter: Event; diff --git a/src/client/interpreter/display/index.ts b/src/client/interpreter/display/index.ts index aabb9f86f6d1..3a602093d4f9 100644 --- a/src/client/interpreter/display/index.ts +++ b/src/client/interpreter/display/index.ts @@ -24,6 +24,7 @@ import { IInterpreterService, IInterpreterStatusbarVisibilityFilter, } from '../contracts'; +import { shouldEnvExtHandleActivation } from '../../envExt/api.internal'; /** * Based on https://github.com/microsoft/vscode-python/issues/18040#issuecomment-992567670. @@ -67,6 +68,9 @@ export class InterpreterDisplay implements IInterpreterDisplay, IExtensionSingle } public async activate(): Promise { + if (shouldEnvExtHandleActivation()) { + return; + } const application = this.serviceContainer.get(IApplicationShell); if (this.useLanguageStatus) { this.languageStatus = application.createLanguageStatusItem('python.selectedInterpreter', { @@ -111,6 +115,12 @@ export class InterpreterDisplay implements IInterpreterDisplay, IExtensionSingle } } private async updateDisplay(workspaceFolder?: Uri) { + if (shouldEnvExtHandleActivation()) { + this.statusBar?.hide(); + this.languageStatus?.dispose(); + this.languageStatus = undefined; + return; + } const interpreter = await this.interpreterService.getActiveInterpreter(workspaceFolder); if ( this.currentlySelectedInterpreterDisplay && diff --git a/src/client/interpreter/display/progressDisplay.ts b/src/client/interpreter/display/progressDisplay.ts index 5194dd8a5103..4b2811043d2f 100644 --- a/src/client/interpreter/display/progressDisplay.ts +++ b/src/client/interpreter/display/progressDisplay.ts @@ -17,7 +17,7 @@ import { IComponentAdapter } from '../contracts'; // The parts of IComponentAdapter used here. @injectable() -export class InterpreterLocatorProgressStatubarHandler implements IExtensionSingleActivationService { +export class InterpreterLocatorProgressStatusBarHandler implements IExtensionSingleActivationService { public readonly supportedWorkspaceTypes = { untrustedWorkspace: false, virtualWorkspace: true }; private deferred: Deferred | undefined; @@ -39,6 +39,8 @@ export class InterpreterLocatorProgressStatubarHandler implements IExtensionSing if (refreshPromise) { refreshPromise.then(() => this.hideProgress()); } + } else if (event.stage === ProgressReportStage.discoveryFinished) { + this.hideProgress(); } }, this, diff --git a/src/client/interpreter/helpers.ts b/src/client/interpreter/helpers.ts index ded855cbd55b..413fa225f3ef 100644 --- a/src/client/interpreter/helpers.ts +++ b/src/client/interpreter/helpers.ts @@ -19,7 +19,7 @@ export function isInterpreterLocatedInWorkspace(interpreter: PythonEnvironment, /** * Build a version-sorted list from the given one, with lowest first. */ -function sortInterpreters(interpreters: PythonEnvironment[]): PythonEnvironment[] { +export function sortInterpreters(interpreters: PythonEnvironment[]): PythonEnvironment[] { if (interpreters.length === 0) { return []; } diff --git a/src/client/debugger/extension/configuration/launch.json/interpreterPathCommand.ts b/src/client/interpreter/interpreterPathCommand.ts similarity index 53% rename from src/client/debugger/extension/configuration/launch.json/interpreterPathCommand.ts rename to src/client/interpreter/interpreterPathCommand.ts index e4c14de407c9..12f6756dafeb 100644 --- a/src/client/debugger/extension/configuration/launch.json/interpreterPathCommand.ts +++ b/src/client/interpreter/interpreterPathCommand.ts @@ -1,51 +1,61 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; - -import { inject, injectable } from 'inversify'; -import { Uri } from 'vscode'; -import { IExtensionSingleActivationService } from '../../../../activation/types'; -import { Commands } from '../../../../common/constants'; -import { IDisposable, IDisposableRegistry } from '../../../../common/types'; -import { registerCommand } from '../../../../common/vscodeApis/commandApis'; -import { IInterpreterService } from '../../../../interpreter/contracts'; - -@injectable() -export class InterpreterPathCommand implements IExtensionSingleActivationService { - public readonly supportedWorkspaceTypes = { untrustedWorkspace: false, virtualWorkspace: false }; - - constructor( - @inject(IInterpreterService) private readonly interpreterService: IInterpreterService, - @inject(IDisposableRegistry) private readonly disposables: IDisposable[], - ) {} - - public async activate(): Promise { - this.disposables.push( - registerCommand(Commands.GetSelectedInterpreterPath, (args) => this._getSelectedInterpreterPath(args)), - ); - } - - public async _getSelectedInterpreterPath(args: { workspaceFolder: string } | string[]): Promise { - // If `launch.json` is launching this command, `args.workspaceFolder` carries the workspaceFolder - // If `tasks.json` is launching this command, `args[1]` carries the workspaceFolder - let workspaceFolder; - if ('workspaceFolder' in args) { - workspaceFolder = args.workspaceFolder; - } else if (args[1]) { - const [, second] = args; - workspaceFolder = second; - } else { - workspaceFolder = undefined; - } - - let workspaceFolderUri; - try { - workspaceFolderUri = workspaceFolder ? Uri.parse(workspaceFolder) : undefined; - } catch (ex) { - workspaceFolderUri = undefined; - } - - return (await this.interpreterService.getActiveInterpreter(workspaceFolderUri))?.path ?? 'python'; - } -} +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { inject, injectable } from 'inversify'; +import { Uri, workspace } from 'vscode'; +import { IExtensionSingleActivationService } from '../activation/types'; +import { Commands } from '../common/constants'; +import { IDisposable, IDisposableRegistry } from '../common/types'; +import { registerCommand } from '../common/vscodeApis/commandApis'; +import { IInterpreterService } from './contracts'; +import { useEnvExtension } from '../envExt/api.internal'; + +@injectable() +export class InterpreterPathCommand implements IExtensionSingleActivationService { + public readonly supportedWorkspaceTypes = { untrustedWorkspace: false, virtualWorkspace: false }; + + constructor( + @inject(IInterpreterService) private readonly interpreterService: IInterpreterService, + @inject(IDisposableRegistry) private readonly disposables: IDisposable[], + ) {} + + public async activate(): Promise { + this.disposables.push( + registerCommand(Commands.GetSelectedInterpreterPath, (args) => this._getSelectedInterpreterPath(args)), + ); + } + + public async _getSelectedInterpreterPath( + args: { workspaceFolder: string; type: string } | string[], + ): Promise { + // If `launch.json` is launching this command, `args.workspaceFolder` carries the workspaceFolder + // If `tasks.json` is launching this command, `args[1]` carries the workspaceFolder + let workspaceFolder; + if ('workspaceFolder' in args) { + workspaceFolder = args.workspaceFolder; + } else if (args[1]) { + const [, second] = args; + workspaceFolder = second; + } else if (useEnvExtension() && 'type' in args && args.type === 'debugpy') { + // If using the envsExt and the type is debugpy, we need to add the workspace folder to get the interpreter path. + if (Array.isArray(workspace.workspaceFolders) && workspace.workspaceFolders.length > 0) { + workspaceFolder = workspace.workspaceFolders[0].uri.fsPath; + } + } else { + workspaceFolder = undefined; + } + + let workspaceFolderUri; + try { + workspaceFolderUri = workspaceFolder ? Uri.file(workspaceFolder) : undefined; + } catch (ex) { + workspaceFolderUri = undefined; + } + + const interpreterPath = + (await this.interpreterService.getActiveInterpreter(workspaceFolderUri))?.path ?? 'python'; + return interpreterPath.toCommandArgumentForPythonExt(); + } +} diff --git a/src/client/interpreter/interpreterService.ts b/src/client/interpreter/interpreterService.ts index 3cfb651977bb..ad06fd7d051d 100644 --- a/src/client/interpreter/interpreterService.ts +++ b/src/client/interpreter/interpreterService.ts @@ -31,15 +31,21 @@ import { PythonEnvironmentsChangedEvent, } from './contracts'; import { traceError, traceLog } from '../logging'; -import { Commands, PYTHON_LANGUAGE } from '../common/constants'; +import { Commands, PVSC_EXTENSION_ID, PYTHON_LANGUAGE } from '../common/constants'; import { reportActiveInterpreterChanged } from '../environmentApi'; import { IPythonExecutionFactory } from '../common/process/types'; import { Interpreters } from '../common/utils/localize'; import { sendTelemetryEvent } from '../telemetry'; import { EventName } from '../telemetry/constants'; import { cache } from '../common/utils/decorators'; -import { PythonLocatorQuery, TriggerRefreshOptions } from '../pythonEnvironments/base/locator'; +import { + GetRefreshEnvironmentsOptions, + PythonLocatorQuery, + TriggerRefreshOptions, +} from '../pythonEnvironments/base/locator'; import { sleep } from '../common/utils/async'; +import { useEnvExtension } from '../envExt/api.internal'; +import { getActiveInterpreterLegacy } from '../envExt/api.legacy'; type StoredPythonEnvironment = PythonEnvironment & { store?: boolean }; @@ -59,6 +65,10 @@ export class InterpreterService implements Disposable, IInterpreterService { return this.pyenvs.getRefreshPromise(); } + public getRefreshPromise(options?: GetRefreshEnvironmentsOptions): Promise | undefined { + return this.pyenvs.getRefreshPromise(options); + } + public get onDidChangeInterpreter(): Event { return this.didChangeInterpreterEmitter.event; } @@ -85,6 +95,11 @@ export class InterpreterService implements Disposable, IInterpreterService { private readonly didChangeInterpreterInformation = new EventEmitter(); + private readonly activeInterpreterPaths = new Map< + string, + { path: string; workspaceFolder: WorkspaceFolder | undefined } + >(); + constructor( @inject(IServiceContainer) private serviceContainer: IServiceContainer, @inject(IComponentAdapter) private readonly pyenvs: IComponentAdapter, @@ -100,10 +115,12 @@ export class InterpreterService implements Disposable, IInterpreterService { const workspaceFolder = this.serviceContainer .get(IWorkspaceService) .getWorkspaceFolder(resource); - this.ensureEnvironmentContainsPython( - this.configService.getSettings(resource).pythonPath, - workspaceFolder, - ).ignoreErrors(); + const path = this.configService.getSettings(resource).pythonPath; + const workspaceKey = this.serviceContainer + .get(IWorkspaceService) + .getWorkspaceFolderIdentifier(resource); + this.activeInterpreterPaths.set(workspaceKey, { path, workspaceFolder }); + this.ensureEnvironmentContainsPython(path, workspaceFolder).ignoreErrors(); } public initialize(): void { @@ -138,7 +155,12 @@ export class InterpreterService implements Disposable, IInterpreterService { return false; } const document = this.docManager.activeTextEditor?.document; - if (document?.fileName.endsWith('settings.json')) { + // Output channel for MS Python related extensions. These contain "ms-python" in their ID. + const pythonOutputChannelPattern = PVSC_EXTENSION_ID.split('.')[0]; + if ( + document?.fileName.endsWith('settings.json') || + document?.fileName.includes(pythonOutputChannelPattern) + ) { return false; } return document?.languageId !== PYTHON_LANGUAGE; @@ -150,6 +172,16 @@ export class InterpreterService implements Disposable, IInterpreterService { const interpreter = e.old ?? e.new; if (interpreter) { this.didChangeInterpreterInformation.fire(interpreter); + for (const { path, workspaceFolder } of this.activeInterpreterPaths.values()) { + if (path === interpreter.path && !e.new) { + // If the active environment got deleted, notify it. + this.didChangeInterpreterEmitter.fire(workspaceFolder?.uri); + reportActiveInterpreterChanged({ + path, + resource: workspaceFolder, + }); + } + } } }), ); @@ -187,6 +219,10 @@ export class InterpreterService implements Disposable, IInterpreterService { } public async getActiveInterpreter(resource?: Uri): Promise { + if (useEnvExtension()) { + return getActiveInterpreterLegacy(resource); + } + const activatedEnvLaunch = this.serviceContainer.get(IActivatedEnvironmentLaunch); let path = await activatedEnvLaunch.selectIfLaunchedViaActivatedEnv(true); // This is being set as interpreter in background, after which it'll show up in `.pythonPath` config. @@ -241,6 +277,10 @@ export class InterpreterService implements Disposable, IInterpreterService { path: pySettings.pythonPath, resource: workspaceFolder, }); + const workspaceKey = this.serviceContainer + .get(IWorkspaceService) + .getWorkspaceFolderIdentifier(resource); + this.activeInterpreterPaths.set(workspaceKey, { path: pySettings.pythonPath, workspaceFolder }); const interpreterDisplay = this.serviceContainer.get(IInterpreterDisplay); interpreterDisplay.refresh().catch((ex) => traceError('Python Extension: display.refresh', ex)); await this.ensureEnvironmentContainsPython(this._pythonPathSetting, workspaceFolder); @@ -249,6 +289,10 @@ export class InterpreterService implements Disposable, IInterpreterService { @cache(-1, true) private async ensureEnvironmentContainsPython(pythonPath: string, workspaceFolder: WorkspaceFolder | undefined) { + if (useEnvExtension()) { + return; + } + const installer = this.serviceContainer.get(IInstaller); if (!(await installer.isInstalled(Product.python))) { // If Python is not installed into the environment, install it. diff --git a/src/client/interpreter/serviceRegistry.ts b/src/client/interpreter/serviceRegistry.ts index 04af15415b04..f54f8e5368fe 100644 --- a/src/client/interpreter/serviceRegistry.ts +++ b/src/client/interpreter/serviceRegistry.ts @@ -6,7 +6,6 @@ import { IExtensionActivationService, IExtensionSingleActivationService } from '../activation/types'; import { IServiceManager } from '../ioc/types'; import { EnvironmentActivationService } from './activation/service'; -import { TerminalEnvVarCollectionService } from './activation/terminalEnvVarCollectionService'; import { IEnvironmentActivationService } from './activation/types'; import { InterpreterAutoSelectionService } from './autoSelection/index'; import { InterpreterAutoSelectionProxyService } from './autoSelection/proxy'; @@ -17,19 +16,22 @@ import { InstallPythonViaTerminal } from './configuration/interpreterSelector/co import { ResetInterpreterCommand } from './configuration/interpreterSelector/commands/resetInterpreter'; import { SetInterpreterCommand } from './configuration/interpreterSelector/commands/setInterpreter'; import { InterpreterSelector } from './configuration/interpreterSelector/interpreterSelector'; +import { RecommendedEnvironmentService } from './configuration/recommededEnvironmentService'; import { PythonPathUpdaterService } from './configuration/pythonPathUpdaterService'; import { PythonPathUpdaterServiceFactory } from './configuration/pythonPathUpdaterServiceFactory'; import { IInterpreterComparer, IInterpreterQuickPick, IInterpreterSelector, + IRecommendedEnvironmentService, IPythonPathUpdaterServiceFactory, IPythonPathUpdaterServiceManager, } from './configuration/types'; import { IActivatedEnvironmentLaunch, IInterpreterDisplay, IInterpreterHelper, IInterpreterService } from './contracts'; import { InterpreterDisplay } from './display'; -import { InterpreterLocatorProgressStatubarHandler } from './display/progressDisplay'; +import { InterpreterLocatorProgressStatusBarHandler } from './display/progressDisplay'; import { InterpreterHelper } from './helpers'; +import { InterpreterPathCommand } from './interpreterPathCommand'; import { InterpreterService } from './interpreterService'; import { ActivatedEnvironmentLaunch } from './virtualEnvs/activatedEnvLaunch'; import { CondaInheritEnvPrompt } from './virtualEnvs/condaInheritEnvPrompt'; @@ -59,6 +61,11 @@ export function registerInterpreterTypes(serviceManager: IServiceManager): void IExtensionSingleActivationService, ResetInterpreterCommand, ); + serviceManager.addSingleton( + IRecommendedEnvironmentService, + RecommendedEnvironmentService, + ); + serviceManager.addBinding(IRecommendedEnvironmentService, IExtensionActivationService); serviceManager.addSingleton(IInterpreterQuickPick, SetInterpreterCommand); serviceManager.addSingleton(IExtensionActivationService, VirtualEnvironmentPrompt); @@ -83,7 +90,7 @@ export function registerInterpreterTypes(serviceManager: IServiceManager): void serviceManager.addSingleton( IExtensionSingleActivationService, - InterpreterLocatorProgressStatubarHandler, + InterpreterLocatorProgressStatusBarHandler, ); serviceManager.addSingleton( @@ -109,8 +116,8 @@ export function registerTypes(serviceManager: IServiceManager): void { IEnvironmentActivationService, EnvironmentActivationService, ); - serviceManager.addSingleton( - IExtensionActivationService, - TerminalEnvVarCollectionService, + serviceManager.addSingleton( + IExtensionSingleActivationService, + InterpreterPathCommand, ); } diff --git a/src/client/interpreter/virtualEnvs/activatedEnvLaunch.ts b/src/client/interpreter/virtualEnvs/activatedEnvLaunch.ts index 468c2dc72a01..6b4334e13100 100644 --- a/src/client/interpreter/virtualEnvs/activatedEnvLaunch.ts +++ b/src/client/interpreter/virtualEnvs/activatedEnvLaunch.ts @@ -1,7 +1,7 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -import { inject, injectable, optional } from 'inversify'; +import { inject, injectable } from 'inversify'; import { ConfigurationTarget } from 'vscode'; import * as path from 'path'; import { IApplicationShell, IWorkspaceService } from '../../common/application/types'; @@ -29,7 +29,7 @@ export class ActivatedEnvironmentLaunch implements IActivatedEnvironmentLaunch { private readonly pythonPathUpdaterService: IPythonPathUpdaterServiceManager, @inject(IInterpreterService) private readonly interpreterService: IInterpreterService, @inject(IProcessServiceFactory) private readonly processServiceFactory: IProcessServiceFactory, - @optional() public wasSelected: boolean = false, + public wasSelected: boolean = false, ) {} @cache(-1, true) @@ -85,15 +85,12 @@ export class ActivatedEnvironmentLaunch implements IActivatedEnvironmentLaunch { @cache(-1, true) private async _selectIfLaunchedViaActivatedEnv(doNotBlockOnSelection = false): Promise { - if (this.workspaceService.workspaceFile) { - // Assuming multiroot workspaces cannot be directly launched via `code .` command. - return undefined; - } if (process.env.VSCODE_CLI !== '1') { // We only want to select the interpreter if VS Code was launched from the command line. - traceVerbose('VS Code was not launched from the command line, not selecting activated interpreter'); + traceLog("Skipping ActivatedEnv Detection: process.env.VSCODE_CLI !== '1'"); return undefined; } + traceVerbose('VS Code was not launched from the command line'); const prefix = await this.getPrefixOfSelectedActivatedEnv(); if (!prefix) { this._promptIfApplicable().ignoreErrors(); diff --git a/src/client/interpreter/virtualEnvs/condaInheritEnvPrompt.ts b/src/client/interpreter/virtualEnvs/condaInheritEnvPrompt.ts index cf9175345cb0..6b5295724449 100644 --- a/src/client/interpreter/virtualEnvs/condaInheritEnvPrompt.ts +++ b/src/client/interpreter/virtualEnvs/condaInheritEnvPrompt.ts @@ -1,7 +1,7 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -import { inject, injectable, optional } from 'inversify'; +import { inject, injectable } from 'inversify'; import { ConfigurationTarget, Uri } from 'vscode'; import { IExtensionActivationService } from '../../activation/types'; import { IApplicationEnvironment, IApplicationShell, IWorkspaceService } from '../../common/application/types'; @@ -26,7 +26,7 @@ export class CondaInheritEnvPrompt implements IExtensionActivationService { @inject(IPersistentStateFactory) private readonly persistentStateFactory: IPersistentStateFactory, @inject(IPlatformService) private readonly platformService: IPlatformService, @inject(IApplicationEnvironment) private readonly appEnvironment: IApplicationEnvironment, - @optional() public hasPromptBeenShownInCurrentSession: boolean = false, + public hasPromptBeenShownInCurrentSession: boolean = false, ) {} public async activate(resource: Uri): Promise { diff --git a/src/client/jupyter/jupyterIntegration.ts b/src/client/jupyter/jupyterIntegration.ts index a0fa0fedb63f..5584682f3b86 100644 --- a/src/client/jupyter/jupyterIntegration.ts +++ b/src/client/jupyter/jupyterIntegration.ts @@ -1,131 +1,50 @@ /* eslint-disable comma-dangle */ -/* eslint-disable implicit-arrow-linebreak */ +/* eslint-disable implicit-arrow-linebreak, max-classes-per-file */ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. import { inject, injectable, named } from 'inversify'; import { dirname } from 'path'; -import { CancellationToken, Event, Extension, Memento, Uri } from 'vscode'; +import { EventEmitter, Extension, Memento, Uri, workspace, Event } from 'vscode'; import type { SemVer } from 'semver'; import { IContextKeyManager, IWorkspaceService } from '../common/application/types'; import { JUPYTER_EXTENSION_ID, PYLANCE_EXTENSION_ID } from '../common/constants'; -import { InterpreterUri, ModuleInstallFlags } from '../common/installer/types'; -import { - GLOBAL_MEMENTO, - IExtensions, - IInstaller, - IMemento, - InstallerResponse, - Product, - ProductInstallStatus, - Resource, -} from '../common/types'; -import { getDebugpyPackagePath } from '../debugger/extension/adapter/remoteLaunchers'; +import { GLOBAL_MEMENTO, IExtensions, IMemento, Resource } from '../common/types'; import { IEnvironmentActivationService } from '../interpreter/activation/types'; -import { IInterpreterQuickPickItem, IInterpreterSelector } from '../interpreter/configuration/types'; import { - IComponentAdapter, + IInterpreterQuickPickItem, + IInterpreterSelector, + IRecommendedEnvironmentService, +} from '../interpreter/configuration/types'; +import { ICondaService, IInterpreterDisplay, IInterpreterService, IInterpreterStatusbarVisibilityFilter, - PythonEnvironmentsChangedEvent, } from '../interpreter/contracts'; -import { PythonEnvironment } from '../pythonEnvironments/info'; -import { IDataViewerDataProvider, IJupyterUriProvider } from './types'; import { PylanceApi } from '../activation/node/pylanceApi'; import { ExtensionContextKey } from '../common/application/contextKeys'; -/** - * This allows Python extension to update Product enum without breaking Jupyter. - * I.e. we have a strict contract, else using numbers (in enums) is bound to break across products. - */ -enum JupyterProductToInstall { - jupyter = 'jupyter', - ipykernel = 'ipykernel', - notebook = 'notebook', - kernelspec = 'kernelspec', - nbconvert = 'nbconvert', - pandas = 'pandas', - pip = 'pip', -} - -const ProductMapping: { [key in JupyterProductToInstall]: Product } = { - [JupyterProductToInstall.ipykernel]: Product.ipykernel, - [JupyterProductToInstall.jupyter]: Product.jupyter, - [JupyterProductToInstall.kernelspec]: Product.kernelspec, - [JupyterProductToInstall.nbconvert]: Product.nbconvert, - [JupyterProductToInstall.notebook]: Product.notebook, - [JupyterProductToInstall.pandas]: Product.pandas, - [JupyterProductToInstall.pip]: Product.pip, -}; +import { getDebugpyPath } from '../debugger/pythonDebugger'; +import type { Environment, EnvironmentPath, PythonExtension } from '../api/types'; +import { DisposableBase } from '../common/utils/resourceLifecycle'; type PythonApiForJupyterExtension = { - /** - * IInterpreterService - */ - onDidChangeInterpreter: Event; - /** - * IInterpreterService - */ - readonly refreshPromise: Promise | undefined; - /** - * IInterpreterService - */ - readonly onDidChangeInterpreters: Event; - /** - * Equivalent to getInterpreters() in IInterpreterService - */ - getKnownInterpreters(resource?: Uri): PythonEnvironment[]; - /** - * @deprecated Use `getKnownInterpreters`, `onDidChangeInterpreters`, and `refreshPromise` instead. - * Equivalent to getAllInterpreters() in IInterpreterService - */ - getInterpreters(resource?: Uri): Promise; - /** - * IInterpreterService - */ - getActiveInterpreter(resource?: Uri): Promise; - /** - * IInterpreterService - */ - getInterpreterDetails(pythonPath: string, resource?: Uri): Promise; - /** * IEnvironmentActivationService */ getActivatedEnvironmentVariables( resource: Resource, - interpreter?: PythonEnvironment, + interpreter: Environment, allowExceptions?: boolean, ): Promise; - isMicrosoftStoreInterpreter(pythonPath: string): Promise; - suggestionToQuickPickItem(suggestion: PythonEnvironment, workspaceUri?: Uri | undefined): IInterpreterQuickPickItem; getKnownSuggestions(resource: Resource): IInterpreterQuickPickItem[]; /** * @deprecated Use `getKnownSuggestions` and `suggestionToQuickPickItem` instead. */ getSuggestions(resource: Resource): Promise; /** - * IInstaller - */ - install( - product: JupyterProductToInstall, - resource?: InterpreterUri, - cancel?: CancellationToken, - reInstallAndUpdate?: boolean, - installPipIfRequired?: boolean, - ): Promise; - /** - * IInstaller - */ - isProductVersionCompatible( - product: Product, - semVerRequirement: string, - resource?: InterpreterUri, - ): Promise; - /** - * Returns path to where `debugpy` is. In python extension this is `/pythonFiles/lib/python`. + * Returns path to where `debugpy` is. In python extension this is `/python_files/lib/python`. */ getDebuggerPath(): Promise; /** @@ -141,10 +60,6 @@ type PythonApiForJupyterExtension = { * Returns the conda executable. */ getCondaFile(): Promise; - getEnvironmentActivationShellCommands( - resource: Resource, - interpreter?: PythonEnvironment, - ): Promise; /** * Call to provide a function that the Python extension can call to request the Python @@ -154,12 +69,17 @@ type PythonApiForJupyterExtension = { registerJupyterPythonPathFunction(func: (uri: Uri) => Promise): void; /** - * Call to provide a function that the Python extension can call to request the notebook - * document URI related to a particular text document URI, or undefined if there is no - * associated notebook. - * @param func : The function that Python should call when requesting the notebook URI. + * Returns the preferred environment for the given URI. */ - registerGetNotebookUriForTextDocumentUriFunction(func: (textDocumentUri: Uri) => Uri | undefined): void; + getRecommededEnvironment( + uri: Uri | undefined, + ): Promise< + | { + environment: EnvironmentPath; + reason: 'globalUserSelected' | 'workspaceUserSelected' | 'defaultRecommended'; + } + | undefined + >; }; type JupyterExtensionApi = { @@ -168,17 +88,6 @@ type JupyterExtensionApi = { * @param interpreterService */ registerPythonApi(interpreterService: PythonApiForJupyterExtension): void; - /** - * Launches Data Viewer component. - * @param {IDataViewerDataProvider} dataProvider Instance that will be used by the Data Viewer component to fetch data. - * @param {string} title Data Viewer title - */ - showDataViewer(dataProvider: IDataViewerDataProvider, title: string): Promise; - /** - * Registers a remote server provider component that's used to pick remote jupyter server URIs - * @param serverProvider object called back when picking jupyter server URI - */ - registerRemoteServerProvider(serverProvider: IJupyterUriProvider): void; }; @injectable() @@ -186,24 +95,23 @@ export class JupyterExtensionIntegration { private jupyterExtension: Extension | undefined; private pylanceExtension: Extension | undefined; - - private jupyterPythonPathFunction: ((uri: Uri) => Promise) | undefined; - - private getNotebookUriForTextDocumentUriFunction: ((textDocumentUri: Uri) => Uri | undefined) | undefined; + private environmentApi: PythonExtension['environments'] | undefined; constructor( @inject(IExtensions) private readonly extensions: IExtensions, - @inject(IInterpreterService) private readonly interpreterService: IInterpreterService, @inject(IInterpreterSelector) private readonly interpreterSelector: IInterpreterSelector, - @inject(IInstaller) private readonly installer: IInstaller, @inject(IEnvironmentActivationService) private readonly envActivation: IEnvironmentActivationService, @inject(IMemento) @named(GLOBAL_MEMENTO) private globalState: Memento, @inject(IInterpreterDisplay) private interpreterDisplay: IInterpreterDisplay, - @inject(IComponentAdapter) private pyenvs: IComponentAdapter, @inject(IWorkspaceService) private workspaceService: IWorkspaceService, @inject(ICondaService) private readonly condaService: ICondaService, @inject(IContextKeyManager) private readonly contextManager: IContextKeyManager, + @inject(IInterpreterService) private interpreterService: IInterpreterService, + @inject(IRecommendedEnvironmentService) private preferredEnvironmentService: IRecommendedEnvironmentService, ) {} + public registerEnvApi(api: PythonExtension['environments']) { + this.environmentApi = api; + } public registerApi(jupyterExtensionApi: JupyterExtensionApi): JupyterExtensionApi | undefined { this.contextManager.setContext(ExtensionContextKey.IsJupyterInstalled, true); @@ -213,55 +121,19 @@ export class JupyterExtensionIntegration { } // Forward python parts jupyterExtensionApi.registerPythonApi({ - onDidChangeInterpreter: this.interpreterService.onDidChangeInterpreter, - getActiveInterpreter: async (resource?: Uri) => this.interpreterService.getActiveInterpreter(resource), - getInterpreterDetails: async (pythonPath: string) => - this.interpreterService.getInterpreterDetails(pythonPath), - refreshPromise: this.interpreterService.refreshPromise, - onDidChangeInterpreters: this.interpreterService.onDidChangeInterpreters, - getKnownInterpreters: (resource: Uri | undefined) => this.pyenvs.getInterpreters(resource), - getInterpreters: async (resource: Uri | undefined) => this.interpreterService.getAllInterpreters(resource), getActivatedEnvironmentVariables: async ( resource: Resource, - interpreter?: PythonEnvironment, + env: Environment, allowExceptions?: boolean, - ) => this.envActivation.getActivatedEnvironmentVariables(resource, interpreter, allowExceptions), - isMicrosoftStoreInterpreter: async (pythonPath: string): Promise => - this.pyenvs.isMicrosoftStoreInterpreter(pythonPath), + ) => { + const interpreter = await this.interpreterService.getInterpreterDetails(env.path); + return this.envActivation.getActivatedEnvironmentVariables(resource, interpreter, allowExceptions); + }, getSuggestions: async (resource: Resource): Promise => this.interpreterSelector.getAllSuggestions(resource), getKnownSuggestions: (resource: Resource): IInterpreterQuickPickItem[] => this.interpreterSelector.getSuggestions(resource), - suggestionToQuickPickItem: ( - suggestion: PythonEnvironment, - workspaceUri?: Uri | undefined, - ): IInterpreterQuickPickItem => - this.interpreterSelector.suggestionToQuickPickItem(suggestion, workspaceUri), - install: async ( - product: JupyterProductToInstall, - resource?: InterpreterUri, - cancel?: CancellationToken, - reInstallAndUpdate?: boolean, - installPipIfRequired?: boolean, - ): Promise => { - let flags = - reInstallAndUpdate === true - ? ModuleInstallFlags.updateDependencies | ModuleInstallFlags.reInstall - : undefined; - if (installPipIfRequired === true) { - flags = flags - ? flags | ModuleInstallFlags.installPipIfRequired - : ModuleInstallFlags.installPipIfRequired; - } - return this.installer.install(ProductMapping[product], resource, cancel, flags); - }, - isProductVersionCompatible: async ( - product: Product, - semVerRequirement: string, - resource?: InterpreterUri, - ): Promise => - this.installer.isProductVersionCompatible(product, semVerRequirement, resource), - getDebuggerPath: async () => dirname(getDebugpyPackagePath()), + getDebuggerPath: async () => dirname(await getDebugpyPath()), getInterpreterPathSelectedForJupyterServer: () => this.globalState.get('INTERPRETER_PATH_SELECTED_FOR_JUPYTER_SERVER'), registerInterpreterStatusFilter: this.interpreterDisplay.registerVisibilityFilter.bind( @@ -269,12 +141,14 @@ export class JupyterExtensionIntegration { ), getCondaFile: () => this.condaService.getCondaFile(), getCondaVersion: () => this.condaService.getCondaVersion(), - getEnvironmentActivationShellCommands: (resource: Resource, interpreter?: PythonEnvironment) => - this.envActivation.getEnvironmentActivationShellCommands(resource, interpreter), registerJupyterPythonPathFunction: (func: (uri: Uri) => Promise) => this.registerJupyterPythonPathFunction(func), - registerGetNotebookUriForTextDocumentUriFunction: (func: (textDocumentUri: Uri) => Uri | undefined) => - this.registerGetNotebookUriForTextDocumentUriFunction(func), + getRecommededEnvironment: async (uri) => { + if (!this.environmentApi) { + return undefined; + } + return this.preferredEnvironmentService.getRecommededEnvironment(uri); + }, }); return undefined; } @@ -286,24 +160,6 @@ export class JupyterExtensionIntegration { } } - public registerRemoteServerProvider(serverProvider: IJupyterUriProvider): void { - this.getExtensionApi() - .then((e) => { - if (e) { - e.registerRemoteServerProvider(serverProvider); - } - }) - .ignoreErrors(); - } - - public async showDataViewer(dataProvider: IDataViewerDataProvider, title: string): Promise { - const api = await this.getExtensionApi(); - if (api) { - return api.showDataViewer(dataProvider, title); - } - return undefined; - } - private async getExtensionApi(): Promise { if (!this.pylanceExtension) { const pylanceExtension = this.extensions.getExtension(PYLANCE_EXTENSION_ID); @@ -337,28 +193,114 @@ export class JupyterExtensionIntegration { } private registerJupyterPythonPathFunction(func: (uri: Uri) => Promise) { - this.jupyterPythonPathFunction = func; - const api = this.getPylanceApi(); if (api) { api.notebook!.registerJupyterPythonPathFunction(func); } } +} + +export interface JupyterPythonEnvironmentApi { + /** + * This event is triggered when the environment associated with a Jupyter Notebook or Interactive Window changes. + * The Uri in the event is the Uri of the Notebook/IW. + */ + onDidChangePythonEnvironment?: Event; + /** + * Returns the EnvironmentPath to the Python environment associated with a Jupyter Notebook or Interactive Window. + * If the Uri is not associated with a Jupyter Notebook or Interactive Window, then this method returns undefined. + * @param uri + */ + getPythonEnvironment?( + uri: Uri, + ): + | undefined + | { + /** + * The ID of the environment. + */ + readonly id: string; + /** + * Path to environment folder or path to python executable that uniquely identifies an environment. Environments + * lacking a python executable are identified by environment folder paths, whereas other envs can be identified + * using python executable path. + */ + readonly path: string; + }; +} + +@injectable() +export class JupyterExtensionPythonEnvironments extends DisposableBase implements JupyterPythonEnvironmentApi { + private jupyterExtension?: JupyterPythonEnvironmentApi; + + private readonly _onDidChangePythonEnvironment = this._register(new EventEmitter()); + + public readonly onDidChangePythonEnvironment = this._onDidChangePythonEnvironment.event; - public getJupyterPythonPathFunction(): ((uri: Uri) => Promise) | undefined { - return this.jupyterPythonPathFunction; + constructor(@inject(IExtensions) private readonly extensions: IExtensions) { + super(); } - public registerGetNotebookUriForTextDocumentUriFunction(func: (textDocumentUri: Uri) => Uri | undefined): void { - this.getNotebookUriForTextDocumentUriFunction = func; + public getPythonEnvironment( + uri: Uri, + ): + | undefined + | { + /** + * The ID of the environment. + */ + readonly id: string; + /** + * Path to environment folder or path to python executable that uniquely identifies an environment. Environments + * lacking a python executable are identified by environment folder paths, whereas other envs can be identified + * using python executable path. + */ + readonly path: string; + } { + if (!isJupyterResource(uri)) { + return undefined; + } + const api = this.getJupyterApi(); + if (api?.getPythonEnvironment) { + return api.getPythonEnvironment(uri); + } + return undefined; + } - const api = this.getPylanceApi(); - if (api) { - api.notebook!.registerGetNotebookUriForTextDocumentUriFunction(func); + private getJupyterApi() { + if (!this.jupyterExtension) { + const ext = this.extensions.getExtension(JUPYTER_EXTENSION_ID); + if (!ext) { + return undefined; + } + if (!ext.isActive) { + ext.activate().then(() => { + this.hookupOnDidChangePythonEnvironment(ext.exports); + }); + return undefined; + } + this.hookupOnDidChangePythonEnvironment(ext.exports); } + return this.jupyterExtension; } - public getGetNotebookUriForTextDocumentUriFunction(): ((textDocumentUri: Uri) => Uri | undefined) | undefined { - return this.getNotebookUriForTextDocumentUriFunction; + private hookupOnDidChangePythonEnvironment(api: JupyterPythonEnvironmentApi) { + this.jupyterExtension = api; + if (api.onDidChangePythonEnvironment) { + this._register( + api.onDidChangePythonEnvironment( + this._onDidChangePythonEnvironment.fire, + this._onDidChangePythonEnvironment, + ), + ); + } } } + +function isJupyterResource(resource: Uri): boolean { + // Jupyter extension only deals with Notebooks and Interactive Windows. + return ( + resource.fsPath.endsWith('.ipynb') || + workspace.notebookDocuments.some((item) => item.uri.toString() === resource.toString()) + ); +} diff --git a/src/client/languageServer/watcher.ts b/src/client/languageServer/watcher.ts index d3eccb71144c..39e6e0bb1ece 100644 --- a/src/client/languageServer/watcher.ts +++ b/src/client/languageServer/watcher.ts @@ -29,6 +29,7 @@ import { PylanceLSExtensionManager } from './pylanceLSExtensionManager'; import { ILanguageServerExtensionManager, ILanguageServerWatcher } from './types'; import { sendTelemetryEvent } from '../telemetry'; import { EventName } from '../telemetry/constants'; +import { StopWatch } from '../common/utils/stopWatch'; @injectable() /** @@ -73,14 +74,18 @@ export class LanguageServerWatcher implements IExtensionActivationService, ILang // IExtensionActivationService - public async activate(resource?: Resource): Promise { + public async activate(resource?: Resource, startupStopWatch?: StopWatch): Promise { this.register(); - await this.startLanguageServer(this.languageServerType, resource); + await this.startLanguageServer(this.languageServerType, resource, startupStopWatch); } // ILanguageServerWatcher - public async startLanguageServer(languageServerType: LanguageServerType, resource?: Resource): Promise { - await this.startAndGetLanguageServer(languageServerType, resource); + public async startLanguageServer( + languageServerType: LanguageServerType, + resource?: Resource, + startupStopWatch?: StopWatch, + ): Promise { + await this.startAndGetLanguageServer(languageServerType, resource, startupStopWatch); } public register(): void { @@ -124,6 +129,7 @@ export class LanguageServerWatcher implements IExtensionActivationService, ILang private async startAndGetLanguageServer( languageServerType: LanguageServerType, resource?: Resource, + startupStopWatch?: StopWatch, ): Promise { const lsResource = this.getWorkspaceUri(resource); const currentInterpreter = this.workspaceInterpreters.get(lsResource.fsPath); @@ -170,6 +176,12 @@ export class LanguageServerWatcher implements IExtensionActivationService, ILang if (languageServerExtensionManager.canStartLanguageServer(interpreter)) { // Start the language server. + if (startupStopWatch) { + // It means that startup is triggering this code, track time it takes since startup to activate this code. + sendTelemetryEvent(EventName.LANGUAGE_SERVER_TRIGGER_TIME, startupStopWatch.elapsedTime, { + triggerTime: startupStopWatch.elapsedTime, + }); + } await languageServerExtensionManager.startLanguageServer(lsResource, interpreter); logStartup(languageServerType, lsResource); diff --git a/src/client/linters/bandit.ts b/src/client/linters/bandit.ts deleted file mode 100644 index bbc8836bfc6b..000000000000 --- a/src/client/linters/bandit.ts +++ /dev/null @@ -1,36 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; - -import { CancellationToken, TextDocument } from 'vscode'; -import '../common/extensions'; -import { Product } from '../common/types'; -import { IServiceContainer } from '../ioc/types'; -import { BaseLinter } from './baseLinter'; -import { ILintMessage, LintMessageSeverity } from './types'; - -const severityMapping: Record = { - LOW: LintMessageSeverity.Information, - MEDIUM: LintMessageSeverity.Warning, - HIGH: LintMessageSeverity.Error, -}; - -export const BANDIT_REGEX = - '(?\\d+),(?(col)?(\\d+)?),(?\\w+),(?\\w+\\d+):(?.*)\\r?(\\n|$)'; - -export class Bandit extends BaseLinter { - constructor(serviceContainer: IServiceContainer) { - super(Product.bandit, serviceContainer); - } - - protected async runLinter(document: TextDocument, cancellation: CancellationToken): Promise { - // View all errors in bandit <= 1.5.1 (https://github.com/PyCQA/bandit/issues/371) - const messages = await this.run([document.uri.fsPath], document, cancellation, BANDIT_REGEX); - - messages.forEach((msg) => { - msg.severity = severityMapping[msg.type]; - }); - return messages; - } -} diff --git a/src/client/linters/baseLinter.ts b/src/client/linters/baseLinter.ts deleted file mode 100644 index bb24bee1637f..000000000000 --- a/src/client/linters/baseLinter.ts +++ /dev/null @@ -1,229 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; - -import * as path from 'path'; -import * as vscode from 'vscode'; -import { IWorkspaceService } from '../common/application/types'; -import { isTestExecution } from '../common/constants'; -import '../common/extensions'; -import { IPythonToolExecutionService } from '../common/process/types'; -import { splitLines } from '../common/stringUtils'; -import { - ExecutionInfo, - Flake8CategorySeverity, - IConfigurationService, - IMypyCategorySeverity, - IPycodestyleCategorySeverity, - IPylintCategorySeverity, - IPythonSettings, - Product, -} from '../common/types'; -import { IServiceContainer } from '../ioc/types'; -import { traceError, traceLog } from '../logging'; -import { ErrorHandler } from './errorHandlers/errorHandler'; -import { ILinter, ILinterInfo, ILinterManager, ILintMessage, LinterId, LintMessageSeverity } from './types'; - -const namedRegexp = require('named-js-regexp'); -// Allow negative column numbers (https://github.com/PyCQA/pylint/issues/1822) -// Allow codes with more than one letter (i.e. ABC123) -const REGEX = '(?\\d+),(?-?\\d+),(?\\w+),(?\\w+\\d+):(?.*)\\r?(\\n|$)'; - -interface IRegexGroup { - line: number; - column: number; - code: string; - message: string; - type: string; -} - -function matchNamedRegEx(data: string, regex: string): IRegexGroup | undefined { - const compiledRegexp = namedRegexp(regex, 'g'); - const rawMatch = compiledRegexp.exec(data); - if (rawMatch !== null) { - return rawMatch.groups(); - } - - return undefined; -} - -export function parseLine(line: string, regex: string, linterID: LinterId, colOffset = 0): ILintMessage | undefined { - const match = matchNamedRegEx(line, regex)!; - if (!match) { - return undefined; - } - - match.line = Number(match.line); - - match.column = Number(match.column); - - return { - code: match.code, - message: match.message, - column: Number.isNaN(match.column) || match.column <= 0 ? 0 : match.column - colOffset, - line: match.line, - type: match.type, - provider: linterID, - }; -} - -export abstract class BaseLinter implements ILinter { - protected readonly configService: IConfigurationService; - - private errorHandler: ErrorHandler; - - private _pythonSettings!: IPythonSettings; - - private _info: ILinterInfo; - - private workspace: IWorkspaceService; - - protected get pythonSettings(): IPythonSettings { - return this._pythonSettings; - } - - constructor( - product: Product, - protected readonly serviceContainer: IServiceContainer, - protected readonly columnOffset = 0, - ) { - this._info = serviceContainer.get(ILinterManager).getLinterInfo(product); - this.errorHandler = new ErrorHandler(this.info.product, serviceContainer); - this.configService = serviceContainer.get(IConfigurationService); - this.workspace = serviceContainer.get(IWorkspaceService); - } - - public get info(): ILinterInfo { - return this._info; - } - - public async lint(document: vscode.TextDocument, cancellation: vscode.CancellationToken): Promise { - this._pythonSettings = this.configService.getSettings(document.uri); - return this.runLinter(document, cancellation); - } - - protected getWorkspaceRootPath(document: vscode.TextDocument): string { - const workspaceFolder = this.workspace.getWorkspaceFolder(document.uri); - const workspaceRootPath = - workspaceFolder && typeof workspaceFolder.uri.fsPath === 'string' ? workspaceFolder.uri.fsPath : undefined; - return typeof workspaceRootPath === 'string' ? workspaceRootPath : path.dirname(document.uri.fsPath); - } - - protected getWorkingDirectoryPath(document: vscode.TextDocument): string { - return this._pythonSettings.linting.cwd || this.getWorkspaceRootPath(document); - } - - protected abstract runLinter( - document: vscode.TextDocument, - cancellation: vscode.CancellationToken, - ): Promise; - - // eslint-disable-next-line class-methods-use-this - protected parseMessagesSeverity( - error: string, - categorySeverity: - | Flake8CategorySeverity - | IMypyCategorySeverity - | IPycodestyleCategorySeverity - | IPylintCategorySeverity, - ): LintMessageSeverity { - const severity = error as keyof typeof categorySeverity; - - if (categorySeverity[severity]) { - const severityName = categorySeverity[severity]; - switch (severityName) { - case 'Error': - return LintMessageSeverity.Error; - case 'Hint': - return LintMessageSeverity.Hint; - case 'Information': - return LintMessageSeverity.Information; - case 'Warning': - return LintMessageSeverity.Warning; - default: { - if (LintMessageSeverity[severityName]) { - return (LintMessageSeverity[severityName] as unknown) as LintMessageSeverity; - } - } - } - } - return LintMessageSeverity.Information; - } - - protected async run( - args: string[], - document: vscode.TextDocument, - cancellation: vscode.CancellationToken, - regEx: string = REGEX, - ): Promise { - if (!this.info.isEnabled(document.uri)) { - return []; - } - const executionInfo = this.info.getExecutionInfo(args, document.uri); - const cwd = this.getWorkingDirectoryPath(document); - const pythonToolsExecutionService = this.serviceContainer.get( - IPythonToolExecutionService, - ); - try { - const result = await pythonToolsExecutionService.execForLinter( - executionInfo, - { cwd, token: cancellation, mergeStdOutErr: false }, - document.uri, - ); - this.displayLinterResultHeader(result.stdout); - return await this.parseMessages(result.stdout, document, cancellation, regEx); - } catch (error) { - await this.handleError(error as Error, document.uri, executionInfo); - return []; - } - } - - protected async parseMessages( - output: string, - _document: vscode.TextDocument, - _token: vscode.CancellationToken, - regEx: string, - ): Promise { - const outputLines = splitLines(output, { removeEmptyEntries: false, trim: false }); - return this.parseLines(outputLines, regEx); - } - - protected async handleError(error: Error, resource: vscode.Uri, execInfo: ExecutionInfo): Promise { - if (isTestExecution()) { - this.errorHandler.handleError(error, resource, execInfo).ignoreErrors(); - } else { - this.errorHandler - .handleError(error, resource, execInfo) - .catch((ex) => traceError('Error in errorHandler.handleError', ex)) - .ignoreErrors(); - } - } - - private parseLine(line: string, regEx: string): ILintMessage | undefined { - return parseLine(line, regEx, this.info.id, this.columnOffset); - } - - private parseLines(outputLines: string[], regEx: string): ILintMessage[] { - const messages: ILintMessage[] = []; - for (const line of outputLines) { - try { - const msg = this.parseLine(line, regEx); - if (msg) { - messages.push(msg); - if (messages.length >= this.pythonSettings.linting.maxNumberOfProblems) { - break; - } - } - } catch (ex) { - traceError(`Linter '${this.info.id}' failed to parse the line '${line}.`, ex); - } - } - return messages; - } - - private displayLinterResultHeader(data: string) { - traceLog(`${'#'.repeat(10)}Linting Output - ${this.info.id}${'#'.repeat(10)}\n`); - traceLog(data); - } -} diff --git a/src/client/linters/constants.ts b/src/client/linters/constants.ts deleted file mode 100644 index 27b7c80db7f4..000000000000 --- a/src/client/linters/constants.ts +++ /dev/null @@ -1,19 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; - -import { Product } from '../common/types'; -import { LinterId } from './types'; - -// All supported linters must be in this map. -export const LINTERID_BY_PRODUCT = new Map([ - [Product.bandit, LinterId.Bandit], - [Product.flake8, LinterId.Flake8], - [Product.pylint, LinterId.PyLint], - [Product.mypy, LinterId.MyPy], - [Product.pycodestyle, LinterId.PyCodeStyle], - [Product.prospector, LinterId.Prospector], - [Product.pydocstyle, LinterId.PyDocStyle], - [Product.pylama, LinterId.PyLama], -]); diff --git a/src/client/linters/errorHandlers/baseErrorHandler.ts b/src/client/linters/errorHandlers/baseErrorHandler.ts deleted file mode 100644 index 16c5e93ae012..000000000000 --- a/src/client/linters/errorHandlers/baseErrorHandler.ts +++ /dev/null @@ -1,27 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -import { Uri } from 'vscode'; -import { ExecutionInfo, IInstaller, Product } from '../../common/types'; -import { IServiceContainer } from '../../ioc/types'; -import { IErrorHandler } from '../types'; - -export abstract class BaseErrorHandler implements IErrorHandler { - protected installer: IInstaller; - - private handler?: IErrorHandler; - - constructor(protected product: Product, protected serviceContainer: IServiceContainer) { - this.installer = this.serviceContainer.get(IInstaller); - } - - protected get nextHandler(): IErrorHandler | undefined { - return this.handler; - } - - public setNextHandler(handler: IErrorHandler): void { - this.handler = handler; - } - - public abstract handleError(error: Error, resource: Uri, execInfo: ExecutionInfo): Promise; -} diff --git a/src/client/linters/errorHandlers/errorHandler.ts b/src/client/linters/errorHandlers/errorHandler.ts deleted file mode 100644 index dc884e97739c..000000000000 --- a/src/client/linters/errorHandlers/errorHandler.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { Uri } from 'vscode'; -import { ExecutionInfo, Product } from '../../common/types'; -import { IServiceContainer } from '../../ioc/types'; -import { IErrorHandler } from '../types'; -import { BaseErrorHandler } from './baseErrorHandler'; -import { NotInstalledErrorHandler } from './notInstalled'; -import { StandardErrorHandler } from './standard'; - -export class ErrorHandler implements IErrorHandler { - private handler: BaseErrorHandler; - - constructor(product: Product, serviceContainer: IServiceContainer) { - // Create chain of handlers. - const standardErrorHandler = new StandardErrorHandler(product, serviceContainer); - this.handler = new NotInstalledErrorHandler(product, serviceContainer); - this.handler.setNextHandler(standardErrorHandler); - } - - public handleError(error: Error, resource: Uri, execInfo: ExecutionInfo): Promise { - return this.handler.handleError(error, resource, execInfo); - } -} diff --git a/src/client/linters/errorHandlers/notInstalled.ts b/src/client/linters/errorHandlers/notInstalled.ts deleted file mode 100644 index 8c598ae5ece2..000000000000 --- a/src/client/linters/errorHandlers/notInstalled.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { Uri } from 'vscode'; -import { IPythonExecutionFactory } from '../../common/process/types'; -import { ExecutionInfo } from '../../common/types'; -import { traceError, traceLog, traceWarn } from '../../logging'; -import { ILinterManager } from '../types'; -import { BaseErrorHandler } from './baseErrorHandler'; - -export class NotInstalledErrorHandler extends BaseErrorHandler { - public async handleError(error: Error, resource: Uri, execInfo: ExecutionInfo): Promise { - const pythonExecutionService = await this.serviceContainer - .get(IPythonExecutionFactory) - .create({ resource }); - const isModuleInstalled = await pythonExecutionService.isModuleInstalled(execInfo.moduleName!); - if (isModuleInstalled) { - return this.nextHandler ? this.nextHandler.handleError(error, resource, execInfo) : false; - } - - this.installer - .promptToInstall(this.product, resource) - .catch((ex) => traceError('NotInstalledErrorHandler.promptToInstall', ex)); - - const linterManager = this.serviceContainer.get(ILinterManager); - const info = linterManager.getLinterInfo(execInfo.product!); - const customError = `Linter '${info.id}' is not installed. Please install it or select another linter".`; - traceLog(`\n${customError}\n${error}`); - traceWarn(customError, error); - return true; - } -} diff --git a/src/client/linters/errorHandlers/standard.ts b/src/client/linters/errorHandlers/standard.ts deleted file mode 100644 index f6e04b50ff19..000000000000 --- a/src/client/linters/errorHandlers/standard.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { l10n, Uri } from 'vscode'; -import { IApplicationShell } from '../../common/application/types'; -import { ExecutionInfo, ILogOutputChannel } from '../../common/types'; -import { traceError, traceLog } from '../../logging'; -import { ILinterManager, LinterId } from '../types'; -import { BaseErrorHandler } from './baseErrorHandler'; - -export class StandardErrorHandler extends BaseErrorHandler { - public async handleError(error: Error, resource: Uri, execInfo: ExecutionInfo): Promise { - if ( - typeof error === 'string' && - (error as string).includes("OSError: [Errno 2] No such file or directory: '/") - ) { - return this.nextHandler ? this.nextHandler.handleError(error, resource, execInfo) : Promise.resolve(false); - } - - const linterManager = this.serviceContainer.get(ILinterManager); - const info = linterManager.getLinterInfo(execInfo.product!); - - traceError(`There was an error in running the linter ${info.id}`, error); - traceLog(`Linting with ${info.id} failed.`); - traceLog(error.toString()); - - this.displayLinterError(info.id).ignoreErrors(); - return true; - } - - private async displayLinterError(linterId: LinterId) { - const message = l10n.t("There was an error in running the linter '{0}'", linterId); - const appShell = this.serviceContainer.get(IApplicationShell); - const outputChannel = this.serviceContainer.get(ILogOutputChannel); - const action = await appShell.showErrorMessage(message, 'View Errors'); - if (action === 'View Errors') { - outputChannel.show(); - } - } -} diff --git a/src/client/linters/flake8.ts b/src/client/linters/flake8.ts deleted file mode 100644 index e79d09158741..000000000000 --- a/src/client/linters/flake8.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { CancellationToken, TextDocument } from 'vscode'; -import '../common/extensions'; -import { Product } from '../common/types'; -import { IServiceContainer } from '../ioc/types'; -import { traceLog } from '../logging'; -import { BaseLinter } from './baseLinter'; -import { isExtensionEnabled } from './prompts/common'; -import { FLAKE8_EXTENSION } from './prompts/flake8Prompt'; -import { IToolsExtensionPrompt } from './prompts/types'; -import { ILintMessage } from './types'; - -const COLUMN_OFF_SET = 1; - -export class Flake8 extends BaseLinter { - constructor(serviceContainer: IServiceContainer, private readonly prompt: IToolsExtensionPrompt) { - super(Product.flake8, serviceContainer, COLUMN_OFF_SET); - } - - protected async runLinter(document: TextDocument, cancellation: CancellationToken): Promise { - await this.prompt.showPrompt(); - - if (isExtensionEnabled(this.serviceContainer, FLAKE8_EXTENSION)) { - traceLog( - 'LINTING: Skipping linting from Python extension, since Flake8 extension is installed and enabled.', - ); - return []; - } - - const messages = await this.run([document.uri.fsPath], document, cancellation); - messages.forEach((msg) => { - msg.severity = this.parseMessagesSeverity(msg.type, this.pythonSettings.linting.flake8CategorySeverity); - // flake8 uses 0th line for some file-wide problems - // but diagnostics expects positive line numbers. - if (msg.line === 0) { - msg.line = 1; - } - }); - return messages; - } -} diff --git a/src/client/linters/linterCommands.ts b/src/client/linters/linterCommands.ts deleted file mode 100644 index cc35e80f26b1..000000000000 --- a/src/client/linters/linterCommands.ts +++ /dev/null @@ -1,114 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; - -import { DiagnosticCollection, Disposable, l10n, QuickPickOptions, Uri } from 'vscode'; -import { IApplicationShell, ICommandManager, IDocumentManager } from '../common/application/types'; -import { Commands } from '../common/constants'; -import { IDisposable } from '../common/types'; -import { Common } from '../common/utils/localize'; -import { IServiceContainer } from '../ioc/types'; -import { sendTelemetryEvent } from '../telemetry'; -import { EventName } from '../telemetry/constants'; -import { ILinterManager, ILintingEngine, LinterId } from './types'; - -export class LinterCommands implements IDisposable { - private disposables: Disposable[] = []; - - private linterManager: ILinterManager; - - private readonly appShell: IApplicationShell; - - private readonly documentManager: IDocumentManager; - - constructor(private serviceContainer: IServiceContainer) { - this.linterManager = this.serviceContainer.get(ILinterManager); - this.appShell = this.serviceContainer.get(IApplicationShell); - this.documentManager = this.serviceContainer.get(IDocumentManager); - - const commandManager = this.serviceContainer.get(ICommandManager); - commandManager.registerCommand(Commands.Set_Linter, this.setLinterAsync.bind(this)); - commandManager.registerCommand(Commands.Enable_Linter, this.enableLintingAsync.bind(this)); - commandManager.registerCommand(Commands.Run_Linter, this.runLinting.bind(this)); - } - - public dispose(): void { - this.disposables.forEach((disposable) => disposable.dispose()); - } - - public async setLinterAsync(): Promise { - const linters = this.linterManager.getAllLinterInfos(); - const suggestions = linters.map((x) => x.id).sort(); - const linterList = ['Disable Linting', ...suggestions]; - const activeLinters = await this.linterManager.getActiveLinters(this.settingsUri); - - let current: string; - switch (activeLinters.length) { - case 0: - current = 'none'; - break; - case 1: - current = activeLinters[0].id; - break; - default: - current = 'multiple selected'; - break; - } - - const quickPickOptions: QuickPickOptions = { - matchOnDetail: true, - matchOnDescription: true, - placeHolder: `current: ${current}`, - }; - - const selection = await this.appShell.showQuickPick(linterList, quickPickOptions); - if (selection !== undefined) { - if (selection === 'Disable Linting') { - await this.linterManager.enableLintingAsync(false); - sendTelemetryEvent(EventName.SELECT_LINTER, undefined, { enabled: false }); - } else { - const index = linters.findIndex((x) => x.id === selection); - if (activeLinters.length > 1) { - const response = await this.appShell.showWarningMessage( - l10n.t("Multiple linters are enabled in settings. Replace with '{0}'?", selection), - Common.bannerLabelYes, - Common.bannerLabelNo, - ); - if (response !== Common.bannerLabelYes) { - return; - } - } - await this.linterManager.setActiveLintersAsync([linters[index].product], this.settingsUri); - sendTelemetryEvent(EventName.SELECT_LINTER, undefined, { tool: selection as LinterId, enabled: true }); - } - } - } - - public async enableLintingAsync(): Promise { - const options = ['Enable', 'Disable']; - const current = (await this.linterManager.isLintingEnabled(this.settingsUri)) ? options[0] : options[1]; - - const quickPickOptions: QuickPickOptions = { - matchOnDetail: true, - matchOnDescription: true, - placeHolder: `current: ${current}`, - }; - - const selection = await this.appShell.showQuickPick(options, quickPickOptions); - - if (selection !== undefined) { - const enable: boolean = selection === options[0]; - await this.linterManager.enableLintingAsync(enable, this.settingsUri); - } - } - - public runLinting(): Promise { - const engine = this.serviceContainer.get(ILintingEngine); - return engine.lintOpenPythonFiles('manual'); - } - - private get settingsUri(): Uri | undefined { - return this.documentManager.activeTextEditor ? this.documentManager.activeTextEditor.document.uri : undefined; - } -} diff --git a/src/client/linters/linterInfo.ts b/src/client/linters/linterInfo.ts deleted file mode 100644 index 321f23b0f304..000000000000 --- a/src/client/linters/linterInfo.ts +++ /dev/null @@ -1,94 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -import * as path from 'path'; -import { Uri } from 'vscode'; -import { linterScript } from '../common/process/internal/scripts'; -import { ExecutionInfo, IConfigurationService, ILintingSettings, Product } from '../common/types'; -import { ILinterInfo, LinterId } from './types'; - -export class LinterInfo implements ILinterInfo { - private _id: LinterId; - - private _product: Product; - - private _configFileNames: string[]; - - constructor( - product: Product, - id: LinterId, - protected configService: IConfigurationService, - configFileNames: string[] = [], - ) { - this._product = product; - this._id = id; - this._configFileNames = configFileNames; - } - - public get id(): LinterId { - return this._id; - } - - public get product(): Product { - return this._product; - } - - public get pathSettingName(): string { - return `${this.id}Path`; - } - - public get argsSettingName(): string { - return `${this.id}Args`; - } - - public get enabledSettingName(): string { - return `${this.id}Enabled`; - } - - public get configFileNames(): string[] { - return this._configFileNames; - } - - public async enableAsync(enabled: boolean, resource?: Uri): Promise { - return this.configService.updateSetting(`linting.${this.enabledSettingName}`, enabled, resource); - } - - public isEnabled(resource?: Uri): boolean { - const settings = this.configService.getSettings(resource); - const name = this.enabledSettingName as keyof ILintingSettings; - return settings.linting[name] as boolean; - } - - public pathName(resource?: Uri): string { - const settings = this.configService.getSettings(resource); - const name = this.pathSettingName as keyof ILintingSettings; - return settings.linting[name] as string; - } - - public linterArgs(resource?: Uri): string[] { - const settings = this.configService.getSettings(resource); - const name = this.argsSettingName as keyof ILintingSettings; - const args = settings.linting[name]; - return Array.isArray(args) ? (args as string[]) : []; - } - - public getExecutionInfo(customArgs: string[], resource?: Uri): ExecutionInfo { - const execPath = this.pathName(resource); - const args = this.linterArgs(resource).concat(customArgs); - const script = linterScript(); - if (path.basename(execPath) === execPath) { - return { - execPath: undefined, - args: [script, '-m', this.id, ...args], - product: this.product, - moduleName: execPath, - }; - } - return { - execPath, - moduleName: this.id, - args: [script, '-p', this.id, execPath, ...args], - product: this.product, - }; - } -} diff --git a/src/client/linters/linterManager.ts b/src/client/linters/linterManager.ts deleted file mode 100644 index 72c92aa1c77d..000000000000 --- a/src/client/linters/linterManager.ts +++ /dev/null @@ -1,134 +0,0 @@ -/* eslint-disable max-classes-per-file */ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; - -import { inject, injectable } from 'inversify'; -import { CancellationToken, TextDocument, Uri } from 'vscode'; -import { IConfigurationService, Product } from '../common/types'; -import { IServiceContainer } from '../ioc/types'; -import { traceError } from '../logging'; -import { Bandit } from './bandit'; -import { Flake8 } from './flake8'; -import { LinterInfo } from './linterInfo'; -import { MyPy } from './mypy'; -import { getOrCreateFlake8Prompt } from './prompts/flake8Prompt'; -import { getOrCreatePylintPrompt } from './prompts/pylintPrompt'; -import { Prospector } from './prospector'; -import { Pycodestyle } from './pycodestyle'; -import { PyDocStyle } from './pydocstyle'; -import { PyLama } from './pylama'; -import { Pylint } from './pylint'; -import { ILinter, ILinterInfo, ILinterManager, ILintMessage, LinterId } from './types'; - -class DisabledLinter implements ILinter { - constructor(private configService: IConfigurationService) {} - - public get info() { - return new LinterInfo(Product.pylint, LinterId.PyLint, this.configService); - } - - // eslint-disable-next-line class-methods-use-this - public async lint(_document: TextDocument, _cancellation: CancellationToken): Promise { - return []; - } -} - -@injectable() -export class LinterManager implements ILinterManager { - protected linters: ILinterInfo[]; - - constructor(@inject(IConfigurationService) private configService: IConfigurationService) { - // Note that we use unit tests to ensure all the linters are here. - this.linters = [ - new LinterInfo(Product.bandit, LinterId.Bandit, this.configService), - new LinterInfo(Product.flake8, LinterId.Flake8, this.configService), - new LinterInfo(Product.pylint, LinterId.PyLint, this.configService, ['pylintrc', '.pylintrc']), - new LinterInfo(Product.mypy, LinterId.MyPy, this.configService), - new LinterInfo(Product.pycodestyle, LinterId.PyCodeStyle, this.configService), - new LinterInfo(Product.prospector, LinterId.Prospector, this.configService), - new LinterInfo(Product.pydocstyle, LinterId.PyDocStyle, this.configService), - new LinterInfo(Product.pylama, LinterId.PyLama, this.configService), - ]; - } - - public getAllLinterInfos(): ILinterInfo[] { - return this.linters; - } - - public getLinterInfo(product: Product): ILinterInfo { - const x = this.linters.findIndex((value, _index, _obj) => value.product === product); - if (x >= 0) { - return this.linters[x]; - } - throw new Error(`Invalid linter '${Product[product]}'`); - } - - public async isLintingEnabled(resource?: Uri): Promise { - const settings = this.configService.getSettings(resource); - const activeLintersPresent = await this.getActiveLinters(resource); - return settings.linting.enabled && activeLintersPresent.length > 0; - } - - public async enableLintingAsync(enable: boolean, resource?: Uri): Promise { - await this.configService.updateSetting('linting.enabled', enable, resource); - } - - public async getActiveLinters(resource?: Uri): Promise { - return this.linters.filter((x) => x.isEnabled(resource)); - } - - public async setActiveLintersAsync(products: Product[], resource?: Uri): Promise { - // ensure we only allow valid linters to be set, otherwise leave things alone. - // filter out any invalid products: - const validProducts = products.filter((product) => { - const foundIndex = this.linters.findIndex((validLinter) => validLinter.product === product); - return foundIndex !== -1; - }); - - // if we have valid linter product(s), enable only those - if (validProducts.length > 0) { - const active = await this.getActiveLinters(resource); - for (const x of active) { - await x.enableAsync(false, resource); - } - if (products.length > 0) { - const toActivate = this.linters.filter((x) => products.findIndex((p) => x.product === p) >= 0); - for (const x of toActivate) { - await x.enableAsync(true, resource); - } - await this.enableLintingAsync(true, resource); - } - } - } - - public async createLinter(product: Product, serviceContainer: IServiceContainer, resource?: Uri): Promise { - if (!(await this.isLintingEnabled(resource))) { - return new DisabledLinter(this.configService); - } - const error = 'Linter manager: Unknown linter'; - switch (product) { - case Product.bandit: - return new Bandit(serviceContainer); - case Product.flake8: - return new Flake8(serviceContainer, getOrCreateFlake8Prompt(serviceContainer)); - case Product.pylint: - return new Pylint(serviceContainer, getOrCreatePylintPrompt(serviceContainer)); - case Product.mypy: - return new MyPy(serviceContainer); - case Product.prospector: - return new Prospector(serviceContainer); - case Product.pylama: - return new PyLama(serviceContainer); - case Product.pydocstyle: - return new PyDocStyle(serviceContainer); - case Product.pycodestyle: - return new Pycodestyle(serviceContainer); - default: - traceError(error); - break; - } - throw new Error(error); - } -} diff --git a/src/client/linters/lintingEngine.ts b/src/client/linters/lintingEngine.ts deleted file mode 100644 index 2a4bf4e10848..000000000000 --- a/src/client/linters/lintingEngine.ts +++ /dev/null @@ -1,209 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; - -import { inject, injectable } from 'inversify'; -import { Minimatch } from 'minimatch'; -import * as path from 'path'; -import * as vscode from 'vscode'; -import { ICommandManager, IDocumentManager, IWorkspaceService } from '../common/application/types'; -import { Commands } from '../common/constants'; -import { IFileSystem } from '../common/platform/types'; -import { IConfigurationService } from '../common/types'; -import { isNotebookCell, noop } from '../common/utils/misc'; -import { StopWatch } from '../common/utils/stopWatch'; -import { IInterpreterService } from '../interpreter/contracts'; -import { IServiceContainer } from '../ioc/types'; -import { sendTelemetryWhenDone } from '../telemetry'; -import { EventName } from '../telemetry/constants'; -import { LinterTrigger, LintingTelemetry } from '../telemetry/types'; -import { ILinterInfo, ILinterManager, ILintingEngine, ILintMessage, LintMessageSeverity } from './types'; - -const PYTHON: vscode.DocumentFilter = { language: 'python' }; - -const lintSeverityToVSSeverity = new Map(); -lintSeverityToVSSeverity.set(LintMessageSeverity.Error, vscode.DiagnosticSeverity.Error); -lintSeverityToVSSeverity.set(LintMessageSeverity.Hint, vscode.DiagnosticSeverity.Hint); -lintSeverityToVSSeverity.set(LintMessageSeverity.Information, vscode.DiagnosticSeverity.Information); -lintSeverityToVSSeverity.set(LintMessageSeverity.Warning, vscode.DiagnosticSeverity.Warning); - -@injectable() -export class LintingEngine implements ILintingEngine { - private workspace: IWorkspaceService; - - private documents: IDocumentManager; - - private configurationService: IConfigurationService; - - private linterManager: ILinterManager; - - private diagnosticCollection: vscode.DiagnosticCollection; - - private pendingLintings = new Map(); - - private fileSystem: IFileSystem; - - constructor(@inject(IServiceContainer) private serviceContainer: IServiceContainer) { - this.documents = serviceContainer.get(IDocumentManager); - this.workspace = serviceContainer.get(IWorkspaceService); - this.configurationService = serviceContainer.get(IConfigurationService); - this.linterManager = serviceContainer.get(ILinterManager); - this.fileSystem = serviceContainer.get(IFileSystem); - this.diagnosticCollection = vscode.languages.createDiagnosticCollection('python'); - } - - public get diagnostics(): vscode.DiagnosticCollection { - return this.diagnosticCollection; - } - - public clearDiagnostics(document: vscode.TextDocument): void { - if (this.diagnosticCollection.has(document.uri)) { - this.diagnosticCollection.delete(document.uri); - } - } - - public async lintOpenPythonFiles(trigger: LinterTrigger = 'auto'): Promise { - this.diagnosticCollection.clear(); - const promises = this.documents.textDocuments.map(async (document) => this.lintDocument(document, trigger)); - await Promise.all(promises); - return this.diagnosticCollection; - } - - public async lintDocument(document: vscode.TextDocument, trigger: LinterTrigger): Promise { - if (isNotebookCell(document)) { - return; - } - this.diagnosticCollection.set(document.uri, []); - - // Check if we need to lint this document - if (!(await this.shouldLintDocument(document, trigger))) { - return; - } - - if (this.pendingLintings.has(document.uri.fsPath)) { - this.pendingLintings.get(document.uri.fsPath)!.cancel(); - this.pendingLintings.delete(document.uri.fsPath); - } - - const cancelToken = new vscode.CancellationTokenSource(); - cancelToken.token.onCancellationRequested(() => { - if (this.pendingLintings.has(document.uri.fsPath)) { - this.pendingLintings.delete(document.uri.fsPath); - } - }); - - this.pendingLintings.set(document.uri.fsPath, cancelToken); - - const activeLinters = await this.linterManager.getActiveLinters(document.uri); - const promises: Promise[] = activeLinters.map(async (info: ILinterInfo) => { - const stopWatch = new StopWatch(); - const linter = await this.linterManager.createLinter(info.product, this.serviceContainer, document.uri); - const promise = linter.lint(document, cancelToken.token); - this.sendLinterRunTelemetry(info, document.uri, promise, stopWatch, trigger); - return promise; - }); - - // linters will resolve asynchronously - keep a track of all - // diagnostics reported as them come in. - let diagnostics: vscode.Diagnostic[] = []; - const settings = this.configurationService.getSettings(document.uri); - - for (const p of promises) { - const msgs = await p; - if (cancelToken.token.isCancellationRequested) { - break; - } - - if (this.isDocumentOpen(document.uri)) { - // Build the message and suffix the message with the name of the linter used. - for (const m of msgs) { - diagnostics.push(this.createDiagnostics(m, document)); - } - // Limit the number of messages to the max value. - diagnostics = diagnostics.filter((_value, index) => index <= settings.linting.maxNumberOfProblems); - } - } - // Set all diagnostics found in this pass, as this method always clears existing diagnostics. - this.diagnosticCollection.set(document.uri, diagnostics); - } - - // eslint-disable-next-line class-methods-use-this - private sendLinterRunTelemetry( - info: ILinterInfo, - resource: vscode.Uri, - promise: Promise, - stopWatch: StopWatch, - trigger: LinterTrigger, - ): void { - const linterExecutablePathName = info.pathName(resource); - const properties: LintingTelemetry = { - tool: info.id, - hasCustomArgs: info.linterArgs(resource).length > 0, - trigger, - executableSpecified: linterExecutablePathName !== info.id, - }; - sendTelemetryWhenDone(EventName.LINTING, promise, stopWatch, properties); - } - - private isDocumentOpen(uri: vscode.Uri): boolean { - return this.documents.textDocuments.some((document) => document.uri.fsPath === uri.fsPath); - } - - // eslint-disable-next-line class-methods-use-this - private createDiagnostics(message: ILintMessage, _document: vscode.TextDocument): vscode.Diagnostic { - const position = new vscode.Position(message.line - 1, message.column); - let endPosition: vscode.Position = position; - if (message.endLine && message.endColumn) { - endPosition = new vscode.Position(message.endLine - 1, message.endColumn); - } - const range = new vscode.Range(position, endPosition); - - const severity = lintSeverityToVSSeverity.get(message.severity!)!; - const diagnostic = new vscode.Diagnostic(range, message.message, severity); - diagnostic.code = message.code; - diagnostic.source = message.provider; - return diagnostic; - } - - private async shouldLintDocument(document: vscode.TextDocument, trigger: LinterTrigger): Promise { - const interpreterService = this.serviceContainer.get(IInterpreterService); - const interpreter = await interpreterService.getActiveInterpreter(document.uri); - if (!interpreter && trigger === 'manual') { - this.serviceContainer - .get(ICommandManager) - .executeCommand(Commands.TriggerEnvironmentSelection, document.uri) - .then(noop, noop); - return false; - } - if (!(await this.linterManager.isLintingEnabled(document.uri))) { - this.diagnosticCollection.set(document.uri, []); - return false; - } - - if (document.languageId !== PYTHON.language) { - return false; - } - - const workspaceFolder = this.workspace.getWorkspaceFolder(document.uri); - const workspaceRootPath = - workspaceFolder && typeof workspaceFolder.uri.fsPath === 'string' ? workspaceFolder.uri.fsPath : undefined; - const relativeFileName = - typeof workspaceRootPath === 'string' - ? path.relative(workspaceRootPath, document.fileName) - : document.fileName; - - const settings = this.configurationService.getSettings(document.uri); - // { dot: true } is important so dirs like `.venv` will be matched by globs - const ignoreMinmatches = settings.linting.ignorePatterns.map( - (pattern) => new Minimatch(pattern, { dot: true }), - ); - if (ignoreMinmatches.some((matcher) => matcher.match(document.fileName) || matcher.match(relativeFileName))) { - return false; - } - if (document.uri.scheme !== 'file' || !document.uri.fsPath) { - return false; - } - return this.fileSystem.fileExists(document.uri.fsPath); - } -} diff --git a/src/client/linters/mypy.ts b/src/client/linters/mypy.ts deleted file mode 100644 index f39eef99b422..000000000000 --- a/src/client/linters/mypy.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { CancellationToken, TextDocument } from 'vscode'; -import '../common/extensions'; -import { escapeRegExp } from 'lodash'; -import { Product } from '../common/types'; -import { IServiceContainer } from '../ioc/types'; -import { BaseLinter } from './baseLinter'; -import { ILintMessage } from './types'; - -export function getRegex(filepath: string): string { - return `${escapeRegExp(filepath)}:(?\\d+)(:(?\\d+))?: (?\\w+): (?.*)\\r?(\\n|$)`; -} -const COLUMN_OFF_SET = 1; - -export class MyPy extends BaseLinter { - constructor(serviceContainer: IServiceContainer) { - super(Product.mypy, serviceContainer, COLUMN_OFF_SET); - } - - protected async runLinter(document: TextDocument, cancellation: CancellationToken): Promise { - const relativeFilePath = document.uri.fsPath.slice(this.getWorkspaceRootPath(document).length + 1); - const regex = getRegex(relativeFilePath); - const messages = await this.run([document.uri.fsPath], document, cancellation, regex); - messages.forEach((msg) => { - msg.severity = this.parseMessagesSeverity(msg.type, this.pythonSettings.linting.mypyCategorySeverity); - msg.code = msg.type; - }); - return messages; - } -} diff --git a/src/client/linters/prompts/common.ts b/src/client/linters/prompts/common.ts deleted file mode 100644 index ab88282db607..000000000000 --- a/src/client/linters/prompts/common.ts +++ /dev/null @@ -1,52 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -import * as fs from 'fs-extra'; -import * as path from 'path'; -import { ShowToolsExtensionPrompt } from '../../common/experiments/groups'; -import { IExperimentService, IExtensions, IPersistentState, IPersistentStateFactory } from '../../common/types'; -import { IServiceContainer } from '../../ioc/types'; -import { traceLog } from '../../logging'; - -export function isExtensionDisabled(serviceContainer: IServiceContainer, extensionId: string): boolean { - const extensions: IExtensions = serviceContainer.get(IExtensions); - // When debugging the python extension this `extensionPath` below will point to your repo. - // If you are debugging this feature then set the `extensionPath` to right location after - // the next line. - const pythonExt = extensions.getExtension('ms-python.python'); - if (pythonExt) { - let found = false; - traceLog(`Extension search path: ${path.dirname(pythonExt.extensionPath)}`); - fs.readdirSync(path.dirname(pythonExt.extensionPath), { withFileTypes: false }).forEach((s) => { - if (s.toString().startsWith(extensionId)) { - found = true; - } - }); - return found; - } - return false; -} - -/** - * Detects if extension is installed and enabled. - */ -export function isExtensionEnabled(serviceContainer: IServiceContainer, extensionId: string): boolean { - const extensions: IExtensions = serviceContainer.get(IExtensions); - const extension = extensions.getExtension(extensionId); - return extension !== undefined; -} - -export function doNotShowPromptState( - serviceContainer: IServiceContainer, - promptKey: string, -): IPersistentState { - const persistFactory: IPersistentStateFactory = serviceContainer.get( - IPersistentStateFactory, - ); - return persistFactory.createWorkspacePersistentState(promptKey, false); -} - -export function inToolsExtensionsExperiment(serviceContainer: IServiceContainer): Promise { - const experiments: IExperimentService = serviceContainer.get(IExperimentService); - return experiments.inExperiment(ShowToolsExtensionPrompt.experiment); -} diff --git a/src/client/linters/prompts/flake8Prompt.ts b/src/client/linters/prompts/flake8Prompt.ts deleted file mode 100644 index fa1969df682a..000000000000 --- a/src/client/linters/prompts/flake8Prompt.ts +++ /dev/null @@ -1,73 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -import { IApplicationEnvironment } from '../../common/application/types'; -import { Common, ToolsExtensions } from '../../common/utils/localize'; -import { executeCommand } from '../../common/vscodeApis/commandApis'; -import { showInformationMessage } from '../../common/vscodeApis/windowApis'; -import { IServiceContainer } from '../../ioc/types'; -import { sendTelemetryEvent } from '../../telemetry'; -import { EventName } from '../../telemetry/constants'; -import { doNotShowPromptState, inToolsExtensionsExperiment, isExtensionDisabled, isExtensionEnabled } from './common'; -import { IToolsExtensionPrompt } from './types'; - -export const FLAKE8_EXTENSION = 'ms-python.flake8'; -const FLAKE8_PROMPT_DONOTSHOW_KEY = 'showFlake8ExtensionPrompt'; - -export class Flake8ExtensionPrompt implements IToolsExtensionPrompt { - private shownThisSession = false; - - public constructor(private readonly serviceContainer: IServiceContainer) {} - - public async showPrompt(): Promise { - const isEnabled = isExtensionEnabled(this.serviceContainer, FLAKE8_EXTENSION); - if (isEnabled || isExtensionDisabled(this.serviceContainer, FLAKE8_EXTENSION)) { - sendTelemetryEvent(EventName.TOOLS_EXTENSIONS_ALREADY_INSTALLED, undefined, { - extensionId: FLAKE8_EXTENSION, - isEnabled, - }); - return true; - } - - const doNotShow = doNotShowPromptState(this.serviceContainer, FLAKE8_PROMPT_DONOTSHOW_KEY); - if (this.shownThisSession || doNotShow.value) { - return false; - } - - if (!(await inToolsExtensionsExperiment(this.serviceContainer))) { - return false; - } - - this.shownThisSession = true; - const response = await showInformationMessage( - ToolsExtensions.flake8PromptMessage, - ToolsExtensions.installFlake8Extension, - Common.doNotShowAgain, - ); - - if (response === Common.doNotShowAgain) { - doNotShow.updateValue(true); - return false; - } - - if (response === ToolsExtensions.installFlake8Extension) { - const appEnv: IApplicationEnvironment = this.serviceContainer.get( - IApplicationEnvironment, - ); - await executeCommand('workbench.extensions.installExtension', FLAKE8_EXTENSION, { - installPreReleaseVersion: appEnv.extensionChannel === 'insiders', - }); - return true; - } - - return false; - } -} - -let _prompt: IToolsExtensionPrompt | undefined; -export function getOrCreateFlake8Prompt(serviceContainer: IServiceContainer): IToolsExtensionPrompt { - if (!_prompt) { - _prompt = new Flake8ExtensionPrompt(serviceContainer); - } - return _prompt; -} diff --git a/src/client/linters/prompts/pylintPrompt.ts b/src/client/linters/prompts/pylintPrompt.ts deleted file mode 100644 index 37e583243078..000000000000 --- a/src/client/linters/prompts/pylintPrompt.ts +++ /dev/null @@ -1,86 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -import { IApplicationEnvironment } from '../../common/application/types'; -import { Common, ToolsExtensions } from '../../common/utils/localize'; -import { executeCommand } from '../../common/vscodeApis/commandApis'; -import { showInformationMessage } from '../../common/vscodeApis/windowApis'; -import { IServiceContainer } from '../../ioc/types'; -import { sendTelemetryEvent } from '../../telemetry'; -import { EventName } from '../../telemetry/constants'; -import { doNotShowPromptState, inToolsExtensionsExperiment, isExtensionDisabled, isExtensionEnabled } from './common'; -import { IToolsExtensionPrompt } from './types'; - -export const PYLINT_EXTENSION = 'ms-python.pylint'; -const PYLINT_PROMPT_DONOTSHOW_KEY = 'showPylintExtensionPrompt'; - -export class PylintExtensionPrompt implements IToolsExtensionPrompt { - private shownThisSession = false; - - public constructor(private readonly serviceContainer: IServiceContainer) {} - - public async showPrompt(): Promise { - const isEnabled = isExtensionEnabled(this.serviceContainer, PYLINT_EXTENSION); - if (isEnabled || isExtensionDisabled(this.serviceContainer, PYLINT_EXTENSION)) { - sendTelemetryEvent(EventName.TOOLS_EXTENSIONS_ALREADY_INSTALLED, undefined, { - extensionId: PYLINT_EXTENSION, - isEnabled, - }); - return true; - } - - const doNotShow = doNotShowPromptState(this.serviceContainer, PYLINT_PROMPT_DONOTSHOW_KEY); - if (this.shownThisSession || doNotShow.value) { - return false; - } - - if (!(await inToolsExtensionsExperiment(this.serviceContainer))) { - return false; - } - - sendTelemetryEvent(EventName.TOOLS_EXTENSIONS_PROMPT_SHOWN, undefined, { extensionId: PYLINT_EXTENSION }); - this.shownThisSession = true; - const response = await showInformationMessage( - ToolsExtensions.pylintPromptMessage, - ToolsExtensions.installPylintExtension, - Common.doNotShowAgain, - ); - - if (response === Common.doNotShowAgain) { - await doNotShow.updateValue(true); - sendTelemetryEvent(EventName.TOOLS_EXTENSIONS_PROMPT_DISMISSED, undefined, { - extensionId: PYLINT_EXTENSION, - dismissType: 'doNotShow', - }); - return false; - } - - if (response === ToolsExtensions.installPylintExtension) { - sendTelemetryEvent(EventName.TOOLS_EXTENSIONS_INSTALL_SELECTED, undefined, { - extensionId: PYLINT_EXTENSION, - }); - const appEnv: IApplicationEnvironment = this.serviceContainer.get( - IApplicationEnvironment, - ); - await executeCommand('workbench.extensions.installExtension', PYLINT_EXTENSION, { - installPreReleaseVersion: appEnv.extensionChannel === 'insiders', - }); - return true; - } - - sendTelemetryEvent(EventName.TOOLS_EXTENSIONS_PROMPT_DISMISSED, undefined, { - extensionId: PYLINT_EXTENSION, - dismissType: 'close', - }); - - return false; - } -} - -let _prompt: IToolsExtensionPrompt | undefined; -export function getOrCreatePylintPrompt(serviceContainer: IServiceContainer): IToolsExtensionPrompt { - if (!_prompt) { - _prompt = new PylintExtensionPrompt(serviceContainer); - } - return _prompt; -} diff --git a/src/client/linters/prospector.ts b/src/client/linters/prospector.ts deleted file mode 100644 index fa4b3907255b..000000000000 --- a/src/client/linters/prospector.ts +++ /dev/null @@ -1,69 +0,0 @@ -import * as path from 'path'; -import { CancellationToken, TextDocument } from 'vscode'; -import '../common/extensions'; -import { Product } from '../common/types'; -import { IServiceContainer } from '../ioc/types'; -import { traceError, traceLog } from '../logging'; -import { BaseLinter } from './baseLinter'; -import { ILintMessage } from './types'; - -interface IProspectorResponse { - messages: IProspectorMessage[]; -} -interface IProspectorMessage { - source: string; - message: string; - code: string; - location: IProspectorLocation; -} -interface IProspectorLocation { - function: string; - path: string; - line: number; - character: number; - module: 'beforeFormat'; -} - -export class Prospector extends BaseLinter { - constructor(serviceContainer: IServiceContainer) { - super(Product.prospector, serviceContainer); - } - - protected async runLinter(document: TextDocument, cancellation: CancellationToken): Promise { - const cwd = this.getWorkingDirectoryPath(document); - const relativePath = path.relative(cwd, document.uri.fsPath); - return this.run([relativePath], document, cancellation); - } - - protected async parseMessages( - output: string, - _document: TextDocument, - _token: CancellationToken, - _regEx: string, - ): Promise { - let parsedData: IProspectorResponse; - try { - parsedData = JSON.parse(output); - } catch (ex) { - traceLog(`${'#'.repeat(10)}Linting Output - ${this.info.id}${'#'.repeat(10)}`); - traceLog(output); - traceError('Failed to parse Prospector output', ex); - return []; - } - return parsedData.messages - .filter((_value, index) => index <= this.pythonSettings.linting.maxNumberOfProblems) - .map((msg) => { - const lineNumber = - msg.location.line === null || Number.isNaN(msg.location.line) ? 1 : msg.location.line; - - return { - code: msg.code, - message: msg.message, - column: msg.location.character, - line: lineNumber, - type: msg.code, - provider: `${this.info.id} - ${msg.source}`, - }; - }); - } -} diff --git a/src/client/linters/pycodestyle.ts b/src/client/linters/pycodestyle.ts deleted file mode 100644 index 30517980e83c..000000000000 --- a/src/client/linters/pycodestyle.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { CancellationToken, TextDocument } from 'vscode'; -import '../common/extensions'; -import { Product } from '../common/types'; -import { IServiceContainer } from '../ioc/types'; -import { BaseLinter } from './baseLinter'; -import { ILintMessage } from './types'; - -const COLUMN_OFF_SET = 1; - -export class Pycodestyle extends BaseLinter { - constructor(serviceContainer: IServiceContainer) { - super(Product.pycodestyle, serviceContainer, COLUMN_OFF_SET); - } - - protected async runLinter(document: TextDocument, cancellation: CancellationToken): Promise { - const messages = await this.run([document.uri.fsPath], document, cancellation); - messages.forEach((msg) => { - msg.severity = this.parseMessagesSeverity( - msg.type, - this.pythonSettings.linting.pycodestyleCategorySeverity, - ); - }); - return messages; - } -} diff --git a/src/client/linters/pydocstyle.ts b/src/client/linters/pydocstyle.ts deleted file mode 100644 index 93c059440fe7..000000000000 --- a/src/client/linters/pydocstyle.ts +++ /dev/null @@ -1,89 +0,0 @@ -import * as path from 'path'; -import { CancellationToken, TextDocument } from 'vscode'; -import '../common/extensions'; -import { Product } from '../common/types'; -import { IServiceContainer } from '../ioc/types'; -import { traceError } from '../logging'; -import { IS_WINDOWS } from '../common/platform/constants'; -import { BaseLinter } from './baseLinter'; -import { ILintMessage, LintMessageSeverity } from './types'; - -export class PyDocStyle extends BaseLinter { - constructor(serviceContainer: IServiceContainer) { - super(Product.pydocstyle, serviceContainer); - } - - protected async runLinter(document: TextDocument, cancellation: CancellationToken): Promise { - const messages = await this.run([document.uri.fsPath], document, cancellation); - // All messages in pep8 are treated as warnings for now. - messages.forEach((msg) => { - msg.severity = LintMessageSeverity.Warning; - }); - - return messages; - } - - protected async parseMessages( - output: string, - document: TextDocument, - _token: CancellationToken, - _regEx: string, - ): Promise { - let outputLines = output.split(/\r?\n/g); - const baseFileName = path.basename(document.uri.fsPath); - - // Remember, the first line of the response contains the file name and line number, the next line contains the error message. - // So we have two lines per message, hence we need to take lines in pairs. - const maxLines = this.pythonSettings.linting.maxNumberOfProblems * 2; - // First line is almost always empty. - const oldOutputLines = outputLines.filter((line) => line.length > 0); - outputLines = []; - for (let counter = 0; counter < oldOutputLines.length / 2; counter += 1) { - outputLines.push(oldOutputLines[2 * counter] + oldOutputLines[2 * counter + 1]); - } - - return ( - outputLines - .filter((value, index) => index < maxLines && value.indexOf(':') >= 0) - .map((line) => { - // Windows will have a : after the drive letter (e.g. c:\). - if (IS_WINDOWS) { - return line.substring(line.indexOf(`${baseFileName}:`) + baseFileName.length + 1).trim(); - } - return line.substring(line.indexOf(':') + 1).trim(); - }) - // Iterate through the lines (skipping the messages). - // So, just iterate the response in pairs. - .map((line) => { - try { - if (line.trim().length === 0) { - return undefined; - } - const lineNumber = parseInt(line.substring(0, line.indexOf(' ')), 10); - const part = line.substring(line.indexOf(':') + 1).trim(); - const code = part.substring(0, part.indexOf(':')).trim(); - const message = part.substring(part.indexOf(':') + 1).trim(); - - const sourceLine = document.lineAt(lineNumber - 1).text; - const trimmedSourceLine = sourceLine.trim(); - const sourceStart = sourceLine.indexOf(trimmedSourceLine); - - return { - code, - message, - column: sourceStart, - line: lineNumber, - type: '', - provider: this.info.id, - } as ILintMessage; - } catch (ex) { - traceError(`Failed to parse pydocstyle line '${line}'`, ex); - } - - return undefined; - }) - .filter((item) => item !== undefined) - .map((item) => item!) - ); - } -} diff --git a/src/client/linters/pylama.ts b/src/client/linters/pylama.ts deleted file mode 100644 index d5930c839445..000000000000 --- a/src/client/linters/pylama.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { CancellationToken, TextDocument } from 'vscode'; -import '../common/extensions'; -import { Product } from '../common/types'; -import { IServiceContainer } from '../ioc/types'; -import { BaseLinter } from './baseLinter'; -import { ILintMessage, LintMessageSeverity } from './types'; - -/** - * Example messages to parse from PyLama - * 1. Linter: pycodestyle - recent version removed an extra colon (:) after line:col, hence made it optional in the regex (to be backward compatibile) - * `src/test_py.py:23:60 [E] E226 missing whitespace around arithmetic operator [pycodestyle]` - * 2. Linter: mypy - output is missing the error code, something like `E226` - hence made it optional in the regex - * `src/test_py.py:7:4 [E] Argument 1 to "fn" has incompatible type "str"; expected "int" [mypy]` - */ - -const REGEX = - '(?.py):(?\\d+):(?\\d+):? \\[(?\\w+)\\]( (?\\w\\d+)?:?)? (?.*)\\r?(\\n|$)'; -const COLUMN_OFF_SET = 1; - -export class PyLama extends BaseLinter { - constructor(serviceContainer: IServiceContainer) { - super(Product.pylama, serviceContainer, COLUMN_OFF_SET); - } - - protected async runLinter(document: TextDocument, cancellation: CancellationToken): Promise { - const messages = await this.run([document.uri.fsPath], document, cancellation, REGEX); - // All messages in pylama are treated as warnings for now. - messages.forEach((msg) => { - msg.severity = LintMessageSeverity.Warning; - }); - - return messages; - } -} diff --git a/src/client/linters/pylint.ts b/src/client/linters/pylint.ts deleted file mode 100644 index 0b635417f906..000000000000 --- a/src/client/linters/pylint.ts +++ /dev/null @@ -1,96 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -import { CancellationToken, TextDocument } from 'vscode'; -import '../common/extensions'; -import { Product } from '../common/types'; -import { IServiceContainer } from '../ioc/types'; -import { traceError, traceLog } from '../logging'; -import { BaseLinter } from './baseLinter'; -import { isExtensionEnabled } from './prompts/common'; -import { PYLINT_EXTENSION } from './prompts/pylintPrompt'; -import { IToolsExtensionPrompt } from './prompts/types'; -import { ILintMessage } from './types'; - -interface IJsonMessage { - column: number | null; - line: number; - message: string; - symbol: string; - type: string; - endLine?: number | null; - endColumn?: number | null; -} - -export class Pylint extends BaseLinter { - constructor(serviceContainer: IServiceContainer, private readonly prompt: IToolsExtensionPrompt) { - super(Product.pylint, serviceContainer); - } - - protected async runLinter(document: TextDocument, cancellation: CancellationToken): Promise { - await this.prompt.showPrompt(); - - if (isExtensionEnabled(this.serviceContainer, PYLINT_EXTENSION)) { - traceLog( - 'LINTING: Skipping linting from Python extension, since Pylint extension is installed and enabled.', - ); - return []; - } - - const { uri } = document; - const settings = this.configService.getSettings(uri); - const args = [uri.fsPath]; - const messages = await this.run(args, document, cancellation); - messages.forEach((msg) => { - msg.severity = this.parseMessagesSeverity(msg.type, settings.linting.pylintCategorySeverity); - }); - return messages; - } - - private parseOutputMessage(outputMsg: IJsonMessage, colOffset = 0): ILintMessage | undefined { - // Both 'endLine' and 'endColumn' are only present on pylint 2.12.2+ - // If present, both can still be 'null' if AST node didn't have endLine and / or endColumn information. - // If 'endColumn' is 'null' or not preset, set it to 'undefined' to - // prevent the lintingEngine from inferring an error range. - if (outputMsg.endColumn) { - outputMsg.endColumn = outputMsg.endColumn <= 0 ? 0 : outputMsg.endColumn - colOffset; - } else { - outputMsg.endColumn = undefined; - } - - return { - code: outputMsg.symbol, - message: outputMsg.message, - column: outputMsg.column === null || outputMsg.column <= 0 ? 0 : outputMsg.column - colOffset, - line: outputMsg.line, - type: outputMsg.type, - provider: this.info.id, - endLine: outputMsg.endLine === null ? undefined : outputMsg.endLine, - endColumn: outputMsg.endColumn, - }; - } - - protected async parseMessages( - output: string, - _document: TextDocument, - _token: CancellationToken, - _: string, - ): Promise { - const messages: ILintMessage[] = []; - try { - const parsedOutput: IJsonMessage[] = JSON.parse(output); - for (const outputMsg of parsedOutput) { - const msg = this.parseOutputMessage(outputMsg, this.columnOffset); - if (msg) { - messages.push(msg); - if (messages.length >= this.pythonSettings.linting.maxNumberOfProblems) { - break; - } - } - } - } catch (ex) { - traceError(`Linter '${this.info.id}' failed to parse the output '${output}.`, ex); - } - return messages; - } -} diff --git a/src/client/linters/serviceRegistry.ts b/src/client/linters/serviceRegistry.ts deleted file mode 100644 index 26ada4d0cc8f..000000000000 --- a/src/client/linters/serviceRegistry.ts +++ /dev/null @@ -1,17 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; - -import { IExtensionActivationService } from '../activation/types'; -import { IServiceManager } from '../ioc/types'; -import { LinterProvider } from '../providers/linterProvider'; -import { LinterManager } from './linterManager'; -import { LintingEngine } from './lintingEngine'; -import { ILinterManager, ILintingEngine } from './types'; - -export function registerTypes(serviceManager: IServiceManager): void { - serviceManager.addSingleton(ILintingEngine, LintingEngine); - serviceManager.addSingleton(ILinterManager, LinterManager); - serviceManager.addSingleton(IExtensionActivationService, LinterProvider); -} diff --git a/src/client/linters/types.ts b/src/client/linters/types.ts deleted file mode 100644 index b24fe508ea1c..000000000000 --- a/src/client/linters/types.ts +++ /dev/null @@ -1,80 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; - -import * as vscode from 'vscode'; -import { ExecutionInfo, Product } from '../common/types'; -import { IServiceContainer } from '../ioc/types'; -import { LinterTrigger } from '../telemetry/types'; - -export interface IErrorHandler { - handleError(error: Error, resource: vscode.Uri, execInfo: ExecutionInfo): Promise; -} - -export enum LinterId { - Flake8 = 'flake8', - MyPy = 'mypy', - PyCodeStyle = 'pycodestyle', - Prospector = 'prospector', - PyDocStyle = 'pydocstyle', - PyLama = 'pylama', - PyLint = 'pylint', - Bandit = 'bandit', -} - -export interface ILinterInfo { - readonly id: LinterId; - readonly product: Product; - readonly pathSettingName: string; - readonly argsSettingName: string; - readonly enabledSettingName: string; - readonly configFileNames: string[]; - enableAsync(enabled: boolean, resource?: vscode.Uri): Promise; - isEnabled(resource?: vscode.Uri): boolean; - pathName(resource?: vscode.Uri): string; - linterArgs(resource?: vscode.Uri): string[]; - getExecutionInfo(customArgs: string[], resource?: vscode.Uri): ExecutionInfo; -} - -export interface ILinter { - readonly info: ILinterInfo; - lint(document: vscode.TextDocument, cancellation: vscode.CancellationToken): Promise; -} - -export const ILinterManager = Symbol('ILinterManager'); -export interface ILinterManager { - getAllLinterInfos(): ILinterInfo[]; - getLinterInfo(product: Product): ILinterInfo; - getActiveLinters(resource?: vscode.Uri): Promise; - isLintingEnabled(resource?: vscode.Uri): Promise; - enableLintingAsync(enable: boolean, resource?: vscode.Uri): Promise; - setActiveLintersAsync(products: Product[], resource?: vscode.Uri): Promise; - createLinter(product: Product, serviceContainer: IServiceContainer, resource?: vscode.Uri): Promise; -} - -export interface ILintMessage { - line: number; - column: number; - endLine?: number; - endColumn?: number; - code: string | undefined; - message: string; - type: string; - severity?: LintMessageSeverity; - provider: string; -} -export enum LintMessageSeverity { - Hint, - Error, - Warning, - Information, -} - -export const ILintingEngine = Symbol('ILintingEngine'); -export interface ILintingEngine { - readonly diagnostics: vscode.DiagnosticCollection; - lintOpenPythonFiles(trigger?: LinterTrigger): Promise; - lintDocument(document: vscode.TextDocument, trigger: LinterTrigger): Promise; - clearDiagnostics(document: vscode.TextDocument): void; -} diff --git a/src/client/providers/codeActionProvider/isortPrompt.ts b/src/client/providers/codeActionProvider/isortPrompt.ts deleted file mode 100644 index ffef481b498d..000000000000 --- a/src/client/providers/codeActionProvider/isortPrompt.ts +++ /dev/null @@ -1,89 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -import { IApplicationEnvironment } from '../../common/application/types'; -import { IPersistentState, IPersistentStateFactory } from '../../common/types'; -import { Common, ToolsExtensions } from '../../common/utils/localize'; -import { executeCommand } from '../../common/vscodeApis/commandApis'; -import { isExtensionDisabled, isExtensionEnabled } from '../../common/vscodeApis/extensionsApi'; -import { showInformationMessage } from '../../common/vscodeApis/windowApis'; -import { IServiceContainer } from '../../ioc/types'; -import { sendTelemetryEvent } from '../../telemetry'; -import { EventName } from '../../telemetry/constants'; - -export const ISORT_EXTENSION = 'ms-python.isort'; -const ISORT_PROMPT_DONOTSHOW_KEY = 'showISortExtensionPrompt'; - -function doNotShowPromptState(serviceContainer: IServiceContainer, promptKey: string): IPersistentState { - const persistFactory: IPersistentStateFactory = serviceContainer.get( - IPersistentStateFactory, - ); - return persistFactory.createWorkspacePersistentState(promptKey, false); -} - -export class ISortExtensionPrompt { - private shownThisSession = false; - - public constructor(private readonly serviceContainer: IServiceContainer) {} - - public async showPrompt(): Promise { - const isEnabled = isExtensionEnabled(ISORT_EXTENSION); - if (isEnabled || isExtensionDisabled(ISORT_EXTENSION)) { - sendTelemetryEvent(EventName.TOOLS_EXTENSIONS_ALREADY_INSTALLED, undefined, { - extensionId: ISORT_EXTENSION, - isEnabled, - }); - return true; - } - - const doNotShow = doNotShowPromptState(this.serviceContainer, ISORT_PROMPT_DONOTSHOW_KEY); - if (this.shownThisSession || doNotShow.value) { - return false; - } - - sendTelemetryEvent(EventName.TOOLS_EXTENSIONS_PROMPT_SHOWN, undefined, { extensionId: ISORT_EXTENSION }); - this.shownThisSession = true; - const response = await showInformationMessage( - ToolsExtensions.isortPromptMessage, - ToolsExtensions.installISortExtension, - Common.doNotShowAgain, - ); - - if (response === Common.doNotShowAgain) { - await doNotShow.updateValue(true); - sendTelemetryEvent(EventName.TOOLS_EXTENSIONS_PROMPT_DISMISSED, undefined, { - extensionId: ISORT_EXTENSION, - dismissType: 'doNotShow', - }); - return false; - } - - if (response === ToolsExtensions.installISortExtension) { - sendTelemetryEvent(EventName.TOOLS_EXTENSIONS_INSTALL_SELECTED, undefined, { - extensionId: ISORT_EXTENSION, - }); - const appEnv: IApplicationEnvironment = this.serviceContainer.get( - IApplicationEnvironment, - ); - await executeCommand('workbench.extensions.installExtension', ISORT_EXTENSION, { - installPreReleaseVersion: appEnv.extensionChannel === 'insiders', - }); - return true; - } - - sendTelemetryEvent(EventName.TOOLS_EXTENSIONS_PROMPT_DISMISSED, undefined, { - extensionId: ISORT_EXTENSION, - dismissType: 'close', - }); - - return false; - } -} - -let _prompt: ISortExtensionPrompt | undefined; -export function getOrCreateISortPrompt(serviceContainer: IServiceContainer): ISortExtensionPrompt { - if (!_prompt) { - _prompt = new ISortExtensionPrompt(serviceContainer); - } - return _prompt; -} diff --git a/src/client/providers/codeActionProvider/main.ts b/src/client/providers/codeActionProvider/main.ts index 40afd4dbb2b2..259f42848606 100644 --- a/src/client/providers/codeActionProvider/main.ts +++ b/src/client/providers/codeActionProvider/main.ts @@ -4,23 +4,14 @@ import { inject, injectable } from 'inversify'; import * as vscodeTypes from 'vscode'; import { IExtensionSingleActivationService } from '../../activation/types'; -import { Commands } from '../../common/constants'; import { IDisposableRegistry } from '../../common/types'; -import { executeCommand, registerCommand } from '../../common/vscodeApis/commandApis'; -import { isExtensionEnabled } from '../../common/vscodeApis/extensionsApi'; -import { IServiceContainer } from '../../ioc/types'; -import { traceLog } from '../../logging'; -import { getOrCreateISortPrompt, ISORT_EXTENSION } from './isortPrompt'; import { LaunchJsonCodeActionProvider } from './launchJsonCodeActionProvider'; @injectable() export class CodeActionProviderService implements IExtensionSingleActivationService { public readonly supportedWorkspaceTypes = { untrustedWorkspace: false, virtualWorkspace: false }; - constructor( - @inject(IDisposableRegistry) private disposableRegistry: IDisposableRegistry, - @inject(IServiceContainer) private serviceContainer: IServiceContainer, - ) {} + constructor(@inject(IDisposableRegistry) private disposableRegistry: IDisposableRegistry) {} public async activate(): Promise { // eslint-disable-next-line global-require @@ -35,19 +26,5 @@ export class CodeActionProviderService implements IExtensionSingleActivationServ providedCodeActionKinds: [vscode.CodeActionKind.QuickFix], }), ); - this.disposableRegistry.push( - registerCommand(Commands.Sort_Imports, async () => { - const prompt = getOrCreateISortPrompt(this.serviceContainer); - await prompt.showPrompt(); - if (!isExtensionEnabled(ISORT_EXTENSION)) { - traceLog( - 'Sort Imports: Please install and enable `ms-python.isort` extension to use this feature.', - ); - return; - } - - executeCommand('editor.action.organizeImports'); - }), - ); } } diff --git a/src/client/providers/formatProvider.ts b/src/client/providers/formatProvider.ts deleted file mode 100644 index 1ea239c03bec..000000000000 --- a/src/client/providers/formatProvider.ts +++ /dev/null @@ -1,135 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -import * as vscode from 'vscode'; -import { ICommandManager, IDocumentManager, IWorkspaceService } from '../common/application/types'; -import { PYTHON_LANGUAGE } from '../common/constants'; -import { IConfigurationService } from '../common/types'; -import { IInterpreterService } from '../interpreter/contracts'; -import { IServiceContainer } from '../ioc/types'; -import { AutoPep8Formatter } from '../formatters/autoPep8Formatter'; -import { BaseFormatter } from '../formatters/baseFormatter'; -import { BlackFormatter } from '../formatters/blackFormatter'; -import { DummyFormatter } from '../formatters/dummyFormatter'; -import { YapfFormatter } from '../formatters/yapfFormatter'; - -export class PythonFormattingEditProvider - implements vscode.DocumentFormattingEditProvider, vscode.DocumentRangeFormattingEditProvider, vscode.Disposable { - private readonly config: IConfigurationService; - - private readonly workspace: IWorkspaceService; - - private readonly documentManager: IDocumentManager; - - private readonly commands: ICommandManager; - - private formatters = new Map(); - - private disposables: vscode.Disposable[] = []; - - // Workaround for https://github.com/Microsoft/vscode/issues/41194 - private documentVersionBeforeFormatting = -1; - - private formatterMadeChanges = false; - - private saving = false; - - public constructor(_context: vscode.ExtensionContext, serviceContainer: IServiceContainer) { - const yapfFormatter = new YapfFormatter(serviceContainer); - const autoPep8 = new AutoPep8Formatter(serviceContainer); - const black = new BlackFormatter(serviceContainer); - const dummy = new DummyFormatter(serviceContainer); - this.formatters.set(yapfFormatter.Id, yapfFormatter); - this.formatters.set(black.Id, black); - this.formatters.set(autoPep8.Id, autoPep8); - this.formatters.set(dummy.Id, dummy); - - this.commands = serviceContainer.get(ICommandManager); - this.workspace = serviceContainer.get(IWorkspaceService); - this.documentManager = serviceContainer.get(IDocumentManager); - this.config = serviceContainer.get(IConfigurationService); - const interpreterService = serviceContainer.get(IInterpreterService); - this.disposables.push( - this.documentManager.onDidSaveTextDocument(async (document) => this.onSaveDocument(document)), - ); - this.disposables.push( - interpreterService.onDidChangeInterpreter(async () => { - if (this.documentManager.activeTextEditor) { - return this.onSaveDocument(this.documentManager.activeTextEditor.document); - } - - return undefined; - }), - ); - } - - public dispose(): void { - this.disposables.forEach((d) => d.dispose()); - } - - public provideDocumentFormattingEdits( - document: vscode.TextDocument, - options: vscode.FormattingOptions, - token: vscode.CancellationToken, - ): Promise { - return this.provideDocumentRangeFormattingEdits(document, undefined, options, token); - } - - public async provideDocumentRangeFormattingEdits( - document: vscode.TextDocument, - range: vscode.Range | undefined, - options: vscode.FormattingOptions, - token: vscode.CancellationToken, - ): Promise { - // Workaround for https://github.com/Microsoft/vscode/issues/41194 - // VSC rejects 'format on save' promise in 750 ms. Python formatting may take quite a bit longer. - // Workaround is to resolve promise to nothing here, then execute format document and force new save. - // However, we need to know if this is 'format document' or formatting on save. - - if (this.saving || document.languageId !== PYTHON_LANGUAGE) { - // We are saving after formatting (see onSaveDocument below) - // so we do not want to format again. - return []; - } - - // Remember content before formatting so we can detect if - // formatting edits have been really applied - const editorConfig = this.workspace.getConfiguration('editor', document.uri); - if (editorConfig.get('formatOnSave') === true) { - this.documentVersionBeforeFormatting = document.version; - } - - const settings = this.config.getSettings(document.uri); - const formatter = this.formatters.get(settings.formatting.provider)!; - const edits = await formatter.formatDocument(document, options, token, range); - - this.formatterMadeChanges = edits.length > 0; - return edits; - } - - private async onSaveDocument(document: vscode.TextDocument): Promise { - // Promise was rejected = formatting took too long. - // Don't format inside the event handler, do it on timeout - setTimeout(() => { - try { - if ( - this.formatterMadeChanges && - !document.isDirty && - document.version === this.documentVersionBeforeFormatting - ) { - // Formatter changes were not actually applied due to the timeout on save. - // Force formatting now and then save the document. - this.commands.executeCommand('editor.action.formatDocument').then(async () => { - this.saving = true; - await document.save(); - this.saving = false; - }); - } - } finally { - this.documentVersionBeforeFormatting = -1; - this.saving = false; - this.formatterMadeChanges = false; - } - }, 50); - } -} diff --git a/src/client/providers/linterProvider.ts b/src/client/providers/linterProvider.ts deleted file mode 100644 index 7821eaeccd53..000000000000 --- a/src/client/providers/linterProvider.ts +++ /dev/null @@ -1,123 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; - -import { inject, injectable } from 'inversify'; -import * as path from 'path'; -import { ConfigurationChangeEvent, Disposable, TextDocument, Uri, workspace } from 'vscode'; -import { IExtensionActivationService } from '../activation/types'; -import { IDocumentManager, IWorkspaceService } from '../common/application/types'; -import { isTestExecution } from '../common/constants'; -import '../common/extensions'; -import { IFileSystem } from '../common/platform/types'; -import { IConfigurationService, IDisposable } from '../common/types'; -import { IInterpreterService } from '../interpreter/contracts'; -import { IServiceContainer } from '../ioc/types'; -import { ILinterManager, ILintingEngine } from '../linters/types'; - -@injectable() -export class LinterProvider implements IExtensionActivationService, Disposable { - public readonly supportedWorkspaceTypes = { untrustedWorkspace: false, virtualWorkspace: false }; - - private interpreterService: IInterpreterService; - - private documents: IDocumentManager; - - private configuration: IConfigurationService; - - private linterManager: ILinterManager; - - private engine: ILintingEngine; - - private fs: IFileSystem; - - private readonly disposables: IDisposable[] = []; - - private workspaceService: IWorkspaceService; - - private activatedOnce = false; - - constructor(@inject(IServiceContainer) private serviceContainer: IServiceContainer) { - this.serviceContainer = serviceContainer; - this.fs = this.serviceContainer.get(IFileSystem); - this.engine = this.serviceContainer.get(ILintingEngine); - this.linterManager = this.serviceContainer.get(ILinterManager); - this.interpreterService = this.serviceContainer.get(IInterpreterService); - this.documents = this.serviceContainer.get(IDocumentManager); - this.configuration = this.serviceContainer.get(IConfigurationService); - this.workspaceService = this.serviceContainer.get(IWorkspaceService); - } - - public async activate(): Promise { - if (this.activatedOnce) { - return; - } - this.activatedOnce = true; - this.disposables.push(this.interpreterService.onDidChangeInterpreter(() => this.engine.lintOpenPythonFiles())); - - this.documents.onDidOpenTextDocument((e) => this.onDocumentOpened(e), this.disposables); - this.documents.onDidCloseTextDocument((e) => this.onDocumentClosed(e), this.disposables); - this.documents.onDidSaveTextDocument((e) => this.onDocumentSaved(e), this.disposables); - - const disposable = this.workspaceService.onDidChangeConfiguration(this.lintSettingsChangedHandler.bind(this)); - this.disposables.push(disposable); - - // On workspace reopen we don't get `onDocumentOpened` since it is first opened - // and then the extension is activated. So schedule linting pass now. - if (!isTestExecution()) { - const timer = setTimeout(() => this.engine.lintOpenPythonFiles().ignoreErrors(), 1200); - this.disposables.push({ dispose: () => clearTimeout(timer) }); - } - } - - public dispose(): void { - this.disposables.forEach((d) => d.dispose()); - } - - private isDocumentOpen(uri: Uri): boolean { - return this.documents.textDocuments.some((document) => this.fs.arePathsSame(document.uri.fsPath, uri.fsPath)); - } - - private lintSettingsChangedHandler(e: ConfigurationChangeEvent) { - // Look for python files that belong to the specified workspace folder. - workspace.textDocuments.forEach((document) => { - if (e.affectsConfiguration('python.linting', document.uri)) { - this.engine.lintDocument(document, 'auto').ignoreErrors(); - } - }); - } - - private onDocumentOpened(document: TextDocument): void { - this.engine.lintDocument(document, 'auto').ignoreErrors(); - } - - private onDocumentSaved(document: TextDocument): void { - const settings = this.configuration.getSettings(document.uri); - if (document.languageId === 'python' && settings.linting.enabled && settings.linting.lintOnSave) { - this.engine.lintDocument(document, 'save').ignoreErrors(); - return; - } - - this.linterManager - .getActiveLinters(document.uri) - .then((linters) => { - const fileName = path.basename(document.uri.fsPath).toLowerCase(); - const watchers = linters.filter((info) => info.configFileNames.indexOf(fileName) >= 0); - if (watchers.length > 0) { - setTimeout(() => this.engine.lintOpenPythonFiles(), 1000); - } - }) - .ignoreErrors(); - } - - private onDocumentClosed(document: TextDocument) { - if (!document || !document.fileName || !document.uri) { - return; - } - // Check if this document is still open as a duplicate editor. - if (!this.isDocumentOpen(document.uri)) { - this.engine.clearDiagnostics(document); - } - } -} diff --git a/src/client/providers/prompts/installFormatterPrompt.ts b/src/client/providers/prompts/installFormatterPrompt.ts deleted file mode 100644 index 5743f8402053..000000000000 --- a/src/client/providers/prompts/installFormatterPrompt.ts +++ /dev/null @@ -1,145 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -import { Uri } from 'vscode'; -import { inject, injectable } from 'inversify'; -import { IDisposableRegistry } from '../../common/types'; -import { Common, ToolsExtensions } from '../../common/utils/localize'; -import { isExtensionEnabled } from '../../common/vscodeApis/extensionsApi'; -import { showInformationMessage } from '../../common/vscodeApis/windowApis'; -import { getConfiguration, onDidSaveTextDocument } from '../../common/vscodeApis/workspaceApis'; -import { IServiceContainer } from '../../ioc/types'; -import { - doNotShowPromptState, - inFormatterExtensionExperiment, - installFormatterExtension, - updateDefaultFormatter, -} from './promptUtils'; -import { AUTOPEP8_EXTENSION, BLACK_EXTENSION, IInstallFormatterPrompt } from './types'; - -const SHOW_FORMATTER_INSTALL_PROMPT_DONOTSHOW_KEY = 'showFormatterExtensionInstallPrompt'; - -@injectable() -export class InstallFormatterPrompt implements IInstallFormatterPrompt { - private currentlyShown = false; - - constructor(@inject(IServiceContainer) private readonly serviceContainer: IServiceContainer) {} - - /* - * This method is called when the user saves a python file or a cell. - * Returns true if an extension was selected. Otherwise returns false. - */ - public async showInstallFormatterPrompt(resource?: Uri): Promise { - if (!inFormatterExtensionExperiment(this.serviceContainer)) { - return false; - } - - const promptState = doNotShowPromptState(SHOW_FORMATTER_INSTALL_PROMPT_DONOTSHOW_KEY, this.serviceContainer); - if (this.currentlyShown || promptState.value) { - return false; - } - - const config = getConfiguration('python', resource); - const formatter = config.get('formatting.provider', 'none'); - if (!['autopep8', 'black'].includes(formatter)) { - return false; - } - - const editorConfig = getConfiguration('editor', { uri: resource, languageId: 'python' }); - const defaultFormatter = editorConfig.get('defaultFormatter', ''); - if ([BLACK_EXTENSION, AUTOPEP8_EXTENSION].includes(defaultFormatter)) { - return false; - } - - const black = isExtensionEnabled(BLACK_EXTENSION); - const autopep8 = isExtensionEnabled(AUTOPEP8_EXTENSION); - - let selection: string | undefined; - - if (black || autopep8) { - this.currentlyShown = true; - if (black && autopep8) { - selection = await showInformationMessage( - ToolsExtensions.selectMultipleFormattersPrompt, - 'Black', - 'Autopep8', - Common.doNotShowAgain, - ); - } else if (black) { - selection = await showInformationMessage( - ToolsExtensions.selectBlackFormatterPrompt, - Common.bannerLabelYes, - Common.doNotShowAgain, - ); - if (selection === Common.bannerLabelYes) { - selection = 'Black'; - } - } else if (autopep8) { - selection = await showInformationMessage( - ToolsExtensions.selectAutopep8FormatterPrompt, - Common.bannerLabelYes, - Common.doNotShowAgain, - ); - if (selection === Common.bannerLabelYes) { - selection = 'Autopep8'; - } - } - } else if (formatter === 'black' && !black) { - this.currentlyShown = true; - selection = await showInformationMessage( - ToolsExtensions.installBlackFormatterPrompt, - 'Black', - 'Autopep8', - Common.doNotShowAgain, - ); - } else if (formatter === 'autopep8' && !autopep8) { - this.currentlyShown = true; - selection = await showInformationMessage( - ToolsExtensions.installAutopep8FormatterPrompt, - 'Black', - 'Autopep8', - Common.doNotShowAgain, - ); - } - - let userSelectedAnExtension = false; - if (selection === 'Black') { - if (black) { - userSelectedAnExtension = true; - await updateDefaultFormatter(BLACK_EXTENSION, resource); - } else { - userSelectedAnExtension = true; - await installFormatterExtension(BLACK_EXTENSION, resource); - } - } else if (selection === 'Autopep8') { - if (autopep8) { - userSelectedAnExtension = true; - await updateDefaultFormatter(AUTOPEP8_EXTENSION, resource); - } else { - userSelectedAnExtension = true; - await installFormatterExtension(AUTOPEP8_EXTENSION, resource); - } - } else if (selection === Common.doNotShowAgain) { - userSelectedAnExtension = false; - await promptState.updateValue(true); - } else { - userSelectedAnExtension = false; - } - - this.currentlyShown = false; - return userSelectedAnExtension; - } -} - -export function registerInstallFormatterPrompt(serviceContainer: IServiceContainer): void { - const disposables = serviceContainer.get(IDisposableRegistry); - const installFormatterPrompt = serviceContainer.get(IInstallFormatterPrompt); - disposables.push( - onDidSaveTextDocument(async (e) => { - const editorConfig = getConfiguration('editor', { uri: e.uri, languageId: 'python' }); - if (e.languageId === 'python' && editorConfig.get('formatOnSave')) { - await installFormatterPrompt.showInstallFormatterPrompt(e.uri); - } - }), - ); -} diff --git a/src/client/providers/prompts/promptUtils.ts b/src/client/providers/prompts/promptUtils.ts deleted file mode 100644 index 05b1b28f061a..000000000000 --- a/src/client/providers/prompts/promptUtils.ts +++ /dev/null @@ -1,38 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -import { ConfigurationTarget, Uri } from 'vscode'; -import { ShowFormatterExtensionPrompt } from '../../common/experiments/groups'; -import { IExperimentService, IPersistentState, IPersistentStateFactory } from '../../common/types'; -import { executeCommand } from '../../common/vscodeApis/commandApis'; -import { isInsider } from '../../common/vscodeApis/extensionsApi'; -import { getConfiguration, getWorkspaceFolder } from '../../common/vscodeApis/workspaceApis'; -import { IServiceContainer } from '../../ioc/types'; - -export function inFormatterExtensionExperiment(serviceContainer: IServiceContainer): boolean { - const experiment = serviceContainer.get(IExperimentService); - return experiment.inExperimentSync(ShowFormatterExtensionPrompt.experiment); -} - -export function doNotShowPromptState(key: string, serviceContainer: IServiceContainer): IPersistentState { - const persistFactory = serviceContainer.get(IPersistentStateFactory); - const promptState = persistFactory.createWorkspacePersistentState(key, false); - return promptState; -} - -export async function updateDefaultFormatter(extensionId: string, resource?: Uri): Promise { - const scope = getWorkspaceFolder(resource) ? ConfigurationTarget.Workspace : ConfigurationTarget.Global; - - const config = getConfiguration('python', resource); - const editorConfig = getConfiguration('editor', { uri: resource, languageId: 'python' }); - await editorConfig.update('defaultFormatter', extensionId, scope, true); - await config.update('formatting.provider', 'none', scope); -} - -export async function installFormatterExtension(extensionId: string, resource?: Uri): Promise { - await executeCommand('workbench.extensions.installExtension', extensionId, { - installPreReleaseVersion: isInsider(), - }); - - await updateDefaultFormatter(extensionId, resource); -} diff --git a/src/client/providers/prompts/types.ts b/src/client/providers/prompts/types.ts deleted file mode 100644 index 4edaadb46b46..000000000000 --- a/src/client/providers/prompts/types.ts +++ /dev/null @@ -1,12 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -import { Uri } from 'vscode'; - -export const BLACK_EXTENSION = 'ms-python.black-formatter'; -export const AUTOPEP8_EXTENSION = 'ms-python.autopep8'; - -export const IInstallFormatterPrompt = Symbol('IInstallFormatterPrompt'); -export interface IInstallFormatterPrompt { - showInstallFormatterPrompt(resource?: Uri): Promise; -} diff --git a/src/client/providers/replProvider.ts b/src/client/providers/replProvider.ts index db0e459c12dd..dd9df89a78a3 100644 --- a/src/client/providers/replProvider.ts +++ b/src/client/providers/replProvider.ts @@ -4,8 +4,6 @@ import { Commands } from '../common/constants'; import { noop } from '../common/utils/misc'; import { IInterpreterService } from '../interpreter/contracts'; import { IServiceContainer } from '../ioc/types'; -import { captureTelemetry } from '../telemetry'; -import { EventName } from '../telemetry/constants'; import { ICodeExecutionService } from '../terminals/types'; export class ReplProvider implements Disposable { @@ -28,7 +26,6 @@ export class ReplProvider implements Disposable { this.disposables.push(disposable); } - @captureTelemetry(EventName.REPL) private async commandHandler() { const resource = this.activeResourceService.getActiveResource(); const interpreterService = this.serviceContainer.get(IInterpreterService); @@ -40,7 +37,7 @@ export class ReplProvider implements Disposable { .then(noop, noop); return; } - const replProvider = this.serviceContainer.get(ICodeExecutionService, 'repl'); + const replProvider = this.serviceContainer.get(ICodeExecutionService, 'standard'); await replProvider.initializeRepl(resource); } } diff --git a/src/client/providers/serviceRegistry.ts b/src/client/providers/serviceRegistry.ts index 70fc6dc34135..a96ec14ff5e9 100644 --- a/src/client/providers/serviceRegistry.ts +++ b/src/client/providers/serviceRegistry.ts @@ -6,13 +6,10 @@ import { IExtensionSingleActivationService } from '../activation/types'; import { IServiceManager } from '../ioc/types'; import { CodeActionProviderService } from './codeActionProvider/main'; -import { InstallFormatterPrompt } from './prompts/installFormatterPrompt'; -import { IInstallFormatterPrompt } from './prompts/types'; export function registerTypes(serviceManager: IServiceManager): void { serviceManager.addSingleton( IExtensionSingleActivationService, CodeActionProviderService, ); - serviceManager.addSingleton(IInstallFormatterPrompt, InstallFormatterPrompt); } diff --git a/src/client/providers/terminalProvider.ts b/src/client/providers/terminalProvider.ts index d047ea4b6d82..f68f151110ec 100644 --- a/src/client/providers/terminalProvider.ts +++ b/src/client/providers/terminalProvider.ts @@ -11,6 +11,7 @@ import { swallowExceptions } from '../common/utils/decorators'; import { IServiceContainer } from '../ioc/types'; import { captureTelemetry, sendTelemetryEvent } from '../telemetry'; import { EventName } from '../telemetry/constants'; +import { useEnvExtension, shouldEnvExtHandleActivation } from '../envExt/api.internal'; export class TerminalProvider implements Disposable { private disposables: Disposable[] = []; @@ -31,7 +32,8 @@ export class TerminalProvider implements Disposable { if ( currentTerminal && pythonSettings.terminal.activateEnvInCurrentTerminal && - !inTerminalEnvVarExperiment(experimentService) + !inTerminalEnvVarExperiment(experimentService) && + !shouldEnvExtHandleActivation() ) { const hideFromUser = 'hideFromUser' in currentTerminal.creationOptions && currentTerminal.creationOptions.hideFromUser; @@ -58,8 +60,13 @@ export class TerminalProvider implements Disposable { @captureTelemetry(EventName.TERMINAL_CREATE, { triggeredBy: 'commandpalette' }) private async onCreateTerminal() { - const terminalService = this.serviceContainer.get(ITerminalServiceFactory); const activeResource = this.activeResourceService.getActiveResource(); + if (useEnvExtension()) { + const commandManager = this.serviceContainer.get(ICommandManager); + await commandManager.executeCommand('python-envs.createTerminal', activeResource); + } + + const terminalService = this.serviceContainer.get(ITerminalServiceFactory); await terminalService.createTerminalService(activeResource, 'Python').show(false); } } diff --git a/src/client/pythonEnvironments/base/info/env.ts b/src/client/pythonEnvironments/base/info/env.ts index 2527f18202cd..5c5b9317e169 100644 --- a/src/client/pythonEnvironments/base/info/env.ts +++ b/src/client/pythonEnvironments/base/info/env.ts @@ -42,6 +42,12 @@ export function buildEnvInfo(init?: { sysPrefix?: string; searchLocation?: Uri; type?: PythonEnvType; + /** + * Command used to run Python in this environment. + * E.g. `conda run -n envName python` or `python.exe` + */ + pythonRunCommand?: string[]; + identifiedUsingNativeLocator?: boolean; }): PythonEnvInfo { const env: PythonEnvInfo = { name: init?.name ?? '', @@ -69,6 +75,8 @@ export function buildEnvInfo(init?: { org: init?.org ?? '', }, source: init?.source ?? [], + pythonRunCommand: init?.pythonRunCommand, + identifiedUsingNativeLocator: init?.identifiedUsingNativeLocator, }; if (init !== undefined) { updateEnv(env, init); @@ -87,7 +95,13 @@ export function areEnvsDeepEqual(env1: PythonEnvInfo, env2: PythonEnvInfo): bool env2Clone.source = env2Clone.source.sort(); const searchLocation1 = env1.searchLocation?.fsPath ?? ''; const searchLocation2 = env2.searchLocation?.fsPath ?? ''; - return isEqual(env1Clone, env2Clone) && arePathsSame(searchLocation1, searchLocation2); + const searchLocation1Scheme = env1.searchLocation?.scheme ?? ''; + const searchLocation2Scheme = env2.searchLocation?.scheme ?? ''; + return ( + isEqual(env1Clone, env2Clone) && + arePathsSame(searchLocation1, searchLocation2) && + searchLocation1Scheme === searchLocation2Scheme + ); } /** @@ -154,7 +168,7 @@ export function setEnvDisplayString(env: PythonEnvInfo): void { function buildEnvDisplayString(env: PythonEnvInfo, getAllDetails = false): string { // main parts - const shouldDisplayKind = getAllDetails || env.searchLocation || globallyInstalledEnvKinds.includes(env.kind); + const shouldDisplayKind = getAllDetails || globallyInstalledEnvKinds.includes(env.kind); const shouldDisplayArch = !virtualEnvKinds.includes(env.kind); const displayNameParts: string[] = ['Python']; if (env.version && !isVersionEmpty(env.version)) { @@ -173,6 +187,11 @@ function buildEnvDisplayString(env: PythonEnvInfo, getAllDetails = false): strin const envSuffixParts: string[] = []; if (env.name && env.name !== '') { envSuffixParts.push(`'${env.name}'`); + } else if (env.location && env.location !== '') { + if (env.kind === PythonEnvKind.Conda) { + const condaEnvName = path.basename(env.location); + envSuffixParts.push(`'${condaEnvName}'`); + } } if (shouldDisplayKind) { const kindName = getKindDisplayName(env.kind); @@ -265,14 +284,20 @@ export function areSameEnv( if (leftInfo === undefined || rightInfo === undefined) { return undefined; } - const leftFilename = leftInfo.executable!.filename; - const rightFilename = rightInfo.executable!.filename; - + if ( + (leftInfo.executable?.filename && !rightInfo.executable?.filename) || + (!leftInfo.executable?.filename && rightInfo.executable?.filename) + ) { + return false; + } if (leftInfo.id && leftInfo.id === rightInfo.id) { // In case IDs are available, use it. return true; } + const leftFilename = leftInfo.executable!.filename; + const rightFilename = rightInfo.executable!.filename; + if (getEnvID(leftFilename, leftInfo.location) === getEnvID(rightFilename, rightInfo.location)) { // Otherwise use ID function to get the ID. Note ID returned by function may itself change if executable of // an environment changes, for eg. when conda installs python into the env. So only use it as a fallback if diff --git a/src/client/pythonEnvironments/base/info/envKind.ts b/src/client/pythonEnvironments/base/info/envKind.ts index 8828003c5ce7..08f4ce55d464 100644 --- a/src/client/pythonEnvironments/base/info/envKind.ts +++ b/src/client/pythonEnvironments/base/info/envKind.ts @@ -12,15 +12,17 @@ export function getKindDisplayName(kind: PythonEnvKind): string { for (const [candidate, value] of [ // Note that Unknown is excluded here. [PythonEnvKind.System, 'system'], - [PythonEnvKind.MicrosoftStore, 'microsoft store'], + [PythonEnvKind.MicrosoftStore, 'Microsoft Store'], [PythonEnvKind.Pyenv, 'pyenv'], - [PythonEnvKind.Poetry, 'poetry'], + [PythonEnvKind.Poetry, 'Poetry'], + [PythonEnvKind.Hatch, 'Hatch'], + [PythonEnvKind.Pixi, 'Pixi'], [PythonEnvKind.Custom, 'custom'], // For now we treat OtherGlobal like Unknown. [PythonEnvKind.Venv, 'venv'], [PythonEnvKind.VirtualEnv, 'virtualenv'], [PythonEnvKind.VirtualEnvWrapper, 'virtualenv'], - [PythonEnvKind.Pipenv, 'pipenv'], + [PythonEnvKind.Pipenv, 'Pipenv'], [PythonEnvKind.Conda, 'conda'], [PythonEnvKind.ActiveState, 'ActiveState'], // For now we treat OtherVirtual like Unknown. @@ -39,12 +41,14 @@ export function getKindDisplayName(kind: PythonEnvKind): string { * Remarks: This is the order of detection based on how the various distributions and tools * configure the environment, and the fall back for identification. * Top level we have the following environment types, since they leave a unique signature - * in the environment or * use a unique path for the environments they create. + * in the environment or use a unique path for the environments they create. * 1. Conda * 2. Microsoft Store * 3. PipEnv * 4. Pyenv * 5. Poetry + * 6. Hatch + * 7. Pixi * * Next level we have the following virtual environment tools. The are here because they * are consumed by the tools above, and can also be used independently. @@ -57,10 +61,12 @@ export function getKindDisplayName(kind: PythonEnvKind): string { export function getPrioritizedEnvKinds(): PythonEnvKind[] { return [ PythonEnvKind.Pyenv, + PythonEnvKind.Pixi, // Placed here since Pixi environments are essentially Conda envs PythonEnvKind.Conda, PythonEnvKind.MicrosoftStore, PythonEnvKind.Pipenv, PythonEnvKind.Poetry, + PythonEnvKind.Hatch, PythonEnvKind.Venv, PythonEnvKind.VirtualEnvWrapper, PythonEnvKind.VirtualEnv, diff --git a/src/client/pythonEnvironments/base/info/index.ts b/src/client/pythonEnvironments/base/info/index.ts index e55031fe8078..4547e7606308 100644 --- a/src/client/pythonEnvironments/base/info/index.ts +++ b/src/client/pythonEnvironments/base/info/index.ts @@ -15,6 +15,8 @@ export enum PythonEnvKind { MicrosoftStore = 'global-microsoft-store', Pyenv = 'global-pyenv', Poetry = 'poetry', + Hatch = 'hatch', + Pixi = 'pixi', ActiveState = 'activestate', Custom = 'global-custom', OtherGlobal = 'global-other', @@ -44,6 +46,8 @@ export interface EnvPathType { export const virtualEnvKinds = [ PythonEnvKind.Poetry, + PythonEnvKind.Hatch, + PythonEnvKind.Pixi, PythonEnvKind.Pipenv, PythonEnvKind.Venv, PythonEnvKind.VirtualEnvWrapper, @@ -194,13 +198,19 @@ type _PythonEnvInfo = PythonEnvBaseInfo & PythonBuildInfo; * @prop distro - the installed Python distro that this env is using or belongs to * @prop display - the text to use when showing the env to users * @prop detailedDisplayName - display name containing all details - * @prop searchLocation - the root under which a locator found this env, if any + * @prop searchLocation - the project to which this env is related to, if any */ export type PythonEnvInfo = _PythonEnvInfo & { distro: PythonDistroInfo; display?: string; detailedDisplayName?: string; searchLocation?: Uri; + /** + * Command used to run Python in this environment. + * E.g. `conda run -n envName python` or `python.exe` + */ + pythonRunCommand?: string[]; + identifiedUsingNativeLocator?: boolean; }; /** diff --git a/src/client/pythonEnvironments/base/info/interpreter.ts b/src/client/pythonEnvironments/base/info/interpreter.ts index d0cb1f13f8f8..e19e1f0d45c2 100644 --- a/src/client/pythonEnvironments/base/info/interpreter.ts +++ b/src/client/pythonEnvironments/base/info/interpreter.ts @@ -8,7 +8,7 @@ import { InterpreterInfoJson, } from '../../../common/process/internal/scripts'; import { Architecture } from '../../../common/utils/platform'; -import { traceError, traceVerbose } from '../../../logging'; +import { traceError, traceInfo, traceVerbose } from '../../../logging'; import { shellExecute } from '../../common/externalDependencies'; import { copyPythonExecInfo, PythonExecInfo } from '../../exec'; import { parseVersion } from './pythonVersion'; @@ -82,7 +82,13 @@ export async function getInterpreterInfo( ); // Sometimes on CI, the python process takes a long time to start up. This is a workaround for that. - const standardTimeout = isCI ? 30000 : 15000; + let standardTimeout = isCI ? 30000 : 15000; + if (process.env.VSC_PYTHON_INTERPRETER_INFO_TIMEOUT !== undefined) { + // Custom override for setups where the initial Python setup process may take longer than the standard timeout. + standardTimeout = parseInt(process.env.VSC_PYTHON_INTERPRETER_INFO_TIMEOUT, 10); + traceInfo(`Custom interpreter discovery timeout: ${standardTimeout}`); + } + // Try shell execing the command, followed by the arguments. This will make node kill the process if it // takes too long. // Sometimes the python path isn't valid, timeout if that's the case. diff --git a/src/client/pythonEnvironments/base/locator.ts b/src/client/pythonEnvironments/base/locator.ts index ab3b17629bc5..0c15f8b27e5f 100644 --- a/src/client/pythonEnvironments/base/locator.ts +++ b/src/client/pythonEnvironments/base/locator.ts @@ -5,13 +5,14 @@ import { Event, Uri } from 'vscode'; import { IAsyncIterableIterator, iterEmpty } from '../../common/utils/async'; -import { PythonEnvInfo, PythonEnvKind, PythonEnvSource } from './info'; +import { PythonEnvInfo, PythonEnvKind, PythonEnvSource, PythonVersion } from './info'; import { IPythonEnvsWatcher, PythonEnvCollectionChangedEvent, PythonEnvsChangedEvent, PythonEnvsWatcher, } from './watcher'; +import type { Architecture } from '../../common/utils/platform'; /** * A single update to a previously provided Python env object. @@ -67,6 +68,7 @@ export interface IPythonEnvsIterator extends IAsyncIterableIt } export enum ProgressReportStage { + idle = 'idle', discoveryStarted = 'discoveryStarted', allPathsDiscovered = 'allPathsDiscovered', discoveryFinished = 'discoveryFinished', @@ -132,7 +134,7 @@ export type PythonLocatorQuery = BasicPythonLocatorQuery & { */ providerId?: string; /** - * If provided, results area limited to this env. + * If provided, results are limited to this env. */ envPath?: string; }; @@ -144,6 +146,22 @@ export type BasicEnvInfo = { executablePath: string; source?: PythonEnvSource[]; envPath?: string; + /** + * The project to which this env is related to, if any + * E.g. the project directory when dealing with pipenv virtual environments. + */ + searchLocation?: Uri; + version?: PythonVersion; + name?: string; + /** + * Display name provided by locators, not generated by us. + * E.g. display name as provided by Windows Registry or Windows Store, etc + */ + displayName?: string; + identifiedUsingNativeLocator?: boolean; + arch?: Architecture; + ctime?: number; + mtime?: number; }; /** @@ -243,7 +261,7 @@ export interface IDiscoveryAPI { resolveEnv(path: string): Promise; } -interface IEmitter { +export interface IEmitter { fire(e: E): void; } diff --git a/src/client/pythonEnvironments/base/locatorUtils.ts b/src/client/pythonEnvironments/base/locatorUtils.ts index 97cb6298416f..6af8c0ee1b69 100644 --- a/src/client/pythonEnvironments/base/locatorUtils.ts +++ b/src/client/pythonEnvironments/base/locatorUtils.ts @@ -85,7 +85,7 @@ export async function getEnvs(iterator: IPythonEnvsIterator; + /** + * Will spawn the provided Python executable and return information about the environment. + * @param executable + */ + resolve(executable: string): Promise; + /** + * Used only for telemetry. + */ + getCondaInfo(): Promise; +} + +interface NativeLog { + level: string; + message: string; +} + +class NativePythonFinderImpl extends DisposableBase implements NativePythonFinder { + private readonly connection: rpc.MessageConnection; + + private firstRefreshResults: undefined | (() => AsyncGenerator); + + private readonly outputChannel = this._register(createLogOutputChannel('Python Locator', { log: true })); + + private initialRefreshMetrics = { + timeToSpawn: 0, + timeToConfigure: 0, + timeToRefresh: 0, + }; + + private readonly suppressErrorNotification: IPersistentStorage; + + constructor(private readonly cacheDirectory?: Uri, private readonly context?: IExtensionContext) { + super(); + this.suppressErrorNotification = this.context + ? getGlobalStorage(this.context, DONT_SHOW_SPAWN_ERROR_AGAIN, false) + : ({ get: () => false, set: async () => {} } as IPersistentStorage); + this.connection = this.start(); + void this.configure(); + this.firstRefreshResults = this.refreshFirstTime(); + } + + public async resolve(executable: string): Promise { + await this.configure(); + const environment = await this.connection.sendRequest('resolve', { + executable, + }); + + this.outputChannel.info(`Resolved Python Environment ${environment.executable}`); + return environment; + } + + async *refresh(options?: NativePythonEnvironmentKind | Uri[]): AsyncIterable { + if (this.firstRefreshResults) { + // If this is the first time we are refreshing, + // Then get the results from the first refresh. + // Those would have started earlier and cached in memory. + const results = this.firstRefreshResults(); + this.firstRefreshResults = undefined; + yield* results; + } else { + const result = this.doRefresh(options); + let completed = false; + void result.completed.finally(() => { + completed = true; + }); + const envs: (NativeEnvInfo | NativeEnvManagerInfo)[] = []; + let discovered = createDeferred(); + const disposable = result.discovered((data) => { + envs.push(data); + discovered.resolve(); + }); + do { + if (!envs.length) { + await Promise.race([result.completed, discovered.promise]); + } + if (envs.length) { + const dataToSend = [...envs]; + envs.length = 0; + for (const data of dataToSend) { + yield data; + } + } + if (!completed) { + discovered = createDeferred(); + } + } while (!completed); + disposable.dispose(); + } + } + + refreshFirstTime() { + const result = this.doRefresh(); + const completed = createDeferredFrom(result.completed); + const envs: NativeEnvInfo[] = []; + let discovered = createDeferred(); + const disposable = result.discovered((data) => { + envs.push(data); + discovered.resolve(); + }); + + const iterable = async function* () { + do { + if (!envs.length) { + await Promise.race([completed.promise, discovered.promise]); + } + if (envs.length) { + const dataToSend = [...envs]; + envs.length = 0; + for (const data of dataToSend) { + yield data; + } + } + if (!completed.completed) { + discovered = createDeferred(); + } + } while (!completed.completed); + disposable.dispose(); + }; + + return iterable.bind(this); + } + + // eslint-disable-next-line class-methods-use-this + private start(): rpc.MessageConnection { + this.outputChannel.info(`Starting Python Locator ${PYTHON_ENV_TOOLS_PATH} server`); + + // jsonrpc package cannot handle messages coming through too quickly. + // Lets handle the messages and close the stream only when + // we have got the exit event. + const readable = new PassThrough(); + const writable = new PassThrough(); + const disposables: Disposable[] = []; + try { + const stopWatch = new StopWatch(); + const proc = ch.spawn(PYTHON_ENV_TOOLS_PATH, ['server'], { env: process.env }); + this.initialRefreshMetrics.timeToSpawn = stopWatch.elapsedTime; + proc.stdout.pipe(readable, { end: false }); + proc.stderr.on('data', (data) => this.outputChannel.error(data.toString())); + writable.pipe(proc.stdin, { end: false }); + + // Handle spawn errors (e.g., missing DLLs on Windows) + proc.on('error', (error) => { + this.outputChannel.error(`Python Locator process error: ${error.message}`); + this.outputChannel.error(`Error details: ${JSON.stringify(error)}`); + this.handleSpawnError(error.message); + }); + + // Handle immediate exits with error codes + let hasStarted = false; + setTimeout(() => { + hasStarted = true; + }, 1000); + + proc.on('exit', (code, signal) => { + if (!hasStarted && code !== null && code !== 0) { + const errorMessage = `Python Locator process exited immediately with code ${code}`; + this.outputChannel.error(errorMessage); + if (signal) { + this.outputChannel.error(`Exit signal: ${signal}`); + } + this.handleSpawnError(errorMessage); + } + }); + + disposables.push({ + dispose: () => { + try { + if (proc.exitCode === null) { + proc.kill(); + } + } catch (ex) { + this.outputChannel.error('Error disposing finder', ex); + } + }, + }); + } catch (ex) { + this.outputChannel.error(`Error starting Python Finder ${PYTHON_ENV_TOOLS_PATH} server`, ex); + } + const disposeStreams = new Disposable(() => { + readable.end(); + writable.end(); + }); + const connection = rpc.createMessageConnection( + new rpc.StreamMessageReader(readable), + new rpc.StreamMessageWriter(writable), + ); + disposables.push( + connection, + disposeStreams, + connection.onError((ex) => { + disposeStreams.dispose(); + this.outputChannel.error('Connection Error:', ex); + }), + connection.onNotification('log', (data: NativeLog) => { + switch (data.level) { + case 'info': + this.outputChannel.info(data.message); + break; + case 'warning': + this.outputChannel.warn(data.message); + break; + case 'error': + this.outputChannel.error(data.message); + break; + case 'debug': + this.outputChannel.debug(data.message); + break; + default: + this.outputChannel.trace(data.message); + } + }), + connection.onNotification('telemetry', (data: NativePythonTelemetry) => + sendNativeTelemetry(data, this.initialRefreshMetrics), + ), + connection.onClose(() => { + disposables.forEach((d) => d.dispose()); + }), + ); + + connection.listen(); + this._register(Disposable.from(...disposables)); + return connection; + } + + private doRefresh( + options?: NativePythonEnvironmentKind | Uri[], + ): { completed: Promise; discovered: Event } { + const disposable = this._register(new DisposableStore()); + const discovered = disposable.add(new EventEmitter()); + const completed = createDeferred(); + const pendingPromises: Promise[] = []; + const stopWatch = new StopWatch(); + + const notifyUponCompletion = () => { + const initialCount = pendingPromises.length; + Promise.all(pendingPromises) + .then(() => { + if (initialCount === pendingPromises.length) { + completed.resolve(); + } else { + setTimeout(notifyUponCompletion, 0); + } + }) + .catch(noop); + }; + const trackPromiseAndNotifyOnCompletion = (promise: Promise) => { + pendingPromises.push(promise); + notifyUponCompletion(); + }; + + // Assumption is server will ensure there's only one refresh at a time. + // Perhaps we should have a request Id or the like to map the results back to the `refresh` request. + disposable.add( + this.connection.onNotification('environment', (data: NativeEnvInfo) => { + this.outputChannel.info(`Discovered env: ${data.executable || data.prefix}`); + // We know that in the Python extension if either Version of Prefix is not provided by locator + // Then we end up resolving the information. + // Lets do that here, + // This is a hack, as the other part of the code that resolves the version information + // doesn't work as expected, as its still a WIP. + if (data.executable && (!data.version || !data.prefix)) { + // HACK = TEMPORARY WORK AROUND, TO GET STUFF WORKING + // HACK = TEMPORARY WORK AROUND, TO GET STUFF WORKING + // HACK = TEMPORARY WORK AROUND, TO GET STUFF WORKING + // HACK = TEMPORARY WORK AROUND, TO GET STUFF WORKING + const promise = this.connection + .sendRequest('resolve', { + executable: data.executable, + }) + .then((environment) => { + this.outputChannel.info(`Resolved ${environment.executable}`); + discovered.fire(environment); + }) + .catch((ex) => this.outputChannel.error(`Error in Resolving ${JSON.stringify(data)}`, ex)); + trackPromiseAndNotifyOnCompletion(promise); + } else { + discovered.fire(data); + } + }), + ); + disposable.add( + this.connection.onNotification('manager', (data: NativeEnvManagerInfo) => { + this.outputChannel.info(`Discovered manager: (${data.tool}) ${data.executable}`); + discovered.fire(data); + }), + ); + + type RefreshOptions = { + searchKind?: NativePythonEnvironmentKind; + searchPaths?: string[]; + }; + + const refreshOptions: RefreshOptions = {}; + if (options && Array.isArray(options) && options.length > 0) { + refreshOptions.searchPaths = options.map((item) => item.fsPath); + } else if (options && typeof options === 'string') { + refreshOptions.searchKind = options; + } + trackPromiseAndNotifyOnCompletion( + this.configure().then(() => + this.connection + .sendRequest<{ duration: number }>('refresh', refreshOptions) + .then(({ duration }) => { + this.outputChannel.info(`Refresh completed in ${duration}ms`); + this.initialRefreshMetrics.timeToRefresh = stopWatch.elapsedTime; + }) + .catch((ex) => this.outputChannel.error('Refresh error', ex)), + ), + ); + + completed.promise.finally(() => disposable.dispose()); + return { + completed: completed.promise, + discovered: discovered.event, + }; + } + + private lastConfiguration?: ConfigurationOptions; + + /** + * Configuration request, this must always be invoked before any other request. + * Must be invoked when ever there are changes to any data related to the configuration details. + */ + private async configure() { + const options: ConfigurationOptions = { + workspaceDirectories: getWorkspaceFolderPaths(), + // We do not want to mix this with `search_paths` + environmentDirectories: getCustomVirtualEnvDirs(), + condaExecutable: getPythonSettingAndUntildify(CONDAPATH_SETTING_KEY), + poetryExecutable: getPythonSettingAndUntildify('poetryPath'), + cacheDirectory: this.cacheDirectory?.fsPath, + }; + // No need to send a configuration request, is there are no changes. + if (JSON.stringify(options) === JSON.stringify(this.lastConfiguration || {})) { + return; + } + try { + const stopWatch = new StopWatch(); + this.lastConfiguration = options; + await this.connection.sendRequest('configure', options); + this.initialRefreshMetrics.timeToConfigure = stopWatch.elapsedTime; + } catch (ex) { + this.outputChannel.error('Refresh error', ex); + } + } + + async getCondaInfo(): Promise { + return this.connection.sendRequest('condaInfo'); + } + + private async handleSpawnError(errorMessage: string): Promise { + // Check if user has chosen to not see this error again + if (this.suppressErrorNotification.get()) { + return; + } + + // Check for Windows runtime DLL issues + if (isWindows() && errorMessage.toLowerCase().includes('vcruntime')) { + this.outputChannel.error(PythonLocator.windowsRuntimeMissing); + } else if (isWindows()) { + this.outputChannel.error(PythonLocator.windowsStartupFailed); + } + + // Show notification to user + const selection = await showWarningMessage( + PythonLocator.startupFailedNotification, + Common.openOutputPanel, + Common.doNotShowAgain, + ); + + if (selection === Common.openOutputPanel) { + await executeCommand(Commands.ViewOutput); + } else if (selection === Common.doNotShowAgain) { + await this.suppressErrorNotification.set(true); + } + } +} + +type ConfigurationOptions = { + workspaceDirectories: string[]; + /** + * Place where virtual envs and the like are stored + * Should not contain workspace folders. + */ + environmentDirectories: string[]; + condaExecutable: string | undefined; + poetryExecutable: string | undefined; + cacheDirectory?: string; +}; +/** + * Gets all custom virtual environment locations to look for environments. + */ +function getCustomVirtualEnvDirs(): string[] { + const venvDirs: string[] = []; + const venvPath = getPythonSettingAndUntildify(VENVPATH_SETTING_KEY); + if (venvPath) { + venvDirs.push(untildify(venvPath)); + } + const venvFolders = getPythonSettingAndUntildify(VENVFOLDERS_SETTING_KEY) ?? []; + const homeDir = getUserHomeDir(); + if (homeDir) { + venvFolders + .map((item) => (item.startsWith(homeDir) ? item : path.join(homeDir, item))) + .forEach((d) => venvDirs.push(d)); + venvFolders.forEach((item) => venvDirs.push(untildify(item))); + } + return Array.from(new Set(venvDirs)); +} + +function getPythonSettingAndUntildify(name: string, scope?: Uri): T | undefined { + const value = getConfiguration('python', scope).get(name); + if (typeof value === 'string') { + return value ? ((untildify(value as string) as unknown) as T) : undefined; + } + return value; +} + +let _finder: NativePythonFinder | undefined; +export function getNativePythonFinder(context?: IExtensionContext): NativePythonFinder { + if (!isTrusted()) { + return { + async *refresh() { + traceError('Python discovery not supported in untrusted workspace'); + yield* []; + }, + async resolve() { + traceError('Python discovery not supported in untrusted workspace'); + return {}; + }, + async getCondaInfo() { + traceError('Python discovery not supported in untrusted workspace'); + return ({} as unknown) as NativeCondaInfo; + }, + dispose() { + // do nothing + }, + }; + } + if (!_finder) { + const cacheDirectory = context ? getCacheDirectory(context) : undefined; + _finder = new NativePythonFinderImpl(cacheDirectory, context); + if (context) { + context.subscriptions.push(_finder); + } + } + return _finder; +} + +export function getCacheDirectory(context: IExtensionContext): Uri { + return Uri.joinPath(context.globalStorageUri, 'pythonLocator'); +} + +export async function clearCacheDirectory(context: IExtensionContext): Promise { + const cacheDirectory = getCacheDirectory(context); + await fs.emptyDir(cacheDirectory.fsPath).catch(noop); +} diff --git a/src/client/pythonEnvironments/base/locators/common/nativePythonTelemetry.ts b/src/client/pythonEnvironments/base/locators/common/nativePythonTelemetry.ts new file mode 100644 index 000000000000..703fdfca01c3 --- /dev/null +++ b/src/client/pythonEnvironments/base/locators/common/nativePythonTelemetry.ts @@ -0,0 +1,147 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { traceError } from '../../../../logging'; +import { sendTelemetryEvent } from '../../../../telemetry'; +import { EventName } from '../../../../telemetry/constants'; + +export type NativePythonTelemetry = MissingCondaEnvironments | MissingPoetryEnvironments | RefreshPerformance; + +export type MissingCondaEnvironments = { + event: 'MissingCondaEnvironments'; + data: { + missingCondaEnvironments: { + missing: number; + envDirsNotFound?: number; + userProvidedCondaExe?: boolean; + rootPrefixNotFound?: boolean; + condaPrefixNotFound?: boolean; + condaManagerNotFound?: boolean; + sysRcNotFound?: boolean; + userRcNotFound?: boolean; + otherRcNotFound?: boolean; + missingEnvDirsFromSysRc?: number; + missingEnvDirsFromUserRc?: number; + missingEnvDirsFromOtherRc?: number; + missingFromSysRcEnvDirs?: number; + missingFromUserRcEnvDirs?: number; + missingFromOtherRcEnvDirs?: number; + }; + }; +}; + +export type MissingPoetryEnvironments = { + event: 'MissingPoetryEnvironments'; + data: { + missingPoetryEnvironments: { + missing: number; + missingInPath: number; + userProvidedPoetryExe?: boolean; + poetryExeNotFound?: boolean; + globalConfigNotFound?: boolean; + cacheDirNotFound?: boolean; + cacheDirIsDifferent?: boolean; + virtualenvsPathNotFound?: boolean; + virtualenvsPathIsDifferent?: boolean; + inProjectIsDifferent?: boolean; + }; + }; +}; + +export type RefreshPerformance = { + event: 'RefreshPerformance'; + data: { + refreshPerformance: { + total: number; + breakdown: { + Locators: number; + Path: number; + GlobalVirtualEnvs: number; + Workspaces: number; + }; + locators: { + Conda?: number; + Homebrew?: number; + LinuxGlobalPython?: number; + MacCmdLineTools?: number; + MacPythonOrg?: number; + MacXCode?: number; + PipEnv?: number; + PixiEnv?: number; + Poetry?: number; + PyEnv?: number; + Venv?: number; + VirtualEnv?: number; + VirtualEnvWrapper?: number; + WindowsRegistry?: number; + WindowsStore?: number; + }; + }; + }; +}; + +let refreshTelemetrySent = false; + +export function sendNativeTelemetry( + data: NativePythonTelemetry, + initialRefreshMetrics: { + timeToSpawn: number; + timeToConfigure: number; + timeToRefresh: number; + }, +): void { + switch (data.event) { + case 'MissingCondaEnvironments': { + sendTelemetryEvent( + EventName.NATIVE_FINDER_MISSING_CONDA_ENVS, + undefined, + data.data.missingCondaEnvironments, + ); + break; + } + case 'MissingPoetryEnvironments': { + sendTelemetryEvent( + EventName.NATIVE_FINDER_MISSING_POETRY_ENVS, + undefined, + data.data.missingPoetryEnvironments, + ); + break; + } + case 'RefreshPerformance': { + if (refreshTelemetrySent) { + break; + } + refreshTelemetrySent = true; + sendTelemetryEvent(EventName.NATIVE_FINDER_PERF, { + duration: data.data.refreshPerformance.total, + totalDuration: data.data.refreshPerformance.total, + breakdownGlobalVirtualEnvs: data.data.refreshPerformance.breakdown.GlobalVirtualEnvs, + breakdownLocators: data.data.refreshPerformance.breakdown.Locators, + breakdownPath: data.data.refreshPerformance.breakdown.Path, + breakdownWorkspaces: data.data.refreshPerformance.breakdown.Workspaces, + locatorConda: data.data.refreshPerformance.locators.Conda || 0, + locatorHomebrew: data.data.refreshPerformance.locators.Homebrew || 0, + locatorLinuxGlobalPython: data.data.refreshPerformance.locators.LinuxGlobalPython || 0, + locatorMacCmdLineTools: data.data.refreshPerformance.locators.MacCmdLineTools || 0, + locatorMacPythonOrg: data.data.refreshPerformance.locators.MacPythonOrg || 0, + locatorMacXCode: data.data.refreshPerformance.locators.MacXCode || 0, + locatorPipEnv: data.data.refreshPerformance.locators.PipEnv || 0, + locatorPixiEnv: data.data.refreshPerformance.locators.PixiEnv || 0, + locatorPoetry: data.data.refreshPerformance.locators.Poetry || 0, + locatorPyEnv: data.data.refreshPerformance.locators.PyEnv || 0, + locatorVenv: data.data.refreshPerformance.locators.Venv || 0, + locatorVirtualEnv: data.data.refreshPerformance.locators.VirtualEnv || 0, + locatorVirtualEnvWrapper: data.data.refreshPerformance.locators.VirtualEnvWrapper || 0, + locatorWindowsRegistry: data.data.refreshPerformance.locators.WindowsRegistry || 0, + locatorWindowsStore: data.data.refreshPerformance.locators.WindowsStore || 0, + timeToSpawn: initialRefreshMetrics.timeToSpawn, + timeToConfigure: initialRefreshMetrics.timeToConfigure, + timeToRefresh: initialRefreshMetrics.timeToRefresh, + }); + break; + } + default: { + traceError(`Unhandled Telemetry Event type ${JSON.stringify(data)}`); + } + } +} diff --git a/src/client/pythonEnvironments/base/locators/common/nativePythonUtils.ts b/src/client/pythonEnvironments/base/locators/common/nativePythonUtils.ts new file mode 100644 index 000000000000..716bdd444633 --- /dev/null +++ b/src/client/pythonEnvironments/base/locators/common/nativePythonUtils.ts @@ -0,0 +1,65 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { LogOutputChannel } from 'vscode'; +import { PythonEnvKind } from '../../info'; +import { traceError } from '../../../../logging'; + +export enum NativePythonEnvironmentKind { + Conda = 'Conda', + Pixi = 'Pixi', + Homebrew = 'Homebrew', + Pyenv = 'Pyenv', + GlobalPaths = 'GlobalPaths', + PyenvVirtualEnv = 'PyenvVirtualEnv', + Pipenv = 'Pipenv', + Poetry = 'Poetry', + MacPythonOrg = 'MacPythonOrg', + MacCommandLineTools = 'MacCommandLineTools', + LinuxGlobal = 'LinuxGlobal', + MacXCode = 'MacXCode', + Venv = 'Venv', + VirtualEnv = 'VirtualEnv', + VirtualEnvWrapper = 'VirtualEnvWrapper', + WindowsStore = 'WindowsStore', + WindowsRegistry = 'WindowsRegistry', + VenvUv = 'Uv', +} + +const mapping = new Map([ + [NativePythonEnvironmentKind.Conda, PythonEnvKind.Conda], + [NativePythonEnvironmentKind.Pixi, PythonEnvKind.Pixi], + [NativePythonEnvironmentKind.GlobalPaths, PythonEnvKind.OtherGlobal], + [NativePythonEnvironmentKind.Pyenv, PythonEnvKind.Pyenv], + [NativePythonEnvironmentKind.PyenvVirtualEnv, PythonEnvKind.Pyenv], + [NativePythonEnvironmentKind.Pipenv, PythonEnvKind.Pipenv], + [NativePythonEnvironmentKind.Poetry, PythonEnvKind.Poetry], + [NativePythonEnvironmentKind.VirtualEnv, PythonEnvKind.VirtualEnv], + [NativePythonEnvironmentKind.VirtualEnvWrapper, PythonEnvKind.VirtualEnvWrapper], + [NativePythonEnvironmentKind.Venv, PythonEnvKind.Venv], + [NativePythonEnvironmentKind.VenvUv, PythonEnvKind.Venv], + [NativePythonEnvironmentKind.WindowsRegistry, PythonEnvKind.System], + [NativePythonEnvironmentKind.WindowsStore, PythonEnvKind.MicrosoftStore], + [NativePythonEnvironmentKind.Homebrew, PythonEnvKind.System], + [NativePythonEnvironmentKind.LinuxGlobal, PythonEnvKind.System], + [NativePythonEnvironmentKind.MacCommandLineTools, PythonEnvKind.System], + [NativePythonEnvironmentKind.MacPythonOrg, PythonEnvKind.System], + [NativePythonEnvironmentKind.MacXCode, PythonEnvKind.System], +]); + +export function categoryToKind(category?: NativePythonEnvironmentKind, logger?: LogOutputChannel): PythonEnvKind { + if (!category) { + return PythonEnvKind.Unknown; + } + const kind = mapping.get(category); + if (kind) { + return kind; + } + + if (logger) { + logger.error(`Unknown Python Environment category '${category}' from Native Locator.`); + } else { + traceError(`Unknown Python Environment category '${category}' from Native Locator.`); + } + return PythonEnvKind.Unknown; +} diff --git a/src/client/pythonEnvironments/base/locators/common/pythonWatcher.ts b/src/client/pythonEnvironments/base/locators/common/pythonWatcher.ts new file mode 100644 index 000000000000..378a0d6c521e --- /dev/null +++ b/src/client/pythonEnvironments/base/locators/common/pythonWatcher.ts @@ -0,0 +1,146 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { Disposable, Event, EventEmitter, GlobPattern, RelativePattern, Uri, WorkspaceFolder } from 'vscode'; +import { createFileSystemWatcher, getWorkspaceFolder } from '../../../../common/vscodeApis/workspaceApis'; +import { isWindows } from '../../../../common/utils/platform'; +import { arePathsSame } from '../../../common/externalDependencies'; +import { FileChangeType } from '../../../../common/platform/fileSystemWatcher'; + +export interface PythonWorkspaceEnvEvent { + type: FileChangeType; + workspaceFolder: WorkspaceFolder; + executable: string; +} + +export interface PythonGlobalEnvEvent { + type: FileChangeType; + uri: Uri; +} + +export interface PythonWatcher extends Disposable { + watchWorkspace(wf: WorkspaceFolder): void; + unwatchWorkspace(wf: WorkspaceFolder): void; + onDidWorkspaceEnvChanged: Event; + + watchPath(uri: Uri, pattern?: string): void; + unwatchPath(uri: Uri): void; + onDidGlobalEnvChanged: Event; +} + +/* + * The pattern to search for python executables in the workspace. + * project + * β”œβ”€β”€ python or python.exe <--- This is what we are looking for. + * β”œβ”€β”€ .conda + * β”‚ └── python or python.exe <--- This is what we are looking for. + * └── .venv + * β”‚ └── Scripts or bin + * β”‚ └── python or python.exe <--- This is what we are looking for. + */ +const WORKSPACE_PATTERN = isWindows() ? '**/python.exe' : '**/python'; + +class PythonWatcherImpl implements PythonWatcher { + private disposables: Disposable[] = []; + + private readonly _onDidWorkspaceEnvChanged = new EventEmitter(); + + private readonly _onDidGlobalEnvChanged = new EventEmitter(); + + private readonly _disposeMap: Map = new Map(); + + constructor() { + this.disposables.push(this._onDidWorkspaceEnvChanged, this._onDidGlobalEnvChanged); + } + + onDidGlobalEnvChanged: Event = this._onDidGlobalEnvChanged.event; + + onDidWorkspaceEnvChanged: Event = this._onDidWorkspaceEnvChanged.event; + + watchWorkspace(wf: WorkspaceFolder): void { + if (this._disposeMap.has(wf.uri.fsPath)) { + const disposer = this._disposeMap.get(wf.uri.fsPath); + disposer?.dispose(); + } + + const disposables: Disposable[] = []; + const watcher = createFileSystemWatcher(new RelativePattern(wf, WORKSPACE_PATTERN)); + disposables.push( + watcher, + watcher.onDidChange((uri) => { + this.fireWorkspaceEvent(FileChangeType.Changed, wf, uri); + }), + watcher.onDidCreate((uri) => { + this.fireWorkspaceEvent(FileChangeType.Created, wf, uri); + }), + watcher.onDidDelete((uri) => { + this.fireWorkspaceEvent(FileChangeType.Deleted, wf, uri); + }), + ); + + const disposable = { + dispose: () => { + disposables.forEach((d) => d.dispose()); + this._disposeMap.delete(wf.uri.fsPath); + }, + }; + this._disposeMap.set(wf.uri.fsPath, disposable); + } + + unwatchWorkspace(wf: WorkspaceFolder): void { + const disposable = this._disposeMap.get(wf.uri.fsPath); + disposable?.dispose(); + } + + private fireWorkspaceEvent(type: FileChangeType, wf: WorkspaceFolder, uri: Uri) { + const uriWorkspace = getWorkspaceFolder(uri); + if (uriWorkspace && arePathsSame(uriWorkspace.uri.fsPath, wf.uri.fsPath)) { + this._onDidWorkspaceEnvChanged.fire({ type, workspaceFolder: wf, executable: uri.fsPath }); + } + } + + watchPath(uri: Uri, pattern?: string): void { + if (this._disposeMap.has(uri.fsPath)) { + const disposer = this._disposeMap.get(uri.fsPath); + disposer?.dispose(); + } + + const glob: GlobPattern = pattern ? new RelativePattern(uri, pattern) : uri.fsPath; + const disposables: Disposable[] = []; + const watcher = createFileSystemWatcher(glob); + disposables.push( + watcher, + watcher.onDidChange(() => { + this._onDidGlobalEnvChanged.fire({ type: FileChangeType.Changed, uri }); + }), + watcher.onDidCreate(() => { + this._onDidGlobalEnvChanged.fire({ type: FileChangeType.Created, uri }); + }), + watcher.onDidDelete(() => { + this._onDidGlobalEnvChanged.fire({ type: FileChangeType.Deleted, uri }); + }), + ); + + const disposable = { + dispose: () => { + disposables.forEach((d) => d.dispose()); + this._disposeMap.delete(uri.fsPath); + }, + }; + this._disposeMap.set(uri.fsPath, disposable); + } + + unwatchPath(uri: Uri): void { + const disposable = this._disposeMap.get(uri.fsPath); + disposable?.dispose(); + } + + dispose() { + this.disposables.forEach((d) => d.dispose()); + this._disposeMap.forEach((d) => d.dispose()); + } +} + +export function createPythonWatcher(): PythonWatcher { + return new PythonWatcherImpl(); +} diff --git a/src/client/pythonEnvironments/base/locators/common/resourceBasedLocator.ts b/src/client/pythonEnvironments/base/locators/common/resourceBasedLocator.ts index 9391888cf0cf..8b56b4c7b8c1 100644 --- a/src/client/pythonEnvironments/base/locators/common/resourceBasedLocator.ts +++ b/src/client/pythonEnvironments/base/locators/common/resourceBasedLocator.ts @@ -4,7 +4,7 @@ import { IDisposable } from '../../../../common/types'; import { createDeferred, Deferred } from '../../../../common/utils/async'; import { Disposables } from '../../../../common/utils/resourceLifecycle'; -import { traceError } from '../../../../logging'; +import { traceError, traceWarn } from '../../../../logging'; import { arePathsSame, isVirtualWorkspace } from '../../../common/externalDependencies'; import { getEnvPath } from '../../info/env'; import { BasicEnvInfo, IPythonEnvsIterator, Locator, PythonLocatorQuery } from '../../locator'; @@ -36,7 +36,11 @@ export abstract class LazyResourceBasedLocator extends Locator imp protected async activate(): Promise { await this.ensureResourcesReady(); // There is not need to wait for the watchers to get started. - this.ensureWatchersReady().ignoreErrors(); + try { + this.ensureWatchersReady(); + } catch (ex) { + traceWarn(`Failed to ensure watchers are ready for locator ${this.constructor.name}`, ex); + } } public async dispose(): Promise { diff --git a/src/client/pythonEnvironments/base/locators/composite/envsCollectionCache.ts b/src/client/pythonEnvironments/base/locators/composite/envsCollectionCache.ts index dadab2512e16..456e8adfa9a4 100644 --- a/src/client/pythonEnvironments/base/locators/composite/envsCollectionCache.ts +++ b/src/client/pythonEnvironments/base/locators/composite/envsCollectionCache.ts @@ -245,6 +245,10 @@ export class PythonEnvInfoCache extends PythonEnvsWatcher { const query: PythonLocatorQuery | undefined = event.providerId @@ -88,14 +92,13 @@ export class EnvsCollectionService extends PythonEnvsWatcher { traceError(`Failed to resolve ${path}`, ex); return undefined; }); - traceVerbose(`Resolved ${path} to ${JSON.stringify(resolved)}`); + traceVerbose(`Resolved ${path} using downstream locator`); if (resolved) { this.cache.addEnv(resolved, true); } @@ -108,16 +111,20 @@ export class EnvsCollectionService extends PythonEnvsWatcher { - const stopWatch = new StopWatch(); let refreshPromise = this.getRefreshPromiseForQuery(query); if (!refreshPromise) { if (options?.ifNotTriggerredAlready && this.hasRefreshFinished(query)) { // Do not trigger another refresh if a refresh has previously finished. return Promise.resolve(); } - refreshPromise = this.startRefresh(query); + const stopWatch = new StopWatch(); + traceInfo(`Starting Environment refresh`); + refreshPromise = this.startRefresh(query).then(() => { + this.sendTelemetry(query, stopWatch); + traceInfo(`Environment refresh took ${stopWatch.elapsedTime} milliseconds`); + }); } - return refreshPromise.then(() => this.sendTelemetry(query, stopWatch)); + return refreshPromise; } private startRefresh(query: PythonLocatorQuery | undefined): Promise { @@ -140,7 +147,7 @@ export class EnvsCollectionService extends PythonEnvsWatcher(); - + const stopWatch = new StopWatch(); if (iterator.onUpdated !== undefined) { const listener = iterator.onUpdated(async (event) => { if (isProgressEvent(event)) { @@ -148,9 +155,13 @@ export class EnvsCollectionService extends PythonEnvsWatcher getEnvPath(e.executable.filename, e.location).pathType === 'envFolderPath', + ).length; + const activeStateEnvs = envs.filter((e) => e.kind === PythonEnvKind.ActiveState).length; + const condaEnvs = envs.filter((e) => e.kind === PythonEnvKind.Conda).length; + const customEnvs = envs.filter((e) => e.kind === PythonEnvKind.Custom).length; + const hatchEnvs = envs.filter((e) => e.kind === PythonEnvKind.Hatch).length; + const microsoftStoreEnvs = envs.filter((e) => e.kind === PythonEnvKind.MicrosoftStore).length; + const otherGlobalEnvs = envs.filter((e) => e.kind === PythonEnvKind.OtherGlobal).length; + const otherVirtualEnvs = envs.filter((e) => e.kind === PythonEnvKind.OtherVirtual).length; + const pipEnvEnvs = envs.filter((e) => e.kind === PythonEnvKind.Pipenv).length; + const poetryEnvs = envs.filter((e) => e.kind === PythonEnvKind.Poetry).length; + const pyenvEnvs = envs.filter((e) => e.kind === PythonEnvKind.Pyenv).length; + const systemEnvs = envs.filter((e) => e.kind === PythonEnvKind.System).length; + const unknownEnvs = envs.filter((e) => e.kind === PythonEnvKind.Unknown).length; + const venvEnvs = envs.filter((e) => e.kind === PythonEnvKind.Venv).length; + const virtualEnvEnvs = envs.filter((e) => e.kind === PythonEnvKind.VirtualEnv).length; + const virtualEnvWrapperEnvs = envs.filter((e) => e.kind === PythonEnvKind.VirtualEnvWrapper).length; + // Intent is to capture time taken for discovery of all envs to complete the first time. sendTelemetryEvent(EventName.PYTHON_INTERPRETER_DISCOVERY, stopWatch.elapsedTime, { interpreters: this.cache.getAllEnvs().length, - environmentsWithoutPython: this.cache - .getAllEnvs() - .filter((e) => getEnvPath(e.executable.filename, e.location).pathType === 'envFolderPath').length, + usingNativeLocator: this.usingNativeLocator, + environmentsWithoutPython, + activeStateEnvs, + condaEnvs, + customEnvs, + hatchEnvs, + microsoftStoreEnvs, + otherGlobalEnvs, + otherVirtualEnvs, + pipEnvEnvs, + poetryEnvs, + pyenvEnvs, + systemEnvs, + unknownEnvs, + venvEnvs, + virtualEnvEnvs, + virtualEnvWrapperEnvs, }); } this.hasRefreshFinishedForQuery.set(query, true); diff --git a/src/client/pythonEnvironments/base/locators/composite/envsReducer.ts b/src/client/pythonEnvironments/base/locators/composite/envsReducer.ts index 49f5b619694e..c3a523b2d086 100644 --- a/src/client/pythonEnvironments/base/locators/composite/envsReducer.ts +++ b/src/client/pythonEnvironments/base/locators/composite/envsReducer.ts @@ -2,8 +2,9 @@ // Licensed under the MIT License. import { cloneDeep, isEqual, uniq } from 'lodash'; -import { Event, EventEmitter } from 'vscode'; +import { Event, EventEmitter, Uri } from 'vscode'; import { traceVerbose } from '../../../../logging'; +import { isParentPath } from '../../../common/externalDependencies'; import { PythonEnvKind } from '../../info'; import { areSameEnv } from '../../info/env'; import { getPrioritizedEnvKinds } from '../../info/envKind'; @@ -51,7 +52,6 @@ async function* iterEnvsIterator( if (iterator.onUpdated !== undefined) { const listener = iterator.onUpdated((event) => { - state.pending += 1; if (isProgressEvent(event)) { if (event.stage === ProgressReportStage.discoveryFinished) { state.done = true; @@ -63,7 +63,7 @@ async function* iterEnvsIterator( throw new Error( 'Unsupported behavior: `undefined` environment updates are not supported from downstream locators in reducer', ); - } else if (seen[event.index] !== undefined) { + } else if (event.index !== undefined && seen[event.index] !== undefined) { const oldEnv = seen[event.index]; seen[event.index] = event.update; didUpdate.fire({ index: event.index, old: oldEnv, update: event.update }); @@ -137,9 +137,24 @@ function resolveEnvCollision(oldEnv: BasicEnvInfo, newEnv: BasicEnvInfo): BasicE const [env] = sortEnvInfoByPriority(oldEnv, newEnv); const merged = cloneDeep(env); merged.source = uniq((oldEnv.source ?? []).concat(newEnv.source ?? [])); + merged.searchLocation = getMergedSearchLocation(oldEnv, newEnv); return merged; } +function getMergedSearchLocation(oldEnv: BasicEnvInfo, newEnv: BasicEnvInfo): Uri | undefined { + if (oldEnv.searchLocation && newEnv.searchLocation) { + // Choose the deeper project path of the two, as that can be used to signify + // that the environment is related to both the projects. + if (isParentPath(oldEnv.searchLocation.fsPath, newEnv.searchLocation.fsPath)) { + return oldEnv.searchLocation; + } + if (isParentPath(newEnv.searchLocation.fsPath, oldEnv.searchLocation.fsPath)) { + return newEnv.searchLocation; + } + } + return oldEnv.searchLocation ?? newEnv.searchLocation; +} + /** * Selects an environment based on the environment selection priority. This should * match the priority in the environment identifier. diff --git a/src/client/pythonEnvironments/base/locators/composite/envsResolver.ts b/src/client/pythonEnvironments/base/locators/composite/envsResolver.ts index 2ba54e07ed9c..6bd342d14d9c 100644 --- a/src/client/pythonEnvironments/base/locators/composite/envsResolver.ts +++ b/src/client/pythonEnvironments/base/locators/composite/envsResolver.ts @@ -3,7 +3,7 @@ import { cloneDeep } from 'lodash'; import { Event, EventEmitter } from 'vscode'; -import { identifyEnvironment } from '../../../common/environmentIdentifier'; +import { isIdentifierRegistered, identifyEnvironment } from '../../../common/environmentIdentifier'; import { IEnvironmentInfoService } from '../../info/environmentInfoService'; import { PythonEnvInfo, PythonEnvKind } from '../../info'; import { getEnvPath, setEnvDisplayString } from '../../info/env'; @@ -95,7 +95,7 @@ export class PythonEnvsResolver implements IResolvingLocator { throw new Error( 'Unsupported behavior: `undefined` environment updates are not supported from downstream locators in resolver', ); - } else if (seen[event.index] !== undefined) { + } else if (event.index !== undefined && seen[event.index] !== undefined) { const old = seen[event.index]; await setKind(event.update, environmentKinds); seen[event.index] = await resolveBasicEnv(event.update); @@ -140,7 +140,7 @@ export class PythonEnvsResolver implements IResolvingLocator { const info = await this.environmentInfoService.getEnvironmentInfo(seen[envIndex]); const old = seen[envIndex]; if (info) { - const resolvedEnv = getResolvedEnv(info, seen[envIndex]); + const resolvedEnv = getResolvedEnv(info, seen[envIndex], old.identifiedUsingNativeLocator); seen[envIndex] = resolvedEnv; didUpdate.fire({ old, index: envIndex, update: resolvedEnv }); } else { @@ -154,8 +154,18 @@ export class PythonEnvsResolver implements IResolvingLocator { async function setKind(env: BasicEnvInfo, environmentKinds: Map) { const { path } = getEnvPath(env.executablePath, env.envPath); + // For native locators, do not try to identify the environment kind. + // its already set by the native locator & thats accurate. + if (env.identifiedUsingNativeLocator) { + environmentKinds.set(path, env.kind); + return; + } let kind = environmentKinds.get(path); if (!kind) { + if (!isIdentifierRegistered(env.kind)) { + // If identifier is not registered, skip setting env kind. + return; + } kind = await identifyEnvironment(path); environmentKinds.set(path, kind); } @@ -178,13 +188,22 @@ function checkIfFinishedAndNotify( } } -function getResolvedEnv(interpreterInfo: InterpreterInformation, environment: PythonEnvInfo) { +function getResolvedEnv( + interpreterInfo: InterpreterInformation, + environment: PythonEnvInfo, + identifiedUsingNativeLocator = false, +) { // Deep copy into a new object const resolvedEnv = cloneDeep(environment); resolvedEnv.executable.sysPrefix = interpreterInfo.executable.sysPrefix; const isEnvLackingPython = getEnvPath(resolvedEnv.executable.filename, resolvedEnv.location).pathType === 'envFolderPath'; - if (isEnvLackingPython) { + // TODO: Shouldn't this only apply to conda, how else can we have an environment and not have Python in it? + // If thats the case, then this should be gated on environment.kind === PythonEnvKind.Conda + // For non-native do not blow away the versions returned by native locator. + // Windows Store and Home brew have exe and sysprefix in different locations, + // Thus above check is not valid for these envs. + if (isEnvLackingPython && environment.kind !== PythonEnvKind.MicrosoftStore && !identifiedUsingNativeLocator) { // Install python later into these envs might change the version, which can be confusing for users. // So avoid displaying any version until it is installed. resolvedEnv.version = getEmptyVersion(); diff --git a/src/client/pythonEnvironments/base/locators/composite/resolverUtils.ts b/src/client/pythonEnvironments/base/locators/composite/resolverUtils.ts index 0cca49e2b4c5..088ae9cc97c1 100644 --- a/src/client/pythonEnvironments/base/locators/composite/resolverUtils.ts +++ b/src/client/pythonEnvironments/base/locators/composite/resolverUtils.ts @@ -19,6 +19,7 @@ import { AnacondaCompanyName, Conda, getCondaInterpreterPath, + getPythonVersionFromConda, isCondaEnvironment, } from '../../../common/environmentManagers/conda'; import { getPyenvVersionsDir, parsePyenvVersion } from '../../../common/environmentManagers/pyenv'; @@ -53,23 +54,37 @@ function getResolvers(): Map Promise { - const { kind, source } = env; + const { kind, source, searchLocation } = env; const resolvers = getResolvers(); const resolverForKind = resolvers.get(kind)!; const resolvedEnv = await resolverForKind(env); - resolvedEnv.searchLocation = getSearchLocation(resolvedEnv); + resolvedEnv.searchLocation = getSearchLocation(resolvedEnv, searchLocation); resolvedEnv.source = uniq(resolvedEnv.source.concat(source ?? [])); - if (getOSType() === OSType.Windows && resolvedEnv.source?.includes(PythonEnvSource.WindowsRegistry)) { + if ( + !env.identifiedUsingNativeLocator && + getOSType() === OSType.Windows && + resolvedEnv.source?.includes(PythonEnvSource.WindowsRegistry) + ) { // We can update env further using information we can get from the Windows registry. await updateEnvUsingRegistry(resolvedEnv); } setEnvDisplayString(resolvedEnv); - const { ctime, mtime } = await getFileInfo(resolvedEnv.executable.filename); - resolvedEnv.executable.ctime = ctime; - resolvedEnv.executable.mtime = mtime; - const type = await getEnvType(resolvedEnv); - if (type) { - resolvedEnv.type = type; + if (env.arch && !resolvedEnv.arch) { + resolvedEnv.arch = env.arch; + } + if (env.ctime && env.mtime) { + resolvedEnv.executable.ctime = env.ctime; + resolvedEnv.executable.mtime = env.mtime; + } else { + const { ctime, mtime } = await getFileInfo(resolvedEnv.executable.filename); + resolvedEnv.executable.ctime = ctime; + resolvedEnv.executable.mtime = mtime; + } + if (!env.identifiedUsingNativeLocator) { + const type = await getEnvType(resolvedEnv); + if (type) { + resolvedEnv.type = type; + } } return resolvedEnv; } @@ -87,7 +102,11 @@ async function getEnvType(env: PythonEnvInfo) { return undefined; } -function getSearchLocation(env: PythonEnvInfo): Uri | undefined { +function getSearchLocation(env: PythonEnvInfo, searchLocation: Uri | undefined): Uri | undefined { + if (searchLocation) { + // A search location has already been established by the downstream locators, simply use that. + return searchLocation; + } const folders = getWorkspaceFolderPaths(); const isRootedEnv = folders.some((f) => isParentPath(env.executable.filename, f) || isParentPath(env.location, f)); if (isRootedEnv) { @@ -135,14 +154,20 @@ async function resolveGloballyInstalledEnv(env: BasicEnvInfo): Promise { const { executablePath, kind } = env; const envInfo = buildEnvInfo({ kind, - version: await getPythonVersionFromPath(executablePath), + version: env.identifiedUsingNativeLocator ? env.version : await getPythonVersionFromPath(executablePath), executable: executablePath, + sysPrefix: env.envPath, + location: env.envPath, + display: env.displayName, + searchLocation: env.searchLocation, + identifiedUsingNativeLocator: env.identifiedUsingNativeLocator, + name: env.name, type: PythonEnvType.Virtual, }); - const location = getEnvironmentDirFromPath(executablePath); + const location = env.envPath ?? getEnvironmentDirFromPath(executablePath); envInfo.location = location; envInfo.name = path.basename(location); return envInfo; } async function resolveCondaEnv(env: BasicEnvInfo): Promise { + if (env.identifiedUsingNativeLocator) { + // New approach using native locator. + const executable = env.executablePath; + const envPath = env.envPath ?? getEnvironmentDirFromPath(executable); + // TODO: Hacky, `executable` is never undefined in the typedef, + // However, in reality with native locator this can be undefined. + const version = env.version ?? (executable ? await getPythonVersionFromPath(executable) : undefined); + const info = buildEnvInfo({ + executable, + kind: PythonEnvKind.Conda, + org: AnacondaCompanyName, + location: envPath, + sysPrefix: envPath, + display: env.displayName, + identifiedUsingNativeLocator: env.identifiedUsingNativeLocator, + searchLocation: env.searchLocation, + source: [], + version, + type: PythonEnvType.Conda, + name: env.name, + }); + + if (env.envPath && executable && path.basename(executable) === executable) { + // For environments without python, set ID using the predicted executable path after python is installed. + // Another alternative could've been to set ID of all conda environments to the environment path, as that + // remains constant even after python installation. + const predictedExecutable = getCondaInterpreterPath(env.envPath); + info.id = getEnvID(predictedExecutable, env.envPath); + } + return info; + } + + // Old approach (without native locator). + // In this approach we need to find conda. const { executablePath } = env; const conda = await Conda.getConda(); if (conda === undefined) { @@ -182,19 +247,18 @@ async function resolveCondaEnv(env: BasicEnvInfo): Promise { } else { executable = await conda.getInterpreterPathForEnvironment({ prefix: envPath }); } + const version = executable ? await getPythonVersionFromConda(executable) : undefined; const info = buildEnvInfo({ executable, kind: PythonEnvKind.Conda, org: AnacondaCompanyName, location: envPath, source: [], - version: executable ? await getPythonVersionFromPath(executable) : undefined, + version, type: PythonEnvType.Conda, + name: env.name ?? (await conda?.getName(envPath)), }); - const name = await conda?.getName(envPath); - if (name) { - info.name = name; - } + if (env.envPath && path.basename(executable) === executable) { // For environments without python, set ID using the predicted executable path after python is installed. // Another alternative could've been to set ID of all conda environments to the environment path, as that @@ -207,7 +271,7 @@ async function resolveCondaEnv(env: BasicEnvInfo): Promise { async function resolvePyenvEnv(env: BasicEnvInfo): Promise { const { executablePath } = env; - const location = getEnvironmentDirFromPath(executablePath); + const location = env.envPath ?? getEnvironmentDirFromPath(executablePath); const name = path.basename(location); // The sub-directory name sometimes can contain distro and python versions. @@ -215,10 +279,17 @@ async function resolvePyenvEnv(env: BasicEnvInfo): Promise { const versionStrings = parsePyenvVersion(name); const envInfo = buildEnvInfo({ - kind: PythonEnvKind.Pyenv, + // If using native resolver, then we can get the kind from the native resolver. + // E.g. pyenv can have conda environments as well. + kind: env.identifiedUsingNativeLocator && env.kind ? env.kind : PythonEnvKind.Pyenv, executable: executablePath, source: [], location, + searchLocation: env.searchLocation, + sysPrefix: env.envPath, + display: env.displayName, + name: env.name, + identifiedUsingNativeLocator: env.identifiedUsingNativeLocator, // Pyenv environments can fall in to these three categories: // 1. Global Installs : These are environments that are created when you install // a supported python distribution using `pyenv install ` command. @@ -237,14 +308,17 @@ async function resolvePyenvEnv(env: BasicEnvInfo): Promise { // // Here we look for near by files, or config files to see if we can get python version info // without running python itself. - version: await getPythonVersionFromPath(executablePath, versionStrings?.pythonVer), + version: env.version ?? (await getPythonVersionFromPath(executablePath, versionStrings?.pythonVer)), org: versionStrings && versionStrings.distro ? versionStrings.distro : '', }); - if (await isBaseCondaPyenvEnvironment(executablePath)) { - envInfo.name = 'base'; - } else { - envInfo.name = name; + // Do this only for the old approach, when not using native locators. + if (!env.identifiedUsingNativeLocator) { + if (await isBaseCondaPyenvEnvironment(executablePath)) { + envInfo.name = 'base'; + } else { + envInfo.name = name; + } } return envInfo; } @@ -253,6 +327,13 @@ async function resolveActiveStateEnv(env: BasicEnvInfo): Promise const info = buildEnvInfo({ kind: env.kind, executable: env.executablePath, + display: env.displayName, + version: env.version, + identifiedUsingNativeLocator: env.identifiedUsingNativeLocator, + location: env.envPath, + name: env.name, + searchLocation: env.searchLocation, + sysPrefix: env.envPath, }); const projects = await ActiveState.getState().then((v) => v?.getProjects()); if (projects) { @@ -282,8 +363,14 @@ async function resolveMicrosoftStoreEnv(env: BasicEnvInfo): Promise { + const stopWatch = new StopWatch(); const state = await ActiveState.getState(); if (state === undefined) { traceVerbose(`Couldn't locate the state binary.`); return; } + traceInfo(`Searching for active state environments`); const projects = await state.getProjects(); if (projects === undefined) { traceVerbose(`Couldn't fetch State Tool projects.`); @@ -40,6 +43,6 @@ export class ActiveStateLocator extends LazyResourceBasedLocator { } } } - traceVerbose(`Finished searching for active state environments`); + traceInfo(`Finished searching for active state environments: ${stopWatch.elapsedTime} milliseconds`); } } diff --git a/src/client/pythonEnvironments/base/locators/lowLevel/condaLocator.ts b/src/client/pythonEnvironments/base/locators/lowLevel/condaLocator.ts index 7cac0cb7df90..bb48ba75b9dd 100644 --- a/src/client/pythonEnvironments/base/locators/lowLevel/condaLocator.ts +++ b/src/client/pythonEnvironments/base/locators/lowLevel/condaLocator.ts @@ -4,8 +4,9 @@ import '../../../../common/extensions'; import { PythonEnvKind } from '../../info'; import { BasicEnvInfo, IPythonEnvsIterator } from '../../locator'; import { Conda, getCondaEnvironmentsTxt } from '../../../common/environmentManagers/conda'; -import { traceError, traceVerbose } from '../../../../logging'; +import { traceError, traceInfo, traceVerbose } from '../../../../logging'; import { FSWatchingLocator } from './fsWatchingLocator'; +import { StopWatch } from '../../../../common/utils/stopWatch'; export class CondaEnvironmentLocator extends FSWatchingLocator { public readonly providerId: string = 'conda-envs'; @@ -19,7 +20,9 @@ export class CondaEnvironmentLocator extends FSWatchingLocator { } // eslint-disable-next-line class-methods-use-this - public async *doIterEnvs(): IPythonEnvsIterator { + public async *doIterEnvs(_: unknown): IPythonEnvsIterator { + const stopWatch = new StopWatch(); + traceInfo('Searching for conda environments'); const conda = await Conda.getConda(); if (conda === undefined) { traceVerbose(`Couldn't locate the conda binary.`); @@ -38,6 +41,6 @@ export class CondaEnvironmentLocator extends FSWatchingLocator { traceError(`Failed to process conda env: ${JSON.stringify(env)}`, ex); } } - traceVerbose(`Finished searching for conda environments`); + traceInfo(`Finished searching for conda environments: ${stopWatch.elapsedTime} milliseconds`); } } diff --git a/src/client/pythonEnvironments/base/locators/lowLevel/customVirtualEnvLocator.ts b/src/client/pythonEnvironments/base/locators/lowLevel/customVirtualEnvLocator.ts index 57ae9187cdc2..6aa83bbc376b 100644 --- a/src/client/pythonEnvironments/base/locators/lowLevel/customVirtualEnvLocator.ts +++ b/src/client/pythonEnvironments/base/locators/lowLevel/customVirtualEnvLocator.ts @@ -9,12 +9,7 @@ import { PythonEnvKind } from '../../info'; import { BasicEnvInfo, IPythonEnvsIterator } from '../../locator'; import { FSWatchingLocator } from './fsWatchingLocator'; import { findInterpretersInDir, looksLikeBasicVirtualPython } from '../../../common/commonUtils'; -import { - getPythonSetting, - onDidChangePythonSetting, - pathExists, - untildify, -} from '../../../common/externalDependencies'; +import { getPythonSetting, onDidChangePythonSetting, pathExists } from '../../../common/externalDependencies'; import { isPipenvEnvironment } from '../../../common/environmentManagers/pipenv'; import { isVenvEnvironment, @@ -23,7 +18,9 @@ import { } from '../../../common/environmentManagers/simplevirtualenvs'; import '../../../../common/extensions'; import { asyncFilter } from '../../../../common/utils/arrayUtils'; -import { traceError, traceVerbose } from '../../../../logging'; +import { traceError, traceInfo, traceVerbose } from '../../../../logging'; +import { StopWatch } from '../../../../common/utils/stopWatch'; +import { untildify } from '../../../../common/helpers'; /** * Default number of levels of sub-directories to recurse when looking for interpreters. */ @@ -44,7 +41,10 @@ async function getCustomVirtualEnvDirs(): Promise { const venvFolders = getPythonSetting(VENVFOLDERS_SETTING_KEY) ?? []; const homeDir = getUserHomeDir(); if (homeDir && (await pathExists(homeDir))) { - venvFolders.map((item) => path.join(homeDir, item)).forEach((d) => venvDirs.push(d)); + venvFolders + .map((item) => (item.startsWith(homeDir) ? item : path.join(homeDir, item))) + .forEach((d) => venvDirs.push(d)); + venvFolders.forEach((item) => venvDirs.push(untildify(item))); } return asyncFilter(uniq(venvDirs), pathExists); } @@ -92,13 +92,15 @@ export class CustomVirtualEnvironmentLocator extends FSWatchingLocator { } protected async initResources(): Promise { - this.disposables.push(onDidChangePythonSetting(VENVPATH_SETTING_KEY, () => this.emitter.fire({}))); - this.disposables.push(onDidChangePythonSetting(VENVFOLDERS_SETTING_KEY, () => this.emitter.fire({}))); + this.disposables.push(onDidChangePythonSetting(VENVPATH_SETTING_KEY, () => this.fire())); + this.disposables.push(onDidChangePythonSetting(VENVFOLDERS_SETTING_KEY, () => this.fire())); } // eslint-disable-next-line class-methods-use-this protected doIterEnvs(): IPythonEnvsIterator { async function* iterator() { + const stopWatch = new StopWatch(); + traceInfo('Searching for custom virtual environments'); const envRootDirs = await getCustomVirtualEnvDirs(); const envGenerators = envRootDirs.map((envRootDir) => { async function* generator() { @@ -132,7 +134,7 @@ export class CustomVirtualEnvironmentLocator extends FSWatchingLocator { }); yield* iterable(chain(envGenerators)); - traceVerbose(`Finished searching for custom virtual envs`); + traceInfo(`Finished searching for custom virtual envs: ${stopWatch.elapsedTime} milliseconds`); } return iterator(); diff --git a/src/client/pythonEnvironments/base/locators/lowLevel/customWorkspaceLocator.ts b/src/client/pythonEnvironments/base/locators/lowLevel/customWorkspaceLocator.ts new file mode 100644 index 000000000000..8a2b857d496a --- /dev/null +++ b/src/client/pythonEnvironments/base/locators/lowLevel/customWorkspaceLocator.ts @@ -0,0 +1,47 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { PythonEnvKind } from '../../info'; +import { BasicEnvInfo, IPythonEnvsIterator } from '../../locator'; +import { FSWatchingLocator } from './fsWatchingLocator'; +import { getPythonSetting, onDidChangePythonSetting } from '../../../common/externalDependencies'; +import '../../../../common/extensions'; +import { traceVerbose } from '../../../../logging'; +import { DEFAULT_INTERPRETER_SETTING } from '../../../../common/constants'; + +export const DEFAULT_INTERPRETER_PATH_SETTING_KEY = 'defaultInterpreterPath'; + +/** + * Finds and resolves custom virtual environments that users have provided. + */ +export class CustomWorkspaceLocator extends FSWatchingLocator { + public readonly providerId: string = 'custom-workspace-locator'; + + constructor(private readonly root: string) { + super( + () => [], + async () => PythonEnvKind.Unknown, + ); + } + + protected async initResources(): Promise { + this.disposables.push( + onDidChangePythonSetting(DEFAULT_INTERPRETER_PATH_SETTING_KEY, () => this.fire(), this.root), + ); + } + + // eslint-disable-next-line class-methods-use-this + protected doIterEnvs(): IPythonEnvsIterator { + const iterator = async function* (root: string) { + traceVerbose('Searching for custom workspace envs'); + const filename = getPythonSetting(DEFAULT_INTERPRETER_PATH_SETTING_KEY, root); + if (!filename || filename === DEFAULT_INTERPRETER_SETTING) { + // If the user has not set a custom interpreter, our job is done. + return; + } + yield { kind: PythonEnvKind.Unknown, executablePath: filename }; + traceVerbose(`Finished searching for custom workspace envs`); + }; + return iterator(this.root); + } +} diff --git a/src/client/pythonEnvironments/base/locators/lowLevel/fsWatchingLocator.ts b/src/client/pythonEnvironments/base/locators/lowLevel/fsWatchingLocator.ts index 0eb1d125200c..dd7db5538565 100644 --- a/src/client/pythonEnvironments/base/locators/lowLevel/fsWatchingLocator.ts +++ b/src/client/pythonEnvironments/base/locators/lowLevel/fsWatchingLocator.ts @@ -6,7 +6,7 @@ import * as path from 'path'; import { Uri } from 'vscode'; import { FileChangeType, watchLocationForPattern } from '../../../../common/platform/fileSystemWatcher'; import { sleep } from '../../../../common/utils/async'; -import { traceError, traceVerbose } from '../../../../logging'; +import { traceVerbose, traceWarn } from '../../../../logging'; import { getEnvironmentDirFromPath } from '../../../common/commonUtils'; import { PythonEnvStructure, @@ -32,13 +32,13 @@ function checkDirWatchable(dirname: string): DirUnwatchableReason { names = fs.readdirSync(dirname); } catch (err) { const exception = err as NodeJS.ErrnoException; - traceError('Reading directory to watch failed', exception); + traceVerbose('Reading directory failed', exception); if (exception.code === 'ENOENT') { // Treat a missing directory as unwatchable since it can lead to CPU load issues: // https://github.com/microsoft/vscode-python/issues/18459 return 'directory does not exist'; } - throw err; // re-throw + return undefined; } // The limit here is an educated guess. if (names.length > 200) { @@ -105,9 +105,7 @@ export abstract class FSWatchingLocator extends LazyResourceBasedLocator { } // Start the FS watchers. - traceVerbose('Getting roots'); let roots = await this.getRoots(); - traceVerbose('Found roots'); if (typeof roots === 'string') { roots = [roots]; } @@ -119,7 +117,7 @@ export abstract class FSWatchingLocator extends LazyResourceBasedLocator { // that might be watched due to a glob are not checked. const unwatchable = await checkDirWatchable(root); if (unwatchable) { - traceError(`Dir "${root}" is not watchable (${unwatchable})`); + traceWarn(`Dir "${root}" is not watchable (${unwatchable})`); return undefined; } return root; @@ -128,6 +126,10 @@ export abstract class FSWatchingLocator extends LazyResourceBasedLocator { watchableRoots.forEach((root) => this.startWatchers(root)); } + protected fire(args = {}): void { + this.emitter.fire({ ...args, providerId: this.providerId }); + } + private startWatchers(root: string): void { const opts = this.creationOptions; if (isWatchingAFile(opts)) { diff --git a/src/client/pythonEnvironments/base/locators/lowLevel/globalVirtualEnvronmentLocator.ts b/src/client/pythonEnvironments/base/locators/lowLevel/globalVirtualEnvronmentLocator.ts index d86b2182d50c..86fbbed55043 100644 --- a/src/client/pythonEnvironments/base/locators/lowLevel/globalVirtualEnvronmentLocator.ts +++ b/src/client/pythonEnvironments/base/locators/lowLevel/globalVirtualEnvronmentLocator.ts @@ -1,16 +1,17 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -import { uniq } from 'lodash'; +import { toLower, uniq, uniqBy } from 'lodash'; import * as path from 'path'; +import { Uri } from 'vscode'; import { chain, iterable } from '../../../../common/utils/async'; import { getEnvironmentVariable, getOSType, getUserHomeDir, OSType } from '../../../../common/utils/platform'; import { PythonEnvKind } from '../../info'; import { BasicEnvInfo, IPythonEnvsIterator } from '../../locator'; import { FSWatchingLocator } from './fsWatchingLocator'; import { findInterpretersInDir, looksLikeBasicVirtualPython } from '../../../common/commonUtils'; -import { pathExists, untildify } from '../../../common/externalDependencies'; -import { isPipenvEnvironment } from '../../../common/environmentManagers/pipenv'; +import { pathExists } from '../../../common/externalDependencies'; +import { getProjectDir, isPipenvEnvironment } from '../../../common/environmentManagers/pipenv'; import { isVenvEnvironment, isVirtualenvEnvironment, @@ -18,7 +19,9 @@ import { } from '../../../common/environmentManagers/simplevirtualenvs'; import '../../../../common/extensions'; import { asyncFilter } from '../../../../common/utils/arrayUtils'; -import { traceError, traceVerbose } from '../../../../logging'; +import { traceError, traceInfo, traceVerbose } from '../../../../logging'; +import { StopWatch } from '../../../../common/utils/stopWatch'; +import { untildify } from '../../../../common/helpers'; const DEFAULT_SEARCH_DEPTH = 2; /** @@ -39,10 +42,14 @@ async function getGlobalVirtualEnvDirs(): Promise { const homeDir = getUserHomeDir(); if (homeDir && (await pathExists(homeDir))) { - const subDirs = ['Envs', '.direnv', '.venvs', '.virtualenvs', path.join('.local', 'share', 'virtualenvs')]; - if (getOSType() !== OSType.Windows) { - subDirs.push('envs'); - } + const subDirs = [ + 'envs', + 'Envs', + '.direnv', + '.venvs', + '.virtualenvs', + path.join('.local', 'share', 'virtualenvs'), + ]; const filtered = await asyncFilter( subDirs.map((d) => path.join(homeDir, d)), pathExists, @@ -50,7 +57,19 @@ async function getGlobalVirtualEnvDirs(): Promise { filtered.forEach((d) => venvDirs.push(d)); } - return uniq(venvDirs); + return [OSType.Windows, OSType.OSX].includes(getOSType()) ? uniqBy(venvDirs, toLower) : uniq(venvDirs); +} + +async function getSearchLocation(env: BasicEnvInfo): Promise { + if (env.kind === PythonEnvKind.Pipenv) { + // Pipenv environments are created only for a specific project, so they must only + // appear if that particular project is being queried. + const project = await getProjectDir(path.dirname(path.dirname(env.executablePath))); + if (project) { + return Uri.file(project); + } + } + return undefined; } /** @@ -101,6 +120,8 @@ export class GlobalVirtualEnvironmentLocator extends FSWatchingLocator { const searchDepth = this.searchDepth ?? DEFAULT_SEARCH_DEPTH; async function* iterator() { + const stopWatch = new StopWatch(); + traceInfo('Searching for global virtual environments'); const envRootDirs = await getGlobalVirtualEnvDirs(); const envGenerators = envRootDirs.map((envRootDir) => { async function* generator() { @@ -119,8 +140,9 @@ export class GlobalVirtualEnvironmentLocator extends FSWatchingLocator { // check multiple times. Those checks are file system heavy and // we can use the kind to determine this anyway. const kind = await getVirtualEnvKind(filename); + const searchLocation = await getSearchLocation({ kind, executablePath: filename }); try { - yield { kind, executablePath: filename }; + yield { kind, executablePath: filename, searchLocation }; traceVerbose(`Global Virtual Environment: [added] ${filename}`); } catch (ex) { traceError(`Failed to process environment: ${filename}`, ex); @@ -134,7 +156,7 @@ export class GlobalVirtualEnvironmentLocator extends FSWatchingLocator { }); yield* iterable(chain(envGenerators)); - traceVerbose(`Finished searching for global virtual envs`); + traceInfo(`Finished searching for global virtual envs: ${stopWatch.elapsedTime} milliseconds`); } return iterator(); diff --git a/src/client/pythonEnvironments/base/locators/lowLevel/hatchLocator.ts b/src/client/pythonEnvironments/base/locators/lowLevel/hatchLocator.ts new file mode 100644 index 000000000000..f7746a8c5a2e --- /dev/null +++ b/src/client/pythonEnvironments/base/locators/lowLevel/hatchLocator.ts @@ -0,0 +1,57 @@ +'use strict'; + +import { PythonEnvKind } from '../../info'; +import { BasicEnvInfo, IPythonEnvsIterator } from '../../locator'; +import { LazyResourceBasedLocator } from '../common/resourceBasedLocator'; +import { Hatch } from '../../../common/environmentManagers/hatch'; +import { asyncFilter } from '../../../../common/utils/arrayUtils'; +import { pathExists } from '../../../common/externalDependencies'; +import { traceError, traceVerbose } from '../../../../logging'; +import { chain, iterable } from '../../../../common/utils/async'; +import { getInterpreterPathFromDir } from '../../../common/commonUtils'; + +/** + * Gets all default virtual environment locations to look for in a workspace. + */ +async function getVirtualEnvDirs(root: string): Promise { + const hatch = await Hatch.getHatch(root); + const envDirs = (await hatch?.getEnvList()) ?? []; + return asyncFilter(envDirs, pathExists); +} + +/** + * Finds and resolves virtual environments created using Hatch. + */ +export class HatchLocator extends LazyResourceBasedLocator { + public readonly providerId: string = 'hatch'; + + public constructor(private readonly root: string) { + super(); + } + + protected doIterEnvs(): IPythonEnvsIterator { + async function* iterator(root: string) { + const envDirs = await getVirtualEnvDirs(root); + const envGenerators = envDirs.map((envDir) => { + async function* generator() { + traceVerbose(`Searching for Hatch virtual envs in: ${envDir}`); + const filename = await getInterpreterPathFromDir(envDir); + if (filename !== undefined) { + try { + yield { executablePath: filename, kind: PythonEnvKind.Hatch }; + traceVerbose(`Hatch Virtual Environment: [added] ${filename}`); + } catch (ex) { + traceError(`Failed to process environment: ${filename}`, ex); + } + } + } + return generator(); + }); + + yield* iterable(chain(envGenerators)); + traceVerbose(`Finished searching for Hatch envs`); + } + + return iterator(this.root); + } +} diff --git a/src/client/pythonEnvironments/base/locators/lowLevel/microsoftStoreLocator.ts b/src/client/pythonEnvironments/base/locators/lowLevel/microsoftStoreLocator.ts index 9b5283f7f967..2068a05f3a69 100644 --- a/src/client/pythonEnvironments/base/locators/lowLevel/microsoftStoreLocator.ts +++ b/src/client/pythonEnvironments/base/locators/lowLevel/microsoftStoreLocator.ts @@ -1,9 +1,9 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -import * as fsapi from 'fs-extra'; import * as minimatch from 'minimatch'; import * as path from 'path'; +import * as fsapi from '../../../../common/platform/fs-paths'; import { PythonEnvKind } from '../../info'; import { IPythonEnvsIterator, BasicEnvInfo } from '../../locator'; import { FSWatchingLocator } from './fsWatchingLocator'; @@ -12,7 +12,8 @@ import { isStorePythonInstalled, getMicrosoftStoreAppsRoot, } from '../../../common/environmentManagers/microsoftStoreEnv'; -import { traceVerbose } from '../../../../logging'; +import { traceInfo } from '../../../../logging'; +import { StopWatch } from '../../../../common/utils/stopWatch'; /** * This is a glob pattern which matches following file names: @@ -34,7 +35,7 @@ const pythonExeGlob = 'python3.{[0-9],[0-9][0-9]}.exe'; * @returns {boolean} : Returns true if the path matches pattern for windows python executable. */ function isMicrosoftStorePythonExePattern(interpreterPath: string): boolean { - return minimatch(path.basename(interpreterPath), pythonExeGlob, { nocase: true }); + return minimatch.default(path.basename(interpreterPath), pythonExeGlob, { nocase: true }); } /** @@ -87,12 +88,14 @@ export class MicrosoftStoreLocator extends FSWatchingLocator { protected doIterEnvs(): IPythonEnvsIterator { const iterator = async function* (kind: PythonEnvKind) { + const stopWatch = new StopWatch(); + traceInfo('Searching for windows store envs'); const exes = await getMicrosoftStorePythonExes(); yield* exes.map(async (executablePath: string) => ({ kind, executablePath, })); - traceVerbose(`Finished searching for windows store envs`); + traceInfo(`Finished searching for windows store envs: ${stopWatch.elapsedTime} milliseconds`); }; return iterator(this.kind); } diff --git a/src/client/pythonEnvironments/base/locators/lowLevel/pixiLocator.ts b/src/client/pythonEnvironments/base/locators/lowLevel/pixiLocator.ts new file mode 100644 index 000000000000..f4a3886a2120 --- /dev/null +++ b/src/client/pythonEnvironments/base/locators/lowLevel/pixiLocator.ts @@ -0,0 +1,77 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import * as path from 'path'; +import { asyncFilter } from '../../../../common/utils/arrayUtils'; +import { chain, iterable } from '../../../../common/utils/async'; +import { traceError, traceVerbose } from '../../../../logging'; +import { getCondaInterpreterPath } from '../../../common/environmentManagers/conda'; +import { pathExists } from '../../../common/externalDependencies'; +import { PythonEnvKind } from '../../info'; +import { IPythonEnvsIterator, BasicEnvInfo } from '../../locator'; +import { FSWatcherKind, FSWatchingLocator } from './fsWatchingLocator'; +import { getPixi } from '../../../common/environmentManagers/pixi'; + +/** + * Returns all virtual environment locations to look for in a workspace. + */ +async function getVirtualEnvDirs(root: string): Promise { + const pixi = await getPixi(); + const envDirs = (await pixi?.getEnvList(root)) ?? []; + return asyncFilter(envDirs, pathExists); +} + +/** + * Returns all virtual environment locations to look for in a workspace. + */ +function getVirtualEnvRootDirs(root: string): string[] { + return [path.join(path.join(root, '.pixi'), 'envs')]; +} + +export class PixiLocator extends FSWatchingLocator { + public readonly providerId: string = 'pixi'; + + public constructor(private readonly root: string) { + super( + async () => getVirtualEnvRootDirs(this.root), + async () => PythonEnvKind.Pixi, + { + // Note detecting kind of virtual env depends on the file structure around the + // executable, so we need to wait before attempting to detect it. + delayOnCreated: 1000, + }, + FSWatcherKind.Workspace, + ); + } + + protected doIterEnvs(): IPythonEnvsIterator { + async function* iterator(root: string) { + const envDirs = await getVirtualEnvDirs(root); + const envGenerators = envDirs.map((envDir) => { + async function* generator() { + traceVerbose(`Searching for Pixi virtual envs in: ${envDir}`); + const filename = await getCondaInterpreterPath(envDir); + if (filename !== undefined) { + try { + yield { + executablePath: filename, + kind: PythonEnvKind.Pixi, + envPath: envDir, + }; + + traceVerbose(`Pixi Virtual Environment: [added] ${filename}`); + } catch (ex) { + traceError(`Failed to process environment: ${filename}`, ex); + } + } + } + return generator(); + }); + + yield* iterable(chain(envGenerators)); + traceVerbose(`Finished searching for Pixi envs`); + } + + return iterator(this.root); + } +} diff --git a/src/client/pythonEnvironments/base/locators/lowLevel/poetryLocator.ts b/src/client/pythonEnvironments/base/locators/lowLevel/poetryLocator.ts index 4084c7a5cfbc..ab1a8cf77444 100644 --- a/src/client/pythonEnvironments/base/locators/lowLevel/poetryLocator.ts +++ b/src/client/pythonEnvironments/base/locators/lowLevel/poetryLocator.ts @@ -4,6 +4,7 @@ 'use strict'; import * as path from 'path'; +import { Uri } from 'vscode'; import { chain, iterable } from '../../../../common/utils/async'; import { PythonEnvKind } from '../../info'; import { BasicEnvInfo, IPythonEnvsIterator } from '../../locator'; @@ -59,7 +60,7 @@ export class PoetryLocator extends LazyResourceBasedLocator { // We should extract the kind here to avoid doing is*Environment() // check multiple times. Those checks are file system heavy and // we can use the kind to determine this anyway. - yield { executablePath: filename, kind }; + yield { executablePath: filename, kind, searchLocation: Uri.file(root) }; traceVerbose(`Poetry Virtual Environment: [added] ${filename}`); } catch (ex) { traceError(`Failed to process environment: ${filename}`, ex); diff --git a/src/client/pythonEnvironments/base/locators/lowLevel/posixKnownPathsLocator.ts b/src/client/pythonEnvironments/base/locators/lowLevel/posixKnownPathsLocator.ts index 97726307c573..daca4b860907 100644 --- a/src/client/pythonEnvironments/base/locators/lowLevel/posixKnownPathsLocator.ts +++ b/src/client/pythonEnvironments/base/locators/lowLevel/posixKnownPathsLocator.ts @@ -9,7 +9,8 @@ import { commonPosixBinPaths, getPythonBinFromPosixPaths } from '../../../common import { isPyenvShimDir } from '../../../common/environmentManagers/pyenv'; import { getOSType, OSType } from '../../../../common/utils/platform'; import { isMacDefaultPythonPath } from '../../../common/environmentManagers/macDefault'; -import { traceError, traceVerbose } from '../../../../logging'; +import { traceError, traceInfo, traceVerbose } from '../../../../logging'; +import { StopWatch } from '../../../../common/utils/stopWatch'; export class PosixKnownPathsLocator extends Locator { public readonly providerId = 'posixKnownPaths'; @@ -26,25 +27,34 @@ export class PosixKnownPathsLocator extends Locator { } const iterator = async function* (kind: PythonEnvKind) { - // Filter out pyenv shims. They are not actual python binaries, they are used to launch - // the binaries specified in .python-version file in the cwd. We should not be reporting - // those binaries as environments. - const knownDirs = (await commonPosixBinPaths()).filter((dirname) => !isPyenvShimDir(dirname)); - let pythonBinaries = await getPythonBinFromPosixPaths(knownDirs); + const stopWatch = new StopWatch(); + traceInfo('Searching for interpreters in posix paths locator'); + try { + // Filter out pyenv shims. They are not actual python binaries, they are used to launch + // the binaries specified in .python-version file in the cwd. We should not be reporting + // those binaries as environments. + const knownDirs = (await commonPosixBinPaths()).filter((dirname) => !isPyenvShimDir(dirname)); + let pythonBinaries = await getPythonBinFromPosixPaths(knownDirs); + traceVerbose(`Found ${pythonBinaries.length} python binaries in posix paths`); - // Filter out MacOS system installs of Python 2 if necessary. - if (isMacPython2Deprecated) { - pythonBinaries = pythonBinaries.filter((binary) => !isMacDefaultPythonPath(binary)); - } + // Filter out MacOS system installs of Python 2 if necessary. + if (isMacPython2Deprecated) { + pythonBinaries = pythonBinaries.filter((binary) => !isMacDefaultPythonPath(binary)); + } - for (const bin of pythonBinaries) { - try { - yield { executablePath: bin, kind, source: [PythonEnvSource.PathEnvVar] }; - } catch (ex) { - traceError(`Failed to process environment: ${bin}`, ex); + for (const bin of pythonBinaries) { + try { + yield { executablePath: bin, kind, source: [PythonEnvSource.PathEnvVar] }; + } catch (ex) { + traceError(`Failed to process environment: ${bin}`, ex); + } } + } catch (ex) { + traceError('Failed to process posix paths', ex); } - traceVerbose('Finished searching for interpreters in posix paths locator'); + traceInfo( + `Finished searching for interpreters in posix paths locator: ${stopWatch.elapsedTime} milliseconds`, + ); }; return iterator(this.kind); } diff --git a/src/client/pythonEnvironments/base/locators/lowLevel/pyenvLocator.ts b/src/client/pythonEnvironments/base/locators/lowLevel/pyenvLocator.ts index dc3290c9993c..e97b69c6b882 100644 --- a/src/client/pythonEnvironments/base/locators/lowLevel/pyenvLocator.ts +++ b/src/client/pythonEnvironments/base/locators/lowLevel/pyenvLocator.ts @@ -7,7 +7,8 @@ import { FSWatchingLocator } from './fsWatchingLocator'; import { getInterpreterPathFromDir } from '../../../common/commonUtils'; import { getSubDirs } from '../../../common/externalDependencies'; import { getPyenvVersionsDir } from '../../../common/environmentManagers/pyenv'; -import { traceError, traceVerbose } from '../../../../logging'; +import { traceError, traceInfo } from '../../../../logging'; +import { StopWatch } from '../../../../common/utils/stopWatch'; /** * Gets all the pyenv environments. @@ -16,24 +17,31 @@ import { traceError, traceVerbose } from '../../../../logging'; * all the environments (global or virtual) in that directory. */ async function* getPyenvEnvironments(): AsyncIterableIterator { - const pyenvVersionDir = getPyenvVersionsDir(); + const stopWatch = new StopWatch(); + traceInfo('Searching for pyenv environments'); + try { + const pyenvVersionDir = getPyenvVersionsDir(); - const subDirs = getSubDirs(pyenvVersionDir, { resolveSymlinks: true }); - for await (const subDirPath of subDirs) { - const interpreterPath = await getInterpreterPathFromDir(subDirPath); + const subDirs = getSubDirs(pyenvVersionDir, { resolveSymlinks: true }); + for await (const subDirPath of subDirs) { + const interpreterPath = await getInterpreterPathFromDir(subDirPath); - if (interpreterPath) { - try { - yield { - kind: PythonEnvKind.Pyenv, - executablePath: interpreterPath, - }; - } catch (ex) { - traceError(`Failed to process environment: ${interpreterPath}`, ex); + if (interpreterPath) { + try { + yield { + kind: PythonEnvKind.Pyenv, + executablePath: interpreterPath, + }; + } catch (ex) { + traceError(`Failed to process environment: ${interpreterPath}`, ex); + } } } + } catch (ex) { + // This is expected when pyenv is not installed + traceInfo(`pyenv is not installed`); } - traceVerbose('Finished searching for pyenv environments'); + traceInfo(`Finished searching for pyenv environments: ${stopWatch.elapsedTime} milliseconds`); } export class PyenvLocator extends FSWatchingLocator { diff --git a/src/client/pythonEnvironments/base/locators/lowLevel/windowsKnownPathsLocator.ts b/src/client/pythonEnvironments/base/locators/lowLevel/windowsKnownPathsLocator.ts index 377b1117b858..440d075b4071 100644 --- a/src/client/pythonEnvironments/base/locators/lowLevel/windowsKnownPathsLocator.ts +++ b/src/client/pythonEnvironments/base/locators/lowLevel/windowsKnownPathsLocator.ts @@ -4,10 +4,10 @@ /* eslint-disable max-classes-per-file */ import { Event } from 'vscode'; +import * as path from 'path'; import { IDisposable } from '../../../../common/types'; import { getSearchPathEntries } from '../../../../common/utils/exec'; import { Disposables } from '../../../../common/utils/resourceLifecycle'; -import { iterPythonExecutablesInDir, looksLikeBasicGlobalPython } from '../../../common/commonUtils'; import { isPyenvShimDir } from '../../../common/environmentManagers/pyenv'; import { isMicrosoftStoreDir } from '../../../common/environmentManagers/microsoftStoreEnv'; import { PythonEnvKind, PythonEnvSource } from '../../info'; @@ -16,7 +16,11 @@ import { Locators } from '../../locators'; import { getEnvs } from '../../locatorUtils'; import { PythonEnvsChangedEvent } from '../../watcher'; import { DirFilesLocator } from './filesLocator'; -import { traceVerbose } from '../../../../logging'; +import { traceInfo } from '../../../../logging'; +import { inExperiment, pathExists } from '../../../common/externalDependencies'; +import { DiscoveryUsingWorkers } from '../../../../common/experiments/groups'; +import { iterPythonExecutablesInDir, looksLikeBasicGlobalPython } from '../../../common/commonUtils'; +import { StopWatch } from '../../../../common/utils/stopWatch'; /** * A locator for Windows locators found under the $PATH env var. @@ -34,6 +38,7 @@ export class WindowsPathEnvVarLocator implements ILocator, IDispos private readonly disposables = new Disposables(); constructor() { + const inExp = inExperiment(DiscoveryUsingWorkers.experiment); const dirLocators: (ILocator & IDisposable)[] = getSearchPathEntries() .filter( (dirname) => @@ -48,7 +53,7 @@ export class WindowsPathEnvVarLocator implements ILocator, IDispos !isMicrosoftStoreDir(dirname) && !isPyenvShimDir(dirname), ) // Build a locator for each directory. - .map((dirname) => getDirFilesLocator(dirname, PythonEnvKind.System, [PythonEnvSource.PathEnvVar])); + .map((dirname) => getDirFilesLocator(dirname, PythonEnvKind.System, [PythonEnvSource.PathEnvVar], inExp)); this.disposables.push(...dirLocators); this.locators = new Locators(dirLocators); this.onChanged = this.locators.onChanged; @@ -63,11 +68,19 @@ export class WindowsPathEnvVarLocator implements ILocator, IDispos // Note that we do no filtering here, including to check if files // are valid executables. That is left to callers (e.g. composite // locators). - return this.locators.iterEnvs(query); + async function* iterator(it: IPythonEnvsIterator) { + const stopWatch = new StopWatch(); + traceInfo(`Searching windows known paths locator`); + for await (const env of it) { + yield env; + } + traceInfo(`Finished searching windows known paths locator: ${stopWatch.elapsedTime} milliseconds`); + } + return iterator(this.locators.iterEnvs(query)); } } -async function* getExecutables(dirname: string): AsyncIterableIterator { +async function* oldGetExecutables(dirname: string): AsyncIterableIterator { for await (const entry of iterPythonExecutablesInDir(dirname)) { if (await looksLikeBasicGlobalPython(entry)) { yield entry.filename; @@ -75,17 +88,26 @@ async function* getExecutables(dirname: string): AsyncIterableIterator { } } +async function* getExecutables(dirname: string): AsyncIterableIterator { + const executable = path.join(dirname, 'python.exe'); + if (await pathExists(executable)) { + yield executable; + } +} + function getDirFilesLocator( // These are passed through to DirFilesLocator. dirname: string, kind: PythonEnvKind, source?: PythonEnvSource[], + inExp?: boolean, ): ILocator & IDisposable { // For now we do not bother using a locator that watches for changes // in the directory. If we did then we would use // `DirFilesWatchingLocator`, but only if not \\windows\system32 and // the `isDirWatchable()` (from fsWatchingLocator.ts) returns true. - const locator = new DirFilesLocator(dirname, kind, getExecutables, source); + const executableFunc = inExp ? getExecutables : oldGetExecutables; + const locator = new DirFilesLocator(dirname, kind, executableFunc, source); const dispose = async () => undefined; // Really we should be checking for symlinks or something more @@ -93,8 +115,7 @@ function getDirFilesLocator( // rather than in each low-level locator. In the meantime we // take a naive approach. async function* iterEnvs(query: PythonLocatorQuery): IPythonEnvsIterator { - yield* await getEnvs(locator.iterEnvs(query)); - traceVerbose('Finished searching for windows path interpreters'); + yield* await getEnvs(locator.iterEnvs(query)).then((res) => res); } return { providerId: locator.providerId, diff --git a/src/client/pythonEnvironments/base/locators/lowLevel/windowsRegistryLocator.ts b/src/client/pythonEnvironments/base/locators/lowLevel/windowsRegistryLocator.ts index 954d1bfd2a41..1447c2a90767 100644 --- a/src/client/pythonEnvironments/base/locators/lowLevel/windowsRegistryLocator.ts +++ b/src/client/pythonEnvironments/base/locators/lowLevel/windowsRegistryLocator.ts @@ -1,40 +1,75 @@ +/* eslint-disable require-yield */ /* eslint-disable no-continue */ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. import { PythonEnvKind, PythonEnvSource } from '../../info'; -import { BasicEnvInfo, IPythonEnvsIterator, Locator } from '../../locator'; +import { BasicEnvInfo, IPythonEnvsIterator, Locator, PythonLocatorQuery, IEmitter } from '../../locator'; import { getRegistryInterpreters } from '../../../common/windowsUtils'; -import { traceError, traceVerbose } from '../../../../logging'; +import { traceError, traceInfo } from '../../../../logging'; import { isMicrosoftStoreDir } from '../../../common/environmentManagers/microsoftStoreEnv'; +import { PythonEnvsChangedEvent } from '../../watcher'; +import { DiscoveryUsingWorkers } from '../../../../common/experiments/groups'; +import { inExperiment } from '../../../common/externalDependencies'; +import { StopWatch } from '../../../../common/utils/stopWatch'; + +export const WINDOWS_REG_PROVIDER_ID = 'windows-registry'; export class WindowsRegistryLocator extends Locator { - public readonly providerId: string = 'windows-registry'; + public readonly providerId: string = WINDOWS_REG_PROVIDER_ID; // eslint-disable-next-line class-methods-use-this - public iterEnvs(): IPythonEnvsIterator { - const iterator = async function* () { - const interpreters = await getRegistryInterpreters(); - for (const interpreter of interpreters) { - try { - // Filter out Microsoft Store app directories. We have a store app locator that handles this. - // The python.exe available in these directories might not be python. It can be a store install - // shortcut that takes you to microsoft store. - if (isMicrosoftStoreDir(interpreter.interpreterPath)) { - continue; - } - const env: BasicEnvInfo = { - kind: PythonEnvKind.OtherGlobal, - executablePath: interpreter.interpreterPath, - source: [PythonEnvSource.WindowsRegistry], - }; - yield env; - } catch (ex) { - traceError(`Failed to process environment: ${interpreter}`, ex); - } + public iterEnvs( + query?: PythonLocatorQuery, + useWorkerThreads = inExperiment(DiscoveryUsingWorkers.experiment), + ): IPythonEnvsIterator { + if (useWorkerThreads) { + /** + * Windows registry is slow and often not necessary, so notify completion immediately, but use watcher + * change events to signal for any new envs which are found. + */ + if (query?.providerId === this.providerId) { + // Query via change event, so iterate all envs. + return iterateEnvs(); + } + return iterateEnvsLazily(this.emitter); + } + return iterateEnvs(); + } +} + +async function* iterateEnvsLazily(changed: IEmitter): IPythonEnvsIterator { + loadAllEnvs(changed).ignoreErrors(); +} + +async function loadAllEnvs(changed: IEmitter) { + const stopWatch = new StopWatch(); + traceInfo('Searching for windows registry interpreters'); + changed.fire({ providerId: WINDOWS_REG_PROVIDER_ID }); + traceInfo(`Finished searching for windows registry interpreters: ${stopWatch.elapsedTime} milliseconds`); +} + +async function* iterateEnvs(): IPythonEnvsIterator { + const stopWatch = new StopWatch(); + traceInfo('Searching for windows registry interpreters'); + const interpreters = await getRegistryInterpreters(); // Value should already be loaded at this point, so this returns immediately. + for (const interpreter of interpreters) { + try { + // Filter out Microsoft Store app directories. We have a store app locator that handles this. + // The python.exe available in these directories might not be python. It can be a store install + // shortcut that takes you to microsoft store. + if (isMicrosoftStoreDir(interpreter.interpreterPath)) { + continue; } - traceVerbose('Finished searching for windows registry interpreters'); - }; - return iterator(); + const env: BasicEnvInfo = { + kind: PythonEnvKind.OtherGlobal, + executablePath: interpreter.interpreterPath, + source: [PythonEnvSource.WindowsRegistry], + }; + yield env; + } catch (ex) { + traceError(`Failed to process environment: ${interpreter}`, ex); + } } + traceInfo(`Finished searching for windows registry interpreters: ${stopWatch.elapsedTime} milliseconds`); } diff --git a/src/client/pythonEnvironments/common/commonUtils.ts b/src/client/pythonEnvironments/common/commonUtils.ts index 85462531e5e3..4bd94e0402ab 100644 --- a/src/client/pythonEnvironments/common/commonUtils.ts +++ b/src/client/pythonEnvironments/common/commonUtils.ts @@ -5,7 +5,7 @@ import * as fs from 'fs'; import * as path from 'path'; import { convertFileType, DirEntry, FileType, getFileFilter, getFileType } from '../../common/utils/filesystem'; import { getOSType, OSType } from '../../common/utils/platform'; -import { traceError } from '../../logging'; +import { traceError, traceVerbose } from '../../logging'; import { PythonVersion, UNKNOWN_PYTHON_VERSION } from '../base/info'; import { comparePythonVersionSpecificity } from '../base/info/env'; import { parseVersion } from '../base/info/pythonVersion'; @@ -246,8 +246,11 @@ export async function getPythonVersionFromPath(interpreterPath: string, hint?: s versionA = UNKNOWN_PYTHON_VERSION; } const versionB = interpreterPath ? await getPythonVersionFromNearByFiles(interpreterPath) : UNKNOWN_PYTHON_VERSION; + traceVerbose('Best effort version B for', interpreterPath, JSON.stringify(versionB)); const versionC = interpreterPath ? await getPythonVersionFromPyvenvCfg(interpreterPath) : UNKNOWN_PYTHON_VERSION; + traceVerbose('Best effort version C for', interpreterPath, JSON.stringify(versionC)); const versionD = interpreterPath ? await getPythonVersionFromConda(interpreterPath) : UNKNOWN_PYTHON_VERSION; + traceVerbose('Best effort version D for', interpreterPath, JSON.stringify(versionD)); let version = UNKNOWN_PYTHON_VERSION; for (const v of [versionA, versionB, versionC, versionD]) { diff --git a/src/client/pythonEnvironments/common/environmentIdentifier.ts b/src/client/pythonEnvironments/common/environmentIdentifier.ts index 2dbc8b2b93d9..89ff84823673 100644 --- a/src/client/pythonEnvironments/common/environmentIdentifier.ts +++ b/src/client/pythonEnvironments/common/environmentIdentifier.ts @@ -16,9 +16,11 @@ import { } from './environmentManagers/simplevirtualenvs'; import { isMicrosoftStoreEnvironment } from './environmentManagers/microsoftStoreEnv'; import { isActiveStateEnvironment } from './environmentManagers/activestate'; +import { isPixiEnvironment } from './environmentManagers/pixi'; + +const notImplemented = () => Promise.resolve(false); function getIdentifiers(): Map Promise> { - const notImplemented = () => Promise.resolve(false); const defaultTrue = () => Promise.resolve(true); const identifier: Map Promise> = new Map(); Object.values(PythonEnvKind).forEach((k) => { @@ -30,6 +32,7 @@ function getIdentifiers(): Map Promise identifier.set(PythonEnvKind.Pipenv, isPipenvEnvironment); identifier.set(PythonEnvKind.Pyenv, isPyenvEnvironment); identifier.set(PythonEnvKind.Poetry, isPoetryEnvironment); + identifier.set(PythonEnvKind.Pixi, isPixiEnvironment); identifier.set(PythonEnvKind.Venv, isVenvEnvironment); identifier.set(PythonEnvKind.VirtualEnvWrapper, isVirtualEnvWrapperEnvironment); identifier.set(PythonEnvKind.VirtualEnv, isVirtualEnvEnvironment); @@ -39,6 +42,15 @@ function getIdentifiers(): Map Promise return identifier; } +export function isIdentifierRegistered(kind: PythonEnvKind): boolean { + const identifiers = getIdentifiers(); + const identifier = identifiers.get(kind); + if (identifier === notImplemented) { + return false; + } + return true; +} + /** * Returns environment type. * @param {string} path : Absolute path to the python interpreter binary or path to environment. diff --git a/src/client/pythonEnvironments/common/environmentManagers/activestate.ts b/src/client/pythonEnvironments/common/environmentManagers/activestate.ts index 75b34f41176c..5f22a96e4f83 100644 --- a/src/client/pythonEnvironments/common/environmentManagers/activestate.ts +++ b/src/client/pythonEnvironments/common/environmentManagers/activestate.ts @@ -75,8 +75,8 @@ export class ActiveState { private static readonly defaultStateCommand: string = 'state'; - @cache(30_000, true, 10_000) // eslint-disable-next-line class-methods-use-this + @cache(30_000, true, 10_000) private async getProjectsCached(): Promise { try { const stateCommand = diff --git a/src/client/pythonEnvironments/common/environmentManagers/conda.ts b/src/client/pythonEnvironments/common/environmentManagers/conda.ts index 88178d02d58a..c1bfd7d68bc2 100644 --- a/src/client/pythonEnvironments/common/environmentManagers/conda.ts +++ b/src/client/pythonEnvironments/common/environmentManagers/conda.ts @@ -1,6 +1,6 @@ -import * as fsapi from 'fs-extra'; import * as path from 'path'; import { lt, SemVer } from 'semver'; +import * as fsapi from '../../../common/platform/fs-paths'; import { getEnvironmentVariable, getOSType, getUserHomeDir, OSType } from '../../../common/utils/platform'; import { arePathsSame, @@ -23,6 +23,8 @@ import { traceError, traceVerbose } from '../../../logging'; import { OUTPUT_MARKER_SCRIPT } from '../../../common/process/internal/scripts'; import { splitLines } from '../../../common/stringUtils'; import { SpawnOptions } from '../../../common/process/types'; +import { sleep } from '../../../common/utils/async'; +import { getConfiguration } from '../../../common/vscodeApis/workspaceApis'; export const AnacondaCompanyName = 'Anaconda, Inc.'; export const CONDAPATH_SETTING_KEY = 'condaPath'; @@ -43,6 +45,10 @@ export type CondaInfo = { root_prefix?: string; // eslint-disable-line camelcase conda_version?: string; // eslint-disable-line camelcase conda_shlvl?: number; // eslint-disable-line camelcase + config_files?: string[]; // eslint-disable-line camelcase + rc_path?: string; // eslint-disable-line camelcase + sys_rc_path?: string; // eslint-disable-line camelcase + user_rc_path?: string; // eslint-disable-line camelcase }; type CondaEnvInfo = { @@ -238,7 +244,7 @@ export function getCondaInterpreterPath(condaEnvironmentPath: string): string { // Minimum version number of conda required to be able to use 'conda run' with '--no-capture-output' flag. export const CONDA_RUN_VERSION = '4.9.0'; export const CONDA_ACTIVATION_TIMEOUT = 45000; -const CONDA_GENERAL_TIMEOUT = 50000; +const CONDA_GENERAL_TIMEOUT = 45000; /** Wraps the "conda" utility, and exposes its functionality. */ @@ -264,7 +270,15 @@ export class Conda { * @param command - Command used to spawn conda. This has the same meaning as the * first argument of spawn() - i.e. it can be a full path, or just a binary name. */ - constructor(readonly command: string, shellCommand?: string, private readonly shellPath?: string) { + constructor( + readonly command: string, + shellCommand?: string, + private readonly shellPath?: string, + private readonly useWorkerThreads?: boolean, + ) { + if (this.useWorkerThreads === undefined) { + this.useWorkerThreads = false; + } this.shellCommand = shellCommand ?? command; onDidChangePythonSetting(CONDAPATH_SETTING_KEY, () => { Conda.condaPromise = new Map>(); @@ -278,6 +292,10 @@ export class Conda { return Conda.condaPromise.get(shellPath); } + public static setConda(condaPath: string): void { + Conda.condaPromise.set(undefined, Promise.resolve(new Conda(condaPath))); + } + /** * Locates the preferred "conda" utility on this system by considering user settings, * binaries on PATH, Python interpreters in the registry, and known install locations. @@ -287,7 +305,12 @@ export class Conda { private static async locate(shellPath?: string): Promise { traceVerbose(`Searching for conda.`); const home = getUserHomeDir(); - const customCondaPath = getPythonSetting(CONDAPATH_SETTING_KEY); + let customCondaPath: string | undefined = 'conda'; + try { + customCondaPath = getPythonSetting(CONDAPATH_SETTING_KEY); + } catch (ex) { + traceError(`Failed to get conda path setting, ${ex}`); + } const suffix = getOSType() === OSType.Windows ? 'Scripts\\conda.exe' : 'bin/conda'; // Produce a list of candidate binaries to be probed by exec'ing them. @@ -328,7 +351,7 @@ export class Conda { prefixes.push(home, path.join(localAppData, 'Continuum')); } } else { - prefixes.push('/usr/share', '/usr/local/share', '/opt'); + prefixes.push('/usr/share', '/usr/local/share', '/opt', '/opt/homebrew/bin'); if (home) { prefixes.push(home, path.join(home, 'opt')); } @@ -439,9 +462,19 @@ export class Conda { if (shellPath) { options.shell = shellPath; } - const result = await exec(command, ['info', '--json'], options); - traceVerbose(`${command} info --json: ${result.stdout}`); - return JSON.parse(result.stdout); + const resultPromise = exec(command, ['info', '--json'], options, this.useWorkerThreads); + // It has been observed that specifying a timeout is still not reliable to terminate the Conda process, see #27915. + // Hence explicitly continue execution after timeout has been reached. + const success = await Promise.race([ + resultPromise.then(() => true), + sleep(CONDA_GENERAL_TIMEOUT + 3000).then(() => false), + ]); + if (success) { + const result = await resultPromise; + traceVerbose(`${command} info --json: ${result.stdout}`); + return JSON.parse(result.stdout); + } + throw new Error(`Launching '${command} info --json' timed out`); } /** @@ -463,6 +496,15 @@ export class Conda { ); } + /** + * Retrieves list of directories where conda environments are stored. + */ + @cache(30_000, true, 10_000) + public async getEnvDirs(): Promise { + const info = await this.getInfo(); + return info.envs_dirs ?? []; + } + public async getName(prefix: string, info?: CondaInfo): Promise { info = info ?? (await this.getInfo(true)); if (info.root_prefix && arePathsSame(prefix, info.root_prefix)) { @@ -522,11 +564,8 @@ export class Conda { return undefined; } const args = []; - if (env.name) { - args.push('-n', env.name); - } else { - args.push('-p', env.prefix); - } + args.push('-p', env.prefix); + const python = [ forShellExecution ? this.shellCommand : this.command, 'run', @@ -540,6 +579,15 @@ export class Conda { return [...python, OUTPUT_MARKER_SCRIPT]; } + public async getListPythonPackagesArgs( + env: CondaEnvInfo, + forShellExecution?: boolean, + ): Promise { + const args = ['-p', env.prefix]; + + return [forShellExecution ? this.shellCommand : this.command, 'list', ...args]; + } + /** * Return the conda version. The version info is cached. */ @@ -583,3 +631,17 @@ export class Conda { return true; } } + +export function setCondaBinary(executable: string): void { + Conda.setConda(executable); +} + +export async function getCondaEnvDirs(): Promise { + const conda = await Conda.getConda(); + return conda?.getEnvDirs(); +} + +export function getCondaPathSetting(): string | undefined { + const config = getConfiguration('python'); + return config.get(CONDAPATH_SETTING_KEY, ''); +} diff --git a/src/client/pythonEnvironments/common/environmentManagers/condaService.ts b/src/client/pythonEnvironments/common/environmentManagers/condaService.ts index 049e19380d4e..0aa91bdbfb45 100644 --- a/src/client/pythonEnvironments/common/environmentManagers/condaService.ts +++ b/src/client/pythonEnvironments/common/environmentManagers/condaService.ts @@ -2,6 +2,7 @@ import { inject, injectable } from 'inversify'; import * as path from 'path'; import { SemVer } from 'semver'; import { IFileSystem, IPlatformService } from '../../../common/platform/types'; +import { traceVerbose } from '../../../logging'; import { cache } from '../../../common/utils/decorators'; import { ICondaService } from '../../../interpreter/contracts'; import { traceDecoratorVerbose } from '../../../logging'; @@ -23,12 +24,15 @@ export class CondaService implements ICondaService { interpreterPath?: string, envName?: string, ): Promise<{ path: string | undefined; type: 'local' | 'global' } | undefined> { + traceVerbose(`Getting activation script for interpreter ${interpreterPath}, env ${envName}`); const condaPath = await this.getCondaFileFromInterpreter(interpreterPath, envName); + traceVerbose(`Found conda path: ${condaPath}`); const activatePath = (condaPath ? path.join(path.dirname(condaPath), 'activate') : 'activate' ).fileToCommandArgumentForPythonExt(); // maybe global activate? + traceVerbose(`Using activate path: ${activatePath}`); // try to find the activate script in the global conda root prefix. if (this.platform.isLinux || this.platform.isMac) { @@ -41,6 +45,7 @@ export class CondaService implements ICondaService { .fileToCommandArgumentForPythonExt(); if (activatePath === globalActivatePath || !(await this.fileSystem.fileExists(activatePath))) { + traceVerbose(`Using global activate path: ${globalActivatePath}`); return { path: globalActivatePath, type: 'global', @@ -55,6 +60,7 @@ export class CondaService implements ICondaService { /** * Return the path to the "conda file". */ + // eslint-disable-next-line class-methods-use-this public async getCondaFile(forShellExecution?: boolean): Promise { return Conda.getConda().then((conda) => { @@ -142,8 +148,9 @@ export class CondaService implements ICondaService { * Return the info reported by the conda install. * The result is cached for 30s. */ - @cache(60_000) + // eslint-disable-next-line class-methods-use-this + @cache(60_000) public async getCondaInfo(): Promise { const conda = await Conda.getConda(); return conda?.getInfo(); diff --git a/src/client/pythonEnvironments/common/environmentManagers/hatch.ts b/src/client/pythonEnvironments/common/environmentManagers/hatch.ts new file mode 100644 index 000000000000..6d7a13ea1557 --- /dev/null +++ b/src/client/pythonEnvironments/common/environmentManagers/hatch.ts @@ -0,0 +1,116 @@ +import { isTestExecution } from '../../../common/constants'; +import { exec, pathExists } from '../externalDependencies'; +import { traceVerbose } from '../../../logging'; +import { cache } from '../../../common/utils/decorators'; +import { getOSType, OSType } from '../../../common/utils/platform'; + +/** Wraps the "Hatch" utility, and exposes its functionality. + */ +export class Hatch { + /** + * Locating Hatch binary can be expensive, since it potentially involves spawning or + * trying to spawn processes; so we only do it once per session. + */ + private static hatchPromise: Map> = new Map< + string, + Promise + >(); + + /** + * Creates a Hatch service corresponding to the corresponding "hatch" command. + * + * @param command - Command used to run hatch. This has the same meaning as the + * first argument of spawn() - i.e. it can be a full path, or just a binary name. + * @param cwd - The working directory to use as cwd when running hatch. + */ + constructor(public readonly command: string, private cwd: string) { + this.fixCwd(); + } + + /** + * Returns a Hatch instance corresponding to the binary which can be used to run commands for the cwd. + * + * Every directory is a valid Hatch project, so this should always return a Hatch instance. + */ + public static async getHatch(cwd: string): Promise { + if (Hatch.hatchPromise.get(cwd) === undefined || isTestExecution()) { + Hatch.hatchPromise.set(cwd, Hatch.locate(cwd)); + } + return Hatch.hatchPromise.get(cwd); + } + + private static async locate(cwd: string): Promise { + // First thing this method awaits on should be hatch command execution, + // hence perform all operations before that synchronously. + const hatchPath = 'hatch'; + traceVerbose(`Probing Hatch binary ${hatchPath}`); + const hatch = new Hatch(hatchPath, cwd); + const virtualenvs = await hatch.getEnvList(); + if (virtualenvs !== undefined) { + traceVerbose(`Found hatch binary ${hatchPath}`); + return hatch; + } + traceVerbose(`Failed to find Hatch binary ${hatchPath}`); + + // Didn't find anything. + traceVerbose(`No Hatch binary found`); + return undefined; + } + + /** + * Retrieves list of Python environments known to Hatch for this working directory. + * Returns `undefined` if we failed to spawn in some way. + * + * Corresponds to "hatch env show --json". Swallows errors if any. + */ + public async getEnvList(): Promise { + return this.getEnvListCached(this.cwd); + } + + /** + * Method created to facilitate caching. The caching decorator uses function arguments as cache key, + * so pass in cwd on which we need to cache. + */ + @cache(30_000, true, 10_000) + private async getEnvListCached(_cwd: string): Promise { + const envInfoOutput = await exec(this.command, ['env', 'show', '--json'], { + cwd: this.cwd, + throwOnStdErr: true, + }).catch(traceVerbose); + if (!envInfoOutput) { + return undefined; + } + const envPaths = await Promise.all( + Object.keys(JSON.parse(envInfoOutput.stdout)).map(async (name) => { + const envPathOutput = await exec(this.command, ['env', 'find', name], { + cwd: this.cwd, + throwOnStdErr: true, + }).catch(traceVerbose); + if (!envPathOutput) return undefined; + const dir = envPathOutput.stdout.trim(); + return (await pathExists(dir)) ? dir : undefined; + }), + ); + return envPaths.flatMap((r) => (r ? [r] : [])); + } + + /** + * Due to an upstream hatch issue on Windows https://github.com/pypa/hatch/issues/1350, + * 'hatch env find default' does not handle case-insensitive paths as cwd, which are valid on Windows. + * So we need to pass the case-exact path as cwd. + * It has been observed that only the drive letter in `cwd` is lowercased here. Unfortunately, + * there's no good way to get case of the drive letter correctly without using Win32 APIs: + * https://stackoverflow.com/questions/33086985/how-to-obtain-case-exact-path-of-a-file-in-node-js-on-windows + * So we do it manually. + */ + private fixCwd(): void { + if (getOSType() === OSType.Windows) { + if (/^[a-z]:/.test(this.cwd)) { + // Replace first character by the upper case version of the character. + const a = this.cwd.split(':'); + a[0] = a[0].toUpperCase(); + this.cwd = a.join(':'); + } + } + } +} diff --git a/src/client/pythonEnvironments/common/environmentManagers/pipenv.ts b/src/client/pythonEnvironments/common/environmentManagers/pipenv.ts index 80a185dc2991..c8651533ed4c 100644 --- a/src/client/pythonEnvironments/common/environmentManagers/pipenv.ts +++ b/src/client/pythonEnvironments/common/environmentManagers/pipenv.ts @@ -3,7 +3,7 @@ import * as path from 'path'; import { getEnvironmentVariable } from '../../../common/utils/platform'; -import { traceError } from '../../../logging'; +import { traceError, traceVerbose } from '../../../logging'; import { arePathsSame, normCasePath, pathExists, readFile } from '../externalDependencies'; function getSearchHeight() { @@ -70,7 +70,7 @@ async function getPipfileIfLocal(interpreterPath: string): Promise { +export async function getProjectDir(envFolder: string): Promise { // Global pipenv environments have a .project file with the absolute path to the project // See https://github.com/pypa/pipenv/blob/v2018.6.25/CHANGELOG.rst#features--improvements // This is the layout we expect @@ -85,7 +85,7 @@ async function getProjectDir(envFolder: string): Promise { } const projectDir = (await readFile(dotProjectFile)).trim(); if (!(await pathExists(projectDir))) { - traceError( + traceVerbose( `The .project file inside environment folder: ${envFolder} doesn't contain a valid path to the project`, ); return undefined; diff --git a/src/client/pythonEnvironments/common/environmentManagers/pixi.ts b/src/client/pythonEnvironments/common/environmentManagers/pixi.ts new file mode 100644 index 000000000000..6443e64f9ae8 --- /dev/null +++ b/src/client/pythonEnvironments/common/environmentManagers/pixi.ts @@ -0,0 +1,386 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import * as path from 'path'; +import { readJSON } from 'fs-extra'; +import which from 'which'; +import { getUserHomeDir, isWindows } from '../../../common/utils/platform'; +import { exec, getPythonSetting, onDidChangePythonSetting, pathExists } from '../externalDependencies'; +import { cache } from '../../../common/utils/decorators'; +import { traceVerbose, traceWarn } from '../../../logging'; +import { OUTPUT_MARKER_SCRIPT } from '../../../common/process/internal/scripts'; +import { IDisposableRegistry } from '../../../common/types'; +import { getWorkspaceFolderPaths } from '../../../common/vscodeApis/workspaceApis'; +import { isTestExecution } from '../../../common/constants'; +import { TerminalShellType } from '../../../common/terminal/types'; + +export const PIXITOOLPATH_SETTING_KEY = 'pixiToolPath'; + +// This type corresponds to the output of 'pixi info --json', and property +// names must be spelled exactly as they are in order to match the schema. +export type PixiInfo = { + platform: string; + virtual_packages: string[]; // eslint-disable-line camelcase + version: string; + cache_dir: string; // eslint-disable-line camelcase + cache_size?: number; // eslint-disable-line camelcase + auth_dir: string; // eslint-disable-line camelcase + + project_info?: PixiProjectInfo /* eslint-disable-line camelcase */; + + environments_info: /* eslint-disable-line camelcase */ { + name: string; + features: string[]; + solve_group: string; // eslint-disable-line camelcase + environment_size: number; // eslint-disable-line camelcase + dependencies: string[]; + tasks: string[]; + channels: string[]; + prefix: string; + }[]; +}; + +export type PixiProjectInfo = { + manifest_path: string; // eslint-disable-line camelcase + last_updated: string; // eslint-disable-line camelcase + pixi_folder_size?: number; // eslint-disable-line camelcase + version: string; +}; + +export type PixiEnvMetadata = { + manifest_path: string; // eslint-disable-line camelcase + pixi_version: string; // eslint-disable-line camelcase + environment_name: string; // eslint-disable-line camelcase +}; + +export async function isPixiEnvironment(interpreterPath: string): Promise { + const prefix = getPrefixFromInterpreterPath(interpreterPath); + return ( + pathExists(path.join(prefix, 'conda-meta/pixi')) || pathExists(path.join(prefix, 'conda-meta/pixi_env_prefix')) + ); +} + +/** + * Returns the path to the environment directory based on the interpreter path. + */ +export function getPrefixFromInterpreterPath(interpreterPath: string): string { + const interpreterDir = path.dirname(interpreterPath); + if (!interpreterDir.endsWith('bin') && !interpreterDir.endsWith('Scripts')) { + return interpreterDir; + } + return path.dirname(interpreterDir); +} + +async function findPixiOnPath(): Promise { + try { + return await which('pixi', { all: true }); + } catch { + // Ignore errors + } + return []; +} + +/** Wraps the "pixi" utility, and exposes its functionality. + */ +export class Pixi { + /** + * Creates a Pixi service corresponding to the corresponding "pixi" command. + * + * @param command - Command used to run pixi. This has the same meaning as the + * first argument of spawn() - i.e. it can be a full path, or just a binary name. + */ + constructor(public readonly command: string) {} + + /** + * Retrieves list of Python environments known to this pixi for the specified directory. + * + * Corresponds to "pixi info --json" and extracting the environments. Swallows errors if any. + */ + public async getEnvList(cwd: string): Promise { + const pixiInfo = await this.getPixiInfo(cwd); + // eslint-disable-next-line camelcase + return pixiInfo?.environments_info.map((env) => env.prefix); + } + + /** + * Method that runs `pixi info` and returns the result. The value is cached for "only" 1 second + * because the output changes if the project manifest is modified. + */ + @cache(1_000, true, 1_000) + public async getPixiInfo(cwd: string): Promise { + try { + const infoOutput = await exec(this.command, ['info', '--json'], { + cwd, + throwOnStdErr: false, + }); + + if (!infoOutput || !infoOutput.stdout) { + return undefined; + } + + const pixiInfo: PixiInfo = JSON.parse(infoOutput.stdout); + return pixiInfo; + } catch (error) { + traceWarn(`Failed to get pixi info for ${cwd}`, error); + return undefined; + } + } + + /** + * Returns the command line arguments to run `python` within a specific pixi environment. + * @param manifestPath The path to the manifest file used by pixi. + * @param envName The name of the environment in the pixi project + * @param isolatedFlag Whether to add `-I` to the python invocation. + * @returns A list of arguments that can be passed to exec. + */ + public getRunPythonArgs(manifestPath: string, envName?: string, isolatedFlag = false): string[] { + let python = [this.command, 'run', '--manifest-path', manifestPath]; + if (isNonDefaultPixiEnvironmentName(envName)) { + python = python.concat(['--environment', envName]); + } + + python.push('python'); + if (isolatedFlag) { + python.push('-I'); + } + return [...python, OUTPUT_MARKER_SCRIPT]; + } + + /** + * Starting from Pixi 0.24.0, each environment has a special file that records some information + * about which manifest created the environment. + * + * @param envDir The root directory (or prefix) of a conda environment + */ + + // eslint-disable-next-line class-methods-use-this + @cache(5_000, true, 10_000) + async getPixiEnvironmentMetadata(envDir: string): Promise { + const pixiPath = path.join(envDir, 'conda-meta/pixi'); + try { + const result: PixiEnvMetadata | undefined = await readJSON(pixiPath); + return result; + } catch (e) { + traceVerbose(`Failed to get pixi environment metadata for ${envDir}`, e); + } + return undefined; + } +} + +async function getPixiTool(): Promise { + let pixi = getPythonSetting(PIXITOOLPATH_SETTING_KEY); + + if (!pixi || pixi === 'pixi' || !(await pathExists(pixi))) { + pixi = undefined; + const paths = await findPixiOnPath(); + for (const p of paths) { + if (await pathExists(p)) { + pixi = p; + break; + } + } + } + + if (!pixi) { + // Check the default installation location + const home = getUserHomeDir(); + if (home) { + const pixiToolPath = path.join(home, '.pixi', 'bin', isWindows() ? 'pixi.exe' : 'pixi'); + if (await pathExists(pixiToolPath)) { + pixi = pixiToolPath; + } + } + } + + return pixi ? new Pixi(pixi) : undefined; +} + +/** + * Locating pixi binary can be expensive, since it potentially involves spawning or + * trying to spawn processes; so we only do it once per session. + */ +let _pixi: Promise | undefined; + +/** + * Returns a Pixi instance corresponding to the binary which can be used to run commands for the cwd. + * + * Pixi commands can be slow and so can be bottleneck to overall discovery time. So trigger command + * execution as soon as possible. To do that we need to ensure the operations before the command are + * performed synchronously. + */ +export function getPixi(): Promise { + if (_pixi === undefined || isTestExecution()) { + _pixi = getPixiTool(); + } + return _pixi; +} + +export type PixiEnvironmentInfo = { + interpreterPath: string; + pixi: Pixi; + pixiVersion: string; + manifestPath: string; + envName?: string; +}; + +function isPixiProjectDir(pixiProjectDir: string): boolean { + const paths = getWorkspaceFolderPaths().map((f) => path.normalize(f)); + const normalized = path.normalize(pixiProjectDir); + return paths.some((p) => p === normalized); +} + +/** + * Given the location of an interpreter, try to deduce information about the environment in which it + * resides. + * @param interpreterPath The full path to the interpreter. + * @param pixi Optionally a pixi instance. If this is not specified it will be located. + * @returns Information about the pixi environment. + */ +export async function getPixiEnvironmentFromInterpreter( + interpreterPath: string, +): Promise { + if (!interpreterPath) { + return undefined; + } + + const prefix = getPrefixFromInterpreterPath(interpreterPath); + const pixi = await getPixi(); + if (!pixi) { + traceVerbose(`could not find a pixi interpreter for the interpreter at ${interpreterPath}`); + return undefined; + } + + // Check if the environment has pixi metadata that we can source. + const metadata = await pixi.getPixiEnvironmentMetadata(prefix); + if (metadata !== undefined) { + return { + interpreterPath, + pixi, + pixiVersion: metadata.pixi_version, + manifestPath: metadata.manifest_path, + envName: metadata.environment_name, + }; + } + + // Otherwise, we'll have to try to deduce this information. + + // Usually the pixi environments are stored under `/.pixi/envs//`. So, + // we walk backwards to determine the project directory. + let envName: string | undefined; + let envsDir: string; + let dotPixiDir: string; + let pixiProjectDir: string; + let pixiInfo: PixiInfo | undefined; + + try { + envName = path.basename(prefix); + envsDir = path.dirname(prefix); + dotPixiDir = path.dirname(envsDir); + pixiProjectDir = path.dirname(dotPixiDir); + if (!isPixiProjectDir(pixiProjectDir)) { + traceVerbose(`could not determine the pixi project directory for the interpreter at ${interpreterPath}`); + return undefined; + } + + // Invoke pixi to get information about the pixi project + pixiInfo = await pixi.getPixiInfo(pixiProjectDir); + + if (!pixiInfo || !pixiInfo.project_info) { + traceWarn(`failed to determine pixi project information for the interpreter at ${interpreterPath}`); + return undefined; + } + + return { + interpreterPath, + pixi, + pixiVersion: pixiInfo.version, + manifestPath: pixiInfo.project_info.manifest_path, + envName, + }; + } catch (error) { + traceWarn('Error processing paths or getting Pixi Info:', error); + } + + return undefined; +} + +/** + * Returns true if the given environment name is *not* the default environment. + */ +export function isNonDefaultPixiEnvironmentName(envName?: string): envName is string { + return envName !== 'default'; +} + +export function registerPixiFeatures(disposables: IDisposableRegistry): void { + disposables.push( + onDidChangePythonSetting(PIXITOOLPATH_SETTING_KEY, () => { + _pixi = getPixiTool(); + }), + ); +} + +/** + * Returns the `pixi run` command + */ +export async function getRunPixiPythonCommand(pythonPath: string): Promise { + const pixiEnv = await getPixiEnvironmentFromInterpreter(pythonPath); + if (!pixiEnv) { + return undefined; + } + + const args = [ + pixiEnv.pixi.command.toCommandArgumentForPythonExt(), + 'run', + '--manifest-path', + pixiEnv.manifestPath.toCommandArgumentForPythonExt(), + ]; + if (isNonDefaultPixiEnvironmentName(pixiEnv.envName)) { + args.push('--environment'); + args.push(pixiEnv.envName.toCommandArgumentForPythonExt()); + } + + args.push('python'); + return args; +} + +export async function getPixiActivationCommands( + pythonPath: string, + _targetShell?: TerminalShellType, +): Promise { + const pixiEnv = await getPixiEnvironmentFromInterpreter(pythonPath); + if (!pixiEnv) { + return undefined; + } + + const args = [ + pixiEnv.pixi.command.toCommandArgumentForPythonExt(), + 'shell', + '--manifest-path', + pixiEnv.manifestPath.toCommandArgumentForPythonExt(), + ]; + if (isNonDefaultPixiEnvironmentName(pixiEnv.envName)) { + args.push('--environment'); + args.push(pixiEnv.envName.toCommandArgumentForPythonExt()); + } + + // const pixiTargetShell = shellTypeToPixiShell(targetShell); + // if (pixiTargetShell) { + // args.push('--shell'); + // args.push(pixiTargetShell); + // } + + // const shellHookOutput = await exec(pixiEnv.pixi.command, args, { + // throwOnStdErr: false, + // }).catch(traceError); + // if (!shellHookOutput) { + // return undefined; + // } + + // return splitLines(shellHookOutput.stdout, { + // removeEmptyEntries: true, + // trim: true, + // }); + return [args.join(' ')]; +} diff --git a/src/client/pythonEnvironments/common/environmentManagers/poetry.ts b/src/client/pythonEnvironments/common/environmentManagers/poetry.ts index 48199b5bdc8f..5e5fa2416208 100644 --- a/src/client/pythonEnvironments/common/environmentManagers/poetry.ts +++ b/src/client/pythonEnvironments/common/environmentManagers/poetry.ts @@ -10,7 +10,7 @@ import { isParentPath, pathExists, pathExistsSync, - readFileSync, + readFile, shellExecute, } from '../externalDependencies'; import { getEnvironmentDirFromPath } from '../commonUtils'; @@ -63,7 +63,7 @@ async function isLocalPoetryEnvironment(interpreterPath: string): Promise { // Following check should be performed synchronously so we trigger poetry execution as soon as possible. - if (!hasValidPyprojectToml(cwd)) { + if (!(await hasValidPyprojectToml(cwd))) { // This check is not expensive and may change during a session, so we need not cache it. return undefined; } @@ -325,12 +325,12 @@ export async function isPoetryEnvironmentRelatedToFolder( * * @param folder Folder to look for pyproject.toml file in. */ -function hasValidPyprojectToml(folder: string): boolean { +async function hasValidPyprojectToml(folder: string): Promise { const pyprojectToml = path.join(folder, 'pyproject.toml'); if (!pathExistsSync(pyprojectToml)) { return false; } - const content = readFileSync(pyprojectToml); + const content = await readFile(pyprojectToml); if (!content.includes('[tool.poetry]')) { return false; } diff --git a/src/client/pythonEnvironments/common/environmentManagers/pyenv.ts b/src/client/pythonEnvironments/common/environmentManagers/pyenv.ts index 229df8970513..8556e6f19f90 100644 --- a/src/client/pythonEnvironments/common/environmentManagers/pyenv.ts +++ b/src/client/pythonEnvironments/common/environmentManagers/pyenv.ts @@ -1,6 +1,7 @@ import * as path from 'path'; import { getEnvironmentVariable, getOSType, getUserHomeDir, OSType } from '../../../common/utils/platform'; -import { arePathsSame, isParentPath, pathExists } from '../externalDependencies'; +import { arePathsSame, isParentPath, pathExists, shellExecute } from '../externalDependencies'; +import { traceVerbose } from '../../../logging'; export function getPyenvDir(): string { // Check if the pyenv environment variables exist: PYENV on Windows, PYENV_ROOT on Unix. @@ -20,6 +21,36 @@ export function getPyenvDir(): string { return pyenvDir; } +let pyenvBinary: string | undefined; + +export function setPyEnvBinary(pyenvBin: string): void { + pyenvBinary = pyenvBin; +} + +async function getPyenvBinary(): Promise { + if (pyenvBinary && (await pathExists(pyenvBinary))) { + return pyenvBinary; + } + + const pyenvDir = getPyenvDir(); + const pyenvBin = path.join(pyenvDir, 'bin', 'pyenv'); + if (await pathExists(pyenvBin)) { + return pyenvBin; + } + return 'pyenv'; +} + +export async function getActivePyenvForDirectory(cwd: string): Promise { + const pyenvBin = await getPyenvBinary(); + try { + const pyenvInterpreterPath = await shellExecute(`${pyenvBin} which python`, { cwd }); + return pyenvInterpreterPath.stdout.trim(); + } catch (ex) { + traceVerbose(ex); + return undefined; + } +} + export function getPyenvVersionsDir(): string { return path.join(getPyenvDir(), 'versions'); } diff --git a/src/client/pythonEnvironments/common/environmentManagers/simplevirtualenvs.ts b/src/client/pythonEnvironments/common/environmentManagers/simplevirtualenvs.ts index 78a018138e2b..0ad24252f341 100644 --- a/src/client/pythonEnvironments/common/environmentManagers/simplevirtualenvs.ts +++ b/src/client/pythonEnvironments/common/environmentManagers/simplevirtualenvs.ts @@ -1,8 +1,8 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -import * as fsapi from 'fs-extra'; import * as path from 'path'; +import * as fsapi from '../../../common/platform/fs-paths'; import '../../../common/extensions'; import { splitLines } from '../../../common/stringUtils'; import { getEnvironmentVariable, getOSType, getUserHomeDir, OSType } from '../../../common/utils/platform'; diff --git a/src/client/pythonEnvironments/common/externalDependencies.ts b/src/client/pythonEnvironments/common/externalDependencies.ts index 54f614ebdd49..b0922f8bab06 100644 --- a/src/client/pythonEnvironments/common/externalDependencies.ts +++ b/src/client/pythonEnvironments/common/externalDependencies.ts @@ -1,16 +1,16 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -import * as fsapi from 'fs-extra'; import * as path from 'path'; import * as vscode from 'vscode'; +import * as fsapi from '../../common/platform/fs-paths'; import { IWorkspaceService } from '../../common/application/types'; import { ExecutionResult, IProcessServiceFactory, ShellOptions, SpawnOptions } from '../../common/process/types'; -import { IDisposable, IConfigurationService } from '../../common/types'; +import { IDisposable, IConfigurationService, IExperimentService } from '../../common/types'; import { chain, iterable } from '../../common/utils/async'; import { getOSType, OSType } from '../../common/utils/platform'; import { IServiceContainer } from '../../ioc/types'; -import { traceVerbose } from '../../logging'; +import { traceError, traceVerbose } from '../../logging'; let internalServiceContainer: IServiceContainer; export function initializeExternalDependencies(serviceContainer: IServiceContainer): void { @@ -20,15 +20,28 @@ export function initializeExternalDependencies(serviceContainer: IServiceContain // processes export async function shellExecute(command: string, options: ShellOptions = {}): Promise> { + const useWorker = false; const service = await internalServiceContainer.get(IProcessServiceFactory).create(); + options = { ...options, useWorker }; return service.shellExec(command, options); } -export async function exec(file: string, args: string[], options: SpawnOptions = {}): Promise> { +export async function exec( + file: string, + args: string[], + options: SpawnOptions = {}, + useWorker = false, +): Promise> { const service = await internalServiceContainer.get(IProcessServiceFactory).create(); + options = { ...options, useWorker }; return service.exec(file, args, options); } +export function inExperiment(experimentName: string): boolean { + const service = internalServiceContainer.get(IExperimentService); + return service.inExperimentSync(experimentName); +} + // Workspace export function isVirtualWorkspace(): boolean { @@ -54,9 +67,6 @@ export function readFileSync(filePath: string): string { return fsapi.readFileSync(filePath, 'utf-8'); } -// eslint-disable-next-line global-require -export const untildify: (value: string) => string = require('untildify'); - /** * Returns true if given file path exists within the given parent directory, false otherwise. * @param filePath File path to check for @@ -93,16 +103,21 @@ export function arePathsSame(path1: string, path2: string): boolean { return normCasePath(path1) === normCasePath(path2); } -export async function resolveSymbolicLink(absPath: string, stats?: fsapi.Stats): Promise { +export async function resolveSymbolicLink(absPath: string, stats?: fsapi.Stats, count?: number): Promise { stats = stats ?? (await fsapi.lstat(absPath)); if (stats.isSymbolicLink()) { + if (count && count > 5) { + traceError(`Detected a potential symbolic link loop at ${absPath}, terminating resolution.`); + return absPath; + } const link = await fsapi.readlink(absPath); // Result from readlink is not guaranteed to be an absolute path. For eg. on Mac it resolves // /usr/local/bin/python3.9 -> ../../../Library/Frameworks/Python.framework/Versions/3.9/bin/python3.9 // // The resultant path is reported relative to the symlink directory we resolve. Convert that to absolute path. const absLinkPath = path.isAbsolute(link) ? link : path.resolve(path.dirname(absPath), link); - return resolveSymbolicLink(absLinkPath); + count = count ? count + 1 : 1; + return resolveSymbolicLink(absLinkPath, undefined, count); } return absPath; } @@ -143,7 +158,7 @@ export async function* getSubDirs( root: string, options?: { resolveSymlinks?: boolean }, ): AsyncIterableIterator { - const dirContents = await fsapi.promises.readdir(root, { withFileTypes: true }); + const dirContents = await fsapi.readdir(root, { withFileTypes: true }); const generators = dirContents.map((item) => { async function* generator() { const fullPath = path.join(root, item.name); @@ -170,8 +185,9 @@ export async function* getSubDirs( * Returns the value for setting `python.`. * @param name The name of the setting. */ -export function getPythonSetting(name: string): T | undefined { - const settings = internalServiceContainer.get(IConfigurationService).getSettings(); +export function getPythonSetting(name: string, root?: string): T | undefined { + const resource = root ? vscode.Uri.file(root) : undefined; + const settings = internalServiceContainer.get(IConfigurationService).getSettings(resource); // eslint-disable-next-line @typescript-eslint/no-explicit-any return (settings as any)[name]; } @@ -181,9 +197,10 @@ export function getPythonSetting(name: string): T | undefined { * @param name The name of the setting. * @param callback The listener function to be called when the setting changes. */ -export function onDidChangePythonSetting(name: string, callback: () => void): IDisposable { +export function onDidChangePythonSetting(name: string, callback: () => void, root?: string): IDisposable { return vscode.workspace.onDidChangeConfiguration((event: vscode.ConfigurationChangeEvent) => { - if (event.affectsConfiguration(`python.${name}`)) { + const scope = root ? vscode.Uri.file(root) : undefined; + if (event.affectsConfiguration(`python.${name}`, scope)) { callback(); } }); diff --git a/src/client/pythonEnvironments/common/posixUtils.ts b/src/client/pythonEnvironments/common/posixUtils.ts index cd8f62bf9a08..8149706a5707 100644 --- a/src/client/pythonEnvironments/common/posixUtils.ts +++ b/src/client/pythonEnvironments/common/posixUtils.ts @@ -2,12 +2,12 @@ // Licensed under the MIT License. import * as fs from 'fs'; -import * as fsapi from 'fs-extra'; import * as path from 'path'; import { uniq } from 'lodash'; +import * as fsapi from '../../common/platform/fs-paths'; import { getSearchPathEntries } from '../../common/utils/exec'; import { resolveSymbolicLink } from './externalDependencies'; -import { traceError, traceInfo } from '../../logging'; +import { traceError, traceInfo, traceVerbose, traceWarn } from '../../logging'; /** * Determine if the given filename looks like the simplest Python executable. @@ -117,12 +117,16 @@ function pickShortestPath(pythonPaths: string[]) { export async function getPythonBinFromPosixPaths(searchDirs: string[]): Promise { const binToLinkMap = new Map(); for (const searchDir of searchDirs) { - const paths = await findPythonBinariesInDir(searchDir); + const paths = await findPythonBinariesInDir(searchDir).catch((ex) => { + traceWarn('Looking for python binaries within', searchDir, 'failed with', ex); + return []; + }); for (const filepath of paths) { // Ensure that we have a collection of unique global binaries by // resolving all symlinks to the target binaries. try { + traceVerbose(`Attempting to resolve symbolic link: ${filepath}`); const resolvedBin = await resolveSymbolicLink(filepath); if (binToLinkMap.has(resolvedBin)) { binToLinkMap.get(resolvedBin)?.push(filepath); diff --git a/src/client/pythonEnvironments/common/pythonBinariesWatcher.ts b/src/client/pythonEnvironments/common/pythonBinariesWatcher.ts index b3c5fba96cb0..efc7d56409c8 100644 --- a/src/client/pythonEnvironments/common/pythonBinariesWatcher.ts +++ b/src/client/pythonEnvironments/common/pythonBinariesWatcher.ts @@ -28,7 +28,7 @@ export function watchLocationForPythonBinaries( const [baseGlob] = resolvedGlob.split('/').slice(-1); function callbackClosure(type: FileChangeType, e: string) { traceVerbose('Received event', type, JSON.stringify(e), 'for baseglob', baseGlob); - const isMatch = minimatch(path.basename(e), baseGlob, { nocase: getOSType() === OSType.Windows }); + const isMatch = minimatch.default(path.basename(e), baseGlob, { nocase: getOSType() === OSType.Windows }); if (!isMatch) { // When deleting the file for some reason path to all directories leading up to python are reported // Skip those events @@ -39,6 +39,7 @@ export function watchLocationForPythonBinaries( return watchLocationForPattern(baseDir, resolvedGlob, callbackClosure); } +// eslint-disable-next-line no-shadow export enum PythonEnvStructure { Standard = 'standard', Flat = 'flat', diff --git a/src/client/pythonEnvironments/common/registryKeys.worker.ts b/src/client/pythonEnvironments/common/registryKeys.worker.ts new file mode 100644 index 000000000000..05996d057f11 --- /dev/null +++ b/src/client/pythonEnvironments/common/registryKeys.worker.ts @@ -0,0 +1,24 @@ +import { Registry } from 'winreg'; +import { parentPort, workerData } from 'worker_threads'; +import { IRegistryKey } from './windowsRegistry'; + +const WinReg = require('winreg'); + +const regKey = new WinReg(workerData); + +function copyRegistryKeys(keys: IRegistryKey[]): IRegistryKey[] { + // Use the map function to create a new array with copies of the specified properties. + return keys.map((key) => ({ + hive: key.hive, + arch: key.arch, + key: key.key, + })); +} + +regKey.keys((err: Error, res: Registry[]) => { + if (!parentPort) { + throw new Error('Not in a worker thread'); + } + const messageRes = copyRegistryKeys(res); + parentPort.postMessage({ err, res: messageRes }); +}); diff --git a/src/client/pythonEnvironments/common/registryValues.worker.ts b/src/client/pythonEnvironments/common/registryValues.worker.ts new file mode 100644 index 000000000000..eaef7cbd58a7 --- /dev/null +++ b/src/client/pythonEnvironments/common/registryValues.worker.ts @@ -0,0 +1,27 @@ +import { RegistryItem } from 'winreg'; +import { parentPort, workerData } from 'worker_threads'; +import { IRegistryValue } from './windowsRegistry'; + +const WinReg = require('winreg'); + +const regKey = new WinReg(workerData); + +function copyRegistryValues(values: IRegistryValue[]): IRegistryValue[] { + // Use the map function to create a new array with copies of the specified properties. + return values.map((value) => ({ + hive: value.hive, + arch: value.arch, + key: value.key, + name: value.name, + type: value.type, + value: value.value, + })); +} + +regKey.values((err: Error, res: RegistryItem[]) => { + if (!parentPort) { + throw new Error('Not in a worker thread'); + } + const messageRes = copyRegistryValues(res); + parentPort.postMessage({ err, res: messageRes }); +}); diff --git a/src/client/pythonEnvironments/common/windowsRegistry.ts b/src/client/pythonEnvironments/common/windowsRegistry.ts index 30047e1c9907..801ef0c907b1 100644 --- a/src/client/pythonEnvironments/common/windowsRegistry.ts +++ b/src/client/pythonEnvironments/common/windowsRegistry.ts @@ -1,8 +1,11 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. import { HKCU, HKLM, Options, REG_SZ, Registry, RegistryItem } from 'winreg'; +import * as path from 'path'; import { createDeferred } from '../../common/utils/async'; +import { executeWorkerFile } from '../../common/process/worker/main'; export { HKCU, HKLM, REG_SZ, Options }; @@ -22,30 +25,36 @@ export interface IRegistryValue { value: string; } -export async function readRegistryValues(options: Options): Promise { - // eslint-disable-next-line global-require - const WinReg = require('winreg'); - const regKey = new WinReg(options); - const deferred = createDeferred(); - regKey.values((err: Error, res: RegistryItem[]) => { - if (err) { - deferred.reject(err); - } - deferred.resolve(res); - }); - return deferred.promise; +export async function readRegistryValues(options: Options, useWorkerThreads: boolean): Promise { + if (!useWorkerThreads) { + // eslint-disable-next-line global-require + const WinReg = require('winreg'); + const regKey = new WinReg(options); + const deferred = createDeferred(); + regKey.values((err: Error, res: RegistryItem[]) => { + if (err) { + deferred.reject(err); + } + deferred.resolve(res); + }); + return deferred.promise; + } + return executeWorkerFile(path.join(__dirname, 'registryValues.worker.js'), options); } -export async function readRegistryKeys(options: Options): Promise { - // eslint-disable-next-line global-require - const WinReg = require('winreg'); - const regKey = new WinReg(options); - const deferred = createDeferred(); - regKey.keys((err: Error, res: Registry[]) => { - if (err) { - deferred.reject(err); - } - deferred.resolve(res); - }); - return deferred.promise; +export async function readRegistryKeys(options: Options, useWorkerThreads: boolean): Promise { + if (!useWorkerThreads) { + // eslint-disable-next-line global-require + const WinReg = require('winreg'); + const regKey = new WinReg(options); + const deferred = createDeferred(); + regKey.keys((err: Error, res: Registry[]) => { + if (err) { + deferred.reject(err); + } + deferred.resolve(res); + }); + return deferred.promise; + } + return executeWorkerFile(path.join(__dirname, 'registryKeys.worker.js'), options); } diff --git a/src/client/pythonEnvironments/common/windowsUtils.ts b/src/client/pythonEnvironments/common/windowsUtils.ts index a47025ceef6f..fe15f71522a5 100644 --- a/src/client/pythonEnvironments/common/windowsUtils.ts +++ b/src/client/pythonEnvironments/common/windowsUtils.ts @@ -54,13 +54,14 @@ export interface IRegistryInterpreterData { async function getInterpreterDataFromKey( { arch, hive, key }: IRegistryKey, distroOrgName: string, + useWorkerThreads: boolean, ): Promise { const result: IRegistryInterpreterData = { interpreterPath: '', distroOrgName, }; - const values: IRegistryValue[] = await readRegistryValues({ arch, hive, key }); + const values: IRegistryValue[] = await readRegistryValues({ arch, hive, key }, useWorkerThreads); for (const value of values) { switch (value.name) { case 'SysArchitecture': @@ -80,10 +81,10 @@ async function getInterpreterDataFromKey( } } - const subKeys: IRegistryKey[] = await readRegistryKeys({ arch, hive, key }); + const subKeys: IRegistryKey[] = await readRegistryKeys({ arch, hive, key }, useWorkerThreads); const subKey = subKeys.map((s) => s.key).find((s) => s.endsWith('InstallPath')); if (subKey) { - const subKeyValues: IRegistryValue[] = await readRegistryValues({ arch, hive, key: subKey }); + const subKeyValues: IRegistryValue[] = await readRegistryValues({ arch, hive, key: subKey }, useWorkerThreads); const value = subKeyValues.find((v) => v.name === 'ExecutablePath'); if (value) { result.interpreterPath = value.value; @@ -103,10 +104,13 @@ export async function getInterpreterDataFromRegistry( arch: string, hive: string, key: string, + useWorkerThreads: boolean, ): Promise { - const subKeys = await readRegistryKeys({ arch, hive, key }); + const subKeys = await readRegistryKeys({ arch, hive, key }, useWorkerThreads); const distroOrgName = key.substr(key.lastIndexOf('\\') + 1); - const allData = await Promise.all(subKeys.map((subKey) => getInterpreterDataFromKey(subKey, distroOrgName))); + const allData = await Promise.all( + subKeys.map((subKey) => getInterpreterDataFromKey(subKey, distroOrgName, useWorkerThreads)), + ); return (allData.filter((data) => data !== undefined) || []) as IRegistryInterpreterData[]; } @@ -130,7 +134,7 @@ export async function getRegistryInterpreters(): Promise { +async function getRegistryInterpretersImpl(useWorkerThreads = false): Promise { let registryData: IRegistryInterpreterData[] = []; for (const arch of ['x64', 'x86']) { @@ -138,13 +142,15 @@ async function getRegistryInterpretersImpl(): Promise k.key); + keys = (await readRegistryKeys({ arch, hive, key: root }, useWorkerThreads)).map((k) => k.key); } catch (ex) { traceError(`Failed to access Registry: ${arch}\\${hive}\\${root}`, ex); } for (const key of keys) { - registryData = registryData.concat(await getInterpreterDataFromRegistry(arch, hive, key)); + registryData = registryData.concat( + await getInterpreterDataFromRegistry(arch, hive, key, useWorkerThreads), + ); } } } diff --git a/src/client/pythonEnvironments/creation/common/commonUtils.ts b/src/client/pythonEnvironments/creation/common/commonUtils.ts index 0e303e70138e..8b6ffe1af450 100644 --- a/src/client/pythonEnvironments/creation/common/commonUtils.ts +++ b/src/client/pythonEnvironments/creation/common/commonUtils.ts @@ -1,9 +1,13 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. +import * as path from 'path'; +import { WorkspaceFolder } from 'vscode'; +import * as fs from '../../../common/platform/fs-paths'; import { Commands } from '../../../common/constants'; import { Common } from '../../../common/utils/localize'; import { executeCommand } from '../../../common/vscodeApis/commandApis'; import { showErrorMessage } from '../../../common/vscodeApis/windowApis'; +import { isWindows } from '../../../common/utils/platform'; export async function showErrorMessageWithLogs(message: string): Promise { const result = await showErrorMessage(message, Common.openOutputPanel, Common.selectPythonInterpreter); @@ -13,3 +17,26 @@ export async function showErrorMessageWithLogs(message: string): Promise { await executeCommand(Commands.Set_Interpreter); } } + +export function getVenvPath(workspaceFolder: WorkspaceFolder): string { + return path.join(workspaceFolder.uri.fsPath, '.venv'); +} + +export async function hasVenv(workspaceFolder: WorkspaceFolder): Promise { + return fs.pathExists(path.join(getVenvPath(workspaceFolder), 'pyvenv.cfg')); +} + +export function getVenvExecutable(workspaceFolder: WorkspaceFolder): string { + if (isWindows()) { + return path.join(getVenvPath(workspaceFolder), 'Scripts', 'python.exe'); + } + return path.join(getVenvPath(workspaceFolder), 'bin', 'python'); +} + +export function getPrefixCondaEnvPath(workspaceFolder: WorkspaceFolder): string { + return path.join(workspaceFolder.uri.fsPath, '.conda'); +} + +export async function hasPrefixCondaEnv(workspaceFolder: WorkspaceFolder): Promise { + return fs.pathExists(getPrefixCondaEnvPath(workspaceFolder)); +} diff --git a/src/client/pythonEnvironments/creation/common/createEnvTriggerUtils.ts b/src/client/pythonEnvironments/creation/common/createEnvTriggerUtils.ts new file mode 100644 index 000000000000..eccbf64a7866 --- /dev/null +++ b/src/client/pythonEnvironments/creation/common/createEnvTriggerUtils.ts @@ -0,0 +1,103 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import * as path from 'path'; +import { ConfigurationTarget, Uri, WorkspaceFolder } from 'vscode'; +import * as fsapi from '../../../common/platform/fs-paths'; +import { getPipRequirementsFiles } from '../provider/venvUtils'; +import { getExtension } from '../../../common/vscodeApis/extensionsApi'; +import { PVSC_EXTENSION_ID } from '../../../common/constants'; +import { PythonExtension } from '../../../api/types'; +import { traceVerbose } from '../../../logging'; +import { getConfiguration } from '../../../common/vscodeApis/workspaceApis'; +import { getWorkspaceStateValue } from '../../../common/persistentState'; + +export const CREATE_ENV_TRIGGER_SETTING_PART = 'createEnvironment.trigger'; +export const CREATE_ENV_TRIGGER_SETTING = `python.${CREATE_ENV_TRIGGER_SETTING_PART}`; + +export async function fileContainsInlineDependencies(_uri: Uri): Promise { + // This is a placeholder for the real implementation of inline dependencies support + // For now we don't detect anything. Once PEP-722/PEP-723 are accepted we can implement + // this properly. + return false; +} + +export async function hasRequirementFiles(workspace: WorkspaceFolder): Promise { + const files = await getPipRequirementsFiles(workspace); + const found = (files?.length ?? 0) > 0; + if (found) { + traceVerbose(`Found requirement files: ${workspace.uri.fsPath}`); + } + return found; +} + +export async function hasKnownFiles(workspace: WorkspaceFolder): Promise { + const filePaths: string[] = [ + 'poetry.lock', + 'conda.yaml', + 'environment.yaml', + 'conda.yml', + 'environment.yml', + 'Pipfile', + 'Pipfile.lock', + ].map((fileName) => path.join(workspace.uri.fsPath, fileName)); + const result = await Promise.all(filePaths.map((f) => fsapi.pathExists(f))); + const found = result.some((r) => r); + if (found) { + traceVerbose(`Found known files: ${workspace.uri.fsPath}`); + } + return found; +} + +export async function isGlobalPythonSelected(workspace: WorkspaceFolder): Promise { + const extension = getExtension(PVSC_EXTENSION_ID); + if (!extension) { + return false; + } + const extensionApi: PythonExtension = extension.exports as PythonExtension; + const interpreter = extensionApi.environments.getActiveEnvironmentPath(workspace.uri); + const details = await extensionApi.environments.resolveEnvironment(interpreter); + const isGlobal = details?.environment === undefined; + if (isGlobal) { + traceVerbose(`Selected python for [${workspace.uri.fsPath}] is [global] type: ${interpreter.path}`); + } + return isGlobal; +} + +/** + * Checks the setting `python.createEnvironment.trigger` to see if we should perform the checks + * to prompt to create an environment. + * Returns True if we should prompt to create an environment. + */ +export function shouldPromptToCreateEnv(): boolean { + const config = getConfiguration('python'); + if (config) { + const value = config.get(CREATE_ENV_TRIGGER_SETTING_PART, 'off'); + return value !== 'off'; + } + + return getWorkspaceStateValue(CREATE_ENV_TRIGGER_SETTING, 'off') !== 'off'; +} + +/** + * Sets `python.createEnvironment.trigger` to 'off' in the user settings. + */ +export function disableCreateEnvironmentTrigger(): void { + const config = getConfiguration('python'); + if (config) { + config.update('createEnvironment.trigger', 'off', ConfigurationTarget.Global); + } +} + +let _alreadyCreateEnvCriteriaCheck = false; +/** + * Run-once wrapper function for the workspace check to prompt to create an environment. + * @returns : True if we should prompt to c environment. + */ +export function isCreateEnvWorkspaceCheckNotRun(): boolean { + if (_alreadyCreateEnvCriteriaCheck) { + return false; + } + _alreadyCreateEnvCriteriaCheck = true; + return true; +} diff --git a/src/client/pythonEnvironments/creation/common/installCheckUtils.ts b/src/client/pythonEnvironments/creation/common/installCheckUtils.ts new file mode 100644 index 000000000000..2d8925cc05f6 --- /dev/null +++ b/src/client/pythonEnvironments/creation/common/installCheckUtils.ts @@ -0,0 +1,82 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License + +import { Diagnostic, DiagnosticSeverity, l10n, Range, TextDocument, Uri } from 'vscode'; +import { installedCheckScript } from '../../../common/process/internal/scripts'; +import { plainExec } from '../../../common/process/rawProcessApis'; +import { traceInfo, traceVerbose, traceError } from '../../../logging'; +import { getConfiguration } from '../../../common/vscodeApis/workspaceApis'; +import { IInterpreterService } from '../../../interpreter/contracts'; + +interface PackageDiagnostic { + package: string; + line: number; + character: number; + endLine: number; + endCharacter: number; + code: string; + severity: DiagnosticSeverity; +} + +export const INSTALL_CHECKER_SOURCE = 'Python-InstalledPackagesChecker'; + +function parseDiagnostics(data: string): Diagnostic[] { + let diagnostics: Diagnostic[] = []; + try { + const raw = JSON.parse(data) as PackageDiagnostic[]; + diagnostics = raw.map((item) => { + const d = new Diagnostic( + new Range(item.line, item.character, item.endLine, item.endCharacter), + l10n.t('Package `{0}` is not installed in the selected environment.', item.package), + item.severity, + ); + d.code = { value: item.code, target: Uri.parse(`https://pypi.org/p/${item.package}`) }; + d.source = INSTALL_CHECKER_SOURCE; + return d; + }); + } catch { + diagnostics = []; + } + return diagnostics; +} + +function getMissingPackageSeverity(doc: TextDocument): number { + const config = getConfiguration('python', doc.uri); + const severity: string = config.get('missingPackage.severity', 'Hint'); + if (severity === 'Error') { + return DiagnosticSeverity.Error; + } + if (severity === 'Warning') { + return DiagnosticSeverity.Warning; + } + if (severity === 'Information') { + return DiagnosticSeverity.Information; + } + return DiagnosticSeverity.Hint; +} + +export async function getInstalledPackagesDiagnostics( + interpreterService: IInterpreterService, + doc: TextDocument, +): Promise { + const interpreter = await interpreterService.getActiveInterpreter(doc.uri); + if (!interpreter) { + return []; + } + const scriptPath = installedCheckScript(); + try { + traceInfo('Running installed packages checker: ', interpreter, scriptPath, doc.uri.fsPath); + const envCopy = { ...process.env, VSCODE_MISSING_PGK_SEVERITY: `${getMissingPackageSeverity(doc)}` }; + const result = await plainExec(interpreter.path, [scriptPath, doc.uri.fsPath], { + env: envCopy, + }); + traceVerbose('Installed packages check result:\n', result.stdout); + if (result.stderr) { + traceError('Installed packages check error:\n', result.stderr); + } + return parseDiagnostics(result.stdout); + } catch (ex) { + traceError('Error while getting installed packages check result:\n', ex); + } + return []; +} diff --git a/src/client/pythonEnvironments/creation/common/workspaceSelection.ts b/src/client/pythonEnvironments/creation/common/workspaceSelection.ts index d145e38134d1..3ebab1c67fb4 100644 --- a/src/client/pythonEnvironments/creation/common/workspaceSelection.ts +++ b/src/client/pythonEnvironments/creation/common/workspaceSelection.ts @@ -1,9 +1,9 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -import * as fsapi from 'fs-extra'; import * as path from 'path'; import { CancellationToken, QuickPickItem, WorkspaceFolder } from 'vscode'; +import * as fsapi from '../../../common/platform/fs-paths'; import { MultiStepAction, showErrorMessage, showQuickPickWithBack } from '../../../common/vscodeApis/windowApis'; import { getWorkspaceFolders } from '../../../common/vscodeApis/workspaceApis'; import { Common, CreateEnv } from '../../../common/utils/localize'; @@ -32,6 +32,7 @@ async function getWorkspacesForQuickPick(workspaces: readonly WorkspaceFolder[]) export interface PickWorkspaceFolderOptions { allowMultiSelect?: boolean; token?: CancellationToken; + preSelectedWorkspace?: WorkspaceFolder; } export async function pickWorkspaceFolder( @@ -52,6 +53,15 @@ export async function pickWorkspaceFolder( return undefined; } + if (options?.preSelectedWorkspace) { + if (context === MultiStepAction.Back) { + // In this case there is no Quick Pick shown, should just go to previous + throw MultiStepAction.Back; + } + + return options.preSelectedWorkspace; + } + if (workspaces.length === 1) { if (context === MultiStepAction.Back) { // In this case there is no Quick Pick shown, should just go to previous @@ -68,6 +78,8 @@ export async function pickWorkspaceFolder( placeHolder: CreateEnv.pickWorkspacePlaceholder, ignoreFocusOut: true, canPickMany: options?.allowMultiSelect, + matchOnDescription: true, + matchOnDetail: true, }, options?.token, ); diff --git a/src/client/pythonEnvironments/creation/createEnvApi.ts b/src/client/pythonEnvironments/creation/createEnvApi.ts index 8858fe92de9d..899f57728804 100644 --- a/src/client/pythonEnvironments/creation/createEnvApi.ts +++ b/src/client/pythonEnvironments/creation/createEnvApi.ts @@ -1,14 +1,14 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -import { ConfigurationTarget, Disposable } from 'vscode'; +import { ConfigurationTarget, Disposable, QuickInputButtons } from 'vscode'; import { Commands } from '../../common/constants'; -import { IDisposableRegistry, IInterpreterPathService, IPathUtils } from '../../common/types'; +import { IDisposableRegistry, IPathUtils } from '../../common/types'; import { executeCommand, registerCommand } from '../../common/vscodeApis/commandApis'; -import { IInterpreterQuickPick } from '../../interpreter/configuration/types'; +import { IInterpreterQuickPick, IPythonPathUpdaterServiceManager } from '../../interpreter/configuration/types'; import { getCreationEvents, handleCreateEnvironmentCommand } from './createEnvironment'; import { condaCreationProvider } from './provider/condaCreationProvider'; -import { VenvCreationProvider } from './provider/venvCreationProvider'; +import { VenvCreationProvider, VenvCreationProviderId } from './provider/venvCreationProvider'; import { showInformationMessage } from '../../common/vscodeApis/windowApis'; import { CreateEnv } from '../../common/utils/localize'; import { @@ -20,6 +20,9 @@ import { } from './proposed.createEnvApis'; import { sendTelemetryEvent } from '../../telemetry'; import { EventName } from '../../telemetry/constants'; +import { CreateEnvironmentOptionsInternal } from './types'; +import { useEnvExtension } from '../../envExt/api.internal'; +import { PythonEnvironment } from '../../envExt/types'; class CreateEnvironmentProviders { private _createEnvProviders: CreateEnvironmentProvider[] = []; @@ -58,15 +61,52 @@ export const { onCreateEnvironmentStarted, onCreateEnvironmentExited, isCreating export function registerCreateEnvironmentFeatures( disposables: IDisposableRegistry, interpreterQuickPick: IInterpreterQuickPick, - interpreterPathService: IInterpreterPathService, + pythonPathUpdater: IPythonPathUpdaterServiceManager, pathUtils: IPathUtils, ): void { disposables.push( registerCommand( Commands.Create_Environment, - (options?: CreateEnvironmentOptions): Promise => { - const providers = _createEnvironmentProviders.getAll(); - return handleCreateEnvironmentCommand(providers, options); + async ( + options?: CreateEnvironmentOptions & CreateEnvironmentOptionsInternal, + ): Promise => { + if (useEnvExtension()) { + try { + sendTelemetryEvent(EventName.ENVIRONMENT_CREATING, undefined, { + environmentType: undefined, + pythonVersion: undefined, + }); + const result = await executeCommand( + 'python-envs.createAny', + options, + ); + if (result) { + const managerId = result.envId.managerId; + if (managerId === 'ms-python.python:venv') { + sendTelemetryEvent(EventName.ENVIRONMENT_CREATED, undefined, { + environmentType: 'venv', + reason: 'created', + }); + } + if (managerId === 'ms-python.python:conda') { + sendTelemetryEvent(EventName.ENVIRONMENT_CREATED, undefined, { + environmentType: 'conda', + reason: 'created', + }); + } + return { path: result.environmentPath.path }; + } + } catch (err) { + if (err === QuickInputButtons.Back) { + return { workspaceFolder: undefined, action: 'Back' }; + } + throw err; + } + } else { + const providers = _createEnvironmentProviders.getAll(); + return handleCreateEnvironmentCommand(providers, options); + } + return undefined; }, ), registerCommand( @@ -80,10 +120,11 @@ export function registerCreateEnvironmentFeatures( registerCreateEnvironmentProvider(condaCreationProvider()), onCreateEnvironmentExited(async (e: EnvironmentDidCreateEvent) => { if (e.path && e.options?.selectEnvironment) { - await interpreterPathService.update( - e.workspaceFolder?.uri, - ConfigurationTarget.WorkspaceFolder, + await pythonPathUpdater.updatePythonPath( e.path, + ConfigurationTarget.WorkspaceFolder, + 'ui', + e.workspaceFolder?.uri, ); showInformationMessage(`${CreateEnv.informEnvCreation} ${pathUtils.getDisplayName(e.path)}`); } @@ -109,3 +150,11 @@ export function buildEnvironmentCreationApi(): ProposedCreateEnvironmentAPI { registerCreateEnvironmentProvider(provider), }; } + +export async function createVirtualEnvironment(options?: CreateEnvironmentOptions & CreateEnvironmentOptionsInternal) { + const provider = _createEnvironmentProviders.getAll().find((p) => p.id === VenvCreationProviderId); + if (!provider) { + return; + } + return handleCreateEnvironmentCommand([provider], { ...options, providerId: provider.id }); +} diff --git a/src/client/pythonEnvironments/creation/createEnvButtonContext.ts b/src/client/pythonEnvironments/creation/createEnvButtonContext.ts new file mode 100644 index 000000000000..4ce7d07ad69d --- /dev/null +++ b/src/client/pythonEnvironments/creation/createEnvButtonContext.ts @@ -0,0 +1,22 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { IDisposableRegistry } from '../../common/types'; +import { executeCommand } from '../../common/vscodeApis/commandApis'; +import { getConfiguration, onDidChangeConfiguration } from '../../common/vscodeApis/workspaceApis'; + +async function setShowCreateEnvButtonContextKey(): Promise { + const config = getConfiguration('python'); + const showCreateEnvButton = config.get('createEnvironment.contentButton', 'show') === 'show'; + await executeCommand('setContext', 'showCreateEnvButton', showCreateEnvButton); +} + +export function registerCreateEnvironmentButtonFeatures(disposables: IDisposableRegistry): void { + disposables.push( + onDidChangeConfiguration(async () => { + await setShowCreateEnvButtonContextKey(); + }), + ); + + setShowCreateEnvButtonContextKey(); +} diff --git a/src/client/pythonEnvironments/creation/createEnvironment.ts b/src/client/pythonEnvironments/creation/createEnvironment.ts index 4593ff1abf92..c7c4e84f445c 100644 --- a/src/client/pythonEnvironments/creation/createEnvironment.ts +++ b/src/client/pythonEnvironments/creation/createEnvironment.ts @@ -17,6 +17,7 @@ import { EnvironmentWillCreateEvent, EnvironmentDidCreateEvent, } from './proposed.createEnvApis'; +import { CreateEnvironmentOptionsInternal } from './types'; const onCreateEnvironmentStartedEvent = new EventEmitter(); const onCreateEnvironmentExitedEvent = new EventEmitter(); @@ -33,14 +34,12 @@ function fireStartedEvent(options?: CreateEnvironmentOptions): void { } function fireExitedEvent(result?: CreateEnvironmentResult, options?: CreateEnvironmentOptions, error?: Error): void { - onCreateEnvironmentExitedEvent.fire({ - options, - workspaceFolder: result?.workspaceFolder, - path: result?.path, - action: result?.action, - error: error || result?.error, - }); startedEventCount -= 1; + if (result) { + onCreateEnvironmentExitedEvent.fire({ options, ...result }); + } else if (error) { + onCreateEnvironmentExitedEvent.fire({ options, error }); + } } export function getCreationEvents(): { @@ -57,7 +56,7 @@ export function getCreationEvents(): { async function createEnvironment( provider: CreateEnvironmentProvider, - options: CreateEnvironmentOptions, + options: CreateEnvironmentOptions & CreateEnvironmentOptionsInternal, ): Promise { let result: CreateEnvironmentResult | undefined; let err: Error | undefined; @@ -85,7 +84,7 @@ interface CreateEnvironmentProviderQuickPickItem extends QuickPickItem { async function showCreateEnvironmentQuickPick( providers: readonly CreateEnvironmentProvider[], - options?: CreateEnvironmentOptions, + options?: CreateEnvironmentOptions & CreateEnvironmentOptionsInternal, ): Promise { const items: CreateEnvironmentProviderQuickPickItem[] = providers.map((p) => ({ label: p.name, @@ -93,6 +92,13 @@ async function showCreateEnvironmentQuickPick( id: p.id, })); + if (options?.providerId) { + const provider = providers.find((p) => p.id === options.providerId); + if (provider) { + return provider; + } + } + let selectedItem: CreateEnvironmentProviderQuickPickItem | CreateEnvironmentProviderQuickPickItem[] | undefined; if (options?.showBackButton) { @@ -121,7 +127,9 @@ async function showCreateEnvironmentQuickPick( return undefined; } -function getOptionsWithDefaults(options?: CreateEnvironmentOptions): CreateEnvironmentOptions { +function getOptionsWithDefaults( + options?: CreateEnvironmentOptions & CreateEnvironmentOptionsInternal, +): CreateEnvironmentOptions & CreateEnvironmentOptionsInternal { return { installPackages: true, ignoreSourceControl: true, @@ -133,7 +141,7 @@ function getOptionsWithDefaults(options?: CreateEnvironmentOptions): CreateEnvir export async function handleCreateEnvironmentCommand( providers: readonly CreateEnvironmentProvider[], - options?: CreateEnvironmentOptions, + options?: CreateEnvironmentOptions & CreateEnvironmentOptionsInternal, ): Promise { const optionsWithDefaults = getOptionsWithDefaults(options); let selectedProvider: CreateEnvironmentProvider | undefined; @@ -195,5 +203,8 @@ export async function handleCreateEnvironmentCommand( } } - return result; + if (result) { + return Object.freeze(result); + } + return undefined; } diff --git a/src/client/pythonEnvironments/creation/createEnvironmentTrigger.ts b/src/client/pythonEnvironments/creation/createEnvironmentTrigger.ts new file mode 100644 index 000000000000..5119290a0c2d --- /dev/null +++ b/src/client/pythonEnvironments/creation/createEnvironmentTrigger.ts @@ -0,0 +1,156 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { Disposable, Uri, WorkspaceFolder } from 'vscode'; +import { + fileContainsInlineDependencies, + hasKnownFiles, + hasRequirementFiles, + isGlobalPythonSelected, + shouldPromptToCreateEnv, + isCreateEnvWorkspaceCheckNotRun, + disableCreateEnvironmentTrigger, +} from './common/createEnvTriggerUtils'; +import { getWorkspaceFolder } from '../../common/vscodeApis/workspaceApis'; +import { traceError, traceInfo, traceVerbose } from '../../logging'; +import { hasPrefixCondaEnv, hasVenv } from './common/commonUtils'; +import { showInformationMessage } from '../../common/vscodeApis/windowApis'; +import { Common, CreateEnv } from '../../common/utils/localize'; +import { executeCommand, registerCommand } from '../../common/vscodeApis/commandApis'; +import { Commands } from '../../common/constants'; +import { Resource } from '../../common/types'; +import { sendTelemetryEvent } from '../../telemetry'; +import { EventName } from '../../telemetry/constants'; + +export enum CreateEnvironmentCheckKind { + /** + * Checks if environment creation is needed based on file location and content. + */ + File = 'file', + + /** + * Checks if environment creation is needed based on workspace contents. + */ + Workspace = 'workspace', +} + +export interface CreateEnvironmentTriggerOptions { + force?: boolean; +} + +async function createEnvironmentCheckForWorkspace(uri: Uri): Promise { + const workspace = getWorkspaceFolder(uri); + if (!workspace) { + traceInfo(`CreateEnv Trigger - Workspace not found for ${uri.fsPath}`); + return; + } + + const missingRequirements = async (workspaceFolder: WorkspaceFolder) => + !(await hasRequirementFiles(workspaceFolder)); + + const isNonGlobalPythonSelected = async (workspaceFolder: WorkspaceFolder) => + !(await isGlobalPythonSelected(workspaceFolder)); + + // Skip showing the Create Environment prompt if one of the following is True: + // 1. The workspace already has a ".venv" or ".conda" env + // 2. The workspace does NOT have "requirements.txt" or "requirements/*.txt" files + // 3. The workspace has known files for other environment types like environment.yml, conda.yml, poetry.lock, etc. + // 4. The selected python is NOT classified as a global python interpreter + const skipPrompt: boolean = ( + await Promise.all([ + hasVenv(workspace), + hasPrefixCondaEnv(workspace), + missingRequirements(workspace), + hasKnownFiles(workspace), + isNonGlobalPythonSelected(workspace), + ]) + ).some((r) => r); + + if (skipPrompt) { + sendTelemetryEvent(EventName.ENVIRONMENT_CHECK_RESULT, undefined, { result: 'criteria-not-met' }); + traceInfo(`CreateEnv Trigger - Skipping for ${uri.fsPath}`); + return; + } + + sendTelemetryEvent(EventName.ENVIRONMENT_CHECK_RESULT, undefined, { result: 'criteria-met' }); + const selection = await showInformationMessage( + CreateEnv.Trigger.workspaceTriggerMessage, + CreateEnv.Trigger.createEnvironment, + Common.doNotShowAgain, + ); + + if (selection === CreateEnv.Trigger.createEnvironment) { + try { + await executeCommand(Commands.Create_Environment); + } catch (error) { + traceError('CreateEnv Trigger - Error while creating environment: ', error); + } + } else if (selection === Common.doNotShowAgain) { + disableCreateEnvironmentTrigger(); + } +} + +function runOnceWorkspaceCheck(uri: Uri, options: CreateEnvironmentTriggerOptions = {}): Promise { + if (isCreateEnvWorkspaceCheckNotRun() || options?.force) { + return createEnvironmentCheckForWorkspace(uri); + } + sendTelemetryEvent(EventName.ENVIRONMENT_CHECK_RESULT, undefined, { result: 'already-ran' }); + traceVerbose('CreateEnv Trigger - skipping this because it was already run'); + return Promise.resolve(); +} + +async function createEnvironmentCheckForFile(uri: Uri, options?: CreateEnvironmentTriggerOptions): Promise { + if (await fileContainsInlineDependencies(uri)) { + // TODO: Handle create environment for each file here. + // pending acceptance of PEP-722/PEP-723 + + // For now we do the same thing as for workspace. + await runOnceWorkspaceCheck(uri, options); + } + + // If the file does not have any inline dependencies, then we do the same thing + // as for workspace. + await runOnceWorkspaceCheck(uri, options); +} + +export async function triggerCreateEnvironmentCheck( + kind: CreateEnvironmentCheckKind, + uri: Resource, + options?: CreateEnvironmentTriggerOptions, +): Promise { + if (!uri) { + sendTelemetryEvent(EventName.ENVIRONMENT_CHECK_RESULT, undefined, { result: 'no-uri' }); + traceVerbose('CreateEnv Trigger - Skipping No URI provided'); + return; + } + + if (shouldPromptToCreateEnv()) { + if (kind === CreateEnvironmentCheckKind.File) { + await createEnvironmentCheckForFile(uri, options); + } else { + await runOnceWorkspaceCheck(uri, options); + } + } else { + sendTelemetryEvent(EventName.ENVIRONMENT_CHECK_RESULT, undefined, { result: 'turned-off' }); + traceVerbose('CreateEnv Trigger - turned off in settings'); + } +} + +export function triggerCreateEnvironmentCheckNonBlocking( + kind: CreateEnvironmentCheckKind, + uri: Resource, + options?: CreateEnvironmentTriggerOptions, +): void { + // The Event loop for Node.js runs functions with setTimeout() with lower priority than setImmediate. + // This is done to intentionally avoid blocking anything that the user wants to do. + setTimeout(() => triggerCreateEnvironmentCheck(kind, uri, options).ignoreErrors(), 0); +} + +export function registerCreateEnvironmentTriggers(disposables: Disposable[]): void { + disposables.push( + registerCommand(Commands.Create_Environment_Check, (file: Resource) => { + sendTelemetryEvent(EventName.ENVIRONMENT_CHECK_TRIGGER, undefined, { trigger: 'as-command' }); + triggerCreateEnvironmentCheckNonBlocking(CreateEnvironmentCheckKind.File, file, { force: true }); + }), + ); +} diff --git a/src/client/pythonEnvironments/creation/globalPipInTerminalTrigger.ts b/src/client/pythonEnvironments/creation/globalPipInTerminalTrigger.ts new file mode 100644 index 000000000000..76a55bea19a0 --- /dev/null +++ b/src/client/pythonEnvironments/creation/globalPipInTerminalTrigger.ts @@ -0,0 +1,85 @@ +import { Disposable, TerminalShellExecutionStartEvent } from 'vscode'; +import { + disableCreateEnvironmentTrigger, + isGlobalPythonSelected, + shouldPromptToCreateEnv, +} from './common/createEnvTriggerUtils'; +import { getWorkspaceFolder, getWorkspaceFolders } from '../../common/vscodeApis/workspaceApis'; +import { Common, CreateEnv } from '../../common/utils/localize'; +import { traceError, traceInfo } from '../../logging'; +import { executeCommand } from '../../common/vscodeApis/commandApis'; +import { Commands, PVSC_EXTENSION_ID } from '../../common/constants'; +import { CreateEnvironmentResult } from './proposed.createEnvApis'; +import { onDidStartTerminalShellExecution, showWarningMessage } from '../../common/vscodeApis/windowApis'; +import { sendTelemetryEvent } from '../../telemetry'; +import { EventName } from '../../telemetry/constants'; + +function checkCommand(command: string): boolean { + const lower = command.toLowerCase(); + return ( + lower.startsWith('pip install') || + lower.startsWith('pip3 install') || + lower.startsWith('python -m pip install') || + lower.startsWith('python3 -m pip install') + ); +} + +export function registerTriggerForPipInTerminal(disposables: Disposable[]): void { + if (!shouldPromptToCreateEnv()) { + return; + } + + const folders = getWorkspaceFolders(); + if (!folders || folders.length === 0) { + return; + } + + const createEnvironmentTriggered: Map = new Map(); + folders.forEach((workspaceFolder) => { + createEnvironmentTriggered.set(workspaceFolder.uri.fsPath, false); + }); + + disposables.push( + onDidStartTerminalShellExecution(async (e: TerminalShellExecutionStartEvent) => { + const workspaceFolder = getWorkspaceFolder(e.shellIntegration.cwd); + if ( + workspaceFolder && + !createEnvironmentTriggered.get(workspaceFolder.uri.fsPath) && + (await isGlobalPythonSelected(workspaceFolder)) + ) { + if (e.execution.commandLine.isTrusted && checkCommand(e.execution.commandLine.value)) { + createEnvironmentTriggered.set(workspaceFolder.uri.fsPath, true); + sendTelemetryEvent(EventName.ENVIRONMENT_TERMINAL_GLOBAL_PIP); + const selection = await showWarningMessage( + CreateEnv.Trigger.globalPipInstallTriggerMessage, + CreateEnv.Trigger.createEnvironment, + Common.doNotShowAgain, + ); + if (selection === CreateEnv.Trigger.createEnvironment) { + try { + const result: CreateEnvironmentResult = await executeCommand(Commands.Create_Environment, { + workspaceFolder, + providerId: `${PVSC_EXTENSION_ID}:venv`, + }); + if (result.path) { + traceInfo('CreateEnv Trigger - Environment created: ', result.path); + traceInfo( + `CreateEnv Trigger - Running: ${ + result.path + } -m ${e.execution.commandLine.value.trim()}`, + ); + e.shellIntegration.executeCommand( + `${result.path} -m ${e.execution.commandLine.value}`.trim(), + ); + } + } catch (error) { + traceError('CreateEnv Trigger - Error while creating environment: ', error); + } + } else if (selection === Common.doNotShowAgain) { + disableCreateEnvironmentTrigger(); + } + } + } + }), + ); +} diff --git a/src/client/pythonEnvironments/creation/installedPackagesDiagnostic.ts b/src/client/pythonEnvironments/creation/installedPackagesDiagnostic.ts new file mode 100644 index 000000000000..0b55e1ec5ce1 --- /dev/null +++ b/src/client/pythonEnvironments/creation/installedPackagesDiagnostic.ts @@ -0,0 +1,88 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License + +import { Diagnostic, DiagnosticCollection, TextDocument, Uri } from 'vscode'; +import { IDisposableRegistry } from '../../common/types'; +import { executeCommand } from '../../common/vscodeApis/commandApis'; +import { createDiagnosticCollection, onDidChangeDiagnostics } from '../../common/vscodeApis/languageApis'; +import { getActiveTextEditor, onDidChangeActiveTextEditor } from '../../common/vscodeApis/windowApis'; +import { + getOpenTextDocuments, + onDidCloseTextDocument, + onDidOpenTextDocument, + onDidSaveTextDocument, +} from '../../common/vscodeApis/workspaceApis'; +import { traceVerbose } from '../../logging'; +import { getInstalledPackagesDiagnostics, INSTALL_CHECKER_SOURCE } from './common/installCheckUtils'; +import { IInterpreterService } from '../../interpreter/contracts'; + +export const DEPS_NOT_INSTALLED_KEY = 'pythonDepsNotInstalled'; + +async function setContextForActiveEditor(diagnosticCollection: DiagnosticCollection): Promise { + const doc = getActiveTextEditor()?.document; + if (doc && (doc.languageId === 'pip-requirements' || doc.fileName.endsWith('pyproject.toml'))) { + const diagnostics = diagnosticCollection.get(doc.uri); + if (diagnostics && diagnostics.length > 0) { + traceVerbose(`Setting context for python dependencies not installed: ${doc.uri.fsPath}`); + await executeCommand('setContext', DEPS_NOT_INSTALLED_KEY, true); + return; + } + } + + // undefined here in the logs means no file was selected + await executeCommand('setContext', DEPS_NOT_INSTALLED_KEY, false); +} + +export function registerInstalledPackagesDiagnosticsProvider( + disposables: IDisposableRegistry, + interpreterService: IInterpreterService, +): void { + const diagnosticCollection = createDiagnosticCollection(INSTALL_CHECKER_SOURCE); + const updateDiagnostics = (uri: Uri, diagnostics: Diagnostic[]) => { + if (diagnostics.length > 0) { + diagnosticCollection.set(uri, diagnostics); + } else if (diagnosticCollection.has(uri)) { + diagnosticCollection.delete(uri); + } + }; + + disposables.push(diagnosticCollection); + disposables.push( + onDidOpenTextDocument(async (doc: TextDocument) => { + if (doc.languageId === 'pip-requirements' || doc.fileName.endsWith('pyproject.toml')) { + const diagnostics = await getInstalledPackagesDiagnostics(interpreterService, doc); + updateDiagnostics(doc.uri, diagnostics); + } + }), + onDidSaveTextDocument(async (doc: TextDocument) => { + if (doc.languageId === 'pip-requirements' || doc.fileName.endsWith('pyproject.toml')) { + const diagnostics = await getInstalledPackagesDiagnostics(interpreterService, doc); + updateDiagnostics(doc.uri, diagnostics); + } + }), + onDidCloseTextDocument((e: TextDocument) => { + updateDiagnostics(e.uri, []); + }), + onDidChangeDiagnostics(async () => { + await setContextForActiveEditor(diagnosticCollection); + }), + onDidChangeActiveTextEditor(async () => { + await setContextForActiveEditor(diagnosticCollection); + }), + interpreterService.onDidChangeInterpreter(() => { + getOpenTextDocuments().forEach(async (doc: TextDocument) => { + if (doc.languageId === 'pip-requirements' || doc.fileName.endsWith('pyproject.toml')) { + const diagnostics = await getInstalledPackagesDiagnostics(interpreterService, doc); + updateDiagnostics(doc.uri, diagnostics); + } + }); + }), + ); + + getOpenTextDocuments().forEach(async (doc: TextDocument) => { + if (doc.languageId === 'pip-requirements' || doc.fileName.endsWith('pyproject.toml')) { + const diagnostics = await getInstalledPackagesDiagnostics(interpreterService, doc); + updateDiagnostics(doc.uri, diagnostics); + } + }); +} diff --git a/src/client/pythonEnvironments/creation/proposed.createEnvApis.ts b/src/client/pythonEnvironments/creation/proposed.createEnvApis.ts index 52209a5a31d0..ea520fdd27e2 100644 --- a/src/client/pythonEnvironments/creation/proposed.createEnvApis.ts +++ b/src/client/pythonEnvironments/creation/proposed.createEnvApis.ts @@ -2,7 +2,7 @@ // Licensed under the MIT License import { Event, Disposable, WorkspaceFolder } from 'vscode'; -import { EnvironmentTools } from '../../apiTypes'; +import { EnvironmentTools } from '../../api/types'; export type CreateEnvironmentUserActions = 'Back' | 'Cancel'; export type EnvironmentProviderId = string; @@ -40,40 +40,83 @@ export interface EnvironmentWillCreateEvent { /** * Options used to create a Python environment. */ - options: CreateEnvironmentOptions | undefined; + readonly options: CreateEnvironmentOptions | undefined; } +export type CreateEnvironmentResult = + | { + /** + * Workspace folder associated with the environment. + */ + readonly workspaceFolder?: WorkspaceFolder; + + /** + * Path to the executable python in the environment + */ + readonly path: string; + + /** + * User action that resulted in exit from the create environment flow. + */ + readonly action?: CreateEnvironmentUserActions; + + /** + * Error if any occurred during environment creation. + */ + readonly error?: Error; + } + | { + /** + * Workspace folder associated with the environment. + */ + readonly workspaceFolder?: WorkspaceFolder; + + /** + * Path to the executable python in the environment + */ + readonly path?: string; + + /** + * User action that resulted in exit from the create environment flow. + */ + readonly action: CreateEnvironmentUserActions; + + /** + * Error if any occurred during environment creation. + */ + readonly error?: Error; + } + | { + /** + * Workspace folder associated with the environment. + */ + readonly workspaceFolder?: WorkspaceFolder; + + /** + * Path to the executable python in the environment + */ + readonly path?: string; + + /** + * User action that resulted in exit from the create environment flow. + */ + readonly action?: CreateEnvironmentUserActions; + + /** + * Error if any occurred during environment creation. + */ + readonly error: Error; + }; + /** * Params passed on `onDidCreateEnvironment` event handler. */ -export interface EnvironmentDidCreateEvent extends CreateEnvironmentResult { +export type EnvironmentDidCreateEvent = CreateEnvironmentResult & { /** * Options used to create the Python environment. */ - options: CreateEnvironmentOptions | undefined; -} - -export interface CreateEnvironmentResult { - /** - * Workspace folder associated with the environment. - */ - workspaceFolder: WorkspaceFolder | undefined; - - /** - * Path to the executable python in the environment - */ - path: string | undefined; - - /** - * User action that resulted in exit from the create environment flow. - */ - action: CreateEnvironmentUserActions | undefined; - - /** - * Error if any occurred during environment creation. - */ - error: Error | undefined; -} + readonly options: CreateEnvironmentOptions | undefined; +}; /** * Extensions that want to contribute their own environment creation can do that by registering an object @@ -120,14 +163,14 @@ export interface ProposedCreateEnvironmentAPI { * provider (including internal providers). This will also receive any options passed in * or defaults used to create environment. */ - onWillCreateEnvironment: Event; + readonly onWillCreateEnvironment: Event; /** * This API can be used to detect when the environment provider exits for any registered * provider (including internal providers). This will also receive created environment path, * any errors, or user actions taken from the provider. */ - onDidCreateEnvironment: Event; + readonly onDidCreateEnvironment: Event; /** * This API will show a QuickPick to select an environment provider from available list of diff --git a/src/client/pythonEnvironments/creation/provider/condaCreationProvider.ts b/src/client/pythonEnvironments/creation/provider/condaCreationProvider.ts index 39cd40afd41a..a7e4e9a21cd1 100644 --- a/src/client/pythonEnvironments/creation/provider/condaCreationProvider.ts +++ b/src/client/pythonEnvironments/creation/provider/condaCreationProvider.ts @@ -1,19 +1,26 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -import { CancellationToken, ProgressLocation, WorkspaceFolder } from 'vscode'; +import { CancellationToken, CancellationTokenSource, ProgressLocation, WorkspaceFolder } from 'vscode'; import * as path from 'path'; import { Commands, PVSC_EXTENSION_ID } from '../../../common/constants'; -import { traceError, traceLog } from '../../../logging'; +import { traceError, traceInfo, traceLog } from '../../../logging'; import { CreateEnvironmentProgress } from '../types'; import { pickWorkspaceFolder } from '../common/workspaceSelection'; import { execObservable } from '../../../common/process/rawProcessApis'; import { createDeferred } from '../../../common/utils/async'; -import { getEnvironmentVariable, getOSType, OSType } from '../../../common/utils/platform'; +import { getOSType, OSType } from '../../../common/utils/platform'; import { createCondaScript } from '../../../common/process/internal/scripts'; import { Common, CreateEnv } from '../../../common/utils/localize'; -import { getCondaBaseEnv, pickPythonVersion } from './condaUtils'; -import { showErrorMessageWithLogs } from '../common/commonUtils'; +import { + ExistingCondaAction, + deleteEnvironment, + getCondaBaseEnv, + getPathEnvVariableForConda, + pickExistingCondaAction, + pickPythonVersion, +} from './condaUtils'; +import { getPrefixCondaEnvPath, showErrorMessageWithLogs } from '../common/commonUtils'; import { MultiStepAction, MultiStepNode, withProgress } from '../../../common/vscodeApis/windowApis'; import { EventName } from '../../../telemetry/constants'; import { sendTelemetryEvent } from '../../../telemetry'; @@ -28,6 +35,8 @@ import { CreateEnvironmentResult, CreateEnvironmentProvider, } from '../proposed.createEnvApis'; +import { shouldDisplayEnvCreationProgress } from './hideEnvCreation'; +import { noop } from '../../../common/utils/misc'; function generateCommandArgs(version?: string, options?: CreateEnvironmentOptions): string[] { let addGitIgnore = true; @@ -77,28 +86,13 @@ async function createCondaEnv( args: string[], progress: CreateEnvironmentProgress, token?: CancellationToken, -): Promise { +): Promise { progress.report({ message: CreateEnv.Conda.creating, }); const deferred = createDeferred(); - let pathEnv = getEnvironmentVariable('PATH') || getEnvironmentVariable('Path') || ''; - if (getOSType() === OSType.Windows) { - // On windows `conda.bat` is used, which adds the following bin directories to PATH - // then launches `conda.exe` which is a stub to `python.exe -m conda`. Here, we are - // instead using the `python.exe` that ships with conda to run a python script that - // handles conda env creation and package installation. - // See conda issue: https://github.com/conda/conda/issues/11399 - const root = path.dirname(command); - const libPath1 = path.join(root, 'Library', 'bin'); - const libPath2 = path.join(root, 'Library', 'mingw-w64', 'bin'); - const libPath3 = path.join(root, 'Library', 'usr', 'bin'); - const libPath4 = path.join(root, 'bin'); - const libPath5 = path.join(root, 'Scripts'); - const libPath = [libPath1, libPath2, libPath3, libPath4, libPath5].join(path.delimiter); - pathEnv = `${libPath}${path.delimiter}${pathEnv}`; - } + const pathEnv = getPathEnvVariableForConda(command); traceLog('Running Conda Env creation script: ', [command, ...args]); const { proc, out, dispose } = execObservable(command, args, { mergeStdOutErr: true, @@ -114,7 +108,7 @@ async function createCondaEnv( out.subscribe( (value) => { const output = splitLines(value.out).join('\r\n'); - traceLog(output); + traceLog(output.trimEnd()); if (output.includes(CONDA_ENV_CREATED_MARKER) || output.includes(CONDA_ENV_EXISTING_MARKER)) { condaEnvPath = getCondaEnvFromOutput(output); } @@ -128,7 +122,9 @@ async function createCondaEnv( dispose(); if (proc?.exitCode !== 0) { traceError('Error while running venv creation script: ', progressAndTelemetry.getLastError()); - deferred.reject(progressAndTelemetry.getLastError()); + deferred.reject( + progressAndTelemetry.getLastError() || `Conda env creation failed with exitCode: ${proc?.exitCode}`, + ); } else { deferred.resolve(condaEnvPath); } @@ -174,39 +170,143 @@ async function createEnvironment(options?: CreateEnvironmentOptions): Promise { + if (workspace && context === MultiStepAction.Continue) { + try { + existingCondaAction = await pickExistingCondaAction(workspace); + return MultiStepAction.Continue; + } catch (ex) { + if (ex === MultiStepAction.Back || ex === MultiStepAction.Cancel) { + return ex; + } + throw ex; + } + } else if (context === MultiStepAction.Back) { + return MultiStepAction.Back; + } + return MultiStepAction.Continue; + }, + undefined, + ); + workspaceStep.next = existingEnvStep; + let version: string | undefined; const versionStep = new MultiStepNode( workspaceStep, - async () => { - try { - version = await pickPythonVersion(); - } catch (ex) { - if (ex === MultiStepAction.Back || ex === MultiStepAction.Cancel) { - return ex; + async (context) => { + if ( + existingCondaAction === ExistingCondaAction.Recreate || + existingCondaAction === ExistingCondaAction.Create + ) { + try { + version = await pickPythonVersion(); + } catch (ex) { + if (ex === MultiStepAction.Back || ex === MultiStepAction.Cancel) { + return ex; + } + throw ex; + } + if (version === undefined) { + traceError('Python version was not selected for creating conda environment.'); + return MultiStepAction.Cancel; + } + traceInfo(`Selected Python version ${version} for creating conda environment.`); + } else if (existingCondaAction === ExistingCondaAction.UseExisting) { + if (context === MultiStepAction.Back) { + return MultiStepAction.Back; } - throw ex; } - if (version === undefined) { - traceError('Python version was not selected for creating conda environment.'); - return MultiStepAction.Cancel; - } return MultiStepAction.Continue; }, undefined, ); - workspaceStep.next = versionStep; + existingEnvStep.next = versionStep; const action = await MultiStepNode.run(workspaceStep); if (action === MultiStepAction.Back || action === MultiStepAction.Cancel) { throw action; } + if (workspace) { + if (existingCondaAction === ExistingCondaAction.Recreate) { + sendTelemetryEvent(EventName.ENVIRONMENT_DELETE, undefined, { + environmentType: 'conda', + status: 'triggered', + }); + if (await deleteEnvironment(workspace, getExecutableCommand(conda))) { + sendTelemetryEvent(EventName.ENVIRONMENT_DELETE, undefined, { + environmentType: 'conda', + status: 'deleted', + }); + } else { + sendTelemetryEvent(EventName.ENVIRONMENT_DELETE, undefined, { + environmentType: 'conda', + status: 'failed', + }); + throw MultiStepAction.Cancel; + } + } else if (existingCondaAction === ExistingCondaAction.UseExisting) { + sendTelemetryEvent(EventName.ENVIRONMENT_REUSE, undefined, { + environmentType: 'conda', + }); + return { path: getPrefixCondaEnvPath(workspace), workspaceFolder: workspace }; + } + } + + const createEnvInternal = async (progress: CreateEnvironmentProgress, token: CancellationToken) => { + progress.report({ + message: CreateEnv.statusStarting, + }); + + let envPath: string | undefined; + try { + sendTelemetryEvent(EventName.ENVIRONMENT_CREATING, undefined, { + environmentType: 'conda', + pythonVersion: version, + }); + if (workspace) { + envPath = await createCondaEnv( + workspace, + getExecutableCommand(conda), + generateCommandArgs(version, options), + progress, + token, + ); + + if (envPath) { + return { path: envPath, workspaceFolder: workspace }; + } + + throw new Error('Failed to create conda environment. See Output > Python for more info.'); + } else { + throw new Error('A workspace is needed to create conda environment'); + } + } catch (ex) { + traceError(ex); + showErrorMessageWithLogs(CreateEnv.Conda.errorCreatingEnvironment); + return { error: ex as Error }; + } + }; + + if (!shouldDisplayEnvCreationProgress()) { + const token = new CancellationTokenSource(); + try { + return await createEnvInternal({ report: noop }, token.token); + } finally { + token.dispose(); + } + } + return withProgress( { location: ProgressLocation.Notification, @@ -216,39 +316,7 @@ async function createEnvironment(options?: CreateEnvironmentOptions): Promise => { - let hasError = false; - - progress.report({ - message: CreateEnv.statusStarting, - }); - - let envPath: string | undefined; - try { - sendTelemetryEvent(EventName.ENVIRONMENT_CREATING, undefined, { - environmentType: 'conda', - pythonVersion: version, - }); - if (workspace) { - envPath = await createCondaEnv( - workspace, - getExecutableCommand(conda), - generateCommandArgs(version, options), - progress, - token, - ); - } - } catch (ex) { - traceError(ex); - hasError = true; - throw ex; - } finally { - if (hasError) { - showErrorMessageWithLogs(CreateEnv.Conda.errorCreatingEnvironment); - } - } - return { path: envPath, workspaceFolder: workspace, action: undefined, error: undefined }; - }, + ): Promise => createEnvInternal(progress, token), ); } diff --git a/src/client/pythonEnvironments/creation/provider/condaDeleteUtils.ts b/src/client/pythonEnvironments/creation/provider/condaDeleteUtils.ts new file mode 100644 index 000000000000..e4f4784f15c8 --- /dev/null +++ b/src/client/pythonEnvironments/creation/provider/condaDeleteUtils.ts @@ -0,0 +1,37 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { WorkspaceFolder } from 'vscode'; +import { plainExec } from '../../../common/process/rawProcessApis'; +import { CreateEnv } from '../../../common/utils/localize'; +import { traceError, traceInfo } from '../../../logging'; +import { getPrefixCondaEnvPath, hasPrefixCondaEnv, showErrorMessageWithLogs } from '../common/commonUtils'; + +export async function deleteCondaEnvironment( + workspace: WorkspaceFolder, + interpreter: string, + pathEnvVar: string, +): Promise { + const condaEnvPath = getPrefixCondaEnvPath(workspace); + const command = interpreter; + const args = ['-m', 'conda', 'env', 'remove', '--prefix', condaEnvPath, '--yes']; + try { + traceInfo(`Deleting conda environment: ${condaEnvPath}`); + traceInfo(`Running command: ${command} ${args.join(' ')}`); + const result = await plainExec(command, args, { mergeStdOutErr: true }, { ...process.env, PATH: pathEnvVar }); + traceInfo(result.stdout); + if (await hasPrefixCondaEnv(workspace)) { + // If conda cannot delete files it will name the files as .conda_trash. + // These need to be deleted manually. + traceError(`Conda environment ${condaEnvPath} could not be deleted.`); + traceError(`Please delete the environment manually: ${condaEnvPath}`); + showErrorMessageWithLogs(CreateEnv.Conda.errorDeletingEnvironment); + return false; + } + } catch (err) { + showErrorMessageWithLogs(CreateEnv.Conda.errorDeletingEnvironment); + traceError(`Deleting conda environment ${condaEnvPath} Failed with error: `, err); + return false; + } + return true; +} diff --git a/src/client/pythonEnvironments/creation/provider/condaUtils.ts b/src/client/pythonEnvironments/creation/provider/condaUtils.ts index 4c4e816f18f1..617a2996801e 100644 --- a/src/client/pythonEnvironments/creation/provider/condaUtils.ts +++ b/src/client/pythonEnvironments/creation/provider/condaUtils.ts @@ -1,16 +1,24 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -import { CancellationToken, QuickPickItem, Uri } from 'vscode'; -import { Common } from '../../../browser/localize'; -import { Octicons } from '../../../common/constants'; -import { CreateEnv } from '../../../common/utils/localize'; +import * as path from 'path'; +import { CancellationToken, ProgressLocation, QuickPickItem, Uri, WorkspaceFolder } from 'vscode'; +import { Commands, Octicons } from '../../../common/constants'; +import { Common, CreateEnv } from '../../../common/utils/localize'; import { executeCommand } from '../../../common/vscodeApis/commandApis'; -import { showErrorMessage, showQuickPickWithBack } from '../../../common/vscodeApis/windowApis'; +import { + MultiStepAction, + showErrorMessage, + showQuickPickWithBack, + withProgress, +} from '../../../common/vscodeApis/windowApis'; import { traceLog } from '../../../logging'; import { Conda } from '../../common/environmentManagers/conda'; +import { getPrefixCondaEnvPath, hasPrefixCondaEnv } from '../common/commonUtils'; +import { OSType, getEnvironmentVariable, getOSType } from '../../../common/utils/platform'; +import { deleteCondaEnvironment } from './condaDeleteUtils'; -const RECOMMENDED_CONDA_PYTHON = '3.10'; +const RECOMMENDED_CONDA_PYTHON = '3.11'; export async function getCondaBaseEnv(): Promise { const conda = await Conda.getConda(); @@ -39,7 +47,7 @@ export async function getCondaBaseEnv(): Promise { } export async function pickPythonVersion(token?: CancellationToken): Promise { - const items: QuickPickItem[] = ['3.10', '3.11', '3.9', '3.8', '3.7'].map((v) => ({ + const items: QuickPickItem[] = ['3.11', '3.12', '3.10', '3.9', '3.8'].map((v) => ({ label: v === RECOMMENDED_CONDA_PYTHON ? `${Octicons.Star} Python` : 'Python', description: v, })); @@ -47,6 +55,8 @@ export async function pickPythonVersion(token?: CancellationToken): Promise { + const condaEnvPath = getPrefixCondaEnvPath(workspaceFolder); + return withProgress( + { + location: ProgressLocation.Notification, + title: `${CreateEnv.Conda.deletingEnvironmentProgress} ([${Common.showLogs}](command:${Commands.ViewOutput})): ${condaEnvPath}`, + cancellable: false, + }, + async () => deleteCondaEnvironment(workspaceFolder, interpreter, getPathEnvVariableForConda(interpreter)), + ); +} + +export enum ExistingCondaAction { + Recreate, + UseExisting, + Create, +} + +export async function pickExistingCondaAction( + workspaceFolder: WorkspaceFolder | undefined, +): Promise { + if (workspaceFolder) { + if (await hasPrefixCondaEnv(workspaceFolder)) { + const items: QuickPickItem[] = [ + { label: CreateEnv.Conda.recreate, description: CreateEnv.Conda.recreateDescription }, + { + label: CreateEnv.Conda.useExisting, + description: CreateEnv.Conda.useExistingDescription, + }, + ]; + + const selection = (await showQuickPickWithBack( + items, + { + placeHolder: CreateEnv.Conda.existingCondaQuickPickPlaceholder, + ignoreFocusOut: true, + }, + undefined, + )) as QuickPickItem | undefined; + + if (selection?.label === CreateEnv.Conda.recreate) { + return ExistingCondaAction.Recreate; + } + + if (selection?.label === CreateEnv.Conda.useExisting) { + return ExistingCondaAction.UseExisting; + } + } else { + return ExistingCondaAction.Create; + } + } + + throw MultiStepAction.Cancel; +} diff --git a/src/client/pythonEnvironments/creation/provider/hideEnvCreation.ts b/src/client/pythonEnvironments/creation/provider/hideEnvCreation.ts new file mode 100644 index 000000000000..5c29a8d7128d --- /dev/null +++ b/src/client/pythonEnvironments/creation/provider/hideEnvCreation.ts @@ -0,0 +1,21 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { Disposable } from 'vscode'; + +const envCreationTracker: Disposable[] = []; + +export function hideEnvCreation(): Disposable { + const disposable = new Disposable(() => { + const index = envCreationTracker.indexOf(disposable); + if (index > -1) { + envCreationTracker.splice(index, 1); + } + }); + envCreationTracker.push(disposable); + return disposable; +} + +export function shouldDisplayEnvCreationProgress(): boolean { + return envCreationTracker.length === 0; +} diff --git a/src/client/pythonEnvironments/creation/provider/venvCreationProvider.ts b/src/client/pythonEnvironments/creation/provider/venvCreationProvider.ts index e18f2fa79fde..c5c82b85357f 100644 --- a/src/client/pythonEnvironments/creation/provider/venvCreationProvider.ts +++ b/src/client/pythonEnvironments/creation/provider/venvCreationProvider.ts @@ -2,14 +2,14 @@ // Licensed under the MIT License. import * as os from 'os'; -import { CancellationToken, ProgressLocation, WorkspaceFolder } from 'vscode'; +import { CancellationToken, CancellationTokenSource, ProgressLocation, WorkspaceFolder } from 'vscode'; import { Commands, PVSC_EXTENSION_ID } from '../../../common/constants'; import { createVenvScript } from '../../../common/process/internal/scripts'; import { execObservable } from '../../../common/process/rawProcessApis'; import { createDeferred } from '../../../common/utils/async'; import { Common, CreateEnv } from '../../../common/utils/localize'; -import { traceError, traceLog, traceVerbose } from '../../../logging'; -import { CreateEnvironmentProgress } from '../types'; +import { traceError, traceInfo, traceLog, traceVerbose } from '../../../logging'; +import { CreateEnvironmentOptionsInternal, CreateEnvironmentProgress } from '../types'; import { pickWorkspaceFolder } from '../common/workspaceSelection'; import { IInterpreterQuickPick } from '../../../interpreter/configuration/types'; import { EnvironmentType, PythonEnvironment } from '../../info'; @@ -17,17 +17,31 @@ import { MultiStepAction, MultiStepNode, withProgress } from '../../../common/vs import { sendTelemetryEvent } from '../../../telemetry'; import { EventName } from '../../../telemetry/constants'; import { VenvProgressAndTelemetry, VENV_CREATED_MARKER, VENV_EXISTING_MARKER } from './venvProgressAndTelemetry'; -import { showErrorMessageWithLogs } from '../common/commonUtils'; -import { IPackageInstallSelection, pickPackagesToInstall } from './venvUtils'; +import { getVenvExecutable, showErrorMessageWithLogs } from '../common/commonUtils'; +import { + ExistingVenvAction, + IPackageInstallSelection, + deleteEnvironment, + pickExistingVenvAction, + pickPackagesToInstall, +} from './venvUtils'; import { InputFlowAction } from '../../../common/utils/multiStepInput'; import { CreateEnvironmentProvider, CreateEnvironmentOptions, CreateEnvironmentResult, } from '../proposed.createEnvApis'; +import { shouldDisplayEnvCreationProgress } from './hideEnvCreation'; +import { noop } from '../../../common/utils/misc'; + +interface IVenvCommandArgs { + argv: string[]; + stdin: string | undefined; +} -function generateCommandArgs(installInfo?: IPackageInstallSelection[], addGitIgnore?: boolean): string[] { +function generateCommandArgs(installInfo?: IPackageInstallSelection[], addGitIgnore?: boolean): IVenvCommandArgs { const command: string[] = [createVenvScript()]; + let stdin: string | undefined; if (addGitIgnore) { command.push('--git-ignore'); @@ -46,14 +60,21 @@ function generateCommandArgs(installInfo?: IPackageInstallSelection[], addGitIgn }); const requirements = installInfo.filter((i) => i.installType === 'requirements').map((i) => i.installItem); - requirements.forEach((r) => { - if (r) { - command.push('--requirements', r); - } - }); + + if (requirements.length < 10) { + requirements.forEach((r) => { + if (r) { + command.push('--requirements', r); + } + }); + } else { + command.push('--stdin'); + // Too many requirements can cause the command line to be too long error. + stdin = JSON.stringify({ requirements }); + } } - return command; + return { argv: command, stdin }; } function getVenvFromOutput(output: string): string | undefined { @@ -75,7 +96,7 @@ function getVenvFromOutput(output: string): string | undefined { async function createVenv( workspace: WorkspaceFolder, command: string, - args: string[], + args: IVenvCommandArgs, progress: CreateEnvironmentProgress, token?: CancellationToken, ): Promise { @@ -88,11 +109,15 @@ async function createVenv( }); const deferred = createDeferred(); - traceLog('Running Env creation script: ', [command, ...args]); - const { proc, out, dispose } = execObservable(command, args, { + traceLog('Running Env creation script: ', [command, ...args.argv]); + if (args.stdin) { + traceLog('Requirements passed in via stdin: ', args.stdin); + } + const { proc, out, dispose } = execObservable(command, args.argv, { mergeStdOutErr: true, token, cwd: workspace.uri.fsPath, + stdinStr: args.stdin, }); const progressAndTelemetry = new VenvProgressAndTelemetry(progress); @@ -100,7 +125,7 @@ async function createVenv( out.subscribe( (value) => { const output = value.out.split(/\r?\n/g).join(os.EOL); - traceLog(output); + traceLog(output.trimEnd()); if (output.includes(VENV_CREATED_MARKER) || output.includes(VENV_EXISTING_MARKER)) { venvPath = getVenvFromOutput(output); } @@ -114,7 +139,10 @@ async function createVenv( dispose(); if (proc?.exitCode !== 0) { traceError('Error while running venv creation script: ', progressAndTelemetry.getLastError()); - deferred.reject(progressAndTelemetry.getLastError()); + deferred.reject( + progressAndTelemetry.getLastError() || + `Failed to create virtual environment with exitCode: ${proc?.exitCode}`, + ); } else { deferred.resolve(venvPath); } @@ -123,16 +151,26 @@ async function createVenv( return deferred.promise; } +export const VenvCreationProviderId = `${PVSC_EXTENSION_ID}:venv`; export class VenvCreationProvider implements CreateEnvironmentProvider { constructor(private readonly interpreterQuickPick: IInterpreterQuickPick) {} - public async createEnvironment(options?: CreateEnvironmentOptions): Promise { - let workspace: WorkspaceFolder | undefined; + public async createEnvironment( + options?: CreateEnvironmentOptions & CreateEnvironmentOptionsInternal, + ): Promise { + let workspace = options?.workspaceFolder; + const bypassQuickPicks = options?.workspaceFolder && options.interpreter && options.providerId ? true : false; const workspaceStep = new MultiStepNode( undefined, async (context?: MultiStepAction) => { try { - workspace = (await pickWorkspaceFolder(undefined, context)) as WorkspaceFolder | undefined; + workspace = + workspace && bypassQuickPicks + ? workspace + : ((await pickWorkspaceFolder( + { preSelectedWorkspace: options?.workspaceFolder }, + context, + )) as WorkspaceFolder | undefined); } catch (ex) { if (ex === MultiStepAction.Back || ex === MultiStepAction.Cancel) { return ex; @@ -144,37 +182,78 @@ export class VenvCreationProvider implements CreateEnvironmentProvider { traceError('Workspace was not selected or found for creating virtual environment.'); return MultiStepAction.Cancel; } + traceInfo(`Selected workspace ${workspace.uri.fsPath} for creating virtual environment.`); return MultiStepAction.Continue; }, undefined, ); - let interpreter: string | undefined; - const interpreterStep = new MultiStepNode( + let existingVenvAction: ExistingVenvAction | undefined; + if (bypassQuickPicks) { + existingVenvAction = ExistingVenvAction.Create; + } + const existingEnvStep = new MultiStepNode( workspaceStep, - async () => { - if (workspace) { + async (context?: MultiStepAction) => { + if (workspace && context === MultiStepAction.Continue) { try { - interpreter = await this.interpreterQuickPick.getInterpreterViaQuickPick( - workspace.uri, - (i: PythonEnvironment) => - [ - EnvironmentType.System, - EnvironmentType.MicrosoftStore, - EnvironmentType.Global, - ].includes(i.envType), - { - skipRecommended: true, - showBackButton: true, - placeholder: CreateEnv.Venv.selectPythonPlaceHolder, - title: null, - }, - ); + existingVenvAction = await pickExistingVenvAction(workspace); + return MultiStepAction.Continue; } catch (ex) { - if (ex === InputFlowAction.back) { + if (ex === MultiStepAction.Back || ex === MultiStepAction.Cancel) { + return ex; + } + throw ex; + } + } else if (context === MultiStepAction.Back) { + return MultiStepAction.Back; + } + return MultiStepAction.Continue; + }, + undefined, + ); + workspaceStep.next = existingEnvStep; + + let interpreter = options?.interpreter; + const interpreterStep = new MultiStepNode( + existingEnvStep, + async (context?: MultiStepAction) => { + if (workspace) { + if ( + existingVenvAction === ExistingVenvAction.Recreate || + existingVenvAction === ExistingVenvAction.Create + ) { + try { + interpreter = + interpreter && bypassQuickPicks + ? interpreter + : await this.interpreterQuickPick.getInterpreterViaQuickPick( + workspace.uri, + (i: PythonEnvironment) => + [ + EnvironmentType.System, + EnvironmentType.MicrosoftStore, + EnvironmentType.Global, + EnvironmentType.Pyenv, + ].includes(i.envType) && i.type === undefined, // only global intepreters + { + skipRecommended: true, + showBackButton: true, + placeholder: CreateEnv.Venv.selectPythonPlaceHolder, + title: null, + }, + ); + } catch (ex) { + if (ex === InputFlowAction.back) { + return MultiStepAction.Back; + } + interpreter = undefined; + } + } else if (existingVenvAction === ExistingVenvAction.UseExisting) { + if (context === MultiStepAction.Back) { return MultiStepAction.Back; } - interpreter = undefined; + interpreter = getVenvExecutable(workspace); } } @@ -182,11 +261,12 @@ export class VenvCreationProvider implements CreateEnvironmentProvider { traceError('Virtual env creation requires an interpreter.'); return MultiStepAction.Cancel; } + traceInfo(`Selected interpreter ${interpreter} for creating virtual environment.`); return MultiStepAction.Continue; }, undefined, ); - workspaceStep.next = interpreterStep; + existingEnvStep.next = interpreterStep; let addGitIgnore = true; let installPackages = true; @@ -197,19 +277,23 @@ export class VenvCreationProvider implements CreateEnvironmentProvider { let installInfo: IPackageInstallSelection[] | undefined; const packagesStep = new MultiStepNode( interpreterStep, - async () => { + async (context?: MultiStepAction) => { if (workspace && installPackages) { - try { - installInfo = await pickPackagesToInstall(workspace); - } catch (ex) { - if (ex === MultiStepAction.Back || ex === MultiStepAction.Cancel) { - return ex; + if (existingVenvAction !== ExistingVenvAction.UseExisting) { + try { + installInfo = await pickPackagesToInstall(workspace); + } catch (ex) { + if (ex === MultiStepAction.Back || ex === MultiStepAction.Cancel) { + return ex; + } + throw ex; } - throw ex; - } - if (!installInfo) { - traceVerbose('Virtual env creation exited during dependencies selection.'); - return MultiStepAction.Cancel; + if (!installInfo) { + traceVerbose('Virtual env creation exited during dependencies selection.'); + return MultiStepAction.Cancel; + } + } else if (context === MultiStepAction.Back) { + return MultiStepAction.Back; } } @@ -224,7 +308,63 @@ export class VenvCreationProvider implements CreateEnvironmentProvider { throw action; } + if (workspace) { + if (existingVenvAction === ExistingVenvAction.Recreate) { + sendTelemetryEvent(EventName.ENVIRONMENT_DELETE, undefined, { + environmentType: 'venv', + status: 'triggered', + }); + if (await deleteEnvironment(workspace, interpreter)) { + sendTelemetryEvent(EventName.ENVIRONMENT_DELETE, undefined, { + environmentType: 'venv', + status: 'deleted', + }); + } else { + sendTelemetryEvent(EventName.ENVIRONMENT_DELETE, undefined, { + environmentType: 'venv', + status: 'failed', + }); + throw MultiStepAction.Cancel; + } + } else if (existingVenvAction === ExistingVenvAction.UseExisting) { + sendTelemetryEvent(EventName.ENVIRONMENT_REUSE, undefined, { + environmentType: 'venv', + }); + return { path: getVenvExecutable(workspace), workspaceFolder: workspace }; + } + } + const args = generateCommandArgs(installInfo, addGitIgnore); + const createEnvInternal = async (progress: CreateEnvironmentProgress, token: CancellationToken) => { + progress.report({ + message: CreateEnv.statusStarting, + }); + + let envPath: string | undefined; + try { + if (interpreter && workspace) { + envPath = await createVenv(workspace, interpreter, args, progress, token); + if (envPath) { + return { path: envPath, workspaceFolder: workspace }; + } + throw new Error('Failed to create virtual environment. See Output > Python for more info.'); + } + throw new Error('Failed to create virtual environment. Either interpreter or workspace is undefined.'); + } catch (ex) { + traceError(ex); + showErrorMessageWithLogs(CreateEnv.Venv.errorCreatingEnvironment); + return { error: ex as Error }; + } + }; + + if (!shouldDisplayEnvCreationProgress()) { + const token = new CancellationTokenSource(); + try { + return await createEnvInternal({ report: noop }, token.token); + } finally { + token.dispose(); + } + } return withProgress( { @@ -235,30 +375,7 @@ export class VenvCreationProvider implements CreateEnvironmentProvider { async ( progress: CreateEnvironmentProgress, token: CancellationToken, - ): Promise => { - let hasError = false; - - progress.report({ - message: CreateEnv.statusStarting, - }); - - let envPath: string | undefined; - try { - if (interpreter && workspace) { - envPath = await createVenv(workspace, interpreter, args, progress, token); - } - } catch (ex) { - traceError(ex); - hasError = true; - throw ex; - } finally { - if (hasError) { - showErrorMessageWithLogs(CreateEnv.Venv.errorCreatingEnvironment); - } - } - - return { path: envPath, workspaceFolder: workspace, action: undefined, error: undefined }; - }, + ): Promise => createEnvInternal(progress, token), ); } @@ -266,7 +383,7 @@ export class VenvCreationProvider implements CreateEnvironmentProvider { description: string = CreateEnv.Venv.providerDescription; - id = `${PVSC_EXTENSION_ID}:venv`; + id = VenvCreationProviderId; tools = ['Venv']; } diff --git a/src/client/pythonEnvironments/creation/provider/venvDeleteUtils.ts b/src/client/pythonEnvironments/creation/provider/venvDeleteUtils.ts new file mode 100644 index 000000000000..9bd410c09f51 --- /dev/null +++ b/src/client/pythonEnvironments/creation/provider/venvDeleteUtils.ts @@ -0,0 +1,99 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import * as path from 'path'; +import { WorkspaceFolder } from 'vscode'; +import * as fs from '../../../common/platform/fs-paths'; +import { traceError, traceInfo } from '../../../logging'; +import { getVenvPath, showErrorMessageWithLogs } from '../common/commonUtils'; +import { CreateEnv } from '../../../common/utils/localize'; +import { sleep } from '../../../common/utils/async'; +import { switchSelectedPython } from './venvSwitchPython'; + +async function tryDeleteFile(file: string): Promise { + try { + if (!(await fs.pathExists(file))) { + return true; + } + await fs.unlink(file); + return true; + } catch (err) { + traceError(`Failed to delete file [${file}]:`, err); + return false; + } +} + +async function tryDeleteDir(dir: string): Promise { + try { + if (!(await fs.pathExists(dir))) { + return true; + } + await fs.rmdir(dir, { + recursive: true, + maxRetries: 10, + retryDelay: 200, + }); + return true; + } catch (err) { + traceError(`Failed to delete directory [${dir}]:`, err); + return false; + } +} + +export async function deleteEnvironmentNonWindows(workspaceFolder: WorkspaceFolder): Promise { + const venvPath = getVenvPath(workspaceFolder); + if (await tryDeleteDir(venvPath)) { + traceInfo(`Deleted venv dir: ${venvPath}`); + return true; + } + showErrorMessageWithLogs(CreateEnv.Venv.errorDeletingEnvironment); + return false; +} + +export async function deleteEnvironmentWindows( + workspaceFolder: WorkspaceFolder, + interpreter: string | undefined, +): Promise { + const venvPath = getVenvPath(workspaceFolder); + const venvPythonPath = path.join(venvPath, 'Scripts', 'python.exe'); + + if (await tryDeleteFile(venvPythonPath)) { + traceInfo(`Deleted python executable: ${venvPythonPath}`); + if (await tryDeleteDir(venvPath)) { + traceInfo(`Deleted ".venv" dir: ${venvPath}`); + return true; + } + + traceError(`Failed to delete ".venv" dir: ${venvPath}`); + traceError( + 'This happens if the virtual environment is still in use, or some binary in the venv is still running.', + ); + traceError(`Please delete the ".venv" manually: [${venvPath}]`); + showErrorMessageWithLogs(CreateEnv.Venv.errorDeletingEnvironment); + return false; + } + traceError(`Failed to delete python executable: ${venvPythonPath}`); + traceError('This happens if the virtual environment is still in use.'); + + if (interpreter) { + traceError('We will attempt to switch python temporarily to delete the ".venv"'); + + await switchSelectedPython(interpreter, workspaceFolder.uri, 'temporarily to delete the ".venv"'); + + traceInfo(`Attempting to delete ".venv" again: ${venvPath}`); + const ms = 500; + for (let i = 0; i < 5; i = i + 1) { + traceInfo(`Waiting for ${ms}ms to let processes exit, before a delete attempt.`); + await sleep(ms); + if (await tryDeleteDir(venvPath)) { + traceInfo(`Deleted ".venv" dir: ${venvPath}`); + return true; + } + traceError(`Failed to delete ".venv" dir [${venvPath}] (attempt ${i + 1}/5).`); + } + } else { + traceError(`Please delete the ".venv" dir manually: [${venvPath}]`); + } + showErrorMessageWithLogs(CreateEnv.Venv.errorDeletingEnvironment); + return false; +} diff --git a/src/client/pythonEnvironments/creation/provider/venvSwitchPython.ts b/src/client/pythonEnvironments/creation/provider/venvSwitchPython.ts new file mode 100644 index 000000000000..e2567dfd114b --- /dev/null +++ b/src/client/pythonEnvironments/creation/provider/venvSwitchPython.ts @@ -0,0 +1,28 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import * as path from 'path'; +import { Disposable, Uri } from 'vscode'; +import { createDeferred } from '../../../common/utils/async'; +import { getExtension } from '../../../common/vscodeApis/extensionsApi'; +import { PVSC_EXTENSION_ID, PythonExtension } from '../../../api/types'; +import { traceInfo } from '../../../logging'; + +export async function switchSelectedPython(interpreter: string, uri: Uri, purpose: string): Promise { + let dispose: Disposable | undefined; + try { + const deferred = createDeferred(); + const api: PythonExtension = getExtension(PVSC_EXTENSION_ID)?.exports as PythonExtension; + dispose = api.environments.onDidChangeActiveEnvironmentPath(async (e) => { + if (path.normalize(e.path) === path.normalize(interpreter)) { + traceInfo(`Switched to interpreter ${purpose}: ${interpreter}`); + deferred.resolve(); + } + }); + api.environments.updateActiveEnvironmentPath(interpreter, uri); + traceInfo(`Switching interpreter ${purpose}: ${interpreter}`); + await deferred.promise; + } finally { + dispose?.dispose(); + } +} diff --git a/src/client/pythonEnvironments/creation/provider/venvUtils.ts b/src/client/pythonEnvironments/creation/provider/venvUtils.ts index 7c6505082fbc..1bfb2c96f224 100644 --- a/src/client/pythonEnvironments/creation/provider/venvUtils.ts +++ b/src/client/pythonEnvironments/creation/provider/venvUtils.ts @@ -2,17 +2,40 @@ // Licensed under the MIT License import * as tomljs from '@iarna/toml'; -import * as fs from 'fs-extra'; import { flatten, isArray } from 'lodash'; import * as path from 'path'; -import { CancellationToken, QuickPickItem, RelativePattern, WorkspaceFolder } from 'vscode'; -import { CreateEnv } from '../../../common/utils/localize'; -import { MultiStepAction, MultiStepNode, showQuickPickWithBack } from '../../../common/vscodeApis/windowApis'; +import { + CancellationToken, + ProgressLocation, + QuickPickItem, + QuickPickItemButtonEvent, + RelativePattern, + ThemeIcon, + Uri, + WorkspaceFolder, +} from 'vscode'; +import * as fs from '../../../common/platform/fs-paths'; +import { Common, CreateEnv } from '../../../common/utils/localize'; +import { + MultiStepAction, + MultiStepNode, + showQuickPickWithBack, + showTextDocument, + withProgress, +} from '../../../common/vscodeApis/windowApis'; import { findFiles } from '../../../common/vscodeApis/workspaceApis'; import { traceError, traceVerbose } from '../../../logging'; +import { Commands } from '../../../common/constants'; +import { isWindows } from '../../../common/utils/platform'; +import { getVenvPath, hasVenv } from '../common/commonUtils'; +import { deleteEnvironmentNonWindows, deleteEnvironmentWindows } from './venvDeleteUtils'; +export const OPEN_REQUIREMENTS_BUTTON = { + iconPath: new ThemeIcon('go-to-file'), + tooltip: CreateEnv.Venv.openRequirementsFile, +}; const exclude = '**/{.venv*,.git,.nox,.tox,.conda,site-packages,__pypackages__}/**'; -async function getPipRequirementsFiles( +export async function getPipRequirementsFiles( workspaceFolder: WorkspaceFolder, token?: CancellationToken, ): Promise { @@ -38,6 +61,10 @@ function tomlHasBuildSystem(toml: tomljs.JsonMap): boolean { return toml['build-system'] !== undefined; } +function tomlHasProject(toml: tomljs.JsonMap): boolean { + return toml.project !== undefined; +} + function getTomlOptionalDeps(toml: tomljs.JsonMap): string[] { const extras: string[] = []; if (toml.project && (toml.project as tomljs.JsonMap)['optional-dependencies']) { @@ -69,8 +96,13 @@ async function pickTomlExtras(extras: string[], token?: CancellationToken): Prom return undefined; } -async function pickRequirementsFiles(files: string[], token?: CancellationToken): Promise { +async function pickRequirementsFiles( + files: string[], + root: string, + token?: CancellationToken, +): Promise { const items: QuickPickItem[] = files + .map((p) => path.relative(root, p)) .sort((a, b) => { const al: number = a.split(/[\\\/]/).length; const bl: number = b.split(/[\\\/]/).length; @@ -82,7 +114,10 @@ async function pickRequirementsFiles(files: string[], token?: CancellationToken) } return al - bl; }) - .map((e) => ({ label: e })); + .map((e) => ({ + label: e, + buttons: [OPEN_REQUIREMENTS_BUTTON], + })); const selection = await showQuickPickWithBack( items, @@ -92,6 +127,11 @@ async function pickRequirementsFiles(files: string[], token?: CancellationToken) canPickMany: true, }, token, + async (e: QuickPickItemButtonEvent) => { + if (e.item.label) { + await showTextDocument(Uri.file(path.join(root, e.item.label))); + } + }, ); if (selection && isArray(selection)) { @@ -103,7 +143,7 @@ async function pickRequirementsFiles(files: string[], token?: CancellationToken) export function isPipInstallableToml(tomlContent: string): boolean { const toml = tomlParse(tomlContent); - return tomlHasBuildSystem(toml); + return tomlHasBuildSystem(toml) && tomlHasProject(toml); } export interface IPackageInstallSelection { @@ -126,12 +166,17 @@ export async function pickPackagesToInstall( let extras: string[] = []; let hasBuildSystem = false; + let hasProject = false; if (await fs.pathExists(tomlPath)) { const toml = tomlParse(await fs.readFile(tomlPath, 'utf-8')); extras = getTomlOptionalDeps(toml); hasBuildSystem = tomlHasBuildSystem(toml); + hasProject = tomlHasProject(toml); + if (!hasProject) { + traceVerbose('Create env: Found toml without project. So we will not use editable install.'); + } if (!hasBuildSystem) { traceVerbose('Create env: Found toml without build system. So we will not use editable install.'); } @@ -143,7 +188,7 @@ export async function pickPackagesToInstall( return MultiStepAction.Back; } - if (hasBuildSystem) { + if (hasBuildSystem && hasProject) { if (extras.length > 0) { traceVerbose('Create Env: Found toml with optional dependencies.'); @@ -186,14 +231,11 @@ export async function pickPackagesToInstall( tomlStep, async (context?: MultiStepAction) => { traceVerbose('Looking for pip requirements.'); - const requirementFiles = (await getPipRequirementsFiles(workspaceFolder, token))?.map((p) => - path.relative(workspaceFolder.uri.fsPath, p), - ); - + const requirementFiles = await getPipRequirementsFiles(workspaceFolder, token); if (requirementFiles && requirementFiles.length > 0) { traceVerbose('Found pip requirements.'); try { - const result = await pickRequirementsFiles(requirementFiles, token); + const result = await pickRequirementsFiles(requirementFiles, workspaceFolder.uri.fsPath, token); const installList = result?.map((p) => path.join(workspaceFolder.uri.fsPath, p)); if (installList) { installList.forEach((i) => { @@ -226,3 +268,69 @@ export async function pickPackagesToInstall( return packages; } + +export async function deleteEnvironment( + workspaceFolder: WorkspaceFolder, + interpreter: string | undefined, +): Promise { + const venvPath = getVenvPath(workspaceFolder); + return withProgress( + { + location: ProgressLocation.Notification, + title: `${CreateEnv.Venv.deletingEnvironmentProgress} ([${Common.showLogs}](command:${Commands.ViewOutput})): ${venvPath}`, + cancellable: false, + }, + async () => { + if (isWindows()) { + return deleteEnvironmentWindows(workspaceFolder, interpreter); + } + return deleteEnvironmentNonWindows(workspaceFolder); + }, + ); +} + +export enum ExistingVenvAction { + Recreate, + UseExisting, + Create, +} + +export async function pickExistingVenvAction( + workspaceFolder: WorkspaceFolder | undefined, +): Promise { + if (workspaceFolder) { + if (await hasVenv(workspaceFolder)) { + const items: QuickPickItem[] = [ + { + label: CreateEnv.Venv.useExisting, + description: CreateEnv.Venv.useExistingDescription, + }, + { + label: CreateEnv.Venv.recreate, + description: CreateEnv.Venv.recreateDescription, + }, + ]; + + const selection = (await showQuickPickWithBack( + items, + { + placeHolder: CreateEnv.Venv.existingVenvQuickPickPlaceholder, + ignoreFocusOut: true, + }, + undefined, + )) as QuickPickItem | undefined; + + if (selection?.label === CreateEnv.Venv.recreate) { + return ExistingVenvAction.Recreate; + } + + if (selection?.label === CreateEnv.Venv.useExisting) { + return ExistingVenvAction.UseExisting; + } + } else { + return ExistingVenvAction.Create; + } + } + + throw MultiStepAction.Cancel; +} diff --git a/src/client/pythonEnvironments/creation/pyprojectTomlCreateEnv.ts b/src/client/pythonEnvironments/creation/pyProjectTomlContext.ts similarity index 58% rename from src/client/pythonEnvironments/creation/pyprojectTomlCreateEnv.ts rename to src/client/pythonEnvironments/creation/pyProjectTomlContext.ts index 5ead37b80dc9..5925b7641f45 100644 --- a/src/client/pythonEnvironments/creation/pyprojectTomlCreateEnv.ts +++ b/src/client/pythonEnvironments/creation/pyProjectTomlContext.ts @@ -1,12 +1,12 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -import { TextDocument, TextDocumentChangeEvent } from 'vscode'; +import { TextDocument } from 'vscode'; import { IDisposableRegistry } from '../../common/types'; import { executeCommand } from '../../common/vscodeApis/commandApis'; import { onDidOpenTextDocument, - onDidChangeTextDocument, + onDidSaveTextDocument, getOpenTextDocuments, } from '../../common/vscodeApis/workspaceApis'; import { isPipInstallableToml } from './provider/venvUtils'; @@ -19,23 +19,26 @@ async function setPyProjectTomlContextKey(doc: TextDocument): Promise { } } -export function registerPyProjectTomlCreateEnvFeatures(disposables: IDisposableRegistry): void { +export function registerPyProjectTomlFeatures(disposables: IDisposableRegistry): void { disposables.push( onDidOpenTextDocument(async (doc: TextDocument) => { if (doc.fileName.endsWith('pyproject.toml')) { await setPyProjectTomlContextKey(doc); } }), - onDidChangeTextDocument(async (e: TextDocumentChangeEvent) => { - if (e.document.fileName.endsWith('pyproject.toml')) { - await setPyProjectTomlContextKey(e.document); + onDidSaveTextDocument(async (doc: TextDocument) => { + if (doc.fileName.endsWith('pyproject.toml')) { + await setPyProjectTomlContextKey(doc); } }), ); - getOpenTextDocuments().forEach(async (doc: TextDocument) => { - if (doc.fileName.endsWith('pyproject.toml')) { - await setPyProjectTomlContextKey(doc); - } - }); + const docs = getOpenTextDocuments().filter( + (doc) => doc.fileName.endsWith('pyproject.toml') && isPipInstallableToml(doc.getText()), + ); + if (docs.length > 0) { + executeCommand('setContext', 'pipInstallableToml', true); + } else { + executeCommand('setContext', 'pipInstallableToml', false); + } } diff --git a/src/client/pythonEnvironments/creation/registrations.ts b/src/client/pythonEnvironments/creation/registrations.ts new file mode 100644 index 000000000000..25141cbec5ac --- /dev/null +++ b/src/client/pythonEnvironments/creation/registrations.ts @@ -0,0 +1,25 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { IDisposableRegistry, IPathUtils } from '../../common/types'; +import { IInterpreterQuickPick, IPythonPathUpdaterServiceManager } from '../../interpreter/configuration/types'; +import { IInterpreterService } from '../../interpreter/contracts'; +import { registerCreateEnvironmentFeatures } from './createEnvApi'; +import { registerCreateEnvironmentButtonFeatures } from './createEnvButtonContext'; +import { registerTriggerForPipInTerminal } from './globalPipInTerminalTrigger'; +import { registerInstalledPackagesDiagnosticsProvider } from './installedPackagesDiagnostic'; +import { registerPyProjectTomlFeatures } from './pyProjectTomlContext'; + +export function registerAllCreateEnvironmentFeatures( + disposables: IDisposableRegistry, + interpreterQuickPick: IInterpreterQuickPick, + pythonPathUpdater: IPythonPathUpdaterServiceManager, + interpreterService: IInterpreterService, + pathUtils: IPathUtils, +): void { + registerCreateEnvironmentFeatures(disposables, interpreterQuickPick, pythonPathUpdater, pathUtils); + registerCreateEnvironmentButtonFeatures(disposables); + registerPyProjectTomlFeatures(disposables); + registerInstalledPackagesDiagnosticsProvider(disposables, interpreterService); + registerTriggerForPipInTerminal(disposables); +} diff --git a/src/client/pythonEnvironments/creation/types.ts b/src/client/pythonEnvironments/creation/types.ts index 611af5bf7841..0e400c2d90f3 100644 --- a/src/client/pythonEnvironments/creation/types.ts +++ b/src/client/pythonEnvironments/creation/types.ts @@ -1,6 +1,16 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License -import { Progress } from 'vscode'; +import { Progress, WorkspaceFolder } from 'vscode'; export interface CreateEnvironmentProgress extends Progress<{ message?: string; increment?: number }> {} + +/** + * The interpreter path to use for the environment creation. If not provided, will prompt the user to select one. + * If the value of `interpreter` & `workspaceFolder` & `providerId` are provided we will not prompt the user to select a provider, nor folder, nor an interpreter. + */ +export interface CreateEnvironmentOptionsInternal { + workspaceFolder?: WorkspaceFolder; + providerId?: string; + interpreter?: string; +} diff --git a/src/client/pythonEnvironments/index.ts b/src/client/pythonEnvironments/index.ts index 8065811a8a62..299dfab59132 100644 --- a/src/client/pythonEnvironments/index.ts +++ b/src/client/pythonEnvironments/index.ts @@ -28,6 +28,7 @@ import { MicrosoftStoreLocator } from './base/locators/lowLevel/microsoftStoreLo import { getEnvironmentInfoService } from './base/info/environmentInfoService'; import { registerNewDiscoveryForIOC } from './legacyIOC'; import { PoetryLocator } from './base/locators/lowLevel/poetryLocator'; +import { HatchLocator } from './base/locators/lowLevel/hatchLocator'; import { createPythonEnvironments } from './api'; import { createCollectionCache as createCache, @@ -37,6 +38,20 @@ import { EnvsCollectionService } from './base/locators/composite/envsCollectionS import { IDisposable } from '../common/types'; import { traceError } from '../logging'; import { ActiveStateLocator } from './base/locators/lowLevel/activeStateLocator'; +import { CustomWorkspaceLocator } from './base/locators/lowLevel/customWorkspaceLocator'; +import { PixiLocator } from './base/locators/lowLevel/pixiLocator'; +import { getConfiguration } from '../common/vscodeApis/workspaceApis'; +import { getNativePythonFinder } from './base/locators/common/nativePythonFinder'; +import { createNativeEnvironmentsApi } from './nativeAPI'; +import { useEnvExtension } from '../envExt/api.internal'; +import { createEnvExtApi } from '../envExt/envExtApi'; + +const PYTHON_ENV_INFO_CACHE_KEY = 'PYTHON_ENV_INFO_CACHEv2'; + +export function shouldUseNativeLocator(): boolean { + const config = getConfiguration('python'); + return config.get('locator', 'js') === 'native'; +} /** * Set up the Python environments component (during extension activation).' @@ -45,6 +60,28 @@ export async function initialize(ext: ExtensionState): Promise { // Set up the legacy IOC container before api is created. initializeLegacyExternalDependencies(ext.legacyIOC.serviceContainer); + if (useEnvExtension()) { + const api = await createEnvExtApi(ext.disposables); + registerNewDiscoveryForIOC( + // These are what get wrapped in the legacy adapter. + ext.legacyIOC.serviceManager, + api, + ); + return api; + } + + if (shouldUseNativeLocator()) { + const finder = getNativePythonFinder(ext.context); + const api = createNativeEnvironmentsApi(finder); + ext.disposables.push(api); + registerNewDiscoveryForIOC( + // These are what get wrapped in the legacy adapter. + ext.legacyIOC.serviceManager, + api, + ); + return api; + } + const api = await createPythonEnvironments(() => createLocator(ext)); registerNewDiscoveryForIOC( // These are what get wrapped in the legacy adapter. @@ -67,7 +104,7 @@ export async function activate(api: IDiscoveryAPI, ext: ExtensionState): Promise */ const folders = vscode.workspace.workspaceFolders; // Trigger discovery if environment cache is empty. - const wasTriggered = getGlobalStorage(ext.context, 'PYTHON_ENV_INFO_CACHE', []).get().length > 0; + const wasTriggered = getGlobalStorage(ext.context, PYTHON_ENV_INFO_CACHE_KEY, []).get().length > 0; if (!wasTriggered) { api.triggerRefresh().ignoreErrors(); folders?.forEach(async (folder) => { @@ -128,6 +165,7 @@ async function createLocator( await createCollectionCache(ext), // This is shared. resolvingLocator, + shouldUseNativeLocator(), ); return caching; } @@ -182,7 +220,13 @@ function watchRoots(args: WatchRootsArgs): IDisposable { function createWorkspaceLocator(ext: ExtensionState): WorkspaceLocators { const locators = new WorkspaceLocators(watchRoots, [ - (root: vscode.Uri) => [new WorkspaceVirtualEnvironmentLocator(root.fsPath), new PoetryLocator(root.fsPath)], + (root: vscode.Uri) => [ + new WorkspaceVirtualEnvironmentLocator(root.fsPath), + new PoetryLocator(root.fsPath), + new HatchLocator(root.fsPath), + new PixiLocator(root.fsPath), + new CustomWorkspaceLocator(root.fsPath), + ], // Add an ILocator factory func here for each kind of workspace-rooted locator. ]); ext.disposables.push(locators); @@ -220,7 +264,7 @@ function putIntoStorage(storage: IPersistentStorage, envs: Pyth } async function createCollectionCache(ext: ExtensionState): Promise { - const storage = getGlobalStorage(ext.context, 'PYTHON_ENV_INFO_CACHE', []); + const storage = getGlobalStorage(ext.context, PYTHON_ENV_INFO_CACHE_KEY, []); const cache = await createCache({ get: () => getFromStorage(storage), store: async (e) => putIntoStorage(storage, e), diff --git a/src/client/pythonEnvironments/info/index.ts b/src/client/pythonEnvironments/info/index.ts index 70abbb0fad76..08310767914a 100644 --- a/src/client/pythonEnvironments/info/index.ts +++ b/src/client/pythonEnvironments/info/index.ts @@ -4,6 +4,7 @@ 'use strict'; import { Architecture } from '../../common/utils/platform'; +import { PythonEnvType } from '../base/info'; import { PythonVersion } from './pythonVersion'; /** @@ -18,15 +19,21 @@ export enum EnvironmentType { Venv = 'Venv', MicrosoftStore = 'MicrosoftStore', Poetry = 'Poetry', + Hatch = 'Hatch', + Pixi = 'Pixi', VirtualEnvWrapper = 'VirtualEnvWrapper', ActiveState = 'ActiveState', Global = 'Global', System = 'System', } +/** + * These envs are only created for a specific workspace, which we're able to detect. + */ +export const workspaceVirtualEnvTypes = [EnvironmentType.Poetry, EnvironmentType.Pipenv, EnvironmentType.Pixi]; export const virtualEnvTypes = [ - EnvironmentType.Poetry, - EnvironmentType.Pipenv, + ...workspaceVirtualEnvTypes, + EnvironmentType.Hatch, // This is also a workspace virtual env, but we're not treating it as such as of today. EnvironmentType.Venv, EnvironmentType.VirtualEnvWrapper, EnvironmentType.Conda, @@ -42,6 +49,7 @@ export enum ModuleInstallerType { Pip = 'Pip', Poetry = 'Poetry', Pipenv = 'Pipenv', + Pixi = 'Pixi', } /** @@ -68,10 +76,11 @@ export type InterpreterInformation = { * * @prop companyDisplayName - the user-facing name of the distro publisher * @prop displayName - the user-facing name for the environment - * @prop type - the kind of Python environment + * @prop envType - the kind of Python environment * @prop envName - the environment's name, if applicable (else `envPath` is set) * @prop envPath - the environment's root dir, if applicable (else `envName`) * @prop cachedEntry - whether or not the info came from a cache + * @prop type - the type of Python environment, if applicable */ // Note that "cachedEntry" is specific to the caching machinery // and doesn't really belong here. @@ -84,6 +93,7 @@ export type PythonEnvironment = InterpreterInformation & { envName?: string; envPath?: string; cachedEntry?: boolean; + type?: PythonEnvType; }; /** @@ -95,7 +105,7 @@ export function getEnvironmentTypeName(environmentType: EnvironmentType): string return 'conda'; } case EnvironmentType.Pipenv: { - return 'pipenv'; + return 'Pipenv'; } case EnvironmentType.Pyenv: { return 'pyenv'; @@ -107,16 +117,22 @@ export function getEnvironmentTypeName(environmentType: EnvironmentType): string return 'virtualenv'; } case EnvironmentType.MicrosoftStore: { - return 'microsoft store'; + return 'Microsoft Store'; } case EnvironmentType.Poetry: { - return 'poetry'; + return 'Poetry'; + } + case EnvironmentType.Hatch: { + return 'Hatch'; + } + case EnvironmentType.Pixi: { + return 'pixi'; } case EnvironmentType.VirtualEnvWrapper: { return 'virtualenvwrapper'; } case EnvironmentType.ActiveState: { - return 'activestate'; + return 'ActiveState'; } default: { return ''; diff --git a/src/client/pythonEnvironments/info/pythonVersion.ts b/src/client/pythonEnvironments/info/pythonVersion.ts index 92260dbb2d3f..d61fcf14db4d 100644 --- a/src/client/pythonEnvironments/info/pythonVersion.ts +++ b/src/client/pythonEnvironments/info/pythonVersion.ts @@ -25,3 +25,11 @@ export type PythonVersion = { build: string[]; prerelease: string[]; }; + +export function isStableVersion(version: PythonVersion): boolean { + // A stable version is one that has no prerelease tags. + return ( + version.prerelease.length === 0 && + (version.build.length === 0 || (version.build.length === 1 && version.build[0] === 'final')) + ); +} diff --git a/src/client/pythonEnvironments/legacyIOC.ts b/src/client/pythonEnvironments/legacyIOC.ts index 0c80c3414728..49df2ee03f21 100644 --- a/src/client/pythonEnvironments/legacyIOC.ts +++ b/src/client/pythonEnvironments/legacyIOC.ts @@ -9,7 +9,12 @@ import { Resource } from '../common/types'; import { IComponentAdapter, ICondaService, PythonEnvironmentsChangedEvent } from '../interpreter/contracts'; import { IServiceManager } from '../ioc/types'; import { PythonEnvInfo, PythonEnvKind, PythonEnvSource } from './base/info'; -import { IDiscoveryAPI, PythonLocatorQuery, TriggerRefreshOptions } from './base/locator'; +import { + GetRefreshEnvironmentsOptions, + IDiscoveryAPI, + PythonLocatorQuery, + TriggerRefreshOptions, +} from './base/locator'; import { isMacDefaultPythonPath } from './common/environmentManagers/macDefault'; import { isParentPath } from './common/externalDependencies'; import { EnvironmentType, PythonEnvironment } from './info'; @@ -21,7 +26,7 @@ import { asyncFilter } from '../common/utils/arrayUtils'; import { CondaEnvironmentInfo, isCondaEnvironment } from './common/environmentManagers/conda'; import { isMicrosoftStoreEnvironment } from './common/environmentManagers/microsoftStoreEnv'; import { CondaService } from './common/environmentManagers/condaService'; -import { traceVerbose } from '../logging'; +import { traceError, traceVerbose } from '../logging'; const convertedKinds = new Map( Object.entries({ @@ -33,12 +38,18 @@ const convertedKinds = new Map( [PythonEnvKind.VirtualEnv]: EnvironmentType.VirtualEnv, [PythonEnvKind.Pipenv]: EnvironmentType.Pipenv, [PythonEnvKind.Poetry]: EnvironmentType.Poetry, + [PythonEnvKind.Hatch]: EnvironmentType.Hatch, + [PythonEnvKind.Pixi]: EnvironmentType.Pixi, [PythonEnvKind.Venv]: EnvironmentType.Venv, [PythonEnvKind.VirtualEnvWrapper]: EnvironmentType.VirtualEnvWrapper, [PythonEnvKind.ActiveState]: EnvironmentType.ActiveState, }), ); +export function convertEnvInfoToPythonEnvironment(info: PythonEnvInfo): PythonEnvironment { + return convertEnvInfo(info); +} + function convertEnvInfo(info: PythonEnvInfo): PythonEnvironment { const { name, location, executable, arch, kind, version, distro, id } = info; const { filename, sysPrefix } = executable; @@ -75,6 +86,7 @@ function convertEnvInfo(info: PythonEnvInfo): PythonEnvironment { } env.displayName = info.display; env.detailedDisplayName = info.detailedDisplayName; + env.type = info.type; // We do not worry about using distro.defaultDisplayName. return env; @@ -101,8 +113,8 @@ class ComponentAdapter implements IComponentAdapter { return this.api.triggerRefresh(query, options); } - public getRefreshPromise() { - return this.api.getRefreshPromise(); + public getRefreshPromise(options?: GetRefreshEnvironmentsOptions) { + return this.api.getRefreshPromise(options); } public get onProgress() { @@ -151,11 +163,16 @@ class ComponentAdapter implements IComponentAdapter { // We use the same getInterpreters() here as for IInterpreterLocatorService. public async getInterpreterDetails(pythonPath: string): Promise { - const env = await this.api.resolveEnv(pythonPath); - if (!env) { + try { + const env = await this.api.resolveEnv(pythonPath); + if (!env) { + return undefined; + } + return convertEnvInfo(env); + } catch (ex) { + traceError(`Failed to resolve interpreter: ${pythonPath}`, ex); return undefined; } - return convertEnvInfo(env); } // Implements ICondaService diff --git a/src/client/pythonEnvironments/nativeAPI.ts b/src/client/pythonEnvironments/nativeAPI.ts new file mode 100644 index 000000000000..62695c8dd543 --- /dev/null +++ b/src/client/pythonEnvironments/nativeAPI.ts @@ -0,0 +1,548 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import * as path from 'path'; +import { Disposable, Event, EventEmitter, Uri, WorkspaceFoldersChangeEvent } from 'vscode'; +import { PythonEnvInfo, PythonEnvKind, PythonEnvType, PythonVersion } from './base/info'; +import { + GetRefreshEnvironmentsOptions, + IDiscoveryAPI, + ProgressNotificationEvent, + ProgressReportStage, + PythonLocatorQuery, + TriggerRefreshOptions, +} from './base/locator'; +import { PythonEnvCollectionChangedEvent } from './base/watcher'; +import { + isNativeEnvInfo, + NativeEnvInfo, + NativeEnvManagerInfo, + NativePythonFinder, +} from './base/locators/common/nativePythonFinder'; +import { createDeferred, Deferred } from '../common/utils/async'; +import { Architecture, getPathEnvVariable, getUserHomeDir } from '../common/utils/platform'; +import { parseVersion } from './base/info/pythonVersion'; +import { cache } from '../common/utils/decorators'; +import { traceError, traceInfo, traceLog, traceWarn } from '../logging'; +import { StopWatch } from '../common/utils/stopWatch'; +import { FileChangeType } from '../common/platform/fileSystemWatcher'; +import { categoryToKind, NativePythonEnvironmentKind } from './base/locators/common/nativePythonUtils'; +import { getCondaEnvDirs, getCondaPathSetting, setCondaBinary } from './common/environmentManagers/conda'; +import { setPyEnvBinary } from './common/environmentManagers/pyenv'; +import { + createPythonWatcher, + PythonGlobalEnvEvent, + PythonWorkspaceEnvEvent, +} from './base/locators/common/pythonWatcher'; +import { getWorkspaceFolders, onDidChangeWorkspaceFolders } from '../common/vscodeApis/workspaceApis'; + +function makeExecutablePath(prefix?: string): string { + if (!prefix) { + return process.platform === 'win32' ? 'python.exe' : 'python'; + } + return process.platform === 'win32' ? path.join(prefix, 'python.exe') : path.join(prefix, 'python'); +} + +function toArch(a: string | undefined): Architecture { + switch (a) { + case 'x86': + return Architecture.x86; + case 'x64': + return Architecture.x64; + default: + return Architecture.Unknown; + } +} + +function getLocation(nativeEnv: NativeEnvInfo, executable: string): string { + if (nativeEnv.kind === NativePythonEnvironmentKind.Conda) { + return nativeEnv.prefix ?? path.dirname(executable); + } + + if (nativeEnv.executable) { + return nativeEnv.executable; + } + + if (nativeEnv.prefix) { + return nativeEnv.prefix; + } + + // This is a path to a generated executable. Needed for backwards compatibility. + return executable; +} + +function kindToShortString(kind: PythonEnvKind): string | undefined { + switch (kind) { + case PythonEnvKind.Poetry: + return 'poetry'; + case PythonEnvKind.Pyenv: + return 'pyenv'; + case PythonEnvKind.VirtualEnv: + case PythonEnvKind.Venv: + case PythonEnvKind.VirtualEnvWrapper: + case PythonEnvKind.OtherVirtual: + return 'venv'; + case PythonEnvKind.Pipenv: + return 'pipenv'; + case PythonEnvKind.Conda: + return 'conda'; + case PythonEnvKind.ActiveState: + return 'active-state'; + case PythonEnvKind.MicrosoftStore: + return 'Microsoft Store'; + case PythonEnvKind.Hatch: + return 'hatch'; + case PythonEnvKind.Pixi: + return 'pixi'; + case PythonEnvKind.System: + case PythonEnvKind.Unknown: + case PythonEnvKind.OtherGlobal: + case PythonEnvKind.Custom: + default: + return undefined; + } +} + +function toShortVersionString(version: PythonVersion): string { + return `${version.major}.${version.minor}.${version.micro}`.trim(); +} + +function getDisplayName(version: PythonVersion, kind: PythonEnvKind, arch: Architecture, name?: string): string { + const versionStr = toShortVersionString(version); + const kindStr = kindToShortString(kind); + if (arch === Architecture.x86) { + if (kindStr) { + return name ? `Python ${versionStr} 32-bit (${name})` : `Python ${versionStr} 32-bit (${kindStr})`; + } + return name ? `Python ${versionStr} 32-bit (${name})` : `Python ${versionStr} 32-bit`; + } + if (kindStr) { + return name ? `Python ${versionStr} (${name})` : `Python ${versionStr} (${kindStr})`; + } + return name ? `Python ${versionStr} (${name})` : `Python ${versionStr}`; +} + +function validEnv(nativeEnv: NativeEnvInfo): boolean { + if (nativeEnv.prefix === undefined && nativeEnv.executable === undefined) { + traceError(`Invalid environment [native]: ${JSON.stringify(nativeEnv)}`); + return false; + } + return true; +} + +function getEnvType(kind: PythonEnvKind): PythonEnvType | undefined { + switch (kind) { + case PythonEnvKind.Poetry: + case PythonEnvKind.Pyenv: + case PythonEnvKind.VirtualEnv: + case PythonEnvKind.Venv: + case PythonEnvKind.VirtualEnvWrapper: + case PythonEnvKind.OtherVirtual: + case PythonEnvKind.Pipenv: + case PythonEnvKind.ActiveState: + case PythonEnvKind.Hatch: + case PythonEnvKind.Pixi: + return PythonEnvType.Virtual; + + case PythonEnvKind.Conda: + return PythonEnvType.Conda; + + case PythonEnvKind.System: + case PythonEnvKind.Unknown: + case PythonEnvKind.OtherGlobal: + case PythonEnvKind.Custom: + case PythonEnvKind.MicrosoftStore: + default: + return undefined; + } +} + +function isSubDir(pathToCheck: string | undefined, parents: string[]): boolean { + return parents.some((prefix) => { + if (pathToCheck) { + return path.normalize(pathToCheck).startsWith(path.normalize(prefix)); + } + return false; + }); +} + +function foundOnPath(fsPath: string): boolean { + const paths = getPathEnvVariable().map((p) => path.normalize(p).toLowerCase()); + const normalized = path.normalize(fsPath).toLowerCase(); + return paths.some((p) => normalized.includes(p)); +} + +function getName(nativeEnv: NativeEnvInfo, kind: PythonEnvKind, condaEnvDirs: string[]): string { + if (nativeEnv.name) { + return nativeEnv.name; + } + + const envType = getEnvType(kind); + if (nativeEnv.prefix && envType === PythonEnvType.Virtual) { + return path.basename(nativeEnv.prefix); + } + + if (nativeEnv.prefix && envType === PythonEnvType.Conda) { + if (nativeEnv.name === 'base') { + return 'base'; + } + + const workspaces = (getWorkspaceFolders() ?? []).map((wf) => wf.uri.fsPath); + if (isSubDir(nativeEnv.prefix, workspaces)) { + traceInfo(`Conda env is --prefix environment: ${nativeEnv.prefix}`); + return ''; + } + + if (condaEnvDirs.length > 0 && isSubDir(nativeEnv.prefix, condaEnvDirs)) { + traceInfo(`Conda env is --named environment: ${nativeEnv.prefix}`); + return path.basename(nativeEnv.prefix); + } + } + + return ''; +} + +function toPythonEnvInfo(nativeEnv: NativeEnvInfo, condaEnvDirs: string[]): PythonEnvInfo | undefined { + if (!validEnv(nativeEnv)) { + return undefined; + } + const kind = categoryToKind(nativeEnv.kind); + const arch = toArch(nativeEnv.arch); + const version: PythonVersion = parseVersion(nativeEnv.version ?? ''); + const name = getName(nativeEnv, kind, condaEnvDirs); + const displayName = nativeEnv.version + ? getDisplayName(version, kind, arch, name) + : nativeEnv.displayName ?? 'Python'; + + const executable = nativeEnv.executable ?? makeExecutablePath(nativeEnv.prefix); + return { + name, + location: getLocation(nativeEnv, executable), + kind, + id: executable, + executable: { + filename: executable, + sysPrefix: nativeEnv.prefix ?? '', + ctime: -1, + mtime: -1, + }, + version: { + sysVersion: nativeEnv.version, + major: version.major, + minor: version.minor, + micro: version.micro, + }, + arch, + distro: { + org: '', + }, + source: [], + detailedDisplayName: displayName, + display: displayName, + type: getEnvType(kind), + }; +} + +function hasChanged(old: PythonEnvInfo, newEnv: PythonEnvInfo): boolean { + if (old.name !== newEnv.name) { + return true; + } + if (old.executable.filename !== newEnv.executable.filename) { + return true; + } + if (old.version.major !== newEnv.version.major) { + return true; + } + if (old.version.minor !== newEnv.version.minor) { + return true; + } + if (old.version.micro !== newEnv.version.micro) { + return true; + } + if (old.location !== newEnv.location) { + return true; + } + if (old.kind !== newEnv.kind) { + return true; + } + if (old.arch !== newEnv.arch) { + return true; + } + + return false; +} + +class NativePythonEnvironments implements IDiscoveryAPI, Disposable { + private _onProgress: EventEmitter; + + private _onChanged: EventEmitter; + + private _refreshPromise?: Deferred; + + private _envs: PythonEnvInfo[] = []; + + private _disposables: Disposable[] = []; + + private _condaEnvDirs: string[] = []; + + constructor(private readonly finder: NativePythonFinder) { + this._onProgress = new EventEmitter(); + this._onChanged = new EventEmitter(); + + this.onProgress = this._onProgress.event; + this.onChanged = this._onChanged.event; + + this.refreshState = ProgressReportStage.idle; + this._disposables.push(this._onProgress, this._onChanged); + + this.initializeWatcher(); + } + + dispose(): void { + this._disposables.forEach((d) => d.dispose()); + } + + refreshState: ProgressReportStage; + + onProgress: Event; + + onChanged: Event; + + getRefreshPromise(_options?: GetRefreshEnvironmentsOptions): Promise | undefined { + return this._refreshPromise?.promise; + } + + triggerRefresh(_query?: PythonLocatorQuery, _options?: TriggerRefreshOptions): Promise { + const stopwatch = new StopWatch(); + traceLog('Native locator: Refresh started'); + if (this.refreshState === ProgressReportStage.discoveryStarted && this._refreshPromise?.promise) { + return this._refreshPromise?.promise; + } + + this.refreshState = ProgressReportStage.discoveryStarted; + this._onProgress.fire({ stage: this.refreshState }); + this._refreshPromise = createDeferred(); + + setImmediate(async () => { + try { + const before = this._envs.map((env) => env.executable.filename); + const after: string[] = []; + for await (const native of this.finder.refresh()) { + const exe = this.processNative(native); + if (exe) { + after.push(exe); + } + } + const envsToRemove = before.filter((item) => !after.includes(item)); + envsToRemove.forEach((item) => this.removeEnv(item)); + this._refreshPromise?.resolve(); + } catch (error) { + this._refreshPromise?.reject(error); + } finally { + traceLog(`Native locator: Refresh finished in ${stopwatch.elapsedTime} ms`); + this.refreshState = ProgressReportStage.discoveryFinished; + this._refreshPromise = undefined; + this._onProgress.fire({ stage: this.refreshState }); + } + }); + + return this._refreshPromise?.promise; + } + + private processNative(native: NativeEnvInfo | NativeEnvManagerInfo): string | undefined { + if (isNativeEnvInfo(native)) { + return this.processEnv(native); + } + this.processEnvManager(native); + + return undefined; + } + + private processEnv(native: NativeEnvInfo): string | undefined { + if (!validEnv(native)) { + return undefined; + } + + try { + const version = native.version ? parseVersion(native.version) : undefined; + + if (categoryToKind(native.kind) === PythonEnvKind.Conda && !native.executable) { + // This is a conda env without python, no point trying to resolve this. + // There is nothing to resolve + return this.addEnv(native)?.executable.filename; + } + if (native.executable && (!version || version.major < 0 || version.minor < 0 || version.micro < 0)) { + // We have a path, but no version info, try to resolve the environment. + this.finder + .resolve(native.executable) + .then((env) => { + if (env) { + this.addEnv(env); + } + }) + .ignoreErrors(); + return native.executable; + } + if (native.executable && version && version.major >= 0 && version.minor >= 0 && version.micro >= 0) { + return this.addEnv(native)?.executable.filename; + } + traceError(`Failed to process environment: ${JSON.stringify(native)}`); + } catch (err) { + traceError(`Failed to process environment: ${err}`); + } + return undefined; + } + + private condaPathAlreadySet: string | undefined; + + // eslint-disable-next-line class-methods-use-this + private processEnvManager(native: NativeEnvManagerInfo) { + const tool = native.tool.toLowerCase(); + switch (tool) { + case 'conda': + { + traceLog(`Conda environment manager found at: ${native.executable}`); + const settingPath = getCondaPathSetting(); + if (!this.condaPathAlreadySet) { + if (settingPath === '' || settingPath === undefined) { + if (foundOnPath(native.executable)) { + setCondaBinary(native.executable); + this.condaPathAlreadySet = native.executable; + traceInfo(`Using conda: ${native.executable}`); + } else { + traceInfo(`Conda not found on PATH, skipping: ${native.executable}`); + traceInfo( + 'You can set the path to conda using the setting: `python.condaPath` if you want to use a different conda binary', + ); + } + } else { + traceInfo(`Using conda from setting: ${settingPath}`); + this.condaPathAlreadySet = settingPath; + } + } else { + traceInfo(`Conda set to: ${this.condaPathAlreadySet}`); + } + } + break; + case 'pyenv': + traceLog(`Pyenv environment manager found at: ${native.executable}`); + setPyEnvBinary(native.executable); + break; + case 'poetry': + traceLog(`Poetry environment manager found at: ${native.executable}`); + break; + default: + traceWarn(`Unknown environment manager: ${native.tool}`); + break; + } + } + + getEnvs(_query?: PythonLocatorQuery): PythonEnvInfo[] { + return this._envs; + } + + private addEnv(native: NativeEnvInfo, searchLocation?: Uri): PythonEnvInfo | undefined { + const info = toPythonEnvInfo(native, this._condaEnvDirs); + if (info) { + const old = this._envs.find((item) => item.executable.filename === info.executable.filename); + if (old) { + this._envs = this._envs.filter((item) => item.executable.filename !== info.executable.filename); + this._envs.push(info); + if (hasChanged(old, info)) { + this._onChanged.fire({ type: FileChangeType.Changed, old, new: info, searchLocation }); + } + } else { + this._envs.push(info); + this._onChanged.fire({ type: FileChangeType.Created, new: info, searchLocation }); + } + } + + return info; + } + + private removeEnv(env: PythonEnvInfo | string): void { + if (typeof env === 'string') { + const old = this._envs.find((item) => item.executable.filename === env); + this._envs = this._envs.filter((item) => item.executable.filename !== env); + this._onChanged.fire({ type: FileChangeType.Deleted, old }); + return; + } + this._envs = this._envs.filter((item) => item.executable.filename !== env.executable.filename); + this._onChanged.fire({ type: FileChangeType.Deleted, old: env }); + } + + @cache(30_000, true) + async resolveEnv(envPath?: string): Promise { + if (envPath === undefined) { + return undefined; + } + try { + const native = await this.finder.resolve(envPath); + if (native) { + if (native.kind === NativePythonEnvironmentKind.Conda && this._condaEnvDirs.length === 0) { + this._condaEnvDirs = (await getCondaEnvDirs()) ?? []; + } + return this.addEnv(native); + } + return undefined; + } catch { + return undefined; + } + } + + private initializeWatcher(): void { + const watcher = createPythonWatcher(); + this._disposables.push( + watcher.onDidGlobalEnvChanged((e) => this.pathEventHandler(e)), + watcher.onDidWorkspaceEnvChanged(async (e) => { + await this.workspaceEventHandler(e); + }), + onDidChangeWorkspaceFolders((e: WorkspaceFoldersChangeEvent) => { + e.removed.forEach((wf) => watcher.unwatchWorkspace(wf)); + e.added.forEach((wf) => watcher.watchWorkspace(wf)); + }), + watcher, + ); + + getWorkspaceFolders()?.forEach((wf) => watcher.watchWorkspace(wf)); + const home = getUserHomeDir(); + if (home) { + watcher.watchPath(Uri.file(path.join(home, '.conda', 'environments.txt'))); + } + } + + private async pathEventHandler(e: PythonGlobalEnvEvent): Promise { + if (e.type === FileChangeType.Created || e.type === FileChangeType.Changed) { + if (e.uri.fsPath.endsWith('environment.txt')) { + const before = this._envs + .filter((env) => env.kind === PythonEnvKind.Conda) + .map((env) => env.executable.filename); + for await (const native of this.finder.refresh(NativePythonEnvironmentKind.Conda)) { + this.processNative(native); + } + const after = this._envs + .filter((env) => env.kind === PythonEnvKind.Conda) + .map((env) => env.executable.filename); + const envsToRemove = before.filter((item) => !after.includes(item)); + envsToRemove.forEach((item) => this.removeEnv(item)); + } + } + } + + private async workspaceEventHandler(e: PythonWorkspaceEnvEvent): Promise { + if (e.type === FileChangeType.Created || e.type === FileChangeType.Changed) { + const native = await this.finder.resolve(e.executable); + if (native) { + this.addEnv(native, e.workspaceFolder.uri); + } + } else { + this.removeEnv(e.executable); + } + } +} + +export function createNativeEnvironmentsApi(finder: NativePythonFinder): IDiscoveryAPI & Disposable { + const native = new NativePythonEnvironments(finder); + native.triggerRefresh().ignoreErrors(); + return native; +} diff --git a/src/client/repl/nativeRepl.ts b/src/client/repl/nativeRepl.ts new file mode 100644 index 000000000000..3f8a085da467 --- /dev/null +++ b/src/client/repl/nativeRepl.ts @@ -0,0 +1,267 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +// Native Repl class that holds instance of pythonServer and replController + +import { NotebookController, NotebookDocument, QuickPickItem, TextEditor, Uri, WorkspaceFolder } from 'vscode'; +import * as path from 'path'; +import { Disposable } from 'vscode-jsonrpc'; +import { PVSC_EXTENSION_ID } from '../common/constants'; +import { showNotebookDocument, showQuickPick } from '../common/vscodeApis/windowApis'; +import { getWorkspaceFolders, onDidCloseNotebookDocument } from '../common/vscodeApis/workspaceApis'; +import { PythonEnvironment } from '../pythonEnvironments/info'; +import { createPythonServer, PythonServer } from './pythonServer'; +import { executeNotebookCell, openInteractiveREPL, selectNotebookKernel } from './replCommandHandler'; +import { createReplController } from './replController'; +import { EventName } from '../telemetry/constants'; +import { sendTelemetryEvent } from '../telemetry'; +import { VariablesProvider } from './variables/variablesProvider'; +import { VariableRequester } from './variables/variableRequester'; +import { getTabNameForUri } from './replUtils'; +import { getWorkspaceStateValue, updateWorkspaceStateValue } from '../common/persistentState'; +import { onDidChangeEnvironmentEnvExt, useEnvExtension } from '../envExt/api.internal'; +import { getActiveInterpreterLegacy } from '../envExt/api.legacy'; + +export const NATIVE_REPL_URI_MEMENTO = 'nativeReplUri'; +let nativeRepl: NativeRepl | undefined; +export class NativeRepl implements Disposable { + // Adding ! since it will get initialized in create method, not the constructor. + private pythonServer!: PythonServer; + + private cwd: string | undefined; + + private interpreter!: PythonEnvironment; + + private disposables: Disposable[] = []; + + private replController!: NotebookController; + + private notebookDocument: NotebookDocument | undefined; + + public newReplSession: boolean | undefined = true; + + private envChangeListenerRegistered = false; + + private pendingInterpreterChange?: { resource?: Uri }; + + // TODO: In the future, could also have attribute of URI for file specific REPL. + private constructor() { + this.watchNotebookClosed(); + } + + // Static async factory method to handle asynchronous initialization + public static async create(interpreter: PythonEnvironment): Promise { + const nativeRepl = new NativeRepl(); + nativeRepl.interpreter = interpreter; + await nativeRepl.setReplDirectory(); + nativeRepl.pythonServer = createPythonServer([interpreter.path as string], nativeRepl.cwd); + nativeRepl.disposables.push(nativeRepl.pythonServer); + nativeRepl.setReplController(); + nativeRepl.registerInterpreterChangeHandler(); + + return nativeRepl; + } + + dispose(): void { + this.disposables.forEach((d) => d.dispose()); + } + + /** + * Function that watches for Notebook Closed event. + * This is for the purposes of correctly updating the notebookEditor and notebookDocument on close. + */ + private watchNotebookClosed(): void { + this.disposables.push( + onDidCloseNotebookDocument(async (nb) => { + if (this.notebookDocument && nb.uri.toString() === this.notebookDocument.uri.toString()) { + this.notebookDocument = undefined; + this.newReplSession = true; + await updateWorkspaceStateValue(NATIVE_REPL_URI_MEMENTO, undefined); + this.pythonServer.dispose(); + this.pythonServer = createPythonServer([this.interpreter.path as string], this.cwd); + this.disposables.push(this.pythonServer); + if (this.replController) { + this.replController.dispose(); + } + nativeRepl = undefined; + } + }), + ); + } + + /** + * Function that set up desired directory for REPL. + * If there is multiple workspaces, prompt the user to choose + * which directory we should set in context of native REPL. + */ + private async setReplDirectory(): Promise { + // Figure out uri via workspaceFolder as uri parameter always + // seem to be undefined from parameter when trying to access from replCommands.ts + const workspaces: readonly WorkspaceFolder[] | undefined = getWorkspaceFolders(); + + if (workspaces) { + // eslint-disable-next-line no-shadow + const workspacesQuickPickItems: QuickPickItem[] = workspaces.map((workspace) => ({ + label: workspace.name, + description: workspace.uri.fsPath, + })); + + if (workspacesQuickPickItems.length === 0) { + this.cwd = process.cwd(); // Yields '/' on no workspace scenario. + } else if (workspacesQuickPickItems.length === 1) { + this.cwd = workspacesQuickPickItems[0].description; + } else { + // Show choices of workspaces for user to choose from. + const selection = (await showQuickPick(workspacesQuickPickItems, { + placeHolder: 'Select current working directory for new REPL', + matchOnDescription: true, + ignoreFocusOut: true, + })) as QuickPickItem; + this.cwd = selection?.description; + } + } + } + + /** + * Function that check if NotebookController for REPL exists, and returns it in Singleton manner. + */ + public setReplController(force: boolean = false): NotebookController { + if (!this.replController || force) { + this.replController = createReplController(this.interpreter!.path, this.disposables, this.cwd); + this.replController.variableProvider = new VariablesProvider( + new VariableRequester(this.pythonServer), + () => this.notebookDocument, + this.pythonServer.onCodeExecuted, + ); + } + return this.replController; + } + + private registerInterpreterChangeHandler(): void { + if (!useEnvExtension() || this.envChangeListenerRegistered) { + return; + } + this.envChangeListenerRegistered = true; + this.disposables.push( + onDidChangeEnvironmentEnvExt((event) => { + this.updateInterpreterForChange(event.uri).catch(() => undefined); + }), + ); + this.disposables.push( + this.pythonServer.onCodeExecuted(() => { + if (this.pendingInterpreterChange) { + const { resource } = this.pendingInterpreterChange; + this.pendingInterpreterChange = undefined; + this.updateInterpreterForChange(resource).catch(() => undefined); + } + }), + ); + } + + private async updateInterpreterForChange(resource?: Uri): Promise { + if (this.pythonServer?.isExecuting) { + this.pendingInterpreterChange = { resource }; + return; + } + if (!this.shouldApplyInterpreterChange(resource)) { + return; + } + const scope = resource ?? (this.cwd ? Uri.file(this.cwd) : undefined); + const interpreter = await getActiveInterpreterLegacy(scope); + if (!interpreter || interpreter.path === this.interpreter?.path) { + return; + } + + this.interpreter = interpreter; + this.pythonServer.dispose(); + this.pythonServer = createPythonServer([interpreter.path as string], this.cwd); + this.disposables.push(this.pythonServer); + if (this.replController) { + this.replController.dispose(); + } + this.setReplController(true); + + if (this.notebookDocument) { + const notebookEditor = await showNotebookDocument(this.notebookDocument, { preserveFocus: true }); + await selectNotebookKernel(notebookEditor, this.replController.id, PVSC_EXTENSION_ID); + } + } + + private shouldApplyInterpreterChange(resource?: Uri): boolean { + if (!resource || !this.cwd) { + return true; + } + const relative = path.relative(this.cwd, resource.fsPath); + return relative === '' || (!relative.startsWith('..') && !path.isAbsolute(relative)); + } + + /** + * Function that checks if native REPL's text input box contains complete code. + * @returns Promise - True if complete/Valid code is present, False otherwise. + */ + public async checkUserInputCompleteCode(activeEditor: TextEditor | undefined): Promise { + let completeCode = false; + let userTextInput; + if (activeEditor) { + const { document } = activeEditor; + userTextInput = document.getText(); + } + + // Check if userTextInput is a complete Python command + if (userTextInput) { + completeCode = await this.pythonServer.checkValidCommand(userTextInput); + } + + return completeCode; + } + + /** + * Function that opens interactive repl, selects kernel, and send/execute code to the native repl. + */ + public async sendToNativeRepl(code?: string | undefined, preserveFocus: boolean = true): Promise { + let wsMementoUri: Uri | undefined; + + if (!this.notebookDocument) { + const wsMemento = getWorkspaceStateValue(NATIVE_REPL_URI_MEMENTO); + wsMementoUri = wsMemento ? Uri.parse(wsMemento) : undefined; + + if (!wsMementoUri || getTabNameForUri(wsMementoUri) !== 'Python REPL') { + await updateWorkspaceStateValue(NATIVE_REPL_URI_MEMENTO, undefined); + wsMementoUri = undefined; + } + } + + const result = await openInteractiveREPL(this.notebookDocument ?? wsMementoUri, preserveFocus); + if (result) { + this.notebookDocument = result.notebookEditor.notebook; + await updateWorkspaceStateValue( + NATIVE_REPL_URI_MEMENTO, + this.notebookDocument.uri.toString(), + ); + + if (result.documentCreated) { + await selectNotebookKernel(result.notebookEditor, this.replController.id, PVSC_EXTENSION_ID); + } + if (code) { + await executeNotebookCell(result.notebookEditor, code); + } + } + } +} + +/** + * Get Singleton Native REPL Instance + * @param interpreter + * @returns Native REPL instance + */ +export async function getNativeRepl(interpreter: PythonEnvironment, disposables: Disposable[]): Promise { + if (!nativeRepl) { + nativeRepl = await NativeRepl.create(interpreter); + disposables.push(nativeRepl); + } + if (nativeRepl && nativeRepl.newReplSession) { + sendTelemetryEvent(EventName.REPL, undefined, { replType: 'Native' }); + nativeRepl.newReplSession = false; + } + return nativeRepl; +} diff --git a/src/client/repl/pythonServer.ts b/src/client/repl/pythonServer.ts new file mode 100644 index 000000000000..c4b1722b5079 --- /dev/null +++ b/src/client/repl/pythonServer.ts @@ -0,0 +1,168 @@ +import * as path from 'path'; +import * as ch from 'child_process'; +import * as rpc from 'vscode-jsonrpc/node'; +import { Disposable, Event, EventEmitter, window } from 'vscode'; +import { EXTENSION_ROOT_DIR } from '../constants'; +import { traceError, traceLog } from '../logging'; +import { captureTelemetry } from '../telemetry'; +import { EventName } from '../telemetry/constants'; + +const SERVER_PATH = path.join(EXTENSION_ROOT_DIR, 'python_files', 'python_server.py'); +let serverInstance: PythonServer | undefined; +export interface ExecutionResult { + status: boolean; + output: string; +} + +export interface PythonServer extends Disposable { + onCodeExecuted: Event; + readonly isExecuting: boolean; + readonly isDisposed: boolean; + execute(code: string): Promise; + executeSilently(code: string): Promise; + interrupt(): void; + input(): void; + checkValidCommand(code: string): Promise; +} + +class PythonServerImpl implements PythonServer, Disposable { + private readonly disposables: Disposable[] = []; + + private readonly _onCodeExecuted = new EventEmitter(); + + onCodeExecuted = this._onCodeExecuted.event; + + private inFlightRequests = 0; + + private disposed = false; + + public get isExecuting(): boolean { + return this.inFlightRequests > 0; + } + + public get isDisposed(): boolean { + return this.disposed; + } + + constructor(private connection: rpc.MessageConnection, private pythonServer: ch.ChildProcess) { + this.initialize(); + this.input(); + } + + private initialize(): void { + this.disposables.push( + this.connection.onNotification('log', (message: string) => { + traceLog('Log:', message); + }), + ); + this.pythonServer.on('exit', (code) => { + traceError(`Python server exited with code ${code}`); + this.markDisposed(); + }); + this.pythonServer.on('error', (err) => { + traceError(err); + this.markDisposed(); + }); + this.connection.listen(); + } + + public input(): void { + // Register input request handler + this.connection.onRequest('input', async (request) => { + // Ask for user input via popup quick input, send it back to Python + let userPrompt = 'Enter your input here: '; + if (request && request.prompt) { + userPrompt = request.prompt; + } + const input = await window.showInputBox({ + title: 'Input Request', + prompt: userPrompt, + ignoreFocusOut: true, + }); + return { userInput: input }; + }); + } + + @captureTelemetry(EventName.EXECUTION_CODE, { scope: 'selection' }, false) + public async execute(code: string): Promise { + const result = await this.executeCode(code); + if (result?.status) { + this._onCodeExecuted.fire(); + } + return result; + } + + public executeSilently(code: string): Promise { + return this.executeCode(code); + } + + private async executeCode(code: string): Promise { + this.inFlightRequests += 1; + try { + const result = await this.connection.sendRequest('execute', code); + return result as ExecutionResult; + } catch (err) { + const error = err as Error; + traceError(`Error getting response from REPL server:`, error); + } finally { + this.inFlightRequests -= 1; + } + return undefined; + } + + public interrupt(): void { + // Passing SIGINT to interrupt only would work for Mac and Linux + if (this.pythonServer.kill('SIGINT')) { + traceLog('Python REPL server interrupted'); + } + } + + public async checkValidCommand(code: string): Promise { + this.inFlightRequests += 1; + try { + const completeCode: ExecutionResult = await this.connection.sendRequest('check_valid_command', code); + return completeCode.output === 'True'; + } finally { + this.inFlightRequests -= 1; + } + } + + public dispose(): void { + if (this.disposed) { + return; + } + this.disposed = true; + this.connection.sendNotification('exit'); + this.disposables.forEach((d) => d.dispose()); + this.connection.dispose(); + serverInstance = undefined; + } + + private markDisposed(): void { + if (this.disposed) { + return; + } + this.disposed = true; + this.connection.dispose(); + serverInstance = undefined; + } +} + +export function createPythonServer(interpreter: string[], cwd?: string): PythonServer { + if (serverInstance && !serverInstance.isDisposed) { + return serverInstance; + } + + const pythonServer = ch.spawn(interpreter[0], [...interpreter.slice(1), SERVER_PATH], { + cwd, // Launch with correct workspace directory + }); + pythonServer.stderr.on('data', (data) => { + traceError(data.toString()); + }); + const connection = rpc.createMessageConnection( + new rpc.StreamMessageReader(pythonServer.stdout), + new rpc.StreamMessageWriter(pythonServer.stdin), + ); + serverInstance = new PythonServerImpl(connection, pythonServer); + return serverInstance; +} diff --git a/src/client/repl/replCommandHandler.ts b/src/client/repl/replCommandHandler.ts new file mode 100644 index 000000000000..630eddfdd565 --- /dev/null +++ b/src/client/repl/replCommandHandler.ts @@ -0,0 +1,98 @@ +import { + NotebookEditor, + ViewColumn, + NotebookDocument, + NotebookCellData, + NotebookCellKind, + NotebookEdit, + WorkspaceEdit, + Uri, +} from 'vscode'; +import { getExistingReplViewColumn, getTabNameForUri } from './replUtils'; +import { showNotebookDocument } from '../common/vscodeApis/windowApis'; +import { openNotebookDocument, applyEdit } from '../common/vscodeApis/workspaceApis'; +import { executeCommand } from '../common/vscodeApis/commandApis'; + +/** + * Function that opens/show REPL using IW UI. + */ +export async function openInteractiveREPL( + notebookDocument: NotebookDocument | Uri | undefined, + preserveFocus: boolean = true, +): Promise<{ notebookEditor: NotebookEditor; documentCreated: boolean } | undefined> { + let viewColumn = ViewColumn.Beside; + let alreadyExists = false; + if (notebookDocument instanceof Uri) { + // Case where NotebookDocument is undefined, but workspace mementoURI exists. + notebookDocument = await openNotebookDocument(notebookDocument); + } else if (notebookDocument) { + // Case where NotebookDocument (REPL document already exists in the tab) + const existingReplViewColumn = getExistingReplViewColumn(notebookDocument); + viewColumn = existingReplViewColumn ?? viewColumn; + alreadyExists = true; + } else if (!notebookDocument) { + // Case where NotebookDocument doesnt exist, or + // became outdated (untitled.ipynb created without Python extension knowing, effectively taking over original Python REPL's URI) + notebookDocument = await openNotebookDocument('jupyter-notebook'); + } + + const notebookEditor = await showNotebookDocument(notebookDocument!, { + viewColumn, + asRepl: 'Python REPL', + preserveFocus, + }); + + // Sanity check that we opened a Native REPL from showNotebookDocument. + if ( + !notebookEditor || + !notebookEditor.notebook || + !notebookEditor.notebook.uri || + getTabNameForUri(notebookEditor.notebook.uri) !== 'Python REPL' + ) { + return undefined; + } + + return { notebookEditor, documentCreated: !alreadyExists }; +} + +/** + * Function that selects notebook Kernel. + */ +export async function selectNotebookKernel( + notebookEditor: NotebookEditor, + notebookControllerId: string, + extensionId: string, +): Promise { + await executeCommand('notebook.selectKernel', { + notebookEditor, + id: notebookControllerId, + extension: extensionId, + }); +} + +/** + * Function that executes notebook cell given code. + */ +export async function executeNotebookCell(notebookEditor: NotebookEditor, code: string): Promise { + const { notebook, replOptions } = notebookEditor; + const cellIndex = replOptions?.appendIndex ?? notebook.cellCount; + await addCellToNotebook(notebook, cellIndex, code); + // Execute the cell + executeCommand('notebook.cell.execute', { + ranges: [{ start: cellIndex, end: cellIndex + 1 }], + document: notebook.uri, + }); +} + +/** + * Function that adds cell to notebook. + * This function will only get called when notebook document is defined. + */ +async function addCellToNotebook(notebookDocument: NotebookDocument, index: number, code: string): Promise { + const notebookCellData = new NotebookCellData(NotebookCellKind.Code, code as string, 'python'); + // Add new cell to interactive window document + const notebookEdit = NotebookEdit.insertCells(index, [notebookCellData]); + const workspaceEdit = new WorkspaceEdit(); + workspaceEdit.set(notebookDocument!.uri, [notebookEdit]); + await applyEdit(workspaceEdit); +} diff --git a/src/client/repl/replCommands.ts b/src/client/repl/replCommands.ts new file mode 100644 index 000000000000..1171e9466ee8 --- /dev/null +++ b/src/client/repl/replCommands.ts @@ -0,0 +1,131 @@ +import { commands, Uri, window } from 'vscode'; +import { Disposable } from 'vscode-jsonrpc'; +import { ICommandManager } from '../common/application/types'; +import { Commands } from '../common/constants'; +import { IInterpreterService } from '../interpreter/contracts'; +import { ICodeExecutionHelper } from '../terminals/types'; +import { getNativeRepl } from './nativeRepl'; +import { + executeInTerminal, + getActiveInterpreter, + getSelectedTextToExecute, + getSendToNativeREPLSetting, + insertNewLineToREPLInput, + isMultiLineText, +} from './replUtils'; +import { registerCommand } from '../common/vscodeApis/commandApis'; +import { sendTelemetryEvent } from '../telemetry'; +import { EventName } from '../telemetry/constants'; +import { ReplType } from './types'; + +/** + * Register Start Native REPL command in the command palette + */ +export async function registerStartNativeReplCommand( + disposables: Disposable[], + interpreterService: IInterpreterService, +): Promise { + disposables.push( + registerCommand(Commands.Start_Native_REPL, async (uri: Uri) => { + sendTelemetryEvent(EventName.REPL, undefined, { replType: 'Native' }); + const interpreter = await getActiveInterpreter(uri, interpreterService); + if (interpreter) { + const nativeRepl = await getNativeRepl(interpreter, disposables); + await nativeRepl.sendToNativeRepl(undefined, false); + } + }), + ); +} + +/** + * Registers REPL command for shift+enter if sendToNativeREPL setting is enabled. + */ +export async function registerReplCommands( + disposables: Disposable[], + interpreterService: IInterpreterService, + executionHelper: ICodeExecutionHelper, + commandManager: ICommandManager, +): Promise { + disposables.push( + commandManager.registerCommand(Commands.Exec_In_REPL, async (uri: Uri) => { + const nativeREPLSetting = getSendToNativeREPLSetting(); + + if (!nativeREPLSetting) { + await executeInTerminal(); + return; + } + const interpreter = await getActiveInterpreter(uri, interpreterService); + + if (interpreter) { + const nativeRepl = await getNativeRepl(interpreter, disposables); + const activeEditor = window.activeTextEditor; + if (activeEditor) { + const code = await getSelectedTextToExecute(activeEditor); + if (code) { + // Smart Send + let wholeFileContent = ''; + if (activeEditor && activeEditor.document) { + wholeFileContent = activeEditor.document.getText(); + } + const normalizedCode = await executionHelper.normalizeLines( + code!, + ReplType.native, + wholeFileContent, + ); + await nativeRepl.sendToNativeRepl(normalizedCode); + } + } + } + }), + ); +} + +/** + * Command triggered for 'Enter': Conditionally call interactive.execute OR insert \n in text input box. + */ +export async function registerReplExecuteOnEnter( + disposables: Disposable[], + interpreterService: IInterpreterService, + commandManager: ICommandManager, +): Promise { + disposables.push( + commandManager.registerCommand(Commands.Exec_In_REPL_Enter, async (uri: Uri) => { + await onInputEnter(uri, 'repl.execute', interpreterService, disposables); + }), + ); + disposables.push( + commandManager.registerCommand(Commands.Exec_In_IW_Enter, async (uri: Uri) => { + await onInputEnter(uri, 'interactive.execute', interpreterService, disposables); + }), + ); +} + +async function onInputEnter( + uri: Uri | undefined, + commandName: string, + interpreterService: IInterpreterService, + disposables: Disposable[], +): Promise { + const interpreter = await getActiveInterpreter(uri, interpreterService); + if (!interpreter) { + return; + } + + const nativeRepl = await getNativeRepl(interpreter, disposables); + const completeCode = await nativeRepl?.checkUserInputCompleteCode(window.activeTextEditor); + const editor = window.activeTextEditor; + + if (editor) { + // Execute right away when complete code and Not multi-line + if (completeCode && !isMultiLineText(editor)) { + await commands.executeCommand(commandName); + } else { + insertNewLineToREPLInput(editor); + + // Handle case when user enters on blank line, just trigger interactive.execute + if (editor && editor.document.lineAt(editor.selection.active.line).text === '') { + await commands.executeCommand(commandName); + } + } + } +} diff --git a/src/client/repl/replController.ts b/src/client/repl/replController.ts new file mode 100644 index 000000000000..f30b8d9cbf6f --- /dev/null +++ b/src/client/repl/replController.ts @@ -0,0 +1,40 @@ +import * as vscode from 'vscode'; +import { createPythonServer } from './pythonServer'; + +export function createReplController( + interpreterPath: string, + disposables: vscode.Disposable[], + cwd?: string, +): vscode.NotebookController { + const server = createPythonServer([interpreterPath], cwd); + disposables.push(server); + + const controller = vscode.notebooks.createNotebookController('pythonREPL', 'jupyter-notebook', 'Python REPL'); + controller.supportedLanguages = ['python']; + + controller.description = 'Python REPL'; + + controller.interruptHandler = async () => { + server.interrupt(); + }; + + controller.executeHandler = async (cells) => { + for (const cell of cells) { + const exec = controller.createNotebookCellExecution(cell); + exec.start(Date.now()); + + const result = await server.execute(cell.document.getText()); + + if (result?.output) { + exec.replaceOutput([ + new vscode.NotebookCellOutput([vscode.NotebookCellOutputItem.text(result.output, 'text/plain')]), + ]); + // TODO: Properly update via NotebookCellOutputItem.error later. + } + + exec.end(result?.status); + } + }; + disposables.push(controller); + return controller; +} diff --git a/src/client/repl/replUtils.ts b/src/client/repl/replUtils.ts new file mode 100644 index 000000000000..93ae6f2a4573 --- /dev/null +++ b/src/client/repl/replUtils.ts @@ -0,0 +1,135 @@ +import { NotebookDocument, TextEditor, Selection, Uri, commands, window, TabInputNotebook, ViewColumn } from 'vscode'; +import { Commands } from '../common/constants'; +import { noop } from '../common/utils/misc'; +import { getActiveResource } from '../common/vscodeApis/windowApis'; +import { getConfiguration } from '../common/vscodeApis/workspaceApis'; +import { IInterpreterService } from '../interpreter/contracts'; +import { PythonEnvironment } from '../pythonEnvironments/info'; +import { getMultiLineSelectionText, getSingleLineSelectionText } from '../terminals/codeExecution/helper'; + +/** + * Function that executes selected code in the terminal. + */ +export async function executeInTerminal(): Promise { + await commands.executeCommand(Commands.Exec_Selection_In_Terminal); +} + +/** + * Function that returns selected text to execute in the REPL. + * @param textEditor + * @returns code - Code to execute in the REPL. + */ +export async function getSelectedTextToExecute(textEditor: TextEditor): Promise { + const { selection } = textEditor; + let code: string; + + if (selection.isEmpty) { + code = textEditor.document.lineAt(selection.start.line).text; + } else if (selection.isSingleLine) { + code = getSingleLineSelectionText(textEditor); + } else { + code = getMultiLineSelectionText(textEditor); + } + + return code; +} + +/** + * Function that returns user's Native REPL setting. + * @returns boolean - True if sendToNativeREPL setting is enabled, False otherwise. + */ +export function getSendToNativeREPLSetting(): boolean { + const uri = getActiveResource(); + const configuration = getConfiguration('python', uri); + return configuration.get('REPL.sendToNativeREPL', false); +} + +// Function that inserts new line in the given (input) text editor +export function insertNewLineToREPLInput(activeEditor: TextEditor | undefined): void { + if (activeEditor) { + const position = activeEditor.selection.active; + const newPosition = position.with(position.line, activeEditor.document.lineAt(position.line).text.length); + activeEditor.selection = new Selection(newPosition, newPosition); + + activeEditor.edit((editBuilder) => { + editBuilder.insert(newPosition, '\n'); + }); + } +} + +export function isMultiLineText(textEditor: TextEditor): boolean { + return (textEditor?.document?.lineCount ?? 0) > 1; +} + +/** + * Function that trigger interpreter warning if invalid interpreter. + * Function will also return undefined or active interpreter + */ +export async function getActiveInterpreter( + uri: Uri | undefined, + interpreterService: IInterpreterService, +): Promise { + const resource = uri ?? getActiveResource(); + const interpreter = await interpreterService.getActiveInterpreter(resource); + if (!interpreter) { + commands.executeCommand(Commands.TriggerEnvironmentSelection, resource).then(noop, noop); + return undefined; + } + return interpreter; +} + +/** + * Function that will return ViewColumn for existing Native REPL that belongs to given NotebookDocument. + */ +export function getExistingReplViewColumn(notebookDocument: NotebookDocument): ViewColumn | undefined { + const ourNotebookUri = notebookDocument.uri.toString(); + // Use Tab groups, to locate previously opened Python REPL tab and fetch view column. + const ourTb = window.tabGroups; + for (const tabGroup of ourTb.all) { + for (const tab of tabGroup.tabs) { + if (tab.label === 'Python REPL') { + const tabInput = (tab.input as unknown) as TabInputNotebook; + const tabUri = tabInput.uri.toString(); + if (tab.input && tabUri === ourNotebookUri) { + // This is the tab we are looking for. + const existingReplViewColumn = tab.group.viewColumn; + return existingReplViewColumn; + } + } + } + } + return undefined; +} + +/** + * Function that will return tab name for before reloading VS Code + * This is so we can make sure tab name is still 'Python REPL' after reloading VS Code, + * and make sure Python REPL does not get 'merged' into unaware untitled.ipynb tab. + */ +export function getTabNameForUri(uri: Uri): string | undefined { + const tabGroups = window.tabGroups.all; + + for (const tabGroup of tabGroups) { + for (const tab of tabGroup.tabs) { + if (tab.input instanceof TabInputNotebook && tab.input.uri.toString() === uri.toString()) { + return tab.label; + } + } + } + + return undefined; +} + +/** + * Function that will return the minor version of current active Python interpreter. + */ +export async function getPythonMinorVersion( + uri: Uri | undefined, + interpreterService: IInterpreterService, +): Promise { + if (uri) { + const pythonVersion = await getActiveInterpreter(uri, interpreterService); + return pythonVersion?.version?.minor; + } + return undefined; +} diff --git a/src/client/linters/prompts/types.ts b/src/client/repl/types.ts similarity index 52% rename from src/client/linters/prompts/types.ts rename to src/client/repl/types.ts index d7c884b3a00d..38de9bfe2137 100644 --- a/src/client/linters/prompts/types.ts +++ b/src/client/repl/types.ts @@ -1,6 +1,9 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -export interface IToolsExtensionPrompt { - showPrompt(): Promise; +'use strict'; + +export enum ReplType { + terminal = 'terminal', + native = 'native', } diff --git a/src/client/repl/variables/types.ts b/src/client/repl/variables/types.ts new file mode 100644 index 000000000000..1e3c80d32077 --- /dev/null +++ b/src/client/repl/variables/types.ts @@ -0,0 +1,17 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import { CancellationToken, Variable } from 'vscode'; + +export interface IVariableDescription extends Variable { + /** The name of the variable at the root scope */ + root: string; + /** How to look up the specific property of the root variable */ + propertyChain: (string | number)[]; + /** The number of children for collection types */ + count?: number; + /** Names of children */ + hasNamedChildren?: boolean; + /** A method to get the children of this variable */ + getChildren?: (start: number, token: CancellationToken) => Promise; +} diff --git a/src/client/repl/variables/variableRequester.ts b/src/client/repl/variables/variableRequester.ts new file mode 100644 index 000000000000..e66afdcd6616 --- /dev/null +++ b/src/client/repl/variables/variableRequester.ts @@ -0,0 +1,59 @@ +import { CancellationToken } from 'vscode'; +import path from 'path'; +import * as fsapi from '../../common/platform/fs-paths'; +import { IVariableDescription } from './types'; +import { PythonServer } from '../pythonServer'; +import { EXTENSION_ROOT_DIR } from '../../constants'; + +const VARIABLE_SCRIPT_LOCATION = path.join(EXTENSION_ROOT_DIR, 'python_files', 'get_variable_info.py'); + +export class VariableRequester { + public static scriptContents: string | undefined; + + constructor(private pythonServer: PythonServer) {} + + async getAllVariableDescriptions( + parent: IVariableDescription | undefined, + start: number, + token: CancellationToken, + ): Promise { + const scriptLines = (await getContentsOfVariablesScript()).split(/(?:\r\n|\n)/); + if (parent) { + const printCall = `import json;return json.dumps(getAllChildrenDescriptions(\'${ + parent.root + }\', ${JSON.stringify(parent.propertyChain)}, ${start}))`; + scriptLines.push(printCall); + } else { + scriptLines.push('import json;return json.dumps(getVariableDescriptions())'); + } + + if (token.isCancellationRequested) { + return []; + } + + const script = wrapScriptInFunction(scriptLines); + const result = await this.pythonServer.executeSilently(script); + + if (result?.output && !token.isCancellationRequested) { + return JSON.parse(result.output) as IVariableDescription[]; + } + + return []; + } +} + +function wrapScriptInFunction(scriptLines: string[]): string { + const indented = scriptLines.map((line) => ` ${line}`).join('\n'); + // put everything into a function scope and then delete that scope + // TODO: run in a background thread + return `def __VSCODE_run_script():\n${indented}\nprint(__VSCODE_run_script())\ndel __VSCODE_run_script`; +} + +async function getContentsOfVariablesScript(): Promise { + if (VariableRequester.scriptContents) { + return VariableRequester.scriptContents; + } + const contents = await fsapi.readFile(VARIABLE_SCRIPT_LOCATION, 'utf-8'); + VariableRequester.scriptContents = contents; + return VariableRequester.scriptContents; +} diff --git a/src/client/repl/variables/variableResultCache.ts b/src/client/repl/variables/variableResultCache.ts new file mode 100644 index 000000000000..1e19415becb7 --- /dev/null +++ b/src/client/repl/variables/variableResultCache.ts @@ -0,0 +1,28 @@ +import { VariablesResult } from 'vscode'; + +export class VariableResultCache { + private cache = new Map(); + + private executionCount = 0; + + getResults(executionCount: number, cacheKey: string): VariablesResult[] | undefined { + if (this.executionCount !== executionCount) { + this.cache.clear(); + this.executionCount = executionCount; + } + + return this.cache.get(cacheKey); + } + + setResults(executionCount: number, cacheKey: string, results: VariablesResult[]): void { + if (this.executionCount < executionCount) { + this.cache.clear(); + this.executionCount = executionCount; + } else if (this.executionCount > executionCount) { + // old results, don't cache + return; + } + + this.cache.set(cacheKey, results); + } +} diff --git a/src/client/repl/variables/variablesProvider.ts b/src/client/repl/variables/variablesProvider.ts new file mode 100644 index 000000000000..f033451dc80e --- /dev/null +++ b/src/client/repl/variables/variablesProvider.ts @@ -0,0 +1,159 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import { + CancellationToken, + NotebookDocument, + Variable, + NotebookVariablesRequestKind, + VariablesResult, + EventEmitter, + Event, + NotebookVariableProvider, + Uri, +} from 'vscode'; +import { VariableResultCache } from './variableResultCache'; +import { IVariableDescription } from './types'; +import { VariableRequester } from './variableRequester'; +import { getConfiguration } from '../../common/vscodeApis/workspaceApis'; + +export class VariablesProvider implements NotebookVariableProvider { + private readonly variableResultCache = new VariableResultCache(); + + private _onDidChangeVariables = new EventEmitter(); + + onDidChangeVariables = this._onDidChangeVariables.event; + + private executionCount = 0; + + constructor( + private readonly variableRequester: VariableRequester, + private readonly getNotebookDocument: () => NotebookDocument | undefined, + codeExecutedEvent: Event, + ) { + codeExecutedEvent(() => this.onDidExecuteCode()); + } + + onDidExecuteCode(): void { + const notebook = this.getNotebookDocument(); + if (notebook) { + this.executionCount += 1; + if (isEnabled(notebook.uri)) { + this._onDidChangeVariables.fire(notebook); + } + } + } + + async *provideVariables( + notebook: NotebookDocument, + parent: Variable | undefined, + kind: NotebookVariablesRequestKind, + start: number, + token: CancellationToken, + ): AsyncIterable { + const notebookDocument = this.getNotebookDocument(); + if ( + !isEnabled(notebook.uri) || + token.isCancellationRequested || + !notebookDocument || + notebookDocument !== notebook + ) { + return; + } + + const { executionCount } = this; + const cacheKey = getVariableResultCacheKey(notebook.uri.toString(), parent, start); + let results = this.variableResultCache.getResults(executionCount, cacheKey); + + if (parent) { + const parentDescription = parent as IVariableDescription; + if (!results && parentDescription.getChildren) { + const variables = await parentDescription.getChildren(start, token); + if (token.isCancellationRequested) { + return; + } + results = variables.map((variable) => this.createVariableResult(variable)); + this.variableResultCache.setResults(executionCount, cacheKey, results); + } else if (!results) { + // no cached results and no way to get children, so return empty + return; + } + + for (const result of results) { + yield result; + } + + // check if we have more indexed children to return + if ( + kind === 2 && + parentDescription.count && + results.length > 0 && + parentDescription.count > start + results.length + ) { + for await (const result of this.provideVariables( + notebook, + parent, + kind, + start + results.length, + token, + )) { + yield result; + } + } + } else { + if (!results) { + const variables = await this.variableRequester.getAllVariableDescriptions(undefined, start, token); + if (token.isCancellationRequested) { + return; + } + results = variables.map((variable) => this.createVariableResult(variable)); + this.variableResultCache.setResults(executionCount, cacheKey, results); + } + + for (const result of results) { + yield result; + } + } + } + + private createVariableResult(result: IVariableDescription): VariablesResult { + const indexedChildrenCount = result.count ?? 0; + const hasNamedChildren = !!result.hasNamedChildren; + const variable = { + getChildren: (start: number, token: CancellationToken) => this.getChildren(variable, start, token), + expression: createExpression(result.root, result.propertyChain), + ...result, + } as Variable; + return { variable, hasNamedChildren, indexedChildrenCount }; + } + + async getChildren(variable: Variable, start: number, token: CancellationToken): Promise { + const parent = variable as IVariableDescription; + return this.variableRequester.getAllVariableDescriptions(parent, start, token); + } +} + +function createExpression(root: string, propertyChain: (string | number)[]): string { + let expression = root; + for (const property of propertyChain) { + if (typeof property === 'string') { + expression += `.${property}`; + } else { + expression += `[${property}]`; + } + } + return expression; +} + +function getVariableResultCacheKey(uri: string, parent: Variable | undefined, start: number) { + let parentKey = ''; + const parentDescription = parent as IVariableDescription; + if (parentDescription) { + parentKey = `${parentDescription.name}.${parentDescription.propertyChain.join('.')}[[${start}`; + } + return `${uri}:${parentKey}`; +} + +function isEnabled(resource?: Uri) { + return getConfiguration('python', resource).get('REPL.provideVariables'); +} diff --git a/src/client/sourceMapSupport.ts b/src/client/sourceMapSupport.ts deleted file mode 100644 index 0d1ba39eb941..000000000000 --- a/src/client/sourceMapSupport.ts +++ /dev/null @@ -1,85 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; - -import * as path from 'path'; -import { WorkspaceConfiguration } from 'vscode'; -import './common/extensions'; -import { FileSystem } from './common/platform/fileSystem'; -import { EXTENSION_ROOT_DIR } from './constants'; -import { traceError } from './logging'; - -type VSCode = typeof import('vscode'); - -const setting = 'sourceMapsEnabled'; - -export class SourceMapSupport { - private readonly config: WorkspaceConfiguration; - constructor(private readonly vscode: VSCode) { - this.config = this.vscode.workspace.getConfiguration('python.diagnostics', null); - } - public async initialize(): Promise { - if (!this.enabled) { - return; - } - await this.enableSourceMaps(true); - require('source-map-support').install(); - const localize = require('./common/utils/localize') as typeof import('./common/utils/localize'); - const disable = localize.Diagnostics.disableSourceMaps; - this.vscode.window.showWarningMessage(localize.Diagnostics.warnSourceMaps, disable).then((selection) => { - if (selection === disable) { - this.disable().ignoreErrors(); - } - }); - } - public get enabled(): boolean { - return this.config.get(setting, false); - } - public async disable(): Promise { - if (this.enabled) { - await this.config.update(setting, false, this.vscode.ConfigurationTarget.Global); - } - await this.enableSourceMaps(false); - } - protected async enableSourceMaps(enable: boolean) { - const extensionSourceFile = path.join(EXTENSION_ROOT_DIR, 'out', 'client', 'extension.js'); - const debuggerSourceFile = path.join( - EXTENSION_ROOT_DIR, - 'out', - 'client', - 'debugger', - 'debugAdapter', - 'main.js', - ); - await Promise.all([ - this.enableSourceMap(enable, extensionSourceFile), - this.enableSourceMap(enable, debuggerSourceFile), - ]); - } - protected async enableSourceMap(enable: boolean, sourceFile: string) { - const sourceMapFile = `${sourceFile}.map`; - const disabledSourceMapFile = `${sourceFile}.map.disabled`; - if (enable) { - await this.rename(disabledSourceMapFile, sourceMapFile); - } else { - await this.rename(sourceMapFile, disabledSourceMapFile); - } - } - protected async rename(sourceFile: string, targetFile: string) { - const fs = new FileSystem(); - if (await fs.fileExists(targetFile)) { - return; - } - await fs.move(sourceFile, targetFile); - } -} -export function initialize(vscode: VSCode = require('vscode')) { - if (!vscode.workspace.getConfiguration('python.diagnostics', null).get('sourceMapsEnabled', false)) { - new SourceMapSupport(vscode).disable().ignoreErrors(); - return; - } - new SourceMapSupport(vscode).initialize().catch((_ex) => { - traceError('Failed to initialize source map support in extension'); - }); -} diff --git a/src/client/startupTelemetry.ts b/src/client/startupTelemetry.ts index f4d3fc254b67..f7a2a6aea517 100644 --- a/src/client/startupTelemetry.ts +++ b/src/client/startupTelemetry.ts @@ -1,6 +1,7 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. +import * as vscode from 'vscode'; import { IWorkspaceService } from './common/application/types'; import { isTestExecution } from './common/constants'; import { ITerminalHelper } from './common/terminal/types'; @@ -15,12 +16,14 @@ import { sendTelemetryEvent } from './telemetry'; import { EventName } from './telemetry/constants'; import { EditorLoadTelemetry } from './telemetry/types'; import { IStartupDurations } from './types'; +import { useEnvExtension } from './envExt/api.internal'; export async function sendStartupTelemetry( activatedPromise: Promise, durations: IStartupDurations, stopWatch: IStopWatch, serviceContainer: IServiceContainer, + isFirstSession: boolean, ) { if (isTestExecution()) { return; @@ -29,7 +32,7 @@ export async function sendStartupTelemetry( try { await activatedPromise; durations.totalNonBlockingActivateTime = stopWatch.elapsedTime - durations.startActivateTime; - const props = await getActivationTelemetryProps(serviceContainer); + const props = await getActivationTelemetryProps(serviceContainer, isFirstSession); sendTelemetryEvent(EventName.EDITOR_LOAD, durations, props); } catch (ex) { traceError('sendStartupTelemetry() failed.', ex); @@ -75,18 +78,22 @@ export function hasUserDefinedPythonPath(resource: Resource, serviceContainer: I : false; } -async function getActivationTelemetryProps(serviceContainer: IServiceContainer): Promise { +async function getActivationTelemetryProps( + serviceContainer: IServiceContainer, + isFirstSession?: boolean, +): Promise { // TODO: Not all of this data is showing up in the database... // TODO: If any one of these parts fails we send no info. We should // be able to partially populate as much as possible instead // (through granular try-catch statements). + const appName = vscode.env.appName; const workspaceService = serviceContainer.get(IWorkspaceService); const workspaceFolderCount = workspaceService.workspaceFolders?.length || 0; const terminalHelper = serviceContainer.get(ITerminalHelper); const terminalShellType = terminalHelper.identifyTerminalShell(); if (!workspaceService.isTrusted) { - return { workspaceFolderCount, terminal: terminalShellType }; + return { workspaceFolderCount, terminal: terminalShellType, isFirstSession }; } const interpreterService = serviceContainer.get(IInterpreterService); const mainWorkspaceUri = workspaceService.workspaceFolders?.length @@ -99,9 +106,19 @@ async function getActivationTelemetryProps(serviceContainer: IServiceContainer): // finish. API getActiveInterpreter() does not block on windows registry by default as // it is slow. await interpreterService.refreshPromise; - const interpreter = await interpreterService - .getActiveInterpreter() - .catch(() => undefined); + let interpreter: PythonEnvironment | undefined; + + // include main workspace uri if using env extension + if (useEnvExtension()) { + interpreter = await interpreterService + .getActiveInterpreter(mainWorkspaceUri) + .catch(() => undefined); + } else { + interpreter = await interpreterService + .getActiveInterpreter() + .catch(() => undefined); + } + const pythonVersion = interpreter && interpreter.version ? interpreter.version.raw : undefined; const interpreterType = interpreter ? interpreter.envType : undefined; if (interpreterType === EnvironmentType.Unknown) { @@ -119,6 +136,7 @@ async function getActivationTelemetryProps(serviceContainer: IServiceContainer): const usingGlobalInterpreter = interpreter ? isUsingGlobalInterpreterInWorkspace(interpreter.path, serviceContainer) : false; + const usingEnvironmentsExtension = useEnvExtension(); return { condaVersion, @@ -129,5 +147,8 @@ async function getActivationTelemetryProps(serviceContainer: IServiceContainer): hasPythonThree, usingUserDefinedInterpreter, usingGlobalInterpreter, + appName, + isFirstSession, + usingEnvironmentsExtension, }; } diff --git a/src/client/telemetry/constants.ts b/src/client/telemetry/constants.ts index 159f5690e5c5..eff32a6e3299 100644 --- a/src/client/telemetry/constants.ts +++ b/src/client/telemetry/constants.ts @@ -4,12 +4,10 @@ 'use strict'; export enum EventName { - FORMAT_SORT_IMPORTS = 'FORMAT.SORT_IMPORTS', - FORMAT = 'FORMAT.FORMAT', FORMAT_ON_TYPE = 'FORMAT.FORMAT_ON_TYPE', EDITOR_LOAD = 'EDITOR.LOAD', - LINTING = 'LINTING', REPL = 'REPL', + INVOKE_TOOL = 'INVOKE_TOOL', CREATE_NEW_FILE_COMMAND = 'CREATE_NEW_FILE_COMMAND', SELECT_INTERPRETER = 'SELECT_INTERPRETER', SELECT_INTERPRETER_ENTER_BUTTON = 'SELECT_INTERPRETER_ENTER_BUTTON', @@ -22,6 +20,10 @@ export enum EventName { ENVIRONMENT_WITHOUT_PYTHON_SELECTED = 'ENVIRONMENT_WITHOUT_PYTHON_SELECTED', PYTHON_ENVIRONMENTS_API = 'PYTHON_ENVIRONMENTS_API', PYTHON_INTERPRETER_DISCOVERY = 'PYTHON_INTERPRETER_DISCOVERY', + NATIVE_FINDER_MISSING_CONDA_ENVS = 'NATIVE_FINDER_MISSING_CONDA_ENVS', + NATIVE_FINDER_MISSING_POETRY_ENVS = 'NATIVE_FINDER_MISSING_POETRY_ENVS', + NATIVE_FINDER_PERF = 'NATIVE_FINDER_PERF', + PYTHON_INTERPRETER_DISCOVERY_INVALID_NATIVE = 'PYTHON_INTERPRETER_DISCOVERY_INVALID_NATIVE', PYTHON_INTERPRETER_AUTO_SELECTION = 'PYTHON_INTERPRETER_AUTO_SELECTION', PYTHON_INTERPRETER_ACTIVATION_ENVIRONMENT_VARIABLES = 'PYTHON_INTERPRETER.ACTIVATION_ENVIRONMENT_VARIABLES', PYTHON_INTERPRETER_ACTIVATION_FOR_RUNNING_CODE = 'PYTHON_INTERPRETER_ACTIVATION_FOR_RUNNING_CODE', @@ -37,18 +39,6 @@ export enum EventName { EXECUTION_CODE = 'EXECUTION_CODE', EXECUTION_DJANGO = 'EXECUTION_DJANGO', - DEBUG_IN_TERMINAL_BUTTON = 'DEBUG.IN_TERMINAL', - DEBUG_ADAPTER_USING_WHEELS_PATH = 'DEBUG_ADAPTER.USING_WHEELS_PATH', - DEBUG_SESSION_ERROR = 'DEBUG_SESSION.ERROR', - DEBUG_SESSION_START = 'DEBUG_SESSION.START', - DEBUG_SESSION_STOP = 'DEBUG_SESSION.STOP', - DEBUG_SESSION_USER_CODE_RUNNING = 'DEBUG_SESSION.USER_CODE_RUNNING', - DEBUGGER = 'DEBUGGER', - DEBUGGER_ATTACH_TO_CHILD_PROCESS = 'DEBUGGER.ATTACH_TO_CHILD_PROCESS', - DEBUGGER_ATTACH_TO_LOCAL_PROCESS = 'DEBUGGER.ATTACH_TO_LOCAL_PROCESS', - DEBUGGER_CONFIGURATION_PROMPTS = 'DEBUGGER.CONFIGURATION.PROMPTS', - DEBUGGER_CONFIGURATION_PROMPTS_IN_LAUNCH_JSON = 'DEBUGGER.CONFIGURATION.PROMPTS.IN.LAUNCH.JSON', - // Python testing specific telemetry UNITTEST_CONFIGURING = 'UNITTEST.CONFIGURING', UNITTEST_CONFIGURE = 'UNITTEST.CONFIGURE', @@ -68,6 +58,7 @@ export enum EventName { EXTENSION_SURVEY_PROMPT = 'EXTENSION_SURVEY_PROMPT', LANGUAGE_SERVER_ENABLED = 'LANGUAGE_SERVER.ENABLED', + LANGUAGE_SERVER_TRIGGER_TIME = 'LANGUAGE_SERVER_TRIGGER_TIME', LANGUAGE_SERVER_STARTUP = 'LANGUAGE_SERVER.STARTUP', LANGUAGE_SERVER_READY = 'LANGUAGE_SERVER.READY', LANGUAGE_SERVER_TELEMETRY = 'LANGUAGE_SERVER.EVENT', @@ -80,10 +71,8 @@ export enum EventName { DIAGNOSTICS_ACTION = 'DIAGNOSTICS.ACTION', DIAGNOSTICS_MESSAGE = 'DIAGNOSTICS.MESSAGE', - SELECT_LINTER = 'LINTING.SELECT', USE_REPORT_ISSUE_COMMAND = 'USE_REPORT_ISSUE_COMMAND', - LINTER_NOT_INSTALLED_PROMPT = 'LINTER_NOT_INSTALLED_PROMPT', HASHED_PACKAGE_NAME = 'HASHED_PACKAGE_NAME', JEDI_LANGUAGE_SERVER_ENABLED = 'JEDI_LANGUAGE_SERVER.ENABLED', @@ -91,19 +80,11 @@ export enum EventName { JEDI_LANGUAGE_SERVER_READY = 'JEDI_LANGUAGE_SERVER.READY', JEDI_LANGUAGE_SERVER_REQUEST = 'JEDI_LANGUAGE_SERVER.REQUEST', - TENSORBOARD_SESSION_LAUNCH = 'TENSORBOARD.SESSION_LAUNCH', - TENSORBOARD_SESSION_DURATION = 'TENSORBOARD.SESSION_DURATION', - TENSORBOARD_SESSION_DAEMON_STARTUP_DURATION = 'TENSORBOARD.SESSION_DAEMON_STARTUP_DURATION', - TENSORBOARD_LAUNCH_PROMPT_SELECTION = 'TENSORBOARD.LAUNCH_PROMPT_SELECTION', - TENSORBOARD_SESSION_E2E_STARTUP_DURATION = 'TENSORBOARD.SESSION_E2E_STARTUP_DURATION', - TENSORBOARD_ENTRYPOINT_SHOWN = 'TENSORBOARD.ENTRYPOINT_SHOWN', TENSORBOARD_INSTALL_PROMPT_SHOWN = 'TENSORBOARD.INSTALL_PROMPT_SHOWN', TENSORBOARD_INSTALL_PROMPT_SELECTION = 'TENSORBOARD.INSTALL_PROMPT_SELECTION', TENSORBOARD_DETECTED_IN_INTEGRATED_TERMINAL = 'TENSORBOARD_DETECTED_IN_INTEGRATED_TERMINAL', TENSORBOARD_PACKAGE_INSTALL_RESULT = 'TENSORBOARD.PACKAGE_INSTALL_RESULT', TENSORBOARD_TORCH_PROFILER_IMPORT = 'TENSORBOARD.TORCH_PROFILER_IMPORT', - TENSORBOARD_JUMP_TO_SOURCE_REQUEST = 'TENSORBOARD_JUMP_TO_SOURCE_REQUEST', - TENSORBOARD_JUMP_TO_SOURCE_FILE_NOT_FOUND = 'TENSORBOARD_JUMP_TO_SOURCE_FILE_NOT_FOUND', ENVIRONMENT_CREATING = 'ENVIRONMENT.CREATING', ENVIRONMENT_CREATED = 'ENVIRONMENT.CREATED', @@ -112,11 +93,12 @@ export enum EventName { ENVIRONMENT_INSTALLED_PACKAGES = 'ENVIRONMENT.INSTALLED_PACKAGES', ENVIRONMENT_INSTALLING_PACKAGES_FAILED = 'ENVIRONMENT.INSTALLING_PACKAGES_FAILED', ENVIRONMENT_BUTTON = 'ENVIRONMENT.BUTTON', + ENVIRONMENT_DELETE = 'ENVIRONMENT.DELETE', + ENVIRONMENT_REUSE = 'ENVIRONMENT.REUSE', - TOOLS_EXTENSIONS_ALREADY_INSTALLED = 'TOOLS_EXTENSIONS.ALREADY_INSTALLED', - TOOLS_EXTENSIONS_PROMPT_SHOWN = 'TOOLS_EXTENSIONS.PROMPT_SHOWN', - TOOLS_EXTENSIONS_INSTALL_SELECTED = 'TOOLS_EXTENSIONS.INSTALL_SELECTED', - TOOLS_EXTENSIONS_PROMPT_DISMISSED = 'TOOLS_EXTENSIONS.PROMPT_DISMISSED', + ENVIRONMENT_CHECK_TRIGGER = 'ENVIRONMENT.CHECK.TRIGGER', + ENVIRONMENT_CHECK_RESULT = 'ENVIRONMENT.CHECK.RESULT', + ENVIRONMENT_TERMINAL_GLOBAL_PIP = 'ENVIRONMENT.TERMINAL.GLOBAL_PIP', } export enum PlatformErrors { diff --git a/src/client/telemetry/importTracker.ts b/src/client/telemetry/importTracker.ts index 06991a815140..cf8e1ed48837 100644 --- a/src/client/telemetry/importTracker.ts +++ b/src/client/telemetry/importTracker.ts @@ -1,3 +1,4 @@ +/* eslint-disable class-methods-use-this */ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. @@ -7,6 +8,7 @@ import { inject, injectable } from 'inversify'; import * as path from 'path'; import { clearTimeout, setTimeout } from 'timers'; import { TextDocument } from 'vscode'; +import { createHash } from 'crypto'; import { sendTelemetryEvent } from '.'; import { IExtensionSingleActivationService } from '../activation/types'; import { IDocumentManager } from '../common/application/types'; @@ -49,13 +51,10 @@ const testExecution = isTestExecution(); export class ImportTracker implements IExtensionSingleActivationService { public readonly supportedWorkspaceTypes = { untrustedWorkspace: false, virtualWorkspace: true }; - private pendingChecks = new Map(); + private pendingChecks = new Map(); private static sentMatches: Set = new Set(); - // eslint-disable-next-line global-require - private hashFn = require('hash.js').sha256; - constructor( @inject(IDocumentManager) private documentManager: IDocumentManager, @inject(IDisposableRegistry) private disposables: IDisposableRegistry, @@ -120,7 +119,7 @@ export class ImportTracker implements IExtensionSingleActivationService { ImportTracker.sentMatches.add(packageName); // Hash the package name so that we will never accidentally see a // user's private package name. - const hash = this.hashFn().update(packageName).digest('hex'); + const hash = createHash('sha256').update(packageName).digest('hex'); sendTelemetryEvent(EventName.HASHED_PACKAGE_NAME, undefined, { hashedName: hash }); } diff --git a/src/client/telemetry/index.ts b/src/client/telemetry/index.ts index 5dff35067196..763f7405aa0d 100644 --- a/src/client/telemetry/index.ts +++ b/src/client/telemetry/index.ts @@ -3,31 +3,21 @@ // Licensed under the MIT License. import TelemetryReporter from '@vscode/extension-telemetry'; - +import type * as vscodeTypes from 'vscode'; import { DiagnosticCodes } from '../application/diagnostics/constants'; -import { IWorkspaceService } from '../common/application/types'; -import { AppinsightsKey, isTestExecution, isUnitTestExecution } from '../common/constants'; +import { AppinsightsKey, isTestExecution, isUnitTestExecution, PVSC_EXTENSION_ID } from '../common/constants'; import type { TerminalShellType } from '../common/terminal/types'; -import { StopWatch } from '../common/utils/stopWatch'; import { isPromise } from '../common/utils/async'; -import { DebugConfigurationType } from '../debugger/extension/types'; -import { ConsoleType, TriggerType } from '../debugger/types'; -import { LinterId } from '../linters/types'; +import { StopWatch } from '../common/utils/stopWatch'; import { EnvironmentType, PythonEnvironment } from '../pythonEnvironments/info'; -import { - TensorBoardPromptSelection, - TensorBoardEntrypointTrigger, - TensorBoardSessionStartResult, - TensorBoardEntrypoint, -} from '../tensorBoard/constants'; +import { TensorBoardPromptSelection } from '../tensorBoard/constants'; import { EventName } from './constants'; -import type { LinterTrigger, TestTool } from './types'; +import type { TestTool } from './types'; /** * Checks whether telemetry is supported. * Its possible this function gets called within Debug Adapter, vscode isn't available in there. * Within DA, there's a completely different way to send telemetry. - * @returns {boolean} */ function isTelemetrySupported(): boolean { try { @@ -40,13 +30,19 @@ function isTelemetrySupported(): boolean { } } +// eslint-disable-next-line @typescript-eslint/no-explicit-any +let packageJSON: any; + /** - * Checks if the telemetry is disabled in user settings - * @returns {boolean} + * Checks if the telemetry is disabled */ -export function isTelemetryDisabled(workspaceService: IWorkspaceService): boolean { - const settings = workspaceService.getConfiguration('telemetry').inspect('enableTelemetry')!; - return settings.globalValue === false; +export function isTelemetryDisabled(): boolean { + if (!packageJSON) { + const vscode = require('vscode') as typeof vscodeTypes; + const pythonExtension = vscode.extensions.getExtension(PVSC_EXTENSION_ID)!; + packageJSON = pythonExtension.packageJSON; + } + return !packageJSON.enableTelemetry; } const sharedProperties: Record = {}; @@ -101,7 +97,7 @@ export function sendTelemetryEvent

the total amount of time taken for the execObservable daemon to report successful TB session launch - * 2. 'canceled' --> the total amount of time that the user waited for the daemon to start before canceling launch - * 3. 'error' --> 60_000ms, i.e. we timed out waiting for the daemon to launch - * In the first two cases, `duration` should not be more than 60_000ms. - */ - /* __GDPR__ - "tensorboard.session_daemon_startup_duration" : { - "duration" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "donjayamanne" }, - "result" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "donjayamanne" } - } - */ - [EventName.TENSORBOARD_SESSION_DAEMON_STARTUP_DURATION]: { - result: TensorBoardSessionStartResult; - }; - /** - * Telemetry event sent after the webview framing the TensorBoard website has been successfully shown. - * This event is sent with `duration` which represents the total time to create a TensorBoardSession. - * Note that this event is only sent if an integrated TensorBoard session is successfully created in full. - * This includes checking whether the tensorboard package is installed and installing it if it's not already - * installed, requesting the user to select a log directory, starting the tensorboard - * program instance in a daemon, and showing the TensorBoard UI in a webpanel, in that order. - */ - /* __GDPR__ - "tensorboard.session_e2e_startup_duration" : { - "duration" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "donjayamanne" } - } - */ - [EventName.TENSORBOARD_SESSION_E2E_STARTUP_DURATION]: never | undefined; - /** - * Telemetry event sent after the user has closed a TensorBoard webview panel. This event is - * sent with `duration` specifying the total duration of time that the TensorBoard session - * ran for before the user terminated the session. - */ - /* __GDPR__ - "tensorboard.session_duration" : { - "duration" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "donjayamanne" } - } - */ - [EventName.TENSORBOARD_SESSION_DURATION]: never | undefined; - /** - * Telemetry event sent when an entrypoint is displayed to the user. This event is sent once - * per entrypoint per session to minimize redundant events since codelenses - * can be displayed multiple times per file. - * The `entrypoint` property indicates whether the command was executed directly by the - * user from the command palette or from a codelens or the user clicking 'yes' - * on the launch prompt we display. - * The `trigger` property indicates whether the entrypoint was triggered by the user - * importing tensorboard, using tensorboard in a notebook, detected tfevent files in - * the workspace. For the palette entrypoint, the trigger is also 'palette'. - */ - /* __GDPR__ - "tensorboard.entrypoint_shown" : { - "entrypoint" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "donjayamanne" }, - "trigger": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "donjayamanne" } - } - */ - [EventName.TENSORBOARD_ENTRYPOINT_SHOWN]: { - entrypoint: TensorBoardEntrypoint; - trigger: TensorBoardEntrypointTrigger; - }; /** * Telemetry event sent when the user is prompted to install Python packages that are * dependencies for launching an integrated TensorBoard session. @@ -2000,25 +2354,6 @@ export interface IEventNamePropertyMapping { "tensorboard.torch_profiler_import" : { "owner": "donjayamanne" } */ [EventName.TENSORBOARD_TORCH_PROFILER_IMPORT]: never | undefined; - /** - * Telemetry event sent when the extension host receives a message from the - * TensorBoard webview containing a valid jump to source payload from the - * PyTorch profiler TensorBoard plugin. - */ - /* __GDPR__ - "tensorboard_jump_to_source_request" : { "owner": "donjayamanne" } - */ - [EventName.TENSORBOARD_JUMP_TO_SOURCE_REQUEST]: never | undefined; - /** - * Telemetry event sent when the extension host receives a message from the - * TensorBoard webview containing a valid jump to source payload from the - * PyTorch profiler TensorBoard plugin, but the source file does not exist - * on the machine currently running TensorBoard. - */ - /* __GDPR__ - "tensorboard_jump_to_source_file_not_found" : { "owner": "donjayamanne" } - */ - [EventName.TENSORBOARD_JUMP_TO_SOURCE_FILE_NOT_FOUND]: never | undefined; [EventName.TENSORBOARD_DETECTED_IN_INTEGRATED_TERMINAL]: never | undefined; /** * Telemetry event sent before creating an environment. @@ -2030,7 +2365,7 @@ export interface IEventNamePropertyMapping { } */ [EventName.ENVIRONMENT_CREATING]: { - environmentType: 'venv' | 'conda' | 'microvenv'; + environmentType: 'venv' | 'conda' | 'microvenv' | undefined; pythonVersion: string | undefined; }; /** @@ -2106,52 +2441,64 @@ export interface IEventNamePropertyMapping { */ [EventName.ENVIRONMENT_BUTTON]: never | undefined; /** - * Telemetry event sent when a linter or formatter extension is already installed. + * Telemetry event if user selected to delete the existing environment. */ /* __GDPR__ - "tools_extensions.already_installed" : { - "extensionId" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "owner": "karthiknadig" } + "environment.delete" : { + "environmentType" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "owner": "karthiknadig" }, + "status" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "owner": "karthiknadig" } } */ - [EventName.TOOLS_EXTENSIONS_ALREADY_INSTALLED]: { - extensionId: 'ms-python.pylint' | 'ms-python.flake8' | 'ms-python.isort'; - isEnabled: boolean; + [EventName.ENVIRONMENT_DELETE]: { + environmentType: 'venv' | 'conda'; + status: 'triggered' | 'deleted' | 'failed'; }; /** - * Telemetry event sent when install linter or formatter extension prompt is shown. + * Telemetry event if user selected to re-use the existing environment. */ /* __GDPR__ - "tools_extensions.prompt_shown" : { - "extensionId" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "owner": "karthiknadig" } + "environment.reuse" : { + "environmentType" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "owner": "karthiknadig" } } */ - [EventName.TOOLS_EXTENSIONS_PROMPT_SHOWN]: { - extensionId: 'ms-python.pylint' | 'ms-python.flake8' | 'ms-python.isort'; + [EventName.ENVIRONMENT_REUSE]: { + environmentType: 'venv' | 'conda'; }; /** - * Telemetry event sent when clicking to install linter or formatter extension from the suggestion prompt. + * Telemetry event sent when a check for environment creation conditions is triggered. */ /* __GDPR__ - "tools_extensions.install_selected" : { - "extensionId" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "owner": "karthiknadig" } + "environment.check.trigger" : { + "trigger" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "owner": "karthiknadig" } } */ - [EventName.TOOLS_EXTENSIONS_INSTALL_SELECTED]: { - extensionId: 'ms-python.pylint' | 'ms-python.flake8' | 'ms-python.isort'; + [EventName.ENVIRONMENT_CHECK_TRIGGER]: { + trigger: + | 'run-in-terminal' + | 'debug-in-terminal' + | 'run-selection' + | 'on-workspace-load' + | 'as-command' + | 'debug'; }; /** - * Telemetry event sent when dismissing prompt suggesting to install the linter or formatter extension. + * Telemetry event sent when a check for environment creation condition is computed. */ /* __GDPR__ - "tools_extensions.prompt_dismissed" : { - "extensionId" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "owner": "karthiknadig" }, - "dismissType" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "owner": "karthiknadig" } + "environment.check.result" : { + "result" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "owner": "karthiknadig" } } */ - [EventName.TOOLS_EXTENSIONS_PROMPT_DISMISSED]: { - extensionId: 'ms-python.pylint' | 'ms-python.flake8' | 'ms-python.isort'; - dismissType: 'close' | 'doNotShow'; + [EventName.ENVIRONMENT_CHECK_RESULT]: { + result: 'criteria-met' | 'criteria-not-met' | 'already-ran' | 'turned-off' | 'no-uri'; }; + /** + * Telemetry event sent when `pip install` was called from a global env in a shell where shell inegration is supported. + */ + /* __GDPR__ + "environment.terminal.global_pip" : { "owner": "karthiknadig" } + */ + [EventName.ENVIRONMENT_TERMINAL_GLOBAL_PIP]: never | undefined; /* __GDPR__ "query-expfeature" : { "owner": "luabud", diff --git a/src/client/telemetry/pylance.ts b/src/client/telemetry/pylance.ts index 905cacc5fbf2..63bd113893e2 100644 --- a/src/client/telemetry/pylance.ts +++ b/src/client/telemetry/pylance.ts @@ -1,371 +1,484 @@ -/* __GDPR__ - "language_server.enabled" : { - "duration" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, - "errorname" : { "classification": "CallstackOrException", "purpose": "PerformanceAndHealth" }, - "errorstack" : { "classification": "CallstackOrException", "purpose": "PerformanceAndHealth" }, - "lsversion" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" } - } -*/ -/* __GDPR__ - "language_server.ready" : { - "duration" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, - "errorname" : { "classification": "CallstackOrException", "purpose": "PerformanceAndHealth" }, - "errorstack" : { "classification": "CallstackOrException", "purpose": "PerformanceAndHealth" }, - "lsversion" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" } - } -*/ -/* __GDPR__ - "language_server.request" : { - "duration" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, - "errorname" : { "classification": "CallstackOrException", "purpose": "PerformanceAndHealth" }, - "errorstack" : { "classification": "CallstackOrException", "purpose": "PerformanceAndHealth" }, - "lsversion" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, - "method" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, - "modulehash" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" } - } -*/ -/* __GDPR__ - "language_server.startup" : { - "duration" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, - "errorname" : { "classification": "CallstackOrException", "purpose": "PerformanceAndHealth" }, - "errorstack" : { "classification": "CallstackOrException", "purpose": "PerformanceAndHealth" }, - "lsversion" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" } - } -*/ -/* __GDPR__ - "language_server/analysis_complete" : { - "common.remotename" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, - "common.uikind" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, - "configparseerroroccurred" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, - "elapsedms" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, - "externalmb" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, - "fatalerroroccurred" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, - "heaptotalmb" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, - "heapusedmb" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, - "installsource" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, - "isdone" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, - "isfirstrun" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, - "lsversion" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, - "numfilesanalyzed" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, - "numfilesinprogram" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, - "peakrssmb" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, - "resolverid" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, - "rssmb" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" } - } -*/ -/* __GDPR__ - "language_server/analysis_exception" : { - "errorname" : { "classification": "CallstackOrException", "purpose": "PerformanceAndHealth" }, - "errorstack" : { "classification": "CallstackOrException", "purpose": "PerformanceAndHealth" }, - "lsversion" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" } - } -*/ -/* __GDPR__ - "language_server/completion_accepted" : { - "autoimport" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, - "dictionarykey" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, - "memberaccess" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, - "keyword" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" } - } -*/ -/* __GDPR__ - "language_server/completion_coverage" : { - "failures" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, - "lsversion" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, - "overallfailures" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, - "overallsuccesses" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, - "overalltotal" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, - "successes" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, - "total" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" } - } -*/ -/* __GDPR__ - "language_server/completion_metrics" : { - "installsource" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, - "lastknownmembernamehash" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, - "lastknownmodulehash" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, - "lsversion" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, - "packagehash" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, - "unknownmembernamehash" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" } - } -*/ -/* __GDPR__ - "language_server/completion_slow" : { - "bindcallcount" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, - "bindtime" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, - "correlationid" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, - "custom_autoimportadditiontimeinms" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, - "custom_autoimportedittimeinms" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, - "custom_autoimportimportaliascount" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, - "custom_autoimportimportaliastimeinms" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, - "custom_autoimportindexcount" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, - "custom_autoimportindextimeinms" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, - "custom_autoimportindexused" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, - "custom_autoimportitemcount" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, - "custom_autoimportmoduleresolvetimeinms" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, - "custom_autoimportmoduletimeinms" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, - "custom_autoimportsymbolcount" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, - "custom_autoimporttotaltimeinms" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, - "custom_autoimportuserindexcount" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, - "custom_completionitems" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, - "custom_completionitemtelemetrybuildtimeinms" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, - "custom_extensiontotaltimeinms" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, - "custom_selecteditemtelemetrybuildtimeinms" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, - "lsversion" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, - "parsecallcount" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, - "parsetime" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, - "readfilecallcount" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, - "readfiletime" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, - "resolvecallcount" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, - "resolvetime" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, - "tokenizecallcount" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, - "tokenizetime" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, - "totaltime" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, - "typeevalcount" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, - "typeevaltime" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" } - } -*/ -/* __GDPR__ - "language_server/exception_intellicode" : { - "common.remotename" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, - "common.uikind" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" } - } -*/ -/* __GDPR__ - "language_server/execute_command" : { - "lsversion" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, - "name" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" } - } -*/ -/* __GDPR__ - "language_server/goto_def_inside_string" : { - "resultlength" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" } - } -*/ -/* __GDPR__ - "language_server/import_heuristic" : { - "avgcost" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, - "avglevel" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, - "conflicts" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, - "nativemodules" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, - "nativepackages" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, - "reason_because_it_is_not_a_valid_directory" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, - "reason_could_not_parse_output" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, - "reason_did_not_find_file" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, - "reason_no_python_interpreter_search_path" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, - "reason_typeshed_path_not_found" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, - "resolverid" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, - "success" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, - "total" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" } - } -*/ -/* __GDPR__ - "language_server/import_metrics" : { - "absolutestubs" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, - "absolutetotal" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, - "absoluteunresolved" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, - "absoluteuserunresolved" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, - "builtinimportstubs" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, - "builtinimporttotal" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, - "common.remotename" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, - "common.uikind" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, - "installsource" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, - "localimportstubs" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, - "localimporttotal" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, - "lsversion" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, - "nativemodules" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, - "nativepackages" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, - "relativestubs" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, - "relativetotal" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, - "relativeunresolved" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, - "resolverid" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, - "stubs" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, - "thirdpartyimportstubs" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, - "thirdpartyimporttotal" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, - "total" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, - "unresolvedmodules" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, - "unresolvedpackages" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, - "unresolvedpackageslowercase" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, - "unresolvedtotal" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" } - } -*/ -/* __GDPR__ - "language_server/index_slow" : { - "bindcallcount" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, - "bindtime" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, - "custom_count" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, - "lsversion" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, - "parsecallcount" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, - "parsetime" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, - "readfilecallcount" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, - "readfiletime" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, - "resolvecallcount" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, - "resolvetime" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, - "tokenizecallcount" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, - "tokenizetime" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, - "totaltime" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, - "typeevalcount" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, - "typeevaltime" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" } - } -*/ -/* __GDPR__ - "language_server/installed_packages" : { - "packagesbitarray" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, - "packageslowercase" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, - "resolverid" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" } - } -*/ -/* __GDPR__ - "language_server/intellicode_completion_item_selected" : { - "class" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, - "common.remotename" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, - "common.uikind" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, - "count" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, - "elapsedtime" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, - "failurereason" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, - "id" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, - "index" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, - "installsource" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, - "isintellicodecommit" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, - "language" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, - "lsversion" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, - "memoryincreasekb" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, - "method" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, - "methods" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, - "modeltype" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, - "modelversion" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, - "selecteditemtelemetrybuildtimeinms" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" } - } -*/ -/* __GDPR__ - "language_server/intellicode_enabled" : { - "common.remotename" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, - "common.uikind" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, - "enabled" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, - "installsource" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, - "lsversion" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, - "startup" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" } - } -*/ -/* __GDPR__ - "language_server/intellicode_model_load_failed" : { - "errorname" : { "classification": "CallstackOrException", "purpose": "PerformanceAndHealth" }, - "errorstack" : { "classification": "CallstackOrException", "purpose": "PerformanceAndHealth" }, - "installsource" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, - "lsversion" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, - "reason" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" } - } -*/ -/* __GDPR__ - "language_server/intellicode_onnx_load_failed" : { - "errorname" : { "classification": "CallstackOrException", "purpose": "PerformanceAndHealth" }, - "errorstack" : { "classification": "CallstackOrException", "purpose": "PerformanceAndHealth" }, - "installsource" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, - "lsversion" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, - "reason" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" } - } -*/ -/* __GDPR__ - "language_server/rename_files" : { - "affectedfilescount" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, - "bindcallcount" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, - "bindtime" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, - "filerenamed" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, - "parsecallcount" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, - "parsetime" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, - "readfilecallcount" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, - "readfiletime" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, - "resolvecallcount" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, - "resolvetime" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, - "tokenizecallcount" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, - "tokenizetime" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, - "totaltime" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, - "type" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, - "typeevalcount" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, - "typeevaltime" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" } - } -*/ -/* __GDPR__ - "language_server/semantictokens_slow" : { - "bindcallcount" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, - "bindtime" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, - "custom_count" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, - "lsversion" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, - "parsecallcount" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, - "parsetime" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, - "readfilecallcount" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, - "readfiletime" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, - "resolvecallcount" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, - "resolvetime" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, - "tokenizecallcount" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, - "tokenizetime" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, - "totaltime" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, - "typeevalcount" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, - "typeevaltime" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" } - } -*/ -/* __GDPR__ - "language_server/settings" : { - "addimportexactmatchonly" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, - "autoimportcompletions" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, - "autosearchpaths" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, - "completefunctionparens" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, - "disableworkspacesymbol" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, - "enableextractcodeaction" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, - "extracommitchars" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, - "formatontype" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, - "functionReturnInlayTypeHints" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, - "hasconfigfile" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, - "hasextrapaths" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, - "importformat" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, - "includeusersymbolsinautoimport" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, - "indexing" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, - "lspinteractivewindows" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, - "lspnotebooks" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, - "movesymbol" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, - "openfilesonly" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, - "pytestparameterinlaytypehints" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, - "typecheckingmode" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, - "useimportheuristic" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, - "uselibrarycodefortypes" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, - "variableinlaytypehints" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, - "watchforlibrarychanges" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, - "workspacecount" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" } - } -*/ -/* __GDPR__ - "language_server/startup_metrics" : { - "analysisms" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, - "peakrssmb" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, - "presetfileopenms" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, - "tokendeltams" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, - "tokenfullms" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, - "tokenrangems" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, - "totalms" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, - "userindexms" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" } - } -*/ -/* __GDPR__ - "language_server/workspaceindex_slow" : { - "bindcallcount" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, - "bindtime" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, - "custom_count" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, - "lsversion" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, - "parsecallcount" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, - "parsetime" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, - "readfilecallcount" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, - "readfiletime" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, - "resolvecallcount" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, - "resolvetime" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, - "tokenizecallcount" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, - "tokenizetime" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, - "totaltime" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, - "typeevalcount" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, - "typeevaltime" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" } - } -*/ -/* __GDPR__ - "language_server/workspaceindex_threshold_reached" : { - "index_count" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" } - } -*/ +/* __GDPR__ + "language_server.enabled" : { + "duration" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, + "errorname" : { "classification": "CallstackOrException", "purpose": "PerformanceAndHealth" }, + "errorstack" : { "classification": "CallstackOrException", "purpose": "PerformanceAndHealth" }, + "lsversion" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "failed" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" } + } +*/ +/* __GDPR__ + "language_server.jinja_usage" : { + "lsversion" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" } , + "openfileextensions" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "failed" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" } + } +*/ +/* __GDPR__ + "language_server.ready" : { + "duration" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, + "errorname" : { "classification": "CallstackOrException", "purpose": "PerformanceAndHealth" }, + "errorstack" : { "classification": "CallstackOrException", "purpose": "PerformanceAndHealth" }, + "lsversion" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "failed" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" } + } +*/ +/* __GDPR__ + "language_server.request" : { + "duration" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, + "errorname" : { "classification": "CallstackOrException", "purpose": "PerformanceAndHealth" }, + "errorstack" : { "classification": "CallstackOrException", "purpose": "PerformanceAndHealth" }, + "lsversion" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "method" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "modulehash" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "moduleversion" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "resultlength" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, + "failed" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" } + } +*/ +/* __GDPR__ + "language_server.startup" : { + "duration" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, + "errorname" : { "classification": "CallstackOrException", "purpose": "PerformanceAndHealth" }, + "errorstack" : { "classification": "CallstackOrException", "purpose": "PerformanceAndHealth" }, + "lsversion" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "failed" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" } + } +*/ +/* __GDPR__ + "language_server/analysis_complete" : { + "configparseerroroccurred" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "elapsedms" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "externalmb" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "fatalerroroccurred" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "heaptotalmb" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "heapusedmb" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "installsource" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "isdone" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "isfirstrun" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "lsversion" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "numfilesanalyzed" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "numfilesinprogram" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "peakrssmb" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "resolverid" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "rssmb" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "diagnosticsseen" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "failed" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "editablepthcount": { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "computedpthcount": { "classification": "SystemMetaData", "purpose": "FeatureInsight" } + + } +*/ +/* __GDPR__ + "language_server/analysis_exception" : { + "errorname" : { "classification": "CallstackOrException", "purpose": "PerformanceAndHealth" }, + "errorstack" : { "classification": "CallstackOrException", "purpose": "PerformanceAndHealth" }, + "lsversion" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "failed" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" } + } +*/ +/* __GDPR__ + "language_server/completion_accepted" : { + "autoimport" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "dictionarykey" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "memberaccess" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "keyword" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "failed" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" } + } +*/ +/* __GDPR__ + "language_server/completion_coverage" : { + "failures" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "lsversion" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "overallfailures" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "overallsuccesses" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "overalltotal" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "successes" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "total" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "failed" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" } + } +*/ +/* __GDPR__ + "language_server/completion_metrics" : { + "installsource" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "lastknownmembernamehash" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "lastknownmodulehash" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "lsversion" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "packagehash" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "unknownmembernamehash" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "failed" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" } + } +*/ +/* __GDPR__ + "language_server/completion_slow" : { + "bindcallcount" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "bindtime" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "correlationid" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "custom_autoimportadditiontimeinms" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "custom_autoimportedittimeinms" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "custom_autoimportimportaliascount" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "custom_autoimportimportaliastimeinms" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "custom_autoimportindexcount" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "custom_autoimportindextimeinms" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "custom_autoimportindexused" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "custom_autoimportitemcount" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "custom_autoimportmoduleresolvetimeinms" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "custom_autoimportmoduletimeinms" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "custom_autoimportsymbolcount" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "custom_autoimporttotaltimeinms" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "custom_autoimportuserindexcount" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "custom_completionitems" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "custom_completionitemtelemetrybuildtimeinms" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "custom_extensiontotaltimeinms" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "custom_selecteditemtelemetrybuildtimeinms" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "custom_completiontype" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "custom_filetype" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "lsversion" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "parsecallcount" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "parsetime" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "readfilecallcount" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "readfiletime" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "resolvecallcount" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "resolvetime" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "tokenizecallcount" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "tokenizetime" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "totaltime" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "typeevalcount" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "typeevaltime" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "failed" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" } + } +*/ +/* __GDPR__ + "language_server/completion_context_items" : { + "lsversion" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "context" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "count" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" } + } +*/ +/* __GDPR__ + "language_server/documentcolor_slow" : { + "bindcallcount" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "bindtime" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "lsversion" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "parsecallcount" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "parsetime" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "readfilecallcount" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "readfiletime" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "resolvecallcount" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "resolvetime" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "tokenizecallcount" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "tokenizetime" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "totaltime" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "typeevalcount" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "typeevaltime" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "failed" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" } + } +*/ +/* __GDPR__ + "language_server/exception_intellicode" : { + "lsversion" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "failed" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" } + } +*/ +/* __GDPR__ + "language_server/execute_command" : { + "lsversion" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "name" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "failed" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" } + } +*/ +/* __GDPR__ + "language_server/goto_def_inside_string" : { + "resultlength" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "lsversion" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "failed" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" } + } +*/ +/* __GDPR__ + "language_server/import_heuristic" : { + "avgcost" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "avglevel" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "conflicts" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "nativemodules" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "nativepackages" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "reason_because_it_is_not_a_valid_directory" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "reason_could_not_parse_output" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "reason_did_not_find_file" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "reason_no_python_interpreter_search_path" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "reason_typeshed_path_not_found" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "resolverid" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "success" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "total" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "lsversion" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "failed" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" } + } +*/ +/* __GDPR__ + "language_server/import_metrics" : { + "absolutestubs" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "absolutetotal" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "absoluteunresolved" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "absoluteuserunresolved" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "builtinimportstubs" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "builtinimporttotal" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "installsource" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "localimportstubs" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "localimporttotal" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "lsversion" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "nativemodules" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "nativepackages" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "relativestubs" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "relativetotal" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "relativeunresolved" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "resolverid" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "stubs" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "thirdpartyimportstubs" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "thirdpartyimporttotal" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "total" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "unresolvedmodules" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "unresolvedpackages" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "unresolvedpackageslowercase" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "unresolvedtotal" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "failed" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" } + } +*/ +/* __GDPR__ + "language_server/index_slow" : { + "bindcallcount" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "bindtime" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "custom_count" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "lsversion" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "parsecallcount" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "parsetime" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "readfilecallcount" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "readfiletime" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "resolvecallcount" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "resolvetime" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "tokenizecallcount" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "tokenizetime" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "totaltime" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "typeevalcount" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "typeevaltime" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "failed" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" } + } +*/ +/* __GDPR__ + "language_server/installed_packages" : { + "packagesbitarray" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "packageslowercase" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "resolverid" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "failed" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "lsversion" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "editablepthcount": { "classification": "SystemMetaData", "purpose": "FeatureInsight" } + } +*/ +/* __GDPR__ + "language_server/intellicode_completion_item_selected" : { + "class" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "count" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "elapsedtime" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "failurereason" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "id" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "index" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "installsource" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "isintellicodecommit" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "language" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "lsversion" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "memoryincreasekb" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "method" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "methods" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "modeltype" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "modelversion" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "selecteditemtelemetrybuildtimeinms" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "failed" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" } + } +*/ +/* __GDPR__ + "language_server/intellicode_enabled" : { + "enabled" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "installsource" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "lsversion" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "startup" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "failed" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" } + } +*/ +/* __GDPR__ + "language_server/intellicode_model_load_failed" : { + "errorname" : { "classification": "CallstackOrException", "purpose": "PerformanceAndHealth" }, + "errorstack" : { "classification": "CallstackOrException", "purpose": "PerformanceAndHealth" }, + "installsource" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "lsversion" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "reason" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "failed" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" } + } +*/ +/* __GDPR__ + "language_server/intellicode_onnx_load_failed" : { + "errorname" : { "classification": "CallstackOrException", "purpose": "PerformanceAndHealth" }, + "errorstack" : { "classification": "CallstackOrException", "purpose": "PerformanceAndHealth" }, + "installsource" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "lsversion" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "reason" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "failed" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" } + } +*/ +/* __GDPR__ + "language_server/rename_files" : { + "affectedfilescount" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "bindcallcount" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "bindtime" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "filerenamed" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "parsecallcount" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "parsetime" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "readfilecallcount" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "readfiletime" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "resolvecallcount" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "resolvetime" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "tokenizecallcount" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "tokenizetime" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "totaltime" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "type" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "typeevalcount" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "typeevaltime" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "failed" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "lsversion" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" } + } +*/ +/* __GDPR__ + "language_server/semantictokens_slow" : { + "bindcallcount" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "bindtime" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "custom_count" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "lsversion" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "parsecallcount" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "parsetime" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "readfilecallcount" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "readfiletime" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "resolvecallcount" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "resolvetime" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "tokenizecallcount" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "tokenizetime" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "totaltime" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "typeevalcount" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "typeevaltime" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "failed" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" } + } +*/ +/* __GDPR__ + "language_server/server_side_request" : { + "duration" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, + "lsversion" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "method" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "modulehash" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "resultlength" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, + "failed" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" } + } +*/ +/* __GDPR__ + "language_server/settings" : { + "addimportexactmatchonly" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "aicodeactionsimplementabstractclasses" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "aiCodeActionsGenerateDocstring" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "aiCodeActionsGenerateSymbols" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "aiCodeActionsConvertFormatString" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "autoimportcompletions" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "autosearchpaths" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "callArgumentNameInlayHints" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "completefunctionparens" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "disableTaggedHints" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "disableworkspacesymbol" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "enableextractcodeaction" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "enablePytestSupport" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "extracommitchars" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "formatontype" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "functionReturnInlayTypeHints" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "hasconfigfile" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "hasextrapaths" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "importformat" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "intelliCodeEnabled" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "includeusersymbolsinautoimport" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "indexing" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "languageservermode" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "lspinteractivewindows" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "lspnotebooks" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "movesymbol" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "nodeExecutable" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "openfilesonly" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "pytestparameterinlaytypehints" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "typecheckingmode" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "unusablecompilerflags": { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "useimportheuristic" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "uselibrarycodefortypes" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "variableinlaytypehints" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "watchforlibrarychanges" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "workspacecount" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "failed" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "lsversion" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" } + } +*/ +/* __GDPR__ + "language_server/startup_metrics" : { + "analysisms" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "peakrssmb" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "presetfileopenms" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "tokendeltams" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "tokenfullms" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "tokenrangems" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "totalms" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "userindexms" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "failed" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "lsversion" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" } + } +*/ +/* __GDPR__ + "language_server/workspaceindex_slow" : { + "bindcallcount" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "bindtime" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "custom_count" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "lsversion" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "parsecallcount" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "parsetime" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "readfilecallcount" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "readfiletime" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "resolvecallcount" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "resolvetime" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "tokenizecallcount" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "tokenizetime" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "totaltime" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "typeevalcount" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "typeevaltime" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "failed" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" } + } +*/ +/* __GDPR__ + "language_server/workspaceindex_threshold_reached" : { + "index_count" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "failed" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "lsversion" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" } + } +*/ +/* __GDPR__ + "language_server/mcp_tool" : { + "kind" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "duration" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "cancelled" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "cancellation_reason" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "lsversion" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "failed" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" } + } +*/ +/* __GDPR__ + "language_server/copilot_hover" : { + "symbolName" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" } + } +*/ +/** + * Telemetry event sent when LSP server crashes + */ +/* __GDPR__ +"language_server.crash" : { + "oom" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "rchiodo" }, + "lsversion" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "rchiodo" }, + "failed" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" } +} +*/ diff --git a/src/client/telemetry/types.ts b/src/client/telemetry/types.ts index ae98707d94a8..42e51b261129 100644 --- a/src/client/telemetry/types.ts +++ b/src/client/telemetry/types.ts @@ -8,12 +8,7 @@ import { EventName } from './constants'; export type EditorLoadTelemetry = IEventNamePropertyMapping[EventName.EDITOR_LOAD]; -export type LinterTrigger = 'auto' | 'save' | 'manual'; - -export type LintingTelemetry = IEventNamePropertyMapping[EventName.LINTING]; - export type PythonInterpreterTelemetry = IEventNamePropertyMapping[EventName.PYTHON_INTERPRETER]; -export type DebuggerTelemetry = IEventNamePropertyMapping[EventName.DEBUGGER]; export type TestTool = 'pytest' | 'unittest'; export type TestRunTelemetry = IEventNamePropertyMapping[EventName.UNITTEST_RUN]; export type TestDiscoveryTelemetry = IEventNamePropertyMapping[EventName.UNITTEST_DISCOVERY_DONE]; diff --git a/src/client/tensorBoard/helpers.ts b/src/client/tensorBoard/helpers.ts index 3efb6aca04f9..8da3ef6a38f2 100644 --- a/src/client/tensorBoard/helpers.ts +++ b/src/client/tensorBoard/helpers.ts @@ -1,8 +1,6 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -import { noop } from '../common/utils/misc'; - // While it is uncommon for users to `import tensorboard`, TensorBoard is frequently // included as a submodule of other packages, e.g. torch.utils.tensorboard. // This is a modified version of the regex from src/client/telemetry/importTracker.ts @@ -11,28 +9,3 @@ import { noop } from '../common/utils/misc'; // RegEx to match `import torch.profiler` or `from torch import profiler` export const TorchProfilerImportRegEx = /^\s*(?:import (?:(\w+, )*torch\.profiler(, \w+)*))|(?:from torch import (?:(\w+, )*profiler(, \w+)*))/; -// RegEx to match `from torch.utils import tensorboard`, `import torch.utils.tensorboard`, `import tensorboardX`, `import tensorboard` -const TensorBoardImportRegEx = /^\s*(?:from torch\.utils\.tensorboard import \w+)|(?:from torch\.utils import (?:(\w+, )*tensorboard(, \w+)*))|(?:from tensorboardX import \w+)|(?:import (\w+, )*((torch\.utils\.tensorboard)|(tensorboardX)|(tensorboard))(, \w+)*)/; - -export function containsTensorBoardImport(lines: (string | undefined)[]): boolean { - try { - for (const s of lines) { - if (s && (TensorBoardImportRegEx.test(s) || TorchProfilerImportRegEx.test(s))) { - return true; - } - } - } catch { - // Don't care about failures. - noop(); - } - return false; -} - -export function containsNotebookExtension(lines: (string | undefined)[]): boolean { - for (const s of lines) { - if (s?.startsWith('%tensorboard') || s?.startsWith('%load_ext tensorboard')) { - return true; - } - } - return false; -} diff --git a/src/client/tensorBoard/nbextensionCodeLensProvider.ts b/src/client/tensorBoard/nbextensionCodeLensProvider.ts deleted file mode 100644 index 6d4c844cd392..000000000000 --- a/src/client/tensorBoard/nbextensionCodeLensProvider.ts +++ /dev/null @@ -1,67 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -import { inject, injectable } from 'inversify'; -import { once } from 'lodash'; -import { CancellationToken, CodeLens, Command, languages, Position, Range, TextDocument } from 'vscode'; -import { IExtensionSingleActivationService } from '../activation/types'; -import { Commands, NotebookCellScheme, PYTHON_LANGUAGE } from '../common/constants'; -import { IDisposableRegistry } from '../common/types'; -import { TensorBoard } from '../common/utils/localize'; -import { sendTelemetryEvent } from '../telemetry'; -import { EventName } from '../telemetry/constants'; -import { TensorBoardEntrypoint, TensorBoardEntrypointTrigger } from './constants'; -import { containsNotebookExtension } from './helpers'; - -@injectable() -export class TensorBoardNbextensionCodeLensProvider implements IExtensionSingleActivationService { - public readonly supportedWorkspaceTypes = { untrustedWorkspace: false, virtualWorkspace: false }; - - private sendTelemetryOnce = once( - sendTelemetryEvent.bind(this, EventName.TENSORBOARD_ENTRYPOINT_SHOWN, undefined, { - trigger: TensorBoardEntrypointTrigger.nbextension, - entrypoint: TensorBoardEntrypoint.codelens, - }), - ); - - constructor(@inject(IDisposableRegistry) private disposables: IDisposableRegistry) {} - - public async activate(): Promise { - this.activateInternal().ignoreErrors(); - } - - private async activateInternal() { - this.disposables.push( - languages.registerCodeLensProvider( - [ - { scheme: NotebookCellScheme, language: PYTHON_LANGUAGE }, - { scheme: 'vscode-notebook', language: PYTHON_LANGUAGE }, - ], - this, - ), - ); - } - - public provideCodeLenses(document: TextDocument, cancelToken: CancellationToken): CodeLens[] { - const command: Command = { - title: TensorBoard.launchNativeTensorBoardSessionCodeLens, - command: Commands.LaunchTensorBoard, - arguments: [ - { trigger: TensorBoardEntrypointTrigger.nbextension, entrypoint: TensorBoardEntrypoint.codelens }, - ], - }; - const codelenses: CodeLens[] = []; - for (let index = 0; index < document.lineCount; index += 1) { - if (cancelToken.isCancellationRequested) { - return codelenses; - } - const line = document.lineAt(index); - if (containsNotebookExtension([line.text])) { - const range = new Range(new Position(line.lineNumber, 0), new Position(line.lineNumber, 1)); - codelenses.push(new CodeLens(range, command)); - this.sendTelemetryOnce(); - } - } - return codelenses; - } -} diff --git a/src/client/tensorBoard/serviceRegistry.ts b/src/client/tensorBoard/serviceRegistry.ts index 8d16766f70c5..9f53af72053e 100644 --- a/src/client/tensorBoard/serviceRegistry.ts +++ b/src/client/tensorBoard/serviceRegistry.ts @@ -1,35 +1,11 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -import { IExtensionSingleActivationService } from '../activation/types'; import { IServiceManager } from '../ioc/types'; -import { TensorBoardImportCodeLensProvider } from './tensorBoardImportCodeLensProvider'; -import { TensorBoardFileWatcher } from './tensorBoardFileWatcher'; -import { TensorBoardUsageTracker } from './tensorBoardUsageTracker'; import { TensorBoardPrompt } from './tensorBoardPrompt'; -import { TensorBoardSessionProvider } from './tensorBoardSessionProvider'; -import { TensorBoardNbextensionCodeLensProvider } from './nbextensionCodeLensProvider'; -import { TerminalWatcher } from './terminalWatcher'; +import { TensorboardDependencyChecker } from './tensorboardDependencyChecker'; export function registerTypes(serviceManager: IServiceManager): void { - serviceManager.addSingleton(TensorBoardSessionProvider, TensorBoardSessionProvider); - serviceManager.addBinding(TensorBoardSessionProvider, IExtensionSingleActivationService); - serviceManager.addSingleton(TensorBoardFileWatcher, TensorBoardFileWatcher); - serviceManager.addBinding(TensorBoardFileWatcher, IExtensionSingleActivationService); serviceManager.addSingleton(TensorBoardPrompt, TensorBoardPrompt); - serviceManager.addSingleton( - IExtensionSingleActivationService, - TensorBoardUsageTracker, - ); - serviceManager.addSingleton( - TensorBoardImportCodeLensProvider, - TensorBoardImportCodeLensProvider, - ); - serviceManager.addBinding(TensorBoardImportCodeLensProvider, IExtensionSingleActivationService); - serviceManager.addSingleton( - TensorBoardNbextensionCodeLensProvider, - TensorBoardNbextensionCodeLensProvider, - ); - serviceManager.addBinding(TensorBoardNbextensionCodeLensProvider, IExtensionSingleActivationService); - serviceManager.addSingleton(IExtensionSingleActivationService, TerminalWatcher); + serviceManager.addSingleton(TensorboardDependencyChecker, TensorboardDependencyChecker); } diff --git a/src/client/tensorBoard/tensorBoardFileWatcher.ts b/src/client/tensorBoard/tensorBoardFileWatcher.ts deleted file mode 100644 index 81c62f1f8de3..000000000000 --- a/src/client/tensorBoard/tensorBoardFileWatcher.ts +++ /dev/null @@ -1,82 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -import { inject, injectable } from 'inversify'; -import { FileSystemWatcher, RelativePattern, WorkspaceFolder, WorkspaceFoldersChangeEvent } from 'vscode'; -import { IExtensionSingleActivationService } from '../activation/types'; -import { IWorkspaceService } from '../common/application/types'; -import { IDisposableRegistry } from '../common/types'; -import { TensorBoardEntrypointTrigger } from './constants'; -import { TensorBoardPrompt } from './tensorBoardPrompt'; - -@injectable() -export class TensorBoardFileWatcher implements IExtensionSingleActivationService { - public readonly supportedWorkspaceTypes = { untrustedWorkspace: false, virtualWorkspace: false }; - - private fileSystemWatchers = new Map(); - - private globPatterns = ['*tfevents*', '*/*tfevents*', '*/*/*tfevents*']; - - constructor( - @inject(IWorkspaceService) private workspaceService: IWorkspaceService, - @inject(TensorBoardPrompt) private tensorBoardPrompt: TensorBoardPrompt, - @inject(IDisposableRegistry) private readonly disposables: IDisposableRegistry, - ) {} - - public async activate(): Promise { - this.activateInternal().ignoreErrors(); - } - - private async activateInternal() { - const folders = this.workspaceService.workspaceFolders; - if (!folders) { - return; - } - - // If the user creates or changes tfevent files, listen for those too - for (const folder of folders) { - this.createFileSystemWatcher(folder); - } - - // If workspace folders change, ensure we update our FileSystemWatchers - this.disposables.push( - this.workspaceService.onDidChangeWorkspaceFolders((e) => this.updateFileSystemWatchers(e)), - ); - } - - private async updateFileSystemWatchers(event: WorkspaceFoldersChangeEvent) { - for (const added of event.added) { - this.createFileSystemWatcher(added); - } - for (const removed of event.removed) { - const fileSystemWatchers = this.fileSystemWatchers.get(removed); - if (fileSystemWatchers) { - fileSystemWatchers.forEach((fileWatcher) => fileWatcher.dispose()); - this.fileSystemWatchers.delete(removed); - } - } - } - - private createFileSystemWatcher(folder: WorkspaceFolder) { - const fileWatchers = []; - for (const pattern of this.globPatterns) { - const relativePattern = new RelativePattern(folder, pattern); - const fileSystemWatcher = this.workspaceService.createFileSystemWatcher(relativePattern); - - // When a file is created or changed that matches `this.globPattern`, try to show our prompt - this.disposables.push( - fileSystemWatcher.onDidCreate(() => - this.tensorBoardPrompt.showNativeTensorBoardPrompt(TensorBoardEntrypointTrigger.tfeventfiles), - ), - ); - this.disposables.push( - fileSystemWatcher.onDidChange(() => - this.tensorBoardPrompt.showNativeTensorBoardPrompt(TensorBoardEntrypointTrigger.tfeventfiles), - ), - ); - this.disposables.push(fileSystemWatcher); - fileWatchers.push(fileSystemWatcher); - } - this.fileSystemWatchers.set(folder, fileWatchers); - } -} diff --git a/src/client/tensorBoard/tensorBoardImportCodeLensProvider.ts b/src/client/tensorBoard/tensorBoardImportCodeLensProvider.ts deleted file mode 100644 index cac29b1d7e7a..000000000000 --- a/src/client/tensorBoard/tensorBoardImportCodeLensProvider.ts +++ /dev/null @@ -1,60 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -import { inject, injectable } from 'inversify'; -import { once } from 'lodash'; -import { CancellationToken, CodeLens, Command, languages, Position, Range, TextDocument } from 'vscode'; -import { IExtensionSingleActivationService } from '../activation/types'; -import { Commands, PYTHON } from '../common/constants'; -import { IDisposableRegistry } from '../common/types'; -import { TensorBoard } from '../common/utils/localize'; -import { sendTelemetryEvent } from '../telemetry'; -import { EventName } from '../telemetry/constants'; -import { TensorBoardEntrypoint, TensorBoardEntrypointTrigger } from './constants'; -import { containsTensorBoardImport } from './helpers'; - -@injectable() -export class TensorBoardImportCodeLensProvider implements IExtensionSingleActivationService { - public readonly supportedWorkspaceTypes = { untrustedWorkspace: false, virtualWorkspace: false }; - - private sendTelemetryOnce = once( - sendTelemetryEvent.bind(this, EventName.TENSORBOARD_ENTRYPOINT_SHOWN, undefined, { - trigger: TensorBoardEntrypointTrigger.fileimport, - entrypoint: TensorBoardEntrypoint.codelens, - }), - ); - - constructor(@inject(IDisposableRegistry) private disposables: IDisposableRegistry) {} - - public async activate(): Promise { - this.activateInternal().ignoreErrors(); - } - - // eslint-disable-next-line class-methods-use-this - public provideCodeLenses(document: TextDocument, cancelToken: CancellationToken): CodeLens[] { - const command: Command = { - title: TensorBoard.launchNativeTensorBoardSessionCodeLens, - command: Commands.LaunchTensorBoard, - arguments: [ - { trigger: TensorBoardEntrypointTrigger.fileimport, entrypoint: TensorBoardEntrypoint.codelens }, - ], - }; - const codelenses: CodeLens[] = []; - for (let index = 0; index < document.lineCount; index += 1) { - if (cancelToken.isCancellationRequested) { - return codelenses; - } - const line = document.lineAt(index); - if (containsTensorBoardImport([line.text])) { - const range = new Range(new Position(line.lineNumber, 0), new Position(line.lineNumber, 1)); - codelenses.push(new CodeLens(range, command)); - this.sendTelemetryOnce(); - } - } - return codelenses; - } - - private async activateInternal() { - this.disposables.push(languages.registerCodeLensProvider(PYTHON, this)); - } -} diff --git a/src/client/tensorBoard/tensorBoardPrompt.ts b/src/client/tensorBoard/tensorBoardPrompt.ts index 1c03a696dc1d..563419bd4ea6 100644 --- a/src/client/tensorBoard/tensorBoardPrompt.ts +++ b/src/client/tensorBoard/tensorBoardPrompt.ts @@ -2,14 +2,7 @@ // Licensed under the MIT License. import { inject, injectable } from 'inversify'; -import { once } from 'lodash'; -import { IApplicationShell, ICommandManager } from '../common/application/types'; -import { Commands } from '../common/constants'; import { IPersistentState, IPersistentStateFactory } from '../common/types'; -import { Common, TensorBoard } from '../common/utils/localize'; -import { sendTelemetryEvent } from '../telemetry'; -import { EventName } from '../telemetry/constants'; -import { TensorBoardEntrypoint, TensorBoardEntrypointTrigger, TensorBoardPromptSelection } from './constants'; enum TensorBoardPromptStateKeys { ShowNativeTensorBoardPrompt = 'showNativeTensorBoardPrompt', @@ -19,76 +12,14 @@ enum TensorBoardPromptStateKeys { export class TensorBoardPrompt { private state: IPersistentState; - private enabled: boolean; - - private enabledInCurrentSession = true; - - private waitingForUserSelection = false; - - private sendTelemetryOnce = once((trigger) => { - sendTelemetryEvent(EventName.TENSORBOARD_ENTRYPOINT_SHOWN, undefined, { - entrypoint: TensorBoardEntrypoint.prompt, - trigger, - }); - }); - - constructor( - @inject(IApplicationShell) private applicationShell: IApplicationShell, - @inject(ICommandManager) private commandManager: ICommandManager, - @inject(IPersistentStateFactory) private persistentStateFactory: IPersistentStateFactory, - ) { + constructor(@inject(IPersistentStateFactory) private persistentStateFactory: IPersistentStateFactory) { this.state = this.persistentStateFactory.createWorkspacePersistentState( TensorBoardPromptStateKeys.ShowNativeTensorBoardPrompt, true, ); - this.enabled = this.isPromptEnabled(); } - public async showNativeTensorBoardPrompt(trigger: TensorBoardEntrypointTrigger): Promise { - if (this.enabled && this.enabledInCurrentSession && !this.waitingForUserSelection) { - const yes = Common.bannerLabelYes; - const no = Common.bannerLabelNo; - const doNotAskAgain = Common.doNotShowAgain; - const options = [yes, no, doNotAskAgain]; - this.waitingForUserSelection = true; - this.sendTelemetryOnce(trigger); - const selection = await this.applicationShell.showInformationMessage( - TensorBoard.nativeTensorBoardPrompt, - ...options, - ); - this.waitingForUserSelection = false; - this.enabledInCurrentSession = false; - let telemetrySelection = TensorBoardPromptSelection.None; - switch (selection) { - case yes: - telemetrySelection = TensorBoardPromptSelection.Yes; - await this.commandManager.executeCommand( - Commands.LaunchTensorBoard, - TensorBoardEntrypoint.prompt, - trigger, - ); - break; - case doNotAskAgain: - telemetrySelection = TensorBoardPromptSelection.DoNotAskAgain; - await this.disablePrompt(); - break; - case no: - telemetrySelection = TensorBoardPromptSelection.No; - break; - default: - break; - } - sendTelemetryEvent(EventName.TENSORBOARD_LAUNCH_PROMPT_SELECTION, undefined, { - selection: telemetrySelection, - }); - } - } - - private isPromptEnabled(): boolean { + public isPromptEnabled(): boolean { return this.state.value; } - - private async disablePrompt() { - await this.state.updateValue(false); - } } diff --git a/src/client/tensorBoard/tensorBoardSession.ts b/src/client/tensorBoard/tensorBoardSession.ts index 1d24e8c313f7..b18202810e45 100644 --- a/src/client/tensorBoard/tensorBoardSession.ts +++ b/src/client/tensorBoard/tensorBoardSession.ts @@ -1,57 +1,18 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -import * as fs from 'fs-extra'; -import { ChildProcess } from 'child_process'; -import * as path from 'path'; -import { - CancellationToken, - CancellationTokenSource, - env, - Event, - EventEmitter, - l10n, - Position, - Progress, - ProgressLocation, - ProgressOptions, - QuickPickItem, - Selection, - TextEditorRevealType, - Uri, - ViewColumn, - WebviewPanel, - WebviewPanelOnDidChangeViewStateEvent, - window, - workspace, -} from 'vscode'; -import { IApplicationShell, ICommandManager, IWorkspaceService } from '../common/application/types'; +import { CancellationTokenSource, Uri } from 'vscode'; +import { IApplicationShell, ICommandManager } from '../common/application/types'; import { createPromiseFromCancellation } from '../common/cancellation'; -import { tensorboardLauncher } from '../common/process/internal/scripts'; -import { IPythonExecutionFactory, ObservableExecutionResult } from '../common/process/types'; -import { - IDisposableRegistry, - IInstaller, - InstallerResponse, - ProductInstallStatus, - Product, - IPersistentState, - IConfigurationService, -} from '../common/types'; -import { createDeferred, sleep } from '../common/utils/async'; +import { IInstaller, InstallerResponse, ProductInstallStatus, Product } from '../common/types'; import { Common, TensorBoard } from '../common/utils/localize'; -import { StopWatch } from '../common/utils/stopWatch'; import { IInterpreterService } from '../interpreter/contracts'; import { sendTelemetryEvent } from '../telemetry'; import { EventName } from '../telemetry/constants'; import { ImportTracker } from '../telemetry/importTracker'; -import { TensorBoardPromptSelection, TensorBoardSessionStartResult } from './constants'; -import { IMultiStepInputFactory } from '../common/utils/multiStepInput'; +import { TensorBoardPromptSelection } from './constants'; import { ModuleInstallFlags } from '../common/installer/types'; import { traceError, traceVerbose } from '../logging'; -enum Messages { - JumpToSource = 'jump_to_source', -} const TensorBoardSemVerRequirement = '>= 2.4.1'; const TorchProfilerSemVerRequirement = '>= 0.2.0'; @@ -66,85 +27,13 @@ const TorchProfilerSemVerRequirement = '>= 0.2.0'; * - shuts down the TensorBoard process when the webview is closed */ export class TensorBoardSession { - public get panel(): WebviewPanel | undefined { - return this.webviewPanel; - } - - public get daemon(): ChildProcess | undefined { - return this.process; - } - - private _active = false; - - private webviewPanel: WebviewPanel | undefined; - - private url: string | undefined; - - private process: ChildProcess | undefined; - - private onDidChangeViewStateEventEmitter = new EventEmitter(); - - private onDidDisposeEventEmitter = new EventEmitter(); - - // This tracks the total duration of time that the user kept the TensorBoard panel open - private sessionDurationStopwatch: StopWatch | undefined; - constructor( private readonly installer: IInstaller, private readonly interpreterService: IInterpreterService, - private readonly workspaceService: IWorkspaceService, - private readonly pythonExecFactory: IPythonExecutionFactory, private readonly commandManager: ICommandManager, - private readonly disposables: IDisposableRegistry, private readonly applicationShell: IApplicationShell, - private readonly globalMemento: IPersistentState, - private readonly multiStepFactory: IMultiStepInputFactory, - private readonly configurationService: IConfigurationService, ) {} - public get onDidDispose(): Event { - return this.onDidDisposeEventEmitter.event; - } - - public get onDidChangeViewState(): Event { - return this.onDidChangeViewStateEventEmitter.event; - } - - public get active(): boolean { - return this._active; - } - - public async refresh(): Promise { - if (!this.webviewPanel) { - return; - } - this.webviewPanel.webview.html = ''; - this.webviewPanel.webview.html = await this.getHtml(); - } - - public async initialize(): Promise { - const e2eStartupDurationStopwatch = new StopWatch(); - const tensorBoardWasInstalled = await this.ensurePrerequisitesAreInstalled(); - if (!tensorBoardWasInstalled) { - return; - } - const logDir = await this.getLogDirectory(); - if (!logDir) { - return; - } - const startedSuccessfully = await this.startTensorboardSession(logDir); - if (startedSuccessfully) { - await this.showPanel(); - // Not using captureTelemetry on this method as we only want to send - // this particular telemetry event if the whole session creation succeeded - sendTelemetryEvent( - EventName.TENSORBOARD_SESSION_E2E_STARTUP_DURATION, - e2eStartupDurationStopwatch.elapsedTime, - ); - } - this.sessionDurationStopwatch = new StopWatch(); - } - private async promptToInstall( tensorBoardInstallStatus: ProductInstallStatus, profilerPluginInstallStatus: ProductInstallStatus, @@ -189,10 +78,10 @@ export class TensorBoardSession { // to start a TensorBoard session. If the user has a torch import in // any of their open documents, also try to install the torch-tb-plugin // package, but don't block if installing that fails. - private async ensurePrerequisitesAreInstalled() { + public async ensurePrerequisitesAreInstalled(resource?: Uri): Promise { traceVerbose('Ensuring TensorBoard package is installed into active interpreter'); const interpreter = - (await this.interpreterService.getActiveInterpreter()) || + (await this.interpreterService.getActiveInterpreter(resource)) || (await this.commandManager.executeCommand('python.setInterpreter')); if (!interpreter) { return false; @@ -291,376 +180,4 @@ export class TensorBoardSession { } return tensorboardInstallStatus === ProductInstallStatus.Installed; } - - private async showFilePicker(): Promise { - const selection = await this.applicationShell.showOpenDialog({ - canSelectFiles: false, - canSelectFolders: true, - canSelectMany: false, - }); - // If the user selected a folder, return the uri.fsPath - // There will only be one selection since canSelectMany: false - if (selection) { - return selection[0].fsPath; - } - return undefined; - } - - // eslint-disable-next-line class-methods-use-this - private getQuickPickItems(logDir: string | undefined) { - const items = []; - - if (logDir) { - const useCwd = { - label: TensorBoard.useCurrentWorkingDirectory, - detail: TensorBoard.useCurrentWorkingDirectoryDetail, - }; - const selectAnotherFolder = { - label: TensorBoard.selectAnotherFolder, - detail: TensorBoard.selectAnotherFolderDetail, - }; - items.push(useCwd, selectAnotherFolder); - } else { - const selectAFolder = { - label: TensorBoard.selectAFolder, - detail: TensorBoard.selectAFolderDetail, - }; - items.push(selectAFolder); - } - - items.push({ - label: TensorBoard.enterRemoteUrl, - detail: TensorBoard.enterRemoteUrlDetail, - }); - - return items; - } - - // Display a quickpick asking the user to acknowledge our autopopulated log directory or - // select a new one using the file picker. Default this to the folder that is open in - // the editor, if any, then the directory that the active text editor is in, if any. - private async getLogDirectory(): Promise { - // See if the user told us to always use a specific log directory - const settings = this.configurationService.getSettings(); - const settingValue = settings.tensorBoard?.logDirectory; - if (settingValue) { - traceVerbose(`Using log directory resolved by python.tensorBoard.logDirectory setting: ${settingValue}`); - return settingValue; - } - // No log directory in settings. Ask the user which directory to use - const logDir = this.autopopulateLogDirectoryPath(); - const { useCurrentWorkingDirectory } = TensorBoard; - const { selectAFolder } = TensorBoard; - const { selectAnotherFolder } = TensorBoard; - const { enterRemoteUrl } = TensorBoard; - const items: QuickPickItem[] = this.getQuickPickItems(logDir); - const item = await this.applicationShell.showQuickPick(items, { - canPickMany: false, - ignoreFocusOut: false, - placeHolder: logDir ? l10n.t('Current: {0}', logDir) : undefined, - }); - switch (item?.label) { - case useCurrentWorkingDirectory: - return logDir; - case selectAFolder: - case selectAnotherFolder: - return this.showFilePicker(); - case enterRemoteUrl: - return this.applicationShell.showInputBox({ - prompt: TensorBoard.enterRemoteUrlDetail, - }); - default: - return undefined; - } - } - - // Spawn a process which uses TensorBoard's Python API to start a TensorBoard session. - // Times out if it hasn't started up after 1 minute. - // Hold on to the process so we can kill it when the webview is closed. - private async startTensorboardSession(logDir: string): Promise { - const interpreter = await this.interpreterService.getActiveInterpreter(); - if (!interpreter) { - return false; - } - - // Timeout waiting for TensorBoard to start after 60 seconds. - // This is the same time limit that TensorBoard itself uses when waiting for - // its webserver to start up. - const timeout = 60_000; - - // Display a progress indicator as TensorBoard takes at least a couple seconds to launch - const progressOptions: ProgressOptions = { - title: TensorBoard.progressMessage, - location: ProgressLocation.Notification, - cancellable: true, - }; - - const processService = await this.pythonExecFactory.createActivatedEnvironment({ - allowEnvironmentFetchExceptions: true, - interpreter, - }); - const args = tensorboardLauncher([logDir]); - const sessionStartStopwatch = new StopWatch(); - const observable = processService.execObservable(args, {}); - - const result = await this.applicationShell.withProgress( - progressOptions, - (_progress: Progress, token: CancellationToken) => { - traceVerbose(`Starting TensorBoard with log directory ${logDir}...`); - - const spawnTensorBoard = this.waitForTensorBoardStart(observable); - const userCancellation = createPromiseFromCancellation({ - token, - cancelAction: 'resolve', - defaultValue: 'canceled', - }); - - return Promise.race([sleep(timeout), spawnTensorBoard, userCancellation]); - }, - ); - - switch (result) { - case 'canceled': - traceVerbose('Canceled starting TensorBoard session.'); - sendTelemetryEvent( - EventName.TENSORBOARD_SESSION_DAEMON_STARTUP_DURATION, - sessionStartStopwatch.elapsedTime, - { - result: TensorBoardSessionStartResult.cancel, - }, - ); - observable.dispose(); - return false; - case 'success': - this.process = observable.proc; - sendTelemetryEvent( - EventName.TENSORBOARD_SESSION_DAEMON_STARTUP_DURATION, - sessionStartStopwatch.elapsedTime, - { - result: TensorBoardSessionStartResult.success, - }, - ); - return true; - case timeout: - sendTelemetryEvent( - EventName.TENSORBOARD_SESSION_DAEMON_STARTUP_DURATION, - sessionStartStopwatch.elapsedTime, - { - result: TensorBoardSessionStartResult.error, - }, - ); - throw new Error(`Timed out after ${timeout / 1000} seconds waiting for TensorBoard to launch.`); - default: - // We should never get here - throw new Error(`Failed to start TensorBoard, received unknown promise result: ${result}`); - } - } - - private async waitForTensorBoardStart(observable: ObservableExecutionResult) { - const urlThatTensorBoardIsRunningAt = createDeferred(); - - observable.out.subscribe({ - next: (output) => { - if (output.source === 'stdout') { - const match = output.out.match(/TensorBoard started at (.*)/); - if (match && match[1]) { - // eslint-disable-next-line prefer-destructuring - this.url = match[1]; - urlThatTensorBoardIsRunningAt.resolve('success'); - } - traceVerbose(output.out); - } else if (output.source === 'stderr') { - traceError(output.out); - } - }, - error: (err) => { - traceError(err); - }, - }); - - return urlThatTensorBoardIsRunningAt.promise; - } - - private async showPanel() { - traceVerbose('Showing TensorBoard panel'); - const panel = this.webviewPanel || (await this.createPanel()); - panel.reveal(); - this._active = true; - this.onDidChangeViewStateEventEmitter.fire(); - } - - private async createPanel() { - const webviewPanel = window.createWebviewPanel('tensorBoardSession', 'TensorBoard', this.globalMemento.value, { - enableScripts: true, - retainContextWhenHidden: true, - }); - webviewPanel.webview.html = await this.getHtml(); - this.webviewPanel = webviewPanel; - this.disposables.push( - webviewPanel.onDidDispose(() => { - this.webviewPanel = undefined; - // Kill the running TensorBoard session - this.process?.kill(); - sendTelemetryEvent(EventName.TENSORBOARD_SESSION_DURATION, this.sessionDurationStopwatch?.elapsedTime); - this.process = undefined; - this._active = false; - this.onDidDisposeEventEmitter.fire(this); - }), - ); - this.disposables.push( - webviewPanel.onDidChangeViewState(async (args: WebviewPanelOnDidChangeViewStateEvent) => { - // The webview has been moved to a different viewgroup if it was active before and remains active now - if (this.active && args.webviewPanel.active) { - await this.globalMemento.updateValue(webviewPanel.viewColumn ?? ViewColumn.Active); - } - this._active = args.webviewPanel.active; - this.onDidChangeViewStateEventEmitter.fire(); - }), - ); - this.disposables.push( - webviewPanel.webview.onDidReceiveMessage((message) => { - // Handle messages posted from the webview - switch (message.command) { - case Messages.JumpToSource: - void this.jumpToSource(message.args.filename, message.args.line); - break; - default: - break; - } - }), - ); - return webviewPanel; - } - - private autopopulateLogDirectoryPath(): string | undefined { - if (this.workspaceService.rootPath) { - return this.workspaceService.rootPath; - } - const { activeTextEditor } = window; - if (activeTextEditor) { - return path.dirname(activeTextEditor.document.uri.fsPath); - } - return undefined; - } - - private async jumpToSource(fsPath: string, line: number) { - sendTelemetryEvent(EventName.TENSORBOARD_JUMP_TO_SOURCE_REQUEST); - let uri: Uri | undefined; - if (fs.existsSync(fsPath)) { - uri = Uri.file(fsPath); - } else { - sendTelemetryEvent(EventName.TENSORBOARD_JUMP_TO_SOURCE_FILE_NOT_FOUND); - traceError( - `Requested jump to source filepath ${fsPath} does not exist. Prompting user to select source file...`, - ); - // Prompt the user to pick the file on disk - const items: QuickPickItem[] = [ - { - label: TensorBoard.selectMissingSourceFile, - description: TensorBoard.selectMissingSourceFileDescription, - }, - ]; - // Using a multistep so that we can add a title to the quickpick - const multiStep = this.multiStepFactory.create(); - await multiStep.run(async (input) => { - const selection = await input.showQuickPick({ - items, - title: TensorBoard.missingSourceFile, - placeholder: fsPath, - }); - switch (selection?.label) { - case TensorBoard.selectMissingSourceFile: { - const filePickerSelection = await this.applicationShell.showOpenDialog({ - canSelectFiles: true, - canSelectFolders: false, - canSelectMany: false, - }); - if (filePickerSelection !== undefined) { - [uri] = filePickerSelection; - } - break; - } - default: - break; - } - }, {}); - } - if (uri === undefined) { - return; - } - const document = await workspace.openTextDocument(uri); - const editor = await window.showTextDocument(document, ViewColumn.Beside); - // Select the line if it exists in the document - if (line < editor.document.lineCount) { - const position = new Position(line, 0); - const selection = new Selection(position, editor.document.lineAt(line).range.end); - editor.selection = selection; - editor.revealRange(selection, TextEditorRevealType.InCenterIfOutsideViewport); - } - } - - private async getHtml() { - // We cannot cache the result of calling asExternalUri, so regenerate - // it each time. From docs: "Note that extensions should not cache the - // result of asExternalUri as the resolved uri may become invalid due - // to a system or user action β€” for example, in remote cases, a user may - // close a port forwarding tunnel that was opened by asExternalUri." - const fullWebServerUri = await env.asExternalUri(Uri.parse(this.url!)); - return ` - - - - - - TensorBoard - - - - - - - `; - } } diff --git a/src/client/tensorBoard/tensorBoardSessionProvider.ts b/src/client/tensorBoard/tensorBoardSessionProvider.ts deleted file mode 100644 index 53878bd543c2..000000000000 --- a/src/client/tensorBoard/tensorBoardSessionProvider.ts +++ /dev/null @@ -1,127 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -import { inject, injectable } from 'inversify'; -import { l10n, ViewColumn } from 'vscode'; -import { IExtensionSingleActivationService } from '../activation/types'; -import { IApplicationShell, ICommandManager, IWorkspaceService } from '../common/application/types'; -import { Commands } from '../common/constants'; -import { ContextKey } from '../common/contextKey'; -import { IPythonExecutionFactory } from '../common/process/types'; -import { - IDisposableRegistry, - IInstaller, - IPersistentState, - IPersistentStateFactory, - IConfigurationService, -} from '../common/types'; -import { IMultiStepInputFactory } from '../common/utils/multiStepInput'; -import { IInterpreterService } from '../interpreter/contracts'; -import { traceError, traceVerbose } from '../logging'; -import { sendTelemetryEvent } from '../telemetry'; -import { EventName } from '../telemetry/constants'; -import { TensorBoardEntrypoint, TensorBoardEntrypointTrigger } from './constants'; -import { TensorBoardSession } from './tensorBoardSession'; - -const PREFERRED_VIEWGROUP = 'PythonTensorBoardWebviewPreferredViewGroup'; - -@injectable() -export class TensorBoardSessionProvider implements IExtensionSingleActivationService { - public readonly supportedWorkspaceTypes = { untrustedWorkspace: false, virtualWorkspace: false }; - - private knownSessions: TensorBoardSession[] = []; - - private preferredViewGroupMemento: IPersistentState; - - private hasActiveTensorBoardSessionContext: ContextKey; - - constructor( - @inject(IInstaller) private readonly installer: IInstaller, - @inject(IInterpreterService) private readonly interpreterService: IInterpreterService, - @inject(IApplicationShell) private readonly applicationShell: IApplicationShell, - @inject(IWorkspaceService) private readonly workspaceService: IWorkspaceService, - @inject(ICommandManager) private readonly commandManager: ICommandManager, - @inject(IDisposableRegistry) private readonly disposables: IDisposableRegistry, - @inject(IPythonExecutionFactory) private readonly pythonExecFactory: IPythonExecutionFactory, - @inject(IPersistentStateFactory) private stateFactory: IPersistentStateFactory, - @inject(IMultiStepInputFactory) private readonly multiStepFactory: IMultiStepInputFactory, - @inject(IConfigurationService) private readonly configurationService: IConfigurationService, - ) { - this.preferredViewGroupMemento = this.stateFactory.createGlobalPersistentState( - PREFERRED_VIEWGROUP, - ViewColumn.Active, - ); - this.hasActiveTensorBoardSessionContext = new ContextKey( - 'python.hasActiveTensorBoardSession', - this.commandManager, - ); - } - - public async activate(): Promise { - this.disposables.push( - this.commandManager.registerCommand( - Commands.LaunchTensorBoard, - ( - entrypoint: TensorBoardEntrypoint = TensorBoardEntrypoint.palette, - trigger: TensorBoardEntrypointTrigger = TensorBoardEntrypointTrigger.palette, - ) => { - sendTelemetryEvent(EventName.TENSORBOARD_SESSION_LAUNCH, undefined, { - trigger, - entrypoint, - }); - return this.createNewSession(); - }, - ), - this.commandManager.registerCommand(Commands.RefreshTensorBoard, () => - this.knownSessions.map((w) => w.refresh()), - ), - ); - } - - private async updateTensorBoardSessionContext() { - let hasActiveTensorBoardSession = false; - this.knownSessions.forEach((viewer) => { - if (viewer.active) { - hasActiveTensorBoardSession = true; - } - }); - await this.hasActiveTensorBoardSessionContext.set(hasActiveTensorBoardSession); - } - - private async didDisposeSession(session: TensorBoardSession) { - this.knownSessions = this.knownSessions.filter((s) => s !== session); - this.updateTensorBoardSessionContext(); - } - - private async createNewSession(): Promise { - traceVerbose('Starting new TensorBoard session...'); - try { - const newSession = new TensorBoardSession( - this.installer, - this.interpreterService, - this.workspaceService, - this.pythonExecFactory, - this.commandManager, - this.disposables, - this.applicationShell, - this.preferredViewGroupMemento, - this.multiStepFactory, - this.configurationService, - ); - newSession.onDidChangeViewState(() => this.updateTensorBoardSessionContext(), this, this.disposables); - newSession.onDidDispose((e) => this.didDisposeSession(e), this, this.disposables); - this.knownSessions.push(newSession); - await newSession.initialize(); - return newSession; - } catch (e) { - traceError(`Encountered error while starting new TensorBoard session: ${e}`); - await this.applicationShell.showErrorMessage( - l10n.t( - 'We failed to start a TensorBoard session due to the following error: {0}', - (e as Error).message, - ), - ); - } - return undefined; - } -} diff --git a/src/client/tensorBoard/tensorBoardUsageTracker.ts b/src/client/tensorBoard/tensorBoardUsageTracker.ts deleted file mode 100644 index 99d82949dcfd..000000000000 --- a/src/client/tensorBoard/tensorBoardUsageTracker.ts +++ /dev/null @@ -1,62 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -import { inject, injectable } from 'inversify'; -import * as path from 'path'; -import { TextEditor } from 'vscode'; -import { IExtensionSingleActivationService } from '../activation/types'; -import { IDocumentManager } from '../common/application/types'; -import { isTestExecution } from '../common/constants'; -import { IDisposableRegistry } from '../common/types'; -import { getDocumentLines } from '../telemetry/importTracker'; -import { TensorBoardEntrypointTrigger } from './constants'; -import { containsTensorBoardImport } from './helpers'; -import { TensorBoardPrompt } from './tensorBoardPrompt'; - -const testExecution = isTestExecution(); - -// Prompt the user to start an integrated TensorBoard session whenever the active Python file or Python notebook -// contains a valid TensorBoard import. -@injectable() -export class TensorBoardUsageTracker implements IExtensionSingleActivationService { - public readonly supportedWorkspaceTypes = { untrustedWorkspace: false, virtualWorkspace: false }; - - constructor( - @inject(IDocumentManager) private documentManager: IDocumentManager, - @inject(IDisposableRegistry) private disposables: IDisposableRegistry, - @inject(TensorBoardPrompt) private prompt: TensorBoardPrompt, - ) {} - - public async activate(): Promise { - if (testExecution) { - await this.activateInternal(); - } else { - this.activateInternal().ignoreErrors(); - } - } - - private async activateInternal() { - // Process currently active text editor - this.onChangedActiveTextEditor(this.documentManager.activeTextEditor); - // Process changes to active text editor as well - this.documentManager.onDidChangeActiveTextEditor( - (e) => this.onChangedActiveTextEditor(e), - this, - this.disposables, - ); - } - - private onChangedActiveTextEditor(editor: TextEditor | undefined): void { - if (!editor || !editor.document) { - return; - } - const { document } = editor; - const extName = path.extname(document.fileName).toLowerCase(); - if (extName === '.py' || (extName === '.ipynb' && document.languageId === 'python')) { - const lines = getDocumentLines(document); - if (containsTensorBoardImport(lines)) { - this.prompt.showNativeTensorBoardPrompt(TensorBoardEntrypointTrigger.fileimport).ignoreErrors(); - } - } - } -} diff --git a/src/client/tensorBoard/tensorboardDependencyChecker.ts b/src/client/tensorBoard/tensorboardDependencyChecker.ts new file mode 100644 index 000000000000..995344284eec --- /dev/null +++ b/src/client/tensorBoard/tensorboardDependencyChecker.ts @@ -0,0 +1,30 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { inject, injectable } from 'inversify'; +import { Uri } from 'vscode'; +import { IApplicationShell, ICommandManager } from '../common/application/types'; +import { IInstaller } from '../common/types'; +import { IInterpreterService } from '../interpreter/contracts'; +import { TensorBoardSession } from './tensorBoardSession'; + +@injectable() +export class TensorboardDependencyChecker { + constructor( + @inject(IInstaller) private readonly installer: IInstaller, + @inject(IInterpreterService) private readonly interpreterService: IInterpreterService, + @inject(IApplicationShell) private readonly applicationShell: IApplicationShell, + @inject(ICommandManager) private readonly commandManager: ICommandManager, + ) {} + + public async ensureDependenciesAreInstalled(resource?: Uri): Promise { + const newSession = new TensorBoardSession( + this.installer, + this.interpreterService, + this.commandManager, + this.applicationShell, + ); + const result = await newSession.ensurePrerequisitesAreInstalled(resource); + return result; + } +} diff --git a/src/client/tensorBoard/tensorboardIntegration.ts b/src/client/tensorBoard/tensorboardIntegration.ts new file mode 100644 index 000000000000..f3cbad59977b --- /dev/null +++ b/src/client/tensorBoard/tensorboardIntegration.ts @@ -0,0 +1,88 @@ +/* eslint-disable comma-dangle */ + +/* eslint-disable implicit-arrow-linebreak */ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { inject, injectable } from 'inversify'; +import { Extension, Uri } from 'vscode'; +import { IWorkspaceService } from '../common/application/types'; +import { TENSORBOARD_EXTENSION_ID } from '../common/constants'; +import { IExtensions, Resource } from '../common/types'; +import { IEnvironmentActivationService } from '../interpreter/activation/types'; +import { TensorBoardPrompt } from './tensorBoardPrompt'; +import { TensorboardDependencyChecker } from './tensorboardDependencyChecker'; + +type PythonApiForTensorboardExtension = { + /** + * Gets activated env vars for the active Python Environment for the given resource. + */ + getActivatedEnvironmentVariables(resource: Resource): Promise; + /** + * Ensures that the dependencies required for TensorBoard are installed in Active Environment for the given resource. + */ + ensureDependenciesAreInstalled(resource?: Uri): Promise; + /** + * Whether to allow displaying tensorboard prompt. + */ + isPromptEnabled(): boolean; +}; + +type TensorboardExtensionApi = { + /** + * Registers python extension specific parts with the tensorboard extension + */ + registerPythonApi(interpreterService: PythonApiForTensorboardExtension): void; +}; + +@injectable() +export class TensorboardExtensionIntegration { + private tensorboardExtension: Extension | undefined; + + constructor( + @inject(IExtensions) private readonly extensions: IExtensions, + @inject(IEnvironmentActivationService) private readonly envActivation: IEnvironmentActivationService, + @inject(IWorkspaceService) private workspaceService: IWorkspaceService, + @inject(TensorboardDependencyChecker) private readonly dependencyChcker: TensorboardDependencyChecker, + @inject(TensorBoardPrompt) private readonly tensorBoardPrompt: TensorBoardPrompt, + ) {} + + public registerApi(tensorboardExtensionApi: TensorboardExtensionApi): TensorboardExtensionApi | undefined { + if (!this.workspaceService.isTrusted) { + this.workspaceService.onDidGrantWorkspaceTrust(() => this.registerApi(tensorboardExtensionApi)); + return undefined; + } + tensorboardExtensionApi.registerPythonApi({ + getActivatedEnvironmentVariables: async (resource: Resource) => + this.envActivation.getActivatedEnvironmentVariables(resource, undefined, true), + ensureDependenciesAreInstalled: async (resource?: Uri): Promise => + this.dependencyChcker.ensureDependenciesAreInstalled(resource), + isPromptEnabled: () => this.tensorBoardPrompt.isPromptEnabled(), + }); + return undefined; + } + + public async integrateWithTensorboardExtension(): Promise { + const api = await this.getExtensionApi(); + if (api) { + this.registerApi(api); + } + } + + private async getExtensionApi(): Promise { + if (!this.tensorboardExtension) { + const extension = this.extensions.getExtension(TENSORBOARD_EXTENSION_ID); + if (!extension) { + return undefined; + } + await extension.activate(); + if (extension.isActive) { + this.tensorboardExtension = extension; + return this.tensorboardExtension.exports; + } + } else { + return this.tensorboardExtension.exports; + } + return undefined; + } +} diff --git a/src/client/tensorBoard/terminalWatcher.ts b/src/client/tensorBoard/terminalWatcher.ts deleted file mode 100644 index 5aadc12dc4c0..000000000000 --- a/src/client/tensorBoard/terminalWatcher.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { inject, injectable } from 'inversify'; -import { window } from 'vscode'; -import { IExtensionSingleActivationService } from '../activation/types'; -import { IDisposable, IDisposableRegistry } from '../common/types'; -import { sendTelemetryEvent } from '../telemetry'; -import { EventName } from '../telemetry/constants'; - -// Every 5 min look, through active terminals to see if any are running `tensorboard` -@injectable() -export class TerminalWatcher implements IExtensionSingleActivationService, IDisposable { - public readonly supportedWorkspaceTypes = { untrustedWorkspace: false, virtualWorkspace: false }; - - private handle: NodeJS.Timeout | undefined; - - constructor(@inject(IDisposableRegistry) private disposables: IDisposableRegistry) {} - - public async activate(): Promise { - const handle = setInterval(() => { - // When user runs a command in VSCode terminal, the terminal's name - // becomes the program that is currently running. Since tensorboard - // stays running in the terminal while the webapp is running and - // until the user kills it, the terminal with the updated name should - // stick around for long enough that we only need to run this check - // every 5 min or so - const matches = window.terminals.filter((terminal) => terminal.name === 'tensorboard'); - if (matches.length > 0) { - sendTelemetryEvent(EventName.TENSORBOARD_DETECTED_IN_INTEGRATED_TERMINAL); - clearInterval(handle); // Only need telemetry sent once per VS Code session - } - }, 300_000); - this.handle = handle; - this.disposables.push(this); - } - - public dispose(): void { - if (this.handle) { - clearInterval(this.handle); - } - } -} diff --git a/src/client/tensorBoard/types.ts b/src/client/tensorBoard/types.ts deleted file mode 100644 index 6e2c274d63f4..000000000000 --- a/src/client/tensorBoard/types.ts +++ /dev/null @@ -1,9 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -import { Event } from 'vscode'; - -export const ITensorBoardImportTracker = Symbol('ITensorBoardImportTracker'); -export interface ITensorBoardImportTracker { - onDidImportTensorBoard: Event; -} diff --git a/src/client/terminals/activation.ts b/src/client/terminals/activation.ts index 143a2de14e5c..ed26916e3eaa 100644 --- a/src/client/terminals/activation.ts +++ b/src/client/terminals/activation.ts @@ -9,6 +9,7 @@ import { IActiveResourceService, ITerminalManager } from '../common/application/ import { ITerminalActivator } from '../common/terminal/types'; import { IDisposable, IDisposableRegistry } from '../common/types'; import { ITerminalAutoActivation } from './types'; +import { shouldEnvExtHandleActivation } from '../envExt/api.internal'; @injectable() export class TerminalAutoActivation implements ITerminalAutoActivation { @@ -49,6 +50,9 @@ export class TerminalAutoActivation implements ITerminalAutoActivation { if (this.terminalsNotToAutoActivate.has(terminal)) { return; } + if (shouldEnvExtHandleActivation()) { + return; + } if ('hideFromUser' in terminal.creationOptions && terminal.creationOptions.hideFromUser) { return; } diff --git a/src/client/terminals/codeExecution/codeExecutionManager.ts b/src/client/terminals/codeExecution/codeExecutionManager.ts index ed671f2846a2..48165adcd169 100644 --- a/src/client/terminals/codeExecution/codeExecutionManager.ts +++ b/src/client/terminals/codeExecution/codeExecutionManager.ts @@ -4,20 +4,25 @@ 'use strict'; import { inject, injectable } from 'inversify'; -import { Disposable, Event, EventEmitter, Uri } from 'vscode'; - +import { Disposable, EventEmitter, Terminal, Uri } from 'vscode'; +import * as path from 'path'; import { ICommandManager, IDocumentManager } from '../../common/application/types'; import { Commands } from '../../common/constants'; import '../../common/extensions'; -import { IFileSystem } from '../../common/platform/types'; import { IDisposableRegistry, IConfigurationService, Resource } from '../../common/types'; import { noop } from '../../common/utils/misc'; import { IInterpreterService } from '../../interpreter/contracts'; import { IServiceContainer } from '../../ioc/types'; -import { traceError } from '../../logging'; +import { traceError, traceVerbose } from '../../logging'; import { captureTelemetry, sendTelemetryEvent } from '../../telemetry'; import { EventName } from '../../telemetry/constants'; import { ICodeExecutionHelper, ICodeExecutionManager, ICodeExecutionService } from '../../terminals/types'; +import { + CreateEnvironmentCheckKind, + triggerCreateEnvironmentCheckNonBlocking, +} from '../../pythonEnvironments/creation/createEnvironmentTrigger'; +import { ReplType } from '../../repl/types'; +import { runInDedicatedTerminal, runInTerminal, useEnvExtension } from '../../envExt/api.internal'; @injectable() export class CodeExecutionManager implements ICodeExecutionManager { @@ -26,35 +31,61 @@ export class CodeExecutionManager implements ICodeExecutionManager { @inject(ICommandManager) private commandManager: ICommandManager, @inject(IDocumentManager) private documentManager: IDocumentManager, @inject(IDisposableRegistry) private disposableRegistry: Disposable[], - @inject(IFileSystem) private fileSystem: IFileSystem, @inject(IConfigurationService) private readonly configSettings: IConfigurationService, @inject(IServiceContainer) private serviceContainer: IServiceContainer, ) {} - public get onExecutedCode(): Event { - return this.eventEmitter.event; - } - public registerCommands() { - [Commands.Exec_In_Terminal, Commands.Exec_In_Terminal_Icon].forEach((cmd) => { - this.disposableRegistry.push( - this.commandManager.registerCommand(cmd as any, async (file: Resource) => { - const interpreterService = this.serviceContainer.get(IInterpreterService); - const interpreter = await interpreterService.getActiveInterpreter(file); - if (!interpreter) { - this.commandManager.executeCommand(Commands.TriggerEnvironmentSelection, file).then(noop, noop); - return; - } - const trigger = cmd === Commands.Exec_In_Terminal ? 'command' : 'icon'; - await this.executeFileInTerminal(file, trigger) - .then(() => { - if (this.shouldTerminalFocusOnStart(file)) - this.commandManager.executeCommand('workbench.action.terminal.focus'); + [Commands.Exec_In_Terminal, Commands.Exec_In_Terminal_Icon, Commands.Exec_In_Separate_Terminal].forEach( + (cmd) => { + this.disposableRegistry.push( + this.commandManager.registerCommand(cmd as any, async (file: Resource) => { + traceVerbose(`Attempting to run Python file`, file?.fsPath); + const trigger = cmd === Commands.Exec_In_Terminal ? 'command' : 'icon'; + const newTerminalPerFile = cmd === Commands.Exec_In_Separate_Terminal; + + if (useEnvExtension()) { + try { + await this.executeUsingExtension(file, cmd === Commands.Exec_In_Separate_Terminal); + } catch (ex) { + traceError('Failed to execute file in terminal', ex); + } + sendTelemetryEvent(EventName.ENVIRONMENT_CHECK_TRIGGER, undefined, { + trigger: 'run-in-terminal', + }); + sendTelemetryEvent(EventName.EXECUTION_CODE, undefined, { + scope: 'file', + trigger, + newTerminalPerFile, + }); + return; + } + + const interpreterService = this.serviceContainer.get(IInterpreterService); + const interpreter = await interpreterService.getActiveInterpreter(file); + if (!interpreter) { + this.commandManager + .executeCommand(Commands.TriggerEnvironmentSelection, file) + .then(noop, noop); + return; + } + sendTelemetryEvent(EventName.ENVIRONMENT_CHECK_TRIGGER, undefined, { + trigger: 'run-in-terminal', + }); + triggerCreateEnvironmentCheckNonBlocking(CreateEnvironmentCheckKind.File, file); + + await this.executeFileInTerminal(file, trigger, { + newTerminalPerFile, }) - .catch((ex) => traceError('Failed to execute file in terminal', ex)); - }), - ); - }); + .then(() => { + if (this.shouldTerminalFocusOnStart(file)) + this.commandManager.executeCommand('workbench.action.terminal.focus'); + }) + .catch((ex) => traceError('Failed to execute file in terminal', ex)); + }), + ); + }, + ); this.disposableRegistry.push( this.commandManager.registerCommand(Commands.Exec_Selection_In_Terminal as any, async (file: Resource) => { const interpreterService = this.serviceContainer.get(IInterpreterService); @@ -63,6 +94,8 @@ export class CodeExecutionManager implements ICodeExecutionManager { this.commandManager.executeCommand(Commands.TriggerEnvironmentSelection, file).then(noop, noop); return; } + sendTelemetryEvent(EventName.ENVIRONMENT_CHECK_TRIGGER, undefined, { trigger: 'run-selection' }); + triggerCreateEnvironmentCheckNonBlocking(CreateEnvironmentCheckKind.File, file); await this.executeSelectionInTerminal().then(() => { if (this.shouldTerminalFocusOnStart(file)) this.commandManager.executeCommand('workbench.action.terminal.focus'); @@ -79,6 +112,8 @@ export class CodeExecutionManager implements ICodeExecutionManager { this.commandManager.executeCommand(Commands.TriggerEnvironmentSelection, file).then(noop, noop); return; } + sendTelemetryEvent(EventName.ENVIRONMENT_CHECK_TRIGGER, undefined, { trigger: 'run-selection' }); + triggerCreateEnvironmentCheckNonBlocking(CreateEnvironmentCheckKind.File, file); await this.executeSelectionInDjangoShell().then(() => { if (this.shouldTerminalFocusOnStart(file)) this.commandManager.executeCommand('workbench.action.terminal.focus'); @@ -87,30 +122,64 @@ export class CodeExecutionManager implements ICodeExecutionManager { ), ); } - private async executeFileInTerminal(file: Resource, trigger: 'command' | 'icon') { - sendTelemetryEvent(EventName.EXECUTION_CODE, undefined, { scope: 'file', trigger }); + + private async executeUsingExtension(file: Resource, dedicated: boolean): Promise { const codeExecutionHelper = this.serviceContainer.get(ICodeExecutionHelper); file = file instanceof Uri ? file : undefined; let fileToExecute = file ? file : await codeExecutionHelper.getFileToExecute(); if (!fileToExecute) { return; } + const fileAfterSave = await codeExecutionHelper.saveFileIfDirty(fileToExecute); if (fileAfterSave) { fileToExecute = fileAfterSave; } - try { - const contents = await this.fileSystem.readFile(fileToExecute.fsPath); - this.eventEmitter.fire(contents); - } catch { - // Ignore any errors that occur for firing this event. It's only used - // for telemetry - noop(); + // Check on setting terminal.executeInFileDir + const pythonSettings = this.configSettings.getSettings(file); + let cwd = pythonSettings.terminal.executeInFileDir ? path.dirname(fileToExecute.fsPath) : undefined; + + // Check on setting terminal.launchArgs + const launchArgs = pythonSettings.terminal.launchArgs; + const totalArgs = [...launchArgs, fileToExecute.fsPath.fileToCommandArgumentForPythonExt()]; + + const show = this.shouldTerminalFocusOnStart(fileToExecute); + let terminal: Terminal | undefined; + if (dedicated) { + terminal = await runInDedicatedTerminal(fileToExecute, totalArgs, cwd, show); + } else { + terminal = await runInTerminal(fileToExecute, totalArgs, cwd, show); + } + + if (terminal) { + terminal.show(); + } + } + + private async executeFileInTerminal( + file: Resource, + trigger: 'command' | 'icon', + options?: { newTerminalPerFile: boolean }, + ): Promise { + sendTelemetryEvent(EventName.EXECUTION_CODE, undefined, { + scope: 'file', + trigger, + newTerminalPerFile: options?.newTerminalPerFile, + }); + const codeExecutionHelper = this.serviceContainer.get(ICodeExecutionHelper); + file = file instanceof Uri ? file : undefined; + let fileToExecute = file ? file : await codeExecutionHelper.getFileToExecute(); + if (!fileToExecute) { + return; + } + const fileAfterSave = await codeExecutionHelper.saveFileIfDirty(fileToExecute); + if (fileAfterSave) { + fileToExecute = fileAfterSave; } const executionService = this.serviceContainer.get(ICodeExecutionService, 'standard'); - await executionService.executeFile(fileToExecute); + await executionService.executeFile(fileToExecute, options); } @captureTelemetry(EventName.EXECUTION_CODE, { scope: 'selection' }, false) @@ -132,8 +201,16 @@ export class CodeExecutionManager implements ICodeExecutionManager { return; } const codeExecutionHelper = this.serviceContainer.get(ICodeExecutionHelper); - const codeToExecute = await codeExecutionHelper.getSelectedTextToExecute(activeEditor!); - const normalizedCode = await codeExecutionHelper.normalizeLines(codeToExecute!); + const codeToExecute = await codeExecutionHelper.getSelectedTextToExecute(activeEditor); + let wholeFileContent = ''; + if (activeEditor && activeEditor.document) { + wholeFileContent = activeEditor.document.getText(); + } + const normalizedCode = await codeExecutionHelper.normalizeLines( + codeToExecute!, + ReplType.terminal, + wholeFileContent, + ); if (!normalizedCode || normalizedCode.trim().length === 0) { return; } @@ -146,7 +223,7 @@ export class CodeExecutionManager implements ICodeExecutionManager { noop(); } - await executionService.execute(normalizedCode, activeEditor!.document.uri); + await executionService.execute(normalizedCode, activeEditor.document.uri); } private shouldTerminalFocusOnStart(uri: Uri | undefined): boolean { diff --git a/src/client/terminals/codeExecution/djangoShellCodeExecution.ts b/src/client/terminals/codeExecution/djangoShellCodeExecution.ts index c70cd896b225..05a1470b5727 100644 --- a/src/client/terminals/codeExecution/djangoShellCodeExecution.ts +++ b/src/client/terminals/codeExecution/djangoShellCodeExecution.ts @@ -6,7 +6,12 @@ import { inject, injectable } from 'inversify'; import * as path from 'path'; import { Disposable, Uri } from 'vscode'; -import { ICommandManager, IDocumentManager, IWorkspaceService } from '../../common/application/types'; +import { + IApplicationShell, + ICommandManager, + IDocumentManager, + IWorkspaceService, +} from '../../common/application/types'; import '../../common/extensions'; import { IFileSystem, IPlatformService } from '../../common/platform/types'; import { ITerminalServiceFactory } from '../../common/terminal/types'; @@ -28,6 +33,7 @@ export class DjangoShellCodeExecutionProvider extends TerminalCodeExecutionProvi @inject(IFileSystem) fileSystem: IFileSystem, @inject(IDisposableRegistry) disposableRegistry: Disposable[], @inject(IInterpreterService) interpreterService: IInterpreterService, + @inject(IApplicationShell) applicationShell: IApplicationShell, ) { super( terminalServiceFactory, @@ -36,6 +42,8 @@ export class DjangoShellCodeExecutionProvider extends TerminalCodeExecutionProvi disposableRegistry, platformService, interpreterService, + commandManager, + applicationShell, ); this.terminalTitle = 'Django Shell'; disposableRegistry.push(new DjangoContextInitializer(documentManager, workspace, fileSystem, commandManager)); diff --git a/src/client/terminals/codeExecution/helper.ts b/src/client/terminals/codeExecution/helper.ts index 0a1ae9f0be06..4efad5ee174e 100644 --- a/src/client/terminals/codeExecution/helper.ts +++ b/src/client/terminals/codeExecution/helper.ts @@ -5,7 +5,13 @@ import '../../common/extensions'; import { inject, injectable } from 'inversify'; import { l10n, Position, Range, TextEditor, Uri } from 'vscode'; -import { IApplicationShell, IDocumentManager, IWorkspaceService } from '../../common/application/types'; +import { + IActiveResourceService, + IApplicationShell, + ICommandManager, + IDocumentManager, + IWorkspaceService, +} from '../../common/application/types'; import { PYTHON_LANGUAGE } from '../../common/constants'; import * as internalScripts from '../../common/process/internal/scripts'; import { IProcessServiceFactory } from '../../common/process/types'; @@ -14,7 +20,10 @@ import { IInterpreterService } from '../../interpreter/contracts'; import { IServiceContainer } from '../../ioc/types'; import { ICodeExecutionHelper } from '../types'; import { traceError } from '../../logging'; -import { Resource } from '../../common/types'; +import { IConfigurationService, Resource } from '../../common/types'; +import { sendTelemetryEvent } from '../../telemetry'; +import { EventName } from '../../telemetry/constants'; +import { ReplType } from '../../repl/types'; @injectable() export class CodeExecutionHelper implements ICodeExecutionHelper { @@ -26,14 +35,30 @@ export class CodeExecutionHelper implements ICodeExecutionHelper { private readonly interpreterService: IInterpreterService; + private readonly commandManager: ICommandManager; + + private activeResourceService: IActiveResourceService; + + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-expect-error TS6133: 'configSettings' is declared but its value is never read. + private readonly configSettings: IConfigurationService; + constructor(@inject(IServiceContainer) private readonly serviceContainer: IServiceContainer) { this.documentManager = serviceContainer.get(IDocumentManager); this.applicationShell = serviceContainer.get(IApplicationShell); this.processServiceFactory = serviceContainer.get(IProcessServiceFactory); this.interpreterService = serviceContainer.get(IInterpreterService); + this.configSettings = serviceContainer.get(IConfigurationService); + this.commandManager = serviceContainer.get(ICommandManager); + this.activeResourceService = this.serviceContainer.get(IActiveResourceService); } - public async normalizeLines(code: string, resource?: Uri): Promise { + public async normalizeLines( + code: string, + _replType: ReplType, + wholeFileContent?: string, + resource?: Uri, + ): Promise { try { if (code.trim().length === 0) { return ''; @@ -42,6 +67,7 @@ export class CodeExecutionHelper implements ICodeExecutionHelper { // So just remove cr from the input. code = code.replace(new RegExp('\\r', 'g'), ''); + const activeEditor = this.documentManager.activeTextEditor; const interpreter = await this.interpreterService.getActiveInterpreter(resource); const processService = await this.processServiceFactory.create(resource); @@ -63,10 +89,32 @@ export class CodeExecutionHelper implements ICodeExecutionHelper { normalizeOutput.resolve(normalized); }, }); - + // If there is no explicit selection, we are exeucting 'line' or 'block'. + if (activeEditor?.selection?.isEmpty) { + sendTelemetryEvent(EventName.EXECUTION_CODE, undefined, { scope: 'line' }); + } // The normalization script expects a serialized JSON object, with the selection under the "code" key. // We're using a JSON object so that we don't have to worry about encoding, or escaping non-ASCII characters. - const input = JSON.stringify({ code }); + const startLineVal = activeEditor?.selection?.start.line ?? 0; + const endLineVal = activeEditor?.selection?.end.line ?? 0; + const emptyHighlightVal = activeEditor?.selection?.isEmpty ?? true; + let smartSendSettingsEnabledVal = true; + let shellIntegrationEnabled = false; + const configuration = this.serviceContainer.get(IConfigurationService); + if (configuration) { + const pythonSettings = configuration.getSettings(this.activeResourceService.getActiveResource()); + smartSendSettingsEnabledVal = pythonSettings.REPL.enableREPLSmartSend; + shellIntegrationEnabled = pythonSettings.terminal.shellIntegration.enabled; + } + + const input = JSON.stringify({ + code, + wholeFileContent, + startLine: startLineVal, + endLine: endLineVal, + emptyHighlight: emptyHighlightVal, + smartSendSettingsEnabled: smartSendSettingsEnabledVal, + }); observable.proc?.stdin?.write(input); observable.proc?.stdin?.end(); @@ -74,6 +122,21 @@ export class CodeExecutionHelper implements ICodeExecutionHelper { const result = await normalizeOutput.promise; const object = JSON.parse(result); + if (activeEditor?.selection && smartSendSettingsEnabledVal && object.normalized !== 'deprecated') { + const lineOffset = object.nextBlockLineno - activeEditor!.selection.start.line - 1; + await this.moveToNextBlock(lineOffset, activeEditor); + } + + // For new _pyrepl for Python3.13+ && !shellIntegration, we need to send code via bracketed paste mode. + if (object.attach_bracket_paste && !shellIntegrationEnabled && _replType === ReplType.terminal) { + let trimmedNormalized = object.normalized.replace(/\n$/, ''); + if (trimmedNormalized.endsWith(':\n')) { + // In case where statement is unfinished via :, truncate so auto-indentation lands nicely. + trimmedNormalized = trimmedNormalized.replace(/\n$/, ''); + } + return `\u001b[200~${trimmedNormalized}\u001b[201~`; + } + return parse(object.normalized); } catch (ex) { traceError(ex, 'Python: Failed to normalize code for execution in terminal'); @@ -81,6 +144,29 @@ export class CodeExecutionHelper implements ICodeExecutionHelper { } } + /** + * Depending on whether or not user is in experiment for smart send, + * dynamically move the cursor to the next block of code. + * The cursor movement is not moved by one everytime, + * since with the smart selection, the next executable code block + * can be multiple lines away. + * Intended to provide smooth shift+enter user experience + * bringing user's cursor to the next executable block of code when used with smart selection. + */ + // eslint-disable-next-line class-methods-use-this + private async moveToNextBlock(lineOffset: number, activeEditor?: TextEditor): Promise { + if (activeEditor?.selection?.isEmpty) { + await this.commandManager.executeCommand('cursorMove', { + to: 'down', + by: 'line', + value: Number(lineOffset), + }); + await this.commandManager.executeCommand('cursorEnd'); + } + + return Promise.resolve(); + } + public async getFileToExecute(): Promise { const activeEditor = this.documentManager.activeTextEditor; if (!activeEditor) { @@ -92,7 +178,7 @@ export class CodeExecutionHelper implements ICodeExecutionHelper { return undefined; } if (activeEditor.document.languageId !== PYTHON_LANGUAGE) { - this.applicationShell.showErrorMessage(l10n.t('The active file is not a Python source file)')); + this.applicationShell.showErrorMessage(l10n.t('The active file is not a Python source file')); return undefined; } if (activeEditor.document.isDirty) { @@ -110,6 +196,7 @@ export class CodeExecutionHelper implements ICodeExecutionHelper { const { selection } = textEditor; let code: string; + if (selection.isEmpty) { code = textEditor.document.lineAt(selection.start.line).text; } else if (selection.isSingleLine) { @@ -117,20 +204,21 @@ export class CodeExecutionHelper implements ICodeExecutionHelper { } else { code = getMultiLineSelectionText(textEditor); } + return code; } public async saveFileIfDirty(file: Uri): Promise { const docs = this.documentManager.textDocuments.filter((d) => d.uri.path === file.path); - if (docs.length === 1 && docs[0].isDirty) { + if (docs.length === 1 && (docs[0].isDirty || docs[0].isUntitled)) { const workspaceService = this.serviceContainer.get(IWorkspaceService); - return workspaceService.saveAs(docs[0].uri); + return workspaceService.save(docs[0].uri); } return undefined; } } -function getSingleLineSelectionText(textEditor: TextEditor): string { +export function getSingleLineSelectionText(textEditor: TextEditor): string { const { selection } = textEditor; const selectionRange = new Range(selection.start, selection.end); const selectionText = textEditor.document.getText(selectionRange); @@ -157,7 +245,7 @@ function getSingleLineSelectionText(textEditor: TextEditor): string { return selectionText; } -function getMultiLineSelectionText(textEditor: TextEditor): string { +export function getMultiLineSelectionText(textEditor: TextEditor): string { const { selection } = textEditor; const selectionRange = new Range(selection.start, selection.end); const selectionText = textEditor.document.getText(selectionRange); diff --git a/src/client/terminals/codeExecution/repl.ts b/src/client/terminals/codeExecution/repl.ts index e3a4bc7582c2..bc9a30af1fac 100644 --- a/src/client/terminals/codeExecution/repl.ts +++ b/src/client/terminals/codeExecution/repl.ts @@ -5,7 +5,7 @@ import { inject, injectable } from 'inversify'; import { Disposable } from 'vscode'; -import { IWorkspaceService } from '../../common/application/types'; +import { IApplicationShell, ICommandManager, IWorkspaceService } from '../../common/application/types'; import { IPlatformService } from '../../common/platform/types'; import { ITerminalServiceFactory } from '../../common/terminal/types'; import { IConfigurationService, IDisposableRegistry } from '../../common/types'; @@ -21,6 +21,8 @@ export class ReplProvider extends TerminalCodeExecutionProvider { @inject(IDisposableRegistry) disposableRegistry: Disposable[], @inject(IPlatformService) platformService: IPlatformService, @inject(IInterpreterService) interpreterService: IInterpreterService, + @inject(ICommandManager) commandManager: ICommandManager, + @inject(IApplicationShell) applicationShell: IApplicationShell, ) { super( terminalServiceFactory, @@ -29,6 +31,8 @@ export class ReplProvider extends TerminalCodeExecutionProvider { disposableRegistry, platformService, interpreterService, + commandManager, + applicationShell, ); this.terminalTitle = 'REPL'; } diff --git a/src/client/terminals/codeExecution/terminalCodeExecution.ts b/src/client/terminals/codeExecution/terminalCodeExecution.ts index b604d062e81e..ea444af4d89e 100644 --- a/src/client/terminals/codeExecution/terminalCodeExecution.ts +++ b/src/client/terminals/codeExecution/terminalCodeExecution.ts @@ -6,20 +6,26 @@ import { inject, injectable } from 'inversify'; import * as path from 'path'; import { Disposable, Uri } from 'vscode'; -import { IWorkspaceService } from '../../common/application/types'; +import { IApplicationShell, ICommandManager, IWorkspaceService } from '../../common/application/types'; import '../../common/extensions'; import { IPlatformService } from '../../common/platform/types'; import { ITerminalService, ITerminalServiceFactory } from '../../common/terminal/types'; -import { IConfigurationService, IDisposableRegistry } from '../../common/types'; +import { IConfigurationService, IDisposable, IDisposableRegistry, Resource } from '../../common/types'; +import { Diagnostics, Repl } from '../../common/utils/localize'; +import { showWarningMessage } from '../../common/vscodeApis/windowApis'; import { IInterpreterService } from '../../interpreter/contracts'; +import { traceInfo } from '../../logging'; import { buildPythonExecInfo, PythonExecInfo } from '../../pythonEnvironments/exec'; import { ICodeExecutionService } from '../../terminals/types'; +import { EventName } from '../../telemetry/constants'; +import { sendTelemetryEvent } from '../../telemetry'; @injectable() export class TerminalCodeExecutionProvider implements ICodeExecutionService { private hasRanOutsideCurrentDrive = false; protected terminalTitle!: string; - private replActive = new Map>(); + private replActive?: Promise; + constructor( @inject(ITerminalServiceFactory) protected readonly terminalServiceFactory: ITerminalServiceFactory, @inject(IConfigurationService) protected readonly configurationService: IConfigurationService, @@ -27,47 +33,82 @@ export class TerminalCodeExecutionProvider implements ICodeExecutionService { @inject(IDisposableRegistry) protected readonly disposables: Disposable[], @inject(IPlatformService) protected readonly platformService: IPlatformService, @inject(IInterpreterService) protected readonly interpreterService: IInterpreterService, + @inject(ICommandManager) protected readonly commandManager: ICommandManager, + @inject(IApplicationShell) protected readonly applicationShell: IApplicationShell, ) {} - public async executeFile(file: Uri) { - await this.setCwdForFileExecution(file); + public async executeFile(file: Uri, options?: { newTerminalPerFile: boolean }) { + await this.setCwdForFileExecution(file, options); const { command, args } = await this.getExecuteFileArgs(file, [ file.fsPath.fileToCommandArgumentForPythonExt(), ]); - await this.getTerminalService(file).sendCommand(command, args); + await this.getTerminalService(file, options).sendCommand(command, args); } public async execute(code: string, resource?: Uri): Promise { if (!code || code.trim().length === 0) { return; } - - await this.initializeRepl(); - await this.getTerminalService(resource).sendText(code); + await this.initializeRepl(resource); + if (code == 'deprecated') { + // If user is trying to smart send deprecated code show warning + const selection = await showWarningMessage(Diagnostics.invalidSmartSendMessage, Repl.disableSmartSend); + traceInfo(`Selected file contains invalid Python or Deprecated Python 2 code`); + if (selection === Repl.disableSmartSend) { + this.configurationService.updateSetting('REPL.enableREPLSmartSend', false, resource); + } + } else { + await this.getTerminalService(resource).executeCommand(code, true); + } } - public async initializeRepl(resource?: Uri) { + + public async initializeRepl(resource: Resource) { const terminalService = this.getTerminalService(resource); - let replActive = this.replActive.get(resource?.fsPath || ''); - if (replActive && (await replActive)) { + if (this.replActive && (await this.replActive)) { await terminalService.show(); return; } - replActive = new Promise(async (resolve) => { + sendTelemetryEvent(EventName.REPL, undefined, { replType: 'Terminal' }); + this.replActive = new Promise(async (resolve) => { const replCommandArgs = await this.getExecutableInfo(resource); - terminalService.sendCommand(replCommandArgs.command, replCommandArgs.args); + let listener: IDisposable; + Promise.race([ + new Promise((resolve) => setTimeout(() => resolve(true), 3000)), + new Promise((resolve) => { + let count = 0; + const terminalDataTimeout = setTimeout(() => { + resolve(true); // Fall back for test case scenarios. + }, 3000); + // Watch TerminalData to see if REPL launched. + listener = this.applicationShell.onDidWriteTerminalData((e) => { + for (let i = 0; i < e.data.length; i++) { + if (e.data[i] === '>') { + count++; + if (count === 3) { + clearTimeout(terminalDataTimeout); + resolve(true); + } + } + } + }); + }), + ]).then(() => { + if (listener) { + listener.dispose(); + } + resolve(true); + }); - // Give python repl time to start before we start sending text. - setTimeout(() => resolve(true), 1000); + await terminalService.sendCommand(replCommandArgs.command, replCommandArgs.args); }); - this.replActive.set(resource?.fsPath || '', replActive); this.disposables.push( terminalService.onDidCloseTerminal(() => { - this.replActive.delete(resource?.fsPath || ''); + this.replActive = undefined; }), ); - await replActive; + await this.replActive; } public async getExecutableInfo(resource?: Uri, args: string[] = []): Promise { @@ -83,13 +124,14 @@ export class TerminalCodeExecutionProvider implements ICodeExecutionService { public async getExecuteFileArgs(resource?: Uri, executeArgs: string[] = []): Promise { return this.getExecutableInfo(resource, executeArgs); } - private getTerminalService(resource?: Uri): ITerminalService { + private getTerminalService(resource: Resource, options?: { newTerminalPerFile: boolean }): ITerminalService { return this.terminalServiceFactory.getTerminalService({ resource, title: this.terminalTitle, + newTerminalPerFile: options?.newTerminalPerFile, }); } - private async setCwdForFileExecution(file: Uri) { + private async setCwdForFileExecution(file: Uri, options?: { newTerminalPerFile: boolean }) { const pythonSettings = this.configurationService.getSettings(file); if (!pythonSettings.terminal.executeInFileDir) { return; @@ -107,7 +149,9 @@ export class TerminalCodeExecutionProvider implements ICodeExecutionService { await this.getTerminalService(file).sendText(`${fileDrive}:`); } } - await this.getTerminalService(file).sendText(`cd ${fileDirPath.fileToCommandArgumentForPythonExt()}`); + await this.getTerminalService(file, options).sendText( + `cd ${fileDirPath.fileToCommandArgumentForPythonExt()}`, + ); } } } diff --git a/src/client/terminals/codeExecution/terminalReplWatcher.ts b/src/client/terminals/codeExecution/terminalReplWatcher.ts new file mode 100644 index 000000000000..951961ab6901 --- /dev/null +++ b/src/client/terminals/codeExecution/terminalReplWatcher.ts @@ -0,0 +1,27 @@ +import { Disposable, TerminalShellExecutionStartEvent } from 'vscode'; +import { onDidStartTerminalShellExecution } from '../../common/vscodeApis/windowApis'; +import { sendTelemetryEvent } from '../../telemetry'; +import { EventName } from '../../telemetry/constants'; + +function checkREPLCommand(command: string): undefined | 'manualTerminal' | `runningScript` { + const lower = command.toLowerCase().trimStart(); + if (lower.startsWith('python') || lower.startsWith('py ')) { + const parts = lower.split(' '); + if (parts.length === 1) { + return 'manualTerminal'; + } + return 'runningScript'; + } + return undefined; +} + +export function registerTriggerForTerminalREPL(disposables: Disposable[]): void { + disposables.push( + onDidStartTerminalShellExecution(async (e: TerminalShellExecutionStartEvent) => { + const replType = checkREPLCommand(e.execution.commandLine.value); + if (e.execution.commandLine.isTrusted && replType) { + sendTelemetryEvent(EventName.REPL, undefined, { replType }); + } + }), + ); +} diff --git a/src/client/terminals/envCollectionActivation/deactivateService.ts b/src/client/terminals/envCollectionActivation/deactivateService.ts new file mode 100644 index 000000000000..0758f3e22311 --- /dev/null +++ b/src/client/terminals/envCollectionActivation/deactivateService.ts @@ -0,0 +1,102 @@ +/* eslint-disable class-methods-use-this */ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { inject, injectable } from 'inversify'; +import * as path from 'path'; +import { ITerminalManager } from '../../common/application/types'; +import { pathExists } from '../../common/platform/fs-paths'; +import { _SCRIPTS_DIR } from '../../common/process/internal/scripts/constants'; +import { identifyShellFromShellPath } from '../../common/terminal/shellDetectors/baseShellDetector'; +import { ITerminalHelper, TerminalShellType } from '../../common/terminal/types'; +import { Resource } from '../../common/types'; +import { waitForCondition } from '../../common/utils/async'; +import { cache } from '../../common/utils/decorators'; +import { StopWatch } from '../../common/utils/stopWatch'; +import { IInterpreterService } from '../../interpreter/contracts'; +import { traceVerbose } from '../../logging'; +import { PythonEnvType } from '../../pythonEnvironments/base/info'; +import { ITerminalDeactivateService } from '../types'; + +/** + * This is a list of shells which support shell integration: + * https://code.visualstudio.com/docs/terminal/shell-integration + */ +const ShellIntegrationShells = [ + TerminalShellType.powershell, + TerminalShellType.powershellCore, + TerminalShellType.bash, + TerminalShellType.zsh, + TerminalShellType.fish, +]; + +@injectable() +export class TerminalDeactivateService implements ITerminalDeactivateService { + private readonly envVarScript = path.join(_SCRIPTS_DIR, 'printEnvVariablesToFile.py'); + + constructor( + @inject(ITerminalManager) private readonly terminalManager: ITerminalManager, + @inject(IInterpreterService) private readonly interpreterService: IInterpreterService, + @inject(ITerminalHelper) private readonly terminalHelper: ITerminalHelper, + ) {} + + @cache(-1, true) + public async initializeScriptParams(shell: string): Promise { + const location = this.getLocation(shell); + if (!location) { + return; + } + const shellType = identifyShellFromShellPath(shell); + const terminal = this.terminalManager.createTerminal({ + name: `Python ${shellType} Deactivate`, + shellPath: shell, + hideFromUser: true, + cwd: location, + }); + const globalInterpreters = this.interpreterService.getInterpreters().filter((i) => !i.type); + const outputFile = path.join(location, `envVars.txt`); + const interpreterPath = + globalInterpreters.length > 0 && globalInterpreters[0] ? globalInterpreters[0].path : 'python'; + const checkIfFileHasBeenCreated = () => pathExists(outputFile); + const stopWatch = new StopWatch(); + const command = this.terminalHelper.buildCommandForTerminal(shellType, interpreterPath, [ + this.envVarScript, + outputFile, + ]); + terminal.sendText(command); + await waitForCondition(checkIfFileHasBeenCreated, 30_000, `"${outputFile}" file not created`); + traceVerbose(`Time taken to get env vars using terminal is ${stopWatch.elapsedTime}ms`); + } + + public async getScriptLocation(shell: string, resource: Resource): Promise { + const interpreter = await this.interpreterService.getActiveInterpreter(resource); + if (interpreter?.type !== PythonEnvType.Virtual) { + return undefined; + } + return this.getLocation(shell); + } + + private getLocation(shell: string) { + const shellType = identifyShellFromShellPath(shell); + if (!ShellIntegrationShells.includes(shellType)) { + return undefined; + } + return path.join(_SCRIPTS_DIR, 'deactivate', this.getShellFolderName(shellType)); + } + + private getShellFolderName(shellType: TerminalShellType): string { + switch (shellType) { + case TerminalShellType.powershell: + case TerminalShellType.powershellCore: + return 'powershell'; + case TerminalShellType.fish: + return 'fish'; + case TerminalShellType.zsh: + return 'zsh'; + case TerminalShellType.bash: + return 'bash'; + default: + throw new Error(`Unsupported shell type ${shellType}`); + } + } +} diff --git a/src/client/terminals/envCollectionActivation/indicatorPrompt.ts b/src/client/terminals/envCollectionActivation/indicatorPrompt.ts new file mode 100644 index 000000000000..5701bf78603e --- /dev/null +++ b/src/client/terminals/envCollectionActivation/indicatorPrompt.ts @@ -0,0 +1,116 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { inject, injectable } from 'inversify'; +import { Uri } from 'vscode'; +import * as path from 'path'; +import { IActiveResourceService, IApplicationShell, ITerminalManager } from '../../common/application/types'; +import { + IConfigurationService, + IDisposableRegistry, + IExperimentService, + IPersistentStateFactory, + Resource, +} from '../../common/types'; +import { Common, Interpreters } from '../../common/utils/localize'; +import { IExtensionSingleActivationService } from '../../activation/types'; +import { inTerminalEnvVarExperiment } from '../../common/experiments/helpers'; +import { IInterpreterService } from '../../interpreter/contracts'; +import { PythonEnvironment } from '../../pythonEnvironments/info'; +import { ITerminalEnvVarCollectionService } from '../types'; +import { sleep } from '../../common/utils/async'; +import { isTestExecution } from '../../common/constants'; +import { PythonEnvType } from '../../pythonEnvironments/base/info'; +import { useEnvExtension } from '../../envExt/api.internal'; + +export const terminalEnvCollectionPromptKey = 'TERMINAL_ENV_COLLECTION_PROMPT_KEY'; + +@injectable() +export class TerminalIndicatorPrompt implements IExtensionSingleActivationService { + public readonly supportedWorkspaceTypes = { untrustedWorkspace: false, virtualWorkspace: false }; + + constructor( + @inject(IApplicationShell) private readonly appShell: IApplicationShell, + @inject(IPersistentStateFactory) private readonly persistentStateFactory: IPersistentStateFactory, + @inject(ITerminalManager) private readonly terminalManager: ITerminalManager, + @inject(IDisposableRegistry) private readonly disposableRegistry: IDisposableRegistry, + @inject(IActiveResourceService) private readonly activeResourceService: IActiveResourceService, + @inject(ITerminalEnvVarCollectionService) + private readonly terminalEnvVarCollectionService: ITerminalEnvVarCollectionService, + @inject(IConfigurationService) private readonly configurationService: IConfigurationService, + @inject(IInterpreterService) private readonly interpreterService: IInterpreterService, + @inject(IExperimentService) private readonly experimentService: IExperimentService, + ) {} + + public async activate(): Promise { + if (!inTerminalEnvVarExperiment(this.experimentService) || useEnvExtension()) { + return; + } + if (!isTestExecution()) { + // Avoid showing prompt until startup completes. + await sleep(6000); + } + this.disposableRegistry.push( + this.terminalManager.onDidOpenTerminal(async (terminal) => { + const hideFromUser = + 'hideFromUser' in terminal.creationOptions && terminal.creationOptions.hideFromUser; + const strictEnv = 'strictEnv' in terminal.creationOptions && terminal.creationOptions.strictEnv; + if (hideFromUser || strictEnv || terminal.creationOptions.name) { + // Only show this notification for basic terminals created using the '+' button. + return; + } + const cwd = + 'cwd' in terminal.creationOptions && terminal.creationOptions.cwd + ? terminal.creationOptions.cwd + : this.activeResourceService.getActiveResource(); + const resource = typeof cwd === 'string' ? Uri.file(cwd) : cwd; + const settings = this.configurationService.getSettings(resource); + if (!settings.terminal.activateEnvironment) { + return; + } + if (this.terminalEnvVarCollectionService.isTerminalPromptSetCorrectly(resource)) { + // No need to show notification if terminal prompt already indicates when env is activated. + return; + } + await this.notifyUsers(resource); + }), + ); + } + + private async notifyUsers(resource: Resource): Promise { + const notificationPromptEnabled = this.persistentStateFactory.createGlobalPersistentState( + terminalEnvCollectionPromptKey, + true, + ); + if (!notificationPromptEnabled.value) { + return; + } + const prompts = [Common.doNotShowAgain]; + const interpreter = await this.interpreterService.getActiveInterpreter(resource); + if (!interpreter || !interpreter.type) { + return; + } + const terminalPromptName = getPromptName(interpreter); + const environmentType = interpreter.type === PythonEnvType.Conda ? 'Selected conda' : 'Python virtual'; + const selection = await this.appShell.showInformationMessage( + Interpreters.terminalEnvVarCollectionPrompt.format(environmentType, terminalPromptName), + ...prompts, + ); + if (!selection) { + return; + } + if (selection === prompts[0]) { + await notificationPromptEnabled.updateValue(false); + } + } +} + +function getPromptName(interpreter: PythonEnvironment) { + if (interpreter.envName) { + return `"(${interpreter.envName})"`; + } + if (interpreter.envPath) { + return `"(${path.basename(interpreter.envPath)})"`; + } + return 'environment indicator'; +} diff --git a/src/client/terminals/envCollectionActivation/service.ts b/src/client/terminals/envCollectionActivation/service.ts new file mode 100644 index 000000000000..2ce8d5d5d86a --- /dev/null +++ b/src/client/terminals/envCollectionActivation/service.ts @@ -0,0 +1,515 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import * as path from 'path'; +import { inject, injectable } from 'inversify'; +import { + MarkdownString, + WorkspaceFolder, + GlobalEnvironmentVariableCollection, + EnvironmentVariableScope, + EnvironmentVariableMutatorOptions, + ProgressLocation, +} from 'vscode'; +import { pathExists, normCase } from '../../common/platform/fs-paths'; +import { IExtensionActivationService } from '../../activation/types'; +import { IApplicationShell, IApplicationEnvironment, IWorkspaceService } from '../../common/application/types'; +import { inTerminalEnvVarExperiment } from '../../common/experiments/helpers'; +import { IPlatformService } from '../../common/platform/types'; +import { identifyShellFromShellPath } from '../../common/terminal/shellDetectors/baseShellDetector'; +import { + IExtensionContext, + IExperimentService, + Resource, + IDisposableRegistry, + IConfigurationService, + IPathUtils, +} from '../../common/types'; +import { Interpreters } from '../../common/utils/localize'; +import { traceError, traceInfo, traceLog, traceVerbose, traceWarn } from '../../logging'; +import { IInterpreterService } from '../../interpreter/contracts'; +import { defaultShells } from '../../interpreter/activation/service'; +import { IEnvironmentActivationService } from '../../interpreter/activation/types'; +import { EnvironmentType, PythonEnvironment } from '../../pythonEnvironments/info'; +import { getSearchPathEnvVarNames } from '../../common/utils/exec'; +import { EnvironmentVariables, IEnvironmentVariablesProvider } from '../../common/variables/types'; +import { TerminalShellType } from '../../common/terminal/types'; +import { OSType } from '../../common/utils/platform'; + +import { PythonEnvType } from '../../pythonEnvironments/base/info'; +import { + IShellIntegrationDetectionService, + ITerminalDeactivateService, + ITerminalEnvVarCollectionService, +} from '../types'; +import { ProgressService } from '../../common/application/progressService'; +import { useEnvExtension } from '../../envExt/api.internal'; +import { registerPythonStartup } from '../pythonStartup'; + +@injectable() +export class TerminalEnvVarCollectionService implements IExtensionActivationService, ITerminalEnvVarCollectionService { + public readonly supportedWorkspaceTypes = { + untrustedWorkspace: false, + virtualWorkspace: false, + }; + + /** + * Prompts for these shells cannot be set reliably using variables + */ + private noPromptVariableShells = [ + TerminalShellType.powershell, + TerminalShellType.powershellCore, + TerminalShellType.fish, + ]; + + private registeredOnce = false; + + /** + * Carries default environment variables for the currently selected shell. + */ + private processEnvVars: EnvironmentVariables | undefined; + + private readonly progressService: ProgressService; + + private separator: string; + + constructor( + @inject(IPlatformService) private readonly platform: IPlatformService, + @inject(IInterpreterService) private interpreterService: IInterpreterService, + @inject(IExtensionContext) private context: IExtensionContext, + @inject(IApplicationShell) private shell: IApplicationShell, + @inject(IExperimentService) private experimentService: IExperimentService, + @inject(IApplicationEnvironment) private applicationEnvironment: IApplicationEnvironment, + @inject(IDisposableRegistry) private disposables: IDisposableRegistry, + @inject(IEnvironmentActivationService) private environmentActivationService: IEnvironmentActivationService, + @inject(IWorkspaceService) private workspaceService: IWorkspaceService, + @inject(IConfigurationService) private readonly configurationService: IConfigurationService, + @inject(ITerminalDeactivateService) private readonly terminalDeactivateService: ITerminalDeactivateService, + @inject(IPathUtils) private readonly pathUtils: IPathUtils, + @inject(IShellIntegrationDetectionService) + private readonly shellIntegrationDetectionService: IShellIntegrationDetectionService, + @inject(IEnvironmentVariablesProvider) + private readonly environmentVariablesProvider: IEnvironmentVariablesProvider, + ) { + this.separator = platform.osType === OSType.Windows ? ';' : ':'; + this.progressService = new ProgressService(this.shell); + } + + public async activate(resource: Resource): Promise { + try { + if (useEnvExtension()) { + traceVerbose('Ignoring environment variable experiment since env extension is being used'); + this.context.environmentVariableCollection.clear(); + // Needed for shell integration + await registerPythonStartup(this.context); + return; + } + + if (!inTerminalEnvVarExperiment(this.experimentService)) { + this.context.environmentVariableCollection.clear(); + await this.handleMicroVenv(resource); + if (!this.registeredOnce) { + this.interpreterService.onDidChangeInterpreter( + async (r) => { + await this.handleMicroVenv(r); + }, + this, + this.disposables, + ); + this.registeredOnce = true; + } + await registerPythonStartup(this.context); + return; + } + if (!this.registeredOnce) { + this.interpreterService.onDidChangeInterpreter( + async (r) => { + await this._applyCollection(r).ignoreErrors(); + }, + this, + this.disposables, + ); + this.shellIntegrationDetectionService.onDidChangeStatus( + async () => { + traceInfo("Shell integration status changed, can confirm it's working."); + await this._applyCollection(undefined).ignoreErrors(); + }, + this, + this.disposables, + ); + this.environmentVariablesProvider.onDidEnvironmentVariablesChange( + async (r: Resource) => { + await this._applyCollection(r).ignoreErrors(); + }, + this, + this.disposables, + ); + this.applicationEnvironment.onDidChangeShell( + async (shell: string) => { + this.processEnvVars = undefined; + // Pass in the shell where known instead of relying on the application environment, because of bug + // on VSCode: https://github.com/microsoft/vscode/issues/160694 + await this._applyCollection(undefined, shell).ignoreErrors(); + }, + this, + this.disposables, + ); + const { shell } = this.applicationEnvironment; + const isActive = await this.shellIntegrationDetectionService.isWorking(); + const shellType = identifyShellFromShellPath(shell); + if (!isActive && shellType !== TerminalShellType.commandPrompt) { + traceWarn( + `Shell integration may not be active, environment activated may be overridden by the shell.`, + ); + } + this.registeredOnce = true; + } + this._applyCollection(resource).ignoreErrors(); + } catch (ex) { + traceError(`Activating terminal env collection failed`, ex); + } + } + + public async _applyCollection(resource: Resource, shell?: string): Promise { + this.progressService.showProgress({ + location: ProgressLocation.Window, + title: Interpreters.activatingTerminals, + }); + await this._applyCollectionImpl(resource, shell).catch((ex) => { + traceError(`Failed to apply terminal env vars`, shell, ex); + return Promise.reject(ex); // Ensures progress indicator does not disappear in case of errors, so we can catch issues faster. + }); + this.progressService.hideProgress(); + } + + private async _applyCollectionImpl(resource: Resource, shell = this.applicationEnvironment.shell): Promise { + const workspaceFolder = this.getWorkspaceFolder(resource); + const settings = this.configurationService.getSettings(resource); + const envVarCollection = this.getEnvironmentVariableCollection({ workspaceFolder }); + if (useEnvExtension()) { + envVarCollection.clear(); + traceVerbose('Do not activate terminal env vars as env extension is being used'); + return; + } + + if (!settings.terminal.activateEnvironment) { + envVarCollection.clear(); + traceVerbose('Activating environments in terminal is disabled for', resource?.fsPath); + return; + } + const activatedEnv = await this.environmentActivationService.getActivatedEnvironmentVariables( + resource, + undefined, + undefined, + shell, + ); + const env = activatedEnv ? normCaseKeys(activatedEnv) : undefined; + traceVerbose(`Activated environment variables for ${resource?.fsPath}`, env); + if (!env) { + const shellType = identifyShellFromShellPath(shell); + const defaultShell = defaultShells[this.platform.osType]; + if (defaultShell?.shellType !== shellType) { + // Commands to fetch env vars may fail in custom shells due to unknown reasons, in that case + // fallback to default shells as they are known to work better. + await this._applyCollectionImpl(resource, defaultShell?.shell); + return; + } + await this.trackTerminalPrompt(shell, resource, env); + envVarCollection.clear(); + this.processEnvVars = undefined; + return; + } + if (!this.processEnvVars) { + this.processEnvVars = await this.environmentActivationService.getProcessEnvironmentVariables( + resource, + shell, + ); + } + const processEnv = normCaseKeys(this.processEnvVars); + + // PS1 in some cases is a shell variable (not an env variable) so "env" might not contain it, calculate it in that case. + env.PS1 = await this.getPS1(shell, resource, env); + const defaultPrependOptions = await this.getPrependOptions(); + + // Clear any previously set env vars from collection + envVarCollection.clear(); + const deactivate = await this.terminalDeactivateService.getScriptLocation(shell, resource); + Object.keys(env).forEach((key) => { + if (shouldSkip(key)) { + return; + } + let value = env[key]; + const prevValue = processEnv[key]; + if (prevValue !== value) { + if (value !== undefined) { + if (key === 'PS1') { + // We cannot have the full PS1 without executing in terminal, which we do not. Hence prepend it. + traceLog( + `Prepending environment variable ${key} in collection with ${value} ${JSON.stringify( + defaultPrependOptions, + )}`, + ); + envVarCollection.prepend(key, value, defaultPrependOptions); + return; + } + if (key === 'PATH') { + const options = { + applyAtShellIntegration: true, + applyAtProcessCreation: true, + }; + if (processEnv.PATH && env.PATH?.endsWith(processEnv.PATH)) { + // Prefer prepending to PATH instead of replacing it, as we do not want to replace any + // changes to PATH users might have made it in their init scripts (~/.bashrc etc.) + value = env.PATH.slice(0, -processEnv.PATH.length); + if (deactivate) { + value = `${deactivate}${this.separator}${value}`; + } + traceLog( + `Prepending environment variable ${key} in collection with ${value} ${JSON.stringify( + options, + )}`, + ); + envVarCollection.prepend(key, value, options); + } else { + if (!value.endsWith(this.separator)) { + value = value.concat(this.separator); + } + if (deactivate) { + value = `${deactivate}${this.separator}${value}`; + } + traceLog( + `Prepending environment variable ${key} in collection to ${value} ${JSON.stringify( + options, + )}`, + ); + envVarCollection.prepend(key, value, options); + } + return; + } + const options = { + applyAtShellIntegration: true, + applyAtProcessCreation: true, + }; + traceLog( + `Setting environment variable ${key} in collection to ${value} ${JSON.stringify(options)}`, + ); + envVarCollection.replace(key, value, options); + } + } + }); + + const displayPath = this.pathUtils.getDisplayName(settings.pythonPath, workspaceFolder?.uri.fsPath); + const description = new MarkdownString(`${Interpreters.activateTerminalDescription} \`${displayPath}\``); + envVarCollection.description = description; + + await this.trackTerminalPrompt(shell, resource, env); + await this.terminalDeactivateService.initializeScriptParams(shell).catch((ex) => { + traceError(`Failed to initialize deactivate script`, shell, ex); + }); + } + + private isPromptSet = new Map(); + + // eslint-disable-next-line class-methods-use-this + public isTerminalPromptSetCorrectly(resource?: Resource): boolean { + const workspaceFolder = this.getWorkspaceFolder(resource); + return !!this.isPromptSet.get(workspaceFolder?.index); + } + + /** + * Call this once we know terminal prompt is set correctly for terminal owned by this resource. + */ + private terminalPromptIsCorrect(resource: Resource) { + const key = this.getWorkspaceFolder(resource)?.index; + this.isPromptSet.set(key, true); + } + + private terminalPromptIsUnknown(resource: Resource) { + const key = this.getWorkspaceFolder(resource)?.index; + this.isPromptSet.delete(key); + } + + /** + * Tracks whether prompt for terminal was correctly set. + */ + private async trackTerminalPrompt(shell: string, resource: Resource, env: EnvironmentVariables | undefined) { + this.terminalPromptIsUnknown(resource); + if (!env) { + this.terminalPromptIsCorrect(resource); + return; + } + const customShellType = identifyShellFromShellPath(shell); + if (this.noPromptVariableShells.includes(customShellType)) { + return; + } + if (this.platform.osType !== OSType.Windows) { + // These shells are expected to set PS1 variable for terminal prompt for virtual/conda environments. + const interpreter = await this.interpreterService.getActiveInterpreter(resource); + const shouldSetPS1 = shouldPS1BeSet(interpreter?.type, env); + if (shouldSetPS1 && !env.PS1) { + // PS1 should be set but no PS1 was set. + return; + } + const config = await this.shellIntegrationDetectionService.isWorking(); + if (!config) { + traceVerbose('PS1 is not set when shell integration is disabled.'); + return; + } + } + this.terminalPromptIsCorrect(resource); + } + + private async getPS1(shell: string, resource: Resource, env: EnvironmentVariables) { + // PS1 returned by shell is not predictable: #22078 + // Hence calculate it ourselves where possible. Should no longer be needed once #22128 is available. + const customShellType = identifyShellFromShellPath(shell); + if (this.noPromptVariableShells.includes(customShellType)) { + return env.PS1; + } + if (this.platform.osType !== OSType.Windows) { + // These shells are expected to set PS1 variable for terminal prompt for virtual/conda environments. + const interpreter = await this.interpreterService.getActiveInterpreter(resource); + const shouldSetPS1 = shouldPS1BeSet(interpreter?.type, env); + if (shouldSetPS1) { + const prompt = getPromptForEnv(interpreter, env); + if (prompt) { + return prompt; + } + } + } + if (env.PS1) { + // Prefer PS1 set by env vars, as env.PS1 may or may not contain the full PS1: #22056. + return env.PS1; + } + return undefined; + } + + private async handleMicroVenv(resource: Resource) { + try { + const settings = this.configurationService.getSettings(resource); + const workspaceFolder = this.getWorkspaceFolder(resource); + if (useEnvExtension()) { + this.getEnvironmentVariableCollection({ workspaceFolder }).clear(); + traceVerbose('Do not activate microvenv as env extension is being used'); + return; + } + if (!settings.terminal.activateEnvironment) { + this.getEnvironmentVariableCollection({ workspaceFolder }).clear(); + traceVerbose( + 'Do not activate microvenv as activating environments in terminal is disabled for', + resource?.fsPath, + ); + return; + } + const interpreter = await this.interpreterService.getActiveInterpreter(resource); + if (interpreter?.envType === EnvironmentType.Venv) { + const activatePath = path.join(path.dirname(interpreter.path), 'activate'); + if (!(await pathExists(activatePath))) { + const envVarCollection = this.getEnvironmentVariableCollection({ workspaceFolder }); + const pathVarName = getSearchPathEnvVarNames()[0]; + envVarCollection.replace( + 'PATH', + `${path.dirname(interpreter.path)}${path.delimiter}${process.env[pathVarName]}`, + { applyAtShellIntegration: true, applyAtProcessCreation: true }, + ); + return; + } + this.getEnvironmentVariableCollection({ workspaceFolder }).clear(); + } + } catch (ex) { + traceWarn(`Microvenv failed as it is using proposed API which is constantly changing`, ex); + } + } + + private async getPrependOptions(): Promise { + const isActive = await this.shellIntegrationDetectionService.isWorking(); + // Ideally we would want to prepend exactly once, either at shell integration or process creation. + // TODO: Stop prepending altogether once https://github.com/microsoft/vscode/issues/145234 is available. + return isActive + ? { + applyAtShellIntegration: true, + applyAtProcessCreation: false, + } + : { + applyAtShellIntegration: true, // Takes care of false negatives in case manual integration is being used. + applyAtProcessCreation: true, + }; + } + + private getEnvironmentVariableCollection(scope: EnvironmentVariableScope = {}) { + const envVarCollection = this.context.environmentVariableCollection as GlobalEnvironmentVariableCollection; + return envVarCollection.getScoped(scope); + } + + private getWorkspaceFolder(resource: Resource): WorkspaceFolder | undefined { + let workspaceFolder = this.workspaceService.getWorkspaceFolder(resource); + if ( + !workspaceFolder && + Array.isArray(this.workspaceService.workspaceFolders) && + this.workspaceService.workspaceFolders.length > 0 + ) { + [workspaceFolder] = this.workspaceService.workspaceFolders; + } + return workspaceFolder; + } +} + +function shouldPS1BeSet(type: PythonEnvType | undefined, env: EnvironmentVariables): boolean { + if (env.PS1) { + // Activated variables contain PS1, meaning it was supposed to be set. + return true; + } + if (type === PythonEnvType.Virtual) { + const promptDisabledVar = env.VIRTUAL_ENV_DISABLE_PROMPT; + const isPromptDisabled = promptDisabledVar && promptDisabledVar !== undefined; + return !isPromptDisabled; + } + if (type === PythonEnvType.Conda) { + // Instead of checking config value using `conda config --get changeps1`, simply check + // `CONDA_PROMPT_MODIFER` to avoid the cost of launching the conda binary. + const promptEnabledVar = env.CONDA_PROMPT_MODIFIER; + const isPromptEnabled = promptEnabledVar && promptEnabledVar !== ''; + return !!isPromptEnabled; + } + return false; +} + +function shouldSkip(env: string) { + return [ + '_', + 'SHLVL', + // Even though this maybe returned, setting it can result in output encoding errors in terminal. + 'PYTHONUTF8', + // We have deactivate service which takes care of setting it. + '_OLD_VIRTUAL_PATH', + 'PWD', + ].includes(env); +} + +function getPromptForEnv(interpreter: PythonEnvironment | undefined, env: EnvironmentVariables) { + if (!interpreter) { + return undefined; + } + if (interpreter.envName) { + if (interpreter.envName === 'base') { + // If conda base environment is selected, it can lead to "(base)" appearing twice if we return the env name. + return undefined; + } + if (interpreter.type === PythonEnvType.Virtual && env.VIRTUAL_ENV_PROMPT) { + return `${env.VIRTUAL_ENV_PROMPT}`; + } + return `(${interpreter.envName}) `; + } + if (interpreter.envPath) { + return `(${path.basename(interpreter.envPath)}) `; + } + return undefined; +} + +function normCaseKeys(env: EnvironmentVariables): EnvironmentVariables { + const result: EnvironmentVariables = {}; + Object.keys(env).forEach((key) => { + result[normCase(key)] = env[key]; + }); + return result; +} diff --git a/src/client/terminals/envCollectionActivation/shellIntegrationService.ts b/src/client/terminals/envCollectionActivation/shellIntegrationService.ts new file mode 100644 index 000000000000..92bb98029892 --- /dev/null +++ b/src/client/terminals/envCollectionActivation/shellIntegrationService.ts @@ -0,0 +1,166 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { injectable, inject } from 'inversify'; +import { EventEmitter } from 'vscode'; +import { + IApplicationEnvironment, + IApplicationShell, + ITerminalManager, + IWorkspaceService, +} from '../../common/application/types'; +import { identifyShellFromShellPath } from '../../common/terminal/shellDetectors/baseShellDetector'; +import { TerminalShellType } from '../../common/terminal/types'; +import { IDisposableRegistry, IPersistentStateFactory } from '../../common/types'; +import { sleep } from '../../common/utils/async'; +import { traceError, traceVerbose } from '../../logging'; +import { IShellIntegrationDetectionService } from '../types'; +import { isTrusted } from '../../common/vscodeApis/workspaceApis'; + +/** + * This is a list of shells which support shell integration: + * https://code.visualstudio.com/docs/terminal/shell-integration + */ +const ShellIntegrationShells = [ + TerminalShellType.powershell, + TerminalShellType.powershellCore, + TerminalShellType.bash, + TerminalShellType.zsh, + TerminalShellType.fish, +]; + +export enum isShellIntegrationWorking { + key = 'SHELL_INTEGRATION_WORKING_KEY', +} + +@injectable() +export class ShellIntegrationDetectionService implements IShellIntegrationDetectionService { + private isWorkingForShell = new Set(); + + private readonly didChange = new EventEmitter(); + + private isDataWriteEventWorking = true; + + constructor( + @inject(ITerminalManager) private readonly terminalManager: ITerminalManager, + @inject(IApplicationShell) private readonly appShell: IApplicationShell, + @inject(IWorkspaceService) private readonly workspaceService: IWorkspaceService, + @inject(IPersistentStateFactory) private readonly persistentStateFactory: IPersistentStateFactory, + @inject(IApplicationEnvironment) private readonly appEnvironment: IApplicationEnvironment, + @inject(IDisposableRegistry) private readonly disposables: IDisposableRegistry, + ) { + try { + const activeShellType = identifyShellFromShellPath(this.appEnvironment.shell); + const key = getKeyForShell(activeShellType); + const persistedResult = this.persistentStateFactory.createGlobalPersistentState(key); + if (persistedResult.value) { + this.isWorkingForShell.add(activeShellType); + } + this.appShell.onDidWriteTerminalData( + (e) => { + if (e.data.includes('\x1b]633;A\x07') || e.data.includes('\x1b]133;A\x07')) { + let { shell } = this.appEnvironment; + if ('shellPath' in e.terminal.creationOptions && e.terminal.creationOptions.shellPath) { + shell = e.terminal.creationOptions.shellPath; + } + const shellType = identifyShellFromShellPath(shell); + traceVerbose('Received shell integration sequence for', shellType); + const wasWorking = this.isWorkingForShell.has(shellType); + this.isWorkingForShell.add(shellType); + if (!wasWorking) { + // If it wasn't working previously, status has changed. + this.didChange.fire(); + } + } + }, + this, + this.disposables, + ); + this.appEnvironment.onDidChangeShell( + async (shell: string) => { + this.createDummyHiddenTerminal(shell); + }, + this, + this.disposables, + ); + this.createDummyHiddenTerminal(this.appEnvironment.shell); + } catch (ex) { + this.isDataWriteEventWorking = false; + traceError('Unable to check if shell integration is active', ex); + } + const isEnabled = !!this.workspaceService + .getConfiguration('terminal') + .get('integrated.shellIntegration.enabled'); + if (!isEnabled) { + traceVerbose('Shell integration is disabled in user settings.'); + } + } + + public readonly onDidChangeStatus = this.didChange.event; + + public async isWorking(): Promise { + const { shell } = this.appEnvironment; + return this._isWorking(shell).catch((ex) => { + traceError(`Failed to determine if shell supports shell integration`, shell, ex); + return false; + }); + } + + public async _isWorking(shell: string): Promise { + const shellType = identifyShellFromShellPath(shell); + const isSupposedToWork = ShellIntegrationShells.includes(shellType); + if (!isSupposedToWork) { + return false; + } + const key = getKeyForShell(shellType); + const persistedResult = this.persistentStateFactory.createGlobalPersistentState(key); + if (persistedResult.value !== undefined) { + return persistedResult.value; + } + const result = await this.useDataWriteApproach(shellType); + if (result) { + // Once we know that shell integration is working for a shell, persist it so we need not do this check every session. + await persistedResult.updateValue(result); + } + return result; + } + + private async useDataWriteApproach(shellType: TerminalShellType) { + // For now, based on problems with using the command approach, use terminal data write event. + if (!this.isDataWriteEventWorking) { + // Assume shell integration is working, if data write event isn't working. + return true; + } + if (shellType === TerminalShellType.powershell || shellType === TerminalShellType.powershellCore) { + // Due to upstream bug: https://github.com/microsoft/vscode/issues/204616, assume shell integration is working for now. + return true; + } + if (!this.isWorkingForShell.has(shellType)) { + // Maybe data write event has not been processed yet, wait a bit. + await sleep(1000); + } + traceVerbose( + 'Did we determine shell integration to be working for', + shellType, + '?', + this.isWorkingForShell.has(shellType), + ); + return this.isWorkingForShell.has(shellType); + } + + /** + * Creates a dummy terminal so that we are guaranteed a data write event for this shell type. + */ + private createDummyHiddenTerminal(shell: string) { + if (isTrusted()) { + this.terminalManager.createTerminal({ + shellPath: shell, + hideFromUser: true, + }); + } + } +} + +function getKeyForShell(shellType: TerminalShellType) { + return `${isShellIntegrationWorking.key}_${shellType}`; +} diff --git a/src/client/terminals/pythonStartup.ts b/src/client/terminals/pythonStartup.ts new file mode 100644 index 000000000000..b6f68c860b46 --- /dev/null +++ b/src/client/terminals/pythonStartup.ts @@ -0,0 +1,48 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { ExtensionContext, MarkdownString, Uri } from 'vscode'; +import * as path from 'path'; +import { copy, createDirectory, getConfiguration, onDidChangeConfiguration } from '../common/vscodeApis/workspaceApis'; +import { EXTENSION_ROOT_DIR } from '../constants'; +import { Interpreters } from '../common/utils/localize'; + +async function applyPythonStartupSetting(context: ExtensionContext): Promise { + const config = getConfiguration('python'); + const pythonrcSetting = config.get('terminal.shellIntegration.enabled'); + + if (pythonrcSetting) { + const storageUri = context.storageUri || context.globalStorageUri; + try { + await createDirectory(storageUri); + } catch { + // already exists, most likely + } + const destPath = Uri.joinPath(storageUri, 'pythonrc.py'); + const sourcePath = path.join(EXTENSION_ROOT_DIR, 'python_files', 'pythonrc.py'); + await copy(Uri.file(sourcePath), destPath, { overwrite: true }); + context.environmentVariableCollection.replace('PYTHONSTARTUP', destPath.fsPath); + // When shell integration is enabled, we disable PyREPL from cpython. + context.environmentVariableCollection.replace('PYTHON_BASIC_REPL', '1'); + context.environmentVariableCollection.description = new MarkdownString( + Interpreters.shellIntegrationEnvVarCollectionDescription, + ); + } else { + context.environmentVariableCollection.delete('PYTHONSTARTUP'); + context.environmentVariableCollection.delete('PYTHON_BASIC_REPL'); + context.environmentVariableCollection.description = new MarkdownString( + Interpreters.shellIntegrationDisabledEnvVarCollectionDescription, + ); + } +} + +export async function registerPythonStartup(context: ExtensionContext): Promise { + await applyPythonStartupSetting(context); + context.subscriptions.push( + onDidChangeConfiguration(async (e) => { + if (e.affectsConfiguration('python.terminal.shellIntegration.enabled')) { + await applyPythonStartupSetting(context); + } + }), + ); +} diff --git a/src/client/terminals/pythonStartupLinkProvider.ts b/src/client/terminals/pythonStartupLinkProvider.ts new file mode 100644 index 000000000000..aba1270f1412 --- /dev/null +++ b/src/client/terminals/pythonStartupLinkProvider.ts @@ -0,0 +1,50 @@ +/* eslint-disable class-methods-use-this */ +import { + CancellationToken, + Disposable, + ProviderResult, + TerminalLink, + TerminalLinkContext, + TerminalLinkProvider, +} from 'vscode'; +import { executeCommand } from '../common/vscodeApis/commandApis'; +import { registerTerminalLinkProvider } from '../common/vscodeApis/windowApis'; +import { Repl } from '../common/utils/localize'; + +interface CustomTerminalLink extends TerminalLink { + command: string; +} + +export class CustomTerminalLinkProvider implements TerminalLinkProvider { + provideTerminalLinks( + context: TerminalLinkContext, + _token: CancellationToken, + ): ProviderResult { + const links: CustomTerminalLink[] = []; + let expectedNativeLink; + + if (process.platform === 'darwin') { + expectedNativeLink = 'Cmd click to launch VS Code Native REPL'; + } else { + expectedNativeLink = 'Ctrl click to launch VS Code Native REPL'; + } + + if (context.line.includes(expectedNativeLink)) { + links.push({ + startIndex: context.line.indexOf(expectedNativeLink), + length: expectedNativeLink.length, + tooltip: Repl.launchNativeRepl, + command: 'python.startNativeREPL', + }); + } + return links; + } + + async handleTerminalLink(link: CustomTerminalLink): Promise { + await executeCommand(link.command); + } +} + +export function registerCustomTerminalLinkProvider(disposables: Disposable[]): void { + disposables.push(registerTerminalLinkProvider(new CustomTerminalLinkProvider())); +} diff --git a/src/client/terminals/serviceRegistry.ts b/src/client/terminals/serviceRegistry.ts index a39ef31a8fe4..e62701dcec0e 100644 --- a/src/client/terminals/serviceRegistry.ts +++ b/src/client/terminals/serviceRegistry.ts @@ -1,25 +1,29 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -import { interfaces } from 'inversify'; -import { ClassType } from '../ioc/types'; +import { IServiceManager } from '../ioc/types'; import { TerminalAutoActivation } from './activation'; import { CodeExecutionManager } from './codeExecution/codeExecutionManager'; import { DjangoShellCodeExecutionProvider } from './codeExecution/djangoShellCodeExecution'; import { CodeExecutionHelper } from './codeExecution/helper'; import { ReplProvider } from './codeExecution/repl'; import { TerminalCodeExecutionProvider } from './codeExecution/terminalCodeExecution'; -import { ICodeExecutionHelper, ICodeExecutionManager, ICodeExecutionService, ITerminalAutoActivation } from './types'; +import { + ICodeExecutionHelper, + ICodeExecutionManager, + ICodeExecutionService, + IShellIntegrationDetectionService, + ITerminalAutoActivation, + ITerminalDeactivateService, + ITerminalEnvVarCollectionService, +} from './types'; +import { TerminalEnvVarCollectionService } from './envCollectionActivation/service'; +import { IExtensionActivationService, IExtensionSingleActivationService } from '../activation/types'; +import { TerminalIndicatorPrompt } from './envCollectionActivation/indicatorPrompt'; +import { TerminalDeactivateService } from './envCollectionActivation/deactivateService'; +import { ShellIntegrationDetectionService } from './envCollectionActivation/shellIntegrationService'; -interface IServiceRegistry { - addSingleton( - serviceIdentifier: interfaces.ServiceIdentifier, - constructor: ClassType, - name?: string | number | symbol, - ): void; -} - -export function registerTypes(serviceManager: IServiceRegistry): void { +export function registerTypes(serviceManager: IServiceManager): void { serviceManager.addSingleton(ICodeExecutionHelper, CodeExecutionHelper); serviceManager.addSingleton(ICodeExecutionManager, CodeExecutionManager); @@ -37,4 +41,19 @@ export function registerTypes(serviceManager: IServiceRegistry): void { serviceManager.addSingleton(ICodeExecutionService, ReplProvider, 'repl'); serviceManager.addSingleton(ITerminalAutoActivation, TerminalAutoActivation); + serviceManager.addSingleton( + ITerminalEnvVarCollectionService, + TerminalEnvVarCollectionService, + ); + serviceManager.addSingleton(ITerminalDeactivateService, TerminalDeactivateService); + serviceManager.addSingleton( + IExtensionSingleActivationService, + TerminalIndicatorPrompt, + ); + serviceManager.addSingleton( + IShellIntegrationDetectionService, + ShellIntegrationDetectionService, + ); + + serviceManager.addBinding(ITerminalEnvVarCollectionService, IExtensionActivationService); } diff --git a/src/client/terminals/types.ts b/src/client/terminals/types.ts index cf31f4ef1dd0..1384057c3b7c 100644 --- a/src/client/terminals/types.ts +++ b/src/client/terminals/types.ts @@ -3,19 +3,20 @@ import { Event, Terminal, TextEditor, Uri } from 'vscode'; import { IDisposable, Resource } from '../common/types'; +import { ReplType } from '../repl/types'; export const ICodeExecutionService = Symbol('ICodeExecutionService'); export interface ICodeExecutionService { execute(code: string, resource?: Uri): Promise; - executeFile(file: Uri): Promise; + executeFile(file: Uri, options?: { newTerminalPerFile: boolean }): Promise; initializeRepl(resource?: Uri): Promise; } export const ICodeExecutionHelper = Symbol('ICodeExecutionHelper'); export interface ICodeExecutionHelper { - normalizeLines(code: string): Promise; + normalizeLines(code: string, replType: ReplType, wholeFileContent?: string, resource?: Uri): Promise; getFileToExecute(): Promise; saveFileIfDirty(file: Uri): Promise; getSelectedTextToExecute(textEditor: TextEditor): Promise; @@ -24,7 +25,6 @@ export interface ICodeExecutionHelper { export const ICodeExecutionManager = Symbol('ICodeExecutionManager'); export interface ICodeExecutionManager { - onExecutedCode: Event; registerCommands(): void; } @@ -33,3 +33,28 @@ export interface ITerminalAutoActivation extends IDisposable { register(): void; disableAutoActivation(terminal: Terminal): void; } + +export const ITerminalEnvVarCollectionService = Symbol('ITerminalEnvVarCollectionService'); +export interface ITerminalEnvVarCollectionService { + /** + * Returns true if we know with high certainity the terminal prompt is set correctly for a particular resource. + */ + isTerminalPromptSetCorrectly(resource?: Resource): boolean; +} + +export const IShellIntegrationDetectionService = Symbol('IShellIntegrationDetectionService'); +export interface IShellIntegrationDetectionService { + onDidChangeStatus: Event; + isWorking(): Promise; +} + +export const ITerminalDeactivateService = Symbol('ITerminalDeactivateService'); +export interface ITerminalDeactivateService { + initializeScriptParams(shell: string): Promise; + getScriptLocation(shell: string, resource: Resource): Promise; +} + +export const IPythonStartupEnvVarService = Symbol('IPythonStartupEnvVarService'); +export interface IPythonStartupEnvVarService { + register(): void; +} diff --git a/src/client/testing/common/debugLauncher.ts b/src/client/testing/common/debugLauncher.ts index 36432c0bd831..037bfb265088 100644 --- a/src/client/testing/common/debugLauncher.ts +++ b/src/client/testing/common/debugLauncher.ts @@ -1,20 +1,30 @@ import { inject, injectable, named } from 'inversify'; import * as path from 'path'; -import { DebugConfiguration, l10n, Uri, WorkspaceFolder } from 'vscode'; +import { DebugConfiguration, l10n, Uri, WorkspaceFolder, DebugSession, DebugSessionOptions, Disposable } from 'vscode'; import { IApplicationShell, IDebugService } from '../../common/application/types'; import { EXTENSION_ROOT_DIR } from '../../common/constants'; import * as internalScripts from '../../common/process/internal/scripts'; import { IConfigurationService, IPythonSettings } from '../../common/types'; -import { DebuggerTypeName } from '../../debugger/constants'; +import { DebuggerTypeName, PythonDebuggerTypeName } from '../../debugger/constants'; import { IDebugConfigurationResolver } from '../../debugger/extension/configuration/types'; import { DebugPurpose, LaunchRequestArguments } from '../../debugger/types'; import { IServiceContainer } from '../../ioc/types'; -import { traceError } from '../../logging'; +import { traceError, traceVerbose } from '../../logging'; import { TestProvider } from '../types'; import { ITestDebugLauncher, LaunchOptions } from './types'; import { getConfigurationsForWorkspace } from '../../debugger/extension/configuration/launch.json/launchJsonReader'; import { getWorkspaceFolder, getWorkspaceFolders } from '../../common/vscodeApis/workspaceApis'; import { showErrorMessage } from '../../common/vscodeApis/windowApis'; +import { createDeferred } from '../../common/utils/async'; +import { addPathToPythonpath } from './helpers'; +import * as envExtApi from '../../envExt/api.internal'; + +/** + * Key used to mark debug configurations with a unique session identifier. + * This allows us to track which debug session belongs to which launchDebugger() call + * when multiple debug sessions are launched in parallel. + */ +const TEST_SESSION_MARKER_KEY = '__vscodeTestSessionMarker'; @injectable() export class DebugLauncher implements ITestDebugLauncher { @@ -29,9 +39,45 @@ export class DebugLauncher implements ITestDebugLauncher { this.configService = this.serviceContainer.get(IConfigurationService); } - public async launchDebugger(options: LaunchOptions): Promise { + /** + * Launches a debug session for test execution. + * Handles cancellation, multi-session support via unique markers, and cleanup. + */ + public async launchDebugger( + options: LaunchOptions, + callback?: () => void, + sessionOptions?: DebugSessionOptions, + ): Promise { + const deferred = createDeferred(); + let hasCallbackBeenCalled = false; + + // Collect disposables for cleanup when debugging completes + const disposables: Disposable[] = []; + + // Ensure callback is only invoked once, even if multiple termination paths fire + const callCallbackOnce = () => { + if (!hasCallbackBeenCalled) { + hasCallbackBeenCalled = true; + callback?.(); + } + }; + + // Early exit if already cancelled before we start if (options.token && options.token.isCancellationRequested) { - return undefined; + callCallbackOnce(); + deferred.resolve(); + return deferred.promise; + } + + // Listen for cancellation from the test run (e.g., user clicks stop in Test Explorer) + // This allows the caller to clean up resources even if the debug session is still running + if (options.token) { + disposables.push( + options.token.onCancellationRequested(() => { + deferred.resolve(); + callCallbackOnce(); + }), + ); } const workspaceFolder = DebugLauncher.resolveWorkspaceFolder(options.cwd); @@ -42,16 +88,58 @@ export class DebugLauncher implements ITestDebugLauncher { ); const debugManager = this.serviceContainer.get(IDebugService); - return debugManager.startDebugging(workspaceFolder, launchArgs).then( - // Wait for debug session to be complete. - () => - new Promise((resolve) => { - debugManager.onDidTerminateDebugSession(() => { - resolve(); - }); - }), - (ex) => traceError('Failed to start debugging tests', ex), + // Unique marker to identify this session among concurrent debug sessions + const sessionMarker = `test-${Date.now()}-${Math.random().toString(36).slice(2)}`; + launchArgs[TEST_SESSION_MARKER_KEY] = sessionMarker; + + let ourSession: DebugSession | undefined; + + // Capture our specific debug session when it starts by matching the marker. + // This fires for ALL debug sessions, so we filter to only our marker. + disposables.push( + debugManager.onDidStartDebugSession((session) => { + if (session.configuration[TEST_SESSION_MARKER_KEY] === sessionMarker) { + ourSession = session; + traceVerbose(`[test-debug] Debug session started: ${session.name} (${session.id})`); + } + }), + ); + + // Handle debug session termination (user stops debugging, or tests complete). + // Only react to OUR session terminating - other parallel sessions should + // continue running independently. + disposables.push( + debugManager.onDidTerminateDebugSession((session) => { + if (ourSession && session.id === ourSession.id) { + traceVerbose(`[test-debug] Debug session terminated: ${session.name} (${session.id})`); + deferred.resolve(); + callCallbackOnce(); + } + }), ); + + // Clean up event subscriptions when debugging completes (success, failure, or cancellation) + deferred.promise.finally(() => { + disposables.forEach((d) => d.dispose()); + }); + + // Start the debug session + let started = false; + try { + started = await debugManager.startDebugging(workspaceFolder, launchArgs, sessionOptions); + } catch (error) { + traceError('Error starting debug session', error); + deferred.reject(error); + callCallbackOnce(); + return deferred.promise; + } + if (!started) { + traceError('Failed to start debug session'); + deferred.resolve(); + callCallbackOnce(); + } + + return deferred.promise; } private static resolveWorkspaceFolder(cwd: string): WorkspaceFolder { @@ -78,19 +166,26 @@ export class DebugLauncher implements ITestDebugLauncher { if (!debugConfig) { debugConfig = { name: 'Debug Unit Test', - type: 'python', + type: 'debugpy', request: 'test', subProcess: true, }; } + + // Use project name in debug session name if provided + if (options.project) { + debugConfig.name = `Debug Tests: ${options.project.name}`; + } + if (!debugConfig.rules) { debugConfig.rules = []; } debugConfig.rules.push({ - path: path.join(EXTENSION_ROOT_DIR, 'pythonFiles'), + path: path.join(EXTENSION_ROOT_DIR, 'python_files'), include: false, }); - DebugLauncher.applyDefaults(debugConfig!, workspaceFolder, configSettings); + + DebugLauncher.applyDefaults(debugConfig!, workspaceFolder, configSettings, options.cwd); return this.convertConfigToArgs(debugConfig!, workspaceFolder, options); } @@ -117,7 +212,7 @@ export class DebugLauncher implements ITestDebugLauncher { for (const cfg of configs) { if ( cfg.name && - cfg.type === DebuggerTypeName && + (cfg.type === DebuggerTypeName || cfg.type === PythonDebuggerTypeName) && (cfg.request === 'test' || (cfg as LaunchRequestArguments).purpose?.includes(DebugPurpose.DebugTest)) ) { @@ -137,16 +232,17 @@ export class DebugLauncher implements ITestDebugLauncher { cfg: LaunchRequestArguments, workspaceFolder: WorkspaceFolder, configSettings: IPythonSettings, + optionsCwd?: string, ) { // cfg.pythonPath is handled by LaunchConfigurationResolver. - // Default value of justMyCode is not provided intentionally, for now we derive its value required for launchArgs using debugStdLib - // Have to provide it if and when we remove complete support for debugStdLib if (!cfg.console) { cfg.console = 'internalConsole'; } if (!cfg.cwd) { - cfg.cwd = workspaceFolder.uri.fsPath; + // For project-based testing, use the project's cwd (optionsCwd) if provided. + // Otherwise fall back to settings.testing.cwd or the workspace folder. + cfg.cwd = optionsCwd || configSettings.testing.cwd || workspaceFolder.uri.fsPath; } if (!cfg.env) { cfg.env = {}; @@ -181,6 +277,7 @@ export class DebugLauncher implements ITestDebugLauncher { const args = script(testArgs); const [program] = args; configArgs.program = program; + configArgs.args = args.slice(1); // We leave configArgs.request as "test" so it will be sent in telemetry. @@ -202,21 +299,63 @@ export class DebugLauncher implements ITestDebugLauncher { } launchArgs.request = 'launch'; + if (options.pytestPort && options.runTestIdsPort) { + launchArgs.env = { + ...launchArgs.env, + TEST_RUN_PIPE: options.pytestPort, + RUN_TEST_IDS_PIPE: options.runTestIdsPort, + }; + } else { + throw Error( + `Missing value for debug setup, both port and uuid need to be defined. port: "${options.pytestPort}" uuid: "${options.pytestUUID}"`, + ); + } + + const pluginPath = path.join(EXTENSION_ROOT_DIR, 'python_files'); + // check if PYTHONPATH is already set in the environment variables + if (launchArgs.env) { + const additionalPythonPath = [pluginPath]; + if (launchArgs.cwd) { + additionalPythonPath.push(launchArgs.cwd); + } else if (options.cwd) { + additionalPythonPath.push(options.cwd); + } + // add the plugin path or cwd to PYTHONPATH if it is not already there using the following function + // this function will handle if PYTHONPATH is undefined + addPathToPythonpath(additionalPythonPath, launchArgs.env.PYTHONPATH); + } + // Clear out purpose so we can detect if the configuration was used to // run via F5 style debugging. launchArgs.purpose = []; + // For project-based execution, get the Python path from the project's environment. + // Fallback: if env API unavailable or fails, LaunchConfigurationResolver already set + // launchArgs.python from the active interpreter, so debugging still works. + if (options.project && envExtApi.useEnvExtension()) { + try { + const pythonEnv = await envExtApi.getEnvironment(options.project.uri); + if (pythonEnv?.execInfo?.run?.executable) { + launchArgs.python = pythonEnv.execInfo.run.executable; + traceVerbose( + `[test-by-project] Debug session using Python path from project: ${launchArgs.python}`, + ); + } + } catch (error) { + traceVerbose(`[test-by-project] Could not get environment for project, using default: ${error}`); + } + } + return launchArgs; } private static getTestLauncherScript(testProvider: TestProvider) { switch (testProvider) { case 'unittest': { - return internalScripts.visualstudio_py_testlauncher; // old way unittest execution, debugger - // return internalScripts.execution_py_testlauncher; // this is the new way to run unittest execution, debugger + return internalScripts.execution_py_testlauncher; // this is the new way to run unittest execution, debugger } case 'pytest': { - return internalScripts.testlauncher; + return internalScripts.pytestlauncher; // this is the new way to run pytest execution, debugger } default: { throw new Error(`Unknown test provider '${testProvider}'`); diff --git a/src/client/testing/common/helpers.ts b/src/client/testing/common/helpers.ts new file mode 100644 index 000000000000..021849277b33 --- /dev/null +++ b/src/client/testing/common/helpers.ts @@ -0,0 +1,37 @@ +import * as path from 'path'; + +/** + * This function normalizes the provided paths and the existing paths in PYTHONPATH, + * adds the provided paths to PYTHONPATH if they're not already present, + * and then returns the updated PYTHONPATH. + * + * @param newPaths - An array of paths to be added to PYTHONPATH + * @param launchPythonPath - The initial PYTHONPATH + * @returns The updated PYTHONPATH + */ +export function addPathToPythonpath(newPaths: string[], launchPythonPath: string | undefined): string { + // Split PYTHONPATH into array of paths if it exists + let paths: string[]; + if (!launchPythonPath) { + paths = []; + } else { + paths = launchPythonPath.split(path.delimiter); + } + + // Normalize each path in the existing PYTHONPATH + paths = paths.map((p) => path.normalize(p)); + + // Normalize each new path and add it to PYTHONPATH if it's not already present + newPaths.forEach((newPath) => { + const normalizedNewPath: string = path.normalize(newPath); + + if (!paths.includes(normalizedNewPath)) { + paths.push(normalizedNewPath); + } + }); + + // Join the paths with ':' to create the updated PYTHONPATH + const updatedPythonPath: string = paths.join(path.delimiter); + + return updatedPythonPath; +} diff --git a/src/client/testing/common/runner.ts b/src/client/testing/common/runner.ts deleted file mode 100644 index b6e6f2fb3b24..000000000000 --- a/src/client/testing/common/runner.ts +++ /dev/null @@ -1,129 +0,0 @@ -import { inject, injectable } from 'inversify'; -import * as path from 'path'; -import { ErrorUtils } from '../../common/errors/errorUtils'; -import { ModuleNotInstalledError } from '../../common/errors/moduleNotInstalledError'; -import { - IPythonExecutionFactory, - IPythonExecutionService, - IPythonToolExecutionService, - ObservableExecutionResult, - SpawnOptions, -} from '../../common/process/types'; -import { ExecutionInfo, IConfigurationService, IPythonSettings } from '../../common/types'; -import { IServiceContainer } from '../../ioc/types'; -import { TestProvider } from '../types'; -import { PYTEST_PROVIDER, UNITTEST_PROVIDER } from './constants'; -import { ITestRunner, ITestsHelper, Options } from './types'; - -@injectable() -export class TestRunner implements ITestRunner { - constructor(@inject(IServiceContainer) private serviceContainer: IServiceContainer) {} - public run(testProvider: TestProvider, options: Options): Promise { - return run(this.serviceContainer, testProvider, options); - } -} - -async function run(serviceContainer: IServiceContainer, testProvider: TestProvider, options: Options): Promise { - const testExecutablePath = getExecutablePath( - testProvider, - serviceContainer.get(IConfigurationService).getSettings(options.workspaceFolder), - ); - const moduleName = getTestModuleName(testProvider); - const spawnOptions = options as SpawnOptions; - let pythonExecutionServicePromise: Promise | undefined; - spawnOptions.mergeStdOutErr = typeof spawnOptions.mergeStdOutErr === 'boolean' ? spawnOptions.mergeStdOutErr : true; - - let promise: Promise>; - - // Since conda 4.4.0 we have found that running python code needs the environment activated. - // So if running an executable, there's no way we can activate, if its a module, then activate and run the module. - const testHelper = serviceContainer.get(ITestsHelper); - const executionInfo: ExecutionInfo = { - execPath: testExecutablePath, - args: options.args, - moduleName: testExecutablePath && testExecutablePath.length > 0 ? undefined : moduleName, - product: testHelper.parseProduct(testProvider), - }; - - if (testProvider === UNITTEST_PROVIDER) { - promise = serviceContainer - .get(IPythonExecutionFactory) - .createActivatedEnvironment({ resource: options.workspaceFolder }) - .then((executionService) => executionService.execObservable(options.args, { ...spawnOptions })); - } else if (typeof executionInfo.moduleName === 'string' && executionInfo.moduleName.length > 0) { - pythonExecutionServicePromise = serviceContainer - .get(IPythonExecutionFactory) - .createActivatedEnvironment({ resource: options.workspaceFolder }); - promise = pythonExecutionServicePromise.then((executionService) => - executionService.execModuleObservable(executionInfo.moduleName!, executionInfo.args, options), - ); - } else { - const pythonToolsExecutionService = serviceContainer.get( - IPythonToolExecutionService, - ); - promise = pythonToolsExecutionService.execObservable(executionInfo, spawnOptions, options.workspaceFolder); - } - - return promise.then((result) => { - return new Promise((resolve, reject) => { - let stdOut = ''; - let stdErr = ''; - result.out.subscribe( - (output) => { - stdOut += output.out; - // If the test runner python module is not installed we'll have something in stderr. - // Hence track that separately and check at the end. - if (output.source === 'stderr') { - stdErr += output.out; - } - if (options.outChannel) { - options.outChannel.append(output.out); - } - }, - reject, - async () => { - // If the test runner python module is not installed we'll have something in stderr. - if ( - moduleName && - pythonExecutionServicePromise && - ErrorUtils.outputHasModuleNotInstalledError(moduleName, stdErr) - ) { - const pythonExecutionService = await pythonExecutionServicePromise; - const isInstalled = await pythonExecutionService.isModuleInstalled(moduleName); - if (!isInstalled) { - return reject(new ModuleNotInstalledError(moduleName)); - } - } - resolve(stdOut); - }, - ); - }); - }); -} - -function getExecutablePath(testProvider: TestProvider, settings: IPythonSettings): string | undefined { - let testRunnerExecutablePath: string | undefined; - switch (testProvider) { - case PYTEST_PROVIDER: { - testRunnerExecutablePath = settings.testing.pytestPath; - break; - } - default: { - return undefined; - } - } - return path.basename(testRunnerExecutablePath) === testRunnerExecutablePath ? undefined : testRunnerExecutablePath; -} -function getTestModuleName(testProvider: TestProvider) { - switch (testProvider) { - case PYTEST_PROVIDER: { - return 'pytest'; - } - case UNITTEST_PROVIDER: { - return 'unittest'; - } - default: { - throw new Error(`Test provider '${testProvider}' not supported`); - } - } -} diff --git a/src/client/testing/common/socketServer.ts b/src/client/testing/common/socketServer.ts deleted file mode 100644 index 554d8c8a0c76..000000000000 --- a/src/client/testing/common/socketServer.ts +++ /dev/null @@ -1,135 +0,0 @@ -'use strict'; - -import { EventEmitter } from 'events'; -import { injectable } from 'inversify'; -import * as net from 'net'; -import { createDeferred, Deferred } from '../../common/utils/async'; -import { IUnitTestSocketServer } from './types'; - -const MaxConnections = 100; - -@injectable() -export class UnitTestSocketServer extends EventEmitter implements IUnitTestSocketServer { - private server?: net.Server; - - private startedDef?: Deferred; - - private sockets: net.Socket[] = []; - - private ipcBuffer = ''; - - constructor() { - super(); - } - - public get clientsConnected(): boolean { - return this.sockets.length > 0; - } - - public dispose() { - this.stop(); - } - - public stop() { - if (this.server) { - this.server.close(); - this.server = undefined; - } - } - - public start({ port, host }: { port: number; host: string } = { port: 0, host: 'localhost' }): Promise { - this.ipcBuffer = ''; - this.startedDef = createDeferred(); - this.server = net.createServer(this.connectionListener.bind(this)); - this.server.maxConnections = MaxConnections; - this.server.on('error', (err) => { - if (this.startedDef) { - this.startedDef.reject(err); - this.startedDef = undefined; - } - this.emit('error', err); - }); - this.log('starting server as', 'TCP'); - if (host.trim().length === 0) { - host = 'localhost'; - } - this.server.on('connection', (socket: net.Socket) => { - this.emit('start', socket); - }); - this.server.listen(port, host, () => { - this.startedDef?.resolve((this.server?.address() as net.AddressInfo).port); - this.startedDef = undefined; - }); - return this.startedDef?.promise; - } - - private connectionListener(socket: net.Socket) { - this.sockets.push(socket); - socket.setEncoding('utf8'); - this.log('## socket connection to server detected ##'); - socket.on('close', () => { - this.ipcBuffer = ''; - this.onCloseSocket(); - }); - socket.on('error', (err) => { - this.log('server socket error', err); - this.emit('error', err); - }); - socket.on('data', (data) => { - const sock = socket; - // Assume we have just one client socket connection - let dataStr = (this.ipcBuffer += data); - - while (true) { - const startIndex = dataStr.indexOf('{'); - if (startIndex === -1) { - return; - } - const lengthOfMessage = parseInt( - dataStr.slice(dataStr.indexOf(':') + 1, dataStr.indexOf('{')).trim(), - 10, - ); - if (dataStr.length < startIndex + lengthOfMessage) { - return; - } - - let message: any; - try { - message = JSON.parse(dataStr.substring(startIndex, lengthOfMessage + startIndex)); - } catch (jsonErr) { - this.emit('error', jsonErr); - return; - } - dataStr = this.ipcBuffer = dataStr.substring(startIndex + lengthOfMessage); - this.emit(message.event, message.body, sock); - } - }); - this.emit('connect', socket); - } - - private log(message: string, ...data: any[]) { - this.emit('log', message, ...data); - } - - private onCloseSocket() { - for (let i = 0, count = this.sockets.length; i < count; i += 1) { - const socket = this.sockets[i]; - - if (socket && socket.readable) { - continue; - } - - let destroyedSocketId; - if ((socket as any).id) { - destroyedSocketId = (socket as any).id; - } - this.log('socket disconnected', destroyedSocketId.toString()); - if (socket && socket.destroy) { - socket.destroy(); - } - this.sockets.splice(i, 1); - this.emit('socket.disconnected', socket, destroyedSocketId); - return; - } - } -} diff --git a/src/client/testing/common/types.ts b/src/client/testing/common/types.ts index b1476e74435f..e2fa2d6d2e5a 100644 --- a/src/client/testing/common/types.ts +++ b/src/client/testing/common/types.ts @@ -1,7 +1,8 @@ -import { CancellationToken, Disposable, OutputChannel, Uri } from 'vscode'; +import { CancellationToken, DebugSessionOptions, OutputChannel, Uri } from 'vscode'; import { Product } from '../../common/types'; import { TestSettingsPropertyNames } from '../configuration/types'; import { TestProvider } from '../types'; +import { PythonProject } from '../../envExt/types'; export type UnitTestProduct = Product.pytest | Product.unittest; @@ -17,24 +18,17 @@ export type TestDiscoveryOptions = { outChannel?: OutputChannel; }; -export type UnitTestParserOptions = TestDiscoveryOptions & { startDirectory: string }; - export type LaunchOptions = { cwd: string; args: string[]; testProvider: TestProvider; token?: CancellationToken; outChannel?: OutputChannel; -}; - -export type ParserOptions = TestDiscoveryOptions; - -export type Options = { - workspaceFolder: Uri; - cwd: string; - args: string[]; - outChannel?: OutputChannel; - token?: CancellationToken; + pytestPort?: string; + pytestUUID?: string; + runTestIdsPort?: string; + /** Optional Python project for project-based execution. */ + project?: PythonProject; }; export enum TestFilter { @@ -58,7 +52,7 @@ export interface ITestsHelper { export const ITestConfigurationService = Symbol('ITestConfigurationService'); export interface ITestConfigurationService { - displayTestFrameworkError(wkspace: Uri): Promise; + hasConfiguredTests(wkspace: Uri): boolean; selectTestRunner(placeHolderMessage: string): Promise; enableTest(wkspace: Uri, product: UnitTestProduct): Promise; promptToEnableAndConfigureTestFramework(wkspace: Uri): Promise; @@ -85,19 +79,5 @@ export interface ITestConfigurationManagerFactory { } export const ITestDebugLauncher = Symbol('ITestDebugLauncher'); export interface ITestDebugLauncher { - launchDebugger(options: LaunchOptions): Promise; -} - -export const IUnitTestSocketServer = Symbol('IUnitTestSocketServer'); -export interface IUnitTestSocketServer extends Disposable { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - on(event: string | symbol, listener: (...args: any[]) => void): this; - removeAllListeners(event?: string | symbol): this; - start(options?: { port?: number; host?: string }): Promise; - stop(): void; -} - -export const ITestRunner = Symbol('ITestRunner'); -export interface ITestRunner { - run(testProvider: TestProvider, options: Options): Promise; + launchDebugger(options: LaunchOptions, callback?: () => void, sessionOptions?: DebugSessionOptions): Promise; } diff --git a/src/client/testing/configuration/index.ts b/src/client/testing/configuration/index.ts index e85154e72738..b78475293594 100644 --- a/src/client/testing/configuration/index.ts +++ b/src/client/testing/configuration/index.ts @@ -35,26 +35,9 @@ export class UnitTestConfigurationService implements ITestConfigurationService { this.workspaceService = serviceContainer.get(IWorkspaceService); } - public async displayTestFrameworkError(wkspace: Uri): Promise { + public hasConfiguredTests(wkspace: Uri): boolean { const settings = this.configurationService.getSettings(wkspace); - let enabledCount = settings.testing.pytestEnabled ? 1 : 0; - enabledCount += settings.testing.unittestEnabled ? 1 : 0; - if (enabledCount > 1) { - return this._promptToEnableAndConfigureTestFramework( - wkspace, - 'Enable only one of the test frameworks (unittest or pytest).', - true, - ); - } - const option = 'Enable and configure a Test Framework'; - const item = await this.appShell.showInformationMessage( - 'No test framework configured (unittest, or pytest)', - option, - ); - if (item !== option) { - throw NONE_SELECTED; - } - return this._promptToEnableAndConfigureTestFramework(wkspace); + return settings.testing.pytestEnabled || settings.testing.unittestEnabled || false; } public async selectTestRunner(placeHolderMessage: string): Promise { diff --git a/src/client/testing/configuration/pytest/testConfigurationManager.ts b/src/client/testing/configuration/pytest/testConfigurationManager.ts index 89f4246346ef..08f88f8564c7 100644 --- a/src/client/testing/configuration/pytest/testConfigurationManager.ts +++ b/src/client/testing/configuration/pytest/testConfigurationManager.ts @@ -3,12 +3,19 @@ import { QuickPickItem, Uri } from 'vscode'; import { IFileSystem } from '../../../common/platform/types'; import { Product } from '../../../common/types'; import { IServiceContainer } from '../../../ioc/types'; +import { IApplicationShell } from '../../../common/application/types'; import { TestConfigurationManager } from '../../common/testConfigurationManager'; import { ITestConfigSettingsService } from '../../common/types'; +import { PytestInstallationHelper } from '../pytestInstallationHelper'; +import { traceInfo } from '../../../logging'; export class ConfigurationManager extends TestConfigurationManager { + private readonly pytestInstallationHelper: PytestInstallationHelper; + constructor(workspace: Uri, serviceContainer: IServiceContainer, cfg?: ITestConfigSettingsService) { super(workspace, Product.pytest, serviceContainer, cfg); + const appShell = serviceContainer.get(IApplicationShell); + this.pytestInstallationHelper = new PytestInstallationHelper(appShell); } public async requiresUserToConfigure(wkspace: Uri): Promise { @@ -42,10 +49,22 @@ export class ConfigurationManager extends TestConfigurationManager { args.push(testDir); } const installed = await this.installer.isInstalled(Product.pytest); + await this.testConfigSettingsService.updateTestArgs(wkspace.fsPath, Product.pytest, args); if (!installed) { - await this.installer.install(Product.pytest); + // Check if Python Environments extension is available for enhanced installation flow + if (this.pytestInstallationHelper.isEnvExtensionAvailable()) { + traceInfo('pytest not installed, prompting user with environment extension integration'); + const installAttempted = await this.pytestInstallationHelper.promptToInstallPytest(wkspace); + if (!installAttempted) { + // User chose to ignore or installation failed + return; + } + } else { + // Fall back to traditional installer + traceInfo('pytest not installed, falling back to traditional installer'); + await this.installer.install(Product.pytest); + } } - await this.testConfigSettingsService.updateTestArgs(wkspace.fsPath, Product.pytest, args); } private async getConfigFiles(rootDir: string): Promise { diff --git a/src/client/testing/configuration/pytestInstallationHelper.ts b/src/client/testing/configuration/pytestInstallationHelper.ts new file mode 100644 index 000000000000..bd5fbcd5bb37 --- /dev/null +++ b/src/client/testing/configuration/pytestInstallationHelper.ts @@ -0,0 +1,95 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { Uri, l10n } from 'vscode'; +import { IApplicationShell } from '../../common/application/types'; +import { traceInfo, traceError } from '../../logging'; +import { useEnvExtension, getEnvExtApi } from '../../envExt/api.internal'; +import { getEnvironment } from '../../envExt/api.internal'; + +/** + * Helper class to handle pytest installation using the appropriate method + * based on whether the Python Environments extension is available. + */ +export class PytestInstallationHelper { + constructor(private readonly appShell: IApplicationShell) {} + + /** + * Prompts the user to install pytest with appropriate installation method. + * @param workspaceUri The workspace URI where pytest should be installed + * @returns Promise that resolves to true if installation was attempted, false otherwise + */ + async promptToInstallPytest(workspaceUri: Uri): Promise { + const message = l10n.t('pytest selected but not installed. Would you like to install pytest?'); + const installOption = l10n.t('Install pytest'); + + const selection = await this.appShell.showInformationMessage(message, { modal: true }, installOption); + + if (selection === installOption) { + return this.installPytest(workspaceUri); + } + + return false; + } + + /** + * Installs pytest using the appropriate method based on available extensions. + * @param workspaceUri The workspace URI where pytest should be installed + * @returns Promise that resolves to true if installation was successful, false otherwise + */ + private async installPytest(workspaceUri: Uri): Promise { + try { + if (useEnvExtension()) { + return this.installPytestWithEnvExtension(workspaceUri); + } else { + // Fall back to traditional installer if environments extension is not available + traceInfo( + 'Python Environments extension not available, installation cannot proceed via environment extension', + ); + return false; + } + } catch (error) { + traceError('Error installing pytest:', error); + return false; + } + } + + /** + * Installs pytest using the Python Environments extension. + * @param workspaceUri The workspace URI where pytest should be installed + * @returns Promise that resolves to true if installation was successful, false otherwise + */ + private async installPytestWithEnvExtension(workspaceUri: Uri): Promise { + try { + const envExtApi = await getEnvExtApi(); + const environment = await getEnvironment(workspaceUri); + + if (!environment) { + traceError('No Python environment found for workspace:', workspaceUri.fsPath); + await this.appShell.showErrorMessage( + l10n.t('No Python environment found. Please set up a Python environment first.'), + ); + return false; + } + + traceInfo('Installing pytest using Python Environments extension...'); + await envExtApi.managePackages(environment, { + install: ['pytest'], + }); + + traceInfo('pytest installation completed successfully'); + return true; + } catch (error) { + traceError('Failed to install pytest using Python Environments extension:', error); + return false; + } + } + + /** + * Checks if the Python Environments extension is available for package management. + * @returns True if the extension is available, false otherwise + */ + isEnvExtensionAvailable(): boolean { + return useEnvExtension(); + } +} diff --git a/src/client/testing/configuration/types.ts b/src/client/testing/configuration/types.ts index 5da99398283b..3b759bcb39e8 100644 --- a/src/client/testing/configuration/types.ts +++ b/src/client/testing/configuration/types.ts @@ -11,6 +11,7 @@ export interface ITestingSettings { unittestArgs: string[]; cwd?: string; readonly autoTestDiscoverOnSaveEnabled: boolean; + readonly autoTestDiscoverOnSavePattern: string; } export type TestSettingsPropertyNames = { diff --git a/src/client/testing/main.ts b/src/client/testing/main.ts index deebf2b34c06..eed4d70e852c 100644 --- a/src/client/testing/main.ts +++ b/src/client/testing/main.ts @@ -1,7 +1,16 @@ 'use strict'; import { inject, injectable } from 'inversify'; -import { ConfigurationChangeEvent, Disposable, Uri, tests, TestResultState, WorkspaceFolder } from 'vscode'; +import { + ConfigurationChangeEvent, + Disposable, + Uri, + tests, + TestResultState, + WorkspaceFolder, + Command, + TestItem, +} from 'vscode'; import { IApplicationShell, ICommandManager, IContextKeyManager, IWorkspaceService } from '../common/application/types'; import * as constants from '../common/constants'; import '../common/extensions'; @@ -9,7 +18,7 @@ import { IDisposableRegistry, Product } from '../common/types'; import { IInterpreterService } from '../interpreter/contracts'; import { IServiceContainer } from '../ioc/types'; import { EventName } from '../telemetry/constants'; -import { captureTelemetry, sendTelemetryEvent } from '../telemetry/index'; +import { sendTelemetryEvent } from '../telemetry/index'; import { selectTestWorkspace } from './common/testUtils'; import { TestSettingsPropertyNames } from './configuration/types'; import { ITestConfigurationService, ITestsHelper } from './common/types'; @@ -20,7 +29,8 @@ import { DelayedTrigger, IDelayedTrigger } from '../common/utils/delayTrigger'; import { ExtensionContextKey } from '../common/application/contextKeys'; import { checkForFailedTests, updateTestResultMap } from './testController/common/testItemUtilities'; import { Testing } from '../common/utils/localize'; -import { traceVerbose } from '../logging'; +import { traceVerbose, traceWarn } from '../logging'; +import { writeTestIdToClipboard } from './utils'; @injectable() export class TestingService implements ITestingService { @@ -32,6 +42,91 @@ export class TestingService implements ITestingService { } } +/** + * Registers command handlers but defers service resolution until the commands are actually invoked, + * allowing registration to happen before all services are fully initialized. + */ +export function registerTestCommands(serviceContainer: IServiceContainer): void { + // Resolve only the essential services needed for command registration itself + const disposableRegistry = serviceContainer.get(IDisposableRegistry); + const commandManager = serviceContainer.get(ICommandManager); + + // Helper function to configure tests - services are resolved when invoked, not at registration time + const configureTestsHandler = async (resource?: Uri) => { + sendTelemetryEvent(EventName.UNITTEST_CONFIGURE); + + // Resolve services lazily when the command is invoked + const workspaceService = serviceContainer.get(IWorkspaceService); + + let wkspace: Uri | undefined; + if (resource) { + const wkspaceFolder = workspaceService.getWorkspaceFolder(resource); + wkspace = wkspaceFolder ? wkspaceFolder.uri : undefined; + } else { + const appShell = serviceContainer.get(IApplicationShell); + wkspace = await selectTestWorkspace(appShell); + } + if (!wkspace) { + return; + } + const interpreterService = serviceContainer.get(IInterpreterService); + const cmdManager = serviceContainer.get(ICommandManager); + if (!(await interpreterService.getActiveInterpreter(wkspace))) { + cmdManager.executeCommand(constants.Commands.TriggerEnvironmentSelection, wkspace); + return; + } + const configurationService = serviceContainer.get(ITestConfigurationService); + await configurationService.promptToEnableAndConfigureTestFramework(wkspace); + }; + + disposableRegistry.push( + // Command: python.configureTests - prompts user to configure test framework + commandManager.registerCommand( + constants.Commands.Tests_Configure, + (_, _cmdSource: constants.CommandSource = constants.CommandSource.commandPalette, resource?: Uri) => { + // Invoke configuration handler (errors are ignored as this can be called from multiple places) + configureTestsHandler(resource).ignoreErrors(); + traceVerbose('Testing: Trigger refresh after config change'); + // Refresh test data if test controller is available (resolved lazily) + if (tests && !!tests.createTestController) { + const testController = serviceContainer.get(ITestController); + testController?.refreshTestData(resource, { forceRefresh: true }); + } + }, + ), + // Command: python.tests.copilotSetup - Copilot integration for test setup + commandManager.registerCommand(constants.Commands.Tests_CopilotSetup, (resource?: Uri): + | { message: string; command: Command } + | undefined => { + // Resolve services lazily when the command is invoked + const workspaceService = serviceContainer.get(IWorkspaceService); + const wkspaceFolder = + workspaceService.getWorkspaceFolder(resource) || workspaceService.workspaceFolders?.at(0); + if (!wkspaceFolder) { + return undefined; + } + + const configurationService = serviceContainer.get(ITestConfigurationService); + if (configurationService.hasConfiguredTests(wkspaceFolder.uri)) { + return undefined; + } + + return { + message: Testing.copilotSetupMessage, + command: { + title: Testing.configureTests, + command: constants.Commands.Tests_Configure, + arguments: [undefined, constants.CommandSource.ui, resource], + }, + }; + }), + // Command: python.copyTestId - copies test ID to clipboard + commandManager.registerCommand(constants.Commands.CopyTestId, async (testItem: TestItem) => { + writeTestIdToClipboard(testItem); + }), + ); +} + @injectable() export class UnitTestManagementService implements IExtensionActivationService { private activatedOnce: boolean = false; @@ -70,7 +165,6 @@ export class UnitTestManagementService implements IExtensionActivationService { this.activatedOnce = true; this.registerHandlers(); - this.registerCommands(); if (!!tests.testResults) { await this.updateTestUIButtons(); @@ -93,22 +187,9 @@ export class UnitTestManagementService implements IExtensionActivationService { if (unconfigured.length === workspaces.length) { const commandManager = this.serviceContainer.get(ICommandManager); await commandManager.executeCommand('workbench.view.testing.focus'); - - // TODO: this is a workaround for https://github.com/microsoft/vscode/issues/130696 - // Once that is fixed delete this notification and test should be configured from the test view. - const app = this.serviceContainer.get(IApplicationShell); - const response = await app.showInformationMessage( - Testing.testNotConfigured, - Testing.configureTests, + traceWarn( + 'Testing: Run attempted but no test configurations found for any workspace, use command palette to configure tests for python if desired.', ); - if (response === Testing.configureTests) { - await commandManager.executeCommand( - constants.Commands.Tests_Configure, - undefined, - constants.CommandSource.ui, - unconfigured[0].uri, - ); - } } }); } @@ -133,46 +214,6 @@ export class UnitTestManagementService implements IExtensionActivationService { await Promise.all(changedWorkspaces.map((u) => this.testController?.refreshTestData(u))); } - @captureTelemetry(EventName.UNITTEST_CONFIGURE, undefined, false) - private async configureTests(resource?: Uri) { - let wkspace: Uri | undefined; - if (resource) { - const wkspaceFolder = this.workspaceService.getWorkspaceFolder(resource); - wkspace = wkspaceFolder ? wkspaceFolder.uri : undefined; - } else { - const appShell = this.serviceContainer.get(IApplicationShell); - wkspace = await selectTestWorkspace(appShell); - } - if (!wkspace) { - return; - } - const interpreterService = this.serviceContainer.get(IInterpreterService); - const commandManager = this.serviceContainer.get(ICommandManager); - if (!(await interpreterService.getActiveInterpreter(wkspace))) { - commandManager.executeCommand(constants.Commands.TriggerEnvironmentSelection, wkspace); - return; - } - const configurationService = this.serviceContainer.get(ITestConfigurationService); - await configurationService.promptToEnableAndConfigureTestFramework(wkspace!); - } - - private registerCommands(): void { - const commandManager = this.serviceContainer.get(ICommandManager); - - this.disposableRegistry.push( - commandManager.registerCommand( - constants.Commands.Tests_Configure, - (_, _cmdSource: constants.CommandSource = constants.CommandSource.commandPalette, resource?: Uri) => { - // Ignore the exceptions returned. - // This command will be invoked from other places of the extension. - this.configureTests(resource).ignoreErrors(); - traceVerbose('Testing: Trigger refresh after config change'); - this.testController?.refreshTestData(resource, { forceRefresh: true }); - }, - ), - ); - } - private registerHandlers() { const interpreterService = this.serviceContainer.get(IInterpreterService); this.disposableRegistry.push( diff --git a/src/client/testing/serviceRegistry.ts b/src/client/testing/serviceRegistry.ts index 6a7b4b5a1640..d36fab7686f8 100644 --- a/src/client/testing/serviceRegistry.ts +++ b/src/client/testing/serviceRegistry.ts @@ -4,7 +4,6 @@ import { IExtensionActivationService } from '../activation/types'; import { IServiceManager } from '../ioc/types'; import { DebugLauncher } from './common/debugLauncher'; -import { TestRunner } from './common/runner'; import { TestConfigSettingsService } from './common/configSettingService'; import { TestsHelper } from './common/testUtils'; import { @@ -12,24 +11,18 @@ import { ITestConfigurationManagerFactory, ITestConfigurationService, ITestDebugLauncher, - ITestRunner, ITestsHelper, - IUnitTestSocketServer, } from './common/types'; import { UnitTestConfigurationService } from './configuration'; import { TestConfigurationManagerFactory } from './configurationFactory'; import { TestingService, UnitTestManagementService } from './main'; import { ITestingService } from './types'; -import { UnitTestSocketServer } from './common/socketServer'; import { registerTestControllerTypes } from './testController/serviceRegistry'; export function registerTypes(serviceManager: IServiceManager) { serviceManager.addSingleton(ITestDebugLauncher, DebugLauncher); serviceManager.add(ITestsHelper, TestsHelper); - serviceManager.add(IUnitTestSocketServer, UnitTestSocketServer); - - serviceManager.add(ITestRunner, TestRunner); serviceManager.addSingleton(ITestConfigurationService, UnitTestConfigurationService); serviceManager.addSingleton(ITestingService, TestingService); diff --git a/src/client/testing/testController/common/argumentsHelper.ts b/src/client/testing/testController/common/argumentsHelper.ts index ef2999551f02..c155d0197da7 100644 --- a/src/client/testing/testController/common/argumentsHelper.ts +++ b/src/client/testing/testController/common/argumentsHelper.ts @@ -3,22 +3,6 @@ import { traceWarn } from '../../../logging'; -export function getOptionValues(args: string[], option: string): string[] { - const values: string[] = []; - let returnNextValue = false; - for (const arg of args) { - if (returnNextValue) { - values.push(arg); - returnNextValue = false; - } else if (arg.startsWith(`${option}=`)) { - values.push(arg.substring(`${option}=`.length)); - } else if (arg === option) { - returnNextValue = true; - } - } - return values; -} - export function getPositionalArguments( args: string[], optionsWithArguments: string[] = [], diff --git a/src/client/testing/testController/common/discoveryHelper.ts b/src/client/testing/testController/common/discoveryHelper.ts deleted file mode 100644 index dcd8184b7fda..000000000000 --- a/src/client/testing/testController/common/discoveryHelper.ts +++ /dev/null @@ -1,43 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -import { inject, injectable } from 'inversify'; -import { - ExecutionFactoryCreateWithEnvironmentOptions, - IPythonExecutionFactory, - SpawnOptions, -} from '../../../common/process/types'; -import { TestDiscoveryOptions } from '../../common/types'; -import { ITestDiscoveryHelper, RawDiscoveredTests } from './types'; - -@injectable() -export class TestDiscoveryHelper implements ITestDiscoveryHelper { - constructor(@inject(IPythonExecutionFactory) private readonly pythonExecFactory: IPythonExecutionFactory) {} - - public async runTestDiscovery(options: TestDiscoveryOptions): Promise { - const creationOptions: ExecutionFactoryCreateWithEnvironmentOptions = { - allowEnvironmentFetchExceptions: false, - resource: options.workspaceFolder, - }; - const execService = await this.pythonExecFactory.createActivatedEnvironment(creationOptions); - - const spawnOptions: SpawnOptions = { - token: options.token, - cwd: options.cwd, - throwOnStdErr: true, - }; - - if (options.outChannel) { - options.outChannel.appendLine(`python ${options.args.join(' ')}`); - } - - const proc = await execService.exec(options.args, spawnOptions); - try { - return JSON.parse(proc.stdout); - } catch (ex) { - const error = ex as SyntaxError; - error.message = proc.stdout; - throw ex; // re-throw - } - } -} diff --git a/src/client/testing/testController/common/discoveryHelpers.ts b/src/client/testing/testController/common/discoveryHelpers.ts new file mode 100644 index 000000000000..e170ad576ae8 --- /dev/null +++ b/src/client/testing/testController/common/discoveryHelpers.ts @@ -0,0 +1,137 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +import { CancellationToken, CancellationTokenSource, Disposable, Uri } from 'vscode'; +import { Deferred } from '../../../common/utils/async'; +import { traceError, traceInfo, traceVerbose } from '../../../logging'; +import { createDiscoveryErrorPayload, fixLogLinesNoTrailing, startDiscoveryNamedPipe } from './utils'; +import { DiscoveredTestPayload, ITestResultResolver } from './types'; + +/** + * Test provider type for logging purposes. + */ +export type TestProvider = 'pytest' | 'unittest'; + +/** + * Sets up the discovery named pipe and wires up cancellation. + * @param resultResolver The resolver to handle discovered test data + * @param token Optional cancellation token from the caller + * @param uri Workspace URI for logging + * @returns Object containing the pipe name, cancellation source, and disposable for the external token handler + */ +export async function setupDiscoveryPipe( + resultResolver: ITestResultResolver | undefined, + token: CancellationToken | undefined, + uri: Uri, +): Promise<{ pipeName: string; cancellation: CancellationTokenSource; tokenDisposable: Disposable | undefined }> { + const discoveryPipeCancellation = new CancellationTokenSource(); + + // Wire up cancellation from external token and store the disposable + const tokenDisposable = token?.onCancellationRequested(() => { + traceInfo(`Test discovery cancelled.`); + discoveryPipeCancellation.cancel(); + }); + + // Start the named pipe with the discovery listener + const discoveryPipeName = await startDiscoveryNamedPipe((data: DiscoveredTestPayload) => { + if (!token?.isCancellationRequested) { + resultResolver?.resolveDiscovery(data); + } + }, discoveryPipeCancellation.token); + + traceVerbose(`Created discovery pipe: ${discoveryPipeName} for workspace ${uri.fsPath}`); + + return { + pipeName: discoveryPipeName, + cancellation: discoveryPipeCancellation, + tokenDisposable, + }; +} + +/** + * Creates standard process event handlers for test discovery subprocess. + * Handles stdout/stderr logging and error reporting on process exit. + * + * @param testProvider - The test framework being used ('pytest' or 'unittest') + * @param uri - The workspace URI + * @param cwd - The current working directory + * @param resultResolver - Resolver for test discovery results + * @param deferredTillExecClose - Deferred to resolve when process closes + * @param allowedSuccessCodes - Additional exit codes to treat as success (e.g., pytest exit code 5 for no tests found) + */ +export function createProcessHandlers( + testProvider: TestProvider, + uri: Uri, + cwd: string, + resultResolver: ITestResultResolver | undefined, + deferredTillExecClose: Deferred, + allowedSuccessCodes: number[] = [], +): { + onStdout: (data: any) => void; + onStderr: (data: any) => void; + onExit: (code: number | null, signal: NodeJS.Signals | null) => void; + onClose: (code: number | null, signal: NodeJS.Signals | null) => void; +} { + const isSuccessCode = (code: number | null): boolean => { + return code === 0 || (code !== null && allowedSuccessCodes.includes(code)); + }; + + return { + onStdout: (data: any) => { + const out = fixLogLinesNoTrailing(data.toString()); + traceInfo(out); + }, + onStderr: (data: any) => { + const out = fixLogLinesNoTrailing(data.toString()); + traceError(out); + }, + onExit: (code: number | null, _signal: NodeJS.Signals | null) => { + // The 'exit' event fires when the process terminates, but streams may still be open. + // Only log verbose success message here; error handling happens in onClose. + if (isSuccessCode(code)) { + traceVerbose(`${testProvider} discovery subprocess exited successfully for workspace ${uri.fsPath}`); + } + }, + onClose: (code: number | null, signal: NodeJS.Signals | null) => { + // We resolve the deferred here to ensure all output has been captured. + if (!isSuccessCode(code)) { + traceError( + `${testProvider} discovery failed with exit code ${code} and signal ${signal} for workspace ${uri.fsPath}. Creating error payload.`, + ); + resultResolver?.resolveDiscovery(createDiscoveryErrorPayload(code, signal, cwd)); + } else { + traceVerbose(`${testProvider} discovery subprocess streams closed for workspace ${uri.fsPath}`); + } + deferredTillExecClose?.resolve(); + }, + }; +} + +/** + * Handles cleanup when test discovery is cancelled. + * Kills the subprocess (if running), resolves the completion deferred, and cancels the discovery pipe. + * + * @param testProvider - The test framework being used ('pytest' or 'unittest') + * @param proc - The process to kill + * @param processCompletion - Deferred to resolve + * @param pipeCancellation - Cancellation token source to cancel + * @param uri - The workspace URI + */ +export function cleanupOnCancellation( + testProvider: TestProvider, + proc: { kill: () => void } | undefined, + processCompletion: Deferred, + pipeCancellation: CancellationTokenSource, + uri: Uri, +): void { + traceInfo(`Test discovery cancelled, killing ${testProvider} subprocess for workspace ${uri.fsPath}`); + if (proc) { + traceVerbose(`Killing ${testProvider} subprocess for workspace ${uri.fsPath}`); + proc.kill(); + } else { + traceVerbose(`No ${testProvider} subprocess to kill for workspace ${uri.fsPath} (proc is undefined)`); + } + traceVerbose(`Resolving process completion deferred for ${testProvider} discovery in workspace ${uri.fsPath}`); + processCompletion.resolve(); + traceVerbose(`Cancelling discovery pipe for ${testProvider} discovery in workspace ${uri.fsPath}`); + pipeCancellation.cancel(); +} diff --git a/src/client/testing/testController/common/externalDependencies.ts b/src/client/testing/testController/common/externalDependencies.ts deleted file mode 100644 index db7bc9448d27..000000000000 --- a/src/client/testing/testController/common/externalDependencies.ts +++ /dev/null @@ -1,20 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -import * as tmp from 'tmp'; -import { TemporaryFile } from '../../../common/platform/types'; - -export function createTemporaryFile(ext = '.tmp'): Promise { - return new Promise((resolve, reject) => { - tmp.file({ postfix: ext }, (err, filename, _fd, cleanUp): void => { - if (err) { - reject(err); - } else { - resolve({ - filePath: filename, - dispose: cleanUp, - }); - } - }); - }); -} diff --git a/src/client/testing/testController/common/projectAdapter.ts b/src/client/testing/testController/common/projectAdapter.ts new file mode 100644 index 000000000000..cfffbf439ca6 --- /dev/null +++ b/src/client/testing/testController/common/projectAdapter.ts @@ -0,0 +1,88 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { TestItem, Uri } from 'vscode'; +import { TestProvider } from '../../types'; +import { ITestDiscoveryAdapter, ITestExecutionAdapter, ITestResultResolver } from './types'; +import { PythonEnvironment, PythonProject } from '../../../envExt/types'; + +/** + * Represents a single Python project with its own test infrastructure. + * A project is defined as a combination of a Python executable + URI (folder/file). + * Projects are uniquely identified by their projectUri (use projectUri.toString() for map keys). + */ +export interface ProjectAdapter { + // === IDENTITY === + /** + * Display name for the project (e.g., "alice (Python 3.11)"). + */ + projectName: string; + + /** + * URI of the project root folder or file. + * This is the unique identifier for the project. + */ + projectUri: Uri; + + /** + * Parent workspace URI containing this project. + */ + workspaceUri: Uri; + + // === API OBJECTS (from vscode-python-environments extension) === + /** + * The PythonProject object from the environment API. + */ + pythonProject: PythonProject; + + /** + * The resolved PythonEnvironment with execution details. + * Contains execInfo.run.executable for running tests. + */ + pythonEnvironment: PythonEnvironment; + + // === TEST INFRASTRUCTURE === + /** + * Test framework provider ('pytest' | 'unittest'). + */ + testProvider: TestProvider; + + /** + * Adapter for test discovery. + */ + discoveryAdapter: ITestDiscoveryAdapter; + + /** + * Adapter for test execution. + */ + executionAdapter: ITestExecutionAdapter; + + /** + * Result resolver for this project (maps test IDs and handles results). + */ + resultResolver: ITestResultResolver; + + /** + * Absolute paths of nested projects to ignore during discovery. + * Used to pass --ignore flags to pytest or exclusion filters to unittest. + * Only populated for parent projects that contain nested child projects. + */ + nestedProjectPathsToIgnore?: string[]; + + // === LIFECYCLE === + /** + * Whether discovery is currently running for this project. + */ + isDiscovering: boolean; + + /** + * Whether tests are currently executing for this project. + */ + isExecuting: boolean; + + /** + * Root TestItem for this project in the VS Code test tree. + * All project tests are children of this item. + */ + projectRootTestItem?: TestItem; +} diff --git a/src/client/testing/testController/common/projectTestExecution.ts b/src/client/testing/testController/common/projectTestExecution.ts new file mode 100644 index 000000000000..fe3b4f91491a --- /dev/null +++ b/src/client/testing/testController/common/projectTestExecution.ts @@ -0,0 +1,296 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { CancellationToken, FileCoverageDetail, TestItem, TestRun, TestRunProfileKind, TestRunRequest } from 'vscode'; +import { traceError, traceInfo, traceVerbose, traceWarn } from '../../../logging'; +import { sendTelemetryEvent } from '../../../telemetry'; +import { EventName } from '../../../telemetry/constants'; +import { IPythonExecutionFactory } from '../../../common/process/types'; +import { ITestDebugLauncher } from '../../common/types'; +import { ProjectAdapter } from './projectAdapter'; +import { TestProjectRegistry } from './testProjectRegistry'; +import { getProjectId } from './projectUtils'; +import { getEnvExtApi, useEnvExtension } from '../../../envExt/api.internal'; +import { isParentPath } from '../../../pythonEnvironments/common/externalDependencies'; + +/** Dependencies for project-based test execution. */ +export interface ProjectExecutionDependencies { + projectRegistry: TestProjectRegistry; + pythonExecFactory: IPythonExecutionFactory; + debugLauncher: ITestDebugLauncher; +} + +/** Executes tests for multiple projects, grouping by project and using each project's Python environment. */ +export async function executeTestsForProjects( + projects: ProjectAdapter[], + testItems: TestItem[], + runInstance: TestRun, + request: TestRunRequest, + token: CancellationToken, + deps: ProjectExecutionDependencies, +): Promise { + if (projects.length === 0) { + traceError(`[test-by-project] No projects provided for execution`); + return; + } + + // Early exit if already cancelled + if (token.isCancellationRequested) { + traceInfo(`[test-by-project] Execution cancelled before starting`); + return; + } + + // Group test items by project + const testsByProject = await groupTestItemsByProject(testItems, projects); + + const isDebugMode = request.profile?.kind === TestRunProfileKind.Debug; + traceInfo(`[test-by-project] Executing tests across ${testsByProject.size} project(s), debug=${isDebugMode}`); + + // Setup coverage once for all projects (single callback that routes by file path) + if (request.profile?.kind === TestRunProfileKind.Coverage) { + setupCoverageForProjects(request, projects); + } + + // Execute tests for each project in parallel + // For debug mode, multiple debug sessions will be launched in parallel + // Each execution respects cancellation via runInstance.token + const executions = Array.from(testsByProject.entries()).map(async ([_projectId, { project, items }]) => { + // Check for cancellation before starting each project + if (token.isCancellationRequested) { + traceInfo(`[test-by-project] Skipping ${project.projectName} - cancellation requested`); + return; + } + + if (items.length === 0) return; + + traceInfo(`[test-by-project] Executing ${items.length} test item(s) for project: ${project.projectName}`); + + sendTelemetryEvent(EventName.UNITTEST_RUN, undefined, { + tool: project.testProvider, + debugging: isDebugMode, + }); + + try { + await executeTestsForProject(project, items, runInstance, request, deps); + } catch (error) { + // Don't log cancellation as an error + if (!token.isCancellationRequested) { + traceError(`[test-by-project] Execution failed for project ${project.projectName}:`, error); + } + } + }); + + await Promise.all(executions); + + if (token.isCancellationRequested) { + traceInfo(`[test-by-project] Project executions cancelled`); + } else { + traceInfo(`[test-by-project] All project executions completed`); + } +} + +/** Lookup context for caching project lookups within a single test run. */ +interface ProjectLookupContext { + uriToAdapter: Map; + projectPathToAdapter: Map; +} + +/** Groups test items by owning project using env API or path-based matching as fallback. */ +export async function groupTestItemsByProject( + testItems: TestItem[], + projects: ProjectAdapter[], +): Promise> { + const result = new Map(); + + // Initialize entries for all projects + for (const project of projects) { + result.set(getProjectId(project.projectUri), { project, items: [] }); + } + + // Build lookup context for this run - O(p) one-time setup, enables O(1) lookups per item. + // When tests are from a single project, most lookups hit the cache after the first item. + const lookupContext: ProjectLookupContext = { + uriToAdapter: new Map(), + projectPathToAdapter: new Map(projects.map((p) => [p.projectUri.fsPath, p])), + }; + + // Assign each test item to its project + for (const item of testItems) { + const project = await findProjectForTestItem(item, projects, lookupContext); + if (project) { + const entry = result.get(getProjectId(project.projectUri)); + if (entry) { + entry.items.push(item); + } + } else { + // If no project matches, log it + traceWarn(`[test-by-project] Could not match test item ${item.id} to a project`); + } + } + + // Remove projects with no test items + for (const [projectId, entry] of result.entries()) { + if (entry.items.length === 0) { + result.delete(projectId); + } + } + + return result; +} + +/** Finds the project that owns a test item. */ +export async function findProjectForTestItem( + item: TestItem, + projects: ProjectAdapter[], + lookupContext?: ProjectLookupContext, +): Promise { + if (!item.uri) return undefined; + + const uriPath = item.uri.fsPath; + + // Check lookup context first - O(1) + if (lookupContext?.uriToAdapter.has(uriPath)) { + return lookupContext.uriToAdapter.get(uriPath); + } + + let result: ProjectAdapter | undefined; + + // Try using the Python Environment extension API first. + // Legacy path: when useEnvExtension() is false, this block is skipped and we go + // directly to findProjectByPath() below (path-based matching). + if (useEnvExtension()) { + try { + const envExtApi = await getEnvExtApi(); + const pythonProject = envExtApi.getPythonProject(item.uri); + if (pythonProject) { + // Use lookup context for O(1) adapter lookup instead of O(p) linear search + result = lookupContext?.projectPathToAdapter.get(pythonProject.uri.fsPath); + if (!result) { + // Fallback to linear search if lookup context not available + result = projects.find((p) => p.projectUri.fsPath === pythonProject.uri.fsPath); + } + } + } catch (error) { + traceVerbose(`[test-by-project] Failed to use env extension API, falling back to path matching: ${error}`); + } + } + + // Fallback: path-based matching when env API unavailable or didn't find a match. + // O(p) time complexity where p = number of projects. + if (!result) { + result = findProjectByPath(item, projects); + } + + // Store result for future lookups of same file within this run - O(1) + if (lookupContext) { + lookupContext.uriToAdapter.set(uriPath, result); + } + + return result; +} + +/** Fallback: finds project using path-based matching. */ +function findProjectByPath(item: TestItem, projects: ProjectAdapter[]): ProjectAdapter | undefined { + if (!item.uri) return undefined; + + const itemPath = item.uri.fsPath; + let bestMatch: ProjectAdapter | undefined; + let bestMatchLength = 0; + + for (const project of projects) { + const projectPath = project.projectUri.fsPath; + // Use isParentPath for safe path-boundary matching (handles separators and case normalization) + if (isParentPath(itemPath, projectPath) && projectPath.length > bestMatchLength) { + bestMatch = project; + bestMatchLength = projectPath.length; + } + } + + return bestMatch; +} + +/** Executes tests for a single project using the project's Python environment. */ +export async function executeTestsForProject( + project: ProjectAdapter, + testItems: TestItem[], + runInstance: TestRun, + request: TestRunRequest, + deps: ProjectExecutionDependencies, +): Promise { + const processedTestItemIds = new Set(); + const uniqueTestCaseIds = new Set(); + + // Mark items as started and collect test IDs (deduplicated to handle overlapping selections) + for (const item of testItems) { + const testCaseNodes = getTestCaseNodesRecursive(item); + for (const node of testCaseNodes) { + if (processedTestItemIds.has(node.id)) { + continue; + } + processedTestItemIds.add(node.id); + runInstance.started(node); + const runId = project.resultResolver.vsIdToRunId.get(node.id); + if (runId) { + uniqueTestCaseIds.add(runId); + } + } + } + + const testCaseIds = Array.from(uniqueTestCaseIds); + + if (testCaseIds.length === 0) { + traceVerbose(`[test-by-project] No test IDs found for project ${project.projectName}`); + return; + } + + traceInfo(`[test-by-project] Running ${testCaseIds.length} test(s) for project: ${project.projectName}`); + + // Execute tests using the project's execution adapter + await project.executionAdapter.runTests( + project.projectUri, + testCaseIds, + request.profile?.kind, + runInstance, + deps.pythonExecFactory, + deps.debugLauncher, + undefined, // interpreter not needed, project has its own environment + project, + ); +} + +/** Recursively gets all leaf test case nodes from a test item tree. */ +export function getTestCaseNodesRecursive(item: TestItem): TestItem[] { + const results: TestItem[] = []; + if (item.children.size === 0) { + // This is a leaf node (test case) + results.push(item); + } else { + // Recursively get children + item.children.forEach((child) => { + results.push(...getTestCaseNodesRecursive(child)); + }); + } + return results; +} + +/** Sets up detailed coverage loading that routes to the correct project by file path. */ +export function setupCoverageForProjects(request: TestRunRequest, projects: ProjectAdapter[]): void { + if (request.profile?.kind === TestRunProfileKind.Coverage) { + // Create a single callback that routes to the correct project's coverage map by file path + request.profile.loadDetailedCoverage = ( + _testRun: TestRun, + fileCoverage, + _token, + ): Thenable => { + const filePath = fileCoverage.uri.fsPath; + // Find the project that has coverage data for this file + for (const project of projects) { + const details = project.resultResolver.detailedCoverageMap.get(filePath); + if (details) { + return Promise.resolve(details); + } + } + return Promise.resolve([]); + }; + } +} diff --git a/src/client/testing/testController/common/projectUtils.ts b/src/client/testing/testController/common/projectUtils.ts new file mode 100644 index 000000000000..b104b7f6842d --- /dev/null +++ b/src/client/testing/testController/common/projectUtils.ts @@ -0,0 +1,91 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { Uri } from 'vscode'; +import { IConfigurationService } from '../../../common/types'; +import { IEnvironmentVariablesProvider } from '../../../common/variables/types'; +import { UNITTEST_PROVIDER } from '../../common/constants'; +import { TestProvider } from '../../types'; +import { ITestDiscoveryAdapter, ITestExecutionAdapter, ITestResultResolver } from './types'; +import { UnittestTestDiscoveryAdapter } from '../unittest/testDiscoveryAdapter'; +import { UnittestTestExecutionAdapter } from '../unittest/testExecutionAdapter'; +import { PytestTestDiscoveryAdapter } from '../pytest/pytestDiscoveryAdapter'; +import { PytestTestExecutionAdapter } from '../pytest/pytestExecutionAdapter'; + +/** + * Separator used to scope test IDs to a specific project. + * Format: {projectId}{SEPARATOR}{testPath} + * Example: "file:///workspace/project@@PROJECT@@test_file.py::test_name" + */ +export const PROJECT_ID_SEPARATOR = '@@vsc@@'; + +/** + * Gets the project ID from a project URI. + * The project ID is simply the string representation of the URI, matching how + * the Python Environments extension stores projects in Map. + * + * @param projectUri The project URI + * @returns The project ID (URI as string) + */ +export function getProjectId(projectUri: Uri): string { + return projectUri.toString(); +} + +/** + * Parses a project-scoped vsId back into its components. + * + * @param vsId The VS Code test item ID to parse + * @returns A tuple of [projectId, runId]. If the ID is not project-scoped, + * returns [undefined, vsId] (legacy format) + */ +export function parseVsId(vsId: string): [string | undefined, string] { + const separatorIndex = vsId.indexOf(PROJECT_ID_SEPARATOR); + if (separatorIndex === -1) { + return [undefined, vsId]; // Legacy ID without project scope + } + return [vsId.substring(0, separatorIndex), vsId.substring(separatorIndex + PROJECT_ID_SEPARATOR.length)]; +} + +/** + * Creates a display name for a project including Python version. + * Format: "{projectName} (Python {version})" + * + * @param projectName The name of the project + * @param pythonVersion The Python version string (e.g., "3.11.2") + * @returns Formatted display name + */ +export function createProjectDisplayName(projectName: string, pythonVersion: string): string { + // Extract major.minor version if full version provided + const versionMatch = pythonVersion.match(/^(\d+\.\d+)/); + const shortVersion = versionMatch ? versionMatch[1] : pythonVersion; + + return `${projectName} (Python ${shortVersion})`; +} + +/** + * Creates test adapters (discovery and execution) for a given test provider. + * + * @param testProvider The test framework provider ('pytest' | 'unittest') + * @param resultResolver The result resolver to use for test results + * @param configSettings The configuration service + * @param envVarsService The environment variables provider + * @returns An object containing the discovery and execution adapters + */ +export function createTestAdapters( + testProvider: TestProvider, + resultResolver: ITestResultResolver, + configSettings: IConfigurationService, + envVarsService: IEnvironmentVariablesProvider, +): { discoveryAdapter: ITestDiscoveryAdapter; executionAdapter: ITestExecutionAdapter } { + if (testProvider === UNITTEST_PROVIDER) { + return { + discoveryAdapter: new UnittestTestDiscoveryAdapter(configSettings, resultResolver, envVarsService), + executionAdapter: new UnittestTestExecutionAdapter(configSettings, resultResolver, envVarsService), + }; + } + + return { + discoveryAdapter: new PytestTestDiscoveryAdapter(configSettings, resultResolver, envVarsService), + executionAdapter: new PytestTestExecutionAdapter(configSettings, resultResolver, envVarsService), + }; +} diff --git a/src/client/testing/testController/common/resultResolver.ts b/src/client/testing/testController/common/resultResolver.ts new file mode 100644 index 000000000000..c126d233de1b --- /dev/null +++ b/src/client/testing/testController/common/resultResolver.ts @@ -0,0 +1,137 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { CancellationToken, TestController, TestItem, Uri, TestRun, FileCoverageDetail } from 'vscode'; +import { CoveragePayload, DiscoveredTestPayload, ExecutionTestPayload, ITestResultResolver } from './types'; +import { TestProvider } from '../../types'; +import { traceInfo } from '../../../logging'; +import { sendTelemetryEvent } from '../../../telemetry'; +import { EventName } from '../../../telemetry/constants'; +import { TestItemIndex } from './testItemIndex'; +import { TestDiscoveryHandler } from './testDiscoveryHandler'; +import { TestExecutionHandler } from './testExecutionHandler'; +import { TestCoverageHandler } from './testCoverageHandler'; + +export class PythonResultResolver implements ITestResultResolver { + testController: TestController; + + testProvider: TestProvider; + + private testItemIndex: TestItemIndex; + + // Shared singleton handlers + private static discoveryHandler: TestDiscoveryHandler = new TestDiscoveryHandler(); + private static executionHandler: TestExecutionHandler = new TestExecutionHandler(); + private static coverageHandler: TestCoverageHandler = new TestCoverageHandler(); + + public detailedCoverageMap = new Map(); + + /** + * Optional project ID for scoping test IDs. + * When set, all test IDs are prefixed with `{projectId}@@vsc@@` for project-based testing. + * When undefined, uses legacy workspace-level IDs for backward compatibility. + */ + private projectId?: string; + + /** + * Optional project display name for labeling the test tree root. + * When set, the root node label will be "project: {projectName}" instead of the folder name. + */ + private projectName?: string; + + constructor( + testController: TestController, + testProvider: TestProvider, + private workspaceUri: Uri, + projectId?: string, + projectName?: string, + ) { + this.testController = testController; + this.testProvider = testProvider; + this.projectId = projectId; + this.projectName = projectName; + // Initialize a new TestItemIndex which will be used to track test items in this workspace/project + this.testItemIndex = new TestItemIndex(); + } + + // Expose for backward compatibility (WorkspaceTestAdapter accesses these) + public get runIdToTestItem(): Map { + return this.testItemIndex.runIdToTestItemMap; + } + + public get runIdToVSid(): Map { + return this.testItemIndex.runIdToVSidMap; + } + + public get vsIdToRunId(): Map { + return this.testItemIndex.vsIdToRunIdMap; + } + + /** + * Gets the project ID for this resolver (if any). + * Used for project-scoped test ID generation. + */ + public getProjectId(): string | undefined { + return this.projectId; + } + + public resolveDiscovery(payload: DiscoveredTestPayload, token?: CancellationToken): void { + PythonResultResolver.discoveryHandler.processDiscovery( + payload, + this.testController, + this.testItemIndex, + this.workspaceUri, + this.testProvider, + token, + this.projectId, + this.projectName, + ); + sendTelemetryEvent(EventName.UNITTEST_DISCOVERY_DONE, undefined, { + tool: this.testProvider, + failed: false, + }); + } + + public _resolveDiscovery(payload: DiscoveredTestPayload, token?: CancellationToken): void { + // Delegate to the public method for backward compatibility + this.resolveDiscovery(payload, token); + } + + public resolveExecution(payload: ExecutionTestPayload | CoveragePayload, runInstance: TestRun): void { + if ('coverage' in payload) { + // coverage data is sent once per connection + traceInfo('Coverage data received, processing...'); + this.detailedCoverageMap = PythonResultResolver.coverageHandler.processCoverage( + payload as CoveragePayload, + runInstance, + ); + traceInfo('Coverage data processing complete.'); + } else { + PythonResultResolver.executionHandler.processExecution( + payload as ExecutionTestPayload, + runInstance, + this.testItemIndex, + this.testController, + ); + } + } + + public _resolveExecution(payload: ExecutionTestPayload, runInstance: TestRun): void { + // Delegate to the public method for backward compatibility + this.resolveExecution(payload, runInstance); + } + + public _resolveCoverage(payload: CoveragePayload, runInstance: TestRun): void { + // Delegate to the public method for backward compatibility + this.resolveExecution(payload, runInstance); + } + + /** + * Clean up stale test item references from the cache maps. + * Validates cached items and removes any that are no longer in the test tree. + * Delegates to TestItemIndex. + */ + public cleanupStaleReferences(): void { + this.testItemIndex.cleanupStaleReferences(this.testController); + } +} diff --git a/src/client/testing/testController/common/resultsHelper.ts b/src/client/testing/testController/common/resultsHelper.ts deleted file mode 100644 index 2fce78919766..000000000000 --- a/src/client/testing/testController/common/resultsHelper.ts +++ /dev/null @@ -1,174 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -import * as fsapi from 'fs-extra'; -import { Location, TestItem, TestMessage, TestRun } from 'vscode'; -import { getRunIdFromRawData, getTestCaseNodes } from './testItemUtilities'; -import { TestData } from './types'; -import { fixLogLines } from './utils'; - -type TestSuiteResult = { - $: { - errors: string; - failures: string; - name: string; - skips: string; - skip: string; - tests: string; - time: string; - }; - testcase: TestCaseResult[]; -}; -type TestCaseResult = { - $: { - classname: string; - file: string; - line: string; - name: string; - time: string; - }; - failure: { - _: string; - $: { message: string; type: string }; - }[]; - error: { - _: string; - $: { message: string; type: string }; - }[]; - skipped: { - _: string; - $: { message: string; type: string }; - }[]; -}; - -async function parseXML(data: string): Promise { - const xml2js = await import('xml2js'); - - return new Promise((resolve, reject) => { - xml2js.parseString(data, (error: Error, result: unknown) => { - if (error) { - return reject(error); - } - return resolve(result); - }); - }); -} - -function getJunitResults(parserResult: unknown): TestSuiteResult | undefined { - // This is the newer JUnit XML format (e.g. pytest 5.1 and later). - const fullResults = parserResult as { testsuites: { testsuite: TestSuiteResult[] } }; - if (!fullResults.testsuites) { - return (parserResult as { testsuite: TestSuiteResult }).testsuite; - } - - const junitSuites = fullResults.testsuites.testsuite; - if (!Array.isArray(junitSuites)) { - throw Error('bad JUnit XML data'); - } - if (junitSuites.length === 0) { - return undefined; - } - if (junitSuites.length > 1) { - throw Error('got multiple XML results'); - } - return junitSuites[0]; -} - -export async function updateResultFromJunitXml( - outputXmlFile: string, - testNode: TestItem, - runInstance: TestRun, - idToRawData: Map, -): Promise { - const data = await fsapi.readFile(outputXmlFile); - const parserResult = await parseXML(data.toString('utf8')); - const junitSuite = getJunitResults(parserResult); - const testCaseNodes = getTestCaseNodes(testNode); - - if (junitSuite && junitSuite.testcase.length > 0 && testCaseNodes.length > 0) { - let failures = 0; - let skipped = 0; - let errors = 0; - let passed = 0; - - testCaseNodes.forEach((node) => { - const rawTestCaseNode = idToRawData.get(node.id); - if (!rawTestCaseNode) { - return; - } - - const result = junitSuite.testcase.find((t) => { - const idResult = getRunIdFromRawData(`${t.$.classname}::${t.$.name}`); - const idNode = rawTestCaseNode.runId; - return idResult === idNode || idNode.endsWith(idResult); - }); - if (result) { - if (result.error) { - errors += 1; - const error = result.error[0]; - const text = `${rawTestCaseNode.rawId} Failed with Error: [${error.$.type}]${error.$.message}\r\n${error._}\r\n\r\n`; - const message = new TestMessage(text); - - if (node.uri && node.range) { - message.location = new Location(node.uri, node.range); - } - - runInstance.errored(node, message); - runInstance.appendOutput(fixLogLines(text)); - } else if (result.failure) { - failures += 1; - const failure = result.failure[0]; - const text = `${rawTestCaseNode.rawId} Failed: [${failure.$.type}]${failure.$.message}\r\n${failure._}\r\n`; - const message = new TestMessage(text); - - if (node.uri && node.range) { - message.location = new Location(node.uri, node.range); - } - - runInstance.failed(node, message); - runInstance.appendOutput(fixLogLines(text)); - } else if (result.skipped) { - const skip = result.skipped[0]; - let text = ''; - if (skip.$.type === 'pytest.xfail') { - passed += 1; - // pytest.xfail ==> expected failure via @unittest.expectedFailure - text = `${rawTestCaseNode.rawId} Passed: [${skip.$.type}]${skip.$.message}\r\n`; - runInstance.passed(node); - } else { - skipped += 1; - text = `${rawTestCaseNode.rawId} Skipped: [${skip.$.type}]${skip.$.message}\r\n`; - runInstance.skipped(node); - } - runInstance.appendOutput(fixLogLines(text)); - } else { - passed += 1; - const text = `${rawTestCaseNode.rawId} Passed\r\n`; - runInstance.passed(node); - runInstance.appendOutput(fixLogLines(text)); - } - } else { - const text = `Test result not found for: ${rawTestCaseNode.rawId}\r\n`; - runInstance.appendOutput(fixLogLines(text)); - const message = new TestMessage(text); - - if (node.uri && node.range) { - message.location = new Location(node.uri, node.range); - } - runInstance.errored(node, message); - } - }); - - runInstance.appendOutput(`Total number of tests expected to run: ${testCaseNodes.length}\r\n`); - runInstance.appendOutput(`Total number of tests run: ${passed + failures + errors + skipped}\r\n`); - runInstance.appendOutput(`Total number of tests passed: ${passed}\r\n`); - runInstance.appendOutput(`Total number of tests failed: ${failures}\r\n`); - runInstance.appendOutput(`Total number of tests failed with errors: ${errors}\r\n`); - runInstance.appendOutput(`Total number of tests skipped: ${skipped}\r\n`); - runInstance.appendOutput( - `Total number of tests with no result data: ${ - testCaseNodes.length - passed - failures - errors - skipped - }\r\n`, - ); - } -} diff --git a/src/client/testing/testController/common/server.ts b/src/client/testing/testController/common/server.ts deleted file mode 100644 index 6849f0f8969a..000000000000 --- a/src/client/testing/testController/common/server.ts +++ /dev/null @@ -1,161 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -import * as net from 'net'; -import * as crypto from 'crypto'; -import { Disposable, Event, EventEmitter } from 'vscode'; -import { - ExecutionFactoryCreateWithEnvironmentOptions, - IPythonExecutionFactory, - SpawnOptions, -} from '../../../common/process/types'; -import { traceLog } from '../../../logging'; -import { DataReceivedEvent, ITestServer, TestCommandOptions } from './types'; -import { ITestDebugLauncher, LaunchOptions } from '../../common/types'; -import { UNITTEST_PROVIDER } from '../../common/constants'; -import { jsonRPCHeaders, jsonRPCContent, JSONRPC_UUID_HEADER } from './utils'; - -export class PythonTestServer implements ITestServer, Disposable { - private _onDataReceived: EventEmitter = new EventEmitter(); - - private uuids: Array = []; - - private server: net.Server; - - private ready: Promise; - - constructor(private executionFactory: IPythonExecutionFactory, private debugLauncher: ITestDebugLauncher) { - this.server = net.createServer((socket: net.Socket) => { - socket.on('data', (data: Buffer) => { - try { - let rawData: string = data.toString(); - - while (rawData.length > 0) { - const rpcHeaders = jsonRPCHeaders(rawData); - const uuid = rpcHeaders.headers.get(JSONRPC_UUID_HEADER); - rawData = rpcHeaders.remainingRawData; - if (uuid && this.uuids.includes(uuid)) { - const rpcContent = jsonRPCContent(rpcHeaders.headers, rawData); - rawData = rpcContent.remainingRawData; - this._onDataReceived.fire({ uuid, data: rpcContent.extractedJSON }); - this.uuids = this.uuids.filter((u) => u !== uuid); - } else { - traceLog(`Error processing test server request: uuid not found`); - this._onDataReceived.fire({ uuid: '', data: '' }); - return; - } - } - } catch (ex) { - traceLog(`Error processing test server request: ${ex} observe`); - this._onDataReceived.fire({ uuid: '', data: '' }); - } - }); - }); - this.ready = new Promise((resolve, _reject) => { - this.server.listen(undefined, 'localhost', () => { - resolve(); - }); - }); - this.server.on('error', (ex) => { - traceLog(`Error starting test server: ${ex}`); - }); - this.server.on('close', () => { - traceLog('Test server closed.'); - }); - this.server.on('listening', () => { - traceLog('Test server listening.'); - }); - this.server.on('connection', () => { - traceLog('Test server connected to a client.'); - }); - } - - public serverReady(): Promise { - return this.ready; - } - - public getPort(): number { - return (this.server.address() as net.AddressInfo).port; - } - - public createUUID(): string { - const uuid = crypto.randomUUID(); - this.uuids.push(uuid); - return uuid; - } - - public dispose(): void { - this.server.close(); - this._onDataReceived.dispose(); - } - - public get onDataReceived(): Event { - return this._onDataReceived.event; - } - - async sendCommand(options: TestCommandOptions): Promise { - const { uuid } = options; - const spawnOptions: SpawnOptions = { - token: options.token, - cwd: options.cwd, - throwOnStdErr: true, - outputChannel: options.outChannel, - }; - - // Create the Python environment in which to execute the command. - const creationOptions: ExecutionFactoryCreateWithEnvironmentOptions = { - allowEnvironmentFetchExceptions: false, - resource: options.workspaceFolder, - }; - const execService = await this.executionFactory.createActivatedEnvironment(creationOptions); - - // Add the generated UUID to the data to be sent (expecting to receive it back). - // first check if we have testIds passed in (in case of execution) and - // insert appropriate flag and test id array - let args = []; - if (options.testIds) { - args = [ - options.command.script, - '--port', - this.getPort().toString(), - '--uuid', - uuid, - '--testids', - ...options.testIds, - ].concat(options.command.args); - } else { - // if not case of execution, go with the normal args - args = [options.command.script, '--port', this.getPort().toString(), '--uuid', uuid].concat( - options.command.args, - ); - } - - if (options.outChannel) { - options.outChannel.appendLine(`python ${args.join(' ')}`); - } - - try { - if (options.debugBool) { - const launchOptions: LaunchOptions = { - cwd: options.cwd, - args, - token: options.token, - testProvider: UNITTEST_PROVIDER, - }; - - await this.debugLauncher!.launchDebugger(launchOptions); - } else { - await execService.exec(args, spawnOptions); - } - } catch (ex) { - this.uuids = this.uuids.filter((u) => u !== uuid); - this._onDataReceived.fire({ - uuid, - data: JSON.stringify({ - status: 'error', - errors: [(ex as Error).message], - }), - }); - } - } -} diff --git a/src/client/testing/testController/common/testCoverageHandler.ts b/src/client/testing/testController/common/testCoverageHandler.ts new file mode 100644 index 000000000000..81ec80579730 --- /dev/null +++ b/src/client/testing/testController/common/testCoverageHandler.ts @@ -0,0 +1,93 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { TestRun, Uri, TestCoverageCount, FileCoverage, FileCoverageDetail, StatementCoverage, Range } from 'vscode'; +import { CoveragePayload, FileCoverageMetrics } from './types'; + +/** + * Stateless handler for processing coverage payloads and creating coverage objects. + * This handler is shared across all workspaces and contains no instance state. + */ +export class TestCoverageHandler { + /** + * Process coverage payload + * Pure function - returns coverage data without storing it + */ + public processCoverage(payload: CoveragePayload, runInstance: TestRun): Map { + const detailedCoverageMap = new Map(); + + if (payload.result === undefined) { + return detailedCoverageMap; + } + + for (const [key, value] of Object.entries(payload.result)) { + const fileNameStr = key; + const fileCoverageMetrics: FileCoverageMetrics = value; + + // Create FileCoverage object and add to run instance + const fileCoverage = this.createFileCoverage(Uri.file(fileNameStr), fileCoverageMetrics); + runInstance.addCoverage(fileCoverage); + + // Create detailed coverage array for this file + const detailedCoverage = this.createDetailedCoverage( + fileCoverageMetrics.lines_covered ?? [], + fileCoverageMetrics.lines_missed ?? [], + ); + detailedCoverageMap.set(Uri.file(fileNameStr).fsPath, detailedCoverage); + } + + return detailedCoverageMap; + } + + /** + * Create FileCoverage object from metrics + */ + private createFileCoverage(uri: Uri, metrics: FileCoverageMetrics): FileCoverage { + const linesCovered = metrics.lines_covered ?? []; + const linesMissed = metrics.lines_missed ?? []; + const executedBranches = metrics.executed_branches; + const totalBranches = metrics.total_branches; + + const lineCoverageCount = new TestCoverageCount(linesCovered.length, linesCovered.length + linesMissed.length); + + if (totalBranches === -1) { + // branch coverage was not enabled and should not be displayed + return new FileCoverage(uri, lineCoverageCount); + } else { + const branchCoverageCount = new TestCoverageCount(executedBranches, totalBranches); + return new FileCoverage(uri, lineCoverageCount, branchCoverageCount); + } + } + + /** + * Create detailed coverage array for a file + * Only line coverage on detailed, not branch coverage + */ + private createDetailedCoverage(linesCovered: number[], linesMissed: number[]): FileCoverageDetail[] { + const detailedCoverageArray: FileCoverageDetail[] = []; + + // Add covered lines + for (const line of linesCovered) { + // line is 1-indexed, so we need to subtract 1 to get the 0-indexed line number + // true value means line is covered + const statementCoverage = new StatementCoverage( + true, + new Range(line - 1, 0, line - 1, Number.MAX_SAFE_INTEGER), + ); + detailedCoverageArray.push(statementCoverage); + } + + // Add missed lines + for (const line of linesMissed) { + // line is 1-indexed, so we need to subtract 1 to get the 0-indexed line number + // false value means line is NOT covered + const statementCoverage = new StatementCoverage( + false, + new Range(line - 1, 0, line - 1, Number.MAX_SAFE_INTEGER), + ); + detailedCoverageArray.push(statementCoverage); + } + + return detailedCoverageArray; + } +} diff --git a/src/client/testing/testController/common/testDiscoveryHandler.ts b/src/client/testing/testController/common/testDiscoveryHandler.ts new file mode 100644 index 000000000000..3f70e6b68594 --- /dev/null +++ b/src/client/testing/testController/common/testDiscoveryHandler.ts @@ -0,0 +1,141 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { CancellationToken, TestController, Uri, MarkdownString } from 'vscode'; +import * as util from 'util'; +import { DiscoveredTestPayload } from './types'; +import { TestProvider } from '../../types'; +import { traceError, traceWarn } from '../../../logging'; +import { Testing } from '../../../common/utils/localize'; +import { createErrorTestItem } from './testItemUtilities'; +import { buildErrorNodeOptions, populateTestTree } from './utils'; +import { TestItemIndex } from './testItemIndex'; +import { PROJECT_ID_SEPARATOR } from './projectUtils'; + +/** + * Stateless handler for processing discovery payloads and building/updating the TestItem tree. + * This handler is shared across all workspaces and contains no instance state. + */ +export class TestDiscoveryHandler { + /** + * Process discovery payload and update test tree + * Pure function - no instance state used + */ + public processDiscovery( + payload: DiscoveredTestPayload, + testController: TestController, + testItemIndex: TestItemIndex, + workspaceUri: Uri, + testProvider: TestProvider, + token?: CancellationToken, + projectId?: string, + projectName?: string, + ): void { + if (!payload) { + // No test data is available + return; + } + + const workspacePath = workspaceUri.fsPath; + const rawTestData = payload as DiscoveredTestPayload; + + // Check if there were any errors in the discovery process. + if (rawTestData.status === 'error') { + this.createErrorNode(testController, workspaceUri, rawTestData.error, testProvider, projectId, projectName); + } else { + // remove error node only if no errors exist. + const errorNodeId = projectId + ? `${projectId}${PROJECT_ID_SEPARATOR}DiscoveryError:${workspacePath}` + : `DiscoveryError:${workspacePath}`; + testController.items.delete(errorNodeId); + } + + if (rawTestData.tests || rawTestData.tests === null) { + // if any tests exist, they should be populated in the test tree, regardless of whether there were errors or not. + // parse and insert test data. + + // Clear existing mappings before rebuilding test tree + testItemIndex.clear(); + + // If the test root for this folder exists: Workspace refresh, update its children. + // Otherwise, it is a freshly discovered workspace, and we need to create a new test root and populate the test tree. + // Note: populateTestTree will call testItemIndex.registerTestItem() for each discovered test + populateTestTree( + testController, + rawTestData.tests, + undefined, + { + runIdToTestItem: testItemIndex.runIdToTestItemMap, + runIdToVSid: testItemIndex.runIdToVSidMap, + vsIdToRunId: testItemIndex.vsIdToRunIdMap, + }, + token, + projectId, + projectName, + ); + } + } + + /** + * Create an error node for discovery failures + */ + public createErrorNode( + testController: TestController, + workspaceUri: Uri, + error: string[] | undefined, + testProvider: TestProvider, + projectId?: string, + projectName?: string, + ): void { + const workspacePath = workspaceUri.fsPath; + const testingErrorConst = + testProvider === 'pytest' ? Testing.errorPytestDiscovery : Testing.errorUnittestDiscovery; + + traceError(testingErrorConst, 'for workspace: ', workspacePath, '\r\n', error?.join('\r\n\r\n') ?? ''); + + // For unittest in project-based mode, check if the error might be caused by nested project imports + // This helps users understand that import errors from nested projects can be safely ignored + // if those tests are covered by a different project with the correct environment. + if (testProvider === 'unittest' && projectId) { + const errorText = error?.join(' ') ?? ''; + const isImportError = + errorText.includes('ModuleNotFoundError') || + errorText.includes('ImportError') || + errorText.includes('No module named'); + + if (isImportError) { + const warningMessage = + '--- ' + + `[test-by-project] Import error during unittest discovery for project at ${workspacePath}. ` + + 'This may be caused by test files in nested project directories that require different dependencies. ' + + 'If these tests are discovered successfully by their own project (with the correct Python environment), ' + + 'this error can be safely ignored. To avoid this, consider excluding nested project paths from parent project discovery. ' + + '---'; + traceWarn(warningMessage); + } + } + + const errorNodeId = projectId + ? `${projectId}${PROJECT_ID_SEPARATOR}DiscoveryError:${workspacePath}` + : `DiscoveryError:${workspacePath}`; + let errorNode = testController.items.get(errorNodeId); + const message = util.format( + `${testingErrorConst} ${Testing.seePythonOutput}\r\n`, + error?.join('\r\n\r\n') ?? '', + ); + + if (errorNode === undefined) { + const options = buildErrorNodeOptions(workspaceUri, message, testProvider, projectName); + // Update the error node ID to include project scope if applicable + options.id = errorNodeId; + errorNode = createErrorTestItem(testController, options); + testController.items.add(errorNode); + } + + const errorNodeLabel: MarkdownString = new MarkdownString( + `[Show output](command:python.viewOutput) to view error logs`, + ); + errorNodeLabel.isTrusted = true; + errorNode.error = errorNodeLabel; + } +} diff --git a/src/client/testing/testController/common/testExecutionHandler.ts b/src/client/testing/testController/common/testExecutionHandler.ts new file mode 100644 index 000000000000..127e6980ae46 --- /dev/null +++ b/src/client/testing/testController/common/testExecutionHandler.ts @@ -0,0 +1,231 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { TestController, TestRun, TestMessage, Location } from 'vscode'; +import { ExecutionTestPayload } from './types'; +import { TestItemIndex } from './testItemIndex'; +import { splitLines } from '../../../common/stringUtils'; +import { splitTestNameWithRegex } from './utils'; +import { clearAllChildren } from './testItemUtilities'; + +/** + * Stateless handler for processing execution payloads and updating TestRun instances. + * This handler is shared across all workspaces and contains no instance state. + */ +export class TestExecutionHandler { + /** + * Process execution payload and update test run + * Pure function - no instance state used + */ + public processExecution( + payload: ExecutionTestPayload, + runInstance: TestRun, + testItemIndex: TestItemIndex, + testController: TestController, + ): void { + const rawTestExecData = payload as ExecutionTestPayload; + + if (rawTestExecData !== undefined && rawTestExecData.result !== undefined) { + for (const keyTemp of Object.keys(rawTestExecData.result)) { + const testItem = rawTestExecData.result[keyTemp]; + + // Delegate to specific outcome handlers + this.handleTestOutcome(keyTemp, testItem, runInstance, testItemIndex, testController); + } + } + } + + /** + * Handle a single test result based on outcome + */ + private handleTestOutcome( + runId: string, + testItem: any, + runInstance: TestRun, + testItemIndex: TestItemIndex, + testController: TestController, + ): void { + if (testItem.outcome === 'error') { + this.handleTestError(runId, testItem, runInstance, testItemIndex, testController); + } else if (testItem.outcome === 'failure' || testItem.outcome === 'passed-unexpected') { + this.handleTestFailure(runId, testItem, runInstance, testItemIndex, testController); + } else if (testItem.outcome === 'success' || testItem.outcome === 'expected-failure') { + this.handleTestSuccess(runId, runInstance, testItemIndex, testController); + } else if (testItem.outcome === 'skipped') { + this.handleTestSkipped(runId, runInstance, testItemIndex, testController); + } else if (testItem.outcome === 'subtest-failure') { + this.handleSubtestFailure(runId, testItem, runInstance, testItemIndex, testController); + } else if (testItem.outcome === 'subtest-success') { + this.handleSubtestSuccess(runId, runInstance, testItemIndex, testController); + } + } + + /** + * Handle test items that errored during execution + */ + private handleTestError( + runId: string, + testItem: any, + runInstance: TestRun, + testItemIndex: TestItemIndex, + testController: TestController, + ): void { + const rawTraceback = testItem.traceback ?? ''; + const traceback = splitLines(rawTraceback, { + trim: false, + removeEmptyEntries: true, + }).join('\r\n'); + const text = `${testItem.test} failed with error: ${testItem.message ?? testItem.outcome}\r\n${traceback}`; + const message = new TestMessage(text); + + const foundItem = testItemIndex.getTestItem(runId, testController); + + if (foundItem?.uri) { + if (foundItem.range) { + message.location = new Location(foundItem.uri, foundItem.range); + } + runInstance.errored(foundItem, message); + } + } + + /** + * Handle test items that failed during execution + */ + private handleTestFailure( + runId: string, + testItem: any, + runInstance: TestRun, + testItemIndex: TestItemIndex, + testController: TestController, + ): void { + const rawTraceback = testItem.traceback ?? ''; + const traceback = splitLines(rawTraceback, { + trim: false, + removeEmptyEntries: true, + }).join('\r\n'); + + const text = `${testItem.test} failed: ${testItem.message ?? testItem.outcome}\r\n${traceback}`; + const message = new TestMessage(text); + + const foundItem = testItemIndex.getTestItem(runId, testController); + + if (foundItem?.uri) { + if (foundItem.range) { + message.location = new Location(foundItem.uri, foundItem.range); + } + runInstance.failed(foundItem, message); + } + } + + /** + * Handle test items that passed during execution + */ + private handleTestSuccess( + runId: string, + runInstance: TestRun, + testItemIndex: TestItemIndex, + testController: TestController, + ): void { + const foundItem = testItemIndex.getTestItem(runId, testController); + + if (foundItem !== undefined && foundItem.uri) { + runInstance.passed(foundItem); + } + } + + /** + * Handle test items that were skipped during execution + */ + private handleTestSkipped( + runId: string, + runInstance: TestRun, + testItemIndex: TestItemIndex, + testController: TestController, + ): void { + const foundItem = testItemIndex.getTestItem(runId, testController); + + if (foundItem !== undefined && foundItem.uri) { + runInstance.skipped(foundItem); + } + } + + /** + * Handle subtest failures + */ + private handleSubtestFailure( + runId: string, + testItem: any, + runInstance: TestRun, + testItemIndex: TestItemIndex, + testController: TestController, + ): void { + const [parentTestCaseId, subtestId] = splitTestNameWithRegex(runId); + const parentTestItem = testItemIndex.getTestItem(parentTestCaseId, testController); + + if (parentTestItem) { + const stats = testItemIndex.getSubtestStats(parentTestCaseId); + if (stats) { + stats.failed += 1; + } else { + testItemIndex.setSubtestStats(parentTestCaseId, { + failed: 1, + passed: 0, + }); + clearAllChildren(parentTestItem); + } + + const subTestItem = testController?.createTestItem(subtestId, subtestId, parentTestItem.uri); + + if (subTestItem) { + const traceback = testItem.traceback ?? ''; + const text = `${testItem.subtest} failed: ${testItem.message ?? testItem.outcome}\r\n${traceback}`; + parentTestItem.children.add(subTestItem); + runInstance.started(subTestItem); + const message = new TestMessage(text); + if (parentTestItem.uri && parentTestItem.range) { + message.location = new Location(parentTestItem.uri, parentTestItem.range); + } + runInstance.failed(subTestItem, message); + } else { + throw new Error('Unable to create new child node for subtest'); + } + } else { + throw new Error('Parent test item not found'); + } + } + + /** + * Handle subtest successes + */ + private handleSubtestSuccess( + runId: string, + runInstance: TestRun, + testItemIndex: TestItemIndex, + testController: TestController, + ): void { + const [parentTestCaseId, subtestId] = splitTestNameWithRegex(runId); + const parentTestItem = testItemIndex.getTestItem(parentTestCaseId, testController); + + if (parentTestItem) { + const stats = testItemIndex.getSubtestStats(parentTestCaseId); + if (stats) { + stats.passed += 1; + } else { + testItemIndex.setSubtestStats(parentTestCaseId, { failed: 0, passed: 1 }); + clearAllChildren(parentTestItem); + } + + const subTestItem = testController?.createTestItem(subtestId, subtestId, parentTestItem.uri); + + if (subTestItem) { + parentTestItem.children.add(subTestItem); + runInstance.started(subTestItem); + runInstance.passed(subTestItem); + } else { + throw new Error('Unable to create new child node for subtest'); + } + } else { + throw new Error('Parent test item not found'); + } + } +} diff --git a/src/client/testing/testController/common/testItemIndex.ts b/src/client/testing/testController/common/testItemIndex.ts new file mode 100644 index 000000000000..448903eae7d5 --- /dev/null +++ b/src/client/testing/testController/common/testItemIndex.ts @@ -0,0 +1,225 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { TestController, TestItem } from 'vscode'; +import { traceError, traceVerbose } from '../../../logging'; +import { getTestCaseNodes } from './testItemUtilities'; + +export interface SubtestStats { + passed: number; + failed: number; +} + +/** + * Maintains persistent ID mappings between Python test IDs and VS Code TestItems. + * This is a stateful component that bridges discovery and execution phases. + * + * Lifecycle: + * - Created: When PythonResultResolver is instantiated (during workspace activation) + * - Populated: During discovery - each discovered test registers its mappings + * - Queried: During execution - to look up TestItems by Python run ID + * - Cleared: When discovery runs again (fresh start) or workspace is disposed + * - Cleaned: Periodically to remove stale references to deleted tests + */ +export class TestItemIndex { + // THE STATE - these maps persist across discovery and execution + private runIdToTestItem: Map; + private runIdToVSid: Map; + private vsIdToRunId: Map; + private subtestStatsMap: Map; + + constructor() { + this.runIdToTestItem = new Map(); + this.runIdToVSid = new Map(); + this.vsIdToRunId = new Map(); + this.subtestStatsMap = new Map(); + } + + /** + * Register a test item with its Python run ID and VS Code ID + * Called during DISCOVERY to populate the index + */ + public registerTestItem(runId: string, vsId: string, testItem: TestItem): void { + this.runIdToTestItem.set(runId, testItem); + this.runIdToVSid.set(runId, vsId); + this.vsIdToRunId.set(vsId, runId); + } + + /** + * Get TestItem by Python run ID (with validation and fallback strategies) + * Called during EXECUTION to look up tests + * + * Uses a three-tier approach: + * 1. Direct O(1) lookup in runIdToTestItem map + * 2. If stale, try vsId mapping and search by VS Code ID + * 3. Last resort: full tree search + */ + public getTestItem(runId: string, testController: TestController): TestItem | undefined { + // Try direct O(1) lookup first + const directItem = this.runIdToTestItem.get(runId); + if (directItem) { + // Validate the item is still in the test tree + if (this.isTestItemValid(directItem, testController)) { + return directItem; + } else { + // Clean up stale reference + this.runIdToTestItem.delete(runId); + } + } + + // Try vsId mapping as fallback + const vsId = this.runIdToVSid.get(runId); + if (vsId) { + // Search by VS Code ID in the controller + let foundItem: TestItem | undefined; + testController.items.forEach((item) => { + if (item.id === vsId) { + foundItem = item; + return; + } + if (!foundItem) { + item.children.forEach((child) => { + if (child.id === vsId) { + foundItem = child; + } + }); + } + }); + + if (foundItem) { + // Cache for future lookups + this.runIdToTestItem.set(runId, foundItem); + return foundItem; + } else { + // Clean up stale mapping + this.runIdToVSid.delete(runId); + this.vsIdToRunId.delete(vsId); + } + } + + // Last resort: full tree search + traceError(`Falling back to tree search for test: ${runId}`); + const testCases = this.collectAllTestCases(testController); + return testCases.find((item) => item.id === vsId); + } + + /** + * Get Python run ID from VS Code ID + * Called by WorkspaceTestAdapter.executeTests() to convert selected tests to Python IDs + */ + public getRunId(vsId: string): string | undefined { + return this.vsIdToRunId.get(vsId); + } + + /** + * Get VS Code ID from Python run ID + */ + public getVSId(runId: string): string | undefined { + return this.runIdToVSid.get(runId); + } + + /** + * Check if a TestItem reference is still valid in the tree + * + * Time Complexity: O(depth) where depth is the maximum nesting level of the test tree. + * In most cases this is O(1) to O(3) since test trees are typically shallow. + */ + public isTestItemValid(testItem: TestItem, testController: TestController): boolean { + // Simple validation: check if the item's parent chain leads back to the controller + let current: TestItem | undefined = testItem; + while (current?.parent) { + current = current.parent; + } + + // If we reached a root item, check if it's in the controller + if (current) { + return testController.items.get(current.id) === current; + } + + // If no parent chain, check if it's directly in the controller + return testController.items.get(testItem.id) === testItem; + } + + /** + * Get subtest statistics for a parent test case + * Returns undefined if no stats exist yet for this parent + */ + public getSubtestStats(parentId: string): SubtestStats | undefined { + return this.subtestStatsMap.get(parentId); + } + + /** + * Set subtest statistics for a parent test case + */ + public setSubtestStats(parentId: string, stats: SubtestStats): void { + this.subtestStatsMap.set(parentId, stats); + } + + /** + * Remove all mappings + * Called at the start of discovery to ensure clean state + */ + public clear(): void { + this.runIdToTestItem.clear(); + this.runIdToVSid.clear(); + this.vsIdToRunId.clear(); + this.subtestStatsMap.clear(); + } + + /** + * Clean up stale references that no longer exist in the test tree + * Called after test tree modifications + */ + public cleanupStaleReferences(testController: TestController): void { + const staleRunIds: string[] = []; + + // Check all runId->TestItem mappings + this.runIdToTestItem.forEach((testItem, runId) => { + if (!this.isTestItemValid(testItem, testController)) { + staleRunIds.push(runId); + } + }); + + // Remove stale entries + staleRunIds.forEach((runId) => { + const vsId = this.runIdToVSid.get(runId); + this.runIdToTestItem.delete(runId); + this.runIdToVSid.delete(runId); + if (vsId) { + this.vsIdToRunId.delete(vsId); + } + }); + + if (staleRunIds.length > 0) { + traceVerbose(`Cleaned up ${staleRunIds.length} stale test item references`); + } + } + + /** + * Collect all test case items from the test controller tree. + * Note: This performs full tree traversal - use cached lookups when possible. + */ + private collectAllTestCases(testController: TestController): TestItem[] { + const testCases: TestItem[] = []; + + testController.items.forEach((i) => { + const tempArr: TestItem[] = getTestCaseNodes(i); + testCases.push(...tempArr); + }); + + return testCases; + } + + // Expose maps for backward compatibility (read-only access) + public get runIdToTestItemMap(): Map { + return this.runIdToTestItem; + } + + public get runIdToVSidMap(): Map { + return this.runIdToVSid; + } + + public get vsIdToRunIdMap(): Map { + return this.vsIdToRunId; + } +} diff --git a/src/client/testing/testController/common/testItemUtilities.ts b/src/client/testing/testController/common/testItemUtilities.ts index 8b8b59051ec4..43624bba2527 100644 --- a/src/client/testing/testController/common/testItemUtilities.ts +++ b/src/client/testing/testController/common/testItemUtilities.ts @@ -498,13 +498,6 @@ export async function updateTestItemFromRawData( item.busy = false; } -export function getUri(node: TestItem): Uri | undefined { - if (!node.uri && node.parent) { - return getUri(node.parent); - } - return node.uri; -} - export function getTestCaseNodes(testNode: TestItem, collection: TestItem[] = []): TestItem[] { if (!testNode.canResolveChildren && testNode.tags.length > 0) { collection.push(testNode); diff --git a/src/client/testing/testController/common/testProjectRegistry.ts b/src/client/testing/testController/common/testProjectRegistry.ts new file mode 100644 index 000000000000..4f0702ad584c --- /dev/null +++ b/src/client/testing/testController/common/testProjectRegistry.ts @@ -0,0 +1,330 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import * as path from 'path'; +import { TestController, Uri } from 'vscode'; +import { isParentPath } from '../../../common/platform/fs-paths'; +import { IConfigurationService } from '../../../common/types'; +import { IInterpreterService } from '../../../interpreter/contracts'; +import { traceError, traceInfo } from '../../../logging'; +import { UNITTEST_PROVIDER } from '../../common/constants'; +import { TestProvider } from '../../types'; +import { IEnvironmentVariablesProvider } from '../../../common/variables/types'; +import { PythonProject, PythonEnvironment } from '../../../envExt/types'; +import { getEnvExtApi, useEnvExtension } from '../../../envExt/api.internal'; +import { ProjectAdapter } from './projectAdapter'; +import { getProjectId, createProjectDisplayName, createTestAdapters } from './projectUtils'; +import { PythonResultResolver } from './resultResolver'; + +/** + * Registry for Python test projects within workspaces. + * + * Manages the lifecycle of test projects including: + * - Discovering Python projects via Python Environments API + * - Creating and storing ProjectAdapter instances per workspace + * - Computing nested project relationships for ignore lists + * - Fallback to default "legacy" project when API unavailable + * + * **Key concepts:** + * - **Workspace:** A VS Code workspace folder (may contain multiple projects) + * - **Project:** A Python project within a workspace (identified by pyproject.toml, setup.py, etc.) + * - **ProjectUri:** The unique identifier for a project (the URI of the project root directory) + * - Each project gets its own test tree root, Python environment, and test adapters + * + * **Project identification:** + * Projects are identified and tracked by their URI (projectUri.toString()). This matches + * how the Python Environments extension stores projects in its Map. + */ +export class TestProjectRegistry { + /** + * Map of workspace URI -> Map of project URI string -> ProjectAdapter + * + * Projects are keyed by their URI string (projectUri.toString()) which matches how + * the Python Environments extension identifies projects. This enables O(1) lookups + * when given a project URI. + */ + private readonly workspaceProjects: Map> = new Map(); + + constructor( + private readonly testController: TestController, + private readonly configSettings: IConfigurationService, + private readonly interpreterService: IInterpreterService, + private readonly envVarsService: IEnvironmentVariablesProvider, + ) {} + + /** + * Gets the projects map for a workspace, if it exists. + */ + public getWorkspaceProjects(workspaceUri: Uri): Map | undefined { + return this.workspaceProjects.get(workspaceUri); + } + + /** + * Checks if a workspace has been initialized with projects. + */ + public hasProjects(workspaceUri: Uri): boolean { + return this.workspaceProjects.has(workspaceUri); + } + + /** + * Gets all projects for a workspace as an array. + */ + public getProjectsArray(workspaceUri: Uri): ProjectAdapter[] { + const projectsMap = this.workspaceProjects.get(workspaceUri); + return projectsMap ? Array.from(projectsMap.values()) : []; + } + + /** + * Discovers and registers all Python projects for a workspace. + * Returns the discovered projects for the caller to use. + */ + public async discoverAndRegisterProjects(workspaceUri: Uri): Promise { + traceInfo(`[test-by-project] Discovering projects for workspace: ${workspaceUri.fsPath}`); + + const projects = await this.discoverProjects(workspaceUri); + + // Create map for this workspace, keyed by project URI + const projectsMap = new Map(); + projects.forEach((project) => { + projectsMap.set(getProjectId(project.projectUri), project); + }); + + this.workspaceProjects.set(workspaceUri, projectsMap); + traceInfo(`[test-by-project] Registered ${projects.length} project(s) for ${workspaceUri.fsPath}`); + + return projects; + } + + /** + * Computes and populates nested project ignore lists for all projects in a workspace. + * Must be called before discovery to ensure parent projects ignore nested children. + */ + public configureNestedProjectIgnores(workspaceUri: Uri): void { + const projectIgnores = this.computeNestedProjectIgnores(workspaceUri); + const projects = this.getProjectsArray(workspaceUri); + + for (const project of projects) { + const ignorePaths = projectIgnores.get(getProjectId(project.projectUri)); + if (ignorePaths && ignorePaths.length > 0) { + project.nestedProjectPathsToIgnore = ignorePaths; + traceInfo(`[test-by-project] ${project.projectName} will ignore nested: ${ignorePaths.join(', ')}`); + } + } + } + + /** + * Clears all projects for a workspace. + */ + public clearWorkspace(workspaceUri: Uri): void { + this.workspaceProjects.delete(workspaceUri); + } + + // ====== Private Methods ====== + + /** + * Discovers Python projects in a workspace using the Python Environment API. + * Falls back to creating a single default project if API is unavailable. + */ + private async discoverProjects(workspaceUri: Uri): Promise { + try { + if (!useEnvExtension()) { + traceInfo('[test-by-project] Python Environments API not available, using default project'); + return [await this.createDefaultProject(workspaceUri)]; + } + + const envExtApi = await getEnvExtApi(); + const allProjects = envExtApi.getPythonProjects(); + traceInfo(`[test-by-project] Found ${allProjects.length} total Python projects from API`); + + // Filter to projects within this workspace + const workspaceProjects = allProjects.filter((project) => + isParentPath(project.uri.fsPath, workspaceUri.fsPath), + ); + traceInfo(`[test-by-project] Filtered to ${workspaceProjects.length} projects in workspace`); + + if (workspaceProjects.length === 0) { + traceInfo('[test-by-project] No projects found, creating default project'); + return [await this.createDefaultProject(workspaceUri)]; + } + + // Create ProjectAdapter for each discovered project + const adapters: ProjectAdapter[] = []; + for (const pythonProject of workspaceProjects) { + try { + const adapter = await this.createProjectAdapter(pythonProject, workspaceUri); + adapters.push(adapter); + } catch (error) { + traceError(`[test-by-project] Failed to create adapter for ${pythonProject.uri.fsPath}:`, error); + } + } + + if (adapters.length === 0) { + traceInfo('[test-by-project] All adapters failed, falling back to default project'); + return [await this.createDefaultProject(workspaceUri)]; + } + + return adapters; + } catch (error) { + traceError('[test-by-project] Discovery failed, using default project:', error); + return [await this.createDefaultProject(workspaceUri)]; + } + } + + /** + * Creates a ProjectAdapter from a PythonProject. + * + * Each project gets its own isolated test infrastructure: + * - **ResultResolver:** Handles mapping test IDs and processing results for this project + * - **DiscoveryAdapter:** Discovers tests scoped to this project's root directory + * - **ExecutionAdapter:** Runs tests for this project using its Python environment + * + */ + private async createProjectAdapter(pythonProject: PythonProject, workspaceUri: Uri): Promise { + const projectId = getProjectId(pythonProject.uri); + traceInfo(`[test-by-project] Creating adapter for: ${pythonProject.name} at ${projectId}`); + + // Resolve Python environment + const envExtApi = await getEnvExtApi(); + const pythonEnvironment = await envExtApi.getEnvironment(pythonProject.uri); + if (!pythonEnvironment) { + throw new Error(`No Python environment found for project ${projectId}`); + } + + // Create test infrastructure + const testProvider = this.getTestProvider(workspaceUri); + const projectDisplayName = createProjectDisplayName(pythonProject.name, pythonEnvironment.version); + const resultResolver = new PythonResultResolver( + this.testController, + testProvider, + workspaceUri, + projectId, + pythonProject.name, // Use simple project name for test tree label (without version) + ); + const { discoveryAdapter, executionAdapter } = createTestAdapters( + testProvider, + resultResolver, + this.configSettings, + this.envVarsService, + ); + + return { + projectName: projectDisplayName, + projectUri: pythonProject.uri, + workspaceUri, + pythonProject, + pythonEnvironment, + testProvider, + discoveryAdapter, + executionAdapter, + resultResolver, + isDiscovering: false, + isExecuting: false, + }; + } + + /** + * Creates a default project for legacy/fallback mode. + */ + private async createDefaultProject(workspaceUri: Uri): Promise { + traceInfo(`[test-by-project] Creating default project for: ${workspaceUri.fsPath}`); + + const testProvider = this.getTestProvider(workspaceUri); + const resultResolver = new PythonResultResolver(this.testController, testProvider, workspaceUri); + const { discoveryAdapter, executionAdapter } = createTestAdapters( + testProvider, + resultResolver, + this.configSettings, + this.envVarsService, + ); + + const interpreter = await this.interpreterService.getActiveInterpreter(workspaceUri); + + const pythonEnvironment: PythonEnvironment = { + name: 'default', + displayName: interpreter?.displayName || 'Python', + shortDisplayName: interpreter?.displayName || 'Python', + displayPath: interpreter?.path || 'python', + version: interpreter?.version?.raw || '3.x', + environmentPath: Uri.file(interpreter?.path || 'python'), + sysPrefix: interpreter?.sysPrefix || '', + execInfo: { run: { executable: interpreter?.path || 'python' } }, + envId: { id: 'default', managerId: 'default' }, + }; + + const pythonProject: PythonProject = { + name: path.basename(workspaceUri.fsPath) || 'workspace', + uri: workspaceUri, + }; + + return { + projectName: pythonProject.name, + projectUri: workspaceUri, + workspaceUri, + pythonProject, + pythonEnvironment, + testProvider, + discoveryAdapter, + executionAdapter, + resultResolver, + isDiscovering: false, + isExecuting: false, + }; + } + + /** + * Identifies nested projects and returns ignore paths for parent projects. + * + * **Time complexity:** O(nΒ²) where n is the number of projects in the workspace. + * For each project, checks all other projects to find nested relationships. + * + * Note: Uses path.normalize() to handle Windows path separator inconsistencies + * (e.g., paths from URI.fsPath may have mixed separators). + */ + private computeNestedProjectIgnores(workspaceUri: Uri): Map { + const ignoreMap = new Map(); + const projects = this.getProjectsArray(workspaceUri); + + if (projects.length === 0) return ignoreMap; + + for (const parent of projects) { + const nestedPaths: string[] = []; + + for (const child of projects) { + // Skip self-comparison using URI + if (parent.projectUri.toString() === child.projectUri.toString()) continue; + + // Normalize paths to handle Windows path separator inconsistencies + const parentNormalized = path.normalize(parent.projectUri.fsPath); + const childNormalized = path.normalize(child.projectUri.fsPath); + + // Add trailing separator to ensure we match directory boundaries + const parentWithSep = parentNormalized.endsWith(path.sep) + ? parentNormalized + : parentNormalized + path.sep; + const childWithSep = childNormalized.endsWith(path.sep) ? childNormalized : childNormalized + path.sep; + + // Check if child is inside parent (case-insensitive for Windows) + const childIsInsideParent = childWithSep.toLowerCase().startsWith(parentWithSep.toLowerCase()); + + if (childIsInsideParent) { + nestedPaths.push(child.projectUri.fsPath); + traceInfo(`[test-by-project] Nested: ${child.projectName} is inside ${parent.projectName}`); + } + } + + if (nestedPaths.length > 0) { + ignoreMap.set(getProjectId(parent.projectUri), nestedPaths); + } + } + + return ignoreMap; + } + + /** + * Determines the test provider based on workspace settings. + */ + private getTestProvider(workspaceUri: Uri): TestProvider { + const settings = this.configSettings.getSettings(workspaceUri); + return settings.testing.unittestEnabled ? UNITTEST_PROVIDER : 'pytest'; + } +} diff --git a/src/client/testing/testController/common/types.ts b/src/client/testing/testController/common/types.ts index 52c6c787040c..017c41cf3d97 100644 --- a/src/client/testing/testController/common/types.ts +++ b/src/client/testing/testController/common/types.ts @@ -4,6 +4,7 @@ import { CancellationToken, Event, + FileCoverageDetail, OutputChannel, TestController, TestItem, @@ -12,13 +13,10 @@ import { Uri, WorkspaceFolder, } from 'vscode'; -import { TestDiscoveryOptions } from '../../common/types'; +import { ITestDebugLauncher } from '../../common/types'; import { IPythonExecutionFactory } from '../../../common/process/types'; - -export type TestRunInstanceOptions = TestRunOptions & { - exclude?: readonly TestItem[]; - debug: boolean; -}; +import { PythonEnvironment } from '../../../pythonEnvironments/info'; +import { ProjectAdapter } from './projectAdapter'; export enum TestDataKinds { Workspace, @@ -36,11 +34,6 @@ export interface TestData { kind: TestDataKinds; } -export const ITestDiscoveryHelper = Symbol('ITestDiscoveryHelper'); -export interface ITestDiscoveryHelper { - runTestDiscovery(options: TestDiscoveryOptions): Promise; -} - export type TestRefreshOptions = { forceRefresh: boolean }; export const ITestController = Symbol('ITestController'); @@ -52,41 +45,13 @@ export interface ITestController { onRunWithoutConfiguration: Event; } -export interface ITestRun { - includes: readonly TestItem[]; - excludes: readonly TestItem[]; - runKind: TestRunProfileKind; - runInstance: TestRun; -} - export const ITestFrameworkController = Symbol('ITestFrameworkController'); export interface ITestFrameworkController { resolveChildren(testController: TestController, item: TestItem, token?: CancellationToken): Promise; - refreshTestData(testController: TestController, resource?: Uri, token?: CancellationToken): Promise; - runTests( - testRun: ITestRun, - workspace: WorkspaceFolder, - token: CancellationToken, - testController?: TestController, - ): Promise; } export const ITestsRunner = Symbol('ITestsRunner'); -export interface ITestsRunner { - runTests( - testRun: ITestRun, - options: TestRunOptions, - idToRawData: Map, - testController?: TestController, - ): Promise; -} - -export type TestRunOptions = { - workspaceFolder: Uri; - cwd: string; - args: string[]; - token: CancellationToken; -}; +export interface ITestsRunner {} // We expose these here as a convenience and to cut down on churn // elsewhere in the code. @@ -146,58 +111,84 @@ export type TestCommandOptions = { workspaceFolder: Uri; cwd: string; command: TestDiscoveryCommand | TestExecutionCommand; - uuid: string; token?: CancellationToken; outChannel?: OutputChannel; - debugBool?: boolean; + profileKind?: TestRunProfileKind; testIds?: string[]; }; -export type TestCommandOptionsPytest = { - workspaceFolder: Uri; - cwd: string; - commandStr: string; - token?: CancellationToken; - outChannel?: OutputChannel; - debugBool?: boolean; - testIds?: string[]; - env: { [key: string]: string | undefined }; -}; +// /** +// * Interface describing the server that will send test commands to the Python side, and process responses. +// * +// * Consumers will call sendCommand in order to execute Python-related code, +// * and will subscribe to the onDataReceived event to wait for the results. +// */ +// export interface ITestServer { +// readonly onDataReceived: Event; +// readonly onRunDataReceived: Event; +// readonly onDiscoveryDataReceived: Event; +// sendCommand( +// options: TestCommandOptions, +// env: EnvironmentVariables, +// runTestIdsPort?: string, +// runInstance?: TestRun, +// testIds?: string[], +// callback?: () => void, +// executionFactory?: IPythonExecutionFactory, +// ): Promise; +// serverReady(): Promise; +// getPort(): number; +// createUUID(cwd: string): string; +// deleteUUID(uuid: string): void; +// triggerRunDataReceivedEvent(data: DataReceivedEvent): void; +// triggerDiscoveryDataReceivedEvent(data: DataReceivedEvent): void; +// } /** - * Interface describing the server that will send test commands to the Python side, and process responses. - * - * Consumers will call sendCommand in order to execute Python-related code, - * and will subscribe to the onDataReceived event to wait for the results. + * Test item mapping interface used by populateTestTree. + * Contains only the maps needed for building the test tree. */ -export interface ITestServer { - readonly onDataReceived: Event; - sendCommand(options: TestCommandOptions): Promise; - serverReady(): Promise; - getPort(): number; - createUUID(cwd: string): string; +export interface ITestItemMappings { + runIdToVSid: Map; + runIdToTestItem: Map; + vsIdToRunId: Map; } +export interface ITestResultResolver extends ITestItemMappings { + detailedCoverageMap: Map; + + resolveDiscovery(payload: DiscoveredTestPayload, token?: CancellationToken): void; + resolveExecution(payload: ExecutionTestPayload | CoveragePayload, runInstance: TestRun): void; + _resolveDiscovery(payload: DiscoveredTestPayload, token?: CancellationToken): void; + _resolveExecution(payload: ExecutionTestPayload, runInstance: TestRun): void; + _resolveCoverage(payload: CoveragePayload, runInstance: TestRun): void; +} export interface ITestDiscoveryAdapter { - // ** first line old method signature, second line new method signature - discoverTests(uri: Uri): Promise; - discoverTests(uri: Uri, executionFactory: IPythonExecutionFactory): Promise; + discoverTests( + uri: Uri, + executionFactory: IPythonExecutionFactory, + token?: CancellationToken, + interpreter?: PythonEnvironment, + project?: ProjectAdapter, + ): Promise; } // interface for execution/runner adapter export interface ITestExecutionAdapter { - // ** first line old method signature, second line new method signature - runTests(uri: Uri, testIds: string[], debugBool?: boolean): Promise; runTests( uri: Uri, testIds: string[], - debugBool?: boolean, - executionFactory?: IPythonExecutionFactory, - ): Promise; + profileKind: boolean | TestRunProfileKind | undefined, + runInstance: TestRun, + executionFactory: IPythonExecutionFactory, + debugLauncher?: ITestDebugLauncher, + interpreter?: PythonEnvironment, + project?: ProjectAdapter, + ): Promise; } -// Same types as in pythonFiles/unittestadapter/utils.py -export type DiscoveredTestType = 'folder' | 'file' | 'class' | 'test'; +// Same types as in python_files/unittestadapter/utils.py +export type DiscoveredTestType = 'folder' | 'file' | 'class' | 'function' | 'test'; export type DiscoveredTestCommon = { path: string; @@ -208,19 +199,39 @@ export type DiscoveredTestCommon = { }; export type DiscoveredTestItem = DiscoveredTestCommon & { - lineno: number; + lineno: number | string; runID: string; }; export type DiscoveredTestNode = DiscoveredTestCommon & { children: (DiscoveredTestNode | DiscoveredTestItem)[]; + lineno?: number | string; }; export type DiscoveredTestPayload = { cwd: string; tests?: DiscoveredTestNode; status: 'success' | 'error'; - errors?: string[]; + error?: string[]; +}; + +export type CoveragePayload = { + coverage: boolean; + cwd: string; + result?: { + [filePathStr: string]: FileCoverageMetrics; + }; + error: string; +}; + +// using camel-case for these types to match the python side +export type FileCoverageMetrics = { + // eslint-disable-next-line camelcase + lines_covered: number[]; + // eslint-disable-next-line camelcase + lines_missed: number[]; + executed_branches: number; + total_branches: number; }; export type ExecutionTestPayload = { diff --git a/src/client/testing/testController/common/utils.ts b/src/client/testing/testController/common/utils.ts index e0bad383d695..9782487d940b 100644 --- a/src/client/testing/testController/common/utils.ts +++ b/src/client/testing/testController/common/utils.ts @@ -1,52 +1,435 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. +import * as path from 'path'; +import * as fs from 'fs'; +import * as os from 'os'; +import * as crypto from 'crypto'; +import { CancellationToken, Position, TestController, TestItem, Uri, Range, Disposable } from 'vscode'; +import { Message } from 'vscode-jsonrpc'; +import { traceError, traceInfo, traceLog, traceVerbose } from '../../../logging'; +import { DebugTestTag, ErrorTestItemOptions, RunTestTag } from './testItemUtilities'; +import { + DiscoveredTestItem, + DiscoveredTestNode, + DiscoveredTestPayload, + ExecutionTestPayload, + ITestItemMappings, +} from './types'; +import { Deferred, createDeferred } from '../../../common/utils/async'; +import { createReaderPipe, generateRandomPipeName } from '../../../common/pipes/namedPipes'; +import { EXTENSION_ROOT_DIR } from '../../../constants'; +import { PROJECT_ID_SEPARATOR } from './projectUtils'; -export function fixLogLines(content: string): string { +export function fixLogLinesNoTrailing(content: string): string { const lines = content.split(/\r?\n/g); - return `${lines.join('\r\n')}\r\n`; + return `${lines.join('\r\n')}`; } -export interface IJSONRPCContent { - extractedJSON: string; - remainingRawData: string; +export function createTestingDeferred(): Deferred { + return createDeferred(); } -export interface IJSONRPCHeaders { - headers: Map; - remainingRawData: string; +interface ExecutionResultMessage extends Message { + params: ExecutionTestPayload; } -export const JSONRPC_UUID_HEADER = 'Request-uuid'; -export const JSONRPC_CONTENT_LENGTH_HEADER = 'Content-Length'; -export const JSONRPC_CONTENT_TYPE_HEADER = 'Content-Type'; +/** + * Retrieves the path to the temporary directory. + * + * On Windows, it returns the default temporary directory. + * On macOS/Linux, it prefers the `XDG_RUNTIME_DIR` environment variable if set, + * otherwise, it falls back to the default temporary directory. + * + * @returns {string} The path to the temporary directory. + */ +function getTempDir(): string { + if (process.platform === 'win32') { + return os.tmpdir(); // Default Windows behavior + } + return process.env.XDG_RUNTIME_DIR || os.tmpdir(); // Prefer XDG_RUNTIME_DIR on macOS/Linux +} -export function jsonRPCHeaders(rawData: string): IJSONRPCHeaders { - const lines = rawData.split('\n'); - let remainingRawData = ''; - const headerMap = new Map(); - for (let i = 0; i < lines.length; i += 1) { - const line = lines[i]; - if (line === '') { - remainingRawData = lines.slice(i + 1).join('\n'); - break; - } - const [key, value] = line.split(':'); - if ([JSONRPC_UUID_HEADER, JSONRPC_CONTENT_LENGTH_HEADER, JSONRPC_CONTENT_TYPE_HEADER].includes(key)) { - headerMap.set(key.trim(), value.trim()); +/** + * Writes an array of test IDs to a temporary file. + * + * @param testIds - The array of test IDs to write. + * @returns A promise that resolves to the file name of the temporary file. + */ +export async function writeTestIdsFile(testIds: string[]): Promise { + // temp file name in format of test-ids-.txt + const randomSuffix = crypto.randomBytes(10).toString('hex'); + const tempName = `test-ids-${randomSuffix}.txt`; + // create temp file + let tempFileName: string; + const tempDir: string = getTempDir(); + try { + traceLog('Attempting to use temp directory for test ids file, file name:', tempName); + tempFileName = path.join(tempDir, tempName); + // attempt access to written file to check permissions + await fs.promises.access(tempDir); + } catch (error) { + // Handle the error when accessing the temp directory + traceError('Error accessing temp directory:', error, ' Attempt to use extension root dir instead'); + // Make new temp directory in extension root dir + const tempDir = path.join(EXTENSION_ROOT_DIR, '.temp'); + await fs.promises.mkdir(tempDir, { recursive: true }); + tempFileName = path.join(EXTENSION_ROOT_DIR, '.temp', tempName); + traceLog('New temp file:', tempFileName); + } + // write test ids to file + await fs.promises.writeFile(tempFileName, testIds.join('\n')); + // return file name + return tempFileName; +} + +export async function startRunResultNamedPipe( + dataReceivedCallback: (payload: ExecutionTestPayload) => void, + deferredTillServerClose: Deferred, + cancellationToken?: CancellationToken, +): Promise { + traceVerbose('Starting Test Result named pipe'); + const pipeName: string = generateRandomPipeName('python-test-results'); + + const reader = await createReaderPipe(pipeName, cancellationToken); + traceVerbose(`Test Results named pipe ${pipeName} connected`); + let disposables: Disposable[] = []; + const disposable = new Disposable(() => { + traceVerbose(`Test Results named pipe ${pipeName} disposed`); + disposables.forEach((d) => d.dispose()); + disposables = []; + deferredTillServerClose.resolve(); + }); + + if (cancellationToken) { + disposables.push( + cancellationToken?.onCancellationRequested(() => { + traceLog(`Test Result named pipe ${pipeName} cancelled`); + disposable.dispose(); + }), + ); + } + disposables.push( + reader, + reader.listen((data: Message) => { + traceVerbose(`Test Result named pipe ${pipeName} received data`); + // if EOT, call decrement connection count (callback) + dataReceivedCallback((data as ExecutionResultMessage).params as ExecutionTestPayload); + }), + reader.onClose(() => { + // this is called once the server close, once per run instance + traceVerbose(`Test Result named pipe ${pipeName} closed. Disposing of listener/s.`); + // dispose of all data listeners and cancelation listeners + disposable.dispose(); + }), + reader.onError((error) => { + traceError(`Test Results named pipe ${pipeName} error:`, error); + }), + ); + + return pipeName; +} + +interface DiscoveryResultMessage extends Message { + params: DiscoveredTestPayload; +} + +export async function startDiscoveryNamedPipe( + callback: (payload: DiscoveredTestPayload) => void, + cancellationToken?: CancellationToken, +): Promise { + traceVerbose('Starting Test Discovery named pipe'); + // const pipeName: string = '/Users/eleanorboyd/testingFiles/inc_dec_example/temp33.txt'; + const pipeName: string = generateRandomPipeName('python-test-discovery'); + const reader = await createReaderPipe(pipeName, cancellationToken); + + traceVerbose(`Test Discovery named pipe ${pipeName} connected`); + let disposables: Disposable[] = []; + const disposable = new Disposable(() => { + traceVerbose(`Test Discovery named pipe ${pipeName} disposed`); + disposables.forEach((d) => d.dispose()); + disposables = []; + }); + + if (cancellationToken) { + disposables.push( + cancellationToken.onCancellationRequested(() => { + traceVerbose(`Test Discovery named pipe ${pipeName} cancelled`); + disposable.dispose(); + }), + ); + } + + disposables.push( + reader, + reader.listen((data: Message) => { + traceVerbose(`Test Discovery named pipe ${pipeName} received data`); + callback((data as DiscoveryResultMessage).params as DiscoveredTestPayload); + }), + reader.onClose(() => { + traceVerbose(`Test Discovery named pipe ${pipeName} closed`); + disposable.dispose(); + }), + reader.onError((error) => { + traceError(`Test Discovery named pipe ${pipeName} error:`, error); + }), + ); + return pipeName; +} + +/** + * Extracts the missing module name from a ModuleNotFoundError or ImportError message. + * @param message The error message to parse + * @returns The module name if found, undefined otherwise + */ +function extractMissingModuleName(message: string): string | undefined { + // Match patterns like: + // - No module named 'requests' + // - No module named "requests" + // - ModuleNotFoundError: No module named 'requests' + // - ImportError: No module named requests + const patterns = [/No module named ['"]([^'"]+)['"]/, /No module named (\S+)/]; + + for (const pattern of patterns) { + const match = message.match(pattern); + if (match) { + return match[1]; } } + return undefined; +} + +export function buildErrorNodeOptions( + uri: Uri, + message: string, + testType: string, + projectName?: string, +): ErrorTestItemOptions { + let labelText = testType === 'pytest' ? 'pytest Discovery Error' : 'Unittest Discovery Error'; + let errorMessage = message; + + // Check for missing module errors and provide specific messaging + const missingModule = extractMissingModuleName(message); + if (missingModule) { + labelText = `Missing Module: ${missingModule}`; + errorMessage = `The module '${missingModule}' is not installed in the selected Python environment. Please install it to enable test discovery.`; + } + + // Use project name for label if available (project-based testing), otherwise use folder name + const displayName = projectName ?? path.basename(uri.fsPath); return { - headers: headerMap, - remainingRawData, + id: `DiscoveryError:${uri.fsPath}`, + label: `${labelText} [${displayName}]`, + error: errorMessage, + }; +} + +export function populateTestTree( + testController: TestController, + testTreeData: DiscoveredTestNode, + testRoot: TestItem | undefined, + testItemMappings: ITestItemMappings, + token?: CancellationToken, + projectId?: string, + projectName?: string, +): void { + // If testRoot is undefined, use the info of the root item of testTreeData to create a test item, and append it to the test controller. + if (!testRoot) { + // Create project-scoped ID if projectId is provided + const rootId = projectId ? `${projectId}${PROJECT_ID_SEPARATOR}${testTreeData.path}` : testTreeData.path; + // Use "Project: {name}" label for project-based testing, otherwise use folder name + const rootLabel = projectName ? `Project: ${projectName}` : testTreeData.name; + testRoot = testController.createTestItem(rootId, rootLabel, Uri.file(testTreeData.path)); + + testRoot.canResolveChildren = true; + testRoot.tags = [RunTestTag, DebugTestTag]; + + testController.items.add(testRoot); + } + + // Recursively populate the tree with test data. + testTreeData.children.forEach((child) => { + if (!token?.isCancellationRequested) { + if (isTestItem(child)) { + // Create project-scoped vsId + const vsId = projectId ? `${projectId}${PROJECT_ID_SEPARATOR}${child.id_}` : child.id_; + const testItem = testController.createTestItem(vsId, child.name, Uri.file(child.path)); + testItem.tags = [RunTestTag, DebugTestTag]; + + let range: Range | undefined; + if (child.lineno) { + if (Number(child.lineno) === 0) { + range = new Range(new Position(0, 0), new Position(0, 0)); + } else { + range = new Range( + new Position(Number(child.lineno) - 1, 0), + new Position(Number(child.lineno), 0), + ); + } + } + testItem.canResolveChildren = false; + testItem.range = range; + testItem.tags = [RunTestTag, DebugTestTag]; + + testRoot!.children.add(testItem); + // add to our map - use runID as key, vsId as value + testItemMappings.runIdToTestItem.set(child.runID, testItem); + testItemMappings.runIdToVSid.set(child.runID, vsId); + testItemMappings.vsIdToRunId.set(vsId, child.runID); + } else { + // Use project-scoped ID for non-test nodes and look up within the current root + const nodeId = projectId ? `${projectId}${PROJECT_ID_SEPARATOR}${child.id_}` : child.id_; + let node = testRoot!.children.get(nodeId); + + if (!node) { + node = testController.createTestItem(nodeId, child.name, Uri.file(child.path)); + + node.canResolveChildren = true; + node.tags = [RunTestTag, DebugTestTag]; + + // Set range for class nodes (and other nodes) if lineno is available + let range: Range | undefined; + if ('lineno' in child && child.lineno) { + if (Number(child.lineno) === 0) { + range = new Range(new Position(0, 0), new Position(0, 0)); + } else { + range = new Range( + new Position(Number(child.lineno) - 1, 0), + new Position(Number(child.lineno), 0), + ); + } + node.range = range; + } + + testRoot!.children.add(node); + } + populateTestTree(testController, child, node, testItemMappings, token, projectId, projectName); + } + } + }); +} + +function isTestItem(test: DiscoveredTestNode | DiscoveredTestItem): test is DiscoveredTestItem { + return test.type_ === 'test'; +} + +export function createExecutionErrorPayload( + code: number | null, + signal: NodeJS.Signals | null, + testIds: string[], + cwd: string, +): ExecutionTestPayload { + const etp: ExecutionTestPayload = { + cwd, + status: 'error', + error: `Test run failed, the python test process was terminated before it could exit on its own for workspace ${cwd}`, + result: {}, }; + // add error result for each attempted test. + for (let i = 0; i < testIds.length; i = i + 1) { + const test = testIds[i]; + etp.result![test] = { + test, + outcome: 'error', + message: ` \n The python test process was terminated before it could exit on its own, the process errored with: Code: ${code}, Signal: ${signal}`, + }; + } + return etp; } -export function jsonRPCContent(headers: Map, rawData: string): IJSONRPCContent { - const length = parseInt(headers.get('Content-Length') ?? '0', 10); - const data = rawData.slice(0, length); - const remainingRawData = rawData.slice(length); +export function createDiscoveryErrorPayload( + code: number | null, + signal: NodeJS.Signals | null, + cwd: string, +): DiscoveredTestPayload { return { - extractedJSON: data, - remainingRawData, + cwd, + status: 'error', + error: [ + ` \n The python test process was terminated before it could exit on its own, the process errored with: Code: ${code}, Signal: ${signal} for workspace ${cwd}`, + ], }; } + +/** + * Splits a test name into its parent test name and subtest unique section. + * + * @param testName The full test name string. + * @returns A tuple where the first item is the parent test name and the second item is the subtest section or `testName` if no subtest section exists. + */ +export function splitTestNameWithRegex(testName: string): [string, string] { + // If a match is found, return the parent test name and the subtest (whichever was captured between parenthesis or square brackets). + // Otherwise, return the entire testName for the parent and entire testName for the subtest. + const regex = /^(.*?) ([\[(].*[\])])$/; + const match = testName.match(regex); + if (match) { + return [match[1].trim(), match[2] || match[3] || testName]; + } + return [testName, testName]; +} + +/** + * Takes a list of arguments and adds an key-value pair to the list if the key doesn't already exist. Searches each element + * in the array for the key to see if it is contained within the element. + * @param args list of arguments to search + * @param argToAdd argument to add if it doesn't already exist + * @returns the list of arguments with the key-value pair added if it didn't already exist + */ +export function addValueIfKeyNotExist(args: string[], key: string, value: string | null): string[] { + for (const arg of args) { + if (arg.includes(key)) { + traceInfo(`arg: ${key} already exists in args, not adding.`); + return args; + } + } + if (value) { + args.push(`${key}=${value}`); + } else { + args.push(`${key}`); + } + return args; +} + +/** + * Checks if a key exists in a list of arguments. Searches each element in the array + * for the key to see if it is contained within the element. + * @param args list of arguments to search + * @param key string to search for + * @returns true if the key exists in the list of arguments, false otherwise + */ +export function argKeyExists(args: string[], key: string): boolean { + for (const arg of args) { + if (arg.includes(key)) { + return true; + } + } + return false; +} + +/** + * Checks recursively if any parent directories of the given path are symbolic links. + * @param {string} currentPath - The path to start checking from. + * @returns {Promise} - Returns true if any parent directory is a symlink, otherwise false. + */ +export async function hasSymlinkParent(currentPath: string): Promise { + try { + // Resolve the path to an absolute path + const absolutePath = path.resolve(currentPath); + // Get the parent directory + const parentDirectory = path.dirname(absolutePath); + // Check if the current directory is the root directory + if (parentDirectory === absolutePath) { + return false; + } + // Check if the parent directory is a symlink + const stats = await fs.promises.lstat(parentDirectory); + if (stats.isSymbolicLink()) { + traceLog(`Symlink found at: ${parentDirectory}`); + return true; + } + // Recurse up the directory tree + return await hasSymlinkParent(parentDirectory); + } catch (error) { + traceError('Error checking symlinks:', error); + return false; + } +} diff --git a/src/client/testing/testController/controller.ts b/src/client/testing/testController/controller.ts index fb176a30af88..04de209c171d 100644 --- a/src/client/testing/testController/controller.ts +++ b/src/client/testing/testController/controller.ts @@ -3,6 +3,7 @@ import { inject, injectable, named } from 'inversify'; import { uniq } from 'lodash'; +import * as minimatch from 'minimatch'; import { CancellationToken, TestController, @@ -15,35 +16,38 @@ import { CancellationTokenSource, Uri, EventEmitter, + TextDocument, + FileCoverageDetail, + TestRun, + MarkdownString, } from 'vscode'; import { IExtensionSingleActivationService } from '../../activation/types'; import { ICommandManager, IWorkspaceService } from '../../common/application/types'; import * as constants from '../../common/constants'; import { IPythonExecutionFactory } from '../../common/process/types'; -import { IConfigurationService, IDisposableRegistry, ITestOutputChannel, Resource } from '../../common/types'; +import { IConfigurationService, IDisposableRegistry, Resource } from '../../common/types'; import { DelayedTrigger, IDelayedTrigger } from '../../common/utils/delayTrigger'; import { noop } from '../../common/utils/misc'; import { IInterpreterService } from '../../interpreter/contracts'; -import { traceError, traceVerbose } from '../../logging'; +import { traceError, traceInfo, traceVerbose } from '../../logging'; import { IEventNamePropertyMapping, sendTelemetryEvent } from '../../telemetry'; import { EventName } from '../../telemetry/constants'; import { PYTEST_PROVIDER, UNITTEST_PROVIDER } from '../common/constants'; import { TestProvider } from '../types'; -import { PythonTestServer } from './common/server'; -import { DebugTestTag, getNodeByUri, RunTestTag } from './common/testItemUtilities'; -import { - ITestController, - ITestDiscoveryAdapter, - ITestFrameworkController, - TestRefreshOptions, - ITestExecutionAdapter, -} from './common/types'; -import { UnittestTestDiscoveryAdapter } from './unittest/testDiscoveryAdapter'; -import { UnittestTestExecutionAdapter } from './unittest/testExecutionAdapter'; -import { PytestTestDiscoveryAdapter } from './pytest/pytestDiscoveryAdapter'; -import { PytestTestExecutionAdapter } from './pytest/pytestExecutionAdapter'; +import { createErrorTestItem, DebugTestTag, getNodeByUri, RunTestTag } from './common/testItemUtilities'; +import { buildErrorNodeOptions } from './common/utils'; +import { ITestController, ITestFrameworkController, TestRefreshOptions } from './common/types'; import { WorkspaceTestAdapter } from './workspaceTestAdapter'; import { ITestDebugLauncher } from '../common/types'; +import { PythonResultResolver } from './common/resultResolver'; +import { onDidSaveTextDocument } from '../../common/vscodeApis/workspaceApis'; +import { IEnvironmentVariablesProvider } from '../../common/variables/types'; +import { ProjectAdapter } from './common/projectAdapter'; +import { TestProjectRegistry } from './common/testProjectRegistry'; +import { createTestAdapters, getProjectId } from './common/projectUtils'; +import { executeTestsForProjects } from './common/projectTestExecution'; +import { useEnvExtension, getEnvExtApi } from '../../envExt/api.internal'; +import { DidChangePythonProjectsEventArgs, PythonProject } from '../../envExt/types'; // Types gymnastics to make sure that sendTriggerTelemetry only accepts the correct types. type EventPropertyType = IEventNamePropertyMapping[EventName.UNITTEST_DISCOVERY_TRIGGER]; @@ -54,8 +58,12 @@ type TriggerType = EventPropertyType[TriggerKeyType]; export class PythonTestController implements ITestController, IExtensionSingleActivationService { public readonly supportedWorkspaceTypes = { untrustedWorkspace: false, virtualWorkspace: false }; + // Legacy: Single workspace test adapter per workspace (backward compatibility) private readonly testAdapters: Map = new Map(); + // Registry for multi-project testing (one registry instance manages all projects across workspaces) + private readonly projectRegistry: TestProjectRegistry; + private readonly triggerTypes: TriggerType[] = []; private readonly testController: TestController; @@ -72,8 +80,6 @@ export class PythonTestController implements ITestController, IExtensionSingleAc WorkspaceFolder[] >(); - private pythonTestServer: PythonTestServer; - public readonly onRefreshingCompleted = this.refreshingCompletedEvent.event; public readonly onRefreshingStarted = this.refreshingStartedEvent.event; @@ -92,13 +98,21 @@ export class PythonTestController implements ITestController, IExtensionSingleAc @inject(ICommandManager) private readonly commandManager: ICommandManager, @inject(IPythonExecutionFactory) private readonly pythonExecFactory: IPythonExecutionFactory, @inject(ITestDebugLauncher) private readonly debugLauncher: ITestDebugLauncher, - @inject(ITestOutputChannel) private readonly testOutputChannel: ITestOutputChannel, + @inject(IEnvironmentVariablesProvider) private readonly envVarsService: IEnvironmentVariablesProvider, ) { this.refreshCancellation = new CancellationTokenSource(); this.testController = tests.createTestController('python-tests', 'Python Tests'); this.disposables.push(this.testController); + // Initialize project registry for multi-project testing support + this.projectRegistry = new TestProjectRegistry( + this.testController, + this.configSettings, + this.interpreterService, + this.envVarsService, + ); + const delayTrigger = new DelayedTrigger( (uri: Uri, invalidate: boolean) => { this.refreshTestDataInternal(uri); @@ -127,7 +141,15 @@ export class PythonTestController implements ITestController, IExtensionSingleAc true, DebugTestTag, ), + this.testController.createRunProfile( + 'Coverage Tests', + TestRunProfileKind.Coverage, + this.runTests.bind(this), + true, + RunTestTag, + ), ); + this.testController.resolveHandler = this.resolveChildren.bind(this); this.testController.refreshHandler = (token: CancellationToken) => { this.disposables.push( @@ -144,61 +166,262 @@ export class PythonTestController implements ITestController, IExtensionSingleAc }); return this.refreshTestData(undefined, { forceRefresh: true }); }; - this.pythonTestServer = new PythonTestServer(this.pythonExecFactory, this.debugLauncher); } + /** + * Determines the test provider (pytest or unittest) based on workspace settings. + */ + private getTestProvider(workspaceUri: Uri): TestProvider { + const settings = this.configSettings.getSettings(workspaceUri); + return settings.testing.unittestEnabled ? UNITTEST_PROVIDER : PYTEST_PROVIDER; + } + + /** + * Sets up file watchers for test discovery triggers. + */ + private setupFileWatchers(workspace: WorkspaceFolder): void { + const settings = this.configSettings.getSettings(workspace.uri); + if (settings.testing.autoTestDiscoverOnSaveEnabled) { + traceVerbose(`Testing: Setting up watcher for ${workspace.uri.fsPath}`); + this.watchForSettingsChanges(workspace); + this.watchForTestContentChangeOnSave(); + } + } + + /** + * Activates the test controller for all workspaces. + * + * Two activation modes: + * 1. **Project-based mode** (when Python Environments API available): + * 2. **Legacy mode** (fallback): + * + * Uses `Promise.allSettled` for resilient multi-workspace activation: + */ public async activate(): Promise { - traceVerbose('Waiting for test server to start...'); - await this.pythonTestServer.serverReady(); - traceVerbose('Test server started.'); const workspaces: readonly WorkspaceFolder[] = this.workspaceService.workspaceFolders || []; + + // PROJECT-BASED MODE: Uses Python Environments API to discover projects + // Each project becomes its own test tree root with its own Python environment + if (useEnvExtension()) { + traceInfo('[test-by-project] Activating project-based testing mode'); + + // Discover projects in parallel across all workspaces + // Promise.allSettled ensures one workspace failure doesn't block others + const results = await Promise.allSettled( + Array.from(workspaces).map(async (workspace) => { + // Queries Python Environments API and creates ProjectAdapter instances + const projects = await this.projectRegistry.discoverAndRegisterProjects(workspace.uri); + return { workspace, projectCount: projects.length }; + }), + ); + + // Process results: successful workspaces get file watchers, failed ones fall back to legacy + results.forEach((result, index) => { + const workspace = workspaces[index]; + if (result.status === 'fulfilled') { + traceInfo( + `[test-by-project] Activated ${result.value.projectCount} project(s) for ${workspace.uri.fsPath}`, + ); + this.setupFileWatchers(workspace); + } else { + // Graceful degradation: if project discovery fails, use legacy single-adapter mode + traceError(`[test-by-project] Failed for ${workspace.uri.fsPath}:`, result.reason); + this.activateLegacyWorkspace(workspace); + } + }); + // Subscribe to project changes to update test tree when projects are added/removed + await this.subscribeToProjectChanges(); + return; + } + + // LEGACY MODE: Single WorkspaceTestAdapter per workspace (backward compatibility) workspaces.forEach((workspace) => { - const settings = this.configSettings.getSettings(workspace.uri); + this.activateLegacyWorkspace(workspace); + }); + } - let discoveryAdapter: ITestDiscoveryAdapter; - let executionAdapter: ITestExecutionAdapter; - let testProvider: TestProvider; - if (settings.testing.unittestEnabled) { - discoveryAdapter = new UnittestTestDiscoveryAdapter( - this.pythonTestServer, - this.configSettings, - this.testOutputChannel, - ); - executionAdapter = new UnittestTestExecutionAdapter( - this.pythonTestServer, - this.configSettings, - this.testOutputChannel, - ); - testProvider = UNITTEST_PROVIDER; - } else { - discoveryAdapter = new PytestTestDiscoveryAdapter( - this.pythonTestServer, - this.configSettings, - this.testOutputChannel, - ); - executionAdapter = new PytestTestExecutionAdapter( - this.pythonTestServer, - this.configSettings, - this.testOutputChannel, - ); - testProvider = PYTEST_PROVIDER; + /** + * Subscribes to Python project changes from the Python Environments API. + * When projects are added or removed, updates the test tree accordingly. + */ + private async subscribeToProjectChanges(): Promise { + try { + const envExtApi = await getEnvExtApi(); + this.disposables.push( + envExtApi.onDidChangePythonProjects((event: DidChangePythonProjectsEventArgs) => { + this.handleProjectChanges(event).catch((error) => { + traceError('[test-by-project] Error handling project changes:', error); + }); + }), + ); + traceInfo('[test-by-project] Subscribed to Python project changes'); + } catch (error) { + traceError('[test-by-project] Failed to subscribe to project changes:', error); + } + } + + /** + * Handles changes to Python projects (added or removed). + * Cleans up stale test items and re-discovers projects and tests for affected workspaces. + */ + private async handleProjectChanges(event: DidChangePythonProjectsEventArgs): Promise { + const { added, removed } = event; + + if (added.length === 0 && removed.length === 0) { + return; + } + + traceInfo(`[test-by-project] Project changes detected: ${added.length} added, ${removed.length} removed`); + + // Find all affected workspaces + const affectedWorkspaces = new Set(); + + const findWorkspace = (project: PythonProject): WorkspaceFolder | undefined => { + return this.workspaceService.getWorkspaceFolder(project.uri); + }; + + for (const project of [...added, ...removed]) { + const workspace = findWorkspace(project); + if (workspace) { + affectedWorkspaces.add(workspace); } + } - const workspaceTestAdapter = new WorkspaceTestAdapter( - testProvider, - discoveryAdapter, - executionAdapter, - workspace.uri, - ); + // For each affected workspace, clean up and re-discover + for (const workspace of affectedWorkspaces) { + traceInfo(`[test-by-project] Re-discovering projects for workspace: ${workspace.uri.fsPath}`); + + // Get the current projects before clearing to know what to clean up + const existingProjects = this.projectRegistry.getProjectsArray(workspace.uri); + + // Remove ALL test items for the affected workspace's projects + // This ensures no stale items remain from deleted/changed projects + this.removeWorkspaceProjectTestItems(workspace.uri, existingProjects); + + // Also explicitly remove test items for removed projects (in case they weren't tracked) + for (const project of removed) { + const projectWorkspace = findWorkspace(project); + if (projectWorkspace?.uri.toString() === workspace.uri.toString()) { + this.removeProjectTestItems(project); + } + } + + // Re-discover all projects and tests for the workspace in a single pass. + // discoverAllProjectsInWorkspace is responsible for clearing/re-registering + // projects and performing test discovery for the workspace. + await this.discoverAllProjectsInWorkspace(workspace.uri); + } + } - this.testAdapters.set(workspace.uri, workspaceTestAdapter); + /** + * Removes all test items associated with projects in a workspace. + * Used to clean up stale items before re-discovery. + */ + private removeWorkspaceProjectTestItems(workspaceUri: Uri, projects: ProjectAdapter[]): void { + const idsToRemove: string[] = []; + + // Collect IDs of test items belonging to any project in this workspace + for (const project of projects) { + const projectIdPrefix = getProjectId(project.projectUri); + const projectFsPath = project.projectUri.fsPath; + + this.testController.items.forEach((item) => { + // Match by project ID prefix (e.g., "file:///path@@vsc@@...") + if (item.id.startsWith(projectIdPrefix)) { + idsToRemove.push(item.id); + } + // Match by fsPath in ID (legacy items might use path directly) + else if (item.id.includes(projectFsPath)) { + idsToRemove.push(item.id); + } + // Match by item URI being within project directory + else if (item.uri && item.uri.fsPath.startsWith(projectFsPath)) { + idsToRemove.push(item.id); + } + }); + } - if (settings.testing.autoTestDiscoverOnSaveEnabled) { - traceVerbose(`Testing: Setting up watcher for ${workspace.uri.fsPath}`); - this.watchForSettingsChanges(workspace); - this.watchForTestContentChanges(workspace); + // Also remove any items whose URI is within the workspace (catch-all for edge cases) + this.testController.items.forEach((item) => { + if ( + item.uri && + this.workspaceService.getWorkspaceFolder(item.uri)?.uri.toString() === workspaceUri.toString() + ) { + if (!idsToRemove.includes(item.id)) { + idsToRemove.push(item.id); + } } }); + + // Remove all collected items + for (const id of idsToRemove) { + this.testController.items.delete(id); + } + + traceInfo( + `[test-by-project] Cleaned up ${idsToRemove.length} test items for workspace: ${workspaceUri.fsPath}`, + ); + } + + /** + * Removes test items associated with a specific project from the test controller. + * Matches items by project ID prefix, fsPath, or URI. + */ + private removeProjectTestItems(project: PythonProject): void { + const projectId = getProjectId(project.uri); + const projectFsPath = project.uri.fsPath; + const idsToRemove: string[] = []; + + // Find all root items that belong to this project + this.testController.items.forEach((item) => { + // Match by project ID prefix (e.g., "file:///path@@vsc@@...") + if (item.id.startsWith(projectId)) { + idsToRemove.push(item.id); + } + // Match by fsPath in ID (items might use path directly without URI prefix) + else if (item.id.startsWith(projectFsPath) || item.id.includes(projectFsPath)) { + idsToRemove.push(item.id); + } + // Match by item URI being within project directory + else if (item.uri && item.uri.fsPath.startsWith(projectFsPath)) { + idsToRemove.push(item.id); + } + }); + + for (const id of idsToRemove) { + this.testController.items.delete(id); + traceVerbose(`[test-by-project] Removed test item: ${id}`); + } + + if (idsToRemove.length > 0) { + traceInfo(`[test-by-project] Removed ${idsToRemove.length} test items for project: ${project.name}`); + } + } + + /** + * Activates testing for a workspace using the legacy single-adapter approach. + * Used for backward compatibility when project-based testing is disabled or unavailable. + */ + private activateLegacyWorkspace(workspace: WorkspaceFolder): void { + const testProvider = this.getTestProvider(workspace.uri); + const resultResolver = new PythonResultResolver(this.testController, testProvider, workspace.uri); + const { discoveryAdapter, executionAdapter } = createTestAdapters( + testProvider, + resultResolver, + this.configSettings, + this.envVarsService, + ); + + const workspaceTestAdapter = new WorkspaceTestAdapter( + testProvider, + discoveryAdapter, + executionAdapter, + workspace.uri, + resultResolver, + ); + + this.testAdapters.set(workspace.uri, workspaceTestAdapter); + this.setupFileWatchers(workspace); } public refreshTestData(uri?: Resource, options?: TestRefreshOptions): Promise { @@ -238,86 +461,228 @@ export class PythonTestController implements ITestController, IExtensionSingleAc private async refreshTestDataInternal(uri?: Resource): Promise { this.refreshingStartedEvent.fire(); - if (uri) { - const settings = this.configSettings.getSettings(uri); - traceVerbose(`Testing: Refreshing test data for ${uri.fsPath}`); - const rewriteTestingEnabled = process.env.ENABLE_PYTHON_TESTING_REWRITE; - if (settings.testing.pytestEnabled) { - // Ensure we send test telemetry if it gets disabled again - this.sendTestDisabledTelemetry = true; - if (rewriteTestingEnabled) { - // ** rewriteTestingEnabled set to true to use NEW test discovery mechanism - const workspace = this.workspaceService.getWorkspaceFolder(uri); - traceVerbose(`Discover tests for workspace name: ${workspace?.name} - uri: ${uri.fsPath}`); - const testAdapter = - this.testAdapters.get(uri) || (this.testAdapters.values().next().value as WorkspaceTestAdapter); - testAdapter.discoverTests( - this.testController, - this.refreshCancellation.token, - this.testAdapters.size > 1, - this.workspaceService.workspaceFile?.fsPath, - this.pythonExecFactory, - ); - } else { - // else use OLD test discovery mechanism - await this.pytest.refreshTestData(this.testController, uri, this.refreshCancellation.token); - } - } else if (settings.testing.unittestEnabled) { - // ** Ensure we send test telemetry if it gets disabled again - this.sendTestDisabledTelemetry = true; - if (rewriteTestingEnabled) { - // ** rewriteTestingEnabled set to true to use NEW test discovery mechanism - const workspace = this.workspaceService.getWorkspaceFolder(uri); - traceVerbose(`Discover tests for workspace name: ${workspace?.name} - uri: ${uri.fsPath}`); - const testAdapter = - this.testAdapters.get(uri) || (this.testAdapters.values().next().value as WorkspaceTestAdapter); - testAdapter.discoverTests( - this.testController, - this.refreshCancellation.token, - this.testAdapters.size > 1, - this.workspaceService.workspaceFile?.fsPath, - ); - } else { - // else use OLD test discovery mechanism - await this.unittest.refreshTestData(this.testController, uri, this.refreshCancellation.token); - } + try { + if (uri) { + await this.discoverTestsInWorkspace(uri); } else { - if (this.sendTestDisabledTelemetry) { - this.sendTestDisabledTelemetry = false; - sendTelemetryEvent(EventName.UNITTEST_DISABLED); - } - // If we are here we may have to remove an existing node from the tree - // This handles the case where user removes test settings. Which should remove the - // tests for that particular case from the tree view - const workspace = this.workspaceService.getWorkspaceFolder(uri); - if (workspace) { - const toDelete: string[] = []; - this.testController.items.forEach((i: TestItem) => { - const w = this.workspaceService.getWorkspaceFolder(i.uri); - if (w?.uri.fsPath === workspace.uri.fsPath) { - toDelete.push(i.id); - } - }); - toDelete.forEach((i) => this.testController.items.delete(i)); - } + await this.discoverTestsInAllWorkspaces(); } - } else { - traceVerbose('Testing: Refreshing all test data'); - const workspaces: readonly WorkspaceFolder[] = this.workspaceService.workspaceFolders || []; - await Promise.all( - workspaces.map(async (workspace) => { + } finally { + this.refreshingCompletedEvent.fire(); + } + } + + /** + * Discovers tests for a single workspace. + * + * **Discovery flow:** + * 1. If the workspace has registered projects (via Python Environments API), + * uses project-based discovery: each project is discovered independently + * with its own Python environment and test adapters. + * 2. Otherwise, falls back to legacy mode: a single WorkspaceTestAdapter + * discovers all tests in the workspace using the active interpreter. + * + * In project-based mode, the test tree will have separate roots for each project. + * In legacy mode, the workspace folder is the single test tree root. + */ + private async discoverTestsInWorkspace(uri: Uri): Promise { + const workspace = this.workspaceService.getWorkspaceFolder(uri); + if (!workspace?.uri) { + traceError('Unable to find workspace for given file'); + return; + } + + const settings = this.configSettings.getSettings(uri); + traceVerbose(`Discover tests for workspace name: ${workspace.name} - uri: ${uri.fsPath}`); + + // Ensure we send test telemetry if it gets disabled again + this.sendTestDisabledTelemetry = true; + + // Check if any test framework is enabled BEFORE project-based discovery + // This ensures the config screen stays visible when testing is disabled + if (!settings.testing.pytestEnabled && !settings.testing.unittestEnabled) { + await this.handleNoTestProviderEnabled(workspace); + return; + } + + // Use project-based discovery if applicable (only reached if testing is enabled) + if (this.projectRegistry.hasProjects(workspace.uri)) { + await this.discoverAllProjectsInWorkspace(workspace.uri); + return; + } + + // Legacy mode: Single workspace adapter + if (settings.testing.pytestEnabled) { + await this.discoverWorkspaceTestsLegacy(workspace.uri, 'pytest'); + } else if (settings.testing.unittestEnabled) { + await this.discoverWorkspaceTestsLegacy(workspace.uri, 'unittest'); + } + } + + /** + * Discovers tests for all projects within a workspace (project-based mode). + * Re-discovers projects from the Python Environments API before running test discovery. + * This ensures the test tree stays in sync with project changes. + */ + private async discoverAllProjectsInWorkspace(workspaceUri: Uri): Promise { + // Defensive check: ensure testing is enabled (should be checked by caller, but be safe) + const settings = this.configSettings.getSettings(workspaceUri); + if (!settings.testing.pytestEnabled && !settings.testing.unittestEnabled) { + traceVerbose('[test-by-project] Skipping discovery - no test framework enabled'); + return; + } + + // Get existing projects before re-discovery for cleanup + const existingProjects = this.projectRegistry.getProjectsArray(workspaceUri); + + // Clean up all existing test items for this workspace + // This ensures stale items from deleted/changed projects are removed + this.removeWorkspaceProjectTestItems(workspaceUri, existingProjects); + + // Re-discover projects from Python Environments API + // This picks up any added/removed projects since last discovery + this.projectRegistry.clearWorkspace(workspaceUri); + const projects = await this.projectRegistry.discoverAndRegisterProjects(workspaceUri); + + if (projects.length === 0) { + traceError(`[test-by-project] No projects found for workspace: ${workspaceUri.fsPath}`); + return; + } + + traceInfo(`[test-by-project] Starting discovery for ${projects.length} project(s) in workspace`); + + try { + // Configure nested project exclusions before discovery + this.projectRegistry.configureNestedProjectIgnores(workspaceUri); + + // Track completion for progress logging + const projectsCompleted = new Set(); + + // Run discovery for all projects in parallel + await Promise.all(projects.map((project) => this.discoverTestsForProject(project, projectsCompleted))); + + traceInfo( + `[test-by-project] Discovery complete: ${projectsCompleted.size}/${projects.length} projects completed`, + ); + } catch (error) { + traceError(`[test-by-project] Discovery failed for workspace ${workspaceUri.fsPath}:`, error); + } + } + + /** + * Discovers tests for a single project (project-based mode). + * Creates test tree items rooted at the project's directory. + */ + private async discoverTestsForProject(project: ProjectAdapter, projectsCompleted: Set): Promise { + try { + traceInfo(`[test-by-project] Discovering tests for project: ${project.projectName}`); + project.isDiscovering = true; + + // In project-based mode, the discovery adapter uses the Python Environments API + // to get the environment directly, so we don't need to pass the interpreter + await project.discoveryAdapter.discoverTests( + project.projectUri, + this.pythonExecFactory, + this.refreshCancellation.token, + undefined, // Interpreter not needed; adapter uses Python Environments API + project, + ); + + // Mark project as completed (use URI string as unique key) + projectsCompleted.add(project.projectUri.toString()); + traceInfo(`[test-by-project] Project ${project.projectName} discovery completed`); + } catch (error) { + traceError(`[test-by-project] Discovery failed for project ${project.projectName}:`, error); + // Individual project failures don't block others + projectsCompleted.add(project.projectUri.toString()); // Still mark as completed + } finally { + project.isDiscovering = false; + } + } + + /** + * Discovers tests across all workspace folders. + * Iterates each workspace and triggers discovery. + */ + private async discoverTestsInAllWorkspaces(): Promise { + traceVerbose('Testing: Refreshing all test data'); + const workspaces: readonly WorkspaceFolder[] = this.workspaceService.workspaceFolders || []; + + await Promise.all( + workspaces.map(async (workspace) => { + // In project-based mode, each project has its own environment, + // so we don't require a global active interpreter + if (!useEnvExtension()) { if (!(await this.interpreterService.getActiveInterpreter(workspace.uri))) { this.commandManager .executeCommand(constants.Commands.TriggerEnvironmentSelection, workspace.uri) .then(noop, noop); return; } - await this.refreshTestDataInternal(workspace.uri); - }), + } + await this.discoverTestsInWorkspace(workspace.uri); + }), + ); + } + + /** + * Discovers tests for a workspace using legacy single-adapter mode. + */ + private async discoverWorkspaceTestsLegacy(workspaceUri: Uri, expectedProvider: TestProvider): Promise { + const testAdapter = this.testAdapters.get(workspaceUri); + + if (!testAdapter) { + traceError('Unable to find test adapter for workspace.'); + return; + } + + const actualProvider = testAdapter.getTestProvider(); + if (actualProvider !== expectedProvider) { + traceError(`Test provider in adapter is not ${expectedProvider}. Please reload window.`); + this.surfaceErrorNode( + workspaceUri, + 'Test provider types are not aligned, please reload your VS Code window.', + expectedProvider, ); + return; } - this.refreshingCompletedEvent.fire(); - return Promise.resolve(); + + await testAdapter.discoverTests( + this.testController, + this.pythonExecFactory, + this.refreshCancellation.token, + await this.interpreterService.getActiveInterpreter(workspaceUri), + ); + } + + /** + * Handles the case when no test provider is enabled. + * Sends telemetry and removes test items for the workspace from the tree. + */ + private async handleNoTestProviderEnabled(workspace: WorkspaceFolder): Promise { + if (this.sendTestDisabledTelemetry) { + this.sendTestDisabledTelemetry = false; + sendTelemetryEvent(EventName.UNITTEST_DISABLED); + } + + this.removeTestItemsForWorkspace(workspace); + } + + /** + * Removes all test items belonging to a specific workspace from the test controller. + * This is used when test discovery is disabled for a workspace. + */ + private removeTestItemsForWorkspace(workspace: WorkspaceFolder): void { + const itemsToDelete: string[] = []; + + this.testController.items.forEach((testItem: TestItem) => { + const itemWorkspace = this.workspaceService.getWorkspaceFolder(testItem.uri); + if (itemWorkspace?.uri.fsPath === workspace.uri.fsPath) { + itemsToDelete.push(testItem.id); + } + }); + + itemsToDelete.forEach((id) => this.testController.items.delete(id)); } private async resolveChildren(item: TestItem | undefined): Promise { @@ -336,9 +701,13 @@ export class PythonTestController implements ITestController, IExtensionSingleAc const workspaces: readonly WorkspaceFolder[] = this.workspaceService.workspaceFolders || []; await Promise.all( workspaces.map(async (workspace) => { - if (!(await this.interpreterService.getActiveInterpreter(workspace.uri))) { - traceError('Cannot trigger test discovery as a valid interpreter is not selected'); - return; + // In project-based mode, each project has its own environment, + // so we don't require a global active interpreter + if (!useEnvExtension()) { + if (!(await this.interpreterService.getActiveInterpreter(workspace.uri))) { + traceError('Cannot trigger test discovery as a valid interpreter is not selected'); + return; + } } await this.refreshTestDataInternal(workspace.uri); }), @@ -348,16 +717,7 @@ export class PythonTestController implements ITestController, IExtensionSingleAc } private async runTests(request: TestRunRequest, token: CancellationToken): Promise { - const workspaces: WorkspaceFolder[] = []; - if (request.include) { - uniq(request.include.map((r) => this.workspaceService.getWorkspaceFolder(r.uri))).forEach((w) => { - if (w) { - workspaces.push(w); - } - }); - } else { - (this.workspaceService.workspaceFolders || []).forEach((w) => workspaces.push(w)); - } + const workspaces = this.getWorkspacesForTestRun(request); const runInstance = this.testController.createTestRun( request, `Running Tests for Workspace(s): ${workspaces.map((w) => w.uri.fsPath).join(';')}`, @@ -365,103 +725,20 @@ export class PythonTestController implements ITestController, IExtensionSingleAc ); const dispose = token.onCancellationRequested(() => { + runInstance.appendOutput(`\nRun instance cancelled.\r\n`); runInstance.end(); }); const unconfiguredWorkspaces: WorkspaceFolder[] = []; + try { await Promise.all( - workspaces.map(async (workspace) => { - if (!(await this.interpreterService.getActiveInterpreter(workspace.uri))) { - this.commandManager - .executeCommand(constants.Commands.TriggerEnvironmentSelection, workspace.uri) - .then(noop, noop); - return undefined; - } - const testItems: TestItem[] = []; - // If the run request includes test items then collect only items that belong to - // `workspace`. If there are no items in the run request then just run the `workspace` - // root test node. Include will be `undefined` in the "run all" scenario. - (request.include ?? this.testController.items).forEach((i: TestItem) => { - const w = this.workspaceService.getWorkspaceFolder(i.uri); - if (w?.uri.fsPath === workspace.uri.fsPath) { - testItems.push(i); - } - }); - - const settings = this.configSettings.getSettings(workspace.uri); - if (testItems.length > 0) { - const rewriteTestingEnabled = process.env.ENABLE_PYTHON_TESTING_REWRITE; - if (settings.testing.pytestEnabled) { - sendTelemetryEvent(EventName.UNITTEST_RUN, undefined, { - tool: 'pytest', - debugging: request.profile?.kind === TestRunProfileKind.Debug, - }); - // ** rewriteTestingEnabled set to true to use NEW test discovery mechanism - if (rewriteTestingEnabled) { - const testAdapter = - this.testAdapters.get(workspace.uri) || - (this.testAdapters.values().next().value as WorkspaceTestAdapter); - return testAdapter.executeTests( - this.testController, - runInstance, - testItems, - token, - request.profile?.kind === TestRunProfileKind.Debug, - this.pythonExecFactory, - ); - } - return this.pytest.runTests( - { - includes: testItems, - excludes: request.exclude ?? [], - runKind: request.profile?.kind ?? TestRunProfileKind.Run, - runInstance, - }, - workspace, - token, - ); - } - if (settings.testing.unittestEnabled) { - sendTelemetryEvent(EventName.UNITTEST_RUN, undefined, { - tool: 'unittest', - debugging: request.profile?.kind === TestRunProfileKind.Debug, - }); - // ** rewriteTestingEnabled set to true to use NEW test discovery mechanism - if (rewriteTestingEnabled) { - const testAdapter = - this.testAdapters.get(workspace.uri) || - (this.testAdapters.values().next().value as WorkspaceTestAdapter); - return testAdapter.executeTests( - this.testController, - runInstance, - testItems, - token, - request.profile?.kind === TestRunProfileKind.Debug, - ); - } - // below is old way of running unittest execution - return this.unittest.runTests( - { - includes: testItems, - excludes: request.exclude ?? [], - runKind: request.profile?.kind ?? TestRunProfileKind.Run, - runInstance, - }, - workspace, - token, - this.testController, - ); - } - } - - if (!settings.testing.pytestEnabled && !settings.testing.unittestEnabled) { - unconfiguredWorkspaces.push(workspace); - } - return Promise.resolve(); - }), + workspaces.map((workspace) => + this.runTestsForWorkspace(workspace, request, runInstance, token, unconfiguredWorkspaces), + ), ); } finally { + traceVerbose('Finished running tests, ending runInstance.'); runInstance.appendOutput(`Finished running tests!\r\n`); runInstance.end(); dispose.dispose(); @@ -471,6 +748,168 @@ export class PythonTestController implements ITestController, IExtensionSingleAc } } + /** + * Gets the list of workspaces to run tests for based on the test run request. + */ + private getWorkspacesForTestRun(request: TestRunRequest): WorkspaceFolder[] { + if (request.include) { + const workspaces: WorkspaceFolder[] = []; + uniq(request.include.map((r) => this.workspaceService.getWorkspaceFolder(r.uri))).forEach((w) => { + if (w) { + workspaces.push(w); + } + }); + return workspaces; + } + return Array.from(this.workspaceService.workspaceFolders || []); + } + + /** + * Runs tests for a single workspace. + */ + private async runTestsForWorkspace( + workspace: WorkspaceFolder, + request: TestRunRequest, + runInstance: TestRun, + token: CancellationToken, + unconfiguredWorkspaces: WorkspaceFolder[], + ): Promise { + if (!(await this.interpreterService.getActiveInterpreter(workspace.uri))) { + this.commandManager + .executeCommand(constants.Commands.TriggerEnvironmentSelection, workspace.uri) + .then(noop, noop); + return; + } + + const testItems = this.getTestItemsForWorkspace(workspace, request); + const settings = this.configSettings.getSettings(workspace.uri); + + if (testItems.length === 0) { + if (!settings.testing.pytestEnabled && !settings.testing.unittestEnabled) { + unconfiguredWorkspaces.push(workspace); + } + return; + } + + // Check if we're in project-based mode and should use project-specific execution + if (this.projectRegistry.hasProjects(workspace.uri)) { + const projects = this.projectRegistry.getProjectsArray(workspace.uri); + await executeTestsForProjects(projects, testItems, runInstance, request, token, { + projectRegistry: this.projectRegistry, + pythonExecFactory: this.pythonExecFactory, + debugLauncher: this.debugLauncher, + }); + return; + } + + // For unittest (or pytest when not in project mode), use the legacy WorkspaceTestAdapter. + // In project mode, legacy adapters may not be initialized, so create one on demand. + let testAdapter = this.testAdapters.get(workspace.uri); + if (!testAdapter) { + // Initialize legacy adapter on demand (needed for unittest in project mode) + this.activateLegacyWorkspace(workspace); + testAdapter = this.testAdapters.get(workspace.uri); + } + + if (!testAdapter) { + traceError(`[test] No test adapter available for workspace: ${workspace.uri.fsPath}`); + return; + } + + this.setupCoverageIfNeeded(request, testAdapter); + + if (settings.testing.pytestEnabled) { + await this.executeTestsForProvider( + workspace, + testAdapter, + testItems, + runInstance, + request, + token, + 'pytest', + ); + } else if (settings.testing.unittestEnabled) { + await this.executeTestsForProvider( + workspace, + testAdapter, + testItems, + runInstance, + request, + token, + 'unittest', + ); + } else { + unconfiguredWorkspaces.push(workspace); + } + } + + /** + * Gets test items that belong to a specific workspace from the run request. + */ + private getTestItemsForWorkspace(workspace: WorkspaceFolder, request: TestRunRequest): TestItem[] { + const testItems: TestItem[] = []; + // If the run request includes test items then collect only items that belong to + // `workspace`. If there are no items in the run request then just run the `workspace` + // root test node. Include will be `undefined` in the "run all" scenario. + (request.include ?? this.testController.items).forEach((i: TestItem) => { + const w = this.workspaceService.getWorkspaceFolder(i.uri); + if (w?.uri.fsPath === workspace.uri.fsPath) { + testItems.push(i); + } + }); + return testItems; + } + + /** + * Sets up detailed coverage loading if the run profile is for coverage. + */ + private setupCoverageIfNeeded(request: TestRunRequest, testAdapter: WorkspaceTestAdapter): void { + // no profile will have TestRunProfileKind.Coverage if rewrite isn't enabled + if (request.profile?.kind && request.profile?.kind === TestRunProfileKind.Coverage) { + request.profile.loadDetailedCoverage = ( + _testRun: TestRun, + fileCoverage, + _token, + ): Thenable => { + const details = testAdapter.resultResolver.detailedCoverageMap.get(fileCoverage.uri.fsPath); + if (details === undefined) { + // given file has no detailed coverage data + return Promise.resolve([]); + } + return Promise.resolve(details); + }; + } + } + + /** + * Executes tests using the test adapter for a specific test provider. + */ + private async executeTestsForProvider( + workspace: WorkspaceFolder, + testAdapter: WorkspaceTestAdapter, + testItems: TestItem[], + runInstance: TestRun, + request: TestRunRequest, + token: CancellationToken, + provider: TestProvider, + ): Promise { + sendTelemetryEvent(EventName.UNITTEST_RUN, undefined, { + tool: provider, + debugging: request.profile?.kind === TestRunProfileKind.Debug, + }); + + await testAdapter.executeTests( + this.testController, + runInstance, + testItems, + this.pythonExecFactory, + token, + request.profile?.kind, + this.debugLauncher, + await this.interpreterService.getActiveInterpreter(workspace.uri), + ); + } + private invalidateTests(uri: Uri) { this.testController.items.forEach((root) => { const item = getNodeByUri(root, uri); @@ -488,12 +927,23 @@ export class PythonTestController implements ITestController, IExtensionSingleAc this.disposables.push(watcher); this.disposables.push( - watcher.onDidChange((uri) => { - traceVerbose(`Testing: Trigger refresh after change in ${uri.fsPath}`); - this.sendTriggerTelemetry('watching'); - this.refreshData.trigger(uri, false); + onDidSaveTextDocument(async (doc: TextDocument) => { + const file = doc.fileName; + // refresh on any settings file save + if ( + file.includes('settings.json') || + file.includes('pytest.ini') || + file.includes('setup.cfg') || + file.includes('pyproject.toml') + ) { + traceVerbose(`Testing: Trigger refresh after saving ${doc.uri.fsPath}`); + this.sendTriggerTelemetry('watching'); + this.refreshData.trigger(doc.uri, false); + } }), ); + /* Keep both watchers for create and delete since config files can change test behavior without content + due to their impact on pythonPath. */ this.disposables.push( watcher.onDidCreate((uri) => { traceVerbose(`Testing: Trigger refresh after creating ${uri.fsPath}`); @@ -510,31 +960,18 @@ export class PythonTestController implements ITestController, IExtensionSingleAc ); } - private watchForTestContentChanges(workspace: WorkspaceFolder): void { - const pattern = new RelativePattern(workspace, '**/*.py'); - const watcher = this.workspaceService.createFileSystemWatcher(pattern); - this.disposables.push(watcher); - + private watchForTestContentChangeOnSave(): void { this.disposables.push( - watcher.onDidChange((uri) => { - traceVerbose(`Testing: Trigger refresh after change in ${uri.fsPath}`); - this.sendTriggerTelemetry('watching'); - // We want to invalidate tests for code change - this.refreshData.trigger(uri, true); - }), - ); - this.disposables.push( - watcher.onDidCreate((uri) => { - traceVerbose(`Testing: Trigger refresh after creating ${uri.fsPath}`); - this.sendTriggerTelemetry('watching'); - this.refreshData.trigger(uri, false); - }), - ); - this.disposables.push( - watcher.onDidDelete((uri) => { - traceVerbose(`Testing: Trigger refresh after deleting in ${uri.fsPath}`); - this.sendTriggerTelemetry('watching'); - this.refreshData.trigger(uri, false); + onDidSaveTextDocument(async (doc: TextDocument) => { + const settings = this.configSettings.getSettings(doc.uri); + if ( + settings.testing.autoTestDiscoverOnSaveEnabled && + minimatch.default(doc.uri.fsPath, settings.testing.autoTestDiscoverOnSavePattern) + ) { + traceVerbose(`Testing: Trigger refresh after saving ${doc.uri.fsPath}`); + this.sendTriggerTelemetry('watching'); + this.refreshData.trigger(doc.uri, false); + } }), ); } @@ -552,4 +989,16 @@ export class PythonTestController implements ITestController, IExtensionSingleAc this.triggerTypes.push(trigger); } } + + private surfaceErrorNode(workspaceUri: Uri, message: string, testProvider: TestProvider): void { + let errorNode = this.testController.items.get(`DiscoveryError:${workspaceUri.fsPath}`); + if (errorNode === undefined) { + const options = buildErrorNodeOptions(workspaceUri, message, testProvider); + errorNode = createErrorTestItem(this.testController, options); + this.testController.items.add(errorNode); + } + const errorNodeLabel: MarkdownString = new MarkdownString(message); + errorNodeLabel.isTrusted = true; + errorNode.error = errorNodeLabel; + } } diff --git a/src/client/testing/testController/pytest/arguments.ts b/src/client/testing/testController/pytest/arguments.ts index 78b451acdd6b..2b4efbd56f42 100644 --- a/src/client/testing/testController/pytest/arguments.ts +++ b/src/client/testing/testController/pytest/arguments.ts @@ -1,7 +1,7 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -import { TestDiscoveryOptions, TestFilter } from '../../common/types'; +import { TestFilter } from '../../common/types'; import { getPositionalArguments, filterArguments } from '../common/argumentsHelper'; const OptionsWithArguments = [ @@ -134,11 +134,6 @@ const OptionsWithoutArguments = [ '-d', ]; -export function pytestGetTestFilesAndFolders(args: string[]): string[] { - // If users enter test modules/methods, then its not supported. - return getPositionalArguments(args, OptionsWithArguments, OptionsWithoutArguments); -} - export function removePositionalFoldersAndFiles(args: string[]): string[] { return pytestFilterArguments(args, TestFilter.removeTests); } @@ -258,20 +253,3 @@ function pytestFilterArguments(args: string[], argumentToRemoveOrFilter: string[ } return filterArguments(filteredArgs, optionsWithArgsToRemove, optionsWithoutArgsToRemove); } - -export function preparePytestArgumentsForDiscovery(options: TestDiscoveryOptions): string[] { - // Remove unwanted arguments (which happen to be test directories & test specific args). - const args = pytestFilterArguments(options.args, TestFilter.discovery); - if (options.ignoreCache && args.indexOf('--cache-clear') === -1) { - args.splice(0, 0, '--cache-clear'); - } - if (args.indexOf('-s') === -1) { - args.splice(0, 0, '-s'); - } - - // Only add --rootdir if user has not already provided one - if (args.filter((a) => a.startsWith('--rootdir')).length === 0) { - args.splice(0, 0, '--rootdir', options.cwd); - } - return args; -} diff --git a/src/client/testing/testController/pytest/pytestController.ts b/src/client/testing/testController/pytest/pytestController.ts index 793170231210..f75580c11236 100644 --- a/src/client/testing/testController/pytest/pytestController.ts +++ b/src/client/testing/testController/pytest/pytestController.ts @@ -1,38 +1,20 @@ +/* eslint-disable class-methods-use-this */ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -import { inject, injectable, named } from 'inversify'; -import { flatten } from 'lodash'; +import { inject, injectable } from 'inversify'; import * as path from 'path'; -import * as util from 'util'; -import { CancellationToken, TestItem, Uri, TestController, WorkspaceFolder } from 'vscode'; +import { CancellationToken, TestItem, Uri, TestController } from 'vscode'; import { IWorkspaceService } from '../../../common/application/types'; -import { runAdapter } from '../../../common/process/internal/scripts/testing_tools'; -import { IConfigurationService } from '../../../common/types'; import { asyncForEach } from '../../../common/utils/arrayUtils'; -import { createDeferred, Deferred } from '../../../common/utils/async'; -import { traceError } from '../../../logging'; -import { sendTelemetryEvent } from '../../../telemetry'; -import { EventName } from '../../../telemetry/constants'; -import { PYTEST_PROVIDER } from '../../common/constants'; -import { TestDiscoveryOptions } from '../../common/types'; +import { Deferred } from '../../../common/utils/async'; import { - createErrorTestItem, createWorkspaceRootTestItem, - getNodeByUri, getWorkspaceNode, removeItemByIdFromChildren, updateTestItemFromRawData, } from '../common/testItemUtilities'; -import { - ITestFrameworkController, - ITestDiscoveryHelper, - ITestsRunner, - TestData, - RawDiscoveredTests, - ITestRun, -} from '../common/types'; -import { preparePytestArgumentsForDiscovery, pytestGetTestFilesAndFolders } from './arguments'; +import { ITestFrameworkController, TestData, RawDiscoveredTests } from '../common/types'; @injectable() export class PytestController implements ITestFrameworkController { @@ -42,12 +24,7 @@ export class PytestController implements ITestFrameworkController { private idToRawData: Map = new Map(); - constructor( - @inject(ITestDiscoveryHelper) private readonly discoveryHelper: ITestDiscoveryHelper, - @inject(ITestsRunner) @named(PYTEST_PROVIDER) private readonly runner: ITestsRunner, - @inject(IConfigurationService) private readonly configService: IConfigurationService, - @inject(IWorkspaceService) private readonly workspaceService: IWorkspaceService, - ) {} + constructor(@inject(IWorkspaceService) private readonly workspaceService: IWorkspaceService) {} public async resolveChildren( testController: TestController, @@ -162,155 +139,4 @@ export class PytestController implements ITestFrameworkController { } return Promise.resolve(); } - - public async refreshTestData(testController: TestController, uri: Uri, token?: CancellationToken): Promise { - sendTelemetryEvent(EventName.UNITTEST_DISCOVERING, undefined, { tool: 'pytest' }); - const workspace = this.workspaceService.getWorkspaceFolder(uri); - if (workspace) { - // Discovery is expensive. So if it is already running then use the promise - // from the last run - const previous = this.discovering.get(workspace.uri.fsPath); - if (previous) { - return previous.promise; - } - - const settings = this.configService.getSettings(workspace.uri); - const options: TestDiscoveryOptions = { - workspaceFolder: workspace.uri, - cwd: - settings.testing.cwd && settings.testing.cwd.length > 0 - ? settings.testing.cwd - : workspace.uri.fsPath, - args: settings.testing.pytestArgs, - ignoreCache: true, - token, - }; - - // Get individual test files and directories selected by the user. - const testFilesAndDirectories = pytestGetTestFilesAndFolders(options.args); - - // Set arguments to use with pytest discovery script. - const args = runAdapter(['discover', 'pytest', '--', ...preparePytestArgumentsForDiscovery(options)]); - - // Build options for each directory selected by the user. - let discoveryRunOptions: TestDiscoveryOptions[]; - if (testFilesAndDirectories.length === 0) { - // User did not provide any directory. So we don't need to tweak arguments. - discoveryRunOptions = [ - { - ...options, - args, - }, - ]; - } else { - discoveryRunOptions = testFilesAndDirectories.map((testDir) => ({ - ...options, - args: [...args, testDir], - })); - } - - const deferred = createDeferred(); - this.discovering.set(workspace.uri.fsPath, deferred); - - let rawTestData: RawDiscoveredTests[] = []; - try { - // This is where we execute pytest discovery via a common helper. - rawTestData = flatten( - await Promise.all(discoveryRunOptions.map((o) => this.discoveryHelper.runTestDiscovery(o))), - ); - this.testData.set(workspace.uri.fsPath, rawTestData); - - // Remove error node - testController.items.delete(`DiscoveryError:${workspace.uri.fsPath}`); - - deferred.resolve(); - } catch (ex) { - sendTelemetryEvent(EventName.UNITTEST_DISCOVERY_DONE, undefined, { tool: 'pytest', failed: true }); - const cancel = options.token?.isCancellationRequested ? 'Cancelled' : 'Error'; - traceError(`${cancel} discovering pytest tests:\r\n`, ex); - const message = getTestDiscoveryExceptions((ex as Error).message); - - // Report also on the test view. Getting root node is more complicated due to fact - // that in pytest project can be organized in many ways - testController.items.add( - createErrorTestItem(testController, { - id: `DiscoveryError:${workspace.uri.fsPath}`, - label: `Pytest Discovery Error [${path.basename(workspace.uri.fsPath)}]`, - error: util.format( - `${cancel} discovering pytest tests (see Output > Python):\r\n`, - message.length > 0 ? message : ex, - ), - }), - ); - - deferred.reject(ex as Error); - } finally { - // Discovery has finished running we have the raw test data at this point. - this.discovering.delete(workspace.uri.fsPath); - } - const root = rawTestData.length === 1 ? rawTestData[0].root : workspace.uri.fsPath; - const workspaceNode = testController.items.get(root); - if (workspaceNode) { - if (uri.fsPath === workspace.uri.fsPath) { - // this is a workspace level refresh - // This is an existing workspace test node. Just update the children - await this.resolveChildren(testController, workspaceNode, token); - } else { - // This is a child node refresh - const testNode = getNodeByUri(workspaceNode, uri); - if (testNode) { - // We found the node to update - await this.resolveChildren(testController, testNode, token); - } else { - // update the entire workspace tree - await this.resolveChildren(testController, workspaceNode, token); - } - } - } else if (rawTestData.length > 0) { - // This is a new workspace with tests. - const newItem = createWorkspaceRootTestItem(testController, this.idToRawData, { - id: root, - label: path.basename(root), - uri: Uri.file(root), - runId: root, - }); - testController.items.add(newItem); - - await this.resolveChildren(testController, newItem, token); - } - } - sendTelemetryEvent(EventName.UNITTEST_DISCOVERY_DONE, undefined, { tool: 'pytest', failed: false }); - return Promise.resolve(); - } - - public runTests(testRun: ITestRun, workspace: WorkspaceFolder, token: CancellationToken): Promise { - const settings = this.configService.getSettings(workspace.uri); - return this.runner.runTests( - testRun, - { - workspaceFolder: workspace.uri, - cwd: - settings.testing.cwd && settings.testing.cwd.length > 0 - ? settings.testing.cwd - : workspace.uri.fsPath, - token, - args: settings.testing.pytestArgs, - }, - this.idToRawData, - ); - } -} - -function getTestDiscoveryExceptions(content: string): string { - const lines = content.split(/\r?\n/g); - let start = false; - let exceptions = ''; - for (const line of lines) { - if (start) { - exceptions += `${line}\r\n`; - } else if (line.includes(' ERRORS ')) { - start = true; - } - } - return exceptions; } diff --git a/src/client/testing/testController/pytest/pytestDiscoveryAdapter.ts b/src/client/testing/testController/pytest/pytestDiscoveryAdapter.ts index 792826f4c3a5..16e27635e66c 100644 --- a/src/client/testing/testController/pytest/pytestDiscoveryAdapter.ts +++ b/src/client/testing/testController/pytest/pytestDiscoveryAdapter.ts @@ -1,90 +1,225 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. import * as path from 'path'; -import { Uri } from 'vscode'; +import { CancellationToken, Disposable, Uri } from 'vscode'; +import { ChildProcess } from 'child_process'; import { ExecutionFactoryCreateWithEnvironmentOptions, IPythonExecutionFactory, SpawnOptions, } from '../../../common/process/types'; -import { IConfigurationService, ITestOutputChannel } from '../../../common/types'; -import { createDeferred, Deferred } from '../../../common/utils/async'; +import { IConfigurationService } from '../../../common/types'; +import { Deferred } from '../../../common/utils/async'; import { EXTENSION_ROOT_DIR } from '../../../constants'; -import { traceVerbose } from '../../../logging'; -import { DataReceivedEvent, DiscoveredTestPayload, ITestDiscoveryAdapter, ITestServer } from '../common/types'; +import { traceError, traceInfo, traceVerbose } from '../../../logging'; +import { ITestDiscoveryAdapter, ITestResultResolver } from '../common/types'; +import { createTestingDeferred } from '../common/utils'; +import { IEnvironmentVariablesProvider } from '../../../common/variables/types'; +import { PythonEnvironment } from '../../../pythonEnvironments/info'; +import { useEnvExtension, getEnvironment, runInBackground } from '../../../envExt/api.internal'; +import { buildPytestEnv as configureSubprocessEnv, handleSymlinkAndRootDir } from './pytestHelpers'; +import { cleanupOnCancellation, createProcessHandlers, setupDiscoveryPipe } from '../common/discoveryHelpers'; +import { ProjectAdapter } from '../common/projectAdapter'; /** - * Wrapper class for unittest test discovery. This is where we call `runTestCommand`. #this seems incorrectly copied + * Configures the subprocess environment for pytest discovery. + * @param envVarsService Service to retrieve environment variables + * @param uri Workspace URI + * @param discoveryPipeName Name of the discovery pipe to pass to the subprocess + * @returns Configured environment variables for the subprocess */ -export class PytestTestDiscoveryAdapter implements ITestDiscoveryAdapter { - private promiseMap: Map> = new Map(); - - private deferred: Deferred | undefined; +async function configureDiscoveryEnv( + envVarsService: IEnvironmentVariablesProvider | undefined, + uri: Uri, + discoveryPipeName: string, +): Promise { + const fullPluginPath = path.join(EXTENSION_ROOT_DIR, 'python_files'); + const envVars = await envVarsService?.getEnvironmentVariables(uri); + const mutableEnv = configureSubprocessEnv(envVars, fullPluginPath, discoveryPipeName); + return mutableEnv; +} +/** + * Wrapper class for pytest test discovery. This is where we call the pytest subprocess. + */ +export class PytestTestDiscoveryAdapter implements ITestDiscoveryAdapter { constructor( - public testServer: ITestServer, public configSettings: IConfigurationService, - private readonly outputChannel: ITestOutputChannel, - ) { - testServer.onDataReceived(this.onDataReceivedHandler, this); - } + private readonly resultResolver?: ITestResultResolver, + private readonly envVarsService?: IEnvironmentVariablesProvider, + ) {} + + async discoverTests( + uri: Uri, + executionFactory: IPythonExecutionFactory, + token?: CancellationToken, + interpreter?: PythonEnvironment, + project?: ProjectAdapter, + ): Promise { + // Setup discovery pipe and cancellation + const { + pipeName: discoveryPipeName, + cancellation: discoveryPipeCancellation, + tokenDisposable, + } = await setupDiscoveryPipe(this.resultResolver, token, uri); - public onDataReceivedHandler({ uuid, data }: DataReceivedEvent): void { - const deferred = this.promiseMap.get(uuid); - if (deferred) { - deferred.resolve(JSON.parse(data)); - this.promiseMap.delete(uuid); + // Setup process handlers deferred (used by both execution paths) + const deferredTillExecClose: Deferred = createTestingDeferred(); + + // Collect all disposables related to discovery to handle cleanup in finally block + const disposables: Disposable[] = []; + if (tokenDisposable) { + disposables.push(tokenDisposable); } - } - discoverTests(uri: Uri, executionFactory?: IPythonExecutionFactory): Promise { - if (executionFactory !== undefined) { - // ** new version of discover tests. + try { + // Build pytest command and arguments const settings = this.configSettings.getSettings(uri); - const { pytestArgs } = settings.testing; - traceVerbose(pytestArgs); - return this.runPytestDiscovery(uri, executionFactory); - } - // if executionFactory is undefined, we are using the old method signature of discover tests. - traceVerbose(uri); - this.deferred = createDeferred(); - return this.deferred.promise; - } + let { pytestArgs } = settings.testing; + const cwd = settings.testing.cwd && settings.testing.cwd.length > 0 ? settings.testing.cwd : uri.fsPath; + pytestArgs = await handleSymlinkAndRootDir(cwd, pytestArgs); + + // Add --ignore flags for nested projects to prevent duplicate discovery + if (project?.nestedProjectPathsToIgnore?.length) { + const ignoreArgs = project.nestedProjectPathsToIgnore.map((nestedPath) => `--ignore=${nestedPath}`); + pytestArgs = [...pytestArgs, ...ignoreArgs]; + traceInfo( + `[test-by-project] Project ${project.projectName} ignoring nested project(s): ${ignoreArgs.join( + ' ', + )}`, + ); + } + + const commandArgs = ['-m', 'pytest', '-p', 'vscode_pytest', '--collect-only'].concat(pytestArgs); + traceVerbose( + `Running pytest discovery with command: ${commandArgs.join(' ')} for workspace ${uri.fsPath}.`, + ); + + // Configure subprocess environment + const mutableEnv = await configureDiscoveryEnv(this.envVarsService, uri, discoveryPipeName); - async runPytestDiscovery(uri: Uri, executionFactory: IPythonExecutionFactory): Promise { - const deferred = createDeferred(); - const relativePathToPytest = 'pythonFiles'; - const fullPluginPath = path.join(EXTENSION_ROOT_DIR, relativePathToPytest); - const uuid = this.testServer.createUUID(uri.fsPath); - this.promiseMap.set(uuid, deferred); - const settings = this.configSettings.getSettings(uri); - const { pytestArgs } = settings.testing; - - const pythonPathParts: string[] = process.env.PYTHONPATH?.split(path.delimiter) ?? []; - const pythonPathCommand = [fullPluginPath, ...pythonPathParts].join(path.delimiter); - - const spawnOptions: SpawnOptions = { - cwd: uri.fsPath, - throwOnStdErr: true, - extraVariables: { - PYTHONPATH: pythonPathCommand, - TEST_UUID: uuid.toString(), - TEST_PORT: this.testServer.getPort().toString(), - }, - outputChannel: this.outputChannel, - }; - - // Create the Python environment in which to execute the command. - const creationOptions: ExecutionFactoryCreateWithEnvironmentOptions = { - allowEnvironmentFetchExceptions: false, - resource: uri, - }; - const execService = await executionFactory.createActivatedEnvironment(creationOptions); - execService - .exec(['-m', 'pytest', '-p', 'vscode_pytest', '--collect-only'].concat(pytestArgs), spawnOptions) - .catch((ex) => { - deferred.reject(ex as Error); + // Set PROJECT_ROOT_PATH for project-based testing (tells Python where to root the test tree) + if (project) { + mutableEnv.PROJECT_ROOT_PATH = project.projectUri.fsPath; + } + + // Setup process handlers (shared by both execution paths) + const handlers = createProcessHandlers('pytest', uri, cwd, this.resultResolver, deferredTillExecClose, [5]); + + // Execute using environment extension if available + if (useEnvExtension()) { + traceInfo(`Using environment extension for pytest discovery in workspace ${uri.fsPath}`); + const pythonEnv = project?.pythonEnvironment ?? (await getEnvironment(uri)); + if (!pythonEnv) { + traceError( + `Python environment not found for workspace ${uri.fsPath}. Cannot proceed with test discovery.`, + ); + deferredTillExecClose.resolve(); + return; + } + traceVerbose(`Using Python environment: ${JSON.stringify(pythonEnv)}`); + + const proc = await runInBackground(pythonEnv, { + cwd, + args: commandArgs, + env: (mutableEnv as unknown) as { [key: string]: string }, + }); + traceInfo(`Started pytest discovery subprocess (environment extension) for workspace ${uri.fsPath}`); + + // Wire up cancellation and process events + const envExtCancellationHandler = token?.onCancellationRequested(() => { + cleanupOnCancellation('pytest', proc, deferredTillExecClose, discoveryPipeCancellation, uri); + }); + if (envExtCancellationHandler) { + disposables.push(envExtCancellationHandler); + } + proc.stdout.on('data', handlers.onStdout); + proc.stderr.on('data', handlers.onStderr); + proc.onExit((code, signal) => { + handlers.onExit(code, signal); + handlers.onClose(code, signal); + }); + + await deferredTillExecClose.promise; + traceInfo(`Pytest discovery completed for workspace ${uri.fsPath}`); + return; + } + + // Execute using execution factory (fallback path) + traceInfo(`Using execution factory for pytest discovery in workspace ${uri.fsPath}`); + const creationOptions: ExecutionFactoryCreateWithEnvironmentOptions = { + allowEnvironmentFetchExceptions: false, + resource: uri, + interpreter, + }; + const execService = await executionFactory.createActivatedEnvironment(creationOptions); + if (!execService) { + traceError( + `Failed to create execution service for workspace ${uri.fsPath}. Cannot proceed with test discovery.`, + ); + deferredTillExecClose.resolve(); + return; + } + const execInfo = await execService.getExecutablePath(); + traceVerbose(`Using Python executable: ${execInfo} for workspace ${uri.fsPath}`); + + // Check for cancellation before spawning process + if (token?.isCancellationRequested) { + traceInfo(`Pytest discovery cancelled before spawning process for workspace ${uri.fsPath}`); + deferredTillExecClose.resolve(); + return; + } + + const spawnOptions: SpawnOptions = { + cwd, + throwOnStdErr: true, + env: mutableEnv, + token, + }; + + let resultProc: ChildProcess | undefined; + + // Set up cancellation handler after all early return checks + const cancellationHandler = token?.onCancellationRequested(() => { + traceInfo(`Cancellation requested during pytest discovery for workspace ${uri.fsPath}`); + cleanupOnCancellation('pytest', resultProc, deferredTillExecClose, discoveryPipeCancellation, uri); }); - return deferred.promise; + if (cancellationHandler) { + disposables.push(cancellationHandler); + } + + try { + const result = execService.execObservable(commandArgs, spawnOptions); + resultProc = result?.proc; + + if (!resultProc) { + traceError(`Failed to spawn pytest discovery subprocess for workspace ${uri.fsPath}`); + deferredTillExecClose.resolve(); + return; + } + traceInfo(`Started pytest discovery subprocess (execution factory) for workspace ${uri.fsPath}`); + } catch (error) { + traceError(`Error spawning pytest discovery subprocess for workspace ${uri.fsPath}: ${error}`); + deferredTillExecClose.resolve(); + throw error; + } + resultProc.stdout?.on('data', handlers.onStdout); + resultProc.stderr?.on('data', handlers.onStderr); + resultProc.on('exit', handlers.onExit); + resultProc.on('close', handlers.onClose); + + traceVerbose(`Waiting for pytest discovery subprocess to complete for workspace ${uri.fsPath}`); + await deferredTillExecClose.promise; + traceInfo(`Pytest discovery completed for workspace ${uri.fsPath}`); + } catch (error) { + traceError(`Error during pytest discovery for workspace ${uri.fsPath}: ${error}`); + deferredTillExecClose.resolve(); + throw error; + } finally { + // Dispose all cancellation handlers and event subscriptions + disposables.forEach((d) => d.dispose()); + // Dispose the discovery pipe cancellation token + discoveryPipeCancellation.dispose(); + } } } diff --git a/src/client/testing/testController/pytest/pytestExecutionAdapter.ts b/src/client/testing/testController/pytest/pytestExecutionAdapter.ts index d2cbd3151e6f..102841c2e2dd 100644 --- a/src/client/testing/testController/pytest/pytestExecutionAdapter.ts +++ b/src/client/testing/testController/pytest/pytestExecutionAdapter.ts @@ -1,126 +1,304 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -import { Uri } from 'vscode'; +import { CancellationTokenSource, DebugSessionOptions, TestRun, TestRunProfileKind, Uri } from 'vscode'; import * as path from 'path'; -import { IConfigurationService, ITestOutputChannel } from '../../../common/types'; -import { createDeferred, Deferred } from '../../../common/utils/async'; -import { traceVerbose } from '../../../logging'; -import { DataReceivedEvent, ExecutionTestPayload, ITestExecutionAdapter, ITestServer } from '../common/types'; +import { ChildProcess } from 'child_process'; +import { IConfigurationService } from '../../../common/types'; +import { Deferred } from '../../../common/utils/async'; +import { traceError, traceInfo, traceVerbose } from '../../../logging'; +import { ExecutionTestPayload, ITestExecutionAdapter, ITestResultResolver } from '../common/types'; import { ExecutionFactoryCreateWithEnvironmentOptions, IPythonExecutionFactory, SpawnOptions, } from '../../../common/process/types'; -import { EXTENSION_ROOT_DIR } from '../../../constants'; import { removePositionalFoldersAndFiles } from './arguments'; - -// eslint-disable-next-line @typescript-eslint/no-explicit-any -(global as any).EXTENSION_ROOT_DIR = EXTENSION_ROOT_DIR; -/** - * Wrapper Class for pytest test execution. This is where we call `runTestCommand`? - */ +import { ITestDebugLauncher, LaunchOptions } from '../../common/types'; +import { PYTEST_PROVIDER } from '../../common/constants'; +import { EXTENSION_ROOT_DIR } from '../../../common/constants'; +import * as utils from '../common/utils'; +import { IEnvironmentVariablesProvider } from '../../../common/variables/types'; +import { PythonEnvironment } from '../../../pythonEnvironments/info'; +import { getEnvironment, runInBackground, useEnvExtension } from '../../../envExt/api.internal'; +import { ProjectAdapter } from '../common/projectAdapter'; export class PytestTestExecutionAdapter implements ITestExecutionAdapter { - private promiseMap: Map> = new Map(); - - private deferred: Deferred | undefined; - constructor( - public testServer: ITestServer, public configSettings: IConfigurationService, - private readonly outputChannel: ITestOutputChannel, - ) { - testServer.onDataReceived(this.onDataReceivedHandler, this); - } - - public onDataReceivedHandler({ uuid, data }: DataReceivedEvent): void { - const deferred = this.promiseMap.get(uuid); - if (deferred) { - deferred.resolve(JSON.parse(data)); - this.promiseMap.delete(uuid); - } - } + private readonly resultResolver?: ITestResultResolver, + private readonly envVarsService?: IEnvironmentVariablesProvider, + ) {} async runTests( uri: Uri, testIds: string[], - debugBool?: boolean, - executionFactory?: IPythonExecutionFactory, - ): Promise { - traceVerbose(uri, testIds, debugBool); - if (executionFactory !== undefined) { - // ** new version of run tests. - return this.runTestsNew(uri, testIds, debugBool, executionFactory); + profileKind: boolean | TestRunProfileKind | undefined, + runInstance: TestRun, + executionFactory: IPythonExecutionFactory, + debugLauncher?: ITestDebugLauncher, + interpreter?: PythonEnvironment, + project?: ProjectAdapter, + ): Promise { + const deferredTillServerClose: Deferred = utils.createTestingDeferred(); + + // create callback to handle data received on the named pipe + const dataReceivedCallback = (data: ExecutionTestPayload) => { + if (runInstance && !runInstance.token.isCancellationRequested) { + this.resultResolver?.resolveExecution(data, runInstance); + } else { + traceError(`No run instance found, cannot resolve execution, for workspace ${uri.fsPath}.`); + } + }; + const cSource = new CancellationTokenSource(); + runInstance.token.onCancellationRequested(() => cSource.cancel()); + + const name = await utils.startRunResultNamedPipe( + dataReceivedCallback, // callback to handle data received + deferredTillServerClose, // deferred to resolve when server closes + cSource.token, // token to cancel + ); + runInstance.token.onCancellationRequested(() => { + traceInfo(`Test run cancelled, resolving 'TillServerClose' deferred for ${uri.fsPath}.`); + }); + + try { + await this.runTestsNew( + uri, + testIds, + name, + cSource, + runInstance, + profileKind, + executionFactory, + debugLauncher, + interpreter, + project, + ); + } finally { + await deferredTillServerClose.promise; } - // if executionFactory is undefined, we are using the old method signature of run tests. - this.outputChannel.appendLine('Running tests.'); - this.deferred = createDeferred(); - return this.deferred.promise; } private async runTestsNew( uri: Uri, testIds: string[], - debugBool?: boolean, - executionFactory?: IPythonExecutionFactory, + resultNamedPipeName: string, + serverCancel: CancellationTokenSource, + runInstance: TestRun, + profileKind: boolean | TestRunProfileKind | undefined, + executionFactory: IPythonExecutionFactory, + debugLauncher?: ITestDebugLauncher, + interpreter?: PythonEnvironment, + project?: ProjectAdapter, ): Promise { - const deferred = createDeferred(); - const relativePathToPytest = 'pythonFiles'; + const relativePathToPytest = 'python_files'; const fullPluginPath = path.join(EXTENSION_ROOT_DIR, relativePathToPytest); - this.configSettings.isTestExecution(); - const uuid = this.testServer.createUUID(uri.fsPath); - this.promiseMap.set(uuid, deferred); const settings = this.configSettings.getSettings(uri); const { pytestArgs } = settings.testing; - - const pythonPathParts: string[] = process.env.PYTHONPATH?.split(path.delimiter) ?? []; + const cwd = settings.testing.cwd && settings.testing.cwd.length > 0 ? settings.testing.cwd : uri.fsPath; + // get and edit env vars + const mutableEnv = { + ...(await this.envVarsService?.getEnvironmentVariables(uri)), + }; + // get python path from mutable env, it contains process.env as well + const pythonPathParts: string[] = mutableEnv.PYTHONPATH?.split(path.delimiter) ?? []; const pythonPathCommand = [fullPluginPath, ...pythonPathParts].join(path.delimiter); + mutableEnv.PYTHONPATH = pythonPathCommand; + mutableEnv.TEST_RUN_PIPE = resultNamedPipeName; - const spawnOptions: SpawnOptions = { - cwd: uri.fsPath, - throwOnStdErr: true, - extraVariables: { - PYTHONPATH: pythonPathCommand, - TEST_UUID: uuid.toString(), - TEST_PORT: this.testServer.getPort().toString(), - }, - outputChannel: this.outputChannel, - }; + // Set PROJECT_ROOT_PATH for project-based testing (tells Python where to root the test tree) + if (project) { + mutableEnv.PROJECT_ROOT_PATH = project.projectUri.fsPath; + traceInfo(`[test-by-project] Setting PROJECT_ROOT_PATH=${project.projectUri.fsPath} for pytest execution`); + } + + if (profileKind && profileKind === TestRunProfileKind.Coverage) { + mutableEnv.COVERAGE_ENABLED = 'True'; + } + + const debugBool = profileKind && profileKind === TestRunProfileKind.Debug; // Create the Python environment in which to execute the command. const creationOptions: ExecutionFactoryCreateWithEnvironmentOptions = { allowEnvironmentFetchExceptions: false, resource: uri, + interpreter, }; // need to check what will happen in the exec service is NOT defined and is null - const execService = await executionFactory?.createActivatedEnvironment(creationOptions); + const execService = await executionFactory.createActivatedEnvironment(creationOptions); + + const execInfo = await execService?.getExecutablePath(); + traceVerbose(`Executable path for pytest execution: ${execInfo}.`); try { // Remove positional test folders and files, we will add as needed per node - const testArgs = removePositionalFoldersAndFiles(pytestArgs); + let testArgs = removePositionalFoldersAndFiles(pytestArgs); // if user has provided `--rootdir` then use that, otherwise add `cwd` - if (testArgs.filter((a) => a.startsWith('--rootdir')).length === 0) { - // Make sure root dir is set so pytest can find the relative paths - testArgs.splice(0, 0, '--rootdir', uri.fsPath); - } + // root dir is required so pytest can find the relative paths and for symlinks + utils.addValueIfKeyNotExist(testArgs, '--rootdir', cwd); - if (debugBool && !testArgs.some((a) => a.startsWith('--capture') || a === '-s')) { - testArgs.push('--capture', 'no'); + // -s and --capture are both command line options that control how pytest captures output. + // if neither are set, then set --capture=no to prevent pytest from capturing output. + if (debugBool && !utils.argKeyExists(testArgs, '-s')) { + testArgs = utils.addValueIfKeyNotExist(testArgs, '--capture', 'no'); } - console.debug(`Running test with arguments: ${testArgs.join(' ')}\r\n`); - console.debug(`Current working directory: ${uri.fsPath}\r\n`); + // create a file with the test ids and set the environment variable to the file name + const testIdsFileName = await utils.writeTestIdsFile(testIds); + mutableEnv.RUN_TEST_IDS_PIPE = testIdsFileName; + traceInfo( + `Environment variables set for pytest execution: PYTHONPATH=${mutableEnv.PYTHONPATH}, TEST_RUN_PIPE=${mutableEnv.TEST_RUN_PIPE}, RUN_TEST_IDS_PIPE=${mutableEnv.RUN_TEST_IDS_PIPE}`, + ); + + const spawnOptions: SpawnOptions = { + cwd, + throwOnStdErr: true, + env: mutableEnv, + token: runInstance.token, + }; + + if (debugBool) { + const launchOptions: LaunchOptions = { + cwd, + args: testArgs, + token: runInstance.token, + testProvider: PYTEST_PROVIDER, + runTestIdsPort: testIdsFileName, + pytestPort: resultNamedPipeName, + // Pass project for project-based debugging (Python path and session name derived from this) + project: project?.pythonProject, + }; + const sessionOptions: DebugSessionOptions = { + testRun: runInstance, + }; + traceInfo(`Running DEBUG pytest with arguments: ${testArgs} for workspace ${uri.fsPath} \r\n`); + await debugLauncher!.launchDebugger( + launchOptions, + () => { + serverCancel.cancel(); + }, + sessionOptions, + ); + } else if (useEnvExtension()) { + // For project-based execution, use the project's Python environment + // Otherwise, fall back to getting the environment from the URI + const pythonEnv = project?.pythonEnvironment ?? (await getEnvironment(uri)); + if (pythonEnv) { + const deferredTillExecClose: Deferred = utils.createTestingDeferred(); + + const scriptPath = path.join(fullPluginPath, 'vscode_pytest', 'run_pytest_script.py'); + const runArgs = [scriptPath, ...testArgs]; + traceInfo(`Running pytest with arguments: ${runArgs.join(' ')} for workspace ${uri.fsPath} \r\n`); + + const proc = await runInBackground(pythonEnv, { + cwd, + args: runArgs, + env: (mutableEnv as unknown) as { [key: string]: string }, + }); + runInstance.token.onCancellationRequested(() => { + traceInfo(`Test run cancelled, killing pytest subprocess for workspace ${uri.fsPath}`); + proc.kill(); + deferredTillExecClose.resolve(); + serverCancel.cancel(); + }); + proc.stdout.on('data', (data) => { + const out = utils.fixLogLinesNoTrailing(data.toString()); + runInstance.appendOutput(out); + }); + proc.stderr.on('data', (data) => { + const out = utils.fixLogLinesNoTrailing(data.toString()); + runInstance.appendOutput(out); + }); + proc.onExit((code, signal) => { + if (code !== 0) { + traceError( + `Subprocess exited unsuccessfully with exit code ${code} and signal ${signal} on workspace ${uri.fsPath}`, + ); + } + deferredTillExecClose.resolve(); + serverCancel.cancel(); + }); + await deferredTillExecClose.promise; + } else { + traceError(`Python Environment not found for: ${uri.fsPath}`); + } + } else { + // deferredTillExecClose is resolved when all stdout and stderr is read + const deferredTillExecClose: Deferred = utils.createTestingDeferred(); + // combine path to run script with run args + const scriptPath = path.join(fullPluginPath, 'vscode_pytest', 'run_pytest_script.py'); + const runArgs = [scriptPath, ...testArgs]; + traceInfo(`Running pytest with arguments: ${runArgs.join(' ')} for workspace ${uri.fsPath} \r\n`); - const argArray = ['-m', 'pytest', '-p', 'vscode_pytest'].concat(testArgs).concat(testIds); - console.debug('argArray', argArray); - execService?.exec(argArray, spawnOptions); + let resultProc: ChildProcess | undefined; + + runInstance.token.onCancellationRequested(() => { + traceInfo(`Test run cancelled, killing pytest subprocess for workspace ${uri.fsPath}`); + // if the resultProc exists just call kill on it which will handle resolving the ExecClose deferred, otherwise resolve the deferred here. + if (resultProc) { + resultProc?.kill(); + } else { + deferredTillExecClose.resolve(); + serverCancel.cancel(); + } + }); + + const result = execService?.execObservable(runArgs, spawnOptions); + + // Take all output from the subprocess and add it to the test output channel. This will be the pytest output. + // Displays output to user and ensure the subprocess doesn't run into buffer overflow. + result?.proc?.stdout?.on('data', (data) => { + const out = utils.fixLogLinesNoTrailing(data.toString()); + runInstance.appendOutput(out); + }); + result?.proc?.stderr?.on('data', (data) => { + const out = utils.fixLogLinesNoTrailing(data.toString()); + runInstance.appendOutput(out); + }); + result?.proc?.on('exit', (code, signal) => { + if (code !== 0) { + traceError( + `Subprocess exited unsuccessfully with exit code ${code} and signal ${signal} on workspace ${uri.fsPath}`, + ); + } + }); + + result?.proc?.on('close', (code, signal) => { + traceVerbose('Test run finished, subprocess closed.'); + // if the child has testIds then this is a run request + // if the child process exited with a non-zero exit code, then we need to send the error payload. + if (code !== 0) { + traceError( + `Subprocess closed unsuccessfully with exit code ${code} and signal ${signal} for workspace ${uri.fsPath}. Creating and sending error execution payload \n`, + ); + + if (runInstance) { + this.resultResolver?.resolveExecution( + utils.createExecutionErrorPayload(code, signal, testIds, cwd), + runInstance, + ); + } + } + + // deferredTillEOT is resolved when all data sent on stdout and stderr is received, close event is only called when this occurs + // due to the sync reading of the output. + deferredTillExecClose.resolve(); + serverCancel.cancel(); + }); + await deferredTillExecClose.promise; + } } catch (ex) { - console.debug(`Error while running tests: ${testIds}\r\n${ex}\r\n\r\n`); + traceError(`Error while running tests for workspace ${uri}: ${testIds}\r\n${ex}\r\n\r\n`); return Promise.reject(ex); } - return deferred.promise; + const executionPayload: ExecutionTestPayload = { + cwd, + status: 'success', + error: '', + }; + return executionPayload; } } diff --git a/src/client/testing/testController/pytest/pytestHelpers.ts b/src/client/testing/testController/pytest/pytestHelpers.ts new file mode 100644 index 000000000000..c6e748fb85a7 --- /dev/null +++ b/src/client/testing/testController/pytest/pytestHelpers.ts @@ -0,0 +1,58 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +import * as path from 'path'; +import * as fs from 'fs'; +import { traceInfo, traceWarn } from '../../../logging'; +import { addValueIfKeyNotExist, hasSymlinkParent } from '../common/utils'; + +/** + * Checks if the current working directory contains a symlink and ensures --rootdir is set in pytest args. + * This is required for pytest to correctly resolve relative paths in symlinked directories. + */ +export async function handleSymlinkAndRootDir(cwd: string, pytestArgs: string[]): Promise { + const stats = await fs.promises.lstat(cwd); + const resolvedPath = await fs.promises.realpath(cwd); + let isSymbolicLink = false; + if (stats.isSymbolicLink()) { + isSymbolicLink = true; + traceWarn(`Working directory is a symbolic link: ${cwd} -> ${resolvedPath}`); + } else if (resolvedPath !== cwd) { + traceWarn( + `Working directory resolves to different path: ${cwd} -> ${resolvedPath}. Checking for symlinks in parent directories.`, + ); + isSymbolicLink = await hasSymlinkParent(cwd); + } + if (isSymbolicLink) { + traceWarn( + `Symlink detected in path. Adding '--rootdir=${cwd}' to pytest args to ensure correct path resolution.`, + ); + pytestArgs = addValueIfKeyNotExist(pytestArgs, '--rootdir', cwd); + } + // if user has provided `--rootdir` then use that, otherwise add `cwd` + // root dir is required so pytest can find the relative paths and for symlinks + pytestArgs = addValueIfKeyNotExist(pytestArgs, '--rootdir', cwd); + return pytestArgs; +} + +/** + * Builds the environment variables required for pytest discovery. + * Sets PYTHONPATH to include the plugin path and TEST_RUN_PIPE for communication. + */ +export function buildPytestEnv( + envVars: { [key: string]: string | undefined } | undefined, + fullPluginPath: string, + discoveryPipeName: string, +): { [key: string]: string | undefined } { + const mutableEnv = { + ...envVars, + }; + // get python path from mutable env, it contains process.env as well + const pythonPathParts: string[] = mutableEnv.PYTHONPATH?.split(path.delimiter) ?? []; + const pythonPathCommand = [fullPluginPath, ...pythonPathParts].join(path.delimiter); + mutableEnv.PYTHONPATH = pythonPathCommand; + mutableEnv.TEST_RUN_PIPE = discoveryPipeName; + traceInfo( + `Environment variables set for pytest discovery: PYTHONPATH=${mutableEnv.PYTHONPATH}, TEST_RUN_PIPE=${mutableEnv.TEST_RUN_PIPE}`, + ); + return mutableEnv; +} diff --git a/src/client/testing/testController/pytest/runner.ts b/src/client/testing/testController/pytest/runner.ts deleted file mode 100644 index 2c6cff724398..000000000000 --- a/src/client/testing/testController/pytest/runner.ts +++ /dev/null @@ -1,143 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -import { inject, injectable } from 'inversify'; -import { Disposable, TestItem, TestRun, TestRunProfileKind } from 'vscode'; -import { ITestOutputChannel } from '../../../common/types'; -import { PYTEST_PROVIDER } from '../../common/constants'; -import { ITestDebugLauncher, ITestRunner, LaunchOptions, Options } from '../../common/types'; -import { filterArguments, getOptionValues } from '../common/argumentsHelper'; -import { createTemporaryFile } from '../common/externalDependencies'; -import { updateResultFromJunitXml } from '../common/resultsHelper'; -import { getTestCaseNodes } from '../common/testItemUtilities'; -import { ITestRun, ITestsRunner, TestData, TestRunInstanceOptions, TestRunOptions } from '../common/types'; -import { removePositionalFoldersAndFiles } from './arguments'; - -const JunitXmlArgOld = '--junitxml'; -const JunitXmlArg = '--junit-xml'; - -async function getPytestJunitXmlTempFile(args: string[], disposables: Disposable[]): Promise { - const argValues = getOptionValues(args, JunitXmlArg); - if (argValues.length === 1) { - return argValues[0]; - } - const tempFile = await createTemporaryFile('.xml'); - disposables.push(tempFile); - return tempFile.filePath; -} - -@injectable() -export class PytestRunner implements ITestsRunner { - constructor( - @inject(ITestRunner) private readonly runner: ITestRunner, - @inject(ITestDebugLauncher) private readonly debugLauncher: ITestDebugLauncher, - @inject(ITestOutputChannel) private readonly outputChannel: ITestOutputChannel, - ) {} - - public async runTests( - testRun: ITestRun, - options: TestRunOptions, - idToRawData: Map, - ): Promise { - const runOptions: TestRunInstanceOptions = { - ...options, - exclude: testRun.excludes, - debug: testRun.runKind === TestRunProfileKind.Debug, - }; - - try { - await Promise.all( - testRun.includes.map((testNode) => - this.runTest(testNode, testRun.runInstance, runOptions, idToRawData), - ), - ); - } catch (ex) { - testRun.runInstance.appendOutput(`Error while running tests:\r\n${ex}\r\n\r\n`); - } - } - - private async runTest( - testNode: TestItem, - runInstance: TestRun, - options: TestRunInstanceOptions, - idToRawData: Map, - ): Promise { - runInstance.appendOutput(`Running tests (pytest): ${testNode.id}\r\n`); - - // VS Code API requires that we set the run state on the leaf nodes. The state of the - // parent nodes are computed based on the state of child nodes. - const testCaseNodes = getTestCaseNodes(testNode); - testCaseNodes.forEach((node) => runInstance.started(node)); - - // For pytest we currently use JUnit XML to get the results. We create a temporary file here - // to ensure that the file is removed when we are done reading the result. - const disposables: Disposable[] = []; - const junitFilePath = await getPytestJunitXmlTempFile(options.args, disposables); - - try { - // Remove positional test folders and files, we will add as needed per node - let testArgs = removePositionalFoldersAndFiles(options.args); - - // Remove the '--junitxml' or '--junit-xml' if it exists, and add it with our path. - testArgs = filterArguments(testArgs, [JunitXmlArg, JunitXmlArgOld]); - testArgs.splice(0, 0, `${JunitXmlArg}=${junitFilePath}`); - - // Ensure that we use the xunit1 format. - testArgs.splice(0, 0, '--override-ini', 'junit_family=xunit1'); - - // if user has provided `--rootdir` then use that, otherwise add `cwd` - if (testArgs.filter((a) => a.startsWith('--rootdir')).length === 0) { - // Make sure root dir is set so pytest can find the relative paths - testArgs.splice(0, 0, '--rootdir', options.cwd); - } - - if (options.debug && !testArgs.some((a) => a.startsWith('--capture') || a === '-s')) { - testArgs.push('--capture', 'no'); - } - - // Positional arguments control the tests to be run. - const rawData = idToRawData.get(testNode.id); - if (!rawData) { - throw new Error(`Trying to run unknown node: ${testNode.id}`); - } - if (testNode.id !== options.cwd) { - testArgs.push(rawData.rawId); - } - - runInstance.appendOutput(`Running test with arguments: ${testArgs.join(' ')}\r\n`); - runInstance.appendOutput(`Current working directory: ${options.cwd}\r\n`); - runInstance.appendOutput(`Workspace directory: ${options.workspaceFolder.fsPath}\r\n`); - - if (options.debug) { - const debuggerArgs = [options.cwd, 'pytest'].concat(testArgs); - const launchOptions: LaunchOptions = { - cwd: options.cwd, - args: debuggerArgs, - token: options.token, - outChannel: this.outputChannel, - testProvider: PYTEST_PROVIDER, - }; - await this.debugLauncher.launchDebugger(launchOptions); - } else { - const runOptions: Options = { - args: testArgs, - cwd: options.cwd, - outChannel: this.outputChannel, - token: options.token, - workspaceFolder: options.workspaceFolder, - }; - await this.runner.run(PYTEST_PROVIDER, runOptions); - } - - // At this point pytest has finished running, we now have to parse the output - runInstance.appendOutput(`Run completed, parsing output\r\n`); - await updateResultFromJunitXml(junitFilePath, testNode, runInstance, idToRawData); - } catch (ex) { - runInstance.appendOutput(`Error while running tests: ${testNode.label}\r\n${ex}\r\n\r\n`); - return Promise.reject(ex); - } finally { - disposables.forEach((d) => d.dispose()); - } - return Promise.resolve(); - } -} diff --git a/src/client/testing/testController/serviceRegistry.ts b/src/client/testing/testController/serviceRegistry.ts index 840eb14b1f27..03bf883e8eb1 100644 --- a/src/client/testing/testController/serviceRegistry.ts +++ b/src/client/testing/testController/serviceRegistry.ts @@ -4,26 +4,19 @@ import { IExtensionSingleActivationService } from '../../activation/types'; import { IServiceManager } from '../../ioc/types'; import { PYTEST_PROVIDER, UNITTEST_PROVIDER } from '../common/constants'; -import { TestDiscoveryHelper } from './common/discoveryHelper'; -import { ITestFrameworkController, ITestDiscoveryHelper, ITestsRunner, ITestController } from './common/types'; +import { ITestFrameworkController, ITestController } from './common/types'; import { PythonTestController } from './controller'; import { PytestController } from './pytest/pytestController'; -import { PytestRunner } from './pytest/runner'; -import { UnittestRunner } from './unittest/runner'; import { UnittestController } from './unittest/unittestController'; export function registerTestControllerTypes(serviceManager: IServiceManager): void { - serviceManager.addSingleton(ITestDiscoveryHelper, TestDiscoveryHelper); - serviceManager.addSingleton(ITestFrameworkController, PytestController, PYTEST_PROVIDER); - serviceManager.addSingleton(ITestsRunner, PytestRunner, PYTEST_PROVIDER); serviceManager.addSingleton( ITestFrameworkController, UnittestController, UNITTEST_PROVIDER, ); - serviceManager.addSingleton(ITestsRunner, UnittestRunner, UNITTEST_PROVIDER); serviceManager.addSingleton(ITestController, PythonTestController); serviceManager.addBinding(ITestController, IExtensionSingleActivationService); } diff --git a/src/client/testing/testController/unittest/arguments.ts b/src/client/testing/testController/unittest/arguments.ts deleted file mode 100644 index caff87999f6e..000000000000 --- a/src/client/testing/testController/unittest/arguments.ts +++ /dev/null @@ -1,103 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -import { TestFilter } from '../../common/types'; -import { filterArguments, getOptionValues, getPositionalArguments } from '../common/argumentsHelper'; - -const OptionsWithArguments = ['-k', '-p', '-s', '-t', '--pattern', '--start-directory', '--top-level-directory']; - -const OptionsWithoutArguments = [ - '-b', - '-c', - '-f', - '-h', - '-q', - '-v', - '--buffer', - '--catch', - '--failfast', - '--help', - '--locals', - '--quiet', - '--verbose', -]; - -export function unittestFilterArguments(args: string[], argumentToRemoveOrFilter: string[] | TestFilter): string[] { - const optionsWithoutArgsToRemove: string[] = []; - const optionsWithArgsToRemove: string[] = []; - // Positional arguments in pytest positional args are test directories and files. - // So if we want to run a specific test, then remove positional args. - let removePositionalArgs = false; - if (Array.isArray(argumentToRemoveOrFilter)) { - argumentToRemoveOrFilter.forEach((item) => { - if (OptionsWithArguments.indexOf(item) >= 0) { - optionsWithArgsToRemove.push(item); - } - if (OptionsWithoutArguments.indexOf(item) >= 0) { - optionsWithoutArgsToRemove.push(item); - } - }); - } else { - removePositionalArgs = true; - } - - let filteredArgs = args.slice(); - if (removePositionalArgs) { - const positionalArgs = getPositionalArguments(filteredArgs, OptionsWithArguments, OptionsWithoutArguments); - filteredArgs = filteredArgs.filter((item) => positionalArgs.indexOf(item) === -1); - } - return filterArguments(filteredArgs, optionsWithArgsToRemove, optionsWithoutArgsToRemove); -} - -export function unittestGetTestFolders(args: string[]): string[] { - const shortValue = getOptionValues(args, '-s'); - if (shortValue.length === 1) { - return shortValue; - } - const longValue = getOptionValues(args, '--start-directory'); - if (longValue.length === 1) { - return longValue; - } - return ['.']; -} - -export function unittestGetTestPattern(args: string[]): string { - const shortValue = getOptionValues(args, '-p'); - if (shortValue.length === 1) { - return shortValue[0]; - } - const longValue = getOptionValues(args, '--pattern'); - if (longValue.length === 1) { - return longValue[0]; - } - return 'test*.py'; -} - -export function unittestGetTopLevelDirectory(args: string[]): string | null { - const shortValue = getOptionValues(args, '-t'); - if (shortValue.length === 1) { - return shortValue[0]; - } - const longValue = getOptionValues(args, '--top-level-directory'); - if (longValue.length === 1) { - return longValue[0]; - } - return null; -} - -export function getTestRunArgs(args: string[]): string[] { - const startTestDiscoveryDirectory = unittestGetTestFolders(args)[0]; - const pattern = unittestGetTestPattern(args); - const topLevelDir = unittestGetTopLevelDirectory(args); - - const failFast = args.some((arg) => arg.trim() === '-f' || arg.trim() === '--failfast'); - const verbosity = args.some((arg) => arg.trim().indexOf('-v') === 0) ? 2 : 1; - const testArgs = [`--us=${startTestDiscoveryDirectory}`, `--up=${pattern}`, `--uvInt=${verbosity}`]; - if (topLevelDir) { - testArgs.push(`--ut=${topLevelDir}`); - } - if (failFast) { - testArgs.push('--uf'); - } - return testArgs; -} diff --git a/src/client/testing/testController/unittest/runner.ts b/src/client/testing/testController/unittest/runner.ts deleted file mode 100644 index d558f051eccb..000000000000 --- a/src/client/testing/testController/unittest/runner.ts +++ /dev/null @@ -1,317 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -import { injectable, inject } from 'inversify'; -import { Location, TestController, TestItem, TestMessage, TestRun, TestRunProfileKind } from 'vscode'; -import * as internalScripts from '../../../common/process/internal/scripts'; -import { splitLines } from '../../../common/stringUtils'; -import { ITestOutputChannel } from '../../../common/types'; -import { noop } from '../../../common/utils/misc'; -import { traceError, traceVerbose } from '../../../logging'; -import { UNITTEST_PROVIDER } from '../../common/constants'; -import { ITestRunner, ITestDebugLauncher, IUnitTestSocketServer, LaunchOptions, Options } from '../../common/types'; -import { clearAllChildren, getTestCaseNodes } from '../common/testItemUtilities'; -import { ITestRun, ITestsRunner, TestData, TestRunInstanceOptions, TestRunOptions } from '../common/types'; -import { fixLogLines } from '../common/utils'; -import { getTestRunArgs } from './arguments'; - -interface ITestData { - test: string; - message: string; - outcome: string; - traceback: string; - subtest?: string; -} - -function getTracebackForOutput(traceback: string): string { - return splitLines(traceback, { trim: false, removeEmptyEntries: true }).join('\r\n'); -} - -@injectable() -export class UnittestRunner implements ITestsRunner { - constructor( - @inject(ITestRunner) private readonly runner: ITestRunner, - @inject(ITestDebugLauncher) private readonly debugLauncher: ITestDebugLauncher, - @inject(ITestOutputChannel) private readonly outputChannel: ITestOutputChannel, - @inject(IUnitTestSocketServer) private readonly server: IUnitTestSocketServer, - ) {} - - public async runTests( - testRun: ITestRun, - options: TestRunOptions, - idToRawData: Map, - testController?: TestController, - ): Promise { - const runOptions: TestRunInstanceOptions = { - ...options, - exclude: testRun.excludes, - debug: testRun.runKind === TestRunProfileKind.Debug, - }; - - try { - await this.runTest(testRun.includes, testRun.runInstance, runOptions, idToRawData, testController); - } catch (ex) { - testRun.runInstance.appendOutput(`Error while running tests:\r\n${ex}\r\n\r\n`); - } - } - - private async runTest( - testNodes: readonly TestItem[], - runInstance: TestRun, - options: TestRunInstanceOptions, - idToRawData: Map, - testController?: TestController, - ): Promise { - runInstance.appendOutput(`Running tests (unittest): ${testNodes.map((t) => t.id).join(' ; ')}\r\n`); - const testCaseNodes: TestItem[] = []; - const fileToTestCases: Map = new Map(); - - testNodes.forEach((t) => { - const nodes = getTestCaseNodes(t); - nodes.forEach((n) => { - if (n.uri) { - const fsRunIds = fileToTestCases.get(n.uri.fsPath); - if (fsRunIds) { - fsRunIds.push(n); - } else { - fileToTestCases.set(n.uri.fsPath, [n]); - } - } - }); - testCaseNodes.push(...nodes); - }); - - const tested: string[] = []; - - const counts = { - total: 0, - passed: 0, - skipped: 0, - errored: 0, - failed: 0, - }; - const subTestStats: Map = new Map(); - - let failFast = false; - let stopTesting = false; - this.server.on('error', (message: string, ...data: string[]) => { - traceError(`${message} ${data.join(' ')}`); - }); - this.server.on('log', (message: string, ...data: string[]) => { - traceVerbose(`${message} ${data.join(' ')}`); - }); - this.server.on('connect', noop); - this.server.on('start', noop); - this.server.on('result', (data: ITestData) => { - const testCase = testCaseNodes.find((node) => idToRawData.get(node.id)?.runId === data.test); - const rawTestCase = idToRawData.get(testCase?.id ?? ''); - if (testCase && rawTestCase) { - counts.total += 1; - tested.push(rawTestCase.runId); - - if (data.outcome === 'passed' || data.outcome === 'failed-expected') { - const text = `${rawTestCase.rawId} Passed\r\n`; - runInstance.passed(testCase); - runInstance.appendOutput(fixLogLines(text)); - counts.passed += 1; - } else if (data.outcome === 'failed' || data.outcome === 'passed-unexpected') { - const traceback = data.traceback ? getTracebackForOutput(data.traceback) : ''; - const text = `${rawTestCase.rawId} Failed: ${data.message ?? data.outcome}\r\n${traceback}\r\n`; - const message = new TestMessage(text); - - if (testCase.uri && testCase.range) { - message.location = new Location(testCase.uri, testCase.range); - } - - runInstance.failed(testCase, message); - runInstance.appendOutput(fixLogLines(text)); - counts.failed += 1; - if (failFast) { - stopTesting = true; - } - } else if (data.outcome === 'error') { - const traceback = data.traceback ? getTracebackForOutput(data.traceback) : ''; - const text = `${rawTestCase.rawId} Failed with Error: ${data.message}\r\n${traceback}\r\n`; - const message = new TestMessage(text); - - if (testCase.uri && testCase.range) { - message.location = new Location(testCase.uri, testCase.range); - } - - runInstance.errored(testCase, message); - runInstance.appendOutput(fixLogLines(text)); - counts.errored += 1; - if (failFast) { - stopTesting = true; - } - } else if (data.outcome === 'skipped') { - const traceback = data.traceback ? getTracebackForOutput(data.traceback) : ''; - const text = `${rawTestCase.rawId} Skipped: ${data.message}\r\n${traceback}\r\n`; - runInstance.skipped(testCase); - runInstance.appendOutput(fixLogLines(text)); - counts.skipped += 1; - } else if (data.outcome === 'subtest-passed') { - const sub = subTestStats.get(data.test); - if (sub) { - sub.passed += 1; - } else { - counts.passed += 1; - subTestStats.set(data.test, { passed: 1, failed: 0 }); - runInstance.appendOutput(fixLogLines(`${rawTestCase.rawId} [subtests]:\r\n`)); - - // We are seeing the first subtest for this node. Clear all other nodes under it - // because we have no way to detect these at discovery, they can always be different - // for each run. - clearAllChildren(testCase); - } - if (data.subtest) { - runInstance.appendOutput(fixLogLines(`${data.subtest} Passed\r\n`)); - - // This is a runtime only node for unittest subtest, since they can only be detected - // at runtime. So, create a fresh one for each result. - const subtest = testController?.createTestItem(data.subtest, data.subtest); - if (subtest) { - testCase.children.add(subtest); - runInstance.started(subtest); - runInstance.passed(subtest); - } - } - } else if (data.outcome === 'subtest-failed') { - const sub = subTestStats.get(data.test); - if (sub) { - sub.failed += 1; - } else { - counts.failed += 1; - subTestStats.set(data.test, { passed: 0, failed: 1 }); - - runInstance.appendOutput(fixLogLines(`${rawTestCase.rawId} [subtests]:\r\n`)); - - // We are seeing the first subtest for this node. Clear all other nodes under it - // because we have no way to detect these at discovery, they can always be different - // for each run. - clearAllChildren(testCase); - } - - if (data.subtest) { - runInstance.appendOutput(fixLogLines(`${data.subtest} Failed\r\n`)); - const traceback = data.traceback ? getTracebackForOutput(data.traceback) : ''; - const text = `${data.subtest} Failed: ${data.message ?? data.outcome}\r\n${traceback}\r\n`; - runInstance.appendOutput(fixLogLines(text)); - - // This is a runtime only node for unittest subtest, since they can only be detected - // at runtime. So, create a fresh one for each result. - const subtest = testController?.createTestItem(data.subtest, data.subtest); - if (subtest) { - testCase.children.add(subtest); - runInstance.started(subtest); - const message = new TestMessage(text); - if (testCase.uri && testCase.range) { - message.location = new Location(testCase.uri, testCase.range); - } - - runInstance.failed(subtest, message); - } - } - } else { - const text = `Unknown outcome type for test ${rawTestCase.rawId}: ${data.outcome}`; - runInstance.appendOutput(fixLogLines(text)); - const message = new TestMessage(text); - if (testCase.uri && testCase.range) { - message.location = new Location(testCase.uri, testCase.range); - } - runInstance.errored(testCase, message); - } - } else if (data.outcome === 'error') { - const traceback = data.traceback ? getTracebackForOutput(data.traceback) : ''; - const text = `${data.test} Failed with Error: ${data.message}\r\n${traceback}\r\n`; - runInstance.appendOutput(fixLogLines(text)); - } - }); - - const port = await this.server.start(); - const runTestInternal = async (testFilePath: string, testRunIds: string[]): Promise => { - let testArgs = getTestRunArgs(options.args); - failFast = testArgs.indexOf('--uf') >= 0; - testArgs = testArgs.filter((arg) => arg !== '--uf'); - - testArgs.push(`--result-port=${port}`); - testRunIds.forEach((i) => testArgs.push(`-t${i}`)); - testArgs.push(`--testFile=${testFilePath}`); - - if (options.debug === true) { - testArgs.push('--debug'); - const launchOptions: LaunchOptions = { - cwd: options.cwd, - args: testArgs, - token: options.token, - outChannel: this.outputChannel, - testProvider: UNITTEST_PROVIDER, - }; - return this.debugLauncher.launchDebugger(launchOptions); - } - const args = internalScripts.visualstudio_py_testlauncher(testArgs); - - const runOptions: Options = { - args, - cwd: options.cwd, - outChannel: this.outputChannel, - token: options.token, - workspaceFolder: options.workspaceFolder, - }; - await this.runner.run(UNITTEST_PROVIDER, runOptions); - return Promise.resolve(); - }; - - try { - for (const testFile of fileToTestCases.keys()) { - if (stopTesting || options.token.isCancellationRequested) { - break; - } - - const nodes = fileToTestCases.get(testFile); - if (nodes) { - runInstance.appendOutput(`Running tests: ${nodes.map((n) => n.id).join('\r\n')}\r\n`); - const runIds: string[] = []; - nodes.forEach((n) => { - const rawNode = idToRawData.get(n.id); - if (rawNode) { - // VS Code API requires that we set the run state on the leaf nodes. The state of the - // parent nodes are computed based on the state of child nodes. - runInstance.started(n); - runIds.push(rawNode.runId); - } - }); - await runTestInternal(testFile, runIds); - } - } - } catch (ex) { - traceError(ex); - } finally { - this.server.removeAllListeners(); - this.server.stop(); - } - - runInstance.appendOutput(`Total number of tests expected to run: ${testCaseNodes.length}\r\n`); - runInstance.appendOutput(`Total number of tests run: ${counts.total}\r\n`); - runInstance.appendOutput(`Total number of tests passed: ${counts.passed}\r\n`); - runInstance.appendOutput(`Total number of tests failed: ${counts.failed}\r\n`); - runInstance.appendOutput(`Total number of tests failed with errors: ${counts.errored}\r\n`); - runInstance.appendOutput(`Total number of tests skipped: ${counts.skipped}\r\n\r\n`); - - if (subTestStats.size > 0) { - runInstance.appendOutput('Sub-test stats: \r\n'); - } - - subTestStats.forEach((v, k) => { - runInstance.appendOutput( - `Sub-tests for [${k}]: Total=${v.passed + v.failed} Passed=${v.passed} Failed=${v.failed}\r\n\r\n`, - ); - }); - - if (failFast) { - runInstance.appendOutput( - `Total number of tests skipped due to fail fast: ${counts.total - tested.length}\r\n`, - ); - } - } -} diff --git a/src/client/testing/testController/unittest/testDiscoveryAdapter.ts b/src/client/testing/testController/unittest/testDiscoveryAdapter.ts index 3f8ecb5797d3..558e01f3514d 100644 --- a/src/client/testing/testController/unittest/testDiscoveryAdapter.ts +++ b/src/client/testing/testController/unittest/testDiscoveryAdapter.ts @@ -1,77 +1,211 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -import * as path from 'path'; -import { Uri } from 'vscode'; -import { IConfigurationService, ITestOutputChannel } from '../../../common/types'; -import { createDeferred, Deferred } from '../../../common/utils/async'; +import { CancellationToken, Disposable, Uri } from 'vscode'; +import { ChildProcess } from 'child_process'; +import { IConfigurationService } from '../../../common/types'; import { EXTENSION_ROOT_DIR } from '../../../constants'; +import { ITestDiscoveryAdapter, ITestResultResolver } from '../common/types'; +import { IEnvironmentVariablesProvider } from '../../../common/variables/types'; import { - DataReceivedEvent, - DiscoveredTestPayload, - ITestDiscoveryAdapter, - ITestServer, - TestCommandOptions, - TestDiscoveryCommand, -} from '../common/types'; + ExecutionFactoryCreateWithEnvironmentOptions, + IPythonExecutionFactory, + SpawnOptions, +} from '../../../common/process/types'; +import { traceError, traceInfo, traceVerbose } from '../../../logging'; +import { getEnvironment, runInBackground, useEnvExtension } from '../../../envExt/api.internal'; +import { PythonEnvironment } from '../../../pythonEnvironments/info'; +import { createTestingDeferred } from '../common/utils'; +import { buildDiscoveryCommand, buildUnittestEnv as configureSubprocessEnv } from './unittestHelpers'; +import { cleanupOnCancellation, createProcessHandlers, setupDiscoveryPipe } from '../common/discoveryHelpers'; +import { ProjectAdapter } from '../common/projectAdapter'; /** - * Wrapper class for unittest test discovery. This is where we call `runTestCommand`. + * Configures the subprocess environment for unittest discovery. + * @param envVarsService Service to retrieve environment variables + * @param uri Workspace URI + * @param discoveryPipeName Name of the discovery pipe to pass to the subprocess + * @returns Configured environment variables for the subprocess */ -export class UnittestTestDiscoveryAdapter implements ITestDiscoveryAdapter { - private promiseMap: Map> = new Map(); - - private cwd: string | undefined; +async function configureDiscoveryEnv( + envVarsService: IEnvironmentVariablesProvider | undefined, + uri: Uri, + discoveryPipeName: string, +): Promise { + const envVars = await envVarsService?.getEnvironmentVariables(uri); + const mutableEnv = configureSubprocessEnv(envVars, discoveryPipeName); + return mutableEnv; +} +/** + * Wrapper class for unittest test discovery. + */ +export class UnittestTestDiscoveryAdapter implements ITestDiscoveryAdapter { constructor( - public testServer: ITestServer, public configSettings: IConfigurationService, - private readonly outputChannel: ITestOutputChannel, - ) { - testServer.onDataReceived(this.onDataReceivedHandler, this); - } + private readonly resultResolver?: ITestResultResolver, + private readonly envVarsService?: IEnvironmentVariablesProvider, + ) {} + + async discoverTests( + uri: Uri, + executionFactory: IPythonExecutionFactory, + token?: CancellationToken, + interpreter?: PythonEnvironment, + project?: ProjectAdapter, + ): Promise { + // Setup discovery pipe and cancellation + const { + pipeName: discoveryPipeName, + cancellation: discoveryPipeCancellation, + tokenDisposable, + } = await setupDiscoveryPipe(this.resultResolver, token, uri); - public onDataReceivedHandler({ uuid, data }: DataReceivedEvent): void { - const deferred = this.promiseMap.get(uuid); - if (deferred) { - deferred.resolve(JSON.parse(data)); - this.promiseMap.delete(uuid); + // Setup process handlers deferred (used by both execution paths) + const deferredTillExecClose = createTestingDeferred(); + + // Collect all disposables for cleanup in finally block + const disposables: Disposable[] = []; + if (tokenDisposable) { + disposables.push(tokenDisposable); } - } + try { + // Build unittest command and arguments + const settings = this.configSettings.getSettings(uri); + const { unittestArgs } = settings.testing; + const cwd = settings.testing.cwd && settings.testing.cwd.length > 0 ? settings.testing.cwd : uri.fsPath; + const execArgs = buildDiscoveryCommand(unittestArgs, EXTENSION_ROOT_DIR); + traceVerbose(`Running unittest discovery with command: ${execArgs.join(' ')} for workspace ${uri.fsPath}.`); - public async discoverTests(uri: Uri): Promise { - const deferred = createDeferred(); - const settings = this.configSettings.getSettings(uri); - const { unittestArgs } = settings.testing; + // Configure subprocess environment + const mutableEnv = await configureDiscoveryEnv(this.envVarsService, uri, discoveryPipeName); - const command = buildDiscoveryCommand(unittestArgs); + // Set PROJECT_ROOT_PATH for project-based testing (tells Python where to root the test tree) + if (project) { + mutableEnv.PROJECT_ROOT_PATH = project.projectUri.fsPath; + traceInfo( + `[test-by-project] Setting PROJECT_ROOT_PATH=${project.projectUri.fsPath} for unittest discovery`, + ); + } - this.cwd = uri.fsPath; - const uuid = this.testServer.createUUID(uri.fsPath); + // Setup process handlers (shared by both execution paths) + const handlers = createProcessHandlers('unittest', uri, cwd, this.resultResolver, deferredTillExecClose); - const options: TestCommandOptions = { - workspaceFolder: uri, - command, - cwd: this.cwd, - uuid, - outChannel: this.outputChannel, - }; + // Execute using environment extension if available + if (useEnvExtension()) { + traceInfo(`Using environment extension for unittest discovery in workspace ${uri.fsPath}`); + const pythonEnv = project?.pythonEnvironment ?? (await getEnvironment(uri)); + if (!pythonEnv) { + traceError( + `Python environment not found for workspace ${uri.fsPath}. Cannot proceed with test discovery.`, + ); + deferredTillExecClose.resolve(); + return; + } + traceVerbose(`Using Python environment: ${JSON.stringify(pythonEnv)}`); - this.promiseMap.set(uuid, deferred); + const proc = await runInBackground(pythonEnv, { + cwd, + args: execArgs, + env: (mutableEnv as unknown) as { [key: string]: string }, + }); + traceInfo(`Started unittest discovery subprocess (environment extension) for workspace ${uri.fsPath}`); - // Send the test command to the server. - // The server will fire an onDataReceived event once it gets a response. - this.testServer.sendCommand(options); + // Wire up cancellation and process events + const envExtCancellationHandler = token?.onCancellationRequested(() => { + cleanupOnCancellation('unittest', proc, deferredTillExecClose, discoveryPipeCancellation, uri); + }); + if (envExtCancellationHandler) { + disposables.push(envExtCancellationHandler); + } + proc.stdout.on('data', handlers.onStdout); + proc.stderr.on('data', handlers.onStderr); + proc.onExit((code, signal) => { + handlers.onExit(code, signal); + handlers.onClose(code, signal); + }); - return deferred.promise; - } -} + await deferredTillExecClose.promise; + traceInfo(`Unittest discovery completed for workspace ${uri.fsPath}`); + return; + } + + // Execute using execution factory (fallback path) + traceInfo(`Using execution factory for unittest discovery in workspace ${uri.fsPath}`); + const creationOptions: ExecutionFactoryCreateWithEnvironmentOptions = { + allowEnvironmentFetchExceptions: false, + resource: uri, + interpreter, + }; + const execService = await executionFactory.createActivatedEnvironment(creationOptions); + if (!execService) { + traceError( + `Failed to create execution service for workspace ${uri.fsPath}. Cannot proceed with test discovery.`, + ); + deferredTillExecClose.resolve(); + return; + } + const execInfo = await execService.getExecutablePath(); + traceVerbose(`Using Python executable: ${execInfo} for workspace ${uri.fsPath}`); + + // Check for cancellation before spawning process + if (token?.isCancellationRequested) { + traceInfo(`Unittest discovery cancelled before spawning process for workspace ${uri.fsPath}`); + deferredTillExecClose.resolve(); + return; + } + + const spawnOptions: SpawnOptions = { + cwd, + throwOnStdErr: true, + env: mutableEnv, + token, + }; -function buildDiscoveryCommand(args: string[]): TestDiscoveryCommand { - const discoveryScript = path.join(EXTENSION_ROOT_DIR, 'pythonFiles', 'unittestadapter', 'discovery.py'); + let resultProc: ChildProcess | undefined; - return { - script: discoveryScript, - args: ['--udiscovery', ...args], - }; + // Set up cancellation handler after all early return checks + const cancellationHandler = token?.onCancellationRequested(() => { + traceInfo(`Cancellation requested during unittest discovery for workspace ${uri.fsPath}`); + cleanupOnCancellation('unittest', resultProc, deferredTillExecClose, discoveryPipeCancellation, uri); + }); + if (cancellationHandler) { + disposables.push(cancellationHandler); + } + + try { + const result = execService.execObservable(execArgs, spawnOptions); + resultProc = result?.proc; + + if (!resultProc) { + traceError(`Failed to spawn unittest discovery subprocess for workspace ${uri.fsPath}`); + deferredTillExecClose.resolve(); + return; + } + traceInfo(`Started unittest discovery subprocess (execution factory) for workspace ${uri.fsPath}`); + } catch (error) { + traceError(`Error spawning unittest discovery subprocess for workspace ${uri.fsPath}: ${error}`); + deferredTillExecClose.resolve(); + throw error; + } + resultProc.stdout?.on('data', handlers.onStdout); + resultProc.stderr?.on('data', handlers.onStderr); + resultProc.on('exit', handlers.onExit); + resultProc.on('close', handlers.onClose); + + traceVerbose(`Waiting for unittest discovery subprocess to complete for workspace ${uri.fsPath}`); + await deferredTillExecClose.promise; + traceInfo(`Unittest discovery completed for workspace ${uri.fsPath}`); + } catch (error) { + traceError(`Error during unittest discovery for workspace ${uri.fsPath}: ${error}`); + deferredTillExecClose.resolve(); + throw error; + } finally { + traceVerbose(`Cleaning up unittest discovery resources for workspace ${uri.fsPath}`); + // Dispose all cancellation handlers and event subscriptions + disposables.forEach((d) => d.dispose()); + // Dispose the discovery pipe cancellation token + discoveryPipeCancellation.dispose(); + } + } } diff --git a/src/client/testing/testController/unittest/testExecutionAdapter.ts b/src/client/testing/testController/unittest/testExecutionAdapter.ts index b39e0cd29560..c7d21b768c5b 100644 --- a/src/client/testing/testController/unittest/testExecutionAdapter.ts +++ b/src/client/testing/testController/unittest/testExecutionAdapter.ts @@ -2,75 +2,311 @@ // Licensed under the MIT License. import * as path from 'path'; -import { Uri } from 'vscode'; -import { IConfigurationService, ITestOutputChannel } from '../../../common/types'; -import { createDeferred, Deferred } from '../../../common/utils/async'; +import { CancellationTokenSource, DebugSessionOptions, TestRun, TestRunProfileKind, Uri } from 'vscode'; +import { ChildProcess } from 'child_process'; +import { IConfigurationService } from '../../../common/types'; +import { Deferred, createDeferred } from '../../../common/utils/async'; import { EXTENSION_ROOT_DIR } from '../../../constants'; import { - DataReceivedEvent, ExecutionTestPayload, ITestExecutionAdapter, - ITestServer, + ITestResultResolver, TestCommandOptions, TestExecutionCommand, } from '../common/types'; +import { traceError, traceInfo, traceLog, traceVerbose } from '../../../logging'; +import { fixLogLinesNoTrailing } from '../common/utils'; +import { EnvironmentVariables, IEnvironmentVariablesProvider } from '../../../common/variables/types'; +import { + ExecutionFactoryCreateWithEnvironmentOptions, + ExecutionResult, + IPythonExecutionFactory, + SpawnOptions, +} from '../../../common/process/types'; +import { ITestDebugLauncher, LaunchOptions } from '../../common/types'; +import { UNITTEST_PROVIDER } from '../../common/constants'; +import * as utils from '../common/utils'; +import { getEnvironment, runInBackground, useEnvExtension } from '../../../envExt/api.internal'; +import { PythonEnvironment } from '../../../pythonEnvironments/info'; +import { ProjectAdapter } from '../common/projectAdapter'; /** * Wrapper Class for unittest test execution. This is where we call `runTestCommand`? */ export class UnittestTestExecutionAdapter implements ITestExecutionAdapter { - private promiseMap: Map> = new Map(); - - private cwd: string | undefined; - constructor( - public testServer: ITestServer, public configSettings: IConfigurationService, - private readonly outputChannel: ITestOutputChannel, - ) { - testServer.onDataReceived(this.onDataReceivedHandler, this); - } + private readonly resultResolver?: ITestResultResolver, + private readonly envVarsService?: IEnvironmentVariablesProvider, + ) {} - public onDataReceivedHandler({ uuid, data }: DataReceivedEvent): void { - const deferred = this.promiseMap.get(uuid); - if (deferred) { - deferred.resolve(JSON.parse(data)); - this.promiseMap.delete(uuid); + public async runTests( + uri: Uri, + testIds: string[], + profileKind: boolean | TestRunProfileKind | undefined, + runInstance: TestRun, + executionFactory: IPythonExecutionFactory, + debugLauncher?: ITestDebugLauncher, + _interpreter?: PythonEnvironment, + project?: ProjectAdapter, + ): Promise { + // deferredTillServerClose awaits named pipe server close + const deferredTillServerClose: Deferred = utils.createTestingDeferred(); + + // create callback to handle data received on the named pipe + const dataReceivedCallback = (data: ExecutionTestPayload) => { + if (runInstance && !runInstance.token.isCancellationRequested) { + this.resultResolver?.resolveExecution(data, runInstance); + } else { + traceError(`No run instance found, cannot resolve execution, for workspace ${uri.fsPath}.`); + } + }; + const cSource = new CancellationTokenSource(); + runInstance.token.onCancellationRequested(() => cSource.cancel()); + const name = await utils.startRunResultNamedPipe( + dataReceivedCallback, // callback to handle data received + deferredTillServerClose, // deferred to resolve when server closes + cSource.token, // token to cancel + ); + runInstance.token.onCancellationRequested(() => { + console.log(`Test run cancelled, resolving 'till TillAllServerClose' deferred for ${uri.fsPath}.`); + // if canceled, stop listening for results + deferredTillServerClose.resolve(); + }); + try { + await this.runTestsNew( + uri, + testIds, + name, + cSource, + runInstance, + profileKind, + executionFactory, + debugLauncher, + project, + ); + } catch (error) { + traceError(`Error in running unittest tests: ${error}`); + } finally { + await deferredTillServerClose.promise; } } - public async runTests(uri: Uri, testIds: string[], debugBool?: boolean): Promise { + private async runTestsNew( + uri: Uri, + testIds: string[], + resultNamedPipeName: string, + serverCancel: CancellationTokenSource, + runInstance: TestRun, + profileKind: boolean | TestRunProfileKind | undefined, + executionFactory: IPythonExecutionFactory, + debugLauncher?: ITestDebugLauncher, + project?: ProjectAdapter, + ): Promise { const settings = this.configSettings.getSettings(uri); const { unittestArgs } = settings.testing; + const cwd = settings.testing.cwd && settings.testing.cwd.length > 0 ? settings.testing.cwd : uri.fsPath; const command = buildExecutionCommand(unittestArgs); - this.cwd = uri.fsPath; - const uuid = this.testServer.createUUID(uri.fsPath); + let mutableEnv: EnvironmentVariables | undefined = await this.envVarsService?.getEnvironmentVariables(uri); + if (mutableEnv === undefined) { + mutableEnv = {} as EnvironmentVariables; + } + const pythonPathParts: string[] = mutableEnv.PYTHONPATH?.split(path.delimiter) ?? []; + const pythonPathCommand = [cwd, ...pythonPathParts].join(path.delimiter); + mutableEnv.PYTHONPATH = pythonPathCommand; + mutableEnv.TEST_RUN_PIPE = resultNamedPipeName; + + // Set PROJECT_ROOT_PATH for project-based testing (tells Python where to root the test tree) + if (project) { + mutableEnv.PROJECT_ROOT_PATH = project.projectUri.fsPath; + traceInfo( + `[test-by-project] Setting PROJECT_ROOT_PATH=${project.projectUri.fsPath} for unittest execution`, + ); + } + + if (profileKind && profileKind === TestRunProfileKind.Coverage) { + mutableEnv.COVERAGE_ENABLED = cwd; + } const options: TestCommandOptions = { workspaceFolder: uri, command, - cwd: this.cwd, - uuid, - debugBool, + cwd, + profileKind: typeof profileKind === 'boolean' ? undefined : profileKind, testIds, - outChannel: this.outputChannel, + token: runInstance.token, + }; + traceLog(`Running UNITTEST execution for the following test ids: ${testIds}`); + + // create named pipe server to send test ids + const testIdsFileName = await utils.writeTestIdsFile(testIds); + mutableEnv.RUN_TEST_IDS_PIPE = testIdsFileName; + traceInfo( + `All environment variables set for unittest execution, PYTHONPATH: ${JSON.stringify( + mutableEnv.PYTHONPATH, + )}`, + ); + + const spawnOptions: SpawnOptions = { + token: options.token, + cwd: options.cwd, + throwOnStdErr: true, + env: mutableEnv, + }; + // Create the Python environment in which to execute the command. + const creationOptions: ExecutionFactoryCreateWithEnvironmentOptions = { + allowEnvironmentFetchExceptions: false, + resource: options.workspaceFolder, }; + const execService = await executionFactory.createActivatedEnvironment(creationOptions); - const deferred = createDeferred(); - this.promiseMap.set(uuid, deferred); + const execInfo = await execService?.getExecutablePath(); + traceVerbose(`Executable path for unittest execution: ${execInfo}.`); - // Send test command to server. - // Server fire onDataReceived event once it gets response. - this.testServer.sendCommand(options); + const args = [options.command.script].concat(options.command.args); - return deferred.promise; + if (options.outChannel) { + options.outChannel.appendLine(`python ${args.join(' ')}`); + } + + try { + if (options.profileKind && options.profileKind === TestRunProfileKind.Debug) { + const launchOptions: LaunchOptions = { + cwd: options.cwd, + args, + token: options.token, + testProvider: UNITTEST_PROVIDER, + runTestIdsPort: testIdsFileName, + pytestPort: resultNamedPipeName, // change this from pytest + // Pass project for project-based debugging (Python path and session name derived from this) + project: project?.pythonProject, + }; + const sessionOptions: DebugSessionOptions = { + testRun: runInstance, + }; + traceInfo(`Running DEBUG unittest for workspace ${options.cwd} with arguments: ${args}\r\n`); + + if (debugLauncher === undefined) { + traceError('Debug launcher is not defined'); + throw new Error('Debug launcher is not defined'); + } + await debugLauncher.launchDebugger( + launchOptions, + () => { + serverCancel.cancel(); + }, + sessionOptions, + ); + } else if (useEnvExtension()) { + const pythonEnv = project?.pythonEnvironment ?? (await getEnvironment(uri)); + if (pythonEnv) { + traceInfo(`Running unittest with arguments: ${args.join(' ')} for workspace ${uri.fsPath} \r\n`); + const deferredTillExecClose = createDeferred(); + + const proc = await runInBackground(pythonEnv, { + cwd, + args, + env: (mutableEnv as unknown) as { [key: string]: string }, + }); + runInstance.token.onCancellationRequested(() => { + traceInfo(`Test run cancelled, killing unittest subprocess for workspace ${uri.fsPath}`); + proc.kill(); + deferredTillExecClose.resolve(); + serverCancel.cancel(); + }); + proc.stdout.on('data', (data) => { + const out = utils.fixLogLinesNoTrailing(data.toString()); + runInstance.appendOutput(out); + }); + proc.stderr.on('data', (data) => { + const out = utils.fixLogLinesNoTrailing(data.toString()); + runInstance.appendOutput(out); + }); + proc.onExit((code, signal) => { + if (code !== 0) { + traceError( + `Subprocess exited unsuccessfully with exit code ${code} and signal ${signal} on workspace ${uri.fsPath}`, + ); + } + deferredTillExecClose.resolve(); + serverCancel.cancel(); + }); + await deferredTillExecClose.promise; + } else { + traceError(`Python Environment not found for: ${uri.fsPath}`); + } + } else { + // This means it is running the test + traceInfo(`Running unittests for workspace ${cwd} with arguments: ${args}\r\n`); + + const deferredTillExecClose = createDeferred>(); + + let resultProc: ChildProcess | undefined; + + runInstance.token.onCancellationRequested(() => { + traceInfo(`Test run cancelled, killing unittest subprocess for workspace ${cwd}.`); + // if the resultProc exists just call kill on it which will handle resolving the ExecClose deferred, otherwise resolve the deferred here. + if (resultProc) { + resultProc?.kill(); + } else { + deferredTillExecClose?.resolve(); + serverCancel.cancel(); + } + }); + + const result = execService?.execObservable(args, spawnOptions); + resultProc = result?.proc; + + // Displays output to user and ensure the subprocess doesn't run into buffer overflow. + + result?.proc?.stdout?.on('data', (data) => { + const out = fixLogLinesNoTrailing(data.toString()); + runInstance.appendOutput(`${out}`); + }); + result?.proc?.stderr?.on('data', (data) => { + const out = fixLogLinesNoTrailing(data.toString()); + runInstance.appendOutput(`${out}`); + }); + + result?.proc?.on('exit', (code, signal) => { + // if the child has testIds then this is a run request + if (code !== 0 && testIds) { + // This occurs when we are running the test and there is an error which occurs. + + traceError( + `Subprocess exited unsuccessfully with exit code ${code} and signal ${signal} for workspace ${options.cwd}. Creating and sending error execution payload \n`, + ); + if (runInstance) { + this.resultResolver?.resolveExecution( + utils.createExecutionErrorPayload(code, signal, testIds, cwd), + runInstance, + ); + } + } + deferredTillExecClose.resolve(); + serverCancel.cancel(); + }); + await deferredTillExecClose.promise; + } + } catch (ex) { + traceError(`Error while running tests for workspace ${uri}: ${testIds}\r\n${ex}\r\n\r\n`); + return Promise.reject(ex); + } + // placeholder until after the rewrite is adopted + // TODO: remove after adoption. + const executionPayload: ExecutionTestPayload = { + cwd, + status: 'success', + error: '', + }; + return executionPayload; } } function buildExecutionCommand(args: string[]): TestExecutionCommand { - const executionScript = path.join(EXTENSION_ROOT_DIR, 'pythonFiles', 'unittestadapter', 'execution.py'); + const executionScript = path.join(EXTENSION_ROOT_DIR, 'python_files', 'unittestadapter', 'execution.py'); return { script: executionScript, diff --git a/src/client/testing/testController/unittest/unittestController.ts b/src/client/testing/testController/unittest/unittestController.ts index ee79103c4e3e..863f34abd514 100644 --- a/src/client/testing/testController/unittest/unittestController.ts +++ b/src/client/testing/testController/unittest/unittestController.ts @@ -1,36 +1,12 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -import * as path from 'path'; -import * as util from 'util'; -import { inject, injectable, named } from 'inversify'; -import { CancellationToken, TestController, TestItem, Uri, WorkspaceFolder } from 'vscode'; +import { inject, injectable } from 'inversify'; +import { CancellationToken, TestController, TestItem } from 'vscode'; import { IWorkspaceService } from '../../../common/application/types'; -import { IConfigurationService } from '../../../common/types'; -import { createDeferred, Deferred } from '../../../common/utils/async'; -import { UNITTEST_PROVIDER } from '../../common/constants'; -import { ITestRunner, Options, TestDiscoveryOptions } from '../../common/types'; -import { - ITestFrameworkController, - ITestRun, - ITestsRunner, - RawDiscoveredTests, - RawTest, - RawTestParent, - TestData, -} from '../common/types'; -import { unittestGetTestFolders, unittestGetTestPattern, unittestGetTopLevelDirectory } from './arguments'; -import { - createErrorTestItem, - createWorkspaceRootTestItem, - getNodeByUri, - getWorkspaceNode, - updateTestItemFromRawData, -} from '../common/testItemUtilities'; -import { sendTelemetryEvent } from '../../../telemetry'; -import { EventName } from '../../../telemetry/constants'; -import { unittestDiscovery } from '../../../common/process/internal/scripts/testing_tools'; -import { traceError } from '../../../logging'; +import { Deferred } from '../../../common/utils/async'; +import { ITestFrameworkController, RawDiscoveredTests, TestData } from '../common/types'; +import { getWorkspaceNode, updateTestItemFromRawData } from '../common/testItemUtilities'; @injectable() export class UnittestController implements ITestFrameworkController { @@ -40,12 +16,7 @@ export class UnittestController implements ITestFrameworkController { private idToRawData: Map = new Map(); - constructor( - @inject(ITestRunner) private readonly discoveryRunner: ITestRunner, - @inject(ITestsRunner) @named(UNITTEST_PROVIDER) private readonly runner: ITestsRunner, - @inject(IConfigurationService) private readonly configService: IConfigurationService, - @inject(IWorkspaceService) private readonly workspaceService: IWorkspaceService, - ) {} + constructor(@inject(IWorkspaceService) private readonly workspaceService: IWorkspaceService) {} public async resolveChildren( testController: TestController, @@ -104,294 +75,4 @@ export class UnittestController implements ITestFrameworkController { } return Promise.resolve(); } - - public async refreshTestData(testController: TestController, uri: Uri, token?: CancellationToken): Promise { - sendTelemetryEvent(EventName.UNITTEST_DISCOVERING, undefined, { tool: 'unittest' }); - const workspace = this.workspaceService.getWorkspaceFolder(uri); - if (workspace) { - // Discovery is expensive. So if it is already running then use the promise - // from the last run - const previous = this.discovering.get(workspace.uri.fsPath); - if (previous) { - return previous.promise; - } - - const settings = this.configService.getSettings(workspace.uri); - const options: TestDiscoveryOptions = { - workspaceFolder: workspace.uri, - cwd: - settings.testing.cwd && settings.testing.cwd.length > 0 - ? settings.testing.cwd - : workspace.uri.fsPath, - args: settings.testing.unittestArgs, - ignoreCache: true, - token, - }; - - const startDir = unittestGetTestFolders(options.args)[0]; - const pattern = unittestGetTestPattern(options.args); - const topLevelDir = unittestGetTopLevelDirectory(options.args); - let testDir = startDir; - if (path.isAbsolute(startDir)) { - const relative = path.relative(options.cwd, startDir); - testDir = relative.length > 0 ? relative : '.'; - } - - const runOptionsArgs: string[] = - topLevelDir == null ? [startDir, pattern] : [startDir, pattern, topLevelDir]; - - const runOptions: Options = { - // unittest needs to load modules in the workspace - // isolating it breaks unittest discovery - args: unittestDiscovery(runOptionsArgs), - cwd: options.cwd, - workspaceFolder: options.workspaceFolder, - token: options.token, - outChannel: options.outChannel, - }; - - const deferred = createDeferred(); - this.discovering.set(workspace.uri.fsPath, deferred); - - let rawTestData: RawDiscoveredTests | undefined; - try { - const content = await this.discoveryRunner.run(UNITTEST_PROVIDER, runOptions); - rawTestData = await testDiscoveryParser(options.cwd, testDir, getTestIds(content), options.token); - this.testData.set(workspace.uri.fsPath, rawTestData); - - const exceptions = getTestDiscoveryExceptions(content); - if (exceptions.length === 0) { - // Remove error node - testController.items.delete(`DiscoveryError:${workspace.uri.fsPath}`); - } else { - traceError('Error discovering unittest tests:\r\n', exceptions.join('\r\n\r\n')); - - let errorNode = testController.items.get(`DiscoveryError:${workspace.uri.fsPath}`); - const message = util.format( - 'Error discovering unittest tests (see Output > Python):\r\n', - exceptions.join('\r\n\r\n'), - ); - if (errorNode === undefined) { - errorNode = createErrorTestItem(testController, { - id: `DiscoveryError:${workspace.uri.fsPath}`, - label: `Unittest Discovery Error [${path.basename(workspace.uri.fsPath)}]`, - error: message, - }); - errorNode.canResolveChildren = false; - testController.items.add(errorNode); - } - errorNode.error = message; - } - - deferred.resolve(); - } catch (ex) { - sendTelemetryEvent(EventName.UNITTEST_DISCOVERY_DONE, undefined, { tool: 'unittest', failed: true }); - const cancel = options.token?.isCancellationRequested ? 'Cancelled' : 'Error'; - traceError(`${cancel} discovering unittest tests:\r\n`, ex); - - // Report also on the test view. - testController.items.add( - createErrorTestItem(testController, { - id: `DiscoveryError:${workspace.uri.fsPath}`, - label: `Unittest Discovery Error [${path.basename(workspace.uri.fsPath)}]`, - error: util.format(`${cancel} discovering unittest tests (see Output > Python):\r\n`, ex), - }), - ); - - deferred.reject(ex as Error); - } finally { - // Discovery has finished running we have the raw test data at this point. - this.discovering.delete(workspace.uri.fsPath); - } - - if (!rawTestData) { - // No test data is available - return Promise.resolve(); - } - - const workspaceNode = testController.items.get(rawTestData.root); - if (workspaceNode) { - if (uri.fsPath === workspace.uri.fsPath) { - // this is a workspace level refresh - // This is an existing workspace test node. Just update the children - await this.resolveChildren(testController, workspaceNode, token); - } else { - // This is a child node refresh - const testNode = getNodeByUri(workspaceNode, uri); - if (testNode) { - // We found the node to update - await this.resolveChildren(testController, testNode, token); - } else { - // update the entire workspace tree - await this.resolveChildren(testController, workspaceNode, token); - } - } - } else if (rawTestData.tests.length > 0) { - // This is a new workspace with tests. - const newItem = createWorkspaceRootTestItem(testController, this.idToRawData, { - id: rawTestData.root, - label: path.basename(rawTestData.root), - uri: Uri.file(rawTestData.root), - runId: rawTestData.root === '.' ? workspace.uri.fsPath : rawTestData.root, - rawId: rawTestData.rootid, - }); - testController.items.add(newItem); - - await this.resolveChildren(testController, newItem, token); - } - } - sendTelemetryEvent(EventName.UNITTEST_DISCOVERY_DONE, undefined, { tool: 'unittest', failed: false }); - return Promise.resolve(); - } - - public runTests( - testRun: ITestRun, - workspace: WorkspaceFolder, - token: CancellationToken, - testController?: TestController, - ): Promise { - const settings = this.configService.getSettings(workspace.uri); - return this.runner.runTests( - testRun, - { - workspaceFolder: workspace.uri, - cwd: - settings.testing.cwd && settings.testing.cwd.length > 0 - ? settings.testing.cwd - : workspace.uri.fsPath, - token, - args: settings.testing.unittestArgs, - }, - this.idToRawData, - testController, - ); - } -} - -function getTestDiscoveryExceptions(content: string): string[] { - const lines = content.split(/\r?\n/g); - let start = false; - let data = ''; - const exceptions: string[] = []; - for (const line of lines) { - if (start) { - if (line.startsWith('=== exception end ===')) { - exceptions.push(data); - start = false; - } else { - data += `${line}\r\n`; - } - } else if (line.startsWith('=== exception start ===')) { - start = true; - data = ''; - } - } - return exceptions; -} - -function getTestIds(content: string): string[] { - let startedCollecting = false; - const lines = content.split(/\r?\n/g); - - const ids: string[] = []; - for (const line of lines) { - if (!startedCollecting) { - if (line === 'start') { - startedCollecting = true; - } - if (line.startsWith('===')) { - break; - } - } - ids.push(line.trim()); - } - return ids.filter((id) => id.length > 0); -} - -function testDiscoveryParser( - cwd: string, - testDir: string, - testIds: string[], - token: CancellationToken | undefined, -): Promise { - const parents: RawTestParent[] = []; - const tests: RawTest[] = []; - - for (const testId of testIds) { - if (token?.isCancellationRequested) { - break; - } - - const parts = testId.split(':'); - - // At minimum a `unittest` test will have a file, class, function, and line number - // E.g: - // test_math.TestMathMethods.test_numbers:5 - // test_math.TestMathMethods.test_numbers2:9 - if (parts.length > 3) { - const lineNo = parts.pop(); - const functionName = parts.pop(); - const className = parts.pop(); - const fileName = parts.pop(); - const folders = parts; - const pyFileName = `${fileName}.py`; - const relPath = `./${[...folders, pyFileName].join('/')}`; - - if (functionName && className && fileName && lineNo) { - const collectionId = `${relPath}::${className}`; - const fileId = relPath; - tests.push({ - id: `${relPath}::${className}::${functionName}`, - name: functionName, - parentid: collectionId, - source: `${relPath}:${lineNo}`, - }); - - const rawCollection = parents.find((c) => c.id === collectionId); - if (!rawCollection) { - parents.push({ - id: collectionId, - name: className, - parentid: fileId, - kind: 'suite', - }); - } - - const rawFile = parents.find((f) => f.id === fileId); - if (!rawFile) { - parents.push({ - id: fileId, - name: pyFileName, - parentid: folders.length === 0 ? '.' : `./${folders.join('/')}`, - kind: 'file', - relpath: relPath, - } as RawTestParent); - } - - const folderParts = []; - for (const folder of folders) { - const parentId = folderParts.length === 0 ? '.' : `./${folderParts.join('/')}`; - folderParts.push(folder); - const pathId = `./${folderParts.join('/')}`; - const rawFolder = parents.find((f) => f.id === pathId); - if (!rawFolder) { - parents.push({ - id: pathId, - name: folder, - parentid: parentId, - kind: 'folder', - relpath: pathId, - } as RawTestParent); - } - } - } - } - } - - return Promise.resolve({ - rootid: '.', - root: path.isAbsolute(testDir) ? testDir : path.resolve(cwd, testDir), - parents, - tests, - }); } diff --git a/src/client/testing/testController/unittest/unittestHelpers.ts b/src/client/testing/testController/unittest/unittestHelpers.ts new file mode 100644 index 000000000000..249a78dda7b7 --- /dev/null +++ b/src/client/testing/testController/unittest/unittestHelpers.ts @@ -0,0 +1,28 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +import * as path from 'path'; +import { traceInfo } from '../../../logging'; + +/** + * Builds the environment variables required for unittest discovery. + * Sets TEST_RUN_PIPE for communication. + */ +export function buildUnittestEnv( + envVars: { [key: string]: string | undefined } | undefined, + discoveryPipeName: string, +): { [key: string]: string | undefined } { + const mutableEnv = { + ...envVars, + }; + mutableEnv.TEST_RUN_PIPE = discoveryPipeName; + traceInfo(`Environment variables set for unittest discovery: TEST_RUN_PIPE=${mutableEnv.TEST_RUN_PIPE}`); + return mutableEnv; +} + +/** + * Builds the unittest discovery command. + */ +export function buildDiscoveryCommand(args: string[], extensionRootDir: string): string[] { + const discoveryScript = path.join(extensionRootDir, 'python_files', 'unittestadapter', 'discovery.py'); + return [discoveryScript, '--udiscovery', ...args]; +} diff --git a/src/client/testing/testController/workspaceTestAdapter.ts b/src/client/testing/testController/workspaceTestAdapter.ts index 39efc67f7c7e..f17687732f57 100644 --- a/src/client/testing/testController/workspaceTestAdapter.ts +++ b/src/client/testing/testController/workspaceTestAdapter.ts @@ -1,43 +1,21 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -import * as path from 'path'; import * as util from 'util'; -import { - CancellationToken, - Position, - Range, - TestController, - TestItem, - TestMessage, - TestRun, - Uri, - Location, -} from 'vscode'; -import { splitLines } from '../../common/stringUtils'; +import { CancellationToken, TestController, TestItem, TestRun, TestRunProfileKind, Uri } from 'vscode'; import { createDeferred, Deferred } from '../../common/utils/async'; import { Testing } from '../../common/utils/localize'; -import { traceError, traceVerbose } from '../../logging'; +import { traceError } from '../../logging'; import { sendTelemetryEvent } from '../../telemetry'; import { EventName } from '../../telemetry/constants'; import { TestProvider } from '../types'; -import { - clearAllChildren, - createErrorTestItem, - DebugTestTag, - ErrorTestItemOptions, - getTestCaseNodes, - RunTestTag, -} from './common/testItemUtilities'; -import { - DiscoveredTestItem, - DiscoveredTestNode, - DiscoveredTestType, - ITestDiscoveryAdapter, - ITestExecutionAdapter, -} from './common/types'; -import { fixLogLines } from './common/utils'; +import { createErrorTestItem, getTestCaseNodes } from './common/testItemUtilities'; +import { ITestDiscoveryAdapter, ITestExecutionAdapter, ITestResultResolver } from './common/types'; import { IPythonExecutionFactory } from '../../common/process/types'; +import { ITestDebugLauncher } from '../common/types'; +import { buildErrorNodeOptions } from './common/utils'; +import { PythonEnvironment } from '../../pythonEnvironments/info'; +import { ProjectAdapter } from './common/projectAdapter'; /** * This class exposes a test-provider-agnostic way of discovering tests. @@ -53,41 +31,35 @@ export class WorkspaceTestAdapter { private executing: Deferred | undefined; - runIdToTestItem: Map; - - runIdToVSid: Map; - - vsIdToRunId: Map; - constructor( private testProvider: TestProvider, private discoveryAdapter: ITestDiscoveryAdapter, private executionAdapter: ITestExecutionAdapter, private workspaceUri: Uri, - ) { - this.runIdToTestItem = new Map(); - this.runIdToVSid = new Map(); - this.vsIdToRunId = new Map(); - } + public resultResolver: ITestResultResolver, + ) {} public async executeTests( testController: TestController, runInstance: TestRun, includes: TestItem[], + executionFactory: IPythonExecutionFactory, token?: CancellationToken, - debugBool?: boolean, - executionFactory?: IPythonExecutionFactory, + profileKind?: boolean | TestRunProfileKind, + debugLauncher?: ITestDebugLauncher, + interpreter?: PythonEnvironment, + project?: ProjectAdapter, ): Promise { if (this.executing) { + traceError('Test execution already in progress, not starting a new one.'); return this.executing.promise; } const deferred = createDeferred(); this.executing = deferred; - let rawTestExecData; const testCaseNodes: TestItem[] = []; - const testCaseIds: string[] = []; + const testCaseIdsSet = new Set(); try { // first fetch all the individual test Items that we necessarily want includes.forEach((t) => { @@ -97,24 +69,25 @@ export class WorkspaceTestAdapter { // iterate through testItems nodes and fetch their unittest runID to pass in as argument testCaseNodes.forEach((node) => { runInstance.started(node); // do the vscode ui test item start here before runtest - const runId = this.vsIdToRunId.get(node.id); + const runId = this.resultResolver.vsIdToRunId.get(node.id); if (runId) { - testCaseIds.push(runId); + testCaseIdsSet.add(runId); } }); - - // ** execution factory only defined for new rewrite way - if (executionFactory !== undefined) { - rawTestExecData = await this.executionAdapter.runTests( - this.workspaceUri, - testCaseIds, - debugBool, - executionFactory, - ); - traceVerbose('executionFactory defined'); - } else { - rawTestExecData = await this.executionAdapter.runTests(this.workspaceUri, testCaseIds, debugBool); + const testCaseIds = Array.from(testCaseIdsSet); + if (executionFactory === undefined) { + throw new Error('Execution factory is required for test execution'); } + await this.executionAdapter.runTests( + this.workspaceUri, + testCaseIds, + profileKind, + runInstance, + executionFactory, + debugLauncher, + interpreter, + project, + ); deferred.resolve(); } catch (ex) { // handle token and telemetry here @@ -139,176 +112,31 @@ export class WorkspaceTestAdapter { this.executing = undefined; } - if (rawTestExecData !== undefined && rawTestExecData.result !== undefined) { - // Map which holds the subtest information for each test item. - const subTestStats: Map = new Map(); - - // iterate through payload and update the UI accordingly. - for (const keyTemp of Object.keys(rawTestExecData.result)) { - const testCases: TestItem[] = []; - - // grab leaf level test items - testController.items.forEach((i) => { - const tempArr: TestItem[] = getTestCaseNodes(i); - testCases.push(...tempArr); - }); - - if ( - rawTestExecData.result[keyTemp].outcome === 'failure' || - rawTestExecData.result[keyTemp].outcome === 'passed-unexpected' - ) { - const rawTraceback = rawTestExecData.result[keyTemp].traceback ?? ''; - const traceback = splitLines(rawTraceback, { - trim: false, - removeEmptyEntries: true, - }).join('\r\n'); - - const text = `${rawTestExecData.result[keyTemp].test} failed: ${ - rawTestExecData.result[keyTemp].message ?? rawTestExecData.result[keyTemp].outcome - }\r\n${traceback}\r\n`; - const message = new TestMessage(text); - - // note that keyTemp is a runId for unittest library... - const grabVSid = this.runIdToVSid.get(keyTemp); - // search through freshly built array of testItem to find the failed test and update UI. - testCases.forEach((indiItem) => { - if (indiItem.id === grabVSid) { - if (indiItem.uri && indiItem.range) { - message.location = new Location(indiItem.uri, indiItem.range); - runInstance.failed(indiItem, message); - runInstance.appendOutput(fixLogLines(text)); - } - } - }); - } else if ( - rawTestExecData.result[keyTemp].outcome === 'success' || - rawTestExecData.result[keyTemp].outcome === 'expected-failure' - ) { - const grabTestItem = this.runIdToTestItem.get(keyTemp); - const grabVSid = this.runIdToVSid.get(keyTemp); - if (grabTestItem !== undefined) { - testCases.forEach((indiItem) => { - if (indiItem.id === grabVSid) { - if (indiItem.uri && indiItem.range) { - runInstance.passed(grabTestItem); - runInstance.appendOutput('Passed here'); - } - } - }); - } - } else if (rawTestExecData.result[keyTemp].outcome === 'skipped') { - const grabTestItem = this.runIdToTestItem.get(keyTemp); - const grabVSid = this.runIdToVSid.get(keyTemp); - if (grabTestItem !== undefined) { - testCases.forEach((indiItem) => { - if (indiItem.id === grabVSid) { - if (indiItem.uri && indiItem.range) { - runInstance.skipped(grabTestItem); - runInstance.appendOutput('Skipped here'); - } - } - }); - } - } else if (rawTestExecData.result[keyTemp].outcome === 'subtest-failure') { - // split on " " since the subtest ID has the parent test ID in the first part of the ID. - const parentTestCaseId = keyTemp.split(' ')[0]; - const parentTestItem = this.runIdToTestItem.get(parentTestCaseId); - const data = rawTestExecData.result[keyTemp]; - // find the subtest's parent test item - if (parentTestItem) { - const subtestStats = subTestStats.get(parentTestCaseId); - if (subtestStats) { - subtestStats.failed += 1; - } else { - subTestStats.set(parentTestCaseId, { failed: 1, passed: 0 }); - runInstance.appendOutput(fixLogLines(`${parentTestCaseId} [subtests]:\r\n`)); - // clear since subtest items don't persist between runs - clearAllChildren(parentTestItem); - } - const subtestId = keyTemp; - const subTestItem = testController?.createTestItem(subtestId, subtestId); - runInstance.appendOutput(fixLogLines(`${subtestId} Failed\r\n`)); - // create a new test item for the subtest - if (subTestItem) { - const traceback = data.traceback ?? ''; - const text = `${data.subtest} Failed: ${data.message ?? data.outcome}\r\n${traceback}\r\n`; - runInstance.appendOutput(fixLogLines(text)); - parentTestItem.children.add(subTestItem); - runInstance.started(subTestItem); - const message = new TestMessage(rawTestExecData?.result[keyTemp].message ?? ''); - if (parentTestItem.uri && parentTestItem.range) { - message.location = new Location(parentTestItem.uri, parentTestItem.range); - } - runInstance.failed(subTestItem, message); - } else { - throw new Error('Unable to create new child node for subtest'); - } - } else { - throw new Error('Parent test item not found'); - } - } else if (rawTestExecData.result[keyTemp].outcome === 'subtest-success') { - // split on " " since the subtest ID has the parent test ID in the first part of the ID. - const parentTestCaseId = keyTemp.split(' ')[0]; - const parentTestItem = this.runIdToTestItem.get(parentTestCaseId); - - // find the subtest's parent test item - if (parentTestItem) { - const subtestStats = subTestStats.get(parentTestCaseId); - if (subtestStats) { - subtestStats.passed += 1; - } else { - subTestStats.set(parentTestCaseId, { failed: 0, passed: 1 }); - runInstance.appendOutput(fixLogLines(`${parentTestCaseId} [subtests]:\r\n`)); - // clear since subtest items don't persist between runs - clearAllChildren(parentTestItem); - } - const subtestId = keyTemp; - const subTestItem = testController?.createTestItem(subtestId, subtestId); - // create a new test item for the subtest - if (subTestItem) { - parentTestItem.children.add(subTestItem); - runInstance.started(subTestItem); - runInstance.passed(subTestItem); - runInstance.appendOutput(fixLogLines(`${subtestId} Passed\r\n`)); - } else { - throw new Error('Unable to create new child node for subtest'); - } - } else { - throw new Error('Parent test item not found'); - } - } - } - } return Promise.resolve(); } public async discoverTests( testController: TestController, + executionFactory: IPythonExecutionFactory, token?: CancellationToken, - isMultiroot?: boolean, - workspaceFilePath?: string, - executionFactory?: IPythonExecutionFactory, + interpreter?: PythonEnvironment, ): Promise { sendTelemetryEvent(EventName.UNITTEST_DISCOVERING, undefined, { tool: this.testProvider }); - const workspacePath = this.workspaceUri.fsPath; - // Discovery is expensive. If it is already running, use the existing promise. if (this.discovering) { + traceError('Test discovery already in progress, not starting a new one.'); return this.discovering.promise; } const deferred = createDeferred(); this.discovering = deferred; - let rawTestData; try { - // ** execution factory only defined for new rewrite way - if (executionFactory !== undefined) { - rawTestData = await this.discoveryAdapter.discoverTests(this.workspaceUri, executionFactory); - } else { - rawTestData = await this.discoveryAdapter.discoverTests(this.workspaceUri); + if (executionFactory === undefined) { + throw new Error('Execution factory is required for test discovery'); } + await this.discoveryAdapter.discoverTests(this.workspaceUri, executionFactory, token, interpreter); deferred.resolve(); } catch (ex) { sendTelemetryEvent(EventName.UNITTEST_DISCOVERY_DONE, undefined, { tool: this.testProvider, failed: true }); @@ -320,7 +148,7 @@ export class WorkspaceTestAdapter { cancel = token?.isCancellationRequested ? Testing.cancelPytestDiscovery : Testing.errorPytestDiscovery; } - traceError(`${cancel}\r\n`, ex); + traceError(`${cancel} for workspace: ${this.workspaceUri} \r\n`, ex); // Report also on the test view. const message = util.format(`${cancel} ${Testing.seePythonOutput}\r\n`, ex); @@ -328,153 +156,23 @@ export class WorkspaceTestAdapter { const errorNode = createErrorTestItem(testController, options); testController.items.add(errorNode); - deferred.reject(ex as Error); + return deferred.reject(ex as Error); } finally { // Discovery has finished running, we have the data, // we don't need the deferred promise anymore. this.discovering = undefined; } - if (!rawTestData) { - // No test data is available - return Promise.resolve(); - } - - // Check if there were any errors in the discovery process. - if (rawTestData.status === 'error') { - const testingErrorConst = - this.testProvider === 'pytest' ? Testing.errorPytestDiscovery : Testing.errorUnittestDiscovery; - const { errors } = rawTestData; - traceError(testingErrorConst, '\r\n', errors!.join('\r\n\r\n')); - - let errorNode = testController.items.get(`DiscoveryError:${workspacePath}`); - const message = util.format( - `${testingErrorConst} ${Testing.seePythonOutput}\r\n`, - errors!.join('\r\n\r\n'), - ); - - if (errorNode === undefined) { - const options = buildErrorNodeOptions(this.workspaceUri, message, this.testProvider); - errorNode = createErrorTestItem(testController, options); - testController.items.add(errorNode); - } - errorNode.error = message; - } else { - // Remove the error node if necessary, - // then parse and insert test data. - testController.items.delete(`DiscoveryError:${workspacePath}`); - - // Wrap the data under a root node named after the test provider. - const wrappedTests = rawTestData.tests; - - // If we are in a multiroot workspace scenario, wrap the current folder's test result in a tree under the overall root + the current folder name. - let rootPath = workspacePath; - let childrenRootPath = rootPath; - let childrenRootName = path.basename(rootPath); - - if (isMultiroot) { - rootPath = workspaceFilePath!; - childrenRootPath = workspacePath; - childrenRootName = path.basename(workspacePath); - } - - const children = [ - { - path: childrenRootPath, - name: childrenRootName, - type_: 'folder' as DiscoveredTestType, - id_: childrenRootPath, - children: wrappedTests ? [wrappedTests] : [], - }, - ]; - - // Update the raw test data with the wrapped data. - rawTestData.tests = { - path: rootPath, - name: this.testProvider, - type_: 'folder', - id_: rootPath, - children, - }; - - if (rawTestData.tests) { - // If the test root for this folder exists: Workspace refresh, update its children. - // Otherwise, it is a freshly discovered workspace, and we need to create a new test root and populate the test tree. - populateTestTree(testController, rawTestData.tests, undefined, this, token); - } else { - // Delete everything from the test controller. - testController.items.replace([]); - } - } - sendTelemetryEvent(EventName.UNITTEST_DISCOVERY_DONE, undefined, { tool: this.testProvider, failed: false }); return Promise.resolve(); } -} - -function isTestItem(test: DiscoveredTestNode | DiscoveredTestItem): test is DiscoveredTestItem { - return test.type_ === 'test'; -} -// had to switch the order of the original parameter since required param cannot follow optional. -function populateTestTree( - testController: TestController, - testTreeData: DiscoveredTestNode, - testRoot: TestItem | undefined, - wstAdapter: WorkspaceTestAdapter, - token?: CancellationToken, -): void { - // If testRoot is undefined, use the info of the root item of testTreeData to create a test item, and append it to the test controller. - if (!testRoot) { - testRoot = testController.createTestItem(testTreeData.path, testTreeData.name, Uri.file(testTreeData.path)); - - testRoot.canResolveChildren = true; - testRoot.tags = [RunTestTag, DebugTestTag]; - - testController.items.add(testRoot); + /** + * Retrieves the current test provider instance. + * + * @returns {TestProvider} The instance of the test provider. + */ + public getTestProvider(): TestProvider { + return this.testProvider; } - - // Recursively populate the tree with test data. - testTreeData.children.forEach((child) => { - if (!token?.isCancellationRequested) { - if (isTestItem(child)) { - const testItem = testController.createTestItem(child.id_, child.name, Uri.file(child.path)); - testItem.tags = [RunTestTag, DebugTestTag]; - - const range = new Range( - new Position(Number(child.lineno) - 1, 0), - new Position(Number(child.lineno), 0), - ); - testItem.canResolveChildren = false; - testItem.range = range; - testItem.tags = [RunTestTag, DebugTestTag]; - - testRoot!.children.add(testItem); - // add to our map - wstAdapter.runIdToTestItem.set(child.runID, testItem); - wstAdapter.runIdToVSid.set(child.runID, child.id_); - wstAdapter.vsIdToRunId.set(child.id_, child.runID); - } else { - let node = testController.items.get(child.path); - - if (!node) { - node = testController.createTestItem(child.id_, child.name, Uri.file(child.path)); - - node.canResolveChildren = true; - node.tags = [RunTestTag, DebugTestTag]; - testRoot!.children.add(node); - } - populateTestTree(testController, child, node, wstAdapter, token); - } - } - }); -} - -function buildErrorNodeOptions(uri: Uri, message: string, testType: string): ErrorTestItemOptions { - const labelText = testType === 'pytest' ? 'Pytest Discovery Error' : 'Unittest Discovery Error'; - return { - id: `DiscoveryError:${uri.fsPath}`, - label: `${labelText} [${path.basename(uri.fsPath)}]`, - error: message, - }; } diff --git a/src/client/testing/utils.ts b/src/client/testing/utils.ts new file mode 100644 index 000000000000..c1027d4a8dc1 --- /dev/null +++ b/src/client/testing/utils.ts @@ -0,0 +1,49 @@ +import { TestItem, env } from 'vscode'; +import { traceLog } from '../logging'; + +export async function writeTestIdToClipboard(testItem: TestItem): Promise { + if (testItem && typeof testItem.id === 'string') { + if (testItem.id.includes('\\') && testItem.id.indexOf('::') === -1) { + // Convert the id to a module.class.method format as this is a unittest + const moduleClassMethod = idToModuleClassMethod(testItem.id); + if (moduleClassMethod) { + await env.clipboard.writeText(moduleClassMethod); + traceLog('Testing: Copied test id to clipboard, id: ' + moduleClassMethod); + return; + } + } + // Otherwise use the id as is for pytest + await clipboardWriteText(testItem.id); + traceLog('Testing: Copied test id to clipboard, id: ' + testItem.id); + } +} + +export function idToModuleClassMethod(id: string): string | undefined { + // Split by backslash + const parts = id.split('\\'); + if (parts.length === 1) { + // Only one part, likely a parent folder or file + return parts[0]; + } + if (parts.length === 2) { + // Two parts: filePath and className + const [filePath, className] = parts.slice(-2); + const fileName = filePath.split(/[\\/]/).pop(); + if (!fileName) { + return undefined; + } + const module = fileName.replace(/\.py$/, ''); + return `${module}.${className}`; + } + // Three or more parts: filePath, className, methodName + const [filePath, className, methodName] = parts.slice(-3); + const fileName = filePath.split(/[\\/]/).pop(); + if (!fileName) { + return undefined; + } + const module = fileName.replace(/\.py$/, ''); + return `${module}.${className}.${methodName}`; +} +export function clipboardWriteText(text: string): Thenable { + return env.clipboard.writeText(text); +} diff --git a/src/test/.vscode/settings.json b/src/test/.vscode/settings.json index ef9292849a9d..cd2b4152591d 100644 --- a/src/test/.vscode/settings.json +++ b/src/test/.vscode/settings.json @@ -3,7 +3,6 @@ "python.linting.flake8Enabled": false, "python.testing.pytestArgs": [], "python.testing.unittestArgs": ["-s=./tests", "-p=test_*.py", "-v", "-s", ".", "-p", "*test*.py"], - "python.sortImports.args": [], "python.linting.lintOnSave": false, "python.linting.enabled": true, "python.linting.pycodestyleEnabled": false, @@ -12,8 +11,8 @@ "python.linting.pylamaEnabled": false, "python.linting.mypyEnabled": false, "python.linting.banditEnabled": false, - "python.formatting.provider": "yapf", // Don't set this to `Pylance`, for CI we want to use the LS that ships with the extension. "python.languageServer": "Jedi", - "python.pythonPath": "C:\\GIT\\s p\\vscode-python\\.venv\\Scripts\\python.exe" + "python.pythonPath": "C:\\GIT\\s p\\vscode-python\\.venv\\Scripts\\python.exe", + "python.defaultInterpreterPath": "python" } diff --git a/src/test/activation/activationManager.unit.test.ts b/src/test/activation/activationManager.unit.test.ts index 2b8d54f12ee9..6ee2572214b8 100644 --- a/src/test/activation/activationManager.unit.test.ts +++ b/src/test/activation/activationManager.unit.test.ts @@ -82,6 +82,12 @@ suite('Activation Manager', () => { test('If running in a virtual workspace, do not activate services that do not support it', async () => { when(workspaceService.isVirtualWorkspace).thenReturn(true); const resource = Uri.parse('two'); + const workspaceFolder = { + index: 0, + name: 'one', + uri: resource, + }; + when(workspaceService.getWorkspaceFolder(resource)).thenReturn(workspaceFolder); autoSelection .setup((a) => a.autoSelectInterpreter(resource)) @@ -112,6 +118,12 @@ suite('Activation Manager', () => { test('If running in a untrusted workspace, do not activate services that do not support it', async () => { when(workspaceService.isTrusted).thenReturn(false); const resource = Uri.parse('two'); + const workspaceFolder = { + index: 0, + name: 'one', + uri: resource, + }; + when(workspaceService.getWorkspaceFolder(resource)).thenReturn(workspaceFolder); autoSelection .setup((a) => a.autoSelectInterpreter(resource)) @@ -150,6 +162,13 @@ suite('Activation Manager', () => { .returns(() => Promise.resolve()) .verifiable(typemoq.Times.once()); + const workspaceFolder = { + index: 0, + name: 'one', + uri: resource, + }; + when(workspaceService.getWorkspaceFolder(resource)).thenReturn(workspaceFolder); + await managerTest.activateWorkspace(resource); autoSelection.verifyAll(); @@ -289,6 +308,12 @@ suite('Activation Manager', () => { .setup((a) => a.performPreStartupHealthCheck(resource)) .returns(() => Promise.resolve()) .verifiable(typemoq.Times.once()); + const workspaceFolder = { + index: 0, + name: 'one', + uri: resource, + }; + when(workspaceService.getWorkspaceFolder(resource)).thenReturn(workspaceFolder); await managerTest.activateWorkspace(resource); await managerTest.activateWorkspace(resource); diff --git a/src/test/activation/extensionSurvey.unit.test.ts b/src/test/activation/extensionSurvey.unit.test.ts index 6449eae24f31..a89797bfebef 100644 --- a/src/test/activation/extensionSurvey.unit.test.ts +++ b/src/test/activation/extensionSurvey.unit.test.ts @@ -8,7 +8,7 @@ import * as sinon from 'sinon'; import { anything, instance, mock, verify, when } from 'ts-mockito'; import * as TypeMoq from 'typemoq'; import { ExtensionSurveyPrompt, extensionSurveyStateKeys } from '../../client/activation/extensionSurvey'; -import { IApplicationEnvironment, IApplicationShell } from '../../client/common/application/types'; +import { IApplicationEnvironment, IApplicationShell, IWorkspaceService } from '../../client/common/application/types'; import { ShowExtensionSurveyPrompt } from '../../client/common/experiments/groups'; import { PersistentStateFactory } from '../../client/common/persistentState'; import { IPlatformService } from '../../client/common/platform/types'; @@ -23,6 +23,7 @@ import { createDeferred } from '../../client/common/utils/async'; import { Common, ExtensionSurveyBanner } from '../../client/common/utils/localize'; import { OSType } from '../../client/common/utils/platform'; import { sleep } from '../core'; +import { WorkspaceConfiguration } from 'vscode'; suite('Extension survey prompt - shouldShowBanner()', () => { let appShell: TypeMoq.IMock; @@ -35,6 +36,8 @@ suite('Extension survey prompt - shouldShowBanner()', () => { let disableSurveyForTime: TypeMoq.IMock>; let doNotShowAgain: TypeMoq.IMock>; let extensionSurveyPrompt: ExtensionSurveyPrompt; + let workspaceService: TypeMoq.IMock; + setup(() => { experiments = TypeMoq.Mock.ofType(); appShell = TypeMoq.Mock.ofType(); @@ -45,6 +48,7 @@ suite('Extension survey prompt - shouldShowBanner()', () => { doNotShowAgain = TypeMoq.Mock.ofType>(); platformService = TypeMoq.Mock.ofType(); appEnvironment = TypeMoq.Mock.ofType(); + workspaceService = TypeMoq.Mock.ofType(); when( persistentStateFactory.createGlobalPersistentState( extensionSurveyStateKeys.disableSurveyForTime, @@ -63,6 +67,7 @@ suite('Extension survey prompt - shouldShowBanner()', () => { experiments.object, appEnvironment.object, platformService.object, + workspaceService.object, 10, ); }); @@ -122,6 +127,40 @@ suite('Extension survey prompt - shouldShowBanner()', () => { } random.verifyAll(); }); + test('Returns true if telemetry.feedback.enabled is enabled', async () => { + disableSurveyForTime.setup((d) => d.value).returns(() => false); + doNotShowAgain.setup((d) => d.value).returns(() => false); + + const telemetryConfig = TypeMoq.Mock.ofType(); + workspaceService.setup((w) => w.getConfiguration('telemetry')).returns(() => telemetryConfig.object); + telemetryConfig + .setup((t) => t.get(TypeMoq.It.isValue('feedback.enabled'), TypeMoq.It.isValue(true))) + .returns(() => true); + + const result = extensionSurveyPrompt.shouldShowBanner(); + + expect(result).to.equal(true, 'Banner should be shown when telemetry.feedback.enabled is true'); + workspaceService.verify((w) => w.getConfiguration('telemetry'), TypeMoq.Times.once()); + telemetryConfig.verify((t) => t.get('feedback.enabled', true), TypeMoq.Times.once()); + }); + + test('Returns false if telemetry.feedback.enabled is disabled', async () => { + disableSurveyForTime.setup((d) => d.value).returns(() => false); + doNotShowAgain.setup((d) => d.value).returns(() => false); + + const telemetryConfig = TypeMoq.Mock.ofType(); + workspaceService.setup((w) => w.getConfiguration('telemetry')).returns(() => telemetryConfig.object); + telemetryConfig + .setup((t) => t.get(TypeMoq.It.isValue('feedback.enabled'), TypeMoq.It.isValue(true))) + .returns(() => false); + + const result = extensionSurveyPrompt.shouldShowBanner(); + + expect(result).to.equal(false, 'Banner should not be shown when feedback.enabled is false'); + workspaceService.verify((w) => w.getConfiguration('telemetry'), TypeMoq.Times.once()); + telemetryConfig.verify((t) => t.get('feedback.enabled', true), TypeMoq.Times.once()); + }); + test('Returns true if user is in the random sampling', async () => { disableSurveyForTime.setup((d) => d.value).returns(() => false); doNotShowAgain.setup((d) => d.value).returns(() => false); @@ -142,6 +181,7 @@ suite('Extension survey prompt - shouldShowBanner()', () => { experiments.object, appEnvironment.object, platformService.object, + workspaceService.object, 100, ); disableSurveyForTime.setup((d) => d.value).returns(() => false); @@ -162,6 +202,7 @@ suite('Extension survey prompt - shouldShowBanner()', () => { experiments.object, appEnvironment.object, platformService.object, + workspaceService.object, 0, ); disableSurveyForTime.setup((d) => d.value).returns(() => false); @@ -186,6 +227,7 @@ suite('Extension survey prompt - showSurvey()', () => { let platformService: TypeMoq.IMock; let appEnvironment: TypeMoq.IMock; let extensionSurveyPrompt: ExtensionSurveyPrompt; + let workspaceService: TypeMoq.IMock; setup(() => { appShell = TypeMoq.Mock.ofType(); browserService = TypeMoq.Mock.ofType(); @@ -195,6 +237,7 @@ suite('Extension survey prompt - showSurvey()', () => { doNotShowAgain = TypeMoq.Mock.ofType>(); platformService = TypeMoq.Mock.ofType(); appEnvironment = TypeMoq.Mock.ofType(); + workspaceService = TypeMoq.Mock.ofType(); when( persistentStateFactory.createGlobalPersistentState( extensionSurveyStateKeys.disableSurveyForTime, @@ -214,6 +257,7 @@ suite('Extension survey prompt - showSurvey()', () => { experiments.object, appEnvironment.object, platformService.object, + workspaceService.object, 10, ); }); @@ -355,7 +399,7 @@ suite('Extension survey prompt - showSurvey()', () => { platformService.verifyAll(); }); - test("Disable prompt if 'Do not show again' option is clicked", async () => { + test('Disable prompt if "Don\'t show again" option is clicked', async () => { const prompts = [ExtensionSurveyBanner.bannerLabelYes, ExtensionSurveyBanner.maybeLater, Common.doNotShowAgain]; platformService.setup((p) => p.osType).verifiable(TypeMoq.Times.never()); appShell @@ -406,6 +450,7 @@ suite('Extension survey prompt - activate()', () => { let extensionSurveyPrompt: ExtensionSurveyPrompt; let platformService: TypeMoq.IMock; let appEnvironment: TypeMoq.IMock; + let workspaceService: TypeMoq.IMock; setup(() => { appShell = TypeMoq.Mock.ofType(); browserService = TypeMoq.Mock.ofType(); @@ -414,6 +459,7 @@ suite('Extension survey prompt - activate()', () => { experiments = TypeMoq.Mock.ofType(); platformService = TypeMoq.Mock.ofType(); appEnvironment = TypeMoq.Mock.ofType(); + workspaceService = TypeMoq.Mock.ofType(); }); teardown(() => { @@ -431,6 +477,7 @@ suite('Extension survey prompt - activate()', () => { experiments.object, appEnvironment.object, platformService.object, + workspaceService.object, 10, ); experiments @@ -460,6 +507,7 @@ suite('Extension survey prompt - activate()', () => { experiments.object, appEnvironment.object, platformService.object, + workspaceService.object, 10, 50, ); @@ -494,6 +542,7 @@ suite('Extension survey prompt - activate()', () => { experiments.object, appEnvironment.object, platformService.object, + workspaceService.object, 10, 50, ); diff --git a/src/test/activation/jedi/jediAnalysisOptions.unit.test.ts b/src/test/activation/jedi/jediAnalysisOptions.unit.test.ts index 8104ed2730b0..66cb9e0ae604 100644 --- a/src/test/activation/jedi/jediAnalysisOptions.unit.test.ts +++ b/src/test/activation/jedi/jediAnalysisOptions.unit.test.ts @@ -12,6 +12,8 @@ import { WorkspaceService } from '../../../client/common/application/workspace'; import { ConfigurationService } from '../../../client/common/configuration/service'; import { IConfigurationService } from '../../../client/common/types'; import { IEnvironmentVariablesProvider } from '../../../client/common/variables/types'; +import { EnvironmentType, PythonEnvironment } from '../../../client/pythonEnvironments/info'; +import { Architecture } from '../../../client/common/utils/platform'; suite('Jedi LSP - analysis Options', () => { const workspacePath = path.join('this', 'is', 'fake', 'workspace', 'path'); @@ -72,6 +74,26 @@ suite('Jedi LSP - analysis Options', () => { expect(result.initializationOptions.hover.disable.keyword.all).to.deep.equal(true); expect(result.initializationOptions.workspace.extraPaths).to.deep.equal([]); expect(result.initializationOptions.workspace.symbols.maxSymbols).to.deep.equal(0); + expect(result.initializationOptions.semantic_tokens.enable).to.deep.equal(true); + }); + + test('With interpreter path', async () => { + when(workspaceService.getWorkspaceFolder(anything())).thenReturn(undefined); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + when(configurationService.getSettings(anything())).thenReturn({} as any); + const pythonEnvironment: PythonEnvironment = { + envPath: '.../.venv', + id: 'base_env', + envType: EnvironmentType.Conda, + path: '.../.venv/bin/python', + architecture: Architecture.x86, + sysPrefix: 'prefix/path', + }; + analysisOptions.initialize(undefined, pythonEnvironment); + + const result = await analysisOptions.getAnalysisOptions(); + + expect(result.initializationOptions.workspace.environmentPath).to.deep.equal('.../.venv/bin/python'); }); test('Without extraPaths provided and no workspace', async () => { diff --git a/src/test/activation/node/lspInteractiveWindowMiddlewareAddon.unit.test.ts b/src/test/activation/node/lspInteractiveWindowMiddlewareAddon.unit.test.ts deleted file mode 100644 index 256e57a5d724..000000000000 --- a/src/test/activation/node/lspInteractiveWindowMiddlewareAddon.unit.test.ts +++ /dev/null @@ -1,183 +0,0 @@ -/* eslint-disable @typescript-eslint/no-empty-function */ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; - -import { NotebookCell, NotebookCellKind, NotebookDocument, TextDocument, Uri } from 'vscode'; -import { expect } from 'chai'; -import { anything, capture, instance, mock, verify, when } from 'ts-mockito'; -import { LanguageClient } from 'vscode-languageclient/node'; -import { LspInteractiveWindowMiddlewareAddon } from '../../../client/activation/node/lspInteractiveWindowMiddlewareAddon'; -import { JupyterExtensionIntegration } from '../../../client/jupyter/jupyterIntegration'; -import { IExtensions, IInstaller } from '../../../client/common/types'; -import { - IComponentAdapter, - ICondaService, - IInterpreterDisplay, - IInterpreterService, -} from '../../../client/interpreter/contracts'; -import { IInterpreterSelector } from '../../../client/interpreter/configuration/types'; -import { IEnvironmentActivationService } from '../../../client/interpreter/activation/types'; -import { IContextKeyManager, IWorkspaceService } from '../../../client/common/application/types'; -import { MockMemento } from '../../mocks/mementos'; - -suite('Pylance Language Server - Interactive Window LSP Notebooks', () => { - const languageClientMock = mock(); - let languageClient: LanguageClient; - let jupyterApi: JupyterExtensionIntegration; - let middleware: LspInteractiveWindowMiddlewareAddon; - - setup(() => { - languageClient = instance(languageClientMock); - jupyterApi = new JupyterExtensionIntegration( - mock(), - mock(), - mock(), - mock(), - mock(), - new MockMemento(), - mock(), - mock(), - mock(), - mock(), - mock(), - ); - jupyterApi.registerGetNotebookUriForTextDocumentUriFunction(getNotebookUriFunction); - }); - teardown(() => { - middleware?.dispose(); - }); - - test('Unrelated document open should be forwarded to next handler unchanged', async () => { - middleware = makeMiddleware(); - - const uri = Uri.from({ scheme: 'file', path: 'test.py' }); - const textDocument = createTextDocument(uri); - - let nextCalled = false; - await middleware.didOpen(textDocument, async (_) => { - nextCalled = true; - }); - - return expect(nextCalled).to.be.true; - }); - - test('Notebook-related textDocument/didOpen should be swallowed', async () => { - middleware = makeMiddleware(); - - const uri = Uri.from({ scheme: 'test-input', path: 'Test' }); - const textDocument = createTextDocument(uri); - - let nextCalled = false; - await middleware.didOpen(textDocument, async (_) => { - nextCalled = true; - }); - - return expect(nextCalled).to.be.false; - }); - - test('Notebook-related document should be added at end of cells in notebookDocument/didOpen', async () => { - middleware = makeMiddleware(); - - const uri = Uri.from({ scheme: 'test-input', path: 'Test' }); - const textDocument = createTextDocument(uri); - - await middleware.didOpen(textDocument, async (_) => {}); - - const cellCount = 2; - const [notebookDocument, cells] = createNotebookDocument(getNotebookUriFunction(uri)!, cellCount); - await middleware.notebooks.didOpen(notebookDocument, cells, async (_, nextCells) => { - expect(nextCells.length).to.be.equals(cellCount + 1); - expect(nextCells[cellCount]).to.deep.equal({ - index: cellCount, - notebook: notebookDocument, - kind: NotebookCellKind.Code, - document: textDocument, - metadata: {}, - outputs: [], - executionSummary: undefined, - }); - }); - }); - - test('Notebook-related document opened after notebook causes notebookDocument/didChange', async () => { - middleware = makeMiddleware(); - - const uri = Uri.from({ scheme: 'test-input', path: 'Test' }); - const textDocument = createTextDocument(uri); - - const cellCount = 2; - const [notebookDocument, cells] = createNotebookDocument(getNotebookUriFunction(uri)!, cellCount); - await middleware.notebooks.didOpen(notebookDocument, cells, async (_) => {}); - - await middleware.didOpen(textDocument, async (_) => {}); - - verify(languageClientMock.sendNotification(anything(), anything())).once(); - const message = capture(languageClientMock.sendNotification).last()[1]; - - expect(message.notebookDocument.uri).to.equal(notebookDocument.uri.toString()); - expect(message.change.cells.structure).to.deep.equal({ - array: { - start: notebookDocument.cellCount, - deleteCount: 0, - cells: [{ kind: NotebookCellKind.Code, document: textDocument.uri.toString() }], - }, - didOpen: [ - { - uri: textDocument.uri.toString(), - languageId: textDocument.languageId, - version: textDocument.version, - text: textDocument.getText(), - }, - ], - didClose: undefined, - }); - }); - - function makeMiddleware(): LspInteractiveWindowMiddlewareAddon { - return new LspInteractiveWindowMiddlewareAddon(() => languageClient, jupyterApi); - } - - function getNotebookUriFunction(textDocumentUri: Uri): Uri | undefined { - if (textDocumentUri.scheme === 'test-input') { - return textDocumentUri.with({ scheme: 'test-notebook' }); - } - - return undefined; - } - - function createTextDocument(uri: Uri): TextDocument { - const textDocumentMock = mock(); - when(textDocumentMock.uri).thenReturn(uri); - when(textDocumentMock.languageId).thenReturn('python'); - when(textDocumentMock.version).thenReturn(11); - - return instance(textDocumentMock); - } - - function createNotebookDocument(uri: Uri, cellCount: number): [NotebookDocument, NotebookCell[]] { - const notebookDocumentMock = mock(); - when(notebookDocumentMock.uri).thenReturn(uri); - when(notebookDocumentMock.notebookType).thenReturn('jupyter'); - when(notebookDocumentMock.version).thenReturn(20); - when(notebookDocumentMock.cellCount).thenReturn(cellCount); - - const notebookDocument = instance(notebookDocumentMock); - - const cells: NotebookCell[] = []; - for (let i = 0; i < cellCount; i = i + 1) { - cells.push({ - index: i, - notebook: notebookDocument, - kind: NotebookCellKind.Code, - document: createTextDocument(Uri.from({ scheme: 'test-cell', path: `cell${i}` })), - metadata: {}, - outputs: [], - executionSummary: undefined, - }); - } - - return [notebookDocument, cells]; - } -}); diff --git a/src/test/activation/requirementsTxtLinkActivator.unit.test.ts b/src/test/activation/requirementsTxtLinkActivator.unit.test.ts new file mode 100644 index 000000000000..ebea4af29182 --- /dev/null +++ b/src/test/activation/requirementsTxtLinkActivator.unit.test.ts @@ -0,0 +1,25 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { expect } from 'chai'; +import { generatePyPiLink } from '../../client/activation/requirementsTxtLinkActivator'; + +suite('Link to PyPi in requiements test', () => { + [ + ['pytest', 'pytest'], + ['pytest-cov', 'pytest-cov'], + ['pytest_cov', 'pytest_cov'], + ['pytest_cov[an_extra]', 'pytest_cov'], + ['pytest == 0.6.1', 'pytest'], + ['pytest== 0.6.1', 'pytest'], + ['requests [security] >= 2.8.1, == 2.8.* ; python_version < "2.7"', 'requests'], + ['# a comment', null], + ['', null], + ].forEach(([input, expected]) => { + test(`PyPI link case: "${input}"`, () => { + expect(generatePyPiLink(input!)).equal(expected ? `https://pypi.org/project/${expected}/` : null); + }); + }); +}); diff --git a/src/test/activation/serviceRegistry.unit.test.ts b/src/test/activation/serviceRegistry.unit.test.ts index cf715b90ecfe..177eae810810 100644 --- a/src/test/activation/serviceRegistry.unit.test.ts +++ b/src/test/activation/serviceRegistry.unit.test.ts @@ -14,6 +14,7 @@ import { import { ServiceManager } from '../../client/ioc/serviceManager'; import { IServiceManager } from '../../client/ioc/types'; import { LoadLanguageServerExtension } from '../../client/activation/common/loadLanguageServerExtension'; +import { RequirementsTxtLinkActivator } from '../../client/activation/requirementsTxtLinkActivator'; suite('Unit Tests - Language Server Activation Service Registry', () => { let serviceManager: IServiceManager; @@ -46,5 +47,11 @@ suite('Unit Tests - Language Server Activation Service Registry', () => { LoadLanguageServerExtension, ), ).once(); + verify( + serviceManager.addSingleton( + IExtensionSingleActivationService, + RequirementsTxtLinkActivator, + ), + ).once(); }); }); diff --git a/src/test/api.functional.test.ts b/src/test/api.functional.test.ts index 74293f55256c..03016956dbef 100644 --- a/src/test/api.functional.test.ts +++ b/src/test/api.functional.test.ts @@ -5,6 +5,7 @@ import { assert, expect } from 'chai'; import * as path from 'path'; +import * as sinon from 'sinon'; import { instance, mock, when } from 'ts-mockito'; import { buildApi } from '../client/api'; import { ConfigurationService } from '../client/common/configuration/service'; @@ -17,9 +18,16 @@ import { ServiceContainer } from '../client/ioc/container'; import { ServiceManager } from '../client/ioc/serviceManager'; import { IServiceContainer, IServiceManager } from '../client/ioc/types'; import { IDiscoveryAPI } from '../client/pythonEnvironments/base/locator'; +import * as pythonDebugger from '../client/debugger/pythonDebugger'; +import { + JupyterExtensionIntegration, + JupyterExtensionPythonEnvironments, + JupyterPythonEnvironmentApi, +} from '../client/jupyter/jupyterIntegration'; +import { EventEmitter, Uri } from 'vscode'; suite('Extension API', () => { - const debuggerPath = path.join(EXTENSION_ROOT_DIR, 'pythonFiles', 'lib', 'python', 'debugpy'); + const debuggerPath = path.join(EXTENSION_ROOT_DIR, 'python_files', 'lib', 'python', 'debugpy'); const debuggerHost = 'somehost'; const debuggerPort = 12345; @@ -29,6 +37,7 @@ suite('Extension API', () => { let interpreterService: IInterpreterService; let discoverAPI: IDiscoveryAPI; let environmentVariablesProvider: IEnvironmentVariablesProvider; + let getDebugpyPathStub: sinon.SinonStub; setup(() => { serviceContainer = mock(ServiceContainer); @@ -37,6 +46,7 @@ suite('Extension API', () => { interpreterService = mock(InterpreterService); environmentVariablesProvider = mock(); discoverAPI = mock(); + when(discoverAPI.getEnvs()).thenReturn([]); when(serviceContainer.get(IConfigurationService)).thenReturn( instance(configurationService), @@ -44,8 +54,25 @@ suite('Extension API', () => { when(serviceContainer.get(IEnvironmentVariablesProvider)).thenReturn( instance(environmentVariablesProvider), ); + when(serviceContainer.get(JupyterExtensionIntegration)).thenReturn( + instance(mock()), + ); when(serviceContainer.get(IInterpreterService)).thenReturn(instance(interpreterService)); + const onDidChangePythonEnvironment = new EventEmitter(); + const jupyterApi: JupyterPythonEnvironmentApi = { + onDidChangePythonEnvironment: onDidChangePythonEnvironment.event, + getPythonEnvironment: (_uri: Uri) => undefined, + }; + when(serviceContainer.get(JupyterExtensionPythonEnvironments)).thenReturn( + jupyterApi, + ); when(serviceContainer.get(IDisposableRegistry)).thenReturn([]); + getDebugpyPathStub = sinon.stub(pythonDebugger, 'getDebugpyPath'); + getDebugpyPathStub.resolves(debuggerPath); + }); + + teardown(() => { + sinon.restore(); }); test('Test debug launcher args (no-wait)', async () => { diff --git a/src/test/api.test.ts b/src/test/api.test.ts index 24eb78c11bf0..f0813ce16a9b 100644 --- a/src/test/api.test.ts +++ b/src/test/api.test.ts @@ -2,12 +2,12 @@ // Licensed under the MIT License. import { expect } from 'chai'; -import { IExtensionApi } from '../client/apiTypes'; +import { PythonExtension } from '../client/api/types'; import { ProposedExtensionAPI } from '../client/proposedApiTypes'; import { initialize } from './initialize'; suite('Python API tests', () => { - let api: IExtensionApi & ProposedExtensionAPI; + let api: PythonExtension & ProposedExtensionAPI; suiteSetup(async () => { api = await initialize(); }); diff --git a/src/test/application/diagnostics/applicationDiagnostics.unit.test.ts b/src/test/application/diagnostics/applicationDiagnostics.unit.test.ts index 48ee860dc6bb..3a2b9c2f62dd 100644 --- a/src/test/application/diagnostics/applicationDiagnostics.unit.test.ts +++ b/src/test/application/diagnostics/applicationDiagnostics.unit.test.ts @@ -10,12 +10,7 @@ import { DiagnosticSeverity } from 'vscode'; import { ApplicationDiagnostics } from '../../../client/application/diagnostics/applicationDiagnostics'; import { EnvironmentPathVariableDiagnosticsService } from '../../../client/application/diagnostics/checks/envPathVariable'; import { InvalidPythonInterpreterService } from '../../../client/application/diagnostics/checks/pythonInterpreter'; -import { - DiagnosticScope, - IDiagnostic, - IDiagnosticsService, - ISourceMapSupportService, -} from '../../../client/application/diagnostics/types'; +import { DiagnosticScope, IDiagnostic, IDiagnosticsService } from '../../../client/application/diagnostics/types'; import { IApplicationDiagnostics } from '../../../client/application/types'; import { IWorkspaceService } from '../../../client/common/application/types'; import { createDeferred, createDeferredFromPromise } from '../../../client/common/utils/async'; @@ -62,19 +57,6 @@ suite('Application Diagnostics - ApplicationDiagnostics', () => { process.env.VSC_PYTHON_CI_TEST = oldValueOfVSC_PYTHON_CI_TEST; }); - test('Register should register source maps', () => { - const sourceMapService = typemoq.Mock.ofType(); - sourceMapService.setup((s) => s.register()).verifiable(typemoq.Times.once()); - - serviceContainer - .setup((d) => d.get(typemoq.It.isValue(ISourceMapSupportService), typemoq.It.isAny())) - .returns(() => sourceMapService.object); - - appDiagnostics.register(); - - sourceMapService.verifyAll(); - }); - test('Performing Pre Startup Health Check must diagnose all validation checks', async () => { envHealthCheck .setup((e) => e.diagnose(typemoq.It.isAny())) diff --git a/src/test/application/diagnostics/checks/invalidLaunchJsonDebugger.unit.test.ts b/src/test/application/diagnostics/checks/invalidLaunchJsonDebugger.unit.test.ts deleted file mode 100644 index d4eefd69dd5f..000000000000 --- a/src/test/application/diagnostics/checks/invalidLaunchJsonDebugger.unit.test.ts +++ /dev/null @@ -1,462 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; - -import { expect } from 'chai'; -import * as TypeMoq from 'typemoq'; -import { Uri, WorkspaceFolder } from 'vscode'; -import { BaseDiagnosticsService } from '../../../../client/application/diagnostics/base'; -import { - InvalidLaunchJsonDebuggerDiagnostic, - InvalidLaunchJsonDebuggerService, -} from '../../../../client/application/diagnostics/checks/invalidLaunchJsonDebugger'; -import { IDiagnosticsCommandFactory } from '../../../../client/application/diagnostics/commands/types'; -import { DiagnosticCodes } from '../../../../client/application/diagnostics/constants'; -import { MessageCommandPrompt } from '../../../../client/application/diagnostics/promptHandler'; -import { - IDiagnostic, - IDiagnosticHandlerService, - IDiagnosticsService, -} from '../../../../client/application/diagnostics/types'; -import { IWorkspaceService } from '../../../../client/common/application/types'; -import { IFileSystem } from '../../../../client/common/platform/types'; -import { Diagnostics } from '../../../../client/common/utils/localize'; -import { IServiceContainer } from '../../../../client/ioc/types'; - -suite('Application Diagnostics - Checks if launch.json is invalid', () => { - let serviceContainer: TypeMoq.IMock; - let diagnosticService: IDiagnosticsService; - let commandFactory: TypeMoq.IMock; - let fs: TypeMoq.IMock; - let workspaceService: TypeMoq.IMock; - let baseWorkspaceService: TypeMoq.IMock; - let messageHandler: TypeMoq.IMock>; - let workspaceFolder: WorkspaceFolder; - - setup(() => { - workspaceFolder = { uri: Uri.parse('full/path/to/workspace'), name: '', index: 0 }; - serviceContainer = TypeMoq.Mock.ofType(); - commandFactory = TypeMoq.Mock.ofType(); - fs = TypeMoq.Mock.ofType(); - messageHandler = TypeMoq.Mock.ofType>(); - workspaceService = TypeMoq.Mock.ofType(); - baseWorkspaceService = TypeMoq.Mock.ofType(); - serviceContainer - .setup((s) => s.get(TypeMoq.It.isValue(IWorkspaceService))) - .returns(() => baseWorkspaceService.object); - - diagnosticService = new (class extends InvalidLaunchJsonDebuggerService { - public _clear() { - while (BaseDiagnosticsService.handledDiagnosticCodeKeys.length > 0) { - BaseDiagnosticsService.handledDiagnosticCodeKeys.shift(); - } - } - public async fixLaunchJson(code: DiagnosticCodes) { - await super.fixLaunchJson(code); - } - })(serviceContainer.object, fs.object, [], workspaceService.object, messageHandler.object); - (diagnosticService as any)._clear(); - }); - - test('Can handle all InvalidLaunchJsonDebugger diagnostics', async () => { - for (const code of [ - DiagnosticCodes.InvalidDebuggerTypeDiagnostic, - DiagnosticCodes.JustMyCodeDiagnostic, - DiagnosticCodes.ConsoleTypeDiagnostic, - ]) { - const diagnostic = TypeMoq.Mock.ofType(); - diagnostic - .setup((d) => d.code) - .returns(() => code) - .verifiable(TypeMoq.Times.atLeastOnce()); - - const canHandle = await diagnosticService.canHandle(diagnostic.object); - expect(canHandle).to.be.equal(true, `Should be able to handle ${code}`); - diagnostic.verifyAll(); - } - }); - - test('Can not handle non-InvalidLaunchJsonDebugger diagnostics', async () => { - const diagnostic = TypeMoq.Mock.ofType(); - diagnostic - .setup((d) => d.code) - .returns(() => 'Something Else' as any) - .verifiable(TypeMoq.Times.atLeastOnce()); - - const canHandle = await diagnosticService.canHandle(diagnostic.object); - expect(canHandle).to.be.equal(false, 'Invalid value'); - diagnostic.verifyAll(); - }); - - test('Should return empty diagnostics if there are no workspace folders', async () => { - const diagnostics = await diagnosticService.diagnose(undefined); - expect(diagnostics).to.be.deep.equal([]); - workspaceService.verifyAll(); - }); - - test('Should return empty diagnostics if file launch.json does not exist', async () => { - workspaceService - .setup((w) => w.workspaceFolders) - .returns(() => [workspaceFolder]) - .verifiable(TypeMoq.Times.atLeastOnce()); - workspaceService - .setup((w) => w.getWorkspaceFolder(undefined)) - .returns(() => undefined) - .verifiable(TypeMoq.Times.never()); - fs.setup((w) => w.fileExists(TypeMoq.It.isAny())) - .returns(() => Promise.resolve(false)) - .verifiable(TypeMoq.Times.once()); - const diagnostics = await diagnosticService.diagnose(undefined); - expect(diagnostics).to.be.deep.equal([]); - workspaceService.verifyAll(); - fs.verifyAll(); - }); - - test('Should return empty diagnostics if file launch.json does not contain strings "pythonExperimental" and "debugStdLib" ', async () => { - const fileContents = 'Hello I am launch.json, although I am not very jsony'; - workspaceService - .setup((w) => w.workspaceFolders) - .returns(() => [workspaceFolder]) - .verifiable(TypeMoq.Times.atLeastOnce()); - fs.setup((w) => w.fileExists(TypeMoq.It.isAny())) - .returns(() => Promise.resolve(true)) - .verifiable(TypeMoq.Times.once()); - fs.setup((w) => w.readFile(TypeMoq.It.isAny())) - .returns(() => Promise.resolve(fileContents)) - .verifiable(TypeMoq.Times.once()); - const diagnostics = await diagnosticService.diagnose(undefined); - expect(diagnostics).to.be.deep.equal([]); - workspaceService.verifyAll(); - fs.verifyAll(); - }); - - test('Should return InvalidDebuggerTypeDiagnostic if file launch.json contains string "pythonExperimental"', async () => { - const fileContents = 'Hello I am launch.json, I contain string "pythonExperimental"'; - workspaceService - .setup((w) => w.workspaceFolders) - .returns(() => [workspaceFolder]) - .verifiable(TypeMoq.Times.atLeastOnce()); - fs.setup((w) => w.fileExists(TypeMoq.It.isAny())) - .returns(() => Promise.resolve(true)) - .verifiable(TypeMoq.Times.once()); - fs.setup((w) => w.readFile(TypeMoq.It.isAny())) - .returns(() => Promise.resolve(fileContents)) - .verifiable(TypeMoq.Times.once()); - const diagnostics = await diagnosticService.diagnose(undefined); - expect(diagnostics).to.be.deep.equal( - [new InvalidLaunchJsonDebuggerDiagnostic(DiagnosticCodes.InvalidDebuggerTypeDiagnostic, undefined)], - 'Diagnostics returned are not as expected', - ); - workspaceService.verifyAll(); - fs.verifyAll(); - }); - - test('Should return JustMyCodeDiagnostic if file launch.json contains string "debugStdLib"', async () => { - const fileContents = 'Hello I am launch.json, I contain string "debugStdLib"'; - workspaceService - .setup((w) => w.workspaceFolders) - .returns(() => [workspaceFolder]) - .verifiable(TypeMoq.Times.atLeastOnce()); - fs.setup((w) => w.fileExists(TypeMoq.It.isAny())) - .returns(() => Promise.resolve(true)) - .verifiable(TypeMoq.Times.once()); - fs.setup((w) => w.readFile(TypeMoq.It.isAny())) - .returns(() => Promise.resolve(fileContents)) - .verifiable(TypeMoq.Times.once()); - const diagnostics = await diagnosticService.diagnose(undefined); - expect(diagnostics).to.be.deep.equal( - [new InvalidLaunchJsonDebuggerDiagnostic(DiagnosticCodes.JustMyCodeDiagnostic, undefined)], - 'Diagnostics returned are not as expected', - ); - workspaceService.verifyAll(); - fs.verifyAll(); - }); - - test('Should return ConfigPythonPathDiagnostic if file launch.json contains string "{config:python.pythonPath}"', async () => { - const fileContents = 'Hello I am launch.json, I contain string {config:python.pythonPath}'; - workspaceService - .setup((w) => w.workspaceFolders) - .returns(() => [workspaceFolder]) - .verifiable(TypeMoq.Times.atLeastOnce()); - fs.setup((w) => w.fileExists(TypeMoq.It.isAny())) - .returns(() => Promise.resolve(true)) - .verifiable(TypeMoq.Times.once()); - fs.setup((w) => w.readFile(TypeMoq.It.isAny())) - .returns(() => Promise.resolve(fileContents)) - .verifiable(TypeMoq.Times.once()); - const diagnostics = await diagnosticService.diagnose(undefined); - expect(diagnostics).to.be.deep.equal( - [new InvalidLaunchJsonDebuggerDiagnostic(DiagnosticCodes.ConfigPythonPathDiagnostic, undefined, false)], - 'Diagnostics returned are not as expected', - ); - workspaceService.verifyAll(); - fs.verifyAll(); - }); - - test('Should return ConfigPythonPathDiagnostic if file launch.json contains string "{config:python.interpreterPath}"', async () => { - const fileContents = 'Hello I am launch.json, I contain string {config:python.interpreterPath}'; - workspaceService - .setup((w) => w.workspaceFolders) - .returns(() => [workspaceFolder]) - .verifiable(TypeMoq.Times.atLeastOnce()); - fs.setup((w) => w.fileExists(TypeMoq.It.isAny())) - .returns(() => Promise.resolve(true)) - .verifiable(TypeMoq.Times.once()); - fs.setup((w) => w.readFile(TypeMoq.It.isAny())) - .returns(() => Promise.resolve(fileContents)) - .verifiable(TypeMoq.Times.once()); - const diagnostics = await diagnosticService.diagnose(undefined); - expect(diagnostics).to.be.deep.equal( - [new InvalidLaunchJsonDebuggerDiagnostic(DiagnosticCodes.ConfigPythonPathDiagnostic, undefined, false)], - 'Diagnostics returned are not as expected', - ); - workspaceService.verifyAll(); - fs.verifyAll(); - }); - - test('Should return both diagnostics if file launch.json contains string "debugStdLib" and "pythonExperimental"', async () => { - const fileContents = 'Hello I am launch.json, I contain both "debugStdLib" and "pythonExperimental"'; - workspaceService - .setup((w) => w.workspaceFolders) - .returns(() => [workspaceFolder]) - .verifiable(TypeMoq.Times.atLeastOnce()); - fs.setup((w) => w.fileExists(TypeMoq.It.isAny())) - .returns(() => Promise.resolve(true)) - .verifiable(TypeMoq.Times.once()); - fs.setup((w) => w.readFile(TypeMoq.It.isAny())) - .returns(() => Promise.resolve(fileContents)) - .verifiable(TypeMoq.Times.once()); - const diagnostics = await diagnosticService.diagnose(undefined); - expect(diagnostics).to.be.deep.equal( - [ - new InvalidLaunchJsonDebuggerDiagnostic(DiagnosticCodes.InvalidDebuggerTypeDiagnostic, undefined), - new InvalidLaunchJsonDebuggerDiagnostic(DiagnosticCodes.JustMyCodeDiagnostic, undefined), - ], - 'Diagnostics returned are not as expected', - ); - workspaceService.verifyAll(); - fs.verifyAll(); - }); - - test('All InvalidLaunchJsonDebugger diagnostics with `shouldShowPrompt` set to `true` should display a prompt with 2 buttons where clicking the first button will invoke a command', async () => { - for (const code of [ - DiagnosticCodes.InvalidDebuggerTypeDiagnostic, - DiagnosticCodes.JustMyCodeDiagnostic, - DiagnosticCodes.ConsoleTypeDiagnostic, - ]) { - const diagnostic = TypeMoq.Mock.ofType(); - let options: MessageCommandPrompt | undefined; - diagnostic - .setup((d) => d.code) - .returns(() => code) - .verifiable(TypeMoq.Times.atLeastOnce()); - diagnostic - .setup((d) => d.shouldShowPrompt) - .returns(() => true) - .verifiable(TypeMoq.Times.atLeastOnce()); - messageHandler - .setup((m) => m.handle(TypeMoq.It.isAny(), TypeMoq.It.isAny())) - .callback((_, opts: MessageCommandPrompt) => (options = opts)) - .verifiable(TypeMoq.Times.atLeastOnce()); - baseWorkspaceService - .setup((c) => c.getWorkspaceFolder(TypeMoq.It.isAny())) - .returns(() => workspaceFolder) - .verifiable(TypeMoq.Times.atLeastOnce()); - - await diagnosticService.handle([diagnostic.object]); - - diagnostic.verifyAll(); - commandFactory.verifyAll(); - messageHandler.verifyAll(); - baseWorkspaceService.verifyAll(); - expect(options!.commandPrompts).to.be.lengthOf(2); - expect(options!.commandPrompts[0].prompt).to.be.equal(Diagnostics.yesUpdateLaunch); - expect(options!.commandPrompts[0].command).not.to.be.equal(undefined, 'Command not set'); - } - }); - - test('All InvalidLaunchJsonDebugger diagnostics with `shouldShowPrompt` set to `false` should directly fix launch.json', async () => { - for (const code of [DiagnosticCodes.ConfigPythonPathDiagnostic]) { - let called = false; - (diagnosticService as any).fixLaunchJson = () => { - called = true; - }; - const diagnostic = TypeMoq.Mock.ofType(); - diagnostic - .setup((d) => d.code) - .returns(() => code) - .verifiable(TypeMoq.Times.atLeastOnce()); - diagnostic - .setup((d) => d.shouldShowPrompt) - .returns(() => false) - .verifiable(TypeMoq.Times.atLeastOnce()); - messageHandler - .setup((m) => m.handle(TypeMoq.It.isAny(), TypeMoq.It.isAny())) - .verifiable(TypeMoq.Times.never()); - baseWorkspaceService - .setup((c) => c.getWorkspaceFolder(TypeMoq.It.isAny())) - .returns(() => workspaceFolder) - .verifiable(TypeMoq.Times.atLeastOnce()); - - await diagnosticService.handle([diagnostic.object]); - - diagnostic.verifyAll(); - commandFactory.verifyAll(); - messageHandler.verifyAll(); - baseWorkspaceService.verifyAll(); - expect(called).to.equal(true, ''); - } - }); - - test('All InvalidLaunchJsonDebugger diagnostics should display message twice if invoked twice', async () => { - for (const code of [ - DiagnosticCodes.InvalidDebuggerTypeDiagnostic, - DiagnosticCodes.JustMyCodeDiagnostic, - DiagnosticCodes.ConsoleTypeDiagnostic, - ]) { - const diagnostic = TypeMoq.Mock.ofType(); - diagnostic - .setup((d) => d.code) - .returns(() => code) - .verifiable(TypeMoq.Times.atLeastOnce()); - diagnostic - .setup((d) => d.invokeHandler) - .returns(() => 'always') - .verifiable(TypeMoq.Times.atLeastOnce()); - messageHandler.reset(); - messageHandler - .setup((m) => m.handle(TypeMoq.It.isAny(), TypeMoq.It.isAny())) - .verifiable(TypeMoq.Times.exactly(2)); - baseWorkspaceService - .setup((c) => c.getWorkspaceFolder(TypeMoq.It.isAny())) - .returns(() => workspaceFolder) - .verifiable(TypeMoq.Times.never()); - - await diagnosticService.handle([diagnostic.object]); - await diagnosticService.handle([diagnostic.object]); - - diagnostic.verifyAll(); - commandFactory.verifyAll(); - messageHandler.verifyAll(); - baseWorkspaceService.verifyAll(); - } - }); - - const codes = [ - DiagnosticCodes.InvalidDebuggerTypeDiagnostic, - DiagnosticCodes.JustMyCodeDiagnostic, - DiagnosticCodes.ConsoleTypeDiagnostic, - ]; - - codes.forEach((code) => { - test('Function fixLaunchJson() returns if there are no workspace folders', async () => { - workspaceService - .setup((w) => w.workspaceFolders) - .returns(() => undefined) - .verifiable(TypeMoq.Times.atLeastOnce()); - await (diagnosticService as any).fixLaunchJson(code); - workspaceService.verifyAll(); - }); - - test('Function fixLaunchJson() returns if file launch.json does not exist', async () => { - workspaceService - .setup((w) => w.workspaceFolders) - .returns(() => [workspaceFolder]) - .verifiable(TypeMoq.Times.atLeastOnce()); - fs.setup((w) => w.fileExists(TypeMoq.It.isAny())) - .returns(() => Promise.resolve(false)) - .verifiable(TypeMoq.Times.atLeastOnce()); - fs.setup((w) => w.readFile(TypeMoq.It.isAny())) - .returns(() => Promise.resolve('')) - .verifiable(TypeMoq.Times.never()); - await (diagnosticService as any).fixLaunchJson(code); - workspaceService.verifyAll(); - fs.verifyAll(); - }); - }); - - test('File launch.json is fixed correctly when code equals JustMyCodeDiagnostic', async () => { - const launchJson = '{"debugStdLib": true, "debugStdLib": false}'; - const correctedlaunchJson = '{"justMyCode": false, "justMyCode": true}'; - workspaceService - .setup((w) => w.workspaceFolders) - .returns(() => [workspaceFolder]) - .verifiable(TypeMoq.Times.atLeastOnce()); - fs.setup((w) => w.fileExists(TypeMoq.It.isAny())) - .returns(() => Promise.resolve(true)) - .verifiable(TypeMoq.Times.once()); - fs.setup((w) => w.readFile(TypeMoq.It.isAny())) - .returns(() => Promise.resolve(launchJson)) - .verifiable(TypeMoq.Times.atLeastOnce()); - fs.setup((w) => w.writeFile(TypeMoq.It.isAnyString(), correctedlaunchJson)) - .returns(() => Promise.resolve()) - .verifiable(TypeMoq.Times.once()); - await (diagnosticService as any).fixLaunchJson(DiagnosticCodes.JustMyCodeDiagnostic); - workspaceService.verifyAll(); - fs.verifyAll(); - }); - - test('File launch.json is fixed correctly when code equals InvalidDebuggerTypeDiagnostic', async () => { - const launchJson = '{"Python Experimental: task" "pythonExperimental"}'; - const correctedlaunchJson = '{"Python: task" "python"}'; - workspaceService - .setup((w) => w.workspaceFolders) - .returns(() => [workspaceFolder]) - .verifiable(TypeMoq.Times.atLeastOnce()); - fs.setup((w) => w.fileExists(TypeMoq.It.isAny())) - .returns(() => Promise.resolve(true)) - .verifiable(TypeMoq.Times.once()); - fs.setup((w) => w.readFile(TypeMoq.It.isAny())) - .returns(() => Promise.resolve(launchJson)) - .verifiable(TypeMoq.Times.atLeastOnce()); - fs.setup((w) => w.writeFile(TypeMoq.It.isAnyString(), correctedlaunchJson)) - .returns(() => Promise.resolve()) - .verifiable(TypeMoq.Times.once()); - await (diagnosticService as any).fixLaunchJson(DiagnosticCodes.InvalidDebuggerTypeDiagnostic); - workspaceService.verifyAll(); - fs.verifyAll(); - }); - - test('File launch.json is fixed correctly when code equals ConsoleTypeDiagnostic', async () => { - const launchJson = '{"console": "none"}'; - const correctedlaunchJson = '{"console": "internalConsole"}'; - workspaceService - .setup((w) => w.workspaceFolders) - .returns(() => [workspaceFolder]) - .verifiable(TypeMoq.Times.atLeastOnce()); - fs.setup((w) => w.fileExists(TypeMoq.It.isAny())) - .returns(() => Promise.resolve(true)) - .verifiable(TypeMoq.Times.once()); - fs.setup((w) => w.readFile(TypeMoq.It.isAny())) - .returns(() => Promise.resolve(launchJson)) - .verifiable(TypeMoq.Times.atLeastOnce()); - fs.setup((w) => w.writeFile(TypeMoq.It.isAnyString(), correctedlaunchJson)) - .returns(() => Promise.resolve()) - .verifiable(TypeMoq.Times.once()); - await (diagnosticService as any).fixLaunchJson(DiagnosticCodes.ConsoleTypeDiagnostic); - workspaceService.verifyAll(); - fs.verifyAll(); - }); - - test('File launch.json is fixed correctly when code equals ConfigPythonPathDiagnostic', async () => { - const launchJson = '"pythonPath": "{config:python.pythonPath}{config:python.interpreterPath}"'; - const correctedlaunchJson = '"python": "{command:python.interpreterPath}{command:python.interpreterPath}"'; - workspaceService - .setup((w) => w.workspaceFolders) - .returns(() => [workspaceFolder]) - .verifiable(TypeMoq.Times.atLeastOnce()); - fs.setup((w) => w.fileExists(TypeMoq.It.isAny())) - .returns(() => Promise.resolve(true)) - .verifiable(TypeMoq.Times.once()); - fs.setup((w) => w.readFile(TypeMoq.It.isAny())) - .returns(() => Promise.resolve(launchJson)) - .verifiable(TypeMoq.Times.atLeastOnce()); - fs.setup((w) => w.writeFile(TypeMoq.It.isAnyString(), correctedlaunchJson)) - .returns(() => Promise.resolve()) - .verifiable(TypeMoq.Times.once()); - await (diagnosticService as any).fixLaunchJson(DiagnosticCodes.ConfigPythonPathDiagnostic); - workspaceService.verifyAll(); - fs.verifyAll(); - }); -}); diff --git a/src/test/application/diagnostics/checks/invalidPythonPathInDebugger.unit.test.ts b/src/test/application/diagnostics/checks/invalidPythonPathInDebugger.unit.test.ts deleted file mode 100644 index 8c835003ffef..000000000000 --- a/src/test/application/diagnostics/checks/invalidPythonPathInDebugger.unit.test.ts +++ /dev/null @@ -1,417 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; - -import { expect } from 'chai'; -import * as path from 'path'; -import * as typemoq from 'typemoq'; -import { Uri } from 'vscode'; -import { BaseDiagnosticsService } from '../../../../client/application/diagnostics/base'; -import { InvalidPythonPathInDebuggerService } from '../../../../client/application/diagnostics/checks/invalidPythonPathInDebugger'; -import { CommandOption, IDiagnosticsCommandFactory } from '../../../../client/application/diagnostics/commands/types'; -import { DiagnosticCodes } from '../../../../client/application/diagnostics/constants'; -import { - DiagnosticCommandPromptHandlerServiceId, - MessageCommandPrompt, -} from '../../../../client/application/diagnostics/promptHandler'; -import { - IDiagnostic, - IDiagnosticCommand, - IDiagnosticHandlerService, - IInvalidPythonPathInDebuggerService, -} from '../../../../client/application/diagnostics/types'; -import { CommandsWithoutArgs } from '../../../../client/common/application/commands'; -import { IDocumentManager, IWorkspaceService } from '../../../../client/common/application/types'; -import { IConfigurationService, IPythonSettings } from '../../../../client/common/types'; -import { PythonPathSource } from '../../../../client/debugger/extension/types'; -import { IInterpreterHelper } from '../../../../client/interpreter/contracts'; -import { IServiceContainer } from '../../../../client/ioc/types'; - -suite('Application Diagnostics - Checks Python Path in debugger', () => { - let diagnosticService: IInvalidPythonPathInDebuggerService; - let messageHandler: typemoq.IMock>; - let commandFactory: typemoq.IMock; - let configService: typemoq.IMock; - let helper: typemoq.IMock; - let workspaceService: typemoq.IMock; - let docMgr: typemoq.IMock; - setup(() => { - const serviceContainer = typemoq.Mock.ofType(); - messageHandler = typemoq.Mock.ofType>(); - serviceContainer - .setup((s) => - s.get( - typemoq.It.isValue(IDiagnosticHandlerService), - typemoq.It.isValue(DiagnosticCommandPromptHandlerServiceId), - ), - ) - .returns(() => messageHandler.object); - commandFactory = typemoq.Mock.ofType(); - docMgr = typemoq.Mock.ofType(); - serviceContainer - .setup((s) => s.get(typemoq.It.isValue(IDiagnosticsCommandFactory))) - .returns(() => commandFactory.object); - configService = typemoq.Mock.ofType(); - serviceContainer - .setup((s) => s.get(typemoq.It.isValue(IConfigurationService))) - .returns(() => configService.object); - helper = typemoq.Mock.ofType(); - serviceContainer.setup((s) => s.get(typemoq.It.isValue(IInterpreterHelper))).returns(() => helper.object); - workspaceService = typemoq.Mock.ofType(); - serviceContainer - .setup((s) => s.get(typemoq.It.isValue(IWorkspaceService))) - .returns(() => workspaceService.object); - - diagnosticService = new (class extends InvalidPythonPathInDebuggerService { - public _clear() { - while (BaseDiagnosticsService.handledDiagnosticCodeKeys.length > 0) { - BaseDiagnosticsService.handledDiagnosticCodeKeys.shift(); - } - } - })( - serviceContainer.object, - workspaceService.object, - commandFactory.object, - helper.object, - docMgr.object, - configService.object, - [], - messageHandler.object, - ); - (diagnosticService as any)._clear(); - }); - - test('Can handle InvalidPythonPathInDebugger diagnostics', async () => { - for (const code of [ - DiagnosticCodes.InvalidPythonPathInDebuggerSettingsDiagnostic, - DiagnosticCodes.InvalidPythonPathInDebuggerLaunchDiagnostic, - ]) { - const diagnostic = typemoq.Mock.ofType(); - diagnostic - .setup((d) => d.code) - .returns(() => code) - .verifiable(typemoq.Times.atLeastOnce()); - - const canHandle = await diagnosticService.canHandle(diagnostic.object); - expect(canHandle).to.be.equal(true, `Should be able to handle ${code}`); - diagnostic.verifyAll(); - } - }); - test('Can not handle non-InvalidPythonPathInDebugger diagnostics', async () => { - const diagnostic = typemoq.Mock.ofType(); - diagnostic - .setup((d) => d.code) - .returns(() => 'Something Else' as any) - .verifiable(typemoq.Times.atLeastOnce()); - - const canHandle = await diagnosticService.canHandle(diagnostic.object); - expect(canHandle).to.be.equal(false, 'Invalid value'); - diagnostic.verifyAll(); - }); - test('Should return empty diagnostics', async () => { - const diagnostics = await diagnosticService.diagnose(undefined); - expect(diagnostics).to.be.deep.equal([]); - }); - test('InvalidPythonPathInDebuggerSettings diagnostic should display one option to with a command', async () => { - const diagnostic = typemoq.Mock.ofType(); - diagnostic - .setup((d) => d.code) - .returns(() => DiagnosticCodes.InvalidPythonPathInDebuggerSettingsDiagnostic) - .verifiable(typemoq.Times.atLeastOnce()); - const interpreterSelectionCommand = typemoq.Mock.ofType(); - commandFactory - .setup((f) => - f.createCommand( - typemoq.It.isAny(), - typemoq.It.isObjectWith>({ - type: 'executeVSCCommand', - }), - ), - ) - .returns(() => interpreterSelectionCommand.object) - .verifiable(typemoq.Times.once()); - messageHandler.setup((m) => m.handle(typemoq.It.isAny(), typemoq.It.isAny())).verifiable(typemoq.Times.once()); - - await diagnosticService.handle([diagnostic.object]); - - diagnostic.verifyAll(); - commandFactory.verifyAll(); - messageHandler.verifyAll(); - }); - test('InvalidPythonPathInDebuggerSettings diagnostic should display message once if invoked twice', async () => { - const diagnostic = typemoq.Mock.ofType(); - diagnostic - .setup((d) => d.code) - .returns(() => DiagnosticCodes.InvalidPythonPathInDebuggerSettingsDiagnostic) - .verifiable(typemoq.Times.atLeastOnce()); - diagnostic - .setup((d) => d.invokeHandler) - .returns(() => 'default') - .verifiable(typemoq.Times.atLeastOnce()); - const interpreterSelectionCommand = typemoq.Mock.ofType(); - commandFactory - .setup((f) => - f.createCommand( - typemoq.It.isAny(), - typemoq.It.isObjectWith>({ - type: 'executeVSCCommand', - }), - ), - ) - .returns(() => interpreterSelectionCommand.object) - .verifiable(typemoq.Times.exactly(1)); - messageHandler - .setup((m) => m.handle(typemoq.It.isAny(), typemoq.It.isAny())) - .verifiable(typemoq.Times.exactly(1)); - - await diagnosticService.handle([diagnostic.object]); - await diagnosticService.handle([diagnostic.object]); - - diagnostic.verifyAll(); - commandFactory.verifyAll(); - messageHandler.verifyAll(); - }); - test('InvalidPythonPathInDebuggerSettings diagnostic should display message twice if invoked twice', async () => { - const diagnostic = typemoq.Mock.ofType(); - diagnostic - .setup((d) => d.code) - .returns(() => DiagnosticCodes.InvalidPythonPathInDebuggerSettingsDiagnostic) - .verifiable(typemoq.Times.atLeastOnce()); - diagnostic - .setup((d) => d.invokeHandler) - .returns(() => 'always') - .verifiable(typemoq.Times.atLeastOnce()); - const interpreterSelectionCommand = typemoq.Mock.ofType(); - commandFactory - .setup((f) => - f.createCommand( - typemoq.It.isAny(), - typemoq.It.isObjectWith>({ - type: 'executeVSCCommand', - }), - ), - ) - .returns(() => interpreterSelectionCommand.object) - .verifiable(typemoq.Times.exactly(2)); - messageHandler - .setup((m) => m.handle(typemoq.It.isAny(), typemoq.It.isAny())) - .verifiable(typemoq.Times.exactly(2)); - - await diagnosticService.handle([diagnostic.object]); - await diagnosticService.handle([diagnostic.object]); - - diagnostic.verifyAll(); - commandFactory.verifyAll(); - messageHandler.verifyAll(); - }); - test('InvalidPythonPathInDebuggerLaunch diagnostic should display one option to with a command', async () => { - const diagnostic = typemoq.Mock.ofType(); - let options: MessageCommandPrompt | undefined; - diagnostic - .setup((d) => d.code) - .returns(() => DiagnosticCodes.InvalidPythonPathInDebuggerLaunchDiagnostic) - .verifiable(typemoq.Times.atLeastOnce()); - messageHandler - .setup((m) => m.handle(typemoq.It.isAny(), typemoq.It.isAny())) - .callback((_, opts: MessageCommandPrompt) => (options = opts)) - .verifiable(typemoq.Times.once()); - - await diagnosticService.handle([diagnostic.object]); - - diagnostic.verifyAll(); - commandFactory.verifyAll(); - messageHandler.verifyAll(); - expect(options!.commandPrompts).to.be.lengthOf(1); - expect(options!.commandPrompts[0].prompt).to.be.equal('Open launch.json'); - }); - test('Ensure we get python path from config when path = ${command:python.interpreterPath}', async () => { - const pythonPath = '${command:python.interpreterPath}'; - - const settings = typemoq.Mock.ofType(); - settings - .setup((s) => s.pythonPath) - .returns(() => 'p') - .verifiable(typemoq.Times.once()); - configService - .setup((c) => c.getSettings(typemoq.It.isAny())) - .returns(() => settings.object) - .verifiable(typemoq.Times.once()); - helper - .setup((h) => h.getInterpreterInformation(typemoq.It.isValue('p'))) - .returns(() => Promise.resolve({})) - .verifiable(typemoq.Times.once()); - - const valid = await diagnosticService.validatePythonPath(pythonPath); - - settings.verifyAll(); - configService.verifyAll(); - helper.verifyAll(); - expect(valid).to.be.equal(true, 'not valid'); - }); - test('Ensure ${workspaceFolder} is not expanded when a resource is not passed', async () => { - const pythonPath = '${workspaceFolder}/venv/bin/python'; - - workspaceService - .setup((c) => c.getWorkspaceFolder(typemoq.It.isAny())) - .returns(() => undefined) - .verifiable(typemoq.Times.never()); - helper - .setup((h) => h.getInterpreterInformation(typemoq.It.isAny())) - .returns(() => Promise.resolve({})) - .verifiable(typemoq.Times.once()); - - await diagnosticService.validatePythonPath(pythonPath); - - configService.verifyAll(); - helper.verifyAll(); - }); - test('Ensure ${workspaceFolder} is expanded', async () => { - const pythonPath = '${workspaceFolder}/venv/bin/python'; - - const workspaceFolder = { uri: Uri.parse('full/path/to/workspace'), name: '', index: 0 }; - const expectedPath = `${workspaceFolder.uri.fsPath}/venv/bin/python`; - - workspaceService - .setup((c) => c.getWorkspaceFolder(typemoq.It.isAny())) - .returns(() => workspaceFolder) - .verifiable(typemoq.Times.once()); - helper - .setup((h) => h.getInterpreterInformation(typemoq.It.isValue(expectedPath))) - .returns(() => Promise.resolve({})) - .verifiable(typemoq.Times.once()); - - const valid = await diagnosticService.validatePythonPath( - pythonPath, - PythonPathSource.settingsJson, - Uri.parse('something'), - ); - - configService.verifyAll(); - helper.verifyAll(); - expect(valid).to.be.equal(true, 'not valid'); - }); - test('Ensure ${env:XYZ123} is expanded', async () => { - const pythonPath = '${env:XYZ123}/venv/bin/python'; - - process.env.XYZ123 = 'something/else'; - const expectedPath = `${process.env.XYZ123}/venv/bin/python`; - workspaceService - .setup((c) => c.getWorkspaceFolder(typemoq.It.isAny())) - .returns(() => undefined) - .verifiable(typemoq.Times.once()); - helper - .setup((h) => h.getInterpreterInformation(typemoq.It.isValue(expectedPath))) - .returns(() => Promise.resolve({})) - .verifiable(typemoq.Times.once()); - - const valid = await diagnosticService.validatePythonPath(pythonPath); - - configService.verifyAll(); - helper.verifyAll(); - expect(valid).to.be.equal(true, 'not valid'); - }); - test('Ensure we get python path from config when path = undefined', async () => { - const pythonPath = undefined; - - const settings = typemoq.Mock.ofType(); - settings - .setup((s) => s.pythonPath) - .returns(() => 'p') - .verifiable(typemoq.Times.once()); - configService - .setup((c) => c.getSettings(typemoq.It.isAny())) - .returns(() => settings.object) - .verifiable(typemoq.Times.once()); - helper - .setup((h) => h.getInterpreterInformation(typemoq.It.isValue('p'))) - .returns(() => Promise.resolve({})) - .verifiable(typemoq.Times.once()); - - const valid = await diagnosticService.validatePythonPath(pythonPath); - - settings.verifyAll(); - configService.verifyAll(); - helper.verifyAll(); - expect(valid).to.be.equal(true, 'not valid'); - }); - test('Ensure we do not get python path from config when path is provided', async () => { - const pythonPath = path.join('a', 'b'); - - const settings = typemoq.Mock.ofType(); - configService - .setup((c) => c.getSettings(typemoq.It.isAny())) - .returns(() => settings.object) - .verifiable(typemoq.Times.never()); - helper - .setup((h) => h.getInterpreterInformation(typemoq.It.isValue(pythonPath))) - .returns(() => Promise.resolve({})) - .verifiable(typemoq.Times.once()); - - const valid = await diagnosticService.validatePythonPath(pythonPath); - - configService.verifyAll(); - helper.verifyAll(); - expect(valid).to.be.equal(true, 'not valid'); - }); - test('Ensure InvalidPythonPathInDebuggerLaunch diagnostic is handled when path is invalid in launch.json', async () => { - const pythonPath = path.join('a', 'b'); - const settings = typemoq.Mock.ofType(); - configService - .setup((c) => c.getSettings(typemoq.It.isAny())) - .returns(() => settings.object) - .verifiable(typemoq.Times.never()); - let handleInvoked = false; - diagnosticService.handle = (diagnostics) => { - if ( - diagnostics.length !== 0 && - diagnostics[0].code === DiagnosticCodes.InvalidPythonPathInDebuggerLaunchDiagnostic - ) { - handleInvoked = true; - } - return Promise.resolve(); - }; - helper - .setup((h) => h.getInterpreterInformation(typemoq.It.isValue(pythonPath))) - .returns(() => Promise.resolve(undefined)) - .verifiable(typemoq.Times.once()); - - const valid = await diagnosticService.validatePythonPath(pythonPath, PythonPathSource.launchJson); - - helper.verifyAll(); - expect(valid).to.be.equal(false, 'should be invalid'); - expect(handleInvoked).to.be.equal(true, 'should be invoked'); - }); - test('Ensure InvalidPythonPathInDebuggerSettings diagnostic is handled when path is invalid in settings.json', async () => { - const pythonPath = undefined; - const settings = typemoq.Mock.ofType(); - settings - .setup((s) => s.pythonPath) - .returns(() => 'p') - .verifiable(typemoq.Times.once()); - configService - .setup((c) => c.getSettings(typemoq.It.isAny())) - .returns(() => settings.object) - .verifiable(typemoq.Times.once()); - let handleInvoked = false; - diagnosticService.handle = (diagnostics) => { - if ( - diagnostics.length !== 0 && - diagnostics[0].code === DiagnosticCodes.InvalidPythonPathInDebuggerSettingsDiagnostic - ) { - handleInvoked = true; - } - return Promise.resolve(); - }; - helper - .setup((h) => h.getInterpreterInformation(typemoq.It.isValue('p'))) - .returns(() => Promise.resolve(undefined)) - .verifiable(typemoq.Times.once()); - - const valid = await diagnosticService.validatePythonPath(pythonPath, PythonPathSource.settingsJson); - - helper.verifyAll(); - expect(valid).to.be.equal(false, 'should be invalid'); - expect(handleInvoked).to.be.equal(true, 'should be invoked'); - }); -}); diff --git a/src/test/application/diagnostics/checks/macPythonInterpreter.unit.test.ts b/src/test/application/diagnostics/checks/macPythonInterpreter.unit.test.ts index f75375520ec8..ba2436d0ffeb 100644 --- a/src/test/application/diagnostics/checks/macPythonInterpreter.unit.test.ts +++ b/src/test/application/diagnostics/checks/macPythonInterpreter.unit.test.ts @@ -220,7 +220,7 @@ suite('Application Diagnostics - Checks Mac Python Interpreter', () => { expect(messagePrompt).not.be.equal(undefined, 'Message prompt not set'); expect(messagePrompt!.commandPrompts).to.be.deep.equal([ { prompt: 'Select Python Interpreter', command: cmd }, - { prompt: 'Do not show again', command: cmdIgnore }, + { prompt: "Don't show again", command: cmdIgnore }, ]); }); test('Should not display a message if No Interpreters diagnostic has been ignored', async () => { diff --git a/src/test/application/diagnostics/checks/pythonInterpreter.unit.test.ts b/src/test/application/diagnostics/checks/pythonInterpreter.unit.test.ts index 2397743274c1..2eecf052e433 100644 --- a/src/test/application/diagnostics/checks/pythonInterpreter.unit.test.ts +++ b/src/test/application/diagnostics/checks/pythonInterpreter.unit.test.ts @@ -7,7 +7,6 @@ import { expect } from 'chai'; import * as typemoq from 'typemoq'; import { EventEmitter, Uri } from 'vscode'; import { BaseDiagnosticsService } from '../../../../client/application/diagnostics/base'; -import { InvalidLaunchJsonDebuggerDiagnostic } from '../../../../client/application/diagnostics/checks/invalidLaunchJsonDebugger'; import { DefaultShellDiagnostic, InvalidPythonInterpreterDiagnostic, @@ -586,39 +585,6 @@ suite('Application Diagnostics - Checks Python Interpreter', () => { await diagnosticServiceMock.object.handle([diagnostic]); - messageHandler.verifyAll(); - commandFactory.verifyAll(); - }); - test('Getting command prompts for an unsupported diagnostic code should throw an error', async () => { - const diagnostic = new InvalidLaunchJsonDebuggerDiagnostic(DiagnosticCodes.JustMyCodeDiagnostic, undefined); - const cmd = ({} as any) as IDiagnosticCommand; - - messageHandler - .setup((i) => i.handle(typemoq.It.isAny(), typemoq.It.isAny())) - .callback((_d, p: MessageCommandPrompt) => p) - .returns(() => Promise.resolve()) - .verifiable(typemoq.Times.never()); - commandFactory - .setup((f) => - f.createCommand( - typemoq.It.isAny(), - typemoq.It.isObjectWith>({ - type: 'executeVSCCommand', - }), - ), - ) - .returns(() => cmd) - .verifiable(typemoq.Times.never()); - - try { - await diagnosticService.handle([diagnostic]); - } catch (err) { - expect((err as Error).message).to.be.equal( - "Invalid diagnostic for 'InvalidPythonInterpreterService'", - 'Error message is different', - ); - } - messageHandler.verifyAll(); commandFactory.verifyAll(); }); diff --git a/src/test/application/diagnostics/sourceMapSupportService.unit.test.ts b/src/test/application/diagnostics/sourceMapSupportService.unit.test.ts deleted file mode 100644 index 3ff429742eb8..000000000000 --- a/src/test/application/diagnostics/sourceMapSupportService.unit.test.ts +++ /dev/null @@ -1,82 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; - -import { anyFunction, anything, instance, mock, verify, when } from 'ts-mockito'; -import { ConfigurationTarget } from 'vscode'; -import { SourceMapSupportService } from '../../../client/application/diagnostics/surceMapSupportService'; -import { ApplicationShell } from '../../../client/common/application/applicationShell'; -import { CommandManager } from '../../../client/common/application/commandManager'; -import { ConfigurationService } from '../../../client/common/configuration/service'; -import { Commands } from '../../../client/common/constants'; -import { Diagnostics } from '../../../client/common/utils/localize'; - -suite('Diagnostisc - Source Maps', () => { - test('Command is registered', async () => { - const commandManager = mock(CommandManager); - const service = new SourceMapSupportService(instance(commandManager), [], undefined as any, undefined as any); - service.register(); - verify(commandManager.registerCommand(Commands.Enable_SourceMap_Support, anyFunction(), service)).once(); - }); - test('Setting is turned on and vsc reloaded', async () => { - const commandManager = mock(CommandManager); - const configService = mock(ConfigurationService); - const service = new SourceMapSupportService( - instance(commandManager), - [], - instance(configService), - undefined as any, - ); - when( - configService.updateSetting('diagnostics.sourceMapsEnabled', true, undefined, ConfigurationTarget.Global), - ).thenResolve(); - when(commandManager.executeCommand('workbench.action.reloadWindow')).thenResolve(); - - await service.enable(); - - verify( - configService.updateSetting('diagnostics.sourceMapsEnabled', true, undefined, ConfigurationTarget.Global), - ).once(); - verify(commandManager.executeCommand('workbench.action.reloadWindow')).once(); - }); - test('Display prompt and do not enable', async () => { - const shell = mock(ApplicationShell); - const service = new (class extends SourceMapSupportService { - public async enable() { - throw new Error('Should not be invokved'); - } - public async onEnable() { - await super.onEnable(); - } - })(undefined as any, [], undefined as any, instance(shell)); - when(shell.showWarningMessage(anything(), anything())).thenResolve(); - - await service.onEnable(); - }); - test('Display prompt and must enable', async () => { - const commandManager = mock(CommandManager); - const configService = mock(ConfigurationService); - const shell = mock(ApplicationShell); - const service = new (class extends SourceMapSupportService { - public async onEnable() { - await super.onEnable(); - } - })(instance(commandManager), [], instance(configService), instance(shell)); - - when( - configService.updateSetting('diagnostics.sourceMapsEnabled', true, undefined, ConfigurationTarget.Global), - ).thenResolve(); - when(shell.showWarningMessage(anything(), anything())).thenResolve( - Diagnostics.enableSourceMapsAndReloadVSC as any, - ); - when(commandManager.executeCommand('workbench.action.reloadWindow')).thenResolve(); - - await service.onEnable(); - - verify( - configService.updateSetting('diagnostics.sourceMapsEnabled', true, undefined, ConfigurationTarget.Global), - ).once(); - verify(commandManager.executeCommand('workbench.action.reloadWindow')).once(); - }); -}); diff --git a/src/test/chat/utils.unit.test.ts b/src/test/chat/utils.unit.test.ts new file mode 100644 index 000000000000..8d45c1ac118f --- /dev/null +++ b/src/test/chat/utils.unit.test.ts @@ -0,0 +1,248 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { expect } from 'chai'; +import * as sinon from 'sinon'; +import { Uri, WorkspaceFolder } from 'vscode'; +import { resolveFilePath } from '../../client/chat/utils'; +import * as workspaceApis from '../../client/common/vscodeApis/workspaceApis'; + +suite('Chat Utils - resolveFilePath()', () => { + let getWorkspaceFoldersStub: sinon.SinonStub; + + setup(() => { + getWorkspaceFoldersStub = sinon.stub(workspaceApis, 'getWorkspaceFolders'); + getWorkspaceFoldersStub.returns([]); + }); + + teardown(() => { + sinon.restore(); + }); + + suite('When filepath is undefined or empty', () => { + test('Should return first workspace folder URI when workspace folders exist', () => { + const expectedUri = Uri.file('/test/workspace'); + const mockFolder: WorkspaceFolder = { + uri: expectedUri, + name: 'test', + index: 0, + }; + getWorkspaceFoldersStub.returns([mockFolder]); + + const result = resolveFilePath(undefined); + + expect(result?.toString()).to.equal(expectedUri.toString()); + }); + + test('Should return first folder when multiple workspace folders exist', () => { + const firstUri = Uri.file('/first/workspace'); + const secondUri = Uri.file('/second/workspace'); + const mockFolders: WorkspaceFolder[] = [ + { uri: firstUri, name: 'first', index: 0 }, + { uri: secondUri, name: 'second', index: 1 }, + ]; + getWorkspaceFoldersStub.returns(mockFolders); + + const result = resolveFilePath(undefined); + + expect(result?.toString()).to.equal(firstUri.toString()); + }); + + test('Should return undefined when no workspace folders exist', () => { + getWorkspaceFoldersStub.returns(undefined); + + const result = resolveFilePath(undefined); + + expect(result).to.be.undefined; + }); + + test('Should return undefined when workspace folders is empty array', () => { + getWorkspaceFoldersStub.returns([]); + + const result = resolveFilePath(undefined); + + expect(result).to.be.undefined; + }); + + test('Should return undefined for empty string when no workspace folders', () => { + getWorkspaceFoldersStub.returns(undefined); + + const result = resolveFilePath(''); + + expect(result).to.be.undefined; + }); + }); + + suite('Windows file paths', () => { + test('Should handle Windows path with lowercase drive letter', () => { + const filepath = 'c:\\GIT\\tests\\simple-python-app'; + + const result = resolveFilePath(filepath); + + expect(result).to.not.be.undefined; + expect(result?.scheme).to.equal('file'); + // Uri.file normalizes drive letters to lowercase + expect(result?.fsPath.toLowerCase()).to.include('git'); + }); + + test('Should handle Windows path with uppercase drive letter', () => { + const filepath = 'C:\\Users\\test\\project'; + + const result = resolveFilePath(filepath); + + expect(result).to.not.be.undefined; + expect(result?.scheme).to.equal('file'); + expect(result?.fsPath.toLowerCase()).to.include('users'); + }); + + test('Should handle Windows path with forward slashes', () => { + const filepath = 'C:/Users/test/project'; + + const result = resolveFilePath(filepath); + + expect(result).to.not.be.undefined; + expect(result?.scheme).to.equal('file'); + }); + }); + + suite('Unix file paths', () => { + test('Should handle Unix absolute path', () => { + const filepath = '/home/user/projects/myapp'; + + const result = resolveFilePath(filepath); + + expect(result).to.not.be.undefined; + expect(result?.scheme).to.equal('file'); + expect(result?.path).to.include('/home/user/projects/myapp'); + }); + + test('Should handle Unix root path', () => { + const filepath = '/'; + + const result = resolveFilePath(filepath); + + expect(result).to.not.be.undefined; + expect(result?.scheme).to.equal('file'); + }); + }); + + suite('Relative paths', () => { + test('Should handle relative path with dot prefix', () => { + const filepath = './src/main.py'; + + const result = resolveFilePath(filepath); + + expect(result).to.not.be.undefined; + expect(result?.scheme).to.equal('file'); + }); + + test('Should handle relative path without prefix', () => { + const filepath = 'src/main.py'; + + const result = resolveFilePath(filepath); + + expect(result).to.not.be.undefined; + expect(result?.scheme).to.equal('file'); + }); + + test('Should handle parent directory reference', () => { + const filepath = '../other-project/file.py'; + + const result = resolveFilePath(filepath); + + expect(result).to.not.be.undefined; + expect(result?.scheme).to.equal('file'); + }); + }); + + suite('URI schemes', () => { + test('Should handle file:// URI scheme', () => { + const filepath = 'file:///home/user/test.py'; + + const result = resolveFilePath(filepath); + + expect(result).to.not.be.undefined; + expect(result?.scheme).to.equal('file'); + expect(result?.path).to.include('/home/user/test.py'); + }); + + test('Should handle vscode-notebook:// URI scheme', () => { + const filepath = 'vscode-notebook://jupyter/notebook.ipynb'; + + const result = resolveFilePath(filepath); + + expect(result).to.not.be.undefined; + expect(result?.scheme).to.equal('vscode-notebook'); + }); + + test('Should handle untitled: URI scheme without double slash as file path', () => { + const filepath = 'untitled:Untitled-1'; + + const result = resolveFilePath(filepath); + + expect(result).to.not.be.undefined; + // untitled: doesn't have ://, so it will be treated as a file path + expect(result?.scheme).to.equal('file'); + }); + + test('Should handle https:// URI scheme', () => { + const filepath = 'https://example.com/path'; + + const result = resolveFilePath(filepath); + + expect(result).to.not.be.undefined; + expect(result?.scheme).to.equal('https'); + }); + + test('Should handle vscode-vfs:// URI scheme', () => { + const filepath = 'vscode-vfs://github/microsoft/vscode/file.ts'; + + const result = resolveFilePath(filepath); + + expect(result).to.not.be.undefined; + expect(result?.scheme).to.equal('vscode-vfs'); + }); + }); + + suite('Edge cases', () => { + test('Should handle path with spaces', () => { + const filepath = '/home/user/my project/file.py'; + + const result = resolveFilePath(filepath); + + expect(result).to.not.be.undefined; + expect(result?.scheme).to.equal('file'); + }); + + test('Should handle path with special characters', () => { + const filepath = '/home/user/project-name_v2/file.py'; + + const result = resolveFilePath(filepath); + + expect(result).to.not.be.undefined; + expect(result?.scheme).to.equal('file'); + }); + + test('Should not treat Windows drive letter colon as URI scheme', () => { + // Windows path should not be confused with a URI scheme + const filepath = 'd:\\projects\\test'; + + const result = resolveFilePath(filepath); + + expect(result).to.not.be.undefined; + expect(result?.scheme).to.equal('file'); + }); + + test('Should not treat single colon as URI scheme', () => { + // A path with a colon but not :// should be treated as a file + const filepath = 'c:somepath'; + + const result = resolveFilePath(filepath); + + expect(result).to.not.be.undefined; + expect(result?.scheme).to.equal('file'); + }); + }); +}); diff --git a/src/test/common.ts b/src/test/common.ts index 0a76c495830a..886323e815a5 100644 --- a/src/test/common.ts +++ b/src/test/common.ts @@ -5,12 +5,12 @@ // IMPORTANT: Do not import anything from the 'client' folder in this file as that folder is not available during smoke tests. import * as assert from 'assert'; -import * as fs from 'fs-extra'; +import * as fs from '../client/common/platform/fs-paths'; import * as glob from 'glob'; import * as path from 'path'; import { coerce, SemVer } from 'semver'; import { ConfigurationTarget, Event, TextDocument, Uri } from 'vscode'; -import type { IExtensionApi } from '../client/apiTypes'; +import type { PythonExtension } from '../client/api/types'; import { IProcessService } from '../client/common/process/types'; import { IDisposable } from '../client/common/types'; import { IServiceContainer, IServiceManager } from '../client/ioc/types'; @@ -22,7 +22,7 @@ const StreamZip = require('node-stream-zip'); export { sleep } from './core'; -const fileInNonRootWorkspace = path.join(EXTENSION_ROOT_DIR_FOR_TESTS, 'src', 'test', 'pythonFiles', 'dummy.py'); +const fileInNonRootWorkspace = path.join(EXTENSION_ROOT_DIR_FOR_TESTS, 'src', 'test', 'python_files', 'dummy.py'); export const rootWorkspaceUri = getWorkspaceRoot(); export const PYTHON_PATH = getPythonPath(); @@ -40,29 +40,17 @@ export enum OSType { export type PythonSettingKeys = | 'defaultInterpreterPath' | 'languageServer' - | 'linting.lintOnSave' - | 'linting.enabled' - | 'linting.pylintEnabled' - | 'linting.flake8Enabled' - | 'linting.pycodestyleEnabled' - | 'linting.pylamaEnabled' - | 'linting.prospectorEnabled' - | 'linting.pydocstyleEnabled' - | 'linting.mypyEnabled' - | 'linting.banditEnabled' | 'testing.pytestArgs' | 'testing.unittestArgs' | 'formatting.provider' - | 'sortImports.args' | 'testing.pytestEnabled' | 'testing.unittestEnabled' | 'envFile' - | 'linting.ignorePatterns' | 'terminal.activateEnvironment'; async function disposePythonSettings() { if (!IS_SMOKE_TEST) { - const configSettings = await import('../client/common/configSettings'); + const configSettings = await import('../client/common/configSettings.js'); configSettings.PythonSettings.dispose(); } } @@ -74,7 +62,7 @@ export async function updateSetting( configTarget: ConfigurationTarget, ) { const vscode = require('vscode') as typeof import('vscode'); - const settings = vscode.workspace.getConfiguration('python', { uri: resource, languageId: 'python' } || null); + const settings = vscode.workspace.getConfiguration('python', { uri: resource, languageId: 'python' }); const currentValue = settings.inspect(setting); if ( currentValue !== undefined && @@ -236,7 +224,7 @@ export async function deleteFile(file: string) { export async function deleteFiles(globPattern: string) { const items = await new Promise((resolve, reject) => { - glob(globPattern, (ex, files) => (ex ? reject(ex) : resolve(files))); + glob.default(globPattern, (ex, files) => (ex ? reject(ex) : resolve(files))); }); return Promise.all(items.map((item) => fs.remove(item).catch(noop))); @@ -304,7 +292,7 @@ export function correctPathForOsType(pathToCorrect: string, os?: OSType): string * @return `SemVer` version of the Python interpreter, or `undefined` if an error occurs. */ export async function getPythonSemVer(procService?: IProcessService): Promise { - const proc = await import('../client/common/process/proc'); + const proc = await import('../client/common/process/proc.js'); const pythonProcRunner = procService ? procService : new proc.ProcessService(); const pyVerArgs = ['-c', 'import sys;print("{0}.{1}.{2}".format(*sys.version_info[:3]))']; @@ -438,7 +426,7 @@ export async function isPythonVersion(...versions: string[]): Promise { } } -export interface IExtensionTestApi extends IExtensionApi, ProposedExtensionAPI { +export interface IExtensionTestApi extends PythonExtension, ProposedExtensionAPI { serviceContainer: IServiceContainer; serviceManager: IServiceManager; } @@ -464,12 +452,6 @@ export async function unzip(zipFile: string, targetFolder: string): Promise Promise} condition - * @param {number} timeoutMs - * @param {string} errorMessage - * @returns {Promise} */ export async function waitForCondition( condition: () => Promise, @@ -480,6 +462,7 @@ export async function waitForCondition( const timeout = setTimeout(() => { clearTimeout(timeout); + // eslint-disable-next-line @typescript-eslint/no-use-before-define clearTimeout(timer); reject(new Error(errorMessage)); }, timeoutMs); @@ -520,7 +503,7 @@ export async function openFile(file: string): Promise { const vscode = require('vscode') as typeof import('vscode'); const textDocument = await vscode.workspace.openTextDocument(file); await vscode.window.showTextDocument(textDocument); - assert(vscode.window.activeTextEditor, 'No active editor'); + assert.ok(vscode.window.activeTextEditor, 'No active editor'); return textDocument; } diff --git a/src/test/common/application/commands/issueTemplateVenv2.md b/src/test/common/application/commands/issueTemplate.md similarity index 51% rename from src/test/common/application/commands/issueTemplateVenv2.md rename to src/test/common/application/commands/issueTemplate.md index fa9142e5ca4d..a95af90ff7fe 100644 --- a/src/test/common/application/commands/issueTemplateVenv2.md +++ b/src/test/common/application/commands/issueTemplate.md @@ -1,6 +1,5 @@ # Behaviour -## Expected vs. Actual XXX @@ -12,13 +11,9 @@ XXX **After** creating the issue on GitHub, you can add screenshots and GIFs of what is happening. Consider tools like https://www.cockos.com/licecap/, https://github.com/phw/peek or https://www.screentogif.com/ for GIF creation. --> - + # Diagnostic data -- Python version (& distribution if applicable, e.g. Anaconda): 3.9.0 -- Type of virtual environment used (e.g. conda, venv, virtualenv, etc.): Venv -- Value of the `python.languageServer` setting: Pylance -

Output for Python in the Output panel (View→Output, change the drop-down the upper-right of the Output panel to Python) @@ -32,22 +27,3 @@ XXX

- -
- -User Settings - -

- -``` -Multiroot scenario, following user settings may not apply: - -experiments -β€’ enabled: false - -venvPath: "" - -``` - -

-
diff --git a/src/test/common/application/commands/issueTemplateVenv1.md b/src/test/common/application/commands/issueTemplateVenv1.md deleted file mode 100644 index 09cdd2c32eb0..000000000000 --- a/src/test/common/application/commands/issueTemplateVenv1.md +++ /dev/null @@ -1,56 +0,0 @@ - -# Behaviour -## Expected vs. Actual - -XXX - -## Steps to reproduce: - -1. XXX - - - - -# Diagnostic data - -- Python version (& distribution if applicable, e.g. Anaconda): 3.9.0 -- Type of virtual environment used (e.g. conda, venv, virtualenv, etc.): Venv -- Value of the `python.languageServer` setting: Pylance - -
- -Output for Python in the Output panel (View→Output, change the drop-down the upper-right of the Output panel to Python) - - -

- -``` -XXX -``` - -

-
- -
- -User Settings - -

- -``` - -experiments -β€’ enabled: false -β€’ optInto: [] -β€’ optOutFrom: [] - -venvPath: "" - -pipenvPath: "" - -``` - -

-
diff --git a/src/test/common/application/commands/issueUserDataTemplateVenv1.md b/src/test/common/application/commands/issueUserDataTemplateVenv1.md new file mode 100644 index 000000000000..2353d7b9f181 --- /dev/null +++ b/src/test/common/application/commands/issueUserDataTemplateVenv1.md @@ -0,0 +1,30 @@ +- Python version (& distribution if applicable, e.g. Anaconda): 3.9.0 +- Type of virtual environment used (e.g. conda, venv, virtualenv, etc.): Venv +- Value of the `python.languageServer` setting: Pylance + +
+User Settings +

+ +``` + +experiments +β€’ enabled: false +β€’ optInto: [] +β€’ optOutFrom: [] + +venvPath: "" + +pipenvPath: "" + +``` +

+
+ +
+Installed Extensions + +|Extension Name|Extension Id|Version| +|---|---|---| +|python|ms-|2020.2| +
diff --git a/src/test/common/application/commands/issueUserDataTemplateVenv2.md b/src/test/common/application/commands/issueUserDataTemplateVenv2.md new file mode 100644 index 000000000000..98ff2a880cdf --- /dev/null +++ b/src/test/common/application/commands/issueUserDataTemplateVenv2.md @@ -0,0 +1,27 @@ +- Python version (& distribution if applicable, e.g. Anaconda): 3.9.0 +- Type of virtual environment used (e.g. conda, venv, virtualenv, etc.): Venv +- Value of the `python.languageServer` setting: Pylance + +
+User Settings +

+ +``` +Multiroot scenario, following user settings may not apply: + +experiments +β€’ enabled: false + +venvPath: "" + +``` +

+
+ +
+Installed Extensions + +|Extension Name|Extension Id|Version| +|---|---|---| +|python|ms-|2020.2| +
diff --git a/src/test/common/application/commands/reportIssueCommand.unit.test.ts b/src/test/common/application/commands/reportIssueCommand.unit.test.ts index 92b4c0725f3f..175a43d14007 100644 --- a/src/test/common/application/commands/reportIssueCommand.unit.test.ts +++ b/src/test/common/application/commands/reportIssueCommand.unit.test.ts @@ -5,11 +5,11 @@ 'use strict'; import * as sinon from 'sinon'; -import * as fs from 'fs-extra'; import * as path from 'path'; import { anything, capture, instance, mock, verify, when } from 'ts-mockito'; import { expect } from 'chai'; import { WorkspaceFolder } from 'vscode-languageserver-protocol'; +import * as fs from '../../../../client/common/platform/fs-paths'; import * as Telemetry from '../../../../client/telemetry'; import { LanguageServerType } from '../../../../client/activation/types'; import { CommandManager } from '../../../../client/common/application/commandManager'; @@ -30,6 +30,7 @@ import { IConfigurationService } from '../../../../client/common/types'; import { EventName } from '../../../../client/telemetry/constants'; import { EnvironmentType, PythonEnvironment } from '../../../../client/pythonEnvironments/info'; import { EXTENSION_ROOT_DIR_FOR_TESTS } from '../../../constants'; +import * as extensionsApi from '../../../../client/common/vscodeApis/extensionsApi'; suite('Report Issue Command', () => { let reportIssueCommandHandler: ReportIssueCommandHandler; @@ -38,6 +39,8 @@ suite('Report Issue Command', () => { let interpreterService: IInterpreterService; let configurationService: IConfigurationService; let appEnvironment: IApplicationEnvironment; + let expectedIssueBody: string; + let getExtensionsStub: sinon.SinonStub; setup(async () => { workspaceService = mock(WorkspaceService); @@ -45,6 +48,7 @@ suite('Report Issue Command', () => { interpreterService = mock(InterpreterService); configurationService = mock(ConfigurationService); appEnvironment = mock(); + getExtensionsStub = sinon.stub(extensionsApi, 'getExtensions'); when(cmdManager.executeCommand('workbench.action.openIssueReporter', anything())).thenResolve(); when(workspaceService.getConfiguration('python')).thenReturn( @@ -79,6 +83,29 @@ suite('Report Issue Command', () => { instance(appEnvironment), ); await reportIssueCommandHandler.activate(); + + const issueTemplatePath = path.join( + EXTENSION_ROOT_DIR_FOR_TESTS, + 'src', + 'test', + 'common', + 'application', + 'commands', + 'issueTemplate.md', + ); + expectedIssueBody = fs.readFileSync(issueTemplatePath, 'utf8'); + + getExtensionsStub.returns([ + { + id: 'ms-python.python', + packageJSON: { + displayName: 'Python', + version: '2020.2', + name: 'python', + publisher: 'ms-python', + }, + }, + ]); }); teardown(() => { @@ -88,27 +115,28 @@ suite('Report Issue Command', () => { test('Test if issue body is filled correctly when including all the settings', async () => { await reportIssueCommandHandler.openReportIssue(); - const templatePath = path.join( + const userDataTemplatePath = path.join( EXTENSION_ROOT_DIR_FOR_TESTS, 'src', 'test', 'common', 'application', 'commands', - 'issueTemplateVenv1.md', + 'issueUserDataTemplateVenv1.md', ); - const expectedIssueBody = fs.readFileSync(templatePath, 'utf8'); + const expectedData = fs.readFileSync(userDataTemplatePath, 'utf8'); - const args: [string, { extensionId: string; issueBody: string }] = capture< + const args: [string, { extensionId: string; issueBody: string; extensionData: string }] = capture< AllCommands, - { extensionId: string; issueBody: string } + { extensionId: string; issueBody: string; extensionData: string } >(cmdManager.executeCommand).last(); verify(cmdManager.registerCommand(Commands.ReportIssue, anything(), anything())).once(); verify(cmdManager.executeCommand('workbench.action.openIssueReporter', anything())).once(); expect(args[0]).to.be.equal('workbench.action.openIssueReporter'); - const actual = args[1].issueBody; - expect(actual).to.be.equal(expectedIssueBody); + const { issueBody, extensionData } = args[1]; + expect(issueBody).to.be.equal(expectedIssueBody); + expect(extensionData).to.be.equal(expectedData); }); test('Test if issue body is filled when only including settings which are explicitly set', async () => { @@ -128,26 +156,27 @@ suite('Report Issue Command', () => { await reportIssueCommandHandler.activate(); await reportIssueCommandHandler.openReportIssue(); - const templatePath = path.join( + const userDataTemplatePath = path.join( EXTENSION_ROOT_DIR_FOR_TESTS, 'src', 'test', 'common', 'application', 'commands', - 'issueTemplateVenv2.md', + 'issueUserDataTemplateVenv2.md', ); - const expectedIssueBody = fs.readFileSync(templatePath, 'utf8'); + const expectedData = fs.readFileSync(userDataTemplatePath, 'utf8'); - const args: [string, { extensionId: string; issueBody: string }] = capture< + const args: [string, { extensionId: string; issueBody: string; extensionData: string }] = capture< AllCommands, - { extensionId: string; issueBody: string } + { extensionId: string; issueBody: string; extensionData: string } >(cmdManager.executeCommand).last(); verify(cmdManager.executeCommand('workbench.action.openIssueReporter', anything())).once(); expect(args[0]).to.be.equal('workbench.action.openIssueReporter'); - const actual = args[1].issueBody; - expect(actual).to.be.equal(expectedIssueBody); + const { issueBody, extensionData } = args[1]; + expect(issueBody).to.be.equal(expectedIssueBody); + expect(extensionData).to.be.equal(expectedData); }); test('Should send telemetry event when run Report Issue Command', async () => { const sendTelemetryStub = sinon.stub(Telemetry, 'sendTelemetryEvent'); diff --git a/src/test/common/application/progressService.unit.test.ts b/src/test/common/application/progressService.unit.test.ts new file mode 100644 index 000000000000..b9c49ccb4060 --- /dev/null +++ b/src/test/common/application/progressService.unit.test.ts @@ -0,0 +1,55 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { assert, expect } from 'chai'; +import { anything, capture, instance, mock, when } from 'ts-mockito'; +import { CancellationToken, Progress, ProgressLocation, ProgressOptions } from 'vscode'; +import { ApplicationShell } from '../../../client/common/application/applicationShell'; +import { ProgressService } from '../../../client/common/application/progressService'; +import { IApplicationShell } from '../../../client/common/application/types'; +import { createDeferred, createDeferredFromPromise, Deferred, sleep } from '../../../client/common/utils/async'; + +type ProgressTask = ( + progress: Progress<{ message?: string; increment?: number }>, + token: CancellationToken, +) => Thenable; + +suite('Progress Service', () => { + let refreshDeferred: Deferred; + let shell: ApplicationShell; + let progressService: ProgressService; + setup(() => { + refreshDeferred = createDeferred(); + shell = mock(); + progressService = new ProgressService(instance(shell)); + }); + teardown(() => { + refreshDeferred.resolve(); + }); + test('Display discovering message when refreshing interpreters for the first time', async () => { + when(shell.withProgress(anything(), anything())).thenResolve(); + const expectedOptions = { title: 'message', location: ProgressLocation.Window }; + + progressService.showProgress(expectedOptions); + + const options = capture(shell.withProgress as never).last()[0] as ProgressOptions; + assert.deepEqual(options, expectedOptions); + }); + + test('Progress message is hidden when loading has completed', async () => { + when(shell.withProgress(anything(), anything())).thenResolve(); + const options = { title: 'message', location: ProgressLocation.Window }; + progressService.showProgress(options); + + const callback = capture(shell.withProgress as never).last()[1] as ProgressTask; + const promise = callback(undefined as never, undefined as never); + const deferred = createDeferredFromPromise(promise as Promise); + await sleep(1); + expect(deferred.completed).to.be.equal(false, 'Progress disappeared before hiding it'); + progressService.hideProgress(); + await sleep(1); + expect(deferred.completed).to.be.equal(true, 'Progress did not disappear'); + }); +}); diff --git a/src/test/common/configSettings.test.ts b/src/test/common/configSettings.test.ts index 75c20f512bbe..a8b4961f037c 100644 --- a/src/test/common/configSettings.test.ts +++ b/src/test/common/configSettings.test.ts @@ -1,10 +1,10 @@ import * as assert from 'assert'; import * as path from 'path'; import * as vscode from 'vscode'; -import { IS_WINDOWS } from '../../client/common/platform/constants'; import { SystemVariables } from '../../client/common/variables/systemVariables'; import { getExtensionSettings } from '../extensionSettings'; import { initialize } from './../initialize'; +import { isWindows } from '../../client/common/utils/platform'; const workspaceRoot = path.join(__dirname, '..', '..', '..', 'src', 'test'); @@ -27,7 +27,7 @@ suite('Configuration Settings', () => { } const pythonSettingValue = (pythonSettings as any)[key] as string; - if (key.endsWith('Path') && IS_WINDOWS) { + if (key.endsWith('Path') && isWindows()) { assert.strictEqual( settingValue.toUpperCase(), pythonSettingValue.toUpperCase(), diff --git a/src/test/common/configSettings/configSettings.pythonPath.unit.test.ts b/src/test/common/configSettings/configSettings.pythonPath.unit.test.ts index b59ee34877a7..8a2a90b288a3 100644 --- a/src/test/common/configSettings/configSettings.pythonPath.unit.test.ts +++ b/src/test/common/configSettings/configSettings.pythonPath.unit.test.ts @@ -16,8 +16,8 @@ import { noop } from '../../../client/common/utils/misc'; import { PythonEnvironment } from '../../../client/pythonEnvironments/info'; import * as EnvFileTelemetry from '../../../client/telemetry/envFileTelemetry'; import { MockAutoSelectionService } from '../../mocks/autoSelector'; - -const untildify = require('untildify'); +import { untildify } from '../../../client/common/helpers'; +import { MockExtensions } from '../../mocks/extensions'; suite('Python Settings - pythonPath', () => { class CustomPythonSettings extends PythonSettings { @@ -65,6 +65,7 @@ suite('Python Settings - pythonPath', () => { workspaceService.object, interpreterPathService.object, undefined, + new MockExtensions(), ); configSettings.update(pythonSettings.object); @@ -79,6 +80,7 @@ suite('Python Settings - pythonPath', () => { workspaceService.object, interpreterPathService.object, undefined, + new MockExtensions(), ); configSettings.update(pythonSettings.object); @@ -94,6 +96,7 @@ suite('Python Settings - pythonPath', () => { workspaceService.object, interpreterPathService.object, undefined, + new MockExtensions(), ); configSettings.update(pythonSettings.object); @@ -111,6 +114,7 @@ suite('Python Settings - pythonPath', () => { workspaceService.object, interpreterPathService.object, undefined, + new MockExtensions(), ); configSettings.update(pythonSettings.object); @@ -127,6 +131,7 @@ suite('Python Settings - pythonPath', () => { workspaceService.object, interpreterPathService.object, undefined, + new MockExtensions(), ); configSettings.update(pythonSettings.object); @@ -146,6 +151,7 @@ suite('Python Settings - pythonPath', () => { workspaceService.object, interpreterPathService.object, undefined, + new MockExtensions(), ); configSettings.update(pythonSettings.object); @@ -167,6 +173,7 @@ suite('Python Settings - pythonPath', () => { workspaceService.object, interpreterPathService.object, undefined, + new MockExtensions(), ); configSettings.update(pythonSettings.object); @@ -185,6 +192,7 @@ suite('Python Settings - pythonPath', () => { workspaceService.object, interpreterPathService.object, undefined, + new MockExtensions(), ); interpreterPathService.setup((i) => i.get(typemoq.It.isAny())).returns(() => 'custom'); pythonSettings.setup((p) => p.get(typemoq.It.isValue('defaultInterpreterPath'))).returns(() => 'python'); @@ -205,6 +213,7 @@ suite('Python Settings - pythonPath', () => { workspaceService.object, interpreterPathService.object, undefined, + new MockExtensions(), ); interpreterPathService.setup((i) => i.get(resource)).returns(() => 'python'); configSettings.update(pythonSettings.object); diff --git a/src/test/common/configSettings/configSettings.unit.test.ts b/src/test/common/configSettings/configSettings.unit.test.ts index eeaed6aa996b..65afc782d7bb 100644 --- a/src/test/common/configSettings/configSettings.unit.test.ts +++ b/src/test/common/configSettings/configSettings.unit.test.ts @@ -8,7 +8,6 @@ import * as path from 'path'; import * as sinon from 'sinon'; import * as TypeMoq from 'typemoq'; -import untildify = require('untildify'); import { WorkspaceConfiguration } from 'vscode'; import { LanguageServerType } from '../../../client/activation/types'; import { IApplicationEnvironment } from '../../../client/common/application/types'; @@ -19,10 +18,7 @@ import { PersistentStateFactory } from '../../../client/common/persistentState'; import { IAutoCompleteSettings, IExperiments, - IFormattingSettings, IInterpreterSettings, - ILintingSettings, - ISortImportSettings, ITerminalSettings, } from '../../../client/common/types'; import { noop } from '../../../client/common/utils/misc'; @@ -30,6 +26,8 @@ import * as EnvFileTelemetry from '../../../client/telemetry/envFileTelemetry'; import { ITestingSettings } from '../../../client/testing/configuration/types'; import { MockAutoSelectionService } from '../../mocks/autoSelector'; import { MockMemento } from '../../mocks/mementos'; +import { untildify } from '../../../client/common/helpers'; +import { MockExtensions } from '../../mocks/extensions'; suite('Python Settings', async () => { class CustomPythonSettings extends PythonSettings { @@ -43,6 +41,7 @@ suite('Python Settings', async () => { let config: TypeMoq.IMock; let expected: CustomPythonSettings; let settings: CustomPythonSettings; + let extensions: MockExtensions; setup(() => { sinon.stub(EnvFileTelemetry, 'sendSettingTelemetry').returns(); config = TypeMoq.Mock.ofType(undefined, TypeMoq.MockBehavior.Loose); @@ -50,6 +49,7 @@ suite('Python Settings', async () => { const workspaceService = new WorkspaceService(); const workspaceMemento = new MockMemento(); const globalMemento = new MockMemento(); + extensions = new MockExtensions(); const persistentStateFactory = new PersistentStateFactory(globalMemento, workspaceMemento); expected = new CustomPythonSettings( undefined, @@ -58,7 +58,8 @@ suite('Python Settings', async () => { new InterpreterPathService(persistentStateFactory, workspaceService, [], { remoteName: undefined, } as IApplicationEnvironment), - undefined, + { defaultLSType: LanguageServerType.Jedi }, + extensions, ); settings = new CustomPythonSettings( undefined, @@ -67,7 +68,8 @@ suite('Python Settings', async () => { new InterpreterPathService(persistentStateFactory, workspaceService, [], { remoteName: undefined, } as IApplicationEnvironment), - undefined, + { defaultLSType: LanguageServerType.Jedi }, + extensions, ); expected.defaultInterpreterPath = 'python'; }); @@ -86,6 +88,7 @@ suite('Python Settings', async () => { 'pipenvPath', 'envFile', 'poetryPath', + 'pixiToolPath', 'defaultInterpreterPath', ]) { config @@ -117,9 +120,6 @@ suite('Python Settings', async () => { // complex settings config.setup((c) => c.get('interpreter')).returns(() => sourceSettings.interpreter); - config.setup((c) => c.get('linting')).returns(() => sourceSettings.linting); - config.setup((c) => c.get('sortImports')).returns(() => sourceSettings.sortImports); - config.setup((c) => c.get('formatting')).returns(() => sourceSettings.formatting); config.setup((c) => c.get('autoComplete')).returns(() => sourceSettings.autoComplete); config.setup((c) => c.get('testing')).returns(() => sourceSettings.testing); config.setup((c) => c.get('terminal')).returns(() => sourceSettings.terminal); @@ -147,6 +147,7 @@ suite('Python Settings', async () => { 'pipenvPath', 'envFile', 'poetryPath', + 'pixiToolPath', 'defaultInterpreterPath', ].forEach(async (settingName) => { testIfValueIsUpdated(settingName, 'stringValue'); @@ -230,7 +231,7 @@ suite('Python Settings', async () => { const values = [ { ls: LanguageServerType.Jedi, expected: LanguageServerType.Jedi, default: false }, { ls: LanguageServerType.JediLSP, expected: LanguageServerType.Jedi, default: false }, - { ls: LanguageServerType.Microsoft, expected: LanguageServerType.None, default: true }, + { ls: LanguageServerType.Microsoft, expected: LanguageServerType.Jedi, default: true }, { ls: LanguageServerType.Node, expected: LanguageServerType.Node, default: false }, { ls: LanguageServerType.None, expected: LanguageServerType.None, default: false }, ]; @@ -239,7 +240,48 @@ suite('Python Settings', async () => { testLanguageServer(v.ls, v.expected, v.default); }); - testLanguageServer('invalid' as LanguageServerType, LanguageServerType.None, true); + testLanguageServer('invalid' as LanguageServerType, LanguageServerType.Jedi, true); + }); + + function testPyreflySettings(pyreflyInstalled: boolean, pyreflyDisabled: boolean, languageServerDisabled: boolean) { + test(`pyrefly ${pyreflyInstalled ? 'installed' : 'not installed'} and ${ + pyreflyDisabled ? 'disabled' : 'enabled' + }`, () => { + if (pyreflyInstalled) { + extensions.extensionIdsToFind = ['meta.pyrefly']; + } else { + extensions.extensionIdsToFind = []; + } + config.setup((c) => c.get('pyrefly.disableLanguageServices')).returns(() => pyreflyDisabled); + + config + .setup((c) => c.get('languageServer')) + .returns(() => undefined) + .verifiable(TypeMoq.Times.once()); + + settings.update(config.object); + + if (languageServerDisabled) { + expect(settings.languageServer).to.equal(LanguageServerType.None); + } else { + expect(settings.languageServer).not.to.equal(LanguageServerType.None); + } + expect(settings.languageServerIsDefault).to.equal(true); + config.verifyAll(); + }); + } + + suite('pyrefly languageServer settings', async () => { + const values = [ + { pyreflyInstalled: true, pyreflyDisabled: false, languageServerDisabled: true }, + { pyreflyInstalled: true, pyreflyDisabled: true, languageServerDisabled: false }, + { pyreflyInstalled: false, pyreflyDisabled: true, languageServerDisabled: false }, + { pyreflyInstalled: false, pyreflyDisabled: false, languageServerDisabled: false }, + ]; + + values.forEach((v) => { + testPyreflySettings(v.pyreflyInstalled, v.pyreflyDisabled, v.languageServerDisabled); + }); }); function testExperiments(enabled: boolean) { @@ -266,63 +308,4 @@ suite('Python Settings', async () => { test('Experiments (not enabled)', () => testExperiments(false)); test('Experiments (enabled)', () => testExperiments(true)); - - test('Formatter Paths and args', () => { - expected.pythonPath = 'python3'; - - expected.formatting = { - autopep8Args: ['1', '2'], - autopep8Path: 'one', - blackArgs: ['3', '4'], - blackPath: 'two', - yapfArgs: ['5', '6'], - yapfPath: 'three', - provider: '', - }; - expected.formatting.blackPath = 'spam'; - initializeConfig(expected); - config - .setup((c) => c.get('formatting')) - .returns(() => expected.formatting) - .verifiable(TypeMoq.Times.once()); - - settings.update(config.object); - - for (const key of Object.keys(expected.formatting)) { - expect((settings.formatting as any)[key]).to.be.deep.equal((expected.formatting as any)[key]); - } - config.verifyAll(); - }); - test('Formatter Paths (paths relative to home)', () => { - expected.pythonPath = 'python3'; - - expected.formatting = { - autopep8Args: [], - autopep8Path: path.join('~', 'one'), - blackArgs: [], - blackPath: path.join('~', 'two'), - yapfArgs: [], - yapfPath: path.join('~', 'three'), - provider: '', - }; - expected.formatting.blackPath = 'spam'; - initializeConfig(expected); - config - .setup((c) => c.get('formatting')) - .returns(() => expected.formatting) - .verifiable(TypeMoq.Times.once()); - - settings.update(config.object); - - for (const key of Object.keys(expected.formatting)) { - if (!key.endsWith('path')) { - continue; - } - - const expectedPath = untildify((expected.formatting as any)[key]); - - expect((settings.formatting as any)[key]).to.be.equal(expectedPath); - } - config.verifyAll(); - }); }); diff --git a/src/test/common/configuration/service.test.ts b/src/test/common/configuration/service.test.ts index ff47500db731..c57617b2a610 100644 --- a/src/test/common/configuration/service.test.ts +++ b/src/test/common/configuration/service.test.ts @@ -2,8 +2,7 @@ // Licensed under the MIT License. import { expect } from 'chai'; import { workspace } from 'vscode'; -import { IConfigurationService, IDisposableRegistry } from '../../../client/common/types'; -import { disposeAll } from '../../../client/common/utils/resourceLifecycle'; +import { IConfigurationService, IDisposableRegistry, IExtensionContext } from '../../../client/common/types'; import { IServiceContainer } from '../../../client/ioc/types'; import { getExtensionSettings } from '../../extensionSettings'; import { initialize } from '../../initialize'; @@ -23,15 +22,17 @@ suite('Configuration Service', () => { test('Ensure async registry works', async () => { const asyncRegistry = serviceContainer.get(IDisposableRegistry); - let disposed = false; + let subs = serviceContainer.get(IExtensionContext).subscriptions; + const oldLength = subs.length; const disposable = { dispose(): Promise { - disposed = true; return Promise.resolve(); }, }; asyncRegistry.push(disposable); - await disposeAll(asyncRegistry); - expect(disposed).to.be.equal(true, "Didn't dispose during async registry cleanup"); + subs = serviceContainer.get(IExtensionContext).subscriptions; + const newLength = subs.length; + expect(newLength).to.be.equal(oldLength + 1, 'Subscription not added'); + // serviceContainer subscriptions are not disposed of as this breaks other tests that use the service container. }); }); diff --git a/src/test/common/exitCIAfterTestReporter.ts b/src/test/common/exitCIAfterTestReporter.ts index a2350f26a943..cb04d3a90b38 100644 --- a/src/test/common/exitCIAfterTestReporter.ts +++ b/src/test/common/exitCIAfterTestReporter.ts @@ -7,7 +7,8 @@ // This is a hack, however for some reason the process running the tests do not exit. // The hack is to force it to die when tests are done, if this doesn't work we've got a bigger problem on our hands. -import * as fs from 'fs-extra'; +import * as fs from '../../client/common/platform/fs-paths'; + import * as net from 'net'; import * as path from 'path'; import { EXTENSION_ROOT_DIR_FOR_TESTS } from '../constants'; diff --git a/src/test/common/experiments/service.unit.test.ts b/src/test/common/experiments/service.unit.test.ts index 1d96f2e0bd70..661efeaa8bb9 100644 --- a/src/test/common/experiments/service.unit.test.ts +++ b/src/test/common/experiments/service.unit.test.ts @@ -8,12 +8,15 @@ import { assert } from 'chai'; import * as sinon from 'sinon'; import { anything, instance, mock, when } from 'ts-mockito'; import { Disposable } from 'vscode-jsonrpc'; -import * as tasClient from 'vscode-tas-client'; +// sinon can not create a stub if we just point to the exported module +import * as tasClient from 'vscode-tas-client/vscode-tas-client/VSCodeTasClient'; +import * as expService from 'vscode-tas-client'; +import { TargetPopulation } from 'vscode-tas-client'; import { ApplicationEnvironment } from '../../../client/common/application/applicationEnvironment'; import { IApplicationEnvironment, IWorkspaceService } from '../../../client/common/application/types'; import { WorkspaceService } from '../../../client/common/application/workspace'; import { Channel } from '../../../client/common/constants'; -import { ExperimentService, TargetPopulation } from '../../../client/common/experiments/service'; +import { ExperimentService } from '../../../client/common/experiments/service'; import { PersistentState } from '../../../client/common/persistentState'; import { IPersistentStateFactory } from '../../../client/common/types'; import { registerLogger } from '../../../client/logging'; @@ -72,13 +75,13 @@ suite('Experimentation service', () => { } function configureApplicationEnvironment(channel: Channel, version: string, contributes?: Record) { - when(appEnvironment.extensionChannel).thenReturn(channel); + when(appEnvironment.channel).thenReturn(channel); when(appEnvironment.extensionName).thenReturn(PVSC_EXTENSION_ID_FOR_TESTS); when(appEnvironment.packageJson).thenReturn({ version, contributes }); } suite('Initialization', () => { - test('Users with a release version of the extension should be in the Public target population', () => { + test('Users with VS Code stable version should be in the Public target population', () => { const getExperimentationServiceStub = sinon.stub(tasClient, 'getExperimentationService'); configureSettings(true, [], []); configureApplicationEnvironment('stable', extensionVersion); @@ -97,7 +100,7 @@ suite('Experimentation service', () => { ); }); - test('Users with an Insiders version of the extension should be the Insiders target population', () => { + test('Users with VS Code Insiders version should be the Insiders target population', () => { const getExperimentationServiceStub = sinon.stub(tasClient, 'getExperimentationService'); configureSettings(true, [], []); @@ -180,7 +183,7 @@ suite('Experimentation service', () => { getTreatmentVariable = sinon.stub().returns(true); sinon.stub(tasClient, 'getExperimentationService').returns(({ getTreatmentVariable, - } as unknown) as tasClient.IExperimentationService); + } as unknown) as expService.IExperimentationService); configureApplicationEnvironment('stable', extensionVersion); }); @@ -218,7 +221,7 @@ suite('Experimentation service', () => { getTreatmentVariable = sinon.stub().returns(false); sinon.stub(tasClient, 'getExperimentationService').returns(({ getTreatmentVariable, - } as unknown) as tasClient.IExperimentationService); + } as unknown) as expService.IExperimentationService); configureApplicationEnvironment('stable', extensionVersion); @@ -364,7 +367,7 @@ suite('Experimentation service', () => { getTreatmentVariableStub = sinon.stub().returns(Promise.resolve('value')); sinon.stub(tasClient, 'getExperimentationService').returns(({ getTreatmentVariable: getTreatmentVariableStub, - } as unknown) as tasClient.IExperimentationService); + } as unknown) as expService.IExperimentationService); configureApplicationEnvironment('stable', extensionVersion); }); @@ -491,7 +494,10 @@ suite('Experimentation service', () => { await experimentService.activate(); const { properties } = telemetryEvents[1]; - assert.deepStrictEqual(properties, { optedInto: ['foo'], optedOutFrom: ['bar'] }); + assert.deepStrictEqual(properties, { + optedInto: JSON.stringify(['foo']), + optedOutFrom: JSON.stringify(['bar']), + }); }); test('Set telemetry properties to empty arrays if no experiments have been opted into or out from', async () => { @@ -523,7 +529,7 @@ suite('Experimentation service', () => { await experimentService.activate(); const { properties } = telemetryEvents[1]; - assert.deepStrictEqual(properties, { optedInto: [], optedOutFrom: [] }); + assert.deepStrictEqual(properties, { optedInto: '[]', optedOutFrom: '[]' }); }); test('If the entered value for a setting contains "All", do not expand it to be a list of all experiments, and pass it as-is', async () => { @@ -555,7 +561,10 @@ suite('Experimentation service', () => { await experimentService.activate(); const { properties } = telemetryEvents[0]; - assert.deepStrictEqual(properties, { optedInto: ['All'], optedOutFrom: ['All'] }); + assert.deepStrictEqual(properties, { + optedInto: JSON.stringify(['All']), + optedOutFrom: JSON.stringify(['All']), + }); }); // This is an unlikely scenario. @@ -577,7 +586,7 @@ suite('Experimentation service', () => { await experimentService.activate(); const { properties } = telemetryEvents[1]; - assert.deepStrictEqual(properties, { optedInto: [], optedOutFrom: [] }); + assert.deepStrictEqual(properties, { optedInto: '[]', optedOutFrom: '[]' }); }); // This is also an unlikely scenario. @@ -608,7 +617,7 @@ suite('Experimentation service', () => { await experimentService.activate(); const { properties } = telemetryEvents[1]; - assert.deepStrictEqual(properties, { optedInto: [], optedOutFrom: [] }); + assert.deepStrictEqual(properties, { optedInto: '[]', optedOutFrom: '[]' }); }); }); }); diff --git a/src/test/common/installer.test.ts b/src/test/common/installer.test.ts deleted file mode 100644 index 7ff0ee81c27f..000000000000 --- a/src/test/common/installer.test.ts +++ /dev/null @@ -1,342 +0,0 @@ -import * as path from 'path'; -import { instance, mock } from 'ts-mockito'; -import * as TypeMoq from 'typemoq'; -import { ConfigurationTarget, Uri } from 'vscode'; -import { IExtensionSingleActivationService } from '../../client/activation/types'; -import { ActiveResourceService } from '../../client/common/application/activeResource'; -import { ApplicationEnvironment } from '../../client/common/application/applicationEnvironment'; -import { ClipboardService } from '../../client/common/application/clipboard'; -import { ReloadVSCodeCommandHandler } from '../../client/common/application/commands/reloadCommand'; -import { ReportIssueCommandHandler } from '../../client/common/application/commands/reportIssueCommand'; -import { DebugService } from '../../client/common/application/debugService'; -import { DebugSessionTelemetry } from '../../client/common/application/debugSessionTelemetry'; -import { DocumentManager } from '../../client/common/application/documentManager'; -import { Extensions } from '../../client/common/application/extensions'; -import { - IActiveResourceService, - IApplicationEnvironment, - IApplicationShell, - IClipboard, - ICommandManager, - IDebugService, - IDocumentManager, - IWorkspaceService, -} from '../../client/common/application/types'; -import { WorkspaceService } from '../../client/common/application/workspace'; -import { ConfigurationService } from '../../client/common/configuration/service'; -import { EditorUtils } from '../../client/common/editor'; -import { ExperimentService } from '../../client/common/experiments/service'; -import { InstallationChannelManager } from '../../client/common/installer/channelManager'; -import { ProductInstaller } from '../../client/common/installer/productInstaller'; -import { - FormatterProductPathService, - LinterProductPathService, - TestFrameworkProductPathService, -} from '../../client/common/installer/productPath'; -import { ProductService } from '../../client/common/installer/productService'; -import { - IInstallationChannelManager, - IModuleInstaller, - IProductPathService, - IProductService, -} from '../../client/common/installer/types'; -import { InterpreterPathService } from '../../client/common/interpreterPathService'; -import { BrowserService } from '../../client/common/net/browser'; -import { PersistentStateFactory } from '../../client/common/persistentState'; -import { PathUtils } from '../../client/common/platform/pathUtils'; -import { CurrentProcess } from '../../client/common/process/currentProcess'; -import { ProcessLogger } from '../../client/common/process/logger'; -import { IProcessLogger, IProcessServiceFactory } from '../../client/common/process/types'; -import { TerminalActivator } from '../../client/common/terminal/activator'; -import { PowershellTerminalActivationFailedHandler } from '../../client/common/terminal/activator/powershellFailedHandler'; -import { Bash } from '../../client/common/terminal/environmentActivationProviders/bash'; -import { CommandPromptAndPowerShell } from '../../client/common/terminal/environmentActivationProviders/commandPrompt'; -import { Nushell } from '../../client/common/terminal/environmentActivationProviders/nushell'; -import { CondaActivationCommandProvider } from '../../client/common/terminal/environmentActivationProviders/condaActivationProvider'; -import { PipEnvActivationCommandProvider } from '../../client/common/terminal/environmentActivationProviders/pipEnvActivationProvider'; -import { PyEnvActivationCommandProvider } from '../../client/common/terminal/environmentActivationProviders/pyenvActivationProvider'; -import { TerminalServiceFactory } from '../../client/common/terminal/factory'; -import { TerminalHelper } from '../../client/common/terminal/helper'; -import { SettingsShellDetector } from '../../client/common/terminal/shellDetectors/settingsShellDetector'; -import { TerminalNameShellDetector } from '../../client/common/terminal/shellDetectors/terminalNameShellDetector'; -import { UserEnvironmentShellDetector } from '../../client/common/terminal/shellDetectors/userEnvironmentShellDetector'; -import { VSCEnvironmentShellDetector } from '../../client/common/terminal/shellDetectors/vscEnvironmentShellDetector'; -import { - IShellDetector, - ITerminalActivationCommandProvider, - ITerminalActivationHandler, - ITerminalActivator, - ITerminalHelper, - ITerminalServiceFactory, - TerminalActivationProviders, -} from '../../client/common/terminal/types'; -import { - IBrowserService, - IConfigurationService, - ICurrentProcess, - IEditorUtils, - IExperimentService, - IExtensions, - IInstaller, - IInterpreterPathService, - IPathUtils, - IPersistentStateFactory, - IRandom, - IsWindows, - Product, - ProductType, -} from '../../client/common/types'; -import { createDeferred } from '../../client/common/utils/async'; -import { getNamesAndValues } from '../../client/common/utils/enum'; -import { IMultiStepInputFactory, MultiStepInputFactory } from '../../client/common/utils/multiStepInput'; -import { Random } from '../../client/common/utils/random'; -import { ImportTracker } from '../../client/telemetry/importTracker'; -import { IImportTracker } from '../../client/telemetry/types'; -import { rootWorkspaceUri, updateSetting } from '../common'; -import { MockModuleInstaller } from '../mocks/moduleInstaller'; -import { MockProcessService } from '../mocks/proc'; -import { UnitTestIocContainer } from '../testing/serviceRegistry'; -import { closeActiveWindows, initializeTest, IS_MULTI_ROOT_TEST, TEST_TIMEOUT } from '../initialize'; -import { IActivatedEnvironmentLaunch } from '../../client/interpreter/contracts'; -import { ActivatedEnvironmentLaunch } from '../../client/interpreter/virtualEnvs/activatedEnvLaunch'; -import { - IPythonPathUpdaterServiceFactory, - IPythonPathUpdaterServiceManager, -} from '../../client/interpreter/configuration/types'; -import { PythonPathUpdaterService } from '../../client/interpreter/configuration/pythonPathUpdaterService'; -import { PythonPathUpdaterServiceFactory } from '../../client/interpreter/configuration/pythonPathUpdaterServiceFactory'; - -suite('Installer', () => { - let ioc: UnitTestIocContainer; - const workspaceUri = Uri.file(path.join(__dirname, '..', '..', '..', 'src', 'test')); - const resource = IS_MULTI_ROOT_TEST ? workspaceUri : undefined; - suiteSetup(initializeTest); - setup(async () => { - await initializeTest(); - await resetSettings(); - await initializeDI(); - }); - suiteTeardown(async () => { - await closeActiveWindows(); - await resetSettings(); - }); - teardown(async () => { - await ioc.dispose(); - await closeActiveWindows(); - }); - - async function initializeDI() { - ioc = new UnitTestIocContainer(); - ioc.registerUnitTestTypes(); - ioc.registerFileSystemTypes(); - ioc.registerVariableTypes(); - ioc.registerLinterTypes(); - ioc.registerFormatterTypes(); - ioc.registerInterpreterStorageTypes(); - - ioc.serviceManager.addSingleton(IPersistentStateFactory, PersistentStateFactory); - ioc.serviceManager.addSingleton(IInstaller, ProductInstaller); - ioc.serviceManager.addSingleton(IPathUtils, PathUtils); - ioc.serviceManager.addSingleton(IProcessLogger, ProcessLogger); - ioc.serviceManager.addSingleton(ICurrentProcess, CurrentProcess); - ioc.serviceManager.addSingleton( - IInstallationChannelManager, - InstallationChannelManager, - ); - ioc.serviceManager.addSingletonInstance( - ICommandManager, - TypeMoq.Mock.ofType().object, - ); - - ioc.serviceManager.addSingletonInstance( - IApplicationShell, - TypeMoq.Mock.ofType().object, - ); - ioc.serviceManager.addSingleton(IConfigurationService, ConfigurationService); - ioc.serviceManager.addSingleton(IWorkspaceService, WorkspaceService); - - await ioc.registerMockInterpreterTypes(); - ioc.registerMockProcessTypes(); - ioc.serviceManager.addSingletonInstance(IsWindows, false); - ioc.serviceManager.addSingletonInstance(IProductService, new ProductService()); - ioc.serviceManager.addSingleton( - IProductPathService, - FormatterProductPathService, - ProductType.Formatter, - ); - ioc.serviceManager.addSingleton( - IProductPathService, - LinterProductPathService, - ProductType.Linter, - ); - ioc.serviceManager.addSingleton( - IProductPathService, - TestFrameworkProductPathService, - ProductType.TestFramework, - ); - ioc.serviceManager.addSingleton( - IActivatedEnvironmentLaunch, - ActivatedEnvironmentLaunch, - ); - ioc.serviceManager.addSingleton( - IPythonPathUpdaterServiceManager, - PythonPathUpdaterService, - ); - ioc.serviceManager.addSingleton( - IPythonPathUpdaterServiceFactory, - PythonPathUpdaterServiceFactory, - ); - ioc.serviceManager.addSingleton(IActiveResourceService, ActiveResourceService); - ioc.serviceManager.addSingleton(IInterpreterPathService, InterpreterPathService); - ioc.serviceManager.addSingleton(IExtensions, Extensions); - ioc.serviceManager.addSingleton(IRandom, Random); - ioc.serviceManager.addSingleton(ITerminalServiceFactory, TerminalServiceFactory); - ioc.serviceManager.addSingleton(IClipboard, ClipboardService); - ioc.serviceManager.addSingleton(IDocumentManager, DocumentManager); - ioc.serviceManager.addSingleton(IDebugService, DebugService); - ioc.serviceManager.addSingleton(IApplicationEnvironment, ApplicationEnvironment); - ioc.serviceManager.addSingleton(IBrowserService, BrowserService); - ioc.serviceManager.addSingleton(IEditorUtils, EditorUtils); - ioc.serviceManager.addSingleton(ITerminalActivator, TerminalActivator); - ioc.serviceManager.addSingleton( - ITerminalActivationHandler, - PowershellTerminalActivationFailedHandler, - ); - ioc.serviceManager.addSingleton(IExperimentService, ExperimentService); - - ioc.serviceManager.addSingleton(ITerminalHelper, TerminalHelper); - ioc.serviceManager.addSingleton( - ITerminalActivationCommandProvider, - Bash, - TerminalActivationProviders.bashCShellFish, - ); - ioc.serviceManager.addSingleton( - ITerminalActivationCommandProvider, - CommandPromptAndPowerShell, - TerminalActivationProviders.commandPromptAndPowerShell, - ); - ioc.serviceManager.addSingleton( - ITerminalActivationCommandProvider, - Nushell, - TerminalActivationProviders.nushell, - ); - ioc.serviceManager.addSingleton( - ITerminalActivationCommandProvider, - PyEnvActivationCommandProvider, - TerminalActivationProviders.pyenv, - ); - ioc.serviceManager.addSingleton( - ITerminalActivationCommandProvider, - CondaActivationCommandProvider, - TerminalActivationProviders.conda, - ); - ioc.serviceManager.addSingleton( - ITerminalActivationCommandProvider, - PipEnvActivationCommandProvider, - TerminalActivationProviders.pipenv, - ); - ioc.serviceManager.addSingleton(IMultiStepInputFactory, MultiStepInputFactory); - ioc.serviceManager.addSingleton(IImportTracker, ImportTracker); - ioc.serviceManager.addBinding(IImportTracker, IExtensionSingleActivationService); - ioc.serviceManager.addSingleton(IShellDetector, TerminalNameShellDetector); - ioc.serviceManager.addSingleton(IShellDetector, SettingsShellDetector); - ioc.serviceManager.addSingleton(IShellDetector, UserEnvironmentShellDetector); - ioc.serviceManager.addSingleton(IShellDetector, VSCEnvironmentShellDetector); - ioc.serviceManager.addSingleton( - IExtensionSingleActivationService, - ReloadVSCodeCommandHandler, - ); - ioc.serviceManager.addSingleton( - IExtensionSingleActivationService, - ReportIssueCommandHandler, - ); - - ioc.serviceManager.addSingleton( - IExtensionSingleActivationService, - DebugSessionTelemetry, - ); - } - async function resetSettings() { - await updateSetting('linting.pylintEnabled', true, rootWorkspaceUri, ConfigurationTarget.Workspace); - } - - async function testCheckingIfProductIsInstalled(product: Product) { - const installer = ioc.serviceContainer.get(IInstaller); - const processService = (await ioc.serviceContainer - .get(IProcessServiceFactory) - .create()) as MockProcessService; - const checkInstalledDef = createDeferred(); - processService.onExec((_file, args, _options, callback) => { - const moduleName = installer.translateProductToModuleName(product); - if (args.length > 1 && args[0] === '-c' && args[1] === `import ${moduleName}`) { - checkInstalledDef.resolve(true); - } - callback({ stdout: '' }); - }); - await installer.isInstalled(product, resource); - await checkInstalledDef.promise; - } - getNamesAndValues(Product).forEach((prod) => { - test(`Ensure isInstalled for Product: '${prod.name}' executes the right command`, async function () { - if ( - new ProductService().getProductType(prod.value) === ProductType.DataScience || - new ProductService().getProductType(prod.value) === ProductType.Python - ) { - return this.skip(); - } - ioc.serviceManager.addSingletonInstance( - IModuleInstaller, - new MockModuleInstaller('one', false), - ); - ioc.serviceManager.addSingletonInstance( - IModuleInstaller, - new MockModuleInstaller('two', true), - ); - ioc.serviceManager.addSingletonInstance(ITerminalHelper, instance(mock(TerminalHelper))); - if (prod.value === Product.unittest || prod.value === Product.isort) { - return undefined; - } - await testCheckingIfProductIsInstalled(prod.value); - - return undefined; - }).timeout(TEST_TIMEOUT * 3); - }); - - async function testInstallingProduct(product: Product) { - const installer = ioc.serviceContainer.get(IInstaller); - const checkInstalledDef = createDeferred(); - const moduleInstallers = ioc.serviceContainer.getAll(IModuleInstaller); - const moduleInstallerOne = moduleInstallers.find((item) => item.displayName === 'two')!; - - moduleInstallerOne.on('installModule', (name: Product | string) => { - if (product === name) { - checkInstalledDef.resolve(); - } - }); - await installer.install(product); - await checkInstalledDef.promise; - } - getNamesAndValues(Product).forEach((prod) => { - test(`Ensure install for Product: '${prod.name}' executes the right command in IModuleInstaller`, async function () { - const productType = new ProductService().getProductType(prod.value); - if (productType === ProductType.DataScience || productType === ProductType.Python) { - return this.skip(); - } - ioc.serviceManager.addSingletonInstance( - IModuleInstaller, - new MockModuleInstaller('one', false), - ); - ioc.serviceManager.addSingletonInstance( - IModuleInstaller, - new MockModuleInstaller('two', true), - ); - ioc.serviceManager.addSingletonInstance(ITerminalHelper, instance(mock(TerminalHelper))); - if (prod.value === Product.unittest || prod.value === Product.isort) { - return undefined; - } - await testInstallingProduct(prod.value); - - return undefined; - }).timeout(TEST_TIMEOUT * 3); - }); -}); diff --git a/src/test/common/installer/channelManager.unit.test.ts b/src/test/common/installer/channelManager.unit.test.ts index 319a9647fec7..9789f9f18718 100644 --- a/src/test/common/installer/channelManager.unit.test.ts +++ b/src/test/common/installer/channelManager.unit.test.ts @@ -57,7 +57,7 @@ suite('InstallationChannelManager - getInstallationChannel()', () => { showNoInstallersMessage.resolves(); installChannelManager = new InstallationChannelManager(serviceContainer.object); - const channel = await installChannelManager.getInstallationChannel(Product.autopep8, resource); + const channel = await installChannelManager.getInstallationChannel(Product.pytest, resource); expect(channel).to.equal(undefined, 'should be undefined'); assert.ok(showNoInstallersMessage.calledOnceWith(resource)); }); @@ -79,7 +79,7 @@ suite('InstallationChannelManager - getInstallationChannel()', () => { showNoInstallersMessage.resolves(); installChannelManager = new InstallationChannelManager(serviceContainer.object); - const channel = await installChannelManager.getInstallationChannel(Product.autopep8, resource); + const channel = await installChannelManager.getInstallationChannel(Product.pytest, resource); assert.ok(showNoInstallersMessage.notCalled); appShell.verifyAll(); expect(channel).to.equal(undefined, 'Channel should not be set'); @@ -107,7 +107,7 @@ suite('InstallationChannelManager - getInstallationChannel()', () => { showNoInstallersMessage.resolves(); installChannelManager = new InstallationChannelManager(serviceContainer.object); - const channel = await installChannelManager.getInstallationChannel(Product.autopep8, resource); + const channel = await installChannelManager.getInstallationChannel(Product.pytest, resource); assert.ok(showNoInstallersMessage.notCalled); appShell.verifyAll(); expect(channel).to.not.equal(undefined, 'Channel should be set'); diff --git a/src/test/common/installer/installer.invalidPath.unit.test.ts b/src/test/common/installer/installer.invalidPath.unit.test.ts deleted file mode 100644 index 7e8392204600..000000000000 --- a/src/test/common/installer/installer.invalidPath.unit.test.ts +++ /dev/null @@ -1,129 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; - -import { expect, use } from 'chai'; -import * as chaiAsPromised from 'chai-as-promised'; -import * as path from 'path'; -import * as TypeMoq from 'typemoq'; -import { Uri } from 'vscode'; -import { IApplicationShell, IWorkspaceService } from '../../../client/common/application/types'; -import '../../../client/common/extensions'; -import { ProductInstaller } from '../../../client/common/installer/productInstaller'; -import { ProductService } from '../../../client/common/installer/productService'; -import { IProductPathService, IProductService } from '../../../client/common/installer/types'; -import { IPersistentState, IPersistentStateFactory, Product, ProductType } from '../../../client/common/types'; -import { getNamesAndValues } from '../../../client/common/utils/enum'; -import { IInterpreterService } from '../../../client/interpreter/contracts'; -import { IServiceContainer } from '../../../client/ioc/types'; -import { PythonEnvironment } from '../../../client/pythonEnvironments/info'; - -use(chaiAsPromised); - -suite('Module Installer - Invalid Paths', () => { - [undefined, Uri.file('resource')].forEach((resource) => { - ['moduleName', path.join('users', 'dev', 'tool', 'executable')].forEach((pathToExecutable) => { - const isExecutableAModule = path.basename(pathToExecutable) === pathToExecutable; - - getNamesAndValues(Product).forEach((product) => { - let installer: ProductInstaller; - let serviceContainer: TypeMoq.IMock; - let app: TypeMoq.IMock; - let workspaceService: TypeMoq.IMock; - let productPathService: TypeMoq.IMock; - let persistentState: TypeMoq.IMock; - - setup(function () { - if (new ProductService().getProductType(product.value) === ProductType.DataScience) { - return this.skip(); - } - serviceContainer = TypeMoq.Mock.ofType(); - - serviceContainer - .setup((c) => c.get(TypeMoq.It.isValue(IProductService), TypeMoq.It.isAny())) - .returns(() => new ProductService()); - app = TypeMoq.Mock.ofType(); - serviceContainer - .setup((c) => c.get(TypeMoq.It.isValue(IApplicationShell), TypeMoq.It.isAny())) - .returns(() => app.object); - workspaceService = TypeMoq.Mock.ofType(); - serviceContainer - .setup((c) => c.get(TypeMoq.It.isValue(IWorkspaceService), TypeMoq.It.isAny())) - .returns(() => workspaceService.object); - - productPathService = TypeMoq.Mock.ofType(); - serviceContainer - .setup((c) => c.get(TypeMoq.It.isValue(IProductPathService), TypeMoq.It.isAny())) - .returns(() => productPathService.object); - - const interpreterService = TypeMoq.Mock.ofType(); - - const pythonInterpreter = TypeMoq.Mock.ofType(); - - pythonInterpreter.setup((i) => (i as any).then).returns(() => undefined); - interpreterService - .setup((i) => i.getActiveInterpreter(TypeMoq.It.isAny())) - .returns(() => Promise.resolve(pythonInterpreter.object)); - serviceContainer - .setup((c) => c.get(TypeMoq.It.isValue(IInterpreterService), TypeMoq.It.isAny())) - .returns(() => interpreterService.object); - - persistentState = TypeMoq.Mock.ofType(); - serviceContainer - .setup((c) => c.get(TypeMoq.It.isValue(IPersistentStateFactory), TypeMoq.It.isAny())) - .returns(() => persistentState.object); - - installer = new ProductInstaller(serviceContainer.object); - }); - - switch (product.value) { - case Product.isort: - case Product.unittest: { - return; - } - default: { - test(`Ensure invalid path message is ${isExecutableAModule ? 'not displayed' : 'displayed'} ${ - product.name - } (${resource ? 'With a resource' : 'without a resource'})`, async () => { - // If the path to executable is a module, then we won't display error message indicating path is invalid. - - productPathService - .setup((p) => - p.getExecutableNameFromSettings(TypeMoq.It.isAny(), TypeMoq.It.isValue(resource)), - ) - .returns(() => pathToExecutable) - .verifiable(TypeMoq.Times.atLeast(isExecutableAModule ? 0 : 1)); - productPathService - .setup((p) => p.isExecutableAModule(TypeMoq.It.isAny(), TypeMoq.It.isValue(resource))) - .returns(() => isExecutableAModule) - .verifiable(TypeMoq.Times.atLeastOnce()); - const anyParams = [0, 1, 2, 3, 4, 5].map(() => TypeMoq.It.isAny()); - app.setup((a) => a.showErrorMessage(TypeMoq.It.isAny(), ...anyParams)) - .callback((message) => { - if (!isExecutableAModule) { - expect(message).contains(pathToExecutable); - } - }) - .returns(() => Promise.resolve(undefined)) - .verifiable(TypeMoq.Times.exactly(1)); - const persistValue = TypeMoq.Mock.ofType>(); - persistValue.setup((pv) => pv.value).returns(() => false); - persistValue.setup((pv) => pv.updateValue(TypeMoq.It.isValue(true))); - persistentState - .setup((ps) => - ps.createGlobalPersistentState( - TypeMoq.It.isAnyString(), - TypeMoq.It.isValue(undefined), - ), - ) - .returns(() => persistValue.object); - await installer.promptToInstall(product.value, resource); - productPathService.verifyAll(); - }); - } - } - }); - }); - }); -}); diff --git a/src/test/common/installer/installer.unit.test.ts b/src/test/common/installer/installer.unit.test.ts deleted file mode 100644 index bdd7ab32a028..000000000000 --- a/src/test/common/installer/installer.unit.test.ts +++ /dev/null @@ -1,910 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -/* eslint-disable max-classes-per-file */ - -import { assert, expect, use } from 'chai'; -import * as chaiAsPromised from 'chai-as-promised'; -import * as sinon from 'sinon'; -import { anything, instance, mock, verify, when } from 'ts-mockito'; -import * as TypeMoq from 'typemoq'; -import { Disposable, Uri, WorkspaceFolder } from 'vscode'; -import { ApplicationShell } from '../../../client/common/application/applicationShell'; -import { CommandManager } from '../../../client/common/application/commandManager'; -import { IApplicationShell, ICommandManager, IWorkspaceService } from '../../../client/common/application/types'; -import { WorkspaceService } from '../../../client/common/application/workspace'; -import { ConfigurationService } from '../../../client/common/configuration/service'; -import { Commands } from '../../../client/common/constants'; -import { ExperimentService } from '../../../client/common/experiments/service'; -import '../../../client/common/extensions'; -import { - FormatterInstaller, - LinterInstaller, - ProductInstaller, -} from '../../../client/common/installer/productInstaller'; -import { ProductNames } from '../../../client/common/installer/productNames'; -import { LinterProductPathService } from '../../../client/common/installer/productPath'; -import { ProductService } from '../../../client/common/installer/productService'; -import { - IInstallationChannelManager, - IModuleInstaller, - IProductPathService, - IProductService, -} from '../../../client/common/installer/types'; -import { - ExecutionResult, - IProcessService, - IProcessServiceFactory, - IPythonExecutionFactory, - IPythonExecutionService, -} from '../../../client/common/process/types'; -import { - IConfigurationService, - IDisposableRegistry, - IExperimentService, - InstallerResponse, - IPersistentState, - IPersistentStateFactory, - Product, - ProductType, -} from '../../../client/common/types'; -import { createDeferred, Deferred } from '../../../client/common/utils/async'; -import { getNamesAndValues } from '../../../client/common/utils/enum'; -import { IInterpreterService } from '../../../client/interpreter/contracts'; -import { ServiceContainer } from '../../../client/ioc/container'; -import { IServiceContainer } from '../../../client/ioc/types'; -import { LinterManager } from '../../../client/linters/linterManager'; -import { ILinterManager } from '../../../client/linters/types'; -import { PythonEnvironment } from '../../../client/pythonEnvironments/info'; -import { sleep } from '../../common'; - -use(chaiAsPromised); - -suite('Module Installer only', () => { - [undefined, Uri.file('resource')].forEach((resource) => { - getNamesAndValues(Product) - .concat([{ name: 'Unknown product', value: 404 }]) - - .forEach((product) => { - let disposables: Disposable[] = []; - let installer: ProductInstaller; - let installationChannel: TypeMoq.IMock; - let moduleInstaller: TypeMoq.IMock; - let serviceContainer: TypeMoq.IMock; - let app: TypeMoq.IMock; - let promptDeferred: Deferred | undefined; - let workspaceService: TypeMoq.IMock; - let persistentStore: TypeMoq.IMock; - - let productPathService: TypeMoq.IMock; - let interpreterService: TypeMoq.IMock; - const productService = new ProductService(); - - setup(function () { - if (new ProductService().getProductType(product.value) === ProductType.DataScience) { - return this.skip(); - } - promptDeferred = createDeferred(); - serviceContainer = TypeMoq.Mock.ofType(); - - disposables = []; - serviceContainer - .setup((c) => c.get(TypeMoq.It.isValue(IDisposableRegistry), TypeMoq.It.isAny())) - .returns(() => disposables); - serviceContainer - .setup((c) => c.get(TypeMoq.It.isValue(IProductService), TypeMoq.It.isAny())) - .returns(() => productService); - installationChannel = TypeMoq.Mock.ofType(); - serviceContainer - .setup((c) => c.get(TypeMoq.It.isValue(IInstallationChannelManager), TypeMoq.It.isAny())) - .returns(() => installationChannel.object); - app = TypeMoq.Mock.ofType(); - serviceContainer - .setup((c) => c.get(TypeMoq.It.isValue(IApplicationShell), TypeMoq.It.isAny())) - .returns(() => app.object); - workspaceService = TypeMoq.Mock.ofType(); - serviceContainer - .setup((c) => c.get(TypeMoq.It.isValue(IWorkspaceService), TypeMoq.It.isAny())) - .returns(() => workspaceService.object); - persistentStore = TypeMoq.Mock.ofType(); - serviceContainer - .setup((c) => c.get(TypeMoq.It.isValue(IPersistentStateFactory), TypeMoq.It.isAny())) - .returns(() => persistentStore.object); - - moduleInstaller = TypeMoq.Mock.ofType(); - - // eslint-disable-next-line @typescript-eslint/no-explicit-any - moduleInstaller.setup((x: any) => x.then).returns(() => undefined); - installationChannel - .setup((i) => i.getInstallationChannel(TypeMoq.It.isAny(), TypeMoq.It.isAny())) - .returns(() => Promise.resolve(moduleInstaller.object)); - installationChannel - .setup((i) => i.getInstallationChannel(TypeMoq.It.isAny())) - .returns(() => Promise.resolve(moduleInstaller.object)); - - productPathService = TypeMoq.Mock.ofType(); - serviceContainer - .setup((c) => c.get(TypeMoq.It.isValue(IProductPathService), TypeMoq.It.isAny())) - .returns(() => productPathService.object); - productPathService - .setup((p) => p.getExecutableNameFromSettings(TypeMoq.It.isAny(), TypeMoq.It.isValue(resource))) - .returns(() => 'xyz'); - productPathService - .setup((p) => p.isExecutableAModule(TypeMoq.It.isAny(), TypeMoq.It.isValue(resource))) - .returns(() => true); - interpreterService = TypeMoq.Mock.ofType(); - const pythonInterpreter = TypeMoq.Mock.ofType(); - - // eslint-disable-next-line @typescript-eslint/no-explicit-any - pythonInterpreter.setup((i) => (i as any).then).returns(() => undefined); - interpreterService - .setup((i) => i.getActiveInterpreter(TypeMoq.It.isAny())) - .returns(() => Promise.resolve(pythonInterpreter.object)); - serviceContainer - .setup((c) => c.get(TypeMoq.It.isValue(IInterpreterService), TypeMoq.It.isAny())) - .returns(() => interpreterService.object); - installer = new ProductInstaller(serviceContainer.object); - - return undefined; - }); - - teardown(() => { - if (new ProductService().getProductType(product.value) === ProductType.DataScience) { - sinon.restore(); - return; - } - // This must be resolved, else all subsequent tests will fail (as this same promise will be used for other tests). - if (promptDeferred) { - promptDeferred.resolve(); - } - disposables.forEach((disposable) => { - if (disposable) { - disposable.dispose(); - } - }); - sinon.restore(); - }); - - switch (product.value) { - case 404 as Product: { - test(`If product type is not recognized, throw error (${ - resource ? 'With a resource' : 'without a resource' - })`, async () => { - app.setup((a) => - a.showErrorMessage(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny()), - ).verifiable(TypeMoq.Times.never()); - const getProductType = sinon.stub(ProductService.prototype, 'getProductType'); - - getProductType.returns('random' as ProductType); - const promise = installer.promptToInstall(product.value, resource); - await expect(promise).to.eventually.be.rejectedWith(`Unknown product ${product.value}`); - app.verifyAll(); - assert.ok(getProductType.calledOnce); - }); - return; - } - case Product.isort: { - return; - } - case Product.unittest: { - test(`Ensure resource info is passed into the module installer ${product.name} (${ - resource ? 'With a resource' : 'without a resource' - })`, async () => { - const response = await installer.install(product.value, resource); - expect(response).to.be.equal(InstallerResponse.Installed); - }); - test(`Ensure resource info is passed into the module installer (created using ProductInstaller) ${ - product.name - } (${resource ? 'With a resource' : 'without a resource'})`, async () => { - const response = await installer.install(product.value, resource); - expect(response).to.be.equal(InstallerResponse.Installed); - }); - break; - } - - default: - test(`Ensure the prompt is displayed only once, until the prompt is closed, ${product.name} (${ - resource ? 'With a resource' : 'without a resource' - })`, async () => { - workspaceService - .setup((w) => w.getWorkspaceFolder(TypeMoq.It.isValue(resource!))) - .returns(() => TypeMoq.Mock.ofType().object) - .verifiable(TypeMoq.Times.exactly(resource ? 5 : 0)); - app.setup((a) => - a.showErrorMessage( - TypeMoq.It.isAny(), - TypeMoq.It.isAny(), - TypeMoq.It.isAny(), - TypeMoq.It.isAny(), - TypeMoq.It.isAny(), - TypeMoq.It.isAny(), - TypeMoq.It.isAny(), - TypeMoq.It.isAny(), - ), - ) - .returns(() => promptDeferred!.promise) - .verifiable(TypeMoq.Times.once()); - const persistVal = TypeMoq.Mock.ofType>(); - persistVal.setup((p) => p.value).returns(() => false); - persistVal.setup((p) => p.updateValue(TypeMoq.It.isValue(true))); - persistentStore - .setup((ps) => - ps.createGlobalPersistentState( - TypeMoq.It.isAnyString(), - TypeMoq.It.isValue(undefined), - ), - ) - .returns(() => persistVal.object); - - // Display first prompt. - installer.promptToInstall(product.value, resource).ignoreErrors(); - await sleep(1); - - // Display a few more prompts. - installer.promptToInstall(product.value, resource).ignoreErrors(); - await sleep(1); - installer.promptToInstall(product.value, resource).ignoreErrors(); - await sleep(1); - installer.promptToInstall(product.value, resource).ignoreErrors(); - await sleep(1); - installer.promptToInstall(product.value, resource).ignoreErrors(); - await sleep(1); - - app.verifyAll(); - workspaceService.verifyAll(); - }); - test(`Ensure the prompt is displayed again when previous prompt has been closed, ${ - product.name - } (${resource ? 'With a resource' : 'without a resource'})`, async () => { - workspaceService - .setup((w) => w.getWorkspaceFolder(TypeMoq.It.isValue(resource!))) - .returns(() => TypeMoq.Mock.ofType().object) - .verifiable(TypeMoq.Times.exactly(resource ? 3 : 0)); - app.setup((a) => - a.showErrorMessage( - TypeMoq.It.isAny(), - TypeMoq.It.isAny(), - TypeMoq.It.isAny(), - TypeMoq.It.isAny(), - TypeMoq.It.isAny(), - TypeMoq.It.isAny(), - TypeMoq.It.isAny(), - TypeMoq.It.isAny(), - ), - ) - .returns(() => Promise.resolve(undefined)) - .verifiable(TypeMoq.Times.exactly(3)); - const persistVal = TypeMoq.Mock.ofType>(); - persistVal.setup((p) => p.value).returns(() => false); - persistVal.setup((p) => p.updateValue(TypeMoq.It.isValue(true))); - persistentStore - .setup((ps) => - ps.createGlobalPersistentState( - TypeMoq.It.isAnyString(), - TypeMoq.It.isValue(undefined), - ), - ) - .returns(() => persistVal.object); - - await installer.promptToInstall(product.value, resource); - await installer.promptToInstall(product.value, resource); - await installer.promptToInstall(product.value, resource); - - app.verifyAll(); - workspaceService.verifyAll(); - }); - - if (product.value === Product.pylint) { - test(`Ensure the install prompt is not displayed when the user requests it not be shown again, ${ - product.name - } (${resource ? 'With a resource' : 'without a resource'})`, async () => { - workspaceService - .setup((w) => w.getWorkspaceFolder(TypeMoq.It.isValue(resource!))) - .returns(() => TypeMoq.Mock.ofType().object) - .verifiable(TypeMoq.Times.exactly(resource ? 2 : 0)); - app.setup((a) => - a.showErrorMessage( - TypeMoq.It.isAnyString(), - TypeMoq.It.isValue('Install'), - TypeMoq.It.isValue('Select Linter'), - TypeMoq.It.isValue('Do not show again'), - ), - ) - .returns(async () => 'Do not show again') - .verifiable(TypeMoq.Times.once()); - const persistVal = TypeMoq.Mock.ofType>(); - let mockPersistVal = false; - persistVal.setup((p) => p.value).returns(() => mockPersistVal); - persistVal - .setup((p) => p.updateValue(TypeMoq.It.isValue(true))) - .returns(() => { - mockPersistVal = true; - return Promise.resolve(); - }) - .verifiable(TypeMoq.Times.once()); - persistentStore - .setup((ps) => - ps.createGlobalPersistentState( - TypeMoq.It.isAnyString(), - TypeMoq.It.isValue(undefined), - ), - ) - .returns(() => persistVal.object) - .verifiable(TypeMoq.Times.exactly(3)); - - // Display first prompt. - const initialResponse = await installer.promptToInstall(product.value, resource); - - // Display a second prompt. - const secondResponse = await installer.promptToInstall(product.value, resource); - - expect(initialResponse).to.be.equal(InstallerResponse.Ignore); - expect(secondResponse).to.be.equal(InstallerResponse.Ignore); - - app.verifyAll(); - workspaceService.verifyAll(); - persistentStore.verifyAll(); - persistVal.verifyAll(); - }); - } else if (productService.getProductType(product.value) === ProductType.Linter) { - test(`Ensure the 'do not show again' prompt isn't shown for non-pylint linters, ${ - product.name - } (${resource ? 'With a resource' : 'without a resource'})`, async () => { - workspaceService - .setup((w) => w.getWorkspaceFolder(TypeMoq.It.isValue(resource!))) - .returns(() => TypeMoq.Mock.ofType().object); - app.setup((a) => - a.showErrorMessage( - TypeMoq.It.isAnyString(), - TypeMoq.It.isValue('Install'), - TypeMoq.It.isValue('Select Linter'), - ), - ) - .returns(async () => undefined) - .verifiable(TypeMoq.Times.once()); - app.setup((a) => - a.showErrorMessage( - TypeMoq.It.isAnyString(), - TypeMoq.It.isValue('Install'), - TypeMoq.It.isValue('Select Linter'), - TypeMoq.It.isValue('Do not show again'), - ), - ) - .returns(async () => undefined) - .verifiable(TypeMoq.Times.never()); - const persistVal = TypeMoq.Mock.ofType>(); - let mockPersistVal = false; - persistVal.setup((p) => p.value).returns(() => mockPersistVal); - persistVal - .setup((p) => p.updateValue(TypeMoq.It.isValue(true))) - .returns(() => { - mockPersistVal = true; - return Promise.resolve(); - }); - persistentStore - .setup((ps) => - ps.createGlobalPersistentState( - TypeMoq.It.isAnyString(), - TypeMoq.It.isValue(undefined), - ), - ) - .returns(() => persistVal.object); - - // Display the prompt. - await installer.promptToInstall(product.value, resource); - - // we're just ensuring the 'disable pylint' prompt never appears... - app.verifyAll(); - }); - } - - test(`Ensure resource info is passed into the module installer ${product.name} (${ - resource ? 'With a resource' : 'without a resource' - })`, async () => { - moduleInstaller - .setup((m) => - m.installModule( - TypeMoq.It.isValue(product.value), - TypeMoq.It.isValue(resource), - TypeMoq.It.isValue(undefined), - ), - ) - .returns(() => Promise.reject(new Error('UnitTesting'))); - - try { - await installer.install(product.value, resource); - } catch (ex) { - moduleInstaller.verify( - (m) => - m.installModule( - TypeMoq.It.isValue(product.value), - TypeMoq.It.isValue(resource), - TypeMoq.It.isValue(undefined), - ), - TypeMoq.Times.once(), - ); - } - }); - - test(`Return InstallerResponse.Ignore for the module installer ${product.name} (${ - resource ? 'With a resource' : 'without a resource' - }) if installation channel is not defined`, async () => { - moduleInstaller - .setup((m) => - m.installModule( - TypeMoq.It.isValue(product.value), - TypeMoq.It.isValue(resource), - TypeMoq.It.isValue(undefined), - ), - ) - .returns(() => Promise.reject(new Error('UnitTesting'))); - installationChannel.reset(); - installationChannel - .setup((i) => i.getInstallationChannel(TypeMoq.It.isAny(), TypeMoq.It.isAny())) - .returns(() => Promise.resolve(undefined)); - try { - const response = await installer.install(product.value, resource); - expect(response).to.equal(InstallerResponse.Ignore); - } catch (ex) { - assert(false, `Should not throw errors, ${ex}`); - } - }); - test(`Ensure resource info is passed into the module installer (created using ProductInstaller) ${ - product.name - } (${resource ? 'With a resource' : 'without a resource'})`, async () => { - moduleInstaller - .setup((m) => - m.installModule( - TypeMoq.It.isValue(product.value), - TypeMoq.It.isValue(resource), - TypeMoq.It.isValue(undefined), - ), - ) - .returns(() => Promise.reject(new Error('UnitTesting'))); - - try { - await installer.install(product.value, resource); - } catch (ex) { - moduleInstaller.verify( - (m) => - m.installModule( - TypeMoq.It.isValue(product.value), - TypeMoq.It.isValue(resource), - TypeMoq.It.isValue(undefined), - ), - TypeMoq.Times.once(), - ); - } - }); - } - // Test isInstalled() - if (product.value === Product.unittest) { - test(`Method isInstalled() returns true for module installer ${product.name} (${ - resource ? 'With a resource' : 'without a resource' - })`, async () => { - const result = await installer.isInstalled(product.value, resource); - expect(result).to.equal(true, 'Should be true'); - }); - } else { - test(`Method isInstalled() returns true if module is installed for the module installer ${ - product.name - } (${resource ? 'With a resource' : 'without a resource'})`, async () => { - const pythonExecutionFactory = TypeMoq.Mock.ofType(); - const pythonExecutionService = TypeMoq.Mock.ofType(); - serviceContainer - .setup((c) => c.get(TypeMoq.It.isValue(IPythonExecutionFactory))) - .returns(() => pythonExecutionFactory.object); - pythonExecutionFactory - .setup((p) => p.createActivatedEnvironment(TypeMoq.It.isAny())) - .returns(() => Promise.resolve(pythonExecutionService.object)); - // eslint-disable-next-line @typescript-eslint/no-explicit-any - pythonExecutionService.setup((p) => (p as any).then).returns(() => undefined); - pythonExecutionService - .setup((p) => p.isModuleInstalled(TypeMoq.It.isAny())) - .returns(() => Promise.resolve(true)) - .verifiable(TypeMoq.Times.once()); - - const response = await installer.isInstalled(product.value, resource); - expect(response).to.equal(true, 'Should be true'); - pythonExecutionService.verifyAll(); - }); - test(`Method isInstalled() returns false if module is not installed for the module installer ${ - product.name - } (${resource ? 'With a resource' : 'without a resource'})`, async () => { - const pythonExecutionFactory = TypeMoq.Mock.ofType(); - const pythonExecutionService = TypeMoq.Mock.ofType(); - serviceContainer - .setup((c) => c.get(TypeMoq.It.isValue(IPythonExecutionFactory))) - .returns(() => pythonExecutionFactory.object); - pythonExecutionFactory - .setup((p) => p.createActivatedEnvironment(TypeMoq.It.isAny())) - .returns(() => Promise.resolve(pythonExecutionService.object)); - // eslint-disable-next-line @typescript-eslint/no-explicit-any - pythonExecutionService.setup((p) => (p as any).then).returns(() => undefined); - pythonExecutionService - .setup((p) => p.isModuleInstalled(TypeMoq.It.isAny())) - .returns(() => Promise.resolve(false)) - .verifiable(TypeMoq.Times.once()); - - const response = await installer.isInstalled(product.value, resource); - expect(response).to.equal(false, 'Should be false'); - - pythonExecutionService.verifyAll(); - }); - test(`Method isInstalled() returns true if running 'path/to/module_executable --version' succeeds for the module installer ${ - product.name - } (${resource ? 'With a resource' : 'without a resource'})`, async () => { - const processServiceFactory = TypeMoq.Mock.ofType(); - const processService = TypeMoq.Mock.ofType(); - serviceContainer - .setup((c) => c.get(IProcessServiceFactory)) - .returns(() => processServiceFactory.object); - processServiceFactory - .setup((p) => p.create(TypeMoq.It.isAny())) - .returns(() => Promise.resolve(processService.object)); - // eslint-disable-next-line @typescript-eslint/no-explicit-any - processService.setup((p) => (p as any).then).returns(() => undefined); - const executionResult: ExecutionResult = { - stdout: 'output', - }; - processService - .setup((p) => p.exec(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny())) - .returns(() => Promise.resolve(executionResult)) - .verifiable(TypeMoq.Times.once()); - - productPathService.reset(); - productPathService - .setup((p) => p.isExecutableAModule(TypeMoq.It.isAny(), TypeMoq.It.isValue(resource))) - .returns(() => false); - - const response = await installer.isInstalled(product.value, resource); - expect(response).to.equal(true, 'Should be true'); - - processService.verifyAll(); - }); - test(`Method isInstalled() returns false if running 'path/to/module_executable --version' fails for the module installer ${ - product.name - } (${resource ? 'With a resource' : 'without a resource'})`, async () => { - const processServiceFactory = TypeMoq.Mock.ofType(); - const processService = TypeMoq.Mock.ofType(); - serviceContainer - .setup((c) => c.get(IProcessServiceFactory)) - .returns(() => processServiceFactory.object); - processServiceFactory - .setup((p) => p.create(TypeMoq.It.isAny())) - .returns(() => Promise.resolve(processService.object)); - // eslint-disable-next-line @typescript-eslint/no-explicit-any - processService.setup((p) => (p as any).then).returns(() => undefined); - processService - .setup((p) => p.exec(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny())) - .returns(() => Promise.reject(new Error('Kaboom'))) - .verifiable(TypeMoq.Times.once()); - - productPathService.reset(); - productPathService - .setup((p) => p.isExecutableAModule(TypeMoq.It.isAny(), TypeMoq.It.isValue(resource))) - .returns(() => false); - - const response = await installer.isInstalled(product.value, resource); - expect(response).to.equal(false, 'Should be false'); - - processService.verifyAll(); - }); - } - - // Test promptToInstall() when no interpreter is selected - test(`If no interpreter is selected, promptToInstall() doesn't prompt for product ${product.name} (${ - resource ? 'With a resource' : 'without a resource' - })`, async () => { - workspaceService - .setup((w) => w.getWorkspaceFolder(TypeMoq.It.isValue(resource!))) - .returns(() => TypeMoq.Mock.ofType().object) - .verifiable(TypeMoq.Times.never()); - app.setup((a) => - a.showErrorMessage( - TypeMoq.It.isAny(), - TypeMoq.It.isAny(), - TypeMoq.It.isAny(), - TypeMoq.It.isAny(), - TypeMoq.It.isAny(), - TypeMoq.It.isAny(), - TypeMoq.It.isAny(), - TypeMoq.It.isAny(), - ), - ) - .returns(() => Promise.resolve(undefined)) - .verifiable(TypeMoq.Times.never()); - const persistVal = TypeMoq.Mock.ofType>(); - persistVal.setup((p) => p.value).returns(() => false); - persistVal.setup((p) => p.updateValue(TypeMoq.It.isValue(true))); - persistentStore - .setup((ps) => - ps.createGlobalPersistentState( - TypeMoq.It.isAnyString(), - TypeMoq.It.isValue(undefined), - ), - ) - .returns(() => persistVal.object); - - interpreterService.reset(); - interpreterService - .setup((i) => i.getActiveInterpreter(TypeMoq.It.isAny())) - .returns(() => Promise.resolve(undefined)) - .verifiable(TypeMoq.Times.once()); - await installer.promptToInstall(product.value, resource); - - app.verifyAll(); - interpreterService.verifyAll(); - workspaceService.verifyAll(); - }); - }); - - suite('Test FormatterInstaller.promptToInstallImplementation', () => { - class FormatterInstallerTest extends FormatterInstaller { - public async promptToInstallImplementation(product: Product, uri?: Uri): Promise { - return super.promptToInstallImplementation(product, uri); - } - - // eslint-disable-next-line class-methods-use-this - protected getStoredResponse(_key: string) { - return false; - } - - // eslint-disable-next-line class-methods-use-this - protected isExecutableAModule(_product: Product, _resource?: Uri) { - return true; - } - } - let installer: FormatterInstallerTest; - let appShell: IApplicationShell; - let configService: IConfigurationService; - let workspaceService: IWorkspaceService; - let productService: IProductService; - let cmdManager: ICommandManager; - setup(() => { - const serviceContainer = mock(ServiceContainer); - appShell = mock(ApplicationShell); - configService = mock(ConfigurationService); - workspaceService = mock(WorkspaceService); - productService = mock(ProductService); - cmdManager = mock(CommandManager); - - when(serviceContainer.get(IApplicationShell)).thenReturn(instance(appShell)); - when(serviceContainer.get(IConfigurationService)).thenReturn( - instance(configService), - ); - when(serviceContainer.get(IWorkspaceService)).thenReturn(instance(workspaceService)); - when(serviceContainer.get(IProductService)).thenReturn(instance(productService)); - when(serviceContainer.get(ICommandManager)).thenReturn(instance(cmdManager)); - - installer = new FormatterInstallerTest(instance(serviceContainer)); - }); - - teardown(() => { - sinon.restore(); - }); - - test('If nothing is selected, return Ignore as response', async () => { - const product = Product.autopep8; - when( - appShell.showErrorMessage( - `Formatter autopep8 is not installed. Install?`, - 'Yes', - 'Use black', - 'Use yapf', - ), - ).thenReturn((undefined as unknown) as Thenable); - - const response = await installer.promptToInstallImplementation(product, resource); - - verify( - appShell.showErrorMessage( - `Formatter autopep8 is not installed. Install?`, - 'Yes', - 'Use black', - 'Use yapf', - ), - ).once(); - expect(response).to.equal(InstallerResponse.Ignore); - }); - - test('If `Yes` is selected, install product', async () => { - const product = Product.autopep8; - const install = sinon.stub(FormatterInstaller.prototype, 'install'); - install.resolves(InstallerResponse.Installed); - - when( - appShell.showErrorMessage( - `Formatter autopep8 is not installed. Install?`, - 'Yes', - 'Use black', - 'Use yapf', - ), - ).thenReturn(('Yes' as unknown) as Thenable); - const response = await installer.promptToInstallImplementation(product, resource); - - verify( - appShell.showErrorMessage( - `Formatter autopep8 is not installed. Install?`, - 'Yes', - 'Use black', - 'Use yapf', - ), - ).once(); - expect(response).to.equal(InstallerResponse.Installed); - assert.ok(install.calledOnceWith(product, resource, undefined)); - }); - - test('If `Use black` is selected, install black formatter', async () => { - const product = Product.autopep8; - const install = sinon.stub(FormatterInstaller.prototype, 'install'); - install.resolves(InstallerResponse.Installed); - - when( - appShell.showErrorMessage( - `Formatter autopep8 is not installed. Install?`, - 'Yes', - 'Use black', - 'Use yapf', - ), - ).thenReturn(('Use black' as unknown) as Thenable); - when(configService.updateSetting('formatting.provider', 'black', resource)).thenResolve(); - - const response = await installer.promptToInstallImplementation(product, resource); - - verify( - appShell.showErrorMessage( - `Formatter autopep8 is not installed. Install?`, - 'Yes', - 'Use black', - 'Use yapf', - ), - ).once(); - expect(response).to.equal(InstallerResponse.Installed); - verify(configService.updateSetting('formatting.provider', 'black', resource)).once(); - assert.ok(install.calledOnceWith(Product.black, resource, undefined)); - }); - - test('If `Use yapf` is selected, install black formatter', async () => { - const product = Product.autopep8; - const install = sinon.stub(FormatterInstaller.prototype, 'install'); - install.resolves(InstallerResponse.Installed); - - when( - appShell.showErrorMessage( - `Formatter autopep8 is not installed. Install?`, - 'Yes', - 'Use black', - 'Use yapf', - ), - ).thenReturn(('Use yapf' as unknown) as Thenable); - when(configService.updateSetting('formatting.provider', 'yapf', resource)).thenResolve(); - - const response = await installer.promptToInstallImplementation(product, resource); - - verify( - appShell.showErrorMessage( - `Formatter autopep8 is not installed. Install?`, - 'Yes', - 'Use black', - 'Use yapf', - ), - ).once(); - expect(response).to.equal(InstallerResponse.Installed); - verify(configService.updateSetting('formatting.provider', 'yapf', resource)).once(); - assert.ok(install.calledOnceWith(Product.yapf, resource, undefined)); - }); - }); - }); -}); - -[undefined, Uri.file('resource')].forEach((resource) => { - suite(`Test LinterInstaller with resource: ${resource}`, () => { - class LinterInstallerTest extends LinterInstaller { - public isModuleExecutable = true; - - public async promptToInstallImplementation(product: Product, uri?: Uri): Promise { - return super.promptToInstallImplementation(product, uri); - } - - // eslint-disable-next-line class-methods-use-this - protected getStoredResponse(_key: string) { - return false; - } - - protected isExecutableAModule(_product: Product, _resource?: Uri) { - return this.isModuleExecutable; - } - } - - let installer: LinterInstallerTest; - let appShell: IApplicationShell; - let configService: IConfigurationService; - let workspaceService: IWorkspaceService; - let productService: IProductService; - let cmdManager: ICommandManager; - let experimentsService: IExperimentService; - let linterManager: ILinterManager; - let serviceContainer: IServiceContainer; - let productPathService: IProductPathService; - setup(() => { - serviceContainer = mock(ServiceContainer); - appShell = mock(ApplicationShell); - configService = mock(ConfigurationService); - workspaceService = mock(WorkspaceService); - productService = mock(ProductService); - cmdManager = mock(CommandManager); - experimentsService = mock(ExperimentService); - linterManager = mock(LinterManager); - productPathService = mock(LinterProductPathService); - - when(serviceContainer.get(IApplicationShell)).thenReturn(instance(appShell)); - when(serviceContainer.get(IConfigurationService)).thenReturn( - instance(configService), - ); - when(serviceContainer.get(IWorkspaceService)).thenReturn(instance(workspaceService)); - when(serviceContainer.get(IProductService)).thenReturn(instance(productService)); - when(serviceContainer.get(ICommandManager)).thenReturn(instance(cmdManager)); - - const exp = instance(experimentsService); - when(serviceContainer.get(IExperimentService)).thenReturn(exp); - when(experimentsService.inExperiment(anything())).thenResolve(false); - - when(serviceContainer.get(ILinterManager)).thenReturn(instance(linterManager)); - when(serviceContainer.get(IProductPathService, ProductType.Linter)).thenReturn( - instance(productPathService), - ); - - installer = new LinterInstallerTest(instance(serviceContainer)); - }); - - teardown(() => { - sinon.restore(); - }); - - test('Ensure 3 options for pylint', async () => { - const product = Product.pylint; - const options = ['Select Linter', 'Do not show again']; - const productName = ProductNames.get(product)!; - - await installer.promptToInstallImplementation(product, resource); - - verify( - appShell.showErrorMessage(`Linter ${productName} is not installed.`, 'Install', options[0], options[1]), - ).once(); - }); - test('Ensure select linter command is invoked', async () => { - const product = Product.pylint; - const options = ['Select Linter', 'Do not show again']; - const productName = ProductNames.get(product)!; - when( - appShell.showErrorMessage(`Linter ${productName} is not installed.`, 'Install', options[0], options[1]), - ).thenResolve(('Select Linter' as unknown) as void); - when(cmdManager.executeCommand(Commands.Set_Linter)).thenResolve(undefined); - - const response = await installer.promptToInstallImplementation(product, resource); - - verify( - appShell.showErrorMessage(`Linter ${productName} is not installed.`, 'Install', options[0], options[1]), - ).once(); - verify(cmdManager.executeCommand(Commands.Set_Linter)).once(); - expect(response).to.be.equal(InstallerResponse.Ignore); - }); - test('If install button is selected, install linter and return response', async () => { - const product = Product.pylint; - const options = ['Select Linter', 'Do not show again']; - const productName = ProductNames.get(product)!; - when( - appShell.showErrorMessage(`Linter ${productName} is not installed.`, 'Install', options[0], options[1]), - ).thenResolve(('Install' as unknown) as void); - when(cmdManager.executeCommand(Commands.Set_Linter)).thenResolve(undefined); - const install = sinon.stub(LinterInstaller.prototype, 'install'); - install.resolves(InstallerResponse.Installed); - - const response = await installer.promptToInstallImplementation(product, resource); - - expect(response).to.be.equal(InstallerResponse.Installed); - assert.ok(install.calledOnceWith(product, resource, undefined)); - }); - }); -}); diff --git a/src/test/common/installer/moduleInstaller.unit.test.ts b/src/test/common/installer/moduleInstaller.unit.test.ts index 01ac0e315555..3df64ceb2dec 100644 --- a/src/test/common/installer/moduleInstaller.unit.test.ts +++ b/src/test/common/installer/moduleInstaller.unit.test.ts @@ -322,110 +322,6 @@ suite('Module Installer', () => { terminalService.verifyAll(); } - if (product.value === Product.pylint) { - generatePythonInterpreterVersions().forEach((interpreterInfo) => { - const majorVersion = interpreterInfo.version - ? interpreterInfo.version.major - : 0; - if (majorVersion === 2) { - const testTitle = `Ensure install arg is \'pylint<2.0.0\' in ${ - interpreterInfo.version ? interpreterInfo.version.raw : '' - }`; - if (InstallerClass === PipInstaller) { - test(testTitle, async () => { - setActiveInterpreter(interpreterInfo); - const proxyArgs = - proxyServer.length === 0 ? [] : ['--proxy', proxyServer]; - const expectedArgs = [ - '-m', - 'pip', - ...proxyArgs, - 'install', - '-U', - '"pylint<2.0.0"', - ]; - await installModuleAndVerifyCommand(pythonPath, expectedArgs); - }); - } - if (InstallerClass === PipEnvInstaller) { - test(testTitle, async () => { - setActiveInterpreter(interpreterInfo); - const expectedArgs = ['install', '"pylint<2.0.0"', '--dev']; - await installModuleAndVerifyCommand(pipenvName, expectedArgs); - }); - } - if (InstallerClass === CondaInstaller) { - test(testTitle, async () => { - setActiveInterpreter(interpreterInfo); - const expectedArgs = ['install']; - if (condaEnvInfo && condaEnvInfo.name) { - expectedArgs.push('--name'); - expectedArgs.push( - condaEnvInfo.name.toCommandArgumentForPythonExt(), - ); - } else if (condaEnvInfo && condaEnvInfo.path) { - expectedArgs.push('--prefix'); - expectedArgs.push( - condaEnvInfo.path.fileToCommandArgumentForPythonExt(), - ); - } - expectedArgs.push('"pylint<2.0.0"'); - expectedArgs.push('-y'); - await installModuleAndVerifyCommand(condaExecutable, expectedArgs); - }); - } - } else { - const testTitle = `Ensure install arg is \'pylint\' in ${ - interpreterInfo.version ? interpreterInfo.version.raw : '' - }`; - if (InstallerClass === PipInstaller) { - test(testTitle, async () => { - setActiveInterpreter(interpreterInfo); - const proxyArgs = - proxyServer.length === 0 ? [] : ['--proxy', proxyServer]; - const expectedArgs = [ - '-m', - 'pip', - ...proxyArgs, - 'install', - '-U', - 'pylint', - ]; - await installModuleAndVerifyCommand(pythonPath, expectedArgs); - }); - } - if (InstallerClass === PipEnvInstaller) { - test(testTitle, async () => { - setActiveInterpreter(interpreterInfo); - const expectedArgs = ['install', 'pylint', '--dev']; - await installModuleAndVerifyCommand(pipenvName, expectedArgs); - }); - } - if (InstallerClass === CondaInstaller) { - test(testTitle, async () => { - setActiveInterpreter(interpreterInfo); - const expectedArgs = ['install']; - if (condaEnvInfo && condaEnvInfo.name) { - expectedArgs.push('--name'); - expectedArgs.push( - condaEnvInfo.name.toCommandArgumentForPythonExt(), - ); - } else if (condaEnvInfo && condaEnvInfo.path) { - expectedArgs.push('--prefix'); - expectedArgs.push( - condaEnvInfo.path.fileToCommandArgumentForPythonExt(), - ); - } - expectedArgs.push('pylint'); - expectedArgs.push('-y'); - await installModuleAndVerifyCommand(condaExecutable, expectedArgs); - }); - } - } - }); - return; - } - if (InstallerClass === TestModuleInstaller) { suite(`If interpreter type is Unknown (${product.name})`, async () => { test(`If 'python.globalModuleInstallation' is set to true and pythonPath directory is read only, do an elevated install`, async () => { @@ -692,21 +588,6 @@ suite('Module Installer', () => { }); }); -function generatePythonInterpreterVersions() { - const versions: SemVer[] = ['2.7.0-final', '3.4.0-final', '3.5.0-final', '3.6.0-final', '3.7.0-final'].map( - (ver) => new SemVer(ver), - ); - return versions.map((version) => { - const info = TypeMoq.Mock.ofType(); - // eslint-disable-next-line @typescript-eslint/no-explicit-any - info.setup((t: any) => t.then).returns(() => undefined); - info.setup((t) => t.envType).returns(() => EnvironmentType.VirtualEnv); - info.setup((t) => t.version).returns(() => version); - info.setup((t) => t.path).returns(() => pythonPath); - return info.object; - }); -} - function getModuleNamesForTesting(): { name: string; value: Product; moduleName: string }[] { return getNamesAndValues(Product) .map((product) => { diff --git a/src/test/common/installer/productInstaller.unit.test.ts b/src/test/common/installer/productInstaller.unit.test.ts index ed1be158c0aa..2934d613f88f 100644 --- a/src/test/common/installer/productInstaller.unit.test.ts +++ b/src/test/common/installer/productInstaller.unit.test.ts @@ -3,26 +3,15 @@ 'use strict'; -import * as assert from 'assert'; import { expect } from 'chai'; -import * as sinon from 'sinon'; import * as TypeMoq from 'typemoq'; import { IApplicationShell } from '../../../client/common/application/types'; -import { DataScienceInstaller, FormatterInstaller } from '../../../client/common/installer/productInstaller'; -import { ProductNames } from '../../../client/common/installer/productNames'; -import { - IInstallationChannelManager, - IModuleInstaller, - InterpreterUri, - IProductPathService, - IProductService, -} from '../../../client/common/installer/types'; -import { InstallerResponse, IPersistentStateFactory, Product, ProductType } from '../../../client/common/types'; -import { Common } from '../../../client/common/utils/localize'; +import { DataScienceInstaller } from '../../../client/common/installer/productInstaller'; +import { IInstallationChannelManager, IModuleInstaller, InterpreterUri } from '../../../client/common/installer/types'; +import { InstallerResponse, Product } from '../../../client/common/types'; import { Architecture } from '../../../client/common/utils/platform'; import { IServiceContainer } from '../../../client/ioc/types'; import { EnvironmentType, ModuleInstallerType, PythonEnvironment } from '../../../client/pythonEnvironments/info'; -import { MockMemento } from '../../mocks/mementos'; class AlwaysInstalledDataScienceInstaller extends DataScienceInstaller { // eslint-disable-next-line @typescript-eslint/no-unused-vars, class-methods-use-this @@ -57,155 +46,6 @@ suite('DataScienceInstaller install', async () => { // noop }); - test('Requires interpreter Uri', async () => { - let threwUp = false; - try { - await dataScienceInstaller.install(Product.ipykernel); - } catch (ex) { - threwUp = true; - } - expect(threwUp).to.equal(true, 'Should raise exception'); - }); - - test('Will ignore with no installer modules', async () => { - const testEnvironment: PythonEnvironment = { - envType: EnvironmentType.VirtualEnv, - envName: 'test', - envPath: interpreterPath, - path: interpreterPath, - architecture: Architecture.x64, - sysPrefix: '', - }; - installationChannelManager - .setup((c) => c.getInstallationChannels(TypeMoq.It.isAny())) - .returns(() => Promise.resolve([])); - const result = await dataScienceInstaller.install(Product.ipykernel, testEnvironment); - expect(result).to.equal(InstallerResponse.Ignore, 'Should be InstallerResponse.Ignore'); - }); - - test('Will invoke conda for conda environments', async () => { - const testEnvironment: PythonEnvironment = { - envType: EnvironmentType.Conda, - envName: 'test', - envPath: interpreterPath, - path: interpreterPath, - architecture: Architecture.x64, - sysPrefix: '', - }; - const testInstaller = TypeMoq.Mock.ofType(); - testInstaller.setup((c) => c.type).returns(() => ModuleInstallerType.Conda); - testInstaller - .setup((c) => - c.installModule( - TypeMoq.It.isValue(Product.ipykernel), - TypeMoq.It.isValue(testEnvironment), - TypeMoq.It.isAny(), - TypeMoq.It.isAny(), - ), - ) - .returns(() => Promise.resolve()); - - installationChannelManager - .setup((c) => c.getInstallationChannels(TypeMoq.It.isAny())) - .returns(() => Promise.resolve([testInstaller.object])); - - const result = await dataScienceInstaller.install(Product.ipykernel, testEnvironment); - expect(result).to.equal(InstallerResponse.Installed, 'Should be Installed'); - }); - - test('Will invoke pip by default', async () => { - const testEnvironment: PythonEnvironment = { - envType: EnvironmentType.VirtualEnv, - envName: 'test', - envPath: interpreterPath, - path: interpreterPath, - architecture: Architecture.x64, - sysPrefix: '', - }; - const testInstaller = TypeMoq.Mock.ofType(); - - testInstaller.setup((c) => c.type).returns(() => ModuleInstallerType.Pip); - testInstaller - .setup((c) => - c.installModule( - TypeMoq.It.isValue(Product.ipykernel), - TypeMoq.It.isValue(testEnvironment), - TypeMoq.It.isAny(), - TypeMoq.It.isAny(), - ), - ) - .returns(() => Promise.resolve()); - - installationChannelManager - .setup((c) => c.getInstallationChannels(TypeMoq.It.isAny())) - .returns(() => Promise.resolve([testInstaller.object])); - - const result = await dataScienceInstaller.install(Product.ipykernel, testEnvironment); - expect(result).to.equal(InstallerResponse.Installed, 'Should be Installed'); - }); - - test('Will invoke poetry', async () => { - const testEnvironment: PythonEnvironment = { - envType: EnvironmentType.Poetry, - envName: 'test', - envPath: interpreterPath, - path: interpreterPath, - architecture: Architecture.x64, - sysPrefix: '', - }; - const testInstaller = TypeMoq.Mock.ofType(); - - testInstaller.setup((c) => c.type).returns(() => ModuleInstallerType.Poetry); - testInstaller - .setup((c) => - c.installModule( - TypeMoq.It.isValue(Product.ipykernel), - TypeMoq.It.isValue(testEnvironment), - TypeMoq.It.isAny(), - TypeMoq.It.isAny(), - ), - ) - .returns(() => Promise.resolve()); - - installationChannelManager - .setup((c) => c.getInstallationChannels(TypeMoq.It.isAny())) - .returns(() => Promise.resolve([testInstaller.object])); - - const result = await dataScienceInstaller.install(Product.ipykernel, testEnvironment); - expect(result).to.equal(InstallerResponse.Installed, 'Should be Installed'); - }); - - test('Will invoke pipenv', async () => { - const testEnvironment: PythonEnvironment = { - envType: EnvironmentType.Pipenv, - envName: 'test', - envPath: interpreterPath, - path: interpreterPath, - architecture: Architecture.x64, - sysPrefix: '', - }; - const testInstaller = TypeMoq.Mock.ofType(); - - testInstaller.setup((c) => c.type).returns(() => ModuleInstallerType.Pipenv); - testInstaller - .setup((c) => - c.installModule( - TypeMoq.It.isValue(Product.ipykernel), - TypeMoq.It.isValue(testEnvironment), - TypeMoq.It.isAny(), - TypeMoq.It.isAny(), - ), - ) - .returns(() => Promise.resolve()); - - installationChannelManager - .setup((c) => c.getInstallationChannels(TypeMoq.It.isAny())) - .returns(() => Promise.resolve([testInstaller.object])); - - const result = await dataScienceInstaller.install(Product.ipykernel, testEnvironment); - expect(result).to.equal(InstallerResponse.Installed, 'Should be Installed'); - }); - test('Will invoke pip for pytorch with conda environment', async () => { // See https://github.com/microsoft/vscode-jupyter/issues/5034 const testEnvironment: PythonEnvironment = { @@ -238,250 +78,3 @@ suite('DataScienceInstaller install', async () => { expect(result).to.equal(InstallerResponse.Installed, 'Should be Installed'); }); }); - -suite('Formatter installer', async () => { - let serviceContainer: TypeMoq.IMock; - // let outputChannel: TypeMoq.IMock; - let appShell: TypeMoq.IMock; - let persistentStateFactory: TypeMoq.IMock; - let productPathService: TypeMoq.IMock; - // let isExecutableAsModuleStub: sinon.SinonStub; - - // constructor(protected serviceContainer: IServiceContainer, protected outputChannel: OutputChannel) { - // this.appShell = serviceContainer.get(IApplicationShell); - // this.configService = serviceContainer.get(IConfigurationService); - // this.workspaceService = serviceContainer.get(IWorkspaceService); - // this.productService = serviceContainer.get(IProductService); - // this.persistentStateFactory = serviceContainer.get(IPersistentStateFactory); - // } - - setup(() => { - serviceContainer = TypeMoq.Mock.ofType(); - // outputChannel = TypeMoq.Mock.ofType(); - appShell = TypeMoq.Mock.ofType(); - persistentStateFactory = TypeMoq.Mock.ofType(); - productPathService = TypeMoq.Mock.ofType(); - - const installStub = sinon.stub(FormatterInstaller.prototype, 'install'); - installStub.returns(Promise.resolve(InstallerResponse.Installed)); - - const productService = TypeMoq.Mock.ofType(); - productService.setup((p) => p.getProductType(TypeMoq.It.isAny())).returns(() => ProductType.Formatter); - - serviceContainer.setup((c) => c.get(TypeMoq.It.isValue(IApplicationShell))).returns(() => appShell.object); - serviceContainer - .setup((c) => c.get(TypeMoq.It.isValue(IPersistentStateFactory))) - .returns(() => persistentStateFactory.object); - serviceContainer.setup((c) => c.get(TypeMoq.It.isValue(IProductService))).returns(() => productService.object); - serviceContainer - .setup((c) => c.get(TypeMoq.It.isValue(IProductPathService), ProductType.Formatter)) - .returns(() => productPathService.object); - }); - - teardown(() => { - sinon.restore(); - }); - - // - if black not installed, offer autopep8 and yapf options - // - if autopep8 not installed, offer black and yapf options - // - if yapf not installed, offer black and autopep8 options - // - if not executable as a module, display error message - // - if never show again was set to true earlier, ignore - // if never show again is selected, ignore - - test('If black is not installed, offer autopep8 and yapf as options', async () => { - const messageOptions = [ - Common.bannerLabelYes, - `Use ${ProductNames.get(Product.autopep8)!}`, - `Use ${ProductNames.get(Product.yapf)!}`, - Common.doNotShowAgain, - ]; - - appShell - .setup((a) => a.showErrorMessage(TypeMoq.It.isAnyString(), ...messageOptions)) - .returns(() => Promise.resolve(Common.bannerLabelYes)) - .verifiable(TypeMoq.Times.once()); - productPathService - .setup((p) => p.isExecutableAModule(TypeMoq.It.isAny(), TypeMoq.It.isAny())) - .returns(() => true) - .verifiable(TypeMoq.Times.once()); - persistentStateFactory - .setup((p) => p.createGlobalPersistentState(TypeMoq.It.isAnyString(), false)) - .returns(() => ({ - value: false, - updateValue: () => Promise.resolve(), - storage: new MockMemento(), - })); - - const formatterInstaller = new FormatterInstaller(serviceContainer.object); - const result = await formatterInstaller.promptToInstall(Product.black); - - appShell.verifyAll(); - productPathService.verifyAll(); - assert.strictEqual(result, InstallerResponse.Installed); - }); - - test('If autopep8 is not installed, offer black and yapf as options', async () => { - const messageOptions = [ - Common.bannerLabelYes, - - 'Use {0}'.format(ProductNames.get(Product.black)!), - 'Use {0}'.format(ProductNames.get(Product.yapf)!), - Common.doNotShowAgain, - ]; - - appShell - .setup((a) => a.showErrorMessage(TypeMoq.It.isAnyString(), ...messageOptions)) - .returns(() => Promise.resolve(Common.bannerLabelYes)) - .verifiable(TypeMoq.Times.once()); - productPathService - .setup((p) => p.isExecutableAModule(TypeMoq.It.isAny(), TypeMoq.It.isAny())) - .returns(() => true) - .verifiable(TypeMoq.Times.once()); - persistentStateFactory - .setup((p) => p.createGlobalPersistentState(TypeMoq.It.isAnyString(), false)) - .returns(() => ({ - value: false, - updateValue: () => Promise.resolve(), - storage: new MockMemento(), - })); - - const formatterInstaller = new FormatterInstaller(serviceContainer.object); - const result = await formatterInstaller.promptToInstall(Product.autopep8); - - appShell.verifyAll(); - productPathService.verifyAll(); - assert.strictEqual(result, InstallerResponse.Installed); - }); - - test('If yapf is not installed, offer autopep8 and black as options', async () => { - const messageOptions = [ - Common.bannerLabelYes, - `Use ${ProductNames.get(Product.autopep8)!}`, - `Use ${ProductNames.get(Product.black)!}`, - Common.doNotShowAgain, - ]; - - appShell - .setup((a) => a.showErrorMessage(TypeMoq.It.isAnyString(), ...messageOptions)) - .returns(() => Promise.resolve(Common.bannerLabelYes)) - .verifiable(TypeMoq.Times.once()); - productPathService - .setup((p) => p.isExecutableAModule(TypeMoq.It.isAny(), TypeMoq.It.isAny())) - .returns(() => true) - .verifiable(TypeMoq.Times.once()); - persistentStateFactory - .setup((p) => p.createGlobalPersistentState(TypeMoq.It.isAnyString(), false)) - .returns(() => ({ - value: false, - updateValue: () => Promise.resolve(), - storage: new MockMemento(), - })); - - const formatterInstaller = new FormatterInstaller(serviceContainer.object); - const result = await formatterInstaller.promptToInstall(Product.yapf); - - appShell.verifyAll(); - productPathService.verifyAll(); - assert.strictEqual(result, InstallerResponse.Installed); - }); - - test('If the formatter is not executable as a module, display an error message', async () => { - const messageOptions = [ - `Use ${ProductNames.get(Product.autopep8)!}`, - `Use ${ProductNames.get(Product.yapf)!}`, - Common.doNotShowAgain, - ]; - - appShell - .setup((a) => a.showErrorMessage(TypeMoq.It.isAnyString(), ...messageOptions)) - .returns(() => Promise.resolve(Common.bannerLabelYes)) - .verifiable(TypeMoq.Times.once()); - productPathService - .setup((p) => p.isExecutableAModule(TypeMoq.It.isAny(), TypeMoq.It.isAny())) - .returns(() => false) - .verifiable(TypeMoq.Times.once()); - productPathService - .setup((p) => p.getExecutableNameFromSettings(TypeMoq.It.isAny(), TypeMoq.It.isAny())) - .returns(() => 'foo'); - persistentStateFactory - .setup((p) => p.createGlobalPersistentState(TypeMoq.It.isAnyString(), false)) - .returns(() => ({ - value: false, - updateValue: () => Promise.resolve(), - storage: new MockMemento(), - })); - - const formatterInstaller = new FormatterInstaller(serviceContainer.object); - await formatterInstaller.promptToInstall(Product.black); - - appShell.verifyAll(); - productPathService.verifyAll(); - }); - - test('If "Do not show again" has been selected earlier, do not display the prompt', async () => { - const messageOptions = [ - Common.bannerLabelYes, - `Use ${ProductNames.get(Product.autopep8)!}`, - `Use ${ProductNames.get(Product.yapf)!}`, - Common.doNotShowAgain, - ]; - - appShell - .setup((a) => a.showErrorMessage(TypeMoq.It.isAnyString(), ...messageOptions)) - .returns(() => Promise.resolve(Common.bannerLabelYes)) - .verifiable(TypeMoq.Times.never()); - persistentStateFactory - .setup((p) => p.createGlobalPersistentState(TypeMoq.It.isAnyString(), false)) - .returns(() => ({ - value: true, - updateValue: () => Promise.resolve(), - storage: new MockMemento(), - })); - - const formatterInstaller = new FormatterInstaller(serviceContainer.object); - const result = await formatterInstaller.promptToInstall(Product.black); - - appShell.verifyAll(); - assert.strictEqual(result, InstallerResponse.Ignore); - }); - - test('If "Do not show again" is selected, do not install the formatter and do not show the prompt again', async () => { - let value = false; - const messageOptions = [ - Common.bannerLabelYes, - `Use ${ProductNames.get(Product.autopep8)!}`, - `Use ${ProductNames.get(Product.yapf)!}`, - Common.doNotShowAgain, - ]; - - appShell - .setup((a) => a.showErrorMessage(TypeMoq.It.isAnyString(), ...messageOptions)) - .returns(() => Promise.resolve(Common.doNotShowAgain)) - .verifiable(TypeMoq.Times.once()); - productPathService - .setup((p) => p.isExecutableAModule(TypeMoq.It.isAny(), TypeMoq.It.isAny())) - .returns(() => true) - .verifiable(TypeMoq.Times.once()); - - persistentStateFactory - .setup((p) => p.createGlobalPersistentState(TypeMoq.It.isAnyString(), false)) - .returns(() => ({ - value, - updateValue: (newValue) => { - value = newValue; - return Promise.resolve(); - }, - storage: new MockMemento(), - })); - - const formatterInstaller = new FormatterInstaller(serviceContainer.object); - const result = await formatterInstaller.promptToInstall(Product.black); - const resultTwo = await formatterInstaller.promptToInstall(Product.black); - - appShell.verifyAll(); - productPathService.verifyAll(); - assert.strictEqual(result, InstallerResponse.Ignore); - assert.strictEqual(resultTwo, InstallerResponse.Ignore); - }); -}); diff --git a/src/test/common/installer/productPath.unit.test.ts b/src/test/common/installer/productPath.unit.test.ts deleted file mode 100644 index 1e64ca63e117..000000000000 --- a/src/test/common/installer/productPath.unit.test.ts +++ /dev/null @@ -1,227 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; - -import { fail } from 'assert'; -import { expect, use } from 'chai'; -import * as chaiAsPromised from 'chai-as-promised'; -import * as TypeMoq from 'typemoq'; -import { Uri } from 'vscode'; -import '../../../client/common/extensions'; -import { ProductInstaller } from '../../../client/common/installer/productInstaller'; -import { - BaseProductPathsService, - FormatterProductPathService, - LinterProductPathService, - TestFrameworkProductPathService, -} from '../../../client/common/installer/productPath'; -import { ProductService } from '../../../client/common/installer/productService'; -import { IProductService } from '../../../client/common/installer/types'; -import { - IConfigurationService, - IFormattingSettings, - IInstaller, - IPythonSettings, - Product, - ProductType, -} from '../../../client/common/types'; -import { getNamesAndValues } from '../../../client/common/utils/enum'; -import { IFormatterHelper } from '../../../client/formatters/types'; -import { IServiceContainer } from '../../../client/ioc/types'; -import { ILinterInfo, ILinterManager } from '../../../client/linters/types'; -import { ITestsHelper } from '../../../client/testing/common/types'; -import { ITestingSettings } from '../../../client/testing/configuration/types'; - -use(chaiAsPromised); - -suite('Product Path', () => { - [undefined, Uri.file('resource')].forEach((resource) => { - getNamesAndValues(Product).forEach((product) => { - class TestBaseProductPathsService extends BaseProductPathsService { - public getExecutableNameFromSettings(_: Product, _resource?: Uri): string { - return ''; - } - } - let serviceContainer: TypeMoq.IMock; - let formattingSettings: TypeMoq.IMock; - let unitTestSettings: TypeMoq.IMock; - let configService: TypeMoq.IMock; - let productInstaller: ProductInstaller; - setup(function () { - if (new ProductService().getProductType(product.value) === ProductType.DataScience) { - return this.skip(); - } - serviceContainer = TypeMoq.Mock.ofType(); - configService = TypeMoq.Mock.ofType(); - formattingSettings = TypeMoq.Mock.ofType(); - unitTestSettings = TypeMoq.Mock.ofType(); - - productInstaller = new ProductInstaller(serviceContainer.object); - const pythonSettings = TypeMoq.Mock.ofType(); - pythonSettings.setup((p) => p.formatting).returns(() => formattingSettings.object); - pythonSettings.setup((p) => p.testing).returns(() => unitTestSettings.object); - configService - .setup((s) => s.getSettings(TypeMoq.It.isValue(resource))) - .returns(() => pythonSettings.object); - serviceContainer - .setup((s) => s.get(TypeMoq.It.isValue(IConfigurationService), TypeMoq.It.isAny())) - .returns(() => configService.object); - serviceContainer - .setup((s) => s.get(TypeMoq.It.isValue(IInstaller), TypeMoq.It.isAny())) - .returns(() => productInstaller); - serviceContainer - .setup((c) => c.get(TypeMoq.It.isValue(IProductService), TypeMoq.It.isAny())) - .returns(() => new ProductService()); - }); - - if (product.value === Product.isort) { - return; - } - suite('Method isExecutableAModule()', () => { - test('Returns true if User has customized the executable name', () => { - productInstaller.translateProductToModuleName = () => 'moduleName'; - const productPathService = new TestBaseProductPathsService(serviceContainer.object); - productPathService.getExecutableNameFromSettings = () => 'executableName'; - expect(productPathService.isExecutableAModule(product.value)).to.equal(true, 'Should be true'); - }); - test('Returns false if User has customized the full path to executable', () => { - productInstaller.translateProductToModuleName = () => 'moduleName'; - const productPathService = new TestBaseProductPathsService(serviceContainer.object); - productPathService.getExecutableNameFromSettings = () => 'path/to/executable'; - expect(productPathService.isExecutableAModule(product.value)).to.equal(false, 'Should be false'); - }); - test('Returns false if translating product to module name fails with error', () => { - productInstaller.translateProductToModuleName = () => { - return new Error('Kaboom') as any; - }; - const productPathService = new TestBaseProductPathsService(serviceContainer.object); - productPathService.getExecutableNameFromSettings = () => 'executableName'; - expect(productPathService.isExecutableAModule(product.value)).to.equal(false, 'Should be false'); - }); - }); - const productType = new ProductService().getProductType(product.value); - switch (productType) { - case ProductType.Formatter: { - test(`Ensure path is returned for ${product.name} (${ - resource ? 'With a resource' : 'without a resource' - })`, async () => { - const productPathService = new FormatterProductPathService(serviceContainer.object); - const formatterHelper = TypeMoq.Mock.ofType(); - const expectedPath = 'Some Path'; - serviceContainer - .setup((s) => s.get(TypeMoq.It.isValue(IFormatterHelper), TypeMoq.It.isAny())) - .returns(() => formatterHelper.object); - formattingSettings - .setup((f) => f.autopep8Path) - .returns(() => expectedPath) - .verifiable(TypeMoq.Times.atLeastOnce()); - formatterHelper - .setup((f) => f.getSettingsPropertyNames(TypeMoq.It.isValue(product.value))) - .returns(() => { - return { - pathName: 'autopep8Path', - argsName: 'autopep8Args', - }; - }) - .verifiable(TypeMoq.Times.once()); - - const value = productPathService.getExecutableNameFromSettings(product.value, resource); - expect(value).to.be.equal(expectedPath); - formattingSettings.verifyAll(); - formatterHelper.verifyAll(); - }); - break; - } - case ProductType.Linter: { - test(`Ensure path is returned for ${product.name} (${ - resource ? 'With a resource' : 'without a resource' - })`, async () => { - const productPathService = new LinterProductPathService(serviceContainer.object); - const linterManager = TypeMoq.Mock.ofType(); - const linterInfo = TypeMoq.Mock.ofType(); - const expectedPath = 'Some Path'; - serviceContainer - .setup((s) => s.get(TypeMoq.It.isValue(ILinterManager), TypeMoq.It.isAny())) - .returns(() => linterManager.object); - linterInfo - .setup((l) => l.pathName(TypeMoq.It.isValue(resource))) - .returns(() => expectedPath) - .verifiable(TypeMoq.Times.once()); - linterManager - .setup((l) => l.getLinterInfo(TypeMoq.It.isValue(product.value))) - .returns(() => linterInfo.object) - .verifiable(TypeMoq.Times.once()); - - const value = productPathService.getExecutableNameFromSettings(product.value, resource); - expect(value).to.be.equal(expectedPath); - linterInfo.verifyAll(); - linterManager.verifyAll(); - }); - break; - } - case ProductType.TestFramework: { - test(`Ensure path is returned for ${product.name} (${ - resource ? 'With a resource' : 'without a resource' - })`, async () => { - const productPathService = new TestFrameworkProductPathService(serviceContainer.object); - const testHelper = TypeMoq.Mock.ofType(); - const expectedPath = 'Some Path'; - serviceContainer - .setup((s) => s.get(TypeMoq.It.isValue(ITestsHelper), TypeMoq.It.isAny())) - .returns(() => testHelper.object); - testHelper - .setup((t) => t.getSettingsPropertyNames(TypeMoq.It.isValue(product.value))) - .returns(() => { - return { - argsName: 'autoTestDiscoverOnSaveEnabled', - enabledName: 'autoTestDiscoverOnSaveEnabled', - pathName: 'pytestPath', - }; - }) - .verifiable(TypeMoq.Times.once()); - unitTestSettings - .setup((u) => u.pytestPath) - .returns(() => expectedPath) - .verifiable(TypeMoq.Times.atLeastOnce()); - - const value = productPathService.getExecutableNameFromSettings(product.value, resource); - expect(value).to.be.equal(expectedPath); - testHelper.verifyAll(); - unitTestSettings.verifyAll(); - }); - test(`Ensure module name is returned for ${product.name} (${ - resource ? 'With a resource' : 'without a resource' - })`, async () => { - const productPathService = new TestFrameworkProductPathService(serviceContainer.object); - const testHelper = TypeMoq.Mock.ofType(); - serviceContainer - .setup((s) => s.get(TypeMoq.It.isValue(ITestsHelper), TypeMoq.It.isAny())) - .returns(() => testHelper.object); - testHelper - .setup((t) => t.getSettingsPropertyNames(TypeMoq.It.isValue(product.value))) - .returns(() => { - return { - argsName: 'autoTestDiscoverOnSaveEnabled', - enabledName: 'autoTestDiscoverOnSaveEnabled', - pathName: undefined, - }; - }) - .verifiable(TypeMoq.Times.once()); - - const value = productPathService.getExecutableNameFromSettings(product.value, resource); - const moduleName = productInstaller.translateProductToModuleName(product.value); - expect(value).to.be.equal(moduleName); - testHelper.verifyAll(); - }); - break; - } - default: { - test(`No tests for Product Path of this Product Type ${product.name}`, () => { - fail('No tests for Product Path of this Product Type'); - }); - } - } - }); - }); -}); diff --git a/src/test/common/installer/serviceRegistry.unit.test.ts b/src/test/common/installer/serviceRegistry.unit.test.ts index a23cff298d6c..8a811ad7ac4d 100644 --- a/src/test/common/installer/serviceRegistry.unit.test.ts +++ b/src/test/common/installer/serviceRegistry.unit.test.ts @@ -9,11 +9,7 @@ import { CondaInstaller } from '../../../client/common/installer/condaInstaller' import { PipEnvInstaller } from '../../../client/common/installer/pipEnvInstaller'; import { PipInstaller } from '../../../client/common/installer/pipInstaller'; import { PoetryInstaller } from '../../../client/common/installer/poetryInstaller'; -import { - FormatterProductPathService, - LinterProductPathService, - TestFrameworkProductPathService, -} from '../../../client/common/installer/productPath'; +import { TestFrameworkProductPathService } from '../../../client/common/installer/productPath'; import { ProductService } from '../../../client/common/installer/productService'; import { registerTypes } from '../../../client/common/installer/serviceRegistry'; import { @@ -46,20 +42,6 @@ suite('Common installer Service Registry', () => { ), ).once(); verify(serviceManager.addSingleton(IProductService, ProductService)).once(); - verify( - serviceManager.addSingleton( - IProductPathService, - FormatterProductPathService, - ProductType.Formatter, - ), - ).once(); - verify( - serviceManager.addSingleton( - IProductPathService, - LinterProductPathService, - ProductType.Linter, - ), - ).once(); verify( serviceManager.addSingleton( IProductPathService, diff --git a/src/test/common/interpreterPathService.unit.test.ts b/src/test/common/interpreterPathService.unit.test.ts index 6ba63d9d663d..58a34b3cbcde 100644 --- a/src/test/common/interpreterPathService.unit.test.ts +++ b/src/test/common/interpreterPathService.unit.test.ts @@ -15,7 +15,11 @@ import { WorkspaceConfiguration, } from 'vscode'; import { IApplicationEnvironment, IWorkspaceService } from '../../client/common/application/types'; -import { defaultInterpreterPathSetting, InterpreterPathService } from '../../client/common/interpreterPathService'; +import { + defaultInterpreterPathSetting, + getCIPythonPath, + InterpreterPathService, +} from '../../client/common/interpreterPathService'; import { FileSystemPaths } from '../../client/common/platform/fs-paths'; import { InterpreterConfigurationScope, IPersistentState, IPersistentStateFactory } from '../../client/common/types'; import { createDeferred, sleep } from '../../client/common/utils/async'; @@ -447,7 +451,8 @@ suite('Interpreter Path Service', async () => { workspaceValue: undefined, }); const settingValue = interpreterPathService.get(resource); - expect(settingValue).to.equal('python'); + + expect(settingValue).to.equal(getCIPythonPath()); }); test('If defaultInterpreterPathSetting is changed, an event is fired', async () => { diff --git a/src/test/common/moduleInstaller.test.ts b/src/test/common/moduleInstaller.test.ts index a6b647ad181d..0cdb6f270c54 100644 --- a/src/test/common/moduleInstaller.test.ts +++ b/src/test/common/moduleInstaller.test.ts @@ -1,9 +1,9 @@ import { expect, should as chaiShould, use as chaiUse } from 'chai'; import * as chaiAsPromised from 'chai-as-promised'; import { SemVer } from 'semver'; -import { instance, mock, when } from 'ts-mockito'; +import { instance, mock } from 'ts-mockito'; import * as TypeMoq from 'typemoq'; -import { ConfigurationTarget, Uri } from 'vscode'; +import { Uri } from 'vscode'; import { IExtensionSingleActivationService } from '../../client/activation/types'; import { ActiveResourceService } from '../../client/common/application/activeResource'; import { ApplicationEnvironment } from '../../client/common/application/applicationEnvironment'; @@ -13,7 +13,6 @@ import { CommandManager } from '../../client/common/application/commandManager'; import { ReloadVSCodeCommandHandler } from '../../client/common/application/commands/reloadCommand'; import { ReportIssueCommandHandler } from '../../client/common/application/commands/reportIssueCommand'; import { DebugService } from '../../client/common/application/debugService'; -import { DebugSessionTelemetry } from '../../client/common/application/debugSessionTelemetry'; import { DocumentManager } from '../../client/common/application/documentManager'; import { Extensions } from '../../client/common/application/extensions'; import { @@ -29,7 +28,6 @@ import { } from '../../client/common/application/types'; import { WorkspaceService } from '../../client/common/application/workspace'; import { ConfigurationService } from '../../client/common/configuration/service'; -import { EditorUtils } from '../../client/common/editor'; import { ExperimentService } from '../../client/common/experiments/service'; import { CondaInstaller } from '../../client/common/installer/condaInstaller'; import { PipEnvInstaller } from '../../client/common/installer/pipEnvInstaller'; @@ -73,7 +71,6 @@ import { IBrowserService, IConfigurationService, ICurrentProcess, - IEditorUtils, IExperimentService, IExtensions, IInstaller, @@ -98,13 +95,14 @@ import { JupyterExtensionDependencyManager } from '../../client/jupyter/jupyterE import { EnvironmentType, PythonEnvironment } from '../../client/pythonEnvironments/info'; import { ImportTracker } from '../../client/telemetry/importTracker'; import { IImportTracker } from '../../client/telemetry/types'; -import { PYTHON_PATH, rootWorkspaceUri } from '../common'; +import { PYTHON_PATH } from '../common'; import { MockModuleInstaller } from '../mocks/moduleInstaller'; import { MockProcessService } from '../mocks/proc'; import { UnitTestIocContainer } from '../testing/serviceRegistry'; import { closeActiveWindows, initializeTest } from '../initialize'; +import { createTypeMoq } from '../mocks/helper'; -chaiUse(chaiAsPromised); +chaiUse(chaiAsPromised.default); const info: PythonEnvironment = { architecture: Architecture.Unknown, @@ -132,7 +130,6 @@ suite('Module Installer', () => { chaiShould(); await initializeDI(); await initializeTest(); - await resetSettings(); }); suiteTeardown(async () => { await closeActiveWindows(); @@ -146,16 +143,14 @@ suite('Module Installer', () => { ioc = new UnitTestIocContainer(); ioc.registerUnitTestTypes(); ioc.registerVariableTypes(); - ioc.registerLinterTypes(); - ioc.registerFormatterTypes(); ioc.registerInterpreterStorageTypes(); ioc.serviceManager.addSingleton(IPersistentStateFactory, PersistentStateFactory); ioc.serviceManager.addSingleton(IProcessLogger, ProcessLogger); ioc.serviceManager.addSingleton(IInstaller, ProductInstaller); - mockTerminalService = TypeMoq.Mock.ofType(); - mockTerminalFactory = TypeMoq.Mock.ofType(); + mockTerminalService = createTypeMoq(); + mockTerminalFactory = createTypeMoq(); // If resource is provided, then ensure we do not invoke without the resource. mockTerminalFactory .setup((t) => t.getTerminalService(TypeMoq.It.isAny())) @@ -165,11 +160,13 @@ suite('Module Installer', () => { ITerminalServiceFactory, mockTerminalFactory.object, ); - const activatedEnvironmentLaunch = mock(); - when(activatedEnvironmentLaunch.selectIfLaunchedViaActivatedEnv()).thenResolve(undefined); + const activatedEnvironmentLaunch = createTypeMoq(); + activatedEnvironmentLaunch + .setup((t) => t.selectIfLaunchedViaActivatedEnv()) + .returns(() => Promise.resolve(undefined)); ioc.serviceManager.addSingletonInstance( IActivatedEnvironmentLaunch, - instance(activatedEnvironmentLaunch), + activatedEnvironmentLaunch.object, ); ioc.serviceManager.addSingleton(IModuleInstaller, PipInstaller); ioc.serviceManager.addSingleton(IModuleInstaller, CondaInstaller); @@ -187,10 +184,10 @@ suite('Module Installer', () => { ioc.serviceManager.addSingletonInstance(IsWindows, false); await ioc.registerMockInterpreterTypes(); - condaService = TypeMoq.Mock.ofType(); - condaLocatorService = TypeMoq.Mock.ofType(); + condaService = createTypeMoq(); + condaLocatorService = createTypeMoq(); ioc.serviceManager.rebindInstance(ICondaService, condaService.object); - interpreterService = TypeMoq.Mock.ofType(); + interpreterService = createTypeMoq(); ioc.serviceManager.rebindInstance(IInterpreterService, interpreterService.object); ioc.serviceManager.addSingleton(IActiveResourceService, ActiveResourceService); @@ -208,7 +205,6 @@ suite('Module Installer', () => { JupyterExtensionDependencyManager, ); ioc.serviceManager.addSingleton(IBrowserService, BrowserService); - ioc.serviceManager.addSingleton(IEditorUtils, EditorUtils); ioc.serviceManager.addSingleton(ITerminalActivator, TerminalActivator); ioc.serviceManager.addSingleton( ITerminalActivationHandler, @@ -262,19 +258,6 @@ suite('Module Installer', () => { IExtensionSingleActivationService, ReportIssueCommandHandler, ); - ioc.serviceManager.addSingleton( - IExtensionSingleActivationService, - DebugSessionTelemetry, - ); - } - async function resetSettings(): Promise { - const configService = ioc.serviceManager.get(IConfigurationService); - await configService.updateSetting( - 'linting.pylintEnabled', - true, - rootWorkspaceUri, - ConfigurationTarget.Workspace, - ); } test('Ensure pip is supported and conda is not', async () => { ioc.serviceManager.addSingletonInstance( @@ -282,10 +265,8 @@ suite('Module Installer', () => { new MockModuleInstaller('mock', true), ); ioc.serviceManager.addSingletonInstance(ITerminalHelper, instance(mock(TerminalHelper))); - - const processService = (await ioc.serviceContainer - .get(IProcessServiceFactory) - .create()) as MockProcessService; + const factory = ioc.serviceManager.get(IProcessServiceFactory); + const processService = (await factory.create()) as MockProcessService; processService.onExec((file, args, _options, callback) => { if (args.length > 1 && args[0] === '-c' && args[1] === 'import pip') { callback({ stdout: '' }); @@ -336,13 +317,13 @@ suite('Module Installer', () => { await expect(pipInstaller.isSupported()).to.eventually.equal(true, 'Pip is not supported'); }); test('Ensure conda is supported', async () => { - const serviceContainer = TypeMoq.Mock.ofType(); + const serviceContainer = createTypeMoq(); - const configService = TypeMoq.Mock.ofType(); + const configService = createTypeMoq(); serviceContainer .setup((c) => c.get(TypeMoq.It.isValue(IConfigurationService))) .returns(() => configService.object); - const settings = TypeMoq.Mock.ofType(); + const settings = createTypeMoq(); const pythonPath = 'pythonABC'; settings.setup((s) => s.pythonPath).returns(() => pythonPath); configService.setup((c) => c.getSettings(TypeMoq.It.isAny())).returns(() => settings.object); @@ -362,13 +343,13 @@ suite('Module Installer', () => { await expect(condaInstaller.isSupported()).to.eventually.equal(true, 'Conda is not supported'); }); test('Ensure conda is not supported even if conda is available', async () => { - const serviceContainer = TypeMoq.Mock.ofType(); + const serviceContainer = createTypeMoq(); - const configService = TypeMoq.Mock.ofType(); + const configService = createTypeMoq(); serviceContainer .setup((c) => c.get(TypeMoq.It.isValue(IConfigurationService))) .returns(() => configService.object); - const settings = TypeMoq.Mock.ofType(); + const settings = createTypeMoq(); const pythonPath = 'pythonABC'; settings.setup((s) => s.pythonPath).returns(() => pythonPath); configService.setup((c) => c.getSettings(TypeMoq.It.isAny())).returns(() => settings.object); diff --git a/src/test/common/persistentState.unit.test.ts b/src/test/common/persistentState.unit.test.ts index 9af28e2f5860..a77ee571559e 100644 --- a/src/test/common/persistentState.unit.test.ts +++ b/src/test/common/persistentState.unit.test.ts @@ -5,6 +5,7 @@ import { assert, expect } from 'chai'; import * as TypeMoq from 'typemoq'; +import * as sinon from 'sinon'; import { Memento } from 'vscode'; import { ICommandManager } from '../../client/common/application/types'; import { Commands } from '../../client/common/constants'; @@ -17,17 +18,25 @@ import { import { IDisposable } from '../../client/common/types'; import { sleep } from '../core'; import { MockMemento } from '../mocks/mementos'; +import * as apiInt from '../../client/envExt/api.internal'; suite('Persistent State', () => { let cmdManager: TypeMoq.IMock; let persistentStateFactory: PersistentStateFactory; let workspaceMemento: Memento; let globalMemento: Memento; + let useEnvExtensionStub: sinon.SinonStub; setup(() => { cmdManager = TypeMoq.Mock.ofType(); workspaceMemento = new MockMemento(); globalMemento = new MockMemento(); persistentStateFactory = new PersistentStateFactory(globalMemento, workspaceMemento, cmdManager.object); + + useEnvExtensionStub = sinon.stub(apiInt, 'useEnvExtension'); + useEnvExtensionStub.returns(false); + }); + teardown(() => { + sinon.restore(); }); test('Global states created are restored on invoking clean storage command', async () => { diff --git a/src/test/common/platform/filesystem.functional.test.ts b/src/test/common/platform/filesystem.functional.test.ts index 542af602f583..be9a369935f3 100644 --- a/src/test/common/platform/filesystem.functional.test.ts +++ b/src/test/common/platform/filesystem.functional.test.ts @@ -2,9 +2,8 @@ // Licensed under the MIT License. import { expect, use } from 'chai'; -import * as fs from 'fs-extra'; import { convertStat, FileSystem, FileSystemUtils, RawFileSystem } from '../../../client/common/platform/fileSystem'; -import { FileSystemPaths, FileSystemPathUtils } from '../../../client/common/platform/fs-paths'; +import * as fs from '../../../client/common/platform/fs-paths'; import { FileType } from '../../../client/common/platform/types'; import { createDeferred, sleep } from '../../../client/common/utils/async'; import { noop } from '../../../client/common/utils/misc'; @@ -137,7 +136,7 @@ suite('FileSystem - raw', () => { await fileSystem.appendText(filename, dataToAppend); - const actual = await fs.readFile(filename, 'utf8'); + const actual = await fs.readFile(filename, { encoding: 'utf8' }); expect(actual).to.be.equal(expected); }); @@ -148,14 +147,14 @@ suite('FileSystem - raw', () => { await fileSystem.appendText(filename, dataToAppend); - const actual = await fs.readFile(filename, 'utf8'); + const actual = await fs.readFile(filename, { encoding: 'utf8' }); expect(actual).to.be.equal(expected); }); test('creates the file if it does not already exist', async () => { await fileSystem.appendText(DOES_NOT_EXIST, 'spam'); - const actual = await fs.readFile(DOES_NOT_EXIST, 'utf8'); + const actual = await fs.readFile(DOES_NOT_EXIST, { encoding: 'utf8' }); expect(actual).to.be.equal('spam'); }); @@ -497,8 +496,8 @@ suite('FileSystem', () => { }); suite('path-related', () => { - const paths = FileSystemPaths.withDefaults(); - const pathUtils = FileSystemPathUtils.withDefaults(paths); + const paths = fs.FileSystemPaths.withDefaults(); + const pathUtils = fs.FileSystemPathUtils.withDefaults(paths); suite('directorySeparatorChar', () => { // tested fully in the FileSystemPaths tests. @@ -536,7 +535,7 @@ suite('FileSystem', () => { await fileSystem.appendFile(filename, dataToAppend); - const actual = await fs.readFile(filename, 'utf8'); + const actual = await fs.readFile(filename, { encoding: 'utf8' }); expect(actual).to.be.equal(expected); }); }); diff --git a/src/test/common/platform/filesystem.test.ts b/src/test/common/platform/filesystem.test.ts index a95b96af8d14..a1afab02d1fe 100644 --- a/src/test/common/platform/filesystem.test.ts +++ b/src/test/common/platform/filesystem.test.ts @@ -2,7 +2,7 @@ // Licensed under the MIT License. import { expect } from 'chai'; -import * as fsextra from 'fs-extra'; +import * as fsextra from '../../../client/common/platform/fs-paths'; import * as path from 'path'; import { convertStat, FileSystem, FileSystemUtils, RawFileSystem } from '../../../client/common/platform/fileSystem'; import { FileType, IFileSystem, IFileSystemUtils, IRawFileSystem } from '../../../client/common/platform/types'; diff --git a/src/test/common/platform/filesystem.unit.test.ts b/src/test/common/platform/filesystem.unit.test.ts index 8c54b0c08ab7..f012cb9fb27e 100644 --- a/src/test/common/platform/filesystem.unit.test.ts +++ b/src/test/common/platform/filesystem.unit.test.ts @@ -3,7 +3,7 @@ import { expect } from 'chai'; import * as fs from 'fs'; -import * as fsextra from 'fs-extra'; +import * as fsextra from '../../../client/common/platform/fs-paths'; import * as TypeMoq from 'typemoq'; import * as vscode from 'vscode'; import { FileSystemUtils, RawFileSystem } from '../../../client/common/platform/fileSystem'; diff --git a/src/test/common/platform/fs-temp.functional.test.ts b/src/test/common/platform/fs-temp.functional.test.ts index 256d52a81cf0..67bca3338e76 100644 --- a/src/test/common/platform/fs-temp.functional.test.ts +++ b/src/test/common/platform/fs-temp.functional.test.ts @@ -2,10 +2,10 @@ // Licensed under the MIT License. import { expect, use } from 'chai'; -import * as fs from 'fs-extra'; +import * as fs from '../../../client/common/platform/fs-paths'; import { TemporaryFileSystem } from '../../../client/common/platform/fs-temp'; import { TemporaryFile } from '../../../client/common/platform/types'; -import { assertDoesNotExist, assertExists, FSFixture, WINDOWS } from './utils'; +import { assertDoesNotExist, assertExists, FSFixture } from './utils'; const assertArrays = require('chai-arrays'); use(require('chai-as-promised')); @@ -56,21 +56,6 @@ suite('FileSystem - TemporaryFileSystem', () => { expect(filename1).to.not.equal(filename2); }); - test('Ensure writing to a temp file is supported via file stream', async function () { - if (WINDOWS) { - this.skip(); - } - const tempfile = await createFile('.tmp'); - const stream = fs.createWriteStream(tempfile.filePath); - fix.addCleanup(() => stream.destroy()); - const data = '...'; - - stream.write(data, 'utf8'); - - const actual = await fs.readFile(tempfile.filePath, 'utf8'); - expect(actual).to.equal(data); - }); - test('Ensure chmod works against a temporary file', async () => { // Note that on Windows chmod is a noop. const tempfile = await createFile('.tmp'); diff --git a/src/test/common/platform/fs-temp.unit.test.ts b/src/test/common/platform/fs-temp.unit.test.ts index bfc8284b33d6..29b4e5f42b12 100644 --- a/src/test/common/platform/fs-temp.unit.test.ts +++ b/src/test/common/platform/fs-temp.unit.test.ts @@ -7,11 +7,14 @@ import { TemporaryFileSystem } from '../../../client/common/platform/fs-temp'; interface IDeps { // tmp module - file( - config: { postfix?: string; mode?: number }, - - callback?: (err: any, path: string, fd: number, cleanupCallback: () => void) => void, - ): void; + fileSync(config: { + postfix?: string; + mode?: number; + }): { + name: string; + fd: number; + removeCallback(): void; + }; } suite('FileSystem - temp files', () => { @@ -28,7 +31,7 @@ suite('FileSystem - temp files', () => { suite('createFile', () => { test(`fails if the raw call fails`, async () => { const failure = new Error('oops'); - deps.setup((d) => d.file({ postfix: '.tmp', mode: undefined }, TypeMoq.It.isAny())) + deps.setup((d) => d.fileSync({ postfix: '.tmp', mode: undefined })) // fail with an arbitrary error .throws(failure); @@ -40,7 +43,7 @@ suite('FileSystem - temp files', () => { test(`fails if the raw call "returns" an error`, async () => { const failure = new Error('oops'); - deps.setup((d) => d.file({ postfix: '.tmp', mode: undefined }, TypeMoq.It.isAny())).callback((_cfg, cb) => + deps.setup((d) => d.fileSync({ postfix: '.tmp', mode: undefined })).callback((_cfg, cb) => cb(failure, '...', -1, () => {}), ); diff --git a/src/test/common/platform/platformService.functional.test.ts b/src/test/common/platform/platformService.functional.test.ts index 3c2042807ab8..9f16a6ebf386 100644 --- a/src/test/common/platform/platformService.functional.test.ts +++ b/src/test/common/platform/platformService.functional.test.ts @@ -10,7 +10,7 @@ import { parse } from 'semver'; import { PlatformService } from '../../../client/common/platform/platformService'; import { OSType } from '../../../client/common/utils/platform'; -use(chaiAsPromised); +use(chaiAsPromised.default); suite('PlatformService', () => { const osType = getOSType(); diff --git a/src/test/common/platform/utils.ts b/src/test/common/platform/utils.ts index cc30ad84b8b9..881e3cd019b9 100644 --- a/src/test/common/platform/utils.ts +++ b/src/test/common/platform/utils.ts @@ -2,7 +2,7 @@ // Licensed under the MIT License. import { expect } from 'chai'; -import * as fsextra from 'fs-extra'; +import * as fsextra from '../../../client/common/platform/fs-paths'; import * as net from 'net'; import * as path from 'path'; import * as tmpMod from 'tmp'; diff --git a/src/test/common/process/logger.unit.test.ts b/src/test/common/process/logger.unit.test.ts index ebce120b7e6c..366a7056e89e 100644 --- a/src/test/common/process/logger.unit.test.ts +++ b/src/test/common/process/logger.unit.test.ts @@ -7,19 +7,19 @@ import * as path from 'path'; import * as sinon from 'sinon'; import * as TypeMoq from 'typemoq'; -import untildify = require('untildify'); import { WorkspaceFolder } from 'vscode'; import { IWorkspaceService } from '../../../client/common/application/types'; import { ProcessLogger } from '../../../client/common/process/logger'; import { getOSType, OSType } from '../../../client/common/utils/platform'; import * as logging from '../../../client/logging'; +import { untildify } from '../../../client/common/helpers'; suite('ProcessLogger suite', () => { let workspaceService: TypeMoq.IMock; let logger: ProcessLogger; let traceLogStub: sinon.SinonStub; - suiteSetup(() => { + suiteSetup(async () => { workspaceService = TypeMoq.Mock.ofType(); workspaceService .setup((w) => w.workspaceFolders) @@ -109,32 +109,55 @@ suite('ProcessLogger suite', () => { test('Logger replaces the path/to/home with ~ in the command path where the home path IS NOT at the beginning of the path', async () => { const options = { cwd: path.join('debug', 'path') }; - logger.logProcess(path.join('net', untildify('~'), 'test'), ['--foo', '--bar'], options); + const untildifyStr = untildify('~'); + + let p1 = path.join('net', untildifyStr, 'test'); + if (p1.startsWith('.')) { + if (getOSType() === OSType.Windows) { + p1 = p1.replace(/^\.\\+/, ''); + } else { + p1 = p1.replace(/^\.\\/, ''); + } + } + logger.logProcess(p1, ['--foo', '--bar'], options); - sinon.assert.calledWithExactly(traceLogStub, `> ${path.join('net', '~', 'test')} --foo --bar`); + const path1 = path.join('.', 'net', '~', 'test'); + sinon.assert.calledWithExactly(traceLogStub, `> ${path1} --foo --bar`); sinon.assert.calledWithExactly(traceLogStub, `cwd: ${options.cwd}`); }); test('Logger replaces the path/to/home with ~ in the command path where the home path IS NOT at the beginning of the path but another arg contains other ref to home folder', async () => { const options = { cwd: path.join('debug', 'path') }; - logger.logProcess( - path.join('net', untildify('~'), 'test'), - ['--foo', path.join(untildify('~'), 'boo')], - options, - ); + let p1 = path.join('net', untildify('~'), 'test'); + if (p1.startsWith('.')) { + if (getOSType() === OSType.Windows) { + p1 = p1.replace(/^\.\\+/, ''); + } else { + p1 = p1.replace(/^\.\\/, ''); + } + } + logger.logProcess(p1, ['--foo', path.join(untildify('~'), 'boo')], options); sinon.assert.calledWithExactly( traceLogStub, - `> ${path.join('net', '~', 'test')} --foo ${path.join('~', 'boo')}`, + `> ${path.join('.', 'net', '~', 'test')} --foo ${path.join('~', 'boo')}`, ); sinon.assert.calledWithExactly(traceLogStub, `cwd: ${options.cwd}`); }); test('Logger replaces the path/to/home with ~ in the command path where the home path IS NOT at the beginning of the path between doble quotes', async () => { const options = { cwd: path.join('debug', 'path') }; - logger.logProcess(`"${path.join('net', untildify('~'), 'test')}" "--foo" "--bar"`, undefined, options); + let p1 = path.join('net', untildify('~'), 'test'); + if (p1.startsWith('.')) { + if (getOSType() === OSType.Windows) { + p1 = p1.replace(/^\.\\+/, ''); + } else { + p1 = p1.replace(/^\.\\/, ''); + } + } + logger.logProcess(`"${p1}" "--foo" "--bar"`, undefined, options); - sinon.assert.calledWithExactly(traceLogStub, `> "${path.join('net', '~', 'test')}" "--foo" "--bar"`); + sinon.assert.calledWithExactly(traceLogStub, `> "${path.join('.', 'net', '~', 'test')}" "--foo" "--bar"`); sinon.assert.calledWithExactly(traceLogStub, `cwd: ${options.cwd}`); }); diff --git a/src/test/common/process/proc.exec.test.ts b/src/test/common/process/proc.exec.test.ts index c193df95d080..21351d811b63 100644 --- a/src/test/common/process/proc.exec.test.ts +++ b/src/test/common/process/proc.exec.test.ts @@ -13,7 +13,7 @@ import { isOs, isPythonVersion } from '../../common'; import { getExtensionSettings } from '../../extensionSettings'; import { initialize } from './../../initialize'; -use(chaiAsPromised); +use(chaiAsPromised.default); suite('ProcessService Observable', () => { let pythonPath: string; @@ -34,6 +34,16 @@ suite('ProcessService Observable', () => { expect(result.stderr).to.equal(undefined, 'stderr not undefined'); }); + test('When using worker threads, exec should output print statements', async () => { + const procService = new ProcessService(); + const printOutput = '1234'; + const result = await procService.exec(pythonPath, ['-c', `print("${printOutput}")`], { useWorker: true }); + + expect(result).not.to.be.an('undefined', 'result is undefined'); + expect(result.stdout.trim()).to.be.equal(printOutput, 'Invalid output'); + expect(result.stderr).to.equal(undefined, 'stderr not undefined'); + }); + test('exec should output print unicode characters', async function () { // This test has not been working for many months in Python 2.7 under // Windows. Tracked by #2546. (unicode under Py2.7 is tough!) @@ -241,6 +251,18 @@ suite('ProcessService Observable', () => { expect(result.stderr).to.equal(undefined, 'stderr not empty'); expect(result.stdout.trim()).to.be.equal(printOutput, 'Invalid output'); }); + test('When using worker threads, shellExec should be able to run python and filter output using conda related markers', async () => { + const procService = new ProcessService(); + const printOutput = '1234'; + const result = await procService.shellExec( + `"${pythonPath}" -c "print('>>>PYTHON-EXEC-OUTPUT');print('${printOutput}');print('<< { const procService = new ProcessService(); const result = procService.shellExec('invalid command'); diff --git a/src/test/common/process/proc.observable.test.ts b/src/test/common/process/proc.observable.test.ts index 74a613f0ec1d..debae38cc6eb 100644 --- a/src/test/common/process/proc.observable.test.ts +++ b/src/test/common/process/proc.observable.test.ts @@ -10,7 +10,7 @@ import { isOs, OSType } from '../../common'; import { getExtensionSettings } from '../../extensionSettings'; import { initialize } from './../../initialize'; -use(chaiAsPromised); +use(chaiAsPromised.default); suite('ProcessService', () => { let pythonPath: string; diff --git a/src/test/common/process/pythonEnvironment.unit.test.ts b/src/test/common/process/pythonEnvironment.unit.test.ts index 49faa91e2aaf..a2cca66d08be 100644 --- a/src/test/common/process/pythonEnvironment.unit.test.ts +++ b/src/test/common/process/pythonEnvironment.unit.test.ts @@ -17,7 +17,7 @@ import { Architecture } from '../../../client/common/utils/platform'; import { Conda } from '../../../client/pythonEnvironments/common/environmentManagers/conda'; import { OUTPUT_MARKER_SCRIPT } from '../../../client/common/process/internal/scripts'; -use(chaiAsPromised); +use(chaiAsPromised.default); suite('PythonEnvironment', () => { let processService: TypeMoq.IMock; @@ -284,7 +284,7 @@ suite('CondaEnvironment', () => { teardown(() => sinon.restore()); - test('getExecutionInfo with a named environment should return execution info using the environment name', async () => { + test('getExecutionInfo with a named environment should return execution info using the environment path', async () => { const condaInfo = { name: 'foo', path: 'bar' }; const env = await createCondaEnv(condaInfo, processService.object, fileSystem.object); @@ -292,8 +292,8 @@ suite('CondaEnvironment', () => { expect(result).to.deep.equal({ command: condaFile, - args: ['run', '-n', condaInfo.name, '--no-capture-output', 'python', OUTPUT_MARKER_SCRIPT, ...args], - python: [condaFile, 'run', '-n', condaInfo.name, '--no-capture-output', 'python', OUTPUT_MARKER_SCRIPT], + args: ['run', '-p', condaInfo.path, '--no-capture-output', 'python', OUTPUT_MARKER_SCRIPT, ...args], + python: [condaFile, 'run', '-p', condaInfo.path, '--no-capture-output', 'python', OUTPUT_MARKER_SCRIPT], pythonExecutable: pythonPath, }); }); @@ -312,12 +312,12 @@ suite('CondaEnvironment', () => { }); }); - test('getExecutionObservableInfo with a named environment should return execution info using conda full path with the name', async () => { + test('getExecutionObservableInfo with a named environment should return execution info using conda full path with the path', async () => { const condaInfo = { name: 'foo', path: 'bar' }; const expected = { command: condaFile, - args: ['run', '-n', condaInfo.name, '--no-capture-output', 'python', OUTPUT_MARKER_SCRIPT, ...args], - python: [condaFile, 'run', '-n', condaInfo.name, '--no-capture-output', 'python', OUTPUT_MARKER_SCRIPT], + args: ['run', '-p', condaInfo.path, '--no-capture-output', 'python', OUTPUT_MARKER_SCRIPT, ...args], + python: [condaFile, 'run', '-p', condaInfo.path, '--no-capture-output', 'python', OUTPUT_MARKER_SCRIPT], pythonExecutable: pythonPath, }; const env = await createCondaEnv(condaInfo, processService.object, fileSystem.object); diff --git a/src/test/common/process/pythonExecutionFactory.unit.test.ts b/src/test/common/process/pythonExecutionFactory.unit.test.ts index e31a9e4d900e..0981c59e78bb 100644 --- a/src/test/common/process/pythonExecutionFactory.unit.test.ts +++ b/src/test/common/process/pythonExecutionFactory.unit.test.ts @@ -36,6 +36,7 @@ import { ServiceContainer } from '../../../client/ioc/container'; import { EnvironmentType, PythonEnvironment } from '../../../client/pythonEnvironments/info'; import { IInterpreterAutoSelectionService } from '../../../client/interpreter/autoSelection/types'; import { Conda, CONDA_RUN_VERSION } from '../../../client/pythonEnvironments/common/environmentManagers/conda'; +import * as pixi from '../../../client/pythonEnvironments/common/environmentManagers/pixi'; const pythonInterpreter: PythonEnvironment = { path: '/foo/bar/python.exe', @@ -87,10 +88,19 @@ suite('Process - PythonExecutionFactory', () => { let executionService: typemoq.IMock; let autoSelection: IInterpreterAutoSelectionService; let interpreterPathExpHelper: IInterpreterPathService; + let getPixiEnvironmentFromInterpreterStub: sinon.SinonStub; + let getPixiStub: sinon.SinonStub; const pythonPath = 'path/to/python'; setup(() => { sinon.stub(Conda, 'getConda').resolves(new Conda('conda')); sinon.stub(Conda.prototype, 'getInterpreterPathForEnvironment').resolves(pythonPath); + + getPixiEnvironmentFromInterpreterStub = sinon.stub(pixi, 'getPixiEnvironmentFromInterpreter'); + getPixiEnvironmentFromInterpreterStub.resolves(undefined); + + getPixiStub = sinon.stub(pixi, 'getPixi'); + getPixiStub.resolves(undefined); + activationHelper = mock(EnvironmentActivationService); processFactory = mock(ProcessServiceFactory); configService = mock(ConfigurationService); @@ -136,6 +146,9 @@ suite('Process - PythonExecutionFactory', () => { when(serviceContainer.tryGet(IInterpreterService)).thenReturn( instance(interpreterService), ); + when(serviceContainer.get(IConfigurationService)).thenReturn( + instance(configService), + ); factory = new PythonExecutionFactory( instance(serviceContainer), instance(activationHelper), @@ -336,6 +349,7 @@ suite('Process - PythonExecutionFactory', () => { } else { verify(pyenvs.getCondaEnvironment(interpreter!.path)).once(); } + expect(getPixiEnvironmentFromInterpreterStub.notCalled).to.be.equal(true); }); test('Ensure `createActivatedEnvironment` returns a PythonExecutionService instance if createCondaExecutionService() returns undefined', async () => { diff --git a/src/test/common/process/pythonProc.simple.multiroot.test.ts b/src/test/common/process/pythonProc.simple.multiroot.test.ts index 5089af8b5eb3..fc4fbf5328a9 100644 --- a/src/test/common/process/pythonProc.simple.multiroot.test.ts +++ b/src/test/common/process/pythonProc.simple.multiroot.test.ts @@ -6,9 +6,9 @@ import { expect, use } from 'chai'; import * as chaiAsPromised from 'chai-as-promised'; import { execFile } from 'child_process'; -import * as fs from 'fs-extra'; import * as path from 'path'; import { ConfigurationTarget, Uri } from 'vscode'; +import * as fs from '../../../client/common/platform/fs-paths'; import { IPythonExecutionFactory, StdErrError } from '../../../client/common/process/types'; import { IConfigurationService } from '../../../client/common/types'; import { clearCache } from '../../../client/common/utils/cacheUtils'; @@ -18,7 +18,7 @@ import { clearPythonPathInWorkspaceFolder } from '../../common'; import { getExtensionSettings } from '../../extensionSettings'; import { closeActiveWindows, initialize, initializeTest, IS_MULTI_ROOT_TEST, TEST_TIMEOUT } from '../../initialize'; -use(chaiAsPromised); +use(chaiAsPromised.default); const multirootPath = path.join(__dirname, '..', '..', '..', '..', 'src', 'testMultiRootWkspc'); const workspace4Path = Uri.file(path.join(multirootPath, 'workspace4')); diff --git a/src/test/common/process/pythonProcess.unit.test.ts b/src/test/common/process/pythonProcess.unit.test.ts index d799e08b08b5..7382fc9f9869 100644 --- a/src/test/common/process/pythonProcess.unit.test.ts +++ b/src/test/common/process/pythonProcess.unit.test.ts @@ -10,7 +10,7 @@ import { createPythonProcessService } from '../../../client/common/process/pytho import { IProcessService, StdErrError } from '../../../client/common/process/types'; import { noop } from '../../core'; -use(chaiAsPromised); +use(chaiAsPromised.default); suite('PythonProcessService', () => { let processService: TypeMoq.IMock; diff --git a/src/test/common/process/pythonToolService.unit.test.ts b/src/test/common/process/pythonToolService.unit.test.ts index 59733f8a5e8d..bef199ce223a 100644 --- a/src/test/common/process/pythonToolService.unit.test.ts +++ b/src/test/common/process/pythonToolService.unit.test.ts @@ -24,7 +24,7 @@ import { ExecutionInfo } from '../../../client/common/types'; import { ServiceContainer } from '../../../client/ioc/container'; import { noop } from '../../core'; -use(chaiAsPromised); +use(chaiAsPromised.default); suite('Process - Python tool execution service', () => { const resource = Uri.parse('one'); diff --git a/src/test/common/serviceRegistry.unit.test.ts b/src/test/common/serviceRegistry.unit.test.ts index 2964455ada37..9a82681625d4 100644 --- a/src/test/common/serviceRegistry.unit.test.ts +++ b/src/test/common/serviceRegistry.unit.test.ts @@ -28,7 +28,6 @@ import { import { WorkspaceService } from '../../client/common/application/workspace'; import { ConfigurationService } from '../../client/common/configuration/service'; import { PipEnvExecutionPath } from '../../client/common/configuration/executionSettings/pipEnvExecution'; -import { EditorUtils } from '../../client/common/editor'; import { ProductInstaller } from '../../client/common/installer/productInstaller'; import { InterpreterPathService } from '../../client/common/interpreterPathService'; import { BrowserService } from '../../client/common/net/browser'; @@ -63,7 +62,6 @@ import { IBrowserService, IConfigurationService, ICurrentProcess, - IEditorUtils, IExtensions, IInstaller, IInterpreterPathService, @@ -103,7 +101,6 @@ suite('Common - Service Registry', () => { [IApplicationEnvironment, ApplicationEnvironment], [ILanguageService, LanguageService], [IBrowserService, BrowserService], - [IEditorUtils, EditorUtils], [ITerminalActivator, TerminalActivator], [ITerminalActivationHandler, PowershellTerminalActivationFailedHandler], [ITerminalHelper, TerminalHelper], @@ -130,7 +127,7 @@ suite('Common - Service Registry', () => { .setup((s) => s.addSingleton( typemoq.It.isValue(mapping[0] as any), - typemoq.It.is((value) => mapping[1] === value), + typemoq.It.is((value: any) => mapping[1] === value), ), ) .verifiable(typemoq.Times.atLeastOnce()); diff --git a/src/test/common/socketCallbackHandler.test.ts b/src/test/common/socketCallbackHandler.test.ts index 4f4587077f79..5fbac0083125 100644 --- a/src/test/common/socketCallbackHandler.test.ts +++ b/src/test/common/socketCallbackHandler.test.ts @@ -189,7 +189,7 @@ suite('SocketCallbackHandler', () => { expect(port).to.be.greaterThan(0); }); test('Succesfully starts with specific port', async () => { - const availablePort = await getFreePort({ host: 'localhost' }); + const availablePort = await getFreePort.default({ host: 'localhost' }); const port = await socketServer.Start({ port: availablePort, host: 'localhost' }); expect(port).to.be.equal(availablePort); }); @@ -311,7 +311,7 @@ suite('SocketCallbackHandler', () => { }); test('Succesful Handshake with specific port', async () => { const availablePort = await new Promise((resolve, reject) => - getFreePort({ host: 'localhost' }).then(resolve, reject), + getFreePort.default({ host: 'localhost' }).then(resolve, reject), ); const port = await socketServer.Start({ port: availablePort, host: 'localhost' }); diff --git a/src/test/common/terminals/activation.conda.unit.test.ts b/src/test/common/terminals/activation.conda.unit.test.ts index 84e4bffacfc1..39bf58a9a36b 100644 --- a/src/test/common/terminals/activation.conda.unit.test.ts +++ b/src/test/common/terminals/activation.conda.unit.test.ts @@ -31,6 +31,7 @@ import { getNamesAndValues } from '../../../client/common/utils/enum'; import { IComponentAdapter, ICondaService } from '../../../client/interpreter/contracts'; import { InterpreterService } from '../../../client/interpreter/interpreterService'; import { IServiceContainer } from '../../../client/ioc/types'; +import { PixiActivationCommandProvider } from '../../../client/common/terminal/environmentActivationProviders/pixiActivationProvider'; suite('Terminal Environment Activation conda', () => { let terminalHelper: TerminalHelper; @@ -114,6 +115,7 @@ suite('Terminal Environment Activation conda', () => { mock(Nushell), mock(PyEnvActivationCommandProvider), mock(PipEnvActivationCommandProvider), + mock(PixiActivationCommandProvider), [], ); }); diff --git a/src/test/common/terminals/activation.unit.test.ts b/src/test/common/terminals/activation.unit.test.ts index 49ada1c06b11..d87d33ea03e6 100644 --- a/src/test/common/terminals/activation.unit.test.ts +++ b/src/test/common/terminals/activation.unit.test.ts @@ -3,6 +3,7 @@ 'use strict'; import { expect } from 'chai'; +import * as sinon from 'sinon'; import { anything, instance, mock, verify, when } from 'ts-mockito'; import * as TypeMoq from 'typemoq'; import { Terminal, Uri } from 'vscode'; @@ -15,6 +16,7 @@ import { IDisposable } from '../../../client/common/types'; import { TerminalAutoActivation } from '../../../client/terminals/activation'; import { ITerminalAutoActivation } from '../../../client/terminals/types'; import { noop } from '../../core'; +import * as extapi from '../../../client/envExt/api.internal'; suite('Terminal Auto Activation', () => { let activator: ITerminalActivator; @@ -25,6 +27,7 @@ suite('Terminal Auto Activation', () => { let terminal: Terminal; setup(() => { + sinon.stub(extapi, 'shouldEnvExtHandleActivation').returns(false); terminal = ({ dispose: noop, hide: noop, @@ -46,6 +49,9 @@ suite('Terminal Auto Activation', () => { instance(activeResourceService), ); }); + teardown(() => { + sinon.restore(); + }); test('New Terminals should be activated', async () => { type EventHandler = (e: Terminal) => void; diff --git a/src/test/common/terminals/activator/index.unit.test.ts b/src/test/common/terminals/activator/index.unit.test.ts index a50b946c391f..34d1cf8f1bcd 100644 --- a/src/test/common/terminals/activator/index.unit.test.ts +++ b/src/test/common/terminals/activator/index.unit.test.ts @@ -4,8 +4,9 @@ 'use strict'; import { assert } from 'chai'; +import * as sinon from 'sinon'; import * as TypeMoq from 'typemoq'; -import { Terminal } from 'vscode'; +import { Terminal, Uri } from 'vscode'; import { TerminalActivator } from '../../../../client/common/terminal/activator'; import { ITerminalActivationHandler, @@ -18,6 +19,9 @@ import { IPythonSettings, ITerminalSettings, } from '../../../../client/common/types'; +import * as extapi from '../../../../client/envExt/api.internal'; +import * as workspaceApis from '../../../../client/common/vscodeApis/workspaceApis'; +import * as extensionsApi from '../../../../client/common/vscodeApis/extensionsApi'; suite('Terminal Activator', () => { let activator: TerminalActivator; @@ -26,7 +30,14 @@ suite('Terminal Activator', () => { let handler2: TypeMoq.IMock; let terminalSettings: TypeMoq.IMock; let experimentService: TypeMoq.IMock; + let useEnvExtensionStub: sinon.SinonStub; + let shouldEnvExtHandleActivationStub: sinon.SinonStub; setup(() => { + useEnvExtensionStub = sinon.stub(extapi, 'useEnvExtension'); + useEnvExtensionStub.returns(false); + shouldEnvExtHandleActivationStub = sinon.stub(extapi, 'shouldEnvExtHandleActivation'); + shouldEnvExtHandleActivationStub.returns(false); + baseActivator = TypeMoq.Mock.ofType(); terminalSettings = TypeMoq.Mock.ofType(); experimentService = TypeMoq.Mock.ofType(); @@ -52,6 +63,10 @@ suite('Terminal Activator', () => { experimentService.object, ); }); + teardown(() => { + sinon.restore(); + }); + async function testActivationAndHandlers( activationSuccessful: boolean, activateEnvironmentSetting: boolean, @@ -103,4 +118,92 @@ suite('Terminal Activator', () => { test('Terminal is not activated if auto-activate setting is set to true but terminal is hidden', () => testActivationAndHandlers(false, true, true)); test('Terminal is not activated and handlers are invoked', () => testActivationAndHandlers(false, false)); + + test('Terminal is not activated from Python extension when Env extension should handle activation', async () => { + shouldEnvExtHandleActivationStub.returns(true); + terminalSettings.setup((b) => b.activateEnvironment).returns(() => true); + baseActivator + .setup((b) => b.activateEnvironmentInTerminal(TypeMoq.It.isAny(), TypeMoq.It.isAny())) + .returns(() => Promise.resolve(true)) + .verifiable(TypeMoq.Times.never()); + + const terminal = TypeMoq.Mock.ofType(); + const activated = await activator.activateEnvironmentInTerminal(terminal.object, { + preserveFocus: true, + }); + + assert.strictEqual(activated, false); + baseActivator.verifyAll(); + }); +}); + +suite('shouldEnvExtHandleActivation', () => { + let getExtensionStub: sinon.SinonStub; + let getConfigurationStub: sinon.SinonStub; + let getWorkspaceFoldersStub: sinon.SinonStub; + + setup(() => { + getExtensionStub = sinon.stub(extensionsApi, 'getExtension'); + getConfigurationStub = sinon.stub(workspaceApis, 'getConfiguration'); + getWorkspaceFoldersStub = sinon.stub(workspaceApis, 'getWorkspaceFolders'); + getWorkspaceFoldersStub.returns(undefined); + }); + + teardown(() => { + sinon.restore(); + }); + + test('Returns false when envs extension is not installed', () => { + getExtensionStub.returns(undefined); + assert.strictEqual(extapi.shouldEnvExtHandleActivation(), false); + }); + + test('Returns true when envs extension is installed and setting is not explicitly set', () => { + getExtensionStub.returns({ id: extapi.ENVS_EXTENSION_ID }); + getConfigurationStub.returns({ + inspect: () => ({ globalValue: undefined, workspaceValue: undefined }), + }); + assert.strictEqual(extapi.shouldEnvExtHandleActivation(), true); + }); + + test('Returns false when envs extension is installed but globalValue is false', () => { + getExtensionStub.returns({ id: extapi.ENVS_EXTENSION_ID }); + getConfigurationStub.returns({ + inspect: () => ({ globalValue: false, workspaceValue: undefined }), + }); + assert.strictEqual(extapi.shouldEnvExtHandleActivation(), false); + }); + + test('Returns false when envs extension is installed but workspaceValue is false', () => { + getExtensionStub.returns({ id: extapi.ENVS_EXTENSION_ID }); + getConfigurationStub.returns({ + inspect: () => ({ globalValue: undefined, workspaceValue: false }), + }); + assert.strictEqual(extapi.shouldEnvExtHandleActivation(), false); + }); + + test('Returns true when envs extension is installed and setting is explicitly true', () => { + getExtensionStub.returns({ id: extapi.ENVS_EXTENSION_ID }); + getConfigurationStub.returns({ + inspect: () => ({ globalValue: true, workspaceValue: undefined }), + }); + assert.strictEqual(extapi.shouldEnvExtHandleActivation(), true); + }); + + test('Returns false when a workspace folder has workspaceFolderValue set to false', () => { + getExtensionStub.returns({ id: extapi.ENVS_EXTENSION_ID }); + const folderUri = Uri.parse('file:///workspace/folder1'); + getWorkspaceFoldersStub.returns([{ uri: folderUri, name: 'folder1', index: 0 }]); + getConfigurationStub.callsFake((_section: string, scope?: Uri) => { + if (scope) { + return { + inspect: () => ({ workspaceFolderValue: false }), + }; + } + return { + inspect: () => ({ globalValue: undefined, workspaceValue: undefined }), + }; + }); + assert.strictEqual(extapi.shouldEnvExtHandleActivation(), false); + }); }); diff --git a/src/test/common/terminals/environmentActivationProviders/terminalActivation.testvirtualenvs.ts b/src/test/common/terminals/environmentActivationProviders/terminalActivation.testvirtualenvs.ts index cabf293ba958..5a5e65a9c0f2 100644 --- a/src/test/common/terminals/environmentActivationProviders/terminalActivation.testvirtualenvs.ts +++ b/src/test/common/terminals/environmentActivationProviders/terminalActivation.testvirtualenvs.ts @@ -4,7 +4,7 @@ 'use strict'; import { expect } from 'chai'; -import * as fs from 'fs-extra'; +import * as fs from '../../../../client/common/platform/fs-paths'; import * as path from 'path'; import * as sinon from 'sinon'; import * as vscode from 'vscode'; @@ -63,7 +63,8 @@ suite('Activation of Environments in Terminal', () => { await terminalSettings.update('integrated.defaultProfile.linux', 'bash', vscode.ConfigurationTarget.Global); }); - setup(async () => { + setup(async function () { + this.skip(); // https://github.com/microsoft/vscode-python/issues/22264 await initializeTest(); outputFile = path.join( EXTENSION_ROOT_DIR_FOR_TESTS, diff --git a/src/test/common/terminals/factory.unit.test.ts b/src/test/common/terminals/factory.unit.test.ts index f01d5a85fbb5..5ad2da8e793a 100644 --- a/src/test/common/terminals/factory.unit.test.ts +++ b/src/test/common/terminals/factory.unit.test.ts @@ -105,7 +105,7 @@ suite('Terminal Service Factory', () => { expect(notSameAsThirdInstance).to.not.equal(true, 'Instances are the same'); }); - test('Ensure different terminal is returned when using different resources from the same workspace', () => { + test('Ensure same terminal is returned when using different resources from the same workspace', () => { const file1A = Uri.file('1a'); const file2A = Uri.file('2a'); const fileB = Uri.file('b'); @@ -130,6 +130,51 @@ suite('Terminal Service Factory', () => { const terminalForFile2A = factory.getTerminalService({ resource: file2A }) as SynchronousTerminalService; const terminalForFileB = factory.getTerminalService({ resource: fileB }) as SynchronousTerminalService; + const terminalsAreSameForWorkspaceA = terminalForFile1A.terminalService === terminalForFile2A.terminalService; + expect(terminalsAreSameForWorkspaceA).to.equal(true, 'Instances are not the same for Workspace A'); + + const terminalsForWorkspaceABAreDifferent = + terminalForFile1A.terminalService === terminalForFileB.terminalService; + expect(terminalsForWorkspaceABAreDifferent).to.equal( + false, + 'Instances should be different for different workspaces', + ); + }); + + test('When `newTerminalPerFile` is true, ensure different terminal is returned when using different resources from the same workspace', () => { + const file1A = Uri.file('1a'); + const file2A = Uri.file('2a'); + const fileB = Uri.file('b'); + const workspaceUriA = Uri.file('A'); + const workspaceUriB = Uri.file('B'); + const workspaceFolderA = TypeMoq.Mock.ofType(); + workspaceFolderA.setup((w) => w.uri).returns(() => workspaceUriA); + const workspaceFolderB = TypeMoq.Mock.ofType(); + workspaceFolderB.setup((w) => w.uri).returns(() => workspaceUriB); + + workspaceService + .setup((w) => w.getWorkspaceFolder(TypeMoq.It.isValue(file1A))) + .returns(() => workspaceFolderA.object); + workspaceService + .setup((w) => w.getWorkspaceFolder(TypeMoq.It.isValue(file2A))) + .returns(() => workspaceFolderA.object); + workspaceService + .setup((w) => w.getWorkspaceFolder(TypeMoq.It.isValue(fileB))) + .returns(() => workspaceFolderB.object); + + const terminalForFile1A = factory.getTerminalService({ + resource: file1A, + newTerminalPerFile: true, + }) as SynchronousTerminalService; + const terminalForFile2A = factory.getTerminalService({ + resource: file2A, + newTerminalPerFile: true, + }) as SynchronousTerminalService; + const terminalForFileB = factory.getTerminalService({ + resource: fileB, + newTerminalPerFile: true, + }) as SynchronousTerminalService; + const terminalsAreSameForWorkspaceA = terminalForFile1A.terminalService === terminalForFile2A.terminalService; expect(terminalsAreSameForWorkspaceA).to.equal(false, 'Instances are the same for Workspace A'); diff --git a/src/test/common/terminals/helper.unit.test.ts b/src/test/common/terminals/helper.unit.test.ts index b6a8d44ac030..0d130b573408 100644 --- a/src/test/common/terminals/helper.unit.test.ts +++ b/src/test/common/terminals/helper.unit.test.ts @@ -32,6 +32,7 @@ import { IComponentAdapter } from '../../../client/interpreter/contracts'; import { InterpreterService } from '../../../client/interpreter/interpreterService'; import { IServiceContainer } from '../../../client/ioc/types'; import { EnvironmentType, PythonEnvironment } from '../../../client/pythonEnvironments/info'; +import { PixiActivationCommandProvider } from '../../../client/common/terminal/environmentActivationProviders/pixiActivationProvider'; suite('Terminal Service helpers', () => { let helper: TerminalHelper; @@ -46,6 +47,7 @@ suite('Terminal Service helpers', () => { let nushellActivationProvider: ITerminalActivationCommandProvider; let pyenvActivationProvider: ITerminalActivationCommandProvider; let pipenvActivationProvider: ITerminalActivationCommandProvider; + let pixiActivationProvider: ITerminalActivationCommandProvider; let pythonSettings: PythonSettings; let shellDetectorIdentifyTerminalShell: sinon.SinonStub<[(Terminal | undefined)?], TerminalShellType>; let mockDetector: IShellDetector; @@ -72,6 +74,7 @@ suite('Terminal Service helpers', () => { nushellActivationProvider = mock(Nushell); pyenvActivationProvider = mock(PyEnvActivationCommandProvider); pipenvActivationProvider = mock(PipEnvActivationCommandProvider); + pixiActivationProvider = mock(PixiActivationCommandProvider); pythonSettings = mock(PythonSettings); shellDetectorIdentifyTerminalShell = sinon.stub(ShellDetector.prototype, 'identifyTerminalShell'); helper = new TerminalHelper( @@ -86,12 +89,37 @@ suite('Terminal Service helpers', () => { instance(nushellActivationProvider), instance(pyenvActivationProvider), instance(pipenvActivationProvider), + instance(pixiActivationProvider), [instance(mockDetector)], ); } teardown(() => shellDetectorIdentifyTerminalShell.restore()); suite('Misc', () => { setup(doSetup); + test('Creating terminal should not automatically contain PYTHONSTARTUP', () => { + const theTitle = 'Hello'; + const terminal = 'Terminal Created'; + when(terminalManager.createTerminal(anything())).thenReturn(terminal as any); + const term = helper.createTerminal(theTitle); + const args = capture(terminalManager.createTerminal).first()[0]; + expect(term).to.be.deep.equal(terminal); + const terminalOptions = args.env; + const safeTerminalOptions = terminalOptions || {}; + expect(safeTerminalOptions).to.not.have.property('PYTHONSTARTUP'); + }); + + test('Env should be undefined if not explicitly passed in ', () => { + const theTitle = 'Hello'; + const terminal = 'Terminal Created'; + when(terminalManager.createTerminal(anything())).thenReturn(terminal as any); + + const term = helper.createTerminal(theTitle); + + verify(terminalManager.createTerminal(anything())).once(); + const args = capture(terminalManager.createTerminal).first()[0]; + expect(term).to.be.deep.equal(terminal); + expect(args.env).to.be.deep.equal(undefined); + }); test('Create terminal without a title', () => { const terminal = 'Terminal Created'; @@ -207,8 +235,8 @@ suite('Terminal Service helpers', () => { const cmd = await helper.getEnvironmentActivationCommands(anything(), resource); expect(cmd).to.equal(condaActivationCommands); - verify(pythonSettings.pythonPath).once(); - verify(condaService.isCondaEnvironment(pythonPath)).once(); + verify(pythonSettings.pythonPath).atLeast(1); + verify(condaService.isCondaEnvironment(pythonPath)).atLeast(1); verify(condaActivationProvider.getActivationCommands(resource, anything())).once(); }); test('Activation command must return undefined if none of the proivders support the shell', async () => { @@ -227,8 +255,8 @@ suite('Terminal Service helpers', () => { ); expect(cmd).to.equal(undefined, 'Command must be undefined'); - verify(pythonSettings.pythonPath).once(); - verify(condaService.isCondaEnvironment(pythonPath)).once(); + verify(pythonSettings.pythonPath).atLeast(1); + verify(condaService.isCondaEnvironment(pythonPath)).atLeast(1); verify(bashActivationProvider.isShellSupported(anything())).atLeast(1); verify(nushellActivationProvider.isShellSupported(anything())).atLeast(1); verify(pyenvActivationProvider.isShellSupported(anything())).atLeast(1); @@ -251,8 +279,8 @@ suite('Terminal Service helpers', () => { const cmd = await helper.getEnvironmentActivationCommands(anything(), resource); expect(cmd).to.deep.equal(expectCommand); - verify(pythonSettings.pythonPath).once(); - verify(condaService.isCondaEnvironment(pythonPath)).once(); + verify(pythonSettings.pythonPath).atLeast(1); + verify(condaService.isCondaEnvironment(pythonPath)).atLeast(1); verify(bashActivationProvider.isShellSupported(anything())).atLeast(1); verify(bashActivationProvider.getActivationCommands(resource, anything())).once(); verify(nushellActivationProvider.isShellSupported(anything())).atLeast(1); @@ -283,7 +311,7 @@ suite('Terminal Service helpers', () => { const cmd = await helper.getEnvironmentActivationCommands(anything(), resource); expect(cmd).to.deep.equal(expectCommand); - verify(pythonSettings.pythonPath).once(); + verify(pythonSettings.pythonPath).atLeast(1); verify(condaService.isCondaEnvironment(pythonPath)).once(); verify(bashActivationProvider.isShellSupported(anything())).atLeast(1); verify(bashActivationProvider.getActivationCommands(resource, anything())).never(); @@ -309,7 +337,7 @@ suite('Terminal Service helpers', () => { const cmd = await helper.getEnvironmentActivationCommands(anything(), resource); expect(cmd).to.deep.equal(expectCommand); - verify(pythonSettings.pythonPath).once(); + verify(pythonSettings.pythonPath).atLeast(1); verify(condaService.isCondaEnvironment(pythonPath)).once(); verify(bashActivationProvider.isShellSupported(anything())).atLeast(1); verify(nushellActivationProvider.isShellSupported(anything())).atLeast(1); @@ -336,7 +364,7 @@ suite('Terminal Service helpers', () => { const cmd = await helper.getEnvironmentActivationCommands(anything(), resource); expect(cmd).to.deep.equal(expectCommand); - verify(pythonSettings.pythonPath).once(); + verify(pythonSettings.pythonPath).atLeast(1); verify(condaService.isCondaEnvironment(pythonPath)).once(); verify(bashActivationProvider.getActivationCommands(resource, anything())).once(); verify(cmdActivationProvider.getActivationCommands(resource, anything())).once(); @@ -383,8 +411,13 @@ suite('Terminal Service helpers', () => { ); expect(cmd).to.equal(undefined, 'Command must be undefined'); - verify(pythonSettings.pythonPath).times(interpreter ? 0 : 1); - verify(condaService.isCondaEnvironment(pythonPath)).times(interpreter ? 0 : 1); + if (interpreter) { + verify(pythonSettings.pythonPath).never(); + verify(condaService.isCondaEnvironment(pythonPath)).never(); + } else { + verify(pythonSettings.pythonPath).atLeast(1); + verify(condaService.isCondaEnvironment(pythonPath)).atLeast(1); + } verify(bashActivationProvider.isShellSupported(shellToExpect)).atLeast(1); verify(pyenvActivationProvider.isShellSupported(anything())).never(); verify(pipenvActivationProvider.isShellSupported(anything())).never(); diff --git a/src/test/common/terminals/service.unit.test.ts b/src/test/common/terminals/service.unit.test.ts index 2f0d86f4000f..3a6d54c9390b 100644 --- a/src/test/common/terminals/service.unit.test.ts +++ b/src/test/common/terminals/service.unit.test.ts @@ -2,16 +2,39 @@ // Licensed under the MIT License. import { expect } from 'chai'; +import * as path from 'path'; +import * as sinon from 'sinon'; import * as TypeMoq from 'typemoq'; -import { Disposable, Terminal as VSCodeTerminal, WorkspaceConfiguration } from 'vscode'; -import { ITerminalManager, IWorkspaceService } from '../../../client/common/application/types'; +import { + Disposable, + EventEmitter, + TerminalShellExecution, + TerminalShellExecutionEndEvent, + TerminalShellIntegration, + Uri, + Terminal as VSCodeTerminal, + WorkspaceConfiguration, + TerminalDataWriteEvent, +} from 'vscode'; +import { IApplicationShell, ITerminalManager, IWorkspaceService } from '../../../client/common/application/types'; +import { EXTENSION_ROOT_DIR } from '../../../client/common/constants'; import { IPlatformService } from '../../../client/common/platform/types'; import { TerminalService } from '../../../client/common/terminal/service'; -import { ITerminalActivator, ITerminalHelper, TerminalShellType } from '../../../client/common/terminal/types'; +import { + ITerminalActivator, + ITerminalHelper, + TerminalCreationOptions, + TerminalShellType, +} from '../../../client/common/terminal/types'; import { IDisposableRegistry } from '../../../client/common/types'; import { IServiceContainer } from '../../../client/ioc/types'; import { ITerminalAutoActivation } from '../../../client/terminals/types'; import { createPythonInterpreter } from '../../utils/interpreters'; +import * as workspaceApis from '../../../client/common/vscodeApis/workspaceApis'; +import * as platform from '../../../client/common/utils/platform'; +import * as extapi from '../../../client/envExt/api.internal'; +import { IInterpreterService } from '../../../client/interpreter/contracts'; +import { PythonEnvironment } from '../../../client/pythonEnvironments/info'; suite('Terminal Service', () => { let service: TerminalService; @@ -24,9 +47,57 @@ suite('Terminal Service', () => { let disposables: Disposable[] = []; let mockServiceContainer: TypeMoq.IMock; let terminalAutoActivator: TypeMoq.IMock; + let terminalShellIntegration: TypeMoq.IMock; + let onDidEndTerminalShellExecutionEmitter: EventEmitter; + let event: TerminalShellExecutionEndEvent; + let getConfigurationStub: sinon.SinonStub; + let pythonConfig: TypeMoq.IMock; + let editorConfig: TypeMoq.IMock; + let isWindowsStub: sinon.SinonStub; + let useEnvExtensionStub: sinon.SinonStub; + let interpreterService: TypeMoq.IMock; + let options: TypeMoq.IMock; + let applicationShell: TypeMoq.IMock; + let onDidWriteTerminalDataEmitter: EventEmitter; + let onDidChangeTerminalStateEmitter: EventEmitter; + setup(() => { + useEnvExtensionStub = sinon.stub(extapi, 'useEnvExtension'); + useEnvExtensionStub.returns(false); + terminal = TypeMoq.Mock.ofType(); + terminalShellIntegration = TypeMoq.Mock.ofType(); + terminal.setup((t) => t.shellIntegration).returns(() => terminalShellIntegration.object); + + onDidEndTerminalShellExecutionEmitter = new EventEmitter(); terminalManager = TypeMoq.Mock.ofType(); + const execution: TerminalShellExecution = { + commandLine: { + value: 'dummy text', + isTrusted: true, + confidence: 2, + }, + cwd: undefined, + read: function (): AsyncIterable { + throw new Error('Function not implemented.'); + }, + }; + + event = { + execution, + exitCode: 0, + terminal: terminal.object, + shellIntegration: terminalShellIntegration.object, + }; + + terminalShellIntegration.setup((t) => t.executeCommand(TypeMoq.It.isAny())).returns(() => execution); + + terminalManager + .setup((t) => t.onDidEndTerminalShellExecution) + .returns(() => { + setTimeout(() => onDidEndTerminalShellExecutionEmitter.fire(event), 100); + return onDidEndTerminalShellExecutionEmitter.event; + }); platformService = TypeMoq.Mock.ofType(); workspaceService = TypeMoq.Mock.ofType(); terminalHelper = TypeMoq.Mock.ofType(); @@ -35,6 +106,14 @@ suite('Terminal Service', () => { disposables = []; mockServiceContainer = TypeMoq.Mock.ofType(); + interpreterService = TypeMoq.Mock.ofType(); + interpreterService + .setup((i) => i.getActiveInterpreter(TypeMoq.It.isAny())) + .returns(() => Promise.resolve(({ path: 'ps' } as unknown) as PythonEnvironment)); + + options = TypeMoq.Mock.ofType(); + options.setup((o) => o.resource).returns(() => Uri.parse('a')); + mockServiceContainer.setup((c) => c.get(ITerminalManager)).returns(() => terminalManager.object); mockServiceContainer.setup((c) => c.get(ITerminalHelper)).returns(() => terminalHelper.object); mockServiceContainer.setup((c) => c.get(IPlatformService)).returns(() => platformService.object); @@ -42,12 +121,36 @@ suite('Terminal Service', () => { mockServiceContainer.setup((c) => c.get(IWorkspaceService)).returns(() => workspaceService.object); mockServiceContainer.setup((c) => c.get(ITerminalActivator)).returns(() => terminalActivator.object); mockServiceContainer.setup((c) => c.get(ITerminalAutoActivation)).returns(() => terminalAutoActivator.object); + mockServiceContainer.setup((c) => c.get(IInterpreterService)).returns(() => interpreterService.object); + + applicationShell = TypeMoq.Mock.ofType(); + onDidWriteTerminalDataEmitter = new EventEmitter(); + applicationShell.setup((a) => a.onDidWriteTerminalData).returns(() => onDidWriteTerminalDataEmitter.event); + mockServiceContainer.setup((c) => c.get(IApplicationShell)).returns(() => applicationShell.object); + + onDidChangeTerminalStateEmitter = new EventEmitter(); + terminalManager + .setup((t) => t.onDidChangeTerminalState(TypeMoq.It.isAny())) + .returns((handler) => onDidChangeTerminalStateEmitter.event(handler)); + + getConfigurationStub = sinon.stub(workspaceApis, 'getConfiguration'); + isWindowsStub = sinon.stub(platform, 'isWindows'); + pythonConfig = TypeMoq.Mock.ofType(); + editorConfig = TypeMoq.Mock.ofType(); + getConfigurationStub.callsFake((section: string) => { + if (section === 'python') { + return pythonConfig.object; + } + return editorConfig.object; + }); }); teardown(() => { if (service) { service.dispose(); } disposables.filter((item) => !!item).forEach((item) => item.dispose()); + sinon.restore(); + interpreterService.reset(); }); test('Ensure terminal is disposed', async () => { @@ -57,6 +160,7 @@ suite('Terminal Service', () => { const os: string = 'windows'; service = new TerminalService(mockServiceContainer.object); const shellPath = 'powershell.exe'; + // TODO: switch over legacy Terminal code to use workspace getConfiguration from workspaceApis instead of directly from vscode.workspace workspaceService .setup((w) => w.getConfiguration(TypeMoq.It.isValue('terminal.integrated.shell'))) .returns(() => { @@ -64,6 +168,7 @@ suite('Terminal Service', () => { workspaceConfig.setup((c) => c.get(os)).returns(() => shellPath); return workspaceConfig.object; }); + pythonConfig.setup((p) => p.get('terminal.shellIntegration.enabled')).returns(() => false); platformService.setup((p) => p.isWindows).returns(() => os === 'windows'); platformService.setup((p) => p.isLinux).returns(() => os === 'linux'); @@ -73,15 +178,22 @@ suite('Terminal Service', () => { .setup((h) => h.buildCommandForTerminal(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny())) .returns(() => 'dummy text'); + terminalManager + .setup((t) => t.onDidEndTerminalShellExecution) + .returns(() => { + setTimeout(() => onDidEndTerminalShellExecutionEmitter.fire(event), 100); + return onDidEndTerminalShellExecutionEmitter.event; + }); // Sending a command will cause the terminal to be created await service.sendCommand('', []); - terminal.verify((t) => t.show(TypeMoq.It.isValue(true)), TypeMoq.Times.exactly(2)); + terminal.verify((t) => t.show(TypeMoq.It.isValue(true)), TypeMoq.Times.atLeastOnce()); service.dispose(); terminal.verify((t) => t.dispose(), TypeMoq.Times.exactly(1)); }); test('Ensure command is sent to terminal and it is shown', async () => { + pythonConfig.setup((p) => p.get('terminal.shellIntegration.enabled')).returns(() => false); terminalHelper .setup((helper) => helper.getEnvironmentActivationCommands(TypeMoq.It.isAny(), TypeMoq.It.isAny())) .returns(() => Promise.resolve(undefined)); @@ -97,10 +209,10 @@ suite('Terminal Service', () => { await service.sendCommand(commandToSend, args); - terminal.verify((t) => t.show(TypeMoq.It.isValue(true)), TypeMoq.Times.exactly(2)); + terminal.verify((t) => t.show(TypeMoq.It.isValue(true)), TypeMoq.Times.atLeastOnce()); terminal.verify( (t) => t.sendText(TypeMoq.It.isValue(commandToExpect), TypeMoq.It.isValue(true)), - TypeMoq.Times.exactly(1), + TypeMoq.Times.never(), ); }); @@ -119,6 +231,156 @@ suite('Terminal Service', () => { terminal.verify((t) => t.sendText(TypeMoq.It.isValue(textToSend)), TypeMoq.Times.exactly(1)); }); + test('Ensure sendText is used when Python shell integration is disabled', async () => { + pythonConfig + .setup((p) => p.get('terminal.shellIntegration.enabled')) + .returns(() => false) + .verifiable(TypeMoq.Times.once()); + + terminalHelper + .setup((helper) => helper.getEnvironmentActivationCommands(TypeMoq.It.isAny(), TypeMoq.It.isAny())) + .returns(() => Promise.resolve(undefined)); + service = new TerminalService(mockServiceContainer.object); + const textToSend = 'Some Text'; + terminalHelper.setup((h) => h.identifyTerminalShell(TypeMoq.It.isAny())).returns(() => TerminalShellType.bash); + terminalManager.setup((t) => t.createTerminal(TypeMoq.It.isAny())).returns(() => terminal.object); + + await service.ensureTerminal(); + const executePromise = service.executeCommand(textToSend, true); + onDidWriteTerminalDataEmitter.fire({ terminal: terminal.object, data: '>>> ' }); + await executePromise; + + terminal.verify((t) => t.show(TypeMoq.It.isValue(true)), TypeMoq.Times.exactly(1)); + terminal.verify((t) => t.sendText(TypeMoq.It.isValue(textToSend)), TypeMoq.Times.exactly(1)); + }); + + test('Ensure sendText is called when terminal.shellIntegration enabled but Python shell integration disabled', async () => { + pythonConfig + .setup((p) => p.get('terminal.shellIntegration.enabled')) + .returns(() => false) + .verifiable(TypeMoq.Times.once()); + + terminalHelper + .setup((helper) => helper.getEnvironmentActivationCommands(TypeMoq.It.isAny(), TypeMoq.It.isAny())) + .returns(() => Promise.resolve(undefined)); + service = new TerminalService(mockServiceContainer.object); + const textToSend = 'Some Text'; + terminalHelper.setup((h) => h.identifyTerminalShell(TypeMoq.It.isAny())).returns(() => TerminalShellType.bash); + terminalManager.setup((t) => t.createTerminal(TypeMoq.It.isAny())).returns(() => terminal.object); + + await service.ensureTerminal(); + const executePromise = service.executeCommand(textToSend, true); + onDidWriteTerminalDataEmitter.fire({ terminal: terminal.object, data: '>>> ' }); + await executePromise; + + terminal.verify((t) => t.show(TypeMoq.It.isValue(true)), TypeMoq.Times.exactly(1)); + terminal.verify((t) => t.sendText(TypeMoq.It.isValue(textToSend)), TypeMoq.Times.exactly(1)); + }); + + test('Ensure sendText is called when Python shell integration and terminal shell integration are both enabled - Mac, Linux && Python < 3.13', async () => { + isWindowsStub.returns(false); + pythonConfig + .setup((p) => p.get('terminal.shellIntegration.enabled')) + .returns(() => true) + .verifiable(TypeMoq.Times.once()); + + terminalHelper + .setup((helper) => helper.getEnvironmentActivationCommands(TypeMoq.It.isAny(), TypeMoq.It.isAny())) + .returns(() => Promise.resolve(undefined)); + service = new TerminalService(mockServiceContainer.object); + const textToSend = 'Some Text'; + terminalHelper.setup((h) => h.identifyTerminalShell(TypeMoq.It.isAny())).returns(() => TerminalShellType.bash); + terminalManager.setup((t) => t.createTerminal(TypeMoq.It.isAny())).returns(() => terminal.object); + + await service.ensureTerminal(); + const executePromise = service.executeCommand(textToSend, true); + onDidWriteTerminalDataEmitter.fire({ terminal: terminal.object, data: '>>> ' }); + await executePromise; + + terminal.verify((t) => t.show(TypeMoq.It.isValue(true)), TypeMoq.Times.exactly(1)); + terminal.verify((t) => t.sendText(TypeMoq.It.isValue(textToSend)), TypeMoq.Times.exactly(1)); + }); + + test('Ensure sendText is called when Python shell integration and terminal shell integration are both enabled - Mac, Linux && Python >= 3.13', async () => { + interpreterService.reset(); + + interpreterService + .setup((i) => i.getActiveInterpreter(TypeMoq.It.isAny())) + .returns(() => + Promise.resolve({ path: 'yo', version: { major: 3, minor: 13, patch: 0 } } as PythonEnvironment), + ); + + isWindowsStub.returns(false); + pythonConfig + .setup((p) => p.get('terminal.shellIntegration.enabled')) + .returns(() => true) + .verifiable(TypeMoq.Times.once()); + + terminalHelper + .setup((helper) => helper.getEnvironmentActivationCommands(TypeMoq.It.isAny(), TypeMoq.It.isAny())) + .returns(() => Promise.resolve(undefined)); + + service = new TerminalService(mockServiceContainer.object, options.object); + const textToSend = 'Some Text'; + terminalHelper.setup((h) => h.identifyTerminalShell(TypeMoq.It.isAny())).returns(() => TerminalShellType.bash); + terminalManager.setup((t) => t.createTerminal(TypeMoq.It.isAny())).returns(() => terminal.object); + + await service.ensureTerminal(); + const executePromise = service.executeCommand(textToSend, true); + onDidWriteTerminalDataEmitter.fire({ terminal: terminal.object, data: '>>> ' }); + await executePromise; + + terminal.verify((t) => t.sendText(TypeMoq.It.isValue(textToSend)), TypeMoq.Times.once()); + }); + + test('Ensure sendText IS called even when Python shell integration and terminal shell integration are both enabled - Window', async () => { + isWindowsStub.returns(true); + pythonConfig + .setup((p) => p.get('terminal.shellIntegration.enabled')) + .returns(() => true) + .verifiable(TypeMoq.Times.once()); + + terminalHelper + .setup((helper) => helper.getEnvironmentActivationCommands(TypeMoq.It.isAny(), TypeMoq.It.isAny())) + .returns(() => Promise.resolve(undefined)); + service = new TerminalService(mockServiceContainer.object); + const textToSend = 'Some Text'; + terminalHelper.setup((h) => h.identifyTerminalShell(TypeMoq.It.isAny())).returns(() => TerminalShellType.bash); + terminalManager.setup((t) => t.createTerminal(TypeMoq.It.isAny())).returns(() => terminal.object); + + await service.ensureTerminal(); + const executePromise = service.executeCommand(textToSend, true); + onDidWriteTerminalDataEmitter.fire({ terminal: terminal.object, data: '>>> ' }); + await executePromise; + + terminal.verify((t) => t.show(TypeMoq.It.isValue(true)), TypeMoq.Times.exactly(1)); + terminal.verify((t) => t.sendText(TypeMoq.It.isValue(textToSend)), TypeMoq.Times.exactly(1)); + }); + + test('Ensure REPL ready when onDidChangeTerminalState fires with python shell', async () => { + pythonConfig + .setup((p) => p.get('terminal.shellIntegration.enabled')) + .returns(() => false) + .verifiable(TypeMoq.Times.once()); + + terminalHelper + .setup((helper) => helper.getEnvironmentActivationCommands(TypeMoq.It.isAny(), TypeMoq.It.isAny())) + .returns(() => Promise.resolve(undefined)); + service = new TerminalService(mockServiceContainer.object); + const textToSend = 'Some Text'; + terminalHelper.setup((h) => h.identifyTerminalShell(TypeMoq.It.isAny())).returns(() => TerminalShellType.bash); + + terminal.setup((t) => t.state).returns(() => ({ isInteractedWith: true, shell: 'python' })); + terminalManager.setup((t) => t.createTerminal(TypeMoq.It.isAny())).returns(() => terminal.object); + + await service.ensureTerminal(); + const executePromise = service.executeCommand(textToSend, true); + onDidChangeTerminalStateEmitter.fire(terminal.object); + await executePromise; + + terminal.verify((t) => t.sendText(TypeMoq.It.isValue(textToSend)), TypeMoq.Times.exactly(1)); + }); + test('Ensure terminal is not shown if `hideFromUser` option is set to `true`', async () => { terminalHelper .setup((helper) => helper.getEnvironmentActivationCommands(TypeMoq.It.isAny(), TypeMoq.It.isAny())) @@ -158,6 +420,37 @@ suite('Terminal Service', () => { terminal.verify((t) => t.show(TypeMoq.It.isValue(false)), TypeMoq.Times.exactly(2)); }); + test('Ensure PYTHONSTARTUP is injected', async () => { + service = new TerminalService(mockServiceContainer.object); + terminalActivator + .setup((h) => h.activateEnvironmentInTerminal(TypeMoq.It.isAny(), TypeMoq.It.isAny())) + .returns(() => Promise.resolve(true)) + .verifiable(TypeMoq.Times.once()); + terminalManager + .setup((t) => t.createTerminal(TypeMoq.It.isAny())) + .returns(() => terminal.object) + .verifiable(TypeMoq.Times.atLeastOnce()); + const envVarScript = path.join(EXTENSION_ROOT_DIR, 'python_files', 'pythonrc.py'); + terminalManager + .setup((t) => + t.createTerminal({ + name: TypeMoq.It.isAny(), + env: TypeMoq.It.isObjectWith({ PYTHONSTARTUP: envVarScript }), + hideFromUser: TypeMoq.It.isAny(), + }), + ) + .returns(() => terminal.object) + .verifiable(TypeMoq.Times.atLeastOnce()); + await service.show(); + await service.show(); + await service.show(); + await service.show(); + + terminalHelper.verifyAll(); + terminalActivator.verifyAll(); + terminal.verify((t) => t.show(TypeMoq.It.isValue(true)), TypeMoq.Times.atLeastOnce()); + }); + test('Ensure terminal is activated once after creation', async () => { service = new TerminalService(mockServiceContainer.object); terminalActivator diff --git a/src/test/common/terminals/shellDetectors/shellDetectors.unit.test.ts b/src/test/common/terminals/shellDetectors/shellDetectors.unit.test.ts index 07befdda9291..e58e455ea7eb 100644 --- a/src/test/common/terminals/shellDetectors/shellDetectors.unit.test.ts +++ b/src/test/common/terminals/shellDetectors/shellDetectors.unit.test.ts @@ -41,6 +41,7 @@ suite('Shell Detectors', () => { shellPathsAndIdentification.set('/usr/bin/ksh', TerminalShellType.ksh); shellPathsAndIdentification.set('c:\\windows\\system32\\powershell.exe', TerminalShellType.powershell); shellPathsAndIdentification.set('c:\\windows\\system32\\pwsh.exe', TerminalShellType.powershellCore); + shellPathsAndIdentification.set('C:\\Program Files\\nu\\bin\\nu.EXE', TerminalShellType.nushell); shellPathsAndIdentification.set('/usr/microsoft/xxx/powershell/powershell', TerminalShellType.powershell); shellPathsAndIdentification.set('/usr/microsoft/xxx/powershell/pwsh', TerminalShellType.powershellCore); shellPathsAndIdentification.set('/usr/bin/fish', TerminalShellType.fish); diff --git a/src/test/common/terminals/synchronousTerminalService.unit.test.ts b/src/test/common/terminals/synchronousTerminalService.unit.test.ts index f74c529ef470..4b6e77ec8095 100644 --- a/src/test/common/terminals/synchronousTerminalService.unit.test.ts +++ b/src/test/common/terminals/synchronousTerminalService.unit.test.ts @@ -66,7 +66,7 @@ suite('Terminal Service (synchronous)', () => { }); }); suite('sendCommand', () => { - const shellExecFile = path.join(EXTENSION_ROOT_DIR, 'pythonFiles', 'shell_exec.py'); + const shellExecFile = path.join(EXTENSION_ROOT_DIR, 'python_files', 'shell_exec.py'); test('run sendCommand in terminalService if there is no cancellation token', async () => { when(terminalService.sendCommand('cmd', deepEqual(['1', '2']))).thenResolve(); diff --git a/src/test/common/utils/decorators.unit.test.ts b/src/test/common/utils/decorators.unit.test.ts index 753434d0c4f8..b1e86c4e2013 100644 --- a/src/test/common/utils/decorators.unit.test.ts +++ b/src/test/common/utils/decorators.unit.test.ts @@ -8,7 +8,7 @@ import * as chaiPromise from 'chai-as-promised'; import { clearCache } from '../../../client/common/utils/cacheUtils'; import { cache, makeDebounceAsyncDecorator, makeDebounceDecorator } from '../../../client/common/utils/decorators'; import { sleep } from '../../core'; -use(chaiPromise); +use(chaiPromise.default); suite('Common Utils - Decorators', function () { // For some reason, sometimes we have timeouts on CI. @@ -72,8 +72,6 @@ suite('Common Utils - Decorators', function () { * This has an accuracy of around 2-20ms. * However we're dealing with tests that need accuracy of 1ms. * Use API that'll give us better accuracy when dealing with elapsed times. - * - * @returns {number} */ function getHighPrecisionTime(): number { const currentTime = process.hrtime(); @@ -91,9 +89,6 @@ suite('Common Utils - Decorators', function () { * await new Promise(resolve = setTimeout(resolve, 100)) * console.log(currentTime - startTijme) * ``` - * - * @param {number} actualDelay - * @param {number} expectedDelay */ function assertElapsedTimeWithinRange(actualDelay: number, expectedDelay: number) { const difference = actualDelay - expectedDelay; diff --git a/src/test/common/variables/envVarsProvider.multiroot.test.ts b/src/test/common/variables/envVarsProvider.multiroot.test.ts index ccdca42c54a0..3ba073d71474 100644 --- a/src/test/common/variables/envVarsProvider.multiroot.test.ts +++ b/src/test/common/variables/envVarsProvider.multiroot.test.ts @@ -4,7 +4,7 @@ import { expect, use } from 'chai'; import * as chaiAsPromised from 'chai-as-promised'; import * as path from 'path'; -import { anything, instance, mock, when } from 'ts-mockito'; +import { anything } from 'ts-mockito'; import { ConfigurationTarget, Disposable, Uri, workspace } from 'vscode'; import { WorkspaceService } from '../../../client/common/application/workspace'; import { PlatformService } from '../../../client/common/platform/platformService'; @@ -14,7 +14,6 @@ import { getSearchPathEnvVarNames } from '../../../client/common/utils/exec'; import { EnvironmentVariablesService } from '../../../client/common/variables/environment'; import { EnvironmentVariablesProvider } from '../../../client/common/variables/environmentVariablesProvider'; import { EnvironmentVariables } from '../../../client/common/variables/types'; -import { EnvironmentActivationService } from '../../../client/interpreter/activation/service'; import { IEnvironmentActivationService } from '../../../client/interpreter/activation/types'; import { IInterpreterAutoSelectionService } from '../../../client/interpreter/autoSelection/types'; import { clearPythonPathInWorkspaceFolder, isOs, OSType, updateSetting } from '../../common'; @@ -22,8 +21,9 @@ import { closeActiveWindows, initialize, initializeTest, IS_MULTI_ROOT_TEST } fr import { MockAutoSelectionService } from '../../mocks/autoSelector'; import { MockProcess } from '../../mocks/process'; import { UnitTestIocContainer } from '../../testing/serviceRegistry'; +import { createTypeMoq } from '../../mocks/helper'; -use(chaiAsPromised); +use(chaiAsPromised.default); const multirootPath = path.join(__dirname, '..', '..', '..', '..', 'src', 'testMultiRootWkspc'); const workspace4Path = Uri.file(path.join(multirootPath, 'workspace4')); @@ -47,12 +47,21 @@ suite('Multiroot Environment Variables Provider', () => { ioc.registerProcessTypes(); ioc.registerInterpreterStorageTypes(); await ioc.registerMockInterpreterTypes(); - const mockEnvironmentActivationService = mock(EnvironmentActivationService); - when(mockEnvironmentActivationService.getActivatedEnvironmentVariables(anything())).thenResolve(); - ioc.serviceManager.rebindInstance( - IEnvironmentActivationService, - instance(mockEnvironmentActivationService), - ); + const mockEnvironmentActivationService = createTypeMoq(); + mockEnvironmentActivationService + .setup((m) => m.getActivatedEnvironmentVariables(anything())) + .returns(() => Promise.resolve({})); + if (ioc.serviceManager.tryGet(IEnvironmentActivationService)) { + ioc.serviceManager.rebindInstance( + IEnvironmentActivationService, + mockEnvironmentActivationService.object, + ); + } else { + ioc.serviceManager.addSingletonInstance( + IEnvironmentActivationService, + mockEnvironmentActivationService.object, + ); + } return initializeTest(); }); suiteTeardown(closeActiveWindows); diff --git a/src/test/common/variables/envVarsService.functional.test.ts b/src/test/common/variables/envVarsService.functional.test.ts index 0886bc823960..3cf55eddbd45 100644 --- a/src/test/common/variables/envVarsService.functional.test.ts +++ b/src/test/common/variables/envVarsService.functional.test.ts @@ -13,7 +13,7 @@ import { EnvironmentVariablesService } from '../../../client/common/variables/en import { IEnvironmentVariablesService } from '../../../client/common/variables/types'; import { getOSType } from '../../common'; -use(chaiAsPromised); +use(chaiAsPromised.default); // Functional tests that run code using the VS Code API are found // in envVarsService.test.ts. diff --git a/src/test/common/variables/envVarsService.test.ts b/src/test/common/variables/envVarsService.test.ts index f289d291ac19..c7151a8e33b9 100644 --- a/src/test/common/variables/envVarsService.test.ts +++ b/src/test/common/variables/envVarsService.test.ts @@ -14,7 +14,7 @@ import { EnvironmentVariablesService } from '../../../client/common/variables/en import { IEnvironmentVariablesService } from '../../../client/common/variables/types'; import { getOSType } from '../../common'; -use(chaiAsPromised); +use(chaiAsPromised.default); const envFilesFolderPath = path.join(__dirname, '..', '..', '..', '..', 'src', 'testMultiRootWkspc', 'workspace4'); diff --git a/src/test/common/variables/envVarsService.unit.test.ts b/src/test/common/variables/envVarsService.unit.test.ts index 0c978b2f9e86..3709d97b9f62 100644 --- a/src/test/common/variables/envVarsService.unit.test.ts +++ b/src/test/common/variables/envVarsService.unit.test.ts @@ -12,7 +12,7 @@ import { IPathUtils } from '../../../client/common/types'; import { EnvironmentVariablesService, parseEnvFile } from '../../../client/common/variables/environment'; import { getSearchPathEnvVarNames } from '../../../client/common/utils/exec'; -use(chaiAsPromised); +use(chaiAsPromised.default); type PathVar = 'Path' | 'PATH'; const PATHS = getSearchPathEnvVarNames(); diff --git a/src/test/configuration/environmentTypeComparer.unit.test.ts b/src/test/configuration/environmentTypeComparer.unit.test.ts index 62c5d2d66b96..bce20fcb0fef 100644 --- a/src/test/configuration/environmentTypeComparer.unit.test.ts +++ b/src/test/configuration/environmentTypeComparer.unit.test.ts @@ -11,6 +11,8 @@ import { getEnvLocationHeuristic, } from '../../client/interpreter/configuration/environmentTypeComparer'; import { IInterpreterHelper } from '../../client/interpreter/contracts'; +import { PythonEnvType } from '../../client/pythonEnvironments/base/info'; +import * as pyenv from '../../client/pythonEnvironments/common/environmentManagers/pyenv'; import { EnvironmentType, PythonEnvironment } from '../../client/pythonEnvironments/info'; suite('Environment sorting', () => { @@ -18,6 +20,7 @@ suite('Environment sorting', () => { let interpreterHelper: IInterpreterHelper; let getActiveWorkspaceUriStub: sinon.SinonStub; let getInterpreterTypeDisplayNameStub: sinon.SinonStub; + const preferredPyenv = path.join('path', 'to', 'preferred', 'pyenv'); setup(() => { getActiveWorkspaceUriStub = sinon.stub().returns({ folderUri: { fsPath: workspacePath } }); @@ -27,6 +30,8 @@ suite('Environment sorting', () => { getActiveWorkspaceUri: getActiveWorkspaceUriStub, getInterpreterTypeDisplayName: getInterpreterTypeDisplayNameStub, } as unknown) as IInterpreterHelper; + const getActivePyenvForDirectory = sinon.stub(pyenv, 'getActivePyenvForDirectory'); + getActivePyenvForDirectory.resolves(preferredPyenv); }); teardown(() => { @@ -45,6 +50,7 @@ suite('Environment sorting', () => { title: 'Local virtual environment should come first', envA: { envType: EnvironmentType.Venv, + type: PythonEnvType.Virtual, envPath: path.join(workspacePath, '.venv'), version: { major: 3, minor: 10, patch: 2 }, } as PythonEnvironment, @@ -58,11 +64,13 @@ suite('Environment sorting', () => { title: "Non-local virtual environment should not come first when there's a local env", envA: { envType: EnvironmentType.Venv, + type: PythonEnvType.Virtual, envPath: path.join('path', 'to', 'other', 'workspace', '.venv'), version: { major: 3, minor: 10, patch: 2 }, } as PythonEnvironment, envB: { envType: EnvironmentType.Venv, + type: PythonEnvType.Virtual, envPath: path.join(workspacePath, '.venv'), version: { major: 3, minor: 10, patch: 2 }, } as PythonEnvironment, @@ -72,10 +80,12 @@ suite('Environment sorting', () => { title: "Conda environment should not come first when there's a local env", envA: { envType: EnvironmentType.Conda, + type: PythonEnvType.Conda, version: { major: 3, minor: 10, patch: 2 }, } as PythonEnvironment, envB: { envType: EnvironmentType.Venv, + type: PythonEnvType.Virtual, envPath: path.join(workspacePath, '.venv'), version: { major: 3, minor: 10, patch: 2 }, } as PythonEnvironment, @@ -85,11 +95,13 @@ suite('Environment sorting', () => { title: 'Conda base environment should come after any other conda env', envA: { envType: EnvironmentType.Conda, + type: PythonEnvType.Conda, envName: 'base', version: { major: 3, minor: 10, patch: 2 }, } as PythonEnvironment, envB: { envType: EnvironmentType.Conda, + type: PythonEnvType.Conda, envName: 'random-name', version: { major: 3, minor: 10, patch: 2 }, } as PythonEnvironment, @@ -99,6 +111,7 @@ suite('Environment sorting', () => { title: 'Pipenv environment should come before any other conda env', envA: { envType: EnvironmentType.Conda, + type: PythonEnvType.Conda, envName: 'conda-env', version: { major: 3, minor: 10, patch: 2 }, } as PythonEnvironment, @@ -118,24 +131,53 @@ suite('Environment sorting', () => { } as PythonEnvironment, envB: { envType: EnvironmentType.Poetry, + type: PythonEnvType.Virtual, envName: 'poetry-env', version: { major: 3, minor: 10, patch: 2 }, } as PythonEnvironment, expected: 1, }, { - title: 'Pyenv environment should not come first when there are global envs', + title: 'Pyenv interpreter should not come first when there are global envs', envA: { envType: EnvironmentType.Pyenv, version: { major: 3, minor: 10, patch: 2 }, } as PythonEnvironment, envB: { envType: EnvironmentType.Pipenv, + type: PythonEnvType.Virtual, envName: 'pipenv-env', version: { major: 3, minor: 10, patch: 2 }, } as PythonEnvironment, expected: 1, }, + { + title: 'Preferred Pyenv interpreter should come before any global interpreter', + envA: { + envType: EnvironmentType.Pyenv, + version: { major: 3, minor: 12, patch: 2 }, + path: preferredPyenv, + } as PythonEnvironment, + envB: { + envType: EnvironmentType.Pyenv, + version: { major: 3, minor: 10, patch: 2 }, + path: path.join('path', 'to', 'normal', 'pyenv'), + } as PythonEnvironment, + expected: -1, + }, + { + title: 'Pyenv interpreters should come first when there are global interpreters', + envA: { + envType: EnvironmentType.Global, + version: { major: 3, minor: 10, patch: 2 }, + } as PythonEnvironment, + envB: { + envType: EnvironmentType.Pyenv, + version: { major: 3, minor: 7, patch: 2 }, + path: path.join('path', 'to', 'normal', 'pyenv'), + } as PythonEnvironment, + expected: 1, + }, { title: 'Global environment should not come first when there are global envs', envA: { @@ -144,6 +186,7 @@ suite('Environment sorting', () => { } as PythonEnvironment, envB: { envType: EnvironmentType.Poetry, + type: PythonEnvType.Virtual, envName: 'poetry-env', version: { major: 3, minor: 10, patch: 2 }, } as PythonEnvironment, @@ -157,11 +200,25 @@ suite('Environment sorting', () => { } as PythonEnvironment, envB: { envType: EnvironmentType.VirtualEnv, + type: PythonEnvType.Virtual, envName: 'virtualenv-env', version: { major: 3, minor: 10, patch: 2 }, } as PythonEnvironment, expected: 1, }, + { + title: + 'Microsoft Store interpreter should not come first when there are global interpreters with higher version', + envA: { + envType: EnvironmentType.MicrosoftStore, + version: { major: 3, minor: 10, patch: 2, raw: '3.10.2' }, + } as PythonEnvironment, + envB: { + envType: EnvironmentType.Global, + version: { major: 3, minor: 11, patch: 2, raw: '3.11.2' }, + } as PythonEnvironment, + expected: 1, + }, { title: 'Unknown environment should not come first when there are global envs', envA: { @@ -170,6 +227,7 @@ suite('Environment sorting', () => { } as PythonEnvironment, envB: { envType: EnvironmentType.Pipenv, + type: PythonEnvType.Virtual, envName: 'pipenv-env', version: { major: 3, minor: 10, patch: 2 }, } as PythonEnvironment, @@ -179,11 +237,13 @@ suite('Environment sorting', () => { title: 'If 2 environments are of the same type, the most recent Python version comes first', envA: { envType: EnvironmentType.Venv, + type: PythonEnvType.Virtual, envPath: path.join(workspacePath, '.old-venv'), version: { major: 3, minor: 7, patch: 5, raw: '3.7.5' }, } as PythonEnvironment, envB: { envType: EnvironmentType.Venv, + type: PythonEnvType.Virtual, envPath: path.join(workspacePath, '.venv'), version: { major: 3, minor: 10, patch: 2, raw: '3.10.2' }, } as PythonEnvironment, @@ -194,11 +254,13 @@ suite('Environment sorting', () => { "If 2 global environments have the same Python version and there's a Conda one, the Conda env should not come first", envA: { envType: EnvironmentType.Conda, + type: PythonEnvType.Conda, envName: 'conda-env', version: { major: 3, minor: 10, patch: 2 }, } as PythonEnvironment, envB: { envType: EnvironmentType.Pipenv, + type: PythonEnvType.Virtual, envName: 'pipenv-env', version: { major: 3, minor: 10, patch: 2 }, } as PythonEnvironment, @@ -209,11 +271,13 @@ suite('Environment sorting', () => { 'If 2 global environments are of the same type and have the same Python version, they should be sorted by name', envA: { envType: EnvironmentType.Conda, + type: PythonEnvType.Conda, envName: 'conda-foo', version: { major: 3, minor: 10, patch: 2 }, } as PythonEnvironment, envB: { envType: EnvironmentType.Conda, + type: PythonEnvType.Conda, envName: 'conda-bar', version: { major: 3, minor: 10, patch: 2 }, } as PythonEnvironment, @@ -237,6 +301,7 @@ suite('Environment sorting', () => { title: 'Problematic environments should come last', envA: { envType: EnvironmentType.Conda, + type: PythonEnvType.Conda, envPath: path.join(workspacePath, '.venv'), path: 'python', } as PythonEnvironment, @@ -249,8 +314,9 @@ suite('Environment sorting', () => { ]; testcases.forEach(({ title, envA, envB, expected }) => { - test(title, () => { + test(title, async () => { const envTypeComparer = new EnvironmentTypeComparer(interpreterHelper); + await envTypeComparer.initialize(undefined); const result = envTypeComparer.compare(envA, envB); assert.strictEqual(result, expected); diff --git a/src/test/configuration/interpreterSelector/commands/resetInterpreter.unit.test.ts b/src/test/configuration/interpreterSelector/commands/resetInterpreter.unit.test.ts index e1c3a960b99f..a32c794b7dc7 100644 --- a/src/test/configuration/interpreterSelector/commands/resetInterpreter.unit.test.ts +++ b/src/test/configuration/interpreterSelector/commands/resetInterpreter.unit.test.ts @@ -2,6 +2,7 @@ // Licensed under the MIT License. import * as path from 'path'; +import * as sinon from 'sinon'; import * as TypeMoq from 'typemoq'; import { ConfigurationTarget, Uri } from 'vscode'; import { IApplicationShell, ICommandManager, IWorkspaceService } from '../../../../client/common/application/types'; @@ -10,6 +11,7 @@ import { IConfigurationService } from '../../../../client/common/types'; import { Common, Interpreters } from '../../../../client/common/utils/localize'; import { ResetInterpreterCommand } from '../../../../client/interpreter/configuration/interpreterSelector/commands/resetInterpreter'; import { IPythonPathUpdaterServiceManager } from '../../../../client/interpreter/configuration/types'; +import * as extapi from '../../../../client/envExt/api.internal'; suite('Reset Interpreter Command', () => { let workspace: TypeMoq.IMock; @@ -21,8 +23,12 @@ suite('Reset Interpreter Command', () => { const folder2 = { name: 'two', uri: Uri.parse('two'), index: 2 }; let resetInterpreterCommand: ResetInterpreterCommand; + let useEnvExtensionStub: sinon.SinonStub; setup(() => { + useEnvExtensionStub = sinon.stub(extapi, 'useEnvExtension'); + useEnvExtensionStub.returns(false); + configurationService = TypeMoq.Mock.ofType(); configurationService .setup((c) => c.getSettings(TypeMoq.It.isAny())) @@ -42,6 +48,9 @@ suite('Reset Interpreter Command', () => { configurationService.object, ); }); + teardown(() => { + sinon.restore(); + }); suite('Test method resetInterpreter()', async () => { test('Update Global settings when there are no workspaces', async () => { diff --git a/src/test/configuration/interpreterSelector/commands/setInterpreter.unit.test.ts b/src/test/configuration/interpreterSelector/commands/setInterpreter.unit.test.ts index 7059fb7ab26f..7837245ec9d2 100644 --- a/src/test/configuration/interpreterSelector/commands/setInterpreter.unit.test.ts +++ b/src/test/configuration/interpreterSelector/commands/setInterpreter.unit.test.ts @@ -31,6 +31,7 @@ import { import { EnvGroups, InterpreterStateArgs, + QuickPickType, SetInterpreterCommand, } from '../../../../client/interpreter/configuration/interpreterSelector/commands/setInterpreter'; import { @@ -46,8 +47,8 @@ import { Commands, Octicons } from '../../../../client/common/constants'; import { IInterpreterService, PythonEnvironmentsChangedEvent } from '../../../../client/interpreter/contracts'; import { createDeferred, sleep } from '../../../../client/common/utils/async'; import { SystemVariables } from '../../../../client/common/variables/systemVariables'; - -const untildify = require('untildify'); +import { untildify } from '../../../../client/common/helpers'; +import * as extapi from '../../../../client/envExt/api.internal'; type TelemetryEventType = { eventName: EventName; properties: unknown }; @@ -62,12 +63,16 @@ suite('Set Interpreter Command', () => { let platformService: TypeMoq.IMock; let multiStepInputFactory: TypeMoq.IMock; let interpreterService: IInterpreterService; + let useEnvExtensionStub: sinon.SinonStub; const folder1 = { name: 'one', uri: Uri.parse('one'), index: 1 }; const folder2 = { name: 'two', uri: Uri.parse('two'), index: 2 }; let setInterpreterCommand: SetInterpreterCommand; setup(() => { + useEnvExtensionStub = sinon.stub(extapi, 'useEnvExtension'); + useEnvExtensionStub.returns(false); + interpreterSelector = TypeMoq.Mock.ofType(); multiStepInputFactory = TypeMoq.Mock.ofType(); platformService = TypeMoq.Mock.ofType(); @@ -154,7 +159,11 @@ suite('Set Interpreter Command', () => { } as PythonEnvironment, }; const expectedEnterInterpreterPathSuggestion = { - label: `${Octicons.Add} ${InterpreterQuickPickList.enterPath.label}`, + label: `${Octicons.Folder} ${InterpreterQuickPickList.enterPath.label}`, + alwaysShow: true, + }; + const expectedCreateEnvSuggestion = { + label: `${Octicons.Add} ${InterpreterQuickPickList.create.label}`, alwaysShow: true, }; const currentPythonPath = 'python'; @@ -232,10 +241,11 @@ suite('Set Interpreter Command', () => { const state: InterpreterStateArgs = { path: 'some path', workspace: undefined }; const multiStepInput = TypeMoq.Mock.ofType>(); const recommended = cloneDeep(item); - recommended.label = `${Octicons.Star} ${item.label}`; + recommended.label = item.label; recommended.description = interpreterPath; const suggestions = [ expectedEnterInterpreterPathSuggestion, + { kind: QuickPickItemKind.Separator, label: '' }, defaultInterpreterPathSuggestion, { kind: QuickPickItemKind.Separator, label: EnvGroups.Recommended }, recommended, @@ -265,8 +275,68 @@ suite('Set Interpreter Command', () => { delete actualParameters!.initialize; delete actualParameters!.customButtonSetups; delete actualParameters!.onChangeItem; - const activeItem = await actualParameters!.activeItem; - assert.deepStrictEqual(activeItem, recommended); + if (typeof actualParameters!.activeItem === 'function') { + const activeItem = await actualParameters!.activeItem(({ items: suggestions } as unknown) as QuickPick< + QuickPickType + >); + assert.deepStrictEqual(activeItem, recommended); + } else { + assert.ok(false, 'Not a function'); + } + delete actualParameters!.activeItem; + assert.deepStrictEqual(actualParameters, expectedParameters, 'Params not equal'); + }); + + test('Picker should show create env when set in options', async () => { + const state: InterpreterStateArgs = { path: 'some path', workspace: undefined }; + const multiStepInput = TypeMoq.Mock.ofType>(); + const recommended = cloneDeep(item); + recommended.label = item.label; + recommended.description = interpreterPath; + const suggestions = [ + expectedCreateEnvSuggestion, + { kind: QuickPickItemKind.Separator, label: '' }, + expectedEnterInterpreterPathSuggestion, + { kind: QuickPickItemKind.Separator, label: '' }, + defaultInterpreterPathSuggestion, + { kind: QuickPickItemKind.Separator, label: EnvGroups.Recommended }, + recommended, + ]; + const expectedParameters: IQuickPickParameters = { + placeholder: `Selected Interpreter: ${currentPythonPath}`, + items: suggestions, + matchOnDetail: true, + matchOnDescription: true, + title: InterpreterQuickPickList.browsePath.openButtonLabel, + sortByLabel: true, + keepScrollPosition: true, + }; + let actualParameters: IQuickPickParameters | undefined; + multiStepInput + .setup((i) => i.showQuickPick(TypeMoq.It.isAny())) + .callback((options) => { + actualParameters = options; + }) + .returns(() => Promise.resolve((undefined as unknown) as QuickPickItem)); + + await setInterpreterCommand._pickInterpreter(multiStepInput.object, state, undefined, { + showCreateEnvironment: true, + }); + + expect(actualParameters).to.not.equal(undefined, 'Parameters not set'); + const refreshButtons = actualParameters!.customButtonSetups; + expect(refreshButtons).to.not.equal(undefined, 'Callback not set'); + delete actualParameters!.initialize; + delete actualParameters!.customButtonSetups; + delete actualParameters!.onChangeItem; + if (typeof actualParameters!.activeItem === 'function') { + const activeItem = await actualParameters!.activeItem(({ items: suggestions } as unknown) as QuickPick< + QuickPickType + >); + assert.deepStrictEqual(activeItem, recommended); + } else { + assert.ok(false, 'Not a function'); + } delete actualParameters!.activeItem; assert.deepStrictEqual(actualParameters, expectedParameters, 'Params not equal'); }); @@ -276,6 +346,7 @@ suite('Set Interpreter Command', () => { const multiStepInput = TypeMoq.Mock.ofType>(); const suggestions = [ expectedEnterInterpreterPathSuggestion, + { kind: QuickPickItemKind.Separator, label: '' }, defaultInterpreterPathSuggestion, noPythonInstalled, ]; @@ -308,8 +379,14 @@ suite('Set Interpreter Command', () => { delete actualParameters!.initialize; delete actualParameters!.customButtonSetups; delete actualParameters!.onChangeItem; - const activeItem = await actualParameters!.activeItem; - assert.deepStrictEqual(activeItem, noPythonInstalled); + if (typeof actualParameters!.activeItem === 'function') { + const activeItem = await actualParameters!.activeItem(({ items: suggestions } as unknown) as QuickPick< + QuickPickType + >); + assert.deepStrictEqual(activeItem, noPythonInstalled); + } else { + assert.ok(false, 'Not a function'); + } delete actualParameters!.activeItem; assert.deepStrictEqual(actualParameters, expectedParameters, 'Params not equal'); }); @@ -423,10 +500,11 @@ suite('Set Interpreter Command', () => { .setup((i) => i.getRecommendedSuggestion(TypeMoq.It.isAny(), TypeMoq.It.isAny())) .returns(() => item); const recommended = cloneDeep(item); - recommended.label = `${Octicons.Star} ${item.label}`; + recommended.label = item.label; recommended.description = interpreterPath; const suggestions = [ expectedEnterInterpreterPathSuggestion, + { kind: QuickPickItemKind.Separator, label: '' }, defaultInterpreterPathSuggestion, { kind: QuickPickItemKind.Separator, label: EnvGroups.Recommended }, recommended, @@ -539,10 +617,11 @@ suite('Set Interpreter Command', () => { .setup((i) => i.getRecommendedSuggestion(TypeMoq.It.isAny(), TypeMoq.It.isAny())) .returns(() => item); const recommended = cloneDeep(item); - recommended.label = `${Octicons.Star} ${item.label}`; + recommended.label = item.label; recommended.description = interpreterPath; const suggestions = [ expectedEnterInterpreterPathSuggestion, + { kind: QuickPickItemKind.Separator, label: '' }, defaultInterpreterPathSuggestion, { kind: QuickPickItemKind.Separator, label: EnvGroups.Recommended }, recommended, @@ -628,7 +707,7 @@ suite('Set Interpreter Command', () => { const state: InterpreterStateArgs = { path: 'some path', workspace: undefined }; const multiStepInput = TypeMoq.Mock.ofType>(); const recommended = cloneDeep(item); - recommended.label = `${Octicons.Star} ${item.label}`; + recommended.label = item.label; recommended.description = interpreterPath; const separator = { label: EnvGroups.Recommended, kind: QuickPickItemKind.Separator }; @@ -639,7 +718,13 @@ suite('Set Interpreter Command', () => { alwaysShow: true, }; - const suggestions = [expectedEnterInterpreterPathSuggestion, defaultPathSuggestion, separator, recommended]; + const suggestions = [ + expectedEnterInterpreterPathSuggestion, + { kind: QuickPickItemKind.Separator, label: '' }, + defaultPathSuggestion, + separator, + recommended, + ]; const expectedParameters: IQuickPickParameters = { placeholder: `Selected Interpreter: ${currentPythonPath}`, items: suggestions, @@ -666,8 +751,14 @@ suite('Set Interpreter Command', () => { delete actualParameters!.initialize; delete actualParameters!.customButtonSetups; delete actualParameters!.onChangeItem; - const activeItem = await actualParameters!.activeItem; - assert.deepStrictEqual(activeItem, recommended); + if (typeof actualParameters!.activeItem === 'function') { + const activeItem = await actualParameters!.activeItem(({ items: suggestions } as unknown) as QuickPick< + QuickPickType + >); + assert.deepStrictEqual(activeItem, recommended); + } else { + assert.ok(false, 'Not a function'); + } delete actualParameters!.activeItem; assert.deepStrictEqual(actualParameters, expectedParameters, 'Params not equal'); @@ -767,7 +858,7 @@ suite('Set Interpreter Command', () => { await sleep(1); const recommended = cloneDeep(refreshedItem); - recommended.label = `${Octicons.Star} ${refreshedItem.label}`; + recommended.label = refreshedItem.label; recommended.description = `${interpreterPath} - ${Common.recommended}`; assert.deepStrictEqual( quickPick, @@ -885,7 +976,7 @@ suite('Set Interpreter Command', () => { // eslint-disable-next-line @typescript-eslint/no-explicit-any await step!(multiStepInput.object as any, state); - assert( + assert.ok( _enterOrBrowseInterpreterPath.calledOnceWith(multiStepInput.object, { path: undefined, workspace: undefined, @@ -961,6 +1052,7 @@ suite('Set Interpreter Command', () => { openLabel: InterpreterQuickPickList.browsePath.openButtonLabel, canSelectMany: false, title: InterpreterQuickPickList.browsePath.title, + defaultUri: undefined, }; const multiStepInput = TypeMoq.Mock.ofType>(); multiStepInput.setup((i) => i.showQuickPick(TypeMoq.It.isAny())).returns(() => Promise.resolve(items[0])); @@ -982,6 +1074,27 @@ suite('Set Interpreter Command', () => { openLabel: InterpreterQuickPickList.browsePath.openButtonLabel, canSelectMany: false, title: InterpreterQuickPickList.browsePath.title, + defaultUri: undefined, + }; + multiStepInput.setup((i) => i.showQuickPick(TypeMoq.It.isAny())).returns(() => Promise.resolve(items[0])); + appShell.setup((a) => a.showOpenDialog(expectedParams)).verifiable(TypeMoq.Times.once()); + platformService.setup((p) => p.isWindows).returns(() => false); + + await setInterpreterCommand._enterOrBrowseInterpreterPath(multiStepInput.object, state).ignoreErrors(); + + appShell.verifyAll(); + }); + + test('If `Browse...` option is selected with workspace, file browser opens at workspace root', async () => { + const workspaceUri = Uri.parse('file:///workspace/root'); + const state: InterpreterStateArgs = { path: undefined, workspace: workspaceUri }; + const multiStepInput = TypeMoq.Mock.ofType>(); + const expectedParams = { + filters: undefined, + openLabel: InterpreterQuickPickList.browsePath.openButtonLabel, + canSelectMany: false, + title: InterpreterQuickPickList.browsePath.title, + defaultUri: workspaceUri, }; multiStepInput.setup((i) => i.showQuickPick(TypeMoq.It.isAny())).returns(() => Promise.resolve(items[0])); appShell.setup((a) => a.showOpenDialog(expectedParams)).verifiable(TypeMoq.Times.once()); @@ -1035,6 +1148,7 @@ suite('Set Interpreter Command', () => { openLabel: InterpreterQuickPickList.browsePath.openButtonLabel, canSelectMany: false, title: InterpreterQuickPickList.browsePath.title, + defaultUri: undefined, }; multiStepInput .setup((i) => i.showQuickPick(TypeMoq.It.isAny())) @@ -1436,9 +1550,9 @@ suite('Set Interpreter Command', () => { expect(inputStep).to.not.equal(undefined, ''); - assert(pickInterpreter.notCalled); + assert.ok(pickInterpreter.notCalled); await inputStep(); - assert(pickInterpreter.calledOnce); + assert.ok(pickInterpreter.calledOnce); }); }); }); diff --git a/src/test/configuration/interpreterSelector/interpreterSelector.unit.test.ts b/src/test/configuration/interpreterSelector/interpreterSelector.unit.test.ts index 32ba68e03c70..2ec20be66990 100644 --- a/src/test/configuration/interpreterSelector/interpreterSelector.unit.test.ts +++ b/src/test/configuration/interpreterSelector/interpreterSelector.unit.test.ts @@ -14,6 +14,7 @@ import { EnvironmentTypeComparer } from '../../../client/interpreter/configurati import { InterpreterSelector } from '../../../client/interpreter/configuration/interpreterSelector/interpreterSelector'; import { IInterpreterComparer, IInterpreterQuickPickItem } from '../../../client/interpreter/configuration/types'; import { IInterpreterHelper, IInterpreterService, WorkspacePythonPath } from '../../../client/interpreter/contracts'; +import { PythonEnvType } from '../../../client/pythonEnvironments/base/info'; import { EnvironmentType, PythonEnvironment } from '../../../client/pythonEnvironments/info'; import { getOSType, OSType } from '../../common'; @@ -139,12 +140,14 @@ suite('Interpreters - selector', () => { envPath: path.join('path', 'to', 'another', 'workspace', '.venv'), path: path.join('path', 'to', 'another', 'workspace', '.venv', 'bin', 'python'), envType: EnvironmentType.Venv, + type: PythonEnvType.Virtual, }, { displayName: 'two', envPath: path.join(workspacePath, '.venv'), path: path.join(workspacePath, '.venv', 'bin', 'python'), envType: EnvironmentType.Venv, + type: PythonEnvType.Virtual, }, { displayName: 'three', @@ -158,6 +161,7 @@ suite('Interpreters - selector', () => { path: path.join('a', 'conda', 'environment'), envName: 'conda-env', envType: EnvironmentType.Conda, + type: PythonEnvType.Conda, }, ].map((item) => ({ ...info, ...item })); diff --git a/src/test/debugger/common/constants.ts b/src/test/debugger/common/constants.ts deleted file mode 100644 index a9bcc64f1a24..000000000000 --- a/src/test/debugger/common/constants.ts +++ /dev/null @@ -1,7 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; - -// Sometimes PTVSD can take a while for thread & other events to be reported. -export const DEBUGGER_TIMEOUT = 20000; diff --git a/src/test/debugger/common/protocolparser.test.ts b/src/test/debugger/common/protocolparser.test.ts deleted file mode 100644 index 117a58a7bc66..000000000000 --- a/src/test/debugger/common/protocolparser.test.ts +++ /dev/null @@ -1,70 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -import { expect } from 'chai'; -import { PassThrough } from 'stream'; -import { createDeferred } from '../../../client/common/utils/async'; -import { ProtocolParser } from '../../../client/debugger/extension/helpers/protocolParser'; -import { sleep } from '../../common'; - -suite('Debugging - Protocol Parser', () => { - test('Test request, response and event messages', async () => { - const stream = new PassThrough(); - - const protocolParser = new ProtocolParser(); - protocolParser.connect(stream); - let messagesDetected = 0; - protocolParser.on('data', () => (messagesDetected += 1)); - const requestDetected = new Promise((resolve) => { - protocolParser.on('request_initialize', () => resolve(true)); - }); - const responseDetected = new Promise((resolve) => { - protocolParser.on('response_initialize', () => resolve(true)); - }); - const eventDetected = new Promise((resolve) => { - protocolParser.on('event_initialized', () => resolve(true)); - }); - - stream.write( - 'Content-Length: 289\r\n\r\n{"command":"initialize","arguments":{"clientID":"vscode","adapterID":"pythonExperiment","pathFormat":"path","linesStartAt1":true,"columnsStartAt1":true,"supportsVariableType":true,"supportsVariablePaging":true,"supportsRunInTerminalRequest":true,"locale":"en-us"},"type":"request","seq":1}', - ); - await expect(requestDetected).to.eventually.equal(true, 'request not parsed'); - - stream.write( - 'Content-Length: 265\r\n\r\n{"seq":1,"type":"response","request_seq":1,"command":"initialize","success":true,"body":{"supportsEvaluateForHovers":false,"supportsConditionalBreakpoints":true,"supportsConfigurationDoneRequest":true,"supportsFunctionBreakpoints":false,"supportsSetVariable":true}}', - ); - await expect(responseDetected).to.eventually.equal(true, 'response not parsed'); - - stream.write('Content-Length: 63\r\n\r\n{"type": "event", "seq": 1, "event": "initialized", "body": {}}'); - await expect(eventDetected).to.eventually.equal(true, 'event not parsed'); - - expect(messagesDetected).to.be.equal(3, 'incorrect number of protocol messages'); - }); - test('Ensure messages are not received after disposing the parser', async () => { - const stream = new PassThrough(); - - const protocolParser = new ProtocolParser(); - protocolParser.connect(stream); - let messagesDetected = 0; - protocolParser.on('data', () => (messagesDetected += 1)); - const requestDetected = new Promise((resolve) => { - protocolParser.on('request_initialize', () => resolve(true)); - }); - stream.write( - 'Content-Length: 289\r\n\r\n{"command":"initialize","arguments":{"clientID":"vscode","adapterID":"pythonExperiment","pathFormat":"path","linesStartAt1":true,"columnsStartAt1":true,"supportsVariableType":true,"supportsVariablePaging":true,"supportsRunInTerminalRequest":true,"locale":"en-us"},"type":"request","seq":1}', - ); - await expect(requestDetected).to.eventually.equal(true, 'request not parsed'); - - protocolParser.dispose(); - - const responseDetected = createDeferred(); - protocolParser.on('response_initialize', () => responseDetected.resolve(true)); - - stream.write( - 'Content-Length: 265\r\n\r\n{"seq":1,"type":"response","request_seq":1,"command":"initialize","success":true,"body":{"supportsEvaluateForHovers":false,"supportsConditionalBreakpoints":true,"supportsConfigurationDoneRequest":true,"supportsFunctionBreakpoints":false,"supportsSetVariable":true}}', - ); - // Wait for messages to go through and get parsed (unnecenssary, but add for testing edge cases). - await sleep(1000); - expect(responseDetected.completed).to.be.equal(false, 'Promise should not have resolved'); - }); -}); diff --git a/src/test/debugger/envVars.test.ts b/src/test/debugger/envVars.test.ts index c043146fe53d..8b0f55986281 100644 --- a/src/test/debugger/envVars.test.ts +++ b/src/test/debugger/envVars.test.ts @@ -16,8 +16,10 @@ import { isOs, OSType } from '../common'; import { closeActiveWindows, initialize, initializeTest, IS_MULTI_ROOT_TEST, TEST_DEBUGGER } from '../initialize'; import { UnitTestIocContainer } from '../testing/serviceRegistry'; import { normCase } from '../../client/common/platform/fs-paths'; +import { IRecommendedEnvironmentService } from '../../client/interpreter/configuration/types'; +import { RecommendedEnvironmentService } from '../../client/interpreter/configuration/recommededEnvironmentService'; -use(chaiAsPromised); +use(chaiAsPromised.default); suite('Resolving Environment Variables when Debugging', () => { let ioc: UnitTestIocContainer; @@ -53,6 +55,10 @@ suite('Resolving Environment Variables when Debugging', () => { ioc.registerFileSystemTypes(); ioc.registerVariableTypes(); ioc.registerMockProcess(); + ioc.serviceManager.addSingleton( + IRecommendedEnvironmentService, + RecommendedEnvironmentService, + ); } async function testBasicProperties(console: ConsoleType, expectedNumberOfVariables: number) { diff --git a/src/test/debugger/extension/adapter/activator.unit.test.ts b/src/test/debugger/extension/adapter/activator.unit.test.ts deleted file mode 100644 index e8c6ef74fc2a..000000000000 --- a/src/test/debugger/extension/adapter/activator.unit.test.ts +++ /dev/null @@ -1,91 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; - -import * as assert from 'assert'; -import { anything, instance, mock, verify, when } from 'ts-mockito'; -import { IExtensionSingleActivationService } from '../../../../client/activation/types'; -import { CommandManager } from '../../../../client/common/application/commandManager'; -import { DebugService } from '../../../../client/common/application/debugService'; -import { ICommandManager, IDebugService } from '../../../../client/common/application/types'; -import { ConfigurationService } from '../../../../client/common/configuration/service'; -import { IConfigurationService, IDisposableRegistry, IPythonSettings } from '../../../../client/common/types'; -import { DebugAdapterActivator } from '../../../../client/debugger/extension/adapter/activator'; -import { DebugAdapterDescriptorFactory } from '../../../../client/debugger/extension/adapter/factory'; -import { DebugSessionLoggingFactory } from '../../../../client/debugger/extension/adapter/logging'; -import { OutdatedDebuggerPromptFactory } from '../../../../client/debugger/extension/adapter/outdatedDebuggerPrompt'; -import { AttachProcessProviderFactory } from '../../../../client/debugger/extension/attachQuickPick/factory'; -import { IAttachProcessProviderFactory } from '../../../../client/debugger/extension/attachQuickPick/types'; -import { - IDebugAdapterDescriptorFactory, - IDebugSessionLoggingFactory, - IOutdatedDebuggerPromptFactory, -} from '../../../../client/debugger/extension/types'; -import { clearTelemetryReporter } from '../../../../client/telemetry'; -import { noop } from '../../../core'; - -suite('Debugging - Adapter Factory and logger Registration', () => { - let activator: IExtensionSingleActivationService; - let debugService: IDebugService; - let commandManager: ICommandManager; - let descriptorFactory: IDebugAdapterDescriptorFactory; - let loggingFactory: IDebugSessionLoggingFactory; - let debuggerPromptFactory: IOutdatedDebuggerPromptFactory; - let disposableRegistry: IDisposableRegistry; - let attachFactory: IAttachProcessProviderFactory; - let configService: IConfigurationService; - - setup(() => { - attachFactory = mock(AttachProcessProviderFactory); - - debugService = mock(DebugService); - when(debugService.onDidStartDebugSession).thenReturn(() => noop as any); - - commandManager = mock(CommandManager); - - configService = mock(ConfigurationService); - when(configService.getSettings(undefined)).thenReturn(({ - experiments: { enabled: true }, - } as any) as IPythonSettings); - - descriptorFactory = mock(DebugAdapterDescriptorFactory); - loggingFactory = mock(DebugSessionLoggingFactory); - debuggerPromptFactory = mock(OutdatedDebuggerPromptFactory); - disposableRegistry = []; - - activator = new DebugAdapterActivator( - instance(debugService), - instance(configService), - instance(commandManager), - instance(descriptorFactory), - instance(loggingFactory), - instance(debuggerPromptFactory), - disposableRegistry, - instance(attachFactory), - ); - }); - - teardown(() => { - clearTelemetryReporter(); - }); - - test('Register Debug adapter factory', async () => { - await activator.activate(); - - verify(debugService.registerDebugAdapterTrackerFactory('python', instance(loggingFactory))).once(); - verify(debugService.registerDebugAdapterTrackerFactory('python', instance(debuggerPromptFactory))).once(); - verify(debugService.registerDebugAdapterDescriptorFactory('python', instance(descriptorFactory))).once(); - }); - - test('Register a disposable item', async () => { - const disposable = { dispose: noop }; - when(debugService.registerDebugAdapterTrackerFactory(anything(), anything())).thenReturn(disposable); - when(debugService.registerDebugAdapterDescriptorFactory(anything(), anything())).thenReturn(disposable); - when(debugService.onDidStartDebugSession).thenReturn(() => disposable); - - await activator.activate(); - - assert.deepEqual(disposableRegistry, [disposable, disposable, disposable, disposable]); - }); -}); diff --git a/src/test/debugger/extension/adapter/adapter.test.ts b/src/test/debugger/extension/adapter/adapter.test.ts index 7e20d5b930b9..cd53b41102ab 100644 --- a/src/test/debugger/extension/adapter/adapter.test.ts +++ b/src/test/debugger/extension/adapter/adapter.test.ts @@ -4,7 +4,7 @@ 'use strict'; import { expect } from 'chai'; -import * as fs from 'fs-extra'; +import * as fs from '../../../../client/common/platform/fs-paths'; import * as path from 'path'; import * as vscode from 'vscode'; import { openFile } from '../../../common'; @@ -19,7 +19,7 @@ function resolveWSFile(wsRoot: string, ...filePath: string[]): string { } suite('Debugger Integration', () => { - const file = resolveWSFile(WS_ROOT, 'pythonFiles', 'debugging', 'wait_for_file.py'); + const file = resolveWSFile(WS_ROOT, 'python_files', 'debugging', 'wait_for_file.py'); const doneFile = resolveWSFile(WS_ROOT, 'should-not-exist'); const outFile = resolveWSFile(WS_ROOT, 'output.txt'); const resource = vscode.Uri.file(file); @@ -70,7 +70,7 @@ suite('Debugger Integration', () => { } const [configName, scriptArgs] = tests[kind]; test(kind, async () => { - const session = fix.resolveDebugger(configName, file, scriptArgs, workspaceRoot); + const session = await fix.resolveDebugger(configName, file, scriptArgs, workspaceRoot); await session.start(); // Any debugger ops would go here. await new Promise((r) => setTimeout(r, 300)); // 0.3 seconds @@ -93,7 +93,7 @@ suite('Debugger Integration', () => { } const [configName, scriptArgs] = tests[kind]; test(kind, async () => { - const session = fix.resolveDebugger(configName, file, scriptArgs, workspaceRoot); + const session = await fix.resolveDebugger(configName, file, scriptArgs, workspaceRoot); const bp = session.addBreakpoint(file, 21); // line: "time.sleep()" await session.start(); await session.waitForBreakpoint(bp); diff --git a/src/test/debugger/extension/adapter/factory.unit.test.ts b/src/test/debugger/extension/adapter/factory.unit.test.ts index a1e57d44f6cb..50984327e40d 100644 --- a/src/test/debugger/extension/adapter/factory.unit.test.ts +++ b/src/test/debugger/extension/adapter/factory.unit.test.ts @@ -6,6 +6,7 @@ import * as assert from 'assert'; import { expect, use } from 'chai'; import * as chaiAsPromised from 'chai-as-promised'; +import * as fs from '../../../../client/common/platform/fs-paths'; import * as path from 'path'; import * as sinon from 'sinon'; import rewiremock from 'rewiremock'; @@ -22,13 +23,13 @@ import { IInterpreterService } from '../../../../client/interpreter/contracts'; import { InterpreterService } from '../../../../client/interpreter/interpreterService'; import { EnvironmentType } from '../../../../client/pythonEnvironments/info'; import { clearTelemetryReporter } from '../../../../client/telemetry'; -import { EventName } from '../../../../client/telemetry/constants'; import * as windowApis from '../../../../client/common/vscodeApis/windowApis'; import { PersistentState, PersistentStateFactory } from '../../../../client/common/persistentState'; import { ICommandManager } from '../../../../client/common/application/types'; import { CommandManager } from '../../../../client/common/application/commandManager'; +import * as pythonDebugger from '../../../../client/debugger/pythonDebugger'; -use(chaiAsPromised); +use(chaiAsPromised.default); suite('Debugging - Adapter Factory', () => { let factory: IDebugAdapterDescriptorFactory; @@ -36,10 +37,13 @@ suite('Debugging - Adapter Factory', () => { let stateFactory: IPersistentStateFactory; let state: PersistentState; let showErrorMessageStub: sinon.SinonStub; + let readJSONSyncStub: sinon.SinonStub; let commandManager: ICommandManager; + let getDebugpyPathStub: sinon.SinonStub; const nodeExecutable = undefined; - const debugAdapterPath = path.join(EXTENSION_ROOT_DIR, 'pythonFiles', 'lib', 'python', 'debugpy', 'adapter'); + const debugpyPath = path.join(EXTENSION_ROOT_DIR, 'python_files', 'lib', 'python', 'debugpy'); + const debugAdapterPath = path.join(debugpyPath, 'adapter'); const pythonPath = path.join('path', 'to', 'python', 'interpreter'); const interpreter = { architecture: Architecture.Unknown, @@ -66,12 +70,15 @@ suite('Debugging - Adapter Factory', () => { setup(() => { process.env.VSC_PYTHON_UNIT_TEST = undefined; process.env.VSC_PYTHON_CI_TEST = undefined; + readJSONSyncStub = sinon.stub(fs, 'readJSONSync'); + readJSONSyncStub.returns({ enableTelemetry: true }); rewiremock.enable(); rewiremock('@vscode/extension-telemetry').with({ default: Reporter }); stateFactory = mock(PersistentStateFactory); state = mock(PersistentState) as PersistentState; commandManager = mock(CommandManager); - + getDebugpyPathStub = sinon.stub(pythonDebugger, 'getDebugpyPath'); + getDebugpyPathStub.resolves(debugpyPath); showErrorMessageStub = sinon.stub(windowApis, 'showErrorMessage'); when( @@ -261,16 +268,12 @@ suite('Debugging - Adapter Factory', () => { test('Send attach to local process telemetry if attaching to a local process', async () => { const session = createSession({ request: 'attach', processId: 1234 }); await factory.createDebugAdapterDescriptor(session, nodeExecutable); - - assert.ok(Reporter.eventNames.includes(EventName.DEBUGGER_ATTACH_TO_LOCAL_PROCESS)); }); test("Don't send any telemetry if not attaching to a local process", async () => { const session = createSession({}); await factory.createDebugAdapterDescriptor(session, nodeExecutable); - - assert.ok(Reporter.eventNames.includes(EventName.DEBUG_ADAPTER_USING_WHEELS_PATH)); }); test('Use "debugAdapterPath" when specified', async () => { diff --git a/src/test/debugger/extension/adapter/outdatedDebuggerPrompt.unit.test.ts b/src/test/debugger/extension/adapter/outdatedDebuggerPrompt.unit.test.ts index 0ab094119a5c..9f9497317417 100644 --- a/src/test/debugger/extension/adapter/outdatedDebuggerPrompt.unit.test.ts +++ b/src/test/debugger/extension/adapter/outdatedDebuggerPrompt.unit.test.ts @@ -82,7 +82,7 @@ suite('Debugging - Outdated Debugger Prompt tests.', () => { // First call should show info once sinon.assert.calledOnce(showInformationMessageStub); - assert(prompter); + assert.ok(prompter); prompter!.onDidSendMessage!(ptvsdOutputEvent); // Can't use deferred promise here @@ -104,7 +104,7 @@ suite('Debugging - Outdated Debugger Prompt tests.', () => { const session = createSession(); const prompter = await promptFactory.createDebugAdapterTracker(session); - assert(prompter); + assert.ok(prompter); prompter!.onDidSendMessage!(ptvsdOutputEvent); await deferred.promise; @@ -130,7 +130,7 @@ suite('Debugging - Outdated Debugger Prompt tests.', () => { const session = createSession(); const prompter = await promptFactory.createDebugAdapterTracker(session); - assert(prompter); + assert.ok(prompter); prompter!.onDidSendMessage!(debugpyOutputEvent); // Can't use deferred promise here @@ -168,7 +168,7 @@ suite('Debugging - Outdated Debugger Prompt tests.', () => { const session = createSession(); const prompter = await promptFactory.createDebugAdapterTracker(session); - assert(prompter); + assert.ok(prompter); prompter!.onDidSendMessage!(message); // Can't use deferred promise here diff --git a/src/test/debugger/extension/adapter/remoteLaunchers.unit.test.ts b/src/test/debugger/extension/adapter/remoteLaunchers.unit.test.ts index aa520f66faa6..e8e2cbd5d15d 100644 --- a/src/test/debugger/extension/adapter/remoteLaunchers.unit.test.ts +++ b/src/test/debugger/extension/adapter/remoteLaunchers.unit.test.ts @@ -5,7 +5,6 @@ import { expect } from 'chai'; import * as path from 'path'; -import { EXTENSION_ROOT_DIR } from '../../../../client/common/constants'; import '../../../../client/common/extensions'; import * as launchers from '../../../../client/debugger/extension/adapter/remoteLaunchers'; @@ -24,7 +23,7 @@ suite('External debugpy Debugger Launcher', () => { ].forEach((testParams) => { suite(testParams.testName, async () => { test('Test remote debug launcher args (and do not wait for debugger to attach)', async () => { - const args = launchers.getDebugpyLauncherArgs( + const args = await launchers.getDebugpyLauncherArgs( { host: 'something', port: 1234, @@ -36,7 +35,7 @@ suite('External debugpy Debugger Launcher', () => { expect(args).to.be.deep.equal(expectedArgs); }); test('Test remote debug launcher args (and wait for debugger to attach)', async () => { - const args = launchers.getDebugpyLauncherArgs( + const args = await launchers.getDebugpyLauncherArgs( { host: 'something', port: 1234, @@ -50,12 +49,3 @@ suite('External debugpy Debugger Launcher', () => { }); }); }); - -suite('Path To Debugger Package', () => { - const pathToPythonLibDir = path.join(EXTENSION_ROOT_DIR, 'pythonFiles', 'lib', 'python'); - test('Path to debugpy debugger package', () => { - const actual = launchers.getDebugpyPackagePath(); - const expected = path.join(pathToPythonLibDir, 'debugpy'); - expect(actual).to.be.deep.equal(expected); - }); -}); diff --git a/src/test/debugger/extension/configuration/debugConfigurationService.unit.test.ts b/src/test/debugger/extension/configuration/debugConfigurationService.unit.test.ts index 7c7977ab8480..ae13ad375371 100644 --- a/src/test/debugger/extension/configuration/debugConfigurationService.unit.test.ts +++ b/src/test/debugger/extension/configuration/debugConfigurationService.unit.test.ts @@ -6,36 +6,20 @@ import { expect } from 'chai'; import * as typemoq from 'typemoq'; import { DebugConfiguration, Uri } from 'vscode'; -import { IMultiStepInputFactory, MultiStepInput } from '../../../../client/common/utils/multiStepInput'; import { PythonDebugConfigurationService } from '../../../../client/debugger/extension/configuration/debugConfigurationService'; import { IDebugConfigurationResolver } from '../../../../client/debugger/extension/configuration/types'; -import { DebugConfigurationState } from '../../../../client/debugger/extension/types'; import { AttachRequestArguments, LaunchRequestArguments } from '../../../../client/debugger/types'; suite('Debugging - Configuration Service', () => { let attachResolver: typemoq.IMock>; let launchResolver: typemoq.IMock>; let configService: TestPythonDebugConfigurationService; - let multiStepFactory: typemoq.IMock; - class TestPythonDebugConfigurationService extends PythonDebugConfigurationService { - public static async pickDebugConfiguration( - input: MultiStepInput, - state: DebugConfigurationState, - ) { - return PythonDebugConfigurationService.pickDebugConfiguration(input, state); - } - } + class TestPythonDebugConfigurationService extends PythonDebugConfigurationService {} setup(() => { attachResolver = typemoq.Mock.ofType>(); launchResolver = typemoq.Mock.ofType>(); - multiStepFactory = typemoq.Mock.ofType(); - - configService = new TestPythonDebugConfigurationService( - attachResolver.object, - launchResolver.object, - multiStepFactory.object, - ); + configService = new TestPythonDebugConfigurationService(attachResolver.object, launchResolver.object); }); test('Should use attach resolver when passing attach config', async () => { const config = ({ @@ -86,96 +70,4 @@ suite('Debugging - Configuration Service', () => { launchResolver.verifyAll(); }); }); - test('Picker should be displayed', async () => { - const state = ({ configs: [], folder: {}, token: undefined } as unknown) as DebugConfigurationState; - const multiStepInput = typemoq.Mock.ofType>(); - multiStepInput - .setup((i) => i.showQuickPick(typemoq.It.isAny())) - .returns(() => Promise.resolve(undefined)) - .verifiable(typemoq.Times.once()); - - await TestPythonDebugConfigurationService.pickDebugConfiguration(multiStepInput.object, state); - - multiStepInput.verifyAll(); - }); - test('Existing Configuration items must be removed before displaying picker', async () => { - const state = ({ configs: [1, 2, 3], folder: {}, token: undefined } as unknown) as DebugConfigurationState; - const multiStepInput = typemoq.Mock.ofType>(); - multiStepInput - .setup((i) => i.showQuickPick(typemoq.It.isAny())) - .returns(() => Promise.resolve(undefined)) - .verifiable(typemoq.Times.once()); - - await TestPythonDebugConfigurationService.pickDebugConfiguration(multiStepInput.object, state); - - multiStepInput.verifyAll(); - expect(Object.keys(state.config)).to.be.lengthOf(0); - }); - test('Ensure generated config is returned', async () => { - const expectedConfig = { yes: 'Updated' }; - const multiStepInput = { - run: (_: unknown, state: DebugConfiguration) => { - Object.assign(state.config, expectedConfig); - return Promise.resolve(); - }, - }; - multiStepFactory - .setup((f) => f.create()) - .returns(() => multiStepInput as MultiStepInput) - .verifiable(typemoq.Times.once()); - TestPythonDebugConfigurationService.pickDebugConfiguration = (_, state) => { - Object.assign(state.config, expectedConfig); - return Promise.resolve(); - }; - const config = await configService.provideDebugConfigurations!(({} as unknown) as undefined); - - multiStepFactory.verifyAll(); - expect(config).to.deep.equal([expectedConfig]); - }); - test('Ensure `undefined` is returned if QuickPick is cancelled', async () => { - const multiStepInput = { - run: (_: unknown, _state: DebugConfiguration) => Promise.resolve(), - }; - const folder = { name: '1', index: 0, uri: Uri.parse('1234') }; - multiStepFactory - .setup((f) => f.create()) - .returns(() => multiStepInput as MultiStepInput) - .verifiable(typemoq.Times.once()); - const config = await configService.resolveDebugConfiguration(folder, {} as DebugConfiguration); - - multiStepFactory.verifyAll(); - - expect(config).to.equal(undefined, `Config should be undefined`); - }); - test('Use cached debug configuration', async () => { - const folder = { name: '1', index: 0, uri: Uri.parse('1234') }; - const expectedConfig = { - name: 'File', - type: 'python', - request: 'launch', - program: '${file}', - console: 'integratedTerminal', - }; - const multiStepInput = { - run: (_: unknown, state: DebugConfiguration) => { - Object.assign(state.config, expectedConfig); - return Promise.resolve(); - }, - }; - multiStepFactory - .setup((f) => f.create()) - .returns(() => multiStepInput as MultiStepInput) - .verifiable(typemoq.Times.once()); // this should be called only once. - - launchResolver - .setup((a) => a.resolveDebugConfiguration(typemoq.It.isAny(), typemoq.It.isAny(), typemoq.It.isAny())) - .returns(() => Promise.resolve(expectedConfig as LaunchRequestArguments)) - .verifiable(typemoq.Times.exactly(2)); // this should be called twice with the same config. - - await configService.resolveDebugConfiguration(folder, {} as DebugConfiguration); - await configService.resolveDebugConfiguration(folder, {} as DebugConfiguration); - - multiStepFactory.verifyAll(); - launchResolver.verifyAll(); - }); }); diff --git a/src/test/debugger/extension/configuration/launch.json/completionProvider.unit.test.ts b/src/test/debugger/extension/configuration/launch.json/completionProvider.unit.test.ts deleted file mode 100644 index a850d50150ae..000000000000 --- a/src/test/debugger/extension/configuration/launch.json/completionProvider.unit.test.ts +++ /dev/null @@ -1,138 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; - -import * as assert from 'assert'; -import { deepEqual, instance, mock, verify } from 'ts-mockito'; -import * as typemoq from 'typemoq'; -import { - CancellationTokenSource, - CompletionItem, - CompletionItemKind, - Position, - SnippetString, - TextDocument, - Uri, -} from 'vscode'; -import { LanguageService } from '../../../../../client/common/application/languageService'; -import { ILanguageService } from '../../../../../client/common/application/types'; -import { DebugConfigStrings } from '../../../../../client/common/utils/localize'; -import { LaunchJsonCompletionProvider } from '../../../../../client/debugger/extension/configuration/launch.json/completionProvider'; - -suite('Debugging - launch.json Completion Provider', () => { - let completionProvider: LaunchJsonCompletionProvider; - let languageService: ILanguageService; - - setup(() => { - languageService = mock(LanguageService); - completionProvider = new LaunchJsonCompletionProvider(instance(languageService), []); - }); - test('Activation will register the completion provider', async () => { - await completionProvider.activate(); - verify( - languageService.registerCompletionItemProvider(deepEqual({ language: 'json' }), completionProvider), - ).once(); - verify( - languageService.registerCompletionItemProvider(deepEqual({ language: 'jsonc' }), completionProvider), - ).once(); - }); - test('Cannot provide completions for non launch.json files', () => { - const document = typemoq.Mock.ofType(); - const position = new Position(0, 0); - document.setup((doc) => doc.uri).returns(() => Uri.file(__filename)); - assert.strictEqual(LaunchJsonCompletionProvider.canProvideCompletions(document.object, position), false); - - document.reset(); - document.setup((doc) => doc.uri).returns(() => Uri.file('settings.json')); - assert.strictEqual(LaunchJsonCompletionProvider.canProvideCompletions(document.object, position), false); - }); - function testCanProvideCompletions(position: Position, offset: number, json: string, expectedValue: boolean) { - const document = typemoq.Mock.ofType(); - document.setup((doc) => doc.getText(typemoq.It.isAny())).returns(() => json); - document.setup((doc) => doc.uri).returns(() => Uri.file('launch.json')); - document.setup((doc) => doc.offsetAt(typemoq.It.isAny())).returns(() => offset); - const canProvideCompletions = LaunchJsonCompletionProvider.canProvideCompletions(document.object, position); - assert.strictEqual(canProvideCompletions, expectedValue); - } - test('Cannot provide completions when there is no configurations section in json', () => { - const position = new Position(0, 0); - const config = `{ - "version": "0.1.0" -}`; - testCanProvideCompletions(position, 1, config as string, false); - }); - test('Cannot provide completions when cursor position is not in configurations array', () => { - const position = new Position(0, 0); - const json = `{ - "version": "0.1.0", - "configurations": [] -}`; - testCanProvideCompletions(position, 10, json, false); - }); - test('Cannot provide completions when cursor position is in an empty configurations array', () => { - const position = new Position(0, 0); - const json = `{ - "version": "0.1.0", - "configurations": [ - # Cursor Position - ] -}`; - testCanProvideCompletions(position, json.indexOf('# Cursor Position'), json, true); - }); - test('No Completions for non launch.json', async () => { - const document = typemoq.Mock.ofType(); - document.setup((doc) => doc.uri).returns(() => Uri.file('settings.json')); - const { token } = new CancellationTokenSource(); - const position = new Position(0, 0); - - const completions = await completionProvider.provideCompletionItems(document.object, position, token); - - assert.strictEqual(completions.length, 0); - }); - test('No Completions for files ending with launch.json', async () => { - const document = typemoq.Mock.ofType(); - document.setup((doc) => doc.uri).returns(() => Uri.file('x-launch.json')); - const { token } = new CancellationTokenSource(); - const position = new Position(0, 0); - - const completions = await completionProvider.provideCompletionItems(document.object, position, token); - - assert.strictEqual(completions.length, 0); - }); - test('Get Completions', async () => { - const json = `{ - "version": "0.1.0", - "configurations": [ - # Cursor Position - ] -}`; - - const document = typemoq.Mock.ofType(); - document.setup((doc) => doc.getText(typemoq.It.isAny())).returns(() => json); - document.setup((doc) => doc.uri).returns(() => Uri.file('launch.json')); - document.setup((doc) => doc.offsetAt(typemoq.It.isAny())).returns(() => json.indexOf('# Cursor Position')); - const position = new Position(0, 0); - const { token } = new CancellationTokenSource(); - - const completions = await completionProvider.provideCompletionItems(document.object, position, token); - - assert.strictEqual(completions.length, 1); - - const expectedCompletionItem: CompletionItem = { - command: { - command: 'python.SelectAndInsertDebugConfiguration', - title: DebugConfigStrings.launchJsonCompletions.description, - arguments: [document.object, position, token], - }, - documentation: DebugConfigStrings.launchJsonCompletions.description, - sortText: 'AAAA', - preselect: true, - kind: CompletionItemKind.Enum, - label: DebugConfigStrings.launchJsonCompletions.label, - insertText: new SnippetString(), - }; - - assert.deepEqual(completions[0], expectedCompletionItem); - }); -}); diff --git a/src/test/debugger/extension/configuration/launch.json/launchJsonReader.unit.test.ts b/src/test/debugger/extension/configuration/launch.json/launchJsonReader.unit.test.ts new file mode 100644 index 000000000000..4241f3526f1a --- /dev/null +++ b/src/test/debugger/extension/configuration/launch.json/launchJsonReader.unit.test.ts @@ -0,0 +1,86 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import * as sinon from 'sinon'; +import * as path from 'path'; +import { Uri } from 'vscode'; +import { assert } from 'chai'; +import * as fs from '../../../../../client/common/platform/fs-paths'; +import { getConfigurationsForWorkspace } from '../../../../../client/debugger/extension/configuration/launch.json/launchJsonReader'; +import * as vscodeApis from '../../../../../client/common/vscodeApis/workspaceApis'; + +suite('Launch Json Reader', () => { + let pathExistsStub: sinon.SinonStub; + let readFileStub: sinon.SinonStub; + let getConfigurationStub: sinon.SinonStub; + const workspacePath = 'path/to/workspace'; + const workspaceFolder = { + name: 'workspace', + uri: Uri.file(workspacePath), + index: 0, + }; + + setup(() => { + pathExistsStub = sinon.stub(fs, 'pathExists'); + readFileStub = sinon.stub(fs, 'readFile'); + getConfigurationStub = sinon.stub(vscodeApis, 'getConfiguration'); + }); + + teardown(() => { + sinon.restore(); + }); + + test('Return the config in the launch.json file', async () => { + const launchPath = path.join(workspaceFolder.uri.fsPath, '.vscode', 'launch.json'); + pathExistsStub.withArgs(launchPath).resolves(true); + const launchJson = `{ + "version": "0.1.0", + "configurations": [ + { + "name": "Python: Launch.json", + "type": "python", + "request": "launch", + "purpose": ["debug-test"], + }, + ] + }`; + readFileStub.withArgs(launchPath, 'utf-8').returns(launchJson); + + const config = await getConfigurationsForWorkspace(workspaceFolder); + + assert.deepStrictEqual(config, [ + { + name: 'Python: Launch.json', + type: 'python', + request: 'launch', + purpose: ['debug-test'], + }, + ]); + }); + + test('If there is no launch.json return the config in the workspace file', async () => { + getConfigurationStub.withArgs('launch').returns({ + configurations: [ + { + name: 'Python: Workspace File', + type: 'python', + request: 'launch', + purpose: ['debug-test'], + }, + ], + }); + + const config = await getConfigurationsForWorkspace(workspaceFolder); + + assert.deepStrictEqual(config, [ + { + name: 'Python: Workspace File', + type: 'python', + request: 'launch', + purpose: ['debug-test'], + }, + ]); + }); +}); diff --git a/src/test/debugger/extension/configuration/launch.json/updaterServer.unit.test.ts b/src/test/debugger/extension/configuration/launch.json/updaterServer.unit.test.ts deleted file mode 100644 index b2addd24267b..000000000000 --- a/src/test/debugger/extension/configuration/launch.json/updaterServer.unit.test.ts +++ /dev/null @@ -1,34 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; - -import { instance, mock, verify } from 'ts-mockito'; -import { CommandManager } from '../../../../../client/common/application/commandManager'; -import { ICommandManager } from '../../../../../client/common/application/types'; -import { PythonDebugConfigurationService } from '../../../../../client/debugger/extension/configuration/debugConfigurationService'; -import { LaunchJsonUpdaterService } from '../../../../../client/debugger/extension/configuration/launch.json/updaterService'; -import { LaunchJsonUpdaterServiceHelper } from '../../../../../client/debugger/extension/configuration/launch.json/updaterServiceHelper'; -import { IDebugConfigurationService } from '../../../../../client/debugger/extension/types'; - -suite('Debugging - launch.json Updater Service', () => { - let helper: LaunchJsonUpdaterServiceHelper; - let commandManager: ICommandManager; - let debugConfigService: IDebugConfigurationService; - setup(() => { - commandManager = mock(CommandManager); - debugConfigService = mock(PythonDebugConfigurationService); - helper = new LaunchJsonUpdaterServiceHelper(instance(debugConfigService)); - }); - test('Activation will register the required commands', async () => { - const service = new LaunchJsonUpdaterService([], instance(debugConfigService)); - await service.activate(); - verify( - commandManager.registerCommand( - 'python.SelectAndInsertDebugConfiguration', - helper.selectAndInsertDebugConfig, - helper, - ), - ); - }); -}); diff --git a/src/test/debugger/extension/configuration/launch.json/updaterServerHelper.unit.test.ts b/src/test/debugger/extension/configuration/launch.json/updaterServerHelper.unit.test.ts deleted file mode 100644 index 53118d68025e..000000000000 --- a/src/test/debugger/extension/configuration/launch.json/updaterServerHelper.unit.test.ts +++ /dev/null @@ -1,496 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; - -import * as assert from 'assert'; -import * as sinon from 'sinon'; -import { instance, mock, when } from 'ts-mockito'; -import * as typemoq from 'typemoq'; -import { - CancellationTokenSource, - DebugConfiguration, - Position, - Range, - TextDocument, - TextEditor, - TextLine, - Uri, -} from 'vscode'; -import { PythonDebugConfigurationService } from '../../../../../client/debugger/extension/configuration/debugConfigurationService'; -import { LaunchJsonUpdaterServiceHelper } from '../../../../../client/debugger/extension/configuration/launch.json/updaterServiceHelper'; -import { IDebugConfigurationService } from '../../../../../client/debugger/extension/types'; -import * as windowApis from '../../../../../client/common/vscodeApis/windowApis'; -import * as workspaceApis from '../../../../../client/common/vscodeApis/workspaceApis'; -import * as commandApis from '../../../../../client/common/vscodeApis/commandApis'; - -type LaunchJsonSchema = { - version: string; - configurations: DebugConfiguration[]; -}; - -suite('Debugging - launch.json Updater Service', () => { - let helper: LaunchJsonUpdaterServiceHelper; - let getWorkspaceFolderStub: sinon.SinonStub; - let getActiveTextEditorStub: sinon.SinonStub; - let applyEditStub: sinon.SinonStub; - let executeCommandStub: sinon.SinonStub; - let debugConfigService: IDebugConfigurationService; - - const sandbox = sinon.createSandbox(); - setup(() => { - getWorkspaceFolderStub = sinon.stub(workspaceApis, 'getWorkspaceFolder'); - getActiveTextEditorStub = sinon.stub(windowApis, 'getActiveTextEditor'); - applyEditStub = sinon.stub(workspaceApis, 'applyEdit'); - executeCommandStub = sinon.stub(commandApis, 'executeCommand'); - - debugConfigService = mock(PythonDebugConfigurationService); - sandbox.stub(LaunchJsonUpdaterServiceHelper, 'isCommaImmediatelyBeforeCursor').returns(false); - helper = new LaunchJsonUpdaterServiceHelper(instance(debugConfigService)); - }); - teardown(() => { - sandbox.restore(); - sinon.restore(); - }); - - test('Configuration Array is detected as being empty', async () => { - const document = typemoq.Mock.ofType(); - const config: LaunchJsonSchema = { - version: '', - configurations: [], - }; - document.setup((doc) => doc.getText(typemoq.It.isAny())).returns(() => JSON.stringify(config)); - - const isEmpty = LaunchJsonUpdaterServiceHelper.isConfigurationArrayEmpty(document.object); - assert.strictEqual(isEmpty, true); - }); - test('Configuration Array is not empty', async () => { - const document = typemoq.Mock.ofType(); - const config: LaunchJsonSchema = { - version: '', - configurations: [ - { - name: '', - request: 'launch', - type: 'python', - }, - ], - }; - document.setup((doc) => doc.getText(typemoq.It.isAny())).returns(() => JSON.stringify(config)); - - const isEmpty = LaunchJsonUpdaterServiceHelper.isConfigurationArrayEmpty(document.object); - assert.strictEqual(isEmpty, false); - }); - test('Cursor is not positioned in the configurations array', async () => { - const document = typemoq.Mock.ofType(); - const config: LaunchJsonSchema = { - version: '', - configurations: [ - { - name: '', - request: 'launch', - type: 'python', - }, - ], - }; - document.setup((doc) => doc.getText(typemoq.It.isAny())).returns(() => JSON.stringify(config)); - document.setup((doc) => doc.offsetAt(typemoq.It.isAny())).returns(() => 10); - - const cursorPosition = LaunchJsonUpdaterServiceHelper.getCursorPositionInConfigurationsArray( - document.object, - new Position(0, 0), - ); - assert.strictEqual(cursorPosition, undefined); - }); - test('Cursor is positioned in the empty configurations array', async () => { - const document = typemoq.Mock.ofType(); - const json = `{ - "version": "0.1.0", - "configurations": [ - # Cursor Position - ] - }`; - document.setup((doc) => doc.getText(typemoq.It.isAny())).returns(() => json); - document.setup((doc) => doc.offsetAt(typemoq.It.isAny())).returns(() => json.indexOf('#')); - - const cursorPosition = LaunchJsonUpdaterServiceHelper.getCursorPositionInConfigurationsArray( - document.object, - new Position(0, 0), - ); - assert.strictEqual(cursorPosition, 'InsideEmptyArray'); - }); - test('Cursor is positioned before an item in the configurations array', async () => { - const document = typemoq.Mock.ofType(); - const json = `{ - "version": "0.1.0", - "configurations": [ - { - "name":"wow" - } - ] -}`; - document.setup((doc) => doc.getText(typemoq.It.isAny())).returns(() => json); - document.setup((doc) => doc.offsetAt(typemoq.It.isAny())).returns(() => json.lastIndexOf('{') - 1); - - const cursorPosition = LaunchJsonUpdaterServiceHelper.getCursorPositionInConfigurationsArray( - document.object, - new Position(0, 0), - ); - assert.strictEqual(cursorPosition, 'BeforeItem'); - }); - test('Cursor is positioned before an item in the middle of the configurations array', async () => { - const document = typemoq.Mock.ofType(); - const json = `{ - "version": "0.1.0", - "configurations": [ - { - "name":"wow" - },{ - "name":"wow" - } - ] -}`; - document.setup((doc) => doc.getText(typemoq.It.isAny())).returns(() => json); - document.setup((doc) => doc.offsetAt(typemoq.It.isAny())).returns(() => json.indexOf(',{') + 1); - - const cursorPosition = LaunchJsonUpdaterServiceHelper.getCursorPositionInConfigurationsArray( - document.object, - new Position(0, 0), - ); - assert.strictEqual(cursorPosition, 'BeforeItem'); - }); - test('Cursor is positioned after an item in the configurations array', async () => { - const document = typemoq.Mock.ofType(); - const json = `{ - "version": "0.1.0", - "configurations": [ - { - "name":"wow" - }] -}`; - document.setup((doc) => doc.getText(typemoq.It.isAny())).returns(() => json); - document.setup((doc) => doc.offsetAt(typemoq.It.isAny())).returns(() => json.lastIndexOf('}]') + 1); - - const cursorPosition = LaunchJsonUpdaterServiceHelper.getCursorPositionInConfigurationsArray( - document.object, - new Position(0, 0), - ); - assert.strictEqual(cursorPosition, 'AfterItem'); - }); - test('Cursor is positioned after an item in the middle of the configurations array', async () => { - const document = typemoq.Mock.ofType(); - const json = `{ - "version": "0.1.0", - "configurations": [ - { - "name":"wow" - },{ - "name":"wow" - } - ] -}`; - document.setup((doc) => doc.getText(typemoq.It.isAny())).returns(() => json); - document.setup((doc) => doc.offsetAt(typemoq.It.isAny())).returns(() => json.indexOf('},') + 1); - - const cursorPosition = LaunchJsonUpdaterServiceHelper.getCursorPositionInConfigurationsArray( - document.object, - new Position(0, 0), - ); - assert.strictEqual(cursorPosition, 'AfterItem'); - }); - test('Text to be inserted must be prefixed with a comma', async () => { - const config = {} as DebugConfiguration; - const expectedText = `,${JSON.stringify(config)}`; - - const textToInsert = LaunchJsonUpdaterServiceHelper.getTextForInsertion(config, 'AfterItem'); - - assert.strictEqual(textToInsert, expectedText); - }); - test('Text to be inserted must not be prefixed with a comma (as a comma already exists)', async () => { - const config = {} as DebugConfiguration; - const expectedText = JSON.stringify(config); - - const textToInsert = LaunchJsonUpdaterServiceHelper.getTextForInsertion(config, 'AfterItem', 'BeforeCursor'); - - assert.strictEqual(textToInsert, expectedText); - }); - test('Text to be inserted must be suffixed with a comma', async () => { - const config = {} as DebugConfiguration; - const expectedText = `${JSON.stringify(config)},`; - - const textToInsert = LaunchJsonUpdaterServiceHelper.getTextForInsertion(config, 'BeforeItem'); - - assert.strictEqual(textToInsert, expectedText); - }); - test('Text to be inserted must not be prefixed nor suffixed with commas', async () => { - const config = {} as DebugConfiguration; - const expectedText = JSON.stringify(config); - - const textToInsert = LaunchJsonUpdaterServiceHelper.getTextForInsertion(config, 'InsideEmptyArray'); - - assert.strictEqual(textToInsert, expectedText); - }); - test('When inserting the debug config into the json file format the document', async () => { - const json = `{ - "version": "0.1.0", - "configurations": [ - { - "name":"wow" - },{ - "name":"wow" - } - ] -}`; - const config = {} as DebugConfiguration; - const document = typemoq.Mock.ofType(); - document.setup((doc) => doc.getText(typemoq.It.isAny())).returns(() => json); - document.setup((doc) => doc.offsetAt(typemoq.It.isAny())).returns(() => json.indexOf('},') + 1); - applyEditStub.returns(undefined); - executeCommandStub.withArgs('editor.action.formatDocument').resolves(); - - await LaunchJsonUpdaterServiceHelper.insertDebugConfiguration(document.object, new Position(0, 0), config); - - sinon.assert.calledOnce(applyEditStub); - sinon.assert.calledOnce(executeCommandStub.withArgs('editor.action.formatDocument')); - }); - test('No changes to configuration if there is not active document', async () => { - const document = typemoq.Mock.ofType(); - const position = new Position(0, 0); - const { token } = new CancellationTokenSource(); - getActiveTextEditorStub.returns(undefined); - let debugConfigInserted = false; - LaunchJsonUpdaterServiceHelper.insertDebugConfiguration = async () => { - debugConfigInserted = true; - }; - - await helper.selectAndInsertDebugConfig(document.object, position, token); - - sinon.assert.calledOnce(getActiveTextEditorStub); - sinon.assert.notCalled(getWorkspaceFolderStub); - assert.strictEqual(debugConfigInserted, false); - }); - test('No changes to configuration if the active document is not same as the document passed in', async () => { - const document = typemoq.Mock.ofType(); - const position = new Position(0, 0); - const { token } = new CancellationTokenSource(); - const textEditor = typemoq.Mock.ofType(); - textEditor - .setup((t) => t.document) - .returns(() => ('x' as unknown) as TextDocument) - .verifiable(typemoq.Times.atLeastOnce()); - getActiveTextEditorStub.returns(textEditor.object); - let debugConfigInserted = false; - LaunchJsonUpdaterServiceHelper.insertDebugConfiguration = async () => { - debugConfigInserted = true; - }; - - await helper.selectAndInsertDebugConfig(document.object, position, token); - - sinon.assert.calledOnce(getActiveTextEditorStub); - sinon.assert.notCalled(getWorkspaceFolderStub); - textEditor.verifyAll(); - assert.strictEqual(debugConfigInserted, false); - }); - test('No changes to configuration if cancellation token has been cancelled', async () => { - const document = typemoq.Mock.ofType(); - const position = new Position(0, 0); - const tokenSource = new CancellationTokenSource(); - tokenSource.cancel(); - const { token } = tokenSource; - const textEditor = typemoq.Mock.ofType(); - const docUri = Uri.file(__filename); - const folderUri = Uri.file('Folder Uri'); - const folder = { name: '', index: 0, uri: folderUri }; - document - .setup((doc) => doc.uri) - .returns(() => docUri) - .verifiable(typemoq.Times.atLeastOnce()); - textEditor - .setup((t) => t.document) - .returns(() => document.object) - .verifiable(typemoq.Times.atLeastOnce()); - getActiveTextEditorStub.returns(textEditor.object); - getWorkspaceFolderStub.returns(folder); - when(debugConfigService.provideDebugConfigurations!(folder, token)).thenResolve(([''] as unknown) as void); - let debugConfigInserted = false; - LaunchJsonUpdaterServiceHelper.insertDebugConfiguration = async () => { - debugConfigInserted = true; - }; - - await helper.selectAndInsertDebugConfig(document.object, position, token); - - sinon.assert.calledOnce(getActiveTextEditorStub); - sinon.assert.calledOnce(getWorkspaceFolderStub); - textEditor.verifyAll(); - document.verifyAll(); - assert.strictEqual(debugConfigInserted, false); - }); - test('No changes to configuration if no configuration items are returned', async () => { - const document = typemoq.Mock.ofType(); - const position = new Position(0, 0); - const tokenSource = new CancellationTokenSource(); - const { token } = tokenSource; - const textEditor = typemoq.Mock.ofType(); - const docUri = Uri.file(__filename); - const folderUri = Uri.file('Folder Uri'); - const folder = { name: '', index: 0, uri: folderUri }; - document - .setup((doc) => doc.uri) - .returns(() => docUri) - .verifiable(typemoq.Times.atLeastOnce()); - textEditor - .setup((t) => t.document) - .returns(() => document.object) - .verifiable(typemoq.Times.atLeastOnce()); - - getActiveTextEditorStub.returns(textEditor.object); - getWorkspaceFolderStub.returns(folder); - - when(debugConfigService.provideDebugConfigurations!(folder, token)).thenResolve(([] as unknown) as void); - let debugConfigInserted = false; - LaunchJsonUpdaterServiceHelper.insertDebugConfiguration = async () => { - debugConfigInserted = true; - }; - - await helper.selectAndInsertDebugConfig(document.object, position, token); - - sinon.assert.calledOnce(getActiveTextEditorStub); - sinon.assert.calledOnce(getWorkspaceFolderStub.withArgs(docUri)); - textEditor.verifyAll(); - document.verifyAll(); - assert.strictEqual(debugConfigInserted, false); - }); - test('Changes are made to the configuration', async () => { - const document = typemoq.Mock.ofType(); - const position = new Position(0, 0); - const tokenSource = new CancellationTokenSource(); - const { token } = tokenSource; - const textEditor = typemoq.Mock.ofType(); - const docUri = Uri.file(__filename); - const folderUri = Uri.file('Folder Uri'); - const folder = { name: '', index: 0, uri: folderUri }; - document - .setup((doc) => doc.uri) - .returns(() => docUri) - .verifiable(typemoq.Times.atLeastOnce()); - textEditor - .setup((t) => t.document) - .returns(() => document.object) - .verifiable(typemoq.Times.atLeastOnce()); - getActiveTextEditorStub.returns(textEditor.object); - getWorkspaceFolderStub.withArgs(docUri).returns(folder); - when(debugConfigService.provideDebugConfigurations!(folder, token)).thenResolve(([ - 'config', - ] as unknown) as void); - let debugConfigInserted = false; - LaunchJsonUpdaterServiceHelper.insertDebugConfiguration = async () => { - debugConfigInserted = true; - }; - - await helper.selectAndInsertDebugConfig(document.object, position, token); - - sinon.assert.called(getActiveTextEditorStub); - sinon.assert.calledOnce(getWorkspaceFolderStub.withArgs(docUri)); - textEditor.verifyAll(); - document.verifyAll(); - assert.strictEqual(debugConfigInserted, true); - }); - test('If cursor is at the begining of line 1 then there is no comma before cursor', async () => { - sandbox.restore(); - const document = typemoq.Mock.ofType(); - const position = new Position(1, 0); - document - .setup((doc) => doc.lineAt(1)) - .returns(() => ({ range: new Range(1, 0, 1, 1) } as TextLine)) - .verifiable(typemoq.Times.atLeastOnce()); - document - .setup((doc) => doc.getText(typemoq.It.isAny())) - .returns(() => '') - .verifiable(typemoq.Times.atLeastOnce()); - - const isBeforeCursor = LaunchJsonUpdaterServiceHelper.isCommaImmediatelyBeforeCursor(document.object, position); - - assert.ok(!isBeforeCursor); - document.verifyAll(); - }); - test('If cursor is positioned after some text (not a comma) then detect this', async () => { - sandbox.restore(); - const document = typemoq.Mock.ofType(); - const position = new Position(2, 2); - document - .setup((doc) => doc.lineAt(2)) - .returns(() => ({ range: new Range(2, 0, 1, 5) } as TextLine)) - .verifiable(typemoq.Times.atLeastOnce()); - document - .setup((doc) => doc.getText(typemoq.It.isAny())) - .returns(() => 'Hello') - .verifiable(typemoq.Times.atLeastOnce()); - - const isBeforeCursor = LaunchJsonUpdaterServiceHelper.isCommaImmediatelyBeforeCursor(document.object, position); - - assert.ok(!isBeforeCursor); - document.verifyAll(); - }); - test('If cursor is positioned after a comma then detect this', async () => { - sandbox.restore(); - const document = typemoq.Mock.ofType(); - const position = new Position(2, 2); - document - .setup((doc) => doc.lineAt(2)) - .returns(() => ({ range: new Range(2, 0, 2, 3) } as TextLine)) - .verifiable(typemoq.Times.atLeastOnce()); - document - .setup((doc) => doc.getText(typemoq.It.isAny())) - .returns(() => '}, ') - .verifiable(typemoq.Times.atLeastOnce()); - - const isBeforeCursor = LaunchJsonUpdaterServiceHelper.isCommaImmediatelyBeforeCursor(document.object, position); - - assert.ok(isBeforeCursor); - document.verifyAll(); - }); - test('If cursor is positioned in an empty line and previous line ends with comma, then detect this', async () => { - sandbox.restore(); - const document = typemoq.Mock.ofType(); - const position = new Position(2, 2); - document - .setup((doc) => doc.lineAt(1)) - .returns(() => ({ range: new Range(1, 0, 1, 3), text: '}, ' } as TextLine)) - .verifiable(typemoq.Times.atLeastOnce()); - document - .setup((doc) => doc.lineAt(2)) - .returns(() => ({ range: new Range(2, 0, 2, 3), text: ' ' } as TextLine)) - .verifiable(typemoq.Times.atLeastOnce()); - document - .setup((doc) => doc.getText(typemoq.It.isAny())) - .returns(() => ' ') - .verifiable(typemoq.Times.atLeastOnce()); - - const isBeforeCursor = LaunchJsonUpdaterServiceHelper.isCommaImmediatelyBeforeCursor(document.object, position); - - assert.ok(isBeforeCursor); - document.verifyAll(); - }); - test('If cursor is positioned in an empty line and previous line does not end with comma, then detect this', async () => { - sandbox.restore(); - const document = typemoq.Mock.ofType(); - const position = new Position(2, 2); - document - .setup((doc) => doc.lineAt(1)) - .returns(() => ({ range: new Range(1, 0, 1, 3), text: '} ' } as TextLine)) - .verifiable(typemoq.Times.atLeastOnce()); - document - .setup((doc) => doc.lineAt(2)) - .returns(() => ({ range: new Range(2, 0, 2, 3), text: ' ' } as TextLine)) - .verifiable(typemoq.Times.atLeastOnce()); - document - .setup((doc) => doc.getText(typemoq.It.isAny())) - .returns(() => ' ') - .verifiable(typemoq.Times.atLeastOnce()); - - const isBeforeCursor = LaunchJsonUpdaterServiceHelper.isCommaImmediatelyBeforeCursor(document.object, position); - - assert.ok(!isBeforeCursor); - document.verifyAll(); - }); -}); diff --git a/src/test/debugger/extension/configuration/providers/djangoLaunch.unit.test.ts b/src/test/debugger/extension/configuration/providers/djangoLaunch.unit.test.ts deleted file mode 100644 index 8a5898611c82..000000000000 --- a/src/test/debugger/extension/configuration/providers/djangoLaunch.unit.test.ts +++ /dev/null @@ -1,138 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; - -import { Uri } from 'vscode'; -import { expect } from 'chai'; -import * as path from 'path'; -import * as fs from 'fs-extra'; -import * as sinon from 'sinon'; -import { anything, instance, mock, when } from 'ts-mockito'; -import { DebugConfigStrings } from '../../../../../client/common/utils/localize'; -import { MultiStepInput } from '../../../../../client/common/utils/multiStepInput'; -import { DebuggerTypeName } from '../../../../../client/debugger/constants'; -import { DebugConfigurationState } from '../../../../../client/debugger/extension/types'; -import { resolveVariables } from '../../../../../client/debugger/extension/configuration/utils/common'; -import * as djangoLaunch from '../../../../../client/debugger/extension/configuration/providers/djangoLaunch'; -import * as workspaceApis from '../../../../../client/common/vscodeApis/workspaceApis'; - -suite('Debugging - Configuration Provider Django', () => { - let pathExistsStub: sinon.SinonStub; - let pathSeparatorStub: sinon.SinonStub; - let workspaceStub: sinon.SinonStub; - let input: MultiStepInput; - - setup(() => { - input = mock>(MultiStepInput); - pathExistsStub = sinon.stub(fs, 'pathExists'); - pathSeparatorStub = sinon.stub(path, 'sep'); - workspaceStub = sinon.stub(workspaceApis, 'getWorkspaceFolder'); - }); - teardown(() => { - sinon.restore(); - }); - test("getManagePyPath should return undefined if file doesn't exist", async () => { - const folder = { uri: Uri.parse(path.join('one', 'two')), name: '1', index: 0 }; - const managePyPath = path.join(folder.uri.fsPath, 'manage.py'); - pathExistsStub.withArgs(managePyPath).resolves(false); - const file = await djangoLaunch.getManagePyPath(folder); - - expect(file).to.be.equal(undefined, 'Should return undefined'); - }); - test('getManagePyPath should file path', async () => { - const folder = { uri: Uri.parse(path.join('one', 'two')), name: '1', index: 0 }; - const managePyPath = path.join(folder.uri.fsPath, 'manage.py'); - pathExistsStub.withArgs(managePyPath).resolves(true); - pathSeparatorStub.value('-'); - const file = await djangoLaunch.getManagePyPath(folder); - - expect(file).to.be.equal('${workspaceFolder}-manage.py'); - }); - test('Resolve variables (with resource)', async () => { - const folder = { uri: Uri.parse(path.join('one', 'two')), name: '1', index: 0 }; - workspaceStub.returns(folder); - const resolvedPath = resolveVariables('${workspaceFolder}/one.py', undefined, folder); - - expect(resolvedPath).to.be.equal(`${folder.uri.fsPath}/one.py`); - }); - test('Validation of path should return errors if path is undefined', async () => { - const folder = { uri: Uri.parse(path.join('one', 'two')), name: '1', index: 0 }; - const error = await djangoLaunch.validateManagePy(folder, ''); - - expect(error).to.be.length.greaterThan(1); - }); - test('Validation of path should return errors if path is empty', async () => { - const folder = { uri: Uri.parse(path.join('one', 'two')), name: '1', index: 0 }; - const error = await djangoLaunch.validateManagePy(folder, '', ''); - - expect(error).to.be.length.greaterThan(1); - }); - test('Validation of path should return errors if resolved path is empty', async () => { - const folder = { uri: Uri.parse(path.join('one', 'two')), name: '1', index: 0 }; - const error = await djangoLaunch.validateManagePy(folder, '', 'x'); - - expect(error).to.be.length.greaterThan(1); - }); - test("Validation of path should return errors if resolved path doesn't exist", async () => { - const folder = { uri: Uri.parse(path.join('one', 'two')), name: '1', index: 0 }; - pathExistsStub.withArgs('xyz').resolves(false); - const error = await djangoLaunch.validateManagePy(folder, '', 'x'); - - expect(error).to.be.length.greaterThan(1); - }); - test('Validation of path should return errors if resolved path is non-python', async () => { - const folder = { uri: Uri.parse(path.join('one', 'two')), name: '1', index: 0 }; - pathExistsStub.withArgs('xyz.txt').resolves(true); - const error = await djangoLaunch.validateManagePy(folder, '', 'x'); - - expect(error).to.be.length.greaterThan(1); - }); - test('Validation of path should return errors if resolved path is python', async () => { - const folder = { uri: Uri.parse(path.join('one', 'two')), name: '1', index: 0 }; - pathExistsStub.withArgs('xyz.py').resolves(true); - const error = await djangoLaunch.validateManagePy(folder, '', 'xyz.py'); - - expect(error).to.be.equal(undefined, 'should not have errors'); - }); - test('Launch JSON with selected managepy path', async () => { - const folder = { uri: Uri.parse(path.join('one', 'two')), name: '1', index: 0 }; - const state = { config: {}, folder }; - pathSeparatorStub.value('-'); - when(input.showInputBox(anything())).thenResolve('hello'); - await djangoLaunch.buildDjangoLaunchDebugConfiguration(instance(input), state); - - const config = { - name: DebugConfigStrings.django.snippet.name, - type: DebuggerTypeName, - request: 'launch', - program: 'hello', - args: ['runserver'], - django: true, - justMyCode: true, - }; - - expect(state.config).to.be.deep.equal(config); - }); - test('Launch JSON with default managepy path', async () => { - const folder = { uri: Uri.parse(path.join('one', 'two')), name: '1', index: 0 }; - const state = { config: {}, folder }; - const workspaceFolderToken = '${workspaceFolder}'; - const defaultProgram = `${workspaceFolderToken}-manage.py`; - pathSeparatorStub.value('-'); - when(input.showInputBox(anything())).thenResolve(); - await djangoLaunch.buildDjangoLaunchDebugConfiguration(instance(input), state); - - const config = { - name: DebugConfigStrings.django.snippet.name, - type: DebuggerTypeName, - request: 'launch', - program: defaultProgram, - args: ['runserver'], - django: true, - justMyCode: true, - }; - - expect(state.config).to.be.deep.equal(config); - }); -}); diff --git a/src/test/debugger/extension/configuration/providers/fastapiLaunch.unit.test.ts b/src/test/debugger/extension/configuration/providers/fastapiLaunch.unit.test.ts deleted file mode 100644 index f6c20985e4da..000000000000 --- a/src/test/debugger/extension/configuration/providers/fastapiLaunch.unit.test.ts +++ /dev/null @@ -1,83 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; - -import { expect } from 'chai'; -import * as path from 'path'; -import * as fs from 'fs-extra'; -import * as sinon from 'sinon'; -import { anything, instance, mock, when } from 'ts-mockito'; -import { Uri } from 'vscode'; -import { DebugConfigStrings } from '../../../../../client/common/utils/localize'; -import { MultiStepInput } from '../../../../../client/common/utils/multiStepInput'; -import { DebuggerTypeName } from '../../../../../client/debugger/constants'; -import * as fastApiLaunch from '../../../../../client/debugger/extension/configuration/providers/fastapiLaunch'; -import { DebugConfigurationState } from '../../../../../client/debugger/extension/types'; - -suite('Debugging - Configuration Provider FastAPI', () => { - let input: MultiStepInput; - let pathExistsStub: sinon.SinonStub; - - setup(() => { - input = mock>(MultiStepInput); - pathExistsStub = sinon.stub(fs, 'pathExists'); - }); - teardown(() => { - sinon.restore(); - }); - test("getApplicationPath should return undefined if file doesn't exist", async () => { - const folder = { uri: Uri.parse(path.join('one', 'two')), name: '1', index: 0 }; - const appPyPath = path.join(folder.uri.fsPath, 'main.py'); - pathExistsStub.withArgs(appPyPath).resolves(false); - const file = await fastApiLaunch.getApplicationPath(folder); - - expect(file).to.be.equal(undefined, 'Should return undefined'); - }); - test('getApplicationPath should find path', async () => { - const folder = { uri: Uri.parse(path.join('one', 'two')), name: '1', index: 0 }; - const appPyPath = path.join(folder.uri.fsPath, 'main.py'); - pathExistsStub.withArgs(appPyPath).resolves(true); - const file = await fastApiLaunch.getApplicationPath(folder); - - expect(file).to.be.equal('main.py'); - }); - test('Launch JSON with valid python path', async () => { - const folder = { uri: Uri.parse(path.join('one', 'two')), name: '1', index: 0 }; - const state = { config: {}, folder }; - - await fastApiLaunch.buildFastAPILaunchDebugConfiguration(instance(input), state); - - const config = { - name: DebugConfigStrings.fastapi.snippet.name, - type: DebuggerTypeName, - request: 'launch', - module: 'uvicorn', - args: ['main:app'], - jinja: true, - justMyCode: true, - }; - - expect(state.config).to.be.deep.equal(config); - }); - test('Launch JSON with selected app path', async () => { - const folder = { uri: Uri.parse(path.join('one', 'two')), name: '1', index: 0 }; - const state = { config: {}, folder }; - - when(input.showInputBox(anything())).thenResolve('main'); - - await fastApiLaunch.buildFastAPILaunchDebugConfiguration(instance(input), state); - - const config = { - name: DebugConfigStrings.fastapi.snippet.name, - type: DebuggerTypeName, - request: 'launch', - module: 'uvicorn', - args: ['main:app'], - jinja: true, - justMyCode: true, - }; - - expect(state.config).to.be.deep.equal(config); - }); -}); diff --git a/src/test/debugger/extension/configuration/providers/fileLaunch.unit.test.ts b/src/test/debugger/extension/configuration/providers/fileLaunch.unit.test.ts deleted file mode 100644 index f627c7558c51..000000000000 --- a/src/test/debugger/extension/configuration/providers/fileLaunch.unit.test.ts +++ /dev/null @@ -1,36 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; - -import { expect } from 'chai'; -import * as path from 'path'; -import { Uri } from 'vscode'; -import { DebugConfigStrings } from '../../../../../client/common/utils/localize'; -import { MultiStepInput } from '../../../../../client/common/utils/multiStepInput'; -import { DebuggerTypeName } from '../../../../../client/debugger/constants'; -import { buildFileLaunchDebugConfiguration } from '../../../../../client/debugger/extension/configuration/providers/fileLaunch'; -import { DebugConfigurationState } from '../../../../../client/debugger/extension/types'; - -suite('Debugging - Configuration Provider File', () => { - test('Launch JSON with default managepy path', async () => { - const folder = { uri: Uri.parse(path.join('one', 'two')), name: '1', index: 0 }; - const state = { config: {}, folder }; - - await buildFileLaunchDebugConfiguration( - (undefined as unknown) as MultiStepInput, - state, - ); - - const config = { - name: DebugConfigStrings.file.snippet.name, - type: DebuggerTypeName, - request: 'launch', - program: '${file}', - console: 'integratedTerminal', - justMyCode: true, - }; - - expect(state.config).to.be.deep.equal(config); - }); -}); diff --git a/src/test/debugger/extension/configuration/providers/flaskLaunch.unit.test.ts b/src/test/debugger/extension/configuration/providers/flaskLaunch.unit.test.ts deleted file mode 100644 index 08fb5259b282..000000000000 --- a/src/test/debugger/extension/configuration/providers/flaskLaunch.unit.test.ts +++ /dev/null @@ -1,113 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; - -import { expect } from 'chai'; -import * as path from 'path'; -import * as fs from 'fs-extra'; -import * as sinon from 'sinon'; -import { anything, instance, mock, when } from 'ts-mockito'; -import { Uri } from 'vscode'; -import { DebugConfigStrings } from '../../../../../client/common/utils/localize'; -import { MultiStepInput } from '../../../../../client/common/utils/multiStepInput'; -import { DebuggerTypeName } from '../../../../../client/debugger/constants'; -import { DebugConfigurationState } from '../../../../../client/debugger/extension/types'; -import * as flaskLaunch from '../../../../../client/debugger/extension/configuration/providers/flaskLaunch'; - -suite('Debugging - Configuration Provider Flask', () => { - let pathExistsStub: sinon.SinonStub; - let input: MultiStepInput; - setup(() => { - input = mock>(MultiStepInput); - pathExistsStub = sinon.stub(fs, 'pathExists'); - }); - teardown(() => { - sinon.restore(); - }); - test("getApplicationPath should return undefined if file doesn't exist", async () => { - const folder = { uri: Uri.parse(path.join('one', 'two')), name: '1', index: 0 }; - const appPyPath = path.join(folder.uri.fsPath, 'app.py'); - pathExistsStub.withArgs(appPyPath).resolves(false); - const file = await flaskLaunch.getApplicationPath(folder); - - expect(file).to.be.equal(undefined, 'Should return undefined'); - }); - test('getApplicationPath should file path', async () => { - const folder = { uri: Uri.parse(path.join('one', 'two')), name: '1', index: 0 }; - const appPyPath = path.join(folder.uri.fsPath, 'app.py'); - pathExistsStub.withArgs(appPyPath).resolves(true); - const file = await flaskLaunch.getApplicationPath(folder); - - expect(file).to.be.equal('app.py'); - }); - test('Launch JSON with valid python path', async () => { - const folder = { uri: Uri.parse(path.join('one', 'two')), name: '1', index: 0 }; - const state = { config: {}, folder }; - - await flaskLaunch.buildFlaskLaunchDebugConfiguration(instance(input), state); - - const config = { - name: DebugConfigStrings.flask.snippet.name, - type: DebuggerTypeName, - request: 'launch', - module: 'flask', - env: { - FLASK_APP: 'app.py', - FLASK_DEBUG: '1', - }, - args: ['run', '--no-debugger', '--no-reload'], - jinja: true, - justMyCode: true, - }; - - expect(state.config).to.be.deep.equal(config); - }); - test('Launch JSON with selected app path', async () => { - const folder = { uri: Uri.parse(path.join('one', 'two')), name: '1', index: 0 }; - const state = { config: {}, folder }; - - when(input.showInputBox(anything())).thenResolve('hello'); - - await flaskLaunch.buildFlaskLaunchDebugConfiguration(instance(input), state); - - const config = { - name: DebugConfigStrings.flask.snippet.name, - type: DebuggerTypeName, - request: 'launch', - module: 'flask', - env: { - FLASK_APP: 'hello', - FLASK_DEBUG: '1', - }, - args: ['run', '--no-debugger', '--no-reload'], - jinja: true, - justMyCode: true, - }; - - expect(state.config).to.be.deep.equal(config); - }); - test('Launch JSON with default managepy path', async () => { - const folder = { uri: Uri.parse(path.join('one', 'two')), name: '1', index: 0 }; - const state = { config: {}, folder }; - when(input.showInputBox(anything())).thenResolve(); - - await flaskLaunch.buildFlaskLaunchDebugConfiguration(instance(input), state); - - const config = { - name: DebugConfigStrings.flask.snippet.name, - type: DebuggerTypeName, - request: 'launch', - module: 'flask', - env: { - FLASK_APP: 'app.py', - FLASK_DEBUG: '1', - }, - args: ['run', '--no-debugger', '--no-reload'], - jinja: true, - justMyCode: true, - }; - - expect(state.config).to.be.deep.equal(config); - }); -}); diff --git a/src/test/debugger/extension/configuration/providers/moduleLaunch.unit.test.ts b/src/test/debugger/extension/configuration/providers/moduleLaunch.unit.test.ts deleted file mode 100644 index 2508db506ca2..000000000000 --- a/src/test/debugger/extension/configuration/providers/moduleLaunch.unit.test.ts +++ /dev/null @@ -1,55 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; - -import { expect } from 'chai'; -import * as path from 'path'; -import { anything, instance, mock, when } from 'ts-mockito'; -import { Uri } from 'vscode'; -import { DebugConfigStrings } from '../../../../../client/common/utils/localize'; -import { MultiStepInput } from '../../../../../client/common/utils/multiStepInput'; -import { DebuggerTypeName } from '../../../../../client/debugger/constants'; -import { buildModuleLaunchConfiguration } from '../../../../../client/debugger/extension/configuration/providers/moduleLaunch'; -import { DebugConfigurationState } from '../../../../../client/debugger/extension/types'; - -suite('Debugging - Configuration Provider Module', () => { - test('Launch JSON with default module name', async () => { - const folder = { uri: Uri.parse(path.join('one', 'two')), name: '1', index: 0 }; - const state = { config: {}, folder }; - const input = mock>(MultiStepInput); - - when(input.showInputBox(anything())).thenResolve(); - - await buildModuleLaunchConfiguration(instance(input), state); - - const config = { - name: DebugConfigStrings.module.snippet.name, - type: DebuggerTypeName, - request: 'launch', - module: DebugConfigStrings.module.snippet.default, - justMyCode: true, - }; - - expect(state.config).to.be.deep.equal(config); - }); - test('Launch JSON with selected module name', async () => { - const folder = { uri: Uri.parse(path.join('one', 'two')), name: '1', index: 0 }; - const state = { config: {}, folder }; - const input = mock>(MultiStepInput); - - when(input.showInputBox(anything())).thenResolve('hello'); - - await buildModuleLaunchConfiguration(instance(input), state); - - const config = { - name: DebugConfigStrings.module.snippet.name, - type: DebuggerTypeName, - request: 'launch', - module: 'hello', - justMyCode: true, - }; - - expect(state.config).to.be.deep.equal(config); - }); -}); diff --git a/src/test/debugger/extension/configuration/providers/pidAttach.unit.test.ts b/src/test/debugger/extension/configuration/providers/pidAttach.unit.test.ts deleted file mode 100644 index 8217e150aa01..000000000000 --- a/src/test/debugger/extension/configuration/providers/pidAttach.unit.test.ts +++ /dev/null @@ -1,32 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; - -import { expect } from 'chai'; -import * as path from 'path'; -import { Uri } from 'vscode'; -import { DebugConfigStrings } from '../../../../../client/common/utils/localize'; -import { MultiStepInput } from '../../../../../client/common/utils/multiStepInput'; -import { DebuggerTypeName } from '../../../../../client/debugger/constants'; -import { buildPidAttachConfiguration } from '../../../../../client/debugger/extension/configuration/providers/pidAttach'; -import { DebugConfigurationState } from '../../../../../client/debugger/extension/types'; - -suite('Debugging - Configuration Provider File', () => { - test('Launch JSON with default process id', async () => { - const folder = { uri: Uri.parse(path.join('one', 'two')), name: '1', index: 0 }; - const state = { config: {}, folder }; - - await buildPidAttachConfiguration((undefined as unknown) as MultiStepInput, state); - - const config = { - name: DebugConfigStrings.attachPid.snippet.name, - type: DebuggerTypeName, - request: 'attach', - processId: '${command:pickProcess}', - justMyCode: true, - }; - - expect(state.config).to.be.deep.equal(config); - }); -}); diff --git a/src/test/debugger/extension/configuration/providers/pyramidLaunch.unit.test.ts b/src/test/debugger/extension/configuration/providers/pyramidLaunch.unit.test.ts deleted file mode 100644 index 688215259a2f..000000000000 --- a/src/test/debugger/extension/configuration/providers/pyramidLaunch.unit.test.ts +++ /dev/null @@ -1,163 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; - -import { expect } from 'chai'; -import * as path from 'path'; -import * as fs from 'fs-extra'; -import * as sinon from 'sinon'; -import { anything, instance, mock, when } from 'ts-mockito'; -import { Uri } from 'vscode'; -import { DebugConfigStrings } from '../../../../../client/common/utils/localize'; -import { MultiStepInput } from '../../../../../client/common/utils/multiStepInput'; -import { DebuggerTypeName } from '../../../../../client/debugger/constants'; -import { resolveVariables } from '../../../../../client/debugger/extension/configuration/utils/common'; -import * as pyramidLaunch from '../../../../../client/debugger/extension/configuration/providers/pyramidLaunch'; -import { DebugConfigurationState } from '../../../../../client/debugger/extension/types'; -import * as workspaceApis from '../../../../../client/common/vscodeApis/workspaceApis'; - -suite('Debugging - Configuration Provider Pyramid', () => { - let input: MultiStepInput; - let pathExistsStub: sinon.SinonStub; - let pathSeparatorStub: sinon.SinonStub; - let workspaceStub: sinon.SinonStub; - - setup(() => { - input = mock>(MultiStepInput); - pathExistsStub = sinon.stub(fs, 'pathExists'); - pathSeparatorStub = sinon.stub(path, 'sep'); - workspaceStub = sinon.stub(workspaceApis, 'getWorkspaceFolder'); - }); - teardown(() => { - sinon.restore(); - }); - test("getDevelopmentIniPath should return undefined if file doesn't exist", async () => { - const folder = { uri: Uri.parse(path.join('one', 'two')), name: '1', index: 0 }; - const managePyPath = path.join(folder.uri.fsPath, 'development.ini'); - pathExistsStub.withArgs(managePyPath).resolves(false); - const file = await pyramidLaunch.getDevelopmentIniPath(folder); - - expect(file).to.be.equal(undefined, 'Should return undefined'); - }); - test('getDevelopmentIniPath should file path', async () => { - const folder = { uri: Uri.parse(path.join('one', 'two')), name: '1', index: 0 }; - const managePyPath = path.join(folder.uri.fsPath, 'development.ini'); - pathSeparatorStub.value('-'); - pathExistsStub.withArgs(managePyPath).resolves(true); - const file = await pyramidLaunch.getDevelopmentIniPath(folder); - - expect(file).to.be.equal('${workspaceFolder}-development.ini'); - }); - test('Resolve variables (with resource)', async () => { - const folder = { uri: Uri.parse(path.join('one', 'two')), name: '1', index: 0 }; - workspaceStub.returns(folder); - const resolvedPath = resolveVariables('${workspaceFolder}/one.py', undefined, folder); - - expect(resolvedPath).to.be.equal(`${folder.uri.fsPath}/one.py`); - }); - test('Validation of path should return errors if path is undefined', async () => { - const folder = { uri: Uri.parse(path.join('one', 'two')), name: '1', index: 0 }; - const error = await pyramidLaunch.validateIniPath(folder, ''); - - expect(error).to.be.length.greaterThan(1); - }); - test('Validation of path should return errors if path is empty', async () => { - const folder = { uri: Uri.parse(path.join('one', 'two')), name: '1', index: 0 }; - const error = await pyramidLaunch.validateIniPath(folder, '', ''); - - expect(error).to.be.length.greaterThan(1); - }); - test('Validation of path should return errors if resolved path is empty', async () => { - const folder = { uri: Uri.parse(path.join('one', 'two')), name: '1', index: 0 }; - const error = await pyramidLaunch.validateIniPath(folder, '', 'x'); - - expect(error).to.be.length.greaterThan(1); - }); - test("Validation of path should return errors if resolved path doesn't exist", async () => { - const folder = { uri: Uri.parse(path.join('one', 'two')), name: '1', index: 0 }; - pathExistsStub.withArgs('xyz').resolves(false); - const error = await pyramidLaunch.validateIniPath(folder, '', 'x'); - - expect(error).to.be.length.greaterThan(1); - }); - test('Validation of path should return errors if resolved path is non-ini', async () => { - const folder = { uri: Uri.parse(path.join('one', 'two')), name: '1', index: 0 }; - pathExistsStub.withArgs('xyz.txt').resolves(true); - const error = await pyramidLaunch.validateIniPath(folder, '', 'x'); - - expect(error).to.be.length.greaterThan(1); - }); - test('Validation of path should not return errors if resolved path is ini', async () => { - const folder = { uri: Uri.parse(path.join('one', 'two')), name: '1', index: 0 }; - pathExistsStub.withArgs('xyz.ini').resolves(true); - const error = await pyramidLaunch.validateIniPath(folder, '', 'xyz.ini'); - - expect(error).to.be.equal(undefined, 'should not have errors'); - }); - test('Launch JSON with valid ini path', async () => { - const folder = { uri: Uri.parse(path.join('one', 'two')), name: '1', index: 0 }; - const state = { config: {}, folder }; - pathSeparatorStub.value('-'); - - await pyramidLaunch.buildPyramidLaunchConfiguration(instance(input), state); - - const config = { - name: DebugConfigStrings.pyramid.snippet.name, - type: DebuggerTypeName, - request: 'launch', - module: 'pyramid.scripts.pserve', - args: ['${workspaceFolder}-development.ini'], - pyramid: true, - jinja: true, - justMyCode: true, - }; - - expect(state.config).to.be.deep.equal(config); - }); - test('Launch JSON with selected ini path', async () => { - const folder = { uri: Uri.parse(path.join('one', 'two')), name: '1', index: 0 }; - const state = { config: {}, folder }; - pathSeparatorStub.value('-'); - when(input.showInputBox(anything())).thenResolve('hello'); - - await pyramidLaunch.buildPyramidLaunchConfiguration(instance(input), state); - - const config = { - name: DebugConfigStrings.pyramid.snippet.name, - type: DebuggerTypeName, - request: 'launch', - module: 'pyramid.scripts.pserve', - args: ['hello'], - pyramid: true, - jinja: true, - justMyCode: true, - }; - - expect(state.config).to.be.deep.equal(config); - }); - test('Launch JSON with default ini path', async () => { - const folder = { uri: Uri.parse(path.join('one', 'two')), name: '1', index: 0 }; - const state = { config: {}, folder }; - const workspaceFolderToken = '${workspaceFolder}'; - const defaultIni = `${workspaceFolderToken}-development.ini`; - - pathSeparatorStub.value('-'); - when(input.showInputBox(anything())).thenResolve(); - - await pyramidLaunch.buildPyramidLaunchConfiguration(instance(input), state); - - const config = { - name: DebugConfigStrings.pyramid.snippet.name, - type: DebuggerTypeName, - request: 'launch', - module: 'pyramid.scripts.pserve', - args: [defaultIni], - pyramid: true, - jinja: true, - justMyCode: true, - }; - - expect(state.config).to.be.deep.equal(config); - }); -}); diff --git a/src/test/debugger/extension/configuration/providers/remoteAttach.unit.test.ts b/src/test/debugger/extension/configuration/providers/remoteAttach.unit.test.ts deleted file mode 100644 index 323cda94a1eb..000000000000 --- a/src/test/debugger/extension/configuration/providers/remoteAttach.unit.test.ts +++ /dev/null @@ -1,130 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; - -import { expect } from 'chai'; -import * as path from 'path'; -import * as sinon from 'sinon'; -import { anything, instance, mock, verify, when } from 'ts-mockito'; -import { Uri } from 'vscode'; -import { DebugConfigStrings } from '../../../../../client/common/utils/localize'; -import { MultiStepInput } from '../../../../../client/common/utils/multiStepInput'; -import { DebuggerTypeName } from '../../../../../client/debugger/constants'; -import * as configuration from '../../../../../client/debugger/extension/configuration/utils/configuration'; -import * as remoteAttach from '../../../../../client/debugger/extension/configuration/providers/remoteAttach'; -import { DebugConfigurationState } from '../../../../../client/debugger/extension/types'; - -suite('Debugging - Configuration Provider Remote Attach', () => { - let input: MultiStepInput; - - setup(() => { - input = mock>(MultiStepInput); - }); - teardown(() => { - sinon.restore(); - }); - test('Configure port will display prompt', async () => { - when(input.showInputBox(anything())).thenResolve(); - - await configuration.configurePort(instance(input), {}); - - verify(input.showInputBox(anything())).once(); - }); - test('Configure port will default to 5678 if entered value is not a number', async () => { - const config: { connect?: { port?: number } } = {}; - when(input.showInputBox(anything())).thenResolve('xyz'); - - await configuration.configurePort(instance(input), config); - - verify(input.showInputBox(anything())).once(); - expect(config).to.be.deep.equal({ connect: { port: 5678 } }); - }); - test('Configure port will default to 5678', async () => { - const config: { connect?: { port?: number } } = {}; - when(input.showInputBox(anything())).thenResolve(); - - await configuration.configurePort(instance(input), config); - - verify(input.showInputBox(anything())).once(); - expect(config).to.be.deep.equal({ connect: { port: 5678 } }); - }); - test('Configure port will use user selected value', async () => { - const config: { connect?: { port?: number } } = {}; - when(input.showInputBox(anything())).thenResolve('1234'); - - await configuration.configurePort(instance(input), config); - - verify(input.showInputBox(anything())).once(); - expect(config).to.be.deep.equal({ connect: { port: 1234 } }); - }); - test('Launch JSON with default host name', async () => { - const folder = { uri: Uri.parse(path.join('one', 'two')), name: '1', index: 0 }; - const state = { config: {}, folder }; - let portConfigured = false; - when(input.showInputBox(anything())).thenResolve(); - - sinon.stub(configuration, 'configurePort').callsFake(async () => { - portConfigured = true; - }); - - const configurePort = await remoteAttach.buildRemoteAttachConfiguration(instance(input), state); - if (configurePort) { - await configurePort!(input, state); - } - - const config = { - name: DebugConfigStrings.attach.snippet.name, - type: DebuggerTypeName, - request: 'attach', - connect: { - host: 'localhost', - port: 5678, - }, - pathMappings: [ - { - localRoot: '${workspaceFolder}', - remoteRoot: '.', - }, - ], - justMyCode: true, - }; - - expect(state.config).to.be.deep.equal(config); - expect(portConfigured).to.be.equal(true, 'Port not configured'); - }); - test('Launch JSON with user defined host name', async () => { - const folder = { uri: Uri.parse(path.join('one', 'two')), name: '1', index: 0 }; - const state = { config: {}, folder }; - let portConfigured = false; - when(input.showInputBox(anything())).thenResolve('Hello'); - sinon.stub(configuration, 'configurePort').callsFake(async (_, cfg) => { - portConfigured = true; - cfg.connect!.port = 9999; - }); - const configurePort = await remoteAttach.buildRemoteAttachConfiguration(instance(input), state); - if (configurePort) { - await configurePort(input, state); - } - - const config = { - name: DebugConfigStrings.attach.snippet.name, - type: DebuggerTypeName, - request: 'attach', - connect: { - host: 'Hello', - port: 9999, - }, - pathMappings: [ - { - localRoot: '${workspaceFolder}', - remoteRoot: '.', - }, - ], - justMyCode: true, - }; - - expect(state.config).to.be.deep.equal(config); - expect(portConfigured).to.be.equal(true, 'Port not configured'); - }); -}); diff --git a/src/test/debugger/extension/configuration/resolvers/attach.unit.test.ts b/src/test/debugger/extension/configuration/resolvers/attach.unit.test.ts index b245a0b4622f..d557d0e6f2f4 100644 --- a/src/test/debugger/extension/configuration/resolvers/attach.unit.test.ts +++ b/src/test/debugger/extension/configuration/resolvers/attach.unit.test.ts @@ -496,74 +496,5 @@ getInfoPerOS().forEach(([osName, osType, path]) => { expect(debugConfig).to.have.property('clientOS', getClientOS()); expect(debugConfig).to.have.property('debugOptions').to.be.deep.equal(expectedDebugOptions); }); - - const testsForJustMyCode = [ - { - justMyCode: false, - debugStdLib: true, - expectedResult: false, - }, - { - justMyCode: false, - debugStdLib: false, - expectedResult: false, - }, - { - justMyCode: false, - debugStdLib: undefined, - expectedResult: false, - }, - { - justMyCode: true, - debugStdLib: false, - expectedResult: true, - }, - { - justMyCode: true, - debugStdLib: true, - expectedResult: true, - }, - { - justMyCode: true, - debugStdLib: undefined, - expectedResult: true, - }, - { - justMyCode: undefined, - debugStdLib: false, - expectedResult: true, - }, - { - justMyCode: undefined, - debugStdLib: true, - expectedResult: false, - }, - { - justMyCode: undefined, - debugStdLib: undefined, - expectedResult: true, - }, - ]; - test('Ensure justMyCode property is correctly derived from debugStdLib', async () => { - const activeFile = 'xyz.py'; - const workspaceFolder = createMoqWorkspaceFolder(__dirname); - setupActiveEditor(activeFile, PYTHON_LANGUAGE); - const defaultWorkspace = path.join('usr', 'desktop'); - setupWorkspaces([defaultWorkspace]); - - const debugOptions = debugOptionsAvailable - .slice() - .concat(DebugOptions.Jinja, DebugOptions.Sudo) as DebugOptions[]; - - testsForJustMyCode.forEach(async (testParams) => { - const debugConfig = await resolveDebugConfiguration(workspaceFolder, { - ...attach, - debugOptions, - justMyCode: testParams.justMyCode, - debugStdLib: testParams.debugStdLib, - }); - expect(debugConfig).to.have.property('justMyCode', testParams.expectedResult); - }); - }); }); }); diff --git a/src/test/debugger/extension/configuration/resolvers/launch.unit.test.ts b/src/test/debugger/extension/configuration/resolvers/launch.unit.test.ts index 2aec3dcfd041..f312c99b1cbc 100644 --- a/src/test/debugger/extension/configuration/resolvers/launch.unit.test.ts +++ b/src/test/debugger/extension/configuration/resolvers/launch.unit.test.ts @@ -22,6 +22,7 @@ import * as platform from '../../../../../client/common/utils/platform'; import * as windowApis from '../../../../../client/common/vscodeApis/windowApis'; import * as workspaceApis from '../../../../../client/common/vscodeApis/workspaceApis'; import { IEnvironmentActivationService } from '../../../../../client/interpreter/activation/types'; +import * as triggerApis from '../../../../../client/pythonEnvironments/creation/createEnvironmentTrigger'; getInfoPerOS().forEach(([osName, osType, path]) => { if (osType === platform.OSType.Unknown) { @@ -42,12 +43,18 @@ getInfoPerOS().forEach(([osName, osType, path]) => { let getActiveTextEditorStub: sinon.SinonStub; let getOSTypeStub: sinon.SinonStub; let getWorkspaceFolderStub: sinon.SinonStub; + let triggerCreateEnvironmentCheckNonBlockingStub: sinon.SinonStub; setup(() => { getActiveTextEditorStub = sinon.stub(windowApis, 'getActiveTextEditor'); getOSTypeStub = sinon.stub(platform, 'getOSType'); getWorkspaceFolderStub = sinon.stub(workspaceApis, 'getWorkspaceFolders'); getOSTypeStub.returns(osType); + triggerCreateEnvironmentCheckNonBlockingStub = sinon.stub( + triggerApis, + 'triggerCreateEnvironmentCheckNonBlocking', + ); + triggerCreateEnvironmentCheckNonBlockingStub.returns(undefined); }); teardown(() => { @@ -761,80 +768,13 @@ getInfoPerOS().forEach(([osName, osType, path]) => { expect(debugConfig).to.have.property('redirectOutput', true); expect(debugConfig).to.have.property('justMyCode', false); expect(debugConfig).to.have.property('debugOptions'); - const expectedOptions = [ - DebugOptions.DebugStdLib, - DebugOptions.ShowReturnValue, - DebugOptions.RedirectOutput, - ]; + const expectedOptions = [DebugOptions.ShowReturnValue, DebugOptions.RedirectOutput]; if (osType === platform.OSType.Windows) { expectedOptions.push(DebugOptions.FixFilePathCase); } expect((debugConfig as DebugConfiguration).debugOptions).to.be.deep.equal(expectedOptions); }); - const testsForJustMyCode = [ - { - justMyCode: false, - debugStdLib: true, - expectedResult: false, - }, - { - justMyCode: false, - debugStdLib: false, - expectedResult: false, - }, - { - justMyCode: false, - debugStdLib: undefined, - expectedResult: false, - }, - { - justMyCode: true, - debugStdLib: false, - expectedResult: true, - }, - { - justMyCode: true, - debugStdLib: true, - expectedResult: true, - }, - { - justMyCode: true, - debugStdLib: undefined, - expectedResult: true, - }, - { - justMyCode: undefined, - debugStdLib: false, - expectedResult: true, - }, - { - justMyCode: undefined, - debugStdLib: true, - expectedResult: false, - }, - { - justMyCode: undefined, - debugStdLib: undefined, - expectedResult: true, - }, - ]; - test('Ensure justMyCode property is correctly derived from debugStdLib', async () => { - const pythonPath = `PythonPath_${new Date().toString()}`; - const workspaceFolder = createMoqWorkspaceFolder(__dirname); - const pythonFile = 'xyz.py'; - setupIoc(pythonPath); - setupActiveEditor(pythonFile, PYTHON_LANGUAGE); - testsForJustMyCode.forEach(async (testParams) => { - const debugConfig = await resolveDebugConfiguration(workspaceFolder, { - ...launch, - debugStdLib: testParams.debugStdLib, - justMyCode: testParams.justMyCode, - }); - expect(debugConfig).to.have.property('justMyCode', testParams.expectedResult); - }); - }); - const testsForRedirectOutput = [ { console: 'internalConsole', diff --git a/src/test/debugger/extension/debugCommands.unit.test.ts b/src/test/debugger/extension/debugCommands.unit.test.ts index 3c023f3f1450..7d2463072f06 100644 --- a/src/test/debugger/extension/debugCommands.unit.test.ts +++ b/src/test/debugger/extension/debugCommands.unit.test.ts @@ -14,6 +14,7 @@ import { EXTENSION_ROOT_DIR_FOR_TESTS } from '../../constants'; import * as telemetry from '../../../client/telemetry'; import { IInterpreterService } from '../../../client/interpreter/contracts'; import { PythonEnvironment } from '../../../client/pythonEnvironments/info'; +import * as triggerApis from '../../../client/pythonEnvironments/creation/createEnvironmentTrigger'; suite('Debugging - commands', () => { let commandManager: typemoq.IMock; @@ -21,6 +22,7 @@ suite('Debugging - commands', () => { let disposables: typemoq.IMock; let interpreterService: typemoq.IMock; let debugCommands: IExtensionSingleActivationService; + let triggerCreateEnvironmentCheckNonBlockingStub: sinon.SinonStub; setup(() => { commandManager = typemoq.Mock.ofType(); @@ -36,6 +38,11 @@ suite('Debugging - commands', () => { sinon.stub(telemetry, 'sendTelemetryEvent').callsFake(() => { /** noop */ }); + triggerCreateEnvironmentCheckNonBlockingStub = sinon.stub( + triggerApis, + 'triggerCreateEnvironmentCheckNonBlocking', + ); + triggerCreateEnvironmentCheckNonBlockingStub.returns(undefined); }); teardown(() => { sinon.restore(); diff --git a/src/test/debugger/extension/hooks/childProcessAttachHandler.unit.test.ts b/src/test/debugger/extension/hooks/childProcessAttachHandler.unit.test.ts index ee9a59c8e6aa..b1053def2eba 100644 --- a/src/test/debugger/extension/hooks/childProcessAttachHandler.unit.test.ts +++ b/src/test/debugger/extension/hooks/childProcessAttachHandler.unit.test.ts @@ -9,6 +9,7 @@ import { ChildProcessAttachEventHandler } from '../../../../client/debugger/exte import { ChildProcessAttachService } from '../../../../client/debugger/extension/hooks/childProcessAttachService'; import { DebuggerEvents } from '../../../../client/debugger/extension/hooks/constants'; import { AttachRequestArguments } from '../../../../client/debugger/types'; +import { DebuggerTypeName } from '../../../../client/debugger/constants'; suite('Debug - Child Process', () => { test('Do not attach if the event is undefined', async () => { @@ -21,7 +22,15 @@ suite('Debug - Child Process', () => { const attachService = mock(ChildProcessAttachService); const handler = new ChildProcessAttachEventHandler(instance(attachService)); const body: any = {}; - const session: any = {}; + const session: any = { configuration: { type: DebuggerTypeName } }; + await handler.handleCustomEvent({ event: 'abc', body, session }); + verify(attachService.attach(body, session)).never(); + }); + test('Do not attach to child process if debugger type is different', async () => { + const attachService = mock(ChildProcessAttachService); + const handler = new ChildProcessAttachEventHandler(instance(attachService)); + const body: any = {}; + const session: any = { configuration: { type: 'other-type' } }; await handler.handleCustomEvent({ event: 'abc', body, session }); verify(attachService.attach(body, session)).never(); }); @@ -29,7 +38,7 @@ suite('Debug - Child Process', () => { const attachService = mock(ChildProcessAttachService); const handler = new ChildProcessAttachEventHandler(instance(attachService)); const body: any = {}; - const session: any = {}; + const session: any = { configuration: { type: DebuggerTypeName } }; await handler.handleCustomEvent({ event: DebuggerEvents.PtvsdAttachToSubprocess, body, session }); verify(attachService.attach(body, session)).never(); }); @@ -37,7 +46,7 @@ suite('Debug - Child Process', () => { const attachService = mock(ChildProcessAttachService); const handler = new ChildProcessAttachEventHandler(instance(attachService)); const body: any = {}; - const session: any = {}; + const session: any = { configuration: { type: DebuggerTypeName } }; await handler.handleCustomEvent({ event: DebuggerEvents.DebugpyAttachToSubprocess, body, session }); verify(attachService.attach(body, session)).never(); }); @@ -51,9 +60,11 @@ suite('Debug - Child Process', () => { port: 1234, subProcessId: 2, }; - const session: any = {}; + const session: any = { + configuration: { type: DebuggerTypeName }, + }; when(attachService.attach(body, session)).thenThrow(new Error('Kaboom')); - await handler.handleCustomEvent({ event: DebuggerEvents.DebugpyAttachToSubprocess, body, session: {} as any }); + await handler.handleCustomEvent({ event: DebuggerEvents.DebugpyAttachToSubprocess, body, session }); verify(attachService.attach(body, anything())).once(); const [, secondArg] = capture(attachService.attach).last(); expect(secondArg).to.deep.equal(session); diff --git a/src/test/debugger/extension/hooks/childProcessAttachService.unit.test.ts b/src/test/debugger/extension/hooks/childProcessAttachService.unit.test.ts index 2ab9d3e30d2c..118efe416e94 100644 --- a/src/test/debugger/extension/hooks/childProcessAttachService.unit.test.ts +++ b/src/test/debugger/extension/hooks/childProcessAttachService.unit.test.ts @@ -117,7 +117,7 @@ suite('Debug - Attach to Child Process', () => { verify(debugService.startDebugging(undefined, anything(), anything())).once(); sinon.assert.notCalled(showErrorMessageStub); }); - test('Validate debug config is passed as is', async () => { + test('Validate debug config is passed with the correct params', async () => { const data: LaunchRequestArguments | AttachRequestArguments = { request: 'attach', type: 'python', @@ -140,7 +140,7 @@ suite('Debug - Attach to Child Process', () => { verify(debugService.startDebugging(undefined, anything(), anything())).once(); const [, secondArg, thirdArg] = capture(debugService.startDebugging).last(); expect(secondArg).to.deep.equal(debugConfig); - expect(thirdArg).to.deep.equal(session); + expect(thirdArg).to.deep.equal({ parentSession: session, lifecycleManagedByParent: true }); sinon.assert.notCalled(showErrorMessageStub); }); test('Pass data as is if data is attach debug configuration', async () => { @@ -161,7 +161,7 @@ suite('Debug - Attach to Child Process', () => { verify(debugService.startDebugging(undefined, anything(), anything())).once(); const [, secondArg, thirdArg] = capture(debugService.startDebugging).last(); expect(secondArg).to.deep.equal(debugConfig); - expect(thirdArg).to.deep.equal(session); + expect(thirdArg).to.deep.equal({ parentSession: session, lifecycleManagedByParent: true }); sinon.assert.notCalled(showErrorMessageStub); }); test('Validate debug config when parent/root parent was attached', async () => { @@ -189,7 +189,7 @@ suite('Debug - Attach to Child Process', () => { verify(debugService.startDebugging(undefined, anything(), anything())).once(); const [, secondArg, thirdArg] = capture(debugService.startDebugging).last(); expect(secondArg).to.deep.equal(debugConfig); - expect(thirdArg).to.deep.equal(session); + expect(thirdArg).to.deep.equal({ parentSession: session, lifecycleManagedByParent: true }); sinon.assert.notCalled(showErrorMessageStub); }); }); diff --git a/src/test/debugger/extension/serviceRegistry.unit.test.ts b/src/test/debugger/extension/serviceRegistry.unit.test.ts index 43d81bbe1385..056d722c7e0e 100644 --- a/src/test/debugger/extension/serviceRegistry.unit.test.ts +++ b/src/test/debugger/extension/serviceRegistry.unit.test.ts @@ -11,10 +11,6 @@ import { DebugSessionLoggingFactory } from '../../../client/debugger/extension/a import { OutdatedDebuggerPromptFactory } from '../../../client/debugger/extension/adapter/outdatedDebuggerPrompt'; import { AttachProcessProviderFactory } from '../../../client/debugger/extension/attachQuickPick/factory'; import { IAttachProcessProviderFactory } from '../../../client/debugger/extension/attachQuickPick/types'; -import { PythonDebugConfigurationService } from '../../../client/debugger/extension/configuration/debugConfigurationService'; -import { LaunchJsonCompletionProvider } from '../../../client/debugger/extension/configuration/launch.json/completionProvider'; -import { InterpreterPathCommand } from '../../../client/debugger/extension/configuration/launch.json/interpreterPathCommand'; -import { LaunchJsonUpdaterService } from '../../../client/debugger/extension/configuration/launch.json/updaterService'; import { AttachConfigurationResolver } from '../../../client/debugger/extension/configuration/resolvers/attach'; import { LaunchConfigurationResolver } from '../../../client/debugger/extension/configuration/resolvers/launch'; import { IDebugConfigurationResolver } from '../../../client/debugger/extension/configuration/types'; @@ -25,7 +21,6 @@ import { IChildProcessAttachService, IDebugSessionEventHandlers } from '../../.. import { registerTypes } from '../../../client/debugger/extension/serviceRegistry'; import { IDebugAdapterDescriptorFactory, - IDebugConfigurationService, IDebugSessionLoggingFactory, IOutdatedDebuggerPromptFactory, } from '../../../client/debugger/extension/types'; @@ -35,43 +30,18 @@ import { IServiceManager } from '../../../client/ioc/types'; suite('Debugging - Service Registry', () => { let serviceManager: IServiceManager; - setup(() => { serviceManager = mock(ServiceManager); }); test('Registrations', () => { registerTypes(instance(serviceManager)); - verify( - serviceManager.addSingleton( - IExtensionSingleActivationService, - InterpreterPathCommand, - ), - ).once(); - verify( - serviceManager.addSingleton( - IDebugConfigurationService, - PythonDebugConfigurationService, - ), - ).once(); verify( serviceManager.addSingleton( IChildProcessAttachService, ChildProcessAttachService, ), ).once(); - verify( - serviceManager.addSingleton( - IExtensionSingleActivationService, - LaunchJsonCompletionProvider, - ), - ).once(); - verify( - serviceManager.addSingleton( - IExtensionSingleActivationService, - LaunchJsonUpdaterService, - ), - ).once(); verify( serviceManager.addSingleton( IExtensionSingleActivationService, diff --git a/src/test/debugger/utils.ts b/src/test/debugger/utils.ts index 4a41489940b8..9ccb8958b660 100644 --- a/src/test/debugger/utils.ts +++ b/src/test/debugger/utils.ts @@ -4,7 +4,7 @@ 'use strict'; import { expect } from 'chai'; -import * as fs from 'fs-extra'; +import * as fs from '../../client/common/platform/fs-paths'; import * as path from 'path'; import * as vscode from 'vscode'; import { DebugProtocol } from 'vscode-debugprotocol'; @@ -277,12 +277,12 @@ class DebuggerSession { } export class DebuggerFixture extends PythonFixture { - public resolveDebugger( + public async resolveDebugger( configName: string, file: string, scriptArgs: string[], wsRoot?: vscode.WorkspaceFolder, - ): DebuggerSession { + ): Promise { const config = getConfig(configName); let proc: Proc | undefined; if (config.request === 'launch') { @@ -292,7 +292,7 @@ export class DebuggerFixture extends PythonFixture { // XXX set the file in the current vscode editor? } else if (config.request === 'attach') { if (config.port) { - proc = this.runDebugger(config.port, file, ...scriptArgs); + proc = await this.runDebugger(config.port, file, ...scriptArgs); if (wsRoot && config.name === 'attach to a local port') { config.pathMappings.localRoot = wsRoot.uri.fsPath; } @@ -352,8 +352,8 @@ export class DebuggerFixture extends PythonFixture { } } - public runDebugger(port: number, filename: string, ...scriptArgs: string[]) { - const args = getDebugpyLauncherArgs({ + public async runDebugger(port: number, filename: string, ...scriptArgs: string[]) { + const args = await getDebugpyLauncherArgs({ host: 'localhost', port: port, // This causes problems if we set it to true. diff --git a/src/test/debuggerTest.ts b/src/test/debuggerTest.ts index 36a0060b9303..949f14caee3d 100644 --- a/src/test/debuggerTest.ts +++ b/src/test/debuggerTest.ts @@ -4,7 +4,7 @@ import * as path from 'path'; import { runTests } from '@vscode/test-electron'; import { EXTENSION_ROOT_DIR_FOR_TESTS } from './constants'; -import { getVersion } from './utils/vscode'; +import { getChannel } from './utils/vscode'; const workspacePath = path.join(__dirname, '..', '..', 'src', 'testMultiRootWkspc', 'multi.code-workspace'); process.env.IS_CI_SERVER_TEST_DEBUGGER = '1'; @@ -17,7 +17,7 @@ function start() { extensionDevelopmentPath: EXTENSION_ROOT_DIR_FOR_TESTS, extensionTestsPath: path.join(EXTENSION_ROOT_DIR_FOR_TESTS, 'out', 'test', 'index'), launchArgs: [workspacePath], - version: getVersion(), + version: getChannel(), extensionTestsEnv: { ...process.env, UITEST_DISABLE_INSIDERS: '1' }, }).catch((ex) => { console.error('End Debugger tests (with errors)', ex); diff --git a/src/test/environmentApi.unit.test.ts b/src/test/environmentApi.unit.test.ts index a4ea73fb6c92..2e5d13161f7b 100644 --- a/src/test/environmentApi.unit.test.ts +++ b/src/test/environmentApi.unit.test.ts @@ -36,8 +36,9 @@ import { ActiveEnvironmentPathChangeEvent, EnvironmentVariablesChangeEvent, EnvironmentsChangeEvent, - IExtensionApi, -} from '../client/apiTypes'; + PythonExtension, +} from '../client/api/types'; +import { JupyterPythonEnvironmentApi } from '../client/jupyter/jupyterIntegration'; suite('Python Environment API', () => { const workspacePath = 'path/to/workspace'; @@ -57,7 +58,7 @@ suite('Python Environment API', () => { let onDidChangeEnvironments: EventEmitter; let onDidChangeEnvironmentVariables: EventEmitter; - let environmentApi: IExtensionApi['environments']; + let environmentApi: PythonExtension['environments']; setup(() => { serviceContainer = typemoq.Mock.ofType(); @@ -74,14 +75,12 @@ suite('Python Environment API', () => { envVarsProvider = typemoq.Mock.ofType(); extensions .setup((e) => e.determineExtensionFromCallStack()) - .returns(() => Promise.resolve({ extensionId: 'id', displayName: 'displayName', apiName: 'apiName' })) - .verifiable(typemoq.Times.atLeastOnce()); + .returns(() => Promise.resolve({ extensionId: 'id', displayName: 'displayName', apiName: 'apiName' })); interpreterPathService = typemoq.Mock.ofType(); configService = typemoq.Mock.ofType(); onDidChangeRefreshState = new EventEmitter(); onDidChangeEnvironments = new EventEmitter(); onDidChangeEnvironmentVariables = new EventEmitter(); - serviceContainer.setup((s) => s.get(IExtensions)).returns(() => extensions.object); serviceContainer.setup((s) => s.get(IInterpreterPathService)).returns(() => interpreterPathService.object); serviceContainer.setup((s) => s.get(IConfigurationService)).returns(() => configService.object); @@ -94,13 +93,17 @@ suite('Python Environment API', () => { discoverAPI.setup((d) => d.onProgress).returns(() => onDidChangeRefreshState.event); discoverAPI.setup((d) => d.onChanged).returns(() => onDidChangeEnvironments.event); + discoverAPI.setup((d) => d.getEnvs()).returns(() => []); + const onDidChangePythonEnvironment = new EventEmitter(); + const jupyterApi: JupyterPythonEnvironmentApi = { + onDidChangePythonEnvironment: onDidChangePythonEnvironment.event, + getPythonEnvironment: (_uri: Uri) => undefined, + }; - environmentApi = buildEnvironmentApi(discoverAPI.object, serviceContainer.object); + environmentApi = buildEnvironmentApi(discoverAPI.object, serviceContainer.object, jupyterApi); }); teardown(() => { - // Verify each API method sends telemetry regarding who called the API. - extensions.verifyAll(); sinon.restore(); }); @@ -325,6 +328,12 @@ suite('Python Environment API', () => { }, ]; discoverAPI.setup((d) => d.getEnvs()).returns(() => envs); + const onDidChangePythonEnvironment = new EventEmitter(); + const jupyterApi: JupyterPythonEnvironmentApi = { + onDidChangePythonEnvironment: onDidChangePythonEnvironment.event, + getPythonEnvironment: (_uri: Uri) => undefined, + }; + environmentApi = buildEnvironmentApi(discoverAPI.object, serviceContainer.object, jupyterApi); const actual = environmentApi.known; const actualEnvs = actual?.map((a) => (a as EnvironmentReference).internal); assert.deepEqual( @@ -409,10 +418,10 @@ suite('Python Environment API', () => { // Update events events = []; expectedEvents = []; - const updatedEnv = cloneDeep(envs[0]); - updatedEnv.arch = Architecture.x86; - onDidChangeEnvironments.fire({ old: envs[0], new: updatedEnv }); - expectedEvents.push({ env: convertEnvInfo(updatedEnv), type: 'update' }); + const updatedEnv0 = cloneDeep(envs[0]); + updatedEnv0.arch = Architecture.x86; + onDidChangeEnvironments.fire({ old: envs[0], new: updatedEnv0 }); + expectedEvents.push({ env: convertEnvInfo(updatedEnv0), type: 'update' }); eventValues = events.map((e) => ({ env: (e.env as EnvironmentReference).internal, type: e.type })); assert.deepEqual(eventValues, expectedEvents); @@ -423,6 +432,11 @@ suite('Python Environment API', () => { expectedEvents.push({ env: convertEnvInfo(envs[2]), type: 'remove' }); eventValues = events.map((e) => ({ env: (e.env as EnvironmentReference).internal, type: e.type })); assert.deepEqual(eventValues, expectedEvents); + + const expectedEnvs = [convertEnvInfo(updatedEnv0), convertEnvInfo(envs[1])].sort(); + const knownEnvs = environmentApi.known.map((e) => (e as EnvironmentReference).internal).sort(); + + assert.deepEqual(expectedEnvs, knownEnvs); }); test('updateActiveEnvironmentPath: no resource', async () => { diff --git a/src/test/extensionSettings.ts b/src/test/extensionSettings.ts index 66a77589a770..2d35dcb5f4ca 100644 --- a/src/test/extensionSettings.ts +++ b/src/test/extensionSettings.ts @@ -13,6 +13,7 @@ import { PersistentStateFactory } from '../client/common/persistentState'; import { IPythonSettings, Resource } from '../client/common/types'; import { PythonEnvironment } from '../client/pythonEnvironments/info'; import { MockMemento } from './mocks/mementos'; +import { MockExtensions } from './mocks/extensions'; export function getExtensionSettings(resource: Uri | undefined): IPythonSettings { const vscode = require('vscode') as typeof import('vscode'); @@ -41,6 +42,7 @@ export function getExtensionSettings(resource: Uri | undefined): IPythonSettings const workspaceMemento = new MockMemento(); const globalMemento = new MockMemento(); const persistentStateFactory = new PersistentStateFactory(globalMemento, workspaceMemento); + const extensions = new MockExtensions(); return pythonSettings.PythonSettings.getInstance( resource, new AutoSelectionService(), @@ -49,5 +51,6 @@ export function getExtensionSettings(resource: Uri | undefined): IPythonSettings remoteName: undefined, } as IApplicationEnvironment), undefined, + extensions, ); } diff --git a/src/test/fakeVSCFileSystemAPI.ts b/src/test/fakeVSCFileSystemAPI.ts index df5356a04919..1811f51dcd04 100644 --- a/src/test/fakeVSCFileSystemAPI.ts +++ b/src/test/fakeVSCFileSystemAPI.ts @@ -1,9 +1,9 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -import * as fsextra from 'fs-extra'; import * as path from 'path'; import { FileStat, FileType, Uri } from 'vscode'; +import * as fsextra from '../client/common/platform/fs-paths'; import { convertStat } from '../client/common/platform/fileSystem'; import { createDeferred } from '../client/common/utils/async'; diff --git a/src/test/fixtures.ts b/src/test/fixtures.ts index 2b7a5bd9e65d..fbd8c20c9659 100644 --- a/src/test/fixtures.ts +++ b/src/test/fixtures.ts @@ -1,7 +1,7 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -import * as fs from 'fs-extra'; +import * as fs from '../client/common/platform/fs-paths'; import { sleep } from '../client/common/utils/async'; import { PYTHON_PATH } from './common'; import { Proc, spawn } from './proc'; diff --git a/src/test/format/extension.format.test.ts b/src/test/format/extension.format.test.ts deleted file mode 100644 index 40131be24ec2..000000000000 --- a/src/test/format/extension.format.test.ts +++ /dev/null @@ -1,205 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; - -import * as fs from 'fs-extra'; -import * as path from 'path'; -import { CancellationTokenSource, Position, Uri, window, workspace } from 'vscode'; -import { IProcessServiceFactory } from '../../client/common/process/types'; -import { AutoPep8Formatter } from '../../client/formatters/autoPep8Formatter'; -import { BlackFormatter } from '../../client/formatters/blackFormatter'; -import { YapfFormatter } from '../../client/formatters/yapfFormatter'; -import { isPythonVersionInProcess } from '../common'; -import { closeActiveWindows, initialize, initializeTest } from '../initialize'; -import { MockProcessService } from '../mocks/proc'; -import { registerForIOC } from '../pythonEnvironments/legacyIOC'; -import { UnitTestIocContainer } from '../testing/serviceRegistry'; -import { compareFiles } from '../textUtils'; - -const ch = window.createOutputChannel('Tests'); -const formatFilesPath = path.join(__dirname, '..', '..', '..', 'src', 'test', 'pythonFiles', 'formatting'); -const workspaceRootPath = path.join(__dirname, '..', '..', '..', 'src', 'test'); -const originalUnformattedFile = path.join(formatFilesPath, 'fileToFormat.py'); - -const autoPep8FileToFormat = path.join(formatFilesPath, 'autoPep8FileToFormat.py'); -const autoPep8Formatted = path.join(formatFilesPath, 'autoPep8Formatted.py'); -const blackFileToFormat = path.join(formatFilesPath, 'blackFileToFormat.py'); -const blackFormatted = path.join(formatFilesPath, 'blackFormatted.py'); -const yapfFileToFormat = path.join(formatFilesPath, 'yapfFileToFormat.py'); -const yapfFormatted = path.join(formatFilesPath, 'yapfFormatted.py'); - -let formattedYapf = ''; -let formattedBlack = ''; -let formattedAutoPep8 = ''; - -suite('Formatting - General', () => { - let ioc: UnitTestIocContainer; - - suiteSetup(async function () { - // https://github.com/microsoft/vscode-python/issues/12564 - // Skipping one test in the file is resulting in the next one failing, so skipping the entire suiteuntil further investigation. - - return this.skip(); - await initialize(); - await initializeDI(); - [autoPep8FileToFormat, blackFileToFormat, yapfFileToFormat].forEach((file) => { - fs.copySync(originalUnformattedFile, file, { overwrite: true }); - }); - formattedYapf = fs.readFileSync(yapfFormatted).toString(); - formattedAutoPep8 = fs.readFileSync(autoPep8Formatted).toString(); - formattedBlack = fs.readFileSync(blackFormatted).toString(); - }); - - async function formattingTestIsBlackSupported(): Promise { - const processService = await ioc.serviceContainer - .get(IProcessServiceFactory) - .create(Uri.file(workspaceRootPath)); - return !(await isPythonVersionInProcess(processService, '2', '3.0', '3.1', '3.2', '3.3', '3.4', '3.5')); - } - - setup(async () => { - await initializeTest(); - await initializeDI(); - }); - suiteTeardown(async () => { - [autoPep8FileToFormat, blackFileToFormat, yapfFileToFormat].forEach((file) => { - if (fs.existsSync(file)) { - fs.unlinkSync(file); - } - }); - ch.dispose(); - await closeActiveWindows(); - }); - teardown(async () => { - await ioc.dispose(); - }); - - async function initializeDI() { - ioc = new UnitTestIocContainer(); - ioc.registerCommonTypes(); - ioc.registerVariableTypes(); - ioc.registerUnitTestTypes(); - ioc.registerFormatterTypes(); - ioc.registerInterpreterStorageTypes(); - - // Mocks. - ioc.registerMockProcessTypes(); - await ioc.registerMockInterpreterTypes(); - - await registerForIOC(ioc.serviceManager, ioc.serviceContainer); - } - - async function injectFormatOutput(outputFileName: string) { - const procService = (await ioc.serviceContainer - .get(IProcessServiceFactory) - .create()) as MockProcessService; - procService.onExecObservable((_file, args, _options, callback) => { - if (args.indexOf('--diff') >= 0) { - callback({ - out: fs.readFileSync(path.join(formatFilesPath, outputFileName), 'utf8'), - source: 'stdout', - }); - } - }); - } - - async function testFormatting( - formatter: AutoPep8Formatter | BlackFormatter | YapfFormatter, - formattedContents: string, - fileToFormat: string, - outputFileName: string, - ) { - const textDocument = await workspace.openTextDocument(fileToFormat); - const textEditor = await window.showTextDocument(textDocument); - const options = { - insertSpaces: textEditor.options.insertSpaces! as boolean, - tabSize: textEditor.options.tabSize! as number, - }; - - await injectFormatOutput(outputFileName); - - const edits = await formatter.formatDocument(textDocument, options, new CancellationTokenSource().token); - await textEditor.edit((editBuilder) => { - edits.forEach((edit) => editBuilder.replace(edit.range, edit.newText)); - }); - compareFiles(formattedContents, textEditor.document.getText()); - } - - test('AutoPep8', async function () { - // https://github.com/microsoft/vscode-python/issues/12564 - - return this.skip(); - await testFormatting( - new AutoPep8Formatter(ioc.serviceContainer), - formattedAutoPep8, - autoPep8FileToFormat, - 'autopep8.output', - ); - }); - - test('Black', async function () { - // https://github.com/microsoft/vscode-python/issues/12564 - - return this.skip(); - if (!(await formattingTestIsBlackSupported())) { - // Skip for versions of python below 3.6, as Black doesn't support them at all. - - return this.skip(); - } - await testFormatting( - new BlackFormatter(ioc.serviceContainer), - formattedBlack, - blackFileToFormat, - 'black.output', - ); - }); - test('Yapf', async () => - testFormatting(new YapfFormatter(ioc.serviceContainer), formattedYapf, yapfFileToFormat, 'yapf.output')); - - test('Yapf on dirty file', async () => { - const sourceDir = path.join(__dirname, '..', '..', '..', 'src', 'test', 'pythonFiles', 'formatting'); - const targetDir = path.join(__dirname, '..', 'pythonFiles', 'formatting'); - - const originalName = 'formatWhenDirty.py'; - const resultsName = 'formatWhenDirtyResult.py'; - const fileToFormat = path.join(targetDir, originalName); - const formattedFile = path.join(targetDir, resultsName); - - if (!fs.pathExistsSync(targetDir)) { - fs.mkdirpSync(targetDir); - } - fs.copySync(path.join(sourceDir, originalName), fileToFormat, { overwrite: true }); - fs.copySync(path.join(sourceDir, resultsName), formattedFile, { overwrite: true }); - - const textDocument = await workspace.openTextDocument(fileToFormat); - const textEditor = await window.showTextDocument(textDocument); - await textEditor.edit((builder) => { - // Make file dirty. Trailing blanks will be removed. - builder.insert(new Position(0, 0), '\n \n'); - }); - - const dir = path.dirname(fileToFormat); - const configFile = path.join(dir, '.style.yapf'); - try { - // Create yapf configuration file - const content = '[style]\nbased_on_style = pep8\nindent_width=5\n'; - fs.writeFileSync(configFile, content); - - const options = { insertSpaces: textEditor.options.insertSpaces! as boolean, tabSize: 1 }; - const formatter = new YapfFormatter(ioc.serviceContainer); - const edits = await formatter.formatDocument(textDocument, options, new CancellationTokenSource().token); - await textEditor.edit((editBuilder) => { - edits.forEach((edit) => editBuilder.replace(edit.range, edit.newText)); - }); - - const expected = fs.readFileSync(formattedFile).toString(); - const actual = textEditor.document.getText(); - compareFiles(expected, actual); - } finally { - if (fs.existsSync(configFile)) { - fs.unlinkSync(configFile); - } - } - }); -}); diff --git a/src/test/format/format.helper.test.ts b/src/test/format/format.helper.test.ts deleted file mode 100644 index 50000f1af867..000000000000 --- a/src/test/format/format.helper.test.ts +++ /dev/null @@ -1,117 +0,0 @@ -import * as assert from 'assert'; -import * as TypeMoq from 'typemoq'; -import { IConfigurationService, IFormattingSettings, Product } from '../../client/common/types'; -import * as EnumEx from '../../client/common/utils/enum'; -import { FormatterHelper } from '../../client/formatters/helper'; -import { FormatterId } from '../../client/formatters/types'; -import { getExtensionSettings } from '../extensionSettings'; -import { initialize } from '../initialize'; -import { UnitTestIocContainer } from '../testing/serviceRegistry'; - -suite('Formatting - Helper', () => { - let ioc: UnitTestIocContainer; - let formatHelper: FormatterHelper; - - suiteSetup(initialize); - setup(() => { - ioc = new UnitTestIocContainer(); - - const config = TypeMoq.Mock.ofType(); - config.setup((x) => x.getSettings(TypeMoq.It.isAny())).returns(() => getExtensionSettings(undefined)); - - ioc.serviceManager.addSingletonInstance(IConfigurationService, config.object); - formatHelper = new FormatterHelper(ioc.serviceManager); - }); - - test('Ensure product is set in Execution Info', async () => { - [Product.autopep8, Product.black, Product.yapf].forEach((formatter) => { - const info = formatHelper.getExecutionInfo(formatter, []); - assert.strictEqual( - info.product, - formatter, - `Incorrect products for ${formatHelper.translateToId(formatter)}`, - ); - }); - }); - - test('Ensure executable is set in Execution Info', async () => { - const settings = getExtensionSettings(undefined); - - [Product.autopep8, Product.black, Product.yapf].forEach((formatter) => { - const info = formatHelper.getExecutionInfo(formatter, []); - const names = formatHelper.getSettingsPropertyNames(formatter); - const execPath = settings.formatting[names.pathName] as string; - - assert.strictEqual( - info.execPath, - execPath, - `Incorrect executable paths for product ${formatHelper.translateToId(formatter)}`, - ); - }); - }); - - test('Ensure arguments are set in Execution Info', async () => { - const settings = getExtensionSettings(undefined); - const customArgs = ['1', '2', '3']; - - [Product.autopep8, Product.black, Product.yapf].forEach((formatter) => { - const names = formatHelper.getSettingsPropertyNames(formatter); - const args: string[] = Array.isArray(settings.formatting[names.argsName]) - ? (settings.formatting[names.argsName] as string[]) - : []; - const expectedArgs = args.concat(customArgs).join(','); - - assert.strictEqual( - expectedArgs.endsWith(customArgs.join(',')), - true, - `Incorrect custom arguments for product ${formatHelper.translateToId(formatter)}`, - ); - }); - }); - - test('Ensure correct setting names are returned', async () => { - [Product.autopep8, Product.black, Product.yapf].forEach((formatter) => { - const translatedId = formatHelper.translateToId(formatter)!; - const settings = { - argsName: `${translatedId}Args` as keyof IFormattingSettings, - pathName: `${translatedId}Path` as keyof IFormattingSettings, - }; - - assert.deepEqual( - formatHelper.getSettingsPropertyNames(formatter), - settings, - `Incorrect settings for product ${formatHelper.translateToId(formatter)}`, - ); - }); - }); - - test('Ensure translation of ids works', async () => { - const formatterMapping = new Map(); - formatterMapping.set(Product.autopep8, 'autopep8'); - formatterMapping.set(Product.black, 'black'); - formatterMapping.set(Product.yapf, 'yapf'); - - [Product.autopep8, Product.black, Product.yapf].forEach((formatter) => { - const translatedId = formatHelper.translateToId(formatter); - assert.strictEqual( - translatedId, - formatterMapping.get(formatter)!, - `Incorrect translation for product ${formatHelper.translateToId(formatter)}`, - ); - }); - }); - - EnumEx.getValues(Product).forEach((product) => { - const formatterMapping = new Map(); - formatterMapping.set(Product.autopep8, 'autopep8'); - formatterMapping.set(Product.black, 'black'); - formatterMapping.set(Product.yapf, 'yapf'); - if (formatterMapping.has(product)) { - return; - } - - test(`Ensure translation of ids throws exceptions for unknown formatters (${product})`, async () => { - assert.throws(() => formatHelper.translateToId(product)); - }); - }); -}); diff --git a/src/test/format/formatter.unit.test.ts b/src/test/format/formatter.unit.test.ts deleted file mode 100644 index 05970d0c71f6..000000000000 --- a/src/test/format/formatter.unit.test.ts +++ /dev/null @@ -1,171 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; - -import * as assert from 'assert'; -import * as path from 'path'; -import { anything, capture, instance, mock, when } from 'ts-mockito'; -import * as typemoq from 'typemoq'; -import { CancellationTokenSource, FormattingOptions, TextDocument, Uri } from 'vscode'; -import { ApplicationShell } from '../../client/common/application/applicationShell'; -import { IApplicationShell, IWorkspaceService } from '../../client/common/application/types'; -import { WorkspaceService } from '../../client/common/application/workspace'; -import { PythonSettings } from '../../client/common/configSettings'; -import { ConfigurationService } from '../../client/common/configuration/service'; -import { PythonToolExecutionService } from '../../client/common/process/pythonToolService'; -import { IPythonToolExecutionService } from '../../client/common/process/types'; -import { - ExecutionInfo, - IConfigurationService, - IDisposableRegistry, - IFormattingSettings, - ILogOutputChannel, - IPythonSettings, -} from '../../client/common/types'; -import { AutoPep8Formatter } from '../../client/formatters/autoPep8Formatter'; -import { BaseFormatter } from '../../client/formatters/baseFormatter'; -import { BlackFormatter } from '../../client/formatters/blackFormatter'; -import { FormatterHelper } from '../../client/formatters/helper'; -import { IFormatterHelper } from '../../client/formatters/types'; -import { YapfFormatter } from '../../client/formatters/yapfFormatter'; -import { ServiceContainer } from '../../client/ioc/container'; -import { IServiceContainer } from '../../client/ioc/types'; -import { noop } from '../core'; -import { MockOutputChannel } from '../mockClasses'; - -suite('Formatting - Test Arguments', () => { - let container: IServiceContainer; - let outputChannel: ILogOutputChannel; - let workspace: IWorkspaceService; - let settings: IPythonSettings; - const workspaceUri = Uri.file(__dirname); - let document: typemoq.IMock; - const docUri = Uri.file(__filename); - let pythonToolExecutionService: IPythonToolExecutionService; - const options: FormattingOptions = { insertSpaces: false, tabSize: 1 }; - const formattingSettingsWithPath: IFormattingSettings = { - autopep8Args: ['1', '2'], - autopep8Path: path.join('a', 'exe'), - blackArgs: ['1', '2'], - blackPath: path.join('a', 'exe'), - provider: '', - yapfArgs: ['1', '2'], - yapfPath: path.join('a', 'exe'), - }; - - const formattingSettingsWithModuleName: IFormattingSettings = { - autopep8Args: ['1', '2'], - autopep8Path: 'module_name', - blackArgs: ['1', '2'], - blackPath: 'module_name', - provider: '', - yapfArgs: ['1', '2'], - yapfPath: 'module_name', - }; - - setup(() => { - container = mock(ServiceContainer); - outputChannel = mock(MockOutputChannel); - workspace = mock(WorkspaceService); - settings = mock(PythonSettings); - document = typemoq.Mock.ofType(); - document.setup((doc) => doc.getText(typemoq.It.isAny())).returns(() => ''); - document.setup((doc) => doc.isDirty).returns(() => false); - document.setup((doc) => doc.fileName).returns(() => docUri.fsPath); - document.setup((doc) => doc.uri).returns(() => docUri); - pythonToolExecutionService = mock(PythonToolExecutionService); - - const configService = mock(ConfigurationService); - const formatterHelper = new FormatterHelper(instance(container)); - - const appShell = mock(ApplicationShell); - when(appShell.setStatusBarMessage(anything(), anything())).thenReturn({ dispose: noop }); - - when(configService.getSettings(anything())).thenReturn(instance(settings)); - when(workspace.getWorkspaceFolder(anything())).thenReturn({ name: '', index: 0, uri: workspaceUri }); - when(container.get(ILogOutputChannel)).thenReturn(instance(outputChannel)); - when(container.get(IApplicationShell)).thenReturn(instance(appShell)); - when(container.get(IFormatterHelper)).thenReturn(formatterHelper); - when(container.get(IWorkspaceService)).thenReturn(instance(workspace)); - when(container.get(IConfigurationService)).thenReturn(instance(configService)); - when(container.get(IPythonToolExecutionService)).thenReturn( - instance(pythonToolExecutionService), - ); - when(container.get(IDisposableRegistry)).thenReturn([]); - }); - - async function setupFormatter( - formatter: BaseFormatter, - formattingSettings: IFormattingSettings, - ): Promise { - const { token } = new CancellationTokenSource(); - when(settings.formatting).thenReturn(formattingSettings); - when(pythonToolExecutionService.exec(anything(), anything(), anything())).thenResolve({ stdout: '' }); - - await formatter.formatDocument(document.object, options, token); - - const args = capture(pythonToolExecutionService.exec).first(); - return args[0]; - } - test('Ensure blackPath and args used to launch the formatter', async () => { - const formatter = new BlackFormatter(instance(container)); - - const execInfo = await setupFormatter(formatter, formattingSettingsWithPath); - - assert.strictEqual(execInfo.execPath, formattingSettingsWithPath.blackPath); - assert.strictEqual(execInfo.moduleName, undefined); - assert.deepEqual( - execInfo.args, - formattingSettingsWithPath.blackArgs.concat(['--diff', '--quiet', docUri.fsPath]), - ); - }); - test('Ensure black modulename and args used to launch the formatter', async () => { - const formatter = new BlackFormatter(instance(container)); - - const execInfo = await setupFormatter(formatter, formattingSettingsWithModuleName); - - assert.strictEqual(execInfo.execPath, formattingSettingsWithModuleName.blackPath); - assert.strictEqual(execInfo.moduleName, formattingSettingsWithModuleName.blackPath); - assert.deepEqual( - execInfo.args, - formattingSettingsWithPath.blackArgs.concat(['--diff', '--quiet', docUri.fsPath]), - ); - }); - test('Ensure autopep8path and args used to launch the formatter', async () => { - const formatter = new AutoPep8Formatter(instance(container)); - - const execInfo = await setupFormatter(formatter, formattingSettingsWithPath); - - assert.strictEqual(execInfo.execPath, formattingSettingsWithPath.autopep8Path); - assert.strictEqual(execInfo.moduleName, undefined); - assert.deepEqual(execInfo.args, formattingSettingsWithPath.autopep8Args.concat(['--diff', docUri.fsPath])); - }); - test('Ensure autpep8 modulename and args used to launch the formatter', async () => { - const formatter = new AutoPep8Formatter(instance(container)); - - const execInfo = await setupFormatter(formatter, formattingSettingsWithModuleName); - - assert.strictEqual(execInfo.execPath, formattingSettingsWithModuleName.autopep8Path); - assert.strictEqual(execInfo.moduleName, formattingSettingsWithModuleName.autopep8Path); - assert.deepEqual(execInfo.args, formattingSettingsWithPath.autopep8Args.concat(['--diff', docUri.fsPath])); - }); - test('Ensure yapfpath and args used to launch the formatter', async () => { - const formatter = new YapfFormatter(instance(container)); - - const execInfo = await setupFormatter(formatter, formattingSettingsWithPath); - - assert.strictEqual(execInfo.execPath, formattingSettingsWithPath.yapfPath); - assert.strictEqual(execInfo.moduleName, undefined); - assert.deepEqual(execInfo.args, formattingSettingsWithPath.yapfArgs.concat(['--diff', docUri.fsPath])); - }); - test('Ensure yapf modulename and args used to launch the formatter', async () => { - const formatter = new YapfFormatter(instance(container)); - - const execInfo = await setupFormatter(formatter, formattingSettingsWithModuleName); - - assert.strictEqual(execInfo.execPath, formattingSettingsWithModuleName.yapfPath); - assert.strictEqual(execInfo.moduleName, formattingSettingsWithModuleName.yapfPath); - assert.deepEqual(execInfo.args, formattingSettingsWithPath.yapfArgs.concat(['--diff', docUri.fsPath])); - }); -}); diff --git a/src/test/index.ts b/src/test/index.ts index 60c730bffcf2..a4c69a2a9ac6 100644 --- a/src/test/index.ts +++ b/src/test/index.ts @@ -44,8 +44,6 @@ process.on('unhandledRejection', (ex: any, _a) => { /** * Configure the test environment and return the optoins required to run moch tests. - * - * @returns {SetupOptions} */ function configure(): SetupOptions { process.env.VSC_PYTHON_CI_TEST = '1'; @@ -103,11 +101,10 @@ function configure(): SetupOptions { * to complete. * That's when we know out PVSC extension specific code is ready for testing. * So, this code needs to run always for every test running in VS Code (what we call these `system test`) . - * @returns */ function activatePythonExtensionScript() { const ex = new Error('Failed to initialize Python extension for tests after 3 minutes'); - let timer: NodeJS.Timer | undefined; + let timer: NodeJS.Timeout | undefined; const failed = new Promise((_, reject) => { timer = setTimeout(() => reject(ex), MAX_EXTENSION_ACTIVATION_TIME); }); @@ -121,13 +118,10 @@ function activatePythonExtensionScript() { /** * Runner, invoked by VS Code. * More info https://code.visualstudio.com/api/working-with-extensions/testing-extension - * - * @export - * @returns {Promise} */ export async function run(): Promise { const options = configure(); - const mocha = new Mocha(options); + const mocha = new Mocha.default(options); const testsRoot = path.join(__dirname); // Enable source map support. @@ -136,7 +130,7 @@ export async function run(): Promise { // Ignore `ds.test.js` test files when running other tests. const ignoreGlob = options.testFilesSuffix.toLowerCase() === 'ds.test' ? [] : ['**/**.ds.test.js']; const testFiles = await new Promise((resolve, reject) => { - glob( + glob.default( `**/**.${options.testFilesSuffix}.js`, { ignore: ['**/**.unit.test.js', '**/**.functional.test.js'].concat(ignoreGlob), cwd: testsRoot }, (error, files) => { diff --git a/src/test/initialize.ts b/src/test/initialize.ts index f4f37204da85..0ed75a0aa5c1 100644 --- a/src/test/initialize.ts +++ b/src/test/initialize.ts @@ -1,6 +1,6 @@ import * as path from 'path'; import * as vscode from 'vscode'; -import type { IExtensionApi } from '../client/apiTypes'; +import type { PythonExtension } from '../client/api/types'; import { clearPythonPathInWorkspaceFolder, IExtensionTestApi, @@ -14,7 +14,7 @@ import { sleep } from './core'; export * from './constants'; export * from './ciConstants'; -const dummyPythonFile = path.join(__dirname, '..', '..', 'src', 'test', 'pythonFiles', 'dummy.py'); +const dummyPythonFile = path.join(__dirname, '..', '..', 'src', 'test', 'python_files', 'dummy.py'); export const multirootPath = path.join(__dirname, '..', '..', 'src', 'testMultiRootWkspc'); const workspace3Uri = vscode.Uri.file(path.join(multirootPath, 'workspace3')); @@ -31,10 +31,14 @@ export async function initializePython() { export async function initialize(): Promise { await initializePython(); + + const pythonConfig = vscode.workspace.getConfiguration('python'); + await pythonConfig.update('experiments.optInto', ['All'], vscode.ConfigurationTarget.Global); + await pythonConfig.update('experiments.optOutFrom', [], vscode.ConfigurationTarget.Global); const api = await activateExtension(); if (!IS_SMOKE_TEST) { // When running smoke tests, we won't have access to these. - const configSettings = await import('../client/common/configSettings'); + const configSettings = await import('../client/common/configSettings.js'); // Dispose any cached python settings (used only in test env). configSettings.PythonSettings.dispose(); } @@ -42,7 +46,7 @@ export async function initialize(): Promise { return (api as any) as IExtensionTestApi; } export async function activateExtension() { - const extension = vscode.extensions.getExtension(PVSC_EXTENSION_ID_FOR_TESTS)!; + const extension = vscode.extensions.getExtension(PVSC_EXTENSION_ID_FOR_TESTS)!; const api = await extension.activate(); // Wait until its ready to use. await api.ready; @@ -54,7 +58,7 @@ export async function initializeTest(): Promise { await closeActiveWindows(); if (!IS_SMOKE_TEST) { // When running smoke tests, we won't have access to these. - const configSettings = await import('../client/common/configSettings'); + const configSettings = await import('../client/common/configSettings.js'); // Dispose any cached python settings (used only in test env). configSettings.PythonSettings.dispose(); } diff --git a/src/test/install/channelManager.channels.test.ts b/src/test/install/channelManager.channels.test.ts index 5e102a0a5182..e43fa21daf17 100644 --- a/src/test/install/channelManager.channels.test.ts +++ b/src/test/install/channelManager.channels.test.ts @@ -16,6 +16,7 @@ import { ServiceContainer } from '../../client/ioc/container'; import { ServiceManager } from '../../client/ioc/serviceManager'; import { IServiceContainer } from '../../client/ioc/types'; import { MockAutoSelectionService } from '../mocks/autoSelector'; +import { createTypeMoq } from '../mocks/helper'; suite('Installation - installation channels', () => { let serviceManager: ServiceManager; @@ -71,7 +72,7 @@ suite('Installation - installation channels', () => { const installer1 = mockInstaller(true, '1'); const installer2 = mockInstaller(true, '2'); - const appShell = TypeMoq.Mock.ofType(); + const appShell = createTypeMoq(); serviceManager.addSingletonInstance(IApplicationShell, appShell.object); // eslint-disable-next-line @typescript-eslint/no-explicit-any @@ -89,7 +90,7 @@ suite('Installation - installation channels', () => { installer2.setup((x) => x.displayName).returns(() => 'Name 2'); const cm = new InstallationChannelManager(serviceContainer); - await cm.getInstallationChannel(Product.pylint); + await cm.getInstallationChannel(Product.pytest); assert.notStrictEqual(items, undefined, 'showQuickPick not called'); assert.strictEqual(items!.length, 2, 'Incorrect number of installer shown'); @@ -98,7 +99,7 @@ suite('Installation - installation channels', () => { }); function mockInstaller(supported: boolean, name: string, priority?: number): TypeMoq.IMock { - const installer = TypeMoq.Mock.ofType(); + const installer = createTypeMoq(); installer .setup((x) => x.isSupported(TypeMoq.It.isAny())) .returns( diff --git a/src/test/install/channelManager.messages.test.ts b/src/test/install/channelManager.messages.test.ts index c21612e8f56c..1e9953b8b753 100644 --- a/src/test/install/channelManager.messages.test.ts +++ b/src/test/install/channelManager.messages.test.ts @@ -21,6 +21,7 @@ import { ServiceManager } from '../../client/ioc/serviceManager'; import { IServiceContainer } from '../../client/ioc/types'; import { EnvironmentType, PythonEnvironment } from '../../client/pythonEnvironments/info'; import { MockAutoSelectionService } from '../mocks/autoSelector'; +import { createTypeMoq } from '../mocks/helper'; const info: PythonEnvironment = { architecture: Architecture.Unknown, @@ -45,16 +46,16 @@ suite('Installation - channel messages', () => { const serviceManager = new ServiceManager(cont); serviceContainer = new ServiceContainer(cont); - platform = TypeMoq.Mock.ofType(); + platform = createTypeMoq(); serviceManager.addSingletonInstance(IPlatformService, platform.object); - appShell = TypeMoq.Mock.ofType(); + appShell = createTypeMoq(); serviceManager.addSingletonInstance(IApplicationShell, appShell.object); - interpreters = TypeMoq.Mock.ofType(); + interpreters = createTypeMoq(); serviceManager.addSingletonInstance(IInterpreterService, interpreters.object); - const moduleInstaller = TypeMoq.Mock.ofType(); + const moduleInstaller = createTypeMoq(); serviceManager.addSingletonInstance(IModuleInstaller, moduleInstaller.object); serviceManager.addSingleton( IInterpreterAutoSelectionService, @@ -185,7 +186,7 @@ suite('Installation - channel messages', () => { if (methodType === 'showNoInstallersMessage') { await channels.showNoInstallersMessage(); } else { - await channels.getInstallationChannel(Product.pylint); + await channels.getInstallationChannel(Product.pytest); } await verify(message, url); } diff --git a/src/test/interpreters/activation/indicatorPrompt.unit.test.ts b/src/test/interpreters/activation/indicatorPrompt.unit.test.ts new file mode 100644 index 000000000000..b15cd84dc01a --- /dev/null +++ b/src/test/interpreters/activation/indicatorPrompt.unit.test.ts @@ -0,0 +1,227 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import * as sinon from 'sinon'; +import { mock, when, anything, instance, verify, reset } from 'ts-mockito'; +import { EventEmitter, Terminal, Uri } from 'vscode'; +import { IActiveResourceService, IApplicationShell, ITerminalManager } from '../../../client/common/application/types'; +import { + IConfigurationService, + IExperimentService, + IPersistentState, + IPersistentStateFactory, + IPythonSettings, +} from '../../../client/common/types'; +import { TerminalIndicatorPrompt } from '../../../client/terminals/envCollectionActivation/indicatorPrompt'; +import { Common, Interpreters } from '../../../client/common/utils/localize'; +import { TerminalEnvVarActivation } from '../../../client/common/experiments/groups'; +import { sleep } from '../../core'; +import { IInterpreterService } from '../../../client/interpreter/contracts'; +import { PythonEnvironment } from '../../../client/pythonEnvironments/info'; +import { ITerminalEnvVarCollectionService } from '../../../client/terminals/types'; +import { PythonEnvType } from '../../../client/pythonEnvironments/base/info'; +import * as extapi from '../../../client/envExt/api.internal'; + +suite('Terminal Activation Indicator Prompt', () => { + let shell: IApplicationShell; + let terminalManager: ITerminalManager; + let experimentService: IExperimentService; + let activeResourceService: IActiveResourceService; + let terminalEnvVarCollectionService: ITerminalEnvVarCollectionService; + let persistentStateFactory: IPersistentStateFactory; + let terminalEnvVarCollectionPrompt: TerminalIndicatorPrompt; + let terminalEventEmitter: EventEmitter; + let notificationEnabled: IPersistentState; + let configurationService: IConfigurationService; + let interpreterService: IInterpreterService; + let useEnvExtensionStub: sinon.SinonStub; + const prompts = [Common.doNotShowAgain]; + const envName = 'env'; + const type = PythonEnvType.Virtual; + const expectedMessage = Interpreters.terminalEnvVarCollectionPrompt.format('Python virtual', `"(${envName})"`); + + setup(async () => { + useEnvExtensionStub = sinon.stub(extapi, 'useEnvExtension'); + useEnvExtensionStub.returns(false); + + shell = mock(); + terminalManager = mock(); + interpreterService = mock(); + when(interpreterService.getActiveInterpreter(anything())).thenResolve(({ + envName, + type, + } as unknown) as PythonEnvironment); + experimentService = mock(); + activeResourceService = mock(); + persistentStateFactory = mock(); + terminalEnvVarCollectionService = mock(); + configurationService = mock(); + when(configurationService.getSettings(anything())).thenReturn(({ + terminal: { + activateEnvironment: true, + }, + } as unknown) as IPythonSettings); + notificationEnabled = mock>(); + terminalEventEmitter = new EventEmitter(); + when(persistentStateFactory.createGlobalPersistentState(anything(), true)).thenReturn( + instance(notificationEnabled), + ); + when(experimentService.inExperimentSync(TerminalEnvVarActivation.experiment)).thenReturn(true); + when(terminalManager.onDidOpenTerminal).thenReturn(terminalEventEmitter.event); + terminalEnvVarCollectionPrompt = new TerminalIndicatorPrompt( + instance(shell), + instance(persistentStateFactory), + instance(terminalManager), + [], + instance(activeResourceService), + instance(terminalEnvVarCollectionService), + instance(configurationService), + instance(interpreterService), + instance(experimentService), + ); + }); + + teardown(() => { + sinon.restore(); + }); + + test('Show notification when a new terminal is opened for which there is no prompt set', async () => { + const resource = Uri.file('a'); + const terminal = ({ + creationOptions: { + cwd: resource, + }, + } as unknown) as Terminal; + when(terminalEnvVarCollectionService.isTerminalPromptSetCorrectly(resource)).thenReturn(false); + when(notificationEnabled.value).thenReturn(true); + when(shell.showInformationMessage(expectedMessage, ...prompts)).thenResolve(undefined); + + await terminalEnvVarCollectionPrompt.activate(); + terminalEventEmitter.fire(terminal); + await sleep(1); + + verify(shell.showInformationMessage(expectedMessage, ...prompts)).once(); + }); + + test('Do not show notification if automatic terminal activation is turned off', async () => { + reset(configurationService); + when(configurationService.getSettings(anything())).thenReturn(({ + terminal: { + activateEnvironment: false, + }, + } as unknown) as IPythonSettings); + const resource = Uri.file('a'); + const terminal = ({ + creationOptions: { + cwd: resource, + }, + } as unknown) as Terminal; + when(terminalEnvVarCollectionService.isTerminalPromptSetCorrectly(resource)).thenReturn(false); + when(notificationEnabled.value).thenReturn(true); + when(shell.showInformationMessage(expectedMessage, ...prompts)).thenResolve(undefined); + + await terminalEnvVarCollectionPrompt.activate(); + terminalEventEmitter.fire(terminal); + await sleep(1); + + verify(shell.showInformationMessage(expectedMessage, ...prompts)).never(); + }); + + test('When not in experiment, do not show notification for the same', async () => { + const resource = Uri.file('a'); + const terminal = ({ + creationOptions: { + cwd: resource, + }, + } as unknown) as Terminal; + when(terminalEnvVarCollectionService.isTerminalPromptSetCorrectly(resource)).thenReturn(false); + when(notificationEnabled.value).thenReturn(true); + when(shell.showInformationMessage(expectedMessage, ...prompts)).thenResolve(undefined); + + reset(experimentService); + when(experimentService.inExperimentSync(TerminalEnvVarActivation.experiment)).thenReturn(false); + await terminalEnvVarCollectionPrompt.activate(); + terminalEventEmitter.fire(terminal); + await sleep(1); + + verify(shell.showInformationMessage(expectedMessage, ...prompts)).never(); + }); + + test('Do not show notification if notification is disabled', async () => { + const resource = Uri.file('a'); + const terminal = ({ + creationOptions: { + cwd: resource, + }, + } as unknown) as Terminal; + when(terminalEnvVarCollectionService.isTerminalPromptSetCorrectly(resource)).thenReturn(false); + when(notificationEnabled.value).thenReturn(false); + when(shell.showInformationMessage(expectedMessage, ...prompts)).thenResolve(undefined); + + await terminalEnvVarCollectionPrompt.activate(); + terminalEventEmitter.fire(terminal); + await sleep(1); + + verify(shell.showInformationMessage(expectedMessage, ...prompts)).never(); + }); + + test('Do not show notification when a new terminal is opened for which there is prompt set', async () => { + const resource = Uri.file('a'); + const terminal = ({ + creationOptions: { + cwd: resource, + }, + } as unknown) as Terminal; + when(terminalEnvVarCollectionService.isTerminalPromptSetCorrectly(resource)).thenReturn(true); + when(notificationEnabled.value).thenReturn(true); + when(shell.showInformationMessage(expectedMessage, ...prompts)).thenResolve(undefined); + + await terminalEnvVarCollectionPrompt.activate(); + terminalEventEmitter.fire(terminal); + await sleep(1); + + verify(shell.showInformationMessage(expectedMessage, ...prompts)).never(); + }); + + test("Disable notification if `Don't show again` is clicked", async () => { + const resource = Uri.file('a'); + const terminal = ({ + creationOptions: { + cwd: resource, + }, + } as unknown) as Terminal; + when(terminalEnvVarCollectionService.isTerminalPromptSetCorrectly(resource)).thenReturn(false); + when(notificationEnabled.value).thenReturn(true); + when(notificationEnabled.updateValue(false)).thenResolve(); + when(shell.showInformationMessage(expectedMessage, ...prompts)).thenReturn( + Promise.resolve(Common.doNotShowAgain), + ); + + await terminalEnvVarCollectionPrompt.activate(); + terminalEventEmitter.fire(terminal); + await sleep(1); + + verify(notificationEnabled.updateValue(false)).once(); + }); + + test('Do not disable notification if prompt is closed', async () => { + const resource = Uri.file('a'); + const terminal = ({ + creationOptions: { + cwd: resource, + }, + } as unknown) as Terminal; + when(terminalEnvVarCollectionService.isTerminalPromptSetCorrectly(resource)).thenReturn(false); + when(notificationEnabled.value).thenReturn(true); + when(notificationEnabled.updateValue(false)).thenResolve(); + when(shell.showInformationMessage(expectedMessage, ...prompts)).thenReturn(Promise.resolve(undefined)); + + await terminalEnvVarCollectionPrompt.activate(); + terminalEventEmitter.fire(terminal); + await sleep(1); + + verify(notificationEnabled.updateValue(false)).never(); + }); +}); diff --git a/src/test/interpreters/activation/service.unit.test.ts b/src/test/interpreters/activation/service.unit.test.ts index 9b2c121c89b4..a0f9b3bd6915 100644 --- a/src/test/interpreters/activation/service.unit.test.ts +++ b/src/test/interpreters/activation/service.unit.test.ts @@ -161,7 +161,11 @@ suite('Interpreters Activation - Python Environment Variables', () => { const shellCmd = capture(processService.shellExec).first()[0]; - const printEnvPyFile = path.join(EXTENSION_ROOT_DIR, 'pythonFiles', 'printEnvVariables.py'); + const printEnvPyFile = path.join( + EXTENSION_ROOT_DIR, + 'python_files', + 'printEnvVariables.py', + ); const expectedCommand = [ ...cmd, `echo '${getEnvironmentPrefix}'`, diff --git a/src/test/interpreters/activation/terminalEnvVarCollectionService.unit.test.ts b/src/test/interpreters/activation/terminalEnvVarCollectionService.unit.test.ts index feecf63f5577..dfe3ad8c081a 100644 --- a/src/test/interpreters/activation/terminalEnvVarCollectionService.unit.test.ts +++ b/src/test/interpreters/activation/terminalEnvVarCollectionService.unit.test.ts @@ -5,9 +5,17 @@ import * as sinon from 'sinon'; import { assert, expect } from 'chai'; -import { cloneDeep } from 'lodash'; import { mock, instance, when, anything, verify, reset } from 'ts-mockito'; -import { EnvironmentVariableCollection, ProgressLocation, Uri, WorkspaceFolder } from 'vscode'; +import * as TypeMoq from 'typemoq'; +import { + EnvironmentVariableCollection, + EnvironmentVariableMutatorOptions, + GlobalEnvironmentVariableCollection, + ProgressLocation, + Uri, + WorkspaceConfiguration, + WorkspaceFolder, +} from 'vscode'; import { IApplicationShell, IApplicationEnvironment, @@ -25,13 +33,15 @@ import { import { Interpreters } from '../../../client/common/utils/localize'; import { OSType, getOSType } from '../../../client/common/utils/platform'; import { defaultShells } from '../../../client/interpreter/activation/service'; -import { - TerminalEnvVarCollectionService, - _normCaseKeys, -} from '../../../client/interpreter/activation/terminalEnvVarCollectionService'; +import { TerminalEnvVarCollectionService } from '../../../client/terminals/envCollectionActivation/service'; import { IEnvironmentActivationService } from '../../../client/interpreter/activation/types'; import { IInterpreterService } from '../../../client/interpreter/contracts'; import { PathUtils } from '../../../client/common/platform/pathUtils'; +import { PythonEnvType } from '../../../client/pythonEnvironments/base/info'; +import { PythonEnvironment } from '../../../client/pythonEnvironments/info'; +import { IShellIntegrationDetectionService, ITerminalDeactivateService } from '../../../client/terminals/types'; +import { IEnvironmentVariablesProvider } from '../../../client/common/variables/types'; +import * as extapi from '../../../client/envExt/api.internal'; suite('Terminal Environment Variable Collection Service', () => { let platform: IPlatformService; @@ -40,21 +50,32 @@ suite('Terminal Environment Variable Collection Service', () => { let shell: IApplicationShell; let experimentService: IExperimentService; let collection: EnvironmentVariableCollection; + let globalCollection: GlobalEnvironmentVariableCollection; let applicationEnvironment: IApplicationEnvironment; let environmentActivationService: IEnvironmentActivationService; let workspaceService: IWorkspaceService; let terminalEnvVarCollectionService: TerminalEnvVarCollectionService; + let terminalDeactivateService: ITerminalDeactivateService; + let useEnvExtensionStub: sinon.SinonStub; + let pythonConfig: TypeMoq.IMock; const progressOptions = { location: ProgressLocation.Window, title: Interpreters.activatingTerminals, }; let configService: IConfigurationService; + let shellIntegrationService: IShellIntegrationDetectionService; const displayPath = 'display/path'; const customShell = 'powershell'; const defaultShell = defaultShells[getOSType()]; setup(() => { + useEnvExtensionStub = sinon.stub(extapi, 'useEnvExtension'); + useEnvExtensionStub.returns(false); + workspaceService = mock(); + terminalDeactivateService = mock(); + when(terminalDeactivateService.getScriptLocation(anything(), anything())).thenResolve(undefined); + when(terminalDeactivateService.initializeScriptParams(anything())).thenResolve(); when(workspaceService.getWorkspaceFolder(anything())).thenReturn(undefined); when(workspaceService.workspaceFolders).thenReturn(undefined); platform = mock(); @@ -62,8 +83,13 @@ suite('Terminal Environment Variable Collection Service', () => { interpreterService = mock(); context = mock(); shell = mock(); + const envVarProvider = mock(); + shellIntegrationService = mock(); + when(shellIntegrationService.isWorking()).thenResolve(true); + globalCollection = mock(); collection = mock(); - when(context.environmentVariableCollection).thenReturn(instance(collection)); + when(context.environmentVariableCollection).thenReturn(instance(globalCollection)); + when(globalCollection.getScoped(anything())).thenReturn(instance(collection)); experimentService = mock(); when(experimentService.inExperimentSync(TerminalEnvVarActivation.experiment)).thenReturn(true); applicationEnvironment = mock(); @@ -74,11 +100,15 @@ suite('Terminal Environment Variable Collection Service', () => { }) .thenResolve(); environmentActivationService = mock(); + when(environmentActivationService.getProcessEnvironmentVariables(anything(), anything())).thenResolve( + process.env, + ); configService = mock(); when(configService.getSettings(anything())).thenReturn(({ terminal: { activateEnvironment: true }, pythonPath: displayPath, } as unknown) as IPythonSettings); + when(collection.clear()).thenResolve(); terminalEnvVarCollectionService = new TerminalEnvVarCollectionService( instance(platform), instance(interpreterService), @@ -90,8 +120,13 @@ suite('Terminal Environment Variable Collection Service', () => { instance(environmentActivationService), instance(workspaceService), instance(configService), + instance(terminalDeactivateService), new PathUtils(getOSType() === OSType.Windows), + instance(shellIntegrationService), + instance(envVarProvider), ); + pythonConfig = TypeMoq.Mock.ofType(); + pythonConfig.setup((p) => p.get('terminal.shellIntegration.enabled')).returns(() => false); }); teardown(() => { @@ -121,7 +156,7 @@ suite('Terminal Environment Variable Collection Service', () => { verify(applicationEnvironment.onDidChangeShell(anything(), anything(), anything())).never(); assert(applyCollectionStub.notCalled, 'Collection should not be applied on activation'); - verify(collection.clear()).atLeast(1); + verify(globalCollection.clear()).atLeast(1); }); test('When interpreter changes, apply new activated variables to the collection', async () => { @@ -154,8 +189,7 @@ suite('Terminal Environment Variable Collection Service', () => { }); test('If activated variables are returned for custom shell, apply it correctly to the collection', async () => { - const envVars: NodeJS.ProcessEnv = { CONDA_PREFIX: 'prefix/to/conda', ..._normCaseKeys(process.env) }; - delete envVars.PATH; + const envVars: NodeJS.ProcessEnv = { CONDA_PREFIX: 'prefix/to/conda', ...process.env }; when( environmentActivationService.getActivatedEnvironmentVariables( anything(), @@ -166,17 +200,264 @@ suite('Terminal Environment Variable Collection Service', () => { ).thenResolve(envVars); when(collection.replace(anything(), anything(), anything())).thenResolve(); - when(collection.delete(anything(), anything())).thenResolve(); + when(collection.delete(anything())).thenResolve(); await terminalEnvVarCollectionService._applyCollection(undefined, customShell); + verify(collection.clear()).once(); + verify(collection.replace('CONDA_PREFIX', 'prefix/to/conda', anything())).once(); + }); + + // eslint-disable-next-line consistent-return + test('If activated variables contain PS1, prefix it using shell integration', async function () { + if (getOSType() === OSType.Windows) { + return this.skip(); + } + const envVars: NodeJS.ProcessEnv = { + CONDA_PREFIX: 'prefix/to/conda', + ...process.env, + PS1: '(envName) extra prompt', // Should not use this + }; + when( + environmentActivationService.getActivatedEnvironmentVariables(anything(), undefined, undefined, 'bash'), + ).thenResolve(envVars); + + when(interpreterService.getActiveInterpreter(anything())).thenResolve(({ + envName: 'envName', + } as unknown) as PythonEnvironment); + + when(collection.replace(anything(), anything(), anything())).thenResolve(); + when(collection.delete(anything())).thenResolve(); + let opts: EnvironmentVariableMutatorOptions | undefined; + when(collection.prepend('PS1', '(envName) ', anything())).thenCall((_, _v, o) => { + opts = o; + }); + + await terminalEnvVarCollectionService._applyCollection(undefined, customShell); + + verify(collection.clear()).once(); + verify(collection.replace('CONDA_PREFIX', 'prefix/to/conda', anything())).once(); + assert.deepEqual(opts, { applyAtProcessCreation: false, applyAtShellIntegration: true }); + }); + + test('Respect VIRTUAL_ENV_DISABLE_PROMPT when setting PS1 for venv', async () => { + when(platform.osType).thenReturn(OSType.Linux); + const envVars: NodeJS.ProcessEnv = { + VIRTUAL_BIN: 'prefix/to/conda', + ...process.env, + VIRTUAL_ENV_DISABLE_PROMPT: '1', + }; + when( + environmentActivationService.getActivatedEnvironmentVariables(anything(), undefined, undefined, 'bash'), + ).thenResolve(envVars); + when(interpreterService.getActiveInterpreter(anything())).thenResolve(({ + type: PythonEnvType.Virtual, + envName: 'envName', + envPath: 'prefix/to/conda', + } as unknown) as PythonEnvironment); + + when(collection.replace(anything(), anything(), anything())).thenResolve(); + when(collection.delete(anything())).thenResolve(); + when(collection.prepend('PS1', anything(), anything())).thenReturn(); + + await terminalEnvVarCollectionService._applyCollection(undefined, 'bash'); + + verify(collection.prepend('PS1', anything(), anything())).never(); + }); + + test('Otherwise set PS1 for venv even if PS1 is not returned', async () => { + when(platform.osType).thenReturn(OSType.Linux); + const envVars: NodeJS.ProcessEnv = { + VIRTUAL_BIN: 'prefix/to/conda', + ...process.env, + }; + when( + environmentActivationService.getActivatedEnvironmentVariables(anything(), undefined, undefined, 'bash'), + ).thenResolve(envVars); + when(interpreterService.getActiveInterpreter(anything())).thenResolve(({ + type: PythonEnvType.Virtual, + envName: 'envName', + envPath: 'prefix/to/conda', + } as unknown) as PythonEnvironment); + + when(collection.replace(anything(), anything(), anything())).thenResolve(); + when(collection.delete(anything())).thenResolve(); + when(collection.prepend('PS1', '(envName) ', anything())).thenReturn(); + + await terminalEnvVarCollectionService._applyCollection(undefined, 'bash'); + + verify(collection.prepend('PS1', '(envName) ', anything())).once(); + }); + + test('Respect CONDA_PROMPT_MODIFIER when setting PS1 for conda', async () => { + when(platform.osType).thenReturn(OSType.Linux); + const envVars: NodeJS.ProcessEnv = { + CONDA_PREFIX: 'prefix/to/conda', + ...process.env, + CONDA_PROMPT_MODIFIER: '(envName)', + }; + when( + environmentActivationService.getActivatedEnvironmentVariables(anything(), undefined, undefined, 'bash'), + ).thenResolve(envVars); + when(interpreterService.getActiveInterpreter(anything())).thenResolve(({ + type: PythonEnvType.Conda, + envName: 'envName', + envPath: 'prefix/to/conda', + } as unknown) as PythonEnvironment); + + when(collection.replace(anything(), anything(), anything())).thenResolve(); + when(collection.delete(anything())).thenResolve(); + let opts: EnvironmentVariableMutatorOptions | undefined; + when(collection.prepend('PS1', '(envName) ', anything())).thenCall((_, _v, o) => { + opts = o; + }); + + await terminalEnvVarCollectionService._applyCollection(undefined, 'bash'); + + verify(collection.clear()).once(); verify(collection.replace('CONDA_PREFIX', 'prefix/to/conda', anything())).once(); - verify(collection.delete('PATH', anything())).once(); + assert.deepEqual(opts, { applyAtProcessCreation: false, applyAtShellIntegration: true }); + }); + + test('Prepend only "prepend portion of PATH" where applicable', async () => { + const processEnv = { PATH: 'hello/1/2/3' }; + reset(environmentActivationService); + when(environmentActivationService.getProcessEnvironmentVariables(anything(), anything())).thenResolve( + processEnv, + ); + const prependedPart = 'path/to/activate/dir:'; + const envVars: NodeJS.ProcessEnv = { PATH: `${prependedPart}${processEnv.PATH}` }; + when( + environmentActivationService.getActivatedEnvironmentVariables( + anything(), + undefined, + undefined, + customShell, + ), + ).thenResolve(envVars); + + when(collection.replace(anything(), anything(), anything())).thenResolve(); + when(collection.delete(anything())).thenResolve(); + let opts: EnvironmentVariableMutatorOptions | undefined; + when(collection.prepend('PATH', anything(), anything())).thenCall((_, _v, o) => { + opts = o; + }); + + await terminalEnvVarCollectionService._applyCollection(undefined, customShell); + + verify(collection.clear()).once(); + verify(collection.prepend('PATH', prependedPart, anything())).once(); + verify(collection.replace('PATH', anything(), anything())).never(); + assert.deepEqual(opts, { applyAtProcessCreation: true, applyAtShellIntegration: true }); + }); + + test('Also prepend deactivate script location if available', async () => { + reset(terminalDeactivateService); + when(terminalDeactivateService.initializeScriptParams(anything())).thenReject(); // Verify we swallow errors from here + when(terminalDeactivateService.getScriptLocation(anything(), anything())).thenResolve('scriptLocation'); + const processEnv = { PATH: 'hello/1/2/3' }; + reset(environmentActivationService); + when(environmentActivationService.getProcessEnvironmentVariables(anything(), anything())).thenResolve( + processEnv, + ); + const prependedPart = 'path/to/activate/dir:'; + const envVars: NodeJS.ProcessEnv = { PATH: `${prependedPart}${processEnv.PATH}` }; + when( + environmentActivationService.getActivatedEnvironmentVariables( + anything(), + undefined, + undefined, + customShell, + ), + ).thenResolve(envVars); + + when(collection.replace(anything(), anything(), anything())).thenResolve(); + when(collection.delete(anything())).thenResolve(); + let opts: EnvironmentVariableMutatorOptions | undefined; + when(collection.prepend('PATH', anything(), anything())).thenCall((_, _v, o) => { + opts = o; + }); + + await terminalEnvVarCollectionService._applyCollection(undefined, customShell); + + verify(collection.clear()).once(); + const separator = getOSType() === OSType.Windows ? ';' : ':'; + verify(collection.prepend('PATH', `scriptLocation${separator}${prependedPart}`, anything())).once(); + verify(collection.replace('PATH', anything(), anything())).never(); + assert.deepEqual(opts, { applyAtProcessCreation: true, applyAtShellIntegration: true }); + }); + + test('Prepend full PATH with separator otherwise', async () => { + const processEnv = { PATH: 'hello/1/2/3' }; + reset(environmentActivationService); + when(environmentActivationService.getProcessEnvironmentVariables(anything(), anything())).thenResolve( + processEnv, + ); + const separator = getOSType() === OSType.Windows ? ';' : ':'; + const finalPath = 'hello/3/2/1'; + const envVars: NodeJS.ProcessEnv = { PATH: finalPath }; + when( + environmentActivationService.getActivatedEnvironmentVariables( + anything(), + undefined, + undefined, + customShell, + ), + ).thenResolve(envVars); + + when(collection.replace(anything(), anything(), anything())).thenResolve(); + when(collection.delete(anything())).thenResolve(); + let opts: EnvironmentVariableMutatorOptions | undefined; + when(collection.prepend('PATH', anything(), anything())).thenCall((_, _v, o) => { + opts = o; + }); + + await terminalEnvVarCollectionService._applyCollection(undefined, customShell); + + verify(collection.clear()).once(); + verify(collection.prepend('PATH', `${finalPath}${separator}`, anything())).once(); + verify(collection.replace('PATH', anything(), anything())).never(); + assert.deepEqual(opts, { applyAtProcessCreation: true, applyAtShellIntegration: true }); + }); + + test('Prepend full PATH with separator otherwise', async () => { + reset(terminalDeactivateService); + when(terminalDeactivateService.initializeScriptParams(anything())).thenResolve(); + when(terminalDeactivateService.getScriptLocation(anything(), anything())).thenResolve('scriptLocation'); + const processEnv = { PATH: 'hello/1/2/3' }; + reset(environmentActivationService); + when(environmentActivationService.getProcessEnvironmentVariables(anything(), anything())).thenResolve( + processEnv, + ); + const separator = getOSType() === OSType.Windows ? ';' : ':'; + const finalPath = 'hello/3/2/1'; + const envVars: NodeJS.ProcessEnv = { PATH: finalPath }; + when( + environmentActivationService.getActivatedEnvironmentVariables( + anything(), + undefined, + undefined, + customShell, + ), + ).thenResolve(envVars); + + when(collection.replace(anything(), anything(), anything())).thenResolve(); + when(collection.delete(anything())).thenResolve(); + let opts: EnvironmentVariableMutatorOptions | undefined; + when(collection.prepend('PATH', anything(), anything())).thenCall((_, _v, o) => { + opts = o; + }); + + await terminalEnvVarCollectionService._applyCollection(undefined, customShell); + + verify(collection.clear()).once(); + verify(collection.prepend('PATH', `scriptLocation${separator}${finalPath}${separator}`, anything())).once(); + verify(collection.replace('PATH', anything(), anything())).never(); + assert.deepEqual(opts, { applyAtProcessCreation: true, applyAtShellIntegration: true }); }); test('Verify envs are not applied if env activation is disabled', async () => { - const envVars: NodeJS.ProcessEnv = { CONDA_PREFIX: 'prefix/to/conda', ..._normCaseKeys(process.env) }; - delete envVars.PATH; + const envVars: NodeJS.ProcessEnv = { CONDA_PREFIX: 'prefix/to/conda', ...process.env }; when( environmentActivationService.getActivatedEnvironmentVariables( anything(), @@ -187,7 +468,7 @@ suite('Terminal Environment Variable Collection Service', () => { ).thenResolve(envVars); when(collection.replace(anything(), anything(), anything())).thenResolve(); - when(collection.delete(anything(), anything())).thenResolve(); + when(collection.delete(anything())).thenResolve(); reset(configService); when(configService.getSettings(anything())).thenReturn(({ terminal: { activateEnvironment: false }, @@ -196,13 +477,12 @@ suite('Terminal Environment Variable Collection Service', () => { await terminalEnvVarCollectionService._applyCollection(undefined, customShell); + verify(collection.clear()).once(); verify(collection.replace('CONDA_PREFIX', 'prefix/to/conda', anything())).never(); - verify(collection.delete('PATH', anything())).never(); }); - test('Verify correct scope is used when applying envs and setting description', async () => { - const envVars: NodeJS.ProcessEnv = { CONDA_PREFIX: 'prefix/to/conda', ..._normCaseKeys(process.env) }; - delete envVars.PATH; + test('Verify correct options are used when applying envs and setting description', async () => { + const envVars: NodeJS.ProcessEnv = { CONDA_PREFIX: 'prefix/to/conda', ...process.env }; const resource = Uri.file('a'); const workspaceFolder: WorkspaceFolder = { uri: Uri.file('workspacePath'), @@ -214,64 +494,236 @@ suite('Terminal Environment Variable Collection Service', () => { environmentActivationService.getActivatedEnvironmentVariables(resource, undefined, undefined, customShell), ).thenResolve(envVars); - when(collection.replace(anything(), anything(), anything())).thenCall((_e, _v, scope) => { - assert.deepEqual(scope, { workspaceFolder }); - return Promise.resolve(); - }); - when(collection.delete(anything(), anything())).thenCall((_e, scope) => { - assert.deepEqual(scope, { workspaceFolder }); - return Promise.resolve(); - }); - let description = ''; - when(collection.setDescription(anything(), anything())).thenCall((d, scope) => { - assert.deepEqual(scope, { workspaceFolder }); - description = d.value; - }); + when(collection.replace(anything(), anything(), anything())).thenCall( + (_e, _v, options: EnvironmentVariableMutatorOptions) => { + assert.deepEqual(options, { applyAtShellIntegration: true, applyAtProcessCreation: true }); + return Promise.resolve(); + }, + ); await terminalEnvVarCollectionService._applyCollection(resource, customShell); + verify(collection.clear()).once(); verify(collection.replace('CONDA_PREFIX', 'prefix/to/conda', anything())).once(); - verify(collection.delete('PATH', anything())).once(); - expect(description).to.equal(`${Interpreters.activateTerminalDescription} \`${displayPath}\``); }); - test('Only relative changes to previously applied variables are applied to the collection', async () => { + test('Correct track that prompt was set for non-Windows bash where PS1 is set', async () => { + when(platform.osType).thenReturn(OSType.Linux); + const envVars: NodeJS.ProcessEnv = { VIRTUAL_ENV: 'prefix/to/venv', PS1: '(.venv)', ...process.env }; + const ps1Shell = 'bash'; + const resource = Uri.file('a'); + const workspaceFolder: WorkspaceFolder = { + uri: Uri.file('workspacePath'), + name: 'workspace1', + index: 0, + }; + when(interpreterService.getActiveInterpreter(resource)).thenResolve(({ + type: PythonEnvType.Virtual, + } as unknown) as PythonEnvironment); + when(workspaceService.getWorkspaceFolder(resource)).thenReturn(workspaceFolder); + when( + environmentActivationService.getActivatedEnvironmentVariables(resource, undefined, undefined, ps1Shell), + ).thenResolve(envVars); + when(collection.replace(anything(), anything(), anything())).thenReturn(); + + await terminalEnvVarCollectionService._applyCollection(resource, ps1Shell); + + const result = terminalEnvVarCollectionService.isTerminalPromptSetCorrectly(resource); + + expect(result).to.equal(true); + }); + + test('Correct track that prompt was set for PS1 if shell integration is disabled', async () => { + reset(shellIntegrationService); + when(shellIntegrationService.isWorking()).thenResolve(false); + when(platform.osType).thenReturn(OSType.Linux); + const envVars: NodeJS.ProcessEnv = { VIRTUAL_ENV: 'prefix/to/venv', PS1: '(.venv)', ...process.env }; + const ps1Shell = 'bash'; + const resource = Uri.file('a'); + const workspaceFolder: WorkspaceFolder = { + uri: Uri.file('workspacePath'), + name: 'workspace1', + index: 0, + }; + when(interpreterService.getActiveInterpreter(resource)).thenResolve(({ + type: PythonEnvType.Virtual, + } as unknown) as PythonEnvironment); + when(workspaceService.getWorkspaceFolder(resource)).thenReturn(workspaceFolder); + when( + environmentActivationService.getActivatedEnvironmentVariables(resource, undefined, undefined, ps1Shell), + ).thenResolve(envVars); + when(collection.replace(anything(), anything(), anything())).thenReturn(); + + await terminalEnvVarCollectionService._applyCollection(resource, ps1Shell); + + const result = terminalEnvVarCollectionService.isTerminalPromptSetCorrectly(resource); + + expect(result).to.equal(false); + }); + + test('Correct track that prompt was set for non-Windows where PS1 is not set but should be set', async () => { + when(platform.osType).thenReturn(OSType.Linux); + const envVars: NodeJS.ProcessEnv = { CONDA_PREFIX: 'prefix/to/conda', ...process.env }; + const ps1Shell = 'zsh'; + const resource = Uri.file('a'); + const workspaceFolder: WorkspaceFolder = { + uri: Uri.file('workspacePath'), + name: 'workspace1', + index: 0, + }; + when(interpreterService.getActiveInterpreter(resource)).thenResolve(({ + type: PythonEnvType.Conda, + envName: 'envName', + envPath: 'prefix/to/conda', + } as unknown) as PythonEnvironment); + when(workspaceService.getWorkspaceFolder(resource)).thenReturn(workspaceFolder); + when( + environmentActivationService.getActivatedEnvironmentVariables(resource, undefined, undefined, ps1Shell), + ).thenResolve(envVars); + when(collection.replace(anything(), anything(), anything())).thenReturn(); + + await terminalEnvVarCollectionService._applyCollection(resource, ps1Shell); + + const result = terminalEnvVarCollectionService.isTerminalPromptSetCorrectly(resource); + + expect(result).to.equal(true); + }); + + test('Correct track that prompt was not set for non-Windows where PS1 is not set but env name is base', async () => { + when(platform.osType).thenReturn(OSType.Linux); const envVars: NodeJS.ProcessEnv = { - RANDOM_VAR: 'random', CONDA_PREFIX: 'prefix/to/conda', - ..._normCaseKeys(process.env), + ...process.env, + CONDA_PROMPT_MODIFIER: '(base)', }; + const ps1Shell = 'zsh'; + const resource = Uri.file('a'); + const workspaceFolder: WorkspaceFolder = { + uri: Uri.file('workspacePath'), + name: 'workspace1', + index: 0, + }; + when(interpreterService.getActiveInterpreter(resource)).thenResolve(({ + type: PythonEnvType.Conda, + envName: 'base', + envPath: 'prefix/to/conda', + } as unknown) as PythonEnvironment); + when(workspaceService.getWorkspaceFolder(resource)).thenReturn(workspaceFolder); when( - environmentActivationService.getActivatedEnvironmentVariables( - anything(), - undefined, - undefined, - customShell, - ), + environmentActivationService.getActivatedEnvironmentVariables(resource, undefined, undefined, ps1Shell), ).thenResolve(envVars); + when(collection.replace(anything(), anything(), anything())).thenReturn(); - when(collection.replace(anything(), anything(), anything())).thenResolve(); - when(collection.delete(anything(), anything())).thenResolve(); + await terminalEnvVarCollectionService._applyCollection(resource, ps1Shell); - await terminalEnvVarCollectionService._applyCollection(undefined, customShell); + const result = terminalEnvVarCollectionService.isTerminalPromptSetCorrectly(resource); - const newEnvVars = cloneDeep(envVars); - delete newEnvVars.CONDA_PREFIX; - newEnvVars.RANDOM_VAR = undefined; // Deleting the variable from the collection is the same as setting it to undefined. - reset(environmentActivationService); + expect(result).to.equal(false); + }); + + test('Correct track that prompt was not set for non-Windows fish where PS1 is not set', async () => { + when(platform.osType).thenReturn(OSType.Linux); + const envVars: NodeJS.ProcessEnv = { CONDA_PREFIX: 'prefix/to/conda', ...process.env }; + const ps1Shell = 'fish'; + const resource = Uri.file('a'); + const workspaceFolder: WorkspaceFolder = { + uri: Uri.file('workspacePath'), + name: 'workspace1', + index: 0, + }; + when(interpreterService.getActiveInterpreter(resource)).thenResolve(({ + type: PythonEnvType.Conda, + envName: 'envName', + envPath: 'prefix/to/conda', + } as unknown) as PythonEnvironment); + when(workspaceService.getWorkspaceFolder(resource)).thenReturn(workspaceFolder); when( - environmentActivationService.getActivatedEnvironmentVariables( - anything(), - undefined, - undefined, - customShell, - ), - ).thenResolve(newEnvVars); + environmentActivationService.getActivatedEnvironmentVariables(resource, undefined, undefined, ps1Shell), + ).thenResolve(envVars); + when(collection.replace(anything(), anything(), anything())).thenReturn(); - await terminalEnvVarCollectionService._applyCollection(undefined, customShell); + await terminalEnvVarCollectionService._applyCollection(resource, ps1Shell); + + const result = terminalEnvVarCollectionService.isTerminalPromptSetCorrectly(resource); + + expect(result).to.equal(false); + }); + + test('Correct track that prompt was set correctly for global interpreters', async () => { + when(platform.osType).thenReturn(OSType.Linux); + const ps1Shell = 'zsh'; + const resource = Uri.file('a'); + const workspaceFolder: WorkspaceFolder = { + uri: Uri.file('workspacePath'), + name: 'workspace1', + index: 0, + }; + when(interpreterService.getActiveInterpreter(resource)).thenResolve(({ + type: undefined, + } as unknown) as PythonEnvironment); + when(workspaceService.getWorkspaceFolder(resource)).thenReturn(workspaceFolder); + when( + environmentActivationService.getActivatedEnvironmentVariables(resource, undefined, undefined, ps1Shell), + ).thenResolve(undefined); + when(collection.replace(anything(), anything(), anything())).thenReturn(); + + await terminalEnvVarCollectionService._applyCollection(resource, ps1Shell); + + const result = terminalEnvVarCollectionService.isTerminalPromptSetCorrectly(resource); + + expect(result).to.equal(true); + }); + + test('Correct track that prompt was set for Windows when not using powershell', async () => { + when(platform.osType).thenReturn(OSType.Windows); + const envVars: NodeJS.ProcessEnv = { VIRTUAL_ENV: 'prefix/to/venv', ...process.env }; + const windowsShell = 'cmd'; + const resource = Uri.file('a'); + const workspaceFolder: WorkspaceFolder = { + uri: Uri.file('workspacePath'), + name: 'workspace1', + index: 0, + }; + when(interpreterService.getActiveInterpreter(resource)).thenResolve(({ + type: PythonEnvType.Virtual, + } as unknown) as PythonEnvironment); + when(workspaceService.getWorkspaceFolder(resource)).thenReturn(workspaceFolder); + when( + environmentActivationService.getActivatedEnvironmentVariables(resource, undefined, undefined, windowsShell), + ).thenResolve(envVars); + when(collection.replace(anything(), anything(), anything())).thenReturn(); + + await terminalEnvVarCollectionService._applyCollection(resource, windowsShell); + + const result = terminalEnvVarCollectionService.isTerminalPromptSetCorrectly(resource); + + expect(result).to.equal(true); + }); + + test('Correct track that prompt was not set for Windows when using powershell', async () => { + when(platform.osType).thenReturn(OSType.Linux); + const envVars: NodeJS.ProcessEnv = { VIRTUAL_ENV: 'prefix/to/venv', ...process.env }; + const windowsShell = 'powershell'; + const resource = Uri.file('a'); + const workspaceFolder: WorkspaceFolder = { + uri: Uri.file('workspacePath'), + name: 'workspace1', + index: 0, + }; + when(interpreterService.getActiveInterpreter(resource)).thenResolve(({ + type: PythonEnvType.Virtual, + } as unknown) as PythonEnvironment); + when(workspaceService.getWorkspaceFolder(resource)).thenReturn(workspaceFolder); + when( + environmentActivationService.getActivatedEnvironmentVariables(resource, undefined, undefined, windowsShell), + ).thenResolve(envVars); + when(collection.replace(anything(), anything(), anything())).thenReturn(); + + await terminalEnvVarCollectionService._applyCollection(resource, windowsShell); + + const result = terminalEnvVarCollectionService.isTerminalPromptSetCorrectly(resource); - verify(collection.delete('CONDA_PREFIX', anything())).once(); - verify(collection.delete('RANDOM_VAR', anything())).once(); + expect(result).to.equal(false); }); test('If no activated variables are returned for custom shell, fallback to using default shell', async () => { @@ -283,7 +735,7 @@ suite('Terminal Environment Variable Collection Service', () => { customShell, ), ).thenResolve(undefined); - const envVars = { CONDA_PREFIX: 'prefix/to/conda', ..._normCaseKeys(process.env) }; + const envVars = { CONDA_PREFIX: 'prefix/to/conda', ...process.env }; when( environmentActivationService.getActivatedEnvironmentVariables( anything(), @@ -294,12 +746,11 @@ suite('Terminal Environment Variable Collection Service', () => { ).thenResolve(envVars); when(collection.replace(anything(), anything(), anything())).thenResolve(); - when(collection.delete(anything(), anything())).thenResolve(); await terminalEnvVarCollectionService._applyCollection(undefined, customShell); verify(collection.replace('CONDA_PREFIX', 'prefix/to/conda', anything())).once(); - verify(collection.delete(anything(), anything())).never(); + verify(collection.clear()).once(); }); test('If no activated variables are returned for default shell, clear collection', async () => { @@ -313,12 +764,10 @@ suite('Terminal Environment Variable Collection Service', () => { ).thenResolve(undefined); when(collection.replace(anything(), anything(), anything())).thenResolve(); - when(collection.delete(anything(), anything())).thenResolve(); - when(collection.setDescription(anything(), anything())).thenReturn(); + when(collection.delete(anything())).thenResolve(); await terminalEnvVarCollectionService._applyCollection(undefined, defaultShell?.shell); - verify(collection.clear(anything())).once(); - verify(collection.setDescription(anything(), anything())).never(); + verify(collection.clear()).once(); }); }); diff --git a/src/test/interpreters/autoSelection/index.unit.test.ts b/src/test/interpreters/autoSelection/index.unit.test.ts index f9c57f867085..6c5473546614 100644 --- a/src/test/interpreters/autoSelection/index.unit.test.ts +++ b/src/test/interpreters/autoSelection/index.unit.test.ts @@ -14,7 +14,7 @@ import { WorkspaceService } from '../../../client/common/application/workspace'; import { PersistentState, PersistentStateFactory } from '../../../client/common/persistentState'; import { FileSystem } from '../../../client/common/platform/fileSystem'; import { IFileSystem } from '../../../client/common/platform/types'; -import { IPersistentStateFactory, Resource } from '../../../client/common/types'; +import { IExperimentService, IPersistentStateFactory, Resource } from '../../../client/common/types'; import { createDeferred } from '../../../client/common/utils/async'; import { InterpreterAutoSelectionService } from '../../../client/interpreter/autoSelection'; import { InterpreterAutoSelectionProxyService } from '../../../client/interpreter/autoSelection/proxy'; @@ -23,6 +23,7 @@ import { EnvironmentTypeComparer } from '../../../client/interpreter/configurati import { IInterpreterHelper, IInterpreterService, WorkspacePythonPath } from '../../../client/interpreter/contracts'; import { InterpreterHelper } from '../../../client/interpreter/helpers'; import { InterpreterService } from '../../../client/interpreter/interpreterService'; +import { PythonEnvType } from '../../../client/pythonEnvironments/base/info'; import { EnvironmentType, PythonEnvironment } from '../../../client/pythonEnvironments/info'; import * as Telemetry from '../../../client/telemetry'; import { EventName } from '../../../client/telemetry/constants'; @@ -40,6 +41,7 @@ suite('Interpreters - Auto Selection', () => { let helper: IInterpreterHelper; let proxy: IInterpreterAutoSelectionProxyService; let interpreterService: IInterpreterService; + let experimentService: IExperimentService; let sendTelemetryEventStub: sinon.SinonStub; let telemetryEvents: { eventName: string; properties: Record }[] = []; class InterpreterAutoSelectionServiceTest extends InterpreterAutoSelectionService { @@ -63,6 +65,8 @@ suite('Interpreters - Auto Selection', () => { helper = mock(InterpreterHelper); proxy = mock(InterpreterAutoSelectionProxyService); interpreterService = mock(InterpreterService); + experimentService = mock(); + when(experimentService.inExperimentSync(anything())).thenReturn(false); const interpreterComparer = new EnvironmentTypeComparer(instance(helper)); @@ -74,6 +78,7 @@ suite('Interpreters - Auto Selection', () => { interpreterComparer, instance(proxy), instance(helper), + instance(experimentService), ); when(interpreterService.refreshPromise).thenReturn(undefined); @@ -139,6 +144,12 @@ suite('Interpreters - Auto Selection', () => { undefined, ), ).thenReturn(instance(state)); + when( + stateFactory.createGlobalPersistentState( + 'autoSelectionInterpretersQueriedOnce', + undefined, + ), + ).thenReturn(instance(state)); when(workspaceService.getWorkspaceFolderIdentifier(anything(), '')).thenReturn('workspaceIdentifier'); autoSelectionService.onDidChangeAutoSelectedInterpreter(() => { @@ -150,6 +161,7 @@ suite('Interpreters - Auto Selection', () => { test('If there is a local environment select it', async () => { const localEnv = { envType: EnvironmentType.Venv, + type: PythonEnvType.Virtual, envPath: path.join(workspacePath, '.venv'), version: { major: 3, minor: 10, patch: 0 }, } as PythonEnvironment; @@ -157,6 +169,7 @@ suite('Interpreters - Auto Selection', () => { when(interpreterService.getInterpreters(resource)).thenCall((_) => [ { envType: EnvironmentType.Conda, + type: PythonEnvType.Conda, envPath: path.join('some', 'conda', 'env'), version: { major: 3, minor: 7, patch: 2 }, } as PythonEnvironment, @@ -205,6 +218,13 @@ suite('Interpreters - Auto Selection', () => { test('getInterpreters is called with ignoreCache at true if there is no value set in the workspace persistent state', async () => { const interpreterComparer = new EnvironmentTypeComparer(instance(helper)); + + const globalQueriedState = mock(PersistentState) as PersistentState; + when(globalQueriedState.value).thenReturn(true); + when(stateFactory.createGlobalPersistentState(anyString(), undefined)).thenReturn( + instance(globalQueriedState), + ); + const queryState = mock(PersistentState) as PersistentState; when(queryState.value).thenReturn(undefined); @@ -233,6 +253,7 @@ suite('Interpreters - Auto Selection', () => { interpreterComparer, instance(proxy), instance(helper), + instance(experimentService), ); autoSelectionService.initializeStore = () => Promise.resolve(); @@ -272,6 +293,7 @@ suite('Interpreters - Auto Selection', () => { interpreterComparer, instance(proxy), instance(helper), + instance(experimentService), ); autoSelectionService.initializeStore = () => Promise.resolve(); @@ -306,6 +328,7 @@ suite('Interpreters - Auto Selection', () => { interpreterComparer, instance(proxy), instance(helper), + instance(experimentService), ); autoSelectionService.initializeStore = () => Promise.resolve(); @@ -349,6 +372,7 @@ suite('Interpreters - Auto Selection', () => { interpreterComparer, instance(proxy), instance(helper), + instance(experimentService), ); autoSelectionService.initializeStore = () => Promise.resolve(); @@ -382,6 +406,10 @@ suite('Interpreters - Auto Selection', () => { when(stateFactory.createWorkspacePersistentState(anyString(), undefined)).thenReturn( instance(queryState), ); + when(queryState.value).thenReturn(undefined); + when(stateFactory.createGlobalPersistentState(anyString(), undefined)).thenReturn( + instance(queryState), + ); let initialize = false; let eventFired = false; diff --git a/src/test/interpreters/display.unit.test.ts b/src/test/interpreters/display.unit.test.ts index 3537425f2efc..d9be806ff709 100644 --- a/src/test/interpreters/display.unit.test.ts +++ b/src/test/interpreters/display.unit.test.ts @@ -32,6 +32,7 @@ import { IServiceContainer } from '../../client/ioc/types'; import * as logging from '../../client/logging'; import { EnvironmentType, PythonEnvironment } from '../../client/pythonEnvironments/info'; import { ThemeColor } from '../mocks/vsc'; +import * as extapi from '../../client/envExt/api.internal'; const info: PythonEnvironment = { architecture: Architecture.Unknown, @@ -58,6 +59,7 @@ suite('Interpreters Display', () => { let pathUtils: TypeMoq.IMock; let languageStatusItem: TypeMoq.IMock; let traceLogStub: sinon.SinonStub; + let shouldEnvExtHandleActivationStub: sinon.SinonStub; async function createInterpreterDisplay(filters: IInterpreterStatusbarVisibilityFilter[] = []) { interpreterDisplay = new InterpreterDisplay(serviceContainer.object); try { @@ -67,6 +69,9 @@ suite('Interpreters Display', () => { } async function setupMocks(useLanguageStatus: boolean) { + shouldEnvExtHandleActivationStub = sinon.stub(extapi, 'shouldEnvExtHandleActivation'); + shouldEnvExtHandleActivationStub.returns(false); + serviceContainer = TypeMoq.Mock.ofType(); workspaceService = TypeMoq.Mock.ofType(); applicationShell = TypeMoq.Mock.ofType(); diff --git a/src/test/interpreters/display/progressDisplay.unit.test.ts b/src/test/interpreters/display/progressDisplay.unit.test.ts index 77c5f16f4471..b1acecd44434 100644 --- a/src/test/interpreters/display/progressDisplay.unit.test.ts +++ b/src/test/interpreters/display/progressDisplay.unit.test.ts @@ -11,7 +11,7 @@ import { Commands } from '../../../client/common/constants'; import { createDeferred, Deferred } from '../../../client/common/utils/async'; import { Interpreters } from '../../../client/common/utils/localize'; import { IComponentAdapter } from '../../../client/interpreter/contracts'; -import { InterpreterLocatorProgressStatubarHandler } from '../../../client/interpreter/display/progressDisplay'; +import { InterpreterLocatorProgressStatusBarHandler } from '../../../client/interpreter/display/progressDisplay'; import { ProgressNotificationEvent, ProgressReportStage } from '../../../client/pythonEnvironments/base/locator'; import { noop } from '../../core'; @@ -41,7 +41,7 @@ suite('Interpreters - Display Progress', () => { }); test('Display discovering message when refreshing interpreters for the first time', async () => { const shell = mock(ApplicationShell); - const statusBar = new InterpreterLocatorProgressStatubarHandler(instance(shell), [], componentAdapter); + const statusBar = new InterpreterLocatorProgressStatusBarHandler(instance(shell), [], componentAdapter); when(shell.withProgress(anything(), anything())).thenResolve(); await statusBar.activate(); @@ -53,7 +53,7 @@ suite('Interpreters - Display Progress', () => { test('Display refreshing message when refreshing interpreters for the second time', async () => { const shell = mock(ApplicationShell); - const statusBar = new InterpreterLocatorProgressStatubarHandler(instance(shell), [], componentAdapter); + const statusBar = new InterpreterLocatorProgressStatusBarHandler(instance(shell), [], componentAdapter); when(shell.withProgress(anything(), anything())).thenResolve(); await statusBar.activate(); @@ -70,7 +70,7 @@ suite('Interpreters - Display Progress', () => { test('Progress message is hidden when loading has completed', async () => { const shell = mock(ApplicationShell); - const statusBar = new InterpreterLocatorProgressStatubarHandler(instance(shell), [], componentAdapter); + const statusBar = new InterpreterLocatorProgressStatusBarHandler(instance(shell), [], componentAdapter); when(shell.withProgress(anything(), anything())).thenResolve(); await statusBar.activate(); diff --git a/src/test/debugger/extension/configuration/launch.json/interpreterPathCommand.unit.test.ts b/src/test/interpreters/interpreterPathCommand.unit.test.ts similarity index 69% rename from src/test/debugger/extension/configuration/launch.json/interpreterPathCommand.unit.test.ts rename to src/test/interpreters/interpreterPathCommand.unit.test.ts index c773e1cbd5bc..8d45ad82577c 100644 --- a/src/test/debugger/extension/configuration/launch.json/interpreterPathCommand.unit.test.ts +++ b/src/test/interpreters/interpreterPathCommand.unit.test.ts @@ -8,20 +8,24 @@ import * as sinon from 'sinon'; import { anything, instance, mock, when } from 'ts-mockito'; import * as TypeMoq from 'typemoq'; import { Uri } from 'vscode'; -import { IDisposable } from '../../../../../client/common/types'; -import * as commandApis from '../../../../../client/common/vscodeApis/commandApis'; -import { InterpreterPathCommand } from '../../../../../client/debugger/extension/configuration/launch.json/interpreterPathCommand'; -import { IInterpreterService } from '../../../../../client/interpreter/contracts'; -import { PythonEnvironment } from '../../../../../client/pythonEnvironments/info'; +import { IDisposable } from '../../client/common/types'; +import * as commandApis from '../../client/common/vscodeApis/commandApis'; +import { InterpreterPathCommand } from '../../client/interpreter/interpreterPathCommand'; +import { IInterpreterService } from '../../client/interpreter/contracts'; +import { PythonEnvironment } from '../../client/pythonEnvironments/info'; +import * as workspaceApis from '../../client/common/vscodeApis/workspaceApis'; suite('Interpreter Path Command', () => { let interpreterService: IInterpreterService; let interpreterPathCommand: InterpreterPathCommand; let registerCommandStub: sinon.SinonStub; + let getConfigurationStub: sinon.SinonStub; + setup(() => { interpreterService = mock(); registerCommandStub = sinon.stub(commandApis, 'registerCommand'); interpreterPathCommand = new InterpreterPathCommand(instance(interpreterService), []); + getConfigurationStub = sinon.stub(workspaceApis, 'getConfiguration'); }); teardown(() => { @@ -43,9 +47,9 @@ suite('Interpreter Path Command', () => { }); test('If `workspaceFolder` property exists in `args`, it is used to retrieve setting from config', async () => { - const args = { workspaceFolder: 'folderPath' }; + const args = { workspaceFolder: 'folderPath', type: 'debugpy' }; when(interpreterService.getActiveInterpreter(anything())).thenCall((arg) => { - assert.deepEqual(arg, Uri.parse('folderPath')); + assert.deepEqual(arg, Uri.file('folderPath')); return Promise.resolve({ path: 'settingValue' }) as unknown; }); @@ -56,7 +60,7 @@ suite('Interpreter Path Command', () => { test('If `args[1]` is defined, it is used to retrieve setting from config', async () => { const args = ['command', 'folderPath']; when(interpreterService.getActiveInterpreter(anything())).thenCall((arg) => { - assert.deepEqual(arg, Uri.parse('folderPath')); + assert.deepEqual(arg, Uri.file('folderPath')); return Promise.resolve({ path: 'settingValue' }) as unknown; }); @@ -64,7 +68,22 @@ suite('Interpreter Path Command', () => { expect(setting).to.equal('settingValue'); }); + test('If interpreter path contains spaces, double quote it before returning', async () => { + const args = ['command', 'folderPath']; + when(interpreterService.getActiveInterpreter(anything())).thenCall((arg) => { + assert.deepEqual(arg, Uri.file('folderPath')); + + return Promise.resolve({ path: 'setting Value' }) as unknown; + }); + const setting = await interpreterPathCommand._getSelectedInterpreterPath(args); + expect(setting).to.equal('"setting Value"'); + }); + test('If neither of these exists, value of workspace folder is `undefined`', async () => { + getConfigurationStub.withArgs('python').returns({ + get: sinon.stub().returns(false), + }); + const args = ['command']; when(interpreterService.getActiveInterpreter(undefined)).thenReturn( @@ -73,14 +92,4 @@ suite('Interpreter Path Command', () => { const setting = await interpreterPathCommand._getSelectedInterpreterPath(args); expect(setting).to.equal('settingValue'); }); - - test('If `args[1]` is not a valid uri', async () => { - const args = ['command', '${input:some_input}']; - when(interpreterService.getActiveInterpreter(anything())).thenCall((arg) => { - assert.deepEqual(arg, undefined); - return Promise.resolve({ path: 'settingValue' }) as unknown; - }); - const setting = await interpreterPathCommand._getSelectedInterpreterPath(args); - expect(setting).to.equal('settingValue'); - }); }); diff --git a/src/test/interpreters/interpreterService.unit.test.ts b/src/test/interpreters/interpreterService.unit.test.ts index d8a0ada23a6f..1d521dad8ec8 100644 --- a/src/test/interpreters/interpreterService.unit.test.ts +++ b/src/test/interpreters/interpreterService.unit.test.ts @@ -36,10 +36,12 @@ import { ServiceManager } from '../../client/ioc/serviceManager'; import { PYTHON_PATH } from '../common'; import { MockAutoSelectionService } from '../mocks/autoSelector'; import * as proposedApi from '../../client/environmentApi'; +import { createTypeMoq } from '../mocks/helper'; +import * as extapi from '../../client/envExt/api.internal'; /* eslint-disable @typescript-eslint/no-explicit-any */ -use(chaiAsPromised); +use(chaiAsPromised.default); suite('Interpreters service', () => { let serviceManager: ServiceManager; @@ -61,29 +63,33 @@ suite('Interpreters service', () => { let installer: TypeMoq.IMock; let appShell: TypeMoq.IMock; let reportActiveInterpreterChangedStub: sinon.SinonStub; + let useEnvExtensionStub: sinon.SinonStub; setup(() => { + useEnvExtensionStub = sinon.stub(extapi, 'useEnvExtension'); + useEnvExtensionStub.returns(false); + const cont = new Container(); serviceManager = new ServiceManager(cont); serviceContainer = new ServiceContainer(cont); - interpreterPathService = TypeMoq.Mock.ofType(); - updater = TypeMoq.Mock.ofType(); - pyenvs = TypeMoq.Mock.ofType(); - helper = TypeMoq.Mock.ofType(); - workspace = TypeMoq.Mock.ofType(); - config = TypeMoq.Mock.ofType(); - fileSystem = TypeMoq.Mock.ofType(); - interpreterDisplay = TypeMoq.Mock.ofType(); - persistentStateFactory = TypeMoq.Mock.ofType(); - pythonExecutionFactory = TypeMoq.Mock.ofType(); - pythonExecutionService = TypeMoq.Mock.ofType(); - configService = TypeMoq.Mock.ofType(); - installer = TypeMoq.Mock.ofType(); - appShell = TypeMoq.Mock.ofType(); - experiments = TypeMoq.Mock.ofType(); - - pythonSettings = TypeMoq.Mock.ofType(); + interpreterPathService = createTypeMoq(); + updater = createTypeMoq(); + pyenvs = createTypeMoq(); + helper = createTypeMoq(); + workspace = createTypeMoq(); + config = createTypeMoq(); + fileSystem = createTypeMoq(); + interpreterDisplay = createTypeMoq(); + persistentStateFactory = createTypeMoq(); + pythonExecutionFactory = createTypeMoq(); + pythonExecutionService = createTypeMoq(); + configService = createTypeMoq(); + installer = createTypeMoq(); + appShell = createTypeMoq(); + experiments = createTypeMoq(); + + pythonSettings = createTypeMoq(); pythonSettings.setup((s) => s.pythonPath).returns(() => PYTHON_PATH); configService.setup((c) => c.getSettings(TypeMoq.It.isAny())).returns(() => pythonSettings.object); @@ -166,7 +172,7 @@ suite('Interpreters service', () => { test('Changes to active document should invoke interpreter.refresh method', async () => { const service = new InterpreterService(serviceContainer, pyenvs.object); - const documentManager = TypeMoq.Mock.ofType(); + const documentManager = createTypeMoq(); workspace.setup((w) => w.workspaceFolders).returns(() => [{ uri: '' }] as any); let activeTextEditorChangeHandler: (e: TextEditor | undefined) => any | undefined; @@ -179,9 +185,9 @@ suite('Interpreters service', () => { serviceManager.addSingletonInstance(IDocumentManager, documentManager.object); service.initialize(); - const textEditor = TypeMoq.Mock.ofType(); + const textEditor = createTypeMoq(); const uri = Uri.file(path.join('usr', 'file.py')); - const document = TypeMoq.Mock.ofType(); + const document = createTypeMoq(); textEditor.setup((t) => t.document).returns(() => document.object); document.setup((d) => d.uri).returns(() => uri); activeTextEditorChangeHandler!(textEditor.object); @@ -191,7 +197,7 @@ suite('Interpreters service', () => { test('If there is no active document then interpreter.refresh should not be invoked', async () => { const service = new InterpreterService(serviceContainer, pyenvs.object); - const documentManager = TypeMoq.Mock.ofType(); + const documentManager = createTypeMoq(); workspace.setup((w) => w.workspaceFolders).returns(() => [{ uri: '' }] as any); let activeTextEditorChangeHandler: (e?: TextEditor | undefined) => any | undefined; @@ -211,7 +217,7 @@ suite('Interpreters service', () => { test('Register the correct handler', async () => { const service = new InterpreterService(serviceContainer, pyenvs.object); - const documentManager = TypeMoq.Mock.ofType(); + const documentManager = createTypeMoq(); workspace.setup((w) => w.workspaceFolders).returns(() => [{ uri: '' }] as any); let interpreterPathServiceHandler: (e: InterpreterConfigurationScope) => any | undefined = () => 0; diff --git a/src/test/interpreters/pythonPathUpdaterFactory.unit.test.ts b/src/test/interpreters/pythonPathUpdaterFactory.unit.test.ts index 762c23d86c8e..5c851b8071f3 100644 --- a/src/test/interpreters/pythonPathUpdaterFactory.unit.test.ts +++ b/src/test/interpreters/pythonPathUpdaterFactory.unit.test.ts @@ -17,6 +17,7 @@ suite('Python Path Settings Updater', () => { serviceContainer = TypeMoq.Mock.ofType(); workspaceService = TypeMoq.Mock.ofType(); interpreterPathService = TypeMoq.Mock.ofType(); + experimentsManager = TypeMoq.Mock.ofType(); serviceContainer .setup((c) => c.get(TypeMoq.It.isValue(IWorkspaceService))) .returns(() => workspaceService.object); diff --git a/src/test/interpreters/recommededEnvironmentService.unit.test.ts b/src/test/interpreters/recommededEnvironmentService.unit.test.ts new file mode 100644 index 000000000000..7cb5aed58239 --- /dev/null +++ b/src/test/interpreters/recommededEnvironmentService.unit.test.ts @@ -0,0 +1,50 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { expect } from 'chai'; +import { anything, reset, verify, when } from 'ts-mockito'; +import { Disposable, Uri } from 'vscode'; +import { mockedVSCodeNamespaces } from '../vscode-mock'; +import { RecommendedEnvironmentService } from '../../client/interpreter/configuration/recommededEnvironmentService'; + +suite('RecommendedEnvironmentService - activate', () => { + let service: RecommendedEnvironmentService; + let subscriptions: Disposable[]; + + setup(() => { + subscriptions = []; + const extensionContext = { + subscriptions, + globalState: { + get: () => undefined, + update: () => Promise.resolve(), + }, + } as any; + + when(mockedVSCodeNamespaces.commands!.registerCommand(anything(), anything())).thenReturn({ + dispose: () => {}, + } as Disposable); + + service = new RecommendedEnvironmentService(extensionContext); + }); + + teardown(() => { + reset(mockedVSCodeNamespaces.commands!); + }); + + test('Multiroot workspace: command is registered only once across multiple activate calls', async () => { + // Simulate multiroot workspace where activate is called once per workspace root + const workspaceRoot1 = Uri.file('/workspace/root1'); + const workspaceRoot2 = Uri.file('/workspace/root2'); + const workspaceRoot3 = Uri.file('/workspace/root3'); + + await service.activate(workspaceRoot1); + await service.activate(workspaceRoot2); + await service.activate(workspaceRoot3); + + verify(mockedVSCodeNamespaces.commands!.registerCommand('python.getRecommendedEnvironment', anything())).once(); + expect(subscriptions).to.have.lengthOf(1); + }); +}); diff --git a/src/test/interpreters/serviceRegistry.unit.test.ts b/src/test/interpreters/serviceRegistry.unit.test.ts index 00090eb4b6e9..ad8614b42d8b 100644 --- a/src/test/interpreters/serviceRegistry.unit.test.ts +++ b/src/test/interpreters/serviceRegistry.unit.test.ts @@ -27,6 +27,7 @@ import { IInterpreterSelector, IPythonPathUpdaterServiceFactory, IPythonPathUpdaterServiceManager, + IRecommendedEnvironmentService, } from '../../client/interpreter/configuration/types'; import { IActivatedEnvironmentLaunch, @@ -35,7 +36,7 @@ import { IInterpreterService, } from '../../client/interpreter/contracts'; import { InterpreterDisplay } from '../../client/interpreter/display'; -import { InterpreterLocatorProgressStatubarHandler } from '../../client/interpreter/display/progressDisplay'; +import { InterpreterLocatorProgressStatusBarHandler } from '../../client/interpreter/display/progressDisplay'; import { InterpreterHelper } from '../../client/interpreter/helpers'; import { InterpreterService } from '../../client/interpreter/interpreterService'; import { registerTypes } from '../../client/interpreter/serviceRegistry'; @@ -43,6 +44,8 @@ import { ActivatedEnvironmentLaunch } from '../../client/interpreter/virtualEnvs import { CondaInheritEnvPrompt } from '../../client/interpreter/virtualEnvs/condaInheritEnvPrompt'; import { VirtualEnvironmentPrompt } from '../../client/interpreter/virtualEnvs/virtualEnvPrompt'; import { ServiceManager } from '../../client/ioc/serviceManager'; +import { InterpreterPathCommand } from '../../client/interpreter/interpreterPathCommand'; +import { RecommendedEnvironmentService } from '../../client/interpreter/configuration/recommededEnvironmentService'; suite('Interpreters - Service Registry', () => { test('Registrations', () => { @@ -63,17 +66,19 @@ suite('Interpreters - Service Registry', () => { [IPythonPathUpdaterServiceFactory, PythonPathUpdaterServiceFactory], [IPythonPathUpdaterServiceManager, PythonPathUpdaterService], + [IRecommendedEnvironmentService, RecommendedEnvironmentService], [IInterpreterSelector, InterpreterSelector], [IInterpreterHelper, InterpreterHelper], [IInterpreterComparer, EnvironmentTypeComparer], - [IExtensionSingleActivationService, InterpreterLocatorProgressStatubarHandler], + [IExtensionSingleActivationService, InterpreterLocatorProgressStatusBarHandler], [IInterpreterAutoSelectionProxyService, InterpreterAutoSelectionProxyService], [IInterpreterAutoSelectionService, InterpreterAutoSelectionService], [EnvironmentActivationService, EnvironmentActivationService], [IEnvironmentActivationService, EnvironmentActivationService], + [IExtensionSingleActivationService, InterpreterPathCommand], [IExtensionActivationService, CondaInheritEnvPrompt], [IActivatedEnvironmentLaunch, ActivatedEnvironmentLaunch], ].forEach((mapping) => { diff --git a/src/test/interpreters/virtualEnvs/activatedEnvLaunch.unit.test.ts b/src/test/interpreters/virtualEnvs/activatedEnvLaunch.unit.test.ts index 2ebdecd000d1..860970bd641e 100644 --- a/src/test/interpreters/virtualEnvs/activatedEnvLaunch.unit.test.ts +++ b/src/test/interpreters/virtualEnvs/activatedEnvLaunch.unit.test.ts @@ -263,37 +263,6 @@ suite('Activated Env Launch', async () => { expect(_promptIfApplicable.notCalled).to.equal(true, 'Prompt should not be displayed'); }); - test('Does not update interpreter path if a multiroot workspace is opened', async () => { - process.env.VIRTUAL_ENV = virtualEnvPrefix; - interpreterService - .setup((i) => i.getInterpreterDetails(TypeMoq.It.isAny())) - .returns(() => Promise.resolve(({ envName: 'base' } as unknown) as PythonEnvironment)); - workspaceService.setup((w) => w.workspaceFile).returns(() => uri); - const workspaceFolder: WorkspaceFolder = { name: 'one', uri, index: 0 }; - workspaceService.setup((w) => w.workspaceFolders).returns(() => [workspaceFolder]); - pythonPathUpdaterService - .setup((p) => - p.updatePythonPath( - TypeMoq.It.isValue(virtualEnvPrefix), - TypeMoq.It.isValue(ConfigurationTarget.WorkspaceFolder), - TypeMoq.It.isValue('load'), - TypeMoq.It.isValue(uri), - ), - ) - .returns(() => Promise.resolve()) - .verifiable(TypeMoq.Times.never()); - activatedEnvLaunch = new ActivatedEnvironmentLaunch( - workspaceService.object, - appShell.object, - pythonPathUpdaterService.object, - interpreterService.object, - processServiceFactory.object, - ); - const result = await activatedEnvLaunch.selectIfLaunchedViaActivatedEnv(); - expect(result).to.be.equal(undefined, 'Incorrect value'); - pythonPathUpdaterService.verifyAll(); - }); - test('Returns `undefined` if env was already selected', async () => { activatedEnvLaunch = new ActivatedEnvironmentLaunch( workspaceService.object, diff --git a/src/test/interpreters/virtualEnvs/virtualEnvPrompt.unit.test.ts b/src/test/interpreters/virtualEnvs/virtualEnvPrompt.unit.test.ts index 9671e393dc43..2ad67831c455 100644 --- a/src/test/interpreters/virtualEnvs/virtualEnvPrompt.unit.test.ts +++ b/src/test/interpreters/virtualEnvs/virtualEnvPrompt.unit.test.ts @@ -223,7 +223,7 @@ suite('Virtual Environment Prompt', () => { notificationPromptEnabled.verifyAll(); }); - test("If user selects 'Do not show again', prompt is disabled", async () => { + test('If user selects "Don\'t show again", prompt is disabled', async () => { const resource = Uri.file('a'); const interpreter1 = { path: 'path/to/interpreter1' }; const prompts = [Common.bannerLabelYes, Common.bannerLabelNo, Common.doNotShowAgain]; diff --git a/src/test/linters/bandit.unit.test.ts b/src/test/linters/bandit.unit.test.ts deleted file mode 100644 index 6a44158034bd..000000000000 --- a/src/test/linters/bandit.unit.test.ts +++ /dev/null @@ -1,87 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; - -import { expect } from 'chai'; -import { parseLine } from '../../client/linters/baseLinter'; -import { BANDIT_REGEX } from '../../client/linters/bandit'; - -import { ILintMessage, LinterId } from '../../client/linters/types'; - -suite('Linting - Bandit', () => { - test('parsing new bandit with col', () => { - const newOutput = `\ -1,0,LOW,B404:Consider possible security implications associated with subprocess module. -19,4,HIGH,B602:subprocess call with shell=True identified, security issue. -`; - - const lines = newOutput.split('\n'); - const tests: [string, ILintMessage | undefined][] = [ - [ - lines[0], - { - code: 'B404', - message: 'Consider possible security implications associated with subprocess module.', - column: 0, - line: 1, - type: 'LOW', - provider: 'bandit', - }, - ], - [ - lines[1], - { - code: 'B602', - message: 'subprocess call with shell=True identified, security issue.', - column: 3, - line: 19, - type: 'HIGH', - provider: 'bandit', - }, - ], - ]; - for (const [line, expected] of tests) { - const msg = parseLine(line, BANDIT_REGEX, LinterId.Bandit, 1); - - expect(msg).to.deep.equal(expected); - } - }); - test('parsing old bandit with no col', () => { - const newOutput = `\ -1,col,LOW,B404:Consider possible security implications associated with subprocess module. -19,col,HIGH,B602:subprocess call with shell=True identified, security issue. -`; - - const lines = newOutput.split('\n'); - const tests: [string, ILintMessage | undefined][] = [ - [ - lines[0], - { - code: 'B404', - message: 'Consider possible security implications associated with subprocess module.', - column: 0, - line: 1, - type: 'LOW', - provider: 'bandit', - }, - ], - [ - lines[1], - { - code: 'B602', - message: 'subprocess call with shell=True identified, security issue.', - column: 0, - line: 19, - type: 'HIGH', - provider: 'bandit', - }, - ], - ]; - for (const [line, expected] of tests) { - const msg = parseLine(line, BANDIT_REGEX, LinterId.Bandit, 1); - - expect(msg).to.deep.equal(expected); - } - }); -}); diff --git a/src/test/linters/common.ts b/src/test/linters/common.ts deleted file mode 100644 index 3c8f72a8d710..000000000000 --- a/src/test/linters/common.ts +++ /dev/null @@ -1,405 +0,0 @@ -/* eslint-disable max-classes-per-file */ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; - -import * as os from 'os'; -import * as TypeMoq from 'typemoq'; -import { DiagnosticSeverity, TextDocument, Uri, WorkspaceFolder } from 'vscode'; -import { LanguageServerType } from '../../client/activation/types'; -import { IApplicationShell, IWorkspaceService } from '../../client/common/application/types'; -import { Product } from '../../client/common/installer/productInstaller'; -import { ProductNames } from '../../client/common/installer/productNames'; -import { IFileSystem, IPlatformService } from '../../client/common/platform/types'; -import { IPythonExecutionFactory, IPythonToolExecutionService } from '../../client/common/process/types'; -import { - Flake8CategorySeverity, - IConfigurationService, - IInstaller, - IMypyCategorySeverity, - ILogOutputChannel, - IPycodestyleCategorySeverity, - IPylintCategorySeverity, - IPythonSettings, -} from '../../client/common/types'; -import { IServiceContainer } from '../../client/ioc/types'; -import { LINTERID_BY_PRODUCT } from '../../client/linters/constants'; -import { LinterManager } from '../../client/linters/linterManager'; -import { ILinter, ILinterManager, ILintMessage, LinterId } from '../../client/linters/types'; - -export function newMockDocument(filename: string): TypeMoq.IMock { - const uri = Uri.file(filename); - const doc = TypeMoq.Mock.ofType(undefined, TypeMoq.MockBehavior.Strict); - doc.setup((s) => s.uri).returns(() => uri); - return doc; -} - -export function linterMessageAsLine(msg: ILintMessage): string { - switch (msg.provider) { - case 'pydocstyle': { - return `:${msg.line} spam:${os.EOL}\t${msg.code}: ${msg.message}`; - } - default: { - return `${msg.line},${msg.column},${msg.type},${msg.code}:${msg.message}`; - } - } -} - -function pylintMessageAsString(msg: ILintMessage, trailingComma = true): string { - return ` { - "type": "${msg.type}", - "line": ${msg.line}, - "column": ${msg.column}, - "symbol": "${msg.code}", - "message": "${msg.message}", - "endLine": ${msg.endLine ?? null}, - "endColumn": ${msg.endColumn ?? null} - }${trailingComma ? ',' : ''}`; -} - -export function pylintLinterMessagesAsOutput(messages: ILintMessage[]): string { - const lines: string[] = ['[']; - if (messages) { - const pylintMessages = messages.slice(0, -1).map((msg) => pylintMessageAsString(msg, true)); - const lastMessage = pylintMessageAsString(messages[messages.length - 1], false); - - lines.push(...pylintMessages, lastMessage); - } - lines.push(']'); - return lines.join(os.EOL); -} - -export function getLinterID(product: Product): LinterId { - const linterID = LINTERID_BY_PRODUCT.get(product); - if (!linterID) { - throwUnknownProduct(product); - } - return linterID!; -} - -export function getProductName(product: Product, capitalize = true): string { - let prodName = ProductNames.get(product); - if (!prodName) { - prodName = Product[product]; - } - if (capitalize) { - return prodName.charAt(0).toUpperCase() + prodName.slice(1); - } - return prodName; -} - -export function throwUnknownProduct(product: Product): void { - throw Error(`unsupported product ${Product[product]} (${product})`); -} - -export class LintingSettings { - public enabled: boolean; - - public cwd?: string; - - public ignorePatterns: string[]; - - public prospectorEnabled: boolean; - - public prospectorArgs: string[]; - - public pylintEnabled: boolean; - - public pylintArgs: string[]; - - public pycodestyleEnabled: boolean; - - public pycodestyleArgs: string[]; - - public pylamaEnabled: boolean; - - public pylamaArgs: string[]; - - public flake8Enabled: boolean; - - public flake8Args: string[]; - - public pydocstyleEnabled: boolean; - - public pydocstyleArgs: string[]; - - public lintOnSave: boolean; - - public maxNumberOfProblems: number; - - public pylintCategorySeverity: IPylintCategorySeverity; - - public pycodestyleCategorySeverity: IPycodestyleCategorySeverity; - - public flake8CategorySeverity: Flake8CategorySeverity; - - public mypyCategorySeverity: IMypyCategorySeverity; - - public prospectorPath: string; - - public pylintPath: string; - - public pycodestylePath: string; - - public pylamaPath: string; - - public flake8Path: string; - - public pydocstylePath: string; - - public mypyEnabled: boolean; - - public mypyArgs: string[]; - - public mypyPath: string; - - public banditEnabled: boolean; - - public banditArgs: string[]; - - public banditPath: string; - - constructor() { - // mostly from configSettings.ts - - this.enabled = true; - this.cwd = undefined; - this.ignorePatterns = []; - this.lintOnSave = false; - this.maxNumberOfProblems = 100; - - this.flake8Enabled = false; - this.flake8Path = 'flake8'; - this.flake8Args = []; - this.flake8CategorySeverity = { - E: DiagnosticSeverity.Error, - W: DiagnosticSeverity.Warning, - F: DiagnosticSeverity.Warning, - }; - - this.mypyEnabled = false; - this.mypyPath = 'mypy'; - this.mypyArgs = []; - this.mypyCategorySeverity = { - error: DiagnosticSeverity.Error, - note: DiagnosticSeverity.Hint, - }; - - this.banditEnabled = false; - this.banditPath = 'bandit'; - this.banditArgs = []; - - this.pycodestyleEnabled = false; - this.pycodestylePath = 'pycodestyle'; - this.pycodestyleArgs = []; - this.pycodestyleCategorySeverity = { - E: DiagnosticSeverity.Error, - W: DiagnosticSeverity.Warning, - }; - - this.pylamaEnabled = false; - this.pylamaPath = 'pylama'; - this.pylamaArgs = []; - - this.prospectorEnabled = false; - this.prospectorPath = 'prospector'; - this.prospectorArgs = []; - - this.pydocstyleEnabled = false; - this.pydocstylePath = 'pydocstyle'; - this.pydocstyleArgs = []; - - this.pylintEnabled = false; - this.pylintPath = 'pylint'; - this.pylintArgs = []; - this.pylintCategorySeverity = { - convention: DiagnosticSeverity.Hint, - error: DiagnosticSeverity.Error, - fatal: DiagnosticSeverity.Error, - refactor: DiagnosticSeverity.Hint, - warning: DiagnosticSeverity.Warning, - }; - } -} - -export class BaseTestFixture { - public serviceContainer: TypeMoq.IMock; - - public linterManager: LinterManager; - - // services - public workspaceService: TypeMoq.IMock; - - public installer: TypeMoq.IMock; - - public appShell: TypeMoq.IMock; - - // config - public configService: TypeMoq.IMock; - - public pythonSettings: TypeMoq.IMock; - - public lintingSettings: LintingSettings; - - // data - public outputChannel: TypeMoq.IMock; - - // artifacts - public output: string; - - public logged: string[]; - - constructor( - platformService: IPlatformService, - filesystem: IFileSystem, - pythonToolExecService: IPythonToolExecutionService, - pythonExecFactory: IPythonExecutionFactory, - configService?: TypeMoq.IMock, - serviceContainer?: TypeMoq.IMock, - ignoreConfigUpdates = false, - public readonly workspaceDir = '.', - protected readonly printLogs = false, - ) { - this.serviceContainer = - serviceContainer || TypeMoq.Mock.ofType(undefined, TypeMoq.MockBehavior.Strict); - - // services - - this.workspaceService = TypeMoq.Mock.ofType(undefined, TypeMoq.MockBehavior.Strict); - this.installer = TypeMoq.Mock.ofType(undefined, TypeMoq.MockBehavior.Strict); - this.appShell = TypeMoq.Mock.ofType(undefined, TypeMoq.MockBehavior.Strict); - - this.serviceContainer - .setup((c) => c.get(TypeMoq.It.isValue(IFileSystem), TypeMoq.It.isAny())) - .returns(() => filesystem); - this.serviceContainer - .setup((c) => c.get(TypeMoq.It.isValue(IWorkspaceService), TypeMoq.It.isAny())) - .returns(() => this.workspaceService.object); - this.serviceContainer - .setup((c) => c.get(TypeMoq.It.isValue(IInstaller), TypeMoq.It.isAny())) - .returns(() => this.installer.object); - this.serviceContainer - .setup((c) => c.get(TypeMoq.It.isValue(IPlatformService), TypeMoq.It.isAny())) - .returns(() => platformService); - this.serviceContainer - .setup((c) => c.get(TypeMoq.It.isValue(IPythonToolExecutionService), TypeMoq.It.isAny())) - .returns(() => pythonToolExecService); - this.serviceContainer - .setup((c) => c.get(TypeMoq.It.isValue(IPythonExecutionFactory), TypeMoq.It.isAny())) - .returns(() => pythonExecFactory); - this.serviceContainer - .setup((c) => c.get(TypeMoq.It.isValue(IApplicationShell), TypeMoq.It.isAny())) - .returns(() => this.appShell.object); - this.initServices(); - - // config - - this.configService = - configService || TypeMoq.Mock.ofType(undefined, TypeMoq.MockBehavior.Strict); - this.pythonSettings = TypeMoq.Mock.ofType(undefined, TypeMoq.MockBehavior.Strict); - this.lintingSettings = new LintingSettings(); - - this.serviceContainer - .setup((c) => c.get(TypeMoq.It.isValue(IConfigurationService), TypeMoq.It.isAny())) - .returns(() => this.configService.object); - this.configService.setup((c) => c.getSettings(TypeMoq.It.isAny())).returns(() => this.pythonSettings.object); - this.pythonSettings.setup((s) => s.linting).returns(() => this.lintingSettings); - this.initConfig(ignoreConfigUpdates); - - // data - - this.outputChannel = TypeMoq.Mock.ofType(undefined, TypeMoq.MockBehavior.Strict); - - this.serviceContainer - .setup((c) => c.get(TypeMoq.It.isValue(ILogOutputChannel))) - .returns(() => this.outputChannel.object); - this.initData(); - - // artifacts - - this.output = ''; - this.logged = []; - - // linting - - this.linterManager = new LinterManager(this.configService.object); - this.serviceContainer - .setup((c) => c.get(TypeMoq.It.isValue(ILinterManager), TypeMoq.It.isAny())) - .returns(() => this.linterManager); - } - - public async getLinter(product: Product, enabled = true): Promise { - const info = this.linterManager.getLinterInfo(product); - - // @ts-ignore We only do this during testing. - this.lintingSettings[info.enabledSettingName] = enabled; - - await this.linterManager.setActiveLintersAsync([product]); - await this.linterManager.enableLintingAsync(enabled); - return this.linterManager.createLinter(product, this.serviceContainer.object); - } - - public async getEnabledLinter(product: Product): Promise { - return this.getLinter(product, true); - } - - public async getDisabledLinter(product: Product): Promise { - return this.getLinter(product, false); - } - - // eslint-disable-next-line class-methods-use-this - protected newMockDocument(filename: string): TypeMoq.IMock { - return newMockDocument(filename); - } - - private initServices(): void { - const workspaceFolder = TypeMoq.Mock.ofType(undefined, TypeMoq.MockBehavior.Strict); - workspaceFolder.setup((f) => f.uri).returns(() => Uri.file(this.workspaceDir)); - this.workspaceService - .setup((s) => s.getWorkspaceFolder(TypeMoq.It.isAny())) - .returns(() => workspaceFolder.object); - - this.appShell - .setup((a) => a.showErrorMessage(TypeMoq.It.isAny(), TypeMoq.It.isAny())) - .returns(() => Promise.resolve(undefined)); - } - - private initConfig(ignoreUpdates = false): void { - this.configService - .setup((c) => - c.updateSetting(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny()), - ) - .callback((setting, value) => { - if (ignoreUpdates) { - return; - } - const prefix = 'linting.'; - if (setting.startsWith(prefix)) { - // @ts-ignore We only do this during testing. - this.lintingSettings[setting.substr(prefix.length)] = value; - } - }) - .returns(() => Promise.resolve(undefined)); - - this.pythonSettings.setup((s) => s.languageServer).returns(() => LanguageServerType.Jedi); - } - - private initData(): void { - this.outputChannel - .setup((o) => o.appendLine(TypeMoq.It.isAny())) - .callback((line) => { - if (this.output === '') { - this.output = line; - } else { - this.output = `${this.output}${os.EOL}${line}`; - } - }); - this.outputChannel - .setup((o) => o.append(TypeMoq.It.isAny())) - .callback((data) => { - this.output += data; - }); - this.outputChannel.setup((o) => o.show()); - } -} diff --git a/src/test/linters/lint.args.test.ts b/src/test/linters/lint.args.test.ts deleted file mode 100644 index 2c32a73052bf..000000000000 --- a/src/test/linters/lint.args.test.ts +++ /dev/null @@ -1,201 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; - -import { expect } from 'chai'; -import { Container } from 'inversify'; -import * as path from 'path'; -import * as TypeMoq from 'typemoq'; -import { CancellationTokenSource, TextDocument, Uri, WorkspaceFolder } from 'vscode'; -import { IDocumentManager, IWorkspaceService } from '../../client/common/application/types'; -import '../../client/common/extensions'; -import { IFileSystem, IPlatformService } from '../../client/common/platform/types'; -import { - IConfigurationService, - IExtensions, - IInstaller, - ILintingSettings, - IPythonSettings, -} from '../../client/common/types'; -import { - IInterpreterAutoSelectionService, - IInterpreterAutoSelectionProxyService, -} from '../../client/interpreter/autoSelection/types'; -import { IInterpreterService } from '../../client/interpreter/contracts'; -import { ServiceContainer } from '../../client/ioc/container'; -import { ServiceManager } from '../../client/ioc/serviceManager'; -import { Bandit } from '../../client/linters/bandit'; -import { BaseLinter } from '../../client/linters/baseLinter'; -import { Flake8 } from '../../client/linters/flake8'; -import { LinterManager } from '../../client/linters/linterManager'; -import { MyPy } from '../../client/linters/mypy'; -import { Prospector } from '../../client/linters/prospector'; -import { Pycodestyle } from '../../client/linters/pycodestyle'; -import { PyDocStyle } from '../../client/linters/pydocstyle'; -import { PyLama } from '../../client/linters/pylama'; -import { Pylint } from '../../client/linters/pylint'; -import { ILinterManager, ILintingEngine } from '../../client/linters/types'; -import { initialize } from '../initialize'; -import { MockAutoSelectionService } from '../mocks/autoSelector'; - -suite('Linting - Arguments', () => { - [undefined, path.join('users', 'dev_user')].forEach((workspaceUri) => { - [ - Uri.file(path.join('users', 'dev_user', 'development path to', 'one.py')), - Uri.file(path.join('users', 'dev_user', 'development', 'one.py')), - ].forEach((fileUri) => { - suite( - `File path ${fileUri.fsPath.indexOf(' ') > 0 ? 'with' : 'without'} spaces and ${ - workspaceUri ? 'without' : 'with' - } a workspace`, - () => { - let interpreterService: TypeMoq.IMock; - let engine: TypeMoq.IMock; - let configService: TypeMoq.IMock; - let docManager: TypeMoq.IMock; - let settings: TypeMoq.IMock; - let lm: ILinterManager; - let serviceContainer: ServiceContainer; - let document: TypeMoq.IMock; - let workspaceService: TypeMoq.IMock; - let extensionsService: TypeMoq.IMock; - const cancellationToken = new CancellationTokenSource().token; - suiteSetup(initialize); - setup(async () => { - const cont = new Container(); - const serviceManager = new ServiceManager(cont); - - serviceContainer = new ServiceContainer(cont); - - const fs = TypeMoq.Mock.ofType(); - fs.setup((x) => x.fileExists(TypeMoq.It.isAny())).returns( - () => new Promise((resolve) => resolve(true)), - ); - fs.setup((x) => x.arePathsSame(TypeMoq.It.isAnyString(), TypeMoq.It.isAnyString())).returns( - () => true, - ); - serviceManager.addSingletonInstance(IFileSystem, fs.object); - - interpreterService = TypeMoq.Mock.ofType(); - serviceManager.addSingletonInstance( - IInterpreterService, - interpreterService.object, - ); - serviceManager.addSingleton( - IInterpreterAutoSelectionService, - MockAutoSelectionService, - ); - serviceManager.addSingleton( - IInterpreterAutoSelectionProxyService, - MockAutoSelectionService, - ); - engine = TypeMoq.Mock.ofType(); - serviceManager.addSingletonInstance(ILintingEngine, engine.object); - - docManager = TypeMoq.Mock.ofType(); - serviceManager.addSingletonInstance(IDocumentManager, docManager.object); - - const lintSettings = TypeMoq.Mock.ofType(); - lintSettings.setup((x) => x.enabled).returns(() => true); - lintSettings.setup((x) => x.lintOnSave).returns(() => true); - lintSettings.setup((x) => x.cwd).returns(() => undefined); - - settings = TypeMoq.Mock.ofType(); - settings.setup((x) => x.linting).returns(() => lintSettings.object); - - configService = TypeMoq.Mock.ofType(); - configService.setup((x) => x.getSettings(TypeMoq.It.isAny())).returns(() => settings.object); - serviceManager.addSingletonInstance( - IConfigurationService, - configService.object, - ); - - const workspaceFolder: WorkspaceFolder | undefined = workspaceUri - ? { uri: Uri.file(workspaceUri), index: 0, name: '' } - : undefined; - workspaceService = TypeMoq.Mock.ofType(); - workspaceService - .setup((w) => w.getWorkspaceFolder(TypeMoq.It.isAny())) - .returns(() => workspaceFolder); - serviceManager.addSingletonInstance( - IWorkspaceService, - workspaceService.object, - ); - - const installer = TypeMoq.Mock.ofType(); - serviceManager.addSingletonInstance(IInstaller, installer.object); - - const platformService = TypeMoq.Mock.ofType(); - serviceManager.addSingletonInstance(IPlatformService, platformService.object); - - extensionsService = TypeMoq.Mock.ofType(); - extensionsService.setup((e) => e.getExtension(TypeMoq.It.isAny())).returns(() => undefined); - serviceManager.addSingletonInstance(IExtensions, extensionsService.object); - - lm = new LinterManager(configService.object); - serviceManager.addSingletonInstance(ILinterManager, lm); - document = TypeMoq.Mock.ofType(); - }); - - async function testLinter(linter: BaseLinter, expectedArgs: string[]) { - document.setup((d) => d.uri).returns(() => fileUri); - - let invoked = false; - // eslint-disable-next-line @typescript-eslint/no-explicit-any - (linter as any).run = (args: string[]) => { - expect(args).to.deep.equal(expectedArgs); - invoked = true; - return Promise.resolve([]); - }; - await linter.lint(document.object, cancellationToken); - expect(invoked).to.be.equal(true, 'method not invoked'); - } - test('Flake8', async () => { - const linter = new Flake8(serviceContainer, { showPrompt: () => Promise.resolve(false) }); - const expectedArgs = [fileUri.fsPath]; - await testLinter(linter, expectedArgs); - }); - test('Pycodestyle', async () => { - const linter = new Pycodestyle(serviceContainer); - const expectedArgs = [fileUri.fsPath]; - await testLinter(linter, expectedArgs); - }); - test('Prospector', async () => { - const linter = new Prospector(serviceContainer); - const expectedPath = workspaceUri - ? fileUri.fsPath.substring(workspaceUri.length + 2) - : path.basename(fileUri.fsPath); - const expectedArgs = [expectedPath]; - await testLinter(linter, expectedArgs); - }); - test('Pylama', async () => { - const linter = new PyLama(serviceContainer); - const expectedArgs = [fileUri.fsPath]; - await testLinter(linter, expectedArgs); - }); - test('MyPy', async () => { - const linter = new MyPy(serviceContainer); - const expectedArgs = [fileUri.fsPath]; - await testLinter(linter, expectedArgs); - }); - test('Pydocstyle', async () => { - const linter = new PyDocStyle(serviceContainer); - const expectedArgs = [fileUri.fsPath]; - await testLinter(linter, expectedArgs); - }); - test('Pylint', async () => { - const linter = new Pylint(serviceContainer, { showPrompt: () => Promise.resolve(false) }); - const expectedArgs = [fileUri.fsPath]; - await testLinter(linter, expectedArgs); - }); - test('Bandit', async () => { - const linter = new Bandit(serviceContainer); - const expectedArgs = [fileUri.fsPath]; - await testLinter(linter, expectedArgs); - }); - }, - ); - }); - }); -}); diff --git a/src/test/linters/lint.functional.test.ts b/src/test/linters/lint.functional.test.ts deleted file mode 100644 index a3dc70b7c21e..000000000000 --- a/src/test/linters/lint.functional.test.ts +++ /dev/null @@ -1,895 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; - -import * as assert from 'assert'; -import * as childProcess from 'child_process'; -import * as fs from 'fs-extra'; -import * as os from 'os'; -import * as path from 'path'; -import * as sinon from 'sinon'; -import { anything, instance, mock, when } from 'ts-mockito'; -import * as TypeMoq from 'typemoq'; -import { CancellationTokenSource, TextDocument, TextLine, Uri } from 'vscode'; -import { Product } from '../../client/common/installer/productInstaller'; -import { FileSystem } from '../../client/common/platform/fileSystem'; -import { PlatformService } from '../../client/common/platform/platformService'; -import { IFileSystem } from '../../client/common/platform/types'; -import { ProcessServiceFactory } from '../../client/common/process/processFactory'; -import { PythonExecutionFactory } from '../../client/common/process/pythonExecutionFactory'; -import { PythonToolExecutionService } from '../../client/common/process/pythonToolService'; -import { - IProcessLogger, - IPythonExecutionFactory, - IPythonToolExecutionService, -} from '../../client/common/process/types'; -import { - IConfigurationService, - IDisposableRegistry, - IInterpreterPathService, - IPersistentState, -} from '../../client/common/types'; -import { IEnvironmentVariablesProvider } from '../../client/common/variables/types'; -import { IEnvironmentActivationService } from '../../client/interpreter/activation/types'; -import { - IActivatedEnvironmentLaunch, - IComponentAdapter, - IInterpreterService, -} from '../../client/interpreter/contracts'; -import { IServiceContainer } from '../../client/ioc/types'; -import { LINTERID_BY_PRODUCT } from '../../client/linters/constants'; -import { ILintMessage, LinterId, LintMessageSeverity } from '../../client/linters/types'; -import { deleteFile, PYTHON_PATH } from '../common'; -import { BaseTestFixture, getLinterID, getProductName, newMockDocument, throwUnknownProduct } from './common'; -import { IInterpreterAutoSelectionService } from '../../client/interpreter/autoSelection/types'; -import { Conda } from '../../client/pythonEnvironments/common/environmentManagers/conda'; -import * as promptApis from '../../client/linters/prompts/common'; - -const workspaceDir = path.join(__dirname, '..', '..', '..', 'src', 'test'); -const workspaceUri = Uri.file(workspaceDir); -const pythonFilesDir = path.join(workspaceDir, 'pythonFiles', 'linting'); -const fileToLint = path.join(pythonFilesDir, 'file.py'); - -const linterConfigDirs = new Map([ - [LinterId.Flake8, path.join(pythonFilesDir, 'flake8config')], - [LinterId.PyCodeStyle, path.join(pythonFilesDir, 'pycodestyleconfig')], - [LinterId.PyDocStyle, path.join(pythonFilesDir, 'pydocstyleconfig27')], - [LinterId.PyLint, path.join(pythonFilesDir, 'pylintconfig')], -]); -const linterConfigRCFiles = new Map([ - [LinterId.PyLint, '.pylintrc'], - [LinterId.PyDocStyle, '.pydocstyle'], -]); - -const pylintMessagesToBeReturned: ILintMessage[] = [ - { - line: 24, - column: 0, - severity: LintMessageSeverity.Information, - code: 'I0011', - message: 'Locally disabling no-member (E1101)', - provider: '', - type: 'warning', - }, - { - line: 30, - column: 0, - severity: LintMessageSeverity.Information, - code: 'I0011', - message: 'Locally disabling no-member (E1101)', - provider: '', - type: 'warning', - }, - { - line: 34, - column: 0, - severity: LintMessageSeverity.Information, - code: 'I0012', - message: 'Locally enabling no-member (E1101)', - provider: '', - type: 'warning', - }, - { - line: 40, - column: 0, - severity: LintMessageSeverity.Information, - code: 'I0011', - message: 'Locally disabling no-member (E1101)', - provider: '', - type: 'warning', - }, - { - line: 44, - column: 0, - severity: LintMessageSeverity.Information, - code: 'I0012', - message: 'Locally enabling no-member (E1101)', - provider: '', - type: 'warning', - }, - { - line: 55, - column: 0, - severity: LintMessageSeverity.Information, - code: 'I0011', - message: 'Locally disabling no-member (E1101)', - provider: '', - type: 'warning', - }, - { - line: 59, - column: 0, - severity: LintMessageSeverity.Information, - code: 'I0012', - message: 'Locally enabling no-member (E1101)', - provider: '', - type: 'warning', - }, - { - line: 62, - column: 0, - severity: LintMessageSeverity.Information, - code: 'I0011', - message: 'Locally disabling undefined-variable (E0602)', - provider: '', - type: 'warning', - }, - { - line: 70, - column: 0, - severity: LintMessageSeverity.Information, - code: 'I0011', - message: 'Locally disabling no-member (E1101)', - provider: '', - type: 'warning', - }, - { - line: 84, - column: 0, - severity: LintMessageSeverity.Information, - code: 'I0011', - message: 'Locally disabling no-member (E1101)', - provider: '', - type: 'warning', - }, - { - line: 87, - column: 0, - severity: LintMessageSeverity.Hint, - code: 'C0304', - message: 'Final newline missing', - provider: '', - type: 'warning', - }, - { - line: 11, - column: 20, - severity: LintMessageSeverity.Warning, - code: 'W0613', - message: "Unused argument 'arg'", - provider: '', - type: 'warning', - }, - { - line: 26, - column: 14, - severity: LintMessageSeverity.Error, - code: 'E1101', - message: "Instance of 'Foo' has no 'blop' member", - provider: '', - type: 'warning', - }, - { - line: 36, - column: 14, - severity: LintMessageSeverity.Error, - code: 'E1101', - message: "Instance of 'Foo' has no 'blip' member", - provider: '', - type: 'warning', - }, - { - line: 46, - column: 18, - severity: LintMessageSeverity.Error, - code: 'E1101', - message: "Instance of 'Foo' has no 'blip' member", - provider: '', - type: 'warning', - }, - { - line: 61, - column: 18, - severity: LintMessageSeverity.Error, - code: 'E1101', - message: "Instance of 'Foo' has no 'blip' member", - provider: '', - type: 'warning', - }, - { - line: 72, - column: 18, - severity: LintMessageSeverity.Error, - code: 'E1101', - message: "Instance of 'Foo' has no 'blip' member", - provider: '', - type: 'warning', - }, - { - line: 75, - column: 18, - severity: LintMessageSeverity.Error, - code: 'E1101', - message: "Instance of 'Foo' has no 'blip' member", - provider: '', - type: 'warning', - }, - { - line: 77, - column: 14, - severity: LintMessageSeverity.Error, - code: 'E1101', - message: "Instance of 'Foo' has no 'blip' member", - provider: '', - type: 'warning', - }, - { - line: 83, - column: 14, - severity: LintMessageSeverity.Error, - code: 'E1101', - message: "Instance of 'Foo' has no 'blip' member", - provider: '', - type: 'warning', - }, -]; -const flake8MessagesToBeReturned: ILintMessage[] = [ - { - line: 5, - column: 1, - severity: LintMessageSeverity.Error, - code: 'E302', - message: 'expected 2 blank lines, found 1', - provider: '', - type: 'E', - }, - { - line: 19, - column: 15, - severity: LintMessageSeverity.Error, - code: 'E127', - message: 'continuation line over-indented for visual indent', - provider: '', - type: 'E', - }, - { - line: 24, - column: 23, - severity: LintMessageSeverity.Error, - code: 'E261', - message: 'at least two spaces before inline comment', - provider: '', - type: 'E', - }, - { - line: 62, - column: 30, - severity: LintMessageSeverity.Error, - code: 'E261', - message: 'at least two spaces before inline comment', - provider: '', - type: 'E', - }, - { - line: 70, - column: 22, - severity: LintMessageSeverity.Error, - code: 'E261', - message: 'at least two spaces before inline comment', - provider: '', - type: 'E', - }, - { - line: 80, - column: 5, - severity: LintMessageSeverity.Error, - code: 'E303', - message: 'too many blank lines (2)', - provider: '', - type: 'E', - }, - { - line: 87, - column: 24, - severity: LintMessageSeverity.Warning, - code: 'W292', - message: 'no newline at end of file', - provider: '', - type: 'E', - }, -]; -const pycodestyleMessagesToBeReturned: ILintMessage[] = [ - { - line: 5, - column: 1, - severity: LintMessageSeverity.Error, - code: 'E302', - message: 'expected 2 blank lines, found 1', - provider: '', - type: 'E', - }, - { - line: 19, - column: 15, - severity: LintMessageSeverity.Error, - code: 'E127', - message: 'continuation line over-indented for visual indent', - provider: '', - type: 'E', - }, - { - line: 24, - column: 23, - severity: LintMessageSeverity.Error, - code: 'E261', - message: 'at least two spaces before inline comment', - provider: '', - type: 'E', - }, - { - line: 62, - column: 30, - severity: LintMessageSeverity.Error, - code: 'E261', - message: 'at least two spaces before inline comment', - provider: '', - type: 'E', - }, - { - line: 70, - column: 22, - severity: LintMessageSeverity.Error, - code: 'E261', - message: 'at least two spaces before inline comment', - provider: '', - type: 'E', - }, - { - line: 80, - column: 5, - severity: LintMessageSeverity.Error, - code: 'E303', - message: 'too many blank lines (2)', - provider: '', - type: 'E', - }, - { - line: 87, - column: 24, - severity: LintMessageSeverity.Warning, - code: 'W292', - message: 'no newline at end of file', - provider: '', - type: 'E', - }, -]; -const pydocstyleMessagesToBeReturned: ILintMessage[] = [ - { - code: 'D400', - severity: LintMessageSeverity.Information, - message: "First line should end with a period (not 'e')", - column: 0, - line: 1, - type: '', - provider: 'pydocstyle', - }, - { - code: 'D400', - severity: LintMessageSeverity.Information, - message: "First line should end with a period (not 't')", - column: 0, - line: 5, - type: '', - provider: 'pydocstyle', - }, - { - code: 'D102', - severity: LintMessageSeverity.Information, - message: 'Missing docstring in public method', - column: 4, - line: 8, - type: '', - provider: 'pydocstyle', - }, - { - code: 'D401', - severity: LintMessageSeverity.Information, - message: "First line should be in imperative mood ('thi', not 'this')", - column: 4, - line: 11, - type: '', - provider: 'pydocstyle', - }, - { - code: 'D403', - severity: LintMessageSeverity.Information, - message: "First word of the first line should be properly capitalized ('This', not 'this')", - column: 4, - line: 11, - type: '', - provider: 'pydocstyle', - }, - { - code: 'D400', - severity: LintMessageSeverity.Information, - message: "First line should end with a period (not 'e')", - column: 4, - line: 11, - type: '', - provider: 'pydocstyle', - }, - { - code: 'D403', - severity: LintMessageSeverity.Information, - message: "First word of the first line should be properly capitalized ('And', not 'and')", - column: 4, - line: 15, - type: '', - provider: 'pydocstyle', - }, - { - code: 'D400', - severity: LintMessageSeverity.Information, - message: "First line should end with a period (not 't')", - column: 4, - line: 15, - type: '', - provider: 'pydocstyle', - }, - { - code: 'D403', - severity: LintMessageSeverity.Information, - message: "First word of the first line should be properly capitalized ('Test', not 'test')", - column: 4, - line: 21, - type: '', - provider: 'pydocstyle', - }, - { - code: 'D400', - severity: LintMessageSeverity.Information, - message: "First line should end with a period (not 'g')", - column: 4, - line: 21, - type: '', - provider: 'pydocstyle', - }, - { - code: 'D403', - severity: LintMessageSeverity.Information, - message: "First word of the first line should be properly capitalized ('Test', not 'test')", - column: 4, - line: 28, - type: '', - provider: 'pydocstyle', - }, - { - code: 'D400', - severity: LintMessageSeverity.Information, - message: "First line should end with a period (not 'g')", - column: 4, - line: 28, - type: '', - provider: 'pydocstyle', - }, - { - code: 'D403', - severity: LintMessageSeverity.Information, - message: "First word of the first line should be properly capitalized ('Test', not 'test')", - column: 4, - line: 38, - type: '', - provider: 'pydocstyle', - }, - { - code: 'D400', - severity: LintMessageSeverity.Information, - message: "First line should end with a period (not 'g')", - column: 4, - line: 38, - type: '', - provider: 'pydocstyle', - }, - { - code: 'D403', - severity: LintMessageSeverity.Information, - message: "First word of the first line should be properly capitalized ('Test', not 'test')", - column: 4, - line: 53, - type: '', - provider: 'pydocstyle', - }, - { - code: 'D400', - severity: LintMessageSeverity.Information, - message: "First line should end with a period (not 'g')", - column: 4, - line: 53, - type: '', - provider: 'pydocstyle', - }, - { - code: 'D403', - severity: LintMessageSeverity.Information, - message: "First word of the first line should be properly capitalized ('Test', not 'test')", - column: 4, - line: 68, - type: '', - provider: 'pydocstyle', - }, - { - code: 'D400', - severity: LintMessageSeverity.Information, - message: "First line should end with a period (not 'g')", - column: 4, - line: 68, - type: '', - provider: 'pydocstyle', - }, - { - code: 'D403', - severity: LintMessageSeverity.Information, - message: "First word of the first line should be properly capitalized ('Test', not 'test')", - column: 4, - line: 80, - type: '', - provider: 'pydocstyle', - }, - { - code: 'D400', - severity: LintMessageSeverity.Information, - message: "First line should end with a period (not 'g')", - column: 4, - line: 80, - type: '', - provider: 'pydocstyle', - }, -]; - -const filteredFlake8MessagesToBeReturned: ILintMessage[] = [ - { - line: 87, - column: 24, - severity: LintMessageSeverity.Warning, - code: 'W292', - message: 'no newline at end of file', - provider: '', - type: '', - }, -]; -const filteredPycodestyleMessagesToBeReturned: ILintMessage[] = [ - { - line: 87, - column: 24, - severity: LintMessageSeverity.Warning, - code: 'W292', - message: 'no newline at end of file', - provider: '', - type: '', - }, -]; - -function getMessages(product: Product): ILintMessage[] { - switch (product) { - case Product.pylint: { - return pylintMessagesToBeReturned; - } - case Product.flake8: { - return flake8MessagesToBeReturned; - } - case Product.pycodestyle: { - return pycodestyleMessagesToBeReturned; - } - case Product.pydocstyle: { - return pydocstyleMessagesToBeReturned; - } - default: { - throwUnknownProduct(product); - return []; - } - } -} - -async function getInfoForConfig(product: Product) { - const prodID = getLinterID(product); - const dirname = linterConfigDirs.get(prodID); - assert.notStrictEqual(dirname, undefined, `tests not set up for ${Product[product]}`); - - const filename = path.join(dirname!, product === Product.pylint ? 'file2.py' : 'file.py'); - let messagesToBeReceived: ILintMessage[] = []; - switch (product) { - case Product.flake8: { - messagesToBeReceived = filteredFlake8MessagesToBeReturned; - break; - } - case Product.pycodestyle: { - messagesToBeReceived = filteredPycodestyleMessagesToBeReturned; - break; - } - default: { - break; - } - } - const basename = linterConfigRCFiles.get(prodID); - return { - filename, - messagesToBeReceived, - origRCFile: basename ? path.join(dirname!, basename) : '', - }; -} - -class TestFixture extends BaseTestFixture { - constructor(printLogs = false) { - const serviceContainer = TypeMoq.Mock.ofType(undefined, TypeMoq.MockBehavior.Strict); - const configService = TypeMoq.Mock.ofType(undefined, TypeMoq.MockBehavior.Strict); - const processLogger = TypeMoq.Mock.ofType(undefined, TypeMoq.MockBehavior.Strict); - const componentAdapter = TypeMoq.Mock.ofType(undefined, TypeMoq.MockBehavior.Strict); - componentAdapter - .setup((c) => c.getCondaEnvironment(TypeMoq.It.isAny())) - .returns(() => Promise.resolve(undefined)); - - const filesystem = new FileSystem(); - processLogger - .setup((p) => p.logProcess(TypeMoq.It.isAnyString(), TypeMoq.It.isAny(), TypeMoq.It.isAny())) - .returns(() => { - /** No body */ - }); - serviceContainer - .setup((s) => s.get(TypeMoq.It.isValue(IProcessLogger), TypeMoq.It.isAny())) - .returns(() => processLogger.object); - serviceContainer - .setup((s) => s.get(TypeMoq.It.isValue(IFileSystem), TypeMoq.It.isAny())) - .returns(() => filesystem); - serviceContainer - .setup((s) => s.get(TypeMoq.It.isValue(IComponentAdapter), TypeMoq.It.isAny())) - .returns(() => componentAdapter.object); - const activatedEnvironmentLaunch = TypeMoq.Mock.ofType(); - activatedEnvironmentLaunch - .setup((a) => a.selectIfLaunchedViaActivatedEnv()) - .returns(() => Promise.resolve(undefined)); - serviceContainer - .setup((s) => s.get(TypeMoq.It.isValue(IActivatedEnvironmentLaunch), TypeMoq.It.isAny())) - .returns(() => activatedEnvironmentLaunch.object); - const platformService = new PlatformService(); - - super( - platformService, - filesystem, - TestFixture.newPythonToolExecService(serviceContainer.object), - TestFixture.newPythonExecFactory(serviceContainer, configService.object), - configService, - serviceContainer, - false, - workspaceDir, - printLogs, - ); - - this.pythonSettings.setup((s) => s.pythonPath).returns(() => PYTHON_PATH); - } - - private static newPythonToolExecService(serviceContainer: IServiceContainer): IPythonToolExecutionService { - // We do not worry about the IProcessServiceFactory possibly - // needed by PythonToolExecutionService. - return new PythonToolExecutionService(serviceContainer); - } - - private static newPythonExecFactory( - serviceContainer: TypeMoq.IMock, - configService: IConfigurationService, - ): IPythonExecutionFactory { - const envVarsService = TypeMoq.Mock.ofType( - undefined, - TypeMoq.MockBehavior.Strict, - ); - envVarsService - .setup((e) => e.getEnvironmentVariables(TypeMoq.It.isAny())) - .returns(() => Promise.resolve(process.env)); - serviceContainer - .setup((c) => c.get(TypeMoq.It.isValue(IEnvironmentVariablesProvider), TypeMoq.It.isAny())) - .returns(() => envVarsService.object); - const disposableRegistry: IDisposableRegistry = []; - serviceContainer - .setup((c) => c.get(TypeMoq.It.isValue(IDisposableRegistry), TypeMoq.It.isAny())) - .returns(() => disposableRegistry); - - const envActivationService = TypeMoq.Mock.ofType( - undefined, - TypeMoq.MockBehavior.Strict, - ); - - const interpreterService = TypeMoq.Mock.ofType(undefined, TypeMoq.MockBehavior.Strict); - interpreterService.setup((i) => i.hasInterpreters()).returns(() => Promise.resolve(true)); - serviceContainer - .setup((c) => c.get(TypeMoq.It.isValue(IInterpreterService), TypeMoq.It.isAny())) - .returns(() => interpreterService.object); - - sinon.stub(Conda, 'getConda').resolves(new Conda('conda')); - sinon.stub(Conda.prototype, 'getCondaVersion').resolves(undefined); - - const processLogger = TypeMoq.Mock.ofType(undefined, TypeMoq.MockBehavior.Strict); - processLogger - .setup((p) => p.logProcess(TypeMoq.It.isAnyString(), TypeMoq.It.isAny(), TypeMoq.It.isAny())) - .returns(() => { - /** No body */ - }); - const procServiceFactory = new ProcessServiceFactory( - envVarsService.object, - processLogger.object, - disposableRegistry, - ); - const pyenvs: IComponentAdapter = mock(); - - const autoSelection = mock(); - const interpreterPathExpHelper = mock(); - when(interpreterPathExpHelper.get(anything())).thenReturn('selected interpreter path'); - - return new PythonExecutionFactory( - serviceContainer.object, - envActivationService.object, - procServiceFactory, - configService, - instance(pyenvs), - instance(autoSelection), - instance(interpreterPathExpHelper), - ); - } - - // eslint-disable-next-line class-methods-use-this - public makeDocument(filename: string): TextDocument { - const doc = newMockDocument(filename); - - doc.setup((d) => d.lineAt(TypeMoq.It.isAny())).returns((lno) => { - const lines = fs.readFileSync(filename).toString().split(os.EOL); - const textline = TypeMoq.Mock.ofType(undefined, TypeMoq.MockBehavior.Strict); - textline.setup((t) => t.text).returns(() => lines[lno]); - return textline.object; - }); - - return doc.object; - } -} - -suite('Linting Functional Tests', () => { - let isExtensionEnabledStub: sinon.SinonStub; - let isExtensionDisabledStub: sinon.SinonStub; - let doNotShowPromptStateStub: sinon.SinonStub; - let persistentState: TypeMoq.IMock>; - setup(() => { - isExtensionEnabledStub = sinon.stub(promptApis, 'isExtensionEnabled'); - isExtensionDisabledStub = sinon.stub(promptApis, 'isExtensionDisabled'); - // For these tests we assume that linter extensions are not installed. - isExtensionEnabledStub.returns(false); - isExtensionDisabledStub.returns(false); - - persistentState = TypeMoq.Mock.ofType>(); - persistentState.setup((p) => p.value).returns(() => true); - doNotShowPromptStateStub = sinon.stub(promptApis, 'doNotShowPromptState'); - doNotShowPromptStateStub.returns(persistentState.object); - }); - teardown(() => { - sinon.restore(); - }); - - const pythonPath = childProcess.execSync(`"${PYTHON_PATH}" -c "import sys;print(sys.executable)"`); - - console.log(`Testing linter with python ${pythonPath}`); - - // These are integration tests that mock out everything except - // the filesystem and process execution. - - async function testLinterMessages( - fixture: TestFixture, - product: Product, - pythonFile: string, - messagesToBeReceived: ILintMessage[], - ) { - const doc = fixture.makeDocument(pythonFile); - await fixture.linterManager.setActiveLintersAsync([product], doc.uri); - const linter = await fixture.linterManager.createLinter(product, fixture.serviceContainer.object); - - const messages = await linter.lint(doc, new CancellationTokenSource().token); - - if (messagesToBeReceived.length === 0) { - assert.strictEqual(messages.length, 0, `No errors in linter, Output - ${fixture.output}`); - } else if (fixture.output.indexOf('ENOENT') === -1) { - // Pylint for Python Version 2.7 could return 80 linter messages, where as in 3.5 it might only return 1. - // Looks like pylint stops linting as soon as it comes across any ERRORS. - assert.notStrictEqual(messages.length, 0, `No errors in linter, Output - ${fixture.output}`); - } - } - for (const product of LINTERID_BY_PRODUCT.keys()) { - test(getProductName(product), async function () { - if ([Product.bandit, Product.mypy, Product.pylama, Product.prospector].some((p) => p === product)) { - return this.skip(); - } - - const fixture = new TestFixture(); - const messagesToBeReturned = getMessages(product); - await testLinterMessages(fixture, product, fileToLint, messagesToBeReturned); - - return undefined; - }); - } - for (const product of LINTERID_BY_PRODUCT.keys()) { - test(`${getProductName(product)} with config in root`, async function () { - if ([Product.bandit, Product.mypy, Product.pylama, Product.prospector].some((p) => p === product)) { - return this.skip(); - } - - const fixture = new TestFixture(); - const { filename, messagesToBeReceived, origRCFile } = await getInfoForConfig(product); - let rcfile = ''; - async function cleanUp() { - if (rcfile !== '') { - await deleteFile(rcfile); - } - } - if (origRCFile !== '') { - rcfile = path.join(workspaceUri.fsPath, path.basename(origRCFile)); - await fs.copy(origRCFile, rcfile); - } - - try { - await testLinterMessages(fixture, product, filename, messagesToBeReceived); - } finally { - await cleanUp(); - } - - return undefined; - }); - } - - async function testLinterMessageCount( - fixture: TestFixture, - product: Product, - pythonFile: string, - messageCountToBeReceived: number, - ) { - const doc = fixture.makeDocument(pythonFile); - await fixture.linterManager.setActiveLintersAsync([product], doc.uri); - const linter = await fixture.linterManager.createLinter(product, fixture.serviceContainer.object); - - const messages = await linter.lint(doc, new CancellationTokenSource().token); - - assert.strictEqual( - messages.length, - messageCountToBeReceived, - 'Expected number of lint errors does not match lint error count', - ); - } - test('Three line output counted as one message', async () => { - const maxErrors = 5; - const fixture = new TestFixture(); - fixture.lintingSettings.maxNumberOfProblems = maxErrors; - await testLinterMessageCount( - fixture, - Product.pylint, - path.join(pythonFilesDir, 'threeLineLints.py'), - maxErrors, - ); - }); - - test('Linters use config in cwd directory', async () => { - const maxErrors = 0; - const fixture = new TestFixture(); - fixture.lintingSettings.cwd = path.join(pythonFilesDir, 'pylintcwd'); - - await testLinterMessageCount( - fixture, - Product.pylint, - path.join(pythonFilesDir, 'threeLineLints.py'), - maxErrors, - ); - }); -}); diff --git a/src/test/linters/lint.multilinter.test.ts b/src/test/linters/lint.multilinter.test.ts deleted file mode 100644 index dba263e78479..000000000000 --- a/src/test/linters/lint.multilinter.test.ts +++ /dev/null @@ -1,126 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; - -import * as assert from 'assert'; -import * as path from 'path'; -import { ConfigurationTarget, DiagnosticCollection, Uri, window, workspace } from 'vscode'; -import { LanguageServerType } from '../../client/activation/types'; -import { ICommandManager } from '../../client/common/application/types'; -import { Product } from '../../client/common/installer/productInstaller'; -import { PythonToolExecutionService } from '../../client/common/process/pythonToolService'; -import { ExecutionResult, IPythonToolExecutionService, SpawnOptions } from '../../client/common/process/types'; -import { ExecutionInfo, IConfigurationService } from '../../client/common/types'; -import { ILinterManager } from '../../client/linters/types'; -import { deleteFile, IExtensionTestApi, PythonSettingKeys, rootWorkspaceUri } from '../common'; -import { closeActiveWindows, initialize, initializeTest, IS_MULTI_ROOT_TEST } from '../initialize'; - -const workspaceUri = Uri.file(path.join(__dirname, '..', '..', '..', 'src', 'test')); -const pythonFilesPath = path.join(__dirname, '..', '..', '..', 'src', 'test', 'pythonFiles', 'linting'); - -// Mocked out python tool execution (all we need is mocked linter return values). -class MockPythonToolExecService extends PythonToolExecutionService { - // Mocked samples of linter messages from flake8 and pylint: - public flake8Msg = - '1,1,W,W391:blank line at end of file\ns:142:13), :1\n1,7,E,E999:SyntaxError: invalid syntax\n'; - - public pylintMsg = `[ - { - "type": "error", - "module": "print", - "obj": "", - "line": 1, - "column": 0, - "path": "print.py", - "symbol": "syntax-error", - "message": "Missing parentheses in call to 'print'. Did you mean print(x)? (, line 1)", - "message-id": "E0001" - } -]`; - - // Depending on moduleName being exec'd, return the appropriate sample. - public async execForLinter( - executionInfo: ExecutionInfo, - _options: SpawnOptions, - _resource: Uri, - ): Promise> { - let msg = this.flake8Msg; - if (executionInfo.moduleName === 'pylint') { - msg = this.pylintMsg; - } - return { stdout: msg }; - } -} - -suite('Linting - Multiple Linters Enabled Test', () => { - let api: IExtensionTestApi; - let configService: IConfigurationService; - let linterManager: ILinterManager; - - suiteSetup(async () => { - api = await initialize(); - configService = api.serviceContainer.get(IConfigurationService); - linterManager = api.serviceContainer.get(ILinterManager); - }); - setup(async () => { - await initializeTest(); - await resetSettings(); - - // We only want to return some valid strings from linters, we don't care if they - // are being returned by actual linters (we aren't testing linters here, only how - // our code responds to those linters). - api.serviceManager.rebind(IPythonToolExecutionService, MockPythonToolExecService); - }); - suiteTeardown(closeActiveWindows); - teardown(async () => { - await closeActiveWindows(); - await resetSettings(); - await deleteFile(path.join(workspaceUri.fsPath, '.pylintrc')); - await deleteFile(path.join(workspaceUri.fsPath, '.pydocstyle')); - - // Restore the execution service as it was... - api.serviceManager.rebind(IPythonToolExecutionService, PythonToolExecutionService); - }); - - async function resetSettings() { - // Don't run these updates in parallel, as they are updating the same file. - const target = IS_MULTI_ROOT_TEST ? ConfigurationTarget.WorkspaceFolder : ConfigurationTarget.Workspace; - - await configService.updateSetting('linting.enabled', true, rootWorkspaceUri, target); - await configService.updateSetting('linting.lintOnSave', false, rootWorkspaceUri, target); - - linterManager.getAllLinterInfos().forEach(async (x) => { - await configService.updateSetting(makeSettingKey(x.product), false, rootWorkspaceUri, target); - }); - } - - function makeSettingKey(product: Product): PythonSettingKeys { - return `linting.${linterManager.getLinterInfo(product).enabledSettingName}` as PythonSettingKeys; - } - - test('Multiple linters', async () => { - await closeActiveWindows(); - const document = await workspace.openTextDocument(path.join(pythonFilesPath, 'print.py')); - await window.showTextDocument(document); - await configService.updateSetting( - 'languageServer', - LanguageServerType.Jedi, - undefined, - ConfigurationTarget.Workspace, - ); - await configService.updateSetting('linting.enabled', true, workspaceUri); - await configService.updateSetting('linting.pylintEnabled', true, workspaceUri); - await configService.updateSetting('linting.flake8Enabled', true, workspaceUri); - - const commands = api.serviceContainer.get(ICommandManager); - - const collection = (await commands.executeCommand('python.runLinting')) as DiagnosticCollection; - assert.notStrictEqual(collection, undefined, 'python.runLinting did not return valid diagnostics collection.'); - - const messages = collection!.get(document.uri); - assert.notStrictEqual(messages!.length, 0, 'No diagnostic messages.'); - assert.notStrictEqual(messages!.filter((x) => x.source === 'pylint').length, 0, 'No pylint messages.'); - assert.notStrictEqual(messages!.filter((x) => x.source === 'flake8').length, 0, 'No flake8 messages.'); - }); -}); diff --git a/src/test/linters/lint.multiroot.test.ts b/src/test/linters/lint.multiroot.test.ts deleted file mode 100644 index f89ee86c0b42..000000000000 --- a/src/test/linters/lint.multiroot.test.ts +++ /dev/null @@ -1,179 +0,0 @@ -import * as assert from 'assert'; -import * as path from 'path'; -import { CancellationTokenSource, ConfigurationTarget, Uri, workspace } from 'vscode'; -import { LanguageServerType } from '../../client/activation/types'; -import { PythonSettings } from '../../client/common/configSettings'; -import { - FormatterProductPathService, - LinterProductPathService, - TestFrameworkProductPathService, -} from '../../client/common/installer/productPath'; -import { ProductService } from '../../client/common/installer/productService'; -import { IProductPathService, IProductService } from '../../client/common/installer/types'; -import { IConfigurationService, Product, ProductType } from '../../client/common/types'; -import { OSType } from '../../client/common/utils/platform'; -import { PythonPathUpdaterService } from '../../client/interpreter/configuration/pythonPathUpdaterService'; -import { PythonPathUpdaterServiceFactory } from '../../client/interpreter/configuration/pythonPathUpdaterServiceFactory'; -import { - IPythonPathUpdaterServiceManager, - IPythonPathUpdaterServiceFactory, -} from '../../client/interpreter/configuration/types'; -import { IActivatedEnvironmentLaunch } from '../../client/interpreter/contracts'; -import { ActivatedEnvironmentLaunch } from '../../client/interpreter/virtualEnvs/activatedEnvLaunch'; -import { ILinter, ILinterManager } from '../../client/linters/types'; -import { isOs } from '../common'; -import { TEST_TIMEOUT } from '../constants'; -import { closeActiveWindows, initialize, initializeTest, IS_MULTI_ROOT_TEST } from '../initialize'; -import { UnitTestIocContainer } from '../testing/serviceRegistry'; - -const multirootPath = path.join(__dirname, '..', '..', '..', 'src', 'testMultiRootWkspc'); - -suite('Multiroot Linting', () => { - const pylintSetting = 'linting.pylintEnabled'; - const flake8Setting = 'linting.flake8Enabled'; - - let ioc: UnitTestIocContainer; - suiteSetup(async function () { - if (!IS_MULTI_ROOT_TEST) { - this.skip(); - } - await initialize(); - await initializeDI(); - await initializeTest(); - }); - suiteTeardown(async () => { - await ioc?.dispose(); - await closeActiveWindows(); - PythonSettings.dispose(); - }); - teardown(async () => { - await closeActiveWindows(); - }); - - async function initializeDI() { - ioc = new UnitTestIocContainer(); - ioc.registerCommonTypes(false); - ioc.registerProcessTypes(); - ioc.registerLinterTypes(); - ioc.registerVariableTypes(); - ioc.registerFileSystemTypes(); - await ioc.registerMockInterpreterTypes(); - ioc.serviceManager.addSingleton( - IActivatedEnvironmentLaunch, - ActivatedEnvironmentLaunch, - ); - ioc.serviceManager.addSingleton( - IPythonPathUpdaterServiceManager, - PythonPathUpdaterService, - ); - ioc.serviceManager.addSingleton( - IPythonPathUpdaterServiceFactory, - PythonPathUpdaterServiceFactory, - ); - ioc.registerInterpreterStorageTypes(); - ioc.serviceManager.addSingletonInstance(IProductService, new ProductService()); - ioc.serviceManager.addSingleton( - IProductPathService, - FormatterProductPathService, - ProductType.Formatter, - ); - ioc.serviceManager.addSingleton( - IProductPathService, - LinterProductPathService, - ProductType.Linter, - ); - ioc.serviceManager.addSingleton( - IProductPathService, - TestFrameworkProductPathService, - ProductType.TestFramework, - ); - } - - async function createLinter(product: Product): Promise { - const lm = ioc.serviceContainer.get(ILinterManager); - return lm.createLinter(product, ioc.serviceContainer); - } - async function testLinterInWorkspaceFolder( - product: Product, - workspaceFolderRelativePath: string, - mustHaveErrors: boolean, - ): Promise { - const fileToLint = path.join(multirootPath, workspaceFolderRelativePath, 'file.py'); - const cancelToken = new CancellationTokenSource(); - const document = await workspace.openTextDocument(fileToLint); - - const linter = await createLinter(product); - const messages = await linter.lint(document, cancelToken.token); - - const errorMessage = mustHaveErrors ? 'No errors returned by linter' : 'Errors returned by linter'; - assert.strictEqual(messages.length > 0, mustHaveErrors, errorMessage); - } - - test('Enabling Pylint in root and also in Workspace, should return errors', async function () { - // Timing out on Windows, tracked by #18337. - if (isOs(OSType.Windows)) { - return this.skip(); - } - - await runTest(Product.pylint, true, true, pylintSetting); - - return undefined; - }).timeout(TEST_TIMEOUT * 2); - test('Enabling Pylint in root and disabling in Workspace, should not return errors', async () => { - await runTest(Product.pylint, true, false, pylintSetting); - }).timeout(TEST_TIMEOUT * 2); - test('Disabling Pylint in root and enabling in Workspace, should return errors', async function () { - // Timing out on Windows, tracked by #18337. - if (isOs(OSType.Windows)) { - return this.skip(); - } - - await runTest(Product.pylint, false, true, pylintSetting); - - return undefined; - }).timeout(TEST_TIMEOUT * 2); - - test('Enabling Flake8 in root and also in Workspace, should return errors', async function () { - // Timing out on Windows, tracked by #18337. - if (isOs(OSType.Windows)) { - return this.skip(); - } - - await runTest(Product.flake8, true, true, flake8Setting); - - return undefined; - }).timeout(TEST_TIMEOUT * 2); - test('Enabling Flake8 in root and disabling in Workspace, should not return errors', async () => { - await runTest(Product.flake8, true, false, flake8Setting); - }).timeout(TEST_TIMEOUT * 2); - test('Disabling Flake8 in root and enabling in Workspace, should return errors', async function () { - // Timing out on Windows, tracked by #18337. - if (isOs(OSType.Windows)) { - return this.skip(); - } - - await runTest(Product.flake8, false, true, flake8Setting); - - return undefined; - }).timeout(TEST_TIMEOUT * 2); - - async function runTest(product: Product, global: boolean, wks: boolean, setting: string): Promise { - const config = ioc.serviceContainer.get(IConfigurationService); - await config.updateSetting( - 'languageServer', - LanguageServerType.Jedi, - Uri.file(multirootPath), - ConfigurationTarget.Global, - ); - await Promise.all([ - config.updateSetting(setting, global, Uri.file(multirootPath), ConfigurationTarget.Global), - config.updateSetting(setting, wks, Uri.file(multirootPath), ConfigurationTarget.Workspace), - ]); - await testLinterInWorkspaceFolder(product, 'workspace1', wks); - await Promise.all( - [ConfigurationTarget.Global, ConfigurationTarget.Workspace].map((configTarget) => - config.updateSetting(setting, undefined, Uri.file(multirootPath), configTarget), - ), - ); - } -}); diff --git a/src/test/linters/lint.provider.test.ts b/src/test/linters/lint.provider.test.ts deleted file mode 100644 index 760c2282ba05..000000000000 --- a/src/test/linters/lint.provider.test.ts +++ /dev/null @@ -1,217 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; - -import { Container } from 'inversify'; -import * as TypeMoq from 'typemoq'; -import * as vscode from 'vscode'; -import { LanguageServerType } from '../../client/activation/types'; -import { - IApplicationShell, - ICommandManager, - IDocumentManager, - IWorkspaceService, -} from '../../client/common/application/types'; -import { PersistentStateFactory } from '../../client/common/persistentState'; -import { IFileSystem } from '../../client/common/platform/types'; -import { - GLOBAL_MEMENTO, - IConfigurationService, - IInstaller, - ILintingSettings, - IMemento, - IPersistentStateFactory, - IPythonSettings, - Product, - Resource, - WORKSPACE_MEMENTO, -} from '../../client/common/types'; -import { createDeferred } from '../../client/common/utils/async'; -import { - IInterpreterAutoSelectionService, - IInterpreterAutoSelectionProxyService, -} from '../../client/interpreter/autoSelection/types'; -import { IInterpreterService } from '../../client/interpreter/contracts'; -import { ServiceContainer } from '../../client/ioc/container'; -import { ServiceManager } from '../../client/ioc/serviceManager'; -import { LinterManager } from '../../client/linters/linterManager'; -import { ILinterManager, ILintingEngine } from '../../client/linters/types'; -import { LinterProvider } from '../../client/providers/linterProvider'; -import { initialize } from '../initialize'; -import { MockAutoSelectionService } from '../mocks/autoSelector'; -import { MockMemento } from '../mocks/mementos'; - -suite('Linting - Provider', () => { - let interpreterService: TypeMoq.IMock; - let engine: TypeMoq.IMock; - let configService: TypeMoq.IMock; - let docManager: TypeMoq.IMock; - let settings: TypeMoq.IMock; - let lm: ILinterManager; - let serviceContainer: ServiceContainer; - let emitter: vscode.EventEmitter; - let document: TypeMoq.IMock; - let fs: TypeMoq.IMock; - let appShell: TypeMoq.IMock; - let linterInstaller: TypeMoq.IMock; - let workspaceService: TypeMoq.IMock; - let workspaceConfig: TypeMoq.IMock; - - suiteSetup(initialize); - setup(async () => { - const cont = new Container(); - const serviceManager = new ServiceManager(cont); - - serviceContainer = new ServiceContainer(cont); - - fs = TypeMoq.Mock.ofType(); - fs.setup((x) => x.fileExists(TypeMoq.It.isAny())).returns( - () => new Promise((resolve) => resolve(true)), - ); - fs.setup((x) => x.arePathsSame(TypeMoq.It.isAnyString(), TypeMoq.It.isAnyString())).returns(() => true); - serviceManager.addSingletonInstance(IFileSystem, fs.object); - - interpreterService = TypeMoq.Mock.ofType(); - serviceManager.addSingletonInstance(IInterpreterService, interpreterService.object); - - engine = TypeMoq.Mock.ofType(); - serviceManager.addSingletonInstance(ILintingEngine, engine.object); - - docManager = TypeMoq.Mock.ofType(); - serviceManager.addSingletonInstance(IDocumentManager, docManager.object); - - const lintSettings = TypeMoq.Mock.ofType(); - lintSettings.setup((x) => x.enabled).returns(() => true); - lintSettings.setup((x) => x.lintOnSave).returns(() => true); - - settings = TypeMoq.Mock.ofType(); - settings.setup((x) => x.linting).returns(() => lintSettings.object); - settings.setup((p) => p.languageServer).returns(() => LanguageServerType.Jedi); - - configService = TypeMoq.Mock.ofType(); - configService.setup((x) => x.getSettings(TypeMoq.It.isAny())).returns(() => settings.object); - serviceManager.addSingletonInstance(IConfigurationService, configService.object); - - appShell = TypeMoq.Mock.ofType(); - linterInstaller = TypeMoq.Mock.ofType(); - - workspaceService = TypeMoq.Mock.ofType(); - workspaceConfig = TypeMoq.Mock.ofType(); - workspaceService - .setup((w) => w.getConfiguration('python', TypeMoq.It.isAny())) - .returns(() => workspaceConfig.object); - workspaceService.setup((w) => w.getConfiguration('python')).returns(() => workspaceConfig.object); - - serviceManager.addSingletonInstance(IApplicationShell, appShell.object); - serviceManager.addSingletonInstance(IInstaller, linterInstaller.object); - serviceManager.addSingletonInstance(IWorkspaceService, workspaceService.object); - serviceManager.addSingleton( - IInterpreterAutoSelectionService, - MockAutoSelectionService, - ); - serviceManager.addSingleton( - IInterpreterAutoSelectionProxyService, - MockAutoSelectionService, - ); - serviceManager.addSingleton(IPersistentStateFactory, PersistentStateFactory); - serviceManager.addSingleton(IMemento, MockMemento, GLOBAL_MEMENTO); - serviceManager.addSingleton(IMemento, MockMemento, WORKSPACE_MEMENTO); - serviceManager.addSingletonInstance( - ICommandManager, - TypeMoq.Mock.ofType().object, - ); - lm = new LinterManager(configService.object); - serviceManager.addSingletonInstance(ILinterManager, lm); - emitter = new vscode.EventEmitter(); - document = TypeMoq.Mock.ofType(); - }); - - test('Lint on open file', async () => { - docManager.setup((x) => x.onDidOpenTextDocument).returns(() => emitter.event); - document.setup((x) => x.uri).returns(() => vscode.Uri.file('test.py')); - document.setup((x) => x.languageId).returns(() => 'python'); - - const linterProvider = new LinterProvider(serviceContainer); - await linterProvider.activate(); - emitter.fire(document.object); - engine.verify((x) => x.lintDocument(document.object, 'auto'), TypeMoq.Times.once()); - }); - - test('Lint on save file', async () => { - docManager.setup((x) => x.onDidSaveTextDocument).returns(() => emitter.event); - document.setup((x) => x.uri).returns(() => vscode.Uri.file('test.py')); - document.setup((x) => x.languageId).returns(() => 'python'); - - const linterProvider = new LinterProvider(serviceContainer); - await linterProvider.activate(); - emitter.fire(document.object); - engine.verify((x) => x.lintDocument(document.object, 'save'), TypeMoq.Times.once()); - }); - - test('No lint on open other files', async () => { - docManager.setup((x) => x.onDidOpenTextDocument).returns(() => emitter.event); - document.setup((x) => x.uri).returns(() => vscode.Uri.file('test.cs')); - document.setup((x) => x.languageId).returns(() => 'csharp'); - - const linterProvider = new LinterProvider(serviceContainer); - await linterProvider.activate(); - emitter.fire(document.object); - engine.verify((x) => x.lintDocument(document.object, 'save'), TypeMoq.Times.never()); - }); - - test('No lint on save other files', async () => { - docManager.setup((x) => x.onDidSaveTextDocument).returns(() => emitter.event); - document.setup((x) => x.uri).returns(() => vscode.Uri.file('test.cs')); - document.setup((x) => x.languageId).returns(() => 'csharp'); - - const linterProvider = new LinterProvider(serviceContainer); - await linterProvider.activate(); - emitter.fire(document.object); - engine.verify((x) => x.lintDocument(document.object, 'save'), TypeMoq.Times.never()); - }); - - test('Lint on change interpreters', async () => { - const e = new vscode.EventEmitter(); - interpreterService.setup((x) => x.onDidChangeInterpreter).returns(() => e.event); - - const linterProvider = new LinterProvider(serviceContainer); - await linterProvider.activate(); - e.fire(undefined); - engine.verify((x) => x.lintOpenPythonFiles(), TypeMoq.Times.once()); - }); - - test('Lint on save pylintrc', async () => { - docManager.setup((x) => x.onDidSaveTextDocument).returns(() => emitter.event); - document.setup((x) => x.uri).returns(() => vscode.Uri.file('.pylintrc')); - - await lm.setActiveLintersAsync([Product.pylint]); - const linterProvider = new LinterProvider(serviceContainer); - await linterProvider.activate(); - emitter.fire(document.object); - - const deferred = createDeferred(); - setTimeout(() => deferred.resolve(), 2000); - await deferred.promise; - engine.verify((x) => x.lintOpenPythonFiles(), TypeMoq.Times.once()); - }); - - test('Diagnostic cleared on file close', async () => testClearDiagnosticsOnClose(true)); - test('Diagnostic not cleared on file opened in another tab', async () => testClearDiagnosticsOnClose(false)); - - async function testClearDiagnosticsOnClose(closed: boolean) { - docManager.setup((x) => x.onDidCloseTextDocument).returns(() => emitter.event); - - const uri = vscode.Uri.file('test.py'); - document.setup((x) => x.uri).returns(() => uri); - document.setup((x) => x.isClosed).returns(() => closed); - - docManager.setup((x) => x.textDocuments).returns(() => (closed ? [] : [document.object])); - const linterProvider = new LinterProvider(serviceContainer); - await linterProvider.activate(); - - emitter.fire(document.object); - const timesExpected = closed ? TypeMoq.Times.once() : TypeMoq.Times.never(); - engine.verify((x) => x.clearDiagnostics(TypeMoq.It.isAny()), timesExpected); - } -}); diff --git a/src/test/linters/lint.test.ts b/src/test/linters/lint.test.ts deleted file mode 100644 index d2eef3c9e321..000000000000 --- a/src/test/linters/lint.test.ts +++ /dev/null @@ -1,119 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; - -import * as assert from 'assert'; -import { ConfigurationTarget } from 'vscode'; -import { Product } from '../../client/common/installer/productInstaller'; -import { - FormatterProductPathService, - LinterProductPathService, - TestFrameworkProductPathService, -} from '../../client/common/installer/productPath'; -import { ProductService } from '../../client/common/installer/productService'; -import { IProductPathService, IProductService } from '../../client/common/installer/types'; -import { IConfigurationService, ILintingSettings, ProductType } from '../../client/common/types'; -import { LINTERID_BY_PRODUCT } from '../../client/linters/constants'; -import { LinterManager } from '../../client/linters/linterManager'; -import { ILinterManager } from '../../client/linters/types'; -import { rootWorkspaceUri } from '../common'; -import { closeActiveWindows, initialize, initializeTest, IS_MULTI_ROOT_TEST } from '../initialize'; -import { UnitTestIocContainer } from '../testing/serviceRegistry'; - -suite('Linting Settings', () => { - let ioc: UnitTestIocContainer; - let linterManager: ILinterManager; - let configService: IConfigurationService; - - suiteSetup(async () => { - await initialize(); - }); - setup(async () => { - await initializeDI(); - await initializeTest(); - }); - suiteTeardown(closeActiveWindows); - teardown(async () => { - await closeActiveWindows(); - await resetSettings(); - await ioc.dispose(); - }); - - async function initializeDI() { - ioc = new UnitTestIocContainer(); - ioc.registerCommonTypes(false); - ioc.registerProcessTypes(); - ioc.registerLinterTypes(); - ioc.registerVariableTypes(); - ioc.registerPlatformTypes(); - configService = ioc.serviceContainer.get(IConfigurationService); - linterManager = new LinterManager(configService); - ioc.serviceManager.addSingletonInstance(IProductService, new ProductService()); - ioc.serviceManager.addSingleton( - IProductPathService, - FormatterProductPathService, - ProductType.Formatter, - ); - ioc.serviceManager.addSingleton( - IProductPathService, - LinterProductPathService, - ProductType.Linter, - ); - ioc.serviceManager.addSingleton( - IProductPathService, - TestFrameworkProductPathService, - ProductType.TestFramework, - ); - } - - async function resetSettings(lintingEnabled = true) { - // Don't run these updates in parallel, as they are updating the same file. - const target = IS_MULTI_ROOT_TEST ? ConfigurationTarget.WorkspaceFolder : ConfigurationTarget.Workspace; - - await configService.updateSetting('linting.enabled', lintingEnabled, rootWorkspaceUri, target); - await configService.updateSetting('linting.lintOnSave', false, rootWorkspaceUri, target); - - linterManager.getAllLinterInfos().forEach(async (x) => { - const settingKey = `linting.${x.enabledSettingName}`; - await configService.updateSetting(settingKey, false, rootWorkspaceUri, target); - }); - } - - test('enable through manager (global)', async () => { - const settings = configService.getSettings(); - await resetSettings(false); - - await linterManager.enableLintingAsync(false); - assert.strictEqual(settings.linting.enabled, false, 'mismatch'); - - await linterManager.enableLintingAsync(true); - assert.strictEqual(settings.linting.enabled, true, 'mismatch'); - }); - - LINTERID_BY_PRODUCT.forEach((_, key) => { - const product = Product[key]; - - test(`enable through manager (${product})`, async () => { - const settings = configService.getSettings(); - await resetSettings(); - - const name = `${product}Enabled` as keyof ILintingSettings; - - assert.strictEqual(settings.linting[name], false, 'mismatch'); - - await linterManager.setActiveLintersAsync([key]); - - assert.strictEqual(settings.linting[name], true, 'mismatch'); - linterManager.getAllLinterInfos().forEach(async (x) => { - if (x.product !== key) { - assert.strictEqual( - settings.linting[x.enabledSettingName as keyof ILintingSettings], - false, - 'mismatch', - ); - } - }); - }); - }); -}); diff --git a/src/test/linters/lint.unit.test.ts b/src/test/linters/lint.unit.test.ts deleted file mode 100644 index 02bdd4c82c79..000000000000 --- a/src/test/linters/lint.unit.test.ts +++ /dev/null @@ -1,854 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; - -import * as assert from 'assert'; -import * as os from 'os'; -import * as sinon from 'sinon'; -import * as TypeMoq from 'typemoq'; -import { CancellationTokenSource, TextDocument, TextLine } from 'vscode'; -import { Product } from '../../client/common/installer/productInstaller'; -import { ProductNames } from '../../client/common/installer/productNames'; -import { ProductService } from '../../client/common/installer/productService'; -import { IFileSystem, IPlatformService } from '../../client/common/platform/types'; -import { - IPythonExecutionFactory, - IPythonExecutionService, - IPythonToolExecutionService, -} from '../../client/common/process/types'; -import { IPersistentState, ProductType } from '../../client/common/types'; -import { LINTERID_BY_PRODUCT } from '../../client/linters/constants'; -import * as promptApis from '../../client/linters/prompts/common'; -import { ILintMessage, LintMessageSeverity } from '../../client/linters/types'; -import { - BaseTestFixture, - getLinterID, - getProductName, - linterMessageAsLine, - pylintLinterMessagesAsOutput, - throwUnknownProduct, -} from './common'; - -const pylintMessagesToBeReturned: ILintMessage[] = [ - { - line: 24, - column: 0, - severity: LintMessageSeverity.Information, - code: 'I0011', - message: 'Locally disabling no-member (E1101)', - provider: '', - type: 'warning', - }, - { - line: 30, - column: 0, - severity: LintMessageSeverity.Information, - code: 'I0011', - message: 'Locally disabling no-member (E1101)', - provider: '', - type: 'warning', - }, - { - line: 34, - column: 0, - severity: LintMessageSeverity.Information, - code: 'I0012', - message: 'Locally enabling no-member (E1101)', - provider: '', - type: 'warning', - }, - { - line: 40, - column: 0, - severity: LintMessageSeverity.Information, - code: 'I0011', - message: 'Locally disabling no-member (E1101)', - provider: '', - type: 'warning', - }, - { - line: 44, - column: 0, - severity: LintMessageSeverity.Information, - code: 'I0012', - message: 'Locally enabling no-member (E1101)', - provider: '', - type: 'warning', - }, - { - line: 55, - column: 0, - severity: LintMessageSeverity.Information, - code: 'I0011', - message: 'Locally disabling no-member (E1101)', - provider: '', - type: 'warning', - }, - { - line: 59, - column: 0, - severity: LintMessageSeverity.Information, - code: 'I0012', - message: 'Locally enabling no-member (E1101)', - provider: '', - type: 'warning', - }, - { - line: 62, - column: 0, - severity: LintMessageSeverity.Information, - code: 'I0011', - message: 'Locally disabling undefined-variable (E0602)', - provider: '', - type: 'warning', - }, - { - line: 70, - column: 0, - severity: LintMessageSeverity.Information, - code: 'I0011', - message: 'Locally disabling no-member (E1101)', - provider: '', - type: 'warning', - }, - { - line: 84, - column: 0, - severity: LintMessageSeverity.Information, - code: 'I0011', - message: 'Locally disabling no-member (E1101)', - provider: '', - type: 'warning', - }, - { - line: 87, - column: 0, - severity: LintMessageSeverity.Hint, - code: 'C0304', - message: 'Final newline missing', - provider: '', - type: 'warning', - }, - { - line: 11, - column: 20, - severity: LintMessageSeverity.Warning, - code: 'W0613', - message: "Unused argument 'arg'", - provider: '', - type: 'warning', - }, - { - line: 26, - column: 14, - severity: LintMessageSeverity.Error, - code: 'E1101', - message: "Instance of 'Foo' has no 'blop' member", - provider: '', - type: 'warning', - }, - { - line: 36, - column: 14, - severity: LintMessageSeverity.Error, - code: 'E1101', - message: "Instance of 'Foo' has no 'blip' member", - provider: '', - type: 'warning', - }, - { - line: 46, - column: 18, - severity: LintMessageSeverity.Error, - code: 'E1101', - message: "Instance of 'Foo' has no 'blip' member", - provider: '', - type: 'warning', - endLine: undefined, - endColumn: undefined, - }, - { - line: 61, - column: 18, - severity: LintMessageSeverity.Error, - code: 'E1101', - message: "Instance of 'Foo' has no 'blip' member", - provider: '', - type: 'warning', - endLine: 61, - endColumn: undefined, - }, - { - line: 72, - column: 18, - severity: LintMessageSeverity.Error, - code: 'E1101', - message: "Instance of 'Foo' has no 'blip' member", - provider: '', - type: 'warning', - endLine: 72, - endColumn: 28, - }, - { - line: 75, - column: 18, - severity: LintMessageSeverity.Error, - code: 'E1101', - message: "Instance of 'Foo' has no 'blip' member", - provider: '', - type: 'warning', - endLine: 75, - endColumn: 28, - }, - { - line: 77, - column: 14, - severity: LintMessageSeverity.Error, - code: 'E1101', - message: "Instance of 'Foo' has no 'blip' member", - provider: '', - type: 'warning', - endLine: 77, - endColumn: 24, - }, - { - line: 83, - column: 14, - severity: LintMessageSeverity.Error, - code: 'E1101', - message: "Instance of 'Foo' has no 'blip' member", - provider: '', - type: 'warning', - endLine: 83, - endColumn: 24, - }, -]; -const flake8MessagesToBeReturned: ILintMessage[] = [ - { - line: 5, - column: 1, - severity: LintMessageSeverity.Error, - code: 'E302', - message: 'expected 2 blank lines, found 1', - provider: '', - type: 'E', - }, - { - line: 19, - column: 15, - severity: LintMessageSeverity.Error, - code: 'E127', - message: 'continuation line over-indented for visual indent', - provider: '', - type: 'E', - }, - { - line: 24, - column: 23, - severity: LintMessageSeverity.Error, - code: 'E261', - message: 'at least two spaces before inline comment', - provider: '', - type: 'E', - }, - { - line: 62, - column: 30, - severity: LintMessageSeverity.Error, - code: 'E261', - message: 'at least two spaces before inline comment', - provider: '', - type: 'E', - }, - { - line: 70, - column: 22, - severity: LintMessageSeverity.Error, - code: 'E261', - message: 'at least two spaces before inline comment', - provider: '', - type: 'E', - }, - { - line: 80, - column: 5, - severity: LintMessageSeverity.Error, - code: 'E303', - message: 'too many blank lines (2)', - provider: '', - type: 'E', - }, - { - line: 87, - column: 24, - severity: LintMessageSeverity.Warning, - code: 'W292', - message: 'no newline at end of file', - provider: '', - type: 'E', - }, -]; -const pycodestyleMessagesToBeReturned: ILintMessage[] = [ - { - line: 5, - column: 1, - severity: LintMessageSeverity.Error, - code: 'E302', - message: 'expected 2 blank lines, found 1', - provider: '', - type: 'E', - }, - { - line: 19, - column: 15, - severity: LintMessageSeverity.Error, - code: 'E127', - message: 'continuation line over-indented for visual indent', - provider: '', - type: 'E', - }, - { - line: 24, - column: 23, - severity: LintMessageSeverity.Error, - code: 'E261', - message: 'at least two spaces before inline comment', - provider: '', - type: 'E', - }, - { - line: 62, - column: 30, - severity: LintMessageSeverity.Error, - code: 'E261', - message: 'at least two spaces before inline comment', - provider: '', - type: 'E', - }, - { - line: 70, - column: 22, - severity: LintMessageSeverity.Error, - code: 'E261', - message: 'at least two spaces before inline comment', - provider: '', - type: 'E', - }, - { - line: 80, - column: 5, - severity: LintMessageSeverity.Error, - code: 'E303', - message: 'too many blank lines (2)', - provider: '', - type: 'E', - }, - { - line: 87, - column: 24, - severity: LintMessageSeverity.Warning, - code: 'W292', - message: 'no newline at end of file', - provider: '', - type: 'E', - }, -]; -const pydocstyleMessagesToBeReturned: ILintMessage[] = [ - { - code: 'D400', - severity: LintMessageSeverity.Information, - message: "First line should end with a period (not 'e')", - column: 0, - line: 1, - type: '', - provider: 'pydocstyle', - }, - { - code: 'D400', - severity: LintMessageSeverity.Information, - message: "First line should end with a period (not 't')", - column: 0, - line: 5, - type: '', - provider: 'pydocstyle', - }, - { - code: 'D102', - severity: LintMessageSeverity.Information, - message: 'Missing docstring in public method', - column: 4, - line: 8, - type: '', - provider: 'pydocstyle', - }, - { - code: 'D401', - severity: LintMessageSeverity.Information, - message: "First line should be in imperative mood ('thi', not 'this')", - column: 4, - line: 11, - type: '', - provider: 'pydocstyle', - }, - { - code: 'D403', - severity: LintMessageSeverity.Information, - message: "First word of the first line should be properly capitalized ('This', not 'this')", - column: 4, - line: 11, - type: '', - provider: 'pydocstyle', - }, - { - code: 'D400', - severity: LintMessageSeverity.Information, - message: "First line should end with a period (not 'e')", - column: 4, - line: 11, - type: '', - provider: 'pydocstyle', - }, - { - code: 'D403', - severity: LintMessageSeverity.Information, - message: "First word of the first line should be properly capitalized ('And', not 'and')", - column: 4, - line: 15, - type: '', - provider: 'pydocstyle', - }, - { - code: 'D400', - severity: LintMessageSeverity.Information, - message: "First line should end with a period (not 't')", - column: 4, - line: 15, - type: '', - provider: 'pydocstyle', - }, - { - code: 'D403', - severity: LintMessageSeverity.Information, - message: "First word of the first line should be properly capitalized ('Test', not 'test')", - column: 4, - line: 21, - type: '', - provider: 'pydocstyle', - }, - { - code: 'D400', - severity: LintMessageSeverity.Information, - message: "First line should end with a period (not 'g')", - column: 4, - line: 21, - type: '', - provider: 'pydocstyle', - }, - { - code: 'D403', - severity: LintMessageSeverity.Information, - message: "First word of the first line should be properly capitalized ('Test', not 'test')", - column: 4, - line: 28, - type: '', - provider: 'pydocstyle', - }, - { - code: 'D400', - severity: LintMessageSeverity.Information, - message: "First line should end with a period (not 'g')", - column: 4, - line: 28, - type: '', - provider: 'pydocstyle', - }, - { - code: 'D403', - severity: LintMessageSeverity.Information, - message: "First word of the first line should be properly capitalized ('Test', not 'test')", - column: 4, - line: 38, - type: '', - provider: 'pydocstyle', - }, - { - code: 'D400', - severity: LintMessageSeverity.Information, - message: "First line should end with a period (not 'g')", - column: 4, - line: 38, - type: '', - provider: 'pydocstyle', - }, - { - code: 'D403', - severity: LintMessageSeverity.Information, - message: "First word of the first line should be properly capitalized ('Test', not 'test')", - column: 4, - line: 53, - type: '', - provider: 'pydocstyle', - }, - { - code: 'D400', - severity: LintMessageSeverity.Information, - message: "First line should end with a period (not 'g')", - column: 4, - line: 53, - type: '', - provider: 'pydocstyle', - }, - { - code: 'D403', - severity: LintMessageSeverity.Information, - message: "First word of the first line should be properly capitalized ('Test', not 'test')", - column: 4, - line: 68, - type: '', - provider: 'pydocstyle', - }, - { - code: 'D400', - severity: LintMessageSeverity.Information, - message: "First line should end with a period (not 'g')", - column: 4, - line: 68, - type: '', - provider: 'pydocstyle', - }, - { - code: 'D403', - severity: LintMessageSeverity.Information, - message: "First word of the first line should be properly capitalized ('Test', not 'test')", - column: 4, - line: 80, - type: '', - provider: 'pydocstyle', - }, - { - code: 'D400', - severity: LintMessageSeverity.Information, - message: "First line should end with a period (not 'g')", - column: 4, - line: 80, - type: '', - provider: 'pydocstyle', - }, -]; - -class TestFixture extends BaseTestFixture { - public platformService: TypeMoq.IMock; - - public filesystem: TypeMoq.IMock; - - public pythonToolExecService: TypeMoq.IMock; - - public pythonExecService: TypeMoq.IMock; - - public pythonExecFactory: TypeMoq.IMock; - - constructor(workspaceDir = '.', printLogs = false) { - const platformService = TypeMoq.Mock.ofType(undefined, TypeMoq.MockBehavior.Strict); - const filesystem = TypeMoq.Mock.ofType(undefined, TypeMoq.MockBehavior.Strict); - const pythonToolExecService = TypeMoq.Mock.ofType( - undefined, - TypeMoq.MockBehavior.Strict, - ); - const pythonExecFactory = TypeMoq.Mock.ofType(undefined, TypeMoq.MockBehavior.Strict); - super( - platformService.object, - filesystem.object, - pythonToolExecService.object, - pythonExecFactory.object, - undefined, - undefined, - true, - workspaceDir, - printLogs, - ); - - this.platformService = platformService; - this.filesystem = filesystem; - this.pythonToolExecService = pythonToolExecService; - this.pythonExecService = TypeMoq.Mock.ofType(undefined, TypeMoq.MockBehavior.Strict); - this.pythonExecFactory = pythonExecFactory; - - this.filesystem.setup((f) => f.fileExists(TypeMoq.It.isAny())).returns(() => Promise.resolve(true)); - // eslint-disable-next-line @typescript-eslint/no-explicit-any - this.pythonExecService.setup((s: any) => s.then).returns(() => undefined); - this.pythonExecService - .setup((s) => s.isModuleInstalled(TypeMoq.It.isAny())) - .returns(() => Promise.resolve(true)); - - this.pythonExecFactory - .setup((f) => f.create(TypeMoq.It.isAny())) - .returns(() => Promise.resolve(this.pythonExecService.object)); - } - - public makeDocument(product: Product, filename: string): TextDocument { - const doc = this.newMockDocument(filename); - if (product === Product.pydocstyle) { - const dummyLine = TypeMoq.Mock.ofType(undefined, TypeMoq.MockBehavior.Strict); - dummyLine.setup((d) => d.text).returns(() => ' ...'); - doc.setup((s) => s.lineAt(TypeMoq.It.isAny())).returns(() => dummyLine.object); - } - return doc.object; - } - - public setDefaultMessages(product: Product): ILintMessage[] { - let messages: ILintMessage[]; - switch (product) { - case Product.pylint: { - messages = pylintMessagesToBeReturned; - break; - } - case Product.flake8: { - messages = flake8MessagesToBeReturned; - break; - } - case Product.pycodestyle: { - messages = pycodestyleMessagesToBeReturned; - break; - } - case Product.pydocstyle: { - messages = pydocstyleMessagesToBeReturned; - break; - } - default: { - throwUnknownProduct(product); - return []; - } - } - this.setMessages(messages, product); - return messages; - } - - public setMessages(messages: ILintMessage[], product?: Product) { - if (messages.length === 0) { - this.setStdout(''); - return; - } - - if (product && getLinterID(product) === 'pylint') { - this.setStdout(pylintLinterMessagesAsOutput(messages)); - return; - } - const lines: string[] = []; - for (const msg of messages) { - if (msg.provider === '' && product) { - msg.provider = getLinterID(product); - } - const line = linterMessageAsLine(msg); - lines.push(line); - } - this.setStdout(lines.join(os.EOL) + os.EOL); - } - - public setStdout(stdout: string) { - this.pythonToolExecService - .setup((s) => s.execForLinter(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny())) - .returns(() => Promise.resolve({ stdout })); - } -} - -suite('Linting Scenarios', () => { - // Note that these aren't actually unit tests. Instead they are - // integration tests with heavy usage of mocks. - - let isExtensionEnabledStub: sinon.SinonStub; - let isExtensionDisabledStub: sinon.SinonStub; - let doNotShowPromptStateStub: sinon.SinonStub; - let persistentState: TypeMoq.IMock>; - setup(() => { - isExtensionEnabledStub = sinon.stub(promptApis, 'isExtensionEnabled'); - isExtensionDisabledStub = sinon.stub(promptApis, 'isExtensionDisabled'); - // For these tests we assume that linter extensions are not installed. - isExtensionEnabledStub.returns(false); - isExtensionDisabledStub.returns(false); - - persistentState = TypeMoq.Mock.ofType>(); - persistentState.setup((p) => p.value).returns(() => true); - doNotShowPromptStateStub = sinon.stub(promptApis, 'doNotShowPromptState'); - doNotShowPromptStateStub.returns(persistentState.object); - }); - - teardown(() => { - sinon.restore(); - }); - - test('No linting with PyLint (enabled) when disabled at top-level', async () => { - const product = Product.pylint; - const fixture = new TestFixture(); - fixture.lintingSettings.enabled = false; - fixture.setDefaultMessages(product); - const linter = await fixture.getEnabledLinter(product); - - const messages = await linter.lint( - fixture.makeDocument(product, 'spam.py'), - new CancellationTokenSource().token, - ); - - assert.strictEqual( - messages.length, - 0, - `Unexpected linter errors when linting is disabled, Output - ${fixture.output}`, - ); - }); - - test('No linting with Pylint disabled (and Flake8 enabled)', async () => { - const product = Product.pylint; - const fixture = new TestFixture(); - fixture.lintingSettings.enabled = true; - fixture.lintingSettings.flake8Enabled = true; - fixture.setDefaultMessages(Product.pylint); - const linter = await fixture.getDisabledLinter(product); - - const messages = await linter.lint( - fixture.makeDocument(product, 'spam.py'), - new CancellationTokenSource().token, - ); - - assert.strictEqual( - messages.length, - 0, - `Unexpected linter errors when linting is disabled, Output - ${fixture.output}`, - ); - }); - - async function testEnablingDisablingOfLinter(fixture: TestFixture, product: Product, enabled: boolean) { - fixture.lintingSettings.enabled = true; - fixture.setDefaultMessages(product); - if (enabled) { - fixture.setDefaultMessages(product); - } - const linter = await fixture.getLinter(product, enabled); - - const messages = await linter.lint( - fixture.makeDocument(product, 'spam.py'), - new CancellationTokenSource().token, - ); - - if (enabled) { - assert.notStrictEqual( - messages.length, - 0, - `Expected linter errors when linter is enabled, Output - ${fixture.output}`, - ); - } else { - assert.strictEqual( - messages.length, - 0, - `Unexpected linter errors when linter is disabled, Output - ${fixture.output}`, - ); - } - } - for (const product of LINTERID_BY_PRODUCT.keys()) { - for (const enabled of [false, true]) { - test(`${enabled ? 'Enable' : 'Disable'} ${getProductName(product)} and run linter`, async function () { - // TODO: Add coverage for these linters. - if ([Product.bandit, Product.mypy, Product.pylama, Product.prospector].some((p) => p === product)) { - this.skip(); - } - - const fixture = new TestFixture(); - await testEnablingDisablingOfLinter(fixture, product, enabled); - }); - } - } - for (const useMinimal of [true, false]) { - for (const enabled of [true, false]) { - test(`PyLint ${enabled ? 'enabled' : 'disabled'} with${ - useMinimal ? '' : 'out' - } minimal checkers`, async () => { - const fixture = new TestFixture(); - await testEnablingDisablingOfLinter(fixture, Product.pylint, enabled); - }); - } - } - - async function testLinterMessages(fixture: TestFixture, product: Product) { - const messagesToBeReceived = fixture.setDefaultMessages(product); - const linter = await fixture.getEnabledLinter(product); - - const messages = await linter.lint( - fixture.makeDocument(product, 'spam.py'), - new CancellationTokenSource().token, - ); - - if (messagesToBeReceived.length === 0) { - assert.strictEqual(messages.length, 0, `No errors in linter, Output - ${fixture.output}`); - } else if (fixture.output.indexOf('ENOENT') === -1) { - // Pylint for Python Version 2.7 could return 80 linter messages, where as in 3.5 it might only return 1. - // Looks like pylint stops linting as soon as it comes across any ERRORS. - assert.notStrictEqual(messages.length, 0, `No errors in linter, Output - ${fixture.output}`); - } - } - for (const product of LINTERID_BY_PRODUCT.keys()) { - test(`Check ${getProductName(product)} messages`, async function () { - // TODO: Add coverage for these linters. - if ([Product.bandit, Product.mypy, Product.pylama, Product.prospector].some((p) => p === product)) { - this.skip(); - } - - const fixture = new TestFixture(); - await testLinterMessages(fixture, product); - }); - } - - async function testLinterMessageCount(fixture: TestFixture, product: Product, messageCountToBeReceived: number) { - fixture.setDefaultMessages(product); - const linter = await fixture.getEnabledLinter(product); - - const messages = await linter.lint( - fixture.makeDocument(product, 'spam.py'), - new CancellationTokenSource().token, - ); - - assert.strictEqual( - messages.length, - messageCountToBeReceived, - `Expected number of lint errors does not match lint error count, Output - ${fixture.output}`, - ); - } - test('Three line output counted as one message (Pylint)', async () => { - const maxErrors = 5; - const fixture = new TestFixture(); - fixture.lintingSettings.maxNumberOfProblems = maxErrors; - - await testLinterMessageCount(fixture, Product.pylint, maxErrors); - }); -}); - -suite('Linting Products', () => { - const prodService = new ProductService(); - - test('All linting products are represented by linters', async () => { - const products = Object.keys(Product) - .filter((item) => Number.isNaN(Number(item))) - .map((key) => Product[Number(key)]); - - products.forEach((p) => { - const product = (p as unknown) as Product; - if (prodService.getProductType(product) === ProductType.Linter) { - const found = LINTERID_BY_PRODUCT.get(product); - assert.notStrictEqual(found, undefined, `did find linter ${Product[product]}`); - } - }); - }); - - test('All linters match linting products', async () => { - for (const product of LINTERID_BY_PRODUCT.keys()) { - const prodType = prodService.getProductType(product); - assert.notStrictEqual(prodType, undefined, `${Product[product]} is not not properly registered`); - assert.strictEqual(prodType, ProductType.Linter, `${Product[product]} is not a linter product`); - } - }); - - test('All linting product names match linter IDs', async () => { - for (const [product, linterID] of LINTERID_BY_PRODUCT) { - const prodName = ProductNames.get(product); - assert.strictEqual(prodName, linterID, 'product name does not match linter ID'); - } - }); -}); diff --git a/src/test/linters/lintengine.test.ts b/src/test/linters/lintengine.test.ts deleted file mode 100644 index 1bf77c502af5..000000000000 --- a/src/test/linters/lintengine.test.ts +++ /dev/null @@ -1,178 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; - -import * as TypeMoq from 'typemoq'; -import { TextDocument, Uri } from 'vscode'; -import { IDocumentManager, IWorkspaceService } from '../../client/common/application/types'; -import { PYTHON_LANGUAGE } from '../../client/common/constants'; -import '../../client/common/extensions'; -import { IFileSystem } from '../../client/common/platform/types'; -import { IConfigurationService, ILintingSettings, ILogOutputChannel, IPythonSettings } from '../../client/common/types'; -import { IInterpreterService } from '../../client/interpreter/contracts'; -import { IServiceContainer } from '../../client/ioc/types'; -import { LintingEngine } from '../../client/linters/lintingEngine'; -import { ILinterManager, ILintingEngine } from '../../client/linters/types'; -import { PythonEnvironment } from '../../client/pythonEnvironments/info'; -import { initialize } from '../initialize'; - -suite('Linting - LintingEngine', () => { - let serviceContainer: TypeMoq.IMock; - let lintManager: TypeMoq.IMock; - let settings: TypeMoq.IMock; - let lintSettings: TypeMoq.IMock; - let fileSystem: TypeMoq.IMock; - let lintingEngine: ILintingEngine; - - suiteSetup(initialize); - setup(async () => { - serviceContainer = TypeMoq.Mock.ofType(); - - const docManager = TypeMoq.Mock.ofType(); - serviceContainer - .setup((c) => c.get(TypeMoq.It.isValue(IDocumentManager), TypeMoq.It.isAny())) - .returns(() => docManager.object); - - const workspaceService = TypeMoq.Mock.ofType(); - serviceContainer - .setup((c) => c.get(TypeMoq.It.isValue(IWorkspaceService), TypeMoq.It.isAny())) - .returns(() => workspaceService.object); - - fileSystem = TypeMoq.Mock.ofType(); - serviceContainer - .setup((c) => c.get(TypeMoq.It.isValue(IFileSystem), TypeMoq.It.isAny())) - .returns(() => fileSystem.object); - - lintSettings = TypeMoq.Mock.ofType(); - settings = TypeMoq.Mock.ofType(); - - const configService = TypeMoq.Mock.ofType(); - configService.setup((x) => x.getSettings(TypeMoq.It.isAny())).returns(() => settings.object); - configService.setup((x) => x.isTestExecution()).returns(() => true); - serviceContainer - .setup((c) => c.get(TypeMoq.It.isValue(IConfigurationService), TypeMoq.It.isAny())) - .returns(() => configService.object); - - const outputChannel = TypeMoq.Mock.ofType(); - serviceContainer.setup((c) => c.get(TypeMoq.It.isValue(ILogOutputChannel))).returns(() => outputChannel.object); - - lintManager = TypeMoq.Mock.ofType(); - lintManager.setup((x) => x.isLintingEnabled(TypeMoq.It.isAny())).returns(async () => true); - serviceContainer - .setup((c) => c.get(TypeMoq.It.isValue(ILinterManager), TypeMoq.It.isAny())) - .returns(() => lintManager.object); - - lintingEngine = new LintingEngine(serviceContainer.object); - serviceContainer - .setup((c) => c.get(TypeMoq.It.isValue(ILintingEngine), TypeMoq.It.isAny())) - .returns(() => lintingEngine); - - const interpreterService = TypeMoq.Mock.ofType(); - interpreterService - .setup((i) => i.getActiveInterpreter(TypeMoq.It.isAny())) - .returns(() => Promise.resolve(({ path: 'ps' } as unknown) as PythonEnvironment)); - serviceContainer.setup((c) => c.get(IInterpreterService)).returns(() => interpreterService.object); - }); - - test('Ensure document.uri is passed into isLintingEnabled', () => { - const doc = mockTextDocument('a.py', PYTHON_LANGUAGE, true); - try { - lintingEngine.lintDocument(doc, 'auto').ignoreErrors(); - } catch { - lintManager.verify((l) => l.isLintingEnabled(TypeMoq.It.isValue(doc.uri)), TypeMoq.Times.once()); - } - }); - test('Ensure document.uri is passed into createLinter', () => { - const doc = mockTextDocument('a.py', PYTHON_LANGUAGE, true); - try { - lintingEngine.lintDocument(doc, 'auto').ignoreErrors(); - } catch { - lintManager.verify( - (l) => - l.createLinter( - TypeMoq.It.isAny(), - - TypeMoq.It.isAny(), - TypeMoq.It.isValue(doc.uri), - ), - TypeMoq.Times.atLeastOnce(), - ); - } - }); - - test('Verify files that match ignore pattern are not linted', async () => { - const doc = mockTextDocument('a1.py', PYTHON_LANGUAGE, true, ['a*.py']); - await lintingEngine.lintDocument(doc, 'auto'); - lintManager.verify( - (l) => l.createLinter(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny()), - TypeMoq.Times.never(), - ); - }); - - test('Ensure non-Python files are not linted', async () => { - const doc = mockTextDocument('a.ts', 'typescript', true); - await lintingEngine.lintDocument(doc, 'auto'); - lintManager.verify( - (l) => l.createLinter(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny()), - TypeMoq.Times.never(), - ); - }); - - test('Ensure files with git scheme are not linted', async () => { - const doc = mockTextDocument('a1.py', PYTHON_LANGUAGE, false, [], 'git'); - await lintingEngine.lintDocument(doc, 'auto'); - lintManager.verify( - (l) => l.createLinter(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny()), - TypeMoq.Times.never(), - ); - }); - test('Ensure files with showModifications scheme are not linted', async () => { - const doc = mockTextDocument('a1.py', PYTHON_LANGUAGE, false, [], 'showModifications'); - await lintingEngine.lintDocument(doc, 'auto'); - lintManager.verify( - (l) => l.createLinter(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny()), - TypeMoq.Times.never(), - ); - }); - test('Ensure files with svn scheme are not linted', async () => { - const doc = mockTextDocument('a1.py', PYTHON_LANGUAGE, false, [], 'svn'); - await lintingEngine.lintDocument(doc, 'auto'); - lintManager.verify( - (l) => l.createLinter(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny()), - TypeMoq.Times.never(), - ); - }); - - test('Ensure non-existing files are not linted', async () => { - const doc = mockTextDocument('file.py', PYTHON_LANGUAGE, false, []); - await lintingEngine.lintDocument(doc, 'auto'); - lintManager.verify( - (l) => l.createLinter(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny()), - TypeMoq.Times.never(), - ); - }); - - function mockTextDocument( - fileName: string, - language: string, - exists: boolean, - ignorePattern: string[] = [], - scheme?: string, - ): TextDocument { - fileSystem.setup((x) => x.fileExists(TypeMoq.It.isAnyString())).returns(() => Promise.resolve(exists)); - - lintSettings.setup((l) => l.ignorePatterns).returns(() => ignorePattern); - settings.setup((x) => x.linting).returns(() => lintSettings.object); - - const doc = TypeMoq.Mock.ofType(); - if (scheme) { - doc.setup((d) => d.uri).returns(() => Uri.parse(`${scheme}:${fileName}`)); - } else { - doc.setup((d) => d.uri).returns(() => Uri.file(fileName)); - } - doc.setup((d) => d.fileName).returns(() => fileName); - doc.setup((d) => d.languageId).returns(() => language); - return doc.object; - } -}); diff --git a/src/test/linters/linterCommands.unit.test.ts b/src/test/linters/linterCommands.unit.test.ts deleted file mode 100644 index b3d5c4693832..000000000000 --- a/src/test/linters/linterCommands.unit.test.ts +++ /dev/null @@ -1,182 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; - -import { expect } from 'chai'; -import { anything, capture, deepEqual, instance, mock, verify, when } from 'ts-mockito'; -import { DiagnosticCollection } from 'vscode'; -import { ApplicationShell } from '../../client/common/application/applicationShell'; -import { CommandManager } from '../../client/common/application/commandManager'; -import { DocumentManager } from '../../client/common/application/documentManager'; -import { IApplicationShell, ICommandManager, IDocumentManager } from '../../client/common/application/types'; -import { Commands } from '../../client/common/constants'; -import { Product } from '../../client/common/types'; -import { ServiceContainer } from '../../client/ioc/container'; -import { LinterCommands } from '../../client/linters/linterCommands'; -import { LinterManager } from '../../client/linters/linterManager'; -import { LintingEngine } from '../../client/linters/lintingEngine'; -import { ILinterInfo, ILinterManager, ILintingEngine } from '../../client/linters/types'; - -suite('Linting - Linter Commands', () => { - let linterCommands: LinterCommands; - let manager: ILinterManager; - let shell: IApplicationShell; - let docManager: IDocumentManager; - let cmdManager: ICommandManager; - let lintingEngine: ILintingEngine; - setup(() => { - const svcContainer = mock(ServiceContainer); - manager = mock(LinterManager); - shell = mock(ApplicationShell); - docManager = mock(DocumentManager); - cmdManager = mock(CommandManager); - lintingEngine = mock(LintingEngine); - when(svcContainer.get(ILinterManager)).thenReturn(instance(manager)); - when(svcContainer.get(IApplicationShell)).thenReturn(instance(shell)); - when(svcContainer.get(IDocumentManager)).thenReturn(instance(docManager)); - when(svcContainer.get(ICommandManager)).thenReturn(instance(cmdManager)); - when(svcContainer.get(ILintingEngine)).thenReturn(instance(lintingEngine)); - linterCommands = new LinterCommands(instance(svcContainer)); - }); - - test('Commands are registered', () => { - verify(cmdManager.registerCommand(Commands.Set_Linter, anything())).once(); - verify(cmdManager.registerCommand(Commands.Enable_Linter, anything())).once(); - verify(cmdManager.registerCommand(Commands.Run_Linter, anything())).once(); - }); - - test('Run Linting method will lint all open files', async () => { - when(lintingEngine.lintOpenPythonFiles('manual')).thenResolve(('Hello' as unknown) as DiagnosticCollection); - - const result = await linterCommands.runLinting(); - - expect(result).to.be.equal('Hello'); - }); - - async function testEnableLintingWithCurrentState( - currentState: boolean, - selectedState: 'Enable' | 'Disable' | undefined, - ) { - when(manager.isLintingEnabled(anything())).thenResolve(currentState); - const expectedQuickPickOptions = { - matchOnDetail: true, - matchOnDescription: true, - placeHolder: `current: ${currentState ? 'Enable' : 'Disable'}`, - }; - when(shell.showQuickPick(anything(), anything())).thenReturn(Promise.resolve(selectedState)); - - await linterCommands.enableLintingAsync(); - - verify(shell.showQuickPick(anything(), anything())).once(); - const options = capture(shell.showQuickPick).last()[0]; - const quickPickOptions = capture(shell.showQuickPick).last()[1]; - expect(options).to.deep.equal(['Enable', 'Disable']); - expect(quickPickOptions).to.deep.equal(expectedQuickPickOptions); - - if (selectedState) { - verify(manager.enableLintingAsync(selectedState === 'Enable', anything())).once(); - } else { - verify(manager.enableLintingAsync(anything(), anything())).never(); - } - } - test("Enable linting should check if linting is enabled, and display current state of 'Enable' and select nothing", async () => { - await testEnableLintingWithCurrentState(true, undefined); - }); - - test("Enable linting should check if linting is enabled, and display current state of 'Enable' and select 'Enable'", async () => { - await testEnableLintingWithCurrentState(true, 'Enable'); - }); - - test("Enable linting should check if linting is enabled, and display current state of 'Enable' and select 'Disable'", async () => { - await testEnableLintingWithCurrentState(true, 'Disable'); - }); - - test("Enable linting should check if linting is enabled, and display current state of 'Disable' and select 'Enable'", async () => { - await testEnableLintingWithCurrentState(true, 'Enable'); - }); - - test("Enable linting should check if linting is enabled, and display current state of 'Disable' and select 'Disable'", async () => { - await testEnableLintingWithCurrentState(true, 'Disable'); - }); - - test('Set Linter should display a quickpick', async () => { - when(manager.getAllLinterInfos()).thenReturn([]); - when(manager.getActiveLinters(anything())).thenResolve([]); - when(shell.showQuickPick(anything(), anything())).thenResolve(); - const expectedQuickPickOptions = { - matchOnDetail: true, - matchOnDescription: true, - placeHolder: 'current: none', - }; - - await linterCommands.setLinterAsync(); - - verify(shell.showQuickPick(anything(), anything())); - const quickPickOptions = capture(shell.showQuickPick).last()[1]; - expect(quickPickOptions).to.deep.equal(expectedQuickPickOptions); - }); - - test('Set Linter should display a quickpick and currently active linter when only one is enabled', async () => { - const linterId = 'Hello World'; - const activeLinters: ILinterInfo[] = [({ id: linterId } as unknown) as ILinterInfo]; - when(manager.getAllLinterInfos()).thenReturn([]); - when(manager.getActiveLinters(anything())).thenResolve(activeLinters); - when(shell.showQuickPick(anything(), anything())).thenResolve(); - const expectedQuickPickOptions = { - matchOnDetail: true, - matchOnDescription: true, - placeHolder: `current: ${linterId}`, - }; - - await linterCommands.setLinterAsync(); - - verify(shell.showQuickPick(anything(), anything())).once(); - const quickPickOptions = capture(shell.showQuickPick).last()[1]; - expect(quickPickOptions).to.deep.equal(expectedQuickPickOptions); - }); - - test('Set Linter should display a quickpick and with message about multiple linters being enabled', async () => { - const activeLinters: ILinterInfo[] = ([{ id: 'linterId' }, { id: 'linterId2' }] as unknown) as ILinterInfo[]; - when(manager.getAllLinterInfos()).thenReturn([]); - when(manager.getActiveLinters(anything())).thenResolve(activeLinters); - when(shell.showQuickPick(anything(), anything())).thenResolve(); - const expectedQuickPickOptions = { - matchOnDetail: true, - matchOnDescription: true, - placeHolder: 'current: multiple selected', - }; - - await linterCommands.setLinterAsync(); - - verify(shell.showQuickPick(anything(), anything())); - const quickPickOptions = capture(shell.showQuickPick).last()[1]; - expect(quickPickOptions).to.deep.equal(expectedQuickPickOptions); - }); - - test('Selecting a linter should display warning message about multiple linters', async () => { - const linters: ILinterInfo[] = ([ - { id: '1' }, - { id: '2' }, - { id: '3', product: 'Three' }, - ] as unknown) as ILinterInfo[]; - const activeLinters: ILinterInfo[] = ([{ id: '1' }, { id: '3' }] as unknown) as ILinterInfo[]; - when(manager.getAllLinterInfos()).thenReturn(linters); - when(manager.getActiveLinters(anything())).thenResolve(activeLinters); - when(shell.showQuickPick(anything(), anything())).thenReturn(Promise.resolve('3')); - when(shell.showWarningMessage(anything(), 'Yes', 'No')).thenReturn(Promise.resolve('Yes')); - const expectedQuickPickOptions = { - matchOnDetail: true, - matchOnDescription: true, - placeHolder: 'current: multiple selected', - }; - - await linterCommands.setLinterAsync(); - - verify(shell.showQuickPick(anything(), anything())).once(); - verify(shell.showWarningMessage(anything(), 'Yes', 'No')).once(); - const quickPickOptions = capture(shell.showQuickPick).last()[1]; - expect(quickPickOptions).to.deep.equal(expectedQuickPickOptions); - verify(manager.setActiveLintersAsync(deepEqual([('Three' as unknown) as Product]), anything())).once(); - }); -}); diff --git a/src/test/linters/linterManager.unit.test.ts b/src/test/linters/linterManager.unit.test.ts deleted file mode 100644 index 42feb642ce8c..000000000000 --- a/src/test/linters/linterManager.unit.test.ts +++ /dev/null @@ -1,178 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; - -import * as assert from 'assert'; -import { expect } from 'chai'; -import { anything, instance, mock, verify, when } from 'ts-mockito'; -import { Uri } from 'vscode'; -import { ApplicationShell } from '../../client/common/application/applicationShell'; -import { CommandManager } from '../../client/common/application/commandManager'; -import { DocumentManager } from '../../client/common/application/documentManager'; -import { - IApplicationShell, - ICommandManager, - IDocumentManager, - IWorkspaceService, -} from '../../client/common/application/types'; -import { WorkspaceService } from '../../client/common/application/workspace'; -import { ConfigurationService } from '../../client/common/configuration/service'; -import { ProductNames } from '../../client/common/installer/productNames'; -import { ProductService } from '../../client/common/installer/productService'; -import { IConfigurationService, Product, ProductType } from '../../client/common/types'; -import { getNamesAndValues } from '../../client/common/utils/enum'; -import { ServiceContainer } from '../../client/ioc/container'; -import { LinterInfo } from '../../client/linters/linterInfo'; -import { LinterManager } from '../../client/linters/linterManager'; -import { LintingEngine } from '../../client/linters/lintingEngine'; -import { ILinterInfo, ILintingEngine } from '../../client/linters/types'; - -suite('Linting - Linter Manager', () => { - let linterManager: LinterManagerTest; - let shell: IApplicationShell; - let docManager: IDocumentManager; - let cmdManager: ICommandManager; - let lintingEngine: ILintingEngine; - let configService: IConfigurationService; - let workspaceService: IWorkspaceService; - class LinterManagerTest extends LinterManager { - // Override base class property to make it public. - public linters!: ILinterInfo[]; - } - setup(() => { - const svcContainer = mock(ServiceContainer); - shell = mock(ApplicationShell); - docManager = mock(DocumentManager); - cmdManager = mock(CommandManager); - lintingEngine = mock(LintingEngine); - configService = mock(ConfigurationService); - workspaceService = mock(WorkspaceService); - when(svcContainer.get(IApplicationShell)).thenReturn(instance(shell)); - when(svcContainer.get(IDocumentManager)).thenReturn(instance(docManager)); - when(svcContainer.get(ICommandManager)).thenReturn(instance(cmdManager)); - when(svcContainer.get(ILintingEngine)).thenReturn(instance(lintingEngine)); - when(svcContainer.get(IConfigurationService)).thenReturn(instance(configService)); - when(svcContainer.get(IWorkspaceService)).thenReturn(instance(workspaceService)); - linterManager = new LinterManagerTest(instance(configService)); - }); - - test('Get all linters will return a list of all linters', () => { - const linters = linterManager.getAllLinterInfos(); - - expect(linters).to.be.lengthOf(8); - - const productService = new ProductService(); - const linterProducts = getNamesAndValues(Product) - .filter((product) => productService.getProductType(product.value) === ProductType.Linter) - .map((item) => ProductNames.get(item.value)); - expect(linters.map((item) => item.id).sort()).to.be.deep.equal(linterProducts.sort()); - }); - - test('Get linter info for non-linter product should throw an exception', () => { - const productService = new ProductService(); - getNamesAndValues(Product).forEach((prod) => { - if (productService.getProductType(prod.value) === ProductType.Linter) { - const info = linterManager.getLinterInfo(prod.value); - expect(info.id).to.equal(ProductNames.get(prod.value)); - expect(info).not.to.be.equal(undefined, 'should not be unedfined'); - } else { - expect(() => linterManager.getLinterInfo(prod.value)).to.throw(); - } - }); - }); - test('Pylint configuration file watch', async () => { - const pylint = linterManager.getLinterInfo(Product.pylint); - assert.strictEqual(pylint.configFileNames.length, 2, 'Pylint configuration file count is incorrect.'); - assert.notStrictEqual( - pylint.configFileNames.indexOf('pylintrc'), - -1, - 'Pylint configuration files miss pylintrc.', - ); - assert.notStrictEqual( - pylint.configFileNames.indexOf('.pylintrc'), - -1, - 'Pylint configuration files miss .pylintrc.', - ); - }); - - [undefined, Uri.parse('something')].forEach((resource) => { - const testResourceSuffix = `(${resource ? 'with a resource' : 'without a resource'})`; - [true, false].forEach((enabled) => { - const testSuffix = `(${enabled ? 'enable' : 'disable'}) & ${testResourceSuffix}`; - test(`Enable linting should update config ${testSuffix}`, async () => { - when(configService.updateSetting('linting.enabled', enabled, resource)).thenResolve(); - - await linterManager.enableLintingAsync(enabled, resource); - - verify(configService.updateSetting('linting.enabled', enabled, resource)).once(); - }); - }); - test(`getActiveLinters will check if linter is enabled and in silent mode ${testResourceSuffix}`, async () => { - const linterInfo = mock(LinterInfo); - const instanceOfLinterInfo = instance(linterInfo); - linterManager.linters = [instanceOfLinterInfo]; - when(linterInfo.isEnabled(resource)).thenReturn(true); - - const linters = await linterManager.getActiveLinters(resource); - - verify(linterInfo.isEnabled(resource)).once(); - expect(linters[0]).to.deep.equal(instanceOfLinterInfo); - }); - - test(`setActiveLintersAsync with invalid products does nothing ${testResourceSuffix}`, async () => { - let getActiveLintersInvoked = false; - linterManager.getActiveLinters = async () => { - getActiveLintersInvoked = true; - return []; - }; - - await linterManager.setActiveLintersAsync([Product.pytest], resource); - - expect(getActiveLintersInvoked).to.be.equal(false, 'Should not be invoked'); - }); - test(`setActiveLintersAsync with single product will disable it then enable it ${testResourceSuffix}`, async () => { - const linterInfo = mock(LinterInfo); - const instanceOfLinterInfo = instance(linterInfo); - linterManager.linters = [instanceOfLinterInfo]; - when(linterInfo.product).thenReturn(Product.flake8); - when(linterInfo.enableAsync(false, resource)).thenResolve(); - linterManager.getActiveLinters = () => Promise.resolve([instanceOfLinterInfo]); - linterManager.enableLintingAsync = () => Promise.resolve(); - - await linterManager.setActiveLintersAsync([Product.flake8], resource); - - verify(linterInfo.enableAsync(false, resource)).atLeast(1); - verify(linterInfo.enableAsync(true, resource)).atLeast(1); - }); - test(`setActiveLintersAsync with single product will disable all existing then enable the necessary two ${testResourceSuffix}`, async () => { - const linters = new Map(); - const linterInstances = new Map(); - linterManager.linters = []; - [Product.flake8, Product.mypy, Product.prospector, Product.bandit, Product.pydocstyle].forEach( - (product) => { - const linterInfo = mock(LinterInfo); - const instanceOfLinterInfo = instance(linterInfo); - linterManager.linters.push(instanceOfLinterInfo); - linters.set(product, linterInfo); - linterInstances.set(product, instanceOfLinterInfo); - when(linterInfo.product).thenReturn(product); - when(linterInfo.enableAsync(anything(), resource)).thenResolve(); - }, - ); - - linterManager.getActiveLinters = () => Promise.resolve(Array.from(linterInstances.values())); - linterManager.enableLintingAsync = () => Promise.resolve(); - - const lintersToEnable = [Product.flake8, Product.mypy, Product.pydocstyle]; - await linterManager.setActiveLintersAsync([Product.flake8, Product.mypy, Product.pydocstyle], resource); - - linters.forEach((item, product) => { - verify(item.enableAsync(false, resource)).atLeast(1); - if (lintersToEnable.indexOf(product) >= 0) { - verify(item.enableAsync(true, resource)).atLeast(1); - } - }); - }); - }); -}); diff --git a/src/test/linters/mypy.unit.test.ts b/src/test/linters/mypy.unit.test.ts deleted file mode 100644 index b697a719a475..000000000000 --- a/src/test/linters/mypy.unit.test.ts +++ /dev/null @@ -1,99 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; - -import { expect } from 'chai'; -import { parseLine } from '../../client/linters/baseLinter'; -import { getRegex } from '../../client/linters/mypy'; -import { ILintMessage, LinterId } from '../../client/linters/types'; - -// This following is a real-world example. See gh=2380. - -const output = ` -provider.pyi:10: error: Incompatible types in assignment (expression has type "str", variable has type "int") -provider.pyi:11: error: Name 'not_declared_var' is not defined -provider.pyi:12:21: error: Expression has type "Any" -`; - -suite('Linting - MyPy', () => { - test('regex', async () => { - const lines = output.split('\n'); - const tests: [string, ILintMessage][] = [ - [ - lines[1], - { - code: undefined, - message: 'Incompatible types in assignment (expression has type "str", variable has type "int")', - column: 0, - line: 10, - type: 'error', - provider: 'mypy', - } as ILintMessage, - ], - [ - lines[2], - { - code: undefined, - message: "Name 'not_declared_var' is not defined", - column: 0, - line: 11, - type: 'error', - provider: 'mypy', - } as ILintMessage, - ], - [ - lines[3], - { - code: undefined, - message: 'Expression has type "Any"', - column: 20, - line: 12, - type: 'error', - provider: 'mypy', - } as ILintMessage, - ], - ]; - for (const [line, expected] of tests) { - const msg = parseLine(line, getRegex('provider.pyi'), LinterId.MyPy, 1); - - expect(msg).to.deep.equal(expected); - } - }); - test('regex excludes unexpected files', () => { - // mypy run against `foo/bar.py` returning errors for foo/__init__.py - const outputWithUnexpectedFile = `\ -foo/__init__.py:4:5: error: Statement is unreachable [unreachable] -foo/bar.py:2:14: error: Incompatible types in assignment (expression has type "str", variable has type "int") [assignment] -Found 2 errors in 2 files (checked 1 source file) -`; - - const lines = outputWithUnexpectedFile.split('\n'); - const tests: [string, ILintMessage | undefined][] = [ - [lines[0], undefined], - [ - lines[1], - { - code: undefined, - message: - 'Incompatible types in assignment (expression has type "str", variable has type "int") [assignment]', - column: 13, - line: 2, - type: 'error', - provider: 'mypy', - }, - ], - [lines[2], undefined], - ]; - for (const [line, expected] of tests) { - const msg = parseLine(line, getRegex('foo/bar.py'), LinterId.MyPy, 1); - - expect(msg).to.deep.equal(expected); - } - }); - test('getRegex escapes filename correctly', () => { - expect(getRegex('foo/bar.py')).to.eql( - String.raw`foo/bar\.py:(?\d+)(:(?\d+))?: (?\w+): (?.*)\r?(\n|$)`, - ); - }); -}); diff --git a/src/test/linters/prompts/flake8Prompt.unit.test.ts b/src/test/linters/prompts/flake8Prompt.unit.test.ts deleted file mode 100644 index 7bbe52ae6d96..000000000000 --- a/src/test/linters/prompts/flake8Prompt.unit.test.ts +++ /dev/null @@ -1,152 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -import { assert } from 'chai'; -import * as sinon from 'sinon'; -import * as TypeMoq from 'typemoq'; -import { IApplicationEnvironment } from '../../../client/common/application/types'; -import { IPersistentState } from '../../../client/common/types'; -import { Common, ToolsExtensions } from '../../../client/common/utils/localize'; -import * as commandApis from '../../../client/common/vscodeApis/commandApis'; -import * as windowsApis from '../../../client/common/vscodeApis/windowApis'; -import { IServiceContainer } from '../../../client/ioc/types'; -import * as promptCommons from '../../../client/linters/prompts/common'; -import { Flake8ExtensionPrompt, FLAKE8_EXTENSION } from '../../../client/linters/prompts/flake8Prompt'; -import { IToolsExtensionPrompt } from '../../../client/linters/prompts/types'; - -suite('Flake8 Extension prompt tests', () => { - let isExtensionEnabledStub: sinon.SinonStub; - let isExtensionDisabledStub: sinon.SinonStub; - let doNotShowPromptStateStub: sinon.SinonStub; - let inToolsExtensionsExperimentStub: sinon.SinonStub; - let showInformationMessageStub: sinon.SinonStub; - let executeCommandStub: sinon.SinonStub; - let serviceContainer: TypeMoq.IMock; - let doNotState: TypeMoq.IMock>; - let appEnv: TypeMoq.IMock; - let prompt: IToolsExtensionPrompt; - - setup(() => { - isExtensionEnabledStub = sinon.stub(promptCommons, 'isExtensionEnabled'); - isExtensionDisabledStub = sinon.stub(promptCommons, 'isExtensionDisabled'); - doNotShowPromptStateStub = sinon.stub(promptCommons, 'doNotShowPromptState'); - inToolsExtensionsExperimentStub = sinon.stub(promptCommons, 'inToolsExtensionsExperiment'); - showInformationMessageStub = sinon.stub(windowsApis, 'showInformationMessage'); - executeCommandStub = sinon.stub(commandApis, 'executeCommand'); - - appEnv = TypeMoq.Mock.ofType(); - serviceContainer = TypeMoq.Mock.ofType(); - serviceContainer - .setup((s) => s.get(IApplicationEnvironment)) - .returns(() => appEnv.object); - - doNotState = TypeMoq.Mock.ofType>(); - prompt = new Flake8ExtensionPrompt(serviceContainer.object); - }); - - teardown(() => { - sinon.restore(); - }); - - test('Extension already installed and enabled', async () => { - isExtensionEnabledStub.returns(true); - - assert.isTrue(await prompt.showPrompt()); - }); - - test('Extension already installed, but disabled', async () => { - isExtensionEnabledStub.returns(false); - isExtensionDisabledStub.returns(true); - - assert.isTrue(await prompt.showPrompt()); - }); - - test('Test do not show again persistent state', async () => { - isExtensionEnabledStub.returns(false); - isExtensionDisabledStub.returns(false); - - doNotState.setup((d) => d.value).returns(() => true); - doNotShowPromptStateStub.returns(doNotState.object); - - assert.isFalse(await prompt.showPrompt()); - }); - - test('User not in experiment', async () => { - isExtensionEnabledStub.returns(false); - isExtensionDisabledStub.returns(false); - - doNotState.setup((d) => d.value).returns(() => false); - doNotShowPromptStateStub.returns(doNotState.object); - inToolsExtensionsExperimentStub.resolves(false); - - assert.isFalse(await prompt.showPrompt()); - }); - - test('User selected: install extension (insiders)', async () => { - isExtensionEnabledStub.returns(false); - isExtensionDisabledStub.returns(false); - - doNotState.setup((d) => d.value).returns(() => false); - doNotShowPromptStateStub.returns(doNotState.object); - inToolsExtensionsExperimentStub.resolves(true); - - appEnv.setup((a) => a.extensionChannel).returns(() => 'insiders'); - executeCommandStub.resolves(undefined); - - showInformationMessageStub.resolves(ToolsExtensions.installFlake8Extension); - assert.isTrue(await prompt.showPrompt()); - - executeCommandStub.calledOnceWith('workbench.extensions.installExtension', FLAKE8_EXTENSION, { - installPreReleaseVersion: true, - }); - }); - - test('User selected: install extension (stable)', async () => { - isExtensionEnabledStub.returns(false); - isExtensionDisabledStub.returns(false); - - doNotState.setup((d) => d.value).returns(() => false); - doNotShowPromptStateStub.returns(doNotState.object); - inToolsExtensionsExperimentStub.resolves(true); - - appEnv.setup((a) => a.extensionChannel).returns(() => 'stable'); - executeCommandStub.resolves(undefined); - - showInformationMessageStub.resolves(ToolsExtensions.installFlake8Extension); - assert.isTrue(await prompt.showPrompt()); - - executeCommandStub.calledOnceWith('workbench.extensions.installExtension', FLAKE8_EXTENSION, { - installPreReleaseVersion: false, - }); - }); - - test('User selected: do not show again', async () => { - isExtensionEnabledStub.returns(false); - isExtensionDisabledStub.returns(false); - - doNotState.setup((d) => d.value).returns(() => false); - doNotShowPromptStateStub.returns(doNotState.object); - inToolsExtensionsExperimentStub.resolves(true); - - doNotState - .setup((d) => d.updateValue(true)) - .returns(() => Promise.resolve()) - .verifiable(TypeMoq.Times.once()); - showInformationMessageStub.resolves(Common.doNotShowAgain); - assert.isFalse(await prompt.showPrompt()); - - doNotState.verifyAll(); - }); - - test('User selected: close', async () => { - isExtensionEnabledStub.returns(false); - isExtensionDisabledStub.returns(false); - - doNotState.setup((d) => d.value).returns(() => false); - doNotShowPromptStateStub.returns(doNotState.object); - inToolsExtensionsExperimentStub.resolves(true); - - showInformationMessageStub.resolves(undefined); - assert.isFalse(await prompt.showPrompt()); - }); -}); diff --git a/src/test/linters/prompts/pylintPrompt.unit.test.ts b/src/test/linters/prompts/pylintPrompt.unit.test.ts deleted file mode 100644 index 65b579f258af..000000000000 --- a/src/test/linters/prompts/pylintPrompt.unit.test.ts +++ /dev/null @@ -1,142 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -import { assert } from 'chai'; -import * as sinon from 'sinon'; -import * as TypeMoq from 'typemoq'; -import { IApplicationEnvironment } from '../../../client/common/application/types'; -import { IPersistentState } from '../../../client/common/types'; -import { Common, ToolsExtensions } from '../../../client/common/utils/localize'; -import * as commandApis from '../../../client/common/vscodeApis/commandApis'; -import * as windowsApis from '../../../client/common/vscodeApis/windowApis'; -import { IServiceContainer } from '../../../client/ioc/types'; -import * as promptCommons from '../../../client/linters/prompts/common'; -import { PylintExtensionPrompt, PYLINT_EXTENSION } from '../../../client/linters/prompts/pylintPrompt'; -import { IToolsExtensionPrompt } from '../../../client/linters/prompts/types'; - -suite('Pylint Extension prompt tests', () => { - let isExtensionEnabledStub: sinon.SinonStub; - let isExtensionDisabledStub: sinon.SinonStub; - let doNotShowPromptStateStub: sinon.SinonStub; - let inToolsExtensionsExperimentStub: sinon.SinonStub; - let showInformationMessageStub: sinon.SinonStub; - let executeCommandStub: sinon.SinonStub; - let serviceContainer: TypeMoq.IMock; - let doNotState: TypeMoq.IMock>; - let appEnv: TypeMoq.IMock; - let prompt: IToolsExtensionPrompt; - - setup(() => { - isExtensionEnabledStub = sinon.stub(promptCommons, 'isExtensionEnabled'); - isExtensionDisabledStub = sinon.stub(promptCommons, 'isExtensionDisabled'); - doNotShowPromptStateStub = sinon.stub(promptCommons, 'doNotShowPromptState'); - inToolsExtensionsExperimentStub = sinon.stub(promptCommons, 'inToolsExtensionsExperiment'); - showInformationMessageStub = sinon.stub(windowsApis, 'showInformationMessage'); - executeCommandStub = sinon.stub(commandApis, 'executeCommand'); - - appEnv = TypeMoq.Mock.ofType(); - serviceContainer = TypeMoq.Mock.ofType(); - serviceContainer - .setup((s) => s.get(IApplicationEnvironment)) - .returns(() => appEnv.object); - - doNotState = TypeMoq.Mock.ofType>(); - prompt = new PylintExtensionPrompt(serviceContainer.object); - }); - - teardown(() => { - sinon.restore(); - }); - - test('Extension already installed and enabled', async () => { - isExtensionEnabledStub.returns(true); - - assert.isTrue(await prompt.showPrompt()); - }); - - test('Extension already installed, but disabled', async () => { - isExtensionEnabledStub.returns(false); - isExtensionDisabledStub.returns(true); - - assert.isTrue(await prompt.showPrompt()); - }); - - test('User not in experiment', async () => { - isExtensionEnabledStub.returns(false); - isExtensionDisabledStub.returns(false); - - doNotState.setup((d) => d.value).returns(() => false); - doNotShowPromptStateStub.returns(doNotState.object); - inToolsExtensionsExperimentStub.resolves(false); - - assert.isFalse(await prompt.showPrompt()); - }); - - test('User selected: install extension (insiders)', async () => { - isExtensionEnabledStub.returns(false); - isExtensionDisabledStub.returns(false); - - doNotState.setup((d) => d.value).returns(() => false); - doNotShowPromptStateStub.returns(doNotState.object); - inToolsExtensionsExperimentStub.resolves(true); - - appEnv.setup((a) => a.extensionChannel).returns(() => 'insiders'); - executeCommandStub.resolves(undefined); - - showInformationMessageStub.resolves(ToolsExtensions.installPylintExtension); - assert.isTrue(await prompt.showPrompt()); - - executeCommandStub.calledOnceWith('workbench.extensions.installExtension', PYLINT_EXTENSION, { - installPreReleaseVersion: true, - }); - }); - - test('User selected: install extension (stable)', async () => { - isExtensionEnabledStub.returns(false); - isExtensionDisabledStub.returns(false); - - doNotState.setup((d) => d.value).returns(() => false); - doNotShowPromptStateStub.returns(doNotState.object); - inToolsExtensionsExperimentStub.resolves(true); - - appEnv.setup((a) => a.extensionChannel).returns(() => 'stable'); - executeCommandStub.resolves(undefined); - - showInformationMessageStub.resolves(ToolsExtensions.installPylintExtension); - assert.isTrue(await prompt.showPrompt()); - - executeCommandStub.calledOnceWith('workbench.extensions.installExtension', PYLINT_EXTENSION, { - installPreReleaseVersion: false, - }); - }); - - test('User selected: do not show again', async () => { - isExtensionEnabledStub.returns(false); - isExtensionDisabledStub.returns(false); - - doNotState.setup((d) => d.value).returns(() => false); - doNotShowPromptStateStub.returns(doNotState.object); - inToolsExtensionsExperimentStub.resolves(true); - - doNotState - .setup((d) => d.updateValue(true)) - .returns(() => Promise.resolve()) - .verifiable(TypeMoq.Times.once()); - showInformationMessageStub.resolves(Common.doNotShowAgain); - assert.isFalse(await prompt.showPrompt()); - - doNotState.verifyAll(); - }); - - test('User selected: close', async () => { - isExtensionEnabledStub.returns(false); - isExtensionDisabledStub.returns(false); - - doNotState.setup((d) => d.value).returns(() => false); - doNotShowPromptStateStub.returns(doNotState.object); - inToolsExtensionsExperimentStub.resolves(true); - - showInformationMessageStub.resolves(undefined); - assert.isFalse(await prompt.showPrompt()); - }); -}); diff --git a/src/test/linters/pylint.test.ts b/src/test/linters/pylint.test.ts deleted file mode 100644 index e1cec249c662..000000000000 --- a/src/test/linters/pylint.test.ts +++ /dev/null @@ -1,163 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -import { expect } from 'chai'; -import { Container } from 'inversify'; -import * as os from 'os'; -import * as path from 'path'; -import * as TypeMoq from 'typemoq'; -import { - CancellationTokenSource, - DiagnosticSeverity, - TextDocument, - Uri, - WorkspaceConfiguration, - WorkspaceFolder, -} from 'vscode'; -import { LanguageServerType } from '../../client/activation/types'; -import { IWorkspaceService } from '../../client/common/application/types'; -import { IFileSystem, IPlatformService } from '../../client/common/platform/types'; -import { IPythonToolExecutionService } from '../../client/common/process/types'; -import { IConfigurationService, IExtensions, IInstaller, IPythonSettings } from '../../client/common/types'; -import { - IInterpreterAutoSelectionService, - IInterpreterAutoSelectionProxyService, -} from '../../client/interpreter/autoSelection/types'; -import { ServiceContainer } from '../../client/ioc/container'; -import { ServiceManager } from '../../client/ioc/serviceManager'; -import { LinterManager } from '../../client/linters/linterManager'; -import { Pylint } from '../../client/linters/pylint'; -import { ILinterManager } from '../../client/linters/types'; -import { MockLintingSettings } from '../mockClasses'; -import { MockAutoSelectionService } from '../mocks/autoSelector'; - -suite('Linting - Pylint', () => { - let fileSystem: TypeMoq.IMock; - let platformService: TypeMoq.IMock; - let workspace: TypeMoq.IMock; - let execService: TypeMoq.IMock; - let config: TypeMoq.IMock; - let workspaceConfig: TypeMoq.IMock; - let pythonSettings: TypeMoq.IMock; - let serviceContainer: ServiceContainer; - let extensionsService: TypeMoq.IMock; - - setup(() => { - fileSystem = TypeMoq.Mock.ofType(); - fileSystem - .setup((x) => x.arePathsSame(TypeMoq.It.isAnyString(), TypeMoq.It.isAnyString())) - .returns((a, b) => a === b); - - platformService = TypeMoq.Mock.ofType(); - platformService.setup((x) => x.isWindows).returns(() => false); - - extensionsService = TypeMoq.Mock.ofType(); - extensionsService.setup((e) => e.getExtension(TypeMoq.It.isAny())).returns(() => undefined); - - workspace = TypeMoq.Mock.ofType(); - execService = TypeMoq.Mock.ofType(); - - const cont = new Container(); - const serviceManager = new ServiceManager(cont); - serviceContainer = new ServiceContainer(cont); - - serviceManager.addSingletonInstance(IFileSystem, fileSystem.object); - serviceManager.addSingletonInstance(IWorkspaceService, workspace.object); - serviceManager.addSingletonInstance( - IPythonToolExecutionService, - execService.object, - ); - serviceManager.addSingletonInstance(IPlatformService, platformService.object); - serviceManager.addSingleton( - IInterpreterAutoSelectionService, - MockAutoSelectionService, - ); - serviceManager.addSingleton( - IInterpreterAutoSelectionProxyService, - MockAutoSelectionService, - ); - serviceManager.addSingletonInstance(IExtensions, extensionsService.object); - - pythonSettings = TypeMoq.Mock.ofType(); - pythonSettings.setup((p) => p.languageServer).returns(() => LanguageServerType.Jedi); - - config = TypeMoq.Mock.ofType(); - config.setup((c) => c.getSettings()).returns(() => pythonSettings.object); - - workspaceConfig = TypeMoq.Mock.ofType(); - workspace.setup((w) => w.getConfiguration('python')).returns(() => workspaceConfig.object); - - serviceManager.addSingletonInstance(IConfigurationService, config.object); - const linterManager = new LinterManager(config.object); - serviceManager.addSingletonInstance(ILinterManager, linterManager); - const installer = TypeMoq.Mock.ofType(); - serviceManager.addSingletonInstance(IInstaller, installer.object); - }); - - test('Negative column numbers should be treated 0', async () => { - const fileFolder = '/user/a/b/c'; - const pylinter = new Pylint(serviceContainer, { showPrompt: () => Promise.resolve(false) }); - - const document = TypeMoq.Mock.ofType(); - document.setup((x) => x.uri).returns(() => Uri.file(path.join(fileFolder, 'test.py'))); - - const wsf = TypeMoq.Mock.ofType(); - wsf.setup((x) => x.uri).returns(() => Uri.file(fileFolder)); - - workspace.setup((x) => x.getWorkspaceFolder(TypeMoq.It.isAny())).returns(() => wsf.object); - - const linterOutput = [ - '[', - ' {', - ' "type": "convention",', - ' "module": "test",', - ' "obj": "",', - ' "line": 1,', - ' "column": 1,', - ` "path": "${fileFolder}/test.py",`, - ' "symbol": "missing-module-docstring",', - ' "message": "Missing module docstring",', - ' "message-id": "C0114",', - ' "endLine": null,', - ' "endColumn": null', - ' },', - ' {', - ' "type": "error",', - ' "module": "test",', - ' "obj": "",', - ' "line": 3,', - ' "column": -1,', - ` "path": "${fileFolder}/test.py",`, - ' "symbol": "too-many-format-args",', - ' "message": "Too many arguments for format string",', - ' "message-id": "E1305"', - ' }', - ']', - ].join(os.EOL); - execService - .setup((x) => x.execForLinter(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny())) - .returns(() => Promise.resolve({ stdout: linterOutput, stderr: '' })); - - const lintSettings = new MockLintingSettings(); - lintSettings.maxNumberOfProblems = 1000; - lintSettings.pylintPath = 'pyLint'; - lintSettings.pylintEnabled = true; - lintSettings.pylintCategorySeverity = { - convention: DiagnosticSeverity.Hint, - error: DiagnosticSeverity.Error, - fatal: DiagnosticSeverity.Error, - refactor: DiagnosticSeverity.Hint, - warning: DiagnosticSeverity.Warning, - }; - - const settings = TypeMoq.Mock.ofType(); - settings.setup((x) => x.linting).returns(() => lintSettings); - settings.setup((x) => x.languageServer).returns(() => LanguageServerType.Jedi); - config.setup((x) => x.getSettings(TypeMoq.It.isAny())).returns(() => settings.object); - - const messages = await pylinter.lint(document.object, new CancellationTokenSource().token); - expect(messages).to.be.lengthOf(2); - expect(messages[0].column).to.be.equal(1); - expect(messages[1].column).to.be.equal(0); - }); -}); diff --git a/src/test/linters/pylint.unit.test.ts b/src/test/linters/pylint.unit.test.ts deleted file mode 100644 index ee6954e870a5..000000000000 --- a/src/test/linters/pylint.unit.test.ts +++ /dev/null @@ -1,289 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -import { assert } from 'chai'; -import * as sinon from 'sinon'; -import { mock } from 'ts-mockito'; -import * as TypeMoq from 'typemoq'; -import * as vscode from 'vscode'; -import { IWorkspaceService } from '../../client/common/application/types'; -import { IFileSystem, IPlatformService } from '../../client/common/platform/types'; -import { IConfigurationService, IExtensions, IPythonSettings } from '../../client/common/types'; -import { IServiceContainer } from '../../client/ioc/types'; -import { IToolsExtensionPrompt } from '../../client/linters/prompts/types'; -import { Pylint } from '../../client/linters/pylint'; -import { ILinterInfo, ILinterManager, ILintMessage, LinterId, LintMessageSeverity } from '../../client/linters/types'; - -suite('Pylint - Function runLinter()', () => { - let fileSystem: TypeMoq.IMock; - let serviceContainer: TypeMoq.IMock; - let workspaceService: TypeMoq.IMock; - let configService: TypeMoq.IMock; - let manager: TypeMoq.IMock; - let _info: TypeMoq.IMock; - let platformService: TypeMoq.IMock; - let extensionsService: TypeMoq.IMock; - let run: sinon.SinonStub; - let parseMessagesSeverity: sinon.SinonStub; - let extensionPrompt: TypeMoq.IMock; - const doc = { - uri: vscode.Uri.file('path/to/doc'), - }; - const args = [doc.uri.fsPath]; - class PylintTest extends Pylint { - // eslint-disable-next-line class-methods-use-this - public async run( - _args: string[], - _document: vscode.TextDocument, - _cancellation: vscode.CancellationToken, - _regEx: string, - ): Promise { - return []; - } - - // eslint-disable-next-line class-methods-use-this - public parseMessagesSeverity(_error: string, _categorySeverity: unknown): LintMessageSeverity { - return ('Severity' as unknown) as LintMessageSeverity; - } - - // eslint-disable-next-line class-methods-use-this - public get info(): ILinterInfo { - return _info.object; - } - - public async runLinter( - document: vscode.TextDocument, - cancellation: vscode.CancellationToken, - ): Promise { - return super.runLinter(document, cancellation); - } - - // eslint-disable-next-line class-methods-use-this - public getWorkingDirectoryPath(_document: vscode.TextDocument): string { - return 'path/to/workspaceRoot'; - } - - public async parseMessages( - output: string, - _document: vscode.TextDocument, - _token: vscode.CancellationToken, - ): Promise { - return super.parseMessages(output, _document, _token, ''); - } - } - - setup(() => { - platformService = TypeMoq.Mock.ofType(); - _info = TypeMoq.Mock.ofType(); - serviceContainer = TypeMoq.Mock.ofType(); - workspaceService = TypeMoq.Mock.ofType(); - configService = TypeMoq.Mock.ofType(); - extensionsService = TypeMoq.Mock.ofType(); - manager = TypeMoq.Mock.ofType(); - serviceContainer.setup((c) => c.get(TypeMoq.It.isValue(ILinterManager))).returns(() => manager.object); - serviceContainer - .setup((c) => c.get(TypeMoq.It.isValue(IConfigurationService))) - .returns(() => configService.object); - serviceContainer - .setup((c) => c.get(TypeMoq.It.isValue(IWorkspaceService))) - .returns(() => workspaceService.object); - serviceContainer.setup((c) => c.get(TypeMoq.It.isValue(IFileSystem))).returns(() => fileSystem.object); - serviceContainer - .setup((c) => c.get(TypeMoq.It.isValue(IPlatformService))) - .returns(() => platformService.object); - serviceContainer.setup((c) => c.get(TypeMoq.It.isValue(IExtensions))).returns(() => extensionsService.object); - fileSystem = TypeMoq.Mock.ofType(); - fileSystem - .setup((x) => x.arePathsSame(TypeMoq.It.isAnyString(), TypeMoq.It.isAnyString())) - .returns((a, b) => a === b); - manager.setup((m) => m.getLinterInfo(TypeMoq.It.isAny())).returns(() => (undefined as unknown) as ILinterInfo); - _info.setup((x) => x.id).returns(() => LinterId.PyLint); - extensionPrompt = TypeMoq.Mock.ofType(); - extensionPrompt.setup((e) => e.showPrompt()).returns(() => Promise.resolve(false)); - }); - - teardown(() => { - sinon.restore(); - }); - - test('Test pylint with default settings.', async () => { - const settings = { - linting: { - pylintEnabled: true, - }, - }; - configService.setup((c) => c.getSettings(doc.uri)).returns(() => settings as IPythonSettings); - _info.setup((info) => info.linterArgs(doc.uri)).returns(() => []); - run = sinon.stub(PylintTest.prototype, 'run'); - run.callsFake(() => Promise.resolve([])); - parseMessagesSeverity = sinon.stub(PylintTest.prototype, 'parseMessagesSeverity'); - parseMessagesSeverity.callsFake(() => 'Severity'); - const pylint = new PylintTest(serviceContainer.object, extensionPrompt.object); - await pylint.runLinter(doc as vscode.TextDocument, mock(vscode.CancellationTokenSource).token); - assert.deepEqual(run.args[0][0], args); - assert.ok(parseMessagesSeverity.notCalled); - assert.ok(run.calledOnce); - }); - - test('Message returned by runLinter() is as expected', async () => { - const message = [ - { - type: 'messageType', - }, - ]; - const expectedResult = [ - { - type: 'messageType', - severity: 'LintMessageSeverity', - }, - ]; - const settings = { - linting: { - pylintEnabled: true, - }, - }; - configService.setup((c) => c.getSettings(doc.uri)).returns(() => settings as IPythonSettings); - _info.setup((info) => info.linterArgs(doc.uri)).returns(() => []); - run = sinon.stub(PylintTest.prototype, 'run'); - run.callsFake(() => Promise.resolve(message)); - parseMessagesSeverity = sinon.stub(PylintTest.prototype, 'parseMessagesSeverity'); - parseMessagesSeverity.callsFake(() => 'LintMessageSeverity'); - const pylint = new PylintTest(serviceContainer.object, extensionPrompt.object); - const result = await pylint.runLinter(doc as vscode.TextDocument, mock(vscode.CancellationTokenSource).token); - assert.deepEqual(result, (expectedResult as unknown) as ILintMessage[]); - assert.ok(parseMessagesSeverity.calledOnce); - assert.ok(run.calledOnce); - }); - - test('Parse json output', async () => { - // If 'endLine' and 'endColumn' are missing in JSON output, - // both should be set to 'undefined' - const jsonOutput = `[ - { - "type": "error", - "module": "file", - "obj": "Foo.meth3", - "line": 26, - "column": 15, - "path": "file.py", - "symbol": "no-member", - "message": "Instance of 'Foo' has no 'blop' member", - "message-id": "E1101" - } -]`; - const expectedMessages: ILintMessage[] = [ - { - code: 'no-member', - message: "Instance of 'Foo' has no 'blop' member", - column: 15, - line: 26, - type: 'error', - provider: LinterId.PyLint, - endLine: undefined, - endColumn: undefined, - }, - ]; - const settings = { - linting: { - pylintEnabled: true, - }, - }; - configService.setup((c) => c.getSettings(doc.uri)).returns(() => settings as IPythonSettings); - const pylint = new PylintTest(serviceContainer.object, extensionPrompt.object); - const result = await pylint.parseMessages( - jsonOutput, - doc as vscode.TextDocument, - mock(vscode.CancellationTokenSource).token, - ); - assert.deepEqual(result, expectedMessages); - }); - - test('Parse json output with endLine', async () => { - const jsonOutput = `[ - { - "type": "error", - "module": "file", - "obj": "Foo.meth3", - "line": 26, - "column": 15, - "endLine": 26, - "endColumn": 24, - "path": "file.py", - "symbol": "no-member", - "message": "Instance of 'Foo' has no 'blop' member", - "message-id": "E1101" - } -]`; - const expectedMessages: ILintMessage[] = [ - { - code: 'no-member', - message: "Instance of 'Foo' has no 'blop' member", - column: 15, - line: 26, - type: 'error', - provider: LinterId.PyLint, - endLine: 26, - endColumn: 24, - }, - ]; - const settings = { - linting: { - pylintEnabled: true, - }, - }; - configService.setup((c) => c.getSettings(doc.uri)).returns(() => settings as IPythonSettings); - const pylint = new PylintTest(serviceContainer.object, extensionPrompt.object); - const result = await pylint.parseMessages( - jsonOutput, - doc as vscode.TextDocument, - mock(vscode.CancellationTokenSource).token, - ); - assert.deepEqual(result, expectedMessages); - }); - - test('Parse json output with unknown endLine', async () => { - // If 'endLine' and 'endColumn' are present in JSON output - // but 'null', 'endLine' should be set to 'undefined'. - // 'endColumn' defaults to 0. - const jsonOutput = `[ - { - "type": "error", - "module": "file", - "obj": "Foo.meth3", - "line": 26, - "column": 15, - "endLine": null, - "endColumn": null, - "path": "file.py", - "symbol": "no-member", - "message": "Instance of 'Foo' has no 'blop' member", - "message-id": "E1101" - } -]`; - const expectedMessages: ILintMessage[] = [ - { - code: 'no-member', - message: "Instance of 'Foo' has no 'blop' member", - column: 15, - line: 26, - type: 'error', - provider: LinterId.PyLint, - endLine: undefined, - endColumn: undefined, - }, - ]; - const settings = { - linting: { - pylintEnabled: true, - }, - }; - configService.setup((c) => c.getSettings(doc.uri)).returns(() => settings as IPythonSettings); - const pylint = new PylintTest(serviceContainer.object, extensionPrompt.object); - const result = await pylint.parseMessages( - jsonOutput, - doc as vscode.TextDocument, - mock(vscode.CancellationTokenSource).token, - ); - assert.deepEqual(result, expectedMessages); - }); -}); diff --git a/src/test/linters/serviceRegistry.unit.test.ts b/src/test/linters/serviceRegistry.unit.test.ts deleted file mode 100644 index a27c244af344..000000000000 --- a/src/test/linters/serviceRegistry.unit.test.ts +++ /dev/null @@ -1,31 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; - -import { instance, mock, verify } from 'ts-mockito'; -import { IExtensionActivationService } from '../../client/activation/types'; -import { ServiceManager } from '../../client/ioc/serviceManager'; -import { IServiceManager } from '../../client/ioc/types'; -import { LinterManager } from '../../client/linters/linterManager'; -import { LintingEngine } from '../../client/linters/lintingEngine'; -import { registerTypes } from '../../client/linters/serviceRegistry'; -import { ILinterManager, ILintingEngine } from '../../client/linters/types'; -import { LinterProvider } from '../../client/providers/linterProvider'; - -suite('Linters Service Registry', () => { - let serviceManager: IServiceManager; - - setup(() => { - serviceManager = mock(ServiceManager); - }); - - test('Ensure services are registered', async () => { - registerTypes(instance(serviceManager)); - verify(serviceManager.addSingleton(ILintingEngine, LintingEngine)).once(); - verify(serviceManager.addSingleton(ILinterManager, LinterManager)).once(); - verify( - serviceManager.addSingleton(IExtensionActivationService, LinterProvider), - ).once(); - }); -}); diff --git a/src/test/mockClasses.ts b/src/test/mockClasses.ts index c962c4d67ca4..e2de7e649b87 100644 --- a/src/test/mockClasses.ts +++ b/src/test/mockClasses.ts @@ -1,12 +1,5 @@ import * as vscode from 'vscode'; import * as util from 'util'; -import { - Flake8CategorySeverity, - ILintingSettings, - IMypyCategorySeverity, - IPycodestyleCategorySeverity, - IPylintCategorySeverity, -} from '../client/common/types'; export class MockOutputChannel implements vscode.LogOutputChannel { public name: string; @@ -79,39 +72,3 @@ export class MockStatusBarItem implements vscode.StatusBarItem { public dispose(): void {} } - -export class MockLintingSettings implements ILintingSettings { - public enabled!: boolean; - public cwd?: string; - public ignorePatterns!: string[]; - public prospectorEnabled!: boolean; - public prospectorArgs!: string[]; - public pylintEnabled!: boolean; - public pylintArgs!: string[]; - public pycodestyleEnabled!: boolean; - public pycodestyleArgs!: string[]; - public pylamaEnabled!: boolean; - public pylamaArgs!: string[]; - public flake8Enabled!: boolean; - public flake8Args!: string[]; - public pydocstyleEnabled!: boolean; - public pydocstyleArgs!: string[]; - public lintOnSave!: boolean; - public maxNumberOfProblems!: number; - public pylintCategorySeverity!: IPylintCategorySeverity; - public pycodestyleCategorySeverity!: IPycodestyleCategorySeverity; - public flake8CategorySeverity!: Flake8CategorySeverity; - public mypyCategorySeverity!: IMypyCategorySeverity; - public prospectorPath!: string; - public pylintPath!: string; - public pycodestylePath!: string; - public pylamaPath!: string; - public flake8Path!: string; - public pydocstylePath!: string; - public mypyEnabled!: boolean; - public mypyArgs!: string[]; - public mypyPath!: string; - public banditEnabled!: boolean; - public banditArgs!: string[]; - public banditPath!: string; -} diff --git a/src/test/mocks/extension.ts b/src/test/mocks/extension.ts new file mode 100644 index 000000000000..61d70eb5ee9e --- /dev/null +++ b/src/test/mocks/extension.ts @@ -0,0 +1,16 @@ +import { injectable } from 'inversify'; +import { Extension, ExtensionKind, Uri } from 'vscode'; + +@injectable() +export class MockExtension implements Extension { + id!: string; + extensionUri!: Uri; + extensionPath!: string; + isActive!: boolean; + packageJSON: any; + extensionKind!: ExtensionKind; + exports!: T; + activate(): Thenable { + throw new Error('Method not implemented.'); + } +} diff --git a/src/test/mocks/extensions.ts b/src/test/mocks/extensions.ts new file mode 100644 index 000000000000..efe9b6b8ca31 --- /dev/null +++ b/src/test/mocks/extensions.ts @@ -0,0 +1,23 @@ +import { injectable } from 'inversify'; +import { IExtensions } from '../../client/common/types'; +import { Extension, Event } from 'vscode'; +import { MockExtension } from './extension'; + +@injectable() +export class MockExtensions implements IExtensions { + extensionIdsToFind: unknown[] = []; + all: readonly Extension[] = []; + onDidChange: Event = () => { + throw new Error('Method not implemented'); + }; + getExtension(extensionId: string): Extension | undefined; + getExtension(extensionId: string): Extension | undefined; + getExtension(extensionId: unknown): import('vscode').Extension | undefined { + if (this.extensionIdsToFind.includes(extensionId)) { + return new MockExtension(); + } + } + determineExtensionFromCallStack(): Promise<{ extensionId: string; displayName: string }> { + throw new Error('Method not implemented.'); + } +} diff --git a/src/test/mocks/helper.ts b/src/test/mocks/helper.ts new file mode 100644 index 000000000000..d61bf728a25c --- /dev/null +++ b/src/test/mocks/helper.ts @@ -0,0 +1,28 @@ +/* eslint-disable @typescript-eslint/no-unused-vars */ +/* eslint-disable @typescript-eslint/no-explicit-any */ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +import * as TypeMoq from 'typemoq'; +import { Readable } from 'stream'; +// eslint-disable-next-line import/no-unresolved +import * as common from 'typemoq/Common/_all'; + +export class FakeReadableStream extends Readable { + _read(_size: unknown): void | null { + // custom reading logic here + this.push(null); // end the stream + } +} + +export function createTypeMoq( + targetCtor?: common.CtorWithArgs, + behavior?: TypeMoq.MockBehavior, + shouldOverrideTarget?: boolean, + ...targetCtorArgs: any[] +): TypeMoq.IMock { + // Use typemoqs for those things that are resolved as promises. mockito doesn't allow nesting of mocks. ES6 Proxy class + // is the problem. We still need to make it thenable though. See this issue: https://github.com/florinn/typemoq/issues/67 + const result = TypeMoq.Mock.ofType(targetCtor, behavior, shouldOverrideTarget, ...targetCtorArgs); + result.setup((x: any) => x.then).returns(() => undefined); + return result; +} diff --git a/src/test/mocks/mockChildProcess.ts b/src/test/mocks/mockChildProcess.ts new file mode 100644 index 000000000000..e26ea1c7aa45 --- /dev/null +++ b/src/test/mocks/mockChildProcess.ts @@ -0,0 +1,243 @@ +/* eslint-disable @typescript-eslint/no-unused-vars */ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import { Serializable, SendHandle, MessageOptions } from 'child_process'; +import { EventEmitter } from 'node:events'; +import { Writable, Readable, Pipe } from 'stream'; +import { FakeReadableStream } from './helper'; + +export class MockChildProcess extends EventEmitter { + constructor(spawnfile: string, spawnargs: string[]) { + super(); + this.spawnfile = spawnfile; + this.spawnargs = spawnargs; + this.stdin = new Writable(); + this.stdout = new FakeReadableStream(); + this.stderr = new FakeReadableStream(); + this.channel = null; + this.stdio = [this.stdin, this.stdout, this.stdout, this.stderr, null]; + this.killed = false; + this.connected = false; + this.exitCode = null; + this.signalCode = null; + this.eventMap = new Map(); + } + + stdin: Writable | null; + + stdout: Readable | null; + + stderr: Readable | null; + + eventMap: Map; + + readonly channel?: Pipe | null | undefined; + + readonly stdio: [ + Writable | null, + // stdin + Readable | null, + // stdout + Readable | null, + // stderr + Readable | Writable | null | undefined, + // extra + Readable | Writable | null | undefined, // extra + ]; + + readonly killed: boolean; + + readonly pid?: number | undefined; + + readonly connected: boolean; + + readonly exitCode: number | null; + + readonly signalCode: NodeJS.Signals | null; + + readonly spawnargs: string[]; + + readonly spawnfile: string; + + signal?: NodeJS.Signals | number; + + send(message: Serializable, callback?: (error: Error | null) => void): boolean; + + send(message: Serializable, sendHandle?: SendHandle, callback?: (error: Error | null) => void): boolean; + + send( + message: Serializable, + sendHandle?: SendHandle, + options?: MessageOptions, + callback?: (error: Error | null) => void, + ): boolean; + + send( + message: Serializable, + _sendHandleOrCallback?: SendHandle | ((error: Error | null) => void), + _optionsOrCallback?: MessageOptions | ((error: Error | null) => void), + _callback?: (error: Error | null) => void, + ): boolean { + // Implementation of the send method + // For example, you might want to emit a 'message' event + this.stdout?.push(message.toString()); + return true; + } + + // eslint-disable-next-line class-methods-use-this + disconnect(): void { + /* noop */ + } + + // eslint-disable-next-line class-methods-use-this + unref(): void { + /* noop */ + } + + // eslint-disable-next-line class-methods-use-this + ref(): void { + /* noop */ + } + + addListener(event: 'close', listener: (code: number | null, signal: NodeJS.Signals | null) => void): this; + + addListener(event: 'disconnect', listener: () => void): this; + + addListener(event: 'error', listener: (err: Error) => void): this; + + addListener(event: 'exit', listener: (code: number | null, signal: NodeJS.Signals | null) => void): this; + + addListener(event: 'message', listener: (message: Serializable, sendHandle: SendHandle) => void): this; + + addListener(event: 'spawn', listener: () => void): this; + + addListener(event: string, listener: (...args: any[]) => void): this { + if (this.eventMap.has(event)) { + this.eventMap.get(event).push(listener); + } else { + this.eventMap.set(event, [listener]); + } + return this; + } + + emit(event: 'close', code: number | null, signal: NodeJS.Signals | null): boolean; + + emit(event: 'disconnect'): boolean; + + emit(event: 'error', err: Error): boolean; + + emit(event: 'exit', code: number | null, signal: NodeJS.Signals | null): boolean; + + emit(event: 'message', message: Serializable, sendHandle: SendHandle): boolean; + + emit(event: 'spawn', listener: () => void): boolean; + + emit(event: string | symbol, ...args: unknown[]): boolean { + if (this.eventMap.has(event.toString())) { + this.eventMap.get(event.toString()).forEach((listener: (...arg0: unknown[]) => void) => { + const argsArray: unknown[] = Array.isArray(args) ? args : [args]; + listener(...argsArray); + }); + } + return true; + } + + on(event: 'close', listener: (code: number | null, signal: NodeJS.Signals | null) => void): this; + + on(event: 'disconnect', listener: () => void): this; + + on(event: 'error', listener: (err: Error) => void): this; + + on(event: 'exit', listener: (code: number | null, signal: NodeJS.Signals | null) => void): this; + + on(event: 'message', listener: (message: Serializable, sendHandle: SendHandle) => void): this; + + on(event: 'spawn', listener: () => void): this; + + on(event: string, listener: (...args: any[]) => void): this { + if (this.eventMap.has(event)) { + this.eventMap.get(event).push(listener); + } else { + this.eventMap.set(event, [listener]); + } + return this; + } + + once(event: 'close', listener: (code: number | null, signal: NodeJS.Signals | null) => void): this; + + once(event: 'disconnect', listener: () => void): this; + + once(event: 'error', listener: (err: Error) => void): this; + + once(event: 'exit', listener: (code: number | null, signal: NodeJS.Signals | null) => void): this; + + once(event: 'message', listener: (message: Serializable, sendHandle: SendHandle) => void): this; + + once(event: 'spawn', listener: () => void): this; + + once(event: string, listener: (...args: any[]) => void): this { + if (this.eventMap.has(event)) { + this.eventMap.get(event).push(listener); + } else { + this.eventMap.set(event, [listener]); + } + return this; + } + + prependListener(event: 'close', listener: (code: number | null, signal: NodeJS.Signals | null) => void): this; + + prependListener(event: 'disconnect', listener: () => void): this; + + prependListener(event: 'error', listener: (err: Error) => void): this; + + prependListener(event: 'exit', listener: (code: number | null, signal: NodeJS.Signals | null) => void): this; + + prependListener(event: 'message', listener: (message: Serializable, sendHandle: SendHandle) => void): this; + + prependListener(event: 'spawn', listener: () => void): this; + + prependListener(event: string, listener: (...args: any[]) => void): this { + if (this.eventMap.has(event)) { + this.eventMap.get(event).push(listener); + } else { + this.eventMap.set(event, [listener]); + } + return this; + } + + prependOnceListener(event: 'close', listener: (code: number | null, signal: NodeJS.Signals | null) => void): this; + + prependOnceListener(event: 'disconnect', listener: () => void): this; + + prependOnceListener(event: 'error', listener: (err: Error) => void): this; + + prependOnceListener(event: 'exit', listener: (code: number | null, signal: NodeJS.Signals | null) => void): this; + + prependOnceListener(event: 'message', listener: (message: Serializable, sendHandle: SendHandle) => void): this; + + prependOnceListener(event: 'spawn', listener: () => void): this; + + prependOnceListener(event: string, listener: (...args: any[]) => void): this { + if (this.eventMap.has(event)) { + this.eventMap.get(event).push(listener); + } else { + this.eventMap.set(event, [listener]); + } + return this; + } + + trigger(event: string): Array { + if (this.eventMap.has(event)) { + return this.eventMap.get(event); + } + return []; + } + + kill(_signal?: NodeJS.Signals | number): boolean { + this.stdout?.destroy(); + return true; + } + + dispose(): void { + this.stdout?.destroy(); + } +} diff --git a/src/test/mocks/mockDocument.ts b/src/test/mocks/mockDocument.ts index 811c591420bd..a9cd39985311 100644 --- a/src/test/mocks/mockDocument.ts +++ b/src/test/mocks/mockDocument.ts @@ -84,6 +84,7 @@ export class MockDocument implements TextDocument { this._onSave = onSave; this._language = language ?? this._language; } + encoding: string = 'utf8'; public setContent(contents: string): void { this._contents = contents; diff --git a/src/test/mocks/process.ts b/src/test/mocks/process.ts index 3c1b339aff44..d290cae5bf71 100644 --- a/src/test/mocks/process.ts +++ b/src/test/mocks/process.ts @@ -4,9 +4,9 @@ 'use strict'; import { injectable } from 'inversify'; -import * as TypeMoq from 'typemoq'; import { ICurrentProcess } from '../../client/common/types'; import { EnvironmentVariables } from '../../client/common/variables/types'; +import { createTypeMoq } from './helper'; @injectable() export class MockProcess implements ICurrentProcess { @@ -24,12 +24,12 @@ export class MockProcess implements ICurrentProcess { // eslint-disable-next-line class-methods-use-this public get stdout(): NodeJS.WriteStream { - return TypeMoq.Mock.ofType().object; + return createTypeMoq().object; } // eslint-disable-next-line class-methods-use-this public get stdin(): NodeJS.ReadStream { - return TypeMoq.Mock.ofType().object; + return createTypeMoq().object; } // eslint-disable-next-line class-methods-use-this diff --git a/src/test/mocks/vsc/arrays.ts b/src/test/mocks/vsc/arrays.ts index c06cefa7c27f..ad2020c57110 100644 --- a/src/test/mocks/vsc/arrays.ts +++ b/src/test/mocks/vsc/arrays.ts @@ -186,9 +186,6 @@ export function sortedDiff(before: T[], after: T[], compare: (a: T, b: T) => /** * Takes two *sorted* arrays and computes their delta (removed, added elements). * Finishes in `Math.min(before.length, after.length)` steps. - * @param before - * @param after - * @param compare */ export function delta(before: T[], after: T[], compare: (a: T, b: T) => number): { removed: T[]; added: T[] } { const splices = sortedDiff(before, after, compare); diff --git a/src/test/mocks/vsc/charCode.ts b/src/test/mocks/vsc/charCode.ts index d0fac68fbc57..fe450d491ef1 100644 --- a/src/test/mocks/vsc/charCode.ts +++ b/src/test/mocks/vsc/charCode.ts @@ -346,8 +346,8 @@ export const enum CharCode { LINE_SEPARATOR_2028 = 8232, // http://www.fileformat.info/info/unicode/category/Sk/list.htm - U_CIRCUMFLEX = 0x005e, // U+005E CIRCUMFLEX - U_GRAVE_ACCENT = 0x0060, // U+0060 GRAVE ACCENT + U_CIRCUMFLEX = Caret, // U+005E CIRCUMFLEX + U_GRAVE_ACCENT = BackTick, // U+0060 GRAVE ACCENT U_DIAERESIS = 0x00a8, // U+00A8 DIAERESIS U_MACRON = 0x00af, // U+00AF MACRON U_ACUTE_ACCENT = 0x00b4, // U+00B4 ACUTE ACCENT diff --git a/src/test/mocks/vsc/extHostedTypes.ts b/src/test/mocks/vsc/extHostedTypes.ts index 4921b24629d1..c2c1188c3449 100644 --- a/src/test/mocks/vsc/extHostedTypes.ts +++ b/src/test/mocks/vsc/extHostedTypes.ts @@ -775,7 +775,7 @@ export class SnippetString { this._tabstop = nested._tabstop; defaultValue = nested.value; } else if (typeof defaultValue === 'string') { - defaultValue = defaultValue.replace(/\$|}/g, '\\$&'); + defaultValue = defaultValue.replace(/\$|}/g, '\\$&'); // CodeQL [SM02383] don't escape backslashes here (by design) } this.value += '${'; @@ -2000,12 +2000,31 @@ export enum TreeItemCollapsibleState { Expanded = 2, } +/** + * Represents an icon in the UI. This is either an uri, separate uris for the light- and dark-themes, + * or a {@link ThemeIcon theme icon}. + */ +export type IconPath = + | vscUri.URI + | { + /** + * The icon path for the light theme. + */ + light: vscUri.URI; + /** + * The icon path for the dark theme. + */ + dark: vscUri.URI; + } + | ThemeIcon; + export class TreeItem { - label?: string; + label?: string | vscode.TreeItemLabel; + id?: string; resourceUri?: vscUri.URI; - iconPath?: string | vscUri.URI | { light: string | vscUri.URI; dark: string | vscUri.URI }; + iconPath?: string | IconPath; command?: vscode.Command; diff --git a/src/test/mocks/vsc/index.ts b/src/test/mocks/vsc/index.ts index 89f4ab1a2d07..152beb64cdf4 100644 --- a/src/test/mocks/vsc/index.ts +++ b/src/test/mocks/vsc/index.ts @@ -6,6 +6,7 @@ import { EventEmitter as NodeEventEmitter } from 'events'; import * as vscode from 'vscode'; + // export * from './range'; // export * from './position'; // export * from './selection'; @@ -53,11 +54,30 @@ export enum QuickPickItemKind { } export class Disposable { - constructor(private callOnDispose: () => void) {} + static from(...disposables: { dispose(): () => void }[]): Disposable { + return new Disposable(() => { + if (disposables) { + for (const disposable of disposables) { + if (disposable && typeof disposable.dispose === 'function') { + disposable.dispose(); + } + } + + disposables = []; + } + }); + } - public dispose(): void { - if (this.callOnDispose) { - this.callOnDispose(); + private _callOnDispose: (() => void) | undefined; + + constructor(callOnDispose: () => void) { + this._callOnDispose = callOnDispose; + } + + dispose(): void { + if (typeof this._callOnDispose === 'function') { + this._callOnDispose(); + this._callOnDispose = undefined; } } } @@ -284,7 +304,7 @@ export class MarkdownString { // escape markdown syntax tokens: http://daringfireball.net/projects/markdown/syntax#backslash this.value += (this.supportThemeIcons ? escapeCodicons(value) : value) .replace(/[\\`*_{}[\]()#+\-.!]/g, '\\$&') - .replace(/\n/, '\n\n'); + .replace(/\n/g, '\n\n'); return this; } @@ -349,6 +369,8 @@ export class CodeActionKind { public static readonly SourceFixAll: CodeActionKind = new CodeActionKind('source.fix.all'); + public static readonly Notebook: CodeActionKind = new CodeActionKind('notebook'); + private constructor(private _value: string) {} public append(parts: string): CodeActionKind { @@ -443,3 +465,132 @@ export enum LogLevel { */ Error = 5, } + +export class TestMessage { + /** + * Human-readable message text to display. + */ + message: string | MarkdownString; + + /** + * Expected test output. If given with {@link TestMessage.actualOutput actualOutput }, a diff view will be shown. + */ + expectedOutput?: string; + + /** + * Actual test output. If given with {@link TestMessage.expectedOutput expectedOutput }, a diff view will be shown. + */ + actualOutput?: string; + + /** + * Associated file location. + */ + location?: vscode.Location; + + /** + * Creates a new TestMessage that will present as a diff in the editor. + * @param message Message to display to the user. + * @param expected Expected output. + * @param actual Actual output. + */ + static diff(message: string | MarkdownString, expected: string, actual: string): TestMessage { + const testMessage = new TestMessage(message); + testMessage.expectedOutput = expected; + testMessage.actualOutput = actual; + return testMessage; + } + + /** + * Creates a new TestMessage instance. + * @param message The message to show to the user. + */ + constructor(message: string | MarkdownString) { + this.message = message; + } +} + +export interface TestItemCollection extends Iterable<[string, vscode.TestItem]> { + /** + * Gets the number of items in the collection. + */ + readonly size: number; + + /** + * Replaces the items stored by the collection. + * @param items Items to store. + */ + replace(items: readonly vscode.TestItem[]): void; + + /** + * Iterate over each entry in this collection. + * + * @param callback Function to execute for each entry. + * @param thisArg The `this` context used when invoking the handler function. + */ + forEach(callback: (item: vscode.TestItem, collection: TestItemCollection) => unknown, thisArg?: unknown): void; + + /** + * Adds the test item to the children. If an item with the same ID already + * exists, it'll be replaced. + * @param item Item to add. + */ + add(item: vscode.TestItem): void; + + /** + * Removes a single test item from the collection. + * @param itemId Item ID to delete. + */ + delete(itemId: string): void; + + /** + * Efficiently gets a test item by ID, if it exists, in the children. + * @param itemId Item ID to get. + * @returns The found item or undefined if it does not exist. + */ + get(itemId: string): vscode.TestItem | undefined; +} + +/** + * Represents a location inside a resource, such as a line + * inside a text file. + */ +export class Location { + /** + * The resource identifier of this location. + */ + uri: vscode.Uri; + + /** + * The document range of this location. + */ + range: vscode.Range; + + /** + * Creates a new location object. + * + * @param uri The resource identifier. + * @param rangeOrPosition The range or position. Positions will be converted to an empty range. + */ + constructor(uri: vscode.Uri, rangeOrPosition: vscode.Range) { + this.uri = uri; + this.range = rangeOrPosition; + } +} + +/** + * The kind of executions that {@link TestRunProfile TestRunProfiles} control. + */ +export enum TestRunProfileKind { + /** + * The `Run` test profile kind. + */ + Run = 1, + /** + * The `Debug` test profile kind. + */ + Debug = 2, + /** + * The `Coverage` test profile kind. + */ + Coverage = 3, +} diff --git a/src/test/multiRootTest.ts b/src/test/multiRootTest.ts index 9a6d7cd1b904..c8c63b6dabe5 100644 --- a/src/test/multiRootTest.ts +++ b/src/test/multiRootTest.ts @@ -2,7 +2,7 @@ import * as path from 'path'; import { runTests } from '@vscode/test-electron'; import { EXTENSION_ROOT_DIR_FOR_TESTS } from './constants'; import { initializeLogger } from './testLogger'; -import { getVersion } from './utils/vscode'; +import { getChannel } from './utils/vscode'; const workspacePath = path.join(__dirname, '..', '..', 'src', 'testMultiRootWkspc', 'multi.code-workspace'); process.env.IS_CI_SERVER_TEST_DEBUGGER = ''; @@ -17,7 +17,7 @@ function start() { extensionDevelopmentPath: EXTENSION_ROOT_DIR_FOR_TESTS, extensionTestsPath: path.join(EXTENSION_ROOT_DIR_FOR_TESTS, 'out', 'test', 'index'), launchArgs: [workspacePath], - version: getVersion(), + version: getChannel(), extensionTestsEnv: { ...process.env, UITEST_DISABLE_INSIDERS: '1' }, }).catch((ex) => { console.error('End Multiroot tests (with errors)', ex); diff --git a/src/test/performance/load.perf.test.ts b/src/test/performance/load.perf.test.ts index 3fe9c6caa37d..0067803af8f0 100644 --- a/src/test/performance/load.perf.test.ts +++ b/src/test/performance/load.perf.test.ts @@ -4,7 +4,7 @@ 'use strict'; import { expect } from 'chai'; -import * as fs from 'fs-extra'; +import * as fs from '../../client/common/platform/fs-paths'; import { EOL } from 'os'; import * as path from 'path'; import { commands, extensions } from 'vscode'; diff --git a/src/test/performanceTest.ts b/src/test/performanceTest.ts index d4ac6bf262d0..2398f745c27a 100644 --- a/src/test/performanceTest.ts +++ b/src/test/performanceTest.ts @@ -17,7 +17,7 @@ process.env.VSC_PYTHON_PERF_TEST = '1'; import { spawn } from 'child_process'; import * as download from 'download'; -import * as fs from 'fs-extra'; +import * as fs from '../client/common/platform/fs-paths'; import * as path from 'path'; import * as bent from 'bent'; import { LanguageServerType } from '../client/activation/types'; @@ -123,7 +123,7 @@ class TestRunner { private async getReleaseVersion(): Promise { const url = `https://marketplace.visualstudio.com/items?itemName=${PVSC_EXTENSION_ID}`; - const request = bent('string', 'GET', 200); + const request = bent.default('string', 'GET', 200); const content: string = await request(url); const re = NamedRegexp('"version"S?:S?"(:\\d{4}\\.\\d{1,2}\\.\\d{1,2})"', 'g'); @@ -143,7 +143,7 @@ class TestRunner { return destination; } - await download(url, path.dirname(destination), { filename: path.basename(destination) }); + await download.default(url, path.dirname(destination), { filename: path.basename(destination) }); return destination; } } diff --git a/src/test/providers/codeActionProvider/main.unit.test.ts b/src/test/providers/codeActionProvider/main.unit.test.ts index 501c3c7eca2b..55644d80ae54 100644 --- a/src/test/providers/codeActionProvider/main.unit.test.ts +++ b/src/test/providers/codeActionProvider/main.unit.test.ts @@ -8,7 +8,6 @@ import rewiremock from 'rewiremock'; import * as typemoq from 'typemoq'; import { CodeActionKind, CodeActionProvider, CodeActionProviderMetadata, DocumentSelector } from 'vscode'; import { IDisposableRegistry } from '../../../client/common/types'; -import { IServiceContainer } from '../../../client/ioc/types'; import { LaunchJsonCodeActionProvider } from '../../../client/providers/codeActionProvider/launchJsonCodeActionProvider'; import { CodeActionProviderService } from '../../../client/providers/codeActionProvider/main'; @@ -38,10 +37,7 @@ suite('Code Action Provider service', async () => { }; rewiremock.enable(); rewiremock('vscode').with(vscodeMock); - const quickFixService = new CodeActionProviderService( - typemoq.Mock.ofType().object, - typemoq.Mock.ofType().object, - ); + const quickFixService = new CodeActionProviderService(typemoq.Mock.ofType().object); await quickFixService.activate(); diff --git a/src/test/providers/prompt/installFormatterPrompt.unit.test.ts b/src/test/providers/prompt/installFormatterPrompt.unit.test.ts deleted file mode 100644 index fbd3a72d8cef..000000000000 --- a/src/test/providers/prompt/installFormatterPrompt.unit.test.ts +++ /dev/null @@ -1,335 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -import { assert } from 'chai'; -import * as sinon from 'sinon'; -import * as TypeMoq from 'typemoq'; -import { WorkspaceConfiguration } from 'vscode'; -import { IPersistentState } from '../../../client/common/types'; -import * as workspaceApis from '../../../client/common/vscodeApis/workspaceApis'; -import * as windowApis from '../../../client/common/vscodeApis/windowApis'; -import * as extensionsApi from '../../../client/common/vscodeApis/extensionsApi'; -import { IServiceContainer } from '../../../client/ioc/types'; -import { InstallFormatterPrompt } from '../../../client/providers/prompts/installFormatterPrompt'; -import * as promptUtils from '../../../client/providers/prompts/promptUtils'; -import { AUTOPEP8_EXTENSION, BLACK_EXTENSION, IInstallFormatterPrompt } from '../../../client/providers/prompts/types'; -import { Common, ToolsExtensions } from '../../../client/common/utils/localize'; - -suite('Formatter Extension prompt tests', () => { - let inFormatterExtensionExperimentStub: sinon.SinonStub; - let doNotShowPromptStateStub: sinon.SinonStub; - let prompt: IInstallFormatterPrompt; - let serviceContainer: TypeMoq.IMock; - let persistState: TypeMoq.IMock>; - let getConfigurationStub: sinon.SinonStub; - let isExtensionEnabledStub: sinon.SinonStub; - let pythonConfig: TypeMoq.IMock; - let editorConfig: TypeMoq.IMock; - let showInformationMessageStub: sinon.SinonStub; - let installFormatterExtensionStub: sinon.SinonStub; - let updateDefaultFormatterStub: sinon.SinonStub; - - setup(() => { - inFormatterExtensionExperimentStub = sinon.stub(promptUtils, 'inFormatterExtensionExperiment'); - inFormatterExtensionExperimentStub.returns(true); - - doNotShowPromptStateStub = sinon.stub(promptUtils, 'doNotShowPromptState'); - persistState = TypeMoq.Mock.ofType>(); - doNotShowPromptStateStub.returns(persistState.object); - - getConfigurationStub = sinon.stub(workspaceApis, 'getConfiguration'); - pythonConfig = TypeMoq.Mock.ofType(); - editorConfig = TypeMoq.Mock.ofType(); - getConfigurationStub.callsFake((section: string) => { - if (section === 'python') { - return pythonConfig.object; - } - return editorConfig.object; - }); - isExtensionEnabledStub = sinon.stub(extensionsApi, 'isExtensionEnabled'); - showInformationMessageStub = sinon.stub(windowApis, 'showInformationMessage'); - installFormatterExtensionStub = sinon.stub(promptUtils, 'installFormatterExtension'); - updateDefaultFormatterStub = sinon.stub(promptUtils, 'updateDefaultFormatter'); - - serviceContainer = TypeMoq.Mock.ofType(); - - prompt = new InstallFormatterPrompt(serviceContainer.object); - }); - - teardown(() => { - sinon.restore(); - }); - - test('Not in experiment', async () => { - inFormatterExtensionExperimentStub.returns(false); - - await prompt.showInstallFormatterPrompt(); - assert.isTrue(doNotShowPromptStateStub.notCalled); - }); - - test('Do not show was set', async () => { - persistState.setup((p) => p.value).returns(() => true); - - await prompt.showInstallFormatterPrompt(); - assert.isTrue(getConfigurationStub.notCalled); - }); - - test('Formatting provider is set to none', async () => { - persistState.setup((p) => p.value).returns(() => false); - pythonConfig.setup((p) => p.get('formatting.provider', TypeMoq.It.isAny())).returns(() => 'none'); - - await prompt.showInstallFormatterPrompt(); - assert.isTrue(isExtensionEnabledStub.notCalled); - }); - - test('Formatting provider is set to yapf', async () => { - persistState.setup((p) => p.value).returns(() => false); - pythonConfig.setup((p) => p.get('formatting.provider', TypeMoq.It.isAny())).returns(() => 'yapf'); - - await prompt.showInstallFormatterPrompt(); - assert.isTrue(isExtensionEnabledStub.notCalled); - }); - - test('Formatting provider is set to autopep8, and autopep8 extension is set as default formatter', async () => { - persistState.setup((p) => p.value).returns(() => false); - pythonConfig.setup((p) => p.get('formatting.provider', TypeMoq.It.isAny())).returns(() => 'autopep8'); - editorConfig.setup((p) => p.get('defaultFormatter', TypeMoq.It.isAny())).returns(() => AUTOPEP8_EXTENSION); - - await prompt.showInstallFormatterPrompt(); - assert.isTrue(isExtensionEnabledStub.notCalled); - }); - - test('Formatting provider is set to black, and black extension is set as default formatter', async () => { - persistState.setup((p) => p.value).returns(() => false); - pythonConfig.setup((p) => p.get('formatting.provider', TypeMoq.It.isAny())).returns(() => 'black'); - editorConfig.setup((p) => p.get('defaultFormatter', TypeMoq.It.isAny())).returns(() => BLACK_EXTENSION); - - await prompt.showInstallFormatterPrompt(); - assert.isTrue(isExtensionEnabledStub.notCalled); - }); - - test('Prompt: user selects do not show', async () => { - persistState.setup((p) => p.value).returns(() => false); - persistState - .setup((p) => p.updateValue(true)) - .returns(() => Promise.resolve()) - .verifiable(TypeMoq.Times.atLeastOnce()); - pythonConfig.setup((p) => p.get('formatting.provider', TypeMoq.It.isAny())).returns(() => 'autopep8'); - editorConfig.setup((p) => p.get('defaultFormatter', TypeMoq.It.isAny())).returns(() => ''); - isExtensionEnabledStub.returns(undefined); - - showInformationMessageStub.resolves(Common.doNotShowAgain); - - await prompt.showInstallFormatterPrompt(); - assert.isTrue( - showInformationMessageStub.calledWith( - ToolsExtensions.installAutopep8FormatterPrompt, - 'Black', - 'Autopep8', - Common.doNotShowAgain, - ), - 'showInformationMessage should be called', - ); - persistState.verifyAll(); - }); - - test('Prompt (autopep8): user selects Autopep8', async () => { - persistState.setup((p) => p.value).returns(() => false); - pythonConfig.setup((p) => p.get('formatting.provider', TypeMoq.It.isAny())).returns(() => 'autopep8'); - editorConfig.setup((p) => p.get('defaultFormatter', TypeMoq.It.isAny())).returns(() => ''); - isExtensionEnabledStub.returns(undefined); - - showInformationMessageStub.resolves('Autopep8'); - - await prompt.showInstallFormatterPrompt(); - assert.isTrue( - showInformationMessageStub.calledWith( - ToolsExtensions.installAutopep8FormatterPrompt, - 'Black', - 'Autopep8', - Common.doNotShowAgain, - ), - 'showInformationMessage should be called', - ); - assert.isTrue( - installFormatterExtensionStub.calledWith(AUTOPEP8_EXTENSION, undefined), - 'installFormatterExtension should be called', - ); - }); - - test('Prompt (autopep8): user selects Black', async () => { - persistState.setup((p) => p.value).returns(() => false); - pythonConfig.setup((p) => p.get('formatting.provider', TypeMoq.It.isAny())).returns(() => 'autopep8'); - editorConfig.setup((p) => p.get('defaultFormatter', TypeMoq.It.isAny())).returns(() => ''); - isExtensionEnabledStub.returns(undefined); - - showInformationMessageStub.resolves('Black'); - - await prompt.showInstallFormatterPrompt(); - assert.isTrue( - showInformationMessageStub.calledWith( - ToolsExtensions.installAutopep8FormatterPrompt, - 'Black', - 'Autopep8', - Common.doNotShowAgain, - ), - 'showInformationMessage should be called', - ); - assert.isTrue( - installFormatterExtensionStub.calledWith(BLACK_EXTENSION, undefined), - 'installFormatterExtension should be called', - ); - }); - - test('Prompt (black): user selects Autopep8', async () => { - persistState.setup((p) => p.value).returns(() => false); - pythonConfig.setup((p) => p.get('formatting.provider', TypeMoq.It.isAny())).returns(() => 'black'); - editorConfig.setup((p) => p.get('defaultFormatter', TypeMoq.It.isAny())).returns(() => ''); - isExtensionEnabledStub.returns(undefined); - - showInformationMessageStub.resolves('Autopep8'); - - await prompt.showInstallFormatterPrompt(); - assert.isTrue( - showInformationMessageStub.calledWith( - ToolsExtensions.installBlackFormatterPrompt, - 'Black', - 'Autopep8', - Common.doNotShowAgain, - ), - 'showInformationMessage should be called', - ); - assert.isTrue( - installFormatterExtensionStub.calledWith(AUTOPEP8_EXTENSION, undefined), - 'installFormatterExtension should be called', - ); - }); - - test('Prompt (black): user selects Black', async () => { - persistState.setup((p) => p.value).returns(() => false); - pythonConfig.setup((p) => p.get('formatting.provider', TypeMoq.It.isAny())).returns(() => 'black'); - editorConfig.setup((p) => p.get('defaultFormatter', TypeMoq.It.isAny())).returns(() => ''); - isExtensionEnabledStub.returns(undefined); - - showInformationMessageStub.resolves('Black'); - - await prompt.showInstallFormatterPrompt(); - assert.isTrue( - showInformationMessageStub.calledWith( - ToolsExtensions.installBlackFormatterPrompt, - 'Black', - 'Autopep8', - Common.doNotShowAgain, - ), - 'showInformationMessage should be called', - ); - assert.isTrue( - installFormatterExtensionStub.calledWith(BLACK_EXTENSION, undefined), - 'installFormatterExtension should be called', - ); - }); - - test('Prompt: Black and Autopep8 installed user selects Black as default', async () => { - persistState.setup((p) => p.value).returns(() => false); - pythonConfig.setup((p) => p.get('formatting.provider', TypeMoq.It.isAny())).returns(() => 'black'); - editorConfig.setup((p) => p.get('defaultFormatter', TypeMoq.It.isAny())).returns(() => ''); - isExtensionEnabledStub.returns({}); - - showInformationMessageStub.resolves('Black'); - - await prompt.showInstallFormatterPrompt(); - assert.isTrue( - showInformationMessageStub.calledWith( - ToolsExtensions.selectMultipleFormattersPrompt, - 'Black', - 'Autopep8', - Common.doNotShowAgain, - ), - 'showInformationMessage should be called', - ); - assert.isTrue( - updateDefaultFormatterStub.calledWith(BLACK_EXTENSION, undefined), - 'updateDefaultFormatter should be called', - ); - }); - - test('Prompt: Black and Autopep8 installed user selects Autopep8 as default', async () => { - persistState.setup((p) => p.value).returns(() => false); - pythonConfig.setup((p) => p.get('formatting.provider', TypeMoq.It.isAny())).returns(() => 'autopep8'); - editorConfig.setup((p) => p.get('defaultFormatter', TypeMoq.It.isAny())).returns(() => ''); - isExtensionEnabledStub.returns({}); - - showInformationMessageStub.resolves('Autopep8'); - - await prompt.showInstallFormatterPrompt(); - assert.isTrue( - showInformationMessageStub.calledWith( - ToolsExtensions.selectMultipleFormattersPrompt, - 'Black', - 'Autopep8', - Common.doNotShowAgain, - ), - 'showInformationMessage should be called', - ); - assert.isTrue( - updateDefaultFormatterStub.calledWith(AUTOPEP8_EXTENSION, undefined), - 'updateDefaultFormatter should be called', - ); - }); - - test('Prompt: Black installed user selects Black as default', async () => { - persistState.setup((p) => p.value).returns(() => false); - pythonConfig.setup((p) => p.get('formatting.provider', TypeMoq.It.isAny())).returns(() => 'black'); - editorConfig.setup((p) => p.get('defaultFormatter', TypeMoq.It.isAny())).returns(() => ''); - isExtensionEnabledStub.callsFake((extensionId) => { - if (extensionId === BLACK_EXTENSION) { - return {}; - } - return undefined; - }); - - showInformationMessageStub.resolves('Black'); - - await prompt.showInstallFormatterPrompt(); - assert.isTrue( - showInformationMessageStub.calledWith( - ToolsExtensions.selectBlackFormatterPrompt, - Common.bannerLabelYes, - Common.doNotShowAgain, - ), - 'showInformationMessage should be called', - ); - assert.isTrue( - updateDefaultFormatterStub.calledWith(BLACK_EXTENSION, undefined), - 'updateDefaultFormatter should be called', - ); - }); - - test('Prompt: Autopep8 installed user selects Autopep8 as default', async () => { - persistState.setup((p) => p.value).returns(() => false); - pythonConfig.setup((p) => p.get('formatting.provider', TypeMoq.It.isAny())).returns(() => 'autopep8'); - editorConfig.setup((p) => p.get('defaultFormatter', TypeMoq.It.isAny())).returns(() => ''); - isExtensionEnabledStub.callsFake((extensionId) => { - if (extensionId === AUTOPEP8_EXTENSION) { - return {}; - } - return undefined; - }); - - showInformationMessageStub.resolves('Autopep8'); - - await prompt.showInstallFormatterPrompt(); - assert.isTrue( - showInformationMessageStub.calledWith( - ToolsExtensions.selectAutopep8FormatterPrompt, - Common.bannerLabelYes, - Common.doNotShowAgain, - ), - 'showInformationMessage should be called', - ); - assert.isTrue( - updateDefaultFormatterStub.calledWith(AUTOPEP8_EXTENSION, undefined), - 'updateDefaultFormatter should be called', - ); - }); -}); diff --git a/src/test/providers/repl.unit.test.ts b/src/test/providers/repl.unit.test.ts index 87811e243bfd..72adfa95a4a0 100644 --- a/src/test/providers/repl.unit.test.ts +++ b/src/test/providers/repl.unit.test.ts @@ -36,7 +36,7 @@ suite('REPL Provider', () => { serviceContainer.setup((c) => c.get(ICommandManager)).returns(() => commandManager.object); serviceContainer.setup((c) => c.get(IWorkspaceService)).returns(() => workspace.object); serviceContainer - .setup((c) => c.get(ICodeExecutionService, TypeMoq.It.isValue('repl'))) + .setup((s) => s.get(TypeMoq.It.isValue(ICodeExecutionService), TypeMoq.It.isValue('standard'))) .returns(() => codeExecutionService.object); serviceContainer.setup((c) => c.get(IDocumentManager)).returns(() => documentManager.object); serviceContainer.setup((c) => c.get(IActiveResourceService)).returns(() => activeResourceService.object); @@ -80,6 +80,7 @@ suite('REPL Provider', () => { const resource = Uri.parse('a'); const disposable = TypeMoq.Mock.ofType(); let commandHandler: undefined | (() => Promise); + commandManager .setup((c) => c.registerCommand(TypeMoq.It.isValue(Commands.Start_REPL), TypeMoq.It.isAny(), TypeMoq.It.isAny()), @@ -98,7 +99,7 @@ suite('REPL Provider', () => { await commandHandler!.call(replProvider); serviceContainer.verify( - (c) => c.get(TypeMoq.It.isValue(ICodeExecutionService), TypeMoq.It.isValue('repl')), + (c) => c.get(TypeMoq.It.isValue(ICodeExecutionService), TypeMoq.It.isValue('standard')), TypeMoq.Times.once(), ); codeExecutionService.verify((c) => c.initializeRepl(TypeMoq.It.isValue(resource)), TypeMoq.Times.once()); diff --git a/src/test/providers/terminal.unit.test.ts b/src/test/providers/terminal.unit.test.ts index 603c0710f8c5..8f684835b7cf 100644 --- a/src/test/providers/terminal.unit.test.ts +++ b/src/test/providers/terminal.unit.test.ts @@ -2,6 +2,7 @@ // Licensed under the MIT License. import * as assert from 'assert'; +import * as sinon from 'sinon'; import { expect } from 'chai'; import * as TypeMoq from 'typemoq'; import { Disposable, Terminal, Uri } from 'vscode'; @@ -18,6 +19,7 @@ import { } from '../../client/common/types'; import { IServiceContainer } from '../../client/ioc/types'; import { TerminalProvider } from '../../client/providers/terminalProvider'; +import * as extapi from '../../client/envExt/api.internal'; suite('Terminal Provider', () => { let serviceContainer: TypeMoq.IMock; @@ -26,8 +28,15 @@ suite('Terminal Provider', () => { let activeResourceService: TypeMoq.IMock; let experimentService: TypeMoq.IMock; let terminalProvider: TerminalProvider; + let useEnvExtensionStub: sinon.SinonStub; + let shouldEnvExtHandleActivationStub: sinon.SinonStub; const resource = Uri.parse('a'); setup(() => { + useEnvExtensionStub = sinon.stub(extapi, 'useEnvExtension'); + useEnvExtensionStub.returns(false); + shouldEnvExtHandleActivationStub = sinon.stub(extapi, 'shouldEnvExtHandleActivation'); + shouldEnvExtHandleActivationStub.returns(false); + serviceContainer = TypeMoq.Mock.ofType(); commandManager = TypeMoq.Mock.ofType(); experimentService = TypeMoq.Mock.ofType(); @@ -40,6 +49,7 @@ suite('Terminal Provider', () => { serviceContainer.setup((c) => c.get(IActiveResourceService)).returns(() => activeResourceService.object); }); teardown(() => { + sinon.restore(); try { terminalProvider.dispose(); } catch { @@ -233,7 +243,7 @@ suite('Terminal Provider', () => { try { await terminalProvider.initialize(undefined); } catch (ex) { - assert(false, `No error should be thrown, ${ex}`); + assert.ok(false, `No error should be thrown, ${ex}`); } }); }); diff --git a/src/test/pythonEnvironments/base/common.ts b/src/test/pythonEnvironments/base/common.ts index 847d6e752273..9577e7ada490 100644 --- a/src/test/pythonEnvironments/base/common.ts +++ b/src/test/pythonEnvironments/base/common.ts @@ -203,7 +203,7 @@ export async function getEnvsWithUpdates( } updatesDone.resolve(); listener.dispose(); - } else { + } else if (event.index !== undefined) { const { index, update } = event; // We don't worry about if envs[index] is set already. envs[index] = update; diff --git a/src/test/pythonEnvironments/base/info/env.unit.test.ts b/src/test/pythonEnvironments/base/info/env.unit.test.ts index bb67a4465f9e..20bff8d71249 100644 --- a/src/test/pythonEnvironments/base/info/env.unit.test.ts +++ b/src/test/pythonEnvironments/base/info/env.unit.test.ts @@ -22,6 +22,9 @@ suite('Environment helpers', () => { version: parseVersionInfo('1.2.3')?.version, binDir: 'distroX/bin', }; + const locationConda1 = 'x/y/z/conda1'; + const locationConda2 = 'x/y/z/conda2'; + const kindConda = PythonEnvKind.Conda; function getEnv(info: { version?: string; arch?: Architecture; @@ -65,6 +68,10 @@ suite('Environment helpers', () => { "Python 3.8.1 64-bit ('my-env')", "Python 3.8.1 64-bit ('my-env')", ], + // conda env.name is empty. + [getEnv({ kind: kindConda }), 'Python', 'Python (conda)'], + [getEnv({ location: locationConda1, kind: kindConda }), "Python ('conda1')", "Python ('conda1': conda)"], + [getEnv({ location: locationConda2, kind: kindConda }), "Python ('conda2')", "Python ('conda2': conda)"], ]; return tests; } diff --git a/src/test/pythonEnvironments/base/info/envKind.unit.test.ts b/src/test/pythonEnvironments/base/info/envKind.unit.test.ts index fdf174b4c551..6d0866754330 100644 --- a/src/test/pythonEnvironments/base/info/envKind.unit.test.ts +++ b/src/test/pythonEnvironments/base/info/envKind.unit.test.ts @@ -13,6 +13,8 @@ const KIND_NAMES: [PythonEnvKind, string][] = [ [PythonEnvKind.MicrosoftStore, 'winStore'], [PythonEnvKind.Pyenv, 'pyenv'], [PythonEnvKind.Poetry, 'poetry'], + [PythonEnvKind.Hatch, 'hatch'], + [PythonEnvKind.Pixi, 'pixi'], [PythonEnvKind.Custom, 'customGlobal'], [PythonEnvKind.OtherGlobal, 'otherGlobal'], [PythonEnvKind.Venv, 'venv'], diff --git a/src/test/pythonEnvironments/base/locators/composite/envsCollectionService.unit.test.ts b/src/test/pythonEnvironments/base/locators/composite/envsCollectionService.unit.test.ts index f48d91cf24ae..9fe481c4da3f 100644 --- a/src/test/pythonEnvironments/base/locators/composite/envsCollectionService.unit.test.ts +++ b/src/test/pythonEnvironments/base/locators/composite/envsCollectionService.unit.test.ts @@ -1,6 +1,7 @@ -/* eslint-disable @typescript-eslint/no-explicit-any */ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. +/* eslint-disable class-methods-use-this */ +/* eslint-disable @typescript-eslint/no-explicit-any */ import { assert, expect } from 'chai'; import { cloneDeep } from 'lodash'; @@ -25,8 +26,37 @@ import { TEST_LAYOUT_ROOT } from '../../../common/commonTestConstants'; import { SimpleLocator } from '../../common'; import { assertEnvEqual, assertEnvsEqual, createFile, deleteFile } from '../envTestUtils'; import { OSType, getOSType } from '../../../../common'; +import * as nativeFinder from '../../../../../client/pythonEnvironments/base/locators/common/nativePythonFinder'; + +class MockNativePythonFinder implements nativeFinder.NativePythonFinder { + find(_searchPath: string): Promise { + throw new Error('Method not implemented.'); + } + + getCondaInfo(): Promise { + throw new Error('Method not implemented.'); + } + + resolve(_executable: string): Promise { + throw new Error('Method not implemented.'); + } + + refresh(): AsyncIterable { + const envs: nativeFinder.NativeEnvInfo[] = []; + return (async function* () { + for (const env of envs) { + yield env; + } + })(); + } + + dispose() { + /** noop */ + } +} suite('Python envs locator - Environments Collection', async () => { + let getNativePythonFinderStub: sinon.SinonStub; let collectionService: EnvsCollectionService; let storage: PythonEnvInfo[]; @@ -78,6 +108,9 @@ suite('Python envs locator - Environments Collection', async () => { ) { const env = buildEnvInfo({ executable, searchLocation, name, location, kind }); env.id = id ?? env.id; + env.version.major = 3; + env.version.minor = 10; + env.version.micro = 10; return env; } @@ -135,6 +168,8 @@ suite('Python envs locator - Environments Collection', async () => { } setup(async () => { + getNativePythonFinderStub = sinon.stub(nativeFinder, 'getNativePythonFinder'); + getNativePythonFinderStub.returns(new MockNativePythonFinder()); storage = []; const parentLocator = new SimpleLocator(getLocatorEnvs()); const cache = await createCollectionCache({ @@ -143,7 +178,7 @@ suite('Python envs locator - Environments Collection', async () => { storage = envs; }, }); - collectionService = new EnvsCollectionService(cache, parentLocator); + collectionService = new EnvsCollectionService(cache, parentLocator, false); }); teardown(async () => { @@ -189,7 +224,7 @@ suite('Python envs locator - Environments Collection', async () => { storage = e; }, }); - collectionService = new EnvsCollectionService(cache, parentLocator); + collectionService = new EnvsCollectionService(cache, parentLocator, false); await collectionService.triggerRefresh(undefined); await collectionService.triggerRefresh(undefined, { ifNotTriggerredAlready: true }); @@ -223,7 +258,7 @@ suite('Python envs locator - Environments Collection', async () => { storage = e; }, }); - collectionService = new EnvsCollectionService(cache, parentLocator); + collectionService = new EnvsCollectionService(cache, parentLocator, false); const events: PythonEnvCollectionChangedEvent[] = []; collectionService.onChanged((e) => { @@ -264,7 +299,7 @@ suite('Python envs locator - Environments Collection', async () => { storage = e; }, }); - collectionService = new EnvsCollectionService(cache, parentLocator); + collectionService = new EnvsCollectionService(cache, parentLocator, false); let events: PythonEnvCollectionChangedEvent[] = []; collectionService.onChanged((e) => { @@ -315,7 +350,7 @@ suite('Python envs locator - Environments Collection', async () => { storage = e; }, }); - collectionService = new EnvsCollectionService(cache, parentLocator); + collectionService = new EnvsCollectionService(cache, parentLocator, false); const events: PythonEnvCollectionChangedEvent[] = []; collectionService.onChanged((e) => { @@ -368,7 +403,7 @@ suite('Python envs locator - Environments Collection', async () => { storage = e; }, }); - collectionService = new EnvsCollectionService(cache, parentLocator); + collectionService = new EnvsCollectionService(cache, parentLocator, false); let stage: ProgressReportStage | undefined; collectionService.onProgress((e) => { stage = e.stage; @@ -439,7 +474,7 @@ suite('Python envs locator - Environments Collection', async () => { get: () => cachedEnvs, store: async () => noop(), }); - collectionService = new EnvsCollectionService(cache, parentLocator); + collectionService = new EnvsCollectionService(cache, parentLocator, false); const resolved = await collectionService.resolveEnv(env.executable.filename); assertEnvEqual(resolved, env); }); @@ -469,10 +504,10 @@ suite('Python envs locator - Environments Collection', async () => { get: () => [], store: async () => noop(), }); - collectionService = new EnvsCollectionService(cache, parentLocator); + collectionService = new EnvsCollectionService(cache, parentLocator, false); collectionService.triggerRefresh().ignoreErrors(); await waitDeferred.promise; // Cache should already contain `env` at this point, although it is not complete. - collectionService = new EnvsCollectionService(cache, parentLocator); + collectionService = new EnvsCollectionService(cache, parentLocator, false); const resolved = await collectionService.resolveEnv(env.executable.filename); assertEnvEqual(resolved, resolvedViaLocator); }); @@ -499,7 +534,7 @@ suite('Python envs locator - Environments Collection', async () => { get: () => cachedEnvs, store: async () => noop(), }); - collectionService = new EnvsCollectionService(cache, parentLocator); + collectionService = new EnvsCollectionService(cache, parentLocator, false); const resolved = await collectionService.resolveEnv(env.executable.filename); assertEnvEqual(resolved, resolvedViaLocator); }); @@ -518,7 +553,7 @@ suite('Python envs locator - Environments Collection', async () => { get: () => [], store: async () => noop(), }); - collectionService = new EnvsCollectionService(cache, parentLocator); + collectionService = new EnvsCollectionService(cache, parentLocator, false); const resolved = await collectionService.resolveEnv(resolvedViaLocator.executable.filename); const envs = collectionService.getEnvs(); assertEnvsEqual(envs, [resolved]); @@ -542,7 +577,7 @@ suite('Python envs locator - Environments Collection', async () => { get: () => cachedEnvs, store: async () => noop(), }); - collectionService = new EnvsCollectionService(cache, parentLocator); + collectionService = new EnvsCollectionService(cache, parentLocator, false); let resolved = await collectionService.resolveEnv(condaEnvWithoutPython.location); assertEnvEqual(resolved, condaEnvWithoutPython); // Ensure cache is used to resolve such envs. @@ -580,7 +615,7 @@ suite('Python envs locator - Environments Collection', async () => { get: () => [], store: async () => noop(), }); - collectionService = new EnvsCollectionService(cache, parentLocator); + collectionService = new EnvsCollectionService(cache, parentLocator, false); const events: PythonEnvCollectionChangedEvent[] = []; collectionService.onChanged((e) => { events.push(e); diff --git a/src/test/pythonEnvironments/base/locators/composite/envsReducer.unit.test.ts b/src/test/pythonEnvironments/base/locators/composite/envsReducer.unit.test.ts index 592586118d14..a7f44abbbf94 100644 --- a/src/test/pythonEnvironments/base/locators/composite/envsReducer.unit.test.ts +++ b/src/test/pythonEnvironments/base/locators/composite/envsReducer.unit.test.ts @@ -3,16 +3,13 @@ import { assert, expect } from 'chai'; import * as path from 'path'; -import { EventEmitter } from 'vscode'; import { PythonEnvKind, PythonEnvSource } from '../../../../../client/pythonEnvironments/base/info'; import { PythonEnvsReducer } from '../../../../../client/pythonEnvironments/base/locators/composite/envsReducer'; import { PythonEnvsChangedEvent } from '../../../../../client/pythonEnvironments/base/watcher'; import { assertBasicEnvsEqual } from '../envTestUtils'; import { createBasicEnv, getEnvs, getEnvsWithUpdates, SimpleLocator } from '../../common'; import { - PythonEnvUpdatedEvent, BasicEnvInfo, - ProgressNotificationEvent, ProgressReportStage, isProgressEvent, } from '../../../../../client/pythonEnvironments/base/locator'; @@ -65,31 +62,6 @@ suite('Python envs locator - Environments Reducer', () => { assertBasicEnvsEqual(envs, expected); }); - test('Updates to environments from the incoming iterator replaces the previous info', async () => { - // Arrange - const env = createBasicEnv(PythonEnvKind.Poetry, path.join('path', 'to', 'exec1')); - const updatedEnv = createBasicEnv(PythonEnvKind.Venv, path.join('path', 'to', 'exec1')); - const envsReturnedByParentLocator = [env]; - const didUpdate = new EventEmitter | ProgressNotificationEvent>(); - const parentLocator = new SimpleLocator(envsReturnedByParentLocator, { - onUpdated: didUpdate.event, - }); - const reducer = new PythonEnvsReducer(parentLocator); - - // Act - const iterator = reducer.iterEnvs(); - - const iteratorUpdateCallback = () => { - didUpdate.fire({ index: 0, old: env, update: updatedEnv }); - didUpdate.fire({ stage: ProgressReportStage.discoveryFinished }); // It is essential for the incoming iterator to fire "null" event signifying it's done - }; - const envs = await getEnvsWithUpdates(iterator, iteratorUpdateCallback); - - // Assert - assertBasicEnvsEqual(envs, [updatedEnv]); - didUpdate.dispose(); - }); - test('Ensure progress updates are emitted correctly', async () => { // Arrange const env1 = createBasicEnv(PythonEnvKind.Venv, path.join('path', 'to', 'exec1')); diff --git a/src/test/pythonEnvironments/base/locators/composite/envsResolver.unit.test.ts b/src/test/pythonEnvironments/base/locators/composite/envsResolver.unit.test.ts index 4a480cfd6e44..0d189da35282 100644 --- a/src/test/pythonEnvironments/base/locators/composite/envsResolver.unit.test.ts +++ b/src/test/pythonEnvironments/base/locators/composite/envsResolver.unit.test.ts @@ -57,7 +57,11 @@ suite('Python envs locator - Environments Resolver', () => { /** * Returns the expected environment to be returned by Environment info service */ - function createExpectedEnvInfo(env: PythonEnvInfo, expectedDisplay: string): PythonEnvInfo { + function createExpectedEnvInfo( + env: PythonEnvInfo, + expectedDisplay: string, + expectedDetailedDisplay: string, + ): PythonEnvInfo { const updatedEnv = cloneDeep(env); updatedEnv.version = { ...parseVersion('3.8.3-final'), @@ -67,7 +71,9 @@ suite('Python envs locator - Environments Resolver', () => { updatedEnv.executable.sysPrefix = 'path'; updatedEnv.arch = Architecture.x64; updatedEnv.display = expectedDisplay; - updatedEnv.detailedDisplayName = expectedDisplay; + updatedEnv.detailedDisplayName = expectedDetailedDisplay; + updatedEnv.identifiedUsingNativeLocator = updatedEnv.identifiedUsingNativeLocator ?? undefined; + updatedEnv.pythonRunCommand = updatedEnv.pythonRunCommand ?? undefined; if (env.kind === PythonEnvKind.Conda) { env.type = PythonEnvType.Conda; } @@ -82,6 +88,7 @@ suite('Python envs locator - Environments Resolver', () => { location = '', display: string | undefined = undefined, type?: PythonEnvType, + detailedDisplay?: string, ): PythonEnvInfo { return { name, @@ -94,13 +101,15 @@ suite('Python envs locator - Environments Resolver', () => { mtime: -1, }, display, - detailedDisplayName: display, + detailedDisplayName: detailedDisplay ?? display, version, arch: Architecture.Unknown, distro: { org: '' }, searchLocation: Uri.file(location), source: [], type, + identifiedUsingNativeLocator: undefined, + pythonRunCommand: undefined, }; } suite('iterEnvs()', () => { @@ -134,8 +143,9 @@ suite('Python envs locator - Environments Resolver', () => { undefined, 'win1', path.join(testVirtualHomeDir, '.venvs', 'win1'), - "Python ('win1': venv)", + "Python ('win1')", PythonEnvType.Virtual, + "Python ('win1': venv)", ); const envsReturnedByParentLocator = [env1]; const parentLocator = new SimpleLocator(envsReturnedByParentLocator); @@ -170,7 +180,11 @@ suite('Python envs locator - Environments Resolver', () => { const envs = await getEnvsWithUpdates(iterator); assertEnvsEqual(envs, [ - createExpectedEnvInfo(resolvedEnvReturnedByBasicResolver, "Python 3.8.3 ('win1': venv)"), + createExpectedEnvInfo( + resolvedEnvReturnedByBasicResolver, + "Python 3.8.3 ('win1')", + "Python 3.8.3 ('win1': venv)", + ), ]); }); @@ -237,7 +251,11 @@ suite('Python envs locator - Environments Resolver', () => { // Assert assertEnvsEqual(envs, [ - createExpectedEnvInfo(resolvedUpdatedEnvReturnedByBasicResolver, "Python 3.8.3 ('win1': venv)"), + createExpectedEnvInfo( + resolvedUpdatedEnvReturnedByBasicResolver, + "Python 3.8.3 ('win1')", + "Python 3.8.3 ('win1': venv)", + ), ]); didUpdate.dispose(); }); @@ -377,7 +395,11 @@ suite('Python envs locator - Environments Resolver', () => { assertEnvEqual( expected, - createExpectedEnvInfo(resolvedEnvReturnedByBasicResolver, "Python 3.8.3 ('win1': venv)"), + createExpectedEnvInfo( + resolvedEnvReturnedByBasicResolver, + "Python 3.8.3 ('win1')", + "Python 3.8.3 ('win1': venv)", + ), ); }); diff --git a/src/test/pythonEnvironments/base/locators/composite/resolverUtils.unit.test.ts b/src/test/pythonEnvironments/base/locators/composite/resolverUtils.unit.test.ts index dbd41715db0f..22b2f0c01304 100644 --- a/src/test/pythonEnvironments/base/locators/composite/resolverUtils.unit.test.ts +++ b/src/test/pythonEnvironments/base/locators/composite/resolverUtils.unit.test.ts @@ -153,6 +153,8 @@ suite('Resolver Utils', () => { kind: PythonEnvKind.MicrosoftStore, distro: { org: 'Microsoft' }, source: [PythonEnvSource.PathEnvVar], + identifiedUsingNativeLocator: undefined, + pythonRunCommand: undefined, ...createExpectedInterpreterInfo(python38path), }; setEnvDisplayString(expected); @@ -175,6 +177,8 @@ suite('Resolver Utils', () => { kind: PythonEnvKind.MicrosoftStore, distro: { org: 'Microsoft' }, source: [PythonEnvSource.PathEnvVar], + identifiedUsingNativeLocator: undefined, + pythonRunCommand: undefined, ...createExpectedInterpreterInfo(python38path), }; setEnvDisplayString(expected); @@ -239,6 +243,8 @@ suite('Resolver Utils', () => { distro: { org: '' }, searchLocation: undefined, source: [], + identifiedUsingNativeLocator: undefined, + pythonRunCommand: undefined, }; info.type = PythonEnvType.Conda; setEnvDisplayString(info); @@ -351,6 +357,8 @@ suite('Resolver Utils', () => { searchLocation: Uri.file(location), source: [], type: PythonEnvType.Virtual, + identifiedUsingNativeLocator: undefined, + pythonRunCommand: undefined, }; setEnvDisplayString(info); return info; @@ -406,6 +414,8 @@ suite('Resolver Utils', () => { distro: { org: '' }, searchLocation: undefined, source: [], + identifiedUsingNativeLocator: undefined, + pythonRunCommand: undefined, }; setEnvDisplayString(info); return info; diff --git a/src/test/pythonEnvironments/base/locators/envTestUtils.ts b/src/test/pythonEnvironments/base/locators/envTestUtils.ts index d1099ee4f840..db29575d29ba 100644 --- a/src/test/pythonEnvironments/base/locators/envTestUtils.ts +++ b/src/test/pythonEnvironments/base/locators/envTestUtils.ts @@ -1,11 +1,11 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -import * as fsapi from 'fs-extra'; import * as assert from 'assert'; import { exec } from 'child_process'; import { cloneDeep, zip } from 'lodash'; import { promisify } from 'util'; +import * as fsapi from '../../../../client/common/platform/fs-paths'; import { PythonEnvInfo, PythonVersion, UNKNOWN_PYTHON_VERSION } from '../../../../client/pythonEnvironments/base/info'; import { getEmptyVersion } from '../../../../client/pythonEnvironments/base/info/pythonVersion'; import { BasicEnvInfo } from '../../../../client/pythonEnvironments/base/locator'; @@ -103,10 +103,12 @@ export function assertBasicEnvsEqual(actualEnvs: BasicEnvInfo[], expectedEnvs: B const [actual, expected] = value; if (actual) { actual.source = actual.source ?? []; + actual.searchLocation = actual.searchLocation ?? undefined; actual.source.sort(); } if (expected) { expected.source = expected.source ?? []; + expected.searchLocation = expected.searchLocation ?? undefined; expected.source.sort(); } assert.deepStrictEqual(actual, expected); diff --git a/src/test/pythonEnvironments/base/locators/lowLevel/activestateLocator.unit.test.ts b/src/test/pythonEnvironments/base/locators/lowLevel/activestateLocator.unit.test.ts index bf86db883433..b0b18fb3827e 100644 --- a/src/test/pythonEnvironments/base/locators/lowLevel/activestateLocator.unit.test.ts +++ b/src/test/pythonEnvironments/base/locators/lowLevel/activestateLocator.unit.test.ts @@ -3,7 +3,7 @@ import * as path from 'path'; import * as sinon from 'sinon'; -import * as fsapi from 'fs-extra'; +import * as fsapi from '../../../../../client/common/platform/fs-paths'; import { PythonEnvKind } from '../../../../../client/pythonEnvironments/base/info'; import * as externalDependencies from '../../../../../client/pythonEnvironments/common/externalDependencies'; import { getEnvs } from '../../../../../client/pythonEnvironments/base/locatorUtils'; @@ -38,7 +38,7 @@ suite('ActiveState Locator', () => { const stateToolDir = ActiveState.getStateToolDir(); if (stateToolDir) { - sinon.stub(fsapi, 'pathExists').callsFake((dir: string) => dir === stateToolDir); + sinon.stub(fsapi, 'pathExists').callsFake((dir: string) => Promise.resolve(dir === stateToolDir)); } sinon.stub(externalDependencies, 'getPythonSetting').returns(undefined); diff --git a/src/test/pythonEnvironments/base/locators/lowLevel/condaLocator.testvirtualenvs.ts b/src/test/pythonEnvironments/base/locators/lowLevel/condaLocator.testvirtualenvs.ts index 1b2ea8715d35..3c7d4348b1c5 100644 --- a/src/test/pythonEnvironments/base/locators/lowLevel/condaLocator.testvirtualenvs.ts +++ b/src/test/pythonEnvironments/base/locators/lowLevel/condaLocator.testvirtualenvs.ts @@ -1,18 +1,22 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. import * as path from 'path'; -import * as fs from 'fs-extra'; import { assert } from 'chai'; import * as sinon from 'sinon'; +import * as fs from '../../../../../client/common/platform/fs-paths'; import * as platformUtils from '../../../../../client/common/utils/platform'; import { CondaEnvironmentLocator } from '../../../../../client/pythonEnvironments/base/locators/lowLevel/condaLocator'; import { sleep } from '../../../../core'; import { createDeferred, Deferred } from '../../../../../client/common/utils/async'; import { PythonEnvsChangedEvent } from '../../../../../client/pythonEnvironments/base/watcher'; -import { TEST_TIMEOUT } from '../../../../constants'; +import { EXTENSION_ROOT_DIR_FOR_TESTS, TEST_TIMEOUT } from '../../../../constants'; import { traceWarn } from '../../../../../client/logging'; import { TEST_LAYOUT_ROOT } from '../../../common/commonTestConstants'; +import { PYTHON_VIRTUAL_ENVS_LOCATION } from '../../../../ciConstants'; +import { isCI } from '../../../../../client/common/constants'; +import * as externalDependencies from '../../../../../client/pythonEnvironments/common/externalDependencies'; class CondaEnvs { private readonly condaEnvironmentsTxt; @@ -50,9 +54,13 @@ class CondaEnvs { } } -suite('Conda Env Watcher', async () => { +suite('Conda Env Locator', async () => { let locator: CondaEnvironmentLocator; let condaEnvsTxt: CondaEnvs; + const envsLocation = + PYTHON_VIRTUAL_ENVS_LOCATION !== undefined + ? path.join(EXTENSION_ROOT_DIR_FOR_TESTS, PYTHON_VIRTUAL_ENVS_LOCATION) + : path.join(EXTENSION_ROOT_DIR_FOR_TESTS, 'src', 'tmp', 'envPaths.json'); async function waitForChangeToBeDetected(deferred: Deferred) { const timeout = setTimeout(() => { @@ -61,11 +69,21 @@ suite('Conda Env Watcher', async () => { }, TEST_TIMEOUT); await deferred.promise; } + let envPaths: any; + + suiteSetup(async () => { + if (isCI) { + envPaths = await fs.readJson(envsLocation); + } + }); setup(async () => { sinon.stub(platformUtils, 'getUserHomeDir').returns(TEST_LAYOUT_ROOT); condaEnvsTxt = new CondaEnvs(); await condaEnvsTxt.cleanUp(); + if (isCI) { + sinon.stub(externalDependencies, 'getPythonSetting').returns(envPaths.condaExecPath); + } }); async function setupLocator(onChanged: (e: PythonEnvsChangedEvent) => Promise) { diff --git a/src/test/pythonEnvironments/base/locators/lowLevel/condaLocator.unit.test.ts b/src/test/pythonEnvironments/base/locators/lowLevel/condaLocator.unit.test.ts index 276f28cd665b..605109b7a67e 100644 --- a/src/test/pythonEnvironments/base/locators/lowLevel/condaLocator.unit.test.ts +++ b/src/test/pythonEnvironments/base/locators/lowLevel/condaLocator.unit.test.ts @@ -1,9 +1,9 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -import * as fsapi from 'fs-extra'; import * as path from 'path'; import * as sinon from 'sinon'; +import * as fsapi from '../../../../../client/common/platform/fs-paths'; import { PythonReleaseLevel, PythonVersion } from '../../../../../client/pythonEnvironments/base/info'; import * as externalDeps from '../../../../../client/pythonEnvironments/common/externalDependencies'; import { getPythonVersionFromConda } from '../../../../../client/pythonEnvironments/common/environmentManagers/conda'; @@ -17,14 +17,14 @@ suite('Conda Python Version Parser Tests', () => { setup(() => { readFileStub = sinon.stub(externalDeps, 'readFile'); + sinon.stub(externalDeps, 'inExperiment').returns(false); pathExistsStub = sinon.stub(externalDeps, 'pathExists'); pathExistsStub.resolves(true); }); teardown(() => { - readFileStub.restore(); - pathExistsStub.restore(); + sinon.restore(); }); interface ICondaPythonVersionTestData { diff --git a/src/test/pythonEnvironments/base/locators/lowLevel/customVirtualEnvLocator.unit.test.ts b/src/test/pythonEnvironments/base/locators/lowLevel/customVirtualEnvLocator.unit.test.ts index b1925e284426..e570c3fb72da 100644 --- a/src/test/pythonEnvironments/base/locators/lowLevel/customVirtualEnvLocator.unit.test.ts +++ b/src/test/pythonEnvironments/base/locators/lowLevel/customVirtualEnvLocator.unit.test.ts @@ -9,6 +9,7 @@ import * as platformUtils from '../../../../../client/common/utils/platform'; import { PythonEnvKind } from '../../../../../client/pythonEnvironments/base/info'; import { getEnvs } from '../../../../../client/pythonEnvironments/base/locatorUtils'; import { PythonEnvsChangedEvent } from '../../../../../client/pythonEnvironments/base/watcher'; +import * as helpers from '../../../../../client/common/helpers'; import * as externalDependencies from '../../../../../client/pythonEnvironments/common/externalDependencies'; import { CustomVirtualEnvironmentLocator, @@ -32,7 +33,7 @@ suite('CustomVirtualEnvironment Locator', () => { let untildify: sinon.SinonStub; setup(async () => { - untildify = sinon.stub(externalDependencies, 'untildify'); + untildify = sinon.stub(helpers, 'untildify'); untildify.callsFake((value: string) => value.replace('~', testVirtualHomeDir)); getUserHomeDirStub = sinon.stub(platformUtils, 'getUserHomeDir'); getUserHomeDirStub.returns(testVirtualHomeDir); @@ -213,7 +214,7 @@ suite('CustomVirtualEnvironment Locator', () => { test('onChanged fires if venvPath setting changes', async () => { const events: PythonEnvsChangedEvent[] = []; - const expected: PythonEnvsChangedEvent[] = [{}]; + const expected: PythonEnvsChangedEvent[] = [{ providerId: locator.providerId }]; locator.onChanged((e) => events.push(e)); await getEnvs(locator.iterEnvs()); @@ -228,7 +229,7 @@ suite('CustomVirtualEnvironment Locator', () => { test('onChanged fires if venvFolders setting changes', async () => { const events: PythonEnvsChangedEvent[] = []; - const expected: PythonEnvsChangedEvent[] = [{}]; + const expected: PythonEnvsChangedEvent[] = [{ providerId: locator.providerId }]; locator.onChanged((e) => events.push(e)); await getEnvs(locator.iterEnvs()); diff --git a/src/test/pythonEnvironments/base/locators/lowLevel/globalVirtualEnvironmentLocator.unit.test.ts b/src/test/pythonEnvironments/base/locators/lowLevel/globalVirtualEnvironmentLocator.unit.test.ts index 6998d9f4050f..ede947073ea2 100644 --- a/src/test/pythonEnvironments/base/locators/lowLevel/globalVirtualEnvironmentLocator.unit.test.ts +++ b/src/test/pythonEnvironments/base/locators/lowLevel/globalVirtualEnvironmentLocator.unit.test.ts @@ -3,6 +3,7 @@ import * as path from 'path'; import * as sinon from 'sinon'; +import { Uri } from 'vscode'; import * as fsWatcher from '../../../../../client/common/platform/fileSystemWatcher'; import * as platformUtils from '../../../../../client/common/utils/platform'; import { PythonEnvKind } from '../../../../../client/pythonEnvironments/base/info'; @@ -22,6 +23,7 @@ suite('GlobalVirtualEnvironment Locator', () => { let readFileStub: sinon.SinonStub; let locator: GlobalVirtualEnvironmentLocator; let watchLocationForPatternStub: sinon.SinonStub; + const project2 = path.join(TEST_LAYOUT_ROOT, 'pipenv', 'project2'); setup(async () => { getEnvVariableStub = sinon.stub(platformUtils, 'getEnvironmentVariable'); @@ -49,7 +51,7 @@ suite('GlobalVirtualEnvironment Locator', () => { '.project', ); readFileStub = sinon.stub(externalDependencies, 'readFile'); - readFileStub.withArgs(expectedDotProjectFile).returns(path.join(TEST_LAYOUT_ROOT, 'pipenv', 'project2')); + readFileStub.withArgs(expectedDotProjectFile).returns(project2); readFileStub.callThrough(); }); teardown(async () => { @@ -131,6 +133,11 @@ suite('GlobalVirtualEnvironment Locator', () => { }); test('iterEnvs(): Non-Windows', async () => { + const pipenv = createBasicEnv( + PythonEnvKind.Pipenv, + path.join(testVirtualHomeDir, '.local', 'share', 'virtualenvs', 'project2-vnNIWe9P', 'bin', 'python'), + ); + pipenv.searchLocation = Uri.file(project2); const expectedEnvs = [ createBasicEnv(PythonEnvKind.Venv, path.join(testVirtualHomeDir, '.venvs', 'posix1', 'python')), createBasicEnv(PythonEnvKind.Venv, path.join(testVirtualHomeDir, '.venvs', 'posix2', 'bin', 'python')), @@ -147,10 +154,7 @@ suite('GlobalVirtualEnvironment Locator', () => { PythonEnvKind.VirtualEnvWrapper, path.join(testVirtualHomeDir, 'workonhome', 'posix2', 'bin', 'python'), ), - createBasicEnv( - PythonEnvKind.Pipenv, - path.join(testVirtualHomeDir, '.local', 'share', 'virtualenvs', 'project2-vnNIWe9P', 'bin', 'python'), - ), + pipenv, ]; locator = new GlobalVirtualEnvironmentLocator(); @@ -179,6 +183,11 @@ suite('GlobalVirtualEnvironment Locator', () => { test('iterEnvs(): Non-Windows (WORKON_HOME not set)', async () => { getEnvVariableStub.withArgs('WORKON_HOME').returns(undefined); + const pipenv = createBasicEnv( + PythonEnvKind.Pipenv, + path.join(testVirtualHomeDir, '.local', 'share', 'virtualenvs', 'project2-vnNIWe9P', 'bin', 'python'), + ); + pipenv.searchLocation = Uri.file(project2); const expectedEnvs = [ createBasicEnv(PythonEnvKind.Venv, path.join(testVirtualHomeDir, '.venvs', 'posix1', 'python')), createBasicEnv(PythonEnvKind.Venv, path.join(testVirtualHomeDir, '.venvs', 'posix2', 'bin', 'python')), @@ -190,10 +199,7 @@ suite('GlobalVirtualEnvironment Locator', () => { PythonEnvKind.VirtualEnvWrapper, path.join(testVirtualHomeDir, '.virtualenvs', 'posix2', 'bin', 'python'), ), - createBasicEnv( - PythonEnvKind.Pipenv, - path.join(testVirtualHomeDir, '.local', 'share', 'virtualenvs', 'project2-vnNIWe9P', 'bin', 'python'), - ), + pipenv, ]; locator = new GlobalVirtualEnvironmentLocator(); diff --git a/src/test/pythonEnvironments/base/locators/lowLevel/hatchLocator.unit.test.ts b/src/test/pythonEnvironments/base/locators/lowLevel/hatchLocator.unit.test.ts new file mode 100644 index 000000000000..9a2a69908f2a --- /dev/null +++ b/src/test/pythonEnvironments/base/locators/lowLevel/hatchLocator.unit.test.ts @@ -0,0 +1,76 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import * as sinon from 'sinon'; +import * as path from 'path'; +import { PythonEnvKind } from '../../../../../client/pythonEnvironments/base/info'; +import * as externalDependencies from '../../../../../client/pythonEnvironments/common/externalDependencies'; +import * as platformUtils from '../../../../../client/common/utils/platform'; +import { getEnvs } from '../../../../../client/pythonEnvironments/base/locatorUtils'; +import { HatchLocator } from '../../../../../client/pythonEnvironments/base/locators/lowLevel/hatchLocator'; +import { assertBasicEnvsEqual } from '../envTestUtils'; +import { createBasicEnv } from '../../common'; +import { makeExecHandler, projectDirs, venvDirs } from '../../../common/environmentManagers/hatch.unit.test'; + +suite('Hatch Locator', () => { + let exec: sinon.SinonStub; + let getPythonSetting: sinon.SinonStub; + let getOSType: sinon.SinonStub; + let locator: HatchLocator; + + suiteSetup(() => { + getPythonSetting = sinon.stub(externalDependencies, 'getPythonSetting'); + getPythonSetting.returns('hatch'); + getOSType = sinon.stub(platformUtils, 'getOSType'); + exec = sinon.stub(externalDependencies, 'exec'); + }); + + suiteTeardown(() => sinon.restore()); + + suite('iterEnvs()', () => { + setup(() => { + getOSType.returns(platformUtils.OSType.Linux); + }); + + interface TestArgs { + osType?: platformUtils.OSType; + pythonBin?: string; + } + + const testProj1 = async ({ osType, pythonBin = 'bin/python' }: TestArgs = {}) => { + if (osType) { + getOSType.returns(osType); + } + + locator = new HatchLocator(projectDirs.project1); + exec.callsFake(makeExecHandler(venvDirs.project1, { path: true, cwd: projectDirs.project1 })); + + const iterator = locator.iterEnvs(); + const actualEnvs = await getEnvs(iterator); + + const expectedEnvs = [createBasicEnv(PythonEnvKind.Hatch, path.join(venvDirs.project1.default, pythonBin))]; + assertBasicEnvsEqual(actualEnvs, expectedEnvs); + }; + + test('project with only the default env', () => testProj1()); + test('project with only the default env on Windows', () => + testProj1({ + osType: platformUtils.OSType.Windows, + pythonBin: 'Scripts/python.exe', + })); + + test('project with multiple defined envs', async () => { + locator = new HatchLocator(projectDirs.project2); + exec.callsFake(makeExecHandler(venvDirs.project2, { path: true, cwd: projectDirs.project2 })); + + const iterator = locator.iterEnvs(); + const actualEnvs = await getEnvs(iterator); + + const expectedEnvs = [ + createBasicEnv(PythonEnvKind.Hatch, path.join(venvDirs.project2.default, 'bin/python')), + createBasicEnv(PythonEnvKind.Hatch, path.join(venvDirs.project2.test, 'bin/python')), + ]; + assertBasicEnvsEqual(actualEnvs, expectedEnvs); + }); + }); +}); diff --git a/src/test/pythonEnvironments/base/locators/lowLevel/microsoftStoreLocator.test.ts b/src/test/pythonEnvironments/base/locators/lowLevel/microsoftStoreLocator.test.ts index fc3e9b7f5663..511597dd28db 100644 --- a/src/test/pythonEnvironments/base/locators/lowLevel/microsoftStoreLocator.test.ts +++ b/src/test/pythonEnvironments/base/locators/lowLevel/microsoftStoreLocator.test.ts @@ -2,9 +2,9 @@ // Licensed under the MIT License. import { assert } from 'chai'; -import * as fs from 'fs-extra'; import * as path from 'path'; import { Uri } from 'vscode'; +import * as fs from '../../../../../client/common/platform/fs-paths'; import { FileChangeType } from '../../../../../client/common/platform/fileSystemWatcher'; import { createDeferred, Deferred, sleep } from '../../../../../client/common/utils/async'; import { PythonEnvKind } from '../../../../../client/pythonEnvironments/base/info'; diff --git a/src/test/pythonEnvironments/base/locators/lowLevel/pixiLocator.unit.test.ts b/src/test/pythonEnvironments/base/locators/lowLevel/pixiLocator.unit.test.ts new file mode 100644 index 000000000000..b55f61c3a771 --- /dev/null +++ b/src/test/pythonEnvironments/base/locators/lowLevel/pixiLocator.unit.test.ts @@ -0,0 +1,84 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import * as sinon from 'sinon'; +import * as path from 'path'; +import { PixiLocator } from '../../../../../client/pythonEnvironments/base/locators/lowLevel/pixiLocator'; +import * as externalDependencies from '../../../../../client/pythonEnvironments/common/externalDependencies'; +import * as platformUtils from '../../../../../client/common/utils/platform'; +import { makeExecHandler, projectDirs } from '../../../common/environmentManagers/pixi.unit.test'; +import { getEnvs } from '../../../../../client/pythonEnvironments/base/locatorUtils'; +import { createBasicEnv } from '../../common'; +import { PythonEnvKind } from '../../../../../client/pythonEnvironments/base/info'; +import { assertBasicEnvsEqual } from '../envTestUtils'; + +suite('Pixi Locator', () => { + let exec: sinon.SinonStub; + let getPythonSetting: sinon.SinonStub; + let getOSType: sinon.SinonStub; + let locator: PixiLocator; + let pathExistsStub: sinon.SinonStub; + + suiteSetup(() => { + getPythonSetting = sinon.stub(externalDependencies, 'getPythonSetting'); + getPythonSetting.returns('pixi'); + getOSType = sinon.stub(platformUtils, 'getOSType'); + exec = sinon.stub(externalDependencies, 'exec'); + pathExistsStub = sinon.stub(externalDependencies, 'pathExists'); + pathExistsStub.resolves(true); + }); + + suiteTeardown(() => sinon.restore()); + + suite('iterEnvs()', () => { + interface TestArgs { + projectDir: string; + osType: platformUtils.OSType; + pythonBin: string; + } + + const testProject = async ({ projectDir, osType, pythonBin }: TestArgs) => { + getOSType.returns(osType); + + locator = new PixiLocator(projectDir); + exec.callsFake(makeExecHandler({ cwd: projectDir })); + + const iterator = locator.iterEnvs(); + const actualEnvs = await getEnvs(iterator); + + const envPath = path.join(projectDir, '.pixi', 'envs', 'default'); + const expectedEnvs = [ + createBasicEnv(PythonEnvKind.Pixi, path.join(envPath, pythonBin), undefined, envPath), + ]; + assertBasicEnvsEqual(actualEnvs, expectedEnvs); + }; + + test('project with only the default env', () => + testProject({ + projectDir: projectDirs.nonWindows.path, + osType: platformUtils.OSType.Linux, + pythonBin: 'bin/python', + })); + test('project with only the default env on Windows', () => + testProject({ + projectDir: projectDirs.windows.path, + osType: platformUtils.OSType.Windows, + pythonBin: 'python.exe', + })); + + test('project with multiple environments', async () => { + getOSType.returns(platformUtils.OSType.Linux); + + exec.callsFake(makeExecHandler({ cwd: projectDirs.multiEnv.path })); + + locator = new PixiLocator(projectDirs.multiEnv.path); + const iterator = locator.iterEnvs(); + const actualEnvs = await getEnvs(iterator); + + const expectedEnvs = projectDirs.multiEnv.info.environments_info.map((info) => + createBasicEnv(PythonEnvKind.Pixi, path.join(info.prefix, 'bin/python'), undefined, info.prefix), + ); + assertBasicEnvsEqual(actualEnvs, expectedEnvs); + }); + }); +}); diff --git a/src/test/pythonEnvironments/base/locators/lowLevel/poetryLocator.unit.test.ts b/src/test/pythonEnvironments/base/locators/lowLevel/poetryLocator.unit.test.ts index 95c1a401df54..e7982a4c4e9a 100644 --- a/src/test/pythonEnvironments/base/locators/lowLevel/poetryLocator.unit.test.ts +++ b/src/test/pythonEnvironments/base/locators/lowLevel/poetryLocator.unit.test.ts @@ -3,7 +3,8 @@ import * as path from 'path'; import * as sinon from 'sinon'; -import { PythonEnvKind } from '../../../../../client/pythonEnvironments/base/info'; +import { Uri } from 'vscode'; +import { PythonEnvKind, PythonEnvSource } from '../../../../../client/pythonEnvironments/base/info'; import * as externalDependencies from '../../../../../client/pythonEnvironments/common/externalDependencies'; import * as platformUtils from '../../../../../client/common/utils/platform'; import { getEnvs } from '../../../../../client/pythonEnvironments/base/locatorUtils'; @@ -11,7 +12,8 @@ import { PoetryLocator } from '../../../../../client/pythonEnvironments/base/loc import { TEST_LAYOUT_ROOT } from '../../../common/commonTestConstants'; import { assertBasicEnvsEqual } from '../envTestUtils'; import { ExecutionResult, ShellOptions } from '../../../../../client/common/process/types'; -import { createBasicEnv } from '../../common'; +import { createBasicEnv as createBasicEnvCommon } from '../../common'; +import { BasicEnvInfo } from '../../../../../client/pythonEnvironments/base/locator'; suite('Poetry Locator', () => { let shellExecute: sinon.SinonStub; @@ -31,6 +33,17 @@ suite('Poetry Locator', () => { suite('Windows', () => { const project1 = path.join(testPoetryDir, 'project1'); + + function createBasicEnv( + kind: PythonEnvKind, + executablePath: string, + source?: PythonEnvSource[], + envPath?: string, + ): BasicEnvInfo { + const basicEnv = createBasicEnvCommon(kind, executablePath, source, envPath); + basicEnv.searchLocation = Uri.file(project1); + return basicEnv; + } setup(() => { locator = new PoetryLocator(project1); getOSTypeStub.returns(platformUtils.OSType.Windows); @@ -72,6 +85,17 @@ suite('Poetry Locator', () => { suite('Non-Windows', () => { const project2 = path.join(testPoetryDir, 'project2'); + + function createBasicEnv( + kind: PythonEnvKind, + executablePath: string, + source?: PythonEnvSource[], + envPath?: string, + ): BasicEnvInfo { + const basicEnv = createBasicEnvCommon(kind, executablePath, source, envPath); + basicEnv.searchLocation = Uri.file(project2); + return basicEnv; + } setup(() => { locator = new PoetryLocator(project2); getOSTypeStub.returns(platformUtils.OSType.Linux); diff --git a/src/test/pythonEnvironments/base/locators/lowLevel/watcherTestUtils.ts b/src/test/pythonEnvironments/base/locators/lowLevel/watcherTestUtils.ts index 6e269f5680ba..e9c7be3ec321 100644 --- a/src/test/pythonEnvironments/base/locators/lowLevel/watcherTestUtils.ts +++ b/src/test/pythonEnvironments/base/locators/lowLevel/watcherTestUtils.ts @@ -2,8 +2,8 @@ // Licensed under the MIT License. import { assert } from 'chai'; -import * as fs from 'fs-extra'; import * as path from 'path'; +import * as fs from '../../../../../client/common/platform/fs-paths'; import { FileChangeType } from '../../../../../client/common/platform/fileSystemWatcher'; import { IDisposable } from '../../../../../client/common/types'; import { createDeferred, Deferred, sleep } from '../../../../../client/common/utils/async'; diff --git a/src/test/pythonEnvironments/base/locators/lowLevel/windowsRegistryLocator.unit.test.ts b/src/test/pythonEnvironments/base/locators/lowLevel/windowsRegistryLocator.unit.test.ts index c4621b267ad6..07a7a864ef74 100644 --- a/src/test/pythonEnvironments/base/locators/lowLevel/windowsRegistryLocator.unit.test.ts +++ b/src/test/pythonEnvironments/base/locators/lowLevel/windowsRegistryLocator.unit.test.ts @@ -4,13 +4,18 @@ import * as assert from 'assert'; import * as path from 'path'; import * as sinon from 'sinon'; +import { expect } from 'chai'; import { PythonEnvKind, PythonEnvSource } from '../../../../../client/pythonEnvironments/base/info'; import { getEnvs } from '../../../../../client/pythonEnvironments/base/locatorUtils'; import * as winreg from '../../../../../client/pythonEnvironments/common/windowsRegistry'; -import { WindowsRegistryLocator } from '../../../../../client/pythonEnvironments/base/locators/lowLevel/windowsRegistryLocator'; +import { + WindowsRegistryLocator, + WINDOWS_REG_PROVIDER_ID, +} from '../../../../../client/pythonEnvironments/base/locators/lowLevel/windowsRegistryLocator'; import { createBasicEnv } from '../../common'; import { TEST_LAYOUT_ROOT } from '../../../common/commonTestConstants'; import { assertBasicEnvsEqual } from '../envTestUtils'; +import * as externalDependencies from '../../../../../client/pythonEnvironments/common/externalDependencies'; suite('Windows Registry', () => { let stubReadRegistryValues: sinon.SinonStub; @@ -200,6 +205,7 @@ suite('Windows Registry', () => { } setup(async () => { + sinon.stub(externalDependencies, 'inExperiment').returns(true); stubReadRegistryValues = sinon.stub(winreg, 'readRegistryValues'); stubReadRegistryKeys = sinon.stub(winreg, 'readRegistryKeys'); stubReadRegistryValues.callsFake(fakeRegistryValues); @@ -220,18 +226,29 @@ suite('Windows Registry', () => { createBasicEnv(PythonEnvKind.OtherGlobal, path.join(regTestRoot, 'python38', 'python.exe')), ].map((e) => ({ ...e, source: [PythonEnvSource.WindowsRegistry] })); - const iterator = locator.iterEnvs(); + const lazyIterator = locator.iterEnvs(undefined, true); + const envs = await getEnvs(lazyIterator); + expect(envs.length).to.equal(0); + + const iterator = locator.iterEnvs({ providerId: WINDOWS_REG_PROVIDER_ID }, true); const actualEnvs = await getEnvs(iterator); assertBasicEnvsEqual(actualEnvs, expectedEnvs); }); + test('iterEnvs(): query is undefined', async () => { + // Iterate no envs when query is `undefined`, i.e notify completion immediately. + const lazyIterator = locator.iterEnvs(undefined, true); + const envs = await getEnvs(lazyIterator); + expect(envs.length).to.equal(0); + }); + test('iterEnvs(): no registry permission', async () => { stubReadRegistryKeys.callsFake(() => { throw Error(); }); - const iterator = locator.iterEnvs(); + const iterator = locator.iterEnvs({ providerId: WINDOWS_REG_PROVIDER_ID }, true); const actualEnvs = await getEnvs(iterator); assert.deepStrictEqual(actualEnvs, []); @@ -250,7 +267,7 @@ suite('Windows Registry', () => { createBasicEnv(PythonEnvKind.OtherGlobal, path.join(regTestRoot, 'python38', 'python.exe')), ].map((e) => ({ ...e, source: [PythonEnvSource.WindowsRegistry] })); - const iterator = locator.iterEnvs(); + const iterator = locator.iterEnvs({ providerId: WINDOWS_REG_PROVIDER_ID }, true); const actualEnvs = await getEnvs(iterator); assertBasicEnvsEqual(actualEnvs, expectedEnvs); diff --git a/src/test/pythonEnvironments/common/commonUtils.functional.test.ts b/src/test/pythonEnvironments/common/commonUtils.functional.test.ts index e0c1f755e2c8..647a17a40a90 100644 --- a/src/test/pythonEnvironments/common/commonUtils.functional.test.ts +++ b/src/test/pythonEnvironments/common/commonUtils.functional.test.ts @@ -86,7 +86,6 @@ suite('pyenvs common utils - finding Python executables', () => { python3 -> sub2/sub2.2/python3 python3.7 -> sub2/sub2.1/sub2.1.1/python - python2.7 -> does-not-exist `); } }); @@ -106,7 +105,6 @@ suite('pyenvs common utils - finding Python executables', () => { // These will match. 'python', 'python2', - 'python2.7', 'python3', 'python3.7', 'python3.8', @@ -137,7 +135,6 @@ suite('pyenvs common utils - finding Python executables', () => { // These will match. 'python', 'python2', - 'python2.7', 'python3', 'python3.7', 'python3.8', @@ -167,7 +164,6 @@ suite('pyenvs common utils - finding Python executables', () => { // These will match. 'python', 'python2', - 'python2.7', 'python3', 'python3.7', 'python3.8', diff --git a/src/test/pythonEnvironments/common/environmentIdentifier.unit.test.ts b/src/test/pythonEnvironments/common/environmentIdentifier.unit.test.ts index 6aeb320a0b11..af719c3e40ed 100644 --- a/src/test/pythonEnvironments/common/environmentIdentifier.unit.test.ts +++ b/src/test/pythonEnvironments/common/environmentIdentifier.unit.test.ts @@ -148,7 +148,7 @@ suite('Environment Identifier', () => { test(`Path using forward slashes (${exe})`, async () => { const interpreterPath = path .join(fakeLocalAppDataPath, 'Microsoft', 'WindowsApps', exe) - .replace('\\', '/'); + .replace(/\\/g, '/'); const envType: PythonEnvKind = await identifyEnvironment(interpreterPath); assert.deepEqual(envType, PythonEnvKind.MicrosoftStore); }); diff --git a/src/test/pythonEnvironments/common/environmentManagers/conda.unit.test.ts b/src/test/pythonEnvironments/common/environmentManagers/conda.unit.test.ts index ca0e24d5f3d3..9480dffe6a59 100644 --- a/src/test/pythonEnvironments/common/environmentManagers/conda.unit.test.ts +++ b/src/test/pythonEnvironments/common/environmentManagers/conda.unit.test.ts @@ -1,10 +1,10 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ import { assert, expect } from 'chai'; -import * as fs from 'fs'; -import * as fsapi from 'fs-extra'; import * as path from 'path'; import * as sinon from 'sinon'; import * as util from 'util'; import { eq } from 'semver'; +import * as fs from '../../../../client/common/platform/fs-paths'; import * as platform from '../../../../client/common/utils/platform'; import { PythonEnvKind } from '../../../../client/pythonEnvironments/base/info'; import { getEnvs } from '../../../../client/pythonEnvironments/base/locatorUtils'; @@ -105,17 +105,17 @@ suite('Conda and its environments are located correctly', () => { sinon.stub(platform, 'getUserHomeDir').callsFake(() => homeDir); - sinon.stub(fsapi, 'lstat').callsFake(async (filePath: fs.PathLike) => { + sinon.stub(fs, 'lstat').callsFake(async (filePath: fs.PathLike) => { if (typeof filePath !== 'string') { throw new Error(`expected filePath to be string, got ${typeof filePath}`); } const file = getFile(filePath, 'throwIfMissing'); return { isDirectory: () => typeof file !== 'string', - } as fsapi.Stats; + } as fs.Stats; }); - sinon.stub(fsapi, 'pathExists').callsFake(async (filePath: string | Buffer) => { + sinon.stub(fs, 'pathExists').callsFake(async (filePath: string | Buffer) => { if (typeof filePath !== 'string') { throw new Error(`expected filePath to be string, got ${typeof filePath}`); } @@ -127,16 +127,9 @@ suite('Conda and its environments are located correctly', () => { return true; }); - sinon.stub(fsapi, 'readdir').callsFake(async (filePath: fs.PathLike) => { - if (typeof filePath !== 'string') { - throw new Error(`expected filePath to be string, got ${typeof filePath}`); - } - return (Object.keys(getFile(filePath, 'throwIfMissing')) as unknown) as fs.Dirent[]; - }); - - sinon - .stub(fs.promises, 'readdir' as any) // eslint-disable-line @typescript-eslint/no-explicit-any - .callsFake(async (filePath: fs.PathLike, options?: { withFileTypes?: boolean }) => { + sinon.stub(fs, 'readdir').callsFake( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + async (filePath: fs.PathLike, options?: { withFileTypes?: boolean }): Promise => { if (typeof filePath !== 'string') { throw new Error(`expected path to be string, got ${typeof path}`); } @@ -146,6 +139,10 @@ suite('Conda and its environments are located correctly', () => { throw new Error(`${path} is not a directory`); } + if (options === undefined) { + return (Object.keys(getFile(filePath, 'throwIfMissing')) as unknown) as fs.Dirent[]; + } + const names = Object.keys(dir); if (!options?.withFileTypes) { return names; @@ -156,6 +153,7 @@ suite('Conda and its environments are located correctly', () => { const isFile = typeof dir[name] === 'string'; return { name, + path: dir.name?.toString() ?? '', isFile: () => isFile, isDirectory: () => !isFile, isBlockDevice: () => false, @@ -163,27 +161,34 @@ suite('Conda and its environments are located correctly', () => { isSymbolicLink: () => false, isFIFO: () => false, isSocket: () => false, + parentPath: '', }; }, ); - }); - - sinon - .stub(fsapi, 'readFile' as any) // eslint-disable-line @typescript-eslint/no-explicit-any - .callsFake(async (filePath: string | Buffer | number, encoding: string) => { - if (typeof filePath !== 'string') { - throw new Error(`expected filePath to be string, got ${typeof filePath}`); - } else if (encoding !== 'utf8') { - throw new Error(`Unsupported encoding ${encoding}`); + }, + ); + const readFileStub = async ( + filePath: fs.PathOrFileDescriptor, + options: { encoding: BufferEncoding; flag?: string | undefined } | BufferEncoding, + ): Promise => { + if (typeof filePath !== 'string') { + throw new Error(`expected filePath to be string, got ${typeof filePath}`); + } else if (typeof options === 'string') { + if (options !== 'utf8') { + throw new Error(`Unsupported encoding ${options}`); } + } else if ((options as any).encoding !== 'utf8') { + throw new Error(`Unsupported encoding ${(options as any).encoding}`); + } - const contents = getFile(filePath); - if (typeof contents !== 'string') { - throw new Error(`${filePath} is not a file`); - } + const contents = getFile(filePath); + if (typeof contents !== 'string') { + throw new Error(`${filePath} is not a file`); + } - return contents; - }); + return contents; + }; + sinon.stub(fs, 'readFile' as any).callsFake(readFileStub as any); sinon.stub(externalDependencies, 'exec').callsFake(async (command: string, args: string[]) => { for (const prefix of ['', ...execPath]) { @@ -275,7 +280,11 @@ suite('Conda and its environments are located correctly', () => { opt: {}, }, }, - opt: {}, + opt: { + homebrew: { + bin: {}, + }, + }, usr: { share: { doc: {}, @@ -289,7 +298,14 @@ suite('Conda and its environments are located correctly', () => { }; }); - ['/usr/share', '/usr/local/share', '/opt', '/home/user', '/home/user/opt'].forEach((prefix) => { + [ + '/usr/share', + '/usr/local/share', + '/opt', + '/opt/homebrew/bin', + '/home/user', + '/home/user/opt', + ].forEach((prefix) => { const condaPath = `${prefix}/${condaDirName}`; test(`Must find conda in ${condaPath}`, async () => { @@ -520,7 +536,7 @@ suite('Conda and its environments are located correctly', () => { expect(args).to.not.equal(undefined); assert.deepStrictEqual( args, - ['conda', 'run', '-n', 'envName', '--no-capture-output', 'python', OUTPUT_MARKER_SCRIPT], + ['conda', 'run', '-p', 'envPrefix', '--no-capture-output', 'python', OUTPUT_MARKER_SCRIPT], 'Incorrect args for case 1', ); @@ -592,6 +608,11 @@ suite('Conda and its environments are located correctly', () => { }, }, }; + sinon.stub(externalDependencies, 'inExperiment').returns(false); + }); + + teardown(() => { + sinon.restore(); }); test('Must compute conda environment name from prefix', async () => { diff --git a/src/test/pythonEnvironments/common/environmentManagers/hatch.unit.test.ts b/src/test/pythonEnvironments/common/environmentManagers/hatch.unit.test.ts new file mode 100644 index 000000000000..5d348aa2b131 --- /dev/null +++ b/src/test/pythonEnvironments/common/environmentManagers/hatch.unit.test.ts @@ -0,0 +1,102 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { expect } from 'chai'; +import * as path from 'path'; +import * as sinon from 'sinon'; +import { ExecutionResult, ShellOptions } from '../../../../client/common/process/types'; +import * as externalDependencies from '../../../../client/pythonEnvironments/common/externalDependencies'; +import { Hatch } from '../../../../client/pythonEnvironments/common/environmentManagers/hatch'; +import { TEST_LAYOUT_ROOT } from '../commonTestConstants'; + +export type HatchCommand = { cmd: 'env show --json' } | { cmd: 'env find'; env: string } | { cmd: null }; + +export function hatchCommand(args: string[]): HatchCommand { + if (args.length < 2) { + return { cmd: null }; + } + if (args[0] === 'env' && args[1] === 'show' && args[2] === '--json') { + return { cmd: 'env show --json' }; + } + if (args[0] === 'env' && args[1] === 'find') { + return { cmd: 'env find', env: args[2] }; + } + return { cmd: null }; +} + +interface VerifyOptions { + path?: boolean; + cwd?: string; +} + +export function makeExecHandler(venvDirs: Record, verify: VerifyOptions = {}) { + return async (file: string, args: string[], options: ShellOptions): Promise> => { + if (verify.path && file !== 'hatch') { + throw new Error('Command failed'); + } + if (verify.cwd) { + const cwd = typeof options.cwd === 'string' ? options.cwd : options.cwd?.toString(); + if (!cwd || !externalDependencies.arePathsSame(cwd, verify.cwd)) { + throw new Error('Command failed'); + } + } + const cmd = hatchCommand(args); + if (cmd.cmd === 'env show --json') { + const envs = Object.fromEntries(Object.keys(venvDirs).map((name) => [name, { type: 'virtual' }])); + return { stdout: JSON.stringify(envs) }; + } + if (cmd.cmd === 'env find' && cmd.env in venvDirs) { + return { stdout: venvDirs[cmd.env] }; + } + throw new Error('Command failed'); + }; +} + +const testHatchDir = path.join(TEST_LAYOUT_ROOT, 'hatch'); +// This is usually in /hatch, e.g. `~/.local/share/hatch` +const hatchEnvsDir = path.join(testHatchDir, 'env/virtual/python'); +export const projectDirs = { + project1: path.join(testHatchDir, 'project1'), + project2: path.join(testHatchDir, 'project2'), +}; +export const venvDirs = { + project1: { default: path.join(hatchEnvsDir, 'cK2g6fIm/project1') }, + project2: { + default: path.join(hatchEnvsDir, 'q4In3tK-/project2'), + test: path.join(hatchEnvsDir, 'q4In3tK-/test'), + }, +}; + +suite('Hatch binary is located correctly', async () => { + let exec: sinon.SinonStub; + let getPythonSetting: sinon.SinonStub; + + setup(() => { + getPythonSetting = sinon.stub(externalDependencies, 'getPythonSetting'); + exec = sinon.stub(externalDependencies, 'exec'); + }); + + teardown(() => { + sinon.restore(); + }); + + const testPath = async (verify = true) => { + // If `verify` is false, don’t verify that the command has been called with that path + exec.callsFake( + makeExecHandler(venvDirs.project1, verify ? { path: true, cwd: projectDirs.project1 } : undefined), + ); + const hatch = await Hatch.getHatch(projectDirs.project1); + expect(hatch?.command).to.equal('hatch'); + }; + + test('Use Hatch on PATH if available', () => testPath()); + + test('Return undefined if Hatch cannot be found', async () => { + getPythonSetting.returns('hatch'); + exec.callsFake((_file: string, _args: string[], _options: ShellOptions) => + Promise.reject(new Error('Command failed')), + ); + const hatch = await Hatch.getHatch(projectDirs.project1); + expect(hatch?.command).to.equal(undefined); + }); +}); diff --git a/src/test/pythonEnvironments/common/environmentManagers/pixi.unit.test.ts b/src/test/pythonEnvironments/common/environmentManagers/pixi.unit.test.ts new file mode 100644 index 000000000000..0cbc6b25145c --- /dev/null +++ b/src/test/pythonEnvironments/common/environmentManagers/pixi.unit.test.ts @@ -0,0 +1,147 @@ +import { expect } from 'chai'; +import * as path from 'path'; +import * as sinon from 'sinon'; +import { ExecutionResult, ShellOptions } from '../../../../client/common/process/types'; +import * as externalDependencies from '../../../../client/pythonEnvironments/common/externalDependencies'; +import { TEST_LAYOUT_ROOT } from '../commonTestConstants'; +import { getPixi } from '../../../../client/pythonEnvironments/common/environmentManagers/pixi'; + +export type PixiCommand = { cmd: 'info --json' } | { cmd: '--version' } | { cmd: null }; + +const textPixiDir = path.join(TEST_LAYOUT_ROOT, 'pixi'); +export const projectDirs = { + windows: { + path: path.join(textPixiDir, 'windows'), + info: { + environments_info: [ + { + prefix: path.join(textPixiDir, 'windows', '.pixi', 'envs', 'default'), + }, + ], + }, + }, + nonWindows: { + path: path.join(textPixiDir, 'non-windows'), + info: { + environments_info: [ + { + prefix: path.join(textPixiDir, 'non-windows', '.pixi', 'envs', 'default'), + }, + ], + }, + }, + multiEnv: { + path: path.join(textPixiDir, 'multi-env'), + info: { + environments_info: [ + { + prefix: path.join(textPixiDir, 'multi-env', '.pixi', 'envs', 'default'), + }, + { + prefix: path.join(textPixiDir, 'multi-env', '.pixi', 'envs', 'py310'), + }, + { + prefix: path.join(textPixiDir, 'multi-env', '.pixi', 'envs', 'py311'), + }, + ], + }, + }, +}; + +/** + * Convert the command line arguments into a typed command. + */ +export function pixiCommand(args: string[]): PixiCommand { + if (args[0] === '--version') { + return { cmd: '--version' }; + } + + if (args.length < 2) { + return { cmd: null }; + } + if (args[0] === 'info' && args[1] === '--json') { + return { cmd: 'info --json' }; + } + return { cmd: null }; +} +interface VerifyOptions { + pixiPath?: string; + cwd?: string; +} + +export function makeExecHandler(verify: VerifyOptions = {}) { + return async (file: string, args: string[], options: ShellOptions): Promise> => { + /// Verify that the executable path is indeed the one we expect it to be + if (verify.pixiPath && file !== verify.pixiPath) { + throw new Error('Command failed: not the correct pixi path'); + } + + const cmd = pixiCommand(args); + if (cmd.cmd === '--version') { + return { stdout: 'pixi 0.24.1' }; + } + + /// Verify that the working directory is the expected one + const cwd = typeof options.cwd === 'string' ? options.cwd : options.cwd?.toString(); + if (verify.cwd) { + if (!cwd || !externalDependencies.arePathsSame(cwd, verify.cwd)) { + throw new Error(`Command failed: not the correct path, expected: ${verify.cwd}, got: ${cwd}`); + } + } + + /// Convert the command into a single string + if (cmd.cmd === 'info --json') { + const project = Object.values(projectDirs).find((p) => cwd?.startsWith(p.path)); + if (!project) { + throw new Error('Command failed: could not find project'); + } + return { stdout: JSON.stringify(project.info) }; + } + + throw new Error(`Command failed: unknown command ${args}`); + }; +} + +suite('Pixi binary is located correctly', async () => { + let exec: sinon.SinonStub; + let getPythonSetting: sinon.SinonStub; + let pathExists: sinon.SinonStub; + + setup(() => { + getPythonSetting = sinon.stub(externalDependencies, 'getPythonSetting'); + exec = sinon.stub(externalDependencies, 'exec'); + pathExists = sinon.stub(externalDependencies, 'pathExists'); + }); + + teardown(() => { + sinon.restore(); + }); + + const testPath = async (pixiPath: string, verify = true) => { + getPythonSetting.returns(pixiPath); + pathExists.returns(pixiPath !== 'pixi'); + // If `verify` is false, don’t verify that the command has been called with that path + exec.callsFake(makeExecHandler(verify ? { pixiPath } : undefined)); + const pixi = await getPixi(); + + if (pixiPath === 'pixi') { + expect(pixi).to.equal(undefined); + } else { + expect(pixi?.command).to.equal(pixiPath); + } + }; + + test('Return a Pixi instance in an empty directory', () => testPath('pixiPath', false)); + test('When user has specified a valid Pixi path, use it', () => testPath('path/to/pixi/binary')); + // 'pixi' is the default value + test('When user hasn’t specified a path, use Pixi on PATH if available', () => testPath('pixi')); + + test('Return undefined if Pixi cannot be found', async () => { + getPythonSetting.returns('pixi'); + exec.callsFake((_file: string, _args: string[], _options: ShellOptions) => + Promise.reject(new Error('Command failed')), + ); + const pixi = await getPixi(); + expect(pixi?.command).to.equal(undefined); + }); +}); diff --git a/src/test/pythonEnvironments/common/environmentManagers/simplevirtualenvs.unit.test.ts b/src/test/pythonEnvironments/common/environmentManagers/simplevirtualenvs.unit.test.ts index 8ffb64b741a3..6d75668b8556 100644 --- a/src/test/pythonEnvironments/common/environmentManagers/simplevirtualenvs.unit.test.ts +++ b/src/test/pythonEnvironments/common/environmentManagers/simplevirtualenvs.unit.test.ts @@ -2,9 +2,9 @@ // Licensed under the MIT License. import * as assert from 'assert'; -import * as fsapi from 'fs-extra'; import * as path from 'path'; import * as sinon from 'sinon'; +import * as fsapi from '../../../../client/common/platform/fs-paths'; import * as platformUtils from '../../../../client/common/utils/platform'; import { PythonReleaseLevel, PythonVersion } from '../../../../client/pythonEnvironments/base/info'; import * as fileUtils from '../../../../client/pythonEnvironments/common/externalDependencies'; diff --git a/src/test/pythonEnvironments/common/envlayouts/hatch/env/virtual/python/cK2g6fIm/project1/Scripts/python.exe b/src/test/pythonEnvironments/common/envlayouts/hatch/env/virtual/python/cK2g6fIm/project1/Scripts/python.exe new file mode 100644 index 000000000000..a37b666d049e --- /dev/null +++ b/src/test/pythonEnvironments/common/envlayouts/hatch/env/virtual/python/cK2g6fIm/project1/Scripts/python.exe @@ -0,0 +1 @@ +Not real python exe diff --git a/src/test/pythonFiles/environments/conda/bin/python b/src/test/pythonEnvironments/common/envlayouts/hatch/env/virtual/python/cK2g6fIm/project1/bin/python similarity index 100% rename from src/test/pythonFiles/environments/conda/bin/python rename to src/test/pythonEnvironments/common/envlayouts/hatch/env/virtual/python/cK2g6fIm/project1/bin/python diff --git a/src/test/pythonEnvironments/common/envlayouts/hatch/env/virtual/python/cK2g6fIm/project1/pyvenv.cfg b/src/test/pythonEnvironments/common/envlayouts/hatch/env/virtual/python/cK2g6fIm/project1/pyvenv.cfg new file mode 100644 index 000000000000..365d6f5eacee --- /dev/null +++ b/src/test/pythonEnvironments/common/envlayouts/hatch/env/virtual/python/cK2g6fIm/project1/pyvenv.cfg @@ -0,0 +1,3 @@ +home = /usr/bin/python3.11 +include-system-site-packages = false +version = 3.11.1 diff --git a/src/test/pythonFiles/environments/conda/envs/numpy/bin/python b/src/test/pythonEnvironments/common/envlayouts/hatch/env/virtual/python/q4In3tK-/project2/bin/python similarity index 100% rename from src/test/pythonFiles/environments/conda/envs/numpy/bin/python rename to src/test/pythonEnvironments/common/envlayouts/hatch/env/virtual/python/q4In3tK-/project2/bin/python diff --git a/src/test/pythonEnvironments/common/envlayouts/hatch/env/virtual/python/q4In3tK-/project2/pyvenv.cfg b/src/test/pythonEnvironments/common/envlayouts/hatch/env/virtual/python/q4In3tK-/project2/pyvenv.cfg new file mode 100644 index 000000000000..a67a28be91b5 --- /dev/null +++ b/src/test/pythonEnvironments/common/envlayouts/hatch/env/virtual/python/q4In3tK-/project2/pyvenv.cfg @@ -0,0 +1,3 @@ +home = /usr/bin/python3.10 +include-system-site-packages = false +version = 3.10.3 diff --git a/src/test/pythonFiles/environments/conda/envs/scipy/bin/python b/src/test/pythonEnvironments/common/envlayouts/hatch/env/virtual/python/q4In3tK-/test/bin/python similarity index 100% rename from src/test/pythonFiles/environments/conda/envs/scipy/bin/python rename to src/test/pythonEnvironments/common/envlayouts/hatch/env/virtual/python/q4In3tK-/test/bin/python diff --git a/src/test/pythonEnvironments/common/envlayouts/hatch/env/virtual/python/q4In3tK-/test/pyvenv.cfg b/src/test/pythonEnvironments/common/envlayouts/hatch/env/virtual/python/q4In3tK-/test/pyvenv.cfg new file mode 100644 index 000000000000..a67a28be91b5 --- /dev/null +++ b/src/test/pythonEnvironments/common/envlayouts/hatch/env/virtual/python/q4In3tK-/test/pyvenv.cfg @@ -0,0 +1,3 @@ +home = /usr/bin/python3.10 +include-system-site-packages = false +version = 3.10.3 diff --git a/pythonFiles/tests/testing_tools/adapter/.data/complex/tests/x/__init__.py b/src/test/pythonEnvironments/common/envlayouts/hatch/project1/.gitkeep similarity index 100% rename from pythonFiles/tests/testing_tools/adapter/.data/complex/tests/x/__init__.py rename to src/test/pythonEnvironments/common/envlayouts/hatch/project1/.gitkeep diff --git a/src/test/pythonEnvironments/common/envlayouts/hatch/project2/hatch.toml b/src/test/pythonEnvironments/common/envlayouts/hatch/project2/hatch.toml new file mode 100644 index 000000000000..9848374b54fd --- /dev/null +++ b/src/test/pythonEnvironments/common/envlayouts/hatch/project2/hatch.toml @@ -0,0 +1,6 @@ +# this file is not actually used in tests, as all is mocked out + +# The default environment always exists +#[envs.default] + +[envs.test] diff --git a/src/test/pythonEnvironments/common/envlayouts/pipenv/project1/CustomPipfileName b/src/test/pythonEnvironments/common/envlayouts/pipenv/project1/CustomPipfileName index b723d0199f86..b5846df18ca8 100644 --- a/src/test/pythonEnvironments/common/envlayouts/pipenv/project1/CustomPipfileName +++ b/src/test/pythonEnvironments/common/envlayouts/pipenv/project1/CustomPipfileName @@ -8,4 +8,4 @@ verify_ssl = true [packages] [requires] -python_version = "3.7" +python_version = "3.8" diff --git a/src/test/pythonEnvironments/common/envlayouts/pipenv/project2/Pipfile b/src/test/pythonEnvironments/common/envlayouts/pipenv/project2/Pipfile index b723d0199f86..b5846df18ca8 100644 --- a/src/test/pythonEnvironments/common/envlayouts/pipenv/project2/Pipfile +++ b/src/test/pythonEnvironments/common/envlayouts/pipenv/project2/Pipfile @@ -8,4 +8,4 @@ verify_ssl = true [packages] [requires] -python_version = "3.7" +python_version = "3.8" diff --git a/src/test/pythonEnvironments/common/envlayouts/pipenv/project3/Pipfile b/src/test/pythonEnvironments/common/envlayouts/pipenv/project3/Pipfile index b723d0199f86..b5846df18ca8 100644 --- a/src/test/pythonEnvironments/common/envlayouts/pipenv/project3/Pipfile +++ b/src/test/pythonEnvironments/common/envlayouts/pipenv/project3/Pipfile @@ -8,4 +8,4 @@ verify_ssl = true [packages] [requires] -python_version = "3.7" +python_version = "3.8" diff --git a/src/test/pythonEnvironments/common/envlayouts/pixi/multi-env/.pixi/envs/py310/bin/python b/src/test/pythonEnvironments/common/envlayouts/pixi/multi-env/.pixi/envs/py310/bin/python new file mode 100644 index 000000000000..a37b666d049e --- /dev/null +++ b/src/test/pythonEnvironments/common/envlayouts/pixi/multi-env/.pixi/envs/py310/bin/python @@ -0,0 +1 @@ +Not real python exe diff --git a/pythonFiles/tests/testing_tools/adapter/.data/complex/tests/x/y/__init__.py b/src/test/pythonEnvironments/common/envlayouts/pixi/multi-env/.pixi/envs/py310/conda-meta/pixi similarity index 100% rename from pythonFiles/tests/testing_tools/adapter/.data/complex/tests/x/y/__init__.py rename to src/test/pythonEnvironments/common/envlayouts/pixi/multi-env/.pixi/envs/py310/conda-meta/pixi diff --git a/src/test/pythonEnvironments/common/envlayouts/pixi/multi-env/.pixi/envs/py311/bin/python b/src/test/pythonEnvironments/common/envlayouts/pixi/multi-env/.pixi/envs/py311/bin/python new file mode 100644 index 000000000000..a37b666d049e --- /dev/null +++ b/src/test/pythonEnvironments/common/envlayouts/pixi/multi-env/.pixi/envs/py311/bin/python @@ -0,0 +1 @@ +Not real python exe diff --git a/pythonFiles/tests/testing_tools/adapter/.data/complex/tests/x/y/z/__init__.py b/src/test/pythonEnvironments/common/envlayouts/pixi/multi-env/.pixi/envs/py311/conda-meta/pixi similarity index 100% rename from pythonFiles/tests/testing_tools/adapter/.data/complex/tests/x/y/z/__init__.py rename to src/test/pythonEnvironments/common/envlayouts/pixi/multi-env/.pixi/envs/py311/conda-meta/pixi diff --git a/pythonFiles/tests/testing_tools/adapter/.data/complex/tests/x/y/z/a/__init__.py b/src/test/pythonEnvironments/common/envlayouts/pixi/multi-env/.pixi/envs/py311/python similarity index 100% rename from pythonFiles/tests/testing_tools/adapter/.data/complex/tests/x/y/z/a/__init__.py rename to src/test/pythonEnvironments/common/envlayouts/pixi/multi-env/.pixi/envs/py311/python diff --git a/src/test/pythonEnvironments/common/envlayouts/pixi/multi-env/pixi.toml b/src/test/pythonEnvironments/common/envlayouts/pixi/multi-env/pixi.toml new file mode 100644 index 000000000000..9b93e638e9ab --- /dev/null +++ b/src/test/pythonEnvironments/common/envlayouts/pixi/multi-env/pixi.toml @@ -0,0 +1,14 @@ +[project] +name = "multi-env" +channels = ["conda-forge"] +platforms = ["win-64"] + +[feature.py310.dependencies] +python = "~=3.10" + +[feature.py311.dependencies] +python = "~=3.11" + +[environments] +py310 = ["py310"] +py311 = ["py311"] diff --git a/pythonFiles/tests/testing_tools/adapter/.data/complex/tests/x/y/z/b/__init__.py b/src/test/pythonEnvironments/common/envlayouts/pixi/non-windows/.pixi/envs/default/bin/python similarity index 100% rename from pythonFiles/tests/testing_tools/adapter/.data/complex/tests/x/y/z/b/__init__.py rename to src/test/pythonEnvironments/common/envlayouts/pixi/non-windows/.pixi/envs/default/bin/python diff --git a/pythonFiles/tests/testing_tools/adapter/.data/notests/tests/__init__.py b/src/test/pythonEnvironments/common/envlayouts/pixi/non-windows/.pixi/envs/default/conda-meta/pixi similarity index 100% rename from pythonFiles/tests/testing_tools/adapter/.data/notests/tests/__init__.py rename to src/test/pythonEnvironments/common/envlayouts/pixi/non-windows/.pixi/envs/default/conda-meta/pixi diff --git a/src/test/pythonEnvironments/common/envlayouts/pixi/non-windows/pixi.toml b/src/test/pythonEnvironments/common/envlayouts/pixi/non-windows/pixi.toml new file mode 100644 index 000000000000..f11ab3b42360 --- /dev/null +++ b/src/test/pythonEnvironments/common/envlayouts/pixi/non-windows/pixi.toml @@ -0,0 +1,11 @@ +[project] +name = "non-windows" +version = "0.1.0" +description = "Add a short description here" +authors = ["Bas Zalmstra "] +channels = ["conda-forge"] +platforms = ["win-64"] + +[tasks] + +[dependencies] diff --git a/src/test/pythonEnvironments/common/envlayouts/pixi/windows/.pixi/envs/default/python.exe b/src/test/pythonEnvironments/common/envlayouts/pixi/windows/.pixi/envs/default/python.exe new file mode 100644 index 000000000000..a37b666d049e --- /dev/null +++ b/src/test/pythonEnvironments/common/envlayouts/pixi/windows/.pixi/envs/default/python.exe @@ -0,0 +1 @@ +Not real python exe diff --git a/src/test/pythonEnvironments/common/envlayouts/pixi/windows/pixi.toml b/src/test/pythonEnvironments/common/envlayouts/pixi/windows/pixi.toml new file mode 100644 index 000000000000..1341496c5590 --- /dev/null +++ b/src/test/pythonEnvironments/common/envlayouts/pixi/windows/pixi.toml @@ -0,0 +1,12 @@ +[project] +name = "windows" +version = "0.1.0" +description = "Add a short description here" +authors = ["Bas Zalmstra "] +channels = ["conda-forge"] +platforms = ["win-64"] + +[tasks] + +[dependencies] +python = "~=3.8.0" diff --git a/src/test/pythonEnvironments/common/envlayouts/workspace/folder1/Pipfile b/src/test/pythonEnvironments/common/envlayouts/workspace/folder1/Pipfile index b723d0199f86..b5846df18ca8 100644 --- a/src/test/pythonEnvironments/common/envlayouts/workspace/folder1/Pipfile +++ b/src/test/pythonEnvironments/common/envlayouts/workspace/folder1/Pipfile @@ -8,4 +8,4 @@ verify_ssl = true [packages] [requires] -python_version = "3.7" +python_version = "3.8" diff --git a/src/test/pythonEnvironments/creation/common/installCheckUtils.unit.test.ts b/src/test/pythonEnvironments/creation/common/installCheckUtils.unit.test.ts new file mode 100644 index 000000000000..2900b9b89c8f --- /dev/null +++ b/src/test/pythonEnvironments/creation/common/installCheckUtils.unit.test.ts @@ -0,0 +1,118 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License + +import * as chaiAsPromised from 'chai-as-promised'; +import * as sinon from 'sinon'; +import * as typemoq from 'typemoq'; +import { assert, use as chaiUse } from 'chai'; +import { Diagnostic, TextDocument, Range, Uri, WorkspaceConfiguration, ConfigurationScope } from 'vscode'; +import * as rawProcessApis from '../../../../client/common/process/rawProcessApis'; +import { getInstalledPackagesDiagnostics } from '../../../../client/pythonEnvironments/creation/common/installCheckUtils'; +import * as workspaceApis from '../../../../client/common/vscodeApis/workspaceApis'; +import { SpawnOptions } from '../../../../client/common/process/types'; +import { IInterpreterService } from '../../../../client/interpreter/contracts'; +import { PythonEnvironment } from '../../../../client/pythonEnvironments/info'; + +chaiUse(chaiAsPromised.default); + +function getSomeRequirementFile(): typemoq.IMock { + const someFilePath = 'requirements.txt'; + const someFile = typemoq.Mock.ofType(); + someFile.setup((p) => p.languageId).returns(() => 'pip-requirements'); + someFile.setup((p) => p.fileName).returns(() => someFilePath); + someFile.setup((p) => p.getText(typemoq.It.isAny())).returns(() => 'flake8-csv'); + return someFile; +} + +const MISSING_PACKAGES_STR = + '[{"line": 8, "character": 34, "endLine": 8, "endCharacter": 44, "package": "flake8-csv", "code": "not-installed", "severity": 3}]'; +const MISSING_PACKAGES: Diagnostic[] = [ + { + range: new Range(8, 34, 8, 44), + message: 'Package `flake8-csv` is not installed in the selected environment.', + source: 'Python-InstalledPackagesChecker', + code: { value: 'not-installed', target: Uri.parse(`https://pypi.org/p/flake8-csv`) }, + severity: 3, + relatedInformation: [], + }, +]; + +suite('Install check diagnostics tests', () => { + let plainExecStub: sinon.SinonStub; + let interpreterService: typemoq.IMock; + let getConfigurationStub: sinon.SinonStub; + let configMock: typemoq.IMock; + + setup(() => { + configMock = typemoq.Mock.ofType(); + plainExecStub = sinon.stub(rawProcessApis, 'plainExec'); + interpreterService = typemoq.Mock.ofType(); + interpreterService + .setup((i) => i.getActiveInterpreter(typemoq.It.isAny())) + .returns(() => Promise.resolve(({ path: 'python' } as unknown) as PythonEnvironment)); + getConfigurationStub = sinon.stub(workspaceApis, 'getConfiguration'); + getConfigurationStub.callsFake((section?: string, _scope?: ConfigurationScope | null) => { + if (section === 'python') { + return configMock.object; + } + return undefined; + }); + }); + + teardown(() => { + sinon.restore(); + }); + + test('Test parse diagnostics', async () => { + configMock + .setup((c) => c.get('missingPackage.severity', 'Hint')) + .returns(() => 'Error') + .verifiable(typemoq.Times.atLeastOnce()); + plainExecStub.resolves({ stdout: MISSING_PACKAGES_STR, stderr: '' }); + const someFile = getSomeRequirementFile(); + const result = await getInstalledPackagesDiagnostics(interpreterService.object, someFile.object); + + assert.deepStrictEqual(result, MISSING_PACKAGES); + configMock.verifyAll(); + }); + + test('Test parse empty diagnostics', async () => { + configMock + .setup((c) => c.get('missingPackage.severity', 'Hint')) + .returns(() => 'Error') + .verifiable(typemoq.Times.atLeastOnce()); + plainExecStub.resolves({ stdout: '', stderr: '' }); + const someFile = getSomeRequirementFile(); + const result = await getInstalledPackagesDiagnostics(interpreterService.object, someFile.object); + + assert.deepStrictEqual(result, []); + configMock.verifyAll(); + }); + + [ + ['Error', '0'], + ['Warning', '1'], + ['Information', '2'], + ['Hint', '3'], + ].forEach((severityType: string[]) => { + const setting = severityType[0]; + const expected = severityType[1]; + test(`Test missing package severity: ${setting}`, async () => { + configMock + .setup((c) => c.get('missingPackage.severity', 'Hint')) + .returns(() => setting) + .verifiable(typemoq.Times.atLeastOnce()); + let severity: string | undefined; + plainExecStub.callsFake((_cmd: string, _args: string[], options: SpawnOptions) => { + severity = options.env?.VSCODE_MISSING_PGK_SEVERITY; + return { stdout: '', stderr: '' }; + }); + const someFile = getSomeRequirementFile(); + const result = await getInstalledPackagesDiagnostics(interpreterService.object, someFile.object); + + assert.deepStrictEqual(result, []); + assert.deepStrictEqual(severity, expected); + configMock.verifyAll(); + }); + }); +}); diff --git a/src/test/pythonEnvironments/creation/common/workspaceSelection.unit.test.ts b/src/test/pythonEnvironments/creation/common/workspaceSelection.unit.test.ts index 57047db1d2bc..1d3df521fd0a 100644 --- a/src/test/pythonEnvironments/creation/common/workspaceSelection.unit.test.ts +++ b/src/test/pythonEnvironments/creation/common/workspaceSelection.unit.test.ts @@ -12,7 +12,7 @@ import { pickWorkspaceFolder } from '../../../../client/pythonEnvironments/creat import * as windowApis from '../../../../client/common/vscodeApis/windowApis'; import { EXTENSION_ROOT_DIR_FOR_TESTS } from '../../../constants'; -chaiUse(chaiAsPromised); +chaiUse(chaiAsPromised.default); suite('Create environment workspace selection tests', () => { let showQuickPickWithBackStub: sinon.SinonStub; diff --git a/src/test/pythonEnvironments/creation/createEnvApi.unit.test.ts b/src/test/pythonEnvironments/creation/createEnvApi.unit.test.ts index 786bd26a881c..dd09203d65cc 100644 --- a/src/test/pythonEnvironments/creation/createEnvApi.unit.test.ts +++ b/src/test/pythonEnvironments/creation/createEnvApi.unit.test.ts @@ -7,15 +7,18 @@ import * as sinon from 'sinon'; import * as typemoq from 'typemoq'; import { assert, use as chaiUse } from 'chai'; import { ConfigurationTarget, Uri } from 'vscode'; -import { IDisposableRegistry, IInterpreterPathService, IPathUtils } from '../../../client/common/types'; +import { IDisposableRegistry, IPathUtils } from '../../../client/common/types'; import * as commandApis from '../../../client/common/vscodeApis/commandApis'; -import { IInterpreterQuickPick } from '../../../client/interpreter/configuration/types'; +import { + IInterpreterQuickPick, + IPythonPathUpdaterServiceManager, +} from '../../../client/interpreter/configuration/types'; import { registerCreateEnvironmentFeatures } from '../../../client/pythonEnvironments/creation/createEnvApi'; import * as windowApis from '../../../client/common/vscodeApis/windowApis'; import { handleCreateEnvironmentCommand } from '../../../client/pythonEnvironments/creation/createEnvironment'; import { CreateEnvironmentProvider } from '../../../client/pythonEnvironments/creation/proposed.createEnvApis'; -chaiUse(chaiAsPromised); +chaiUse(chaiAsPromised.default); suite('Create Environment APIs', () => { let registerCommandStub: sinon.SinonStub; @@ -23,7 +26,7 @@ suite('Create Environment APIs', () => { let showInformationMessageStub: sinon.SinonStub; const disposables: IDisposableRegistry = []; let interpreterQuickPick: typemoq.IMock; - let interpreterPathService: typemoq.IMock; + let interpreterPathService: typemoq.IMock; let pathUtils: typemoq.IMock; setup(() => { @@ -32,7 +35,7 @@ suite('Create Environment APIs', () => { registerCommandStub = sinon.stub(commandApis, 'registerCommand'); interpreterQuickPick = typemoq.Mock.ofType(); - interpreterPathService = typemoq.Mock.ofType(); + interpreterPathService = typemoq.Mock.ofType(); pathUtils = typemoq.Mock.ofType(); registerCommandStub.callsFake((_command: string, _callback: (...args: any[]) => any) => ({ @@ -82,10 +85,11 @@ suite('Create Environment APIs', () => { interpreterPathService .setup((p) => - p.update( - typemoq.It.isAny(), - ConfigurationTarget.WorkspaceFolder, + p.updatePythonPath( typemoq.It.isValue('/path/to/env'), + ConfigurationTarget.WorkspaceFolder, + 'ui', + typemoq.It.isAny(), ), ) .returns(() => Promise.resolve()) diff --git a/src/test/pythonEnvironments/creation/createEnvButtonContext.unit.test.ts b/src/test/pythonEnvironments/creation/createEnvButtonContext.unit.test.ts new file mode 100644 index 000000000000..b666191b37bf --- /dev/null +++ b/src/test/pythonEnvironments/creation/createEnvButtonContext.unit.test.ts @@ -0,0 +1,105 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import * as chaiAsPromised from 'chai-as-promised'; +import * as sinon from 'sinon'; +import * as typemoq from 'typemoq'; +import { assert, use as chaiUse } from 'chai'; +import { WorkspaceConfiguration } from 'vscode'; +import * as cmdApis from '../../../client/common/vscodeApis/commandApis'; +import * as workspaceApis from '../../../client/common/vscodeApis/workspaceApis'; +import { IDisposableRegistry } from '../../../client/common/types'; +import { registerCreateEnvironmentButtonFeatures } from '../../../client/pythonEnvironments/creation/createEnvButtonContext'; + +chaiUse(chaiAsPromised.default); + +class FakeDisposable { + public dispose() { + // Do nothing + } +} + +suite('Create Env content button settings tests', () => { + let executeCommandStub: sinon.SinonStub; + const disposables: IDisposableRegistry = []; + let onDidChangeConfigurationStub: sinon.SinonStub; + let getConfigurationStub: sinon.SinonStub; + let configMock: typemoq.IMock; + + setup(() => { + executeCommandStub = sinon.stub(cmdApis, 'executeCommand'); + getConfigurationStub = sinon.stub(workspaceApis, 'getConfiguration'); + onDidChangeConfigurationStub = sinon.stub(workspaceApis, 'onDidChangeConfiguration'); + onDidChangeConfigurationStub.returns(new FakeDisposable()); + + configMock = typemoq.Mock.ofType(); + configMock.setup((c) => c.get(typemoq.It.isAny(), typemoq.It.isAny())).returns(() => 'show'); + getConfigurationStub.returns(configMock.object); + }); + + teardown(() => { + sinon.restore(); + disposables.forEach((d) => d.dispose()); + }); + + test('python.createEnvironment.contentButton setting is set to "show", no files open', async () => { + registerCreateEnvironmentButtonFeatures(disposables); + + assert.ok(executeCommandStub.calledWithExactly('setContext', 'showCreateEnvButton', true)); + }); + + test('python.createEnvironment.contentButton setting is set to "hide", no files open', async () => { + configMock.reset(); + configMock.setup((c) => c.get(typemoq.It.isAny(), typemoq.It.isAny())).returns(() => 'hide'); + + registerCreateEnvironmentButtonFeatures(disposables); + + assert.ok(executeCommandStub.calledWithExactly('setContext', 'showCreateEnvButton', false)); + }); + + test('python.createEnvironment.contentButton setting changed from "hide" to "show"', async () => { + configMock.reset(); + configMock.setup((c) => c.get(typemoq.It.isAny(), typemoq.It.isAny())).returns(() => 'hide'); + + let handler: () => void = () => { + /* do nothing */ + }; + onDidChangeConfigurationStub.callsFake((callback) => { + handler = callback; + return new FakeDisposable(); + }); + + registerCreateEnvironmentButtonFeatures(disposables); + assert.ok(executeCommandStub.calledWithExactly('setContext', 'showCreateEnvButton', false)); + executeCommandStub.reset(); + + configMock.reset(); + configMock.setup((c) => c.get(typemoq.It.isAny(), typemoq.It.isAny())).returns(() => 'show'); + handler(); + + assert.ok(executeCommandStub.calledWithExactly('setContext', 'showCreateEnvButton', true)); + }); + + test('python.createEnvironment.contentButton setting changed from "show" to "hide"', async () => { + configMock.reset(); + configMock.setup((c) => c.get(typemoq.It.isAny(), typemoq.It.isAny())).returns(() => 'show'); + + let handler: () => void = () => { + /* do nothing */ + }; + onDidChangeConfigurationStub.callsFake((callback) => { + handler = callback; + return new FakeDisposable(); + }); + + registerCreateEnvironmentButtonFeatures(disposables); + assert.ok(executeCommandStub.calledWithExactly('setContext', 'showCreateEnvButton', true)); + executeCommandStub.reset(); + + configMock.reset(); + configMock.setup((c) => c.get(typemoq.It.isAny(), typemoq.It.isAny())).returns(() => 'hide'); + handler(); + + assert.ok(executeCommandStub.calledWithExactly('setContext', 'showCreateEnvButton', false)); + }); +}); diff --git a/src/test/pythonEnvironments/creation/createEnvironment.unit.test.ts b/src/test/pythonEnvironments/creation/createEnvironment.unit.test.ts index f16f81233369..9aa9a606d22f 100644 --- a/src/test/pythonEnvironments/creation/createEnvironment.unit.test.ts +++ b/src/test/pythonEnvironments/creation/createEnvironment.unit.test.ts @@ -12,7 +12,7 @@ import { IDisposableRegistry } from '../../../client/common/types'; import { onCreateEnvironmentStarted } from '../../../client/pythonEnvironments/creation/createEnvApi'; import { CreateEnvironmentProvider } from '../../../client/pythonEnvironments/creation/proposed.createEnvApis'; -chaiUse(chaiAsPromised); +chaiUse(chaiAsPromised.default); suite('Create Environments Tests', () => { let showQuickPickStub: sinon.SinonStub; diff --git a/src/test/pythonEnvironments/creation/createEnvironmentTrigger.unit.test.ts b/src/test/pythonEnvironments/creation/createEnvironmentTrigger.unit.test.ts new file mode 100644 index 000000000000..d4041ef4bb88 --- /dev/null +++ b/src/test/pythonEnvironments/creation/createEnvironmentTrigger.unit.test.ts @@ -0,0 +1,253 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import * as path from 'path'; +import * as sinon from 'sinon'; +import { Uri } from 'vscode'; +import * as triggerUtils from '../../../client/pythonEnvironments/creation/common/createEnvTriggerUtils'; +import * as commonUtils from '../../../client/pythonEnvironments/creation/common/commonUtils'; +import * as windowApis from '../../../client/common/vscodeApis/windowApis'; +import { EXTENSION_ROOT_DIR_FOR_TESTS } from '../../constants'; +import { + CreateEnvironmentCheckKind, + triggerCreateEnvironmentCheck, +} from '../../../client/pythonEnvironments/creation/createEnvironmentTrigger'; +import * as workspaceApis from '../../../client/common/vscodeApis/workspaceApis'; +import * as commandApis from '../../../client/common/vscodeApis/commandApis'; +import { Commands } from '../../../client/common/constants'; +import { Common, CreateEnv } from '../../../client/common/utils/localize'; + +suite('Create Environment Trigger', () => { + let shouldPromptToCreateEnvStub: sinon.SinonStub; + let hasVenvStub: sinon.SinonStub; + let hasPrefixCondaEnvStub: sinon.SinonStub; + let hasRequirementFilesStub: sinon.SinonStub; + let hasKnownFilesStub: sinon.SinonStub; + let isGlobalPythonSelectedStub: sinon.SinonStub; + let showInformationMessageStub: sinon.SinonStub; + let isCreateEnvWorkspaceCheckNotRunStub: sinon.SinonStub; + let getWorkspaceFolderStub: sinon.SinonStub; + let executeCommandStub: sinon.SinonStub; + let disableCreateEnvironmentTriggerStub: sinon.SinonStub; + + const workspace1 = { + uri: Uri.file(path.join(EXTENSION_ROOT_DIR_FOR_TESTS, 'src', 'testMultiRootWkspc', 'workspace1')), + name: 'workspace1', + index: 0, + }; + + setup(() => { + shouldPromptToCreateEnvStub = sinon.stub(triggerUtils, 'shouldPromptToCreateEnv'); + hasVenvStub = sinon.stub(commonUtils, 'hasVenv'); + hasPrefixCondaEnvStub = sinon.stub(commonUtils, 'hasPrefixCondaEnv'); + hasRequirementFilesStub = sinon.stub(triggerUtils, 'hasRequirementFiles'); + hasKnownFilesStub = sinon.stub(triggerUtils, 'hasKnownFiles'); + isGlobalPythonSelectedStub = sinon.stub(triggerUtils, 'isGlobalPythonSelected'); + showInformationMessageStub = sinon.stub(windowApis, 'showInformationMessage'); + + isCreateEnvWorkspaceCheckNotRunStub = sinon.stub(triggerUtils, 'isCreateEnvWorkspaceCheckNotRun'); + isCreateEnvWorkspaceCheckNotRunStub.returns(true); + + getWorkspaceFolderStub = sinon.stub(workspaceApis, 'getWorkspaceFolder'); + getWorkspaceFolderStub.returns(workspace1); + + executeCommandStub = sinon.stub(commandApis, 'executeCommand'); + disableCreateEnvironmentTriggerStub = sinon.stub(triggerUtils, 'disableCreateEnvironmentTrigger'); + }); + + teardown(() => { + sinon.restore(); + }); + + test('No Uri', async () => { + await triggerCreateEnvironmentCheck(CreateEnvironmentCheckKind.Workspace, undefined); + sinon.assert.notCalled(shouldPromptToCreateEnvStub); + }); + + test('Should not perform checks if user set trigger to "off"', async () => { + shouldPromptToCreateEnvStub.returns(false); + + await triggerCreateEnvironmentCheck(CreateEnvironmentCheckKind.Workspace, workspace1.uri); + + sinon.assert.calledOnce(shouldPromptToCreateEnvStub); + sinon.assert.notCalled(hasVenvStub); + sinon.assert.notCalled(hasPrefixCondaEnvStub); + sinon.assert.notCalled(hasRequirementFilesStub); + sinon.assert.notCalled(hasKnownFilesStub); + sinon.assert.notCalled(isGlobalPythonSelectedStub); + sinon.assert.notCalled(showInformationMessageStub); + }); + + test('Should not perform checks even if force is true, if user set trigger to "off"', async () => { + shouldPromptToCreateEnvStub.returns(false); + await triggerCreateEnvironmentCheck(CreateEnvironmentCheckKind.Workspace, workspace1.uri, { + force: true, + }); + + sinon.assert.calledOnce(shouldPromptToCreateEnvStub); + sinon.assert.notCalled(hasVenvStub); + sinon.assert.notCalled(hasPrefixCondaEnvStub); + sinon.assert.notCalled(hasRequirementFilesStub); + sinon.assert.notCalled(hasKnownFilesStub); + sinon.assert.notCalled(isGlobalPythonSelectedStub); + sinon.assert.notCalled(showInformationMessageStub); + }); + + test('Should not show prompt if there is a ".venv"', async () => { + shouldPromptToCreateEnvStub.returns(true); + hasVenvStub.resolves(true); + hasPrefixCondaEnvStub.resolves(false); + hasRequirementFilesStub.resolves(true); + hasKnownFilesStub.resolves(false); + isGlobalPythonSelectedStub.resolves(true); + await triggerCreateEnvironmentCheck(CreateEnvironmentCheckKind.Workspace, workspace1.uri); + + sinon.assert.calledOnce(shouldPromptToCreateEnvStub); + sinon.assert.calledOnce(hasVenvStub); + sinon.assert.calledOnce(hasPrefixCondaEnvStub); + sinon.assert.calledOnce(hasRequirementFilesStub); + sinon.assert.calledOnce(hasKnownFilesStub); + sinon.assert.calledOnce(isGlobalPythonSelectedStub); + sinon.assert.notCalled(showInformationMessageStub); + }); + + test('Should not show prompt if there is a ".conda"', async () => { + shouldPromptToCreateEnvStub.returns(true); + hasVenvStub.resolves(false); + hasPrefixCondaEnvStub.resolves(true); + hasRequirementFilesStub.resolves(true); + hasKnownFilesStub.resolves(false); + isGlobalPythonSelectedStub.resolves(true); + await triggerCreateEnvironmentCheck(CreateEnvironmentCheckKind.Workspace, workspace1.uri); + + sinon.assert.calledOnce(shouldPromptToCreateEnvStub); + sinon.assert.calledOnce(hasVenvStub); + sinon.assert.calledOnce(hasPrefixCondaEnvStub); + sinon.assert.calledOnce(hasRequirementFilesStub); + sinon.assert.calledOnce(hasKnownFilesStub); + sinon.assert.calledOnce(isGlobalPythonSelectedStub); + sinon.assert.notCalled(showInformationMessageStub); + }); + + test('Should not show prompt if there are no requirements', async () => { + shouldPromptToCreateEnvStub.returns(true); + hasVenvStub.resolves(false); + hasPrefixCondaEnvStub.resolves(false); + hasRequirementFilesStub.resolves(false); + hasKnownFilesStub.resolves(false); + isGlobalPythonSelectedStub.resolves(true); + await triggerCreateEnvironmentCheck(CreateEnvironmentCheckKind.Workspace, workspace1.uri); + + sinon.assert.calledOnce(shouldPromptToCreateEnvStub); + sinon.assert.calledOnce(hasVenvStub); + sinon.assert.calledOnce(hasPrefixCondaEnvStub); + sinon.assert.calledOnce(hasRequirementFilesStub); + sinon.assert.calledOnce(hasKnownFilesStub); + sinon.assert.calledOnce(isGlobalPythonSelectedStub); + sinon.assert.notCalled(showInformationMessageStub); + }); + + test('Should not show prompt if there are known files', async () => { + shouldPromptToCreateEnvStub.returns(true); + hasVenvStub.resolves(false); + hasPrefixCondaEnvStub.resolves(false); + hasRequirementFilesStub.resolves(false); + hasKnownFilesStub.resolves(true); + isGlobalPythonSelectedStub.resolves(true); + await triggerCreateEnvironmentCheck(CreateEnvironmentCheckKind.Workspace, workspace1.uri); + + sinon.assert.calledOnce(shouldPromptToCreateEnvStub); + sinon.assert.calledOnce(hasVenvStub); + sinon.assert.calledOnce(hasPrefixCondaEnvStub); + sinon.assert.calledOnce(hasRequirementFilesStub); + sinon.assert.calledOnce(hasKnownFilesStub); + sinon.assert.calledOnce(isGlobalPythonSelectedStub); + sinon.assert.notCalled(showInformationMessageStub); + }); + + test('Should not show prompt if selected python is not global', async () => { + shouldPromptToCreateEnvStub.returns(true); + hasVenvStub.resolves(false); + hasPrefixCondaEnvStub.resolves(false); + hasRequirementFilesStub.resolves(true); + hasKnownFilesStub.resolves(false); + isGlobalPythonSelectedStub.resolves(false); + await triggerCreateEnvironmentCheck(CreateEnvironmentCheckKind.Workspace, workspace1.uri); + + sinon.assert.calledOnce(shouldPromptToCreateEnvStub); + sinon.assert.calledOnce(hasVenvStub); + sinon.assert.calledOnce(hasPrefixCondaEnvStub); + sinon.assert.calledOnce(hasRequirementFilesStub); + sinon.assert.calledOnce(hasKnownFilesStub); + sinon.assert.calledOnce(isGlobalPythonSelectedStub); + sinon.assert.notCalled(showInformationMessageStub); + }); + + test('Should show prompt if all conditions met: User closes prompt', async () => { + shouldPromptToCreateEnvStub.returns(true); + hasVenvStub.resolves(false); + hasPrefixCondaEnvStub.resolves(false); + hasRequirementFilesStub.resolves(true); + hasKnownFilesStub.resolves(false); + isGlobalPythonSelectedStub.resolves(true); + showInformationMessageStub.resolves(undefined); + await triggerCreateEnvironmentCheck(CreateEnvironmentCheckKind.Workspace, workspace1.uri); + + sinon.assert.calledOnce(shouldPromptToCreateEnvStub); + sinon.assert.calledOnce(hasVenvStub); + sinon.assert.calledOnce(hasPrefixCondaEnvStub); + sinon.assert.calledOnce(hasRequirementFilesStub); + sinon.assert.calledOnce(hasKnownFilesStub); + sinon.assert.calledOnce(isGlobalPythonSelectedStub); + sinon.assert.calledOnce(showInformationMessageStub); + + sinon.assert.notCalled(executeCommandStub); + sinon.assert.notCalled(disableCreateEnvironmentTriggerStub); + }); + + test('Should show prompt if all conditions met: User clicks create', async () => { + shouldPromptToCreateEnvStub.returns(true); + hasVenvStub.resolves(false); + hasPrefixCondaEnvStub.resolves(false); + hasRequirementFilesStub.resolves(true); + hasKnownFilesStub.resolves(false); + isGlobalPythonSelectedStub.resolves(true); + + showInformationMessageStub.resolves(CreateEnv.Trigger.createEnvironment); + await triggerCreateEnvironmentCheck(CreateEnvironmentCheckKind.Workspace, workspace1.uri); + + sinon.assert.calledOnce(shouldPromptToCreateEnvStub); + sinon.assert.calledOnce(hasVenvStub); + sinon.assert.calledOnce(hasPrefixCondaEnvStub); + sinon.assert.calledOnce(hasRequirementFilesStub); + sinon.assert.calledOnce(hasKnownFilesStub); + sinon.assert.calledOnce(isGlobalPythonSelectedStub); + sinon.assert.calledOnce(showInformationMessageStub); + + sinon.assert.calledOnceWithExactly(executeCommandStub, Commands.Create_Environment); + sinon.assert.notCalled(disableCreateEnvironmentTriggerStub); + }); + + test("Should show prompt if all conditions met: User clicks don't show again", async () => { + shouldPromptToCreateEnvStub.returns(true); + hasVenvStub.resolves(false); + hasPrefixCondaEnvStub.resolves(false); + hasRequirementFilesStub.resolves(true); + hasKnownFilesStub.resolves(false); + isGlobalPythonSelectedStub.resolves(true); + + showInformationMessageStub.resolves(Common.doNotShowAgain); + await triggerCreateEnvironmentCheck(CreateEnvironmentCheckKind.Workspace, workspace1.uri); + + sinon.assert.calledOnce(shouldPromptToCreateEnvStub); + sinon.assert.calledOnce(hasVenvStub); + sinon.assert.calledOnce(hasPrefixCondaEnvStub); + sinon.assert.calledOnce(hasRequirementFilesStub); + sinon.assert.calledOnce(hasKnownFilesStub); + sinon.assert.calledOnce(isGlobalPythonSelectedStub); + sinon.assert.calledOnce(showInformationMessageStub); + + sinon.assert.notCalled(executeCommandStub); + sinon.assert.calledOnce(disableCreateEnvironmentTriggerStub); + }); +}); diff --git a/src/test/pythonEnvironments/creation/globalPipInTerminalTrigger.unit.test.ts b/src/test/pythonEnvironments/creation/globalPipInTerminalTrigger.unit.test.ts new file mode 100644 index 000000000000..2b6a8df91d82 --- /dev/null +++ b/src/test/pythonEnvironments/creation/globalPipInTerminalTrigger.unit.test.ts @@ -0,0 +1,278 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import * as path from 'path'; +import * as sinon from 'sinon'; +import { assert } from 'chai'; +import * as typemoq from 'typemoq'; +import { + Disposable, + Terminal, + TerminalShellExecution, + TerminalShellExecutionStartEvent, + TerminalShellIntegration, + Uri, +} from 'vscode'; +import * as triggerUtils from '../../../client/pythonEnvironments/creation/common/createEnvTriggerUtils'; +import * as windowApis from '../../../client/common/vscodeApis/windowApis'; +import * as workspaceApis from '../../../client/common/vscodeApis/workspaceApis'; +import * as commandApis from '../../../client/common/vscodeApis/commandApis'; +import { registerTriggerForPipInTerminal } from '../../../client/pythonEnvironments/creation/globalPipInTerminalTrigger'; +import { EXTENSION_ROOT_DIR_FOR_TESTS } from '../../constants'; +import { Common, CreateEnv } from '../../../client/common/utils/localize'; + +suite('Global Pip in Terminal Trigger', () => { + let shouldPromptToCreateEnvStub: sinon.SinonStub; + let getWorkspaceFoldersStub: sinon.SinonStub; + let getWorkspaceFolderStub: sinon.SinonStub; + let isGlobalPythonSelectedStub: sinon.SinonStub; + let showWarningMessageStub: sinon.SinonStub; + let executeCommandStub: sinon.SinonStub; + let disableCreateEnvironmentTriggerStub: sinon.SinonStub; + let onDidStartTerminalShellExecutionStub: sinon.SinonStub; + let handler: undefined | ((e: TerminalShellExecutionStartEvent) => Promise); + let execEvent: typemoq.IMock; + let shellIntegration: typemoq.IMock; + + const workspace1 = { + uri: Uri.file(path.join(EXTENSION_ROOT_DIR_FOR_TESTS, 'src', 'testMultiRootWkspc', 'workspace1')), + name: 'workspace1', + index: 0, + }; + + const outsideWorkspace = Uri.file( + path.join(EXTENSION_ROOT_DIR_FOR_TESTS, 'src', 'testMultiRootWkspc', 'outsideWorkspace'), + ); + + setup(() => { + shouldPromptToCreateEnvStub = sinon.stub(triggerUtils, 'shouldPromptToCreateEnv'); + + getWorkspaceFoldersStub = sinon.stub(workspaceApis, 'getWorkspaceFolders'); + getWorkspaceFoldersStub.returns([workspace1]); + + getWorkspaceFolderStub = sinon.stub(workspaceApis, 'getWorkspaceFolder'); + getWorkspaceFolderStub.returns(workspace1); + + isGlobalPythonSelectedStub = sinon.stub(triggerUtils, 'isGlobalPythonSelected'); + showWarningMessageStub = sinon.stub(windowApis, 'showWarningMessage'); + + executeCommandStub = sinon.stub(commandApis, 'executeCommand'); + executeCommandStub.resolves({ path: 'some/python' }); + + disableCreateEnvironmentTriggerStub = sinon.stub(triggerUtils, 'disableCreateEnvironmentTrigger'); + + onDidStartTerminalShellExecutionStub = sinon.stub(windowApis, 'onDidStartTerminalShellExecution'); + onDidStartTerminalShellExecutionStub.callsFake((cb) => { + handler = cb; + return { + dispose: () => { + handler = undefined; + }, + }; + }); + + shellIntegration = typemoq.Mock.ofType(); + execEvent = typemoq.Mock.ofType(); + execEvent.setup((e) => e.shellIntegration).returns(() => shellIntegration.object); + shellIntegration + .setup((s) => s.executeCommand(typemoq.It.isAnyString())) + .returns(() => (({} as unknown) as TerminalShellExecution)); + }); + + teardown(() => { + sinon.restore(); + }); + + test('Should not prompt to create environment if setting is off', async () => { + shouldPromptToCreateEnvStub.returns(false); + + const disposables: Disposable[] = []; + registerTriggerForPipInTerminal(disposables); + + assert.strictEqual(disposables.length, 0); + sinon.assert.calledOnce(shouldPromptToCreateEnvStub); + }); + + test('Should not prompt to create environment if no workspace folders', async () => { + shouldPromptToCreateEnvStub.returns(true); + getWorkspaceFoldersStub.returns([]); + + const disposables: Disposable[] = []; + registerTriggerForPipInTerminal(disposables); + + assert.strictEqual(disposables.length, 0); + sinon.assert.calledOnce(shouldPromptToCreateEnvStub); + sinon.assert.calledOnce(getWorkspaceFoldersStub); + }); + + test('Should not prompt to create environment if workspace folder is not found', async () => { + shouldPromptToCreateEnvStub.returns(true); + getWorkspaceFolderStub.returns(undefined); + + const disposables: Disposable[] = []; + registerTriggerForPipInTerminal(disposables); + + shellIntegration.setup((s) => s.cwd).returns(() => outsideWorkspace); + await handler?.(({ shellIntegration: shellIntegration.object } as unknown) as TerminalShellExecutionStartEvent); + + assert.strictEqual(disposables.length, 1); + sinon.assert.calledOnce(shouldPromptToCreateEnvStub); + sinon.assert.calledOnce(getWorkspaceFolderStub); + sinon.assert.notCalled(isGlobalPythonSelectedStub); + sinon.assert.notCalled(showWarningMessageStub); + }); + + test('Should not prompt to create environment if global python is not selected', async () => { + shouldPromptToCreateEnvStub.returns(true); + isGlobalPythonSelectedStub.returns(false); + + const disposables: Disposable[] = []; + registerTriggerForPipInTerminal(disposables); + + await handler?.(({ shellIntegration: shellIntegration.object } as unknown) as TerminalShellExecutionStartEvent); + + assert.strictEqual(disposables.length, 1); + sinon.assert.calledOnce(shouldPromptToCreateEnvStub); + sinon.assert.calledOnce(getWorkspaceFolderStub); + sinon.assert.calledOnce(isGlobalPythonSelectedStub); + + sinon.assert.notCalled(showWarningMessageStub); + }); + + test('Should not prompt to create environment if command is not trusted', async () => { + shouldPromptToCreateEnvStub.returns(true); + isGlobalPythonSelectedStub.returns(true); + + const disposables: Disposable[] = []; + registerTriggerForPipInTerminal(disposables); + + await handler?.({ + terminal: ({} as unknown) as Terminal, + shellIntegration: shellIntegration.object, + execution: { + cwd: workspace1.uri, + commandLine: { + isTrusted: false, + value: 'pip install', + confidence: 0, + }, + read: () => + (async function* () { + yield Promise.resolve('pip install'); + })(), + }, + }); + + assert.strictEqual(disposables.length, 1); + sinon.assert.calledOnce(shouldPromptToCreateEnvStub); + sinon.assert.calledOnce(getWorkspaceFolderStub); + sinon.assert.calledOnce(isGlobalPythonSelectedStub); + + sinon.assert.notCalled(showWarningMessageStub); + }); + + test('Should not prompt to create environment if command does not start with pip install', async () => { + shouldPromptToCreateEnvStub.returns(true); + isGlobalPythonSelectedStub.returns(true); + + const disposables: Disposable[] = []; + registerTriggerForPipInTerminal(disposables); + + await handler?.({ + terminal: ({} as unknown) as Terminal, + shellIntegration: shellIntegration.object, + execution: { + cwd: workspace1.uri, + commandLine: { + isTrusted: false, + value: 'some command pip install', + confidence: 0, + }, + read: () => + (async function* () { + yield Promise.resolve('pip install'); + })(), + }, + }); + + assert.strictEqual(disposables.length, 1); + sinon.assert.calledOnce(shouldPromptToCreateEnvStub); + sinon.assert.calledOnce(getWorkspaceFolderStub); + sinon.assert.calledOnce(isGlobalPythonSelectedStub); + + sinon.assert.notCalled(showWarningMessageStub); + }); + + ['pip install', 'pip3 install', 'python -m pip install', 'python3 -m pip install'].forEach((command) => { + test(`Should prompt to create environment if all conditions are met: ${command}`, async () => { + shouldPromptToCreateEnvStub.returns(true); + isGlobalPythonSelectedStub.returns(true); + showWarningMessageStub.resolves(CreateEnv.Trigger.createEnvironment); + + const disposables: Disposable[] = []; + registerTriggerForPipInTerminal(disposables); + + await handler?.({ + terminal: ({} as unknown) as Terminal, + shellIntegration: shellIntegration.object, + execution: { + cwd: workspace1.uri, + commandLine: { + isTrusted: true, + value: command, + confidence: 0, + }, + read: () => + (async function* () { + yield Promise.resolve(command); + })(), + }, + }); + + assert.strictEqual(disposables.length, 1); + sinon.assert.calledOnce(shouldPromptToCreateEnvStub); + sinon.assert.calledOnce(getWorkspaceFolderStub); + sinon.assert.calledOnce(isGlobalPythonSelectedStub); + sinon.assert.calledOnce(showWarningMessageStub); + sinon.assert.calledOnce(executeCommandStub); + sinon.assert.notCalled(disableCreateEnvironmentTriggerStub); + + shellIntegration.verify((s) => s.executeCommand(typemoq.It.isAnyString()), typemoq.Times.once()); + }); + }); + + test("Should disable create environment trigger if user selects don't show again", async () => { + shouldPromptToCreateEnvStub.returns(true); + + isGlobalPythonSelectedStub.returns(true); + showWarningMessageStub.resolves(Common.doNotShowAgain); + + const disposables: Disposable[] = []; + registerTriggerForPipInTerminal(disposables); + + await handler?.({ + terminal: ({} as unknown) as Terminal, + shellIntegration: shellIntegration.object, + execution: { + cwd: workspace1.uri, + commandLine: { + isTrusted: true, + value: 'pip install', + confidence: 0, + }, + read: () => + (async function* () { + yield Promise.resolve('pip install'); + })(), + }, + }); + + assert.strictEqual(disposables.length, 1); + sinon.assert.calledOnce(shouldPromptToCreateEnvStub); + sinon.assert.calledOnce(getWorkspaceFolderStub); + sinon.assert.calledOnce(isGlobalPythonSelectedStub); + sinon.assert.calledOnce(showWarningMessageStub); + sinon.assert.notCalled(executeCommandStub); + sinon.assert.calledOnce(disableCreateEnvironmentTriggerStub); + }); +}); diff --git a/src/test/pythonEnvironments/creation/installedPackagesDiagnostics.unit.test.ts b/src/test/pythonEnvironments/creation/installedPackagesDiagnostics.unit.test.ts new file mode 100644 index 000000000000..21bddd33c678 --- /dev/null +++ b/src/test/pythonEnvironments/creation/installedPackagesDiagnostics.unit.test.ts @@ -0,0 +1,334 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import * as chaiAsPromised from 'chai-as-promised'; +import * as sinon from 'sinon'; +import * as typemoq from 'typemoq'; +import { assert, use as chaiUse } from 'chai'; +import { Diagnostic, DiagnosticCollection, TextEditor, Range, Uri, TextDocument } from 'vscode'; +import * as cmdApis from '../../../client/common/vscodeApis/commandApis'; +import * as workspaceApis from '../../../client/common/vscodeApis/workspaceApis'; +import * as languageApis from '../../../client/common/vscodeApis/languageApis'; +import * as windowApis from '../../../client/common/vscodeApis/windowApis'; +import { IDisposableRegistry } from '../../../client/common/types'; +import * as installUtils from '../../../client/pythonEnvironments/creation/common/installCheckUtils'; +import { + DEPS_NOT_INSTALLED_KEY, + registerInstalledPackagesDiagnosticsProvider, +} from '../../../client/pythonEnvironments/creation/installedPackagesDiagnostic'; +import { IInterpreterService } from '../../../client/interpreter/contracts'; + +chaiUse(chaiAsPromised.default); + +class FakeDisposable { + public dispose() { + // Do nothing + } +} + +const MISSING_PACKAGES: Diagnostic[] = [ + { + range: new Range(8, 34, 8, 44), + message: 'Package `flake8-csv` is not installed in the selected environment.', + source: 'Python-InstalledPackagesChecker', + code: { value: 'not-installed', target: Uri.parse(`https://pypi.org/p/flake8-csv`) }, + severity: 3, + relatedInformation: [], + }, +]; + +function getSomeFile(): typemoq.IMock { + const someFilePath = 'something.py'; + const someFile = typemoq.Mock.ofType(); + someFile.setup((p) => p.fileName).returns(() => someFilePath); + someFile.setup((p) => p.getText(typemoq.It.isAny())).returns(() => 'print("Hello World")'); + return someFile; +} + +function getSomeRequirementFile(): typemoq.IMock { + const someFilePath = 'requirements.txt'; + const someFile = typemoq.Mock.ofType(); + someFile.setup((p) => p.languageId).returns(() => 'pip-requirements'); + someFile.setup((p) => p.fileName).returns(() => someFilePath); + someFile.setup((p) => p.getText(typemoq.It.isAny())).returns(() => 'flake8-csv'); + return someFile; +} + +function getPyProjectTomlFile(): typemoq.IMock { + const someFilePath = 'pyproject.toml'; + const someFile = typemoq.Mock.ofType(); + someFile.setup((p) => p.languageId).returns(() => 'toml'); + someFile.setup((p) => p.fileName).returns(() => someFilePath); + someFile + .setup((p) => p.getText(typemoq.It.isAny())) + .returns( + () => + '[build-system]\nrequires = ["flit_core >=3.2,<4"]\nbuild-backend = "flit_core.buildapi"\n\n[project]\nname = "something"\nversion = "2023.0.0"\nrequires-python = ">=3.8"\ndependencies = ["attrs>=21.3.0", "flake8-csv"]\n ', + ); + return someFile; +} + +function getSomeTomlFile(): typemoq.IMock { + const someFilePath = 'something.toml'; + const someFile = typemoq.Mock.ofType(); + someFile.setup((p) => p.languageId).returns(() => 'toml'); + someFile.setup((p) => p.fileName).returns(() => someFilePath); + someFile + .setup((p) => p.getText(typemoq.It.isAny())) + .returns( + () => + '[build-system]\nrequires = ["flit_core >=3.2,<4"]\nbuild-backend = "flit_core.buildapi"\n\n[something]\nname = "something"\nversion = "2023.0.0"\nrequires-python = ">=3.8"\ndependencies = ["attrs>=21.3.0", "flake8-csv"]\n ', + ); + return someFile; +} + +suite('Create Env content button settings tests', () => { + let executeCommandStub: sinon.SinonStub; + const disposables: IDisposableRegistry = []; + let getOpenTextDocumentsStub: sinon.SinonStub; + let onDidOpenTextDocumentStub: sinon.SinonStub; + let onDidSaveTextDocumentStub: sinon.SinonStub; + let onDidCloseTextDocumentStub: sinon.SinonStub; + let onDidChangeDiagnosticsStub: sinon.SinonStub; + let onDidChangeActiveTextEditorStub: sinon.SinonStub; + let createDiagnosticCollectionStub: sinon.SinonStub; + let diagnosticCollection: typemoq.IMock; + let getActiveTextEditorStub: sinon.SinonStub; + let textEditor: typemoq.IMock; + let getInstalledPackagesDiagnosticsStub: sinon.SinonStub; + let interpreterService: typemoq.IMock; + + setup(() => { + executeCommandStub = sinon.stub(cmdApis, 'executeCommand'); + + getOpenTextDocumentsStub = sinon.stub(workspaceApis, 'getOpenTextDocuments'); + getOpenTextDocumentsStub.returns([]); + + onDidOpenTextDocumentStub = sinon.stub(workspaceApis, 'onDidOpenTextDocument'); + onDidSaveTextDocumentStub = sinon.stub(workspaceApis, 'onDidSaveTextDocument'); + onDidCloseTextDocumentStub = sinon.stub(workspaceApis, 'onDidCloseTextDocument'); + onDidOpenTextDocumentStub.returns(new FakeDisposable()); + onDidSaveTextDocumentStub.returns(new FakeDisposable()); + onDidCloseTextDocumentStub.returns(new FakeDisposable()); + + onDidChangeDiagnosticsStub = sinon.stub(languageApis, 'onDidChangeDiagnostics'); + onDidChangeDiagnosticsStub.returns(new FakeDisposable()); + createDiagnosticCollectionStub = sinon.stub(languageApis, 'createDiagnosticCollection'); + diagnosticCollection = typemoq.Mock.ofType(); + diagnosticCollection.setup((d) => d.set(typemoq.It.isAny(), typemoq.It.isAny())).returns(() => undefined); + diagnosticCollection.setup((d) => d.clear()).returns(() => undefined); + diagnosticCollection.setup((d) => d.delete(typemoq.It.isAny())).returns(() => undefined); + diagnosticCollection.setup((d) => d.has(typemoq.It.isAny())).returns(() => false); + createDiagnosticCollectionStub.returns(diagnosticCollection.object); + + onDidChangeActiveTextEditorStub = sinon.stub(windowApis, 'onDidChangeActiveTextEditor'); + onDidChangeActiveTextEditorStub.returns(new FakeDisposable()); + getActiveTextEditorStub = sinon.stub(windowApis, 'getActiveTextEditor'); + textEditor = typemoq.Mock.ofType(); + getActiveTextEditorStub.returns(textEditor.object); + + getInstalledPackagesDiagnosticsStub = sinon.stub(installUtils, 'getInstalledPackagesDiagnostics'); + interpreterService = typemoq.Mock.ofType(); + interpreterService + .setup((i) => i.onDidChangeInterpreter(typemoq.It.isAny(), undefined, undefined)) + .returns(() => new FakeDisposable()); + }); + + teardown(() => { + sinon.restore(); + disposables.forEach((d) => d.dispose()); + }); + + test('Ensure nothing is run if there are no open documents', () => { + registerInstalledPackagesDiagnosticsProvider(disposables, interpreterService.object); + assert.ok(executeCommandStub.notCalled); + assert.ok(getInstalledPackagesDiagnosticsStub.notCalled); + }); + + test('Should not run packages check if opened files are not dep files', () => { + const someFile = getSomeFile(); + const someTomlFile = getSomeTomlFile(); + getOpenTextDocumentsStub.returns([someFile.object, someTomlFile.object]); + registerInstalledPackagesDiagnosticsProvider(disposables, interpreterService.object); + assert.ok(executeCommandStub.notCalled); + assert.ok(getInstalledPackagesDiagnosticsStub.notCalled); + }); + + test('Should run packages check if opened files are dep files', () => { + const reqFile = getSomeRequirementFile(); + const tomlFile = getPyProjectTomlFile(); + getOpenTextDocumentsStub.returns([reqFile.object, tomlFile.object]); + registerInstalledPackagesDiagnosticsProvider(disposables, interpreterService.object); + assert.ok(getInstalledPackagesDiagnosticsStub.calledTwice); + }); + + [getSomeRequirementFile().object, getPyProjectTomlFile().object].forEach((file) => { + test(`Should run packages check on open of a dep file: ${file.fileName}`, () => { + let handler: (doc: TextDocument) => void = () => { + /* do nothing */ + }; + onDidOpenTextDocumentStub.callsFake((callback) => { + handler = callback; + return new FakeDisposable(); + }); + + registerInstalledPackagesDiagnosticsProvider(disposables, interpreterService.object); + getInstalledPackagesDiagnosticsStub.reset(); + + getInstalledPackagesDiagnosticsStub.returns(Promise.resolve(MISSING_PACKAGES)); + + handler(file); + assert.ok(getInstalledPackagesDiagnosticsStub.calledOnce); + }); + + test(`Should run packages check on save of a dep file: ${file.fileName}`, () => { + let handler: (doc: TextDocument) => void = () => { + /* do nothing */ + }; + onDidSaveTextDocumentStub.callsFake((callback) => { + handler = callback; + return new FakeDisposable(); + }); + + registerInstalledPackagesDiagnosticsProvider(disposables, interpreterService.object); + getInstalledPackagesDiagnosticsStub.reset(); + + getInstalledPackagesDiagnosticsStub.returns(Promise.resolve(MISSING_PACKAGES)); + + handler(file); + assert.ok(getInstalledPackagesDiagnosticsStub.calledOnce); + }); + + test(`Should run packages check on close of a dep file: ${file.fileName}`, () => { + let handler: (doc: TextDocument) => void = () => { + /* do nothing */ + }; + onDidCloseTextDocumentStub.callsFake((callback) => { + handler = callback; + return new FakeDisposable(); + }); + + registerInstalledPackagesDiagnosticsProvider(disposables, interpreterService.object); + + diagnosticCollection.reset(); + diagnosticCollection.setup((d) => d.delete(typemoq.It.isAny())).verifiable(typemoq.Times.once()); + diagnosticCollection + .setup((d) => d.has(typemoq.It.isAny())) + .returns(() => true) + .verifiable(typemoq.Times.once()); + + handler(file); + diagnosticCollection.verifyAll(); + }); + + test(`Should trigger a context update on active editor switch to dep file: ${file.fileName}`, () => { + let handler: () => void = () => { + /* do nothing */ + }; + onDidChangeActiveTextEditorStub.callsFake((callback) => { + handler = callback; + return new FakeDisposable(); + }); + + registerInstalledPackagesDiagnosticsProvider(disposables, interpreterService.object); + + getActiveTextEditorStub.returns({ document: file }); + diagnosticCollection.setup((d) => d.get(typemoq.It.isAny())).returns(() => MISSING_PACKAGES); + + handler(); + assert.ok(executeCommandStub.calledOnceWithExactly('setContext', DEPS_NOT_INSTALLED_KEY, true)); + }); + + test(`Should trigger a context update to true on diagnostic change to dep file: ${file.fileName}`, () => { + let handler: () => void = () => { + /* do nothing */ + }; + onDidChangeDiagnosticsStub.callsFake((callback) => { + handler = callback; + return new FakeDisposable(); + }); + + registerInstalledPackagesDiagnosticsProvider(disposables, interpreterService.object); + + getActiveTextEditorStub.returns({ document: file }); + diagnosticCollection.setup((d) => d.get(typemoq.It.isAny())).returns(() => MISSING_PACKAGES); + + handler(); + assert.ok(executeCommandStub.calledOnceWithExactly('setContext', DEPS_NOT_INSTALLED_KEY, true)); + }); + }); + + [getSomeFile().object, getSomeTomlFile().object].forEach((file) => { + test(`Should not run packages check on open of a non dep file: ${file.fileName}`, () => { + let handler: (doc: TextDocument) => void = () => { + /* do nothing */ + }; + onDidOpenTextDocumentStub.callsFake((callback) => { + handler = callback; + return new FakeDisposable(); + }); + + registerInstalledPackagesDiagnosticsProvider(disposables, interpreterService.object); + getInstalledPackagesDiagnosticsStub.reset(); + + getInstalledPackagesDiagnosticsStub.returns(Promise.resolve(MISSING_PACKAGES)); + + handler(file); + assert.ok(getInstalledPackagesDiagnosticsStub.notCalled); + }); + + test(`Should not run packages check on save of a non dep file: ${file.fileName}`, () => { + let handler: (doc: TextDocument) => void = () => { + /* do nothing */ + }; + onDidSaveTextDocumentStub.callsFake((callback) => { + handler = callback; + return new FakeDisposable(); + }); + + registerInstalledPackagesDiagnosticsProvider(disposables, interpreterService.object); + getInstalledPackagesDiagnosticsStub.reset(); + + getInstalledPackagesDiagnosticsStub.returns(Promise.resolve(MISSING_PACKAGES)); + + handler(file); + assert.ok(getInstalledPackagesDiagnosticsStub.notCalled); + }); + + test(`Should trigger a context update on active editor switch to non-dep file: ${file.fileName}`, () => { + let handler: () => void = () => { + /* do nothing */ + }; + onDidChangeActiveTextEditorStub.callsFake((callback) => { + handler = callback; + return new FakeDisposable(); + }); + + registerInstalledPackagesDiagnosticsProvider(disposables, interpreterService.object); + + getActiveTextEditorStub.returns({ document: file }); + diagnosticCollection.setup((d) => d.get(typemoq.It.isAny())).returns(() => []); + + handler(); + assert.ok(executeCommandStub.calledOnceWithExactly('setContext', DEPS_NOT_INSTALLED_KEY, false)); + }); + + test(`Should trigger a context update to false on diagnostic change to non-dep file: ${file.fileName}`, () => { + let handler: () => void = () => { + /* do nothing */ + }; + onDidChangeDiagnosticsStub.callsFake((callback) => { + handler = callback; + return new FakeDisposable(); + }); + + registerInstalledPackagesDiagnosticsProvider(disposables, interpreterService.object); + + getActiveTextEditorStub.returns({ document: file }); + diagnosticCollection.setup((d) => d.get(typemoq.It.isAny())).returns(() => []); + + handler(); + assert.ok(executeCommandStub.calledOnceWithExactly('setContext', DEPS_NOT_INSTALLED_KEY, false)); + }); + }); +}); diff --git a/src/test/pythonEnvironments/creation/provider/commonUtils.unit.test.ts b/src/test/pythonEnvironments/creation/provider/commonUtils.unit.test.ts new file mode 100644 index 000000000000..ee177a58c779 --- /dev/null +++ b/src/test/pythonEnvironments/creation/provider/commonUtils.unit.test.ts @@ -0,0 +1,43 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { expect } from 'chai'; +import * as path from 'path'; +import * as sinon from 'sinon'; +import { Uri } from 'vscode'; +import * as fs from '../../../../client/common/platform/fs-paths'; +import { hasVenv } from '../../../../client/pythonEnvironments/creation/common/commonUtils'; +import { EXTENSION_ROOT_DIR_FOR_TESTS } from '../../../constants'; + +suite('CommonUtils', () => { + let fileExistsStub: sinon.SinonStub; + const workspace1 = { + uri: Uri.file(path.join(EXTENSION_ROOT_DIR_FOR_TESTS, 'src', 'testMultiRootWkspc', 'workspace1')), + name: 'workspace1', + index: 0, + }; + + setup(() => { + fileExistsStub = sinon.stub(fs, 'pathExists'); + }); + + teardown(() => { + sinon.restore(); + }); + + test('Venv exists test', async () => { + fileExistsStub.resolves(true); + const result = await hasVenv(workspace1); + expect(result).to.be.equal(true, 'Incorrect result'); + + fileExistsStub.calledOnceWith(path.join(workspace1.uri.fsPath, '.venv', 'pyvenv.cfg')); + }); + + test('Venv does not exist test', async () => { + fileExistsStub.resolves(false); + const result = await hasVenv(workspace1); + expect(result).to.be.equal(false, 'Incorrect result'); + + fileExistsStub.calledOnceWith(path.join(workspace1.uri.fsPath, '.venv', 'pyvenv.cfg')); + }); +}); diff --git a/src/test/pythonEnvironments/creation/provider/condaCreationProvider.unit.test.ts b/src/test/pythonEnvironments/creation/provider/condaCreationProvider.unit.test.ts index cb4df95c8c1f..e2ff9b2ab486 100644 --- a/src/test/pythonEnvironments/creation/provider/condaCreationProvider.unit.test.ts +++ b/src/test/pythonEnvironments/creation/provider/condaCreationProvider.unit.test.ts @@ -24,7 +24,7 @@ import { CreateEnvironmentResult, } from '../../../../client/pythonEnvironments/creation/proposed.createEnvApis'; -chaiUse(chaiAsPromised); +chaiUse(chaiAsPromised.default); suite('Conda Creation provider tests', () => { let condaProvider: CreateEnvironmentProvider; @@ -35,6 +35,8 @@ suite('Conda Creation provider tests', () => { let execObservableStub: sinon.SinonStub; let withProgressStub: sinon.SinonStub; let showErrorMessageWithLogsStub: sinon.SinonStub; + let pickExistingCondaActionStub: sinon.SinonStub; + let getPrefixCondaEnvPathStub: sinon.SinonStub; setup(() => { pickWorkspaceFolderStub = sinon.stub(wsSelect, 'pickWorkspaceFolder'); @@ -46,6 +48,11 @@ suite('Conda Creation provider tests', () => { showErrorMessageWithLogsStub = sinon.stub(commonUtils, 'showErrorMessageWithLogs'); showErrorMessageWithLogsStub.resolves(); + pickExistingCondaActionStub = sinon.stub(condaUtils, 'pickExistingCondaAction'); + pickExistingCondaActionStub.resolves(condaUtils.ExistingCondaAction.Create); + + getPrefixCondaEnvPathStub = sinon.stub(commonUtils, 'getPrefixCondaEnvPath'); + progressMock = typemoq.Mock.ofType(); condaProvider = condaCreationProvider(); }); @@ -77,6 +84,7 @@ suite('Conda Creation provider tests', () => { pickPythonVersionStub.resolves(undefined); await assert.isRejected(condaProvider.createEnvironment()); + assert.isTrue(pickExistingCondaActionStub.calledOnce); }); test('Create conda environment', async () => { @@ -134,10 +142,9 @@ suite('Conda Creation provider tests', () => { assert.deepStrictEqual(await promise, { path: 'new_environment', workspaceFolder: workspace1, - action: undefined, - error: undefined, }); assert.isTrue(showErrorMessageWithLogsStub.notCalled); + assert.isTrue(pickExistingCondaActionStub.calledOnce); }); test('Create conda environment failed', async () => { @@ -159,6 +166,7 @@ suite('Conda Creation provider tests', () => { out: { subscribe: ( _next?: (value: Output) => void, + // eslint-disable-next-line no-shadow error?: (error: unknown) => void, complete?: () => void, ) => { @@ -187,8 +195,10 @@ suite('Conda Creation provider tests', () => { assert.isDefined(_error); _error!('bad arguments'); _complete!(); - await assert.isRejected(promise); + const result = await promise; + assert.ok(result?.error); assert.isTrue(showErrorMessageWithLogsStub.calledOnce); + assert.isTrue(pickExistingCondaActionStub.calledOnce); }); test('Create conda environment failed (non-zero exit code)', async () => { @@ -243,7 +253,29 @@ suite('Conda Creation provider tests', () => { _next!({ out: `${CONDA_ENV_CREATED_MARKER}new_environment`, source: 'stdout' }); _complete!(); - await assert.isRejected(promise); + const result = await promise; + assert.ok(result?.error); assert.isTrue(showErrorMessageWithLogsStub.calledOnce); + assert.isTrue(pickExistingCondaActionStub.calledOnce); + }); + + test('Use existing conda environment', async () => { + getCondaBaseEnvStub.resolves('/usr/bin/conda'); + const workspace1 = { + uri: Uri.file(path.join(EXTENSION_ROOT_DIR_FOR_TESTS, 'src', 'testMultiRootWkspc', 'workspace1')), + name: 'workspace1', + index: 0, + }; + pickWorkspaceFolderStub.resolves(workspace1); + pickExistingCondaActionStub.resolves(condaUtils.ExistingCondaAction.UseExisting); + getPrefixCondaEnvPathStub.returns('existing_environment'); + + const result = await condaProvider.createEnvironment(); + assert.isTrue(showErrorMessageWithLogsStub.notCalled); + assert.isTrue(pickPythonVersionStub.notCalled); + assert.isTrue(execObservableStub.notCalled); + assert.isTrue(withProgressStub.notCalled); + + assert.deepStrictEqual(result, { path: 'existing_environment', workspaceFolder: workspace1 }); }); }); diff --git a/src/test/pythonEnvironments/creation/provider/condaDeleteUtils.unit.test.ts b/src/test/pythonEnvironments/creation/provider/condaDeleteUtils.unit.test.ts new file mode 100644 index 000000000000..b1acd0678714 --- /dev/null +++ b/src/test/pythonEnvironments/creation/provider/condaDeleteUtils.unit.test.ts @@ -0,0 +1,71 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { assert } from 'chai'; +import * as sinon from 'sinon'; +import * as path from 'path'; +import { Uri } from 'vscode'; +import * as commonUtils from '../../../../client/pythonEnvironments/creation/common/commonUtils'; +import * as rawProcessApis from '../../../../client/common/process/rawProcessApis'; +import { EXTENSION_ROOT_DIR_FOR_TESTS } from '../../../constants'; +import { deleteCondaEnvironment } from '../../../../client/pythonEnvironments/creation/provider/condaDeleteUtils'; + +suite('Conda Delete test', () => { + let plainExecStub: sinon.SinonStub; + let getPrefixCondaEnvPathStub: sinon.SinonStub; + let hasPrefixCondaEnvStub: sinon.SinonStub; + let showErrorMessageWithLogsStub: sinon.SinonStub; + + const workspace1 = { + uri: Uri.file(path.join(EXTENSION_ROOT_DIR_FOR_TESTS, 'src', 'testMultiRootWkspc', 'workspace1')), + name: 'workspace1', + index: 0, + }; + + setup(() => { + plainExecStub = sinon.stub(rawProcessApis, 'plainExec'); + getPrefixCondaEnvPathStub = sinon.stub(commonUtils, 'getPrefixCondaEnvPath'); + hasPrefixCondaEnvStub = sinon.stub(commonUtils, 'hasPrefixCondaEnv'); + showErrorMessageWithLogsStub = sinon.stub(commonUtils, 'showErrorMessageWithLogs'); + }); + + teardown(() => { + sinon.restore(); + }); + + test('Delete conda env ', async () => { + getPrefixCondaEnvPathStub.returns('condaEnvPath'); + hasPrefixCondaEnvStub.resolves(false); + plainExecStub.resolves({ stdout: 'stdout' }); + const result = await deleteCondaEnvironment(workspace1, 'interpreter', 'pathEnvVar'); + assert.isTrue(result); + assert.isTrue(plainExecStub.calledOnce); + assert.isTrue(getPrefixCondaEnvPathStub.calledOnce); + assert.isTrue(hasPrefixCondaEnvStub.calledOnce); + assert.isTrue(showErrorMessageWithLogsStub.notCalled); + }); + + test('Delete conda env with error', async () => { + getPrefixCondaEnvPathStub.returns('condaEnvPath'); + hasPrefixCondaEnvStub.resolves(true); + plainExecStub.resolves({ stdout: 'stdout' }); + const result = await deleteCondaEnvironment(workspace1, 'interpreter', 'pathEnvVar'); + assert.isFalse(result); + assert.isTrue(plainExecStub.calledOnce); + assert.isTrue(getPrefixCondaEnvPathStub.calledOnce); + assert.isTrue(hasPrefixCondaEnvStub.calledOnce); + assert.isTrue(showErrorMessageWithLogsStub.calledOnce); + }); + + test('Delete conda env with exception', async () => { + getPrefixCondaEnvPathStub.returns('condaEnvPath'); + hasPrefixCondaEnvStub.resolves(false); + plainExecStub.rejects(new Error('error')); + const result = await deleteCondaEnvironment(workspace1, 'interpreter', 'pathEnvVar'); + assert.isFalse(result); + assert.isTrue(plainExecStub.calledOnce); + assert.isTrue(getPrefixCondaEnvPathStub.calledOnce); + assert.isTrue(hasPrefixCondaEnvStub.notCalled); + assert.isTrue(showErrorMessageWithLogsStub.calledOnce); + }); +}); diff --git a/src/test/pythonEnvironments/creation/provider/condaUtils.unit.test.ts b/src/test/pythonEnvironments/creation/provider/condaUtils.unit.test.ts index 3f115f9f58ed..a3f4a1abe905 100644 --- a/src/test/pythonEnvironments/creation/provider/condaUtils.unit.test.ts +++ b/src/test/pythonEnvironments/creation/provider/condaUtils.unit.test.ts @@ -3,9 +3,17 @@ import { assert } from 'chai'; import * as sinon from 'sinon'; -import { CancellationTokenSource } from 'vscode'; +import * as path from 'path'; +import { CancellationTokenSource, Uri } from 'vscode'; import * as windowApis from '../../../../client/common/vscodeApis/windowApis'; -import { pickPythonVersion } from '../../../../client/pythonEnvironments/creation/provider/condaUtils'; +import { + ExistingCondaAction, + pickExistingCondaAction, + pickPythonVersion, +} from '../../../../client/pythonEnvironments/creation/provider/condaUtils'; +import * as commonUtils from '../../../../client/pythonEnvironments/creation/common/commonUtils'; +import { EXTENSION_ROOT_DIR_FOR_TESTS } from '../../../constants'; +import { CreateEnv } from '../../../../client/common/utils/localize'; suite('Conda Utils test', () => { let showQuickPickWithBackStub: sinon.SinonStub; @@ -43,3 +51,60 @@ suite('Conda Utils test', () => { assert.isUndefined(actual); }); }); + +suite('Existing .conda env test', () => { + let hasPrefixCondaEnvStub: sinon.SinonStub; + let showQuickPickWithBackStub: sinon.SinonStub; + + const workspace1 = { + uri: Uri.file(path.join(EXTENSION_ROOT_DIR_FOR_TESTS, 'src', 'testMultiRootWkspc', 'workspace1')), + name: 'workspace1', + index: 0, + }; + + setup(() => { + hasPrefixCondaEnvStub = sinon.stub(commonUtils, 'hasPrefixCondaEnv'); + showQuickPickWithBackStub = sinon.stub(windowApis, 'showQuickPickWithBack'); + }); + + teardown(() => { + sinon.restore(); + }); + + test('No .conda found', async () => { + hasPrefixCondaEnvStub.resolves(false); + showQuickPickWithBackStub.resolves(undefined); + + const actual = await pickExistingCondaAction(workspace1); + assert.deepStrictEqual(actual, ExistingCondaAction.Create); + assert.isTrue(showQuickPickWithBackStub.notCalled); + }); + + test('User presses escape', async () => { + hasPrefixCondaEnvStub.resolves(true); + showQuickPickWithBackStub.resolves(undefined); + await assert.isRejected(pickExistingCondaAction(workspace1)); + }); + + test('.conda found and user selected to re-create', async () => { + hasPrefixCondaEnvStub.resolves(true); + showQuickPickWithBackStub.resolves({ + label: CreateEnv.Conda.recreate, + description: CreateEnv.Conda.recreateDescription, + }); + + const actual = await pickExistingCondaAction(workspace1); + assert.deepStrictEqual(actual, ExistingCondaAction.Recreate); + }); + + test('.conda found and user selected to re-use', async () => { + hasPrefixCondaEnvStub.resolves(true); + showQuickPickWithBackStub.resolves({ + label: CreateEnv.Conda.useExisting, + description: CreateEnv.Conda.useExistingDescription, + }); + + const actual = await pickExistingCondaAction(workspace1); + assert.deepStrictEqual(actual, ExistingCondaAction.UseExisting); + }); +}); diff --git a/src/test/pythonEnvironments/creation/provider/venvCreationProvider.unit.test.ts b/src/test/pythonEnvironments/creation/provider/venvCreationProvider.unit.test.ts index 1c22264f2ada..aa2d317c405e 100644 --- a/src/test/pythonEnvironments/creation/provider/venvCreationProvider.unit.test.ts +++ b/src/test/pythonEnvironments/creation/provider/venvCreationProvider.unit.test.ts @@ -15,7 +15,7 @@ import * as rawProcessApis from '../../../../client/common/process/rawProcessApi import * as commonUtils from '../../../../client/pythonEnvironments/creation/common/commonUtils'; import { EXTENSION_ROOT_DIR_FOR_TESTS } from '../../../constants'; import { createDeferred } from '../../../../client/common/utils/async'; -import { Output } from '../../../../client/common/process/types'; +import { Output, SpawnOptions } from '../../../../client/common/process/types'; import { VENV_CREATED_MARKER } from '../../../../client/pythonEnvironments/creation/provider/venvProgressAndTelemetry'; import { CreateEnv } from '../../../../client/common/utils/localize'; import * as venvUtils from '../../../../client/pythonEnvironments/creation/provider/venvUtils'; @@ -24,7 +24,7 @@ import { CreateEnvironmentResult, } from '../../../../client/pythonEnvironments/creation/proposed.createEnvApis'; -chaiUse(chaiAsPromised); +chaiUse(chaiAsPromised.default); suite('venv Creation provider tests', () => { let venvProvider: CreateEnvironmentProvider; @@ -35,6 +35,8 @@ suite('venv Creation provider tests', () => { let withProgressStub: sinon.SinonStub; let showErrorMessageWithLogsStub: sinon.SinonStub; let pickPackagesToInstallStub: sinon.SinonStub; + let pickExistingVenvActionStub: sinon.SinonStub; + let deleteEnvironmentStub: sinon.SinonStub; const workspace1 = { uri: Uri.file(path.join(EXTENSION_ROOT_DIR_FOR_TESTS, 'src', 'testMultiRootWkspc', 'workspace1')), @@ -43,6 +45,8 @@ suite('venv Creation provider tests', () => { }; setup(() => { + pickExistingVenvActionStub = sinon.stub(venvUtils, 'pickExistingVenvAction'); + deleteEnvironmentStub = sinon.stub(venvUtils, 'deleteEnvironment'); pickWorkspaceFolderStub = sinon.stub(wsSelect, 'pickWorkspaceFolder'); execObservableStub = sinon.stub(rawProcessApis, 'execObservable'); interpreterQuickPick = typemoq.Mock.ofType(); @@ -54,6 +58,9 @@ suite('venv Creation provider tests', () => { progressMock = typemoq.Mock.ofType(); venvProvider = new VenvCreationProvider(interpreterQuickPick.object); + + pickExistingVenvActionStub.resolves(venvUtils.ExistingVenvAction.Create); + deleteEnvironmentStub.resolves(true); }); teardown(() => { @@ -70,6 +77,8 @@ suite('venv Creation provider tests', () => { assert.isTrue(pickWorkspaceFolderStub.calledOnce); interpreterQuickPick.verifyAll(); assert.isTrue(pickPackagesToInstallStub.notCalled); + assert.isTrue(pickExistingVenvActionStub.notCalled); + assert.isTrue(deleteEnvironmentStub.notCalled); }); test('No Python selected', async () => { @@ -85,6 +94,7 @@ suite('venv Creation provider tests', () => { assert.isTrue(pickWorkspaceFolderStub.calledOnce); interpreterQuickPick.verifyAll(); assert.isTrue(pickPackagesToInstallStub.notCalled); + assert.isTrue(deleteEnvironmentStub.notCalled); }); test('User pressed Esc while selecting dependencies', async () => { @@ -99,6 +109,7 @@ suite('venv Creation provider tests', () => { await assert.isRejected(venvProvider.createEnvironment()); assert.isTrue(pickPackagesToInstallStub.calledOnce); + assert.isTrue(deleteEnvironmentStub.notCalled); }); test('Create venv with python selected by user no packages selected', async () => { @@ -158,12 +169,11 @@ suite('venv Creation provider tests', () => { assert.deepStrictEqual(actual, { path: 'new_environment', workspaceFolder: workspace1, - action: undefined, - error: undefined, }); interpreterQuickPick.verifyAll(); progressMock.verifyAll(); assert.isTrue(showErrorMessageWithLogsStub.notCalled); + assert.isTrue(deleteEnvironmentStub.notCalled); }); test('Create venv failed', async () => { @@ -188,6 +198,7 @@ suite('venv Creation provider tests', () => { out: { subscribe: ( _next?: (value: Output) => void, + // eslint-disable-next-line no-shadow error?: (error: unknown) => void, complete?: () => void, ) => { @@ -216,8 +227,10 @@ suite('venv Creation provider tests', () => { assert.isDefined(_error); _error!('bad arguments'); _complete!(); - await assert.isRejected(promise); + const result = await promise; + assert.ok(result?.error); assert.isTrue(showErrorMessageWithLogsStub.calledOnce); + assert.isTrue(deleteEnvironmentStub.notCalled); }); test('Create venv failed (non-zero exit code)', async () => { @@ -272,9 +285,267 @@ suite('venv Creation provider tests', () => { _next!({ out: `${VENV_CREATED_MARKER}new_environment`, source: 'stdout' }); _complete!(); - await assert.isRejected(promise); + const result = await promise; + assert.ok(result?.error); interpreterQuickPick.verifyAll(); progressMock.verifyAll(); assert.isTrue(showErrorMessageWithLogsStub.calledOnce); + assert.isTrue(deleteEnvironmentStub.notCalled); + }); + + test('Create venv with pre-existing .venv, user selects re-create', async () => { + pickExistingVenvActionStub.resolves(venvUtils.ExistingVenvAction.Recreate); + pickWorkspaceFolderStub.resolves(workspace1); + + interpreterQuickPick + .setup((i) => i.getInterpreterViaQuickPick(typemoq.It.isAny(), typemoq.It.isAny(), typemoq.It.isAny())) + .returns(() => Promise.resolve('/usr/bin/python')) + .verifiable(typemoq.Times.once()); + + pickPackagesToInstallStub.resolves([]); + + const deferred = createDeferred(); + let _next: undefined | ((value: Output) => void); + let _complete: undefined | (() => void); + execObservableStub.callsFake(() => { + deferred.resolve(); + return { + proc: { + exitCode: 0, + }, + out: { + subscribe: ( + next?: (value: Output) => void, + _error?: (error: unknown) => void, + complete?: () => void, + ) => { + _next = next; + _complete = complete; + }, + }, + dispose: () => undefined, + }; + }); + + progressMock.setup((p) => p.report({ message: CreateEnv.statusStarting })).verifiable(typemoq.Times.once()); + + withProgressStub.callsFake( + ( + _options: ProgressOptions, + task: ( + progress: CreateEnvironmentProgress, + token?: CancellationToken, + ) => Thenable, + ) => task(progressMock.object), + ); + + const promise = venvProvider.createEnvironment(); + await deferred.promise; + assert.isDefined(_next); + assert.isDefined(_complete); + + _next!({ out: `${VENV_CREATED_MARKER}new_environment`, source: 'stdout' }); + _complete!(); + + const actual = await promise; + assert.deepStrictEqual(actual, { + path: 'new_environment', + workspaceFolder: workspace1, + }); + interpreterQuickPick.verifyAll(); + progressMock.verifyAll(); + assert.isTrue(showErrorMessageWithLogsStub.notCalled); + assert.isTrue(deleteEnvironmentStub.calledOnce); + }); + + test('Create venv with pre-existing .venv, user selects re-create, delete env failed', async () => { + pickExistingVenvActionStub.resolves(venvUtils.ExistingVenvAction.Recreate); + pickWorkspaceFolderStub.resolves(workspace1); + deleteEnvironmentStub.resolves(false); + + interpreterQuickPick + .setup((i) => i.getInterpreterViaQuickPick(typemoq.It.isAny(), typemoq.It.isAny(), typemoq.It.isAny())) + .returns(() => Promise.resolve('/usr/bin/python')) + .verifiable(typemoq.Times.once()); + + pickPackagesToInstallStub.resolves([]); + + await assert.isRejected(venvProvider.createEnvironment()); + + interpreterQuickPick.verifyAll(); + assert.isTrue(withProgressStub.notCalled); + assert.isTrue(showErrorMessageWithLogsStub.notCalled); + assert.isTrue(deleteEnvironmentStub.calledOnce); + }); + + test('Create venv with pre-existing .venv, user selects use existing', async () => { + pickExistingVenvActionStub.resolves(venvUtils.ExistingVenvAction.UseExisting); + pickWorkspaceFolderStub.resolves(workspace1); + + interpreterQuickPick + .setup((i) => i.getInterpreterViaQuickPick(typemoq.It.isAny(), typemoq.It.isAny(), typemoq.It.isAny())) + .returns(() => Promise.resolve('/usr/bin/python')) + .verifiable(typemoq.Times.never()); + + pickPackagesToInstallStub.resolves([]); + + interpreterQuickPick.verifyAll(); + assert.isTrue(withProgressStub.notCalled); + assert.isTrue(pickPackagesToInstallStub.notCalled); + assert.isTrue(showErrorMessageWithLogsStub.notCalled); + assert.isTrue(deleteEnvironmentStub.notCalled); + }); + + test('Create venv with 1000 requirement files', async () => { + pickWorkspaceFolderStub.resolves(workspace1); + + interpreterQuickPick + .setup((i) => i.getInterpreterViaQuickPick(typemoq.It.isAny(), typemoq.It.isAny(), typemoq.It.isAny())) + .returns(() => Promise.resolve('/usr/bin/python')) + .verifiable(typemoq.Times.once()); + + const requirements = Array.from({ length: 1000 }, (_, i) => ({ + installType: 'requirements', + installItem: `requirements${i}.txt`, + })); + pickPackagesToInstallStub.resolves(requirements); + const expected = JSON.stringify({ requirements: requirements.map((r) => r.installItem) }); + + const deferred = createDeferred(); + let _next: undefined | ((value: Output) => void); + let _complete: undefined | (() => void); + let stdin: undefined | string; + let hasStdinArg = false; + execObservableStub.callsFake((_c, argv: string[], options) => { + stdin = options?.stdinStr; + hasStdinArg = argv.includes('--stdin'); + deferred.resolve(); + return { + proc: { + exitCode: 0, + }, + out: { + subscribe: ( + next?: (value: Output) => void, + _error?: (error: unknown) => void, + complete?: () => void, + ) => { + _next = next; + _complete = complete; + }, + }, + dispose: () => undefined, + }; + }); + + progressMock.setup((p) => p.report({ message: CreateEnv.statusStarting })).verifiable(typemoq.Times.once()); + + withProgressStub.callsFake( + ( + _options: ProgressOptions, + task: ( + progress: CreateEnvironmentProgress, + token?: CancellationToken, + ) => Thenable, + ) => task(progressMock.object), + ); + + const promise = venvProvider.createEnvironment(); + await deferred.promise; + assert.isDefined(_next); + assert.isDefined(_complete); + + _next!({ out: `${VENV_CREATED_MARKER}new_environment`, source: 'stdout' }); + _complete!(); + + const actual = await promise; + assert.deepStrictEqual(actual, { + path: 'new_environment', + workspaceFolder: workspace1, + }); + interpreterQuickPick.verifyAll(); + progressMock.verifyAll(); + assert.isTrue(showErrorMessageWithLogsStub.notCalled); + assert.isTrue(deleteEnvironmentStub.notCalled); + assert.strictEqual(stdin, expected); + assert.isTrue(hasStdinArg); + }); + + test('Create venv with 5 requirement files', async () => { + pickWorkspaceFolderStub.resolves(workspace1); + + interpreterQuickPick + .setup((i) => i.getInterpreterViaQuickPick(typemoq.It.isAny(), typemoq.It.isAny(), typemoq.It.isAny())) + .returns(() => Promise.resolve('/usr/bin/python')) + .verifiable(typemoq.Times.once()); + + const requirements = Array.from({ length: 5 }, (_, i) => ({ + installType: 'requirements', + installItem: `requirements${i}.txt`, + })); + pickPackagesToInstallStub.resolves(requirements); + const expectedRequirements = requirements.map((r) => r.installItem).sort(); + + const deferred = createDeferred(); + let _next: undefined | ((value: Output) => void); + let _complete: undefined | (() => void); + let stdin: undefined | string; + let hasStdinArg = false; + let actualRequirements: string[] = []; + execObservableStub.callsFake((_c, argv: string[], options: SpawnOptions) => { + stdin = options?.stdinStr; + actualRequirements = argv.filter((arg) => arg.startsWith('requirements')).sort(); + hasStdinArg = argv.includes('--stdin'); + deferred.resolve(); + return { + proc: { + exitCode: 0, + }, + out: { + subscribe: ( + next?: (value: Output) => void, + _error?: (error: unknown) => void, + complete?: () => void, + ) => { + _next = next; + _complete = complete; + }, + }, + dispose: () => undefined, + }; + }); + + progressMock.setup((p) => p.report({ message: CreateEnv.statusStarting })).verifiable(typemoq.Times.once()); + + withProgressStub.callsFake( + ( + _options: ProgressOptions, + task: ( + progress: CreateEnvironmentProgress, + token?: CancellationToken, + ) => Thenable, + ) => task(progressMock.object), + ); + + const promise = venvProvider.createEnvironment(); + await deferred.promise; + assert.isDefined(_next); + assert.isDefined(_complete); + + _next!({ out: `${VENV_CREATED_MARKER}new_environment`, source: 'stdout' }); + _complete!(); + + const actual = await promise; + assert.deepStrictEqual(actual, { + path: 'new_environment', + workspaceFolder: workspace1, + }); + interpreterQuickPick.verifyAll(); + progressMock.verifyAll(); + assert.isTrue(showErrorMessageWithLogsStub.notCalled); + assert.isTrue(deleteEnvironmentStub.notCalled); + assert.isUndefined(stdin); + assert.deepStrictEqual(actualRequirements, expectedRequirements); + assert.isFalse(hasStdinArg); }); }); diff --git a/src/test/pythonEnvironments/creation/provider/venvDeleteUtils.unit.test.ts b/src/test/pythonEnvironments/creation/provider/venvDeleteUtils.unit.test.ts new file mode 100644 index 000000000000..231222acbaec --- /dev/null +++ b/src/test/pythonEnvironments/creation/provider/venvDeleteUtils.unit.test.ts @@ -0,0 +1,142 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +import * as sinon from 'sinon'; +import { Uri, WorkspaceFolder } from 'vscode'; +import { assert } from 'chai'; +import * as path from 'path'; +import * as fs from '../../../../client/common/platform/fs-paths'; +import { EXTENSION_ROOT_DIR_FOR_TESTS } from '../../../constants'; +import * as commonUtils from '../../../../client/pythonEnvironments/creation/common/commonUtils'; +import { + deleteEnvironmentNonWindows, + deleteEnvironmentWindows, +} from '../../../../client/pythonEnvironments/creation/provider/venvDeleteUtils'; +import * as switchPython from '../../../../client/pythonEnvironments/creation/provider/venvSwitchPython'; +import * as asyncApi from '../../../../client/common/utils/async'; + +suite('Test Delete environments (windows)', () => { + let pathExistsStub: sinon.SinonStub; + let rmdirStub: sinon.SinonStub; + let unlinkStub: sinon.SinonStub; + let showErrorMessageWithLogsStub: sinon.SinonStub; + let switchPythonStub: sinon.SinonStub; + let sleepStub: sinon.SinonStub; + + const workspace1: WorkspaceFolder = { + uri: Uri.file(path.join(EXTENSION_ROOT_DIR_FOR_TESTS, 'src', 'testMultiRootWkspc', 'workspace1')), + name: 'workspace1', + index: 0, + }; + + setup(() => { + pathExistsStub = sinon.stub(fs, 'pathExists'); + pathExistsStub.resolves(true); + + rmdirStub = sinon.stub(fs, 'rmdir'); + unlinkStub = sinon.stub(fs, 'unlink'); + + sleepStub = sinon.stub(asyncApi, 'sleep'); + sleepStub.resolves(); + + showErrorMessageWithLogsStub = sinon.stub(commonUtils, 'showErrorMessageWithLogs'); + showErrorMessageWithLogsStub.resolves(); + + switchPythonStub = sinon.stub(switchPython, 'switchSelectedPython'); + switchPythonStub.resolves(); + }); + + teardown(() => { + sinon.restore(); + }); + + test('Delete venv folder succeeded', async () => { + rmdirStub.resolves(); + unlinkStub.resolves(); + assert.ok(await deleteEnvironmentWindows(workspace1, 'python.exe')); + + assert.ok(rmdirStub.calledOnce); + assert.ok(unlinkStub.calledOnce); + assert.ok(showErrorMessageWithLogsStub.notCalled); + }); + + test('Delete python.exe succeeded but venv dir failed', async () => { + rmdirStub.rejects(); + unlinkStub.resolves(); + assert.notOk(await deleteEnvironmentWindows(workspace1, 'python.exe')); + + assert.ok(rmdirStub.calledOnce); + assert.ok(unlinkStub.calledOnce); + assert.ok(showErrorMessageWithLogsStub.calledOnce); + }); + + test('Delete python.exe failed first attempt', async () => { + unlinkStub.rejects(); + rmdirStub.resolves(); + assert.ok(await deleteEnvironmentWindows(workspace1, 'python.exe')); + + assert.ok(rmdirStub.calledOnce); + assert.ok(switchPythonStub.calledOnce); + assert.ok(showErrorMessageWithLogsStub.notCalled); + }); + + test('Delete python.exe failed all attempts', async () => { + unlinkStub.rejects(); + rmdirStub.rejects(); + assert.notOk(await deleteEnvironmentWindows(workspace1, 'python.exe')); + assert.ok(switchPythonStub.calledOnce); + assert.ok(showErrorMessageWithLogsStub.calledOnce); + }); + + test('Delete python.exe failed no interpreter', async () => { + unlinkStub.rejects(); + rmdirStub.rejects(); + assert.notOk(await deleteEnvironmentWindows(workspace1, undefined)); + assert.ok(switchPythonStub.notCalled); + assert.ok(showErrorMessageWithLogsStub.calledOnce); + }); +}); + +suite('Test Delete environments (linux/mac)', () => { + let pathExistsStub: sinon.SinonStub; + let rmdirStub: sinon.SinonStub; + let showErrorMessageWithLogsStub: sinon.SinonStub; + + const workspace1: WorkspaceFolder = { + uri: Uri.file(path.join(EXTENSION_ROOT_DIR_FOR_TESTS, 'src', 'testMultiRootWkspc', 'workspace1')), + name: 'workspace1', + index: 0, + }; + + setup(() => { + pathExistsStub = sinon.stub(fs, 'pathExists'); + rmdirStub = sinon.stub(fs, 'rmdir'); + + showErrorMessageWithLogsStub = sinon.stub(commonUtils, 'showErrorMessageWithLogs'); + showErrorMessageWithLogsStub.resolves(); + }); + + teardown(() => { + sinon.restore(); + }); + + test('Delete venv folder succeeded', async () => { + pathExistsStub.resolves(true); + rmdirStub.resolves(); + + assert.ok(await deleteEnvironmentNonWindows(workspace1)); + + assert.ok(pathExistsStub.calledOnce); + assert.ok(rmdirStub.calledOnce); + assert.ok(showErrorMessageWithLogsStub.notCalled); + }); + + test('Delete venv folder failed', async () => { + pathExistsStub.resolves(true); + rmdirStub.rejects(); + assert.notOk(await deleteEnvironmentNonWindows(workspace1)); + + assert.ok(pathExistsStub.calledOnce); + assert.ok(rmdirStub.calledOnce); + assert.ok(showErrorMessageWithLogsStub.calledOnce); + }); +}); diff --git a/src/test/pythonEnvironments/creation/provider/venvUtils.unit.test.ts b/src/test/pythonEnvironments/creation/provider/venvUtils.unit.test.ts index 360bb43fad4b..2c8ec2ebce87 100644 --- a/src/test/pythonEnvironments/creation/provider/venvUtils.unit.test.ts +++ b/src/test/pythonEnvironments/creation/provider/venvUtils.unit.test.ts @@ -2,23 +2,30 @@ // Licensed under the MIT License. import { assert, use as chaiUse } from 'chai'; import * as chaiAsPromised from 'chai-as-promised'; -import * as fs from 'fs-extra'; import * as sinon from 'sinon'; import { Uri } from 'vscode'; import * as path from 'path'; +import * as fs from '../../../../client/common/platform/fs-paths'; import * as windowApis from '../../../../client/common/vscodeApis/windowApis'; import * as workspaceApis from '../../../../client/common/vscodeApis/workspaceApis'; -import { pickPackagesToInstall } from '../../../../client/pythonEnvironments/creation/provider/venvUtils'; +import { + ExistingVenvAction, + OPEN_REQUIREMENTS_BUTTON, + pickExistingVenvAction, + pickPackagesToInstall, +} from '../../../../client/pythonEnvironments/creation/provider/venvUtils'; import { EXTENSION_ROOT_DIR_FOR_TESTS } from '../../../constants'; import { CreateEnv } from '../../../../client/common/utils/localize'; +import { createDeferred } from '../../../../client/common/utils/async'; -chaiUse(chaiAsPromised); +chaiUse(chaiAsPromised.default); suite('Venv Utils test', () => { let findFilesStub: sinon.SinonStub; let showQuickPickWithBackStub: sinon.SinonStub; let pathExistsStub: sinon.SinonStub; let readFileStub: sinon.SinonStub; + let showTextDocumentStub: sinon.SinonStub; const workspace1 = { uri: Uri.file(path.join(EXTENSION_ROOT_DIR_FOR_TESTS, 'src', 'testMultiRootWkspc', 'workspace1')), @@ -31,6 +38,7 @@ suite('Venv Utils test', () => { showQuickPickWithBackStub = sinon.stub(windowApis, 'showQuickPickWithBack'); pathExistsStub = sinon.stub(fs, 'pathExists'); readFileStub = sinon.stub(fs, 'readFile'); + showTextDocumentStub = sinon.stub(windowApis, 'showTextDocument'); }); teardown(() => { @@ -56,6 +64,18 @@ suite('Venv Utils test', () => { assert.deepStrictEqual(actual, []); }); + test('Toml found with no project table', async () => { + findFilesStub.resolves([]); + pathExistsStub.resolves(true); + readFileStub.resolves( + '[tool.poetry]\nname = "spam"\nversion = "2020.0.0"\n[build-system]\nrequires = ["setuptools ~= 58.0", "cython ~= 0.29.0"]', + ); + + const actual = await pickPackagesToInstall(workspace1); + assert.isTrue(showQuickPickWithBackStub.notCalled); + assert.deepStrictEqual(actual, []); + }); + test('Toml found with no optional deps', async () => { findFilesStub.resolves([]); pathExistsStub.resolves(true); @@ -220,13 +240,18 @@ suite('Venv Utils test', () => { await assert.isRejected(pickPackagesToInstall(workspace1)); assert.isTrue( showQuickPickWithBackStub.calledWithExactly( - [{ label: 'requirements.txt' }, { label: 'dev-requirements.txt' }, { label: 'test-requirements.txt' }], + [ + { label: 'requirements.txt', buttons: [OPEN_REQUIREMENTS_BUTTON] }, + { label: 'dev-requirements.txt', buttons: [OPEN_REQUIREMENTS_BUTTON] }, + { label: 'test-requirements.txt', buttons: [OPEN_REQUIREMENTS_BUTTON] }, + ], { placeHolder: CreateEnv.Venv.requirementsQuickPickTitle, ignoreFocusOut: true, canPickMany: true, }, undefined, + sinon.match.func, ), ); assert.isTrue(readFileStub.calledOnce); @@ -253,13 +278,18 @@ suite('Venv Utils test', () => { const actual = await pickPackagesToInstall(workspace1); assert.isTrue( showQuickPickWithBackStub.calledWithExactly( - [{ label: 'requirements.txt' }, { label: 'dev-requirements.txt' }, { label: 'test-requirements.txt' }], + [ + { label: 'requirements.txt', buttons: [OPEN_REQUIREMENTS_BUTTON] }, + { label: 'dev-requirements.txt', buttons: [OPEN_REQUIREMENTS_BUTTON] }, + { label: 'test-requirements.txt', buttons: [OPEN_REQUIREMENTS_BUTTON] }, + ], { placeHolder: CreateEnv.Venv.requirementsQuickPickTitle, ignoreFocusOut: true, canPickMany: true, }, undefined, + sinon.match.func, ), ); assert.deepStrictEqual(actual, []); @@ -286,13 +316,18 @@ suite('Venv Utils test', () => { const actual = await pickPackagesToInstall(workspace1); assert.isTrue( showQuickPickWithBackStub.calledWithExactly( - [{ label: 'requirements.txt' }, { label: 'dev-requirements.txt' }, { label: 'test-requirements.txt' }], + [ + { label: 'requirements.txt', buttons: [OPEN_REQUIREMENTS_BUTTON] }, + { label: 'dev-requirements.txt', buttons: [OPEN_REQUIREMENTS_BUTTON] }, + { label: 'test-requirements.txt', buttons: [OPEN_REQUIREMENTS_BUTTON] }, + ], { placeHolder: CreateEnv.Venv.requirementsQuickPickTitle, ignoreFocusOut: true, canPickMany: true, }, undefined, + sinon.match.func, ), ); assert.deepStrictEqual(actual, [ @@ -324,13 +359,18 @@ suite('Venv Utils test', () => { const actual = await pickPackagesToInstall(workspace1); assert.isTrue( showQuickPickWithBackStub.calledWithExactly( - [{ label: 'requirements.txt' }, { label: 'dev-requirements.txt' }, { label: 'test-requirements.txt' }], + [ + { label: 'requirements.txt', buttons: [OPEN_REQUIREMENTS_BUTTON] }, + { label: 'dev-requirements.txt', buttons: [OPEN_REQUIREMENTS_BUTTON] }, + { label: 'test-requirements.txt', buttons: [OPEN_REQUIREMENTS_BUTTON] }, + ], { placeHolder: CreateEnv.Venv.requirementsQuickPickTitle, ignoreFocusOut: true, canPickMany: true, }, undefined, + sinon.match.func, ), ); assert.deepStrictEqual(actual, [ @@ -345,4 +385,105 @@ suite('Venv Utils test', () => { ]); assert.isTrue(readFileStub.notCalled); }); + + test('User clicks button to open requirements.txt', async () => { + let allow = true; + findFilesStub.callsFake(() => { + if (allow) { + allow = false; + return Promise.resolve([ + Uri.file(path.join(workspace1.uri.fsPath, 'requirements.txt')), + Uri.file(path.join(workspace1.uri.fsPath, 'dev-requirements.txt')), + Uri.file(path.join(workspace1.uri.fsPath, 'test-requirements.txt')), + ]); + } + return Promise.resolve([]); + }); + pathExistsStub.resolves(false); + + const deferred = createDeferred(); + showQuickPickWithBackStub.callsFake(async (_items, _options, _token, callback) => { + callback({ + button: OPEN_REQUIREMENTS_BUTTON, + item: { label: 'requirements.txt' }, + }); + await deferred.promise; + return [{ label: 'requirements.txt' }]; + }); + + let uri: Uri | undefined; + showTextDocumentStub.callsFake((arg: Uri) => { + uri = arg; + deferred.resolve(); + return Promise.resolve(); + }); + + await pickPackagesToInstall(workspace1); + assert.deepStrictEqual( + uri?.toString(), + Uri.file(path.join(workspace1.uri.fsPath, 'requirements.txt')).toString(), + ); + }); +}); + +suite('Test pick existing venv action', () => { + let withProgressStub: sinon.SinonStub; + let showQuickPickWithBackStub: sinon.SinonStub; + let pathExistsStub: sinon.SinonStub; + + const workspace1 = { + uri: Uri.file(path.join(EXTENSION_ROOT_DIR_FOR_TESTS, 'src', 'testMultiRootWkspc', 'workspace1')), + name: 'workspace1', + index: 0, + }; + + setup(() => { + pathExistsStub = sinon.stub(fs, 'pathExists'); + withProgressStub = sinon.stub(windowApis, 'withProgress'); + showQuickPickWithBackStub = sinon.stub(windowApis, 'showQuickPickWithBack'); + }); + teardown(() => { + sinon.restore(); + }); + + test('User selects existing venv', async () => { + pathExistsStub.resolves(true); + showQuickPickWithBackStub.resolves({ + label: CreateEnv.Venv.useExisting, + description: CreateEnv.Venv.useExistingDescription, + }); + const actual = await pickExistingVenvAction(workspace1); + assert.deepStrictEqual(actual, ExistingVenvAction.UseExisting); + }); + + test('User presses escape', async () => { + pathExistsStub.resolves(true); + showQuickPickWithBackStub.resolves(undefined); + await assert.isRejected(pickExistingVenvAction(workspace1)); + }); + + test('User selects delete venv', async () => { + pathExistsStub.resolves(true); + showQuickPickWithBackStub.resolves({ + label: CreateEnv.Venv.recreate, + description: CreateEnv.Venv.recreateDescription, + }); + withProgressStub.resolves(true); + const actual = await pickExistingVenvAction(workspace1); + assert.deepStrictEqual(actual, ExistingVenvAction.Recreate); + }); + + test('User clicks on back', async () => { + pathExistsStub.resolves(true); + // We use reject with "Back" to simulate the user clicking on back. + showQuickPickWithBackStub.rejects(windowApis.MultiStepAction.Back); + withProgressStub.resolves(false); + await assert.isRejected(pickExistingVenvAction(workspace1)); + }); + + test('No venv found', async () => { + pathExistsStub.resolves(false); + const actual = await pickExistingVenvAction(workspace1); + assert.deepStrictEqual(actual, ExistingVenvAction.Create); + }); }); diff --git a/src/test/pythonEnvironments/creation/pyprojectTomlCreateEnv.unit.test.ts b/src/test/pythonEnvironments/creation/pyProjectTomlContext.unit.test.ts similarity index 59% rename from src/test/pythonEnvironments/creation/pyprojectTomlCreateEnv.unit.test.ts rename to src/test/pythonEnvironments/creation/pyProjectTomlContext.unit.test.ts index 3f19aa5775b3..3e787570304a 100644 --- a/src/test/pythonEnvironments/creation/pyprojectTomlCreateEnv.unit.test.ts +++ b/src/test/pythonEnvironments/creation/pyProjectTomlContext.unit.test.ts @@ -6,13 +6,13 @@ import * as chaiAsPromised from 'chai-as-promised'; import * as sinon from 'sinon'; import * as typemoq from 'typemoq'; import { assert, use as chaiUse } from 'chai'; -import { TextDocument, TextDocumentChangeEvent } from 'vscode'; +import { TextDocument } from 'vscode'; import * as cmdApis from '../../../client/common/vscodeApis/commandApis'; import * as workspaceApis from '../../../client/common/vscodeApis/workspaceApis'; import { IDisposableRegistry } from '../../../client/common/types'; -import { registerPyProjectTomlCreateEnvFeatures } from '../../../client/pythonEnvironments/creation/pyprojectTomlCreateEnv'; +import { registerPyProjectTomlFeatures } from '../../../client/pythonEnvironments/creation/pyProjectTomlContext'; -chaiUse(chaiAsPromised); +chaiUse(chaiAsPromised.default); class FakeDisposable { public dispose() { @@ -28,7 +28,7 @@ function getInstallableToml(): typemoq.IMock { .setup((p) => p.getText(typemoq.It.isAny())) .returns( () => - '[project]\nname = "spam"\nversion = "2020.0.0"\n[build-system]\nrequires = ["setuptools ~= 58.0", "cython ~= 0.29.0"]\n[project.optional-dependencies]\ntest = ["pytest"]\ndoc = ["sphinx", "furo"]', + '[project]\nname = "spam"\nversion = "2020.0.0"\n[build-system]\nrequires = ["setuptools ~= 58.0", "cython ~= 0.29.0"]\n[dependency-groups]\ndev = ["ruff", { include-group = "test" }]\ntest = ["pytest"]', ); return pyprojectToml; } @@ -56,16 +56,16 @@ suite('PyProject.toml Create Env Features', () => { const disposables: IDisposableRegistry = []; let getOpenTextDocumentsStub: sinon.SinonStub; let onDidOpenTextDocumentStub: sinon.SinonStub; - let onDidChangeTextDocumentStub: sinon.SinonStub; + let onDidSaveTextDocumentStub: sinon.SinonStub; setup(() => { executeCommandStub = sinon.stub(cmdApis, 'executeCommand'); getOpenTextDocumentsStub = sinon.stub(workspaceApis, 'getOpenTextDocuments'); onDidOpenTextDocumentStub = sinon.stub(workspaceApis, 'onDidOpenTextDocument'); - onDidChangeTextDocumentStub = sinon.stub(workspaceApis, 'onDidChangeTextDocument'); + onDidSaveTextDocumentStub = sinon.stub(workspaceApis, 'onDidSaveTextDocument'); onDidOpenTextDocumentStub.returns(new FakeDisposable()); - onDidChangeTextDocumentStub.returns(new FakeDisposable()); + onDidSaveTextDocumentStub.returns(new FakeDisposable()); }); teardown(() => { @@ -77,27 +77,27 @@ suite('PyProject.toml Create Env Features', () => { const pyprojectToml = getInstallableToml(); getOpenTextDocumentsStub.returns([pyprojectToml.object]); - registerPyProjectTomlCreateEnvFeatures(disposables); + registerPyProjectTomlFeatures(disposables); - assert.ok(executeCommandStub.calledOnceWithExactly('setContext', 'pipInstallableToml', true)); + assert.ok(executeCommandStub.calledWithExactly('setContext', 'pipInstallableToml', true)); }); test('Non installable pyproject.toml is already open in the editor on extension activate', async () => { const pyprojectToml = getNonInstallableToml(); getOpenTextDocumentsStub.returns([pyprojectToml.object]); - registerPyProjectTomlCreateEnvFeatures(disposables); + registerPyProjectTomlFeatures(disposables); - assert.ok(executeCommandStub.calledOnceWithExactly('setContext', 'pipInstallableToml', false)); + assert.ok(executeCommandStub.calledWithExactly('setContext', 'pipInstallableToml', false)); }); test('Some random file open in the editor on extension activate', async () => { const someFile = getSomeFile(); getOpenTextDocumentsStub.returns([someFile.object]); - registerPyProjectTomlCreateEnvFeatures(disposables); + registerPyProjectTomlFeatures(disposables); - assert.ok(executeCommandStub.notCalled); + assert.ok(executeCommandStub.calledWithExactly('setContext', 'pipInstallableToml', false)); }); test('Installable pyproject.toml is opened in the editor', async () => { @@ -113,10 +113,11 @@ suite('PyProject.toml Create Env Features', () => { const pyprojectToml = getInstallableToml(); - registerPyProjectTomlCreateEnvFeatures(disposables); - handler(pyprojectToml.object); + registerPyProjectTomlFeatures(disposables); + assert.ok(executeCommandStub.neverCalledWith('setContext', 'pipInstallableToml', true)); - assert.ok(executeCommandStub.calledOnceWithExactly('setContext', 'pipInstallableToml', true)); + handler(pyprojectToml.object); + assert.ok(executeCommandStub.calledWithExactly('setContext', 'pipInstallableToml', true)); }); test('Non Installable pyproject.toml is opened in the editor', async () => { @@ -132,10 +133,13 @@ suite('PyProject.toml Create Env Features', () => { const pyprojectToml = getNonInstallableToml(); - registerPyProjectTomlCreateEnvFeatures(disposables); + registerPyProjectTomlFeatures(disposables); + assert.ok(executeCommandStub.calledWithExactly('setContext', 'pipInstallableToml', false)); + executeCommandStub.reset(); + handler(pyprojectToml.object); - assert.ok(executeCommandStub.calledOnceWithExactly('setContext', 'pipInstallableToml', false)); + assert.ok(executeCommandStub.calledWithExactly('setContext', 'pipInstallableToml', false)); }); test('Some random file is opened in the editor', async () => { @@ -151,65 +155,111 @@ suite('PyProject.toml Create Env Features', () => { const someFile = getSomeFile(); - registerPyProjectTomlCreateEnvFeatures(disposables); + registerPyProjectTomlFeatures(disposables); + assert.ok(executeCommandStub.calledWithExactly('setContext', 'pipInstallableToml', false)); + executeCommandStub.reset(); + handler(someFile.object); - assert.ok(executeCommandStub.notCalled); + assert.ok(executeCommandStub.neverCalledWith('setContext', 'pipInstallableToml', false)); }); test('Installable pyproject.toml is changed', async () => { getOpenTextDocumentsStub.returns([]); - let handler: (d: TextDocumentChangeEvent) => void = () => { + let handler: (d: TextDocument) => void = () => { /* do nothing */ }; - onDidChangeTextDocumentStub.callsFake((callback) => { + onDidSaveTextDocumentStub.callsFake((callback) => { handler = callback; return new FakeDisposable(); }); const pyprojectToml = getInstallableToml(); - registerPyProjectTomlCreateEnvFeatures(disposables); - handler({ contentChanges: [], document: pyprojectToml.object, reason: undefined }); + registerPyProjectTomlFeatures(disposables); + assert.ok(executeCommandStub.calledWithExactly('setContext', 'pipInstallableToml', false)); - assert.ok(executeCommandStub.calledOnceWithExactly('setContext', 'pipInstallableToml', true)); + handler(pyprojectToml.object); + + assert.ok(executeCommandStub.calledWithExactly('setContext', 'pipInstallableToml', true)); }); test('Non Installable pyproject.toml is changed', async () => { getOpenTextDocumentsStub.returns([]); - let handler: (d: TextDocumentChangeEvent) => void = () => { + let handler: (d: TextDocument) => void = () => { /* do nothing */ }; - onDidChangeTextDocumentStub.callsFake((callback) => { + onDidSaveTextDocumentStub.callsFake((callback) => { handler = callback; return new FakeDisposable(); }); const pyprojectToml = getNonInstallableToml(); - registerPyProjectTomlCreateEnvFeatures(disposables); - handler({ contentChanges: [], document: pyprojectToml.object, reason: undefined }); + registerPyProjectTomlFeatures(disposables); + assert.ok(executeCommandStub.calledWithExactly('setContext', 'pipInstallableToml', false)); + executeCommandStub.reset(); + + handler(pyprojectToml.object); + + assert.ok(executeCommandStub.calledOnceWithExactly('setContext', 'pipInstallableToml', false)); + }); + + test('Non Installable pyproject.toml is changed to Installable', async () => { + getOpenTextDocumentsStub.returns([]); + + let openHandler: (doc: TextDocument) => void = () => { + /* do nothing */ + }; + onDidOpenTextDocumentStub.callsFake((callback) => { + openHandler = callback; + return new FakeDisposable(); + }); + let changeHandler: (d: TextDocument) => void = () => { + /* do nothing */ + }; + onDidSaveTextDocumentStub.callsFake((callback) => { + changeHandler = callback; + return new FakeDisposable(); + }); + + const nonInatallablePyprojectToml = getNonInstallableToml(); + const installablePyprojectToml = getInstallableToml(); + + registerPyProjectTomlFeatures(disposables); + assert.ok(executeCommandStub.calledWithExactly('setContext', 'pipInstallableToml', false)); + executeCommandStub.reset(); + + openHandler(nonInatallablePyprojectToml.object); assert.ok(executeCommandStub.calledOnceWithExactly('setContext', 'pipInstallableToml', false)); + executeCommandStub.reset(); + + changeHandler(installablePyprojectToml.object); + + assert.ok(executeCommandStub.calledOnceWithExactly('setContext', 'pipInstallableToml', true)); }); test('Some random file is changed', async () => { getOpenTextDocumentsStub.returns([]); - let handler: (d: TextDocumentChangeEvent) => void = () => { + let handler: (d: TextDocument) => void = () => { /* do nothing */ }; - onDidChangeTextDocumentStub.callsFake((callback) => { + onDidSaveTextDocumentStub.callsFake((callback) => { handler = callback; return new FakeDisposable(); }); const someFile = getSomeFile(); - registerPyProjectTomlCreateEnvFeatures(disposables); - handler({ contentChanges: [], document: someFile.object, reason: undefined }); + registerPyProjectTomlFeatures(disposables); + assert.ok(executeCommandStub.calledWithExactly('setContext', 'pipInstallableToml', false)); + executeCommandStub.reset(); + + handler(someFile.object); assert.ok(executeCommandStub.notCalled); }); diff --git a/src/test/pythonEnvironments/discovery/locators/windowsKnownPathsLocator.functional.test.ts b/src/test/pythonEnvironments/discovery/locators/windowsKnownPathsLocator.functional.test.ts index cd7715a1bf75..ebebf2a8220e 100644 --- a/src/test/pythonEnvironments/discovery/locators/windowsKnownPathsLocator.functional.test.ts +++ b/src/test/pythonEnvironments/discovery/locators/windowsKnownPathsLocator.functional.test.ts @@ -3,6 +3,7 @@ import { assert } from 'chai'; import * as path from 'path'; +import * as sinon from 'sinon'; import { getOSType, OSType } from '../../../../client/common/utils/platform'; import { PythonEnvKind, PythonEnvSource } from '../../../../client/pythonEnvironments/base/info'; import { BasicEnvInfo, PythonLocatorQuery } from '../../../../client/pythonEnvironments/base/locator'; @@ -10,6 +11,7 @@ import { WindowsPathEnvVarLocator } from '../../../../client/pythonEnvironments/ import { ensureFSTree } from '../../../utils/fs'; import { assertBasicEnvsEqual } from '../../base/locators/envTestUtils'; import { createBasicEnv, getEnvs } from '../../base/common'; +import * as externalDependencies from '../../../../client/pythonEnvironments/common/externalDependencies'; const IS_WINDOWS = getOSType() === OSType.Windows; @@ -71,17 +73,17 @@ suite('Python envs locator - WindowsPathEnvVarLocator', async () => { if (!process.env.PVSC_TEST_FORCE) { this.skip(); } - // eslint-disable-next-line global-require - const sinon = require('sinon'); + } + await ensureFSTree(dataTree, __dirname); + }); + setup(async () => { + if (!IS_WINDOWS) { // eslint-disable-next-line global-require const platformAPI = require('../../../../../client/common/utils/platform'); const stub = sinon.stub(platformAPI, 'getOSType'); stub.returns(OSType.Windows); } - - await ensureFSTree(dataTree, __dirname); - }); - setup(() => { + sinon.stub(externalDependencies, 'inExperiment').returns(true); cleanUps = []; const oldSearchPath = process.env[ENV_VAR]; @@ -97,6 +99,7 @@ suite('Python envs locator - WindowsPathEnvVarLocator', async () => { console.log(err); } }); + sinon.restore(); }); function getActiveLocator(...roots: string[]): WindowsPathEnvVarLocator { diff --git a/src/test/pythonEnvironments/info/interpreter.unit.test.ts b/src/test/pythonEnvironments/info/interpreter.unit.test.ts index 38a916d1db9b..967454dd6c7e 100644 --- a/src/test/pythonEnvironments/info/interpreter.unit.test.ts +++ b/src/test/pythonEnvironments/info/interpreter.unit.test.ts @@ -11,7 +11,7 @@ import { buildPythonExecInfo } from '../../../client/pythonEnvironments/exec'; import { getInterpreterInfo } from '../../../client/pythonEnvironments/info/interpreter'; import { EXTENSION_ROOT_DIR_FOR_TESTS } from '../../constants'; -const script = pathJoin(EXTENSION_ROOT_DIR_FOR_TESTS, 'pythonFiles', 'interpreterInfo.py'); +const script = pathJoin(EXTENSION_ROOT_DIR_FOR_TESTS, 'python_files', 'interpreterInfo.py'); suite('extractInterpreterInfo()', () => { // Tests go here. diff --git a/src/test/pythonEnvironments/nativeAPI.unit.test.ts b/src/test/pythonEnvironments/nativeAPI.unit.test.ts new file mode 100644 index 000000000000..a3696b59c6ac --- /dev/null +++ b/src/test/pythonEnvironments/nativeAPI.unit.test.ts @@ -0,0 +1,338 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +/* eslint-disable class-methods-use-this */ + +import { assert } from 'chai'; +import * as path from 'path'; +import * as typemoq from 'typemoq'; +import * as sinon from 'sinon'; +import * as nativeAPI from '../../client/pythonEnvironments/nativeAPI'; +import { IDiscoveryAPI } from '../../client/pythonEnvironments/base/locator'; +import { + NativeEnvInfo, + NativeEnvManagerInfo, + NativePythonFinder, +} from '../../client/pythonEnvironments/base/locators/common/nativePythonFinder'; +import { Architecture, getPathEnvVariable, isWindows } from '../../client/common/utils/platform'; +import { PythonEnvInfo, PythonEnvKind, PythonEnvType } from '../../client/pythonEnvironments/base/info'; +import { NativePythonEnvironmentKind } from '../../client/pythonEnvironments/base/locators/common/nativePythonUtils'; +import * as condaApi from '../../client/pythonEnvironments/common/environmentManagers/conda'; +import * as pyenvApi from '../../client/pythonEnvironments/common/environmentManagers/pyenv'; +import * as pw from '../../client/pythonEnvironments/base/locators/common/pythonWatcher'; +import * as ws from '../../client/common/vscodeApis/workspaceApis'; + +suite('Native Python API', () => { + let api: IDiscoveryAPI; + let mockFinder: typemoq.IMock; + let setCondaBinaryStub: sinon.SinonStub; + let getCondaPathSettingStub: sinon.SinonStub; + let getCondaEnvDirsStub: sinon.SinonStub; + let setPyEnvBinaryStub: sinon.SinonStub; + let createPythonWatcherStub: sinon.SinonStub; + let mockWatcher: typemoq.IMock; + let getWorkspaceFoldersStub: sinon.SinonStub; + + const basicEnv: NativeEnvInfo = { + displayName: 'Basic Python', + name: 'basic_python', + executable: '/usr/bin/python', + kind: NativePythonEnvironmentKind.LinuxGlobal, + version: `3.12.0`, + prefix: '/usr/bin', + }; + + const basicEnv2: NativeEnvInfo = { + displayName: 'Basic Python', + name: 'basic_python', + executable: '/usr/bin/python', + kind: NativePythonEnvironmentKind.LinuxGlobal, + version: undefined, // this is intentionally set to trigger resolve + prefix: '/usr/bin', + }; + + const expectedBasicEnv: PythonEnvInfo = { + arch: Architecture.Unknown, + id: '/usr/bin/python', + detailedDisplayName: 'Python 3.12.0 (basic_python)', + display: 'Python 3.12.0 (basic_python)', + distro: { org: '' }, + executable: { filename: '/usr/bin/python', sysPrefix: '/usr/bin', ctime: -1, mtime: -1 }, + kind: PythonEnvKind.System, + location: '/usr/bin/python', + source: [], + name: 'basic_python', + type: undefined, + version: { sysVersion: '3.12.0', major: 3, minor: 12, micro: 0 }, + }; + + const conda: NativeEnvInfo = { + displayName: 'Conda Python', + name: 'conda_python', + executable: '/home/user/.conda/envs/conda_python/python', + kind: NativePythonEnvironmentKind.Conda, + version: `3.12.0`, + prefix: '/home/user/.conda/envs/conda_python', + }; + + const conda1: NativeEnvInfo = { + displayName: 'Conda Python', + name: 'conda_python', + executable: '/home/user/.conda/envs/conda_python/python', + kind: NativePythonEnvironmentKind.Conda, + version: undefined, // this is intentionally set to test conda without python + prefix: '/home/user/.conda/envs/conda_python', + }; + + const conda2: NativeEnvInfo = { + displayName: 'Conda Python', + name: 'conda_python', + executable: undefined, // this is intentionally set to test env with no executable + kind: NativePythonEnvironmentKind.Conda, + version: undefined, // this is intentionally set to test conda without python + prefix: '/home/user/.conda/envs/conda_python', + }; + + const exePath = isWindows() + ? path.join('/home/user/.conda/envs/conda_python', 'python.exe') + : path.join('/home/user/.conda/envs/conda_python', 'python'); + + const expectedConda1: PythonEnvInfo = { + arch: Architecture.Unknown, + detailedDisplayName: 'Python 3.12.0 (conda_python)', + display: 'Python 3.12.0 (conda_python)', + distro: { org: '' }, + id: '/home/user/.conda/envs/conda_python/python', + executable: { + filename: '/home/user/.conda/envs/conda_python/python', + sysPrefix: '/home/user/.conda/envs/conda_python', + ctime: -1, + mtime: -1, + }, + kind: PythonEnvKind.Conda, + location: '/home/user/.conda/envs/conda_python', + source: [], + name: 'conda_python', + type: PythonEnvType.Conda, + version: { sysVersion: '3.12.0', major: 3, minor: 12, micro: 0 }, + }; + + const expectedConda2: PythonEnvInfo = { + arch: Architecture.Unknown, + detailedDisplayName: 'Conda Python', + display: 'Conda Python', + distro: { org: '' }, + id: exePath, + executable: { + filename: exePath, + sysPrefix: '/home/user/.conda/envs/conda_python', + ctime: -1, + mtime: -1, + }, + kind: PythonEnvKind.Conda, + location: '/home/user/.conda/envs/conda_python', + source: [], + name: 'conda_python', + type: PythonEnvType.Conda, + version: { sysVersion: undefined, major: -1, minor: -1, micro: -1 }, + }; + + setup(() => { + setCondaBinaryStub = sinon.stub(condaApi, 'setCondaBinary'); + getCondaEnvDirsStub = sinon.stub(condaApi, 'getCondaEnvDirs'); + getCondaPathSettingStub = sinon.stub(condaApi, 'getCondaPathSetting'); + setPyEnvBinaryStub = sinon.stub(pyenvApi, 'setPyEnvBinary'); + getWorkspaceFoldersStub = sinon.stub(ws, 'getWorkspaceFolders'); + getWorkspaceFoldersStub.returns([]); + + createPythonWatcherStub = sinon.stub(pw, 'createPythonWatcher'); + mockWatcher = typemoq.Mock.ofType(); + createPythonWatcherStub.returns(mockWatcher.object); + + mockWatcher.setup((w) => w.watchWorkspace(typemoq.It.isAny())).returns(() => undefined); + mockWatcher.setup((w) => w.watchPath(typemoq.It.isAny(), typemoq.It.isAny())).returns(() => undefined); + mockWatcher.setup((w) => w.unwatchWorkspace(typemoq.It.isAny())).returns(() => undefined); + mockWatcher.setup((w) => w.unwatchPath(typemoq.It.isAny())).returns(() => undefined); + + mockFinder = typemoq.Mock.ofType(); + api = nativeAPI.createNativeEnvironmentsApi(mockFinder.object); + }); + + teardown(() => { + sinon.restore(); + }); + + test('Trigger refresh without resolve', async () => { + mockFinder + .setup((f) => f.refresh()) + .returns(() => { + async function* generator() { + yield* [basicEnv]; + } + return generator(); + }) + .verifiable(typemoq.Times.once()); + + mockFinder.setup((f) => f.resolve(typemoq.It.isAny())).verifiable(typemoq.Times.never()); + + await api.triggerRefresh(); + const actual = api.getEnvs(); + assert.deepEqual(actual, [expectedBasicEnv]); + }); + + test('Trigger refresh with resolve', async () => { + mockFinder + .setup((f) => f.refresh()) + .returns(() => { + async function* generator() { + yield* [basicEnv2]; + } + return generator(); + }) + .verifiable(typemoq.Times.once()); + + mockFinder + .setup((f) => f.resolve(typemoq.It.isAny())) + .returns(() => Promise.resolve(basicEnv)) + .verifiable(typemoq.Times.once()); + + api.triggerRefresh(); + await api.getRefreshPromise(); + + const actual = api.getEnvs(); + assert.deepEqual(actual, [expectedBasicEnv]); + }); + + test('Trigger refresh and use refresh promise API', async () => { + mockFinder + .setup((f) => f.refresh()) + .returns(() => { + async function* generator() { + yield* [basicEnv]; + } + return generator(); + }) + .verifiable(typemoq.Times.once()); + + mockFinder.setup((f) => f.resolve(typemoq.It.isAny())).verifiable(typemoq.Times.never()); + + api.triggerRefresh(); + await api.getRefreshPromise(); + + const actual = api.getEnvs(); + assert.deepEqual(actual, [expectedBasicEnv]); + }); + + test('Conda environment with resolve', async () => { + mockFinder + .setup((f) => f.refresh()) + .returns(() => { + async function* generator() { + yield* [conda1]; + } + return generator(); + }) + .verifiable(typemoq.Times.once()); + mockFinder + .setup((f) => f.resolve(typemoq.It.isAny())) + .returns(() => Promise.resolve(conda)) + .verifiable(typemoq.Times.once()); + + await api.triggerRefresh(); + const actual = api.getEnvs(); + assert.deepEqual(actual, [expectedConda1]); + }); + + test('Ensure no duplication on resolve', async () => { + mockFinder + .setup((f) => f.refresh()) + .returns(() => { + async function* generator() { + yield* [conda1]; + } + return generator(); + }) + .verifiable(typemoq.Times.once()); + mockFinder + .setup((f) => f.resolve(typemoq.It.isAny())) + .returns(() => Promise.resolve(conda)) + .verifiable(typemoq.Times.once()); + + await api.triggerRefresh(); + await api.resolveEnv('/home/user/.conda/envs/conda_python/python'); + const actual = api.getEnvs(); + assert.deepEqual(actual, [expectedConda1]); + }); + + test('Conda environment with no python', async () => { + mockFinder + .setup((f) => f.refresh()) + .returns(() => { + async function* generator() { + yield* [conda2]; + } + return generator(); + }) + .verifiable(typemoq.Times.once()); + mockFinder.setup((f) => f.resolve(typemoq.It.isAny())).verifiable(typemoq.Times.never()); + + await api.triggerRefresh(); + const actual = api.getEnvs(); + assert.deepEqual(actual, [expectedConda2]); + }); + + test('Refresh promise undefined after refresh', async () => { + mockFinder + .setup((f) => f.refresh()) + .returns(() => { + async function* generator() { + yield* [basicEnv]; + } + return generator(); + }) + .verifiable(typemoq.Times.once()); + + mockFinder.setup((f) => f.resolve(typemoq.It.isAny())).verifiable(typemoq.Times.never()); + + await api.triggerRefresh(); + assert.isUndefined(api.getRefreshPromise()); + }); + + test('Setting conda binary', async () => { + getCondaPathSettingStub.returns(undefined); + getCondaEnvDirsStub.resolves(undefined); + const condaFakeDir = getPathEnvVariable()[0]; + const condaMgr: NativeEnvManagerInfo = { + tool: 'Conda', + executable: path.join(condaFakeDir, 'conda'), + }; + mockFinder + .setup((f) => f.refresh()) + .returns(() => { + async function* generator() { + yield* [condaMgr]; + } + return generator(); + }) + .verifiable(typemoq.Times.once()); + await api.triggerRefresh(); + assert.isTrue(setCondaBinaryStub.calledOnceWith(condaMgr.executable)); + }); + + test('Setting pyenv binary', async () => { + const pyenvMgr: NativeEnvManagerInfo = { + tool: 'PyEnv', + executable: '/usr/bin/pyenv', + }; + mockFinder + .setup((f) => f.refresh()) + .returns(() => { + async function* generator() { + yield* [pyenvMgr]; + } + return generator(); + }) + .verifiable(typemoq.Times.once()); + await api.triggerRefresh(); + assert.isTrue(setPyEnvBinaryStub.calledOnceWith(pyenvMgr.executable)); + }); +}); diff --git a/src/test/pythonEnvironments/nativePythonFinder.unit.test.ts b/src/test/pythonEnvironments/nativePythonFinder.unit.test.ts new file mode 100644 index 000000000000..b6182da8111f --- /dev/null +++ b/src/test/pythonEnvironments/nativePythonFinder.unit.test.ts @@ -0,0 +1,90 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { assert } from 'chai'; +import * as sinon from 'sinon'; +import * as typemoq from 'typemoq'; +import { WorkspaceConfiguration } from 'vscode'; +import { + getNativePythonFinder, + isNativeEnvInfo, + NativeEnvInfo, + NativePythonFinder, +} from '../../client/pythonEnvironments/base/locators/common/nativePythonFinder'; +import * as windowsApis from '../../client/common/vscodeApis/windowApis'; +import { MockOutputChannel } from '../mockClasses'; +import * as workspaceApis from '../../client/common/vscodeApis/workspaceApis'; + +suite('Native Python Finder', () => { + let finder: NativePythonFinder; + let createLogOutputChannelStub: sinon.SinonStub; + let getConfigurationStub: sinon.SinonStub; + let configMock: typemoq.IMock; + let getWorkspaceFolderPathsStub: sinon.SinonStub; + + setup(() => { + createLogOutputChannelStub = sinon.stub(windowsApis, 'createLogOutputChannel'); + createLogOutputChannelStub.returns(new MockOutputChannel('locator')); + + getWorkspaceFolderPathsStub = sinon.stub(workspaceApis, 'getWorkspaceFolderPaths'); + getWorkspaceFolderPathsStub.returns([]); + + getConfigurationStub = sinon.stub(workspaceApis, 'getConfiguration'); + configMock = typemoq.Mock.ofType(); + configMock.setup((c) => c.get('venvPath')).returns(() => undefined); + configMock.setup((c) => c.get('venvFolders')).returns(() => []); + configMock.setup((c) => c.get('condaPath')).returns(() => ''); + configMock.setup((c) => c.get('poetryPath')).returns(() => ''); + getConfigurationStub.returns(configMock.object); + + finder = getNativePythonFinder(); + }); + + teardown(() => { + sinon.restore(); + }); + + suiteTeardown(() => { + finder.dispose(); + }); + + test('Refresh should return python environments', async () => { + const envs = []; + for await (const env of finder.refresh()) { + envs.push(env); + } + + // typically all test envs should have at least one environment + assert.isNotEmpty(envs); + }); + + test('Resolve should return python environments with version', async () => { + const envs = []; + for await (const env of finder.refresh()) { + envs.push(env); + } + + // typically all test envs should have at least one environment + assert.isNotEmpty(envs); + + // pick and env without version + const env: NativeEnvInfo | undefined = envs + .filter((e) => isNativeEnvInfo(e)) + .find((e) => e.version && e.version.length > 0 && (e.executable || (e as NativeEnvInfo).prefix)); + + if (env) { + env.version = undefined; + } else { + assert.fail('Expected at least one env with valid version'); + } + + const envPath = env.executable ?? env.prefix; + if (envPath) { + const resolved = await finder.resolve(envPath); + assert.isString(resolved.version, 'Version must be a string'); + assert.isTrue((resolved?.version?.length ?? 0) > 0, 'Version must not be empty'); + } else { + assert.fail('Expected either executable or prefix to be defined'); + } + }); +}); diff --git a/src/test/pythonFiles/formatting/autoPep8Formatted.py b/src/test/pythonFiles/formatting/autoPep8Formatted.py deleted file mode 100644 index e63158d6d4fd..000000000000 --- a/src/test/pythonFiles/formatting/autoPep8Formatted.py +++ /dev/null @@ -1,32 +0,0 @@ - -import math -import sys - - -def example1(): - # This is a long comment. This should be wrapped to fit within 72 characters. - some_tuple = (1, 2, 3, 'a') - some_variable = {'long': 'Long code lines should be wrapped within 79 characters.', - 'other': [math.pi, 100, 200, 300, 9876543210, 'This is a long string that goes on'], - 'more': {'inner': 'This whole logical line should be wrapped.', some_tuple: [1, - 20, 300, 40000, 500000000, 60000000000000000]}} - return (some_tuple, some_variable) - - -def example2(): return {'has_key() is deprecated': True}.has_key( - {'f': 2}.has_key('')) - - -class Example3(object): - def __init__(self, bar): - # Comments should have a space after the hash. - if bar: - bar += 1 - bar = bar * bar - return bar - else: - some_string = """ - Indentation in multiline strings should not be touched. -Only actual code should be reindented. -""" - return (sys.path, some_string) diff --git a/src/test/pythonFiles/formatting/autopep8.output b/src/test/pythonFiles/formatting/autopep8.output deleted file mode 100644 index 80cb3a445811..000000000000 --- a/src/test/pythonFiles/formatting/autopep8.output +++ /dev/null @@ -1,50 +0,0 @@ ---- original/C:\GIT\issues\s p\vscode-python\src\test\pythonFiles\formatting\fileToformat.py -+++ fixed/C:\GIT\issues\s p\vscode-python\src\test\pythonFiles\formatting\fileToformat.py -@@ -1,22 +1,32 @@ - --import math, sys; -+import math -+import sys -+ - - def example1(): -- ####This is a long comment. This should be wrapped to fit within 72 characters. -- some_tuple=( 1,2, 3,'a' ); -- some_variable={'long':'Long code lines should be wrapped within 79 characters.', -- 'other':[math.pi, 100,200,300,9876543210,'This is a long string that goes on'], -- 'more':{'inner':'This whole logical line should be wrapped.',some_tuple:[1, -- 20,300,40000,500000000,60000000000000000]}} -+ # This is a long comment. This should be wrapped to fit within 72 characters. -+ some_tuple = (1, 2, 3, 'a') -+ some_variable = {'long': 'Long code lines should be wrapped within 79 characters.', -+ 'other': [math.pi, 100, 200, 300, 9876543210, 'This is a long string that goes on'], -+ 'more': {'inner': 'This whole logical line should be wrapped.', some_tuple: [1, -+ 20, 300, 40000, 500000000, 60000000000000000]}} - return (some_tuple, some_variable) --def example2(): return {'has_key() is deprecated':True}.has_key({'f':2}.has_key('')); --class Example3( object ): -- def __init__ ( self, bar ): -- #Comments should have a space after the hash. -- if bar : bar+=1; bar=bar* bar ; return bar -- else: -- some_string = """ -+ -+ -+def example2(): return {'has_key() is deprecated': True}.has_key( -+ {'f': 2}.has_key('')) -+ -+ -+class Example3(object): -+ def __init__(self, bar): -+ # Comments should have a space after the hash. -+ if bar: -+ bar += 1 -+ bar = bar * bar -+ return bar -+ else: -+ some_string = """ - Indentation in multiline strings should not be touched. - Only actual code should be reindented. - """ -- return (sys.path, some_string) -+ return (sys.path, some_string) \ No newline at end of file diff --git a/src/test/pythonFiles/formatting/black.output b/src/test/pythonFiles/formatting/black.output deleted file mode 100644 index 4c14d61f2b9b..000000000000 --- a/src/test/pythonFiles/formatting/black.output +++ /dev/null @@ -1,59 +0,0 @@ ---- C:\GIT\issues\s p\vscode-python\src\test\pythonFiles\formatting\fileToformat.py 2020-05-11 18:56:39.835398 +0000 -+++ C:\GIT\issues\s p\vscode-python\src\test\pythonFiles\formatting\fileToformat.py 2020-05-11 19:05:50.969508 +0000 -@@ -1,23 +1,42 @@ -+import math, sys - --import math, sys; - - def example1(): - ####This is a long comment. This should be wrapped to fit within 72 characters. -- some_tuple=( 1,2, 3,'a' ); -- some_variable={'long':'Long code lines should be wrapped within 79 characters.', -- 'other':[math.pi, 100,200,300,9876543210,'This is a long string that goes on'], -- 'more':{'inner':'This whole logical line should be wrapped.',some_tuple:[1, -- 20,300,40000,500000000,60000000000000000]}} -+ some_tuple = (1, 2, 3, "a") -+ some_variable = { -+ "long": "Long code lines should be wrapped within 79 characters.", -+ "other": [ -+ math.pi, -+ 100, -+ 200, -+ 300, -+ 9876543210, -+ "This is a long string that goes on", -+ ], -+ "more": { -+ "inner": "This whole logical line should be wrapped.", -+ some_tuple: [1, 20, 300, 40000, 500000000, 60000000000000000], -+ }, -+ } - return (some_tuple, some_variable) --def example2(): return {'has_key() is deprecated':True}.has_key({'f':2}.has_key('')); --class Example3( object ): -- def __init__ ( self, bar ): -- #Comments should have a space after the hash. -- if bar : bar+=1; bar=bar* bar ; return bar -- else: -- some_string = """ -+ -+ -+def example2(): -+ return {"has_key() is deprecated": True}.has_key({"f": 2}.has_key("")) -+ -+ -+class Example3(object): -+ def __init__(self, bar): -+ # Comments should have a space after the hash. -+ if bar: -+ bar += 1 -+ bar = bar * bar -+ return bar -+ else: -+ some_string = """ - Indentation in multiline strings should not be touched. - Only actual code should be reindented. - """ -- return (sys.path, some_string) -+ return (sys.path, some_string) - \ No newline at end of file diff --git a/src/test/pythonFiles/formatting/blackFormatted.py b/src/test/pythonFiles/formatting/blackFormatted.py deleted file mode 100644 index e7bca8b1298c..000000000000 --- a/src/test/pythonFiles/formatting/blackFormatted.py +++ /dev/null @@ -1,41 +0,0 @@ -import math, sys - - -def example1(): - ####This is a long comment. This should be wrapped to fit within 72 characters. - some_tuple = (1, 2, 3, "a") - some_variable = { - "long": "Long code lines should be wrapped within 79 characters.", - "other": [ - math.pi, - 100, - 200, - 300, - 9876543210, - "This is a long string that goes on", - ], - "more": { - "inner": "This whole logical line should be wrapped.", - some_tuple: [1, 20, 300, 40000, 500000000, 60000000000000000], - }, - } - return (some_tuple, some_variable) - - -def example2(): - return {"has_key() is deprecated": True}.has_key({"f": 2}.has_key("")) - - -class Example3(object): - def __init__(self, bar): - # Comments should have a space after the hash. - if bar: - bar += 1 - bar = bar * bar - return bar - else: - some_string = """ - Indentation in multiline strings should not be touched. -Only actual code should be reindented. -""" - return (sys.path, some_string) diff --git a/src/test/pythonFiles/formatting/dummy.ts b/src/test/pythonFiles/formatting/dummy.ts deleted file mode 100644 index cbab6669e3b8..000000000000 --- a/src/test/pythonFiles/formatting/dummy.ts +++ /dev/null @@ -1,4 +0,0 @@ -// Dummy ts file to ensure this folder gets created in output directory. - -// Code to ensure linter doesn't complain about empty files. -const a = '1'; diff --git a/src/test/pythonFiles/formatting/fileToFormat.py b/src/test/pythonFiles/formatting/fileToFormat.py deleted file mode 100644 index 5b544bd8504d..000000000000 --- a/src/test/pythonFiles/formatting/fileToFormat.py +++ /dev/null @@ -1,22 +0,0 @@ - -import math, sys; - -def example1(): - ####This is a long comment. This should be wrapped to fit within 72 characters. - some_tuple=( 1,2, 3,'a' ); - some_variable={'long':'Long code lines should be wrapped within 79 characters.', - 'other':[math.pi, 100,200,300,9876543210,'This is a long string that goes on'], - 'more':{'inner':'This whole logical line should be wrapped.',some_tuple:[1, - 20,300,40000,500000000,60000000000000000]}} - return (some_tuple, some_variable) -def example2(): return {'has_key() is deprecated':True}.has_key({'f':2}.has_key('')); -class Example3( object ): - def __init__ ( self, bar ): - #Comments should have a space after the hash. - if bar : bar+=1; bar=bar* bar ; return bar - else: - some_string = """ - Indentation in multiline strings should not be touched. -Only actual code should be reindented. -""" - return (sys.path, some_string) diff --git a/src/test/pythonFiles/formatting/fileToFormatOnEnter.py b/src/test/pythonFiles/formatting/fileToFormatOnEnter.py deleted file mode 100644 index 8adfd1fa1233..000000000000 --- a/src/test/pythonFiles/formatting/fileToFormatOnEnter.py +++ /dev/null @@ -1,13 +0,0 @@ -x=1 -"""x=1 -""" - # comment -# x=1 -x+1 # -@x -x.y -if x<=1: -if 1<=x: -def __init__(self, age = 23) -while(1) -x+""" diff --git a/src/test/pythonFiles/formatting/formatWhenDirty.py b/src/test/pythonFiles/formatting/formatWhenDirty.py deleted file mode 100644 index 3fe1b80fde86..000000000000 --- a/src/test/pythonFiles/formatting/formatWhenDirty.py +++ /dev/null @@ -1,3 +0,0 @@ -x = 0 -if x > 0: - x = 1 diff --git a/src/test/pythonFiles/formatting/formatWhenDirtyResult.py b/src/test/pythonFiles/formatting/formatWhenDirtyResult.py deleted file mode 100644 index d0ae06a2a59b..000000000000 --- a/src/test/pythonFiles/formatting/formatWhenDirtyResult.py +++ /dev/null @@ -1,3 +0,0 @@ -x = 0 -if x > 0: - x = 1 diff --git a/src/test/pythonFiles/formatting/pythonGrammar.py b/src/test/pythonFiles/formatting/pythonGrammar.py deleted file mode 100644 index 937cba401d3f..000000000000 --- a/src/test/pythonFiles/formatting/pythonGrammar.py +++ /dev/null @@ -1,1572 +0,0 @@ -# Python test set -- part 1, grammar. -# This just tests whether the parser accepts them all. - -from test.support import check_syntax_error -import inspect -import unittest -import sys -# testing import * -from sys import * - -# different import patterns to check that __annotations__ does not interfere -# with import machinery -import test.ann_module as ann_module -import typing -from collections import ChainMap -from test import ann_module2 -import test - -# These are shared with test_tokenize and other test modules. -# -# Note: since several test cases filter out floats by looking for "e" and ".", -# don't add hexadecimal literals that contain "e" or "E". -VALID_UNDERSCORE_LITERALS = [ - '0_0_0', - '4_2', - '1_0000_0000', - '0b1001_0100', - '0xffff_ffff', - '0o5_7_7', - '1_00_00.5', - '1_00_00.5e5', - '1_00_00e5_1', - '1e1_0', - '.1_4', - '.1_4e1', - '0b_0', - '0x_f', - '0o_5', - '1_00_00j', - '1_00_00.5j', - '1_00_00e5_1j', - '.1_4j', - '(1_2.5+3_3j)', - '(.5_6j)', -] -INVALID_UNDERSCORE_LITERALS = [ - # Trailing underscores: - '0_', - '42_', - '1.4j_', - '0x_', - '0b1_', - '0xf_', - '0o5_', - '0 if 1_Else 1', - # Underscores in the base selector: - '0_b0', - '0_xf', - '0_o5', - # Old-style octal, still disallowed: - '0_7', - '09_99', - # Multiple consecutive underscores: - '4_______2', - '0.1__4', - '0.1__4j', - '0b1001__0100', - '0xffff__ffff', - '0x___', - '0o5__77', - '1e1__0', - '1e1__0j', - # Underscore right before a dot: - '1_.4', - '1_.4j', - # Underscore right after a dot: - '1._4', - '1._4j', - '._5', - '._5j', - # Underscore right after a sign: - '1.0e+_1', - '1.0e+_1j', - # Underscore right before j: - '1.4_j', - '1.4e5_j', - # Underscore right before e: - '1_e1', - '1.4_e1', - '1.4_e1j', - # Underscore right after e: - '1e_1', - '1.4e_1', - '1.4e_1j', - # Complex cases with parens: - '(1+1.5_j_)', - '(1+1.5_j)', -] - - -class TokenTests(unittest.TestCase): - - def test_backslash(self): - # Backslash means line continuation: - x = 1 \ - + 1 - self.assertEqual(x, 2, 'backslash for line continuation') - - # Backslash does not means continuation in comments :\ - x = 0 - self.assertEqual(x, 0, 'backslash ending comment') - - def test_plain_integers(self): - self.assertEqual(type(000), type(0)) - self.assertEqual(0xff, 255) - self.assertEqual(0o377, 255) - self.assertEqual(2147483647, 0o17777777777) - self.assertEqual(0b1001, 9) - # "0x" is not a valid literal - self.assertRaises(SyntaxError, eval, "0x") - from sys import maxsize - if maxsize == 2147483647: - self.assertEqual(-2147483647 - 1, -0o20000000000) - # XXX -2147483648 - self.assertTrue(0o37777777777 > 0) - self.assertTrue(0xffffffff > 0) - self.assertTrue(0b1111111111111111111111111111111 > 0) - for s in ('2147483648', '0o40000000000', '0x100000000', - '0b10000000000000000000000000000000'): - try: - x = eval(s) - except OverflowError: - self.fail("OverflowError on huge integer literal %r" % s) - elif maxsize == 9223372036854775807: - self.assertEqual(-9223372036854775807 - 1, -0o1000000000000000000000) - self.assertTrue(0o1777777777777777777777 > 0) - self.assertTrue(0xffffffffffffffff > 0) - self.assertTrue(0b11111111111111111111111111111111111111111111111111111111111111 > 0) - for s in '9223372036854775808', '0o2000000000000000000000', \ - '0x10000000000000000', \ - '0b100000000000000000000000000000000000000000000000000000000000000': - try: - x = eval(s) - except OverflowError: - self.fail("OverflowError on huge integer literal %r" % s) - else: - self.fail('Weird maxsize value %r' % maxsize) - - def test_long_integers(self): - x = 0 - x = 0xffffffffffffffff - x = 0Xffffffffffffffff - x = 0o77777777777777777 - x = 0O77777777777777777 - x = 123456789012345678901234567890 - x = 0b100000000000000000000000000000000000000000000000000000000000000000000 - x = 0B111111111111111111111111111111111111111111111111111111111111111111111 - - def test_floats(self): - x = 3.14 - x = 314. - x = 0.314 - # XXX x = 000.314 - x = .314 - x = 3e14 - x = 3E14 - x = 3e-14 - x = 3e+14 - x = 3.e14 - x = .3e14 - x = 3.1e4 - - def test_float_exponent_tokenization(self): - # See issue 21642. - self.assertEqual(1 if 1 else 0, 1) - self.assertEqual(1 if 0 else 0, 0) - self.assertRaises(SyntaxError, eval, "0 if 1Else 0") - - def test_underscore_literals(self): - for lit in VALID_UNDERSCORE_LITERALS: - self.assertEqual(eval(lit), eval(lit.replace('_', ''))) - for lit in INVALID_UNDERSCORE_LITERALS: - self.assertRaises(SyntaxError, eval, lit) - # Sanity check: no literal begins with an underscore - self.assertRaises(NameError, eval, "_0") - - def test_string_literals(self): - x = ''; y = ""; self.assertTrue(len(x) == 0 and x == y) - x = '\''; y = "'"; self.assertTrue(len(x) == 1 and x == y and ord(x) == 39) - x = '"'; y = "\""; self.assertTrue(len(x) == 1 and x == y and ord(x) == 34) - x = "doesn't \"shrink\" does it" - y = 'doesn\'t "shrink" does it' - self.assertTrue(len(x) == 24 and x == y) - x = "does \"shrink\" doesn't it" - y = 'does "shrink" doesn\'t it' - self.assertTrue(len(x) == 24 and x == y) - x = """ -The "quick" -brown fox -jumps over -the 'lazy' dog. -""" - y = '\nThe "quick"\nbrown fox\njumps over\nthe \'lazy\' dog.\n' - self.assertEqual(x, y) - y = ''' -The "quick" -brown fox -jumps over -the 'lazy' dog. -''' - self.assertEqual(x, y) - y = "\n\ -The \"quick\"\n\ -brown fox\n\ -jumps over\n\ -the 'lazy' dog.\n\ -" - self.assertEqual(x, y) - y = '\n\ -The \"quick\"\n\ -brown fox\n\ -jumps over\n\ -the \'lazy\' dog.\n\ -' - self.assertEqual(x, y) - - def test_ellipsis(self): - x = ... - self.assertTrue(x is Ellipsis) - self.assertRaises(SyntaxError, eval, ".. .") - - def test_eof_error(self): - samples = ("def foo(", "\ndef foo(", "def foo(\n") - for s in samples: - with self.assertRaises(SyntaxError) as cm: - compile(s, "", "exec") - self.assertIn("unexpected EOF", str(cm.exception)) - -var_annot_global: int # a global annotated is necessary for test_var_annot - -# custom namespace for testing __annotations__ - -class CNS: - def __init__(self): - self._dct = {} - def __setitem__(self, item, value): - self._dct[item.lower()] = value - def __getitem__(self, item): - return self._dct[item] - - -class GrammarTests(unittest.TestCase): - - check_syntax_error = check_syntax_error - - # single_input: NEWLINE | simple_stmt | compound_stmt NEWLINE - # XXX can't test in a script -- this rule is only used when interactive - - # file_input: (NEWLINE | stmt)* ENDMARKER - # Being tested as this very moment this very module - - # expr_input: testlist NEWLINE - # XXX Hard to test -- used only in calls to input() - - def test_eval_input(self): - # testlist ENDMARKER - x = eval('1, 0 or 1') - - def test_var_annot_basics(self): - # all these should be allowed - var1: int = 5 - var2: [int, str] - my_lst = [42] - def one(): - return 1 - int.new_attr: int - [list][0]: type - my_lst[one() - 1]: int = 5 - self.assertEqual(my_lst, [5]) - - def test_var_annot_syntax_errors(self): - # parser pass - check_syntax_error(self, "def f: int") - check_syntax_error(self, "x: int: str") - check_syntax_error(self, "def f():\n" - " nonlocal x: int\n") - # AST pass - check_syntax_error(self, "[x, 0]: int\n") - check_syntax_error(self, "f(): int\n") - check_syntax_error(self, "(x,): int") - check_syntax_error(self, "def f():\n" - " (x, y): int = (1, 2)\n") - # symtable pass - check_syntax_error(self, "def f():\n" - " x: int\n" - " global x\n") - check_syntax_error(self, "def f():\n" - " global x\n" - " x: int\n") - - def test_var_annot_basic_semantics(self): - # execution order - with self.assertRaises(ZeroDivisionError): - no_name[does_not_exist]: no_name_again = 1 / 0 - with self.assertRaises(NameError): - no_name[does_not_exist]: 1 / 0 = 0 - global var_annot_global - - # function semantics - def f(): - st: str = "Hello" - a.b: int = (1, 2) - return st - self.assertEqual(f.__annotations__, {}) - def f_OK(): - x: 1 / 0 - f_OK() - def fbad(): - x: int - print(x) - with self.assertRaises(UnboundLocalError): - fbad() - def f2bad(): - (no_such_global): int - print(no_such_global) - try: - f2bad() - except Exception as e: - self.assertIs(type(e), NameError) - - # class semantics - class C: - __foo: int - s: str = "attr" - z = 2 - def __init__(self, x): - self.x: int = x - self.assertEqual(C.__annotations__, {'_C__foo': int, 's': str}) - with self.assertRaises(NameError): - class CBad: - no_such_name_defined.attr: int = 0 - with self.assertRaises(NameError): - class Cbad2(C): - x: int - x.y: list = [] - - def test_var_annot_metaclass_semantics(self): - class CMeta(type): - @classmethod - def __prepare__(metacls, name, bases, **kwds): - return {'__annotations__': CNS()} - class CC(metaclass=CMeta): - XX: 'ANNOT' - self.assertEqual(CC.__annotations__['xx'], 'ANNOT') - - def test_var_annot_module_semantics(self): - with self.assertRaises(AttributeError): - print(test.__annotations__) - self.assertEqual(ann_module.__annotations__, - {1: 2, 'x': int, 'y': str, 'f': typing.Tuple[int, int]}) - self.assertEqual(ann_module.M.__annotations__, - {'123': 123, 'o': type}) - self.assertEqual(ann_module2.__annotations__, {}) - - def test_var_annot_in_module(self): - # check that functions fail the same way when executed - # outside of module where they were defined - from test.ann_module3 import f_bad_ann, g_bad_ann, D_bad_ann - with self.assertRaises(NameError): - f_bad_ann() - with self.assertRaises(NameError): - g_bad_ann() - with self.assertRaises(NameError): - D_bad_ann(5) - - def test_var_annot_simple_exec(self): - gns = {}; lns = {} - exec("'docstring'\n" - "__annotations__[1] = 2\n" - "x: int = 5\n", gns, lns) - self.assertEqual(lns["__annotations__"], {1: 2, 'x': int}) - with self.assertRaises(KeyError): - gns['__annotations__'] - - def test_var_annot_custom_maps(self): - # tests with custom locals() and __annotations__ - ns = {'__annotations__': CNS()} - exec('X: int; Z: str = "Z"; (w): complex = 1j', ns) - self.assertEqual(ns['__annotations__']['x'], int) - self.assertEqual(ns['__annotations__']['z'], str) - with self.assertRaises(KeyError): - ns['__annotations__']['w'] - nonloc_ns = {} - class CNS2: - def __init__(self): - self._dct = {} - def __setitem__(self, item, value): - nonlocal nonloc_ns - self._dct[item] = value - nonloc_ns[item] = value - def __getitem__(self, item): - return self._dct[item] - exec('x: int = 1', {}, CNS2()) - self.assertEqual(nonloc_ns['__annotations__']['x'], int) - - def test_var_annot_refleak(self): - # complex case: custom locals plus custom __annotations__ - # this was causing refleak - cns = CNS() - nonloc_ns = {'__annotations__': cns} - class CNS2: - def __init__(self): - self._dct = {'__annotations__': cns} - def __setitem__(self, item, value): - nonlocal nonloc_ns - self._dct[item] = value - nonloc_ns[item] = value - def __getitem__(self, item): - return self._dct[item] - exec('X: str', {}, CNS2()) - self.assertEqual(nonloc_ns['__annotations__']['x'], str) - - def test_funcdef(self): - ### [decorators] 'def' NAME parameters ['->' test] ':' suite - ### decorator: '@' dotted_name [ '(' [arglist] ')' ] NEWLINE - ### decorators: decorator+ - ### parameters: '(' [typedargslist] ')' - ### typedargslist: ((tfpdef ['=' test] ',')* - ### ('*' [tfpdef] (',' tfpdef ['=' test])* [',' '**' tfpdef] | '**' tfpdef) - ### | tfpdef ['=' test] (',' tfpdef ['=' test])* [',']) - ### tfpdef: NAME [':' test] - ### varargslist: ((vfpdef ['=' test] ',')* - ### ('*' [vfpdef] (',' vfpdef ['=' test])* [',' '**' vfpdef] | '**' vfpdef) - ### | vfpdef ['=' test] (',' vfpdef ['=' test])* [',']) - ### vfpdef: NAME - def f1(): pass - f1() - f1(*()) - f1(*(), **{}) - def f2(one_argument): pass - def f3(two, arguments): pass - self.assertEqual(f2.__code__.co_varnames, ('one_argument',)) - self.assertEqual(f3.__code__.co_varnames, ('two', 'arguments')) - def a1(one_arg,): pass - def a2(two, args,): pass - def v0(*rest): pass - def v1(a, *rest): pass - def v2(a, b, *rest): pass - - f1() - f2(1) - f2(1,) - f3(1, 2) - f3(1, 2,) - v0() - v0(1) - v0(1,) - v0(1, 2) - v0(1, 2, 3, 4, 5, 6, 7, 8, 9, 0) - v1(1) - v1(1,) - v1(1, 2) - v1(1, 2, 3) - v1(1, 2, 3, 4, 5, 6, 7, 8, 9, 0) - v2(1, 2) - v2(1, 2, 3) - v2(1, 2, 3, 4) - v2(1, 2, 3, 4, 5, 6, 7, 8, 9, 0) - - def d01(a=1): pass - d01() - d01(1) - d01(*(1,)) - d01(*[] or [2]) - d01(*() or (), *{} and (), **() or {}) - d01(**{'a': 2}) - d01(**{'a': 2} or {}) - def d11(a, b=1): pass - d11(1) - d11(1, 2) - d11(1, **{'b': 2}) - def d21(a, b, c=1): pass - d21(1, 2) - d21(1, 2, 3) - d21(*(1, 2, 3)) - d21(1, *(2, 3)) - d21(1, 2, *(3,)) - d21(1, 2, **{'c': 3}) - def d02(a=1, b=2): pass - d02() - d02(1) - d02(1, 2) - d02(*(1, 2)) - d02(1, *(2,)) - d02(1, **{'b': 2}) - d02(**{'a': 1, 'b': 2}) - def d12(a, b=1, c=2): pass - d12(1) - d12(1, 2) - d12(1, 2, 3) - def d22(a, b, c=1, d=2): pass - d22(1, 2) - d22(1, 2, 3) - d22(1, 2, 3, 4) - def d01v(a=1, *rest): pass - d01v() - d01v(1) - d01v(1, 2) - d01v(*(1, 2, 3, 4)) - d01v(*(1,)) - d01v(**{'a': 2}) - def d11v(a, b=1, *rest): pass - d11v(1) - d11v(1, 2) - d11v(1, 2, 3) - def d21v(a, b, c=1, *rest): pass - d21v(1, 2) - d21v(1, 2, 3) - d21v(1, 2, 3, 4) - d21v(*(1, 2, 3, 4)) - d21v(1, 2, **{'c': 3}) - def d02v(a=1, b=2, *rest): pass - d02v() - d02v(1) - d02v(1, 2) - d02v(1, 2, 3) - d02v(1, *(2, 3, 4)) - d02v(**{'a': 1, 'b': 2}) - def d12v(a, b=1, c=2, *rest): pass - d12v(1) - d12v(1, 2) - d12v(1, 2, 3) - d12v(1, 2, 3, 4) - d12v(*(1, 2, 3, 4)) - d12v(1, 2, *(3, 4, 5)) - d12v(1, *(2,), **{'c': 3}) - def d22v(a, b, c=1, d=2, *rest): pass - d22v(1, 2) - d22v(1, 2, 3) - d22v(1, 2, 3, 4) - d22v(1, 2, 3, 4, 5) - d22v(*(1, 2, 3, 4)) - d22v(1, 2, *(3, 4, 5)) - d22v(1, *(2, 3), **{'d': 4}) - - # keyword argument type tests - try: - str('x', **{b'foo': 1}) - except TypeError: - pass - else: - self.fail('Bytes should not work as keyword argument names') - # keyword only argument tests - def pos0key1(*, key): return key - pos0key1(key=100) - def pos2key2(p1, p2, *, k1, k2=100): return p1, p2, k1, k2 - pos2key2(1, 2, k1=100) - pos2key2(1, 2, k1=100, k2=200) - pos2key2(1, 2, k2=100, k1=200) - def pos2key2dict(p1, p2, *, k1=100, k2, **kwarg): return p1, p2, k1, k2, kwarg - pos2key2dict(1, 2, k2=100, tokwarg1=100, tokwarg2=200) - pos2key2dict(1, 2, tokwarg1=100, tokwarg2=200, k2=100) - - self.assertRaises(SyntaxError, eval, "def f(*): pass") - self.assertRaises(SyntaxError, eval, "def f(*,): pass") - self.assertRaises(SyntaxError, eval, "def f(*, **kwds): pass") - - # keyword arguments after *arglist - def f(*args, **kwargs): - return args, kwargs - self.assertEqual(f(1, x=2, *[3, 4], y=5), ((1, 3, 4), - {'x': 2, 'y': 5})) - self.assertEqual(f(1, *(2, 3), 4), ((1, 2, 3, 4), {})) - self.assertRaises(SyntaxError, eval, "f(1, x=2, *(3,4), x=5)") - self.assertEqual(f(**{'eggs': 'scrambled', 'spam': 'fried'}), - ((), {'eggs': 'scrambled', 'spam': 'fried'})) - self.assertEqual(f(spam='fried', **{'eggs': 'scrambled'}), - ((), {'eggs': 'scrambled', 'spam': 'fried'})) - - # Check ast errors in *args and *kwargs - check_syntax_error(self, "f(*g(1=2))") - check_syntax_error(self, "f(**g(1=2))") - - # argument annotation tests - def f(x) -> list: pass - self.assertEqual(f.__annotations__, {'return': list}) - def f(x: int): pass - self.assertEqual(f.__annotations__, {'x': int}) - def f(*x: str): pass - self.assertEqual(f.__annotations__, {'x': str}) - def f(**x: float): pass - self.assertEqual(f.__annotations__, {'x': float}) - def f(x, y: 1 + 2): pass - self.assertEqual(f.__annotations__, {'y': 3}) - def f(a, b: 1, c: 2, d): pass - self.assertEqual(f.__annotations__, {'b': 1, 'c': 2}) - def f(a, b: 1, c: 2, d, e: 3 = 4, f=5, *g: 6): pass - self.assertEqual(f.__annotations__, - {'b': 1, 'c': 2, 'e': 3, 'g': 6}) - def f(a, b: 1, c: 2, d, e: 3 = 4, f=5, *g: 6, h: 7, i=8, j: 9 = 10, - **k: 11) -> 12: pass - self.assertEqual(f.__annotations__, - {'b': 1, 'c': 2, 'e': 3, 'g': 6, 'h': 7, 'j': 9, - 'k': 11, 'return': 12}) - # Check for issue #20625 -- annotations mangling - class Spam: - def f(self, *, __kw: 1): - pass - class Ham(Spam): pass - self.assertEqual(Spam.f.__annotations__, {'_Spam__kw': 1}) - self.assertEqual(Ham.f.__annotations__, {'_Spam__kw': 1}) - # Check for SF Bug #1697248 - mixing decorators and a return annotation - def null(x): return x - @null - def f(x) -> list: pass - self.assertEqual(f.__annotations__, {'return': list}) - - # test closures with a variety of opargs - closure = 1 - def f(): return closure - def f(x=1): return closure - def f(*, k=1): return closure - def f() -> int: return closure - - # Check trailing commas are permitted in funcdef argument list - def f(a,): pass - def f(*args,): pass - def f(**kwds,): pass - def f(a, *args,): pass - def f(a, **kwds,): pass - def f(*args, b,): pass - def f(*, b,): pass - def f(*args, **kwds,): pass - def f(a, *args, b,): pass - def f(a, *, b,): pass - def f(a, *args, **kwds,): pass - def f(*args, b, **kwds,): pass - def f(*, b, **kwds,): pass - def f(a, *args, b, **kwds,): pass - def f(a, *, b, **kwds,): pass - - def test_lambdef(self): - ### lambdef: 'lambda' [varargslist] ':' test - l1 = lambda: 0 - self.assertEqual(l1(), 0) - l2 = lambda: a[d] # XXX just testing the expression - l3 = lambda: [2 < x for x in [-1, 3, 0]] - self.assertEqual(l3(), [0, 1, 0]) - l4 = lambda x=lambda y=lambda z=1: z: y(): x() - self.assertEqual(l4(), 1) - l5 = lambda x, y, z=2: x + y + z - self.assertEqual(l5(1, 2), 5) - self.assertEqual(l5(1, 2, 3), 6) - check_syntax_error(self, "lambda x: x = 2") - check_syntax_error(self, "lambda (None,): None") - l6 = lambda x, y, *, k=20: x + y + k - self.assertEqual(l6(1, 2), 1 + 2 + 20) - self.assertEqual(l6(1, 2, k=10), 1 + 2 + 10) - - # check that trailing commas are permitted - l10 = lambda a,: 0 - l11 = lambda *args,: 0 - l12 = lambda **kwds,: 0 - l13 = lambda a, *args,: 0 - l14 = lambda a, **kwds,: 0 - l15 = lambda *args, b,: 0 - l16 = lambda *, b,: 0 - l17 = lambda *args, **kwds,: 0 - l18 = lambda a, *args, b,: 0 - l19 = lambda a, *, b,: 0 - l20 = lambda a, *args, **kwds,: 0 - l21 = lambda *args, b, **kwds,: 0 - l22 = lambda *, b, **kwds,: 0 - l23 = lambda a, *args, b, **kwds,: 0 - l24 = lambda a, *, b, **kwds,: 0 - - - ### stmt: simple_stmt | compound_stmt - # Tested below - - def test_simple_stmt(self): - ### simple_stmt: small_stmt (';' small_stmt)* [';'] - x = 1; pass; del x - def foo(): - # verify statements that end with semi-colons - x = 1; pass; del x; - foo() - - ### small_stmt: expr_stmt | pass_stmt | del_stmt | flow_stmt | import_stmt | global_stmt | access_stmt - # Tested below - - def test_expr_stmt(self): - # (exprlist '=')* exprlist - 1 - 1, 2, 3 - x = 1 - x = 1, 2, 3 - x = y = z = 1, 2, 3 - x, y, z = 1, 2, 3 - abc = a, b, c = x, y, z = xyz = 1, 2, (3, 4) - - check_syntax_error(self, "x + 1 = 1") - check_syntax_error(self, "a + 1 = b + 2") - - # Check the heuristic for print & exec covers significant cases - # As well as placing some limits on false positives - def test_former_statements_refer_to_builtins(self): - keywords = "print", "exec" - # Cases where we want the custom error - cases = [ - "{} foo", - "{} {{1:foo}}", - "if 1: {} foo", - "if 1: {} {{1:foo}}", - "if 1:\n {} foo", - "if 1:\n {} {{1:foo}}", - ] - for keyword in keywords: - custom_msg = "call to '{}'".format(keyword) - for case in cases: - source = case.format(keyword) - with self.subTest(source=source): - with self.assertRaisesRegex(SyntaxError, custom_msg): - exec(source) - source = source.replace("foo", "(foo.)") - with self.subTest(source=source): - with self.assertRaisesRegex(SyntaxError, "invalid syntax"): - exec(source) - - def test_del_stmt(self): - # 'del' exprlist - abc = [1, 2, 3] - x, y, z = abc - xyz = x, y, z - - del abc - del x, y, (z, xyz) - - def test_pass_stmt(self): - # 'pass' - pass - - # flow_stmt: break_stmt | continue_stmt | return_stmt | raise_stmt - # Tested below - - def test_break_stmt(self): - # 'break' - while 1: break - - def test_continue_stmt(self): - # 'continue' - i = 1 - while i: i = 0; continue - - msg = "" - while not msg: - msg = "ok" - try: - continue - msg = "continue failed to continue inside try" - except: - msg = "continue inside try called except block" - if msg != "ok": - self.fail(msg) - - msg = "" - while not msg: - msg = "finally block not called" - try: - continue - finally: - msg = "ok" - if msg != "ok": - self.fail(msg) - - def test_break_continue_loop(self): - # This test warrants an explanation. It is a test specifically for SF bugs - # #463359 and #462937. The bug is that a 'break' statement executed or - # exception raised inside a try/except inside a loop, *after* a continue - # statement has been executed in that loop, will cause the wrong number of - # arguments to be popped off the stack and the instruction pointer reset to - # a very small number (usually 0.) Because of this, the following test - # *must* written as a function, and the tracking vars *must* be function - # arguments with default values. Otherwise, the test will loop and loop. - - def test_inner(extra_burning_oil=1, count=0): - big_hippo = 2 - while big_hippo: - count += 1 - try: - if extra_burning_oil and big_hippo == 1: - extra_burning_oil -= 1 - break - big_hippo -= 1 - continue - except: - raise - if count > 2 or big_hippo != 1: - self.fail("continue then break in try/except in loop broken!") - test_inner() - - def test_return(self): - # 'return' [testlist] - def g1(): return - def g2(): return 1 - g1() - x = g2() - check_syntax_error(self, "class foo:return 1") - - def test_break_in_finally(self): - count = 0 - while count < 2: - count += 1 - try: - pass - finally: - break - self.assertEqual(count, 1) - - count = 0 - while count < 2: - count += 1 - try: - continue - finally: - break - self.assertEqual(count, 1) - - count = 0 - while count < 2: - count += 1 - try: - 1 / 0 - finally: - break - self.assertEqual(count, 1) - - for count in [0, 1]: - self.assertEqual(count, 0) - try: - pass - finally: - break - self.assertEqual(count, 0) - - for count in [0, 1]: - self.assertEqual(count, 0) - try: - continue - finally: - break - self.assertEqual(count, 0) - - for count in [0, 1]: - self.assertEqual(count, 0) - try: - 1 / 0 - finally: - break - self.assertEqual(count, 0) - - def test_continue_in_finally(self): - count = 0 - while count < 2: - count += 1 - try: - pass - finally: - continue - break - self.assertEqual(count, 2) - - count = 0 - while count < 2: - count += 1 - try: - break - finally: - continue - self.assertEqual(count, 2) - - count = 0 - while count < 2: - count += 1 - try: - 1 / 0 - finally: - continue - break - self.assertEqual(count, 2) - - for count in [0, 1]: - try: - pass - finally: - continue - break - self.assertEqual(count, 1) - - for count in [0, 1]: - try: - break - finally: - continue - self.assertEqual(count, 1) - - for count in [0, 1]: - try: - 1 / 0 - finally: - continue - break - self.assertEqual(count, 1) - - def test_return_in_finally(self): - def g1(): - try: - pass - finally: - return 1 - self.assertEqual(g1(), 1) - - def g2(): - try: - return 2 - finally: - return 3 - self.assertEqual(g2(), 3) - - def g3(): - try: - 1 / 0 - finally: - return 4 - self.assertEqual(g3(), 4) - - def test_yield(self): - # Allowed as standalone statement - def g(): yield 1 - def g(): yield from () - # Allowed as RHS of assignment - def g(): x = yield 1 - def g(): x = yield from () - # Ordinary yield accepts implicit tuples - def g(): yield 1, 1 - def g(): x = yield 1, 1 - # 'yield from' does not - check_syntax_error(self, "def g(): yield from (), 1") - check_syntax_error(self, "def g(): x = yield from (), 1") - # Requires parentheses as subexpression - def g(): 1, (yield 1) - def g(): 1, (yield from ()) - check_syntax_error(self, "def g(): 1, yield 1") - check_syntax_error(self, "def g(): 1, yield from ()") - # Requires parentheses as call argument - def g(): f((yield 1)) - def g(): f((yield 1), 1) - def g(): f((yield from ())) - def g(): f((yield from ()), 1) - check_syntax_error(self, "def g(): f(yield 1)") - check_syntax_error(self, "def g(): f(yield 1, 1)") - check_syntax_error(self, "def g(): f(yield from ())") - check_syntax_error(self, "def g(): f(yield from (), 1)") - # Not allowed at top level - check_syntax_error(self, "yield") - check_syntax_error(self, "yield from") - # Not allowed at class scope - check_syntax_error(self, "class foo:yield 1") - check_syntax_error(self, "class foo:yield from ()") - # Check annotation refleak on SyntaxError - check_syntax_error(self, "def g(a:(yield)): pass") - - def test_yield_in_comprehensions(self): - # Check yield in comprehensions - def g(): [x for x in [(yield 1)]] - def g(): [x for x in [(yield from ())]] - - check = self.check_syntax_error - check("def g(): [(yield x) for x in ()]", - "'yield' inside list comprehension") - check("def g(): [x for x in () if not (yield x)]", - "'yield' inside list comprehension") - check("def g(): [y for x in () for y in [(yield x)]]", - "'yield' inside list comprehension") - check("def g(): {(yield x) for x in ()}", - "'yield' inside set comprehension") - check("def g(): {(yield x): x for x in ()}", - "'yield' inside dict comprehension") - check("def g(): {x: (yield x) for x in ()}", - "'yield' inside dict comprehension") - check("def g(): ((yield x) for x in ())", - "'yield' inside generator expression") - check("def g(): [(yield from x) for x in ()]", - "'yield' inside list comprehension") - check("class C: [(yield x) for x in ()]", - "'yield' inside list comprehension") - check("[(yield x) for x in ()]", - "'yield' inside list comprehension") - - def test_raise(self): - # 'raise' test [',' test] - try: raise RuntimeError('just testing') - except RuntimeError: pass - try: raise KeyboardInterrupt - except KeyboardInterrupt: pass - - def test_import(self): - # 'import' dotted_as_names - import sys - import time, sys - # 'from' dotted_name 'import' ('*' | '(' import_as_names ')' | import_as_names) - from time import time - from time import (time) - # not testable inside a function, but already done at top of the module - # from sys import * - from sys import path, argv - from sys import (path, argv) - from sys import (path, argv,) - - def test_global(self): - # 'global' NAME (',' NAME)* - global a - global a, b - global one, two, three, four, five, six, seven, eight, nine, ten - - def test_nonlocal(self): - # 'nonlocal' NAME (',' NAME)* - x = 0 - y = 0 - def f(): - nonlocal x - nonlocal x, y - - def test_assert(self): - # assertTruestmt: 'assert' test [',' test] - assert 1 - assert 1, 1 - assert lambda x: x - assert 1, lambda x: x + 1 - - try: - assert True - except AssertionError as e: - self.fail("'assert True' should not have raised an AssertionError") - - try: - assert True, 'this should always pass' - except AssertionError as e: - self.fail("'assert True, msg' should not have " - "raised an AssertionError") - - # these tests fail if python is run with -O, so check __debug__ - @unittest.skipUnless(__debug__, "Won't work if __debug__ is False") - def testAssert2(self): - try: - assert 0, "msg" - except AssertionError as e: - self.assertEqual(e.args[0], "msg") - else: - self.fail("AssertionError not raised by assert 0") - - try: - assert False - except AssertionError as e: - self.assertEqual(len(e.args), 0) - else: - self.fail("AssertionError not raised by 'assert False'") - - - ### compound_stmt: if_stmt | while_stmt | for_stmt | try_stmt | funcdef | classdef - # Tested below - - def test_if(self): - # 'if' test ':' suite ('elif' test ':' suite)* ['else' ':' suite] - if 1: pass - if 1: pass - else: pass - if 0: pass - elif 0: pass - if 0: pass - elif 0: pass - elif 0: pass - elif 0: pass - else: pass - - def test_while(self): - # 'while' test ':' suite ['else' ':' suite] - while 0: pass - while 0: pass - else: pass - - # Issue1920: "while 0" is optimized away, - # ensure that the "else" clause is still present. - x = 0 - while 0: - x = 1 - else: - x = 2 - self.assertEqual(x, 2) - - def test_for(self): - # 'for' exprlist 'in' exprlist ':' suite ['else' ':' suite] - for i in 1, 2, 3: pass - for i, j, k in (): pass - else: pass - class Squares: - def __init__(self, max): - self.max = max - self.sofar = [] - def __len__(self): return len(self.sofar) - def __getitem__(self, i): - if not 0 <= i < self.max: raise IndexError - n = len(self.sofar) - while n <= i: - self.sofar.append(n * n) - n = n + 1 - return self.sofar[i] - n = 0 - for x in Squares(10): n = n + x - if n != 285: - self.fail('for over growing sequence') - - result = [] - for x, in [(1,), (2,), (3,)]: - result.append(x) - self.assertEqual(result, [1, 2, 3]) - - def test_try(self): - ### try_stmt: 'try' ':' suite (except_clause ':' suite)+ ['else' ':' suite] - ### | 'try' ':' suite 'finally' ':' suite - ### except_clause: 'except' [expr ['as' expr]] - try: - 1 / 0 - except ZeroDivisionError: - pass - else: - pass - try: 1 / 0 - except EOFError: pass - except TypeError as msg: pass - except: pass - else: pass - try: 1 / 0 - except (EOFError, TypeError, ZeroDivisionError): pass - try: 1 / 0 - except (EOFError, TypeError, ZeroDivisionError) as msg: pass - try: pass - finally: pass - - def test_suite(self): - # simple_stmt | NEWLINE INDENT NEWLINE* (stmt NEWLINE*)+ DEDENT - if 1: pass - if 1: - pass - if 1: - # - # - # - pass - pass - # - pass - # - - def test_test(self): - ### and_test ('or' and_test)* - ### and_test: not_test ('and' not_test)* - ### not_test: 'not' not_test | comparison - if not 1: pass - if 1 and 1: pass - if 1 or 1: pass - if not not not 1: pass - if not 1 and 1 and 1: pass - if 1 and 1 or 1 and 1 and 1 or not 1 and 1: pass - - def test_comparison(self): - ### comparison: expr (comp_op expr)* - ### comp_op: '<'|'>'|'=='|'>='|'<='|'!='|'in'|'not' 'in'|'is'|'is' 'not' - if 1: pass - x = (1 == 1) - if 1 == 1: pass - if 1 != 1: pass - if 1 < 1: pass - if 1 > 1: pass - if 1 <= 1: pass - if 1 >= 1: pass - if 1 is 1: pass - if 1 is not 1: pass - if 1 in (): pass - if 1 not in (): pass - if 1 < 1 > 1 == 1 >= 1 <= 1 != 1 in 1 not in 1 is 1 is not 1: pass - - def test_binary_mask_ops(self): - x = 1 & 1 - x = 1 ^ 1 - x = 1 | 1 - - def test_shift_ops(self): - x = 1 << 1 - x = 1 >> 1 - x = 1 << 1 >> 1 - - def test_additive_ops(self): - x = 1 - x = 1 + 1 - x = 1 - 1 - 1 - x = 1 - 1 + 1 - 1 + 1 - - def test_multiplicative_ops(self): - x = 1 * 1 - x = 1 / 1 - x = 1 % 1 - x = 1 / 1 * 1 % 1 - - def test_unary_ops(self): - x = +1 - x = -1 - x = ~1 - x = ~1 ^ 1 & 1 | 1 & 1 ^ -1 - x = -1 * 1 / 1 + 1 * 1 - -1 * 1 - - def test_selectors(self): - ### trailer: '(' [testlist] ')' | '[' subscript ']' | '.' NAME - ### subscript: expr | [expr] ':' [expr] - - import sys, time - c = sys.path[0] - x = time.time() - x = sys.modules['time'].time() - a = '01234' - c = a[0] - c = a[-1] - s = a[0:5] - s = a[:5] - s = a[0:] - s = a[:] - s = a[-5:] - s = a[:-1] - s = a[-4:-3] - # A rough test of SF bug 1333982. http://python.org/sf/1333982 - # The testing here is fairly incomplete. - # Test cases should include: commas with 1 and 2 colons - d = {} - d[1] = 1 - d[1,] = 2 - d[1, 2] = 3 - d[1, 2, 3] = 4 - L = list(d) - L.sort(key=lambda x: (type(x).__name__, x)) - self.assertEqual(str(L), '[1, (1,), (1, 2), (1, 2, 3)]') - - def test_atoms(self): - ### atom: '(' [testlist] ')' | '[' [testlist] ']' | '{' [dictsetmaker] '}' | NAME | NUMBER | STRING - ### dictsetmaker: (test ':' test (',' test ':' test)* [',']) | (test (',' test)* [',']) - - x = (1) - x = (1 or 2 or 3) - x = (1 or 2 or 3, 2, 3) - - x = [] - x = [1] - x = [1 or 2 or 3] - x = [1 or 2 or 3, 2, 3] - x = [] - - x = {} - x = {'one': 1} - x = {'one': 1,} - x = {'one' or 'two': 1 or 2} - x = {'one': 1, 'two': 2} - x = {'one': 1, 'two': 2,} - x = {'one': 1, 'two': 2, 'three': 3, 'four': 4, 'five': 5, 'six': 6} - - x = {'one'} - x = {'one', 1,} - x = {'one', 'two', 'three'} - x = {2, 3, 4,} - - x = x - x = 'x' - x = 123 - - ### exprlist: expr (',' expr)* [','] - ### testlist: test (',' test)* [','] - # These have been exercised enough above - - def test_classdef(self): - # 'class' NAME ['(' [testlist] ')'] ':' suite - class B: pass - class B2(): pass - class C1(B): pass - class C2(B): pass - class D(C1, C2, B): pass - class C: - def meth1(self): pass - def meth2(self, arg): pass - def meth3(self, a1, a2): pass - - # decorator: '@' dotted_name [ '(' [arglist] ')' ] NEWLINE - # decorators: decorator+ - # decorated: decorators (classdef | funcdef) - def class_decorator(x): return x - @class_decorator - class G: pass - - def test_dictcomps(self): - # dictorsetmaker: ( (test ':' test (comp_for | - # (',' test ':' test)* [','])) | - # (test (comp_for | (',' test)* [','])) ) - nums = [1, 2, 3] - self.assertEqual({i: i + 1 for i in nums}, {1: 2, 2: 3, 3: 4}) - - def test_listcomps(self): - # list comprehension tests - nums = [1, 2, 3, 4, 5] - strs = ["Apple", "Banana", "Coconut"] - spcs = [" Apple", " Banana ", "Coco nut "] - - self.assertEqual([s.strip() for s in spcs], ['Apple', 'Banana', 'Coco nut']) - self.assertEqual([3 * x for x in nums], [3, 6, 9, 12, 15]) - self.assertEqual([x for x in nums if x > 2], [3, 4, 5]) - self.assertEqual([(i, s) for i in nums for s in strs], - [(1, 'Apple'), (1, 'Banana'), (1, 'Coconut'), - (2, 'Apple'), (2, 'Banana'), (2, 'Coconut'), - (3, 'Apple'), (3, 'Banana'), (3, 'Coconut'), - (4, 'Apple'), (4, 'Banana'), (4, 'Coconut'), - (5, 'Apple'), (5, 'Banana'), (5, 'Coconut')]) - self.assertEqual([(i, s) for i in nums for s in [f for f in strs if "n" in f]], - [(1, 'Banana'), (1, 'Coconut'), (2, 'Banana'), (2, 'Coconut'), - (3, 'Banana'), (3, 'Coconut'), (4, 'Banana'), (4, 'Coconut'), - (5, 'Banana'), (5, 'Coconut')]) - self.assertEqual([(lambda a:[a ** i for i in range(a + 1)])(j) for j in range(5)], - [[1], [1, 1], [1, 2, 4], [1, 3, 9, 27], [1, 4, 16, 64, 256]]) - - def test_in_func(l): - return [0 < x < 3 for x in l if x > 2] - - self.assertEqual(test_in_func(nums), [False, False, False]) - - def test_nested_front(): - self.assertEqual([[y for y in [x, x + 1]] for x in [1, 3, 5]], - [[1, 2], [3, 4], [5, 6]]) - - test_nested_front() - - check_syntax_error(self, "[i, s for i in nums for s in strs]") - check_syntax_error(self, "[x if y]") - - suppliers = [ - (1, "Boeing"), - (2, "Ford"), - (3, "Macdonalds") - ] - - parts = [ - (10, "Airliner"), - (20, "Engine"), - (30, "Cheeseburger") - ] - - suppart = [ - (1, 10), (1, 20), (2, 20), (3, 30) - ] - - x = [ - (sname, pname) - for (sno, sname) in suppliers - for (pno, pname) in parts - for (sp_sno, sp_pno) in suppart - if sno == sp_sno and pno == sp_pno - ] - - self.assertEqual(x, [('Boeing', 'Airliner'), ('Boeing', 'Engine'), ('Ford', 'Engine'), - ('Macdonalds', 'Cheeseburger')]) - - def test_genexps(self): - # generator expression tests - g = ([x for x in range(10)] for x in range(1)) - self.assertEqual(next(g), [x for x in range(10)]) - try: - next(g) - self.fail('should produce StopIteration exception') - except StopIteration: - pass - - a = 1 - try: - g = (a for d in a) - next(g) - self.fail('should produce TypeError') - except TypeError: - pass - - self.assertEqual(list((x, y) for x in 'abcd' for y in 'abcd'), [(x, y) for x in 'abcd' for y in 'abcd']) - self.assertEqual(list((x, y) for x in 'ab' for y in 'xy'), [(x, y) for x in 'ab' for y in 'xy']) - - a = [x for x in range(10)] - b = (x for x in (y for y in a)) - self.assertEqual(sum(b), sum([x for x in range(10)])) - - self.assertEqual(sum(x ** 2 for x in range(10)), sum([x ** 2 for x in range(10)])) - self.assertEqual(sum(x * x for x in range(10) if x % 2), sum([x * x for x in range(10) if x % 2])) - self.assertEqual(sum(x for x in (y for y in range(10))), sum([x for x in range(10)])) - self.assertEqual(sum(x for x in (y for y in (z for z in range(10)))), sum([x for x in range(10)])) - self.assertEqual(sum(x for x in [y for y in (z for z in range(10))]), sum([x for x in range(10)])) - self.assertEqual(sum(x for x in (y for y in (z for z in range(10) if True)) if True), sum([x for x in range(10)])) - self.assertEqual(sum(x for x in (y for y in (z for z in range(10) if True) if False) if True), 0) - check_syntax_error(self, "foo(x for x in range(10), 100)") - check_syntax_error(self, "foo(100, x for x in range(10))") - - def test_comprehension_specials(self): - # test for outmost iterable precomputation - x = 10; g = (i for i in range(x)); x = 5 - self.assertEqual(len(list(g)), 10) - - # This should hold, since we're only precomputing outmost iterable. - x = 10; t = False; g = ((i, j) for i in range(x) if t for j in range(x)) - x = 5; t = True; - self.assertEqual([(i, j) for i in range(10) for j in range(5)], list(g)) - - # Grammar allows multiple adjacent 'if's in listcomps and genexps, - # even though it's silly. Make sure it works (ifelse broke this.) - self.assertEqual([x for x in range(10) if x % 2 if x % 3], [1, 5, 7]) - self.assertEqual(list(x for x in range(10) if x % 2 if x % 3), [1, 5, 7]) - - # verify unpacking single element tuples in listcomp/genexp. - self.assertEqual([x for x, in [(4,), (5,), (6,)]], [4, 5, 6]) - self.assertEqual(list(x for x, in [(7,), (8,), (9,)]), [7, 8, 9]) - - def test_with_statement(self): - class manager(object): - def __enter__(self): - return (1, 2) - def __exit__(self, *args): - pass - - with manager(): - pass - with manager() as x: - pass - with manager() as (x, y): - pass - with manager(), manager(): - pass - with manager() as x, manager() as y: - pass - with manager() as x, manager(): - pass - - def test_if_else_expr(self): - # Test ifelse expressions in various cases - def _checkeval(msg, ret): - "helper to check that evaluation of expressions is done correctly" - print(msg) - return ret - - # the next line is not allowed anymore - #self.assertEqual([ x() for x in lambda: True, lambda: False if x() ], [True]) - self.assertEqual([x() for x in (lambda:True, lambda:False) if x()], [True]) - self.assertEqual([x(False) for x in (lambda x:False if x else True, lambda x:True if x else False) if x(False)], [True]) - self.assertEqual((5 if 1 else _checkeval("check 1", 0)), 5) - self.assertEqual((_checkeval("check 2", 0) if 0 else 5), 5) - self.assertEqual((5 and 6 if 0 else 1), 1) - self.assertEqual(((5 and 6) if 0 else 1), 1) - self.assertEqual((5 and (6 if 1 else 1)), 6) - self.assertEqual((0 or _checkeval("check 3", 2) if 0 else 3), 3) - self.assertEqual((1 or _checkeval("check 4", 2) if 1 else _checkeval("check 5", 3)), 1) - self.assertEqual((0 or 5 if 1 else _checkeval("check 6", 3)), 5) - self.assertEqual((not 5 if 1 else 1), False) - self.assertEqual((not 5 if 0 else 1), 1) - self.assertEqual((6 + 1 if 1 else 2), 7) - self.assertEqual((6 - 1 if 1 else 2), 5) - self.assertEqual((6 * 2 if 1 else 4), 12) - self.assertEqual((6 / 2 if 1 else 3), 3) - self.assertEqual((6 < 4 if 0 else 2), 2) - - def test_paren_evaluation(self): - self.assertEqual(16 // (4 // 2), 8) - self.assertEqual((16 // 4) // 2, 2) - self.assertEqual(16 // 4 // 2, 2) - self.assertTrue(False is (2 is 3)) - self.assertFalse((False is 2) is 3) - self.assertFalse(False is 2 is 3) - - def test_matrix_mul(self): - # This is not intended to be a comprehensive test, rather just to be few - # samples of the @ operator in test_grammar.py. - class M: - def __matmul__(self, o): - return 4 - def __imatmul__(self, o): - self.other = o - return self - m = M() - self.assertEqual(m @ m, 4) - m @= 42 - self.assertEqual(m.other, 42) - - def test_async_await(self): - async def test(): - def sum(): - pass - if 1: - await someobj() - - self.assertEqual(test.__name__, 'test') - self.assertTrue(bool(test.__code__.co_flags & inspect.CO_COROUTINE)) - - def decorator(func): - setattr(func, '_marked', True) - return func - - @decorator - async def test2(): - return 22 - self.assertTrue(test2._marked) - self.assertEqual(test2.__name__, 'test2') - self.assertTrue(bool(test2.__code__.co_flags & inspect.CO_COROUTINE)) - - def test_async_for(self): - class Done(Exception): pass - - class AIter: - def __aiter__(self): - return self - async def __anext__(self): - raise StopAsyncIteration - - async def foo(): - async for i in AIter(): - pass - async for i, j in AIter(): - pass - async for i in AIter(): - pass - else: - pass - raise Done - - with self.assertRaises(Done): - foo().send(None) - - def test_async_with(self): - class Done(Exception): pass - - class manager: - async def __aenter__(self): - return (1, 2) - async def __aexit__(self, *exc): - return False - - async def foo(): - async with manager(): - pass - async with manager() as x: - pass - async with manager() as (x, y): - pass - async with manager(), manager(): - pass - async with manager() as x, manager() as y: - pass - async with manager() as x, manager(): - pass - raise Done - - with self.assertRaises(Done): - foo().send(None) - - -if __name__ == '__main__': - unittest.main() diff --git a/src/test/pythonFiles/formatting/yapf.output b/src/test/pythonFiles/formatting/yapf.output deleted file mode 100644 index 43897b1753b0..000000000000 --- a/src/test/pythonFiles/formatting/yapf.output +++ /dev/null @@ -1,57 +0,0 @@ ---- C:\GIT\issues\s p\vscode-python\src\test\pythonFiles\formatting\fileToformat.py (original) -+++ C:\GIT\issues\s p\vscode-python\src\test\pythonFiles\formatting\fileToformat.py (reformatted) -@@ -1,22 +1,40 @@ -+import math, sys - --import math, sys; - - def example1(): - ####This is a long comment. This should be wrapped to fit within 72 characters. -- some_tuple=( 1,2, 3,'a' ); -- some_variable={'long':'Long code lines should be wrapped within 79 characters.', -- 'other':[math.pi, 100,200,300,9876543210,'This is a long string that goes on'], -- 'more':{'inner':'This whole logical line should be wrapped.',some_tuple:[1, -- 20,300,40000,500000000,60000000000000000]}} -+ some_tuple = (1, 2, 3, 'a') -+ some_variable = { -+ 'long': -+ 'Long code lines should be wrapped within 79 characters.', -+ 'other': [ -+ math.pi, 100, 200, 300, 9876543210, -+ 'This is a long string that goes on' -+ ], -+ 'more': { -+ 'inner': 'This whole logical line should be wrapped.', -+ some_tuple: [1, 20, 300, 40000, 500000000, 60000000000000000] -+ } -+ } - return (some_tuple, some_variable) --def example2(): return {'has_key() is deprecated':True}.has_key({'f':2}.has_key('')); --class Example3( object ): -- def __init__ ( self, bar ): -- #Comments should have a space after the hash. -- if bar : bar+=1; bar=bar* bar ; return bar -- else: -- some_string = """ -+ -+ -+def example2(): -+ return { -+ 'has_key() is deprecated': True -+ }.has_key({'f': 2}.has_key('')) -+ -+ -+class Example3(object): -+ def __init__(self, bar): -+ #Comments should have a space after the hash. -+ if bar: -+ bar += 1 -+ bar = bar * bar -+ return bar -+ else: -+ some_string = """ - Indentation in multiline strings should not be touched. - Only actual code should be reindented. - """ -- return (sys.path, some_string) -+ return (sys.path, some_string) diff --git a/src/test/pythonFiles/formatting/yapfFormatted.py b/src/test/pythonFiles/formatting/yapfFormatted.py deleted file mode 100644 index aa3b079379a2..000000000000 --- a/src/test/pythonFiles/formatting/yapfFormatted.py +++ /dev/null @@ -1,40 +0,0 @@ -import math, sys - - -def example1(): - ####This is a long comment. This should be wrapped to fit within 72 characters. - some_tuple = (1, 2, 3, 'a') - some_variable = { - 'long': - 'Long code lines should be wrapped within 79 characters.', - 'other': [ - math.pi, 100, 200, 300, 9876543210, - 'This is a long string that goes on' - ], - 'more': { - 'inner': 'This whole logical line should be wrapped.', - some_tuple: [1, 20, 300, 40000, 500000000, 60000000000000000] - } - } - return (some_tuple, some_variable) - - -def example2(): - return { - 'has_key() is deprecated': True - }.has_key({'f': 2}.has_key('')) - - -class Example3(object): - def __init__(self, bar): - #Comments should have a space after the hash. - if bar: - bar += 1 - bar = bar * bar - return bar - else: - some_string = """ - Indentation in multiline strings should not be touched. -Only actual code should be reindented. -""" - return (sys.path, some_string) diff --git a/src/test/pythonFiles/linting/cwd/.pylintrc b/src/test/pythonFiles/linting/cwd/.pylintrc deleted file mode 100644 index 8530187c095f..000000000000 --- a/src/test/pythonFiles/linting/cwd/.pylintrc +++ /dev/null @@ -1,2 +0,0 @@ -[MESSAGES CONTROL] -disable=C0326,I0011,I0012,C0304,C0103,W0613,E0001,E1101 diff --git a/src/test/pythonFiles/linting/file.py b/src/test/pythonFiles/linting/file.py deleted file mode 100644 index 7b625a769243..000000000000 --- a/src/test/pythonFiles/linting/file.py +++ /dev/null @@ -1,87 +0,0 @@ -"""pylint option block-disable""" - -__revision__ = None - -class Foo(object): - """block-disable test""" - - def __init__(self): - pass - - def meth1(self, arg): - """this issues a message""" - print (self) - - def meth2(self, arg): - """and this one not""" - # pylint: disable=unused-argument - print (self\ - + "foo") - - def meth3(self): - """test one line disabling""" - # no error - print (self.bla) # pylint: disable=no-member - # error - print (self.blop) - - def meth4(self): - """test re-enabling""" - # pylint: disable=no-member - # no error - print (self.bla) - print (self.blop) - # pylint: enable=no-member - # error - print (self.blip) - - def meth5(self): - """test IF sub-block re-enabling""" - # pylint: disable=no-member - # no error - print (self.bla) - if self.blop: - # pylint: enable=no-member - # error - print (self.blip) - else: - # no error - print (self.blip) - # no error - print (self.blip) - - def meth6(self): - """test TRY/EXCEPT sub-block re-enabling""" - # pylint: disable=no-member - # no error - print (self.bla) - try: - # pylint: enable=no-member - # error - print (self.blip) - except UndefinedName: # pylint: disable=undefined-variable - # no error - print (self.blip) - # no error - print (self.blip) - - def meth7(self): - """test one line block opening disabling""" - if self.blop: # pylint: disable=no-member - # error - print (self.blip) - else: - # error - print (self.blip) - # error - print (self.blip) - - - def meth8(self): - """test late disabling""" - # error - print (self.blip) - # pylint: disable=no-member - # no error - print (self.bla) - print (self.blop) diff --git a/src/test/pythonFiles/linting/flake8config/.flake8 b/src/test/pythonFiles/linting/flake8config/.flake8 deleted file mode 100644 index 99ff2b9f819c..000000000000 --- a/src/test/pythonFiles/linting/flake8config/.flake8 +++ /dev/null @@ -1,2 +0,0 @@ -[flake8] -ignore = E302,E901,E127,E261,E261,E261,E303 \ No newline at end of file diff --git a/src/test/pythonFiles/linting/flake8config/file.py b/src/test/pythonFiles/linting/flake8config/file.py deleted file mode 100644 index 9abe4993dddd..000000000000 --- a/src/test/pythonFiles/linting/flake8config/file.py +++ /dev/null @@ -1,86 +0,0 @@ -"""pylint option block-disable""" - -__revision__ = None - - -class Foo(object): - """block-disable test""" - - def __init__(self): - pass - - def meth1(self, arg): - """this issues a message""" - print(self) - - def meth2(self, arg): - """and this one not""" - # pylint: disable=unused-argument - print(self + "foo") - - def meth3(self): - """test one line disabling""" - # no error - print(self.bla) # pylint: disable=no-member - # error - print(self.blop) - - def meth4(self): - """test re-enabling""" - # pylint: disable=no-member - # no error - print(self.bla) - print(self.blop) - # pylint: enable=no-member - # error - print(self.blip) - - def meth5(self): - """test IF sub-block re-enabling""" - # pylint: disable=no-member - # no error - print(self.bla) - if self.blop: - # pylint: enable=no-member - # error - print(self.blip) - else: - # no error - print(self.blip) - # no error - print(self.blip) - - def meth6(self): - """test TRY/EXCEPT sub-block re-enabling""" - # pylint: disable=no-member - # no error - print(self.bla) - try: - # pylint: enable=no-member - # error - print(self.blip) - except UndefinedName: # pylint: disable=undefined-variable - # no error - print(self.blip) - # no error - print(self.blip) - - def meth7(self): - """test one line block opening disabling""" - if self.blop: # pylint: disable=no-member - # error - print(self.blip) - else: - # error - print(self.blip) - # error - print(self.blip) - - def meth8(self): - """test late disabling""" - # error - print(self.blip) - # pylint: disable=no-member - # no error - print(self.bla) - print(self.blop) diff --git a/src/test/pythonFiles/linting/minCheck.py b/src/test/pythonFiles/linting/minCheck.py deleted file mode 100644 index d93fa56f7e8a..000000000000 --- a/src/test/pythonFiles/linting/minCheck.py +++ /dev/null @@ -1 +0,0 @@ -filter(lambda x: x == 1, [1, 1, 2]) diff --git a/src/test/pythonFiles/linting/print.py b/src/test/pythonFiles/linting/print.py deleted file mode 100644 index fca61311fc84..000000000000 --- a/src/test/pythonFiles/linting/print.py +++ /dev/null @@ -1 +0,0 @@ -print x \ No newline at end of file diff --git a/src/test/pythonFiles/linting/pycodestyleconfig/.pycodestyle b/src/test/pythonFiles/linting/pycodestyleconfig/.pycodestyle deleted file mode 100644 index b7c78f49db84..000000000000 --- a/src/test/pythonFiles/linting/pycodestyleconfig/.pycodestyle +++ /dev/null @@ -1,2 +0,0 @@ -[pycodestyle] -ignore = E302,E901,E127,E261,E261,E261,E303 diff --git a/src/test/pythonFiles/linting/pycodestyleconfig/file.py b/src/test/pythonFiles/linting/pycodestyleconfig/file.py deleted file mode 100644 index 047ba0dc679e..000000000000 --- a/src/test/pythonFiles/linting/pycodestyleconfig/file.py +++ /dev/null @@ -1,87 +0,0 @@ -"""pylint option block-disable""" - -__revision__ = None - -class Foo(object): - """block-disable test""" - - def __init__(self): - pass - - def meth1(self, arg): - """this issues a message""" - print self - - def meth2(self, arg): - """and this one not""" - # pylint: disable=unused-argument - print self\ - + "foo" - - def meth3(self): - """test one line disabling""" - # no error - print self.bla # pylint: disable=no-member - # error - print self.blop - - def meth4(self): - """test re-enabling""" - # pylint: disable=no-member - # no error - print self.bla - print self.blop - # pylint: enable=no-member - # error - print self.blip - - def meth5(self): - """test IF sub-block re-enabling""" - # pylint: disable=no-member - # no error - print self.bla - if self.blop: - # pylint: enable=no-member - # error - print self.blip - else: - # no error - print self.blip - # no error - print self.blip - - def meth6(self): - """test TRY/EXCEPT sub-block re-enabling""" - # pylint: disable=no-member - # no error - print self.bla - try: - # pylint: enable=no-member - # error - print self.blip - except UndefinedName: # pylint: disable=undefined-variable - # no error - print self.blip - # no error - print self.blip - - def meth7(self): - """test one line block opening disabling""" - if self.blop: # pylint: disable=no-member - # error - print self.blip - else: - # error - print self.blip - # error - print self.blip - - - def meth8(self): - """test late disabling""" - # error - print self.blip - # pylint: disable=no-member - # no error - print self.bla - print self.blop \ No newline at end of file diff --git a/src/test/pythonFiles/linting/pydocstyleconfig27/.pydocstyle b/src/test/pythonFiles/linting/pydocstyleconfig27/.pydocstyle deleted file mode 100644 index 19020834ad32..000000000000 --- a/src/test/pythonFiles/linting/pydocstyleconfig27/.pydocstyle +++ /dev/null @@ -1,2 +0,0 @@ -[pydocstyle] -ignore=D400,D401,D402,D403,D404,D203,D102,D107 diff --git a/src/test/pythonFiles/linting/pydocstyleconfig27/file.py b/src/test/pythonFiles/linting/pydocstyleconfig27/file.py deleted file mode 100644 index 047ba0dc679e..000000000000 --- a/src/test/pythonFiles/linting/pydocstyleconfig27/file.py +++ /dev/null @@ -1,87 +0,0 @@ -"""pylint option block-disable""" - -__revision__ = None - -class Foo(object): - """block-disable test""" - - def __init__(self): - pass - - def meth1(self, arg): - """this issues a message""" - print self - - def meth2(self, arg): - """and this one not""" - # pylint: disable=unused-argument - print self\ - + "foo" - - def meth3(self): - """test one line disabling""" - # no error - print self.bla # pylint: disable=no-member - # error - print self.blop - - def meth4(self): - """test re-enabling""" - # pylint: disable=no-member - # no error - print self.bla - print self.blop - # pylint: enable=no-member - # error - print self.blip - - def meth5(self): - """test IF sub-block re-enabling""" - # pylint: disable=no-member - # no error - print self.bla - if self.blop: - # pylint: enable=no-member - # error - print self.blip - else: - # no error - print self.blip - # no error - print self.blip - - def meth6(self): - """test TRY/EXCEPT sub-block re-enabling""" - # pylint: disable=no-member - # no error - print self.bla - try: - # pylint: enable=no-member - # error - print self.blip - except UndefinedName: # pylint: disable=undefined-variable - # no error - print self.blip - # no error - print self.blip - - def meth7(self): - """test one line block opening disabling""" - if self.blop: # pylint: disable=no-member - # error - print self.blip - else: - # error - print self.blip - # error - print self.blip - - - def meth8(self): - """test late disabling""" - # error - print self.blip - # pylint: disable=no-member - # no error - print self.bla - print self.blop \ No newline at end of file diff --git a/src/test/pythonFiles/linting/pylintconfig/.pylintrc b/src/test/pythonFiles/linting/pylintconfig/.pylintrc deleted file mode 100644 index 74ea19101f81..000000000000 --- a/src/test/pythonFiles/linting/pylintconfig/.pylintrc +++ /dev/null @@ -1,2 +0,0 @@ -[MESSAGES CONTROL] -disable=I0011,C0304,C0103,W0613,E0001,E1101 diff --git a/src/test/pythonFiles/linting/pylintconfig/file.py b/src/test/pythonFiles/linting/pylintconfig/file.py deleted file mode 100644 index 047ba0dc679e..000000000000 --- a/src/test/pythonFiles/linting/pylintconfig/file.py +++ /dev/null @@ -1,87 +0,0 @@ -"""pylint option block-disable""" - -__revision__ = None - -class Foo(object): - """block-disable test""" - - def __init__(self): - pass - - def meth1(self, arg): - """this issues a message""" - print self - - def meth2(self, arg): - """and this one not""" - # pylint: disable=unused-argument - print self\ - + "foo" - - def meth3(self): - """test one line disabling""" - # no error - print self.bla # pylint: disable=no-member - # error - print self.blop - - def meth4(self): - """test re-enabling""" - # pylint: disable=no-member - # no error - print self.bla - print self.blop - # pylint: enable=no-member - # error - print self.blip - - def meth5(self): - """test IF sub-block re-enabling""" - # pylint: disable=no-member - # no error - print self.bla - if self.blop: - # pylint: enable=no-member - # error - print self.blip - else: - # no error - print self.blip - # no error - print self.blip - - def meth6(self): - """test TRY/EXCEPT sub-block re-enabling""" - # pylint: disable=no-member - # no error - print self.bla - try: - # pylint: enable=no-member - # error - print self.blip - except UndefinedName: # pylint: disable=undefined-variable - # no error - print self.blip - # no error - print self.blip - - def meth7(self): - """test one line block opening disabling""" - if self.blop: # pylint: disable=no-member - # error - print self.blip - else: - # error - print self.blip - # error - print self.blip - - - def meth8(self): - """test late disabling""" - # error - print self.blip - # pylint: disable=no-member - # no error - print self.bla - print self.blop \ No newline at end of file diff --git a/src/test/pythonFiles/linting/pylintconfig/file2.py b/src/test/pythonFiles/linting/pylintconfig/file2.py deleted file mode 100644 index f375c984aa2e..000000000000 --- a/src/test/pythonFiles/linting/pylintconfig/file2.py +++ /dev/null @@ -1,19 +0,0 @@ -"""pylint option block-disable""" - -__revision__ = None - -class Foo(object): - """block-disable test""" - - def __init__(self): - pass - - def meth1(self, arg): - """meth1""" - print self.blop - - def meth2(self, arg): - """meth2""" - # pylint: disable=unused-argument - print self\ - + "foo" diff --git a/src/test/pythonFiles/linting/threeLineLints.py b/src/test/pythonFiles/linting/threeLineLints.py deleted file mode 100644 index e8b578d93f11..000000000000 --- a/src/test/pythonFiles/linting/threeLineLints.py +++ /dev/null @@ -1,24 +0,0 @@ -"""pylint messages with three lines of output""" - -__revision__ = None - -class Foo(object): - - def __init__(self): - pass - - def meth1(self,arg): - """missing a space between 'self' and 'arg'. This should trigger the - following three line lint warning:: - - C: 10, 0: Exactly one space required after comma - def meth1(self,arg): - ^ (bad-whitespace) - - The following three lines of tuples should also cause three-line lint - errors due to "Exactly one space required after comma" messages. - """ - a = (1,2) - b = (1,2) - c = (1,2) - print (self) diff --git a/src/test/pythonFiles/refactoring/source folder/.vscode/.ropeproject/config.py b/src/test/pythonFiles/refactoring/source folder/.vscode/.ropeproject/config.py deleted file mode 100644 index dee2d1ae9a6b..000000000000 --- a/src/test/pythonFiles/refactoring/source folder/.vscode/.ropeproject/config.py +++ /dev/null @@ -1,114 +0,0 @@ -# The default ``config.py`` -# flake8: noqa - - -def set_prefs(prefs): - """This function is called before opening the project""" - - # Specify which files and folders to ignore in the project. - # Changes to ignored resources are not added to the history and - # VCSs. Also they are not returned in `Project.get_files()`. - # Note that ``?`` and ``*`` match all characters but slashes. - # '*.pyc': matches 'test.pyc' and 'pkg/test.pyc' - # 'mod*.pyc': matches 'test/mod1.pyc' but not 'mod/1.pyc' - # '.svn': matches 'pkg/.svn' and all of its children - # 'build/*.o': matches 'build/lib.o' but not 'build/sub/lib.o' - # 'build//*.o': matches 'build/lib.o' and 'build/sub/lib.o' - prefs['ignored_resources'] = ['*.pyc', '*~', '.ropeproject', - '.hg', '.svn', '_svn', '.git', '.tox'] - - # Specifies which files should be considered python files. It is - # useful when you have scripts inside your project. Only files - # ending with ``.py`` are considered to be python files by - # default. - # prefs['python_files'] = ['*.py'] - - # Custom source folders: By default rope searches the project - # for finding source folders (folders that should be searched - # for finding modules). You can add paths to that list. Note - # that rope guesses project source folders correctly most of the - # time; use this if you have any problems. - # The folders should be relative to project root and use '/' for - # separating folders regardless of the platform rope is running on. - # 'src/my_source_folder' for instance. - # prefs.add('source_folders', 'src') - - # You can extend python path for looking up modules - # prefs.add('python_path', '~/python/') - - # Should rope save object information or not. - prefs['save_objectdb'] = True - prefs['compress_objectdb'] = False - - # If `True`, rope analyzes each module when it is being saved. - prefs['automatic_soa'] = True - # The depth of calls to follow in static object analysis - prefs['soa_followed_calls'] = 0 - - # If `False` when running modules or unit tests "dynamic object - # analysis" is turned off. This makes them much faster. - prefs['perform_doa'] = True - - # Rope can check the validity of its object DB when running. - prefs['validate_objectdb'] = True - - # How many undos to hold? - prefs['max_history_items'] = 32 - - # Shows whether to save history across sessions. - prefs['save_history'] = True - prefs['compress_history'] = False - - # Set the number spaces used for indenting. According to - # :PEP:`8`, it is best to use 4 spaces. Since most of rope's - # unit-tests use 4 spaces it is more reliable, too. - prefs['indent_size'] = 4 - - # Builtin and c-extension modules that are allowed to be imported - # and inspected by rope. - prefs['extension_modules'] = [] - - # Add all standard c-extensions to extension_modules list. - prefs['import_dynload_stdmods'] = True - - # If `True` modules with syntax errors are considered to be empty. - # The default value is `False`; When `False` syntax errors raise - # `rope.base.exceptions.ModuleSyntaxError` exception. - prefs['ignore_syntax_errors'] = False - - # If `True`, rope ignores unresolvable imports. Otherwise, they - # appear in the importing namespace. - prefs['ignore_bad_imports'] = False - - # If `True`, rope will insert new module imports as - # `from import ` by default. - prefs['prefer_module_from_imports'] = False - - # If `True`, rope will transform a comma list of imports into - # multiple separate import statements when organizing - # imports. - prefs['split_imports'] = False - - # If `True`, rope will remove all top-level import statements and - # reinsert them at the top of the module when making changes. - prefs['pull_imports_to_top'] = True - - # If `True`, rope will sort imports alphabetically by module name instead - # of alphabetically by import statement, with from imports after normal - # imports. - prefs['sort_imports_alphabetically'] = False - - # Location of implementation of - # rope.base.oi.type_hinting.interfaces.ITypeHintingFactory In general - # case, you don't have to change this value, unless you're an rope expert. - # Change this value to inject you own implementations of interfaces - # listed in module rope.base.oi.type_hinting.providers.interfaces - # For example, you can add you own providers for Django Models, or disable - # the search type-hinting in a class hierarchy, etc. - prefs['type_hinting_factory'] = ( - 'rope.base.oi.type_hinting.factory.default_type_hinting_factory') - - -def project_opened(project): - """This function is called after opening the project""" - # Do whatever you like here! diff --git a/src/test/pythonFiles/refactoring/source folder/.vscode/.ropeproject/objectdb b/src/test/pythonFiles/refactoring/source folder/.vscode/.ropeproject/objectdb deleted file mode 100644 index 0a47446c0ad2..000000000000 Binary files a/src/test/pythonFiles/refactoring/source folder/.vscode/.ropeproject/objectdb and /dev/null differ diff --git a/src/test/pythonFiles/sorting/noconfig/after.py b/src/test/pythonFiles/sorting/noconfig/after.py deleted file mode 100644 index b768c396014c..000000000000 --- a/src/test/pythonFiles/sorting/noconfig/after.py +++ /dev/null @@ -1,16 +0,0 @@ -import io; sys; json -import traceback - -import rope -import rope.base.project -import rope.base.taskhandle -from rope.base import libutils -from rope.refactor.extract import ExtractMethod, ExtractVariable -from rope.refactor.rename import Rename - -WORKSPACE_ROOT = sys.argv[1] -ROPE_PROJECT_FOLDER = sys.argv[2] - - -def test(): - pass diff --git a/src/test/pythonFiles/sorting/noconfig/before.py b/src/test/pythonFiles/sorting/noconfig/before.py deleted file mode 100644 index fcd7318b5c02..000000000000 --- a/src/test/pythonFiles/sorting/noconfig/before.py +++ /dev/null @@ -1,18 +0,0 @@ -import io; sys; json -import traceback -import rope - -import rope.base.project -import rope.base.taskhandle - -WORKSPACE_ROOT = sys.argv[1] -ROPE_PROJECT_FOLDER = sys.argv[2] - - -def test(): - pass - -from rope.base import libutils -from rope.refactor.rename import Rename -from rope.refactor.extract import ExtractMethod, ExtractVariable - \ No newline at end of file diff --git a/src/test/pythonFiles/sorting/noconfig/original.py b/src/test/pythonFiles/sorting/noconfig/original.py deleted file mode 100644 index fcd7318b5c02..000000000000 --- a/src/test/pythonFiles/sorting/noconfig/original.py +++ /dev/null @@ -1,18 +0,0 @@ -import io; sys; json -import traceback -import rope - -import rope.base.project -import rope.base.taskhandle - -WORKSPACE_ROOT = sys.argv[1] -ROPE_PROJECT_FOLDER = sys.argv[2] - - -def test(): - pass - -from rope.base import libutils -from rope.refactor.rename import Rename -from rope.refactor.extract import ExtractMethod, ExtractVariable - \ No newline at end of file diff --git a/src/test/pythonFiles/sorting/withconfig/.isort.cfg b/src/test/pythonFiles/sorting/withconfig/.isort.cfg deleted file mode 100644 index 68da732e2b4b..000000000000 --- a/src/test/pythonFiles/sorting/withconfig/.isort.cfg +++ /dev/null @@ -1,2 +0,0 @@ -[settings] -force_single_line=True \ No newline at end of file diff --git a/src/test/pythonFiles/sorting/withconfig/after.py b/src/test/pythonFiles/sorting/withconfig/after.py deleted file mode 100644 index e1fd315dbf92..000000000000 --- a/src/test/pythonFiles/sorting/withconfig/after.py +++ /dev/null @@ -1,3 +0,0 @@ -from third_party import (lib1, lib2, lib3, - lib4, lib5, lib6, - lib7, lib8, lib9) \ No newline at end of file diff --git a/src/test/pythonFiles/sorting/withconfig/before.1.py b/src/test/pythonFiles/sorting/withconfig/before.1.py deleted file mode 100644 index e1fd315dbf92..000000000000 --- a/src/test/pythonFiles/sorting/withconfig/before.1.py +++ /dev/null @@ -1,3 +0,0 @@ -from third_party import (lib1, lib2, lib3, - lib4, lib5, lib6, - lib7, lib8, lib9) \ No newline at end of file diff --git a/src/test/pythonFiles/sorting/withconfig/before.py b/src/test/pythonFiles/sorting/withconfig/before.py deleted file mode 100644 index e1fd315dbf92..000000000000 --- a/src/test/pythonFiles/sorting/withconfig/before.py +++ /dev/null @@ -1,3 +0,0 @@ -from third_party import (lib1, lib2, lib3, - lib4, lib5, lib6, - lib7, lib8, lib9) \ No newline at end of file diff --git a/src/test/pythonFiles/sorting/withconfig/original.1.py b/src/test/pythonFiles/sorting/withconfig/original.1.py deleted file mode 100644 index e1fd315dbf92..000000000000 --- a/src/test/pythonFiles/sorting/withconfig/original.1.py +++ /dev/null @@ -1,3 +0,0 @@ -from third_party import (lib1, lib2, lib3, - lib4, lib5, lib6, - lib7, lib8, lib9) \ No newline at end of file diff --git a/src/test/pythonFiles/sorting/withconfig/original.py b/src/test/pythonFiles/sorting/withconfig/original.py deleted file mode 100644 index e1fd315dbf92..000000000000 --- a/src/test/pythonFiles/sorting/withconfig/original.py +++ /dev/null @@ -1,3 +0,0 @@ -from third_party import (lib1, lib2, lib3, - lib4, lib5, lib6, - lib7, lib8, lib9) \ No newline at end of file diff --git a/src/test/pythonFiles/datascience/simple_nb.ipynb b/src/test/python_files/datascience/simple_nb.ipynb similarity index 100% rename from src/test/pythonFiles/datascience/simple_nb.ipynb rename to src/test/python_files/datascience/simple_nb.ipynb diff --git a/src/test/pythonFiles/datascience/simple_note_book.py b/src/test/python_files/datascience/simple_note_book.py similarity index 100% rename from src/test/pythonFiles/datascience/simple_note_book.py rename to src/test/python_files/datascience/simple_note_book.py diff --git a/src/test/pythonFiles/debugging/forever.py b/src/test/python_files/debugging/forever.py similarity index 100% rename from src/test/pythonFiles/debugging/forever.py rename to src/test/python_files/debugging/forever.py diff --git a/src/test/pythonFiles/debugging/logMessage.py b/src/test/python_files/debugging/logMessage.py similarity index 100% rename from src/test/pythonFiles/debugging/logMessage.py rename to src/test/python_files/debugging/logMessage.py diff --git a/src/test/pythonFiles/debugging/loopyTest.py b/src/test/python_files/debugging/loopyTest.py similarity index 100% rename from src/test/pythonFiles/debugging/loopyTest.py rename to src/test/python_files/debugging/loopyTest.py diff --git a/src/test/pythonFiles/debugging/multiThread.py b/src/test/python_files/debugging/multiThread.py similarity index 100% rename from src/test/pythonFiles/debugging/multiThread.py rename to src/test/python_files/debugging/multiThread.py diff --git a/src/test/pythonFiles/debugging/printSysArgv.py b/src/test/python_files/debugging/printSysArgv.py similarity index 100% rename from src/test/pythonFiles/debugging/printSysArgv.py rename to src/test/python_files/debugging/printSysArgv.py diff --git a/src/test/pythonFiles/debugging/sample2.py b/src/test/python_files/debugging/sample2.py similarity index 100% rename from src/test/pythonFiles/debugging/sample2.py rename to src/test/python_files/debugging/sample2.py diff --git a/src/test/pythonFiles/debugging/sample2WithoutSleep.py b/src/test/python_files/debugging/sample2WithoutSleep.py similarity index 100% rename from src/test/pythonFiles/debugging/sample2WithoutSleep.py rename to src/test/python_files/debugging/sample2WithoutSleep.py diff --git a/src/test/pythonFiles/debugging/sample3WithEx.py b/src/test/python_files/debugging/sample3WithEx.py similarity index 100% rename from src/test/pythonFiles/debugging/sample3WithEx.py rename to src/test/python_files/debugging/sample3WithEx.py diff --git a/src/test/pythonFiles/debugging/sampleWithAssertEx.py b/src/test/python_files/debugging/sampleWithAssertEx.py similarity index 100% rename from src/test/pythonFiles/debugging/sampleWithAssertEx.py rename to src/test/python_files/debugging/sampleWithAssertEx.py diff --git a/src/test/pythonFiles/debugging/sampleWithSleep.py b/src/test/python_files/debugging/sampleWithSleep.py similarity index 100% rename from src/test/pythonFiles/debugging/sampleWithSleep.py rename to src/test/python_files/debugging/sampleWithSleep.py diff --git a/src/test/pythonFiles/debugging/simplePrint.py b/src/test/python_files/debugging/simplePrint.py similarity index 100% rename from src/test/pythonFiles/debugging/simplePrint.py rename to src/test/python_files/debugging/simplePrint.py diff --git a/src/test/pythonFiles/debugging/stackFrame.py b/src/test/python_files/debugging/stackFrame.py similarity index 100% rename from src/test/pythonFiles/debugging/stackFrame.py rename to src/test/python_files/debugging/stackFrame.py diff --git a/src/test/pythonFiles/debugging/startAndWait.py b/src/test/python_files/debugging/startAndWait.py similarity index 100% rename from src/test/pythonFiles/debugging/startAndWait.py rename to src/test/python_files/debugging/startAndWait.py diff --git a/src/test/pythonFiles/debugging/stdErrOutput.py b/src/test/python_files/debugging/stdErrOutput.py similarity index 100% rename from src/test/pythonFiles/debugging/stdErrOutput.py rename to src/test/python_files/debugging/stdErrOutput.py diff --git a/src/test/pythonFiles/debugging/stdOutOutput.py b/src/test/python_files/debugging/stdOutOutput.py similarity index 100% rename from src/test/pythonFiles/debugging/stdOutOutput.py rename to src/test/python_files/debugging/stdOutOutput.py diff --git a/src/test/pythonFiles/debugging/wait_for_file.py b/src/test/python_files/debugging/wait_for_file.py similarity index 100% rename from src/test/pythonFiles/debugging/wait_for_file.py rename to src/test/python_files/debugging/wait_for_file.py diff --git a/src/test/python_files/dummy.py b/src/test/python_files/dummy.py new file mode 100644 index 000000000000..10f13768abe0 --- /dev/null +++ b/src/test/python_files/dummy.py @@ -0,0 +1 @@ +#dummy file to be opened by Test VS Code instance, so that Python Configuration (workspace configuration will be initialized) \ No newline at end of file diff --git a/src/test/pythonFiles/environments/conda/Scripts/conda.exe b/src/test/python_files/environments/conda/Scripts/conda.exe similarity index 100% rename from src/test/pythonFiles/environments/conda/Scripts/conda.exe rename to src/test/python_files/environments/conda/Scripts/conda.exe diff --git a/pythonFiles/tests/testing_tools/adapter/.data/simple/tests/__init__.py b/src/test/python_files/environments/conda/bin/python similarity index 100% rename from pythonFiles/tests/testing_tools/adapter/.data/simple/tests/__init__.py rename to src/test/python_files/environments/conda/bin/python diff --git a/pythonFiles/tests/testing_tools/adapter/.data/syntax-error/tests/__init__.py b/src/test/python_files/environments/conda/envs/numpy/bin/python similarity index 100% rename from pythonFiles/tests/testing_tools/adapter/.data/syntax-error/tests/__init__.py rename to src/test/python_files/environments/conda/envs/numpy/bin/python diff --git a/src/test/pythonFiles/environments/conda/envs/numpy/python.exe b/src/test/python_files/environments/conda/envs/numpy/python.exe similarity index 100% rename from src/test/pythonFiles/environments/conda/envs/numpy/python.exe rename to src/test/python_files/environments/conda/envs/numpy/python.exe diff --git a/src/test/pythonFiles/environments/conda/envs/scipy/python.exe b/src/test/python_files/environments/conda/envs/scipy/bin/python similarity index 100% rename from src/test/pythonFiles/environments/conda/envs/scipy/python.exe rename to src/test/python_files/environments/conda/envs/scipy/bin/python diff --git a/src/test/pythonFiles/environments/path1/python.exe b/src/test/python_files/environments/conda/envs/scipy/python.exe similarity index 100% rename from src/test/pythonFiles/environments/path1/python.exe rename to src/test/python_files/environments/conda/envs/scipy/python.exe diff --git a/src/test/pythonFiles/environments/path1/one b/src/test/python_files/environments/path1/one similarity index 100% rename from src/test/pythonFiles/environments/path1/one rename to src/test/python_files/environments/path1/one diff --git a/src/test/pythonFiles/environments/path1/one.exe b/src/test/python_files/environments/path1/one.exe similarity index 100% rename from src/test/pythonFiles/environments/path1/one.exe rename to src/test/python_files/environments/path1/one.exe diff --git a/src/test/pythonFiles/environments/path2/python.exe b/src/test/python_files/environments/path1/python.exe similarity index 100% rename from src/test/pythonFiles/environments/path2/python.exe rename to src/test/python_files/environments/path1/python.exe diff --git a/src/test/pythonFiles/environments/path2/one b/src/test/python_files/environments/path2/one similarity index 100% rename from src/test/pythonFiles/environments/path2/one rename to src/test/python_files/environments/path2/one diff --git a/src/test/pythonFiles/environments/path2/one.exe b/src/test/python_files/environments/path2/one.exe similarity index 100% rename from src/test/pythonFiles/environments/path2/one.exe rename to src/test/python_files/environments/path2/one.exe diff --git a/src/test/pythonFiles/tensorBoard/noMatch.py b/src/test/python_files/environments/path2/python.exe similarity index 100% rename from src/test/pythonFiles/tensorBoard/noMatch.py rename to src/test/python_files/environments/path2/python.exe diff --git a/src/test/pythonFiles/intellisense/test.py b/src/test/python_files/intellisense/test.py similarity index 100% rename from src/test/pythonFiles/intellisense/test.py rename to src/test/python_files/intellisense/test.py diff --git a/src/test/pythonFiles/shebang/plain.py b/src/test/python_files/shebang/plain.py similarity index 100% rename from src/test/pythonFiles/shebang/plain.py rename to src/test/python_files/shebang/plain.py diff --git a/src/test/pythonFiles/shebang/shebang.py b/src/test/python_files/shebang/shebang.py similarity index 100% rename from src/test/pythonFiles/shebang/shebang.py rename to src/test/python_files/shebang/shebang.py diff --git a/src/test/pythonFiles/shebang/shebangEnv.py b/src/test/python_files/shebang/shebangEnv.py similarity index 100% rename from src/test/pythonFiles/shebang/shebangEnv.py rename to src/test/python_files/shebang/shebangEnv.py diff --git a/src/test/pythonFiles/shebang/shebangInvalid.py b/src/test/python_files/shebang/shebangInvalid.py similarity index 100% rename from src/test/pythonFiles/shebang/shebangInvalid.py rename to src/test/python_files/shebang/shebangInvalid.py diff --git a/src/test/python_files/tensorBoard/noMatch.py b/src/test/python_files/tensorBoard/noMatch.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/src/test/pythonFiles/tensorBoard/sourcefile.py b/src/test/python_files/tensorBoard/sourcefile.py similarity index 100% rename from src/test/pythonFiles/tensorBoard/sourcefile.py rename to src/test/python_files/tensorBoard/sourcefile.py diff --git a/src/test/pythonFiles/tensorBoard/tensorboard_import.ipynb b/src/test/python_files/tensorBoard/tensorboard_import.ipynb similarity index 100% rename from src/test/pythonFiles/tensorBoard/tensorboard_import.ipynb rename to src/test/python_files/tensorBoard/tensorboard_import.ipynb diff --git a/src/test/pythonFiles/tensorBoard/tensorboard_imports.py b/src/test/python_files/tensorBoard/tensorboard_imports.py similarity index 100% rename from src/test/pythonFiles/tensorBoard/tensorboard_imports.py rename to src/test/python_files/tensorBoard/tensorboard_imports.py diff --git a/src/test/pythonFiles/tensorBoard/tensorboard_launch.py b/src/test/python_files/tensorBoard/tensorboard_launch.py similarity index 100% rename from src/test/pythonFiles/tensorBoard/tensorboard_launch.py rename to src/test/python_files/tensorBoard/tensorboard_launch.py diff --git a/src/test/pythonFiles/tensorBoard/tensorboard_nbextension.ipynb b/src/test/python_files/tensorBoard/tensorboard_nbextension.ipynb similarity index 100% rename from src/test/pythonFiles/tensorBoard/tensorboard_nbextension.ipynb rename to src/test/python_files/tensorBoard/tensorboard_nbextension.ipynb diff --git a/src/test/pythonFiles/terminalExec/sample1_normalized.py b/src/test/python_files/terminalExec/sample1_normalized.py similarity index 100% rename from src/test/pythonFiles/terminalExec/sample1_normalized.py rename to src/test/python_files/terminalExec/sample1_normalized.py diff --git a/src/test/pythonFiles/terminalExec/sample1_normalized_selection.py b/src/test/python_files/terminalExec/sample1_normalized_selection.py similarity index 100% rename from src/test/pythonFiles/terminalExec/sample1_normalized_selection.py rename to src/test/python_files/terminalExec/sample1_normalized_selection.py diff --git a/src/test/pythonFiles/terminalExec/sample1_raw.py b/src/test/python_files/terminalExec/sample1_raw.py similarity index 100% rename from src/test/pythonFiles/terminalExec/sample1_raw.py rename to src/test/python_files/terminalExec/sample1_raw.py diff --git a/src/test/pythonFiles/terminalExec/sample2_normalized.py b/src/test/python_files/terminalExec/sample2_normalized.py similarity index 100% rename from src/test/pythonFiles/terminalExec/sample2_normalized.py rename to src/test/python_files/terminalExec/sample2_normalized.py diff --git a/src/test/pythonFiles/terminalExec/sample2_normalized_selection.py b/src/test/python_files/terminalExec/sample2_normalized_selection.py similarity index 100% rename from src/test/pythonFiles/terminalExec/sample2_normalized_selection.py rename to src/test/python_files/terminalExec/sample2_normalized_selection.py diff --git a/src/test/pythonFiles/terminalExec/sample2_raw.py b/src/test/python_files/terminalExec/sample2_raw.py similarity index 100% rename from src/test/pythonFiles/terminalExec/sample2_raw.py rename to src/test/python_files/terminalExec/sample2_raw.py diff --git a/src/test/pythonFiles/terminalExec/sample3_normalized.py b/src/test/python_files/terminalExec/sample3_normalized.py similarity index 100% rename from src/test/pythonFiles/terminalExec/sample3_normalized.py rename to src/test/python_files/terminalExec/sample3_normalized.py diff --git a/src/test/pythonFiles/terminalExec/sample3_normalized_selection.py b/src/test/python_files/terminalExec/sample3_normalized_selection.py similarity index 100% rename from src/test/pythonFiles/terminalExec/sample3_normalized_selection.py rename to src/test/python_files/terminalExec/sample3_normalized_selection.py diff --git a/src/test/pythonFiles/terminalExec/sample3_raw.py b/src/test/python_files/terminalExec/sample3_raw.py similarity index 100% rename from src/test/pythonFiles/terminalExec/sample3_raw.py rename to src/test/python_files/terminalExec/sample3_raw.py diff --git a/src/test/pythonFiles/terminalExec/sample4_normalized.py b/src/test/python_files/terminalExec/sample4_normalized.py similarity index 100% rename from src/test/pythonFiles/terminalExec/sample4_normalized.py rename to src/test/python_files/terminalExec/sample4_normalized.py diff --git a/src/test/pythonFiles/terminalExec/sample4_normalized_selection.py b/src/test/python_files/terminalExec/sample4_normalized_selection.py similarity index 100% rename from src/test/pythonFiles/terminalExec/sample4_normalized_selection.py rename to src/test/python_files/terminalExec/sample4_normalized_selection.py diff --git a/src/test/pythonFiles/terminalExec/sample4_raw.py b/src/test/python_files/terminalExec/sample4_raw.py similarity index 100% rename from src/test/pythonFiles/terminalExec/sample4_raw.py rename to src/test/python_files/terminalExec/sample4_raw.py diff --git a/src/test/pythonFiles/terminalExec/sample5_normalized.py b/src/test/python_files/terminalExec/sample5_normalized.py similarity index 100% rename from src/test/pythonFiles/terminalExec/sample5_normalized.py rename to src/test/python_files/terminalExec/sample5_normalized.py diff --git a/src/test/pythonFiles/terminalExec/sample5_normalized_selection.py b/src/test/python_files/terminalExec/sample5_normalized_selection.py similarity index 100% rename from src/test/pythonFiles/terminalExec/sample5_normalized_selection.py rename to src/test/python_files/terminalExec/sample5_normalized_selection.py diff --git a/src/test/pythonFiles/terminalExec/sample5_raw.py b/src/test/python_files/terminalExec/sample5_raw.py similarity index 100% rename from src/test/pythonFiles/terminalExec/sample5_raw.py rename to src/test/python_files/terminalExec/sample5_raw.py diff --git a/src/test/pythonFiles/terminalExec/sample6_normalized.py b/src/test/python_files/terminalExec/sample6_normalized.py similarity index 100% rename from src/test/pythonFiles/terminalExec/sample6_normalized.py rename to src/test/python_files/terminalExec/sample6_normalized.py diff --git a/src/test/pythonFiles/terminalExec/sample6_normalized_selection.py b/src/test/python_files/terminalExec/sample6_normalized_selection.py similarity index 100% rename from src/test/pythonFiles/terminalExec/sample6_normalized_selection.py rename to src/test/python_files/terminalExec/sample6_normalized_selection.py diff --git a/src/test/pythonFiles/terminalExec/sample6_raw.py b/src/test/python_files/terminalExec/sample6_raw.py similarity index 100% rename from src/test/pythonFiles/terminalExec/sample6_raw.py rename to src/test/python_files/terminalExec/sample6_raw.py diff --git a/src/test/pythonFiles/terminalExec/sample7_normalized.py b/src/test/python_files/terminalExec/sample7_normalized.py similarity index 100% rename from src/test/pythonFiles/terminalExec/sample7_normalized.py rename to src/test/python_files/terminalExec/sample7_normalized.py diff --git a/src/test/pythonFiles/terminalExec/sample7_normalized_selection.py b/src/test/python_files/terminalExec/sample7_normalized_selection.py similarity index 100% rename from src/test/pythonFiles/terminalExec/sample7_normalized_selection.py rename to src/test/python_files/terminalExec/sample7_normalized_selection.py diff --git a/src/test/pythonFiles/terminalExec/sample7_raw.py b/src/test/python_files/terminalExec/sample7_raw.py similarity index 100% rename from src/test/pythonFiles/terminalExec/sample7_raw.py rename to src/test/python_files/terminalExec/sample7_raw.py diff --git a/src/test/pythonFiles/terminalExec/sample8_normalized.py b/src/test/python_files/terminalExec/sample8_normalized.py similarity index 100% rename from src/test/pythonFiles/terminalExec/sample8_normalized.py rename to src/test/python_files/terminalExec/sample8_normalized.py diff --git a/src/test/pythonFiles/terminalExec/sample8_normalized_selection.py b/src/test/python_files/terminalExec/sample8_normalized_selection.py similarity index 100% rename from src/test/pythonFiles/terminalExec/sample8_normalized_selection.py rename to src/test/python_files/terminalExec/sample8_normalized_selection.py diff --git a/src/test/pythonFiles/terminalExec/sample8_raw.py b/src/test/python_files/terminalExec/sample8_raw.py similarity index 100% rename from src/test/pythonFiles/terminalExec/sample8_raw.py rename to src/test/python_files/terminalExec/sample8_raw.py diff --git a/src/test/python_files/terminalExec/sample_invalid_smart_selection.py b/src/test/python_files/terminalExec/sample_invalid_smart_selection.py new file mode 100644 index 000000000000..73d9e0fba066 --- /dev/null +++ b/src/test/python_files/terminalExec/sample_invalid_smart_selection.py @@ -0,0 +1,10 @@ +def beliebig(x, y, *mehr): + print "x=", x, ", x=", y + print "mehr: ", mehr + +list = [ +1, +2, +3, +] +print("Above is invalid");print("deprecated");print("show warning") diff --git a/src/test/pythonFiles/terminalExec/sample_normalized.py b/src/test/python_files/terminalExec/sample_normalized.py similarity index 100% rename from src/test/pythonFiles/terminalExec/sample_normalized.py rename to src/test/python_files/terminalExec/sample_normalized.py diff --git a/src/test/pythonFiles/terminalExec/sample_normalized_selection.py b/src/test/python_files/terminalExec/sample_normalized_selection.py similarity index 100% rename from src/test/pythonFiles/terminalExec/sample_normalized_selection.py rename to src/test/python_files/terminalExec/sample_normalized_selection.py diff --git a/src/test/pythonFiles/terminalExec/sample_raw.py b/src/test/python_files/terminalExec/sample_raw.py similarity index 100% rename from src/test/pythonFiles/terminalExec/sample_raw.py rename to src/test/python_files/terminalExec/sample_raw.py diff --git a/src/test/python_files/terminalExec/sample_smart_selection.py b/src/test/python_files/terminalExec/sample_smart_selection.py new file mode 100644 index 000000000000..3933f06b5d65 --- /dev/null +++ b/src/test/python_files/terminalExec/sample_smart_selection.py @@ -0,0 +1,21 @@ +my_dict = { + "key1": "value1", + "key2": "value2" +} +#Sample + +print("Audi");print("BMW");print("Mercedes") + +# print("dont print me") + +def my_dogs(): + print("Corgi") + print("Husky") + print("Corgi2") + print("Husky2") + print("no dogs") + +# Skip me to prove that you did a good job +def next_func(): + print("You") + diff --git a/src/test/repl/nativeRepl.test.ts b/src/test/repl/nativeRepl.test.ts new file mode 100644 index 000000000000..2cf18cefe1f7 --- /dev/null +++ b/src/test/repl/nativeRepl.test.ts @@ -0,0 +1,189 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +/* eslint-disable no-unused-expressions */ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import * as TypeMoq from 'typemoq'; +import * as sinon from 'sinon'; +import { Disposable, EventEmitter, NotebookDocument, Uri } from 'vscode'; +import { expect } from 'chai'; + +import { IInterpreterService } from '../../client/interpreter/contracts'; +import { PythonEnvironment } from '../../client/pythonEnvironments/info'; +import * as NativeReplModule from '../../client/repl/nativeRepl'; +import * as persistentState from '../../client/common/persistentState'; +import * as PythonServer from '../../client/repl/pythonServer'; +import * as vscodeWorkspaceApis from '../../client/common/vscodeApis/workspaceApis'; +import * as replController from '../../client/repl/replController'; +import { executeCommand } from '../../client/common/vscodeApis/commandApis'; + +suite('REPL - Native REPL', () => { + let interpreterService: TypeMoq.IMock; + + let disposable: TypeMoq.IMock; + let disposableArray: Disposable[] = []; + let setReplDirectoryStub: sinon.SinonStub; + let setReplControllerSpy: sinon.SinonSpy; + let getWorkspaceStateValueStub: sinon.SinonStub; + let updateWorkspaceStateValueStub: sinon.SinonStub; + let createReplControllerStub: sinon.SinonStub; + let mockNotebookController: any; + + setup(() => { + (NativeReplModule as any).nativeRepl = undefined; + + mockNotebookController = { + id: 'mockController', + dispose: sinon.stub(), + updateNotebookAffinity: sinon.stub(), + createNotebookCellExecution: sinon.stub(), + variableProvider: null, + }; + + interpreterService = TypeMoq.Mock.ofType(); + interpreterService + .setup((i) => i.getActiveInterpreter(TypeMoq.It.isAny())) + .returns(() => Promise.resolve(({ path: 'ps' } as unknown) as PythonEnvironment)); + disposable = TypeMoq.Mock.ofType(); + disposableArray = [disposable.object]; + + createReplControllerStub = sinon.stub(replController, 'createReplController').returns(mockNotebookController); + setReplDirectoryStub = sinon.stub(NativeReplModule.NativeRepl.prototype as any, 'setReplDirectory').resolves(); + setReplControllerSpy = sinon.spy(NativeReplModule.NativeRepl.prototype, 'setReplController'); + updateWorkspaceStateValueStub = sinon.stub(persistentState, 'updateWorkspaceStateValue').resolves(); + }); + + teardown(async () => { + disposableArray.forEach((d) => { + if (d) { + d.dispose(); + } + }); + disposableArray = []; + sinon.restore(); + executeCommand('workbench.action.closeActiveEditor'); + }); + + test('getNativeRepl should call create constructor', async () => { + const createMethodStub = sinon.stub(NativeReplModule.NativeRepl, 'create'); + interpreterService + .setup((i) => i.getActiveInterpreter(TypeMoq.It.isAny())) + .returns(() => Promise.resolve(({ path: 'ps' } as unknown) as PythonEnvironment)); + const interpreter = await interpreterService.object.getActiveInterpreter(); + await NativeReplModule.getNativeRepl(interpreter as PythonEnvironment, disposableArray); + + expect(createMethodStub.calledOnce).to.be.true; + }); + + test('sendToNativeRepl should look for memento URI if notebook document is undefined', async () => { + getWorkspaceStateValueStub = sinon.stub(persistentState, 'getWorkspaceStateValue').returns(undefined); + interpreterService + .setup((i) => i.getActiveInterpreter(TypeMoq.It.isAny())) + .returns(() => Promise.resolve(({ path: 'ps' } as unknown) as PythonEnvironment)); + const interpreter = await interpreterService.object.getActiveInterpreter(); + const nativeRepl = await NativeReplModule.getNativeRepl(interpreter as PythonEnvironment, disposableArray); + + nativeRepl.sendToNativeRepl(undefined, false); + + expect(getWorkspaceStateValueStub.calledOnce).to.be.true; + }); + + test('sendToNativeRepl should call updateWorkspaceStateValue', async () => { + getWorkspaceStateValueStub = sinon.stub(persistentState, 'getWorkspaceStateValue').returns('myNameIsMemento'); + interpreterService + .setup((i) => i.getActiveInterpreter(TypeMoq.It.isAny())) + .returns(() => Promise.resolve(({ path: 'ps' } as unknown) as PythonEnvironment)); + const interpreter = await interpreterService.object.getActiveInterpreter(); + const nativeRepl = await NativeReplModule.getNativeRepl(interpreter as PythonEnvironment, disposableArray); + + nativeRepl.sendToNativeRepl(undefined, false); + + expect(updateWorkspaceStateValueStub.calledOnce).to.be.true; + }); + + test('create should call setReplDirectory, setReplController', async () => { + const interpreter = await interpreterService.object.getActiveInterpreter(); + interpreterService + .setup((i) => i.getActiveInterpreter(TypeMoq.It.isAny())) + .returns(() => Promise.resolve(({ path: 'ps' } as unknown) as PythonEnvironment)); + + await NativeReplModule.NativeRepl.create(interpreter as PythonEnvironment); + + expect(setReplDirectoryStub.calledOnce).to.be.true; + expect(setReplControllerSpy.calledOnce).to.be.true; + expect(createReplControllerStub.calledOnce).to.be.true; + }); + + test('watchNotebookClosed should clean up resources when notebook is closed', async () => { + const notebookCloseEmitter = new EventEmitter(); + sinon.stub(vscodeWorkspaceApis, 'onDidCloseNotebookDocument').callsFake((handler) => { + const disposable = notebookCloseEmitter.event(handler); + return disposable; + }); + + const mockPythonServer = { + onCodeExecuted: new EventEmitter().event, + execute: sinon.stub().resolves({ status: true, output: 'test output' }), + executeSilently: sinon.stub().resolves({ status: true, output: 'test output' }), + interrupt: sinon.stub(), + input: sinon.stub(), + checkValidCommand: sinon.stub().resolves(true), + dispose: sinon.stub(), + isExecuting: false, + isDisposed: false, + }; + + // Track the number of times createPythonServer was called + let createPythonServerCallCount = 0; + sinon.stub(PythonServer, 'createPythonServer').callsFake(() => { + // eslint-disable-next-line no-plusplus + createPythonServerCallCount++; + return mockPythonServer; + }); + + const interpreter = await interpreterService.object.getActiveInterpreter(); + + // Create NativeRepl directly to have more control over its state, go around private constructor. + const nativeRepl = new (NativeReplModule.NativeRepl as any)(); + nativeRepl.interpreter = interpreter as PythonEnvironment; + nativeRepl.cwd = '/helloJustMockedCwd/cwd'; + nativeRepl.pythonServer = mockPythonServer; + nativeRepl.replController = mockNotebookController; + nativeRepl.disposables = []; + + // Make the singleton point to our instance for testing + // Otherwise, it gets mixed with Native Repl from .create from test above. + (NativeReplModule as any).nativeRepl = nativeRepl; + + // Reset call count after initial setup + createPythonServerCallCount = 0; + + // Set notebookDocument to a mock document + const mockReplUri = Uri.parse('untitled:Untitled-999.ipynb?jupyter-notebook'); + const mockNotebookDocument = ({ + uri: mockReplUri, + toString: () => mockReplUri.toString(), + } as unknown) as NotebookDocument; + + nativeRepl.notebookDocument = mockNotebookDocument; + + // Create a mock notebook document for closing event with same URI + const closingNotebookDocument = ({ + uri: mockReplUri, + toString: () => mockReplUri.toString(), + } as unknown) as NotebookDocument; + + notebookCloseEmitter.fire(closingNotebookDocument); + await new Promise((resolve) => setTimeout(resolve, 50)); + + expect( + updateWorkspaceStateValueStub.calledWith(NativeReplModule.NATIVE_REPL_URI_MEMENTO, undefined), + 'updateWorkspaceStateValue should be called with NATIVE_REPL_URI_MEMENTO and undefined', + ).to.be.true; + expect(mockPythonServer.dispose.calledOnce, 'pythonServer.dispose() should be called once').to.be.true; + expect(createPythonServerCallCount, 'createPythonServer should be called to create a new server').to.equal(1); + expect(nativeRepl.notebookDocument, 'notebookDocument should be undefined after closing').to.be.undefined; + expect(nativeRepl.newReplSession, 'newReplSession should be set to true after closing').to.be.true; + expect(mockNotebookController.dispose.calledOnce, 'replController.dispose() should be called once').to.be.true; + }); +}); diff --git a/src/test/repl/replCommand.test.ts b/src/test/repl/replCommand.test.ts new file mode 100644 index 000000000000..0b5edda863f9 --- /dev/null +++ b/src/test/repl/replCommand.test.ts @@ -0,0 +1,250 @@ +// Create test suite and test cases for the `replUtils` module +import * as TypeMoq from 'typemoq'; +import { commands, Disposable, Uri } from 'vscode'; +import * as sinon from 'sinon'; +import { expect } from 'chai'; +import { IInterpreterService } from '../../client/interpreter/contracts'; +import { ICommandManager } from '../../client/common/application/types'; +import { ICodeExecutionHelper } from '../../client/terminals/types'; +import * as replCommands from '../../client/repl/replCommands'; +import * as replUtils from '../../client/repl/replUtils'; +import * as nativeRepl from '../../client/repl/nativeRepl'; +import * as windowApis from '../../client/common/vscodeApis/windowApis'; +import { Commands } from '../../client/common/constants'; +import { PythonEnvironment } from '../../client/pythonEnvironments/info'; + +suite('REPL - register native repl command', () => { + let interpreterService: TypeMoq.IMock; + let commandManager: TypeMoq.IMock; + let executionHelper: TypeMoq.IMock; + let getSendToNativeREPLSettingStub: sinon.SinonStub; + // @ts-ignore: TS6133 + // eslint-disable-next-line @typescript-eslint/no-unused-vars + let registerCommandSpy: sinon.SinonSpy; + let executeInTerminalStub: sinon.SinonStub; + let getNativeReplStub: sinon.SinonStub; + let disposable: TypeMoq.IMock; + let disposableArray: Disposable[] = []; + + setup(() => { + interpreterService = TypeMoq.Mock.ofType(); + commandManager = TypeMoq.Mock.ofType(); + executionHelper = TypeMoq.Mock.ofType(); + commandManager + .setup((cm) => cm.registerCommand(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny())) + .returns(() => TypeMoq.Mock.ofType().object); + + getSendToNativeREPLSettingStub = sinon.stub(replUtils, 'getSendToNativeREPLSetting'); + getSendToNativeREPLSettingStub.returns(false); + executeInTerminalStub = sinon.stub(replUtils, 'executeInTerminal'); + executeInTerminalStub.returns(Promise.resolve()); + registerCommandSpy = sinon.spy(commandManager.object, 'registerCommand'); + disposable = TypeMoq.Mock.ofType(); + disposableArray = [disposable.object]; + }); + + teardown(() => { + sinon.restore(); + disposableArray.forEach((d) => { + if (d) { + d.dispose(); + } + }); + + disposableArray = []; + }); + + test('Ensure repl command is registered', async () => { + interpreterService + .setup((i) => i.getActiveInterpreter(TypeMoq.It.isAny())) + .returns(() => Promise.resolve(({ path: 'ps' } as unknown) as PythonEnvironment)); + + await replCommands.registerReplCommands( + disposableArray, + interpreterService.object, + executionHelper.object, + commandManager.object, + ); + + commandManager.verify( + (c) => c.registerCommand(TypeMoq.It.isAny(), TypeMoq.It.isAny()), + TypeMoq.Times.atLeastOnce(), + ); + }); + + test('Ensure getSendToNativeREPLSetting is called', async () => { + interpreterService + .setup((i) => i.getActiveInterpreter(TypeMoq.It.isAny())) + .returns(() => Promise.resolve(({ path: 'ps' } as unknown) as PythonEnvironment)); + + let commandHandler: undefined | (() => Promise); + commandManager + // eslint-disable-next-line @typescript-eslint/no-explicit-any + .setup((c) => c.registerCommand as any) + // eslint-disable-next-line @typescript-eslint/no-explicit-any + .returns(() => (command: string, callback: (...args: any[]) => any, _thisArg?: any) => { + if (command === Commands.Exec_In_REPL) { + commandHandler = callback; + } + // eslint-disable-next-line no-void + return { dispose: () => void 0 }; + }); + replCommands.registerReplCommands( + disposableArray, + interpreterService.object, + executionHelper.object, + commandManager.object, + ); + + expect(commandHandler).not.to.be.an('undefined', 'Command handler not initialized'); + + await commandHandler!(); + + sinon.assert.calledOnce(getSendToNativeREPLSettingStub); + }); + + test('Ensure executeInTerminal is called when getSendToNativeREPLSetting returns false', async () => { + interpreterService + .setup((i) => i.getActiveInterpreter(TypeMoq.It.isAny())) + .returns(() => Promise.resolve(({ path: 'ps' } as unknown) as PythonEnvironment)); + getSendToNativeREPLSettingStub.returns(false); + + let commandHandler: undefined | (() => Promise); + commandManager + // eslint-disable-next-line @typescript-eslint/no-explicit-any + .setup((c) => c.registerCommand as any) + // eslint-disable-next-line @typescript-eslint/no-explicit-any + .returns(() => (command: string, callback: (...args: any[]) => any, _thisArg?: any) => { + if (command === Commands.Exec_In_REPL) { + commandHandler = callback; + } + // eslint-disable-next-line no-void + return { dispose: () => void 0 }; + }); + replCommands.registerReplCommands( + disposableArray, + interpreterService.object, + executionHelper.object, + commandManager.object, + ); + + expect(commandHandler).not.to.be.an('undefined', 'Command handler not initialized'); + + await commandHandler!(); + + sinon.assert.calledOnce(executeInTerminalStub); + }); + + test('Ensure we call getNativeREPL() when interpreter exist', async () => { + interpreterService + .setup((i) => i.getActiveInterpreter(TypeMoq.It.isAny())) + .returns(() => Promise.resolve(({ path: 'ps' } as unknown) as PythonEnvironment)); + getSendToNativeREPLSettingStub.returns(true); + getNativeReplStub = sinon.stub(nativeRepl, 'getNativeRepl'); + + let commandHandler: undefined | ((uri: string) => Promise); + commandManager + // eslint-disable-next-line @typescript-eslint/no-explicit-any + .setup((c) => c.registerCommand as any) + // eslint-disable-next-line @typescript-eslint/no-explicit-any + .returns(() => (command: string, callback: (...args: any[]) => any, _thisArg?: any) => { + if (command === Commands.Exec_In_REPL) { + commandHandler = callback; + } + // eslint-disable-next-line no-void + return { dispose: () => void 0 }; + }); + replCommands.registerReplCommands( + disposableArray, + interpreterService.object, + executionHelper.object, + commandManager.object, + ); + + expect(commandHandler).not.to.be.an('undefined', 'Command handler not initialized'); + + await commandHandler!('uri'); + sinon.assert.calledOnce(getNativeReplStub); + }); + + test('Ensure we do not call getNativeREPL() when interpreter does not exist', async () => { + getNativeReplStub = sinon.stub(nativeRepl, 'getNativeRepl'); + getSendToNativeREPLSettingStub.returns(true); + + interpreterService + .setup((i) => i.getActiveInterpreter(TypeMoq.It.isAny())) + .returns(() => Promise.resolve(undefined)); + + let commandHandler: undefined | ((uri: string) => Promise); + commandManager + // eslint-disable-next-line @typescript-eslint/no-explicit-any + .setup((c) => c.registerCommand as any) + // eslint-disable-next-line @typescript-eslint/no-explicit-any + .returns(() => (command: string, callback: (...args: any[]) => any, _thisArg?: any) => { + if (command === Commands.Exec_In_REPL) { + commandHandler = callback; + } + // eslint-disable-next-line no-void + return { dispose: () => void 0 }; + }); + interpreterService + .setup((i) => i.getActiveInterpreter(TypeMoq.It.isAny())) + .returns(() => Promise.resolve(undefined)); + + replCommands.registerReplCommands( + disposableArray, + interpreterService.object, + executionHelper.object, + commandManager.object, + ); + + expect(commandHandler).not.to.be.an('undefined', 'Command handler not initialized'); + + await commandHandler!('uri'); + sinon.assert.notCalled(getNativeReplStub); + }); +}); + +suite('Native REPL getActiveInterpreter', () => { + let interpreterService: TypeMoq.IMock; + let executeCommandStub: sinon.SinonStub; + let getActiveResourceStub: sinon.SinonStub; + + setup(() => { + interpreterService = TypeMoq.Mock.ofType(); + executeCommandStub = sinon.stub(commands, 'executeCommand').resolves(undefined); + getActiveResourceStub = sinon.stub(windowApis, 'getActiveResource'); + }); + + teardown(() => { + sinon.restore(); + }); + + test('Uses active resource when uri is undefined', async () => { + const resource = Uri.file('/workspace/app.py'); + const expected = ({ path: 'ps' } as unknown) as PythonEnvironment; + getActiveResourceStub.returns(resource); + interpreterService + .setup((i) => i.getActiveInterpreter(TypeMoq.It.isValue(resource))) + .returns(() => Promise.resolve(expected)); + + const result = await replUtils.getActiveInterpreter(undefined, interpreterService.object); + + expect(result).to.equal(expected); + interpreterService.verify((i) => i.getActiveInterpreter(TypeMoq.It.isValue(resource)), TypeMoq.Times.once()); + sinon.assert.notCalled(executeCommandStub); + }); + + test('Triggers environment selection using active resource when interpreter is missing', async () => { + const resource = Uri.file('/workspace/app.py'); + getActiveResourceStub.returns(resource); + interpreterService + .setup((i) => i.getActiveInterpreter(TypeMoq.It.isValue(resource))) + .returns(() => Promise.resolve(undefined)); + + const result = await replUtils.getActiveInterpreter(undefined, interpreterService.object); + + expect(result).to.equal(undefined); + sinon.assert.calledWith(executeCommandStub, Commands.TriggerEnvironmentSelection, resource); + }); +}); diff --git a/src/test/repl/variableProvider.test.ts b/src/test/repl/variableProvider.test.ts new file mode 100644 index 000000000000..e401041e17d9 --- /dev/null +++ b/src/test/repl/variableProvider.test.ts @@ -0,0 +1,299 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import { assert } from 'chai'; +import sinon from 'sinon'; +import { + NotebookDocument, + CancellationTokenSource, + VariablesResult, + Variable, + EventEmitter, + ConfigurationScope, + WorkspaceConfiguration, +} from 'vscode'; +import * as TypeMoq from 'typemoq'; +import { IVariableDescription } from '../../client/repl/variables/types'; +import { VariablesProvider } from '../../client/repl/variables/variablesProvider'; +import { VariableRequester } from '../../client/repl/variables/variableRequester'; +import * as workspaceApis from '../../client/common/vscodeApis/workspaceApis'; + +suite('ReplVariablesProvider', () => { + let provider: VariablesProvider; + let varRequester: TypeMoq.IMock; + let notebook: TypeMoq.IMock; + let getConfigurationStub: sinon.SinonStub; + let configMock: TypeMoq.IMock; + let enabled: boolean; + const executionEventEmitter = new EventEmitter(); + const cancellationToken = new CancellationTokenSource().token; + + const objectVariable: IVariableDescription = { + name: 'myObject', + value: '...', + root: 'myObject', + hasNamedChildren: true, + propertyChain: [], + }; + + const listVariable: IVariableDescription = { + name: 'myList', + value: '[...]', + count: 3, + root: 'myObject', + propertyChain: ['myList'], + }; + + function createListItem(index: number): IVariableDescription { + return { + name: index.toString(), + value: `value${index}`, + count: index, + root: 'myObject', + propertyChain: ['myList', index], + }; + } + + function setVariablesForParent( + parent: IVariableDescription | undefined, + result: IVariableDescription[], + updated?: IVariableDescription[], + startIndex?: number, + ) { + let returnedOnce = false; + varRequester + .setup((v) => v.getAllVariableDescriptions(parent, startIndex ?? TypeMoq.It.isAny(), TypeMoq.It.isAny())) + .returns(() => { + if (updated && returnedOnce) { + return Promise.resolve(updated); + } + returnedOnce = true; + return Promise.resolve(result); + }); + } + + async function provideVariables(parent: Variable | undefined, kind = 1) { + const results: VariablesResult[] = []; + for await (const result of provider.provideVariables(notebook.object, parent, kind, 0, cancellationToken)) { + results.push(result); + } + return results; + } + + setup(() => { + enabled = true; + varRequester = TypeMoq.Mock.ofType(); + notebook = TypeMoq.Mock.ofType(); + provider = new VariablesProvider(varRequester.object, () => notebook.object, executionEventEmitter.event); + configMock = TypeMoq.Mock.ofType(); + configMock.setup((c) => c.get('REPL.provideVariables')).returns(() => enabled); + getConfigurationStub = sinon.stub(workspaceApis, 'getConfiguration'); + getConfigurationStub.callsFake((section?: string, _scope?: ConfigurationScope | null) => { + if (section === 'python') { + return configMock.object; + } + return undefined; + }); + }); + + teardown(() => { + sinon.restore(); + }); + + test('provideVariables without parent should yield variables', async () => { + setVariablesForParent(undefined, [objectVariable]); + + const results = await provideVariables(undefined); + + assert.isNotEmpty(results); + assert.equal(results.length, 1); + assert.equal(results[0].variable.name, 'myObject'); + assert.equal(results[0].variable.expression, 'myObject'); + }); + + test('No variables are returned when variable provider is disabled', async () => { + enabled = false; + setVariablesForParent(undefined, [objectVariable]); + + const results = await provideVariables(undefined); + + assert.isEmpty(results); + }); + + test('No change event from provider when disabled', async () => { + enabled = false; + let eventFired = false; + provider.onDidChangeVariables(() => { + eventFired = true; + }); + + executionEventEmitter.fire(); + + assert.isFalse(eventFired, 'event should not have fired'); + }); + + test('Variables change event from provider should fire when execution happens', async () => { + let eventFired = false; + provider.onDidChangeVariables(() => { + eventFired = true; + }); + + executionEventEmitter.fire(); + + assert.isTrue(eventFired, 'event should have fired'); + }); + + test('provideVariables with a parent should call get children correctly', async () => { + const listVariableItems = [0, 1, 2].map(createListItem); + setVariablesForParent(undefined, [objectVariable]); + + // pass each the result as the parent in the next call + const rootVariable = (await provideVariables(undefined))[0]; + setVariablesForParent(rootVariable.variable as IVariableDescription, [listVariable]); + const listResult = (await provideVariables(rootVariable!.variable))[0]; + setVariablesForParent(listResult.variable as IVariableDescription, listVariableItems); + const listItems = await provideVariables(listResult!.variable, 2); + + assert.equal(listResult.variable.name, 'myList'); + assert.equal(listResult.variable.expression, 'myObject.myList'); + assert.isNotEmpty(listItems); + assert.equal(listItems.length, 3); + listItems.forEach((item, index) => { + assert.equal(item.variable.name, index.toString()); + assert.equal(item.variable.value, `value${index}`); + assert.equal(item.variable.expression, `myObject.myList[${index}]`); + }); + }); + + test('All indexed variables should be returned when requested', async () => { + const listVariable: IVariableDescription = { + name: 'myList', + value: '[...]', + count: 6, + root: 'myList', + propertyChain: [], + }; + + setVariablesForParent(undefined, [listVariable]); + const rootVariable = (await provideVariables(undefined))[0]; + const firstPage = [0, 1, 2].map(createListItem); + const secondPage = [3, 4, 5].map(createListItem); + setVariablesForParent(rootVariable.variable as IVariableDescription, firstPage, undefined, 0); + setVariablesForParent(rootVariable.variable as IVariableDescription, secondPage, undefined, firstPage.length); + + const listItemResult = await provideVariables(rootVariable!.variable, 2); + + assert.equal(listItemResult.length, 6, 'full list of items should be returned'); + listItemResult.forEach((item, index) => { + assert.equal(item.variable.name, index.toString()); + assert.equal(item.variable.value, `value${index}`); + }); + }); + + test('Getting less indexed items than the specified count is handled', async () => { + const listVariable: IVariableDescription = { + name: 'myList', + value: '[...]', + count: 6, + root: 'myList', + propertyChain: [], + }; + + const firstPage = [0, 1, 2].map(createListItem); + const secondPage = [3, 4].map(createListItem); + setVariablesForParent(undefined, [listVariable]); + const rootVariable = (await provideVariables(undefined))[0]; + setVariablesForParent(rootVariable.variable as IVariableDescription, firstPage, undefined, 0); + setVariablesForParent(rootVariable.variable as IVariableDescription, secondPage, undefined, firstPage.length); + setVariablesForParent(rootVariable.variable as IVariableDescription, [], undefined, 5); + + const listItemResult = await provideVariables(rootVariable!.variable, 2); + + assert.equal(listItemResult.length, 5); + listItemResult.forEach((item, index) => { + assert.equal(item.variable.name, index.toString()); + assert.equal(item.variable.value, `value${index}`); + }); + }); + + test('Getting variables again with new execution count should get updated variables', async () => { + const intVariable: IVariableDescription = { + name: 'myInt', + value: '1', + root: '', + propertyChain: [], + }; + setVariablesForParent(undefined, [intVariable], [{ ...intVariable, value: '2' }]); + + const first = await provideVariables(undefined); + executionEventEmitter.fire(); + const second = await provideVariables(undefined); + + assert.equal(first.length, 1); + assert.equal(second.length, 1); + assert.equal(first[0].variable.value, '1'); + assert.equal(second[0].variable.value, '2'); + }); + + test('Getting variables again with same execution count should not make another call', async () => { + const intVariable: IVariableDescription = { + name: 'myInt', + value: '1', + root: '', + propertyChain: [], + }; + + setVariablesForParent(undefined, [intVariable]); + + const first = await provideVariables(undefined); + const second = await provideVariables(undefined); + + assert.equal(first.length, 1); + assert.equal(second.length, 1); + assert.equal(first[0].variable.value, '1'); + + varRequester.verify( + (x) => x.getAllVariableDescriptions(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny()), + TypeMoq.Times.once(), + ); + }); + + test('Cache pages of indexed children correctly', async () => { + const listVariable: IVariableDescription = { + name: 'myList', + value: '[...]', + count: 6, + root: 'myList', + propertyChain: [], + }; + + const firstPage = [0, 1, 2].map(createListItem); + const secondPage = [3, 4, 5].map(createListItem); + setVariablesForParent(undefined, [listVariable]); + const rootVariable = (await provideVariables(undefined))[0]; + setVariablesForParent(rootVariable.variable as IVariableDescription, firstPage, undefined, 0); + setVariablesForParent(rootVariable.variable as IVariableDescription, secondPage, undefined, firstPage.length); + + await provideVariables(rootVariable!.variable, 2); + + // once for the parent and once for each of the two pages of list items + varRequester.verify( + (x) => x.getAllVariableDescriptions(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny()), + TypeMoq.Times.exactly(3), + ); + + const listItemResult = await provideVariables(rootVariable!.variable, 2); + + assert.equal(listItemResult.length, 6, 'full list of items should be returned'); + listItemResult.forEach((item, index) => { + assert.equal(item.variable.name, index.toString()); + assert.equal(item.variable.value, `value${index}`); + }); + + // no extra calls for getting the children again + varRequester.verify( + (x) => x.getAllVariableDescriptions(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny()), + TypeMoq.Times.exactly(3), + ); + }); +}); diff --git a/src/test/serviceRegistry.ts b/src/test/serviceRegistry.ts index 1b8a9d78d580..382659b3f838 100644 --- a/src/test/serviceRegistry.ts +++ b/src/test/serviceRegistry.ts @@ -2,13 +2,13 @@ // Licensed under the MIT License. import { Container } from 'inversify'; -import { anything, instance, mock, when } from 'ts-mockito'; +import { anything } from 'ts-mockito'; import * as TypeMoq from 'typemoq'; import { Disposable, Memento } from 'vscode'; -import { IS_WINDOWS } from '../client/common/platform/constants'; import { FileSystem } from '../client/common/platform/fileSystem'; import { PathUtils } from '../client/common/platform/pathUtils'; import { PlatformService } from '../client/common/platform/platformService'; +import { isWindows } from '../client/common/utils/platform'; import { RegistryImplementation } from '../client/common/platform/registry'; import { registerTypes as platformRegisterTypes } from '../client/common/platform/serviceRegistry'; import { IFileSystem, IPlatformService, IRegistry } from '../client/common/platform/types'; @@ -27,14 +27,12 @@ import { ICurrentProcess, IDisposableRegistry, IMemento, - ILogOutputChannel, IPathUtils, IsWindows, WORKSPACE_MEMENTO, - ITestOutputChannel, + ILogOutputChannel, } from '../client/common/types'; import { registerTypes as variableRegisterTypes } from '../client/common/variables/serviceRegistry'; -import { registerTypes as formattersRegisterTypes } from '../client/formatters/serviceRegistry'; import { EnvironmentActivationService } from '../client/interpreter/activation/service'; import { IEnvironmentActivationService } from '../client/interpreter/activation/types'; import { @@ -47,7 +45,6 @@ import { registerInterpreterTypes } from '../client/interpreter/serviceRegistry' import { ServiceContainer } from '../client/ioc/container'; import { ServiceManager } from '../client/ioc/serviceManager'; import { IServiceContainer, IServiceManager } from '../client/ioc/types'; -import { registerTypes as lintersRegisterTypes } from '../client/linters/serviceRegistry'; import { registerTypes as unittestsRegisterTypes } from '../client/testing/serviceRegistry'; import { LegacyFileSystem } from './legacyFileSystem'; import { MockOutputChannel } from './mockClasses'; @@ -56,6 +53,7 @@ import { MockMemento } from './mocks/mementos'; import { MockProcessService } from './mocks/proc'; import { MockProcess } from './mocks/process'; import { registerForIOC } from './pythonEnvironments/legacyIOC'; +import { createTypeMoq } from './mocks/helper'; export class IocContainer { // This may be set (before any registration happens) to indicate @@ -85,7 +83,7 @@ export class IocContainer { this.serviceManager.addSingletonInstance(ILogOutputChannel, stdOutputChannel); const testOutputChannel = new MockOutputChannel('Python Test - UnitTests'); this.disposables.push(testOutputChannel); - this.serviceManager.addSingletonInstance(ITestOutputChannel, testOutputChannel); + this.serviceManager.addSingletonInstance(ILogOutputChannel, testOutputChannel); this.serviceManager.addSingleton( IInterpreterAutoSelectionService, @@ -128,12 +126,10 @@ export class IocContainer { public registerProcessTypes(): void { processRegisterTypes(this.serviceManager); - const mockEnvironmentActivationService = mock(EnvironmentActivationService); - when(mockEnvironmentActivationService.getActivatedEnvironmentVariables(anything())).thenResolve(); - this.serviceManager.addSingletonInstance( - IEnvironmentActivationService, - instance(mockEnvironmentActivationService), - ); + const mockEnvironmentActivationService = createTypeMoq(); + mockEnvironmentActivationService + .setup((f) => f.getActivatedEnvironmentVariables(anything())) + .returns(() => Promise.resolve(undefined)); } public registerVariableTypes(): void { @@ -144,14 +140,6 @@ export class IocContainer { unittestsRegisterTypes(this.serviceManager); } - public registerLinterTypes(): void { - lintersRegisterTypes(this.serviceManager); - } - - public registerFormatterTypes(): void { - formattersRegisterTypes(this.serviceManager); - } - public registerPlatformTypes(): void { platformRegisterTypes(this.serviceManager); } @@ -162,7 +150,7 @@ export class IocContainer { } public registerMockProcessTypes(): void { - const processServiceFactory = TypeMoq.Mock.ofType(); + const processServiceFactory = createTypeMoq(); // eslint-disable-next-line @typescript-eslint/no-explicit-any const processService = new MockProcessService(new ProcessService(process.env as any)); @@ -180,11 +168,13 @@ export class IocContainer { IEnvironmentActivationService, EnvironmentActivationService, ); - const mockEnvironmentActivationService = mock(EnvironmentActivationService); - when(mockEnvironmentActivationService.getActivatedEnvironmentVariables(anything())).thenResolve(); + const mockEnvironmentActivationService = createTypeMoq(); + mockEnvironmentActivationService + .setup((m) => m.getActivatedEnvironmentVariables(anything())) + .returns(() => Promise.resolve(undefined)); this.serviceManager.rebindInstance( IEnvironmentActivationService, - instance(mockEnvironmentActivationService), + mockEnvironmentActivationService.object, ); } @@ -195,7 +185,7 @@ export class IocContainer { } public registerMockProcess(): void { - this.serviceManager.addSingletonInstance(IsWindows, IS_WINDOWS); + this.serviceManager.addSingletonInstance(IsWindows, isWindows()); this.serviceManager.addSingleton(IPathUtils, PathUtils); this.serviceManager.addSingleton(ICurrentProcess, MockProcess); diff --git a/src/test/smoke/common.ts b/src/test/smoke/common.ts index faf18ebd286e..5f5b691fb496 100644 --- a/src/test/smoke/common.ts +++ b/src/test/smoke/common.ts @@ -4,10 +4,10 @@ 'use strict'; import * as assert from 'assert'; -import * as fs from 'fs-extra'; import * as glob from 'glob'; import * as path from 'path'; import * as vscode from 'vscode'; +import * as fs from '../../client/common/platform/fs-paths'; import { JUPYTER_EXTENSION_ID } from '../../client/common/constants'; import { SMOKE_TEST_EXTENSIONS_DIR } from '../constants'; import { noop, sleep } from '../core'; @@ -25,7 +25,7 @@ export async function removeLanguageServerFiles(): Promise { } async function getLanguageServerFolders(): Promise { return new Promise((resolve, reject) => { - glob('languageServer.*', { cwd: SMOKE_TEST_EXTENSIONS_DIR }, (ex, matches) => { + glob.default('languageServer.*', { cwd: SMOKE_TEST_EXTENSIONS_DIR }, (ex, matches) => { if (ex) { reject(ex); } else { @@ -51,7 +51,7 @@ export async function openNotebook(file: string): Promise { assert.fail(`Something went wrong showing the text document: ${err}`); }); - assert(vscode.window.activeTextEditor, 'No active editor'); + assert.ok(vscode.window.activeTextEditor, 'No active editor'); // Make sure LS completes file loading and analysis. // In test mode it awaits for the completion before trying // to fetch data for completion, hover.etc. diff --git a/src/test/smoke/datascience.smoke.test.ts b/src/test/smoke/datascience.smoke.test.ts index ebd101b5849f..9f4421de4676 100644 --- a/src/test/smoke/datascience.smoke.test.ts +++ b/src/test/smoke/datascience.smoke.test.ts @@ -4,9 +4,9 @@ 'use strict'; import * as assert from 'assert'; -import * as fs from 'fs-extra'; import * as path from 'path'; import * as vscode from 'vscode'; +import * as fs from '../../client/common/platform/fs-paths'; import { openFile, waitForCondition } from '../common'; import { EXTENSION_ROOT_DIR_FOR_TESTS } from '../constants'; import { sleep } from '../core'; @@ -35,7 +35,7 @@ suite('Smoke Test: Datascience', () => { EXTENSION_ROOT_DIR_FOR_TESTS, 'src', 'test', - 'pythonFiles', + 'python_files', 'datascience', 'simple_note_book.py', ); @@ -60,7 +60,7 @@ suite('Smoke Test: Datascience', () => { EXTENSION_ROOT_DIR_FOR_TESTS, 'src', 'test', - 'pythonFiles', + 'python_files', 'datascience', 'simple_nb.ipynb', ); diff --git a/src/test/smoke/jedilsp.smoke.test.ts b/src/test/smoke/jedilsp.smoke.test.ts index acde436442e1..a2087ff42085 100644 --- a/src/test/smoke/jedilsp.smoke.test.ts +++ b/src/test/smoke/jedilsp.smoke.test.ts @@ -3,9 +3,9 @@ 'use strict'; -import * as fs from 'fs-extra'; import * as path from 'path'; import * as vscode from 'vscode'; +import * as fs from '../../client/common/platform/fs-paths'; import { openFile, waitForCondition } from '../common'; import { EXTENSION_ROOT_DIR_FOR_TESTS, IS_SMOKE_TEST } from '../constants'; @@ -24,7 +24,7 @@ suite('Smoke Test: Jedi LSP', () => { teardown(closeActiveWindows); test('Verify diagnostics on a python file', async () => { - const file = path.join(EXTENSION_ROOT_DIR_FOR_TESTS, 'src', 'test', 'pythonFiles', 'intellisense', 'test.py'); + const file = path.join(EXTENSION_ROOT_DIR_FOR_TESTS, 'src', 'test', 'python_files', 'intellisense', 'test.py'); const outputFile = path.join(path.dirname(file), 'ds.log'); if (await fs.pathExists(outputFile)) { await fs.unlink(outputFile); diff --git a/src/test/smoke/runInTerminal.smoke.test.ts b/src/test/smoke/runInTerminal.smoke.test.ts index 43b53e4480e0..4bdec0843862 100644 --- a/src/test/smoke/runInTerminal.smoke.test.ts +++ b/src/test/smoke/runInTerminal.smoke.test.ts @@ -4,9 +4,9 @@ 'use strict'; import * as assert from 'assert'; -import * as fs from 'fs-extra'; import * as path from 'path'; import * as vscode from 'vscode'; +import * as fs from '../../client/common/platform/fs-paths'; import { openFile, waitForCondition } from '../common'; import { EXTENSION_ROOT_DIR_FOR_TESTS, IS_SMOKE_TEST } from '../constants'; import { closeActiveWindows, initialize, initializeTest } from '../initialize'; @@ -17,13 +17,22 @@ suite('Smoke Test: Run Python File In Terminal', () => { return this.skip(); } await initialize(); + // Ensure the environments extension is not used for this test + await vscode.workspace + .getConfiguration('python') + .update('useEnvironmentsExtension', false, vscode.ConfigurationTarget.Global); return undefined; }); + setup(initializeTest); suiteTeardown(closeActiveWindows); teardown(closeActiveWindows); - test('Exec', async () => { + // TODO: Re-enable this test once the flakiness on Windows is resolved + test('Exec', async function () { + if (process.platform === 'win32') { + return this.skip(); + } const file = path.join( EXTENSION_ROOT_DIR_FOR_TESTS, 'src', diff --git a/src/test/smoke/smartSend.smoke.test.ts b/src/test/smoke/smartSend.smoke.test.ts new file mode 100644 index 000000000000..cae41cc094d5 --- /dev/null +++ b/src/test/smoke/smartSend.smoke.test.ts @@ -0,0 +1,84 @@ +import * as vscode from 'vscode'; +import * as path from 'path'; +import { assert } from 'chai'; +import * as fs from '../../client/common/platform/fs-paths'; +import { EXTENSION_ROOT_DIR_FOR_TESTS, IS_SMOKE_TEST } from '../constants'; +import { closeActiveWindows, initialize, initializeTest } from '../initialize'; +import { openFile, waitForCondition } from '../common'; + +suite('Smoke Test: Run Smart Selection and Advance Cursor', async () => { + suiteSetup(async function () { + if (!IS_SMOKE_TEST) { + return this.skip(); + } + await initialize(); + return undefined; + }); + + setup(initializeTest); + suiteTeardown(closeActiveWindows); + teardown(closeActiveWindows); + + // TODO: Re-enable this test once the flakiness on Windows, linux are resolved + test.skip('Smart Send', async function () { + const file = path.join( + EXTENSION_ROOT_DIR_FOR_TESTS, + 'src', + 'testMultiRootWkspc', + 'smokeTests', + 'create_delete_file.py', + ); + const outputFile = path.join( + EXTENSION_ROOT_DIR_FOR_TESTS, + 'src', + 'testMultiRootWkspc', + 'smokeTests', + 'smart_send_smoke.txt', + ); + + await fs.remove(outputFile); + + const textDocument = await openFile(file); + + if (vscode.window.activeTextEditor) { + const myPos = new vscode.Position(0, 0); + vscode.window.activeTextEditor!.selections = [new vscode.Selection(myPos, myPos)]; + } + await vscode.commands + .executeCommand('python.execSelectionInTerminal', textDocument.uri) + .then(undefined, (err) => { + assert.fail(`Something went wrong running the Python file in the terminal: ${err}`); + }); + + const checkIfFileHasBeenCreated = () => fs.pathExists(outputFile); + await waitForCondition(checkIfFileHasBeenCreated, 20_000, `"${outputFile}" file not created`); + + await vscode.commands + .executeCommand('python.execSelectionInTerminal', textDocument.uri) + .then(undefined, (err) => { + assert.fail(`Something went wrong running the Python file in the terminal: ${err}`); + }); + await vscode.commands + .executeCommand('python.execSelectionInTerminal', textDocument.uri) + .then(undefined, (err) => { + assert.fail(`Something went wrong running the Python file in the terminal: ${err}`); + }); + + async function wait() { + return new Promise((resolve) => { + setTimeout(() => { + resolve(); + }, 10000); + }); + } + + await wait(); + + const deletedFile = !(await fs.pathExists(outputFile)); + if (deletedFile) { + assert.ok(true, `"${outputFile}" file has been deleted`); + } else { + assert.fail(`"${outputFile}" file still exists`); + } + }); +}); diff --git a/src/test/smokeTest.ts b/src/test/smokeTest.ts index de66e7aba5f0..a101e961e03d 100644 --- a/src/test/smokeTest.ts +++ b/src/test/smokeTest.ts @@ -5,9 +5,8 @@ // Must always be on top to setup expected env. process.env.VSC_PYTHON_SMOKE_TEST = '1'; - import { spawn } from 'child_process'; -import * as fs from 'fs-extra'; +import * as fs from '../client/common/platform/fs-paths'; import * as glob from 'glob'; import * as path from 'path'; import { unzip } from './common'; @@ -81,7 +80,7 @@ class TestRunner { private async extractLatestExtension(targetDir: string): Promise { const extensionFile = await new Promise((resolve, reject) => - glob('*.vsix', (ex, files) => (ex ? reject(ex) : resolve(files[0]))), + glob.default('*.vsix', (ex, files) => (ex ? reject(ex) : resolve(files[0]))), ); await unzip(extensionFile, targetDir); } diff --git a/src/test/sourceMapSupport.test.ts b/src/test/sourceMapSupport.test.ts deleted file mode 100644 index a591e1236619..000000000000 --- a/src/test/sourceMapSupport.test.ts +++ /dev/null @@ -1,95 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; - -import { expect } from 'chai'; -import * as fs from 'fs'; -import { ConfigurationTarget, Disposable } from 'vscode'; -import { FileSystem } from '../client/common/platform/fileSystem'; -import { Diagnostics } from '../client/common/utils/localize'; -import { SourceMapSupport } from '../client/sourceMapSupport'; -import { noop } from './core'; - -suite('Source Map Support', () => { - function createVSCStub(isEnabled: boolean = false, selectDisableButton: boolean = false) { - const stubInfo = { - configValueRetrieved: false, - configValueUpdated: false, - messageDisplayed: false, - }; - const vscode = { - workspace: { - getConfiguration: (setting: string, _defaultValue: any) => { - if (setting !== 'python.diagnostics') { - return; - } - return { - get: (prop: string) => { - stubInfo.configValueRetrieved = prop === 'sourceMapsEnabled'; - return isEnabled; - }, - update: (prop: string, value: boolean, scope: ConfigurationTarget) => { - if ( - prop === 'sourceMapsEnabled' && - value === false && - scope === ConfigurationTarget.Global - ) { - stubInfo.configValueUpdated = true; - } - }, - }; - }, - }, - window: { - showWarningMessage: () => { - stubInfo.messageDisplayed = true; - return Promise.resolve(selectDisableButton ? Diagnostics.disableSourceMaps : undefined); - }, - }, - ConfigurationTarget: ConfigurationTarget, - }; - return { stubInfo, vscode }; - } - - const disposables: Disposable[] = []; - teardown(() => { - disposables.forEach((disposable) => { - try { - disposable.dispose(); - } catch { - noop(); - } - }); - }); - test('When disabling source maps, the map file is renamed and vice versa', async () => { - const fileSystem = new FileSystem(); - const jsFile = await fileSystem.createTemporaryFile('.js'); - disposables.push(jsFile); - const mapFile = `${jsFile.filePath}.map`; - disposables.push({ - dispose: () => fs.unlinkSync(mapFile), - }); - await fileSystem.writeFile(mapFile, 'ABC'); - expect(await fileSystem.fileExists(mapFile)).to.be.true; - - const stub = createVSCStub(true, true); - const instance = new (class extends SourceMapSupport { - public async enableSourceMap(enable: boolean, sourceFile: string) { - return super.enableSourceMap(enable, sourceFile); - } - })(stub.vscode as any); - - await instance.enableSourceMap(false, jsFile.filePath); - - expect(await fileSystem.fileExists(jsFile.filePath)).to.be.equal(true, 'Source file does not exist'); - expect(await fileSystem.fileExists(mapFile)).to.be.equal(false, 'Source map file not renamed'); - expect(await fileSystem.fileExists(`${mapFile}.disabled`)).to.be.equal(true, 'Expected renamed file not found'); - - await instance.enableSourceMap(true, jsFile.filePath); - - expect(await fileSystem.fileExists(jsFile.filePath)).to.be.equal(true, 'Source file does not exist'); - expect(await fileSystem.fileExists(mapFile)).to.be.equal(true, 'Source map file not found'); - expect(await fileSystem.fileExists(`${mapFile}.disabled`)).to.be.equal(false, 'Source map file not renamed'); - }); -}); diff --git a/src/test/sourceMapSupport.unit.test.ts b/src/test/sourceMapSupport.unit.test.ts deleted file mode 100644 index 3ce5249eca01..000000000000 --- a/src/test/sourceMapSupport.unit.test.ts +++ /dev/null @@ -1,129 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; - -import { expect } from 'chai'; -import * as path from 'path'; -import rewiremock from 'rewiremock'; -import * as sinon from 'sinon'; -import { ConfigurationTarget, Disposable } from 'vscode'; -import { Diagnostics } from '../client/common/utils/localize'; -import { EXTENSION_ROOT_DIR } from '../client/constants'; -import { initialize, SourceMapSupport } from '../client/sourceMapSupport'; -import { noop, sleep } from './core'; - -suite('Source Map Support', () => { - function createVSCStub(isEnabled: boolean = false, selectDisableButton: boolean = false) { - const stubInfo = { - configValueRetrieved: false, - configValueUpdated: false, - messageDisplayed: false, - }; - const vscode = { - workspace: { - getConfiguration: (setting: string, _defaultValue: any) => { - if (setting !== 'python.diagnostics') { - return; - } - return { - get: (prop: string) => { - stubInfo.configValueRetrieved = prop === 'sourceMapsEnabled'; - return isEnabled; - }, - update: (prop: string, value: boolean, scope: ConfigurationTarget) => { - if ( - prop === 'sourceMapsEnabled' && - value === false && - scope === ConfigurationTarget.Global - ) { - stubInfo.configValueUpdated = true; - } - }, - }; - }, - }, - window: { - showWarningMessage: () => { - stubInfo.messageDisplayed = true; - return Promise.resolve(selectDisableButton ? Diagnostics.disableSourceMaps : undefined); - }, - }, - ConfigurationTarget: ConfigurationTarget, - }; - return { stubInfo, vscode }; - } - - const disposables: Disposable[] = []; - teardown(() => { - rewiremock.disable(); - disposables.forEach((disposable) => { - try { - disposable.dispose(); - } catch { - noop(); - } - }); - }); - test('Test message is not displayed when source maps are not enabled', async () => { - const stub = createVSCStub(false); - - initialize(stub.vscode as any); - await sleep(100); - expect(stub.stubInfo.configValueRetrieved).to.be.equal(true, 'Config Value not retrieved'); - expect(stub.stubInfo.messageDisplayed).to.be.equal(false, 'Message displayed'); - }); - test('Test message is displayed when source maps are not enabled', async () => { - const stub = createVSCStub(true); - const instance = new (class extends SourceMapSupport { - protected async enableSourceMaps(_enable: boolean) { - noop(); - } - })(stub.vscode as any); - rewiremock.enable(); - const installStub = sinon.stub(); - rewiremock('source-map-support').with({ install: installStub }); - await instance.initialize(); - - expect(installStub.callCount).to.be.equal(1); - expect(stub.stubInfo.configValueRetrieved).to.be.equal(true, 'Config Value not retrieved'); - expect(stub.stubInfo.messageDisplayed).to.be.equal(true, 'Message displayed'); - expect(stub.stubInfo.configValueUpdated).to.be.equal(false, 'Config Value updated'); - }); - test('Test message is not displayed when source maps are not enabled', async () => { - const stub = createVSCStub(true, true); - const instance = new (class extends SourceMapSupport { - protected async enableSourceMaps(_enable: boolean) { - noop(); - } - })(stub.vscode as any); - - await instance.initialize(); - expect(stub.stubInfo.configValueRetrieved).to.be.equal(true, 'Config Value not retrieved'); - expect(stub.stubInfo.messageDisplayed).to.be.equal(true, 'Message displayed'); - expect(stub.stubInfo.configValueUpdated).to.be.equal(true, 'Config Value not updated'); - }); - async function testRenamingFilesWhenEnablingDisablingSourceMaps(enableSourceMaps: boolean) { - const stub = createVSCStub(true, true); - const sourceFilesPassed: string[] = []; - const instance = new (class extends SourceMapSupport { - public async enableSourceMaps(enable: boolean) { - return super.enableSourceMaps(enable); - } - public async enableSourceMap(enable: boolean, sourceFile: string) { - expect(enable).to.equal(enableSourceMaps); - sourceFilesPassed.push(sourceFile); - return Promise.resolve(); - } - })(stub.vscode as any); - - await instance.enableSourceMaps(enableSourceMaps); - const extensionSourceMap = path.join(EXTENSION_ROOT_DIR, 'out', 'client', 'extension.js'); - const debuggerSourceMap = path.join(EXTENSION_ROOT_DIR, 'out', 'client', 'debugger', 'debugAdapter', 'main.js'); - expect(sourceFilesPassed).to.deep.equal([extensionSourceMap, debuggerSourceMap]); - } - test('Rename extension and debugger source maps when enabling source maps', () => - testRenamingFilesWhenEnablingDisablingSourceMaps(true)); - test('Rename extension and debugger source maps when disabling source maps', () => - testRenamingFilesWhenEnablingDisablingSourceMaps(false)); -}); diff --git a/src/test/standardTest.ts b/src/test/standardTest.ts index c8416ebf1d7d..c3a7968c9c7a 100644 --- a/src/test/standardTest.ts +++ b/src/test/standardTest.ts @@ -1,11 +1,12 @@ import { spawnSync } from 'child_process'; -import * as fs from 'fs-extra'; +import * as fs from '../client/common/platform/fs-paths'; import * as os from 'os'; import * as path from 'path'; import { downloadAndUnzipVSCode, resolveCliPathFromVSCodeExecutablePath, runTests } from '@vscode/test-electron'; import { JUPYTER_EXTENSION_ID, PYLANCE_EXTENSION_ID } from '../client/common/constants'; import { EXTENSION_ROOT_DIR_FOR_TESTS } from './constants'; -import { getVersion } from './utils/vscode'; +import { getChannel } from './utils/vscode'; +import { TestOptions } from '@vscode/test-electron/out/runTest'; // If running smoke tests, we don't have access to this. if (process.env.TEST_FILES_SUFFIX !== 'smoke.test') { @@ -76,7 +77,7 @@ async function installPylanceExtension(vscodeExecutablePath: string) { async function start() { console.log('*'.repeat(100)); console.log('Start Standard tests'); - const channel = getVersion(); + const channel = getChannel(); console.log(`Using ${channel} build of VS Code.`); const vscodeExecutablePath = await downloadAndUnzipVSCode(channel); const baseLaunchArgs = @@ -85,18 +86,20 @@ async function start() { : ['--disable-extensions']; await installJupyterExtension(vscodeExecutablePath); await installPylanceExtension(vscodeExecutablePath); + console.log('VS Code executable', vscodeExecutablePath); const launchArgs = baseLaunchArgs .concat([workspacePath]) - .concat(channel === 'insiders' ? ['--enable-proposed-api'] : []) + .concat(['--enable-proposed-api']) .concat(['--timeout', '5000']); console.log(`Starting vscode ${channel} with args ${launchArgs.join(' ')}`); - await runTests({ + const options: TestOptions = { extensionDevelopmentPath: extensionDevelopmentPath, extensionTestsPath: path.join(EXTENSION_ROOT_DIR_FOR_TESTS, 'out', 'test'), launchArgs, version: channel, extensionTestsEnv: { ...process.env, UITEST_DISABLE_INSIDERS: '1' }, - }); + }; + await runTests(options); } start().catch((ex) => { console.error('End Standard tests (with errors)', ex); diff --git a/src/test/telemetry/index.unit.test.ts b/src/test/telemetry/index.unit.test.ts index 00272aad1f64..d8a6b72eedc6 100644 --- a/src/test/telemetry/index.unit.test.ts +++ b/src/test/telemetry/index.unit.test.ts @@ -4,24 +4,20 @@ import { expect } from 'chai'; import rewiremock from 'rewiremock'; -import * as TypeMoq from 'typemoq'; +import * as sinon from 'sinon'; +import * as fs from '../../client/common/platform/fs-paths'; -import { instance, mock, verify, when } from 'ts-mockito'; -import { WorkspaceConfiguration } from 'vscode'; -import { IWorkspaceService } from '../../client/common/application/types'; -import { WorkspaceService } from '../../client/common/application/workspace'; import { _resetSharedProperties, clearTelemetryReporter, - isTelemetryDisabled, sendTelemetryEvent, setSharedProperty, } from '../../client/telemetry'; suite('Telemetry', () => { - let workspaceService: IWorkspaceService; const oldValueOfVSC_PYTHON_UNIT_TEST = process.env.VSC_PYTHON_UNIT_TEST; const oldValueOfVSC_PYTHON_CI_TEST = process.env.VSC_PYTHON_CI_TEST; + let readJSONSyncStub: sinon.SinonStub; class Reporter { public static eventName: string[] = []; @@ -48,9 +44,10 @@ suite('Telemetry', () => { } setup(() => { - workspaceService = mock(WorkspaceService); process.env.VSC_PYTHON_UNIT_TEST = undefined; process.env.VSC_PYTHON_CI_TEST = undefined; + readJSONSyncStub = sinon.stub(fs, 'readJSONSync'); + readJSONSyncStub.returns({ enableTelemetry: true }); clearTelemetryReporter(); Reporter.clear(); }); @@ -59,37 +56,7 @@ suite('Telemetry', () => { process.env.VSC_PYTHON_CI_TEST = oldValueOfVSC_PYTHON_CI_TEST; rewiremock.disable(); _resetSharedProperties(); - }); - - const testsForisTelemetryDisabled = [ - { - testName: 'Returns true when globalValue is set to false', - settings: { globalValue: false }, - expectedResult: true, - }, - { - testName: 'Returns false otherwise', - settings: {}, - expectedResult: false, - }, - ]; - - suite('Function isTelemetryDisabled()', () => { - testsForisTelemetryDisabled.forEach((testParams) => { - test(testParams.testName, async () => { - const workspaceConfig = TypeMoq.Mock.ofType(); - when(workspaceService.getConfiguration('telemetry')).thenReturn(workspaceConfig.object); - workspaceConfig - .setup((c) => c.inspect('enableTelemetry')) - .returns(() => testParams.settings as any) - .verifiable(TypeMoq.Times.once()); - - expect(isTelemetryDisabled(instance(workspaceService))).to.equal(testParams.expectedResult); - - verify(workspaceService.getConfiguration('telemetry')).once(); - workspaceConfig.verifyAll(); - }); - }); + sinon.restore(); }); test('Send Telemetry', () => { diff --git a/src/test/tensorBoard/helpers.ts b/src/test/tensorBoard/helpers.ts deleted file mode 100644 index b9f90226b28e..000000000000 --- a/src/test/tensorBoard/helpers.ts +++ /dev/null @@ -1,18 +0,0 @@ -import * as TypeMoq from 'typemoq'; -import { IApplicationShell, ICommandManager } from '../../client/common/application/types'; -import { IPersistentStateFactory } from '../../client/common/types'; -import { TensorBoardPrompt } from '../../client/tensorBoard/tensorBoardPrompt'; -import { MockState } from '../interpreters/mocks'; - -export function createTensorBoardPromptWithMocks(): TensorBoardPrompt { - const appShell = TypeMoq.Mock.ofType(); - const commandManager = TypeMoq.Mock.ofType(); - const persistentStateFactory = TypeMoq.Mock.ofType(); - const persistentState = new MockState(true); - persistentStateFactory - .setup((factory) => { - factory.createWorkspacePersistentState(TypeMoq.It.isAny(), TypeMoq.It.isAny()); - }) - .returns(() => persistentState); - return new TensorBoardPrompt(appShell.object, commandManager.object, persistentStateFactory.object); -} diff --git a/src/test/tensorBoard/nbextensionCodeLensProvider.unit.test.ts b/src/test/tensorBoard/nbextensionCodeLensProvider.unit.test.ts deleted file mode 100644 index 9a46d92c1422..000000000000 --- a/src/test/tensorBoard/nbextensionCodeLensProvider.unit.test.ts +++ /dev/null @@ -1,49 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -import { assert } from 'chai'; -import { CancellationTokenSource } from 'vscode'; -import { TensorBoardNbextensionCodeLensProvider } from '../../client/tensorBoard/nbextensionCodeLensProvider'; -import { MockDocument } from '../mocks/mockDocument'; - -suite('TensorBoard nbextension code lens provider', () => { - let codeLensProvider: TensorBoardNbextensionCodeLensProvider; - let cancelTokenSource: CancellationTokenSource; - - setup(() => { - codeLensProvider = new TensorBoardNbextensionCodeLensProvider([]); - cancelTokenSource = new CancellationTokenSource(); - }); - teardown(() => { - cancelTokenSource.dispose(); - }); - - test('Provide code lens for Python notebook loading tensorboard nbextension', async () => { - const document = new MockDocument('a=1\n%load_ext tensorboard', 'foo.ipynb', async () => true); - const codeLens = codeLensProvider.provideCodeLenses(document, cancelTokenSource.token); - assert.ok(codeLens.length > 0, 'Failed to provide code lens for file loading tensorboard nbextension'); - }); - test('Provide code lens for Python notebook launching tensorboard nbextension', async () => { - const document = new MockDocument('a=1\n%tensorboard --logdir logs/fit', 'foo.ipynb', async () => true); - const codeLens = codeLensProvider.provideCodeLenses(document, cancelTokenSource.token); - assert.ok(codeLens.length > 0, 'Failed to provide code lens for file loading tensorboard nbextension'); - }); - test('Fails when cancellation is signaled', () => { - const document = new MockDocument('a=1\n%tensorboard --logdir logs/fit', 'foo.ipynb', async () => true); - cancelTokenSource.cancel(); - const codeLens = codeLensProvider.provideCodeLenses(document, cancelTokenSource.token); - assert.ok(codeLens.length === 0, 'Provided codelens even after cancellation was requested'); - }); - // Can't verify these cases without running in vscode as we depend on vscode to not call us - // based on the DocumentSelector we provided. See nbExtensionCodeLensProvider.test.ts for that. - // test('Does not provide code lens for Python file loading tensorboard nbextension', async () => { - // const document = new MockDocument('a=1\n%load_ext tensorboard', 'foo.py', async () => true); - // const codeLens = codeLensProvider.provideCodeLenses(document); - // assert.ok(codeLens.length === 0, 'Provided code lens for Python file loading tensorboard nbextension'); - // }); - // test('Does not provide code lens for Python file launching tensorboard nbextension', async () => { - // const document = new MockDocument('a=1\n%tensorboard --logdir logs/fit', 'foo.py', async () => true); - // const codeLens = codeLensProvider.provideCodeLenses(document); - // assert.ok(codeLens.length === 0, 'Provided code lens for Python file loading tensorboard nbextension'); - // }); -}); diff --git a/src/test/tensorBoard/tensorBoardFileWatcher.test.ts b/src/test/tensorBoard/tensorBoardFileWatcher.test.ts deleted file mode 100644 index cd2692aabcfd..000000000000 --- a/src/test/tensorBoard/tensorBoardFileWatcher.test.ts +++ /dev/null @@ -1,94 +0,0 @@ -import { assert } from 'chai'; -import * as fse from 'fs-extra'; -import * as path from 'path'; -import * as sinon from 'sinon'; -import { IWorkspaceService } from '../../client/common/application/types'; -import { IExperimentService } from '../../client/common/types'; -import { TensorBoardFileWatcher } from '../../client/tensorBoard/tensorBoardFileWatcher'; -import { TensorBoardPrompt } from '../../client/tensorBoard/tensorBoardPrompt'; -import { waitForCondition } from '../common'; -import { initialize } from '../initialize'; - -suite('TensorBoard file system watcher', async () => { - const tfeventfileName = 'events.out.tfevents.1606887221.24672.162.v2'; - const currentDirectory = process.env.CODE_TESTS_WORKSPACE ?? path.join(__dirname, '..', '..', '..', 'src', 'test'); - let showNativeTensorBoardPrompt: sinon.SinonSpy; - const sandbox = sinon.createSandbox(); - let eventFile: string | undefined; - let eventFileDirectory: string | undefined; - - async function createFiles(directory: string) { - eventFileDirectory = directory; - await fse.ensureDir(directory); - eventFile = path.join(directory, tfeventfileName); - await fse.writeFile(eventFile, ''); - } - - async function configureStubsAndActivate() { - const { serviceManager } = await initialize(); - // Stub the prompt show method so we can verify that it was called - const prompt = serviceManager.get(TensorBoardPrompt); - showNativeTensorBoardPrompt = sandbox.stub(prompt, 'showNativeTensorBoardPrompt'); - serviceManager.rebindInstance(TensorBoardPrompt, prompt); - const experimentService = serviceManager.get(IExperimentService); - sandbox.stub(experimentService, 'inExperiment').resolves(true); - const fileWatcher = serviceManager.get(TensorBoardFileWatcher); - // eslint-disable-next-line @typescript-eslint/no-explicit-any - await (fileWatcher as any).activateInternal(); - } - - teardown(async () => { - sandbox.restore(); - if (eventFile) { - await fse.unlink(eventFile); - eventFile = undefined; - } - }); - - suiteTeardown(async () => { - if (eventFileDirectory && eventFileDirectory !== currentDirectory) { - await fse.rmdir(eventFileDirectory); - eventFileDirectory = undefined; - } - }); - - test('Creating tfeventfile one directory down results in prompt being shown', async () => { - const dir1 = path.join(currentDirectory, '1'); - await configureStubsAndActivate(); - await createFiles(dir1); - await waitForCondition(async () => showNativeTensorBoardPrompt.called, 5000, 'Prompt not shown'); - }); - - test('Creating tfeventfile two directories down results in prompt being called', async () => { - const dir2 = path.join(currentDirectory, '1', '2'); - await configureStubsAndActivate(); - await createFiles(dir2); - await waitForCondition(async () => showNativeTensorBoardPrompt.called, 5000, 'Prompt not shown'); - }); - - test('Creating tfeventfile three directories down does not result in prompt being called', async () => { - const dir3 = path.join(currentDirectory, '1', '2', '3'); - await configureStubsAndActivate(); - await createFiles(dir3); - await waitForCondition(async () => showNativeTensorBoardPrompt.notCalled, 5000, 'Prompt shown'); - }); - - test('No workspace folder open, prompt is not called', async () => { - const { serviceManager } = await initialize(); - - // Stub the prompt show method so we can verify that it was called - const prompt = serviceManager.get(TensorBoardPrompt); - showNativeTensorBoardPrompt = sandbox.stub(prompt, 'showNativeTensorBoardPrompt'); - serviceManager.rebindInstance(TensorBoardPrompt, prompt); - - // Pretend there are no open folders - const workspaceService = serviceManager.get(IWorkspaceService); - sandbox.stub(workspaceService, 'workspaceFolders').get(() => undefined); - serviceManager.rebindInstance(IWorkspaceService, workspaceService); - const fileWatcher = serviceManager.get(TensorBoardFileWatcher); - // eslint-disable-next-line @typescript-eslint/no-explicit-any - await (fileWatcher as any).activateInternal(); - - assert.ok(showNativeTensorBoardPrompt.notCalled); - }); -}); diff --git a/src/test/tensorBoard/tensorBoardImportCodeLensProvider.unit.test.ts b/src/test/tensorBoard/tensorBoardImportCodeLensProvider.unit.test.ts deleted file mode 100644 index 9b691c9af17c..000000000000 --- a/src/test/tensorBoard/tensorBoardImportCodeLensProvider.unit.test.ts +++ /dev/null @@ -1,58 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -import { assert } from 'chai'; -import { CancellationTokenSource } from 'vscode'; -import { TensorBoardImportCodeLensProvider } from '../../client/tensorBoard/tensorBoardImportCodeLensProvider'; -import { MockDocument } from '../mocks/mockDocument'; - -suite('TensorBoard import code lens provider', () => { - let codeLensProvider: TensorBoardImportCodeLensProvider; - let cancelTokenSource: CancellationTokenSource; - - setup(() => { - codeLensProvider = new TensorBoardImportCodeLensProvider([]); - cancelTokenSource = new CancellationTokenSource(); - }); - teardown(() => { - cancelTokenSource.dispose(); - }); - [ - 'import tensorboard', - 'import foo, tensorboard', - 'import foo, tensorboard, bar', - 'import tensorboardX', - 'import tensorboardX, bar', - 'import torch.profiler', - 'import foo, torch.profiler', - 'from torch.utils import tensorboard', - 'from torch.utils import foo, tensorboard', - 'import torch.utils.tensorboard, foo', - 'from torch import profiler', - ].forEach((importStatement) => { - test(`Provides code lens for Python files containing ${importStatement}`, () => { - const document = new MockDocument(importStatement, 'foo.py', async () => true); - const codeLens = codeLensProvider.provideCodeLenses(document, cancelTokenSource.token); - assert.ok(codeLens.length > 0, `Failed to provide code lens for file containing ${importStatement} import`); - }); - test(`Provides code lens for Python ipynbs containing ${importStatement}`, () => { - const document = new MockDocument(importStatement, 'foo.ipynb', async () => true); - const codeLens = codeLensProvider.provideCodeLenses(document, cancelTokenSource.token); - assert.ok( - codeLens.length > 0, - `Failed to provide code lens for ipynb containing ${importStatement} import`, - ); - }); - test('Fails when cancellation is signaled', () => { - const document = new MockDocument(importStatement, 'foo.py', async () => true); - cancelTokenSource.cancel(); - const codeLens = codeLensProvider.provideCodeLenses(document, cancelTokenSource.token); - assert.ok(codeLens.length === 0, 'Provided codelens even after cancellation was requested'); - }); - }); - test('Does not provide code lens if no matching import', () => { - const document = new MockDocument('import foo', 'foo.ipynb', async () => true); - const codeLens = codeLensProvider.provideCodeLenses(document, cancelTokenSource.token); - assert.ok(codeLens.length === 0, 'Provided code lens for file without tensorboard import'); - }); -}); diff --git a/src/test/tensorBoard/tensorBoardPrompt.unit.test.ts b/src/test/tensorBoard/tensorBoardPrompt.unit.test.ts deleted file mode 100644 index 6f096e560d70..000000000000 --- a/src/test/tensorBoard/tensorBoardPrompt.unit.test.ts +++ /dev/null @@ -1,65 +0,0 @@ -import { anything, instance, mock, verify, when } from 'ts-mockito'; -import { ApplicationShell } from '../../client/common/application/applicationShell'; -import { CommandManager } from '../../client/common/application/commandManager'; -import { Commands } from '../../client/common/constants'; -import { PersistentState, PersistentStateFactory } from '../../client/common/persistentState'; -import { Common } from '../../client/common/utils/localize'; -import { TensorBoardEntrypointTrigger } from '../../client/tensorBoard/constants'; -import { TensorBoardPrompt } from '../../client/tensorBoard/tensorBoardPrompt'; - -suite('TensorBoard prompt', () => { - let applicationShell: ApplicationShell; - let commandManager: CommandManager; - let persistentState: PersistentState; - let persistentStateFactory: PersistentStateFactory; - let prompt: TensorBoardPrompt; - - async function setupPromptWithOptions(persistentStateValue = true, selection = 'Yes') { - applicationShell = mock(ApplicationShell); - when(applicationShell.showInformationMessage(anything(), anything(), anything(), anything())).thenReturn( - Promise.resolve(selection), - ); - - commandManager = mock(CommandManager); - when(commandManager.executeCommand(Commands.LaunchTensorBoard, anything(), anything())).thenResolve(); - - persistentStateFactory = mock(PersistentStateFactory); - persistentState = mock(PersistentState) as PersistentState; - when(persistentState.value).thenReturn(persistentStateValue); - when(persistentState.updateValue(anything())).thenResolve(); - when(persistentStateFactory.createWorkspacePersistentState(anything(), anything())).thenReturn( - instance(persistentState), - ); - - prompt = new TensorBoardPrompt( - instance(applicationShell), - instance(commandManager), - instance(persistentStateFactory), - ); - await prompt.showNativeTensorBoardPrompt(TensorBoardEntrypointTrigger.palette); - } - - test('Show prompt if user is in experiment, and prompt has not previously been disabled or shown', async () => { - await setupPromptWithOptions(); - verify(applicationShell.showInformationMessage(anything(), anything(), anything(), anything())).once(); - verify(commandManager.executeCommand(Commands.LaunchTensorBoard, anything(), anything())).once(); - }); - - test('Disable prompt if user selects "Do not show again"', async () => { - await setupPromptWithOptions(true, Common.doNotShowAgain); - verify(persistentState.updateValue(false)).once(); - }); - - test('Do not show prompt if user has previously disabled prompt', async () => { - await setupPromptWithOptions(false); - verify(applicationShell.showInformationMessage(anything(), anything(), anything(), anything())).never(); - verify(commandManager.executeCommand(Commands.LaunchTensorBoard, anything(), anything())).never(); - }); - - test('Do not show prompt more than once per session', async () => { - await setupPromptWithOptions(); - verify(applicationShell.showInformationMessage(anything(), anything(), anything(), anything())).once(); - await prompt.showNativeTensorBoardPrompt(TensorBoardEntrypointTrigger.palette); - verify(applicationShell.showInformationMessage(anything(), anything(), anything(), anything())).once(); - }); -}); diff --git a/src/test/tensorBoard/tensorBoardSession.test.ts b/src/test/tensorBoard/tensorBoardSession.test.ts deleted file mode 100644 index 2faaf8e39611..000000000000 --- a/src/test/tensorBoard/tensorBoardSession.test.ts +++ /dev/null @@ -1,510 +0,0 @@ -import * as path from 'path'; -import { assert } from 'chai'; -import Sinon, * as sinon from 'sinon'; -import { SemVer } from 'semver'; -import { Uri, ViewColumn, window, workspace, WorkspaceConfiguration } from 'vscode'; -import { - IExperimentService, - IInstaller, - InstallerResponse, - Product, - ProductInstallStatus, -} from '../../client/common/types'; -import { Common, TensorBoard } from '../../client/common/utils/localize'; -import { IApplicationShell, ICommandManager } from '../../client/common/application/types'; -import { IServiceManager } from '../../client/ioc/types'; -import { TensorBoardEntrypoint, TensorBoardEntrypointTrigger } from '../../client/tensorBoard/constants'; -import { TensorBoardSession } from '../../client/tensorBoard/tensorBoardSession'; -import { closeActiveWindows, EXTENSION_ROOT_DIR_FOR_TESTS, initialize } from '../initialize'; -import { IInterpreterService } from '../../client/interpreter/contracts'; -import { Architecture } from '../../client/common/utils/platform'; -import { PythonEnvironment, EnvironmentType } from '../../client/pythonEnvironments/info'; -import { PYTHON_PATH } from '../common'; -import { ImportTracker } from '../../client/telemetry/importTracker'; -import { IMultiStepInput, IMultiStepInputFactory } from '../../client/common/utils/multiStepInput'; -import { ModuleInstallFlags } from '../../client/common/installer/types'; - -// Class methods exposed just for testing purposes -interface ITensorBoardSessionTestAPI { - jumpToSource(fsPath: string, line: number): Promise; -} - -const info: PythonEnvironment = { - architecture: Architecture.Unknown, - companyDisplayName: '', - displayName: '', - envName: '', - path: '', - envType: EnvironmentType.Unknown, - version: new SemVer('0.0.0-alpha'), - sysPrefix: '', - sysVersion: '', -}; - -const interpreter: PythonEnvironment = { - ...info, - envType: EnvironmentType.Unknown, - path: PYTHON_PATH, -}; - -suite('TensorBoard session creation', async () => { - let serviceManager: IServiceManager; - let errorMessageStub: Sinon.SinonStub; - let sandbox: Sinon.SinonSandbox; - let applicationShell: IApplicationShell; - let commandManager: ICommandManager; - let experimentService: IExperimentService; - let installer: IInstaller; - let initialValue: string | undefined; - let workspaceConfiguration: WorkspaceConfiguration; - - suiteSetup(function () { - if (process.env.CI_PYTHON_VERSION === '2.7') { - // TensorBoard 2.4.1 not available for Python 2.7 - this.skip(); - } - - // See: https://github.com/microsoft/vscode-python/issues/18130 - this.skip(); - }); - - setup(async () => { - sandbox = sinon.createSandbox(); - ({ serviceManager } = await initialize()); - - experimentService = serviceManager.get(IExperimentService); - const interpreterService = serviceManager.get(IInterpreterService); - sandbox.stub(interpreterService, 'getActiveInterpreter').resolves(interpreter); - - applicationShell = serviceManager.get(IApplicationShell); - commandManager = serviceManager.get(ICommandManager); - installer = serviceManager.get(IInstaller); - workspaceConfiguration = workspace.getConfiguration('python.tensorBoard'); - initialValue = workspaceConfiguration.get('logDirectory'); - await workspaceConfiguration.update('logDirectory', undefined, true); - }); - - teardown(async () => { - await workspaceConfiguration.update('logDirectory', initialValue, true); - await closeActiveWindows(); - sandbox.restore(); - }); - - function configureStubs( - hasTorchImports: boolean, - tensorBoardInstallStatus: ProductInstallStatus, - torchProfilerPackageInstallStatus: ProductInstallStatus, - installPromptSelection: 'Yes' | 'No', - ) { - sandbox.stub(ImportTracker, 'hasModuleImport').withArgs('torch').returns(hasTorchImports); - const isProductVersionCompatible = sandbox.stub(installer, 'isProductVersionCompatible'); - isProductVersionCompatible - .withArgs(Product.tensorboard, '>= 2.4.1', interpreter) - .resolves(tensorBoardInstallStatus); - isProductVersionCompatible - .withArgs(Product.torchProfilerImportName, '>= 0.2.0', interpreter) - .resolves(torchProfilerPackageInstallStatus); - errorMessageStub = sandbox.stub(applicationShell, 'showErrorMessage'); - errorMessageStub.resolves(installPromptSelection); - } - async function createSession() { - errorMessageStub = sandbox.stub(applicationShell, 'showErrorMessage'); - // Stub user selections - sandbox.stub(applicationShell, 'showQuickPick').resolves({ label: TensorBoard.useCurrentWorkingDirectory }); - - const session = (await commandManager.executeCommand( - 'python.launchTensorBoard', - TensorBoardEntrypoint.palette, - TensorBoardEntrypointTrigger.palette, - )) as TensorBoardSession; - - assert.ok(session.panel?.viewColumn === ViewColumn.One, 'Panel opened in wrong group'); - assert.ok(session.panel?.visible, 'Webview panel not shown on session creation golden path'); - assert.ok(errorMessageStub.notCalled, 'Error message shown on session creation golden path'); - return session; - } - suite('Core functionality', async () => { - test('Golden path: TensorBoard session starts successfully and webview is shown', async () => { - await createSession(); - }); - test('When webview is closed, session is killed', async () => { - const session = await createSession(); - const { daemon, panel } = session; - assert.ok(panel?.visible, 'Webview panel not shown'); - panel?.dispose(); - assert.ok(session.panel === undefined, 'Webview still visible'); - assert.ok(daemon?.killed, 'TensorBoard session process not killed after webview closed'); - }); - test('When user selects file picker, display file picker', async () => { - // Stub user selections - sandbox.stub(applicationShell, 'showQuickPick').resolves({ label: TensorBoard.selectAnotherFolder }); - const filePickerStub = sandbox.stub(applicationShell, 'showOpenDialog'); - - // Create session - await commandManager.executeCommand( - 'python.launchTensorBoard', - TensorBoardEntrypoint.palette, - TensorBoardEntrypointTrigger.palette, - ); - - assert.ok(filePickerStub.called, 'User requests to select another folder and file picker was not shown'); - }); - test('When user selects remote URL, display input box', async () => { - sandbox.stub(applicationShell, 'showQuickPick').resolves({ label: TensorBoard.enterRemoteUrl }); - const inputBoxStub = sandbox.stub(applicationShell, 'showInputBox'); - - // Create session - await commandManager.executeCommand( - 'python.launchTensorBoard', - TensorBoardEntrypoint.palette, - TensorBoardEntrypointTrigger.palette, - ); - - assert.ok( - inputBoxStub.called, - 'User requested to enter remote URL and input box to enter URL was not shown', - ); - }); - }); - suite('Installation prompt message', async () => { - async function createSessionAndVerifyMessage(message: string) { - sandbox.stub(applicationShell, 'showQuickPick').resolves({ label: TensorBoard.useCurrentWorkingDirectory }); - await commandManager.executeCommand( - 'python.launchTensorBoard', - TensorBoardEntrypoint.palette, - TensorBoardEntrypointTrigger.palette, - ); - assert.ok( - errorMessageStub.calledOnceWith(message, Common.bannerLabelYes, Common.bannerLabelNo), - 'Wrong error message shown', - ); - } - suite('Install profiler package + upgrade tensorboard', async () => { - async function runTest(expectTensorBoardUpgrade: boolean) { - const installStub = sandbox.stub(installer, 'install').resolves(InstallerResponse.Installed); - await createSessionAndVerifyMessage(TensorBoard.installTensorBoardAndProfilerPluginPrompt); - assert.ok(installStub.calledTwice, `Expected 2 installs but got ${installStub.callCount} calls`); - assert.ok(installStub.calledWith(Product.torchProfilerInstallName)); - assert.ok( - installStub.calledWith( - Product.tensorboard, - sinon.match.any, - sinon.match.any, - expectTensorBoardUpgrade ? ModuleInstallFlags.upgrade : undefined, - ), - ); - } - test('Has torch imports: true, is profiler package installed: false, TensorBoard needs upgrade', async () => { - configureStubs(true, ProductInstallStatus.NeedsUpgrade, ProductInstallStatus.NotInstalled, 'Yes'); - await runTest(true); - }); - test('Has torch imports: true, is profiler package installed: false, TensorBoard not installed', async () => { - configureStubs(true, ProductInstallStatus.NotInstalled, ProductInstallStatus.NotInstalled, 'Yes'); - await runTest(false); - }); - }); - suite('Install profiler only', async () => { - test('Has torch imports: true, is profiler package installed: false, TensorBoard installed', async () => { - configureStubs(true, ProductInstallStatus.Installed, ProductInstallStatus.NotInstalled, 'Yes'); - sandbox - .stub(applicationShell, 'showQuickPick') - .resolves({ label: TensorBoard.useCurrentWorkingDirectory }); - // Ensure we ask to install the profiler package and that it resolves to a cancellation - sandbox - .stub(installer, 'install') - .withArgs(Product.torchProfilerInstallName, sinon.match.any, sinon.match.any) - .resolves(InstallerResponse.Ignore); - - const session = (await commandManager.executeCommand( - 'python.launchTensorBoard', - TensorBoardEntrypoint.palette, - TensorBoardEntrypointTrigger.palette, - )) as TensorBoardSession; - - assert.ok(session.panel?.visible, 'Webview panel not shown, expected successful session creation'); - assert.ok( - errorMessageStub.calledOnceWith( - TensorBoard.installProfilerPluginPrompt, - Common.bannerLabelYes, - Common.bannerLabelNo, - ), - 'Wrong error message shown', - ); - }); - }); - suite('Install tensorboard only', async () => { - [false, true].forEach(async (hasTorchImports) => { - [ - ProductInstallStatus.Installed, - ProductInstallStatus.NotInstalled, - ProductInstallStatus.NeedsUpgrade, - ].forEach(async (torchProfilerInstallStatus) => { - const isTorchProfilerPackageInstalled = - torchProfilerInstallStatus === ProductInstallStatus.Installed; - if (!(hasTorchImports && !isTorchProfilerPackageInstalled)) { - test(`Has torch imports: ${hasTorchImports}, is profiler package installed: ${isTorchProfilerPackageInstalled}, TensorBoard not installed`, async () => { - configureStubs( - hasTorchImports, - ProductInstallStatus.NotInstalled, - torchProfilerInstallStatus, - 'No', - ); - await createSessionAndVerifyMessage(TensorBoard.installPrompt); - }); - } - }); - }); - }); - suite('Upgrade tensorboard only', async () => { - async function runTest() { - const installStub = sandbox.stub(installer, 'install').resolves(InstallerResponse.Installed); - await createSessionAndVerifyMessage(TensorBoard.upgradePrompt); - - assert.ok(installStub.calledOnce, `Expected 1 install but got ${installStub.callCount} installs`); - assert.ok(installStub.args[0][0] === Product.tensorboard, 'Did not install tensorboard'); - assert.ok( - installStub.args.filter((argsList) => argsList[0] === Product.torchProfilerInstallName).length === - 0, - 'Unexpected attempt to install profiler package', - ); - } - - [false, true].forEach(async (hasTorchImports) => { - [ - ProductInstallStatus.Installed, - ProductInstallStatus.NotInstalled, - ProductInstallStatus.NeedsUpgrade, - ].forEach(async (torchProfilerInstallStatus) => { - const isTorchProfilerPackageInstalled = - torchProfilerInstallStatus === ProductInstallStatus.Installed; - if (!(hasTorchImports && !isTorchProfilerPackageInstalled)) { - test(`Has torch imports: ${hasTorchImports}, is profiler package installed: ${isTorchProfilerPackageInstalled}, TensorBoard needs upgrade`, async () => { - configureStubs( - hasTorchImports, - ProductInstallStatus.NeedsUpgrade, - torchProfilerInstallStatus, - 'Yes', - ); - await runTest(); - }); - } - }); - }); - }); - suite('No prompt', async () => { - async function runTest() { - sandbox - .stub(applicationShell, 'showQuickPick') - .resolves({ label: TensorBoard.useCurrentWorkingDirectory }); - await commandManager.executeCommand( - 'python.launchTensorBoard', - TensorBoardEntrypoint.palette, - TensorBoardEntrypointTrigger.palette, - ); - assert.ok(errorMessageStub.notCalled, 'Prompt was unexpectedly shown'); - } - - [false, true].forEach(async (hasTorchImports) => { - [ - ProductInstallStatus.Installed, - ProductInstallStatus.NotInstalled, - ProductInstallStatus.NeedsUpgrade, - ].forEach(async (torchProfilerInstallStatus) => { - const isTorchProfilerPackageInstalled = - torchProfilerInstallStatus === ProductInstallStatus.Installed; - if (!(hasTorchImports && !isTorchProfilerPackageInstalled)) { - test(`Has torch imports: ${hasTorchImports}, is profiler package installed: ${isTorchProfilerPackageInstalled}, TensorBoard installed`, async () => { - configureStubs( - hasTorchImports, - ProductInstallStatus.Installed, - torchProfilerInstallStatus, - 'Yes', - ); - await runTest(); - }); - } - }); - }); - }); - }); - suite('Error messages', async () => { - test('If user cancels starting TensorBoard session, do not show error', async () => { - sandbox.stub(applicationShell, 'showQuickPick').resolves({ label: TensorBoard.useCurrentWorkingDirectory }); - sandbox.stub(applicationShell, 'withProgress').resolves('canceled'); - errorMessageStub = sandbox.stub(applicationShell, 'showErrorMessage'); - - await commandManager.executeCommand( - 'python.launchTensorBoard', - TensorBoardEntrypoint.palette, - TensorBoardEntrypointTrigger.palette, - ); - - assert.ok(errorMessageStub.notCalled, 'User canceled session start and error was shown'); - }); - test('If existing install of TensorBoard is outdated and user cancels installation, do not show error', async () => { - sandbox.stub(experimentService, 'inExperiment').resolves(true); - errorMessageStub = sandbox.stub(applicationShell, 'showErrorMessage'); - sandbox.stub(installer, 'isProductVersionCompatible').resolves(ProductInstallStatus.NeedsUpgrade); - sandbox.stub(installer, 'install').resolves(InstallerResponse.Ignore); - const quickPickStub = sandbox.stub(applicationShell, 'showQuickPick'); - - await commandManager.executeCommand( - 'python.launchTensorBoard', - TensorBoardEntrypoint.palette, - TensorBoardEntrypointTrigger.palette, - ); - - assert.ok(quickPickStub.notCalled, 'User opted not to upgrade and we proceeded to create session'); - }); - test('If TensorBoard is not installed and user chooses not to install, do not show error', async () => { - configureStubs(true, ProductInstallStatus.NotInstalled, ProductInstallStatus.NotInstalled, 'Yes'); - sandbox.stub(installer, 'install').resolves(InstallerResponse.Ignore); - - await commandManager.executeCommand( - 'python.launchTensorBoard', - TensorBoardEntrypoint.palette, - TensorBoardEntrypointTrigger.palette, - ); - - assert.ok( - errorMessageStub.calledOnceWith( - TensorBoard.installTensorBoardAndProfilerPluginPrompt, - Common.bannerLabelYes, - Common.bannerLabelNo, - ), - 'User opted not to install and error was shown', - ); - }); - test('If user does not select a logdir, do not show error', async () => { - sandbox.stub(experimentService, 'inExperiment').resolves(true); - errorMessageStub = sandbox.stub(applicationShell, 'showErrorMessage'); - // Stub user selections - sandbox.stub(applicationShell, 'showQuickPick').resolves({ label: TensorBoard.selectAFolder }); - sandbox.stub(applicationShell, 'showOpenDialog').resolves(undefined); - - // Create session - await commandManager.executeCommand( - 'python.launchTensorBoard', - TensorBoardEntrypoint.palette, - TensorBoardEntrypointTrigger.palette, - ); - - assert.ok(errorMessageStub.notCalled, 'User opted not to select a logdir and error was shown'); - }); - test('If starting TensorBoard times out, show error', async () => { - sandbox.stub(applicationShell, 'showQuickPick').resolves({ label: TensorBoard.useCurrentWorkingDirectory }); - sandbox.stub(applicationShell, 'withProgress').resolves(60_000); - errorMessageStub = sandbox.stub(applicationShell, 'showErrorMessage'); - - await commandManager.executeCommand( - 'python.launchTensorBoard', - TensorBoardEntrypoint.palette, - TensorBoardEntrypointTrigger.palette, - ); - - assert.ok(errorMessageStub.called, 'TensorBoard timed out but no error was shown'); - }); - test('If installing the profiler package fails, do not show error, continue to create session', async () => { - configureStubs(true, ProductInstallStatus.Installed, ProductInstallStatus.NotInstalled, 'Yes'); - sandbox.stub(applicationShell, 'showQuickPick').resolves({ label: TensorBoard.useCurrentWorkingDirectory }); - // Ensure we ask to install the profiler package and that it resolves to a cancellation - sandbox - .stub(installer, 'install') - .withArgs(Product.torchProfilerInstallName, sinon.match.any, sinon.match.any) - .resolves(InstallerResponse.Ignore); - - const session = (await commandManager.executeCommand( - 'python.launchTensorBoard', - TensorBoardEntrypoint.palette, - TensorBoardEntrypointTrigger.palette, - )) as TensorBoardSession; - - assert.ok(session.panel?.visible, 'Webview panel not shown, expected successful session creation'); - }); - test('If user opts not to install profiler package and tensorboard is already installed, continue to create session', async () => { - configureStubs(true, ProductInstallStatus.Installed, ProductInstallStatus.NotInstalled, 'No'); - sandbox.stub(applicationShell, 'showQuickPick').resolves({ label: TensorBoard.useCurrentWorkingDirectory }); - const session = (await commandManager.executeCommand( - 'python.launchTensorBoard', - TensorBoardEntrypoint.palette, - TensorBoardEntrypointTrigger.palette, - )) as TensorBoardSession; - assert.ok(session.panel?.visible, 'Webview panel not shown, expected successful session creation'); - }); - }); - test('If python.tensorBoard.logDirectory is provided, do not prompt user to pick a log directory', async () => { - const selectDirectoryStub = sandbox - .stub(applicationShell, 'showQuickPick') - .resolves({ label: TensorBoard.useCurrentWorkingDirectory }); - errorMessageStub = sandbox.stub(applicationShell, 'showErrorMessage'); - await workspaceConfiguration.update('logDirectory', 'logs/fit', true); - - const session = (await commandManager.executeCommand( - 'python.launchTensorBoard', - TensorBoardEntrypoint.palette, - TensorBoardEntrypointTrigger.palette, - )) as TensorBoardSession; - - assert.ok(session.panel?.visible, 'Expected successful session creation but webpanel not shown'); - assert.ok(errorMessageStub.notCalled, 'Expected successful session creation but error message was shown'); - assert.ok( - selectDirectoryStub.notCalled, - 'Prompted user to select log directory although setting was specified', - ); - }); - suite('Jump to source', async () => { - // We can't test a full E2E scenario with the TB profiler plugin because we can't - // accurately target simulated clicks at iframed content. This only tests - // code from the moment that the VS Code webview posts a message back - // to the extension. - const fsPath = path.join( - EXTENSION_ROOT_DIR_FOR_TESTS, - 'src', - 'test', - 'pythonFiles', - 'tensorBoard', - 'sourcefile.py', - ); - teardown(() => { - sandbox.restore(); - }); - function setupStubsForMultiStepInput() { - // Stub the factory to return our stubbed multistep input when it's asked to create one - const multiStepFactory = serviceManager.get(IMultiStepInputFactory); - const inputInstance = multiStepFactory.create(); - // Create a multistep input with stubs for methods - const showQuickPickStub = sandbox.stub(inputInstance, 'showQuickPick').resolves({ - label: TensorBoard.selectMissingSourceFile, - description: TensorBoard.selectMissingSourceFileDescription, - }); - const createInputStub = sandbox - .stub(multiStepFactory, 'create') - .returns(inputInstance as IMultiStepInput); - // Stub the system file picker - const filePickerStub = sandbox.stub(applicationShell, 'showOpenDialog').resolves([Uri.file(fsPath)]); - return [showQuickPickStub, createInputStub, filePickerStub]; - } - test('Resolves filepaths without displaying prompt', async () => { - const session = ((await createSession()) as unknown) as ITensorBoardSessionTestAPI; - const stubs = setupStubsForMultiStepInput(); - await session.jumpToSource(fsPath, 0); - assert.ok(window.activeTextEditor !== undefined, 'Source file not resolved'); - assert.ok(window.activeTextEditor?.document.uri.fsPath === fsPath, 'Wrong source file opened'); - assert.ok( - stubs.reduce((prev, current) => current.notCalled && prev, true), - 'Stubs were called when file is present', - ); - }); - test('Display quickpick to user if filepath is not on disk', async () => { - const session = ((await createSession()) as unknown) as ITensorBoardSessionTestAPI; - const stubs = setupStubsForMultiStepInput(); - await session.jumpToSource('/nonexistent/file/path.py', 0); - assert.ok(window.activeTextEditor !== undefined, 'Source file not resolved'); - assert.ok(window.activeTextEditor?.document.uri.fsPath === fsPath, 'Wrong source file opened'); - assert.ok( - stubs.reduce((prev, current) => current.calledOnce && prev, true), - 'Stubs called an unexpected number of times', - ); - }); - }); -}); diff --git a/src/test/tensorBoard/tensorBoardUsageTracker.unit.test.ts b/src/test/tensorBoard/tensorBoardUsageTracker.unit.test.ts deleted file mode 100644 index b6efad083a57..000000000000 --- a/src/test/tensorBoard/tensorBoardUsageTracker.unit.test.ts +++ /dev/null @@ -1,91 +0,0 @@ -import { assert } from 'chai'; -import * as sinon from 'sinon'; -import { TensorBoardUsageTracker } from '../../client/tensorBoard/tensorBoardUsageTracker'; -import { TensorBoardPrompt } from '../../client/tensorBoard/tensorBoardPrompt'; -import { MockDocumentManager } from '../mocks/mockDocumentManager'; -import { createTensorBoardPromptWithMocks } from './helpers'; - -suite('TensorBoard usage tracker', () => { - let documentManager: MockDocumentManager; - let tensorBoardImportTracker: TensorBoardUsageTracker; - let prompt: TensorBoardPrompt; - let showNativeTensorBoardPrompt: sinon.SinonSpy; - - setup(() => { - documentManager = new MockDocumentManager(); - prompt = createTensorBoardPromptWithMocks(); - showNativeTensorBoardPrompt = sinon.spy(prompt, 'showNativeTensorBoardPrompt'); - tensorBoardImportTracker = new TensorBoardUsageTracker(documentManager, [], prompt); - }); - - test('Simple tensorboard import in Python file', async () => { - const document = documentManager.addDocument('import tensorboard', 'foo.py'); - await documentManager.showTextDocument(document); - await tensorBoardImportTracker.activate(); - assert.ok(showNativeTensorBoardPrompt.calledOnce); - }); - test('Simple tensorboardX import in Python file', async () => { - const document = documentManager.addDocument('import tensorboardX', 'foo.py'); - await documentManager.showTextDocument(document); - await tensorBoardImportTracker.activate(); - assert.ok(showNativeTensorBoardPrompt.calledOnce); - }); - test('Simple tensorboard import in Python ipynb', async () => { - const document = documentManager.addDocument('import tensorboard', 'foo.ipynb'); - await documentManager.showTextDocument(document); - await tensorBoardImportTracker.activate(); - assert.ok(showNativeTensorBoardPrompt.calledOnce); - }); - test('`from x.y.tensorboard import z` import', async () => { - const document = documentManager.addDocument('from torch.utils.tensorboard import SummaryWriter', 'foo.py'); - await documentManager.showTextDocument(document); - await tensorBoardImportTracker.activate(); - assert.ok(showNativeTensorBoardPrompt.calledOnce); - }); - test('`from x.y import tensorboard` import', async () => { - const document = documentManager.addDocument('from torch.utils import tensorboard', 'foo.py'); - await documentManager.showTextDocument(document); - await tensorBoardImportTracker.activate(); - assert.ok(showNativeTensorBoardPrompt.calledOnce); - }); - test('`from tensorboardX import x` import', async () => { - const document = documentManager.addDocument('from tensorboardX import SummaryWriter', 'foo.py'); - await documentManager.showTextDocument(document); - await tensorBoardImportTracker.activate(); - assert.ok(showNativeTensorBoardPrompt.calledOnce); - }); - test('`import x, y` import', async () => { - const document = documentManager.addDocument('import tensorboard, tensorflow', 'foo.py'); - await documentManager.showTextDocument(document); - await tensorBoardImportTracker.activate(); - assert.ok(showNativeTensorBoardPrompt.calledOnce); - }); - test('`import pkg as _` import', async () => { - const document = documentManager.addDocument('import tensorboard as tb', 'foo.py'); - await documentManager.showTextDocument(document); - await tensorBoardImportTracker.activate(); - assert.ok(showNativeTensorBoardPrompt.calledOnce); - }); - test('Show prompt on changed text editor', async () => { - await tensorBoardImportTracker.activate(); - const document = documentManager.addDocument('import tensorboard as tb', 'foo.py'); - await documentManager.showTextDocument(document); - assert.ok(showNativeTensorBoardPrompt.calledOnce); - }); - test('Do not show prompt if no tensorboard import', async () => { - const document = documentManager.addDocument('import tensorflow as tf\nfrom torch.utils import foo', 'foo.py'); - await documentManager.showTextDocument(document); - await tensorBoardImportTracker.activate(); - assert.ok(showNativeTensorBoardPrompt.notCalled); - }); - test('Do not show prompt if language is not Python', async () => { - const document = documentManager.addDocument( - 'import tensorflow as tf\nfrom torch.utils import foo', - 'foo.cpp', - 'cpp', - ); - await documentManager.showTextDocument(document); - await tensorBoardImportTracker.activate(); - assert.ok(showNativeTensorBoardPrompt.notCalled); - }); -}); diff --git a/src/test/terminals/activation.unit.test.ts b/src/test/terminals/activation.unit.test.ts index dea0c891229d..4c5294a82f49 100644 --- a/src/test/terminals/activation.unit.test.ts +++ b/src/test/terminals/activation.unit.test.ts @@ -2,6 +2,7 @@ // Licensed under the MIT License. import { anything, instance, mock, verify, when } from 'ts-mockito'; +import * as sinon from 'sinon'; import { EventEmitter, Terminal } from 'vscode'; import { ActiveResourceService } from '../../client/common/application/activeResource'; import { TerminalManager } from '../../client/common/application/terminalManager'; @@ -11,6 +12,7 @@ import { ITerminalActivator } from '../../client/common/terminal/types'; import { TerminalAutoActivation } from '../../client/terminals/activation'; import { ITerminalAutoActivation } from '../../client/terminals/types'; import { noop } from '../core'; +import * as extapi from '../../client/envExt/api.internal'; suite('Terminal', () => { suite('Terminal Auto Activation', () => { @@ -21,8 +23,12 @@ suite('Terminal', () => { let onDidOpenTerminalEventEmitter: EventEmitter; let terminal: Terminal; let nonActivatedTerminal: Terminal; + let shouldEnvExtHandleActivationStub: sinon.SinonStub; setup(() => { + shouldEnvExtHandleActivationStub = sinon.stub(extapi, 'shouldEnvExtHandleActivation'); + shouldEnvExtHandleActivationStub.returns(false); + manager = mock(TerminalManager); activator = mock(TerminalActivator); resourceService = mock(ActiveResourceService); @@ -60,6 +66,9 @@ suite('Terminal', () => { autoActivation.register(); }); // teardown(() => fakeTimer.uninstall()); + teardown(() => { + sinon.restore(); + }); test('Should activate terminal', async () => { // Trigger opening a terminal. @@ -77,5 +86,12 @@ suite('Terminal', () => { // The terminal should get activated. verify(activator.activateEnvironmentInTerminal(anything(), anything())).never(); }); + test('Should not activate terminal when envs extension should handle activation', async () => { + shouldEnvExtHandleActivationStub.returns(true); + + await ((onDidOpenTerminalEventEmitter.fire(terminal) as unknown) as Promise); + + verify(activator.activateEnvironmentInTerminal(anything(), anything())).never(); + }); }); }); diff --git a/src/test/terminals/codeExecution/codeExecutionManager.unit.test.ts b/src/test/terminals/codeExecution/codeExecutionManager.unit.test.ts index 3676834873a0..726b118ce180 100644 --- a/src/test/terminals/codeExecution/codeExecutionManager.unit.test.ts +++ b/src/test/terminals/codeExecution/codeExecutionManager.unit.test.ts @@ -2,17 +2,19 @@ // Licensed under the MIT License. import { expect } from 'chai'; import * as TypeMoq from 'typemoq'; +import * as sinon from 'sinon'; import { Disposable, TextDocument, TextEditor, Uri } from 'vscode'; import { ICommandManager, IDocumentManager, IWorkspaceService } from '../../../client/common/application/types'; import { Commands } from '../../../client/common/constants'; -import { IFileSystem } from '../../../client/common/platform/types'; import { IServiceContainer } from '../../../client/ioc/types'; import { CodeExecutionManager } from '../../../client/terminals/codeExecution/codeExecutionManager'; import { ICodeExecutionHelper, ICodeExecutionManager, ICodeExecutionService } from '../../../client/terminals/types'; import { IConfigurationService } from '../../../client/common/types'; import { IInterpreterService } from '../../../client/interpreter/contracts'; import { PythonEnvironment } from '../../../client/pythonEnvironments/info'; +import * as triggerApis from '../../../client/pythonEnvironments/creation/createEnvironmentTrigger'; +import * as extapi from '../../../client/envExt/api.internal'; suite('Terminal - Code Execution Manager', () => { let executionManager: ICodeExecutionManager; @@ -22,11 +24,13 @@ suite('Terminal - Code Execution Manager', () => { let serviceContainer: TypeMoq.IMock; let documentManager: TypeMoq.IMock; let configService: TypeMoq.IMock; - let fileSystem: TypeMoq.IMock; let interpreterService: TypeMoq.IMock; + let triggerCreateEnvironmentCheckNonBlockingStub: sinon.SinonStub; + let useEnvExtensionStub: sinon.SinonStub; setup(() => { - fileSystem = TypeMoq.Mock.ofType(); - fileSystem.setup((f) => f.readFile(TypeMoq.It.isAny())).returns(() => Promise.resolve('')); + useEnvExtensionStub = sinon.stub(extapi, 'useEnvExtension'); + useEnvExtensionStub.returns(false); + workspace = TypeMoq.Mock.ofType(); workspace .setup((c) => c.onDidChangeWorkspaceFolders(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny())) @@ -48,12 +52,17 @@ suite('Terminal - Code Execution Manager', () => { commandManager.object, documentManager.object, disposables, - fileSystem.object, configService.object, serviceContainer.object, ); + triggerCreateEnvironmentCheckNonBlockingStub = sinon.stub( + triggerApis, + 'triggerCreateEnvironmentCheckNonBlocking', + ); + triggerCreateEnvironmentCheckNonBlockingStub.returns(undefined); }); teardown(() => { + sinon.restore(); disposables.forEach((disposable) => { if (disposable) { disposable.dispose(); @@ -77,12 +86,15 @@ suite('Terminal - Code Execution Manager', () => { executionManager.registerCommands(); const sorted = registered.sort(); - expect(sorted).to.deep.equal([ - Commands.Exec_In_Terminal, - Commands.Exec_In_Terminal_Icon, - Commands.Exec_Selection_In_Django_Shell, - Commands.Exec_Selection_In_Terminal, - ]); + expect(sorted).to.deep.equal( + [ + Commands.Exec_In_Separate_Terminal, + Commands.Exec_In_Terminal, + Commands.Exec_In_Terminal_Icon, + Commands.Exec_Selection_In_Django_Shell, + Commands.Exec_Selection_In_Terminal, + ].sort(), + ); }); test('Ensure executeFileInterTerminal will do nothing if no file is avialble', async () => { @@ -135,7 +147,10 @@ suite('Terminal - Code Execution Manager', () => { const fileToExecute = Uri.file('x'); await commandHandler!(fileToExecute); helper.verify(async (h) => h.getFileToExecute(), TypeMoq.Times.never()); - executionService.verify(async (e) => e.executeFile(TypeMoq.It.isValue(fileToExecute)), TypeMoq.Times.once()); + executionService.verify( + async (e) => e.executeFile(TypeMoq.It.isValue(fileToExecute), TypeMoq.It.isAny()), + TypeMoq.Times.once(), + ); }); test('Ensure executeFileInterTerminal will use active file', async () => { @@ -164,7 +179,10 @@ suite('Terminal - Code Execution Manager', () => { .returns(() => executionService.object); await commandHandler!(fileToExecute); - executionService.verify(async (e) => e.executeFile(TypeMoq.It.isValue(fileToExecute)), TypeMoq.Times.once()); + executionService.verify( + async (e) => e.executeFile(TypeMoq.It.isValue(fileToExecute), TypeMoq.It.isAny()), + TypeMoq.Times.once(), + ); }); async function testExecutionOfSelectionWithoutAnyActiveDocument(commandId: string, executionSericeId: string) { diff --git a/src/test/terminals/codeExecution/djangoShellCodeExect.unit.test.ts b/src/test/terminals/codeExecution/djangoShellCodeExect.unit.test.ts index 6c6cf5baec76..749d94672765 100644 --- a/src/test/terminals/codeExecution/djangoShellCodeExect.unit.test.ts +++ b/src/test/terminals/codeExecution/djangoShellCodeExect.unit.test.ts @@ -6,7 +6,12 @@ import * as path from 'path'; import * as TypeMoq from 'typemoq'; import * as sinon from 'sinon'; import { Disposable, Uri, WorkspaceFolder } from 'vscode'; -import { ICommandManager, IDocumentManager, IWorkspaceService } from '../../../client/common/application/types'; +import { + IApplicationShell, + ICommandManager, + IDocumentManager, + IWorkspaceService, +} from '../../../client/common/application/types'; import { IFileSystem, IPlatformService } from '../../../client/common/platform/types'; import { createCondaEnv } from '../../../client/common/process/pythonEnvironment'; import { createPythonProcessService } from '../../../client/common/process/pythonProcess'; @@ -32,12 +37,14 @@ suite('Terminal - Django Shell Code Execution', () => { let settings: TypeMoq.IMock; let interpreterService: TypeMoq.IMock; let pythonExecutionFactory: TypeMoq.IMock; + let applicationShell: TypeMoq.IMock; let disposables: Disposable[] = []; setup(() => { const terminalFactory = TypeMoq.Mock.ofType(); terminalSettings = TypeMoq.Mock.ofType(); terminalService = TypeMoq.Mock.ofType(); const configService = TypeMoq.Mock.ofType(); + applicationShell = TypeMoq.Mock.ofType(); workspace = TypeMoq.Mock.ofType(); workspace .setup((c) => c.onDidChangeWorkspaceFolders(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny())) @@ -62,6 +69,7 @@ suite('Terminal - Django Shell Code Execution', () => { fileSystem.object, disposables, interpreterService.object, + applicationShell.object, ); terminalFactory.setup((f) => f.getTerminalService(TypeMoq.It.isAny())).returns(() => terminalService.object); diff --git a/src/test/terminals/codeExecution/helper.test.ts b/src/test/terminals/codeExecution/helper.test.ts index 684ca22b6c75..b7e0d1617884 100644 --- a/src/test/terminals/codeExecution/helper.test.ts +++ b/src/test/terminals/codeExecution/helper.test.ts @@ -4,12 +4,14 @@ 'use strict'; import { expect } from 'chai'; -import * as fs from 'fs-extra'; import * as path from 'path'; import { SemVer } from 'semver'; import * as TypeMoq from 'typemoq'; import { Position, Range, Selection, TextDocument, TextEditor, TextLine, Uri } from 'vscode'; +import * as sinon from 'sinon'; +import * as fs from '../../../client/common/platform/fs-paths'; import { + IActiveResourceService, IApplicationShell, ICommandManager, IDocumentManager, @@ -23,6 +25,7 @@ import { IProcessServiceFactory, ObservableExecutionResult, } from '../../../client/common/process/types'; +import { IConfigurationService, IPythonSettings } from '../../../client/common/types'; import { Architecture } from '../../../client/common/utils/platform'; import { IEnvironmentVariablesProvider } from '../../../client/common/variables/types'; import { IInterpreterService } from '../../../client/interpreter/contracts'; @@ -30,11 +33,13 @@ import { IServiceContainer } from '../../../client/ioc/types'; import { EnvironmentType, PythonEnvironment } from '../../../client/pythonEnvironments/info'; import { CodeExecutionHelper } from '../../../client/terminals/codeExecution/helper'; import { ICodeExecutionHelper } from '../../../client/terminals/types'; -import { PYTHON_PATH } from '../../common'; +import { PYTHON_PATH, getPythonSemVer } from '../../common'; +import { ReplType } from '../../../client/repl/types'; -const TEST_FILES_PATH = path.join(EXTENSION_ROOT_DIR, 'src', 'test', 'pythonFiles', 'terminalExec'); +const TEST_FILES_PATH = path.join(EXTENSION_ROOT_DIR, 'src', 'test', 'python_files', 'terminalExec'); -suite('Terminal - Code Execution Helper', () => { +suite('Terminal - Code Execution Helper', async () => { + let activeResourceService: TypeMoq.IMock; let documentManager: TypeMoq.IMock; let applicationShell: TypeMoq.IMock; let helper: ICodeExecutionHelper; @@ -44,6 +49,9 @@ suite('Terminal - Code Execution Helper', () => { let interpreterService: TypeMoq.IMock; let commandManager: TypeMoq.IMock; let workspaceService: TypeMoq.IMock; + let configurationService: TypeMoq.IMock; + let pythonSettings: TypeMoq.IMock; + let jsonParseStub: sinon.SinonStub; const workingPython: PythonEnvironment = { path: PYTHON_PATH, version: new SemVer('3.6.6-final'), @@ -57,13 +65,16 @@ suite('Terminal - Code Execution Helper', () => { setup(() => { const serviceContainer = TypeMoq.Mock.ofType(); commandManager = TypeMoq.Mock.ofType(); + configurationService = TypeMoq.Mock.ofType(); workspaceService = TypeMoq.Mock.ofType(); documentManager = TypeMoq.Mock.ofType(); applicationShell = TypeMoq.Mock.ofType(); const envVariablesProvider = TypeMoq.Mock.ofType(); processService = TypeMoq.Mock.ofType(); interpreterService = TypeMoq.Mock.ofType(); - + activeResourceService = TypeMoq.Mock.ofType(); + pythonSettings = TypeMoq.Mock.ofType(); + const resource = Uri.parse('a'); // eslint-disable-next-line @typescript-eslint/no-explicit-any processService.setup((x: any) => x.then).returns(() => undefined); interpreterService @@ -95,6 +106,30 @@ suite('Terminal - Code Execution Helper', () => { serviceContainer .setup((c) => c.get(TypeMoq.It.isValue(IEnvironmentVariablesProvider), TypeMoq.It.isAny())) .returns(() => envVariablesProvider.object); + serviceContainer + .setup((c) => c.get(TypeMoq.It.isValue(IConfigurationService))) + .returns(() => configurationService.object); + serviceContainer + .setup((c) => c.get(TypeMoq.It.isValue(IActiveResourceService))) + .returns(() => activeResourceService.object); + activeResourceService.setup((a) => a.getActiveResource()).returns(() => resource); + pythonSettings + .setup((s) => s.REPL) + .returns(() => ({ + enableREPLSmartSend: false, + REPLSmartSend: false, + sendToNativeREPL: false, + })); + configurationService.setup((x) => x.getSettings(TypeMoq.It.isAny())).returns(() => pythonSettings.object); + configurationService + .setup((c) => c.getSettings(TypeMoq.It.isAny())) + .returns({ + REPL: { + EnableREPLSmartSend: false, + REPLSmartSend: false, + }, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + } as any); helper = new CodeExecutionHelper(serviceContainer.object); document = TypeMoq.Mock.ofType(); @@ -102,7 +137,68 @@ suite('Terminal - Code Execution Helper', () => { editor.setup((e) => e.document).returns(() => document.object); }); + test('normalizeLines with BASIC_REPL does not attach bracketed paste mode', async () => { + configurationService + .setup((c) => c.getSettings(TypeMoq.It.isAny())) + .returns({ + REPL: { + EnableREPLSmartSend: false, + REPLSmartSend: false, + }, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + } as any); + const actualProcessService = new ProcessService(); + processService + .setup((p) => p.execObservable(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny())) + .returns((file, args, options) => + actualProcessService.execObservable.apply(actualProcessService, [file, args, options]), + ); + + jsonParseStub = sinon.stub(JSON, 'parse'); + const mockResult = { + normalized: 'print("Looks like you are on 3.13")', + attach_bracket_paste: true, + }; + jsonParseStub.returns(mockResult); + + const result = await helper.normalizeLines('print("Looks like you are on 3.13")', ReplType.terminal); + + expect(result).to.equal(`print("Looks like you are on 3.13")`); + jsonParseStub.restore(); + }); + + test('normalizeLines should not attach bracketed paste for < 3.13', async () => { + jsonParseStub = sinon.stub(JSON, 'parse'); + const mockResult = { + normalized: 'print("Looks like you are not on 3.13")', + attach_bracket_paste: false, + }; + jsonParseStub.returns(mockResult); + + configurationService + .setup((c) => c.getSettings(TypeMoq.It.isAny())) + .returns({ + REPL: { + EnableREPLSmartSend: false, + REPLSmartSend: false, + }, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + } as any); + const actualProcessService = new ProcessService(); + processService + .setup((p) => p.execObservable(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny())) + .returns((file, args, options) => + actualProcessService.execObservable.apply(actualProcessService, [file, args, options]), + ); + + const result = await helper.normalizeLines('print("Looks like you are not on 3.13")', ReplType.terminal); + + expect(result).to.equal('print("Looks like you are not on 3.13")'); + jsonParseStub.restore(); + }); + test('normalizeLines should call normalizeSelection.py', async () => { + jsonParseStub.restore(); let execArgs = ''; processService @@ -112,34 +208,54 @@ suite('Terminal - Code Execution Helper', () => { return ({} as unknown) as ObservableExecutionResult; }); - await helper.normalizeLines('print("hello")'); + await helper.normalizeLines('print("hello")', ReplType.terminal); expect(execArgs).to.contain('normalizeSelection.py'); }); async function ensureCodeIsNormalized(source: string, expectedSource: string) { + configurationService + .setup((c) => c.getSettings(TypeMoq.It.isAny())) + .returns({ + REPL: { + EnableREPLSmartSend: false, + REPLSmartSend: false, + }, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + } as any); const actualProcessService = new ProcessService(); processService .setup((p) => p.execObservable(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny())) .returns((file, args, options) => actualProcessService.execObservable.apply(actualProcessService, [file, args, options]), ); - const normalizedCode = await helper.normalizeLines(source); + const normalizedCode = await helper.normalizeLines(source, ReplType.terminal); const normalizedExpected = expectedSource.replace(/\r\n/g, '\n'); expect(normalizedCode).to.be.equal(normalizedExpected); } - ['', '1', '2', '3', '4', '5', '6', '7', '8'].forEach((fileNameSuffix) => { - test(`Ensure code is normalized (Sample${fileNameSuffix})`, async () => { - const code = await fs.readFile(path.join(TEST_FILES_PATH, `sample${fileNameSuffix}_raw.py`), 'utf8'); - const expectedCode = await fs.readFile( - path.join(TEST_FILES_PATH, `sample${fileNameSuffix}_normalized_selection.py`), - 'utf8', - ); - - await ensureCodeIsNormalized(code, expectedCode); + const pythonTestVersion = await getPythonSemVer(); + if (pythonTestVersion && pythonTestVersion.minor < 13) { + ['', '1', '2', '3', '4', '5', '6', '7', '8'].forEach((fileNameSuffix) => { + test(`Ensure code is normalized (Sample${fileNameSuffix}) - Python < 3.13`, async () => { + configurationService + .setup((c) => c.getSettings(TypeMoq.It.isAny())) + .returns({ + REPL: { + EnableREPLSmartSend: false, + REPLSmartSend: false, + }, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + } as any); + const code = await fs.readFile(path.join(TEST_FILES_PATH, `sample${fileNameSuffix}_raw.py`), 'utf8'); + const expectedCode = await fs.readFile( + path.join(TEST_FILES_PATH, `sample${fileNameSuffix}_normalized_selection.py`), + 'utf8', + ); + await ensureCodeIsNormalized(code, expectedCode); + }); }); - }); + } test("Display message if there's no active file", async () => { documentManager.setup((doc) => doc.activeTextEditor).returns(() => undefined); @@ -383,7 +499,7 @@ suite('Terminal - Code Execution Helper', () => { const untitledUri = Uri.file('Untitled-1'); document.setup((doc) => doc.uri).returns(() => untitledUri); const expectedSavedUri = Uri.file('one.py'); - workspaceService.setup((w) => w.saveAs(TypeMoq.It.isAny())).returns(() => Promise.resolve(expectedSavedUri)); + workspaceService.setup((w) => w.save(TypeMoq.It.isAny())).returns(() => Promise.resolve(expectedSavedUri)); const savedUri = await helper.saveFileIfDirty(untitledUri); diff --git a/src/test/terminals/codeExecution/smartSend.test.ts b/src/test/terminals/codeExecution/smartSend.test.ts new file mode 100644 index 000000000000..99ccd5d51d80 --- /dev/null +++ b/src/test/terminals/codeExecution/smartSend.test.ts @@ -0,0 +1,315 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import * as TypeMoq from 'typemoq'; +import * as path from 'path'; +import { TextEditor, Selection, Position, TextDocument, Uri } from 'vscode'; +import { SemVer } from 'semver'; +import { assert, expect } from 'chai'; +import * as fs from '../../../client/common/platform/fs-paths'; +import { + IActiveResourceService, + IApplicationShell, + ICommandManager, + IDocumentManager, +} from '../../../client/common/application/types'; +import { IProcessService, IProcessServiceFactory } from '../../../client/common/process/types'; +import { IInterpreterService } from '../../../client/interpreter/contracts'; +import { IConfigurationService, IExperimentService, IPythonSettings } from '../../../client/common/types'; +import { CodeExecutionHelper } from '../../../client/terminals/codeExecution/helper'; +import { IServiceContainer } from '../../../client/ioc/types'; +import { ICodeExecutionHelper } from '../../../client/terminals/types'; +import { Commands, EXTENSION_ROOT_DIR } from '../../../client/common/constants'; +import { EnvironmentType, PythonEnvironment } from '../../../client/pythonEnvironments/info'; +import { PYTHON_PATH, getPythonSemVer } from '../../common'; +import { Architecture } from '../../../client/common/utils/platform'; +import { ProcessService } from '../../../client/common/process/proc'; +import { l10n } from '../../mocks/vsc'; +import { ReplType } from '../../../client/repl/types'; + +const TEST_FILES_PATH = path.join(EXTENSION_ROOT_DIR, 'src', 'test', 'python_files', 'terminalExec'); + +suite('REPL - Smart Send', async () => { + let documentManager: TypeMoq.IMock; + let applicationShell: TypeMoq.IMock; + + let interpreterService: TypeMoq.IMock; + let commandManager: TypeMoq.IMock; + + let processServiceFactory: TypeMoq.IMock; + let configurationService: TypeMoq.IMock; + + let serviceContainer: TypeMoq.IMock; + let codeExecutionHelper: ICodeExecutionHelper; + let experimentService: TypeMoq.IMock; + + let processService: TypeMoq.IMock; + let activeResourceService: TypeMoq.IMock; + + let document: TypeMoq.IMock; + let pythonSettings: TypeMoq.IMock; + + const workingPython: PythonEnvironment = { + path: PYTHON_PATH, + version: new SemVer('3.6.6-final'), + sysVersion: '1.0.0.0', + sysPrefix: 'Python', + displayName: 'Python', + envType: EnvironmentType.Unknown, + architecture: Architecture.x64, + }; + + // suite set up only run once for each suite. Very start + // set up --- before each test + // tests -- actual tests + // tear down -- run after each test + // suite tear down only run once at the very end. + + // all object that is common to every test. What each test needs + setup(() => { + documentManager = TypeMoq.Mock.ofType(); + applicationShell = TypeMoq.Mock.ofType(); + processServiceFactory = TypeMoq.Mock.ofType(); + interpreterService = TypeMoq.Mock.ofType(); + commandManager = TypeMoq.Mock.ofType(); + configurationService = TypeMoq.Mock.ofType(); + serviceContainer = TypeMoq.Mock.ofType(); + experimentService = TypeMoq.Mock.ofType(); + processService = TypeMoq.Mock.ofType(); + activeResourceService = TypeMoq.Mock.ofType(); + pythonSettings = TypeMoq.Mock.ofType(); + const resource = Uri.parse('a'); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + processService.setup((x: any) => x.then).returns(() => undefined); + serviceContainer + .setup((c) => c.get(TypeMoq.It.isValue(IDocumentManager))) + .returns(() => documentManager.object); + serviceContainer + .setup((c) => c.get(TypeMoq.It.isValue(IApplicationShell))) + .returns(() => applicationShell.object); + processServiceFactory + .setup((p) => p.create(TypeMoq.It.isAny())) + .returns(() => Promise.resolve(processService.object)); + serviceContainer + .setup((c) => c.get(TypeMoq.It.isValue(IProcessServiceFactory))) + .returns(() => processServiceFactory.object); + serviceContainer + .setup((c) => c.get(TypeMoq.It.isValue(IInterpreterService))) + .returns(() => interpreterService.object); + serviceContainer.setup((c) => c.get(TypeMoq.It.isValue(ICommandManager))).returns(() => commandManager.object); + serviceContainer + .setup((c) => c.get(TypeMoq.It.isValue(IConfigurationService))) + .returns(() => configurationService.object); + serviceContainer + .setup((s) => s.get(TypeMoq.It.isValue(IExperimentService))) + .returns(() => experimentService.object); + interpreterService + .setup((i) => i.getActiveInterpreter(TypeMoq.It.isAny())) + .returns(() => Promise.resolve(workingPython)); + serviceContainer + .setup((c) => c.get(TypeMoq.It.isValue(IActiveResourceService))) + .returns(() => activeResourceService.object); + activeResourceService.setup((a) => a.getActiveResource()).returns(() => resource); + + pythonSettings + .setup((s) => s.REPL) + .returns(() => ({ + enableREPLSmartSend: true, + REPLSmartSend: true, + sendToNativeREPL: false, + })); + + configurationService.setup((x) => x.getSettings(TypeMoq.It.isAny())).returns(() => pythonSettings.object); + + codeExecutionHelper = new CodeExecutionHelper(serviceContainer.object); + document = TypeMoq.Mock.ofType(); + }); + + test('Cursor is not moved when explicit selection is present', async () => { + const activeEditor = TypeMoq.Mock.ofType(); + const firstIndexPosition = new Position(0, 0); + const selection = TypeMoq.Mock.ofType(); + const wholeFileContent = await fs.readFile(path.join(TEST_FILES_PATH, `sample_smart_selection.py`), 'utf8'); + + selection.setup((s) => s.anchor).returns(() => firstIndexPosition); + selection.setup((s) => s.active).returns(() => firstIndexPosition); + selection.setup((s) => s.isEmpty).returns(() => false); + activeEditor.setup((e) => e.selection).returns(() => selection.object); + + documentManager.setup((d) => d.activeTextEditor).returns(() => activeEditor.object); + document.setup((d) => d.getText(TypeMoq.It.isAny())).returns(() => wholeFileContent); + const actualProcessService = new ProcessService(); + + const { execObservable } = actualProcessService; + + processService + .setup((p) => p.execObservable(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny())) + .returns((file, args, options) => execObservable.apply(actualProcessService, [file, args, options])); + + await codeExecutionHelper.normalizeLines('my_dict = {', ReplType.terminal, wholeFileContent); + + commandManager + .setup((c) => c.executeCommand('cursorMove', TypeMoq.It.isAny())) + .callback((_, arg2) => { + assert.deepEqual(arg2, { + to: 'down', + by: 'line', + value: 3, + }); + return Promise.resolve(); + }) + .verifiable(TypeMoq.Times.never()); + + commandManager + .setup((c) => c.executeCommand('cursorEnd')) + .returns(() => Promise.resolve()) + .verifiable(TypeMoq.Times.never()); + + commandManager.verifyAll(); + }); + + const pythonTestVersion = await getPythonSemVer(); + + if (pythonTestVersion && pythonTestVersion.minor < 13) { + test('Smart send should perform smart selection and move cursor - Python < 3.13', async () => { + configurationService + .setup((c) => c.getSettings(TypeMoq.It.isAny())) + .returns({ + REPL: { + REPLSmartSend: true, + }, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + } as any); + + const activeEditor = TypeMoq.Mock.ofType(); + const firstIndexPosition = new Position(0, 0); + const selection = TypeMoq.Mock.ofType(); + const wholeFileContent = await fs.readFile(path.join(TEST_FILES_PATH, `sample_smart_selection.py`), 'utf8'); + + selection.setup((s) => s.anchor).returns(() => firstIndexPosition); + selection.setup((s) => s.active).returns(() => firstIndexPosition); + selection.setup((s) => s.isEmpty).returns(() => true); + activeEditor.setup((e) => e.selection).returns(() => selection.object); + + documentManager.setup((d) => d.activeTextEditor).returns(() => activeEditor.object); + document.setup((d) => d.getText(TypeMoq.It.isAny())).returns(() => wholeFileContent); + const actualProcessService = new ProcessService(); + + const { execObservable } = actualProcessService; + + processService + .setup((p) => p.execObservable(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny())) + .returns((file, args, options) => execObservable.apply(actualProcessService, [file, args, options])); + + const actualSmartOutput = await codeExecutionHelper.normalizeLines( + 'my_dict = {', + ReplType.terminal, + wholeFileContent, + ); + + // my_dict = { <----- smart shift+enter here + // "key1": "value1", + // "key2": "value2" + // } <---- cursor should be here afterwards, hence offset 3 + commandManager + .setup((c) => c.executeCommand('cursorMove', TypeMoq.It.isAny())) + .callback((_, arg2) => { + assert.deepEqual(arg2, { + to: 'down', + by: 'line', + value: 3, + }); + return Promise.resolve(); + }) + .verifiable(TypeMoq.Times.once()); + + commandManager + .setup((c) => c.executeCommand('cursorEnd')) + .returns(() => Promise.resolve()) + .verifiable(TypeMoq.Times.once()); + + const expectedSmartOutput = 'my_dict = {\n "key1": "value1",\n "key2": "value2"\n}\n'; + expect(actualSmartOutput).to.be.equal(expectedSmartOutput); + commandManager.verifyAll(); + }); + } + + // Do not perform smart selection when there is explicit selection + test('Smart send should not perform smart selection when there is explicit selection', async () => { + const activeEditor = TypeMoq.Mock.ofType(); + const firstIndexPosition = new Position(0, 0); + const selection = TypeMoq.Mock.ofType(); + const wholeFileContent = await fs.readFile(path.join(TEST_FILES_PATH, `sample_smart_selection.py`), 'utf8'); + + selection.setup((s) => s.anchor).returns(() => firstIndexPosition); + selection.setup((s) => s.active).returns(() => firstIndexPosition); + selection.setup((s) => s.isEmpty).returns(() => false); + activeEditor.setup((e) => e.selection).returns(() => selection.object); + + documentManager.setup((d) => d.activeTextEditor).returns(() => activeEditor.object); + document.setup((d) => d.getText(TypeMoq.It.isAny())).returns(() => wholeFileContent); + const actualProcessService = new ProcessService(); + + const { execObservable } = actualProcessService; + + processService + .setup((p) => p.execObservable(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny())) + .returns((file, args, options) => execObservable.apply(actualProcessService, [file, args, options])); + + const actualNonSmartResult = await codeExecutionHelper.normalizeLines( + 'my_dict = {', + ReplType.terminal, + wholeFileContent, + ); + const expectedNonSmartResult = 'my_dict = {\n\n'; // Standard for previous normalization logic + expect(actualNonSmartResult).to.be.equal(expectedNonSmartResult); + }); + + test('Smart Send should provide warning when code is not valid', async () => { + configurationService + .setup((c) => c.getSettings(TypeMoq.It.isAny())) + .returns({ + REPL: { + REPLSmartSend: true, + }, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + } as any); + + const activeEditor = TypeMoq.Mock.ofType(); + const firstIndexPosition = new Position(0, 0); + const selection = TypeMoq.Mock.ofType(); + const wholeFileContent = await fs.readFile( + path.join(TEST_FILES_PATH, `sample_invalid_smart_selection.py`), + 'utf8', + ); + + selection.setup((s) => s.anchor).returns(() => firstIndexPosition); + selection.setup((s) => s.active).returns(() => firstIndexPosition); + selection.setup((s) => s.isEmpty).returns(() => true); + activeEditor.setup((e) => e.selection).returns(() => selection.object); + + documentManager.setup((d) => d.activeTextEditor).returns(() => activeEditor.object); + document.setup((d) => d.getText(TypeMoq.It.isAny())).returns(() => wholeFileContent); + const actualProcessService = new ProcessService(); + + const { execObservable } = actualProcessService; + + processService + .setup((p) => p.execObservable(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny())) + .returns((file, args, options) => execObservable.apply(actualProcessService, [file, args, options])); + + await codeExecutionHelper.normalizeLines('my_dict = {', ReplType.terminal, wholeFileContent); + + applicationShell + .setup((a) => + a.showWarningMessage( + l10n.t( + 'Python is unable to parse the code provided. Please turn off Smart Send if you wish to always run line by line or explicitly select code to force run. [logs](command:{0}) for more details.', + Commands.ViewOutput, + ), + 'Switch to line-by-line', + ), + ) + .verifiable(TypeMoq.Times.once()); + }); +}); diff --git a/src/test/terminals/codeExecution/terminalCodeExec.unit.test.ts b/src/test/terminals/codeExecution/terminalCodeExec.unit.test.ts index 1f33b619fad0..b5bcecd971ea 100644 --- a/src/test/terminals/codeExecution/terminalCodeExec.unit.test.ts +++ b/src/test/terminals/codeExecution/terminalCodeExec.unit.test.ts @@ -6,7 +6,12 @@ import * as path from 'path'; import { SemVer } from 'semver'; import * as TypeMoq from 'typemoq'; import { Disposable, Uri, WorkspaceFolder } from 'vscode'; -import { ICommandManager, IDocumentManager, IWorkspaceService } from '../../../client/common/application/types'; +import { + IApplicationShell, + ICommandManager, + IDocumentManager, + IWorkspaceService, +} from '../../../client/common/application/types'; import { IFileSystem, IPlatformService } from '../../../client/common/platform/types'; import { createCondaEnv } from '../../../client/common/process/pythonEnvironment'; import { createPythonProcessService } from '../../../client/common/process/pythonProcess'; @@ -25,7 +30,7 @@ import { TerminalCodeExecutionProvider } from '../../../client/terminals/codeExe import { ICodeExecutionService } from '../../../client/terminals/types'; import { PYTHON_PATH } from '../../common'; import * as sinon from 'sinon'; -import assert from 'assert'; +import { assert } from 'chai'; import { PythonEnvironment } from '../../../client/pythonEnvironments/info'; import { IInterpreterService } from '../../../client/interpreter/contracts'; @@ -47,6 +52,7 @@ suite('Terminal - Code Execution', () => { let pythonExecutionFactory: TypeMoq.IMock; let interpreterService: TypeMoq.IMock; let isDjangoRepl: boolean; + let applicationShell: TypeMoq.IMock; teardown(() => { disposables.forEach((disposable) => { @@ -71,6 +77,7 @@ suite('Terminal - Code Execution', () => { fileSystem = TypeMoq.Mock.ofType(); pythonExecutionFactory = TypeMoq.Mock.ofType(); interpreterService = TypeMoq.Mock.ofType(); + applicationShell = TypeMoq.Mock.ofType(); settings = TypeMoq.Mock.ofType(); settings.setup((s) => s.terminal).returns(() => terminalSettings.object); configService.setup((c) => c.getSettings(TypeMoq.It.isAny())).returns(() => settings.object); @@ -84,6 +91,8 @@ suite('Terminal - Code Execution', () => { disposables, platform.object, interpreterService.object, + commandManager.object, + applicationShell.object, ); break; } @@ -95,6 +104,8 @@ suite('Terminal - Code Execution', () => { disposables, platform.object, interpreterService.object, + commandManager.object, + applicationShell.object, ); expectedTerminalTitle = 'REPL'; break; @@ -118,6 +129,7 @@ suite('Terminal - Code Execution', () => { fileSystem.object, disposables, interpreterService.object, + applicationShell.object, ); expectedTerminalTitle = 'Django Shell'; break; @@ -390,6 +402,7 @@ suite('Terminal - Code Execution', () => { const env = await createCondaEnv(condaEnv, procService.object, fileSystem.object); if (!env) { assert(false, 'Should not be undefined for conda version 4.9.0'); + return; } const procs = createPythonProcessService(procService.object, env); const condaExecutionService = { @@ -509,6 +522,7 @@ suite('Terminal - Code Execution', () => { const env = await createCondaEnv(condaEnv, procService.object, fileSystem.object); if (!env) { assert(false, 'Should not be undefined for conda version 4.9.0'); + return; } const procs = createPythonProcessService(procService.object, env); const condaExecutionService = { @@ -586,7 +600,7 @@ suite('Terminal - Code Execution', () => { ); }); - test('Ensure repl is re-initialized when terminal is closed', async () => { + test('Ensure REPL launches after reducing risk of command being ignored or duplicated', async () => { const pythonPath = 'usr/bin/python1234'; const terminalArgs = ['-a', 'b', 'c']; platform.setup((p) => p.isWindows).returns(() => false); @@ -595,43 +609,27 @@ suite('Terminal - Code Execution', () => { .returns(() => Promise.resolve(({ path: pythonPath } as unknown) as PythonEnvironment)); terminalSettings.setup((t) => t.launchArgs).returns(() => terminalArgs); - let closeTerminalCallback: undefined | (() => void); - terminalService - .setup((t) => t.onDidCloseTerminal(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny())) - .returns((callback) => { - closeTerminalCallback = callback; - return { - dispose: noop, - }; - }); - await executor.execute('cmd1'); await executor.execute('cmd2'); await executor.execute('cmd3'); - const expectedTerminalArgs = isDjangoRepl ? terminalArgs.concat(['manage.py', 'shell']) : terminalArgs; - - expect(closeTerminalCallback).not.to.be.an('undefined', 'Callback not initialized'); - terminalService.verify( - async (t) => - t.sendCommand(TypeMoq.It.isValue(pythonPath), TypeMoq.It.isValue(expectedTerminalArgs)), - TypeMoq.Times.once(), + // Now check if sendCommand from the initializeRepl is called atLeastOnce. + // This is due to newly added Promise race and fallback to lower risk of swollen first command. + applicationShell.verify( + async (t) => t.onDidWriteTerminalData(TypeMoq.It.isAny(), TypeMoq.It.isAny()), + TypeMoq.Times.atLeastOnce(), ); - closeTerminalCallback!.call(terminalService.object); await executor.execute('cmd4'); - terminalService.verify( - async (t) => - t.sendCommand(TypeMoq.It.isValue(pythonPath), TypeMoq.It.isValue(expectedTerminalArgs)), - TypeMoq.Times.exactly(2), + applicationShell.verify( + async (t) => t.onDidWriteTerminalData(TypeMoq.It.isAny(), TypeMoq.It.isAny()), + TypeMoq.Times.atLeastOnce(), ); - closeTerminalCallback!.call(terminalService.object); await executor.execute('cmd5'); - terminalService.verify( - async (t) => - t.sendCommand(TypeMoq.It.isValue(pythonPath), TypeMoq.It.isValue(expectedTerminalArgs)), - TypeMoq.Times.exactly(3), + applicationShell.verify( + async (t) => t.onDidWriteTerminalData(TypeMoq.It.isAny(), TypeMoq.It.isAny()), + TypeMoq.Times.atLeastOnce(), ); }); @@ -645,10 +643,35 @@ suite('Terminal - Code Execution', () => { terminalSettings.setup((t) => t.launchArgs).returns(() => terminalArgs); await executor.execute('cmd1'); - terminalService.verify(async (t) => t.sendText('cmd1'), TypeMoq.Times.once()); + terminalService.verify(async (t) => t.executeCommand('cmd1', true), TypeMoq.Times.once()); await executor.execute('cmd2'); - terminalService.verify(async (t) => t.sendText('cmd2'), TypeMoq.Times.once()); + terminalService.verify(async (t) => t.executeCommand('cmd2', true), TypeMoq.Times.once()); + }); + + test('Ensure code is sent to the same terminal for a particular resource', async () => { + const resource = Uri.file('a'); + terminalFactory.reset(); + terminalFactory + .setup((f) => f.getTerminalService(TypeMoq.It.isAny())) + .callback((options: TerminalCreationOptions) => { + assert.deepEqual(options.resource, resource); + }) + .returns(() => terminalService.object); + + const pythonPath = 'usr/bin/python1234'; + const terminalArgs = ['-a', 'b', 'c']; + platform.setup((p) => p.isWindows).returns(() => false); + interpreterService + .setup((s) => s.getActiveInterpreter(TypeMoq.It.isAny())) + .returns(() => Promise.resolve(({ path: pythonPath } as unknown) as PythonEnvironment)); + terminalSettings.setup((t) => t.launchArgs).returns(() => terminalArgs); + + await executor.execute('cmd1', resource); + terminalService.verify(async (t) => t.executeCommand('cmd1', true), TypeMoq.Times.once()); + + await executor.execute('cmd2', resource); + terminalService.verify(async (t) => t.executeCommand('cmd2', true), TypeMoq.Times.once()); }); }); }); diff --git a/src/test/terminals/serviceRegistry.unit.test.ts b/src/test/terminals/serviceRegistry.unit.test.ts index 38a9a9744e91..4f865cdedc0d 100644 --- a/src/test/terminals/serviceRegistry.unit.test.ts +++ b/src/test/terminals/serviceRegistry.unit.test.ts @@ -1,7 +1,9 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. import * as typemoq from 'typemoq'; +import { IExtensionActivationService, IExtensionSingleActivationService } from '../../client/activation/types'; import { IServiceManager } from '../../client/ioc/types'; import { TerminalAutoActivation } from '../../client/terminals/activation'; import { CodeExecutionManager } from '../../client/terminals/codeExecution/codeExecutionManager'; @@ -9,13 +11,20 @@ import { DjangoShellCodeExecutionProvider } from '../../client/terminals/codeExe import { CodeExecutionHelper } from '../../client/terminals/codeExecution/helper'; import { ReplProvider } from '../../client/terminals/codeExecution/repl'; import { TerminalCodeExecutionProvider } from '../../client/terminals/codeExecution/terminalCodeExecution'; +import { TerminalDeactivateService } from '../../client/terminals/envCollectionActivation/deactivateService'; +import { TerminalIndicatorPrompt } from '../../client/terminals/envCollectionActivation/indicatorPrompt'; +import { TerminalEnvVarCollectionService } from '../../client/terminals/envCollectionActivation/service'; import { registerTypes } from '../../client/terminals/serviceRegistry'; import { ICodeExecutionHelper, ICodeExecutionManager, ICodeExecutionService, + IShellIntegrationDetectionService, ITerminalAutoActivation, + ITerminalDeactivateService, + ITerminalEnvVarCollectionService, } from '../../client/terminals/types'; +import { ShellIntegrationDetectionService } from '../../client/terminals/envCollectionActivation/shellIntegrationService'; suite('Terminal - Service Registry', () => { test('Ensure all services get registered', () => { @@ -27,13 +36,17 @@ suite('Terminal - Service Registry', () => { [ICodeExecutionService, ReplProvider, 'repl'], [ITerminalAutoActivation, TerminalAutoActivation], [ICodeExecutionService, TerminalCodeExecutionProvider, 'standard'], + [ITerminalEnvVarCollectionService, TerminalEnvVarCollectionService], + [IExtensionSingleActivationService, TerminalIndicatorPrompt], + [ITerminalDeactivateService, TerminalDeactivateService], + [IShellIntegrationDetectionService, ShellIntegrationDetectionService], ].forEach((args) => { if (args.length === 2) { services .setup((s) => s.addSingleton( - typemoq.It.is((v) => args[0] === v), - typemoq.It.is((value) => args[1] === value), + typemoq.It.is((v: any) => args[0] === v), + typemoq.It.is((value: any) => args[1] === value), ), ) .verifiable(typemoq.Times.once()); @@ -41,8 +54,8 @@ suite('Terminal - Service Registry', () => { services .setup((s) => s.addSingleton( - typemoq.It.is((v) => args[0] === v), - typemoq.It.is((value) => args[1] === value), + typemoq.It.is((v: any) => args[0] === v), + typemoq.It.is((value: any) => args[1] === value), typemoq.It.isValue((args[2] as unknown) as string), ), @@ -50,6 +63,14 @@ suite('Terminal - Service Registry', () => { .verifiable(typemoq.Times.once()); } }); + services + .setup((s) => + s.addBinding( + typemoq.It.is((v: any) => ITerminalEnvVarCollectionService === v), + typemoq.It.is((value: any) => IExtensionActivationService === value), + ), + ) + .verifiable(typemoq.Times.once()); registerTypes(services.object); diff --git a/src/test/terminals/shellIntegration/pythonStartup.test.ts b/src/test/terminals/shellIntegration/pythonStartup.test.ts new file mode 100644 index 000000000000..833a4f29e972 --- /dev/null +++ b/src/test/terminals/shellIntegration/pythonStartup.test.ts @@ -0,0 +1,272 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import * as sinon from 'sinon'; +import * as TypeMoq from 'typemoq'; +import { + GlobalEnvironmentVariableCollection, + Uri, + WorkspaceConfiguration, + Disposable, + CancellationToken, + TerminalLinkContext, + Terminal, + EventEmitter, + workspace, +} from 'vscode'; +import { assert } from 'chai'; +import * as workspaceApis from '../../../client/common/vscodeApis/workspaceApis'; +import { registerPythonStartup } from '../../../client/terminals/pythonStartup'; +import { IExtensionContext } from '../../../client/common/types'; +import * as pythonStartupLinkProvider from '../../../client/terminals/pythonStartupLinkProvider'; +import { CustomTerminalLinkProvider } from '../../../client/terminals/pythonStartupLinkProvider'; +import { Repl } from '../../../client/common/utils/localize'; + +suite('Terminal - Shell Integration with PYTHONSTARTUP', () => { + let getConfigurationStub: sinon.SinonStub; + let pythonConfig: TypeMoq.IMock; + let editorConfig: TypeMoq.IMock; + let context: TypeMoq.IMock; + let createDirectoryStub: sinon.SinonStub; + let copyStub: sinon.SinonStub; + let globalEnvironmentVariableCollection: TypeMoq.IMock; + + setup(() => { + context = TypeMoq.Mock.ofType(); + globalEnvironmentVariableCollection = TypeMoq.Mock.ofType(); + context.setup((c) => c.environmentVariableCollection).returns(() => globalEnvironmentVariableCollection.object); + context.setup((c) => c.storageUri).returns(() => Uri.parse('a')); + context.setup((c) => c.subscriptions).returns(() => []); + + globalEnvironmentVariableCollection + .setup((c) => c.replace(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny())) + .returns(() => Promise.resolve()); + + globalEnvironmentVariableCollection.setup((c) => c.delete(TypeMoq.It.isAny())).returns(() => Promise.resolve()); + + getConfigurationStub = sinon.stub(workspaceApis, 'getConfiguration'); + createDirectoryStub = sinon.stub(workspaceApis, 'createDirectory'); + copyStub = sinon.stub(workspaceApis, 'copy'); + + pythonConfig = TypeMoq.Mock.ofType(); + editorConfig = TypeMoq.Mock.ofType(); + getConfigurationStub.callsFake((section: string) => { + if (section === 'python') { + return pythonConfig.object; + } + return editorConfig.object; + }); + + createDirectoryStub.callsFake((_) => Promise.resolve()); + copyStub.callsFake((_, __, ___) => Promise.resolve()); + }); + + teardown(() => { + sinon.restore(); + }); + + test('Verify createDirectory is called when shell integration is enabled', async () => { + pythonConfig.setup((p) => p.get('terminal.shellIntegration.enabled')).returns(() => true); + + await registerPythonStartup(context.object); + + sinon.assert.calledOnce(createDirectoryStub); + }); + + test('Verify createDirectory is not called when shell integration is disabled', async () => { + pythonConfig.setup((p) => p.get('terminal.shellIntegration.enabled')).returns(() => false); + + await registerPythonStartup(context.object); + + sinon.assert.notCalled(createDirectoryStub); + }); + + test('Verify copy is called when shell integration is enabled', async () => { + pythonConfig.setup((p) => p.get('terminal.shellIntegration.enabled')).returns(() => true); + + await registerPythonStartup(context.object); + + sinon.assert.calledOnce(copyStub); + }); + + test('Verify copy is not called when shell integration is disabled', async () => { + pythonConfig.setup((p) => p.get('terminal.shellIntegration.enabled')).returns(() => false); + + await registerPythonStartup(context.object); + + sinon.assert.notCalled(copyStub); + }); + + test('PYTHONSTARTUP is set when enableShellIntegration setting is true', async () => { + pythonConfig.setup((p) => p.get('terminal.shellIntegration.enabled')).returns(() => true); + + await registerPythonStartup(context.object); + + globalEnvironmentVariableCollection.verify( + (c) => c.replace('PYTHONSTARTUP', TypeMoq.It.isAny(), TypeMoq.It.isAny()), + TypeMoq.Times.once(), + ); + }); + + test('environmentCollection should not remove PYTHONSTARTUP when enableShellIntegration setting is true', async () => { + pythonConfig.setup((p) => p.get('terminal.shellIntegration.enabled')).returns(() => true); + + await registerPythonStartup(context.object); + + globalEnvironmentVariableCollection.verify((c) => c.delete('PYTHONSTARTUP'), TypeMoq.Times.never()); + }); + + test('PYTHONSTARTUP is not set when enableShellIntegration setting is false', async () => { + pythonConfig.setup((p) => p.get('terminal.shellIntegration.enabled')).returns(() => false); + + await registerPythonStartup(context.object); + + globalEnvironmentVariableCollection.verify( + (c) => c.replace('PYTHONSTARTUP', TypeMoq.It.isAny(), TypeMoq.It.isAny()), + TypeMoq.Times.never(), + ); + }); + + test('PYTHONSTARTUP is deleted when enableShellIntegration setting is false', async () => { + pythonConfig.setup((p) => p.get('terminal.shellIntegration.enabled')).returns(() => false); + + await registerPythonStartup(context.object); + + globalEnvironmentVariableCollection.verify((c) => c.delete('PYTHONSTARTUP'), TypeMoq.Times.once()); + }); + + test('PYTHON_BASIC_REPL is set when shell integration is enabled', async () => { + pythonConfig.setup((p) => p.get('terminal.shellIntegration.enabled')).returns(() => true); + await registerPythonStartup(context.object); + globalEnvironmentVariableCollection.verify( + (c) => c.replace('PYTHON_BASIC_REPL', '1', TypeMoq.It.isAny()), + TypeMoq.Times.once(), + ); + }); + + test('Ensure registering terminal link calls registerTerminalLinkProvider', async () => { + const registerTerminalLinkProviderStub = sinon.stub( + pythonStartupLinkProvider, + 'registerCustomTerminalLinkProvider', + ); + const disposableArray: Disposable[] = []; + pythonStartupLinkProvider.registerCustomTerminalLinkProvider(disposableArray); + + sinon.assert.calledOnce(registerTerminalLinkProviderStub); + sinon.assert.calledWith(registerTerminalLinkProviderStub, disposableArray); + + registerTerminalLinkProviderStub.restore(); + }); + + test('Verify onDidChangeConfiguration is called when configuration changes', async () => { + const onDidChangeConfigurationSpy = sinon.spy(workspace, 'onDidChangeConfiguration'); + pythonConfig.setup((p) => p.get('terminal.shellIntegration.enabled')).returns(() => true); + + await registerPythonStartup(context.object); + + assert.isTrue(onDidChangeConfigurationSpy.calledOnce); + onDidChangeConfigurationSpy.restore(); + }); + + if (process.platform === 'darwin') { + test('Mac - Verify provideTerminalLinks returns links when context.line contains expectedNativeLink', () => { + const provider = new CustomTerminalLinkProvider(); + const context: TerminalLinkContext = { + line: 'Some random string with Cmd click to launch VS Code Native REPL', + terminal: {} as Terminal, + }; + const token: CancellationToken = { + isCancellationRequested: false, + onCancellationRequested: new EventEmitter().event, + }; + + const links = provider.provideTerminalLinks(context, token); + + assert.isNotNull(links, 'Expected links to be not undefined'); + assert.isArray(links, 'Expected links to be an array'); + assert.isNotEmpty(links, 'Expected links to be not empty'); + + if (Array.isArray(links)) { + assert.equal( + links[0].command, + 'python.startNativeREPL', + 'Expected command to be python.startNativeREPL', + ); + assert.equal( + links[0].startIndex, + context.line.indexOf('Cmd click to launch VS Code Native REPL'), + 'start index should match', + ); + assert.equal( + links[0].length, + 'Cmd click to launch VS Code Native REPL'.length, + 'Match expected length', + ); + assert.equal( + links[0].tooltip, + Repl.launchNativeRepl, + 'Expected tooltip to be Launch VS Code Native REPL', + ); + } + }); + } + if (process.platform !== 'darwin') { + test('Windows/Linux - Verify provideTerminalLinks returns links when context.line contains expectedNativeLink', () => { + const provider = new CustomTerminalLinkProvider(); + const context: TerminalLinkContext = { + line: 'Some random string with Ctrl click to launch VS Code Native REPL', + terminal: {} as Terminal, + }; + const token: CancellationToken = { + isCancellationRequested: false, + onCancellationRequested: new EventEmitter().event, + }; + + const links = provider.provideTerminalLinks(context, token); + + assert.isNotNull(links, 'Expected links to be not undefined'); + assert.isArray(links, 'Expected links to be an array'); + assert.isNotEmpty(links, 'Expected links to be not empty'); + + if (Array.isArray(links)) { + assert.equal( + links[0].command, + 'python.startNativeREPL', + 'Expected command to be python.startNativeREPL', + ); + assert.equal( + links[0].startIndex, + context.line.indexOf('Ctrl click to launch VS Code Native REPL'), + 'start index should match', + ); + assert.equal( + links[0].length, + 'Ctrl click to launch VS Code Native REPL'.length, + 'Match expected Length', + ); + assert.equal( + links[0].tooltip, + Repl.launchNativeRepl, + 'Expected tooltip to be Launch VS Code Native REPL', + ); + } + }); + } + + test('Verify provideTerminalLinks returns no links when context.line does not contain expectedNativeLink', () => { + const provider = new CustomTerminalLinkProvider(); + const context: TerminalLinkContext = { + line: 'Some random string without the expected link', + terminal: {} as Terminal, + }; + const token: CancellationToken = { + isCancellationRequested: false, + onCancellationRequested: new EventEmitter().event, + }; + + const links = provider.provideTerminalLinks(context, token); + + assert.isArray(links, 'Expected links to be an array'); + assert.isEmpty(links, 'Expected links to be empty'); + }); +}); diff --git a/src/test/testBootstrap.ts b/src/test/testBootstrap.ts index 03f24a680d0d..ab902255203b 100644 --- a/src/test/testBootstrap.ts +++ b/src/test/testBootstrap.ts @@ -4,7 +4,7 @@ 'use strict'; import { ChildProcess, spawn, SpawnOptions } from 'child_process'; -import * as fs from 'fs-extra'; +import * as fs from '../client/common/platform/fs-paths'; import { AddressInfo, createServer, Server } from 'net'; import * as path from 'path'; import { EXTENSION_ROOT_DIR } from '../client/constants'; diff --git a/src/test/testRunner.ts b/src/test/testRunner.ts index 9dd9ead56e58..6187597a46a3 100644 --- a/src/test/testRunner.ts +++ b/src/test/testRunner.ts @@ -19,7 +19,7 @@ if (!tty.getWindowSize) { }; } -let mocha = new Mocha({ +let mocha = new Mocha.default({ ui: 'tdd', colors: true, }); @@ -40,7 +40,7 @@ export function configure(setupOptions: SetupOptions): void { } // Force Mocha to exit. (setupOptions as any).exit = true; - mocha = new Mocha(setupOptions); + mocha = new Mocha.default(setupOptions); } export async function run(): Promise { @@ -59,7 +59,7 @@ export async function run(): Promise { */ function initializationScript() { const ex = new Error('Failed to initialize Python extension for tests after 3 minutes'); - let timer: NodeJS.Timer | undefined; + let timer: NodeJS.Timeout | undefined; const failed = new Promise((_, reject) => { timer = setTimeout(() => reject(ex), MAX_EXTENSION_ACTIVATION_TIME); }); @@ -69,7 +69,7 @@ export async function run(): Promise { } // Run the tests. await new Promise((resolve, reject) => { - glob( + glob.default( `**/**.${testFilesGlob}.js`, { ignore: ['**/**.unit.test.js', '**/**.functional.test.js'], cwd: testsRoot }, (error, files) => { diff --git a/src/test/testing/common/debugLauncher.unit.test.ts b/src/test/testing/common/debugLauncher.unit.test.ts index b8b7d5c55130..86e862103bf6 100644 --- a/src/test/testing/common/debugLauncher.unit.test.ts +++ b/src/test/testing/common/debugLauncher.unit.test.ts @@ -8,15 +8,15 @@ import * as chaiAsPromised from 'chai-as-promised'; import * as path from 'path'; import * as sinon from 'sinon'; import * as TypeMoq from 'typemoq'; -import * as fs from 'fs-extra'; +import * as fs from '../../../client/common/platform/fs-paths'; import * as workspaceApis from '../../../client/common/vscodeApis/workspaceApis'; -import { CancellationTokenSource, DebugConfiguration, Uri, WorkspaceFolder } from 'vscode'; +import { CancellationTokenSource, DebugConfiguration, DebugSession, Uri, WorkspaceFolder } from 'vscode'; import { IInvalidPythonPathInDebuggerService } from '../../../client/application/diagnostics/types'; import { IApplicationShell, IDebugService } from '../../../client/common/application/types'; import { EXTENSION_ROOT_DIR } from '../../../client/common/constants'; import '../../../client/common/extensions'; import { IConfigurationService, IPythonSettings } from '../../../client/common/types'; -import { DebuggerTypeName } from '../../../client/debugger/constants'; +import { PythonDebuggerTypeName } from '../../../client/debugger/constants'; import { IDebugEnvironmentVariablesService } from '../../../client/debugger/extension/configuration/resolvers/helper'; import { LaunchConfigurationResolver } from '../../../client/debugger/extension/configuration/resolvers/launch'; import { DebugOptions } from '../../../client/debugger/types'; @@ -29,8 +29,10 @@ import { ITestingSettings } from '../../../client/testing/configuration/types'; import { TestProvider } from '../../../client/testing/types'; import { isOs, OSType } from '../../common'; import { IEnvironmentActivationService } from '../../../client/interpreter/activation/types'; +import { createDeferred } from '../../../client/common/utils/async'; +import * as envExtApi from '../../../client/envExt/api.internal'; -use(chaiAsPromised); +use(chaiAsPromised.default); suite('Unit Tests - Debug Launcher', () => { let serviceContainer: TypeMoq.IMock; @@ -73,7 +75,7 @@ suite('Unit Tests - Debug Launcher', () => { settings = TypeMoq.Mock.ofType(undefined, TypeMoq.MockBehavior.Strict); configService.setup((c) => c.getSettings(TypeMoq.It.isAny())).returns(() => settings.object); - unitTestSettings = TypeMoq.Mock.ofType(undefined, TypeMoq.MockBehavior.Strict); + unitTestSettings = TypeMoq.Mock.ofType(); settings.setup((p) => p.testing).returns(() => unitTestSettings.object); debugEnvHelper = TypeMoq.Mock.ofType(undefined, TypeMoq.MockBehavior.Strict); @@ -105,7 +107,7 @@ suite('Unit Tests - Debug Launcher', () => { ); } function setupDebugManager( - workspaceFolder: WorkspaceFolder, + _workspaceFolder: WorkspaceFolder, expected: DebugConfiguration, testProvider: TestProvider, ) { @@ -121,20 +123,49 @@ suite('Unit Tests - Debug Launcher', () => { .setup((x) => x.getEnvironmentVariables(TypeMoq.It.isAny(), TypeMoq.It.isAny())) .returns(() => Promise.resolve(expected.env)); + const deferred = createDeferred(); + let capturedConfig: DebugConfiguration | undefined; + + // Use TypeMoq.It.isAny() because the implementation adds a session marker to the config debugService - .setup((d) => d.startDebugging(TypeMoq.It.isValue(workspaceFolder), TypeMoq.It.isValue(expected))) - .returns((_wspc: WorkspaceFolder, _expectedParam: DebugConfiguration) => { - return Promise.resolve(undefined as any); + .setup((d) => d.startDebugging(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny())) + .callback((_wspc: WorkspaceFolder, config: DebugConfiguration) => { + capturedConfig = config; + deferred.resolve(); }) - .verifiable(TypeMoq.Times.once()); + .returns(() => Promise.resolve(true)); + + // Setup onDidStartDebugSession - the new implementation uses this to capture the session + debugService + .setup((d) => d.onDidStartDebugSession(TypeMoq.It.isAny())) + .returns((callback) => { + deferred.promise.then(() => { + if (capturedConfig) { + callback(({ + id: 'test-session-id', + configuration: capturedConfig, + } as unknown) as DebugSession); + } + }); + return { dispose: () => {} }; + }); + // Setup onDidTerminateDebugSession - fires after the session starts debugService .setup((d) => d.onDidTerminateDebugSession(TypeMoq.It.isAny())) .returns((callback) => { - callback(); - return undefined as any; - }) - .verifiable(TypeMoq.Times.once()); + deferred.promise.then(() => { + setTimeout(() => { + if (capturedConfig) { + callback(({ + id: 'test-session-id', + configuration: capturedConfig, + } as unknown) as DebugSession); + } + }, 10); + }); + return { dispose: () => {} }; + }); } function createWorkspaceFolder(folderPath: string): WorkspaceFolder { return { @@ -143,23 +174,26 @@ suite('Unit Tests - Debug Launcher', () => { uri: Uri.file(folderPath), }; } - function getTestLauncherScript(testProvider: TestProvider) { - switch (testProvider) { - case 'unittest': { - return path.join(EXTENSION_ROOT_DIR, 'pythonFiles', 'visualstudio_py_testlauncher.py'); - } - case 'pytest': { - return path.join(EXTENSION_ROOT_DIR, 'pythonFiles', 'testlauncher.py'); - } - default: { - throw new Error(`Unknown test provider '${testProvider}'`); + function getTestLauncherScript(testProvider: TestProvider, pythonTestAdapterRewriteExperiment?: boolean) { + if (!pythonTestAdapterRewriteExperiment) { + switch (testProvider) { + case 'unittest': { + return path.join(EXTENSION_ROOT_DIR, 'python_files', 'unittestadapter', 'execution.py'); + } + case 'pytest': { + return path.join(EXTENSION_ROOT_DIR, 'python_files', 'vscode_pytest', 'run_pytest_script.py'); + } + default: { + throw new Error(`Unknown test provider '${testProvider}'`); + } } } } + function getDefaultDebugConfig(): DebugConfiguration { return { name: 'Debug Unit Test', - type: DebuggerTypeName, + type: PythonDebuggerTypeName, request: 'launch', console: 'internalConsole', env: {}, @@ -178,7 +212,7 @@ suite('Unit Tests - Debug Launcher', () => { expected?: DebugConfiguration, debugConfigs?: string | DebugConfiguration[], ) { - const testLaunchScript = getTestLauncherScript(testProvider); + const testLaunchScript = getTestLauncherScript(testProvider, false); const workspaceFolders = [createWorkspaceFolder(options.cwd), createWorkspaceFolder('five/six/seven')]; getWorkspaceFoldersStub.returns(workspaceFolders); @@ -201,13 +235,18 @@ suite('Unit Tests - Debug Launcher', () => { if (!expected) { expected = getDefaultDebugConfig(); } - expected.rules = [{ path: path.join(EXTENSION_ROOT_DIR, 'pythonFiles'), include: false }]; + expected.rules = [{ path: path.join(EXTENSION_ROOT_DIR, 'python_files'), include: false }]; expected.program = testLaunchScript; expected.args = options.args; if (!expected.cwd) { expected.cwd = workspaceFolders[0].uri.fsPath; } + const pluginPath = path.join(EXTENSION_ROOT_DIR, 'python_files'); + const pythonPath = `${pluginPath}${path.delimiter}${expected.cwd}`; + expected.env.PYTHONPATH = pythonPath; + expected.env.TEST_RUN_PIPE = 'pytestPort'; + expected.env.RUN_TEST_IDS_PIPE = 'runTestIdsPort'; // added by LaunchConfigurationResolver: if (!expected.python) { @@ -224,13 +263,6 @@ suite('Unit Tests - Debug Launcher', () => { } expected.workspaceFolder = workspaceFolders[0].uri.fsPath; expected.debugOptions = []; - if (expected.justMyCode === undefined) { - // Populate justMyCode using debugStdLib - expected.justMyCode = !expected.debugStdLib; - } - if (!expected.justMyCode) { - expected.debugOptions.push(DebugOptions.DebugStdLib); - } if (expected.stopOnEntry) { expected.debugOptions.push(DebugOptions.StopOnEntry); } @@ -260,18 +292,26 @@ suite('Unit Tests - Debug Launcher', () => { cwd: 'one/two/three', args: ['/one/two/three/testfile.py'], testProvider, + runTestIdsPort: 'runTestIdsPort', + pytestPort: 'pytestPort', }; setupSuccess(options, testProvider); await debugLauncher.launchDebugger(options); - debugService.verifyAll(); + try { + debugService.verifyAll(); + } catch (ex) { + console.log(ex); + } }); test(`Must launch debugger with arguments ${testTitleSuffix}`, async () => { const options = { cwd: 'one/two/three', args: ['/one/two/three/testfile.py', '--debug', '1'], testProvider, + runTestIdsPort: 'runTestIdsPort', + pytestPort: 'pytestPort', }; setupSuccess(options, testProvider); @@ -281,7 +321,7 @@ suite('Unit Tests - Debug Launcher', () => { }); test(`Must not launch debugger if cancelled ${testTitleSuffix}`, async () => { debugService - .setup((d) => d.startDebugging(TypeMoq.It.isAny(), TypeMoq.It.isAny())) + .setup((d) => d.startDebugging(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny())) .returns(() => { return Promise.resolve(undefined as any); }) @@ -290,7 +330,14 @@ suite('Unit Tests - Debug Launcher', () => { const cancellationToken = new CancellationTokenSource(); cancellationToken.cancel(); const token = cancellationToken.token; - const options: LaunchOptions = { cwd: '', args: [], token, testProvider }; + const options: LaunchOptions = { + cwd: '', + args: [], + token, + testProvider, + runTestIdsPort: 'runTestIdsPort', + pytestPort: 'pytestPort', + }; await expect(debugLauncher.launchDebugger(options)).to.be.eventually.equal(undefined, 'not undefined'); @@ -300,10 +347,19 @@ suite('Unit Tests - Debug Launcher', () => { getWorkspaceFoldersStub.returns(undefined); debugService .setup((d) => d.startDebugging(TypeMoq.It.isAny(), TypeMoq.It.isAny())) - .returns(() => Promise.resolve(undefined as any)) + .returns(() => { + console.log('Debugging should not start'); + return Promise.resolve(undefined as any); + }) .verifiable(TypeMoq.Times.never()); - const options: LaunchOptions = { cwd: '', args: [], testProvider }; + const options: LaunchOptions = { + cwd: '', + args: [], + testProvider, + runTestIdsPort: 'runTestIdsPort', + pytestPort: 'pytestPort', + }; await expect(debugLauncher.launchDebugger(options)).to.eventually.rejectedWith('Please open a workspace'); @@ -316,11 +372,34 @@ suite('Unit Tests - Debug Launcher', () => { cwd: 'one/two/three', args: ['/one/two/three/testfile.py'], testProvider: 'unittest', + runTestIdsPort: 'runTestIdsPort', + pytestPort: 'pytestPort', }; const expected = getDefaultDebugConfig(); expected.name = 'spam'; - setupSuccess(options, 'unittest', expected, [{ name: 'spam', type: DebuggerTypeName, request: 'test' }]); + setupSuccess(options, 'unittest', expected, [{ name: 'spam', type: PythonDebuggerTypeName, request: 'test' }]); + + await debugLauncher.launchDebugger(options); + + debugService.verifyAll(); + }); + + test('Use cwd value in settings if exist', async () => { + unitTestSettings.setup((p) => p.cwd).returns(() => 'path/to/settings/cwd'); + const options: LaunchOptions = { + cwd: 'one/two/three', + args: ['/one/two/three/testfile.py'], + testProvider: 'unittest', + runTestIdsPort: 'runTestIdsPort', + pytestPort: 'pytestPort', + }; + const expected = getDefaultDebugConfig(); + expected.cwd = 'path/to/settings/cwd'; + const pluginPath = path.join(EXTENSION_ROOT_DIR, 'python_files'); + const pythonPath = `${pluginPath}${path.delimiter}${expected.cwd}`; + expected.env.PYTHONPATH = pythonPath; + setupSuccess(options, 'unittest', expected); await debugLauncher.launchDebugger(options); debugService.verifyAll(); @@ -331,10 +410,12 @@ suite('Unit Tests - Debug Launcher', () => { cwd: 'one/two/three', args: ['/one/two/three/testfile.py'], testProvider: 'unittest', + runTestIdsPort: 'runTestIdsPort', + pytestPort: 'pytestPort', }; const expected = { name: 'my tests', - type: DebuggerTypeName, + type: PythonDebuggerTypeName, request: 'launch', python: 'some/dir/bin/py3', debugAdapterPython: 'some/dir/bin/py3', @@ -344,12 +425,14 @@ suite('Unit Tests - Debug Launcher', () => { console: 'integratedTerminal', cwd: 'some/dir', env: { + PYTHONPATH: 'one/two/three', SPAM: 'EGGS', + TEST_RUN_PIPE: 'pytestPort', + RUN_TEST_IDS_PIPE: 'runTestIdsPort', }, envFile: 'some/dir/.env', redirectOutput: false, debugStdLib: true, - justMyCode: false, // added by LaunchConfigurationResolver: internalConsoleOptions: 'neverOpen', subProcess: true, @@ -358,7 +441,7 @@ suite('Unit Tests - Debug Launcher', () => { setupSuccess(options, 'unittest', expected, [ { name: 'my tests', - type: DebuggerTypeName, + type: PythonDebuggerTypeName, request: 'test', pythonPath: expected.python, stopOnEntry: expected.stopOnEntry, @@ -369,7 +452,6 @@ suite('Unit Tests - Debug Launcher', () => { envFile: expected.envFile, redirectOutput: expected.redirectOutput, debugStdLib: expected.debugStdLib, - justMyCode: undefined, }, ]); @@ -383,13 +465,15 @@ suite('Unit Tests - Debug Launcher', () => { cwd: 'one/two/three', args: ['/one/two/three/testfile.py'], testProvider: 'unittest', + runTestIdsPort: 'runTestIdsPort', + pytestPort: 'pytestPort', }; const expected = getDefaultDebugConfig(); expected.name = 'spam1'; setupSuccess(options, 'unittest', expected, [ - { name: 'spam1', type: DebuggerTypeName, request: 'test' }, - { name: 'spam2', type: DebuggerTypeName, request: 'test' }, - { name: 'spam3', type: DebuggerTypeName, request: 'test' }, + { name: 'spam1', type: PythonDebuggerTypeName, request: 'test' }, + { name: 'spam2', type: PythonDebuggerTypeName, request: 'test' }, + { name: 'spam3', type: PythonDebuggerTypeName, request: 'test' }, ]); await debugLauncher.launchDebugger(options); @@ -402,6 +486,8 @@ suite('Unit Tests - Debug Launcher', () => { cwd: 'one/two/three', args: ['/one/two/three/testfile.py'], testProvider: 'unittest', + runTestIdsPort: 'runTestIdsPort', + pytestPort: 'pytestPort', }; const expected = getDefaultDebugConfig(); setupSuccess(options, 'unittest', expected, ']'); @@ -416,7 +502,7 @@ suite('Unit Tests - Debug Launcher', () => { '// test 2 \n\ { \n\ "name": "spam", \n\ - "type": "python", \n\ + "type": "debugpy", \n\ "request": "test" \n\ } \n\ ', @@ -424,7 +510,7 @@ suite('Unit Tests - Debug Launcher', () => { [ \n\ { \n\ "name": "spam", \n\ - "type": "python", \n\ + "type": "debugpy", \n\ "request": "test" \n\ } \n\ ] \n\ @@ -434,7 +520,7 @@ suite('Unit Tests - Debug Launcher', () => { "configurations": [ \n\ { \n\ "name": "spam", \n\ - "type": "python", \n\ + "type": "debugpy", \n\ "request": "test" \n\ } \n\ ] \n\ @@ -448,6 +534,8 @@ suite('Unit Tests - Debug Launcher', () => { cwd: 'one/two/three', args: ['/one/two/three/testfile.py'], testProvider: 'unittest', + runTestIdsPort: 'runTestIdsPort', + pytestPort: 'pytestPort', }; const expected = getDefaultDebugConfig(); setupSuccess(options, 'unittest', expected, text); @@ -463,16 +551,18 @@ suite('Unit Tests - Debug Launcher', () => { cwd: 'one/two/three', args: ['/one/two/three/testfile.py'], testProvider: 'unittest', + runTestIdsPort: 'runTestIdsPort', + pytestPort: 'pytestPort', }; const expected = getDefaultDebugConfig(); setupSuccess(options, 'unittest', expected, [ {} as DebugConfiguration, { name: 'spam1' } as DebugConfiguration, - { name: 'spam2', type: DebuggerTypeName } as DebugConfiguration, + { name: 'spam2', type: PythonDebuggerTypeName } as DebugConfiguration, { name: 'spam3', request: 'test' } as DebugConfiguration, - { type: DebuggerTypeName } as DebugConfiguration, - { type: DebuggerTypeName, request: 'test' } as DebugConfiguration, + { type: PythonDebuggerTypeName } as DebugConfiguration, + { type: PythonDebuggerTypeName, request: 'test' } as DebugConfiguration, { request: 'test' } as DebugConfiguration, ]); @@ -486,6 +576,8 @@ suite('Unit Tests - Debug Launcher', () => { cwd: 'one/two/three', args: ['/one/two/three/testfile.py'], testProvider: 'unittest', + runTestIdsPort: 'runTestIdsPort', + pytestPort: 'pytestPort', }; const expected = getDefaultDebugConfig(); setupSuccess(options, 'unittest', expected, [{ name: 'foo', type: 'other', request: 'bar' }]); @@ -500,9 +592,11 @@ suite('Unit Tests - Debug Launcher', () => { cwd: 'one/two/three', args: ['/one/two/three/testfile.py'], testProvider: 'unittest', + runTestIdsPort: 'runTestIdsPort', + pytestPort: 'pytestPort', }; const expected = getDefaultDebugConfig(); - setupSuccess(options, 'unittest', expected, [{ name: 'spam', type: DebuggerTypeName, request: 'bogus' }]); + setupSuccess(options, 'unittest', expected, [{ name: 'spam', type: PythonDebuggerTypeName, request: 'bogus' }]); await debugLauncher.launchDebugger(options); @@ -514,11 +608,13 @@ suite('Unit Tests - Debug Launcher', () => { cwd: 'one/two/three', args: ['/one/two/three/testfile.py'], testProvider: 'unittest', + runTestIdsPort: 'runTestIdsPort', + pytestPort: 'pytestPort', }; const expected = getDefaultDebugConfig(); setupSuccess(options, 'unittest', expected, [ - { name: 'spam', type: DebuggerTypeName, request: 'launch' }, - { name: 'spam', type: DebuggerTypeName, request: 'attach' }, + { name: 'spam', type: PythonDebuggerTypeName, request: 'launch' }, + { name: 'spam', type: PythonDebuggerTypeName, request: 'attach' }, ]); await debugLauncher.launchDebugger(options); @@ -531,15 +627,17 @@ suite('Unit Tests - Debug Launcher', () => { cwd: 'one/two/three', args: ['/one/two/three/testfile.py'], testProvider: 'unittest', + runTestIdsPort: 'runTestIdsPort', + pytestPort: 'pytestPort', }; const expected = getDefaultDebugConfig(); expected.name = 'spam2'; setupSuccess(options, 'unittest', expected, [ { name: 'foo1', type: 'other', request: 'bar' }, { name: 'foo2', type: 'other', request: 'bar' }, - { name: 'spam1', type: DebuggerTypeName, request: 'launch' }, - { name: 'spam2', type: DebuggerTypeName, request: 'test' }, - { name: 'spam3', type: DebuggerTypeName, request: 'attach' }, + { name: 'spam1', type: PythonDebuggerTypeName, request: 'launch' }, + { name: 'spam2', type: PythonDebuggerTypeName, request: 'test' }, + { name: 'spam3', type: PythonDebuggerTypeName, request: 'attach' }, { name: 'xyz', type: 'another', request: 'abc' }, ]); @@ -553,6 +651,8 @@ suite('Unit Tests - Debug Launcher', () => { cwd: 'one/two/three', args: ['/one/two/three/testfile.py'], testProvider: 'unittest', + runTestIdsPort: 'runTestIdsPort', + pytestPort: 'pytestPort', }; const expected = getDefaultDebugConfig(); expected.name = 'spam'; @@ -569,7 +669,7 @@ suite('Unit Tests - Debug Launcher', () => { { \n\ // "test" debug config \n\ "name": "spam", /* non-empty */ \n\ - "type": "python", /* must be "python" */ \n\ + "type": "debugpy", /* must be "python" */ \n\ "request": "test", /* must be "test" */ \n\ // extra stuff here: \n\ "stopOnEntry": true \n\ @@ -606,4 +706,228 @@ suite('Unit Tests - Debug Launcher', () => { expect(configs).to.be.deep.equal([]); }); + + // ===== PROJECT-BASED DEBUG SESSION TESTS ===== + + suite('Project-based debug sessions', () => { + function setupForProjectTests(options: LaunchOptions) { + interpreterService + .setup((i) => i.getActiveInterpreter(TypeMoq.It.isAny())) + .returns(() => Promise.resolve(({ path: 'python' } as unknown) as PythonEnvironment)); + settings.setup((p) => p.envFile).returns(() => __filename); + + debugEnvHelper + .setup((x) => x.getEnvironmentVariables(TypeMoq.It.isAny(), TypeMoq.It.isAny())) + .returns(() => Promise.resolve({})); + + const workspaceFolders = [{ index: 0, name: 'test', uri: Uri.file(options.cwd) }]; + getWorkspaceFoldersStub.returns(workspaceFolders); + getWorkspaceFolderStub.returns(workspaceFolders[0]); + pathExistsStub.resolves(false); + + // Stub useEnvExtension to avoid null reference errors in tests + sinon.stub(envExtApi, 'useEnvExtension').returns(false); + } + + /** + * Helper to setup debug service mocks with proper session lifecycle simulation. + * The implementation uses onDidStartDebugSession to capture the session via marker, + * then onDidTerminateDebugSession to resolve when that session ends. + */ + function setupDebugServiceWithSessionLifecycle(): { + capturedConfigs: DebugConfiguration[]; + } { + const capturedConfigs: DebugConfiguration[] = []; + let startCallback: ((session: DebugSession) => void) | undefined; + let terminateCallback: ((session: DebugSession) => void) | undefined; + + debugService + .setup((d) => d.startDebugging(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny())) + .callback((_, config) => { + capturedConfigs.push(config); + // Simulate the full session lifecycle after startDebugging resolves + setTimeout(() => { + const session = ({ + id: `session-${capturedConfigs.length}`, + configuration: config, + } as unknown) as DebugSession; + // Fire start first (so ourSession is captured) + startCallback?.(session); + // Then fire terminate (so the promise resolves) + setTimeout(() => terminateCallback?.(session), 5); + }, 5); + }) + .returns(() => Promise.resolve(true)); + + debugService + .setup((d) => d.onDidStartDebugSession(TypeMoq.It.isAny())) + .callback((cb) => { + startCallback = cb; + }) + .returns(() => ({ dispose: () => {} })); + + debugService + .setup((d) => d.onDidTerminateDebugSession(TypeMoq.It.isAny())) + .callback((cb) => { + terminateCallback = cb; + }) + .returns(() => ({ dispose: () => {} })); + + return { capturedConfigs }; + } + + test('should use project name in config name when provided', async () => { + const options: LaunchOptions = { + cwd: 'one/two/three', + args: ['/one/two/three/testfile.py'], + testProvider: 'pytest', + runTestIdsPort: 'runTestIdsPort', + pytestPort: 'pytestPort', + project: { name: 'myproject (Python 3.11)', uri: Uri.file('one/two/three') }, + }; + + setupForProjectTests(options); + const { capturedConfigs } = setupDebugServiceWithSessionLifecycle(); + + await debugLauncher.launchDebugger(options); + + expect(capturedConfigs).to.have.length(1); + expect(capturedConfigs[0].name).to.equal('Debug Tests: myproject (Python 3.11)'); + }); + + test('should use default python when no project provided', async () => { + const options: LaunchOptions = { + cwd: 'one/two/three', + args: ['/one/two/three/testfile.py'], + testProvider: 'pytest', + runTestIdsPort: 'runTestIdsPort', + pytestPort: 'pytestPort', + }; + + setupForProjectTests(options); + const { capturedConfigs } = setupDebugServiceWithSessionLifecycle(); + + await debugLauncher.launchDebugger(options); + + expect(capturedConfigs).to.have.length(1); + // Should use the default 'python' from interpreterService mock + expect(capturedConfigs[0].python).to.equal('python'); + }); + + test('should add unique session marker to launch config', async () => { + const options: LaunchOptions = { + cwd: 'one/two/three', + args: ['/one/two/three/testfile.py'], + testProvider: 'pytest', + runTestIdsPort: 'runTestIdsPort', + pytestPort: 'pytestPort', + }; + + setupForProjectTests(options); + const { capturedConfigs } = setupDebugServiceWithSessionLifecycle(); + + await debugLauncher.launchDebugger(options); + + expect(capturedConfigs).to.have.length(1); + // Should have a session marker of format 'test-{timestamp}-{random}' + const marker = (capturedConfigs[0] as any).__vscodeTestSessionMarker; + expect(marker).to.be.a('string'); + expect(marker).to.match(/^test-\d+-[a-z0-9]+$/); + }); + + test('should generate unique markers for each launch', async () => { + const options: LaunchOptions = { + cwd: 'one/two/three', + args: ['/one/two/three/testfile.py'], + testProvider: 'pytest', + runTestIdsPort: 'runTestIdsPort', + pytestPort: 'pytestPort', + }; + + setupForProjectTests(options); + const { capturedConfigs } = setupDebugServiceWithSessionLifecycle(); + + // Launch twice + await debugLauncher.launchDebugger(options); + await debugLauncher.launchDebugger(options); + + expect(capturedConfigs).to.have.length(2); + const marker1 = (capturedConfigs[0] as any).__vscodeTestSessionMarker; + const marker2 = (capturedConfigs[1] as any).__vscodeTestSessionMarker; + expect(marker1).to.not.equal(marker2); + }); + + test('should only resolve when matching session terminates', async () => { + const options: LaunchOptions = { + cwd: 'one/two/three', + args: ['/one/two/three/testfile.py'], + testProvider: 'pytest', + runTestIdsPort: 'runTestIdsPort', + pytestPort: 'pytestPort', + }; + + setupForProjectTests(options); + + let capturedConfig: DebugConfiguration | undefined; + let terminateCallback: ((session: DebugSession) => void) | undefined; + let startCallback: ((session: DebugSession) => void) | undefined; + + debugService + .setup((d) => d.startDebugging(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny())) + .callback((_, config) => { + capturedConfig = config; + }) + .returns(() => Promise.resolve(true)); + + debugService + .setup((d) => d.onDidStartDebugSession(TypeMoq.It.isAny())) + .callback((cb) => { + startCallback = cb; + }) + .returns(() => ({ dispose: () => {} })); + + debugService + .setup((d) => d.onDidTerminateDebugSession(TypeMoq.It.isAny())) + .callback((cb) => { + terminateCallback = cb; + }) + .returns(() => ({ dispose: () => {} })); + + const launchPromise = debugLauncher.launchDebugger(options); + + // Wait for config to be captured + await new Promise((r) => setTimeout(r, 10)); + + // Simulate our session starting + const ourSession = ({ + id: 'our-session-id', + configuration: capturedConfig!, + } as unknown) as DebugSession; + startCallback?.(ourSession); + + // Create a different session (like another project's debug) + const otherSession = ({ + id: 'other-session-id', + configuration: { __vscodeTestSessionMarker: 'different-marker' }, + } as unknown) as DebugSession; + + // Terminate the OTHER session first - should NOT resolve our promise + terminateCallback?.(otherSession); + + // Wait a bit to ensure it didn't resolve + let resolved = false; + const checkPromise = launchPromise.then(() => { + resolved = true; + }); + + await new Promise((r) => setTimeout(r, 20)); + expect(resolved).to.be.false; + + // Now terminate OUR session - should resolve + terminateCallback?.(ourSession); + + await checkPromise; + expect(resolved).to.be.true; + }); + }); }); diff --git a/src/test/testing/common/helpers.unit.test.ts b/src/test/testing/common/helpers.unit.test.ts new file mode 100644 index 000000000000..441b257d4d0e --- /dev/null +++ b/src/test/testing/common/helpers.unit.test.ts @@ -0,0 +1,48 @@ +import * as path from 'path'; +import * as assert from 'assert'; +import { addPathToPythonpath } from '../../../client/testing/common/helpers'; + +suite('Unit Tests - Test Helpers', () => { + const newPaths = [path.join('path', 'to', 'new')]; + test('addPathToPythonpath handles undefined path', async () => { + const launchPythonPath = undefined; + const actualPath = addPathToPythonpath(newPaths, launchPythonPath); + assert.equal(actualPath, path.join('path', 'to', 'new')); + }); + test('addPathToPythonpath adds path if it does not exist in the python path', async () => { + const launchPythonPath = path.join('random', 'existing', 'pythonpath'); + const actualPath = addPathToPythonpath(newPaths, launchPythonPath); + const expectedPath = + path.join('random', 'existing', 'pythonpath') + path.delimiter + path.join('path', 'to', 'new'); + assert.equal(actualPath, expectedPath); + }); + test('addPathToPythonpath does not add to python path if the given python path already contains the path', async () => { + const launchPythonPath = path.join('path', 'to', 'new'); + const actualPath = addPathToPythonpath(newPaths, launchPythonPath); + const expectedPath = path.join('path', 'to', 'new'); + assert.equal(actualPath, expectedPath); + }); + test('addPathToPythonpath correctly normalizes both existing and new paths', async () => { + const newerPaths = [path.join('path', 'to', '/', 'new')]; + const launchPythonPath = path.join('path', 'to', '..', 'old'); + const actualPath = addPathToPythonpath(newerPaths, launchPythonPath); + const expectedPath = path.join('path', 'old') + path.delimiter + path.join('path', 'to', 'new'); + assert.equal(actualPath, expectedPath); + }); + test('addPathToPythonpath splits pythonpath then rejoins it', async () => { + const launchPythonPath = + path.join('path', 'to', 'new') + + path.delimiter + + path.join('path', 'to', 'old') + + path.delimiter + + path.join('path', 'to', 'random'); + const actualPath = addPathToPythonpath(newPaths, launchPythonPath); + const expectedPath = + path.join('path', 'to', 'new') + + path.delimiter + + path.join('path', 'to', 'old') + + path.delimiter + + path.join('path', 'to', 'random'); + assert.equal(actualPath, expectedPath); + }); +}); diff --git a/src/test/testing/common/managers/testConfigurationManager.unit.test.ts b/src/test/testing/common/managers/testConfigurationManager.unit.test.ts index c8b6085e599d..1b049d4f3fbe 100644 --- a/src/test/testing/common/managers/testConfigurationManager.unit.test.ts +++ b/src/test/testing/common/managers/testConfigurationManager.unit.test.ts @@ -5,7 +5,7 @@ import * as TypeMoq from 'typemoq'; import { OutputChannel, Uri } from 'vscode'; -import { IInstaller, ITestOutputChannel, Product } from '../../../../client/common/types'; +import { IInstaller, ILogOutputChannel, Product } from '../../../../client/common/types'; import { getNamesAndValues } from '../../../../client/common/utils/enum'; import { IServiceContainer } from '../../../../client/ioc/types'; import { UNIT_TEST_PRODUCTS } from '../../../../client/testing/common/constants'; @@ -41,7 +41,7 @@ suite('Unit Test Configuration Manager (unit)', () => { const installer = TypeMoq.Mock.ofType().object; const serviceContainer = TypeMoq.Mock.ofType(); serviceContainer - .setup((s) => s.get(TypeMoq.It.isValue(ITestOutputChannel))) + .setup((s) => s.get(TypeMoq.It.isValue(ILogOutputChannel))) .returns(() => outputChannel); serviceContainer .setup((s) => s.get(TypeMoq.It.isValue(ITestConfigSettingsService))) diff --git a/src/test/testing/common/services/configSettingService.unit.test.ts b/src/test/testing/common/services/configSettingService.unit.test.ts index 92d66f021491..d369d7ead825 100644 --- a/src/test/testing/common/services/configSettingService.unit.test.ts +++ b/src/test/testing/common/services/configSettingService.unit.test.ts @@ -16,7 +16,7 @@ import { TestConfigSettingsService } from '../../../../client/testing/common/con import { ITestConfigSettingsService, UnitTestProduct } from '../../../../client/testing/common/types'; import { BufferedTestConfigSettingsService } from '../../../../client/testing/common/bufferedTestConfigSettingService'; -use(chaiPromise); +use(chaiPromise.default); const updateMethods: (keyof Omit)[] = [ 'updateTestArgs', diff --git a/src/test/testing/common/testingAdapter.test.ts b/src/test/testing/common/testingAdapter.test.ts new file mode 100644 index 000000000000..478e9dd85744 --- /dev/null +++ b/src/test/testing/common/testingAdapter.test.ts @@ -0,0 +1,1242 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +import { TestController, TestRun, TestRunProfileKind, Uri } from 'vscode'; +import * as typeMoq from 'typemoq'; +import * as path from 'path'; +import * as assert from 'assert'; +import * as fs from 'fs'; +import * as os from 'os'; +import * as sinon from 'sinon'; +import { PytestTestDiscoveryAdapter } from '../../../client/testing/testController/pytest/pytestDiscoveryAdapter'; +import { + ITestController, + ITestResultResolver, + ExecutionTestPayload, +} from '../../../client/testing/testController/common/types'; +import { IPythonExecutionFactory } from '../../../client/common/process/types'; +import { IConfigurationService } from '../../../client/common/types'; +import { IServiceContainer } from '../../../client/ioc/types'; +import { EXTENSION_ROOT_DIR_FOR_TESTS, initialize } from '../../initialize'; +import { traceError, traceLog } from '../../../client/logging'; +import { PytestTestExecutionAdapter } from '../../../client/testing/testController/pytest/pytestExecutionAdapter'; +import { UnittestTestDiscoveryAdapter } from '../../../client/testing/testController/unittest/testDiscoveryAdapter'; +import { UnittestTestExecutionAdapter } from '../../../client/testing/testController/unittest/testExecutionAdapter'; +import { PythonResultResolver } from '../../../client/testing/testController/common/resultResolver'; +import { TestProvider } from '../../../client/testing/types'; +import { PYTEST_PROVIDER, UNITTEST_PROVIDER } from '../../../client/testing/common/constants'; +import { IEnvironmentVariablesProvider } from '../../../client/common/variables/types'; +import * as pixi from '../../../client/pythonEnvironments/common/environmentManagers/pixi'; + +suite('End to End Tests: test adapters', () => { + let resultResolver: ITestResultResolver; + let pythonExecFactory: IPythonExecutionFactory; + let configService: IConfigurationService; + let serviceContainer: IServiceContainer; + let envVarsService: IEnvironmentVariablesProvider; + let workspaceUri: Uri; + let testController: TestController; + let getPixiStub: sinon.SinonStub; + const unittestProvider: TestProvider = UNITTEST_PROVIDER; + const pytestProvider: TestProvider = PYTEST_PROVIDER; + const rootPathSmallWorkspace = path.join( + EXTENSION_ROOT_DIR_FOR_TESTS, + 'src', + 'testTestingRootWkspc', + 'smallWorkspace', + ); + const rootPathLargeWorkspace = path.join( + EXTENSION_ROOT_DIR_FOR_TESTS, + 'src', + 'testTestingRootWkspc', + 'largeWorkspace', + ); + const rootPathErrorWorkspace = path.join( + EXTENSION_ROOT_DIR_FOR_TESTS, + 'src', + 'testTestingRootWkspc', + 'errorWorkspace', + ); + const rootPathDiscoveryErrorWorkspace = path.join( + EXTENSION_ROOT_DIR_FOR_TESTS, + 'src', + 'testTestingRootWkspc', + 'discoveryErrorWorkspace', + ); + const rootPathDiscoverySymlink = path.join( + EXTENSION_ROOT_DIR_FOR_TESTS, + 'src', + 'testTestingRootWkspc', + 'symlinkWorkspace', + ); + const nestedTarget = path.join(EXTENSION_ROOT_DIR_FOR_TESTS, 'src', 'testTestingRootWkspc', 'target workspace'); + const nestedSymlink = path.join( + EXTENSION_ROOT_DIR_FOR_TESTS, + 'src', + 'testTestingRootWkspc', + 'symlink_parent-folder', + ); + const rootPathCoverageWorkspace = path.join( + EXTENSION_ROOT_DIR_FOR_TESTS, + 'src', + 'testTestingRootWkspc', + 'coverageWorkspace', + ); + suiteSetup(async () => { + // create symlink for specific symlink test + const target = rootPathSmallWorkspace; + const dest = rootPathDiscoverySymlink; + try { + fs.symlink(target, dest, 'dir', (err) => { + if (err) { + traceError(err); + } else { + traceLog('Symlink created successfully for regular symlink end to end tests.'); + } + }); + fs.symlink(nestedTarget, nestedSymlink, 'dir', (err) => { + if (err) { + traceError(err); + } else { + traceLog('Symlink created successfully for nested symlink end to end tests.'); + } + }); + } catch (err) { + traceError(err); + } + }); + + setup(async () => { + serviceContainer = (await initialize()).serviceContainer; + getPixiStub = sinon.stub(pixi, 'getPixi'); + getPixiStub.resolves(undefined); + + // create objects that were injected + configService = serviceContainer.get(IConfigurationService); + pythonExecFactory = serviceContainer.get(IPythonExecutionFactory); + testController = serviceContainer.get(ITestController); + envVarsService = serviceContainer.get(IEnvironmentVariablesProvider); + + // create objects that were not injected + }); + teardown(() => { + sinon.restore(); + }); + suiteTeardown(async () => { + // remove symlink + const dest = rootPathDiscoverySymlink; + if (fs.existsSync(dest)) { + fs.unlink(dest, (err) => { + if (err) { + traceError(err); + } else { + traceLog('Symlink removed successfully after tests, rootPathDiscoverySymlink.'); + } + }); + } else { + traceLog('Symlink was not found to remove after tests, exiting successfully, rootPathDiscoverySymlink.'); + } + + if (fs.existsSync(nestedSymlink)) { + fs.unlink(nestedSymlink, (err) => { + if (err) { + traceError(err); + } else { + traceLog('Symlink removed successfully after tests, nestedSymlink.'); + } + }); + } else { + traceLog('Symlink was not found to remove after tests, exiting successfully, nestedSymlink.'); + } + }); + test('unittest discovery adapter small workspace', async () => { + // result resolver and saved data for assertions + let actualData: { + cwd: string; + tests?: unknown; + status: 'success' | 'error'; + error?: string[]; + }; + workspaceUri = Uri.parse(rootPathSmallWorkspace); + resultResolver = new PythonResultResolver(testController, unittestProvider, workspaceUri); + let callCount = 0; + // const deferredTillEOT = createTestingDeferred(); + resultResolver.resolveDiscovery = (payload, _token?) => { + traceLog(`resolveDiscovery ${payload}`); + callCount = callCount + 1; + actualData = payload; + }; + + // set workspace to test workspace folder and set up settings + + configService.getSettings(workspaceUri).testing.unittestArgs = ['-s', '.', '-p', '*test*.py']; + + // run unittest discovery + const discoveryAdapter = new UnittestTestDiscoveryAdapter(configService, resultResolver, envVarsService); + + await discoveryAdapter.discoverTests(workspaceUri, pythonExecFactory).finally(() => { + // verification after discovery is complete + + // 1. Check the status is "success" + assert.strictEqual( + actualData.status, + 'success', + `Expected status to be 'success' instead status is ${actualData.status}`, + ); + // 2. Confirm no errors + assert.strictEqual(actualData.error, undefined, "Expected no errors in 'error' field"); + // 3. Confirm tests are found + assert.ok(actualData.tests, 'Expected tests to be present'); + + assert.strictEqual(callCount, 1, 'Expected _resolveDiscovery to be called once'); + }); + }); + test('unittest discovery adapter large workspace', async () => { + // result resolver and saved data for assertions + let actualData: { + cwd: string; + tests?: unknown; + status: 'success' | 'error'; + error?: string[]; + }; + resultResolver = new PythonResultResolver(testController, unittestProvider, workspaceUri); + let callCount = 0; + resultResolver.resolveDiscovery = (payload, _token?) => { + traceLog(`resolveDiscovery ${payload}`); + callCount = callCount + 1; + actualData = payload; + }; + + // set settings to work for the given workspace + workspaceUri = Uri.parse(rootPathLargeWorkspace); + configService.getSettings(workspaceUri).testing.unittestArgs = ['-s', '.', '-p', '*test*.py']; + // run discovery + const discoveryAdapter = new UnittestTestDiscoveryAdapter(configService, resultResolver, envVarsService); + + await discoveryAdapter.discoverTests(workspaceUri, pythonExecFactory).finally(() => { + // 1. Check the status is "success" + assert.strictEqual( + actualData.status, + 'success', + `Expected status to be 'success' instead status is ${actualData.status}`, + ); + // 2. Confirm no errors + assert.strictEqual(actualData.error, undefined, "Expected no errors in 'error' field"); + // 3. Confirm tests are found + assert.ok(actualData.tests, 'Expected tests to be present'); + + assert.strictEqual(callCount, 1, 'Expected _resolveDiscovery to be called once'); + }); + }); + test('pytest discovery adapter small workspace', async () => { + // result resolver and saved data for assertions + let actualData: { + cwd: string; + tests?: unknown; + status: 'success' | 'error'; + error?: string[]; + }; + // set workspace to test workspace folder + workspaceUri = Uri.parse(rootPathSmallWorkspace); + resultResolver = new PythonResultResolver(testController, pytestProvider, workspaceUri); + let callCount = 0; + resultResolver.resolveDiscovery = (payload, _token?) => { + callCount = callCount + 1; + actualData = payload; + }; + // run pytest discovery + const discoveryAdapter = new PytestTestDiscoveryAdapter(configService, resultResolver, envVarsService); + + await discoveryAdapter.discoverTests(workspaceUri, pythonExecFactory).finally(() => { + // verification after discovery is complete + + // 1. Check the status is "success" + assert.strictEqual( + actualData.status, + 'success', + `Expected status to be 'success' instead status is ${actualData.status}`, + ); // 2. Confirm no errors + assert.strictEqual(actualData.error?.length, 0, "Expected no errors in 'error' field"); + // 3. Confirm tests are found + assert.ok(actualData.tests, 'Expected tests to be present'); + + assert.strictEqual(callCount, 1, 'Expected _resolveDiscovery to be called once'); + }); + }); + test('pytest discovery adapter nested symlink', async () => { + if (os.platform() === 'win32') { + console.log('Skipping test for windows'); + return; + } + + // result resolver and saved data for assertions + let actualData: { + cwd: string; + tests?: unknown; + status: 'success' | 'error'; + error?: string[]; + }; + // set workspace to test workspace folder + const workspacePath = path.join(nestedSymlink, 'custom_sub_folder'); + const workspacePathParent = nestedSymlink; + workspaceUri = Uri.parse(workspacePath); + const filePath = path.join(workspacePath, 'test_simple.py'); + const stats = fs.lstatSync(workspacePathParent); + + // confirm that the path is a symbolic link + assert.ok(stats.isSymbolicLink(), 'The PARENT path is not a symbolic link but must be for this test.'); + + resultResolver = new PythonResultResolver(testController, pytestProvider, workspaceUri); + let callCount = 0; + resultResolver.resolveDiscovery = (payload, _token?) => { + traceLog(`resolveDiscovery ${payload}`); + callCount = callCount + 1; + actualData = payload; + }; + // run pytest discovery + const discoveryAdapter = new PytestTestDiscoveryAdapter(configService, resultResolver, envVarsService); + configService.getSettings(workspaceUri).testing.pytestArgs = []; + + await discoveryAdapter.discoverTests(workspaceUri, pythonExecFactory).finally(() => { + // verification after discovery is complete + + // 1. Check the status is "success" + assert.strictEqual( + actualData.status, + 'success', + `Expected status to be 'success' instead status is ${actualData.status}`, + ); // 2. Confirm no errors + assert.strictEqual(actualData.error?.length, 0, "Expected no errors in 'error' field"); + // 3. Confirm tests are found + assert.ok(actualData.tests, 'Expected tests to be present'); + // 4. Confirm that the cwd returned is the symlink path and the test's path is also using the symlink as the root + if (process.platform === 'win32') { + // covert string to lowercase for windows as the path is case insensitive + traceLog('windows machine detected, converting path to lowercase for comparison'); + const a = actualData.cwd.toLowerCase(); + const b = filePath.toLowerCase(); + const testSimpleActual = (actualData.tests as { + children: { + path: string; + }[]; + }).children[0].path.toLowerCase(); + const testSimpleExpected = filePath.toLowerCase(); + assert.strictEqual(a, b, `Expected cwd to be the symlink path actual: ${a} expected: ${b}`); + assert.strictEqual( + testSimpleActual, + testSimpleExpected, + `Expected test path to be the symlink path actual: ${testSimpleActual} expected: ${testSimpleExpected}`, + ); + } else { + assert.strictEqual( + path.join(actualData.cwd), + path.join(workspacePath), + 'Expected cwd to be the symlink path, check for non-windows machines', + ); + assert.strictEqual( + (actualData.tests as { + children: { + path: string; + }[]; + }).children[0].path, + filePath, + 'Expected test path to be the symlink path, check for non windows machines', + ); + } + + // 5. Confirm that resolveDiscovery was called once + assert.strictEqual(callCount, 1, 'Expected _resolveDiscovery to be called once'); + }); + }); + test('pytest discovery adapter small workspace with symlink', async () => { + if (os.platform() === 'win32') { + console.log('Skipping test for windows'); + return; + } + + // result resolver and saved data for assertions + let actualData: { + cwd: string; + tests?: unknown; + status: 'success' | 'error'; + error?: string[]; + }; + // set workspace to test workspace folder + const testSimpleSymlinkPath = path.join(rootPathDiscoverySymlink, 'test_simple.py'); + workspaceUri = Uri.parse(rootPathDiscoverySymlink); + const stats = fs.lstatSync(rootPathDiscoverySymlink); + + // confirm that the path is a symbolic link + assert.ok(stats.isSymbolicLink(), 'The path is not a symbolic link but must be for this test.'); + + resultResolver = new PythonResultResolver(testController, pytestProvider, workspaceUri); + let callCount = 0; + resultResolver.resolveDiscovery = (payload, _token?) => { + traceLog(`resolveDiscovery ${payload}`); + callCount = callCount + 1; + actualData = payload; + }; + // run pytest discovery + const discoveryAdapter = new PytestTestDiscoveryAdapter(configService, resultResolver, envVarsService); + configService.getSettings(workspaceUri).testing.pytestArgs = []; + + await discoveryAdapter.discoverTests(workspaceUri, pythonExecFactory).finally(() => { + // verification after discovery is complete + + // 1. Check the status is "success" + assert.strictEqual( + actualData.status, + 'success', + `Expected status to be 'success' instead status is ${actualData.status}`, + ); // 2. Confirm no errors + assert.strictEqual(actualData.error?.length, 0, "Expected no errors in 'error' field"); + // 3. Confirm tests are found + assert.ok(actualData.tests, 'Expected tests to be present'); + // 4. Confirm that the cwd returned is the symlink path and the test's path is also using the symlink as the root + if (process.platform === 'win32') { + // covert string to lowercase for windows as the path is case insensitive + traceLog('windows machine detected, converting path to lowercase for comparison'); + const a = actualData.cwd.toLowerCase(); + const b = rootPathDiscoverySymlink.toLowerCase(); + const testSimpleActual = (actualData.tests as { + children: { + path: string; + }[]; + }).children[0].path.toLowerCase(); + const testSimpleExpected = testSimpleSymlinkPath.toLowerCase(); + assert.strictEqual(a, b, `Expected cwd to be the symlink path actual: ${a} expected: ${b}`); + assert.strictEqual( + testSimpleActual, + testSimpleExpected, + `Expected test path to be the symlink path actual: ${testSimpleActual} expected: ${testSimpleExpected}`, + ); + } else { + assert.strictEqual( + path.join(actualData.cwd), + path.join(rootPathDiscoverySymlink), + 'Expected cwd to be the symlink path, check for non-windows machines', + ); + assert.strictEqual( + (actualData.tests as { + children: { + path: string; + }[]; + }).children[0].path, + testSimpleSymlinkPath, + 'Expected test path to be the symlink path, check for non windows machines', + ); + } + + // 5. Confirm that resolveDiscovery was called once + assert.strictEqual(callCount, 1, 'Expected _resolveDiscovery to be called once'); + }); + }); + test('pytest discovery adapter large workspace', async () => { + // result resolver and saved data for assertions + let actualData: { + cwd: string; + tests?: unknown; + status: 'success' | 'error'; + error?: string[]; + }; + resultResolver = new PythonResultResolver(testController, pytestProvider, workspaceUri); + let callCount = 0; + resultResolver.resolveDiscovery = (payload, _token?) => { + traceLog(`resolveDiscovery ${payload}`); + callCount = callCount + 1; + actualData = payload; + }; + // run pytest discovery + const discoveryAdapter = new PytestTestDiscoveryAdapter(configService, resultResolver, envVarsService); + + // set workspace to test workspace folder + workspaceUri = Uri.parse(rootPathLargeWorkspace); + configService.getSettings(workspaceUri).testing.pytestArgs = []; + + await discoveryAdapter.discoverTests(workspaceUri, pythonExecFactory).finally(() => { + // verification after discovery is complete + // 1. Check the status is "success" + assert.strictEqual( + actualData.status, + 'success', + `Expected status to be 'success' instead status is ${actualData.status}`, + ); // 2. Confirm no errors + assert.strictEqual(actualData.error?.length, 0, "Expected no errors in 'error' field"); + // 3. Confirm tests are found + assert.ok(actualData.tests, 'Expected tests to be present'); + + assert.strictEqual(callCount, 1, 'Expected _resolveDiscovery to be called once'); + }); + }); + test('unittest execution adapter small workspace with correct output', async () => { + // result resolver and saved data for assertions + resultResolver = new PythonResultResolver(testController, unittestProvider, workspaceUri); + let callCount = 0; + let failureOccurred = false; + let failureMsg = ''; + resultResolver.resolveExecution = (payload, _token?) => { + traceLog(`resolveDiscovery ${payload}`); + callCount = callCount + 1; + // the payloads that get to the _resolveExecution are all data and should be successful. + try { + if ('status' in payload) { + assert.strictEqual( + payload.status, + 'success', + `Expected status to be 'success', instead status is ${payload.status}`, + ); + assert.ok(payload.result, 'Expected results to be present'); + } + } catch (err) { + failureMsg = err ? (err as Error).toString() : ''; + failureOccurred = true; + } + }; + + // set workspace to test workspace folder + workspaceUri = Uri.parse(rootPathSmallWorkspace); + configService.getSettings(workspaceUri).testing.unittestArgs = ['-s', '.', '-p', '*test*.py']; + // run execution + const executionAdapter = new UnittestTestExecutionAdapter(configService, resultResolver, envVarsService); + const testRun = typeMoq.Mock.ofType(); + testRun + .setup((t) => t.token) + .returns( + () => + ({ + onCancellationRequested: () => undefined, + } as any), + ); + let collectedOutput = ''; + testRun + .setup((t) => t.appendOutput(typeMoq.It.isAny())) + .callback((output: string) => { + collectedOutput += output; + traceLog('appendOutput was called with:', output); + }) + .returns(() => false); + await executionAdapter + .runTests( + workspaceUri, + ['test_simple.SimpleClass.test_simple_unit'], + TestRunProfileKind.Run, + testRun.object, + pythonExecFactory, + ) + .finally(() => { + // verify that the _resolveExecution was called once per test + assert.strictEqual(callCount, 1, 'Expected _resolveExecution to be called once'); + assert.strictEqual(failureOccurred, false, failureMsg); + + // verify output works for stdout and stderr as well as unittest output + assert.ok( + collectedOutput.includes('expected printed output, stdout'), + 'The test string does not contain the expected stdout output.', + ); + assert.ok( + collectedOutput.includes('expected printed output, stderr'), + 'The test string does not contain the expected stderr output.', + ); + assert.ok( + collectedOutput.includes('Ran 1 test in'), + 'The test string does not contain the expected unittest output.', + ); + }); + }); + test('unittest execution adapter large workspace', async () => { + // result resolver and saved data for assertions + resultResolver = new PythonResultResolver(testController, unittestProvider, workspaceUri); + let callCount = 0; + let failureOccurred = false; + let failureMsg = ''; + resultResolver.resolveExecution = (payload, _token?) => { + traceLog(`resolveDiscovery ${payload}`); + callCount = callCount + 1; + // the payloads that get to the _resolveExecution are all data and should be successful. + try { + if ('status' in payload) { + const validStatuses = ['subtest-success', 'subtest-failure']; + assert.ok( + validStatuses.includes(payload.status), + `Expected status to be one of ${validStatuses.join(', ')}, but instead status is ${ + payload.status + }`, + ); + assert.ok(payload.result, 'Expected results to be present'); + } + } catch (err) { + failureMsg = err ? (err as Error).toString() : ''; + failureOccurred = true; + } + }; + + // set workspace to test workspace folder + workspaceUri = Uri.parse(rootPathLargeWorkspace); + configService.getSettings(workspaceUri).testing.unittestArgs = ['-s', '.', '-p', '*test*.py']; + + // run unittest execution + const executionAdapter = new UnittestTestExecutionAdapter(configService, resultResolver, envVarsService); + const testRun = typeMoq.Mock.ofType(); + testRun + .setup((t) => t.token) + .returns( + () => + ({ + onCancellationRequested: () => undefined, + } as any), + ); + let collectedOutput = ''; + testRun + .setup((t) => t.appendOutput(typeMoq.It.isAny())) + .callback((output: string) => { + collectedOutput += output; + traceLog('appendOutput was called with:', output); + }) + .returns(() => false); + await executionAdapter + .runTests( + workspaceUri, + ['test_parameterized_subtest.NumbersTest.test_even'], + TestRunProfileKind.Run, + testRun.object, + pythonExecFactory, + ) + .then(() => { + // verify that the _resolveExecution was called once per test + assert.strictEqual(callCount, 2000, 'Expected _resolveExecution to be called once'); + assert.strictEqual(failureOccurred, false, failureMsg); + + // verify output + assert.ok( + collectedOutput.includes('test_parameterized_subtest.py'), + 'The test string does not contain the correct test name which should be printed', + ); + assert.ok( + collectedOutput.includes('FAILED (failures=1000)'), + 'The test string does not contain the last of the unittest output', + ); + }); + }); + test('pytest execution adapter small workspace with correct output', async () => { + // result resolver and saved data for assertions + resultResolver = new PythonResultResolver(testController, pytestProvider, workspaceUri); + let callCount = 0; + let failureOccurred = false; + let failureMsg = ''; + resultResolver.resolveExecution = (payload, _token?) => { + traceLog(`resolveDiscovery ${payload}`); + callCount = callCount + 1; + // the payloads that get to the _resolveExecution are all data and should be successful. + try { + if ('status' in payload) { + assert.strictEqual( + payload.status, + 'success', + `Expected status to be 'success', instead status is ${payload.status}`, + ); + assert.ok(payload.result, 'Expected results to be present'); + } + } catch (err) { + failureMsg = err ? (err as Error).toString() : ''; + failureOccurred = true; + } + }; + // set workspace to test workspace folder + workspaceUri = Uri.parse(rootPathSmallWorkspace); + configService.getSettings(workspaceUri).testing.pytestArgs = []; + + // run pytest execution + const executionAdapter = new PytestTestExecutionAdapter(configService, resultResolver, envVarsService); + const testRun = typeMoq.Mock.ofType(); + testRun + .setup((t) => t.token) + .returns( + () => + ({ + onCancellationRequested: () => undefined, + } as any), + ); + let collectedOutput = ''; + testRun + .setup((t) => t.appendOutput(typeMoq.It.isAny())) + .callback((output: string) => { + collectedOutput += output; + traceLog('appendOutput was called with:', output); + }) + .returns(() => false); + await executionAdapter + .runTests( + workspaceUri, + [`${rootPathSmallWorkspace}/test_simple.py::test_a`], + TestRunProfileKind.Run, + testRun.object, + pythonExecFactory, + ) + .then(() => { + // verify that the _resolveExecution was called once per test + assert.strictEqual(callCount, 1, 'Expected _resolveExecution to be called once'); + assert.strictEqual(failureOccurred, false, failureMsg); + + // verify output works for stdout and stderr as well as pytest output + assert.ok( + collectedOutput.includes('test session starts'), + 'The test string does not contain the expected stdout output.', + ); + assert.ok( + collectedOutput.includes('Captured log call'), + 'The test string does not contain the expected log section.', + ); + const searchStrings = [ + 'This is a warning message.', + 'This is an error message.', + 'This is a critical message.', + ]; + let searchString: string; + for (searchString of searchStrings) { + const count: number = (collectedOutput.match(new RegExp(searchString, 'g')) || []).length; + assert.strictEqual( + count, + 2, + `The test string does not contain two instances of ${searchString}. Should appear twice from logging output and stack trace`, + ); + } + }); + }); + + test('Unittest execution with coverage, small workspace', async () => { + // result resolver and saved data for assertions + resultResolver = new PythonResultResolver(testController, unittestProvider, workspaceUri); + resultResolver._resolveCoverage = (payload, _token?) => { + assert.strictEqual(payload.cwd, rootPathCoverageWorkspace, 'Expected cwd to be the workspace folder'); + assert.ok(payload.result, 'Expected results to be present'); + const simpleFileCov = payload.result[`${rootPathCoverageWorkspace}/even.py`]; + assert.ok(simpleFileCov, 'Expected test_simple.py coverage to be present'); + // since only one test was run, the other test in the same file will have missed coverage lines + assert.strictEqual(simpleFileCov.lines_covered.length, 3, 'Expected 1 line to be covered in even.py'); + assert.strictEqual(simpleFileCov.lines_missed.length, 1, 'Expected 3 lines to be missed in even.py'); + assert.strictEqual(simpleFileCov.executed_branches, 1, 'Expected 1 branch to be executed in even.py'); + assert.strictEqual(simpleFileCov.total_branches, 2, 'Expected 2 branches in even.py'); + }; + + // set workspace to test workspace folder + workspaceUri = Uri.parse(rootPathCoverageWorkspace); + configService.getSettings(workspaceUri).testing.unittestArgs = ['-s', '.', '-p', '*test*.py']; + // run execution + const executionAdapter = new UnittestTestExecutionAdapter(configService, resultResolver, envVarsService); + const testRun = typeMoq.Mock.ofType(); + testRun + .setup((t) => t.token) + .returns( + () => + ({ + onCancellationRequested: () => undefined, + } as any), + ); + let collectedOutput = ''; + testRun + .setup((t) => t.appendOutput(typeMoq.It.isAny())) + .callback((output: string) => { + collectedOutput += output; + traceLog('appendOutput was called with:', output); + }) + .returns(() => false); + await executionAdapter + .runTests( + workspaceUri, + ['test_even.TestNumbers.test_odd'], + TestRunProfileKind.Coverage, + testRun.object, + pythonExecFactory, + ) + .finally(() => { + assert.ok(collectedOutput, 'expect output to be collected'); + }); + }); + test('pytest coverage execution, small workspace', async () => { + // result resolver and saved data for assertions + resultResolver = new PythonResultResolver(testController, pytestProvider, workspaceUri); + resultResolver._resolveCoverage = (payload, _runInstance?) => { + assert.strictEqual(payload.cwd, rootPathCoverageWorkspace, 'Expected cwd to be the workspace folder'); + assert.ok(payload.result, 'Expected results to be present'); + const simpleFileCov = payload.result[`${rootPathCoverageWorkspace}/even.py`]; + assert.ok(simpleFileCov, 'Expected test_simple.py coverage to be present'); + // since only one test was run, the other test in the same file will have missed coverage lines + assert.strictEqual(simpleFileCov.lines_covered.length, 3, 'Expected 1 line to be covered in even.py'); + assert.strictEqual(simpleFileCov.lines_missed.length, 1, 'Expected 3 lines to be missed in even.py'); + assert.strictEqual(simpleFileCov.executed_branches, 1, 'Expected 1 branch to be executed in even.py'); + assert.strictEqual(simpleFileCov.total_branches, 2, 'Expected 2 branches in even.py'); + }; + // set workspace to test workspace folder + workspaceUri = Uri.parse(rootPathCoverageWorkspace); + configService.getSettings(workspaceUri).testing.pytestArgs = []; + + // run pytest execution + const executionAdapter = new PytestTestExecutionAdapter(configService, resultResolver, envVarsService); + const testRun = typeMoq.Mock.ofType(); + testRun + .setup((t) => t.token) + .returns( + () => + ({ + onCancellationRequested: () => undefined, + } as any), + ); + let collectedOutput = ''; + testRun + .setup((t) => t.appendOutput(typeMoq.It.isAny())) + .callback((output: string) => { + collectedOutput += output; + traceLog('appendOutput was called with:', output); + }) + .returns(() => false); + await executionAdapter + .runTests( + workspaceUri, + [`${rootPathCoverageWorkspace}/test_even.py::TestNumbers::test_odd`], + TestRunProfileKind.Coverage, + testRun.object, + pythonExecFactory, + ) + .then(() => { + assert.ok(collectedOutput, 'expect output to be collected'); + }); + }); + test('pytest execution adapter large workspace', async () => { + // result resolver and saved data for assertions + resultResolver = new PythonResultResolver(testController, pytestProvider, workspaceUri); + let callCount = 0; + let failureOccurred = false; + let failureMsg = ''; + resultResolver.resolveExecution = (payload, _token?) => { + traceLog(`resolveDiscovery ${payload}`); + callCount = callCount + 1; + // the payloads that get to the _resolveExecution are all data and should be successful. + try { + if ('status' in payload) { + assert.strictEqual( + payload.status, + 'success', + `Expected status to be 'success', instead status is ${payload.status}`, + ); + assert.ok(payload.result, 'Expected results to be present'); + } + } catch (err) { + failureMsg = err ? (err as Error).toString() : ''; + failureOccurred = true; + } + }; + + // set workspace to test workspace folder + workspaceUri = Uri.parse(rootPathLargeWorkspace); + configService.getSettings(workspaceUri).testing.pytestArgs = []; + + // generate list of test_ids + const testIds: string[] = []; + for (let i = 0; i < 2000; i = i + 1) { + const testId = `${rootPathLargeWorkspace}/test_parameterized_subtest.py::test_odd_even[${i}]`; + testIds.push(testId); + } + + // run pytest execution + const executionAdapter = new PytestTestExecutionAdapter(configService, resultResolver, envVarsService); + const testRun = typeMoq.Mock.ofType(); + testRun + .setup((t) => t.token) + .returns( + () => + ({ + onCancellationRequested: () => undefined, + } as any), + ); + let collectedOutput = ''; + testRun + .setup((t) => t.appendOutput(typeMoq.It.isAny())) + .callback((output: string) => { + collectedOutput += output; + traceLog('appendOutput was called with:', output); + }) + .returns(() => false); + await executionAdapter + .runTests(workspaceUri, testIds, TestRunProfileKind.Run, testRun.object, pythonExecFactory) + .then(() => { + // verify that the _resolveExecution was called once per test + assert.strictEqual(callCount, 2000, 'Expected _resolveExecution to be called once'); + assert.strictEqual(failureOccurred, false, failureMsg); + + // verify output works for large repo + assert.ok( + collectedOutput.includes('test session starts'), + 'The test string does not contain the expected stdout output from pytest.', + ); + }); + }); + test('unittest discovery adapter seg fault error handling', async () => { + resultResolver = new PythonResultResolver(testController, unittestProvider, workspaceUri); + let callCount = 0; + let failureOccurred = false; + let failureMsg = ''; + resultResolver.resolveDiscovery = (data, _token?) => { + // do the following asserts for each time resolveExecution is called, should be called once per test. + callCount = callCount + 1; + traceLog(`unittest discovery adapter seg fault error handling \n ${JSON.stringify(data)}`); + try { + if (data.status === 'error') { + if (data.error === undefined) { + // Dereference a NULL pointer + const indexOfTest = JSON.stringify(data).search('Dereference a NULL pointer'); + assert.notDeepEqual(indexOfTest, -1, 'Expected test to have a null pointer'); + } else { + assert.ok(data.error, "Expected errors in 'error' field"); + } + } else { + const indexOfTest = JSON.stringify(data.tests).search('error'); + assert.notDeepEqual( + indexOfTest, + -1, + 'If payload status is not error then the individual tests should be marked as errors. This should occur on windows machines.', + ); + } + } catch (err) { + failureMsg = err ? (err as Error).toString() : ''; + failureOccurred = true; + } + }; + + // set workspace to test workspace folder + workspaceUri = Uri.parse(rootPathDiscoveryErrorWorkspace); + configService.getSettings(workspaceUri).testing.unittestArgs = ['-s', '.', '-p', '*test*.py']; + + const discoveryAdapter = new UnittestTestDiscoveryAdapter(configService, resultResolver, envVarsService); + const testRun = typeMoq.Mock.ofType(); + testRun + .setup((t) => t.token) + .returns( + () => + ({ + onCancellationRequested: () => undefined, + } as any), + ); + await discoveryAdapter.discoverTests(workspaceUri, pythonExecFactory).finally(() => { + assert.strictEqual(callCount, 1, 'Expected _resolveDiscovery to be called once'); + assert.strictEqual(failureOccurred, false, failureMsg); + }); + }); + test('pytest discovery seg fault error handling', async () => { + // result resolver and saved data for assertions + resultResolver = new PythonResultResolver(testController, pytestProvider, workspaceUri); + let callCount = 0; + let failureOccurred = false; + let failureMsg = ''; + resultResolver.resolveDiscovery = (data, _token?) => { + // do the following asserts for each time resolveExecution is called, should be called once per test. + callCount = callCount + 1; + traceLog(`add one to call count, is now ${callCount}`); + traceLog(`pytest discovery adapter seg fault error handling \n ${JSON.stringify(data)}`); + try { + if (data.status === 'error') { + if (data.error === undefined) { + // Dereference a NULL pointer + const indexOfTest = JSON.stringify(data).search('Dereference a NULL pointer'); + if (indexOfTest === -1) { + failureOccurred = true; + failureMsg = 'Expected test to have a null pointer'; + } + } else if (data.error.length === 0) { + failureOccurred = true; + failureMsg = "Expected errors in 'error' field"; + } + } else { + const indexOfTest = JSON.stringify(data.tests).search('error'); + if (indexOfTest === -1) { + failureOccurred = true; + failureMsg = + 'If payload status is not error then the individual tests should be marked as errors. This should occur on windows machines.'; + } + } + } catch (err) { + failureMsg = err ? (err as Error).toString() : ''; + failureOccurred = true; + } + }; + // run pytest discovery + const discoveryAdapter = new PytestTestDiscoveryAdapter(configService, resultResolver, envVarsService); + + // set workspace to test workspace folder + workspaceUri = Uri.parse(rootPathDiscoveryErrorWorkspace); + configService.getSettings(workspaceUri).testing.pytestArgs = []; + + await discoveryAdapter.discoverTests(workspaceUri, pythonExecFactory).finally(() => { + // verification after discovery is complete + assert.ok( + callCount >= 1, + `Expected _resolveDiscovery to be called at least once, call count was instead ${callCount}`, + ); + assert.strictEqual(failureOccurred, false, failureMsg); + }); + }); + test('pytest execution adapter seg fault error handling', async () => { + resultResolver = new PythonResultResolver(testController, pytestProvider, workspaceUri); + let callCount = 0; + let failureOccurred = false; + let failureMsg = ''; + resultResolver.resolveExecution = (data, _token?) => { + // do the following asserts for each time resolveExecution is called, should be called once per test. + console.log(`pytest execution adapter seg fault error handling \n ${JSON.stringify(data)}`); + callCount = callCount + 1; + try { + if ('status' in data) { + if (data.status === 'error') { + assert.ok(data.error, "Expected errors in 'error' field"); + } else { + const indexOfTest = JSON.stringify(data.result).search('error'); + assert.notDeepEqual( + indexOfTest, + -1, + 'If payload status is not error then the individual tests should be marked as errors. This should occur on windows machines.', + ); + } + assert.ok(data.result, 'Expected results to be present'); + } + // make sure the testID is found in the results + const indexOfTest = JSON.stringify(data).search( + 'test_seg_fault.py::TestSegmentationFault::test_segfault', + ); + assert.notDeepEqual(indexOfTest, -1, 'Expected testId to be present'); + } catch (err) { + failureMsg = err ? (err as Error).toString() : ''; + failureOccurred = true; + } + }; + + const testId = `${rootPathErrorWorkspace}/test_seg_fault.py::TestSegmentationFault::test_segfault`; + const testIds: string[] = [testId]; + + // set workspace to test workspace folder + workspaceUri = Uri.parse(rootPathErrorWorkspace); + configService.getSettings(workspaceUri).testing.pytestArgs = []; + + // run pytest execution + const executionAdapter = new PytestTestExecutionAdapter(configService, resultResolver, envVarsService); + const testRun = typeMoq.Mock.ofType(); + testRun + .setup((t) => t.token) + .returns( + () => + ({ + onCancellationRequested: () => undefined, + } as any), + ); + await executionAdapter + .runTests(workspaceUri, testIds, TestRunProfileKind.Run, testRun.object, pythonExecFactory) + .finally(() => { + assert.strictEqual(callCount, 1, 'Expected _resolveExecution to be called once'); + assert.strictEqual(failureOccurred, false, failureMsg); + }); + }); + + test('resolveExecution performance test: validates efficient test result processing', async () => { + // This test validates that resolveExecution processes test results efficiently + // without expensive tree rebuilding or linear searching operations. + // + // The test ensures that processing many test results (like parameterized tests) + // remains fast and doesn't cause performance issues or stack overflow. + + // ================================================================ + // SETUP: Initialize test environment and tracking variables + // ================================================================ + resultResolver = new PythonResultResolver(testController, pytestProvider, workspaceUri); + + // Performance tracking variables + let totalCallTime = 0; + let callCount = 0; + const callTimes: number[] = []; + let treeRebuildCount = 0; + let totalSearchOperations = 0; + + // Test configuration - Moderate scale to validate efficiency + const numTestFiles = 5; // Multiple test files + const testFunctionsPerFile = 10; // Test functions per file + const totalTestItems = numTestFiles * testFunctionsPerFile; // Total test items in mock tree + const numParameterizedResults = 15; // Number of parameterized test results to process + + // ================================================================ + // MOCK: Set up spies and function wrapping to track performance + // ================================================================ + + // Mock getTestCaseNodes to track expensive tree operations + const originalGetTestCaseNodes = require('../../../client/testing/testController/common/testItemUtilities') + .getTestCaseNodes; + const getTestCaseNodesSpy = sinon.stub().callsFake((item) => { + treeRebuildCount++; + const result = originalGetTestCaseNodes(item); + // Track search operations through tree items + // Safely handle undefined results + if (result && Array.isArray(result)) { + totalSearchOperations += result.length; + } + return result || []; // Return empty array if undefined + }); + + // Replace the real function with our spy + const testItemUtilities = require('../../../client/testing/testController/common/testItemUtilities'); + testItemUtilities.getTestCaseNodes = getTestCaseNodesSpy; + + // Stub isTestItemValid to always return true for performance test + // This prevents expensive tree searches during validation + const testItemIndexStub = sinon.stub((resultResolver as any).testItemIndex, 'isTestItemValid').returns(true); + + // Wrap the _resolveExecution function to measure performance + const original_resolveExecution = resultResolver.resolveExecution.bind(resultResolver); + resultResolver.resolveExecution = (payload, runInstance) => { + const startTime = performance.now(); + callCount++; + + // Call the actual implementation + original_resolveExecution(payload, runInstance); + + const endTime = performance.now(); + const callTime = endTime - startTime; + callTimes.push(callTime); + totalCallTime += callTime; + }; + + // ================================================================ + // SETUP: Create test data that simulates realistic test scenarios + // ================================================================ + + // Create a mock TestController with the methods we need + const mockTestController = { + items: new Map(), + createTestItem: (id: string, label: string, uri?: Uri) => { + const childrenMap = new Map(); + // Add forEach method to children map to simulate TestItemCollection + (childrenMap as any).forEach = function (callback: (item: any) => void) { + Map.prototype.forEach.call(this, callback); + }; + + const mockTestItem = { + id, + label, + uri, + children: childrenMap, + parent: undefined, + canResolveChildren: false, + tags: [{ id: 'python-run' }, { id: 'python-debug' }], + }; + return mockTestItem; + }, + // Add a forEach method to simulate the problematic iteration + forEach: function (callback: (item: any) => void) { + this.items.forEach(callback); + }, + }; // Replace the testController in our resolver + (resultResolver as any).testController = mockTestController; + + // Create test controller with many test items (simulates real workspace) + for (let i = 0; i < numTestFiles; i++) { + const testItem = mockTestController.createTestItem( + `test_file_${i}`, + `Test File ${i}`, + Uri.file(`/test_${i}.py`), + ); + mockTestController.items.set(`test_file_${i}`, testItem); + + // Add child test items to each file + for (let j = 0; j < testFunctionsPerFile; j++) { + const childItem = mockTestController.createTestItem( + `test_${i}_${j}`, + `test_method_${j}`, + Uri.file(`/test_${i}.py`), + ); + testItem.children.set(`test_${i}_${j}`, childItem); + + // Set up the ID mappings that the resolver uses + resultResolver.runIdToTestItem.set(`test_${i}_${j}`, childItem as any); + resultResolver.runIdToVSid.set(`test_${i}_${j}`, `test_${i}_${j}`); + resultResolver.vsIdToRunId.set(`test_${i}_${j}`, `test_${i}_${j}`); + } + } // Create payload with multiple test results (simulates real test execution) + const testResults: Record = {}; + for (let i = 0; i < numParameterizedResults; i++) { + // Use test IDs that actually exist in our mock setup (test_0_0 through test_0_9) + testResults[`test_0_${i % testFunctionsPerFile}`] = { + test: `test_method[${i}]`, + outcome: 'success', + message: null, + traceback: null, + subtest: null, + }; + } + + const payload: ExecutionTestPayload = { + cwd: '/test', + status: 'success' as const, + error: '', + result: testResults, + }; + + const mockRunInstance = { + passed: sinon.stub(), + failed: sinon.stub(), + errored: sinon.stub(), + skipped: sinon.stub(), + }; + + // ================================================================ + // EXECUTION: Run the performance test + // ================================================================ + + const overallStartTime = performance.now(); + + // Run the resolveExecution function with test data + await resultResolver.resolveExecution(payload, mockRunInstance as any); + + const overallEndTime = performance.now(); + const totalTime = overallEndTime - overallStartTime; + + // ================================================================ + // CLEANUP: Restore original functions + // ================================================================ + testItemUtilities.getTestCaseNodes = originalGetTestCaseNodes; + testItemIndexStub.restore(); + + // ================================================================ + // ASSERT: Verify efficient performance characteristics + // ================================================================ + console.log(`\n=== PERFORMANCE RESULTS ===`); + console.log( + `Test setup: ${numTestFiles} files Γ— ${testFunctionsPerFile} test functions = ${totalTestItems} total items`, + ); + console.log(`Total execution time: ${totalTime.toFixed(2)}ms`); + console.log(`Tree operations performed: ${treeRebuildCount}`); + console.log(`Search operations: ${totalSearchOperations}`); + console.log(`Average time per call: ${(totalCallTime / callCount).toFixed(2)}ms`); + console.log(`Results processed: ${numParameterizedResults}`); + + // Basic function call verification + assert.strictEqual(callCount, 1, 'Expected resolveExecution to be called once'); + + // EFFICIENCY VERIFICATION: Ensure minimal expensive operations + assert.strictEqual( + treeRebuildCount, + 0, + 'Expected ZERO tree rebuilds - efficient implementation should use cached lookups', + ); + + assert.strictEqual( + totalSearchOperations, + 0, + 'Expected ZERO linear search operations - efficient implementation should use direct lookups', + ); + + // Performance threshold verification - should be fast + assert.ok(totalTime < 100, `Function should complete quickly, took ${totalTime}ms (should be under 100ms)`); + + // Scalability check - time should not grow significantly with more results + const timePerResult = totalTime / numParameterizedResults; + assert.ok( + timePerResult < 10, + `Time per result should be minimal: ${timePerResult.toFixed(2)}ms per result (should be under 10ms)`, + ); + }); +}); diff --git a/src/test/testing/configuration.unit.test.ts b/src/test/testing/configuration.unit.test.ts index abb57aac2309..e259587ecccd 100644 --- a/src/test/testing/configuration.unit.test.ts +++ b/src/test/testing/configuration.unit.test.ts @@ -10,7 +10,7 @@ import { IApplicationShell, ICommandManager, IWorkspaceService } from '../../cli import { IConfigurationService, IInstaller, - ITestOutputChannel, + ILogOutputChannel, IPythonSettings, Product, } from '../../client/common/types'; @@ -25,7 +25,7 @@ import { ITestsHelper, } from '../../client/testing/common/types'; import { ITestingSettings } from '../../client/testing/configuration/types'; -import { NONE_SELECTED, UnitTestConfigurationService } from '../../client/testing/configuration'; +import { UnitTestConfigurationService } from '../../client/testing/configuration'; suite('Unit Tests - ConfigurationService', () => { UNIT_TEST_PRODUCTS.forEach((product) => { @@ -61,7 +61,7 @@ suite('Unit Tests - ConfigurationService', () => { configurationService.setup((c) => c.getSettings(workspaceUri)).returns(() => pythonSettings.object); serviceContainer - .setup((c) => c.get(typeMoq.It.isValue(ITestOutputChannel))) + .setup((c) => c.get(typeMoq.It.isValue(ILogOutputChannel))) .returns(() => outputChannel.object); serviceContainer.setup((c) => c.get(typeMoq.It.isValue(IInstaller))).returns(() => installer.object); serviceContainer @@ -250,197 +250,14 @@ suite('Unit Tests - ConfigurationService', () => { expect(selectedItem).to.be.equal(undefined, 'invalid value'); appShell.verifyAll(); }); - test('Prompt to enable a test if a test framework is not enabled', async () => { - unitTestSettings.setup((u) => u.pytestEnabled).returns(() => false); - unitTestSettings.setup((u) => u.unittestEnabled).returns(() => false); - - appShell - .setup((s) => s.showInformationMessage(typeMoq.It.isAny(), typeMoq.It.isAny())) - .returns(() => Promise.resolve(undefined)) - .verifiable(typeMoq.Times.once()); - - let exceptionThrown = false; - try { - await testConfigService.target.displayTestFrameworkError(workspaceUri); - } catch (exc) { - if (exc !== NONE_SELECTED) { - throw exc; - } - exceptionThrown = true; - } - - expect(exceptionThrown).to.be.equal(true, 'Exception not thrown'); - appShell.verifyAll(); - }); - test('Prompt to select a test if a test framework is not enabled', async () => { - unitTestSettings.setup((u) => u.pytestEnabled).returns(() => false); - unitTestSettings.setup((u) => u.unittestEnabled).returns(() => false); - - appShell - .setup((s) => s.showInformationMessage(typeMoq.It.isAny(), typeMoq.It.isAny())) - .returns((_msg, option) => Promise.resolve(option)) - .verifiable(typeMoq.Times.once()); - - let exceptionThrown = false; - let selectTestRunnerInvoked = false; - try { - testConfigService.callBase = false; - testConfigService - .setup((t) => t.selectTestRunner(typeMoq.It.isAny())) - .returns(() => { - selectTestRunnerInvoked = true; - return Promise.resolve(undefined); - }); - await testConfigService.target.displayTestFrameworkError(workspaceUri); - } catch (exc) { - if (exc !== NONE_SELECTED) { - throw exc; - } - exceptionThrown = true; - } - - expect(selectTestRunnerInvoked).to.be.equal(true, 'Method not invoked'); - expect(exceptionThrown).to.be.equal(true, 'Exception not thrown'); - appShell.verifyAll(); - }); - test('Configure selected test framework and disable others', async () => { - unitTestSettings.setup((u) => u.pytestEnabled).returns(() => false); + test('Correctly returns hasConfiguredTests', () => { + let enabled = false; unitTestSettings.setup((u) => u.unittestEnabled).returns(() => false); + unitTestSettings.setup((u) => u.pytestEnabled).returns(() => enabled); - const workspaceConfig = typeMoq.Mock.ofType( - undefined, - typeMoq.MockBehavior.Strict, - ); - workspaceConfig - .setup((w) => w.get(typeMoq.It.isAny())) - .returns(() => true) - .verifiable(typeMoq.Times.once()); - workspaceService - .setup((w) => w.getConfiguration(typeMoq.It.isValue('python'), workspaceUri)) - .returns(() => workspaceConfig.object) - .verifiable(typeMoq.Times.once()); - - appShell - .setup((s) => s.showInformationMessage(typeMoq.It.isAny(), typeMoq.It.isAny())) - .returns((_msg, option) => Promise.resolve(option)) - .verifiable(typeMoq.Times.once()); - - let selectTestRunnerInvoked = false; - testConfigService.callBase = false; - testConfigService - .setup((t) => t.selectTestRunner(typeMoq.It.isAny())) - .returns(() => { - selectTestRunnerInvoked = true; - return Promise.resolve(product); - }); - - const configMgr = typeMoq.Mock.ofType( - undefined, - typeMoq.MockBehavior.Strict, - ); - factory - .setup((f) => - f.create(typeMoq.It.isValue(workspaceUri), typeMoq.It.isValue(product), typeMoq.It.isAny()), - ) - .returns(() => configMgr.object) - .verifiable(typeMoq.Times.once()); - - configMgr - .setup((c) => c.configure(typeMoq.It.isValue(workspaceUri))) - .returns(() => Promise.resolve()) - .verifiable(typeMoq.Times.once()); - configMgr - .setup((c) => c.enable()) - .returns(() => Promise.resolve()) - .verifiable(typeMoq.Times.once()); - - await testConfigService.target.displayTestFrameworkError(workspaceUri); - - expect(selectTestRunnerInvoked).to.be.equal(true, 'Select Test Runner not invoked'); - appShell.verifyAll(); - factory.verifyAll(); - configMgr.verifyAll(); - workspaceConfig.verifyAll(); - }); - test('If more than one test framework is enabled, then prompt to select a test framework', async () => { - unitTestSettings.setup((u) => u.pytestEnabled).returns(() => true); - unitTestSettings.setup((u) => u.unittestEnabled).returns(() => true); - - appShell - .setup((s) => s.showInformationMessage(typeMoq.It.isAny(), typeMoq.It.isAny())) - .returns(() => Promise.resolve(undefined)) - .verifiable(typeMoq.Times.never()); - appShell - .setup((s) => s.showQuickPick(typeMoq.It.isAny(), typeMoq.It.isAny())) - .returns(() => Promise.resolve(undefined)) - .verifiable(typeMoq.Times.once()); - - let exceptionThrown = false; - try { - await testConfigService.target.displayTestFrameworkError(workspaceUri); - } catch (exc) { - if (exc !== NONE_SELECTED) { - throw exc; - } - exceptionThrown = true; - } - - expect(exceptionThrown).to.be.equal(true, 'Exception not thrown'); - appShell.verifyAll(); - }); - test('If more than one test framework is enabled, then prompt to select a test framework and enable test, but do not configure', async () => { - unitTestSettings.setup((u) => u.pytestEnabled).returns(() => true); - unitTestSettings.setup((u) => u.unittestEnabled).returns(() => true); - - appShell - .setup((s) => s.showInformationMessage(typeMoq.It.isAny(), typeMoq.It.isAny())) - .returns((_msg, option) => Promise.resolve(option)) - .verifiable(typeMoq.Times.never()); - - let selectTestRunnerInvoked = false; - testConfigService.callBase = false; - testConfigService - .setup((t) => t.selectTestRunner(typeMoq.It.isAny())) - .returns(() => { - selectTestRunnerInvoked = true; - return Promise.resolve(product); - }); - - let enableTestInvoked = false; - testConfigService - .setup((t) => t.enableTest(typeMoq.It.isValue(workspaceUri), typeMoq.It.isValue(product))) - .returns(() => { - enableTestInvoked = true; - return Promise.resolve(); - }); - - const configMgr = typeMoq.Mock.ofType( - undefined, - typeMoq.MockBehavior.Strict, - ); - factory - .setup((f) => - f.create(typeMoq.It.isValue(workspaceUri), typeMoq.It.isValue(product), typeMoq.It.isAny()), - ) - .returns(() => configMgr.object) - .verifiable(typeMoq.Times.once()); - - configMgr - .setup((c) => c.configure(typeMoq.It.isValue(workspaceUri))) - .returns(() => Promise.resolve()) - .verifiable(typeMoq.Times.never()); - configMgr - .setup((c) => c.enable()) - .returns(() => Promise.resolve()) - .verifiable(typeMoq.Times.once()); - - await testConfigService.target.displayTestFrameworkError(workspaceUri); - - expect(selectTestRunnerInvoked).to.be.equal(true, 'Select Test Runner not invoked'); - expect(enableTestInvoked).to.be.equal(false, 'Enable Test is invoked'); - factory.verifyAll(); - appShell.verifyAll(); - configMgr.verifyAll(); + expect(testConfigService.target.hasConfiguredTests(workspaceUri)).to.equal(false); + enabled = true; + expect(testConfigService.target.hasConfiguredTests(workspaceUri)).to.equal(true); }); test('Prompt to enable and configure selected test framework', async () => { diff --git a/src/test/testing/configuration/pytestInstallationHelper.unit.test.ts b/src/test/testing/configuration/pytestInstallationHelper.unit.test.ts new file mode 100644 index 000000000000..d7a1313df591 --- /dev/null +++ b/src/test/testing/configuration/pytestInstallationHelper.unit.test.ts @@ -0,0 +1,131 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { expect } from 'chai'; +import * as sinon from 'sinon'; +import { Uri } from 'vscode'; +import * as TypeMoq from 'typemoq'; +import { IApplicationShell } from '../../../client/common/application/types'; +import { PytestInstallationHelper } from '../../../client/testing/configuration/pytestInstallationHelper'; +import * as envExtApi from '../../../client/envExt/api.internal'; + +suite('PytestInstallationHelper', () => { + let appShell: TypeMoq.IMock; + let helper: PytestInstallationHelper; + let useEnvExtensionStub: sinon.SinonStub; + let getEnvExtApiStub: sinon.SinonStub; + let getEnvironmentStub: sinon.SinonStub; + + const workspaceUri = Uri.file('/test/workspace'); + + setup(() => { + appShell = TypeMoq.Mock.ofType(); + helper = new PytestInstallationHelper(appShell.object); + + useEnvExtensionStub = sinon.stub(envExtApi, 'useEnvExtension'); + getEnvExtApiStub = sinon.stub(envExtApi, 'getEnvExtApi'); + getEnvironmentStub = sinon.stub(envExtApi, 'getEnvironment'); + }); + + teardown(() => { + sinon.restore(); + }); + + test('promptToInstallPytest should return false if user selects ignore', async () => { + appShell + .setup((a) => + a.showInformationMessage( + TypeMoq.It.isAny(), + TypeMoq.It.isAny(), + TypeMoq.It.isAny(), + TypeMoq.It.isAny(), + ), + ) + .returns(() => Promise.resolve('Ignore')) + .verifiable(TypeMoq.Times.once()); + + const result = await helper.promptToInstallPytest(workspaceUri); + + expect(result).to.be.false; + appShell.verifyAll(); + }); + + test('promptToInstallPytest should return false if user cancels', async () => { + appShell + .setup((a) => + a.showInformationMessage( + TypeMoq.It.isAny(), + TypeMoq.It.isAny(), + TypeMoq.It.isAny(), + TypeMoq.It.isAny(), + ), + ) + .returns(() => Promise.resolve(undefined)) + .verifiable(TypeMoq.Times.once()); + + const result = await helper.promptToInstallPytest(workspaceUri); + + expect(result).to.be.false; + appShell.verifyAll(); + }); + + test('isEnvExtensionAvailable should return result from useEnvExtension', () => { + useEnvExtensionStub.returns(true); + + const result = helper.isEnvExtensionAvailable(); + + expect(result).to.be.true; + expect(useEnvExtensionStub.calledOnce).to.be.true; + }); + + test('promptToInstallPytest should return false if env extension not available', async () => { + useEnvExtensionStub.returns(false); + + appShell + .setup((a) => + a.showInformationMessage( + TypeMoq.It.isAny(), + TypeMoq.It.isAny(), + TypeMoq.It.isAny(), + TypeMoq.It.isAny(), + ), + ) + .returns(() => Promise.resolve('Install pytest')) + .verifiable(TypeMoq.Times.once()); + + const result = await helper.promptToInstallPytest(workspaceUri); + + expect(result).to.be.false; + appShell.verifyAll(); + }); + + test('promptToInstallPytest should attempt installation when env extension is available', async () => { + useEnvExtensionStub.returns(true); + + const mockEnvironment = { envId: { id: 'test-env', managerId: 'test-manager' } }; + const mockEnvExtApi = { + managePackages: sinon.stub().resolves(), + }; + + getEnvExtApiStub.resolves(mockEnvExtApi); + getEnvironmentStub.resolves(mockEnvironment); + + appShell + .setup((a) => + a.showInformationMessage( + TypeMoq.It.is((msg: string) => msg.includes('pytest selected but not installed')), + TypeMoq.It.isAny(), + TypeMoq.It.isAny(), + TypeMoq.It.isAny(), + ), + ) + .returns(() => Promise.resolve('Install pytest')) + .verifiable(TypeMoq.Times.once()); + + const result = await helper.promptToInstallPytest(workspaceUri); + + expect(result).to.be.true; + expect(mockEnvExtApi.managePackages.calledOnceWithExactly(mockEnvironment, { install: ['pytest'] })).to.be.true; + appShell.verifyAll(); + }); +}); diff --git a/src/test/testing/configurationFactory.unit.test.ts b/src/test/testing/configurationFactory.unit.test.ts index 74f7dd0da19b..493dfcc00b95 100644 --- a/src/test/testing/configurationFactory.unit.test.ts +++ b/src/test/testing/configurationFactory.unit.test.ts @@ -7,14 +7,14 @@ import { expect, use } from 'chai'; import * as chaiAsPromised from 'chai-as-promised'; import * as typeMoq from 'typemoq'; import { OutputChannel, Uri } from 'vscode'; -import { IInstaller, ITestOutputChannel, Product } from '../../client/common/types'; +import { IInstaller, ILogOutputChannel, Product } from '../../client/common/types'; import { IServiceContainer } from '../../client/ioc/types'; import { ITestConfigSettingsService, ITestConfigurationManagerFactory } from '../../client/testing/common/types'; import { TestConfigurationManagerFactory } from '../../client/testing/configurationFactory'; import * as pytest from '../../client/testing/configuration/pytest/testConfigurationManager'; import * as unittest from '../../client/testing/configuration/unittest/testConfigurationManager'; -use(chaiAsPromised); +use(chaiAsPromised.default); suite('Unit Tests - ConfigurationManagerFactory', () => { let factory: ITestConfigurationManagerFactory; @@ -24,9 +24,7 @@ suite('Unit Tests - ConfigurationManagerFactory', () => { const installer = typeMoq.Mock.ofType(); const testConfigService = typeMoq.Mock.ofType(); - serviceContainer - .setup((c) => c.get(typeMoq.It.isValue(ITestOutputChannel))) - .returns(() => outputChannel.object); + serviceContainer.setup((c) => c.get(typeMoq.It.isValue(ILogOutputChannel))).returns(() => outputChannel.object); serviceContainer.setup((c) => c.get(typeMoq.It.isValue(IInstaller))).returns(() => installer.object); serviceContainer .setup((c) => c.get(typeMoq.It.isValue(ITestConfigSettingsService))) diff --git a/src/test/testing/mocks.ts b/src/test/testing/mocks.ts deleted file mode 100644 index dec62c23e747..000000000000 --- a/src/test/testing/mocks.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { EventEmitter } from 'events'; -import { injectable } from 'inversify'; - -import { IUnitTestSocketServer } from '../../client/testing/common/types'; - -@injectable() -export class MockUnitTestSocketServer extends EventEmitter implements IUnitTestSocketServer { - private results: {}[] = []; - public reset() { - this.removeAllListeners(); - } - public addResults(results: {}[]) { - this.results.push(...results); - } - public async start(options: { port: number; host: string } = { port: 0, host: 'localhost' }): Promise { - this.results.forEach((result) => { - this.emit('result', result); - }); - this.results = []; - return typeof options.port === 'number' ? options.port! : 0; - } - - public stop(): void {} - - public dispose() {} -} diff --git a/src/test/testing/serviceRegistry.ts b/src/test/testing/serviceRegistry.ts index ddd1cde115d1..231716b653ba 100644 --- a/src/test/testing/serviceRegistry.ts +++ b/src/test/testing/serviceRegistry.ts @@ -9,10 +9,9 @@ import { IProcessServiceFactory } from '../../client/common/process/types'; import { IInterpreterHelper } from '../../client/interpreter/contracts'; import { InterpreterHelper } from '../../client/interpreter/helpers'; import { TestsHelper } from '../../client/testing/common/testUtils'; -import { ITestsHelper, IUnitTestSocketServer } from '../../client/testing/common/types'; +import { ITestsHelper } from '../../client/testing/common/types'; import { getPythonSemVer } from '../common'; import { IocContainer } from '../serviceRegistry'; -import { MockUnitTestSocketServer } from './mocks'; export class UnitTestIocContainer extends IocContainer { public async getPythonMajorVersion(resource: Uri): Promise { @@ -32,8 +31,4 @@ export class UnitTestIocContainer extends IocContainer { public registerInterpreterStorageTypes(): void { this.serviceManager.add(IInterpreterHelper, InterpreterHelper); } - - public registerMockUnitTestSocketServer(): void { - this.serviceManager.addSingleton(IUnitTestSocketServer, MockUnitTestSocketServer); - } } diff --git a/src/test/testing/testController/common/buildErrorNodeOptions.unit.test.ts b/src/test/testing/testController/common/buildErrorNodeOptions.unit.test.ts new file mode 100644 index 000000000000..643ea17903e6 --- /dev/null +++ b/src/test/testing/testController/common/buildErrorNodeOptions.unit.test.ts @@ -0,0 +1,111 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { expect } from 'chai'; +import { Uri } from 'vscode'; +import { buildErrorNodeOptions } from '../../../../client/testing/testController/common/utils'; + +suite('buildErrorNodeOptions - missing module detection', () => { + const workspaceUri = Uri.file('/test/workspace'); + + test('Should detect pytest ModuleNotFoundError and show missing module label', () => { + const errorMessage = + 'Traceback (most recent call last):\n File "", line 1, in \n import pytest\nModuleNotFoundError: No module named \'pytest\''; + + const result = buildErrorNodeOptions(workspaceUri, errorMessage, 'pytest'); + + expect(result.label).to.equal('Missing Module: pytest [workspace]'); + expect(result.error).to.equal( + "The module 'pytest' is not installed in the selected Python environment. Please install it to enable test discovery.", + ); + }); + + test('Should detect pytest ImportError and show missing module label', () => { + const errorMessage = 'ImportError: No module named pytest'; + + const result = buildErrorNodeOptions(workspaceUri, errorMessage, 'pytest'); + + expect(result.label).to.equal('Missing Module: pytest [workspace]'); + expect(result.error).to.equal( + "The module 'pytest' is not installed in the selected Python environment. Please install it to enable test discovery.", + ); + }); + + test('Should detect other missing modules and show module name in label', () => { + const errorMessage = + "bob\\test_bob.py:3: in \n import requests\nE ModuleNotFoundError: No module named 'requests'\n=========================== short test summary info"; + + const result = buildErrorNodeOptions(workspaceUri, errorMessage, 'pytest'); + + expect(result.label).to.equal('Missing Module: requests [workspace]'); + expect(result.error).to.equal( + "The module 'requests' is not installed in the selected Python environment. Please install it to enable test discovery.", + ); + }); + + test('Should detect missing module with double quotes', () => { + const errorMessage = 'ModuleNotFoundError: No module named "numpy"'; + + const result = buildErrorNodeOptions(workspaceUri, errorMessage, 'pytest'); + + expect(result.label).to.equal('Missing Module: numpy [workspace]'); + expect(result.error).to.equal( + "The module 'numpy' is not installed in the selected Python environment. Please install it to enable test discovery.", + ); + }); + + test('Should use generic error for non-module-related errors', () => { + const errorMessage = 'Some other error occurred'; + + const result = buildErrorNodeOptions(workspaceUri, errorMessage, 'pytest'); + + expect(result.label).to.equal('pytest Discovery Error [workspace]'); + expect(result.error).to.equal('Some other error occurred'); + }); + + test('Should detect missing module for unittest errors', () => { + const errorMessage = "ModuleNotFoundError: No module named 'pandas'"; + + const result = buildErrorNodeOptions(workspaceUri, errorMessage, 'unittest'); + + expect(result.label).to.equal('Missing Module: pandas [workspace]'); + expect(result.error).to.equal( + "The module 'pandas' is not installed in the selected Python environment. Please install it to enable test discovery.", + ); + }); + + test('Should use generic error for unittest non-module errors', () => { + const errorMessage = 'Some other error occurred'; + + const result = buildErrorNodeOptions(workspaceUri, errorMessage, 'unittest'); + + expect(result.label).to.equal('Unittest Discovery Error [workspace]'); + expect(result.error).to.equal('Some other error occurred'); + }); + + test('Should use project name in label when projectName is provided', () => { + const errorMessage = 'Some error occurred'; + + const result = buildErrorNodeOptions(workspaceUri, errorMessage, 'unittest', 'my-project'); + + expect(result.label).to.equal('Unittest Discovery Error [my-project]'); + expect(result.error).to.equal('Some error occurred'); + }); + + test('Should use project name in label for pytest when projectName is provided', () => { + const errorMessage = 'Some error occurred'; + + const result = buildErrorNodeOptions(workspaceUri, errorMessage, 'pytest', 'ada'); + + expect(result.label).to.equal('pytest Discovery Error [ada]'); + expect(result.error).to.equal('Some error occurred'); + }); + + test('Should use folder name when projectName is undefined', () => { + const errorMessage = 'Some error occurred'; + + const result = buildErrorNodeOptions(workspaceUri, errorMessage, 'unittest', undefined); + + expect(result.label).to.equal('Unittest Discovery Error [workspace]'); + }); +}); diff --git a/src/test/testing/testController/common/projectTestExecution.unit.test.ts b/src/test/testing/testController/common/projectTestExecution.unit.test.ts new file mode 100644 index 000000000000..1cce2d1a8ce0 --- /dev/null +++ b/src/test/testing/testController/common/projectTestExecution.unit.test.ts @@ -0,0 +1,740 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { expect } from 'chai'; +import * as sinon from 'sinon'; +import * as typemoq from 'typemoq'; +import { + CancellationToken, + CancellationTokenSource, + TestRun, + TestRunProfile, + TestRunProfileKind, + TestRunRequest, + Uri, +} from 'vscode'; +import { + createMockDependencies, + createMockProjectAdapter, + createMockTestItem, + createMockTestItemWithoutUri, + createMockTestRun, +} from '../testMocks'; +import { + executeTestsForProject, + executeTestsForProjects, + findProjectForTestItem, + getTestCaseNodesRecursive, + groupTestItemsByProject, + setupCoverageForProjects, +} from '../../../../client/testing/testController/common/projectTestExecution'; +import * as telemetry from '../../../../client/telemetry'; +import * as envExtApi from '../../../../client/envExt/api.internal'; + +suite('Project Test Execution', () => { + let sandbox: sinon.SinonSandbox; + let useEnvExtensionStub: sinon.SinonStub; + + setup(() => { + sandbox = sinon.createSandbox(); + // Default to disabled env extension for path-based fallback tests + useEnvExtensionStub = sandbox.stub(envExtApi, 'useEnvExtension').returns(false); + }); + + teardown(() => { + sandbox.restore(); + }); + + // ===== findProjectForTestItem Tests ===== + + suite('findProjectForTestItem', () => { + test('should return undefined when test item has no URI', async () => { + // Mock + const item = createMockTestItemWithoutUri('test1'); + const projects = [createMockProjectAdapter({ projectPath: '/workspace/proj', projectName: 'proj' })]; + + // Run + const result = await findProjectForTestItem(item, projects); + + // Assert + expect(result).to.be.undefined; + }); + + test('should return matching project when item path is within project directory', async () => { + // Mock + const item = createMockTestItem('test1', '/workspace/proj/tests/test_file.py'); + const project = createMockProjectAdapter({ projectPath: '/workspace/proj', projectName: 'proj' }); + + // Run + const result = await findProjectForTestItem(item, [project]); + + // Assert + expect(result).to.equal(project); + }); + + test('should return undefined when item path is outside all project directories', async () => { + // Mock + const item = createMockTestItem('test1', '/other/path/test.py'); + const project = createMockProjectAdapter({ projectPath: '/workspace/proj', projectName: 'proj' }); + + // Run + const result = await findProjectForTestItem(item, [project]); + + // Assert + expect(result).to.be.undefined; + }); + + test('should return most specific (deepest) project when nested projects exist', async () => { + // Mock - parent and child project with overlapping paths + const item = createMockTestItem('test1', '/workspace/parent/child/tests/test.py'); + const parentProject = createMockProjectAdapter({ projectPath: '/workspace/parent', projectName: 'parent' }); + const childProject = createMockProjectAdapter({ + projectPath: '/workspace/parent/child', + projectName: 'child', + }); + + // Run + const result = await findProjectForTestItem(item, [parentProject, childProject]); + + // Assert - should match child (longer path) not parent + expect(result).to.equal(childProject); + }); + + test('should return most specific project regardless of input order', async () => { + // Mock - same as above but different order + const item = createMockTestItem('test1', '/workspace/parent/child/tests/test.py'); + const parentProject = createMockProjectAdapter({ projectPath: '/workspace/parent', projectName: 'parent' }); + const childProject = createMockProjectAdapter({ + projectPath: '/workspace/parent/child', + projectName: 'child', + }); + + // Run - pass child first, then parent + const result = await findProjectForTestItem(item, [childProject, parentProject]); + + // Assert - order shouldn't affect result + expect(result).to.equal(childProject); + }); + + test('should match item at project root level', async () => { + // Mock + const item = createMockTestItem('test1', '/workspace/proj/test.py'); + const project = createMockProjectAdapter({ projectPath: '/workspace/proj', projectName: 'proj' }); + + // Run + const result = await findProjectForTestItem(item, [project]); + + // Assert + expect(result).to.equal(project); + }); + + test('should use env extension API when available', async () => { + // Enable env extension + useEnvExtensionStub.returns(true); + + // Mock the env extension API + const item = createMockTestItem('test1', '/workspace/proj/tests/test_file.py'); + const project = createMockProjectAdapter({ projectPath: '/workspace/proj', projectName: 'proj' }); + + const mockEnvApi = { + getPythonProject: sandbox.stub().returns({ uri: project.projectUri }), + }; + sandbox.stub(envExtApi, 'getEnvExtApi').resolves(mockEnvApi as any); + + // Run + const result = await findProjectForTestItem(item, [project]); + + // Assert + expect(result).to.equal(project); + expect(mockEnvApi.getPythonProject.calledOnceWith(item.uri)).to.be.true; + }); + + test('should fall back to path matching when env extension API is unavailable', async () => { + // Env extension enabled but throws + useEnvExtensionStub.returns(true); + sandbox.stub(envExtApi, 'getEnvExtApi').rejects(new Error('API unavailable')); + + // Mock + const item = createMockTestItem('test1', '/workspace/proj/tests/test_file.py'); + const project = createMockProjectAdapter({ projectPath: '/workspace/proj', projectName: 'proj' }); + + // Run + const result = await findProjectForTestItem(item, [project]); + + // Assert - should still work via fallback + expect(result).to.equal(project); + }); + }); + + // ===== groupTestItemsByProject Tests ===== + + suite('groupTestItemsByProject', () => { + test('should group single test item to its matching project', async () => { + // Mock + const item = createMockTestItem('test1', '/workspace/proj/test.py'); + const project = createMockProjectAdapter({ projectPath: '/workspace/proj', projectName: 'proj' }); + + // Run + const result = await groupTestItemsByProject([item], [project]); + + // Assert + expect(result.size).to.equal(1); + const entry = Array.from(result.values())[0]; + expect(entry.project).to.equal(project); + expect(entry.items).to.deep.equal([item]); + }); + + test('should aggregate multiple items belonging to same project', async () => { + // Mock + const item1 = createMockTestItem('test1', '/workspace/proj/tests/test1.py'); + const item2 = createMockTestItem('test2', '/workspace/proj/tests/test2.py'); + const item3 = createMockTestItem('test3', '/workspace/proj/test3.py'); + const project = createMockProjectAdapter({ projectPath: '/workspace/proj', projectName: 'proj' }); + + // Run + const result = await groupTestItemsByProject([item1, item2, item3], [project]); + + // Assert - use Set for order-agnostic comparison + expect(result.size).to.equal(1); + const entry = Array.from(result.values())[0]; + expect(entry.items).to.have.length(3); + expect(new Set(entry.items)).to.deep.equal(new Set([item1, item2, item3])); + }); + + test('should separate items into groups by their owning project', async () => { + // Mock + const item1 = createMockTestItem('test1', '/workspace/proj1/test.py'); + const item2 = createMockTestItem('test2', '/workspace/proj2/test.py'); + const item3 = createMockTestItem('test3', '/workspace/proj1/other_test.py'); + const proj1 = createMockProjectAdapter({ projectPath: '/workspace/proj1', projectName: 'proj1' }); + const proj2 = createMockProjectAdapter({ projectPath: '/workspace/proj2', projectName: 'proj2' }); + + // Run + const result = await groupTestItemsByProject([item1, item2, item3], [proj1, proj2]); + + // Assert - use Set for order-agnostic comparison + expect(result.size).to.equal(2); + const proj1Entry = result.get(proj1.projectUri.toString()); + const proj2Entry = result.get(proj2.projectUri.toString()); + expect(proj1Entry?.items).to.have.length(2); + expect(new Set(proj1Entry?.items)).to.deep.equal(new Set([item1, item3])); + expect(proj2Entry?.items).to.deep.equal([item2]); + }); + + test('should return empty map when no test items provided', async () => { + // Mock + const project = createMockProjectAdapter({ projectPath: '/workspace/proj', projectName: 'proj' }); + + // Run + const result = await groupTestItemsByProject([], [project]); + + // Assert + expect(result.size).to.equal(0); + }); + + test('should exclude items that do not match any project path', async () => { + // Mock + const item = createMockTestItem('test1', '/other/path/test.py'); + const project = createMockProjectAdapter({ projectPath: '/workspace/proj', projectName: 'proj' }); + + // Run + const result = await groupTestItemsByProject([item], [project]); + + // Assert + expect(result.size).to.equal(0); + }); + + test('should assign item to most specific (deepest) project for nested paths', async () => { + // Mock + const item = createMockTestItem('test1', '/workspace/parent/child/test.py'); + const parentProject = createMockProjectAdapter({ projectPath: '/workspace/parent', projectName: 'parent' }); + const childProject = createMockProjectAdapter({ + projectPath: '/workspace/parent/child', + projectName: 'child', + }); + + // Run + const result = await groupTestItemsByProject([item], [parentProject, childProject]); + + // Assert + expect(result.size).to.equal(1); + const entry = result.get(childProject.projectUri.toString()); + expect(entry?.project).to.equal(childProject); + expect(entry?.items).to.deep.equal([item]); + }); + + test('should omit projects that have no matching test items', async () => { + // Mock + const item = createMockTestItem('test1', '/workspace/proj1/test.py'); + const proj1 = createMockProjectAdapter({ projectPath: '/workspace/proj1', projectName: 'proj1' }); + const proj2 = createMockProjectAdapter({ projectPath: '/workspace/proj2', projectName: 'proj2' }); + + // Run + const result = await groupTestItemsByProject([item], [proj1, proj2]); + + // Assert + expect(result.size).to.equal(1); + expect(result.has(proj1.projectUri.toString())).to.be.true; + expect(result.has(proj2.projectUri.toString())).to.be.false; + }); + }); + + // ===== getTestCaseNodesRecursive Tests ===== + + suite('getTestCaseNodesRecursive', () => { + test('should return single item when it is a leaf node with no children', () => { + // Mock + const item = createMockTestItem('test_func', '/test.py'); + + // Run + const result = getTestCaseNodesRecursive(item); + + // Assert + expect(result).to.deep.equal([item]); + }); + + test('should return all leaf nodes from single-level nested structure', () => { + // Mock + const leaf1 = createMockTestItem('test_method1', '/test.py'); + const leaf2 = createMockTestItem('test_method2', '/test.py'); + const classItem = createMockTestItem('TestClass', '/test.py', [leaf1, leaf2]); + + // Run + const result = getTestCaseNodesRecursive(classItem); + + // Assert - use Set for order-agnostic comparison + expect(result).to.have.length(2); + expect(new Set(result)).to.deep.equal(new Set([leaf1, leaf2])); + }); + + test('should traverse deeply nested structure to find all leaf nodes', () => { + // Mock - 3 levels deep: file β†’ class β†’ inner class β†’ test + const leaf1 = createMockTestItem('test1', '/test.py'); + const leaf2 = createMockTestItem('test2', '/test.py'); + const innerClass = createMockTestItem('InnerClass', '/test.py', [leaf2]); + const outerClass = createMockTestItem('OuterClass', '/test.py', [leaf1, innerClass]); + const fileItem = createMockTestItem('test_file.py', '/test.py', [outerClass]); + + // Run + const result = getTestCaseNodesRecursive(fileItem); + + // Assert - use Set for order-agnostic comparison + expect(result).to.have.length(2); + expect(new Set(result)).to.deep.equal(new Set([leaf1, leaf2])); + }); + + test('should collect leaves from multiple sibling branches', () => { + // Mock - multiple test classes at same level + const leaf1 = createMockTestItem('test1', '/test.py'); + const leaf2 = createMockTestItem('test2', '/test.py'); + const leaf3 = createMockTestItem('test3', '/test.py'); + const class1 = createMockTestItem('Class1', '/test.py', [leaf1]); + const class2 = createMockTestItem('Class2', '/test.py', [leaf2, leaf3]); + const fileItem = createMockTestItem('test_file.py', '/test.py', [class1, class2]); + + // Run + const result = getTestCaseNodesRecursive(fileItem); + + // Assert - use Set for order-agnostic comparison + expect(result).to.have.length(3); + expect(new Set(result)).to.deep.equal(new Set([leaf1, leaf2, leaf3])); + }); + }); + + // ===== executeTestsForProject Tests ===== + + suite('executeTestsForProject', () => { + test('should call executionAdapter.runTests with project URI and mapped test IDs', async () => { + // Mock + const project = createMockProjectAdapter({ projectPath: '/workspace/proj', projectName: 'proj' }); + project.resultResolver.vsIdToRunId.set('test1', 'test_file.py::test1'); + const testItem = createMockTestItem('test1', '/workspace/proj/test.py'); + const runMock = createMockTestRun(); + const request = { profile: { kind: TestRunProfileKind.Run } } as TestRunRequest; + const deps = createMockDependencies(); + + // Run + await executeTestsForProject(project, [testItem], runMock.object, request, deps); + + // Assert + expect(project.executionAdapterStub.calledOnce).to.be.true; + const callArgs = project.executionAdapterStub.firstCall.args; + expect(callArgs[0].fsPath).to.equal(project.projectUri.fsPath); // uri + expect(callArgs[1]).to.deep.equal(['test_file.py::test1']); // testCaseIds + expect(callArgs[7]).to.equal(project); // project + }); + + test('should mark all leaf test items as started in the test run', async () => { + // Mock + const project = createMockProjectAdapter({ projectPath: '/workspace/proj', projectName: 'proj' }); + project.resultResolver.vsIdToRunId.set('test1', 'runId1'); + project.resultResolver.vsIdToRunId.set('test2', 'runId2'); + const item1 = createMockTestItem('test1', '/workspace/proj/test.py'); + const item2 = createMockTestItem('test2', '/workspace/proj/test.py'); + const runMock = createMockTestRun(); + const request = { profile: { kind: TestRunProfileKind.Run } } as TestRunRequest; + const deps = createMockDependencies(); + + // Run + await executeTestsForProject(project, [item1, item2], runMock.object, request, deps); + + // Assert - both items marked as started + runMock.verify((r) => r.started(item1), typemoq.Times.once()); + runMock.verify((r) => r.started(item2), typemoq.Times.once()); + }); + + test('should resolve test IDs via resultResolver.vsIdToRunId mapping', async () => { + // Mock + const project = createMockProjectAdapter({ projectPath: '/workspace/proj', projectName: 'proj' }); + project.resultResolver.vsIdToRunId.set('test1', 'path/to/test1'); + project.resultResolver.vsIdToRunId.set('test2', 'path/to/test2'); + const item1 = createMockTestItem('test1', '/workspace/proj/test.py'); + const item2 = createMockTestItem('test2', '/workspace/proj/test.py'); + const runMock = createMockTestRun(); + const request = { profile: { kind: TestRunProfileKind.Run } } as TestRunRequest; + const deps = createMockDependencies(); + + // Run + await executeTestsForProject(project, [item1, item2], runMock.object, request, deps); + + // Assert - use Set for order-agnostic comparison + const passedTestIds = project.executionAdapterStub.firstCall.args[1] as string[]; + expect(new Set(passedTestIds)).to.deep.equal(new Set(['path/to/test1', 'path/to/test2'])); + }); + + test('should skip execution when no items have vsIdToRunId mappings', async () => { + // Mock - no mappings set, so lookups return undefined + const project = createMockProjectAdapter({ projectPath: '/workspace/proj', projectName: 'proj' }); + const item = createMockTestItem('unmapped_test', '/workspace/proj/test.py'); + const runMock = createMockTestRun(); + const request = { profile: { kind: TestRunProfileKind.Run } } as TestRunRequest; + const deps = createMockDependencies(); + + // Run + await executeTestsForProject(project, [item], runMock.object, request, deps); + + // Assert - execution adapter never called + expect(project.executionAdapterStub.called).to.be.false; + }); + + test('should recursively expand nested test items to find leaf nodes', async () => { + // Mock - class containing two test methods + const project = createMockProjectAdapter({ projectPath: '/workspace/proj', projectName: 'proj' }); + const leaf1 = createMockTestItem('test1', '/workspace/proj/test.py'); + const leaf2 = createMockTestItem('test2', '/workspace/proj/test.py'); + const classItem = createMockTestItem('TestClass', '/workspace/proj/test.py', [leaf1, leaf2]); + project.resultResolver.vsIdToRunId.set('test1', 'runId1'); + project.resultResolver.vsIdToRunId.set('test2', 'runId2'); + const runMock = createMockTestRun(); + const request = { profile: { kind: TestRunProfileKind.Run } } as TestRunRequest; + const deps = createMockDependencies(); + + // Run + await executeTestsForProject(project, [classItem], runMock.object, request, deps); + + // Assert - leaf nodes marked as started, not the parent class + runMock.verify((r) => r.started(leaf1), typemoq.Times.once()); + runMock.verify((r) => r.started(leaf2), typemoq.Times.once()); + const passedTestIds = project.executionAdapterStub.firstCall.args[1] as string[]; + expect(passedTestIds).to.have.length(2); + }); + }); + + // ===== executeTestsForProjects Tests ===== + + suite('executeTestsForProjects', () => { + let telemetryStub: sinon.SinonStub; + + setup(() => { + telemetryStub = sandbox.stub(telemetry, 'sendTelemetryEvent'); + }); + + test('should return immediately when empty projects array provided', async () => { + // Mock + const runMock = createMockTestRun(); + const token = new CancellationTokenSource().token; + const request = { profile: { kind: TestRunProfileKind.Run } } as TestRunRequest; + const deps = createMockDependencies(); + + // Run + await executeTestsForProjects([], [], runMock.object, request, token, deps); + + // Assert - no telemetry sent since no projects executed + expect(telemetryStub.called).to.be.false; + }); + + test('should skip execution when cancellation requested before start', async () => { + // Mock + const project = createMockProjectAdapter({ projectPath: '/workspace/proj', projectName: 'proj' }); + const item = createMockTestItem('test1', '/workspace/proj/test.py'); + const runMock = createMockTestRun(); + const tokenSource = new CancellationTokenSource(); + tokenSource.cancel(); // Pre-cancel + const request = { profile: { kind: TestRunProfileKind.Run } } as TestRunRequest; + const deps = createMockDependencies(); + + // Run + await executeTestsForProjects([project], [item], runMock.object, request, tokenSource.token, deps); + + // Assert - execution adapter never called + expect(project.executionAdapterStub.called).to.be.false; + }); + + test('should execute tests for each project when multiple projects provided', async () => { + // Mock + const proj1 = createMockProjectAdapter({ projectPath: '/workspace/proj1', projectName: 'proj1' }); + const proj2 = createMockProjectAdapter({ projectPath: '/workspace/proj2', projectName: 'proj2' }); + proj1.resultResolver.vsIdToRunId.set('test1', 'runId1'); + proj2.resultResolver.vsIdToRunId.set('test2', 'runId2'); + const item1 = createMockTestItem('test1', '/workspace/proj1/test.py'); + const item2 = createMockTestItem('test2', '/workspace/proj2/test.py'); + const runMock = createMockTestRun(); + const token = new CancellationTokenSource().token; + const request = { profile: { kind: TestRunProfileKind.Run } } as TestRunRequest; + const deps = createMockDependencies(); + + // Run + await executeTestsForProjects([proj1, proj2], [item1, item2], runMock.object, request, token, deps); + + // Assert - both projects had their execution adapters called + expect(proj1.executionAdapterStub.calledOnce).to.be.true; + expect(proj2.executionAdapterStub.calledOnce).to.be.true; + }); + + test('should emit telemetry event for each project execution', async () => { + // Mock + const proj1 = createMockProjectAdapter({ projectPath: '/workspace/proj1', projectName: 'proj1' }); + const proj2 = createMockProjectAdapter({ projectPath: '/workspace/proj2', projectName: 'proj2' }); + proj1.resultResolver.vsIdToRunId.set('test1', 'runId1'); + proj2.resultResolver.vsIdToRunId.set('test2', 'runId2'); + const item1 = createMockTestItem('test1', '/workspace/proj1/test.py'); + const item2 = createMockTestItem('test2', '/workspace/proj2/test.py'); + const runMock = createMockTestRun(); + const token = new CancellationTokenSource().token; + const request = { profile: { kind: TestRunProfileKind.Run } } as TestRunRequest; + const deps = createMockDependencies(); + + // Run + await executeTestsForProjects([proj1, proj2], [item1, item2], runMock.object, request, token, deps); + + // Assert - telemetry sent twice (once per project) + expect(telemetryStub.callCount).to.equal(2); + }); + + test('should stop processing remaining projects when cancellation requested mid-execution', async () => { + // Mock + const tokenSource = new CancellationTokenSource(); + const proj1 = createMockProjectAdapter({ projectPath: '/workspace/proj1', projectName: 'proj1' }); + const proj2 = createMockProjectAdapter({ projectPath: '/workspace/proj2', projectName: 'proj2' }); + // First project triggers cancellation during its execution + proj1.executionAdapterStub.callsFake(async () => { + tokenSource.cancel(); + }); + proj1.resultResolver.vsIdToRunId.set('test1', 'runId1'); + proj2.resultResolver.vsIdToRunId.set('test2', 'runId2'); + const item1 = createMockTestItem('test1', '/workspace/proj1/test.py'); + const item2 = createMockTestItem('test2', '/workspace/proj2/test.py'); + const runMock = createMockTestRun(); + const request = { profile: { kind: TestRunProfileKind.Run } } as TestRunRequest; + const deps = createMockDependencies(); + + // Run + await executeTestsForProjects( + [proj1, proj2], + [item1, item2], + runMock.object, + request, + tokenSource.token, + deps, + ); + + // Assert - first project executed, second may be skipped due to cancellation check + expect(proj1.executionAdapterStub.calledOnce).to.be.true; + }); + + test('should continue executing remaining projects when one project fails', async () => { + // Mock + const proj1 = createMockProjectAdapter({ projectPath: '/workspace/proj1', projectName: 'proj1' }); + const proj2 = createMockProjectAdapter({ projectPath: '/workspace/proj2', projectName: 'proj2' }); + proj1.executionAdapterStub.rejects(new Error('Execution failed')); + proj1.resultResolver.vsIdToRunId.set('test1', 'runId1'); + proj2.resultResolver.vsIdToRunId.set('test2', 'runId2'); + const item1 = createMockTestItem('test1', '/workspace/proj1/test.py'); + const item2 = createMockTestItem('test2', '/workspace/proj2/test.py'); + const runMock = createMockTestRun(); + const token = new CancellationTokenSource().token; + const request = { profile: { kind: TestRunProfileKind.Run } } as TestRunRequest; + const deps = createMockDependencies(); + + // Run - should not throw + await executeTestsForProjects([proj1, proj2], [item1, item2], runMock.object, request, token, deps); + + // Assert - second project still executed despite first failing + expect(proj2.executionAdapterStub.calledOnce).to.be.true; + }); + + test('should configure loadDetailedCoverage callback when run profile is Coverage', async () => { + // Mock + const project = createMockProjectAdapter({ projectPath: '/workspace/proj', projectName: 'proj' }); + project.resultResolver.vsIdToRunId.set('test1', 'runId1'); + const item = createMockTestItem('test1', '/workspace/proj/test.py'); + const runMock = createMockTestRun(); + const token = new CancellationTokenSource().token; + const profileMock = ({ + kind: TestRunProfileKind.Coverage, + loadDetailedCoverage: undefined, + } as unknown) as TestRunProfile; + const request = { profile: profileMock } as TestRunRequest; + const deps = createMockDependencies(); + + // Run + await executeTestsForProjects([project], [item], runMock.object, request, token, deps); + + // Assert - loadDetailedCoverage callback was configured + expect(profileMock.loadDetailedCoverage).to.not.be.undefined; + }); + + test('should include debugging=true in telemetry when run profile is Debug', async () => { + // Mock + const project = createMockProjectAdapter({ projectPath: '/workspace/proj', projectName: 'proj' }); + project.resultResolver.vsIdToRunId.set('test1', 'runId1'); + const item = createMockTestItem('test1', '/workspace/proj/test.py'); + const runMock = createMockTestRun(); + const token = new CancellationTokenSource().token; + const request = { profile: { kind: TestRunProfileKind.Debug } } as TestRunRequest; + const deps = createMockDependencies(); + + // Run + await executeTestsForProjects([project], [item], runMock.object, request, token, deps); + + // Assert - telemetry contains debugging=true + expect(telemetryStub.calledOnce).to.be.true; + const telemetryProps = telemetryStub.firstCall.args[2]; + expect(telemetryProps.debugging).to.be.true; + }); + }); + + // ===== setupCoverageForProjects Tests ===== + + suite('setupCoverageForProjects', () => { + test('should configure loadDetailedCoverage callback when profile kind is Coverage', () => { + // Mock + const project = createMockProjectAdapter({ projectPath: '/workspace/proj', projectName: 'proj' }); + const profileMock = ({ + kind: TestRunProfileKind.Coverage, + loadDetailedCoverage: undefined, + } as unknown) as TestRunProfile; + const request = { profile: profileMock } as TestRunRequest; + + // Run + setupCoverageForProjects(request, [project]); + + // Assert + expect(profileMock.loadDetailedCoverage).to.be.a('function'); + }); + + test('should leave loadDetailedCoverage undefined when profile kind is Run', () => { + // Mock + const project = createMockProjectAdapter({ projectPath: '/workspace/proj', projectName: 'proj' }); + const profileMock = ({ + kind: TestRunProfileKind.Run, + loadDetailedCoverage: undefined, + } as unknown) as TestRunProfile; + const request = { profile: profileMock } as TestRunRequest; + + // Run + setupCoverageForProjects(request, [project]); + + // Assert + expect(profileMock.loadDetailedCoverage).to.be.undefined; + }); + + test('should return coverage data from detailedCoverageMap when loadDetailedCoverage is called', async () => { + // Mock + const project = createMockProjectAdapter({ projectPath: '/workspace/proj', projectName: 'proj' }); + const mockCoverageDetails = [{ line: 1, executed: true }]; + // Use Uri.fsPath as the key to match the implementation's lookup + const fileUri = Uri.file('/workspace/proj/file.py'); + project.resultResolver.detailedCoverageMap.set(fileUri.fsPath, mockCoverageDetails as any); + const profileMock = ({ + kind: TestRunProfileKind.Coverage, + loadDetailedCoverage: undefined, + } as unknown) as TestRunProfile; + const request = { profile: profileMock } as TestRunRequest; + + // Run - configure coverage + setupCoverageForProjects(request, [project]); + + // Run - call the configured callback + const fileCoverage = { uri: fileUri }; + const result = await profileMock.loadDetailedCoverage!( + {} as TestRun, + fileCoverage as any, + {} as CancellationToken, + ); + + // Assert + expect(result).to.deep.equal(mockCoverageDetails); + }); + + test('should return empty array when file has no coverage data in map', async () => { + // Mock + const project = createMockProjectAdapter({ projectPath: '/workspace/proj', projectName: 'proj' }); + const profileMock = ({ + kind: TestRunProfileKind.Coverage, + loadDetailedCoverage: undefined, + } as unknown) as TestRunProfile; + const request = { profile: profileMock } as TestRunRequest; + + // Run - configure coverage + setupCoverageForProjects(request, [project]); + + // Run - call callback for file not in map + const fileCoverage = { uri: Uri.file('/workspace/proj/uncovered_file.py') }; + const result = await profileMock.loadDetailedCoverage!( + {} as TestRun, + fileCoverage as any, + {} as CancellationToken, + ); + + // Assert + expect(result).to.deep.equal([]); + }); + + test('should route to correct project when multiple projects have coverage data', async () => { + // Mock - two projects with different coverage data + const project1 = createMockProjectAdapter({ projectPath: '/workspace/proj1', projectName: 'proj1' }); + const project2 = createMockProjectAdapter({ projectPath: '/workspace/proj2', projectName: 'proj2' }); + const coverage1 = [{ line: 1, executed: true }]; + const coverage2 = [{ line: 2, executed: false }]; + const file1Uri = Uri.file('/workspace/proj1/file1.py'); + const file2Uri = Uri.file('/workspace/proj2/file2.py'); + project1.resultResolver.detailedCoverageMap.set(file1Uri.fsPath, coverage1 as any); + project2.resultResolver.detailedCoverageMap.set(file2Uri.fsPath, coverage2 as any); + + const profileMock = ({ + kind: TestRunProfileKind.Coverage, + loadDetailedCoverage: undefined, + } as unknown) as TestRunProfile; + const request = { profile: profileMock } as TestRunRequest; + + // Run - configure coverage with both projects + setupCoverageForProjects(request, [project1, project2]); + + // Assert - can get coverage from both projects through single callback + const result1 = await profileMock.loadDetailedCoverage!( + {} as TestRun, + { uri: file1Uri } as any, + {} as CancellationToken, + ); + const result2 = await profileMock.loadDetailedCoverage!( + {} as TestRun, + { uri: file2Uri } as any, + {} as CancellationToken, + ); + + expect(result1).to.deep.equal(coverage1); + expect(result2).to.deep.equal(coverage2); + }); + }); +}); diff --git a/src/test/testing/testController/common/projectUtils.unit.test.ts b/src/test/testing/testController/common/projectUtils.unit.test.ts new file mode 100644 index 000000000000..75f399e89fc0 --- /dev/null +++ b/src/test/testing/testController/common/projectUtils.unit.test.ts @@ -0,0 +1,241 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { expect } from 'chai'; +import { Uri } from 'vscode'; +import { + getProjectId, + createProjectDisplayName, + parseVsId, + PROJECT_ID_SEPARATOR, +} from '../../../../client/testing/testController/common/projectUtils'; + +suite('Project Utils Tests', () => { + suite('getProjectId', () => { + test('should return URI string representation', () => { + const uri = Uri.file('/workspace/project'); + + const id = getProjectId(uri); + + expect(id).to.equal(uri.toString()); + }); + + test('should be consistent for same URI', () => { + const uri = Uri.file('/workspace/project'); + + const id1 = getProjectId(uri); + const id2 = getProjectId(uri); + + expect(id1).to.equal(id2); + }); + + test('should be different for different URIs', () => { + const uri1 = Uri.file('/workspace/project1'); + const uri2 = Uri.file('/workspace/project2'); + + const id1 = getProjectId(uri1); + const id2 = getProjectId(uri2); + + expect(id1).to.not.equal(id2); + }); + + test('should handle Windows paths', () => { + const uri = Uri.file('C:\\workspace\\project'); + + const id = getProjectId(uri); + + expect(id).to.be.a('string'); + expect(id).to.have.length.greaterThan(0); + }); + + test('should handle nested project paths', () => { + const parentUri = Uri.file('/workspace/parent'); + const childUri = Uri.file('/workspace/parent/child'); + + const parentId = getProjectId(parentUri); + const childId = getProjectId(childUri); + + expect(parentId).to.not.equal(childId); + }); + + test('should match Python Environments extension format', () => { + const uri = Uri.file('/workspace/project'); + + const id = getProjectId(uri); + + // Should match how Python Environments extension keys projects + expect(id).to.equal(uri.toString()); + expect(typeof id).to.equal('string'); + }); + }); + + suite('createProjectDisplayName', () => { + test('should format name with major.minor version', () => { + const result = createProjectDisplayName('MyProject', '3.11.2'); + + expect(result).to.equal('MyProject (Python 3.11)'); + }); + + test('should handle version with patch and pre-release', () => { + const result = createProjectDisplayName('MyProject', '3.12.0rc1'); + + expect(result).to.equal('MyProject (Python 3.12)'); + }); + + test('should handle version with only major.minor', () => { + const result = createProjectDisplayName('MyProject', '3.10'); + + expect(result).to.equal('MyProject (Python 3.10)'); + }); + + test('should handle invalid version format gracefully', () => { + const result = createProjectDisplayName('MyProject', 'invalid-version'); + + expect(result).to.equal('MyProject (Python invalid-version)'); + }); + + test('should handle empty version string', () => { + const result = createProjectDisplayName('MyProject', ''); + + expect(result).to.equal('MyProject (Python )'); + }); + + test('should handle version with single digit', () => { + const result = createProjectDisplayName('MyProject', '3'); + + expect(result).to.equal('MyProject (Python 3)'); + }); + + test('should handle project name with special characters', () => { + const result = createProjectDisplayName('My-Project_123', '3.11.5'); + + expect(result).to.equal('My-Project_123 (Python 3.11)'); + }); + + test('should handle empty project name', () => { + const result = createProjectDisplayName('', '3.11.2'); + + expect(result).to.equal(' (Python 3.11)'); + }); + }); + + suite('parseVsId', () => { + test('should parse project-scoped ID correctly', () => { + const projectUri = Uri.file('/workspace/project'); + const projectId = getProjectId(projectUri); + const vsId = `${projectId}${PROJECT_ID_SEPARATOR}test_file.py::test_name`; + + const [parsedProjectId, runId] = parseVsId(vsId); + + expect(parsedProjectId).to.equal(projectId); + expect(runId).to.equal('test_file.py::test_name'); + }); + + test('should handle legacy ID without project scope', () => { + const vsId = 'test_file.py'; + + const [projectId, runId] = parseVsId(vsId); + + expect(projectId).to.be.undefined; + expect(runId).to.equal('test_file.py'); + }); + + test('should handle runId containing separator', () => { + const projectUri = Uri.file('/workspace/project'); + const projectId = getProjectId(projectUri); + const vsId = `${projectId}${PROJECT_ID_SEPARATOR}test_file.py::test_class::test_method`; + + const [parsedProjectId, runId] = parseVsId(vsId); + + expect(parsedProjectId).to.equal(projectId); + expect(runId).to.equal('test_file.py::test_class::test_method'); + }); + + test('should handle empty project ID', () => { + const vsId = `${PROJECT_ID_SEPARATOR}test_file.py::test_name`; + + const [projectId, runId] = parseVsId(vsId); + + expect(projectId).to.equal(''); + expect(runId).to.equal('test_file.py::test_name'); + }); + + test('should handle empty runId', () => { + const vsId = `project-abc123def456${PROJECT_ID_SEPARATOR}`; + + const [projectId, runId] = parseVsId(vsId); + + expect(projectId).to.equal('project-abc123def456'); + expect(runId).to.equal(''); + }); + + test('should handle ID with file path', () => { + const vsId = `project-abc123def456${PROJECT_ID_SEPARATOR}/workspace/tests/test_file.py`; + + const [projectId, runId] = parseVsId(vsId); + + expect(projectId).to.equal('project-abc123def456'); + expect(runId).to.equal('/workspace/tests/test_file.py'); + }); + + test('should handle Windows file paths', () => { + const projectUri = Uri.file('/workspace/project'); + const projectId = getProjectId(projectUri); + const vsId = `${projectId}${PROJECT_ID_SEPARATOR}C:\\workspace\\tests\\test_file.py`; + + const [parsedProjectId, runId] = parseVsId(vsId); + + expect(parsedProjectId).to.equal(projectId); + expect(runId).to.equal('C:\\workspace\\tests\\test_file.py'); + }); + }); + + suite('Integration Tests', () => { + test('should generate unique IDs for different URIs', () => { + const uris = [ + Uri.file('/workspace/a'), + Uri.file('/workspace/b'), + Uri.file('/workspace/c'), + Uri.file('/workspace/d'), + Uri.file('/workspace/e'), + ]; + + const ids = uris.map((uri) => getProjectId(uri)); + const uniqueIds = new Set(ids); + + expect(uniqueIds.size).to.equal(uris.length, 'All IDs should be unique'); + }); + + test('should handle nested project paths', () => { + const parentUri = Uri.file('/workspace/parent'); + const childUri = Uri.file('/workspace/parent/child'); + + const parentId = getProjectId(parentUri); + const childId = getProjectId(childUri); + + expect(parentId).to.not.equal(childId); + }); + + test('should create complete vsId and parse it back', () => { + const projectUri = Uri.file('/workspace/myproject'); + const projectId = getProjectId(projectUri); + const runId = 'tests/test_module.py::TestClass::test_method'; + const vsId = `${projectId}${PROJECT_ID_SEPARATOR}${runId}`; + + const [parsedProjectId, parsedRunId] = parseVsId(vsId); + + expect(parsedProjectId).to.equal(projectId); + expect(parsedRunId).to.equal(runId); + }); + + test('should match Python Environments extension URI format', () => { + const uri = Uri.file('/workspace/project'); + + const projectId = getProjectId(uri); + + // Should be string representation of URI + expect(projectId).to.equal(uri.toString()); + expect(typeof projectId).to.equal('string'); + }); + }); +}); diff --git a/src/test/testing/testController/common/testCoverageHandler.unit.test.ts b/src/test/testing/testController/common/testCoverageHandler.unit.test.ts new file mode 100644 index 000000000000..a81aed591128 --- /dev/null +++ b/src/test/testing/testController/common/testCoverageHandler.unit.test.ts @@ -0,0 +1,502 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { TestRun, Uri, FileCoverage } from 'vscode'; +import * as typemoq from 'typemoq'; +import * as assert from 'assert'; +import { TestCoverageHandler } from '../../../../client/testing/testController/common/testCoverageHandler'; +import { CoveragePayload } from '../../../../client/testing/testController/common/types'; + +suite('TestCoverageHandler', () => { + let coverageHandler: TestCoverageHandler; + let runInstanceMock: typemoq.IMock; + + setup(() => { + coverageHandler = new TestCoverageHandler(); + runInstanceMock = typemoq.Mock.ofType(); + }); + + suite('processCoverage', () => { + test('should return empty map for undefined result', () => { + const payload: CoveragePayload = { + coverage: true, + cwd: '/foo/bar', + result: undefined, + error: '', + }; + + const result = coverageHandler.processCoverage(payload, runInstanceMock.object); + + assert.strictEqual(result.size, 0); + runInstanceMock.verify((r) => r.addCoverage(typemoq.It.isAny()), typemoq.Times.never()); + }); + + test('should create FileCoverage for each file', () => { + const payload: CoveragePayload = { + coverage: true, + cwd: '/foo/bar', + result: { + '/path/to/file1.py': { + lines_covered: [1, 2, 3], + lines_missed: [4, 5], + executed_branches: 5, + total_branches: 10, + }, + '/path/to/file2.py': { + lines_covered: [1, 2], + lines_missed: [3], + executed_branches: 2, + total_branches: 4, + }, + }, + error: '', + }; + + coverageHandler.processCoverage(payload, runInstanceMock.object); + + runInstanceMock.verify((r) => r.addCoverage(typemoq.It.isAny()), typemoq.Times.exactly(2)); + }); + + test('should call runInstance.addCoverage with correct FileCoverage', () => { + const payload: CoveragePayload = { + coverage: true, + cwd: '/foo/bar', + result: { + '/path/to/file.py': { + lines_covered: [1, 2, 3], + lines_missed: [4, 5], + executed_branches: 5, + total_branches: 10, + }, + }, + error: '', + }; + + let capturedCoverage: FileCoverage | undefined; + runInstanceMock + .setup((r) => r.addCoverage(typemoq.It.isAny())) + .callback((coverage: FileCoverage) => { + capturedCoverage = coverage; + }); + + coverageHandler.processCoverage(payload, runInstanceMock.object); + + assert.ok(capturedCoverage); + assert.strictEqual(capturedCoverage!.uri.fsPath, Uri.file('/path/to/file.py').fsPath); + }); + + test('should return detailed coverage map with correct keys', () => { + const payload: CoveragePayload = { + coverage: true, + cwd: '/foo/bar', + result: { + '/path/to/file1.py': { + lines_covered: [1, 2], + lines_missed: [3], + executed_branches: 2, + total_branches: 4, + }, + '/path/to/file2.py': { + lines_covered: [5, 6, 7], + lines_missed: [], + executed_branches: 3, + total_branches: 3, + }, + }, + error: '', + }; + + const result = coverageHandler.processCoverage(payload, runInstanceMock.object); + + assert.strictEqual(result.size, 2); + assert.ok(result.has(Uri.file('/path/to/file1.py').fsPath)); + assert.ok(result.has(Uri.file('/path/to/file2.py').fsPath)); + }); + + test('should handle empty coverage data', () => { + const payload: CoveragePayload = { + coverage: true, + cwd: '/foo/bar', + result: {}, + error: '', + }; + + const result = coverageHandler.processCoverage(payload, runInstanceMock.object); + + assert.strictEqual(result.size, 0); + }); + + test('should handle file with no covered lines', () => { + const payload: CoveragePayload = { + coverage: true, + cwd: '/foo/bar', + result: { + '/path/to/file.py': { + lines_covered: [], + lines_missed: [1, 2, 3], + executed_branches: 0, + total_branches: 5, + }, + }, + error: '', + }; + + const result = coverageHandler.processCoverage(payload, runInstanceMock.object); + + const detailedCoverage = result.get(Uri.file('/path/to/file.py').fsPath); + assert.ok(detailedCoverage); + assert.strictEqual(detailedCoverage!.length, 3); // Only missed lines + }); + + test('should handle file with no missed lines', () => { + const payload: CoveragePayload = { + coverage: true, + cwd: '/foo/bar', + result: { + '/path/to/file.py': { + lines_covered: [1, 2, 3], + lines_missed: [], + executed_branches: 5, + total_branches: 5, + }, + }, + error: '', + }; + + const result = coverageHandler.processCoverage(payload, runInstanceMock.object); + + const detailedCoverage = result.get(Uri.file('/path/to/file.py').fsPath); + assert.ok(detailedCoverage); + assert.strictEqual(detailedCoverage!.length, 3); // Only covered lines + }); + + test('should handle undefined lines_covered', () => { + const payload: CoveragePayload = { + coverage: true, + cwd: '/foo/bar', + result: { + '/path/to/file.py': { + lines_covered: undefined as any, + lines_missed: [1, 2], + executed_branches: 0, + total_branches: 2, + }, + }, + error: '', + }; + + const result = coverageHandler.processCoverage(payload, runInstanceMock.object); + + const detailedCoverage = result.get(Uri.file('/path/to/file.py').fsPath); + assert.ok(detailedCoverage); + assert.strictEqual(detailedCoverage!.length, 2); // Only missed lines + }); + + test('should handle undefined lines_missed', () => { + const payload: CoveragePayload = { + coverage: true, + cwd: '/foo/bar', + result: { + '/path/to/file.py': { + lines_covered: [1, 2], + lines_missed: undefined as any, + executed_branches: 2, + total_branches: 2, + }, + }, + error: '', + }; + + const result = coverageHandler.processCoverage(payload, runInstanceMock.object); + + const detailedCoverage = result.get(Uri.file('/path/to/file.py').fsPath); + assert.ok(detailedCoverage); + assert.strictEqual(detailedCoverage!.length, 2); // Only covered lines + }); + }); + + suite('createFileCoverage', () => { + test('should handle line coverage only when totalBranches is -1', () => { + const payload: CoveragePayload = { + coverage: true, + cwd: '/foo/bar', + result: { + '/path/to/file.py': { + lines_covered: [1, 2, 3], + lines_missed: [4, 5], + executed_branches: 0, + total_branches: -1, // Branch coverage disabled + }, + }, + error: '', + }; + + let capturedCoverage: FileCoverage | undefined; + runInstanceMock + .setup((r) => r.addCoverage(typemoq.It.isAny())) + .callback((coverage: FileCoverage) => { + capturedCoverage = coverage; + }); + + coverageHandler.processCoverage(payload, runInstanceMock.object); + + assert.ok(capturedCoverage); + // Branch coverage should not be included + assert.strictEqual((capturedCoverage as any).branchCoverage, undefined); + }); + + test('should include branch coverage when available', () => { + const payload: CoveragePayload = { + coverage: true, + cwd: '/foo/bar', + result: { + '/path/to/file.py': { + lines_covered: [1, 2, 3], + lines_missed: [4], + executed_branches: 7, + total_branches: 10, + }, + }, + error: '', + }; + + let capturedCoverage: FileCoverage | undefined; + runInstanceMock + .setup((r) => r.addCoverage(typemoq.It.isAny())) + .callback((coverage: FileCoverage) => { + capturedCoverage = coverage; + }); + + coverageHandler.processCoverage(payload, runInstanceMock.object); + + assert.ok(capturedCoverage); + // Should have branch coverage + assert.ok((capturedCoverage as any).branchCoverage); + }); + + test('should calculate line coverage counts correctly', () => { + const payload: CoveragePayload = { + coverage: true, + cwd: '/foo/bar', + result: { + '/path/to/file.py': { + lines_covered: [1, 2, 3, 4, 5], + lines_missed: [6, 7], + executed_branches: 0, + total_branches: -1, + }, + }, + error: '', + }; + + let capturedCoverage: FileCoverage | undefined; + runInstanceMock + .setup((r) => r.addCoverage(typemoq.It.isAny())) + .callback((coverage: FileCoverage) => { + capturedCoverage = coverage; + }); + + coverageHandler.processCoverage(payload, runInstanceMock.object); + + assert.ok(capturedCoverage); + // 5 covered out of 7 total (5 covered + 2 missed) + assert.strictEqual((capturedCoverage as any).statementCoverage.covered, 5); + assert.strictEqual((capturedCoverage as any).statementCoverage.total, 7); + }); + }); + + suite('createDetailedCoverage', () => { + test('should create StatementCoverage for covered lines', () => { + const payload: CoveragePayload = { + coverage: true, + cwd: '/foo/bar', + result: { + '/path/to/file.py': { + lines_covered: [1, 2, 3], + lines_missed: [], + executed_branches: 0, + total_branches: -1, + }, + }, + error: '', + }; + + const result = coverageHandler.processCoverage(payload, runInstanceMock.object); + + const detailedCoverage = result.get(Uri.file('/path/to/file.py').fsPath); + assert.ok(detailedCoverage); + assert.strictEqual(detailedCoverage!.length, 3); + + // All should be covered (true) + detailedCoverage!.forEach((coverage) => { + assert.strictEqual((coverage as any).executed, true); + }); + }); + + test('should create StatementCoverage for missed lines', () => { + const payload: CoveragePayload = { + coverage: true, + cwd: '/foo/bar', + result: { + '/path/to/file.py': { + lines_covered: [], + lines_missed: [1, 2, 3], + executed_branches: 0, + total_branches: -1, + }, + }, + error: '', + }; + + const result = coverageHandler.processCoverage(payload, runInstanceMock.object); + + const detailedCoverage = result.get(Uri.file('/path/to/file.py').fsPath); + assert.ok(detailedCoverage); + assert.strictEqual(detailedCoverage!.length, 3); + + // All should be NOT covered (false) + detailedCoverage!.forEach((coverage) => { + assert.strictEqual((coverage as any).executed, false); + }); + }); + + test('should convert 1-indexed to 0-indexed line numbers for covered lines', () => { + const payload: CoveragePayload = { + coverage: true, + cwd: '/foo/bar', + result: { + '/path/to/file.py': { + lines_covered: [1, 5, 10], + lines_missed: [], + executed_branches: 0, + total_branches: -1, + }, + }, + error: '', + }; + + const result = coverageHandler.processCoverage(payload, runInstanceMock.object); + + const detailedCoverage = result.get(Uri.file('/path/to/file.py').fsPath); + assert.ok(detailedCoverage); + + // Line 1 should map to range starting at line 0 + assert.strictEqual((detailedCoverage![0] as any).location.start.line, 0); + // Line 5 should map to range starting at line 4 + assert.strictEqual((detailedCoverage![1] as any).location.start.line, 4); + // Line 10 should map to range starting at line 9 + assert.strictEqual((detailedCoverage![2] as any).location.start.line, 9); + }); + + test('should convert 1-indexed to 0-indexed line numbers for missed lines', () => { + const payload: CoveragePayload = { + coverage: true, + cwd: '/foo/bar', + result: { + '/path/to/file.py': { + lines_covered: [], + lines_missed: [3, 7, 12], + executed_branches: 0, + total_branches: -1, + }, + }, + error: '', + }; + + const result = coverageHandler.processCoverage(payload, runInstanceMock.object); + + const detailedCoverage = result.get(Uri.file('/path/to/file.py').fsPath); + assert.ok(detailedCoverage); + + // Line 3 should map to range starting at line 2 + assert.strictEqual((detailedCoverage![0] as any).location.start.line, 2); + // Line 7 should map to range starting at line 6 + assert.strictEqual((detailedCoverage![1] as any).location.start.line, 6); + // Line 12 should map to range starting at line 11 + assert.strictEqual((detailedCoverage![2] as any).location.start.line, 11); + }); + + test('should handle large line numbers', () => { + const payload: CoveragePayload = { + coverage: true, + cwd: '/foo/bar', + result: { + '/path/to/file.py': { + lines_covered: [1000, 5000, 10000], + lines_missed: [], + executed_branches: 0, + total_branches: -1, + }, + }, + error: '', + }; + + const result = coverageHandler.processCoverage(payload, runInstanceMock.object); + + const detailedCoverage = result.get(Uri.file('/path/to/file.py').fsPath); + assert.ok(detailedCoverage); + assert.strictEqual(detailedCoverage!.length, 3); + + // Verify conversion is correct for large numbers + assert.strictEqual((detailedCoverage![0] as any).location.start.line, 999); + assert.strictEqual((detailedCoverage![1] as any).location.start.line, 4999); + assert.strictEqual((detailedCoverage![2] as any).location.start.line, 9999); + }); + + test('should create detailed coverage with both covered and missed lines', () => { + const payload: CoveragePayload = { + coverage: true, + cwd: '/foo/bar', + result: { + '/path/to/file.py': { + lines_covered: [1, 3, 5], + lines_missed: [2, 4, 6], + executed_branches: 3, + total_branches: 6, + }, + }, + error: '', + }; + + const result = coverageHandler.processCoverage(payload, runInstanceMock.object); + + const detailedCoverage = result.get(Uri.file('/path/to/file.py').fsPath); + assert.ok(detailedCoverage); + assert.strictEqual(detailedCoverage!.length, 6); // 3 covered + 3 missed + + // Count covered vs not covered + const covered = detailedCoverage!.filter((c) => (c as any).executed === true); + const notCovered = detailedCoverage!.filter((c) => (c as any).executed === false); + + assert.strictEqual(covered.length, 3); + assert.strictEqual(notCovered.length, 3); + }); + + test('should set range to cover entire line', () => { + const payload: CoveragePayload = { + coverage: true, + cwd: '/foo/bar', + result: { + '/path/to/file.py': { + lines_covered: [1], + lines_missed: [], + executed_branches: 0, + total_branches: -1, + }, + }, + error: '', + }; + + const result = coverageHandler.processCoverage(payload, runInstanceMock.object); + + const detailedCoverage = result.get(Uri.file('/path/to/file.py').fsPath); + assert.ok(detailedCoverage); + + const coverage = detailedCoverage![0] as any; + // Start at column 0 + assert.strictEqual(coverage.location.start.character, 0); + // End at max safe integer (entire line) + assert.strictEqual(coverage.location.end.character, Number.MAX_SAFE_INTEGER); + }); + }); +}); diff --git a/src/test/testing/testController/common/testDiscoveryHandler.unit.test.ts b/src/test/testing/testController/common/testDiscoveryHandler.unit.test.ts new file mode 100644 index 000000000000..458e3d984405 --- /dev/null +++ b/src/test/testing/testController/common/testDiscoveryHandler.unit.test.ts @@ -0,0 +1,517 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { TestController, TestItem, Uri, CancellationToken, TestItemCollection } from 'vscode'; +import * as typemoq from 'typemoq'; +import * as assert from 'assert'; +import * as sinon from 'sinon'; +import { TestDiscoveryHandler } from '../../../../client/testing/testController/common/testDiscoveryHandler'; +import { TestItemIndex } from '../../../../client/testing/testController/common/testItemIndex'; +import { DiscoveredTestPayload, DiscoveredTestNode } from '../../../../client/testing/testController/common/types'; +import { TestProvider } from '../../../../client/testing/types'; +import * as utils from '../../../../client/testing/testController/common/utils'; +import * as testItemUtilities from '../../../../client/testing/testController/common/testItemUtilities'; + +suite('TestDiscoveryHandler', () => { + let discoveryHandler: TestDiscoveryHandler; + let testControllerMock: typemoq.IMock; + let testItemIndexMock: typemoq.IMock; + let testItemCollectionMock: typemoq.IMock; + let workspaceUri: Uri; + let testProvider: TestProvider; + let cancelationToken: CancellationToken; + + setup(() => { + discoveryHandler = new TestDiscoveryHandler(); + testControllerMock = typemoq.Mock.ofType(); + testItemIndexMock = typemoq.Mock.ofType(); + testItemCollectionMock = typemoq.Mock.ofType(); + + // Setup default test controller items mock + testControllerMock.setup((t) => t.items).returns(() => testItemCollectionMock.object); + testItemCollectionMock.setup((x) => x.delete(typemoq.It.isAny())).returns(() => undefined); + testItemCollectionMock.setup((x) => x.get(typemoq.It.isAny())).returns(() => undefined); + testItemCollectionMock.setup((x) => x.add(typemoq.It.isAny())).returns(() => undefined); + + workspaceUri = Uri.file('/foo/bar'); + testProvider = 'pytest'; + cancelationToken = ({ + isCancellationRequested: false, + } as unknown) as CancellationToken; + }); + + teardown(() => { + sinon.restore(); + }); + + suite('processDiscovery', () => { + test('should handle null payload gracefully', () => { + discoveryHandler.processDiscovery( + null as any, + testControllerMock.object, + testItemIndexMock.object, + workspaceUri, + testProvider, + cancelationToken, + ); + + // Should not throw and should not call populateTestTree + testItemIndexMock.verify((x) => x.clear(), typemoq.Times.never()); + }); + + test('should call populateTestTree with correct params on success', () => { + const tests: DiscoveredTestNode = { + path: '/foo/bar', + name: 'root', + type_: 'folder', + id_: 'root_id', + children: [], + }; + + const payload: DiscoveredTestPayload = { + cwd: workspaceUri.fsPath, + status: 'success', + tests, + }; + + const populateTestTreeStub = sinon.stub(utils, 'populateTestTree'); + testItemIndexMock.setup((x) => x.clear()).returns(() => undefined); + + // Setup map getters for populateTestTree + const mockRunIdMap = new Map(); + const mockVSidMap = new Map(); + const mockVStoRunMap = new Map(); + testItemIndexMock.setup((x) => x.runIdToTestItemMap).returns(() => mockRunIdMap); + testItemIndexMock.setup((x) => x.runIdToVSidMap).returns(() => mockVSidMap); + testItemIndexMock.setup((x) => x.vsIdToRunIdMap).returns(() => mockVStoRunMap); + + discoveryHandler.processDiscovery( + payload, + testControllerMock.object, + testItemIndexMock.object, + workspaceUri, + testProvider, + cancelationToken, + ); + + testItemIndexMock.verify((x) => x.clear(), typemoq.Times.once()); + assert.ok(populateTestTreeStub.calledOnce); + sinon.assert.calledWith( + populateTestTreeStub, + testControllerMock.object, + tests, + undefined, + sinon.match.any, + cancelationToken, + ); + }); + + test('should clear index before populating', () => { + const tests: DiscoveredTestNode = { + path: '/foo/bar', + name: 'root', + type_: 'folder', + id_: 'root_id', + children: [], + }; + + const payload: DiscoveredTestPayload = { + cwd: workspaceUri.fsPath, + status: 'success', + tests, + }; + + sinon.stub(utils, 'populateTestTree'); + + const clearSpy = sinon.spy(); + testItemIndexMock.setup((x) => x.clear()).callback(clearSpy); + testItemIndexMock.setup((x) => x.runIdToTestItemMap).returns(() => new Map()); + testItemIndexMock.setup((x) => x.runIdToVSidMap).returns(() => new Map()); + testItemIndexMock.setup((x) => x.vsIdToRunIdMap).returns(() => new Map()); + + discoveryHandler.processDiscovery( + payload, + testControllerMock.object, + testItemIndexMock.object, + workspaceUri, + testProvider, + cancelationToken, + ); + + assert.ok(clearSpy.calledOnce); + }); + + test('should handle error status and create error node', () => { + const payload: DiscoveredTestPayload = { + cwd: workspaceUri.fsPath, + status: 'error', + error: ['Error message 1', 'Error message 2'], + }; + + const createErrorNodeSpy = sinon.spy(discoveryHandler, 'createErrorNode'); + + // Mock createTestItem to return a proper TestItem + const mockErrorItem = ({ + id: 'error_id', + error: null, + canResolveChildren: false, + tags: [], + } as unknown) as TestItem; + testControllerMock + .setup((t) => t.createTestItem(typemoq.It.isAny(), typemoq.It.isAny())) + .returns(() => mockErrorItem); + + discoveryHandler.processDiscovery( + payload, + testControllerMock.object, + testItemIndexMock.object, + workspaceUri, + testProvider, + cancelationToken, + ); + + assert.ok(createErrorNodeSpy.calledOnce); + assert.ok( + createErrorNodeSpy.calledWith(testControllerMock.object, workspaceUri, payload.error, testProvider), + ); + }); + + test('should handle both errors and tests in same payload', () => { + const tests: DiscoveredTestNode = { + path: '/foo/bar', + name: 'root', + type_: 'folder', + id_: 'root_id', + children: [], + }; + + const payload: DiscoveredTestPayload = { + cwd: workspaceUri.fsPath, + status: 'error', + error: ['Partial error'], + tests, + }; + + sinon.stub(utils, 'populateTestTree'); + const createErrorNodeSpy = sinon.spy(discoveryHandler, 'createErrorNode'); + + // Mock createTestItem to return a proper TestItem + const mockErrorItem = ({ + id: 'error_id', + error: null, + canResolveChildren: false, + tags: [], + } as unknown) as TestItem; + testControllerMock + .setup((t) => t.createTestItem(typemoq.It.isAny(), typemoq.It.isAny())) + .returns(() => mockErrorItem); + + testItemIndexMock.setup((x) => x.clear()).returns(() => undefined); + testItemIndexMock.setup((x) => x.runIdToTestItemMap).returns(() => new Map()); + testItemIndexMock.setup((x) => x.runIdToVSidMap).returns(() => new Map()); + testItemIndexMock.setup((x) => x.vsIdToRunIdMap).returns(() => new Map()); + + discoveryHandler.processDiscovery( + payload, + testControllerMock.object, + testItemIndexMock.object, + workspaceUri, + testProvider, + cancelationToken, + ); + + // Should create error node AND populate test tree + assert.ok(createErrorNodeSpy.calledOnce); + testItemIndexMock.verify((x) => x.clear(), typemoq.Times.once()); + }); + + test('should delete error node on successful discovery', () => { + const tests: DiscoveredTestNode = { + path: '/foo/bar', + name: 'root', + type_: 'folder', + id_: 'root_id', + children: [], + }; + + const payload: DiscoveredTestPayload = { + cwd: workspaceUri.fsPath, + status: 'success', + tests, + }; + + const deleteSpy = sinon.spy(); + // Reset and reconfigure the collection mock to capture delete call + testItemCollectionMock.reset(); + testItemCollectionMock + .setup((x) => x.delete(typemoq.It.isAny())) + .callback(deleteSpy) + .returns(() => undefined); + testItemCollectionMock.setup((x) => x.get(typemoq.It.isAny())).returns(() => undefined); + testItemCollectionMock.setup((x) => x.add(typemoq.It.isAny())).returns(() => undefined); + testControllerMock.reset(); + testControllerMock.setup((t) => t.items).returns(() => testItemCollectionMock.object); + + sinon.stub(utils, 'populateTestTree'); + testItemIndexMock.setup((x) => x.clear()).returns(() => undefined); + testItemIndexMock.setup((x) => x.runIdToTestItemMap).returns(() => new Map()); + testItemIndexMock.setup((x) => x.runIdToVSidMap).returns(() => new Map()); + testItemIndexMock.setup((x) => x.vsIdToRunIdMap).returns(() => new Map()); + + discoveryHandler.processDiscovery( + payload, + testControllerMock.object, + testItemIndexMock.object, + workspaceUri, + testProvider, + cancelationToken, + ); + + assert.ok(deleteSpy.calledOnce); + assert.ok(deleteSpy.calledWith(`DiscoveryError:${workspaceUri.fsPath}`)); + }); + + test('should respect cancellation token', () => { + const tests: DiscoveredTestNode = { + path: '/foo/bar', + name: 'root', + type_: 'folder', + id_: 'root_id', + children: [], + }; + + const payload: DiscoveredTestPayload = { + cwd: workspaceUri.fsPath, + status: 'success', + tests, + }; + + const populateTestTreeStub = sinon.stub(utils, 'populateTestTree'); + testItemIndexMock.setup((x) => x.clear()).returns(() => undefined); + testItemIndexMock.setup((x) => x.runIdToTestItemMap).returns(() => new Map()); + testItemIndexMock.setup((x) => x.runIdToVSidMap).returns(() => new Map()); + testItemIndexMock.setup((x) => x.vsIdToRunIdMap).returns(() => new Map()); + + discoveryHandler.processDiscovery( + payload, + testControllerMock.object, + testItemIndexMock.object, + workspaceUri, + testProvider, + cancelationToken, + ); + + // Verify token was passed to populateTestTree + assert.ok(populateTestTreeStub.calledOnce); + const lastArg = populateTestTreeStub.getCall(0).args[4]; + assert.strictEqual(lastArg, cancelationToken); + }); + + test('should handle null tests in payload', () => { + const payload: DiscoveredTestPayload = { + cwd: workspaceUri.fsPath, + status: 'success', + tests: null as any, + }; + + const populateTestTreeStub = sinon.stub(utils, 'populateTestTree'); + testItemIndexMock.setup((x) => x.clear()).returns(() => undefined); + testItemIndexMock.setup((x) => x.runIdToTestItemMap).returns(() => new Map()); + testItemIndexMock.setup((x) => x.runIdToVSidMap).returns(() => new Map()); + testItemIndexMock.setup((x) => x.vsIdToRunIdMap).returns(() => new Map()); + + discoveryHandler.processDiscovery( + payload, + testControllerMock.object, + testItemIndexMock.object, + workspaceUri, + testProvider, + cancelationToken, + ); + + // Should still call populateTestTree with null + assert.ok(populateTestTreeStub.calledOnce); + testItemIndexMock.verify((x) => x.clear(), typemoq.Times.once()); + }); + }); + + suite('createErrorNode', () => { + test('should create error with correct message for pytest', () => { + const error = ['Error line 1', 'Error line 2']; + testProvider = 'pytest'; + + const buildErrorNodeOptionsStub = sinon.stub(utils, 'buildErrorNodeOptions').returns({ + id: 'error_id', + label: 'Error Label', + error: 'Error Message', + }); + + const mockErrorItem = ({ + id: 'error_id', + error: null, + } as unknown) as TestItem; + + const createErrorTestItemStub = sinon.stub(testItemUtilities, 'createErrorTestItem').returns(mockErrorItem); + + const testItemCollectionMock = typemoq.Mock.ofType(); + testItemCollectionMock.setup((x) => x.get(typemoq.It.isAny())).returns(() => undefined); + testItemCollectionMock.setup((x) => x.add(typemoq.It.isAny())).returns(() => undefined); + testControllerMock.setup((t) => t.items).returns(() => testItemCollectionMock.object); + + discoveryHandler.createErrorNode(testControllerMock.object, workspaceUri, error, testProvider); + + assert.ok(buildErrorNodeOptionsStub.calledOnce); + assert.ok(createErrorTestItemStub.calledOnce); + assert.ok(mockErrorItem.error !== null); + }); + + test('should create error with correct message for unittest', () => { + const error = ['Unittest error']; + testProvider = 'unittest'; + + sinon.stub(utils, 'buildErrorNodeOptions').returns({ + id: 'error_id', + label: 'Error Label', + error: 'Error Message', + }); + + const mockErrorItem = ({ + id: 'error_id', + error: null, + } as unknown) as TestItem; + + sinon.stub(testItemUtilities, 'createErrorTestItem').returns(mockErrorItem); + + const testItemCollectionMock = typemoq.Mock.ofType(); + testItemCollectionMock.setup((x) => x.get(typemoq.It.isAny())).returns(() => undefined); + testItemCollectionMock.setup((x) => x.add(typemoq.It.isAny())).returns(() => undefined); + testControllerMock.setup((t) => t.items).returns(() => testItemCollectionMock.object); + + discoveryHandler.createErrorNode(testControllerMock.object, workspaceUri, error, testProvider); + + assert.ok(mockErrorItem.error !== null); + }); + + test('should set markdown error label correctly', () => { + const error = ['Test error']; + + sinon.stub(utils, 'buildErrorNodeOptions').returns({ + id: 'error_id', + label: 'Error Label', + error: 'Error Message', + }); + + const mockErrorItem = ({ + id: 'error_id', + error: null, + } as unknown) as TestItem; + + sinon.stub(testItemUtilities, 'createErrorTestItem').returns(mockErrorItem); + + const testItemCollectionMock = typemoq.Mock.ofType(); + testItemCollectionMock.setup((x) => x.get(typemoq.It.isAny())).returns(() => undefined); + testItemCollectionMock.setup((x) => x.add(typemoq.It.isAny())).returns(() => undefined); + testControllerMock.setup((t) => t.items).returns(() => testItemCollectionMock.object); + + discoveryHandler.createErrorNode(testControllerMock.object, workspaceUri, error, testProvider); + + assert.ok(mockErrorItem.error); + assert.strictEqual( + (mockErrorItem.error as any).value, + '[Show output](command:python.viewOutput) to view error logs', + ); + assert.strictEqual((mockErrorItem.error as any).isTrusted, true); + }); + + test('should handle undefined error array', () => { + sinon.stub(utils, 'buildErrorNodeOptions').returns({ + id: 'error_id', + label: 'Error Label', + error: 'Error Message', + }); + + const mockErrorItem = ({ + id: 'error_id', + error: null, + } as unknown) as TestItem; + + sinon.stub(testItemUtilities, 'createErrorTestItem').returns(mockErrorItem); + + const testItemCollectionMock = typemoq.Mock.ofType(); + testItemCollectionMock.setup((x) => x.get(typemoq.It.isAny())).returns(() => undefined); + testItemCollectionMock.setup((x) => x.add(typemoq.It.isAny())).returns(() => undefined); + testControllerMock.setup((t) => t.items).returns(() => testItemCollectionMock.object); + + discoveryHandler.createErrorNode(testControllerMock.object, workspaceUri, undefined, testProvider); + + // Should not throw + assert.ok(mockErrorItem.error !== null); + }); + + test('should reuse existing error node if present', () => { + const error = ['Error']; + + // Create a proper object with settable error property + const existingErrorItem: any = { + id: `DiscoveryError:${workspaceUri.fsPath}`, + error: null, + canResolveChildren: false, + tags: [], + }; + + sinon.stub(utils, 'buildErrorNodeOptions').returns({ + id: `DiscoveryError:${workspaceUri.fsPath}`, + label: 'Error Label', + error: 'Error Message', + }); + + const createErrorTestItemStub = sinon.stub(testItemUtilities, 'createErrorTestItem'); + + // Reset and setup collection to return existing item + testItemCollectionMock.reset(); + testItemCollectionMock + .setup((x) => x.get(`DiscoveryError:${workspaceUri.fsPath}`)) + .returns(() => existingErrorItem); + testItemCollectionMock.setup((x) => x.add(typemoq.It.isAny())).returns(() => undefined); + testControllerMock.reset(); + testControllerMock.setup((t) => t.items).returns(() => testItemCollectionMock.object); + + discoveryHandler.createErrorNode(testControllerMock.object, workspaceUri, error, testProvider); + + // Should not create a new error item + assert.ok(createErrorTestItemStub.notCalled); + // Should still update the error property + assert.ok(existingErrorItem.error !== null); + }); + + test('should handle multiple error messages', () => { + const error = ['Error 1', 'Error 2', 'Error 3']; + + const buildStub = sinon.stub(utils, 'buildErrorNodeOptions').returns({ + id: 'error_id', + label: 'Error Label', + error: 'Error Message', + }); + + const mockErrorItem = ({ + id: 'error_id', + error: null, + } as unknown) as TestItem; + + sinon.stub(testItemUtilities, 'createErrorTestItem').returns(mockErrorItem); + + const testItemCollectionMock = typemoq.Mock.ofType(); + testItemCollectionMock.setup((x) => x.get(typemoq.It.isAny())).returns(() => undefined); + testItemCollectionMock.setup((x) => x.add(typemoq.It.isAny())).returns(() => undefined); + testControllerMock.setup((t) => t.items).returns(() => testItemCollectionMock.object); + + discoveryHandler.createErrorNode(testControllerMock.object, workspaceUri, error, testProvider); + + // Verify the error messages are joined + const expectedMessage = sinon.match((value: string) => { + return value.includes('Error 1') && value.includes('Error 2') && value.includes('Error 3'); + }); + sinon.assert.calledWith(buildStub, workspaceUri, expectedMessage, testProvider); + }); + }); +}); diff --git a/src/test/testing/testController/common/testExecutionHandler.unit.test.ts b/src/test/testing/testController/common/testExecutionHandler.unit.test.ts new file mode 100644 index 000000000000..c6be4548c192 --- /dev/null +++ b/src/test/testing/testController/common/testExecutionHandler.unit.test.ts @@ -0,0 +1,922 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { TestController, TestItem, TestRun, TestMessage, Uri, Range, TestItemCollection, MarkdownString } from 'vscode'; +import * as typemoq from 'typemoq'; +import * as assert from 'assert'; +import * as sinon from 'sinon'; +import { TestExecutionHandler } from '../../../../client/testing/testController/common/testExecutionHandler'; +import { TestItemIndex } from '../../../../client/testing/testController/common/testItemIndex'; +import { ExecutionTestPayload } from '../../../../client/testing/testController/common/types'; + +suite('TestExecutionHandler', () => { + let executionHandler: TestExecutionHandler; + let testControllerMock: typemoq.IMock; + let testItemIndexMock: typemoq.IMock; + let runInstanceMock: typemoq.IMock; + let mockTestItem: TestItem; + let mockParentItem: TestItem; + + setup(() => { + executionHandler = new TestExecutionHandler(); + testControllerMock = typemoq.Mock.ofType(); + testItemIndexMock = typemoq.Mock.ofType(); + runInstanceMock = typemoq.Mock.ofType(); + + mockTestItem = createMockTestItem('test1', 'Test 1'); + mockParentItem = createMockTestItem('parentTest', 'Parent Test'); + }); + + teardown(() => { + sinon.restore(); + }); + + suite('processExecution', () => { + test('should process empty payload without errors', () => { + const payload: ExecutionTestPayload = { + cwd: '/foo/bar', + status: 'success', + result: {}, + error: '', + }; + + executionHandler.processExecution( + payload, + runInstanceMock.object, + testItemIndexMock.object, + testControllerMock.object, + ); + + // No errors should be thrown + }); + + test('should process undefined result without errors', () => { + const payload: ExecutionTestPayload = { + cwd: '/foo/bar', + status: 'success', + error: '', + }; + + executionHandler.processExecution( + payload, + runInstanceMock.object, + testItemIndexMock.object, + testControllerMock.object, + ); + + // No errors should be thrown + }); + + test('should process multiple test results', () => { + const payload: ExecutionTestPayload = { + cwd: '/foo/bar', + status: 'success', + result: { + test1: { test: 'test1', outcome: 'success', message: '', traceback: '' }, + test2: { test: 'test2', outcome: 'failure', message: 'Failed', traceback: 'traceback' }, + }, + error: '', + }; + + const mockTestItem2 = createMockTestItem('test2', 'Test 2'); + + testItemIndexMock + .setup((x) => x.getTestItem('test1', testControllerMock.object)) + .returns(() => mockTestItem); + testItemIndexMock + .setup((x) => x.getTestItem('test2', testControllerMock.object)) + .returns(() => mockTestItem2); + + executionHandler.processExecution( + payload, + runInstanceMock.object, + testItemIndexMock.object, + testControllerMock.object, + ); + + runInstanceMock.verify((r) => r.passed(mockTestItem), typemoq.Times.once()); + runInstanceMock.verify((r) => r.failed(mockTestItem2, typemoq.It.isAny()), typemoq.Times.once()); + }); + }); + + suite('handleTestError', () => { + test('should create error message with traceback', () => { + const payload: ExecutionTestPayload = { + cwd: '/foo/bar', + status: 'success', + result: { + test1: { + test: 'test1', + outcome: 'error', + message: 'Error occurred', + traceback: 'line1\nline2\nline3', + }, + }, + error: '', + }; + + testItemIndexMock + .setup((x) => x.getTestItem('test1', testControllerMock.object)) + .returns(() => mockTestItem); + + let capturedMessage: TestMessage | undefined; + runInstanceMock + .setup((r) => r.errored(mockTestItem, typemoq.It.isAny())) + .callback((_, message: TestMessage) => { + capturedMessage = message; + }); + + executionHandler.processExecution( + payload, + runInstanceMock.object, + testItemIndexMock.object, + testControllerMock.object, + ); + + assert.ok(capturedMessage); + const messageText = + capturedMessage!.message instanceof MarkdownString + ? capturedMessage!.message.value + : capturedMessage!.message; + assert.ok(messageText.includes('Error occurred')); + assert.ok(messageText.includes('line1')); + assert.ok(messageText.includes('line2')); + runInstanceMock.verify((r) => r.errored(mockTestItem, typemoq.It.isAny()), typemoq.Times.once()); + }); + + test('should set location when test item has range', () => { + const payload: ExecutionTestPayload = { + cwd: '/foo/bar', + status: 'success', + result: { + test1: { + test: 'test1', + outcome: 'error', + message: 'Error', + traceback: '', + }, + }, + error: '', + }; + + testItemIndexMock + .setup((x) => x.getTestItem('test1', testControllerMock.object)) + .returns(() => mockTestItem); + + let capturedMessage: TestMessage | undefined; + runInstanceMock + .setup((r) => r.errored(mockTestItem, typemoq.It.isAny())) + .callback((_, message: TestMessage) => { + capturedMessage = message; + }); + + executionHandler.processExecution( + payload, + runInstanceMock.object, + testItemIndexMock.object, + testControllerMock.object, + ); + + assert.ok(capturedMessage); + assert.ok(capturedMessage!.location); + assert.strictEqual(capturedMessage!.location!.uri.fsPath, mockTestItem.uri!.fsPath); + }); + + test('should handle missing traceback', () => { + const payload: ExecutionTestPayload = { + cwd: '/foo/bar', + status: 'success', + result: { + test1: { + test: 'test1', + outcome: 'error', + message: 'Error', + }, + }, + error: '', + }; + + testItemIndexMock + .setup((x) => x.getTestItem('test1', testControllerMock.object)) + .returns(() => mockTestItem); + + executionHandler.processExecution( + payload, + runInstanceMock.object, + testItemIndexMock.object, + testControllerMock.object, + ); + + runInstanceMock.verify((r) => r.errored(mockTestItem, typemoq.It.isAny()), typemoq.Times.once()); + }); + }); + + suite('handleTestFailure', () => { + test('should create failure message with traceback', () => { + const payload: ExecutionTestPayload = { + cwd: '/foo/bar', + status: 'success', + result: { + test1: { + test: 'test1', + outcome: 'failure', + message: 'Assertion failed', + traceback: 'AssertionError\nline1', + }, + }, + error: '', + }; + + testItemIndexMock + .setup((x) => x.getTestItem('test1', testControllerMock.object)) + .returns(() => mockTestItem); + + let capturedMessage: TestMessage | undefined; + runInstanceMock + .setup((r) => r.failed(mockTestItem, typemoq.It.isAny())) + .callback((_, message: TestMessage) => { + capturedMessage = message; + }); + + executionHandler.processExecution( + payload, + runInstanceMock.object, + testItemIndexMock.object, + testControllerMock.object, + ); + + assert.ok(capturedMessage); + const messageText = + capturedMessage!.message instanceof MarkdownString + ? capturedMessage!.message.value + : capturedMessage!.message; + assert.ok(messageText.includes('Assertion failed')); + assert.ok(messageText.includes('AssertionError')); + runInstanceMock.verify((r) => r.failed(mockTestItem, typemoq.It.isAny()), typemoq.Times.once()); + }); + + test('should handle passed-unexpected outcome', () => { + const payload: ExecutionTestPayload = { + cwd: '/foo/bar', + status: 'success', + result: { + test1: { + test: 'test1', + outcome: 'passed-unexpected', + message: 'Unexpected pass', + traceback: '', + }, + }, + error: '', + }; + + testItemIndexMock + .setup((x) => x.getTestItem('test1', testControllerMock.object)) + .returns(() => mockTestItem); + + executionHandler.processExecution( + payload, + runInstanceMock.object, + testItemIndexMock.object, + testControllerMock.object, + ); + + runInstanceMock.verify((r) => r.failed(mockTestItem, typemoq.It.isAny()), typemoq.Times.once()); + }); + }); + + suite('handleTestSuccess', () => { + test('should mark test as passed', () => { + const payload: ExecutionTestPayload = { + cwd: '/foo/bar', + status: 'success', + result: { + test1: { + test: 'test1', + outcome: 'success', + message: '', + traceback: '', + }, + }, + error: '', + }; + + testItemIndexMock + .setup((x) => x.getTestItem('test1', testControllerMock.object)) + .returns(() => mockTestItem); + + executionHandler.processExecution( + payload, + runInstanceMock.object, + testItemIndexMock.object, + testControllerMock.object, + ); + + runInstanceMock.verify((r) => r.passed(mockTestItem), typemoq.Times.once()); + }); + + test('should handle expected-failure outcome', () => { + const payload: ExecutionTestPayload = { + cwd: '/foo/bar', + status: 'success', + result: { + test1: { + test: 'test1', + outcome: 'expected-failure', + message: '', + traceback: '', + }, + }, + error: '', + }; + + testItemIndexMock + .setup((x) => x.getTestItem('test1', testControllerMock.object)) + .returns(() => mockTestItem); + + executionHandler.processExecution( + payload, + runInstanceMock.object, + testItemIndexMock.object, + testControllerMock.object, + ); + + runInstanceMock.verify((r) => r.passed(mockTestItem), typemoq.Times.once()); + }); + + test('should not call passed when test item not found', () => { + const payload: ExecutionTestPayload = { + cwd: '/foo/bar', + status: 'success', + result: { + test1: { + test: 'test1', + outcome: 'success', + message: '', + traceback: '', + }, + }, + error: '', + }; + + testItemIndexMock.setup((x) => x.getTestItem('test1', testControllerMock.object)).returns(() => undefined); + + executionHandler.processExecution( + payload, + runInstanceMock.object, + testItemIndexMock.object, + testControllerMock.object, + ); + + runInstanceMock.verify((r) => r.passed(typemoq.It.isAny()), typemoq.Times.never()); + }); + }); + + suite('handleTestSkipped', () => { + test('should mark test as skipped', () => { + const payload: ExecutionTestPayload = { + cwd: '/foo/bar', + status: 'success', + result: { + test1: { + test: 'test1', + outcome: 'skipped', + message: 'Test skipped', + traceback: '', + }, + }, + error: '', + }; + + testItemIndexMock + .setup((x) => x.getTestItem('test1', testControllerMock.object)) + .returns(() => mockTestItem); + + executionHandler.processExecution( + payload, + runInstanceMock.object, + testItemIndexMock.object, + testControllerMock.object, + ); + + runInstanceMock.verify((r) => r.skipped(mockTestItem), typemoq.Times.once()); + }); + }); + + suite('handleSubtestFailure', () => { + test('should create child test item for subtest', () => { + const payload: ExecutionTestPayload = { + cwd: '/foo/bar', + status: 'success', + result: { + 'parentTest (subtest1)': { + test: 'parentTest', + outcome: 'subtest-failure', + message: 'Subtest failed', + traceback: 'traceback', + subtest: 'subtest1', + }, + }, + error: '', + }; + + const mockSubtestItem = createMockTestItem('subtest1', 'Subtest 1'); + + testItemIndexMock + .setup((x) => x.getTestItem('parentTest', testControllerMock.object)) + .returns(() => mockParentItem); + testItemIndexMock.setup((x) => x.getSubtestStats('parentTest')).returns(() => undefined); + testItemIndexMock + .setup((x) => x.setSubtestStats('parentTest', typemoq.It.isAny())) + .returns(() => undefined); + testControllerMock + .setup((t) => t.createTestItem(typemoq.It.isAny(), typemoq.It.isAny(), typemoq.It.isAny())) + .returns(() => mockSubtestItem); + + executionHandler.processExecution( + payload, + runInstanceMock.object, + testItemIndexMock.object, + testControllerMock.object, + ); + + // Verify stats were set correctly + testItemIndexMock.verify( + (x) => + x.setSubtestStats( + 'parentTest', + typemoq.It.is((stats) => stats.failed === 1 && stats.passed === 0), + ), + typemoq.Times.once(), + ); + + runInstanceMock.verify((r) => r.started(mockSubtestItem), typemoq.Times.once()); + runInstanceMock.verify((r) => r.failed(mockSubtestItem, typemoq.It.isAny()), typemoq.Times.once()); + }); + + test('should update stats correctly for multiple subtests', () => { + const payload1: ExecutionTestPayload = { + cwd: '/foo/bar', + status: 'success', + result: { + 'parentTest (subtest1)': { + test: 'parentTest', + outcome: 'subtest-failure', + message: 'Failed', + traceback: '', + subtest: 'subtest1', + }, + }, + error: '', + }; + + const payload2: ExecutionTestPayload = { + cwd: '/foo/bar', + status: 'success', + result: { + 'parentTest (subtest2)': { + test: 'parentTest', + outcome: 'subtest-failure', + message: 'Failed', + traceback: '', + subtest: 'subtest2', + }, + }, + error: '', + }; + + const mockSubtest1 = createMockTestItem('subtest1', 'Subtest 1'); + const mockSubtest2 = createMockTestItem('subtest2', 'Subtest 2'); + + testItemIndexMock + .setup((x) => x.getTestItem('parentTest', testControllerMock.object)) + .returns(() => mockParentItem); + + // First subtest: no existing stats + testItemIndexMock.setup((x) => x.getSubtestStats('parentTest')).returns(() => undefined); + testItemIndexMock + .setup((x) => x.setSubtestStats('parentTest', typemoq.It.isAny())) + .returns(() => undefined); + + // Return different items based on call order + let callCount = 0; + testControllerMock + .setup((t) => t.createTestItem(typemoq.It.isAny(), typemoq.It.isAny(), typemoq.It.isAny())) + .returns(() => { + callCount++; + return callCount === 1 ? mockSubtest1 : mockSubtest2; + }); + + executionHandler.processExecution( + payload1, + runInstanceMock.object, + testItemIndexMock.object, + testControllerMock.object, + ); + + // Second subtest: should have existing stats from first + testItemIndexMock.reset(); + testItemIndexMock + .setup((x) => x.getTestItem('parentTest', testControllerMock.object)) + .returns(() => mockParentItem); + testItemIndexMock.setup((x) => x.getSubtestStats('parentTest')).returns(() => ({ failed: 1, passed: 0 })); + + executionHandler.processExecution( + payload2, + runInstanceMock.object, + testItemIndexMock.object, + testControllerMock.object, + ); + + // Verify the first subtest set initial stats + runInstanceMock.verify((r) => r.started(mockSubtest1), typemoq.Times.once()); + runInstanceMock.verify((r) => r.started(mockSubtest2), typemoq.Times.once()); + }); + + test('should throw error when parent test item not found', () => { + const payload: ExecutionTestPayload = { + cwd: '/foo/bar', + status: 'success', + result: { + 'parentTest (subtest1)': { + test: 'parentTest', + outcome: 'subtest-failure', + message: 'Failed', + traceback: '', + subtest: 'subtest1', + }, + }, + error: '', + }; + + testItemIndexMock + .setup((x) => x.getTestItem('parentTest', testControllerMock.object)) + .returns(() => undefined); + + assert.throws(() => { + executionHandler.processExecution( + payload, + runInstanceMock.object, + testItemIndexMock.object, + testControllerMock.object, + ); + }, /Parent test item not found/); + }); + }); + + suite('handleSubtestSuccess', () => { + test('should create passing subtest', () => { + const payload: ExecutionTestPayload = { + cwd: '/foo/bar', + status: 'success', + result: { + 'parentTest (subtest1)': { + test: 'parentTest', + outcome: 'subtest-success', + message: '', + traceback: '', + subtest: 'subtest1', + }, + }, + error: '', + }; + + const mockSubtestItem = createMockTestItem('subtest1', 'Subtest 1'); + + testItemIndexMock + .setup((x) => x.getTestItem('parentTest', testControllerMock.object)) + .returns(() => mockParentItem); + testItemIndexMock.setup((x) => x.getSubtestStats('parentTest')).returns(() => undefined); + testItemIndexMock + .setup((x) => x.setSubtestStats('parentTest', typemoq.It.isAny())) + .returns(() => undefined); + testControllerMock + .setup((t) => t.createTestItem(typemoq.It.isAny(), typemoq.It.isAny(), typemoq.It.isAny())) + .returns(() => mockSubtestItem); + + executionHandler.processExecution( + payload, + runInstanceMock.object, + testItemIndexMock.object, + testControllerMock.object, + ); + + // Verify stats were set correctly + testItemIndexMock.verify( + (x) => + x.setSubtestStats( + 'parentTest', + typemoq.It.is((stats) => stats.passed === 1 && stats.failed === 0), + ), + typemoq.Times.once(), + ); + + runInstanceMock.verify((r) => r.started(mockSubtestItem), typemoq.Times.once()); + runInstanceMock.verify((r) => r.passed(mockSubtestItem), typemoq.Times.once()); + }); + + test('should handle subtest with special characters in name', () => { + const payload: ExecutionTestPayload = { + cwd: '/foo/bar', + status: 'success', + result: { + 'parentTest [subtest with spaces and [brackets]]': { + test: 'parentTest', + outcome: 'subtest-success', + message: '', + traceback: '', + subtest: 'subtest with spaces and [brackets]', + }, + }, + error: '', + }; + + const mockSubtestItem = createMockTestItem('[subtest with spaces and [brackets]]', 'Subtest'); + + testItemIndexMock + .setup((x) => x.getTestItem('parentTest', testControllerMock.object)) + .returns(() => mockParentItem); + testItemIndexMock.setup((x) => x.getSubtestStats('parentTest')).returns(() => undefined); + testItemIndexMock + .setup((x) => x.setSubtestStats('parentTest', typemoq.It.isAny())) + .returns(() => undefined); + testControllerMock + .setup((t) => t.createTestItem(typemoq.It.isAny(), typemoq.It.isAny(), typemoq.It.isAny())) + .returns(() => mockSubtestItem); + + executionHandler.processExecution( + payload, + runInstanceMock.object, + testItemIndexMock.object, + testControllerMock.object, + ); + + runInstanceMock.verify((r) => r.passed(mockSubtestItem), typemoq.Times.once()); + }); + }); + + suite('Comprehensive Subtest Scenarios', () => { + test('should handle mixed passing and failing subtests in sequence', () => { + // Simulates unittest with subtests like: test_even with i=0,1,2,3,4,5 + const mockSubtest0 = createMockTestItem('(i=0)', '(i=0)'); + const mockSubtest1 = createMockTestItem('(i=1)', '(i=1)'); + const mockSubtest2 = createMockTestItem('(i=2)', '(i=2)'); + const mockSubtest3 = createMockTestItem('(i=3)', '(i=3)'); + const mockSubtest4 = createMockTestItem('(i=4)', '(i=4)'); + const mockSubtest5 = createMockTestItem('(i=5)', '(i=5)'); + + const subtestItems = [mockSubtest0, mockSubtest1, mockSubtest2, mockSubtest3, mockSubtest4, mockSubtest5]; + + testItemIndexMock + .setup((x) => x.getTestItem('test_even', testControllerMock.object)) + .returns(() => mockParentItem); + + let subtestCallCount = 0; + testControllerMock + .setup((t) => t.createTestItem(typemoq.It.isAny(), typemoq.It.isAny(), typemoq.It.isAny())) + .returns(() => subtestItems[subtestCallCount++]); + + // First subtest (i=0) - passes + testItemIndexMock.setup((x) => x.getSubtestStats('test_even')).returns(() => undefined); + testItemIndexMock.setup((x) => x.setSubtestStats('test_even', typemoq.It.isAny())).returns(() => undefined); + + const payload0: ExecutionTestPayload = { + cwd: '/foo/bar', + status: 'success', + result: { + 'test_even (i=0)': { + test: 'test_even', + outcome: 'subtest-success', + message: '', + traceback: '', + subtest: '(i=0)', + }, + }, + error: '', + }; + + executionHandler.processExecution( + payload0, + runInstanceMock.object, + testItemIndexMock.object, + testControllerMock.object, + ); + + // Verify first subtest created stats + testItemIndexMock.verify( + (x) => + x.setSubtestStats( + 'test_even', + typemoq.It.is((stats) => stats.passed === 1 && stats.failed === 0), + ), + typemoq.Times.once(), + ); + + // Second subtest (i=1) - fails + testItemIndexMock.reset(); + testItemIndexMock + .setup((x) => x.getTestItem('test_even', testControllerMock.object)) + .returns(() => mockParentItem); + testItemIndexMock.setup((x) => x.getSubtestStats('test_even')).returns(() => ({ passed: 1, failed: 0 })); + + const payload1: ExecutionTestPayload = { + cwd: '/foo/bar', + status: 'success', + result: { + 'test_even (i=1)': { + test: 'test_even', + outcome: 'subtest-failure', + message: '1 is not even', + traceback: 'AssertionError', + subtest: '(i=1)', + }, + }, + error: '', + }; + + executionHandler.processExecution( + payload1, + runInstanceMock.object, + testItemIndexMock.object, + testControllerMock.object, + ); + + // Third subtest (i=2) - passes + testItemIndexMock.reset(); + testItemIndexMock + .setup((x) => x.getTestItem('test_even', testControllerMock.object)) + .returns(() => mockParentItem); + testItemIndexMock.setup((x) => x.getSubtestStats('test_even')).returns(() => ({ passed: 1, failed: 1 })); + + const payload2: ExecutionTestPayload = { + cwd: '/foo/bar', + status: 'success', + result: { + 'test_even (i=2)': { + test: 'test_even', + outcome: 'subtest-success', + message: '', + traceback: '', + subtest: '(i=2)', + }, + }, + error: '', + }; + + executionHandler.processExecution( + payload2, + runInstanceMock.object, + testItemIndexMock.object, + testControllerMock.object, + ); + + // Verify all subtests were started and had outcomes + runInstanceMock.verify((r) => r.started(mockSubtest0), typemoq.Times.once()); + runInstanceMock.verify((r) => r.passed(mockSubtest0), typemoq.Times.once()); + runInstanceMock.verify((r) => r.started(mockSubtest1), typemoq.Times.once()); + runInstanceMock.verify((r) => r.failed(mockSubtest1, typemoq.It.isAny()), typemoq.Times.once()); + runInstanceMock.verify((r) => r.started(mockSubtest2), typemoq.Times.once()); + runInstanceMock.verify((r) => r.passed(mockSubtest2), typemoq.Times.once()); + }); + + test('should persist stats across multiple processExecution calls', () => { + // Test that stats persist in TestItemIndex across multiple processExecution calls + const mockSubtest1 = createMockTestItem('subtest1', 'Subtest 1'); + const mockSubtest2 = createMockTestItem('subtest2', 'Subtest 2'); + + testItemIndexMock + .setup((x) => x.getTestItem('parentTest', testControllerMock.object)) + .returns(() => mockParentItem); + testItemIndexMock.setup((x) => x.getSubtestStats('parentTest')).returns(() => undefined); + testItemIndexMock + .setup((x) => x.setSubtestStats('parentTest', typemoq.It.isAny())) + .returns(() => undefined); + + let callCount = 0; + testControllerMock + .setup((t) => t.createTestItem(typemoq.It.isAny(), typemoq.It.isAny(), typemoq.It.isAny())) + .returns(() => (callCount++ === 0 ? mockSubtest1 : mockSubtest2)); + + const payload1: ExecutionTestPayload = { + cwd: '/foo/bar', + status: 'success', + result: { + 'parentTest (subtest1)': { + test: 'parentTest', + outcome: 'subtest-success', + message: '', + traceback: '', + subtest: 'subtest1', + }, + }, + error: '', + }; + + // First call - no existing stats + executionHandler.processExecution( + payload1, + runInstanceMock.object, + testItemIndexMock.object, + testControllerMock.object, + ); + + // Simulate stats being stored in TestItemIndex + testItemIndexMock.reset(); + testItemIndexMock + .setup((x) => x.getTestItem('parentTest', testControllerMock.object)) + .returns(() => mockParentItem); + testItemIndexMock.setup((x) => x.getSubtestStats('parentTest')).returns(() => ({ passed: 1, failed: 0 })); + + const payload2: ExecutionTestPayload = { + cwd: '/foo/bar', + status: 'success', + result: { + 'parentTest (subtest2)': { + test: 'parentTest', + outcome: 'subtest-failure', + message: 'Failed', + traceback: '', + subtest: 'subtest2', + }, + }, + error: '', + }; + + // Second call - existing stats should be retrieved and updated + executionHandler.processExecution( + payload2, + runInstanceMock.object, + testItemIndexMock.object, + testControllerMock.object, + ); + + // Verify getSubtestStats was called to retrieve existing stats + testItemIndexMock.verify((x) => x.getSubtestStats('parentTest'), typemoq.Times.once()); + + // Verify both subtests were processed + runInstanceMock.verify((r) => r.passed(mockSubtest1), typemoq.Times.once()); + runInstanceMock.verify((r) => r.failed(mockSubtest2, typemoq.It.isAny()), typemoq.Times.once()); + }); + + test('should clear children only on first subtest when no existing stats', () => { + // When first subtest arrives, children should be cleared + // Subsequent subtests should NOT clear children + const mockSubtest1 = createMockTestItem('subtest1', 'Subtest 1'); + + testItemIndexMock + .setup((x) => x.getTestItem('parentTest', testControllerMock.object)) + .returns(() => mockParentItem); + testItemIndexMock.setup((x) => x.getSubtestStats('parentTest')).returns(() => undefined); + testItemIndexMock + .setup((x) => x.setSubtestStats('parentTest', typemoq.It.isAny())) + .returns(() => undefined); + testControllerMock + .setup((t) => t.createTestItem(typemoq.It.isAny(), typemoq.It.isAny(), typemoq.It.isAny())) + .returns(() => mockSubtest1); + + const payload: ExecutionTestPayload = { + cwd: '/foo/bar', + status: 'success', + result: { + 'parentTest (subtest1)': { + test: 'parentTest', + outcome: 'subtest-success', + message: '', + traceback: '', + subtest: 'subtest1', + }, + }, + error: '', + }; + + executionHandler.processExecution( + payload, + runInstanceMock.object, + testItemIndexMock.object, + testControllerMock.object, + ); + + // Verify setSubtestStats was called (which happens when creating new stats) + testItemIndexMock.verify((x) => x.setSubtestStats('parentTest', typemoq.It.isAny()), typemoq.Times.once()); + }); + }); +}); + +function createMockTestItem(id: string, label: string): TestItem { + const range = new Range(0, 0, 0, 0); + const mockChildren = typemoq.Mock.ofType(); + mockChildren.setup((x) => x.add(typemoq.It.isAny())).returns(() => undefined); + + const mockTestItem = ({ + id, + label, + canResolveChildren: false, + tags: [], + children: mockChildren.object, + range, + uri: Uri.file('/foo/bar/test.py'), + parent: undefined, + } as unknown) as TestItem; + + return mockTestItem; +} diff --git a/src/test/testing/testController/common/testItemIndex.unit.test.ts b/src/test/testing/testController/common/testItemIndex.unit.test.ts new file mode 100644 index 000000000000..6712d90ff667 --- /dev/null +++ b/src/test/testing/testController/common/testItemIndex.unit.test.ts @@ -0,0 +1,359 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { TestController, TestItem, Uri, Range, TestItemCollection } from 'vscode'; +import * as typemoq from 'typemoq'; +import * as assert from 'assert'; +import * as sinon from 'sinon'; +import { TestItemIndex } from '../../../../client/testing/testController/common/testItemIndex'; + +suite('TestItemIndex', () => { + let testItemIndex: TestItemIndex; + let testControllerMock: typemoq.IMock; + let mockTestItem1: TestItem; + let mockTestItem2: TestItem; + let mockParentItem: TestItem; + + setup(() => { + testItemIndex = new TestItemIndex(); + testControllerMock = typemoq.Mock.ofType(); + + // Create mock test items + mockTestItem1 = createMockTestItem('test1', 'Test 1'); + mockTestItem2 = createMockTestItem('test2', 'Test 2'); + mockParentItem = createMockTestItem('parent', 'Parent'); + }); + + teardown(() => { + sinon.restore(); + }); + + suite('registerTestItem', () => { + test('should store all three mappings correctly', () => { + const runId = 'test_file.py::test_example'; + const vsId = 'test_file.py::test_example'; + + testItemIndex.registerTestItem(runId, vsId, mockTestItem1); + + assert.strictEqual(testItemIndex.runIdToTestItemMap.get(runId), mockTestItem1); + assert.strictEqual(testItemIndex.runIdToVSidMap.get(runId), vsId); + assert.strictEqual(testItemIndex.vsIdToRunIdMap.get(vsId), runId); + }); + + test('should overwrite existing mappings', () => { + const runId = 'test_file.py::test_example'; + const vsId = 'test_file.py::test_example'; + + testItemIndex.registerTestItem(runId, vsId, mockTestItem1); + testItemIndex.registerTestItem(runId, vsId, mockTestItem2); + + assert.strictEqual(testItemIndex.runIdToTestItemMap.get(runId), mockTestItem2); + }); + + test('should handle different runId and vsId', () => { + const runId = 'test_file.py::TestClass::test_method'; + const vsId = 'different_id'; + + testItemIndex.registerTestItem(runId, vsId, mockTestItem1); + + assert.strictEqual(testItemIndex.runIdToVSidMap.get(runId), vsId); + assert.strictEqual(testItemIndex.vsIdToRunIdMap.get(vsId), runId); + }); + }); + + suite('getTestItem', () => { + test('should return item on direct lookup when valid', () => { + const runId = 'test_file.py::test_example'; + const vsId = 'test_file.py::test_example'; + + // Register the item + testItemIndex.registerTestItem(runId, vsId, mockTestItem1); + + // Mock the validation to return true + const isValidStub = sinon.stub(testItemIndex, 'isTestItemValid').returns(true); + + const result = testItemIndex.getTestItem(runId, testControllerMock.object); + + assert.strictEqual(result, mockTestItem1); + assert.ok(isValidStub.calledOnce); + }); + + test('should remove stale item and try vsId fallback', () => { + const runId = 'test_file.py::test_example'; + const vsId = 'test_file.py::test_example'; + + testItemIndex.registerTestItem(runId, vsId, mockTestItem1); + + // Mock validation to fail on first call (stale item) + const isValidStub = sinon.stub(testItemIndex, 'isTestItemValid').returns(false); + + // Setup controller to not find the item + const testItemCollectionMock = typemoq.Mock.ofType(); + testItemCollectionMock.setup((x) => x.forEach(typemoq.It.isAny())).returns(() => undefined); + testControllerMock.setup((t) => t.items).returns(() => testItemCollectionMock.object); + + const result = testItemIndex.getTestItem(runId, testControllerMock.object); + + // Should have removed the stale item + assert.strictEqual(testItemIndex.runIdToTestItemMap.get(runId), undefined); + assert.strictEqual(result, undefined); + assert.ok(isValidStub.calledOnce); + }); + + test('should perform vsId search when direct lookup is stale', () => { + const runId = 'test_file.py::test_example'; + const vsId = 'test_file.py::test_example'; + + // Create test item with correct ID + const searchableTestItem = createMockTestItem(vsId, 'Test Example'); + + testItemIndex.registerTestItem(runId, vsId, searchableTestItem); + + // First validation fails (stale), need to search by vsId + sinon.stub(testItemIndex, 'isTestItemValid').returns(false); + + // Setup controller to find item by vsId + const testItemCollectionMock = typemoq.Mock.ofType(); + testItemCollectionMock + .setup((x) => x.forEach(typemoq.It.isAny())) + .callback((callback) => { + callback(searchableTestItem); + }) + .returns(() => undefined); + testControllerMock.setup((t) => t.items).returns(() => testItemCollectionMock.object); + + const result = testItemIndex.getTestItem(runId, testControllerMock.object); + + // Should recache the found item + assert.strictEqual(testItemIndex.runIdToTestItemMap.get(runId), searchableTestItem); + assert.strictEqual(result, searchableTestItem); + }); + + test('should return undefined if not found anywhere', () => { + const runId = 'nonexistent'; + + const testItemCollectionMock = typemoq.Mock.ofType(); + testItemCollectionMock.setup((x) => x.forEach(typemoq.It.isAny())).returns(() => undefined); + testControllerMock.setup((t) => t.items).returns(() => testItemCollectionMock.object); + + const result = testItemIndex.getTestItem(runId, testControllerMock.object); + + assert.strictEqual(result, undefined); + }); + }); + + suite('getRunId and getVSId', () => { + test('getRunId should convert VS Code ID to Python run ID', () => { + const runId = 'test_file.py::test_example'; + const vsId = 'vscode_id'; + + testItemIndex.registerTestItem(runId, vsId, mockTestItem1); + + assert.strictEqual(testItemIndex.getRunId(vsId), runId); + }); + + test('getRunId should return undefined for unknown vsId', () => { + assert.strictEqual(testItemIndex.getRunId('unknown'), undefined); + }); + + test('getVSId should convert Python run ID to VS Code ID', () => { + const runId = 'test_file.py::test_example'; + const vsId = 'vscode_id'; + + testItemIndex.registerTestItem(runId, vsId, mockTestItem1); + + assert.strictEqual(testItemIndex.getVSId(runId), vsId); + }); + + test('getVSId should return undefined for unknown runId', () => { + assert.strictEqual(testItemIndex.getVSId('unknown'), undefined); + }); + }); + + suite('clear', () => { + test('should remove all mappings', () => { + testItemIndex.registerTestItem('runId1', 'vsId1', mockTestItem1); + testItemIndex.registerTestItem('runId2', 'vsId2', mockTestItem2); + + assert.strictEqual(testItemIndex.runIdToTestItemMap.size, 2); + assert.strictEqual(testItemIndex.runIdToVSidMap.size, 2); + assert.strictEqual(testItemIndex.vsIdToRunIdMap.size, 2); + + testItemIndex.clear(); + + assert.strictEqual(testItemIndex.runIdToTestItemMap.size, 0); + assert.strictEqual(testItemIndex.runIdToVSidMap.size, 0); + assert.strictEqual(testItemIndex.vsIdToRunIdMap.size, 0); + }); + + test('should handle clearing empty index', () => { + testItemIndex.clear(); + + assert.strictEqual(testItemIndex.runIdToTestItemMap.size, 0); + assert.strictEqual(testItemIndex.runIdToVSidMap.size, 0); + assert.strictEqual(testItemIndex.vsIdToRunIdMap.size, 0); + }); + }); + + suite('isTestItemValid', () => { + test('should return true for item with valid parent chain leading to controller', () => { + const childItem = createMockTestItem('child', 'Child'); + (childItem as any).parent = mockParentItem; + + const testItemCollectionMock = typemoq.Mock.ofType(); + testItemCollectionMock.setup((x) => x.get(mockParentItem.id)).returns(() => mockParentItem); + testControllerMock.setup((t) => t.items).returns(() => testItemCollectionMock.object); + + const result = testItemIndex.isTestItemValid(childItem, testControllerMock.object); + + assert.strictEqual(result, true); + }); + + test('should return false for orphaned item', () => { + const orphanedItem = createMockTestItem('orphaned', 'Orphaned'); + (orphanedItem as any).parent = mockParentItem; + + const testItemCollectionMock = typemoq.Mock.ofType(); + testItemCollectionMock.setup((x) => x.get(typemoq.It.isAny())).returns(() => undefined); + testControllerMock.setup((t) => t.items).returns(() => testItemCollectionMock.object); + + const result = testItemIndex.isTestItemValid(orphanedItem, testControllerMock.object); + + assert.strictEqual(result, false); + }); + + test('should return true for root item in controller', () => { + const testItemCollectionMock = typemoq.Mock.ofType(); + testItemCollectionMock.setup((x) => x.get(mockTestItem1.id)).returns(() => mockTestItem1); + testControllerMock.setup((t) => t.items).returns(() => testItemCollectionMock.object); + + const result = testItemIndex.isTestItemValid(mockTestItem1, testControllerMock.object); + + assert.strictEqual(result, true); + }); + + test('should return false for item not in controller and no parent', () => { + const testItemCollectionMock = typemoq.Mock.ofType(); + testItemCollectionMock.setup((x) => x.get(typemoq.It.isAny())).returns(() => undefined); + testControllerMock.setup((t) => t.items).returns(() => testItemCollectionMock.object); + + const result = testItemIndex.isTestItemValid(mockTestItem1, testControllerMock.object); + + assert.strictEqual(result, false); + }); + }); + + suite('cleanupStaleReferences', () => { + test('should remove items not in controller', () => { + const runId1 = 'test1'; + const runId2 = 'test2'; + const vsId1 = 'vs1'; + const vsId2 = 'vs2'; + + testItemIndex.registerTestItem(runId1, vsId1, mockTestItem1); + testItemIndex.registerTestItem(runId2, vsId2, mockTestItem2); + + // Mock validation: first item invalid, second valid + const isValidStub = sinon.stub(testItemIndex, 'isTestItemValid'); + isValidStub.onFirstCall().returns(false); // mockTestItem1 is invalid + isValidStub.onSecondCall().returns(true); // mockTestItem2 is valid + + testItemIndex.cleanupStaleReferences(testControllerMock.object); + + // First item should be removed + assert.strictEqual(testItemIndex.runIdToTestItemMap.get(runId1), undefined); + assert.strictEqual(testItemIndex.runIdToVSidMap.get(runId1), undefined); + assert.strictEqual(testItemIndex.vsIdToRunIdMap.get(vsId1), undefined); + + // Second item should remain + assert.strictEqual(testItemIndex.runIdToTestItemMap.get(runId2), mockTestItem2); + assert.strictEqual(testItemIndex.runIdToVSidMap.get(runId2), vsId2); + assert.strictEqual(testItemIndex.vsIdToRunIdMap.get(vsId2), runId2); + }); + + test('should keep all valid items', () => { + const runId1 = 'test1'; + const vsId1 = 'vs1'; + + testItemIndex.registerTestItem(runId1, vsId1, mockTestItem1); + + sinon.stub(testItemIndex, 'isTestItemValid').returns(true); + + testItemIndex.cleanupStaleReferences(testControllerMock.object); + + // Item should still be there + assert.strictEqual(testItemIndex.runIdToTestItemMap.get(runId1), mockTestItem1); + assert.strictEqual(testItemIndex.runIdToVSidMap.get(runId1), vsId1); + assert.strictEqual(testItemIndex.vsIdToRunIdMap.get(vsId1), runId1); + }); + + test('should handle empty index', () => { + testItemIndex.cleanupStaleReferences(testControllerMock.object); + + assert.strictEqual(testItemIndex.runIdToTestItemMap.size, 0); + }); + + test('should remove all items when all are invalid', () => { + testItemIndex.registerTestItem('test1', 'vs1', mockTestItem1); + testItemIndex.registerTestItem('test2', 'vs2', mockTestItem2); + + sinon.stub(testItemIndex, 'isTestItemValid').returns(false); + + testItemIndex.cleanupStaleReferences(testControllerMock.object); + + assert.strictEqual(testItemIndex.runIdToTestItemMap.size, 0); + assert.strictEqual(testItemIndex.runIdToVSidMap.size, 0); + assert.strictEqual(testItemIndex.vsIdToRunIdMap.size, 0); + }); + }); + + suite('Backward compatibility getters', () => { + test('runIdToTestItemMap should return the internal map', () => { + const runId = 'test1'; + testItemIndex.registerTestItem(runId, 'vs1', mockTestItem1); + + const map = testItemIndex.runIdToTestItemMap; + + assert.strictEqual(map.get(runId), mockTestItem1); + }); + + test('runIdToVSidMap should return the internal map', () => { + const runId = 'test1'; + const vsId = 'vs1'; + testItemIndex.registerTestItem(runId, vsId, mockTestItem1); + + const map = testItemIndex.runIdToVSidMap; + + assert.strictEqual(map.get(runId), vsId); + }); + + test('vsIdToRunIdMap should return the internal map', () => { + const runId = 'test1'; + const vsId = 'vs1'; + testItemIndex.registerTestItem(runId, vsId, mockTestItem1); + + const map = testItemIndex.vsIdToRunIdMap; + + assert.strictEqual(map.get(vsId), runId); + }); + }); +}); + +function createMockTestItem(id: string, label: string): TestItem { + const range = new Range(0, 0, 0, 0); + const mockChildren = typemoq.Mock.ofType(); + mockChildren.setup((x) => x.add(typemoq.It.isAny())).returns(() => undefined); + + const mockTestItem = ({ + id, + label, + canResolveChildren: false, + tags: [], + children: mockChildren.object, + range, + uri: Uri.file('/foo/bar'), + parent: undefined, + } as unknown) as TestItem; + + return mockTestItem; +} diff --git a/src/test/testing/testController/common/testProjectRegistry.unit.test.ts b/src/test/testing/testController/common/testProjectRegistry.unit.test.ts new file mode 100644 index 000000000000..5d04930d0e88 --- /dev/null +++ b/src/test/testing/testController/common/testProjectRegistry.unit.test.ts @@ -0,0 +1,440 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { expect } from 'chai'; +import * as path from 'path'; +import * as sinon from 'sinon'; +import { TestController, Uri } from 'vscode'; +import { IConfigurationService } from '../../../../client/common/types'; +import { IEnvironmentVariablesProvider } from '../../../../client/common/variables/types'; +import { IInterpreterService } from '../../../../client/interpreter/contracts'; +import { TestProjectRegistry } from '../../../../client/testing/testController/common/testProjectRegistry'; +import * as envExtApiInternal from '../../../../client/envExt/api.internal'; +import { PythonProject, PythonEnvironment } from '../../../../client/envExt/types'; + +suite('TestProjectRegistry', () => { + let sandbox: sinon.SinonSandbox; + let testController: TestController; + let configSettings: IConfigurationService; + let interpreterService: IInterpreterService; + let envVarsService: IEnvironmentVariablesProvider; + let registry: TestProjectRegistry; + + setup(() => { + sandbox = sinon.createSandbox(); + + // Create mock test controller + testController = ({ + items: { + get: sandbox.stub(), + add: sandbox.stub(), + delete: sandbox.stub(), + forEach: sandbox.stub(), + }, + createTestItem: sandbox.stub(), + dispose: sandbox.stub(), + } as unknown) as TestController; + + // Create mock config settings + configSettings = ({ + getSettings: sandbox.stub().returns({ + testing: { + pytestEnabled: true, + unittestEnabled: false, + }, + }), + } as unknown) as IConfigurationService; + + // Create mock interpreter service + interpreterService = ({ + getActiveInterpreter: sandbox.stub().resolves({ + displayName: 'Python 3.11', + path: '/usr/bin/python3', + version: { raw: '3.11.8' }, + sysPrefix: '/usr', + }), + } as unknown) as IInterpreterService; + + // Create mock env vars service + envVarsService = ({ + getEnvironmentVariables: sandbox.stub().resolves({}), + } as unknown) as IEnvironmentVariablesProvider; + + registry = new TestProjectRegistry(testController, configSettings, interpreterService, envVarsService); + }); + + teardown(() => { + sandbox.restore(); + }); + + suite('hasProjects', () => { + test('should return false for uninitialized workspace', () => { + const workspaceUri = Uri.file('/workspace'); + + const result = registry.hasProjects(workspaceUri); + + expect(result).to.be.false; + }); + + test('should return true after projects are registered', async () => { + const workspaceUri = Uri.file('/workspace'); + + // Mock useEnvExtension to return false to use default project path + sandbox.stub(envExtApiInternal, 'useEnvExtension').returns(false); + + await registry.discoverAndRegisterProjects(workspaceUri); + + const result = registry.hasProjects(workspaceUri); + + expect(result).to.be.true; + }); + }); + + suite('getProjectsArray', () => { + test('should return empty array for uninitialized workspace', () => { + const workspaceUri = Uri.file('/workspace'); + + const result = registry.getProjectsArray(workspaceUri); + + expect(result).to.be.an('array').that.is.empty; + }); + + test('should return projects after registration', async () => { + const workspaceUri = Uri.file('/workspace'); + + // Mock useEnvExtension to return false to use default project path + sandbox.stub(envExtApiInternal, 'useEnvExtension').returns(false); + + await registry.discoverAndRegisterProjects(workspaceUri); + + const result = registry.getProjectsArray(workspaceUri); + + expect(result).to.be.an('array').with.length(1); + expect(result[0].projectUri.fsPath).to.equal(workspaceUri.fsPath); + }); + }); + + suite('discoverAndRegisterProjects', () => { + test('should create default project when env extension not available', async () => { + const workspaceUri = Uri.file('/workspace/myproject'); + + sandbox.stub(envExtApiInternal, 'useEnvExtension').returns(false); + + const projects = await registry.discoverAndRegisterProjects(workspaceUri); + + expect(projects).to.have.length(1); + expect(projects[0].projectUri.fsPath).to.equal(workspaceUri.fsPath); + expect(projects[0].testProvider).to.equal('pytest'); + }); + + test('should use unittest when configured', async () => { + const workspaceUri = Uri.file('/workspace/myproject'); + + (configSettings.getSettings as sinon.SinonStub).returns({ + testing: { + pytestEnabled: false, + unittestEnabled: true, + }, + }); + + sandbox.stub(envExtApiInternal, 'useEnvExtension').returns(false); + + const projects = await registry.discoverAndRegisterProjects(workspaceUri); + + expect(projects).to.have.length(1); + expect(projects[0].testProvider).to.equal('unittest'); + }); + + test('should discover projects from Python Environments API', async () => { + const workspaceUri = Uri.file('/workspace'); + const projectUri = Uri.file('/workspace/project1'); + + const mockPythonProject: PythonProject = { + name: 'project1', + uri: projectUri, + }; + + const mockPythonEnv: PythonEnvironment = { + name: 'env1', + displayName: 'Python 3.11', + shortDisplayName: 'Python 3.11', + displayPath: '/usr/bin/python3', + version: '3.11.8', + environmentPath: Uri.file('/usr/bin/python3'), + sysPrefix: '/usr', + execInfo: { run: { executable: '/usr/bin/python3' } }, + envId: { id: 'env1', managerId: 'manager1' }, + }; + + sandbox.stub(envExtApiInternal, 'useEnvExtension').returns(true); + sandbox.stub(envExtApiInternal, 'getEnvExtApi').resolves({ + getPythonProjects: () => [mockPythonProject], + getEnvironment: sandbox.stub().resolves(mockPythonEnv), + } as any); + + const projects = await registry.discoverAndRegisterProjects(workspaceUri); + + expect(projects).to.have.length(1); + expect(projects[0].projectName).to.include('project1'); + expect(projects[0].pythonEnvironment).to.deep.equal(mockPythonEnv); + }); + + test('should filter projects to current workspace', async () => { + const workspaceUri = Uri.file('/workspace1'); + const projectInWorkspace = Uri.file('/workspace1/project1'); + const projectOutsideWorkspace = Uri.file('/workspace2/project2'); + + const mockProjects: PythonProject[] = [ + { name: 'project1', uri: projectInWorkspace }, + { name: 'project2', uri: projectOutsideWorkspace }, + ]; + + const mockPythonEnv: PythonEnvironment = { + name: 'env1', + displayName: 'Python 3.11', + shortDisplayName: 'Python 3.11', + displayPath: '/usr/bin/python3', + version: '3.11.8', + environmentPath: Uri.file('/usr/bin/python3'), + sysPrefix: '/usr', + execInfo: { run: { executable: '/usr/bin/python3' } }, + envId: { id: 'env1', managerId: 'manager1' }, + }; + + sandbox.stub(envExtApiInternal, 'useEnvExtension').returns(true); + sandbox.stub(envExtApiInternal, 'getEnvExtApi').resolves({ + getPythonProjects: () => mockProjects, + getEnvironment: sandbox.stub().resolves(mockPythonEnv), + } as any); + + const projects = await registry.discoverAndRegisterProjects(workspaceUri); + + expect(projects).to.have.length(1); + expect(projects[0].projectUri.fsPath).to.equal(projectInWorkspace.fsPath); + }); + + test('should fallback to default project when no projects found', async () => { + const workspaceUri = Uri.file('/workspace'); + + sandbox.stub(envExtApiInternal, 'useEnvExtension').returns(true); + sandbox.stub(envExtApiInternal, 'getEnvExtApi').resolves({ + getPythonProjects: () => [], + } as any); + + const projects = await registry.discoverAndRegisterProjects(workspaceUri); + + expect(projects).to.have.length(1); + expect(projects[0].projectUri.fsPath).to.equal(workspaceUri.fsPath); + }); + + test('should fallback to default project on API error', async () => { + const workspaceUri = Uri.file('/workspace'); + + sandbox.stub(envExtApiInternal, 'useEnvExtension').returns(true); + sandbox.stub(envExtApiInternal, 'getEnvExtApi').rejects(new Error('API error')); + + const projects = await registry.discoverAndRegisterProjects(workspaceUri); + + expect(projects).to.have.length(1); + expect(projects[0].projectUri.fsPath).to.equal(workspaceUri.fsPath); + }); + }); + + suite('configureNestedProjectIgnores', () => { + test('should not set ignores when no nested projects', async () => { + const workspaceUri = Uri.file('/workspace'); + const projectUri = Uri.file('/workspace/project1'); + + const mockPythonProject: PythonProject = { + name: 'project1', + uri: projectUri, + }; + + const mockPythonEnv: PythonEnvironment = { + name: 'env1', + displayName: 'Python 3.11', + shortDisplayName: 'Python 3.11', + displayPath: '/usr/bin/python3', + version: '3.11.8', + environmentPath: Uri.file('/usr/bin/python3'), + sysPrefix: '/usr', + execInfo: { run: { executable: '/usr/bin/python3' } }, + envId: { id: 'env1', managerId: 'manager1' }, + }; + + sandbox.stub(envExtApiInternal, 'useEnvExtension').returns(true); + sandbox.stub(envExtApiInternal, 'getEnvExtApi').resolves({ + getPythonProjects: () => [mockPythonProject], + getEnvironment: sandbox.stub().resolves(mockPythonEnv), + } as any); + + await registry.discoverAndRegisterProjects(workspaceUri); + registry.configureNestedProjectIgnores(workspaceUri); + + const projects = registry.getProjectsArray(workspaceUri); + expect(projects[0].nestedProjectPathsToIgnore).to.be.undefined; + }); + + test('should configure ignore paths for nested projects', async () => { + const workspaceUri = Uri.file('/workspace'); + const parentProjectUri = Uri.file('/workspace/parent'); + const childProjectUri = Uri.file(path.join('/workspace/parent', 'child')); + + const mockProjects: PythonProject[] = [ + { name: 'parent', uri: parentProjectUri }, + { name: 'child', uri: childProjectUri }, + ]; + + const mockPythonEnv: PythonEnvironment = { + name: 'env1', + displayName: 'Python 3.11', + shortDisplayName: 'Python 3.11', + displayPath: '/usr/bin/python3', + version: '3.11.8', + environmentPath: Uri.file('/usr/bin/python3'), + sysPrefix: '/usr', + execInfo: { run: { executable: '/usr/bin/python3' } }, + envId: { id: 'env1', managerId: 'manager1' }, + }; + + sandbox.stub(envExtApiInternal, 'useEnvExtension').returns(true); + sandbox.stub(envExtApiInternal, 'getEnvExtApi').resolves({ + getPythonProjects: () => mockProjects, + getEnvironment: sandbox.stub().resolves(mockPythonEnv), + } as any); + + await registry.discoverAndRegisterProjects(workspaceUri); + registry.configureNestedProjectIgnores(workspaceUri); + + const projects = registry.getProjectsArray(workspaceUri); + const parentProject = projects.find((p) => p.projectUri.fsPath === parentProjectUri.fsPath); + + expect(parentProject?.nestedProjectPathsToIgnore).to.include(childProjectUri.fsPath); + }); + + test('should not set child project as ignored for sibling projects', async () => { + const workspaceUri = Uri.file('/workspace'); + const project1Uri = Uri.file('/workspace/project1'); + const project2Uri = Uri.file('/workspace/project2'); + + const mockProjects: PythonProject[] = [ + { name: 'project1', uri: project1Uri }, + { name: 'project2', uri: project2Uri }, + ]; + + const mockPythonEnv: PythonEnvironment = { + name: 'env1', + displayName: 'Python 3.11', + shortDisplayName: 'Python 3.11', + displayPath: '/usr/bin/python3', + version: '3.11.8', + environmentPath: Uri.file('/usr/bin/python3'), + sysPrefix: '/usr', + execInfo: { run: { executable: '/usr/bin/python3' } }, + envId: { id: 'env1', managerId: 'manager1' }, + }; + + sandbox.stub(envExtApiInternal, 'useEnvExtension').returns(true); + sandbox.stub(envExtApiInternal, 'getEnvExtApi').resolves({ + getPythonProjects: () => mockProjects, + getEnvironment: sandbox.stub().resolves(mockPythonEnv), + } as any); + + await registry.discoverAndRegisterProjects(workspaceUri); + registry.configureNestedProjectIgnores(workspaceUri); + + const projects = registry.getProjectsArray(workspaceUri); + projects.forEach((project) => { + expect(project.nestedProjectPathsToIgnore).to.be.undefined; + }); + }); + }); + + suite('clearWorkspace', () => { + test('should remove all projects for a workspace', async () => { + const workspaceUri = Uri.file('/workspace'); + + sandbox.stub(envExtApiInternal, 'useEnvExtension').returns(false); + + await registry.discoverAndRegisterProjects(workspaceUri); + expect(registry.hasProjects(workspaceUri)).to.be.true; + + registry.clearWorkspace(workspaceUri); + + expect(registry.hasProjects(workspaceUri)).to.be.false; + expect(registry.getProjectsArray(workspaceUri)).to.be.empty; + }); + + test('should not affect other workspaces', async () => { + const workspace1Uri = Uri.file('/workspace1'); + const workspace2Uri = Uri.file('/workspace2'); + + sandbox.stub(envExtApiInternal, 'useEnvExtension').returns(false); + + await registry.discoverAndRegisterProjects(workspace1Uri); + await registry.discoverAndRegisterProjects(workspace2Uri); + + registry.clearWorkspace(workspace1Uri); + + expect(registry.hasProjects(workspace1Uri)).to.be.false; + expect(registry.hasProjects(workspace2Uri)).to.be.true; + }); + }); + + suite('getWorkspaceProjects', () => { + test('should return undefined for uninitialized workspace', () => { + const workspaceUri = Uri.file('/workspace'); + + const result = registry.getWorkspaceProjects(workspaceUri); + + expect(result).to.be.undefined; + }); + + test('should return map after registration', async () => { + const workspaceUri = Uri.file('/workspace'); + + sandbox.stub(envExtApiInternal, 'useEnvExtension').returns(false); + + await registry.discoverAndRegisterProjects(workspaceUri); + + const result = registry.getWorkspaceProjects(workspaceUri); + + expect(result).to.be.instanceOf(Map); + expect(result?.size).to.equal(1); + }); + }); + + suite('ProjectAdapter properties', () => { + test('should create adapter with correct test infrastructure', async () => { + const workspaceUri = Uri.file('/workspace/myproject'); + + sandbox.stub(envExtApiInternal, 'useEnvExtension').returns(false); + + const projects = await registry.discoverAndRegisterProjects(workspaceUri); + const project = projects[0]; + + expect(project.projectName).to.be.a('string'); + expect(project.projectUri.fsPath).to.equal(workspaceUri.fsPath); + expect(project.workspaceUri.fsPath).to.equal(workspaceUri.fsPath); + expect(project.testProvider).to.equal('pytest'); + expect(project.discoveryAdapter).to.exist; + expect(project.executionAdapter).to.exist; + expect(project.resultResolver).to.exist; + expect(project.isDiscovering).to.be.false; + expect(project.isExecuting).to.be.false; + }); + + test('should include python environment details', async () => { + const workspaceUri = Uri.file('/workspace/myproject'); + + sandbox.stub(envExtApiInternal, 'useEnvExtension').returns(false); + + const projects = await registry.discoverAndRegisterProjects(workspaceUri); + const project = projects[0]; + + expect(project.pythonEnvironment).to.exist; + expect(project.pythonProject).to.exist; + expect(project.pythonProject.name).to.equal('myproject'); + }); + }); +}); diff --git a/src/test/testing/testController/controller.unit.test.ts b/src/test/testing/testController/controller.unit.test.ts new file mode 100644 index 000000000000..feb5f36fc797 --- /dev/null +++ b/src/test/testing/testController/controller.unit.test.ts @@ -0,0 +1,344 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import * as assert from 'assert'; +import * as sinon from 'sinon'; +import * as vscode from 'vscode'; +import { TestController, Uri } from 'vscode'; + +import { PYTEST_PROVIDER, UNITTEST_PROVIDER } from '../../../client/testing/common/constants'; +import * as envExtApiInternal from '../../../client/envExt/api.internal'; +import * as projectUtils from '../../../client/testing/testController/common/projectUtils'; +import { PythonTestController } from '../../../client/testing/testController/controller'; +import { TestProjectRegistry } from '../../../client/testing/testController/common/testProjectRegistry'; + +function createStubTestController(): TestController { + const disposable = { dispose: () => undefined }; + + const controller = ({ + items: { + forEach: sinon.stub(), + get: sinon.stub(), + add: sinon.stub(), + replace: sinon.stub(), + delete: sinon.stub(), + size: 0, + [Symbol.iterator]: sinon.stub(), + }, + createRunProfile: sinon.stub().returns(disposable), + createTestItem: sinon.stub(), + dispose: sinon.stub(), + resolveHandler: undefined, + refreshHandler: undefined, + } as unknown) as TestController; + + return controller; +} + +suite('PythonTestController', () => { + let sandbox: sinon.SinonSandbox; + + setup(() => { + sandbox = sinon.createSandbox(); + }); + + teardown(() => { + sandbox.restore(); + }); + + function createController(options?: { unittestEnabled?: boolean; interpreter?: any }): any { + const unittestEnabled = options?.unittestEnabled ?? false; + const interpreter = + options?.interpreter ?? + ({ + displayName: 'Python 3.11', + path: '/usr/bin/python3', + version: { raw: '3.11.8' }, + sysPrefix: '/usr', + } as any); + + const workspaceService = ({ workspaceFolders: [] } as unknown) as any; + const configSettings = ({ + getSettings: sandbox.stub().returns({ + testing: { + unittestEnabled, + autoTestDiscoverOnSaveEnabled: false, + }, + }), + } as unknown) as any; + + const pytest = ({} as unknown) as any; + const unittest = ({} as unknown) as any; + const disposables: any[] = []; + const interpreterService = ({ + getActiveInterpreter: sandbox.stub().resolves(interpreter), + } as unknown) as any; + + const commandManager = ({ + registerCommand: sandbox.stub().returns({ dispose: () => undefined }), + } as unknown) as any; + const pythonExecFactory = ({} as unknown) as any; + const debugLauncher = ({} as unknown) as any; + const envVarsService = ({} as unknown) as any; + + return new PythonTestController( + workspaceService, + configSettings, + pytest, + unittest, + disposables, + interpreterService, + commandManager, + pythonExecFactory, + debugLauncher, + envVarsService, + ); + } + + suite('getTestProvider', () => { + test('returns unittest when enabled', () => { + const controller = createController({ unittestEnabled: true }); + const workspaceUri: Uri = vscode.Uri.file('/workspace'); + + const provider = (controller as any).getTestProvider(workspaceUri); + + assert.strictEqual(provider, UNITTEST_PROVIDER); + }); + + test('returns pytest when unittest not enabled', () => { + const controller = createController({ unittestEnabled: false }); + const workspaceUri: Uri = vscode.Uri.file('/workspace'); + + const provider = (controller as any).getTestProvider(workspaceUri); + + assert.strictEqual(provider, PYTEST_PROVIDER); + }); + }); + + suite('createDefaultProject (via TestProjectRegistry)', () => { + test('creates a single default project using active interpreter', async () => { + const workspaceUri: Uri = vscode.Uri.file('/workspace/myws'); + const interpreter = { + displayName: 'My Python', + path: '/opt/py/bin/python', + version: { raw: '3.12.1' }, + sysPrefix: '/opt/py', + }; + + const fakeDiscoveryAdapter = { kind: 'discovery' }; + const fakeExecutionAdapter = { kind: 'execution' }; + sandbox.stub(projectUtils, 'createTestAdapters').returns({ + discoveryAdapter: fakeDiscoveryAdapter, + executionAdapter: fakeExecutionAdapter, + } as any); + + // Stub useEnvExtension to return false so createDefaultProject is called + sandbox.stub(envExtApiInternal, 'useEnvExtension').returns(false); + + const interpreterService = { + getActiveInterpreter: sandbox.stub().resolves(interpreter), + } as any; + + const configSettings = { + getSettings: sandbox.stub().returns({ + testing: { unittestEnabled: false }, + }), + } as any; + + const testController = createStubTestController(); + const envVarsService = {} as any; + + const registry = new TestProjectRegistry( + testController, + configSettings, + interpreterService, + envVarsService, + ); + + const projects = await registry.discoverAndRegisterProjects(workspaceUri); + const project = projects[0]; + + assert.strictEqual(projects.length, 1); + assert.strictEqual(project.workspaceUri.toString(), workspaceUri.toString()); + assert.strictEqual(project.projectUri.toString(), workspaceUri.toString()); + assert.strictEqual(project.projectName, 'myws'); + + assert.strictEqual(project.testProvider, PYTEST_PROVIDER); + assert.strictEqual(project.discoveryAdapter, fakeDiscoveryAdapter); + assert.strictEqual(project.executionAdapter, fakeExecutionAdapter); + + assert.strictEqual(project.pythonProject.uri.toString(), workspaceUri.toString()); + assert.strictEqual(project.pythonProject.name, 'myws'); + + assert.strictEqual(project.pythonEnvironment.displayName, 'My Python'); + assert.strictEqual(project.pythonEnvironment.version, '3.12.1'); + assert.strictEqual(project.pythonEnvironment.execInfo.run.executable, '/opt/py/bin/python'); + }); + }); + + suite('discoverWorkspaceProjects (via TestProjectRegistry)', () => { + test('respects useEnvExtension() == false and falls back to single default project', async () => { + const workspaceUri: Uri = vscode.Uri.file('/workspace/a'); + + const useEnvExtensionStub = sandbox.stub(envExtApiInternal, 'useEnvExtension').returns(false); + const getEnvExtApiStub = sandbox.stub(envExtApiInternal, 'getEnvExtApi'); + + const fakeDiscoveryAdapter = { kind: 'discovery' }; + const fakeExecutionAdapter = { kind: 'execution' }; + sandbox.stub(projectUtils, 'createTestAdapters').returns({ + discoveryAdapter: fakeDiscoveryAdapter, + executionAdapter: fakeExecutionAdapter, + } as any); + + const interpreterService = { + getActiveInterpreter: sandbox.stub().resolves({ + displayName: 'Python 3.11', + path: '/usr/bin/python3', + version: { raw: '3.11.8' }, + sysPrefix: '/usr', + }), + } as any; + + const configSettings = { + getSettings: sandbox.stub().returns({ + testing: { unittestEnabled: false }, + }), + } as any; + + const testController = createStubTestController(); + const envVarsService = {} as any; + + const registry = new TestProjectRegistry( + testController, + configSettings, + interpreterService, + envVarsService, + ); + + const projects = await registry.discoverAndRegisterProjects(workspaceUri); + + assert.strictEqual(useEnvExtensionStub.called, true); + assert.strictEqual(getEnvExtApiStub.notCalled, true); + assert.strictEqual(projects.length, 1); + assert.strictEqual(projects[0].projectUri.toString(), workspaceUri.toString()); + }); + + test('filters Python projects to workspace and creates adapters for each', async () => { + const workspaceUri: Uri = vscode.Uri.file('/workspace/root'); + + const pythonProjects = [ + { name: 'p1', uri: vscode.Uri.file('/workspace/root/p1') }, + { name: 'p2', uri: vscode.Uri.file('/workspace/root/nested/p2') }, + { name: 'other', uri: vscode.Uri.file('/other/root/p3') }, + ]; + + sandbox.stub(envExtApiInternal, 'useEnvExtension').returns(true); + sandbox.stub(envExtApiInternal, 'getEnvExtApi').resolves({ + getPythonProjects: () => pythonProjects, + getEnvironment: sandbox.stub().resolves({ + name: 'env', + displayName: 'Python 3.11', + shortDisplayName: 'Python 3.11', + displayPath: '/usr/bin/python3', + version: '3.11.8', + environmentPath: vscode.Uri.file('/usr/bin/python3'), + sysPrefix: '/usr', + execInfo: { run: { executable: '/usr/bin/python3' } }, + envId: { id: 'test', managerId: 'test' }, + }), + } as any); + + const fakeDiscoveryAdapter = { kind: 'discovery' }; + const fakeExecutionAdapter = { kind: 'execution' }; + sandbox.stub(projectUtils, 'createTestAdapters').returns({ + discoveryAdapter: fakeDiscoveryAdapter, + executionAdapter: fakeExecutionAdapter, + } as any); + + const interpreterService = { + getActiveInterpreter: sandbox.stub().resolves(null), + } as any; + + const configSettings = { + getSettings: sandbox.stub().returns({ + testing: { unittestEnabled: false }, + }), + } as any; + + const testController = createStubTestController(); + const envVarsService = {} as any; + + const registry = new TestProjectRegistry( + testController, + configSettings, + interpreterService, + envVarsService, + ); + + const projects = await registry.discoverAndRegisterProjects(workspaceUri); + + // Should only create adapters for the 2 projects in the workspace (not 'other') + assert.strictEqual(projects.length, 2); + const projectUris = projects.map((p: { projectUri: { fsPath: string } }) => p.projectUri.fsPath); + const expectedInWorkspace = [ + vscode.Uri.file('/workspace/root/p1').fsPath, + vscode.Uri.file('/workspace/root/nested/p2').fsPath, + ]; + const expectedOutOfWorkspace = vscode.Uri.file('/other/root/p3').fsPath; + + expectedInWorkspace.forEach((expectedPath) => { + assert.ok(projectUris.includes(expectedPath)); + }); + assert.ok(!projectUris.includes(expectedOutOfWorkspace)); + }); + + test('falls back to default project when no projects are in the workspace', async () => { + const workspaceUri: Uri = vscode.Uri.file('/workspace/root'); + + sandbox.stub(envExtApiInternal, 'useEnvExtension').returns(true); + sandbox.stub(envExtApiInternal, 'getEnvExtApi').resolves({ + getPythonProjects: () => [{ name: 'other', uri: vscode.Uri.file('/other/root/p3') }], + } as any); + + const fakeDiscoveryAdapter = { kind: 'discovery' }; + const fakeExecutionAdapter = { kind: 'execution' }; + sandbox.stub(projectUtils, 'createTestAdapters').returns({ + discoveryAdapter: fakeDiscoveryAdapter, + executionAdapter: fakeExecutionAdapter, + } as any); + + const interpreter = { + displayName: 'Python 3.11', + path: '/usr/bin/python3', + version: { raw: '3.11.8' }, + sysPrefix: '/usr', + }; + + const interpreterService = { + getActiveInterpreter: sandbox.stub().resolves(interpreter), + } as any; + + const configSettings = { + getSettings: sandbox.stub().returns({ + testing: { unittestEnabled: false }, + }), + } as any; + + const testController = createStubTestController(); + const envVarsService = {} as any; + + const registry = new TestProjectRegistry( + testController, + configSettings, + interpreterService, + envVarsService, + ); + + const projects = await registry.discoverAndRegisterProjects(workspaceUri); + + // Should fall back to default project since no projects are in the workspace + assert.strictEqual(projects.length, 1); + assert.strictEqual(projects[0].projectUri.toString(), workspaceUri.toString()); + }); + }); +}); diff --git a/src/test/testing/testController/payloadTestCases.ts b/src/test/testing/testController/payloadTestCases.ts new file mode 100644 index 000000000000..7f2f5e23bfc3 --- /dev/null +++ b/src/test/testing/testController/payloadTestCases.ts @@ -0,0 +1,171 @@ +export interface DataWithPayloadChunks { + payloadArray: string[]; + data: string; +} + +const SINGLE_UNITTEST_SUBTEST = { + cwd: '/home/runner/work/vscode-python/vscode-python/path with spaces/src/testTestingRootWkspc/largeWorkspace', + status: 'success', + result: { + 'test_parameterized_subtest.NumbersTest.test_even (i=0)': { + test: 'test_parameterized_subtest.NumbersTest.test_even', + outcome: 'success', + message: 'None', + traceback: null, + subtest: 'test_parameterized_subtest.NumbersTest.test_even (i=0)', + }, + }, +}; + +export const SINGLE_PYTEST_PAYLOAD = { + cwd: 'path/to', + status: 'success', + result: { + 'path/to/file.py::test_funct': { + test: 'path/to/file.py::test_funct', + outcome: 'success', + message: 'None', + traceback: null, + subtest: 'path/to/file.py::test_funct', + }, + }, +}; + +const SINGLE_PYTEST_PAYLOAD_TWO = { + cwd: 'path/to/second', + status: 'success', + result: { + 'path/to/workspace/parametrize_tests.py::test_adding[3+5-8]': { + test: 'path/to/workspace/parametrize_tests.py::test_adding[3+5-8]', + outcome: 'success', + message: 'None', + traceback: null, + }, + }, +}; + +function splitIntoRandomSubstrings(payload: string): string[] { + // split payload at random + const splitPayload = []; + const n = payload.length; + let remaining = n; + while (remaining > 0) { + // Randomly split what remains of the string + const randomSize = Math.floor(Math.random() * remaining) + 1; + splitPayload.push(payload.slice(n - remaining, n - remaining + randomSize)); + + remaining -= randomSize; + } + return splitPayload; +} + +export function createPayload(uuid: string, data: unknown): string { + return `Content-Length: ${JSON.stringify(data).length} +Content-Type: application/json +Request-uuid: ${uuid} + +${JSON.stringify(data)}`; +} + +export function createPayload2(data: unknown): string { + return `Content-Length: ${JSON.stringify(data).length} +Content-Type: application/json + +${JSON.stringify(data)}`; +} + +export function PAYLOAD_SINGLE_CHUNK(uuid: string): DataWithPayloadChunks { + const payload = createPayload(uuid, SINGLE_UNITTEST_SUBTEST); + + return { + payloadArray: [payload], + data: JSON.stringify(SINGLE_UNITTEST_SUBTEST.result), + }; +} + +// more than one payload (item with header) per chunk sent +// payload has 3 SINGLE_UNITTEST_SUBTEST +export function PAYLOAD_MULTI_CHUNK(uuid: string): DataWithPayloadChunks { + let payload = ''; + let result = ''; + for (let i = 0; i < 3; i = i + 1) { + payload += createPayload(uuid, SINGLE_UNITTEST_SUBTEST); + result += JSON.stringify(SINGLE_UNITTEST_SUBTEST.result); + } + return { + payloadArray: [payload], + data: result, + }; +} + +// more than one payload, split so the first one is only 'Content-Length' to confirm headers +// with null values are ignored +export function PAYLOAD_ONLY_HEADER_MULTI_CHUNK(uuid: string): DataWithPayloadChunks { + const payloadArray: string[] = []; + const result = JSON.stringify(SINGLE_UNITTEST_SUBTEST.result); + + const val = createPayload(uuid, SINGLE_UNITTEST_SUBTEST); + const firstSpaceIndex = val.indexOf(' '); + const payload1 = val.substring(0, firstSpaceIndex); + const payload2 = val.substring(firstSpaceIndex); + payloadArray.push(payload1); + payloadArray.push(payload2); + return { + payloadArray, + data: result, + }; +} + +// single payload divided by an arbitrary character and split across payloads +export function PAYLOAD_SPLIT_ACROSS_CHUNKS_ARRAY(uuid: string): DataWithPayloadChunks { + const payload = createPayload(uuid, SINGLE_PYTEST_PAYLOAD); + const splitPayload = splitIntoRandomSubstrings(payload); + const finalResult = JSON.stringify(SINGLE_PYTEST_PAYLOAD.result); + return { + payloadArray: splitPayload, + data: finalResult, + }; +} + +// here a payload is split across the buffer chunks and there are multiple payloads in a single buffer chunk +export function PAYLOAD_SPLIT_MULTI_CHUNK_ARRAY(uuid: string): DataWithPayloadChunks { + const payload = createPayload(uuid, SINGLE_PYTEST_PAYLOAD).concat(createPayload(uuid, SINGLE_PYTEST_PAYLOAD_TWO)); + const splitPayload = splitIntoRandomSubstrings(payload); + const finalResult = JSON.stringify(SINGLE_PYTEST_PAYLOAD.result).concat( + JSON.stringify(SINGLE_PYTEST_PAYLOAD_TWO.result), + ); + + return { + payloadArray: splitPayload, + data: finalResult, + }; +} + +export function PAYLOAD_SPLIT_MULTI_CHUNK_RAN_ORDER_ARRAY(uuid: string): Array { + return [ + `Content-Length: 411 +Content-Type: application/json +Request-uuid: ${uuid} + +{"cwd": "/home/runner/work/vscode-python/vscode-python/path with spaces/src/testTestingRootWkspc/largeWorkspace", "status": "subtest-success", "result": {"test_parameterized_subtest.NumbersTest.test_even (i=0)": {"test": "test_parameterized_subtest.NumbersTest.test_even", "outcome": "subtest-success", "message": "None", "traceback": null, "subtest": "test_parameterized_subtest.NumbersTest.test_even (i=0)"}}} + +Content-Length: 411 +Content-Type: application/json +Request-uuid: 9${uuid} + +{"cwd": "/home/runner/work/vscode-`, + `python/vscode-python/path with`, + ` spaces/src" + +Content-Length: 959 +Content-Type: application/json +Request-uuid: ${uuid} + +{"cwd": "/home/runner/work/vscode-python/vscode-python/path with spaces/src/testTestingRootWkspc/largeWorkspace", "status": "subtest-failure", "result": {"test_parameterized_subtest.NumbersTest.test_even (i=1)": {"test": "test_parameterized_subtest.NumbersTest.test_even", "outcome": "subtest-failure", "message": "(, AssertionError('1 != 0'), )", "traceback": " File \"/opt/hostedtoolcache/Python/3.11.4/x64/lib/python3.11/unittest/case.py\", line 57, in testPartExecutor\n yield\n File \"/opt/hostedtoolcache/Python/3.11.4/x64/lib/python3.11/unittest/case.py\", line 538, in subTest\n yield\n File \"/home/runner/work/vscode-python/vscode-python/path with spaces/src/testTestingRootWkspc/largeWorkspace/test_parameterized_subtest.py\", line 16, in test_even\n self.assertEqual(i % 2, 0)\nAssertionError: 1 != 0\n", "subtest": "test_parameterized_subtest.NumbersTest.test_even (i=1)"}}} +Content-Length: 411 +Content-Type: application/json +Request-uuid: ${uuid} + +{"cwd": "/home/runner/work/vscode-python/vscode-python/path with spaces/src/testTestingRootWkspc/largeWorkspace", "status": "subtest-success", "result": {"test_parameterized_subtest.NumbersTest.test_even (i=2)": {"test": "test_parameterized_subtest.NumbersTest.test_even", "outcome": "subtest-success", "message": "None", "traceback": null, "subtest": "test_parameterized_subtest.NumbersTest.test_even (i=2)"}}}`, + ]; +} diff --git a/src/test/testing/testController/pytest/pytestDiscoveryAdapter.unit.test.ts b/src/test/testing/testController/pytest/pytestDiscoveryAdapter.unit.test.ts index 12c79a23c7fd..ec155ee3107d 100644 --- a/src/test/testing/testController/pytest/pytestDiscoveryAdapter.unit.test.ts +++ b/src/test/testing/testController/pytest/pytestDiscoveryAdapter.unit.test.ts @@ -2,93 +2,413 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. import * as assert from 'assert'; -import { Uri } from 'vscode'; +import { Uri, CancellationTokenSource } from 'vscode'; import * as typeMoq from 'typemoq'; -import { IConfigurationService, ITestOutputChannel } from '../../../../client/common/types'; +import * as path from 'path'; +import { Observable } from 'rxjs/Observable'; +import * as fs from 'fs'; +import * as sinon from 'sinon'; +import { IConfigurationService } from '../../../../client/common/types'; import { PytestTestDiscoveryAdapter } from '../../../../client/testing/testController/pytest/pytestDiscoveryAdapter'; -import { DataReceivedEvent, ITestServer } from '../../../../client/testing/testController/common/types'; -import { IPythonExecutionFactory, IPythonExecutionService } from '../../../../client/common/process/types'; -import { createDeferred, Deferred } from '../../../../client/common/utils/async'; +import { + IPythonExecutionFactory, + IPythonExecutionService, + // eslint-disable-next-line @typescript-eslint/no-unused-vars + SpawnOptions, + Output, +} from '../../../../client/common/process/types'; +import { EXTENSION_ROOT_DIR } from '../../../../client/constants'; +import { MockChildProcess } from '../../../mocks/mockChildProcess'; +import { Deferred, createDeferred } from '../../../../client/common/utils/async'; +import * as util from '../../../../client/testing/testController/common/utils'; +import * as extapi from '../../../../client/envExt/api.internal'; suite('pytest test discovery adapter', () => { - let testServer: typeMoq.IMock; let configService: IConfigurationService; let execFactory = typeMoq.Mock.ofType(); let adapter: PytestTestDiscoveryAdapter; let execService: typeMoq.IMock; let deferred: Deferred; - let outputChannel: typeMoq.IMock; + let expectedPath: string; + let uri: Uri; + // eslint-disable-next-line @typescript-eslint/no-unused-vars + let expectedExtraVariables: Record; + let mockProc: MockChildProcess; + let deferred2: Deferred; + let utilsStartDiscoveryNamedPipeStub: sinon.SinonStub; + let useEnvExtensionStub: sinon.SinonStub; + let cancellationTokenSource: CancellationTokenSource; setup(() => { - testServer = typeMoq.Mock.ofType(); - testServer.setup((t) => t.getPort()).returns(() => 12345); - testServer - .setup((t) => t.onDataReceived(typeMoq.It.isAny(), typeMoq.It.isAny())) - .returns(() => ({ - dispose: () => { - /* no-body */ - }, - })); + useEnvExtensionStub = sinon.stub(extapi, 'useEnvExtension'); + useEnvExtensionStub.returns(false); + + const mockExtensionRootDir = typeMoq.Mock.ofType(); + mockExtensionRootDir.setup((m) => m.toString()).returns(() => '/mocked/extension/root/dir'); + + utilsStartDiscoveryNamedPipeStub = sinon.stub(util, 'startDiscoveryNamedPipe'); + utilsStartDiscoveryNamedPipeStub.callsFake(() => Promise.resolve('discoveryResultPipe-mockName')); + + // constants + expectedPath = path.join('/', 'my', 'test', 'path'); + uri = Uri.file(expectedPath); + const relativePathToPytest = 'python_files'; + const fullPluginPath = path.join(EXTENSION_ROOT_DIR, relativePathToPytest); + expectedExtraVariables = { + PYTHONPATH: fullPluginPath, + TEST_RUN_PIPE: 'discoveryResultPipe-mockName', + }; + + // set up config service configService = ({ getSettings: () => ({ testing: { pytestArgs: ['.'] }, }), } as unknown) as IConfigurationService; - execFactory = typeMoq.Mock.ofType(); + + // set up exec service with child process + mockProc = new MockChildProcess('', ['']); execService = typeMoq.Mock.ofType(); + execService.setup((p) => ((p as unknown) as any).then).returns(() => undefined); + execService.setup((x) => x.getExecutablePath()).returns(() => Promise.resolve('/mock/path/to/python')); + + const output = new Observable>(() => { + /* no op */ + }); + deferred2 = createDeferred(); + execService + .setup((x) => x.execObservable(typeMoq.It.isAny(), typeMoq.It.isAny())) + .returns(() => { + deferred2.resolve(); + return { + proc: mockProc as any, + out: output, + dispose: () => { + /* no-body */ + }, + }; + }); + + cancellationTokenSource = new CancellationTokenSource(); + }); + teardown(() => { + sinon.restore(); + cancellationTokenSource.dispose(); + }); + test('Discovery should call exec with correct basic args', async () => { + // set up exec mock + deferred = createDeferred(); + execFactory = typeMoq.Mock.ofType(); execFactory .setup((x) => x.createActivatedEnvironment(typeMoq.It.isAny())) - .returns(() => Promise.resolve(execService.object)); + .returns(() => { + deferred.resolve(); + return Promise.resolve(execService.object); + }); + + sinon.stub(fs.promises, 'lstat').callsFake( + async () => + ({ + isFile: () => true, + isSymbolicLink: () => false, + } as fs.Stats), + ); + sinon.stub(fs.promises, 'realpath').callsFake(async (pathEntered) => pathEntered.toString()); + + adapter = new PytestTestDiscoveryAdapter(configService); + adapter.discoverTests(uri, execFactory.object); + // add in await and trigger + await deferred.promise; + await deferred2.promise; + mockProc.trigger('close'); + + // verification + execService.verify( + (x) => + x.execObservable( + typeMoq.It.isAny(), + typeMoq.It.is((options) => { + try { + assert.deepEqual(options.env, expectedExtraVariables); + assert.equal(options.cwd, expectedPath); + assert.equal(options.throwOnStdErr, true); + return true; + } catch (e) { + console.error(e); + throw e; + } + }), + ), + typeMoq.Times.once(), + ); + }); + test('Test discovery correctly pulls pytest args from config service settings', async () => { + // set up a config service with different pytest args + const expectedPathNew = path.join('other', 'path'); + const configServiceNew: IConfigurationService = ({ + getSettings: () => ({ + testing: { + pytestArgs: ['.', 'abc', 'xyz'], + cwd: expectedPathNew, + }, + }), + } as unknown) as IConfigurationService; + + sinon.stub(fs.promises, 'lstat').callsFake( + async () => + ({ + isFile: () => true, + isSymbolicLink: () => false, + } as fs.Stats), + ); + sinon.stub(fs.promises, 'realpath').callsFake(async (pathEntered) => pathEntered.toString()); + + // set up exec mock deferred = createDeferred(); - execService - .setup((x) => x.exec(typeMoq.It.isAny(), typeMoq.It.isAny())) + execFactory = typeMoq.Mock.ofType(); + execFactory + .setup((x) => x.createActivatedEnvironment(typeMoq.It.isAny())) .returns(() => { deferred.resolve(); - return Promise.resolve({ stdout: '{}' }); + return Promise.resolve(execService.object); }); - execFactory.setup((p) => ((p as unknown) as any).then).returns(() => undefined); - execService.setup((p) => ((p as unknown) as any).then).returns(() => undefined); - outputChannel = typeMoq.Mock.ofType(); + + adapter = new PytestTestDiscoveryAdapter(configServiceNew); + adapter.discoverTests(uri, execFactory.object); + // add in await and trigger + await deferred.promise; + await deferred2.promise; + mockProc.trigger('close'); + + // verification + + const expectedArgs = [ + '-m', + 'pytest', + '-p', + 'vscode_pytest', + '--collect-only', + '.', + 'abc', + 'xyz', + `--rootdir=${expectedPathNew}`, + ]; + execService.verify( + (x) => + x.execObservable( + expectedArgs, + typeMoq.It.is((options) => { + assert.deepEqual(options.env, expectedExtraVariables); + assert.equal(options.cwd, expectedPathNew); + assert.equal(options.throwOnStdErr, true); + return true; + }), + ), + typeMoq.Times.once(), + ); }); - test('onDataReceivedHandler should parse only if known UUID', async () => { - const uri = Uri.file('/my/test/path/'); - const uuid = 'uuid123'; - const data = { status: 'success' }; - testServer.setup((t) => t.createUUID(typeMoq.It.isAny())).returns(() => uuid); - const eventData: DataReceivedEvent = { - uuid, - data: JSON.stringify(data), - }; + test('Test discovery adds cwd to pytest args when path is symlink', async () => { + sinon.stub(fs.promises, 'lstat').callsFake( + async () => + ({ + isFile: () => true, + isSymbolicLink: () => true, + } as fs.Stats), + ); + sinon.stub(fs.promises, 'realpath').callsFake(async (pathEntered) => pathEntered.toString()); + + // set up a config service with different pytest args + const configServiceNew: IConfigurationService = ({ + getSettings: () => ({ + testing: { + pytestArgs: ['.', 'abc', 'xyz'], + cwd: expectedPath, + }, + }), + } as unknown) as IConfigurationService; + + // set up exec mock + deferred = createDeferred(); + execFactory = typeMoq.Mock.ofType(); + execFactory + .setup((x) => x.createActivatedEnvironment(typeMoq.It.isAny())) + .returns(() => { + deferred.resolve(); + return Promise.resolve(execService.object); + }); - adapter = new PytestTestDiscoveryAdapter(testServer.object, configService, outputChannel.object); - const promise = adapter.discoverTests(uri, execFactory.object); - // const promise = adapter.discoverTests(uri); + adapter = new PytestTestDiscoveryAdapter(configServiceNew); + adapter.discoverTests(uri, execFactory.object); + // add in await and trigger await deferred.promise; - adapter.onDataReceivedHandler(eventData); - const result = await promise; - assert.deepStrictEqual(result, data); + await deferred2.promise; + mockProc.trigger('close'); + + // verification + const expectedArgs = [ + '-m', + 'pytest', + '-p', + 'vscode_pytest', + '--collect-only', + '.', + 'abc', + 'xyz', + `--rootdir=${expectedPath}`, + ]; + execService.verify( + (x) => + x.execObservable( + expectedArgs, + typeMoq.It.is((options) => { + assert.deepEqual(options.env, expectedExtraVariables); + assert.equal(options.cwd, expectedPath); + assert.equal(options.throwOnStdErr, true); + return true; + }), + ), + typeMoq.Times.once(), + ); }); - test('onDataReceivedHandler should not parse if it is unknown UUID', async () => { - const uri = Uri.file('/my/test/path/'); - const uuid = 'uuid456'; - let data = { status: 'error' }; - testServer.setup((t) => t.createUUID(typeMoq.It.isAny())).returns(() => uuid); - const wrongUriEventData: DataReceivedEvent = { - uuid: 'incorrect-uuid456', - data: JSON.stringify(data), - }; - adapter = new PytestTestDiscoveryAdapter(testServer.object, configService, outputChannel.object); - const promise = adapter.discoverTests(uri, execFactory.object); - // const promise = adapter.discoverTests(uri); - adapter.onDataReceivedHandler(wrongUriEventData); - - data = { status: 'success' }; - const correctUriEventData: DataReceivedEvent = { - uuid, - data: JSON.stringify(data), - }; - adapter.onDataReceivedHandler(correctUriEventData); - const result = await promise; - assert.deepStrictEqual(result, data); + test('Test discovery adds cwd to pytest args when path parent is symlink', async () => { + let counter = 0; + sinon.stub(fs.promises, 'lstat').callsFake( + async () => + ({ + isFile: () => true, + isSymbolicLink: () => { + counter = counter + 1; + return counter > 2; + }, + } as fs.Stats), + ); + + sinon.stub(fs.promises, 'realpath').callsFake(async () => 'diff value'); + + // set up a config service with different pytest args + const configServiceNew: IConfigurationService = ({ + getSettings: () => ({ + testing: { + pytestArgs: ['.', 'abc', 'xyz'], + cwd: expectedPath, + }, + }), + } as unknown) as IConfigurationService; + + // set up exec mock + deferred = createDeferred(); + execFactory = typeMoq.Mock.ofType(); + execFactory + .setup((x) => x.createActivatedEnvironment(typeMoq.It.isAny())) + .returns(() => { + deferred.resolve(); + return Promise.resolve(execService.object); + }); + + adapter = new PytestTestDiscoveryAdapter(configServiceNew); + adapter.discoverTests(uri, execFactory.object); + // add in await and trigger + await deferred.promise; + await deferred2.promise; + mockProc.trigger('close'); + + // verification + const expectedArgs = [ + '-m', + 'pytest', + '-p', + 'vscode_pytest', + '--collect-only', + '.', + 'abc', + 'xyz', + `--rootdir=${expectedPath}`, + ]; + execService.verify( + (x) => + x.execObservable( + expectedArgs, + typeMoq.It.is((options) => { + assert.deepEqual(options.env, expectedExtraVariables); + assert.equal(options.cwd, expectedPath); + assert.equal(options.throwOnStdErr, true); + return true; + }), + ), + typeMoq.Times.once(), + ); + }); + test('Test discovery canceled before exec observable call finishes', async () => { + // set up exec mock + execFactory = typeMoq.Mock.ofType(); + execFactory + .setup((x) => x.createActivatedEnvironment(typeMoq.It.isAny())) + .returns(() => Promise.resolve(execService.object)); + + sinon.stub(fs.promises, 'lstat').callsFake( + async () => + ({ + isFile: () => true, + isSymbolicLink: () => false, + } as fs.Stats), + ); + sinon.stub(fs.promises, 'realpath').callsFake(async (pathEntered) => pathEntered.toString()); + + adapter = new PytestTestDiscoveryAdapter(configService); + const discoveryPromise = adapter.discoverTests(uri, execFactory.object, cancellationTokenSource.token); + + // Trigger cancellation before exec observable call finishes + cancellationTokenSource.cancel(); + + await discoveryPromise; + + assert.ok( + true, + 'Test resolves correctly when triggering a cancellation token immediately after starting discovery.', + ); + }); + + test('Test discovery cancelled while exec observable is running and proc is closed', async () => { + // + const execService2 = typeMoq.Mock.ofType(); + execService2.setup((p) => ((p as unknown) as any).then).returns(() => undefined); + execService2 + .setup((x) => x.execObservable(typeMoq.It.isAny(), typeMoq.It.isAny())) + .returns(() => { + // Trigger cancellation while exec observable is running + cancellationTokenSource.cancel(); + return { + proc: mockProc as any, + out: new Observable>(), + dispose: () => { + /* no-body */ + }, + }; + }); + // set up exec mock + deferred = createDeferred(); + execFactory = typeMoq.Mock.ofType(); + execFactory + .setup((x) => x.createActivatedEnvironment(typeMoq.It.isAny())) + .returns(() => { + deferred.resolve(); + return Promise.resolve(execService2.object); + }); + + sinon.stub(fs.promises, 'lstat').callsFake( + async () => + ({ + isFile: () => true, + isSymbolicLink: () => false, + } as fs.Stats), + ); + sinon.stub(fs.promises, 'realpath').callsFake(async (pathEntered) => pathEntered.toString()); + + adapter = new PytestTestDiscoveryAdapter(configService); + const discoveryPromise = adapter.discoverTests(uri, execFactory.object, cancellationTokenSource.token); + + // add in await and trigger + await discoveryPromise; + assert.ok(true, 'Test resolves correctly when triggering a cancellation token in exec observable.'); }); }); diff --git a/src/test/testing/testController/pytest/pytestExecutionAdapter.unit.test.ts b/src/test/testing/testController/pytest/pytestExecutionAdapter.unit.test.ts index ac6c6bd274a4..40c701b22641 100644 --- a/src/test/testing/testController/pytest/pytestExecutionAdapter.unit.test.ts +++ b/src/test/testing/testController/pytest/pytestExecutionAdapter.unit.test.ts @@ -1,90 +1,535 @@ -// /* eslint-disable @typescript-eslint/no-explicit-any */ -// // Copyright (c) Microsoft Corporation. All rights reserved. -// // Licensed under the MIT License. -// import * as assert from 'assert'; -// import { Uri } from 'vscode'; -// import * as typeMoq from 'typemoq'; -// import { IConfigurationService } from '../../../../client/common/types'; -// import { DataReceivedEvent, ITestServer } from '../../../../client/testing/testController/common/types'; -// import { IPythonExecutionFactory, IPythonExecutionService } from '../../../../client/common/process/types'; -// import { createDeferred, Deferred } from '../../../../client/common/utils/async'; -// import { PytestTestExecutionAdapter } from '../../../../client/testing/testController/pytest/pytestExecutionAdapter'; - -// suite('pytest test execution adapter', () => { -// let testServer: typeMoq.IMock; -// let configService: IConfigurationService; -// let execFactory = typeMoq.Mock.ofType(); -// let adapter: PytestTestExecutionAdapter; -// let execService: typeMoq.IMock; -// let deferred: Deferred; -// setup(() => { -// testServer = typeMoq.Mock.ofType(); -// testServer.setup((t) => t.getPort()).returns(() => 12345); -// testServer -// .setup((t) => t.onDataReceived(typeMoq.It.isAny(), typeMoq.It.isAny())) -// .returns(() => ({ -// dispose: () => { -// /* no-body */ -// }, -// })); -// configService = ({ -// getSettings: () => ({ -// testing: { pytestArgs: ['.'] }, -// }), -// isTestExecution: () => false, -// } as unknown) as IConfigurationService; -// execFactory = typeMoq.Mock.ofType(); -// execService = typeMoq.Mock.ofType(); -// execFactory -// .setup((x) => x.createActivatedEnvironment(typeMoq.It.isAny())) -// .returns(() => Promise.resolve(execService.object)); -// deferred = createDeferred(); -// execService -// .setup((x) => x.exec(typeMoq.It.isAny(), typeMoq.It.isAny())) -// .returns(() => { -// deferred.resolve(); -// return Promise.resolve({ stdout: '{}' }); -// }); -// execFactory.setup((p) => ((p as unknown) as any).then).returns(() => undefined); -// execService.setup((p) => ((p as unknown) as any).then).returns(() => undefined); -// }); -// test('onDataReceivedHandler should parse only if known UUID', async () => { -// const uri = Uri.file('/my/test/path/'); -// const uuid = 'uuid123'; -// const data = { status: 'success' }; -// testServer.setup((t) => t.createUUID(typeMoq.It.isAny())).returns(() => uuid); -// const eventData: DataReceivedEvent = { -// uuid, -// data: JSON.stringify(data), -// }; - -// adapter = new PytestTestExecutionAdapter(testServer.object, configService); -// const promise = adapter.runTests(uri, [], false); -// await deferred.promise; -// adapter.onDataReceivedHandler(eventData); -// const result = await promise; -// assert.deepStrictEqual(result, data); -// }); -// test('onDataReceivedHandler should not parse if it is unknown UUID', async () => { -// const uri = Uri.file('/my/test/path/'); -// const uuid = 'uuid456'; -// let data = { status: 'error' }; -// testServer.setup((t) => t.createUUID(typeMoq.It.isAny())).returns(() => uuid); -// const wrongUriEventData: DataReceivedEvent = { -// uuid: 'incorrect-uuid456', -// data: JSON.stringify(data), -// }; -// adapter = new PytestTestExecutionAdapter(testServer.object, configService); -// const promise = adapter.runTests(uri, [], false); -// adapter.onDataReceivedHandler(wrongUriEventData); - -// data = { status: 'success' }; -// const correctUriEventData: DataReceivedEvent = { -// uuid, -// data: JSON.stringify(data), -// }; -// adapter.onDataReceivedHandler(correctUriEventData); -// const result = await promise; -// assert.deepStrictEqual(result, data); -// }); -// }); +/* eslint-disable @typescript-eslint/no-explicit-any */ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +import * as assert from 'assert'; +import { TestRun, Uri, TestRunProfileKind, DebugSessionOptions } from 'vscode'; +import * as typeMoq from 'typemoq'; +import * as sinon from 'sinon'; +import * as path from 'path'; +import { Observable } from 'rxjs/Observable'; +import { IConfigurationService } from '../../../../client/common/types'; +import { + IPythonExecutionFactory, + IPythonExecutionService, + Output, + SpawnOptions, +} from '../../../../client/common/process/types'; +import { createDeferred, Deferred } from '../../../../client/common/utils/async'; +import { PytestTestExecutionAdapter } from '../../../../client/testing/testController/pytest/pytestExecutionAdapter'; +import { ITestDebugLauncher, LaunchOptions } from '../../../../client/testing/common/types'; +import * as util from '../../../../client/testing/testController/common/utils'; +import { EXTENSION_ROOT_DIR } from '../../../../client/constants'; +import { MockChildProcess } from '../../../mocks/mockChildProcess'; +import { traceInfo } from '../../../../client/logging'; +import * as extapi from '../../../../client/envExt/api.internal'; +import { createMockProjectAdapter } from '../testMocks'; + +suite('pytest test execution adapter', () => { + let useEnvExtensionStub: sinon.SinonStub; + let configService: IConfigurationService; + let execFactory = typeMoq.Mock.ofType(); + let adapter: PytestTestExecutionAdapter; + let execService: typeMoq.IMock; + let deferred: Deferred; + let deferred4: Deferred; + let debugLauncher: typeMoq.IMock; + (global as any).EXTENSION_ROOT_DIR = EXTENSION_ROOT_DIR; + let myTestPath: string; + let mockProc: MockChildProcess; + let utilsWriteTestIdsFileStub: sinon.SinonStub; + let utilsStartRunResultNamedPipeStub: sinon.SinonStub; + + setup(() => { + useEnvExtensionStub = sinon.stub(extapi, 'useEnvExtension'); + useEnvExtensionStub.returns(false); + configService = ({ + getSettings: () => ({ + testing: { pytestArgs: ['.'] }, + }), + isTestExecution: () => false, + } as unknown) as IConfigurationService; + + // set up exec service with child process + mockProc = new MockChildProcess('', ['']); + const output = new Observable>(() => { + /* no op */ + }); + deferred4 = createDeferred(); + execService = typeMoq.Mock.ofType(); + execService + .setup((x) => x.execObservable(typeMoq.It.isAny(), typeMoq.It.isAny())) + .returns(() => { + deferred4.resolve(); + return { + proc: mockProc as any, + out: output, + dispose: () => { + /* no-body */ + }, + }; + }); + execFactory = typeMoq.Mock.ofType(); + + // added + utilsWriteTestIdsFileStub = sinon.stub(util, 'writeTestIdsFile'); + debugLauncher = typeMoq.Mock.ofType(); + execFactory + .setup((x) => x.createActivatedEnvironment(typeMoq.It.isAny())) + .returns(() => Promise.resolve(execService.object)); + deferred = createDeferred(); + execService + .setup((x) => x.exec(typeMoq.It.isAny(), typeMoq.It.isAny())) + .returns(() => { + deferred.resolve(); + return Promise.resolve({ stdout: '{}' }); + }); + execFactory.setup((p) => ((p as unknown) as any).then).returns(() => undefined); + execService.setup((p) => ((p as unknown) as any).then).returns(() => undefined); + debugLauncher.setup((p) => ((p as unknown) as any).then).returns(() => undefined); + myTestPath = path.join('/', 'my', 'test', 'path', '/'); + + utilsStartRunResultNamedPipeStub = sinon.stub(util, 'startRunResultNamedPipe'); + utilsStartRunResultNamedPipeStub.callsFake(() => Promise.resolve('runResultPipe-mockName')); + + execService.setup((x) => x.getExecutablePath()).returns(() => Promise.resolve('/mock/path/to/python')); + }); + teardown(() => { + sinon.restore(); + }); + test('WriteTestIdsFile called with correct testIds', async () => { + const deferred2 = createDeferred(); + const deferred3 = createDeferred(); + execFactory = typeMoq.Mock.ofType(); + execFactory + .setup((x) => x.createActivatedEnvironment(typeMoq.It.isAny())) + .returns(() => { + deferred2.resolve(); + return Promise.resolve(execService.object); + }); + utilsWriteTestIdsFileStub.callsFake(() => { + deferred3.resolve(); + return Promise.resolve({ + name: 'mockName', + dispose: () => { + /* no-op */ + }, + }); + }); + const testRun = typeMoq.Mock.ofType(); + testRun.setup((t) => t.token).returns(() => ({ onCancellationRequested: () => undefined } as any)); + const uri = Uri.file(myTestPath); + adapter = new PytestTestExecutionAdapter(configService); + const testIds = ['test1id', 'test2id']; + + adapter.runTests(uri, testIds, TestRunProfileKind.Run, testRun.object, execFactory.object); + + // add in await and trigger + await deferred2.promise; + await deferred3.promise; + mockProc.trigger('close'); + + // assert + sinon.assert.calledWithExactly(utilsWriteTestIdsFileStub, testIds); + }); + test('pytest execution called with correct args', async () => { + const deferred2 = createDeferred(); + const deferred3 = createDeferred(); + execFactory = typeMoq.Mock.ofType(); + execFactory + .setup((x) => x.createActivatedEnvironment(typeMoq.It.isAny())) + .returns(() => { + deferred2.resolve(); + return Promise.resolve(execService.object); + }); + utilsWriteTestIdsFileStub.callsFake(() => { + deferred3.resolve(); + return Promise.resolve('testIdPipe-mockName'); + }); + const testRun = typeMoq.Mock.ofType(); + testRun.setup((t) => t.token).returns(() => ({ onCancellationRequested: () => undefined } as any)); + const uri = Uri.file(myTestPath); + adapter = new PytestTestExecutionAdapter(configService); + adapter.runTests(uri, [], TestRunProfileKind.Run, testRun.object, execFactory.object); + + await deferred2.promise; + await deferred3.promise; + await deferred4.promise; + mockProc.trigger('close'); + + const pathToPythonFiles = path.join(EXTENSION_ROOT_DIR, 'python_files'); + const pathToPythonScript = path.join(pathToPythonFiles, 'vscode_pytest', 'run_pytest_script.py'); + const rootDirArg = `--rootdir=${myTestPath}`; + const expectedArgs = [pathToPythonScript, rootDirArg]; + const expectedExtraVariables = { + PYTHONPATH: pathToPythonFiles, + TEST_RUN_PIPE: 'runResultPipe-mockName', + RUN_TEST_IDS_PIPE: 'testIdPipe-mockName', + }; + execService.verify( + (x) => + x.execObservable( + expectedArgs, + typeMoq.It.is((options) => { + assert.equal(options.env?.PYTHONPATH, expectedExtraVariables.PYTHONPATH); + assert.equal(options.env?.TEST_RUN_PIPE, expectedExtraVariables.TEST_RUN_PIPE); + assert.equal(options.env?.RUN_TEST_IDS_PIPE, expectedExtraVariables.RUN_TEST_IDS_PIPE); + assert.equal(options.env?.COVERAGE_ENABLED, undefined); // coverage not enabled + assert.equal(options.cwd, uri.fsPath); + assert.equal(options.throwOnStdErr, true); + return true; + }), + ), + typeMoq.Times.once(), + ); + }); + test('pytest execution respects settings.testing.cwd when present', async () => { + const deferred2 = createDeferred(); + const deferred3 = createDeferred(); + execFactory = typeMoq.Mock.ofType(); + execFactory + .setup((x) => x.createActivatedEnvironment(typeMoq.It.isAny())) + .returns(() => { + deferred2.resolve(); + return Promise.resolve(execService.object); + }); + utilsWriteTestIdsFileStub.callsFake(() => { + deferred3.resolve(); + return Promise.resolve('testIdPipe-mockName'); + }); + const testRun = typeMoq.Mock.ofType(); + testRun.setup((t) => t.token).returns(() => ({ onCancellationRequested: () => undefined } as any)); + const newCwd = path.join('new', 'path'); + configService = ({ + getSettings: () => ({ + testing: { pytestArgs: ['.'], cwd: newCwd }, + }), + isTestExecution: () => false, + } as unknown) as IConfigurationService; + const uri = Uri.file(myTestPath); + adapter = new PytestTestExecutionAdapter(configService); + adapter.runTests(uri, [], TestRunProfileKind.Run, testRun.object, execFactory.object); + + await deferred2.promise; + await deferred3.promise; + await deferred4.promise; + mockProc.trigger('close'); + + const pathToPythonFiles = path.join(EXTENSION_ROOT_DIR, 'python_files'); + const pathToPythonScript = path.join(pathToPythonFiles, 'vscode_pytest', 'run_pytest_script.py'); + const expectedArgs = [pathToPythonScript, `--rootdir=${newCwd}`]; + const expectedExtraVariables = { + PYTHONPATH: pathToPythonFiles, + TEST_RUN_PIPE: 'runResultPipe-mockName', + RUN_TEST_IDS_PIPE: 'testIdPipe-mockName', + }; + + execService.verify( + (x) => + x.execObservable( + expectedArgs, + typeMoq.It.is((options) => { + assert.equal(options.env?.PYTHONPATH, expectedExtraVariables.PYTHONPATH); + assert.equal(options.env?.TEST_RUN_PIPE, expectedExtraVariables.TEST_RUN_PIPE); + assert.equal(options.env?.RUN_TEST_IDS_PIPE, expectedExtraVariables.RUN_TEST_IDS_PIPE); + assert.equal(options.cwd, newCwd); + assert.equal(options.throwOnStdErr, true); + return true; + }), + ), + typeMoq.Times.once(), + ); + }); + test('Debug launched correctly for pytest', async () => { + const deferred3 = createDeferred(); + utilsWriteTestIdsFileStub.callsFake(() => Promise.resolve('testIdPipe-mockName')); + debugLauncher + .setup((dl) => dl.launchDebugger(typeMoq.It.isAny(), typeMoq.It.isAny(), typeMoq.It.isAny())) + .returns(async (_opts, callback) => { + traceInfo('stubs launch debugger'); + if (typeof callback === 'function') { + deferred3.resolve(); + callback(); + } + }); + const testRun = typeMoq.Mock.ofType(); + testRun + .setup((t) => t.token) + .returns( + () => + ({ + onCancellationRequested: () => undefined, + } as any), + ); + const uri = Uri.file(myTestPath); + adapter = new PytestTestExecutionAdapter(configService); + adapter.runTests(uri, [], TestRunProfileKind.Debug, testRun.object, execFactory.object, debugLauncher.object); + await deferred3.promise; + debugLauncher.verify( + (x) => + x.launchDebugger( + typeMoq.It.is((launchOptions) => { + assert.equal(launchOptions.cwd, uri.fsPath); + assert.deepEqual(launchOptions.args, [`--rootdir=${myTestPath}`, '--capture=no']); + assert.equal(launchOptions.testProvider, 'pytest'); + assert.equal(launchOptions.pytestPort, 'runResultPipe-mockName'); + assert.strictEqual(launchOptions.runTestIdsPort, 'testIdPipe-mockName'); + assert.notEqual(launchOptions.token, undefined); + return true; + }), + typeMoq.It.isAny(), + typeMoq.It.is((sessionOptions) => { + assert.equal(sessionOptions.testRun, testRun.object); + return true; + }), + ), + typeMoq.Times.once(), + ); + }); + test('pytest execution with coverage turns on correctly', async () => { + const deferred2 = createDeferred(); + const deferred3 = createDeferred(); + execFactory = typeMoq.Mock.ofType(); + execFactory + .setup((x) => x.createActivatedEnvironment(typeMoq.It.isAny())) + .returns(() => { + deferred2.resolve(); + return Promise.resolve(execService.object); + }); + utilsWriteTestIdsFileStub.callsFake(() => { + deferred3.resolve(); + return Promise.resolve('testIdPipe-mockName'); + }); + const testRun = typeMoq.Mock.ofType(); + testRun.setup((t) => t.token).returns(() => ({ onCancellationRequested: () => undefined } as any)); + const uri = Uri.file(myTestPath); + adapter = new PytestTestExecutionAdapter(configService); + adapter.runTests(uri, [], TestRunProfileKind.Coverage, testRun.object, execFactory.object); + + await deferred2.promise; + await deferred3.promise; + await deferred4.promise; + mockProc.trigger('close'); + + const pathToPythonFiles = path.join(EXTENSION_ROOT_DIR, 'python_files'); + const pathToPythonScript = path.join(pathToPythonFiles, 'vscode_pytest', 'run_pytest_script.py'); + const rootDirArg = `--rootdir=${myTestPath}`; + const expectedArgs = [pathToPythonScript, rootDirArg]; + execService.verify( + (x) => + x.execObservable( + expectedArgs, + typeMoq.It.is((options) => { + assert.equal(options.env?.COVERAGE_ENABLED, 'True'); + return true; + }), + ), + typeMoq.Times.once(), + ); + }); + + // ===== PROJECT-BASED EXECUTION TESTS ===== + + suite('project-based execution', () => { + test('should set PROJECT_ROOT_PATH env var when project provided', async () => { + const deferred2 = createDeferred(); + const deferred3 = createDeferred(); + execFactory = typeMoq.Mock.ofType(); + execFactory + .setup((x) => x.createActivatedEnvironment(typeMoq.It.isAny())) + .returns(() => { + deferred2.resolve(); + return Promise.resolve(execService.object); + }); + utilsWriteTestIdsFileStub.callsFake(() => { + deferred3.resolve(); + return Promise.resolve('testIdPipe-mockName'); + }); + const testRun = typeMoq.Mock.ofType(); + testRun.setup((t) => t.token).returns(() => ({ onCancellationRequested: () => undefined } as any)); + + const projectPath = path.join('/', 'workspace', 'myproject'); + const mockProject = createMockProjectAdapter({ + projectPath, + projectName: 'myproject', + pythonPath: '/custom/python/path', + }); + + const uri = Uri.file(myTestPath); + adapter = new PytestTestExecutionAdapter(configService); + adapter.runTests( + uri, + [], + TestRunProfileKind.Run, + testRun.object, + execFactory.object, + undefined, + undefined, + mockProject, + ); + + await deferred2.promise; + await deferred3.promise; + await deferred4.promise; + mockProc.trigger('close'); + + execService.verify( + (x) => + x.execObservable( + typeMoq.It.isAny(), + typeMoq.It.is((options) => { + assert.equal(options.env?.PROJECT_ROOT_PATH, projectPath); + return true; + }), + ), + typeMoq.Times.once(), + ); + }); + + test('should pass debugSessionName in LaunchOptions for debug mode with project', async () => { + const deferred3 = createDeferred(); + utilsWriteTestIdsFileStub.callsFake(() => Promise.resolve('testIdPipe-mockName')); + + debugLauncher + .setup((dl) => dl.launchDebugger(typeMoq.It.isAny(), typeMoq.It.isAny(), typeMoq.It.isAny())) + .returns(async (_opts, callback) => { + traceInfo('stubs launch debugger'); + if (typeof callback === 'function') { + deferred3.resolve(); + callback(); + } + }); + + const testRun = typeMoq.Mock.ofType(); + testRun + .setup((t) => t.token) + .returns( + () => + ({ + onCancellationRequested: () => undefined, + } as any), + ); + + const projectPath = path.join('/', 'workspace', 'myproject'); + const mockProject = createMockProjectAdapter({ + projectPath, + projectName: 'myproject (Python 3.11)', + pythonPath: '/custom/python/path', + }); + + const uri = Uri.file(myTestPath); + adapter = new PytestTestExecutionAdapter(configService); + adapter.runTests( + uri, + [], + TestRunProfileKind.Debug, + testRun.object, + execFactory.object, + debugLauncher.object, + undefined, + mockProject, + ); + + await deferred3.promise; + + debugLauncher.verify( + (x) => + x.launchDebugger( + typeMoq.It.is((launchOptions) => { + // Project should be passed for project-based debugging + assert.ok(launchOptions.project, 'project should be defined'); + assert.equal(launchOptions.project?.name, 'myproject (Python 3.11)'); + assert.equal(launchOptions.project?.uri.fsPath, projectPath); + return true; + }), + typeMoq.It.isAny(), + typeMoq.It.isAny(), + ), + typeMoq.Times.once(), + ); + }); + + test('should not set PROJECT_ROOT_PATH when no project provided', async () => { + const deferred2 = createDeferred(); + const deferred3 = createDeferred(); + execFactory = typeMoq.Mock.ofType(); + execFactory + .setup((x) => x.createActivatedEnvironment(typeMoq.It.isAny())) + .returns(() => { + deferred2.resolve(); + return Promise.resolve(execService.object); + }); + utilsWriteTestIdsFileStub.callsFake(() => { + deferred3.resolve(); + return Promise.resolve('testIdPipe-mockName'); + }); + const testRun = typeMoq.Mock.ofType(); + testRun.setup((t) => t.token).returns(() => ({ onCancellationRequested: () => undefined } as any)); + + const uri = Uri.file(myTestPath); + adapter = new PytestTestExecutionAdapter(configService); + // Call without project parameter + adapter.runTests(uri, [], TestRunProfileKind.Run, testRun.object, execFactory.object); + + await deferred2.promise; + await deferred3.promise; + await deferred4.promise; + mockProc.trigger('close'); + + execService.verify( + (x) => + x.execObservable( + typeMoq.It.isAny(), + typeMoq.It.is((options) => { + assert.equal(options.env?.PROJECT_ROOT_PATH, undefined); + return true; + }), + ), + typeMoq.Times.once(), + ); + }); + + test('should not set project in LaunchOptions when no project provided', async () => { + const deferred3 = createDeferred(); + utilsWriteTestIdsFileStub.callsFake(() => Promise.resolve('testIdPipe-mockName')); + + debugLauncher + .setup((dl) => dl.launchDebugger(typeMoq.It.isAny(), typeMoq.It.isAny(), typeMoq.It.isAny())) + .returns(async (_opts, callback) => { + if (typeof callback === 'function') { + deferred3.resolve(); + callback(); + } + }); + + const testRun = typeMoq.Mock.ofType(); + testRun.setup((t) => t.token).returns(() => ({ onCancellationRequested: () => undefined } as any)); + + const uri = Uri.file(myTestPath); + adapter = new PytestTestExecutionAdapter(configService); + // Call without project parameter + adapter.runTests( + uri, + [], + TestRunProfileKind.Debug, + testRun.object, + execFactory.object, + debugLauncher.object, + ); + + await deferred3.promise; + + debugLauncher.verify( + (x) => + x.launchDebugger( + typeMoq.It.is((launchOptions) => { + assert.equal(launchOptions.project, undefined); + return true; + }), + typeMoq.It.isAny(), + typeMoq.It.isAny(), + ), + typeMoq.Times.once(), + ); + }); + }); +}); diff --git a/src/test/testing/testController/resultResolver.unit.test.ts b/src/test/testing/testController/resultResolver.unit.test.ts new file mode 100644 index 000000000000..e4b350a20750 --- /dev/null +++ b/src/test/testing/testController/resultResolver.unit.test.ts @@ -0,0 +1,613 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { TestController, Uri, TestItem, CancellationToken, TestRun, TestItemCollection, Range } from 'vscode'; +import * as typemoq from 'typemoq'; +import * as sinon from 'sinon'; +import * as assert from 'assert'; +import { TestProvider } from '../../../client/testing/types'; +import { + DiscoveredTestNode, + DiscoveredTestPayload, + ExecutionTestPayload, +} from '../../../client/testing/testController/common/types'; +import * as testItemUtilities from '../../../client/testing/testController/common/testItemUtilities'; +import * as ResultResolver from '../../../client/testing/testController/common/resultResolver'; +import * as util from '../../../client/testing/testController/common/utils'; +import { traceLog } from '../../../client/logging'; + +suite('Result Resolver tests', () => { + suite('Test discovery', () => { + let resultResolver: ResultResolver.PythonResultResolver; + let testController: TestController; + const log: string[] = []; + let workspaceUri: Uri; + let testProvider: TestProvider; + let defaultErrorMessage: string; + let blankTestItem: TestItem; + let cancelationToken: CancellationToken; + + setup(() => { + testController = ({ + items: { + get: () => { + log.push('get'); + }, + add: () => { + log.push('add'); + }, + replace: () => { + log.push('replace'); + }, + delete: () => { + log.push('delete'); + }, + }, + + dispose: () => { + // empty + }, + } as unknown) as TestController; + defaultErrorMessage = 'pytest test discovery error (see Output > Python)'; + blankTestItem = ({ + canResolveChildren: false, + tags: [], + children: { + add: () => { + // empty + }, + }, + } as unknown) as TestItem; + cancelationToken = ({ + isCancellationRequested: false, + } as unknown) as CancellationToken; + }); + teardown(() => { + sinon.restore(); + }); + + test('resolveDiscovery calls populate test tree correctly', async () => { + // test specific constants used expected values + testProvider = 'pytest'; + workspaceUri = Uri.file('/foo/bar'); + resultResolver = new ResultResolver.PythonResultResolver(testController, testProvider, workspaceUri); + const tests: DiscoveredTestNode = { + path: 'path', + name: 'name', + type_: 'folder', + id_: 'id', + children: [], + }; + const payload: DiscoveredTestPayload = { + cwd: workspaceUri.fsPath, + status: 'success', + tests, + }; + + // stub out functionality of populateTestTreeStub which is called in resolveDiscovery + const populateTestTreeStub = sinon.stub(util, 'populateTestTree').returns(); + + // call resolve discovery + resultResolver.resolveDiscovery(payload, cancelationToken); + + // assert the stub functions were called with the correct parameters + + // header of populateTestTree is (testController: TestController, testTreeData: DiscoveredTestNode, testRoot: TestItem | undefined, resultResolver: ITestResultResolver, token?: CancellationToken) + // After refactor, an inline object with testItemIndex maps is passed instead of resultResolver + sinon.assert.calledWithMatch( + populateTestTreeStub, + testController, // testController + tests, // testTreeData + undefined, // testRoot + sinon.match.has('runIdToTestItem'), // inline object with maps + cancelationToken, // token + ); + }); + test('resolveDiscovery should create error node on error with correct params and no root node with tests in payload', async () => { + // test specific constants used expected values + testProvider = 'pytest'; + workspaceUri = Uri.file('/foo/bar'); + resultResolver = new ResultResolver.PythonResultResolver(testController, testProvider, workspaceUri); + const errorMessage = 'error msg A'; + const expectedErrorMessage = `${defaultErrorMessage}\r\n ${errorMessage}`; + + // stub out return values of functions called in resolveDiscovery + const payload: DiscoveredTestPayload = { + cwd: workspaceUri.fsPath, + status: 'error', + error: [errorMessage], + }; + const errorTestItemOptions: testItemUtilities.ErrorTestItemOptions = { + id: 'id', + label: 'label', + error: 'error', + }; + + // stub out functionality of buildErrorNodeOptions and createErrorTestItem which are called in resolveDiscovery + const buildErrorNodeOptionsStub = sinon.stub(util, 'buildErrorNodeOptions').returns(errorTestItemOptions); + const createErrorTestItemStub = sinon.stub(testItemUtilities, 'createErrorTestItem').returns(blankTestItem); + + // call resolve discovery + resultResolver.resolveDiscovery(payload, cancelationToken); + + // assert the stub functions were called with the correct parameters + + // header of buildErrorNodeOptions is (uri: Uri, message: string, testType: string) + sinon.assert.calledWithMatch(buildErrorNodeOptionsStub, workspaceUri, expectedErrorMessage, testProvider); + // header of createErrorTestItem is (options: ErrorTestItemOptions, testController: TestController, uri: Uri) + sinon.assert.calledWithMatch(createErrorTestItemStub, sinon.match.any, sinon.match.any); + }); + test('resolveDiscovery should create error and root node when error and tests exist on payload', async () => { + // test specific constants used expected values + testProvider = 'pytest'; + workspaceUri = Uri.file('/foo/bar'); + resultResolver = new ResultResolver.PythonResultResolver(testController, testProvider, workspaceUri); + const errorMessage = 'error msg A'; + const expectedErrorMessage = `${defaultErrorMessage}\r\n ${errorMessage}`; + + // create test result node + const tests: DiscoveredTestNode = { + path: 'path', + name: 'name', + type_: 'folder', + id_: 'id', + children: [], + }; + // stub out return values of functions called in resolveDiscovery + const payload: DiscoveredTestPayload = { + cwd: workspaceUri.fsPath, + status: 'error', + error: [errorMessage], + tests, + }; + const errorTestItemOptions: testItemUtilities.ErrorTestItemOptions = { + id: 'id', + label: 'label', + error: 'error', + }; + + // stub out functionality of buildErrorNodeOptions and createErrorTestItem which are called in resolveDiscovery + const buildErrorNodeOptionsStub = sinon.stub(util, 'buildErrorNodeOptions').returns(errorTestItemOptions); + const createErrorTestItemStub = sinon.stub(testItemUtilities, 'createErrorTestItem').returns(blankTestItem); + + // stub out functionality of populateTestTreeStub which is called in resolveDiscovery + const populateTestTreeStub = sinon.stub(util, 'populateTestTree').returns(); + // call resolve discovery + resultResolver.resolveDiscovery(payload, cancelationToken); + + // assert the stub functions were called with the correct parameters + + // builds an error node root + sinon.assert.calledWithMatch(buildErrorNodeOptionsStub, workspaceUri, expectedErrorMessage, testProvider); + // builds an error item + sinon.assert.calledWithMatch(createErrorTestItemStub, sinon.match.any, sinon.match.any); + + // also calls populateTestTree with the discovery test results + // After refactor, an inline object with testItemIndex maps is passed instead of resultResolver + sinon.assert.calledWithMatch( + populateTestTreeStub, + testController, // testController + tests, // testTreeData + undefined, // testRoot + sinon.match.has('runIdToTestItem'), // inline object with maps + cancelationToken, // token + ); + }); + test('resolveDiscovery should create error and not clear test items to allow for error tolerant discovery', async () => { + // test specific constants used expected values + testProvider = 'pytest'; + workspaceUri = Uri.file('/foo/bar'); + resultResolver = new ResultResolver.PythonResultResolver(testController, testProvider, workspaceUri); + const errorMessage = 'error msg A'; + const expectedErrorMessage = `${defaultErrorMessage}\r\n ${errorMessage}`; + + // create test result node + const tests: DiscoveredTestNode = { + path: 'path', + name: 'name', + type_: 'folder', + id_: 'id', + children: [], + }; + // stub out return values of functions called in resolveDiscovery + const errorPayload: DiscoveredTestPayload = { + cwd: workspaceUri.fsPath, + status: 'error', + error: [errorMessage], + }; + const regPayload: DiscoveredTestPayload = { + cwd: workspaceUri.fsPath, + status: 'success', + error: [errorMessage], + tests, + }; + const errorTestItemOptions: testItemUtilities.ErrorTestItemOptions = { + id: 'id', + label: 'label', + error: 'error', + }; + + // stub out functionality of buildErrorNodeOptions and createErrorTestItem which are called in resolveDiscovery + const buildErrorNodeOptionsStub = sinon.stub(util, 'buildErrorNodeOptions').returns(errorTestItemOptions); + const createErrorTestItemStub = sinon.stub(testItemUtilities, 'createErrorTestItem').returns(blankTestItem); + + // stub out functionality of populateTestTreeStub which is called in resolveDiscovery + sinon.stub(util, 'populateTestTree').returns(); + // add spies to insure these aren't called + const deleteSpy = sinon.spy(testController.items, 'delete'); + const replaceSpy = sinon.spy(testController.items, 'replace'); + // call resolve discovery + resultResolver.resolveDiscovery(regPayload, cancelationToken); + resultResolver.resolveDiscovery(errorPayload, cancelationToken); + + // assert the stub functions were called with the correct parameters + + // builds an error node root + sinon.assert.calledWithMatch(buildErrorNodeOptionsStub, workspaceUri, expectedErrorMessage, testProvider); + // builds an error item + sinon.assert.calledWithMatch(createErrorTestItemStub, sinon.match.any, sinon.match.any); + + if (!deleteSpy.calledOnce) { + throw new Error("The delete method was called, but it shouldn't have been."); + } + if (replaceSpy.called) { + throw new Error("The replace method was called, but it shouldn't have been."); + } + }); + }); + suite('Test execution result resolver', () => { + let resultResolver: ResultResolver.PythonResultResolver; + const log: string[] = []; + let workspaceUri: Uri; + let testProvider: TestProvider; + let cancelationToken: CancellationToken; + let runInstance: typemoq.IMock; + let testControllerMock: typemoq.IMock; + let mockTestItem1: TestItem; + let mockTestItem2: TestItem; + + setup(() => { + // create mock test items + mockTestItem1 = createMockTestItem('mockTestItem1'); + mockTestItem2 = createMockTestItem('mockTestItem2'); + + // create mock testItems to pass into a iterable + const mockTestItems: [string, TestItem][] = [ + ['1', mockTestItem1], + ['2', mockTestItem2], + ]; + const iterableMock = mockTestItems[Symbol.iterator](); + + // create mock testItemCollection + const testItemCollectionMock = typemoq.Mock.ofType(); + testItemCollectionMock + .setup((x) => x.forEach(typemoq.It.isAny())) + .callback((callback) => { + let result = iterableMock.next(); + while (!result.done) { + callback(result.value[1]); + result = iterableMock.next(); + } + }) + .returns(() => mockTestItem1); + + // create mock testController + testControllerMock = typemoq.Mock.ofType(); + testControllerMock.setup((t) => t.items).returns(() => testItemCollectionMock.object); + + cancelationToken = ({ + isCancellationRequested: false, + } as unknown) as CancellationToken; + + // define functions within runInstance + runInstance = typemoq.Mock.ofType(); + runInstance.setup((r) => r.name).returns(() => 'name'); + runInstance.setup((r) => r.token).returns(() => cancelationToken); + runInstance.setup((r) => r.isPersisted).returns(() => true); + runInstance + .setup((r) => r.enqueued(typemoq.It.isAny())) + .returns(() => { + // empty + log.push('enqueue'); + return undefined; + }); + runInstance + .setup((r) => r.started(typemoq.It.isAny())) + .returns(() => { + // empty + log.push('start'); + }); + + // mock getTestCaseNodes to just return the given testNode added + sinon.stub(testItemUtilities, 'getTestCaseNodes').callsFake((testNode: TestItem) => [testNode]); + }); + teardown(() => { + sinon.restore(); + }); + test('resolveExecution create correct subtest item for unittest', async () => { + // test specific constants used expected values + sinon.stub(testItemUtilities, 'clearAllChildren').callsFake(() => undefined); + testProvider = 'unittest'; + workspaceUri = Uri.file('/foo/bar'); + + // Create parent test item with correct ID + const mockParentItem = createMockTestItem('parentTest'); + + // Update testControllerMock to include parent item in its collection + const mockTestItems: [string, TestItem][] = [ + ['1', mockTestItem1], + ['2', mockTestItem2], + ['parentTest', mockParentItem], + ]; + const iterableMock = mockTestItems[Symbol.iterator](); + + const testItemCollectionMock = typemoq.Mock.ofType(); + testItemCollectionMock + .setup((x) => x.forEach(typemoq.It.isAny())) + .callback((callback) => { + let result = iterableMock.next(); + while (!result.done) { + callback(result.value[1]); + result = iterableMock.next(); + } + }) + .returns(() => mockTestItem1); + testItemCollectionMock.setup((x) => x.get('parentTest')).returns(() => mockParentItem); + + testControllerMock.reset(); + testControllerMock.setup((t) => t.items).returns(() => testItemCollectionMock.object); + + resultResolver = new ResultResolver.PythonResultResolver( + testControllerMock.object, + testProvider, + workspaceUri, + ); + const subtestName = 'parentTest [subTest with spaces and [brackets]]'; + const mockSubtestItem = createMockTestItem(subtestName); + + // add a mock test item to the map of known VSCode ids to run ids + resultResolver.runIdToVSid.set('mockTestItem2', 'mockTestItem2'); + // creates a mock test item with a space which will be used to split the runId + resultResolver.runIdToVSid.set(subtestName, subtestName); + // Register parent test in testItemIndex so it can be found by getTestItem + resultResolver.runIdToVSid.set('parentTest', 'parentTest'); + + // add this mock test to the map of known test items + resultResolver.runIdToTestItem.set('parentTest', mockParentItem); + resultResolver.runIdToTestItem.set(subtestName, mockSubtestItem); + + let generatedId: string | undefined; + let generatedUri: Uri | undefined; + testControllerMock + .setup((t) => t.createTestItem(typemoq.It.isAny(), typemoq.It.isAny(), typemoq.It.isAny())) + .callback((id: string) => { + generatedId = id; + generatedUri = workspaceUri; + traceLog('createTestItem function called with id:', id); + }) + .returns(() => ({ id: 'id_this', label: 'label_this', uri: workspaceUri } as TestItem)); + + // create a successful payload with a single test called mockTestItem1 + const successPayload: ExecutionTestPayload = { + cwd: workspaceUri.fsPath, + status: 'success', + result: { + 'parentTest [subTest with spaces and [brackets]]': { + test: 'parentTest', + outcome: 'subtest-success', // failure, passed-unexpected, skipped, success, expected-failure, subtest-failure, subtest-succcess + message: 'message', + traceback: 'traceback', + subtest: subtestName, + }, + }, + error: '', + }; + + // call resolveExecution + resultResolver.resolveExecution(successPayload, runInstance.object); + + // verify that the passed function was called for the single test item + assert.ok(generatedId); + assert.strictEqual(generatedUri, workspaceUri); + assert.strictEqual(generatedId, '[subTest with spaces and [brackets]]'); + }); + test('resolveExecution handles failed tests correctly', async () => { + // test specific constants used expected values + testProvider = 'pytest'; + workspaceUri = Uri.file('/foo/bar'); + resultResolver = new ResultResolver.PythonResultResolver( + testControllerMock.object, + testProvider, + workspaceUri, + ); + // add a mock test item to the map of known VSCode ids to run ids + resultResolver.runIdToVSid.set('mockTestItem1', 'mockTestItem1'); + resultResolver.runIdToVSid.set('mockTestItem2', 'mockTestItem2'); + + // add this mock test to the map of known test items + resultResolver.runIdToTestItem.set('mockTestItem1', mockTestItem1); + resultResolver.runIdToTestItem.set('mockTestItem2', mockTestItem2); + + // create a successful payload with a single test called mockTestItem1 + const successPayload: ExecutionTestPayload = { + cwd: workspaceUri.fsPath, + status: 'success', + result: { + mockTestItem1: { + test: 'test', + outcome: 'failure', // failure, passed-unexpected, skipped, success, expected-failure, subtest-failure, subtest-succcess + message: 'message', + traceback: 'traceback', + subtest: 'subtest', + }, + }, + error: '', + }; + + // call resolveExecution + resultResolver.resolveExecution(successPayload, runInstance.object); + + // verify that the passed function was called for the single test item + runInstance.verify((r) => r.failed(typemoq.It.isAny(), typemoq.It.isAny()), typemoq.Times.once()); + }); + test('resolveExecution handles skipped correctly', async () => { + // test specific constants used expected values + testProvider = 'pytest'; + workspaceUri = Uri.file('/foo/bar'); + resultResolver = new ResultResolver.PythonResultResolver( + testControllerMock.object, + testProvider, + workspaceUri, + ); + // add a mock test item to the map of known VSCode ids to run ids + resultResolver.runIdToVSid.set('mockTestItem1', 'mockTestItem1'); + resultResolver.runIdToVSid.set('mockTestItem2', 'mockTestItem2'); + + // add this mock test to the map of known test items + resultResolver.runIdToTestItem.set('mockTestItem1', mockTestItem1); + resultResolver.runIdToTestItem.set('mockTestItem2', mockTestItem2); + + // create a successful payload with a single test called mockTestItem1 + const successPayload: ExecutionTestPayload = { + cwd: workspaceUri.fsPath, + status: 'success', + result: { + mockTestItem1: { + test: 'test', + outcome: 'skipped', // failure, passed-unexpected, skipped, success, expected-failure, subtest-failure, subtest-succcess + message: 'message', + traceback: 'traceback', + subtest: 'subtest', + }, + }, + error: '', + }; + + // call resolveExecution + resultResolver.resolveExecution(successPayload, runInstance.object); + + // verify that the passed function was called for the single test item + runInstance.verify((r) => r.skipped(typemoq.It.isAny()), typemoq.Times.once()); + }); + test('resolveExecution handles error correctly as test outcome', async () => { + // test specific constants used expected values + testProvider = 'pytest'; + workspaceUri = Uri.file('/foo/bar'); + resultResolver = new ResultResolver.PythonResultResolver( + testControllerMock.object, + testProvider, + workspaceUri, + ); + // add a mock test item to the map of known VSCode ids to run ids + resultResolver.runIdToVSid.set('mockTestItem1', 'mockTestItem1'); + resultResolver.runIdToVSid.set('mockTestItem2', 'mockTestItem2'); + + // add this mock test to the map of known test items + resultResolver.runIdToTestItem.set('mockTestItem1', mockTestItem1); + resultResolver.runIdToTestItem.set('mockTestItem2', mockTestItem2); + + // create a successful payload with a single test called mockTestItem1 + const successPayload: ExecutionTestPayload = { + cwd: workspaceUri.fsPath, + status: 'success', + result: { + mockTestItem1: { + test: 'test', + outcome: 'error', // failure, passed-unexpected, skipped, success, expected-failure, subtest-failure, subtest-succcess + message: 'message', + traceback: 'traceback', + subtest: 'subtest', + }, + }, + error: '', + }; + + // call resolveExecution + resultResolver.resolveExecution(successPayload, runInstance.object); + + // verify that the passed function was called for the single test item + runInstance.verify((r) => r.errored(typemoq.It.isAny(), typemoq.It.isAny()), typemoq.Times.once()); + }); + test('resolveExecution handles success correctly', async () => { + // test specific constants used expected values + testProvider = 'pytest'; + workspaceUri = Uri.file('/foo/bar'); + resultResolver = new ResultResolver.PythonResultResolver( + testControllerMock.object, + testProvider, + workspaceUri, + ); + // add a mock test item to the map of known VSCode ids to run ids + resultResolver.runIdToVSid.set('mockTestItem1', 'mockTestItem1'); + resultResolver.runIdToVSid.set('mockTestItem2', 'mockTestItem2'); + + // add this mock test to the map of known test items + resultResolver.runIdToTestItem.set('mockTestItem1', mockTestItem1); + resultResolver.runIdToTestItem.set('mockTestItem2', mockTestItem2); + + // create a successful payload with a single test called mockTestItem1 + const successPayload: ExecutionTestPayload = { + cwd: workspaceUri.fsPath, + status: 'success', + result: { + mockTestItem1: { + test: 'test', + outcome: 'success', // failure, passed-unexpected, skipped, success, expected-failure, subtest-failure, subtest-succcess + message: 'message', + traceback: 'traceback', + subtest: 'subtest', + }, + }, + error: '', + }; + + // call resolveExecution + resultResolver.resolveExecution(successPayload, runInstance.object); + + // verify that the passed function was called for the single test item + runInstance.verify((r) => r.passed(typemoq.It.isAny()), typemoq.Times.once()); + }); + test('resolveExecution handles error correctly', async () => { + // test specific constants used expected values + testProvider = 'pytest'; + workspaceUri = Uri.file('/foo/bar'); + resultResolver = new ResultResolver.PythonResultResolver( + testControllerMock.object, + testProvider, + workspaceUri, + ); + + const errorPayload: ExecutionTestPayload = { + cwd: workspaceUri.fsPath, + status: 'error', + error: 'error', + }; + + resultResolver.resolveExecution(errorPayload, runInstance.object); + + // verify that none of these functions are called + + runInstance.verify((r) => r.passed(typemoq.It.isAny()), typemoq.Times.never()); + runInstance.verify((r) => r.failed(typemoq.It.isAny(), typemoq.It.isAny()), typemoq.Times.never()); + runInstance.verify((r) => r.skipped(typemoq.It.isAny()), typemoq.Times.never()); + }); + }); +}); + +function createMockTestItem(id: string): TestItem { + const range = new Range(0, 0, 0, 0); + const mockChildren = typemoq.Mock.ofType(); + mockChildren.setup((x) => x.add(typemoq.It.isAny())).returns(() => undefined); + mockChildren.setup((x) => x.forEach(typemoq.It.isAny())).returns(() => undefined); + + const mockTestItem = ({ + id, + canResolveChildren: false, + tags: [], + children: mockChildren.object, + range, + uri: Uri.file('/foo/bar'), + } as unknown) as TestItem; + + return mockTestItem; +} diff --git a/src/test/testing/testController/server.unit.test.ts b/src/test/testing/testController/server.unit.test.ts deleted file mode 100644 index d7b3a242ee9a..000000000000 --- a/src/test/testing/testController/server.unit.test.ts +++ /dev/null @@ -1,158 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -import * as assert from 'assert'; -import * as net from 'net'; -import * as sinon from 'sinon'; -import * as crypto from 'crypto'; -import { OutputChannel, Uri } from 'vscode'; -import { IPythonExecutionFactory, IPythonExecutionService } from '../../../client/common/process/types'; -import { PythonTestServer } from '../../../client/testing/testController/common/server'; -import { ITestDebugLauncher } from '../../../client/testing/common/types'; -import { createDeferred } from '../../../client/common/utils/async'; - -suite('Python Test Server', () => { - const fakeUuid = 'fake-uuid'; - - let stubExecutionFactory: IPythonExecutionFactory; - let stubExecutionService: IPythonExecutionService; - let server: PythonTestServer; - let sandbox: sinon.SinonSandbox; - let execArgs: string[]; - let v4Stub: sinon.SinonStub; - let debugLauncher: ITestDebugLauncher; - - setup(() => { - sandbox = sinon.createSandbox(); - v4Stub = sandbox.stub(crypto, 'randomUUID'); - - v4Stub.returns(fakeUuid); - stubExecutionService = ({ - exec: (args: string[]) => { - execArgs = args; - return Promise.resolve({ stdout: '', stderr: '' }); - }, - } as unknown) as IPythonExecutionService; - - stubExecutionFactory = ({ - createActivatedEnvironment: () => Promise.resolve(stubExecutionService), - } as unknown) as IPythonExecutionFactory; - }); - - teardown(() => { - sandbox.restore(); - execArgs = []; - server.dispose(); - }); - - test('sendCommand should add the port to the command being sent', async () => { - const options = { - command: { script: 'myscript', args: ['-foo', 'foo'] }, - workspaceFolder: Uri.file('/foo/bar'), - cwd: '/foo/bar', - uuid: fakeUuid, - }; - - server = new PythonTestServer(stubExecutionFactory, debugLauncher); - await server.serverReady(); - - await server.sendCommand(options); - const port = server.getPort(); - - assert.deepStrictEqual(execArgs, ['myscript', '--port', `${port}`, '--uuid', fakeUuid, '-foo', 'foo']); - }); - - test('sendCommand should write to an output channel if it is provided as an option', async () => { - const output: string[] = []; - const outChannel = { - appendLine: (str: string) => { - output.push(str); - }, - } as OutputChannel; - const options = { - command: { script: 'myscript', args: ['-foo', 'foo'] }, - workspaceFolder: Uri.file('/foo/bar'), - cwd: '/foo/bar', - uuid: fakeUuid, - outChannel, - }; - - server = new PythonTestServer(stubExecutionFactory, debugLauncher); - await server.serverReady(); - - await server.sendCommand(options); - - const port = server.getPort(); - const expected = ['python', 'myscript', '--port', `${port}`, '--uuid', fakeUuid, '-foo', 'foo'].join(' '); - - assert.deepStrictEqual(output, [expected]); - }); - - test('If script execution fails during sendCommand, an onDataReceived event should be fired with the "error" status', async () => { - let eventData: { status: string; errors: string[] }; - stubExecutionService = ({ - exec: () => { - throw new Error('Failed to execute'); - }, - } as unknown) as IPythonExecutionService; - - const options = { - command: { script: 'myscript', args: ['-foo', 'foo'] }, - workspaceFolder: Uri.file('/foo/bar'), - cwd: '/foo/bar', - uuid: fakeUuid, - }; - - server = new PythonTestServer(stubExecutionFactory, debugLauncher); - await server.serverReady(); - - server.onDataReceived(({ data }) => { - eventData = JSON.parse(data); - }); - - await server.sendCommand(options); - - assert.deepStrictEqual(eventData!.status, 'error'); - assert.deepStrictEqual(eventData!.errors, ['Failed to execute']); - }); - - test('If the server receives malformed data, it should display a log message, and not fire an event', async () => { - let eventData: string | undefined; - const client = new net.Socket(); - const deferred = createDeferred(); - - const options = { - command: { script: 'myscript', args: ['-foo', 'foo'] }, - workspaceFolder: Uri.file('/foo/bar'), - cwd: '/foo/bar', - uuid: fakeUuid, - }; - - stubExecutionService = ({ - exec: async () => { - client.connect(server.getPort()); - return Promise.resolve({ stdout: '', stderr: '' }); - }, - } as unknown) as IPythonExecutionService; - - server = new PythonTestServer(stubExecutionFactory, debugLauncher); - await server.serverReady(); - server.onDataReceived(({ data }) => { - eventData = data; - deferred.resolve(); - }); - - client.on('connect', () => { - console.log('Socket connected, local port:', client.localPort); - client.write('malformed data'); - client.end(); - }); - client.on('error', (error) => { - console.log('Socket connection error:', error); - }); - - await server.sendCommand(options); - await deferred.promise; - assert.deepStrictEqual(eventData, ''); - }); -}); diff --git a/src/test/testing/testController/testCancellationRunAdapters.unit.test.ts b/src/test/testing/testController/testCancellationRunAdapters.unit.test.ts new file mode 100644 index 000000000000..cdf0d00c5dc4 --- /dev/null +++ b/src/test/testing/testController/testCancellationRunAdapters.unit.test.ts @@ -0,0 +1,234 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +import { CancellationTokenSource, TestRun, TestRunProfileKind, Uri } from 'vscode'; +import * as typeMoq from 'typemoq'; +import * as sinon from 'sinon'; +import * as path from 'path'; +import { Observable } from 'rxjs'; +import { IPythonExecutionFactory, IPythonExecutionService, Output } from '../../../client/common/process/types'; +import { IConfigurationService } from '../../../client/common/types'; +import { Deferred, createDeferred } from '../../../client/common/utils/async'; +import { EXTENSION_ROOT_DIR } from '../../../client/constants'; +import { ITestDebugLauncher } from '../../../client/testing/common/types'; +import { PytestTestExecutionAdapter } from '../../../client/testing/testController/pytest/pytestExecutionAdapter'; +import { UnittestTestExecutionAdapter } from '../../../client/testing/testController/unittest/testExecutionAdapter'; +import { MockChildProcess } from '../../mocks/mockChildProcess'; +import * as util from '../../../client/testing/testController/common/utils'; +import * as extapi from '../../../client/envExt/api.internal'; +import { noop } from '../../core'; + +const adapters: Array = ['pytest', 'unittest']; + +suite('Execution Flow Run Adapters', () => { + // define suit level variables + let configService: IConfigurationService; + let execFactoryStub = typeMoq.Mock.ofType(); + let execServiceStub: typeMoq.IMock; + // let deferred: Deferred; + let debugLauncher: typeMoq.IMock; + (global as any).EXTENSION_ROOT_DIR = EXTENSION_ROOT_DIR; + let myTestPath: string; + let mockProc: MockChildProcess; + let utilsWriteTestIdsFileStub: sinon.SinonStub; + let utilsStartRunResultNamedPipe: sinon.SinonStub; + let serverDisposeStub: sinon.SinonStub; + + let useEnvExtensionStub: sinon.SinonStub; + + setup(() => { + const proc = typeMoq.Mock.ofType(); + proc.setup((p) => p.on).returns(() => noop as any); + proc.setup((p) => p.stdout).returns(() => null); + proc.setup((p) => p.stderr).returns(() => null); + mockProc = proc.object; + useEnvExtensionStub = sinon.stub(extapi, 'useEnvExtension'); + useEnvExtensionStub.returns(false); + // general vars + myTestPath = path.join('/', 'my', 'test', 'path', '/'); + configService = ({ + getSettings: () => ({ + testing: { pytestArgs: ['.'], unittestArgs: ['-v', '-s', '.', '-p', 'test*'] }, + }), + isTestExecution: () => false, + } as unknown) as IConfigurationService; + + // set up execService and execFactory, all mocked + execServiceStub = typeMoq.Mock.ofType(); + execFactoryStub = typeMoq.Mock.ofType(); + + // mocked utility functions that handle pipe related functions + utilsWriteTestIdsFileStub = sinon.stub(util, 'writeTestIdsFile'); + utilsStartRunResultNamedPipe = sinon.stub(util, 'startRunResultNamedPipe'); + serverDisposeStub = sinon.stub(); + + // debug specific mocks + debugLauncher = typeMoq.Mock.ofType(); + debugLauncher.setup((p) => ((p as unknown) as any).then).returns(() => undefined); + }); + teardown(() => { + sinon.restore(); + }); + adapters.forEach((adapter) => { + test(`Adapter ${adapter}: cancelation token called mid-run resolves correctly`, async () => { + // mock test run and cancelation token + const testRunMock = typeMoq.Mock.ofType(); + const cancellationToken = new CancellationTokenSource(); + const { token } = cancellationToken; + testRunMock.setup((t) => t.token).returns(() => token); + + // run result pipe mocking and the related server close dispose + let deferredTillServerCloseTester: Deferred | undefined; + + // // mock exec service and exec factory + execServiceStub + .setup((x) => x.execObservable(typeMoq.It.isAny(), typeMoq.It.isAny())) + .returns(() => { + cancellationToken.cancel(); + return { + proc: mockProc as any, + out: typeMoq.Mock.ofType>>().object, + dispose: () => { + /* no-body */ + }, + }; + }); + execFactoryStub + .setup((x) => x.createActivatedEnvironment(typeMoq.It.isAny())) + .returns(() => Promise.resolve(execServiceStub.object)); + execFactoryStub.setup((p) => ((p as unknown) as any).then).returns(() => undefined); + execServiceStub.setup((p) => ((p as unknown) as any).then).returns(() => undefined); + + // test ids named pipe mocking + const deferredStartTestIdsNamedPipe = createDeferred(); + utilsWriteTestIdsFileStub.callsFake(() => { + deferredStartTestIdsNamedPipe.resolve(); + return Promise.resolve('named-pipe'); + }); + + utilsStartRunResultNamedPipe.callsFake((_callback, deferredTillServerClose, token) => { + deferredTillServerCloseTester = deferredTillServerClose; + token?.onCancellationRequested(() => { + deferredTillServerCloseTester?.resolve(); + }); + + return Promise.resolve('named-pipes-socket-name'); + }); + serverDisposeStub.callsFake(() => { + console.log('server disposed'); + if (deferredTillServerCloseTester) { + deferredTillServerCloseTester.resolve(); + } else { + console.log('deferredTillServerCloseTester is undefined'); + throw new Error( + 'deferredTillServerCloseTester is undefined, should be defined from startRunResultNamedPipe', + ); + } + }); + + // define adapter and run tests + const testAdapter = createAdapter(adapter, configService); + await testAdapter.runTests( + Uri.file(myTestPath), + [], + TestRunProfileKind.Run, + testRunMock.object, + execFactoryStub.object, + debugLauncher.object, + ); + // wait for server to start to keep test from failing + await deferredStartTestIdsNamedPipe.promise; + }); + test(`Adapter ${adapter}: token called mid-debug resolves correctly`, async () => { + // mock test run and cancelation token + const testRunMock = typeMoq.Mock.ofType(); + const cancellationToken = new CancellationTokenSource(); + const { token } = cancellationToken; + testRunMock.setup((t) => t.token).returns(() => token); + + // run result pipe mocking and the related server close dispose + let deferredTillServerCloseTester: Deferred | undefined; + + // // mock exec service and exec factory + execServiceStub + .setup((x) => x.execObservable(typeMoq.It.isAny(), typeMoq.It.isAny())) + .returns(() => { + cancellationToken.cancel(); + return { + proc: mockProc as any, + out: typeMoq.Mock.ofType>>().object, + dispose: () => { + /* no-body */ + }, + }; + }); + execFactoryStub + .setup((x) => x.createActivatedEnvironment(typeMoq.It.isAny())) + .returns(() => Promise.resolve(execServiceStub.object)); + execFactoryStub.setup((p) => ((p as unknown) as any).then).returns(() => undefined); + execServiceStub.setup((p) => ((p as unknown) as any).then).returns(() => undefined); + + // test ids named pipe mocking + const deferredStartTestIdsNamedPipe = createDeferred(); + utilsWriteTestIdsFileStub.callsFake(() => { + deferredStartTestIdsNamedPipe.resolve(); + return Promise.resolve('named-pipe'); + }); + + utilsStartRunResultNamedPipe.callsFake((_callback, deferredTillServerClose, _token) => { + deferredTillServerCloseTester = deferredTillServerClose; + token?.onCancellationRequested(() => { + deferredTillServerCloseTester?.resolve(); + }); + return Promise.resolve('named-pipes-socket-name'); + }); + serverDisposeStub.callsFake(() => { + console.log('server disposed'); + if (deferredTillServerCloseTester) { + deferredTillServerCloseTester.resolve(); + } else { + console.log('deferredTillServerCloseTester is undefined'); + throw new Error( + 'deferredTillServerCloseTester is undefined, should be defined from startRunResultNamedPipe', + ); + } + }); + + // debugLauncher mocked + debugLauncher + .setup((dl) => dl.launchDebugger(typeMoq.It.isAny(), typeMoq.It.isAny(), typeMoq.It.isAny())) + .callback((_options, callback) => { + if (callback) { + callback(); + } + }) + .returns(async () => { + cancellationToken.cancel(); + return Promise.resolve(); + }); + + // define adapter and run tests + const testAdapter = createAdapter(adapter, configService); + await testAdapter.runTests( + Uri.file(myTestPath), + [], + TestRunProfileKind.Debug, + testRunMock.object, + execFactoryStub.object, + debugLauncher.object, + ); + // wait for server to start to keep test from failing + await deferredStartTestIdsNamedPipe.promise; + }); + }); +}); + +// Helper function to create an adapter based on the specified type +function createAdapter( + adapterType: string, + configService: IConfigurationService, +): PytestTestExecutionAdapter | UnittestTestExecutionAdapter { + if (adapterType === 'pytest') return new PytestTestExecutionAdapter(configService); + if (adapterType === 'unittest') return new UnittestTestExecutionAdapter(configService); + throw Error('un-compatible adapter type'); +} diff --git a/src/test/testing/testController/testMocks.ts b/src/test/testing/testController/testMocks.ts new file mode 100644 index 000000000000..eb37d492f1d9 --- /dev/null +++ b/src/test/testing/testController/testMocks.ts @@ -0,0 +1,152 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +/** + * Centralized mock utilities for testing testController components. + * Re-use these helpers across multiple test files for consistency. + */ + +import * as sinon from 'sinon'; +import * as typemoq from 'typemoq'; +import { TestItem, TestItemCollection, TestRun, Uri } from 'vscode'; +import { IPythonExecutionFactory } from '../../../client/common/process/types'; +import { ITestDebugLauncher } from '../../../client/testing/common/types'; +import { ProjectAdapter } from '../../../client/testing/testController/common/projectAdapter'; +import { ProjectExecutionDependencies } from '../../../client/testing/testController/common/projectTestExecution'; +import { TestProjectRegistry } from '../../../client/testing/testController/common/testProjectRegistry'; +import { ITestExecutionAdapter, ITestResultResolver } from '../../../client/testing/testController/common/types'; + +/** + * Creates a mock TestItem with configurable properties. + * @param id - The unique ID of the test item + * @param uriPath - The file path for the test item's URI + * @param children - Optional array of child test items + */ +export function createMockTestItem(id: string, uriPath: string, children?: TestItem[]): TestItem { + const childMap = new Map(); + children?.forEach((c) => childMap.set(c.id, c)); + + const mockChildren: TestItemCollection = { + size: childMap.size, + forEach: (callback: (item: TestItem, collection: TestItemCollection) => void) => { + childMap.forEach((item) => callback(item, mockChildren)); + }, + get: (itemId: string) => childMap.get(itemId), + add: () => {}, + delete: () => {}, + replace: () => {}, + [Symbol.iterator]: function* () { + for (const [key, value] of childMap) { + yield [key, value] as [string, TestItem]; + } + }, + } as TestItemCollection; + + return ({ + id, + uri: Uri.file(uriPath), + children: mockChildren, + label: id, + canResolveChildren: false, + busy: false, + tags: [], + range: undefined, + error: undefined, + parent: undefined, + } as unknown) as TestItem; +} + +/** + * Creates a mock TestItem without a URI. + * Useful for testing edge cases where test items have no associated file. + * @param id - The unique ID of the test item + */ +export function createMockTestItemWithoutUri(id: string): TestItem { + return ({ + id, + uri: undefined, + children: ({ size: 0, forEach: () => {} } as unknown) as TestItemCollection, + label: id, + } as unknown) as TestItem; +} + +export interface MockProjectAdapterConfig { + projectPath: string; + projectName: string; + pythonPath?: string; + testProvider?: 'pytest' | 'unittest'; +} + +export type MockProjectAdapter = ProjectAdapter & { executionAdapterStub: sinon.SinonStub }; + +/** + * Creates a mock ProjectAdapter for testing project-based test execution. + * @param config - Configuration object with project details + * @returns A mock ProjectAdapter with an exposed executionAdapterStub for verification + */ +export function createMockProjectAdapter(config: MockProjectAdapterConfig): MockProjectAdapter { + const runTestsStub = sinon.stub().resolves(); + const executionAdapter: ITestExecutionAdapter = ({ + runTests: runTestsStub, + } as unknown) as ITestExecutionAdapter; + + const resultResolverMock: ITestResultResolver = ({ + vsIdToRunId: new Map(), + runIdToVSid: new Map(), + runIdToTestItem: new Map(), + detailedCoverageMap: new Map(), + resolveDiscovery: () => Promise.resolve(), + resolveExecution: () => {}, + } as unknown) as ITestResultResolver; + + const adapter = ({ + projectUri: Uri.file(config.projectPath), + projectName: config.projectName, + workspaceUri: Uri.file(config.projectPath), + testProvider: config.testProvider ?? 'pytest', + pythonEnvironment: config.pythonPath + ? { + execInfo: { run: { executable: config.pythonPath } }, + } + : undefined, + pythonProject: { + name: config.projectName, + uri: Uri.file(config.projectPath), + }, + executionAdapter, + discoveryAdapter: {} as any, + resultResolver: resultResolverMock, + isDiscovering: false, + isExecuting: false, + // Expose the stub for testing + executionAdapterStub: runTestsStub, + } as unknown) as MockProjectAdapter; + + return adapter; +} + +/** + * Creates mock dependencies for project test execution. + * @returns An object containing mocked ProjectExecutionDependencies + */ +export function createMockDependencies(): ProjectExecutionDependencies { + return { + projectRegistry: typemoq.Mock.ofType().object, + pythonExecFactory: typemoq.Mock.ofType().object, + debugLauncher: typemoq.Mock.ofType().object, + }; +} + +/** + * Creates a mock TestRun with common setup methods. + * @returns A TypeMoq mock of TestRun + */ +export function createMockTestRun(): typemoq.IMock { + const runMock = typemoq.Mock.ofType(); + runMock.setup((r) => r.started(typemoq.It.isAny())); + runMock.setup((r) => r.passed(typemoq.It.isAny(), typemoq.It.isAny())); + runMock.setup((r) => r.failed(typemoq.It.isAny(), typemoq.It.isAny(), typemoq.It.isAny())); + runMock.setup((r) => r.skipped(typemoq.It.isAny())); + runMock.setup((r) => r.end()); + return runMock; +} diff --git a/src/test/testing/testController/unittest/testDiscoveryAdapter.unit.test.ts b/src/test/testing/testController/unittest/testDiscoveryAdapter.unit.test.ts index 3d3521291f74..031f30afba8a 100644 --- a/src/test/testing/testController/unittest/testDiscoveryAdapter.unit.test.ts +++ b/src/test/testing/testController/unittest/testDiscoveryAdapter.unit.test.ts @@ -1,107 +1,342 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. +/* eslint-disable @typescript-eslint/no-explicit-any */ import * as assert from 'assert'; import * as path from 'path'; -import * as typemoq from 'typemoq'; -import { Uri } from 'vscode'; -import { IConfigurationService, ITestOutputChannel } from '../../../../client/common/types'; +import * as typeMoq from 'typemoq'; +import * as fs from 'fs'; +import { CancellationTokenSource, Uri } from 'vscode'; +import { Observable } from 'rxjs'; +import * as sinon from 'sinon'; +import { IConfigurationService } from '../../../../client/common/types'; import { EXTENSION_ROOT_DIR } from '../../../../client/constants'; -import { ITestServer, TestCommandOptions } from '../../../../client/testing/testController/common/types'; import { UnittestTestDiscoveryAdapter } from '../../../../client/testing/testController/unittest/testDiscoveryAdapter'; +import { Deferred, createDeferred } from '../../../../client/common/utils/async'; +import { MockChildProcess } from '../../../mocks/mockChildProcess'; +import * as util from '../../../../client/testing/testController/common/utils'; +import { + IPythonExecutionFactory, + IPythonExecutionService, + Output, + SpawnOptions, +} from '../../../../client/common/process/types'; +import * as extapi from '../../../../client/envExt/api.internal'; +import { ProjectAdapter } from '../../../../client/testing/testController/common/projectAdapter'; suite('Unittest test discovery adapter', () => { - let stubConfigSettings: IConfigurationService; - let outputChannel: typemoq.IMock; + let configService: IConfigurationService; + let mockProc: MockChildProcess; + let execService: typeMoq.IMock; + let execFactory = typeMoq.Mock.ofType(); + let deferred: Deferred; + let expectedExtraVariables: Record; + let expectedPath: string; + let uri: Uri; + let utilsStartDiscoveryNamedPipeStub: sinon.SinonStub; + let useEnvExtensionStub: sinon.SinonStub; + let cancellationTokenSource: CancellationTokenSource; setup(() => { - stubConfigSettings = ({ + useEnvExtensionStub = sinon.stub(extapi, 'useEnvExtension'); + useEnvExtensionStub.returns(false); + + expectedPath = path.join('/', 'new', 'cwd'); + configService = ({ getSettings: () => ({ testing: { unittestArgs: ['-v', '-s', '.', '-p', 'test*'] }, }), } as unknown) as IConfigurationService; - outputChannel = typemoq.Mock.ofType(); - }); - test('discoverTests should send the discovery command to the test server', async () => { - let options: TestCommandOptions | undefined; - - const stubTestServer = ({ - sendCommand(opt: TestCommandOptions): Promise { - delete opt.outChannel; - options = opt; - return Promise.resolve(); - }, - onDataReceived: () => { - // no body - }, - createUUID: () => '123456789', - } as unknown) as ITestServer; - - const uri = Uri.file('/foo/bar'); - const script = path.join(EXTENSION_ROOT_DIR, 'pythonFiles', 'unittestadapter', 'discovery.py'); - - const adapter = new UnittestTestDiscoveryAdapter(stubTestServer, stubConfigSettings, outputChannel.object); - adapter.discoverTests(uri); - - assert.deepStrictEqual(options, { - workspaceFolder: uri, - cwd: uri.fsPath, - command: { script, args: ['--udiscovery', '-v', '-s', '.', '-p', 'test*'] }, - uuid: '123456789', + // set up exec service with child process + mockProc = new MockChildProcess('', ['']); + const output = new Observable>(() => { + /* no op */ }); + execService = typeMoq.Mock.ofType(); + execService + .setup((x) => x.execObservable(typeMoq.It.isAny(), typeMoq.It.isAny())) + .returns(() => { + deferred.resolve(); + console.log('execObservable is returning'); + return { + proc: mockProc as any, + out: output, + dispose: () => { + /* no-body */ + }, + }; + }); + execService.setup((x) => x.getExecutablePath()).returns(() => Promise.resolve('/mock/path/to/python')); + execFactory = typeMoq.Mock.ofType(); + deferred = createDeferred(); + execFactory + .setup((x) => x.createActivatedEnvironment(typeMoq.It.isAny())) + .returns(() => Promise.resolve(execService.object)); + execFactory.setup((p) => ((p as unknown) as any).then).returns(() => undefined); + execService.setup((p) => ((p as unknown) as any).then).returns(() => undefined); + + // constants + expectedPath = path.join('/', 'my', 'test', 'path'); + uri = Uri.file(expectedPath); + expectedExtraVariables = { + TEST_RUN_PIPE: 'discoveryResultPipe-mockName', + }; + + utilsStartDiscoveryNamedPipeStub = sinon.stub(util, 'startDiscoveryNamedPipe'); + utilsStartDiscoveryNamedPipeStub.callsFake(() => Promise.resolve('discoveryResultPipe-mockName')); + cancellationTokenSource = new CancellationTokenSource(); + }); + teardown(() => { + sinon.restore(); + cancellationTokenSource.dispose(); }); - test("onDataReceivedHandler should parse the data if the cwd from the payload matches the test adapter's cwd", async () => { - const stubTestServer = ({ - sendCommand(): Promise { - return Promise.resolve(); - }, - onDataReceived: () => { - // no body - }, - createUUID: () => '123456789', - } as unknown) as ITestServer; + test('DiscoverTests should send the discovery command to the test server with the correct args', async () => { + const adapter = new UnittestTestDiscoveryAdapter(configService); + adapter.discoverTests(uri, execFactory.object); + const script = path.join(EXTENSION_ROOT_DIR, 'python_files', 'unittestadapter', 'discovery.py'); + const argsExpected = [script, '--udiscovery', '-v', '-s', '.', '-p', 'test*']; - const uri = Uri.file('/foo/bar'); - const data = { status: 'success' }; - const uuid = '123456789'; - const adapter = new UnittestTestDiscoveryAdapter(stubTestServer, stubConfigSettings, outputChannel.object); - const promise = adapter.discoverTests(uri); + // must await until the execObservable is called in order to verify it + await deferred.promise; - adapter.onDataReceivedHandler({ uuid, data: JSON.stringify(data) }); + execService.verify( + (x) => + x.execObservable( + typeMoq.It.is>((argsActual) => { + try { + assert.equal(argsActual.length, argsExpected.length); + assert.deepEqual(argsActual, argsExpected); + return true; + } catch (e) { + console.error(e); + throw e; + } + }), + typeMoq.It.is((options) => { + try { + assert.deepEqual(options.env, expectedExtraVariables); + assert.equal(options.cwd, expectedPath); + assert.equal(options.throwOnStdErr, true); + return true; + } catch (e) { + console.error(e); + throw e; + } + }), + ), + typeMoq.Times.once(), + ); + }); + test('DiscoverTests should respect settings.testings.cwd when present', async () => { + const expectedNewPath = path.join('/', 'new', 'cwd'); + configService = ({ + getSettings: () => ({ + testing: { unittestArgs: ['-v', '-s', '.', '-p', 'test*'], cwd: expectedNewPath.toString() }, + }), + } as unknown) as IConfigurationService; + const adapter = new UnittestTestDiscoveryAdapter(configService); + adapter.discoverTests(uri, execFactory.object); + const script = path.join(EXTENSION_ROOT_DIR, 'python_files', 'unittestadapter', 'discovery.py'); + const argsExpected = [script, '--udiscovery', '-v', '-s', '.', '-p', 'test*']; - const result = await promise; + // must await until the execObservable is called in order to verify it + await deferred.promise; - assert.deepStrictEqual(result, data); + execService.verify( + (x) => + x.execObservable( + typeMoq.It.is>((argsActual) => { + try { + assert.equal(argsActual.length, argsExpected.length); + assert.deepEqual(argsActual, argsExpected); + return true; + } catch (e) { + console.error(e); + throw e; + } + }), + typeMoq.It.is((options) => { + try { + assert.deepEqual(options.env, expectedExtraVariables); + assert.equal(options.cwd, expectedNewPath); + assert.equal(options.throwOnStdErr, true); + return true; + } catch (e) { + console.error(e); + throw e; + } + }), + ), + typeMoq.Times.once(), + ); }); + test('Test discovery canceled before exec observable call finishes', async () => { + // set up exec mock + execFactory = typeMoq.Mock.ofType(); + execFactory + .setup((x) => x.createActivatedEnvironment(typeMoq.It.isAny())) + .returns(() => Promise.resolve(execService.object)); - test("onDataReceivedHandler should ignore the data if the cwd from the payload does not match the test adapter's cwd", async () => { - const correctUuid = '123456789'; - const incorrectUuid = '987654321'; - const stubTestServer = ({ - sendCommand(): Promise { - return Promise.resolve(); - }, - onDataReceived: () => { - // no body - }, - createUUID: () => correctUuid, - } as unknown) as ITestServer; + sinon.stub(fs.promises, 'lstat').callsFake( + async () => + ({ + isFile: () => true, + isSymbolicLink: () => false, + } as fs.Stats), + ); + sinon.stub(fs.promises, 'realpath').callsFake(async (pathEntered) => pathEntered.toString()); - const uri = Uri.file('/foo/bar'); + const adapter = new UnittestTestDiscoveryAdapter(configService); + const discoveryPromise = adapter.discoverTests(uri, execFactory.object, cancellationTokenSource.token); - const adapter = new UnittestTestDiscoveryAdapter(stubTestServer, stubConfigSettings, outputChannel.object); - const promise = adapter.discoverTests(uri); + // Trigger cancellation before exec observable call finishes + cancellationTokenSource.cancel(); - const data = { status: 'success' }; - adapter.onDataReceivedHandler({ uuid: incorrectUuid, data: JSON.stringify(data) }); + await discoveryPromise; + + assert.ok( + true, + 'Test resolves correctly when triggering a cancellation token immediately after starting discovery.', + ); + }); + + test('Test discovery cancelled while exec observable is running and proc is closed', async () => { + // + const execService2 = typeMoq.Mock.ofType(); + execService2.setup((p) => ((p as unknown) as any).then).returns(() => undefined); + execService2 + .setup((x) => x.execObservable(typeMoq.It.isAny(), typeMoq.It.isAny())) + .returns(() => { + // Trigger cancellation while exec observable is running + cancellationTokenSource.cancel(); + return { + proc: mockProc as any, + out: new Observable>(), + dispose: () => { + /* no-body */ + }, + }; + }); + // set up exec mock + deferred = createDeferred(); + execFactory = typeMoq.Mock.ofType(); + execFactory + .setup((x) => x.createActivatedEnvironment(typeMoq.It.isAny())) + .returns(() => { + deferred.resolve(); + return Promise.resolve(execService2.object); + }); + + sinon.stub(fs.promises, 'lstat').callsFake( + async () => + ({ + isFile: () => true, + isSymbolicLink: () => false, + } as fs.Stats), + ); + sinon.stub(fs.promises, 'realpath').callsFake(async (pathEntered) => pathEntered.toString()); + + const adapter = new UnittestTestDiscoveryAdapter(configService); + const discoveryPromise = adapter.discoverTests(uri, execFactory.object, cancellationTokenSource.token); + + // add in await and trigger + await discoveryPromise; + assert.ok(true, 'Test resolves correctly when triggering a cancellation token in exec observable.'); + }); + + test('DiscoverTests should set PROJECT_ROOT_PATH when project is provided', async () => { + const projectPath = path.join('/', 'workspace', 'myproject'); + const mockProject = ({ + projectId: 'file:///workspace/myproject', + projectUri: Uri.file(projectPath), + projectName: 'myproject', + workspaceUri: Uri.file('/workspace'), + } as unknown) as ProjectAdapter; + + const adapter = new UnittestTestDiscoveryAdapter(configService); + adapter.discoverTests(uri, execFactory.object, undefined, undefined, mockProject); + const script = path.join(EXTENSION_ROOT_DIR, 'python_files', 'unittestadapter', 'discovery.py'); + const argsExpected = [script, '--udiscovery', '-v', '-s', '.', '-p', 'test*']; + + // must await until the execObservable is called in order to verify it + await deferred.promise; + + execService.verify( + (x) => + x.execObservable( + typeMoq.It.is>((argsActual) => { + try { + assert.equal(argsActual.length, argsExpected.length); + assert.deepEqual(argsActual, argsExpected); + return true; + } catch (e) { + console.error(e); + throw e; + } + }), + typeMoq.It.is((options) => { + try { + // Verify PROJECT_ROOT_PATH is set when project is provided + assert.strictEqual( + options.env?.PROJECT_ROOT_PATH, + projectPath, + 'PROJECT_ROOT_PATH should be set to project URI path', + ); + assert.equal(options.cwd, expectedPath); + assert.equal(options.throwOnStdErr, true); + return true; + } catch (e) { + console.error(e); + throw e; + } + }), + ), + typeMoq.Times.once(), + ); + }); - const nextData = { status: 'error' }; - adapter.onDataReceivedHandler({ uuid: correctUuid, data: JSON.stringify(nextData) }); + test('DiscoverTests should NOT set PROJECT_ROOT_PATH when no project is provided', async () => { + const adapter = new UnittestTestDiscoveryAdapter(configService); + adapter.discoverTests(uri, execFactory.object); + const script = path.join(EXTENSION_ROOT_DIR, 'python_files', 'unittestadapter', 'discovery.py'); + const argsExpected = [script, '--udiscovery', '-v', '-s', '.', '-p', 'test*']; - const result = await promise; + // must await until the execObservable is called in order to verify it + await deferred.promise; - assert.deepStrictEqual(result, nextData); + execService.verify( + (x) => + x.execObservable( + typeMoq.It.is>((argsActual) => { + try { + assert.equal(argsActual.length, argsExpected.length); + assert.deepEqual(argsActual, argsExpected); + return true; + } catch (e) { + console.error(e); + throw e; + } + }), + typeMoq.It.is((options) => { + try { + // Verify PROJECT_ROOT_PATH is NOT set when no project is provided + assert.strictEqual( + options.env?.PROJECT_ROOT_PATH, + undefined, + 'PROJECT_ROOT_PATH should NOT be set when no project is provided', + ); + assert.equal(options.cwd, expectedPath); + assert.equal(options.throwOnStdErr, true); + return true; + } catch (e) { + console.error(e); + throw e; + } + }), + ), + typeMoq.Times.once(), + ); }); }); diff --git a/src/test/testing/testController/unittest/testExecutionAdapter.unit.test.ts b/src/test/testing/testController/unittest/testExecutionAdapter.unit.test.ts index d88f033d39a4..8a86e9228567 100644 --- a/src/test/testing/testController/unittest/testExecutionAdapter.unit.test.ts +++ b/src/test/testing/testController/unittest/testExecutionAdapter.unit.test.ts @@ -1,118 +1,581 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - +/* eslint-disable @typescript-eslint/no-explicit-any */ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. import * as assert from 'assert'; +import { DebugSessionOptions, TestRun, TestRunProfileKind, Uri } from 'vscode'; +import * as typeMoq from 'typemoq'; +import * as sinon from 'sinon'; import * as path from 'path'; -import * as typemoq from 'typemoq'; -import { Uri } from 'vscode'; -import { IConfigurationService, ITestOutputChannel } from '../../../../client/common/types'; +import { Observable } from 'rxjs/Observable'; +import { IConfigurationService } from '../../../../client/common/types'; +import { + IPythonExecutionFactory, + IPythonExecutionService, + Output, + SpawnOptions, +} from '../../../../client/common/process/types'; +import { createDeferred, Deferred } from '../../../../client/common/utils/async'; +import { ITestDebugLauncher, LaunchOptions } from '../../../../client/testing/common/types'; +import * as util from '../../../../client/testing/testController/common/utils'; import { EXTENSION_ROOT_DIR } from '../../../../client/constants'; -import { ITestServer, TestCommandOptions } from '../../../../client/testing/testController/common/types'; +import { MockChildProcess } from '../../../mocks/mockChildProcess'; +import { traceInfo } from '../../../../client/logging'; import { UnittestTestExecutionAdapter } from '../../../../client/testing/testController/unittest/testExecutionAdapter'; +import * as extapi from '../../../../client/envExt/api.internal'; +import { ProjectAdapter } from '../../../../client/testing/testController/common/projectAdapter'; +import { createMockProjectAdapter } from '../testMocks'; suite('Unittest test execution adapter', () => { - let stubConfigSettings: IConfigurationService; - let outputChannel: typemoq.IMock; - + let configService: IConfigurationService; + let execFactory = typeMoq.Mock.ofType(); + let adapter: UnittestTestExecutionAdapter; + let execService: typeMoq.IMock; + let deferred: Deferred; + let deferred4: Deferred; + let debugLauncher: typeMoq.IMock; + (global as any).EXTENSION_ROOT_DIR = EXTENSION_ROOT_DIR; + let myTestPath: string; + let mockProc: MockChildProcess; + let utilsWriteTestIdsFileStub: sinon.SinonStub; + let utilsStartRunResultNamedPipeStub: sinon.SinonStub; + let useEnvExtensionStub: sinon.SinonStub; setup(() => { - stubConfigSettings = ({ + useEnvExtensionStub = sinon.stub(extapi, 'useEnvExtension'); + useEnvExtensionStub.returns(false); + configService = ({ getSettings: () => ({ - testing: { unittestArgs: ['-v', '-s', '.', '-p', 'test*'] }, + testing: { unittestArgs: ['.'] }, }), + isTestExecution: () => false, } as unknown) as IConfigurationService; - outputChannel = typemoq.Mock.ofType(); + + // set up exec service with child process + mockProc = new MockChildProcess('', ['']); + const output = new Observable>(() => { + /* no op */ + }); + deferred4 = createDeferred(); + execService = typeMoq.Mock.ofType(); + execService + .setup((x) => x.execObservable(typeMoq.It.isAny(), typeMoq.It.isAny())) + .returns(() => { + deferred4.resolve(); + return { + proc: mockProc as any, + out: output, + dispose: () => { + /* no-body */ + }, + }; + }); + execFactory = typeMoq.Mock.ofType(); + + // added + utilsWriteTestIdsFileStub = sinon.stub(util, 'writeTestIdsFile'); + debugLauncher = typeMoq.Mock.ofType(); + execFactory + .setup((x) => x.createActivatedEnvironment(typeMoq.It.isAny())) + .returns(() => Promise.resolve(execService.object)); + deferred = createDeferred(); + execService + .setup((x) => x.exec(typeMoq.It.isAny(), typeMoq.It.isAny())) + .returns(() => { + deferred.resolve(); + return Promise.resolve({ stdout: '{}' }); + }); + execFactory.setup((p) => ((p as unknown) as any).then).returns(() => undefined); + execService.setup((p) => ((p as unknown) as any).then).returns(() => undefined); + debugLauncher.setup((p) => ((p as unknown) as any).then).returns(() => undefined); + myTestPath = path.join('/', 'my', 'test', 'path', '/'); + + utilsStartRunResultNamedPipeStub = sinon.stub(util, 'startRunResultNamedPipe'); + utilsStartRunResultNamedPipeStub.callsFake(() => Promise.resolve('runResultPipe-mockName')); + + execService.setup((x) => x.getExecutablePath()).returns(() => Promise.resolve('/mock/path/to/python')); }); + teardown(() => { + sinon.restore(); + }); + test('startTestIdServer called with correct testIds', async () => { + const deferred2 = createDeferred(); + const deferred3 = createDeferred(); + execFactory = typeMoq.Mock.ofType(); + execFactory + .setup((x) => x.createActivatedEnvironment(typeMoq.It.isAny())) + .returns(() => { + deferred2.resolve(); + return Promise.resolve(execService.object); + }); + utilsWriteTestIdsFileStub.callsFake(() => { + deferred3.resolve(); + return Promise.resolve({ + name: 'mockName', + dispose: () => { + /* no-op */ + }, + }); + }); + const testRun = typeMoq.Mock.ofType(); + testRun.setup((t) => t.token).returns(() => ({ onCancellationRequested: () => undefined } as any)); + const uri = Uri.file(myTestPath); + adapter = new UnittestTestExecutionAdapter(configService); + const testIds = ['test1id', 'test2id']; - test('runTests should send the run command to the test server', async () => { - let options: TestCommandOptions | undefined; + adapter.runTests(uri, testIds, TestRunProfileKind.Run, testRun.object, execFactory.object); - const stubTestServer = ({ - sendCommand(opt: TestCommandOptions): Promise { - delete opt.outChannel; - options = opt; - return Promise.resolve(); - }, - onDataReceived: () => { - // no body - }, - createUUID: () => '123456789', - } as unknown) as ITestServer; - - const uri = Uri.file('/foo/bar'); - const script = path.join(EXTENSION_ROOT_DIR, 'pythonFiles', 'unittestadapter', 'execution.py'); - - const adapter = new UnittestTestExecutionAdapter(stubTestServer, stubConfigSettings, outputChannel.object); - adapter.runTests(uri, [], false); - - const expectedOptions: TestCommandOptions = { - workspaceFolder: uri, - command: { script, args: ['--udiscovery', '-v', '-s', '.', '-p', 'test*'] }, - cwd: uri.fsPath, - uuid: '123456789', - debugBool: false, - testIds: [], + // add in await and trigger + await deferred2.promise; + await deferred3.promise; + mockProc.trigger('close'); + + // assert + sinon.assert.calledWithExactly(utilsWriteTestIdsFileStub, testIds); + }); + test('unittest execution called with correct args', async () => { + const deferred2 = createDeferred(); + const deferred3 = createDeferred(); + execFactory = typeMoq.Mock.ofType(); + execFactory + .setup((x) => x.createActivatedEnvironment(typeMoq.It.isAny())) + .returns(() => { + deferred2.resolve(); + return Promise.resolve(execService.object); + }); + utilsWriteTestIdsFileStub.callsFake(() => { + deferred3.resolve(); + return Promise.resolve('testIdPipe-mockName'); + }); + const testRun = typeMoq.Mock.ofType(); + testRun.setup((t) => t.token).returns(() => ({ onCancellationRequested: () => undefined } as any)); + const uri = Uri.file(myTestPath); + adapter = new UnittestTestExecutionAdapter(configService); + adapter.runTests(uri, [], TestRunProfileKind.Run, testRun.object, execFactory.object); + + await deferred2.promise; + await deferred3.promise; + await deferred4.promise; + mockProc.trigger('close'); + + const pathToPythonFiles = path.join(EXTENSION_ROOT_DIR, 'python_files'); + const pathToExecutionScript = path.join(pathToPythonFiles, 'unittestadapter', 'execution.py'); + const expectedArgs = [pathToExecutionScript, '--udiscovery', '.']; + const expectedExtraVariables = { + PYTHONPATH: myTestPath, + TEST_RUN_PIPE: 'runResultPipe-mockName', + RUN_TEST_IDS_PIPE: 'testIdPipe-mockName', }; + execService.verify( + (x) => + x.execObservable( + expectedArgs, + typeMoq.It.is((options) => { + assert.equal(options.env?.PYTHONPATH, expectedExtraVariables.PYTHONPATH); + assert.equal(options.env?.TEST_RUN_PIPE, expectedExtraVariables.TEST_RUN_PIPE); + assert.equal(options.env?.RUN_TEST_IDS_PIPE, expectedExtraVariables.RUN_TEST_IDS_PIPE); + assert.equal(options.env?.COVERAGE_ENABLED, undefined); // coverage not enabled + assert.equal(options.cwd, uri.fsPath); + assert.equal(options.throwOnStdErr, true); + return true; + }), + ), + typeMoq.Times.once(), + ); + }); + test('unittest execution respects settings.testing.cwd when present', async () => { + const deferred2 = createDeferred(); + const deferred3 = createDeferred(); + execFactory = typeMoq.Mock.ofType(); + execFactory + .setup((x) => x.createActivatedEnvironment(typeMoq.It.isAny())) + .returns(() => { + deferred2.resolve(); + return Promise.resolve(execService.object); + }); + utilsWriteTestIdsFileStub.callsFake(() => { + deferred3.resolve(); + return Promise.resolve('testIdPipe-mockName'); + }); + const testRun = typeMoq.Mock.ofType(); + testRun.setup((t) => t.token).returns(() => ({ onCancellationRequested: () => undefined } as any)); + const newCwd = path.join('new', 'path'); + configService = ({ + getSettings: () => ({ + testing: { unittestArgs: ['.'], cwd: newCwd }, + }), + isTestExecution: () => false, + } as unknown) as IConfigurationService; + const uri = Uri.file(myTestPath); + adapter = new UnittestTestExecutionAdapter(configService); + adapter.runTests(uri, [], TestRunProfileKind.Run, testRun.object, execFactory.object); + + await deferred2.promise; + await deferred3.promise; + await deferred4.promise; + mockProc.trigger('close'); - assert.deepStrictEqual(options, expectedOptions); + const pathToPythonFiles = path.join(EXTENSION_ROOT_DIR, 'python_files'); + const pathToExecutionScript = path.join(pathToPythonFiles, 'unittestadapter', 'execution.py'); + const expectedArgs = [pathToExecutionScript, '--udiscovery', '.']; + const expectedExtraVariables = { + PYTHONPATH: newCwd, + TEST_RUN_PIPE: 'runResultPipe-mockName', + RUN_TEST_IDS_PIPE: 'testIdPipe-mockName', + }; + + execService.verify( + (x) => + x.execObservable( + expectedArgs, + typeMoq.It.is((options) => { + assert.equal(options.env?.PYTHONPATH, expectedExtraVariables.PYTHONPATH); + assert.equal(options.env?.TEST_RUN_PIPE, expectedExtraVariables.TEST_RUN_PIPE); + assert.equal(options.env?.RUN_TEST_IDS_PIPE, expectedExtraVariables.RUN_TEST_IDS_PIPE); + assert.equal(options.cwd, newCwd); + assert.equal(options.throwOnStdErr, true); + return true; + }), + ), + typeMoq.Times.once(), + ); }); - test("onDataReceivedHandler should parse the data if the cwd from the payload matches the test adapter's cwd", async () => { - const stubTestServer = ({ - sendCommand(): Promise { - return Promise.resolve(); - }, - onDataReceived: () => { - // no body - }, - createUUID: () => '123456789', - } as unknown) as ITestServer; + test('Debug launched correctly for unittest', async () => { + const deferred3 = createDeferred(); + utilsWriteTestIdsFileStub.callsFake(() => Promise.resolve('testIdPipe-mockName')); + debugLauncher + .setup((dl) => dl.launchDebugger(typeMoq.It.isAny(), typeMoq.It.isAny(), typeMoq.It.isAny())) + .returns(async (_opts, callback) => { + traceInfo('stubs launch debugger'); + if (typeof callback === 'function') { + deferred3.resolve(); + callback(); + } + }); + const testRun = typeMoq.Mock.ofType(); + testRun + .setup((t) => t.token) + .returns( + () => + ({ + onCancellationRequested: () => undefined, + } as any), + ); + const uri = Uri.file(myTestPath); + adapter = new UnittestTestExecutionAdapter(configService); + adapter.runTests(uri, [], TestRunProfileKind.Debug, testRun.object, execFactory.object, debugLauncher.object); + await deferred3.promise; + debugLauncher.verify( + (x) => + x.launchDebugger( + typeMoq.It.is((launchOptions) => { + assert.equal(launchOptions.cwd, uri.fsPath); + assert.equal(launchOptions.testProvider, 'unittest'); + assert.equal(launchOptions.pytestPort, 'runResultPipe-mockName'); + assert.strictEqual(launchOptions.runTestIdsPort, 'testIdPipe-mockName'); + assert.notEqual(launchOptions.token, undefined); + return true; + }), + typeMoq.It.isAny(), + typeMoq.It.is((sessionOptions) => { + assert.equal(sessionOptions.testRun, testRun.object); + return true; + }), + ), + typeMoq.Times.once(), + ); + }); + test('unittest execution with coverage turned on correctly', async () => { + const deferred2 = createDeferred(); + const deferred3 = createDeferred(); + execFactory = typeMoq.Mock.ofType(); + execFactory + .setup((x) => x.createActivatedEnvironment(typeMoq.It.isAny())) + .returns(() => { + deferred2.resolve(); + return Promise.resolve(execService.object); + }); + utilsWriteTestIdsFileStub.callsFake(() => { + deferred3.resolve(); + return Promise.resolve('testIdPipe-mockName'); + }); + const testRun = typeMoq.Mock.ofType(); + testRun.setup((t) => t.token).returns(() => ({ onCancellationRequested: () => undefined } as any)); + const uri = Uri.file(myTestPath); + adapter = new UnittestTestExecutionAdapter(configService); + adapter.runTests(uri, [], TestRunProfileKind.Coverage, testRun.object, execFactory.object); - const uri = Uri.file('/foo/bar'); - const data = { status: 'success' }; - const uuid = '123456789'; + await deferred2.promise; + await deferred3.promise; + await deferred4.promise; + mockProc.trigger('close'); - const adapter = new UnittestTestExecutionAdapter(stubTestServer, stubConfigSettings, outputChannel.object); + const pathToPythonFiles = path.join(EXTENSION_ROOT_DIR, 'python_files'); + const pathToExecutionScript = path.join(pathToPythonFiles, 'unittestadapter', 'execution.py'); + const expectedArgs = [pathToExecutionScript, '--udiscovery', '.']; + execService.verify( + (x) => + x.execObservable( + expectedArgs, + typeMoq.It.is((options) => { + assert.equal(options.env?.COVERAGE_ENABLED, uri.fsPath); + return true; + }), + ), + typeMoq.Times.once(), + ); + }); + + test('RunTests should set PROJECT_ROOT_PATH when project is provided', async () => { + const deferred2 = createDeferred(); + const deferred3 = createDeferred(); + execFactory = typeMoq.Mock.ofType(); + execFactory + .setup((x) => x.createActivatedEnvironment(typeMoq.It.isAny())) + .returns(() => { + deferred2.resolve(); + return Promise.resolve(execService.object); + }); + utilsWriteTestIdsFileStub.callsFake(() => { + deferred3.resolve(); + return Promise.resolve('testIdPipe-mockName'); + }); + + const projectPath = path.join('/', 'workspace', 'myproject'); + const mockProject = ({ + projectId: 'file:///workspace/myproject', + projectUri: Uri.file(projectPath), + projectName: 'myproject', + workspaceUri: Uri.file('/workspace'), + } as unknown) as ProjectAdapter; - // triggers runTests flow which will run onDataReceivedHandler and the - // promise resolves into the parsed data. - const promise = adapter.runTests(uri, [], false); + const testRun = typeMoq.Mock.ofType(); + testRun.setup((t) => t.token).returns(() => ({ onCancellationRequested: () => undefined } as any)); + const uri = Uri.file(myTestPath); + adapter = new UnittestTestExecutionAdapter(configService); + adapter.runTests( + uri, + [], + TestRunProfileKind.Run, + testRun.object, + execFactory.object, + undefined, // debugLauncher + undefined, // interpreter + mockProject, + ); - adapter.onDataReceivedHandler({ uuid, data: JSON.stringify(data) }); + await deferred2.promise; + await deferred3.promise; + await deferred4.promise; + mockProc.trigger('close'); - const result = await promise; + const pathToPythonFiles = path.join(EXTENSION_ROOT_DIR, 'python_files'); + const pathToExecutionScript = path.join(pathToPythonFiles, 'unittestadapter', 'execution.py'); + const expectedArgs = [pathToExecutionScript, '--udiscovery', '.']; - assert.deepStrictEqual(result, data); + execService.verify( + (x) => + x.execObservable( + expectedArgs, + typeMoq.It.is((options) => { + // Verify PROJECT_ROOT_PATH is set when project is provided + assert.strictEqual( + options.env?.PROJECT_ROOT_PATH, + projectPath, + 'PROJECT_ROOT_PATH should be set to project URI path', + ); + return true; + }), + ), + typeMoq.Times.once(), + ); }); - test("onDataReceivedHandler should ignore the data if the cwd from the payload does not match the test adapter's cwd", async () => { - const correctUuid = '123456789'; - const incorrectUuid = '987654321'; - const stubTestServer = ({ - sendCommand(): Promise { - return Promise.resolve(); - }, - onDataReceived: () => { - // no body + + test('RunTests should NOT set PROJECT_ROOT_PATH when no project is provided', async () => { + const deferred2 = createDeferred(); + const deferred3 = createDeferred(); + execFactory = typeMoq.Mock.ofType(); + execFactory + .setup((x) => x.createActivatedEnvironment(typeMoq.It.isAny())) + .returns(() => { + deferred2.resolve(); + return Promise.resolve(execService.object); + }); + utilsWriteTestIdsFileStub.callsFake(() => { + deferred3.resolve(); + return Promise.resolve('testIdPipe-mockName'); + }); + const testRun = typeMoq.Mock.ofType(); + testRun.setup((t) => t.token).returns(() => ({ onCancellationRequested: () => undefined } as any)); + const uri = Uri.file(myTestPath); + adapter = new UnittestTestExecutionAdapter(configService); + adapter.runTests(uri, [], TestRunProfileKind.Run, testRun.object, execFactory.object); + + await deferred2.promise; + await deferred3.promise; + await deferred4.promise; + mockProc.trigger('close'); + + const pathToPythonFiles = path.join(EXTENSION_ROOT_DIR, 'python_files'); + const pathToExecutionScript = path.join(pathToPythonFiles, 'unittestadapter', 'execution.py'); + const expectedArgs = [pathToExecutionScript, '--udiscovery', '.']; + + execService.verify( + (x) => + x.execObservable( + expectedArgs, + typeMoq.It.is((options) => { + // Verify PROJECT_ROOT_PATH is NOT set when no project is provided + assert.strictEqual( + options.env?.PROJECT_ROOT_PATH, + undefined, + 'PROJECT_ROOT_PATH should NOT be set when no project is provided', + ); + return true; + }), + ), + typeMoq.Times.once(), + ); + }); + + test('Debug mode with project should pass project.pythonProject to debug launcher', async () => { + const deferred3 = createDeferred(); + utilsWriteTestIdsFileStub.callsFake(() => Promise.resolve('testIdPipe-mockName')); + + debugLauncher + .setup((dl) => dl.launchDebugger(typeMoq.It.isAny(), typeMoq.It.isAny(), typeMoq.It.isAny())) + .returns(async (_opts, callback) => { + traceInfo('stubs launch debugger'); + if (typeof callback === 'function') { + deferred3.resolve(); + callback(); + } + }); + + const testRun = typeMoq.Mock.ofType(); + testRun + .setup((t) => t.token) + .returns( + () => + ({ + onCancellationRequested: () => undefined, + } as any), + ); + + const projectPath = path.join('/', 'workspace', 'myproject'); + const mockProject = createMockProjectAdapter({ + projectPath, + projectName: 'myproject (Python 3.11)', + pythonPath: '/custom/python/path', + testProvider: 'unittest', + }); + + const uri = Uri.file(myTestPath); + adapter = new UnittestTestExecutionAdapter(configService); + adapter.runTests( + uri, + [], + TestRunProfileKind.Debug, + testRun.object, + execFactory.object, + debugLauncher.object, + undefined, + mockProject, + ); + + await deferred3.promise; + + debugLauncher.verify( + (x) => + x.launchDebugger( + typeMoq.It.is((launchOptions) => { + // Project should be passed for project-based debugging + assert.ok(launchOptions.project, 'project should be defined'); + assert.equal(launchOptions.project?.name, 'myproject (Python 3.11)'); + assert.equal(launchOptions.project?.uri.fsPath, projectPath); + return true; + }), + typeMoq.It.isAny(), + typeMoq.It.isAny(), + ), + typeMoq.Times.once(), + ); + }); + + test('useEnvExtension mode with project should use project pythonEnvironment', async () => { + // Enable the useEnvExtension path + useEnvExtensionStub.returns(true); + + utilsWriteTestIdsFileStub.callsFake(() => Promise.resolve('testIdPipe-mockName')); + + // Store the deferredTillServerClose so we can resolve it + let serverCloseDeferred: Deferred | undefined; + utilsStartRunResultNamedPipeStub.callsFake((_callback: unknown, deferred: Deferred, _token: unknown) => { + serverCloseDeferred = deferred; + return Promise.resolve('runResultPipe-mockName'); + }); + + const projectPath = path.join('/', 'workspace', 'myproject'); + const mockProject = createMockProjectAdapter({ + projectPath, + projectName: 'myproject (Python 3.11)', + pythonPath: '/custom/python/path', + testProvider: 'unittest', + }); + + // Stub runInBackground to capture which environment was used + const runInBackgroundStub = sinon.stub(extapi, 'runInBackground'); + const exitCallbacks: ((code: number, signal: string | null) => void)[] = []; + // Promise that resolves when the production code registers its onExit handler + const onExitRegistered = createDeferred(); + const mockProc2 = { + stdout: { on: sinon.stub() }, + stderr: { on: sinon.stub() }, + onExit: (cb: (code: number, signal: string | null) => void) => { + exitCallbacks.push(cb); + onExitRegistered.resolve(); }, - createUUID: () => correctUuid, - } as unknown) as ITestServer; + kill: sinon.stub(), + }; + runInBackgroundStub.callsFake(() => Promise.resolve(mockProc2 as any)); - const uri = Uri.file('/foo/bar'); + const testRun = typeMoq.Mock.ofType(); + testRun + .setup((t) => t.token) + .returns( + () => + ({ + onCancellationRequested: () => undefined, + } as any), + ); - const adapter = new UnittestTestExecutionAdapter(stubTestServer, stubConfigSettings, outputChannel.object); + const uri = Uri.file(myTestPath); + adapter = new UnittestTestExecutionAdapter(configService); + const runPromise = adapter.runTests( + uri, + [], + TestRunProfileKind.Run, + testRun.object, + execFactory.object, + debugLauncher.object, + undefined, + mockProject, + ); - // triggers runTests flow which will run onDataReceivedHandler and the - // promise resolves into the parsed data. - const promise = adapter.runTests(uri, [], false); + // Wait for production code to register its onExit handler + await onExitRegistered.promise; - const data = { status: 'success' }; - // will not resolve due to incorrect UUID - adapter.onDataReceivedHandler({ uuid: incorrectUuid, data: JSON.stringify(data) }); + // Simulate process exit to complete the test + exitCallbacks.forEach((cb) => cb(0, null)); - const nextData = { status: 'error' }; - // will resolve and nextData will be returned as result - adapter.onDataReceivedHandler({ uuid: correctUuid, data: JSON.stringify(nextData) }); + // Resolve the server close deferred to allow the runTests to complete + serverCloseDeferred?.resolve(); - const result = await promise; + await runPromise; - assert.deepStrictEqual(result, nextData); + // Verify runInBackground was called with the project's Python environment + sinon.assert.calledOnce(runInBackgroundStub); + const envArg = runInBackgroundStub.firstCall.args[0]; + // The environment should be the project's pythonEnvironment + assert.ok(envArg, 'runInBackground should be called with an environment'); + assert.equal(envArg.execInfo?.run?.executable, '/custom/python/path'); }); }); diff --git a/src/test/testing/testController/utils.unit.test.ts b/src/test/testing/testController/utils.unit.test.ts index d971c7d37c9f..3cba6fb697a5 100644 --- a/src/test/testing/testController/utils.unit.test.ts +++ b/src/test/testing/testController/utils.unit.test.ts @@ -1,67 +1,754 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - import * as assert from 'assert'; +import * as sinon from 'sinon'; +import * as fs from 'fs'; +import * as path from 'path'; +import { CancellationToken, TestController, TestItem, Uri, Range, Position } from 'vscode'; +import { writeTestIdsFile, populateTestTree } from '../../../client/testing/testController/common/utils'; +import { EXTENSION_ROOT_DIR } from '../../../client/constants'; import { - JSONRPC_CONTENT_LENGTH_HEADER, - JSONRPC_CONTENT_TYPE_HEADER, - JSONRPC_UUID_HEADER, - jsonRPCContent, - jsonRPCHeaders, -} from '../../../client/testing/testController/common/utils'; - -suite('Test Controller Utils: JSON RPC', () => { - test('Empty raw data string', async () => { - const rawDataString = ''; - - const output = jsonRPCHeaders(rawDataString); - assert.deepStrictEqual(output.headers.size, 0); - assert.deepStrictEqual(output.remainingRawData, ''); - }); - - test('Valid data empty JSON', async () => { - const rawDataString = `${JSONRPC_CONTENT_LENGTH_HEADER}: 2\n${JSONRPC_CONTENT_TYPE_HEADER}: application/json\n${JSONRPC_UUID_HEADER}: 1234\n\n{}`; - - const rpcHeaders = jsonRPCHeaders(rawDataString); - assert.deepStrictEqual(rpcHeaders.headers.size, 3); - assert.deepStrictEqual(rpcHeaders.remainingRawData, '{}'); - const rpcContent = jsonRPCContent(rpcHeaders.headers, rpcHeaders.remainingRawData); - assert.deepStrictEqual(rpcContent.extractedJSON, '{}'); - }); - - test('Valid data NO JSON', async () => { - const rawDataString = `${JSONRPC_CONTENT_LENGTH_HEADER}: 0\n${JSONRPC_CONTENT_TYPE_HEADER}: application/json\n${JSONRPC_UUID_HEADER}: 1234\n\n`; - - const rpcHeaders = jsonRPCHeaders(rawDataString); - assert.deepStrictEqual(rpcHeaders.headers.size, 3); - assert.deepStrictEqual(rpcHeaders.remainingRawData, ''); - const rpcContent = jsonRPCContent(rpcHeaders.headers, rpcHeaders.remainingRawData); - assert.deepStrictEqual(rpcContent.extractedJSON, ''); - }); - - test('Valid data with full JSON', async () => { - // this is just some random JSON - const json = - '{"jsonrpc": "2.0", "method": "initialize", "params": {"processId": 1234, "rootPath": "/home/user/project", "rootUri": "file:///home/user/project", "capabilities": {}}, "id": 0}'; - const rawDataString = `${JSONRPC_CONTENT_LENGTH_HEADER}: ${json.length}\n${JSONRPC_CONTENT_TYPE_HEADER}: application/json\n${JSONRPC_UUID_HEADER}: 1234\n\n${json}`; - - const rpcHeaders = jsonRPCHeaders(rawDataString); - assert.deepStrictEqual(rpcHeaders.headers.size, 3); - assert.deepStrictEqual(rpcHeaders.remainingRawData, json); - const rpcContent = jsonRPCContent(rpcHeaders.headers, rpcHeaders.remainingRawData); - assert.deepStrictEqual(rpcContent.extractedJSON, json); - }); - - test('Valid data with multiple JSON', async () => { - const json = - '{"jsonrpc": "2.0", "method": "initialize", "params": {"processId": 1234, "rootPath": "/home/user/project", "rootUri": "file:///home/user/project", "capabilities": {}}, "id": 0}'; - const rawDataString = `${JSONRPC_CONTENT_LENGTH_HEADER}: ${json.length}\n${JSONRPC_CONTENT_TYPE_HEADER}: application/json\n${JSONRPC_UUID_HEADER}: 1234\n\n${json}`; - const rawDataString2 = rawDataString + rawDataString; - - const rpcHeaders = jsonRPCHeaders(rawDataString2); - assert.deepStrictEqual(rpcHeaders.headers.size, 3); - const rpcContent = jsonRPCContent(rpcHeaders.headers, rpcHeaders.remainingRawData); - assert.deepStrictEqual(rpcContent.extractedJSON, json); - assert.deepStrictEqual(rpcContent.remainingRawData, rawDataString); + DiscoveredTestNode, + DiscoveredTestItem, + ITestResultResolver, +} from '../../../client/testing/testController/common/types'; +import { RunTestTag, DebugTestTag } from '../../../client/testing/testController/common/testItemUtilities'; + +suite('writeTestIdsFile tests', () => { + let sandbox: sinon.SinonSandbox; + + setup(() => { + sandbox = sinon.createSandbox(); + }); + + teardown(() => { + sandbox.restore(); + }); + + test('should write test IDs to a temporary file', async () => { + const testIds = ['test1', 'test2', 'test3']; + const writeFileStub = sandbox.stub(fs.promises, 'writeFile').resolves(); + + // Set up XDG_RUNTIME_DIR + process.env = { + ...process.env, + XDG_RUNTIME_DIR: '/xdg/runtime/dir', + }; + + await writeTestIdsFile(testIds); + + assert.ok(writeFileStub.calledOnceWith(sinon.match.string, testIds.join('\n'))); + }); + + test('should handle error when accessing temp directory', async () => { + const testIds = ['test1', 'test2', 'test3']; + const error = new Error('Access error'); + const accessStub = sandbox.stub(fs.promises, 'access').rejects(error); + const writeFileStub = sandbox.stub(fs.promises, 'writeFile').resolves(); + const mkdirStub = sandbox.stub(fs.promises, 'mkdir').resolves(); + + const result = await writeTestIdsFile(testIds); + + const tempFileFolder = path.join(EXTENSION_ROOT_DIR, '.temp'); + + assert.ok(result.startsWith(tempFileFolder)); + + assert.ok(accessStub.called); + assert.ok(mkdirStub.called); + assert.ok(writeFileStub.calledOnceWith(sinon.match.string, testIds.join('\n'))); + }); +}); + +suite('getTempDir tests', () => { + let sandbox: sinon.SinonSandbox; + let originalPlatform: NodeJS.Platform; + let originalEnv: NodeJS.ProcessEnv; + + setup(() => { + sandbox = sinon.createSandbox(); + originalPlatform = process.platform; + originalEnv = process.env; + }); + + teardown(() => { + sandbox.restore(); + Object.defineProperty(process, 'platform', { value: originalPlatform }); + process.env = originalEnv; + }); + + test('should use XDG_RUNTIME_DIR on non-Windows if available', async () => { + if (process.platform === 'win32') { + return; + } + // Force platform to be Linux + Object.defineProperty(process, 'platform', { value: 'linux' }); + + // Set up XDG_RUNTIME_DIR + process.env = { ...process.env, XDG_RUNTIME_DIR: '/xdg/runtime/dir' }; + + const testIds = ['test1', 'test2', 'test3']; + sandbox.stub(fs.promises, 'access').resolves(); + sandbox.stub(fs.promises, 'writeFile').resolves(); + + // This will use getTempDir internally + const result = await writeTestIdsFile(testIds); + + assert.ok(result.startsWith('/xdg/runtime/dir')); + }); +}); + +suite('populateTestTree tests', () => { + let sandbox: sinon.SinonSandbox; + let testController: TestController; + let resultResolver: ITestResultResolver; + let cancelationToken: CancellationToken; + let createTestItemStub: sinon.SinonStub; + let itemsAddStub: sinon.SinonStub; + let itemsGetStub: sinon.SinonStub; + + setup(() => { + sandbox = sinon.createSandbox(); + + // Create stubs for TestController methods + createTestItemStub = sandbox.stub(); + itemsAddStub = sandbox.stub(); + itemsGetStub = sandbox.stub(); + + // Create mock TestController + testController = { + createTestItem: createTestItemStub, + items: { + add: itemsAddStub, + get: itemsGetStub, + delete: sandbox.stub(), + replace: sandbox.stub(), + forEach: sandbox.stub(), + size: 0, + [Symbol.iterator]: sandbox.stub(), + }, + } as any; + + // Create mock result resolver + resultResolver = { + runIdToTestItem: new Map(), + runIdToVSid: new Map(), + vsIdToRunId: new Map(), + detailedCoverageMap: new Map(), + resolveDiscovery: sandbox.stub(), + resolveExecution: sandbox.stub(), + _resolveDiscovery: sandbox.stub(), + _resolveExecution: sandbox.stub(), + _resolveCoverage: sandbox.stub(), + }; + + // Mock cancellation token + cancelationToken = { + isCancellationRequested: false, + onCancellationRequested: sandbox.stub(), + } as any; + }); + + teardown(() => { + sandbox.restore(); + }); + + test('should create a root node if testRoot is undefined', () => { + // Arrange + const testTreeData: DiscoveredTestNode = { + path: '/test/path/root', + name: 'RootTest', + type_: 'folder', + id_: 'root-id', + children: [], + }; + + const mockRootItem: TestItem = { + id: '/test/path/root', + label: 'RootTest', + uri: Uri.file('/test/path/root'), + canResolveChildren: true, + tags: [RunTestTag, DebugTestTag], + children: { + add: sandbox.stub(), + get: sandbox.stub(), + delete: sandbox.stub(), + replace: sandbox.stub(), + forEach: sandbox.stub(), + size: 0, + [Symbol.iterator]: sandbox.stub(), + }, + } as any; + + createTestItemStub.returns(mockRootItem); + + // Act + populateTestTree(testController, testTreeData, undefined, resultResolver, cancelationToken); + + // Assert + assert.ok(createTestItemStub.calledOnce); + // Check the args manually - function uses testTreeData.path as id + const call = createTestItemStub.firstCall; + assert.strictEqual(call.args[0], '/test/path/root'); + assert.strictEqual(call.args[1], 'RootTest'); + // Don't check Uri.file since it's complex to compare + assert.ok(itemsAddStub.calledOnceWith(mockRootItem)); + assert.strictEqual(mockRootItem.canResolveChildren, true); + assert.deepStrictEqual(mockRootItem.tags, [RunTestTag, DebugTestTag]); + }); + + test('should recursively add children as TestItems', () => { + // Arrange + // Tree structure: + // RootWorkspaceFolder (folder) + // └── test_example (test) + const testItem: DiscoveredTestItem = { + path: '/test/path/test.py', + name: 'test_example', + type_: 'test', + id_: 'test-id', + lineno: 10, + runID: 'run-id-123', + }; + + const testTreeData: DiscoveredTestNode = { + path: '/test/path/root', + name: 'RootWorkspaceFolder', + type_: 'folder', + id_: 'root-id', + children: [testItem], + }; + + const childrenAddStub = sandbox.stub(); + const mockRootItem: TestItem = { + id: 'root-id', + children: { + add: childrenAddStub, + }, + } as any; + + const mockTestItem: TestItem = { + id: 'test-id', + label: 'test_example', + uri: Uri.file('/test/path/test.py'), + canResolveChildren: false, + tags: [], + range: undefined, + } as any; + + createTestItemStub.returns(mockTestItem); + + // Act + populateTestTree(testController, testTreeData, mockRootItem, resultResolver, cancelationToken); + + // Assert + assert.ok(createTestItemStub.calledOnceWith('test-id', 'test_example', sinon.match.any)); + assert.ok(childrenAddStub.calledOnceWith(mockTestItem)); + assert.strictEqual(mockTestItem.canResolveChildren, false); + assert.deepStrictEqual(mockTestItem.tags, [RunTestTag, DebugTestTag]); + }); + + test('should create TestItem with correct range when lineno is provided', () => { + // Arrange + const testItem: DiscoveredTestItem = { + path: '/test/path/test.py', + name: 'test_example', + type_: 'test', + id_: 'test-id', + lineno: 5, + runID: 'run-id-123', + }; + + const testTreeData: DiscoveredTestNode = { + path: '/test/path/root', + name: 'RootTest', + type_: 'folder', + id_: 'root-id', + children: [testItem], + }; + + const mockRootItem: TestItem = { + children: { add: sandbox.stub() }, + } as any; + + const mockTestItem: TestItem = { + tags: [], + range: undefined, + } as any; + + createTestItemStub.returns(mockTestItem); + + // Act + populateTestTree(testController, testTreeData, mockRootItem, resultResolver, cancelationToken); + + // Assert + const expectedRange = new Range(new Position(4, 0), new Position(5, 0)); + assert.deepStrictEqual(mockTestItem.range, expectedRange); + }); + + test('should handle lineno = 0 correctly', () => { + // Arrange + const testItem: DiscoveredTestItem = { + path: '/test/path/test.py', + name: 'test_example', + type_: 'test', + id_: 'test-id', + lineno: '0', + runID: 'run-id-123', + }; + + const testTreeData: DiscoveredTestNode = { + path: '/test/path/root', + name: 'RootTest', + type_: 'folder', + id_: 'root-id', + children: [testItem], + }; + + const mockRootItem: TestItem = { + children: { add: sandbox.stub() }, + } as any; + + const mockTestItem: TestItem = { + tags: [], + range: undefined, + } as any; + + createTestItemStub.returns(mockTestItem); + + // Act + populateTestTree(testController, testTreeData, mockRootItem, resultResolver, cancelationToken); + + // Assert- if lineno is '0', range should be defined but at the top + const expectedRange = new Range(new Position(0, 0), new Position(0, 0)); + + assert.deepStrictEqual(mockTestItem.range, expectedRange); + }); + + test('should update resultResolver mappings correctly for test items', () => { + // Arrange + const testItem: DiscoveredTestItem = { + path: '/test/path/test.py', + name: 'test_example', + type_: 'test', + id_: 'test-id', + lineno: 10, + runID: 'run-id-123', + }; + + const testTreeData: DiscoveredTestNode = { + path: '/test/path/root', + name: 'RootTest', + type_: 'folder', + id_: 'root-id', + children: [testItem], + }; + + const mockRootItem: TestItem = { + children: { add: sandbox.stub() }, + } as any; + + const mockTestItem: TestItem = { + id: 'test-id', + tags: [], + } as any; + + createTestItemStub.returns(mockTestItem); + + // Act + populateTestTree(testController, testTreeData, mockRootItem, resultResolver, cancelationToken); + + // Assert + assert.strictEqual(resultResolver.runIdToTestItem.get('run-id-123'), mockTestItem); + assert.strictEqual(resultResolver.runIdToVSid.get('run-id-123'), 'test-id'); + assert.strictEqual(resultResolver.vsIdToRunId.get('test-id'), 'run-id-123'); + }); + + test('should create nodes for non-leaf items and recurse', () => { + // Arrange + // Tree structure: + // RootTest (folder) + // └── NestedFolder (folder) + // └── nested_test (test) + const nestedTestItem: DiscoveredTestItem = { + path: '/test/path/nested_test.py', + name: 'nested_test', + type_: 'test', + id_: 'nested-test-id', + lineno: 5, + runID: 'nested-run-id', + }; + + const nestedNode: DiscoveredTestNode = { + path: '/test/path/nested', + name: 'NestedFolder', + type_: 'folder', + id_: 'nested-id', + children: [nestedTestItem], + }; + + const testTreeData: DiscoveredTestNode = { + path: '/test/path/root', + name: 'RootTest', + type_: 'folder', + id_: 'root-id', + children: [nestedNode], + }; + + const rootChildrenAddStub = sandbox.stub(); + const rootChildrenGetStub = sandbox.stub().returns(undefined); + const mockRootItem: TestItem = { + children: { add: rootChildrenAddStub, get: rootChildrenGetStub }, + } as any; + + const nestedChildrenAddStub = sandbox.stub(); + const nestedChildrenGetStub = sandbox.stub().returns(undefined); + const mockNestedNode: TestItem = { + id: 'nested-id', + canResolveChildren: true, + tags: [], + children: { add: nestedChildrenAddStub, get: nestedChildrenGetStub }, + } as any; + + const mockNestedTestItem: TestItem = { + id: 'nested-test-id', + tags: [], + } as any; + + createTestItemStub.onFirstCall().returns(mockNestedNode); + createTestItemStub.onSecondCall().returns(mockNestedTestItem); + + // Act + populateTestTree(testController, testTreeData, mockRootItem, resultResolver, cancelationToken); + + // Assert + // Should create nested node - uses child.id_ for non-leaf nodes + assert.ok(createTestItemStub.calledWith('nested-id', 'NestedFolder', sinon.match.any)); + assert.ok(rootChildrenAddStub.calledWith(mockNestedNode)); + assert.strictEqual(mockNestedNode.canResolveChildren, true); + assert.deepStrictEqual(mockNestedNode.tags, [RunTestTag, DebugTestTag]); + + // Should create nested test item - uses child.id_ for test items too + assert.ok(createTestItemStub.calledWith('nested-test-id', 'nested_test', sinon.match.any)); + assert.ok(nestedChildrenAddStub.calledWith(mockNestedTestItem)); + }); + + test('should reuse existing nodes when they already exist', () => { + // Arrange + // Tree structure: + // RootTest (folder) + // └── ExistingFolder (folder, already exists) + // └── test_example (test) + const testItem: DiscoveredTestItem = { + path: '/test/path/test.py', + name: 'test_example', + type_: 'test', + id_: 'test-id', + lineno: 10, + runID: 'run-id-123', + }; + + const nestedNode: DiscoveredTestNode = { + path: '/test/path/existing', + name: 'ExistingFolder', + type_: 'folder', + id_: 'existing-id', + children: [testItem], + }; + + const testTreeData: DiscoveredTestNode = { + path: '/test/path/root', + name: 'RootTest', + type_: 'folder', + id_: 'root-id', + children: [nestedNode], + }; + + const rootChildrenAddStub = sandbox.stub(); + const existingChildrenAddStub = sandbox.stub(); + const existingChildrenGetStub = sandbox.stub().returns(undefined); + const existingNode: TestItem = { + id: 'existing-id', + children: { add: existingChildrenAddStub, get: existingChildrenGetStub }, + } as any; + const rootChildrenGetStub = sandbox.stub().withArgs('existing-id').returns(existingNode); + const mockRootItem: TestItem = { + children: { add: rootChildrenAddStub, get: rootChildrenGetStub }, + } as any; + + const mockTestItem: TestItem = { + tags: [], + } as any; + + // Mock existing node in testController.items + itemsGetStub.withArgs('/test/path/existing').returns(existingNode); + createTestItemStub.returns(mockTestItem); + + // Act + populateTestTree(testController, testTreeData, mockRootItem, resultResolver, cancelationToken); + + // Assert + // Should not create a new node, should reuse existing one + assert.ok(createTestItemStub.calledOnceWith('test-id', 'test_example', sinon.match.any)); + // Should not create a new node for the existing folder + assert.ok(createTestItemStub.neverCalledWith('existing-id', 'ExistingFolder', sinon.match.any)); + assert.ok(existingChildrenAddStub.calledWith(mockTestItem)); + // Should not add existing node to root children again + assert.ok(rootChildrenAddStub.notCalled); + }); + + test('should respect cancellation token and stop processing', () => { + // Arrange + const testItem1: DiscoveredTestItem = { + path: '/test/path/test1.py', + name: 'test1', + type_: 'test', + id_: 'test1-id', + lineno: 10, + runID: 'run-id-1', + }; + + const testItem2: DiscoveredTestItem = { + path: '/test/path/test2.py', + name: 'test2', + type_: 'test', + id_: 'test2-id', + lineno: 20, + runID: 'run-id-2', + }; + + const testTreeData: DiscoveredTestNode = { + path: '/test/path/root', + name: 'RootTest', + type_: 'folder', + id_: 'root-id', + children: [testItem1, testItem2], + }; + + const rootChildrenAddStub = sandbox.stub(); + const mockRootItem: TestItem = { + children: { add: rootChildrenAddStub }, + } as any; + + // Set cancellation token to be cancelled + const cancelledToken = { + isCancellationRequested: true, + onCancellationRequested: sandbox.stub(), + } as any; + + // Act + populateTestTree(testController, testTreeData, mockRootItem, resultResolver, cancelledToken); + + // Assert - no test items should be created when cancelled + assert.ok(createTestItemStub.notCalled); + assert.ok(rootChildrenAddStub.notCalled); + assert.strictEqual(resultResolver.runIdToTestItem.size, 0); + }); + + test('should handle empty children array gracefully', () => { + // Arrange + const testTreeData: DiscoveredTestNode = { + path: '/test/path/root', + name: 'RootTest', + type_: 'folder', + id_: 'root-id', + children: [], + }; + + const rootChildrenAddStub = sandbox.stub(); + const mockRootItem: TestItem = { + children: { add: rootChildrenAddStub }, + } as any; + + // Act + populateTestTree(testController, testTreeData, mockRootItem, resultResolver, cancelationToken); + + // Assert - should complete without errors + assert.ok(createTestItemStub.notCalled); + assert.ok(rootChildrenAddStub.notCalled); + }); + + test('should add correct tags to all created items', () => { + // Arrange + // Tree structure: + // RootTest (folder) + // └── NestedFolder (folder) + // └── test_example (test) + const testItem: DiscoveredTestItem = { + path: '/test/path/test.py', + name: 'test_example', + type_: 'test', + id_: 'test-id', + lineno: 10, + runID: 'run-id-123', + }; + + const nestedNode: DiscoveredTestNode = { + path: '/test/path/nested', + name: 'NestedFolder', + type_: 'folder', + id_: 'nested-id', + children: [testItem], + }; + + const testTreeData: DiscoveredTestNode = { + path: '/test/path/root', + name: 'RootTest', + type_: 'folder', + id_: 'root-id', + children: [nestedNode], + }; + + const mockRootItem: TestItem = { + id: 'root-id', + tags: [], + canResolveChildren: true, + children: { add: sandbox.stub(), get: sandbox.stub().returns(undefined) }, + } as any; + + const mockNestedNode: TestItem = { + id: 'nested-id', + tags: [], + canResolveChildren: true, + children: { add: sandbox.stub(), get: sandbox.stub().returns(undefined) }, + } as any; + + const mockTestItem: TestItem = { + id: 'test-id', + tags: [], + canResolveChildren: false, + } as any; + + createTestItemStub.onCall(0).returns(mockRootItem); + createTestItemStub.onCall(1).returns(mockNestedNode); + createTestItemStub.onCall(2).returns(mockTestItem); + + // Act + populateTestTree(testController, testTreeData, undefined, resultResolver, cancelationToken); + + // Assert - All items should have RunTestTag and DebugTestTag + assert.deepStrictEqual(mockRootItem.tags, [RunTestTag, DebugTestTag]); + assert.deepStrictEqual(mockNestedNode.tags, [RunTestTag, DebugTestTag]); + assert.deepStrictEqual(mockTestItem.tags, [RunTestTag, DebugTestTag]); + }); + test('should handle a test node with no lineno property', () => { + // Arrange + // Tree structure: + // RootTest (folder) + // └── test_without_lineno (test, no lineno) + const testItem = { + path: '/test/path/test.py', + name: 'test_without_lineno', + type_: 'test', + id_: 'test-no-lineno-id', + runID: 'run-id-no-lineno', + } as DiscoveredTestItem; + + const testTreeData: DiscoveredTestNode = { + path: '/test/path/root', + name: 'RootTest', + type_: 'folder', + id_: 'root-id', + children: [testItem], + }; + + const childrenAddStub = sandbox.stub(); + const mockRootItem: TestItem = { + id: 'root-id', + children: { + add: childrenAddStub, + }, + } as any; + + const mockTestItem: TestItem = { + id: 'test-no-lineno-id', + label: 'test_without_lineno', + uri: Uri.file('/test/path/test.py'), + canResolveChildren: false, + tags: [], + range: undefined, + } as any; + + createTestItemStub.returns(mockTestItem); + + // Act + populateTestTree(testController, testTreeData, mockRootItem, resultResolver, cancelationToken); + + // Assert + assert.ok(createTestItemStub.calledOnceWith('test-no-lineno-id', 'test_without_lineno', sinon.match.any)); + assert.ok(childrenAddStub.calledOnceWith(mockTestItem)); + // range is undefined since lineno is not provided + assert.strictEqual(mockTestItem.range, undefined); + assert.deepStrictEqual(mockTestItem.tags, [RunTestTag, DebugTestTag]); + }); + + test('should handle a node with multiple children', () => { + // Arrange + // Tree structure: + // RootTest (folder) + // β”œβ”€β”€ test_one (test) + // └── test_two (test) + const testItem1: DiscoveredTestItem = { + path: '/test/path/test1.py', + name: 'test_one', + type_: 'test', + id_: 'test-one-id', + lineno: 3, + runID: 'run-id-one', + }; + const testItem2: DiscoveredTestItem = { + path: '/test/path/test2.py', + name: 'test_two', + type_: 'test', + id_: 'test-two-id', + lineno: 7, + runID: 'run-id-two', + }; + + const testTreeData: DiscoveredTestNode = { + path: '/test/path/root', + name: 'RootTest', + type_: 'folder', + id_: 'root-id', + children: [testItem1, testItem2], + }; + + const childrenAddStub = sandbox.stub(); + const mockRootItem: TestItem = { + id: 'root-id', + children: { + add: childrenAddStub, + }, + } as any; + + const mockTestItem1: TestItem = { + id: 'test-one-id', + label: 'test_one', + uri: Uri.file('/test/path/test1.py'), + canResolveChildren: false, + tags: [], + range: new Range(new Position(2, 0), new Position(3, 0)), + } as any; + const mockTestItem2: TestItem = { + id: 'test-two-id', + label: 'test_two', + uri: Uri.file('/test/path/test2.py'), + canResolveChildren: false, + tags: [], + range: new Range(new Position(6, 0), new Position(7, 0)), + } as any; + + createTestItemStub.onFirstCall().returns(mockTestItem1); + createTestItemStub.onSecondCall().returns(mockTestItem2); + + // Act + populateTestTree(testController, testTreeData, mockRootItem, resultResolver, cancelationToken); + + // Assert + assert.ok(createTestItemStub.calledWith('test-one-id', 'test_one', sinon.match.any)); + assert.ok(createTestItemStub.calledWith('test-two-id', 'test_two', sinon.match.any)); + // two test items called with mockRootItem's method childrenAddStub + assert.strictEqual(childrenAddStub.callCount, 2); + assert.deepStrictEqual(mockTestItem1.tags, [RunTestTag, DebugTestTag]); + assert.deepStrictEqual(mockTestItem2.tags, [RunTestTag, DebugTestTag]); + assert.deepStrictEqual(mockTestItem1.range, new Range(new Position(2, 0), new Position(3, 0))); + assert.deepStrictEqual(mockTestItem2.range, new Range(new Position(6, 0), new Position(7, 0))); }); }); diff --git a/src/test/testing/testController/workspaceTestAdapter.unit.test.ts b/src/test/testing/testController/workspaceTestAdapter.unit.test.ts index 539647aece9f..6d2895ca2979 100644 --- a/src/test/testing/testController/workspaceTestAdapter.unit.test.ts +++ b/src/test/testing/testController/workspaceTestAdapter.unit.test.ts @@ -5,32 +5,34 @@ import * as assert from 'assert'; import * as sinon from 'sinon'; import * as typemoq from 'typemoq'; -import { TestController, TestItem, Uri } from 'vscode'; -import { IConfigurationService, ITestOutputChannel } from '../../../client/common/types'; +import { TestController, TestItem, TestItemCollection, TestRun, Uri } from 'vscode'; +import { IConfigurationService } from '../../../client/common/types'; import { UnittestTestDiscoveryAdapter } from '../../../client/testing/testController/unittest/testDiscoveryAdapter'; import { UnittestTestExecutionAdapter } from '../../../client/testing/testController/unittest/testExecutionAdapter'; // 7/7 import { WorkspaceTestAdapter } from '../../../client/testing/testController/workspaceTestAdapter'; import * as Telemetry from '../../../client/telemetry'; import { EventName } from '../../../client/telemetry/constants'; -import { ITestServer } from '../../../client/testing/testController/common/types'; +import { ITestResultResolver } from '../../../client/testing/testController/common/types'; +import * as testItemUtilities from '../../../client/testing/testController/common/testItemUtilities'; +import * as util from '../../../client/testing/testController/common/utils'; +import * as ResultResolver from '../../../client/testing/testController/common/resultResolver'; +import { IPythonExecutionFactory } from '../../../client/common/process/types'; suite('Workspace test adapter', () => { suite('Test discovery', () => { - let stubTestServer: ITestServer; let stubConfigSettings: IConfigurationService; + let stubResultResolver: ITestResultResolver; let discoverTestsStub: sinon.SinonStub; let sendTelemetryStub: sinon.SinonStub; - let outputChannel: typemoq.IMock; let telemetryEvent: { eventName: EventName; properties: Record }[] = []; + let execFactory: typemoq.IMock; // Stubbed test controller (see comment around L.40) let testController: TestController; let log: string[] = []; - const sandbox = sinon.createSandbox(); - setup(() => { stubConfigSettings = ({ getSettings: () => ({ @@ -38,14 +40,18 @@ suite('Workspace test adapter', () => { }), } as unknown) as IConfigurationService; - stubTestServer = ({ - sendCommand(): Promise { - return Promise.resolve(); + stubResultResolver = ({ + resolveDiscovery: () => { + // no body }, - onDataReceived: () => { + resolveExecution: () => { // no body }, - } as unknown) as ITestServer; + } as unknown) as ITestResultResolver; + + // const vsIdToRunIdGetStub = sinon.stub(stubResultResolver.vsIdToRunId, 'get'); + // const expectedRunId = 'expectedRunId'; + // vsIdToRunIdGetStub.withArgs(sinon.match.any).returns(expectedRunId); // For some reason the 'tests' namespace in vscode returns undefined. // While I figure out how to expose to the tests, they will run @@ -97,39 +103,70 @@ suite('Workspace test adapter', () => { }); }; - discoverTestsStub = sandbox.stub(UnittestTestDiscoveryAdapter.prototype, 'discoverTests'); - sendTelemetryStub = sandbox.stub(Telemetry, 'sendTelemetryEvent').callsFake(mockSendTelemetryEvent); - outputChannel = typemoq.Mock.ofType(); + discoverTestsStub = sinon.stub(UnittestTestDiscoveryAdapter.prototype, 'discoverTests'); + sendTelemetryStub = sinon.stub(Telemetry, 'sendTelemetryEvent').callsFake(mockSendTelemetryEvent); }); teardown(() => { telemetryEvent = []; log = []; testController.dispose(); - sandbox.restore(); + sinon.restore(); + }); + + test('If discovery failed correctly create error node', async () => { + discoverTestsStub.rejects(new Error('foo')); + + const testDiscoveryAdapter = new UnittestTestDiscoveryAdapter(stubConfigSettings); + const testExecutionAdapter = new UnittestTestExecutionAdapter(stubConfigSettings); + const uriFoo = Uri.parse('foo'); + const workspaceTestAdapter = new WorkspaceTestAdapter( + 'unittest', + testDiscoveryAdapter, + testExecutionAdapter, + uriFoo, + stubResultResolver, + ); + + const blankTestItem = ({ + canResolveChildren: false, + tags: [], + children: { + add: () => { + // empty + }, + }, + } as unknown) as TestItem; + const errorTestItemOptions: testItemUtilities.ErrorTestItemOptions = { + id: 'id', + label: 'label', + error: 'error', + }; + const createErrorTestItemStub = sinon.stub(testItemUtilities, 'createErrorTestItem').returns(blankTestItem); + const buildErrorNodeOptionsStub = sinon.stub(util, 'buildErrorNodeOptions').returns(errorTestItemOptions); + const testProvider = 'unittest'; + + execFactory = typemoq.Mock.ofType(); + await workspaceTestAdapter.discoverTests(testController, execFactory.object); + + sinon.assert.calledWithMatch(createErrorTestItemStub, sinon.match.any, sinon.match.any); + sinon.assert.calledWithMatch(buildErrorNodeOptionsStub, uriFoo, sinon.match.any, testProvider); }); test("When discovering tests, the workspace test adapter should call the test discovery adapter's discoverTest method", async () => { discoverTestsStub.resolves(); - const testDiscoveryAdapter = new UnittestTestDiscoveryAdapter( - stubTestServer, - stubConfigSettings, - outputChannel.object, - ); - const testExecutionAdapter = new UnittestTestExecutionAdapter( - stubTestServer, - stubConfigSettings, - outputChannel.object, - ); + const testDiscoveryAdapter = new UnittestTestDiscoveryAdapter(stubConfigSettings); + const testExecutionAdapter = new UnittestTestExecutionAdapter(stubConfigSettings); const workspaceTestAdapter = new WorkspaceTestAdapter( 'unittest', testDiscoveryAdapter, testExecutionAdapter, Uri.parse('foo'), + stubResultResolver, ); - await workspaceTestAdapter.discoverTests(testController); + await workspaceTestAdapter.discoverTests(testController, execFactory.object); sinon.assert.calledOnce(discoverTestsStub); }); @@ -145,26 +182,19 @@ suite('Workspace test adapter', () => { }), ); - const testDiscoveryAdapter = new UnittestTestDiscoveryAdapter( - stubTestServer, - stubConfigSettings, - outputChannel.object, - ); - const testExecutionAdapter = new UnittestTestExecutionAdapter( - stubTestServer, - stubConfigSettings, - outputChannel.object, - ); + const testDiscoveryAdapter = new UnittestTestDiscoveryAdapter(stubConfigSettings); + const testExecutionAdapter = new UnittestTestExecutionAdapter(stubConfigSettings); const workspaceTestAdapter = new WorkspaceTestAdapter( 'unittest', testDiscoveryAdapter, testExecutionAdapter, Uri.parse('foo'), + stubResultResolver, ); // Try running discovery twice - const one = workspaceTestAdapter.discoverTests(testController); - const two = workspaceTestAdapter.discoverTests(testController); + const one = workspaceTestAdapter.discoverTests(testController, execFactory.object); + const two = workspaceTestAdapter.discoverTests(testController, execFactory.object); Promise.all([one, two]); @@ -174,25 +204,18 @@ suite('Workspace test adapter', () => { test('If discovery succeeds, send a telemetry event with the "failed" key set to false', async () => { discoverTestsStub.resolves({ status: 'success' }); - const testDiscoveryAdapter = new UnittestTestDiscoveryAdapter( - stubTestServer, - stubConfigSettings, - outputChannel.object, - ); - const testExecutionAdapter = new UnittestTestExecutionAdapter( - stubTestServer, - stubConfigSettings, - outputChannel.object, - ); + const testDiscoveryAdapter = new UnittestTestDiscoveryAdapter(stubConfigSettings); + const testExecutionAdapter = new UnittestTestExecutionAdapter(stubConfigSettings); const workspaceTestAdapter = new WorkspaceTestAdapter( 'unittest', testDiscoveryAdapter, testExecutionAdapter, Uri.parse('foo'), + stubResultResolver, ); - await workspaceTestAdapter.discoverTests(testController); + await workspaceTestAdapter.discoverTests(testController, execFactory.object); sinon.assert.calledWith(sendTelemetryStub, EventName.UNITTEST_DISCOVERY_DONE); assert.strictEqual(telemetryEvent.length, 2); @@ -204,43 +227,295 @@ suite('Workspace test adapter', () => { test('If discovery failed, send a telemetry event with the "failed" key set to true, and add an error node to the test controller', async () => { discoverTestsStub.rejects(new Error('foo')); - const testDiscoveryAdapter = new UnittestTestDiscoveryAdapter( - stubTestServer, - stubConfigSettings, - outputChannel.object, - ); - const testExecutionAdapter = new UnittestTestExecutionAdapter( - stubTestServer, - stubConfigSettings, - outputChannel.object, - ); + const testDiscoveryAdapter = new UnittestTestDiscoveryAdapter(stubConfigSettings); + const testExecutionAdapter = new UnittestTestExecutionAdapter(stubConfigSettings); const workspaceTestAdapter = new WorkspaceTestAdapter( 'unittest', testDiscoveryAdapter, testExecutionAdapter, Uri.parse('foo'), + stubResultResolver, ); - await workspaceTestAdapter.discoverTests(testController); + await workspaceTestAdapter.discoverTests(testController, execFactory.object); sinon.assert.calledWith(sendTelemetryStub, EventName.UNITTEST_DISCOVERY_DONE); assert.strictEqual(telemetryEvent.length, 2); const lastEvent = telemetryEvent[1]; assert.ok(lastEvent.properties.failed); + }); + }); + suite('Test execution workspace test adapter', () => { + let stubConfigSettings: IConfigurationService; + let stubResultResolver: ITestResultResolver; + let executionTestsStub: sinon.SinonStub; + let sendTelemetryStub: sinon.SinonStub; + let runInstance: typemoq.IMock; + let testControllerMock: typemoq.IMock; + let telemetryEvent: { eventName: EventName; properties: Record }[] = []; + let resultResolver: ResultResolver.PythonResultResolver; + let execFactory: typemoq.IMock; + + // Stubbed test controller (see comment around L.40) + let testController: TestController; + let log: string[] = []; + + const sandbox = sinon.createSandbox(); + + setup(() => { + stubConfigSettings = ({ + getSettings: () => ({ + testing: { unittestArgs: ['--foo'] }, + }), + } as unknown) as IConfigurationService; + + stubResultResolver = ({ + resolveDiscovery: () => { + // no body + }, + resolveExecution: () => { + // no body + }, + vsIdToRunId: { + get: sinon.stub().returns('expectedRunId'), + }, + } as unknown) as ITestResultResolver; + const testItem = ({ + canResolveChildren: false, + tags: [], + children: { + add: () => { + // empty + }, + }, + } as unknown) as TestItem; + + testController = ({ + items: { + get: () => { + log.push('get'); + }, + add: () => { + log.push('add'); + }, + replace: () => { + log.push('replace'); + }, + delete: () => { + log.push('delete'); + }, + }, + createTestItem: () => { + log.push('createTestItem'); + return testItem; + }, + dispose: () => { + // empty + }, + } as unknown) as TestController; - assert.deepStrictEqual(log, ['createTestItem', 'add']); + const mockSendTelemetryEvent = ( + eventName: EventName, + _: number | Record | undefined, + properties: unknown, + ) => { + telemetryEvent.push({ + eventName, + properties: properties as Record, + }); + }; + + executionTestsStub = sandbox.stub(UnittestTestExecutionAdapter.prototype, 'runTests'); + sendTelemetryStub = sandbox.stub(Telemetry, 'sendTelemetryEvent').callsFake(mockSendTelemetryEvent); + execFactory = typemoq.Mock.ofType(); + runInstance = typemoq.Mock.ofType(); + + const testProvider = 'pytest'; + const workspaceUri = Uri.file('foo'); + resultResolver = new ResultResolver.PythonResultResolver(testController, testProvider, workspaceUri); + }); + + teardown(() => { + telemetryEvent = []; + log = []; + testController.dispose(); + sandbox.restore(); + }); + test('When executing tests, the right tests should be sent to be executed', async () => { + const testDiscoveryAdapter = new UnittestTestDiscoveryAdapter(stubConfigSettings); + const testExecutionAdapter = new UnittestTestExecutionAdapter(stubConfigSettings); + const workspaceTestAdapter = new WorkspaceTestAdapter( + 'unittest', + testDiscoveryAdapter, + testExecutionAdapter, + Uri.parse('foo'), + resultResolver, + ); + resultResolver.runIdToVSid.set('mockTestItem1', 'mockTestItem1'); + + sinon.stub(testItemUtilities, 'getTestCaseNodes').callsFake((testNode: TestItem) => + // Custom implementation logic here based on the provided testNode and collection + + // Example implementation: returning a predefined array of TestItem objects + [testNode], + ); + + const mockTestItem1 = createMockTestItem('mockTestItem1'); + const mockTestItem2 = createMockTestItem('mockTestItem2'); + const mockTestItems: [string, TestItem][] = [ + ['1', mockTestItem1], + ['2', mockTestItem2], + // Add as many mock TestItems as needed + ]; + const iterableMock = mockTestItems[Symbol.iterator](); + + const testItemCollectionMock = typemoq.Mock.ofType(); + + testItemCollectionMock + .setup((x) => x.forEach(typemoq.It.isAny())) + .callback((callback) => { + let result = iterableMock.next(); + while (!result.done) { + callback(result.value[1]); + result = iterableMock.next(); + } + }) + .returns(() => mockTestItem1); + testControllerMock = typemoq.Mock.ofType(); + testControllerMock.setup((t) => t.items).returns(() => testItemCollectionMock.object); + + await workspaceTestAdapter.executeTests( + testController, + runInstance.object, + [mockTestItem1, mockTestItem2], + execFactory.object, + ); + + runInstance.verify((r) => r.started(typemoq.It.isAny()), typemoq.Times.exactly(2)); + }); + + test("When executing tests, the workspace test adapter should call the test execute adapter's executionTest method", async () => { + const testDiscoveryAdapter = new UnittestTestDiscoveryAdapter(stubConfigSettings); + const testExecutionAdapter = new UnittestTestExecutionAdapter(stubConfigSettings); + const workspaceTestAdapter = new WorkspaceTestAdapter( + 'unittest', + testDiscoveryAdapter, + testExecutionAdapter, + Uri.parse('foo'), + stubResultResolver, + ); + + await workspaceTestAdapter.executeTests(testController, runInstance.object, [], execFactory.object); + + sinon.assert.calledOnce(executionTestsStub); }); - /** - * TODO To test: - * - successful discovery but no data: delete everything from the test controller - * - successful discovery with error status: add error node to tree - * - single root: populate tree if there's no root node - * - single root: update tree if there's a root node - * - single root: delete tree if there are no tests in the test data - * - multiroot: update the correct folders - */ + test('If execution is already running, do not call executionAdapter.runTests again', async () => { + executionTestsStub.callsFake( + async () => + new Promise((resolve) => { + setTimeout(() => { + // Simulate time taken by discovery. + resolve(); + }, 2000); + }), + ); + + const testDiscoveryAdapter = new UnittestTestDiscoveryAdapter(stubConfigSettings); + const testExecutionAdapter = new UnittestTestExecutionAdapter(stubConfigSettings); + const workspaceTestAdapter = new WorkspaceTestAdapter( + 'unittest', + testDiscoveryAdapter, + testExecutionAdapter, + Uri.parse('foo'), + stubResultResolver, + ); + + // Try running discovery twice + const one = workspaceTestAdapter.executeTests(testController, runInstance.object, [], execFactory.object); + const two = workspaceTestAdapter.executeTests(testController, runInstance.object, [], execFactory.object); + + Promise.all([one, two]); + + sinon.assert.calledOnce(executionTestsStub); + }); + + test('If execution failed correctly create error node', async () => { + executionTestsStub.rejects(new Error('foo')); + + const testDiscoveryAdapter = new UnittestTestDiscoveryAdapter(stubConfigSettings); + const testExecutionAdapter = new UnittestTestExecutionAdapter(stubConfigSettings); + + const workspaceTestAdapter = new WorkspaceTestAdapter( + 'unittest', + testDiscoveryAdapter, + testExecutionAdapter, + Uri.parse('foo'), + stubResultResolver, + ); + + const blankTestItem = ({ + canResolveChildren: false, + tags: [], + children: { + add: () => { + // empty + }, + }, + } as unknown) as TestItem; + const errorTestItemOptions: testItemUtilities.ErrorTestItemOptions = { + id: 'id', + label: 'label', + error: 'error', + }; + const createErrorTestItemStub = sinon.stub(testItemUtilities, 'createErrorTestItem').returns(blankTestItem); + const buildErrorNodeOptionsStub = sinon.stub(util, 'buildErrorNodeOptions').returns(errorTestItemOptions); + const testProvider = 'unittest'; + + await workspaceTestAdapter.executeTests(testController, runInstance.object, [], execFactory.object); + + sinon.assert.calledWithMatch(createErrorTestItemStub, sinon.match.any, sinon.match.any); + sinon.assert.calledWithMatch(buildErrorNodeOptionsStub, Uri.parse('foo'), sinon.match.any, testProvider); + }); + + test('If execution failed, send a telemetry event with the "failed" key set to true, and add an error node to the test controller', async () => { + executionTestsStub.rejects(new Error('foo')); + + const testDiscoveryAdapter = new UnittestTestDiscoveryAdapter(stubConfigSettings); + const testExecutionAdapter = new UnittestTestExecutionAdapter(stubConfigSettings); + + const workspaceTestAdapter = new WorkspaceTestAdapter( + 'unittest', + testDiscoveryAdapter, + testExecutionAdapter, + Uri.parse('foo'), + stubResultResolver, + ); + + await workspaceTestAdapter.executeTests(testController, runInstance.object, [], execFactory.object); + + sinon.assert.calledWith(sendTelemetryStub, EventName.UNITTEST_RUN_ALL_FAILED); + assert.strictEqual(telemetryEvent.length, 1); + }); }); }); + +function createMockTestItem(id: string): TestItem { + const range = typemoq.Mock.ofType(); + const mockTestItem = ({ + id, + canResolveChildren: false, + tags: [], + children: { + add: () => { + // empty + }, + }, + range, + uri: Uri.file('/foo/bar'), + } as unknown) as TestItem; + + return mockTestItem; +} diff --git a/src/test/testing/utils.unit.test.ts b/src/test/testing/utils.unit.test.ts new file mode 100644 index 000000000000..8efa0cee0e65 --- /dev/null +++ b/src/test/testing/utils.unit.test.ts @@ -0,0 +1,51 @@ +import { expect, use } from 'chai'; +import * as chaiAsPromised from 'chai-as-promised'; +import * as utils from '../../client/testing/utils'; +import sinon from 'sinon'; +use(chaiAsPromised.default); + +function test_idToModuleClassMethod() { + try { + expect(utils.idToModuleClassMethod('foo')).to.equal('foo'); + expect(utils.idToModuleClassMethod('a/b/c.pyMyClass')).to.equal('c.MyClass'); + expect(utils.idToModuleClassMethod('a/b/c.pyMyClassmy_method')).to.equal('c.MyClass.my_method'); + expect(utils.idToModuleClassMethod('\\MyClass')).to.be.undefined; + console.log('test_idToModuleClassMethod passed'); + } catch (e) { + console.error('test_idToModuleClassMethod failed:', e); + } +} + +async function test_writeTestIdToClipboard() { + let clipboardStub = sinon.stub(utils, 'clipboardWriteText').resolves(); + const { writeTestIdToClipboard } = utils; + try { + // unittest id + const testItem = { id: 'a/b/c.pyMyClass\\my_method' }; + await writeTestIdToClipboard(testItem as any); + sinon.assert.calledOnceWithExactly(clipboardStub, 'c.MyClass.my_method'); + clipboardStub.resetHistory(); + + // pytest id + const testItem2 = { id: 'tests/test_foo.py::TestClass::test_method' }; + await writeTestIdToClipboard(testItem2 as any); + sinon.assert.calledOnceWithExactly(clipboardStub, 'tests/test_foo.py::TestClass::test_method'); + clipboardStub.resetHistory(); + + // undefined + await writeTestIdToClipboard(undefined as any); + sinon.assert.notCalled(clipboardStub); + + console.log('test_writeTestIdToClipboard passed'); + } catch (e) { + console.error('test_writeTestIdToClipboard failed:', e); + } finally { + sinon.restore(); + } +} + +// Run tests +(async () => { + test_idToModuleClassMethod(); + await test_writeTestIdToClipboard(); +})(); diff --git a/src/test/utils/fs.ts b/src/test/utils/fs.ts index 2b78184d13e2..13f46bd38f82 100644 --- a/src/test/utils/fs.ts +++ b/src/test/utils/fs.ts @@ -3,7 +3,7 @@ 'use strict'; -import * as fsapi from 'fs-extra'; +import * as fsapi from '../../client/common/platform/fs-paths'; import * as path from 'path'; import * as tmp from 'tmp'; import { parseTree } from '../../client/common/utils/text'; diff --git a/src/test/utils/interpreters.ts b/src/test/utils/interpreters.ts index e499c85ca96e..ece3b7731c5c 100644 --- a/src/test/utils/interpreters.ts +++ b/src/test/utils/interpreters.ts @@ -9,10 +9,6 @@ import { EnvironmentType, PythonEnvironment } from '../../client/pythonEnvironme /** * Creates a PythonInterpreter object for testing purposes, with unique name, version and path. * If required a custom name, version and the like can be provided. - * - * @export - * @param {Partial} [info] - * @returns {PythonEnvironment} */ export function createPythonInterpreter(info?: Partial): PythonEnvironment { const rnd = new Date().getTime().toString(); diff --git a/src/test/utils/vscode.ts b/src/test/utils/vscode.ts index 44f745dbaf90..4364c507c36f 100644 --- a/src/test/utils/vscode.ts +++ b/src/test/utils/vscode.ts @@ -1,15 +1,23 @@ import * as path from 'path'; -import * as fs from 'fs-extra'; +import * as fs from '../../client/common/platform/fs-paths'; import { EXTENSION_ROOT_DIR } from '../../client/common/constants'; -export function getVersion(): string { +const insidersVersion = /^\^(\d+\.\d+\.\d+)-(insider|\d{8})$/; + +export function getChannel(): string { if (process.env.VSC_PYTHON_CI_TEST_VSC_CHANNEL) { return process.env.VSC_PYTHON_CI_TEST_VSC_CHANNEL; } const packageJsonPath = path.join(EXTENSION_ROOT_DIR, 'package.json'); if (fs.pathExistsSync(packageJsonPath)) { const packageJson = fs.readJSONSync(packageJsonPath); - return packageJson.engines.vscode.replace('^', ''); + const engineVersion = packageJson.engines.vscode; + if (insidersVersion.test(engineVersion)) { + // Can't pass in the version number for an insiders build; + // https://github.com/microsoft/vscode-test/issues/176 + return 'insiders'; + } + return engineVersion.replace('^', ''); } return 'stable'; } diff --git a/src/test/vscode-mock.ts b/src/test/vscode-mock.ts index ebbe7ca59e72..b7ea2bc549a0 100644 --- a/src/test/vscode-mock.ts +++ b/src/test/vscode-mock.ts @@ -3,21 +3,22 @@ 'use strict'; -import * as TypeMoq from 'typemoq'; import * as vscode from 'vscode'; import * as vscodeMocks from './mocks/vsc'; import { vscMockTelemetryReporter } from './mocks/vsc/telemetryReporter'; +import { anything, instance, mock, when } from 'ts-mockito'; +import { TestItem } from 'vscode'; const Module = require('module'); type VSCode = typeof vscode; const mockedVSCode: Partial = {}; -export const mockedVSCodeNamespaces: { [P in keyof VSCode]?: TypeMoq.IMock } = {}; +export const mockedVSCodeNamespaces: { [P in keyof VSCode]?: VSCode[P] } = {}; const originalLoad = Module._load; function generateMock(name: K): void { - const mockedObj = TypeMoq.Mock.ofType(); - (mockedVSCode as any)[name] = mockedObj.object; + const mockedObj = mock(); + (mockedVSCode as any)[name] = instance(mockedObj); mockedVSCodeNamespaces[name] = mockedObj as any; } @@ -35,15 +36,26 @@ export function initialize() { generateMock('window'); generateMock('commands'); generateMock('languages'); + generateMock('extensions'); generateMock('env'); generateMock('debug'); generateMock('scm'); - generateNotebookMocks(); + generateMock('notebooks'); // Use mock clipboard fo testing purposes. const clipboard = new MockClipboard(); - mockedVSCodeNamespaces.env?.setup((e) => e.clipboard).returns(() => clipboard); - mockedVSCodeNamespaces.env?.setup((e) => e.appName).returns(() => 'Insider'); + when(mockedVSCodeNamespaces.env!.clipboard).thenReturn(clipboard); + when(mockedVSCodeNamespaces.env!.appName).thenReturn('Insider'); + + // This API is used in src/client/telemetry/telemetry.ts + const extension = mock>(); + const packageJson = mock(); + const contributes = mock(); + when(extension.packageJSON).thenReturn(instance(packageJson)); + when(packageJson.contributes).thenReturn(instance(contributes)); + when(contributes.debuggers).thenReturn([{ aiKey: '' }]); + when(mockedVSCodeNamespaces.extensions!.getExtension(anything())).thenReturn(instance(extension)); + when(mockedVSCodeNamespaces.extensions!.all).thenReturn([]); // When upgrading to npm 9-10, this might have to change, as we could have explicit imports (named imports). Module._load = function (request: any, _parent: any) { @@ -70,6 +82,8 @@ mockedVSCode.Hover = vscodeMocks.Hover; mockedVSCode.Disposable = vscodeMocks.Disposable as any; mockedVSCode.ExtensionKind = vscodeMocks.ExtensionKind; mockedVSCode.CodeAction = vscodeMocks.CodeAction; +mockedVSCode.TestMessage = vscodeMocks.TestMessage; +mockedVSCode.Location = vscodeMocks.Location; mockedVSCode.EventEmitter = vscodeMocks.EventEmitter; mockedVSCode.CancellationTokenSource = vscodeMocks.CancellationTokenSource; mockedVSCode.CompletionItemKind = vscodeMocks.CompletionItemKind; @@ -100,7 +114,7 @@ mockedVSCode.ViewColumn = vscodeMocks.vscMockExtHostedTypes.ViewColumn; mockedVSCode.TextEditorRevealType = vscodeMocks.vscMockExtHostedTypes.TextEditorRevealType; mockedVSCode.TreeItem = vscodeMocks.vscMockExtHostedTypes.TreeItem; mockedVSCode.TreeItemCollapsibleState = vscodeMocks.vscMockExtHostedTypes.TreeItemCollapsibleState; -mockedVSCode.CodeActionKind = vscodeMocks.CodeActionKind; +(mockedVSCode as any).CodeActionKind = vscodeMocks.CodeActionKind; mockedVSCode.CompletionItemKind = vscodeMocks.CompletionItemKind; mockedVSCode.CompletionTriggerKind = vscodeMocks.CompletionTriggerKind; mockedVSCode.DebugAdapterExecutable = vscodeMocks.DebugAdapterExecutable; @@ -120,21 +134,44 @@ mockedVSCode.LogLevel = vscodeMocks.LogLevel; (mockedVSCode as any).ProtocolTypeHierarchyItem = vscodeMocks.vscMockExtHostedTypes.ProtocolTypeHierarchyItem; (mockedVSCode as any).CancellationError = vscodeMocks.vscMockExtHostedTypes.CancellationError; (mockedVSCode as any).LSPCancellationError = vscodeMocks.vscMockExtHostedTypes.LSPCancellationError; +mockedVSCode.TestRunProfileKind = vscodeMocks.TestRunProfileKind; +(mockedVSCode as any).TestCoverageCount = class TestCoverageCount { + constructor(public covered: number, public total: number) {} +}; +(mockedVSCode as any).FileCoverage = class FileCoverage { + constructor( + public uri: any, + public statementCoverage: any, + public branchCoverage?: any, + public declarationCoverage?: any, + ) {} +}; +(mockedVSCode as any).StatementCoverage = class StatementCoverage { + constructor(public executed: number | boolean, public location: any, public branches?: any) {} +}; -// This API is used in src/client/telemetry/telemetry.ts -const extensions = TypeMoq.Mock.ofType(); -extensions.setup((e) => e.all).returns(() => []); -const extension = TypeMoq.Mock.ofType>(); -const packageJson = TypeMoq.Mock.ofType(); -const contributes = TypeMoq.Mock.ofType(); -extension.setup((e) => e.packageJSON).returns(() => packageJson.object); -packageJson.setup((p) => p.contributes).returns(() => contributes.object); -contributes.setup((p) => p.debuggers).returns(() => [{ aiKey: '' }]); -extensions.setup((e) => e.getExtension(TypeMoq.It.isAny())).returns(() => extension.object); -mockedVSCode.extensions = extensions.object; - -function generateNotebookMocks() { - const mockedObj = TypeMoq.Mock.ofType<{}>(); - (mockedVSCode as any).notebook = mockedObj.object; - (mockedVSCodeNamespaces as any).notebook = mockedObj as any; +// Mock TestController for vscode.tests namespace +function createMockTestController(): vscode.TestController { + const disposable = { dispose: () => undefined }; + return ({ + items: { + forEach: () => undefined, + get: () => undefined, + add: () => undefined, + replace: () => undefined, + delete: () => undefined, + size: 0, + [Symbol.iterator]: function* () {}, + }, + createRunProfile: () => disposable, + createTestItem: () => ({} as TestItem), + dispose: () => undefined, + resolveHandler: undefined, + refreshHandler: undefined, + } as unknown) as vscode.TestController; } + +// Add tests namespace with createTestController +(mockedVSCode as any).tests = { + createTestController: (_id: string, _label: string) => createMockTestController(), +}; diff --git a/src/testMultiRootWkspc/multi.code-workspace b/src/testMultiRootWkspc/multi.code-workspace index 1daf409a0836..51d218783041 100644 --- a/src/testMultiRootWkspc/multi.code-workspace +++ b/src/testMultiRootWkspc/multi.code-workspace @@ -37,11 +37,6 @@ "python.linting.pylintEnabled": true, "python.linting.pycodestyleEnabled": false, "python.linting.prospectorEnabled": false, - "python.formatting.provider": "yapf", - "python.sortImports.args": [ - "-sp", - "/Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/sorting/withconfig" - ], "python.linting.lintOnSave": false, "python.linting.enabled": true, "python.pythonPath": "python" diff --git a/src/testMultiRootWkspc/smokeTests/create_delete_file.py b/src/testMultiRootWkspc/smokeTests/create_delete_file.py new file mode 100644 index 000000000000..399bc4863c15 --- /dev/null +++ b/src/testMultiRootWkspc/smokeTests/create_delete_file.py @@ -0,0 +1,5 @@ +with open('smart_send_smoke.txt', 'w') as f: + f.write('This is for smart send smoke test') +import os + +os.remove('smart_send_smoke.txt') diff --git a/src/testTestingRootWkspc/coverageWorkspace/even.py b/src/testTestingRootWkspc/coverageWorkspace/even.py new file mode 100644 index 000000000000..e395b024ecc5 --- /dev/null +++ b/src/testTestingRootWkspc/coverageWorkspace/even.py @@ -0,0 +1,8 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + + +def number_type(n: int) -> str: + if n % 2 == 0: + return "even" + return "odd" diff --git a/src/testTestingRootWkspc/coverageWorkspace/test_even.py b/src/testTestingRootWkspc/coverageWorkspace/test_even.py new file mode 100644 index 000000000000..ca78535860f4 --- /dev/null +++ b/src/testTestingRootWkspc/coverageWorkspace/test_even.py @@ -0,0 +1,11 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +from even import number_type +import unittest + + +class TestNumbers(unittest.TestCase): + def test_odd(self): + n = number_type(1) + assert n == "odd" diff --git a/src/testTestingRootWkspc/discoveryErrorWorkspace/test_seg_fault_discovery.py b/src/testTestingRootWkspc/discoveryErrorWorkspace/test_seg_fault_discovery.py new file mode 100644 index 000000000000..5aac911b575a --- /dev/null +++ b/src/testTestingRootWkspc/discoveryErrorWorkspace/test_seg_fault_discovery.py @@ -0,0 +1,16 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +import unittest +import ctypes + +ctypes.string_at(0) # Dereference a NULL pointer + + +class TestSegmentationFault(unittest.TestCase): + def test_segfault(self): + assert True + + +if __name__ == "__main__": + unittest.main() diff --git a/src/testTestingRootWkspc/errorWorkspace/test_seg_fault.py b/src/testTestingRootWkspc/errorWorkspace/test_seg_fault.py new file mode 100644 index 000000000000..80be80f023c2 --- /dev/null +++ b/src/testTestingRootWkspc/errorWorkspace/test_seg_fault.py @@ -0,0 +1,19 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +import unittest +import ctypes + + +class TestSegmentationFault(unittest.TestCase): + def cause_segfault(self): + print("Causing a segmentation fault") + ctypes.string_at(0) # Dereference a NULL pointer + + def test_segfault(self): + self.cause_segfault() + assert True + + +if __name__ == "__main__": + unittest.main() diff --git a/src/testTestingRootWkspc/largeWorkspace/test_parameterized_subtest.py b/src/testTestingRootWkspc/largeWorkspace/test_parameterized_subtest.py new file mode 100644 index 000000000000..40c5de531f7c --- /dev/null +++ b/src/testTestingRootWkspc/largeWorkspace/test_parameterized_subtest.py @@ -0,0 +1,119 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. +import pytest +import unittest + + +@pytest.mark.parametrize("num", range(0, 2000)) +def test_odd_even(num): + assert num % 2 == 0 + + +class NumbersTest(unittest.TestCase): + def test_even(self): + for i in range(0, 2000): + with self.subTest(i=i): + self.assertEqual(i % 2, 0) + + +# The repeated tests below are to test the unittest communication as it hits it maximum limit of bytes. + + +class NumberedTests1(unittest.TestCase): + def test_abc(self): + self.assertEqual(1 % 2, 0) + + +class NumberedTests2(unittest.TestCase): + def test_abc(self): + self.assertEqual(1 % 2, 0) + + +class NumberedTests3(unittest.TestCase): + def test_abc(self): + self.assertEqual(1 % 2, 0) + + +class NumberedTests4(unittest.TestCase): + def test_abc(self): + self.assertEqual(1 % 2, 0) + + +class NumberedTests5(unittest.TestCase): + def test_abc(self): + self.assertEqual(1 % 2, 0) + + +class NumberedTests6(unittest.TestCase): + def test_abc(self): + self.assertEqual(1 % 2, 0) + + +class NumberedTests7(unittest.TestCase): + def test_abc(self): + self.assertEqual(1 % 2, 0) + + +class NumberedTests8(unittest.TestCase): + def test_abc(self): + self.assertEqual(1 % 2, 0) + + +class NumberedTests9(unittest.TestCase): + def test_abc(self): + self.assertEqual(1 % 2, 0) + + +class NumberedTests10(unittest.TestCase): + def test_abc(self): + self.assertEqual(1 % 2, 0) + + +class NumberedTests11(unittest.TestCase): + def test_abc(self): + self.assertEqual(1 % 2, 0) + + +class NumberedTests12(unittest.TestCase): + def test_abc(self): + self.assertEqual(1 % 2, 0) + + +class NumberedTests13(unittest.TestCase): + def test_abc(self): + self.assertEqual(1 % 2, 0) + + +class NumberedTests14(unittest.TestCase): + def test_abc(self): + self.assertEqual(1 % 2, 0) + + +class NumberedTests15(unittest.TestCase): + def test_abc(self): + self.assertEqual(1 % 2, 0) + + +class NumberedTests16(unittest.TestCase): + def test_abc(self): + self.assertEqual(1 % 2, 0) + + +class NumberedTests17(unittest.TestCase): + def test_abc(self): + self.assertEqual(1 % 2, 0) + + +class NumberedTests18(unittest.TestCase): + def test_abc(self): + self.assertEqual(1 % 2, 0) + + +class NumberedTests19(unittest.TestCase): + def test_abc(self): + self.assertEqual(1 % 2, 0) + + +class NumberedTests20(unittest.TestCase): + def test_abc(self): + self.assertEqual(1 % 2, 0) diff --git a/src/testTestingRootWkspc/loggingWorkspace/test_logging.py b/src/testTestingRootWkspc/loggingWorkspace/test_logging.py new file mode 100644 index 000000000000..a3e77f06ae78 --- /dev/null +++ b/src/testTestingRootWkspc/loggingWorkspace/test_logging.py @@ -0,0 +1,13 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +import logging + + +def test_logging(caplog): + logger = logging.getLogger(__name__) + caplog.set_level(logging.DEBUG) # Set minimum log level to capture + + logger.debug("This is a debug message.") + logger.info("This is an info message.") + logger.warning("This is a warning message.") + logger.error("This is an error message.") + logger.critical("This is a critical message.") diff --git a/src/testTestingRootWkspc/smallWorkspace/test_simple.py b/src/testTestingRootWkspc/smallWorkspace/test_simple.py new file mode 100644 index 000000000000..f68a0d7d0d93 --- /dev/null +++ b/src/testTestingRootWkspc/smallWorkspace/test_simple.py @@ -0,0 +1,25 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. +import unittest +import logging +import sys + + +def test_a(caplog): + logger = logging.getLogger(__name__) + # caplog.set_level(logging.ERROR) # Set minimum log level to capture + logger.setLevel(logging.WARN) + + logger.debug("This is a debug message.") + logger.info("This is an info message.") + logger.warning("This is a warning message.") + logger.error("This is an error message.") + logger.critical("This is a critical message.") + assert False + + +class SimpleClass(unittest.TestCase): + def test_simple_unit(self): + print("expected printed output, stdout") + print("expected printed output, stderr", file=sys.stderr) + assert True diff --git a/src/testTestingRootWkspc/target workspace/custom_sub_folder/test_simple.py b/src/testTestingRootWkspc/target workspace/custom_sub_folder/test_simple.py new file mode 100644 index 000000000000..179d6420c76f --- /dev/null +++ b/src/testTestingRootWkspc/target workspace/custom_sub_folder/test_simple.py @@ -0,0 +1,8 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. +import unittest + + +class SimpleClass(unittest.TestCase): + def test_simple_unit(self): + assert True diff --git a/tsconfig.browser.json b/tsconfig.browser.json index ca00a4e2b193..e34f3f6788ac 100644 --- a/tsconfig.browser.json +++ b/tsconfig.browser.json @@ -2,6 +2,7 @@ "extends": "./tsconfig.json", "include": [ "./src/client/browser", - "./types" + "./types", + "./typings/*.d.ts", ] } diff --git a/tsconfig.json b/tsconfig.json index 89f7a9c808b8..718d4ab4aad1 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -4,7 +4,8 @@ "paths": { "*": ["types/*"] }, - "module": "commonjs", + "module": "NodeNext", + "moduleResolution": "NodeNext", "target": "es2018", "outDir": "out", "lib": [ @@ -38,6 +39,7 @@ "src/smoke", "build", "out", - "tmp" + "tmp", + "pythonExtensionApi" ] } diff --git a/types/vscode.proposed.envCollectionOptions.d.ts b/types/vscode.proposed.envCollectionOptions.d.ts new file mode 100644 index 000000000000..d25a92725a4d --- /dev/null +++ b/types/vscode.proposed.envCollectionOptions.d.ts @@ -0,0 +1,56 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +declare module 'vscode' { + + // https://github.com/microsoft/vscode/issues/179476 + + /** + * Options applied to the mutator. + */ + export interface EnvironmentVariableMutatorOptions { + /** + * Apply to the environment just before the process is created. + * + * Defaults to true. + */ + applyAtProcessCreation?: boolean; + + /** + * Apply to the environment in the shell integration script. Note that this _will not_ apply + * the mutator if shell integration is disabled or not working for some reason. + * + * Defaults to false. + */ + applyAtShellIntegration?: boolean; + } + + /** + * A type of mutation and its value to be applied to an environment variable. + */ + export interface EnvironmentVariableMutator { + /** + * Options applied to the mutator. + */ + readonly options: EnvironmentVariableMutatorOptions; + } + + export interface EnvironmentVariableCollection extends Iterable<[variable: string, mutator: EnvironmentVariableMutator]> { + /** + * @param options Options applied to the mutator. + */ + replace(variable: string, value: string, options?: EnvironmentVariableMutatorOptions): void; + + /** + * @param options Options applied to the mutator. + */ + append(variable: string, value: string, options?: EnvironmentVariableMutatorOptions): void; + + /** + * @param options Options applied to the mutator. + */ + prepend(variable: string, value: string, options?: EnvironmentVariableMutatorOptions): void; + } +} diff --git a/types/vscode.proposed.envCollectionWorkspace.d.ts b/types/vscode.proposed.envCollectionWorkspace.d.ts index b1176a9d46c2..a03a639b5ee2 100644 --- a/types/vscode.proposed.envCollectionWorkspace.d.ts +++ b/types/vscode.proposed.envCollectionWorkspace.d.ts @@ -4,35 +4,27 @@ *--------------------------------------------------------------------------------------------*/ declare module 'vscode' { + // https://github.com/microsoft/vscode/issues/171173 - // https://github.com/microsoft/vscode/issues/171173 + // export interface ExtensionContext { + // /** + // * Gets the extension's global environment variable collection for this workspace, enabling changes to be + // * applied to terminal environment variables. + // */ + // readonly environmentVariableCollection: GlobalEnvironmentVariableCollection; + // } - export interface EnvironmentVariableMutator { - readonly type: EnvironmentVariableMutatorType; - readonly value: string; - readonly scope: EnvironmentVariableScope | undefined; - } - - export interface EnvironmentVariableCollection extends Iterable<[variable: string, mutator: EnvironmentVariableMutator]> { - /** - * Sets a description for the environment variable collection, this will be used to describe the changes in the UI. - * @param description A description for the environment variable collection. - * @param scope Specific scope to which this description applies to. - */ - setDescription(description: string | MarkdownString | undefined, scope?: EnvironmentVariableScope): void; - replace(variable: string, value: string, scope?: EnvironmentVariableScope): void; - append(variable: string, value: string, scope?: EnvironmentVariableScope): void; - prepend(variable: string, value: string, scope?: EnvironmentVariableScope): void; - get(variable: string, scope?: EnvironmentVariableScope): EnvironmentVariableMutator | undefined; - delete(variable: string, scope?: EnvironmentVariableScope): void; - clear(scope?: EnvironmentVariableScope): void; - - } - - export type EnvironmentVariableScope = { - /** - * The workspace folder to which this collection applies to. If unspecified, collection applies to all workspace folders. - */ - workspaceFolder?: WorkspaceFolder; - }; + export interface GlobalEnvironmentVariableCollection extends EnvironmentVariableCollection { + /** + * Gets scope-specific environment variable collection for the extension. This enables alterations to + * terminal environment variables solely within the designated scope, and is applied in addition to (and + * after) the global collection. + * + * Each object obtained through this method is isolated and does not impact objects for other scopes, + * including the global collection. + * + * @param scope The scope to which the environment variable collection applies to. + */ + getScoped(scope: EnvironmentVariableScope): EnvironmentVariableCollection; + } } diff --git a/types/vscode.proposed.notebookReplDocument.d.ts b/types/vscode.proposed.notebookReplDocument.d.ts new file mode 100644 index 000000000000..d78450e944a8 --- /dev/null +++ b/types/vscode.proposed.notebookReplDocument.d.ts @@ -0,0 +1,33 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +declare module 'vscode' { + + export interface NotebookDocumentShowOptions { + /** + * The notebook should be opened in a REPL editor, + * where the last cell of the notebook is an input box and the other cells are the read-only history. + * When the value is a string, it will be used as the label for the editor tab. + */ + readonly asRepl?: boolean | string | { + /** + * The label to be used for the editor tab. + */ + readonly label: string; + }; + } + + export interface NotebookEditor { + /** + * Information about the REPL editor if the notebook was opened as a repl. + */ + replOptions?: { + /** + * The index where new cells should be appended. + */ + appendIndex: number; + }; + } +} diff --git a/types/vscode.proposed.notebookVariableProvider.d.ts b/types/vscode.proposed.notebookVariableProvider.d.ts new file mode 100644 index 000000000000..4fac96c45f0a --- /dev/null +++ b/types/vscode.proposed.notebookVariableProvider.d.ts @@ -0,0 +1,55 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +declare module 'vscode' { + + export interface NotebookController { + /** Set this to attach a variable provider to this controller. */ + variableProvider?: NotebookVariableProvider; + } + + export enum NotebookVariablesRequestKind { + Named = 1, + Indexed = 2 + } + + interface VariablesResult { + variable: Variable; + hasNamedChildren: boolean; + indexedChildrenCount: number; + } + + interface NotebookVariableProvider { + onDidChangeVariables: Event; + + /** When parent is undefined, this is requesting global Variables. When a variable is passed, it's requesting child props of that Variable. */ + provideVariables(notebook: NotebookDocument, parent: Variable | undefined, kind: NotebookVariablesRequestKind, start: number, token: CancellationToken): AsyncIterable; + } + + interface Variable { + /** The variable's name. */ + name: string; + + /** The variable's value. + This can be a multi-line text, e.g. for a function the body of a function. + For structured variables (which do not have a simple value), it is recommended to provide a one-line representation of the structured object. + This helps to identify the structured object in the collapsed state when its children are not yet visible. + An empty string can be used if no value should be shown in the UI. + */ + value: string; + + /** The code that represents how the variable would be accessed in the runtime environment */ + expression?: string; + + /** The type of the variable's value */ + type?: string; + + /** The interfaces or contracts that the type satisfies */ + interfaces?: string[]; + + /** The language of the variable's value */ + language?: string; + } + +} diff --git a/typings/vscode-proposed/vscode.proposed.terminalDataWriteEvent.d.ts b/typings/vscode-proposed/vscode.proposed.terminalDataWriteEvent.d.ts new file mode 100644 index 000000000000..6913b862c70f --- /dev/null +++ b/typings/vscode-proposed/vscode.proposed.terminalDataWriteEvent.d.ts @@ -0,0 +1,31 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +declare module 'vscode' { + // https://github.com/microsoft/vscode/issues/78502 + // + // This API is still proposed but we don't intent on promoting it to stable due to problems + // around performance. See #145234 for a more likely API to get stabilized. + + export interface TerminalDataWriteEvent { + /** + * The {@link Terminal} for which the data was written. + */ + readonly terminal: Terminal; + /** + * The data being written. + */ + readonly data: string; + } + + namespace window { + /** + * An event which fires when the terminal's child pseudo-device is written to (the shell). + * In other words, this provides access to the raw data stream from the process running + * within the terminal, including VT sequences. + */ + export const onDidWriteTerminalData: Event; + } +} diff --git a/typings/vscode-proposed/vscode.proposed.terminalExecuteCommandEvent.d.ts b/typings/vscode-proposed/vscode.proposed.terminalExecuteCommandEvent.d.ts new file mode 100644 index 000000000000..7f503f1aa6da --- /dev/null +++ b/typings/vscode-proposed/vscode.proposed.terminalExecuteCommandEvent.d.ts @@ -0,0 +1,45 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +declare module 'vscode' { + // https://github.com/microsoft/vscode/issues/145234 + + export interface TerminalExecutedCommand { + /** + * The {@link Terminal} the command was executed in. + */ + terminal: Terminal; + /** + * The full command line that was executed, including both the command and the arguments. + */ + commandLine: string | undefined; + /** + * The current working directory that was reported by the shell. This will be a {@link Uri} + * if the string reported by the shell can reliably be mapped to the connected machine. + */ + cwd: Uri | string | undefined; + /** + * The exit code reported by the shell. + */ + exitCode: number | undefined; + /** + * The output of the command when it has finished executing. This is the plain text shown in + * the terminal buffer and does not include raw escape sequences. Depending on the shell + * setup, this may include the command line as part of the output. + */ + output: string | undefined; + } + + export namespace window { + /** + * An event that is emitted when a terminal with shell integration activated has completed + * executing a command. + * + * Note that this event will not fire if the executed command exits the shell, listen to + * {@link onDidCloseTerminal} to handle that case. + */ + export const onDidExecuteTerminalCommand: Event; + } +}