├── .github ├── dependabot.yml └── workflows │ ├── no-response.yml │ └── test-package.yml ├── .gitignore ├── AUTHORS ├── CHANGELOG.md ├── LICENSE ├── PATENTS ├── README.md ├── analysis_options.yaml ├── benchmark ├── .gitignore ├── many_isolates.dart └── run_benchmarks.dart ├── bin ├── collect_coverage.dart ├── format_coverage.dart ├── run_and_collect.dart └── test_with_coverage.dart ├── lib ├── coverage.dart └── src │ ├── chrome.dart │ ├── collect.dart │ ├── formatter.dart │ ├── hitmap.dart │ ├── resolver.dart │ ├── run_and_collect.dart │ └── util.dart ├── pubspec.yaml └── test ├── README.md ├── chrome_test.dart ├── collect_coverage_api_test.dart ├── collect_coverage_mock_test.dart ├── collect_coverage_mock_test.mocks.dart ├── collect_coverage_test.dart ├── format_coverage_test.dart ├── function_coverage_test.dart ├── lcov_test.dart ├── resolver_test.dart ├── run_and_collect_test.dart ├── test_files ├── chrome_precise_report.txt ├── function_coverage_app.dart ├── main_test.js ├── main_test.js.map ├── test_app.dart ├── test_app.g.dart ├── test_app_isolate.dart ├── test_library.dart └── test_library_part.dart ├── test_util.dart ├── test_with_coverage_package ├── lib │ └── validate_lib.dart ├── pubspec.yaml └── test │ ├── product_test.dart │ └── sum_test.dart ├── test_with_coverage_test.dart └── util_test.dart /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # Dependabot configuration file. 2 | # See https://docs.github.com/en/code-security/dependabot/dependabot-version-updates 3 | version: 2 4 | 5 | updates: 6 | - package-ecosystem: github-actions 7 | directory: / 8 | schedule: 9 | interval: monthly 10 | labels: 11 | - autosubmit 12 | groups: 13 | github-actions: 14 | patterns: 15 | - "*" 16 | -------------------------------------------------------------------------------- /.github/workflows/no-response.yml: -------------------------------------------------------------------------------- 1 | # A workflow to close issues where the author hasn't responded to a request for 2 | # more information; see https://github.com/actions/stale. 3 | 4 | name: No Response 5 | 6 | # Run as a daily cron. 7 | on: 8 | schedule: 9 | # Every day at 8am 10 | - cron: '0 8 * * *' 11 | 12 | # All permissions not specified are set to 'none'. 13 | permissions: 14 | issues: write 15 | pull-requests: write 16 | 17 | jobs: 18 | no-response: 19 | runs-on: ubuntu-latest 20 | if: ${{ github.repository_owner == 'dart-lang' }} 21 | steps: 22 | - uses: actions/stale@28ca1036281a5e5922ead5184a1bbf96e5fc984e 23 | with: 24 | days-before-stale: -1 25 | days-before-close: 14 26 | stale-issue-label: "needs-info" 27 | close-issue-message: > 28 | Without additional information we're not able to resolve this issue. 29 | Feel free to add more info or respond to any questions above and we 30 | can reopen the case. Thanks for your contribution! 31 | stale-pr-label: "needs-info" 32 | close-pr-message: > 33 | Without additional information we're not able to resolve this PR. 34 | Feel free to add more info or respond to any questions above. 35 | Thanks for your contribution! 36 | -------------------------------------------------------------------------------- /.github/workflows/test-package.yml: -------------------------------------------------------------------------------- 1 | name: Dart CI 2 | 3 | on: 4 | # Run on PRs and pushes to the default branch. 5 | push: 6 | branches: [ master ] 7 | pull_request: 8 | branches: [ master ] 9 | schedule: 10 | - cron: "0 0 * * 0" 11 | 12 | env: 13 | PUB_ENVIRONMENT: bot.github 14 | 15 | jobs: 16 | # Check code formatting and static analysis on a single OS (linux) 17 | # against Dart dev. 18 | analyze: 19 | runs-on: ubuntu-latest 20 | strategy: 21 | fail-fast: false 22 | matrix: 23 | sdk: [dev] 24 | steps: 25 | - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 26 | - uses: dart-lang/setup-dart@0a8a0fc875eb934c15d08629302413c671d3f672 27 | with: 28 | sdk: ${{ matrix.sdk }} 29 | - id: install 30 | name: Install dependencies 31 | run: dart pub get 32 | - name: Check formatting 33 | run: dart format --output=none --set-exit-if-changed . 34 | if: always() && steps.install.outcome == 'success' 35 | - name: Analyze code 36 | run: dart analyze --fatal-infos 37 | if: always() && steps.install.outcome == 'success' 38 | 39 | # Run tests on a matrix consisting of two dimensions: 40 | # 1. OS: ubuntu-latest, macos-latest, windows-latest 41 | # 2. release channel: dev 42 | test: 43 | needs: analyze 44 | runs-on: ${{ matrix.os }} 45 | strategy: 46 | fail-fast: false 47 | matrix: 48 | os: [ubuntu-latest, macos-latest, windows-latest] 49 | sdk: [3.4, dev] 50 | exclude: 51 | # VM service times out on windows before Dart 3.5 52 | # https://github.com/dart-lang/coverage/issues/490 53 | - os: windows-latest 54 | sdk: 3.4 55 | steps: 56 | - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 57 | - uses: dart-lang/setup-dart@0a8a0fc875eb934c15d08629302413c671d3f672 58 | with: 59 | sdk: ${{ matrix.sdk }} 60 | - id: install 61 | name: Install dependencies 62 | run: dart pub get 63 | - name: Run VM tests 64 | run: dart test --platform vm 65 | if: always() && steps.install.outcome == 'success' 66 | 67 | coverage: 68 | needs: test 69 | runs-on: ubuntu-latest 70 | steps: 71 | - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 72 | - uses: dart-lang/setup-dart@0a8a0fc875eb934c15d08629302413c671d3f672 73 | with: 74 | sdk: dev 75 | - id: install 76 | name: Install dependencies 77 | run: dart pub get 78 | - name: Collect and report coverage 79 | run: dart run bin/test_with_coverage.dart --port=9292 80 | - name: Upload coverage 81 | uses: coverallsapp/github-action@643bc377ffa44ace6394b2b5d0d3950076de9f63 82 | with: 83 | github-token: ${{ secrets.GITHUB_TOKEN }} 84 | path-to-lcov: coverage/lcov.info 85 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Pub 2 | packages 3 | pubspec.lock 4 | build 5 | .dart_tool/ 6 | .pub 7 | .packages 8 | 9 | # IDEs 10 | .project 11 | .settings 12 | .idea 13 | *.iml 14 | 15 | # Temp files 16 | *~ 17 | coverage/ 18 | var/ 19 | -------------------------------------------------------------------------------- /AUTHORS: -------------------------------------------------------------------------------- 1 | # Below is a list of people and organizations that have contributed 2 | # to the coverage project. Names should be added to the list like so: 3 | # 4 | # Name/Organization 5 | 6 | Google Inc. 7 | Achilleas Anagnostopoulos 8 | Adam Singer 9 | Cédric Belin 10 | Evan Weible 11 | Günter Zöchbauer 12 | Will Drach 13 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 1.9.1-wip 2 | 3 | - Remove outdated VM service version checks. 4 | 5 | ## 1.9.0 6 | 7 | - Require Dart ^3.4 8 | - Fix bug where some ranges were able to bypass the `--scope-output` filters. 9 | - Add --ignore-files option allowing to exclude files from coverage reports using glob patterns 10 | 11 | ## 1.8.0 12 | 13 | - Copy collect_coverage's `--scope-output` flag to test_with_coverage. 14 | 15 | ## 1.7.2 16 | 17 | - Update `package:vm_service` constraints to '>=12.0.0 <15.0.0'. 18 | 19 | ## 1.7.1 20 | 21 | - Update `package:vm_service` constraints to '>=12.0.0 <14.0.0'. 22 | 23 | ## 1.7.0 24 | 25 | - Require Dart 3.0.0 26 | - Update `package:vm_service` constraints to '^12.0.0'. 27 | - Add `coverableLineCache` parameter to `collect`. This allows the set of 28 | coverable lines to be cached between calls to `collect`, avoiding the need to 29 | force compile the same libraries repeatedly. This is only useful when running 30 | multiple coverage collections over the same libraries. 31 | 32 | ## 1.6.4 33 | 34 | - allow omitting space between `//` and `coverage` in coverage ignore comments 35 | - allow text after coverage ignore comments 36 | - throw FormatException when encountering unbalanced ignore comments instead of silently erroring 37 | - Update `package:vm_service` constraints to '>= 9.4.0 <12.0.0'. 38 | 39 | ## 1.6.3 40 | 41 | - Require Dart 2.18 42 | - Update `package:vm_service` constraints to '>=9.4.0 <12.0.0'. 43 | 44 | ## 1.6.2 45 | 46 | - Update `package:vm_service` constraints to '>=9.4.0 <11.0.0'. 47 | 48 | ## 1.6.1 49 | 50 | - Handle SentinelExceptions thrown by vm_service. 51 | 52 | ## 1.6.0 53 | 54 | - Update to vm_service 9.4.0. 55 | - Use IsolateRef.isolateGroupId to speed up coverage collection. 56 | - Ignore uncoverable abstract methods. 57 | - Fix bug where 'ingore-line' comments etc are applied even if they're inside 58 | string literals. 59 | - Change the LICENSE file to the standard Dart BSD license. 60 | 61 | ## 1.5.0 62 | 63 | - Support passing extra arguments to `test_with_coverage` which are then passed 64 | to `package:test`. 65 | 66 | Example: `dart run coverage:test_with_coverage -- --preset CI` 67 | 68 | ## 1.4.0 69 | 70 | - Added `HitMap.parseJsonSync` which takes a cache of ignored lines which can 71 | speedup calls when `checkIgnoredLines` is true and the function is called 72 | several times with overlapping files in the input json. 73 | - Bump the version of vm_service to 9.0.0. 74 | 75 | ## 1.3.2 76 | 77 | - Fix test_with_coverage listening to an unsupported signal on windows. 78 | - Fix `--reportOn` on windows using incorrect path separators. 79 | 80 | ## 1.3.1 81 | 82 | - Fix running `dart pub global run coverage:test_with_coverage` or 83 | `dart run coverage:test_with_coverage` 84 | 85 | ## 1.3.0 86 | 87 | - Bump the minimum Dart SDK version to 2.15.0 88 | - Add a `--package` flag, which takes the package's root directory, instead of 89 | the .package file. Deprecate the `--packages` flag. 90 | - Deprecate the packagesPath parameter and add packagePath instead, in 91 | `HitMap.parseJson`, `HitMap.parseFiles`, `createHitmap`, and `parseCoverage`. 92 | - Add a new executable to the package, `test_with_coverage`. This simplifies the 93 | most common use case of coverage, running all the tests for a package, and 94 | generating an lcov.info file. 95 | - Use the `libraryFilters` option in `getSourceReport` to speed up coverage runs 96 | that use `scopedOutput`. 97 | 98 | ## 1.2.0 99 | 100 | - Support branch level coverage information, when running tests in the Dart VM. 101 | This is not supported for web tests yet. 102 | - Add flag `--branch-coverage` (abbr `-b`) to collect_coverage that collects 103 | branch coverage information. The VM must also be run with the 104 | `--branch-coverage` flag. 105 | - Add flag `--pretty-print-branch` to format_coverage that works similarly to 106 | pretty print, but outputs branch level coverage, rather than line level. 107 | - Update `--lcov` (abbr `-l`) in format_coverage to output branch level 108 | coverage, in addition to line level. 109 | - Add an optional bool flag to `collect` that controls whether branch coverage 110 | is collected. 111 | - Add a `branchHits` field to `HitMap`. 112 | - Add support for scraping the service URI from the new Dart VM service message. 113 | - Correctly parse package_config files on Windows when the root URI is relative. 114 | 115 | ## 1.1.0 116 | 117 | - Support function level coverage information, when running tests in the Dart 118 | VM. This is not supported for web tests yet. 119 | - Add flag `--function-coverage` (abbr `-f`) to collect_coverage that collects 120 | function coverage information. 121 | - Add flag `--pretty-print-func` (abbr `-f`) to format_coverage that works 122 | similarly to pretty print, but outputs function level coverage, rather than 123 | line level. 124 | - Update `--lcov` (abbr `-l`) in format_coverage to output function level 125 | coverage, in addition to line level. 126 | - Add an optional bool flag to `collect` that controls whether function coverage 127 | is collected. 128 | - Added `HitMap.parseJson`, `FileHitMaps.merge`, `HitMap.parseFiles`, 129 | `HitMap.toJson`, `FileHitMapsFormatter.formatLcov`, and 130 | `FileHitMapsFormatter.prettyPrint` that switch from using `Map` to 131 | represent line coverage to using `HitMap` (which contains both line and 132 | function coverage). Document the old versions of these functions as 133 | deprecated. We will delete the old functions when we update to coverage 134 | version 2.0.0. 135 | - Ensure `createHitmap` returns a sorted hitmap. This fixes a potential issue 136 | with ignore line annotations. 137 | - Use the `reportLines` flag in `vm_service`'s `getSourceReport` RPC. This 138 | typically halves the number of RPCs that the coverage collector needs to run. 139 | - Require Dart `>=2.14.0` 140 | 141 | ## 1.0.4 142 | 143 | - Updated dependency on `vm_service` package from `>=6.1.0 <8.0.0`to 144 | `>=8.1.0 <9.0.0`. 145 | 146 | ## 1.0.3 147 | 148 | - Updated dependency on `vm_service` package from `^6.1.0` to `>=6.1.0 <8.0.0`. 149 | 150 | ## 1.0.2 151 | 152 | - Fix an issue where the `--packages` argument wasn't passed to 153 | `format_coverage`. 154 | 155 | ## 1.0.1 156 | 157 | - Allow the chrome `sourceUriProvider` to return `null`. 158 | 159 | ## 1.0.0 160 | 161 | - Migrate to null safety. 162 | - Removed support for SDK `1.x.x`. 163 | 164 | ## 0.15.2 165 | 166 | - Update `args`, `logging`, and `package_config` deps to allow the latest stable 167 | releases. 168 | 169 | ## 0.15.1 170 | 171 | - Updated dependency on `vm_service` package from `>=1.0.0 < 5.0.0` to 172 | `>=1.0.0 <7.0.0`. 173 | 174 | ## 0.15.0 175 | 176 | - BREAKING CHANGE: Eliminate the `--package-root` option from 177 | `bin/run_and_collect.dart` and `bin/format_coverage.dart` as well as from 178 | `runAndCollect` and the `Resolver` constructor. 179 | 180 | ## 0.14.2 181 | 182 | - Fix an issue where `--wait-paused` with `collect` would attempt to collect 183 | coverage if no isolates have started. 184 | 185 | ## 0.14.1 186 | 187 | - Updated dependency on `vm_service` package from `>=1.0.0 < 5.0.0` to 188 | `>=1.0.0 <6.0.0`. 189 | 190 | ## 0.14.0 191 | 192 | - Add flag `--check-ignore` that is used to ignore lines from coverage depending 193 | on the comments. 194 | 195 | Use // coverage:ignore-line to ignore one line. Use // coverage:ignore-start 196 | and // coverage:ignore-end to ignore range of lines inclusive. Use // 197 | coverage:ignore-file to ignore the whole file. 198 | 199 | ## 0.13.11 200 | 201 | - Revert breaking change in 13.10 202 | 203 | ## 0.13.10 204 | 205 | - Add flag `--check-ignore` that is used to ignore lines from coverage depending 206 | on the comments. 207 | 208 | Use // coverage:ignore-line to ignore one line. Use // coverage:ignore-start 209 | and // coverage:ignore-end to ignore range of lines inclusive. Use // 210 | coverage:ignore-file to ignore the whole file. 211 | 212 | ## 0.13.9 213 | 214 | - Don't crash on empty JSON input files. 215 | - Loosen the dependency on the `vm_service` package from `>=1.0.0 <4.0.0` to 216 | `>=1.0.0 <5.0.0`. 217 | 218 | ## 0.13.8 219 | 220 | - Update to package_config `1.9.0` which supports package_config.json files and 221 | should be forwards compatible with `2.0.0`. 222 | - Deprecate the `packageRoot` argument on `Resolver`. 223 | 224 | ## 0.13.7 225 | 226 | - Loosen the dependency on the `vm_service` package from `>=1.0.0 <3.0.0` to 227 | `>=1.0.0 <4.0.0`. 228 | 229 | ## 0.13.6 230 | 231 | - Now consider all `.json` files for the `format_coverage` command. 232 | 233 | ## 0.13.5 234 | 235 | - Update `parseChromeCoverage` to merge coverage information for a given line. 236 | - Handle source map parse errors in `parseChromeCoverage`. Coverage will not be 237 | considered for Dart files that have corresponding invalid source maps. 238 | 239 | ## 0.13.4 240 | 241 | - Add `parseChromeCoverage` for creating a Dart based coverage report from a 242 | Chrome coverage report. 243 | 244 | ## 0.13.3+3 - 2019-12-03 245 | 246 | - Re-loosen the dependency on the `vm_service` package from `>=1.0.0 < 2.1.2` to 247 | `>=1.0.0 <3.0.0` now that breakage introduced in version `2.1.2` has been 248 | resolved. Fixed in: 249 | https://github.com/dart-lang/sdk/commit/7a911ce3f1e945f2cbd1967c6109127e3acbab5a. 250 | 251 | ## 0.13.3+2 - 2019-12-02 252 | 253 | - Tighten the dependency on the `vm_service` package from `>=1.0.0 <3.0.0` down 254 | to `>=1.0.0 <2.1.2` in order to exclude version `2.1.2` which is broken on the 255 | current stable Dart VM due to a missing SDK constraint in its pubspec.yaml. 256 | The breakage was introduced in: 257 | https://github.com/dart-lang/sdk/commit/9e636b5ab4de850fb19bc262e0686fdf14bfbfc0. 258 | 259 | ## 0.13.3+1 - 2019-10-10 260 | 261 | - Loosen the dependency on the `vm_service` package from `^1.0.0` to 262 | `>=1.0.0 <3.0.0`. Ensures dependency version range compatibility with the 263 | latest versions of package `test`. 264 | 265 | ## 0.13.3 266 | 267 | - Adds a new named argument to `collect` to filter coverage results by a set of 268 | VM isolate IDs. 269 | - Migrates implementation of VM service protocol library from 270 | `package:vm_service_lib`, which is no longer maintained, to 271 | `package:vm_service`, which is. 272 | 273 | ## 0.13.2 274 | 275 | - Add new multi-flag option `--scope-output` which restricts coverage output so 276 | that only scripts that start with the provided path are considered. 277 | 278 | ## 0.13.1 279 | 280 | - Handle scenario where the VM returns empty coverage information for a range. 281 | 282 | ## 0.13.0 283 | 284 | - BREAKING CHANGE: Skips collecting coverage for `dart:` libraries by default, 285 | which provides a significant performance boost. To restore the previous 286 | behaviour and collect coverage for these libraries, use the `--include-dart` 287 | flag. 288 | - Disables WebSocket compression for coverage collection. Since almost all 289 | coverage collection runs happen over the loopback interface to localhost, this 290 | improves performance and reduces CPU usage. 291 | - Migrates implementation of VM service protocol library from 292 | `package:vm_service_client`, which is no longer maintained, to 293 | `package:vm_service_lib`, which is. 294 | 295 | ## 0.12.4 296 | 297 | - `collect()` now immediately throws `ArgumentError` if a null URI is passed in 298 | the `serviceUri` parameter to avoid a less-easily debuggable null dereference 299 | later. See dart-lang/coverage#240 for details. 300 | 301 | ## 0.12.3 302 | 303 | - Fixed dart-lang/coverage#194. During collection, we now track each script by 304 | its (unique) VMScriptRef. This ensures we look up the correct script when 305 | computing the affected line for each hit token. The hitmap remains URI based, 306 | since in the end, we want a single, unified set of line to hitCount mappings 307 | per script. 308 | 309 | ## 0.12.2 310 | 311 | - Dart SDK upper bound raised to <3.0.0. 312 | 313 | ## 0.12.1 314 | 315 | - Minor type, dartfmt fixes. 316 | - Require package:args >= 1.4.0. 317 | 318 | ## 0.12.0 319 | 320 | - BREAKING CHANGE: This version requires Dart SDK 2.0.0-dev.64.1 or later. 321 | - Strong mode fixes as of Dart SDK 2.0.0-dev.64.1. 322 | 323 | ## 0.11.0 324 | 325 | - BREAKING CHANGE: This version requires Dart SDK 2.0.0-dev.30 or later. 326 | - Updated to Dart 2.0 constants from dart:convert. 327 | 328 | ## 0.10.0 329 | 330 | - BREAKING CHANGE: `createHitmap` and `mergeHitmaps` now specify generic types 331 | (`Map>`) on their hit map parameter/return value. 332 | - Updated package:args dependency to 1.0.0. 333 | 334 | ## 0.9.3 335 | 336 | - Strong mode fixes as of Dart SDK 1.24.0. 337 | - Restrict the SDK lower version constraint to `>=1.21.0`. Required for method 338 | generics. 339 | - Eliminate dependency on package:async. 340 | 341 | ## 0.9.2 342 | 343 | - Strong mode fixes as of Dart SDK 1.22.0. 344 | 345 | ## 0.9.1 346 | 347 | - Temporarily add back support for the `--host` and `--port` options to 348 | `collect_coverage`. This is a temporary measure for backwards-compatibility 349 | that may stop working on Dart SDKs >= 1.22. See the related 350 | [breaking change note](https://groups.google.com/a/dartlang.org/forum/#!msg/announce/VxSw-V5tx8k/wPV0GfX7BwAJ) 351 | for the Dart VM service protocol. 352 | 353 | ## 0.9.0 354 | 355 | - BREAKING CHANGE: `collect` no longer supports the `host` and `port` 356 | parameters. These are replaced with a `serviceUri` parameter. As of Dart SDK 357 | 1.22, the Dart VM will emit Observatory URIs that include an authentication 358 | token for security reasons. Automated tools will need to scrape stdout for 359 | this URI and pass it to `collect_coverage`. 360 | - BREAKING CHANGE: `collect_coverage`: the `--host` and `--port` options have 361 | been replaced with a `--uri` option. See the above change for details. 362 | - BREAKING CHANGE: `runAndCollect` now defaults to running in checked mode. 363 | - Added `extractObservatoryUri`: scrapes an input string for an Observatory URI. 364 | Potentially useful for automated tooling after Dart SDK 1.22. 365 | 366 | ## 0.8.1 367 | 368 | - Added optional `checked` parameter to `runAndCollect` to run in checked mode. 369 | 370 | ## 0.8.0+2 371 | 372 | - Strong mode fixes as of Dart SDK 1.20.1. 373 | 374 | ## 0.8.0+1 375 | 376 | - Make strong mode clean. 377 | 378 | ## 0.8.0 379 | 380 | - Moved `Formatter.format` parameters `reportOn` and `basePath` to constructor. 381 | Eliminated `pathFilter` parameter. 382 | 383 | ## 0.7.9 384 | 385 | - `format_coverage`: add `--base-directory` option. Source paths in 386 | LCOV/pretty-print output are relative to this directory, or absolute if 387 | unspecified. 388 | 389 | ## 0.7.8 390 | 391 | - `format_coverage`: support `--packages` option for package specs. 392 | 393 | ## 0.7.7 394 | 395 | - Add fallback URI resolution for Bazel http(s) URIs that don't contain a 396 | `packages` path component. 397 | 398 | ## 0.7.6 399 | 400 | - Add [Bazel](http://bazel.io) support to `format_coverage`. 401 | 402 | ## 0.7.5 403 | 404 | - Bugfix in `collect_coverage`: prevent hang if initial VM service connection is 405 | slow. 406 | - Workaround for VM behaviour in which `evaluate:source` ranges may appear in 407 | the returned source report manifesting in a crash in `collect_coverage`. These 408 | generally correspond to source evaluations in the debugger and add little 409 | value to line coverage. 410 | - `format_coverage`: may be slower for large sets of coverage JSON input files. 411 | Unlikely to be an issue due to elimination of `--coverage-dir` VM flag. 412 | 413 | ## 0.7.4 414 | 415 | - Require at least Dart SDK 1.16.0. 416 | 417 | - Bugfix in format_coverage: if `--report-on` is not specified, emit all 418 | coverage, rather than none. 419 | 420 | ## 0.7.3 421 | 422 | - Added support for the latest Dart SDK. 423 | 424 | ## 0.7.2 425 | 426 | - `Formatter.format` added two optional arguments: `reportOn` and `pathFilter`. 427 | They can be used independently to limit the files which are included in the 428 | output. 429 | 430 | - Added `runAndCollect` API to library. 431 | 432 | ## 0.7.1 433 | 434 | - Added `collect` top-level method. 435 | 436 | - Updated support for latest `0.11.0` dev build. 437 | 438 | - Replaced `ServiceEvent.eventType` with `ServiceEvent.kind`. 439 | - `ServiceEvent.eventType` is deprecated and will be removed in `0.8`. 440 | 441 | ## 0.7.0 442 | 443 | - `format_coverage` no longer emits SDK coverage unless --sdk-root is set 444 | explicitly. 445 | 446 | - Removed support for collecting coverage from old (<1.9.0) Dart SDKs. 447 | 448 | - Removed deprecated `Resolver.pkgRoot`. 449 | 450 | ## 0.6.5 451 | 452 | - Fixed early collection bug when --wait-paused is set. 453 | 454 | ## 0.6.4 455 | 456 | - Optimized formatters and fixed return value of `format` methods. 457 | 458 | - Added `Resolver.packageRoot` – deprecated `Resolver.pkgRoot`. 459 | 460 | ## 0.6.3 461 | 462 | - Support the latest release of `args` package. 463 | 464 | - Support the latest release of `logging` package. 465 | 466 | - Fixed error when trying to access invalid paths. 467 | 468 | - Require at least Dart SDK v1.9.0. 469 | 470 | ## 0.6.2 471 | 472 | - Support observatory protocol changes for VM >= 1.11.0. 473 | 474 | ## 0.6.1 475 | 476 | - Support observatory protocol changes for VM >= 1.10.0. 477 | 478 | ## 0.6.0+1 479 | 480 | - Add support for `pub global run`. 481 | 482 | ## 0.6.0 483 | 484 | - Add support for SDK versions >= 1.9.0. For Dartium/content-shell versions past 485 | 1.9.0, coverage collection is no longer done over the remote debugging port, 486 | but via the observatory port emitted on stdout. Backward compatibility with 487 | SDKs back to 1.5.x is provided. 488 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2014, the Dart project authors. 2 | 3 | Redistribution and use in source and binary forms, with or without 4 | modification, are permitted provided that the following conditions are 5 | met: 6 | 7 | * Redistributions of source code must retain the above copyright 8 | notice, this list of conditions and the following disclaimer. 9 | * Redistributions in binary form must reproduce the above 10 | copyright notice, this list of conditions and the following 11 | disclaimer in the documentation and/or other materials provided 12 | with the distribution. 13 | * Neither the name of Google LLC nor the names of its 14 | contributors may be used to endorse or promote products derived 15 | from this software without specific prior written permission. 16 | 17 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 18 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 19 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 20 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 21 | OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 22 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 23 | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 24 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 25 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 26 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 27 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 28 | -------------------------------------------------------------------------------- /PATENTS: -------------------------------------------------------------------------------- 1 | Additional IP Rights Grant (Patents) 2 | 3 | "This implementation" means the copyrightable works distributed by 4 | Google as part of the Dart Project. 5 | 6 | Google hereby grants to you a perpetual, worldwide, non-exclusive, 7 | no-charge, royalty-free, irrevocable (except as stated in this 8 | section) patent license to make, have made, use, offer to sell, sell, 9 | import, transfer, and otherwise run, modify and propagate the contents 10 | of this implementation of Dart, where such license applies only to 11 | those patent claims, both currently owned by Google and acquired in 12 | the future, licensable by Google that are necessarily infringed by 13 | this implementation of Dart. This grant does not include claims that 14 | would be infringed only as a consequence of further modification of 15 | this implementation. If you or your agent or exclusive licensee 16 | institute or order or agree to the institution of patent litigation 17 | against any entity (including a cross-claim or counterclaim in a 18 | lawsuit) alleging that this implementation of Dart or any code 19 | incorporated within this implementation of Dart constitutes direct or 20 | contributory patent infringement, or inducement of patent 21 | infringement, then any patent rights granted to you under this License 22 | for this implementation of Dart shall terminate as of the date such 23 | litigation is filed. 24 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | > [!IMPORTANT] 2 | > This repo has moved to https://github.com/dart-lang/tools/tree/main/pkgs/coverage 3 | 4 | Coverage provides coverage data collection, manipulation, and formatting for 5 | Dart. 6 | 7 | [![Build Status](https://github.com/dart-lang/coverage/workflows/Dart%20CI/badge.svg)](https://github.com/dart-lang/coverage/actions?query=workflow%3A"Dart+CI"+branch%3Amaster) 8 | [![Coverage Status](https://coveralls.io/repos/dart-lang/coverage/badge.svg?branch=master)](https://coveralls.io/r/dart-lang/coverage) 9 | [![Pub](https://img.shields.io/pub/v/coverage.svg)](https://pub.dev/packages/coverage) 10 | 11 | ## Tools 12 | 13 | `collect_coverage` collects coverage JSON from the Dart VM Service. 14 | `format_coverage` formats JSON coverage data into either 15 | [LCOV](https://github.com/linux-test-project/lcov) or pretty-printed format. 16 | 17 | #### Install coverage 18 | 19 | dart pub global activate coverage 20 | 21 | Consider adding the `dart pub global run` executables directory to your path. 22 | See 23 | [Running a script from your PATH](https://dart.dev/tools/pub/cmd/pub-global#running-a-script-from-your-path) 24 | for more details. 25 | 26 | #### Running tests with coverage 27 | 28 | For the common use case where you just want to run all your tests, and generate 29 | an lcov.info file, you can use the test_with_coverage script: 30 | 31 | ``` 32 | dart pub global run coverage:test_with_coverage 33 | ``` 34 | 35 | By default, this script assumes it's being run from the root directory of a 36 | package, and outputs a coverage.json and lcov.info file to ./coverage/ 37 | 38 | This script is essentially the same as running: 39 | 40 | ``` 41 | dart run --pause-isolates-on-exit --disable-service-auth-codes --enable-vm-service=8181 test & 42 | dart pub global run coverage:collect_coverage --wait-paused --uri=http://127.0.0.1:8181/ -o coverage/coverage.json --resume-isolates --scope-output=foo 43 | dart pub global run coverage:format_coverage --packages=.dart_tool/package_config.json --lcov -i coverage/coverage.json -o coverage/lcov.info 44 | ``` 45 | 46 | For more complicated use cases, where you want to control each of these stages, 47 | see the sections below. 48 | 49 | #### Collecting coverage from the VM 50 | 51 | ``` 52 | dart --pause-isolates-on-exit --disable-service-auth-codes --enable-vm-service=NNNN script.dart 53 | dart pub global run coverage:collect_coverage --uri=http://... -o coverage.json --resume-isolates 54 | ``` 55 | 56 | or if the `dart pub global run` executables are on your PATH, 57 | 58 | ``` 59 | collect_coverage --uri=http://... -o coverage.json --resume-isolates 60 | ``` 61 | 62 | where `--uri` specifies the Dart VM Service URI emitted by the VM. 63 | 64 | If `collect_coverage` is invoked before the script from which coverage is to be 65 | collected, it will wait until it detects a VM observatory to which it can 66 | connect. An optional `--connect-timeout` may be specified (in seconds). The 67 | `--wait-paused` flag may be enabled, causing `collect_coverage` to wait until 68 | all isolates are paused before collecting coverage. 69 | 70 | #### Formatting coverage data 71 | 72 | ``` 73 | dart pub global run coverage:format_coverage --package=app_package -i coverage.json 74 | ``` 75 | 76 | or if the `dart pub global run` exectuables are on your PATH, 77 | 78 | ``` 79 | format_coverage --package=app_package -i coverage.json 80 | ``` 81 | 82 | where `app_package` is the path to the package whose coverage is being collected 83 | (defaults to the current working directory). If `--sdk-root` is set, Dart SDK 84 | coverage will also be output. 85 | 86 | #### Ignore lines from coverage 87 | 88 | - `// coverage:ignore-line` to ignore one line. 89 | - `// coverage:ignore-start` and `// coverage:ignore-end` to ignore range of 90 | lines inclusive. 91 | - `// coverage:ignore-file` to ignore the whole file. 92 | 93 | Then pass `--check-ignore` to `format_coverage`. 94 | 95 | #### Function and branch coverage 96 | 97 | To gather function level coverage information, pass `--function-coverage` to 98 | collect_coverage: 99 | 100 | ``` 101 | dart --pause-isolates-on-exit --disable-service-auth-codes --enable-vm-service=NNNN script.dart 102 | dart pub global run coverage:collect_coverage --uri=http://... -o coverage.json --resume-isolates --function-coverage 103 | ``` 104 | 105 | To gather branch level coverage information, pass `--branch-coverage` to _both_ 106 | collect_coverage and the Dart command you're gathering coverage from: 107 | 108 | ``` 109 | dart --pause-isolates-on-exit --disable-service-auth-codes --enable-vm-service=NNNN --branch-coverage script.dart 110 | dart pub global run coverage:collect_coverage --uri=http://... -o coverage.json --resume-isolates --branch-coverage 111 | ``` 112 | 113 | Branch coverage requires Dart VM 2.17.0, with service API v3.56. Function, 114 | branch, and line coverage can all be gathered at the same time, by combining 115 | those flags: 116 | 117 | ``` 118 | dart --pause-isolates-on-exit --disable-service-auth-codes --enable-vm-service=NNNN --branch-coverage script.dart 119 | dart pub global run coverage:collect_coverage --uri=http://... -o coverage.json --resume-isolates --function-coverage --branch-coverage 120 | ``` 121 | 122 | These flags can also be passed to test_with_coverage: 123 | 124 | ``` 125 | pub global run coverage:test_with_coverage --branch-coverage --function-coverage 126 | ``` 127 | -------------------------------------------------------------------------------- /analysis_options.yaml: -------------------------------------------------------------------------------- 1 | include: package:dart_flutter_team_lints/analysis_options.yaml 2 | 3 | analyzer: 4 | language: 5 | strict-casts: true 6 | 7 | exclude: 8 | - var/** 9 | 10 | linter: 11 | rules: 12 | - always_declare_return_types 13 | - avoid_slow_async_io 14 | - cancel_subscriptions 15 | - comment_references 16 | - literal_only_boolean_expressions 17 | - package_api_docs 18 | - prefer_final_locals 19 | - sort_constructors_first 20 | - sort_unnamed_constructors_first 21 | - test_types_in_equals 22 | - throw_in_finally 23 | - type_annotate_public_apis 24 | -------------------------------------------------------------------------------- /benchmark/.gitignore: -------------------------------------------------------------------------------- 1 | data 2 | -------------------------------------------------------------------------------- /benchmark/many_isolates.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2022, the Dart project authors. Please see the AUTHORS file 2 | // for details. All rights reserved. Use of this source code is governed by a 3 | // BSD-style license that can be found in the LICENSE file. 4 | 5 | import 'dart:isolate'; 6 | import 'dart:math'; 7 | 8 | Future main(List args, dynamic message) async { 9 | if (message == null) { 10 | // If there is no message, it means this instance was created by 11 | // run_benchmarks.dart. In that case, this is the parent instance that 12 | // spawns all the others. 13 | var sum = 0; 14 | for (var i = 0; i < 10; ++i) { 15 | final port = ReceivePort(); 16 | final isolate = 17 | Isolate.spawnUri(Uri.file('many_isolates.dart'), [], port.sendPort); 18 | sum += await port.first as int; 19 | await isolate; 20 | } 21 | print('sum = $sum'); 22 | } else { 23 | // If there is a message, it means this instance is one of the child 24 | // instances. The message is the port that this instance replies on. 25 | (message as SendPort).send(Random().nextInt(1000)); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /benchmark/run_benchmarks.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2022, the Dart project authors. Please see the AUTHORS file 2 | // for details. All rights reserved. Use of this source code is governed by a 3 | // BSD-style license that can be found in the LICENSE file. 4 | 5 | import 'dart:async'; 6 | import 'dart:io'; 7 | 8 | import 'package:benchmark_harness/benchmark_harness.dart'; 9 | 10 | import '../bin/collect_coverage.dart' as collect_coverage; 11 | import '../bin/format_coverage.dart' as format_coverage; 12 | 13 | // Runs a test script with various different coverage configurations. 14 | class CoverageBenchmark extends AsyncBenchmarkBase { 15 | CoverageBenchmark( 16 | ScoreEmitter emitter, 17 | super.name, 18 | this.script, { 19 | this.gatherCoverage = false, 20 | this.functionCoverage = false, 21 | this.branchCoverage = false, 22 | }) : super(emitter: emitter); 23 | 24 | final String script; 25 | final bool gatherCoverage; 26 | final bool functionCoverage; 27 | final bool branchCoverage; 28 | int iteration = 0; 29 | 30 | @override 31 | Future run() async { 32 | print('Running $name...'); 33 | final covFile = 'data/$name $iteration coverage.json'; 34 | final lcovFile = 'data/$name $iteration lcov.info'; 35 | ++iteration; 36 | 37 | await Process.start( 38 | Platform.executable, 39 | [ 40 | if (branchCoverage) '--branch-coverage', 41 | 'run', 42 | if (gatherCoverage) ...[ 43 | '--pause-isolates-on-exit', 44 | '--disable-service-auth-codes', 45 | '--enable-vm-service=1234', 46 | ], 47 | script, 48 | ], 49 | mode: ProcessStartMode.detached, 50 | ); 51 | if (gatherCoverage) { 52 | await collect_coverage.main([ 53 | '--wait-paused', 54 | '--resume-isolates', 55 | '--uri=http://127.0.0.1:1234/', 56 | if (branchCoverage) '--branch-coverage', 57 | if (functionCoverage) '--function-coverage', 58 | '-o', 59 | covFile, 60 | ]); 61 | 62 | await format_coverage.main([ 63 | '--lcov', 64 | '--check-ignore', 65 | '-i', 66 | covFile, 67 | '-o', 68 | lcovFile, 69 | ]); 70 | } 71 | } 72 | } 73 | 74 | // Emitter that just captures the value. 75 | class CaptureEmitter implements ScoreEmitter { 76 | late double capturedValue; 77 | 78 | @override 79 | void emit(String testName, double value) { 80 | capturedValue = value; 81 | } 82 | } 83 | 84 | // Prints a JSON representation of the benchmark results, in a format compatible 85 | // with the github benchmark action. 86 | class JsonEmitter implements ScoreEmitter { 87 | JsonEmitter(this._baseline); 88 | 89 | final double _baseline; 90 | final _results = {}; 91 | 92 | @override 93 | void emit(String testName, double value) { 94 | _results[testName] = value; 95 | } 96 | 97 | String write() => '[${_results.entries.map((entry) => """{ 98 | "name": "${entry.key}", 99 | "unit": "times slower", 100 | "value": ${(entry.value / _baseline).toStringAsFixed(2)} 101 | }""").join(',\n')}]'; 102 | } 103 | 104 | Future runBenchmark(CoverageBenchmark benchmark) async { 105 | for (var i = 0; i < 3; ++i) { 106 | try { 107 | await benchmark.report().timeout(const Duration(minutes: 2)); 108 | return; 109 | } on TimeoutException { 110 | print('Timed out'); 111 | } 112 | } 113 | print('Timed out too many times. Giving up.'); 114 | exit(127); 115 | } 116 | 117 | Future runBenchmarkSet(String name, String script) async { 118 | final captureEmitter = CaptureEmitter(); 119 | await runBenchmark( 120 | CoverageBenchmark(captureEmitter, '$name - no coverage', script)); 121 | final benchmarkBaseline = captureEmitter.capturedValue; 122 | 123 | final emitter = JsonEmitter(benchmarkBaseline); 124 | await runBenchmark(CoverageBenchmark( 125 | emitter, '$name - basic coverage', script, 126 | gatherCoverage: true)); 127 | await runBenchmark(CoverageBenchmark( 128 | emitter, '$name - function coverage', script, 129 | gatherCoverage: true, functionCoverage: true)); 130 | await runBenchmark(CoverageBenchmark( 131 | emitter, '$name - branch coverage', script, 132 | gatherCoverage: true, branchCoverage: true)); 133 | return emitter.write(); 134 | } 135 | 136 | Future main() async { 137 | // Assume this script was started from the root coverage directory. Change to 138 | // the benchmark directory. 139 | Directory.current = 'benchmark'; 140 | final result = await runBenchmarkSet('Many isolates', 'many_isolates.dart'); 141 | await File('data/benchmark_result.json').writeAsString(result); 142 | exit(0); 143 | } 144 | -------------------------------------------------------------------------------- /bin/collect_coverage.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2014, the Dart project authors. Please see the AUTHORS file 2 | // for details. All rights reserved. Use of this source code is governed by a 3 | // BSD-style license that can be found in the LICENSE file. 4 | 5 | import 'dart:async'; 6 | import 'dart:convert' show json; 7 | import 'dart:io'; 8 | 9 | import 'package:args/args.dart'; 10 | import 'package:coverage/src/collect.dart'; 11 | import 'package:logging/logging.dart'; 12 | import 'package:stack_trace/stack_trace.dart'; 13 | 14 | Future main(List arguments) async { 15 | Logger.root.level = Level.WARNING; 16 | Logger.root.onRecord.listen((LogRecord rec) { 17 | print('${rec.level.name}: ${rec.time}: ${rec.message}'); 18 | }); 19 | 20 | final options = _parseArgs(arguments); 21 | await Chain.capture(() async { 22 | final coverage = await collect(options.serviceUri, options.resume, 23 | options.waitPaused, options.includeDart, options.scopedOutput, 24 | timeout: options.timeout, 25 | functionCoverage: options.functionCoverage, 26 | branchCoverage: options.branchCoverage); 27 | options.out.write(json.encode(coverage)); 28 | await options.out.close(); 29 | }, onError: (dynamic error, Chain chain) { 30 | stderr.writeln(error); 31 | stderr.writeln(chain.terse); 32 | // See http://www.retro11.de/ouxr/211bsd/usr/include/sysexits.h.html 33 | // EX_SOFTWARE 34 | exit(70); 35 | }); 36 | } 37 | 38 | class Options { 39 | Options( 40 | this.serviceUri, 41 | this.out, 42 | this.timeout, 43 | this.waitPaused, 44 | this.resume, 45 | this.includeDart, 46 | this.functionCoverage, 47 | this.branchCoverage, 48 | this.scopedOutput); 49 | 50 | final Uri serviceUri; 51 | final IOSink out; 52 | final Duration? timeout; 53 | final bool waitPaused; 54 | final bool resume; 55 | final bool includeDart; 56 | final bool functionCoverage; 57 | final bool branchCoverage; 58 | final Set scopedOutput; 59 | } 60 | 61 | Options _parseArgs(List arguments) { 62 | final parser = ArgParser() 63 | ..addOption('host', 64 | abbr: 'H', 65 | help: 'remote VM host. DEPRECATED: use --uri', 66 | defaultsTo: '127.0.0.1') 67 | ..addOption('port', 68 | abbr: 'p', 69 | help: 'remote VM port. DEPRECATED: use --uri', 70 | defaultsTo: '8181') 71 | ..addOption('uri', abbr: 'u', help: 'VM observatory service URI') 72 | ..addOption('out', 73 | abbr: 'o', defaultsTo: 'stdout', help: 'output: may be file or stdout') 74 | ..addOption('connect-timeout', 75 | abbr: 't', help: 'connect timeout in seconds') 76 | ..addMultiOption('scope-output', 77 | help: 'restrict coverage results so that only scripts that start with ' 78 | 'the provided package path are considered') 79 | ..addFlag('wait-paused', 80 | abbr: 'w', 81 | defaultsTo: false, 82 | help: 'wait for all isolates to be paused before collecting coverage') 83 | ..addFlag('resume-isolates', 84 | abbr: 'r', defaultsTo: false, help: 'resume all isolates on exit') 85 | ..addFlag('include-dart', 86 | abbr: 'd', defaultsTo: false, help: 'include "dart:" libraries') 87 | ..addFlag('function-coverage', 88 | abbr: 'f', defaultsTo: false, help: 'Collect function coverage info') 89 | ..addFlag('branch-coverage', 90 | abbr: 'b', 91 | defaultsTo: false, 92 | help: 'Collect branch coverage info (Dart VM must also be run with ' 93 | '--branch-coverage for this to work)') 94 | ..addFlag('help', abbr: 'h', negatable: false, help: 'show this help'); 95 | 96 | final args = parser.parse(arguments); 97 | 98 | void printUsage() { 99 | print('Usage: dart collect_coverage.dart --uri=http://... [OPTION...]\n'); 100 | print(parser.usage); 101 | } 102 | 103 | Never fail(String message) { 104 | print('Error: $message\n'); 105 | printUsage(); 106 | exit(1); 107 | } 108 | 109 | if (args['help'] as bool) { 110 | printUsage(); 111 | exit(0); 112 | } 113 | 114 | Uri serviceUri; 115 | if (args['uri'] == null) { 116 | // TODO(cbracken) eliminate --host and --port support when VM defaults to 117 | // requiring an auth token. Estimated for Dart SDK 1.22. 118 | serviceUri = Uri.parse('http://${args['host']}:${args['port']}/'); 119 | } else { 120 | try { 121 | serviceUri = Uri.parse(args['uri'] as String); 122 | } on FormatException { 123 | fail('Invalid service URI specified: ${args['uri']}'); 124 | } 125 | } 126 | 127 | final scopedOutput = args['scope-output'] as List; 128 | IOSink out; 129 | if (args['out'] == 'stdout') { 130 | out = stdout; 131 | } else { 132 | final outfile = File(args['out'] as String)..createSync(recursive: true); 133 | out = outfile.openWrite(); 134 | } 135 | final timeout = (args['connect-timeout'] == null) 136 | ? null 137 | : Duration(seconds: int.parse(args['connect-timeout'] as String)); 138 | return Options( 139 | serviceUri, 140 | out, 141 | timeout, 142 | args['wait-paused'] as bool, 143 | args['resume-isolates'] as bool, 144 | args['include-dart'] as bool, 145 | args['function-coverage'] as bool, 146 | args['branch-coverage'] as bool, 147 | scopedOutput.toSet(), 148 | ); 149 | } 150 | -------------------------------------------------------------------------------- /bin/format_coverage.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2013, the Dart project authors. Please see the AUTHORS file 2 | // for details. All rights reserved. Use of this source code is governed by a 3 | // BSD-style license that can be found in the LICENSE file. 4 | 5 | import 'dart:io'; 6 | 7 | import 'package:args/args.dart'; 8 | import 'package:coverage/coverage.dart'; 9 | import 'package:glob/glob.dart'; 10 | import 'package:path/path.dart' as p; 11 | 12 | /// [Environment] stores gathered arguments information. 13 | class Environment { 14 | Environment({ 15 | required this.baseDirectory, 16 | required this.bazel, 17 | required this.bazelWorkspace, 18 | required this.checkIgnore, 19 | required this.input, 20 | required this.lcov, 21 | required this.output, 22 | required this.packagesPath, 23 | required this.packagePath, 24 | required this.prettyPrint, 25 | required this.prettyPrintFunc, 26 | required this.prettyPrintBranch, 27 | required this.reportOn, 28 | required this.ignoreFiles, 29 | required this.sdkRoot, 30 | required this.verbose, 31 | required this.workers, 32 | }); 33 | 34 | String? baseDirectory; 35 | bool bazel; 36 | String bazelWorkspace; 37 | bool checkIgnore; 38 | String input; 39 | bool lcov; 40 | IOSink output; 41 | String? packagesPath; 42 | String packagePath; 43 | bool prettyPrint; 44 | bool prettyPrintFunc; 45 | bool prettyPrintBranch; 46 | List? reportOn; 47 | List? ignoreFiles; 48 | String? sdkRoot; 49 | bool verbose; 50 | int workers; 51 | } 52 | 53 | Future main(List arguments) async { 54 | final env = parseArgs(arguments); 55 | 56 | final files = filesToProcess(env.input); 57 | if (env.verbose) { 58 | print('Environment:'); 59 | print(' # files: ${files.length}'); 60 | print(' # workers: ${env.workers}'); 61 | print(' sdk-root: ${env.sdkRoot}'); 62 | print(' package-path: ${env.packagePath}'); 63 | print(' packages-path: ${env.packagesPath}'); 64 | print(' report-on: ${env.reportOn}'); 65 | print(' check-ignore: ${env.checkIgnore}'); 66 | } 67 | 68 | final clock = Stopwatch()..start(); 69 | final hitmap = await HitMap.parseFiles( 70 | files, 71 | checkIgnoredLines: env.checkIgnore, 72 | // ignore: deprecated_member_use_from_same_package 73 | packagesPath: env.packagesPath, 74 | packagePath: env.packagePath, 75 | ); 76 | 77 | // All workers are done. Process the data. 78 | if (env.verbose) { 79 | print('Done creating global hitmap. Took ${clock.elapsedMilliseconds} ms.'); 80 | } 81 | 82 | final ignoreGlobs = env.ignoreFiles?.map(Glob.new).toSet(); 83 | 84 | String output; 85 | final resolver = env.bazel 86 | ? BazelResolver(workspacePath: env.bazelWorkspace) 87 | : await Resolver.create( 88 | packagesPath: env.packagesPath, 89 | packagePath: env.packagePath, 90 | sdkRoot: env.sdkRoot, 91 | ); 92 | final loader = Loader(); 93 | if (env.prettyPrint) { 94 | output = await hitmap.prettyPrint(resolver, loader, 95 | reportOn: env.reportOn, 96 | ignoreGlobs: ignoreGlobs, 97 | reportFuncs: env.prettyPrintFunc, 98 | reportBranches: env.prettyPrintBranch); 99 | } else { 100 | assert(env.lcov); 101 | output = hitmap.formatLcov(resolver, 102 | reportOn: env.reportOn, 103 | ignoreGlobs: ignoreGlobs, 104 | basePath: env.baseDirectory); 105 | } 106 | 107 | env.output.write(output); 108 | await env.output.flush(); 109 | if (env.verbose) { 110 | print('Done flushing output. Took ${clock.elapsedMilliseconds} ms.'); 111 | } 112 | 113 | if (env.verbose) { 114 | if (resolver.failed.isNotEmpty) { 115 | print('Failed to resolve:'); 116 | for (var error in resolver.failed.toSet()) { 117 | print(' $error'); 118 | } 119 | } 120 | if (loader.failed.isNotEmpty) { 121 | print('Failed to load:'); 122 | for (var error in loader.failed.toSet()) { 123 | print(' $error'); 124 | } 125 | } 126 | } 127 | await env.output.close(); 128 | } 129 | 130 | /// Checks the validity of the provided arguments. Does not initialize actual 131 | /// processing. 132 | Environment parseArgs(List arguments) { 133 | final parser = ArgParser(); 134 | 135 | parser 136 | ..addOption('sdk-root', abbr: 's', help: 'path to the SDK root') 137 | ..addOption('packages', help: '[DEPRECATED] path to the package spec file') 138 | ..addOption('package', 139 | help: 'root directory of the package', defaultsTo: '.') 140 | ..addOption('in', abbr: 'i', help: 'input(s): may be file or directory') 141 | ..addOption('out', 142 | abbr: 'o', defaultsTo: 'stdout', help: 'output: may be file or stdout') 143 | ..addMultiOption('report-on', 144 | help: 'which directories or files to report coverage on') 145 | ..addOption('workers', 146 | abbr: 'j', defaultsTo: '1', help: 'number of workers') 147 | ..addOption('bazel-workspace', 148 | defaultsTo: '', help: 'Bazel workspace directory') 149 | ..addOption('base-directory', 150 | abbr: 'b', 151 | help: 'the base directory relative to which source paths are output') 152 | ..addFlag('bazel', 153 | defaultsTo: false, help: 'use Bazel-style path resolution') 154 | ..addFlag('pretty-print', 155 | abbr: 'r', 156 | negatable: false, 157 | help: 'convert line coverage data to pretty print format') 158 | ..addFlag('pretty-print-func', 159 | abbr: 'f', 160 | negatable: false, 161 | help: 'convert function coverage data to pretty print format') 162 | ..addFlag('pretty-print-branch', 163 | negatable: false, 164 | help: 'convert branch coverage data to pretty print format') 165 | ..addFlag('lcov', 166 | abbr: 'l', 167 | negatable: false, 168 | help: 'convert coverage data to lcov format') 169 | ..addFlag('verbose', abbr: 'v', negatable: false, help: 'verbose output') 170 | ..addFlag( 171 | 'check-ignore', 172 | abbr: 'c', 173 | negatable: false, 174 | help: 'check for coverage ignore comments.' 175 | ' Not supported in web coverage.', 176 | ) 177 | ..addMultiOption( 178 | 'ignore-files', 179 | defaultsTo: [], 180 | help: 'Ignore files by glob patterns', 181 | ) 182 | ..addFlag('help', abbr: 'h', negatable: false, help: 'show this help'); 183 | 184 | final args = parser.parse(arguments); 185 | 186 | void printUsage() { 187 | print('Usage: dart format_coverage.dart [OPTION...]\n'); 188 | print(parser.usage); 189 | } 190 | 191 | Never fail(String msg) { 192 | print('\n$msg\n'); 193 | printUsage(); 194 | exit(1); 195 | } 196 | 197 | if (args['help'] as bool) { 198 | printUsage(); 199 | exit(0); 200 | } 201 | 202 | var sdkRoot = args['sdk-root'] as String?; 203 | if (sdkRoot != null) { 204 | sdkRoot = p.normalize(p.join(p.absolute(sdkRoot), 'lib')); 205 | if (!FileSystemEntity.isDirectorySync(sdkRoot)) { 206 | fail('Provided SDK root "${args["sdk-root"]}" is not a valid SDK ' 207 | 'top-level directory'); 208 | } 209 | } 210 | 211 | final packagesPath = args['packages'] as String?; 212 | if (packagesPath != null) { 213 | if (!FileSystemEntity.isFileSync(packagesPath)) { 214 | fail('Package spec "${args["packages"]}" not found, or not a file.'); 215 | } 216 | } 217 | 218 | final packagePath = args['package'] as String; 219 | if (!FileSystemEntity.isDirectorySync(packagePath)) { 220 | fail('Package spec "${args["package"]}" not found, or not a directory.'); 221 | } 222 | 223 | if (args['in'] == null) fail('No input files given.'); 224 | final input = p.absolute(p.normalize(args['in'] as String)); 225 | if (!FileSystemEntity.isDirectorySync(input) && 226 | !FileSystemEntity.isFileSync(input)) { 227 | fail('Provided input "${args["in"]}" is neither a directory nor a file.'); 228 | } 229 | 230 | IOSink output; 231 | if (args['out'] == 'stdout') { 232 | output = stdout; 233 | } else { 234 | final outpath = p.absolute(p.normalize(args['out'] as String)); 235 | final outfile = File(outpath)..createSync(recursive: true); 236 | output = outfile.openWrite(); 237 | } 238 | 239 | final reportOnRaw = args['report-on'] as List; 240 | final reportOn = reportOnRaw.isNotEmpty ? reportOnRaw : null; 241 | 242 | final bazel = args['bazel'] as bool; 243 | final bazelWorkspace = args['bazel-workspace'] as String; 244 | if (bazelWorkspace.isNotEmpty && !bazel) { 245 | stderr.writeln('warning: ignoring --bazel-workspace: --bazel not set'); 246 | } 247 | 248 | String? baseDirectory; 249 | if (args['base-directory'] != null) { 250 | baseDirectory = p.absolute(args['base-directory'] as String); 251 | } 252 | 253 | final lcov = args['lcov'] as bool; 254 | var prettyPrint = args['pretty-print'] as bool; 255 | final prettyPrintFunc = args['pretty-print-func'] as bool; 256 | final prettyPrintBranch = args['pretty-print-branch'] as bool; 257 | final numModesChosen = (prettyPrint ? 1 : 0) + 258 | (prettyPrintFunc ? 1 : 0) + 259 | (prettyPrintBranch ? 1 : 0) + 260 | (lcov ? 1 : 0); 261 | if (numModesChosen > 1) { 262 | fail('Choose one of the pretty-print modes or lcov output'); 263 | } 264 | 265 | // The pretty printer is used by all modes other than lcov. 266 | if (!lcov) prettyPrint = true; 267 | 268 | int workers; 269 | try { 270 | workers = int.parse('${args["workers"]}'); 271 | } catch (e) { 272 | fail('Invalid worker count: $e'); 273 | } 274 | 275 | final checkIgnore = args['check-ignore'] as bool; 276 | final ignoredGlobs = args['ignore-files'] as List; 277 | final verbose = args['verbose'] as bool; 278 | return Environment( 279 | baseDirectory: baseDirectory, 280 | bazel: bazel, 281 | bazelWorkspace: bazelWorkspace, 282 | checkIgnore: checkIgnore, 283 | input: input, 284 | lcov: lcov, 285 | output: output, 286 | packagesPath: packagesPath, 287 | packagePath: packagePath, 288 | prettyPrint: prettyPrint, 289 | prettyPrintFunc: prettyPrintFunc, 290 | prettyPrintBranch: prettyPrintBranch, 291 | reportOn: reportOn, 292 | ignoreFiles: ignoredGlobs, 293 | sdkRoot: sdkRoot, 294 | verbose: verbose, 295 | workers: workers); 296 | } 297 | 298 | /// Given an absolute path absPath, this function returns a [List] of files 299 | /// are contained by it if it is a directory, or a [List] containing the file if 300 | /// it is a file. 301 | List filesToProcess(String absPath) { 302 | if (FileSystemEntity.isDirectorySync(absPath)) { 303 | return Directory(absPath) 304 | .listSync(recursive: true) 305 | .whereType() 306 | .where((e) => e.path.endsWith('.json')) 307 | .toList(); 308 | } 309 | return [File(absPath)]; 310 | } 311 | -------------------------------------------------------------------------------- /bin/run_and_collect.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2017, the Dart project authors. Please see the AUTHORS file 2 | // for details. All rights reserved. Use of this source code is governed by a 3 | // BSD-style license that can be found in the LICENSE file. 4 | 5 | import 'dart:async'; 6 | 7 | import 'package:coverage/src/run_and_collect.dart'; 8 | 9 | Future main(List args) async { 10 | final Map results = await runAndCollect(args[0]); 11 | print(results); 12 | } 13 | -------------------------------------------------------------------------------- /bin/test_with_coverage.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2022, the Dart project authors. Please see the AUTHORS file 2 | // for details. All rights reserved. Use of this source code is governed by a 3 | // BSD-style license that can be found in the LICENSE file. 4 | 5 | import 'dart:async'; 6 | import 'dart:io'; 7 | 8 | import 'package:args/args.dart'; 9 | import 'package:coverage/src/util.dart' 10 | show StandardOutExtension, extractVMServiceUri; 11 | import 'package:package_config/package_config.dart'; 12 | import 'package:path/path.dart' as path; 13 | 14 | import 'collect_coverage.dart' as collect_coverage; 15 | import 'format_coverage.dart' as format_coverage; 16 | 17 | final _allProcesses = []; 18 | 19 | Future _dartRun(List args, 20 | {void Function(String)? onStdout, String? workingDir}) async { 21 | final process = await Process.start( 22 | Platform.executable, 23 | args, 24 | workingDirectory: workingDir, 25 | ); 26 | _allProcesses.add(process); 27 | final broadStdout = process.stdout.asBroadcastStream(); 28 | broadStdout.listen(stdout.add); 29 | if (onStdout != null) { 30 | broadStdout.lines().listen(onStdout); 31 | } 32 | process.stderr.listen(stderr.add); 33 | final result = await process.exitCode; 34 | if (result != 0) { 35 | throw ProcessException(Platform.executable, args, '', result); 36 | } 37 | } 38 | 39 | Future _packageNameFromConfig(String packageDir) async { 40 | final config = await findPackageConfig(Directory(packageDir)); 41 | return config?.packageOf(Uri.directory(packageDir))?.name; 42 | } 43 | 44 | void _watchExitSignal(ProcessSignal signal) { 45 | signal.watch().listen((sig) { 46 | for (final process in _allProcesses) { 47 | process.kill(sig); 48 | } 49 | exit(1); 50 | }); 51 | } 52 | 53 | ArgParser _createArgParser() => ArgParser() 54 | ..addOption( 55 | 'package', 56 | help: 'Root directory of the package to test.', 57 | defaultsTo: '.', 58 | ) 59 | ..addOption( 60 | 'package-name', 61 | help: 'Name of the package to test. ' 62 | 'Deduced from --package if not provided.', 63 | ) 64 | ..addOption('port', help: 'VM service port.', defaultsTo: '8181') 65 | ..addOption( 66 | 'out', 67 | abbr: 'o', 68 | help: 'Output directory. Defaults to /coverage.', 69 | ) 70 | ..addOption('test', help: 'Test script to run.', defaultsTo: 'test') 71 | ..addFlag( 72 | 'function-coverage', 73 | abbr: 'f', 74 | defaultsTo: false, 75 | help: 'Collect function coverage info.', 76 | ) 77 | ..addFlag( 78 | 'branch-coverage', 79 | abbr: 'b', 80 | defaultsTo: false, 81 | help: 'Collect branch coverage info.', 82 | ) 83 | ..addMultiOption('scope-output', 84 | help: 'restrict coverage results so that only scripts that start with ' 85 | 'the provided package path are considered. Defaults to the name of ' 86 | 'the package under test.') 87 | ..addFlag('help', abbr: 'h', negatable: false, help: 'Show this help.'); 88 | 89 | class Flags { 90 | Flags( 91 | this.packageDir, 92 | this.packageName, 93 | this.outDir, 94 | this.port, 95 | this.testScript, 96 | this.functionCoverage, 97 | this.branchCoverage, 98 | this.scopeOutput, { 99 | required this.rest, 100 | }); 101 | 102 | final String packageDir; 103 | final String packageName; 104 | final String outDir; 105 | final String port; 106 | final String testScript; 107 | final bool functionCoverage; 108 | final bool branchCoverage; 109 | final List scopeOutput; 110 | final List rest; 111 | } 112 | 113 | Future _parseArgs(List arguments) async { 114 | final parser = _createArgParser(); 115 | final args = parser.parse(arguments); 116 | 117 | void printUsage() { 118 | print(''' 119 | Runs tests and collects coverage for a package. 120 | 121 | By default this script assumes it's being run from the root directory of a 122 | package, and outputs a coverage.json and lcov.info to ./coverage/ 123 | 124 | Usage: test_with_coverage [OPTIONS...] [-- ] 125 | 126 | ${parser.usage} 127 | '''); 128 | } 129 | 130 | Never fail(String msg) { 131 | print('\n$msg\n'); 132 | printUsage(); 133 | exit(1); 134 | } 135 | 136 | if (args['help'] as bool) { 137 | printUsage(); 138 | exit(0); 139 | } 140 | 141 | final packageDir = path.canonicalize(args['package'] as String); 142 | if (!FileSystemEntity.isDirectorySync(packageDir)) { 143 | fail('--package is not a valid directory.'); 144 | } 145 | 146 | final packageName = (args['package-name'] as String?) ?? 147 | await _packageNameFromConfig(packageDir); 148 | if (packageName == null) { 149 | fail( 150 | "Couldn't figure out package name from --package. Make sure this is a " 151 | 'package directory, or try passing --package-name explicitly.', 152 | ); 153 | } 154 | 155 | return Flags( 156 | packageDir, 157 | packageName, 158 | (args['out'] as String?) ?? path.join(packageDir, 'coverage'), 159 | args['port'] as String, 160 | args['test'] as String, 161 | args['function-coverage'] as bool, 162 | args['branch-coverage'] as bool, 163 | args['scope-output'] as List, 164 | rest: args.rest, 165 | ); 166 | } 167 | 168 | Future main(List arguments) async { 169 | final flags = await _parseArgs(arguments); 170 | final outJson = path.join(flags.outDir, 'coverage.json'); 171 | final outLcov = path.join(flags.outDir, 'lcov.info'); 172 | 173 | if (!FileSystemEntity.isDirectorySync(flags.outDir)) { 174 | await Directory(flags.outDir).create(recursive: true); 175 | } 176 | 177 | _watchExitSignal(ProcessSignal.sighup); 178 | _watchExitSignal(ProcessSignal.sigint); 179 | if (!Platform.isWindows) { 180 | _watchExitSignal(ProcessSignal.sigterm); 181 | } 182 | 183 | final serviceUriCompleter = Completer(); 184 | final testProcess = _dartRun( 185 | [ 186 | if (flags.branchCoverage) '--branch-coverage', 187 | 'run', 188 | '--pause-isolates-on-exit', 189 | '--disable-service-auth-codes', 190 | '--enable-vm-service=${flags.port}', 191 | flags.testScript, 192 | ...flags.rest, 193 | ], 194 | onStdout: (line) { 195 | if (!serviceUriCompleter.isCompleted) { 196 | final uri = extractVMServiceUri(line); 197 | if (uri != null) { 198 | serviceUriCompleter.complete(uri); 199 | } 200 | } 201 | }, 202 | ); 203 | final serviceUri = await serviceUriCompleter.future; 204 | 205 | final scopes = 206 | flags.scopeOutput.isEmpty ? [flags.packageName] : flags.scopeOutput; 207 | await collect_coverage.main([ 208 | '--wait-paused', 209 | '--resume-isolates', 210 | '--uri=$serviceUri', 211 | for (final scope in scopes) '--scope-output=$scope', 212 | if (flags.branchCoverage) '--branch-coverage', 213 | if (flags.functionCoverage) '--function-coverage', 214 | '-o', 215 | outJson, 216 | ]); 217 | await testProcess; 218 | 219 | await format_coverage.main([ 220 | '--lcov', 221 | '--check-ignore', 222 | '--package=${flags.packageDir}', 223 | '-i', 224 | outJson, 225 | '-o', 226 | outLcov, 227 | ]); 228 | exit(0); 229 | } 230 | -------------------------------------------------------------------------------- /lib/coverage.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2014, the Dart project authors. Please see the AUTHORS file 2 | // for details. All rights reserved. Use of this source code is governed by a 3 | // BSD-style license that can be found in the LICENSE file. 4 | 5 | export 'src/chrome.dart'; 6 | export 'src/collect.dart'; 7 | export 'src/formatter.dart'; 8 | export 'src/hitmap.dart' hide hitmapToJson; 9 | export 'src/resolver.dart'; 10 | export 'src/run_and_collect.dart'; 11 | -------------------------------------------------------------------------------- /lib/src/chrome.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020, the Dart project authors. Please see the AUTHORS file 2 | // for details. All rights reserved. Use of this source code is governed by a 3 | // BSD-style license that can be found in the LICENSE file. 4 | 5 | import 'package:source_maps/parser.dart'; 6 | 7 | import 'hitmap.dart'; 8 | 9 | /// Returns a Dart based hit-map containing coverage report for the provided 10 | /// Chrome [preciseCoverage]. 11 | /// 12 | /// [sourceProvider] returns the source content for the Chrome scriptId, or null 13 | /// if not available. 14 | /// 15 | /// [sourceMapProvider] returns the associated source map content for the Chrome 16 | /// scriptId, or null if not available. 17 | /// 18 | /// [sourceUriProvider] returns the uri for the provided sourceUrl and 19 | /// associated scriptId, or null if not available. 20 | /// 21 | /// Chrome coverage information for which the corresponding source map or source 22 | /// content is null will be ignored. 23 | Future> parseChromeCoverage( 24 | List> preciseCoverage, 25 | Future Function(String scriptId) sourceProvider, 26 | Future Function(String scriptId) sourceMapProvider, 27 | Future Function(String sourceUrl, String scriptId) sourceUriProvider, 28 | ) async { 29 | final coverageReport = >{}; 30 | for (var entry in preciseCoverage) { 31 | final scriptId = entry['scriptId'] as String; 32 | 33 | final mapResponse = await sourceMapProvider(scriptId); 34 | if (mapResponse == null) continue; 35 | 36 | SingleMapping mapping; 37 | try { 38 | mapping = parse(mapResponse) as SingleMapping; 39 | } on FormatException { 40 | continue; 41 | // ignore: avoid_catching_errors 42 | } on ArgumentError { 43 | continue; 44 | } 45 | 46 | final compiledSource = await sourceProvider(scriptId); 47 | if (compiledSource == null) continue; 48 | 49 | final coverageInfo = _coverageInfoFor(entry); 50 | final offsetCoverage = _offsetCoverage(coverageInfo, compiledSource.length); 51 | final coveredPositions = _coveredPositions(compiledSource, offsetCoverage); 52 | 53 | for (var lineEntry in mapping.lines) { 54 | for (var columnEntry in lineEntry.entries) { 55 | final sourceUrlId = columnEntry.sourceUrlId; 56 | if (sourceUrlId == null) continue; 57 | final sourceUrl = mapping.urls[sourceUrlId]; 58 | 59 | // Ignore coverage information for the SDK. 60 | if (sourceUrl.startsWith('org-dartlang-sdk:')) continue; 61 | 62 | final uri = await sourceUriProvider(sourceUrl, scriptId); 63 | if (uri == null) continue; 64 | final coverage = coverageReport.putIfAbsent(uri, () => {}); 65 | 66 | final sourceLine = columnEntry.sourceLine!; 67 | final current = coverage[sourceLine + 1] ?? false; 68 | coverage[sourceLine + 1] = current || 69 | coveredPositions.contains( 70 | _Position(lineEntry.line + 1, columnEntry.column + 1)); 71 | } 72 | } 73 | } 74 | 75 | final coverageHitMaps = {}; 76 | coverageReport.forEach((uri, coverage) { 77 | final hitMap = HitMap(); 78 | for (var line in coverage.keys.toList()..sort()) { 79 | hitMap.lineHits[line] = coverage[line]! ? 1 : 0; 80 | } 81 | coverageHitMaps[uri] = hitMap; 82 | }); 83 | 84 | final allCoverage = >[]; 85 | coverageHitMaps.forEach((uri, hitMap) { 86 | allCoverage.add(hitmapToJson(hitMap, uri)); 87 | }); 88 | return {'type': 'CodeCoverage', 'coverage': allCoverage}; 89 | } 90 | 91 | /// Returns all covered positions in a provided source. 92 | Set<_Position> _coveredPositions( 93 | String compiledSource, List offsetCoverage) { 94 | final positions = <_Position>{}; 95 | // Line is 1 based. 96 | var line = 1; 97 | // Column is 1 based. 98 | var column = 0; 99 | for (var offset = 0; offset < compiledSource.length; offset++) { 100 | if (compiledSource[offset] == '\n') { 101 | line++; 102 | column = 0; 103 | } else { 104 | column++; 105 | } 106 | if (offsetCoverage[offset]) positions.add(_Position(line, column)); 107 | } 108 | return positions; 109 | } 110 | 111 | /// Returns coverage information for a Chrome entry. 112 | List<_CoverageInfo> _coverageInfoFor(Map entry) { 113 | final result = <_CoverageInfo>[]; 114 | for (var functions 115 | in (entry['functions'] as List).cast>()) { 116 | for (var range 117 | in (functions['ranges'] as List).cast>()) { 118 | result.add(_CoverageInfo( 119 | range['startOffset'] as int, 120 | range['endOffset'] as int, 121 | (range['count'] as int) > 0, 122 | )); 123 | } 124 | } 125 | return result; 126 | } 127 | 128 | /// Returns the coverage information for each offset. 129 | List _offsetCoverage(List<_CoverageInfo> coverageInfo, int sourceLength) { 130 | final offsetCoverage = List.filled(sourceLength, false); 131 | 132 | // Sort coverage information by their size. 133 | // Coverage information takes granularity as precedence. 134 | coverageInfo.sort((a, b) => 135 | (b.endOffset - b.startOffset).compareTo(a.endOffset - a.startOffset)); 136 | 137 | for (var range in coverageInfo) { 138 | for (var i = range.startOffset; i < range.endOffset; i++) { 139 | offsetCoverage[i] = range.isCovered; 140 | } 141 | } 142 | 143 | return offsetCoverage; 144 | } 145 | 146 | class _CoverageInfo { 147 | _CoverageInfo(this.startOffset, this.endOffset, this.isCovered); 148 | 149 | /// 0 based byte offset. 150 | final int startOffset; 151 | 152 | /// 0 based byte offset. 153 | final int endOffset; 154 | 155 | final bool isCovered; 156 | } 157 | 158 | /// A covered position in a source file where [line] and [column] are 1 based. 159 | class _Position { 160 | _Position(this.line, this.column); 161 | 162 | final int line; 163 | final int column; 164 | 165 | @override 166 | int get hashCode => Object.hash(line, column); 167 | 168 | @override 169 | bool operator ==(Object o) => 170 | o is _Position && o.line == line && o.column == column; 171 | } 172 | -------------------------------------------------------------------------------- /lib/src/collect.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2014, the Dart project authors. Please see the AUTHORS file 2 | // for details. All rights reserved. Use of this source code is governed by a 3 | // BSD-style license that can be found in the LICENSE file. 4 | 5 | import 'dart:async'; 6 | import 'dart:io'; 7 | 8 | import 'package:vm_service/vm_service.dart'; 9 | 10 | import 'hitmap.dart'; 11 | import 'util.dart'; 12 | 13 | const _retryInterval = Duration(milliseconds: 200); 14 | const _debugTokenPositions = bool.fromEnvironment('DEBUG_COVERAGE'); 15 | 16 | /// Collects coverage for all isolates in the running VM. 17 | /// 18 | /// Collects a hit-map containing merged coverage for all isolates in the Dart 19 | /// VM associated with the specified [serviceUri]. Returns a map suitable for 20 | /// input to the coverage formatters that ship with this package. 21 | /// 22 | /// [serviceUri] must specify the http/https URI of the service port of a 23 | /// running Dart VM and must not be null. 24 | /// 25 | /// If [resume] is true, all isolates will be resumed once coverage collection 26 | /// is complete. 27 | /// 28 | /// If [waitPaused] is true, collection will not begin until all isolates are 29 | /// in the paused state. 30 | /// 31 | /// If [includeDart] is true, code coverage for core `dart:*` libraries will be 32 | /// collected. 33 | /// 34 | /// If [functionCoverage] is true, function coverage information will be 35 | /// collected. 36 | /// 37 | /// If [branchCoverage] is true, branch coverage information will be collected. 38 | /// This will only work correctly if the target VM was run with the 39 | /// --branch-coverage flag. 40 | /// 41 | /// If [scopedOutput] is non-empty, coverage will be restricted so that only 42 | /// scripts that start with any of the provided paths are considered. 43 | /// 44 | /// If [isolateIds] is set, the coverage gathering will be restricted to only 45 | /// those VM isolates. 46 | /// 47 | /// If [coverableLineCache] is set, the collector will avoid recompiling 48 | /// libraries it has already seen (see VmService.getSourceReport's 49 | /// librariesAlreadyCompiled parameter). This is only useful when doing more 50 | /// than one [collect] call over the same libraries. Pass an empty map to the 51 | /// first call, and then pass the same map to all subsequent calls. 52 | /// 53 | /// [serviceOverrideForTesting] is for internal testing only, and should not be 54 | /// set by users. 55 | Future> collect(Uri serviceUri, bool resume, 56 | bool waitPaused, bool includeDart, Set? scopedOutput, 57 | {Set? isolateIds, 58 | Duration? timeout, 59 | bool functionCoverage = false, 60 | bool branchCoverage = false, 61 | Map>? coverableLineCache, 62 | VmService? serviceOverrideForTesting}) async { 63 | scopedOutput ??= {}; 64 | 65 | late VmService service; 66 | if (serviceOverrideForTesting != null) { 67 | service = serviceOverrideForTesting; 68 | } else { 69 | // Create websocket URI. Handle any trailing slashes. 70 | final pathSegments = 71 | serviceUri.pathSegments.where((c) => c.isNotEmpty).toList()..add('ws'); 72 | final uri = serviceUri.replace(scheme: 'ws', pathSegments: pathSegments); 73 | 74 | await retry(() async { 75 | try { 76 | final options = const CompressionOptions(enabled: false); 77 | final socket = await WebSocket.connect('$uri', compression: options); 78 | final controller = StreamController(); 79 | socket.listen((data) => controller.add(data as String), onDone: () { 80 | controller.close(); 81 | service.dispose(); 82 | }); 83 | service = VmService(controller.stream, socket.add, 84 | log: StdoutLog(), disposeHandler: socket.close); 85 | await service.getVM().timeout(_retryInterval); 86 | } on TimeoutException { 87 | // The signature changed in vm_service version 6.0.0. 88 | // ignore: await_only_futures 89 | await service.dispose(); 90 | rethrow; 91 | } 92 | }, _retryInterval, timeout: timeout); 93 | } 94 | 95 | try { 96 | if (waitPaused) { 97 | await _waitIsolatesPaused(service, timeout: timeout); 98 | } 99 | 100 | return await _getAllCoverage(service, includeDart, functionCoverage, 101 | branchCoverage, scopedOutput, isolateIds, coverableLineCache); 102 | } finally { 103 | if (resume) { 104 | await _resumeIsolates(service); 105 | } 106 | // The signature changed in vm_service version 6.0.0. 107 | // ignore: await_only_futures 108 | await service.dispose(); 109 | } 110 | } 111 | 112 | Future> _getAllCoverage( 113 | VmService service, 114 | bool includeDart, 115 | bool functionCoverage, 116 | bool branchCoverage, 117 | Set? scopedOutput, 118 | Set? isolateIds, 119 | Map>? coverableLineCache) async { 120 | scopedOutput ??= {}; 121 | final vm = await service.getVM(); 122 | final allCoverage = >[]; 123 | 124 | final sourceReportKinds = [ 125 | SourceReportKind.kCoverage, 126 | if (branchCoverage) SourceReportKind.kBranchCoverage, 127 | ]; 128 | 129 | final librariesAlreadyCompiled = coverableLineCache?.keys.toList(); 130 | 131 | // Program counters are shared between isolates in the same group. So we need 132 | // to make sure we're only gathering coverage data for one isolate in each 133 | // group, otherwise we'll double count the hits. 134 | final coveredIsolateGroups = {}; 135 | 136 | for (var isolateRef in vm.isolates!) { 137 | if (isolateIds != null && !isolateIds.contains(isolateRef.id)) continue; 138 | final isolateGroupId = isolateRef.isolateGroupId; 139 | if (isolateGroupId != null) { 140 | if (coveredIsolateGroups.contains(isolateGroupId)) continue; 141 | coveredIsolateGroups.add(isolateGroupId); 142 | } 143 | 144 | late final SourceReport isolateReport; 145 | try { 146 | isolateReport = await service.getSourceReport( 147 | isolateRef.id!, 148 | sourceReportKinds, 149 | forceCompile: true, 150 | reportLines: true, 151 | libraryFilters: scopedOutput.isNotEmpty 152 | ? List.from(scopedOutput.map((filter) => 'package:$filter/')) 153 | : null, 154 | librariesAlreadyCompiled: librariesAlreadyCompiled, 155 | ); 156 | } on SentinelException { 157 | continue; 158 | } 159 | final coverage = await _processSourceReport( 160 | service, 161 | isolateRef, 162 | isolateReport, 163 | includeDart, 164 | functionCoverage, 165 | coverableLineCache, 166 | scopedOutput); 167 | allCoverage.addAll(coverage); 168 | } 169 | return {'type': 'CodeCoverage', 'coverage': allCoverage}; 170 | } 171 | 172 | Future _resumeIsolates(VmService service) async { 173 | final vm = await service.getVM(); 174 | final futures = []; 175 | for (var isolateRef in vm.isolates!) { 176 | // Guard against sync as well as async errors: sync - when we are writing 177 | // message to the socket, the socket might be closed; async - when we are 178 | // waiting for the response, the socket again closes. 179 | futures.add(Future.sync(() async { 180 | final isolate = await service.getIsolate(isolateRef.id!); 181 | if (isolate.pauseEvent!.kind != EventKind.kResume) { 182 | await service.resume(isolateRef.id!); 183 | } 184 | })); 185 | } 186 | try { 187 | await Future.wait(futures); 188 | } catch (_) { 189 | // Ignore resume isolate failures 190 | } 191 | } 192 | 193 | Future _waitIsolatesPaused(VmService service, {Duration? timeout}) async { 194 | final pauseEvents = { 195 | EventKind.kPauseStart, 196 | EventKind.kPauseException, 197 | EventKind.kPauseExit, 198 | EventKind.kPauseInterrupted, 199 | EventKind.kPauseBreakpoint 200 | }; 201 | 202 | Future allPaused() async { 203 | final vm = await service.getVM(); 204 | if (vm.isolates!.isEmpty) throw StateError('No isolates.'); 205 | for (var isolateRef in vm.isolates!) { 206 | final isolate = await service.getIsolate(isolateRef.id!); 207 | if (!pauseEvents.contains(isolate.pauseEvent!.kind)) { 208 | throw StateError('Unpaused isolates remaining.'); 209 | } 210 | } 211 | } 212 | 213 | return retry(allPaused, _retryInterval, timeout: timeout); 214 | } 215 | 216 | /// Returns the line number to which the specified token position maps. 217 | /// 218 | /// Performs a binary search within the script's token position table to locate 219 | /// the line in question. 220 | int? _getLineFromTokenPos(Script script, int tokenPos) { 221 | // TODO(cbracken): investigate whether caching this lookup results in 222 | // significant performance gains. 223 | var min = 0; 224 | var max = script.tokenPosTable!.length; 225 | while (min < max) { 226 | final mid = min + ((max - min) >> 1); 227 | final row = script.tokenPosTable![mid]; 228 | if (row[1] > tokenPos) { 229 | max = mid; 230 | } else { 231 | for (var i = 1; i < row.length; i += 2) { 232 | if (row[i] == tokenPos) return row.first; 233 | } 234 | min = mid + 1; 235 | } 236 | } 237 | return null; 238 | } 239 | 240 | /// Returns a JSON coverage list backward-compatible with pre-1.16.0 SDKs. 241 | Future>> _processSourceReport( 242 | VmService service, 243 | IsolateRef isolateRef, 244 | SourceReport report, 245 | bool includeDart, 246 | bool functionCoverage, 247 | Map>? coverableLineCache, 248 | Set scopedOutput) async { 249 | final hitMaps = {}; 250 | final scripts = {}; 251 | final libraries = {}; 252 | final needScripts = functionCoverage; 253 | 254 | Future getScript(ScriptRef? scriptRef) async { 255 | if (scriptRef == null) { 256 | return null; 257 | } 258 | if (!scripts.containsKey(scriptRef)) { 259 | scripts[scriptRef] = 260 | await service.getObject(isolateRef.id!, scriptRef.id!) as Script; 261 | } 262 | return scripts[scriptRef]; 263 | } 264 | 265 | HitMap getHitMap(Uri scriptUri) => hitMaps.putIfAbsent(scriptUri, HitMap.new); 266 | 267 | Future processFunction(FuncRef funcRef) async { 268 | final func = await service.getObject(isolateRef.id!, funcRef.id!) as Func; 269 | if ((func.implicit ?? false) || (func.isAbstract ?? false)) { 270 | return; 271 | } 272 | final location = func.location; 273 | if (location == null) { 274 | return; 275 | } 276 | final script = await getScript(location.script); 277 | if (script == null) { 278 | return; 279 | } 280 | final funcName = await _getFuncName(service, isolateRef, func); 281 | final tokenPos = location.tokenPos!; 282 | final line = _getLineFromTokenPos(script, tokenPos); 283 | if (line == null) { 284 | if (_debugTokenPositions) { 285 | stderr.writeln( 286 | 'tokenPos $tokenPos in function ${funcRef.name} has no line ' 287 | 'mapping for script ${script.uri!}'); 288 | } 289 | return; 290 | } 291 | final hits = getHitMap(Uri.parse(script.uri!)); 292 | hits.funcHits ??= {}; 293 | (hits.funcNames ??= {})[line] = funcName; 294 | } 295 | 296 | for (var range in report.ranges!) { 297 | final scriptRef = report.scripts![range.scriptIndex!]; 298 | final scriptUriString = scriptRef.uri; 299 | if (!scopedOutput.includesScript(scriptUriString)) { 300 | // Sometimes a range's script can be different to the function's script 301 | // (eg mixins), so we have to re-check the scope filter. 302 | // See https://github.com/dart-lang/coverage/issues/495 303 | continue; 304 | } 305 | final scriptUri = Uri.parse(scriptUriString!); 306 | 307 | // If we have a coverableLineCache, use it in the same way we use 308 | // SourceReportCoverage.misses: to add zeros to the coverage result for all 309 | // the lines that don't have a hit. Afterwards, add all the lines that were 310 | // hit or missed to the cache, so that the next coverage collection won't 311 | // need to compile this libarry. 312 | final coverableLines = 313 | coverableLineCache?.putIfAbsent(scriptUriString, () => {}); 314 | 315 | // Not returned in scripts section of source report. 316 | if (scriptUri.scheme == 'evaluate') continue; 317 | 318 | // Skip scripts from dart:. 319 | if (!includeDart && scriptUri.scheme == 'dart') continue; 320 | 321 | // Look up the hit maps for this script (shared across isolates). 322 | final hits = getHitMap(scriptUri); 323 | 324 | Script? script; 325 | if (needScripts) { 326 | script = await getScript(scriptRef); 327 | if (script == null) continue; 328 | } 329 | 330 | // If the script's library isn't loaded, load it then look up all its funcs. 331 | final libRef = script?.library; 332 | if (functionCoverage && libRef != null && !libraries.contains(libRef)) { 333 | libraries.add(libRef); 334 | final library = 335 | await service.getObject(isolateRef.id!, libRef.id!) as Library; 336 | if (library.functions != null) { 337 | for (var funcRef in library.functions!) { 338 | await processFunction(funcRef); 339 | } 340 | } 341 | if (library.classes != null) { 342 | for (var classRef in library.classes!) { 343 | final clazz = 344 | await service.getObject(isolateRef.id!, classRef.id!) as Class; 345 | if (clazz.functions != null) { 346 | for (var funcRef in clazz.functions!) { 347 | await processFunction(funcRef); 348 | } 349 | } 350 | } 351 | } 352 | } 353 | 354 | // Collect hits and misses. 355 | final coverage = range.coverage; 356 | 357 | if (coverage == null) continue; 358 | 359 | void forEachLine(List? tokenPositions, void Function(int line) body) { 360 | if (tokenPositions == null) return; 361 | for (final line in tokenPositions) { 362 | body(line); 363 | } 364 | } 365 | 366 | if (coverableLines != null) { 367 | for (final line in coverableLines) { 368 | hits.lineHits.putIfAbsent(line, () => 0); 369 | } 370 | } 371 | 372 | forEachLine(coverage.hits, (line) { 373 | hits.lineHits.increment(line); 374 | coverableLines?.add(line); 375 | if (hits.funcNames != null && hits.funcNames!.containsKey(line)) { 376 | hits.funcHits!.increment(line); 377 | } 378 | }); 379 | forEachLine(coverage.misses, (line) { 380 | hits.lineHits.putIfAbsent(line, () => 0); 381 | coverableLines?.add(line); 382 | }); 383 | hits.funcNames?.forEach((line, funcName) { 384 | hits.funcHits?.putIfAbsent(line, () => 0); 385 | }); 386 | 387 | final branchCoverage = range.branchCoverage; 388 | if (branchCoverage != null) { 389 | hits.branchHits ??= {}; 390 | forEachLine(branchCoverage.hits, (line) { 391 | hits.branchHits!.increment(line); 392 | }); 393 | forEachLine(branchCoverage.misses, (line) { 394 | hits.branchHits!.putIfAbsent(line, () => 0); 395 | }); 396 | } 397 | } 398 | 399 | // Output JSON 400 | final coverage = >[]; 401 | hitMaps.forEach((uri, hits) { 402 | coverage.add(hitmapToJson(hits, uri)); 403 | }); 404 | return coverage; 405 | } 406 | 407 | extension _MapExtension on Map { 408 | void increment(T key) => this[key] = (this[key] ?? 0) + 1; 409 | } 410 | 411 | Future _getFuncName( 412 | VmService service, IsolateRef isolateRef, Func func) async { 413 | if (func.name == null) { 414 | return '${func.type}:${func.location!.tokenPos}'; 415 | } 416 | final owner = func.owner; 417 | if (owner is ClassRef) { 418 | final cls = await service.getObject(isolateRef.id!, owner.id!) as Class; 419 | if (cls.name != null) return '${cls.name}.${func.name}'; 420 | } 421 | return func.name!; 422 | } 423 | 424 | class StdoutLog extends Log { 425 | @override 426 | void warning(String message) => print(message); 427 | 428 | @override 429 | void severe(String message) => print(message); 430 | } 431 | 432 | extension _ScopedOutput on Set { 433 | bool includesScript(String? scriptUriString) { 434 | if (scriptUriString == null) return false; 435 | 436 | // If the set is empty, it means the user didn't specify a --scope-output 437 | // flag, so allow everything. 438 | if (isEmpty) return true; 439 | 440 | final scriptUri = Uri.parse(scriptUriString); 441 | if (scriptUri.scheme != 'package') return false; 442 | 443 | final scope = scriptUri.pathSegments.first; 444 | return contains(scope); 445 | } 446 | } 447 | -------------------------------------------------------------------------------- /lib/src/formatter.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2014, the Dart project authors. Please see the AUTHORS file 2 | // for details. All rights reserved. Use of this source code is governed by a 3 | // BSD-style license that can be found in the LICENSE file. 4 | 5 | import 'package:glob/glob.dart'; 6 | import 'package:path/path.dart' as p; 7 | 8 | import 'hitmap.dart'; 9 | import 'resolver.dart'; 10 | 11 | @Deprecated('Migrate to FileHitMapsFormatter') 12 | abstract class Formatter { 13 | /// Returns the formatted coverage data. 14 | Future format(Map> hitmap); 15 | } 16 | 17 | /// Converts the given hitmap to lcov format and appends the result to 18 | /// env.output. 19 | /// 20 | /// Returns a [Future] that completes as soon as all map entries have been 21 | /// emitted. 22 | @Deprecated('Migrate to FileHitMapsFormatter.formatLcov') 23 | class LcovFormatter implements Formatter { 24 | /// Creates a LCOV formatter. 25 | /// 26 | /// If [reportOn] is provided, coverage report output is limited to files 27 | /// prefixed with one of the paths included. If [basePath] is provided, paths 28 | /// are reported relative to that path. 29 | LcovFormatter(this.resolver, {this.reportOn, this.basePath}); 30 | 31 | final Resolver resolver; 32 | final String? basePath; 33 | final List? reportOn; 34 | 35 | @override 36 | Future format(Map> hitmap) { 37 | return Future.value(hitmap 38 | .map((key, value) => MapEntry(key, HitMap(value))) 39 | .formatLcov(resolver, basePath: basePath, reportOn: reportOn)); 40 | } 41 | } 42 | 43 | /// Converts the given hitmap to a pretty-print format and appends the result 44 | /// to env.output. 45 | /// 46 | /// Returns a [Future] that completes as soon as all map entries have been 47 | /// emitted. 48 | @Deprecated('Migrate to FileHitMapsFormatter.prettyPrint') 49 | class PrettyPrintFormatter implements Formatter { 50 | /// Creates a pretty-print formatter. 51 | /// 52 | /// If [reportOn] is provided, coverage report output is limited to files 53 | /// prefixed with one of the paths included. 54 | PrettyPrintFormatter(this.resolver, this.loader, 55 | {this.reportOn, this.reportFuncs = false}); 56 | 57 | final Resolver resolver; 58 | final Loader loader; 59 | final List? reportOn; 60 | final bool reportFuncs; 61 | 62 | @override 63 | Future format(Map> hitmap) { 64 | return hitmap.map((key, value) => MapEntry(key, HitMap(value))).prettyPrint( 65 | resolver, loader, 66 | reportOn: reportOn, reportFuncs: reportFuncs); 67 | } 68 | } 69 | 70 | extension FileHitMapsFormatter on Map { 71 | /// Converts the given hitmap to lcov format. 72 | /// 73 | /// If [reportOn] is provided, coverage report output is limited to files 74 | /// prefixed with one of the paths included. If [basePath] is provided, paths 75 | /// are reported relative to that path. 76 | String formatLcov( 77 | Resolver resolver, { 78 | String? basePath, 79 | List? reportOn, 80 | Set? ignoreGlobs, 81 | }) { 82 | final pathFilter = _getPathFilter( 83 | reportOn: reportOn, 84 | ignoreGlobs: ignoreGlobs, 85 | ); 86 | final buf = StringBuffer(); 87 | for (final entry in entries) { 88 | final v = entry.value; 89 | final lineHits = v.lineHits; 90 | final funcHits = v.funcHits; 91 | final funcNames = v.funcNames; 92 | final branchHits = v.branchHits; 93 | var source = resolver.resolve(entry.key); 94 | if (source == null) { 95 | continue; 96 | } 97 | 98 | if (!pathFilter(source)) { 99 | continue; 100 | } 101 | 102 | if (basePath != null) { 103 | source = p.relative(source, from: basePath); 104 | } 105 | 106 | buf.write('SF:$source\n'); 107 | if (funcHits != null && funcNames != null) { 108 | for (final k in funcNames.keys.toList()..sort()) { 109 | buf.write('FN:$k,${funcNames[k]}\n'); 110 | } 111 | for (final k in funcHits.keys.toList()..sort()) { 112 | if (funcHits[k]! != 0) { 113 | buf.write('FNDA:${funcHits[k]},${funcNames[k]}\n'); 114 | } 115 | } 116 | buf.write('FNF:${funcNames.length}\n'); 117 | buf.write('FNH:${funcHits.values.where((v) => v > 0).length}\n'); 118 | } 119 | for (final k in lineHits.keys.toList()..sort()) { 120 | buf.write('DA:$k,${lineHits[k]}\n'); 121 | } 122 | buf.write('LF:${lineHits.length}\n'); 123 | buf.write('LH:${lineHits.values.where((v) => v > 0).length}\n'); 124 | if (branchHits != null) { 125 | for (final k in branchHits.keys.toList()..sort()) { 126 | buf.write('BRDA:$k,0,0,${branchHits[k]}\n'); 127 | } 128 | } 129 | buf.write('end_of_record\n'); 130 | } 131 | 132 | return buf.toString(); 133 | } 134 | 135 | /// Converts the given hitmap to a pretty-print format. 136 | /// 137 | /// If [reportOn] is provided, coverage report output is limited to files 138 | /// prefixed with one of the paths included. If [reportFuncs] is provided, 139 | /// only function coverage information will be shown. 140 | Future prettyPrint( 141 | Resolver resolver, 142 | Loader loader, { 143 | List? reportOn, 144 | Set? ignoreGlobs, 145 | bool reportFuncs = false, 146 | bool reportBranches = false, 147 | }) async { 148 | final pathFilter = _getPathFilter( 149 | reportOn: reportOn, 150 | ignoreGlobs: ignoreGlobs, 151 | ); 152 | final buf = StringBuffer(); 153 | for (final entry in entries) { 154 | final v = entry.value; 155 | if (reportFuncs && v.funcHits == null) { 156 | throw StateError( 157 | 'Function coverage formatting was requested, but the hit map is ' 158 | 'missing function coverage information. Did you run ' 159 | 'collect_coverage with the --function-coverage flag?', 160 | ); 161 | } 162 | if (reportBranches && v.branchHits == null) { 163 | throw StateError( 164 | 'Branch coverage formatting was requested, but the hit map is ' 165 | 'missing branch coverage information. Did you run ' 166 | 'collect_coverage with the --branch-coverage flag?'); 167 | } 168 | final hits = reportFuncs 169 | ? v.funcHits! 170 | : reportBranches 171 | ? v.branchHits! 172 | : v.lineHits; 173 | final source = resolver.resolve(entry.key); 174 | if (source == null) { 175 | continue; 176 | } 177 | 178 | if (!pathFilter(source)) { 179 | continue; 180 | } 181 | 182 | final lines = await loader.load(source); 183 | if (lines == null) { 184 | continue; 185 | } 186 | buf.writeln(source); 187 | for (var line = 1; line <= lines.length; line++) { 188 | var prefix = _prefix; 189 | if (hits.containsKey(line)) { 190 | prefix = hits[line].toString().padLeft(_prefix.length); 191 | } 192 | buf.writeln('$prefix|${lines[line - 1]}'); 193 | } 194 | } 195 | 196 | return buf.toString(); 197 | } 198 | } 199 | 200 | const _prefix = ' '; 201 | 202 | typedef _PathFilter = bool Function(String path); 203 | 204 | _PathFilter _getPathFilter({List? reportOn, Set? ignoreGlobs}) { 205 | if (reportOn == null && ignoreGlobs == null) return (String path) => true; 206 | 207 | final absolutePaths = reportOn?.map(p.canonicalize).toList(); 208 | 209 | return (String path) { 210 | final canonicalizedPath = p.canonicalize(path); 211 | 212 | if (absolutePaths != null && 213 | !absolutePaths.any(canonicalizedPath.startsWith)) { 214 | return false; 215 | } 216 | if (ignoreGlobs != null && 217 | ignoreGlobs.any((glob) => glob.matches(canonicalizedPath))) { 218 | return false; 219 | } 220 | 221 | return true; 222 | }; 223 | } 224 | -------------------------------------------------------------------------------- /lib/src/hitmap.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2014, the Dart project authors. Please see the AUTHORS file 2 | // for details. All rights reserved. Use of this source code is governed by a 3 | // BSD-style license that can be found in the LICENSE file. 4 | 5 | import 'dart:convert' show json; 6 | import 'dart:io'; 7 | 8 | import 'resolver.dart'; 9 | import 'util.dart'; 10 | 11 | /// Contains line and function hit information for a single script. 12 | class HitMap { 13 | /// Constructs a HitMap. 14 | HitMap([ 15 | Map? lineHits, 16 | this.funcHits, 17 | this.funcNames, 18 | this.branchHits, 19 | ]) : lineHits = lineHits ?? {}; 20 | 21 | /// Map from line to hit count for that line. 22 | final Map lineHits; 23 | 24 | /// Map from the first line of each function, to the hit count for that 25 | /// function. Null if function coverage info was not gathered. 26 | Map? funcHits; 27 | 28 | /// Map from the first line of each function, to the function name. Null if 29 | /// function coverage info was not gathered. 30 | Map? funcNames; 31 | 32 | /// Map from branch line, to the hit count for that branch. Null if branch 33 | /// coverage info was not gathered. 34 | Map? branchHits; 35 | 36 | /// Creates a single hitmap from a raw json object. 37 | /// 38 | /// Note that when [checkIgnoredLines] is `true` all files will be 39 | /// read to get ignore comments. This will add some overhead. 40 | /// To combat this when calling this function multiple times from the 41 | /// same source (e.g. test runs of different files) a cache is taken 42 | /// via [ignoredLinesInFilesCache]. If this cache contains the parsed 43 | /// data for the specific file already, the file will not be read and 44 | /// parsed again. 45 | /// 46 | /// Throws away all entries that are not resolvable. 47 | static Map parseJsonSync( 48 | List> jsonResult, { 49 | required bool checkIgnoredLines, 50 | required Map>?> ignoredLinesInFilesCache, 51 | required Resolver resolver, 52 | }) { 53 | final loader = Loader(); 54 | 55 | // Map of source file to map of line to hit count for that line. 56 | final globalHitMap = {}; 57 | 58 | for (var e in jsonResult) { 59 | final source = e['source'] as String?; 60 | if (source == null) { 61 | // Couldn't resolve import, so skip this entry. 62 | continue; 63 | } 64 | 65 | var ignoredLinesList = >[]; 66 | 67 | if (checkIgnoredLines) { 68 | if (ignoredLinesInFilesCache.containsKey(source)) { 69 | final cacheHit = ignoredLinesInFilesCache[source]; 70 | if (cacheHit == null) { 71 | // Null-entry indicates that the whole file was ignored. 72 | continue; 73 | } 74 | ignoredLinesList = cacheHit; 75 | } else { 76 | final path = resolver.resolve(source); 77 | if (path != null) { 78 | final lines = loader.loadSync(path) ?? []; 79 | ignoredLinesList = getIgnoredLines(path, lines); 80 | 81 | // Ignore the whole file. 82 | if (ignoredLinesList.length == 1 && 83 | ignoredLinesList[0][0] == 0 && 84 | ignoredLinesList[0][1] == lines.length) { 85 | // Null-entry indicates that the whole file was ignored. 86 | ignoredLinesInFilesCache[source] = null; 87 | continue; 88 | } 89 | ignoredLinesInFilesCache[source] = ignoredLinesList; 90 | } else { 91 | // Couldn't resolve source. Allow cache to answer next time 92 | // anyway. 93 | ignoredLinesInFilesCache[source] = ignoredLinesList; 94 | } 95 | } 96 | } 97 | 98 | // Move to the first ignore range. 99 | final ignoredLines = ignoredLinesList.iterator; 100 | var hasCurrent = ignoredLines.moveNext(); 101 | 102 | bool shouldIgnoreLine(Iterator> ignoredRanges, int line) { 103 | if (!hasCurrent || ignoredRanges.current.isEmpty) { 104 | return false; 105 | } 106 | 107 | if (line < ignoredRanges.current[0]) return false; 108 | 109 | while (hasCurrent && 110 | ignoredRanges.current.isNotEmpty && 111 | ignoredRanges.current[1] < line) { 112 | hasCurrent = ignoredRanges.moveNext(); 113 | } 114 | 115 | if (hasCurrent && 116 | ignoredRanges.current.isNotEmpty && 117 | ignoredRanges.current[0] <= line && 118 | line <= ignoredRanges.current[1]) { 119 | return true; 120 | } 121 | 122 | return false; 123 | } 124 | 125 | void addToMap(Map map, int line, int count) { 126 | final oldCount = map.putIfAbsent(line, () => 0); 127 | map[line] = count + oldCount; 128 | } 129 | 130 | void fillHitMap(List hits, Map hitMap) { 131 | // Ignore line annotations require hits to be sorted. 132 | hits = _sortHits(hits); 133 | // hits is a flat array of the following format: 134 | // [ , ,...] 135 | // line: number. 136 | // linerange: '-'. 137 | for (var i = 0; i < hits.length; i += 2) { 138 | final k = hits[i]; 139 | if (k is int) { 140 | // Single line. 141 | if (shouldIgnoreLine(ignoredLines, k)) continue; 142 | 143 | addToMap(hitMap, k, hits[i + 1] as int); 144 | } else if (k is String) { 145 | // Linerange. We expand line ranges to actual lines at this point. 146 | final splitPos = k.indexOf('-'); 147 | final start = int.parse(k.substring(0, splitPos)); 148 | final end = int.parse(k.substring(splitPos + 1)); 149 | for (var j = start; j <= end; j++) { 150 | if (shouldIgnoreLine(ignoredLines, j)) continue; 151 | 152 | addToMap(hitMap, j, hits[i + 1] as int); 153 | } 154 | } else { 155 | throw StateError('Expected value of type int or String'); 156 | } 157 | } 158 | } 159 | 160 | final sourceHitMap = globalHitMap.putIfAbsent(source, HitMap.new); 161 | fillHitMap(e['hits'] as List, sourceHitMap.lineHits); 162 | if (e.containsKey('funcHits')) { 163 | sourceHitMap.funcHits ??= {}; 164 | fillHitMap(e['funcHits'] as List, sourceHitMap.funcHits!); 165 | } 166 | if (e.containsKey('funcNames')) { 167 | sourceHitMap.funcNames ??= {}; 168 | final funcNames = e['funcNames'] as List; 169 | for (var i = 0; i < funcNames.length; i += 2) { 170 | sourceHitMap.funcNames![funcNames[i] as int] = 171 | funcNames[i + 1] as String; 172 | } 173 | } 174 | if (e.containsKey('branchHits')) { 175 | sourceHitMap.branchHits ??= {}; 176 | fillHitMap(e['branchHits'] as List, sourceHitMap.branchHits!); 177 | } 178 | } 179 | return globalHitMap; 180 | } 181 | 182 | /// Creates a single hitmap from a raw json object. 183 | /// 184 | /// Throws away all entries that are not resolvable. 185 | static Future> parseJson( 186 | List> jsonResult, { 187 | bool checkIgnoredLines = false, 188 | @Deprecated('Use packagePath') String? packagesPath, 189 | String? packagePath, 190 | }) async { 191 | final resolver = await Resolver.create( 192 | packagesPath: packagesPath, packagePath: packagePath); 193 | return parseJsonSync(jsonResult, 194 | checkIgnoredLines: checkIgnoredLines, 195 | ignoredLinesInFilesCache: {}, 196 | resolver: resolver); 197 | } 198 | 199 | /// Generates a merged hitmap from a set of coverage JSON files. 200 | static Future> parseFiles( 201 | Iterable files, { 202 | bool checkIgnoredLines = false, 203 | @Deprecated('Use packagePath') String? packagesPath, 204 | String? packagePath, 205 | }) async { 206 | final globalHitmap = {}; 207 | for (var file in files) { 208 | final contents = file.readAsStringSync(); 209 | final jsonMap = json.decode(contents) as Map; 210 | if (jsonMap.containsKey('coverage')) { 211 | final jsonResult = jsonMap['coverage'] as List; 212 | globalHitmap.merge(await HitMap.parseJson( 213 | jsonResult.cast>(), 214 | checkIgnoredLines: checkIgnoredLines, 215 | // ignore: deprecated_member_use_from_same_package 216 | packagesPath: packagesPath, 217 | packagePath: packagePath, 218 | )); 219 | } 220 | } 221 | return globalHitmap; 222 | } 223 | } 224 | 225 | extension FileHitMaps on Map { 226 | /// Merges [newMap] into this one. 227 | void merge(Map newMap) { 228 | newMap.forEach((file, v) { 229 | final fileResult = this[file]; 230 | if (fileResult != null) { 231 | _mergeHitCounts(v.lineHits, fileResult.lineHits); 232 | if (v.funcHits != null) { 233 | fileResult.funcHits ??= {}; 234 | _mergeHitCounts(v.funcHits!, fileResult.funcHits!); 235 | } 236 | if (v.funcNames != null) { 237 | fileResult.funcNames ??= {}; 238 | v.funcNames?.forEach((line, name) { 239 | fileResult.funcNames![line] = name; 240 | }); 241 | } 242 | if (v.branchHits != null) { 243 | fileResult.branchHits ??= {}; 244 | _mergeHitCounts(v.branchHits!, fileResult.branchHits!); 245 | } 246 | } else { 247 | this[file] = v; 248 | } 249 | }); 250 | } 251 | 252 | static void _mergeHitCounts(Map src, Map dest) { 253 | src.forEach((line, count) { 254 | final lineFileResult = dest[line]; 255 | if (lineFileResult == null) { 256 | dest[line] = count; 257 | } else { 258 | dest[line] = lineFileResult + count; 259 | } 260 | }); 261 | } 262 | } 263 | 264 | /// Class containing information about a coverage hit. 265 | class _HitInfo { 266 | _HitInfo(this.firstLine, this.hitRange, this.hitCount); 267 | 268 | /// The line number of the first line of this hit range. 269 | final int firstLine; 270 | 271 | /// A hit range is either a number (1 line) or a String of the form 272 | /// "start-end" (multi-line range). 273 | final dynamic hitRange; 274 | 275 | /// How many times this hit range was executed. 276 | final int hitCount; 277 | } 278 | 279 | /// Creates a single hitmap from a raw json object. 280 | /// 281 | /// Throws away all entries that are not resolvable. 282 | @Deprecated('Migrate to HitMap.parseJson') 283 | Future>> createHitmap( 284 | List> jsonResult, { 285 | bool checkIgnoredLines = false, 286 | @Deprecated('Use packagePath') String? packagesPath, 287 | String? packagePath, 288 | }) async { 289 | final result = await HitMap.parseJson( 290 | jsonResult, 291 | checkIgnoredLines: checkIgnoredLines, 292 | packagesPath: packagesPath, 293 | packagePath: packagePath, 294 | ); 295 | return result.map((key, value) => MapEntry(key, value.lineHits)); 296 | } 297 | 298 | /// Merges [newMap] into [result]. 299 | @Deprecated('Migrate to FileHitMaps.merge') 300 | void mergeHitmaps( 301 | Map> newMap, Map> result) { 302 | newMap.forEach((file, v) { 303 | final fileResult = result[file]; 304 | if (fileResult != null) { 305 | v.forEach((line, count) { 306 | final lineFileResult = fileResult[line]; 307 | if (lineFileResult == null) { 308 | fileResult[line] = count; 309 | } else { 310 | fileResult[line] = lineFileResult + count; 311 | } 312 | }); 313 | } else { 314 | result[file] = v; 315 | } 316 | }); 317 | } 318 | 319 | /// Generates a merged hitmap from a set of coverage JSON files. 320 | @Deprecated('Migrate to HitMap.parseFiles') 321 | Future>> parseCoverage( 322 | Iterable files, 323 | int _, { 324 | bool checkIgnoredLines = false, 325 | @Deprecated('Use packagePath') String? packagesPath, 326 | String? packagePath, 327 | }) async { 328 | final result = await HitMap.parseFiles(files, 329 | checkIgnoredLines: checkIgnoredLines, 330 | packagesPath: packagesPath, 331 | packagePath: packagePath); 332 | return result.map((key, value) => MapEntry(key, value.lineHits)); 333 | } 334 | 335 | /// Returns a JSON hit map backward-compatible with pre-1.16.0 SDKs. 336 | @Deprecated('Will be removed in 2.0.0') 337 | Map toScriptCoverageJson(Uri scriptUri, Map hitMap) { 338 | return hitmapToJson(HitMap(hitMap), scriptUri); 339 | } 340 | 341 | List _flattenMap(Map map) { 342 | final kvs = []; 343 | map.forEach((k, v) { 344 | kvs.add(k as T); 345 | kvs.add(v as T); 346 | }); 347 | return kvs; 348 | } 349 | 350 | /// Returns a JSON hit map backward-compatible with pre-1.16.0 SDKs. 351 | Map hitmapToJson(HitMap hitmap, Uri scriptUri) => 352 | { 353 | 'source': '$scriptUri', 354 | 'script': { 355 | 'type': '@Script', 356 | 'fixedId': true, 357 | 'id': 358 | 'libraries/1/scripts/${Uri.encodeComponent(scriptUri.toString())}', 359 | 'uri': '$scriptUri', 360 | '_kind': 'library', 361 | }, 362 | 'hits': _flattenMap(hitmap.lineHits), 363 | if (hitmap.funcHits != null) 364 | 'funcHits': _flattenMap(hitmap.funcHits!), 365 | if (hitmap.funcNames != null) 366 | 'funcNames': _flattenMap(hitmap.funcNames!), 367 | if (hitmap.branchHits != null) 368 | 'branchHits': _flattenMap(hitmap.branchHits!), 369 | }; 370 | 371 | /// Sorts the hits array based on the line numbers. 372 | List _sortHits(List hits) { 373 | final structuredHits = <_HitInfo>[]; 374 | for (var i = 0; i < hits.length - 1; i += 2) { 375 | final lineOrLineRange = hits[i]; 376 | final firstLineInRange = lineOrLineRange is int 377 | ? lineOrLineRange 378 | : int.parse((lineOrLineRange as String).split('-')[0]); 379 | structuredHits.add(_HitInfo(firstLineInRange, hits[i], hits[i + 1] as int)); 380 | } 381 | structuredHits.sort((a, b) => a.firstLine.compareTo(b.firstLine)); 382 | return structuredHits 383 | .map((item) => [item.hitRange, item.hitCount]) 384 | .expand((item) => item) 385 | .toList(); 386 | } 387 | -------------------------------------------------------------------------------- /lib/src/resolver.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2014, the Dart project authors. Please see the AUTHORS file 2 | // for details. All rights reserved. Use of this source code is governed by a 3 | // BSD-style license that can be found in the LICENSE file. 4 | 5 | import 'dart:io'; 6 | 7 | import 'package:package_config/package_config.dart'; 8 | import 'package:path/path.dart' as p; 9 | 10 | /// [Resolver] resolves imports with respect to a given environment. 11 | class Resolver { 12 | @Deprecated('Use Resolver.create') 13 | Resolver({this.packagesPath, this.sdkRoot}) 14 | : _packages = packagesPath != null ? _parsePackages(packagesPath) : null, 15 | packagePath = null; 16 | 17 | Resolver._( 18 | {this.packagesPath, 19 | this.packagePath, 20 | this.sdkRoot, 21 | Map? packages}) 22 | : _packages = packages; 23 | 24 | static Future create({ 25 | String? packagesPath, 26 | String? packagePath, 27 | String? sdkRoot, 28 | }) async { 29 | return Resolver._( 30 | packagesPath: packagesPath, 31 | packagePath: packagePath, 32 | sdkRoot: sdkRoot, 33 | packages: packagesPath != null 34 | ? _parsePackages(packagesPath) 35 | : (packagePath != null ? await _parsePackage(packagePath) : null), 36 | ); 37 | } 38 | 39 | final String? packagesPath; 40 | final String? packagePath; 41 | final String? sdkRoot; 42 | final List failed = []; 43 | final Map? _packages; 44 | 45 | /// Returns the absolute path wrt. to the given environment or null, if the 46 | /// import could not be resolved. 47 | String? resolve(String scriptUri) { 48 | final uri = Uri.parse(scriptUri); 49 | if (uri.scheme == 'dart') { 50 | final sdkRoot = this.sdkRoot; 51 | if (sdkRoot == null) { 52 | // No sdk-root given, do not resolve dart: URIs. 53 | return null; 54 | } 55 | String filePath; 56 | if (uri.pathSegments.length > 1) { 57 | var path = uri.pathSegments[0]; 58 | // Drop patch files, since we don't have their source in the compiled 59 | // SDK. 60 | if (path.endsWith('-patch')) { 61 | failed.add('$uri'); 62 | return null; 63 | } 64 | // Canonicalize path. For instance: _collection-dev => _collection_dev. 65 | path = path.replaceAll('-', '_'); 66 | final pathSegments = [ 67 | sdkRoot, 68 | path, 69 | ...uri.pathSegments.sublist(1), 70 | ]; 71 | filePath = p.joinAll(pathSegments); 72 | } else { 73 | // Resolve 'dart:something' to be something/something.dart in the SDK. 74 | final lib = uri.path; 75 | filePath = p.join(sdkRoot, lib, '$lib.dart'); 76 | } 77 | return resolveSymbolicLinks(filePath); 78 | } 79 | if (uri.scheme == 'package') { 80 | final packages = _packages; 81 | if (packages == null) { 82 | return null; 83 | } 84 | 85 | final packageName = uri.pathSegments[0]; 86 | final packageUri = packages[packageName]; 87 | if (packageUri == null) { 88 | failed.add('$uri'); 89 | return null; 90 | } 91 | final packagePath = p.fromUri(packageUri); 92 | final pathInPackage = p.joinAll(uri.pathSegments.sublist(1)); 93 | return resolveSymbolicLinks(p.join(packagePath, pathInPackage)); 94 | } 95 | if (uri.scheme == 'file') { 96 | return resolveSymbolicLinks(p.fromUri(uri)); 97 | } 98 | // We cannot deal with anything else. 99 | failed.add('$uri'); 100 | return null; 101 | } 102 | 103 | /// Returns a canonicalized path, or `null` if the path cannot be resolved. 104 | String? resolveSymbolicLinks(String path) { 105 | final normalizedPath = p.normalize(path); 106 | final type = FileSystemEntity.typeSync(normalizedPath, followLinks: true); 107 | if (type == FileSystemEntityType.notFound) return null; 108 | return File(normalizedPath).resolveSymbolicLinksSync(); 109 | } 110 | 111 | static Map _parsePackages(String packagesPath) { 112 | final content = File(packagesPath).readAsStringSync(); 113 | final packagesUri = p.toUri(packagesPath); 114 | final parsed = 115 | PackageConfig.parseString(content, Uri.base.resolveUri(packagesUri)); 116 | return { 117 | for (var package in parsed.packages) package.name: package.packageUriRoot 118 | }; 119 | } 120 | 121 | static Future?> _parsePackage(String packagePath) async { 122 | final parsed = await findPackageConfig(Directory(packagePath)); 123 | if (parsed == null) return null; 124 | return { 125 | for (var package in parsed.packages) package.name: package.packageUriRoot 126 | }; 127 | } 128 | } 129 | 130 | /// Bazel URI resolver. 131 | class BazelResolver extends Resolver { 132 | /// Creates a Bazel resolver with the specified workspace path, if any. 133 | BazelResolver({this.workspacePath = ''}); 134 | 135 | final String workspacePath; 136 | 137 | /// Returns the absolute path wrt. to the given environment or null, if the 138 | /// import could not be resolved. 139 | @override 140 | String? resolve(String scriptUri) { 141 | final uri = Uri.parse(scriptUri); 142 | if (uri.scheme == 'dart') { 143 | // Ignore the SDK 144 | return null; 145 | } 146 | if (uri.scheme == 'package') { 147 | // TODO(cbracken) belongs in a Bazel package 148 | return _resolveBazelPackage(uri.pathSegments); 149 | } 150 | if (uri.scheme == 'file') { 151 | final runfilesPathSegment = 152 | '.runfiles/$workspacePath'.replaceAll(RegExp(r'/*$'), '/'); 153 | final runfilesPos = uri.path.indexOf(runfilesPathSegment); 154 | if (runfilesPos >= 0) { 155 | final pathStart = runfilesPos + runfilesPathSegment.length; 156 | return uri.path.substring(pathStart); 157 | } 158 | return null; 159 | } 160 | if (uri.scheme == 'https' || uri.scheme == 'http') { 161 | return _extractHttpPath(uri); 162 | } 163 | // We cannot deal with anything else. 164 | failed.add('$uri'); 165 | return null; 166 | } 167 | 168 | String _extractHttpPath(Uri uri) { 169 | final packagesPos = uri.pathSegments.indexOf('packages'); 170 | if (packagesPos >= 0) { 171 | final workspacePath = uri.pathSegments.sublist(packagesPos + 1); 172 | return _resolveBazelPackage(workspacePath); 173 | } 174 | return uri.pathSegments.join('/'); 175 | } 176 | 177 | String _resolveBazelPackage(List pathSegments) { 178 | // TODO(cbracken) belongs in a Bazel package 179 | final packageName = pathSegments[0]; 180 | final pathInPackage = pathSegments.sublist(1).join('/'); 181 | final packagePath = packageName.contains('.') 182 | ? packageName.replaceAll('.', '/') 183 | : 'third_party/dart/$packageName'; 184 | return '$packagePath/lib/$pathInPackage'; 185 | } 186 | } 187 | 188 | /// Loads the lines of imported resources. 189 | class Loader { 190 | final List failed = []; 191 | 192 | /// Loads an imported resource and returns a [Future] with a [List] of lines. 193 | /// Returns `null` if the resource could not be loaded. 194 | Future?> load(String path) async { 195 | try { 196 | // Ensure `readAsLines` runs within the try block so errors are caught. 197 | return await File(path).readAsLines(); 198 | } catch (_) { 199 | failed.add(path); 200 | return null; 201 | } 202 | } 203 | 204 | /// Loads an imported resource and returns a [List] of lines. 205 | /// Returns `null` if the resource could not be loaded. 206 | List? loadSync(String path) { 207 | try { 208 | // Ensure `readAsLinesSync` runs within the try block so errors are 209 | // caught. 210 | return File(path).readAsLinesSync(); 211 | } catch (_) { 212 | failed.add(path); 213 | return null; 214 | } 215 | } 216 | } 217 | -------------------------------------------------------------------------------- /lib/src/run_and_collect.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2015, the Dart project authors. Please see the AUTHORS file 2 | // for details. All rights reserved. Use of this source code is governed by a 3 | // BSD-style license that can be found in the LICENSE file. 4 | 5 | import 'dart:async'; 6 | import 'dart:io'; 7 | 8 | import 'collect.dart'; 9 | import 'util.dart'; 10 | 11 | Future> runAndCollect(String scriptPath, 12 | {List? scriptArgs, 13 | bool checked = false, 14 | bool includeDart = false, 15 | Duration? timeout}) async { 16 | final dartArgs = [ 17 | '--enable-vm-service', 18 | '--pause_isolates_on_exit', 19 | if (checked) '--checked', 20 | scriptPath, 21 | ...?scriptArgs, 22 | ]; 23 | 24 | final process = await Process.start(Platform.executable, dartArgs); 25 | 26 | final serviceUri = await serviceUriFromProcess(process.stdout.lines()); 27 | Map coverage; 28 | try { 29 | coverage = await collect( 30 | serviceUri, 31 | true, 32 | true, 33 | includeDart, 34 | {}, 35 | timeout: timeout, 36 | ); 37 | } finally { 38 | await process.stderr.drain(); 39 | } 40 | final exitStatus = await process.exitCode; 41 | if (exitStatus != 0) { 42 | throw ProcessException( 43 | Platform.executable, 44 | dartArgs, 45 | 'Process failed.', 46 | exitStatus, 47 | ); 48 | } 49 | return coverage; 50 | } 51 | -------------------------------------------------------------------------------- /lib/src/util.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2014, the Dart project authors. Please see the AUTHORS file 2 | // for details. All rights reserved. Use of this source code is governed by a 3 | // BSD-style license that can be found in the LICENSE file. 4 | 5 | import 'dart:async'; 6 | import 'dart:convert'; 7 | import 'dart:io'; 8 | 9 | // TODO(cbracken) make generic 10 | /// Retries the specified function with the specified interval and returns 11 | /// the result on successful completion. 12 | Future retry(Future Function() f, Duration interval, 13 | {Duration? timeout}) async { 14 | var keepGoing = true; 15 | 16 | Future withTimeout(Future Function() f, {Duration? duration}) { 17 | if (duration == null) { 18 | return f(); 19 | } 20 | 21 | return f().timeout(duration, onTimeout: () { 22 | keepGoing = false; 23 | final msg = duration.inSeconds == 0 24 | ? '${duration.inMilliseconds}ms' 25 | : '${duration.inSeconds}s'; 26 | throw StateError('Failed to complete within $msg'); 27 | }); 28 | } 29 | 30 | return withTimeout(() async { 31 | while (keepGoing) { 32 | try { 33 | return await f(); 34 | } catch (_) { 35 | if (keepGoing) { 36 | await Future.delayed(interval); 37 | } 38 | } 39 | } 40 | }, duration: timeout); 41 | } 42 | 43 | /// Scrapes and returns the Dart VM service URI from a string, or null if not 44 | /// found. 45 | /// 46 | /// Potentially useful as a means to extract it from log statements. 47 | Uri? extractVMServiceUri(String str) { 48 | final listeningMessageRegExp = RegExp( 49 | r'(?:Observatory|The Dart VM service is) listening on ((http|//)[a-zA-Z0-9:/=_\-\.\[\]]+)', 50 | ); 51 | final match = listeningMessageRegExp.firstMatch(str); 52 | if (match != null) { 53 | return Uri.parse(match[1]!); 54 | } 55 | return null; 56 | } 57 | 58 | /// Returns an open port by creating a temporary Socket 59 | Future getOpenPort() async { 60 | ServerSocket socket; 61 | 62 | try { 63 | socket = await ServerSocket.bind(InternetAddress.loopbackIPv4, 0); 64 | } catch (_) { 65 | // try again v/ V6 only. Slight possibility that V4 is disabled 66 | socket = 67 | await ServerSocket.bind(InternetAddress.loopbackIPv6, 0, v6Only: true); 68 | } 69 | 70 | try { 71 | return socket.port; 72 | } finally { 73 | await socket.close(); 74 | } 75 | } 76 | 77 | final muliLineIgnoreStart = RegExp(r'//\s*coverage:ignore-start[\w\d\s]*$'); 78 | final muliLineIgnoreEnd = RegExp(r'//\s*coverage:ignore-end[\w\d\s]*$'); 79 | final singleLineIgnore = RegExp(r'//\s*coverage:ignore-line[\w\d\s]*$'); 80 | final ignoreFile = RegExp(r'//\s*coverage:ignore-file[\w\d\s]*$'); 81 | 82 | /// Return list containing inclusive range of lines to be ignored by coverage. 83 | /// If there is a error in balancing the statements it will throw a 84 | /// [FormatException], 85 | /// unless `coverage:ignore-file` is found. 86 | /// Return [0, lines.length] if the whole file is ignored. 87 | /// 88 | /// ``` 89 | /// 1. final str = ''; // coverage:ignore-line 90 | /// 2. final str = ''; 91 | /// 3. final str = ''; // coverage:ignore-start 92 | /// 4. final str = ''; 93 | /// 5. final str = ''; // coverage:ignore-end 94 | /// ``` 95 | /// 96 | /// Returns 97 | /// ``` 98 | /// [ 99 | /// [1,1], 100 | /// [3,5], 101 | /// ] 102 | /// ``` 103 | /// 104 | List> getIgnoredLines(String filePath, List? lines) { 105 | final ignoredLines = >[]; 106 | if (lines == null) return ignoredLines; 107 | 108 | final allLines = [ 109 | [0, lines.length] 110 | ]; 111 | 112 | FormatException? err; 113 | var i = 0; 114 | while (i < lines.length) { 115 | if (lines[i].contains(ignoreFile)) return allLines; 116 | 117 | if (lines[i].contains(muliLineIgnoreEnd)) { 118 | err ??= FormatException( 119 | 'unmatched coverage:ignore-end found at $filePath:${i + 1}', 120 | ); 121 | } 122 | 123 | if (lines[i].contains(singleLineIgnore)) ignoredLines.add([i + 1, i + 1]); 124 | 125 | if (lines[i].contains(muliLineIgnoreStart)) { 126 | final start = i; 127 | var isUnmatched = true; 128 | ++i; 129 | while (i < lines.length) { 130 | if (lines[i].contains(ignoreFile)) return allLines; 131 | if (lines[i].contains(muliLineIgnoreStart)) { 132 | err ??= FormatException( 133 | 'coverage:ignore-start found at $filePath:${i + 1}' 134 | ' before previous coverage:ignore-start ended', 135 | ); 136 | break; 137 | } 138 | 139 | if (lines[i].contains(muliLineIgnoreEnd)) { 140 | ignoredLines.add([start + 1, i + 1]); 141 | isUnmatched = false; 142 | break; 143 | } 144 | ++i; 145 | } 146 | 147 | if (isUnmatched) { 148 | err ??= FormatException( 149 | 'coverage:ignore-start found at $filePath:${start + 1}' 150 | ' has no matching coverage:ignore-end', 151 | ); 152 | } 153 | } 154 | ++i; 155 | } 156 | 157 | if (err == null) { 158 | return ignoredLines; 159 | } 160 | 161 | throw err; 162 | } 163 | 164 | extension StandardOutExtension on Stream> { 165 | Stream lines() => 166 | transform(const SystemEncoding().decoder).transform(const LineSplitter()); 167 | } 168 | 169 | Future serviceUriFromProcess(Stream procStdout) { 170 | // Capture the VM service URI. 171 | final serviceUriCompleter = Completer(); 172 | procStdout.listen((line) { 173 | if (!serviceUriCompleter.isCompleted) { 174 | final serviceUri = extractVMServiceUri(line); 175 | if (serviceUri != null) { 176 | serviceUriCompleter.complete(serviceUri); 177 | } 178 | } 179 | }); 180 | return serviceUriCompleter.future; 181 | } 182 | -------------------------------------------------------------------------------- /pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: coverage 2 | version: 1.9.1-wip 3 | description: Coverage data manipulation and formatting 4 | repository: https://github.com/dart-lang/coverage 5 | 6 | environment: 7 | sdk: ^3.4.0 8 | 9 | dependencies: 10 | args: ^2.0.0 11 | glob: ^2.1.2 12 | logging: ^1.0.0 13 | package_config: ^2.0.0 14 | path: ^1.8.0 15 | source_maps: ^0.10.10 16 | stack_trace: ^1.10.0 17 | vm_service: '>=12.0.0 <15.0.0' 18 | 19 | dev_dependencies: 20 | benchmark_harness: ^2.2.0 21 | build_runner: ^2.3.1 22 | dart_flutter_team_lints: ^3.0.0 23 | mockito: ^5.4.1 24 | test: ^1.24.7 25 | test_descriptor: ^2.0.0 26 | test_process: ^2.0.0 27 | 28 | executables: 29 | collect_coverage: 30 | format_coverage: 31 | test_with_coverage: 32 | -------------------------------------------------------------------------------- /test/README.md: -------------------------------------------------------------------------------- 1 | ## Regenerating mocks 2 | 3 | Some of the tests use a mock VmService that is automatically generated by 4 | Mockito. If the VmService changes, run this command in the root directory of 5 | this repo to regenerate that mock: 6 | 7 | ```bash 8 | dart run build_runner build 9 | ``` 10 | -------------------------------------------------------------------------------- /test/chrome_test.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020, the Dart project authors. Please see the AUTHORS file 2 | // for details. All rights reserved. Use of this source code is governed by a 3 | // BSD-style license that can be found in the LICENSE file. 4 | 5 | // TODO(#388): Fix and re-enable this test. 6 | @TestOn('!windows') 7 | library; 8 | 9 | import 'dart:convert'; 10 | import 'dart:io'; 11 | 12 | import 'package:coverage/coverage.dart'; 13 | import 'package:test/test.dart'; 14 | 15 | // The scriptId for the main_test.js in the sample report. 16 | const String mainScriptId = '31'; 17 | 18 | Future sourceMapProvider(String scriptId) async { 19 | if (scriptId != mainScriptId) { 20 | return 'something invalid!'; 21 | } 22 | return File('test/test_files/main_test.js.map').readAsString(); 23 | } 24 | 25 | Future sourceProvider(String scriptId) async { 26 | if (scriptId != mainScriptId) return null; 27 | return File('test/test_files/main_test.js').readAsString(); 28 | } 29 | 30 | Future sourceUriProvider(String sourceUrl, String scriptId) async => 31 | Uri.parse(sourceUrl); 32 | 33 | void main() { 34 | test('reports correctly', () async { 35 | final preciseCoverage = json.decode( 36 | await File('test/test_files/chrome_precise_report.txt') 37 | .readAsString()) as List; 38 | 39 | final report = await parseChromeCoverage( 40 | preciseCoverage.cast(), 41 | sourceProvider, 42 | sourceMapProvider, 43 | sourceUriProvider, 44 | ); 45 | 46 | final sourceReport = 47 | (report['coverage'] as List>).firstWhere( 48 | (Map report) => 49 | report['source'].toString().contains('main_test.dart'), 50 | ); 51 | 52 | final expectedHits = { 53 | 7: 1, 54 | 11: 1, 55 | 13: 1, 56 | 14: 1, 57 | 17: 0, 58 | 19: 0, 59 | 20: 0, 60 | 22: 1, 61 | 23: 1, 62 | 24: 1, 63 | 25: 1, 64 | 28: 1, 65 | 30: 0, 66 | 32: 1, 67 | 34: 1, 68 | 35: 1, 69 | 36: 1, 70 | }; 71 | 72 | final hitMap = sourceReport['hits'] as List; 73 | expect(hitMap.length, equals(expectedHits.keys.length * 2)); 74 | for (var i = 0; i < hitMap.length; i += 2) { 75 | expect(expectedHits[hitMap[i]], equals(hitMap[i + 1])); 76 | } 77 | }); 78 | } 79 | -------------------------------------------------------------------------------- /test/collect_coverage_api_test.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2015, the Dart project authors. Please see the AUTHORS file 2 | // for details. All rights reserved. Use of this source code is governed by a 3 | // BSD-style license that can be found in the LICENSE file. 4 | 5 | import 'dart:async'; 6 | 7 | import 'package:coverage/coverage.dart'; 8 | import 'package:coverage/src/util.dart'; 9 | import 'package:path/path.dart' as p; 10 | import 'package:test/test.dart'; 11 | 12 | import 'test_util.dart'; 13 | 14 | final _isolateLibPath = p.join('test', 'test_files', 'test_app_isolate.dart'); 15 | 16 | final _sampleAppFileUri = p.toUri(p.absolute(testAppPath)).toString(); 17 | final _isolateLibFileUri = p.toUri(p.absolute(_isolateLibPath)).toString(); 18 | 19 | void main() { 20 | test('collect_coverage_api', () async { 21 | final coverage = coverageDataFromJson(await _collectCoverage()); 22 | expect(coverage, isNotEmpty); 23 | 24 | final sources = coverage.sources(); 25 | 26 | for (var sampleCoverageData in sources[_sampleAppFileUri]!) { 27 | expect(sampleCoverageData['hits'], isNotEmpty); 28 | } 29 | 30 | for (var sampleCoverageData in sources[_isolateLibFileUri]!) { 31 | expect(sampleCoverageData['hits'], isNotEmpty); 32 | } 33 | }); 34 | 35 | test('collect_coverage_api with scoped output', () async { 36 | final coverage = coverageDataFromJson( 37 | await _collectCoverage(scopedOutput: {}..add('coverage')), 38 | ); 39 | expect(coverage, isNotEmpty); 40 | 41 | final sources = coverage.sources(); 42 | 43 | for (var key in sources.keys) { 44 | final uri = Uri.parse(key); 45 | expect(uri.path.startsWith('coverage'), isTrue); 46 | } 47 | }); 48 | 49 | test('collect_coverage_api with isolateIds', () async { 50 | final coverage = 51 | coverageDataFromJson(await _collectCoverage(isolateIds: true)); 52 | expect(coverage, isEmpty); 53 | }); 54 | 55 | test('collect_coverage_api with function coverage', () async { 56 | final coverage = 57 | coverageDataFromJson(await _collectCoverage(functionCoverage: true)); 58 | expect(coverage, isNotEmpty); 59 | 60 | final sources = coverage.sources(); 61 | 62 | final functionInfo = functionInfoFromSources(sources); 63 | 64 | expect( 65 | functionInfo[_sampleAppFileUri]!, 66 | { 67 | 'main': 1, 68 | 'usedMethod': 1, 69 | 'unusedMethod': 0, 70 | }, 71 | ); 72 | 73 | expect( 74 | functionInfo[_isolateLibFileUri]!, 75 | { 76 | 'BarClass.BarClass': 1, 77 | 'fooAsync': 1, 78 | 'fooSync': 1, 79 | 'isolateTask': 1, 80 | 'BarClass.baz': 1 81 | }, 82 | ); 83 | }); 84 | 85 | test('collect_coverage_api with branch coverage', () async { 86 | final coverage = 87 | coverageDataFromJson(await _collectCoverage(branchCoverage: true)); 88 | expect(coverage, isNotEmpty); 89 | 90 | final sources = coverage.sources(); 91 | 92 | // Dart VM versions before 2.17 don't support branch coverage. 93 | expect(sources[_sampleAppFileUri], 94 | everyElement(containsPair('branchHits', isNotEmpty))); 95 | expect(sources[_isolateLibFileUri], 96 | everyElement(containsPair('branchHits', isNotEmpty))); 97 | }); 98 | 99 | test('collect_coverage_api with coverableLineCache', () async { 100 | final coverableLineCache = >{}; 101 | final coverage = 102 | await _collectCoverage(coverableLineCache: coverableLineCache); 103 | final result = await HitMap.parseJson( 104 | coverage['coverage'] as List>); 105 | 106 | expect(coverableLineCache, contains(_sampleAppFileUri)); 107 | expect(coverableLineCache, contains(_isolateLibFileUri)); 108 | 109 | // Expect that we have some missed lines. 110 | expect(result[_sampleAppFileUri]!.lineHits.containsValue(0), isTrue); 111 | expect(result[_isolateLibFileUri]!.lineHits.containsValue(0), isTrue); 112 | 113 | // Clear _sampleAppFileUri's cache entry, then gather coverage again. We're 114 | // doing this to verify that force compilation is disabled for these 115 | // libraries. The result should be that _isolateLibFileUri should be the 116 | // same, but _sampleAppFileUri should be missing all its missed lines. 117 | coverableLineCache[_sampleAppFileUri] = {}; 118 | final coverage2 = 119 | await _collectCoverage(coverableLineCache: coverableLineCache); 120 | final result2 = await HitMap.parseJson( 121 | coverage2['coverage'] as List>); 122 | 123 | // _isolateLibFileUri still has missed lines, but _sampleAppFileUri doesn't. 124 | expect(result2[_sampleAppFileUri]!.lineHits.containsValue(0), isFalse); 125 | expect(result2[_isolateLibFileUri]!.lineHits.containsValue(0), isTrue); 126 | 127 | // _isolateLibFileUri is the same. _sampleAppFileUri is the same, but 128 | // without all its missed lines. 129 | expect(result2[_isolateLibFileUri]!.lineHits, 130 | result[_isolateLibFileUri]!.lineHits); 131 | result[_sampleAppFileUri]!.lineHits.removeWhere((line, hits) => hits == 0); 132 | expect(result2[_sampleAppFileUri]!.lineHits, 133 | result[_sampleAppFileUri]!.lineHits); 134 | }, skip: !platformVersionCheck(3, 2)); 135 | } 136 | 137 | Future> _collectCoverage( 138 | {Set scopedOutput = const {}, 139 | bool isolateIds = false, 140 | bool functionCoverage = false, 141 | bool branchCoverage = false, 142 | Map>? coverableLineCache}) async { 143 | final openPort = await getOpenPort(); 144 | 145 | // run the sample app, with the right flags 146 | final sampleProcess = await runTestApp(openPort); 147 | 148 | final serviceUri = await serviceUriFromProcess(sampleProcess.stdoutStream()); 149 | final isolateIdSet = isolateIds ? {} : null; 150 | 151 | return collect(serviceUri, true, true, false, scopedOutput, 152 | timeout: timeout, 153 | isolateIds: isolateIdSet, 154 | functionCoverage: functionCoverage, 155 | branchCoverage: branchCoverage, 156 | coverableLineCache: coverableLineCache); 157 | } 158 | -------------------------------------------------------------------------------- /test/collect_coverage_mock_test.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2022, the Dart project authors. Please see the AUTHORS file 2 | // for details. All rights reserved. Use of this source code is governed by a 3 | // BSD-style license that can be found in the LICENSE file. 4 | 5 | import 'package:coverage/coverage.dart'; 6 | import 'package:mockito/annotations.dart'; 7 | import 'package:mockito/mockito.dart'; 8 | import 'package:test/test.dart'; 9 | import 'package:vm_service/vm_service.dart'; 10 | 11 | import 'collect_coverage_mock_test.mocks.dart'; 12 | 13 | @GenerateMocks([VmService]) 14 | SourceReportRange _range(int scriptIndex, SourceReportCoverage coverage) => 15 | SourceReportRange(scriptIndex: scriptIndex, coverage: coverage); 16 | 17 | IsolateRef _isoRef(String id, String isoGroupId) => 18 | IsolateRef(id: id, isolateGroupId: isoGroupId); 19 | 20 | IsolateGroupRef _isoGroupRef(String id) => IsolateGroupRef(id: id); 21 | 22 | IsolateGroup _isoGroup(String id, List isolates) => 23 | IsolateGroup(id: id, isolates: isolates); 24 | 25 | class FakeSentinelException implements SentinelException { 26 | @override 27 | dynamic noSuchMethod(Invocation invocation) {} 28 | } 29 | 30 | MockVmService _mockService( 31 | int majorVersion, 32 | int minorVersion, { 33 | Map> isolateGroups = const { 34 | 'isolateGroup': ['isolate'], 35 | }, 36 | }) { 37 | final service = MockVmService(); 38 | final isoRefs = []; 39 | final isoGroupRefs = []; 40 | final isoGroups = []; 41 | for (final group in isolateGroups.entries) { 42 | isoGroupRefs.add(_isoGroupRef(group.key)); 43 | final isosOfGroup = []; 44 | for (final isoId in group.value) { 45 | isosOfGroup.add(_isoRef(isoId, group.key)); 46 | } 47 | isoGroups.add(_isoGroup(group.key, isosOfGroup)); 48 | isoRefs.addAll(isosOfGroup); 49 | } 50 | when(service.getVM()).thenAnswer( 51 | (_) async => VM(isolates: isoRefs, isolateGroups: isoGroupRefs)); 52 | for (final group in isoGroups) { 53 | when(service.getIsolateGroup(group.id)).thenAnswer((_) async => group); 54 | } 55 | when(service.getVersion()).thenAnswer( 56 | (_) async => Version(major: majorVersion, minor: minorVersion)); 57 | return service; 58 | } 59 | 60 | void main() { 61 | group('Mock VM Service', () { 62 | test('Collect coverage', () async { 63 | final service = _mockService(4, 13); 64 | when(service.getSourceReport( 65 | 'isolate', 66 | ['Coverage'], 67 | forceCompile: true, 68 | reportLines: true, 69 | )).thenAnswer((_) async => SourceReport( 70 | ranges: [ 71 | _range( 72 | 0, 73 | SourceReportCoverage( 74 | hits: [12], 75 | misses: [47], 76 | ), 77 | ), 78 | _range( 79 | 1, 80 | SourceReportCoverage( 81 | hits: [95], 82 | misses: [52], 83 | ), 84 | ), 85 | ], 86 | scripts: [ 87 | ScriptRef( 88 | uri: 'package:foo/foo.dart', 89 | id: 'foo', 90 | ), 91 | ScriptRef( 92 | uri: 'package:bar/bar.dart', 93 | id: 'bar', 94 | ), 95 | ], 96 | )); 97 | 98 | final jsonResult = await collect(Uri(), false, false, false, null, 99 | serviceOverrideForTesting: service); 100 | final result = await HitMap.parseJson( 101 | jsonResult['coverage'] as List>); 102 | 103 | expect(result.length, 2); 104 | expect(result['package:foo/foo.dart']?.lineHits, {12: 1, 47: 0}); 105 | expect(result['package:bar/bar.dart']?.lineHits, {95: 1, 52: 0}); 106 | }); 107 | 108 | test('Collect coverage, scoped output', () async { 109 | final service = _mockService(4, 13); 110 | when(service.getSourceReport( 111 | 'isolate', 112 | ['Coverage'], 113 | forceCompile: true, 114 | reportLines: true, 115 | libraryFilters: ['package:foo/'], 116 | )).thenAnswer((_) async => SourceReport( 117 | ranges: [ 118 | _range( 119 | 0, 120 | SourceReportCoverage( 121 | hits: [12], 122 | misses: [47], 123 | ), 124 | ), 125 | ], 126 | scripts: [ 127 | ScriptRef( 128 | uri: 'package:foo/foo.dart', 129 | id: 'foo', 130 | ), 131 | ], 132 | )); 133 | 134 | final jsonResult = await collect(Uri(), false, false, false, {'foo'}, 135 | serviceOverrideForTesting: service); 136 | final result = await HitMap.parseJson( 137 | jsonResult['coverage'] as List>); 138 | 139 | expect(result.length, 1); 140 | expect(result['package:foo/foo.dart']?.lineHits, {12: 1, 47: 0}); 141 | }); 142 | 143 | test('Collect coverage, fast isolate group deduping', () async { 144 | final service = _mockService(4, 13, isolateGroups: { 145 | 'isolateGroupA': ['isolate1', 'isolate2'], 146 | 'isolateGroupB': ['isolate3'], 147 | }); 148 | when(service.getSourceReport( 149 | 'isolate1', 150 | ['Coverage'], 151 | forceCompile: true, 152 | reportLines: true, 153 | )).thenAnswer((_) async => SourceReport( 154 | ranges: [ 155 | _range( 156 | 0, 157 | SourceReportCoverage( 158 | hits: [12], 159 | misses: [47], 160 | ), 161 | ), 162 | _range( 163 | 1, 164 | SourceReportCoverage( 165 | hits: [95], 166 | misses: [52], 167 | ), 168 | ), 169 | ], 170 | scripts: [ 171 | ScriptRef( 172 | uri: 'package:foo/foo.dart', 173 | id: 'foo', 174 | ), 175 | ScriptRef( 176 | uri: 'package:bar/bar.dart', 177 | id: 'bar', 178 | ), 179 | ], 180 | )); 181 | when(service.getSourceReport( 182 | 'isolate3', 183 | ['Coverage'], 184 | forceCompile: true, 185 | reportLines: true, 186 | )).thenAnswer((_) async => SourceReport( 187 | ranges: [ 188 | _range( 189 | 0, 190 | SourceReportCoverage( 191 | hits: [34], 192 | misses: [61], 193 | ), 194 | ), 195 | ], 196 | scripts: [ 197 | ScriptRef( 198 | uri: 'package:baz/baz.dart', 199 | id: 'baz', 200 | ), 201 | ], 202 | )); 203 | 204 | final jsonResult = await collect(Uri(), false, false, false, null, 205 | serviceOverrideForTesting: service); 206 | final result = await HitMap.parseJson( 207 | jsonResult['coverage'] as List>); 208 | 209 | expect(result.length, 3); 210 | expect(result['package:foo/foo.dart']?.lineHits, {12: 1, 47: 0}); 211 | expect(result['package:bar/bar.dart']?.lineHits, {95: 1, 52: 0}); 212 | expect(result['package:baz/baz.dart']?.lineHits, {34: 1, 61: 0}); 213 | verifyNever(service.getSourceReport('isolate2', ['Coverage'], 214 | forceCompile: true, reportLines: true)); 215 | verifyNever(service.getIsolateGroup('isolateGroupA')); 216 | verifyNever(service.getIsolateGroup('isolateGroupB')); 217 | }); 218 | 219 | test( 220 | 'Collect coverage, no scoped output, ' 221 | 'handles SentinelException from getSourceReport', () async { 222 | final service = _mockService(4, 13); 223 | when(service.getSourceReport( 224 | 'isolate', 225 | ['Coverage'], 226 | forceCompile: true, 227 | reportLines: true, 228 | )).thenThrow(FakeSentinelException()); 229 | 230 | final jsonResult = await collect(Uri(), false, false, false, null, 231 | serviceOverrideForTesting: service); 232 | final result = await HitMap.parseJson( 233 | jsonResult['coverage'] as List>); 234 | 235 | expect(result.length, 0); 236 | }); 237 | 238 | test('Collect coverage, coverableLineCache', () async { 239 | // Expect that on the first getSourceReport call, librariesAlreadyCompiled 240 | // is empty. 241 | final service = _mockService(4, 13); 242 | when(service.getSourceReport( 243 | 'isolate', 244 | ['Coverage'], 245 | forceCompile: true, 246 | reportLines: true, 247 | librariesAlreadyCompiled: [], 248 | )).thenAnswer((_) async => SourceReport( 249 | ranges: [ 250 | _range( 251 | 0, 252 | SourceReportCoverage( 253 | hits: [12], 254 | misses: [47], 255 | ), 256 | ), 257 | _range( 258 | 1, 259 | SourceReportCoverage( 260 | hits: [95], 261 | misses: [52], 262 | ), 263 | ), 264 | ], 265 | scripts: [ 266 | ScriptRef( 267 | uri: 'package:foo/foo.dart', 268 | id: 'foo', 269 | ), 270 | ScriptRef( 271 | uri: 'package:bar/bar.dart', 272 | id: 'bar', 273 | ), 274 | ], 275 | )); 276 | 277 | final coverableLineCache = >{}; 278 | final jsonResult = await collect(Uri(), false, false, false, null, 279 | coverableLineCache: coverableLineCache, 280 | serviceOverrideForTesting: service); 281 | final result = await HitMap.parseJson( 282 | jsonResult['coverage'] as List>); 283 | 284 | expect(result.length, 2); 285 | expect(result['package:foo/foo.dart']?.lineHits, {12: 1, 47: 0}); 286 | expect(result['package:bar/bar.dart']?.lineHits, {95: 1, 52: 0}); 287 | 288 | // The coverableLineCache should now be filled with all the lines that 289 | // were hit or missed. 290 | expect(coverableLineCache, { 291 | 'package:foo/foo.dart': {12, 47}, 292 | 'package:bar/bar.dart': {95, 52}, 293 | }); 294 | 295 | // The second getSourceReport call should now list all the libraries we've 296 | // seen. The response won't contain any misses for these libraries, 297 | // because they won't be force compiled. We'll also return a 3rd library, 298 | // which will contain misses, as it hasn't been compiled yet. 299 | when(service.getSourceReport( 300 | 'isolate', 301 | ['Coverage'], 302 | forceCompile: true, 303 | reportLines: true, 304 | librariesAlreadyCompiled: [ 305 | 'package:foo/foo.dart', 306 | 'package:bar/bar.dart' 307 | ], 308 | )).thenAnswer((_) async => SourceReport( 309 | ranges: [ 310 | _range( 311 | 0, 312 | SourceReportCoverage( 313 | hits: [47], 314 | ), 315 | ), 316 | _range( 317 | 1, 318 | SourceReportCoverage( 319 | hits: [95], 320 | ), 321 | ), 322 | _range( 323 | 2, 324 | SourceReportCoverage( 325 | hits: [36], 326 | misses: [81], 327 | ), 328 | ), 329 | ], 330 | scripts: [ 331 | ScriptRef( 332 | uri: 'package:foo/foo.dart', 333 | id: 'foo', 334 | ), 335 | ScriptRef( 336 | uri: 'package:bar/bar.dart', 337 | id: 'bar', 338 | ), 339 | ScriptRef( 340 | uri: 'package:baz/baz.dart', 341 | id: 'baz', 342 | ), 343 | ], 344 | )); 345 | 346 | final jsonResult2 = await collect(Uri(), false, false, false, null, 347 | coverableLineCache: coverableLineCache, 348 | serviceOverrideForTesting: service); 349 | final result2 = await HitMap.parseJson( 350 | jsonResult2['coverage'] as List>); 351 | 352 | // The missed lines still appear in foo and bar, even though they weren't 353 | // returned in the response. They were read from the cache. 354 | expect(result2.length, 3); 355 | expect(result2['package:foo/foo.dart']?.lineHits, {12: 0, 47: 1}); 356 | expect(result2['package:bar/bar.dart']?.lineHits, {95: 1, 52: 0}); 357 | expect(result2['package:baz/baz.dart']?.lineHits, {36: 1, 81: 0}); 358 | 359 | // The coverableLineCache should now also contain the baz library. 360 | expect(coverableLineCache, { 361 | 'package:foo/foo.dart': {12, 47}, 362 | 'package:bar/bar.dart': {95, 52}, 363 | 'package:baz/baz.dart': {36, 81}, 364 | }); 365 | }); 366 | 367 | test( 368 | 'Collect coverage, scoped output, ' 369 | 'handles SourceReports that contain unfiltered ranges', () async { 370 | // Regression test for https://github.com/dart-lang/coverage/issues/495 371 | final service = _mockService(4, 13); 372 | when(service.getSourceReport( 373 | 'isolate', 374 | ['Coverage'], 375 | forceCompile: true, 376 | reportLines: true, 377 | libraryFilters: ['package:foo/'], 378 | )).thenAnswer((_) async => SourceReport( 379 | ranges: [ 380 | _range( 381 | 0, 382 | SourceReportCoverage( 383 | hits: [12], 384 | misses: [47], 385 | ), 386 | ), 387 | _range( 388 | 1, 389 | SourceReportCoverage( 390 | hits: [86], 391 | misses: [91], 392 | ), 393 | ), 394 | ], 395 | scripts: [ 396 | ScriptRef( 397 | uri: 'package:foo/foo.dart', 398 | id: 'foo', 399 | ), 400 | ScriptRef( 401 | uri: 'package:bar/bar.dart', 402 | id: 'bar', 403 | ), 404 | ], 405 | )); 406 | 407 | final jsonResult = await collect(Uri(), false, false, false, {'foo'}, 408 | serviceOverrideForTesting: service); 409 | final result = await HitMap.parseJson( 410 | jsonResult['coverage'] as List>); 411 | 412 | expect(result.length, 1); 413 | expect(result['package:foo/foo.dart']?.lineHits, {12: 1, 47: 0}); 414 | }); 415 | }); 416 | } 417 | -------------------------------------------------------------------------------- /test/collect_coverage_test.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2015, the Dart project authors. Please see the AUTHORS file 2 | // for details. All rights reserved. Use of this source code is governed by a 3 | // BSD-style license that can be found in the LICENSE file. 4 | 5 | @Retry(3) 6 | library; 7 | 8 | import 'dart:async'; 9 | import 'dart:convert' show json; 10 | import 'dart:io'; 11 | 12 | import 'package:coverage/coverage.dart'; 13 | import 'package:coverage/src/util.dart'; 14 | import 'package:path/path.dart' as p; 15 | import 'package:test/test.dart'; 16 | import 'package:test_process/test_process.dart'; 17 | 18 | import 'test_util.dart'; 19 | 20 | final _isolateLibPath = p.join('test', 'test_files', 'test_app_isolate.dart'); 21 | final _collectAppPath = p.join('bin', 'collect_coverage.dart'); 22 | 23 | final _sampleAppFileUri = p.toUri(p.absolute(testAppPath)).toString(); 24 | final _isolateLibFileUri = p.toUri(p.absolute(_isolateLibPath)).toString(); 25 | 26 | void main() { 27 | test('collect_coverage', () async { 28 | final resultString = await _getCoverageResult(); 29 | 30 | // analyze the output json 31 | final coverage = 32 | coverageDataFromJson(json.decode(resultString) as Map); 33 | 34 | expect(coverage, isNotEmpty); 35 | 36 | final sources = coverage.sources(); 37 | 38 | for (var sampleCoverageData in sources[_sampleAppFileUri]!) { 39 | expect(sampleCoverageData['hits'], isNotNull); 40 | } 41 | 42 | for (var sampleCoverageData in sources[_isolateLibFileUri]!) { 43 | expect(sampleCoverageData['hits'], isNotEmpty); 44 | } 45 | }); 46 | 47 | test('createHitmap returns a sorted hitmap', () async { 48 | final coverage = [ 49 | { 50 | 'source': 'foo', 51 | 'script': '{type: @Script, fixedId: true, ' 52 | 'id: bar.dart, uri: bar.dart, _kind: library}', 53 | 'hits': [ 54 | 45, 55 | 1, 56 | 46, 57 | 1, 58 | 49, 59 | 0, 60 | 50, 61 | 0, 62 | 15, 63 | 1, 64 | 16, 65 | 2, 66 | 17, 67 | 2, 68 | ] 69 | } 70 | ]; 71 | // ignore: deprecated_member_use_from_same_package 72 | final hitMap = await createHitmap( 73 | coverage.cast>(), 74 | ); 75 | final expectedHits = {15: 1, 16: 2, 17: 2, 45: 1, 46: 1, 49: 0, 50: 0}; 76 | expect(hitMap['foo'], expectedHits); 77 | }); 78 | 79 | test('HitMap.parseJson returns a sorted hitmap', () async { 80 | final coverage = [ 81 | { 82 | 'source': 'foo', 83 | 'script': '{type: @Script, fixedId: true, ' 84 | 'id: bar.dart, uri: bar.dart, _kind: library}', 85 | 'hits': [ 86 | 45, 87 | 1, 88 | 46, 89 | 1, 90 | 49, 91 | 0, 92 | 50, 93 | 0, 94 | 15, 95 | 1, 96 | 16, 97 | 2, 98 | 17, 99 | 2, 100 | ] 101 | } 102 | ]; 103 | final hitMap = await HitMap.parseJson( 104 | coverage.cast>(), 105 | ); 106 | final expectedHits = {15: 1, 16: 2, 17: 2, 45: 1, 46: 1, 49: 0, 50: 0}; 107 | expect(hitMap['foo']?.lineHits, expectedHits); 108 | }); 109 | 110 | test('HitMap.parseJson', () async { 111 | final resultString = await _collectCoverage(true, true); 112 | final jsonResult = json.decode(resultString) as Map; 113 | final coverage = jsonResult['coverage'] as List; 114 | final hitMap = await HitMap.parseJson( 115 | coverage.cast>(), 116 | ); 117 | expect(hitMap, contains(_sampleAppFileUri)); 118 | 119 | final isolateFile = hitMap[_isolateLibFileUri]; 120 | final expectedHits = { 121 | 11: 1, 122 | 12: 1, 123 | 13: 1, 124 | 15: 0, 125 | 19: 1, 126 | 23: 1, 127 | 24: 2, 128 | 28: 1, 129 | 29: 1, 130 | 30: 1, 131 | 32: 0, 132 | 38: 1, 133 | 39: 1, 134 | 41: 1, 135 | 42: 3, 136 | 43: 1, 137 | 44: 3, 138 | 45: 1, 139 | 48: 1, 140 | 49: 1, 141 | 51: 1, 142 | 54: 1, 143 | 55: 1, 144 | 56: 1, 145 | 59: 1, 146 | 60: 1, 147 | 62: 1, 148 | 63: 1, 149 | 64: 1, 150 | 66: 1, 151 | 67: 1, 152 | 68: 1 153 | }; 154 | expect(isolateFile?.lineHits, expectedHits); 155 | expect(isolateFile?.funcHits, {11: 1, 19: 1, 23: 1, 28: 1, 38: 1}); 156 | expect(isolateFile?.funcNames, { 157 | 11: 'fooSync', 158 | 19: 'BarClass.BarClass', 159 | 23: 'BarClass.baz', 160 | 28: 'fooAsync', 161 | 38: 'isolateTask' 162 | }); 163 | expect( 164 | isolateFile?.branchHits, 165 | {11: 1, 12: 1, 15: 0, 19: 1, 23: 1, 28: 1, 29: 1, 32: 0, 38: 1, 42: 1}, 166 | ); 167 | }); 168 | 169 | test('HitMap.parseJson, old VM without branch coverage', () async { 170 | final resultString = await _collectCoverage(true, true); 171 | final jsonResult = json.decode(resultString) as Map; 172 | final coverage = jsonResult['coverage'] as List; 173 | final hitMap = await HitMap.parseJson( 174 | coverage.cast>(), 175 | ); 176 | expect(hitMap, contains(_sampleAppFileUri)); 177 | 178 | final isolateFile = hitMap[_isolateLibFileUri]; 179 | final expectedHits = { 180 | 11: 1, 181 | 12: 1, 182 | 13: 1, 183 | 15: 0, 184 | 19: 1, 185 | 23: 1, 186 | 24: 2, 187 | 28: 1, 188 | 29: 1, 189 | 30: 1, 190 | 32: 0, 191 | 38: 1, 192 | 39: 1, 193 | 41: 1, 194 | 42: 3, 195 | 43: 1, 196 | 44: 3, 197 | 45: 1, 198 | 48: 1, 199 | 49: 1, 200 | 51: 1, 201 | 54: 1, 202 | 55: 1, 203 | 56: 1, 204 | 59: 1, 205 | 60: 1, 206 | 62: 1, 207 | 63: 1, 208 | 64: 1, 209 | 66: 1, 210 | 67: 1, 211 | 68: 1 212 | }; 213 | expect(isolateFile?.lineHits, expectedHits); 214 | expect(isolateFile?.funcHits, {11: 1, 19: 1, 23: 1, 28: 1, 38: 1}); 215 | expect(isolateFile?.funcNames, { 216 | 11: 'fooSync', 217 | 19: 'BarClass.BarClass', 218 | 23: 'BarClass.baz', 219 | 28: 'fooAsync', 220 | 38: 'isolateTask' 221 | }); 222 | }); 223 | 224 | test('parseCoverage', () async { 225 | final tempDir = await Directory.systemTemp.createTemp('coverage.test.'); 226 | 227 | try { 228 | final outputFile = File(p.join(tempDir.path, 'coverage.json')); 229 | 230 | final coverageResults = await _getCoverageResult(); 231 | await outputFile.writeAsString(coverageResults, flush: true); 232 | 233 | // ignore: deprecated_member_use_from_same_package 234 | final parsedResult = await parseCoverage([outputFile], 1); 235 | 236 | expect(parsedResult, contains(_sampleAppFileUri)); 237 | expect(parsedResult, contains(_isolateLibFileUri)); 238 | } finally { 239 | await tempDir.delete(recursive: true); 240 | } 241 | }); 242 | 243 | test('HitMap.parseFiles', () async { 244 | final tempDir = await Directory.systemTemp.createTemp('coverage.test.'); 245 | 246 | try { 247 | final outputFile = File(p.join(tempDir.path, 'coverage.json')); 248 | 249 | final coverageResults = await _getCoverageResult(); 250 | await outputFile.writeAsString(coverageResults, flush: true); 251 | 252 | final parsedResult = await HitMap.parseFiles([outputFile]); 253 | 254 | expect(parsedResult, contains(_sampleAppFileUri)); 255 | expect(parsedResult, contains(_isolateLibFileUri)); 256 | } finally { 257 | await tempDir.delete(recursive: true); 258 | } 259 | }); 260 | 261 | test('HitMap.parseFiles with packagesPath and checkIgnoredLines', () async { 262 | final tempDir = await Directory.systemTemp.createTemp('coverage.test.'); 263 | 264 | try { 265 | final outputFile = File(p.join(tempDir.path, 'coverage.json')); 266 | 267 | final coverageResults = await _getCoverageResult(); 268 | await outputFile.writeAsString(coverageResults, flush: true); 269 | 270 | final parsedResult = await HitMap.parseFiles([outputFile], 271 | packagePath: '.', checkIgnoredLines: true); 272 | 273 | // This file has ignore:coverage-file. 274 | expect(parsedResult, isNot(contains(_sampleAppFileUri))); 275 | expect(parsedResult, contains(_isolateLibFileUri)); 276 | } finally { 277 | await tempDir.delete(recursive: true); 278 | } 279 | }); 280 | 281 | test('mergeHitmaps', () { 282 | final resultMap = >{ 283 | 'foo.dart': {10: 2, 20: 0}, 284 | 'bar.dart': {10: 3, 20: 1, 30: 0}, 285 | }; 286 | final newMap = >{ 287 | 'bar.dart': {10: 2, 20: 0, 40: 3}, 288 | 'baz.dart': {10: 1, 20: 0, 30: 1}, 289 | }; 290 | // ignore: deprecated_member_use_from_same_package 291 | mergeHitmaps(newMap, resultMap); 292 | expect(resultMap, >{ 293 | 'foo.dart': {10: 2, 20: 0}, 294 | 'bar.dart': {10: 5, 20: 1, 30: 0, 40: 3}, 295 | 'baz.dart': {10: 1, 20: 0, 30: 1}, 296 | }); 297 | }); 298 | 299 | test('FileHitMaps.merge', () { 300 | final resultMap = { 301 | 'foo.dart': 302 | HitMap({10: 2, 20: 0}, {15: 0, 25: 1}, {15: 'bobble', 25: 'cobble'}), 303 | 'bar.dart': HitMap( 304 | {10: 3, 20: 1, 30: 0}, {15: 5, 25: 0}, {15: 'gobble', 25: 'wobble'}), 305 | }; 306 | final newMap = { 307 | 'bar.dart': HitMap( 308 | {10: 2, 20: 0, 40: 3}, {15: 1, 35: 4}, {15: 'gobble', 35: 'dobble'}), 309 | 'baz.dart': HitMap( 310 | {10: 1, 20: 0, 30: 1}, {15: 0, 25: 2}, {15: 'lobble', 25: 'zobble'}), 311 | }; 312 | resultMap.merge(newMap); 313 | expect(resultMap['foo.dart']?.lineHits, {10: 2, 20: 0}); 314 | expect(resultMap['foo.dart']?.funcHits, {15: 0, 25: 1}); 315 | expect(resultMap['foo.dart']?.funcNames, 316 | {15: 'bobble', 25: 'cobble'}); 317 | expect(resultMap['bar.dart']?.lineHits, 318 | {10: 5, 20: 1, 30: 0, 40: 3}); 319 | expect(resultMap['bar.dart']?.funcHits, {15: 6, 25: 0, 35: 4}); 320 | expect(resultMap['bar.dart']?.funcNames, 321 | {15: 'gobble', 25: 'wobble', 35: 'dobble'}); 322 | expect(resultMap['baz.dart']?.lineHits, {10: 1, 20: 0, 30: 1}); 323 | expect(resultMap['baz.dart']?.funcHits, {15: 0, 25: 2}); 324 | expect(resultMap['baz.dart']?.funcNames, 325 | {15: 'lobble', 25: 'zobble'}); 326 | }); 327 | } 328 | 329 | String? _coverageData; 330 | 331 | Future _getCoverageResult() async => 332 | _coverageData ??= await _collectCoverage(false, false); 333 | 334 | Future _collectCoverage( 335 | bool functionCoverage, bool branchCoverage) async { 336 | expect(FileSystemEntity.isFileSync(testAppPath), isTrue); 337 | 338 | final openPort = await getOpenPort(); 339 | 340 | // Run the sample app with the right flags. 341 | final sampleProcess = await runTestApp(openPort); 342 | 343 | // Capture the VM service URI. 344 | final serviceUri = await serviceUriFromProcess(sampleProcess.stdoutStream()); 345 | 346 | // Run the collection tool. 347 | // TODO: need to get all of this functionality in the lib 348 | final toolResult = await TestProcess.start(Platform.resolvedExecutable, [ 349 | _collectAppPath, 350 | if (functionCoverage) '--function-coverage', 351 | if (branchCoverage) '--branch-coverage', 352 | '--uri', 353 | '$serviceUri', 354 | '--resume-isolates', 355 | '--wait-paused' 356 | ]); 357 | 358 | await toolResult.shouldExit(0).timeout( 359 | timeout, 360 | onTimeout: () => 361 | throw StateError('We timed out waiting for the tool to finish.'), 362 | ); 363 | 364 | await sampleProcess.shouldExit(); 365 | 366 | return toolResult.stdoutStream().join('\n'); 367 | } 368 | -------------------------------------------------------------------------------- /test/format_coverage_test.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | 3 | import 'package:path/path.dart' as p; 4 | import 'package:test/test.dart'; 5 | 6 | import '../bin/format_coverage.dart'; 7 | 8 | void main() { 9 | late Directory testDir; 10 | setUp(() { 11 | testDir = Directory.systemTemp.createTempSync('coverage_test_temp'); 12 | }); 13 | 14 | tearDown(() async { 15 | if (testDir.existsSync()) testDir.deleteSync(recursive: true); 16 | }); 17 | 18 | test('considers all json files', () async { 19 | final fileA = File(p.join(testDir.path, 'coverage_a.json')); 20 | fileA.createSync(); 21 | final fileB = File(p.join(testDir.path, 'coverage_b.json')); 22 | fileB.createSync(); 23 | final fileC = File(p.join(testDir.path, 'not_coverage.foo')); 24 | fileC.createSync(); 25 | 26 | final files = filesToProcess(testDir.path); 27 | expect(files.length, equals(2)); 28 | expect( 29 | files.map((f) => f.path), 30 | containsAll( 31 | [endsWith('coverage_a.json'), endsWith('coverage_b.json')])); 32 | }); 33 | } 34 | -------------------------------------------------------------------------------- /test/function_coverage_test.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2022, the Dart project authors. Please see the AUTHORS file 2 | // for details. All rights reserved. Use of this source code is governed by a 3 | // BSD-style license that can be found in the LICENSE file. 4 | 5 | import 'dart:async'; 6 | import 'dart:convert' show json; 7 | import 'dart:io'; 8 | 9 | import 'package:coverage/coverage.dart'; 10 | import 'package:coverage/src/util.dart'; 11 | import 'package:path/path.dart' as p; 12 | import 'package:test/test.dart'; 13 | import 'package:test_process/test_process.dart'; 14 | 15 | import 'test_util.dart'; 16 | 17 | final _collectAppPath = p.join('bin', 'collect_coverage.dart'); 18 | final _funcCovApp = p.join('test', 'test_files', 'function_coverage_app.dart'); 19 | final _sampleAppFileUri = p.toUri(p.absolute(_funcCovApp)).toString(); 20 | 21 | void main() { 22 | test('Function coverage', () async { 23 | final resultString = await _collectCoverage(); 24 | final jsonResult = json.decode(resultString) as Map; 25 | final coverage = jsonResult['coverage'] as List; 26 | final hitMap = await HitMap.parseJson( 27 | coverage.cast>(), 28 | ); 29 | 30 | // function_coverage_app.dart. 31 | expect(hitMap, contains(_sampleAppFileUri)); 32 | final isolateFile = hitMap[_sampleAppFileUri]!; 33 | expect(isolateFile.funcHits, { 34 | 7: 1, 35 | 16: 1, 36 | 21: 1, 37 | 25: 1, 38 | 29: 1, 39 | 36: 1, 40 | 42: 1, 41 | 47: 1, 42 | }); 43 | expect(isolateFile.funcNames, { 44 | 7: 'normalFunction', 45 | 16: 'SomeClass.SomeClass', 46 | 21: 'SomeClass.normalMethod', 47 | 25: 'SomeClass.staticMethod', 48 | 29: 'SomeClass.abstractMethod', 49 | 36: 'SomeExtension.extensionMethod', 50 | 42: 'OtherClass.otherMethod', 51 | 47: 'main', 52 | }); 53 | 54 | // test_library.dart. 55 | final testLibraryPath = 56 | p.absolute(p.join('test', 'test_files', 'test_library.dart')); 57 | final testLibraryUri = p.toUri(testLibraryPath).toString(); 58 | expect(hitMap, contains(testLibraryUri)); 59 | final libraryfile = hitMap[testLibraryUri]!; 60 | expect(libraryfile.funcHits, {7: 1}); 61 | expect(libraryfile.funcNames, {7: 'libraryFunction'}); 62 | 63 | // test_library_part.dart. 64 | final testLibraryPartPath = 65 | p.absolute(p.join('test', 'test_files', 'test_library_part.dart')); 66 | final testLibraryPartUri = p.toUri(testLibraryPartPath).toString(); 67 | expect(hitMap, contains(testLibraryPartUri)); 68 | final libraryPartFile = hitMap[testLibraryPartUri]!; 69 | expect(libraryPartFile.funcHits, {7: 1}); 70 | expect(libraryPartFile.funcNames, {7: 'otherLibraryFunction'}); 71 | }); 72 | } 73 | 74 | Future _collectCoverage() async { 75 | expect(FileSystemEntity.isFileSync(_funcCovApp), isTrue); 76 | 77 | final openPort = await getOpenPort(); 78 | 79 | // Run the sample app with the right flags. 80 | final sampleProcess = await TestProcess.start(Platform.resolvedExecutable, [ 81 | '--enable-vm-service=$openPort', 82 | '--pause_isolates_on_exit', 83 | _funcCovApp 84 | ]); 85 | 86 | final serviceUri = await serviceUriFromProcess(sampleProcess.stdoutStream()); 87 | 88 | // Run the collection tool. 89 | final toolResult = await TestProcess.start(Platform.resolvedExecutable, [ 90 | _collectAppPath, 91 | '--function-coverage', 92 | '--uri', 93 | '$serviceUri', 94 | '--resume-isolates', 95 | '--wait-paused' 96 | ]); 97 | 98 | await toolResult.shouldExit(0).timeout( 99 | timeout, 100 | onTimeout: () => 101 | throw StateError('We timed out waiting for the tool to finish.'), 102 | ); 103 | 104 | await sampleProcess.shouldExit(); 105 | 106 | return toolResult.stdoutStream().join('\n'); 107 | } 108 | -------------------------------------------------------------------------------- /test/lcov_test.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2015, the Dart project authors. Please see the AUTHORS file 2 | // for details. All rights reserved. Use of this source code is governed by a 3 | // BSD-style license that can be found in the LICENSE file. 4 | 5 | import 'dart:async'; 6 | import 'dart:io'; 7 | 8 | import 'package:coverage/coverage.dart'; 9 | import 'package:coverage/src/util.dart'; 10 | import 'package:glob/glob.dart'; 11 | import 'package:path/path.dart' as p; 12 | import 'package:test/test.dart'; 13 | import 'package:test_process/test_process.dart'; 14 | 15 | final _sampleAppPath = p.join('test', 'test_files', 'test_app.dart'); 16 | final _sampleGeneratedPath = p.join('test', 'test_files', 'test_app.g.dart'); 17 | final _isolateLibPath = p.join('test', 'test_files', 'test_app_isolate.dart'); 18 | 19 | final _sampleAppFileUri = p.toUri(p.absolute(_sampleAppPath)).toString(); 20 | final _sampleGeneratedFileUri = 21 | p.toUri(p.absolute(_sampleGeneratedPath)).toString(); 22 | final _isolateLibFileUri = p.toUri(p.absolute(_isolateLibPath)).toString(); 23 | 24 | void main() { 25 | test('validate hitMap', () async { 26 | final hitmap = await _getHitMap(); 27 | 28 | expect(hitmap, contains(_sampleAppFileUri)); 29 | expect(hitmap, contains(_sampleGeneratedFileUri)); 30 | expect(hitmap, contains(_isolateLibFileUri)); 31 | expect(hitmap, contains('package:coverage/src/util.dart')); 32 | 33 | final sampleAppHitMap = hitmap[_sampleAppFileUri]; 34 | final sampleAppHitLines = sampleAppHitMap?.lineHits; 35 | final sampleAppHitFuncs = sampleAppHitMap?.funcHits; 36 | final sampleAppFuncNames = sampleAppHitMap?.funcNames; 37 | final sampleAppBranchHits = sampleAppHitMap?.branchHits; 38 | 39 | expect(sampleAppHitLines, containsPair(53, greaterThanOrEqualTo(1)), 40 | reason: 'be careful if you modify the test file'); 41 | expect(sampleAppHitLines, containsPair(57, 0), 42 | reason: 'be careful if you modify the test file'); 43 | expect(sampleAppHitLines, isNot(contains(39)), 44 | reason: 'be careful if you modify the test file'); 45 | expect(sampleAppHitFuncs, containsPair(52, 1), 46 | reason: 'be careful if you modify the test file'); 47 | expect(sampleAppHitFuncs, containsPair(56, 0), 48 | reason: 'be careful if you modify the test file'); 49 | expect(sampleAppFuncNames, containsPair(52, 'usedMethod'), 50 | reason: 'be careful if you modify the test file'); 51 | expect(sampleAppBranchHits, containsPair(48, 1), 52 | reason: 'be careful if you modify the test file'); 53 | }); 54 | 55 | test('validate hitMap, old VM without branch coverage', () async { 56 | final hitmap = await _getHitMap(); 57 | 58 | expect(hitmap, contains(_sampleAppFileUri)); 59 | expect(hitmap, contains(_sampleGeneratedFileUri)); 60 | expect(hitmap, contains(_isolateLibFileUri)); 61 | expect(hitmap, contains('package:coverage/src/util.dart')); 62 | 63 | final sampleAppHitMap = hitmap[_sampleAppFileUri]; 64 | final sampleAppHitLines = sampleAppHitMap?.lineHits; 65 | final sampleAppHitFuncs = sampleAppHitMap?.funcHits; 66 | final sampleAppFuncNames = sampleAppHitMap?.funcNames; 67 | 68 | expect(sampleAppHitLines, containsPair(53, greaterThanOrEqualTo(1)), 69 | reason: 'be careful if you modify the test file'); 70 | expect(sampleAppHitLines, containsPair(57, 0), 71 | reason: 'be careful if you modify the test file'); 72 | expect(sampleAppHitLines, isNot(contains(39)), 73 | reason: 'be careful if you modify the test file'); 74 | expect(sampleAppHitFuncs, containsPair(52, 1), 75 | reason: 'be careful if you modify the test file'); 76 | expect(sampleAppHitFuncs, containsPair(56, 0), 77 | reason: 'be careful if you modify the test file'); 78 | expect(sampleAppFuncNames, containsPair(52, 'usedMethod'), 79 | reason: 'be careful if you modify the test file'); 80 | }); 81 | 82 | group('LcovFormatter', () { 83 | test('format()', () async { 84 | final hitmap = await _getHitMap(); 85 | 86 | final resolver = await Resolver.create(packagePath: '.'); 87 | // ignore: deprecated_member_use_from_same_package 88 | final formatter = LcovFormatter(resolver); 89 | 90 | final res = await formatter 91 | .format(hitmap.map((key, value) => MapEntry(key, value.lineHits))); 92 | 93 | expect(res, contains(p.absolute(_sampleAppPath))); 94 | expect(res, contains(p.absolute(_sampleGeneratedPath))); 95 | expect(res, contains(p.absolute(_isolateLibPath))); 96 | expect(res, contains(p.absolute(p.join('lib', 'src', 'util.dart')))); 97 | }); 98 | 99 | test('formatLcov()', () async { 100 | final hitmap = await _getHitMap(); 101 | 102 | final resolver = await Resolver.create(packagePath: '.'); 103 | final res = hitmap.formatLcov(resolver); 104 | 105 | expect(res, contains(p.absolute(_sampleAppPath))); 106 | expect(res, contains(p.absolute(_sampleGeneratedPath))); 107 | expect(res, contains(p.absolute(_isolateLibPath))); 108 | expect(res, contains(p.absolute(p.join('lib', 'src', 'util.dart')))); 109 | }); 110 | 111 | test('formatLcov() includes files in reportOn list', () async { 112 | final hitmap = await _getHitMap(); 113 | 114 | final resolver = await Resolver.create(packagePath: '.'); 115 | final res = hitmap.formatLcov(resolver, reportOn: ['lib/', 'test/']); 116 | 117 | expect(res, contains(p.absolute(_sampleAppPath))); 118 | expect(res, contains(p.absolute(_sampleGeneratedPath))); 119 | expect(res, contains(p.absolute(_isolateLibPath))); 120 | expect(res, contains(p.absolute(p.join('lib', 'src', 'util.dart')))); 121 | }); 122 | 123 | test('formatLcov() excludes files not in reportOn list', () async { 124 | final hitmap = await _getHitMap(); 125 | 126 | final resolver = await Resolver.create(packagePath: '.'); 127 | final res = hitmap.formatLcov(resolver, reportOn: ['lib/']); 128 | 129 | expect(res, isNot(contains(p.absolute(_sampleAppPath)))); 130 | expect(res, isNot(contains(p.absolute(_sampleGeneratedPath)))); 131 | expect(res, isNot(contains(p.absolute(_isolateLibPath)))); 132 | expect(res, contains(p.absolute(p.join('lib', 'src', 'util.dart')))); 133 | }); 134 | 135 | test('formatLcov() excludes files matching glob patterns', () async { 136 | final hitmap = await _getHitMap(); 137 | 138 | final resolver = await Resolver.create(packagePath: '.'); 139 | final res = hitmap.formatLcov( 140 | resolver, 141 | ignoreGlobs: {Glob('**/*.g.dart'), Glob('**/util.dart')}, 142 | ); 143 | 144 | expect(res, contains(p.absolute(_sampleAppPath))); 145 | expect(res, isNot(contains(p.absolute(_sampleGeneratedPath)))); 146 | expect(res, contains(p.absolute(_isolateLibPath))); 147 | expect( 148 | res, 149 | isNot(contains(p.absolute(p.join('lib', 'src', 'util.dart')))), 150 | ); 151 | }); 152 | 153 | test( 154 | 'formatLcov() excludes files matching glob patterns regardless of their' 155 | 'presence on reportOn list', () async { 156 | final hitmap = await _getHitMap(); 157 | 158 | final resolver = await Resolver.create(packagePath: '.'); 159 | final res = hitmap.formatLcov( 160 | resolver, 161 | reportOn: ['test/'], 162 | ignoreGlobs: {Glob('**/*.g.dart')}, 163 | ); 164 | 165 | expect(res, contains(p.absolute(_sampleAppPath))); 166 | expect(res, isNot(contains(p.absolute(_sampleGeneratedPath)))); 167 | expect(res, contains(p.absolute(_isolateLibPath))); 168 | }); 169 | 170 | test('formatLcov() uses paths relative to basePath', () async { 171 | final hitmap = await _getHitMap(); 172 | 173 | final resolver = await Resolver.create(packagePath: '.'); 174 | final res = hitmap.formatLcov(resolver, basePath: p.absolute('lib')); 175 | 176 | expect( 177 | res, isNot(contains(p.absolute(p.join('lib', 'src', 'util.dart'))))); 178 | expect(res, contains(p.join('src', 'util.dart'))); 179 | }); 180 | }); 181 | 182 | group('PrettyPrintFormatter', () { 183 | test('format()', () async { 184 | final hitmap = await _getHitMap(); 185 | 186 | final resolver = await Resolver.create(packagePath: '.'); 187 | // ignore: deprecated_member_use_from_same_package 188 | final formatter = PrettyPrintFormatter(resolver, Loader()); 189 | 190 | final res = await formatter 191 | .format(hitmap.map((key, value) => MapEntry(key, value.lineHits))); 192 | 193 | expect(res, contains(p.absolute(_sampleAppPath))); 194 | expect(res, contains(p.absolute(_sampleGeneratedPath))); 195 | expect(res, contains(p.absolute(_isolateLibPath))); 196 | expect(res, contains(p.absolute(p.join('lib', 'src', 'util.dart')))); 197 | 198 | // be very careful if you change the test file 199 | expect(res, contains(' 0| return a - b;')); 200 | 201 | expect(res, contains('| return withTimeout(() async {'), 202 | reason: 'be careful if you change lib/src/util.dart'); 203 | 204 | final hitLineRegexp = RegExp(r'\s+(\d+)\| return a \+ b;'); 205 | final match = hitLineRegexp.allMatches(res).single; 206 | 207 | final hitCount = int.parse(match[1]!); 208 | expect(hitCount, greaterThanOrEqualTo(1)); 209 | }); 210 | 211 | test('prettyPrint()', () async { 212 | final hitmap = await _getHitMap(); 213 | 214 | final resolver = await Resolver.create(packagePath: '.'); 215 | final res = await hitmap.prettyPrint(resolver, Loader()); 216 | 217 | expect(res, contains(p.absolute(_sampleAppPath))); 218 | expect(res, contains(p.absolute(_sampleGeneratedPath))); 219 | expect(res, contains(p.absolute(_isolateLibPath))); 220 | expect(res, contains(p.absolute(p.join('lib', 'src', 'util.dart')))); 221 | 222 | // be very careful if you change the test file 223 | expect(res, contains(' 0| return a - b;')); 224 | 225 | expect(res, contains('| return withTimeout(() async {'), 226 | reason: 'be careful if you change lib/src/util.dart'); 227 | 228 | final hitLineRegexp = RegExp(r'\s+(\d+)\| return a \+ b;'); 229 | final match = hitLineRegexp.allMatches(res).single; 230 | 231 | final hitCount = int.parse(match[1]!); 232 | expect(hitCount, greaterThanOrEqualTo(1)); 233 | }); 234 | 235 | test('prettyPrint() includes files in reportOn list', () async { 236 | final hitmap = await _getHitMap(); 237 | 238 | final resolver = await Resolver.create(packagePath: '.'); 239 | final res = await hitmap 240 | .prettyPrint(resolver, Loader(), reportOn: ['lib/', 'test/']); 241 | 242 | expect(res, contains(p.absolute(_sampleAppPath))); 243 | expect(res, contains(p.absolute(_sampleGeneratedPath))); 244 | expect(res, contains(p.absolute(_isolateLibPath))); 245 | expect(res, contains(p.absolute(p.join('lib', 'src', 'util.dart')))); 246 | }); 247 | 248 | test('prettyPrint() excludes files not in reportOn list', () async { 249 | final hitmap = await _getHitMap(); 250 | 251 | final resolver = await Resolver.create(packagePath: '.'); 252 | final res = 253 | await hitmap.prettyPrint(resolver, Loader(), reportOn: ['lib/']); 254 | 255 | expect(res, isNot(contains(p.absolute(_sampleAppPath)))); 256 | expect(res, isNot(contains(p.absolute(_sampleGeneratedPath)))); 257 | expect(res, isNot(contains(p.absolute(_isolateLibPath)))); 258 | expect(res, contains(p.absolute(p.join('lib', 'src', 'util.dart')))); 259 | }); 260 | 261 | test('prettyPrint() excludes files matching glob patterns', () async { 262 | final hitmap = await _getHitMap(); 263 | 264 | final resolver = await Resolver.create(packagePath: '.'); 265 | final res = await hitmap.prettyPrint( 266 | resolver, 267 | Loader(), 268 | ignoreGlobs: {Glob('**/*.g.dart'), Glob('**/util.dart')}, 269 | ); 270 | 271 | expect(res, contains(p.absolute(_sampleAppPath))); 272 | expect(res, isNot(contains(p.absolute(_sampleGeneratedPath)))); 273 | expect(res, contains(p.absolute(_isolateLibPath))); 274 | expect( 275 | res, 276 | isNot(contains(p.absolute(p.join('lib', 'src', 'util.dart')))), 277 | ); 278 | }); 279 | 280 | test( 281 | 'prettyPrint() excludes files matching glob patterns regardless of' 282 | 'their presence on reportOn list', () async { 283 | final hitmap = await _getHitMap(); 284 | 285 | final resolver = await Resolver.create(packagePath: '.'); 286 | final res = await hitmap.prettyPrint( 287 | resolver, 288 | Loader(), 289 | reportOn: ['test/'], 290 | ignoreGlobs: {Glob('**/*.g.dart')}, 291 | ); 292 | 293 | expect(res, contains(p.absolute(_sampleAppPath))); 294 | expect(res, isNot(contains(p.absolute(_sampleGeneratedPath)))); 295 | expect(res, contains(p.absolute(_isolateLibPath))); 296 | }); 297 | 298 | test('prettyPrint() functions', () async { 299 | final hitmap = await _getHitMap(); 300 | 301 | final resolver = await Resolver.create(packagePath: '.'); 302 | final res = 303 | await hitmap.prettyPrint(resolver, Loader(), reportFuncs: true); 304 | 305 | expect(res, contains(p.absolute(_sampleAppPath))); 306 | expect(res, contains(p.absolute(_sampleGeneratedPath))); 307 | expect(res, contains(p.absolute(_isolateLibPath))); 308 | expect(res, contains(p.absolute(p.join('lib', 'src', 'util.dart')))); 309 | 310 | // be very careful if you change the test file 311 | expect(res, contains(' 1|Future main() async {')); 312 | expect(res, contains(' 1|int usedMethod(int a, int b) {')); 313 | expect(res, contains(' 0|int unusedMethod(int a, int b) {')); 314 | expect(res, contains(' | return a + b;')); 315 | }); 316 | 317 | test('prettyPrint() branches', () async { 318 | final hitmap = await _getHitMap(); 319 | 320 | final resolver = await Resolver.create(packagePath: '.'); 321 | final res = 322 | await hitmap.prettyPrint(resolver, Loader(), reportBranches: true); 323 | 324 | expect(res, contains(p.absolute(_sampleAppPath))); 325 | expect(res, contains(p.absolute(_sampleGeneratedPath))); 326 | expect(res, contains(p.absolute(_isolateLibPath))); 327 | expect(res, contains(p.absolute(p.join('lib', 'src', 'util.dart')))); 328 | 329 | // be very careful if you change the test file 330 | expect(res, contains(' 1| if (x == answer) {')); 331 | expect(res, contains(' 0| while (i < lines.length) {')); 332 | expect(res, contains(' | bar.baz();')); 333 | }); 334 | }); 335 | } 336 | 337 | Future> _getHitMap() async { 338 | expect(FileSystemEntity.isFileSync(_sampleAppPath), isTrue); 339 | 340 | // select service port. 341 | final port = await getOpenPort(); 342 | 343 | // start sample app. 344 | final sampleAppArgs = [ 345 | '--pause-isolates-on-exit', 346 | '--enable-vm-service=$port', 347 | '--branch-coverage', 348 | _sampleAppPath 349 | ]; 350 | final sampleProcess = 351 | await TestProcess.start(Platform.resolvedExecutable, sampleAppArgs); 352 | 353 | final serviceUri = await serviceUriFromProcess(sampleProcess.stdoutStream()); 354 | 355 | // collect hit map. 356 | final coverageJson = (await collect(serviceUri, true, true, false, {}, 357 | functionCoverage: true, 358 | branchCoverage: true))['coverage'] as List>; 359 | final hitMap = HitMap.parseJson(coverageJson); 360 | 361 | await sampleProcess.shouldExit(0); 362 | 363 | return hitMap; 364 | } 365 | -------------------------------------------------------------------------------- /test/resolver_test.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2016, the Dart project authors. Please see the AUTHORS file 2 | // for details. All rights reserved. Use of this source code is governed by a 3 | // BSD-style license that can be found in the LICENSE file. 4 | 5 | import 'package:coverage/src/resolver.dart'; 6 | import 'package:path/path.dart' as p; 7 | import 'package:test/test.dart'; 8 | import 'package:test_descriptor/test_descriptor.dart' as d; 9 | 10 | void main() { 11 | group('Default Resolver', () { 12 | setUp(() async { 13 | final sandboxUriPath = p.toUri(d.sandbox).toString(); 14 | await d.dir('bar', [ 15 | d.dir('lib', [ 16 | d.file('bar.dart', 'final fizz = "bar";'), 17 | ]) 18 | ]).create(); 19 | 20 | await d.dir('foo', [ 21 | d.dir('.dart_tool', [ 22 | d.file('bad_package_config.json', 'thisIsntAPackageConfigFile!'), 23 | d.file('package_config.json', ''' 24 | { 25 | "configVersion": 2, 26 | "packages": [ 27 | { 28 | "name": "foo", 29 | "rootUri": "../", 30 | "packageUri": "lib/" 31 | }, 32 | { 33 | "name": "bar", 34 | "rootUri": "$sandboxUriPath/bar", 35 | "packageUri": "lib/" 36 | } 37 | ] 38 | } 39 | '''), 40 | ]), 41 | d.dir('lib', [ 42 | d.file('foo.dart', 'final foo = "bar";'), 43 | ]), 44 | ]).create(); 45 | 46 | await d.dir('sdk', [ 47 | d.dir('io', [ 48 | d.file('io.dart', 'final io = "hello";'), 49 | ]), 50 | d.dir('io_patch', [ 51 | d.file('io.dart', 'final patch = true;'), 52 | ]), 53 | d.dir('io_dev', [ 54 | d.file('io.dart', 'final dev = true;'), 55 | ]), 56 | ]).create(); 57 | }); 58 | 59 | test('can be created from a package_config.json', () async { 60 | final resolver = await Resolver.create( 61 | packagesPath: 62 | p.join(d.sandbox, 'foo', '.dart_tool', 'package_config.json')); 63 | expect(resolver.resolve('package:foo/foo.dart'), 64 | p.join(d.sandbox, 'foo', 'lib', 'foo.dart')); 65 | expect(resolver.resolve('package:bar/bar.dart'), 66 | p.join(d.sandbox, 'bar', 'lib', 'bar.dart')); 67 | }); 68 | 69 | test('can be created from a package directory', () async { 70 | final resolver = 71 | await Resolver.create(packagePath: p.join(d.sandbox, 'foo')); 72 | expect(resolver.resolve('package:foo/foo.dart'), 73 | p.join(d.sandbox, 'foo', 'lib', 'foo.dart')); 74 | }); 75 | 76 | test('errors if the packagesFile is an unknown format', () async { 77 | expect( 78 | () async => await Resolver.create( 79 | packagesPath: p.join( 80 | d.sandbox, 'foo', '.dart_tool', 'bad_package_config.json')), 81 | throwsA(isA())); 82 | }); 83 | 84 | test('resolves dart: URIs', () async { 85 | final resolver = await Resolver.create( 86 | packagePath: p.join(d.sandbox, 'foo'), 87 | sdkRoot: p.join(d.sandbox, 'sdk')); 88 | expect(resolver.resolve('dart:io'), 89 | p.join(d.sandbox, 'sdk', 'io', 'io.dart')); 90 | expect(resolver.resolve('dart:io-patch/io.dart'), null); 91 | expect(resolver.resolve('dart:io-dev/io.dart'), 92 | p.join(d.sandbox, 'sdk', 'io_dev', 'io.dart')); 93 | }); 94 | 95 | test('cannot resolve SDK URIs if sdkRoot is null', () async { 96 | final resolver = 97 | await Resolver.create(packagePath: p.join(d.sandbox, 'foo')); 98 | expect(resolver.resolve('dart:convert'), null); 99 | }); 100 | 101 | test('cannot resolve package URIs if packagePath is null', () async { 102 | // ignore: deprecated_member_use_from_same_package 103 | final resolver = Resolver(); 104 | expect(resolver.resolve('package:foo/foo.dart'), null); 105 | }); 106 | 107 | test('cannot resolve package URIs if packagePath is not found', () async { 108 | final resolver = 109 | await Resolver.create(packagePath: p.join(d.sandbox, 'foo')); 110 | expect(resolver.resolve('package:baz/baz.dart'), null); 111 | }); 112 | 113 | test('cannot resolve unexpected URI schemes', () async { 114 | final resolver = 115 | await Resolver.create(packagePath: p.join(d.sandbox, 'foo')); 116 | expect(resolver.resolve('thing:foo/foo.dart'), null); 117 | }); 118 | }); 119 | 120 | group('Bazel resolver', () { 121 | const workspace = 'foo'; 122 | final resolver = BazelResolver(workspacePath: workspace); 123 | 124 | test('does not resolve SDK URIs', () { 125 | expect(resolver.resolve('dart:convert'), null); 126 | }); 127 | 128 | test('resolves third-party package URIs', () { 129 | expect(resolver.resolve('package:foo/bar.dart'), 130 | 'third_party/dart/foo/lib/bar.dart'); 131 | expect(resolver.resolve('package:foo/src/bar.dart'), 132 | 'third_party/dart/foo/lib/src/bar.dart'); 133 | }); 134 | 135 | test('resolves non-third-party package URIs', () { 136 | expect( 137 | resolver.resolve('package:foo.bar/baz.dart'), 'foo/bar/lib/baz.dart'); 138 | expect(resolver.resolve('package:foo.bar/src/baz.dart'), 139 | 'foo/bar/lib/src/baz.dart'); 140 | }); 141 | 142 | test('resolves file URIs', () { 143 | expect( 144 | resolver 145 | .resolve('file://x/y/z.runfiles/$workspace/foo/bar/lib/baz.dart'), 146 | 'foo/bar/lib/baz.dart'); 147 | expect( 148 | resolver.resolve( 149 | 'file://x/y/z.runfiles/$workspace/foo/bar/lib/src/baz.dart'), 150 | 'foo/bar/lib/src/baz.dart'); 151 | }); 152 | 153 | test('resolves HTTPS URIs containing /packages/', () { 154 | expect(resolver.resolve('https://host:8080/a/b/packages/foo/bar.dart'), 155 | 'third_party/dart/foo/lib/bar.dart'); 156 | expect( 157 | resolver.resolve('https://host:8080/a/b/packages/foo/src/bar.dart'), 158 | 'third_party/dart/foo/lib/src/bar.dart'); 159 | expect( 160 | resolver.resolve('https://host:8080/a/b/packages/foo.bar/baz.dart'), 161 | 'foo/bar/lib/baz.dart'); 162 | expect( 163 | resolver 164 | .resolve('https://host:8080/a/b/packages/foo.bar/src/baz.dart'), 165 | 'foo/bar/lib/src/baz.dart'); 166 | }); 167 | 168 | test('resolves HTTP URIs containing /packages/', () { 169 | expect(resolver.resolve('http://host:8080/a/b/packages/foo/bar.dart'), 170 | 'third_party/dart/foo/lib/bar.dart'); 171 | expect(resolver.resolve('http://host:8080/a/b/packages/foo/src/bar.dart'), 172 | 'third_party/dart/foo/lib/src/bar.dart'); 173 | expect(resolver.resolve('http://host:8080/a/b/packages/foo.bar/baz.dart'), 174 | 'foo/bar/lib/baz.dart'); 175 | expect( 176 | resolver 177 | .resolve('http://host:8080/a/b/packages/foo.bar/src/baz.dart'), 178 | 'foo/bar/lib/src/baz.dart'); 179 | }); 180 | 181 | test('resolves HTTPS URIs without /packages/', () { 182 | expect( 183 | resolver 184 | .resolve('https://host:8080/third_party/dart/foo/lib/bar.dart'), 185 | 'third_party/dart/foo/lib/bar.dart'); 186 | expect( 187 | resolver.resolve( 188 | 'https://host:8080/third_party/dart/foo/lib/src/bar.dart'), 189 | 'third_party/dart/foo/lib/src/bar.dart'); 190 | expect(resolver.resolve('https://host:8080/foo/lib/bar.dart'), 191 | 'foo/lib/bar.dart'); 192 | expect(resolver.resolve('https://host:8080/foo/lib/src/bar.dart'), 193 | 'foo/lib/src/bar.dart'); 194 | }); 195 | 196 | test('resolves HTTP URIs without /packages/', () { 197 | expect( 198 | resolver 199 | .resolve('http://host:8080/third_party/dart/foo/lib/bar.dart'), 200 | 'third_party/dart/foo/lib/bar.dart'); 201 | expect( 202 | resolver.resolve( 203 | 'http://host:8080/third_party/dart/foo/lib/src/bar.dart'), 204 | 'third_party/dart/foo/lib/src/bar.dart'); 205 | expect(resolver.resolve('http://host:8080/foo/lib/bar.dart'), 206 | 'foo/lib/bar.dart'); 207 | expect(resolver.resolve('http://host:8080/foo/lib/src/bar.dart'), 208 | 'foo/lib/src/bar.dart'); 209 | }); 210 | }); 211 | } 212 | -------------------------------------------------------------------------------- /test/run_and_collect_test.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2015, the Dart project authors. Please see the AUTHORS file 2 | // for details. All rights reserved. Use of this source code is governed by a 3 | // BSD-style license that can be found in the LICENSE file. 4 | 5 | import 'package:coverage/coverage.dart'; 6 | import 'package:path/path.dart' as p; 7 | import 'package:test/test.dart'; 8 | 9 | import 'test_util.dart'; 10 | 11 | final _isolateLibPath = p.join('test', 'test_files', 'test_app_isolate.dart'); 12 | 13 | final _sampleAppFileUri = p.toUri(p.absolute(testAppPath)).toString(); 14 | final _isolateLibFileUri = p.toUri(p.absolute(_isolateLibPath)).toString(); 15 | 16 | void main() { 17 | test('runAndCollect', () async { 18 | // use runAndCollect and verify that the results match w/ running manually 19 | final coverage = coverageDataFromJson(await runAndCollect(testAppPath)); 20 | expect(coverage, isNotEmpty); 21 | 22 | final sources = coverage.sources(); 23 | 24 | for (var sampleCoverageData in sources[_sampleAppFileUri]!) { 25 | expect(sampleCoverageData['hits'], isNotNull); 26 | } 27 | 28 | for (var sampleCoverageData in sources[_isolateLibFileUri]!) { 29 | expect(sampleCoverageData['hits'], isNotEmpty); 30 | } 31 | 32 | final hitMap = await HitMap.parseJson(coverage, checkIgnoredLines: true); 33 | checkHitmap(hitMap); 34 | final resolver = await Resolver.create(); 35 | final ignoredLinesInFilesCache = >?>{}; 36 | final hitMap2 = HitMap.parseJsonSync(coverage, 37 | checkIgnoredLines: true, 38 | ignoredLinesInFilesCache: ignoredLinesInFilesCache, 39 | resolver: resolver); 40 | checkHitmap(hitMap2); 41 | checkIgnoredLinesInFilesCache(ignoredLinesInFilesCache); 42 | 43 | // Asking again the cache should answer questions about ignored lines, 44 | // so providing a resolver that throws when asked for files should be ok. 45 | final hitMap3 = HitMap.parseJsonSync(coverage, 46 | checkIgnoredLines: true, 47 | ignoredLinesInFilesCache: ignoredLinesInFilesCache, 48 | resolver: ThrowingResolver()); 49 | checkHitmap(hitMap3); 50 | checkIgnoredLinesInFilesCache(ignoredLinesInFilesCache); 51 | }); 52 | } 53 | 54 | class ThrowingResolver implements Resolver { 55 | @override 56 | List get failed => throw UnimplementedError(); 57 | 58 | @override 59 | String? get packagePath => throw UnimplementedError(); 60 | 61 | @override 62 | String? get packagesPath => throw UnimplementedError(); 63 | 64 | @override 65 | String? resolve(String scriptUri) => throw UnimplementedError(); 66 | 67 | @override 68 | String? resolveSymbolicLinks(String path) => throw UnimplementedError(); 69 | 70 | @override 71 | String? get sdkRoot => throw UnimplementedError(); 72 | } 73 | 74 | void checkIgnoredLinesInFilesCache( 75 | Map>?> ignoredLinesInFilesCache) { 76 | expect(ignoredLinesInFilesCache.length, 4); 77 | final keys = ignoredLinesInFilesCache.keys.toList(); 78 | final testAppKey = 79 | keys.where((element) => element.endsWith('test_app.dart')).single; 80 | final testAppIsolateKey = 81 | keys.where((element) => element.endsWith('test_app_isolate.dart')).single; 82 | final packageUtilKey = keys 83 | .where((element) => element.endsWith('package:coverage/src/util.dart')) 84 | .single; 85 | expect(ignoredLinesInFilesCache[packageUtilKey], isEmpty); 86 | expect(ignoredLinesInFilesCache[testAppKey], null /* means whole file */); 87 | expect(ignoredLinesInFilesCache[testAppIsolateKey], [ 88 | [51, 51], 89 | [53, 57], 90 | [62, 65], 91 | [66, 69] 92 | ]); 93 | } 94 | 95 | void checkHitmap(Map hitMap) { 96 | expect(hitMap, isNot(contains(_sampleAppFileUri))); 97 | 98 | final actualHitMap = hitMap[_isolateLibFileUri]; 99 | final actualLineHits = actualHitMap?.lineHits; 100 | final expectedLineHits = { 101 | 11: 1, 102 | 12: 1, 103 | 13: 1, 104 | 15: 0, 105 | 19: 1, 106 | 23: 1, 107 | 24: 2, 108 | 28: 1, 109 | 29: 1, 110 | 30: 1, 111 | 32: 0, 112 | 38: 1, 113 | 39: 1, 114 | 41: 1, 115 | 42: 3, 116 | 43: 1, 117 | 44: 3, 118 | 45: 1, 119 | 48: 1, 120 | 49: 1, 121 | 59: 1, 122 | 60: 1 123 | }; 124 | 125 | expect(actualLineHits, expectedLineHits); 126 | expect(actualHitMap?.funcHits, isNull); 127 | expect(actualHitMap?.funcNames, isNull); 128 | expect(actualHitMap?.branchHits, isNull); 129 | } 130 | -------------------------------------------------------------------------------- /test/test_files/function_coverage_app.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2022, the Dart project authors. Please see the AUTHORS file 2 | // for details. All rights reserved. Use of this source code is governed by a 3 | // BSD-style license that can be found in the LICENSE file. 4 | 5 | import 'test_library.dart'; 6 | 7 | int normalFunction() { 8 | return 123; 9 | } 10 | 11 | abstract class BaseClass { 12 | int abstractMethod(); 13 | } 14 | 15 | class SomeClass extends BaseClass { 16 | SomeClass() : x = 123; 17 | 18 | // Creates an implicit getter and setter that should be ignored. 19 | int x; 20 | 21 | int normalMethod() { 22 | return 123; 23 | } 24 | 25 | static int staticMethod() { 26 | return 123; 27 | } 28 | 29 | @override 30 | int abstractMethod() { 31 | return 123; 32 | } 33 | } 34 | 35 | extension SomeExtension on SomeClass { 36 | int extensionMethod() { 37 | return 123; 38 | } 39 | } 40 | 41 | class OtherClass { 42 | int otherMethod() { 43 | return 123; 44 | } 45 | } 46 | 47 | void main() { 48 | print(normalFunction()); 49 | print(SomeClass().normalMethod()); 50 | print(SomeClass.staticMethod()); 51 | print(SomeClass().extensionMethod()); 52 | print(SomeClass().abstractMethod()); 53 | print(OtherClass().otherMethod()); 54 | print(libraryFunction()); 55 | print(otherLibraryFunction()); 56 | } 57 | 58 | // ignore_for_file: unreachable_from_main 59 | -------------------------------------------------------------------------------- /test/test_files/test_app.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2015, the Dart project authors. Please see the AUTHORS file 2 | // for details. All rights reserved. Use of this source code is governed by a 3 | // BSD-style license that can be found in the LICENSE file. 4 | 5 | // coverage:ignore-file 6 | 7 | import 'dart:async'; 8 | import 'dart:developer'; 9 | import 'dart:isolate'; 10 | 11 | import 'package:coverage/src/util.dart'; 12 | 13 | import 'test_app_isolate.dart'; 14 | 15 | part 'test_app.g.dart'; 16 | 17 | Future main() async { 18 | for (var i = 0; i < 10; i++) { 19 | for (var j = 0; j < 10; j++) { 20 | final sum = usedMethod(i, j); 21 | if (sum != (i + j)) { 22 | throw 'bad method!'; 23 | } 24 | 25 | final multiplication = usedGeneratedMethod(i, j); 26 | if (multiplication != i * j) { 27 | throw 'bad generated method!'; 28 | } 29 | } 30 | } 31 | 32 | final port = ReceivePort(); 33 | 34 | final isolate = 35 | await Isolate.spawn(isolateTask, [port.sendPort, 1, 2], paused: true); 36 | await Service.controlWebServer(enable: true); 37 | final isolateID = Service.getIsolateID(isolate); 38 | print('isolateId = $isolateID'); 39 | 40 | isolate.addOnExitListener(port.sendPort); 41 | isolate.resume(isolate.pauseCapability!); 42 | 43 | final value = await port.first as int; 44 | if (value != 3) { 45 | throw 'expected 3!'; 46 | } 47 | 48 | final result = await retry(() async => 42, const Duration(seconds: 1)) as int; 49 | print(result); 50 | } 51 | 52 | int usedMethod(int a, int b) { 53 | return a + b; 54 | } 55 | 56 | int unusedMethod(int a, int b) { 57 | return a - b; 58 | } 59 | 60 | // ignore_for_file: unreachable_from_main, only_throw_errors 61 | // ignore_for_file: deprecated_member_use 62 | -------------------------------------------------------------------------------- /test/test_files/test_app.g.dart: -------------------------------------------------------------------------------- 1 | part of 'test_app.dart'; 2 | 3 | int usedGeneratedMethod(int a, int b) { 4 | return a * b; 5 | } 6 | -------------------------------------------------------------------------------- /test/test_files/test_app_isolate.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2015, the Dart project authors. Please see the AUTHORS file 2 | // for details. All rights reserved. Use of this source code is governed by a 3 | // BSD-style license that can be found in the LICENSE file. 4 | 5 | import 'dart:async'; 6 | import 'dart:io'; 7 | import 'dart:isolate'; 8 | 9 | const int answer = 42; 10 | 11 | String fooSync(int x) { 12 | if (x == answer) { 13 | return '*' * x; 14 | } 15 | return List.generate(x, (_) => 'xyzzy').join(' '); 16 | } 17 | 18 | class BarClass { 19 | BarClass(this.x); 20 | 21 | int x; 22 | 23 | void baz() { 24 | print(x); 25 | } 26 | } 27 | 28 | Future fooAsync(int x) async { 29 | if (x == answer) { 30 | return '*' * x; 31 | } 32 | return List.generate(x, (_) => 'xyzzy').join(' '); 33 | } 34 | 35 | /// The number of covered lines is tested and expected to be 4. 36 | /// 37 | /// If you modify this method, you may have to update the tests! 38 | void isolateTask(List threeThings) { 39 | sleep(const Duration(milliseconds: 500)); 40 | 41 | fooSync(answer); 42 | fooAsync(answer).then((_) { 43 | final port = threeThings.first as SendPort; 44 | final sum = (threeThings[1] as int) + (threeThings[2] as int); 45 | port.send(sum); 46 | }); 47 | 48 | final bar = BarClass(123); 49 | bar.baz(); 50 | 51 | print('678'); // coverage:ignore-line 52 | 53 | // coverage:ignore-start 54 | print('1'); 55 | print('2'); 56 | print('3'); 57 | // coverage:ignore-end 58 | 59 | print('4 // coverage:ignore-line'); 60 | print('5 // coverage:ignore-file'); 61 | 62 | print('6'); // coverage:ignore-start 63 | print('7'); 64 | print('8'); 65 | // coverage:ignore-end 66 | print('9'); // coverage:ignore-start 67 | print('10'); 68 | print('11'); // coverage:ignore-line 69 | // coverage:ignore-end 70 | } 71 | -------------------------------------------------------------------------------- /test/test_files/test_library.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2022, the Dart project authors. Please see the AUTHORS file 2 | // for details. All rights reserved. Use of this source code is governed by a 3 | // BSD-style license that can be found in the LICENSE file. 4 | 5 | part 'test_library_part.dart'; 6 | 7 | int libraryFunction() { 8 | return 123; 9 | } 10 | -------------------------------------------------------------------------------- /test/test_files/test_library_part.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2022, the Dart project authors. Please see the AUTHORS file 2 | // for details. All rights reserved. Use of this source code is governed by a 3 | // BSD-style license that can be found in the LICENSE file. 4 | 5 | part of 'test_library.dart'; 6 | 7 | int otherLibraryFunction() { 8 | return 123; 9 | } 10 | -------------------------------------------------------------------------------- /test/test_util.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2015, the Dart project authors. Please see the AUTHORS file 2 | // for details. All rights reserved. Use of this source code is governed by a 3 | // BSD-style license that can be found in the LICENSE file. 4 | 5 | import 'dart:async'; 6 | import 'dart:io'; 7 | 8 | import 'package:path/path.dart' as p; 9 | import 'package:test/test.dart'; 10 | import 'package:test_process/test_process.dart'; 11 | 12 | final String testAppPath = p.join('test', 'test_files', 'test_app.dart'); 13 | 14 | const Duration timeout = Duration(seconds: 20); 15 | 16 | Future runTestApp(int openPort) => TestProcess.start( 17 | Platform.resolvedExecutable, 18 | [ 19 | '--enable-vm-service=$openPort', 20 | '--pause_isolates_on_exit', 21 | '--branch-coverage', 22 | testAppPath 23 | ], 24 | ); 25 | 26 | List> coverageDataFromJson(Map json) { 27 | expect(json.keys, unorderedEquals(['type', 'coverage'])); 28 | expect(json, containsPair('type', 'CodeCoverage')); 29 | 30 | return (json['coverage'] as List).cast>(); 31 | } 32 | 33 | final _versionPattern = RegExp('([0-9]+)\\.([0-9]+)\\.([0-9]+)'); 34 | 35 | bool platformVersionCheck(int minMajor, int minMinor) { 36 | final match = _versionPattern.matchAsPrefix(Platform.version); 37 | if (match == null) return false; 38 | if (match.groupCount < 3) return false; 39 | final major = int.parse(match.group(1)!); 40 | final minor = int.parse(match.group(2)!); 41 | return major > minMajor || (major == minMajor && minor >= minMinor); 42 | } 43 | 44 | /// Returns a mapping of (URL: (function_name: hit_count)) from [sources]. 45 | Map> functionInfoFromSources( 46 | Map>> sources, 47 | ) { 48 | Map getFuncNames(List list) { 49 | return { 50 | for (var i = 0; i < list.length; i += 2) 51 | list[i] as int: list[i + 1] as String, 52 | }; 53 | } 54 | 55 | Map getFuncHits(List list) { 56 | return { 57 | for (var i = 0; i < list.length; i += 2) 58 | list[i] as int: list[i + 1] as int, 59 | }; 60 | } 61 | 62 | return { 63 | for (var entry in sources.entries) 64 | entry.key: entry.value.fold( 65 | {}, 66 | (previousValue, element) { 67 | expect(element['source'], entry.key); 68 | final names = getFuncNames(element['funcNames'] as List); 69 | final hits = getFuncHits(element['funcHits'] as List); 70 | 71 | for (var pair in hits.entries) { 72 | previousValue[names[pair.key]!] = 73 | (previousValue[names[pair.key]!] ?? 0) + pair.value; 74 | } 75 | 76 | return previousValue; 77 | }, 78 | ), 79 | }; 80 | } 81 | 82 | extension ListTestExtension on List { 83 | Map>> sources() => cast().fold( 84 | >{}, 85 | (Map> map, value) { 86 | final sourceUri = value['source'] as String; 87 | map.putIfAbsent(sourceUri, () => []).add(value); 88 | return map; 89 | }, 90 | ); 91 | } 92 | -------------------------------------------------------------------------------- /test/test_with_coverage_package/lib/validate_lib.dart: -------------------------------------------------------------------------------- 1 | int sum(Iterable values) { 2 | var val = 0; 3 | for (var value in values) { 4 | val += value; 5 | } 6 | return val; 7 | } 8 | 9 | int product(Iterable values) { 10 | var val = 1; 11 | for (var value in values) { 12 | val *= value; 13 | } 14 | return val; 15 | } 16 | -------------------------------------------------------------------------------- /test/test_with_coverage_package/pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: coverage_integration_test_for_test_with_coverage 2 | publish_to: none 3 | 4 | environment: 5 | sdk: '>=2.15.0 <3.0.0' 6 | 7 | dev_dependencies: 8 | test: ^1.16.0 9 | 10 | dependency_overrides: 11 | coverage: 12 | path: ../../ 13 | -------------------------------------------------------------------------------- /test/test_with_coverage_package/test/product_test.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2022, the Dart project authors. Please see the AUTHORS file 2 | // for details. All rights reserved. Use of this source code is governed by a 3 | // BSD-style license that can be found in the LICENSE file. 4 | 5 | import 'package:test/test.dart'; 6 | 7 | // ignore: avoid_relative_lib_imports 8 | import '../lib/validate_lib.dart'; 9 | 10 | void main() { 11 | test('product', () { 12 | expect(product([2, 3]), 6); 13 | }); 14 | } 15 | -------------------------------------------------------------------------------- /test/test_with_coverage_package/test/sum_test.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2022, the Dart project authors. Please see the AUTHORS file 2 | // for details. All rights reserved. Use of this source code is governed by a 3 | // BSD-style license that can be found in the LICENSE file. 4 | 5 | import 'package:test/test.dart'; 6 | 7 | // ignore: avoid_relative_lib_imports 8 | import '../lib/validate_lib.dart'; 9 | 10 | void main() { 11 | test('sum', () { 12 | expect(sum([1, 2]), 3); 13 | }); 14 | } 15 | -------------------------------------------------------------------------------- /test/test_with_coverage_test.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2022, the Dart project authors. Please see the AUTHORS file 2 | // for details. All rights reserved. Use of this source code is governed by a 3 | // BSD-style license that can be found in the LICENSE file. 4 | 5 | @Timeout(Duration(seconds: 60)) 6 | library; 7 | 8 | import 'dart:convert'; 9 | import 'dart:io'; 10 | 11 | import 'package:path/path.dart' as p; 12 | import 'package:test/test.dart'; 13 | import 'package:test_descriptor/test_descriptor.dart' as d; 14 | import 'package:test_process/test_process.dart'; 15 | 16 | import 'test_util.dart'; 17 | 18 | // this package 19 | final _pkgDir = p.absolute(''); 20 | final _testWithCoveragePath = p.join(_pkgDir, 'bin', 'test_with_coverage.dart'); 21 | 22 | // test package 23 | final _testPkgDirPath = p.join(_pkgDir, 'test', 'test_with_coverage_package'); 24 | 25 | /// Override PUB_CACHE 26 | /// 27 | /// Use a subdirectory different from `test/` just in case there is a problem 28 | /// with the clean up. If other packages are present under the `test/` 29 | /// subdirectory their tests may accidentally get run when running `dart test` 30 | final _pubCachePathInTestPkgSubDir = p.join(_pkgDir, 'var', 'pub-cache'); 31 | final _env = {'PUB_CACHE': _pubCachePathInTestPkgSubDir}; 32 | 33 | const _testPackageName = 'coverage_integration_test_for_test_with_coverage'; 34 | 35 | int _port = 9300; 36 | 37 | Iterable _dartFiles(String dir) => 38 | Directory(p.join(_testPkgDirPath, dir)).listSync().whereType(); 39 | 40 | String _fixTestFile(String content) => content.replaceAll( 41 | "import '../lib/", 42 | "import 'package:$_testPackageName/", 43 | ); 44 | 45 | void main() { 46 | setUpAll(() async { 47 | for (var dir in const ['lib', 'test']) { 48 | await d.dir(dir, [ 49 | for (var dartFile in _dartFiles(dir)) 50 | d.file( 51 | p.basename(dartFile.path), 52 | _fixTestFile(dartFile.readAsStringSync()), 53 | ), 54 | ]).create(); 55 | } 56 | 57 | var pubspecContent = 58 | File(p.join(_testPkgDirPath, 'pubspec.yaml')).readAsStringSync(); 59 | 60 | expect( 61 | pubspecContent.replaceAll('\r\n', '\n'), 62 | contains(r''' 63 | dependency_overrides: 64 | coverage: 65 | path: ../../ 66 | '''), 67 | ); 68 | 69 | pubspecContent = 70 | pubspecContent.replaceFirst('path: ../../', 'path: $_pkgDir'); 71 | 72 | await d.file('pubspec.yaml', pubspecContent).create(); 73 | 74 | final localPub = await _run(['pub', 'get']); 75 | await localPub.shouldExit(0); 76 | }); 77 | 78 | test('dart run bin/test_with_coverage.dart -f', () async { 79 | final list = await _runTest(['run', _testWithCoveragePath, '-f']); 80 | 81 | final sources = list.sources(); 82 | final functionHits = functionInfoFromSources(sources); 83 | 84 | expect( 85 | functionHits['package:$_testPackageName/validate_lib.dart'], 86 | { 87 | 'product': 1, 88 | 'sum': 1, 89 | }, 90 | ); 91 | }); 92 | 93 | test('dart run bin/test_with_coverage.dart -f -- -N sum', () async { 94 | final list = await _runTest( 95 | ['run', _testWithCoveragePath, '-f'], 96 | extraArgs: ['--', '-N', 'sum'], 97 | ); 98 | 99 | final sources = list.sources(); 100 | final functionHits = functionInfoFromSources(sources); 101 | 102 | expect( 103 | functionHits['package:$_testPackageName/validate_lib.dart'], 104 | { 105 | 'product': 0, 106 | 'sum': 1, 107 | }, 108 | reason: 'only `sum` tests should be run', 109 | ); 110 | }); 111 | 112 | test('dart run coverage:test_with_coverage', () async { 113 | await _runTest(['run', 'coverage:test_with_coverage']); 114 | }); 115 | 116 | test('dart pub global run coverage:test_with_coverage', () async { 117 | final globalPub = 118 | await _run(['pub', 'global', 'activate', '-s', 'path', _pkgDir]); 119 | await globalPub.shouldExit(0); 120 | 121 | await _runTest( 122 | ['pub', 'global', 'run', 'coverage:test_with_coverage'], 123 | ); 124 | }); 125 | } 126 | 127 | Future _run(List args) => TestProcess.start( 128 | Platform.executable, 129 | args, 130 | workingDirectory: d.sandbox, 131 | environment: _env, 132 | ); 133 | 134 | Future>> _runTest( 135 | List invokeArgs, { 136 | List? extraArgs, 137 | }) async { 138 | final process = await _run([ 139 | ...invokeArgs, 140 | '--port', 141 | '${_port++}', 142 | ...?extraArgs, 143 | ]); 144 | 145 | await process.shouldExit(0); 146 | 147 | await d.dir( 148 | 'coverage', 149 | [d.file('coverage.json', isNotEmpty), d.file('lcov.info', isNotEmpty)], 150 | ).validate(); 151 | 152 | final coverageDataFile = File(p.join(d.sandbox, 'coverage', 'coverage.json')); 153 | 154 | final json = jsonDecode(coverageDataFile.readAsStringSync()); 155 | 156 | return coverageDataFromJson(json as Map); 157 | } 158 | -------------------------------------------------------------------------------- /test/util_test.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2015, the Dart project authors. Please see the AUTHORS file 2 | // for details. All rights reserved. Use of this source code is governed by a 3 | // BSD-style license that can be found in the LICENSE file. 4 | 5 | // ignore_for_file: only_throw_errors 6 | 7 | import 'dart:async'; 8 | 9 | import 'package:coverage/src/util.dart'; 10 | import 'package:test/test.dart'; 11 | 12 | const _failCount = 5; 13 | const _delay = Duration(milliseconds: 10); 14 | 15 | void main() { 16 | test('retry', () async { 17 | var count = 0; 18 | final stopwatch = Stopwatch()..start(); 19 | 20 | Future failCountTimes() async { 21 | expect(stopwatch.elapsed, greaterThanOrEqualTo(_delay * count)); 22 | 23 | while (count < _failCount) { 24 | count++; 25 | throw 'not yet!'; 26 | } 27 | return 42; 28 | } 29 | 30 | final value = await retry(failCountTimes, _delay) as int; 31 | 32 | expect(value, 42); 33 | expect(count, _failCount); 34 | expect(stopwatch.elapsed, greaterThanOrEqualTo(_delay * count)); 35 | }); 36 | 37 | group('retry with timeout', () { 38 | test('if it finishes', () async { 39 | var count = 0; 40 | final stopwatch = Stopwatch()..start(); 41 | 42 | Future failCountTimes() async { 43 | expect(stopwatch.elapsed, greaterThanOrEqualTo(_delay * count)); 44 | 45 | while (count < _failCount) { 46 | count++; 47 | throw 'not yet!'; 48 | } 49 | return 42; 50 | } 51 | 52 | final safeTimoutDuration = _delay * _failCount * 10; 53 | final value = await retry( 54 | failCountTimes, 55 | _delay, 56 | timeout: safeTimoutDuration, 57 | ) as int; 58 | 59 | expect(value, 42); 60 | expect(count, _failCount); 61 | expect(stopwatch.elapsed, greaterThanOrEqualTo(_delay * count)); 62 | }); 63 | 64 | test('if it does not finish', () async { 65 | var count = 0; 66 | final stopwatch = Stopwatch()..start(); 67 | 68 | var caught = false; 69 | var countAfterError = 0; 70 | 71 | Future failCountTimes() async { 72 | if (caught) { 73 | countAfterError++; 74 | } 75 | expect(stopwatch.elapsed, greaterThanOrEqualTo(_delay * count)); 76 | 77 | count++; 78 | throw 'never'; 79 | } 80 | 81 | final unsafeTimeoutDuration = _delay * (_failCount / 2); 82 | 83 | try { 84 | await retry(failCountTimes, _delay, timeout: unsafeTimeoutDuration); 85 | // ignore: avoid_catching_errors 86 | } on StateError catch (e) { 87 | expect(e.message, 'Failed to complete within 25ms'); 88 | caught = true; 89 | 90 | expect(countAfterError, 0, 91 | reason: 'Execution should stop after a timeout'); 92 | 93 | await Future.delayed(_delay * 3); 94 | 95 | expect(countAfterError, 0, reason: 'Even after a delay'); 96 | } 97 | 98 | expect(caught, isTrue); 99 | }); 100 | }); 101 | 102 | group('extractVMServiceUri', () { 103 | test('returns null when not found', () { 104 | expect(extractVMServiceUri('foo bar baz'), isNull); 105 | }); 106 | 107 | test('returns null for an incorrectly formatted URI', () { 108 | const msg = 'Observatory listening on :://'; 109 | expect(extractVMServiceUri(msg), null); 110 | }); 111 | 112 | test('returns URI at end of string', () { 113 | const msg = 'Observatory listening on http://foo.bar:9999/'; 114 | expect(extractVMServiceUri(msg), Uri.parse('http://foo.bar:9999/')); 115 | }); 116 | 117 | test('returns URI with auth token at end of string', () { 118 | const msg = 'Observatory listening on http://foo.bar:9999/cG90YXRv/'; 119 | expect( 120 | extractVMServiceUri(msg), Uri.parse('http://foo.bar:9999/cG90YXRv/')); 121 | }); 122 | 123 | test('return URI embedded within string', () { 124 | const msg = '1985-10-26 Observatory listening on http://foo.bar:9999/ **'; 125 | expect(extractVMServiceUri(msg), Uri.parse('http://foo.bar:9999/')); 126 | }); 127 | 128 | test('return URI with auth token embedded within string', () { 129 | const msg = 130 | '1985-10-26 Observatory listening on http://foo.bar:9999/cG90YXRv/ **'; 131 | expect( 132 | extractVMServiceUri(msg), Uri.parse('http://foo.bar:9999/cG90YXRv/')); 133 | }); 134 | 135 | test('handles new Dart VM service message format', () { 136 | const msg = 137 | 'The Dart VM service is listening on http://foo.bar:9999/cG90YXRv/'; 138 | expect( 139 | extractVMServiceUri(msg), Uri.parse('http://foo.bar:9999/cG90YXRv/')); 140 | }); 141 | }); 142 | 143 | group('getIgnoredLines', () { 144 | const invalidSources = [ 145 | '''final str = ''; // coverage:ignore-start 146 | final str = ''; 147 | final str = ''; // coverage:ignore-start 148 | ''', 149 | '''final str = ''; // coverage:ignore-start 150 | final str = ''; 151 | final str = ''; // coverage:ignore-start 152 | final str = ''; // coverage:ignore-end 153 | final str = ''; 154 | final str = ''; // coverage:ignore-end 155 | ''', 156 | '''final str = ''; // coverage:ignore-start 157 | final str = ''; 158 | final str = ''; // coverage:ignore-end 159 | final str = ''; 160 | final str = ''; // coverage:ignore-end 161 | ''', 162 | '''final str = ''; // coverage:ignore-end 163 | final str = ''; 164 | final str = ''; // coverage:ignore-start 165 | final str = ''; 166 | final str = ''; // coverage:ignore-end 167 | ''', 168 | '''final str = ''; // coverage:ignore-end 169 | final str = ''; 170 | final str = ''; // coverage:ignore-end 171 | ''', 172 | '''final str = ''; // coverage:ignore-end 173 | final str = ''; 174 | final str = ''; // coverage:ignore-start 175 | ''', 176 | '''final str = ''; // coverage:ignore-end 177 | ''', 178 | '''final str = ''; // coverage:ignore-start 179 | ''', 180 | ]; 181 | 182 | test('throws FormatException when the annotations are not balanced', () { 183 | void runTest(int index, String errMsg) { 184 | final content = invalidSources[index].split('\n'); 185 | expect( 186 | () => getIgnoredLines('content-$index.dart', content), 187 | throwsA( 188 | allOf( 189 | isFormatException, 190 | predicate((FormatException e) => e.message == errMsg), 191 | ), 192 | ), 193 | reason: 'expected FormatException with message "$errMsg"', 194 | ); 195 | } 196 | 197 | runTest( 198 | 0, 199 | 'coverage:ignore-start found at content-0.dart:' 200 | '3 before previous coverage:ignore-start ended', 201 | ); 202 | runTest( 203 | 1, 204 | 'coverage:ignore-start found at content-1.dart:' 205 | '3 before previous coverage:ignore-start ended', 206 | ); 207 | runTest( 208 | 2, 209 | 'unmatched coverage:ignore-end found at content-2.dart:5', 210 | ); 211 | runTest( 212 | 3, 213 | 'unmatched coverage:ignore-end found at content-3.dart:1', 214 | ); 215 | runTest( 216 | 4, 217 | 'unmatched coverage:ignore-end found at content-4.dart:1', 218 | ); 219 | runTest( 220 | 5, 221 | 'unmatched coverage:ignore-end found at content-5.dart:1', 222 | ); 223 | runTest( 224 | 6, 225 | 'unmatched coverage:ignore-end found at content-6.dart:1', 226 | ); 227 | runTest( 228 | 7, 229 | 'coverage:ignore-start found at content-7.dart:' 230 | '1 has no matching coverage:ignore-end', 231 | ); 232 | }); 233 | 234 | test( 235 | 'returns [[0,lines.length]] when the annotations are not ' 236 | 'balanced but the whole file is ignored', () { 237 | for (final content in invalidSources) { 238 | final lines = content.split('\n'); 239 | lines.add(' // coverage:ignore-file'); 240 | expect(getIgnoredLines('', lines), [ 241 | [0, lines.length] 242 | ]); 243 | } 244 | }); 245 | 246 | test('returns [[0,lines.length]] when the whole file is ignored', () { 247 | final lines = '''final str = ''; // coverage:ignore-start 248 | final str = ''; // coverage:ignore-end 249 | final str = ''; // coverage:ignore-file 250 | ''' 251 | .split('\n'); 252 | 253 | expect(getIgnoredLines('', lines), [ 254 | [0, lines.length] 255 | ]); 256 | }); 257 | 258 | test('return the correct range of lines ignored', () { 259 | final lines = ''' 260 | final str = ''; // coverage:ignore-start 261 | final str = ''; // coverage:ignore-line 262 | final str = ''; // coverage:ignore-end 263 | final str = ''; // coverage:ignore-start 264 | final str = ''; // coverage:ignore-line 265 | final str = ''; // coverage:ignore-end 266 | ''' 267 | .split('\n'); 268 | 269 | expect(getIgnoredLines('', lines), [ 270 | [1, 3], 271 | [4, 6], 272 | ]); 273 | }); 274 | 275 | test('return the correct list of lines ignored', () { 276 | final lines = ''' 277 | final str = ''; // coverage:ignore-line 278 | final str = ''; // coverage:ignore-line 279 | final str = ''; // coverage:ignore-line 280 | ''' 281 | .split('\n'); 282 | 283 | expect(getIgnoredLines('', lines), [ 284 | [1, 1], 285 | [2, 2], 286 | [3, 3], 287 | ]); 288 | }); 289 | 290 | test('ignore comments have no effect inside string literals', () { 291 | final lines = ''' 292 | final str = '// coverage:ignore-file'; 293 | final str = '// coverage:ignore-line'; 294 | final str = ''; // coverage:ignore-line 295 | final str = '// coverage:ignore-start'; 296 | final str = '// coverage:ignore-end'; 297 | ''' 298 | .split('\n'); 299 | 300 | expect(getIgnoredLines('', lines), [ 301 | [3, 3], 302 | ]); 303 | }); 304 | 305 | test('allow white-space after ignore comments', () { 306 | // Using multiple strings, rather than splitting a multi-line string, 307 | // because many code editors remove trailing white-space. 308 | final lines = [ 309 | "final str = ''; // coverage:ignore-start ", 310 | "final str = ''; // coverage:ignore-line\t", 311 | "final str = ''; // coverage:ignore-end \t \t ", 312 | "final str = ''; // coverage:ignore-line \t ", 313 | "final str = ''; // coverage:ignore-start \t ", 314 | "final str = ''; // coverage:ignore-end \t \t ", 315 | ]; 316 | 317 | expect(getIgnoredLines('', lines), [ 318 | [1, 3], 319 | [4, 4], 320 | [5, 6], 321 | ]); 322 | }); 323 | 324 | test('allow omitting space after //', () { 325 | final lines = [ 326 | "final str = ''; //coverage:ignore-start", 327 | "final str = ''; //coverage:ignore-line", 328 | "final str = ''; //coverage:ignore-end", 329 | "final str = ''; //coverage:ignore-line", 330 | "final str = ''; //coverage:ignore-start", 331 | "final str = ''; //coverage:ignore-end", 332 | ]; 333 | 334 | expect(getIgnoredLines('', lines), [ 335 | [1, 3], 336 | [4, 4], 337 | [5, 6], 338 | ]); 339 | }); 340 | 341 | test('allow text after ignore comments', () { 342 | final lines = [ 343 | "final str = ''; // coverage:ignore-start due to XYZ", 344 | "final str = ''; // coverage:ignore-line", 345 | "final str = ''; // coverage:ignore-end due to XYZ", 346 | "final str = ''; // coverage:ignore-line due to 123", 347 | "final str = ''; // coverage:ignore-start", 348 | "final str = ''; // coverage:ignore-end it is done", 349 | ]; 350 | 351 | expect(getIgnoredLines('', lines), [ 352 | [1, 3], 353 | [4, 4], 354 | [5, 6], 355 | ]); 356 | }); 357 | }); 358 | } 359 | --------------------------------------------------------------------------------