coverage.mjs 3.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293
  1. /*---------------------------------------------------------
  2. * Copyright (C) Microsoft Corporation. All rights reserved.
  3. *--------------------------------------------------------*/
  4. import { Report } from 'c8';
  5. import { randomUUID } from 'crypto';
  6. import { existsSync, promises as fs, mkdirSync } from 'fs';
  7. import { tmpdir } from 'os';
  8. import { join, resolve } from 'path';
  9. import { CliExpectedError } from './error.mjs';
  10. const srcDirCandidates = ['src', 'lib', '.'];
  11. /**
  12. * Manages collecting coverage data from test runs. All runs, regardless of
  13. * platform, expect coverage data given in the V8 coverage format. We then
  14. * use c8 to convert it to the common Istanbul format and represent it with
  15. * a variety of reporters.
  16. */
  17. export class Coverage {
  18. config;
  19. args;
  20. targetDir = join(tmpdir(), `vsc-coverage-${randomUUID()}`);
  21. constructor(config, args) {
  22. this.config = config;
  23. this.args = args;
  24. mkdirSync(this.targetDir, { recursive: true });
  25. }
  26. async write() {
  27. const cfg = this.config.coverage || {};
  28. let defaultReporters = ['text-summary', 'html'];
  29. let reporterOptions;
  30. if (Array.isArray(cfg.reporter)) {
  31. defaultReporters = cfg.reporter;
  32. }
  33. else if (cfg.reporter) {
  34. defaultReporters = Object.keys(cfg.reporter);
  35. reporterOptions = cfg.reporter;
  36. }
  37. try {
  38. const report = new Report({
  39. tempDirectory: this.targetDir,
  40. exclude: cfg.exclude,
  41. include: cfg.include,
  42. reporter: this.args.coverageReporter?.length
  43. ? this.args.coverageReporter.map(String)
  44. : defaultReporters,
  45. reporterOptions,
  46. reportsDirectory: this.args.coverageOutput || join(this.config.dir, 'coverage'),
  47. src: this.getSourcesDirectories(),
  48. all: cfg.includeAll,
  49. excludeNodeModules: true,
  50. // not yet in the .d.ts for c8:
  51. //@ts-ignore
  52. mergeAsync: true,
  53. });
  54. // A hacky fix due to an outstanding bug in Istanbul's exclusion testing
  55. // code: its subdirectory checks are case-sensitive on Windows, but file
  56. // URIs might have mixed casing.
  57. //
  58. // Setting `relativePath: false` on the exclude bypasses this code path.
  59. //
  60. // https://github.com/istanbuljs/test-exclude/issues/43
  61. // https://github.com/istanbuljs/test-exclude/blob/a5b1d07584109f5f553ccef97de64c6cbfca4764/index.js#L91
  62. report.exclude.relativePath = false;
  63. // While we're hacking, may as well keep hacking: we don't want to mess
  64. // with default excludes, but we want to exclude the .vscode-test internals
  65. report.exclude.exclude.push('**/.vscode-test/**');
  66. await report.run();
  67. }
  68. catch (e) {
  69. throw new CliExpectedError(`Coverage report generated failed, please file an issue with original reports located in ${this.targetDir}:\n\n${e}`);
  70. }
  71. await fs.rm(this.targetDir, { recursive: true, force: true });
  72. }
  73. getSourcesDirectories() {
  74. const srcs = new Set();
  75. for (const test of this.config.tests) {
  76. const dir = this.config.extensionDevelopmentPath(test);
  77. let srcDir = test.srcDir;
  78. for (const candidate of srcDirCandidates) {
  79. if (srcDir) {
  80. break;
  81. }
  82. const candidatePath = resolve(dir[0], candidate);
  83. if (existsSync(candidatePath)) {
  84. srcDir = candidatePath;
  85. }
  86. }
  87. if (srcDir) {
  88. srcs.add(srcDir);
  89. }
  90. }
  91. return [...srcs];
  92. }
  93. }