-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": []
+ }
},
{
- "fileMatch": "meta.yaml",
- "url": "./schemas/conda-meta.json"
- }
- ],
- "views": {
- "test": [
- {
- "id": "python_tests",
- "name": "Python",
- "when": "testsDiscovered"
+ "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",
- "compile-webviews-watch": "cross-env NODE_OPTIONS=--max_old_space_size=9096 webpack --config ./build/webpack/webpack.startPage-ui-viewers.config.js --watch",
- "compile-webviews-watchd": "deemon npm run compile-webviews-watch",
- "kill-compile-webviews-watchd": "deemon --kill npm run compile-webviews-watch",
"checkDependencies": "gulp checkDependencies",
"test": "node ./out/test/standardTest.js && node ./out/test/multiRootTest.js",
"test:unittests": "mocha --config ./build/.mocha.unittests.json",
@@ -2064,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,.tsx,.js src build",
- "lint-fix": "eslint --fix --ext .ts,.tsx,.js src build gulpfile.js",
- "format-check": "prettier --check 'src/**/*.ts' 'src/**/*.tsx' 'build/**/*.js' '.github/**/*.yml' gulpfile.js",
- "format-fix": "prettier --write 'src/**/*.ts' 'src/**/*.tsx' 'build/**/*.js' '.github/**/*.yml' 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",
@@ -2075,236 +1705,109 @@
"webpack": "webpack"
},
"dependencies": {
+ "@iarna/toml": "^3.0.0",
+ "@vscode/extension-telemetry": "^0.8.4",
"arch": "^2.1.0",
- "azure-storage": "^2.10.4",
- "chokidar": "^3.4.3",
- "diff-match-patch": "^1.0.0",
- "fs-extra": "^9.1.0",
- "fuzzy": "^0.1.3",
- "get-port": "^3.2.0",
- "glob": "^7.1.2",
- "hash.js": "^1.1.7",
- "iconv-lite": "^0.4.21",
- "inversify": "^5.0.4",
- "jsonc-parser": "^2.0.3",
- "line-by-line": "^0.1.6",
- "lodash": "^4.17.21",
- "log4js": "^6.1.2",
- "md5": "^2.2.1",
- "minimatch": "^3.0.4",
+ "fs-extra": "^11.2.0",
+ "glob": "^7.2.0",
+ "iconv-lite": "^0.6.3",
+ "inversify": "^6.0.2",
+ "jsonc-parser": "^3.0.0",
+ "lodash": "^4.18.1",
+ "minimatch": "^5.1.8",
"named-js-regexp": "^1.3.3",
- "node-fetch": "^2.6.1",
"node-stream-zip": "^1.6.0",
- "portfinder": "^1.0.25",
- "reflect-metadata": "^0.1.12",
- "request": "^2.87.0",
- "request-progress": "^3.0.0",
+ "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",
- "string-argv": "^0.3.1",
- "strip-ansi": "^5.2.0",
- "sudo-prompt": "^8.2.0",
- "tmp": "^0.0.29",
- "tree-kill": "^1.2.2",
- "typescript-char": "^0.0.0",
- "uint64be": "^1.0.1",
- "unicode": "^10.0.0",
- "untildify": "^3.0.2",
- "vscode-debugadapter": "^1.28.0",
+ "sudo-prompt": "^9.2.1",
+ "tmp": "^0.2.5",
+ "uint64be": "^3.0.0",
+ "unicode": "^14.0.0",
"vscode-debugprotocol": "^1.28.0",
- "vscode-extension-telemetry": "0.1.4",
- "vscode-jsonrpc": "6.0.0",
- "vscode-languageclient": "7.0.0",
- "vscode-languageserver": "7.0.0",
- "vscode-languageserver-protocol": "3.16.0",
- "vscode-tas-client": "^0.1.22",
+ "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",
- "winston": "^3.2.1",
- "xml2js": "^0.4.19"
+ "xml2js": "^0.5.0"
},
"devDependencies": {
- "@enonic/fnv-plus": "^1.3.0",
- "@istanbuljs/nyc-config-typescript": "^0.1.3",
- "@sinonjs/fake-timers": "^6.0.1",
- "@types/ansi-regex": "^4.0.0",
+ "@istanbuljs/nyc-config-typescript": "^1.0.2",
+ "@types/bent": "^7.3.0",
"@types/chai": "^4.1.2",
- "@types/chai-arrays": "^1.0.2",
+ "@types/chai-arrays": "^2.0.0",
"@types/chai-as-promised": "^7.1.0",
- "@types/copy-webpack-plugin": "^4.4.2",
- "@types/cors": "^2.8.6",
- "@types/debug": "^4.1.5",
- "@types/dedent": "^0.7.0",
- "@types/del": "^3.0.0",
- "@types/diff-match-patch": "^1.0.32",
- "@types/download": "^6.2.2",
- "@types/enzyme": "^3.1.14",
- "@types/enzyme-adapter-react-16": "^1.0.3",
- "@types/event-stream": "^3.3.33",
- "@types/fs-extra": "^5.0.1",
- "@types/get-port": "^3.2.0",
- "@types/glob": "^5.0.35",
- "@types/html-webpack-plugin": "^3.2.0",
- "@types/iconv-lite": "^0.0.1",
- "@types/jsdom": "^11.12.0",
+ "@types/download": "^8.0.1",
+ "@types/fs-extra": "^11.0.4",
+ "@types/glob": "^7.2.0",
"@types/lodash": "^4.14.104",
- "@types/md5": "^2.1.32",
- "@types/memoize-one": "^4.1.1",
- "@types/mocha": "^5.2.7",
- "@types/nock": "^10.0.3",
- "@types/node": "^12.19.12",
- "@types/promisify-node": "^0.4.0",
- "@types/react": "^16.4.14",
- "@types/react-dom": "^16.0.8",
- "@types/react-json-tree": "^0.6.8",
- "@types/request": "^2.47.0",
+ "@types/mocha": "^9.1.0",
+ "@types/node": "^22.19.1",
"@types/semver": "^5.5.0",
"@types/shortid": "^0.0.29",
- "@types/sinon": "^7.5.1",
- "@types/sinonjs__fake-timers": "^6.0.1",
- "@types/socket.io": "^2.1.4",
+ "@types/sinon": "^17.0.3",
"@types/stack-trace": "0.0.29",
- "@types/temp": "^0.8.32",
- "@types/tmp": "0.0.33",
- "@types/untildify": "^3.0.0",
- "@types/uuid": "^3.4.3",
- "@types/vscode": "~1.53.0",
- "@types/webpack-bundle-analyzer": "^2.13.0",
+ "@types/tmp": "^0.0.33",
+ "@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",
- "acorn": "^6.4.1",
- "ansi-to-html": "^0.6.7",
- "babel-loader": "^8.0.3",
- "babel-plugin-inline-json-import": "^0.3.1",
- "babel-plugin-transform-runtime": "^6.23.0",
- "babel-polyfill": "^6.26.0",
- "cache-loader": "^4.1.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",
- "chai-http": "^4.3.0",
- "copy-webpack-plugin": "^5.1.2",
- "cors": "^2.8.5",
- "cross-env": "^6.0.3",
+ "copy-webpack-plugin": "^9.1.0",
+ "cross-env": "^7.0.3",
"cross-spawn": "^6.0.5",
- "css-loader": "^1.0.1",
- "dedent": "^0.7.0",
- "deemon": "^1.4.0",
- "del": "^3.0.0",
- "download": "^7.0.0",
- "enzyme": "^3.7.0",
- "enzyme-adapter-react-16": "^1.6.0",
- "eslint": "^7.2.0",
- "eslint-config-airbnb": "^18.2.0",
+ "del": "^6.0.0",
+ "download": "^8.0.0",
+ "eslint": "^8.57.1",
"eslint-config-prettier": "^8.3.0",
- "eslint-plugin-import": "^2.22.0",
+ "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": "^0.7.5",
- "express": "^4.17.1",
- "extract-zip": "^1.6.7",
- "fast-xml-parser": "^3.16.0",
- "file-loader": "^5.1.0",
- "filemanager-webpack-plugin-fixed": "^2.0.9",
- "flat": "^4.0.0",
- "fork-ts-checker-webpack-plugin": "^4.1.6",
- "gulp": "^4.0.0",
- "gulp-azure-storage": "^0.11.1",
- "gulp-chmod": "^2.0.0",
- "gulp-gunzip": "^1.1.0",
- "gulp-rename": "^1.4.0",
- "gulp-sourcemaps": "^2.6.4",
- "gulp-typescript": "^4.0.1",
- "gulp-untar": "0.0.8",
- "gulp-vinyl-zip": "^2.1.2",
- "html-webpack-plugin": "^3.2.0",
- "husky": "^1.1.2",
- "immutable": "^4.0.0-rc.12",
- "jsdom": "^15.0.0",
- "json-loader": "^0.5.7",
- "less": "^3.9.0",
- "less-loader": "^5.0.0",
- "less-plugin-inline-urls": "^1.2.0",
- "loader-utils": "^1.1.0",
- "lolex": "^5.1.2",
- "memoize-one": "^5.1.1",
- "mocha": "^8.1.1",
- "mocha-junit-reporter": "^1.17.0",
+ "expose-loader": "^3.1.0",
+ "flat": "^5.0.2",
+ "get-port": "^5.1.1",
+ "gulp": "^5.0.0",
+ "gulp-typescript": "^5.0.0",
+ "mocha": "^11.1.0",
+ "mocha-junit-reporter": "^2.0.2",
"mocha-multi-reporters": "^1.1.7",
- "monaco-editor": "0.18.1",
- "monaco-editor-textmate": "^2.2.1",
- "monaco-editor-webpack-plugin": "^1.7.0",
- "monaco-textmate": "^3.0.1",
- "nocache": "^2.1.0",
- "nock": "^10.0.6",
"node-has-native-dependencies": "^1.0.2",
- "node-html-parser": "^1.1.13",
"node-loader": "^1.0.2",
+ "node-polyfill-webpack-plugin": "^1.1.4",
"nyc": "^15.0.0",
- "postcss": "^8.2.10",
- "postcss-cssnext": "^3.1.0",
- "postcss-import": "^12.0.1",
- "postcss-loader": "^3.0.0",
"prettier": "^2.0.2",
- "range-inclusive": "^1.0.2",
- "raw-loader": "^0.5.1",
- "react": "^16.5.2",
- "react-dev-utils": "^11.0.4",
- "react-dom": "^16.5.2",
- "react-svgmt": "^1.1.8",
- "remove-files-webpack-plugin": "^1.4.0",
- "requirejs": "^2.3.6",
"rewiremock": "^3.13.0",
- "rimraf": "^3.0.2",
- "serialize-javascript": "^3.1.0",
"shortid": "^2.2.8",
- "sinon": "^8.0.1",
- "socket.io": "^2.4.0",
+ "sinon": "^18.0.0",
"source-map-support": "^0.5.12",
- "style-loader": "^0.23.1",
- "styled-jsx": "^3.1.0",
- "svg-inline-loader": "^0.8.0",
- "svg-inline-react": "^3.1.0",
- "terser-webpack-plugin": "^3.1.0",
- "thread-loader": "^2.1.3",
- "transform-loader": "^0.2.4",
- "ts-loader": "^5.3.0",
- "ts-mock-imports": "^1.3.0",
+ "ts-loader": "^9.2.8",
"ts-mockito": "^2.5.0",
- "ts-node": "^8.3.0",
+ "ts-node": "^10.7.0",
"tsconfig-paths-webpack-plugin": "^3.2.0",
- "typed-react-markdown": "^0.1.0",
"typemoq": "^2.1.0",
- "typescript": "4.1.5",
- "typescript-formatter": "^7.1.0",
- "unicode-properties": "^1.3.1",
- "url-loader": "^1.1.2",
- "uuid": "^3.3.2",
- "vinyl-fs": "^3.0.3",
- "vsce": "^1.59.0",
- "vscode-debugadapter-testsupport": "^1.27.0",
- "vscode-test": "^1.2.3",
- "webpack": "^4.33.0",
- "webpack-bundle-analyzer": "^3.6.0",
- "webpack-cli": "^3.1.2",
+ "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": "^4.1.4",
- "webpack-node-externals": "^1.7.2",
- "webpack-require-from": "^1.8.0",
- "why-is-node-running": "^2.0.3",
- "wtfnode": "^0.8.0",
+ "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"
- },
- "__metadata": {
- "id": "f1f59ae4-9318-4f3c-a9b5-81b2eaa5f8a5",
- "publisherDisplayName": "Microsoft",
- "publisherId": "998b010b-e2af-44a5-a6cd-0b5fd3b9b6f8"
- },
- "optionalDependencies": {
- "canvas": "^2.7.0"
}
}
diff --git a/package.nls.de.json b/package.nls.de.json
deleted file mode 100644
index 77ec14b3ee9c..000000000000
--- a/package.nls.de.json
+++ /dev/null
@@ -1,31 +0,0 @@
-{
- "python.command.python.sortImports.title": "Sortieren der Importe",
- "python.command.python.startREPL.title": "Starten des REPL",
- "python.command.python.createTerminal.title": "Terminal erstellen",
- "python.command.python.buildWorkspaceSymbols.title": "Arbeitsplatz-Symbole erstellen",
- "python.command.python.runtests.title": "Alle Unittests ausführen",
- "python.command.python.debugtests.title": "Alle Unittests debuggen",
- "python.command.python.execInTerminal.title": "Python-Datei im Terminal ausführen",
- "python.command.python.setInterpreter.title": "Interpreter auswählen",
- "python.command.python.refactorExtractVariable.title": "Variable extrahieren",
- "python.command.python.refactorExtractMethod.title": "Methode extrahieren",
- "python.command.python.viewTestOutput.title": "Unittest-Ausgabe anzeigen",
- "python.command.python.selectAndRunTestMethod.title": "Unittest-Methode ausführen ...",
- "python.command.python.selectAndDebugTestMethod.title": "Unittest-Debug-Methode ausführen ...",
- "python.command.python.selectAndRunTestFile.title": "Unittest-Datei ausführen ...",
- "python.command.python.runCurrentTestFile.title": "Ausgewählte Unittest-Datei ausführen",
- "python.command.python.runFailedTests.title": "Fehlerhafte Unittests ausführen",
- "python.command.python.discoverTests.title": "Unittests durchsuchen",
- "python.command.python.execSelectionInTerminal.title": "Selektion/Reihe in Python-Terminal ausführen",
- "python.command.python.execSelectionInDjangoShell.title": "Selektion/Reihe in Django-Shell ausführen",
- "python.command.python.goToPythonObject.title": "Gehe zu Python-Objekt",
- "python.command.python.setLinter.title": "Linter auswählen",
- "python.command.python.enableLinting.title": "Linting aktivieren",
- "python.command.python.runLinting.title": "Linting ausführen",
- "python.snippet.launch.standard.label": "Python: Aktuelle Datei",
- "python.snippet.launch.module.label": "Python: Modul",
- "python.snippet.launch.django.label": "Python: Django",
- "python.snippet.launch.flask.label": "Python: Flask",
- "python.snippet.launch.pyramid.label": "Python: Pyramid-Anwendung",
- "python.snippet.launch.attach.label": "Python: Anfügen"
-}
diff --git a/package.nls.es.json b/package.nls.es.json
deleted file mode 100644
index 227e268dbc8d..000000000000
--- a/package.nls.es.json
+++ /dev/null
@@ -1,31 +0,0 @@
-{
- "python.command.python.sortImports.title": "Ordenar importaciones",
- "python.command.python.startREPL.title": "Nuevo REPL",
- "python.command.python.createTerminal.title": "Nueva terminal",
- "python.command.python.buildWorkspaceSymbols.title": "Compilar símbolos del área de trabajo",
- "python.command.python.runtests.title": "Ejecutar todas las pruebas unitarias",
- "python.command.python.debugtests.title": "Depurar todas las pruebas unitarias",
- "python.command.python.execInTerminal.title": "Ejecutar archivo Python en la terminal",
- "python.command.python.setInterpreter.title": "Seleccionar intérprete",
- "python.command.python.refactorExtractVariable.title": "Extraer variable",
- "python.command.python.refactorExtractMethod.title": "Extraer método",
- "python.command.python.viewTestOutput.title": "Mostrar resultados de la prueba unitaria",
- "python.command.python.selectAndRunTestMethod.title": "Método de ejecución de pruebas unitarias ...",
- "python.command.python.selectAndDebugTestMethod.title": "Método de depuración de pruebas unitarias ...",
- "python.command.python.selectAndRunTestFile.title": "Ejecutar archivo de prueba unitaria ...",
- "python.command.python.runCurrentTestFile.title": "Ejecutar archivo de prueba unitaria actual",
- "python.command.python.runFailedTests.title": "Ejecutar pruebas unitarias fallidas",
- "python.command.python.discoverTests.title": "Encontrar pruebas unitarias",
- "python.command.python.execSelectionInTerminal.title": "Ejecutar línea/selección en la terminal",
- "python.command.python.execSelectionInDjangoShell.title": "Ejecutar línea/selección en el intérprete de Django",
- "python.command.python.goToPythonObject.title": "Ir al objeto de Python",
- "python.command.python.setLinter.title": "Seleccionar Linter",
- "python.command.python.enableLinting.title": "Habilitar Linting",
- "python.command.python.runLinting.title": "Ejecutar Linting",
- "python.snippet.launch.standard.label": "Python: Archivo actual",
- "python.snippet.launch.module.label": "Python: Módulo",
- "python.snippet.launch.django.label": "Python: Django",
- "python.snippet.launch.flask.label": "Python: Flask",
- "python.snippet.launch.pyramid.label": "Python: Pyramid",
- "python.snippet.launch.attach.label": "Python: Adjuntar"
-}
diff --git a/package.nls.fr.json b/package.nls.fr.json
deleted file mode 100644
index ad28a94d6e09..000000000000
--- a/package.nls.fr.json
+++ /dev/null
@@ -1,30 +0,0 @@
-{
- "python.command.python.sortImports.title": "Trier les imports",
- "python.command.python.startREPL.title": "Démarrer la console interactive",
- "python.command.python.createTerminal.title": "Créer un terminal",
- "python.command.python.buildWorkspaceSymbols.title": "Construire les symboles de l'espace de travail",
- "python.command.python.runtests.title": "Exécuter tous les tests unitaires",
- "python.command.python.debugtests.title": "Déboguer tous les tests unitaires",
- "python.command.python.execInTerminal.title": "Exécuter le script Python dans un terminal",
- "python.command.python.setInterpreter.title": "Sélectionner l'interpreteur",
- "python.command.python.refactorExtractVariable.title": "Extraire la variable",
- "python.command.python.refactorExtractMethod.title": "Extraire la méthode",
- "python.command.python.viewTestOutput.title": "Afficher la sortie des tests unitaires",
- "python.command.python.selectAndRunTestMethod.title": "Exécuter la méthode de test unitaire ...",
- "python.command.python.selectAndDebugTestMethod.title": "Déboguer la méthode de test unitaire ...",
- "python.command.python.selectAndRunTestFile.title": "Exécuter le fichier de test unitaire ...",
- "python.command.python.runCurrentTestFile.title": "Exécuter le fichier de test unitaire courant",
- "python.command.python.runFailedTests.title": "Exécuter les derniers test unitaires échoués",
- "python.command.python.execSelectionInTerminal.title": "Exécuter la ligne/sélection dans un terminal Python",
- "python.command.python.execSelectionInDjangoShell.title": "Exécuter la ligne/sélection dans un shell Django",
- "python.command.python.goToPythonObject.title": "Se rendre à l'objet Python",
- "python.command.python.setLinter.title": "Sélectionner le linter",
- "python.command.python.enableLinting.title": "Activer le linting",
- "python.command.python.runLinting.title": "Exécuter le linting",
- "python.snippet.launch.standard.label": "Python : Fichier actuel",
- "python.snippet.launch.module.label": "Python: Module",
- "python.snippet.launch.django.label": "Python : Django",
- "python.snippet.launch.flask.label": "Python : Flask",
- "python.snippet.launch.pyramid.label": "Python : application Pyramid",
- "python.snippet.launch.attach.label": "Python: Attacher"
-}
diff --git a/package.nls.it.json b/package.nls.it.json
deleted file mode 100644
index c16d6ce74241..000000000000
--- a/package.nls.it.json
+++ /dev/null
@@ -1,31 +0,0 @@
-{
- "python.command.python.sortImports.title": "Ordina gli import",
- "python.command.python.startREPL.title": "Apri nuova REPL",
- "python.command.python.createTerminal.title": "Apri nuovo terminale",
- "python.command.python.buildWorkspaceSymbols.title": "Compila simboli dello spazio di lavoro",
- "python.command.python.runtests.title": "Esegui tutti i test",
- "python.command.python.debugtests.title": "Esegui debug di tutti i test",
- "python.command.python.execInTerminal.title": "Esegui file Python nel terminale",
- "python.command.python.setInterpreter.title": "Seleziona interprete",
- "python.command.python.refactorExtractVariable.title": "Estrai variable",
- "python.command.python.refactorExtractMethod.title": "Estrai metodo",
- "python.command.python.viewTestOutput.title": "Mostra output dei test",
- "python.command.python.selectAndRunTestMethod.title": "Esegui metodo di test ...",
- "python.command.python.selectAndDebugTestMethod.title": "Esegui debug del metodo di test ...",
- "python.command.python.selectAndRunTestFile.title": "Esegui file di test ...",
- "python.command.python.runCurrentTestFile.title": "Esegui file di test attuale",
- "python.command.python.runFailedTests.title": "Esegui test falliti",
- "python.command.python.execSelectionInTerminal.title": "Esegui selezione/linea nel terminale di Python",
- "python.command.python.execSelectionInDjangoShell.title": "Esegui selezione/linea nella shell Django",
- "python.command.python.goToPythonObject.title": "Vai a oggetto Python",
- "python.command.python.setLinter.title": "Selezione Linter",
- "python.command.python.enableLinting.title": "Attiva Linting",
- "python.command.python.runLinting.title": "Esegui Linting",
- "python.snippet.launch.standard.label": "Python: File corrente",
- "python.snippet.launch.module.label": "Python: Modulo",
- "python.snippet.launch.django.label": "Python: Django",
- "python.snippet.launch.flask.label": "Python: Flask",
- "python.snippet.launch.pyramid.label": "Python: Applicazione Pyramid",
- "python.snippet.launch.attach.label": "Python: Allega",
- "ExtensionSurveyBanner.bannerLabelYes": "Sì, prenderò il sondaggio ora"
-}
diff --git a/package.nls.ja.json b/package.nls.ja.json
deleted file mode 100644
index a2db0a823636..000000000000
--- a/package.nls.ja.json
+++ /dev/null
@@ -1,26 +0,0 @@
-{
- "python.command.python.sortImports.title": "import 文を並び替える",
- "python.command.python.startREPL.title": "REPL を開始",
- "python.command.python.buildWorkspaceSymbols.title": "ワークスペースのシンボルをビルド",
- "python.command.python.runtests.title": "すべての単体テストを実行",
- "python.command.python.debugtests.title": "すべての単体テストをデバッグ",
- "python.command.python.execInTerminal.title": "ターミナルで Python ファイルを実行",
- "python.command.python.setInterpreter.title": "インタープリターを選択",
- "python.command.python.refactorExtractVariable.title": "変数を抽出",
- "python.command.python.refactorExtractMethod.title": "メソッドを抽出",
- "python.command.python.viewTestOutput.title": "単体テストの出力を表示",
- "python.command.python.selectAndRunTestMethod.title": "単体テストメソッドを実行...",
- "python.command.python.selectAndDebugTestMethod.title": "単体テストメソッドをデバッグ...",
- "python.command.python.selectAndRunTestFile.title": "単体テストファイルを実行...",
- "python.command.python.runCurrentTestFile.title": "現在の単体テストファイルを実行",
- "python.command.python.runFailedTests.title": "失敗した単体テストを実行",
- "python.command.python.execSelectionInTerminal.title": "Python ターミナルで選択範囲/行を実行",
- "python.command.python.execSelectionInDjangoShell.title": "Django シェルで選択範囲/行を実行",
- "python.command.python.goToPythonObject.title": "Python オブジェクトに移動",
- "python.snippet.launch.standard.label": "Python: Current File",
- "python.snippet.launch.module.label": "Python: モジュール",
- "python.snippet.launch.django.label": "Python: Django",
- "python.snippet.launch.flask.label": "Python: Flask",
- "python.snippet.launch.pyramid.label": "Python: Pyramid アプリケーション",
- "python.snippet.launch.attach.label": "Python: アタッチ"
-}
diff --git a/package.nls.json b/package.nls.json
index 96ee3df8c1e9..57f2ed95b2c0 100644
--- a/package.nls.json
+++ b/package.nls.json
@@ -1,255 +1,177 @@
{
- "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.buildWorkspaceSymbols.title": "Build Workspace Symbols",
- "python.command.python.runtests.title": "Run All Tests",
- "python.command.python.debugtests.title": "Debug All Tests",
"python.command.python.execInTerminal.title": "Run Python File in Terminal",
+ "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.switchOffInsidersChannel.title": "Switch to Default Channel",
- "python.command.python.switchToDailyChannel.title": "Switch to Insiders Daily Channel",
- "python.command.python.switchToWeeklyChannel.title": "Switch to Insiders Weekly Channel",
"python.command.python.clearWorkspaceInterpreter.title": "Clear Workspace Interpreter Setting",
- "python.command.python.refactorExtractVariable.title": "Extract Variable",
- "python.command.python.refactorExtractMethod.title": "Extract Method",
"python.command.python.viewOutput.title": "Show Output",
- "python.command.python.viewTestOutput.title": "Show Test Output",
+ "python.command.python.installJupyter.title": "Install the Jupyter extension",
"python.command.python.viewLanguageServerOutput.title": "Show Language Server Output",
- "python.command.python.selectAndRunTestMethod.title": "Run Test Method ...",
- "python.command.python.selectAndDebugTestMethod.title": "Debug Test Method ...",
- "python.command.python.selectAndRunTestFile.title": "Run Test File ...",
- "python.command.python.runCurrentTestFile.title": "Run Current Test File",
- "python.command.python.runFailedTests.title": "Run Failed Tests",
- "python.command.python.discoverTests.title": "Discover Tests",
- "python.command.python.discoveringTests.title": "Discovering...",
- "python.command.python.stopTests.title": "Stop",
"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.goToPythonObject.title": "Go to Python Object",
- "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.clearPersistentStorage.title": "Clear Internal Extension Cache",
- "python.command.python.startPage.open.title": "Open Start Page",
- "python.command.python.analysis.clearCache.title": "Clear Module Analysis Cache",
+ "python.command.python.reportIssue.title": "Report Issue...",
+ "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.snippet.launch.standard.label": "Python: Current File",
- "python.snippet.launch.module.label": "Python: Module",
- "python.snippet.launch.module.default": "enter-your-module-name",
- "python.snippet.launch.attach.label": "Python: Remote Attach",
- "python.snippet.launch.attachpid.label": "Python: Attach using Process Id",
- "python.snippet.launch.django.label": "Python: Django",
- "python.snippet.launch.fastapi.label": "Python: FastAPI",
- "python.snippet.launch.flask.label": "Python: Flask",
- "python.snippet.launch.pyramid.label": "Python: Pyramid Application",
- "Pylance.remindMeLater": "Remind me later",
- "Pylance.pylanceNotInstalledMessage": "Pylance extension is not installed.",
- "Pylance.pylanceInstalledReloadPromptMessage": "Pylance extension is now installed. Reload window to activate?",
- "Pylance.pylanceRevertToJediPrompt": "The Pylance extension is not installed but the python.languageServer value is set to \"Pylance\". Would you like to install the Pylance extension to use Pylance, or revert back to Jedi?",
- "Pylance.pylanceInstallPylance": "Install Pylance",
- "Pylance.pylanceRevertToJedi": "Revert to Jedi",
- "Experiments.inGroup": "User belongs to experiment group '{0}'",
- "Experiments.optedOutOf": "User opted out of experiment group '{0}'",
- "Interpreters.RefreshingInterpreters": "Refreshing Python Interpreters",
- "Interpreters.entireWorkspace": "Entire workspace",
- "Interpreters.pythonInterpreterPath": "Python interpreter path: {0}",
- "Interpreters.LoadingInterpreters": "Loading Python Interpreters",
- "Interpreters.condaInheritEnvMessage": "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.",
- "Logging.CurrentWorkingDirectory": "cwd:",
- "InterpreterQuickPickList.quickPickListPlaceholder": "Current: {0}",
- "InterpreterQuickPickList.enterPath.detail": "Enter path or find an existing interpreter",
- "InterpreterQuickPickList.enterPath.label": "Enter interpreter path...",
- "InterpreterQuickPickList.enterPath.placeholder": "Enter path to a Python interpreter.",
- "InterpreterQuickPickList.findPath.detail": "Browse the file system to find a Python interpreter.",
- "InterpreterQuickPickList.findPath.label": "I can't find the interpreter I want to select...",
- "InterpreterQuickPickList.refreshInterpreterList": "Refresh Interpreter list",
- "InterpreterQuickPickList.browsePath.label": "Find...",
- "InterpreterQuickPickList.browsePath.detail": "Browse your file system to find a Python interpreter.",
- "InterpreterQuickPickList.browsePath.title": "Select Python interpreter",
- "InterpreterQuickPickList.defaultInterpreterPath.label": "Use default Python interpreter path",
- "diagnostics.upgradeCodeRunner": "Please update the Code Runner extension for it to be compatible with the Python extension.",
- "Common.bannerLabelYes": "Yes",
- "Common.bannerLabelNo": "No",
- "Common.doNotShowAgain": "Do not show again",
- "Common.reload": "Reload",
- "Common.moreInfo": "More Info",
- "Common.and": "and",
- "Common.ok": "Ok",
- "Common.install": "Install",
- "Common.learnMore": "Learn more",
- "Common.reportThisIssue": "Report this issue",
- "CommonSurvey.remindMeLaterLabel": "Remind me later",
- "CommonSurvey.yesLabel": "Yes, take survey now",
- "CommonSurvey.noLabel": "No, thanks",
- "OutputChannelNames.languageServer": "Python Language Server",
- "OutputChannelNames.python": "Python",
- "OutputChannelNames.pythonTest": "Python Test Log",
- "ExtensionSurveyBanner.bannerMessage": "Can you please take 2 minutes to tell us how the Python extension is working for you?",
- "ExtensionSurveyBanner.bannerLabelYes": "Yes, take survey now",
- "ExtensionSurveyBanner.bannerLabelNo": "No, thanks",
- "ExtensionSurveyBanner.maybeLater": "Maybe later",
- "ExtensionSurveyBanner.mplsMessage": "Can you please take 2 minutes to tell us about your experience using the Microsoft Python Language Server?",
- "ExtensionChannels.installingInsidersMessage": "Installing Insiders... ",
- "ExtensionChannels.installingStableMessage": "Installing Stable... ",
- "ExtensionChannels.installationCompleteMessage": "complete.",
- "ExtensionChannels.downloadingInsidersMessage": "Downloading Insiders Extension... ",
- "ExtensionChannels.yesWeekly": "Yes, weekly",
- "ExtensionChannels.yesDaily": "Yes, daily",
- "ExtensionChannels.promptMessage": "We noticed you are using Visual Studio Code Insiders. Would you like to use the Insiders build of the Python extension?",
- "ExtensionChannels.reloadToUseInsidersMessage": "Please reload Visual Studio Code to use the insiders build of the Python extension.",
- "ExtensionChannels.downloadCompletedOutputMessage": "Insiders build download complete.",
- "ExtensionChannels.startingDownloadOutputMessage": "Starting download for Insiders build.",
- "Interpreters.environmentPromptMessage": "We noticed a new virtual environment has been created. Do you want to select it for the workspace folder?",
- "Linter.enableLinter": "Enable {0}",
- "Linter.enablePylint": "You have a pylintrc file in your workspace. Do you want to enable pylint?",
- "Linter.replaceWithSelectedLinter": "Multiple linters are enabled in settings. Replace with '{0}'?",
- "Linter.install": "Install a linter to get error reporting.",
- "Linter.installPylint": "Install pylint",
- "Linter.installFlake8": "Install flake8",
- "Linter.selectLinter": "Select Linter",
- "Installer.noCondaOrPipInstaller": "There is no Conda or Pip installer available in the selected environment.",
- "Installer.noPipInstaller": "There is no Pip installer available in the selected environment.",
- "Installer.searchForHelp": "Search for help",
- "Installer.couldNotInstallLibrary": "Could not install {0}. If pip is not available, please use the package manager of your choice to manually install this library into your Python environment.",
- "Installer.dataScienceInstallPrompt": "Data Science library {0} is not installed. Install?",
- "diagnostics.removedPythonPathFromSettings": "The \"python.pythonPath\" setting in your settings.json is no longer used by the Python extension. If you want, you can use a new setting called \"python.defaultInterpreterPath\" instead. Keep in mind that you need to change the value of this setting manually as the Python extension doesn’t modify it when you change interpreters. [Learn more](https://aka.ms/AA7jfor).",
- "diagnostics.warnSourceMaps": "Source map support is enabled in the Python Extension, this will adversely impact performance of the extension.",
- "diagnostics.disableSourceMaps": "Disable Source Map Support",
- "diagnostics.warnBeforeEnablingSourceMaps": "Enabling source map support in the Python Extension will adversely impact performance of the extension.",
- "diagnostics.enableSourceMapsAndReloadVSC": "Enable and reload Window",
- "diagnostics.lsNotSupported": "Your operating system does not meet the minimum requirements of the Python Language Server. Reverting to the alternative autocompletion provider, Jedi.",
- "diagnostics.invalidPythonPathInDebuggerSettings": "You need to select a Python interpreter before you start debugging.\n\nTip: click on \"Select Python Interpreter\" in the status bar.",
- "diagnostics.invalidPythonPathInDebuggerLaunch": "The Python path in your debug configuration is invalid.",
- "diagnostics.invalidDebuggerTypeDiagnostic": "Your launch.json file needs to be updated to change the \"pythonExperimental\" debug configurations to use the \"python\" debugger type, otherwise Python debugging may not work. Would you like to automatically update your launch.json file now?",
- "diagnostics.consoleTypeDiagnostic": "Your launch.json file needs to be updated to change the console type string from \"none\" to \"internalConsole\", otherwise Python debugging may not work. Would you like to automatically update your launch.json file now?",
- "diagnostics.justMyCodeDiagnostic": "Configuration \"debugStdLib\" in launch.json is no longer supported. It's recommended to replace it with \"justMyCode\", which is the exact opposite of using \"debugStdLib\". Would you like to automatically update your launch.json file to do that?",
- "diagnostics.checkIsort5UpgradeGuide": "We found outdated configuration for sorting imports in this workspace. Check the [isort upgrade guide](https://aka.ms/AA9j5x4) to update your settings.",
- "diagnostics.yesUpdateLaunch": "Yes, update launch.json",
- "diagnostics.invalidTestSettings": "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?",
- "diagnostics.pylanceDefaultMessage": "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).",
- "Common.canceled": "Canceled",
- "Common.cancel": "Cancel",
- "Common.yesPlease": "Yes, please",
- "Common.loadingPythonExtension": "Python extension loading...",
- "debug.selectConfigurationTitle": "Select a debug configuration",
- "debug.selectConfigurationPlaceholder": "Debug Configuration",
- "debug.launchJsonConfigurationsCompletionLabel": "Python",
- "debug.launchJsonConfigurationsCompletionDescription": "Select a Python debug configuration",
- "debug.debugFileConfigurationLabel": "Python File",
- "debug.debugFileConfigurationDescription": "Debug the currently active Python file",
- "debug.debugModuleConfigurationLabel": "Module",
- "debug.debugModuleConfigurationDescription": "Debug a Python module by invoking it with '-m'",
- "debug.moduleEnterModuleTitle": "Debug Module",
- "debug.moduleEnterModulePrompt": "Enter a Python module/package name",
- "debug.moduleEnterModuleDefault": "enter-your-module-name",
- "debug.moduleEnterModuleInvalidNameError": "Enter a valid module name",
- "debug.remoteAttachConfigurationLabel": "Remote Attach",
- "debug.remoteAttachConfigurationDescription": "Attach to a remote debug server",
- "debug.attachRemoteHostTitle": "Remote Debugging",
- "debug.attachRemoteHostPrompt": "Enter the host name",
- "debug.attachRemoteHostValidationError": "Enter a valid host name or IP address",
- "debug.attachRemotePortTitle": "Remote Debugging",
- "debug.attachRemotePortPrompt": "Enter the port number that the debug server is listening on",
- "debug.attachRemotePortValidationError": "Enter a valid port number",
- "debug.attachPidConfigurationLabel": "Attach using Process ID",
- "debug.attachPidConfigurationDescription": "Attach to a local process",
- "debug.debugDjangoConfigurationLabel": "Django",
- "debug.debugDjangoConfigurationDescription": "Launch and debug a Django web application",
- "debug.djangoEnterManagePyPathTitle": "Debug Django",
- "debug.djangoEnterManagePyPathPrompt": "Enter the path to manage.py ('${workspaceFolderToken}' points to the root of the current workspace folder)",
- "debug.djangoEnterManagePyPathInvalidFilePathError": "Enter a valid Python file path",
- "debug.debugFastAPIConfigurationLabel": "FastAPI",
- "debug.debugFastAPIConfigurationDescription": "Launch and debug a FastAPI web application",
- "debug.fastapiEnterAppPathOrNamePathTitle": "Debug FastAPI",
- "debug.fastapiEnterAppPathOrNamePathPrompt": "Enter the path to the application, e.g. 'main.py' or 'main'",
- "debug.fastapiEnterAppPathOrNamePathInvalidNameError": "Enter a valid name",
- "debug.debugFlaskConfigurationLabel": "Flask",
- "debug.debugFlaskConfigurationDescription": "Launch and debug a Flask web application",
- "debug.flaskEnterAppPathOrNamePathTitle": "Debug Flask",
- "debug.flaskEnterAppPathOrNamePathPrompt": "Enter the path to the application, e.g. 'app.py' or 'app'",
- "debug.flaskEnterAppPathOrNamePathInvalidNameError": "Enter a valid name",
- "debug.debugPyramidConfigurationLabel": "Pyramid",
- "debug.debugPyramidConfigurationDescription": "Launch and debug a Pyramid web application",
- "debug.pyramidEnterDevelopmentIniPathTitle": "Debug Pyramid",
- "debug.pyramidEnterDevelopmentIniPathPrompt": "`Enter the path to development.ini ('${workspaceFolderToken}' points to the root of the current workspace folder)`",
- "debug.pyramidEnterDevelopmentIniPathInvalidFilePathError": "Enter a valid file path",
- "Testing.testErrorDiagnosticMessage": "Error",
- "Testing.testFailDiagnosticMessage": "Fail",
- "Testing.testSkippedDiagnosticMessage": "Skipped",
- "Testing.configureTests": "Configure Test Framework",
- "Testing.disableTests": "Disable Tests",
- "Common.openOutputPanel": "Show output",
- "LanguageService.lsFailedToStart": "We encountered an issue starting the language server. Reverting to Jedi language engine. Check the Python output panel for details.",
- "LanguageService.lsFailedToDownload": "We encountered an issue downloading the language server. Reverting to Jedi language engine. Check the Python output panel for details.",
- "LanguageService.lsFailedToExtract": "We encountered an issue extracting the language server. Reverting to Jedi language engine. Check the Python output panel for details.",
- "LanguageService.downloadFailedOutputMessage": "Language server download failed",
- "LanguageService.extractionFailedOutputMessage": "Language server extraction failed",
- "LanguageService.extractionCompletedOutputMessage": "Language server download complete",
- "LanguageService.extractionDoneOutputMessage": "done",
- "LanguageService.reloadVSCodeIfSeachPathHasChanged": "Search paths have changed for this Python interpreter. Please reload the extension to ensure that the IntelliSense works correctly",
- "LanguageService.startingJedi": "Starting Jedi Python language engine.",
- "LanguageService.startingMicrosoft": "Starting Microsoft Python language server.",
- "LanguageService.startingPylance": "Starting Pylance language server.",
- "LanguageService.startingJediLSP": "Starting Jedi language server.",
- "LanguageService.startingNone": "Editor support is inactive since language server is set to None.",
- "LanguageService.reloadAfterLanguageServerChange": "Please reload the window switching between language servers.",
- "AttachProcess.unsupportedOS": "Operating system '{0}' not supported.",
- "AttachProcess.attachTitle": "Attach to process",
- "AttachProcess.selectProcessPlaceholder": "Select the process to attach to",
- "AttachProcess.noProcessSelected": "No process selected",
- "AttachProcess.refreshList": "Refresh process list",
- "diagnostics.updateSettings": "Yes, update settings",
- "Common.noIWillDoItLater": "No, I will do it later",
- "Common.notNow": "Not now",
- "Common.gotIt": "Got it!",
- "Interpreters.selectInterpreterTip": "Tip: you can change the Python interpreter used by the Python extension by clicking on the Python version in the status bar",
- "downloading.file": "Downloading {0}...",
- "downloading.file.progress": "{0}{1} of {2} KB ({3}%)",
- "products.installingModule": "Installing {0}",
- "OutdatedDebugger.updateDebuggerMessage": "We noticed you are attaching to ptvsd (Python debugger), which was deprecated on May 1st, 2020. Please switch to [debugpy](https://aka.ms/migrateToDebugpy).",
- "StartPage.getStarted": "Python - Get Started",
- "StartPage.pythonExtensionTitle": "Python Extension",
- "StartPage.createJupyterNotebook": "Create a Jupyter Notebook",
- "StartPage.notebookDescription": "- Run \"Create New Blank Notebook
\" in the Command Palette (Shift + Command + P
) - Explore our sample notebook
to learn about notebook features",
- "StartPage.createAPythonFile": "Create a Python File",
- "StartPage.pythonFileDescription": "- Create a new file
with a .py extension",
- "StartPage.openInteractiveWindow": "Use the Interactive Window to develop Python Scripts",
- "StartPage.interactiveWindowDesc": "- You can create cells on a Python file by typing \"#%%\". Make sure you have the Jupyter extension installed. - Use \"Shift + Enter
\" to run a cell, the output will be shown in the interactive window",
- "StartPage.releaseNotes": "Take a look at our Release Notes to learn more about the latest features.",
- "StartPage.mailingList": "Sign up for tips and tutorials through our mailing list.",
- "StartPage.tutorialAndDoc": "Explore more features in our Tutorials or check Documentation for tips and troubleshooting.",
- "StartPage.dontShowAgain": "Don't show this page again",
- "StartPage.helloWorld": "Hello world",
- "StartPage.sampleNotebook": "Notebooks intro",
- "StartPage.openFolder": "Open a Folder or Workspace",
- "StartPage.folderDesc": "- Open a Folder
- Open a Workspace
",
- "StartPage.badWebPanelFormatString": "{0} is not a valid file name ",
- "Jupyter.extensionRequired": "The Jupyter extension is required to perform that task. Click Yes to open the Jupyter extension installation page.",
- "Jupyter.extensionNotInstalled": "This feature is available in the Jupyter extension, which isn't currently installed.",
- "TensorBoard.missingSourceFile": "We could not locate the requested source file on disk. Please manually specify the file.",
- "TensorBoard.selectMissingSourceFile": "Choose File",
- "TensorBoard.selectMissingSourceFileDescription": "The source file's contents may not match the original contents in the trace.",
- "TensorBoard.useCurrentWorkingDirectory": "Use current working directory",
- "TensorBoard.currentDirectory": "Current: {0}",
- "TensorBoard.logDirectoryPrompt": "Select a log directory to start TensorBoard with",
- "TensorBoard.progressMessage": "Starting TensorBoard session...",
- "TensorBoard.failedToStartSessionError": "We failed to start a TensorBoard session due to the following error: {0}",
- "TensorBoard.nativeTensorBoardPrompt": "VS Code now has integrated TensorBoard support. Would you like to launch TensorBoard? (Tip: Launch TensorBoard anytime by opening the command palette and searching for \"Launch TensorBoard\".)",
- "TensorBoard.selectAFolder": "Select a folder",
- "TensorBoard.selectAnotherFolder": "Select another folder",
- "TensorBoard.selectAFolderDetail": "Select a log directory containing tfevent files",
- "TensorBoard.selectAnotherFolderDetail": "Use the file explorer to select another folder",
- "TensorBoard.useCurrentWorkingDirectoryDetail": "TensorBoard will search for tfevent files in all subdirectories of the current working directory",
- "TensorBoard.installPrompt": "The package TensorBoard is required to launch a TensorBoard session. Would you like to install it?",
- "TensorBoard.installTensorBoardAndProfilerPluginPrompt": "TensorBoard >= 2.4.1 and the PyTorch Profiler TensorBoard Plugin are required. Would you like to install these packages?",
- "TensorBoard.installProfilerPluginPrompt": "We recommend installing the PyTorch Profiler TensorBoard plugin. Would you like to install the package?",
- "TensorBoard.upgradePrompt": "Integrated TensorBoard support is only available for TensorBoard >= 2.4.1. Would you like to upgrade your copy of TensorBoard?",
- "TensorBoard.launchNativeTensorBoardSessionCodeAction": "Launch TensorBoard session",
- "TensorBoard.launchNativeTensorBoardSessionCodeLens": "▶ Launch TensorBoard Session"
+ "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.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 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.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.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.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.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. 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).",
+ "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.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": {
+ "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": {
+ "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.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": "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",
+ "walkthrough.step.python.installJupyterExt.description": "If you haven't already, install the [Jupyter extension](command:workbench.extensions.search?\"ms-toolsai.jupyter\") to take full advantage of notebooks experiences in VS Code!\n \n[Search Jupyter extension](command:workbench.extensions.search?\"ms-toolsai.jupyter\")",
+ "walkthrough.step.python.createNewNotebook.title": "Create or open a Jupyter Notebook",
+ "walkthrough.step.python.createNewNotebook.description": "Right click in the file explorer and create a new file with an .ipynb extension. Or, open the [Command Palette](command:workbench.action.showCommands) and run the command \n``Jupyter: Create New Blank Notebook``.\n[Create new Jupyter Notebook](command:toSide:jupyter.createnewnotebook)\n If you have an existing project, you can also [open a folder](command:workbench.action.files.openFolder) and/or clone a project from GitHub: [clone a Git repository](command:git.clone).",
+ "walkthrough.step.python.openInteractiveWindow.title": "Open the Python Interactive Window",
+ "walkthrough.step.python.openInteractiveWindow.description": "The Python Interactive Window is a Python shell where you can execute and view the results of your Python code. You can create cells on a Python file by typing ``#%%``.\n \nTo open the interactive window anytime, open the [Command Palette](command:workbench.action.showCommands) and run the command \n``Jupyter: Create Interactive Window``.\n[Open Interactive Window](command:jupyter.createnewinteractive)",
+ "walkthrough.step.python.dataScienceLearnMore.title": "Find out more!",
+ "walkthrough.step.python.dataScienceLearnMore.description": "📒 Take a look into the [Jupyter extension](command:workbench.extensions.search?\"ms-toolsai.jupyter\") features, by looking for \"Jupyter\" in the [Command Palette](command:workbench.action.showCommands). \n 🏃🏻 Find out more features in our [Tutorials](https://aka.ms/AAdjzpd). \n[Learn more](https://aka.ms/AAdar6q)",
+ "walkthrough.step.python.createPythonFile.altText": "Open a Python file or a folder with a Python project.",
+ "walkthrough.step.python.selectInterpreter.altText": "Selecting a Python interpreter from the status bar",
+ "walkthrough.step.python.createEnvironment.altText": "Creating a Python environment from the Command Palette",
+ "walkthrough.step.python.runAndDebug.altText": "How to run and debug in VS Code with F5 or the play button on the top right.",
+ "walkthrough.step.python.learnMoreWithDS.altText": "Image representing our documentation page and mailing list resources.",
+ "walkthrough.step.python.installJupyterExt.altText": "Creating a new Jupyter notebook",
+ "walkthrough.step.python.createNewNotebook.altText": "Creating a new Jupyter notebook",
+ "walkthrough.step.python.openInteractiveWindow.altText": "Opening Python interactive window",
+ "walkthrough.step.python.dataScienceLearnMore.altText": "Image representing our documentation page and mailing list resources."
}
diff --git a/package.nls.ko-kr.json b/package.nls.ko-kr.json
deleted file mode 100644
index 5309a9b07b81..000000000000
--- a/package.nls.ko-kr.json
+++ /dev/null
@@ -1,26 +0,0 @@
-{
- "python.command.python.sortImports.title": "Import문 정렬",
- "python.command.python.startREPL.title": "REPL 시작",
- "python.command.python.buildWorkspaceSymbols.title": "작업 영역 기호 빌드",
- "python.command.python.runtests.title": "모든 단위 테스트 실행",
- "python.command.python.debugtests.title": "모든 단위 테스트 디버그",
- "python.command.python.execInTerminal.title": "터미널에서 Python 파일 실행",
- "python.command.python.setInterpreter.title": "인터프리터 선택",
- "python.command.python.refactorExtractVariable.title": "변수 추출",
- "python.command.python.refactorExtractMethod.title": "메서드 추출",
- "python.command.python.viewTestOutput.title": "단위 테스트 결과 보기",
- "python.command.python.selectAndRunTestMethod.title": "단위 테스트 메서드 실행 ...",
- "python.command.python.selectAndDebugTestMethod.title": "단위 테스트 메서드 디버그 ...",
- "python.command.python.selectAndRunTestFile.title": "단위 테스트 파일 실행 ...",
- "python.command.python.runCurrentTestFile.title": "현재 단위 테스트 파일 실행",
- "python.command.python.runFailedTests.title": "실패한 단위 테스트 실행",
- "python.command.python.execSelectionInTerminal.title": "Python 터미널에서 선택 영역/줄 실행",
- "python.command.python.execSelectionInDjangoShell.title": "Django 셸에서 선택 영역/줄 실행",
- "python.command.python.goToPythonObject.title": " Python 객체로 이동",
- "python.snippet.launch.standard.label": "Python: Current File",
- "python.snippet.launch.module.label": "Python: 모듈",
- "python.snippet.launch.django.label": "Python: Django",
- "python.snippet.launch.flask.label": "Python: Flask",
- "python.snippet.launch.pyramid.label": "Python: Pyramid 응용 프로그램",
- "python.snippet.launch.attach.label": "Python: 연결"
-}
diff --git a/package.nls.nl.json b/package.nls.nl.json
deleted file mode 100644
index 08c9534dc962..000000000000
--- a/package.nls.nl.json
+++ /dev/null
@@ -1,83 +0,0 @@
-{
- "python.command.python.sortImports.title": "Import sorteren",
- "python.command.python.startREPL.title": "REPL starten",
- "python.command.python.createTerminal.title": "Terminal aanmaken",
- "python.command.python.buildWorkspaceSymbols.title": "Werkruimte-symbolen aanmaken",
- "python.command.python.runtests.title": "Alle unittests uitvoeren",
- "python.command.python.debugtests.title": "Alle unittests debuggen",
- "python.command.python.execInTerminal.title": "Python-bestand in terminal uitvoeren",
- "python.command.python.setInterpreter.title": "Interpreter selecteren",
- "python.command.python.refactorExtractVariable.title": "Variabelen selecteren",
- "python.command.python.refactorExtractMethod.title": "Methode selecteren",
- "python.command.python.viewTestOutput.title": "Unittest-resultaat laten zien",
- "python.command.python.selectAndRunTestMethod.title": "Unittest-methode uitvoeren ...",
- "python.command.python.selectAndDebugTestMethod.title": "Unittest-methode debuggen ...",
- "python.command.python.selectAndRunTestFile.title": "Unittest-bestand uitvoeren ...",
- "python.command.python.runCurrentTestFile.title": "Huidige unittest-bestand uitvoeren",
- "python.command.python.runFailedTests.title": "Gefaalde unittests uitvoeren",
- "python.command.python.discoverTests.title": "Unittests doorzoeken",
- "python.command.python.execSelectionInTerminal.title": "Selectie/rij in Python-terminal uitvoeren",
- "python.command.python.execSelectionInDjangoShell.title": "Selectie/rij in Django-shell uitvoeren",
- "python.command.python.goToPythonObject.title": "Naar Python-object gaan",
- "python.command.python.setLinter.title": "Linter selecteren",
- "python.command.python.enableLinting.title": "Linting activeren",
- "python.command.python.runLinting.title": "Linting uitvoeren",
- "python.command.python.enableSourceMapSupport.title": "Bronkaartondersteuning voor extensie-debugging inschakelen",
- "python.snippet.launch.standard.label": "Python: Huidige bestand",
- "python.snippet.launch.module.label": "Python: Module",
- "python.snippet.launch.django.label": "Python: Django",
- "python.snippet.launch.flask.label": "Python: Flask",
- "python.snippet.launch.pyramid.label": "Python: Pyramid-applicatie",
- "python.snippet.launch.attach.label": "Python: aankoppelen",
- "ExtensionSurveyBanner.bannerLabelYes": "Ja, neem nu deel aan het onderzoek",
- "ExtensionSurveyBanner.bannerLabelNo": "Nee, bedankt",
- "LanguageService.lsFailedToStart": "We zijn een probleem tegengekomen bij het starten van de language server. Aan het terugschakelen naar het alternatief, Jedi. Bekijk het weergavepaneel voor details.",
- "LanguageService.lsFailedToDownload": "We zijn een probleem tegengekomen bij het downloaden van de language server. Aan het terugschakelen naar het alternatief, Jedi. Bekijk het weergavepaneel voor details.",
- "LanguageService.lsFailedToExtract": "We zijn een probleem tegengekomen bij het uitpakken van de language server. Aan het terugschakelen naar het alternatief, Jedi. Bekijk het weergavepaneel voor details.",
- "Interpreters.RefreshingInterpreters": "Python-Interpreters verversen",
- "Interpreters.LoadingInterpreters": "Python-Interpreters laden",
- "Linter.InstalledButNotEnabled": "Linter {0} is geinstalleerd maar niet ingeschakeld.",
- "Linter.replaceWithSelectedLinter": "Meerdere linters zijn ingeschakeld in de instellingen. Vervangen met '{0}'?",
- "diagnostics.warnSourceMaps": "Bronkaartondersteuning is ingeschakeld in de Python-extensie, dit zal een ongunstige impact hebben op de uitvoering van de extensie.",
- "diagnostics.disableSourceMaps": "Bronkaartondersteuning uitschakelen",
- "diagnostics.warnBeforeEnablingSourceMaps": "Bronkaartondersteuning inschakelen in de Python-extensie zal een ongunstige impact hebben op de uitvoering van de extensie.",
- "diagnostics.enableSourceMapsAndReloadVSC": "Venster inschakelen en herladen",
- "diagnostics.lsNotSupported": "Uw besturingssysteem voldoet niet aan de minimumeisen van de language server. Aan het terugschakelen naar het alternatief, Jedi.",
- "Common.canceled": "Geannuleerd",
- "Common.loadingPythonExtension": "Python-extensie aan het laden...",
- "debug.selectConfigurationTitle": "Een debug-configuratie selecteren",
- "debug.selectConfigurationPlaceholder": "Debug-configuratie",
- "debug.debugFileConfigurationLabel": "Python-bestand",
- "debug.debugFileConfigurationDescription": "Python-bestand debuggen",
- "debug.debugModuleConfigurationLabel": "Module",
- "debug.debugModuleConfigurationDescription": "Python module/package debuggen",
- "debug.remoteAttachConfigurationLabel": "Extern aankoppelen",
- "debug.remoteAttachConfigurationDescription": "Een externe Python-applicatie debuggen",
- "debug.debugDjangoConfigurationLabel": "Django",
- "debug.debugDjangoConfigurationDescription": "Web-applicatie",
- "debug.debugFlaskConfigurationLabel": "Flask",
- "debug.debugFlaskConfigurationDescription": "Web-applicatie",
- "debug.debugPyramidConfigurationLabel": "Pyramid",
- "debug.debugPyramidConfigurationDescription": "Web-applicatie",
- "debug.djangoEnterManagePyPathTitle": "Django debuggen",
- "debug.djangoEnterManagePyPathPrompt": "Voer een pad in naar manage.py ('${workspaceFolderToken}' verwijzen naar de root van de huidige werkruimtemap)",
- "debug.djangoEnterManagePyPathInvalidFilePathError": "Voer een geldig Python-bestandspad in",
- "debug.flaskEnterAppPathOrNamePathTitle": "Flask debuggen",
- "debug.flaskEnterAppPathOrNamePathPrompt": "Voer een pad in naar een applicatie, bijvoorbeeld 'app.py' of 'app'",
- "debug.flaskEnterAppPathOrNamePathInvalidNameError": "Voer een geldige naam in",
- "debug.moduleEnterModuleTitle": "Module debuggen",
- "debug.moduleEnterModulePrompt": "Voer Python module/package naam in",
- "debug.moduleEnterModuleInvalidNameError": "Voer een geldige naam in",
- "debug.pyramidEnterDevelopmentIniPathTitle": "Pyramid debuggen",
- "debug.pyramidEnterDevelopmentIniPathPrompt": "`Voer een pad in naar development.ini ('${workspaceFolderToken}' verwijzen naar de root van de huidige werkruimtemap)`",
- "debug.pyramidEnterDevelopmentIniPathInvalidFilePathError": "Voer een geldig bestandspad in",
- "debug.attachRemotePortTitle": "Extern debuggen",
- "debug.attachRemotePortPrompt": "Voer een port-nummer in",
- "debug.attachRemotePortValidationError": "Voer een geldig port-nummer in",
- "debug.attachRemoteHostTitle": "Extern debuggen",
- "debug.attachRemoteHostPrompt": "Voer een hostname of IP-adres in",
- "debug.attachRemoteHostValidationError": "Voer een geldige hostname of IP-adres in",
- "Testing.testErrorDiagnosticMessage": "Error",
- "Testing.testFailDiagnosticMessage": "Mislukt",
- "Testing.testSkippedDiagnosticMessage": "Overgeslagen"
-}
diff --git a/package.nls.pl.json b/package.nls.pl.json
deleted file mode 100644
index 00f186f2ebab..000000000000
--- a/package.nls.pl.json
+++ /dev/null
@@ -1,28 +0,0 @@
-{
- "python.command.python.sortImports.title": "Sortuj importy",
- "python.command.python.startREPL.title": "Uruchom REPL",
- "python.command.python.createTerminal.title": "Otwórz Terminal",
- "python.command.python.buildWorkspaceSymbols.title": "Zbuduj symbole dla przestrzeni roboczej",
- "python.command.python.runtests.title": "Uruchom wszystkie testy jednostkowe",
- "python.command.python.debugtests.title": "Debuguj wszystkie testy jednostkowe",
- "python.command.python.execInTerminal.title": "Uruchom plik pythonowy w terminalu",
- "python.command.python.setInterpreter.title": "Wybierz wersję interpretera",
- "python.command.python.refactorExtractVariable.title": "Wyodrębnij zmienną",
- "python.command.python.refactorExtractMethod.title": "Wyodrębnij metodę",
- "python.command.python.viewOutput.title": "Pokaż wyniki",
- "python.command.python.viewTestOutput.title": "Pokaż wyniki testów jednostkowych",
- "python.command.python.selectAndRunTestMethod.title": "Uruchom metodę testów jednostkowych ...",
- "python.command.python.selectAndDebugTestMethod.title": "Debuguj metodę testów jednostkowych ...",
- "python.command.python.selectAndRunTestFile.title": "Uruchom plik z testami jednostkowymi ...",
- "python.command.python.runCurrentTestFile.title": "Uruchom bieżący plik z testami jednostkowymi",
- "python.command.python.runFailedTests.title": "Uruchom testy jednostkowe, które się nie powiodły",
- "python.command.python.discoverTests.title": "Wyszukaj testy jednostkowe",
- "python.command.python.configureTests.title": "Konfiguruj testy jednostkowe",
- "python.command.python.execSelectionInTerminal.title": "Uruchom zaznaczony obszar w interpreterze Pythona",
- "python.command.python.execSelectionInDjangoShell.title": "Uruchom zaznaczony obszar w powłoce Django",
- "python.command.python.goToPythonObject.title": "Idź do obiektu pythonowego",
- "python.command.python.setLinter.title": "Wybierz linter",
- "python.command.python.enableLinting.title": "Włącz linting",
- "python.command.python.runLinting.title": "Uruchom linting",
- "python.command.python.enableSourceMapSupport.title": "Włącz obsługę map źródłowych do debugowania rozszerzeń"
-}
diff --git a/package.nls.pt-br.json b/package.nls.pt-br.json
deleted file mode 100644
index 1acc94053ca0..000000000000
--- a/package.nls.pt-br.json
+++ /dev/null
@@ -1,31 +0,0 @@
-{
- "python.command.python.sortImports.title": "Ordenar Importações",
- "python.command.python.startREPL.title": "Iniciar REPL",
- "python.command.python.createTerminal.title": "Criar Terminal",
- "python.command.python.buildWorkspaceSymbols.title": "Construir Símbolos da Área de Trabalho",
- "python.command.python.runtests.title": "Executar Todos os Testes Unitários",
- "python.command.python.debugtests.title": "Depurar Todos os Testes Unitários",
- "python.command.python.execInTerminal.title": "Executar Arquivo no Terminal",
- "python.command.python.setInterpreter.title": "Selecionar Interpretador",
- "python.command.python.refactorExtractVariable.title": "Extrair Variável",
- "python.command.python.refactorExtractMethod.title": "Extrair Método",
- "python.command.python.viewTestOutput.title": "Exibir Resultados dos Testes Unitários",
- "python.command.python.selectAndRunTestMethod.title": "Executar Testes Unitários do Método ...",
- "python.command.python.selectAndDebugTestMethod.title": "Depurar Testes Unitários do Método ...",
- "python.command.python.selectAndRunTestFile.title": "Executar Arquivo de Testes Unitários ...",
- "python.command.python.runCurrentTestFile.title": "Executar o Arquivo de Testes Unitários Atual",
- "python.command.python.runFailedTests.title": "Executar Testes Unitários com Falhas",
- "python.command.python.discoverTests.title": "Descobrir Testes Unitários",
- "python.command.python.execSelectionInTerminal.title": "Executar Seleção/Linha no Terminal",
- "python.command.python.execSelectionInDjangoShell.title": "Executar Seleção/Linha no Django Shell",
- "python.command.python.goToPythonObject.title": "Ir para Objeto Python",
- "python.command.python.setLinter.title": "Selecionar Linter",
- "python.command.python.enableLinting.title": "Habilitar Linting",
- "python.command.python.runLinting.title": "Executar Linting",
- "python.snippet.launch.standard.label": "Python: Arquivo Atual",
- "python.snippet.launch.module.label": "Python: Módulo",
- "python.snippet.launch.django.label": "Python: Django",
- "python.snippet.launch.flask.label": "Python: Flask",
- "python.snippet.launch.pyramid.label": "Python: Aplicação Pyramid",
- "python.snippet.launch.attach.label": "Python: Anexar"
-}
diff --git a/package.nls.ru.json b/package.nls.ru.json
deleted file mode 100644
index 4be739ab2bb7..000000000000
--- a/package.nls.ru.json
+++ /dev/null
@@ -1,38 +0,0 @@
-{
- "python.command.python.sortImports.title": "Отсортировать Imports",
- "python.command.python.startREPL.title": "Открыть REPL",
- "python.command.python.buildWorkspaceSymbols.title": "Собрать символы рабочего пространства",
- "python.command.python.runtests.title": "Запустить все тесты",
- "python.command.python.debugtests.title": "Запустить все тесты под отладчиком",
- "python.command.python.execInTerminal.title": "Выполнить файл в консоли",
- "python.command.python.setInterpreter.title": "Выбрать интерпретатор",
- "python.command.python.refactorExtractVariable.title": "Извлечь в переменную",
- "python.command.python.refactorExtractMethod.title": "Извлечь в метод",
- "python.command.python.viewTestOutput.title": "Показать вывод теста",
- "python.command.python.selectAndRunTestMethod.title": "Запусть тестовый метод...",
- "python.command.python.selectAndDebugTestMethod.title": "Отладить тестовый метод...",
- "python.command.python.selectAndRunTestFile.title": "Запустить тестовый файл...",
- "python.command.python.runCurrentTestFile.title": "Запустить текущий тестовый файл",
- "python.command.python.runFailedTests.title": "Запустить непрошедшие тесты",
- "python.command.python.discoverTests.title": "Обнаружить тесты",
- "python.command.python.execSelectionInTerminal.title": "Выполнить выбранный текст или текущую строку в консоли",
- "python.command.python.execSelectionInDjangoShell.title": "Выполнить выбранный текст или текущую строку в оболочке Django",
- "python.command.python.goToPythonObject.title": "Перейти к объекту Python",
- "python.command.python.setLinter.title": "Выбрать анализатор кода",
- "python.command.python.enableLinting.title": "Включить анализатор кода",
- "python.command.python.runLinting.title": "Выполнить анализ кода",
- "python.snippet.launch.standard.label": "Python: Текущий файл",
- "python.snippet.launch.module.label": "Python: Модуль",
- "python.snippet.launch.django.label": "Python: Django",
- "python.snippet.launch.flask.label": "Python: Flask",
- "python.snippet.launch.pyramid.label": "Python: Приложение Pyramid",
- "python.snippet.launch.attach.label": "Python: Подключить отладчик",
- "ExtensionSurveyBanner.bannerLabelYes": "Да, открыть опрос сейчас",
- "ExtensionSurveyBanner.bannerLabelNo": "Нет, спасибо",
- "ExtensionSurveyBanner.maybeLater": "Может быть, позже",
- "ExtensionSurveyBanner.bannerMessage": "Не могли бы вы потратить пару минут на опрос о языковом сервере Pylance?",
- "Pylance.remindMeLater": "Напомните позже",
- "Pylance.pylanceNotInstalledMessage": "Расширение Pylance не установлено.",
- "Pylance.pylanceInstalledReloadPromptMessage": "Расширение Pylance установлено. Перезагрузить окно для его активации?",
- "LanguageService.reloadAfterLanguageServerChange": "Пожалуйста, перезагрузите окно после смены типа языкового сервера."
-}
diff --git a/package.nls.tr.json b/package.nls.tr.json
deleted file mode 100644
index 0e648bb38fdf..000000000000
--- a/package.nls.tr.json
+++ /dev/null
@@ -1,33 +0,0 @@
-{
- "python.command.python.sortImports.title": "Import İfadelerini Sırala",
- "python.command.python.startREPL.title": "REPL Başlat",
- "python.command.python.createTerminal.title": "Terminal Oluştur",
- "python.command.python.buildWorkspaceSymbols.title": "Çalışma Alanındaki Sembolleri Derle",
- "python.command.python.runtests.title": "Testleri Çalıştır",
- "python.command.python.debugtests.title": "Testleri Debug Et",
- "python.command.python.execInTerminal.title": "Terminalde Çalıştır",
- "python.command.python.setInterpreter.title": "Bir Interpreter Seçin",
- "python.command.python.refactorExtractVariable.title": "Değişken Çıkar",
- "python.command.python.refactorExtractMethod.title": "Metot Çıkar",
- "python.command.python.viewTestOutput.title": "Test Çıktısını Görüntüle",
- "python.command.python.selectAndRunTestMethod.title": "Test Metodu Çalıştır",
- "python.command.python.selectAndDebugTestMethod.title": "Test Metodu Debug Et",
- "python.command.python.selectAndRunTestFile.title": "Bir Test Dosyası Seç ve Çalıştır",
- "python.command.python.runCurrentTestFile.title": "Aktif Test Dosyasını Çalıştır",
- "python.command.python.runFailedTests.title": "Başarısız Testleri Çalıştır",
- "python.command.python.discoverTests.title": "Testleri Keşfet",
- "python.command.python.discoveringTests.title": "Testler Keşfediliyor...",
- "python.command.python.execSelectionInTerminal.title": "Seçimi/Satırı Terminalde Çalıştır",
- "python.command.python.execSelectionInDjangoShell.title": "Seçimi/Satırı Django Shell'inde Çalıştır",
- "python.command.python.goToPythonObject.title": "Python Nesnesine Git",
- "python.command.python.setLinter.title": "Bir Linter Seç",
- "python.command.python.enableLinting.title": "Linting'i Aktifleştir",
- "python.command.python.runLinting.title": "Linter Çalıştır",
- "python.snippet.launch.standard.label": "Python: Geçerli Dosya",
- "python.snippet.launch.module.label": "Python: Modül",
- "python.snippet.launch.module.default": "modül-adını-yazın",
- "python.snippet.launch.attach.label": "Python: Remote Attach",
- "python.snippet.launch.django.label": "Python: Django",
- "python.snippet.launch.flask.label": "Python: Flask",
- "python.snippet.launch.pyramid.label": "Python: Pyramid Uygulaması"
-}
diff --git a/package.nls.zh-cn.json b/package.nls.zh-cn.json
deleted file mode 100644
index df3c5ccdf158..000000000000
--- a/package.nls.zh-cn.json
+++ /dev/null
@@ -1,232 +0,0 @@
-{
- "python.command.python.sortImports.title": "排序 import 语句",
- "python.command.python.startREPL.title": "启动 REPL",
- "python.command.python.createTerminal.title": "创建终端",
- "python.command.python.buildWorkspaceSymbols.title": "构建工作区符号",
- "python.command.python.runtests.title": "运行所有单元测试",
- "python.command.python.debugtests.title": "调试所有单元测试",
- "python.command.python.execInTerminal.title": "在终端中运行 Python 文件",
- "python.command.python.setInterpreter.title": "选择解释器",
- "python.command.python.switchOffInsidersChannel.title": "切换到默认版本",
- "python.command.python.switchToDailyChannel.title": "切换到每日预览版本",
- "python.command.python.switchToWeeklyChannel.title": "切换到每周预览版本",
- "python.command.python.clearWorkspaceInterpreter.title": "清除工作区解释器设置",
- "python.command.python.refactorExtractVariable.title": "提取变量",
- "python.command.python.refactorExtractMethod.title": "提取方法",
- "python.command.python.viewOutput.title": "显示输出",
- "python.command.python.viewTestOutput.title": "显示单元测试输出",
- "python.command.python.viewLanguageServerOutput.title": "显示语言服务器输出",
- "python.command.python.selectAndRunTestMethod.title": "运行单元测试方法...",
- "python.command.python.selectAndDebugTestMethod.title": "调试单元测试方法...",
- "python.command.python.selectAndRunTestFile.title": "运行单元测试文件...",
- "python.command.python.runCurrentTestFile.title": "运行当前单元测试文件",
- "python.command.python.runFailedTests.title": "运行失败的单元测试",
- "python.command.python.discoverTests.title": "检测单元测试",
- "python.command.python.discoveringTests.title": "检测中...",
- "python.command.python.stopTests.title": "停止",
- "python.command.python.configureTests.title": "配置单元测试",
- "python.command.python.execSelectionInTerminal.title": "在 Python 终端中运行选定内容/行",
- "python.command.python.execSelectionInDjangoShell.title": "在 Django Shell 中运行选定内容/行",
- "python.command.python.goToPythonObject.title": "转到 Python 对象",
- "python.command.python.setLinter.title": "选择代码检查器",
- "python.command.python.enableLinting.title": "启用代码检查",
- "python.command.python.runLinting.title": "执行代码检查",
- "python.command.python.enableSourceMapSupport.title": "为扩展调试启用 Source Map 支持",
- "python.command.python.startPage.open.title": "打开起始页",
- "python.command.python.analysis.clearCache.title": "清除模块分析缓存",
- "python.command.python.analysis.restartLanguageServer.title": "重启语言服务器",
- "python.command.python.launchTensorBoard.title": "启动 TensorBoard",
- "python.snippet.launch.standard.label": "Python: 当前文件",
- "python.snippet.launch.module.label": "Python: 模块",
- "python.snippet.launch.module.default": "请输入模块名称",
- "python.snippet.launch.attach.label": "Python: 远程连接",
- "python.snippet.launch.attachpid.label": "Python: 使用 PID 连接",
- "python.snippet.launch.django.label": "Python: Django",
- "python.snippet.launch.fastapi.label": "Python: FastAPI",
- "python.snippet.launch.flask.label": "Python: Flask",
- "python.snippet.launch.pyramid.label": "Python: Pyramid 应用",
- "Pylance.remindMeLater": "稍后提醒",
- "Pylance.pylanceNotInstalledMessage": "Pylance 扩展未安装。",
- "Pylance.pylanceInstalledReloadPromptMessage": "Pylance 扩展未安装。重新加载窗口以激活?",
- "Experiments.inGroup": "用户属于 '{0}' 实验组",
- "Interpreters.RefreshingInterpreters": "正在刷新 Python 解释器",
- "Interpreters.entireWorkspace": "完整工作区",
- "Interpreters.pythonInterpreterPath": "Python 解释器路径: {0}",
- "Interpreters.LoadingInterpreters": "正在加载 Python 解释器",
- "Interpreters.condaInheritEnvMessage": "您正在使用 conda 环境,如果您在集成终端中遇到相关问题,建议您允许 Python 扩展将用户设置中的 \"terminal.integrated.inheritEnv\" 改为 false。",
- "Logging.CurrentWorkingDirectory": "cwd:",
- "InterpreterQuickPickList.quickPickListPlaceholder": "当前: {0}",
- "InterpreterQuickPickList.enterPath.detail": "输入路径或选择一个现有的解释器",
- "InterpreterQuickPickList.enterPath.label": "输入解释器路径...",
- "InterpreterQuickPickList.enterPath.placeholder": "请输入 Python 解释器的路径。",
- "InterpreterQuickPickList.browsePath.label": "浏览...",
- "InterpreterQuickPickList.browsePath.detail": "浏览文件系统来选择一个 Python 解释器。",
- "InterpreterQuickPickList.browsePath.title": "选择 Python 解释器",
- "diagnostics.upgradeCodeRunner": "请更新 Code Runner 扩展,使其与 Python 扩展兼容。",
- "Common.bannerLabelYes": "是",
- "Common.bannerLabelNo": "否",
- "Common.doNotShowAgain": "不再提示",
- "Common.reload": "重新加载",
- "Common.moreInfo": "更多信息",
- "Common.and": "和",
- "Common.ok": "好的",
- "Common.install": "安装",
- "Common.learnMore": "了解更多",
- "Common.reportThisIssue": "反馈此问题",
- "CommonSurvey.remindMeLaterLabel": "稍后提醒",
- "CommonSurvey.yesLabel": "是的,现在接受调查",
- "CommonSurvey.noLabel": "不,谢谢",
- "OutputChannelNames.languageServer": "Python 语言服务器",
- "OutputChannelNames.python": "Python",
- "OutputChannelNames.pythonTest": "Python 测试日志",
- "ExtensionSurveyBanner.bannerMessage": "请您花两分钟的时间告诉我们 Python 扩展是否正常工作?",
- "ExtensionSurveyBanner.bannerLabelYes": "是的,现在接受调查",
- "ExtensionSurveyBanner.bannerLabelNo": "不,谢谢",
- "ExtensionSurveyBanner.maybeLater": "稍后提醒",
- "ExtensionChannels.installingInsidersMessage": "正在安装预览版... ",
- "ExtensionChannels.installingStableMessage": "正在安装稳定版... ",
- "ExtensionChannels.installationCompleteMessage": "完成。",
- "ExtensionChannels.downloadingInsidersMessage": "正在下载预览版... ",
- "ExtensionChannels.yesWeekly": "是,每周版",
- "ExtensionChannels.yesDaily": "是,每日版",
- "ExtensionChannels.promptMessage": "您正在使用 Visual Studio Code 预览版,是否安装 Python 扩展的预览版?",
- "ExtensionChannels.reloadToUseInsidersMessage": "请重新加载 Visual Studio Code 以使用 Python 扩展的预览版。",
- "ExtensionChannels.downloadCompletedOutputMessage": "预览版下载完成。",
- "ExtensionChannels.startingDownloadOutputMessage": "开始下载预览版。",
- "Interpreters.environmentPromptMessage": "检测到新的虚拟环境,是否在此工作区中使用它?",
- "Linter.enableLinter": "启用 {0}",
- "Linter.enablePylint": "该工作区有一个 pylintrc 文件,是否启用 pylint?",
- "Linter.replaceWithSelectedLinter": "设置中启用了多个代码检查器,是否用 '{0}' 替换?",
- "Linter.install": "请安装一个代码检查器以获得错误报告。",
- "Linter.installPylint": "安装 pylint",
- "Linter.installFlake8": "安装 flake8",
- "Linter.selectLinter": "选择代码检查器",
- "Installer.noCondaOrPipInstaller": "所选环境中没有可用的 Conda 或 pip 安装器。",
- "Installer.noPipInstaller": "所选环境中没有可用的 pip 安装器。",
- "Installer.searchForHelp": "搜索帮助",
- "Installer.couldNotInstallLibrary": "无法安装 {0} 。如果 pip 不可用,请使用选择的包管理器手动将此库安装到您的 Python 环境中。",
- "Installer.dataScienceInstallPrompt": "数据科学库 {0} 未安装,是否安装?",
- "diagnostics.warnSourceMaps": "已启用 Source Map 支持,这会影响 Python 扩展的性能。",
- "diagnostics.disableSourceMaps": "禁用 Source Map 支持",
- "diagnostics.warnBeforeEnablingSourceMaps": "启用 Source Map 支持将影响 Python 扩展的性能。",
- "diagnostics.enableSourceMapsAndReloadVSC": "启用并重新加载窗口",
- "diagnostics.lsNotSupported": "该操作系统不符合 Python 语言服务器的最低要求,正在恢复替代的自动补全器 Jedi。",
- "diagnostics.invalidPythonPathInDebuggerSettings": "您需要在开始调试前选择一个 Python 解释器。\n\n提示: 点击状态栏中的 \"选择解释器\"。",
- "diagnostics.invalidPythonPathInDebuggerLaunch": "调试设置中的 Python 路径无效。",
- "diagnostics.invalidDebuggerTypeDiagnostic": "您的 launch.json 文件需要更新,以将 \"pythonExperimental\" 调试设置设为使用 \"python\" 调试器,否则 Python 调试器可能无法工作。立即自动更新 launch.json?",
- "diagnostics.consoleTypeDiagnostic": "您的 launch.json 文件需要更新,以将控制台类型字符串从 \"none\" 改为 \"internalConsole\",否则 Python 调试器可能无法工作。立即自动更新 launch.json?",
- "diagnostics.justMyCodeDiagnostic": "不再支持 launch.json 中的配置 \"debugStdLib\",建议用 \"justMyCode\" 代替,这与使用 \"debugStdLib\" 完全相反。是否自动更新 launch.json?",
- "diagnostics.checkIsort5UpgradeGuide": "此工作区的排序 import 语句配置已过时。查看 [isort 升级指南](https://aka.ms/AA9j5x4) 来更新设置。",
- "diagnostics.yesUpdateLaunch": "是,更新 launch.json",
- "diagnostics.invalidTestSettings": "您的设置需要更新,以将设置 \"python.unitTest.\" 改为 \"python.testing.\",否则使用该扩展测试 Python 代码可能无法工作。是否自动更新设置?",
- "Common.canceled": "已取消",
- "Common.cancel": "取消",
- "Common.yesPlease": "好的",
- "Common.loadingPythonExtension": "Python 扩展正在加载...",
- "debug.selectConfigurationTitle": "选择调试配置",
- "debug.selectConfigurationPlaceholder": "调试配置",
- "debug.launchJsonConfigurationsCompletionLabel": "Python",
- "debug.launchJsonConfigurationsCompletionDescription": "选择 Python 调试配置",
- "debug.debugFileConfigurationLabel": "Python 文件",
- "debug.debugFileConfigurationDescription": "调试打开的 Python 文件",
- "debug.debugModuleConfigurationLabel": "模块",
- "debug.debugModuleConfigurationDescription": "用'-m'调用 Python 模块进行调试",
- "debug.moduleEnterModuleTitle": "调试模块",
- "debug.moduleEnterModulePrompt": "请输入 Python 模块/包名",
- "debug.moduleEnterModuleDefault": "请输入模块名称",
- "debug.moduleEnterModuleInvalidNameError": "请输入有效的模块名称",
- "debug.remoteAttachConfigurationLabel": "远程连接",
- "debug.remoteAttachConfigurationDescription": "连接到远程调试服务器",
- "debug.attachRemoteHostTitle": "远程连接",
- "debug.attachRemoteHostPrompt": "请输入主机名",
- "debug.attachRemoteHostValidationError": "请输入有效的主机名或 IP 地址",
- "debug.attachRemotePortTitle": "远程调试",
- "debug.attachRemotePortPrompt": "请输入调试服务器的监听端口号",
- "debug.attachRemotePortValidationError": "请输入有效的端口号",
- "debug.attachPidConfigurationLabel": "使用 PID 连接",
- "debug.attachPidConfigurationDescription": "连接到本地进程",
- "debug.debugDjangoConfigurationLabel": "Django",
- "debug.debugDjangoConfigurationDescription": "启动并调试 Django Web 应用",
- "debug.djangoEnterManagePyPathTitle": "调试 Django",
- "debug.djangoEnterManagePyPathPrompt": "请输入 manage.py 的路径('${workspaceFolderToken}'指向当前工作区文件夹的根目录)",
- "debug.djangoEnterManagePyPathInvalidFilePathError": "请输入有效的 Python 文件路径",
- "debug.debugFastAPIConfigurationLabel": "FastAPI",
- "debug.debugFastAPIConfigurationDescription": "启动并调试 FastAPI Web 应用",
- "debug.fastapiEnterAppPathOrNamePathTitle": "调试 FastAPI",
- "debug.fastapiEnterAppPathOrNamePathPrompt": "请输入应用路径,例如 'main.py' 或 'main'",
- "debug.fastapiEnterAppPathOrNamePathInvalidNameError": "请输入有效的名称",
- "debug.debugFlaskConfigurationLabel": "Flask",
- "debug.debugFlaskConfigurationDescription": "启动并调试 Flask Web 应用",
- "debug.flaskEnterAppPathOrNamePathTitle": "调试 Flask",
- "debug.flaskEnterAppPathOrNamePathPrompt": "请输入应用路径,例如 'app.py' 或 'app'",
- "debug.flaskEnterAppPathOrNamePathInvalidNameError": "请输入有效的名称",
- "debug.debugPyramidConfigurationLabel": "Pyramid",
- "debug.debugPyramidConfigurationDescription": "Web 应用",
- "debug.pyramidEnterDevelopmentIniPathTitle": "调试 Pyramid",
- "debug.pyramidEnterDevelopmentIniPathPrompt": "`请输入development.ini的路径('${workspaceFolderToken}'指向当前工作区文件夹的根目录)`",
- "debug.pyramidEnterDevelopmentIniPathInvalidFilePathError": "请输入有效的文件路径",
- "Testing.testErrorDiagnosticMessage": "Error",
- "Testing.testFailDiagnosticMessage": "Fail",
- "Testing.testSkippedDiagnosticMessage": "Skipped",
- "Testing.configureTests": "配置单元测试框架",
- "Testing.disableTests": "禁用单元测试",
- "Common.openOutputPanel": "显示输出",
- "LanguageService.lsFailedToStart": "启动语言服务器时出错,正在恢复到 Jedi 语言引擎。查看 Python 输出面板了解详情。",
- "LanguageService.lsFailedToDownload": "下载语言服务器时出错,正在恢复到 Jedi 语言引擎。查看 Python 输出面板了解详情。",
- "LanguageService.lsFailedToExtract": "提取语言服务器时出错,正在恢复到 Jedi 语言引擎。查看 Python 输出面板了解详情。",
- "LanguageService.downloadFailedOutputMessage": "语言服务器下载失败",
- "LanguageService.extractionFailedOutputMessage": "语言服务器提取失败",
- "LanguageService.extractionCompletedOutputMessage": "语言服务器下载完成",
- "LanguageService.extractionDoneOutputMessage": "完成",
- "LanguageService.reloadVSCodeIfSeachPathHasChanged": "该 Python 解释器的搜索路径已改变,请重新加载扩展以确保 IntelliSense 正常工作。",
- "LanguageService.startingJedi": "正在启动 Jedi Python 语言引擎。",
- "LanguageService.startingMicrosoft": "正在启动 Microsoft Python 语言服务器。",
- "LanguageService.startingPylance": "正在启动 Pylance 语言服务器。",
- "LanguageService.startingNone": "由于语言服务器设置为空,编辑器支持处于非活动状态。",
- "LanguageService.reloadAfterLanguageServerChange": "切换语言服务器后请重新加载窗口。",
- "AttachProcess.unsupportedOS": "不支持 '{0}' 操作系统。",
- "AttachProcess.attachTitle": "连接到进程",
- "AttachProcess.selectProcessPlaceholder": "选择要连接的流程",
- "AttachProcess.noProcessSelected": "未选择进程",
- "AttachProcess.refreshList": "刷新进程列表",
- "diagnostics.updateSettings": "是,更新设置",
- "Common.noIWillDoItLater": "稍后再做",
- "Common.notNow": "稍后提醒",
- "Common.gotIt": "好的!",
- "Interpreters.selectInterpreterTip": "提示:您可以通过点击状态栏中的 Python 版本来更改 Python 扩展所使用的 Python 解释器",
- "downloading.file": "正在下载 {0}...",
- "downloading.file.progress": "{2} 中的 {0}{1} KB ({3}%)",
- "products.installingModule": "正在安装 {0}",
- "OutdatedDebugger.updateDebuggerMessage": "您正在连接至 ptvsd (Python 调试器),而 ptvsd 已于2020年5月1日停止更新。请切换至 [debugpy](https://aka.ms/migrateToDebugpy)。",
- "StartPage.getStarted": "Python - 开始",
- "StartPage.pythonExtensionTitle": "Python 扩展",
- "StartPage.createJupyterNotebook": "创建 Jupyter 笔记本",
- "StartPage.notebookDescription": "- 在命令面板 (Shift + Command + P
) 中运行 \"创建新的 笔记本
\" - 探索 示例笔记本
来了解笔记本的功能",
- "StartPage.createAPythonFile": "创建 Python 文件",
- "StartPage.pythonFileDescription": "- 创建以 .py 为扩展名的新文件
",
- "StartPage.openInteractiveWindow": "使用交互式窗口开发 Python 脚本",
- "StartPage.interactiveWindowDesc": "- 您可以在 Python 文件中输入 \"#%%\" 来创建一个单元 - 使用 \"Shift + Enter
\" 来运行一个单元,输出将显示在交互式窗口中",
- "StartPage.releaseNotes": "请查看 发行说明 ,了解更多最新功能。",
- "StartPage.tutorialAndDoc": "在 教程 中探索更多的功能,或查看 文档 以获得提示和排除故障。",
- "StartPage.dontShowAgain": "不再显示此页面",
- "StartPage.helloWorld": "Hello world",
- "StartPage.sampleNotebook": "笔记本介绍",
- "StartPage.openFolder": "打开文件夹或工作区",
- "StartPage.folderDesc": "- 打开 文件夹
- 打开 工作区
",
- "StartPage.badWebPanelFormatString": "{0} 文件名无效 ",
- "Jupyter.extensionRequired": "执行该任务需要 Jupyter 扩展。点击\"是 \"打开 Jupyter 扩展的安装页面。",
- "TensorBoard.useCurrentWorkingDirectory": "使用当前工作目录",
- "TensorBoard.currentDirectory": "当前:{0}",
- "TensorBoard.logDirectoryPrompt": "选择一个日志目录来启动 TensorBoard",
- "TensorBoard.progressMessage": "正在启动 TensorBoard 会话...",
- "TensorBoard.failedToStartSessionError": "启动 TensorBoard 会话失败,错误:{0}",
- "TensorBoard.nativeTensorBoardPrompt": "VS Code 现已集成了 TensorBoard 支持。是否启动 TensorBoard?(提示:打开命令面板并搜索 \"启动 TensorBoard\",即可随时启动 TensorBoard。)",
- "TensorBoard.selectAFolder": "选择一个文件夹",
- "TensorBoard.selectAnotherFolder": "选择另一个文件夹",
- "TensorBoard.selectAFolderDetail": "选择一个包含 tfevent 文件的日志目录",
- "TensorBoard.selectAnotherFolderDetail": "使用文件资源管理器选择另一个文件夹",
- "TensorBoard.useCurrentWorkingDirectoryDetail": "TensorBoard 将在当前工作目录的所有子目录中搜索 tfevent 文件",
- "TensorBoard.installPrompt": "启动 TensorBoard 会话需要安装 TensorBoard 包。是否安装?",
- "TensorBoard.launchNativeTensorBoardSessionCodeAction": "启动 TensorBoard 会话",
- "TensorBoard.launchNativeTensorBoardSessionCodeLens": "▶ 启动 TensorBoard 会话"
-}
diff --git a/package.nls.zh-tw.json b/package.nls.zh-tw.json
deleted file mode 100644
index 373a44299d87..000000000000
--- a/package.nls.zh-tw.json
+++ /dev/null
@@ -1,154 +0,0 @@
-{
- "python.command.python.sortImports.title": "排序 Import 語句",
- "python.command.python.startREPL.title": "啟動 REPL",
- "python.command.python.createTerminal.title": "建立終端機",
- "python.command.python.buildWorkspaceSymbols.title": "建構工作區符號",
- "python.command.python.runtests.title": "執行所有單元測試",
- "python.command.python.debugtests.title": "偵錯所有單元測試",
- "python.command.python.execInTerminal.title": "在終端機中執行 Python 檔案",
- "python.command.python.setInterpreter.title": "選擇直譯器",
- "python.command.python.refactorExtractVariable.title": "提取變數",
- "python.command.python.refactorExtractMethod.title": "提取方法",
- "python.command.python.viewTestOutput.title": "顯示單元測試輸出",
- "python.command.python.selectAndRunTestMethod.title": "執行單元測試方法…",
- "python.command.python.selectAndDebugTestMethod.title": "偵錯單元測試方法…",
- "python.command.python.selectAndRunTestFile.title": "執行單元測試檔案…",
- "python.command.python.runCurrentTestFile.title": "執行目前單元測試檔案",
- "python.command.python.runFailedTests.title": "執行失敗的單元測試",
- "python.command.python.execSelectionInTerminal.title": "在 Python 終端機中執行選定內容/行",
- "python.command.python.execSelectionInDjangoShell.title": "在 Django Shell 中執行選定內容/行",
- "python.command.python.goToPythonObject.title": "跳至 Python 物件",
- "python.command.python.setLinter.title": "選擇 Linter",
- "python.command.python.enableLinting.title": "啟用 Linting",
- "python.command.python.runLinting.title": "執行 Linting",
- "python.snippet.launch.standard.label": "Python: 目前檔案",
- "python.snippet.launch.module.label": "Python: 模組",
- "python.snippet.launch.django.label": "Python: Django",
- "python.snippet.launch.flask.label": "Python: Flask",
- "python.snippet.launch.pyramid.label": "Python: Pyramid 程式",
- "python.snippet.launch.attach.label": "Python: 附加",
- "python.command.python.discoverTests.title": "探索 Unit 測試項目",
- "python.command.python.switchOffInsidersChannel.title": "切換至預設頻道",
- "python.command.python.switchToDailyChannel.title": "切換至 Insiders 每日頻道",
- "python.command.python.switchToWeeklyChannel.title": "切換至 Insiders 每週頻道",
- "python.command.python.viewOutput.title": "顯示輸出",
- "python.command.python.viewLanguageServerOutput.title": "顯示語言伺服器輸出",
- "python.command.python.discoveringTests.title": "正在探索...",
- "python.command.python.stopTests.title": "停止",
- "python.command.python.configureTests.title": "設定測試",
- "python.command.python.enableSourceMapSupport.title": "啟用供偵錯延伸模組的原始碼映射 (Source Map) 支援",
- "python.command.python.analysis.clearCache.title": "清除模組分析快取",
- "python.snippet.launch.module.default": "請輸入-模組-名稱",
- "python.snippet.launch.attachpid.label": "Python: 使用處理程序 ID 連結",
- "LanguageService.lsFailedToStart": "啟動語言伺服器時遇到問題。改回使用替代方案 \"Jedi\"。請檢查 Python 輸出面板以取得更多資訊。",
- "LanguageService.lsFailedToDownload": "下載語言伺服器時遇到問題。改回使用替代方案 \"Jedi\"。請檢查 Python 輸出面板以取得更多資訊。",
- "LanguageService.lsFailedToExtract": "擷取語言伺服器時遇到問題。改回使用替代方案 \"Jedi\"。請檢查 Python 輸出面板以取得更多資訊。",
- "Experiments.inGroup": "使用者屬於 \"{0}\" 實驗性群組",
- "Interpreters.RefreshingInterpreters": "正在重新整理 Python 解譯器",
- "Interpreters.LoadingInterpreters": "正在載入 Python 解譯器",
- "Interpreters.condaInheritEnvMessage": "我們發覺到您在使用 conda 環境。如果你在整合式終端器中使用這個環境時遇到問題,建議您讓 Python 延伸模組變更使用者設定中的 \"terminal.integrated.inheritEnv\" 為 false。",
- "Logging.CurrentWorkingDirectory": "cwd:",
- "Common.doNotShowAgain": "不再顯示",
- "Common.reload": "重新載入",
- "Common.moreInfo": "更多資訊",
- "OutputChannelNames.languageServer": "Python 語言伺服器",
- "OutputChannelNames.python": "Python",
- "OutputChannelNames.pythonTest": "Python 測試記錄",
- "ExtensionSurveyBanner.bannerMessage": "請問您是否可以用兩分鐘的時間,告訴我們 Python 延伸模組在您環境中的運作情況?",
- "ExtensionSurveyBanner.bannerLabelYes": "是,現在填寫調查",
- "ExtensionSurveyBanner.bannerLabelNo": "不了,謝謝",
- "ExtensionSurveyBanner.maybeLater": "等一下",
- "ExtensionChannels.installingInsidersMessage": "正在安裝 Insiders... ",
- "ExtensionChannels.installingStableMessage": "正在安裝穩定版... ",
- "ExtensionChannels.installationCompleteMessage": "完成。",
- "ExtensionChannels.downloadingInsidersMessage": "正在下載 Insiders 延伸模組... ",
- "ExtensionChannels.yesWeekly": "是,每週",
- "ExtensionChannels.yesDaily": "是,每天",
- "ExtensionChannels.promptMessage": "我們發覺到您在使用 Visual Studio Code Insiders。請問您是否想使用 Python 延伸模組的 Insiders 組建?",
- "ExtensionChannels.reloadToUseInsidersMessage": "請重新載入 Visual Studio Code 以使用 Python 延伸模組的 Insiders 組建。",
- "ExtensionChannels.downloadCompletedOutputMessage": "Insiders 組建下載完成。",
- "ExtensionChannels.startingDownloadOutputMessage": "開始下載 Insiders 組建。",
- "Interpreters.environmentPromptMessage": "We noticed a new virtual environment has been created. Do you want to select it for the workspace folder?",
- "Linter.enableLinter": "啟用 {0}",
- "Linter.enablePylint": "您的工作區有 pylintrc 檔案。是否啟用 pylint?",
- "Linter.replaceWithSelectedLinter": "設定中啟用了多個 Linter。是否用 '{0}' 取代?",
- "Installer.noCondaOrPipInstaller": "選取環境中沒有可用的 Conda 或 Pip 安裝工具。",
- "Installer.noPipInstaller": "選取環境中沒有可用的 Pip 安裝工具。",
- "Installer.searchForHelp": "搜尋說明",
- "diagnostics.warnSourceMaps": "已在 Python 延伸模組中啟用原始碼映射 (Source Map) 支援,這會降低延伸模組效能。",
- "diagnostics.disableSourceMaps": "停用原始碼映射 (Source Map) 支援",
- "diagnostics.warnBeforeEnablingSourceMaps": "在 Python 延伸模組中啟用原始碼映射 (Source Map) 支援會降低延伸模組效能。",
- "diagnostics.enableSourceMapsAndReloadVSC": "啟用並重新載入視窗",
- "diagnostics.lsNotSupported": "您的作業系統不符合 Python 語言伺服器的最低需求。改回使用替代自動完成提供者 \"Jedi\"。",
- "diagnostics.invalidPythonPathInDebuggerSettings": "開始偵錯前,您需要選取 Python 解譯器。\n\n小提示:按一下狀態列的 \"選擇 Python 解譯器\"。",
- "diagnostics.invalidPythonPathInDebuggerLaunch": "偵錯設定檔的 Python 路徑無效。",
- "diagnostics.invalidDebuggerTypeDiagnostic": "Your launch.json file needs to be updated to change the \"pythonExperimental\" debug configurations to use the \"python\" debugger type, otherwise Python debugging may not work. Would you like to automatically update your launch.json file now?",
- "diagnostics.consoleTypeDiagnostic": "Your launch.json file needs to be updated to change the console type string from \"none\" to \"internalConsole\", otherwise Python debugging may not work. Would you like to automatically update your launch.json file now?",
- "diagnostics.justMyCodeDiagnostic": "Configuration \"debugStdLib\" in launch.json is no longer supported. It's recommended to replace it with \"justMyCode\", which is the exact opposite of using \"debugStdLib\". Would you like to automatically update your launch.json file to do that?",
- "diagnostics.yesUpdateLaunch": "是,更新 launch.json",
- "diagnostics.invalidTestSettings": "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?",
- "Common.canceled": "已取消",
- "Common.cancel": "取消",
- "Common.loadingPythonExtension": "正在載入 Python 延伸模組...",
- "debug.selectConfigurationTitle": "選擇偵錯設定檔",
- "debug.selectConfigurationPlaceholder": "偵錯設定檔",
- "debug.launchJsonConfigurationsCompletionLabel": "Python",
- "debug.launchJsonConfigurationsCompletionDescription": "選取 Python 偵錯設定檔",
- "debug.debugFileConfigurationLabel": "Python 檔案",
- "debug.debugFileConfigurationDescription": "偵錯目前使用中的 Python 檔案",
- "debug.debugModuleConfigurationLabel": "模組",
- "debug.debugModuleConfigurationDescription": "使用 '-m' 叫用以偵錯 Python 模組",
- "debug.moduleEnterModuleTitle": "偵錯模組",
- "debug.moduleEnterModulePrompt": "輸入 Python 模組 / 套件名稱",
- "debug.moduleEnterModuleDefault": "輸入-模組-名稱",
- "debug.moduleEnterModuleInvalidNameError": "請輸入有效的模組名稱",
- "debug.remoteAttachConfigurationLabel": "遠端連結",
- "debug.remoteAttachConfigurationDescription": "連結到遠端偵錯伺服器",
- "debug.attachRemoteHostTitle": "遠端偵錯",
- "debug.attachRemoteHostPrompt": "輸入主機名稱",
- "debug.attachRemoteHostValidationError": "請輸入有效的主機名稱或 IP 位址",
- "debug.attachRemotePortTitle": "遠端偵錯",
- "debug.attachRemotePortPrompt": "輸入偵錯伺服器正在監聽的連線埠號。",
- "debug.attachRemotePortValidationError": "請輸入有效的連線埠號。",
- "debug.attachPidConfigurationLabel": "使用處理程序 ID 連結",
- "debug.attachPidConfigurationDescription": "連結至本機處理程序",
- "debug.debugDjangoConfigurationLabel": "Django",
- "debug.debugDjangoConfigurationDescription": "執行並偵錯 Django 網路應用程式",
- "debug.djangoEnterManagePyPathTitle": "偵錯 Django",
- "debug.djangoEnterManagePyPathPrompt": "請輸入 manage.py 的路徑 ('${workspaceFolderToken}' 指向目前工作區資料夾的根目錄)",
- "debug.djangoEnterManagePyPathInvalidFilePathError": "請輸入有效的 Python 檔案路徑",
- "debug.debugFlaskConfigurationLabel": "Flask",
- "debug.debugFlaskConfigurationDescription": "執行並偵錯 Flask 網路應用程式",
- "debug.flaskEnterAppPathOrNamePathTitle": "偵錯 Flask",
- "debug.flaskEnterAppPathOrNamePathPrompt": "請輸入應用程式路徑。例如:'app.py' 或 'app'",
- "debug.flaskEnterAppPathOrNamePathInvalidNameError": "請輸入有效名稱",
- "debug.debugPyramidConfigurationLabel": "Pyramid",
- "debug.debugPyramidConfigurationDescription": "網路應用程式",
- "debug.pyramidEnterDevelopmentIniPathTitle": "偵錯 Pyramid",
- "debug.pyramidEnterDevelopmentIniPathPrompt": "`請輸入 development.ini 的路徑 ('${workspaceFolderToken}' 指向目前工作區資料夾的根目錄)`",
- "debug.pyramidEnterDevelopmentIniPathInvalidFilePathError": "請輸入有效的檔案路徑",
- "Testing.testErrorDiagnosticMessage": "錯誤",
- "Testing.testFailDiagnosticMessage": "失敗",
- "Testing.testSkippedDiagnosticMessage": "略過",
- "Testing.configureTests": "設定測試框架",
- "Testing.disableTests": "停用測試",
- "Common.openOutputPanel": "顯示輸出",
- "LanguageService.downloadFailedOutputMessage": "下載語言伺服器失敗",
- "LanguageService.extractionFailedOutputMessage": "擷取語言伺服器失敗",
- "LanguageService.extractionCompletedOutputMessage": "下載語言伺服器完成",
- "LanguageService.extractionDoneOutputMessage": "完成",
- "LanguageService.reloadVSCodeIfSeachPathHasChanged": "已為此 Python 解譯器變更搜尋路徑。請重新載入延伸模組以確保 IntelliSense 能夠正常運作",
- "AttachProcess.unsupportedOS": "不支援 '{0}' 作業系統。",
- "AttachProcess.attachTitle": "連結至處理程序",
- "AttachProcess.selectProcessPlaceholder": "選擇要連結的處理程序",
- "AttachProcess.noProcessSelected": "沒有選取的處理程序",
- "AttachProcess.refreshList": "重新整理處理程序列表",
- "diagnostics.updateSettings": "是,更新設定",
- "Common.noIWillDoItLater": "否,我稍候再做",
- "Common.notNow": "先不要",
- "Common.gotIt": "懂了!",
- "Interpreters.selectInterpreterTip": "小提示:您能透過按下狀態列中的 Python 版本,變更 Python 延伸模組使用的 Python 解譯器。",
- "downloading.file": "正在下載 {0}...",
- "downloading.file.progress": "目前 {0}{1},總共 {2} KB ({3}%)",
- "products.installingModule": "正在安裝 {0}"
-}
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/pythonExtensionApi/src/main.ts b/pythonExtensionApi/src/main.ts
new file mode 100644
index 000000000000..2173245cbb28
--- /dev/null
+++ b/pythonExtensionApi/src/main.ts
@@ -0,0 +1,348 @@
+// 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'
+ | '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/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/completion.py b/pythonFiles/completion.py
deleted file mode 100644
index 5a82d8177ecf..000000000000
--- a/pythonFiles/completion.py
+++ /dev/null
@@ -1,692 +0,0 @@
-import os
-import os.path
-import io
-import re
-import sys
-import json
-import traceback
-import platform
-
-jediPreview = False
-
-
-class RedirectStdout(object):
- def __init__(self, new_stdout=None):
- """If stdout is None, redirect to /dev/null"""
- self._new_stdout = new_stdout or open(os.devnull, "w")
-
- def __enter__(self):
- sys.stdout.flush()
- self.oldstdout_fno = os.dup(sys.stdout.fileno())
- os.dup2(self._new_stdout.fileno(), 1)
-
- def __exit__(self, exc_type, exc_value, traceback):
- self._new_stdout.flush()
- os.dup2(self.oldstdout_fno, 1)
- os.close(self.oldstdout_fno)
-
-
-class JediCompletion(object):
- basic_types = {
- "module": "import",
- "instance": "variable",
- "statement": "value",
- "param": "variable",
- }
-
- def __init__(self):
- self.default_sys_path = sys.path
- self._input = io.open(sys.stdin.fileno(), encoding="utf-8")
- if (os.path.sep == "/") and (platform.uname()[2].find("Microsoft") > -1):
- # WSL; does not support UNC paths
- self.drive_mount = "/mnt/"
- elif sys.platform == "cygwin":
- # cygwin
- self.drive_mount = "/cygdrive/"
- else:
- # Do no normalization, e.g. Windows build of Python.
- # Could add additional test: ((os.path.sep == '/') and os.path.isdir('/mnt/c'))
- # However, this may have more false positives trying to identify Windows/*nix hybrids
- self.drive_mount = ""
-
- def _get_definition_type(self, definition):
- # if definition.type not in ['import', 'keyword'] and is_built_in():
- # return 'builtin'
- try:
- if definition.type in ["statement"] and definition.name.isupper():
- return "constant"
- return self.basic_types.get(definition.type, definition.type)
- except Exception:
- return "builtin"
-
- def _additional_info(self, completion):
- """Provide additional information about the completion object."""
- if not hasattr(completion, "_definition") or completion._definition is None:
- return ""
- if completion.type == "statement":
- nodes_to_display = ["InstanceElement", "String", "Node", "Lambda", "Number"]
- return "".join(
- c.get_code()
- for c in completion._definition.children
- if type(c).__name__ in nodes_to_display
- ).replace("\n", "")
- return ""
-
- @classmethod
- def _get_top_level_module(cls, path):
- """Recursively walk through directories looking for top level module.
-
- Jedi will use current filepath to look for another modules at same
- path, but it will not be able to see modules **above**, so our goal
- is to find the higher python module available from filepath.
- """
- _path, _ = os.path.split(path)
- if os.path.isfile(os.path.join(_path, "__init__.py")):
- return cls._get_top_level_module(_path)
- return path
-
- def _generate_signature(self, completion):
- """Generate signature with function arguments."""
- if completion.type in ["module"] or not hasattr(completion, "params"):
- return ""
- return "%s(%s)" % (
- completion.name,
- ", ".join(p.description[6:] for p in completion.params if p),
- )
-
- def _get_call_signatures(self, script):
- """Extract call signatures from jedi.api.Script object in failsafe way.
-
- Returns:
- Tuple with original signature object, name and value.
- """
- _signatures = []
- try:
- call_signatures = script.call_signatures()
- except KeyError:
- call_signatures = []
- except:
- call_signatures = []
- for signature in call_signatures:
- for pos, param in enumerate(signature.params):
- if not param.name:
- continue
-
- name = self._get_param_name(param)
- if param.name == "self" and pos == 0:
- continue
- if name.startswith("*"):
- continue
-
- value = self._get_param_value(param)
- _signatures.append((signature, name, value))
- return _signatures
-
- def _get_param_name(self, p):
- if p.name.startswith("param "):
- return p.name[6:] # drop leading 'param '
- return p.name
-
- def _get_param_value(self, p):
- pair = p.description.split("=")
- if len(pair) > 1:
- return pair[1]
- return None
-
- def _get_call_signatures_with_args(self, script):
- """Extract call signatures from jedi.api.Script object in failsafe way.
-
- Returns:
- Array with dictionary
- """
- _signatures = []
- try:
- call_signatures = script.call_signatures()
- except KeyError:
- call_signatures = []
- for signature in call_signatures:
- sig = {
- "name": "",
- "description": "",
- "docstring": "",
- "paramindex": 0,
- "params": [],
- "bracketstart": [],
- }
- sig["description"] = signature.description
- try:
- sig["docstring"] = signature.docstring()
- sig["raw_docstring"] = signature.docstring(raw=True)
- except Exception:
- sig["docstring"] = ""
- sig["raw_docstring"] = ""
-
- sig["name"] = signature.name
- sig["paramindex"] = signature.index
- sig["bracketstart"].append(signature.index)
-
- _signatures.append(sig)
- for pos, param in enumerate(signature.params):
- if not param.name:
- continue
-
- name = self._get_param_name(param)
- if param.name == "self" and pos == 0:
- continue
-
- value = self._get_param_value(param)
- paramDocstring = ""
- try:
- paramDocstring = param.docstring()
- except Exception:
- paramDocstring = ""
-
- sig["params"].append(
- {
- "name": name,
- "value": value,
- "docstring": paramDocstring,
- "description": param.description,
- }
- )
- return _signatures
-
- def _serialize_completions(self, script, identifier=None, prefix=""):
- """Serialize response to be read from VSCode.
-
- Args:
- script: Instance of jedi.api.Script object.
- identifier: Unique completion identifier to pass back to VSCode.
- prefix: String with prefix to filter function arguments.
- Used only when fuzzy matcher turned off.
-
- Returns:
- Serialized string to send to VSCode.
- """
- _completions = []
-
- for signature, name, value in self._get_call_signatures(script):
- if not self.fuzzy_matcher and not name.lower().startswith(prefix.lower()):
- continue
- _completion = {
- "type": "property",
- "raw_type": "",
- "rightLabel": self._additional_info(signature),
- }
- _completion["description"] = ""
- _completion["raw_docstring"] = ""
-
- # we pass 'text' here only for fuzzy matcher
- if value:
- _completion["snippet"] = "%s=${1:%s}$0" % (name, value)
- _completion["text"] = "%s=" % (name)
- else:
- _completion["snippet"] = "%s=$1$0" % name
- _completion["text"] = name
- _completion["displayText"] = name
- _completions.append(_completion)
-
- try:
- completions = script.completions()
- except KeyError:
- completions = []
- except:
- completions = []
- for completion in completions:
- try:
- _completion = {
- "text": completion.name,
- "type": self._get_definition_type(completion),
- "raw_type": completion.type,
- "rightLabel": self._additional_info(completion),
- }
- except Exception:
- continue
-
- for c in _completions:
- if c["text"] == _completion["text"]:
- c["type"] = _completion["type"]
- c["raw_type"] = _completion["raw_type"]
-
- if any(
- [c["text"].split("=")[0] == _completion["text"] for c in _completions]
- ):
- # ignore function arguments we already have
- continue
- _completions.append(_completion)
- return json.dumps({"id": identifier, "results": _completions})
-
- def _serialize_methods(self, script, identifier=None, prefix=""):
- _methods = []
- try:
- completions = script.completions()
- except KeyError:
- return []
-
- for completion in completions:
- if completion.name == "__autocomplete_python":
- instance = completion.parent().name
- break
- else:
- instance = "self.__class__"
-
- for completion in completions:
- params = []
- if hasattr(completion, "params"):
- params = [p.description for p in completion.params if p]
- if completion.parent().type == "class":
- _methods.append(
- {
- "parent": completion.parent().name,
- "instance": instance,
- "name": completion.name,
- "params": params,
- "moduleName": completion.module_name,
- "fileName": completion.module_path,
- "line": completion.line,
- "column": completion.column,
- }
- )
- return json.dumps({"id": identifier, "results": _methods})
-
- def _serialize_arguments(self, script, identifier=None):
- """Serialize response to be read from VSCode.
-
- Args:
- script: Instance of jedi.api.Script object.
- identifier: Unique completion identifier to pass back to VSCode.
-
- Returns:
- Serialized string to send to VSCode.
- """
- return json.dumps(
- {"id": identifier, "results": self._get_call_signatures_with_args(script)}
- )
-
- def _top_definition(self, definition):
- for d in definition.goto_assignments():
- if d == definition:
- continue
- if d.type == "import":
- return self._top_definition(d)
- else:
- return d
- return definition
-
- def _extract_range_jedi_0_11_1(self, definition):
- from parso.utils import split_lines
-
- # get the scope range
- try:
- if definition.type in ["class", "function"]:
- tree_name = definition._name.tree_name
- scope = tree_name.get_definition()
- start_line = scope.start_pos[0] - 1
- start_column = scope.start_pos[1]
- # get the lines
- code = scope.get_code(include_prefix=False)
- lines = split_lines(code)
- # trim the lines
- lines = "\n".join(lines).rstrip().split("\n")
- end_line = start_line + len(lines) - 1
- end_column = len(lines[-1]) - 1
- else:
- symbol = definition._name.tree_name
- start_line = symbol.start_pos[0] - 1
- start_column = symbol.start_pos[1]
- end_line = symbol.end_pos[0] - 1
- end_column = symbol.end_pos[1]
- return {
- "start_line": start_line,
- "start_column": start_column,
- "end_line": end_line,
- "end_column": end_column,
- }
- except Exception as e:
- return {
- "start_line": definition.line - 1,
- "start_column": definition.column,
- "end_line": definition.line - 1,
- "end_column": definition.column,
- }
-
- def _extract_range(self, definition):
- """Provides the definition range of a given definition
-
- For regular symbols it returns the start and end location of the
- characters making up the symbol.
-
- For scoped containers it will return the entire definition of the
- scope.
-
- The scope that jedi provides ends with the first character of the next
- scope so it's not ideal. For vscode we need the scope to end with the
- last character of actual code. That's why we extract the lines that
- make up our scope and trim the trailing whitespace.
- """
- return self._extract_range_jedi_0_11_1(definition)
-
- def _get_definitionsx(self, definitions, identifier=None, ignoreNoModulePath=False):
- """Serialize response to be read from VSCode.
-
- Args:
- definitions: List of jedi.api.classes.Definition objects.
- identifier: Unique completion identifier to pass back to VSCode.
-
- Returns:
- Serialized string to send to VSCode.
- """
- _definitions = []
- for definition in definitions:
- try:
- if definition.type == "import":
- definition = self._top_definition(definition)
- definitionRange = {
- "start_line": 0,
- "start_column": 0,
- "end_line": 0,
- "end_column": 0,
- }
- module_path = ""
- if hasattr(definition, "module_path") and definition.module_path:
- module_path = definition.module_path
- definitionRange = self._extract_range(definition)
- else:
- if not ignoreNoModulePath:
- continue
- try:
- parent = definition.parent()
- container = parent.name if parent.type != "module" else ""
- except Exception:
- container = ""
-
- try:
- docstring = definition.docstring()
- rawdocstring = definition.docstring(raw=True)
- except Exception:
- docstring = ""
- rawdocstring = ""
- _definition = {
- "text": definition.name,
- "type": self._get_definition_type(definition),
- "raw_type": definition.type,
- "fileName": module_path,
- "container": container,
- "range": definitionRange,
- "description": definition.description,
- "docstring": docstring,
- "raw_docstring": rawdocstring,
- "signature": self._generate_signature(definition),
- }
- _definitions.append(_definition)
- except Exception as e:
- pass
- return _definitions
-
- def _serialize_definitions(self, definitions, identifier=None):
- """Serialize response to be read from VSCode.
-
- Args:
- definitions: List of jedi.api.classes.Definition objects.
- identifier: Unique completion identifier to pass back to VSCode.
-
- Returns:
- Serialized string to send to VSCode.
- """
- _definitions = []
- for definition in definitions:
- try:
- if definition.module_path:
- if definition.type == "import":
- definition = self._top_definition(definition)
- if not definition.module_path:
- continue
- try:
- parent = definition.parent()
- container = parent.name if parent.type != "module" else ""
- except Exception:
- container = ""
-
- try:
- docstring = definition.docstring()
- rawdocstring = definition.docstring(raw=True)
- except Exception:
- docstring = ""
- rawdocstring = ""
- _definition = {
- "text": definition.name,
- "type": self._get_definition_type(definition),
- "raw_type": definition.type,
- "fileName": definition.module_path,
- "container": container,
- "range": self._extract_range(definition),
- "description": definition.description,
- "docstring": docstring,
- "raw_docstring": rawdocstring,
- }
- _definitions.append(_definition)
- except Exception as e:
- pass
- return json.dumps({"id": identifier, "results": _definitions})
-
- def _serialize_tooltip(self, definitions, identifier=None):
- _definitions = []
- for definition in definitions:
- signature = definition.name
- description = None
- if definition.type in ["class", "function"]:
- signature = self._generate_signature(definition)
- try:
- description = definition.docstring(raw=True).strip()
- except Exception:
- description = ""
- if not description and not hasattr(definition, "get_line_code"):
- # jedi returns an empty string for compiled objects
- description = definition.docstring().strip()
- if definition.type == "module":
- signature = definition.full_name
- try:
- description = definition.docstring(raw=True).strip()
- except Exception:
- description = ""
- if not description and hasattr(definition, "get_line_code"):
- # jedi returns an empty string for compiled objects
- description = definition.docstring().strip()
- _definition = {
- "type": self._get_definition_type(definition),
- "text": definition.name,
- "description": description,
- "docstring": description,
- "signature": signature,
- }
- _definitions.append(_definition)
- return json.dumps({"id": identifier, "results": _definitions})
-
- def _serialize_usages(self, usages, identifier=None):
- _usages = []
- for usage in usages:
- _usages.append(
- {
- "name": usage.name,
- "moduleName": usage.module_name,
- "fileName": usage.module_path,
- "line": usage.line,
- "column": usage.column,
- }
- )
- return json.dumps({"id": identifier, "results": _usages})
-
- def _deserialize(self, request):
- """Deserialize request from VSCode.
-
- Args:
- request: String with raw request from VSCode.
-
- Returns:
- Python dictionary with request data.
- """
- return json.loads(request)
-
- def _set_request_config(self, config):
- """Sets config values for current request.
-
- This includes sys.path modifications which is getting restored to
- default value on each request so each project should be isolated
- from each other.
-
- Args:
- config: Dictionary with config values.
- """
- sys.path = self.default_sys_path
- self.use_snippets = config.get("useSnippets")
- self.show_doc_strings = config.get("showDescriptions", True)
- self.fuzzy_matcher = config.get("fuzzyMatcher", False)
- jedi.settings.case_insensitive_completion = config.get(
- "caseInsensitiveCompletion", True
- )
- for path in config.get("extraPaths", []):
- if path and path not in sys.path:
- sys.path.insert(0, path)
-
- def _normalize_request_path(self, request):
- """Normalize any Windows paths received by a *nix build of
- Python. Does not alter the reverse os.path.sep=='\\',
- i.e. *nix paths received by a Windows build of Python.
- """
- if "path" in request:
- if not self.drive_mount:
- return
- newPath = request["path"].replace("\\", "/")
- if newPath[0:1] == "/":
- # is absolute path with no drive letter
- request["path"] = newPath
- elif newPath[1:2] == ":":
- # is path with drive letter, only absolute can be mapped
- request["path"] = self.drive_mount + newPath[0:1].lower() + newPath[2:]
- else:
- # is relative path
- request["path"] = newPath
-
- def _process_request(self, request):
- """Accept serialized request from VSCode and write response."""
- request = self._deserialize(request)
-
- self._set_request_config(request.get("config", {}))
-
- self._normalize_request_path(request)
- path = self._get_top_level_module(request.get("path", ""))
- if len(path) > 0 and path not in sys.path:
- sys.path.insert(0, path)
- lookup = request.get("lookup", "completions")
-
- if lookup == "names":
- return self._serialize_definitions(
- jedi.Script(
- source=request.get("source", None), path=request.get("path", "")
- ).get_names(all_scopes=True),
- request["id"],
- )
-
- script = jedi.Script(
- source=request.get("source", None),
- line=request["line"] + 1,
- column=request["column"],
- path=request.get("path", ""),
- project=jedi.get_default_project(os.path.dirname(path)),
- sys_path=sys.path,
- )
-
- if lookup == "definitions":
- defs = self._get_definitionsx(
- script.goto_assignments(follow_imports=True), request["id"]
- )
- return json.dumps({"id": request["id"], "results": defs})
- if lookup == "tooltip":
- if jediPreview:
- defs = []
- try:
- defs = self._get_definitionsx(
- script.goto_definitions(), request["id"], True
- )
- except:
- pass
- try:
- if len(defs) == 0:
- defs = self._get_definitionsx(
- script.goto_assignments(), request["id"], True
- )
- except:
- pass
- return json.dumps({"id": request["id"], "results": defs})
- else:
- try:
- return self._serialize_tooltip(
- script.goto_definitions(), request["id"]
- )
- except:
- return json.dumps({"id": request["id"], "results": []})
- elif lookup == "arguments":
- return self._serialize_arguments(script, request["id"])
- elif lookup == "usages":
- return self._serialize_usages(script.usages(), request["id"])
- elif lookup == "methods":
- return self._serialize_methods(
- script, request["id"], request.get("prefix", "")
- )
- else:
- return self._serialize_completions(
- script, request["id"], request.get("prefix", "")
- )
-
- def _write_response(self, response):
- sys.stdout.write(response + "\n")
- sys.stdout.flush()
-
- def watch(self):
- while True:
- try:
- rq = self._input.readline()
- if len(rq) == 0:
- # Reached EOF - indication our parent process is gone.
- sys.stderr.write(
- "Received EOF from the standard input,exiting" + "\n"
- )
- sys.stderr.flush()
- return
- with RedirectStdout():
- response = self._process_request(rq)
- self._write_response(response)
-
- except Exception:
- sys.stderr.write(traceback.format_exc() + "\n")
- sys.stderr.flush()
-
-
-if __name__ == "__main__":
- cachePrefix = "v"
- modulesToLoad = ""
- if len(sys.argv) > 2 and sys.argv[1] == "custom":
- jediPath = sys.argv[2]
- jediPreview = True
- cachePrefix = "custom_v"
- if len(sys.argv) > 3:
- modulesToLoad = sys.argv[3]
- else:
- # release
- jediPath = os.path.join(os.path.dirname(__file__), "lib", "python")
- if len(sys.argv) > 1:
- modulesToLoad = sys.argv[1]
-
- sys.path.insert(0, jediPath)
- import jedi
-
- if jediPreview:
- jedi.settings.cache_directory = os.path.join(
- jedi.settings.cache_directory,
- cachePrefix + jedi.__version__.replace(".", ""),
- )
- # remove jedi from path after we import it so it will not be completed
- sys.path.pop(0)
- if len(modulesToLoad) > 0:
- jedi.preload_module(*modulesToLoad.split(","))
- JediCompletion().watch()
diff --git a/pythonFiles/install_debugpy.py b/pythonFiles/install_debugpy.py
deleted file mode 100644
index e9361a6aeb3e..000000000000
--- a/pythonFiles/install_debugpy.py
+++ /dev/null
@@ -1,63 +0,0 @@
-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_VERSIONS = ("cp38",)
-
-
-def _contains(s, parts=()):
- return any(p for p in parts if p in s)
-
-
-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_VERSIONS)
- )
-
-
-def _download_and_extract(root, url, version):
- root = os.getcwd() if root is None or root == "." else root
- prefix = os.path.join("debugpy-{0}.data".format(version), "purelib")
- with url_lib.urlopen(url) as response:
- # Extract only the contents of the purelib subfolder (parent folder of debugpy),
- # since debugpy files rely on the presence of a 'debugpy' folder.
- with zipfile.ZipFile(io.BytesIO(response.read()), "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
- # Normalize path for Windows, the wheel folder structure
- # uses forward slashes.
- normalized = os.path.normpath(zip_info.filename)
- # Flatten the folder structure.
- zip_info.filename = normalized.split(prefix)[-1]
- wheel.extract(zip_info, root)
-
-
-def main(root):
- data = _get_package_data()
- latest_version = max(data["releases"].keys(), key=version_parser)
-
- for url in _get_debugger_wheel_urls(data, latest_version):
- _download_and_extract(root, url, latest_version)
-
-
-if __name__ == "__main__":
- main(DEBUGGER_DEST)
diff --git a/pythonFiles/normalizeSelection.py b/pythonFiles/normalizeSelection.py
deleted file mode 100644
index 621c59b806af..000000000000
--- a/pythonFiles/normalizeSelection.py
+++ /dev/null
@@ -1,125 +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 = [node.lineno - 1 for node in tree.body[1:]] + [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
- 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 52c7c96d11e7..000000000000
--- a/pythonFiles/pyproject.toml
+++ /dev/null
@@ -1,11 +0,0 @@
-[tool.black]
-exclude = '''
-
-(
- /(
- .data
- | .vscode
- | lib
- )/
-)
-'''
diff --git a/pythonFiles/refactor.py b/pythonFiles/refactor.py
deleted file mode 100644
index f9d3b29e3b19..000000000000
--- a/pythonFiles/refactor.py
+++ /dev/null
@@ -1,395 +0,0 @@
-# Arguments are:
-# 1. Working directory.
-# 2. Rope folder
-
-import difflib
-import io
-import json
-import os
-import sys
-import traceback
-
-try:
- import rope
- from rope.base import libutils
- from rope.refactor.rename import Rename
- from rope.refactor.extract import ExtractMethod, ExtractVariable
- import rope.base.project
- import rope.base.taskhandle
-except:
- jsonMessage = {
- "error": True,
- "message": "Rope not installed",
- "traceback": "",
- "type": "ModuleNotFoundError",
- }
- sys.stderr.write(json.dumps(jsonMessage))
- sys.stderr.flush()
-
-WORKSPACE_ROOT = sys.argv[1]
-ROPE_PROJECT_FOLDER = ".vscode/.ropeproject"
-
-
-class RefactorProgress:
- """
- Refactor progress information
- """
-
- def __init__(self, name="Task Name", message=None, percent=0):
- self.name = name
- self.message = message
- self.percent = percent
-
-
-class ChangeType:
- """
- Change Type Enum
- """
-
- EDIT = 0
- NEW = 1
- DELETE = 2
-
-
-class Change:
- """ """
-
- EDIT = 0
- NEW = 1
- DELETE = 2
-
- def __init__(self, filePath, fileMode=ChangeType.EDIT, diff=""):
- self.filePath = filePath
- self.diff = diff
- self.fileMode = fileMode
-
-
-def get_diff(changeset):
- """This is a copy of the code form the ChangeSet.get_description method found in Rope."""
- new = changeset.new_contents
- old = changeset.old_contents
- if old is None:
- if changeset.resource.exists():
- old = changeset.resource.read()
- else:
- old = ""
-
- # Ensure code has a trailing empty lines, before generating a diff.
- # https://github.com/Microsoft/vscode-python/issues/695.
- old_lines = old.splitlines(True)
- if not old_lines[-1].endswith("\n"):
- old_lines[-1] = old_lines[-1] + os.linesep
- new = new + os.linesep
-
- result = difflib.unified_diff(
- old_lines,
- new.splitlines(True),
- "a/" + changeset.resource.path,
- "b/" + changeset.resource.path,
- )
- return "".join(list(result))
-
-
-class BaseRefactoring(object):
- """
- Base class for refactorings
- """
-
- def __init__(self, project, resource, name="Refactor", progressCallback=None):
- self._progressCallback = progressCallback
- self._handle = rope.base.taskhandle.TaskHandle(name)
- self._handle.add_observer(self._update_progress)
- self.project = project
- self.resource = resource
- self.changes = []
-
- def _update_progress(self):
- jobset = self._handle.current_jobset()
- if jobset and not self._progressCallback is None:
- progress = RefactorProgress()
- # getting current job set name
- if jobset.get_name() is not None:
- progress.name = jobset.get_name()
- # getting active job name
- if jobset.get_active_job_name() is not None:
- progress.message = jobset.get_active_job_name()
- # adding done percent
- percent = jobset.get_percent_done()
- if percent is not None:
- progress.percent = percent
- if not self._progressCallback is None:
- self._progressCallback(progress)
-
- def stop(self):
- self._handle.stop()
-
- def refactor(self):
- try:
- self.onRefactor()
- except rope.base.exceptions.InterruptedTaskError:
- # we can ignore this exception, as user has cancelled refactoring
- pass
-
- def onRefactor(self):
- """
- To be implemented by each base class
- """
- pass
-
-
-class RenameRefactor(BaseRefactoring):
- def __init__(
- self,
- project,
- resource,
- name="Rename",
- progressCallback=None,
- startOffset=None,
- newName="new_Name",
- ):
- BaseRefactoring.__init__(self, project, resource, name, progressCallback)
- self._newName = newName
- self.startOffset = startOffset
-
- def onRefactor(self):
- renamed = Rename(self.project, self.resource, self.startOffset)
- changes = renamed.get_changes(self._newName, task_handle=self._handle)
- for item in changes.changes:
- if isinstance(item, rope.base.change.ChangeContents):
- self.changes.append(
- Change(item.resource.real_path, ChangeType.EDIT, get_diff(item))
- )
- else:
- raise Exception("Unknown Change")
-
-
-class ExtractVariableRefactor(BaseRefactoring):
- def __init__(
- self,
- project,
- resource,
- name="Extract Variable",
- progressCallback=None,
- startOffset=None,
- endOffset=None,
- newName="new_Name",
- similar=False,
- global_=False,
- ):
- BaseRefactoring.__init__(self, project, resource, name, progressCallback)
- self._newName = newName
- self._startOffset = startOffset
- self._endOffset = endOffset
- self._similar = similar
- self._global = global_
-
- def onRefactor(self):
- renamed = ExtractVariable(
- self.project, self.resource, self._startOffset, self._endOffset
- )
- changes = renamed.get_changes(self._newName, self._similar, self._global)
- for item in changes.changes:
- if isinstance(item, rope.base.change.ChangeContents):
- self.changes.append(
- Change(item.resource.real_path, ChangeType.EDIT, get_diff(item))
- )
- else:
- raise Exception("Unknown Change")
-
-
-class ExtractMethodRefactor(ExtractVariableRefactor):
- def __init__(
- self,
- project,
- resource,
- name="Extract Method",
- progressCallback=None,
- startOffset=None,
- endOffset=None,
- newName="new_Name",
- similar=False,
- global_=False,
- ):
- ExtractVariableRefactor.__init__(
- self,
- project,
- resource,
- name,
- progressCallback,
- startOffset=startOffset,
- endOffset=endOffset,
- newName=newName,
- similar=similar,
- global_=global_,
- )
-
- def onRefactor(self):
- renamed = ExtractMethod(
- self.project, self.resource, self._startOffset, self._endOffset
- )
- changes = renamed.get_changes(self._newName, self._similar, self._global)
- for item in changes.changes:
- if isinstance(item, rope.base.change.ChangeContents):
- self.changes.append(
- Change(item.resource.real_path, ChangeType.EDIT, get_diff(item))
- )
- else:
- raise Exception("Unknown Change")
-
-
-class RopeRefactoring(object):
- def __init__(self):
- self.default_sys_path = sys.path
- self._input = io.open(sys.stdin.fileno(), encoding="utf-8")
-
- def _rename(self, filePath, start, newName, indent_size):
- """
- Renames a variable
- """
- project = rope.base.project.Project(
- WORKSPACE_ROOT,
- ropefolder=ROPE_PROJECT_FOLDER,
- save_history=False,
- indent_size=indent_size,
- )
- resourceToRefactor = libutils.path_to_resource(project, filePath)
- refactor = RenameRefactor(
- project, resourceToRefactor, startOffset=start, newName=newName
- )
- refactor.refactor()
- changes = refactor.changes
- project.close()
- valueToReturn = []
- for change in changes:
- valueToReturn.append({"diff": change.diff})
- return valueToReturn
-
- def _extractVariable(self, filePath, start, end, newName, indent_size):
- """
- Extracts a variable
- """
- project = rope.base.project.Project(
- WORKSPACE_ROOT,
- ropefolder=ROPE_PROJECT_FOLDER,
- save_history=False,
- indent_size=indent_size,
- )
- resourceToRefactor = libutils.path_to_resource(project, filePath)
- refactor = ExtractVariableRefactor(
- project,
- resourceToRefactor,
- startOffset=start,
- endOffset=end,
- newName=newName,
- similar=True,
- )
- refactor.refactor()
- changes = refactor.changes
- project.close()
- valueToReturn = []
- for change in changes:
- valueToReturn.append({"diff": change.diff})
- return valueToReturn
-
- def _extractMethod(self, filePath, start, end, newName, indent_size):
- """
- Extracts a method
- """
- project = rope.base.project.Project(
- WORKSPACE_ROOT,
- ropefolder=ROPE_PROJECT_FOLDER,
- save_history=False,
- indent_size=indent_size,
- )
- resourceToRefactor = libutils.path_to_resource(project, filePath)
- refactor = ExtractMethodRefactor(
- project,
- resourceToRefactor,
- startOffset=start,
- endOffset=end,
- newName=newName,
- similar=True,
- )
- refactor.refactor()
- changes = refactor.changes
- project.close()
- valueToReturn = []
- for change in changes:
- valueToReturn.append({"diff": change.diff})
- return valueToReturn
-
- def _serialize(self, identifier, results):
- """
- Serializes the refactor results
- """
- return json.dumps({"id": identifier, "results": results})
-
- def _deserialize(self, request):
- """Deserialize request from VSCode.
-
- Args:
- request: String with raw request from VSCode.
-
- Returns:
- Python dictionary with request data.
- """
- return json.loads(request)
-
- def _process_request(self, request):
- """Accept serialized request from VSCode and write response."""
- request = self._deserialize(request)
- lookup = request.get("lookup", "")
-
- if lookup == "":
- pass
- elif lookup == "rename":
- changes = self._rename(
- request["file"],
- int(request["start"]),
- request["name"],
- int(request["indent_size"]),
- )
- return self._write_response(self._serialize(request["id"], changes))
- elif lookup == "extract_variable":
- changes = self._extractVariable(
- request["file"],
- int(request["start"]),
- int(request["end"]),
- request["name"],
- int(request["indent_size"]),
- )
- return self._write_response(self._serialize(request["id"], changes))
- elif lookup == "extract_method":
- changes = self._extractMethod(
- request["file"],
- int(request["start"]),
- int(request["end"]),
- request["name"],
- int(request["indent_size"]),
- )
- return self._write_response(self._serialize(request["id"], changes))
-
- def _write_response(self, response):
- sys.stdout.write(response + "\n")
- sys.stdout.flush()
-
- def watch(self):
- self._write_response("STARTED")
- while True:
- try:
- self._process_request(self._input.readline())
- except:
- exc_type, exc_value, exc_tb = sys.exc_info()
- tb_info = traceback.extract_tb(exc_tb)
- jsonMessage = {
- "error": True,
- "message": str(exc_value),
- "traceback": str(tb_info),
- "type": str(exc_type),
- }
- sys.stderr.write(json.dumps(jsonMessage))
- sys.stderr.flush()
-
-
-if __name__ == "__main__":
- RopeRefactoring().watch()
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/sortImports.py b/pythonFiles/sortImports.py
deleted file mode 100644
index 070f7883fd66..000000000000
--- a/pythonFiles/sortImports.py
+++ /dev/null
@@ -1,14 +0,0 @@
-# Copyright (c) Microsoft Corporation. All rights reserved.
-# Licensed under the MIT License.
-
-import io
-import os
-import os.path
-import sys
-
-isort_path = os.path.join(os.path.dirname(__file__), "lib", "python")
-sys.path.insert(0, isort_path)
-
-import isort.main
-
-isort.main.main()
diff --git a/pythonFiles/symbolProvider.py b/pythonFiles/symbolProvider.py
deleted file mode 100644
index 033ce4b99900..000000000000
--- a/pythonFiles/symbolProvider.py
+++ /dev/null
@@ -1,89 +0,0 @@
-# Copyright (c) Microsoft Corporation. All rights reserved.
-# Licensed under the MIT License.
-
-import ast
-import json
-import sys
-
-
-class Visitor(ast.NodeVisitor):
- def __init__(self):
- self.symbols = {"classes": [], "methods": [], "functions": []}
-
- def visit_Module(self, node):
- self.visitChildren(node)
-
- def visitChildren(self, node, namespace=""):
- for child in node.body:
- if isinstance(child, ast.FunctionDef):
- self.visitDef(child, namespace)
- if isinstance(child, ast.ClassDef):
- self.visitClassDef(child, namespace)
- try:
- if isinstance(child, ast.AsyncFunctionDef):
- self.visitDef(child, namespace)
- except Exception:
- pass
-
- def visitDef(self, node, namespace=""):
- end_position = self.getEndPosition(node)
- symbol = "functions" if namespace == "" else "methods"
- self.symbols[symbol].append(self.getDataObject(node, namespace))
-
- def visitClassDef(self, node, namespace=""):
- end_position = self.getEndPosition(node)
- self.symbols["classes"].append(self.getDataObject(node, namespace))
-
- if len(namespace) > 0:
- namespace = "{0}::{1}".format(namespace, node.name)
- else:
- namespace = node.name
- self.visitChildren(node, namespace)
-
- def getDataObject(self, node, namespace=""):
- end_position = self.getEndPosition(node)
- return {
- "namespace": namespace,
- "name": node.name,
- "range": {
- "start": {"line": node.lineno - 1, "character": node.col_offset},
- "end": {"line": end_position[0], "character": end_position[1]},
- },
- }
-
- def getEndPosition(self, node):
- if not hasattr(node, "body") or len(node.body) == 0:
- return (node.lineno - 1, node.col_offset)
- return self.getEndPosition(node.body[-1])
-
-
-def provide_symbols(source):
- """Provides a list of all symbols in provided code.
-
- The list comprises of 3-item tuples that contain the starting line number,
- ending line number and whether the statement is a single line.
-
- """
- tree = ast.parse(source)
- visitor = Visitor()
- visitor.visit(tree)
- sys.stdout.write(json.dumps(visitor.symbols))
- sys.stdout.flush()
-
-
-if __name__ == "__main__":
- if len(sys.argv) == 3:
- contents = sys.argv[2]
- else:
- with open(sys.argv[1], "r") as source:
- contents = source.read()
-
- try:
- default_encoding = sys.getdefaultencoding()
- encoded_contents = contents.encode(default_encoding, "surrogateescape")
- contents = encoded_contents.decode(default_encoding, "replace")
- except (UnicodeError, LookupError):
- pass
- if isinstance(contents, bytes):
- contents = contents.decode("utf8")
- provide_symbols(contents)
diff --git a/pythonFiles/testing_tools/adapter/__main__.py b/pythonFiles/testing_tools/adapter/__main__.py
deleted file mode 100644
index 5857c63db049..000000000000
--- a/pythonFiles/testing_tools/adapter/__main__.py
+++ /dev/null
@@ -1,106 +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 UnsupportedToolError, UnsupportedCommandError
-
-
-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 f99ce0b6f9a2..000000000000
--- a/pythonFiles/testing_tools/adapter/info.py
+++ /dev/null
@@ -1,120 +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 51c94527302d..000000000000
--- a/pythonFiles/testing_tools/adapter/pytest/_discovery.py
+++ /dev/null
@@ -1,109 +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 util, discovery
-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 != 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 f9ed1fe0f289..000000000000
--- a/pythonFiles/testing_tools/adapter/pytest/_pytest_item.py
+++ /dev/null
@@ -1,604 +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
-import _pytest.doctest
-import _pytest.unittest
-
-from ..info import SingleTestInfo, SingleTestPath
-from ..util import fix_fileid, PATH_SEP, NORMCASE
-
-
-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
- (nodeid, parents, fileid, testfunc, parameterized) = _parse_node_id(
- item.nodeid, kind
- )
- # 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=item.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
-
- if kind == "function" and nodeid.endswith("]"):
- funcid, sep, parameterized = nodeid.partition("[")
- if not sep:
- raise should_never_reach_here(
- nodeid,
- # ...
- )
- yield (nodeid, sep + parameterized, "subtest")
- nodeid = funcid
-
- 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("::()::", "::")
- 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 77778c5b6126..000000000000
--- a/pythonFiles/testing_tools/adapter/util.py
+++ /dev/null
@@ -1,287 +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
-
-
-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/testlauncher.py b/pythonFiles/testlauncher.py
deleted file mode 100644
index 25b56b7c0604..000000000000
--- a/pythonFiles/testlauncher.py
+++ /dev/null
@@ -1,51 +0,0 @@
-# Copyright (c) Microsoft Corporation. All rights reserved.
-# Licensed under the MIT License.
-
-import os
-import sys
-
-
-def parse_argv():
- """Parses arguments for use with the test launcher.
- Arguments are:
- 1. Working directory.
- 2. Test runner, `pytest` or `nose`
- 3. Rest of the arguments are passed into the test runner.
- """
- cwd = sys.argv[1]
- testRunner = sys.argv[2]
- args = sys.argv[3:]
- if testRunner == "nose":
- # Nose expects the program name to be first argument in vargs
- args.insert(0, sys.argv[0])
-
- return (cwd, testRunner, args)
-
-
-def run(cwd, testRunner, args):
- """Runs the test
- cwd -- the current directory to be set
- testRunner -- test runner to be used `pytest` or `nose`
- args -- arguments passed into the test runner
- """
-
- sys.path[0] = os.getcwd()
- os.chdir(cwd)
-
- try:
- if testRunner == "pytest":
- import pytest
-
- pytest.main(args)
- else:
- import nose
-
- nose.run(argv=args)
- sys.exit(0)
- finally:
- pass
-
-
-if __name__ == "__main__":
- cwd, testRunner, args = parse_argv()
- run(cwd, testRunner, args)
diff --git a/pythonFiles/tests/__init__.py b/pythonFiles/tests/__init__.py
deleted file mode 100644
index 4f762cd1f81a..000000000000
--- a/pythonFiles/tests/__init__.py
+++ /dev/null
@@ -1,11 +0,0 @@
-# Copyright (c) Microsoft Corporation. All rights reserved.
-# Licensed under the MIT License.
-import os.path
-
-TEST_ROOT = os.path.dirname(__file__)
-SRC_ROOT = os.path.dirname(TEST_ROOT)
-PROJECT_ROOT = os.path.dirname(SRC_ROOT)
-TESTING_TOOLS_ROOT = os.path.join(SRC_ROOT, "testing_tools")
-DEBUG_ADAPTER_ROOT = os.path.join(SRC_ROOT, "debug_adapter")
-
-PYTHONFILES = os.path.join(SRC_ROOT, "lib", "python")
diff --git a/pythonFiles/tests/__main__.py b/pythonFiles/tests/__main__.py
deleted file mode 100644
index 901385d41d87..000000000000
--- a/pythonFiles/tests/__main__.py
+++ /dev/null
@@ -1,58 +0,0 @@
-# Copyright (c) Microsoft Corporation. All rights reserved.
-# Licensed under the MIT License.
-
-import argparse
-import sys
-
-import pytest
-
-from . import DEBUG_ADAPTER_ROOT, SRC_ROOT, TEST_ROOT, TESTING_TOOLS_ROOT
-
-
-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(
- "--no-functional", dest="markers", action="append_const", const="not functional"
- )
- args, remainder = parser.parse_known_args()
-
- ns = vars(args)
-
- if remainder:
- for arg in remainder:
- if arg.startswith("-") and arg not in ("-v", "--verbose", "-h", "--help"):
- specific = False
- break
- else:
- specific = True
- else:
- specific = False
- args.specific = specific
-
- return ns, remainder
-
-
-def main(pytestargs, markers=None, specific=False):
- sys.path.insert(1, TESTING_TOOLS_ROOT)
- sys.path.insert(1, DEBUG_ADAPTER_ROOT)
-
- if not specific:
- pytestargs.insert(0, TEST_ROOT)
- pytestargs.insert(0, "--rootdir")
- pytestargs.insert(1, SRC_ROOT)
- for marker in reversed(markers or ()):
- pytestargs.insert(0, marker)
- pytestargs.insert(0, "-m")
-
- ec = pytest.main(pytestargs)
- return ec
-
-
-if __name__ == "__main__":
- mainkwargs, pytestargs = parse_args()
- ec = main(pytestargs, **mainkwargs)
- sys.exit(ec)
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/run_all.py b/pythonFiles/tests/run_all.py
deleted file mode 100644
index ce5a62649962..000000000000
--- a/pythonFiles/tests/run_all.py
+++ /dev/null
@@ -1,16 +0,0 @@
-# Copyright (c) Microsoft Corporation. All rights reserved.
-# Licensed under the MIT License.
-
-# Replace the "." entry.
-import os.path
-import sys
-
-sys.path[0] = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
-
-from tests.__main__ import main, parse_args
-
-
-if __name__ == "__main__":
- mainkwargs, pytestargs = parse_args()
- ec = main(pytestargs, **mainkwargs)
- sys.exit(ec)
diff --git a/pythonFiles/tests/test_normalize_selection.py b/pythonFiles/tests/test_normalize_selection.py
deleted file mode 100644
index 6d852e9ae43b..000000000000
--- a/pythonFiles/tests/test_normalize_selection.py
+++ /dev/null
@@ -1,181 +0,0 @@
-# Copyright (c) Microsoft Corporation. All rights reserved.
-# Licensed under the MIT License.
-
-import textwrap
-
-import normalizeSelection
-
-
-class TestNormalizationScript(object):
- """Unit tests for the normalization script."""
-
- def test_basicNormalization(self):
- src = 'print("this is a test")'
- expected = src + "\n"
- result = normalizeSelection.normalize_lines(src)
- assert result == expected
-
- def test_moreThanOneLine(self):
- src = textwrap.dedent(
- """\
- # Some rando comment
-
- def show_something():
- print("Something")
- """
- )
- expected = textwrap.dedent(
- """\
- def show_something():
- print("Something")
-
- """
- )
- result = normalizeSelection.normalize_lines(src)
- assert result == expected
-
- def test_withHangingIndent(self):
- src = textwrap.dedent(
- """\
- x = 22
- y = 30
- z = -10
- result = x + y + z
-
- if result == 42:
- print("The answer to life, the universe, and everything")
- """
- )
- expected = textwrap.dedent(
- """\
- x = 22
- y = 30
- z = -10
- result = x + y + z
- if result == 42:
- print("The answer to life, the universe, and everything")
-
- """
- )
- result = normalizeSelection.normalize_lines(src)
- assert result == expected
-
- def test_clearOutExtraneousNewlines(self):
- src = textwrap.dedent(
- """\
- value_x = 22
-
- value_y = 30
-
- value_z = -10
-
- print(value_x + value_y + value_z)
-
- """
- )
- expected = textwrap.dedent(
- """\
- value_x = 22
- value_y = 30
- value_z = -10
- print(value_x + value_y + value_z)
- """
- )
- result = normalizeSelection.normalize_lines(src)
- assert result == expected
-
- def test_clearOutExtraLinesAndWhitespace(self):
- src = textwrap.dedent(
- """\
- if True:
- x = 22
-
- y = 30
-
- z = -10
-
- print(x + y + z)
-
- """
- )
- expected = textwrap.dedent(
- """\
- if True:
- x = 22
- y = 30
- z = -10
-
- print(x + y + z)
- """
- )
- result = normalizeSelection.normalize_lines(src)
- assert result == expected
-
- def test_partialSingleLine(self):
- src = " print('foo')"
- expected = textwrap.dedent(src) + "\n"
- result = normalizeSelection.normalize_lines(src)
- assert result == expected
-
- def test_multiLineWithIndent(self):
- src = """\
-
- if (x > 0
- and condition == True):
- print('foo')
- else:
-
- print('bar')
- """
-
- expected = textwrap.dedent(
- """\
- if (x > 0
- and condition == True):
- print('foo')
- else:
- print('bar')
-
- """
- )
-
- result = normalizeSelection.normalize_lines(src)
- assert result == expected
-
- def test_multiLineWithComment(self):
- src = textwrap.dedent(
- """\
-
- def show_something():
- # A comment
- print("Something")
- """
- )
- expected = textwrap.dedent(
- """\
- def show_something():
- # A comment
- print("Something")
-
- """
- )
- result = normalizeSelection.normalize_lines(src)
- assert result == expected
-
- def test_exception(self):
- src = " if True:"
- expected = src + "\n\n"
- result = normalizeSelection.normalize_lines(src)
- assert result == expected
-
- def test_multilineException(self):
- src = textwrap.dedent(
- """\
-
- def show_something():
- if True:
- """
- )
- expected = src + "\n\n"
- result = normalizeSelection.normalize_lines(src)
- assert result == expected
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 8ef4305f40b9..000000000000
--- a/pythonFiles/tests/testing_tools/adapter/pytest/test_discovery.py
+++ /dev/null
@@ -1,1553 +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
-import _pytest.doctest
-
-from .... import util
-from testing_tools.adapter import util as adapter_util
-from testing_tools.adapter.pytest import _pytest_item as pytest_item
-from testing_tools.adapter import info
-from testing_tools.adapter.pytest import _discovery
-
-# In Python 3.8 __len__ is called twice, which impacts some of the test assertions we do below.
-PYTHON_38_OR_LATER = sys.version_info[0] >= 3 and sys.version_info[1] >= 8
-
-
-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),
- ]
-
- # In Python 3.8 __len__ is called twice.
- if PYTHON_38_OR_LATER:
- calls.insert(3, ("discovered.__len__", None, None))
-
- parents, tests = _discovery.discover(
- [], _pytest_main=stubpytest.main, _plugin=plugin
- )
-
- self.assertEqual(parents, [])
- self.assertEqual(tests, expected)
- self.assertEqual(stub.calls, 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),
- ]
-
- # In Python 3.8 __len__ is called twice.
- if PYTHON_38_OR_LATER:
- calls.insert(3, ("discovered.__len__", None, None))
-
- parents, tests = _discovery.discover(
- [], _pytest_main=pytest.main, _plugin=plugin
- )
-
- self.assertEqual(parents, [])
- self.assertEqual(tests, expected)
- self.assertEqual(stub.calls, 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"
-
- # In Python 3.8 __len__ is called twice.
- if PYTHON_38_OR_LATER:
- calls.insert(3, ("discovered.__len__", None, None))
-
- # 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()
-
- self.assertEqual(captured, "")
- self.assertEqual(stub.calls, 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"
-
- # In Python 3.8 __len__ is called twice.
- if PYTHON_38_OR_LATER:
- calls.insert(3, ("discovered.__len__", None, None))
-
- 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()
-
- self.assertEqual(captured, pytest_stdout)
- self.assertEqual(stub.calls, 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.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",
- location=("test_spam.py", 12, "SpamTests.test_one"),
- fspath=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",
- location=("test_spam.py", 19, "SpamTests.test_other"),
- fspath=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",
- location=("test_spam.py", 144, "test_all"),
- fspath=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]",
- location=("test_spam.py", 273, "test_each[10-10]"),
- fspath=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",
- location=(relfile2, 31, "All.BasicTests.test_first"),
- fspath=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]",
- location=(relfile2, 62, "All.BasicTests.test_each[1+2-3]"),
- fspath=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
- self.assertEqual(
- stub.calls,
- [
- ("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="test_each[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="test_each[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",
- ),
- ),
- ),
- ],
- )
-
- def test_finish(self):
- stub = util.Stub()
- discovered = StubDiscoveredTests(stub)
- session = StubPytestSession(stub)
- testroot = 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",
- location=(relfile, 12, "SpamTests.test_spam"),
- fspath=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.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"),
- fspath=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"),
- fspath=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"),
- fspath=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"),
- fspath=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.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]",
- location=(relfile, 12, "SpamTests.test_spam[a-[b]-c]"),
- fspath=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="test_spam[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.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",
- location=(relfile, 12, "SpamTests.Ham.Eggs.test_spam"),
- fspath=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",
- ),
- ),
- ),
- ],
- )
-
- def test_windows(self):
- stub = util.Stub()
- discovered = StubDiscoveredTests(stub)
- session = StubPytestSession(stub)
- testroot = r"C:\A\B\C"
- altroot = testroot.replace("\\", "/")
- 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",
- # normal path separator (contrast with nodeid):
- location=(relfile, 12, "SpamTests.test_spam"),
- # path separator matches location:
- fspath=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",
- location=(locfile, 12, "test_spam"),
- fspath=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
- self.assertEqual(
- stub.calls,
- [
- ("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"),
- (".", altroot, "folder"),
- ],
- test=info.SingleTestInfo(
- id=r"./X/test_b.py::test_spam",
- name="test_spam",
- path=info.SingleTestPath(
- root=altroot,
- 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"),
- (".", altroot, "folder"),
- ],
- test=info.SingleTestInfo(
- id=r"./X/test_d.py::test_spam",
- name="test_spam",
- path=info.SingleTestPath(
- root=altroot,
- 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"),
- (".", altroot, "folder"),
- ],
- test=info.SingleTestInfo(
- id=r"./X/test_f.py::test_spam",
- name="test_spam",
- path=info.SingleTestPath(
- root=altroot,
- 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"),
- (".", altroot, "folder"),
- ],
- test=info.SingleTestInfo(
- id=r"./X/test_h.py::test_spam",
- name="test_spam",
- path=info.SingleTestPath(
- root=altroot,
- 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",
- ),
- ),
- ),
- ],
- )
-
- def test_mysterious_parens(self):
- stub = util.Stub()
- discovered = StubDiscoveredTests(stub)
- session = StubPytestSession(stub)
- testroot = 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",
- location=(relfile, 12, "SpamTests.test_spam"),
- fspath=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.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",
- location=(srcfile, 12, "SpamTests.test_spam"),
- fspath=adapter_util.PATH_JOIN(testroot, relfile),
- function=FakeFunc("test_spam"),
- ),
- create_stub_function_item(
- stub,
- nodeid=relfile + "::test_ham",
- name="test_ham",
- location=(srcfile, 3, "test_ham"),
- fspath=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 53500a2f4afe..000000000000
--- a/pythonFiles/tests/testing_tools/adapter/test___main__.py
+++ /dev/null
@@ -1,210 +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.__main__ import (
- parse_args,
- main,
- UnsupportedToolError,
- UnsupportedCommandError,
-)
-
-
-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", "nose"])
- 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(
- "nose",
- "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 ec3d198b0108..000000000000
--- a/pythonFiles/tests/testing_tools/adapter/test_discovery.py
+++ /dev/null
@@ -1,675 +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.util import fix_path, fix_relpath
-from testing_tools.adapter.info import SingleTestInfo, SingleTestPath, ParentInfo
-from testing_tools.adapter.discovery import fix_nodeid, DiscoveredTests
-
-
-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 153ad5508d9b..000000000000
--- a/pythonFiles/tests/testing_tools/adapter/test_functional.py
+++ /dev/null
@@ -1,1535 +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 ...__main__ import TESTING_TOOLS_ROOT
-from testing_tools.adapter.util import fix_path, PATH_SEP
-
-# 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": "test_param_01[]",
- "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": "test_param_11[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": "test_param_13[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": "test_param_13[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": "test_param_13[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": "test_param_13_repeat[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": "test_param_13_repeat[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": "test_param_13_repeat[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": "test_param_33[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": "test_param_33[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": "test_param_33[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": "test_param_33_ids[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": "test_param_33_ids[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": "test_param_33_ids[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": "test_param_23_13[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": "test_param_23_13[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": "test_param_23_13[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": "test_param_23_13[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": "test_param_23_13[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": "test_param_23_13[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": "test_param_23_13[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": "test_param_23_13[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": "test_param_23_13[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": "test_param_13_markers[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": "test_param_13_markers[???]",
- "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": "test_param_13_markers[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": "test_param_13_skipped[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": "test_param_13_skipped[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": "test_param_13_skipped[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": "test_param_23_raises[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": "test_param_23_raises[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": "test_param_23_raises[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": "test_param_13[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": "test_param_13[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": "test_param_13[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": "test_param_13[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": "test_param_13[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": "test_param_13[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": "test_spam_13[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": "test_spam_13[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": "test_spam_13[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": "test_param_fixture[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": "test_param_fixture[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": "test_param_fixture[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[x0]",
- "name": "test_param_mark_fixture[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": "test_param_mark_fixture[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_param_mark_fixture[x2]",
- "name": "test_param_mark_fixture[x2]",
- "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": "test_fixture_param[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": "test_fixture_param[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": "test_param_13[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": "test_param_13[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": "test_param_13[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": "test_param_13[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": "test_param_13[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": "test_param_13[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": "test_spam_13[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": "test_spam_13[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": "test_spam_13[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/util.py b/pythonFiles/tests/util.py
deleted file mode 100644
index 45c3536145cf..000000000000
--- a/pythonFiles/tests/util.py
+++ /dev/null
@@ -1,26 +0,0 @@
-# Copyright (c) Microsoft Corporation. All rights reserved.
-# Licensed under the MIT License.
-
-
-class Stub(object):
- def __init__(self):
- self.calls = []
-
- def add_call(self, name, args=None, kwargs=None):
- self.calls.append((name, args, kwargs))
-
-
-class StubProxy(object):
- def __init__(self, stub=None, name=None):
- self.name = name
- self.stub = stub if stub is not None else Stub()
-
- @property
- def calls(self):
- return self.stub.calls
-
- def add_call(self, funcname, *args, **kwargs):
- callname = funcname
- if self.name:
- callname = "{}.{}".format(self.name, funcname)
- return self.stub.add_call(callname, *args, **kwargs)
diff --git a/pythonFiles/visualstudio_py_testlauncher.py b/pythonFiles/visualstudio_py_testlauncher.py
deleted file mode 100644
index 3a43c1e1be34..000000000000
--- a/pythonFiles/visualstudio_py_testlauncher.py
+++ /dev/null
@@ -1,392 +0,0 @@
-# Python Tools for Visual Studio
-# Copyright(c) Microsoft Corporation
-# All rights reserved.
-#
-# 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
-#
-# THIS CODE IS PROVIDED ON AN *AS IS* BASIS, WITHOUT WARRANTIES OR CONDITIONS
-# OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION ANY
-# IMPLIED WARRANTIES OR CONDITIONS OF TITLE, FITNESS FOR A PARTICULAR PURPOSE,
-# MERCHANTABLITY OR NON-INFRINGEMENT.
-#
-# See the Apache Version 2.0 License for specific language governing
-# permissions and limitations under the License.
-
-__author__ = "Microsoft Corporation "
-__version__ = "3.0.0.0"
-
-import os
-import sys
-import json
-import unittest
-import socket
-import traceback
-from types import CodeType, FunctionType
-import signal
-
-try:
- import thread
-except:
- import _thread as thread
-
-
-class _TestOutput(object):
- """file like object which redirects output to the repl window."""
-
- errors = "strict"
-
- 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"):
- self.buffer = _TestOutputBuffer(old_out.buffer, is_stdout)
-
- def flush(self):
- if self.old_out:
- self.old_out.flush()
-
- def writelines(self, lines):
- for line in lines:
- self.write(line)
-
- @property
- def encoding(self):
- return "utf8"
-
- def write(self, value):
- _channel.send_event("stdout" if self.is_stdout else "stderr", content=value)
- if self.old_out:
- self.old_out.write(value)
- # flush immediately, else things go wonky and out of order
- self.flush()
-
- def isatty(self):
- return True
-
- def next(self):
- pass
-
- @property
- def name(self):
- if self.is_stdout:
- return ""
- else:
- return ""
-
- def __getattr__(self, name):
- return getattr(self.old_out, name)
-
-
-class _TestOutputBuffer(object):
- def __init__(self, old_buffer, is_stdout):
- self.buffer = old_buffer
- self.is_stdout = is_stdout
-
- def write(self, data):
- _channel.send_event("stdout" if self.is_stdout else "stderr", content=data)
- self.buffer.write(data)
-
- def flush(self):
- self.buffer.flush()
-
- def truncate(self, pos=None):
- return self.buffer.truncate(pos)
-
- def tell(self):
- return self.buffer.tell()
-
- def seek(self, pos, whence=0):
- return self.buffer.seek(pos, whence)
-
-
-class _IpcChannel(object):
- def __init__(self, socket, callback):
- self.socket = socket
- self.seq = 0
- self.callback = 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, ())
-
- def close(self):
- self._closed = True
-
- def readSocket(self):
- try:
- data = self.socket.recv(1024)
- self.callback()
- except OSError:
- if not self._closed:
- raise
-
- def receive(self):
- pass
-
- def send_event(self, name, **args):
- with self.lock:
- 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")
- self.socket.send(headers)
- self.socket.send(content)
-
-
-_channel = None
-
-
-class VsTestResult(unittest.TextTestResult):
- def startTest(self, test):
- super(VsTestResult, self).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)
- self.sendResult(test, "error", err)
-
- def addFailure(self, test, err):
- super(VsTestResult, self).addFailure(test, err)
- self.sendResult(test, "failed", err)
-
- def addSuccess(self, test):
- super(VsTestResult, self).addSuccess(test)
- self.sendResult(test, "passed")
-
- def addSkip(self, test, reason):
- super(VsTestResult, self).addSkip(test, reason)
- self.sendResult(test, "skipped")
-
- def addExpectedFailure(self, test, err):
- super(VsTestResult, self).addExpectedFailure(test, err)
- self.sendResult(test, "failed", err)
-
- def addUnexpectedSuccess(self, test):
- super(VsTestResult, self).addUnexpectedSuccess(test)
- self.sendResult(test, "passed")
-
- def sendResult(self, test, outcome, trace=None):
- if _channel is not None:
- tb = None
- message = None
- if trace is not None:
- traceback.print_exc()
- formatted = traceback.format_exception(*trace)
- # Remove the 'Traceback (most recent call last)'
- formatted = formatted[1:]
- tb = "".join(formatted)
- message = str(trace[1])
- _channel.send_event(
- name="result",
- outcome=outcome,
- traceback=tb,
- message=message,
- test=test.id(),
- )
-
-
-def stopTests():
- try:
- os.kill(os.getpid(), signal.SIGUSR1)
- except:
- try:
- os.kill(os.getpid(), signal.SIGTERM)
- except:
- pass
-
-
-class ExitCommand(Exception):
- pass
-
-
-def signal_handler(signal, frame):
- raise ExitCommand()
-
-
-def main():
- import os
- import sys
- import unittest
- from optparse import OptionParser
-
- global _channel
-
- parser = OptionParser(
- prog="visualstudio_py_testlauncher",
- usage="Usage: %prog [] ... ",
- )
- parser.add_option(
- "--debug", action="store_true", help="Whether debugging the unit tests"
- )
- parser.add_option(
- "-x",
- "--mixed-mode",
- action="store_true",
- help="wait for mixed-mode debugger to attach",
- )
- parser.add_option(
- "-t",
- "--test",
- type="str",
- dest="tests",
- action="append",
- help="specifies a test to run",
- )
- parser.add_option(
- "--testFile", type="str", help="Fully qualitified path to file name"
- )
- parser.add_option(
- "-c", "--coverage", type="str", help="enable code coverage and specify filename"
- )
- parser.add_option(
- "-r",
- "--result-port",
- type="int",
- help="connect to port on localhost and send test results",
- )
- parser.add_option("--us", type="str", help="Directory to start discovery")
- parser.add_option(
- "--up", type="str", help="Pattern to match test files (" "test*.py" " default)"
- )
- parser.add_option(
- "--ut",
- type="str",
- help="Top level directory of project (default to start directory)",
- )
- parser.add_option(
- "--uvInt",
- "--verboseInt",
- type="int",
- help="Verbose output (0 none, 1 (no -v) simple, 2 (-v) full)",
- )
- parser.add_option("--uf", "--failfast", type="str", help="Stop on first failure")
- parser.add_option(
- "--uc", "--catch", type="str", help="Catch control-C and display results"
- )
- (opts, _) = parser.parse_args()
-
- sys.path[0] = os.getcwd()
- if opts.result_port:
- try:
- signal.signal(signal.SIGUSR1, signal_handler)
- except:
- try:
- signal.signal(signal.SIGTERM, signal_handler)
- except:
- pass
- _channel = _IpcChannel(
- socket.create_connection(("127.0.0.1", opts.result_port)), stopTests
- )
- sys.stdout = _TestOutput(sys.stdout, is_stdout=True)
- sys.stderr = _TestOutput(sys.stderr, is_stdout=False)
-
- if opts.mixed_mode:
- # For mixed-mode attach, there's no ptvsd and hence no wait_for_attach(),
- # so we have to use Win32 API in a loop to do the same thing.
- from time import sleep
- from ctypes import windll, c_char
-
- while True:
- if windll.kernel32.IsDebuggerPresent() != 0:
- break
- sleep(0.1)
- try:
- debugger_helper = windll["Microsoft.PythonTools.Debugger.Helper.x86.dll"]
- except WindowsError:
- debugger_helper = windll["Microsoft.PythonTools.Debugger.Helper.x64.dll"]
- isTracing = c_char.in_dll(debugger_helper, "isTracing")
- while True:
- if isTracing.value != 0:
- break
- sleep(0.1)
-
- cov = None
- try:
- if opts.coverage:
- try:
- import coverage
-
- cov = coverage.coverage(opts.coverage)
- cov.load()
- cov.start()
- except:
- pass
- if opts.tests is None and opts.testFile is None:
- if opts.us is None:
- opts.us = "."
- if opts.up is None:
- opts.up = "test*.py"
- tests = unittest.defaultTestLoader.discover(opts.us, opts.up)
- else:
- # loadTestsFromNames doesn't work well (with duplicate file names or class names)
- # Easier approach is find the test suite and use that for running
- loader = unittest.TestLoader()
- # opts.us will be passed in
- suites = loader.discover(opts.us, pattern=os.path.basename(opts.testFile))
- suite = None
- tests = None
- if opts.tests is None:
- # Run everything in the test file
- tests = suites
- else:
- # Run a specific test class or test method
- for test_suite in suites._tests:
- for cls in test_suite._tests:
- try:
- for m in cls._tests:
- testId = m.id()
- if testId.startswith(opts.tests[0]):
- suite = cls
- if testId == opts.tests[0]:
- tests = unittest.TestSuite([m])
- break
- except Exception as err:
- errorMessage = traceback.format_exc()
- if tests is None:
- tests = suite
- if tests is None and suite is None:
- _channel.send_event(
- name="error",
- outcome="",
- traceback="",
- message="Failed to identify the test",
- test="",
- )
- if opts.uvInt is None:
- opts.uvInt = 0
- if opts.uf is not None:
- runner = unittest.TextTestRunner(
- verbosity=opts.uvInt, resultclass=VsTestResult, failfast=True
- )
- else:
- runner = unittest.TextTestRunner(
- verbosity=opts.uvInt, resultclass=VsTestResult
- )
- result = runner.run(tests)
- if _channel is not None:
- _channel.close()
- sys.exit(not result.wasSuccessful())
- finally:
- if cov is not None:
- cov.stop()
- cov.save()
- cov.xml_report(outfile=opts.coverage + ".xml", omit=__file__)
- if _channel is not None:
- _channel.send_event(name="done")
- _channel.socket.close()
- # prevent generation of the error 'Error in sys.exitfunc:'
- try:
- sys.stdout.close()
- except:
- pass
- try:
- sys.stderr.close()
- except:
- pass
-
-
-if __name__ == "__main__":
- main()
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/.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  /markdown  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  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  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  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  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  in the top toolbar.\r\n",
- "\r\n",
- "\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  which you can click to open the data viewer.\r\n",
- "\r\n",
- "\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  in the top toolbar \r\n",
- "\r\n",
- "\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  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",
- "\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",
- "\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  in the top toolbar.\n",
+ "\n",
+ "\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  which you can click to open the data viewer.\n",
+ "\n",
+ "\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  in the top toolbar \n",
+ "\n",
+ "\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  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",
+ "\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",
+ "\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",
""
]
},
@@ -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/python_files/create_conda.py b/python_files/create_conda.py
new file mode 100644
index 000000000000..284f734081b2
--- /dev/null
+++ b/python_files/create_conda.py
@@ -0,0 +1,130 @@
+# Copyright (c) Microsoft Corporation. All rights reserved.
+# Licensed under the MIT License.
+
+import argparse
+import os
+import pathlib
+import subprocess
+import sys
+from typing import Optional, Sequence, Union
+
+CONDA_ENV_NAME = ".conda"
+CWD = pathlib.Path.cwd()
+
+
+class VenvError(Exception):
+ pass
+
+
+def parse_args(argv: Sequence[str]) -> argparse.Namespace:
+ parser = argparse.ArgumentParser()
+ parser.add_argument(
+ "--python",
+ action="store",
+ help="Python version to install in the virtual environment.",
+ default=f"{sys.version_info.major}.{sys.version_info.minor}",
+ )
+ parser.add_argument(
+ "--install",
+ action="store_true",
+ default=False,
+ help="Install packages into the virtual environment.",
+ )
+ parser.add_argument(
+ "--git-ignore",
+ action="store_true",
+ default=False,
+ help="Add .gitignore to the newly created virtual environment.",
+ )
+ parser.add_argument(
+ "--name",
+ default=CONDA_ENV_NAME,
+ type=str,
+ help="Name of the virtual environment.",
+ metavar="NAME",
+ action="store",
+ )
+ return parser.parse_args(argv)
+
+
+def file_exists(path: Union[str, pathlib.PurePath]) -> bool:
+ return os.path.exists(path) # noqa: PTH110
+
+
+def conda_env_exists(name: Union[str, pathlib.PurePath]) -> bool:
+ 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) # noqa: PTH109
+ except subprocess.CalledProcessError as exc:
+ raise VenvError(error_message) from exc
+
+
+def get_conda_env_path(name: str) -> str:
+ return os.fspath(CWD / name)
+
+
+def install_packages(env_path: str) -> None:
+ yml = os.fspath(CWD / "environment.yml")
+ if file_exists(yml):
+ print(f"CONDA_INSTALLING_YML: {yml}")
+ run_process(
+ [
+ sys.executable,
+ "-m",
+ "conda",
+ "env",
+ "update",
+ "--prefix",
+ env_path,
+ "--file",
+ yml,
+ ],
+ "CREATE_CONDA.FAILED_INSTALL_YML",
+ )
+ print("CREATE_CONDA.INSTALLED_YML")
+
+
+def add_gitignore(name: str) -> None:
+ 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:
+ if argv is None:
+ argv = []
+ args = parse_args(argv)
+
+ if conda_env_exists(args.name):
+ env_path = get_conda_env_path(args.name)
+ print(f"EXISTING_CONDA_ENV:{env_path}")
+ else:
+ run_process(
+ [
+ sys.executable,
+ "-m",
+ "conda",
+ "create",
+ "--yes",
+ "--prefix",
+ args.name,
+ f"python={args.python}",
+ ],
+ "CREATE_CONDA.ENV_FAILED_CREATION",
+ )
+ env_path = get_conda_env_path(args.name)
+ print(f"CREATED_CONDA_ENV:{env_path}")
+ if args.git_ignore:
+ add_gitignore(args.name)
+
+ if args.install:
+ install_packages(env_path)
+
+
+if __name__ == "__main__":
+ main(sys.argv[1:])
diff --git a/python_files/create_microvenv.py b/python_files/create_microvenv.py
new file mode 100644
index 000000000000..2f2135444bc1
--- /dev/null
+++ b/python_files/create_microvenv.py
@@ -0,0 +1,60 @@
+# Copyright (c) Microsoft Corporation. All rights reserved.
+# Licensed under the MIT License.
+
+import argparse
+import os
+import pathlib
+import subprocess
+import sys
+from typing import Optional, Sequence
+
+VENV_NAME = ".venv"
+LIB_ROOT = pathlib.Path(__file__).parent / "lib" / "python"
+CWD = pathlib.Path.cwd()
+
+
+class MicroVenvError(Exception):
+ pass
+
+
+def run_process(args: Sequence[str], error_message: str) -> None:
+ try:
+ print("Running: " + " ".join(args))
+ 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:
+ parser = argparse.ArgumentParser()
+
+ parser.add_argument(
+ "--name",
+ default=VENV_NAME,
+ type=str,
+ help="Name of the virtual environment.",
+ metavar="NAME",
+ action="store",
+ )
+ return parser.parse_args(argv)
+
+
+def create_microvenv(name: str):
+ run_process(
+ [sys.executable, os.fspath(LIB_ROOT / "microvenv.py"), name],
+ "CREATE_MICROVENV.MICROVENV_FAILED_CREATION",
+ )
+
+
+def main(argv: Optional[Sequence[str]] = None) -> None:
+ if argv is None:
+ argv = []
+ args = parse_args(argv)
+
+ print("CREATE_MICROVENV.CREATING_MICROVENV")
+ create_microvenv(args.name)
+ print("CREATE_MICROVENV.CREATED_MICROVENV")
+
+
+if __name__ == "__main__":
+ main(sys.argv[1:])
diff --git a/python_files/create_venv.py b/python_files/create_venv.py
new file mode 100644
index 000000000000..83106bd889f8
--- /dev/null
+++ b/python_files/create_venv.py
@@ -0,0 +1,271 @@
+# Copyright (c) Microsoft Corporation. All rights reserved.
+# Licensed under the MIT License.
+
+import argparse
+import importlib.util as import_util
+import json
+import os
+import pathlib
+import subprocess
+import sys
+import urllib.request as url_lib
+from typing import List, Optional, Sequence, Union
+
+VENV_NAME = ".venv"
+CWD = pathlib.Path.cwd()
+MICROVENV_SCRIPT_PATH = pathlib.Path(__file__).parent / "create_microvenv.py"
+
+
+class VenvError(Exception):
+ pass
+
+
+def parse_args(argv: Sequence[str]) -> argparse.Namespace:
+ parser = argparse.ArgumentParser()
+
+ parser.add_argument(
+ "--requirements",
+ action="append",
+ default=[],
+ help="Install additional dependencies into the virtual environment.",
+ )
+
+ parser.add_argument(
+ "--toml",
+ action="store",
+ default=None,
+ help="Install additional dependencies from sources like `pyproject.toml` into the virtual environment.",
+ )
+
+ parser.add_argument(
+ "--extras",
+ action="append",
+ default=[],
+ help="Install specific package groups from `pyproject.toml` into the virtual environment.",
+ )
+
+ parser.add_argument(
+ "--git-ignore",
+ action="store_true",
+ default=False,
+ help="Add .gitignore to the newly created virtual environment.",
+ )
+
+ parser.add_argument(
+ "--name",
+ default=VENV_NAME,
+ type=str,
+ help="Name of the virtual environment.",
+ metavar="NAME",
+ action="store",
+ )
+
+ parser.add_argument(
+ "--stdin",
+ action="store_true",
+ default=False,
+ help="Read arguments from stdin.",
+ )
+
+ return parser.parse_args(argv)
+
+
+def is_installed(module: str) -> bool:
+ return import_util.find_spec(module) is not None
+
+
+def file_exists(path: Union[str, pathlib.PurePath]) -> bool:
+ 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 (
+ (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) # 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 get_win_venv_path(name)
+ else:
+ return os.fspath(CWD / name / "bin" / "python")
+
+
+def install_requirements(venv_path: str, requirements: List[str]) -> None:
+ if not requirements:
+ return
+
+ for requirement in requirements:
+ print(f"VENV_INSTALLING_REQUIREMENTS: {requirement}")
+ run_process(
+ [venv_path, "-m", "pip", "install", "-r", requirement],
+ "CREATE_VENV.PIP_FAILED_INSTALL_REQUIREMENTS",
+ )
+ print("CREATE_VENV.PIP_INSTALLED_REQUIREMENTS")
+
+
+def install_toml(venv_path: str, extras: List[str]) -> None:
+ args = "." if len(extras) == 0 else f".[{','.join(extras)}]"
+ run_process(
+ [venv_path, "-m", "pip", "install", "-e", args],
+ "CREATE_VENV.PIP_FAILED_INSTALL_PYPROJECT",
+ )
+ print("CREATE_VENV.PIP_INSTALLED_PYPROJECT")
+
+
+def upgrade_pip(venv_path: str) -> None:
+ print("CREATE_VENV.UPGRADING_PIP")
+ run_process(
+ [venv_path, "-m", "pip", "install", "--upgrade", "pip"],
+ "CREATE_VENV.UPGRADE_PIP_FAILED",
+ )
+ 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 is_file(git_ignore):
+ create_gitignore(git_ignore)
+
+
+def download_pip_pyz(name: str):
+ url = "https://bootstrap.pypa.io/pip/pip.pyz"
+ print("CREATE_VENV.DOWNLOADING_PIP")
+
+ try:
+ with url_lib.urlopen(url) as response:
+ 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):
+ pip_pyz_path = os.fspath(CWD / name / "pip.pyz")
+ executable = get_venv_path(name)
+ print("CREATE_VENV.INSTALLING_PIP")
+ run_process(
+ [executable, pip_pyz_path, "install", "pip"],
+ "CREATE_VENV.INSTALL_PIP_FAILED",
+ )
+
+
+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 = []
+ args = parse_args(argv)
+
+ use_micro_venv = False
+ venv_installed = is_installed("venv")
+ pip_installed = is_installed("pip")
+ ensure_pip_installed = is_installed("ensurepip")
+ distutils_installed = is_installed("distutils")
+
+ if not venv_installed:
+ if sys.platform == "win32":
+ raise VenvError("CREATE_VENV.VENV_NOT_FOUND")
+ else:
+ use_micro_venv = True
+ if not distutils_installed:
+ print("Install `python3-distutils` package or equivalent for your OS.")
+ print("On Debian/Ubuntu: `sudo apt install python3-distutils`")
+ raise VenvError("CREATE_VENV.DISTUTILS_NOT_INSTALLED")
+
+ if venv_exists(args.name):
+ # A virtual environment with same name exists.
+ # We will use the existing virtual environment.
+ venv_path = get_venv_path(args.name)
+ print(f"EXISTING_VENV:{venv_path}")
+ else:
+ if use_micro_venv:
+ # `venv` was not found but on this platform we can use `microvenv`
+ run_process(
+ [
+ sys.executable,
+ os.fspath(MICROVENV_SCRIPT_PATH),
+ "--name",
+ args.name,
+ ],
+ "CREATE_VENV.MICROVENV_FAILED_CREATION",
+ )
+ elif not pip_installed or not ensure_pip_installed:
+ # `venv` was found but `pip` or `ensurepip` was not found.
+ # We create a venv without `pip` in it. We will later install `pip`.
+ run_process(
+ [sys.executable, "-m", "venv", "--without-pip", args.name],
+ "CREATE_VENV.VENV_FAILED_CREATION",
+ )
+ else:
+ # Both `venv` and `pip` were found. So create a .venv normally
+ run_process(
+ [sys.executable, "-m", "venv", args.name],
+ "CREATE_VENV.VENV_FAILED_CREATION",
+ )
+
+ venv_path = get_venv_path(args.name)
+ print(f"CREATED_VENV:{venv_path}")
+
+ if args.git_ignore:
+ add_gitignore(args.name)
+
+ # At this point we have a .venv. Now we handle installing `pip`.
+ if pip_installed and ensure_pip_installed:
+ # We upgrade pip if it is already installed.
+ upgrade_pip(venv_path)
+ else:
+ # `pip` was not found, so we download it and install it.
+ 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 __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/python_files/get_output_via_markers.py b/python_files/get_output_via_markers.py
new file mode 100644
index 000000000000..e37f7f8c5df0
--- /dev/null
+++ b/python_files/get_output_via_markers.py
@@ -0,0 +1,32 @@
+# Copyright (c) Microsoft Corporation. All rights reserved.
+# Licensed under the MIT License.
+
+import runpy
+import sys
+
+# Sometimes executing scripts can print out stuff before the actual output is
+# printed. For eg. when activating conda. Hence, printing out markers to make
+# it more resilient to pull the output.
+print(">>>PYTHON-EXEC-OUTPUT")
+
+module = sys.argv[1]
+try:
+ if module == "-c":
+ ns = {}
+ code = sys.argv[2]
+ del sys.argv[2]
+ del sys.argv[0]
+ exec(code, ns, ns)
+ elif module.startswith("-m"):
+ module_name = sys.argv[2]
+ sys.argv = sys.argv[2:] # It should begin with the module name.
+ 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__")
+ elif module.startswith("-"):
+ raise NotImplementedError(sys.argv)
+ else:
+ runpy.run_module(module, run_name="__main__", alter_sys=True)
+finally:
+ print("<< 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 86%
rename from pythonFiles/interpreterInfo.py
rename to python_files/interpreterInfo.py
index 601959b7c2d5..f15da9e48ea3 100644
--- a/pythonFiles/interpreterInfo.py
+++ b/python_files/interpreterInfo.py
@@ -8,6 +8,6 @@
obj["versionInfo"] = tuple(sys.version_info)
obj["sysPrefix"] = sys.prefix
obj["sysVersion"] = sys.version
-obj["is64Bit"] = sys.maxsize > 2 ** 32
+obj["is64Bit"] = sys.maxsize > 2**32
print(json.dumps(obj))
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/python_files/linter.py b/python_files/linter.py
new file mode 100644
index 000000000000..edbbe9dfafe5
--- /dev/null
+++ b/python_files/linter.py
@@ -0,0 +1,51 @@
+import subprocess
+import sys
+
+linter_settings = {
+ "pylint": {
+ "args": ["--reports=n", "--output-format=json"],
+ },
+ "flake8": {
+ "args": ["--format", "%(row)d,%(col)d,%(code).1s,%(code)s:%(text)s"],
+ },
+ "bandit": {
+ "args": [
+ "-f",
+ "custom",
+ "--msg-template",
+ "{line},{col},{severity},{test_id}:{msg}",
+ "-n",
+ "-1",
+ ],
+ },
+ "mypy": {"args": []},
+ "prospector": {
+ "args": ["--absolute-paths", "--output-format=json"],
+ },
+ "pycodestyle": {
+ "args": ["--format", "%(row)d,%(col)d,%(code).1s,%(code)s:%(text)s"],
+ },
+ "pydocstyle": {
+ "args": [],
+ },
+ "pylama": {"args": ["--format=parsable"]},
+}
+
+
+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:]
+ else:
+ linter = sys.argv[2]
+ args = [sys.argv[3]] + linter_settings[linter]["args"] + sys.argv[4:]
+
+ if hasattr(subprocess, "run"):
+ subprocess.run(args, encoding="utf-8", stdout=sys.stdout, stderr=sys.stderr)
+ else:
+ subprocess.call(args, stdout=sys.stdout, stderr=sys.stderr)
+
+
+if __name__ == "__main__":
+ main()
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/python_files/testlauncher.py b/python_files/testlauncher.py
new file mode 100644
index 000000000000..2309a203363b
--- /dev/null
+++ b/python_files/testlauncher.py
@@ -0,0 +1,45 @@
+# Copyright (c) Microsoft Corporation. All rights reserved.
+# Licensed under the MIT License.
+
+import os
+import sys
+
+
+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]
+ test_runner = sys.argv[2]
+ args = sys.argv[3:]
+
+ return (cwd, test_runner, args)
+
+
+def run(cwd, test_runner, 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() # noqa: PTH109
+ os.chdir(cwd)
+
+ try:
+ if test_runner == "pytest":
+ import pytest
+
+ pytest.main(args)
+ sys.exit(0)
+ finally:
+ pass
+
+
+if __name__ == "__main__":
+ cwd, test_runner, args = parse_argv()
+ run(cwd, test_runner, args)
diff --git a/python_files/tests/__init__.py b/python_files/tests/__init__.py
new file mode 100644
index 000000000000..86bc29ff33e8
--- /dev/null
+++ b/python_files/tests/__init__.py
@@ -0,0 +1,12 @@
+# 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__)
+SRC_ROOT = os.path.dirname(TEST_ROOT)
+PROJECT_ROOT = os.path.dirname(SRC_ROOT)
+TESTING_TOOLS_ROOT = os.path.join(SRC_ROOT, "testing_tools")
+DEBUG_ADAPTER_ROOT = os.path.join(SRC_ROOT, "debug_adapter")
+
+PYTHONFILES = os.path.join(SRC_ROOT, "lib", "python")
diff --git a/python_files/tests/__main__.py b/python_files/tests/__main__.py
new file mode 100644
index 000000000000..2595fce358e4
--- /dev/null
+++ b/python_files/tests/__main__.py
@@ -0,0 +1,55 @@
+# Copyright (c) Microsoft Corporation. All rights reserved.
+# Licensed under the MIT License.
+
+import argparse
+import sys
+
+import pytest
+
+from . import DEBUG_ADAPTER_ROOT, SRC_ROOT, TEST_ROOT, TESTING_TOOLS_ROOT
+
+
+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(
+ "--no-functional", dest="markers", action="append_const", const="not functional"
+ )
+ args, remainder = parser.parse_known_args()
+
+ ns = vars(args)
+
+ if remainder:
+ for arg in remainder:
+ if arg.startswith("-") and arg not in ("-v", "--verbose", "-h", "--help"):
+ specific = False
+ break
+ else:
+ specific = True
+ else:
+ specific = False
+ args.specific = specific
+
+ return ns, remainder
+
+
+def main(pytestargs, markers=None, specific=False): # noqa: FBT002
+ sys.path.insert(1, TESTING_TOOLS_ROOT)
+ sys.path.insert(1, DEBUG_ADAPTER_ROOT)
+
+ if not specific:
+ pytestargs.insert(0, TEST_ROOT)
+ pytestargs.insert(0, "--rootdir")
+ pytestargs.insert(1, SRC_ROOT)
+ for marker in reversed(markers or ()):
+ pytestargs.insert(0, marker)
+ pytestargs.insert(0, "-m")
+
+ return pytest.main(pytestargs)
+
+
+if __name__ == "__main__":
+ mainkwargs, pytestargs = parse_args()
+ ec = main(pytestargs, **mainkwargs)
+ sys.exit(ec)
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/python_files/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
new file mode 100644
index 000000000000..59738aeba37f
--- /dev/null
+++ b/python_files/tests/pytestadapter/.data/dual_level_nested_folder/nested_folder_one/test_bottom_folder.py
@@ -0,0 +1,14 @@
+# Copyright (c) Microsoft Corporation. All rights reserved.
+# Licensed under the MIT License.
+
+
+# This test's id is dual_level_nested_folder/nested_folder_one/test_bottom_folder.py::test_bottom_function_t.
+# This test passes.
+def test_bottom_function_t(): # test_marker--test_bottom_function_t
+ assert True
+
+
+# This test's id is dual_level_nested_folder/nested_folder_one/test_bottom_folder.py::test_bottom_function_f.
+# This test fails.
+def test_bottom_function_f(): # test_marker--test_bottom_function_f
+ assert False
diff --git a/python_files/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
new file mode 100644
index 000000000000..010c54cf4461
--- /dev/null
+++ b/python_files/tests/pytestadapter/.data/dual_level_nested_folder/test_top_folder.py
@@ -0,0 +1,14 @@
+# Copyright (c) Microsoft Corporation. All rights reserved.
+# Licensed under the MIT License.
+
+
+# This test's id is dual_level_nested_folder/test_top_folder.py::test_top_function_t.
+# This test passes.
+def test_top_function_t(): # test_marker--test_top_function_t
+ assert True
+
+
+# This test's id is dual_level_nested_folder/test_top_folder.py::test_top_function_f.
+# This test fails.
+def test_top_function_f(): # test_marker--test_top_function_f
+ assert False
diff --git a/python_files/tests/pytestadapter/.data/empty_discovery.py b/python_files/tests/pytestadapter/.data/empty_discovery.py
new file mode 100644
index 000000000000..5f4ea27aec7f
--- /dev/null
+++ b/python_files/tests/pytestadapter/.data/empty_discovery.py
@@ -0,0 +1,7 @@
+# Copyright (c) Microsoft Corporation. All rights reserved.
+# Licensed under the MIT License.
+
+
+# This file has no tests in it; the discovery will return an empty list of tests.
+def function_function(string):
+ return string
diff --git a/python_files/tests/pytestadapter/.data/error_parametrize_discovery.py b/python_files/tests/pytestadapter/.data/error_parametrize_discovery.py
new file mode 100644
index 000000000000..8e48224edf3b
--- /dev/null
+++ b/python_files/tests/pytestadapter/.data/error_parametrize_discovery.py
@@ -0,0 +1,10 @@
+# Copyright (c) Microsoft Corporation. All rights reserved.
+# Licensed under the MIT License.
+import pytest
+
+
+# This test has an error which will appear on pytest discovery.
+# This error is intentional and is meant to test pytest discovery error handling.
+@pytest.mark.parametrize("actual,expected", [("3+5", 8), ("2+4", 6), ("6*9", 42)])
+def test_function():
+ assert True
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/python_files/tests/pytestadapter/.data/error_syntax_discovery.txt b/python_files/tests/pytestadapter/.data/error_syntax_discovery.txt
new file mode 100644
index 000000000000..78627fffb351
--- /dev/null
+++ b/python_files/tests/pytestadapter/.data/error_syntax_discovery.txt
@@ -0,0 +1,7 @@
+# Copyright (c) Microsoft Corporation. All rights reserved.
+# Licensed under the MIT License.
+
+# This test has a syntax error.
+# This error is intentional and is meant to test pytest discovery error handling.
+def test_function()
+ assert True
diff --git a/python_files/tests/pytestadapter/.data/folder_a/folder_b/folder_a/test_nest.py b/python_files/tests/pytestadapter/.data/folder_a/folder_b/folder_a/test_nest.py
new file mode 100644
index 000000000000..9ac9f7017f87
--- /dev/null
+++ b/python_files/tests/pytestadapter/.data/folder_a/folder_b/folder_a/test_nest.py
@@ -0,0 +1,8 @@
+# Copyright (c) Microsoft Corporation. All rights reserved.
+# Licensed under the MIT License.
+
+
+# This test's id is double_nested_folder/nested_folder_one/nested_folder_two/test_nest.py::test_function.
+# This test passes.
+def test_function(): # test_marker--test_function
+ assert 1 == 1
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/python_files/tests/pytestadapter/.data/folder_with_script/test_simple.py b/python_files/tests/pytestadapter/.data/folder_with_script/test_simple.py
new file mode 100644
index 000000000000..9f9bfb014f3d
--- /dev/null
+++ b/python_files/tests/pytestadapter/.data/folder_with_script/test_simple.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/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/python_files/tests/pytestadapter/.data/text_docstring.txt b/python_files/tests/pytestadapter/.data/text_docstring.txt
new file mode 100644
index 000000000000..b29132c10b57
--- /dev/null
+++ b/python_files/tests/pytestadapter/.data/text_docstring.txt
@@ -0,0 +1,4 @@
+This is a doctest test which passes #test_marker--text_docstring.txt
+>>> x = 3
+>>> x
+3
diff --git a/python_files/tests/pytestadapter/.data/unittest_folder/test_add.py b/python_files/tests/pytestadapter/.data/unittest_folder/test_add.py
new file mode 100644
index 000000000000..e9bdda0ad2ad
--- /dev/null
+++ b/python_files/tests/pytestadapter/.data/unittest_folder/test_add.py
@@ -0,0 +1,29 @@
+# Copyright (c) Microsoft Corporation. All rights reserved.
+# Licensed under the MIT License.
+import unittest
+
+
+def add(a, b):
+ return a + b
+
+
+class TestAddFunction(unittest.TestCase):
+ # This test's id is unittest_folder/test_add.py::TestAddFunction::test_add_positive_numbers.
+ # This test passes.
+ def test_add_positive_numbers(self): # test_marker--test_add_positive_numbers
+ result = add(2, 3)
+ self.assertEqual(result, 5)
+
+ # This test's id is unittest_folder/test_add.py::TestAddFunction::test_add_negative_numbers.
+ # This test passes.
+ 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/python_files/tests/pytestadapter/.data/unittest_folder/test_subtract.py b/python_files/tests/pytestadapter/.data/unittest_folder/test_subtract.py
new file mode 100644
index 000000000000..634a6d81f9eb
--- /dev/null
+++ b/python_files/tests/pytestadapter/.data/unittest_folder/test_subtract.py
@@ -0,0 +1,34 @@
+# Copyright (c) Microsoft Corporation. All rights reserved.
+# Licensed under the MIT License.
+import unittest
+
+
+def subtract(a, b):
+ return a - b
+
+
+class TestSubtractFunction(unittest.TestCase):
+ # This test's id is unittest_folder/test_subtract.py::TestSubtractFunction::test_subtract_positive_numbers.
+ # This test passes.
+ def test_subtract_positive_numbers( # test_marker--test_subtract_positive_numbers
+ self,
+ ):
+ result = subtract(5, 3)
+ self.assertEqual(result, 2)
+
+ # This test's id is unittest_folder/test_subtract.py::TestSubtractFunction::test_subtract_negative_numbers.
+ # This test passes.
+ def test_subtract_negative_numbers( # test_marker--test_subtract_negative_numbers
+ self,
+ ):
+ 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/python_files/tests/pytestadapter/.data/unittest_pytest_same_file.py b/python_files/tests/pytestadapter/.data/unittest_pytest_same_file.py
new file mode 100644
index 000000000000..ac66779b9cbe
--- /dev/null
+++ b/python_files/tests/pytestadapter/.data/unittest_pytest_same_file.py
@@ -0,0 +1,17 @@
+# Copyright (c) Microsoft Corporation. All rights reserved.
+# Licensed under the MIT License.
+
+import unittest
+
+
+class TestExample(unittest.TestCase):
+ # This test's id is unittest_pytest_same_file.py::TestExample::test_true_unittest.
+ # Test type is unittest and this test passes.
+ def test_true_unittest(self): # test_marker--test_true_unittest
+ assert True
+
+
+# This test's id is unittest_pytest_same_file.py::test_true_pytest.
+# Test type is pytest and this test passes.
+def test_true_pytest(): # test_marker--test_true_pytest
+ assert True
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/python_files/tests/run_all.py b/python_files/tests/run_all.py
new file mode 100644
index 000000000000..3edb3cd3440c
--- /dev/null
+++ b/python_files/tests/run_all.py
@@ -0,0 +1,16 @@
+# Copyright (c) Microsoft Corporation. All rights reserved.
+# Licensed under the MIT License.
+
+# Replace the "." entry.
+import os
+import pathlib
+import sys
+
+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()
+ ec = main(pytestargs, **mainkwargs)
+ sys.exit(ec)
diff --git a/python_files/tests/test_create_conda.py b/python_files/tests/test_create_conda.py
new file mode 100644
index 000000000000..82daafbea9dc
--- /dev/null
+++ b/python_files/tests/test_create_conda.py
@@ -0,0 +1,71 @@
+# Copyright (c) Microsoft Corporation. All rights reserved.
+# Licensed under the MIT License.
+
+import importlib
+import sys
+
+import pytest
+
+import create_conda
+
+
+@pytest.mark.parametrize("env_exists", [True, False])
+@pytest.mark.parametrize("git_ignore", [True, False])
+@pytest.mark.parametrize("install", [True, False])
+@pytest.mark.parametrize("python", [True, False])
+def test_create_env(env_exists, git_ignore, install, python):
+ importlib.reload(create_conda)
+ create_conda.conda_env_exists = lambda _n: env_exists
+
+ install_packages_called = False
+
+ def install_packages(_name):
+ nonlocal install_packages_called
+ install_packages_called = True
+
+ create_conda.install_packages = install_packages
+
+ run_process_called = False
+
+ 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}"
+ if not env_exists:
+ assert args == [
+ sys.executable,
+ "-m",
+ "conda",
+ "create",
+ "--yes",
+ "--prefix",
+ create_conda.CONDA_ENV_NAME,
+ f"python={version}",
+ ]
+ assert error_message == "CREATE_CONDA.ENV_FAILED_CREATION"
+
+ create_conda.run_process = run_process
+
+ add_gitignore_called = False
+
+ def add_gitignore(_name):
+ nonlocal add_gitignore_called
+ add_gitignore_called = True
+
+ create_conda.add_gitignore = add_gitignore
+
+ args = []
+ if git_ignore:
+ args.append("--git-ignore")
+ if install:
+ args.append("--install")
+ if python:
+ args.extend(["--python", "12345"])
+ create_conda.main(args)
+ assert install_packages_called == install
+
+ # run_process is called when the venv does not exist
+ assert run_process_called != env_exists
+
+ # add_gitignore is called when new venv is created and git_ignore is True
+ assert add_gitignore_called == (not env_exists and git_ignore)
diff --git a/python_files/tests/test_create_microvenv.py b/python_files/tests/test_create_microvenv.py
new file mode 100644
index 000000000000..e5d4e68802e9
--- /dev/null
+++ b/python_files/tests/test_create_microvenv.py
@@ -0,0 +1,28 @@
+# Copyright (c) Microsoft Corporation. All rights reserved.
+# Licensed under the MIT License.
+
+import importlib
+import os
+import sys
+
+import create_microvenv
+
+
+def test_create_microvenv():
+ importlib.reload(create_microvenv)
+ run_process_called = False
+
+ def run_process(args, error_message):
+ nonlocal run_process_called
+ run_process_called = True
+ assert args == [
+ sys.executable,
+ os.fspath(create_microvenv.LIB_ROOT / "microvenv.py"),
+ create_microvenv.VENV_NAME,
+ ]
+ assert error_message == "CREATE_MICROVENV.MICROVENV_FAILED_CREATION"
+
+ create_microvenv.run_process = run_process
+
+ create_microvenv.main()
+ assert run_process_called is True
diff --git a/python_files/tests/test_create_venv.py b/python_files/tests/test_create_venv.py
new file mode 100644
index 000000000000..6308934d71a0
--- /dev/null
+++ b/python_files/tests/test_create_venv.py
@@ -0,0 +1,300 @@
+# 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 pytest
+
+import create_venv
+
+
+@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"
+ run_process_called = False
+
+ def run_process(args, error_message):
+ nonlocal run_process_called
+ microvenv_path = os.fspath(create_venv.MICROVENV_SCRIPT_PATH)
+ if microvenv_path in args:
+ run_process_called = True
+ assert args == [
+ sys.executable,
+ microvenv_path,
+ "--name",
+ ".test_venv",
+ ]
+ assert error_message == "CREATE_VENV.MICROVENV_FAILED_CREATION"
+
+ create_venv.run_process = run_process
+
+ create_venv.main(["--name", ".test_venv"])
+
+ # run_process is called when the venv does not exist
+ assert run_process_called is True
+
+
+@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"
+ with pytest.raises(create_venv.VenvError) as e:
+ create_venv.main()
+ assert str(e.value) == "CREATE_VENV.VENV_NOT_FOUND"
+
+
+@pytest.mark.parametrize("env_exists", ["hasEnv", "noEnv"])
+@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
+
+ def install_packages(_env, _name):
+ nonlocal install_packages_called
+ install_packages_called = True
+
+ create_venv.install_requirements = install_packages
+ create_venv.install_toml = install_packages
+
+ run_process_called = False
+
+ def run_process(args, error_message):
+ nonlocal run_process_called
+ run_process_called = True
+ if env_exists == "noEnv":
+ assert args == [sys.executable, "-m", "venv", create_venv.VENV_NAME]
+ assert error_message == "CREATE_VENV.VENV_FAILED_CREATION"
+
+ create_venv.run_process = run_process
+
+ add_gitignore_called = False
+
+ 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"]
+ if install == "requirements":
+ args += ["--requirements", "requirements-for-test.txt"]
+ elif install == "toml":
+ args += ["--toml", "pyproject.toml", "--extras", "test"]
+
+ create_venv.main(args)
+ assert install_packages_called == (install != "skipInstall")
+
+ # run_process is called when the venv does not exist
+ 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 create_gitignore_called == (add_gitignore_called and (git_ignore != "gitIgnoreExists"))
+
+
+@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 str(x)
+
+ pip_upgraded = False
+ installing = None
+
+ order = []
+
+ def run_process(args, error_message):
+ 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
+
+ if install_type == "requirements":
+ 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
+ if install_type == "both":
+ assert order == ["requirements", "pyproject"]
+ else:
+ assert installing == install_type
+
+
+@pytest.mark.parametrize(
+ ("extras", "expected"),
+ [
+ ([], ["-m", "pip", "install", "-e", "."]),
+ (["test"], ["-m", "pip", "install", "-e", ".[test]"]),
+ (["test", "doc"], ["-m", "pip", "install", "-e", ".[test,doc]"]),
+ ],
+)
+def test_toml_args(extras, expected):
+ importlib.reload(create_venv)
+
+ actual = []
+
+ def run_process(args, error_message): # noqa: ARG001
+ nonlocal actual
+ actual = args[1:]
+
+ create_venv.run_process = run_process
+
+ create_venv.install_toml(sys.executable, extras)
+
+ assert actual == expected
+
+
+@pytest.mark.parametrize(
+ ("extras", "expected"),
+ [
+ ([], []),
+ (
+ ["requirements/test.txt"],
+ [[sys.executable, "-m", "pip", "install", "-r", "requirements/test.txt"]],
+ ),
+ (
+ ["requirements/test.txt", "requirements/doc.txt"],
+ [
+ [sys.executable, "-m", "pip", "install", "-r", "requirements/test.txt"],
+ [sys.executable, "-m", "pip", "install", "-r", "requirements/doc.txt"],
+ ],
+ ),
+ ],
+)
+def test_requirements_args(extras, expected):
+ importlib.reload(create_venv)
+
+ actual = []
+
+ def run_process(args, error_message): # noqa: ARG001
+ nonlocal actual
+ actual.append(args)
+
+ create_venv.run_process = run_process
+
+ create_venv.install_requirements(sys.executable, extras)
+
+ assert actual == expected
+
+
+def test_create_venv_missing_pip():
+ importlib.reload(create_venv)
+ create_venv.venv_exists = lambda _n: True
+ create_venv.is_installed = lambda module: module != "pip"
+
+ download_pip_pyz_called = False
+
+ def download_pip_pyz(name):
+ nonlocal download_pip_pyz_called
+ download_pip_pyz_called = True
+ assert name == create_venv.VENV_NAME
+
+ create_venv.download_pip_pyz = download_pip_pyz
+
+ run_process_called = False
+
+ 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")
+ 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/python_files/tests/test_normalize_selection.py b/python_files/tests/test_normalize_selection.py
new file mode 100644
index 000000000000..779bb9720bfa
--- /dev/null
+++ b/python_files/tests/test_normalize_selection.py
@@ -0,0 +1,317 @@
+# 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:
+ """Unit tests for the normalization script."""
+
+ def test_basic_normalization(self):
+ src = 'print("this is a test")'
+ expected = src + "\n"
+ result = normalizeSelection.normalize_lines(src)
+ assert result == expected
+
+ def test_more_than_one_line(self):
+ src = textwrap.dedent(
+ """\
+ # Some rando comment
+
+ def show_something():
+ print("Something")
+ """
+ )
+ expected = textwrap.dedent(
+ """\
+ def show_something():
+ print("Something")
+
+ """
+ )
+ result = normalizeSelection.normalize_lines(src)
+ assert result == expected
+
+ def test_with_hanging_indent(self):
+ src = textwrap.dedent(
+ """\
+ x = 22
+ y = 30
+ z = -10
+ result = x + y + z
+
+ if result == 42:
+ print("The answer to life, the universe, and everything")
+ """
+ )
+ expected = textwrap.dedent(
+ """\
+ x = 22
+ y = 30
+ z = -10
+ result = x + y + z
+ if result == 42:
+ print("The answer to life, the universe, and everything")
+
+ """
+ )
+ result = normalizeSelection.normalize_lines(src)
+ assert result == expected
+
+ def test_clear_out_extraneous_newlines(self):
+ src = textwrap.dedent(
+ """\
+ value_x = 22
+
+ value_y = 30
+
+ value_z = -10
+
+ print(value_x + value_y + value_z)
+
+ """
+ )
+ expected = textwrap.dedent(
+ """\
+ value_x = 22
+ value_y = 30
+ value_z = -10
+ print(value_x + value_y + value_z)
+ """
+ )
+ result = normalizeSelection.normalize_lines(src)
+ assert result == expected
+
+ def test_clear_out_extra_lines_and_whitespace(self):
+ src = textwrap.dedent(
+ """\
+ if True:
+ x = 22
+
+ y = 30
+
+ z = -10
+
+ print(x + y + z)
+
+ """
+ )
+ expected = textwrap.dedent(
+ """\
+ if True:
+ x = 22
+ y = 30
+ z = -10
+
+ print(x + y + z)
+ """
+ )
+ result = normalizeSelection.normalize_lines(src)
+ assert result == expected
+
+ def test_partial_single_line(self):
+ src = " print('foo')"
+ expected = textwrap.dedent(src) + "\n"
+ result = normalizeSelection.normalize_lines(src)
+ assert result == expected
+
+ def test_multiline_with_indent(self):
+ src = """\
+
+ if (x > 0
+ and condition == True):
+ print('foo')
+ else:
+
+ print('bar')
+ """
+
+ expected = textwrap.dedent(
+ """\
+ if (x > 0
+ and condition == True):
+ print('foo')
+ else:
+ print('bar')
+
+ """
+ )
+
+ result = normalizeSelection.normalize_lines(src)
+ assert result == expected
+
+ def test_multiline_with_comment(self):
+ src = textwrap.dedent(
+ """\
+
+ def show_something():
+ # A comment
+ print("Something")
+ """
+ )
+ expected = textwrap.dedent(
+ """\
+ def show_something():
+ # A comment
+ print("Something")
+
+ """
+ )
+ result = normalizeSelection.normalize_lines(src)
+ assert result == expected
+
+ def test_exception(self):
+ src = " if True:"
+ expected = src + "\n\n"
+ result = normalizeSelection.normalize_lines(src)
+ assert result == expected
+
+ def test_multiline_exception(self):
+ src = textwrap.dedent(
+ """\
+
+ def show_something():
+ if True:
+ """
+ )
+ expected = src + "\n\n"
+ result = normalizeSelection.normalize_lines(src)
+ assert result == expected
+
+ def test_decorators(self):
+ src = textwrap.dedent(
+ """\
+ def foo(func):
+
+ def wrapper():
+ print('before')
+ func()
+ print('after')
+
+ return wrapper
+
+
+ @foo
+ def show_something():
+ print("Something")
+ """
+ )
+ expected = textwrap.dedent(
+ """\
+ def foo(func):
+ def wrapper():
+ print('before')
+ func()
+ print('after')
+ return wrapper
+
+ @foo
+ def show_something():
+ print("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/testing_tools/__init__.py b/python_files/tests/unittestadapter/.data/coverage_ex/__init__.py
similarity index 100%
rename from pythonFiles/tests/testing_tools/__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/python_files/tests/unittestadapter/.data/discovery_empty.py b/python_files/tests/unittestadapter/.data/discovery_empty.py
new file mode 100644
index 000000000000..9af5071303ce
--- /dev/null
+++ b/python_files/tests/unittestadapter/.data/discovery_empty.py
@@ -0,0 +1,15 @@
+# Copyright (c) Microsoft Corporation. All rights reserved.
+# Licensed under the MIT License.
+
+import unittest
+
+
+class DiscoveryEmpty(unittest.TestCase):
+ """Test class for the test_empty_discovery test.
+
+ 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.
+ """
+
+ def something(self) -> bool:
+ return True
diff --git a/python_files/tests/unittestadapter/.data/discovery_error/file_one.py b/python_files/tests/unittestadapter/.data/discovery_error/file_one.py
new file mode 100644
index 000000000000..031b6f6c9d68
--- /dev/null
+++ b/python_files/tests/unittestadapter/.data/discovery_error/file_one.py
@@ -0,0 +1,20 @@
+# Copyright (c) Microsoft Corporation. All rights reserved.
+# Licensed under the MIT License.
+
+import unittest
+
+import something_else # type: ignore # noqa: F401
+
+
+class DiscoveryErrorOne(unittest.TestCase):
+ """Test class for the test_error_discovery test.
+
+ 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.
+ """
+
+ def test_one(self) -> None:
+ self.assertGreater(2, 1)
+
+ def test_two(self) -> None:
+ self.assertNotEqual(2, 1)
diff --git a/python_files/tests/unittestadapter/.data/discovery_error/file_two.py b/python_files/tests/unittestadapter/.data/discovery_error/file_two.py
new file mode 100644
index 000000000000..5d6d54f886a1
--- /dev/null
+++ b/python_files/tests/unittestadapter/.data/discovery_error/file_two.py
@@ -0,0 +1,18 @@
+# Copyright (c) Microsoft Corporation. All rights reserved.
+# Licensed under the MIT License.
+
+import unittest
+
+
+class DiscoveryErrorTwo(unittest.TestCase):
+ """Test class for the test_error_discovery test.
+
+ 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.
+ """
+
+ def test_one(self) -> None:
+ self.assertGreater(2, 1)
+
+ def test_two(self) -> None:
+ self.assertNotEqual(2, 1)
diff --git a/python_files/tests/unittestadapter/.data/discovery_simple.py b/python_files/tests/unittestadapter/.data/discovery_simple.py
new file mode 100644
index 000000000000..1859436d5b5b
--- /dev/null
+++ b/python_files/tests/unittestadapter/.data/discovery_simple.py
@@ -0,0 +1,18 @@
+# Copyright (c) Microsoft Corporation. All rights reserved.
+# Licensed under the MIT License.
+
+import unittest
+
+
+class DiscoverySimple(unittest.TestCase):
+ """Test class for the test_simple_discovery test.
+
+ The discover_tests function should return a dictionary with a "success" status, no errors, and a test tree
+ if unittest discovery was performed successfully.
+ """
+
+ def test_one(self) -> None:
+ self.assertGreater(2, 1)
+
+ def test_two(self) -> None:
+ self.assertNotEqual(2, 1)
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/adapter/__init__.py b/python_files/tests/unittestadapter/.data/simple_django/mysite/__init__.py
similarity index 100%
rename from pythonFiles/tests/testing_tools/adapter/__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/pytest/__init__.py b/python_files/tests/unittestadapter/.data/simple_django/polls/__init__.py
similarity index 100%
rename from pythonFiles/tests/testing_tools/adapter/pytest/__init__.py
rename to python_files/tests/unittestadapter/.data/simple_django/polls/__init__.py
diff --git a/python_files/tests/unittestadapter/.data/simple_django/polls/admin.py b/python_files/tests/unittestadapter/.data/simple_django/polls/admin.py
new file mode 100644
index 000000000000..5b7f7a925cc0
--- /dev/null
+++ b/python_files/tests/unittestadapter/.data/simple_django/polls/admin.py
@@ -0,0 +1,2 @@
+# Copyright (c) Microsoft Corporation. All rights reserved.
+# Licensed under the MIT License.
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/python_files/tests/unittestadapter/.data/simple_django/polls/migrations/__init__.py b/python_files/tests/unittestadapter/.data/simple_django/polls/migrations/__init__.py
new file mode 100644
index 000000000000..5b7f7a925cc0
--- /dev/null
+++ b/python_files/tests/unittestadapter/.data/simple_django/polls/migrations/__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/.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/python_files/tests/unittestadapter/.data/test_fail_simple.py b/python_files/tests/unittestadapter/.data/test_fail_simple.py
new file mode 100644
index 000000000000..e329c3fd7003
--- /dev/null
+++ b/python_files/tests/unittestadapter/.data/test_fail_simple.py
@@ -0,0 +1,21 @@
+# Copyright (c) Microsoft Corporation. All rights reserved.
+# Licensed under the MIT License.
+
+import unittest
+
+# Test class for the test_fail_simple test.
+# The test_failed_tests function should return a dictionary with a "success" status
+# and the two tests with their outcome as "failed".
+
+class RunFailSimple(unittest.TestCase):
+ """Test class for the test_fail_simple test.
+
+ The test_failed_tests function should return a dictionary with a "success" status
+ and the two tests with their outcome as "failed".
+ """
+
+ def test_one_fail(self) -> None:
+ self.assertGreater(2, 3)
+
+ def test_two_fail(self) -> None:
+ self.assertNotEqual(1, 1)
diff --git a/python_files/tests/unittestadapter/.data/test_scenarios/tests/__init__.py b/python_files/tests/unittestadapter/.data/test_scenarios/tests/__init__.py
new file mode 100644
index 000000000000..5b7f7a925cc0
--- /dev/null
+++ b/python_files/tests/unittestadapter/.data/test_scenarios/tests/__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/.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/python_files/tests/unittestadapter/.data/test_subtest.py b/python_files/tests/unittestadapter/.data/test_subtest.py
new file mode 100644
index 000000000000..b913b8773701
--- /dev/null
+++ b/python_files/tests/unittestadapter/.data/test_subtest.py
@@ -0,0 +1,18 @@
+# Copyright (c) Microsoft Corporation. All rights reserved.
+# Licensed under the MIT License.
+
+import unittest
+
+# Test class for the test_subtest_run test.
+# The test_failed_tests function should return a dictionary that has a "success" status
+# and the "result" value is a dict with 6 entries, one for each subtest.
+
+
+class NumbersTest(unittest.TestCase):
+ def test_even(self):
+ """
+ Test that numbers between 0 and 5 are all even.
+ """
+ for i in range(0, 6):
+ with self.subTest(i=i):
+ self.assertEqual(i % 2, 0)
diff --git a/python_files/tests/unittestadapter/.data/test_two_classes.py b/python_files/tests/unittestadapter/.data/test_two_classes.py
new file mode 100644
index 000000000000..60b26706ad42
--- /dev/null
+++ b/python_files/tests/unittestadapter/.data/test_two_classes.py
@@ -0,0 +1,20 @@
+# Copyright (c) Microsoft Corporation. All rights reserved.
+# Licensed under the MIT License.
+
+import unittest
+
+# Test class which runs for the test_multiple_ids_run test with the two class parameters.
+# Both test functions will be returned in a dictionary with a "success" status,
+# and the two tests with their outcome as "success".
+
+
+class ClassOne(unittest.TestCase):
+
+ def test_one(self) -> None:
+ self.assertGreater(2, 1)
+
+class ClassTwo(unittest.TestCase):
+
+ def test_two(self) -> None:
+ self.assertGreater(2, 1)
+
diff --git a/python_files/tests/unittestadapter/.data/two_patterns/pattern_a_test.py b/python_files/tests/unittestadapter/.data/two_patterns/pattern_a_test.py
new file mode 100644
index 000000000000..52641360b526
--- /dev/null
+++ b/python_files/tests/unittestadapter/.data/two_patterns/pattern_a_test.py
@@ -0,0 +1,21 @@
+# Copyright (c) Microsoft Corporation. All rights reserved.
+# Licensed under the MIT License.
+import unittest
+
+# Test class for the two file pattern test. It is pattern *test.py.
+# The test_ids_multiple_runs function should return a dictionary with a "success" status,
+# 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
+
+ The test_ids_multiple_runs function should return a dictionary with a "success" status,
+ and the two tests with their outcome as "success".
+ """
+
+ def test_one_a(self) -> None:
+ self.assertGreater(2, 1)
+
+ def test_two_a(self) -> None:
+ self.assertNotEqual(2, 1)
diff --git a/python_files/tests/unittestadapter/.data/two_patterns/test_pattern_b.py b/python_files/tests/unittestadapter/.data/two_patterns/test_pattern_b.py
new file mode 100644
index 000000000000..06b6a818537d
--- /dev/null
+++ b/python_files/tests/unittestadapter/.data/two_patterns/test_pattern_b.py
@@ -0,0 +1,15 @@
+# Copyright (c) Microsoft Corporation. All rights reserved.
+# Licensed under the MIT License.
+import unittest
+
+# Test class for the two file pattern test. This file is pattern test*.py.
+# 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):
+ def test_one_b(self) -> None:
+ self.assertGreater(2, 1)
+
+ def test_two_b(self) -> None:
+ self.assertNotEqual(2, 1)
diff --git a/python_files/tests/unittestadapter/.data/unittest_folder/test_add.py b/python_files/tests/unittestadapter/.data/unittest_folder/test_add.py
new file mode 100644
index 000000000000..f562474b596a
--- /dev/null
+++ b/python_files/tests/unittestadapter/.data/unittest_folder/test_add.py
@@ -0,0 +1,21 @@
+# Copyright (c) Microsoft Corporation. All rights reserved.
+# Licensed under the MIT License.
+import unittest
+
+# Test class which runs for the test_multiple_ids_run test with the two test
+# 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):
+ result = add(2, 3)
+ self.assertEqual(result, 5)
+
+ def test_add_negative_numbers(self):
+ result = add(-2, -3)
+ self.assertEqual(result, -5)
diff --git a/python_files/tests/unittestadapter/.data/unittest_folder/test_subtract.py b/python_files/tests/unittestadapter/.data/unittest_folder/test_subtract.py
new file mode 100644
index 000000000000..8ac3988a3251
--- /dev/null
+++ b/python_files/tests/unittestadapter/.data/unittest_folder/test_subtract.py
@@ -0,0 +1,21 @@
+# Copyright (c) Microsoft Corporation. All rights reserved.
+# Licensed under the MIT License.
+import unittest
+
+# Test class which runs for the test_multiple_ids_run test with the two test
+# 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
+
+
+class TestSubtractFunction(unittest.TestCase):
+ 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)
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/python_files/tests/unittestadapter/.data/utils_decorated_tree.py b/python_files/tests/unittestadapter/.data/utils_decorated_tree.py
new file mode 100644
index 000000000000..90fdfc89a27b
--- /dev/null
+++ b/python_files/tests/unittestadapter/.data/utils_decorated_tree.py
@@ -0,0 +1,29 @@
+# Copyright (c) Microsoft Corporation. All rights reserved.
+# Licensed under the MIT License.
+
+import unittest
+from functools import wraps
+
+
+def my_decorator(f):
+ @wraps(f)
+ def wrapper(*args, **kwds):
+ print("Calling decorated function")
+ return f(*args, **kwds)
+
+ return wrapper
+
+
+class TreeOne(unittest.TestCase):
+ """Test class for the test_build_decorated_tree test.
+
+ build_test_tree should build a test tree with these test cases.
+ """
+
+ @my_decorator
+ def test_one(self) -> None:
+ self.assertGreater(2, 1)
+
+ @my_decorator
+ def test_two(self) -> None:
+ self.assertNotEqual(2, 1)
diff --git a/python_files/tests/unittestadapter/.data/utils_nested_cases/file_one.py b/python_files/tests/unittestadapter/.data/utils_nested_cases/file_one.py
new file mode 100644
index 000000000000..84f7fefc4ebd
--- /dev/null
+++ b/python_files/tests/unittestadapter/.data/utils_nested_cases/file_one.py
@@ -0,0 +1,17 @@
+# Copyright (c) Microsoft Corporation. All rights reserved.
+# Licensed under the MIT License.
+
+import unittest
+
+
+class CaseTwoFileOne(unittest.TestCase):
+ """Test class for the test_nested_test_cases test.
+
+ get_test_case should return tests from the test suites in this folder.
+ """
+
+ def test_one(self) -> None:
+ self.assertGreater(2, 1)
+
+ def test_two(self) -> None:
+ self.assertNotEqual(2, 1)
diff --git a/python_files/tests/unittestadapter/.data/utils_nested_cases/folder/__init__.py b/python_files/tests/unittestadapter/.data/utils_nested_cases/folder/__init__.py
new file mode 100644
index 000000000000..5b7f7a925cc0
--- /dev/null
+++ b/python_files/tests/unittestadapter/.data/utils_nested_cases/folder/__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/.data/utils_nested_cases/folder/file_two.py b/python_files/tests/unittestadapter/.data/utils_nested_cases/folder/file_two.py
new file mode 100644
index 000000000000..235a104016a3
--- /dev/null
+++ b/python_files/tests/unittestadapter/.data/utils_nested_cases/folder/file_two.py
@@ -0,0 +1,17 @@
+# Copyright (c) Microsoft Corporation. All rights reserved.
+# Licensed under the MIT License.
+
+import unittest
+
+
+class CaseTwoFileTwo(unittest.TestCase):
+ """Test class for the test_nested_test_cases test.
+
+ get_test_case should return tests from the test suites in this folder.
+ """
+
+ def test_one(self) -> None:
+ self.assertGreater(2, 1)
+
+ def test_two(self) -> None:
+ self.assertNotEqual(2, 1)
diff --git a/python_files/tests/unittestadapter/.data/utils_simple_cases.py b/python_files/tests/unittestadapter/.data/utils_simple_cases.py
new file mode 100644
index 000000000000..fb3ae7eb7909
--- /dev/null
+++ b/python_files/tests/unittestadapter/.data/utils_simple_cases.py
@@ -0,0 +1,17 @@
+# Copyright (c) Microsoft Corporation. All rights reserved.
+# Licensed under the MIT License.
+
+import unittest
+
+
+class CaseOne(unittest.TestCase):
+ """Test class for the test_simple_test_cases test.
+
+ get_test_case should return tests from the test suite.
+ """
+
+ def test_one(self) -> None:
+ self.assertGreater(2, 1)
+
+ def test_two(self) -> None:
+ self.assertNotEqual(2, 1)
diff --git a/python_files/tests/unittestadapter/.data/utils_simple_tree.py b/python_files/tests/unittestadapter/.data/utils_simple_tree.py
new file mode 100644
index 000000000000..6db51a4fd80b
--- /dev/null
+++ b/python_files/tests/unittestadapter/.data/utils_simple_tree.py
@@ -0,0 +1,17 @@
+# Copyright (c) Microsoft Corporation. All rights reserved.
+# Licensed under the MIT License.
+
+import unittest
+
+
+class TreeOne(unittest.TestCase):
+ """Test class for the test_build_simple_tree test.
+
+ build_test_tree should build a test tree with these test cases.
+ """
+
+ def test_one(self) -> None:
+ self.assertGreater(2, 1)
+
+ def test_two(self) -> None:
+ self.assertNotEqual(2, 1)
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/python_files/tests/unittestadapter/test_utils.py b/python_files/tests/unittestadapter/test_utils.py
new file mode 100644
index 000000000000..dc8a81175e70
--- /dev/null
+++ b/python_files/tests/unittestadapter/test_utils.py
@@ -0,0 +1,339 @@
+# Copyright (c) Microsoft Corporation. All rights reserved.
+# Licensed under the MIT License.
+
+import os
+import pathlib
+import sys
+import unittest
+
+import pytest
+
+from unittestadapter.pvsc_utils import (
+ TestNode,
+ TestNodeTypeEnum,
+ build_test_tree,
+ get_child_node,
+ get_test_case,
+)
+
+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"),
+ [
+ (
+ ".",
+ "utils_simple_cases*",
+ [
+ "utils_simple_cases.CaseOne.test_one",
+ "utils_simple_cases.CaseOne.test_two",
+ ],
+ ),
+ (
+ "utils_nested_cases",
+ "file*",
+ [
+ "file_one.CaseTwoFileOne.test_one",
+ "file_one.CaseTwoFileOne.test_two",
+ "folder.file_two.CaseTwoFileTwo.test_one",
+ "folder.file_two.CaseTwoFileTwo.test_two",
+ ],
+ ),
+ ],
+)
+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/.
+ start_dir = os.fsdecode(TEST_DATA_PATH / directory)
+
+ loader = unittest.TestLoader()
+ suite = loader.discover(start_dir, pattern)
+
+ # Iterate on get_test_case and save the 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",
+ "type_": TestNodeTypeEnum.folder,
+ "children": [
+ {
+ "name": "childOne",
+ "path": "child/one",
+ "type_": TestNodeTypeEnum.folder,
+ "children": [
+ {
+ "name": "nestedOne",
+ "path": "nested/one",
+ "type_": TestNodeTypeEnum.folder,
+ "children": [],
+ "id_": "nested/one",
+ },
+ {
+ "name": "nestedTwo",
+ "path": "nested/two",
+ "type_": TestNodeTypeEnum.folder,
+ "children": [],
+ "id_": "nested/two",
+ },
+ ],
+ "id_": "child/one",
+ },
+ {
+ "name": "childTwo",
+ "path": "child/two",
+ "type_": TestNodeTypeEnum.folder,
+ "children": [],
+ "id_": "child/two",
+ },
+ ],
+ "id_": "foo",
+ }
+
+ get_child_node("childTwo", "child/two", TestNodeTypeEnum.folder, tree)
+ tree_copy = tree.copy()
+
+ # Check that the tree didn't get mutated by get_child_node.
+ 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",
+ "type_": TestNodeTypeEnum.folder,
+ "children": [
+ {
+ "name": "childOne",
+ "path": "child/one",
+ "type_": TestNodeTypeEnum.folder,
+ "children": [
+ {
+ "name": "nestedOne",
+ "path": "nested/one",
+ "type_": TestNodeTypeEnum.folder,
+ "children": [],
+ "id_": "nested/one",
+ },
+ {
+ "name": "nestedTwo",
+ "path": "nested/two",
+ "type_": TestNodeTypeEnum.folder,
+ "children": [],
+ "id_": "nested/two",
+ },
+ ],
+ "id_": "child/one",
+ },
+ {
+ "name": "childTwo",
+ "path": "child/two",
+ "type_": TestNodeTypeEnum.folder,
+ "children": [],
+ "id_": "child/two",
+ },
+ ],
+ "id_": "foo",
+ }
+
+ # Make a separate copy of tree["children"].
+ tree_before = tree.copy()
+ tree_before["children"] = tree["children"][:]
+
+ get_child_node("childThree", "child/three", TestNodeTypeEnum.folder, tree)
+
+ tree_after = tree.copy()
+ 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, ["id_", "lineno", "name"])
+
+ # Check for the added node.
+ last_child = tree["children"][-1]
+ assert last_child["name"] == "childThree"
+
+
+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."""
+ # Discovery tests in utils_simple_tree.py.
+ start_dir = os.fsdecode(TEST_DATA_PATH)
+ pattern = "utils_simple_tree*"
+ file_path = os.fsdecode(pathlib.PurePath(TEST_DATA_PATH, "utils_simple_tree.py"))
+
+ expected: TestNode = {
+ "path": start_dir,
+ "type_": TestNodeTypeEnum.folder,
+ "name": ".data",
+ "children": [
+ {
+ "name": "utils_simple_tree.py",
+ "type_": TestNodeTypeEnum.file,
+ "path": file_path,
+ "children": [
+ {
+ "name": "TreeOne",
+ "path": file_path,
+ "type_": TestNodeTypeEnum.class_,
+ "children": [
+ {
+ "name": "test_one",
+ "path": file_path,
+ "type_": TestNodeTypeEnum.test,
+ "lineno": "13",
+ "id_": file_path + "\\" + "TreeOne" + "\\" + "test_one",
+ "runID": "utils_simple_tree.TreeOne.test_one",
+ },
+ {
+ "name": "test_two",
+ "path": file_path,
+ "type_": TestNodeTypeEnum.test,
+ "lineno": "16",
+ "id_": file_path + "\\" + "TreeOne" + "\\" + "test_two",
+ "runID": "utils_simple_tree.TreeOne.test_two",
+ },
+ ],
+ "id_": file_path + "\\" + "TreeOne",
+ }
+ ],
+ "id_": file_path,
+ }
+ ],
+ "id_": start_dir,
+ }
+
+ loader = unittest.TestLoader()
+ suite = loader.discover(start_dir, pattern)
+ tests, errors = build_test_tree(suite, start_dir)
+
+ 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."""
+ # Discovery tests in utils_decorated_tree.py.
+ start_dir = os.fsdecode(TEST_DATA_PATH)
+ pattern = "utils_decorated_tree*"
+ file_path = os.fsdecode(pathlib.PurePath(TEST_DATA_PATH, "utils_decorated_tree.py"))
+
+ expected: TestNode = {
+ "path": start_dir,
+ "type_": TestNodeTypeEnum.folder,
+ "name": ".data",
+ "children": [
+ {
+ "name": "utils_decorated_tree.py",
+ "type_": TestNodeTypeEnum.file,
+ "path": file_path,
+ "children": [
+ {
+ "name": "TreeOne",
+ "path": file_path,
+ "type_": TestNodeTypeEnum.class_,
+ "children": [
+ {
+ "name": "test_one",
+ "path": file_path,
+ "type_": TestNodeTypeEnum.test,
+ "lineno": "24",
+ "id_": file_path + "\\" + "TreeOne" + "\\" + "test_one",
+ "runID": "utils_decorated_tree.TreeOne.test_one",
+ },
+ {
+ "name": "test_two",
+ "path": file_path,
+ "type_": TestNodeTypeEnum.test,
+ "lineno": "28",
+ "id_": file_path + "\\" + "TreeOne" + "\\" + "test_two",
+ "runID": "utils_decorated_tree.TreeOne.test_two",
+ },
+ ],
+ "id_": file_path + "\\" + "TreeOne",
+ }
+ ],
+ "id_": file_path,
+ }
+ ],
+ "id_": start_dir,
+ }
+
+ loader = unittest.TestLoader()
+ suite = loader.discover(start_dir, pattern)
+ tests, errors = build_test_tree(suite, start_dir)
+
+ 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*"
+
+ loader = unittest.TestLoader()
+ suite = loader.discover(start_dir, pattern)
+ tests, errors = build_test_tree(suite, start_dir)
+
+ 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/python_files/tests/util.py b/python_files/tests/util.py
new file mode 100644
index 000000000000..ee240cd95202
--- /dev/null
+++ b/python_files/tests/util.py
@@ -0,0 +1,26 @@
+# Copyright (c) Microsoft Corporation. All rights reserved.
+# Licensed under the MIT License.
+
+
+class Stub:
+ def __init__(self):
+ self.calls = []
+
+ def add_call(self, name, args=None, kwargs=None):
+ self.calls.append((name, args, kwargs))
+
+
+class StubProxy:
+ def __init__(self, stub=None, name=None):
+ self.name = name
+ self.stub = stub if stub is not None else Stub()
+
+ @property
+ def calls(self):
+ return self.stub.calls
+
+ def add_call(self, funcname, *args, **kwargs):
+ callname = funcname
+ if self.name:
+ 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/python_files/visualstudio_py_testlauncher.py b/python_files/visualstudio_py_testlauncher.py
new file mode 100644
index 000000000000..878491083a71
--- /dev/null
+++ b/python_files/visualstudio_py_testlauncher.py
@@ -0,0 +1,382 @@
+# Python Tools for Visual Studio
+# Copyright(c) Microsoft Corporation
+# All rights reserved.
+#
+# 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
+#
+# THIS CODE IS PROVIDED ON AN *AS IS* BASIS, WITHOUT WARRANTIES OR CONDITIONS
+# OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION ANY
+# IMPLIED WARRANTIES OR CONDITIONS OF TITLE, FITNESS FOR A PARTICULAR PURPOSE,
+# MERCHANTABLITY OR NON-INFRINGEMENT.
+#
+# See the Apache Version 2.0 License for specific language governing
+# permissions and limitations under the License.
+
+__author__ = "Microsoft Corporation "
+__version__ = "3.0.0.0"
+
+import contextlib
+import json
+import os
+import signal
+import socket
+import sys
+import traceback
+import unittest
+
+try:
+ import thread
+except ModuleNotFoundError:
+ import _thread as thread
+
+
+class _TestOutput:
+ """file like object which redirects output to the repl window."""
+
+ errors = "strict"
+
+ def __init__(self, old_out, is_stdout):
+ self.is_stdout = is_stdout
+ self.old_out = old_out
+ if sys.version_info[0] >= 3 and hasattr(old_out, "buffer"):
+ self.buffer = _TestOutputBuffer(old_out.buffer, is_stdout)
+
+ def flush(self):
+ if self.old_out:
+ self.old_out.flush()
+
+ def writelines(self, lines):
+ for line in lines:
+ self.write(line)
+
+ @property
+ def encoding(self):
+ return "utf8"
+
+ def write(self, value):
+ _channel.send_event("stdout" if self.is_stdout else "stderr", content=value)
+ if self.old_out:
+ self.old_out.write(value)
+ # flush immediately, else things go wonky and out of order
+ self.flush()
+
+ def isatty(self):
+ return True
+
+ def next(self):
+ pass
+
+ @property
+ def name(self):
+ if self.is_stdout:
+ return ""
+ else:
+ return ""
+
+ def __getattr__(self, name):
+ return getattr(self.old_out, name)
+
+
+class _TestOutputBuffer:
+ def __init__(self, old_buffer, is_stdout):
+ self.buffer = old_buffer
+ self.is_stdout = is_stdout
+
+ def write(self, data):
+ _channel.send_event("stdout" if self.is_stdout else "stderr", content=data)
+ self.buffer.write(data)
+
+ def flush(self):
+ self.buffer.flush()
+
+ def truncate(self, pos=None):
+ return self.buffer.truncate(pos)
+
+ def tell(self):
+ return self.buffer.tell()
+
+ def seek(self, pos, whence=0):
+ return self.buffer.seek(pos, whence)
+
+
+class _IpcChannel:
+ def __init__(self, socket, callback):
+ self.socket = socket
+ self.seq = 0
+ self.callback = callback
+ self.lock = thread.allocate_lock()
+ self._closed = False
+ # start the testing reader thread loop
+ self.test_thread_id = thread.start_new_thread(self.read_socket, ())
+
+ def close(self):
+ self._closed = True
+
+ def read_socket(self):
+ try:
+ self.socket.recv(1024)
+ self.callback()
+ except OSError:
+ if not self._closed:
+ raise
+
+ def receive(self):
+ pass
+
+ def send_event(self, name, **args):
+ with self.lock:
+ body = {"type": "event", "seq": self.seq, "event": name, "body": args}
+ self.seq += 1
+ content = json.dumps(body).encode("utf8")
+ headers = f"Content-Length: {len(content)}\n\n".encode()
+ self.socket.send(headers)
+ self.socket.send(content)
+
+
+_channel = None
+
+
+class VsTestResult(unittest.TextTestResult):
+ 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): # noqa: N802
+ super().addError(test, err)
+ self.sendResult(test, "error", err)
+
+ def addFailure(self, test, err): # noqa: N802
+ super().addFailure(test, err)
+ self.sendResult(test, "failed", err)
+
+ def addSuccess(self, test): # noqa: N802
+ super().addSuccess(test)
+ self.sendResult(test, "passed")
+
+ def addSkip(self, test, reason): # noqa: N802
+ super().addSkip(test, reason)
+ self.sendResult(test, "skipped")
+
+ def addExpectedFailure(self, test, err): # noqa: N802
+ super().addExpectedFailure(test, err)
+ self.sendResult(test, "failed-expected", err)
+
+ def addUnexpectedSuccess(self, test): # noqa: N802
+ super().addUnexpectedSuccess(test)
+ self.sendResult(test, "passed-unexpected")
+
+ 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): # noqa: N802
+ if _channel is not None:
+ tb = None
+ message = None
+ if trace is not None:
+ traceback.print_exc()
+ formatted = traceback.format_exception(*trace)
+ # Remove the 'Traceback (most recent call last)'
+ formatted = formatted[1:]
+ tb = "".join(formatted)
+ message = str(trace[1])
+
+ result = {
+ "outcome": outcome,
+ "traceback": tb,
+ "message": message,
+ "test": test.id(),
+ }
+ if subtest is not None:
+ result["subtest"] = subtest.id()
+ _channel.send_event("result", **result)
+
+
+def stop_tests():
+ try:
+ os.kill(os.getpid(), signal.SIGUSR1)
+ except Exception:
+ os.kill(os.getpid(), signal.SIGTERM)
+
+
+class ExitCommand(Exception): # noqa: N818
+ pass
+
+
+def signal_handler(signal, frame): # noqa: ARG001
+ raise ExitCommand
+
+
+def main():
+ import os
+ import sys
+ import unittest
+ from optparse import OptionParser
+
+ global _channel
+
+ parser = OptionParser(
+ prog="visualstudio_py_testlauncher",
+ usage="Usage: %prog [] ... ",
+ )
+ parser.add_option("--debug", action="store_true", help="Whether debugging the unit tests")
+ parser.add_option(
+ "-x",
+ "--mixed-mode",
+ action="store_true",
+ help="wait for mixed-mode debugger to attach",
+ )
+ parser.add_option(
+ "-t",
+ "--test",
+ type="str",
+ dest="tests",
+ action="append",
+ help="specifies a test to run",
+ )
+ parser.add_option("--testFile", type="str", help="Fully qualitified path to file name")
+ parser.add_option(
+ "-c", "--coverage", type="str", help="enable code coverage and specify filename"
+ )
+ parser.add_option(
+ "-r",
+ "--result-port",
+ type="int",
+ help="connect to port on localhost and send test results",
+ )
+ parser.add_option("--us", type="str", help="Directory to start discovery")
+ parser.add_option("--up", type="str", help="Pattern to match test files (test*.py default)")
+ parser.add_option(
+ "--ut",
+ type="str",
+ help="Top level directory of project (default to start directory)",
+ )
+ parser.add_option(
+ "--uvInt",
+ "--verboseInt",
+ type="int",
+ help="Verbose output (0 none, 1 (no -v) simple, 2 (-v) full)",
+ )
+ parser.add_option("--uf", "--failfast", type="str", help="Stop on first failure")
+ parser.add_option("--uc", "--catch", type="str", help="Catch control-C and display results")
+ (opts, _) = parser.parse_args()
+
+ sys.path[0] = os.getcwd() # noqa: PTH109
+ if opts.result_port:
+ try:
+ signal.signal(signal.SIGUSR1, signal_handler)
+ except Exception:
+ with contextlib.suppress(Exception):
+ signal.signal(signal.SIGTERM, signal_handler)
+ _channel = _IpcChannel(
+ socket.create_connection(("127.0.0.1", opts.result_port)), stop_tests
+ )
+ sys.stdout = _TestOutput(sys.stdout, is_stdout=True)
+ sys.stderr = _TestOutput(sys.stderr, is_stdout=False)
+
+ if opts.mixed_mode:
+ # For mixed-mode attach, there's no ptvsd and hence no wait_for_attach(),
+ # so we have to use Win32 API in a loop to do the same thing.
+ from ctypes import c_char, windll
+ from time import sleep
+
+ while True:
+ if windll.kernel32.IsDebuggerPresent() != 0:
+ break
+ sleep(0.1)
+ try:
+ debugger_helper = windll["Microsoft.PythonTools.Debugger.Helper.x86.dll"]
+ except OSError:
+ debugger_helper = windll["Microsoft.PythonTools.Debugger.Helper.x64.dll"]
+ is_tracing = c_char.in_dll(debugger_helper, "isTracing")
+ while True:
+ if is_tracing.value != 0:
+ break
+ sleep(0.1)
+
+ cov = None
+ try:
+ if opts.coverage:
+ with contextlib.suppress(Exception):
+ import coverage
+
+ cov = coverage.coverage(opts.coverage)
+ cov.load()
+ cov.start()
+ if opts.tests is None and opts.testFile is None:
+ if opts.us is None:
+ opts.us = "."
+ if opts.up is None:
+ opts.up = "test*.py"
+ tests = unittest.defaultTestLoader.discover(opts.us, opts.up)
+ else:
+ # loadTestsFromNames doesn't work well (with duplicate file names or class names)
+ # Easier approach is find the test suite and use that for running
+ loader = unittest.TestLoader()
+ # opts.us will be passed in
+ suites = loader.discover(
+ opts.us,
+ pattern=os.path.basename(opts.testFile), # noqa: PTH119
+ top_level_dir=opts.ut,
+ )
+ suite = None
+ tests = None
+ if opts.tests is None:
+ # Run everything in the test file
+ tests = suites
+ else:
+ # Run a specific test class or test method
+ for test_suite in suites._tests: # noqa: SLF001
+ for cls in test_suite._tests: # noqa: SLF001
+ with contextlib.suppress(Exception):
+ for m in cls._tests:
+ test_id = m.id()
+ if test_id.startswith(opts.tests[0]):
+ suite = cls
+ if test_id in opts.tests:
+ if tests is None:
+ tests = unittest.TestSuite([m])
+ else:
+ tests.addTest(m)
+ if tests is None:
+ tests = suite
+ if tests is None and suite is None:
+ _channel.send_event(
+ name="error",
+ outcome="",
+ traceback="",
+ message="Failed to identify the test",
+ test="",
+ )
+ if opts.uvInt is None:
+ opts.uvInt = 0
+ if opts.uf is not None:
+ runner = unittest.TextTestRunner(
+ verbosity=opts.uvInt, resultclass=VsTestResult, failfast=True
+ )
+ else:
+ runner = unittest.TextTestRunner(verbosity=opts.uvInt, resultclass=VsTestResult)
+ result = runner.run(tests)
+ if _channel is not None:
+ _channel.close()
+ sys.exit(not result.wasSuccessful())
+ finally:
+ if cov is not None:
+ cov.stop()
+ cov.save()
+ cov.xml_report(outfile=opts.coverage + ".xml", omit=__file__)
+ if _channel is not None:
+ _channel.send_event(name="done")
+ _channel.socket.close()
+ # prevent generation of the error 'Error in sys.exitfunc:'
+ with contextlib.suppress(Exception):
+ sys.stdout.close()
+ with contextlib.suppress(Exception):
+ sys.stderr.close()
+
+
+if __name__ == "__main__":
+ main()
diff --git a/pythonFiles/tests/testing_tools/adapter/.data/complex/tests/__init__.py b/python_files/vscode_datascience_helpers/__init__.py
similarity index 100%
rename from pythonFiles/tests/testing_tools/adapter/.data/complex/tests/__init__.py
rename to python_files/vscode_datascience_helpers/__init__.py
diff --git a/pythonFiles/tests/testing_tools/adapter/.data/complex/tests/v/__init__.py b/python_files/vscode_datascience_helpers/tests/__init__.py
similarity index 100%
rename from pythonFiles/tests/testing_tools/adapter/.data/complex/tests/v/__init__.py
rename to python_files/vscode_datascience_helpers/tests/__init__.py
diff --git a/python_files/vscode_datascience_helpers/tests/logParser.py b/python_files/vscode_datascience_helpers/tests/logParser.py
new file mode 100644
index 000000000000..12c090ec581f
--- /dev/null
+++ b/python_files/vscode_datascience_helpers/tests/logParser.py
@@ -0,0 +1,95 @@
+import argparse # noqa: N999
+import os
+import re
+from io import TextIOWrapper
+from pathlib import Path
+
+os.system("color")
+
+
+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 strip_timestamp(line: str):
+ match = timestamp_regex.match(line)
+ if match:
+ return line[match.end() :]
+ return line
+
+
+def read_strip_lines(f: TextIOWrapper):
+ return map(strip_timestamp, f.readlines())
+
+
+def print_test_output(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 read_strip_lines(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 split_by_pid(testlog):
+ # Split testlog into prefixed logs based on pid
+ p = Path(testlog[0])
+ pids = set()
+ logs = {}
+ pid = None
+ try:
+ with p.open() as f:
+ for line in read_strip_lines(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 pid not in pids:
+ pids.add(pid)
+ log_file = p.with_name(f"{p.stem}_{pid}.log")
+ print("Writing to new log:", os.fsdecode(log_file))
+ logs[pid] = log_file.open(mode="w")
+
+ # Add this line to the log
+ if pid is not None:
+ logs[pid].write(line)
+ finally:
+ # Close all of the open logs
+ for key in logs:
+ logs[key].close()
+
+
+def do_work(args):
+ if not args.testlog:
+ print("Test log should be passed")
+ elif args.testoutput:
+ print_test_output(args.testlog)
+ elif args.split:
+ split_by_pid(args.testlog)
+ else:
+ parser.print_usage()
+
+
+def main():
+ do_work(parser.parse_args())
+
+
+if __name__ == "__main__":
+ main()
diff --git a/python_files/vscode_pytest/__init__.py b/python_files/vscode_pytest/__init__.py
new file mode 100644
index 000000000000..be4e3daaa843
--- /dev/null
+++ b/python_files/vscode_pytest/__init__.py
@@ -0,0 +1,1197 @@
+# Copyright (c) Microsoft Corporation. All rights reserved.
+# Licensed under the MIT License.
+
+from __future__ import annotations
+
+import atexit
+import contextlib
+import json
+import os
+import pathlib
+import sys
+import traceback
+from typing import (
+ TYPE_CHECKING,
+ Any,
+ Dict,
+ Generator,
+ Literal,
+ Protocol,
+ TypedDict,
+ cast,
+)
+
+import pytest
+
+if TYPE_CHECKING:
+ from pluggy import Result
+ from typing_extensions import NotRequired
+
+USES_PYTEST_DESCRIBE = False
+
+with contextlib.suppress(ImportError):
+ from pytest_describe.plugin import DescribeBlock
+
+ USES_PYTEST_DESCRIBE = True
+
+
+class HasPathOrFspath(Protocol):
+ """Protocol defining objects that have either a path or fspath attribute."""
+
+ path: pathlib.Path | None = None
+ fspath: Any | None = None
+
+
+class TestData(TypedDict):
+ """A general class that all test objects inherit from."""
+
+ name: str
+ path: pathlib.Path
+ type_: Literal["class", "function", "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[TestNode | TestItem | None]
+ lineno: NotRequired[str] # Optional field for class/function nodes
+
+
+class VSCodePytestError(Exception):
+ """A custom exception class for pytest errors."""
+
+ def __init__(self, message):
+ super().__init__(message)
+
+
+ERRORS = []
+IS_DISCOVERY = False
+map_id_to_path = {}
+collected_tests_so_far = set()
+TEST_RUN_PIPE = os.getenv("TEST_RUN_PIPE")
+PROJECT_ROOT_PATH = os.getenv(
+ "PROJECT_ROOT_PATH"
+) # Path to project root for multi-project workspaces
+SYMLINK_PATH = None
+INCLUDE_BRANCHES = False
+
+# Performance optimization caches for path resolution
+_path_cache: dict[int, pathlib.Path] = {} # Cache node paths by object id
+_path_to_str_cache: dict[pathlib.Path, str] = {} # Cache path-to-string conversions
+_CACHED_CWD: pathlib.Path | None = None
+
+
+def get_test_root_path() -> pathlib.Path:
+ """Get the root path for the test tree.
+
+ For project-based testing, this returns PROJECT_ROOT_PATH (the project root).
+ For legacy mode, this returns the current working directory.
+
+ Returns:
+ pathlib.Path: The root path to use for the test tree.
+ """
+ if PROJECT_ROOT_PATH:
+ return pathlib.Path(PROJECT_ROOT_PATH)
+ return pathlib.Path.cwd()
+
+
+def pytest_load_initial_conftests(early_config, parser, args): # noqa: ARG001
+ has_pytest_cov = early_config.pluginmanager.hasplugin(
+ "pytest_cov"
+ ) or early_config.pluginmanager.hasplugin("pytest_cov.plugin")
+ has_cov_arg = any("--cov" in arg for arg in args)
+ if has_cov_arg and not has_pytest_cov:
+ raise VSCodePytestError(
+ "\n \nERROR: pytest-cov is not installed, please install this before running pytest with coverage as pytest-cov is required. \n"
+ )
+ if "--cov-branch" in args:
+ global INCLUDE_BRANCHES
+ INCLUDE_BRANCHES = True
+
+ global TEST_RUN_PIPE
+ TEST_RUN_PIPE = os.getenv("TEST_RUN_PIPE")
+ error_string = (
+ "PYTEST ERROR: TEST_RUN_PIPE is not set at the time of pytest starting. "
+ "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"
+ )
+ if not TEST_RUN_PIPE:
+ print(error_string, file=sys.stderr)
+ if "--collect-only" in args:
+ global IS_DISCOVERY
+ IS_DISCOVERY = True
+
+ # check if --rootdir is in the args
+ for arg in args:
+ if "--rootdir=" in arg:
+ rootdir = pathlib.Path(arg.split("--rootdir=")[1])
+ if not rootdir.exists():
+ raise VSCodePytestError(
+ f"The path set in the argument --rootdir={rootdir} does not exist."
+ )
+
+ # Check if the rootdir is a symlink or a child of a symlink to the current cwd.
+ is_symlink = False
+
+ if rootdir.is_symlink():
+ is_symlink = True
+ print(
+ f"Plugin info[vscode-pytest]: rootdir argument, {rootdir}, is identified as a symlink."
+ )
+ elif rootdir.resolve() != rootdir:
+ print("Plugin info[vscode-pytest]: Checking if rootdir is a child of a symlink.")
+ is_symlink = has_symlink_parent(rootdir)
+ if is_symlink:
+ print(
+ f"Plugin info[vscode-pytest]: rootdir argument, {rootdir}, is identified as a symlink or child of a symlink, adjusting pytest paths accordingly.",
+ )
+ global SYMLINK_PATH
+ SYMLINK_PATH = rootdir
+
+
+def pytest_internalerror(excrepr, excinfo): # noqa: ARG001
+ """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() + "\n Check Python Logs for more details.")
+
+
+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 it is during discovery, then add the error to error logs.
+ if IS_DISCOVERY:
+ if call.excinfo and call.excinfo.typename != "AssertionError":
+ if report.outcome == "skipped" and "SkipTest" in str(call):
+ return
+ ERRORS.append(call.excinfo.exconly() + "\n Check Python Logs for more details.")
+ else:
+ ERRORS.append(report.longreprtext + "\n Check Python Logs for more details.")
+ else:
+ # If during execution, send this data that the given node failed.
+ report_value = "error"
+ if call.excinfo.typename == "AssertionError":
+ report_value = "failure"
+ node_id = get_absolute_test_id(node.nodeid, get_node_path(node))
+ if node_id not in collected_tests_so_far:
+ collected_tests_so_far.add(node_id)
+ item_result = create_test_outcome(
+ node_id,
+ report_value,
+ "Test failed with exception",
+ report.longreprtext,
+ )
+ collected_test = TestRunResultDict()
+ collected_test[node_id] = item_result
+ cwd = pathlib.Path.cwd()
+ send_execution_message(
+ os.fsdecode(cwd),
+ "success",
+ collected_test or None,
+ )
+
+
+def has_symlink_parent(current_path):
+ """Recursively checks if any parent directories of the given path are symbolic links."""
+ # Convert the current path to an absolute Path object
+ curr_path = pathlib.Path(current_path)
+ print("Checking for symlink parent starting at current path: ", curr_path)
+
+ # Iterate over all parent directories
+ for parent in curr_path.parents:
+ # Check if the parent directory is a symlink
+ if parent.is_symlink():
+ print(f"Symlink found at: {parent}")
+ return True
+ return False
+
+
+def get_absolute_test_id(test_id: str, test_path: pathlib.Path) -> str:
+ """A function that returns the absolute test id.
+
+ This is necessary because testIds are relative to the rootdir.
+ This does not work for our case since testIds when referenced during run time are relative to the instantiation
+ location. Absolute paths for testIds are necessary for the test tree ensures configurations that change the rootdir
+ of pytest are handled correctly.
+
+ Keyword arguments:
+ test_id -- the pytest id of the test which is relative to the rootdir.
+ testPath -- the path to the file the test is located in, as a pathlib.Path object.
+ """
+ split_id = test_id.split("::")[1:]
+ return "::".join([str(test_path), *split_id])
+
+
+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() + "\n Check Python Logs for more details.")
+
+
+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", "error"]
+ message: str | None
+ traceback: str | None
+ subtest: str | None
+
+
+def create_test_outcome(
+ testid: str,
+ outcome: str,
+ message: str | None,
+ traceback: str | None,
+ subtype: str | None = None, # noqa: ARG001
+) -> TestOutcome:
+ """A function that creates a TestOutcome object."""
+ return TestOutcome(
+ test=testid,
+ 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]
+
+
+@pytest.hookimpl(hookwrapper=True, trylast=True)
+def pytest_report_teststatus(report, config): # noqa: ARG001
+ """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.
+ """
+ cwd = pathlib.Path.cwd()
+ if SYMLINK_PATH:
+ cwd = SYMLINK_PATH
+
+ if report.when == "call" or (report.when == "setup" and report.skipped):
+ traceback = None
+ message = None
+ report_value = "skipped"
+ if report.passed:
+ report_value = "success"
+ elif report.failed:
+ report_value = "failure"
+ message = report.longreprtext
+ try:
+ node_path = map_id_to_path[report.nodeid]
+ except KeyError:
+ node_path = cwd
+ # Calculate the absolute test id and use this as the ID moving forward.
+ absolute_node_id = get_absolute_test_id(report.nodeid, node_path)
+ if absolute_node_id not in collected_tests_so_far:
+ collected_tests_so_far.add(absolute_node_id)
+ item_result = create_test_outcome(
+ absolute_node_id,
+ report_value,
+ message,
+ traceback,
+ )
+ collected_test = TestRunResultDict()
+ collected_test[absolute_node_id] = item_result
+ send_execution_message(
+ os.fsdecode(cwd),
+ "success",
+ collected_test or None,
+ )
+ yield
+
+
+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.",
+}
+
+
+@pytest.hookimpl(hookwrapper=True, trylast=True)
+def pytest_runtest_protocol(item, nextitem): # noqa: ARG001
+ map_id_to_path[item.nodeid] = get_node_path(item)
+ skipped = check_skipped_wrapper(item)
+ if skipped:
+ absolute_node_id = get_absolute_test_id(item.nodeid, get_node_path(item))
+ report_value = "skipped"
+ cwd = pathlib.Path.cwd()
+ if absolute_node_id not in collected_tests_so_far:
+ collected_tests_so_far.add(absolute_node_id)
+ item_result = create_test_outcome(
+ absolute_node_id,
+ report_value,
+ None,
+ None,
+ )
+ collected_test = TestRunResultDict()
+ collected_test[absolute_node_id] = item_result
+ send_execution_message(
+ os.fsdecode(cwd),
+ "success",
+ collected_test or None,
+ )
+ yield
+
+
+def check_skipped_wrapper(item):
+ """A function that checks if a test is skipped or not by check its markers and its parent markers.
+
+ Returns True if the test is marked as skipped at any level, False otherwise.
+
+ Keyword arguments:
+ item -- the pytest item object.
+ """
+ if item.own_markers and check_skipped_condition(item):
+ return True
+ parent = item.parent
+ while isinstance(parent, pytest.Class):
+ if parent.own_markers and check_skipped_condition(parent):
+ return True
+ parent = parent.parent
+ return False
+
+
+def check_skipped_condition(item):
+ """A helper function that checks if a item has a skip or a true skip condition.
+
+ Keyword arguments:
+ item -- the pytest item object.
+ """
+ for marker in item.own_markers:
+ # If the test is marked with skip then it will not hit the pytest_report_teststatus hook,
+ # therefore we need to handle it as skipped here.
+ skip_condition = False
+ if marker.name == "skipif":
+ skip_condition = any(marker.args)
+ if marker.name == "skip" or skip_condition:
+ return True
+ return False
+
+
+class FileCoverageInfo(TypedDict):
+ lines_covered: list[int]
+ lines_missed: list[int]
+ executed_branches: int
+ total_branches: int
+
+
+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.
+
+ Exit code 0: All tests were collected and passed successfully
+ Exit code 1: Tests were collected and run but some of the tests failed
+ Exit code 2: Test execution was interrupted by the user
+ Exit code 3: Internal error happened while executing tests
+ Exit code 4: pytest command line usage error
+ Exit code 5: No tests were collected
+ """
+ # Get the root path for the test tree structure (not the CWD for test execution)
+ # This is PROJECT_ROOT_PATH in project-based mode, or cwd in legacy mode
+ test_root_path = get_test_root_path()
+ if SYMLINK_PATH:
+ print("Plugin warning[vscode-pytest]: SYMLINK set, adjusting test root path.")
+ test_root_path = pathlib.Path(SYMLINK_PATH)
+
+ if IS_DISCOVERY:
+ if not (exitstatus == 0 or exitstatus == 1 or exitstatus == 5):
+ error_node: TestNode = {
+ "name": "",
+ "path": test_root_path,
+ "type_": "error",
+ "children": [],
+ "id_": "",
+ }
+ send_discovery_message(os.fsdecode(test_root_path), error_node)
+ try:
+ session_node: TestNode | None = build_test_tree(session)
+ if not session_node:
+ raise VSCodePytestError(
+ "Something went wrong following pytest finish, \
+ no session node was created"
+ )
+ send_discovery_message(os.fsdecode(test_root_path), session_node)
+ except Exception as e:
+ ERRORS.append(
+ f"Error Occurred, traceback: {(traceback.format_exc() if e.__traceback__ else '')}"
+ )
+ error_node: TestNode = {
+ "name": "",
+ "path": test_root_path,
+ "type_": "error",
+ "children": [],
+ "id_": "",
+ }
+ send_discovery_message(os.fsdecode(test_root_path), error_node)
+ 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"
+
+ send_execution_message(
+ os.fsdecode(test_root_path),
+ exitstatus_bool,
+ None,
+ )
+ # send end of transmission token
+
+ # send coverage if enabled
+ is_coverage_run = os.environ.get("COVERAGE_ENABLED")
+ if is_coverage_run == "True":
+ # load the report and build the json result to return
+ 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__)
+ global INCLUDE_BRANCHES
+ # 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") and INCLUDE_BRANCHES:
+ print(
+ "Plugin warning[vscode-pytest]: Branch coverage not supported in this coverage versions < 7.7.0. Please upgrade coverage package if you would like to see branch coverage."
+ )
+ INCLUDE_BRANCHES = False
+
+ try:
+ from coverage.exceptions import NoSource
+ except ImportError:
+ from coverage.misc import NoSource
+
+ cov = coverage.Coverage()
+ cov.load()
+
+ file_set: set[str] = cov.get_data().measured_files()
+ file_coverage_map: dict[str, FileCoverageInfo] = {}
+
+ # remove files omitted per coverage report config if any
+ omit_files: list[str] | None = cov.config.report_omit
+ if omit_files is not None:
+ for pattern in omit_files:
+ for file in list(file_set):
+ if pathlib.Path(file).match(pattern):
+ file_set.remove(file)
+
+ for file in file_set:
+ try:
+ 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()]
+ )
+
+ except NoSource:
+ # as per issue 24308 this best way to handle this edge case
+ continue
+ except Exception as e:
+ print(
+ f"Plugin error[vscode-pytest]: Skipping analysis of file: {file} due to error: {e}"
+ )
+ continue
+ 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,
+ }
+ # convert relative path to absolute path
+ if not pathlib.Path(file).is_absolute():
+ file = str(pathlib.Path(file).resolve())
+ file_coverage_map[file] = file_info
+
+ payload: CoveragePayloadDict = CoveragePayloadDict(
+ coverage=True,
+ cwd=os.fspath(test_root_path),
+ result=file_coverage_map,
+ error=None,
+ )
+ send_message(payload)
+
+
+def construct_nested_folders(
+ file_nodes_dict: dict[str, TestNode],
+ session_node: TestNode,
+ session_children_dict: dict[str, TestNode],
+) -> dict[str, TestNode]:
+ """Iterate through all files and construct them into nested folders.
+
+ Keyword arguments:
+ file_nodes_dict -- Dictionary of all file nodes
+ session_node -- The session node that will be parent to the folder structure
+ session_children_dict -- Dictionary of session's children nodes indexed by ID
+
+ Returns:
+ dict[str, TestNode] -- Updated session_children_dict with folder nodes added
+ """
+ created_files_folders_dict: dict[str, TestNode] = {}
+ for file_node in file_nodes_dict.values():
+ # Iterate through all the files that exist and construct them into nested folders.
+ root_folder_node: TestNode
+ try:
+ root_folder_node: TestNode = build_nested_folders(
+ file_node, created_files_folders_dict, session_node
+ )
+ except ValueError:
+ # This exception is raised when the session node is not a parent of the file node.
+ print(
+ "[vscode-pytest]: Session path not a parent of test paths, adjusting session node to common parent."
+ )
+ file_path_str: str = str(file_node["path"])
+ session_path_str: str = str(session_node["path"])
+ common_parent = os.path.commonpath([file_path_str, session_path_str])
+ common_parent_path = pathlib.Path(common_parent)
+ print("[vscode-pytest]: Session node now set to: ", common_parent)
+ session_node["path"] = common_parent_path # pathlib.Path
+ session_node["id_"] = common_parent # str
+ session_node["name"] = common_parent_path.name # str
+ root_folder_node = build_nested_folders(
+ file_node, created_files_folders_dict, session_node
+ )
+ # 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
+
+ return session_children_dict
+
+
+def process_parameterized_test(
+ test_case: pytest.Item,
+ test_node: TestItem,
+ function_nodes_dict: dict[str, TestNode],
+ file_nodes_dict: dict[str, TestNode],
+) -> TestNode:
+ """Process a parameterized test case and create appropriate function nodes.
+
+ Keyword arguments:
+ test_case -- the parameterized pytest test case; must have callspec attribute
+ test_node -- the test node created from the test case
+ function_nodes_dict -- dictionary of function nodes indexed by ID
+ file_nodes_dict -- dictionary of file nodes indexed by path
+
+ Returns:
+ TestNode -- the node to use for further processing (function node or original test node)
+ """
+ function_name: str = ""
+ # parameterized test cases cut the repetitive part of the name off.
+ parent_part, parameterized_section = test_node["name"].split("[", 1)
+ test_node["name"] = "[" + parameterized_section
+
+ first_split = test_case.nodeid.rsplit(
+ "::", 1
+ ) # splits the parameterized test name from the rest of the nodeid
+ second_split = first_split[0].rsplit(
+ ".py", 1
+ ) # splits the file path from the rest of the nodeid
+
+ class_and_method = second_split[1] + "::" # This has "::" separator at both ends
+ # construct the parent id, so it is absolute path :: any class and method :: parent_part
+ parent_id = cached_fsdecode(get_node_path(test_case)) + class_and_method + parent_part
+
+ try:
+ function_name = test_case.originalname # type: ignore
+ except AttributeError: # actual error has occurred
+ ERRORS.append(
+ f"unable to find original name for {test_case.name} with parameterization detected."
+ )
+ raise VSCodePytestError(
+ "Unable to find original name for parameterized test case"
+ ) from None
+
+ function_test_node = function_nodes_dict.get(parent_id)
+ if function_test_node is None:
+ function_test_node = create_parameterized_function_node(
+ function_name, get_node_path(test_case), parent_id
+ )
+ function_nodes_dict[parent_id] = function_test_node
+
+ if test_node not in function_test_node["children"]:
+ function_test_node["children"].append(test_node)
+
+ # Check if the parent node of the function is file, if so create/add to this file node.
+ if isinstance(test_case.parent, pytest.File):
+ # calculate the parent path of the test case
+ parent_path = get_node_path(test_case.parent)
+ parent_path_key = cached_fsdecode(parent_path)
+ parent_test_case = file_nodes_dict.get(parent_path_key)
+ if parent_test_case is None:
+ parent_test_case = create_file_node(parent_path)
+ file_nodes_dict[parent_path_key] = parent_test_case
+ if function_test_node not in parent_test_case["children"]:
+ parent_test_case["children"].append(function_test_node)
+
+ # Return the function node as the test node to handle subsequent nesting
+ return function_test_node
+
+
+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 that contains test items.
+
+ Returns:
+ TestNode -- The root node of the constructed test tree.
+ """
+ session_node = create_session_node(session)
+ session_children_dict: dict[str, TestNode] = {}
+ file_nodes_dict: dict[str, TestNode] = {}
+ class_nodes_dict: dict[str, TestNode] = {}
+ function_nodes_dict: dict[str, TestNode] = {}
+
+ # Check to see if the global variable for symlink path is set
+ if SYMLINK_PATH:
+ session_node["path"] = SYMLINK_PATH
+ session_node["id_"] = os.fspath(SYMLINK_PATH)
+
+ for test_case in session.items:
+ test_node = create_test_node(test_case)
+ if hasattr(test_case, "callspec"): # This means it is a parameterized test.
+ # Process parameterized test and get the function node to use for further processing
+ test_node = process_parameterized_test(
+ test_case, test_node, function_nodes_dict, file_nodes_dict
+ )
+ if isinstance(test_case.parent, pytest.Class) or (
+ USES_PYTEST_DESCRIBE and isinstance(test_case.parent, DescribeBlock)
+ ):
+ case_iter = test_case.parent
+ node_child_iter = test_node
+ test_class_node: TestNode | None = None
+ while isinstance(case_iter, pytest.Class) or (
+ USES_PYTEST_DESCRIBE and isinstance(case_iter, DescribeBlock)
+ ):
+ # While the given node is a class, create a class and nest the previous node as a child.
+ test_class_node = class_nodes_dict.get(case_iter.nodeid)
+ if test_class_node is None:
+ test_class_node = create_class_node(case_iter)
+ class_nodes_dict[case_iter.nodeid] = test_class_node
+ # Check if the class already has the child node. This will occur if the test is parameterized.
+ if node_child_iter not in test_class_node["children"]:
+ test_class_node["children"].append(node_child_iter)
+ # Iterate up.
+ node_child_iter = test_class_node
+ case_iter = case_iter.parent
+ # Now the parent node is not a class node, it is a file node.
+ if case_iter:
+ parent_module = case_iter
+ else:
+ ERRORS.append(f"Test class {case_iter} has no parent")
+ break
+ parent_path = get_node_path(parent_module)
+ # Create a file node that has the last class as a child.
+ parent_path_key = cached_fsdecode(parent_path)
+ test_file_node = file_nodes_dict.get(parent_path_key)
+ if test_file_node is None:
+ test_file_node = create_file_node(parent_path)
+ file_nodes_dict[parent_path_key] = test_file_node
+ # Check if the class is already a child of the file node.
+ if test_class_node is not None and test_class_node not in test_file_node["children"]:
+ test_file_node["children"].append(test_class_node)
+ elif not hasattr(test_case, "callspec"):
+ # This includes test cases that are pytest functions or a doctests.
+ if test_case.parent is None:
+ ERRORS.append(f"Test case {test_case.name} has no parent")
+ continue
+ parent_path = get_node_path(
+ cast(
+ "pytest.Session | pytest.Item | pytest.File | pytest.Class | pytest.Module | HasPathOrFspath",
+ test_case.parent,
+ )
+ )
+ parent_path_key = cached_fsdecode(parent_path)
+ parent_test_case = file_nodes_dict.get(parent_path_key)
+ if parent_test_case is None:
+ parent_test_case = create_file_node(parent_path)
+ file_nodes_dict[parent_path_key] = parent_test_case
+ parent_test_case["children"].append(test_node)
+ # Process all files and construct them into nested folders
+ session_children_dict = construct_nested_folders(
+ file_nodes_dict, session_node, session_children_dict
+ )
+ session_node["children"] = list(session_children_dict.values())
+ return session_node
+
+
+def build_nested_folders(
+ file_node: TestNode,
+ created_files_folders_dict: dict[str, TestNode],
+ session_node: TestNode,
+) -> 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 where the key is the path.
+ session -- the pytest session object.
+ """
+ # check if session node is a parent of the file node, throw error if not.
+ session_node_path = session_node["path"]
+ is_relative = False
+ try:
+ is_relative = file_node["path"].is_relative_to(session_node_path)
+ except AttributeError:
+ is_relative = file_node["path"].relative_to(session_node_path)
+ if not is_relative:
+ # If the session node is not a parent of the file node, we need to find their common parent.
+ raise ValueError("session and file not relative to each other, fixing now....")
+
+ # Begin the iterator_path one level above the current file.
+ prev_folder_node = file_node
+ iterator_path = file_node["path"].parent
+ counter = 0
+ max_iter = 100
+ while iterator_path != session_node_path:
+ curr_folder_name = iterator_path.name
+ iterator_path_key = cached_fsdecode(iterator_path)
+ curr_folder_node = created_files_folders_dict.get(iterator_path_key)
+ if curr_folder_node is None:
+ curr_folder_node = create_folder_node(curr_folder_name, iterator_path)
+ created_files_folders_dict[iterator_path_key] = 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
+ # Handles error where infinite loop occurs.
+ counter += 1
+ if counter > max_iter:
+ raise ValueError(
+ "[vscode-pytest]: Infinite loop occurred in build_nested_folders. iterator_path: ",
+ iterator_path,
+ "session_node_path: ",
+ session_node_path,
+ )
+ 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 ""
+ )
+ absolute_test_id = get_absolute_test_id(test_case.nodeid, get_node_path(test_case))
+ return {
+ "name": test_case.name,
+ "path": get_node_path(test_case),
+ "lineno": test_case_loc,
+ "type_": "test",
+ "id_": absolute_test_id,
+ "runID": absolute_test_id,
+ }
+
+
+def create_session_node(session: pytest.Session) -> TestNode:
+ """Creates a session node from a pytest session.
+
+ Keyword arguments:
+ session -- the pytest session.
+ """
+ # Use PROJECT_ROOT_PATH if set (project-based testing), otherwise use session path (legacy)
+ node_path = pathlib.Path(PROJECT_ROOT_PATH) if PROJECT_ROOT_PATH else get_node_path(session)
+ return {
+ "name": node_path.name,
+ "path": node_path,
+ "type_": "folder",
+ "children": [],
+ "id_": os.fspath(node_path),
+ }
+
+
+def create_class_node(class_module: pytest.Class | DescribeBlock) -> TestNode:
+ """Creates a class node from a pytest class object.
+
+ Keyword arguments:
+ class_module -- the pytest object representing a class module.
+ """
+ # Get line number for the class definition
+ class_line = ""
+ try:
+ if hasattr(class_module, "obj"):
+ import inspect
+
+ _, lineno = inspect.getsourcelines(class_module.obj)
+ class_line = str(lineno)
+ except (OSError, TypeError):
+ # If we can't get the source lines, leave lineno empty
+ pass
+
+ return {
+ "name": class_module.name,
+ "path": get_node_path(class_module),
+ "type_": "class",
+ "children": [],
+ "id_": get_absolute_test_id(class_module.nodeid, get_node_path(class_module)),
+ "lineno": class_line,
+ }
+
+
+def create_parameterized_function_node(
+ function_name: str, test_path: pathlib.Path, function_id: str
+) -> TestNode:
+ """Creates a function node to be the parent for the parameterized test nodes.
+
+ Keyword arguments:
+ function_name -- the name of the function.
+ test_path -- the path to the test file.
+ function_id -- the previously constructed function id that fits the pattern- absolute path :: any class and method :: parent_part
+ must be edited to get a unique id for the function node.
+ """
+ return {
+ "name": function_name,
+ "path": test_path,
+ "type_": "function",
+ "children": [],
+ "id_": function_id,
+ }
+
+
+def create_file_node(calculated_node_path: pathlib.Path) -> TestNode:
+ """Creates a file node from a path which has already been calculated using the get_node_path function.
+
+ Keyword arguments:
+ calculated_node_path -- the pytest file path.
+ """
+ return {
+ "name": calculated_node_path.name,
+ "path": calculated_node_path,
+ "type_": "file",
+ "id_": os.fspath(calculated_node_path),
+ "children": [],
+ }
+
+
+def create_folder_node(folder_name: 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": folder_name,
+ "path": 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: TestNode | None
+ error: list[str] | None
+
+
+class ExecutionPayloadDict(Dict):
+ """A dictionary that is used to send a execution post request to the server."""
+
+ cwd: str
+ status: Literal["success", "error"]
+ result: TestRunResultDict | None
+ not_found: list[str] | None # Currently unused need to check
+ error: str | None # Currently unused need to check
+
+
+class CoveragePayloadDict(Dict):
+ """A dictionary that is used to send a execution post request to the server."""
+
+ coverage: bool
+ cwd: str
+ result: dict[str, FileCoverageInfo] | None
+ error: str | None # Currently unused need to check
+
+
+def cached_fsdecode(path: pathlib.Path) -> str:
+ """Convert path to string with caching for performance.
+
+ This function caches path-to-string conversions to avoid redundant
+ os.fsdecode() calls during test tree building.
+
+ Parameters:
+ path: The pathlib.Path object to convert to string.
+
+ Returns:
+ str: The string representation of the path.
+ """
+ if path not in _path_to_str_cache:
+ _path_to_str_cache[path] = os.fspath(path)
+ return _path_to_str_cache[path]
+
+
+def get_node_path(
+ node: pytest.Session
+ | pytest.Item
+ | pytest.File
+ | pytest.Class
+ | pytest.Module
+ | HasPathOrFspath,
+) -> pathlib.Path:
+ """A function that returns the path of a node given the switch to pathlib.Path.
+
+ It also evaluates if the node is a symlink and returns the equivalent path.
+
+ Parameters:
+ node: A pytest object or any object that has a path or fspath attribute.
+ Do NOT pass a pathlib.Path object directly; use it directly instead.
+
+ Returns:
+ pathlib.Path: The resolved path for the node.
+ """
+ cache_key = id(node)
+ if cache_key in _path_cache:
+ return _path_cache[cache_key]
+
+ node_path = getattr(node, "path", None)
+ if node_path is None:
+ fspath = getattr(node, "fspath", None)
+ node_path = pathlib.Path(fspath) if fspath is not None else None
+
+ if not node_path:
+ raise VSCodePytestError(
+ f"Unable to find path for node: {node}, node.path: {node.path}, node.fspath: {node.fspath}"
+ )
+
+ # Check for the session node since it has the symlink already.
+ if SYMLINK_PATH and not isinstance(node, pytest.Session):
+ # Get relative between the cwd (resolved path) and the node path.
+ try:
+ # Check to see if the node path contains the symlink root already
+ # Convert Path objects to strings for os.path.commonpath
+ symlink_str: str = str(SYMLINK_PATH)
+ node_path_str: str = str(node_path)
+ common_path = os.path.commonpath([symlink_str, node_path_str])
+ if common_path == os.fsdecode(SYMLINK_PATH):
+ # The node path is already relative to the SYMLINK_PATH root therefore return
+ result = node_path
+ else:
+ # If the node path is not a symlink, then we need to calculate the equivalent symlink path
+ # get the relative path between the cwd and the node path (as the node path is not a symlink).
+ # Use cached cwd to avoid repeated system calls
+ global _CACHED_CWD
+ if _CACHED_CWD is None:
+ _CACHED_CWD = pathlib.Path.cwd()
+ rel_path = node_path.relative_to(_CACHED_CWD)
+ # combine the difference between the cwd and the node path with the symlink path
+ result = pathlib.Path(SYMLINK_PATH, rel_path)
+ except Exception as e:
+ raise VSCodePytestError(
+ f"Error occurred while calculating symlink equivalent from node path: {e}"
+ f"\n SYMLINK_PATH: {SYMLINK_PATH}, \n node path: {node_path}, \n cwd: {_CACHED_CWD or pathlib.Path.cwd()}"
+ ) from e
+ else:
+ result = node_path
+
+ # Cache before returning
+ _path_cache[cache_key] = result
+ return result
+
+
+__writer = None
+atexit.register(lambda: __writer.close() if __writer else None)
+
+
+def send_execution_message(
+ cwd: str, status: Literal["success", "error"], tests: TestRunResultDict | None
+):
+ """Sends message execution payload details.
+
+ Args:
+ cwd (str): Current working directory.
+ status (Literal["success", "error"]): Execution status indicating success or error.
+ tests (Union[testRunResultDict, None]): Test run results, if available.
+ """
+ payload: ExecutionPayloadDict = ExecutionPayloadDict(
+ cwd=cwd, status=status, result=tests, not_found=None, error=None
+ )
+ if ERRORS:
+ payload["error"] = ERRORS
+ send_message(payload)
+
+
+def send_discovery_message(cwd: str, session_node: TestNode) -> None:
+ """
+ Sends a POST request with test session details in payload.
+
+ Args:
+ cwd (str): Current working directory.
+ session_node (TestNode): Node information of the test session.
+ """
+ payload: DiscoveryPayloadDict = {
+ "cwd": cwd,
+ "status": "success" if not ERRORS else "error",
+ "tests": session_node,
+ "error": [],
+ }
+ if ERRORS is not None:
+ payload["error"] = ERRORS
+ send_message(payload, cls_encoder=PathEncoder)
+
+
+class PathEncoder(json.JSONEncoder):
+ """A custom JSON encoder that encodes pathlib.Path objects as strings."""
+
+ def default(self, o):
+ if isinstance(o, pathlib.Path):
+ return os.fspath(o)
+ return super().default(o)
+
+
+def send_message(
+ payload: ExecutionPayloadDict | DiscoveryPayloadDict | CoveragePayloadDict,
+ cls_encoder=None,
+):
+ """
+ Sends a post request to the server.
+
+ Keyword arguments:
+ payload -- the payload data to be sent.
+ cls_encoder -- a custom encoder if needed.
+ """
+ if not TEST_RUN_PIPE:
+ error_msg = (
+ "PYTEST ERROR: TEST_RUN_PIPE is not set at the time of pytest starting. "
+ "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 VSCodePytestError(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-pytest]: {error}"
+ print(error_msg, file=sys.stderr)
+ print(
+ "If you are on a Windows machine, this error may be occurring if any of your tests clear environment variables"
+ " as they are required to communicate with the extension. Please reference https://docs.pytest.org/en/stable/how-to/monkeypatch.html#monkeypatching-environment-variables"
+ "for the correct way to clear environment variables during testing.\n",
+ file=sys.stderr,
+ )
+ __writer = None
+ raise VSCodePytestError(error_msg) from error
+
+ rpc = {
+ "jsonrpc": "2.0",
+ "params": payload,
+ }
+ data = json.dumps(rpc, cls=cls_encoder)
+ 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"Plugin error connection error[vscode-pytest], writer is None \n[vscode-pytest] data: \n{data} \n",
+ file=sys.stderr,
+ )
+ except Exception as error:
+ print(
+ f"Plugin error, exception thrown while attempting to send data[vscode-pytest]: {error} \n[vscode-pytest] data: \n{data}\n",
+ file=sys.stderr,
+ )
+
+
+class DeferPlugin:
+ @pytest.hookimpl(hookwrapper=True)
+ def pytest_xdist_auto_num_workers(
+ self, config: pytest.Config
+ ) -> Generator[None, Result[int], None]:
+ """Determine how many workers to use based on how many tests were selected in the test explorer."""
+ outcome = yield
+ result = min(outcome.get_result(), len(config.option.file_or_dir))
+ if result == 1:
+ result = 0
+ outcome.force_result(result)
+
+
+def pytest_plugin_registered(plugin: object, manager: pytest.PytestPluginManager):
+ plugin_name = "vscode_xdist"
+ if (
+ # only register the plugin if xdist is enabled:
+ manager.hasplugin("xdist")
+ # prevent infinite recursion:
+ and not isinstance(plugin, DeferPlugin)
+ # prevent this plugin from being registered multiple times:
+ and not manager.hasplugin(plugin_name)
+ ):
+ manager.register(DeferPlugin(), name=plugin_name)
diff --git a/python_files/vscode_pytest/_common.py b/python_files/vscode_pytest/_common.py
new file mode 100644
index 000000000000..9f835f555b6e
--- /dev/null
+++ b/python_files/vscode_pytest/_common.py
@@ -0,0 +1,2 @@
+# def send_post_request():
+# return
diff --git a/python_files/vscode_pytest/run_pytest_script.py b/python_files/vscode_pytest/run_pytest_script.py
new file mode 100644
index 000000000000..50ab12a35423
--- /dev/null
+++ b/python_files/vscode_pytest/run_pytest_script.py
@@ -0,0 +1,73 @@
+# Copyright (c) Microsoft Corporation.
+# Licensed under the MIT License.
+import os
+import pathlib
+import sys
+import sysconfig
+
+import pytest
+
+# 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.parent
+sys.path.append(os.fspath(script_dir))
+sys.path.append(os.fspath(script_dir / "lib" / "python"))
+
+
+def run_pytest(args):
+ arg_array = ["-p", "vscode_pytest", *args]
+ pytest.main(arg_array)
+
+
+# This script handles running pytest via pytest.main(). It is called via run in the
+# pytest execution adapter and gets the test_ids to run via stdin and the rest of the
+# args through sys.argv. It then runs pytest.main() with the args and test_ids.
+
+if __name__ == "__main__":
+ # Add the root directory to the path so that we can import the plugin.
+ directory_path = pathlib.Path(__file__).parent.parent
+ sys.path.append(os.fspath(directory_path))
+ sys.path.insert(0, os.getcwd()) # noqa: PTH109
+ # Get the rest of the args to run with pytest.
+ args = sys.argv[1:]
+
+ # Check if coverage is enabled and adjust the args accordingly.
+ is_coverage_run = os.environ.get("COVERAGE_ENABLED")
+ coverage_enabled = False
+ if is_coverage_run == "True":
+ # If coverage is enabled, check if the coverage plugin is already in the args, if so keep user args.
+ for arg in args:
+ # if '--cov' is an arg or if '--cov=' is in an arg (check to see if this arg is set to not override user intent)
+ if arg == "--cov" or "--cov=" in arg:
+ print("coverage already enabled with specific args")
+ coverage_enabled = True
+ break
+ if not coverage_enabled:
+ args = [*args, "--cov=.", "--cov-branch"]
+
+ run_test_ids_pipe = os.environ.get("RUN_TEST_IDS_PIPE")
+ if run_test_ids_pipe:
+ ids_path = pathlib.Path(run_test_ids_pipe)
+ try:
+ # Read the test ids from the file and run pytest.
+ ids = ids_path.read_text(encoding="utf-8").splitlines()
+ except Exception as e:
+ print("Error[vscode-pytest]: unable to read testIds from temp file" + str(e))
+ run_pytest(args)
+ else:
+ arg_array = ["-p", "vscode_pytest", *args, *ids]
+ print("Running pytest with args: " + str(arg_array))
+ pytest.main(arg_array)
+ finally:
+ # Delete the test ids temp file.
+ try:
+ ids_path.unlink()
+ except Exception as e:
+ print("Error[vscode-pytest]: unable to delete temp file" + str(e))
+ else:
+ print("Error[vscode-pytest]: RUN_TEST_IDS_PIPE env var is not set.")
+ run_pytest(args)
diff --git a/requirements.in b/requirements.in
index c6b52697690c..8bbc9a0f3728 100644
--- a/requirements.in
+++ b/requirements.in
@@ -1,9 +1,15 @@
# This file is used to generate requirements.txt.
# To update requirements.txt, run the following commands.
-# 1) pip install pip-tools
-# 2) pip-compile --generate-hashes requirements.in
+# 1) Install `uv` https://docs.astral.sh/uv/getting-started/installation/
+# 2) uv pip compile --generate-hashes --upgrade requirements.in -o requirements.txt
-# IntelliSense via Jedi
-jedi<0.18 # For Python 2.7 support
-# Sort Imports
-isort==5.8.0; python_version >= '3.6'
+# Unittest test adapter
+typing-extensions==4.15.0
+
+# Fallback env creator for debian
+microvenv
+
+# Checker for installed packages
+importlib_metadata
+packaging
+tomli
diff --git a/requirements.txt b/requirements.txt
index c4cdd1a4f6a0..540590ed2ae7 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,18 +1,71 @@
-#
-# This file is autogenerated by pip-compile
-# To update, run:
-#
-# pip-compile --generate-hashes requirements.in
-#
-isort==5.8.0 ; python_version >= "3.6" \
- --hash=sha256:0a943902919f65c5684ac4e0154b1ad4fac6dcaa5d9f3426b732f1c8b5419be6 \
- --hash=sha256:2bb1680aad211e3c9944dbce1d4ba09a989f04e238296c87fe2139faa26d655d
+# This file was autogenerated by uv via the following command:
+# uv pip compile --generate-hashes requirements.in -o requirements.txt
+importlib-metadata==9.0.0 \
+ --hash=sha256:2d21d1cc5a017bd0559e36150c21c830ab1dc304dedd1b7ea85d20f45ef3edd7 \
+ --hash=sha256:a4f57ab599e6a2e3016d7595cfd72eb4661a5106e787a95bcc90c7105b831efc
# via -r requirements.in
-jedi==0.17.2 \
- --hash=sha256:86ed7d9b750603e4ba582ea8edc678657fb4007894a12bcf6f4bb97892f31d20 \
- --hash=sha256:98cc583fa0f2f8304968199b01b6b4b94f469a1f4a74c1560506ca2a211378b5
+microvenv==2025.0 \
+ --hash=sha256:568155ec18af01c89f270d35d123ab803b09672b480c3702d15fd69e9cc5bd1e \
+ --hash=sha256:8a2568a8390a4ffb5af2f05e7642454e03b887e582d192b6316326974eab5d0f
# via -r requirements.in
-parso==0.7.1 \
- --hash=sha256:97218d9159b2520ff45eb78028ba8b50d2bc61dcc062a9682666f2dc4bd331ea \
- --hash=sha256:caba44724b994a8a5e086460bb212abc5a8bc46951bf4a9a1210745953622eb9
- # via jedi
+packaging==26.2 \
+ --hash=sha256:5fc45236b9446107ff2415ce77c807cee2862cb6fac22b8a73826d0693b0980e \
+ --hash=sha256:ff452ff5a3e828ce110190feff1178bb1f2ea2281fa2075aadb987c2fb221661
+ # via -r requirements.in
+tomli==2.4.1 \
+ --hash=sha256:01f520d4f53ef97964a240a035ec2a869fe1a37dde002b57ebc4417a27ccd853 \
+ --hash=sha256:0d85819802132122da43cb86656f8d1f8c6587d54ae7dcaf30e90533028b49fe \
+ --hash=sha256:136443dbd7e1dee43c68ac2694fde36b2849865fa258d39bf822c10e8068eac5 \
+ --hash=sha256:1d8591993e228b0c930c4bb0db464bdad97b3289fb981255d6c9a41aedc84b2d \
+ --hash=sha256:2190f2e9dd7508d2a90ded5ed369255980a1bcdd58e52f7fe24b8162bf9fedbd \
+ --hash=sha256:2c1c351919aca02858f740c6d33adea0c5deea37f9ecca1cc1ef9e884a619d26 \
+ --hash=sha256:36d2bd2ad5fb9eaddba5226aa02c8ec3fa4f192631e347b3ed28186d43be6b54 \
+ --hash=sha256:3d48a93ee1c9b79c04bb38772ee1b64dcf18ff43085896ea460ca8dec96f35f6 \
+ --hash=sha256:47149d5bd38761ac8be13a84864bf0b7b70bc051806bc3669ab1cbc56216b23c \
+ --hash=sha256:4ab97e64ccda8756376892c53a72bd1f964e519c77236368527f758fbc36a53a \
+ --hash=sha256:4b605484e43cdc43f0954ddae319fb75f04cc10dd80d830540060ee7cd0243cd \
+ --hash=sha256:504aa796fe0569bb43171066009ead363de03675276d2d121ac1a4572397870f \
+ --hash=sha256:51529d40e3ca50046d7606fa99ce3956a617f9b36380da3b7f0dd3dd28e68cb5 \
+ --hash=sha256:52c8ef851d9a240f11a88c003eacb03c31fc1c9c4ec64a99a0f922b93874fda9 \
+ --hash=sha256:559db847dc486944896521f68d8190be1c9e719fced785720d2216fe7022b662 \
+ --hash=sha256:5a881ab208c0baf688221f8cecc5401bd291d67e38a1ac884d6736cbcd8247e9 \
+ --hash=sha256:5cb41aa38891e073ee49d55fbc7839cfdb2bc0e600add13874d048c94aadddd1 \
+ --hash=sha256:5e262d41726bc187e69af7825504c933b6794dc3fbd5945e41a79bb14c31f585 \
+ --hash=sha256:5ee18d9ebdb417e384b58fe414e8d6af9f4e7a0ae761519fb50f721de398dd4e \
+ --hash=sha256:7008df2e7655c495dd12d2a4ad038ff878d4ca4b81fccaf82b714e07eae4402c \
+ --hash=sha256:734e20b57ba95624ecf1841e72b53f6e186355e216e5412de414e3c51e5e3c41 \
+ --hash=sha256:7c7e1a961a0b2f2472c1ac5b69affa0ae1132c39adcb67aba98568702b9cc23f \
+ --hash=sha256:7f86fd587c4ed9dd76f318225e7d9b29cfc5a9d43de44e5754db8d1128487085 \
+ --hash=sha256:7f94b27a62cfad8496c8d2513e1a222dd446f095fca8987fceef261225538a15 \
+ --hash=sha256:88dceee75c2c63af144e456745e10101eb67361050196b0b6af5d717254dddf7 \
+ --hash=sha256:8a650c2dbafa08d42e51ba0b62740dae4ecb9338eefa093aa5c78ceb546fcd5c \
+ --hash=sha256:8d65a2fbf9d2f8352685bc1364177ee3923d6baf5e7f43ea4959d7d8bc326a36 \
+ --hash=sha256:96481a5786729fd470164b47cdb3e0e58062a496f455ee41b4403be77cb5a076 \
+ --hash=sha256:a120733b01c45e9a0c34aeef92bf0cf1d56cfe81ed9d47d562f9ed591a9828ac \
+ --hash=sha256:b1d22e6e9387bf4739fbe23bfa80e93f6b0373a7f1b96c6227c32bef95a4d7a8 \
+ --hash=sha256:b8c198f8c1805dc42708689ed6864951fd2494f924149d3e4bce7710f8eb5232 \
+ --hash=sha256:c2541745709bad0264b7d4705ad453b76ccd191e64aa6f0fc66b69a293a45ece \
+ --hash=sha256:c742f741d58a28940ce01d58f0ab2ea3ced8b12402f162f4d534dfe18ba1cd6a \
+ --hash=sha256:c7f2c7f2b9ca6bdeef8f0fa897f8e05085923eb091721675170254cbc5b02897 \
+ --hash=sha256:d312ef37c91508b0ab2cee7da26ec0b3ed2f03ce12bd87a588d771ae15dcf82d \
+ --hash=sha256:d4d8fe59808a54658fcc0160ecfb1b30f9089906c50b23bcb4c69eddc19ec2b4 \
+ --hash=sha256:da25dc3563bff5965356133435b757a795a17b17d01dbc0f42fb32447ddfd917 \
+ --hash=sha256:eab21f45c7f66c13f2a9e0e1535309cee140182a9cdae1e041d02e47291e8396 \
+ --hash=sha256:eb0dc4e38e6a1fd579e5d50369aa2e10acfc9cace504579b2faabb478e76941a \
+ --hash=sha256:ec9bfaf3ad2df51ace80688143a6a4ebc09a248f6ff781a9945e51937008fcbc \
+ --hash=sha256:ede3e6487c5ef5d28634ba3f31f989030ad6af71edfb0055cbbd14189ff240ba \
+ --hash=sha256:f3c6818a1a86dd6dca7ddcaaf76947d5ba31aecc28cb1b67009a5877c9a64f3f \
+ --hash=sha256:f758f1b9299d059cc3f6546ae2af89670cb1c4d48ea29c3cacc4fe7de3058257 \
+ --hash=sha256:f8f0fc26ec2cc2b965b7a3b87cd19c5c6b8c5e5f436b984e85f486d652285c30 \
+ --hash=sha256:fd0409a3653af6c147209d267a0e4243f0ae46b011aa978b1080359fddc9b6cf \
+ --hash=sha256:ff18e6a727ee0ab0388507b89d1bc6a22b138d1e2fa56d1ad494586d61d2eae9 \
+ --hash=sha256:ff2983983d34813c1aeb0fa89091e76c3a22889ee83ab27c5eeb45100560c049
+ # via -r requirements.in
+typing-extensions==4.15.0 \
+ --hash=sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466 \
+ --hash=sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548
+ # via -r requirements.in
+zipp==3.21.0 \
+ --hash=sha256:2c9958f6430a2040341a52eb608ed6dd93ef4392e02ffe219417c1b28b5dd1f4 \
+ --hash=sha256:ac1bbe05fd2991f160ebce24ffbac5f6d11d83dc90891255885223d42b3cd931
+ # via importlib-metadata
diff --git a/resources/ctagOptions b/resources/ctagOptions
deleted file mode 100644
index 3b656ac370fe..000000000000
--- a/resources/ctagOptions
+++ /dev/null
@@ -1,23 +0,0 @@
---recurse=yes
---tag-relative=yes
---exclude=.git
---exclude=log
---exclude=tmp
---exclude=doc
---exclude=deps
---exclude=node_modules
---exclude=.vscode
---exclude=public/assets
---exclude=*.git*
---exclude=*.pyc
---exclude=*.pyo
---exclude=.DS_Store
---exclude=**/*.jar
---exclude=**/*.class
---exclude=**/.idea/
---exclude=build
---exclude=Builds
---exclude=doc
---fields=Knz
---extra=+f
---append=no
\ No newline at end of file
diff --git a/resources/report_issue_template.md b/resources/report_issue_template.md
index 11cb49166ea5..a95af90ff7fe 100644
--- a/resources/report_issue_template.md
+++ b/resources/report_issue_template.md
@@ -1,46 +1,28 @@
-
-
+
# Behaviour
-[**NOTE**: If you suspect that your issue is related to the Microsoft Python Language Server (`python.languageServer: 'Microsoft'`), please download our new language server [Pylance](https://marketplace.visualstudio.com/items?itemName=ms-python.vscode-pylance) from the VS Code marketplace to see if that fixes your issue]
-
-
-## Expected
-
-XXX
-
-## Actual
-
XXX
## Steps to reproduce:
-[**NOTE**: Self-contained, minimal reproducing code samples are **extremely** helpful and will expedite addressing your issue]
-
-1.
+1. XXX
+
# Diagnostic data
-- 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}
-
-"Python" channel in the OUTPUT panel
+Output for Python in the Output panel (View→Output, change the drop-down the upper-right of the Output panel to Python)
+
-
```
-{3}
+XXX
```
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
new file mode 100644
index 000000000000..7e034651c46d
--- /dev/null
+++ b/resources/report_issue_user_settings.json
@@ -0,0 +1,99 @@
+{
+ "initialize": false,
+ "pythonPath": "placeholder",
+ "onDidChange": false,
+ "defaultInterpreterPath": "placeholder",
+ "defaultLS": false,
+ "envFile": "placeholder",
+ "venvPath": "placeholder",
+ "venvFolders": "placeholder",
+ "activeStateToolPath": "placeholder",
+ "condaPath": "placeholder",
+ "pipenvPath": "placeholder",
+ "poetryPath": "placeholder",
+ "pixiToolPath": "placeholder",
+ "devOptions": false,
+ "globalModuleInstallation": false,
+ "languageServer": true,
+ "languageServerIsDefault": false,
+ "logging": true,
+ "useIsolation": false,
+ "changed": false,
+ "_pythonPath": false,
+ "_defaultInterpreterPath": false,
+ "workspace": false,
+ "workspaceRoot": false,
+ "linting": {
+ "enabled": true,
+ "cwd": "placeholder",
+ "flake8Args": "placeholder",
+ "flake8CategorySeverity": false,
+ "flake8Enabled": true,
+ "flake8Path": "placeholder",
+ "ignorePatterns": false,
+ "lintOnSave": true,
+ "maxNumberOfProblems": false,
+ "banditArgs": "placeholder",
+ "banditEnabled": true,
+ "banditPath": "placeholder",
+ "mypyArgs": "placeholder",
+ "mypyCategorySeverity": false,
+ "mypyEnabled": true,
+ "mypyPath": "placeholder",
+ "pycodestyleArgs": "placeholder",
+ "pycodestyleCategorySeverity": false,
+ "pycodestyleEnabled": true,
+ "pycodestylePath": "placeholder",
+ "prospectorArgs": "placeholder",
+ "prospectorEnabled": true,
+ "prospectorPath": "placeholder",
+ "pydocstyleArgs": "placeholder",
+ "pydocstyleEnabled": true,
+ "pydocstylePath": "placeholder",
+ "pylamaArgs": "placeholder",
+ "pylamaEnabled": true,
+ "pylamaPath": "placeholder",
+ "pylintArgs": "placeholder",
+ "pylintCategorySeverity": false,
+ "pylintEnabled": false,
+ "pylintPath": "placeholder"
+ },
+ "analysis": {
+ "completeFunctionParens": true,
+ "autoImportCompletions": true,
+ "autoSearchPaths": "placeholder",
+ "stubPath": "placeholder",
+ "diagnosticMode": true,
+ "extraPaths": "placeholder",
+ "useLibraryCodeForTypes": true,
+ "typeCheckingMode": true,
+ "memory": true,
+ "symbolsHierarchyDepthLimit": false
+ },
+ "testing": {
+ "cwd": "placeholder",
+ "debugPort": true,
+ "promptToConfigure": true,
+ "pytestArgs": "placeholder",
+ "pytestEnabled": true,
+ "pytestPath": "placeholder",
+ "unittestArgs": "placeholder",
+ "unittestEnabled": true,
+ "autoTestDiscoverOnSaveEnabled": true,
+ "autoTestDiscoverOnSavePattern": "placeholder"
+ },
+ "terminal": {
+ "activateEnvironment": true,
+ "executeInFileDir": "placeholder",
+ "launchArgs": "placeholder",
+ "activateEnvInCurrentTerminal": false
+ },
+ "tensorBoard": {
+ "logDirectory": "placeholder"
+ },
+ "experiments": {
+ "enabled": true,
+ "optInto": true,
+ "optOutFrom": true
+ }
+}
diff --git a/resources/walkthrough/create-environment.svg b/resources/walkthrough/create-environment.svg
new file mode 100644
index 000000000000..bb48e1b16711
--- /dev/null
+++ b/resources/walkthrough/create-environment.svg
@@ -0,0 +1,75 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/resources/walkthrough/create-notebook.svg b/resources/walkthrough/create-notebook.svg
new file mode 100644
index 000000000000..05dadc0cc6de
--- /dev/null
+++ b/resources/walkthrough/create-notebook.svg
@@ -0,0 +1,75 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/resources/walkthrough/data-science.svg b/resources/walkthrough/data-science.svg
new file mode 100644
index 000000000000..506bed2161b1
--- /dev/null
+++ b/resources/walkthrough/data-science.svg
@@ -0,0 +1,42 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
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
+
+
+
+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/resources/walkthrough/install-python-linux.md b/resources/walkthrough/install-python-linux.md
new file mode 100644
index 000000000000..78a12870799f
--- /dev/null
+++ b/resources/walkthrough/install-python-linux.md
@@ -0,0 +1,22 @@
+# Install Python on Linux
+
+To install the latest version of Python on [Debian-based Linux distributions](https://www.debian.org/), you can create a new terminal (Ctrl + Shift + ` ) and run the following commands:
+
+
+```
+sudo apt-get update
+sudo apt-get install python3 python3-venv python3-pip
+```
+
+For [Fedora-based Linux distributions](https://getfedora.org/), you can run the following:
+
+```
+sudo dnf install python3
+```
+
+To verify if Python was successfully installed, run the following command in the terminal:
+
+
+```
+python3 --version
+```
diff --git a/resources/walkthrough/install-python-macos.md b/resources/walkthrough/install-python-macos.md
new file mode 100644
index 000000000000..470d682d4eb2
--- /dev/null
+++ b/resources/walkthrough/install-python-macos.md
@@ -0,0 +1,15 @@
+# Install Python on macOS
+
+If you have [Homebrew](https://brew.sh/) installed, you can install Python by running the following command in the terminal (Ctrl + Shift + ` ):
+
+```
+brew install python3
+```
+
+If you don't have Homebrew, you can download a Python installer for macOS from [python.org](https://www.python.org/downloads/mac-osx/).
+
+To verify if Python was successfully installed, run the following command in the terminal:
+
+```
+python3 --version
+```
diff --git a/resources/walkthrough/install-python-windows-8.md b/resources/walkthrough/install-python-windows-8.md
new file mode 100644
index 000000000000..f25f2f7d024d
--- /dev/null
+++ b/resources/walkthrough/install-python-windows-8.md
@@ -0,0 +1,15 @@
+## Install Python on Windows
+
+If you don't have Python installed on your Windows machine, you can install it [from python.org](https://www.python.org/downloads).
+
+To verify it's installed, create a new terminal (Ctrl + Shift + ` ) and try running the following command:
+
+```
+python --version
+```
+
+You should see something similar to the following:
+```
+Python 3.9.5
+```
+For additional information about using Python on Windows, see [Using Python on Windows at Python.org](https://docs.python.org/3.10/using/windows.html).
diff --git a/resources/walkthrough/interactive-window.svg b/resources/walkthrough/interactive-window.svg
new file mode 100644
index 000000000000..83446ed8e66a
--- /dev/null
+++ b/resources/walkthrough/interactive-window.svg
@@ -0,0 +1,67 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/resources/walkthrough/learnmore.svg b/resources/walkthrough/learnmore.svg
new file mode 100644
index 000000000000..c5fd67e75471
--- /dev/null
+++ b/resources/walkthrough/learnmore.svg
@@ -0,0 +1,94 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/resources/walkthrough/open-folder.svg b/resources/walkthrough/open-folder.svg
new file mode 100644
index 000000000000..1615718a83dd
--- /dev/null
+++ b/resources/walkthrough/open-folder.svg
@@ -0,0 +1,91 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/resources/walkthrough/play-button-dark.png b/resources/walkthrough/play-button-dark.png
new file mode 100644
index 000000000000..113ad62b87c2
Binary files /dev/null and b/resources/walkthrough/play-button-dark.png differ
diff --git a/resources/walkthrough/python-interpreter.svg b/resources/walkthrough/python-interpreter.svg
new file mode 100644
index 000000000000..0f6e262321ec
--- /dev/null
+++ b/resources/walkthrough/python-interpreter.svg
@@ -0,0 +1,82 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/resources/walkthrough/rundebug2.svg b/resources/walkthrough/rundebug2.svg
new file mode 100644
index 000000000000..6d1fe753cc4f
--- /dev/null
+++ b/resources/walkthrough/rundebug2.svg
@@ -0,0 +1,84 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
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/sprint-planning.github-issues b/sprint-planning.github-issues
old mode 100755
new mode 100644
index 73c40eae0253..1fbd09a790e8
--- a/sprint-planning.github-issues
+++ b/sprint-planning.github-issues
@@ -2,97 +2,71 @@
{
"kind": 1,
"language": "markdown",
- "value": "# Query constants",
- "editable": true
+ "value": "# Query constants"
},
{
"kind": 2,
"language": "github-issues",
- "value": "$pvsc=repo:microsoft/vscode-python\n$not_DS=-label:\"data science\"\n$open=is:open",
- "editable": true
+ "value": "$pvsc=repo:microsoft/vscode-python\n$open=is:open\n$upvotes=sort:reactions-+1-desc"
},
{
"kind": 1,
"language": "markdown",
- "value": "# Priority issues 🚨",
- "editable": true
+ "value": "# Priority issues 🚨"
},
{
"kind": 1,
"language": "markdown",
- "value": "## P0",
- "editable": true
+ "value": "## Important/P1"
},
{
"kind": 2,
"language": "github-issues",
- "value": "$pvsc $not_DS $open label:\"P0\"",
- "editable": true
+ "value": "$pvsc $open label:\"important\""
},
{
"kind": 1,
"language": "markdown",
- "value": "## P1",
- "editable": true
+ "value": "# Regressions 🔙"
},
{
"kind": 2,
"language": "github-issues",
- "value": "$pvsc $not_DS $open label:\"P1\"",
- "editable": true
+ "value": "$pvsc $open label:\"regression\""
},
{
"kind": 1,
"language": "markdown",
- "value": "# Regressions 🔙",
- "editable": true
+ "value": "# Partner asks"
},
{
"kind": 2,
"language": "github-issues",
- "value": "$pvsc $not_DS $open label:\"reason-regression\"",
- "editable": true
+ "value": "$pvsc $open label:\"partner ask\""
},
{
"kind": 1,
"language": "markdown",
- "value": "# Partner asks",
- "editable": true
- },
- {
- "kind": 2,
- "language": "github-issues",
- "value": "$pvsc $not_DS $open label:\"partner ask\"",
- "editable": true
- },
- {
- "kind": 1,
- "language": "markdown",
- "value": "# Upvotes 👍",
- "editable": true
+ "value": "# Upvotes 👍"
},
{
"kind": 1,
"language": "markdown",
- "value": "## Enhancements 💪",
- "editable": true
+ "value": "## Enhancements 💪"
},
{
"kind": 2,
"language": "github-issues",
- "value": "$pvsc $not_DS $open sort:reactions-+1-desc label:\"type-enhancement\" ",
- "editable": true
+ "value": "$pvsc $open $upvotes label:\"feature-request\" "
},
{
"kind": 1,
"language": "markdown",
- "value": "## Bugs 🐜",
- "editable": true
+ "value": "## Bugs 🐜"
},
{
"kind": 2,
"language": "github-issues",
- "value": "$pvsc $not_DS $open sort:reactions-+1-desc label:\"type-bug\"",
- "editable": true
+ "value": "$pvsc $open $upvotes label:\"bug\""
}
-]
\ No newline at end of file
+]
diff --git a/src/client/activation/activationManager.ts b/src/client/activation/activationManager.ts
index bd7fc45f6517..9e97c5c48857 100644
--- a/src/client/activation/activationManager.ts
+++ b/src/client/activation/activationManager.ts
@@ -8,13 +8,12 @@ import { TextDocument } from 'vscode';
import { IApplicationDiagnostics } from '../application/types';
import { IActiveResourceService, IDocumentManager, IWorkspaceService } from '../common/application/types';
import { PYTHON_LANGUAGE } from '../common/constants';
-import { DeprecatePythonPath } from '../common/experiments/groups';
-import { traceDecorators } from '../common/logger';
import { IFileSystem } from '../common/platform/types';
-import { IDisposable, IExperimentService, IInterpreterPathService, Resource } from '../common/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 { IInterpreterService } from '../interpreter/contracts';
+import { traceDecoratorError } from '../logging';
import { sendActivationTelemetry } from '../telemetry/envFileTelemetry';
import { IExtensionActivationManager, IExtensionActivationService, IExtensionSingleActivationService } from './types';
@@ -29,20 +28,37 @@ export class ExtensionActivationManager implements IExtensionActivationManager {
private docOpenedHandler?: IDisposable;
constructor(
- @multiInject(IExtensionActivationService) private readonly activationServices: IExtensionActivationService[],
+ @multiInject(IExtensionActivationService) private activationServices: IExtensionActivationService[],
@multiInject(IExtensionSingleActivationService)
- private readonly singleActivationServices: IExtensionSingleActivationService[],
+ private singleActivationServices: IExtensionSingleActivationService[],
@inject(IDocumentManager) private readonly documentManager: IDocumentManager,
- @inject(IInterpreterService) private readonly interpreterService: IInterpreterService,
@inject(IInterpreterAutoSelectionService) private readonly autoSelection: IInterpreterAutoSelectionService,
@inject(IApplicationDiagnostics) private readonly appDiagnostics: IApplicationDiagnostics,
@inject(IWorkspaceService) private readonly workspaceService: IWorkspaceService,
@inject(IFileSystem) private readonly fileSystem: IFileSystem,
@inject(IActiveResourceService) private readonly activeResourceService: IActiveResourceService,
- @inject(IExperimentService) private readonly experiments: IExperimentService,
@inject(IInterpreterPathService) private readonly interpreterPathService: IInterpreterPathService,
) {}
+ private filterServices() {
+ if (!this.workspaceService.isTrusted) {
+ this.activationServices = this.activationServices.filter(
+ (service) => service.supportedWorkspaceTypes.untrustedWorkspace,
+ );
+ this.singleActivationServices = this.singleActivationServices.filter(
+ (service) => service.supportedWorkspaceTypes.untrustedWorkspace,
+ );
+ }
+ if (this.workspaceService.isVirtualWorkspace) {
+ this.activationServices = this.activationServices.filter(
+ (service) => service.supportedWorkspaceTypes.virtualWorkspace,
+ );
+ this.singleActivationServices = this.singleActivationServices.filter(
+ (service) => service.supportedWorkspaceTypes.virtualWorkspace,
+ );
+ }
+ }
+
public dispose(): void {
while (this.disposables.length > 0) {
const disposable = this.disposables.shift()!;
@@ -54,35 +70,35 @@ export class ExtensionActivationManager implements IExtensionActivationManager {
}
}
- public async activate(): Promise {
+ public async activate(startupStopWatch: StopWatch): Promise {
+ this.filterServices();
await this.initialize();
+
// Activate all activation services together.
+
await Promise.all([
- Promise.all(this.singleActivationServices.map((item) => item.activate())),
- this.activateWorkspace(this.activeResourceService.getActiveResource()),
+ ...this.singleActivationServices.map((item) => item.activate()),
+ this.activateWorkspace(this.activeResourceService.getActiveResource(), startupStopWatch),
]);
- await this.autoSelection.autoSelectInterpreter(undefined);
}
- @traceDecorators.error('Failed to activate a workspace')
- public async activateWorkspace(resource: Resource): Promise {
+ @traceDecoratorError('Failed to activate a workspace')
+ 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;
}
this.activatedWorkspaces.add(key);
- if (this.experiments.inExperimentSync(DeprecatePythonPath.experiment)) {
+ if (this.workspaceService.isTrusted) {
+ // Do not interact with interpreters in a untrusted workspace.
+ await this.autoSelection.autoSelectInterpreter(resource);
await this.interpreterPathService.copyOldInterpreterStorageValuesToNew(resource);
}
-
- // Get latest interpreter list in the background.
- this.interpreterService.getInterpreters(resource).ignoreErrors();
-
await sendActivationTelemetry(this.fileSystem, this.workspaceService, resource);
-
- await this.autoSelection.autoSelectInterpreter(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);
}
@@ -96,15 +112,15 @@ export class ExtensionActivationManager implements IExtensionActivationManager {
return;
}
const key = this.getWorkspaceKey(doc.uri);
+ const hasWorkspaceFolders = (this.workspaceService.workspaceFolders?.length || 0) > 0;
// If we have opened a doc that does not belong to workspace, then do nothing.
- if (key === '' && this.workspaceService.hasWorkspaceFolders) {
+ if (key === '' && hasWorkspaceFolders) {
return;
}
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 {
@@ -140,7 +156,7 @@ export class ExtensionActivationManager implements IExtensionActivationManager {
}
protected hasMultipleWorkspaces(): boolean {
- return this.workspaceService.hasWorkspaceFolders && this.workspaceService.workspaceFolders!.length > 1;
+ return (this.workspaceService.workspaceFolders?.length || 0) > 1;
}
protected getWorkspaceKey(resource: Resource): string {
diff --git a/src/client/activation/activationService.ts b/src/client/activation/activationService.ts
deleted file mode 100644
index 82705898e514..000000000000
--- a/src/client/activation/activationService.ts
+++ /dev/null
@@ -1,351 +0,0 @@
-// Copyright (c) Microsoft Corporation. All rights reserved.
-// Licensed under the MIT License.
-import '../common/extensions';
-
-import { inject, injectable } from 'inversify';
-import { ConfigurationChangeEvent, Disposable, OutputChannel, Uri } from 'vscode';
-
-import { LSNotSupportedDiagnosticServiceId } from '../application/diagnostics/checks/lsNotSupported';
-import { IDiagnosticsService } from '../application/diagnostics/types';
-import { IApplicationShell, ICommandManager, IWorkspaceService } from '../common/application/types';
-import { STANDARD_OUTPUT_CHANNEL } from '../common/constants';
-import { traceError } from '../common/logger';
-import {
- IConfigurationService,
- IDisposableRegistry,
- IExtensions,
- IOutputChannel,
- IPersistentStateFactory,
- IPythonSettings,
- Resource,
-} from '../common/types';
-import { swallowExceptions } from '../common/utils/decorators';
-import { LanguageService } from '../common/utils/localize';
-import { noop } from '../common/utils/misc';
-import { IInterpreterService } from '../interpreter/contracts';
-import { IServiceContainer } from '../ioc/types';
-import { PythonEnvironment } from '../pythonEnvironments/info';
-import { sendTelemetryEvent } from '../telemetry';
-import { EventName } from '../telemetry/constants';
-import { Commands } from './commands';
-import { LanguageServerChangeHandler } from './common/languageServerChangeHandler';
-import { RefCountedLanguageServer } from './refCountedLanguageServer';
-import {
- IExtensionActivationService,
- ILanguageServerActivator,
- ILanguageServerCache,
- LanguageServerType,
-} from './types';
-import { StopWatch } from '../common/utils/stopWatch';
-
-const languageServerSetting: keyof IPythonSettings = 'languageServer';
-const workspacePathNameForGlobalWorkspaces = '';
-
-interface IActivatedServer {
- key: string;
- server: ILanguageServerActivator;
- jedi: boolean;
-}
-
-@injectable()
-export class LanguageServerExtensionActivationService
- implements IExtensionActivationService, ILanguageServerCache, Disposable {
- private cache = new Map>();
-
- private activatedServer?: IActivatedServer;
-
- private readonly workspaceService: IWorkspaceService;
-
- private readonly output: OutputChannel;
-
- private readonly interpreterService: IInterpreterService;
-
- private readonly languageServerChangeHandler: LanguageServerChangeHandler;
-
- private resource!: Resource;
-
- constructor(
- @inject(IServiceContainer) private serviceContainer: IServiceContainer,
- @inject(IPersistentStateFactory) private stateFactory: IPersistentStateFactory,
- ) {
- this.workspaceService = this.serviceContainer.get(IWorkspaceService);
- this.interpreterService = this.serviceContainer.get(IInterpreterService);
- this.output = this.serviceContainer.get(IOutputChannel, STANDARD_OUTPUT_CHANNEL);
-
- const commandManager = this.serviceContainer.get(ICommandManager);
- const disposables = serviceContainer.get(IDisposableRegistry);
- disposables.push(this);
- disposables.push(this.workspaceService.onDidChangeConfiguration(this.onDidChangeConfiguration.bind(this)));
- disposables.push(this.workspaceService.onDidChangeWorkspaceFolders(this.onWorkspaceFoldersChanged, this));
- disposables.push(this.interpreterService.onDidChangeInterpreter(this.onDidChangeInterpreter.bind(this)));
- disposables.push(
- commandManager.registerCommand(Commands.ClearAnalyisCache, this.onClearAnalysisCaches.bind(this)),
- );
-
- this.languageServerChangeHandler = new LanguageServerChangeHandler(
- this.getCurrentLanguageServerType(),
- this.serviceContainer.get(IExtensions),
- this.serviceContainer.get(IApplicationShell),
- this.serviceContainer.get(ICommandManager),
- this.serviceContainer.get(IWorkspaceService),
- this.serviceContainer.get(IConfigurationService),
- );
- disposables.push(this.languageServerChangeHandler);
- }
-
- public async activate(resource: Resource): Promise {
- const stopWatch = new StopWatch();
- // Get a new server and dispose of the old one (might be the same one)
- this.resource = resource;
- const interpreter = await this.interpreterService.getActiveInterpreter(resource);
- const key = await this.getKey(resource, interpreter);
-
- // If we have an old server with a different key, then deactivate it as the
- // creation of the new server may fail if this server is still connected
- if (this.activatedServer && this.activatedServer.key !== key) {
- this.activatedServer.server.deactivate();
- }
-
- // Get the new item
- const result = await this.get(resource, interpreter);
-
- // Now we dispose. This ensures the object stays alive if it's the same object because
- // we dispose after we increment the ref count.
- if (this.activatedServer) {
- this.activatedServer.server.dispose();
- }
-
- // Save our active server.
- this.activatedServer = { key, server: result, jedi: result.type === LanguageServerType.Jedi };
-
- // Force this server to reconnect (if disconnected) as it should be the active
- // language server for all of VS code.
- this.activatedServer.server.activate();
- sendTelemetryEvent(EventName.PYTHON_LANGUAGE_SERVER_STARTUP_DURATION, stopWatch.elapsedTime, {
- languageServerType: result.type,
- });
- }
-
- public async get(resource: Resource, interpreter?: PythonEnvironment): Promise {
- // See if we already have it or not
- const key = await this.getKey(resource, interpreter);
- let result: Promise | undefined = this.cache.get(key);
- if (!result) {
- // Create a special ref counted result so we don't dispose of the
- // server too soon.
- result = this.createRefCountedServer(resource, interpreter, key);
- this.cache.set(key, result);
- } else {
- // Increment ref count if already exists.
- result = result.then((r) => {
- r.increment();
- return r;
- });
- }
- return result;
- }
-
- public dispose(): void {
- if (this.activatedServer) {
- this.activatedServer.server.dispose();
- }
- }
-
- @swallowExceptions('Send telemetry for language server current selection')
- public async sendTelemetryForChosenLanguageServer(languageServer: LanguageServerType): Promise {
- const state = this.stateFactory.createGlobalPersistentState