├── .github ├── ISSUE_TEMPLATE │ └── bug_report.md └── workflows │ └── test.yml ├── .gitignore ├── .vscode ├── launch.json ├── settings.json └── tasks.json ├── .vscodeignore ├── CHANGELOG.md ├── LICENSE ├── README.md ├── bin └── setup ├── custom_formatter.rb ├── img ├── ruby-test-explorer.png └── screenshot.png ├── package-lock.json ├── package.json ├── ruby ├── .vscode │ └── settings.json ├── Gemfile ├── Rakefile ├── debug_minitest.rb ├── debug_rspec.rb ├── minispecs │ ├── nested_spec.rb │ ├── spec_spec.rb │ └── unit_test.rb ├── rspecs │ ├── rspec_spec.rb │ └── unit_test.rb ├── test │ └── minitest │ │ ├── rake_task_test.rb │ │ └── test_helper.rb ├── vscode.rake └── vscode │ ├── minitest.rb │ └── minitest │ ├── reporter.rb │ ├── runner.rb │ └── tests.rb ├── src ├── adapter.ts ├── main.ts ├── minitestTests.ts ├── rspecTests.ts └── tests.ts ├── test ├── fixtures │ ├── minitest │ │ ├── .vscode │ │ │ └── settings.json │ │ ├── Gemfile │ │ ├── Gemfile.lock │ │ ├── Rakefile │ │ ├── lib │ │ │ ├── abs.rb │ │ │ └── square.rb │ │ └── test │ │ │ ├── abs_test.rb │ │ │ ├── square_test.rb │ │ │ └── test_helper.rb │ └── rspec │ │ ├── .vscode │ │ └── settings.json │ │ ├── Gemfile │ │ ├── Gemfile.lock │ │ ├── Rakefile │ │ ├── lib │ │ ├── abs.rb │ │ └── square.rb │ │ └── spec │ │ ├── abs_spec.rb │ │ ├── square_spec.rb │ │ └── test_helper.rb ├── runMinitestTests.ts ├── runRspecTests.ts └── suite │ ├── DummyController.ts │ └── frameworks │ ├── minitest │ ├── index.ts │ └── minitest.test.ts │ └── rspec │ ├── index.ts │ └── rspec.test.ts └── tsconfig.json /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Report an issue with the extension 4 | title: '' 5 | labels: bug 6 | assignees: '' 7 | 8 | --- 9 | 10 | ### Your environment 11 | 12 | - `vscode-ruby-test-adapter` version: 13 | - Ruby version: 14 | - VS Code version: 15 | - Operating System: 16 | - RSpec or Minitest version: 17 | 18 | ### Expected behavior 19 | 20 | ### Actual behavior 21 | 22 | *This section should include any relevant screenshots, code samples, console output, etc. The more information we have to reproduce the better!* 23 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Test 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: {} 8 | 9 | jobs: 10 | test: 11 | runs-on: "ubuntu-latest" 12 | steps: 13 | - uses: actions/checkout@v3 14 | - uses: actions/setup-node@v3 15 | with: 16 | node-version: 16 17 | - name: Set up Ruby 18 | uses: ruby/setup-ruby@v1 19 | with: 20 | ruby-version: '3.0' 21 | - name: Update rubygems & bundler 22 | run: | 23 | ruby -v 24 | gem update --system 25 | - name: Install dependencies 26 | run: | 27 | bin/setup 28 | - name: Compile extension 29 | run: | 30 | npm run build 31 | npm run package 32 | - name: Run minitest tests 33 | run: | 34 | xvfb-run -a node ./out/test/runMinitestTests.js 35 | - name: Run rspec tests 36 | run: | 37 | xvfb-run -a node ./out/test/runRspecTests.js 38 | - name: Run Ruby test 39 | run: | 40 | cd ruby && bundle exec rake 41 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | out/ 3 | .DS_Store 4 | ruby/Gemfile.lock 5 | /.vscode-test 6 | *.vsix 7 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "type": "extensionHost", 6 | "request": "launch", 7 | "name": "Ruby adapter", 8 | "runtimeExecutable": "${execPath}", 9 | "args": [ 10 | "--extensionDevelopmentPath=${workspaceFolder}" 11 | ], 12 | "outFiles": [ 13 | "${workspaceFolder}/out/src" 14 | ] 15 | }, 16 | { 17 | "name": "Run tests for Minitest", 18 | "type": "extensionHost", 19 | "request": "launch", 20 | "runtimeExecutable": "${execPath}", 21 | "args": [ 22 | "--extensionDevelopmentPath=${workspaceFolder}", 23 | "--extensionTestsPath=${workspaceFolder}/out/test/suite/frameworks/minitest/index", 24 | "${workspaceFolder}/test/fixtures/minitest" 25 | ], 26 | "outFiles": ["${workspaceFolder}/out/test/**/*.js"] 27 | }, 28 | { 29 | "name": "Run tests for RSpec", 30 | "type": "extensionHost", 31 | "request": "launch", 32 | "runtimeExecutable": "${execPath}", 33 | "args": [ 34 | "--extensionDevelopmentPath=${workspaceFolder}", 35 | "--extensionTestsPath=${workspaceFolder}/out/test/suite/frameworks/rspec/index", 36 | "${workspaceFolder}/test/fixtures/rspec" 37 | ], 38 | "outFiles": ["${workspaceFolder}/out/test/**/**/*.js"] 39 | } 40 | ] 41 | } 42 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "files.exclude": { 3 | ".git": true, 4 | "node_modules": true, 5 | "out": true 6 | }, 7 | "typescript.tsdk": "node_modules/typescript/lib", 8 | "[ruby]": { 9 | "editor.formatOnSave": false 10 | }, 11 | "ruby.format": false, 12 | "ruby.lint": { 13 | "rubocop": false 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | // See https://go.microsoft.com/fwlink/?LinkId=733558 3 | // for the documentation about the tasks.json format 4 | "version": "2.0.0", 5 | "tasks": [ 6 | { 7 | "type": "npm", 8 | "script": "watch", 9 | "problemMatcher": [ 10 | "$tsc-watch" 11 | ], 12 | "isBackground": true, 13 | "runOptions": { 14 | "runOn": "folderOpen" 15 | } 16 | } 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /.vscodeignore: -------------------------------------------------------------------------------- 1 | src/** 2 | test/** 3 | **/*.map 4 | package-lock.json 5 | tsconfig.json 6 | .vscode/** 7 | .gitignore 8 | *.vsix 9 | out/test/** 10 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | All notable changes to this project will be documented in this file. 3 | 4 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). 5 | 6 | ## [Unreleased] 7 | 8 | ## [0.9.2] - 2023-01-17 9 | ### Fixed 10 | - Fix loading tests when symlinks are involved. Thanks [@naveg](https://github.com/naveg)! ([#115](https://github.com/connorshea/vscode-ruby-test-adapter/pull/115)) 11 | 12 | ## [0.9.1] - 2022-08-04 13 | ### Fixed 14 | - Fix extension failures related to the way Minitest sets up its seed (or in this case, doesn't). Thanks [@blowmage](https://github.com/blowmage), [@pnomolos](https://github.com/pnomolos), and [@SergeyBurtsev](https://github.com/SergeyBurtsev)! ([#100](https://github.com/connorshea/vscode-ruby-test-adapter/pull/100)) 15 | 16 | ### Internal 17 | - Fix CI failures due to a mistake in `bin/setup`. ([#103](https://github.com/connorshea/vscode-ruby-test-adapter/pull/103)) 18 | 19 | ## [0.9.0] - 2021-05-14 20 | ### Added 21 | - Add `rubyTestExplorer.debugCommand` configuration. Allows customizing how the Ruby debugger is accessed, e.g. `bundle exec rdebug-ide`. Thanks [@Juice10](https://github.com/Juice10)! ([#72](https://github.com/connorshea/vscode-ruby-test-adapter/pull/72)) 22 | 23 | ### Changed 24 | - Do not replace underscores in Minitest test names that contain whitespace (e.g. `'Object#foo_bar should work'`). Thanks [@jochenseeber](https://github.com/jochenseeber)! ([#67](https://github.com/connorshea/vscode-ruby-test-adapter/pull/67)) 25 | - Remove colon and leading whitespace from beginning of Minitest test names, when relevant. Thanks [@jochenseeber](https://github.com/jochenseeber)! ([#62](https://github.com/connorshea/vscode-ruby-test-adapter/pull/62)) 26 | 27 | ### Fixed 28 | - Make test paths platform agnostic so test reloading works on Windows. Thanks [@cscorley](https://github.com/cscorley)! ([#64](https://github.com/connorshea/vscode-ruby-test-adapter/pull/64)) 29 | 30 | ### Internal 31 | - Add `.vsix` files to `.gitignore` and `.vscodeignore`. Thanks [@jochenseeber](https://github.com/jochenseeber)! ([#61](https://github.com/connorshea/vscode-ruby-test-adapter/pull/61)) 32 | 33 | ## [0.8.1] - 2021-05-14 34 | ### Changed 35 | - Increase the minimum VS Code version required for the extension to v1.54 (the February 2021 release). 36 | - Disable the extension in Untrusted Workspaces and Virtual Workspaces. It shouldn't be enabled if the code in the repo isn't trusted (since it essentially executes arbitrary code on-load) and cannot work in a Virtual Workspace since Ruby gems need to be installed and test files must all be available. 37 | 38 | ### Internal 39 | - Add an automated test suite for the extension. Thanks [@soutaro](https://github.com/soutaro)! ([#74](https://github.com/connorshea/vscode-ruby-test-adapter/pull/74)) 40 | 41 | ## [0.8.0] - 2020-10-25 42 | ### Added 43 | - Add support for debugging specs. Thanks [@baelter](https://github.com/baelter) and [@CezaryGapinski](https://github.com/CezaryGapinski)! ([#51](https://github.com/connorshea/vscode-ruby-test-adapter/pull/51)) 44 | - Add `filePattern` configuration support for RSpec. ([#51](https://github.com/connorshea/vscode-ruby-test-adapter/pull/51)) 45 | 46 | ### Changed 47 | - **BREAKING**: `minitestFilePattern` renamed to `filePattern` to make it work for both test frameworks we support. ([#51](https://github.com/connorshea/vscode-ruby-test-adapter/pull/51)) 48 | 49 | ### Fixed 50 | - Fix extension failing when `TESTS_DIR` environment variable wasn't set correctly. Thanks [@dwarburt](https://github.com/dwarburt)! ([#47](https://github.com/connorshea/vscode-ruby-test-adapter/pull/47)) 51 | - Fix `EXT_DIR` environment variable and line number handling in minitests for Windows OS. Thanks [@CezaryGapinski](https://github.com/CezaryGapinski)! ([#51](https://github.com/connorshea/vscode-ruby-test-adapter/pull/51)) 52 | 53 | ### Internal 54 | - Add RSpec tests. 55 | 56 | ## [0.7.1] - 2020-02-12 57 | ### Changed 58 | - Improve the way errors are handled when loading a project's RSpec tests. ([#43](https://github.com/connorshea/vscode-ruby-test-adapter/pull/43)) 59 | 60 | ## [0.7.0] - 2020-02-12 61 | ### Added 62 | - Add support for `Minitest::Spec`-style tests and allow configuration of the minitest files' file pattern with `minitestFilePattern`. Thanks [@baelter](https://github.com/baelter)! ([#34](https://github.com/connorshea/vscode-ruby-test-adapter/pull/34)) 63 | 64 | ### Fixed 65 | - Fix minitest nested tests. Thanks [@baelter](https://github.com/baelter)! ([#37](https://github.com/connorshea/vscode-ruby-test-adapter/pull/37)) 66 | - Fix tests not running properly when the path had a space in it. Thanks [@noniq](https://github.com/noniq)! ([#42](https://github.com/connorshea/vscode-ruby-test-adapter/pull/42)) 67 | 68 | ## [0.6.1] - 2019-12-10 69 | ### Changed 70 | - Update npm dependencies. 71 | 72 | ### Fixed 73 | - Fix a typo in the README config table. Thanks [@maryamkaka](https://github.com/maryamkaka)! ([#24](https://github.com/connorshea/vscode-ruby-test-adapter/pull/24)) 74 | - Fix a missing require and detection of tests when test files start with `test_` rather than ending with it. Thanks [@agilbert201](https://github.com/agilbert201)! ([#33](https://github.com/connorshea/vscode-ruby-test-adapter/pull/33)) 75 | 76 | ## [0.6.0] - 2019-07-07 77 | ### Added 78 | - Add support for multi-root workspaces. The test adapter should now work properly when run with multiple workspaces open at once. 79 | 80 | ## [0.5.6] - 2019-06-22 81 | ### Fixed 82 | - Fix error when running Minitest suites if JSON wasn't loaded. Thanks [@afuerstenau](https://github.com/afuerstenau)! ([#19](https://github.com/connorshea/vscode-ruby-test-adapter/pull/19)) 83 | 84 | ## [0.5.5] - 2019-06-11 85 | ### Fixed 86 | - Fix Minitest integration relying implicitly on Rails/ActiveSupport functionality. Thanks [@ttilberg](https://github.com/ttilberg)! ([#17](https://github.com/connorshea/vscode-ruby-test-adapter/pull/17)) 87 | 88 | ## [0.5.4] - 2019-06-04 89 | ### Fixed 90 | - Fix the 'open source file' button not working on test suites. 91 | 92 | ## [0.5.3] - 2019-06-03 93 | ### Fixed 94 | - Fix the problem where the test runner was able to get into a state where the tests would never finish, leading to a "Stop" button that was stuck forever. 95 | 96 | ## [0.5.2] - 2019-06-01 97 | ### Fixed 98 | - Fix an issue where the test runner could get stuck without being able to finish. 99 | 100 | ## [0.5.1] - 2019-06-01 101 | ### Added 102 | - Add line decorations in RSpec file where a given test failed. Includes the error message. 103 | 104 | ### Fixed 105 | - Catch an error that can occur while auto-detecting the test framework where `bundle list` can fail. 106 | 107 | ## [0.5.0] - 2019-06-01 108 | ### Added 109 | - Add Minitest support. Thanks [@cristianbica](https://github.com/cristianbica)! ([#14](https://github.com/connorshea/vscode-ruby-test-adapter/pull/14)) 110 | - The test framework is detected automatically based on the gems installed in the current Bundler environment, no changes should be necessary to continue using the extension. You can also override the test framework manually with the `testFramework` setting if necessary. 111 | - Add an automated test watcher, tests will now reload automatically when a file in the configured test/spec directory changes. 112 | 113 | ### Changed 114 | - [BREAKING] Renamed `specDirectory` config option to `rspecDirectory` for consistency. If you've configured a special RSpec directory you'll need to change the setting name. 115 | 116 | ## [0.4.6] - 2019-05-24 117 | ### Fixed 118 | - Fix `ActiveSupport#to_json` error when test is not wrapped with a string-based describe/context block. Thanks [@apolzon](https://github.com/apolzon)! 119 | 120 | ## [0.4.5] - 2019-05-22 121 | ### Added 122 | - Add Troubleshooting section to extension README. 123 | 124 | ## [0.4.4] - 2019-05-22 125 | ### Added 126 | - Add better logging throughout the extension. 127 | 128 | ### Fixed 129 | - Fix a bug that caused an RSpec 'dry-run' to be run before every test run. Test suites should run a bit faster now. 130 | 131 | ## [0.4.3] - 2019-05-17 132 | ### Fixed 133 | - Fix parsing initial JSON so it's less likely to fail when there are other curly braces in the RSpec output. 134 | - Fix 'max buffer' errors by raising the max buffer size to 64MB. Hopefully no one ever hits this. 135 | 136 | ## [0.4.2] - 2019-05-14 137 | ### Changed 138 | - Run tests in a given file at once, rather than one-at-a-time. This makes running tests for a file much faster than it was previously. 139 | 140 | ## [0.4.1] - 2019-05-14 141 | ### Added 142 | - Add support for cancelling a test run. 143 | 144 | ## [0.4.0] - 2019-05-13 145 | ### Added 146 | - The extension now uses a custom RSpec formatter. This is mostly useful for future enhancements. 147 | 148 | ### Changed 149 | - Test statuses will now be updated live as the test suite is run. 150 | 151 | ## [0.3.3] - 2019-05-12 152 | ### Changed 153 | - Strip repetitive model names from test labels. e.g. "GameGenre Validations blah blah blah" becomes "Validations blah blah blah", since GameGenre can be assumed from the filename. 154 | 155 | ## [0.3.2] - 2019-05-12 156 | ### Fixed 157 | - Fix randomized ordering of tests in the explorer by having RSpec order be defined when getting the tests initially. 158 | 159 | ## [0.3.1] - 2019-05-11 160 | ### Fixed 161 | - Only activate the extension when Ruby files are present. This prevents warning messages about initializing RSpec in projects that don't use Ruby. 162 | 163 | ## [0.3.0] - 2019-05-11 164 | ### Added 165 | - Add proper hierarchy information based on the subdirectory of the spec file. 166 | - Add a warning message if the extension fails to initialize RSpec. 167 | - Add configuration setting for the `spec` directory. 168 | 169 | ## [0.2.3] - 2019-05-08 170 | ### Changed 171 | - Add information to README. 172 | - Improve extension icon. 173 | 174 | ## [0.2.2] - 2019-04-27 175 | ### Changed 176 | - Add setup instructions to the README. 177 | 178 | ## [0.2.1] - 2019-04-27 179 | ### Fixed 180 | - Fix `rspecCommand` configuration not working. 181 | 182 | ## [0.2.0] - 2019-04-27 183 | ### Added 184 | - Add configuration option `rubyTestExplorer.rspecCommand` for setting a custom Rspec command for the runner (default is `bundle exec rspec`). 185 | 186 | ## [0.1.0] - 2019-04-27 187 | 188 | Initial release. 189 | 190 | [Unreleased]: https://github.com/connorshea/vscode-ruby-test-adapter/compare/v0.9.1...HEAD 191 | [0.9.1]: https://github.com/connorshea/vscode-ruby-test-adapter/compare/v0.9.0...v0.9.1 192 | [0.9.0]: https://github.com/connorshea/vscode-ruby-test-adapter/compare/v0.8.1...v0.9.0 193 | [0.8.1]: https://github.com/connorshea/vscode-ruby-test-adapter/compare/v0.8.0...v0.8.1 194 | [0.8.0]: https://github.com/connorshea/vscode-ruby-test-adapter/compare/v0.7.1...v0.8.0 195 | [0.7.1]: https://github.com/connorshea/vscode-ruby-test-adapter/compare/v0.7.0...v0.7.1 196 | [0.7.0]: https://github.com/connorshea/vscode-ruby-test-adapter/compare/v0.6.1...v0.7.0 197 | [0.6.1]: https://github.com/connorshea/vscode-ruby-test-adapter/compare/v0.6.0...v0.6.1 198 | [0.6.0]: https://github.com/connorshea/vscode-ruby-test-adapter/compare/v0.5.6...v0.6.0 199 | [0.5.6]: https://github.com/connorshea/vscode-ruby-test-adapter/compare/v0.5.5...v0.5.6 200 | [0.5.5]: https://github.com/connorshea/vscode-ruby-test-adapter/compare/v0.5.4...v0.5.5 201 | [0.5.4]: https://github.com/connorshea/vscode-ruby-test-adapter/compare/v0.5.3...v0.5.4 202 | [0.5.3]: https://github.com/connorshea/vscode-ruby-test-adapter/compare/v0.5.2...v0.5.3 203 | [0.5.2]: https://github.com/connorshea/vscode-ruby-test-adapter/compare/v0.5.1...v0.5.2 204 | [0.5.1]: https://github.com/connorshea/vscode-ruby-test-adapter/compare/v0.5.0...v0.5.1 205 | [0.5.0]: https://github.com/connorshea/vscode-ruby-test-adapter/compare/v0.4.6...v0.5.0 206 | [0.4.6]: https://github.com/connorshea/vscode-ruby-test-adapter/compare/v0.4.5...v0.4.6 207 | [0.4.5]: https://github.com/connorshea/vscode-ruby-test-adapter/compare/v0.4.4...v0.4.5 208 | [0.4.4]: https://github.com/connorshea/vscode-ruby-test-adapter/compare/v0.4.3...v0.4.4 209 | [0.4.3]: https://github.com/connorshea/vscode-ruby-test-adapter/compare/v0.4.2...v0.4.3 210 | [0.4.2]: https://github.com/connorshea/vscode-ruby-test-adapter/compare/v0.4.1...v0.4.2 211 | [0.4.1]: https://github.com/connorshea/vscode-ruby-test-adapter/compare/v0.4.0...v0.4.1 212 | [0.4.0]: https://github.com/connorshea/vscode-ruby-test-adapter/compare/v0.3.3...v0.4.0 213 | [0.3.3]: https://github.com/connorshea/vscode-ruby-test-adapter/compare/v0.3.2...v0.3.3 214 | [0.3.2]: https://github.com/connorshea/vscode-ruby-test-adapter/compare/v0.3.1...v0.3.2 215 | [0.3.1]: https://github.com/connorshea/vscode-ruby-test-adapter/compare/v0.3.0...v0.3.1 216 | [0.3.0]: https://github.com/connorshea/vscode-ruby-test-adapter/compare/v0.2.3...v0.3.0 217 | [0.2.3]: https://github.com/connorshea/vscode-ruby-test-adapter/compare/v0.2.2...v0.2.3 218 | [0.2.2]: https://github.com/connorshea/vscode-ruby-test-adapter/compare/v0.2.1...v0.2.2 219 | [0.2.1]: https://github.com/connorshea/vscode-ruby-test-adapter/compare/v0.2.0...v0.2.1 220 | [0.2.0]: https://github.com/connorshea/vscode-ruby-test-adapter/compare/v0.1.0...v0.2.0 221 | [0.1.0]: https://github.com/connorshea/vscode-ruby-test-adapter/compare/2cc6839...v0.1.0 222 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019-2021 Connor Shea 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Ruby Test Explorer 2 | **[Install it from the VS Code Marketplace.](https://marketplace.visualstudio.com/items?itemName=connorshea.vscode-ruby-test-adapter)** 3 | 4 | This is a Ruby Test Explorer extension for the [VS Code Test Explorer](https://marketplace.visualstudio.com/items?itemName=hbenl.vscode-test-explorer) extension. 5 | 6 | ![An example screenshot of the extension in use](/img/screenshot.png) 7 | 8 | The extension supports the RSpec and Minitest test frameworks. 9 | 10 | ## Setup 11 | 12 | ### RSpec 13 | 14 | The extension needs Ruby and the `rspec-core` gem installed (and any other dependencies required by your test suite). It's been tested with Ruby 2.6 and Rspec 3.8, but it should work with most recent versions of Ruby and all versions of Rspec 3.x above 3.6.0 (versions before 3.6.0 do not currently work because they don't expose an `id` property for tests in the JSON formatter). 15 | 16 | By default, you need to have `rspec` installed via Bundler with a `Gemfile` and `bundle install`, otherwise `bundle exec rspec` won't work. If you want to run your Rspec tests with a command other than `bundle exec rspec`, you can configure the command with the `rubyTestExplorer.rspecCommand` setting. 17 | 18 | ### Minitest 19 | 20 | The extension needs Ruby and the `minitest` gem installed (and any other dependencies required by your test suite). It's been tested with Ruby 2.5 and 2.6, and Minitest 5.x. It should work with most recent versions of Ruby and Minitest. 21 | 22 | ## Features 23 | 24 | Currently supported: 25 | 26 | - Support for RSpec and Minitest suites. 27 | - Automatic detection of test framework (based on gems listed by `bundle list`), as well as manual override if necessary. 28 | - Running individual tests. 29 | - Running full test suite. 30 | - Running tests for a specific file. 31 | - Viewing test output for failed tests (click the test in the test explorer sidebar to open the Output view). 32 | - Line decorations in test files when a test fails. 33 | - Displaying test statuses. Success, failure, and pending (called 'skipped' in the extension). 34 | - Live test status updates as the test suite runs. 35 | - File locations for each test. 36 | - Configurable RSpec command. 37 | - Configurable RSpec `spec/` directory. 38 | - Configurable Minitest command. 39 | - Configurable Minitest `test/` directory. 40 | - Test hierarchy information. 41 | - Automatic reloading of test suite info when a file in the test directory changes. 42 | - Multi-root workspaces. 43 | 44 | ## Configuration 45 | 46 | The following configuration options are available: 47 | 48 | Property | Description 49 | ---------------------------------------|--------------------------------------------------------------- 50 | `rubyTestExplorer.logpanel` | Whether to write diagnotic logs to an output panel. 51 | `rubyTestExplorer.logfile` | Write diagnostic logs to the given file. 52 | `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. 53 | `rubyTestExplorer.filePattern` | Define the pattern to match test files by, for example `["*_test.rb", "test_*.rb", "*_spec.rb"]`. 54 | `rubyTestExplorer.debuggerHost` | Define the host to connect the debugger to, for example `127.0.0.1`. 55 | `rubyTestExplorer.debuggerPort` | Define the port to connect the debugger to, for example `1234`. 56 | `rubyTestExplorer.debugCommand` | Define how to run rdebug-ide, for example `rdebug-ide` or `bundle exec rdebug-ide`. 57 | `rubyTestExplorer.rspecCommand` | Define the command to run RSpec tests with, for example `bundle exec rspec`, `spring rspec`, or `rspec`. 58 | `rubyTestExplorer.rspecDirectory` | Define the relative directory of the specs in a given workspace, for example `./spec/`. 59 | `rubyTestExplorer.minitestCommand` | Define how to run Minitest with Rake, for example `./bin/rake`, `bundle exec rake` or `rake`. Must be a Rake command. 60 | `rubyTestExplorer.minitestDirectory` | Define the relative location of your `test` directory, for example `./test/`. 61 | 62 | ## Troubleshooting 63 | 64 | If the extension doesn't work for you, here are a few things you can try: 65 | 66 | - Make sure you've run `bundle install` and that any gems specified in your `Gemfile.lock` have been installed (assuming you're using Bundler). 67 | - Disable parallel tests if you are using minitest. To do this set `parallelize(workers: 1)` in `test_helper.rb`, or add `PARALLEL_WORKERS=1` to the `rubyTestExplorer.minitestCommand`. 68 | - Enable the `rubyTestExplorer.logpanel` config setting and take a look at the output in Output > Ruby Test Explorer Log. This should show what the extension is doing and provide more context on what's happening behind the scenes. (You can alternatively use `rubyTestExplorer.logfile` to log to a specific file instead). 69 | - Check the VS Code Developer Tools (Command Palette > 'Developer: Toggle Developer Tools') for any JSON parsing errors, or anything else that looks like it might come from the extension. That could be a bug in the extension, or a problem with your setup. 70 | - If you're using RSpec, make sure you're using a recent version of the `rspec-core` gem. If you're on a version prior to 3.6.0, the extension may not work. 71 | - If you're using RSpec, make sure that the RSpec command and `spec` directory are configured correctly. By default, tests are run with `bundle exec rspec` and the tests are assumed to be in the `./spec/` directory. You can configure these with `rubyTestExplorer.rspecCommand` and `rubyTestExplorer.rspecDirectory` respectively. 72 | - If the test suite info isn't loading, your `testFramework` config may be set to `none` or the auto-detect may be failing to determine the test framework. Try setting the `testFramework` config to `rspec` or `minitest` depending on what you want to use. 73 | 74 | If all else fails or you suspect something is broken with the extension, please feel free to open an issue! :) 75 | 76 | ## Contributing 77 | 78 | You'll need VS Code, Node (any version >= 12 should probably work), and Ruby installed. 79 | 80 | - Clone the repository: `git clone https://github.com/connorshea/vscode-ruby-test-adapter` 81 | - Run `bin/setup` to install dependencies. 82 | - Open the directory in VS Code. 83 | - Run `npm run watch` or start the `watch` Task in VS Code to get the TypeScript compiler running. 84 | - Go to the Debug section in the sidebar and run "Ruby adapter". This will start a separate VS Code instance for testing the extension in. It gets updated code whenever "Reload Window" is run in the Command Palette. 85 | - You'll need a Ruby project if you want to actually use the extension to run tests, I generally use my project [vglist](https://github.com/connorshea/vglist) for testing, but any Ruby project with RSpec or Minitest tests will work. 86 | 87 | This extension is based on [the example test adapter](https://github.com/hbenl/vscode-example-test-adapter), it may be useful to check that repository for more information. Test adapters for other languages may also be useful references. 88 | 89 | ### Running tests 90 | 91 | There are two groups of tests included in the repository. 92 | 93 | - Tests for Ruby scripts to collect test information and run tests. Run with `bundle exec rake` in `ruby` directory. 94 | - Tests for VS Code extension which invokes the Ruby scripts. Run from VS Code's debug panel with the "Run tests for" configurations. 95 | - There are separate debug configurations for each supported test framework. 96 | - 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. 97 | 98 | You can see `.github/workflows/test.yml` for CI configurations. 99 | 100 | ### Publishing a new version 101 | 102 | See [the VS Code extension docs](https://code.visualstudio.com/api/working-with-extensions/publishing-extension) for more info. 103 | 104 | Before publishing, make sure to update the `CHANGELOG.md` file. You also need to be logged in to `vsce`. When creating a Personal Access Token to log in, make sure to give it access to _all organizations_ in your Azure DevOps account. Otherwise, it won't work correctly. 105 | 106 | `vsce publish VERSION`, e.g. `vsce publish 1.0.0` will automatically handle creating the git commit and git tag, updating the `package.json`, and publishing the new version to the Visual Studio Marketplace. You'll need to manually run `git push` and `git push --tags` after publishing. 107 | 108 | Alternatively, you can bump the extension version with `vsce publish major`, `vsce publish minor`, or `vsce publish patch`. 109 | -------------------------------------------------------------------------------- /bin/setup: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -euo pipefail 3 | IFS=$'\n\t' 4 | set -vx 5 | 6 | npm install 7 | bundle install --gemfile=ruby/Gemfile 8 | bundle install --gemfile=test/fixtures/rspec/Gemfile 9 | bundle install --gemfile=test/fixtures/minitest/Gemfile 10 | -------------------------------------------------------------------------------- /custom_formatter.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'rspec/core' 4 | require 'rspec/core/formatters/base_formatter' 5 | require 'json' 6 | 7 | class CustomFormatter < RSpec::Core::Formatters::BaseFormatter 8 | RSpec::Core::Formatters.register self, 9 | :message, 10 | :dump_summary, 11 | :stop, 12 | :seed, 13 | :close, 14 | :example_passed, 15 | :example_failed, 16 | :example_pending 17 | 18 | attr_reader :output_hash 19 | 20 | def initialize(output) 21 | super 22 | @output_hash = { 23 | version: RSpec::Core::Version::STRING 24 | } 25 | end 26 | 27 | def message(notification) 28 | (@output_hash[:messages] ||= []) << notification.message 29 | end 30 | 31 | def dump_summary(summary) 32 | @output_hash[:summary] = { 33 | duration: summary.duration, 34 | example_count: summary.example_count, 35 | failure_count: summary.failure_count, 36 | pending_count: summary.pending_count, 37 | errors_outside_of_examples_count: summary.errors_outside_of_examples_count 38 | } 39 | @output_hash[:summary_line] = summary.totals_line 40 | end 41 | 42 | def stop(notification) 43 | @output_hash[:examples] = notification.examples.map do |example| 44 | format_example(example).tap do |hash| 45 | e = example.exception 46 | if e 47 | hash[:exception] = { 48 | class: e.class.name, 49 | message: e.message, 50 | backtrace: e.backtrace 51 | } 52 | end 53 | end 54 | end 55 | end 56 | 57 | def seed(notification) 58 | return unless notification.seed_used? 59 | 60 | @output_hash[:seed] = notification.seed 61 | end 62 | 63 | def close(_notification) 64 | output.write "START_OF_TEST_JSON#{@output_hash.to_json}END_OF_TEST_JSON\n" 65 | end 66 | 67 | def example_passed(notification) 68 | output.write "PASSED: #{notification.example.id}\n" 69 | end 70 | 71 | def example_failed(notification) 72 | output.write "FAILED: #{notification.example.id}\n" 73 | # This isn't exposed for simplicity, need to figure out how to handle this later. 74 | # output.write "#{notification.exception.backtrace.to_json}\n" 75 | end 76 | 77 | def example_pending(notification) 78 | output.write "PENDING: #{notification.example.id}\n" 79 | end 80 | 81 | private 82 | 83 | # Properties of example: 84 | # block 85 | # description_args 86 | # description 87 | # full_description 88 | # described_class 89 | # file_path 90 | # line_number 91 | # location 92 | # absolute_file_path 93 | # rerun_file_path 94 | # scoped_id 95 | # type 96 | # execution_result 97 | # example_group 98 | # shared_group_inclusion_backtrace 99 | # last_run_status 100 | def format_example(example) 101 | { 102 | id: example.id, 103 | description: example.description, 104 | full_description: example.full_description, 105 | status: example.execution_result.status.to_s, 106 | file_path: example.metadata[:file_path], 107 | line_number: example.metadata[:line_number], 108 | type: example.metadata[:type], 109 | pending_message: example.execution_result.pending_message 110 | } 111 | end 112 | end 113 | -------------------------------------------------------------------------------- /img/ruby-test-explorer.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/connorshea/vscode-ruby-test-adapter/9a1fc573033619393a6a8bd8672ef44fc2efd26f/img/ruby-test-explorer.png -------------------------------------------------------------------------------- /img/screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/connorshea/vscode-ruby-test-adapter/9a1fc573033619393a6a8bd8672ef44fc2efd26f/img/screenshot.png -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vscode-ruby-test-adapter", 3 | "displayName": "Ruby Test Explorer", 4 | "description": "Run your Ruby tests in the Sidebar of Visual Studio Code", 5 | "icon": "img/ruby-test-explorer.png", 6 | "author": "Connor Shea ", 7 | "publisher": "connorshea", 8 | "version": "0.9.2", 9 | "license": "MIT", 10 | "homepage": "https://github.com/connorshea/vscode-ruby-test-adapter", 11 | "repository": { 12 | "type": "git", 13 | "url": "https://github.com/connorshea/vscode-ruby-test-adapter.git" 14 | }, 15 | "bugs": { 16 | "url": "https://github.com/connorshea/vscode-ruby-test-adapter/issues" 17 | }, 18 | "categories": [ 19 | "Other" 20 | ], 21 | "keywords": [ 22 | "test", 23 | "testing", 24 | "rspec", 25 | "minitest", 26 | "mini_test", 27 | "ruby", 28 | "test explorer", 29 | "test adapter" 30 | ], 31 | "main": "out/src/main.js", 32 | "scripts": { 33 | "clean": "rimraf out *.vsix", 34 | "build": "tsc", 35 | "watch": "tsc -w", 36 | "rebuild": "npm run clean && npm run build", 37 | "package": "vsce package", 38 | "publish": "vsce publish", 39 | "test:minitest": "npm run build && node ./out/test/runMinitestTests.js", 40 | "test:rspec": "npm run build && node ./out/test/runRspecTests.js" 41 | }, 42 | "dependencies": { 43 | "split2": "^4.1.0", 44 | "tslib": "^2.4.0", 45 | "vscode-test-adapter-api": "^1.9.0", 46 | "vscode-test-adapter-util": "^0.7.1" 47 | }, 48 | "devDependencies": { 49 | "@types/glob": "^7.2.0", 50 | "@types/mocha": "^9.1.0", 51 | "@types/split2": "^3.2.1", 52 | "@types/vscode": "^1.69.0", 53 | "glob": "^8.0.3", 54 | "mocha": "^9.2.2", 55 | "rimraf": "^3.0.0", 56 | "typescript": "^4.7.4", 57 | "vsce": "^2.10.0", 58 | "vscode-test": "^1.6.1" 59 | }, 60 | "engines": { 61 | "vscode": "^1.69.0" 62 | }, 63 | "extensionDependencies": [ 64 | "hbenl.vscode-test-explorer" 65 | ], 66 | "activationEvents": [ 67 | "onLanguage:ruby", 68 | "onLanguage:erb", 69 | "workspaceContains:**/Rakefile", 70 | "workspaceContains:**/Gemfile", 71 | "workspaceContains:**/*.rb", 72 | "onCommand:commandId" 73 | ], 74 | "contributes": { 75 | "configuration": { 76 | "type": "object", 77 | "title": "Ruby Test Explorer configuration", 78 | "properties": { 79 | "rubyTestExplorer.logpanel": { 80 | "description": "Write diagnotic logs to an output panel.", 81 | "type": "boolean", 82 | "scope": "resource" 83 | }, 84 | "rubyTestExplorer.logfile": { 85 | "description": "Write diagnostic logs to the given file.", 86 | "type": "string", 87 | "scope": "resource" 88 | }, 89 | "rubyTestExplorer.testFramework": { 90 | "description": "Test framework to use by default, for example rspec or minitest.", 91 | "type": "string", 92 | "default": "auto", 93 | "enum": [ 94 | "none", 95 | "auto", 96 | "rspec", 97 | "minitest" 98 | ], 99 | "enumDescriptions": [ 100 | "Disable Ruby Test Runner.", 101 | "Automatically detect test framework for a given workspace using Bundler.", 102 | "Use RSpec test framework.", 103 | "Use Minitest test framework." 104 | ], 105 | "scope": "resource" 106 | }, 107 | "rubyTestExplorer.rspecCommand": { 108 | "markdownDescription": "Define the command to run Rspec tests with, for example `bundle exec rspec`, `spring rspec`, or `rspec`.", 109 | "default": "bundle exec rspec", 110 | "type": "string", 111 | "scope": "resource" 112 | }, 113 | "rubyTestExplorer.rspecDirectory": { 114 | "markdownDescription": "The location of your RSpec directory relative to the root of the workspace.", 115 | "default": "./spec/", 116 | "type": "string", 117 | "scope": "resource" 118 | }, 119 | "rubyTestExplorer.minitestCommand": { 120 | "markdownDescription": "Define how to run Minitest with Rake, for example `./bin/rake`, `bundle exec rake` or `rake`.", 121 | "default": "bundle exec rake", 122 | "type": "string", 123 | "scope": "resource" 124 | }, 125 | "rubyTestExplorer.minitestDirectory": { 126 | "markdownDescription": "The location of your test directory relative to the root of the workspace.", 127 | "default": "./test/", 128 | "type": "string", 129 | "scope": "resource" 130 | }, 131 | "rubyTestExplorer.filePattern": { 132 | "markdownDescription": "The naming pattern for your tests.", 133 | "default": [ 134 | "*_test.rb", 135 | "test_*.rb", 136 | "*_spec.rb" 137 | ], 138 | "type": "array", 139 | "items": { 140 | "type": "string" 141 | }, 142 | "scope": "resource" 143 | }, 144 | "rubyTestExplorer.debuggerHost": { 145 | "markdownDescription": "The host to connect the debugger to.", 146 | "default": "127.0.0.1", 147 | "type": "string", 148 | "scope": "resource" 149 | }, 150 | "rubyTestExplorer.debuggerPort": { 151 | "markdownDescription": "The port to connect the debugger to.", 152 | "default": "1234", 153 | "type": "string", 154 | "scope": "resource" 155 | }, 156 | "rubyTestExplorer.debugCommand": { 157 | "markdownDescription": "Define how to run rdebug-ide, for example `rdebug-ide` or `bundle exec rdebug-ide`.", 158 | "default": "rdebug-ide", 159 | "type": "string", 160 | "scope": "resource" 161 | } 162 | } 163 | } 164 | }, 165 | "capabilities": { 166 | "untrustedWorkspaces": { 167 | "supported": false, 168 | "description": "This extension runs code from tests and therefore can't be assumed to be safe to use in an untrusted workspace." 169 | }, 170 | "virtualWorkspaces": false 171 | } 172 | } 173 | -------------------------------------------------------------------------------- /ruby/.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "rubyTestExplorer.filePattern": [ 3 | "*_test.rb", 4 | "test_*.rb", 5 | "*_spec.rb" 6 | ], 7 | "rubyTestExplorer.minitestDirectory": "./minispecs/", 8 | "rubyTestExplorer.rspecDirectory": "./rspecs/", 9 | "rubyTestExplorer.testFramework": "rspec" 10 | } 11 | -------------------------------------------------------------------------------- /ruby/Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | group :development, :test do 4 | gem "bundler" 5 | gem "rake" 6 | gem "minitest" 7 | gem "rspec" 8 | end 9 | -------------------------------------------------------------------------------- /ruby/Rakefile: -------------------------------------------------------------------------------- 1 | require "bundler/setup" 2 | require "rake/testtask" 3 | require 'rspec/core/rake_task' 4 | 5 | RSpec::Core::RakeTask.new(:rspectest) do |t| 6 | t.pattern = ['rspecs/**/*_spec.rb', 'rspecs/**/*_test.rb'] 7 | end 8 | 9 | Rake::TestTask.new(:minitest) do |t| 10 | t.libs << "minispecs" 11 | t.test_files = FileList['minispecs/**/*_spec.rb', 'minispecs/**/*_test.rb'] 12 | end 13 | 14 | task default: [:minitest, :rspectest, :minitest_rake_task] 15 | 16 | Rake::TestTask.new(:minitest_rake_task) do |t| 17 | t.test_files = FileList['test/minitest/*_test.rb'] 18 | end 19 | -------------------------------------------------------------------------------- /ruby/debug_minitest.rb: -------------------------------------------------------------------------------- 1 | $LOAD_PATH << File.expand_path(__dir__) 2 | require "vscode/minitest" 3 | 4 | VSCode::Minitest.run(*ARGV) 5 | -------------------------------------------------------------------------------- /ruby/debug_rspec.rb: -------------------------------------------------------------------------------- 1 | $LOAD_PATH << File.expand_path(__dir__) 2 | require 'rspec/core' 3 | RSpec::Core::Runner.invoke 4 | -------------------------------------------------------------------------------- /ruby/minispecs/nested_spec.rb: -------------------------------------------------------------------------------- 1 | require "bundler/setup" 2 | require "minitest/autorun" 3 | 4 | class NestMeme 5 | def i_can_has_cheezburger? 6 | "OHAI!" 7 | end 8 | 9 | def will_it_blend? 10 | "yes" 11 | end 12 | end 13 | 14 | describe NestMeme do 15 | before do 16 | @meme = NestMeme.new 17 | end 18 | 19 | describe "when asked about cheeseburgers" do 20 | it "must respond positively" do 21 | _(@meme.i_can_has_cheezburger?).must_equal "OHAI!" 22 | end 23 | end 24 | 25 | describe "when asked about blending possibilities" do 26 | it "won't say no" do 27 | _(@meme.will_it_blend?).wont_match(/^no/i) 28 | end 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /ruby/minispecs/spec_spec.rb: -------------------------------------------------------------------------------- 1 | require "bundler/setup" 2 | require "minitest/autorun" 3 | 4 | class Meme 5 | def i_can_has_cheezburger? 6 | "OHAI!" 7 | end 8 | 9 | def will_it_blend? 10 | "yes" 11 | end 12 | end 13 | 14 | describe Meme do 15 | before do 16 | @meme = Meme.new 17 | end 18 | 19 | it "must respond positively" do 20 | _(@meme.i_can_has_cheezburger?).must_equal "OHAI!" 21 | end 22 | 23 | it "won't say no" do 24 | _(@meme.will_it_blend?).wont_match(/^no/i) 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /ruby/minispecs/unit_test.rb: -------------------------------------------------------------------------------- 1 | require "bundler/setup" 2 | require "minitest/autorun" 3 | 4 | class TestMeme < Minitest::Test 5 | def setup 6 | @meme = Meme.new 7 | end 8 | 9 | def test_that_kitty_can_eat 10 | assert_equal "OHAI!", @meme.i_can_has_cheezburger? 11 | end 12 | 13 | def test_that_it_will_not_blend 14 | refute_match(/^no/i, @meme.will_it_blend?) 15 | end 16 | 17 | def test_that_will_be_skipped 18 | skip "test this later" 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /ruby/rspecs/rspec_spec.rb: -------------------------------------------------------------------------------- 1 | require 'rspec' 2 | 3 | class Factorial 4 | def factorial_of(n) 5 | (1..n).inject(:*) 6 | end 7 | end 8 | 9 | describe Factorial do 10 | it "finds the factorial of 5" do 11 | calculator = Factorial.new 12 | expect(calculator.factorial_of(5)).to eq(120) 13 | end 14 | 15 | it "finds the factorial of 6" do 16 | calculator = Factorial.new 17 | expect(calculator.factorial_of(6)).to eq(720) 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /ruby/rspecs/unit_test.rb: -------------------------------------------------------------------------------- 1 | require 'rspec' 2 | 3 | class Square 4 | def square_of(n) 5 | n * n 6 | end 7 | end 8 | 9 | describe Square do 10 | before do 11 | @calculator = Square.new 12 | end 13 | 14 | it "finds the square of 2" do 15 | expect(@calculator.square_of(2)).to eq(4) 16 | end 17 | 18 | it "finds the square of 3" do 19 | expect(@calculator.square_of(3)).to eq(9) 20 | end 21 | end -------------------------------------------------------------------------------- /ruby/test/minitest/rake_task_test.rb: -------------------------------------------------------------------------------- 1 | require_relative "test_helper" 2 | 3 | FILES = {} 4 | 5 | FILES["Rakefile"] = < "test", 72 | "TESTS_PATTERN" => '*_test.rb' 73 | } 74 | end 75 | 76 | def test_test_list 77 | stdout, _, status = Open3.capture3(env, "rake -R #{__dir__}/../.. vscode:minitest:list", chdir: dir.to_s) 78 | 79 | assert_predicate status, :success? 80 | assert_match(/START_OF_TEST_JSON(.*)END_OF_TEST_JSON/, stdout) 81 | 82 | stdout =~ /START_OF_TEST_JSON(.*)END_OF_TEST_JSON/ 83 | json = JSON.parse($1, symbolize_names: true) 84 | 85 | assert_equal( 86 | [ 87 | { 88 | description: "square of one", 89 | full_description: "square of one", 90 | file_path: "./test/square_test.rb", 91 | full_path: (dir + "test/square_test.rb").to_s, 92 | line_number: 4, 93 | klass: "SquareTest", 94 | method: "test_square_of_one", 95 | runnable: "SquareTest", 96 | id: "./test/square_test.rb[4]" 97 | }, 98 | { 99 | description: "square of two", 100 | full_description: "square of two", 101 | file_path: "./test/square_test.rb", 102 | full_path: (dir + "test/square_test.rb").to_s, 103 | line_number: 8, 104 | klass: "SquareTest", 105 | method: "test_square_of_two", 106 | runnable: "SquareTest", 107 | id: "./test/square_test.rb[8]" 108 | }, 109 | { 110 | description: "square error", 111 | full_description: "square error", 112 | file_path: "./test/square_test.rb", 113 | full_path: (dir + "test/square_test.rb").to_s, 114 | line_number: 12, 115 | klass: "SquareTest", 116 | method: "test_square_error", 117 | runnable: "SquareTest", 118 | id: "./test/square_test.rb[12]" 119 | }, 120 | { 121 | description: "square skip", 122 | full_description: "square skip", 123 | file_path: "./test/square_test.rb", 124 | full_path: (dir + "test/square_test.rb").to_s, 125 | line_number: 16, 126 | klass: "SquareTest", 127 | method: "test_square_skip", 128 | runnable: "SquareTest", 129 | id: "./test/square_test.rb[16]" 130 | } 131 | ], 132 | json[:examples] 133 | ) 134 | end 135 | 136 | def test_test_run_all 137 | stdout, _, status = Open3.capture3(env, "rake -R #{__dir__}/../.. vscode:minitest:run test", chdir: dir.to_s) 138 | 139 | refute_predicate status, :success? 140 | assert_match(/START_OF_TEST_JSON(.*)END_OF_TEST_JSON/, stdout) 141 | 142 | stdout =~ /START_OF_TEST_JSON(.*)END_OF_TEST_JSON/ 143 | json = JSON.parse($1, symbolize_names: true) 144 | 145 | examples = json[:examples] 146 | 147 | assert_equal 4, examples.size 148 | 149 | assert_any(examples, pass_count: 1) do |example| 150 | assert_equal "square error", example[:description] 151 | assert_equal "failed", example[:status] 152 | assert_nil example[:pending_message] 153 | refute_nil example[:exception] 154 | assert_equal "Minitest::UnexpectedError", example.dig(:exception, :class) 155 | assert_match(/RuntimeError:/, example.dig(:exception, :message)) 156 | assert_instance_of Array, example.dig(:exception, :backtrace) 157 | assert_instance_of Array, example.dig(:exception, :full_backtrace) 158 | assert_equal 13, example.dig(:exception, :position) 159 | end 160 | 161 | assert_any(examples, pass_count: 1) do |example| 162 | assert_equal "square of one", example[:description] 163 | assert_equal "passed", example[:status] 164 | assert_nil example[:pending_message] 165 | assert_nil example[:exception] 166 | end 167 | 168 | assert_any(examples, pass_count: 1) do |example| 169 | assert_equal "square of two", example[:description] 170 | assert_equal "failed", example[:status] 171 | assert_nil example[:pending_message] 172 | refute_nil example[:exception] 173 | assert_equal "Minitest::Assertion", example.dig(:exception, :class) 174 | assert_equal "Expected: 3\n Actual: 4", example.dig(:exception, :message) 175 | assert_instance_of Array, example.dig(:exception, :backtrace) 176 | assert_instance_of Array, example.dig(:exception, :full_backtrace) 177 | assert_equal 9, example.dig(:exception, :position) 178 | end 179 | 180 | assert_any(examples, pass_count: 1) do |example| 181 | assert_equal "square skip", example[:description] 182 | assert_equal "failed", example[:status] 183 | assert_equal "This is skip", example[:pending_message] 184 | assert_nil example[:exception] 185 | end 186 | end 187 | 188 | def test_test_run_file 189 | stdout, _, status = Open3.capture3(env, "rake -R #{__dir__}/../.. vscode:minitest:run test/square_test.rb", chdir: dir.to_s) 190 | 191 | refute_predicate status, :success? 192 | assert_match(/START_OF_TEST_JSON(.*)END_OF_TEST_JSON/, stdout) 193 | 194 | stdout =~ /START_OF_TEST_JSON(.*)END_OF_TEST_JSON/ 195 | json = JSON.parse($1, symbolize_names: true) 196 | 197 | examples = json[:examples] 198 | 199 | assert_equal 4, examples.size 200 | end 201 | 202 | def test_test_run_file_line 203 | stdout, _, status = Open3.capture3(env, "rake -R #{__dir__}/../.. vscode:minitest:run test/square_test.rb:4 test/square_test.rb:16", chdir: dir.to_s) 204 | 205 | assert_predicate status, :success? 206 | assert_match(/START_OF_TEST_JSON(.*)END_OF_TEST_JSON/, stdout) 207 | 208 | stdout =~ /START_OF_TEST_JSON(.*)END_OF_TEST_JSON/ 209 | json = JSON.parse($1, symbolize_names: true) 210 | 211 | examples = json[:examples] 212 | 213 | assert_equal 2, examples.size 214 | 215 | assert_any(examples, pass_count: 1) do |example| 216 | assert_equal "square of one", example[:description] 217 | assert_equal "passed", example[:status] 218 | end 219 | 220 | assert_any(examples, pass_count: 1) do |example| 221 | assert_equal "square skip", example[:description] 222 | assert_equal "failed", example[:status] 223 | end 224 | end 225 | end 226 | -------------------------------------------------------------------------------- /ruby/test/minitest/test_helper.rb: -------------------------------------------------------------------------------- 1 | $LOAD_PATH.unshift File.expand_path('../../', __dir__) 2 | require "minitest/autorun" 3 | require "tmpdir" 4 | require "pathname" 5 | require "open3" 6 | require "json" 7 | 8 | module TestHelper 9 | def assert_any(collection, count: nil, pass_count: nil, &block) 10 | assert_equal count, collection.count if count 11 | 12 | good_items = [] 13 | errors = [] 14 | 15 | collection.each do |c| 16 | begin 17 | block[c] 18 | good_items << c 19 | rescue Minitest::Assertion => error 20 | errors << error 21 | end 22 | end 23 | 24 | if good_items.empty? 25 | raise errors.max_by(&:location) 26 | else 27 | assert_equal pass_count, good_items.size, "Expect #{pass_count} items pass the test" if pass_count 28 | end 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /ruby/vscode.rake: -------------------------------------------------------------------------------- 1 | $LOAD_PATH << File.expand_path(__dir__) 2 | require "vscode/minitest" 3 | 4 | namespace :vscode do 5 | namespace :minitest do 6 | desc "List minitest available tests" 7 | task :list do 8 | VSCode::Minitest.list 9 | end 10 | 11 | desc "Run tests (accepts one or more files, folders or file:line formats)" 12 | task :run do |t| 13 | args = ARGV.dup.drop_while { |a| a != t.name }.drop(1) 14 | VSCode::Minitest.run(*args) 15 | end 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /ruby/vscode/minitest.rb: -------------------------------------------------------------------------------- 1 | require 'minitest' 2 | require "vscode/minitest/tests" 3 | require "vscode/minitest/reporter" 4 | require "vscode/minitest/runner" 5 | require "json" 6 | require "pathname" 7 | 8 | module Minitest 9 | # we don't want tests to autorun 10 | def self.autorun; end 11 | end 12 | 13 | module VSCode 14 | module_function 15 | 16 | def project_root 17 | @project_root ||= Pathname.new(Dir.pwd) 18 | end 19 | 20 | module Minitest 21 | module_function 22 | 23 | def list(io = $stdout) 24 | io.sync = true if io.respond_to?(:"sync=") 25 | data = { version: ::Minitest::VERSION, examples: tests.all } 26 | json = ENV.key?("PRETTY") ? JSON.pretty_generate(data) : JSON.generate(data) 27 | io.puts "START_OF_TEST_JSON#{json}END_OF_TEST_JSON" 28 | end 29 | 30 | def run(*args) 31 | args = [ENV['TESTS_DIR']] if args.empty? 32 | reporter = Reporter.new 33 | reporter.start 34 | runner = Runner.new(reporter: reporter) 35 | args.each { |arg| runner.add(arg) } 36 | runner.run 37 | reporter.report 38 | exit(reporter.passed?) 39 | end 40 | 41 | def tests 42 | @tests ||= Tests.new 43 | end 44 | end 45 | end 46 | -------------------------------------------------------------------------------- /ruby/vscode/minitest/reporter.rb: -------------------------------------------------------------------------------- 1 | module VSCode 2 | module Minitest 3 | class Reporter < ::Minitest::Reporter 4 | attr_accessor :assertions, :count, :results, :start_time, :total_time, :failures, :errors, :skips 5 | 6 | def initialize(io = $stdout, options = {}) 7 | super 8 | io.sync = true if io.respond_to?(:"sync=") 9 | self.assertions = 0 10 | self.count = 0 11 | self.results = [] 12 | end 13 | 14 | def start 15 | self.start_time = ::Minitest.clock_time 16 | end 17 | 18 | def prerecord(klass, meth) 19 | data = VSCode::Minitest.tests.find_by(klass: klass.to_s, method: meth) 20 | io.puts "\nRUNNING: #{data[:id]}\n" 21 | end 22 | 23 | def record(result) 24 | self.count += 1 25 | self.assertions += result.assertions 26 | results << result 27 | data = vscode_result(result) 28 | if result.skipped? 29 | io.puts "\nPENDING: #{data[:id]}\n" 30 | else 31 | io.puts "\n#{data[:status].upcase}: #{data[:id]}\n" 32 | end 33 | end 34 | 35 | def report 36 | aggregate = results.group_by { |r| r.failure.class } 37 | aggregate.default = [] # dumb. group_by should provide this 38 | self.total_time = (::Minitest.clock_time - start_time).round(2) 39 | self.failures = aggregate[::Minitest::Assertion].size 40 | self.errors = aggregate[::Minitest::UnexpectedError].size 41 | self.skips = aggregate[::Minitest::Skip].size 42 | json = ENV.key?("PRETTY") ? JSON.pretty_generate(vscode_data) : JSON.generate(vscode_data) 43 | io.puts "START_OF_TEST_JSON#{json}END_OF_TEST_JSON" 44 | end 45 | 46 | def passed? 47 | failures.zero? 48 | end 49 | 50 | def vscode_data 51 | { 52 | version: ::Minitest::VERSION, 53 | summary: { 54 | duration: total_time, 55 | example_count: assertions, 56 | failure_count: failures, 57 | pending_count: skips, 58 | errors_outside_of_examples_count: errors 59 | }, 60 | summary_line: "Total time: #{total_time}, Runs: #{count}, Assertions: #{assertions}, Failures: #{failures}, Errors: #{errors}, Skips: #{skips}", 61 | examples: results.map { |r| vscode_result(r) } 62 | } 63 | end 64 | 65 | def vscode_result(r) 66 | base = VSCode::Minitest.tests.find_by(klass: r.klass, method: r.name).dup 67 | if r.skipped? 68 | base[:status] = "failed" 69 | base[:pending_message] = r.failure.message 70 | elsif r.passed? 71 | base[:status] = "passed" 72 | else 73 | base[:status] = "failed" 74 | base[:pending_message] = nil 75 | e = r.failure.exception 76 | backtrace = expand_backtrace(e.backtrace) 77 | base[:exception] = { 78 | class: e.class.name, 79 | message: e.message, 80 | backtrace: clean_backtrace(backtrace), 81 | full_backtrace: backtrace, 82 | position: exception_position(backtrace, base[:full_path]) || base[:line_number] 83 | } 84 | end 85 | base 86 | end 87 | 88 | def expand_backtrace(backtrace) 89 | backtrace.map do |line| 90 | parts = line.split(":") 91 | parts[0] = File.expand_path(parts[0], VSCode.project_root) 92 | parts.join(":") 93 | end 94 | end 95 | 96 | def clean_backtrace(backtrace) 97 | backtrace.map do |line| 98 | next unless line.start_with?(VSCode.project_root.to_s) 99 | line.gsub(VSCode.project_root.to_s + "/", "") 100 | end.compact 101 | end 102 | 103 | def exception_position(backtrace, file) 104 | line = backtrace.find { |frame| frame.start_with?(file) } 105 | return unless line 106 | line.split(":")[1].to_i 107 | end 108 | end 109 | end 110 | end 111 | -------------------------------------------------------------------------------- /ruby/vscode/minitest/runner.rb: -------------------------------------------------------------------------------- 1 | module VSCode 2 | module Minitest 3 | class Runner 4 | attr_reader :reporter, :runnables 5 | 6 | def initialize(reporter:) 7 | @reporter = reporter 8 | @runnables = [] 9 | end 10 | 11 | def add(runnable) 12 | path, line = runnable.split(":") 13 | path = File.expand_path(path, VSCode.project_root) 14 | return add_dir(path) if File.directory?(path) 15 | return add_file_with_line(path, line.to_i) if File.file?(path) && line 16 | return add_file(path) if File.file?(path) 17 | raise "Can't add #{runnable.inspect}" 18 | end 19 | 20 | def add_dir(path) 21 | patterns = ENV.fetch('TESTS_PATTERN').split(',').map { |p| "#{path}/**/#{p}" } 22 | Rake::FileList[*patterns].each do |file| 23 | add_file(file) 24 | end 25 | end 26 | 27 | def add_file(file) 28 | test = VSCode::Minitest.tests.find_by(full_path: file) 29 | runnables << [test[:runnable], {}] if test 30 | end 31 | 32 | def add_file_with_line(file, line) 33 | test = VSCode::Minitest.tests.find_by(full_path: file, line_number: line) 34 | raise "There is no test the the given location: #{file}:#{line}" unless test 35 | runnables << [test[:runnable], filter: test[:method]] 36 | end 37 | 38 | def run 39 | runnables.each do |runnable, options| 40 | runnable.run(reporter, options) 41 | end 42 | end 43 | end 44 | end 45 | end 46 | -------------------------------------------------------------------------------- /ruby/vscode/minitest/tests.rb: -------------------------------------------------------------------------------- 1 | require "rake" 2 | 3 | module VSCode 4 | module Minitest 5 | class Tests 6 | def all 7 | @all ||= begin 8 | load_files 9 | build_list 10 | end 11 | end 12 | 13 | def find_by(**filters) 14 | all.find do |test| 15 | test.values_at(*filters.keys) == filters.values 16 | end 17 | end 18 | 19 | def load_files 20 | # Take the tests dir in the format of `./test/` and turn it into `test`. 21 | test_dir = ENV['TESTS_DIR'] || './test/' 22 | test_dir = test_dir.gsub('./', '') 23 | test_dir = test_dir[0...-1] if test_dir.end_with?('/') 24 | $LOAD_PATH << VSCode.project_root.join(test_dir).to_s 25 | patterns = ENV.fetch('TESTS_PATTERN').split(',').map { |p| "#{test_dir}/**/#{p}" } 26 | file_list = Rake::FileList[*patterns] 27 | file_list.each { |path| require File.expand_path(path) } 28 | end 29 | 30 | def build_list 31 | if ::Minitest.respond_to?(:seed) && ::Minitest.seed.nil? 32 | ::Minitest.seed = (ENV['SEED'] || srand).to_i % 0xFFFF 33 | end 34 | 35 | tests = [] 36 | ::Minitest::Runnable.runnables.map do |runnable| 37 | file_tests = runnable.runnable_methods.map do |test_name| 38 | path, line = runnable.instance_method(test_name).source_location 39 | full_path = File.expand_path(path, VSCode.project_root) 40 | path = full_path.gsub(VSCode.project_root.to_s, ".") 41 | path = "./#{path}" unless path.match?(/^\./) 42 | description = test_name.gsub(/^test_[:\s]*/, "") 43 | description = description.tr("_", " ") unless description.match?(/\s/) 44 | 45 | { 46 | description: description, 47 | full_description: description, 48 | file_path: path, 49 | full_path: full_path, 50 | line_number: line, 51 | klass: runnable.name, 52 | method: test_name, 53 | runnable: runnable 54 | } 55 | end 56 | file_tests.sort_by! { |t| t[:line_number] } 57 | file_tests.each do |t| 58 | t[:id] = "#{t[:file_path]}[#{t[:line_number]}]" 59 | end 60 | tests.concat(file_tests) 61 | end 62 | tests 63 | end 64 | end 65 | end 66 | end 67 | -------------------------------------------------------------------------------- /src/adapter.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | import { TestAdapter, TestLoadStartedEvent, TestLoadFinishedEvent, TestRunStartedEvent, TestRunFinishedEvent, TestSuiteEvent, TestEvent } from 'vscode-test-adapter-api'; 3 | import { Log } from 'vscode-test-adapter-util'; 4 | import * as childProcess from 'child_process'; 5 | import { Tests } from './tests'; 6 | import { RspecTests } from './rspecTests'; 7 | import { MinitestTests } from './minitestTests'; 8 | import * as path from 'path'; 9 | 10 | export class RubyAdapter implements TestAdapter { 11 | private disposables: { dispose(): void }[] = []; 12 | 13 | private readonly testsEmitter = new vscode.EventEmitter(); 14 | private readonly testStatesEmitter = new vscode.EventEmitter(); 15 | private readonly autorunEmitter = new vscode.EventEmitter(); 16 | private testsInstance: Tests | undefined; 17 | private currentTestFramework: string | undefined; 18 | 19 | get tests(): vscode.Event { return this.testsEmitter.event; } 20 | get testStates(): vscode.Event { return this.testStatesEmitter.event; } 21 | get autorun(): vscode.Event | undefined { return this.autorunEmitter.event; } 22 | 23 | constructor( 24 | public readonly workspace: vscode.WorkspaceFolder, 25 | private readonly log: Log, 26 | private readonly context: vscode.ExtensionContext 27 | ) { 28 | this.log.info('Initializing Ruby adapter'); 29 | 30 | this.disposables.push(this.testsEmitter); 31 | this.disposables.push(this.testStatesEmitter); 32 | this.disposables.push(this.autorunEmitter); 33 | this.disposables.push(this.createWatcher()); 34 | this.disposables.push(this.configWatcher()); 35 | } 36 | 37 | async load(): Promise { 38 | this.log.info('Loading Ruby tests...'); 39 | this.testsEmitter.fire({ type: 'started' }); 40 | if (this.getTestFramework() === "rspec") { 41 | this.log.info('Loading RSpec tests...'); 42 | this.testsInstance = new RspecTests(this.context, this.testStatesEmitter, this.log, this.workspace); 43 | const loadedTests = await this.testsInstance.loadTests(); 44 | this.testsEmitter.fire({ type: 'finished', suite: loadedTests }); 45 | } else if (this.getTestFramework() === "minitest") { 46 | this.log.info('Loading Minitest tests...'); 47 | this.testsInstance = new MinitestTests(this.context, this.testStatesEmitter, this.log, this.workspace); 48 | const loadedTests = await this.testsInstance.loadTests(); 49 | this.testsEmitter.fire({ type: 'finished', suite: loadedTests }); 50 | } else { 51 | this.log.warn('No test framework detected. Configure the rubyTestExplorer.testFramework setting if you want to use the Ruby Test Explorer.'); 52 | this.testsEmitter.fire({ type: 'finished' }); 53 | } 54 | } 55 | 56 | async run(tests: string[], debuggerConfig?: vscode.DebugConfiguration): Promise { 57 | this.log.info(`Running Ruby tests ${JSON.stringify(tests)}`); 58 | this.testStatesEmitter.fire({ type: 'started', tests }); 59 | if (!this.testsInstance) { 60 | let testFramework = this.getTestFramework(); 61 | if (testFramework === "rspec") { 62 | this.testsInstance = new RspecTests(this.context, this.testStatesEmitter, this.log, this.workspace); 63 | } else if (testFramework === "minitest") { 64 | this.testsInstance = new MinitestTests(this.context, this.testStatesEmitter, this.log, this.workspace); 65 | } 66 | } 67 | if (this.testsInstance) { 68 | await this.testsInstance.runTests(tests, debuggerConfig); 69 | } 70 | } 71 | 72 | async debug(testsToRun: string[]): Promise { 73 | this.log.info(`Debugging test(s) ${JSON.stringify(testsToRun)} of ${this.workspace.uri.fsPath}`); 74 | 75 | const config = vscode.workspace.getConfiguration('rubyTestExplorer', null) 76 | 77 | const debuggerConfig = { 78 | name: "Debug Ruby Tests", 79 | type: "Ruby", 80 | request: "attach", 81 | remoteHost: config.get('debuggerHost') || "127.0.0.1", 82 | remotePort: config.get('debuggerPort') || "1234", 83 | remoteWorkspaceRoot: "${workspaceRoot}" 84 | } 85 | 86 | const testRunPromise = this.run(testsToRun, debuggerConfig); 87 | 88 | this.log.info('Starting the debug session'); 89 | let debugSession: any; 90 | try { 91 | await this.testsInstance!.debugCommandStarted() 92 | debugSession = await this.startDebugging(debuggerConfig); 93 | } catch (err) { 94 | this.log.error('Failed starting the debug session - aborting', err); 95 | this.cancel(); 96 | return; 97 | } 98 | 99 | const subscription = this.onDidTerminateDebugSession((session) => { 100 | if (debugSession != session) return; 101 | this.log.info('Debug session ended'); 102 | this.cancel(); // terminate the test run 103 | subscription.dispose(); 104 | }); 105 | 106 | await testRunPromise; 107 | } 108 | 109 | cancel(): void { 110 | if (this.testsInstance) { 111 | this.log.info('Killing currently-running tests.'); 112 | this.testsInstance.killChild(); 113 | } else { 114 | this.log.info('No tests running currently, no process to kill.'); 115 | } 116 | } 117 | 118 | dispose(): void { 119 | this.cancel(); 120 | for (const disposable of this.disposables) { 121 | disposable.dispose(); 122 | } 123 | this.disposables = []; 124 | } 125 | 126 | /** 127 | * Get the configured test framework. 128 | */ 129 | protected getTestFramework(): string { 130 | // Short-circuit the test framework check if we've already determined the current test framework. 131 | if (this.currentTestFramework !== undefined) { 132 | return this.currentTestFramework; 133 | } 134 | 135 | let testFramework: string = (vscode.workspace.getConfiguration('rubyTestExplorer', null).get('testFramework') as string); 136 | // If the test framework is something other than auto, return the value. 137 | if (['rspec', 'minitest', 'none'].includes(testFramework)) { 138 | this.currentTestFramework = testFramework; 139 | return testFramework; 140 | // If the test framework is auto, we need to try to detect the test framework type. 141 | } else { 142 | let detectedTestFramework = this.detectTestFramework(); 143 | this.currentTestFramework = detectedTestFramework; 144 | return detectedTestFramework; 145 | } 146 | } 147 | 148 | /** 149 | * Detect the current test framework using 'bundle list'. 150 | */ 151 | protected detectTestFramework(): string { 152 | this.log.info(`Getting a list of Bundler dependencies with 'bundle list'.`); 153 | 154 | const execArgs: childProcess.ExecOptions = { 155 | cwd: this.workspace.uri.fsPath, 156 | maxBuffer: 8192 * 8192 157 | }; 158 | 159 | try { 160 | // Run 'bundle list' and set the output to bundlerList. 161 | // Execute this syncronously to avoid the test explorer getting stuck loading. 162 | let err, stdout = childProcess.execSync('bundle list', execArgs); 163 | 164 | if (err) { 165 | this.log.error(`Error while listing Bundler dependencies: ${err}`); 166 | this.log.error(`Output: ${stdout}`); 167 | throw err; 168 | } 169 | 170 | let bundlerList = stdout.toString(); 171 | 172 | // Search for rspec or minitest in the output of 'bundle list'. 173 | // The search function returns the index where the string is found, or -1 otherwise. 174 | if (bundlerList.search('rspec-core') >= 0) { 175 | this.log.info(`Detected RSpec test framework.`); 176 | return 'rspec'; 177 | } else if (bundlerList.search('minitest') >= 0) { 178 | this.log.info(`Detected Minitest test framework.`); 179 | return 'minitest'; 180 | } else { 181 | this.log.info(`Unable to automatically detect a test framework.`); 182 | return 'none'; 183 | } 184 | } catch (error) { 185 | this.log.error(error); 186 | return 'none'; 187 | } 188 | } 189 | 190 | protected async startDebugging(debuggerConfig: vscode.DebugConfiguration): Promise { 191 | const debugSessionPromise = new Promise((resolve, reject) => { 192 | 193 | let subscription: vscode.Disposable | undefined; 194 | subscription = vscode.debug.onDidStartDebugSession(debugSession => { 195 | if ((debugSession.name === debuggerConfig.name) && subscription) { 196 | resolve(debugSession); 197 | subscription.dispose(); 198 | subscription = undefined; 199 | } 200 | }); 201 | 202 | setTimeout(() => { 203 | if (subscription) { 204 | reject(new Error('Debug session failed to start within 5 seconds')); 205 | subscription.dispose(); 206 | subscription = undefined; 207 | } 208 | }, 5000); 209 | }); 210 | 211 | const started = await vscode.debug.startDebugging(this.workspace, debuggerConfig); 212 | if (started) { 213 | return await debugSessionPromise; 214 | } else { 215 | throw new Error('Debug session couldn\'t be started'); 216 | } 217 | } 218 | 219 | protected onDidTerminateDebugSession(cb: (session: vscode.DebugSession) => any): vscode.Disposable { 220 | return vscode.debug.onDidTerminateDebugSession(cb); 221 | } 222 | 223 | /** 224 | * Get the test directory based on the configuration value if there's a configured test framework. 225 | */ 226 | private getTestDirectory(): string | undefined { 227 | let testFramework = this.getTestFramework(); 228 | let testDirectory = ''; 229 | if (testFramework === 'rspec') { 230 | testDirectory = 231 | (vscode.workspace.getConfiguration('rubyTestExplorer', null).get('rspecDirectory') as string) 232 | || path.join('.', 'spec'); 233 | } else if (testFramework === 'minitest') { 234 | testDirectory = 235 | (vscode.workspace.getConfiguration('rubyTestExplorer', null).get('minitestDirectory') as string) 236 | || path.join('.', 'test'); 237 | } 238 | 239 | if (testDirectory === '') { 240 | return undefined; 241 | } 242 | 243 | return path.join(this.workspace.uri.fsPath, testDirectory); 244 | } 245 | 246 | /** 247 | * Create a file watcher that will reload the test tree when a relevant file is changed. 248 | */ 249 | private createWatcher(): vscode.Disposable { 250 | return vscode.workspace.onDidSaveTextDocument(document => { 251 | // If there isn't a configured/detected test framework, short-circuit to avoid doing unnecessary work. 252 | if (this.currentTestFramework === 'none') { 253 | this.log.info('No test framework configured. Ignoring file change.'); 254 | return; 255 | } 256 | const filename = document.uri.fsPath; 257 | this.log.info(`${filename} was saved - checking if this effects ${this.workspace.uri.fsPath}`); 258 | if (filename.startsWith(this.workspace.uri.fsPath)) { 259 | let testDirectory = this.getTestDirectory(); 260 | 261 | // In the case that there's no configured test directory, we shouldn't try to reload the tests. 262 | if (testDirectory !== undefined && filename.startsWith(testDirectory)) { 263 | this.log.info('A test file has been edited, reloading tests.'); 264 | this.load(); 265 | } 266 | 267 | // Send an autorun event when a relevant file changes. 268 | // This only causes a run if the user has autorun enabled. 269 | this.log.info('Sending autorun event'); 270 | this.autorunEmitter.fire(); 271 | } 272 | }) 273 | } 274 | 275 | private configWatcher(): vscode.Disposable { 276 | return vscode.workspace.onDidChangeConfiguration(configChange => { 277 | this.log.info('Configuration changed'); 278 | if (configChange.affectsConfiguration("rubyTestExplorer")) { 279 | this.cancel(); 280 | this.currentTestFramework = undefined; 281 | this.load(); 282 | this.autorunEmitter.fire(); 283 | } 284 | }) 285 | } 286 | } 287 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | import { TestHub, testExplorerExtensionId } from 'vscode-test-adapter-api'; 3 | import { Log, TestAdapterRegistrar } from 'vscode-test-adapter-util'; 4 | import { RubyAdapter } from './adapter'; 5 | 6 | export async function activate(context: vscode.ExtensionContext) { 7 | // Determine whether to send the logger a workspace. 8 | let logWorkspaceFolder = (vscode.workspace.workspaceFolders || [])[0]; 9 | // create a simple logger that can be configured with the configuration variables 10 | // `rubyTestExplorer.logpanel` and `rubyTestExplorer.logfile` 11 | let log = new Log('rubyTestExplorer', logWorkspaceFolder, 'Ruby Test Explorer Log'); 12 | context.subscriptions.push(log); 13 | 14 | // get the Test Explorer extension 15 | const testExplorerExtension = vscode.extensions.getExtension(testExplorerExtensionId); 16 | if (log.enabled) { 17 | log.info(`Test Explorer ${testExplorerExtension ? '' : 'not '}found`); 18 | } 19 | 20 | let testFramework: string = (vscode.workspace.getConfiguration('rubyTestExplorer', null).get('testFramework') as string) || 'none'; 21 | 22 | if (testExplorerExtension && testFramework !== "none") { 23 | const testHub = testExplorerExtension.exports; 24 | 25 | // this will register a RubyTestAdapter for each WorkspaceFolder 26 | context.subscriptions.push(new TestAdapterRegistrar( 27 | testHub, 28 | workspaceFolder => new RubyAdapter(workspaceFolder, log, context), 29 | log 30 | )); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/minitestTests.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | import { TestSuiteInfo, TestEvent } from 'vscode-test-adapter-api'; 3 | import * as childProcess from 'child_process'; 4 | import { Tests } from './tests'; 5 | 6 | export class MinitestTests extends Tests { 7 | testFrameworkName = 'Minitest'; 8 | 9 | /** 10 | * Representation of the Minitest test suite as a TestSuiteInfo object. 11 | * 12 | * @return The Minitest test suite as a TestSuiteInfo object. 13 | */ 14 | tests = async () => new Promise((resolve, reject) => { 15 | try { 16 | // If test suite already exists, use testSuite. Otherwise, load them. 17 | let minitestTests = this.testSuite ? this.testSuite : this.loadTests(); 18 | return resolve(minitestTests); 19 | } catch (err) { 20 | if (err instanceof Error) { 21 | this.log.error(`Error while attempting to load Minitest tests: ${err.message}`); 22 | return reject(err); 23 | } 24 | } 25 | }); 26 | 27 | /** 28 | * Perform a dry-run of the test suite to get information about every test. 29 | * 30 | * @return The raw output from the Minitest JSON formatter. 31 | */ 32 | initTests = async () => new Promise((resolve, reject) => { 33 | let cmd = `${this.getTestCommand()} vscode:minitest:list`; 34 | 35 | // Allow a buffer of 64MB. 36 | const execArgs: childProcess.ExecOptions = { 37 | cwd: this.workspace.uri.fsPath, 38 | maxBuffer: 8192 * 8192, 39 | env: this.getProcessEnv() 40 | }; 41 | 42 | this.log.info(`Getting a list of Minitest tests in suite with the following command: ${cmd}`); 43 | 44 | childProcess.exec(cmd, execArgs, (err, stdout) => { 45 | if (err) { 46 | this.log.error(`Error while finding Minitest test suite: ${err.message}`); 47 | this.log.error(`Output: ${stdout}`); 48 | // Show an error message. 49 | 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."); 50 | vscode.window.showErrorMessage(err.message); 51 | throw err; 52 | } 53 | resolve(stdout); 54 | }); 55 | }); 56 | 57 | /** 58 | * Get the user-configured Minitest command, if there is one. 59 | * 60 | * @return The Minitest command 61 | */ 62 | protected getTestCommand(): string { 63 | let command: string = (vscode.workspace.getConfiguration('rubyTestExplorer', null).get('minitestCommand') as string) || 'bundle exec rake'; 64 | return `${command} -R ${(process.platform == 'win32') ? '%EXT_DIR%' : '$EXT_DIR'}`; 65 | } 66 | 67 | /** 68 | * Get the user-configured rdebug-ide command, if there is one. 69 | * 70 | * @return The rdebug-ide command 71 | */ 72 | protected getDebugCommand(debuggerConfig: vscode.DebugConfiguration): string { 73 | let command: string = 74 | (vscode.workspace.getConfiguration('rubyTestExplorer', null).get('debugCommand') as string) || 75 | 'rdebug-ide'; 76 | 77 | return ( 78 | `${command} --host ${debuggerConfig.remoteHost} --port ${debuggerConfig.remotePort}` + 79 | ` -- ${process.platform == 'win32' ? '%EXT_DIR%' : '$EXT_DIR'}/debug_minitest.rb` 80 | ); 81 | } 82 | 83 | /** 84 | * Get the user-configured test directory, if there is one. 85 | * 86 | * @return The test directory 87 | */ 88 | getTestDirectory(): string { 89 | let directory: string = (vscode.workspace.getConfiguration('rubyTestExplorer', null).get('minitestDirectory') as string); 90 | return directory || './test/'; 91 | } 92 | 93 | /** 94 | * Get the absolute path of the custom_formatter.rb file. 95 | * 96 | * @return The spec directory 97 | */ 98 | protected getRubyScriptsLocation(): string { 99 | return this.context.asAbsolutePath('./ruby'); 100 | } 101 | 102 | /** 103 | * Get the env vars to run the subprocess with. 104 | * 105 | * @return The env 106 | */ 107 | protected getProcessEnv(): any { 108 | return Object.assign({}, process.env, { 109 | "RAILS_ENV": "test", 110 | "EXT_DIR": this.getRubyScriptsLocation(), 111 | "TESTS_DIR": this.getTestDirectory(), 112 | "TESTS_PATTERN": this.getFilePattern().join(',') 113 | }); 114 | } 115 | 116 | /** 117 | * Get test command with formatter and debugger arguments 118 | * 119 | * @param debuggerConfig A VS Code debugger configuration. 120 | * @return The test command 121 | */ 122 | protected testCommandWithDebugger(debuggerConfig?: vscode.DebugConfiguration): string { 123 | let cmd = `${this.getTestCommand()} vscode:minitest:run` 124 | if (debuggerConfig) { 125 | cmd = this.getDebugCommand(debuggerConfig); 126 | } 127 | return cmd; 128 | } 129 | 130 | /** 131 | * Runs a single test. 132 | * 133 | * @param testLocation A file path with a line number, e.g. `/path/to/spec.rb:12`. 134 | * @param debuggerConfig A VS Code debugger configuration. 135 | * @return The raw output from running the test. 136 | */ 137 | runSingleTest = async (testLocation: string, debuggerConfig?: vscode.DebugConfiguration) => new Promise(async (resolve, reject) => { 138 | this.log.info(`Running single test: ${testLocation}`); 139 | let line = testLocation.split(':').pop(); 140 | let relativeLocation = testLocation.split(/:\d+$/)[0].replace(`${this.workspace.uri.fsPath}/`, "") 141 | const spawnArgs: childProcess.SpawnOptions = { 142 | cwd: this.workspace.uri.fsPath, 143 | shell: true, 144 | env: this.getProcessEnv() 145 | }; 146 | 147 | let testCommand = `${this.testCommandWithDebugger(debuggerConfig)} '${relativeLocation}:${line}'`; 148 | this.log.info(`Running command: ${testCommand}`); 149 | 150 | let testProcess = childProcess.spawn( 151 | testCommand, 152 | spawnArgs 153 | ); 154 | 155 | resolve(await this.handleChildProcess(testProcess)); 156 | }); 157 | 158 | /** 159 | * Runs tests in a given file. 160 | * 161 | * @param testFile The test file's file path, e.g. `/path/to/test.rb`. 162 | * @param debuggerConfig A VS Code debugger configuration. 163 | * @return The raw output from running the tests. 164 | */ 165 | runTestFile = async (testFile: string, debuggerConfig?: vscode.DebugConfiguration) => new Promise(async (resolve, reject) => { 166 | this.log.info(`Running test file: ${testFile}`); 167 | let relativeFile = testFile.replace(`${this.workspace.uri.fsPath}/`, "").replace(`./`, "") 168 | const spawnArgs: childProcess.SpawnOptions = { 169 | cwd: this.workspace.uri.fsPath, 170 | shell: true, 171 | env: this.getProcessEnv() 172 | }; 173 | 174 | // Run tests for a given file at once with a single command. 175 | let testCommand = `${this.testCommandWithDebugger(debuggerConfig)} '${relativeFile}'`; 176 | this.log.info(`Running command: ${testCommand}`); 177 | 178 | let testProcess = childProcess.spawn( 179 | testCommand, 180 | spawnArgs 181 | ); 182 | 183 | resolve(await this.handleChildProcess(testProcess)); 184 | }); 185 | 186 | /** 187 | * Runs the full test suite for the current workspace. 188 | * 189 | * @param debuggerConfig A VS Code debugger configuration. 190 | * @return The raw output from running the test suite. 191 | */ 192 | runFullTestSuite = async (debuggerConfig?: vscode.DebugConfiguration) => new Promise(async (resolve, reject) => { 193 | this.log.info(`Running full test suite.`); 194 | const spawnArgs: childProcess.SpawnOptions = { 195 | cwd: this.workspace.uri.fsPath, 196 | shell: true, 197 | env: this.getProcessEnv() 198 | }; 199 | 200 | let testCommand = this.testCommandWithDebugger(debuggerConfig); 201 | this.log.info(`Running command: ${testCommand}`); 202 | 203 | let testProcess = childProcess.spawn( 204 | testCommand, 205 | spawnArgs 206 | ); 207 | 208 | resolve(await this.handleChildProcess(testProcess)); 209 | }); 210 | 211 | /** 212 | * Handles test state based on the output returned by the Minitest Rake task. 213 | * 214 | * @param test The test that we want to handle. 215 | */ 216 | handleStatus(test: any): void { 217 | this.log.debug(`Handling status of test: ${JSON.stringify(test)}`); 218 | if (test.status === "passed") { 219 | this.testStatesEmitter.fire({ type: 'test', test: test.id, state: 'passed' }); 220 | } else if (test.status === "failed" && test.pending_message === null) { 221 | let errorMessageShort: string = test.exception.message; 222 | let errorMessageLine: number = test.line_number; 223 | let errorMessage: string = test.exception.message; 224 | 225 | if (test.exception.position) { 226 | errorMessageLine = test.exception.position; 227 | } 228 | 229 | // Add backtrace to errorMessage if it exists. 230 | if (test.exception.backtrace) { 231 | errorMessage += `\n\nBacktrace:\n`; 232 | test.exception.backtrace.forEach((line: string) => { 233 | errorMessage += `${line}\n`; 234 | }); 235 | errorMessage += `\n\nFull Backtrace:\n`; 236 | test.exception.full_backtrace.forEach((line: string) => { 237 | errorMessage += `${line}\n`; 238 | }); 239 | } 240 | 241 | this.testStatesEmitter.fire({ 242 | type: 'test', 243 | test: test.id, 244 | state: 'failed', 245 | message: errorMessage, 246 | decorations: [{ 247 | message: errorMessageShort, 248 | line: errorMessageLine - 1 249 | }] 250 | }); 251 | } else if (test.status === "failed" && test.pending_message !== null) { 252 | // Handle pending test cases. 253 | this.testStatesEmitter.fire({ type: 'test', test: test.id, state: 'skipped', message: test.pending_message }); 254 | } 255 | }; 256 | } 257 | -------------------------------------------------------------------------------- /src/rspecTests.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | import { TestSuiteInfo, TestEvent } from 'vscode-test-adapter-api'; 3 | import * as childProcess from 'child_process'; 4 | import { Tests } from './tests'; 5 | 6 | export class RspecTests extends Tests { 7 | testFrameworkName = 'RSpec'; 8 | 9 | /** 10 | * Representation of the RSpec test suite as a TestSuiteInfo object. 11 | * 12 | * @return The RSpec test suite as a TestSuiteInfo object. 13 | */ 14 | tests = async () => new Promise((resolve, reject) => { 15 | try { 16 | // If test suite already exists, use testSuite. Otherwise, load them. 17 | let rspecTests = this.testSuite ? this.testSuite : this.loadTests(); 18 | return resolve(rspecTests); 19 | } catch (err) { 20 | if (err instanceof Error) { 21 | this.log.error(`Error while attempting to load RSpec tests: ${err.message}`); 22 | return reject(err); 23 | } 24 | } 25 | }); 26 | 27 | /** 28 | * Perform a dry-run of the test suite to get information about every test. 29 | * 30 | * @return The raw output from the RSpec JSON formatter. 31 | */ 32 | initTests = async () => new Promise((resolve, reject) => { 33 | let cmd = `${this.getTestCommandWithFilePattern()} --require ${this.getCustomFormatterLocation()}` 34 | + ` --format CustomFormatter --order defined --dry-run`; 35 | 36 | this.log.info(`Running dry-run of RSpec test suite with the following command: ${cmd}`); 37 | 38 | // Allow a buffer of 64MB. 39 | const execArgs: childProcess.ExecOptions = { 40 | cwd: this.workspace.uri.fsPath, 41 | maxBuffer: 8192 * 8192 42 | }; 43 | 44 | childProcess.exec(cmd, execArgs, (err, stdout) => { 45 | if (err) { 46 | this.log.error(`Error while finding RSpec test suite: ${err.message}`); 47 | // Show an error message. 48 | vscode.window.showWarningMessage( 49 | "Ruby Test Explorer failed to find an RSpec test suite. Make sure RSpec is installed and your configured RSpec command is correct.", 50 | "View error message" 51 | ).then(selection => { 52 | if (selection === "View error message") { 53 | let outputJson = JSON.parse(Tests.getJsonFromOutput(stdout)); 54 | let outputChannel = vscode.window.createOutputChannel('Ruby Test Explorer Error Message'); 55 | 56 | if (outputJson.messages.length > 0) { 57 | let outputJsonString = outputJson.messages.join("\n\n"); 58 | let outputJsonArray = outputJsonString.split("\n"); 59 | outputJsonArray.forEach((line: string) => { 60 | outputChannel.appendLine(line); 61 | }) 62 | } else { 63 | outputChannel.append(err.message); 64 | } 65 | outputChannel.show(false); 66 | } 67 | }); 68 | 69 | throw err; 70 | } 71 | resolve(stdout); 72 | }); 73 | }); 74 | 75 | /** 76 | * Get the user-configured RSpec command, if there is one. 77 | * 78 | * @return The RSpec command 79 | */ 80 | protected getTestCommand(): string { 81 | let command: string = (vscode.workspace.getConfiguration('rubyTestExplorer', null).get('rspecCommand') as string); 82 | return command || `bundle exec rspec` 83 | } 84 | 85 | /** 86 | * Get the user-configured rdebug-ide command, if there is one. 87 | * 88 | * @return The rdebug-ide command 89 | */ 90 | protected getDebugCommand(debuggerConfig: vscode.DebugConfiguration, args: string): string { 91 | let command: string = 92 | (vscode.workspace.getConfiguration('rubyTestExplorer', null).get('debugCommand') as string) || 93 | 'rdebug-ide'; 94 | 95 | return ( 96 | `${command} --host ${debuggerConfig.remoteHost} --port ${debuggerConfig.remotePort}` + 97 | ` -- ${process.platform == 'win32' ? '%EXT_DIR%' : '$EXT_DIR'}/debug_rspec.rb ${args}` 98 | ); 99 | } 100 | /** 101 | * Get the user-configured RSpec command and add file pattern detection. 102 | * 103 | * @return The RSpec command 104 | */ 105 | protected getTestCommandWithFilePattern(): string { 106 | let command: string = (vscode.workspace.getConfiguration('rubyTestExplorer', null).get('rspecCommand') as string); 107 | const dir = this.getTestDirectory(); 108 | let pattern = this.getFilePattern().map(p => `${dir}/**{,/*/**}/${p}`).join(',') 109 | command = command || `bundle exec rspec` 110 | return `${command} --pattern '${pattern}'`; 111 | } 112 | 113 | /** 114 | * Get the user-configured test directory, if there is one. 115 | * 116 | * @return The spec directory 117 | */ 118 | getTestDirectory(): string { 119 | let directory: string = (vscode.workspace.getConfiguration('rubyTestExplorer', null).get('rspecDirectory') as string); 120 | return directory || './spec/'; 121 | } 122 | 123 | /** 124 | * Get the absolute path of the custom_formatter.rb file. 125 | * 126 | * @return The spec directory 127 | */ 128 | protected getCustomFormatterLocation(): string { 129 | return this.context.asAbsolutePath('./custom_formatter.rb'); 130 | } 131 | 132 | /** 133 | * Get test command with formatter and debugger arguments 134 | * 135 | * @param debuggerConfig A VS Code debugger configuration. 136 | * @return The test command 137 | */ 138 | protected testCommandWithFormatterAndDebugger(debuggerConfig?: vscode.DebugConfiguration): string { 139 | let args = `--require ${this.getCustomFormatterLocation()} --format CustomFormatter` 140 | let cmd = `${this.getTestCommand()} ${args}` 141 | if (debuggerConfig) { 142 | cmd = this.getDebugCommand(debuggerConfig, args); 143 | } 144 | return cmd 145 | } 146 | 147 | /** 148 | * Get the env vars to run the subprocess with. 149 | * 150 | * @return The env 151 | */ 152 | protected getProcessEnv(): any { 153 | return Object.assign({}, process.env, { 154 | "EXT_DIR": this.getRubyScriptsLocation(), 155 | }); 156 | } 157 | 158 | /** 159 | * Runs a single test. 160 | * 161 | * @param testLocation A file path with a line number, e.g. `/path/to/spec.rb:12`. 162 | * @param debuggerConfig A VS Code debugger configuration. 163 | * @return The raw output from running the test. 164 | */ 165 | runSingleTest = async (testLocation: string, debuggerConfig?: vscode.DebugConfiguration) => new Promise(async (resolve, reject) => { 166 | this.log.info(`Running single test: ${testLocation}`); 167 | const spawnArgs: childProcess.SpawnOptions = { 168 | cwd: this.workspace.uri.fsPath, 169 | shell: true, 170 | env: this.getProcessEnv() 171 | }; 172 | 173 | let testCommand = `${this.testCommandWithFormatterAndDebugger(debuggerConfig)} '${testLocation}'`; 174 | this.log.info(`Running command: ${testCommand}`); 175 | 176 | let testProcess = childProcess.spawn( 177 | testCommand, 178 | spawnArgs 179 | ); 180 | 181 | resolve(await this.handleChildProcess(testProcess)); 182 | }); 183 | 184 | /** 185 | * Runs tests in a given file. 186 | * 187 | * @param testFile The test file's file path, e.g. `/path/to/spec.rb`. 188 | * @param debuggerConfig A VS Code debugger configuration. 189 | * @return The raw output from running the tests. 190 | */ 191 | runTestFile = async (testFile: string, debuggerConfig?: vscode.DebugConfiguration) => new Promise(async (resolve, reject) => { 192 | this.log.info(`Running test file: ${testFile}`); 193 | const spawnArgs: childProcess.SpawnOptions = { 194 | cwd: this.workspace.uri.fsPath, 195 | shell: true 196 | }; 197 | 198 | // Run tests for a given file at once with a single command. 199 | let testCommand = `${this.testCommandWithFormatterAndDebugger(debuggerConfig)} '${testFile}'`; 200 | this.log.info(`Running command: ${testCommand}`); 201 | 202 | let testProcess = childProcess.spawn( 203 | testCommand, 204 | spawnArgs 205 | ); 206 | 207 | resolve(await this.handleChildProcess(testProcess)); 208 | }); 209 | 210 | /** 211 | * Runs the full test suite for the current workspace. 212 | * 213 | * @param debuggerConfig A VS Code debugger configuration. 214 | * @return The raw output from running the test suite. 215 | */ 216 | runFullTestSuite = async (debuggerConfig?: vscode.DebugConfiguration) => new Promise(async (resolve, reject) => { 217 | this.log.info(`Running full test suite.`); 218 | const spawnArgs: childProcess.SpawnOptions = { 219 | cwd: this.workspace.uri.fsPath, 220 | shell: true 221 | }; 222 | 223 | let testCommand = this.testCommandWithFormatterAndDebugger(debuggerConfig); 224 | this.log.info(`Running command: ${testCommand}`); 225 | 226 | let testProcess = childProcess.spawn( 227 | testCommand, 228 | spawnArgs 229 | ); 230 | 231 | resolve(await this.handleChildProcess(testProcess)); 232 | }); 233 | 234 | /** 235 | * Handles test state based on the output returned by the custom RSpec formatter. 236 | * 237 | * @param test The test that we want to handle. 238 | */ 239 | handleStatus(test: any): void { 240 | this.log.debug(`Handling status of test: ${JSON.stringify(test)}`); 241 | if (test.status === "passed") { 242 | this.testStatesEmitter.fire({ type: 'test', test: test.id, state: 'passed' }); 243 | } else if (test.status === "failed" && test.pending_message === null) { 244 | // Remove linebreaks from error message. 245 | let errorMessageNoLinebreaks = test.exception.message.replace(/(\r\n|\n|\r)/, ' '); 246 | // Prepend the class name to the error message string. 247 | let errorMessage: string = `${test.exception.class}:\n${errorMessageNoLinebreaks}`; 248 | 249 | let fileBacktraceLineNumber: number | undefined; 250 | 251 | let filePath = test.file_path.replace('./', ''); 252 | 253 | // Add backtrace to errorMessage if it exists. 254 | if (test.exception.backtrace) { 255 | errorMessage += `\n\nBacktrace:\n`; 256 | test.exception.backtrace.forEach((line: string) => { 257 | errorMessage += `${line}\n`; 258 | // If the backtrace line includes the current file path, try to get the line number from it. 259 | if (line.includes(filePath)) { 260 | let filePathArray = filePath.split('/'); 261 | let fileName = filePathArray[filePathArray.length - 1]; 262 | // Input: spec/models/game_spec.rb:75:in `block (3 levels) in 263 | // Output: 75 264 | let regex = new RegExp(`${fileName}\:(\\d+)`); 265 | let match = line.match(regex); 266 | if (match && match[1]) { 267 | fileBacktraceLineNumber = parseInt(match[1]); 268 | } 269 | } 270 | }); 271 | } 272 | 273 | this.testStatesEmitter.fire({ 274 | type: 'test', 275 | test: test.id, 276 | state: 'failed', 277 | message: errorMessage, 278 | decorations: [{ 279 | // Strip line breaks from the message. 280 | message: errorMessageNoLinebreaks, 281 | line: (fileBacktraceLineNumber ? fileBacktraceLineNumber : test.line_number) - 1 282 | }] 283 | }); 284 | } else if (test.status === "failed" && test.pending_message !== null) { 285 | // Handle pending test cases. 286 | this.testStatesEmitter.fire({ type: 'test', test: test.id, state: 'skipped', message: test.pending_message }); 287 | } 288 | }; 289 | } 290 | -------------------------------------------------------------------------------- /src/tests.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | import { TestSuiteInfo, TestInfo, TestRunStartedEvent, TestRunFinishedEvent, TestSuiteEvent, TestEvent } from 'vscode-test-adapter-api'; 3 | import * as childProcess from 'child_process'; 4 | import * as split2 from 'split2'; 5 | import { Log } from 'vscode-test-adapter-util'; 6 | 7 | export abstract class Tests { 8 | protected context: vscode.ExtensionContext; 9 | protected testStatesEmitter: vscode.EventEmitter; 10 | protected currentChildProcess: childProcess.ChildProcess | undefined; 11 | protected log: Log; 12 | protected testSuite: TestSuiteInfo | undefined; 13 | protected workspace: vscode.WorkspaceFolder; 14 | abstract testFrameworkName: string; 15 | protected debugCommandStartedResolver: Function | undefined; 16 | 17 | /** 18 | * @param context Extension context provided by vscode. 19 | * @param testStatesEmitter An emitter for the test suite's state. 20 | * @param log The Test Adapter logger, for logging. 21 | */ 22 | constructor( 23 | context: vscode.ExtensionContext, 24 | testStatesEmitter: vscode.EventEmitter, 25 | log: Log, 26 | workspace: vscode.WorkspaceFolder 27 | ) { 28 | this.context = context; 29 | this.testStatesEmitter = testStatesEmitter; 30 | this.log = log; 31 | this.workspace = workspace; 32 | } 33 | 34 | abstract tests: () => Promise; 35 | 36 | abstract initTests: () => Promise; 37 | 38 | /** 39 | * Takes the output from initTests() and parses the resulting 40 | * JSON into a TestSuiteInfo object. 41 | * 42 | * @return The full test suite. 43 | */ 44 | public async loadTests(): Promise { 45 | let output = await this.initTests(); 46 | this.log.debug('Passing raw output from dry-run into getJsonFromOutput.'); 47 | this.log.debug(`${output}`); 48 | output = Tests.getJsonFromOutput(output); 49 | this.log.debug('Parsing the below JSON:'); 50 | this.log.debug(`${output}`); 51 | let testMetadata; 52 | try { 53 | testMetadata = JSON.parse(output); 54 | } catch (error) { 55 | this.log.error(`JSON parsing failed: ${error}`); 56 | } 57 | 58 | let tests: Array<{ id: string; full_description: string; description: string; file_path: string; line_number: number; location: number; }> = []; 59 | 60 | testMetadata.examples.forEach((test: { id: string; full_description: string; description: string; file_path: string; line_number: number; location: number; }) => { 61 | let test_location_array: Array = test.id.substring(test.id.indexOf("[") + 1, test.id.lastIndexOf("]")).split(':'); 62 | let test_location_string: string = test_location_array.join(''); 63 | test.location = parseInt(test_location_string); 64 | tests.push(test); 65 | }); 66 | 67 | let testSuite: TestSuiteInfo = await this.getBaseTestSuite(tests); 68 | 69 | // Sort the children of each test suite based on their location in the test tree. 70 | (testSuite.children as Array).forEach((suite: TestSuiteInfo) => { 71 | // NOTE: This will only sort correctly if everything is nested at the same 72 | // level, e.g. 111, 112, 121, etc. Once a fourth level of indentation is 73 | // introduced, the location is generated as e.g. 1231, which won't 74 | // sort properly relative to everything else. 75 | (suite.children as Array).sort((a: TestInfo, b: TestInfo) => { 76 | if ((a as TestInfo).type === "test" && (b as TestInfo).type === "test") { 77 | let aLocation: number = this.getTestLocation(a as TestInfo); 78 | let bLocation: number = this.getTestLocation(b as TestInfo); 79 | return aLocation - bLocation; 80 | } else { 81 | return 0; 82 | } 83 | }) 84 | }); 85 | 86 | this.testSuite = testSuite; 87 | 88 | return Promise.resolve(testSuite); 89 | } 90 | 91 | /** 92 | * Kills the current child process if one exists. 93 | */ 94 | public killChild(): void { 95 | if (this.currentChildProcess) { 96 | this.currentChildProcess.kill(); 97 | } 98 | } 99 | 100 | /** 101 | * Get the user-configured test file pattern. 102 | * 103 | * @return The file pattern 104 | */ 105 | getFilePattern(): Array { 106 | let pattern: Array = (vscode.workspace.getConfiguration('rubyTestExplorer', null).get('filePattern') as Array); 107 | return pattern || ['*_test.rb', 'test_*.rb']; 108 | } 109 | 110 | /** 111 | * Get the user-configured test directory, if there is one. 112 | * 113 | * @return The test directory 114 | */ 115 | abstract getTestDirectory(): string; 116 | 117 | /** 118 | * Pull JSON out of the test framework output. 119 | * 120 | * RSpec and Minitest frequently return bad data even when they're told to 121 | * format the output as JSON, e.g. due to code coverage messages and other 122 | * injections from gems. This gets the JSON by searching for 123 | * `START_OF_TEST_JSON` and an opening curly brace, as well as a closing 124 | * curly brace and `END_OF_TEST_JSON`. These are output by the custom 125 | * RSpec formatter or Minitest Rake task as part of the final JSON output. 126 | * 127 | * @param output The output returned by running a command. 128 | * @return A string representation of the JSON found in the output. 129 | */ 130 | static getJsonFromOutput(output: string): string { 131 | output = output.substring(output.indexOf('START_OF_TEST_JSON{'), output.lastIndexOf('}END_OF_TEST_JSON') + 1); 132 | // Get rid of the `START_OF_TEST_JSON` and `END_OF_TEST_JSON` to verify that the JSON is valid. 133 | return output.substring(output.indexOf("{"), output.lastIndexOf("}") + 1); 134 | } 135 | 136 | /** 137 | * Get the location of the test in the testing tree. 138 | * 139 | * Test ids are in the form of `/spec/model/game_spec.rb[1:1:1]`, and this 140 | * function turns that into `111`. The number is used to order the tests 141 | * in the explorer. 142 | * 143 | * @param test The test we want to get the location of. 144 | * @return A number representing the location of the test in the test tree. 145 | */ 146 | protected getTestLocation(test: TestInfo): number { 147 | return parseInt(test.id.substring(test.id.indexOf("[") + 1, test.id.lastIndexOf("]")).split(':').join('')); 148 | } 149 | 150 | /** 151 | * Convert a string from snake_case to PascalCase. 152 | * Note that the function will return the input string unchanged if it 153 | * includes a '/'. 154 | * 155 | * @param string The string to convert to PascalCase. 156 | * @return The converted string. 157 | */ 158 | protected snakeToPascalCase(string: string): string { 159 | if (string.includes('/')) { return string } 160 | return string.split("_").map(substr => substr.charAt(0).toUpperCase() + substr.slice(1)).join(""); 161 | } 162 | 163 | /** 164 | * Sorts an array of TestSuiteInfo objects by label. 165 | * 166 | * @param testSuiteChildren An array of TestSuiteInfo objects, generally the children of another TestSuiteInfo object. 167 | * @return The input array, sorted by label. 168 | */ 169 | protected sortTestSuiteChildren(testSuiteChildren: Array): Array { 170 | testSuiteChildren = testSuiteChildren.sort((a: TestSuiteInfo, b: TestSuiteInfo) => { 171 | let comparison = 0; 172 | if (a.label > b.label) { 173 | comparison = 1; 174 | } else if (a.label < b.label) { 175 | comparison = -1; 176 | } 177 | return comparison; 178 | }); 179 | 180 | return testSuiteChildren; 181 | } 182 | 183 | /** 184 | * Get the tests in a given file. 185 | */ 186 | public getTestSuiteForFile( 187 | { tests, currentFile, directory }: { 188 | tests: Array<{ 189 | id: string; 190 | full_description: string; 191 | description: string; 192 | file_path: string; 193 | line_number: number; 194 | location: number; 195 | }>; currentFile: string; directory?: string; 196 | }): TestSuiteInfo { 197 | let currentFileTests = tests.filter(test => { 198 | return test.file_path === currentFile 199 | }); 200 | 201 | let currentFileTestsInfo = currentFileTests as unknown as Array; 202 | currentFileTestsInfo.forEach((test: TestInfo) => { 203 | test.type = 'test'; 204 | test.label = ''; 205 | }); 206 | 207 | let currentFileLabel = ''; 208 | 209 | if (directory) { 210 | currentFileLabel = currentFile.replace(`${this.getTestDirectory()}${directory}/`, ''); 211 | } else { 212 | currentFileLabel = currentFile.replace(`${this.getTestDirectory()}`, ''); 213 | } 214 | 215 | let pascalCurrentFileLabel = this.snakeToPascalCase(currentFileLabel.replace('_spec.rb', '')); 216 | 217 | let currentFileTestInfoArray: Array = currentFileTests.map((test) => { 218 | // Concatenation of "/Users/username/whatever/project_dir" and "./spec/path/here.rb", 219 | // but with the latter's first character stripped. 220 | let filePath: string = `${this.workspace.uri.fsPath}${test.file_path.substr(1)}`; 221 | 222 | // RSpec provides test ids like "file_name.rb[1:2:3]". 223 | // This uses the digits at the end of the id to create 224 | // an array of numbers representing the location of the 225 | // test in the file. 226 | let testLocationArray: Array = test.id.substring(test.id.indexOf("[") + 1, test.id.lastIndexOf("]")).split(':').map((x) => { 227 | return parseInt(x); 228 | }); 229 | 230 | // Get the last element in the location array. 231 | let testNumber: number = testLocationArray[testLocationArray.length - 1]; 232 | // If the test doesn't have a name (because it uses the 'it do' syntax), "test #n" 233 | // is appended to the test description to distinguish between separate tests. 234 | let description: string = test.description.startsWith('example at ') ? `${test.full_description}test #${testNumber}` : test.full_description; 235 | 236 | // If the current file label doesn't have a slash in it and it starts with the PascalCase'd 237 | // file name, remove the from the start of the description. This turns, e.g. 238 | // `ExternalAccount Validations blah blah blah' into 'Validations blah blah blah'. 239 | if (!pascalCurrentFileLabel.includes('/') && description.startsWith(pascalCurrentFileLabel)) { 240 | // Optional check for a space following the PascalCase file name. In some 241 | // cases, e.g. 'FileName#method_name` there's no space after the file name. 242 | let regexString = `${pascalCurrentFileLabel}[ ]?`; 243 | let regex = new RegExp(regexString, "g"); 244 | description = description.replace(regex, ''); 245 | } 246 | 247 | let testInfo: TestInfo = { 248 | type: 'test', 249 | id: test.id, 250 | label: description, 251 | file: filePath, 252 | // Line numbers are 0-indexed 253 | line: test.line_number - 1 254 | } 255 | 256 | return testInfo; 257 | }); 258 | 259 | let currentFileAsAbsolutePath = `${this.workspace.uri.fsPath}${currentFile.substr(1)}`; 260 | 261 | let currentFileTestSuite: TestSuiteInfo = { 262 | type: 'suite', 263 | id: currentFile, 264 | label: currentFileLabel, 265 | file: currentFileAsAbsolutePath, 266 | children: currentFileTestInfoArray 267 | } 268 | 269 | return currentFileTestSuite; 270 | } 271 | 272 | /** 273 | * Create the base test suite with a root node and one layer of child nodes 274 | * representing the subdirectories of spec/, and then any files under the 275 | * given subdirectory. 276 | * 277 | * @param tests Test objects returned by our custom RSpec formatter or Minitest Rake task. 278 | * @return The test suite root with its children. 279 | */ 280 | public async getBaseTestSuite( 281 | tests: any[] 282 | ): Promise { 283 | let rootTestSuite: TestSuiteInfo = { 284 | type: 'suite', 285 | id: 'root', 286 | label: `${this.workspace.name} ${this.testFrameworkName}`, 287 | children: [] 288 | }; 289 | 290 | // Create an array of all test files and then abuse Sets to make it unique. 291 | let uniqueFiles = [...new Set(tests.map((test: { file_path: string; }) => test.file_path))]; 292 | 293 | let splitFilesArray: Array = []; 294 | 295 | // Remove the spec/ directory from all the file path. 296 | uniqueFiles.forEach((file) => { 297 | splitFilesArray.push(file.replace(`${this.getTestDirectory()}`, "").split('/')); 298 | }); 299 | 300 | // This gets the main types of tests, e.g. features, helpers, models, requests, etc. 301 | let subdirectories: Array = []; 302 | splitFilesArray.forEach((splitFile) => { 303 | if (splitFile.length > 1) { 304 | subdirectories.push(splitFile[0]); 305 | } 306 | }); 307 | subdirectories = [...new Set(subdirectories)]; 308 | 309 | // A nested loop to iterate through the direct subdirectories of spec/ and then 310 | // organize the files under those subdirectories. 311 | subdirectories.forEach((directory) => { 312 | let filesInDirectory: Array = []; 313 | 314 | let uniqueFilesInDirectory: Array = uniqueFiles.filter((file) => { 315 | return file.startsWith(`${this.getTestDirectory()}${directory}/`); 316 | }); 317 | 318 | // Get the sets of tests for each file in the current directory. 319 | uniqueFilesInDirectory.forEach((currentFile: string) => { 320 | let currentFileTestSuite = this.getTestSuiteForFile({ tests, currentFile, directory }); 321 | filesInDirectory.push(currentFileTestSuite); 322 | }); 323 | 324 | let directoryTestSuite: TestSuiteInfo = { 325 | type: 'suite', 326 | id: directory, 327 | label: directory, 328 | children: filesInDirectory 329 | }; 330 | 331 | rootTestSuite.children.push(directoryTestSuite); 332 | }); 333 | 334 | // Sort test suite types alphabetically. 335 | rootTestSuite.children = this.sortTestSuiteChildren(rootTestSuite.children as Array); 336 | 337 | // Get files that are direct descendants of the spec/ directory. 338 | let topDirectoryFiles = uniqueFiles.filter((filePath) => { 339 | return filePath.replace(`${this.getTestDirectory()}`, "").split('/').length === 1; 340 | }); 341 | 342 | topDirectoryFiles.forEach((currentFile) => { 343 | let currentFileTestSuite = this.getTestSuiteForFile({ tests, currentFile }); 344 | rootTestSuite.children.push(currentFileTestSuite); 345 | }); 346 | 347 | return rootTestSuite; 348 | } 349 | 350 | /** 351 | * Assigns the process to currentChildProcess and handles its output and what happens when it exits. 352 | * 353 | * @param process A process running the tests. 354 | * @return A promise that resolves when the test run completes. 355 | */ 356 | handleChildProcess = async (process: childProcess.ChildProcess) => new Promise((resolve, reject) => { 357 | this.currentChildProcess = process; 358 | 359 | this.currentChildProcess.on('exit', () => { 360 | this.log.info('Child process has exited. Sending test run finish event.'); 361 | this.currentChildProcess = undefined; 362 | this.testStatesEmitter.fire({ type: 'finished' }); 363 | resolve('{}'); 364 | }); 365 | 366 | this.currentChildProcess.stderr!.pipe(split2()).on('data', (data) => { 367 | data = data.toString(); 368 | this.log.debug(`[CHILD PROCESS OUTPUT] ${data}`); 369 | if (data.startsWith('Fast Debugger') && this.debugCommandStartedResolver) { 370 | this.debugCommandStartedResolver() 371 | } 372 | }); 373 | 374 | this.currentChildProcess.stdout!.pipe(split2()).on('data', (data) => { 375 | data = data.toString(); 376 | this.log.debug(`[CHILD PROCESS OUTPUT] ${data}`); 377 | if (data.startsWith('PASSED:')) { 378 | data = data.replace('PASSED: ', ''); 379 | this.testStatesEmitter.fire({ type: 'test', test: data, state: 'passed' }); 380 | } else if (data.startsWith('FAILED:')) { 381 | data = data.replace('FAILED: ', ''); 382 | this.testStatesEmitter.fire({ type: 'test', test: data, state: 'failed' }); 383 | } else if (data.startsWith('RUNNING:')) { 384 | data = data.replace('RUNNING: ', ''); 385 | this.testStatesEmitter.fire({ type: 'test', test: data, state: 'running' }); 386 | } else if (data.startsWith('PENDING:')) { 387 | data = data.replace('PENDING: ', ''); 388 | this.testStatesEmitter.fire({ type: 'test', test: data, state: 'skipped' }); 389 | } 390 | if (data.includes('START_OF_TEST_JSON')) { 391 | resolve(data); 392 | } 393 | }); 394 | }); 395 | 396 | /** 397 | * Runs the test suite by iterating through each test and running it. 398 | * 399 | * @param tests 400 | * @param debuggerConfig A VS Code debugger configuration. 401 | */ 402 | runTests = async (tests: string[], debuggerConfig?: vscode.DebugConfiguration): Promise => { 403 | let testSuite: TestSuiteInfo = await this.tests(); 404 | 405 | for (const suiteOrTestId of tests) { 406 | const node = this.findNode(testSuite, suiteOrTestId); 407 | if (node) { 408 | await this.runNode(node, debuggerConfig); 409 | } 410 | } 411 | } 412 | 413 | /** 414 | * Recursively search for a node in the test suite list. 415 | * 416 | * @param searchNode The test or test suite to search in. 417 | * @param id The id of the test or test suite. 418 | */ 419 | protected findNode(searchNode: TestSuiteInfo | TestInfo, id: string): TestSuiteInfo | TestInfo | undefined { 420 | if (searchNode.id === id) { 421 | return searchNode; 422 | } else if (searchNode.type === 'suite') { 423 | for (const child of searchNode.children) { 424 | const found = this.findNode(child, id); 425 | if (found) return found; 426 | } 427 | } 428 | return undefined; 429 | } 430 | 431 | /** 432 | * Recursively run a node or its children. 433 | * 434 | * @param node A test or test suite. 435 | * @param debuggerConfig A VS Code debugger configuration. 436 | */ 437 | protected async runNode(node: TestSuiteInfo | TestInfo, debuggerConfig?: vscode.DebugConfiguration): Promise { 438 | // Special case handling for the root suite, since it can be run 439 | // with runFullTestSuite() 440 | if (node.type === 'suite' && node.id === 'root') { 441 | this.testStatesEmitter.fire({ type: 'test', test: node.id, state: 'running' }); 442 | 443 | let testOutput = await this.runFullTestSuite(debuggerConfig); 444 | testOutput = Tests.getJsonFromOutput(testOutput); 445 | this.log.debug('Parsing the below JSON:'); 446 | this.log.debug(`${testOutput}`); 447 | let testMetadata = JSON.parse(testOutput); 448 | let tests: Array = testMetadata.examples; 449 | 450 | if (tests && tests.length > 0) { 451 | tests.forEach((test: { id: string | TestInfo; }) => { 452 | this.handleStatus(test); 453 | }); 454 | } 455 | 456 | this.testStatesEmitter.fire({ type: 'suite', suite: node.id, state: 'completed' }); 457 | // If the suite is a file, run the tests as a file rather than as separate tests. 458 | } else if (node.type === 'suite' && node.label.endsWith('.rb')) { 459 | this.testStatesEmitter.fire({ type: 'suite', suite: node.id, state: 'running' }); 460 | 461 | let testOutput = await this.runTestFile(`${node.file}`, debuggerConfig); 462 | 463 | testOutput = Tests.getJsonFromOutput(testOutput); 464 | this.log.debug('Parsing the below JSON:'); 465 | this.log.debug(`${testOutput}`); 466 | let testMetadata = JSON.parse(testOutput); 467 | let tests: Array = testMetadata.examples; 468 | 469 | if (tests && tests.length > 0) { 470 | tests.forEach((test: { id: string | TestInfo; }) => { 471 | this.handleStatus(test); 472 | }); 473 | } 474 | 475 | this.testStatesEmitter.fire({ type: 'suite', suite: node.id, state: 'completed' }); 476 | 477 | } else if (node.type === 'suite') { 478 | 479 | this.testStatesEmitter.fire({ type: 'suite', suite: node.id, state: 'running' }); 480 | 481 | for (const child of node.children) { 482 | await this.runNode(child, debuggerConfig); 483 | } 484 | 485 | this.testStatesEmitter.fire({ type: 'suite', suite: node.id, state: 'completed' }); 486 | 487 | } else if (node.type === 'test') { 488 | if (node.file !== undefined && node.line !== undefined) { 489 | this.testStatesEmitter.fire({ type: 'test', test: node.id, state: 'running' }); 490 | 491 | // Run the test at the given line, add one since the line is 0-indexed in 492 | // VS Code and 1-indexed for RSpec/Minitest. 493 | let testOutput = await this.runSingleTest(`${node.file}:${node.line + 1}`, debuggerConfig); 494 | 495 | testOutput = Tests.getJsonFromOutput(testOutput); 496 | this.log.debug('Parsing the below JSON:'); 497 | this.log.debug(`${testOutput}`); 498 | let testMetadata = JSON.parse(testOutput); 499 | let currentTest = testMetadata.examples[0]; 500 | 501 | this.handleStatus(currentTest); 502 | } 503 | } 504 | } 505 | 506 | public async debugCommandStarted(): Promise { 507 | return new Promise(async (resolve, reject) => { 508 | this.debugCommandStartedResolver = resolve; 509 | setTimeout(() => { reject("debugCommandStarted timed out") }, 10000) 510 | }) 511 | } 512 | 513 | /** 514 | * Get the absolute path of the custom_formatter.rb file. 515 | * 516 | * @return The spec directory 517 | */ 518 | protected getRubyScriptsLocation(): string { 519 | return this.context.asAbsolutePath('./ruby'); 520 | } 521 | 522 | /** 523 | * Runs a single test. 524 | * 525 | * @param testLocation A file path with a line number, e.g. `/path/to/test.rb:12`. 526 | * @param debuggerConfig A VS Code debugger configuration. 527 | * @return The raw output from running the test. 528 | */ 529 | abstract runSingleTest: (testLocation: string, debuggerConfig?: vscode.DebugConfiguration) => Promise; 530 | 531 | /** 532 | * Runs tests in a given file. 533 | * 534 | * @param testFile The test file's file path, e.g. `/path/to/test.rb`. 535 | * @param debuggerConfig A VS Code debugger configuration. 536 | * @return The raw output from running the tests. 537 | */ 538 | abstract runTestFile: (testFile: string, debuggerConfig?: vscode.DebugConfiguration) => Promise; 539 | 540 | /** 541 | * Runs the full test suite for the current workspace. 542 | * 543 | * @param debuggerConfig A VS Code debugger configuration. 544 | * @return The raw output from running the test suite. 545 | */ 546 | abstract runFullTestSuite: (debuggerConfig?: vscode.DebugConfiguration) => Promise; 547 | 548 | /** 549 | * Handles test state based on the output returned by the test command. 550 | * 551 | * @param test The test that we want to handle. 552 | */ 553 | abstract handleStatus(test: any): void; 554 | } 555 | -------------------------------------------------------------------------------- /test/fixtures/minitest/.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "rubyTestExplorer": { 3 | "testFramework": "minitest" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /test/fixtures/minitest/Gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | 3 | gem "minitest" 4 | gem "rake" 5 | -------------------------------------------------------------------------------- /test/fixtures/minitest/Gemfile.lock: -------------------------------------------------------------------------------- 1 | GEM 2 | remote: https://rubygems.org/ 3 | specs: 4 | minitest (5.14.4) 5 | rake (13.0.3) 6 | 7 | PLATFORMS 8 | ruby 9 | 10 | DEPENDENCIES 11 | minitest 12 | rake 13 | 14 | BUNDLED WITH 15 | 2.1.4 16 | -------------------------------------------------------------------------------- /test/fixtures/minitest/Rakefile: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/connorshea/vscode-ruby-test-adapter/9a1fc573033619393a6a8bd8672ef44fc2efd26f/test/fixtures/minitest/Rakefile -------------------------------------------------------------------------------- /test/fixtures/minitest/lib/abs.rb: -------------------------------------------------------------------------------- 1 | class Abs 2 | def apply(n) 3 | case 4 | when n > 0 5 | n 6 | when n == 0 7 | raise "Abs for zero is not supported" 8 | end 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /test/fixtures/minitest/lib/square.rb: -------------------------------------------------------------------------------- 1 | class Square 2 | def apply(n) 3 | n + n 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /test/fixtures/minitest/test/abs_test.rb: -------------------------------------------------------------------------------- 1 | require "test_helper" 2 | 3 | class AbsTest < Minitest::Test 4 | def test_abs_positive 5 | assert_equal 1, Abs.new().apply(1) 6 | end 7 | 8 | def test_abs_0 9 | assert_equal 0, Abs.new().apply(0) 10 | end 11 | 12 | def test_abs_negative 13 | skip "Not implemented yet" 14 | assert_equal 1, Abs.new().apply(-1) 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /test/fixtures/minitest/test/square_test.rb: -------------------------------------------------------------------------------- 1 | require_relative "test_helper" 2 | 3 | class SquareTest < Minitest::Test 4 | def test_square_2 5 | assert_equal 4, Square.new().apply(2) 6 | end 7 | 8 | def test_square_3 9 | assert_equal 9, Square.new().apply(3) 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /test/fixtures/minitest/test/test_helper.rb: -------------------------------------------------------------------------------- 1 | $LOAD_PATH << File.join(__dir__, "../lib") 2 | 3 | require "abs" 4 | require "square" 5 | require "minitest/autorun" 6 | -------------------------------------------------------------------------------- /test/fixtures/rspec/.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "rubyTestExplorer": { 3 | "testFramework": "rspec" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /test/fixtures/rspec/Gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | 3 | gem "rspec" 4 | gem "rake" 5 | -------------------------------------------------------------------------------- /test/fixtures/rspec/Gemfile.lock: -------------------------------------------------------------------------------- 1 | GEM 2 | remote: https://rubygems.org/ 3 | specs: 4 | diff-lcs (1.4.4) 5 | rake (13.0.3) 6 | rspec (3.10.0) 7 | rspec-core (~> 3.10.0) 8 | rspec-expectations (~> 3.10.0) 9 | rspec-mocks (~> 3.10.0) 10 | rspec-core (3.10.1) 11 | rspec-support (~> 3.10.0) 12 | rspec-expectations (3.10.1) 13 | diff-lcs (>= 1.2.0, < 2.0) 14 | rspec-support (~> 3.10.0) 15 | rspec-mocks (3.10.2) 16 | diff-lcs (>= 1.2.0, < 2.0) 17 | rspec-support (~> 3.10.0) 18 | rspec-support (3.10.2) 19 | 20 | PLATFORMS 21 | ruby 22 | 23 | DEPENDENCIES 24 | rake 25 | rspec 26 | 27 | BUNDLED WITH 28 | 2.2.13 29 | -------------------------------------------------------------------------------- /test/fixtures/rspec/Rakefile: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/connorshea/vscode-ruby-test-adapter/9a1fc573033619393a6a8bd8672ef44fc2efd26f/test/fixtures/rspec/Rakefile -------------------------------------------------------------------------------- /test/fixtures/rspec/lib/abs.rb: -------------------------------------------------------------------------------- 1 | class Abs 2 | def apply(n) 3 | case 4 | when n > 0 5 | n 6 | when n == 0 7 | raise "Abs for zero is not supported" 8 | end 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /test/fixtures/rspec/lib/square.rb: -------------------------------------------------------------------------------- 1 | class Square 2 | def apply(n) 3 | n + n 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /test/fixtures/rspec/spec/abs_spec.rb: -------------------------------------------------------------------------------- 1 | require "test_helper" 2 | 3 | describe Abs do 4 | it "finds the absolute value of 1" do 5 | expect(Abs.new.apply(1)).to eq(1) 6 | end 7 | 8 | it "finds the absolute value of 0" do 9 | expect(Abs.new.apply(0)).to eq(0) 10 | end 11 | 12 | it "finds the absolute value of -1" do 13 | skip 14 | expect(Abs.new.apply(-1)).to eq(1) 15 | end 16 | end 17 | 18 | -------------------------------------------------------------------------------- /test/fixtures/rspec/spec/square_spec.rb: -------------------------------------------------------------------------------- 1 | require "test_helper" 2 | 3 | describe Square do 4 | it "finds the square of 2" do 5 | expect(Square.new.apply(2)).to eq(4) 6 | end 7 | 8 | it "finds the square of 3" do 9 | expect(Square.new.apply(3)).to eq(9) 10 | end 11 | end 12 | 13 | -------------------------------------------------------------------------------- /test/fixtures/rspec/spec/test_helper.rb: -------------------------------------------------------------------------------- 1 | $LOAD_PATH << File.join(__dir__, "../lib") 2 | 3 | require "abs" 4 | require "square" 5 | require "rspec" 6 | -------------------------------------------------------------------------------- /test/runMinitestTests.ts: -------------------------------------------------------------------------------- 1 | import * as path from 'path'; 2 | import * as cp from 'child_process'; 3 | 4 | import { runTests, downloadAndUnzipVSCode, resolveCliPathFromVSCodeExecutablePath } from 'vscode-test'; 5 | 6 | async function main() { 7 | try { 8 | const extensionDevelopmentPath = path.resolve(__dirname, '../../'); 9 | 10 | const vscodeExecutablePath = await downloadAndUnzipVSCode('stable') 11 | 12 | const cliPath = resolveCliPathFromVSCodeExecutablePath(vscodeExecutablePath) 13 | cp.spawnSync(cliPath, ['--install-extension', 'hbenl.vscode-test-explorer'], { 14 | encoding: 'utf-8', 15 | stdio: 'inherit' 16 | }) 17 | 18 | await runTests( 19 | { 20 | extensionDevelopmentPath, 21 | extensionTestsPath: path.resolve(__dirname, './suite/frameworks/minitest/index'), 22 | launchArgs: [path.resolve(extensionDevelopmentPath, 'test/fixtures/minitest')] 23 | } 24 | ); 25 | } catch (err) { 26 | console.error(err); 27 | console.error('Failed to run tests'); 28 | process.exit(1); 29 | } 30 | } 31 | 32 | main(); 33 | -------------------------------------------------------------------------------- /test/runRspecTests.ts: -------------------------------------------------------------------------------- 1 | import * as path from 'path'; 2 | import * as cp from 'child_process'; 3 | 4 | import { runTests, downloadAndUnzipVSCode, resolveCliPathFromVSCodeExecutablePath } from 'vscode-test'; 5 | 6 | async function main() { 7 | try { 8 | const extensionDevelopmentPath = path.resolve(__dirname, '../../'); 9 | 10 | const vscodeExecutablePath = await downloadAndUnzipVSCode('stable') 11 | 12 | const cliPath = resolveCliPathFromVSCodeExecutablePath(vscodeExecutablePath) 13 | cp.spawnSync(cliPath, ['--install-extension', 'hbenl.vscode-test-explorer'], { 14 | encoding: 'utf-8', 15 | stdio: 'inherit' 16 | }) 17 | 18 | await runTests( 19 | { 20 | extensionDevelopmentPath, 21 | extensionTestsPath: path.resolve(__dirname, './suite/frameworks/rspec/index'), 22 | launchArgs: [path.resolve(extensionDevelopmentPath, 'test/fixtures/rspec')] 23 | } 24 | ); 25 | } catch (err) { 26 | console.error(err); 27 | console.error('Failed to run tests'); 28 | process.exit(1); 29 | } 30 | } 31 | 32 | main(); 33 | -------------------------------------------------------------------------------- /test/suite/DummyController.ts: -------------------------------------------------------------------------------- 1 | import { TestAdapter, TestController, TestEvent, TestSuiteInfo } from "vscode-test-adapter-api"; 2 | 3 | export const sleep = (msec: number) => new Promise(resolve => setTimeout(resolve, msec)); 4 | 5 | export class DummyController implements TestController { 6 | adapter: TestAdapter | undefined 7 | suite: TestSuiteInfo | undefined 8 | testEvents: { [testRunId: string]: TestEvent[] } 9 | 10 | constructor() { 11 | this.testEvents = {} 12 | } 13 | 14 | async load() { 15 | await this.adapter?.load() 16 | } 17 | 18 | async runTest(...testRunIds: string[]) { 19 | await this.adapter?.run(testRunIds) 20 | } 21 | 22 | registerTestAdapter(adapter: TestAdapter) { 23 | if (this.adapter === undefined) { 24 | this.adapter = adapter 25 | 26 | adapter.tests(event => { 27 | switch (event.type) { 28 | case 'started': 29 | this.suite = undefined 30 | this.testEvents = {} 31 | break 32 | case 'finished': 33 | this.suite = event.suite 34 | break 35 | } 36 | }) 37 | 38 | adapter.testStates(event => { 39 | switch (event.type) { 40 | case 'test': 41 | const id = event.test 42 | if (typeof id === "string") { 43 | const value = this.testEvents[id] 44 | if (!Array.isArray(value)) { 45 | this.testEvents[id] = [event] 46 | } else { 47 | value.push(event) 48 | } 49 | } 50 | } 51 | }) 52 | } 53 | } 54 | 55 | unregisterTestAdapter(adapter: TestAdapter) { 56 | if (this.adapter === adapter) { 57 | this.adapter = undefined 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /test/suite/frameworks/minitest/index.ts: -------------------------------------------------------------------------------- 1 | import * as path from 'path'; 2 | import * as Mocha from 'mocha'; 3 | import * as glob from 'glob'; 4 | 5 | export function run(): Promise { 6 | // Create the mocha test 7 | const mocha = new Mocha({ 8 | ui: 'tdd' 9 | }); 10 | 11 | return new Promise((c, e) => { 12 | glob('**.test.js', { cwd: __dirname }, (err, files) => { 13 | if (err) { 14 | return e(err); 15 | } 16 | 17 | // Add files to the test suite 18 | files.forEach(f => mocha.addFile(path.resolve(__dirname, f))); 19 | 20 | try { 21 | // Run the mocha test 22 | mocha.run(failures => { 23 | if (failures > 0) { 24 | e(new Error(`${failures} tests failed.`)); 25 | } else { 26 | c(); 27 | } 28 | }); 29 | } catch (err) { 30 | e(err); 31 | } 32 | }); 33 | }); 34 | } 35 | -------------------------------------------------------------------------------- /test/suite/frameworks/minitest/minitest.test.ts: -------------------------------------------------------------------------------- 1 | import * as assert from 'assert'; 2 | import * as path from 'path'; 3 | 4 | // You can import and use all API from the 'vscode' module 5 | // as well as import your extension to test it 6 | import * as vscode from 'vscode'; 7 | import { testExplorerExtensionId, TestHub, TestSuiteInfo } from 'vscode-test-adapter-api'; 8 | import { DummyController } from '../../DummyController'; 9 | 10 | suite('Extension Test for Minitest', () => { 11 | test('Load all tests', async () => { 12 | const controller = new DummyController() 13 | const dirPath = vscode.workspace.workspaceFolders![0].uri.path 14 | 15 | const testExplorerExtension = vscode.extensions.getExtension(testExplorerExtensionId)!; 16 | const testHub = testExplorerExtension.exports; 17 | 18 | testHub.registerTestController(controller); 19 | 20 | await controller.load() 21 | 22 | assert.deepStrictEqual( 23 | controller.suite, 24 | { 25 | type: 'suite', 26 | id: 'root', 27 | label: 'minitest Minitest', 28 | children: [ 29 | { 30 | file: path.resolve(dirPath, "test/abs_test.rb"), 31 | id: "./test/abs_test.rb", 32 | label: "abs_test.rb", 33 | type: "suite", 34 | children: [ 35 | { 36 | file: path.resolve(dirPath, "test/abs_test.rb"), 37 | id: "./test/abs_test.rb[4]", 38 | label: "abs positive", 39 | line: 3, 40 | type: "test" 41 | }, 42 | { 43 | file: path.resolve(dirPath, "test/abs_test.rb"), 44 | id: "./test/abs_test.rb[8]", 45 | label: "abs 0", 46 | line: 7, 47 | type: "test" 48 | }, 49 | { 50 | file: path.resolve(dirPath, "test/abs_test.rb"), 51 | id: "./test/abs_test.rb[12]", 52 | label: "abs negative", 53 | line: 11, 54 | type: "test" 55 | } 56 | ] 57 | }, 58 | { 59 | file: path.resolve(dirPath, "test/square_test.rb"), 60 | id: "./test/square_test.rb", 61 | label: "square_test.rb", 62 | type: "suite", 63 | children: [ 64 | { 65 | file: path.resolve(dirPath, "test/square_test.rb"), 66 | id: "./test/square_test.rb[4]", 67 | label: "square 2", 68 | line: 3, 69 | type: "test" 70 | }, 71 | { 72 | file: path.resolve(dirPath, "test/square_test.rb"), 73 | id: "./test/square_test.rb[8]", 74 | label: "square 3", 75 | line: 7, 76 | type: "test" 77 | } 78 | ] 79 | } 80 | ] 81 | } as TestSuiteInfo 82 | ) 83 | }) 84 | 85 | test('run test success', async () => { 86 | const controller = new DummyController() 87 | 88 | const testExplorerExtension = vscode.extensions.getExtension(testExplorerExtensionId)!; 89 | const testHub = testExplorerExtension.exports; 90 | 91 | testHub.registerTestController(controller); 92 | 93 | await controller.load() 94 | await controller.runTest('./test/square_test.rb[4]') 95 | 96 | assert.deepStrictEqual( 97 | controller.testEvents['./test/square_test.rb[4]'], 98 | [ 99 | { state: "running", test: "./test/square_test.rb[4]", type: "test" }, 100 | { state: "running", test: "./test/square_test.rb[4]", type: "test" }, 101 | { state: "passed", test: "./test/square_test.rb[4]", type: "test" }, 102 | { state: "passed", test: "./test/square_test.rb[4]", type: "test" } 103 | ] 104 | ) 105 | }) 106 | 107 | test('run test failure', async () => { 108 | const controller = new DummyController() 109 | 110 | const testExplorerExtension = vscode.extensions.getExtension(testExplorerExtensionId)!; 111 | const testHub = testExplorerExtension.exports; 112 | 113 | testHub.registerTestController(controller); 114 | 115 | await controller.load() 116 | await controller.runTest('./test/square_test.rb[8]') 117 | 118 | assert.deepStrictEqual( 119 | controller.testEvents['./test/square_test.rb[8]'][0], 120 | { state: "running", test: "./test/square_test.rb[8]", type: "test" } 121 | ) 122 | 123 | assert.deepStrictEqual( 124 | controller.testEvents['./test/square_test.rb[8]'][1], 125 | { state: "running", test: "./test/square_test.rb[8]", type: "test" } 126 | ) 127 | 128 | assert.deepStrictEqual( 129 | controller.testEvents['./test/square_test.rb[8]'][2], 130 | { state: "failed", test: "./test/square_test.rb[8]", type: "test" } 131 | ) 132 | 133 | const lastEvent = controller.testEvents['./test/square_test.rb[8]'][3] 134 | assert.strictEqual(lastEvent.state, "failed") 135 | assert.strictEqual(lastEvent.line, undefined) 136 | assert.strictEqual(lastEvent.tooltip, undefined) 137 | assert.strictEqual(lastEvent.description, undefined) 138 | assert.ok(lastEvent.message?.startsWith("Expected: 9\n Actual: 6\n")) 139 | 140 | assert.strictEqual(lastEvent.decorations!.length, 1) 141 | const decoration = lastEvent.decorations![0] 142 | assert.strictEqual(decoration.line, 8) 143 | assert.strictEqual(decoration.file, undefined) 144 | assert.strictEqual(decoration.hover, undefined) 145 | assert.strictEqual(decoration.message, "Expected: 9\n Actual: 6") 146 | }) 147 | 148 | test('run test error', async () => { 149 | const controller = new DummyController() 150 | 151 | const testExplorerExtension = vscode.extensions.getExtension(testExplorerExtensionId)!; 152 | const testHub = testExplorerExtension.exports; 153 | 154 | testHub.registerTestController(controller); 155 | 156 | await controller.load() 157 | await controller.runTest('./test/abs_test.rb[8]') 158 | 159 | assert.deepStrictEqual( 160 | controller.testEvents['./test/abs_test.rb[8]'][0], 161 | { state: "running", test: "./test/abs_test.rb[8]", type: "test" } 162 | ) 163 | 164 | assert.deepStrictEqual( 165 | controller.testEvents['./test/abs_test.rb[8]'][1], 166 | { state: "running", test: "./test/abs_test.rb[8]", type: "test" } 167 | ) 168 | 169 | assert.deepStrictEqual( 170 | controller.testEvents['./test/abs_test.rb[8]'][2], 171 | { state: "failed", test: "./test/abs_test.rb[8]", type: "test" } 172 | ) 173 | 174 | const lastEvent = controller.testEvents['./test/abs_test.rb[8]'][3] 175 | assert.strictEqual(lastEvent.state, "failed") 176 | assert.strictEqual(lastEvent.line, undefined) 177 | assert.strictEqual(lastEvent.tooltip, undefined) 178 | assert.strictEqual(lastEvent.description, undefined) 179 | assert.ok(lastEvent.message?.startsWith("RuntimeError: Abs for zero is not supported\n")) 180 | 181 | assert.strictEqual(lastEvent.decorations!.length, 1) 182 | const decoration = lastEvent.decorations![0] 183 | assert.strictEqual(decoration.line, 8) 184 | assert.strictEqual(decoration.file, undefined) 185 | assert.strictEqual(decoration.hover, undefined) 186 | assert.ok(decoration.message?.startsWith("RuntimeError: Abs for zero is not supported\n")) 187 | }) 188 | 189 | test('run test skip', async () => { 190 | const controller = new DummyController() 191 | 192 | const testExplorerExtension = vscode.extensions.getExtension(testExplorerExtensionId)!; 193 | const testHub = testExplorerExtension.exports; 194 | 195 | testHub.registerTestController(controller); 196 | 197 | await controller.load() 198 | await controller.runTest('./test/abs_test.rb[12]') 199 | 200 | assert.deepStrictEqual( 201 | controller.testEvents['./test/abs_test.rb[12]'][0], 202 | { state: "running", test: "./test/abs_test.rb[12]", type: "test" } 203 | ) 204 | 205 | assert.deepStrictEqual( 206 | controller.testEvents['./test/abs_test.rb[12]'][1], 207 | { state: "running", test: "./test/abs_test.rb[12]", type: "test" } 208 | ) 209 | 210 | assert.deepStrictEqual( 211 | controller.testEvents['./test/abs_test.rb[12]'][2], 212 | { state: "skipped", test: "./test/abs_test.rb[12]", type: "test" } 213 | ) 214 | 215 | const lastEvent = controller.testEvents['./test/abs_test.rb[12]'][3] 216 | assert.strictEqual(lastEvent.state, "skipped") 217 | assert.strictEqual(lastEvent.line, undefined) 218 | assert.strictEqual(lastEvent.tooltip, undefined) 219 | assert.strictEqual(lastEvent.description, undefined) 220 | assert.strictEqual(lastEvent.message, "Not implemented yet") 221 | 222 | assert.strictEqual(lastEvent.decorations, undefined) 223 | }) 224 | }); 225 | -------------------------------------------------------------------------------- /test/suite/frameworks/rspec/index.ts: -------------------------------------------------------------------------------- 1 | import * as path from 'path'; 2 | import * as Mocha from 'mocha'; 3 | import * as glob from 'glob'; 4 | 5 | export function run(): Promise { 6 | // Create the mocha test 7 | const mocha = new Mocha({ 8 | ui: 'tdd' 9 | }); 10 | 11 | return new Promise((c, e) => { 12 | glob('**.test.js', { cwd: __dirname }, (err, files) => { 13 | if (err) { 14 | return e(err); 15 | } 16 | 17 | // Add files to the test suite 18 | files.forEach(f => mocha.addFile(path.resolve(__dirname, f))); 19 | 20 | try { 21 | // Run the mocha test 22 | mocha.run(failures => { 23 | if (failures > 0) { 24 | e(new Error(`${failures} tests failed.`)); 25 | } else { 26 | c(); 27 | } 28 | }); 29 | } catch (err) { 30 | e(err); 31 | } 32 | }); 33 | }); 34 | } 35 | -------------------------------------------------------------------------------- /test/suite/frameworks/rspec/rspec.test.ts: -------------------------------------------------------------------------------- 1 | import * as assert from 'assert'; 2 | import * as path from 'path'; 3 | 4 | // You can import and use all API from the 'vscode' module 5 | // as well as import your extension to test it 6 | import * as vscode from 'vscode'; 7 | import { testExplorerExtensionId, TestHub, TestSuiteInfo } from 'vscode-test-adapter-api'; 8 | import { DummyController } from '../../DummyController'; 9 | 10 | suite('Extension Test for RSpec', () => { 11 | test('Load all tests', async () => { 12 | const controller = new DummyController() 13 | const dirPath = vscode.workspace.workspaceFolders![0].uri.path 14 | 15 | const testExplorerExtension = vscode.extensions.getExtension(testExplorerExtensionId)!; 16 | const testHub = testExplorerExtension.exports; 17 | 18 | testHub.registerTestController(controller); 19 | 20 | await controller.load() 21 | 22 | assert.deepStrictEqual( 23 | controller.suite, 24 | { 25 | type: 'suite', 26 | id: 'root', 27 | label: 'rspec RSpec', 28 | children: [ 29 | { 30 | file: path.resolve(dirPath, "spec/abs_spec.rb"), 31 | id: "./spec/abs_spec.rb", 32 | label: "abs_spec.rb", 33 | type: "suite", 34 | children: [ 35 | { 36 | file: path.resolve(dirPath, "spec/abs_spec.rb"), 37 | id: "./spec/abs_spec.rb[1:1]", 38 | label: "finds the absolute value of 1", 39 | line: 3, 40 | type: "test" 41 | }, 42 | { 43 | file: path.resolve(dirPath, "spec/abs_spec.rb"), 44 | id: "./spec/abs_spec.rb[1:2]", 45 | label: "finds the absolute value of 0", 46 | line: 7, 47 | type: "test" 48 | }, 49 | { 50 | file: path.resolve(dirPath, "spec/abs_spec.rb"), 51 | id: "./spec/abs_spec.rb[1:3]", 52 | label: "finds the absolute value of -1", 53 | line: 11, 54 | type: "test" 55 | } 56 | ] 57 | }, 58 | { 59 | file: path.resolve(dirPath, "spec/square_spec.rb"), 60 | id: "./spec/square_spec.rb", 61 | label: "square_spec.rb", 62 | type: "suite", 63 | children: [ 64 | { 65 | file: path.resolve(dirPath, "spec/square_spec.rb"), 66 | id: "./spec/square_spec.rb[1:1]", 67 | label: "finds the square of 2", 68 | line: 3, 69 | type: "test" 70 | }, 71 | { 72 | file: path.resolve(dirPath, "spec/square_spec.rb"), 73 | id: "./spec/square_spec.rb[1:2]", 74 | label: "finds the square of 3", 75 | line: 7, 76 | type: "test" 77 | } 78 | ] 79 | } 80 | ] 81 | } as TestSuiteInfo 82 | ) 83 | }) 84 | 85 | test('run test success', async () => { 86 | const controller = new DummyController() 87 | 88 | const testExplorerExtension = vscode.extensions.getExtension(testExplorerExtensionId)!; 89 | const testHub = testExplorerExtension.exports; 90 | 91 | testHub.registerTestController(controller); 92 | 93 | await controller.load() 94 | await controller.runTest('./spec/square_spec.rb') 95 | 96 | assert.deepStrictEqual( 97 | controller.testEvents['./spec/square_spec.rb[1:1]'], 98 | [ 99 | { state: "passed", test: "./spec/square_spec.rb[1:1]", type: "test" }, 100 | { state: "passed", test: "./spec/square_spec.rb[1:1]", type: "test" } 101 | ] 102 | ) 103 | }) 104 | 105 | test('run test failure', async () => { 106 | const controller = new DummyController() 107 | 108 | const testExplorerExtension = vscode.extensions.getExtension(testExplorerExtensionId)!; 109 | const testHub = testExplorerExtension.exports; 110 | 111 | testHub.registerTestController(controller); 112 | 113 | await controller.load() 114 | await controller.runTest('./spec/square_spec.rb') 115 | 116 | assert.deepStrictEqual( 117 | controller.testEvents['./spec/square_spec.rb[1:2]'][0], 118 | { state: "failed", test: "./spec/square_spec.rb[1:2]", type: "test" } 119 | ) 120 | 121 | const lastEvent = controller.testEvents['./spec/square_spec.rb[1:2]'][1] 122 | assert.strictEqual(lastEvent.state, "failed") 123 | assert.strictEqual(lastEvent.line, undefined) 124 | assert.strictEqual(lastEvent.tooltip, undefined) 125 | assert.strictEqual(lastEvent.description, undefined) 126 | assert.ok(lastEvent.message?.startsWith("RSpec::Expectations::ExpectationNotMetError:\n expected: 9\n got: 6\n")) 127 | 128 | assert.strictEqual(lastEvent.decorations!.length, 1) 129 | const decoration = lastEvent.decorations![0] 130 | assert.strictEqual(decoration.line, 8) 131 | assert.strictEqual(decoration.file, undefined) 132 | assert.strictEqual(decoration.hover, undefined) 133 | assert.strictEqual(decoration.message, " expected: 9\n got: 6\n\n(compared using ==)\n") 134 | }) 135 | 136 | test('run test error', async () => { 137 | const controller = new DummyController() 138 | 139 | const testExplorerExtension = vscode.extensions.getExtension(testExplorerExtensionId)!; 140 | const testHub = testExplorerExtension.exports; 141 | 142 | testHub.registerTestController(controller); 143 | 144 | await controller.load() 145 | await controller.runTest('./spec/abs_spec.rb[1:2]') 146 | 147 | assert.deepStrictEqual( 148 | controller.testEvents['./spec/abs_spec.rb[1:2]'][0], 149 | { state: "running", test: "./spec/abs_spec.rb[1:2]", type: "test" } 150 | ) 151 | 152 | assert.deepStrictEqual( 153 | controller.testEvents['./spec/abs_spec.rb[1:2]'][1], 154 | { state: "failed", test: "./spec/abs_spec.rb[1:2]", type: "test" } 155 | ) 156 | 157 | const lastEvent = controller.testEvents['./spec/abs_spec.rb[1:2]'][2] 158 | assert.strictEqual(lastEvent.state, "failed") 159 | assert.strictEqual(lastEvent.line, undefined) 160 | assert.strictEqual(lastEvent.tooltip, undefined) 161 | assert.strictEqual(lastEvent.description, undefined) 162 | assert.ok(lastEvent.message?.startsWith("RuntimeError:\nAbs for zero is not supported")) 163 | 164 | assert.strictEqual(lastEvent.decorations!.length, 1) 165 | const decoration = lastEvent.decorations![0] 166 | assert.strictEqual(decoration.line, 8) 167 | assert.strictEqual(decoration.file, undefined) 168 | assert.strictEqual(decoration.hover, undefined) 169 | assert.ok(decoration.message?.startsWith("Abs for zero is not supported")) 170 | }) 171 | 172 | test('run test skip', async () => { 173 | const controller = new DummyController() 174 | 175 | const testExplorerExtension = vscode.extensions.getExtension(testExplorerExtensionId)!; 176 | const testHub = testExplorerExtension.exports; 177 | 178 | testHub.registerTestController(controller); 179 | 180 | await controller.load() 181 | await controller.runTest('./spec/abs_spec.rb[1:3]') 182 | 183 | assert.deepStrictEqual( 184 | controller.testEvents['./spec/abs_spec.rb[1:3]'][0], 185 | { state: "running", test: "./spec/abs_spec.rb[1:3]", type: "test" } 186 | ) 187 | 188 | assert.deepStrictEqual( 189 | controller.testEvents['./spec/abs_spec.rb[1:3]'][1], 190 | { state: "skipped", test: "./spec/abs_spec.rb[1:3]", type: "test" } 191 | ) 192 | }) 193 | }); 194 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2019", 4 | "lib": [ "es2019" ], 5 | "module": "commonjs", 6 | "outDir": "out", 7 | "importHelpers": true, 8 | "sourceMap": true, 9 | "rootDirs": ["src", "test"], 10 | "strict": true, 11 | "noImplicitReturns": true, 12 | "noUnusedLocals": true, 13 | "removeComments": true, 14 | "skipLibCheck": true 15 | }, 16 | } 17 | --------------------------------------------------------------------------------