diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..0f17867 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,9 @@ +root = true + +[*] +charset = utf-8 +end_of_line = lf +indent_size = 2 +indent_style = space +insert_final_newline = true +trim_trailing_whitespace = true diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 3f84761..bbac89f 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -29,12 +29,15 @@ jobs: run: | npm run build npm run package + - name: Run unit tests + run: | + xvfb-run -a node ./out/test/runFrameworkTests.js unitTests - name: Run minitest tests run: | - xvfb-run -a node ./out/test/runMinitestTests.js + xvfb-run -a node ./out/test/runFrameworkTests.js minitest - name: Run rspec tests run: | - xvfb-run -a node ./out/test/runRspecTests.js + xvfb-run -a node ./out/test/runFrameworkTests.js rspec - name: Run Ruby test run: | cd ruby && bundle exec rake diff --git a/.gitignore b/.gitignore index a0d6d58..2bc19e0 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,6 @@ out/ ruby/Gemfile.lock /.vscode-test *.vsix +.rbenv-gemsets +.ruby-version +**/.bundle/ diff --git a/.nvmrc b/.nvmrc new file mode 100644 index 0000000..ab2e7f4 --- /dev/null +++ b/.nvmrc @@ -0,0 +1,2 @@ +lts/* + diff --git a/.vscode/launch.json b/.vscode/launch.json index b7a6aed..fe17153 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -20,10 +20,11 @@ "runtimeExecutable": "${execPath}", "args": [ "--extensionDevelopmentPath=${workspaceFolder}", - "--extensionTestsPath=${workspaceFolder}/out/test/suite/frameworks/minitest/index", + "--extensionTestsPath=${workspaceFolder}/out/test/suite", "${workspaceFolder}/test/fixtures/minitest" ], - "outFiles": ["${workspaceFolder}/out/test/**/*.js"] + "outFiles": ["${workspaceFolder}/out/test/**/*.js"], + "env": { "TEST_SUITE": "minitest" } }, { "name": "Run tests for RSpec", @@ -32,10 +33,24 @@ "runtimeExecutable": "${execPath}", "args": [ "--extensionDevelopmentPath=${workspaceFolder}", - "--extensionTestsPath=${workspaceFolder}/out/test/suite/frameworks/rspec/index", + "--extensionTestsPath=${workspaceFolder}/out/test/suite", "${workspaceFolder}/test/fixtures/rspec" ], - "outFiles": ["${workspaceFolder}/out/test/**/**/*.js"] + "outFiles": ["${workspaceFolder}/out/test/**/*.js"], + "env": { "TEST_SUITE": "rspec" } + }, + { + "name": "Run unit tests", + "type": "extensionHost", + "request": "launch", + "runtimeExecutable": "${execPath}", + "args": [ + "--extensionDevelopmentPath=${workspaceFolder}", + "--extensionTestsPath=${workspaceFolder}/out/test/suite", + "${workspaceFolder}/test/fixtures/unitTests" + ], + "outFiles": ["${workspaceFolder}/out/test/**/*.js"], + "env": { "TEST_SUITE": "unitTests" } } ] } diff --git a/.vscode/settings.json b/.vscode/settings.json index 1c715b5..12a57ed 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -11,5 +11,6 @@ "ruby.format": false, "ruby.lint": { "rubocop": false - } + }, + "task.allowAutomaticTasks": "on" } diff --git a/CHANGELOG.md b/CHANGELOG.md index 40b9962..55ed75a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,9 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). ## [Unreleased] +### Changed +- Rewrite extension to use the native [VSCode testing API](https://code.visualstudio.com/api/extension-guides/testing) instead of the older one provided by the [Test Explorer UI](https://marketplace.visualstudio.com/items?itemName=hbenl.vscode-test-explorer) extension. + - Allows for partial test tree updates, lazy loading of tests, much more responsive UI, filtering of tests, as well as greater efficiency by interacting directly with VSCode instead of going through another extension, and much more. ## [0.9.2] - 2023-01-17 ### Fixed diff --git a/README.md b/README.md index adea8a2..80c3d39 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,5 @@ # Ruby Test Explorer + **[Install it from the VS Code Marketplace.](https://marketplace.visualstudio.com/items?itemName=connorshea.vscode-ruby-test-adapter)** This is a Ruby Test Explorer extension for the [VS Code Test Explorer](https://marketplace.visualstudio.com/items?itemName=hbenl.vscode-test-explorer) extension. @@ -49,6 +50,7 @@ Property | Description ---------------------------------------|--------------------------------------------------------------- `rubyTestExplorer.logpanel` | Whether to write diagnotic logs to an output panel. `rubyTestExplorer.logfile` | Write diagnostic logs to the given file. +`rubyTestExplorer.logLevel` | The level of information to log. One of `off` (no logs), `error`, `warn`, `info` (default), `debug`, or `trace` (all logs). Note: `debug` and `trace` are very noisy and are not reccomended for daily use. `rubyTestExplorer.testFramework` | `none`, `auto`, `rspec`, or `minitest`. `auto` by default, which automatically detects the test framework based on the gems listed by Bundler. Can disable the extension functionality with `none` or set the test framework explicitly, if auto-detect isn't working properly. `rubyTestExplorer.filePattern` | Define the pattern to match test files by, for example `["*_test.rb", "test_*.rb", "*_spec.rb"]`. `rubyTestExplorer.debuggerHost` | Define the host to connect the debugger to, for example `127.0.0.1`. @@ -92,7 +94,7 @@ There are two groups of tests included in the repository. - Tests for Ruby scripts to collect test information and run tests. Run with `bundle exec rake` in `ruby` directory. - Tests for VS Code extension which invokes the Ruby scripts. Run from VS Code's debug panel with the "Run tests for" configurations. - - There are separate debug configurations for each supported test framework. + - There are separate debug configurations for each supported test framework, as well as unit tests for the extension itself. - Note that you'll need to run `npm run build && npm run package` before you'll be able to successfully run the extension tests. You'll also need to re-run these every time you make changes to the extension code or your tests. You can see `.github/workflows/test.yml` for CI configurations. diff --git a/bin/setup b/bin/setup index 736a08c..bb37feb 100755 --- a/bin/setup +++ b/bin/setup @@ -7,3 +7,4 @@ npm install bundle install --gemfile=ruby/Gemfile bundle install --gemfile=test/fixtures/rspec/Gemfile bundle install --gemfile=test/fixtures/minitest/Gemfile +bundle install --gemfile=test/fixtures/rspec/Gemfile diff --git a/docs/architecture.md b/docs/architecture.md new file mode 100644 index 0000000..316ee81 --- /dev/null +++ b/docs/architecture.md @@ -0,0 +1,136 @@ +# Architecture + +This extension is essentially comprised of 4 main parts: + +- [Configuration and setup](#configuration-and-setup) +- [Management of the test tree state](#management-of-the-test-tree-state) +- [Discovering tests](#discovering-tests) + - [1. File changes](#1-file-changes) + - [2. Resolve handler](#2-resolve-handler) + - [Loading queue](#loading-queue) +- [Running tests](#running-tests) + +As much as possible, anything that might need to be cleaned up or cancelled extends the [Disposable](https://code.visualstudio.com/api/references/vscode-api#Disposable) interface, so that it provides a clear, uniform way for other classes to do so. + +## Configuration and setup + +These parts of the extension are the most straightforward: + +- `Config` + - Abstract base class for test framework configuration. + - Ensures that the rest of the extension does not need to care which test framework is being used when building commands to run, or getting file patterns, etc. +- `RspecConfig` & `MinitestConfig` + - Framework-specific subclasses of `Config`. + - Implement the abstract functions from `Config` as well as any other configuration/processing needed to supply the extension with the relevant data to interact correctly with their respective frameworks. +- `TestFactory` + - Creates the `TestLoader` and `TestRunner` instances used by the extension. + - Disposes of the `TestLoader` and `TestRunner` if necessary when the configuration changes, so that they clean up and terminate any running processes and can be recreated for the new configuration. +- `main.ts` + - Extension entry point - called by VSCode to intialize the extension + - Creates the logger, `TestController` (see [Management of the test tree state](#management-of-the-test-tree-state)), `TestFactory`, and `Config` instances + - Creates the three [TestRunProfile](https://code.visualstudio.com/api/references/vscode-api#TestRunProfile) instances used by the extension (see [Running tests](#running-tests)) + - Creates the debug configuration used by the `Debug` profile + - Registers the controller, factory and profiles with VSCode to be disposed of when the extension is unloaded + - Registers a `resolveHandler` with the controller and initializes the `TestLoader` (see [Discovering tests](#discovering-tests)) + +## Management of the test tree state + +There are only two classes that deal with this: + +- [TestController](https://code.visualstudio.com/api/references/vscode-api#TestController) + - Part of the VSCode API, and is the link between VSCode and this extension +- `TestSuiteManager` + - Provides functions for the rest of the extension to use to update and access the test tree. + +The [TestController](https://code.visualstudio.com/api/references/vscode-api#TestController) instance is the heart of the VSCode testing API: + +- It is used to create [TestRunProfiles](https://code.visualstudio.com/api/references/vscode-api#TestRunProfile), which make it easy for tests to be run in different ways +- It is used to create [TestItems](https://code.visualstudio.com/api/references/vscode-api#TestItem) which represent the various folders, files, describes/contexts/groups, and tests +- It holds the list of known tests allowing them to be displayed in the UI, and run/discovered via the handler functions registered on the controller or the profiles created with it. +- It is used to create [TestRuns](https://code.visualstudio.com/api/references/vscode-api#TestRun) from [TestRunRequests](https://code.visualstudio.com/api/references/vscode-api#TestRunRequest), which are used for reporting the statuses of tests that are run, as well as grouping results. + +The classes and functions provided by the VSCode API for managing the state of the test tree are, by necessity, very basic as they cannot predict what will be appropriate for any particular test provider. For example, the [TestItemCollection](https://code.visualstudio.com/api/references/vscode-api#TestItemCollection) used by the controller to hold the known tests cannot retrieve [TestItems](https://code.visualstudio.com/api/references/vscode-api#TestItem) that are children of the top-level items. + +Because of this, as well as other constraints we must satisfy when using the testing API, and to make things easier in the rest of the extension, we have the `TestSuiteManager` class which keeps all the logic for managing a hierarchy of tests in a single place. It takes care of the following: + +- Creating [TestItem](https://code.visualstudio.com/api/references/vscode-api#TestItem) instances to ensure that all the following constraints are always satisfied: + - Test IDs must all be unique. We use the relative path to the test from the test folder root, and its location, e.g. for the second RSpec test in `./spec/foo/bar_spec.rb`, the ID would be `foo/bar_spec.rb[1:2]`. + - Test item URIs are optional, but we want them to always be set with both the absolute file path and [Range](https://code.visualstudio.com/api/references/vscode-api#TestItem). + - `canResolveChildren` should be `true` for all items that can have children which is non-trivial to determine. + - Folders, files and some test groups don't get included in test framework output, so we need to ensure that all parent items are also created when an item needs creating. +- Retrieving [TestItems](https://code.visualstudio.com/api/references/vscode-api#TestItem) + - For the same reason that we need to create parents when creating items, we also have to walk the test tree to find a test item when needed. +- Deleting [TestItems](https://code.visualstudio.com/api/references/vscode-api#TestItem) + - To delete an item we also have to walk the tree to find the collection that contains it. +- Normalising IDs + - Because we use the relative path to a test file as part of the ID, it's helpful to allow other classes to not have to worry about things like stripping leading `/`s, `./`s, etc + +## Discovering tests + +([VS Code API docs](https://code.visualstudio.com/api/extension-guides/testing#discovering-tests)) + +Discovering tests is done by the `TestLoader` class in two main ways: + +### 1. File changes + +When the `TestLoader` is created, or when the configuration that affects finding test files is changed, a set of [FileSystemWatchers](https://code.visualstudio.com/api/references/vscode-api#FileSystemWatcher) are created using the configured file patterns. + +1. When a file is created: + A [TestItem](https://code.visualstudio.com/api/references/vscode-api#TestItem) is created for the new file and added to the tree. +2. When a file is changed: + The [TestItem](https://code.visualstudio.com/api/references/vscode-api#TestItem) for the changed file is retrieved from the tree, and enqueued to be loaded by the test framework to get new/updated information about the tests within it. +3. When a file is deleted: + The [TestItem](https://code.visualstudio.com/api/references/vscode-api#TestItem) for the deleted file is removed from the [TestItemCollection](https://code.visualstudio.com/api/references/vscode-api#TestItemCollection) that contains it, along with all its children. + +### 2. Resolve handler + +When the extension is initialized, a `resolveHandler` function is registered with the [TestController](https://code.visualstudio.com/api/references/vscode-api#TestController). + +This function is called called whenever an item that may contain children is expanded in the test sidebar by clicking on the arrow icon, and is passed the relevant [TestItem](https://code.visualstudio.com/api/references/vscode-api#TestItem) from the tree as a parameter. This item is then enqueued to be loaded by the test framework. + +This function It may also be called with no arguments to resolve all tests if a request to reload the entire tree is made, in which case the test framework is run immediately to load all tests. + +### Loading queue + +As mentioned, the `TestLoader` makes use of a queue for loading tests. The main reason for this is that the [FileSystemWatchers](https://code.visualstudio.com/api/references/vscode-api#FileSystemWatcher) only report changes one file at a time, and on large repositories this can easily result in hundreds of test processes being spawned in a short amount of time which will easily grind even powerful computers to a halt. + +To avoid this, tests are added to a queue to be loaded, which behaves as follows: + +- An async worker function checks the queue for [TestItems](https://code.visualstudio.com/api/references/vscode-api#TestItem) to run + - If any are found, it: + - Drains the queue, so that [TestItems](https://code.visualstudio.com/api/references/vscode-api#TestItem) enqueued while it is running don't get missed + - Sets the `busy` flag on all the [TestItems](https://code.visualstudio.com/api/references/vscode-api#TestItem) - this causes a spinner to be displayed in the UI for those items. + - Creates a [TestRunRequest](https://code.visualstudio.com/api/references/vscode-api#TestItem) containing the items to be loaded, using the `ResolveTests` profile and runs it with the profile's `runHandler` (see below) to load all the tests that were in the queue. + - Once this is completed, it unsets the `busy` flag on the [TestItems](https://code.visualstudio.com/api/references/vscode-api#TestItem) and checks the queue for more items. + - If the queue is empty, it creates a promise and waits for it to be resolved. Once resolved, it checks the queue again. +- When a test is added to the queue, if the async worker is waiting for items, the resolve function for the promise it is waiting on is resolved to notify it that items have been added. + +This ensures that only one test process at a time is running to load tests. + +## Running tests + +([VS Code API docs](https://code.visualstudio.com/api/extension-guides/testing#running-tests)) + +When the extension is initialized (see [Configuration and setup](#configuration-and-setup)), three [TestRunProfiles](https://code.visualstudio.com/api/references/vscode-api#TestRunProfile) are registered with the controller: + +- The `Run` profile, used for running tests (default profile for the `Run` [profile kind](https://code.visualstudio.com/api/references/vscode-api#TestRunProfileKind)) +- The `Debug` profile, used for debugging tests (default profile for the `Debug` [profile kind](https://code.visualstudio.com/api/references/vscode-api#TestRunProfileKind)) +- The `ResolveTests` profile, used for loading tests (using the `Run` [profile kind](https://code.visualstudio.com/api/references/vscode-api#TestRunProfileKind)) + - This profile can only be used internally by the extension, and is used by the `TestLoader` for loading tests + +Note: There is a third possible profile kind, `Profile`, intended to be used for profiling tests that is not currently used by this extension. + +When a user runs/debugs one or more tests from the UI, the `runHandler` function associated with the default profile for that [profile kind](https://code.visualstudio.com/api/references/vscode-api#TestRunProfileKind) is called. For all three profiles, this is `TestRunner.runHandler`. + +The `runHandler` does the following: + +- Creates a [TestRun](https://code.visualstudio.com/api/references/vscode-api#TestRun) from the [TestRunRequest](https://code.visualstudio.com/api/references/vscode-api#TestRunRequest) passed in as a parameter +- If the profile in the request is a `Debug` profile, it starts a debugging session and continues +- Marks all the [TestItems](https://code.visualstudio.com/api/references/vscode-api#TestItem) in the request as enqueued. +- Builds the command to run the requested tests (obtained from the `RspecConfig`/`MinitestConfig` classes, as appropriate) + - If the profile in the request is the `ResolveTests` profile, it builds a dry-run (RSpec)/list tests (Minitest) command +- Creates a `FrameworkProcess` instance, passing in the command to be run + - `FrameworkProcess` is a wrapper around the `child_process` in which the test framework runs. It parses the output, and emits status events based on it, and handles the lifetime of the child process, terminating it early if needed. +- Registers a `TestStatusListener` with the `FrameworkProcess` to call the [TestRun](https://code.visualstudio.com/api/references/vscode-api#TestRun) with information about the test results as they are received. +- Tells the `FrameworkProcess` instance to spawn the child process and waits for it to finish +- Calls `end` on the [TestRun](https://code.visualstudio.com/api/references/vscode-api#TestRun) to let VSCode know the test run is finished. diff --git a/package-lock.json b/package-lock.json index e016ed1..e2e932e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,35 +1,56 @@ { "name": "vscode-ruby-test-adapter", - "version": "0.9.2", + "version": "0.10.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "vscode-ruby-test-adapter", - "version": "0.9.2", + "version": "0.10.0", "license": "MIT", "dependencies": { + "@vscode-logging/logger": "^1.2.3", "split2": "^4.1.0", - "tslib": "^2.4.0", - "vscode-test-adapter-api": "^1.9.0", - "vscode-test-adapter-util": "^0.7.1" + "ts-mutex": "^1.0.0", + "tslib": "^2.4.0" }, "devDependencies": { + "@types/chai": "^4.3.0", "@types/glob": "^7.2.0", "@types/mocha": "^9.1.0", "@types/split2": "^3.2.1", "@types/vscode": "^1.69.0", + "@typestrong/ts-mockito": "^2.6.4", + "@vscode/test-electron": "^2.1.2", + "@vscode/vsce": "^2.16.0", + "chai": "^4.3.6", "glob": "^8.0.3", "mocha": "^9.2.2", "rimraf": "^3.0.0", - "typescript": "^4.7.4", - "vsce": "^2.10.0", - "vscode-test": "^1.6.1" + "typescript": "^4.7.4" }, "engines": { "vscode": "^1.69.0" } }, + "node_modules/@colors/colors": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.5.0.tgz", + "integrity": "sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==", + "engines": { + "node": ">=0.1.90" + } + }, + "node_modules/@dabh/diagnostics": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@dabh/diagnostics/-/diagnostics-2.0.3.tgz", + "integrity": "sha512-hrlQOIi7hAfzsMqlGSFyVucrx38O+j6wiGOf//H2ecvIEqYN4ADBSS2iLMh5UFyDunCNniUIPk/q3riFv45xRA==", + "dependencies": { + "colorspace": "1.1.x", + "enabled": "2.0.x", + "kuler": "^2.0.0" + } + }, "node_modules/@tootallnate/once": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-1.1.2.tgz", @@ -39,6 +60,12 @@ "node": ">= 6" } }, + "node_modules/@types/chai": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-4.3.3.tgz", + "integrity": "sha512-hC7OMnszpxhZPduX+m+nrx+uFoLkWOMiR4oa/AZF3MuSETYTZmFfJAHqZEM8MVlvfG7BEUcgvtwoCTxBp6hm3g==", + "dev": true + }, "node_modules/@types/glob": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/@types/glob/-/glob-7.2.0.tgz", @@ -62,9 +89,9 @@ "dev": true }, "node_modules/@types/node": { - "version": "16.11.6", - "resolved": "https://registry.npmjs.org/@types/node/-/node-16.11.6.tgz", - "integrity": "sha512-ua7PgUoeQFjmWPcoo9khiPum3Pd60k4/2ZGXt18sm2Slk0W0xZTqt5Y0Ny1NyBiN1EVQ/+FaF9NcY4Qe6rwk5w==", + "version": "18.6.5", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.6.5.tgz", + "integrity": "sha512-Xjt5ZGUa5WusGZJ4WJPbOT8QOqp6nDynVFRKcUt32bOgvXEoc6o085WNkYTMO7ifAj2isEfQQ2cseE+wT6jsRw==", "dev": true }, "node_modules/@types/split2": { @@ -77,17 +104,212 @@ } }, "node_modules/@types/vscode": { - "version": "1.69.1", - "resolved": "https://registry.npmjs.org/@types/vscode/-/vscode-1.69.1.tgz", - "integrity": "sha512-YZ77g3u9S9Xw3dwAgRgNAwnKNS3nPlhSu3XKOIYQzCcItUrZovfJUlf/29wjON2VZvHGuYQnhKuJUP15ccpVIQ==", + "version": "1.70.0", + "resolved": "https://registry.npmjs.org/@types/vscode/-/vscode-1.70.0.tgz", + "integrity": "sha512-3/9Fz0F2eBgwciazc94Ien+9u1elnjFg9YAhvAb3qDy/WeFWD9VrOPU7CIytryOVUdbxus8uzL4VZYONA0gDtA==", "dev": true }, + "node_modules/@typestrong/ts-mockito": { + "version": "2.6.4", + "resolved": "https://registry.npmjs.org/@typestrong/ts-mockito/-/ts-mockito-2.6.4.tgz", + "integrity": "sha512-IMUau44ixvAbO7ylg/NJAwtGoAIcC5SwAm1b+QcZWMMTqw0n2NZn4nUkMWZfUY478whZ50vvMiu7rJJfCR9C9A==", + "dev": true, + "dependencies": { + "lodash": "^4.17.5" + } + }, "node_modules/@ungap/promise-all-settled": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/@ungap/promise-all-settled/-/promise-all-settled-1.1.2.tgz", "integrity": "sha512-sL/cEvJWAnClXw0wHk85/2L0G6Sj8UB0Ctc1TEMbKSsmpRosqhwj9gWgFRZSrBr2f9tiXISwNhCPmlfqUqyb9Q==", "dev": true }, + "node_modules/@vscode-logging/logger": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@vscode-logging/logger/-/logger-1.2.3.tgz", + "integrity": "sha512-kQgRjbZmBBUktklaEat8YtTBuddCNRJezjJTk0Y+4VIs49KWjYkfF02GoyiCYASvCSMSqw9bBTQN3ijCO2aq+A==", + "dependencies": { + "@vscode-logging/types": "^0.1.4", + "fast-safe-stringify": "2.0.7", + "fs-extra": "9.1.0", + "lodash": "^4.17.21", + "stacktrace-js": "2.0.2", + "streamroller": "2.2.3", + "triple-beam": "1.3.0", + "winston": "3.3.3", + "winston-transport": "4.3.0" + } + }, + "node_modules/@vscode-logging/types": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/@vscode-logging/types/-/types-0.1.4.tgz", + "integrity": "sha512-uxuHQfpX9RbkgSj5unJFmciXRczyFSaAI2aA829MYYkE8jxlhZLRLoiJLymTNiojNVdV7fFE3CILF5Q6M+EBsA==" + }, + "node_modules/@vscode/test-electron": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@vscode/test-electron/-/test-electron-2.1.5.tgz", + "integrity": "sha512-O/ioqFpV+RvKbRykX2ItYPnbcZ4Hk5V0rY4uhQjQTLhGL9WZUvS7exzuYQCCI+ilSqJpctvxq2llTfGXf9UnnA==", + "dev": true, + "dependencies": { + "http-proxy-agent": "^4.0.1", + "https-proxy-agent": "^5.0.0", + "rimraf": "^3.0.2", + "unzipper": "^0.10.11" + }, + "engines": { + "node": ">=8.9.3" + } + }, + "node_modules/@vscode/vsce": { + "version": "2.19.0", + "resolved": "https://registry.npmjs.org/@vscode/vsce/-/vsce-2.19.0.tgz", + "integrity": "sha512-dAlILxC5ggOutcvJY24jxz913wimGiUrHaPkk16Gm9/PGFbz1YezWtrXsTKUtJws4fIlpX2UIlVlVESWq8lkfQ==", + "dev": true, + "dependencies": { + "azure-devops-node-api": "^11.0.1", + "chalk": "^2.4.2", + "cheerio": "^1.0.0-rc.9", + "commander": "^6.1.0", + "glob": "^7.0.6", + "hosted-git-info": "^4.0.2", + "jsonc-parser": "^3.2.0", + "leven": "^3.1.0", + "markdown-it": "^12.3.2", + "mime": "^1.3.4", + "minimatch": "^3.0.3", + "parse-semver": "^1.1.1", + "read": "^1.0.7", + "semver": "^5.1.0", + "tmp": "^0.2.1", + "typed-rest-client": "^1.8.4", + "url-join": "^4.0.1", + "xml2js": "^0.5.0", + "yauzl": "^2.3.1", + "yazl": "^2.2.2" + }, + "bin": { + "vsce": "vsce" + }, + "engines": { + "node": ">= 14" + }, + "optionalDependencies": { + "keytar": "^7.7.0" + } + }, + "node_modules/@vscode/vsce/node_modules/ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "dependencies": { + "color-convert": "^1.9.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@vscode/vsce/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/@vscode/vsce/node_modules/chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "dependencies": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@vscode/vsce/node_modules/color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, + "dependencies": { + "color-name": "1.1.3" + } + }, + "node_modules/@vscode/vsce/node_modules/color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", + "dev": true + }, + "node_modules/@vscode/vsce/node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "dev": true, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/@vscode/vsce/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "dev": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@vscode/vsce/node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/@vscode/vsce/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/@vscode/vsce/node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/agent-base": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", @@ -152,10 +374,32 @@ "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", "dev": true }, + "node_modules/assertion-error": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.1.0.tgz", + "integrity": "sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==", + "dev": true, + "engines": { + "node": "*" + } + }, + "node_modules/async": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.4.tgz", + "integrity": "sha512-iAB+JbDEGXhyIUavoDl9WP/Jj106Kz9DEn1DPgYw5ruDn0e3Wgi3sKFm55sASdGBNOQB8F59d9qQ7deqrHA8wQ==" + }, + "node_modules/at-least-node": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/at-least-node/-/at-least-node-1.0.0.tgz", + "integrity": "sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==", + "engines": { + "node": ">= 4.0.0" + } + }, "node_modules/azure-devops-node-api": { - "version": "11.0.1", - "resolved": "https://registry.npmjs.org/azure-devops-node-api/-/azure-devops-node-api-11.0.1.tgz", - "integrity": "sha512-YMdjAw9l5p/6leiyIloxj3k7VIvYThKjvqgiQn88r3nhT93ENwsoDS3A83CyJ4uTWzCZ5f5jCi6c27rTU5Pz+A==", + "version": "11.2.0", + "resolved": "https://registry.npmjs.org/azure-devops-node-api/-/azure-devops-node-api-11.2.0.tgz", + "integrity": "sha512-XdiGPhrpaT5J8wdERRKs5g8E0Zy1pvOYTli7z9E8nmOn3YGp4FhtjhrOyFmX/8veWCwdI69mCHKJw6l+4J/bHA==", "dev": true, "dependencies": { "tunnel": "0.0.6", @@ -186,12 +430,13 @@ "type": "consulting", "url": "https://feross.org/support" } - ] + ], + "optional": true }, "node_modules/big-integer": { - "version": "1.6.50", - "resolved": "https://registry.npmjs.org/big-integer/-/big-integer-1.6.50.tgz", - "integrity": "sha512-+O2uoQWFRo8ysZNo/rjtri2jIwjr3XfeAgRjAUADRqGG+ZITvyn8J1kvXLTaKVr3hhGXk+f23tKfdzmklVM9vQ==", + "version": "1.6.51", + "resolved": "https://registry.npmjs.org/big-integer/-/big-integer-1.6.51.tgz", + "integrity": "sha512-GPEid2Y9QU1Exl1rpO9B2IPJGHPSupF5GnVIP0blYvNOMer2bTvSWs1jGOUg04hTmu67nmLsQ9TBo1puaotBHg==", "dev": true, "engines": { "node": ">=0.6" @@ -200,7 +445,7 @@ "node_modules/binary": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/binary/-/binary-0.3.0.tgz", - "integrity": "sha1-n2BVO8XOjDOG87VTz/R0Yq3sqnk=", + "integrity": "sha512-D4H1y5KYwpJgK8wk1Cue5LLPgmwHKYSChkbspQg5JtVuR5ulGckxfR62H3AE9UDkdMC8yyXlqYihuz3Aqg2XZg==", "dev": true, "dependencies": { "buffers": "~0.1.1", @@ -224,6 +469,7 @@ "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", "dev": true, + "optional": true, "dependencies": { "buffer": "^5.5.0", "inherits": "^2.0.4", @@ -235,6 +481,7 @@ "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", "dev": true, + "optional": true, "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", @@ -247,23 +494,22 @@ "node_modules/bluebird": { "version": "3.4.7", "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.4.7.tgz", - "integrity": "sha1-9y12C+Cbf3bQjtj66Ysomo0F+rM=", + "integrity": "sha512-iD3898SR7sWVRHbiQv+sHUtHnMvC1o3nW5rAcqnq3uOn07DSAppZYUkIGslDz6gXC7HfunPe7YVBgoEJASPcHA==", "dev": true }, "node_modules/boolbase": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", - "integrity": "sha1-aN/1++YMUes3cl6p4+0xDcwed24=", + "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==", "dev": true }, "node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", "dev": true, "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" + "balanced-match": "^1.0.0" } }, "node_modules/braces": { @@ -303,6 +549,7 @@ "url": "https://feross.org/support" } ], + "optional": true, "dependencies": { "base64-js": "^1.3.1", "ieee754": "^1.1.13" @@ -311,7 +558,7 @@ "node_modules/buffer-crc32": { "version": "0.2.13", "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", - "integrity": "sha1-DTM+PwDqxQqhRUq9MO+MKl2ackI=", + "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==", "dev": true, "engines": { "node": "*" @@ -329,7 +576,7 @@ "node_modules/buffers": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/buffers/-/buffers-0.1.1.tgz", - "integrity": "sha1-skV5w77U1tOWru5tmorn9Ugqt7s=", + "integrity": "sha512-9q/rDEGSb/Qsvv2qvzIzdluL5k7AaJOTrw23z9reQthrbF7is4CtlT0DXyO1oei2DCp4uojjzQ7igaSHp1kAEQ==", "dev": true, "engines": { "node": ">=0.2.0" @@ -349,9 +596,9 @@ } }, "node_modules/camelcase": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.2.0.tgz", - "integrity": "sha512-c7wVvbw3f37nuobQNtgsgG9POC9qMbNuMQmTCqZv23b6MIz0fcYpBiOlv9gEN/hdLdnZTDQhg6e9Dq5M1vKvfg==", + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", + "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", "dev": true, "engines": { "node": ">=10" @@ -360,10 +607,28 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/chai": { + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/chai/-/chai-4.3.6.tgz", + "integrity": "sha512-bbcp3YfHCUzMOvKqsztczerVgBKSsEijCySNlHHbX3VG1nskvqjz5Rfso1gGwD6w6oOV3eI60pKuMOV5MV7p3Q==", + "dev": true, + "dependencies": { + "assertion-error": "^1.1.0", + "check-error": "^1.0.2", + "deep-eql": "^3.0.1", + "get-func-name": "^2.0.0", + "loupe": "^2.3.1", + "pathval": "^1.1.1", + "type-detect": "^4.0.5" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/chainsaw": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/chainsaw/-/chainsaw-0.1.0.tgz", - "integrity": "sha1-XqtQsor+WAdNDVgpE4iCi15fvJg=", + "integrity": "sha512-75kWfWt6MEKNC8xYXIdRpDehRYY/tNSgwKaJq+dbbDcxORuVrrQ+SEHoWsniVn9XPYfP4gmdWIeDk/4YNp1rNQ==", "dev": true, "dependencies": { "traverse": ">=0.3.0 <0.4" @@ -400,19 +665,28 @@ "node": ">=8" } }, + "node_modules/check-error": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.2.tgz", + "integrity": "sha512-BrgHpW9NURQgzoNyjfq0Wu6VFO6D7IZEmJNdtgNqpzGG8RuNFHt2jQxWlAs4HMe119chBnv+34syEZtc6IhLtA==", + "dev": true, + "engines": { + "node": "*" + } + }, "node_modules/cheerio": { - "version": "1.0.0-rc.10", - "resolved": "https://registry.npmjs.org/cheerio/-/cheerio-1.0.0-rc.10.tgz", - "integrity": "sha512-g0J0q/O6mW8z5zxQ3A8E8J1hUgp4SMOvEoW/x84OwyHKe/Zccz83PVT4y5Crcr530FV6NgmKI1qvGTKVl9XXVw==", + "version": "1.0.0-rc.12", + "resolved": "https://registry.npmjs.org/cheerio/-/cheerio-1.0.0-rc.12.tgz", + "integrity": "sha512-VqR8m68vM46BNnuZ5NtnGBKIE/DfN0cRIzg9n40EIq9NOv90ayxLBXA8fXC5gquFRGJSTRqBq25Jt2ECLR431Q==", "dev": true, "dependencies": { - "cheerio-select": "^1.5.0", - "dom-serializer": "^1.3.2", - "domhandler": "^4.2.0", - "htmlparser2": "^6.1.0", - "parse5": "^6.0.1", - "parse5-htmlparser2-tree-adapter": "^6.0.1", - "tslib": "^2.2.0" + "cheerio-select": "^2.1.0", + "dom-serializer": "^2.0.0", + "domhandler": "^5.0.3", + "domutils": "^3.0.1", + "htmlparser2": "^8.0.1", + "parse5": "^7.0.0", + "parse5-htmlparser2-tree-adapter": "^7.0.0" }, "engines": { "node": ">= 6" @@ -422,16 +696,17 @@ } }, "node_modules/cheerio-select": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/cheerio-select/-/cheerio-select-1.5.0.tgz", - "integrity": "sha512-qocaHPv5ypefh6YNxvnbABM07KMxExbtbfuJoIie3iZXX1ERwYmJcIiRrr9H05ucQP1k28dav8rpdDgjQd8drg==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/cheerio-select/-/cheerio-select-2.1.0.tgz", + "integrity": "sha512-9v9kG0LvzrlcungtnJtpGNxY+fzECQKhK4EGJX2vByejiMX84MFNQw4UxPJl3bFbTMw+Dfs37XaIkCwTZfLh4g==", "dev": true, "dependencies": { - "css-select": "^4.1.3", - "css-what": "^5.0.1", - "domelementtype": "^2.2.0", - "domhandler": "^4.2.0", - "domutils": "^2.7.0" + "boolbase": "^1.0.0", + "css-select": "^5.1.0", + "css-what": "^6.1.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3", + "domutils": "^3.0.1" }, "funding": { "url": "https://github.com/sponsors/fb55" @@ -468,7 +743,8 @@ "version": "1.1.4", "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", - "dev": true + "dev": true, + "optional": true }, "node_modules/cliui": { "version": "7.0.4", @@ -481,6 +757,15 @@ "wrap-ansi": "^7.0.0" } }, + "node_modules/color": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/color/-/color-3.2.1.tgz", + "integrity": "sha512-aBl7dZI9ENN6fUGC7mWpMTPNHmWUSNan9tuWN6ahh5ZLNk9baLJOnSMlrQkHcrfFgz2/RigjUVAjdx36VcemKA==", + "dependencies": { + "color-convert": "^1.9.3", + "color-string": "^1.6.0" + } + }, "node_modules/color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", @@ -496,8 +781,38 @@ "node_modules/color-name": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, + "node_modules/color-string": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/color-string/-/color-string-1.9.1.tgz", + "integrity": "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==", + "dependencies": { + "color-name": "^1.0.0", + "simple-swizzle": "^0.2.2" + } + }, + "node_modules/color/node_modules/color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dependencies": { + "color-name": "1.1.3" + } + }, + "node_modules/color/node_modules/color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==" + }, + "node_modules/colorspace": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/colorspace/-/colorspace-1.1.4.tgz", + "integrity": "sha512-BgvKJiuVu1igBUF2kEjRCZXol6wiiGbY5ipL/oVPwm0BL9sIpMIzM8IK7vwuxIIzOXMV3Ey5w+vxhm0rR/TN8w==", + "dependencies": { + "color": "^3.1.3", + "text-hex": "1.0.x" + } }, "node_modules/commander": { "version": "6.2.1", @@ -511,35 +826,34 @@ "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", "dev": true }, "node_modules/core-util-is": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", - "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", - "dev": true + "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==" }, "node_modules/css-select": { - "version": "4.1.3", - "resolved": "https://registry.npmjs.org/css-select/-/css-select-4.1.3.tgz", - "integrity": "sha512-gT3wBNd9Nj49rAbmtFHj1cljIAOLYSX1nZ8CB7TBO3INYckygm5B7LISU/szY//YmdiSLbJvDLOx9VnMVpMBxA==", + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/css-select/-/css-select-5.1.0.tgz", + "integrity": "sha512-nwoRF1rvRRnnCqqY7updORDsuqKzqYJ28+oSMaJMMgOauh3fvwHqMS7EZpIPqK8GL+g9mKxF1vP/ZjSeNjEVHg==", "dev": true, "dependencies": { "boolbase": "^1.0.0", - "css-what": "^5.0.0", - "domhandler": "^4.2.0", - "domutils": "^2.6.0", - "nth-check": "^2.0.0" + "css-what": "^6.1.0", + "domhandler": "^5.0.2", + "domutils": "^3.0.1", + "nth-check": "^2.0.1" }, "funding": { "url": "https://github.com/sponsors/fb55" } }, "node_modules/css-what": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/css-what/-/css-what-5.1.0.tgz", - "integrity": "sha512-arSMRWIIFY0hV8pIxZMEfmMI47Wj3R/aWpZDDxWYCPEiOMv6tfOrnpDtgxBYPEQD4V0Y/958+1TdC3iWTFcUPw==", + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.1.0.tgz", + "integrity": "sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw==", "dev": true, "engines": { "node": ">= 6" @@ -548,11 +862,19 @@ "url": "https://github.com/sponsors/fb55" } }, + "node_modules/date-format": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/date-format/-/date-format-2.1.0.tgz", + "integrity": "sha512-bYQuGLeFxhkxNOF3rcMtiZxvCBAquGzZm6oWA1oZ0g2THUzivaRhv8uOhdr19LmoobSOLoIAxeUK2RdbM8IFTA==", + "deprecated": "2.x is no longer supported. Please upgrade to 4.x or higher.", + "engines": { + "node": ">=4.0" + } + }, "node_modules/debug": { - "version": "4.3.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.3.tgz", - "integrity": "sha512-/zxw5+vh1Tfv+4Qn7a5nsbcJKPaSvCDhojn6FEl9vupwK2VCSDtEiEtqr8DFtzYFOdz63LBkxec7DYuc2jon6Q==", - "dev": true, + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", "dependencies": { "ms": "2.1.2" }, @@ -568,8 +890,7 @@ "node_modules/debug/node_modules/ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" }, "node_modules/decamelize": { "version": "4.0.0", @@ -588,6 +909,7 @@ "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", "dev": true, + "optional": true, "dependencies": { "mimic-response": "^3.1.0" }, @@ -598,11 +920,24 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/deep-eql": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-3.0.1.tgz", + "integrity": "sha512-+QeIQyN5ZuO+3Uk5DYh6/1eKO0m0YmJFGNmFHGACpf1ClL1nmlV/p4gNgbl2pJGxgXb4faqo6UE+M5ACEMyVcw==", + "dev": true, + "dependencies": { + "type-detect": "^4.0.0" + }, + "engines": { + "node": ">=0.12" + } + }, "node_modules/deep-extend": { "version": "0.6.0", "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", "dev": true, + "optional": true, "engines": { "node": ">=4.0.0" } @@ -612,6 +947,7 @@ "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.1.tgz", "integrity": "sha512-463v3ZeIrcWtdgIg6vI6XUncguvr2TnGl4SzDXinkt9mSLpBJKXT3mW6xT3VQdDN11+WVs29pgvivTc4Lp8v+w==", "dev": true, + "optional": true, "engines": { "node": ">=8" } @@ -626,23 +962,23 @@ } }, "node_modules/dom-serializer": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-1.3.2.tgz", - "integrity": "sha512-5c54Bk5Dw4qAxNOI1pFEizPSjVsx5+bpJKmL2kPn8JhBUq2q09tTCa3mjijun2NfK78NMouDYNMBkOrPZiS+ig==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", + "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", "dev": true, "dependencies": { - "domelementtype": "^2.0.1", - "domhandler": "^4.2.0", - "entities": "^2.0.0" + "domelementtype": "^2.3.0", + "domhandler": "^5.0.2", + "entities": "^4.2.0" }, "funding": { "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" } }, "node_modules/domelementtype": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.2.0.tgz", - "integrity": "sha512-DtBMo82pv1dFtUmHyr48beiuq792Sxohr+8Hm9zoxklYPfa6n0Z3Byjj2IV7bmr2IyqClnqEQhfgHJJ5QF0R5A==", + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", + "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", "dev": true, "funding": [ { @@ -652,12 +988,12 @@ ] }, "node_modules/domhandler": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-4.2.2.tgz", - "integrity": "sha512-PzE9aBMsdZO8TK4BnuJwH0QT41wgMbRzuZrHUcpYncEjmQazq8QEaBWgLG7ZyC/DAZKEgglpIA6j4Qn/HmxS3w==", + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", + "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", "dev": true, "dependencies": { - "domelementtype": "^2.2.0" + "domelementtype": "^2.3.0" }, "engines": { "node": ">= 4" @@ -667,14 +1003,14 @@ } }, "node_modules/domutils": { - "version": "2.8.0", - "resolved": "https://registry.npmjs.org/domutils/-/domutils-2.8.0.tgz", - "integrity": "sha512-w96Cjofp72M5IIhpjgobBimYEfoPjx1Vx0BSX9P30WBdZW2WIKU0T1Bd0kz2eNZ9ikjKgHbEyKx8BB6H1L3h3A==", + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.0.1.tgz", + "integrity": "sha512-z08c1l761iKhDFtfXO04C7kTdPBLi41zwOZl00WS8b5eiaebNpY00HKbztwBq+e3vyqWNwWF3mP9YLUeqIrF+Q==", "dev": true, "dependencies": { - "dom-serializer": "^1.0.1", - "domelementtype": "^2.2.0", - "domhandler": "^4.2.0" + "dom-serializer": "^2.0.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.1" }, "funding": { "url": "https://github.com/fb55/domutils?sponsor=1" @@ -683,7 +1019,7 @@ "node_modules/duplexer2": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/duplexer2/-/duplexer2-0.1.4.tgz", - "integrity": "sha1-ixLauHjA1p4+eJEFFmKjL8a93ME=", + "integrity": "sha512-asLFVfWWtJ90ZyOUHMqk7/S2w2guQKxUI2itj3d92ADHhxUSbCMGi1f1cBcJ7xM1To+pE/Khbwo1yuNbMEPKeA==", "dev": true, "dependencies": { "readable-stream": "^2.0.2" @@ -695,24 +1031,41 @@ "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", "dev": true }, + "node_modules/enabled": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/enabled/-/enabled-2.0.0.tgz", + "integrity": "sha512-AKrN98kuwOzMIdAizXGI86UFBoo26CL21UM763y1h/GMSJ4/OHU9k2YlsmBpyScFo/wbLzWQJBMCW4+IO3/+OQ==" + }, "node_modules/end-of-stream": { "version": "1.4.4", "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", "dev": true, + "optional": true, "dependencies": { "once": "^1.4.0" } }, "node_modules/entities": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/entities/-/entities-2.2.0.tgz", - "integrity": "sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==", + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.3.1.tgz", + "integrity": "sha512-o4q/dYJlmyjP2zfnaWDUC6A3BQFmVTX+tZPezK7k0GLSU9QYCauscf5Y+qcEPzKL+EixVouYDgLQK5H9GrLpkg==", "dev": true, + "engines": { + "node": ">=0.12" + }, "funding": { "url": "https://github.com/fb55/entities?sponsor=1" } }, + "node_modules/error-stack-parser": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/error-stack-parser/-/error-stack-parser-2.1.4.tgz", + "integrity": "sha512-Sk5V6wVazPhq5MhpO+AUxJn5x7XSXGl1R93Vn7i+zS15KDVxQijejNCrz8340/2bgLBjR9GtEG8ZVKONDjcqGQ==", + "dependencies": { + "stackframe": "^1.3.4" + } + }, "node_modules/escalade": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", @@ -739,19 +1092,30 @@ "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", "integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==", "dev": true, + "optional": true, "engines": { "node": ">=6" } }, + "node_modules/fast-safe-stringify": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-2.0.7.tgz", + "integrity": "sha512-Utm6CdzT+6xsDk2m8S6uL8VHxNwI6Jub+e9NYTcAms28T84pTa25GJQV9j0CY0N1rM8hK4x6grpF2BQf+2qwVA==" + }, "node_modules/fd-slicer": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz", - "integrity": "sha1-JcfInLH5B3+IkbvmHY85Dq4lbx4=", + "integrity": "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==", "dev": true, "dependencies": { "pend": "~1.2.0" } }, + "node_modules/fecha": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/fecha/-/fecha-4.2.3.tgz", + "integrity": "sha512-OP2IUU6HeYKJi3i0z4A19kHMQoLVs4Hc+DPqqxI2h/DPZHTm/vjsfC6P0b4jCMy14XizLBqvndQ+UilD7707Jw==" + }, "node_modules/fill-range": { "version": "7.0.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", @@ -789,16 +1153,36 @@ "flat": "cli.js" } }, + "node_modules/fn.name": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fn.name/-/fn.name-1.1.0.tgz", + "integrity": "sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw==" + }, "node_modules/fs-constants": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", - "dev": true + "dev": true, + "optional": true + }, + "node_modules/fs-extra": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", + "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", + "dependencies": { + "at-least-node": "^1.0.0", + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=10" + } }, "node_modules/fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", "dev": true }, "node_modules/fsevents": { @@ -830,6 +1214,16 @@ "node": ">=0.6" } }, + "node_modules/fstream/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, "node_modules/fstream/node_modules/glob": { "version": "7.2.3", "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", @@ -889,15 +1283,24 @@ "node": "6.* || 8.* || >= 10.*" } }, + "node_modules/get-func-name": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.0.tgz", + "integrity": "sha512-Hm0ixYtaSZ/V7C8FJrtZIuBBI+iSgL+1Aq82zSu8VQNB4S3Gk8e7Qs3VwBDJAhmRZcFqkl3tQu36g/Foh5I5ig==", + "dev": true, + "engines": { + "node": "*" + } + }, "node_modules/get-intrinsic": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.1.1.tgz", - "integrity": "sha512-kWZrnVM42QCiEA2Ig1bG8zjoIMOgxWwYCEeNdwY6Tv/cOSeGpcoX4pXHfKUxNKVoArnrEr2e9srnAxxGIraS9Q==", + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.1.2.tgz", + "integrity": "sha512-Jfm3OyCxHh9DJyc28qGk+JmfkpO41A4XkneDSujN9MDXrm4oDKdHvndhZ2dN94+ERNfkYJWDclW6k2L/ZGHjXA==", "dev": true, "dependencies": { "function-bind": "^1.1.1", "has": "^1.0.3", - "has-symbols": "^1.0.1" + "has-symbols": "^1.0.3" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -907,7 +1310,8 @@ "version": "0.0.0", "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz", "integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==", - "dev": true + "dev": true, + "optional": true }, "node_modules/glob": { "version": "8.0.3", @@ -940,19 +1344,10 @@ "node": ">= 6" } }, - "node_modules/glob/node_modules/brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", - "dev": true, - "dependencies": { - "balanced-match": "^1.0.0" - } - }, "node_modules/glob/node_modules/minimatch": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.0.tgz", - "integrity": "sha512-9TPBGGak4nHfGZsPBohm9AWg6NoT7QTCehS3BIJABslyZbzxfV78QM2Y6+i741OPZIafFAaiiEMh5OyIrJPgtg==", + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.2.tgz", + "integrity": "sha512-bNH9mmM9qsJ2X4r2Nat1B//1dJVcn3+iBLa3IgqJ7EbGaDNepL9QSHOxN4ng33s52VMMhhIfgCYDk3C4ZmlDAg==", "dev": true, "dependencies": { "brace-expansion": "^2.0.1" @@ -962,10 +1357,9 @@ } }, "node_modules/graceful-fs": { - "version": "4.2.8", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.8.tgz", - "integrity": "sha512-qkIilPUYcNhJpd33n0GBXTB1MMPp14TxEsEs0pTrsSVucApsYzW5V+Q8Qxhik6KU3evy+qkAAowTByymK0avdg==", - "dev": true + "version": "4.2.10", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.10.tgz", + "integrity": "sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA==" }, "node_modules/growl": { "version": "1.10.5", @@ -998,9 +1392,9 @@ } }, "node_modules/has-symbols": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.2.tgz", - "integrity": "sha512-chXa79rL/UC2KlX17jo3vRGz0azaWEx5tGqZg5pO3NUyEJVB17dMruQlzCCOfUvElghKcm5194+BCRvi2Rv/Gw==", + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", + "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", "dev": true, "engines": { "node": ">= 0.4" @@ -1019,9 +1413,9 @@ } }, "node_modules/hosted-git-info": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-4.0.2.tgz", - "integrity": "sha512-c9OGXbZ3guC/xOlCg1Ci/VgWlwsqDv1yMQL1CWqXDL0hDjXuNcq0zuR4xqPSuasI3kqFDhqSyTjREz5gzq0fXg==", + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-4.1.0.tgz", + "integrity": "sha512-kyCuEOWjJqZuDbRHzL8V93NzQhwIB71oFWSyzVo+KPZI+pnQPPxucdkrOZvkLRnrf5URsQM+IJ09Dw29cRALIA==", "dev": true, "dependencies": { "lru-cache": "^6.0.0" @@ -1031,9 +1425,9 @@ } }, "node_modules/htmlparser2": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-6.1.0.tgz", - "integrity": "sha512-gyyPk6rgonLFEDGoeRgQNaEUvdJ4ktTmmUh/h2t7s+M8oPpIPxgNACWa+6ESR57kXstwqPiCut0V8NRpcwgU7A==", + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-8.0.1.tgz", + "integrity": "sha512-4lVbmc1diZC7GUJQtRQ5yBAeUCL1exyMwmForWkRLnwyzWBFxN633SALPMGYaWZvKe9j1pRZJpauvmxENSp/EA==", "dev": true, "funding": [ "https://github.com/fb55/htmlparser2?sponsor=1", @@ -1043,10 +1437,10 @@ } ], "dependencies": { - "domelementtype": "^2.0.1", - "domhandler": "^4.0.0", - "domutils": "^2.5.2", - "entities": "^2.0.0" + "domelementtype": "^2.3.0", + "domhandler": "^5.0.2", + "domutils": "^3.0.1", + "entities": "^4.3.0" } }, "node_modules/http-proxy-agent": { @@ -1064,9 +1458,9 @@ } }, "node_modules/https-proxy-agent": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.0.tgz", - "integrity": "sha512-EkYm5BcKUGiduxzSt3Eppko+PiNWNEpa4ySk9vTC6wDsQJW9rHSa+UhGNJoRYp7bz6Ht1eaRIa6QaJqO5rCFbA==", + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", "dev": true, "dependencies": { "agent-base": "6", @@ -1094,12 +1488,13 @@ "type": "consulting", "url": "https://feross.org/support" } - ] + ], + "optional": true }, "node_modules/inflight": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", - "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", "dev": true, "dependencies": { "once": "^1.3.0", @@ -1109,14 +1504,19 @@ "node_modules/inherits": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", - "dev": true + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" }, "node_modules/ini": { "version": "1.3.8", "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", - "dev": true + "dev": true, + "optional": true + }, + "node_modules/is-arrayish": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz", + "integrity": "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==" }, "node_modules/is-binary-path": { "version": "2.1.0", @@ -1178,6 +1578,17 @@ "node": ">=8" } }, + "node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/is-unicode-supported": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", @@ -1193,13 +1604,12 @@ "node_modules/isarray": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", - "dev": true + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==" }, "node_modules/isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", "dev": true }, "node_modules/js-yaml": { @@ -1214,17 +1624,40 @@ "js-yaml": "bin/js-yaml.js" } }, + "node_modules/jsonc-parser": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.2.0.tgz", + "integrity": "sha512-gfFQZrcTc8CnKXp6Y4/CBT3fTc0OVuDofpre4aEeEpSBPV5X5v4+Vmx+8snU7RLPrNHPKSgLxGo9YuQzz20o+w==", + "dev": true + }, + "node_modules/jsonfile": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", + "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, "node_modules/keytar": { "version": "7.9.0", "resolved": "https://registry.npmjs.org/keytar/-/keytar-7.9.0.tgz", "integrity": "sha512-VPD8mtVtm5JNtA2AErl6Chp06JBfy7diFQ7TQQhdpWOl6MrCRB+eRbvAZUsbGQS9kiMq0coJsy0W0vHpDCkWsQ==", "dev": true, "hasInstallScript": true, + "optional": true, "dependencies": { "node-addon-api": "^4.3.0", "prebuild-install": "^7.0.1" } }, + "node_modules/kuler": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/kuler/-/kuler-2.0.0.tgz", + "integrity": "sha512-Xq9nH7KlWZmXAtodXDDRE7vs6DU1gTU8zYDHDiWLSip45Egwq3plLHzPn27NgvzL2r1LMPC1vdqh98sQxtqj4A==" + }, "node_modules/leven": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", @@ -1246,7 +1679,7 @@ "node_modules/listenercount": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/listenercount/-/listenercount-1.0.1.tgz", - "integrity": "sha1-hMinKrWcRyUyFIDJdeZQg0LnCTc=", + "integrity": "sha512-3mk/Zag0+IJxeDrxSgaDPy4zZ3w05PRZeJNnlWhzFz5OkX49J4krc+A8X2d2M69vGMBEX0uyl8M+W+8gH+kBqQ==", "dev": true }, "node_modules/locate-path": { @@ -1264,6 +1697,11 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" + }, "node_modules/log-symbols": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", @@ -1280,6 +1718,27 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/logform": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/logform/-/logform-2.4.2.tgz", + "integrity": "sha512-W4c9himeAwXEdZ05dQNerhFz2XG80P9Oj0loPUMV23VC2it0orMHQhJm4hdnnor3rd1HsGf6a2lPwBM1zeXHGw==", + "dependencies": { + "@colors/colors": "1.5.0", + "fecha": "^4.2.0", + "ms": "^2.1.1", + "safe-stable-stringify": "^2.3.1", + "triple-beam": "^1.3.0" + } + }, + "node_modules/loupe": { + "version": "2.3.4", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-2.3.4.tgz", + "integrity": "sha512-OvKfgCC2Ndby6aSTREl5aCCPTNIzlDfQZvZxNUrBrihDhL3xcrYegTblhmEiCrg2kKQz4XsFIaemE5BF4ybSaQ==", + "dev": true, + "dependencies": { + "get-func-name": "^2.0.0" + } + }, "node_modules/lru-cache": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", @@ -1340,6 +1799,7 @@ "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", "dev": true, + "optional": true, "engines": { "node": ">=10" }, @@ -1353,7 +1813,7 @@ "integrity": "sha512-9Uq1ChtSZO+Mxa/CL1eGizn2vRn3MlLgzhT0Iz8zaY8NdvxvB0d5QdPFmCKf7JKA9Lerx5vRrnwO03jsSfGG9g==", "dev": true, "dependencies": { - "brace-expansion": "^1.1.7" + "brace-expansion": "^2.0.1" }, "engines": { "node": ">=10" @@ -1366,12 +1826,12 @@ "dev": true }, "node_modules/mkdirp": { - "version": "0.5.5", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.5.tgz", - "integrity": "sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==", + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", + "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", "dev": true, "dependencies": { - "minimist": "^1.2.5" + "minimist": "^1.2.6" }, "bin": { "mkdirp": "bin/cmd.js" @@ -1381,7 +1841,8 @@ "version": "0.5.3", "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==", - "dev": true + "dev": true, + "optional": true }, "node_modules/mocha": { "version": "9.2.2", @@ -1426,6 +1887,39 @@ "url": "https://opencollective.com/mochajs" } }, + "node_modules/mocha/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/mocha/node_modules/debug": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.3.tgz", + "integrity": "sha512-/zxw5+vh1Tfv+4Qn7a5nsbcJKPaSvCDhojn6FEl9vupwK2VCSDtEiEtqr8DFtzYFOdz63LBkxec7DYuc2jon6Q==", + "dev": true, + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/mocha/node_modules/debug/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, "node_modules/mocha/node_modules/glob": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.0.tgz", @@ -1461,8 +1955,7 @@ "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "dev": true + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" }, "node_modules/mute-stream": { "version": "0.0.8", @@ -1486,13 +1979,15 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-1.0.2.tgz", "integrity": "sha512-ONmRUqK7zj7DWX0D9ADe03wbwOBZxNAfF20PlGfCWQcD3+/MakShIHrMqx9YwPTfxDdF1zLeL+RGZiR9kGMLdg==", - "dev": true + "dev": true, + "optional": true }, "node_modules/node-abi": { "version": "3.24.0", "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.24.0.tgz", "integrity": "sha512-YPG3Co0luSu6GwOBsmIdGW6Wx0NyNDLg/hriIyDllVsNwnI6UeqaWShxC3lbH4LtEQUgoLP3XR1ndXiDAWvmRw==", "dev": true, + "optional": true, "dependencies": { "semver": "^7.3.5" }, @@ -1505,6 +2000,7 @@ "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.7.tgz", "integrity": "sha512-QlYTucUYOews+WeEujDoEGziz4K6c47V/Bd+LjSSYcA94p+DmINdf7ncaUinThfvZyu13lN9OY1XDxt8C0Tw0g==", "dev": true, + "optional": true, "dependencies": { "lru-cache": "^6.0.0" }, @@ -1519,7 +2015,8 @@ "version": "4.3.0", "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-4.3.0.tgz", "integrity": "sha512-73sE9+3UaLYYFmDsFZnqCInzPyh3MqIwZO9cw58yIqAZhONrrabrYyYe3TuIqtIiOuTXVhsGau8hcrhhwSsDIQ==", - "dev": true + "dev": true, + "optional": true }, "node_modules/normalize-path": { "version": "3.0.0", @@ -1531,9 +2028,9 @@ } }, "node_modules/nth-check": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.0.1.tgz", - "integrity": "sha512-it1vE95zF6dTT9lBsYbxvqh0Soy4SPowchj0UBGj/V6cTPnXXtQOPUbhZ6CmGzAD/rW22LQK6E96pcdJXk4A4w==", + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", + "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==", "dev": true, "dependencies": { "boolbase": "^1.0.0" @@ -1543,9 +2040,9 @@ } }, "node_modules/object-inspect": { - "version": "1.11.0", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.11.0.tgz", - "integrity": "sha512-jp7ikS6Sd3GxQfZJPyH3cjcbJF6GZPClgdV+EFygjFLQ5FmW/dRUnTd9PQ9k0JhoNDabWFbpF1yCdSWCC6gexg==", + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.2.tgz", + "integrity": "sha512-z+cPxW0QGUp0mcqcsgQyLVRDoXFQbXOwBaqyF7VIgI4TWNQsDHrBpUQslRmIfAoYWdYzs6UlKJtB2XJpTaNSpQ==", "dev": true, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -1554,12 +2051,20 @@ "node_modules/once": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", "dev": true, "dependencies": { "wrappy": "1" } }, + "node_modules/one-time": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/one-time/-/one-time-1.0.0.tgz", + "integrity": "sha512-5DXOiRKwuSEcQ/l0kGCF6Q3jcADFv5tSmRaJck/OqkVFcOzutB134KRSfF0xDrL39MNnqxbHBbUUcjZIhTgb2g==", + "dependencies": { + "fn.name": "1.x.x" + } + }, "node_modules/p-limit": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", @@ -1593,25 +2098,35 @@ "node_modules/parse-semver": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/parse-semver/-/parse-semver-1.1.1.tgz", - "integrity": "sha1-mkr9bfBj3Egm+T+6SpnPIj9mbLg=", + "integrity": "sha512-Eg1OuNntBMH0ojvEKSrvDSnwLmvVuUOSdylH/pSCPNMIspLlweJyIWXCE+k/5hm3cj/EBUYwmWkjhBALNP4LXQ==", "dev": true, "dependencies": { "semver": "^5.1.0" } }, "node_modules/parse5": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/parse5/-/parse5-6.0.1.tgz", - "integrity": "sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw==", - "dev": true + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.0.0.tgz", + "integrity": "sha512-y/t8IXSPWTuRZqXc0ajH/UwDj4mnqLEbSttNbThcFhGrZuOyoyvNBO85PBp2jQa55wY9d07PBNjsK8ZP3K5U6g==", + "dev": true, + "dependencies": { + "entities": "^4.3.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } }, "node_modules/parse5-htmlparser2-tree-adapter": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/parse5-htmlparser2-tree-adapter/-/parse5-htmlparser2-tree-adapter-6.0.1.tgz", - "integrity": "sha512-qPuWvbLgvDGilKc5BoicRovlT4MtYT6JfJyBOMDsKoiT+GiuP5qyrPCnR9HcPECIJJmZh5jRndyNThnhhb/vlA==", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/parse5-htmlparser2-tree-adapter/-/parse5-htmlparser2-tree-adapter-7.0.0.tgz", + "integrity": "sha512-B77tOZrqqfUfnVcOrUvfdLbz4pu4RopLD/4vmu3HUPswwTA8OH0EMW9BlWR2B0RCoiZRAHEUu7IxeP1Pd1UU+g==", "dev": true, "dependencies": { - "parse5": "^6.0.1" + "domhandler": "^5.0.2", + "parse5": "^7.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" } }, "node_modules/path-exists": { @@ -1626,16 +2141,25 @@ "node_modules/path-is-absolute": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", - "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", "dev": true, "engines": { "node": ">=0.10.0" } }, + "node_modules/pathval": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-1.1.1.tgz", + "integrity": "sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ==", + "dev": true, + "engines": { + "node": "*" + } + }, "node_modules/pend": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", - "integrity": "sha1-elfrVQpng/kRUzH89GY9XI4AelA=", + "integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==", "dev": true }, "node_modules/picomatch": { @@ -1655,6 +2179,7 @@ "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.1.tgz", "integrity": "sha512-jAXscXWMcCK8GgCoHOfIr0ODh5ai8mj63L2nWrjuAgXE6tDyYGnx4/8o/rCgU+B4JSyZBKbeZqzhtwtC3ovxjw==", "dev": true, + "optional": true, "dependencies": { "detect-libc": "^2.0.0", "expand-template": "^2.0.3", @@ -1679,14 +2204,14 @@ "node_modules/process-nextick-args": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", - "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", - "dev": true + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==" }, "node_modules/pump": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", "dev": true, + "optional": true, "dependencies": { "end-of-stream": "^1.1.0", "once": "^1.3.1" @@ -1721,6 +2246,7 @@ "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", "dev": true, + "optional": true, "dependencies": { "deep-extend": "^0.6.0", "ini": "~1.3.0", @@ -1736,6 +2262,7 @@ "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", "dev": true, + "optional": true, "engines": { "node": ">=0.10.0" } @@ -1743,7 +2270,7 @@ "node_modules/read": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/read/-/read-1.0.7.tgz", - "integrity": "sha1-s9oZvQUkMal2cdRKQmNK33ELQMQ=", + "integrity": "sha512-rSOKNYUmaxy0om1BNjMN4ezNT6VKK+2xF4GBhc81mkH7L60i6dp8qPYrkndNLT3QPphoII3maL9PVC9XmhHwVQ==", "dev": true, "dependencies": { "mute-stream": "~0.0.4" @@ -1756,7 +2283,6 @@ "version": "2.3.7", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", - "dev": true, "dependencies": { "core-util-is": "~1.0.0", "inherits": "~2.0.3", @@ -1770,8 +2296,7 @@ "node_modules/readable-stream/node_modules/safe-buffer": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", - "dev": true + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" }, "node_modules/readdirp": { "version": "3.6.0", @@ -1788,7 +2313,7 @@ "node_modules/require-directory": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", - "integrity": "sha1-jGStX9MNqxyXbiNE/+f3kqam30I=", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", "dev": true, "engines": { "node": ">=0.10.0" @@ -1809,6 +2334,16 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/rimraf/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, "node_modules/rimraf/node_modules/glob": { "version": "7.2.3", "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", @@ -1861,6 +2396,14 @@ } ] }, + "node_modules/safe-stable-stringify": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.3.1.tgz", + "integrity": "sha512-kYBSfT+troD9cDA85VDnHZ1rpHC50O0g1e6WlGHVCz/g+JS+9WKLj+XwFYyR8UbrZN8ll9HUpDAAddY58MGisg==", + "engines": { + "node": ">=10" + } + }, "node_modules/sax": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz", @@ -1888,7 +2431,7 @@ "node_modules/setimmediate": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", - "integrity": "sha1-KQy7Iy4waULX1+qbg3Mqt4VvgoU=", + "integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==", "dev": true }, "node_modules/side-channel": { @@ -1923,7 +2466,8 @@ "type": "consulting", "url": "https://feross.org/support" } - ] + ], + "optional": true }, "node_modules/simple-get": { "version": "4.0.1", @@ -1944,12 +2488,29 @@ "url": "https://feross.org/support" } ], + "optional": true, "dependencies": { "decompress-response": "^6.0.0", "once": "^1.3.1", "simple-concat": "^1.0.0" } }, + "node_modules/simple-swizzle": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz", + "integrity": "sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==", + "dependencies": { + "is-arrayish": "^0.3.1" + } + }, + "node_modules/source-map": { + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.6.tgz", + "integrity": "sha512-MjZkVp0NHr5+TPihLcadqnlVoGIoWo4IBHptutGh9wI3ttUYvCG26HkSuDi+K6lsZ25syXJXcctwgyVCt//xqA==", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/split2": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/split2/-/split2-4.1.0.tgz", @@ -1958,11 +2519,93 @@ "node": ">= 10.x" } }, + "node_modules/stack-generator": { + "version": "2.0.10", + "resolved": "https://registry.npmjs.org/stack-generator/-/stack-generator-2.0.10.tgz", + "integrity": "sha512-mwnua/hkqM6pF4k8SnmZ2zfETsRUpWXREfA/goT8SLCV4iOFa4bzOX2nDipWAZFPTjLvQB82f5yaodMVhK0yJQ==", + "dependencies": { + "stackframe": "^1.3.4" + } + }, + "node_modules/stack-trace": { + "version": "0.0.10", + "resolved": "https://registry.npmjs.org/stack-trace/-/stack-trace-0.0.10.tgz", + "integrity": "sha512-KGzahc7puUKkzyMt+IqAep+TVNbKP+k2Lmwhub39m1AsTSkaDutx56aDCo+HLDzf/D26BIHTJWNiTG1KAJiQCg==", + "engines": { + "node": "*" + } + }, + "node_modules/stackframe": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/stackframe/-/stackframe-1.3.4.tgz", + "integrity": "sha512-oeVtt7eWQS+Na6F//S4kJ2K2VbRlS9D43mAlMyVpVWovy9o+jfgH8O9agzANzaiLjclA0oYzUXEM4PurhSUChw==" + }, + "node_modules/stacktrace-gps": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/stacktrace-gps/-/stacktrace-gps-3.1.2.tgz", + "integrity": "sha512-GcUgbO4Jsqqg6RxfyTHFiPxdPqF+3LFmQhm7MgCuYQOYuWyqxo5pwRPz5d/u6/WYJdEnWfK4r+jGbyD8TSggXQ==", + "dependencies": { + "source-map": "0.5.6", + "stackframe": "^1.3.4" + } + }, + "node_modules/stacktrace-js": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/stacktrace-js/-/stacktrace-js-2.0.2.tgz", + "integrity": "sha512-Je5vBeY4S1r/RnLydLl0TBTi3F2qdfWmYsGvtfZgEI+SCprPppaIhQf5nGcal4gI4cGpCV/duLcAzT1np6sQqg==", + "dependencies": { + "error-stack-parser": "^2.0.6", + "stack-generator": "^2.0.5", + "stacktrace-gps": "^3.0.4" + } + }, + "node_modules/streamroller": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/streamroller/-/streamroller-2.2.3.tgz", + "integrity": "sha512-AegmvQsscTRhHVO46PhCDerjIpxi7E+d2GxgUDu+nzw/HuLnUdxHWr6WQ+mVn/4iJgMKKFFdiUwFcFRDvcjCtw==", + "deprecated": "2.x is no longer supported. Please upgrade to 3.x or higher.", + "dependencies": { + "date-format": "^2.1.0", + "debug": "^4.1.1", + "fs-extra": "^8.1.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/streamroller/node_modules/fs-extra": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz", + "integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^4.0.0", + "universalify": "^0.1.0" + }, + "engines": { + "node": ">=6 <7 || >=8" + } + }, + "node_modules/streamroller/node_modules/jsonfile": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", + "integrity": "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==", + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/streamroller/node_modules/universalify": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", + "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", + "engines": { + "node": ">= 4.0.0" + } + }, "node_modules/string_decoder": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "dev": true, "dependencies": { "safe-buffer": "~5.1.0" } @@ -1970,8 +2613,7 @@ "node_modules/string_decoder/node_modules/safe-buffer": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", - "dev": true + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" }, "node_modules/string-width": { "version": "4.2.3", @@ -2031,6 +2673,7 @@ "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.1.tgz", "integrity": "sha512-V0r2Y9scmbDRLCNex/+hYzvp/zyYjvFbHPNgVTKfQvVrb6guiE/fxP+XblDNR011utopbkex2nM4dHNV6GDsng==", "dev": true, + "optional": true, "dependencies": { "chownr": "^1.1.1", "mkdirp-classic": "^0.5.2", @@ -2043,6 +2686,7 @@ "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", "dev": true, + "optional": true, "dependencies": { "bl": "^4.0.3", "end-of-stream": "^1.4.1", @@ -2059,6 +2703,7 @@ "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", "dev": true, + "optional": true, "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", @@ -2068,6 +2713,11 @@ "node": ">= 6" } }, + "node_modules/text-hex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/text-hex/-/text-hex-1.0.0.tgz", + "integrity": "sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg==" + }, "node_modules/tmp": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.1.tgz", @@ -2095,12 +2745,25 @@ "node_modules/traverse": { "version": "0.3.9", "resolved": "https://registry.npmjs.org/traverse/-/traverse-0.3.9.tgz", - "integrity": "sha1-cXuPIgzAu3tE5AUUwisui7xw2Lk=", + "integrity": "sha512-iawgk0hLP3SxGKDfnDJf8wTz4p2qImnyihM5Hh/sGvQ3K37dPi/w8sRhdNIxYA1TwFwc5mDhIJq+O0RsvXBKdQ==", "dev": true, "engines": { "node": "*" } }, + "node_modules/triple-beam": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/triple-beam/-/triple-beam-1.3.0.tgz", + "integrity": "sha512-XrHUvV5HpdLmIj4uVMxHggLbFSZYIn7HEWsqePZcI50pco+MPqJ50wMGY794X7AOOhxOBAjbkqfAbEe/QMp2Lw==" + }, + "node_modules/ts-mutex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/ts-mutex/-/ts-mutex-1.0.0.tgz", + "integrity": "sha512-D+YehFaXHIPShGernhNKQ3saE8vBmDz4tV4KReH6gtPlkkT25rV1VhEjxSypqKVMpysEBjR71I6OCXauat1W4g==", + "engines": { + "node": ">=6.0.0" + } + }, "node_modules/tslib": { "version": "2.4.0", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.0.tgz", @@ -2120,6 +2783,7 @@ "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", "dev": true, + "optional": true, "dependencies": { "safe-buffer": "^5.0.1" }, @@ -2127,10 +2791,19 @@ "node": "*" } }, + "node_modules/type-detect": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", + "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", + "dev": true, + "engines": { + "node": ">=4" + } + }, "node_modules/typed-rest-client": { - "version": "1.8.6", - "resolved": "https://registry.npmjs.org/typed-rest-client/-/typed-rest-client-1.8.6.tgz", - "integrity": "sha512-xcQpTEAJw2DP7GqVNECh4dD+riS+C1qndXLfBCJ3xk0kqprtGN491P5KlmrDbKdtuW8NEcP/5ChxiJI3S9WYTA==", + "version": "1.8.9", + "resolved": "https://registry.npmjs.org/typed-rest-client/-/typed-rest-client-1.8.9.tgz", + "integrity": "sha512-uSmjE38B80wjL85UFX3sTYEUlvZ1JgCRhsWj/fJ4rZ0FqDUFoIuodtiVeE+cUqiVTOKPdKrp/sdftD15MDek6g==", "dev": true, "dependencies": { "qs": "^6.9.1", @@ -2158,11 +2831,19 @@ "dev": true }, "node_modules/underscore": { - "version": "1.13.1", - "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.13.1.tgz", - "integrity": "sha512-hzSoAVtJF+3ZtiFX0VgfFPHEDRm7Y/QPjGyNo4TVdnDTdft3tr8hEkD25a1jC+TjTuE7tkHGKkhwCgs9dgBB2g==", + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.13.4.tgz", + "integrity": "sha512-BQFnUDuAQ4Yf/cYY5LNrK9NCJFKriaRbD9uR1fTeXnBeoa97W0i41qkZfGO9pSo8I5KzjAcSY2XYtdf0oKd7KQ==", "dev": true }, + "node_modules/universalify": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz", + "integrity": "sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==", + "engines": { + "node": ">= 10.0.0" + } + }, "node_modules/unzipper": { "version": "0.10.11", "resolved": "https://registry.npmjs.org/unzipper/-/unzipper-0.10.11.tgz", @@ -2190,199 +2871,78 @@ "node_modules/util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=", - "dev": true + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==" }, - "node_modules/vsce": { - "version": "2.10.0", - "resolved": "https://registry.npmjs.org/vsce/-/vsce-2.10.0.tgz", - "integrity": "sha512-b+wB3XMapEi368g64klSM6uylllZdNutseqbNY+tUoHYSy6g2NwnlWuAGKDQTYc0IqfDUjUFRQBpPgA89Q+Fyw==", + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", "dev": true, "dependencies": { - "azure-devops-node-api": "^11.0.1", - "chalk": "^2.4.2", - "cheerio": "^1.0.0-rc.9", - "commander": "^6.1.0", - "glob": "^7.0.6", - "hosted-git-info": "^4.0.2", - "keytar": "^7.7.0", - "leven": "^3.1.0", - "markdown-it": "^12.3.2", - "mime": "^1.3.4", - "minimatch": "^3.0.3", - "parse-semver": "^1.1.1", - "read": "^1.0.7", - "semver": "^5.1.0", - "tmp": "^0.2.1", - "typed-rest-client": "^1.8.4", - "url-join": "^4.0.1", - "xml2js": "^0.4.23", - "yauzl": "^2.3.1", - "yazl": "^2.2.2" + "isexe": "^2.0.0" }, "bin": { - "vsce": "vsce" - }, - "engines": { - "node": ">= 14" - } - }, - "node_modules/vsce/node_modules/ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "dev": true, - "dependencies": { - "color-convert": "^1.9.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/vsce/node_modules/chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "dev": true, - "dependencies": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/vsce/node_modules/color-convert": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", - "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", - "dev": true, - "dependencies": { - "color-name": "1.1.3" - } - }, - "node_modules/vsce/node_modules/color-name": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=", - "dev": true - }, - "node_modules/vsce/node_modules/escape-string-regexp": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=", - "dev": true, - "engines": { - "node": ">=0.8.0" - } - }, - "node_modules/vsce/node_modules/glob": { - "version": "7.2.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", - "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", - "dev": true, - "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.1.1", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - }, - "engines": { - "node": "*" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/vsce/node_modules/has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", - "dev": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/vsce/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, - "dependencies": { - "brace-expansion": "^1.1.7" + "node-which": "bin/node-which" }, "engines": { - "node": "*" + "node": ">= 8" } }, - "node_modules/vsce/node_modules/supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "dev": true, + "node_modules/winston": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/winston/-/winston-3.3.3.tgz", + "integrity": "sha512-oEXTISQnC8VlSAKf1KYSSd7J6IWuRPQqDdo8eoRNaYKLvwSb5+79Z3Yi1lrl6KDpU6/VWaxpakDAtb1oQ4n9aw==", "dependencies": { - "has-flag": "^3.0.0" + "@dabh/diagnostics": "^2.0.2", + "async": "^3.1.0", + "is-stream": "^2.0.0", + "logform": "^2.2.0", + "one-time": "^1.0.0", + "readable-stream": "^3.4.0", + "stack-trace": "0.0.x", + "triple-beam": "^1.3.0", + "winston-transport": "^4.4.0" }, "engines": { - "node": ">=4" + "node": ">= 6.4.0" } }, - "node_modules/vscode-test": { - "version": "1.6.1", - "resolved": "https://registry.npmjs.org/vscode-test/-/vscode-test-1.6.1.tgz", - "integrity": "sha512-086q88T2ca1k95mUzffvbzb7esqQNvJgiwY4h29ukPhFo8u+vXOOmelUoU5EQUHs3Of8+JuQ3oGdbVCqaxuTXA==", - "dev": true, + "node_modules/winston-transport": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/winston-transport/-/winston-transport-4.3.0.tgz", + "integrity": "sha512-B2wPuwUi3vhzn/51Uukcao4dIduEiPOcOt9HJ3QeaXgkJ5Z7UwpBzxS4ZGNHtrxrUvTwemsQiSys0ihOf8Mp1A==", "dependencies": { - "http-proxy-agent": "^4.0.1", - "https-proxy-agent": "^5.0.0", - "rimraf": "^3.0.2", - "unzipper": "^0.10.11" + "readable-stream": "^2.3.6", + "triple-beam": "^1.2.0" }, "engines": { - "node": ">=8.9.3" - } - }, - "node_modules/vscode-test-adapter-api": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/vscode-test-adapter-api/-/vscode-test-adapter-api-1.9.0.tgz", - "integrity": "sha512-lltjehUP0J9H3R/HBctjlqeUCwn2t9Lbhj2Y500ib+j5Y4H3hw+hVTzuSsfw16LtxY37knlU39QIlasa7svzOQ==", - "engines": { - "vscode": "^1.23.0" + "node": ">= 6.4.0" } }, - "node_modules/vscode-test-adapter-util": { - "version": "0.7.1", - "resolved": "https://registry.npmjs.org/vscode-test-adapter-util/-/vscode-test-adapter-util-0.7.1.tgz", - "integrity": "sha512-OZZvLDDNhayVVISyTmgUntOhMzl6j9/wVGfNqI2zuR5bQIziTQlDs9W29dFXDTGXZOxazS6uiHkrr86BKDzYUA==", + "node_modules/winston/node_modules/readable-stream": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", + "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", "dependencies": { - "tslib": "^1.11.1", - "vscode-test-adapter-api": "^1.8.0" + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" }, "engines": { - "vscode": "^1.24.0" + "node": ">= 6" } }, - "node_modules/vscode-test-adapter-util/node_modules/tslib": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", - "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==" - }, - "node_modules/which": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", - "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "dev": true, + "node_modules/winston/node_modules/winston-transport": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/winston-transport/-/winston-transport-4.5.0.tgz", + "integrity": "sha512-YpZzcUzBedhlTAfJg6vJDlyEai/IFMIVcaEZZyl3UXIl4gmqRpU7AE89AHLkbzLUsv0NVmw7ts+iztqKxxPW1Q==", "dependencies": { - "isexe": "^2.0.0" - }, - "bin": { - "node-which": "bin/node-which" + "logform": "^2.3.2", + "readable-stream": "^3.6.0", + "triple-beam": "^1.3.0" }, "engines": { - "node": ">= 8" + "node": ">= 6.4.0" } }, "node_modules/workerpool": { @@ -2411,13 +2971,13 @@ "node_modules/wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", "dev": true }, "node_modules/xml2js": { - "version": "0.4.23", - "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.4.23.tgz", - "integrity": "sha512-ySPiMjM0+pLDftHgXY4By0uswI3SPKLDw/i3UXbnO8M/p28zqexCUoPmQFrYD+/1BzhGJSs2i1ERWKJAtiLrug==", + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.5.0.tgz", + "integrity": "sha512-drPFnkQJik/O+uPKpqSgr22mpuFHqKdbS835iAQrUC73L2F5WkboIRd63ai/2Yg6I1jzifPFKH2NTK+cfglkIA==", "dev": true, "dependencies": { "sax": ">=0.6.0", @@ -2496,7 +3056,7 @@ "node_modules/yauzl": { "version": "2.10.0", "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz", - "integrity": "sha1-x+sXyT4RLLEIb6bY5R+wZnt5pfk=", + "integrity": "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==", "dev": true, "dependencies": { "buffer-crc32": "~0.2.3", @@ -2526,12 +3086,33 @@ } }, "dependencies": { + "@colors/colors": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.5.0.tgz", + "integrity": "sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==" + }, + "@dabh/diagnostics": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@dabh/diagnostics/-/diagnostics-2.0.3.tgz", + "integrity": "sha512-hrlQOIi7hAfzsMqlGSFyVucrx38O+j6wiGOf//H2ecvIEqYN4ADBSS2iLMh5UFyDunCNniUIPk/q3riFv45xRA==", + "requires": { + "colorspace": "1.1.x", + "enabled": "2.0.x", + "kuler": "^2.0.0" + } + }, "@tootallnate/once": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-1.1.2.tgz", "integrity": "sha512-RbzJvlNzmRq5c3O09UipeuXno4tA1FE6ikOjxZK0tuxVv3412l64l5t1W5pj4+rJq9vpkm/kwiR07aZXnsKPxw==", "dev": true }, + "@types/chai": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-4.3.3.tgz", + "integrity": "sha512-hC7OMnszpxhZPduX+m+nrx+uFoLkWOMiR4oa/AZF3MuSETYTZmFfJAHqZEM8MVlvfG7BEUcgvtwoCTxBp6hm3g==", + "dev": true + }, "@types/glob": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/@types/glob/-/glob-7.2.0.tgz", @@ -2555,9 +3136,9 @@ "dev": true }, "@types/node": { - "version": "16.11.6", - "resolved": "https://registry.npmjs.org/@types/node/-/node-16.11.6.tgz", - "integrity": "sha512-ua7PgUoeQFjmWPcoo9khiPum3Pd60k4/2ZGXt18sm2Slk0W0xZTqt5Y0Ny1NyBiN1EVQ/+FaF9NcY4Qe6rwk5w==", + "version": "18.6.5", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.6.5.tgz", + "integrity": "sha512-Xjt5ZGUa5WusGZJ4WJPbOT8QOqp6nDynVFRKcUt32bOgvXEoc6o085WNkYTMO7ifAj2isEfQQ2cseE+wT6jsRw==", "dev": true }, "@types/split2": { @@ -2566,21 +3147,183 @@ "integrity": "sha512-7uz3yU+LooBq4yNOzlZD9PU9/1Eu0rTD1MjQ6apOVEoHsPrMUrFw7W8XrvWtesm2vK67SBK9AyJcOXtMpl9bgQ==", "dev": true, "requires": { - "@types/node": "*" + "@types/node": "*" + } + }, + "@types/vscode": { + "version": "1.70.0", + "resolved": "https://registry.npmjs.org/@types/vscode/-/vscode-1.70.0.tgz", + "integrity": "sha512-3/9Fz0F2eBgwciazc94Ien+9u1elnjFg9YAhvAb3qDy/WeFWD9VrOPU7CIytryOVUdbxus8uzL4VZYONA0gDtA==", + "dev": true + }, + "@typestrong/ts-mockito": { + "version": "2.6.4", + "resolved": "https://registry.npmjs.org/@typestrong/ts-mockito/-/ts-mockito-2.6.4.tgz", + "integrity": "sha512-IMUau44ixvAbO7ylg/NJAwtGoAIcC5SwAm1b+QcZWMMTqw0n2NZn4nUkMWZfUY478whZ50vvMiu7rJJfCR9C9A==", + "dev": true, + "requires": { + "lodash": "^4.17.5" + } + }, + "@ungap/promise-all-settled": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@ungap/promise-all-settled/-/promise-all-settled-1.1.2.tgz", + "integrity": "sha512-sL/cEvJWAnClXw0wHk85/2L0G6Sj8UB0Ctc1TEMbKSsmpRosqhwj9gWgFRZSrBr2f9tiXISwNhCPmlfqUqyb9Q==", + "dev": true + }, + "@vscode-logging/logger": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@vscode-logging/logger/-/logger-1.2.3.tgz", + "integrity": "sha512-kQgRjbZmBBUktklaEat8YtTBuddCNRJezjJTk0Y+4VIs49KWjYkfF02GoyiCYASvCSMSqw9bBTQN3ijCO2aq+A==", + "requires": { + "@vscode-logging/types": "^0.1.4", + "fast-safe-stringify": "2.0.7", + "fs-extra": "9.1.0", + "lodash": "^4.17.21", + "stacktrace-js": "2.0.2", + "streamroller": "2.2.3", + "triple-beam": "1.3.0", + "winston": "3.3.3", + "winston-transport": "4.3.0" + } + }, + "@vscode-logging/types": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/@vscode-logging/types/-/types-0.1.4.tgz", + "integrity": "sha512-uxuHQfpX9RbkgSj5unJFmciXRczyFSaAI2aA829MYYkE8jxlhZLRLoiJLymTNiojNVdV7fFE3CILF5Q6M+EBsA==" + }, + "@vscode/test-electron": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@vscode/test-electron/-/test-electron-2.1.5.tgz", + "integrity": "sha512-O/ioqFpV+RvKbRykX2ItYPnbcZ4Hk5V0rY4uhQjQTLhGL9WZUvS7exzuYQCCI+ilSqJpctvxq2llTfGXf9UnnA==", + "dev": true, + "requires": { + "http-proxy-agent": "^4.0.1", + "https-proxy-agent": "^5.0.0", + "rimraf": "^3.0.2", + "unzipper": "^0.10.11" + } + }, + "@vscode/vsce": { + "version": "2.19.0", + "resolved": "https://registry.npmjs.org/@vscode/vsce/-/vsce-2.19.0.tgz", + "integrity": "sha512-dAlILxC5ggOutcvJY24jxz913wimGiUrHaPkk16Gm9/PGFbz1YezWtrXsTKUtJws4fIlpX2UIlVlVESWq8lkfQ==", + "dev": true, + "requires": { + "azure-devops-node-api": "^11.0.1", + "chalk": "^2.4.2", + "cheerio": "^1.0.0-rc.9", + "commander": "^6.1.0", + "glob": "^7.0.6", + "hosted-git-info": "^4.0.2", + "jsonc-parser": "^3.2.0", + "keytar": "^7.7.0", + "leven": "^3.1.0", + "markdown-it": "^12.3.2", + "mime": "^1.3.4", + "minimatch": "^3.0.3", + "parse-semver": "^1.1.1", + "read": "^1.0.7", + "semver": "^5.1.0", + "tmp": "^0.2.1", + "typed-rest-client": "^1.8.4", + "url-join": "^4.0.1", + "xml2js": "^0.5.0", + "yauzl": "^2.3.1", + "yazl": "^2.2.2" + }, + "dependencies": { + "ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "requires": { + "color-convert": "^1.9.0" + } + }, + "brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "requires": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "requires": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + } + }, + "color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, + "requires": { + "color-name": "1.1.3" + } + }, + "color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", + "dev": true + }, + "escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "dev": true + }, + "glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "dev": true, + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, + "has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "dev": true + }, + "minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "requires": { + "brace-expansion": "^1.1.7" + } + }, + "supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + } } }, - "@types/vscode": { - "version": "1.69.1", - "resolved": "https://registry.npmjs.org/@types/vscode/-/vscode-1.69.1.tgz", - "integrity": "sha512-YZ77g3u9S9Xw3dwAgRgNAwnKNS3nPlhSu3XKOIYQzCcItUrZovfJUlf/29wjON2VZvHGuYQnhKuJUP15ccpVIQ==", - "dev": true - }, - "@ungap/promise-all-settled": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@ungap/promise-all-settled/-/promise-all-settled-1.1.2.tgz", - "integrity": "sha512-sL/cEvJWAnClXw0wHk85/2L0G6Sj8UB0Ctc1TEMbKSsmpRosqhwj9gWgFRZSrBr2f9tiXISwNhCPmlfqUqyb9Q==", - "dev": true - }, "agent-base": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", @@ -2627,10 +3370,26 @@ "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", "dev": true }, + "assertion-error": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.1.0.tgz", + "integrity": "sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==", + "dev": true + }, + "async": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.4.tgz", + "integrity": "sha512-iAB+JbDEGXhyIUavoDl9WP/Jj106Kz9DEn1DPgYw5ruDn0e3Wgi3sKFm55sASdGBNOQB8F59d9qQ7deqrHA8wQ==" + }, + "at-least-node": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/at-least-node/-/at-least-node-1.0.0.tgz", + "integrity": "sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==" + }, "azure-devops-node-api": { - "version": "11.0.1", - "resolved": "https://registry.npmjs.org/azure-devops-node-api/-/azure-devops-node-api-11.0.1.tgz", - "integrity": "sha512-YMdjAw9l5p/6leiyIloxj3k7VIvYThKjvqgiQn88r3nhT93ENwsoDS3A83CyJ4uTWzCZ5f5jCi6c27rTU5Pz+A==", + "version": "11.2.0", + "resolved": "https://registry.npmjs.org/azure-devops-node-api/-/azure-devops-node-api-11.2.0.tgz", + "integrity": "sha512-XdiGPhrpaT5J8wdERRKs5g8E0Zy1pvOYTli7z9E8nmOn3YGp4FhtjhrOyFmX/8veWCwdI69mCHKJw6l+4J/bHA==", "dev": true, "requires": { "tunnel": "0.0.6", @@ -2647,18 +3406,19 @@ "version": "1.5.1", "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", - "dev": true + "dev": true, + "optional": true }, "big-integer": { - "version": "1.6.50", - "resolved": "https://registry.npmjs.org/big-integer/-/big-integer-1.6.50.tgz", - "integrity": "sha512-+O2uoQWFRo8ysZNo/rjtri2jIwjr3XfeAgRjAUADRqGG+ZITvyn8J1kvXLTaKVr3hhGXk+f23tKfdzmklVM9vQ==", + "version": "1.6.51", + "resolved": "https://registry.npmjs.org/big-integer/-/big-integer-1.6.51.tgz", + "integrity": "sha512-GPEid2Y9QU1Exl1rpO9B2IPJGHPSupF5GnVIP0blYvNOMer2bTvSWs1jGOUg04hTmu67nmLsQ9TBo1puaotBHg==", "dev": true }, "binary": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/binary/-/binary-0.3.0.tgz", - "integrity": "sha1-n2BVO8XOjDOG87VTz/R0Yq3sqnk=", + "integrity": "sha512-D4H1y5KYwpJgK8wk1Cue5LLPgmwHKYSChkbspQg5JtVuR5ulGckxfR62H3AE9UDkdMC8yyXlqYihuz3Aqg2XZg==", "dev": true, "requires": { "buffers": "~0.1.1", @@ -2676,6 +3436,7 @@ "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", "dev": true, + "optional": true, "requires": { "buffer": "^5.5.0", "inherits": "^2.0.4", @@ -2687,6 +3448,7 @@ "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", "dev": true, + "optional": true, "requires": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", @@ -2698,23 +3460,22 @@ "bluebird": { "version": "3.4.7", "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.4.7.tgz", - "integrity": "sha1-9y12C+Cbf3bQjtj66Ysomo0F+rM=", + "integrity": "sha512-iD3898SR7sWVRHbiQv+sHUtHnMvC1o3nW5rAcqnq3uOn07DSAppZYUkIGslDz6gXC7HfunPe7YVBgoEJASPcHA==", "dev": true }, "boolbase": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", - "integrity": "sha1-aN/1++YMUes3cl6p4+0xDcwed24=", + "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==", "dev": true }, "brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", "dev": true, "requires": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" + "balanced-match": "^1.0.0" } }, "braces": { @@ -2737,6 +3498,7 @@ "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", "dev": true, + "optional": true, "requires": { "base64-js": "^1.3.1", "ieee754": "^1.1.13" @@ -2745,7 +3507,7 @@ "buffer-crc32": { "version": "0.2.13", "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", - "integrity": "sha1-DTM+PwDqxQqhRUq9MO+MKl2ackI=", + "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==", "dev": true }, "buffer-indexof-polyfill": { @@ -2757,7 +3519,7 @@ "buffers": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/buffers/-/buffers-0.1.1.tgz", - "integrity": "sha1-skV5w77U1tOWru5tmorn9Ugqt7s=", + "integrity": "sha512-9q/rDEGSb/Qsvv2qvzIzdluL5k7AaJOTrw23z9reQthrbF7is4CtlT0DXyO1oei2DCp4uojjzQ7igaSHp1kAEQ==", "dev": true }, "call-bind": { @@ -2771,15 +3533,30 @@ } }, "camelcase": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.2.0.tgz", - "integrity": "sha512-c7wVvbw3f37nuobQNtgsgG9POC9qMbNuMQmTCqZv23b6MIz0fcYpBiOlv9gEN/hdLdnZTDQhg6e9Dq5M1vKvfg==", + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", + "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", "dev": true }, + "chai": { + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/chai/-/chai-4.3.6.tgz", + "integrity": "sha512-bbcp3YfHCUzMOvKqsztczerVgBKSsEijCySNlHHbX3VG1nskvqjz5Rfso1gGwD6w6oOV3eI60pKuMOV5MV7p3Q==", + "dev": true, + "requires": { + "assertion-error": "^1.1.0", + "check-error": "^1.0.2", + "deep-eql": "^3.0.1", + "get-func-name": "^2.0.0", + "loupe": "^2.3.1", + "pathval": "^1.1.1", + "type-detect": "^4.0.5" + } + }, "chainsaw": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/chainsaw/-/chainsaw-0.1.0.tgz", - "integrity": "sha1-XqtQsor+WAdNDVgpE4iCi15fvJg=", + "integrity": "sha512-75kWfWt6MEKNC8xYXIdRpDehRYY/tNSgwKaJq+dbbDcxORuVrrQ+SEHoWsniVn9XPYfP4gmdWIeDk/4YNp1rNQ==", "dev": true, "requires": { "traverse": ">=0.3.0 <0.4" @@ -2806,32 +3583,39 @@ } } }, + "check-error": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.2.tgz", + "integrity": "sha512-BrgHpW9NURQgzoNyjfq0Wu6VFO6D7IZEmJNdtgNqpzGG8RuNFHt2jQxWlAs4HMe119chBnv+34syEZtc6IhLtA==", + "dev": true + }, "cheerio": { - "version": "1.0.0-rc.10", - "resolved": "https://registry.npmjs.org/cheerio/-/cheerio-1.0.0-rc.10.tgz", - "integrity": "sha512-g0J0q/O6mW8z5zxQ3A8E8J1hUgp4SMOvEoW/x84OwyHKe/Zccz83PVT4y5Crcr530FV6NgmKI1qvGTKVl9XXVw==", + "version": "1.0.0-rc.12", + "resolved": "https://registry.npmjs.org/cheerio/-/cheerio-1.0.0-rc.12.tgz", + "integrity": "sha512-VqR8m68vM46BNnuZ5NtnGBKIE/DfN0cRIzg9n40EIq9NOv90ayxLBXA8fXC5gquFRGJSTRqBq25Jt2ECLR431Q==", "dev": true, "requires": { - "cheerio-select": "^1.5.0", - "dom-serializer": "^1.3.2", - "domhandler": "^4.2.0", - "htmlparser2": "^6.1.0", - "parse5": "^6.0.1", - "parse5-htmlparser2-tree-adapter": "^6.0.1", - "tslib": "^2.2.0" + "cheerio-select": "^2.1.0", + "dom-serializer": "^2.0.0", + "domhandler": "^5.0.3", + "domutils": "^3.0.1", + "htmlparser2": "^8.0.1", + "parse5": "^7.0.0", + "parse5-htmlparser2-tree-adapter": "^7.0.0" } }, "cheerio-select": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/cheerio-select/-/cheerio-select-1.5.0.tgz", - "integrity": "sha512-qocaHPv5ypefh6YNxvnbABM07KMxExbtbfuJoIie3iZXX1ERwYmJcIiRrr9H05ucQP1k28dav8rpdDgjQd8drg==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/cheerio-select/-/cheerio-select-2.1.0.tgz", + "integrity": "sha512-9v9kG0LvzrlcungtnJtpGNxY+fzECQKhK4EGJX2vByejiMX84MFNQw4UxPJl3bFbTMw+Dfs37XaIkCwTZfLh4g==", "dev": true, "requires": { - "css-select": "^4.1.3", - "css-what": "^5.0.1", - "domelementtype": "^2.2.0", - "domhandler": "^4.2.0", - "domutils": "^2.7.0" + "boolbase": "^1.0.0", + "css-select": "^5.1.0", + "css-what": "^6.1.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3", + "domutils": "^3.0.1" } }, "chokidar": { @@ -2854,7 +3638,8 @@ "version": "1.1.4", "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", - "dev": true + "dev": true, + "optional": true }, "cliui": { "version": "7.0.4", @@ -2867,6 +3652,30 @@ "wrap-ansi": "^7.0.0" } }, + "color": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/color/-/color-3.2.1.tgz", + "integrity": "sha512-aBl7dZI9ENN6fUGC7mWpMTPNHmWUSNan9tuWN6ahh5ZLNk9baLJOnSMlrQkHcrfFgz2/RigjUVAjdx36VcemKA==", + "requires": { + "color-convert": "^1.9.3", + "color-string": "^1.6.0" + }, + "dependencies": { + "color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "requires": { + "color-name": "1.1.3" + } + }, + "color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==" + } + } + }, "color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", @@ -2879,8 +3688,25 @@ "color-name": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, + "color-string": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/color-string/-/color-string-1.9.1.tgz", + "integrity": "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==", + "requires": { + "color-name": "^1.0.0", + "simple-swizzle": "^0.2.2" + } + }, + "colorspace": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/colorspace/-/colorspace-1.1.4.tgz", + "integrity": "sha512-BgvKJiuVu1igBUF2kEjRCZXol6wiiGbY5ipL/oVPwm0BL9sIpMIzM8IK7vwuxIIzOXMV3Ey5w+vxhm0rR/TN8w==", + "requires": { + "color": "^3.1.3", + "text-hex": "1.0.x" + } }, "commander": { "version": "6.2.1", @@ -2891,39 +3717,42 @@ "concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", "dev": true }, "core-util-is": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", - "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", - "dev": true + "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==" }, "css-select": { - "version": "4.1.3", - "resolved": "https://registry.npmjs.org/css-select/-/css-select-4.1.3.tgz", - "integrity": "sha512-gT3wBNd9Nj49rAbmtFHj1cljIAOLYSX1nZ8CB7TBO3INYckygm5B7LISU/szY//YmdiSLbJvDLOx9VnMVpMBxA==", + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/css-select/-/css-select-5.1.0.tgz", + "integrity": "sha512-nwoRF1rvRRnnCqqY7updORDsuqKzqYJ28+oSMaJMMgOauh3fvwHqMS7EZpIPqK8GL+g9mKxF1vP/ZjSeNjEVHg==", "dev": true, "requires": { "boolbase": "^1.0.0", - "css-what": "^5.0.0", - "domhandler": "^4.2.0", - "domutils": "^2.6.0", - "nth-check": "^2.0.0" + "css-what": "^6.1.0", + "domhandler": "^5.0.2", + "domutils": "^3.0.1", + "nth-check": "^2.0.1" } }, "css-what": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/css-what/-/css-what-5.1.0.tgz", - "integrity": "sha512-arSMRWIIFY0hV8pIxZMEfmMI47Wj3R/aWpZDDxWYCPEiOMv6tfOrnpDtgxBYPEQD4V0Y/958+1TdC3iWTFcUPw==", + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.1.0.tgz", + "integrity": "sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw==", "dev": true }, + "date-format": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/date-format/-/date-format-2.1.0.tgz", + "integrity": "sha512-bYQuGLeFxhkxNOF3rcMtiZxvCBAquGzZm6oWA1oZ0g2THUzivaRhv8uOhdr19LmoobSOLoIAxeUK2RdbM8IFTA==" + }, "debug": { - "version": "4.3.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.3.tgz", - "integrity": "sha512-/zxw5+vh1Tfv+4Qn7a5nsbcJKPaSvCDhojn6FEl9vupwK2VCSDtEiEtqr8DFtzYFOdz63LBkxec7DYuc2jon6Q==", - "dev": true, + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", "requires": { "ms": "2.1.2" }, @@ -2931,8 +3760,7 @@ "ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" } } }, @@ -2947,21 +3775,33 @@ "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", "dev": true, + "optional": true, "requires": { "mimic-response": "^3.1.0" } }, + "deep-eql": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-3.0.1.tgz", + "integrity": "sha512-+QeIQyN5ZuO+3Uk5DYh6/1eKO0m0YmJFGNmFHGACpf1ClL1nmlV/p4gNgbl2pJGxgXb4faqo6UE+M5ACEMyVcw==", + "dev": true, + "requires": { + "type-detect": "^4.0.0" + } + }, "deep-extend": { "version": "0.6.0", "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", - "dev": true + "dev": true, + "optional": true }, "detect-libc": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.1.tgz", "integrity": "sha512-463v3ZeIrcWtdgIg6vI6XUncguvr2TnGl4SzDXinkt9mSLpBJKXT3mW6xT3VQdDN11+WVs29pgvivTc4Lp8v+w==", - "dev": true + "dev": true, + "optional": true }, "diff": { "version": "5.0.0", @@ -2970,46 +3810,46 @@ "dev": true }, "dom-serializer": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-1.3.2.tgz", - "integrity": "sha512-5c54Bk5Dw4qAxNOI1pFEizPSjVsx5+bpJKmL2kPn8JhBUq2q09tTCa3mjijun2NfK78NMouDYNMBkOrPZiS+ig==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", + "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", "dev": true, "requires": { - "domelementtype": "^2.0.1", - "domhandler": "^4.2.0", - "entities": "^2.0.0" + "domelementtype": "^2.3.0", + "domhandler": "^5.0.2", + "entities": "^4.2.0" } }, "domelementtype": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.2.0.tgz", - "integrity": "sha512-DtBMo82pv1dFtUmHyr48beiuq792Sxohr+8Hm9zoxklYPfa6n0Z3Byjj2IV7bmr2IyqClnqEQhfgHJJ5QF0R5A==", + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", + "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", "dev": true }, "domhandler": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-4.2.2.tgz", - "integrity": "sha512-PzE9aBMsdZO8TK4BnuJwH0QT41wgMbRzuZrHUcpYncEjmQazq8QEaBWgLG7ZyC/DAZKEgglpIA6j4Qn/HmxS3w==", + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", + "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", "dev": true, "requires": { - "domelementtype": "^2.2.0" + "domelementtype": "^2.3.0" } }, "domutils": { - "version": "2.8.0", - "resolved": "https://registry.npmjs.org/domutils/-/domutils-2.8.0.tgz", - "integrity": "sha512-w96Cjofp72M5IIhpjgobBimYEfoPjx1Vx0BSX9P30WBdZW2WIKU0T1Bd0kz2eNZ9ikjKgHbEyKx8BB6H1L3h3A==", + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.0.1.tgz", + "integrity": "sha512-z08c1l761iKhDFtfXO04C7kTdPBLi41zwOZl00WS8b5eiaebNpY00HKbztwBq+e3vyqWNwWF3mP9YLUeqIrF+Q==", "dev": true, "requires": { - "dom-serializer": "^1.0.1", - "domelementtype": "^2.2.0", - "domhandler": "^4.2.0" + "dom-serializer": "^2.0.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.1" } }, "duplexer2": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/duplexer2/-/duplexer2-0.1.4.tgz", - "integrity": "sha1-ixLauHjA1p4+eJEFFmKjL8a93ME=", + "integrity": "sha512-asLFVfWWtJ90ZyOUHMqk7/S2w2guQKxUI2itj3d92ADHhxUSbCMGi1f1cBcJ7xM1To+pE/Khbwo1yuNbMEPKeA==", "dev": true, "requires": { "readable-stream": "^2.0.2" @@ -3021,21 +3861,35 @@ "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", "dev": true }, + "enabled": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/enabled/-/enabled-2.0.0.tgz", + "integrity": "sha512-AKrN98kuwOzMIdAizXGI86UFBoo26CL21UM763y1h/GMSJ4/OHU9k2YlsmBpyScFo/wbLzWQJBMCW4+IO3/+OQ==" + }, "end-of-stream": { "version": "1.4.4", "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", "dev": true, + "optional": true, "requires": { "once": "^1.4.0" } }, "entities": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/entities/-/entities-2.2.0.tgz", - "integrity": "sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==", + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.3.1.tgz", + "integrity": "sha512-o4q/dYJlmyjP2zfnaWDUC6A3BQFmVTX+tZPezK7k0GLSU9QYCauscf5Y+qcEPzKL+EixVouYDgLQK5H9GrLpkg==", "dev": true }, + "error-stack-parser": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/error-stack-parser/-/error-stack-parser-2.1.4.tgz", + "integrity": "sha512-Sk5V6wVazPhq5MhpO+AUxJn5x7XSXGl1R93Vn7i+zS15KDVxQijejNCrz8340/2bgLBjR9GtEG8ZVKONDjcqGQ==", + "requires": { + "stackframe": "^1.3.4" + } + }, "escalade": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", @@ -3052,17 +3906,28 @@ "version": "2.0.3", "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", "integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==", - "dev": true + "dev": true, + "optional": true + }, + "fast-safe-stringify": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-2.0.7.tgz", + "integrity": "sha512-Utm6CdzT+6xsDk2m8S6uL8VHxNwI6Jub+e9NYTcAms28T84pTa25GJQV9j0CY0N1rM8hK4x6grpF2BQf+2qwVA==" }, "fd-slicer": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz", - "integrity": "sha1-JcfInLH5B3+IkbvmHY85Dq4lbx4=", + "integrity": "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==", "dev": true, "requires": { "pend": "~1.2.0" } }, + "fecha": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/fecha/-/fecha-4.2.3.tgz", + "integrity": "sha512-OP2IUU6HeYKJi3i0z4A19kHMQoLVs4Hc+DPqqxI2h/DPZHTm/vjsfC6P0b4jCMy14XizLBqvndQ+UilD7707Jw==" + }, "fill-range": { "version": "7.0.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", @@ -3088,16 +3953,33 @@ "integrity": "sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==", "dev": true }, + "fn.name": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fn.name/-/fn.name-1.1.0.tgz", + "integrity": "sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw==" + }, "fs-constants": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", - "dev": true + "dev": true, + "optional": true + }, + "fs-extra": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", + "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", + "requires": { + "at-least-node": "^1.0.0", + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + } }, "fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", "dev": true }, "fsevents": { @@ -3119,6 +4001,16 @@ "rimraf": "2" }, "dependencies": { + "brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "requires": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, "glob": { "version": "7.2.3", "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", @@ -3165,22 +4057,29 @@ "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", "dev": true }, + "get-func-name": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.0.tgz", + "integrity": "sha512-Hm0ixYtaSZ/V7C8FJrtZIuBBI+iSgL+1Aq82zSu8VQNB4S3Gk8e7Qs3VwBDJAhmRZcFqkl3tQu36g/Foh5I5ig==", + "dev": true + }, "get-intrinsic": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.1.1.tgz", - "integrity": "sha512-kWZrnVM42QCiEA2Ig1bG8zjoIMOgxWwYCEeNdwY6Tv/cOSeGpcoX4pXHfKUxNKVoArnrEr2e9srnAxxGIraS9Q==", + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.1.2.tgz", + "integrity": "sha512-Jfm3OyCxHh9DJyc28qGk+JmfkpO41A4XkneDSujN9MDXrm4oDKdHvndhZ2dN94+ERNfkYJWDclW6k2L/ZGHjXA==", "dev": true, "requires": { "function-bind": "^1.1.1", "has": "^1.0.3", - "has-symbols": "^1.0.1" + "has-symbols": "^1.0.3" } }, "github-from-package": { "version": "0.0.0", "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz", "integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==", - "dev": true + "dev": true, + "optional": true }, "glob": { "version": "8.0.3", @@ -3195,19 +4094,10 @@ "once": "^1.3.0" }, "dependencies": { - "brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", - "dev": true, - "requires": { - "balanced-match": "^1.0.0" - } - }, "minimatch": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.0.tgz", - "integrity": "sha512-9TPBGGak4nHfGZsPBohm9AWg6NoT7QTCehS3BIJABslyZbzxfV78QM2Y6+i741OPZIafFAaiiEMh5OyIrJPgtg==", + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.2.tgz", + "integrity": "sha512-bNH9mmM9qsJ2X4r2Nat1B//1dJVcn3+iBLa3IgqJ7EbGaDNepL9QSHOxN4ng33s52VMMhhIfgCYDk3C4ZmlDAg==", "dev": true, "requires": { "brace-expansion": "^2.0.1" @@ -3225,10 +4115,9 @@ } }, "graceful-fs": { - "version": "4.2.8", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.8.tgz", - "integrity": "sha512-qkIilPUYcNhJpd33n0GBXTB1MMPp14TxEsEs0pTrsSVucApsYzW5V+Q8Qxhik6KU3evy+qkAAowTByymK0avdg==", - "dev": true + "version": "4.2.10", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.10.tgz", + "integrity": "sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA==" }, "growl": { "version": "1.10.5", @@ -3252,9 +4141,9 @@ "dev": true }, "has-symbols": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.2.tgz", - "integrity": "sha512-chXa79rL/UC2KlX17jo3vRGz0azaWEx5tGqZg5pO3NUyEJVB17dMruQlzCCOfUvElghKcm5194+BCRvi2Rv/Gw==", + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", + "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", "dev": true }, "he": { @@ -3264,24 +4153,24 @@ "dev": true }, "hosted-git-info": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-4.0.2.tgz", - "integrity": "sha512-c9OGXbZ3guC/xOlCg1Ci/VgWlwsqDv1yMQL1CWqXDL0hDjXuNcq0zuR4xqPSuasI3kqFDhqSyTjREz5gzq0fXg==", + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-4.1.0.tgz", + "integrity": "sha512-kyCuEOWjJqZuDbRHzL8V93NzQhwIB71oFWSyzVo+KPZI+pnQPPxucdkrOZvkLRnrf5URsQM+IJ09Dw29cRALIA==", "dev": true, "requires": { "lru-cache": "^6.0.0" } }, "htmlparser2": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-6.1.0.tgz", - "integrity": "sha512-gyyPk6rgonLFEDGoeRgQNaEUvdJ4ktTmmUh/h2t7s+M8oPpIPxgNACWa+6ESR57kXstwqPiCut0V8NRpcwgU7A==", + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-8.0.1.tgz", + "integrity": "sha512-4lVbmc1diZC7GUJQtRQ5yBAeUCL1exyMwmForWkRLnwyzWBFxN633SALPMGYaWZvKe9j1pRZJpauvmxENSp/EA==", "dev": true, "requires": { - "domelementtype": "^2.0.1", - "domhandler": "^4.0.0", - "domutils": "^2.5.2", - "entities": "^2.0.0" + "domelementtype": "^2.3.0", + "domhandler": "^5.0.2", + "domutils": "^3.0.1", + "entities": "^4.3.0" } }, "http-proxy-agent": { @@ -3296,9 +4185,9 @@ } }, "https-proxy-agent": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.0.tgz", - "integrity": "sha512-EkYm5BcKUGiduxzSt3Eppko+PiNWNEpa4ySk9vTC6wDsQJW9rHSa+UhGNJoRYp7bz6Ht1eaRIa6QaJqO5rCFbA==", + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", "dev": true, "requires": { "agent-base": "6", @@ -3309,12 +4198,13 @@ "version": "1.2.1", "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", - "dev": true + "dev": true, + "optional": true }, "inflight": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", - "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", "dev": true, "requires": { "once": "^1.3.0", @@ -3324,14 +4214,19 @@ "inherits": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", - "dev": true + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" }, "ini": { "version": "1.3.8", "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", - "dev": true + "dev": true, + "optional": true + }, + "is-arrayish": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz", + "integrity": "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==" }, "is-binary-path": { "version": "2.1.0", @@ -3375,6 +4270,11 @@ "integrity": "sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA==", "dev": true }, + "is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==" + }, "is-unicode-supported": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", @@ -3384,13 +4284,12 @@ "isarray": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", - "dev": true + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==" }, "isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", "dev": true }, "js-yaml": { @@ -3402,16 +4301,37 @@ "argparse": "^2.0.1" } }, + "jsonc-parser": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.2.0.tgz", + "integrity": "sha512-gfFQZrcTc8CnKXp6Y4/CBT3fTc0OVuDofpre4aEeEpSBPV5X5v4+Vmx+8snU7RLPrNHPKSgLxGo9YuQzz20o+w==", + "dev": true + }, + "jsonfile": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", + "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", + "requires": { + "graceful-fs": "^4.1.6", + "universalify": "^2.0.0" + } + }, "keytar": { "version": "7.9.0", "resolved": "https://registry.npmjs.org/keytar/-/keytar-7.9.0.tgz", "integrity": "sha512-VPD8mtVtm5JNtA2AErl6Chp06JBfy7diFQ7TQQhdpWOl6MrCRB+eRbvAZUsbGQS9kiMq0coJsy0W0vHpDCkWsQ==", "dev": true, + "optional": true, "requires": { "node-addon-api": "^4.3.0", "prebuild-install": "^7.0.1" } }, + "kuler": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/kuler/-/kuler-2.0.0.tgz", + "integrity": "sha512-Xq9nH7KlWZmXAtodXDDRE7vs6DU1gTU8zYDHDiWLSip45Egwq3plLHzPn27NgvzL2r1LMPC1vdqh98sQxtqj4A==" + }, "leven": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", @@ -3430,7 +4350,7 @@ "listenercount": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/listenercount/-/listenercount-1.0.1.tgz", - "integrity": "sha1-hMinKrWcRyUyFIDJdeZQg0LnCTc=", + "integrity": "sha512-3mk/Zag0+IJxeDrxSgaDPy4zZ3w05PRZeJNnlWhzFz5OkX49J4krc+A8X2d2M69vGMBEX0uyl8M+W+8gH+kBqQ==", "dev": true }, "locate-path": { @@ -3442,6 +4362,11 @@ "p-locate": "^5.0.0" } }, + "lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" + }, "log-symbols": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", @@ -3452,6 +4377,27 @@ "is-unicode-supported": "^0.1.0" } }, + "logform": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/logform/-/logform-2.4.2.tgz", + "integrity": "sha512-W4c9himeAwXEdZ05dQNerhFz2XG80P9Oj0loPUMV23VC2it0orMHQhJm4hdnnor3rd1HsGf6a2lPwBM1zeXHGw==", + "requires": { + "@colors/colors": "1.5.0", + "fecha": "^4.2.0", + "ms": "^2.1.1", + "safe-stable-stringify": "^2.3.1", + "triple-beam": "^1.3.0" + } + }, + "loupe": { + "version": "2.3.4", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-2.3.4.tgz", + "integrity": "sha512-OvKfgCC2Ndby6aSTREl5aCCPTNIzlDfQZvZxNUrBrihDhL3xcrYegTblhmEiCrg2kKQz4XsFIaemE5BF4ybSaQ==", + "dev": true, + "requires": { + "get-func-name": "^2.0.0" + } + }, "lru-cache": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", @@ -3498,7 +4444,8 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", - "dev": true + "dev": true, + "optional": true }, "minimatch": { "version": "4.2.1", @@ -3506,7 +4453,7 @@ "integrity": "sha512-9Uq1ChtSZO+Mxa/CL1eGizn2vRn3MlLgzhT0Iz8zaY8NdvxvB0d5QdPFmCKf7JKA9Lerx5vRrnwO03jsSfGG9g==", "dev": true, "requires": { - "brace-expansion": "^1.1.7" + "brace-expansion": "^2.0.1" } }, "minimist": { @@ -3516,19 +4463,20 @@ "dev": true }, "mkdirp": { - "version": "0.5.5", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.5.tgz", - "integrity": "sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==", + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", + "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", "dev": true, "requires": { - "minimist": "^1.2.5" + "minimist": "^1.2.6" } }, "mkdirp-classic": { "version": "0.5.3", "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==", - "dev": true + "dev": true, + "optional": true }, "mocha": { "version": "9.2.2", @@ -3562,6 +4510,33 @@ "yargs-unparser": "2.0.0" }, "dependencies": { + "brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "requires": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "debug": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.3.tgz", + "integrity": "sha512-/zxw5+vh1Tfv+4Qn7a5nsbcJKPaSvCDhojn6FEl9vupwK2VCSDtEiEtqr8DFtzYFOdz63LBkxec7DYuc2jon6Q==", + "dev": true, + "requires": { + "ms": "2.1.2" + }, + "dependencies": { + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + } + } + }, "glob": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.0.tgz", @@ -3592,8 +4567,7 @@ "ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "dev": true + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" }, "mute-stream": { "version": "0.0.8", @@ -3611,13 +4585,15 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-1.0.2.tgz", "integrity": "sha512-ONmRUqK7zj7DWX0D9ADe03wbwOBZxNAfF20PlGfCWQcD3+/MakShIHrMqx9YwPTfxDdF1zLeL+RGZiR9kGMLdg==", - "dev": true + "dev": true, + "optional": true }, "node-abi": { "version": "3.24.0", "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.24.0.tgz", "integrity": "sha512-YPG3Co0luSu6GwOBsmIdGW6Wx0NyNDLg/hriIyDllVsNwnI6UeqaWShxC3lbH4LtEQUgoLP3XR1ndXiDAWvmRw==", "dev": true, + "optional": true, "requires": { "semver": "^7.3.5" }, @@ -3627,6 +4603,7 @@ "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.7.tgz", "integrity": "sha512-QlYTucUYOews+WeEujDoEGziz4K6c47V/Bd+LjSSYcA94p+DmINdf7ncaUinThfvZyu13lN9OY1XDxt8C0Tw0g==", "dev": true, + "optional": true, "requires": { "lru-cache": "^6.0.0" } @@ -3637,7 +4614,8 @@ "version": "4.3.0", "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-4.3.0.tgz", "integrity": "sha512-73sE9+3UaLYYFmDsFZnqCInzPyh3MqIwZO9cw58yIqAZhONrrabrYyYe3TuIqtIiOuTXVhsGau8hcrhhwSsDIQ==", - "dev": true + "dev": true, + "optional": true }, "normalize-path": { "version": "3.0.0", @@ -3646,29 +4624,37 @@ "dev": true }, "nth-check": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.0.1.tgz", - "integrity": "sha512-it1vE95zF6dTT9lBsYbxvqh0Soy4SPowchj0UBGj/V6cTPnXXtQOPUbhZ6CmGzAD/rW22LQK6E96pcdJXk4A4w==", + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", + "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==", "dev": true, "requires": { "boolbase": "^1.0.0" } }, "object-inspect": { - "version": "1.11.0", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.11.0.tgz", - "integrity": "sha512-jp7ikS6Sd3GxQfZJPyH3cjcbJF6GZPClgdV+EFygjFLQ5FmW/dRUnTd9PQ9k0JhoNDabWFbpF1yCdSWCC6gexg==", + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.2.tgz", + "integrity": "sha512-z+cPxW0QGUp0mcqcsgQyLVRDoXFQbXOwBaqyF7VIgI4TWNQsDHrBpUQslRmIfAoYWdYzs6UlKJtB2XJpTaNSpQ==", "dev": true }, "once": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", "dev": true, "requires": { "wrappy": "1" } }, + "one-time": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/one-time/-/one-time-1.0.0.tgz", + "integrity": "sha512-5DXOiRKwuSEcQ/l0kGCF6Q3jcADFv5tSmRaJck/OqkVFcOzutB134KRSfF0xDrL39MNnqxbHBbUUcjZIhTgb2g==", + "requires": { + "fn.name": "1.x.x" + } + }, "p-limit": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", @@ -3690,25 +4676,29 @@ "parse-semver": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/parse-semver/-/parse-semver-1.1.1.tgz", - "integrity": "sha1-mkr9bfBj3Egm+T+6SpnPIj9mbLg=", + "integrity": "sha512-Eg1OuNntBMH0ojvEKSrvDSnwLmvVuUOSdylH/pSCPNMIspLlweJyIWXCE+k/5hm3cj/EBUYwmWkjhBALNP4LXQ==", "dev": true, "requires": { "semver": "^5.1.0" } }, "parse5": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/parse5/-/parse5-6.0.1.tgz", - "integrity": "sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw==", - "dev": true + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.0.0.tgz", + "integrity": "sha512-y/t8IXSPWTuRZqXc0ajH/UwDj4mnqLEbSttNbThcFhGrZuOyoyvNBO85PBp2jQa55wY9d07PBNjsK8ZP3K5U6g==", + "dev": true, + "requires": { + "entities": "^4.3.0" + } }, "parse5-htmlparser2-tree-adapter": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/parse5-htmlparser2-tree-adapter/-/parse5-htmlparser2-tree-adapter-6.0.1.tgz", - "integrity": "sha512-qPuWvbLgvDGilKc5BoicRovlT4MtYT6JfJyBOMDsKoiT+GiuP5qyrPCnR9HcPECIJJmZh5jRndyNThnhhb/vlA==", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/parse5-htmlparser2-tree-adapter/-/parse5-htmlparser2-tree-adapter-7.0.0.tgz", + "integrity": "sha512-B77tOZrqqfUfnVcOrUvfdLbz4pu4RopLD/4vmu3HUPswwTA8OH0EMW9BlWR2B0RCoiZRAHEUu7IxeP1Pd1UU+g==", "dev": true, "requires": { - "parse5": "^6.0.1" + "domhandler": "^5.0.2", + "parse5": "^7.0.0" } }, "path-exists": { @@ -3720,13 +4710,19 @@ "path-is-absolute": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", - "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true + }, + "pathval": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-1.1.1.tgz", + "integrity": "sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ==", "dev": true }, "pend": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", - "integrity": "sha1-elfrVQpng/kRUzH89GY9XI4AelA=", + "integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==", "dev": true }, "picomatch": { @@ -3740,6 +4736,7 @@ "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.1.tgz", "integrity": "sha512-jAXscXWMcCK8GgCoHOfIr0ODh5ai8mj63L2nWrjuAgXE6tDyYGnx4/8o/rCgU+B4JSyZBKbeZqzhtwtC3ovxjw==", "dev": true, + "optional": true, "requires": { "detect-libc": "^2.0.0", "expand-template": "^2.0.3", @@ -3758,14 +4755,14 @@ "process-nextick-args": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", - "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", - "dev": true + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==" }, "pump": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", "dev": true, + "optional": true, "requires": { "end-of-stream": "^1.1.0", "once": "^1.3.1" @@ -3794,6 +4791,7 @@ "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", "dev": true, + "optional": true, "requires": { "deep-extend": "^0.6.0", "ini": "~1.3.0", @@ -3805,14 +4803,15 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", - "dev": true + "dev": true, + "optional": true } } }, "read": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/read/-/read-1.0.7.tgz", - "integrity": "sha1-s9oZvQUkMal2cdRKQmNK33ELQMQ=", + "integrity": "sha512-rSOKNYUmaxy0om1BNjMN4ezNT6VKK+2xF4GBhc81mkH7L60i6dp8qPYrkndNLT3QPphoII3maL9PVC9XmhHwVQ==", "dev": true, "requires": { "mute-stream": "~0.0.4" @@ -3822,7 +4821,6 @@ "version": "2.3.7", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", - "dev": true, "requires": { "core-util-is": "~1.0.0", "inherits": "~2.0.3", @@ -3836,8 +4834,7 @@ "safe-buffer": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", - "dev": true + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" } } }, @@ -3853,7 +4850,7 @@ "require-directory": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", - "integrity": "sha1-jGStX9MNqxyXbiNE/+f3kqam30I=", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", "dev": true }, "rimraf": { @@ -3865,6 +4862,16 @@ "glob": "^7.1.3" }, "dependencies": { + "brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "requires": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, "glob": { "version": "7.2.3", "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", @@ -3896,6 +4903,11 @@ "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", "dev": true }, + "safe-stable-stringify": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.3.1.tgz", + "integrity": "sha512-kYBSfT+troD9cDA85VDnHZ1rpHC50O0g1e6WlGHVCz/g+JS+9WKLj+XwFYyR8UbrZN8ll9HUpDAAddY58MGisg==" + }, "sax": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz", @@ -3920,7 +4932,7 @@ "setimmediate": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", - "integrity": "sha1-KQy7Iy4waULX1+qbg3Mqt4VvgoU=", + "integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==", "dev": true }, "side-channel": { @@ -3938,29 +4950,115 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==", - "dev": true + "dev": true, + "optional": true }, "simple-get": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz", "integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==", "dev": true, + "optional": true, "requires": { "decompress-response": "^6.0.0", "once": "^1.3.1", "simple-concat": "^1.0.0" } }, + "simple-swizzle": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz", + "integrity": "sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==", + "requires": { + "is-arrayish": "^0.3.1" + } + }, + "source-map": { + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.6.tgz", + "integrity": "sha512-MjZkVp0NHr5+TPihLcadqnlVoGIoWo4IBHptutGh9wI3ttUYvCG26HkSuDi+K6lsZ25syXJXcctwgyVCt//xqA==" + }, "split2": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/split2/-/split2-4.1.0.tgz", "integrity": "sha512-VBiJxFkxiXRlUIeyMQi8s4hgvKCSjtknJv/LVYbrgALPwf5zSKmEwV9Lst25AkvMDnvxODugjdl6KZgwKM1WYQ==" }, + "stack-generator": { + "version": "2.0.10", + "resolved": "https://registry.npmjs.org/stack-generator/-/stack-generator-2.0.10.tgz", + "integrity": "sha512-mwnua/hkqM6pF4k8SnmZ2zfETsRUpWXREfA/goT8SLCV4iOFa4bzOX2nDipWAZFPTjLvQB82f5yaodMVhK0yJQ==", + "requires": { + "stackframe": "^1.3.4" + } + }, + "stack-trace": { + "version": "0.0.10", + "resolved": "https://registry.npmjs.org/stack-trace/-/stack-trace-0.0.10.tgz", + "integrity": "sha512-KGzahc7puUKkzyMt+IqAep+TVNbKP+k2Lmwhub39m1AsTSkaDutx56aDCo+HLDzf/D26BIHTJWNiTG1KAJiQCg==" + }, + "stackframe": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/stackframe/-/stackframe-1.3.4.tgz", + "integrity": "sha512-oeVtt7eWQS+Na6F//S4kJ2K2VbRlS9D43mAlMyVpVWovy9o+jfgH8O9agzANzaiLjclA0oYzUXEM4PurhSUChw==" + }, + "stacktrace-gps": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/stacktrace-gps/-/stacktrace-gps-3.1.2.tgz", + "integrity": "sha512-GcUgbO4Jsqqg6RxfyTHFiPxdPqF+3LFmQhm7MgCuYQOYuWyqxo5pwRPz5d/u6/WYJdEnWfK4r+jGbyD8TSggXQ==", + "requires": { + "source-map": "0.5.6", + "stackframe": "^1.3.4" + } + }, + "stacktrace-js": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/stacktrace-js/-/stacktrace-js-2.0.2.tgz", + "integrity": "sha512-Je5vBeY4S1r/RnLydLl0TBTi3F2qdfWmYsGvtfZgEI+SCprPppaIhQf5nGcal4gI4cGpCV/duLcAzT1np6sQqg==", + "requires": { + "error-stack-parser": "^2.0.6", + "stack-generator": "^2.0.5", + "stacktrace-gps": "^3.0.4" + } + }, + "streamroller": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/streamroller/-/streamroller-2.2.3.tgz", + "integrity": "sha512-AegmvQsscTRhHVO46PhCDerjIpxi7E+d2GxgUDu+nzw/HuLnUdxHWr6WQ+mVn/4iJgMKKFFdiUwFcFRDvcjCtw==", + "requires": { + "date-format": "^2.1.0", + "debug": "^4.1.1", + "fs-extra": "^8.1.0" + }, + "dependencies": { + "fs-extra": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz", + "integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==", + "requires": { + "graceful-fs": "^4.2.0", + "jsonfile": "^4.0.0", + "universalify": "^0.1.0" + } + }, + "jsonfile": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", + "integrity": "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==", + "requires": { + "graceful-fs": "^4.1.6" + } + }, + "universalify": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", + "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==" + } + } + }, "string_decoder": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "dev": true, "requires": { "safe-buffer": "~5.1.0" }, @@ -3968,8 +5066,7 @@ "safe-buffer": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", - "dev": true + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" } } }, @@ -4013,6 +5110,7 @@ "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.1.tgz", "integrity": "sha512-V0r2Y9scmbDRLCNex/+hYzvp/zyYjvFbHPNgVTKfQvVrb6guiE/fxP+XblDNR011utopbkex2nM4dHNV6GDsng==", "dev": true, + "optional": true, "requires": { "chownr": "^1.1.1", "mkdirp-classic": "^0.5.2", @@ -4025,6 +5123,7 @@ "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", "dev": true, + "optional": true, "requires": { "bl": "^4.0.3", "end-of-stream": "^1.4.1", @@ -4038,6 +5137,7 @@ "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", "dev": true, + "optional": true, "requires": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", @@ -4046,6 +5146,11 @@ } } }, + "text-hex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/text-hex/-/text-hex-1.0.0.tgz", + "integrity": "sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg==" + }, "tmp": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.1.tgz", @@ -4067,9 +5172,19 @@ "traverse": { "version": "0.3.9", "resolved": "https://registry.npmjs.org/traverse/-/traverse-0.3.9.tgz", - "integrity": "sha1-cXuPIgzAu3tE5AUUwisui7xw2Lk=", + "integrity": "sha512-iawgk0hLP3SxGKDfnDJf8wTz4p2qImnyihM5Hh/sGvQ3K37dPi/w8sRhdNIxYA1TwFwc5mDhIJq+O0RsvXBKdQ==", "dev": true }, + "triple-beam": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/triple-beam/-/triple-beam-1.3.0.tgz", + "integrity": "sha512-XrHUvV5HpdLmIj4uVMxHggLbFSZYIn7HEWsqePZcI50pco+MPqJ50wMGY794X7AOOhxOBAjbkqfAbEe/QMp2Lw==" + }, + "ts-mutex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/ts-mutex/-/ts-mutex-1.0.0.tgz", + "integrity": "sha512-D+YehFaXHIPShGernhNKQ3saE8vBmDz4tV4KReH6gtPlkkT25rV1VhEjxSypqKVMpysEBjR71I6OCXauat1W4g==" + }, "tslib": { "version": "2.4.0", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.0.tgz", @@ -4086,14 +5201,21 @@ "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", "dev": true, + "optional": true, "requires": { "safe-buffer": "^5.0.1" } }, + "type-detect": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", + "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", + "dev": true + }, "typed-rest-client": { - "version": "1.8.6", - "resolved": "https://registry.npmjs.org/typed-rest-client/-/typed-rest-client-1.8.6.tgz", - "integrity": "sha512-xcQpTEAJw2DP7GqVNECh4dD+riS+C1qndXLfBCJ3xk0kqprtGN491P5KlmrDbKdtuW8NEcP/5ChxiJI3S9WYTA==", + "version": "1.8.9", + "resolved": "https://registry.npmjs.org/typed-rest-client/-/typed-rest-client-1.8.9.tgz", + "integrity": "sha512-uSmjE38B80wjL85UFX3sTYEUlvZ1JgCRhsWj/fJ4rZ0FqDUFoIuodtiVeE+cUqiVTOKPdKrp/sdftD15MDek6g==", "dev": true, "requires": { "qs": "^6.9.1", @@ -4114,11 +5236,16 @@ "dev": true }, "underscore": { - "version": "1.13.1", - "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.13.1.tgz", - "integrity": "sha512-hzSoAVtJF+3ZtiFX0VgfFPHEDRm7Y/QPjGyNo4TVdnDTdft3tr8hEkD25a1jC+TjTuE7tkHGKkhwCgs9dgBB2g==", + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.13.4.tgz", + "integrity": "sha512-BQFnUDuAQ4Yf/cYY5LNrK9NCJFKriaRbD9uR1fTeXnBeoa97W0i41qkZfGO9pSo8I5KzjAcSY2XYtdf0oKd7KQ==", "dev": true }, + "universalify": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz", + "integrity": "sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==" + }, "unzipper": { "version": "0.10.11", "resolved": "https://registry.npmjs.org/unzipper/-/unzipper-0.10.11.tgz", @@ -4146,158 +5273,62 @@ "util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=", - "dev": true + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==" }, - "vsce": { - "version": "2.10.0", - "resolved": "https://registry.npmjs.org/vsce/-/vsce-2.10.0.tgz", - "integrity": "sha512-b+wB3XMapEi368g64klSM6uylllZdNutseqbNY+tUoHYSy6g2NwnlWuAGKDQTYc0IqfDUjUFRQBpPgA89Q+Fyw==", + "which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", "dev": true, "requires": { - "azure-devops-node-api": "^11.0.1", - "chalk": "^2.4.2", - "cheerio": "^1.0.0-rc.9", - "commander": "^6.1.0", - "glob": "^7.0.6", - "hosted-git-info": "^4.0.2", - "keytar": "^7.7.0", - "leven": "^3.1.0", - "markdown-it": "^12.3.2", - "mime": "^1.3.4", - "minimatch": "^3.0.3", - "parse-semver": "^1.1.1", - "read": "^1.0.7", - "semver": "^5.1.0", - "tmp": "^0.2.1", - "typed-rest-client": "^1.8.4", - "url-join": "^4.0.1", - "xml2js": "^0.4.23", - "yauzl": "^2.3.1", - "yazl": "^2.2.2" + "isexe": "^2.0.0" + } + }, + "winston": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/winston/-/winston-3.3.3.tgz", + "integrity": "sha512-oEXTISQnC8VlSAKf1KYSSd7J6IWuRPQqDdo8eoRNaYKLvwSb5+79Z3Yi1lrl6KDpU6/VWaxpakDAtb1oQ4n9aw==", + "requires": { + "@dabh/diagnostics": "^2.0.2", + "async": "^3.1.0", + "is-stream": "^2.0.0", + "logform": "^2.2.0", + "one-time": "^1.0.0", + "readable-stream": "^3.4.0", + "stack-trace": "0.0.x", + "triple-beam": "^1.3.0", + "winston-transport": "^4.4.0" }, "dependencies": { - "ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "dev": true, - "requires": { - "color-convert": "^1.9.0" - } - }, - "chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "dev": true, - "requires": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - } - }, - "color-convert": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", - "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", - "dev": true, - "requires": { - "color-name": "1.1.3" - } - }, - "color-name": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=", - "dev": true - }, - "escape-string-regexp": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=", - "dev": true - }, - "glob": { - "version": "7.2.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", - "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", - "dev": true, - "requires": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.1.1", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - } - }, - "has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", - "dev": true - }, - "minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, + "readable-stream": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", + "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", "requires": { - "brace-expansion": "^1.1.7" + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" } }, - "supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "dev": true, + "winston-transport": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/winston-transport/-/winston-transport-4.5.0.tgz", + "integrity": "sha512-YpZzcUzBedhlTAfJg6vJDlyEai/IFMIVcaEZZyl3UXIl4gmqRpU7AE89AHLkbzLUsv0NVmw7ts+iztqKxxPW1Q==", "requires": { - "has-flag": "^3.0.0" + "logform": "^2.3.2", + "readable-stream": "^3.6.0", + "triple-beam": "^1.3.0" } } } }, - "vscode-test": { - "version": "1.6.1", - "resolved": "https://registry.npmjs.org/vscode-test/-/vscode-test-1.6.1.tgz", - "integrity": "sha512-086q88T2ca1k95mUzffvbzb7esqQNvJgiwY4h29ukPhFo8u+vXOOmelUoU5EQUHs3Of8+JuQ3oGdbVCqaxuTXA==", - "dev": true, - "requires": { - "http-proxy-agent": "^4.0.1", - "https-proxy-agent": "^5.0.0", - "rimraf": "^3.0.2", - "unzipper": "^0.10.11" - } - }, - "vscode-test-adapter-api": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/vscode-test-adapter-api/-/vscode-test-adapter-api-1.9.0.tgz", - "integrity": "sha512-lltjehUP0J9H3R/HBctjlqeUCwn2t9Lbhj2Y500ib+j5Y4H3hw+hVTzuSsfw16LtxY37knlU39QIlasa7svzOQ==" - }, - "vscode-test-adapter-util": { - "version": "0.7.1", - "resolved": "https://registry.npmjs.org/vscode-test-adapter-util/-/vscode-test-adapter-util-0.7.1.tgz", - "integrity": "sha512-OZZvLDDNhayVVISyTmgUntOhMzl6j9/wVGfNqI2zuR5bQIziTQlDs9W29dFXDTGXZOxazS6uiHkrr86BKDzYUA==", - "requires": { - "tslib": "^1.11.1", - "vscode-test-adapter-api": "^1.8.0" - }, - "dependencies": { - "tslib": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", - "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==" - } - } - }, - "which": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", - "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "dev": true, + "winston-transport": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/winston-transport/-/winston-transport-4.3.0.tgz", + "integrity": "sha512-B2wPuwUi3vhzn/51Uukcao4dIduEiPOcOt9HJ3QeaXgkJ5Z7UwpBzxS4ZGNHtrxrUvTwemsQiSys0ihOf8Mp1A==", "requires": { - "isexe": "^2.0.0" + "readable-stream": "^2.3.6", + "triple-beam": "^1.2.0" } }, "workerpool": { @@ -4320,13 +5351,13 @@ "wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", "dev": true }, "xml2js": { - "version": "0.4.23", - "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.4.23.tgz", - "integrity": "sha512-ySPiMjM0+pLDftHgXY4By0uswI3SPKLDw/i3UXbnO8M/p28zqexCUoPmQFrYD+/1BzhGJSs2i1ERWKJAtiLrug==", + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.5.0.tgz", + "integrity": "sha512-drPFnkQJik/O+uPKpqSgr22mpuFHqKdbS835iAQrUC73L2F5WkboIRd63ai/2Yg6I1jzifPFKH2NTK+cfglkIA==", "dev": true, "requires": { "sax": ">=0.6.0", @@ -4387,7 +5418,7 @@ "yauzl": { "version": "2.10.0", "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz", - "integrity": "sha1-x+sXyT4RLLEIb6bY5R+wZnt5pfk=", + "integrity": "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==", "dev": true, "requires": { "buffer-crc32": "~0.2.3", diff --git a/package.json b/package.json index b7fa0d4..2d31dd4 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,7 @@ "icon": "img/ruby-test-explorer.png", "author": "Connor Shea ", "publisher": "connorshea", - "version": "0.9.2", + "version": "0.10.0", "license": "MIT", "homepage": "https://github.com/connorshea/vscode-ruby-test-adapter", "repository": { @@ -28,6 +28,7 @@ "test explorer", "test adapter" ], + "type": "commonjs", "main": "out/src/main.js", "scripts": { "clean": "rimraf out *.vsix", @@ -36,33 +37,37 @@ "rebuild": "npm run clean && npm run build", "package": "vsce package", "publish": "vsce publish", - "test:minitest": "npm run build && node ./out/test/runMinitestTests.js", - "test:rspec": "npm run build && node ./out/test/runRspecTests.js" + "pretest": "npm run rebuild && npm run package", + "test": "node ./out/test/runFrameworkTests.js", + "test:minitest": "npm run test minitest", + "test:rspec": "npm run test rspec", + "test:unit": "npm run test unitTests" }, "dependencies": { + "@vscode-logging/logger": "^1.2.3", "split2": "^4.1.0", - "tslib": "^2.4.0", - "vscode-test-adapter-api": "^1.9.0", - "vscode-test-adapter-util": "^0.7.1" + "ts-mutex": "^1.0.0", + "tslib": "^2.4.0" }, "devDependencies": { + "@types/chai": "^4.3.0", "@types/glob": "^7.2.0", "@types/mocha": "^9.1.0", "@types/split2": "^3.2.1", "@types/vscode": "^1.69.0", + "@typestrong/ts-mockito": "^2.6.4", + "@vscode/test-electron": "^2.1.2", + "@vscode/vsce": "^2.16.0", + "chai": "^4.3.6", "glob": "^8.0.3", "mocha": "^9.2.2", "rimraf": "^3.0.0", - "typescript": "^4.7.4", - "vsce": "^2.10.0", - "vscode-test": "^1.6.1" + "typescript": "^4.7.4" }, "engines": { "vscode": "^1.69.0" }, - "extensionDependencies": [ - "hbenl.vscode-test-explorer" - ], + "extensionDependencies": [], "activationEvents": [ "onLanguage:ruby", "onLanguage:erb", @@ -86,6 +91,21 @@ "type": "string", "scope": "resource" }, + "rubyTestExplorer.logLevel": { + "description": "Level of information to include in logs", + "type": "string", + "scope": "resource", + "default": "info", + "enum": [ + "off", + "fatal", + "error", + "warn", + "info", + "debug", + "trace" + ] + }, "rubyTestExplorer.testFramework": { "description": "Test framework to use by default, for example rspec or minitest.", "type": "string", diff --git a/custom_formatter.rb b/ruby/custom_formatter.rb similarity index 54% rename from custom_formatter.rb rename to ruby/custom_formatter.rb index d4d1b86..8dcd33c 100644 --- a/custom_formatter.rb +++ b/ruby/custom_formatter.rb @@ -4,16 +4,19 @@ require 'rspec/core/formatters/base_formatter' require 'json' +# Formatter to emit RSpec test status information in the required format for the extension class CustomFormatter < RSpec::Core::Formatters::BaseFormatter RSpec::Core::Formatters.register self, - :message, - :dump_summary, - :stop, - :seed, - :close, - :example_passed, - :example_failed, - :example_pending + :message, + :dump_summary, + :stop, + :seed, + :close, + # :example_group_started, + :example_passed, + :example_failed, + :example_pending, + :example_started attr_reader :output_hash @@ -47,7 +50,8 @@ def stop(notification) hash[:exception] = { class: e.class.name, message: e.message, - backtrace: e.backtrace + backtrace: e.backtrace, + position: exception_position(e.backtrace_locations, example.metadata) } end end @@ -69,15 +73,27 @@ def example_passed(notification) end def example_failed(notification) - output.write "FAILED: #{notification.example.id}\n" + klass = notification.example.exception.class + status = exception_is_error?(klass) ? 'ERRORED' : 'FAILED' + exception_message = notification.example.exception.message.gsub(/\s+/, ' ').strip + output.write "#{status}(#{klass.name}:#{exception_message}): " \ + "#{notification.example.id}\n" # This isn't exposed for simplicity, need to figure out how to handle this later. # output.write "#{notification.exception.backtrace.to_json}\n" end def example_pending(notification) - output.write "PENDING: #{notification.example.id}\n" + output.write "SKIPPED: #{notification.example.id}\n" end + def example_started(notification) + output.write "RUNNING: #{notification.example.id}\n" + end + + # def example_group_started(notification) + # output.write "RUNNING: #{notification.group.id}\n" + # end + private # Properties of example: @@ -102,11 +118,33 @@ def format_example(example) id: example.id, description: example.description, full_description: example.full_description, - status: example.execution_result.status.to_s, + status: example_status(example), file_path: example.metadata[:file_path], line_number: example.metadata[:line_number], type: example.metadata[:type], - pending_message: example.execution_result.pending_message + pending_message: example.execution_result.pending_message, + duration: example.execution_result.run_time } end + + def exception_position(backtrace, metadata) + location = backtrace&.find { |frame| frame.path.end_with?(metadata[:file_path]) } + return metadata[:line_number] unless location + + location.lineno + end + + def example_status(example) + if example.exception && exception_is_error?(example.exception.class) + 'errored' + elsif example.execution_result.status == :pending + 'skipped' + else + example.execution_result.status.to_s + end + end + + def exception_is_error?(exception_class) + !exception_class.to_s.start_with?('RSpec') + end end diff --git a/ruby/test/minitest/rake_task_test.rb b/ruby/test/minitest/rake_task_test.rb index 9ce697d..8e4788c 100644 --- a/ruby/test/minitest/rake_task_test.rb +++ b/ruby/test/minitest/rake_task_test.rb @@ -82,55 +82,54 @@ def test_test_list stdout =~ /START_OF_TEST_JSON(.*)END_OF_TEST_JSON/ json = JSON.parse($1, symbolize_names: true) - assert_equal( - [ - { - description: "square of one", - full_description: "square of one", - file_path: "./test/square_test.rb", - full_path: (dir + "test/square_test.rb").to_s, - line_number: 4, - klass: "SquareTest", - method: "test_square_of_one", - runnable: "SquareTest", - id: "./test/square_test.rb[4]" - }, - { - description: "square of two", - full_description: "square of two", - file_path: "./test/square_test.rb", - full_path: (dir + "test/square_test.rb").to_s, - line_number: 8, - klass: "SquareTest", - method: "test_square_of_two", - runnable: "SquareTest", - id: "./test/square_test.rb[8]" - }, - { - description: "square error", - full_description: "square error", - file_path: "./test/square_test.rb", - full_path: (dir + "test/square_test.rb").to_s, - line_number: 12, - klass: "SquareTest", - method: "test_square_error", - runnable: "SquareTest", - id: "./test/square_test.rb[12]" - }, - { - description: "square skip", - full_description: "square skip", - file_path: "./test/square_test.rb", - full_path: (dir + "test/square_test.rb").to_s, - line_number: 16, - klass: "SquareTest", - method: "test_square_skip", - runnable: "SquareTest", - id: "./test/square_test.rb[16]" - } - ], - json[:examples] - ) + [ + { + description: "square of one", + full_description: "square of one", + file_path: "./test/square_test.rb", + full_path: (dir + "test/square_test.rb").to_s, + line_number: 4, + klass: "SquareTest", + method: "test_square_of_one", + runnable: "SquareTest", + id: "./test/square_test.rb[4]" + }, + { + description: "square of two", + full_description: "square of two", + file_path: "./test/square_test.rb", + full_path: (dir + "test/square_test.rb").to_s, + line_number: 8, + klass: "SquareTest", + method: "test_square_of_two", + runnable: "SquareTest", + id: "./test/square_test.rb[8]" + }, + { + description: "square error", + full_description: "square error", + file_path: "./test/square_test.rb", + full_path: (dir + "test/square_test.rb").to_s, + line_number: 12, + klass: "SquareTest", + method: "test_square_error", + runnable: "SquareTest", + id: "./test/square_test.rb[12]" + }, + { + description: "square skip", + full_description: "square skip", + file_path: "./test/square_test.rb", + full_path: (dir + "test/square_test.rb").to_s, + line_number: 16, + klass: "SquareTest", + method: "test_square_skip", + runnable: "SquareTest", + id: "./test/square_test.rb[16]" + } + ].each do |expectation| + assert_includes(json[:examples], expectation) + end end def test_test_run_all @@ -148,14 +147,14 @@ def test_test_run_all assert_any(examples, pass_count: 1) do |example| assert_equal "square error", example[:description] - assert_equal "failed", example[:status] + assert_equal "errored", example[:status] assert_nil example[:pending_message] refute_nil example[:exception] assert_equal "Minitest::UnexpectedError", example.dig(:exception, :class) assert_match(/RuntimeError:/, example.dig(:exception, :message)) assert_instance_of Array, example.dig(:exception, :backtrace) assert_instance_of Array, example.dig(:exception, :full_backtrace) - assert_equal 13, example.dig(:exception, :position) + assert_equal 12, example.dig(:exception, :position) end assert_any(examples, pass_count: 1) do |example| @@ -174,12 +173,12 @@ def test_test_run_all assert_equal "Expected: 3\n Actual: 4", example.dig(:exception, :message) assert_instance_of Array, example.dig(:exception, :backtrace) assert_instance_of Array, example.dig(:exception, :full_backtrace) - assert_equal 9, example.dig(:exception, :position) + assert_equal 8, example.dig(:exception, :position) end assert_any(examples, pass_count: 1) do |example| assert_equal "square skip", example[:description] - assert_equal "failed", example[:status] + assert_equal "skipped", example[:status] assert_equal "This is skip", example[:pending_message] assert_nil example[:exception] end @@ -219,7 +218,7 @@ def test_test_run_file_line assert_any(examples, pass_count: 1) do |example| assert_equal "square skip", example[:description] - assert_equal "failed", example[:status] + assert_equal "skipped", example[:status] end end end diff --git a/ruby/vscode/minitest/reporter.rb b/ruby/vscode/minitest/reporter.rb index 1b5fcaf..0990740 100644 --- a/ruby/vscode/minitest/reporter.rb +++ b/ruby/vscode/minitest/reporter.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module VSCode module Minitest class Reporter < ::Minitest::Reporter @@ -24,12 +26,9 @@ def record(result) self.count += 1 self.assertions += result.assertions results << result - data = vscode_result(result) - if result.skipped? - io.puts "\nPENDING: #{data[:id]}\n" - else - io.puts "\n#{data[:status].upcase}: #{data[:id]}\n" - end + data = vscode_result(result, false) + + io.puts "#{data[:status]}#{data[:exception]}: #{data[:id]}\n" end def report @@ -39,7 +38,7 @@ def report self.failures = aggregate[::Minitest::Assertion].size self.errors = aggregate[::Minitest::UnexpectedError].size self.skips = aggregate[::Minitest::Skip].size - json = ENV.key?("PRETTY") ? JSON.pretty_generate(vscode_data) : JSON.generate(vscode_data) + json = ENV.key?('PRETTY') ? JSON.pretty_generate(vscode_data) : JSON.generate(vscode_data) io.puts "START_OF_TEST_JSON#{json}END_OF_TEST_JSON" end @@ -57,53 +56,76 @@ def vscode_data pending_count: skips, errors_outside_of_examples_count: errors }, - summary_line: "Total time: #{total_time}, Runs: #{count}, Assertions: #{assertions}, Failures: #{failures}, Errors: #{errors}, Skips: #{skips}", - examples: results.map { |r| vscode_result(r) } + summary_line: "Total time: #{total_time}, Runs: #{count}, Assertions: #{assertions}, " \ + "Failures: #{failures}, Errors: #{errors}, Skips: #{skips}", + examples: results.map { |r| vscode_result(r, true) } } end - def vscode_result(r) - base = VSCode::Minitest.tests.find_by(klass: r.klass, method: r.name).dup - if r.skipped? - base[:status] = "failed" - base[:pending_message] = r.failure.message - elsif r.passed? - base[:status] = "passed" + def vscode_result(result, is_report) + base = VSCode::Minitest.tests.find_by(klass: result.klass, method: result.name).dup + + base[:status] = vscode_status(result, is_report) + base[:pending_message] = result.skipped? ? result.failure.message : nil + base[:exception] = vscode_exception(result, base, is_report) + base[:duration] = result.time + base.compact + end + + def vscode_status(result, is_report) + if result.skipped? + status = 'skipped' + elsif result.passed? + status = 'passed' else - base[:status] = "failed" - base[:pending_message] = nil - e = r.failure.exception - backtrace = expand_backtrace(e.backtrace) - base[:exception] = { - class: e.class.name, - message: e.message, + e = result.failure.exception + status = e.class.name == ::Minitest::UnexpectedError.name ? 'errored' : 'failed' + end + is_report ? status : status.upcase + end + + def vscode_exception(result, data, is_report) + return if result.passed? || result.skipped? + + err = result.failure.exception + backtrace = expand_backtrace(err.backtrace) + if is_report + { + class: err.class.name, + message: err.message, backtrace: clean_backtrace(backtrace), full_backtrace: backtrace, - position: exception_position(backtrace, base[:full_path]) || base[:line_number] + position: exception_position(backtrace, data[:full_path]) || data[:line_number] } + else + "(#{err.class.name}:#{err.message.tr("\n", ' ').strip})" end - base end def expand_backtrace(backtrace) backtrace.map do |line| - parts = line.split(":") + parts = line.split(':') parts[0] = File.expand_path(parts[0], VSCode.project_root) - parts.join(":") + parts.join(':') end end def clean_backtrace(backtrace) backtrace.map do |line| next unless line.start_with?(VSCode.project_root.to_s) - line.gsub(VSCode.project_root.to_s + "/", "") - end.compact + + line[VSCode.project_root.to_s] = '' + line.delete_prefix!('/') + line.delete_prefix!('\\') + line + end end def exception_position(backtrace, file) line = backtrace.find { |frame| frame.start_with?(file) } return unless line - line.split(":")[1].to_i + + line.split(':')[1].to_i end end end diff --git a/src/adapter.ts b/src/adapter.ts deleted file mode 100644 index 89a705c..0000000 --- a/src/adapter.ts +++ /dev/null @@ -1,286 +0,0 @@ -import * as vscode from 'vscode'; -import { TestAdapter, TestLoadStartedEvent, TestLoadFinishedEvent, TestRunStartedEvent, TestRunFinishedEvent, TestSuiteEvent, TestEvent } from 'vscode-test-adapter-api'; -import { Log } from 'vscode-test-adapter-util'; -import * as childProcess from 'child_process'; -import { Tests } from './tests'; -import { RspecTests } from './rspecTests'; -import { MinitestTests } from './minitestTests'; -import * as path from 'path'; - -export class RubyAdapter implements TestAdapter { - private disposables: { dispose(): void }[] = []; - - private readonly testsEmitter = new vscode.EventEmitter(); - private readonly testStatesEmitter = new vscode.EventEmitter(); - private readonly autorunEmitter = new vscode.EventEmitter(); - private testsInstance: Tests | undefined; - private currentTestFramework: string | undefined; - - get tests(): vscode.Event { return this.testsEmitter.event; } - get testStates(): vscode.Event { return this.testStatesEmitter.event; } - get autorun(): vscode.Event | undefined { return this.autorunEmitter.event; } - - constructor( - public readonly workspace: vscode.WorkspaceFolder, - private readonly log: Log, - private readonly context: vscode.ExtensionContext - ) { - this.log.info('Initializing Ruby adapter'); - - this.disposables.push(this.testsEmitter); - this.disposables.push(this.testStatesEmitter); - this.disposables.push(this.autorunEmitter); - this.disposables.push(this.createWatcher()); - this.disposables.push(this.configWatcher()); - } - - async load(): Promise { - this.log.info('Loading Ruby tests...'); - this.testsEmitter.fire({ type: 'started' }); - if (this.getTestFramework() === "rspec") { - this.log.info('Loading RSpec tests...'); - this.testsInstance = new RspecTests(this.context, this.testStatesEmitter, this.log, this.workspace); - const loadedTests = await this.testsInstance.loadTests(); - this.testsEmitter.fire({ type: 'finished', suite: loadedTests }); - } else if (this.getTestFramework() === "minitest") { - this.log.info('Loading Minitest tests...'); - this.testsInstance = new MinitestTests(this.context, this.testStatesEmitter, this.log, this.workspace); - const loadedTests = await this.testsInstance.loadTests(); - this.testsEmitter.fire({ type: 'finished', suite: loadedTests }); - } else { - this.log.warn('No test framework detected. Configure the rubyTestExplorer.testFramework setting if you want to use the Ruby Test Explorer.'); - this.testsEmitter.fire({ type: 'finished' }); - } - } - - async run(tests: string[], debuggerConfig?: vscode.DebugConfiguration): Promise { - this.log.info(`Running Ruby tests ${JSON.stringify(tests)}`); - this.testStatesEmitter.fire({ type: 'started', tests }); - if (!this.testsInstance) { - let testFramework = this.getTestFramework(); - if (testFramework === "rspec") { - this.testsInstance = new RspecTests(this.context, this.testStatesEmitter, this.log, this.workspace); - } else if (testFramework === "minitest") { - this.testsInstance = new MinitestTests(this.context, this.testStatesEmitter, this.log, this.workspace); - } - } - if (this.testsInstance) { - await this.testsInstance.runTests(tests, debuggerConfig); - } - } - - async debug(testsToRun: string[]): Promise { - this.log.info(`Debugging test(s) ${JSON.stringify(testsToRun)} of ${this.workspace.uri.fsPath}`); - - const config = vscode.workspace.getConfiguration('rubyTestExplorer', null) - - const debuggerConfig = { - name: "Debug Ruby Tests", - type: "Ruby", - request: "attach", - remoteHost: config.get('debuggerHost') || "127.0.0.1", - remotePort: config.get('debuggerPort') || "1234", - remoteWorkspaceRoot: "${workspaceRoot}" - } - - const testRunPromise = this.run(testsToRun, debuggerConfig); - - this.log.info('Starting the debug session'); - let debugSession: any; - try { - await this.testsInstance!.debugCommandStarted() - debugSession = await this.startDebugging(debuggerConfig); - } catch (err) { - this.log.error('Failed starting the debug session - aborting', err); - this.cancel(); - return; - } - - const subscription = this.onDidTerminateDebugSession((session) => { - if (debugSession != session) return; - this.log.info('Debug session ended'); - this.cancel(); // terminate the test run - subscription.dispose(); - }); - - await testRunPromise; - } - - cancel(): void { - if (this.testsInstance) { - this.log.info('Killing currently-running tests.'); - this.testsInstance.killChild(); - } else { - this.log.info('No tests running currently, no process to kill.'); - } - } - - dispose(): void { - this.cancel(); - for (const disposable of this.disposables) { - disposable.dispose(); - } - this.disposables = []; - } - - /** - * Get the configured test framework. - */ - protected getTestFramework(): string { - // Short-circuit the test framework check if we've already determined the current test framework. - if (this.currentTestFramework !== undefined) { - return this.currentTestFramework; - } - - let testFramework: string = (vscode.workspace.getConfiguration('rubyTestExplorer', null).get('testFramework') as string); - // If the test framework is something other than auto, return the value. - if (['rspec', 'minitest', 'none'].includes(testFramework)) { - this.currentTestFramework = testFramework; - return testFramework; - // If the test framework is auto, we need to try to detect the test framework type. - } else { - let detectedTestFramework = this.detectTestFramework(); - this.currentTestFramework = detectedTestFramework; - return detectedTestFramework; - } - } - - /** - * Detect the current test framework using 'bundle list'. - */ - protected detectTestFramework(): string { - this.log.info(`Getting a list of Bundler dependencies with 'bundle list'.`); - - const execArgs: childProcess.ExecOptions = { - cwd: this.workspace.uri.fsPath, - maxBuffer: 8192 * 8192 - }; - - try { - // Run 'bundle list' and set the output to bundlerList. - // Execute this syncronously to avoid the test explorer getting stuck loading. - let err, stdout = childProcess.execSync('bundle list', execArgs); - - if (err) { - this.log.error(`Error while listing Bundler dependencies: ${err}`); - this.log.error(`Output: ${stdout}`); - throw err; - } - - let bundlerList = stdout.toString(); - - // Search for rspec or minitest in the output of 'bundle list'. - // The search function returns the index where the string is found, or -1 otherwise. - if (bundlerList.search('rspec-core') >= 0) { - this.log.info(`Detected RSpec test framework.`); - return 'rspec'; - } else if (bundlerList.search('minitest') >= 0) { - this.log.info(`Detected Minitest test framework.`); - return 'minitest'; - } else { - this.log.info(`Unable to automatically detect a test framework.`); - return 'none'; - } - } catch (error) { - this.log.error(error); - return 'none'; - } - } - - protected async startDebugging(debuggerConfig: vscode.DebugConfiguration): Promise { - const debugSessionPromise = new Promise((resolve, reject) => { - - let subscription: vscode.Disposable | undefined; - subscription = vscode.debug.onDidStartDebugSession(debugSession => { - if ((debugSession.name === debuggerConfig.name) && subscription) { - resolve(debugSession); - subscription.dispose(); - subscription = undefined; - } - }); - - setTimeout(() => { - if (subscription) { - reject(new Error('Debug session failed to start within 5 seconds')); - subscription.dispose(); - subscription = undefined; - } - }, 5000); - }); - - const started = await vscode.debug.startDebugging(this.workspace, debuggerConfig); - if (started) { - return await debugSessionPromise; - } else { - throw new Error('Debug session couldn\'t be started'); - } - } - - protected onDidTerminateDebugSession(cb: (session: vscode.DebugSession) => any): vscode.Disposable { - return vscode.debug.onDidTerminateDebugSession(cb); - } - - /** - * Get the test directory based on the configuration value if there's a configured test framework. - */ - private getTestDirectory(): string | undefined { - let testFramework = this.getTestFramework(); - let testDirectory = ''; - if (testFramework === 'rspec') { - testDirectory = - (vscode.workspace.getConfiguration('rubyTestExplorer', null).get('rspecDirectory') as string) - || path.join('.', 'spec'); - } else if (testFramework === 'minitest') { - testDirectory = - (vscode.workspace.getConfiguration('rubyTestExplorer', null).get('minitestDirectory') as string) - || path.join('.', 'test'); - } - - if (testDirectory === '') { - return undefined; - } - - return path.join(this.workspace.uri.fsPath, testDirectory); - } - - /** - * Create a file watcher that will reload the test tree when a relevant file is changed. - */ - private createWatcher(): vscode.Disposable { - return vscode.workspace.onDidSaveTextDocument(document => { - // If there isn't a configured/detected test framework, short-circuit to avoid doing unnecessary work. - if (this.currentTestFramework === 'none') { - this.log.info('No test framework configured. Ignoring file change.'); - return; - } - const filename = document.uri.fsPath; - this.log.info(`${filename} was saved - checking if this effects ${this.workspace.uri.fsPath}`); - if (filename.startsWith(this.workspace.uri.fsPath)) { - let testDirectory = this.getTestDirectory(); - - // In the case that there's no configured test directory, we shouldn't try to reload the tests. - if (testDirectory !== undefined && filename.startsWith(testDirectory)) { - this.log.info('A test file has been edited, reloading tests.'); - this.load(); - } - - // Send an autorun event when a relevant file changes. - // This only causes a run if the user has autorun enabled. - this.log.info('Sending autorun event'); - this.autorunEmitter.fire(); - } - }) - } - - private configWatcher(): vscode.Disposable { - return vscode.workspace.onDidChangeConfiguration(configChange => { - this.log.info('Configuration changed'); - if (configChange.affectsConfiguration("rubyTestExplorer")) { - this.cancel(); - this.currentTestFramework = undefined; - this.load(); - this.autorunEmitter.fire(); - } - }) - } -} diff --git a/src/config.ts b/src/config.ts new file mode 100644 index 0000000..509d775 --- /dev/null +++ b/src/config.ts @@ -0,0 +1,141 @@ +import * as vscode from 'vscode'; +import * as path from 'path'; +import * as childProcess from 'child_process'; +import { IVSCodeExtLogger } from '@vscode-logging/logger'; + +export abstract class Config { + + /** + * Full path to the ruby script directory + */ + public readonly rubyScriptPath: string; + + public readonly workspaceFolder?: vscode.WorkspaceFolder; + + /** + * @param context Either a vscode.ExtensionContext with the extensionUri field set to the location of the extension, + * or a string containing the full path to the ruby script dir (the folder containing custom_formatter.rb) + */ + constructor(context: vscode.ExtensionContext | string, workspaceFolder?: vscode.WorkspaceFolder) { + if (typeof context === "object") { + this.rubyScriptPath = vscode.Uri.joinPath(context?.extensionUri ?? vscode.Uri.file("./"), 'ruby').fsPath; + } else { + this.rubyScriptPath = (context as string) + } + this.workspaceFolder = workspaceFolder + } + + /** + * Printable name of the test framework + */ + public abstract frameworkName(): string + + /** + * Get the user-configured test file pattern. + * + * @return The file pattern + */ + public getFilePattern(): Array { + let pattern: Array = + vscode.workspace.getConfiguration('rubyTestExplorer', null).get('filePattern') as Array; + return pattern || ['*_test.rb', 'test_*.rb']; + } + + /** + * Get the user-configured test directory relative to the test project root folder, if there is one. + * + * @return The test directory + */ + public abstract getRelativeTestDirectory(): string; + + /** + * Get the absolute path to user-configured test directory, if there is one. + * + * @return The test directory + */ + public getAbsoluteTestDirectory(): string { + return path.resolve(this.workspaceFolder?.uri.fsPath || '.', this.getRelativeTestDirectory()) + } + + /** + * Gets the arguments to pass to the command from the test items to be run/loaded + * + * @param testItem[] Array of test items to be run + * @param debugConfiguration debug configuration + */ + public abstract getTestArguments(testItems?: readonly vscode.TestItem[]): string[] + + /** + * Gets the command to run the test framework. + * + * @param debugConfiguration debug configuration + */ + public abstract getRunTestsCommand(debugConfiguration?: vscode.DebugConfiguration): string + + /** + * Gets the command to load some or all of the tests in the suite + * + * @param testItems Array of TestItems to resolve children of, or undefined to resolve all tests + */ + public abstract getResolveTestsCommand(): string + + /** + * Get the env vars to run the subprocess with. + * + * @return The env + */ + public abstract getProcessEnv(): any + + public static getTestFramework(log: IVSCodeExtLogger): string { + let testFramework: string = vscode.workspace.getConfiguration('rubyTestExplorer', null).get('testFramework') || ''; + // If the test framework is something other than auto, return the value. + if (['rspec', 'minitest', 'none'].includes(testFramework)) { + return testFramework; + // If the test framework is auto, we need to try to detect the test framework type. + } else { + return this.detectTestFramework(log); + } + } + + /** + * Detect the current test framework using 'bundle list'. + */ + private static detectTestFramework(log: IVSCodeExtLogger): string { + log.info("Getting a list of Bundler dependencies with 'bundle list'."); + + const execArgs: childProcess.ExecOptions = { + cwd: (vscode.workspace.workspaceFolders || [])[0].uri.fsPath, + maxBuffer: 8192 * 8192 + }; + + try { + // Run 'bundle list' and set the output to bundlerList. + // Execute this syncronously to avoid the test explorer getting stuck loading. + let err, stdout = childProcess.execSync('bundle list', execArgs); + + if (err) { + log.error('Error while listing Bundler dependencies', err); + log.error('Output', stdout); + throw err; + } + + let bundlerList = stdout.toString(); + + // Search for rspec or minitest in the output of 'bundle list'. + // The search function returns the index where the string is found, or -1 otherwise. + if (bundlerList.search('rspec-core') >= 0) { + log.info('Detected RSpec test framework.'); + return 'rspec'; + } else if (bundlerList.search('minitest') >= 0) { + log.info('Detected Minitest test framework.'); + return 'minitest'; + } else { + log.info('Unable to automatically detect a test framework.'); + return 'none'; + } + } catch (error: any) { + log.error('Error while detecting test suite', error); + return 'none'; + } + } +} diff --git a/src/frameworkProcess.ts b/src/frameworkProcess.ts new file mode 100644 index 0000000..6ccb2b3 --- /dev/null +++ b/src/frameworkProcess.ts @@ -0,0 +1,355 @@ +import * as childProcess from 'child_process'; +import * as path from 'path'; +import * as vscode from 'vscode'; +import split2 from 'split2'; +import { IChildLogger } from '@vscode-logging/logger'; +import { Status, TestStatus } from './testStatus'; +import { TestSuiteManager } from './testSuiteManager'; + +type ParsedTest = { + id: string, + full_description: string, + description: string, + file_path: string, + line_number: number, + duration: number, + status?: string, + pending_message?: string | null, + exception?: any, + location?: number, // RSpec + type?: any // RSpec - presumably tag name/focus? + full_path?: string, // Minitest + klass?: string, // Minitest + method?: string, // Minitest + runnable?: string, // Minitest +} + +export class FrameworkProcess implements vscode.Disposable { + private childProcess?: childProcess.ChildProcess; + protected readonly log: IChildLogger; + private readonly disposables: vscode.Disposable[] = [] + private isDisposed = false; + private testRunStarted = false; + private preTestErrorLines: string[] = [] + public readonly testStatusEmitter: vscode.EventEmitter = new vscode.EventEmitter() + private readonly statusPattern = + new RegExp(/(?RUNNING|PASSED|FAILED|ERRORED|SKIPPED)(:?\((:?(?(:?\w*(:?::)?)*)\:)?\s*(?.*)\))?\: (?.*)/) + + constructor( + readonly rootLog: IChildLogger, + private readonly testCommand: string, + private readonly spawnArgs: childProcess.SpawnOptions, + private readonly cancellationToken: vscode.CancellationToken, + private readonly testManager: TestSuiteManager, + ) { + this.log = rootLog.getChildLogger({label: 'FrameworkProcess'}) + this.disposables.push(this.cancellationToken.onCancellationRequested(() => { + this.log.debug('Cancellation requested') + this.dispose() + })) + + /* + * Create a listener so that we know when any tests have actually started running. Until this happens, any output + * is likely to be an error message and needs collecting up to be logged in one message. + */ + let testRunStartedListener = this.testStatusEmitter.event((e) => { + if (e.status == Status.running) { + this.log.info('Test run started - stopped capturing error output', { event: e }) + this.testRunStarted = true + this.preTestErrorLines = [] + testRunStartedListener.dispose() + } + }) + this.disposables.push(testRunStartedListener) + } + + dispose() { + this.log.debug("Dispose called") + this.isDisposed = true + if (this.childProcess) { + if (this.childProcess.kill()) { + this.log.debug("Child process killed") + } else { + this.log.debug("Attempt to kill child process failed") + } + } else { + this.log.debug("Child process not running - not killing") + } + for (const disposable of this.disposables) { + try { + disposable.dispose() + } catch (err) { + this.log.error('Error disposing object', disposable, err) + } + } + } + + public async startProcess( + args: string[], + onDebugStarted?: (value: void | PromiseLike) => void, + ) { + if (this.isDisposed) { + return + } + + try { + this.log.debug('Starting child process', { env: this.spawnArgs }) + this.childProcess = childProcess.spawn(this.testCommand, args, this.spawnArgs) + + this.childProcess.stderr!.pipe(split2()).on('data', (data) => { + let log = this.log.getChildLogger({label: 'stderr'}) + data = data.toString(); + if (data.startsWith('Fast Debugger') && onDebugStarted) { + log.info('Notifying debug session that test process is ready to debug'); + onDebugStarted() + } else { + if (this.testRunStarted) { + log.warn('%s', data); + } else { + this.preTestErrorLines.push(data) + } + } + }) + + this.childProcess.stdout!.pipe(split2()).on('data', (data) => { + let log = this.log.getChildLogger({label: 'stdout'}) + data = data.toString() + log.trace(data) + this.onDataReceived(data) + }); + + return await new Promise<{code:number, signal:string}>((resolve, reject) => { + this.childProcess!.once('exit', (code: number, signal: string) => { + this.log.trace('Child process exited', { exitCode: code, signal: signal }) + }); + this.childProcess!.once('close', (code: number, signal: string) => { + if (code == 0) { + this.log.debug('Child process exited successfully, and all streams closed', { exitCode: code, signal: signal }) + resolve({code, signal}); + } else { + this.log.error('Child process exited abnormally, and all streams closed', { exitCode: code, signal: signal }) + reject(new Error(`Child process exited abnormally. Status code: ${code}${signal ? `, signal: ${signal}` : ''}`)); + } + }); + this.childProcess!.once('error', (err: Error) => { + this.log.error('Error event from child process: %s', err.message) + reject(err); + }); + }) + } finally { + if (this.preTestErrorLines.length > 0) { + this.log.error('Test process failed to run', { message: this.preTestErrorLines }) + } + this.dispose() + } + } + + private onDataReceived(data: string): void { + let log = this.log.getChildLogger({label: 'onDataReceived'}) + + let getTest = (testId: string): vscode.TestItem => { + testId = this.testManager.normaliseTestId(testId) + return this.testManager.getOrCreateTestItem(testId) + } + try { + if (data.includes('START_OF_TEST_JSON')) { + log.trace("Received test run results: %s", data); + this.parseAndHandleTestOutput(data); + } else { + const match = this.statusPattern.exec(data) + if (match && match.groups) { + log.trace("Received test status event: %s", data); + const id = match.groups['id'] + const status = match.groups['status'] + let testItem = getTest(id) + + this.testStatusEmitter.fire(new TestStatus( + testItem, + Status[status.toLocaleLowerCase() as keyof typeof Status], + // undefined, // TODO?: get duration info here if possible + // errorMessage, // TODO: get exception info here once we can send full exception data + )) + } else { + if (this.testRunStarted) { + log.info("stdout: %s", data) + } else { + this.preTestErrorLines.push(data) + } + } + } + } catch (err) { + log.error('Error parsing output', { error: err }) + } + } + + private parseAndHandleTestOutput(testOutput: string): void { + let log = this.log.getChildLogger({label: this.parseAndHandleTestOutput.name}) + testOutput = this.getJsonFromOutput(testOutput); + log.trace('Parsing the below JSON: %s', testOutput); + let testMetadata = JSON.parse(testOutput); + let tests: Array = testMetadata.examples; + + let existingContainers: vscode.TestItem[] = [] + let parsedTests: vscode.TestItem[] = [] + if (tests && tests.length > 0) { + tests.forEach((test: ParsedTest) => { + test.id = this.testManager.normaliseTestId(test.id) + let itemAlreadyExists = true + let testItem = this.testManager.getOrCreateTestItem(test.id, (item) => { if (item.id == test.id) itemAlreadyExists = false }) + + testItem.canResolveChildren = !test.id.endsWith(']') + log.trace('canResolveChildren (%s): %s', test.id, testItem.canResolveChildren) + + testItem.label = this.parseDescription(test) + log.trace('label (%s): %s', test.id, testItem.description) + + testItem.range = this.parseRange(test) + + parsedTests.push(testItem) + if (testItem.canResolveChildren && itemAlreadyExists) { + existingContainers.push(testItem) + } + log.debug('Parsed test', test) + + if (test.status) { + this.handleStatus(testItem, test) + } + }); + for (const testFile of existingContainers) { + /* + * If a container test item (file, folder, suite, etc) already existed and was part of this test run (which + * means that we can be sure all its children are in the test run output) then replace any children it had + * with only the children that were in the test run output + * + * This means that when tests are removed from collections, they will be removed from the test suite + */ + testFile.children.replace(parsedTests.filter(x => !x.canResolveChildren && x.parent == testFile)) + } + } + if (testMetadata.summary) { + log.info('Test run completed in %d ms', testMetadata.summary.duration) + } + } + + private parseDescription(test: ParsedTest): string { + // RSpec provides test ids like "file_name.rb[1:2:3]". + // This uses the digits at the end of the id to create + // an array of numbers representing the location of the + // test in the file. + let test_location_array: Array = test.id.substring(test.id.indexOf("[") + 1, test.id.lastIndexOf("]")).split(':'); + let testNumber = test_location_array[test_location_array.length - 1]; + test.file_path = this.testManager.normaliseTestId(test.file_path).replace(/\[.*/, '') + + // If the test doesn't have a name (because it uses the 'it do' syntax), "test #n" + // is appended to the test description to distinguish between separate tests. + let description = test.description.startsWith('example at ') + ? `${test.full_description}test #${testNumber}` + : test.full_description; + + let currentFileLabel = test.file_path.split(path.sep).slice(-1)[0] + let pascalCurrentFileLabel = this.snakeToPascalCase(currentFileLabel.replace('_spec.rb', '')); + // If the current file label doesn't have a slash in it and it starts with the PascalCase'd + // file name, remove the from the start of the description. This turns, e.g. + // `ExternalAccount Validations blah blah blah' into 'Validations blah blah blah'. + if (!pascalCurrentFileLabel.includes(path.sep) && description.startsWith(pascalCurrentFileLabel)) { + // Optional check for a space following the PascalCase file name. In some + // cases, e.g. 'FileName#method_name` there's no space after the file name. + let regexString = `${pascalCurrentFileLabel}[ ]?`; + let regex = new RegExp(regexString, "g"); + description = description.replace(regex, ''); + } + return description + } + + private parseRange(test: ParsedTest): vscode.Range { + // TODO: get end line numbers of tests, as well as start/end columns + let zeroBasedStartLineNumber = test.line_number - 1 + return new vscode.Range(zeroBasedStartLineNumber, 0, zeroBasedStartLineNumber, 0); + } + + /** + * Handles test state based on the output returned by the custom RSpec formatter. + * + * @param parsedtest The test that we want to handle. + * @param context Test run context + */ + handleStatus(testItem: vscode.TestItem, parsedtest: ParsedTest): void { + const log = this.log.getChildLogger({ label: "handleStatus" }) + log.trace("Handling status of test", parsedtest); + const status = Status[parsedtest.status as keyof typeof Status] + switch (status) { + case Status.skipped: + this.testStatusEmitter.fire(new TestStatus(testItem, status)) + break; + case Status.passed: + this.testStatusEmitter.fire(new TestStatus(testItem, status, parsedtest.duration)) + break; + case Status.failed: + case Status.errored: + this.testStatusEmitter.fire(new TestStatus(testItem, status, parsedtest.duration, this.failureMessage(testItem, parsedtest))) + break; + default: + log.error('Unexpected test status %s for test ID %s', status, testItem.id) + } + } + + private failureMessage(testItem: vscode.TestItem, parsedTest: ParsedTest): vscode.TestMessage { + // Remove linebreaks from error message. + let errorMessageNoLinebreaks = parsedTest.exception.message.replace(/(\r\n|\n|\r)/, ' '); + // Prepend the class name to the error message string. + let errorMessage: string = `${parsedTest.exception.class}:\n${errorMessageNoLinebreaks}`; + + let errorMessageLine: number | undefined; + + // Add backtrace to errorMessage if it exists. + if (parsedTest.exception.backtrace) { + errorMessage += `\n\nBacktrace:\n`; + parsedTest.exception.backtrace.forEach((line: string) => { + errorMessage += `${line}\n`; + }); + } + + if (parsedTest.exception.position) { + errorMessageLine = parsedTest.exception.position; + } + + let testMessage = new vscode.TestMessage(errorMessage) + testMessage.location = new vscode.Location( + testItem.uri!, + new vscode.Position(errorMessageLine || testItem.range!.start.line, 0) + ) + return testMessage + } + + /** + * Convert a string from snake_case to PascalCase. + * Note that the function will return the input string unchanged if it + * includes a '/'. + * + * @param string The string to convert to PascalCase. + * @return The converted string. + */ + private snakeToPascalCase(string: string): string { + if (string.includes('/')) { return string } + return string.split("_").map(substr => substr.charAt(0).toUpperCase() + substr.slice(1)).join(""); + } + + /** + * Pull JSON out of the test framework output. + * + * RSpec and Minitest frequently return bad data even when they're told to + * format the output as JSON, e.g. due to code coverage messages and other + * injections from gems. This gets the JSON by searching for + * `START_OF_TEST_JSON` and an opening curly brace, as well as a closing + * curly brace and `END_OF_TEST_JSON`. These are output by the custom + * RSpec formatter or Minitest Rake task as part of the final JSON output. + * + * @param output The output returned by running a command. + * @return A string representation of the JSON found in the output. + */ + private getJsonFromOutput(output: string): string { + output = output.substring(output.indexOf('START_OF_TEST_JSON{'), output.lastIndexOf('}END_OF_TEST_JSON') + 1); + // Get rid of the `START_OF_TEST_JSON` and `END_OF_TEST_JSON` to verify that the JSON is valid. + return output.substring(output.indexOf("{"), output.lastIndexOf("}") + 1); + } +} diff --git a/src/loaderQueue.ts b/src/loaderQueue.ts new file mode 100644 index 0000000..fbd5690 --- /dev/null +++ b/src/loaderQueue.ts @@ -0,0 +1,134 @@ +import * as vscode from 'vscode'; +import { IChildLogger } from '@vscode-logging/logger'; + +/** + * Type for items in the ResolveQueue + * item: The TestItem that is enqueued + * resolve: Function to call once this item has been loaded to resolve the associated promise + * reject: Function to call if there is an error loading this test (or the batch it is part of), to reject the associated promise + */ +export type QueueItem = { + item: vscode.TestItem, + resolve: () => void, + reject: (reason?: any) => void +} + +/** + * Queue for tests to be resolved/loaded + * + * When there are many files changed at once (e.g. during git checkouts/pulls, or when first loading all files in a + * project), we need to make sure that we don't spawn hundreds of test runners at once as that'll grind the computer + * to a halt. We can't change that fact that the async file resolvers notify us about files individually, so we use + * this queue to batch them up. + * + * When a test item is enqueued, the async worker function is woken up, drains the queue and runs all the enqueued + * items in a batch. While it is running, any additional items that are enqueued will sit in the queue. When the + * worker function finishes a batch, it will drain the queue again if there are items in it and run another batch, + * and if not it will wait until more items have been enqueued + */ +export class LoaderQueue implements vscode.Disposable { + private readonly log: IChildLogger + private readonly queue: Set = new Set() + private isDisposed = false + private notifyQueueWorker?: () => void + private terminateQueueWorker?: () => void + public readonly worker: Promise + + constructor(rootLog: IChildLogger, private readonly processItems: (testItems?: vscode.TestItem[]) => Promise) { + this.log = rootLog.getChildLogger({label: `${LoaderQueue.name}`}) + this.worker = this.resolveItemsInQueueWorker() + } + + /** + * Notifies the worker function that the queue is being disposed, so that it knows to stop processing items + * from the queue and that it must terminate, then waits for the worker function to finish + */ + dispose() { + this.log.info('disposed') + this.isDisposed = true + this.queue.clear() + if (this.terminateQueueWorker) { + // Stop the worker function from waiting for more items + this.log.debug('notifying worker for disposal') + this.terminateQueueWorker() + } + // Wait for worker to finish + this.log.debug('waiting for worker to finish') + this.worker + .then(() => {this.log.info('worker promise resolved')}) + .catch((err) => {this.log.error('Error in worker', err)}) + } + + /** + * Enqueues a test item to be loaded + * + * @param item Test item to be loaded + * @returns A promise that is resolved once the test item has been loaded, or which is rejected if there is + * an error while loading the item (or the batch containing the item) + */ + public enqueue(item: vscode.TestItem): Promise { + this.log.debug('enqueing item to resolve: %s', item.id) + // Create queue item with empty functions + let queueItem: QueueItem = { + item: item, + resolve: () => {}, + reject: () => {}, + } + let itemPromise = new Promise((resolve, reject) => { + // Set the resolve & reject functions in the queue item to resolve/reject this promise + queueItem["resolve"] = () => resolve(item) + queueItem["reject"] = reject + }) + this.queue.add(queueItem) + if (this.notifyQueueWorker) { + this.log.debug('notifying worker of items in queue') + // Notify the worker function that there are items to resolve if it's waiting + this.notifyQueueWorker() + } + return itemPromise + } + + private async resolveItemsInQueueWorker(): Promise { + let log = this.log.getChildLogger({label: 'WorkerFunction'}) + log.info('worker started') + // Check to see if the queue is being disposed + while(!this.isDisposed) { + if (this.queue.size == 0) { + log.debug('awaiting items to resolve') + // While the queue is empty, wait for more items + await new Promise((resolve, reject) => { + // Set notification functions to the resolve/reject functions of this promise + this.notifyQueueWorker = async () => { + log.debug('received notification of items in queue') + resolve() + } + this.terminateQueueWorker = (reason?: any) => { + log.error('received rejection while waiting for items to be enqueued', reason) + reject(reason) + } + }) + // Clear notification functions before draining queue and processing items + this.notifyQueueWorker = undefined + this.terminateQueueWorker = undefined + } else { + // Drain queue to get batch of test items to process + let queueItems = Array.from(this.queue) + this.queue.clear() + + let items = queueItems.map(x => x["item"]) + this.log.debug('worker resolving items', items.map(x => x.id)) + try { + // Load tests for items in queue + await this.processItems(items) + // Resolve promises associated with items in queue that have now been loaded + queueItems.map(x => x["resolve"]()) + } catch (err) { + this.log.error("Error resolving tests from queue", err) + // Reject promises associated with items in queue that we were trying to load + queueItems.map(x => x["reject"](err)) + } + } + } + this.log.debug('worker finished') + } +} diff --git a/src/main.ts b/src/main.ts index 6be7a9d..b9c2427 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,32 +1,110 @@ import * as vscode from 'vscode'; -import { TestHub, testExplorerExtensionId } from 'vscode-test-adapter-api'; -import { Log, TestAdapterRegistrar } from 'vscode-test-adapter-util'; -import { RubyAdapter } from './adapter'; +import { getExtensionLogger, IChildLogger, LogLevel } from "@vscode-logging/logger"; +import { TestFactory } from './testFactory'; +import { RspecConfig } from './rspec/rspecConfig'; +import { MinitestConfig } from './minitest/minitestConfig'; +import { Config } from './config'; + +export const guessWorkspaceFolder = async (rootLog: IChildLogger) => { + let log = rootLog.getChildLogger({ label: "guessWorkspaceFolder" }) + if (!vscode.workspace.workspaceFolders) { + return undefined; + } + + log.debug("Found workspace folders", vscode.workspace.workspaceFolders) + + if (vscode.workspace.workspaceFolders.length < 2) { + return vscode.workspace.workspaceFolders[0]; + } + + for (const folder of vscode.workspace.workspaceFolders) { + try { + await vscode.workspace.fs.stat(vscode.Uri.joinPath(folder.uri, 'src/vs/loader.js')); + return folder; + } catch { + // ignored + } + } + + return undefined; +}; export async function activate(context: vscode.ExtensionContext) { - // Determine whether to send the logger a workspace. - let logWorkspaceFolder = (vscode.workspace.workspaceFolders || [])[0]; - // create a simple logger that can be configured with the configuration variables - // `rubyTestExplorer.logpanel` and `rubyTestExplorer.logfile` - let log = new Log('rubyTestExplorer', logWorkspaceFolder, 'Ruby Test Explorer Log'); - context.subscriptions.push(log); - - // get the Test Explorer extension - const testExplorerExtension = vscode.extensions.getExtension(testExplorerExtensionId); - if (log.enabled) { - log.info(`Test Explorer ${testExplorerExtension ? '' : 'not '}found`); + let extensionConfig = vscode.workspace.getConfiguration('rubyTestExplorer', null) + + let logOutputChannel = vscode.window.createOutputChannel("Ruby Test Explorer log") + context.subscriptions.push(logOutputChannel) + const log = getExtensionLogger({ + extName: "RubyTestExplorer", + level: (extensionConfig.get('logLevel') as LogLevel), // See LogLevel type in @vscode-logging/types for possible logLevels + logPath: context.logUri.fsPath, // The logPath is only available from the `vscode.ExtensionContext` + logOutputChannel: logOutputChannel, // OutputChannel for the logger + sourceLocationTracking: false, + logConsole: (extensionConfig.get('logPanel') as boolean) // define if messages should be logged to the consol + }); + if (vscode.workspace.workspaceFolders == undefined) { + log.error("No workspace opened") } - let testFramework: string = (vscode.workspace.getConfiguration('rubyTestExplorer', null).get('testFramework') as string) || 'none'; + const workspace: vscode.WorkspaceFolder | undefined = await guessWorkspaceFolder(log); + let testFramework: string = Config.getTestFramework(log); - if (testExplorerExtension && testFramework !== "none") { - const testHub = testExplorerExtension.exports; + let testConfig = testFramework == "rspec" + ? new RspecConfig(context, workspace) + : new MinitestConfig(context, workspace) - // this will register a RubyTestAdapter for each WorkspaceFolder - context.subscriptions.push(new TestAdapterRegistrar( - testHub, - workspaceFolder => new RubyAdapter(workspaceFolder, log, context), - log - )); + const debuggerConfig: vscode.DebugConfiguration = { + name: "Debug Ruby Tests", + type: "Ruby", + request: "attach", + remoteHost: extensionConfig.get('debuggerHost') || "127.0.0.1", + remotePort: extensionConfig.get('debuggerPort') || "1234", + remoteWorkspaceRoot: "${workspaceRoot}" + } + + if (testFramework !== "none") { + const controller = vscode.tests.createTestController('ruby-test-explorer', 'Ruby Test Explorer'); + + // TODO: (?) Add a "Profile" profile for profiling tests + const profiles: { runProfile: vscode.TestRunProfile, debugProfile: vscode.TestRunProfile } = { + // Default run profile for running tests + runProfile: controller.createRunProfile( + 'Run', + vscode.TestRunProfileKind.Run, + (request, token) => factory.getRunner().runHandler(request, token), + true // Default run profile + ), + + // Run profile for debugging tests + debugProfile: controller.createRunProfile( + 'Debug', + vscode.TestRunProfileKind.Debug, + (request, token) => factory.getRunner().runHandler(request, token, debuggerConfig), + true + ), + } + + const factory = new TestFactory(log, controller, testConfig, workspace); + + // Ensure disposables are registered with VSC to be disposed of when the extension is deactivated + context.subscriptions.push(controller); + context.subscriptions.push(profiles.runProfile); + context.subscriptions.push(profiles.debugProfile); + context.subscriptions.push(factory); + + controller.resolveHandler = async test => { + log.debug('resolveHandler called', test) + if (!test) { + await factory.getLoader().discoverAllFilesInWorkspace(); + } else if (test.canResolveChildren && test.id.endsWith(".rb")) { + // Only load files - folders are handled by FileWatchers, and contexts will be loaded when their file is loaded/modified + await factory.getLoader().loadTestItem(test); + } + }; + + factory.getLoader().discoverAllFilesInWorkspace(); + } + else { + log.fatal('No test framework detected. Configure the rubyTestExplorer.testFramework setting if you want to use the Ruby Test Explorer.'); } } diff --git a/src/minitest/minitestConfig.ts b/src/minitest/minitestConfig.ts new file mode 100644 index 0000000..5c1407b --- /dev/null +++ b/src/minitest/minitestConfig.ts @@ -0,0 +1,95 @@ +import * as vscode from 'vscode'; +import * as path from 'path'; +import { Config } from "../config"; + +export class MinitestConfig extends Config { + public frameworkName(): string { + return "Minitest" + } + + /** + * Get the user-configured Minitest command, if there is one. + * + * @return The Minitest command + */ + public getTestCommand(): string { + let command: string = (vscode.workspace.getConfiguration('rubyTestExplorer', null).get('minitestCommand') as string) || 'bundle exec rake'; + return `${command} -R ${this.rubyScriptPath}`; + } + + /** + * Get the user-configured rdebug-ide command, if there is one. + * + * @return The rdebug-ide command + */ + public getDebugCommand(debuggerConfig: vscode.DebugConfiguration): string { + let command: string = + (vscode.workspace.getConfiguration('rubyTestExplorer', null).get('debugCommand') as string) || + 'rdebug-ide'; + + return ( + `${command} --host ${debuggerConfig.remoteHost} --port ${debuggerConfig.remotePort}` + + ` -- ${this.rubyScriptPath}/debug_minitest.rb` + ); + } + + /** + * Get test command with formatter and debugger arguments + * + * @param debuggerConfig A VS Code debugger configuration. + * @return The test command + */ + public testCommandWithDebugger(debuggerConfig?: vscode.DebugConfiguration): string { + let cmd = `${this.getTestCommand()} vscode:minitest:run` + if (debuggerConfig) { + cmd = this.getDebugCommand(debuggerConfig); + } + return cmd; + } + + /** + * Get the user-configured test directory, if there is one. + * + * @return The test directory + */ + public getRelativeTestDirectory(): string { + return (vscode.workspace.getConfiguration('rubyTestExplorer', null).get('minitestDirectory') as string) + || path.join('.', 'test'); + } + + public getTestArguments(testItems?: readonly vscode.TestItem[]): string[] { + if (!testItems) return [] + + let args: string[] = [] + for (const testItem of testItems) { + if (testItem.id.includes('[')) { + args.push(`${testItem.uri!.fsPath}:${testItem.range!.start.line + 1}`) + } else { + args.push(testItem.uri!.fsPath) + } + } + return args + } + + public getRunTestsCommand(debugConfiguration?: vscode.DebugConfiguration): string { + return this.testCommandWithDebugger(debugConfiguration) + }; + + public getResolveTestsCommand(testItems?: readonly vscode.TestItem[]): string { + return `${this.getTestCommand()} vscode:minitest:list` + } + + /** + * Get the env vars to run the subprocess with. + * + * @return The env + */ + public getProcessEnv(): any { + return Object.assign({}, process.env, { + "RAILS_ENV": "test", + "EXT_DIR": this.rubyScriptPath, + "TESTS_DIR": this.getRelativeTestDirectory(), + "TESTS_PATTERN": this.getFilePattern().join(',') + }); + } +} diff --git a/src/minitestTests.ts b/src/minitestTests.ts deleted file mode 100644 index da92249..0000000 --- a/src/minitestTests.ts +++ /dev/null @@ -1,256 +0,0 @@ -import * as vscode from 'vscode'; -import { TestSuiteInfo, TestEvent } from 'vscode-test-adapter-api'; -import * as childProcess from 'child_process'; -import { Tests } from './tests'; - -export class MinitestTests extends Tests { - testFrameworkName = 'Minitest'; - - /** - * Representation of the Minitest test suite as a TestSuiteInfo object. - * - * @return The Minitest test suite as a TestSuiteInfo object. - */ - tests = async () => new Promise((resolve, reject) => { - try { - // If test suite already exists, use testSuite. Otherwise, load them. - let minitestTests = this.testSuite ? this.testSuite : this.loadTests(); - return resolve(minitestTests); - } catch (err) { - if (err instanceof Error) { - this.log.error(`Error while attempting to load Minitest tests: ${err.message}`); - return reject(err); - } - } - }); - - /** - * Perform a dry-run of the test suite to get information about every test. - * - * @return The raw output from the Minitest JSON formatter. - */ - initTests = async () => new Promise((resolve, reject) => { - let cmd = `${this.getTestCommand()} vscode:minitest:list`; - - // Allow a buffer of 64MB. - const execArgs: childProcess.ExecOptions = { - cwd: this.workspace.uri.fsPath, - maxBuffer: 8192 * 8192, - env: this.getProcessEnv() - }; - - this.log.info(`Getting a list of Minitest tests in suite with the following command: ${cmd}`); - - childProcess.exec(cmd, execArgs, (err, stdout) => { - if (err) { - this.log.error(`Error while finding Minitest test suite: ${err.message}`); - this.log.error(`Output: ${stdout}`); - // Show an error message. - vscode.window.showWarningMessage("Ruby Test Explorer failed to find a Minitest test suite. Make sure Minitest is installed and your configured Minitest command is correct."); - vscode.window.showErrorMessage(err.message); - throw err; - } - resolve(stdout); - }); - }); - - /** - * Get the user-configured Minitest command, if there is one. - * - * @return The Minitest command - */ - protected getTestCommand(): string { - let command: string = (vscode.workspace.getConfiguration('rubyTestExplorer', null).get('minitestCommand') as string) || 'bundle exec rake'; - return `${command} -R ${(process.platform == 'win32') ? '%EXT_DIR%' : '$EXT_DIR'}`; - } - - /** - * Get the user-configured rdebug-ide command, if there is one. - * - * @return The rdebug-ide command - */ - protected getDebugCommand(debuggerConfig: vscode.DebugConfiguration): string { - let command: string = - (vscode.workspace.getConfiguration('rubyTestExplorer', null).get('debugCommand') as string) || - 'rdebug-ide'; - - return ( - `${command} --host ${debuggerConfig.remoteHost} --port ${debuggerConfig.remotePort}` + - ` -- ${process.platform == 'win32' ? '%EXT_DIR%' : '$EXT_DIR'}/debug_minitest.rb` - ); - } - - /** - * Get the user-configured test directory, if there is one. - * - * @return The test directory - */ - getTestDirectory(): string { - let directory: string = (vscode.workspace.getConfiguration('rubyTestExplorer', null).get('minitestDirectory') as string); - return directory || './test/'; - } - - /** - * Get the absolute path of the custom_formatter.rb file. - * - * @return The spec directory - */ - protected getRubyScriptsLocation(): string { - return this.context.asAbsolutePath('./ruby'); - } - - /** - * Get the env vars to run the subprocess with. - * - * @return The env - */ - protected getProcessEnv(): any { - return Object.assign({}, process.env, { - "RAILS_ENV": "test", - "EXT_DIR": this.getRubyScriptsLocation(), - "TESTS_DIR": this.getTestDirectory(), - "TESTS_PATTERN": this.getFilePattern().join(',') - }); - } - - /** - * Get test command with formatter and debugger arguments - * - * @param debuggerConfig A VS Code debugger configuration. - * @return The test command - */ - protected testCommandWithDebugger(debuggerConfig?: vscode.DebugConfiguration): string { - let cmd = `${this.getTestCommand()} vscode:minitest:run` - if (debuggerConfig) { - cmd = this.getDebugCommand(debuggerConfig); - } - return cmd; - } - - /** - * Runs a single test. - * - * @param testLocation A file path with a line number, e.g. `/path/to/spec.rb:12`. - * @param debuggerConfig A VS Code debugger configuration. - * @return The raw output from running the test. - */ - runSingleTest = async (testLocation: string, debuggerConfig?: vscode.DebugConfiguration) => new Promise(async (resolve, reject) => { - this.log.info(`Running single test: ${testLocation}`); - let line = testLocation.split(':').pop(); - let relativeLocation = testLocation.split(/:\d+$/)[0].replace(`${this.workspace.uri.fsPath}/`, "") - const spawnArgs: childProcess.SpawnOptions = { - cwd: this.workspace.uri.fsPath, - shell: true, - env: this.getProcessEnv() - }; - - let testCommand = `${this.testCommandWithDebugger(debuggerConfig)} '${relativeLocation}:${line}'`; - this.log.info(`Running command: ${testCommand}`); - - let testProcess = childProcess.spawn( - testCommand, - spawnArgs - ); - - resolve(await this.handleChildProcess(testProcess)); - }); - - /** - * Runs tests in a given file. - * - * @param testFile The test file's file path, e.g. `/path/to/test.rb`. - * @param debuggerConfig A VS Code debugger configuration. - * @return The raw output from running the tests. - */ - runTestFile = async (testFile: string, debuggerConfig?: vscode.DebugConfiguration) => new Promise(async (resolve, reject) => { - this.log.info(`Running test file: ${testFile}`); - let relativeFile = testFile.replace(`${this.workspace.uri.fsPath}/`, "").replace(`./`, "") - const spawnArgs: childProcess.SpawnOptions = { - cwd: this.workspace.uri.fsPath, - shell: true, - env: this.getProcessEnv() - }; - - // Run tests for a given file at once with a single command. - let testCommand = `${this.testCommandWithDebugger(debuggerConfig)} '${relativeFile}'`; - this.log.info(`Running command: ${testCommand}`); - - let testProcess = childProcess.spawn( - testCommand, - spawnArgs - ); - - resolve(await this.handleChildProcess(testProcess)); - }); - - /** - * Runs the full test suite for the current workspace. - * - * @param debuggerConfig A VS Code debugger configuration. - * @return The raw output from running the test suite. - */ - runFullTestSuite = async (debuggerConfig?: vscode.DebugConfiguration) => new Promise(async (resolve, reject) => { - this.log.info(`Running full test suite.`); - const spawnArgs: childProcess.SpawnOptions = { - cwd: this.workspace.uri.fsPath, - shell: true, - env: this.getProcessEnv() - }; - - let testCommand = this.testCommandWithDebugger(debuggerConfig); - this.log.info(`Running command: ${testCommand}`); - - let testProcess = childProcess.spawn( - testCommand, - spawnArgs - ); - - resolve(await this.handleChildProcess(testProcess)); - }); - - /** - * Handles test state based on the output returned by the Minitest Rake task. - * - * @param test The test that we want to handle. - */ - handleStatus(test: any): void { - this.log.debug(`Handling status of test: ${JSON.stringify(test)}`); - if (test.status === "passed") { - this.testStatesEmitter.fire({ type: 'test', test: test.id, state: 'passed' }); - } else if (test.status === "failed" && test.pending_message === null) { - let errorMessageShort: string = test.exception.message; - let errorMessageLine: number = test.line_number; - let errorMessage: string = test.exception.message; - - if (test.exception.position) { - errorMessageLine = test.exception.position; - } - - // Add backtrace to errorMessage if it exists. - if (test.exception.backtrace) { - errorMessage += `\n\nBacktrace:\n`; - test.exception.backtrace.forEach((line: string) => { - errorMessage += `${line}\n`; - }); - errorMessage += `\n\nFull Backtrace:\n`; - test.exception.full_backtrace.forEach((line: string) => { - errorMessage += `${line}\n`; - }); - } - - this.testStatesEmitter.fire({ - type: 'test', - test: test.id, - state: 'failed', - message: errorMessage, - decorations: [{ - message: errorMessageShort, - line: errorMessageLine - 1 - }] - }); - } else if (test.status === "failed" && test.pending_message !== null) { - // Handle pending test cases. - this.testStatesEmitter.fire({ type: 'test', test: test.id, state: 'skipped', message: test.pending_message }); - } - }; -} diff --git a/src/rspec/rspecConfig.ts b/src/rspec/rspecConfig.ts new file mode 100644 index 0000000..4ce6045 --- /dev/null +++ b/src/rspec/rspecConfig.ts @@ -0,0 +1,118 @@ +import { Config } from '../config'; +import * as vscode from 'vscode' +import * as path from 'path'; + +export class RspecConfig extends Config { + readonly DEFAULT_TEST_DIRECTORY = 'spec' + + public frameworkName(): string { + return "RSpec" + } + + /** + * Get the user-configured RSpec command, if there is one. + * + * @return The RSpec command + */ + public getTestCommand(): string { + let command: string = + vscode.workspace.getConfiguration('rubyTestExplorer', null).get('rspecCommand') as string; + return command || `bundle exec rspec` + } + + /** + * Get the user-configured rdebug-ide command, if there is one. + * + * @return The rdebug-ide command + */ + public getDebugCommand(debuggerConfig: vscode.DebugConfiguration, args: string): string { + let command: string = + (vscode.workspace.getConfiguration('rubyTestExplorer', null).get('debugCommand') as string) || + 'rdebug-ide'; + + return ( + `${command} --host ${debuggerConfig.remoteHost} --port ${debuggerConfig.remotePort}` + + ` -- ${process.platform == 'win32' ? '%EXT_DIR%' : '$EXT_DIR'}/debug_rspec.rb ${args}` + ); + } + + /** + * Get the user-configured RSpec command and add file pattern detection. + * + * @return The RSpec command + */ + public getFilePatternArg(): string { + const dir = this.getRelativeTestDirectory().replace(/\/$/, ""); + let pattern = this.getFilePattern().map(p => `${dir}/**{,/*/**}/${p}`).join(',') + return `--pattern '${pattern}'`; + } + + /** + * Get the absolute path of the custom_formatter.rb file. + * + * @return The spec directory + */ + public getCustomFormatterLocation(): string { + return path.join(this.rubyScriptPath, '/custom_formatter.rb'); + } + + /** + * Get test command with formatter and debugger arguments + * + * @param debugConfiguration A VS Code debugger configuration. + * @return The test command + */ + public testCommandWithFormatterAndDebugger(debugConfiguration?: vscode.DebugConfiguration): string { + let args = `--require ${this.getCustomFormatterLocation()} --format CustomFormatter` + let cmd = `${this.getTestCommand()} ${args}` + if (debugConfiguration) { + cmd = this.getDebugCommand(debugConfiguration, args); + } + return cmd + } + + public getTestArguments(testItems?: readonly vscode.TestItem[]): string[] { + if (!testItems) return [this.getFilePatternArg()] + + let args: string[] = [] + for (const testItem of testItems) { + if (testItem.id.includes('[')) { + let locationStartIndex = testItem.id.lastIndexOf('[') + 1 + let locationEndIndex = testItem.id.lastIndexOf(']') + args.push(`${testItem.uri!.fsPath}[${testItem.id.slice(locationStartIndex, locationEndIndex)}]`) + } else { + args.push(testItem.uri!.fsPath) + } + } + return args + } + + public getRunTestsCommand(debugConfiguration?: vscode.DebugConfiguration): string { + return this.testCommandWithFormatterAndDebugger(debugConfiguration) + }; + + public getResolveTestsCommand(testItems?: readonly vscode.TestItem[]): string { + return `${this.testCommandWithFormatterAndDebugger()} --order defined --dry-run`; + } + + /** + * Get the env vars to run the subprocess with. + * + * @return The env + */ + public getProcessEnv(): any { + return Object.assign({}, process.env, { + "EXT_DIR": this.rubyScriptPath, + }); + } + + public getRelativeTestDirectory(): string { + let configDir = vscode.workspace.getConfiguration('rubyTestExplorer').get('rspecDirectory') as string + if (!configDir) + return this.DEFAULT_TEST_DIRECTORY + + if (configDir.startsWith('./')) + configDir = configDir.substring(2) + return configDir ?? this.DEFAULT_TEST_DIRECTORY; + } +} diff --git a/src/rspecTests.ts b/src/rspecTests.ts deleted file mode 100644 index 12217ca..0000000 --- a/src/rspecTests.ts +++ /dev/null @@ -1,289 +0,0 @@ -import * as vscode from 'vscode'; -import { TestSuiteInfo, TestEvent } from 'vscode-test-adapter-api'; -import * as childProcess from 'child_process'; -import { Tests } from './tests'; - -export class RspecTests extends Tests { - testFrameworkName = 'RSpec'; - - /** - * Representation of the RSpec test suite as a TestSuiteInfo object. - * - * @return The RSpec test suite as a TestSuiteInfo object. - */ - tests = async () => new Promise((resolve, reject) => { - try { - // If test suite already exists, use testSuite. Otherwise, load them. - let rspecTests = this.testSuite ? this.testSuite : this.loadTests(); - return resolve(rspecTests); - } catch (err) { - if (err instanceof Error) { - this.log.error(`Error while attempting to load RSpec tests: ${err.message}`); - return reject(err); - } - } - }); - - /** - * Perform a dry-run of the test suite to get information about every test. - * - * @return The raw output from the RSpec JSON formatter. - */ - initTests = async () => new Promise((resolve, reject) => { - let cmd = `${this.getTestCommandWithFilePattern()} --require ${this.getCustomFormatterLocation()}` - + ` --format CustomFormatter --order defined --dry-run`; - - this.log.info(`Running dry-run of RSpec test suite with the following command: ${cmd}`); - - // Allow a buffer of 64MB. - const execArgs: childProcess.ExecOptions = { - cwd: this.workspace.uri.fsPath, - maxBuffer: 8192 * 8192 - }; - - childProcess.exec(cmd, execArgs, (err, stdout) => { - if (err) { - this.log.error(`Error while finding RSpec test suite: ${err.message}`); - // Show an error message. - vscode.window.showWarningMessage( - "Ruby Test Explorer failed to find an RSpec test suite. Make sure RSpec is installed and your configured RSpec command is correct.", - "View error message" - ).then(selection => { - if (selection === "View error message") { - let outputJson = JSON.parse(Tests.getJsonFromOutput(stdout)); - let outputChannel = vscode.window.createOutputChannel('Ruby Test Explorer Error Message'); - - if (outputJson.messages.length > 0) { - let outputJsonString = outputJson.messages.join("\n\n"); - let outputJsonArray = outputJsonString.split("\n"); - outputJsonArray.forEach((line: string) => { - outputChannel.appendLine(line); - }) - } else { - outputChannel.append(err.message); - } - outputChannel.show(false); - } - }); - - throw err; - } - resolve(stdout); - }); - }); - - /** - * Get the user-configured RSpec command, if there is one. - * - * @return The RSpec command - */ - protected getTestCommand(): string { - let command: string = (vscode.workspace.getConfiguration('rubyTestExplorer', null).get('rspecCommand') as string); - return command || `bundle exec rspec` - } - - /** - * Get the user-configured rdebug-ide command, if there is one. - * - * @return The rdebug-ide command - */ - protected getDebugCommand(debuggerConfig: vscode.DebugConfiguration, args: string): string { - let command: string = - (vscode.workspace.getConfiguration('rubyTestExplorer', null).get('debugCommand') as string) || - 'rdebug-ide'; - - return ( - `${command} --host ${debuggerConfig.remoteHost} --port ${debuggerConfig.remotePort}` + - ` -- ${process.platform == 'win32' ? '%EXT_DIR%' : '$EXT_DIR'}/debug_rspec.rb ${args}` - ); - } - /** - * Get the user-configured RSpec command and add file pattern detection. - * - * @return The RSpec command - */ - protected getTestCommandWithFilePattern(): string { - let command: string = (vscode.workspace.getConfiguration('rubyTestExplorer', null).get('rspecCommand') as string); - const dir = this.getTestDirectory(); - let pattern = this.getFilePattern().map(p => `${dir}/**{,/*/**}/${p}`).join(',') - command = command || `bundle exec rspec` - return `${command} --pattern '${pattern}'`; - } - - /** - * Get the user-configured test directory, if there is one. - * - * @return The spec directory - */ - getTestDirectory(): string { - let directory: string = (vscode.workspace.getConfiguration('rubyTestExplorer', null).get('rspecDirectory') as string); - return directory || './spec/'; - } - - /** - * Get the absolute path of the custom_formatter.rb file. - * - * @return The spec directory - */ - protected getCustomFormatterLocation(): string { - return this.context.asAbsolutePath('./custom_formatter.rb'); - } - - /** - * Get test command with formatter and debugger arguments - * - * @param debuggerConfig A VS Code debugger configuration. - * @return The test command - */ - protected testCommandWithFormatterAndDebugger(debuggerConfig?: vscode.DebugConfiguration): string { - let args = `--require ${this.getCustomFormatterLocation()} --format CustomFormatter` - let cmd = `${this.getTestCommand()} ${args}` - if (debuggerConfig) { - cmd = this.getDebugCommand(debuggerConfig, args); - } - return cmd - } - - /** - * Get the env vars to run the subprocess with. - * - * @return The env - */ - protected getProcessEnv(): any { - return Object.assign({}, process.env, { - "EXT_DIR": this.getRubyScriptsLocation(), - }); - } - - /** - * Runs a single test. - * - * @param testLocation A file path with a line number, e.g. `/path/to/spec.rb:12`. - * @param debuggerConfig A VS Code debugger configuration. - * @return The raw output from running the test. - */ - runSingleTest = async (testLocation: string, debuggerConfig?: vscode.DebugConfiguration) => new Promise(async (resolve, reject) => { - this.log.info(`Running single test: ${testLocation}`); - const spawnArgs: childProcess.SpawnOptions = { - cwd: this.workspace.uri.fsPath, - shell: true, - env: this.getProcessEnv() - }; - - let testCommand = `${this.testCommandWithFormatterAndDebugger(debuggerConfig)} '${testLocation}'`; - this.log.info(`Running command: ${testCommand}`); - - let testProcess = childProcess.spawn( - testCommand, - spawnArgs - ); - - resolve(await this.handleChildProcess(testProcess)); - }); - - /** - * Runs tests in a given file. - * - * @param testFile The test file's file path, e.g. `/path/to/spec.rb`. - * @param debuggerConfig A VS Code debugger configuration. - * @return The raw output from running the tests. - */ - runTestFile = async (testFile: string, debuggerConfig?: vscode.DebugConfiguration) => new Promise(async (resolve, reject) => { - this.log.info(`Running test file: ${testFile}`); - const spawnArgs: childProcess.SpawnOptions = { - cwd: this.workspace.uri.fsPath, - shell: true - }; - - // Run tests for a given file at once with a single command. - let testCommand = `${this.testCommandWithFormatterAndDebugger(debuggerConfig)} '${testFile}'`; - this.log.info(`Running command: ${testCommand}`); - - let testProcess = childProcess.spawn( - testCommand, - spawnArgs - ); - - resolve(await this.handleChildProcess(testProcess)); - }); - - /** - * Runs the full test suite for the current workspace. - * - * @param debuggerConfig A VS Code debugger configuration. - * @return The raw output from running the test suite. - */ - runFullTestSuite = async (debuggerConfig?: vscode.DebugConfiguration) => new Promise(async (resolve, reject) => { - this.log.info(`Running full test suite.`); - const spawnArgs: childProcess.SpawnOptions = { - cwd: this.workspace.uri.fsPath, - shell: true - }; - - let testCommand = this.testCommandWithFormatterAndDebugger(debuggerConfig); - this.log.info(`Running command: ${testCommand}`); - - let testProcess = childProcess.spawn( - testCommand, - spawnArgs - ); - - resolve(await this.handleChildProcess(testProcess)); - }); - - /** - * Handles test state based on the output returned by the custom RSpec formatter. - * - * @param test The test that we want to handle. - */ - handleStatus(test: any): void { - this.log.debug(`Handling status of test: ${JSON.stringify(test)}`); - if (test.status === "passed") { - this.testStatesEmitter.fire({ type: 'test', test: test.id, state: 'passed' }); - } else if (test.status === "failed" && test.pending_message === null) { - // Remove linebreaks from error message. - let errorMessageNoLinebreaks = test.exception.message.replace(/(\r\n|\n|\r)/, ' '); - // Prepend the class name to the error message string. - let errorMessage: string = `${test.exception.class}:\n${errorMessageNoLinebreaks}`; - - let fileBacktraceLineNumber: number | undefined; - - let filePath = test.file_path.replace('./', ''); - - // Add backtrace to errorMessage if it exists. - if (test.exception.backtrace) { - errorMessage += `\n\nBacktrace:\n`; - test.exception.backtrace.forEach((line: string) => { - errorMessage += `${line}\n`; - // If the backtrace line includes the current file path, try to get the line number from it. - if (line.includes(filePath)) { - let filePathArray = filePath.split('/'); - let fileName = filePathArray[filePathArray.length - 1]; - // Input: spec/models/game_spec.rb:75:in `block (3 levels) in - // Output: 75 - let regex = new RegExp(`${fileName}\:(\\d+)`); - let match = line.match(regex); - if (match && match[1]) { - fileBacktraceLineNumber = parseInt(match[1]); - } - } - }); - } - - this.testStatesEmitter.fire({ - type: 'test', - test: test.id, - state: 'failed', - message: errorMessage, - decorations: [{ - // Strip line breaks from the message. - message: errorMessageNoLinebreaks, - line: (fileBacktraceLineNumber ? fileBacktraceLineNumber : test.line_number) - 1 - }] - }); - } else if (test.status === "failed" && test.pending_message !== null) { - // Handle pending test cases. - this.testStatesEmitter.fire({ type: 'test', test: test.id, state: 'skipped', message: test.pending_message }); - } - }; -} diff --git a/src/testFactory.ts b/src/testFactory.ts new file mode 100644 index 0000000..63ad40d --- /dev/null +++ b/src/testFactory.ts @@ -0,0 +1,154 @@ +import * as vscode from 'vscode'; +import { IChildLogger, IVSCodeExtLogger, LogLevel } from "@vscode-logging/logger"; +import { Config } from './config'; +import { TestLoader } from './testLoader'; +import { RspecConfig } from './rspec/rspecConfig'; +import { MinitestConfig } from './minitest/minitestConfig'; +import { TestSuiteManager } from './testSuiteManager'; +import { TestRunner } from './testRunner'; + +/** + * Factory for (re)creating {@link TestRunner} and {@link TestLoader} instances + * + * Also takes care of disposing them when required + */ +export class TestFactory implements vscode.Disposable { + private readonly log: IChildLogger; + private isDisposed = false; + private loader: TestLoader | null = null; + private runner: TestRunner | null = null; + protected disposables: { dispose(): void }[] = []; + protected framework: string; + private manager: TestSuiteManager + + constructor( + private readonly rootLog: IVSCodeExtLogger, + private readonly controller: vscode.TestController, + private config: Config, + private readonly workspace?: vscode.WorkspaceFolder, + ) { + this.log = rootLog.getChildLogger({ label: `${TestFactory.name}` }) + this.disposables.push(this.configWatcher()); + this.framework = Config.getTestFramework(this.rootLog); + this.manager = new TestSuiteManager(this.log, this.controller, this.config) + } + + dispose(): void { + this.log.debug("Dispose called") + this.isDisposed = true + for (const disposable of this.disposables) { + try { + this.log.debug('Disposing object', disposable) + disposable.dispose(); + } catch (error) { + this.log.error('Error when disposing object', disposable, error) + } + } + this.disposables = []; + } + + /** + * Returns the current {@link TestRunner} instance + * + * If one does not exist, a new instance is created according to the current configured test framework, + * which is then returned + * + * @returns The current {@link TestRunner} instance + * @throws if this factory has been disposed + */ + public getRunner(): TestRunner { + this.checkIfDisposed() + if (!this.runner) { + this.runner = new TestRunner( + this.rootLog, + this.manager, + this.workspace, + ) + this.disposables.push(this.runner); + } + return this.runner + } + + /** + * Returns the current {@link TestLoader} instance + * + * If one does not exist, a new instance is created which is then returned + * + * @returns {@link TestLoader} + * @throws if this factory has been disposed + */ + public getLoader(): TestLoader { + this.checkIfDisposed() + if (!this.loader) { + this.loader = new TestLoader( + this.rootLog, + this.manager, + this.getRunner() + ) + this.disposables.push(this.loader) + } + return this.loader + } + + /** + * Helper method to check the current value of the isDisposed flag and to throw an error + * if it is set, to prevent objects being created after {@link dispose()} has been called + */ + private checkIfDisposed(): void { + if (this.isDisposed) { + throw new Error("Factory has been disposed") + } + } + + /** + * Registers a listener with VSC to be notified if the configuration is changed to use a different test framework. + * + * If an event is received, we dispose the current loader, runner and config so that they can be recreated + * for the new framework + * + * @returns A {@link Disposable} that is used to unregister the config watcher from receiving events + */ + private configWatcher(): vscode.Disposable { + return vscode.workspace.onDidChangeConfiguration(configChange => { + this.log.info('Configuration changed'); + if (configChange.affectsConfiguration("rubyTestExplorer.testFramework")) { + let newFramework = Config.getTestFramework(this.rootLog); + if (newFramework !== this.framework) { + // Config has changed to a different framework - recreate test loader and runner + this.config = newFramework == "rspec" + ? new RspecConfig(this.config.rubyScriptPath) + : new MinitestConfig(this.config.rubyScriptPath) + if (this.loader) { + this.disposeInstance(this.loader) + this.loader = null + } + if (this.runner) { + this.disposeInstance(this.runner) + this.runner = null + } + } + } + if (configChange.affectsConfiguration("rubyTestExplorer.logLevel")) { + this.rootLog.changeLevel( + vscode.workspace.getConfiguration('rubyTestExplorer', null).get('logLevel') as LogLevel + ) + } + }) + } + + /** + * Helper method to dispose of an object and remove it from the list of disposables + * + * @param instance the object to be disposed + */ + private disposeInstance(instance: vscode.Disposable) { + let index = this.disposables.indexOf(instance); + if (index !== -1) { + this.disposables.splice(index) + } + else { + this.log.debug("Factory instance not null but missing from disposables when configuration changed"); + } + instance.dispose() + } +} diff --git a/src/testLoader.ts b/src/testLoader.ts new file mode 100644 index 0000000..3b47382 --- /dev/null +++ b/src/testLoader.ts @@ -0,0 +1,184 @@ +import * as vscode from 'vscode'; +import path from 'path' +import { IChildLogger } from '@vscode-logging/logger'; +import { TestSuiteManager } from './testSuiteManager'; +import { LoaderQueue } from './loaderQueue'; +import { TestRunner } from './testRunner'; + +/** + * Responsible for finding and watching test files, and loading tests from within those + * files + * + * Defers to TestSuite for parsing test information + */ +export class TestLoader implements vscode.Disposable { + protected disposables: { dispose(): void }[] = []; + private readonly log: IChildLogger; + private readonly cancellationTokenSource = new vscode.CancellationTokenSource() + private readonly resolveQueue: LoaderQueue + private readonly runHandler: (request: vscode.TestRunRequest, token: vscode.CancellationToken) => Thenable + + constructor( + readonly rootLog: IChildLogger, + private readonly manager: TestSuiteManager, + private readonly testRunner: TestRunner + ) { + this.log = rootLog.getChildLogger({ label: 'TestLoader' }); + this.resolveQueue = new LoaderQueue(rootLog, async (testItems?: vscode.TestItem[]) => await this.loadTests(testItems)) + this.disposables.push(this.cancellationTokenSource) + this.disposables.push(this.resolveQueue) + this.disposables.push(this.configWatcher()); + this.runHandler = (request, token) => this.testRunner.runHandler(request, token) + } + + dispose(): void { + this.log.debug("Dispose called") + for (const disposable of this.disposables) { + try { + disposable.dispose(); + } catch (err) { + this.log.error("Error disposing object", disposable, err) + } + } + this.disposables = []; + } + + /** + * Create a file watcher that will update the test item tree when: + * - A test file is created + * - A test file is changed + * - A test file is deleted + * @param pattern Glob pattern to use for the file watcher + */ + createFileWatcher(pattern: vscode.GlobPattern): vscode.FileSystemWatcher { + const watcher = vscode.workspace.createFileSystemWatcher(pattern); + this.disposables.push(watcher) + + // When files are created, make sure there's a corresponding "file" node in the test item tree + watcher.onDidCreate(uri => { + let watcherLog = this.log.getChildLogger({label: 'onDidCreate watcher'}) + watcherLog.debug('File created: %s', uri.fsPath) + this.manager.getOrCreateTestItem(this.uriToTestId(uri)) + }) + // When files change, reload them + watcher.onDidChange(uri => { + let watcherLog = this.log.getChildLogger({label: 'onDidChange watcher'}) + watcherLog.debug('File changed, reloading tests: %s', uri.fsPath) + let testItem = this.manager.getTestItem(this.uriToTestId(uri)) + if (!testItem) { + watcherLog.error('Unable to find test item for file', uri) + } else { + this.resolveQueue.enqueue(testItem) + } + }); + // And, finally, delete TestItems for removed files + watcher.onDidDelete(uri => { + let watcherLog = this.log.getChildLogger({label: 'onDidDelete watcher'}) + watcherLog.debug('File deleted: %s', uri.fsPath) + this.manager.deleteTestItem(this.uriToTestId(uri)) + }); + + return watcher; + } + + /** + * Searches the configured test directory for test files, according to the configured glob patterns. + * + * For each pattern, a FileWatcher is created to notify the plugin of changes to files + * For each file found, a request to load tests from that file is enqueued + * + * Waits for all test files to be loaded from the queue before returning + */ + public async discoverAllFilesInWorkspace(): Promise { + let log = this.log.getChildLogger({ label: `${this.discoverAllFilesInWorkspace.name}` }) + let testDir = this.manager.config.getAbsoluteTestDirectory() + + let patterns: Array = [] + this.manager.config.getFilePattern().forEach(pattern => { + patterns.push(new vscode.RelativePattern(testDir, '**/' + pattern)) + }) + + log.debug('Setting up watchers with the following test patterns', patterns) + let resolveFilesPromises: Promise[] = [] + let fileWatchers = await Promise.all(patterns.map(async (pattern) => { + for (const file of await vscode.workspace.findFiles(pattern)) { + log.debug('Found file, creating TestItem', file) + this.manager.getOrCreateTestItem(this.uriToTestId(file)) + } + + // TODO - skip if filewatcher for this pattern exists and dispose filewatchers for patterns no longer in config + let fileWatcher = this.createFileWatcher(pattern) + return fileWatcher + })) + await Promise.all(resolveFilesPromises) + return fileWatchers + } + + /** + * Runs the test runner using the 'ResolveTests' profile to load test information. + * + * Only called from the queue + * + * @param testItems Array of test items to be loaded. If undefined, all tests and files are loaded + */ + private async loadTests(testItems?: vscode.TestItem[]): Promise { + let log = this.log.getChildLogger({label:'loadTests'}) + log.info('Loading tests...', { testIds: testItems?.map(x => x.id) || 'all tests' }); + try { + if (testItems) { for (const item of testItems) { item.busy = true }} + let request = new vscode.TestRunRequest(testItems) + await this.runHandler(request, this.cancellationTokenSource.token) + } catch (e: any) { + log.error('Failed to load tests', e) + return Promise.reject(e) + } finally { + if (testItems) { for (const item of testItems) { item.busy = false }} + } + } + + /** + * Enqueues a single test item to be loaded + * @param testItem the test item to be loaded + * @returns the loaded test item + */ + public async loadTestItem(testItem: vscode.TestItem): Promise { + return await this.resolveQueue.enqueue(testItem) + } + + private configWatcher(): vscode.Disposable { + let log = this.rootLog.getChildLogger({ label: 'TestLoader.configWatcher' }); + log.debug('configWatcher') + return vscode.workspace.onDidChangeConfiguration(configChange => { + this.log.info('Configuration changed'); + if (this.configChangeAffectsFileWatchers(configChange)) { + this.manager.controller.items.replace([]) + this.discoverAllFilesInWorkspace(); + } + }) + } + + private configChangeAffectsFileWatchers(configChange: vscode.ConfigurationChangeEvent): boolean { + return configChange.affectsConfiguration('rubyTestExplorer.filePattern') + || configChange.affectsConfiguration('rubyTestExplorer.testFramework') + || configChange.affectsConfiguration('rubyTestExplorer.rspecDirectory') + || configChange.affectsConfiguration('rubyTestExplorer.minitestDirectory') + } + + /** + * Converts a test URI into a test ID + * @param uri URI of test + * @returns test ID + */ + private uriToTestId(uri: string | vscode.Uri): string { + let log = this.log.getChildLogger({label: 'uriToTestId'}) + if (typeof uri === "string") { + log.debug("uri is string. Returning unchanged") + return uri + } + let fullTestDirPath = this.manager.config.getAbsoluteTestDirectory() + log.debug('Full path to test dir: %s', fullTestDirPath) + let strippedUri = uri.fsPath.replace(fullTestDirPath + path.sep, '') + log.debug('Stripped URI: %s', strippedUri) + return strippedUri + } +} diff --git a/src/testRunner.ts b/src/testRunner.ts new file mode 100644 index 0000000..d713d1e --- /dev/null +++ b/src/testRunner.ts @@ -0,0 +1,270 @@ +import * as vscode from 'vscode'; +import * as childProcess from 'child_process'; +import { IChildLogger } from '@vscode-logging/logger'; +import { __asyncDelegator } from 'tslib'; +import { TestStatusListener } from './testStatusListener'; +import { TestSuiteManager } from './testSuiteManager'; +import { FrameworkProcess } from './frameworkProcess'; + +export class TestRunner implements vscode.Disposable { + protected debugCommandStartedResolver?: () => void; + protected disposables: { dispose(): void }[] = []; + protected readonly log: IChildLogger; + private readonly testProcessMap: Map + + /** + * @param rootLog The Test Adapter logger, for logging. + * @param workspace Open workspace folder + * @param manager TestSuiteManager instance + */ + constructor( + readonly rootLog: IChildLogger, + protected manager: TestSuiteManager, + protected workspace?: vscode.WorkspaceFolder, + ) { + this.log = rootLog.getChildLogger({label: "TestRunner"}) + this.testProcessMap = new Map() + } + + public dispose() { + this.log.debug("Dispose called") + for (const disposable of this.disposables) { + try { + disposable.dispose(); + } catch (err) { + this.log.error('Error disposing object', err) + } + } + this.disposables = []; + } + + /** + * Helper method to dispose of an object and remove it from the list of disposables + * + * @param instance the object to be disposed + */ + private disposeInstance(instance: vscode.Disposable) { + let index = this.disposables.indexOf(instance); + if (index !== -1) { + this.disposables.splice(index) + } + else { + this.log.debug("Factory instance not null but missing from disposables when configuration changed"); + } + instance.dispose() + } + + /** + * Get the location of the test in the testing tree. + * + * Test ids are in the form of `/spec/model/game_spec.rb[1:1:1]`, and this + * function turns that into `111`. The number is used to order the tests + * in the explorer. + * + * @param test The test we want to get the location of. + * @return A number representing the location of the test in the test tree. + */ + protected getTestLocation(test: vscode.TestItem): number { + return parseInt(test.id.substring(test.id.indexOf("[") + 1, test.id.lastIndexOf("]")).split(':').join('')); + } + + /** + * Test run handler + * + * Called by VSC when a user requests a test run + * @param request Request containing tests to be run and tests to be excluded from the run + * @param token Cancellation token which will trigger when a user cancels a test run + * @param debuggerConfig VSC Debugger configuration if a debug run was requested, or `null` + */ + public async runHandler( + request: vscode.TestRunRequest, + token: vscode.CancellationToken, + debuggerConfig?: vscode.DebugConfiguration + ) { + let log = this.log.getChildLogger({ label: 'runHandler' }) + + // Loop through all included tests, or all known tests, and add them to our queue + log.debug('Number of tests in request', request.include?.length || 0); + + log.info('Creating test run', {request: request}); + const testRun = this.manager.controller.createTestRun(request) + + try { + log.trace("Included tests in request", request.include?.map(x => x.id)); + log.trace("Excluded tests in request", request.exclude?.map(x => x.id)); + let testsToRun = request.exclude ? request.include?.filter(x => !request.exclude!.includes(x)) : request.include + log.trace("Running tests", testsToRun?.map(x => x.id)); + + if (!request.profile) { + // Load tests + await this.runTestFramework( + { + command: this.manager.config.getResolveTestsCommand(), + args: this.manager.config.getTestArguments(testsToRun), + }, + testRun + ) + } else { + // Run tests + if (!testsToRun) { + log.debug("Running all tests") + this.manager.controller.items.forEach((item) => { + // Mark selected tests as started + this.enqueTestAndChildren(item, testRun) + }) + } else { + log.debug("Running selected tests") + // Mark selected tests as started + testsToRun.forEach(item => this.enqueTestAndChildren(item, testRun)) + } + let command = { + command: this.manager.config.getRunTestsCommand(debuggerConfig), + args: this.manager.config.getTestArguments(testsToRun) + } + if (debuggerConfig) { + log.debug('Debugging tests', request.include?.map(x => x.id)); + await Promise.all([ + this.startDebugSession(debuggerConfig, request.profile), + this.runTestFramework(command, testRun, request.profile) + ]) + } + else { + log.debug('Running test', request.include?.map(x => x.id)); + await this.runTestFramework(command, testRun, request.profile) + } + } + } + catch (err) { + log.error("Error running tests", err) + } + finally { + // Make sure to end the run after all tests have been executed: + log.debug('Ending test run'); + testRun.end(); + } + if (token.isCancellationRequested) { + log.info('Test run aborted due to cancellation - token passed into runHandler') + } else if (testRun.token.isCancellationRequested) { + log.info('Test run aborted due to cancellation - token from controller via TestRun') + } + } + + private async startDebugSession(debuggerConfig: vscode.DebugConfiguration, profile: vscode.TestRunProfile): Promise { + let log = this.log.getChildLogger({label: 'startDebugSession'}) + + if (this.workspace) { + log.error('Cannot debug without a folder opened') + return + } + + this.log.info('Starting the debug session'); + + const debugCommandStartedPromise = new Promise((resolve, _) => { + this.debugCommandStartedResolver = () => resolve() + }) + try { + let activeDebugSession: vscode.DebugSession + const debugStartSessionSubscription = vscode.debug.onDidStartDebugSession(debugSession => { + if (debugSession.name === debuggerConfig.name) { + log.info('Debug session started', debugSession.name); + activeDebugSession = debugSession + } + }) + try { + await Promise.race( + [ + // Wait for either timeout or for the child process to notify us that it has started + debugCommandStartedPromise, + new Promise((_, reject) => setTimeout(() => reject(new Error('Debug session failed to start within 5 seconds')), 5000)) + ] + ) + } finally { + debugStartSessionSubscription.dispose() + } + + const debugSessionStarted = await vscode.debug.startDebugging(this.workspace, debuggerConfig); + if (!debugSessionStarted) { + throw new Error('Debug session failed to start') + } + + const debugStopSubscription = vscode.debug.onDidTerminateDebugSession(session => { + if (session === activeDebugSession) { + log.info('Debug session ended', session.name); + this.killTestRun(profile) // terminate the test run + debugStopSubscription.dispose(); + } + }) + } catch (err) { + log.error('Error starting debug session', err) + this.killTestRun(profile) + } + } + + /** + * Mark a test node and all its children as being queued for execution + */ + private enqueTestAndChildren(test: vscode.TestItem, testRun: vscode.TestRun) { + // Tests will be marked as started as the runner gets to them + let log = this.log.getChildLogger({label: `${this.enqueTestAndChildren.name}`}) + log.debug('Enqueueing test item: %s', test.id) + testRun.enqueued(test); + if (test.children && test.children.size > 0) { + test.children.forEach(child => { this.enqueTestAndChildren(child, testRun) }) + } + } + + /** + * Spawns a child process to run a command, that will be killed + * if the cancellation token is triggered + * + * @param testCommand The command to run + * @param context Test run context for the cancellation token + * @returns Raw output from process + */ + private async runTestFramework (testCommand: { command: string, args: string[] }, testRun: vscode.TestRun, profile?: vscode.TestRunProfile): Promise { + const spawnArgs: childProcess.SpawnOptions = { + cwd: this.workspace?.uri.fsPath, + shell: true, + env: this.manager.config.getProcessEnv() + }; + + this.log.info('Running command: %s, args: [%s]', testCommand.command, testCommand.args.join(',')); + let testProfileKind = profile?.kind || null + + if (this.testProcessMap.get(testProfileKind)) { + this.log.warn('Test run already in progress for profile kind: %s', testProfileKind) + return + } + let testProcess = new FrameworkProcess(this.log, testCommand.command, spawnArgs, testRun.token, this.manager) + this.disposables.push(testProcess) + this.testProcessMap.set(testProfileKind, testProcess); + + const statusListener = TestStatusListener.listen( + this.rootLog, + testRun, + testProcess.testStatusEmitter, + profile + ) + this.disposables.push(statusListener) + try { + await testProcess.startProcess(testCommand.args, this.debugCommandStartedResolver) + } finally { + this.disposeInstance(statusListener) + this.disposeInstance(testProcess) + this.testProcessMap.delete(testProfileKind) + } + } + + /** + * Terminates the current test run process for the given profile kind if there is one + * @param profile The profile to kill the test run for + */ + private killTestRun(profile?: vscode.TestRunProfile) { + let profileKind = profile?.kind || null + let process = this.testProcessMap.get(profileKind) + if (process) { + this.disposeInstance(process) + this.testProcessMap.delete(profileKind) + } + } +} diff --git a/src/testStatus.ts b/src/testStatus.ts new file mode 100644 index 0000000..1e01d6d --- /dev/null +++ b/src/testStatus.ts @@ -0,0 +1,18 @@ +import * as vscode from 'vscode' + +export enum Status { + passed, + failed, + errored, + running, + skipped +} + +export class TestStatus { + constructor( + public readonly testItem: vscode.TestItem, + public readonly status: Status, + public readonly duration?: number, + public readonly message?: vscode.TestMessage, + ) {} +} \ No newline at end of file diff --git a/src/testStatusListener.ts b/src/testStatusListener.ts new file mode 100644 index 0000000..604ba8e --- /dev/null +++ b/src/testStatusListener.ts @@ -0,0 +1,56 @@ +import * as vscode from 'vscode' +import { IChildLogger } from '@vscode-logging/logger' +import { Status, TestStatus } from './testStatus' + +/** + * Updates the TestRun when test status events are received + */ +export class TestStatusListener { + public static listen( + rootLog: IChildLogger, + testRun: vscode.TestRun, + testStatusEmitter: vscode.EventEmitter, + profile?: vscode.TestRunProfile, + ): vscode.Disposable { + let log = rootLog.getChildLogger({ label: `${TestStatusListener.name}(${profile?.label || "LoadTests"})`}) + return testStatusEmitter.event((event: TestStatus) => { + + switch(event.status) { + case Status.skipped: + log.info('Test skipped: %s', event.testItem.id) + testRun.skipped(event.testItem) + break; + case Status.passed: + if (!profile) { + log.info('Test loaded: %s (duration: %d)', event.testItem.id, event.duration) + } else { + log.info('Test passed: %s (duration: %d)', event.testItem.id, event.duration) + testRun.passed(event.testItem, event.duration) + } + break; + case Status.errored: + log.info('Test errored: %s (duration: %d)', event.testItem.id, event.duration, event.message) + if (event.message) { + testRun.errored(event.testItem, event.message, event.duration) + } + break; + case Status.failed: + log.info('Test failed: %s (duration: %d)', event.testItem.id, event.duration, event.message) + if (event.message) { + testRun.failed(event.testItem, event.message, event.duration) + } + break; + case Status.running: + if (!profile) { + log.debug('Ignored test started event from test load: %s (duration: %d)', event.testItem.id, event.duration) + } else { + log.info('Test started: %s', event.testItem.id) + testRun.started(event.testItem) + } + break; + default: + log.warn('Unexpected status: %s', event.status) + } + }) + } +} diff --git a/src/testSuiteManager.ts b/src/testSuiteManager.ts new file mode 100644 index 0000000..4326e98 --- /dev/null +++ b/src/testSuiteManager.ts @@ -0,0 +1,213 @@ +import * as vscode from 'vscode' +import path from 'path' +import { IChildLogger } from '@vscode-logging/logger'; +import { Config } from './config'; + +export type TestItemCallback = (item: vscode.TestItem) => void + +/** + * Manages the contents and state of the test suite + * + * Responsible for creating, deleting and finding test items + */ +export class TestSuiteManager { + private readonly log: IChildLogger; + + constructor( + readonly rootLog: IChildLogger, + public readonly controller: vscode.TestController, + public readonly config: Config + ) { + this.log = rootLog.getChildLogger({label: `${TestSuiteManager.name}`}); + } + + public deleteTestItem(testId: string) { + let log = this.log.getChildLogger({label: 'deleteTestItem'}) + testId = this.normaliseTestId(testId) + log.debug('Deleting test: %s', testId) + let testItem = this.getTestItem(testId) + if (!testItem) { + log.error('No test item found with given ID: %s', testId) + return + } + let collection = testItem.parent ? testItem.parent.children : this.controller.items + if (collection) { + collection.delete(testId); + log.debug('Removed test: %s', testId) + } else { + log.error('Parent collection not found') + } + } + + /** + * Get the {@link vscode.TestItem} for a test ID + * @param testId Test ID to lookup + * @param onItemCreated Optional callback to be notified when test items are created + * @returns The test item for the ID + * @throws if test item could not be found + */ + public getOrCreateTestItem(testId: string, onItemCreated?: TestItemCallback): vscode.TestItem { + let log = this.log.getChildLogger({label: 'getOrCreateTestItem'}) + return this.getTestItemInternal(log, testId, true, onItemCreated)! + } + + /** + * Gets a TestItem from the list of tests + * @param testId ID of the TestItem to get + * @returns TestItem if found, else undefined + */ + public getTestItem(testId: string): vscode.TestItem | undefined { + let log = this.log.getChildLogger({label: 'getTestItem'}) + return this.getTestItemInternal(log, testId, false) + } + + /** + * Takes a test ID from the test runner output and normalises it to a consistent format + * + * - Removes leading './' if present + * - Removes leading test dir if present + */ + public normaliseTestId(testId: string): string { + let log = this.log.getChildLogger({label: `${this.normaliseTestId.name}`}) + if (testId.startsWith(`.${path.sep}`)) { + testId = testId.substring(2) + } + if (testId.startsWith(this.config.getRelativeTestDirectory())) { + testId = testId.replace(this.config.getRelativeTestDirectory(), '') + } + if (testId.startsWith(path.sep)) { + testId = testId.substring(1) + } + log.debug('Normalised ID: %s', testId) + return testId + } + + private testIdToUri(testId: string): vscode.Uri { + return vscode.Uri.file(path.resolve(this.config.getAbsoluteTestDirectory(), testId.replace(/\[.*\]/, ''))) + } + + /** + * Creates a TestItem and adds it to a TestItemCollection + * @param collection + * @param testId + * @param label + * @param uri + * @param canResolveChildren + * @returns + */ + private createTestItem( + testId: string, + label: string, + parent?: vscode.TestItem, + onItemCreated: TestItemCallback = (_) => {}, + canResolveChildren: boolean = true, + ): vscode.TestItem { + let log = this.log.getChildLogger({ label: `${this.createTestItem.name}` }) + let uri = this.testIdToUri(testId) + log.debug('Creating test item', {label: label, parentId: parent?.id, canResolveChildren: canResolveChildren, uri: uri}) + let item = this.controller.createTestItem(testId, label, uri) + item.canResolveChildren = canResolveChildren; + (parent?.children || this.controller.items).add(item); + log.debug('Added test: %s', item.id) + onItemCreated(item) + return item + } + + /** + * Splits a test ID into an array of all parent IDs to reach the given ID from the test tree root + * @param testId test ID to split + * @returns array of test IDs + */ + private getParentIdsFromId(testId: string): string[] { + let log = this.log.getChildLogger({label: `${this.getParentIdsFromId.name}`}) + testId = this.normaliseTestId(testId) + + // Split path segments + let originalSegments = testId.split(path.sep) + log.debug('originalSegments', originalSegments) + if (originalSegments[0] === "") { + originalSegments.splice(0, 1) + } + + let idSegments = [...originalSegments] + for (let i = 1; i < idSegments.length - 1; i++) { + let precedingSegments = originalSegments.slice(0, i + 1) + idSegments[i] = path.join(...precedingSegments) + } + + // Split location + const match = idSegments.at(-1)?.match(/(?[^\[]*)(?:\[(?[0-9:]+)\])?/) + if (match && match.groups) { + // Get file ID (with path to it if there is one) + let fileId = match.groups["fileId"] + if (idSegments.length > 1) { + fileId = path.join(idSegments.at(-2)!, fileId) + } + // Add file ID to array + idSegments.splice(-1, 1, fileId) + + if (match.groups["location"]) { + let locations = match.groups["location"].split(':') + if (locations.length == 1) { + // Insert ID for minitest location + let contextId = `${fileId}[${locations[0]}]` + idSegments.push(contextId) + } else { + // Insert IDs for each nested RSpec context if there are any + for (let i = 1; i < locations.length; i++) { + let contextId = `${fileId}[${locations.slice(0, i + 1).join(':')}]` + idSegments.push(contextId) + } + } + } + } + log.trace('Final ID segments list', idSegments) + return idSegments + } + + private getTestItemInternal( + log: IChildLogger, + testId: string, + createIfMissing: boolean, + onItemCreated?: TestItemCallback + ): vscode.TestItem | undefined { + testId = this.normaliseTestId(testId) + + log.debug('Looking for test: %s', testId) + let parentIds = this.getParentIdsFromId(testId) + let item: vscode.TestItem | undefined = undefined + let itemCollection: vscode.TestItemCollection = this.controller.items + + // Walk through test folders to find the collection containing our test file, + // creating parent items as needed + for (const id of parentIds) { + log.debug('Getting item %s from parent collection %s', id, item?.id || 'controller') + let child = itemCollection.get(id) + if (!child) { + if (createIfMissing) { + child = this.createTestItem( + id, + id.substring(id.lastIndexOf(path.sep) + 1), // Temporary label + item, + onItemCreated, + this.canResolveChildren(id, testId) // Only the test ID will be a test case. All parents need this set to true + ) + } else { + return undefined + } + } + item = child + itemCollection = child.children + } + + return item + } + + private canResolveChildren(itemId: string, testId: string): boolean { + if (itemId.endsWith(']')) { + return itemId !== testId + } else { + return true + } + } +} diff --git a/src/tests.ts b/src/tests.ts deleted file mode 100644 index ce29bd3..0000000 --- a/src/tests.ts +++ /dev/null @@ -1,554 +0,0 @@ -import * as vscode from 'vscode'; -import { TestSuiteInfo, TestInfo, TestRunStartedEvent, TestRunFinishedEvent, TestSuiteEvent, TestEvent } from 'vscode-test-adapter-api'; -import * as childProcess from 'child_process'; -import * as split2 from 'split2'; -import { Log } from 'vscode-test-adapter-util'; - -export abstract class Tests { - protected context: vscode.ExtensionContext; - protected testStatesEmitter: vscode.EventEmitter; - protected currentChildProcess: childProcess.ChildProcess | undefined; - protected log: Log; - protected testSuite: TestSuiteInfo | undefined; - protected workspace: vscode.WorkspaceFolder; - abstract testFrameworkName: string; - protected debugCommandStartedResolver: Function | undefined; - - /** - * @param context Extension context provided by vscode. - * @param testStatesEmitter An emitter for the test suite's state. - * @param log The Test Adapter logger, for logging. - */ - constructor( - context: vscode.ExtensionContext, - testStatesEmitter: vscode.EventEmitter, - log: Log, - workspace: vscode.WorkspaceFolder - ) { - this.context = context; - this.testStatesEmitter = testStatesEmitter; - this.log = log; - this.workspace = workspace; - } - - abstract tests: () => Promise; - - abstract initTests: () => Promise; - - /** - * Takes the output from initTests() and parses the resulting - * JSON into a TestSuiteInfo object. - * - * @return The full test suite. - */ - public async loadTests(): Promise { - let output = await this.initTests(); - this.log.debug('Passing raw output from dry-run into getJsonFromOutput.'); - this.log.debug(`${output}`); - output = Tests.getJsonFromOutput(output); - this.log.debug('Parsing the below JSON:'); - this.log.debug(`${output}`); - let testMetadata; - try { - testMetadata = JSON.parse(output); - } catch (error) { - this.log.error(`JSON parsing failed: ${error}`); - } - - let tests: Array<{ id: string; full_description: string; description: string; file_path: string; line_number: number; location: number; }> = []; - - testMetadata.examples.forEach((test: { id: string; full_description: string; description: string; file_path: string; line_number: number; location: number; }) => { - let test_location_array: Array = test.id.substring(test.id.indexOf("[") + 1, test.id.lastIndexOf("]")).split(':'); - let test_location_string: string = test_location_array.join(''); - test.location = parseInt(test_location_string); - tests.push(test); - }); - - let testSuite: TestSuiteInfo = await this.getBaseTestSuite(tests); - - // Sort the children of each test suite based on their location in the test tree. - (testSuite.children as Array).forEach((suite: TestSuiteInfo) => { - // NOTE: This will only sort correctly if everything is nested at the same - // level, e.g. 111, 112, 121, etc. Once a fourth level of indentation is - // introduced, the location is generated as e.g. 1231, which won't - // sort properly relative to everything else. - (suite.children as Array).sort((a: TestInfo, b: TestInfo) => { - if ((a as TestInfo).type === "test" && (b as TestInfo).type === "test") { - let aLocation: number = this.getTestLocation(a as TestInfo); - let bLocation: number = this.getTestLocation(b as TestInfo); - return aLocation - bLocation; - } else { - return 0; - } - }) - }); - - this.testSuite = testSuite; - - return Promise.resolve(testSuite); - } - - /** - * Kills the current child process if one exists. - */ - public killChild(): void { - if (this.currentChildProcess) { - this.currentChildProcess.kill(); - } - } - - /** - * Get the user-configured test file pattern. - * - * @return The file pattern - */ - getFilePattern(): Array { - let pattern: Array = (vscode.workspace.getConfiguration('rubyTestExplorer', null).get('filePattern') as Array); - return pattern || ['*_test.rb', 'test_*.rb']; - } - - /** - * Get the user-configured test directory, if there is one. - * - * @return The test directory - */ - abstract getTestDirectory(): string; - - /** - * Pull JSON out of the test framework output. - * - * RSpec and Minitest frequently return bad data even when they're told to - * format the output as JSON, e.g. due to code coverage messages and other - * injections from gems. This gets the JSON by searching for - * `START_OF_TEST_JSON` and an opening curly brace, as well as a closing - * curly brace and `END_OF_TEST_JSON`. These are output by the custom - * RSpec formatter or Minitest Rake task as part of the final JSON output. - * - * @param output The output returned by running a command. - * @return A string representation of the JSON found in the output. - */ - static getJsonFromOutput(output: string): string { - output = output.substring(output.indexOf('START_OF_TEST_JSON{'), output.lastIndexOf('}END_OF_TEST_JSON') + 1); - // Get rid of the `START_OF_TEST_JSON` and `END_OF_TEST_JSON` to verify that the JSON is valid. - return output.substring(output.indexOf("{"), output.lastIndexOf("}") + 1); - } - - /** - * Get the location of the test in the testing tree. - * - * Test ids are in the form of `/spec/model/game_spec.rb[1:1:1]`, and this - * function turns that into `111`. The number is used to order the tests - * in the explorer. - * - * @param test The test we want to get the location of. - * @return A number representing the location of the test in the test tree. - */ - protected getTestLocation(test: TestInfo): number { - return parseInt(test.id.substring(test.id.indexOf("[") + 1, test.id.lastIndexOf("]")).split(':').join('')); - } - - /** - * Convert a string from snake_case to PascalCase. - * Note that the function will return the input string unchanged if it - * includes a '/'. - * - * @param string The string to convert to PascalCase. - * @return The converted string. - */ - protected snakeToPascalCase(string: string): string { - if (string.includes('/')) { return string } - return string.split("_").map(substr => substr.charAt(0).toUpperCase() + substr.slice(1)).join(""); - } - - /** - * Sorts an array of TestSuiteInfo objects by label. - * - * @param testSuiteChildren An array of TestSuiteInfo objects, generally the children of another TestSuiteInfo object. - * @return The input array, sorted by label. - */ - protected sortTestSuiteChildren(testSuiteChildren: Array): Array { - testSuiteChildren = testSuiteChildren.sort((a: TestSuiteInfo, b: TestSuiteInfo) => { - let comparison = 0; - if (a.label > b.label) { - comparison = 1; - } else if (a.label < b.label) { - comparison = -1; - } - return comparison; - }); - - return testSuiteChildren; - } - - /** - * Get the tests in a given file. - */ - public getTestSuiteForFile( - { tests, currentFile, directory }: { - tests: Array<{ - id: string; - full_description: string; - description: string; - file_path: string; - line_number: number; - location: number; - }>; currentFile: string; directory?: string; - }): TestSuiteInfo { - let currentFileTests = tests.filter(test => { - return test.file_path === currentFile - }); - - let currentFileTestsInfo = currentFileTests as unknown as Array; - currentFileTestsInfo.forEach((test: TestInfo) => { - test.type = 'test'; - test.label = ''; - }); - - let currentFileLabel = ''; - - if (directory) { - currentFileLabel = currentFile.replace(`${this.getTestDirectory()}${directory}/`, ''); - } else { - currentFileLabel = currentFile.replace(`${this.getTestDirectory()}`, ''); - } - - let pascalCurrentFileLabel = this.snakeToPascalCase(currentFileLabel.replace('_spec.rb', '')); - - let currentFileTestInfoArray: Array = currentFileTests.map((test) => { - // Concatenation of "/Users/username/whatever/project_dir" and "./spec/path/here.rb", - // but with the latter's first character stripped. - let filePath: string = `${this.workspace.uri.fsPath}${test.file_path.substr(1)}`; - - // RSpec provides test ids like "file_name.rb[1:2:3]". - // This uses the digits at the end of the id to create - // an array of numbers representing the location of the - // test in the file. - let testLocationArray: Array = test.id.substring(test.id.indexOf("[") + 1, test.id.lastIndexOf("]")).split(':').map((x) => { - return parseInt(x); - }); - - // Get the last element in the location array. - let testNumber: number = testLocationArray[testLocationArray.length - 1]; - // If the test doesn't have a name (because it uses the 'it do' syntax), "test #n" - // is appended to the test description to distinguish between separate tests. - let description: string = test.description.startsWith('example at ') ? `${test.full_description}test #${testNumber}` : test.full_description; - - // If the current file label doesn't have a slash in it and it starts with the PascalCase'd - // file name, remove the from the start of the description. This turns, e.g. - // `ExternalAccount Validations blah blah blah' into 'Validations blah blah blah'. - if (!pascalCurrentFileLabel.includes('/') && description.startsWith(pascalCurrentFileLabel)) { - // Optional check for a space following the PascalCase file name. In some - // cases, e.g. 'FileName#method_name` there's no space after the file name. - let regexString = `${pascalCurrentFileLabel}[ ]?`; - let regex = new RegExp(regexString, "g"); - description = description.replace(regex, ''); - } - - let testInfo: TestInfo = { - type: 'test', - id: test.id, - label: description, - file: filePath, - // Line numbers are 0-indexed - line: test.line_number - 1 - } - - return testInfo; - }); - - let currentFileAsAbsolutePath = `${this.workspace.uri.fsPath}${currentFile.substr(1)}`; - - let currentFileTestSuite: TestSuiteInfo = { - type: 'suite', - id: currentFile, - label: currentFileLabel, - file: currentFileAsAbsolutePath, - children: currentFileTestInfoArray - } - - return currentFileTestSuite; - } - - /** - * Create the base test suite with a root node and one layer of child nodes - * representing the subdirectories of spec/, and then any files under the - * given subdirectory. - * - * @param tests Test objects returned by our custom RSpec formatter or Minitest Rake task. - * @return The test suite root with its children. - */ - public async getBaseTestSuite( - tests: any[] - ): Promise { - let rootTestSuite: TestSuiteInfo = { - type: 'suite', - id: 'root', - label: `${this.workspace.name} ${this.testFrameworkName}`, - children: [] - }; - - // Create an array of all test files and then abuse Sets to make it unique. - let uniqueFiles = [...new Set(tests.map((test: { file_path: string; }) => test.file_path))]; - - let splitFilesArray: Array = []; - - // Remove the spec/ directory from all the file path. - uniqueFiles.forEach((file) => { - splitFilesArray.push(file.replace(`${this.getTestDirectory()}`, "").split('/')); - }); - - // This gets the main types of tests, e.g. features, helpers, models, requests, etc. - let subdirectories: Array = []; - splitFilesArray.forEach((splitFile) => { - if (splitFile.length > 1) { - subdirectories.push(splitFile[0]); - } - }); - subdirectories = [...new Set(subdirectories)]; - - // A nested loop to iterate through the direct subdirectories of spec/ and then - // organize the files under those subdirectories. - subdirectories.forEach((directory) => { - let filesInDirectory: Array = []; - - let uniqueFilesInDirectory: Array = uniqueFiles.filter((file) => { - return file.startsWith(`${this.getTestDirectory()}${directory}/`); - }); - - // Get the sets of tests for each file in the current directory. - uniqueFilesInDirectory.forEach((currentFile: string) => { - let currentFileTestSuite = this.getTestSuiteForFile({ tests, currentFile, directory }); - filesInDirectory.push(currentFileTestSuite); - }); - - let directoryTestSuite: TestSuiteInfo = { - type: 'suite', - id: directory, - label: directory, - children: filesInDirectory - }; - - rootTestSuite.children.push(directoryTestSuite); - }); - - // Sort test suite types alphabetically. - rootTestSuite.children = this.sortTestSuiteChildren(rootTestSuite.children as Array); - - // Get files that are direct descendants of the spec/ directory. - let topDirectoryFiles = uniqueFiles.filter((filePath) => { - return filePath.replace(`${this.getTestDirectory()}`, "").split('/').length === 1; - }); - - topDirectoryFiles.forEach((currentFile) => { - let currentFileTestSuite = this.getTestSuiteForFile({ tests, currentFile }); - rootTestSuite.children.push(currentFileTestSuite); - }); - - return rootTestSuite; - } - - /** - * Assigns the process to currentChildProcess and handles its output and what happens when it exits. - * - * @param process A process running the tests. - * @return A promise that resolves when the test run completes. - */ - handleChildProcess = async (process: childProcess.ChildProcess) => new Promise((resolve, reject) => { - this.currentChildProcess = process; - - this.currentChildProcess.on('exit', () => { - this.log.info('Child process has exited. Sending test run finish event.'); - this.currentChildProcess = undefined; - this.testStatesEmitter.fire({ type: 'finished' }); - resolve('{}'); - }); - - this.currentChildProcess.stderr!.pipe(split2()).on('data', (data) => { - data = data.toString(); - this.log.debug(`[CHILD PROCESS OUTPUT] ${data}`); - if (data.startsWith('Fast Debugger') && this.debugCommandStartedResolver) { - this.debugCommandStartedResolver() - } - }); - - this.currentChildProcess.stdout!.pipe(split2()).on('data', (data) => { - data = data.toString(); - this.log.debug(`[CHILD PROCESS OUTPUT] ${data}`); - if (data.startsWith('PASSED:')) { - data = data.replace('PASSED: ', ''); - this.testStatesEmitter.fire({ type: 'test', test: data, state: 'passed' }); - } else if (data.startsWith('FAILED:')) { - data = data.replace('FAILED: ', ''); - this.testStatesEmitter.fire({ type: 'test', test: data, state: 'failed' }); - } else if (data.startsWith('RUNNING:')) { - data = data.replace('RUNNING: ', ''); - this.testStatesEmitter.fire({ type: 'test', test: data, state: 'running' }); - } else if (data.startsWith('PENDING:')) { - data = data.replace('PENDING: ', ''); - this.testStatesEmitter.fire({ type: 'test', test: data, state: 'skipped' }); - } - if (data.includes('START_OF_TEST_JSON')) { - resolve(data); - } - }); - }); - - /** - * Runs the test suite by iterating through each test and running it. - * - * @param tests - * @param debuggerConfig A VS Code debugger configuration. - */ - runTests = async (tests: string[], debuggerConfig?: vscode.DebugConfiguration): Promise => { - let testSuite: TestSuiteInfo = await this.tests(); - - for (const suiteOrTestId of tests) { - const node = this.findNode(testSuite, suiteOrTestId); - if (node) { - await this.runNode(node, debuggerConfig); - } - } - } - - /** - * Recursively search for a node in the test suite list. - * - * @param searchNode The test or test suite to search in. - * @param id The id of the test or test suite. - */ - protected findNode(searchNode: TestSuiteInfo | TestInfo, id: string): TestSuiteInfo | TestInfo | undefined { - if (searchNode.id === id) { - return searchNode; - } else if (searchNode.type === 'suite') { - for (const child of searchNode.children) { - const found = this.findNode(child, id); - if (found) return found; - } - } - return undefined; - } - - /** - * Recursively run a node or its children. - * - * @param node A test or test suite. - * @param debuggerConfig A VS Code debugger configuration. - */ - protected async runNode(node: TestSuiteInfo | TestInfo, debuggerConfig?: vscode.DebugConfiguration): Promise { - // Special case handling for the root suite, since it can be run - // with runFullTestSuite() - if (node.type === 'suite' && node.id === 'root') { - this.testStatesEmitter.fire({ type: 'test', test: node.id, state: 'running' }); - - let testOutput = await this.runFullTestSuite(debuggerConfig); - testOutput = Tests.getJsonFromOutput(testOutput); - this.log.debug('Parsing the below JSON:'); - this.log.debug(`${testOutput}`); - let testMetadata = JSON.parse(testOutput); - let tests: Array = testMetadata.examples; - - if (tests && tests.length > 0) { - tests.forEach((test: { id: string | TestInfo; }) => { - this.handleStatus(test); - }); - } - - this.testStatesEmitter.fire({ type: 'suite', suite: node.id, state: 'completed' }); - // If the suite is a file, run the tests as a file rather than as separate tests. - } else if (node.type === 'suite' && node.label.endsWith('.rb')) { - this.testStatesEmitter.fire({ type: 'suite', suite: node.id, state: 'running' }); - - let testOutput = await this.runTestFile(`${node.file}`, debuggerConfig); - - testOutput = Tests.getJsonFromOutput(testOutput); - this.log.debug('Parsing the below JSON:'); - this.log.debug(`${testOutput}`); - let testMetadata = JSON.parse(testOutput); - let tests: Array = testMetadata.examples; - - if (tests && tests.length > 0) { - tests.forEach((test: { id: string | TestInfo; }) => { - this.handleStatus(test); - }); - } - - this.testStatesEmitter.fire({ type: 'suite', suite: node.id, state: 'completed' }); - - } else if (node.type === 'suite') { - - this.testStatesEmitter.fire({ type: 'suite', suite: node.id, state: 'running' }); - - for (const child of node.children) { - await this.runNode(child, debuggerConfig); - } - - this.testStatesEmitter.fire({ type: 'suite', suite: node.id, state: 'completed' }); - - } else if (node.type === 'test') { - if (node.file !== undefined && node.line !== undefined) { - this.testStatesEmitter.fire({ type: 'test', test: node.id, state: 'running' }); - - // Run the test at the given line, add one since the line is 0-indexed in - // VS Code and 1-indexed for RSpec/Minitest. - let testOutput = await this.runSingleTest(`${node.file}:${node.line + 1}`, debuggerConfig); - - testOutput = Tests.getJsonFromOutput(testOutput); - this.log.debug('Parsing the below JSON:'); - this.log.debug(`${testOutput}`); - let testMetadata = JSON.parse(testOutput); - let currentTest = testMetadata.examples[0]; - - this.handleStatus(currentTest); - } - } - } - - public async debugCommandStarted(): Promise { - return new Promise(async (resolve, reject) => { - this.debugCommandStartedResolver = resolve; - setTimeout(() => { reject("debugCommandStarted timed out") }, 10000) - }) - } - - /** - * Get the absolute path of the custom_formatter.rb file. - * - * @return The spec directory - */ - protected getRubyScriptsLocation(): string { - return this.context.asAbsolutePath('./ruby'); - } - - /** - * Runs a single test. - * - * @param testLocation A file path with a line number, e.g. `/path/to/test.rb:12`. - * @param debuggerConfig A VS Code debugger configuration. - * @return The raw output from running the test. - */ - abstract runSingleTest: (testLocation: string, debuggerConfig?: vscode.DebugConfiguration) => Promise; - - /** - * Runs tests in a given file. - * - * @param testFile The test file's file path, e.g. `/path/to/test.rb`. - * @param debuggerConfig A VS Code debugger configuration. - * @return The raw output from running the tests. - */ - abstract runTestFile: (testFile: string, debuggerConfig?: vscode.DebugConfiguration) => Promise; - - /** - * Runs the full test suite for the current workspace. - * - * @param debuggerConfig A VS Code debugger configuration. - * @return The raw output from running the test suite. - */ - abstract runFullTestSuite: (debuggerConfig?: vscode.DebugConfiguration) => Promise; - - /** - * Handles test state based on the output returned by the test command. - * - * @param test The test that we want to handle. - */ - abstract handleStatus(test: any): void; -} diff --git a/test/fixtures/minitest/.vscode/settings.json b/test/fixtures/minitest/.vscode/settings.json index ca07cde..0fcb80c 100644 --- a/test/fixtures/minitest/.vscode/settings.json +++ b/test/fixtures/minitest/.vscode/settings.json @@ -1,5 +1,9 @@ { "rubyTestExplorer": { "testFramework": "minitest" - } + }, + "rubyTestExplorer.minitestDirectory": "test", + "rubyTestExplorer.filePattern": [ + "*_test.rb" + ] } diff --git a/test/fixtures/minitest/test/square_test.rb b/test/fixtures/minitest/test/square/square_test.rb similarity index 85% rename from test/fixtures/minitest/test/square_test.rb rename to test/fixtures/minitest/test/square/square_test.rb index c41a458..62a712a 100644 --- a/test/fixtures/minitest/test/square_test.rb +++ b/test/fixtures/minitest/test/square/square_test.rb @@ -1,4 +1,4 @@ -require_relative "test_helper" +require "test_helper" class SquareTest < Minitest::Test def test_square_2 diff --git a/test/fixtures/rspec/.vscode/settings.json b/test/fixtures/rspec/.vscode/settings.json index 8806713..b79f474 100644 --- a/test/fixtures/rspec/.vscode/settings.json +++ b/test/fixtures/rspec/.vscode/settings.json @@ -1,5 +1,9 @@ { "rubyTestExplorer": { "testFramework": "rspec" - } + }, + "rubyTestExplorer.rspecDirectory": "spec", + "rubyTestExplorer.filePattern": [ + "*_spec.rb" + ] } diff --git a/test/fixtures/rspec/spec/contexts_spec.rb b/test/fixtures/rspec/spec/contexts_spec.rb new file mode 100644 index 0000000..0d12b34 --- /dev/null +++ b/test/fixtures/rspec/spec/contexts_spec.rb @@ -0,0 +1,31 @@ +# frozen_string_literal: true + +require 'test_helper' + +describe 'Contexts' do + context 'when' do + context 'there' do + context 'are' do + context 'many' do + context 'levels' do + context 'of' do + context 'nested' do + context 'contexts' do + it "doesn't break the extension" do + expect('Hello text explorer!').to be_a(String) + end + end + end + end + end + end + + context 'fewer levels of nested contexts' do + it do + expect('Hello again text explorer!').to be_a(String) + end + end + end + end + end +end diff --git a/test/fixtures/rspec/spec/square_spec.rb b/test/fixtures/rspec/spec/square/square_spec.rb similarity index 100% rename from test/fixtures/rspec/spec/square_spec.rb rename to test/fixtures/rspec/spec/square/square_spec.rb diff --git a/test/fixtures/unitTests/README.md b/test/fixtures/unitTests/README.md new file mode 100644 index 0000000..632d8b2 --- /dev/null +++ b/test/fixtures/unitTests/README.md @@ -0,0 +1 @@ +# This folder is only here to be a workspace for the VSC instance running unit tests to open diff --git a/test/fixtures/unitTests/minitest/dryRunOutput.json b/test/fixtures/unitTests/minitest/dryRunOutput.json new file mode 100644 index 0000000..df04ab6 --- /dev/null +++ b/test/fixtures/unitTests/minitest/dryRunOutput.json @@ -0,0 +1,60 @@ +{ + "version": "5.14.4", + "examples": [ + { + "description": "abs positive", + "full_description": "abs positive", + "file_path": "./test/abs_test.rb", + "full_path": "/home/tabby/git/vscode-ruby-test-adapter/test/fixtures/minitest/test/abs_test.rb", + "line_number": 4, + "klass": "AbsTest", + "method": "test_abs_positive", + "runnable": "AbsTest", + "id": "./test/abs_test.rb[4]" + }, + { + "description": "abs 0", + "full_description": "abs 0", + "file_path": "./test/abs_test.rb", + "full_path": "/home/tabby/git/vscode-ruby-test-adapter/test/fixtures/minitest/test/abs_test.rb", + "line_number": 8, + "klass": "AbsTest", + "method": "test_abs_0", + "runnable": "AbsTest", + "id": "./test/abs_test.rb[8]" + }, + { + "description": "abs negative", + "full_description": "abs negative", + "file_path": "./test/abs_test.rb", + "full_path": "/home/tabby/git/vscode-ruby-test-adapter/test/fixtures/minitest/test/abs_test.rb", + "line_number": 12, + "klass": "AbsTest", + "method": "test_abs_negative", + "runnable": "AbsTest", + "id": "./test/abs_test.rb[12]" + }, + { + "description": "square 2", + "full_description": "square 2", + "file_path": "./test/square/square_test.rb", + "full_path": "/home/tabby/git/vscode-ruby-test-adapter/test/fixtures/minitest/test/square/square_test.rb", + "line_number": 4, + "klass": "SquareTest", + "method": "test_square_2", + "runnable": "SquareTest", + "id": "./test/square/square_test.rb[4]" + }, + { + "description": "square 3", + "full_description": "square 3", + "file_path": "./test/square/square_test.rb", + "full_path": "/home/tabby/git/vscode-ruby-test-adapter/test/fixtures/minitest/test/square/square_test.rb", + "line_number": 8, + "klass": "SquareTest", + "method": "test_square_3", + "runnable": "SquareTest", + "id": "./test/square/square_test.rb[8]" + } + ] +} \ No newline at end of file diff --git a/test/fixtures/unitTests/minitest/testRunOutput.json b/test/fixtures/unitTests/minitest/testRunOutput.json new file mode 100644 index 0000000..f0009862d --- /dev/null +++ b/test/fixtures/unitTests/minitest/testRunOutput.json @@ -0,0 +1,217 @@ +{ + "version": "5.14.4", + "summary": { + "duration": 0.05, + "example_count": 3, + "failure_count": 1, + "pending_count": 1, + "errors_outside_of_examples_count": 1 + }, + "summary_line": "Total time: 0.05, Runs: 5, Assertions: 3, Failures: 1, Errors: 1, Skips: 1", + "examples": [ + { + "description": "abs 0", + "full_description": "abs 0", + "file_path": "./test/abs_test.rb", + "full_path": "/home/tabby/git/vscode-ruby-test-adapter/test/fixtures/minitest/test/abs_test.rb", + "line_number": 8, + "klass": "AbsTest", + "method": "test_abs_0", + "runnable": "AbsTest", + "id": "./test/abs_test.rb[8]", + "status": "failed", + "pending_message": null, + "exception": { + "class": "Minitest::UnexpectedError", + "message": "RuntimeError: Abs for zero is not supported\n /home/tabby/git/vscode-ruby-test-adapter/test/fixtures/minitest/lib/abs.rb:7:in `apply'\n /home/tabby/git/vscode-ruby-test-adapter/test/fixtures/minitest/test/abs_test.rb:9:in `test_abs_0'", + "backtrace": [ + "lib/abs.rb:7:in `apply'", + "test/abs_test.rb:9:in `test_abs_0'" + ], + "full_backtrace": [ + "/home/tabby/git/vscode-ruby-test-adapter/test/fixtures/minitest/lib/abs.rb:7:in `apply'", + "/home/tabby/git/vscode-ruby-test-adapter/test/fixtures/minitest/test/abs_test.rb:9:in `test_abs_0'", + "/home/tabby/.rbenv/versions/3.1.0/lib/ruby/gems/3.1.0/gems/minitest-5.14.4/lib/minitest/test.rb:98:in `block (3 levels) in run'", + "/home/tabby/.rbenv/versions/3.1.0/lib/ruby/gems/3.1.0/gems/minitest-5.14.4/lib/minitest/test.rb:195:in `capture_exceptions'", + "/home/tabby/.rbenv/versions/3.1.0/lib/ruby/gems/3.1.0/gems/minitest-5.14.4/lib/minitest/test.rb:95:in `block (2 levels) in run'", + "/home/tabby/.rbenv/versions/3.1.0/lib/ruby/gems/3.1.0/gems/minitest-5.14.4/lib/minitest.rb:272:in `time_it'", + "/home/tabby/.rbenv/versions/3.1.0/lib/ruby/gems/3.1.0/gems/minitest-5.14.4/lib/minitest/test.rb:94:in `block in run'", + "/home/tabby/.rbenv/versions/3.1.0/lib/ruby/gems/3.1.0/gems/minitest-5.14.4/lib/minitest.rb:367:in `on_signal'", + "/home/tabby/.rbenv/versions/3.1.0/lib/ruby/gems/3.1.0/gems/minitest-5.14.4/lib/minitest/test.rb:211:in `with_info_handler'", + "/home/tabby/.rbenv/versions/3.1.0/lib/ruby/gems/3.1.0/gems/minitest-5.14.4/lib/minitest/test.rb:93:in `run'", + "/home/tabby/.rbenv/versions/3.1.0/lib/ruby/gems/3.1.0/gems/minitest-5.14.4/lib/minitest.rb:1029:in `run_one_method'", + "/home/tabby/.rbenv/versions/3.1.0/lib/ruby/gems/3.1.0/gems/minitest-5.14.4/lib/minitest.rb:341:in `run_one_method'", + "/home/tabby/.rbenv/versions/3.1.0/lib/ruby/gems/3.1.0/gems/minitest-5.14.4/lib/minitest.rb:328:in `block (2 levels) in run'", + "/home/tabby/.rbenv/versions/3.1.0/lib/ruby/gems/3.1.0/gems/minitest-5.14.4/lib/minitest.rb:327:in `each'", + "/home/tabby/.rbenv/versions/3.1.0/lib/ruby/gems/3.1.0/gems/minitest-5.14.4/lib/minitest.rb:327:in `block in run'", + "/home/tabby/.rbenv/versions/3.1.0/lib/ruby/gems/3.1.0/gems/minitest-5.14.4/lib/minitest.rb:367:in `on_signal'", + "/home/tabby/.rbenv/versions/3.1.0/lib/ruby/gems/3.1.0/gems/minitest-5.14.4/lib/minitest.rb:354:in `with_info_handler'", + "/home/tabby/.rbenv/versions/3.1.0/lib/ruby/gems/3.1.0/gems/minitest-5.14.4/lib/minitest.rb:326:in `run'", + "/home/tabby/git/vscode-ruby-test-adapter/ruby/vscode/minitest/runner.rb:40:in `block in run'", + "/home/tabby/git/vscode-ruby-test-adapter/ruby/vscode/minitest/runner.rb:39:in `each'", + "/home/tabby/git/vscode-ruby-test-adapter/ruby/vscode/minitest/runner.rb:39:in `run'", + "/home/tabby/git/vscode-ruby-test-adapter/ruby/vscode/minitest.rb:36:in `run'", + "/home/tabby/git/vscode-ruby-test-adapter/ruby/vscode.rake:14:in `block (3 levels) in '", + "/home/tabby/.rbenv/versions/3.1.0/lib/ruby/gems/3.1.0/gems/rake-13.0.3/lib/rake/task.rb:281:in `block in execute'", + "/home/tabby/.rbenv/versions/3.1.0/lib/ruby/gems/3.1.0/gems/rake-13.0.3/lib/rake/task.rb:281:in `each'", + "/home/tabby/.rbenv/versions/3.1.0/lib/ruby/gems/3.1.0/gems/rake-13.0.3/lib/rake/task.rb:281:in `execute'", + "/home/tabby/.rbenv/versions/3.1.0/lib/ruby/gems/3.1.0/gems/rake-13.0.3/lib/rake/task.rb:219:in `block in invoke_with_call_chain'", + "/home/tabby/.rbenv/versions/3.1.0/lib/ruby/gems/3.1.0/gems/rake-13.0.3/lib/rake/task.rb:199:in `synchronize'", + "/home/tabby/.rbenv/versions/3.1.0/lib/ruby/gems/3.1.0/gems/rake-13.0.3/lib/rake/task.rb:199:in `invoke_with_call_chain'", + "/home/tabby/.rbenv/versions/3.1.0/lib/ruby/gems/3.1.0/gems/rake-13.0.3/lib/rake/task.rb:188:in `invoke'", + "/home/tabby/.rbenv/versions/3.1.0/lib/ruby/gems/3.1.0/gems/rake-13.0.3/lib/rake/application.rb:160:in `invoke_task'", + "/home/tabby/.rbenv/versions/3.1.0/lib/ruby/gems/3.1.0/gems/rake-13.0.3/lib/rake/application.rb:116:in `block (2 levels) in top_level'", + "/home/tabby/.rbenv/versions/3.1.0/lib/ruby/gems/3.1.0/gems/rake-13.0.3/lib/rake/application.rb:116:in `each'", + "/home/tabby/.rbenv/versions/3.1.0/lib/ruby/gems/3.1.0/gems/rake-13.0.3/lib/rake/application.rb:116:in `block in top_level'", + "/home/tabby/.rbenv/versions/3.1.0/lib/ruby/gems/3.1.0/gems/rake-13.0.3/lib/rake/application.rb:125:in `run_with_threads'", + "/home/tabby/.rbenv/versions/3.1.0/lib/ruby/gems/3.1.0/gems/rake-13.0.3/lib/rake/application.rb:110:in `top_level'", + "/home/tabby/.rbenv/versions/3.1.0/lib/ruby/gems/3.1.0/gems/rake-13.0.3/lib/rake/application.rb:83:in `block in run'", + "/home/tabby/.rbenv/versions/3.1.0/lib/ruby/gems/3.1.0/gems/rake-13.0.3/lib/rake/application.rb:186:in `standard_exception_handling'", + "/home/tabby/.rbenv/versions/3.1.0/lib/ruby/gems/3.1.0/gems/rake-13.0.3/lib/rake/application.rb:80:in `run'", + "/home/tabby/.rbenv/versions/3.1.0/lib/ruby/gems/3.1.0/gems/rake-13.0.3/exe/rake:27:in `'", + "/home/tabby/.rbenv/versions/3.1.0/bin/rake:25:in `load'", + "/home/tabby/.rbenv/versions/3.1.0/bin/rake:25:in `'", + "/home/tabby/.rbenv/versions/3.1.0/lib/ruby/gems/3.1.0/gems/bundler-2.1.4/lib/bundler/cli/exec.rb:63:in `load'", + "/home/tabby/.rbenv/versions/3.1.0/lib/ruby/gems/3.1.0/gems/bundler-2.1.4/lib/bundler/cli/exec.rb:63:in `kernel_load'", + "/home/tabby/.rbenv/versions/3.1.0/lib/ruby/gems/3.1.0/gems/bundler-2.1.4/lib/bundler/cli/exec.rb:28:in `run'", + "/home/tabby/.rbenv/versions/3.1.0/lib/ruby/gems/3.1.0/gems/bundler-2.1.4/lib/bundler/cli.rb:476:in `exec'", + "/home/tabby/.rbenv/versions/3.1.0/lib/ruby/gems/3.1.0/gems/bundler-2.1.4/lib/bundler/vendor/thor/lib/thor/command.rb:27:in `run'", + "/home/tabby/.rbenv/versions/3.1.0/lib/ruby/gems/3.1.0/gems/bundler-2.1.4/lib/bundler/vendor/thor/lib/thor/invocation.rb:127:in `invoke_command'", + "/home/tabby/.rbenv/versions/3.1.0/lib/ruby/gems/3.1.0/gems/bundler-2.1.4/lib/bundler/vendor/thor/lib/thor.rb:399:in `dispatch'", + "/home/tabby/.rbenv/versions/3.1.0/lib/ruby/gems/3.1.0/gems/bundler-2.1.4/lib/bundler/cli.rb:30:in `dispatch'", + "/home/tabby/.rbenv/versions/3.1.0/lib/ruby/gems/3.1.0/gems/bundler-2.1.4/lib/bundler/vendor/thor/lib/thor/base.rb:476:in `start'", + "/home/tabby/.rbenv/versions/3.1.0/lib/ruby/gems/3.1.0/gems/bundler-2.1.4/lib/bundler/cli.rb:24:in `start'", + "/home/tabby/.rbenv/versions/3.1.0/lib/ruby/gems/3.1.0/gems/bundler-2.1.4/exe/bundle:46:in `block in '", + "/home/tabby/.rbenv/versions/3.1.0/lib/ruby/gems/3.1.0/gems/bundler-2.1.4/lib/bundler/friendly_errors.rb:123:in `with_friendly_errors'", + "/home/tabby/.rbenv/versions/3.1.0/lib/ruby/gems/3.1.0/gems/bundler-2.1.4/exe/bundle:34:in `'", + "/home/tabby/.rbenv/versions/3.1.0/bin/bundle:25:in `load'", + "/home/tabby/.rbenv/versions/3.1.0/bin/bundle:25:in `
'" + ], + "position": 9 + }, + "duration": 0.00010449799447087571 + }, + { + "description": "abs negative", + "full_description": "abs negative", + "file_path": "./test/abs_test.rb", + "full_path": "/home/tabby/git/vscode-ruby-test-adapter/test/fixtures/minitest/test/abs_test.rb", + "line_number": 12, + "klass": "AbsTest", + "method": "test_abs_negative", + "runnable": "AbsTest", + "id": "./test/abs_test.rb[12]", + "status": "skipped", + "pending_message": "Not implemented yet", + "duration": 0.00031753199436934665 + }, + { + "description": "abs positive", + "full_description": "abs positive", + "file_path": "./test/abs_test.rb", + "full_path": "/home/tabby/git/vscode-ruby-test-adapter/test/fixtures/minitest/test/abs_test.rb", + "line_number": 4, + "klass": "AbsTest", + "method": "test_abs_positive", + "runnable": "AbsTest", + "id": "./test/abs_test.rb[4]", + "status": "passed", + "duration": 6.183300138218328e-05 + }, + { + "description": "square 2", + "full_description": "square 2", + "file_path": "./test/square/square_test.rb", + "full_path": "/home/tabby/git/vscode-ruby-test-adapter/test/fixtures/minitest/test/square/square_test.rb", + "line_number": 4, + "klass": "SquareTest", + "method": "test_square_2", + "runnable": "SquareTest", + "id": "./test/square/square_test.rb[4]", + "status": "passed", + "duration": 8.17399995867163e-05 + }, + { + "description": "square 3", + "full_description": "square 3", + "file_path": "./test/square/square_test.rb", + "full_path": "/home/tabby/git/vscode-ruby-test-adapter/test/fixtures/minitest/test/square/square_test.rb", + "line_number": 8, + "klass": "SquareTest", + "method": "test_square_3", + "runnable": "SquareTest", + "id": "./test/square/square_test.rb[8]", + "status": "failed", + "pending_message": null, + "exception": { + "class": "Minitest::Assertion", + "message": "Expected: 9\n Actual: 6", + "backtrace": [ + "test/square/square_test.rb:9:in `test_square_3'" + ], + "full_backtrace": [ + "/home/tabby/.rbenv/versions/3.1.0/lib/ruby/gems/3.1.0/gems/minitest-5.14.4/lib/minitest/assertions.rb:183:in `assert'", + "/home/tabby/.rbenv/versions/3.1.0/lib/ruby/gems/3.1.0/gems/minitest-5.14.4/lib/minitest/assertions.rb:218:in `assert_equal'", + "/home/tabby/git/vscode-ruby-test-adapter/test/fixtures/minitest/test/square/square_test.rb:9:in `test_square_3'", + "/home/tabby/.rbenv/versions/3.1.0/lib/ruby/gems/3.1.0/gems/minitest-5.14.4/lib/minitest/test.rb:98:in `block (3 levels) in run'", + "/home/tabby/.rbenv/versions/3.1.0/lib/ruby/gems/3.1.0/gems/minitest-5.14.4/lib/minitest/test.rb:195:in `capture_exceptions'", + "/home/tabby/.rbenv/versions/3.1.0/lib/ruby/gems/3.1.0/gems/minitest-5.14.4/lib/minitest/test.rb:95:in `block (2 levels) in run'", + "/home/tabby/.rbenv/versions/3.1.0/lib/ruby/gems/3.1.0/gems/minitest-5.14.4/lib/minitest.rb:272:in `time_it'", + "/home/tabby/.rbenv/versions/3.1.0/lib/ruby/gems/3.1.0/gems/minitest-5.14.4/lib/minitest/test.rb:94:in `block in run'", + "/home/tabby/.rbenv/versions/3.1.0/lib/ruby/gems/3.1.0/gems/minitest-5.14.4/lib/minitest.rb:367:in `on_signal'", + "/home/tabby/.rbenv/versions/3.1.0/lib/ruby/gems/3.1.0/gems/minitest-5.14.4/lib/minitest/test.rb:211:in `with_info_handler'", + "/home/tabby/.rbenv/versions/3.1.0/lib/ruby/gems/3.1.0/gems/minitest-5.14.4/lib/minitest/test.rb:93:in `run'", + "/home/tabby/.rbenv/versions/3.1.0/lib/ruby/gems/3.1.0/gems/minitest-5.14.4/lib/minitest.rb:1029:in `run_one_method'", + "/home/tabby/.rbenv/versions/3.1.0/lib/ruby/gems/3.1.0/gems/minitest-5.14.4/lib/minitest.rb:341:in `run_one_method'", + "/home/tabby/.rbenv/versions/3.1.0/lib/ruby/gems/3.1.0/gems/minitest-5.14.4/lib/minitest.rb:328:in `block (2 levels) in run'", + "/home/tabby/.rbenv/versions/3.1.0/lib/ruby/gems/3.1.0/gems/minitest-5.14.4/lib/minitest.rb:327:in `each'", + "/home/tabby/.rbenv/versions/3.1.0/lib/ruby/gems/3.1.0/gems/minitest-5.14.4/lib/minitest.rb:327:in `block in run'", + "/home/tabby/.rbenv/versions/3.1.0/lib/ruby/gems/3.1.0/gems/minitest-5.14.4/lib/minitest.rb:367:in `on_signal'", + "/home/tabby/.rbenv/versions/3.1.0/lib/ruby/gems/3.1.0/gems/minitest-5.14.4/lib/minitest.rb:354:in `with_info_handler'", + "/home/tabby/.rbenv/versions/3.1.0/lib/ruby/gems/3.1.0/gems/minitest-5.14.4/lib/minitest.rb:326:in `run'", + "/home/tabby/git/vscode-ruby-test-adapter/ruby/vscode/minitest/runner.rb:40:in `block in run'", + "/home/tabby/git/vscode-ruby-test-adapter/ruby/vscode/minitest/runner.rb:39:in `each'", + "/home/tabby/git/vscode-ruby-test-adapter/ruby/vscode/minitest/runner.rb:39:in `run'", + "/home/tabby/git/vscode-ruby-test-adapter/ruby/vscode/minitest.rb:36:in `run'", + "/home/tabby/git/vscode-ruby-test-adapter/ruby/vscode.rake:14:in `block (3 levels) in '", + "/home/tabby/.rbenv/versions/3.1.0/lib/ruby/gems/3.1.0/gems/rake-13.0.3/lib/rake/task.rb:281:in `block in execute'", + "/home/tabby/.rbenv/versions/3.1.0/lib/ruby/gems/3.1.0/gems/rake-13.0.3/lib/rake/task.rb:281:in `each'", + "/home/tabby/.rbenv/versions/3.1.0/lib/ruby/gems/3.1.0/gems/rake-13.0.3/lib/rake/task.rb:281:in `execute'", + "/home/tabby/.rbenv/versions/3.1.0/lib/ruby/gems/3.1.0/gems/rake-13.0.3/lib/rake/task.rb:219:in `block in invoke_with_call_chain'", + "/home/tabby/.rbenv/versions/3.1.0/lib/ruby/gems/3.1.0/gems/rake-13.0.3/lib/rake/task.rb:199:in `synchronize'", + "/home/tabby/.rbenv/versions/3.1.0/lib/ruby/gems/3.1.0/gems/rake-13.0.3/lib/rake/task.rb:199:in `invoke_with_call_chain'", + "/home/tabby/.rbenv/versions/3.1.0/lib/ruby/gems/3.1.0/gems/rake-13.0.3/lib/rake/task.rb:188:in `invoke'", + "/home/tabby/.rbenv/versions/3.1.0/lib/ruby/gems/3.1.0/gems/rake-13.0.3/lib/rake/application.rb:160:in `invoke_task'", + "/home/tabby/.rbenv/versions/3.1.0/lib/ruby/gems/3.1.0/gems/rake-13.0.3/lib/rake/application.rb:116:in `block (2 levels) in top_level'", + "/home/tabby/.rbenv/versions/3.1.0/lib/ruby/gems/3.1.0/gems/rake-13.0.3/lib/rake/application.rb:116:in `each'", + "/home/tabby/.rbenv/versions/3.1.0/lib/ruby/gems/3.1.0/gems/rake-13.0.3/lib/rake/application.rb:116:in `block in top_level'", + "/home/tabby/.rbenv/versions/3.1.0/lib/ruby/gems/3.1.0/gems/rake-13.0.3/lib/rake/application.rb:125:in `run_with_threads'", + "/home/tabby/.rbenv/versions/3.1.0/lib/ruby/gems/3.1.0/gems/rake-13.0.3/lib/rake/application.rb:110:in `top_level'", + "/home/tabby/.rbenv/versions/3.1.0/lib/ruby/gems/3.1.0/gems/rake-13.0.3/lib/rake/application.rb:83:in `block in run'", + "/home/tabby/.rbenv/versions/3.1.0/lib/ruby/gems/3.1.0/gems/rake-13.0.3/lib/rake/application.rb:186:in `standard_exception_handling'", + "/home/tabby/.rbenv/versions/3.1.0/lib/ruby/gems/3.1.0/gems/rake-13.0.3/lib/rake/application.rb:80:in `run'", + "/home/tabby/.rbenv/versions/3.1.0/lib/ruby/gems/3.1.0/gems/rake-13.0.3/exe/rake:27:in `'", + "/home/tabby/.rbenv/versions/3.1.0/bin/rake:25:in `load'", + "/home/tabby/.rbenv/versions/3.1.0/bin/rake:25:in `'", + "/home/tabby/.rbenv/versions/3.1.0/lib/ruby/gems/3.1.0/gems/bundler-2.1.4/lib/bundler/cli/exec.rb:63:in `load'", + "/home/tabby/.rbenv/versions/3.1.0/lib/ruby/gems/3.1.0/gems/bundler-2.1.4/lib/bundler/cli/exec.rb:63:in `kernel_load'", + "/home/tabby/.rbenv/versions/3.1.0/lib/ruby/gems/3.1.0/gems/bundler-2.1.4/lib/bundler/cli/exec.rb:28:in `run'", + "/home/tabby/.rbenv/versions/3.1.0/lib/ruby/gems/3.1.0/gems/bundler-2.1.4/lib/bundler/cli.rb:476:in `exec'", + "/home/tabby/.rbenv/versions/3.1.0/lib/ruby/gems/3.1.0/gems/bundler-2.1.4/lib/bundler/vendor/thor/lib/thor/command.rb:27:in `run'", + "/home/tabby/.rbenv/versions/3.1.0/lib/ruby/gems/3.1.0/gems/bundler-2.1.4/lib/bundler/vendor/thor/lib/thor/invocation.rb:127:in `invoke_command'", + "/home/tabby/.rbenv/versions/3.1.0/lib/ruby/gems/3.1.0/gems/bundler-2.1.4/lib/bundler/vendor/thor/lib/thor.rb:399:in `dispatch'", + "/home/tabby/.rbenv/versions/3.1.0/lib/ruby/gems/3.1.0/gems/bundler-2.1.4/lib/bundler/cli.rb:30:in `dispatch'", + "/home/tabby/.rbenv/versions/3.1.0/lib/ruby/gems/3.1.0/gems/bundler-2.1.4/lib/bundler/vendor/thor/lib/thor/base.rb:476:in `start'", + "/home/tabby/.rbenv/versions/3.1.0/lib/ruby/gems/3.1.0/gems/bundler-2.1.4/lib/bundler/cli.rb:24:in `start'", + "/home/tabby/.rbenv/versions/3.1.0/lib/ruby/gems/3.1.0/gems/bundler-2.1.4/exe/bundle:46:in `block in '", + "/home/tabby/.rbenv/versions/3.1.0/lib/ruby/gems/3.1.0/gems/bundler-2.1.4/lib/bundler/friendly_errors.rb:123:in `with_friendly_errors'", + "/home/tabby/.rbenv/versions/3.1.0/lib/ruby/gems/3.1.0/gems/bundler-2.1.4/exe/bundle:34:in `'", + "/home/tabby/.rbenv/versions/3.1.0/bin/bundle:25:in `load'", + "/home/tabby/.rbenv/versions/3.1.0/bin/bundle:25:in `
'" + ], + "position": 9 + }, + "duration": 0.0003147919996990822 + } + ] +} diff --git a/test/fixtures/unitTests/rspec/dryRunOutput.json b/test/fixtures/unitTests/rspec/dryRunOutput.json new file mode 100644 index 0000000..1313e4b --- /dev/null +++ b/test/fixtures/unitTests/rspec/dryRunOutput.json @@ -0,0 +1,90 @@ +{ + "version": "3.10.1", + "examples": [ + { + "id": "./spec/abs_spec.rb[1:1]", + "description": "finds the absolute value of 1", + "full_description": "Abs finds the absolute value of 1", + "status": "passed", + "file_path": "./spec/abs_spec.rb", + "line_number": 4, + "type": null, + "pending_message": null, + "duration": 3.0758e-05 + }, + { + "id": "./spec/abs_spec.rb[1:2]", + "description": "finds the absolute value of 0", + "full_description": "Abs finds the absolute value of 0", + "status": "passed", + "file_path": "./spec/abs_spec.rb", + "line_number": 8, + "type": null, + "pending_message": null, + "duration": 1.2055e-05 + }, + { + "id": "./spec/abs_spec.rb[1:3]", + "description": "finds the absolute value of -1", + "full_description": "Abs finds the absolute value of -1", + "status": "passed", + "file_path": "./spec/abs_spec.rb", + "line_number": 12, + "type": null, + "pending_message": null, + "duration": 1.0444e-05 + }, + { + "id": "./spec/contexts_spec.rb[1:1:1:1:1:1:1:1:1:1]", + "description": "doesn't break the extension", + "full_description": "Contexts when there are many levels of nested contexts doesn't break the extension", + "status": "passed", + "file_path": "./spec/contexts_spec.rb", + "line_number": 14, + "type": null, + "pending_message": null, + "duration": 3.7666e-05 + }, + { + "id": "./spec/contexts_spec.rb[1:1:1:1:2:1]", + "description": "example at ./spec/contexts_spec.rb:24", + "full_description": "Contexts when there are fewer levels of nested contexts ", + "status": "passed", + "file_path": "./spec/contexts_spec.rb", + "line_number": 24, + "type": null, + "pending_message": null, + "duration": 4.024e-05 + }, + { + "id": "./spec/square/square_spec.rb[1:1]", + "description": "finds the square of 2", + "full_description": "Square finds the square of 2", + "status": "passed", + "file_path": "./spec/square/square_spec.rb", + "line_number": 4, + "type": null, + "pending_message": null, + "duration": 4.3333e-05 + }, + { + "id": "./spec/square/square_spec.rb[1:2]", + "description": "finds the square of 3", + "full_description": "Square finds the square of 3", + "status": "passed", + "file_path": "./spec/square/square_spec.rb", + "line_number": 8, + "type": null, + "pending_message": null, + "duration": 0.000158812 + } + ], + "summary": { + "duration": 0.007978328, + "example_count": 7, + "failure_count": 0, + "pending_count": 0, + "errors_outside_of_examples_count": 0 + }, + "summary_line": "7 examples, 0 failures" +} \ No newline at end of file diff --git a/test/fixtures/unitTests/rspec/testRunOutput.json b/test/fixtures/unitTests/rspec/testRunOutput.json new file mode 100644 index 0000000..bf35dc8 --- /dev/null +++ b/test/fixtures/unitTests/rspec/testRunOutput.json @@ -0,0 +1,199 @@ +{ + "version": "3.10.1", + "examples": [ + { + "id": "./spec/abs_spec.rb[1:1]", + "description": "finds the absolute value of 1", + "full_description": "Abs finds the absolute value of 1", + "status": "passed", + "file_path": "./spec/abs_spec.rb", + "line_number": 4, + "type": null, + "pending_message": null, + "duration": 0.001351962 + }, + { + "id": "./spec/abs_spec.rb[1:2]", + "description": "finds the absolute value of 0", + "full_description": "Abs finds the absolute value of 0", + "status": "failed", + "file_path": "./spec/abs_spec.rb", + "line_number": 8, + "type": null, + "pending_message": null, + "duration": 0.00035244, + "exception": { + "class": "RuntimeError", + "message": "Abs for zero is not supported", + "backtrace": [ + "/home/tabby/git/vscode-ruby-test-adapter/test/fixtures/rspec/lib/abs.rb:7:in `apply'", + "/home/tabby/git/vscode-ruby-test-adapter/test/fixtures/rspec/spec/abs_spec.rb:9:in `block (2 levels) in '", + "/home/tabby/.rbenv/versions/3.1.0/lib/ruby/gems/3.1.0/gems/rspec-core-3.10.1/lib/rspec/core/example.rb:262:in `instance_exec'", + "/home/tabby/.rbenv/versions/3.1.0/lib/ruby/gems/3.1.0/gems/rspec-core-3.10.1/lib/rspec/core/example.rb:262:in `block in run'", + "/home/tabby/.rbenv/versions/3.1.0/lib/ruby/gems/3.1.0/gems/rspec-core-3.10.1/lib/rspec/core/example.rb:508:in `block in with_around_and_singleton_context_hooks'", + "/home/tabby/.rbenv/versions/3.1.0/lib/ruby/gems/3.1.0/gems/rspec-core-3.10.1/lib/rspec/core/example.rb:465:in `block in with_around_example_hooks'", + "/home/tabby/.rbenv/versions/3.1.0/lib/ruby/gems/3.1.0/gems/rspec-core-3.10.1/lib/rspec/core/hooks.rb:486:in `block in run'", + "/home/tabby/.rbenv/versions/3.1.0/lib/ruby/gems/3.1.0/gems/rspec-core-3.10.1/lib/rspec/core/hooks.rb:624:in `run_around_example_hooks_for'", + "/home/tabby/.rbenv/versions/3.1.0/lib/ruby/gems/3.1.0/gems/rspec-core-3.10.1/lib/rspec/core/hooks.rb:486:in `run'", + "/home/tabby/.rbenv/versions/3.1.0/lib/ruby/gems/3.1.0/gems/rspec-core-3.10.1/lib/rspec/core/example.rb:465:in `with_around_example_hooks'", + "/home/tabby/.rbenv/versions/3.1.0/lib/ruby/gems/3.1.0/gems/rspec-core-3.10.1/lib/rspec/core/example.rb:508:in `with_around_and_singleton_context_hooks'", + "/home/tabby/.rbenv/versions/3.1.0/lib/ruby/gems/3.1.0/gems/rspec-core-3.10.1/lib/rspec/core/example.rb:259:in `run'", + "/home/tabby/.rbenv/versions/3.1.0/lib/ruby/gems/3.1.0/gems/rspec-core-3.10.1/lib/rspec/core/example_group.rb:644:in `block in run_examples'", + "/home/tabby/.rbenv/versions/3.1.0/lib/ruby/gems/3.1.0/gems/rspec-core-3.10.1/lib/rspec/core/example_group.rb:640:in `map'", + "/home/tabby/.rbenv/versions/3.1.0/lib/ruby/gems/3.1.0/gems/rspec-core-3.10.1/lib/rspec/core/example_group.rb:640:in `run_examples'", + "/home/tabby/.rbenv/versions/3.1.0/lib/ruby/gems/3.1.0/gems/rspec-core-3.10.1/lib/rspec/core/example_group.rb:606:in `run'", + "/home/tabby/.rbenv/versions/3.1.0/lib/ruby/gems/3.1.0/gems/rspec-core-3.10.1/lib/rspec/core/runner.rb:121:in `block (3 levels) in run_specs'", + "/home/tabby/.rbenv/versions/3.1.0/lib/ruby/gems/3.1.0/gems/rspec-core-3.10.1/lib/rspec/core/runner.rb:121:in `map'", + "/home/tabby/.rbenv/versions/3.1.0/lib/ruby/gems/3.1.0/gems/rspec-core-3.10.1/lib/rspec/core/runner.rb:121:in `block (2 levels) in run_specs'", + "/home/tabby/.rbenv/versions/3.1.0/lib/ruby/gems/3.1.0/gems/rspec-core-3.10.1/lib/rspec/core/configuration.rb:2067:in `with_suite_hooks'", + "/home/tabby/.rbenv/versions/3.1.0/lib/ruby/gems/3.1.0/gems/rspec-core-3.10.1/lib/rspec/core/runner.rb:116:in `block in run_specs'", + "/home/tabby/.rbenv/versions/3.1.0/lib/ruby/gems/3.1.0/gems/rspec-core-3.10.1/lib/rspec/core/reporter.rb:74:in `report'", + "/home/tabby/.rbenv/versions/3.1.0/lib/ruby/gems/3.1.0/gems/rspec-core-3.10.1/lib/rspec/core/runner.rb:115:in `run_specs'", + "/home/tabby/.rbenv/versions/3.1.0/lib/ruby/gems/3.1.0/gems/rspec-core-3.10.1/lib/rspec/core/runner.rb:89:in `run'", + "/home/tabby/.rbenv/versions/3.1.0/lib/ruby/gems/3.1.0/gems/rspec-core-3.10.1/lib/rspec/core/runner.rb:71:in `run'", + "/home/tabby/.rbenv/versions/3.1.0/lib/ruby/gems/3.1.0/gems/rspec-core-3.10.1/lib/rspec/core/runner.rb:45:in `invoke'", + "/home/tabby/.rbenv/versions/3.1.0/lib/ruby/gems/3.1.0/gems/rspec-core-3.10.1/exe/rspec:4:in `'", + "/home/tabby/.rbenv/versions/3.1.0/bin/rspec:25:in `load'", + "/home/tabby/.rbenv/versions/3.1.0/bin/rspec:25:in `'", + "/home/tabby/.rbenv/versions/3.1.0/lib/ruby/gems/3.1.0/gems/bundler-2.2.13/lib/bundler/cli/exec.rb:63:in `load'", + "/home/tabby/.rbenv/versions/3.1.0/lib/ruby/gems/3.1.0/gems/bundler-2.2.13/lib/bundler/cli/exec.rb:63:in `kernel_load'", + "/home/tabby/.rbenv/versions/3.1.0/lib/ruby/gems/3.1.0/gems/bundler-2.2.13/lib/bundler/cli/exec.rb:28:in `run'", + "/home/tabby/.rbenv/versions/3.1.0/lib/ruby/gems/3.1.0/gems/bundler-2.2.13/lib/bundler/cli.rb:494:in `exec'", + "/home/tabby/.rbenv/versions/3.1.0/lib/ruby/gems/3.1.0/gems/bundler-2.2.13/lib/bundler/vendor/thor/lib/thor/command.rb:27:in `run'", + "/home/tabby/.rbenv/versions/3.1.0/lib/ruby/gems/3.1.0/gems/bundler-2.2.13/lib/bundler/vendor/thor/lib/thor/invocation.rb:127:in `invoke_command'", + "/home/tabby/.rbenv/versions/3.1.0/lib/ruby/gems/3.1.0/gems/bundler-2.2.13/lib/bundler/vendor/thor/lib/thor.rb:392:in `dispatch'", + "/home/tabby/.rbenv/versions/3.1.0/lib/ruby/gems/3.1.0/gems/bundler-2.2.13/lib/bundler/cli.rb:30:in `dispatch'", + "/home/tabby/.rbenv/versions/3.1.0/lib/ruby/gems/3.1.0/gems/bundler-2.2.13/lib/bundler/vendor/thor/lib/thor/base.rb:485:in `start'", + "/home/tabby/.rbenv/versions/3.1.0/lib/ruby/gems/3.1.0/gems/bundler-2.2.13/lib/bundler/cli.rb:24:in `start'", + "/home/tabby/.rbenv/versions/3.1.0/lib/ruby/gems/3.1.0/gems/bundler-2.2.13/exe/bundle:49:in `block in '", + "/home/tabby/.rbenv/versions/3.1.0/lib/ruby/gems/3.1.0/gems/bundler-2.2.13/lib/bundler/friendly_errors.rb:130:in `with_friendly_errors'", + "/home/tabby/.rbenv/versions/3.1.0/lib/ruby/gems/3.1.0/gems/bundler-2.2.13/exe/bundle:37:in `'", + "/home/tabby/.rbenv/versions/3.1.0/bin/bundle:25:in `load'", + "/home/tabby/.rbenv/versions/3.1.0/bin/bundle:25:in `
'" + ], + "position": 8 + } + }, + { + "id": "./spec/abs_spec.rb[1:3]", + "description": "finds the absolute value of -1", + "full_description": "Abs finds the absolute value of -1", + "status": "pending", + "file_path": "./spec/abs_spec.rb", + "line_number": 12, + "type": null, + "pending_message": "No reason given", + "duration": 0.000438123 + }, + { + "id": "./spec/contexts_spec.rb[1:1:1:1:1:1:1:1:1:1]", + "description": "doesn't break the extension", + "full_description": "Contexts when there are many levels of nested contexts doesn't break the extension", + "status": "passed", + "file_path": "./spec/contexts_spec.rb", + "line_number": 14, + "type": null, + "pending_message": null, + "duration": 3.7666e-05 + }, + { + "id": "./spec/contexts_spec.rb[1:1:1:1:2:1]", + "description": "example at ./spec/contexts_spec.rb:24", + "full_description": "Contexts when there are fewer levels of nested contexts ", + "status": "passed", + "file_path": "./spec/contexts_spec.rb", + "line_number": 24, + "type": null, + "pending_message": null, + "duration": 4.024e-05 + }, + { + "id": "./spec/square/square_spec.rb[1:1]", + "description": "finds the square of 2", + "full_description": "Square finds the square of 2", + "status": "passed", + "file_path": "./spec/square/square_spec.rb", + "line_number": 4, + "type": null, + "pending_message": null, + "duration": 0.000480215 + }, + { + "id": "./spec/square/square_spec.rb[1:2]", + "description": "finds the square of 3", + "full_description": "Square finds the square of 3", + "status": "failed", + "file_path": "./spec/square/square_spec.rb", + "line_number": 8, + "type": null, + "pending_message": null, + "duration": 0.086019708, + "exception": { + "class": "RSpec::Expectations::ExpectationNotMetError", + "message": "\nexpected: 9\n got: 6\n\n(compared using ==)\n", + "backtrace": [ + "/home/tabby/.rbenv/versions/3.1.0/lib/ruby/gems/3.1.0/gems/rspec-support-3.10.2/lib/rspec/support.rb:102:in `block in '", + "/home/tabby/.rbenv/versions/3.1.0/lib/ruby/gems/3.1.0/gems/rspec-support-3.10.2/lib/rspec/support.rb:111:in `notify_failure'", + "/home/tabby/.rbenv/versions/3.1.0/lib/ruby/gems/3.1.0/gems/rspec-expectations-3.10.1/lib/rspec/expectations/fail_with.rb:35:in `fail_with'", + "/home/tabby/.rbenv/versions/3.1.0/lib/ruby/gems/3.1.0/gems/rspec-expectations-3.10.1/lib/rspec/expectations/handler.rb:38:in `handle_failure'", + "/home/tabby/.rbenv/versions/3.1.0/lib/ruby/gems/3.1.0/gems/rspec-expectations-3.10.1/lib/rspec/expectations/handler.rb:56:in `block in handle_matcher'", + "/home/tabby/.rbenv/versions/3.1.0/lib/ruby/gems/3.1.0/gems/rspec-expectations-3.10.1/lib/rspec/expectations/handler.rb:27:in`with_matcher'", + "/home/tabby/.rbenv/versions/3.1.0/lib/ruby/gems/3.1.0/gems/rspec-expectations-3.10.1/lib/rspec/expectations/handler.rb:48:in `handle_matcher'", + "/home/tabby/.rbenv/versions/3.1.0/lib/ruby/gems/3.1.0/gems/rspec-expectations-3.10.1/lib/rspec/expectations/expectation_target.rb:65:in `to'", + "/home/tabby/git/vscode-ruby-test-adapter/test/fixtures/rspec/spec/square/square_spec.rb:9:in `block (2 levels) in '", + "/home/tabby/.rbenv/versions/3.1.0/lib/ruby/gems/3.1.0/gems/rspec-core-3.10.1/lib/rspec/core/example.rb:262:in `instance_exec'", + "/home/tabby/.rbenv/versions/3.1.0/lib/ruby/gems/3.1.0/gems/rspec-core-3.10.1/lib/rspec/core/example.rb:262:in `block in run'", + "/home/tabby/.rbenv/versions/3.1.0/lib/ruby/gems/3.1.0/gems/rspec-core-3.10.1/lib/rspec/core/example.rb:508:in `block in with_around_and_singleton_context_hooks'", + "/home/tabby/.rbenv/versions/3.1.0/lib/ruby/gems/3.1.0/gems/rspec-core-3.10.1/lib/rspec/core/example.rb:465:in `block in with_around_example_hooks'", + "/home/tabby/.rbenv/versions/3.1.0/lib/ruby/gems/3.1.0/gems/rspec-core-3.10.1/lib/rspec/core/hooks.rb:486:in `block in run'", + "/home/tabby/.rbenv/versions/3.1.0/lib/ruby/gems/3.1.0/gems/rspec-core-3.10.1/lib/rspec/core/hooks.rb:624:in `run_around_example_hooks_for'", + "/home/tabby/.rbenv/versions/3.1.0/lib/ruby/gems/3.1.0/gems/rspec-core-3.10.1/lib/rspec/core/hooks.rb:486:in `run'", + "/home/tabby/.rbenv/versions/3.1.0/lib/ruby/gems/3.1.0/gems/rspec-core-3.10.1/lib/rspec/core/example.rb:465:in `with_around_example_hooks'", + "/home/tabby/.rbenv/versions/3.1.0/lib/ruby/gems/3.1.0/gems/rspec-core-3.10.1/lib/rspec/core/example.rb:508:in `with_around_and_singleton_context_hooks'", + "/home/tabby/.rbenv/versions/3.1.0/lib/ruby/gems/3.1.0/gems/rspec-core-3.10.1/lib/rspec/core/example.rb:259:in `run'", + "/home/tabby/.rbenv/versions/3.1.0/lib/ruby/gems/3.1.0/gems/rspec-core-3.10.1/lib/rspec/core/example_group.rb:644:in `block in run_examples'", + "/home/tabby/.rbenv/versions/3.1.0/lib/ruby/gems/3.1.0/gems/rspec-core-3.10.1/lib/rspec/core/example_group.rb:640:in `map'", + "/home/tabby/.rbenv/versions/3.1.0/lib/ruby/gems/3.1.0/gems/rspec-core-3.10.1/lib/rspec/core/example_group.rb:640:in `run_examples'", + "/home/tabby/.rbenv/versions/3.1.0/lib/ruby/gems/3.1.0/gems/rspec-core-3.10.1/lib/rspec/core/example_group.rb:606:in `run'", + "/home/tabby/.rbenv/versions/3.1.0/lib/ruby/gems/3.1.0/gems/rspec-core-3.10.1/lib/rspec/core/runner.rb:121:in `block (3 levels) in run_specs'", + "/home/tabby/.rbenv/versions/3.1.0/lib/ruby/gems/3.1.0/gems/rspec-core-3.10.1/lib/rspec/core/runner.rb:121:in `map'", + "/home/tabby/.rbenv/versions/3.1.0/lib/ruby/gems/3.1.0/gems/rspec-core-3.10.1/lib/rspec/core/runner.rb:121:in `block (2 levels) in run_specs'", + "/home/tabby/.rbenv/versions/3.1.0/lib/ruby/gems/3.1.0/gems/rspec-core-3.10.1/lib/rspec/core/configuration.rb:2067:in `with_suite_hooks'", + "/home/tabby/.rbenv/versions/3.1.0/lib/ruby/gems/3.1.0/gems/rspec-core-3.10.1/lib/rspec/core/runner.rb:116:in `block in run_specs'", + "/home/tabby/.rbenv/versions/3.1.0/lib/ruby/gems/3.1.0/gems/rspec-core-3.10.1/lib/rspec/core/reporter.rb:74:in `report'", + "/home/tabby/.rbenv/versions/3.1.0/lib/ruby/gems/3.1.0/gems/rspec-core-3.10.1/lib/rspec/core/runner.rb:115:in `run_specs'", + "/home/tabby/.rbenv/versions/3.1.0/lib/ruby/gems/3.1.0/gems/rspec-core-3.10.1/lib/rspec/core/runner.rb:89:in `run'", + "/home/tabby/.rbenv/versions/3.1.0/lib/ruby/gems/3.1.0/gems/rspec-core-3.10.1/lib/rspec/core/runner.rb:71:in `run'", + "/home/tabby/.rbenv/versions/3.1.0/lib/ruby/gems/3.1.0/gems/rspec-core-3.10.1/lib/rspec/core/runner.rb:45:in `invoke'", + "/home/tabby/.rbenv/versions/3.1.0/lib/ruby/gems/3.1.0/gems/rspec-core-3.10.1/exe/rspec:4:in `'", + "/home/tabby/.rbenv/versions/3.1.0/bin/rspec:25:in `load'", + "/home/tabby/.rbenv/versions/3.1.0/bin/rspec:25:in `'", + "/home/tabby/.rbenv/versions/3.1.0/lib/ruby/gems/3.1.0/gems/bundler-2.2.13/lib/bundler/cli/exec.rb:63:in `load'", + "/home/tabby/.rbenv/versions/3.1.0/lib/ruby/gems/3.1.0/gems/bundler-2.2.13/lib/bundler/cli/exec.rb:63:in `kernel_load'", + "/home/tabby/.rbenv/versions/3.1.0/lib/ruby/gems/3.1.0/gems/bundler-2.2.13/lib/bundler/cli/exec.rb:28:in `run'", + "/home/tabby/.rbenv/versions/3.1.0/lib/ruby/gems/3.1.0/gems/bundler-2.2.13/lib/bundler/cli.rb:494:in `exec'", + "/home/tabby/.rbenv/versions/3.1.0/lib/ruby/gems/3.1.0/gems/bundler-2.2.13/lib/bundler/vendor/thor/lib/thor/command.rb:27:in `run'", + "/home/tabby/.rbenv/versions/3.1.0/lib/ruby/gems/3.1.0/gems/bundler-2.2.13/lib/bundler/vendor/thor/lib/thor/invocation.rb:127:in `invoke_command'", + "/home/tabby/.rbenv/versions/3.1.0/lib/ruby/gems/3.1.0/gems/bundler-2.2.13/lib/bundler/vendor/thor/lib/thor.rb:392:in `dispatch'", + "/home/tabby/.rbenv/versions/3.1.0/lib/ruby/gems/3.1.0/gems/bundler-2.2.13/lib/bundler/cli.rb:30:in `dispatch'", + "/home/tabby/.rbenv/versions/3.1.0/lib/ruby/gems/3.1.0/gems/bundler-2.2.13/lib/bundler/vendor/thor/lib/thor/base.rb:485:in `start'", + "/home/tabby/.rbenv/versions/3.1.0/lib/ruby/gems/3.1.0/gems/bundler-2.2.13/lib/bundler/cli.rb:24:in `start'", + "/home/tabby/.rbenv/versions/3.1.0/lib/ruby/gems/3.1.0/gems/bundler-2.2.13/exe/bundle:49:in `block in '", + "/home/tabby/.rbenv/versions/3.1.0/lib/ruby/gems/3.1.0/gems/bundler-2.2.13/lib/bundler/friendly_errors.rb:130:in `with_friendly_errors'", + "/home/tabby/.rbenv/versions/3.1.0/lib/ruby/gems/3.1.0/gems/bundler-2.2.13/exe/bundle:37:in `'", + "/home/tabby/.rbenv/versions/3.1.0/bin/bundle:25:in `load'", + "/home/tabby/.rbenv/versions/3.1.0/bin/bundle:25:in `
'" + ], + "position": 8 + } + } + ], + "summary": { + "duration": 0.094338312, + "example_count": 7, + "failure_count": 2, + "pending_count": 1, + "errors_outside_of_examples_count": 0 + }, + "summary_line": "7 examples, 2 failures, 1 pending" +} \ No newline at end of file diff --git a/test/runFrameworkTests.ts b/test/runFrameworkTests.ts new file mode 100644 index 0000000..a97437e --- /dev/null +++ b/test/runFrameworkTests.ts @@ -0,0 +1,87 @@ +import * as path from 'path' +import { runTests, downloadAndUnzipVSCode } from '@vscode/test-electron'; + +const extensionDevelopmentPath = path.resolve(__dirname, '../../'); +const allowedSuiteArguments = ["all", "rspec", "minitest", "unitTests"] +const maxDownloadRetries = 5; + +async function main(framework: string) { + let vscodeExecutablePath: string | undefined + for (let retries = 0; retries < maxDownloadRetries; retries++) { + try { + vscodeExecutablePath = await downloadAndUnzipVSCode('stable') + break + } catch (error) { + console.warn(`Failed to download Visual Studio Code (attempt ${retries} of ${maxDownloadRetries}): ${error}`) + } + } + if (vscodeExecutablePath) { + if (framework == 'all') { + await runTestSuite(vscodeExecutablePath, 'unitTests') + await runTestSuite(vscodeExecutablePath, 'rspec') + await runTestSuite(vscodeExecutablePath, 'minitest') + } else { + await runTestSuite(vscodeExecutablePath, framework) + } + } else { + console.error("crap") + } +} + +/** + * Sets up and runs a test suite in a child instance of VSCode + * + * If this is run + * + * @param suite Name of the test suite to run (one of the folders in test/suite) + * @param vscodeExecutablePath Path to VS executable to use. If not provided, or doesn't exist, + * the extension test will download the latest stable VS code to run the tests with + */ +async function runTestSuite(vscodeExecutablePath: string, suite: string) { + let testsPath = path.resolve(__dirname, `suite`) + let fixturesPath = path.resolve(extensionDevelopmentPath, `test/fixtures/${suite}`) + + console.debug(`extensionDevelopmentPath: ${extensionDevelopmentPath}`) // Root folder of repository + console.debug(`testsPath: ${testsPath}`) // Path to tests/suite + console.debug(`fixturesPath: ${fixturesPath}`) // Workspace folder + + await runTests({ + extensionDevelopmentPath, + extensionTestsPath: testsPath, + extensionTestsEnv: { "TEST_SUITE": suite }, + launchArgs: ['--disable-gpu', fixturesPath], + vscodeExecutablePath: vscodeExecutablePath + }).catch((error: any) => { + console.error(error); + console.error(`Failed to run ${suite} tests`) + }) +} + +function printHelpAndExit() { + console.log("") + console.log("Please run this script with one of the following available test suite names:") + allowedSuiteArguments.forEach(name => { + console.log(` - ${name}`) + }); + console.log("") + console.log("Example:") + console.log("node ./out/test/runFrameworkTests.js rspec") + + process.exit(1) +} + +// Check a test suite argument was actually given +if (process.argv.length < 3) { + console.error("No test suite requested!") + printHelpAndExit() +} + +let suite = process.argv[2] +// Check the test suite argument is one that we accept +if (!allowedSuiteArguments.includes(suite)) { + console.error(`Invalid test suite requested: ${suite}`) + printHelpAndExit() +} + +console.info(`Running ${suite} tests...`) +main(process.argv[2]) diff --git a/test/runMinitestTests.ts b/test/runMinitestTests.ts deleted file mode 100644 index c84edcd..0000000 --- a/test/runMinitestTests.ts +++ /dev/null @@ -1,32 +0,0 @@ -import * as path from 'path'; -import * as cp from 'child_process'; - -import { runTests, downloadAndUnzipVSCode, resolveCliPathFromVSCodeExecutablePath } from 'vscode-test'; - -async function main() { - try { - const extensionDevelopmentPath = path.resolve(__dirname, '../../'); - - const vscodeExecutablePath = await downloadAndUnzipVSCode('stable') - - const cliPath = resolveCliPathFromVSCodeExecutablePath(vscodeExecutablePath) - cp.spawnSync(cliPath, ['--install-extension', 'hbenl.vscode-test-explorer'], { - encoding: 'utf-8', - stdio: 'inherit' - }) - - await runTests( - { - extensionDevelopmentPath, - extensionTestsPath: path.resolve(__dirname, './suite/frameworks/minitest/index'), - launchArgs: [path.resolve(extensionDevelopmentPath, 'test/fixtures/minitest')] - } - ); - } catch (err) { - console.error(err); - console.error('Failed to run tests'); - process.exit(1); - } -} - -main(); diff --git a/test/runRspecTests.ts b/test/runRspecTests.ts deleted file mode 100644 index b121956..0000000 --- a/test/runRspecTests.ts +++ /dev/null @@ -1,32 +0,0 @@ -import * as path from 'path'; -import * as cp from 'child_process'; - -import { runTests, downloadAndUnzipVSCode, resolveCliPathFromVSCodeExecutablePath } from 'vscode-test'; - -async function main() { - try { - const extensionDevelopmentPath = path.resolve(__dirname, '../../'); - - const vscodeExecutablePath = await downloadAndUnzipVSCode('stable') - - const cliPath = resolveCliPathFromVSCodeExecutablePath(vscodeExecutablePath) - cp.spawnSync(cliPath, ['--install-extension', 'hbenl.vscode-test-explorer'], { - encoding: 'utf-8', - stdio: 'inherit' - }) - - await runTests( - { - extensionDevelopmentPath, - extensionTestsPath: path.resolve(__dirname, './suite/frameworks/rspec/index'), - launchArgs: [path.resolve(extensionDevelopmentPath, 'test/fixtures/rspec')] - } - ); - } catch (err) { - console.error(err); - console.error('Failed to run tests'); - process.exit(1); - } -} - -main(); diff --git a/test/suite/DummyController.ts b/test/suite/DummyController.ts deleted file mode 100644 index 34c87cc..0000000 --- a/test/suite/DummyController.ts +++ /dev/null @@ -1,60 +0,0 @@ -import { TestAdapter, TestController, TestEvent, TestSuiteInfo } from "vscode-test-adapter-api"; - -export const sleep = (msec: number) => new Promise(resolve => setTimeout(resolve, msec)); - -export class DummyController implements TestController { - adapter: TestAdapter | undefined - suite: TestSuiteInfo | undefined - testEvents: { [testRunId: string]: TestEvent[] } - - constructor() { - this.testEvents = {} - } - - async load() { - await this.adapter?.load() - } - - async runTest(...testRunIds: string[]) { - await this.adapter?.run(testRunIds) - } - - registerTestAdapter(adapter: TestAdapter) { - if (this.adapter === undefined) { - this.adapter = adapter - - adapter.tests(event => { - switch (event.type) { - case 'started': - this.suite = undefined - this.testEvents = {} - break - case 'finished': - this.suite = event.suite - break - } - }) - - adapter.testStates(event => { - switch (event.type) { - case 'test': - const id = event.test - if (typeof id === "string") { - const value = this.testEvents[id] - if (!Array.isArray(value)) { - this.testEvents[id] = [event] - } else { - value.push(event) - } - } - } - }) - } - } - - unregisterTestAdapter(adapter: TestAdapter) { - if (this.adapter === adapter) { - this.adapter = undefined - } - } -} diff --git a/test/suite/frameworks/minitest/index.ts b/test/suite/frameworks/minitest/index.ts deleted file mode 100644 index 7768839..0000000 --- a/test/suite/frameworks/minitest/index.ts +++ /dev/null @@ -1,34 +0,0 @@ -import * as path from 'path'; -import * as Mocha from 'mocha'; -import * as glob from 'glob'; - -export function run(): Promise { - // Create the mocha test - const mocha = new Mocha({ - ui: 'tdd' - }); - - return new Promise((c, e) => { - glob('**.test.js', { cwd: __dirname }, (err, files) => { - if (err) { - return e(err); - } - - // Add files to the test suite - files.forEach(f => mocha.addFile(path.resolve(__dirname, f))); - - try { - // Run the mocha test - mocha.run(failures => { - if (failures > 0) { - e(new Error(`${failures} tests failed.`)); - } else { - c(); - } - }); - } catch (err) { - e(err); - } - }); - }); -} diff --git a/test/suite/frameworks/minitest/minitest.test.ts b/test/suite/frameworks/minitest/minitest.test.ts deleted file mode 100644 index 47855d4..0000000 --- a/test/suite/frameworks/minitest/minitest.test.ts +++ /dev/null @@ -1,224 +0,0 @@ -import * as assert from 'assert'; -import * as path from 'path'; - -// You can import and use all API from the 'vscode' module -// as well as import your extension to test it -import * as vscode from 'vscode'; -import { testExplorerExtensionId, TestHub, TestSuiteInfo } from 'vscode-test-adapter-api'; -import { DummyController } from '../../DummyController'; - -suite('Extension Test for Minitest', () => { - test('Load all tests', async () => { - const controller = new DummyController() - const dirPath = vscode.workspace.workspaceFolders![0].uri.path - - const testExplorerExtension = vscode.extensions.getExtension(testExplorerExtensionId)!; - const testHub = testExplorerExtension.exports; - - testHub.registerTestController(controller); - - await controller.load() - - assert.deepStrictEqual( - controller.suite, - { - type: 'suite', - id: 'root', - label: 'minitest Minitest', - children: [ - { - file: path.resolve(dirPath, "test/abs_test.rb"), - id: "./test/abs_test.rb", - label: "abs_test.rb", - type: "suite", - children: [ - { - file: path.resolve(dirPath, "test/abs_test.rb"), - id: "./test/abs_test.rb[4]", - label: "abs positive", - line: 3, - type: "test" - }, - { - file: path.resolve(dirPath, "test/abs_test.rb"), - id: "./test/abs_test.rb[8]", - label: "abs 0", - line: 7, - type: "test" - }, - { - file: path.resolve(dirPath, "test/abs_test.rb"), - id: "./test/abs_test.rb[12]", - label: "abs negative", - line: 11, - type: "test" - } - ] - }, - { - file: path.resolve(dirPath, "test/square_test.rb"), - id: "./test/square_test.rb", - label: "square_test.rb", - type: "suite", - children: [ - { - file: path.resolve(dirPath, "test/square_test.rb"), - id: "./test/square_test.rb[4]", - label: "square 2", - line: 3, - type: "test" - }, - { - file: path.resolve(dirPath, "test/square_test.rb"), - id: "./test/square_test.rb[8]", - label: "square 3", - line: 7, - type: "test" - } - ] - } - ] - } as TestSuiteInfo - ) - }) - - test('run test success', async () => { - const controller = new DummyController() - - const testExplorerExtension = vscode.extensions.getExtension(testExplorerExtensionId)!; - const testHub = testExplorerExtension.exports; - - testHub.registerTestController(controller); - - await controller.load() - await controller.runTest('./test/square_test.rb[4]') - - assert.deepStrictEqual( - controller.testEvents['./test/square_test.rb[4]'], - [ - { state: "running", test: "./test/square_test.rb[4]", type: "test" }, - { state: "running", test: "./test/square_test.rb[4]", type: "test" }, - { state: "passed", test: "./test/square_test.rb[4]", type: "test" }, - { state: "passed", test: "./test/square_test.rb[4]", type: "test" } - ] - ) - }) - - test('run test failure', async () => { - const controller = new DummyController() - - const testExplorerExtension = vscode.extensions.getExtension(testExplorerExtensionId)!; - const testHub = testExplorerExtension.exports; - - testHub.registerTestController(controller); - - await controller.load() - await controller.runTest('./test/square_test.rb[8]') - - assert.deepStrictEqual( - controller.testEvents['./test/square_test.rb[8]'][0], - { state: "running", test: "./test/square_test.rb[8]", type: "test" } - ) - - assert.deepStrictEqual( - controller.testEvents['./test/square_test.rb[8]'][1], - { state: "running", test: "./test/square_test.rb[8]", type: "test" } - ) - - assert.deepStrictEqual( - controller.testEvents['./test/square_test.rb[8]'][2], - { state: "failed", test: "./test/square_test.rb[8]", type: "test" } - ) - - const lastEvent = controller.testEvents['./test/square_test.rb[8]'][3] - assert.strictEqual(lastEvent.state, "failed") - assert.strictEqual(lastEvent.line, undefined) - assert.strictEqual(lastEvent.tooltip, undefined) - assert.strictEqual(lastEvent.description, undefined) - assert.ok(lastEvent.message?.startsWith("Expected: 9\n Actual: 6\n")) - - assert.strictEqual(lastEvent.decorations!.length, 1) - const decoration = lastEvent.decorations![0] - assert.strictEqual(decoration.line, 8) - assert.strictEqual(decoration.file, undefined) - assert.strictEqual(decoration.hover, undefined) - assert.strictEqual(decoration.message, "Expected: 9\n Actual: 6") - }) - - test('run test error', async () => { - const controller = new DummyController() - - const testExplorerExtension = vscode.extensions.getExtension(testExplorerExtensionId)!; - const testHub = testExplorerExtension.exports; - - testHub.registerTestController(controller); - - await controller.load() - await controller.runTest('./test/abs_test.rb[8]') - - assert.deepStrictEqual( - controller.testEvents['./test/abs_test.rb[8]'][0], - { state: "running", test: "./test/abs_test.rb[8]", type: "test" } - ) - - assert.deepStrictEqual( - controller.testEvents['./test/abs_test.rb[8]'][1], - { state: "running", test: "./test/abs_test.rb[8]", type: "test" } - ) - - assert.deepStrictEqual( - controller.testEvents['./test/abs_test.rb[8]'][2], - { state: "failed", test: "./test/abs_test.rb[8]", type: "test" } - ) - - const lastEvent = controller.testEvents['./test/abs_test.rb[8]'][3] - assert.strictEqual(lastEvent.state, "failed") - assert.strictEqual(lastEvent.line, undefined) - assert.strictEqual(lastEvent.tooltip, undefined) - assert.strictEqual(lastEvent.description, undefined) - assert.ok(lastEvent.message?.startsWith("RuntimeError: Abs for zero is not supported\n")) - - assert.strictEqual(lastEvent.decorations!.length, 1) - const decoration = lastEvent.decorations![0] - assert.strictEqual(decoration.line, 8) - assert.strictEqual(decoration.file, undefined) - assert.strictEqual(decoration.hover, undefined) - assert.ok(decoration.message?.startsWith("RuntimeError: Abs for zero is not supported\n")) - }) - - test('run test skip', async () => { - const controller = new DummyController() - - const testExplorerExtension = vscode.extensions.getExtension(testExplorerExtensionId)!; - const testHub = testExplorerExtension.exports; - - testHub.registerTestController(controller); - - await controller.load() - await controller.runTest('./test/abs_test.rb[12]') - - assert.deepStrictEqual( - controller.testEvents['./test/abs_test.rb[12]'][0], - { state: "running", test: "./test/abs_test.rb[12]", type: "test" } - ) - - assert.deepStrictEqual( - controller.testEvents['./test/abs_test.rb[12]'][1], - { state: "running", test: "./test/abs_test.rb[12]", type: "test" } - ) - - assert.deepStrictEqual( - controller.testEvents['./test/abs_test.rb[12]'][2], - { state: "skipped", test: "./test/abs_test.rb[12]", type: "test" } - ) - - const lastEvent = controller.testEvents['./test/abs_test.rb[12]'][3] - assert.strictEqual(lastEvent.state, "skipped") - assert.strictEqual(lastEvent.line, undefined) - assert.strictEqual(lastEvent.tooltip, undefined) - assert.strictEqual(lastEvent.description, undefined) - assert.strictEqual(lastEvent.message, "Not implemented yet") - - assert.strictEqual(lastEvent.decorations, undefined) - }) -}); diff --git a/test/suite/frameworks/rspec/index.ts b/test/suite/frameworks/rspec/index.ts deleted file mode 100644 index 7768839..0000000 --- a/test/suite/frameworks/rspec/index.ts +++ /dev/null @@ -1,34 +0,0 @@ -import * as path from 'path'; -import * as Mocha from 'mocha'; -import * as glob from 'glob'; - -export function run(): Promise { - // Create the mocha test - const mocha = new Mocha({ - ui: 'tdd' - }); - - return new Promise((c, e) => { - glob('**.test.js', { cwd: __dirname }, (err, files) => { - if (err) { - return e(err); - } - - // Add files to the test suite - files.forEach(f => mocha.addFile(path.resolve(__dirname, f))); - - try { - // Run the mocha test - mocha.run(failures => { - if (failures > 0) { - e(new Error(`${failures} tests failed.`)); - } else { - c(); - } - }); - } catch (err) { - e(err); - } - }); - }); -} diff --git a/test/suite/frameworks/rspec/rspec.test.ts b/test/suite/frameworks/rspec/rspec.test.ts deleted file mode 100644 index 06344e3..0000000 --- a/test/suite/frameworks/rspec/rspec.test.ts +++ /dev/null @@ -1,193 +0,0 @@ -import * as assert from 'assert'; -import * as path from 'path'; - -// You can import and use all API from the 'vscode' module -// as well as import your extension to test it -import * as vscode from 'vscode'; -import { testExplorerExtensionId, TestHub, TestSuiteInfo } from 'vscode-test-adapter-api'; -import { DummyController } from '../../DummyController'; - -suite('Extension Test for RSpec', () => { - test('Load all tests', async () => { - const controller = new DummyController() - const dirPath = vscode.workspace.workspaceFolders![0].uri.path - - const testExplorerExtension = vscode.extensions.getExtension(testExplorerExtensionId)!; - const testHub = testExplorerExtension.exports; - - testHub.registerTestController(controller); - - await controller.load() - - assert.deepStrictEqual( - controller.suite, - { - type: 'suite', - id: 'root', - label: 'rspec RSpec', - children: [ - { - file: path.resolve(dirPath, "spec/abs_spec.rb"), - id: "./spec/abs_spec.rb", - label: "abs_spec.rb", - type: "suite", - children: [ - { - file: path.resolve(dirPath, "spec/abs_spec.rb"), - id: "./spec/abs_spec.rb[1:1]", - label: "finds the absolute value of 1", - line: 3, - type: "test" - }, - { - file: path.resolve(dirPath, "spec/abs_spec.rb"), - id: "./spec/abs_spec.rb[1:2]", - label: "finds the absolute value of 0", - line: 7, - type: "test" - }, - { - file: path.resolve(dirPath, "spec/abs_spec.rb"), - id: "./spec/abs_spec.rb[1:3]", - label: "finds the absolute value of -1", - line: 11, - type: "test" - } - ] - }, - { - file: path.resolve(dirPath, "spec/square_spec.rb"), - id: "./spec/square_spec.rb", - label: "square_spec.rb", - type: "suite", - children: [ - { - file: path.resolve(dirPath, "spec/square_spec.rb"), - id: "./spec/square_spec.rb[1:1]", - label: "finds the square of 2", - line: 3, - type: "test" - }, - { - file: path.resolve(dirPath, "spec/square_spec.rb"), - id: "./spec/square_spec.rb[1:2]", - label: "finds the square of 3", - line: 7, - type: "test" - } - ] - } - ] - } as TestSuiteInfo - ) - }) - - test('run test success', async () => { - const controller = new DummyController() - - const testExplorerExtension = vscode.extensions.getExtension(testExplorerExtensionId)!; - const testHub = testExplorerExtension.exports; - - testHub.registerTestController(controller); - - await controller.load() - await controller.runTest('./spec/square_spec.rb') - - assert.deepStrictEqual( - controller.testEvents['./spec/square_spec.rb[1:1]'], - [ - { state: "passed", test: "./spec/square_spec.rb[1:1]", type: "test" }, - { state: "passed", test: "./spec/square_spec.rb[1:1]", type: "test" } - ] - ) - }) - - test('run test failure', async () => { - const controller = new DummyController() - - const testExplorerExtension = vscode.extensions.getExtension(testExplorerExtensionId)!; - const testHub = testExplorerExtension.exports; - - testHub.registerTestController(controller); - - await controller.load() - await controller.runTest('./spec/square_spec.rb') - - assert.deepStrictEqual( - controller.testEvents['./spec/square_spec.rb[1:2]'][0], - { state: "failed", test: "./spec/square_spec.rb[1:2]", type: "test" } - ) - - const lastEvent = controller.testEvents['./spec/square_spec.rb[1:2]'][1] - assert.strictEqual(lastEvent.state, "failed") - assert.strictEqual(lastEvent.line, undefined) - assert.strictEqual(lastEvent.tooltip, undefined) - assert.strictEqual(lastEvent.description, undefined) - assert.ok(lastEvent.message?.startsWith("RSpec::Expectations::ExpectationNotMetError:\n expected: 9\n got: 6\n")) - - assert.strictEqual(lastEvent.decorations!.length, 1) - const decoration = lastEvent.decorations![0] - assert.strictEqual(decoration.line, 8) - assert.strictEqual(decoration.file, undefined) - assert.strictEqual(decoration.hover, undefined) - assert.strictEqual(decoration.message, " expected: 9\n got: 6\n\n(compared using ==)\n") - }) - - test('run test error', async () => { - const controller = new DummyController() - - const testExplorerExtension = vscode.extensions.getExtension(testExplorerExtensionId)!; - const testHub = testExplorerExtension.exports; - - testHub.registerTestController(controller); - - await controller.load() - await controller.runTest('./spec/abs_spec.rb[1:2]') - - assert.deepStrictEqual( - controller.testEvents['./spec/abs_spec.rb[1:2]'][0], - { state: "running", test: "./spec/abs_spec.rb[1:2]", type: "test" } - ) - - assert.deepStrictEqual( - controller.testEvents['./spec/abs_spec.rb[1:2]'][1], - { state: "failed", test: "./spec/abs_spec.rb[1:2]", type: "test" } - ) - - const lastEvent = controller.testEvents['./spec/abs_spec.rb[1:2]'][2] - assert.strictEqual(lastEvent.state, "failed") - assert.strictEqual(lastEvent.line, undefined) - assert.strictEqual(lastEvent.tooltip, undefined) - assert.strictEqual(lastEvent.description, undefined) - assert.ok(lastEvent.message?.startsWith("RuntimeError:\nAbs for zero is not supported")) - - assert.strictEqual(lastEvent.decorations!.length, 1) - const decoration = lastEvent.decorations![0] - assert.strictEqual(decoration.line, 8) - assert.strictEqual(decoration.file, undefined) - assert.strictEqual(decoration.hover, undefined) - assert.ok(decoration.message?.startsWith("Abs for zero is not supported")) - }) - - test('run test skip', async () => { - const controller = new DummyController() - - const testExplorerExtension = vscode.extensions.getExtension(testExplorerExtensionId)!; - const testHub = testExplorerExtension.exports; - - testHub.registerTestController(controller); - - await controller.load() - await controller.runTest('./spec/abs_spec.rb[1:3]') - - assert.deepStrictEqual( - controller.testEvents['./spec/abs_spec.rb[1:3]'][0], - { state: "running", test: "./spec/abs_spec.rb[1:3]", type: "test" } - ) - - assert.deepStrictEqual( - controller.testEvents['./spec/abs_spec.rb[1:3]'][1], - { state: "skipped", test: "./spec/abs_spec.rb[1:3]", type: "test" } - ) - }) -}); diff --git a/test/suite/helpers.ts b/test/suite/helpers.ts new file mode 100644 index 0000000..0dd09b1 --- /dev/null +++ b/test/suite/helpers.ts @@ -0,0 +1,191 @@ +import * as vscode from 'vscode' +import { expect } from 'chai' +import { capture, instance, mock, when } from '@typestrong/ts-mockito'; +import { ArgCaptor1, ArgCaptor2, ArgCaptor3 } from '@typestrong/ts-mockito/lib/capture/ArgCaptor'; + +import { TestSuiteManager } from '../testSuiteManager'; +import { getExtensionLogger, IVSCodeExtLogger, LogLevel } from '@vscode-logging/logger'; + +/** + * Get a logger + * + * @param level One of "off", "fatal", "error", "warn", "info", "debug", "trace" + * @returns a logger that logs to stdout at the specified level + */ +export function logger(level: LogLevel = "info"): IVSCodeExtLogger { + return getExtensionLogger({ + extName: "RubyTestExplorer - Tests", + level: (level as LogLevel), + logConsole: true + }) +} + +/** + * Object to simplify describing a {@link vscode.TestItem TestItem} for testing its values + */ +export type TestItemExpectation = { + id: string, + label: string, + file?: string, + line?: number, + children?: TestItemExpectation[], + canResolveChildren?: boolean, +} + +/** + * Object to simplify describing a {@link vscode.TestItem TestItem} for testing its values + */ +export type TestFailureExpectation = { + message?: RegExp, + actualOutput?: string, + expectedOutput?: string, + line?: number, +} + +export function testUriMatches(testItem: vscode.TestItem, path?: string) { + if (path) { + expect(testItem.uri).to.not.be.undefined + expect(testItem.uri?.path).to.eql(path, `uri mismatch (id: ${testItem.id})`) + } else { + expect(testItem.uri).to.be.undefined + } +} + +/** + * Assert that a {@link vscode.TestItem TestItem} matches the expected values + * @param testItem {@link vscode.TestItem TestItem} to check + * @param expectation {@link TestItemExpectation} to check against + */ +export function testItemMatches(testItem: vscode.TestItem, expectation?: TestItemExpectation, message?: string) { + if (!expectation) expect.fail("No expectation given") + + expect(testItem.id).to.eq(expectation.id, `${message ? message + ' - ' : ''}id mismatch (expected: ${expectation.id})`) + testUriMatches(testItem, expectation.file) + if (expectation.children && expectation.children.length > 0) { + testItemCollectionMatches(testItem.children, expectation.children, testItem) + } + expect(testItem.canResolveChildren).to.eq(expectation.canResolveChildren || false, `${message ? message + ' - ' : ''}canResolveChildren (id: ${expectation.id})`) + expect(testItem.label).to.eq(expectation.label, `${message ? message + ' - ' : ''}label mismatch (id: ${expectation.id})`) + expect(testItem.description).to.be.undefined + //expect(testItem.description).to.eq(expectation.label, 'description mismatch') + if (expectation.line) { + expect(testItem.range).to.not.be.undefined + expect(testItem.range?.start.line).to.eq(expectation.line, `${message ? message + ' - ' : ''}line number mismatch (id: ${expectation.id})`) + } else { + expect(testItem.range).to.be.undefined + } + expect(testItem.error).to.be.undefined +} + +/** + * Loops through an array of {@link vscode.TestItem TestItem}s and asserts whether each in turn matches the expectation with the same index + * @param testItems TestItems to check + * @param expectation Array of {@link TestItemExpectation}s to compare to + */ +export function testItemArrayMatches(testItems: readonly vscode.TestItem[], expectation: TestItemExpectation[]) { + expect(testItems.length).to.eq(expectation.length) + testItems.forEach((testItem: vscode.TestItem, i: number) => { + testItemMatches(testItem, expectation[i]) + }) +} + +/** + * Loops through a {@link vscode.TestItemCollection TestItemCollection} and asserts whether each in turn matches the expectation with the same index + * @param testItems TestItems to check + * @param expectation Array of {@link TestItemExpectation}s to compare to + */ +export function testItemCollectionMatches( + testItems: vscode.TestItemCollection, + expectation: TestItemExpectation[], + parent?: vscode.TestItem +) { + expect(testItems.size).to.eq( + expectation.length, + parent ? `Wrong number of children in item ${parent.id}\n\t${testItems.toString()}` : `Wrong number of items in collection\n\t${testItems.toString()}` + ) + testItems.forEach((testItem: vscode.TestItem) => { + let expectedItem = expectation.find(x => x.id == testItem.id) + if(!expectedItem) { + expect.fail(`${testItem.id} not found in expected items`) + } + testItemMatches(testItem, expectedItem, parent ? `collection(${parent.id})` : undefined) + }) +} + +export function verifyFailure( + index: number, + captor: ArgCaptor3, + expectedTestItem: TestItemExpectation, + expectation: TestFailureExpectation, + message?: string): void +{ + let failure = captor.byCallIndex(index) + let testItem = failure[0] + let failureDetails = failure[1] + let messagePrefix = `${testItem.id} (call: ${index})` + messagePrefix = message ? `${message} - ${messagePrefix}` : messagePrefix + testItemMatches(testItem, expectedTestItem) + if (expectation.message) { + expect(failureDetails.message).to.match(expectation.message, `${messagePrefix}: message`) + } else { + expect(failureDetails.message).to.eq('', "Expected not to receive an exception backtrace") + } + expect(failureDetails.actualOutput).to.eq(expectation.actualOutput, `${messagePrefix}: actualOutput`) + expect(failureDetails.expectedOutput).to.eq(expectation.expectedOutput, `${messagePrefix}: expectedOutput`) + expect(failureDetails.location?.range.start.line).to.eq(expectation.line || 0, `${messagePrefix}: line number`) + expect(failureDetails.location?.uri.fsPath).to.eq(expectedTestItem.file, `${messagePrefix}: path`) +} + +export function setupMockRequest(manager: TestSuiteManager, testId?: string | string[]): vscode.TestRunRequest { + let mockRequest = mock() + if (testId) { + if (Array.isArray(testId)) { + let testItems: vscode.TestItem[] = [] + testId.forEach(id => { + let testItem = manager.getOrCreateTestItem(id) + testItems.push(testItem) + }) + when(mockRequest.include).thenReturn(testItems) + } else { + let testItem = manager.getOrCreateTestItem(testId as string) + when(mockRequest.include).thenReturn([testItem]) + } + } else { + when(mockRequest.include).thenReturn(undefined) + } + when(mockRequest.exclude).thenReturn([]) + let mockRunProfile = mock() + when(mockRunProfile.label).thenReturn('Run') + when(mockRunProfile.kind).thenReturn(vscode.TestRunProfileKind.Run) + when(mockRequest.profile).thenReturn(instance(mockRunProfile)) + return mockRequest +} + +/** + * Argument captors for test state reporting functions + * + * @param mockTestRun mock/spy of the vscode.TestRun used to report test states + * @returns A map of argument captors for test state reporting functions + */ +export function testStateCaptors(mockTestRun: vscode.TestRun) { + let invocationArgs1 = (args: ArgCaptor1, index: number): vscode.TestItem => args.byCallIndex(index)[0] + let invocationArgs2 = (args: ArgCaptor2, index: number): { testItem: vscode.TestItem, duration: number | undefined } => { let abci = args.byCallIndex(index); return {testItem: abci[0], duration: abci[1]}} + let invocationArgs3 = (args: ArgCaptor3, index: number): { testItem: vscode.TestItem, message: vscode.TestMessage, duration: number | undefined } => { let abci = args.byCallIndex(index); return {testItem: abci[0], message: abci[1], duration: abci[2]}} + let captors = { + enqueuedArgs: capture(mockTestRun.enqueued), + erroredArgs: capture(mockTestRun.errored), + failedArgs: capture(mockTestRun.failed), + passedArgs: capture(mockTestRun.passed), + startedArgs: capture(mockTestRun.started), + skippedArgs: capture(mockTestRun.skipped) + } + return { + ...captors, + enqueuedArg: (index: number) => invocationArgs1(captors.enqueuedArgs, index), + erroredArg: (index: number) => invocationArgs3(captors.erroredArgs, index), + failedArg: (index: number) => invocationArgs3(captors.failedArgs, index), + passedArg: (index: number) => invocationArgs2(captors.passedArgs, index), + startedArg: (index: number) => invocationArgs1(captors.startedArgs, index), + skippedArg: (index: number) => invocationArgs1(captors.skippedArgs, index), + } +} diff --git a/test/suite/index.ts b/test/suite/index.ts new file mode 100644 index 0000000..3e454fa --- /dev/null +++ b/test/suite/index.ts @@ -0,0 +1,61 @@ +import path from 'path'; +import Mocha from 'mocha'; +import glob from 'glob'; + +export function run(): Promise { + // Create the mocha test + const mocha = new Mocha({ + ui: 'tdd', + color: true, + diff: true, + bail: false, + fullTrace: true, + timeout: 10000, + }); + + const suite = process.env['TEST_SUITE'] ?? '' + + return new Promise((success, error) => { + console.log(`cwd: ${__dirname}`) + console.log(`Test suite path: ${suite}`) + let testGlob = path.join(suite, '**.test.js') + glob(testGlob, { cwd: __dirname }, (err, files) => { + if (err) { + return error(err); + } + + // Add files to the test suite + files.forEach(f => { + let fPath = path.resolve(__dirname, f) + console.log(fPath) + mocha.addFile(fPath) + }); + + try { + // Run the mocha test + mocha.run(failures => { + if (failures > 0) { + printFailureCount(failures) + error(`${failures} test failures`) + } else { + success() + } + }) + } catch (err) { + error(err); + } + }); + }); +} + +function printFailureCount(failures: number) { + let failureString = `* ${failures} tests failed. *` + let line = new String('*'.repeat(failureString.length)) + let space = `*${new String(' '.repeat(failureString.length - 2))}*` + console.log(line) + console.log(space) + console.log(failureString); + console.log(space) + console.log(line) + console.log("") +} diff --git a/test/suite/minitest/minitest.test.ts b/test/suite/minitest/minitest.test.ts new file mode 100644 index 0000000..0050c4b --- /dev/null +++ b/test/suite/minitest/minitest.test.ts @@ -0,0 +1,350 @@ +import * as vscode from 'vscode'; +import * as path from 'path' +import { anything, instance, mock, reset, verify, when } from '@typestrong/ts-mockito' +import { expect } from 'chai'; +import { after, before, beforeEach } from 'mocha'; + +import { TestLoader } from '../../../src/testLoader'; +import { TestSuiteManager } from '../../../src/testSuiteManager'; +import { TestRunner } from '../../../src/testRunner'; +import { MinitestConfig } from '../../../src/minitest/minitestConfig'; + +import { + logger, + setupMockRequest, + TestFailureExpectation, + testItemCollectionMatches, + TestItemExpectation, + testItemMatches, + testStateCaptors, + verifyFailure +} from '../helpers'; + +suite('Extension Test for Minitest', function() { + let testController: vscode.TestController + let workspaceFolder: vscode.WorkspaceFolder = vscode.workspace.workspaceFolders![0] + let config: MinitestConfig + let testRunner: TestRunner; + let testLoader: TestLoader; + let manager: TestSuiteManager; + let mockTestRun: vscode.TestRun; + let cancellationTokenSource: vscode.CancellationTokenSource; + + const log = logger("off"); + + let expectedPath = (file: string): string => { + return path.resolve( + 'test', + 'fixtures', + 'minitest', + 'test', + file) + } + + let abs_positive_expectation = { + file: expectedPath("abs_test.rb"), + id: "abs_test.rb[4]", + label: "abs positive", + line: 3, + } + let abs_zero_expectation = { + file: expectedPath("abs_test.rb"), + id: "abs_test.rb[8]", + label: "abs 0", + line: 7, + } + let abs_negative_expectation = { + file: expectedPath("abs_test.rb"), + id: "abs_test.rb[12]", + label: "abs negative", + line: 11, + } + let square_2_expectation = { + id: "square/square_test.rb[4]", + file: expectedPath("square/square_test.rb"), + label: "square 2", + line: 3 + } + let square_3_expectation = { + id: "square/square_test.rb[8]", + file: expectedPath("square/square_test.rb"), + label: "square 3", + line: 7 + } + + before(function () { + vscode.workspace.getConfiguration('rubyTestExplorer').update('minitestDirectory', 'test') + vscode.workspace.getConfiguration('rubyTestExplorer').update('filePattern', ['*_test.rb']) + config = new MinitestConfig(path.resolve("ruby"), workspaceFolder) + + testController = vscode.tests.createTestController('ruby-test-explorer-tests', 'Ruby Test Explorer') + mockTestRun = mock() + cancellationTokenSource = new vscode.CancellationTokenSource() + testController.createTestRun = (_: vscode.TestRunRequest, name?: string): vscode.TestRun => { + when(mockTestRun.name).thenReturn(name) + when(mockTestRun.token).thenReturn(cancellationTokenSource.token) + return instance(mockTestRun) + } + + manager = new TestSuiteManager(log, testController, config) + testRunner = new TestRunner(log, manager, workspaceFolder) + testLoader = new TestLoader(log, manager, testRunner); + }) + + beforeEach(function() { + reset(mockTestRun); + }); + + after(function() { + testController.dispose() + cancellationTokenSource.dispose() + }) + + suite('dry run', function() { + beforeEach(function () { + testController.items.replace([]) + }) + + test('Load tests on file resolve request', async function () { + // Populate controller with test files. This would be done by the filesystem globs in the watchers + let createTest = (id: string, label?: string) => { + let item = testController.createTestItem(id, label || id, vscode.Uri.file(expectedPath(id))) + item.canResolveChildren = true + return item + } + let absTestItem = createTest("abs_test.rb") + testController.items.add(absTestItem) + let subfolderItem = createTest("square") + testController.items.add(subfolderItem) + subfolderItem.children.add(createTest("square/square_test.rb", "square_test.rb")) + + // No tests in suite initially, only test files and folders + testItemCollectionMatches(testController.items, + [ + { + file: expectedPath("abs_test.rb"), + id: "abs_test.rb", + //label: "Abs", + label: "abs_test.rb", + canResolveChildren: true, + children: [] + }, + { + file: expectedPath("square"), + id: "square", + label: "square", + canResolveChildren: true, + children: [ + { + file: expectedPath("square/square_test.rb"), + id: "square/square_test.rb", + //label: "Square", + label: "square_test.rb", + canResolveChildren: true, + children: [] + }, + ] + }, + ] + ) + + // Resolve a file (e.g. by clicking on it in the test explorer) + await testLoader.loadTestItem(absTestItem) + + // Tests in that file have now been added to suite + testItemCollectionMatches(testController.items, + [ + { + file: expectedPath("abs_test.rb"), + id: "abs_test.rb", + label: "abs_test.rb", + canResolveChildren: true, + children: [ + abs_positive_expectation, + abs_zero_expectation, + abs_negative_expectation + ] + }, + { + file: expectedPath("square"), + id: "square", + label: "square", + canResolveChildren: true, + children: [ + { + file: expectedPath("square/square_test.rb"), + id: "square/square_test.rb", + label: "square_test.rb", + canResolveChildren: true, + children: [] + }, + ], + }, + ] + ) + }) + + test('Load all tests', async function () { + await testLoader['loadTests']() + + const manager = testController.items + + testItemCollectionMatches(manager, + [ + { + file: expectedPath("abs_test.rb"), + id: "abs_test.rb", + label: "abs_test.rb", + canResolveChildren: true, + children: [ + abs_positive_expectation, + abs_zero_expectation, + abs_negative_expectation + ] + }, + { + file: expectedPath("square"), + id: "square", + label: "square", + canResolveChildren: true, + children: [ + { + file: expectedPath("square/square_test.rb"), + id: "square/square_test.rb", + label: "square_test.rb", + canResolveChildren: true, + children: [ + square_2_expectation, + square_3_expectation + ] + }, + ], + }, + ] + ) + }) + }) + + suite('status events', function() { + before(async function() { + await testLoader['loadTests']() + }) + + suite(`running collections emits correct statuses`, async function() { + test('when running full suite', async function() { + let mockRequest = setupMockRequest(manager) + let request = instance(mockRequest) + await testRunner.runHandler(request, cancellationTokenSource.token) + + verify(mockTestRun.enqueued(anything())).times(8) + verify(mockTestRun.started(anything())).times(5) + verify(mockTestRun.passed(anything(), anything())).times(4) + verify(mockTestRun.failed(anything(), anything(), anything())).times(1) + verify(mockTestRun.errored(anything(), anything(), anything())).times(1) + verify(mockTestRun.skipped(anything())).times(2) + }) + + test('when running all top-level items', async function() { + let mockRequest = setupMockRequest(manager, ["abs_test.rb", "square"]) + let request = instance(mockRequest) + await testRunner.runHandler(request, cancellationTokenSource.token) + + verify(mockTestRun.enqueued(anything())).times(8) + verify(mockTestRun.started(anything())).times(5) + verify(mockTestRun.passed(anything(), anything())).times(4) + verify(mockTestRun.failed(anything(), anything(), anything())).times(1) + verify(mockTestRun.errored(anything(), anything(), anything())).times(1) + verify(mockTestRun.skipped(anything())).times(2) + }) + + test('when running all files', async function() { + let mockRequest = setupMockRequest(manager, ["abs_test.rb", "square/square_test.rb"]) + let request = instance(mockRequest) + await testRunner.runHandler(request, cancellationTokenSource.token) + + // One less 'started' than the other tests as it doesn't include the 'square' folder + verify(mockTestRun.enqueued(anything())).times(7) + verify(mockTestRun.started(anything())).times(5) + verify(mockTestRun.passed(anything(), anything())).times(4) + verify(mockTestRun.failed(anything(), anything(), anything())).times(1) + verify(mockTestRun.errored(anything(), anything(), anything())).times(1) + verify(mockTestRun.skipped(anything())).times(2) + }) + }) + + suite(`running single tests emits correct statuses`, async function() { + let params: {status: string, expectedTest: TestItemExpectation, failureExpectation?: TestFailureExpectation}[] = [ + { + status: "passed", + expectedTest: square_2_expectation, + }, + { + status: "failed", + expectedTest: square_3_expectation, + failureExpectation: { + message: /Expected: 9\s*Actual: 6/, + line: 8, + } + }, + { + status: "passed", + expectedTest: abs_positive_expectation + }, + { + status: "errored", + expectedTest: abs_zero_expectation, + failureExpectation: { + message: /RuntimeError: Abs for zero is not supported/, + line: 8, + } + }, + { + status: "skipped", + expectedTest: abs_negative_expectation + } + ] + for(const {status, expectedTest, failureExpectation} of params) { + test(`id: ${expectedTest.id}, status: ${status}`, async function() { + let mockRequest = setupMockRequest(manager, expectedTest.id) + let request = instance(mockRequest) + await testRunner.runHandler(request, cancellationTokenSource.token) + + switch(status) { + case "passed": + testItemMatches(testStateCaptors(mockTestRun).passedArg(0).testItem, expectedTest) + testItemMatches(testStateCaptors(mockTestRun).passedArg(1).testItem, expectedTest) + verify(mockTestRun.passed(anything(), anything())).times(2) + verify(mockTestRun.failed(anything(), anything(), anything())).times(0) + verify(mockTestRun.errored(anything(), anything(), anything())).times(0) + verify(mockTestRun.skipped(anything())).times(0) + break; + case "failed": + verifyFailure(0, testStateCaptors(mockTestRun).failedArgs, expectedTest, failureExpectation!) + verify(mockTestRun.passed(anything(), anything())).times(0) + verify(mockTestRun.failed(anything(), anything(), anything())).times(1) + verify(mockTestRun.errored(anything(), anything(), anything())).times(0) + verify(mockTestRun.skipped(anything())).times(0) + break; + case "errored": + verifyFailure(0, testStateCaptors(mockTestRun).erroredArgs, expectedTest, failureExpectation!) + verify(mockTestRun.passed(anything(), anything())).times(0) + verify(mockTestRun.failed(anything(), anything(), anything())).times(0) + verify(mockTestRun.errored(anything(), anything(), anything())).times(1) + verify(mockTestRun.skipped(anything())).times(0) + break; + case "skipped": + testItemMatches(testStateCaptors(mockTestRun).skippedArg(0), expectedTest) + verify(mockTestRun.passed(anything(), anything())).times(0) + verify(mockTestRun.failed(anything(), anything(), anything())).times(0) + verify(mockTestRun.errored(anything(), anything(), anything())).times(0) + verify(mockTestRun.skipped(anything())).times(2) + break; + } + expect(testStateCaptors(mockTestRun).startedArg(0).id).to.eq(expectedTest.id) + verify(mockTestRun.started(anything())).times(1) + verify(mockTestRun.enqueued(anything())).times(1) + }) + } + }) + }) +}); diff --git a/test/suite/rspec/rspec.test.ts b/test/suite/rspec/rspec.test.ts new file mode 100644 index 0000000..675b72f --- /dev/null +++ b/test/suite/rspec/rspec.test.ts @@ -0,0 +1,471 @@ +import * as vscode from 'vscode'; +import * as path from 'path' +import { anything, instance, mock, reset, verify, when } from '@typestrong/ts-mockito' +import { after, before, beforeEach } from 'mocha'; +import { expect } from 'chai'; + +import { TestLoader } from '../../../src/testLoader'; +import { TestSuiteManager } from '../../../src/testSuiteManager'; +import { TestRunner } from '../../../src/testRunner'; +import { RspecConfig } from '../../../src/rspec/rspecConfig'; + +import { + logger, + setupMockRequest, + testItemCollectionMatches, + testItemMatches, + testStateCaptors, + verifyFailure, + TestItemExpectation, + TestFailureExpectation +} from '../helpers'; + +suite('Extension Test for RSpec', function() { + let testController: vscode.TestController + let workspaceFolder: vscode.WorkspaceFolder = vscode.workspace.workspaceFolders![0] + let config: RspecConfig + let testRunner: TestRunner; + let testLoader: TestLoader; + let manager: TestSuiteManager; + let mockTestRun: vscode.TestRun; + let cancellationTokenSource: vscode.CancellationTokenSource; + + const log = logger("off"); + + let expectedPath = (file: string): string => { + return path.resolve( + 'test', + 'fixtures', + 'rspec', + 'spec', + file) + } + + let abs_positive_expectation = { + file: expectedPath("abs_spec.rb"), + id: "abs_spec.rb[1:1]", + label: "finds the absolute value of 1", + line: 3, + } + let abs_zero_expectation = { + file: expectedPath("abs_spec.rb"), + id: "abs_spec.rb[1:2]", + label: "finds the absolute value of 0", + line: 7, + } + let abs_negative_expectation = { + file: expectedPath("abs_spec.rb"), + id: "abs_spec.rb[1:3]", + label: "finds the absolute value of -1", + line: 11, + } + let square_2_expectation = { + file: expectedPath("square/square_spec.rb"), + id: "square/square_spec.rb[1:1]", + label: "finds the square of 2", + line: 3, + } + let square_3_expectation = { + file: expectedPath("square/square_spec.rb"), + id: "square/square_spec.rb[1:2]", + label: "finds the square of 3", + line: 7, + } + let contexts_many_expectation = { + file: expectedPath('contexts_spec.rb'), + id: 'contexts_spec.rb[1:1:1:1:1:1:1:1:1:1]', + //label: "doesn't break the extension", + label: "when there are many levels of nested contexts doesn't break the extension", + line: 13, + } + let contexts_fewer_expectation = { + file: expectedPath('contexts_spec.rb'), + id: 'contexts_spec.rb[1:1:1:1:2:1]', + label: "when there are fewer levels of nested contexts test #1", + line: 23, + } + + before(function() { + vscode.workspace.getConfiguration('rubyTestExplorer').update('rspecDirectory', 'spec') + vscode.workspace.getConfiguration('rubyTestExplorer').update('filePattern', ['*_spec.rb']) + config = new RspecConfig(path.resolve("ruby"), workspaceFolder) + + testController = vscode.tests.createTestController('ruby-test-explorer-tests', 'Ruby Test Explorer') + mockTestRun = mock() + cancellationTokenSource = new vscode.CancellationTokenSource() + testController.createTestRun = (_: vscode.TestRunRequest, name?: string): vscode.TestRun => { + when(mockTestRun.name).thenReturn(name) + when(mockTestRun.token).thenReturn(cancellationTokenSource.token) + return instance(mockTestRun) + } + + manager = new TestSuiteManager(log, testController, config) + testRunner = new TestRunner(log, manager, workspaceFolder) + testLoader = new TestLoader(log, manager, testRunner); + }) + + beforeEach(function() { + reset(mockTestRun) + }); + + after(function() { + testController.dispose() + cancellationTokenSource.dispose() + }) + + suite('dry run', function() { + beforeEach(function () { + testController.items.replace([]) + }) + + test('Load tests on file resolve request', async function () { + // Populate controller with test files. This would be done by the filesystem globs in the watchers + let createTest = (id: string, canResolveChildren: boolean, label?: string) => { + let item = testController.createTestItem(id, label || id, vscode.Uri.file(expectedPath(id))) + item.canResolveChildren = canResolveChildren + return item + } + let absSpecItem = createTest("abs_spec.rb", true) + testController.items.add(absSpecItem) + let contextsSpecItem = createTest("contexts_spec.rb", true) + testController.items.add(contextsSpecItem) + let subfolderItem = createTest("square", true) + testController.items.add(subfolderItem) + subfolderItem.children.add(createTest("square/square_spec.rb", true, "square_spec.rb")) + + // No tests in suite initially, just test files and folders + testItemCollectionMatches(testController.items, + [ + { + file: expectedPath("abs_spec.rb"), + id: "abs_spec.rb", + label: "abs_spec.rb", + canResolveChildren: true, + children: [] + }, + { + file: expectedPath("contexts_spec.rb"), + id: "contexts_spec.rb", + label: "contexts_spec.rb", + canResolveChildren: true, + children: [] + }, + { + file: expectedPath("square"), + id: "square", + label: "square", + canResolveChildren: true, + children: [ + { + file: expectedPath("square/square_spec.rb"), + id: "square/square_spec.rb", + label: "square_spec.rb", + canResolveChildren: true, + children: [] + }, + ] + }, + ] + ) + + // Resolve a file (e.g. by clicking on it in the test explorer) + await testLoader.loadTestItem(absSpecItem) + + // Tests in that file have now been added to suite + testItemCollectionMatches(testController.items, + [ + { + file: expectedPath("abs_spec.rb"), + id: "abs_spec.rb", + label: "abs_spec.rb", + canResolveChildren: true, + children: [ + abs_positive_expectation, + abs_zero_expectation, + abs_negative_expectation + ] + }, + { + file: expectedPath("contexts_spec.rb"), + id: "contexts_spec.rb", + label: "contexts_spec.rb", + canResolveChildren: true, + children: [] + }, + { + file: expectedPath("square"), + id: "square", + label: "square", + canResolveChildren: true, + children: [ + { + file: expectedPath("square/square_spec.rb"), + id: "square/square_spec.rb", + label: "square_spec.rb", + canResolveChildren: true, + children: [] + }, + ] + }, + ] + ) + }) + + test('Load all tests', async function () { + await testLoader['loadTests']() + + const manager = testController.items + + testItemCollectionMatches(manager, + [ + { + file: expectedPath("abs_spec.rb"), + id: "abs_spec.rb", + //label: "Abs", + label: "abs_spec.rb", + canResolveChildren: true, + children: [ + abs_positive_expectation, + abs_zero_expectation, + abs_negative_expectation + ] + }, + { + file: expectedPath("contexts_spec.rb"), + id: "contexts_spec.rb", + //label: "Contexts", + label: "contexts_spec.rb", + canResolveChildren: true, + children: [ + { + file: expectedPath("contexts_spec.rb"), + id: "contexts_spec.rb[1:1]", + //label: "when", + label: "contexts_spec.rb[1:1]", + canResolveChildren: true, + children: [ + { + file: expectedPath("contexts_spec.rb"), + id: "contexts_spec.rb[1:1:1]", + //label: "there", + label: "contexts_spec.rb[1:1:1]", + canResolveChildren: true, + children: [ + { + file: expectedPath("contexts_spec.rb"), + id: "contexts_spec.rb[1:1:1:1]", + //label: "are", + label: "contexts_spec.rb[1:1:1:1]", + canResolveChildren: true, + children: [ + { + file: expectedPath("contexts_spec.rb"), + id: "contexts_spec.rb[1:1:1:1:1]", + //label: "many", + label: "contexts_spec.rb[1:1:1:1:1]", + canResolveChildren: true, + children: [ + { + file: expectedPath("contexts_spec.rb"), + id: "contexts_spec.rb[1:1:1:1:1:1]", + //label: "levels", + label: "contexts_spec.rb[1:1:1:1:1:1]", + canResolveChildren: true, + children: [ + { + file: expectedPath("contexts_spec.rb"), + id: "contexts_spec.rb[1:1:1:1:1:1:1]", + //label: "of", + label: "contexts_spec.rb[1:1:1:1:1:1:1]", + canResolveChildren: true, + children: [ + { + file: expectedPath("contexts_spec.rb"), + id: "contexts_spec.rb[1:1:1:1:1:1:1:1]", + //label: "nested", + label: "contexts_spec.rb[1:1:1:1:1:1:1:1]", + canResolveChildren: true, + children: [ + { + file: expectedPath("contexts_spec.rb"), + id: "contexts_spec.rb[1:1:1:1:1:1:1:1:1]", + //label: "contexts", + label: "contexts_spec.rb[1:1:1:1:1:1:1:1:1]", + canResolveChildren: true, + children: [ + contexts_many_expectation, + ] + } + ] + }, + ] + }, + ] + }, + ] + }, + { + file: expectedPath("contexts_spec.rb"), + id: "contexts_spec.rb[1:1:1:1:2]", + //label: "fewer levels of nested contexts", + label: "contexts_spec.rb[1:1:1:1:2]", + canResolveChildren: true, + children: [ + contexts_fewer_expectation, + ] + } + ] + } + ] + } + ] + } + ] + }, + { + file: expectedPath("square"), + id: "square", + label: "square", + canResolveChildren: true, + children: [ + { + file: expectedPath("square/square_spec.rb"), + id: "square/square_spec.rb", + //label: "Square", + label: "square_spec.rb", + canResolveChildren: true, + children: [ + square_2_expectation, + square_3_expectation + ] + } + ] + } + ] + ) + }) + }) + + suite('status events', function() { + before(async function() { + await testLoader['loadTests']() + }) + + suite(`running collections emits correct statuses`, async function() { + test('when running full suite', async function() { + let mockRequest = setupMockRequest(manager) + let request = instance(mockRequest) + await testRunner.runHandler(request, cancellationTokenSource.token) + + verify(mockTestRun.enqueued(anything())).times(20) + verify(mockTestRun.started(anything())).times(7) + verify(mockTestRun.passed(anything(), anything())).times(8) + verify(mockTestRun.failed(anything(), anything(), anything())).times(1) + verify(mockTestRun.errored(anything(), anything(), anything())).times(1) + verify(mockTestRun.skipped(anything())).times(2) + }) + + test('when running all top-level items', async function() { + let mockRequest = setupMockRequest(manager, ["abs_spec.rb", "square"]) + let request = instance(mockRequest) + await testRunner.runHandler(request, cancellationTokenSource.token) + + verify(mockTestRun.enqueued(anything())).times(8) + verify(mockTestRun.started(anything())).times(5) + verify(mockTestRun.passed(anything(), anything())).times(4) + verify(mockTestRun.failed(anything(), anything(), anything())).times(1) + verify(mockTestRun.errored(anything(), anything(), anything())).times(1) + verify(mockTestRun.skipped(anything())).times(2) + }) + + test('when running all files', async function() { + let mockRequest = setupMockRequest(manager, ["abs_spec.rb", "square/square_spec.rb"]) + let request = instance(mockRequest) + await testRunner.runHandler(request, cancellationTokenSource.token) + + verify(mockTestRun.enqueued(anything())).times(7) + // One less 'started' than the other tests as it doesn't include the 'square' folder + verify(mockTestRun.started(anything())).times(5) + verify(mockTestRun.passed(anything(), anything())).times(4) + verify(mockTestRun.failed(anything(), anything(), anything())).times(1) + verify(mockTestRun.errored(anything(), anything(), anything())).times(1) + verify(mockTestRun.skipped(anything())).times(2) + }) + }) + + suite(`running single tests emits correct statuses`, async function() { + let params: {status: string, expectedTest: TestItemExpectation, failureExpectation?: TestFailureExpectation}[] = [ + { + status: "passed", + expectedTest: square_2_expectation, + }, + { + status: "failed", + expectedTest: square_3_expectation, + failureExpectation: { + message: /RSpec::Expectations::ExpectationNotMetError:\s*expected: 9\s*got: 6/, + line: 8, + } + }, + { + status: "passed", + expectedTest: abs_positive_expectation + }, + { + status: "errored", + expectedTest: abs_zero_expectation, + failureExpectation: { + message: /RuntimeError:\s*Abs for zero is not supported/, + line: 8, + } + }, + { + status: "skipped", + expectedTest: abs_negative_expectation + } + ] + for(const {status, expectedTest, failureExpectation} of params) { + test(`id: ${expectedTest.id}, status: ${status}`, async function() { + let mockRequest = setupMockRequest(manager, expectedTest.id) + let request = instance(mockRequest) + await testRunner.runHandler(request, cancellationTokenSource.token) + + switch(status) { + case "passed": + testItemMatches(testStateCaptors(mockTestRun).passedArg(0).testItem, expectedTest) + testItemMatches(testStateCaptors(mockTestRun).passedArg(1).testItem, expectedTest) + verify(mockTestRun.passed(anything(), anything())).times(2) + verify(mockTestRun.failed(anything(), anything(), anything())).times(0) + verify(mockTestRun.errored(anything(), anything(), anything())).times(0) + verify(mockTestRun.skipped(anything())).times(0) + break; + case "failed": + verifyFailure(0, testStateCaptors(mockTestRun).failedArgs, expectedTest, failureExpectation!) + verify(mockTestRun.passed(anything(), anything())).times(0) + verify(mockTestRun.failed(anything(), anything(), anything())).times(1) + verify(mockTestRun.errored(anything(), anything(), anything())).times(0) + verify(mockTestRun.skipped(anything())).times(0) + break; + case "errored": + verifyFailure(0, testStateCaptors(mockTestRun).erroredArgs, expectedTest, failureExpectation!) + verify(mockTestRun.passed(anything(), anything())).times(0) + verify(mockTestRun.failed(anything(), anything(), anything())).times(0) + verify(mockTestRun.errored(anything(), anything(), anything())).times(1) + verify(mockTestRun.skipped(anything())).times(0) + break; + case "skipped": + testItemMatches(testStateCaptors(mockTestRun).skippedArg(0), expectedTest) + verify(mockTestRun.passed(anything(), anything())).times(0) + verify(mockTestRun.failed(anything(), anything(), anything())).times(0) + verify(mockTestRun.errored(anything(), anything(), anything())).times(0) + verify(mockTestRun.skipped(anything())).times(2) + break; + } + expect(testStateCaptors(mockTestRun).startedArg(0).id).to.eq(expectedTest.id) + verify(mockTestRun.started(anything())).times(1) + verify(mockTestRun.enqueued(anything())).times(1) + }) + } + }) + }) +}); diff --git a/test/suite/unitTests/config.test.ts b/test/suite/unitTests/config.test.ts new file mode 100644 index 0000000..18ff6f5 --- /dev/null +++ b/test/suite/unitTests/config.test.ts @@ -0,0 +1,109 @@ +import { expect } from "chai"; +import { spy, when } from '@typestrong/ts-mockito' +import { after, before } from 'mocha'; +import * as vscode from 'vscode' +import * as path from 'path' + +import { Config } from "../../../src/config"; +import { RspecConfig } from "../../../src/rspec/rspecConfig"; +import { logger } from "../helpers"; + +const log = logger("off") + +suite('Config', function() { + let setConfig = (testFramework: string) => { + let spiedWorkspace = spy(vscode.workspace) + when(spiedWorkspace.getConfiguration('rubyTestExplorer', null)) + .thenReturn({ get: (section: string) => testFramework } as vscode.WorkspaceConfiguration) + } + + suite('#getTestFramework()', function() { + test('should return rspec when configuration set to rspec', function() { + let testFramework = "rspec" + setConfig(testFramework) + + expect(Config.getTestFramework(log)).to.eq(testFramework); + }); + + test('should return minitest when configuration set to minitest', function() { + let testFramework = 'minitest' + setConfig(testFramework) + + expect(Config.getTestFramework(log)).to.eq(testFramework); + }); + + test('should return none when configuration set to none', function() { + let testFramework = 'none' + setConfig(testFramework) + + expect(Config.getTestFramework(log)).to.eq(testFramework); + }); + }); + + suite("Rspec specific tests", function() { + const configSection: { get(section: string): any | undefined } | undefined = { + get: (section: string) => { + switch (section) { + case "framework": + return "rspec" + case "filePattern": + return ['*_test.rb', 'test_*.rb'] + default: + return undefined + } + } + } + + test("#getFilePatternArg", function() { + let spiedWorkspace = spy(vscode.workspace) + when(spiedWorkspace.getConfiguration('rubyTestExplorer', null)) + .thenReturn(configSection as vscode.WorkspaceConfiguration) + let config = new RspecConfig(path.resolve('ruby')) + expect(config.getFilePatternArg()).to + .eq("--pattern 'spec/**{,/*/**}/*_test.rb,spec/**{,/*/**}/test_*.rb'") + }) + + suite("#getRelativeTestDirectory()", function() { + test("with no config set, it returns default value", function() { + let config = new RspecConfig(path.resolve('ruby')) + expect(config.getRelativeTestDirectory()).to.eq("spec/") + }) + }) + + suite('#getAbsoluteTestDirectory()', function () { + test('returns path to workspace with relative path appended', function () { + let config = new RspecConfig(path.resolve('ruby')) + expect(config.getAbsoluteTestDirectory()).to.eq(path.resolve('spec')) + }) + }) + + suite('#getTestArguments', function() { + let testController: vscode.TestController + + before(function() { + testController = vscode.tests.createTestController('ruby-test-explorer-tests', 'Ruby Test Explorer') + }) + + after(function() { + testController.dispose() + }) + + test('with no args (run full suite)', function() { + let config = new RspecConfig(path.resolve('ruby')) + expect(config.getTestArguments()).to + .eql(["--pattern 'spec/**{,/*/**}/*_test.rb,spec/**{,/*/**}/test_*.rb'"]) + }) + + test('with file arg', function() { + let config = new RspecConfig(path.resolve('ruby')) + let testId = "test_spec.rb" + let testItem = testController.createTestItem( + testId, + "label", + vscode.Uri.file(path.resolve(config.getAbsoluteTestDirectory(), testId)) + ) + expect(config.getTestArguments([testItem])).to.eql([path.resolve('spec/test_spec.rb')]) + }) + }) + }) +}); diff --git a/test/suite/unitTests/frameworkProcess.test.ts b/test/suite/unitTests/frameworkProcess.test.ts new file mode 100644 index 0000000..4ad0797 --- /dev/null +++ b/test/suite/unitTests/frameworkProcess.test.ts @@ -0,0 +1,558 @@ +import { after, afterEach, before, beforeEach } from 'mocha'; +import { anything, capture, instance, mock, verify, when } from '@typestrong/ts-mockito' +import { expect } from 'chai'; +import { IChildLogger } from '@vscode-logging/logger'; +import * as childProcess from 'child_process'; +import * as fs from 'fs/promises'; +import * as os from 'os'; +import * as vscode from 'vscode' +import * as path from 'path' + +import { Config } from "../../../src/config"; +import { TestSuiteManager } from "../../../src/testSuiteManager"; +import { FrameworkProcess } from '../../../src/frameworkProcess'; +import { Status, TestStatus } from '../../../src/testStatus'; + +import { logger, testItemCollectionMatches, TestItemExpectation } from "../helpers"; + +// JSON Fixtures +import rspecDryRunOutput from '../../fixtures/unitTests/rspec/dryRunOutput.json' +import rspecTestRunOutput from '../../fixtures/unitTests/rspec/testRunOutput.json' +import minitestDryRunOutput from '../../fixtures/unitTests/minitest/dryRunOutput.json' +import minitestTestRunOutput from '../../fixtures/unitTests/minitest/testRunOutput.json' + +const log = logger("off") +const cancellationTokenSource = new vscode.CancellationTokenSource() + +suite('FrameworkProcess', function () { + let manager: TestSuiteManager + let testController: vscode.TestController + let frameworkProcess: FrameworkProcess + let spawnOptions: childProcess.SpawnOptions = {} + + const config = mock() + + afterEach(function() { + if (testController) { + testController.dispose() + } + }) + + suite('#parseAndHandleTestOutput()', function () { + suite('RSpec output', function () { + before(function () { + let relativeTestPath = "spec" + when(config.getRelativeTestDirectory()).thenReturn(relativeTestPath) + when(config.getAbsoluteTestDirectory()).thenReturn(path.resolve(relativeTestPath)) + }) + + beforeEach(function () { + testController = vscode.tests.createTestController('ruby-test-explorer-tests', 'Ruby Test Explorer'); + manager = new TestSuiteManager(log, testController, instance(config)) + frameworkProcess = new FrameworkProcess(log, "testCommand", spawnOptions, cancellationTokenSource.token, manager) + }) + + const expectedTests: TestItemExpectation[] = [ + { + id: "abs_spec.rb", + //label: "Abs", + label: "abs_spec.rb", + file: path.resolve("spec", "abs_spec.rb"), + canResolveChildren: true, + children: [ + { + id: "abs_spec.rb[1:1]", + label: "finds the absolute value of 1", + file: path.resolve("spec", "abs_spec.rb"), + line: 3, + }, + { + id: "abs_spec.rb[1:2]", + label: "finds the absolute value of 0", + file: path.resolve("spec", "abs_spec.rb"), + line: 7, + }, + { + id: "abs_spec.rb[1:3]", + label: "finds the absolute value of -1", + file: path.resolve("spec", "abs_spec.rb"), + line: 11, + } + ] + }, + { + id: "contexts_spec.rb", + //label: "Contexts", + label: "contexts_spec.rb", + file: path.resolve("spec", "contexts_spec.rb"), + canResolveChildren: true, + children: [ + { + id: "contexts_spec.rb[1:1]", + //label: "when", + label: "contexts_spec.rb[1:1]", + file: path.resolve("spec", "contexts_spec.rb"), + canResolveChildren: true, + children: [ + { + id: "contexts_spec.rb[1:1:1]", + //label: "there", + label: "contexts_spec.rb[1:1:1]", + file: path.resolve("spec", "contexts_spec.rb"), + canResolveChildren: true, + children: [ + { + id: "contexts_spec.rb[1:1:1:1]", + //label: "are", + label: "contexts_spec.rb[1:1:1:1]", + file: path.resolve("spec", "contexts_spec.rb"), + canResolveChildren: true, + children: [ + { + id: "contexts_spec.rb[1:1:1:1:1]", + //label: "many", + label: "contexts_spec.rb[1:1:1:1:1]", + file: path.resolve("spec", "contexts_spec.rb"), + canResolveChildren: true, + children: [ + { + id: "contexts_spec.rb[1:1:1:1:1:1]", + //label: "levels", + label: "contexts_spec.rb[1:1:1:1:1:1]", + file: path.resolve("spec", "contexts_spec.rb"), + canResolveChildren: true, + children: [ + { + id: "contexts_spec.rb[1:1:1:1:1:1:1]", + //label: "of", + label: "contexts_spec.rb[1:1:1:1:1:1:1]", + file: path.resolve("spec", "contexts_spec.rb"), + canResolveChildren: true, + children: [ + { + id: "contexts_spec.rb[1:1:1:1:1:1:1:1]", + //label: "nested", + label: "contexts_spec.rb[1:1:1:1:1:1:1:1]", + file: path.resolve("spec", "contexts_spec.rb"), + canResolveChildren: true, + children: [ + { + id: "contexts_spec.rb[1:1:1:1:1:1:1:1:1]", + //label: "contexts", + label: "contexts_spec.rb[1:1:1:1:1:1:1:1:1]", + file: path.resolve("spec", "contexts_spec.rb"), + canResolveChildren: true, + children: [ + { + id: "contexts_spec.rb[1:1:1:1:1:1:1:1:1:1]", + //label: "doesn't break the extension", + label: "when there are many levels of nested contexts doesn't break the extension", + file: path.resolve("spec", "contexts_spec.rb"), + canResolveChildren: false, + line: 13, + }, + ] + }, + ] + }, + ] + }, + ] + }, + ] + }, + { + id: "contexts_spec.rb[1:1:1:1:2]", + //label: "fewer levels of nested contexts", + label: "contexts_spec.rb[1:1:1:1:2]", + file: path.resolve("spec", "contexts_spec.rb"), + canResolveChildren: true, + children: [ + { + id: "contexts_spec.rb[1:1:1:1:2:1]", + label: "when there are fewer levels of nested contexts test #1", + file: path.resolve("spec", "contexts_spec.rb"), + canResolveChildren: false, + line: 23 + }, + ] + }, + ] + }, + ] + }, + ] + } + ] + }, + { + id: "square", + label: "square", + file: path.resolve("spec", "square"), + canResolveChildren: true, + children: [ + { + id: "square/square_spec.rb", + //label: "Square", + label: "square_spec.rb", + file: path.resolve("spec", "square", "square_spec.rb"), + canResolveChildren: true, + children: [ + { + id: "square/square_spec.rb[1:1]", + label: "finds the square of 2", + file: path.resolve("spec", "square", "square_spec.rb"), + line: 3, + }, + { + id: "square/square_spec.rb[1:2]", + label: "finds the square of 3", + file: path.resolve("spec", "square", "square_spec.rb"), + line: 7, + }, + ] + } + ] + }, + ] + + test('parses dry run output correctly', function () { + const output = `START_OF_TEST_JSON${JSON.stringify(rspecDryRunOutput)}END_OF_TEST_JSON` + frameworkProcess['parseAndHandleTestOutput'](output) + testItemCollectionMatches(testController.items, expectedTests) + }) + + test('parses test run output correctly', function () { + const output = `START_OF_TEST_JSON${JSON.stringify(rspecTestRunOutput)}END_OF_TEST_JSON` + frameworkProcess['parseAndHandleTestOutput'](output) + testItemCollectionMatches(testController.items, expectedTests) + }) + }) + + suite('Minitest output - dry run', function () { + before(function () { + let relativeTestPath = "test" + when(config.getRelativeTestDirectory()).thenReturn(relativeTestPath) + when(config.getAbsoluteTestDirectory()).thenReturn(path.resolve(relativeTestPath)) + }) + + beforeEach(function () { + testController = vscode.tests.createTestController('ruby-test-explorer-tests', 'Ruby Test Explorer'); + manager = new TestSuiteManager(log, testController, instance(config)) + frameworkProcess = new FrameworkProcess(log, "testCommand", spawnOptions, cancellationTokenSource.token, manager) + }) + + + + const expectedTests: TestItemExpectation[] = [ + { + id: "square", + label: "square", + file: path.resolve("test", "square"), + canResolveChildren: true, + children: [ + { + id: "square/square_test.rb", + //label: "Square", + label: "square_test.rb", + file: path.resolve("test", "square", "square_test.rb"), + canResolveChildren: true, + children: [ + { + id: "square/square_test.rb[4]", + label: "square 2", + file: path.resolve("test", "square", "square_test.rb"), + line: 3, + }, + { + id: "square/square_test.rb[8]", + label: "square 3", + file: path.resolve("test", "square", "square_test.rb"), + line: 7, + }, + ] + } + ] + }, + { + id: "abs_test.rb", + //label: "Abs", + label: "abs_test.rb", + file: path.resolve("test", "abs_test.rb"), + canResolveChildren: true, + children: [ + { + id: "abs_test.rb[4]", + label: "abs positive", + file: path.resolve("test", "abs_test.rb"), + line: 3, + }, + { + id: "abs_test.rb[8]", + label: "abs 0", + file: path.resolve("test", "abs_test.rb"), + line: 7, + }, + { + id: "abs_test.rb[12]", + label: "abs negative", + file: path.resolve("test", "abs_test.rb"), + line: 11, + } + ] + }, + ] + + test('parses dry run output correctly', function () { + const output = `START_OF_TEST_JSON${JSON.stringify(minitestDryRunOutput)}END_OF_TEST_JSON` + frameworkProcess['parseAndHandleTestOutput'](output) + testItemCollectionMatches(testController.items, expectedTests) + }) + + test('parses test run output correctly', function () { + const output = `START_OF_TEST_JSON${JSON.stringify(minitestTestRunOutput)}END_OF_TEST_JSON` + frameworkProcess['parseAndHandleTestOutput'](output) + testItemCollectionMatches(testController.items, expectedTests) + }) + }) + }) + + suite('error handling', function() { + let tmpDir: string | undefined + let mockLog: IChildLogger + let mockTestManager: TestSuiteManager + let cancellationTokenSource = new vscode.CancellationTokenSource() + + before(async function() { + // Create temp dir in which to put scripts + try { + tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'ruby-test-adapter-')); + } catch (err) { + console.error(err); + } + }); + + after(async function() { + // Delete temp dir and all that's in it + if (tmpDir) { + try { + await fs.rm(tmpDir, { recursive: true, force: true }); + } catch (err) { + console.error(err); + } + } + }) + + beforeEach(function() { + mockLog = mock() + when(mockLog.getChildLogger(anything())).thenReturn(instance(mockLog)) + mockTestManager = mock() + }); + + let echo = (content: string, channel: string = 'stdout'): string => { + let command = 'echo' + if (os.platform() == 'win32' && content.length == 0) { + command = `${command}.` + } else { + command = `${command} "${content}"` + } + if (channel == 'stderr') { + return `${command} 1>&2` + } + return command + } + + let shellCommand = (): string => { + if (os.platform() == 'win32') { + return 'cmd' + } else { + return 'sh' + } + } + + let scriptName = (name: string): string => { + if (os.platform() == 'win32') { + return `${name}.bat` + } else { + return `${name}.sh` + } + } + + let runCommand = async ( + name: string, + content: string[], + statusListener?: (e: TestStatus) => any, + exitCode: number = 0, + onDebugStarted?: () => any) => { + if (tmpDir) { + let script = scriptName(name) + let scriptPath = path.join(tmpDir, script) + let scriptFile = await fs.open(scriptPath, 'w+') + try { + content.push(`exit ${exitCode}`) + await scriptFile.writeFile(content.join("\n")) + await scriptFile.sync() + } finally { + await scriptFile.close() + } + + let command = shellCommand() + + let spawnArgs = { + cwd: tmpDir, + env: process.env + } + + let fp = new FrameworkProcess( + instance(mockLog), command, spawnArgs, cancellationTokenSource.token, instance(mockTestManager) + ) + let listenerDisposable: vscode.Disposable | undefined = undefined + if (statusListener) { + listenerDisposable = fp.testStatusEmitter.event(statusListener) + } + try { + return await fp.startProcess([script], onDebugStarted) + } finally { + if (listenerDisposable) { + listenerDisposable.dispose() + } + fp.dispose() + } + } else { + throw new Error("Missing temp directory for test script") + } + } + + suite('when command has a non-zero exit code', function() { + test('with stdout message', async function() { + const message = [ + 'The tests will not work :(', + 'You won\'t go to space today', + 'Install dependencies' + ] + let statusMessageCount = 0; + let exitCode = 1; + + try { + await runCommand('non-zero-stdout', message.map(x => echo(x)), (_) => {statusMessageCount++}, exitCode) + } catch (err) { + expect((err as Error).message).to.eq(`Child process exited abnormally. Status code: ${exitCode}`) + } + + const [logMessage, logData] = capture(mockLog.error).last(); + expect(logMessage).to.eq('Test process failed to run') + expect(logData).to.eql({ message: message }) + expect(statusMessageCount).to.eq(0) + }); + + test('with stderr message', async function() { + const message = [ + 'What happen?', + 'Someone set us up the bomb!', + 'We get signal', + 'Main screen turn on' + ] + let statusMessageCount = 0; + let exitCode = 10; + + try { + await runCommand('non-zero-stderr', message.map(x => echo(x, 'stderr')), (_) => {statusMessageCount++}, exitCode) + } catch (err) { + expect((err as Error).message).to.eq(`Child process exited abnormally. Status code: ${exitCode}`) + } + + const [logMessage, logData] = capture(mockLog.error).last(); + expect(logMessage).to.eq('Test process failed to run') + expect(logData).to.eql({ message: message }) + expect(statusMessageCount).to.eq(0) + }); + }); + + suite('when command has a zero exit code', function() { + test('with stdout message', async function() { + const messages = [ + 'This should be ignored', + 'RUNNING: with scissors', + 'PANIC: at the disco', + 'PASSED: in the roller rink' + ] + let statusMessageCount = 0; + let exitCode = 0; + let testItem = testController.createTestItem("with scissors", "with scissors") + when(mockTestManager.getOrCreateTestItem(anything())).thenReturn(testItem) + + try { + await runCommand('zero-stdout', messages.map(x => echo(x)), (_) => {statusMessageCount++}, exitCode) + } catch (err) { + expect((err as Error).message).to.eq(`Child process exited abnormally. Status code: ${exitCode}`) + } + + verify(mockLog.error(anything(), anything())).times(0) + verify(mockLog.info(anything(), anything())).times(2) + const [logMessage1, logData1] = capture(mockLog.info).first(); + const [logMessage2, logData2] = capture(mockLog.info).last(); + + expect(logMessage1).to.eq('Test run started - stopped capturing error output') + expect(logData1).to.eql({ event: new TestStatus(testItem, Status.running) }) + expect(logMessage2).to.eq('stdout: %s') + expect(logData2).to.eql('PANIC: at the disco') + expect(statusMessageCount).to.eq(2) + }); + + test('with stderr message', async function() { + const stdoutMessages = [ + 'This should be ignored', + 'RUNNING: with scissors', + 'PANIC: at the disco', + 'PASSED: in the roller rink' + ] + const stderrMessage = "Fast Debugger - the debugger of tomorrow, today!" + + let statusMessageCount = 0; + let debugStarted = false + let exitCode = 0; + let testItem = testController.createTestItem("with scissors", "with scissors") + when(mockTestManager.getOrCreateTestItem(anything())).thenReturn(testItem) + + const scriptContent = [echo(stderrMessage, 'stderr')] + for (const line of stdoutMessages) scriptContent.push(echo(line)) + try { + await runCommand('zero-stdout', scriptContent, (_) => {statusMessageCount++}, exitCode, () => {debugStarted = true}) + } catch (err) { + expect((err as Error).message).to.eq(`Child process exited abnormally. Status code: ${exitCode}`) + } + + verify(mockLog.error(anything(), anything())).times(0) + verify(mockLog.info(anything())).times(1) + verify(mockLog.info(anything(), anything())).times(2) + const [logMessage1, logData1] = capture(mockLog.info).first(); + const [logMessage2, logData2] = capture(mockLog.info).second(); + const [logMessage3, logData3] = capture(mockLog.info).last(); + let logMessages = [logMessage1, logMessage2, logMessage3] + let logData = [logData1, logData2, logData3] + + let expectedLogMessages = [ + 'Notifying debug session that test process is ready to debug', + 'Test run started - stopped capturing error output', + 'stdout: %s' + ] + for (const logMessage of logMessages) { + expect(expectedLogMessages.includes(logMessage)) + .to.eq(true, `log message "${logMessage}" not found`) + } + for (const data of logData) { + if (typeof data == "string") { + expect(data).to.eq('PANIC: at the disco') + } else if (data) { + expect(data).to.eql({ event: new TestStatus(testItem, Status.running) }) + } + } + expect(expectedLogMessages.includes(logMessage2)).to.eq(true, 'second log message') + expect(expectedLogMessages.includes(logMessage3)).to.eq(true, 'third log message') + + expect(statusMessageCount).to.eq(2) + expect(debugStarted).to.eq(true) + }); + }); + }) +}) diff --git a/test/suite/unitTests/testSuiteManager.test.ts b/test/suite/unitTests/testSuiteManager.test.ts new file mode 100644 index 0000000..ac93dac --- /dev/null +++ b/test/suite/unitTests/testSuiteManager.test.ts @@ -0,0 +1,330 @@ +import { expect } from 'chai'; +import { before, beforeEach, afterEach } from 'mocha'; +import { instance, mock, when } from '@typestrong/ts-mockito' +import * as vscode from 'vscode' +import path from 'path' + +import { Config } from '../../../src/config'; +import { TestSuiteManager } from '../../../src/testSuiteManager'; +import { logger, testUriMatches } from '../helpers'; + +const log = logger("off") + +suite('TestSuiteManager', function () { + let mockConfig: Config = mock(); + const config: Config = instance(mockConfig) + let controller: vscode.TestController; + let manager: TestSuiteManager; + + before(function () { + let relativeTestPath = 'path/to/spec' + when(mockConfig.getRelativeTestDirectory()).thenReturn(relativeTestPath) + when(mockConfig.getAbsoluteTestDirectory()).thenReturn(path.resolve(relativeTestPath)) + }); + + beforeEach(function () { + controller = vscode.tests.createTestController('ruby-test-explorer-tests', 'Ruby Test Explorer'); + manager = new TestSuiteManager(log, controller, instance(mockConfig)) + }); + + afterEach(function() { + controller.dispose() + }) + + suite('#normaliseTestId()', function () { + const parameters = [ + { arg: 'test-id', expected: 'test-id' }, + { arg: './test-id', expected: 'test-id' }, + { arg: 'folder/test-id', expected: 'folder/test-id' }, + { arg: './folder/test-id', expected: 'folder/test-id' }, + { arg: 'path/to/spec/test-id', expected: 'test-id' }, + { arg: './path/to/spec/test-id', expected: 'test-id' }, + { arg: 'path/to/spec/folder/test-id', expected: 'folder/test-id' }, + { arg: './path/to/spec/folder/test-id', expected: 'folder/test-id' }, + { arg: './path/to/spec/abs_spec.rb[1:1]', expected: 'abs_spec.rb[1:1]' }, + { arg: './path/to/spec/abs_spec.rb[1]', expected: 'abs_spec.rb[1]' }, + ]; + + parameters.forEach(({ arg, expected }) => { + test(`correctly normalises ${arg} to ${expected}`, function () { + expect(manager.normaliseTestId(arg)).to.eq(expected); + }); + }); + }); + + suite('#deleteTestItem()', function () { + const id = 'test-id' + const label = 'test-label' + + beforeEach(function () { + controller.items.add(controller.createTestItem(id, label)) + }) + + test('deletes only the specified test item', function () { + let secondTestItem = controller.createTestItem('test-id-2', 'test-label-2') + controller.items.add(secondTestItem) + expect(controller.items.size).to.eq(2) + + manager.deleteTestItem(id) + + expect(controller.items.size).to.eq(1) + expect(controller.items.get('test-id-2')).to.eq(secondTestItem) + }) + + test('does nothing if ID not found', function () { + expect(controller.items.size).to.eq(1) + + manager.deleteTestItem('test-id-2') + + expect(controller.items.size).to.eq(1) + }) + }); + + suite('#getTestItem()', function () { + const id = 'test-id' + const label = 'test-label' + const childId = 'folder/child-test' + let testItem: vscode.TestItem + let childItem: vscode.TestItem + let folderItem: vscode.TestItem + + before(function () { + testItem = controller.createTestItem(id, label) + childItem = controller.createTestItem(childId, 'child-test') + folderItem = controller.createTestItem('folder', 'folder') + }) + + beforeEach(function () { + controller.items.add(testItem) + folderItem.children.add(childItem) + controller.items.add(folderItem) + }) + + test('gets the specified test if ID is found', function () { + expect(manager.getTestItem(id)).to.eq(testItem) + }) + + test('returns undefined if ID is not found', function () { + expect(manager.getTestItem('not-found')).to.be.undefined + }) + + test('gets the specified nested test if ID is found', function () { + expect(manager.getTestItem(childId)).to.eq(childItem) + }) + + test('returns undefined if nested ID is not found', function () { + expect(manager.getTestItem('folder/not-found')).to.be.undefined + }) + + test('returns undefined if parent of nested ID is not found', function () { + expect(manager.getTestItem('not-found/child-test')).to.be.undefined + }) + }) + + suite('#getOrCreateTestItem()', function () { + const id = 'test-id.rb' + const label = 'test-label' + const childId = path.join('folder', 'child-test.rb') + let testItem: vscode.TestItem + let childItem: vscode.TestItem + + beforeEach(function () { + testItem = controller.createTestItem(id, label) + childItem = controller.createTestItem(childId, 'child-test') + }) + + test('gets the specified item if ID is found', function () { + controller.items.add(testItem) + expect(manager.getOrCreateTestItem(id)).to.eq(testItem) + }) + + test('creates item if ID is not found', function () { + let testItem = manager.getOrCreateTestItem('not-found') + expect(testItem).to.not.be.undefined + expect(testItem?.id).to.eq('not-found') + testUriMatches(testItem, path.resolve(config.getAbsoluteTestDirectory(), 'not-found')) + }) + + test('gets the specified nested test if ID is found', function () { + let folderItem = controller.createTestItem('folder', 'folder') + controller.items.add(testItem) + folderItem.children.add(childItem) + controller.items.add(folderItem) + + expect(manager.getOrCreateTestItem(childId)).to.eq(childItem) + }) + + test('creates item if nested ID is not found', function () { + let id = path.join('folder', 'not-found.rb') + let folderItem = controller.createTestItem('folder', 'folder') + controller.items.add(folderItem) + + let testItem = manager.getOrCreateTestItem(id) + expect(testItem).to.not.be.undefined + expect(testItem?.id).to.eq(id) + testUriMatches(testItem, path.resolve(config.getAbsoluteTestDirectory(), id)) + }) + + test('creates intermediate items if ID implies contexts', function () { + let fileId = 'not-found' + let contextId = `${fileId}[1:1]` + let testId = `${fileId}[1:1:1]` + + + let testItem = manager.getOrCreateTestItem(testId) + expect(testItem).to.not.be.undefined + expect(testItem?.id).to.eq(testId) + expect(testItem.canResolveChildren).to.eq(false) + testUriMatches(testItem, path.resolve(config.getAbsoluteTestDirectory(), fileId)) + + let contextItem = manager.getTestItem(contextId) + expect(contextItem).to.not.be.undefined + expect(contextItem?.id).to.eq(contextId) + expect(contextItem?.canResolveChildren).to.eq(true) + testUriMatches(testItem, contextItem?.uri?.fsPath) + + let fileItem = manager.getTestItem(fileId) + expect(fileItem).to.not.be.undefined + expect(fileItem?.id).to.eq(fileId) + expect(fileItem?.canResolveChildren).to.eq(true) + testUriMatches(testItem, fileItem?.uri?.fsPath) + }) + + test('creates item and parent if parent of nested file is not found', function () { + let id = path.join('folder', 'not-found.rb') + let testItem = manager.getOrCreateTestItem(id) + expect(testItem).to.not.be.undefined + expect(testItem?.id).to.eq(id) + + let folder = manager.getOrCreateTestItem('folder') + expect(folder?.children.size).to.eq(1) + expect(folder?.children.get(id)).to.eq(testItem) + testUriMatches(testItem, path.resolve(config.getAbsoluteTestDirectory(), id)) + }) + + suite('creates full item tree for specs within files', function () { + let fileId = path.join('folder', 'not-found.rb') + + for (const {suite, location} of [ + {suite: 'minitest', location: '[4]'}, + {suite: 'rspec', location: '[1:2]'}, + ]) { + test(suite, function() { + let id = `${fileId}${location}` + let testItem = manager.getOrCreateTestItem(id) + expect(testItem.id).to.eq(id, 'testItem ID') + expect(testItem.parent?.id).to.eq(fileId, 'file ID') + + let folderItem = manager.getTestItem('folder') + let fileItem = manager.getTestItem(fileId) + expect(folderItem?.children.size).to.eq(1) + expect(fileItem?.children.size).to.eq(1) + testUriMatches(folderItem!, path.resolve(config.getAbsoluteTestDirectory(), 'folder')) + testUriMatches(fileItem!, path.resolve(config.getAbsoluteTestDirectory(), fileId)) + testUriMatches(testItem, path.resolve(config.getAbsoluteTestDirectory(), fileId)) + }) + } + }) + + suite('sets canResolveChildren correctly when creating items', function() { + test('folder', function() { + expect(manager.getOrCreateTestItem('folder').canResolveChildren).to.eq(true) + }); + + test('ruby file', function() { + expect(manager.getOrCreateTestItem('file.rb').canResolveChildren).to.eq(true) + }); + + test('folder and file', function() { + expect(manager.getOrCreateTestItem('folder/file.rb').canResolveChildren).to.eq(true) + expect(manager.getOrCreateTestItem('folder').canResolveChildren).to.eq(true) + }); + + test('test case in file with context (rspec)', function() { + expect(manager.getOrCreateTestItem('file.rb[1:1:1]').canResolveChildren).to.eq(false) + expect(manager.getOrCreateTestItem('file.rb[1:1]').canResolveChildren).to.eq(true) + expect(manager.getOrCreateTestItem('file.rb').canResolveChildren).to.eq(true) + }); + + test('test case in file (minitest)', function() { + expect(manager.getOrCreateTestItem('file.rb[1]').canResolveChildren).to.eq(false) + }); + + test('test case in file (rspec)', function() { + expect(manager.getOrCreateTestItem('file.rb[1:1]').canResolveChildren).to.eq(false) + }); + + test('folder and test case in file with context (rspec)', function() { + expect(manager.getOrCreateTestItem('folder/file.rb[1:1:1]').canResolveChildren).to.eq(false) + expect(manager.getOrCreateTestItem('folder/file.rb[1:1]').canResolveChildren).to.eq(true) + expect(manager.getOrCreateTestItem('folder/file.rb').canResolveChildren).to.eq(true) + expect(manager.getOrCreateTestItem('folder').canResolveChildren).to.eq(true) + }); + + test('folder and test case in file (minitest)', function() { + expect(manager.getOrCreateTestItem('folder/file.rb[1]').canResolveChildren).to.eq(false) + expect(manager.getOrCreateTestItem('folder/file.rb').canResolveChildren).to.eq(true) + expect(manager.getOrCreateTestItem('folder').canResolveChildren).to.eq(true) + }); + + test('folder and test case in file (rspec)', function() { + expect(manager.getOrCreateTestItem('folder/file.rb[1:1]').canResolveChildren).to.eq(false) + expect(manager.getOrCreateTestItem('folder/file.rb').canResolveChildren).to.eq(true) + expect(manager.getOrCreateTestItem('folder').canResolveChildren).to.eq(true) + }); + }) + }) + + suite('#getParentIdsFromId', function() { + test('does nothing for a top level file ID', function() { + let id = "some-file.rb" + expect(manager["getParentIdsFromId"](id)).to.eql([id]) + }) + + test('splits file path segments from folder structure', function() { + let id = path.join("path", "to", "some-file.rb") + expect(manager["getParentIdsFromId"](id)).to.eql( + ['path', path.join("path", "to"), id] + ) + }); + + test('splits RSpec location array', function() { + let id = "some-file.rb[1:2:3]" + expect(manager["getParentIdsFromId"](id)).to.eql( + ['some-file.rb', 'some-file.rb[1:2]', 'some-file.rb[1:2:3]'] + ) + }); + + test('splits file path segments and RSpec location array', function() { + let id = path.join('path', 'to', 'some-file.rb[1:2:3]') + expect(manager['getParentIdsFromId'](id)).to.eql( + [ + 'path', + path.join('path', 'to'), + path.join('path', 'to', 'some-file.rb'), + path.join('path', 'to', 'some-file.rb[1:2]'), + path.join('path', 'to', 'some-file.rb[1:2:3]') + ] + ) + }); + + test('splits Minitest location array', function() { + let id = "some-file.rb[7]" + expect(manager["getParentIdsFromId"](id)).to.eql( + ['some-file.rb', 'some-file.rb[7]'] + ) + }); + + test('splits file path segments and Minitest location array', function() { + let id = path.join('path', 'to', 'some-file.rb[7]') + expect(manager["getParentIdsFromId"](id)).to.eql( + [ + 'path', + path.join('path', 'to'), + path.join('path', 'to', 'some-file.rb'), + path.join('path', 'to', 'some-file.rb[7]') + ] + ) + }); + }) +}); diff --git a/tsconfig.json b/tsconfig.json index 6c32ec1..3dc6d9a 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -11,6 +11,9 @@ "noImplicitReturns": true, "noUnusedLocals": true, "removeComments": true, - "skipLibCheck": true - }, + "skipLibCheck": true, + "esModuleInterop": true, + "resolveJsonModule": true, + "baseUrl": "./", + } }