@teamscale/coverage-collector 0.0.1-alpha.18 → 0.0.1-beta.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -19,9 +19,10 @@ information back to the original source code.
19
19
  ## Building
20
20
 
21
21
  The Collector is written in TypeScript/JavaScript. For building and running it,
22
- NodeJs (>= v14) and Yarn (>= v3) are needed as prerequisites.
22
+ NodeJs (>= v14) and Yarn are needed as prerequisites.
23
23
 
24
24
  ```
25
+ yarn clean
25
26
  yarn install
26
27
  yarn build
27
28
  ```
package/dist/package.json CHANGED
@@ -1,23 +1,27 @@
1
1
  {
2
2
  "name": "@teamscale/coverage-collector",
3
- "version": "0.0.1-alpha.18",
3
+ "version": "0.0.1-beta.3",
4
4
  "description": "Collector for JavaScript code coverage information",
5
- "main": "dist/main.js",
6
- "bin": "dist/main.js",
7
- "types": "dist/main.d.ts",
5
+ "main": "dist/src/main.js",
6
+ "bin": "dist/src/main.js",
7
+ "types": "dist/src/main.d.ts",
8
8
  "author": "CQSE GmbH",
9
9
  "license": "Apache-2.0",
10
+ "repository": {
11
+ "type": "git",
12
+ "url": "https://github.com/cqse/teamscale-javascript-profiler.git"
13
+ },
10
14
  "scripts": {
11
- "clean": "npx rimraf dist tsconfig.tsbuildinfo",
15
+ "clean": "rimraf dist tsconfig.tsbuildinfo",
12
16
  "build": "tsc",
13
- "serve": "node dist/main.js",
14
- "test": "yarn build && jest --forceExit --coverage --silent=true"
17
+ "serve": "node dist/src/main.js",
18
+ "test": "yarn build && NODE_OPTIONS='--experimental-vm-modules' jest --forceExit --coverage --silent=true --detectOpenHandles"
15
19
  },
16
20
  "files": [
17
21
  "dist/**/*"
18
22
  ],
19
23
  "dependencies": {
20
- "@cqse/commons": "workspace:*",
24
+ "@cqse/commons": "^0.0.1-beta.1",
21
25
  "argparse": "^2.0.1",
22
26
  "async": "^3.2.0",
23
27
  "rxjs": "^7.1.0",
@@ -37,7 +41,9 @@
37
41
  "@types/winston": "^2.4.4",
38
42
  "@types/ws": "^7.4.2",
39
43
  "babel-jest": "^27.2.0",
44
+ "esbuild": "^0.13.4",
40
45
  "jest": "^27.2.0",
46
+ "rimraf": "^3.0.2",
41
47
  "ts-jest": "^27.0.5",
42
48
  "ts-node": "^10.2.1",
43
49
  "typescript": "^4.4.3"
package/dist/src/main.js CHANGED
@@ -1,25 +1,33 @@
1
1
  #!/usr/bin/env node
2
- import { version } from '../package.json';
3
- import { ArgumentParser } from 'argparse';
4
- import winston from 'winston';
5
- import { DataStorage } from './storage/DataStorage';
6
- import { WebSocketCollectingServer } from './receiver/CollectingServer';
2
+ "use strict";
3
+ var __importDefault = (this && this.__importDefault) || function (mod) {
4
+ return (mod && mod.__esModule) ? mod : { "default": mod };
5
+ };
6
+ Object.defineProperty(exports, "__esModule", { value: true });
7
+ exports.Main = void 0;
8
+ const package_json_1 = require("../package.json");
9
+ const argparse_1 = require("argparse");
10
+ const winston_1 = __importDefault(require("winston"));
11
+ const DataStorage_1 = require("./storage/DataStorage");
12
+ const CollectingServer_1 = require("./receiver/CollectingServer");
7
13
  /**
8
14
  * The main class of the Teamscale JavaScript Collector.
9
15
  * Used to start-up the collector for with a given configuration.
10
16
  */
11
- export class Main {
17
+ class Main {
12
18
  /**
13
19
  * Construct the object for parsing the command line arguments.
14
20
  */
15
21
  static buildParser() {
16
- const parser = new ArgumentParser({
22
+ const parser = new argparse_1.ArgumentParser({
17
23
  description: 'Collector of the Teamscale JavaScript Profiler. Collects coverage information from a' +
18
24
  '(headless) Web browser that executes code instrumented with our instrumenter.'
19
25
  });
20
- parser.add_argument('-v', '--version', { action: 'version', version });
26
+ parser.add_argument('-v', '--version', { action: 'version', version: package_json_1.version });
21
27
  parser.add_argument('-p', '--port', { help: 'The port to receive coverage information on.', default: 54678 });
22
28
  parser.add_argument('-f', '--dump-to-file', { help: 'Target file', default: './coverage.simple' });
29
+ parser.add_argument('-l', '--log-to-file', { help: 'Log file', default: 'logs/collector-combined.log' });
30
+ parser.add_argument('-e', '--log-level', { help: 'Log level', default: 'info' });
23
31
  parser.add_argument('-s', '--dump-after-secs', {
24
32
  help: 'Dump the coverage information to the target file every N seconds.',
25
33
  default: 120
@@ -40,15 +48,15 @@ export class Main {
40
48
  /**
41
49
  * Construct the logger.
42
50
  */
43
- static buildLogger() {
44
- return winston.createLogger({
45
- level: 'info',
46
- format: winston.format.json(),
51
+ static buildLogger(config) {
52
+ return winston_1.default.createLogger({
53
+ level: config.log_level,
54
+ format: winston_1.default.format.json(),
47
55
  defaultMeta: {},
48
56
  transports: [
49
- new winston.transports.File({ filename: 'logs/collector-error.log', level: 'error' }),
50
- new winston.transports.File({ filename: 'logs/collector-combined.log' }),
51
- new winston.transports.Console({ format: winston.format.simple(), level: 'info' })
57
+ new winston_1.default.transports.File({ filename: 'logs/collector-error.log', level: 'error' }),
58
+ new winston_1.default.transports.File({ filename: config.log_to_file.trim() }),
59
+ new winston_1.default.transports.Console({ format: winston_1.default.format.simple(), level: config.log_level })
52
60
  ]
53
61
  });
54
62
  }
@@ -56,13 +64,15 @@ export class Main {
56
64
  * Entry point of the Teamscale JavaScript Profiler.
57
65
  */
58
66
  static run() {
59
- const logger = this.buildLogger();
60
- logger.info(`Starting collector in working directory "${process.cwd()}".`);
61
67
  // Parse the command line arguments
62
68
  const config = this.parseArguments();
69
+ // Build the logger
70
+ const logger = this.buildLogger(config);
71
+ logger.info(`Starting collector in working directory "${process.cwd()}".`);
72
+ logger.info(`Logging "${config.log_level}" to "${config.log_to_file}".`);
63
73
  // Prepare the storage and the server
64
- const storage = new DataStorage(logger);
65
- const server = new WebSocketCollectingServer(config.port, storage, logger);
74
+ const storage = new DataStorage_1.DataStorage(logger);
75
+ const server = new CollectingServer_1.WebSocketCollectingServer(config.port, storage, logger);
66
76
  // Start the server socket.
67
77
  // ATTENTION: The server is executed asynchronously
68
78
  server.start();
@@ -98,10 +108,11 @@ export class Main {
98
108
  clearInterval(timer);
99
109
  }
100
110
  // ... and do a final dump
101
- logger.info('\nCaught interrupt signal. Writing latest coverage.');
102
- storage.dumpToSimpleCoverageFile(config.dump_to_file);
111
+ const written = storage.dumpToSimpleCoverageFile(config.dump_to_file);
112
+ logger.info(`\nCaught interrupt signal. Written ${written} lines of the latest coverage.`);
103
113
  });
104
114
  }
105
115
  }
106
116
  }
117
+ exports.Main = Main;
107
118
  Main.run();
@@ -1,17 +1,39 @@
1
- import * as WebSocket from 'ws';
2
- import { Contract } from '@cqse/commons';
3
- import { Session } from './Session';
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ Object.defineProperty(o, k2, { enumerable: true, get: function() { return m[k]; } });
5
+ }) : (function(o, m, k, k2) {
6
+ if (k2 === undefined) k2 = k;
7
+ o[k2] = m[k];
8
+ }));
9
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
10
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
11
+ }) : function(o, v) {
12
+ o["default"] = v;
13
+ });
14
+ var __importStar = (this && this.__importStar) || function (mod) {
15
+ if (mod && mod.__esModule) return mod;
16
+ var result = {};
17
+ if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
18
+ __setModuleDefault(result, mod);
19
+ return result;
20
+ };
21
+ Object.defineProperty(exports, "__esModule", { value: true });
22
+ exports.WebSocketCollectingServer = exports.ProtocolMessageTypes = void 0;
23
+ const WebSocket = __importStar(require("ws"));
24
+ const commons_1 = require("@cqse/commons");
25
+ const Session_1 = require("./Session");
4
26
  /**
5
27
  * Various constants that are used to exchange data between
6
28
  * the instrumented application and the coverage collector.
7
29
  */
8
- export var ProtocolMessageTypes;
30
+ var ProtocolMessageTypes;
9
31
  (function (ProtocolMessageTypes) {
10
32
  /** A message that provides a source map */
11
33
  ProtocolMessageTypes["TYPE_SOURCEMAP"] = "s";
12
34
  /** A message that provides coverage information */
13
35
  ProtocolMessageTypes["TYPE_COVERAGE"] = "c";
14
- })(ProtocolMessageTypes || (ProtocolMessageTypes = {}));
36
+ })(ProtocolMessageTypes = exports.ProtocolMessageTypes || (exports.ProtocolMessageTypes = {}));
15
37
  /**
16
38
  * Separates the instrumentation subject from the coverage information.
17
39
  */
@@ -20,7 +42,7 @@ const INSTRUMENTATION_SUBJECT_SEPARATOR = ':';
20
42
  * A WebSocket based implementation of a coverage receiver.
21
43
  * Receives coverage from instrumented JavaScript code.
22
44
  */
23
- export class WebSocketCollectingServer {
45
+ class WebSocketCollectingServer {
24
46
  /**
25
47
  * Constructor.
26
48
  *
@@ -29,9 +51,9 @@ export class WebSocketCollectingServer {
29
51
  * @param logger - The logger to use.
30
52
  */
31
53
  constructor(port, storage, logger) {
32
- Contract.require(port > 0 && port < 65536, 'Port must be valid (range).');
33
- this.storage = Contract.requireDefined(storage);
34
- this.logger = Contract.requireDefined(logger);
54
+ commons_1.Contract.require(port > 0 && port < 65536, 'Port must be valid (range).');
55
+ this.storage = commons_1.Contract.requireDefined(storage);
56
+ this.logger = commons_1.Contract.requireDefined(logger);
35
57
  this.server = new WebSocket.Server({ port: port });
36
58
  }
37
59
  /**
@@ -41,7 +63,7 @@ export class WebSocketCollectingServer {
41
63
  this.logger.info(`Starting server on port ${this.server.options.port}.`);
42
64
  // Handle new connections from clients
43
65
  this.server.on('connection', (webSocket, req) => {
44
- let session = new Session(req.socket, this.storage, this.logger);
66
+ let session = new Session_1.Session(req.socket, this.storage, this.logger);
45
67
  this.logger.debug(`Connection from: ${req.socket.remoteAddress}`);
46
68
  // Handle disconnecting clients
47
69
  webSocket.on('close', code => {
@@ -94,6 +116,7 @@ export class WebSocketCollectingServer {
94
116
  const fileIdSeparatorPosition = body.indexOf(INSTRUMENTATION_SUBJECT_SEPARATOR);
95
117
  if (fileIdSeparatorPosition > -1) {
96
118
  const fileId = body.substring(0, fileIdSeparatorPosition).trim();
119
+ this.logger.debug(`Received source map information for ${fileId}`);
97
120
  const sourcemap = body.substring(fileIdSeparatorPosition + 1);
98
121
  session.putSourcemap(fileId, sourcemap);
99
122
  }
@@ -118,3 +141,4 @@ export class WebSocketCollectingServer {
118
141
  }
119
142
  }
120
143
  }
144
+ exports.WebSocketCollectingServer = WebSocketCollectingServer;
@@ -1,11 +1,33 @@
1
- import * as sourceMap from 'source-map';
2
- import { Contract } from '@cqse/commons';
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ Object.defineProperty(o, k2, { enumerable: true, get: function() { return m[k]; } });
5
+ }) : (function(o, m, k, k2) {
6
+ if (k2 === undefined) k2 = k;
7
+ o[k2] = m[k];
8
+ }));
9
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
10
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
11
+ }) : function(o, v) {
12
+ o["default"] = v;
13
+ });
14
+ var __importStar = (this && this.__importStar) || function (mod) {
15
+ if (mod && mod.__esModule) return mod;
16
+ var result = {};
17
+ if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
18
+ __setModuleDefault(result, mod);
19
+ return result;
20
+ };
21
+ Object.defineProperty(exports, "__esModule", { value: true });
22
+ exports.Session = void 0;
23
+ const sourceMap = __importStar(require("source-map"));
24
+ const commons_1 = require("@cqse/commons");
3
25
  /**
4
26
  * The session maintains the relevant information for a client.
5
27
  * One session is created for each client.
6
28
  * The mapping based on sourcemaps is conducted here.
7
29
  */
8
- export class Session {
30
+ class Session {
9
31
  /**
10
32
  * Constructor
11
33
  *
@@ -14,9 +36,9 @@ export class Session {
14
36
  * @param logger - The logger to use.
15
37
  */
16
38
  constructor(socket, storage, logger) {
17
- this.socket = Contract.requireDefined(socket);
18
- this.storage = Contract.requireDefined(storage);
19
- this.logger = Contract.requireDefined(logger);
39
+ this.socket = commons_1.Contract.requireDefined(socket);
40
+ this.storage = commons_1.Contract.requireDefined(storage);
41
+ this.logger = commons_1.Contract.requireDefined(logger);
20
42
  this.sourceMaps = new Map();
21
43
  this.projectId = ''; // We currently only support coverage for one project.
22
44
  }
@@ -84,3 +106,4 @@ export class Session {
84
106
  }
85
107
  }
86
108
  }
109
+ exports.Session = Session;
@@ -94,6 +94,10 @@ export declare class DataStorage implements IDataStorage {
94
94
  * Logger to use.
95
95
  */
96
96
  private readonly logger;
97
+ /**
98
+ * Times unmapped coverage received.
99
+ */
100
+ private timesUnmappedCoverage;
97
101
  /**
98
102
  * Constructs the data storage.
99
103
  *
@@ -1,16 +1,38 @@
1
- import { Contract, removePrefix } from '@cqse/commons';
2
- import * as fs from 'fs';
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ Object.defineProperty(o, k2, { enumerable: true, get: function() { return m[k]; } });
5
+ }) : (function(o, m, k, k2) {
6
+ if (k2 === undefined) k2 = k;
7
+ o[k2] = m[k];
8
+ }));
9
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
10
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
11
+ }) : function(o, v) {
12
+ o["default"] = v;
13
+ });
14
+ var __importStar = (this && this.__importStar) || function (mod) {
15
+ if (mod && mod.__esModule) return mod;
16
+ var result = {};
17
+ if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
18
+ __setModuleDefault(result, mod);
19
+ return result;
20
+ };
21
+ Object.defineProperty(exports, "__esModule", { value: true });
22
+ exports.DataStorage = exports.ProjectCoverage = void 0;
23
+ const commons_1 = require("@cqse/commons");
24
+ const fs = __importStar(require("fs"));
3
25
  /**
4
26
  * The coverage information received for one particular project.
5
27
  */
6
- export class ProjectCoverage {
28
+ class ProjectCoverage {
7
29
  /**
8
30
  * Constructor.
9
31
  *
10
32
  * @param projectId - The identifier of the project to collect the coverage for.
11
33
  */
12
34
  constructor(projectId) {
13
- this.projectId = Contract.requireDefined(projectId);
35
+ this.projectId = commons_1.Contract.requireDefined(projectId);
14
36
  this.coveredLinesByFile = new Map();
15
37
  }
16
38
  /**
@@ -44,10 +66,11 @@ export class ProjectCoverage {
44
66
  });
45
67
  }
46
68
  }
69
+ exports.ProjectCoverage = ProjectCoverage;
47
70
  /**
48
71
  * The data storage which retrieves coverage information.
49
72
  */
50
- export class DataStorage {
73
+ class DataStorage {
51
74
  /**
52
75
  * Constructs the data storage.
53
76
  *
@@ -55,7 +78,8 @@ export class DataStorage {
55
78
  */
56
79
  constructor(logger) {
57
80
  this.coverageByProject = new Map();
58
- this.logger = Contract.requireDefined(logger);
81
+ this.logger = commons_1.Contract.requireDefined(logger);
82
+ this.timesUnmappedCoverage = 0;
59
83
  }
60
84
  /**
61
85
  * Put coverage into the storage.
@@ -81,14 +105,17 @@ export class DataStorage {
81
105
  * @param sourceFile - The file name to normalize, produced by the instrumenter.
82
106
  */
83
107
  normalizeSourceFileName(sourceFile) {
84
- return removePrefix('webpack:///', sourceFile.replace('\\', '/'));
108
+ return (0, commons_1.removePrefix)('webpack:///', sourceFile.replace('\\', '/'));
85
109
  }
86
110
  /**
87
111
  * {@inheritDoc IWriteableStorage.signalUnmappedCoverage}
88
112
  */
89
113
  signalUnmappedCoverage(project) {
90
114
  // Currently only implemented to log the missing information.
91
- this.logger.debug(`Received unmapped coverage for ${project}`);
115
+ this.timesUnmappedCoverage++;
116
+ if (this.timesUnmappedCoverage === 1) {
117
+ this.logger.debug(`Received unmapped coverage for project "${project}"`);
118
+ }
92
119
  }
93
120
  /**
94
121
  * {@inheritDoc IReadableStorage.getCoverageBySourceFile}
@@ -103,7 +130,7 @@ export class DataStorage {
103
130
  dumpToSimpleCoverageFile(filePath) {
104
131
  const toSimpleCoverage = () => {
105
132
  const result = [];
106
- Contract.require(this.getProjects().length < 2, 'Only one project supported to be handled in parallel.');
133
+ commons_1.Contract.require(this.getProjects().length < 2, 'Only one project supported to be handled in parallel.');
107
134
  for (const project of this.getProjects()) {
108
135
  const projectCoverage = this.getCoverageBySourceFile(project);
109
136
  if (!projectCoverage) {
@@ -119,7 +146,7 @@ export class DataStorage {
119
146
  return [result.length, result.join('\n')];
120
147
  };
121
148
  const [lines, content] = toSimpleCoverage();
122
- fs.writeFileSync(filePath, content, 'utf8');
149
+ fs.writeFileSync(filePath.trim(), content, { flag: 'w', encoding: 'utf8' });
123
150
  return lines;
124
151
  }
125
152
  /**
@@ -129,3 +156,4 @@ export class DataStorage {
129
156
  return Array.from(this.coverageByProject.keys());
130
157
  }
131
158
  }
159
+ exports.DataStorage = DataStorage;
package/package.json CHANGED
@@ -1,23 +1,27 @@
1
1
  {
2
2
  "name": "@teamscale/coverage-collector",
3
- "version": "0.0.1-alpha.18",
3
+ "version": "0.0.1-beta.3",
4
4
  "description": "Collector for JavaScript code coverage information",
5
- "main": "dist/main.js",
6
- "bin": "dist/main.js",
7
- "types": "dist/main.d.ts",
5
+ "main": "dist/src/main.js",
6
+ "bin": "dist/src/main.js",
7
+ "types": "dist/src/main.d.ts",
8
8
  "author": "CQSE GmbH",
9
9
  "license": "Apache-2.0",
10
+ "repository": {
11
+ "type": "git",
12
+ "url": "https://github.com/cqse/teamscale-javascript-profiler.git"
13
+ },
10
14
  "scripts": {
11
- "clean": "npx rimraf dist tsconfig.tsbuildinfo",
15
+ "clean": "rimraf dist tsconfig.tsbuildinfo",
12
16
  "build": "tsc",
13
- "serve": "node dist/main.js",
14
- "test": "yarn build && jest --forceExit --coverage --silent=true"
17
+ "serve": "node dist/src/main.js",
18
+ "test": "yarn build && NODE_OPTIONS='--experimental-vm-modules' jest --forceExit --coverage --silent=true --detectOpenHandles"
15
19
  },
16
20
  "files": [
17
21
  "dist/**/*"
18
22
  ],
19
23
  "dependencies": {
20
- "@cqse/commons": "workspace:*",
24
+ "@cqse/commons": "^0.0.1-beta.1",
21
25
  "argparse": "^2.0.1",
22
26
  "async": "^3.2.0",
23
27
  "rxjs": "^7.1.0",
@@ -37,7 +41,9 @@
37
41
  "@types/winston": "^2.4.4",
38
42
  "@types/ws": "^7.4.2",
39
43
  "babel-jest": "^27.2.0",
44
+ "esbuild": "^0.13.4",
40
45
  "jest": "^27.2.0",
46
+ "rimraf": "^3.0.2",
41
47
  "ts-jest": "^27.0.5",
42
48
  "ts-node": "^10.2.1",
43
49
  "typescript": "^4.4.3"
package/dist/main.d.ts DELETED
@@ -1,31 +0,0 @@
1
- #!/usr/bin/env node
2
- /**
3
- * The main class of the Teamscale JavaScript Collector.
4
- * Used to start-up the collector for with a given configuration.
5
- */
6
- export declare class Main {
7
- /**
8
- * Construct the object for parsing the command line arguments.
9
- */
10
- private static buildParser;
11
- /**
12
- * Parse the given command line arguments into a corresponding options object.
13
- */
14
- private static parseArguments;
15
- /**
16
- * Construct the logger.
17
- */
18
- private static buildLogger;
19
- /**
20
- * Entry point of the Teamscale JavaScript Profiler.
21
- */
22
- static run(): void;
23
- /**
24
- * Start a timer for dumping the data, depending on the configuration.
25
- *
26
- * @param config - The config that determines whether or not to do the timed dump.
27
- * @param storage - The storage with the information to dump.
28
- * @param logger - The logger to use.
29
- */
30
- private static maybeStartDumpTimer;
31
- }
package/dist/main.js DELETED
@@ -1,112 +0,0 @@
1
- #!/usr/bin/env node
2
- import { ArgumentParser } from 'argparse';
3
- import { WebSocketCollectingServer } from './receiver/CollectingServer';
4
- import { DataStorage } from './storage/DataStorage';
5
- import { setInterval } from 'timers';
6
- import * as winston from 'winston';
7
- const { version } = require('../package.json');
8
- /**
9
- * The main class of the Teamscale JavaScript Collector.
10
- * Used to start-up the collector for with a given configuration.
11
- */
12
- export class Main {
13
- /**
14
- * Construct the object for parsing the command line arguments.
15
- */
16
- static buildParser() {
17
- const parser = new ArgumentParser({
18
- description: 'Collector of the Teamscale JavaScript Profiler. Collects coverage information from a' +
19
- '(headless) Web browser that executes code instrumented with our instrumenter.'
20
- });
21
- parser.add_argument('-v', '--version', { action: 'version', version });
22
- parser.add_argument('-p', '--port', { help: 'The port to receive coverage information on.', default: 54678 });
23
- parser.add_argument('-f', '--dump-to-file', { help: 'Target file', default: './coverage.simple' });
24
- parser.add_argument('-s', '--dump-after-secs', {
25
- help: 'Dump the coverage information to the target file every N seconds.',
26
- default: 120
27
- });
28
- parser.add_argument('-d', '--debug', {
29
- help: 'Print received coverage information to the terminal?',
30
- default: false
31
- });
32
- return parser;
33
- }
34
- /**
35
- * Parse the given command line arguments into a corresponding options object.
36
- */
37
- static parseArguments() {
38
- const parser = this.buildParser();
39
- return parser.parse_args();
40
- }
41
- /**
42
- * Construct the logger.
43
- */
44
- static buildLogger() {
45
- return winston.createLogger({
46
- level: 'info',
47
- format: winston.format.json(),
48
- defaultMeta: {},
49
- transports: [
50
- //
51
- // - Write all logs with level `error` and below to `error.log`
52
- // - Write all logs with level `info` and below to `combined.log`
53
- //
54
- new winston.transports.File({ filename: 'logs/collector-error.log', level: 'error' }),
55
- new winston.transports.File({ filename: 'logs/collector-combined.log' }),
56
- new winston.transports.Console({ format: winston.format.simple(), level: 'info' })
57
- ]
58
- });
59
- }
60
- /**
61
- * Entry point of the Teamscale JavaScript Profiler.
62
- */
63
- static run() {
64
- const logger = this.buildLogger();
65
- logger.info(`Starting collector in working directory "${process.cwd()}".`);
66
- // Parse the command line arguments
67
- const config = this.parseArguments();
68
- // Prepare the storage and the server
69
- const storage = new DataStorage(logger);
70
- const server = new WebSocketCollectingServer(config.port, storage, logger);
71
- // Start the server socket.
72
- // ATTENTION: The server is executed asynchronously
73
- server.start();
74
- // Optionally, start a timer that dumps the coverage after a N seconds
75
- this.maybeStartDumpTimer(config, storage, logger);
76
- // Say bye bye on CTRL+C and exit the process
77
- process.on('SIGINT', () => {
78
- logger.info('Bye bye.');
79
- process.exit();
80
- });
81
- }
82
- /**
83
- * Start a timer for dumping the data, depending on the configuration.
84
- *
85
- * @param config - The config that determines whether or not to do the timed dump.
86
- * @param storage - The storage with the information to dump.
87
- * @param logger - The logger to use.
88
- */
89
- static maybeStartDumpTimer(config, storage, logger) {
90
- if (config.dump_after_secs > 0) {
91
- const timer = setInterval(() => {
92
- try {
93
- const lines = storage.dumpToSimpleCoverageFile(config.dump_to_file);
94
- logger.info(`Conducted periodic coverage dump with ${lines} lines to ${config.dump_to_file}.`);
95
- }
96
- catch (e) {
97
- logger.error('Timed coverage dump failed.', e);
98
- }
99
- }, config.dump_after_secs * 1000);
100
- process.on('SIGINT', () => {
101
- // Stop the timed file dump
102
- if (timer) {
103
- clearInterval(timer);
104
- }
105
- // ... and do a final dump
106
- logger.info('\nCaught interrupt signal. Writing latest coverage.');
107
- storage.dumpToSimpleCoverageFile(config.dump_to_file);
108
- });
109
- }
110
- }
111
- }
112
- Main.run();
@@ -1,63 +0,0 @@
1
- import { IDataStorage } from '../storage/DataStorage';
2
- import { Logger } from 'winston';
3
- /**
4
- * Various constants that are used to exchange data between
5
- * the instrumented application and the coverage collector.
6
- */
7
- export declare enum ProtocolMessageTypes {
8
- /** A message that provides a source map */
9
- TYPE_SOURCEMAP = "s",
10
- /** A message that provides coverage information */
11
- TYPE_COVERAGE = "c"
12
- }
13
- /**
14
- * A WebSocket based implementation of a coverage receiver.
15
- * Receives coverage from instrumented JavaScript code.
16
- */
17
- export declare class WebSocketCollectingServer {
18
- /**
19
- * The WebSocket server component.
20
- */
21
- private readonly server;
22
- /**
23
- * The storage to put the received coverage information to for aggregation and further processing.
24
- */
25
- private readonly storage;
26
- /**
27
- * The logger to use.
28
- */
29
- private readonly logger;
30
- /**
31
- * Constructor.
32
- *
33
- * @param port - The port the WebSocket server should listen on.
34
- * @param storage - The storage to put the received coverage information to.
35
- * @param logger - The logger to use.
36
- */
37
- constructor(port: number, storage: IDataStorage, logger: Logger);
38
- /**
39
- * Start the server socket, handle sessions and dispatch messages.
40
- */
41
- start(): void;
42
- /**
43
- * Handle a message from a client.
44
- *
45
- * @param session - The session that has been started for the client.
46
- * @param message - The message to handle.
47
- */
48
- private handleMessage;
49
- /**
50
- * Handle a source map message.
51
- *
52
- * @param session - The session to handle the message for.
53
- * @param body - The body of the message (to be parsed).
54
- */
55
- private handleSourcemapMessage;
56
- /**
57
- * Handle a message with coverage information.
58
- *
59
- * @param session - The session to handle the message for.
60
- * @param body - The body of the message (to be parsed).
61
- */
62
- private handleCoverageMessage;
63
- }
@@ -1,120 +0,0 @@
1
- import * as WebSocket from 'ws';
2
- import { Contract } from '@cqse/commons';
3
- import { Session } from './Session';
4
- /**
5
- * Various constants that are used to exchange data between
6
- * the instrumented application and the coverage collector.
7
- */
8
- export var ProtocolMessageTypes;
9
- (function (ProtocolMessageTypes) {
10
- /** A message that provides a source map */
11
- ProtocolMessageTypes["TYPE_SOURCEMAP"] = "s";
12
- /** A message that provides coverage information */
13
- ProtocolMessageTypes["TYPE_COVERAGE"] = "c";
14
- })(ProtocolMessageTypes || (ProtocolMessageTypes = {}));
15
- /**
16
- * Separates the instrumentation subject from the coverage information.
17
- */
18
- const INSTRUMENTATION_SUBJECT_SEPARATOR = ':';
19
- /**
20
- * A WebSocket based implementation of a coverage receiver.
21
- * Receives coverage from instrumented JavaScript code.
22
- */
23
- export class WebSocketCollectingServer {
24
- /**
25
- * Constructor.
26
- *
27
- * @param port - The port the WebSocket server should listen on.
28
- * @param storage - The storage to put the received coverage information to.
29
- * @param logger - The logger to use.
30
- */
31
- constructor(port, storage, logger) {
32
- Contract.require(port > 0 && port < 65536, 'Port must be valid (range).');
33
- this.storage = Contract.requireDefined(storage);
34
- this.logger = Contract.requireDefined(logger);
35
- this.server = new WebSocket.Server({ port: port });
36
- }
37
- /**
38
- * Start the server socket, handle sessions and dispatch messages.
39
- */
40
- start() {
41
- this.logger.info(`Starting server on port ${this.server.options.port}.`);
42
- // Handle new connections from clients
43
- this.server.on('connection', (webSocket, req) => {
44
- let session = new Session(req.socket, this.storage, this.logger);
45
- this.logger.debug(`Connection from: ${req.socket.remoteAddress}`);
46
- // Handle disconnecting clients
47
- webSocket.on('close', code => {
48
- if (session) {
49
- // Free the memory that is associated with the session (important!)
50
- session.destroy();
51
- session = null;
52
- this.logger.debug(`Closing with code ${code}`);
53
- }
54
- });
55
- // Handle incoming messages
56
- webSocket.on('message', (message) => {
57
- if (session && typeof message === 'string') {
58
- this.handleMessage(session, message);
59
- }
60
- });
61
- // Handle errors
62
- webSocket.on('error', (e) => {
63
- this.logger.error('Error on server socket triggered.', e);
64
- });
65
- });
66
- }
67
- /**
68
- * Handle a message from a client.
69
- *
70
- * @param session - The session that has been started for the client.
71
- * @param message - The message to handle.
72
- */
73
- handleMessage(session, message) {
74
- try {
75
- if (message.startsWith(ProtocolMessageTypes.TYPE_SOURCEMAP)) {
76
- this.handleSourcemapMessage(session, message.substring(1));
77
- }
78
- else if (message.startsWith(ProtocolMessageTypes.TYPE_COVERAGE)) {
79
- this.handleCoverageMessage(session, message.substring(1));
80
- }
81
- }
82
- catch (e) {
83
- this.logger.error(`Error while processing message starting with ${message.substring(0, Math.min(50, message.length))}`);
84
- this.logger.error(e.message);
85
- }
86
- }
87
- /**
88
- * Handle a source map message.
89
- *
90
- * @param session - The session to handle the message for.
91
- * @param body - The body of the message (to be parsed).
92
- */
93
- handleSourcemapMessage(session, body) {
94
- const fileIdSeparatorPosition = body.indexOf(INSTRUMENTATION_SUBJECT_SEPARATOR);
95
- if (fileIdSeparatorPosition > -1) {
96
- const fileId = body.substring(0, fileIdSeparatorPosition).trim();
97
- const sourcemap = body.substring(fileIdSeparatorPosition + 1);
98
- session.putSourcemap(fileId, sourcemap);
99
- }
100
- }
101
- /**
102
- * Handle a message with coverage information.
103
- *
104
- * @param session - The session to handle the message for.
105
- * @param body - The body of the message (to be parsed).
106
- */
107
- handleCoverageMessage(session, body) {
108
- var _a;
109
- const bodyPattern = /(?<fileId>\S+) (?<positions>((\d+:\d+)\s+)*(\d+:\d+))/;
110
- const matches = bodyPattern.exec(body);
111
- if (matches === null || matches === void 0 ? void 0 : matches.groups) {
112
- const fileId = matches.groups['fileId'];
113
- const positions = ((_a = matches.groups['positions']) !== null && _a !== void 0 ? _a : '').split(/\s+/);
114
- for (const position of positions) {
115
- const [line, column] = position.split(':');
116
- session.putCoverage(fileId, Number.parseInt(line), Number.parseInt(column));
117
- }
118
- }
119
- }
120
- }
@@ -1,70 +0,0 @@
1
- /// <reference types="node" />
2
- import { Socket } from 'net';
3
- import { IDataStorage } from '../storage/DataStorage';
4
- import { Logger } from 'winston';
5
- /**
6
- * The session maintains the relevant information for a client.
7
- * One session is created for each client.
8
- * The mapping based on sourcemaps is conducted here.
9
- */
10
- export declare class Session {
11
- /**
12
- * The client socket.
13
- */
14
- private readonly socket;
15
- /**
16
- * The storage to forward coverage information to for aggregation.
17
- */
18
- private readonly storage;
19
- /**
20
- * One browser window can load multiple source files, with different
21
- * source maps. However, there might be only one socket to this
22
- * server per browser window.
23
- */
24
- private readonly sourceMaps;
25
- /**
26
- * The logger to use.
27
- */
28
- private readonly logger;
29
- /**
30
- * The project the coverage information is for.
31
- */
32
- private readonly projectId;
33
- /**
34
- * Constructor
35
- *
36
- * @param socket - The client socket.
37
- * @param storage - The storage to store and aggregate coverage information in.
38
- * @param logger - The logger to use.
39
- */
40
- constructor(socket: Socket, storage: IDataStorage, logger: Logger);
41
- /**
42
- * Put coverage information to the storage for aggregation.
43
- * This method also conducts the mapping based on the source map.
44
- *
45
- * @param fileId - The identifier of the instrumented bundle (file).
46
- * @param line - The line number within the bundle.
47
- * @param column - The column within the bundle.
48
- */
49
- putCoverage(fileId: string, line: number, column: number): void;
50
- /**
51
- * Map to the original file position.
52
- *
53
- * @param fileId - The identifier of the instrumented bundle.
54
- * @param line - The line within the bundle.
55
- * @param column - The column within the bundle.
56
- */
57
- private mapToOriginal;
58
- /**
59
- * Receives the source map and stores it to the session.
60
- *
61
- * @param fileId - The identifier of the file bundle.
62
- * @param sourceMapText - The actual source map.
63
- */
64
- putSourcemap(fileId: string, sourceMapText: string): void;
65
- /**
66
- * Destroy the session and free the memory it allocates.
67
- * In particular the sourcemaps are freed (important to not run out of memory!).
68
- */
69
- destroy(): void;
70
- }
@@ -1,86 +0,0 @@
1
- import * as sourceMap from 'source-map';
2
- import { Contract } from '@cqse/commons';
3
- /**
4
- * The session maintains the relevant information for a client.
5
- * One session is created for each client.
6
- * The mapping based on sourcemaps is conducted here.
7
- */
8
- export class Session {
9
- /**
10
- * Constructor
11
- *
12
- * @param socket - The client socket.
13
- * @param storage - The storage to store and aggregate coverage information in.
14
- * @param logger - The logger to use.
15
- */
16
- constructor(socket, storage, logger) {
17
- this.socket = Contract.requireDefined(socket);
18
- this.storage = Contract.requireDefined(storage);
19
- this.logger = Contract.requireDefined(logger);
20
- this.sourceMaps = new Map();
21
- this.projectId = ''; // We currently only support coverage for one project.
22
- }
23
- /**
24
- * Put coverage information to the storage for aggregation.
25
- * This method also conducts the mapping based on the source map.
26
- *
27
- * @param fileId - The identifier of the instrumented bundle (file).
28
- * @param line - The line number within the bundle.
29
- * @param column - The column within the bundle.
30
- */
31
- putCoverage(fileId, line, column) {
32
- const originalPosition = this.mapToOriginal(fileId, line, column);
33
- if (originalPosition.line && originalPosition.source) {
34
- this.storage.putCoverage(this.projectId, originalPosition.source, [originalPosition.line]);
35
- }
36
- else {
37
- this.storage.signalUnmappedCoverage(this.projectId);
38
- }
39
- }
40
- /**
41
- * Map to the original file position.
42
- *
43
- * @param fileId - The identifier of the instrumented bundle.
44
- * @param line - The line within the bundle.
45
- * @param column - The column within the bundle.
46
- */
47
- mapToOriginal(fileId, line, column) {
48
- const sourceMap = this.sourceMaps.get(fileId);
49
- if (sourceMap) {
50
- const position = { line, column };
51
- return sourceMap.originalPositionFor(position);
52
- }
53
- else {
54
- return { line, column, source: null, name: null };
55
- }
56
- }
57
- /**
58
- * Receives the source map and stores it to the session.
59
- *
60
- * @param fileId - The identifier of the file bundle.
61
- * @param sourceMapText - The actual source map.
62
- */
63
- putSourcemap(fileId, sourceMapText) {
64
- const rawSourceMap = JSON.parse(sourceMapText);
65
- new sourceMap.SourceMapConsumer(rawSourceMap)
66
- .then(consumer => {
67
- this.sourceMaps.set(fileId, consumer);
68
- })
69
- .catch(e => {
70
- this.logger.error(`Consuming source map failed! ${e}`);
71
- });
72
- }
73
- /**
74
- * Destroy the session and free the memory it allocates.
75
- * In particular the sourcemaps are freed (important to not run out of memory!).
76
- */
77
- destroy() {
78
- for (const key of Array.from(this.sourceMaps.keys())) {
79
- const value = this.sourceMaps.get(key);
80
- if (value) {
81
- value.destroy();
82
- this.sourceMaps.delete(key);
83
- }
84
- }
85
- }
86
- }
@@ -1,6 +0,0 @@
1
- import { IDataStorage } from "./DataStorage";
2
- export declare abstract class CoveragePersisterBase {
3
- protected readonly _storage: IDataStorage;
4
- constructor(storage: IDataStorage);
5
- abstract finalizePerstistence(): void;
6
- }
@@ -1,10 +0,0 @@
1
- 'use strict';
2
- Object.defineProperty(exports, '__esModule', { value: true });
3
- exports.CoveragePersisterBase = void 0;
4
- const common_qualities_1 = require('@cqse/commons');
5
- class CoveragePersisterBase {
6
- constructor(storage) {
7
- this._storage = common_qualities_1.Contract.requireDefined(storage);
8
- }
9
- }
10
- exports.CoveragePersisterBase = CoveragePersisterBase;
@@ -1,134 +0,0 @@
1
- import { Logger } from 'winston';
2
- /**
3
- * Lines covered for the specified file.
4
- */
5
- export declare type FileCoverage = {
6
- /** Name of the file in the origin */
7
- sourceFile: string;
8
- /** Lines covered */
9
- coveredLines: Set<number>;
10
- };
11
- /**
12
- * Storage interface for reading information.
13
- */
14
- export interface IReadableStorage {
15
- /**
16
- * The list of projects the collector received coverage information for.
17
- */
18
- getProjects(): string[];
19
- /**
20
- * Retrieve the projects' coverage by source file.
21
- */
22
- getCoverageBySourceFile(project: string): IterableIterator<FileCoverage> | undefined;
23
- /**
24
- * Write the coverage to the specified file.
25
- *
26
- * @param filePath - Full path of the file to write the coverage to.
27
- */
28
- dumpToSimpleCoverageFile(filePath: string): void;
29
- }
30
- /**
31
- * Storage interface for writing information.
32
- */
33
- export interface IWriteableStorage {
34
- /**
35
- * Add coverage information to the storage.
36
- *
37
- * @param project - The project to add the information to.
38
- * @param sourceFilePath - The file for that lines are covered.
39
- * @param coveredOriginalLines - The covered lines.
40
- */
41
- putCoverage(project: string, sourceFilePath: string, coveredOriginalLines: number[]): void;
42
- /**
43
- * Signals that we have received coverage information
44
- * for that no mapping based on sourcemaps was possible.
45
- *
46
- * @param project - The project to add the information to.
47
- */
48
- signalUnmappedCoverage(project: string): void;
49
- }
50
- /**
51
- * Union of write and read interface.
52
- */
53
- export interface IDataStorage extends IReadableStorage, IWriteableStorage {
54
- }
55
- /**
56
- * The coverage information received for one particular project.
57
- */
58
- export declare class ProjectCoverage {
59
- /**
60
- * The identifier of the project.
61
- */
62
- private readonly projectId;
63
- /**
64
- * The coverage.
65
- */
66
- private readonly coveredLinesByFile;
67
- /**
68
- * Constructor.
69
- *
70
- * @param projectId - The identifier of the project to collect the coverage for.
71
- */
72
- constructor(projectId: string);
73
- /**
74
- * Put coverage for a single line to the storage.
75
- *
76
- * @param sourceFile - The file in that the line is covered.
77
- * @param line - The line number.
78
- */
79
- putLine(sourceFile: string, line: number): void;
80
- /**
81
- * Returns an iterator over the projects' coverage.
82
- */
83
- getCoverage(): IterableIterator<FileCoverage>;
84
- }
85
- /**
86
- * The data storage which retrieves coverage information.
87
- */
88
- export declare class DataStorage implements IDataStorage {
89
- /**
90
- * Coverage information by project.
91
- */
92
- private readonly coverageByProject;
93
- /**
94
- * Logger to use.
95
- */
96
- private readonly logger;
97
- /**
98
- * Constructs the data storage.
99
- *
100
- * @param logger - The logger to use.
101
- */
102
- constructor(logger: Logger);
103
- /**
104
- * Put coverage into the storage.
105
- *
106
- * @param project - The project to add it to.
107
- * @param sourceFilePath - The covered file.
108
- * @param coveredOriginalLines - The lines covered in the file.
109
- */
110
- putCoverage(project: string, sourceFilePath: string, coveredOriginalLines: number[]): void;
111
- /**
112
- * Normalize the source file names provided by the Web browsers / from the
113
- * instrumentation such that they can be matched to the original source code by Teamscale.
114
- *
115
- * @param sourceFile - The file name to normalize, produced by the instrumenter.
116
- */
117
- private normalizeSourceFileName;
118
- /**
119
- * {@inheritDoc IWriteableStorage.signalUnmappedCoverage}
120
- */
121
- signalUnmappedCoverage(project: string): void;
122
- /**
123
- * {@inheritDoc IReadableStorage.getCoverageBySourceFile}
124
- */
125
- getCoverageBySourceFile(project: string): IterableIterator<FileCoverage> | undefined;
126
- /**
127
- * {@inheritDoc IReadableStorage.writeToSimpleCoverageFile}
128
- */
129
- dumpToSimpleCoverageFile(filePath: string): number;
130
- /**
131
- * {@inheritDoc IReadableStorage.getProjects}
132
- */
133
- getProjects(): string[];
134
- }
@@ -1,130 +0,0 @@
1
- import { Contract, removePrefix } from '@cqse/commons';
2
- import * as fs from 'fs';
3
- /**
4
- * The coverage information received for one particular project.
5
- */
6
- export class ProjectCoverage {
7
- /**
8
- * Constructor.
9
- *
10
- * @param projectId - The identifier of the project to collect the coverage for.
11
- */
12
- constructor(projectId) {
13
- this.projectId = Contract.requireDefined(projectId);
14
- this.coveredLinesByFile = new Map();
15
- }
16
- /**
17
- * Put coverage for a single line to the storage.
18
- *
19
- * @param sourceFile - The file in that the line is covered.
20
- * @param line - The line number.
21
- */
22
- putLine(sourceFile, line) {
23
- let targetSet = this.coveredLinesByFile.get(sourceFile);
24
- if (!targetSet) {
25
- targetSet = new Set();
26
- this.coveredLinesByFile.set(sourceFile, targetSet);
27
- }
28
- targetSet.add(line);
29
- }
30
- /**
31
- * Returns an iterator over the projects' coverage.
32
- */
33
- getCoverage() {
34
- function* iterate(iterable, transform) {
35
- for (const e of iterable) {
36
- yield transform(e);
37
- }
38
- }
39
- return iterate(this.coveredLinesByFile.entries(), ([file, lines]) => {
40
- return {
41
- sourceFile: file,
42
- coveredLines: lines
43
- };
44
- });
45
- }
46
- }
47
- /**
48
- * The data storage which retrieves coverage information.
49
- */
50
- export class DataStorage {
51
- /**
52
- * Constructs the data storage.
53
- *
54
- * @param logger - The logger to use.
55
- */
56
- constructor(logger) {
57
- this.coverageByProject = new Map();
58
- this.logger = Contract.requireDefined(logger);
59
- }
60
- /**
61
- * Put coverage into the storage.
62
- *
63
- * @param project - The project to add it to.
64
- * @param sourceFilePath - The covered file.
65
- * @param coveredOriginalLines - The lines covered in the file.
66
- */
67
- putCoverage(project, sourceFilePath, coveredOriginalLines) {
68
- const uniformPath = this.normalizeSourceFileName(sourceFilePath);
69
- let projectCoverage = this.coverageByProject.get(project);
70
- if (!projectCoverage) {
71
- projectCoverage = new ProjectCoverage(project);
72
- this.coverageByProject.set(project, projectCoverage);
73
- }
74
- coveredOriginalLines.forEach(line => projectCoverage === null || projectCoverage === void 0 ? void 0 : projectCoverage.putLine(sourceFilePath, line));
75
- this.logger.debug(`Mapped Coverage: ${project} ${uniformPath} ${coveredOriginalLines}`);
76
- }
77
- /**
78
- * Normalize the source file names provided by the Web browsers / from the
79
- * instrumentation such that they can be matched to the original source code by Teamscale.
80
- *
81
- * @param sourceFile - The file name to normalize, produced by the instrumenter.
82
- */
83
- normalizeSourceFileName(sourceFile) {
84
- return removePrefix('webpack:///', sourceFile.replace('\\', '/'));
85
- }
86
- /**
87
- * {@inheritDoc IWriteableStorage.signalUnmappedCoverage}
88
- */
89
- signalUnmappedCoverage(project) {
90
- // Currently not used.
91
- }
92
- /**
93
- * {@inheritDoc IReadableStorage.getCoverageBySourceFile}
94
- */
95
- getCoverageBySourceFile(project) {
96
- const projectCoverage = this.coverageByProject.get(project);
97
- return projectCoverage === null || projectCoverage === void 0 ? void 0 : projectCoverage.getCoverage();
98
- }
99
- /**
100
- * {@inheritDoc IReadableStorage.writeToSimpleCoverageFile}
101
- */
102
- dumpToSimpleCoverageFile(filePath) {
103
- const toSimpleCoverage = () => {
104
- const result = [];
105
- Contract.require(this.getProjects().length < 2, 'Only one project supported to be handled in parallel.');
106
- for (const project of this.getProjects()) {
107
- const projectCoverage = this.getCoverageBySourceFile(project);
108
- if (!projectCoverage) {
109
- return [0, ''];
110
- }
111
- for (const entry of projectCoverage) {
112
- result.push(this.normalizeSourceFileName(entry.sourceFile));
113
- for (const lineNo of entry.coveredLines) {
114
- result.push(String(lineNo));
115
- }
116
- }
117
- }
118
- return [result.length, result.join('\n')];
119
- };
120
- const [lines, content] = toSimpleCoverage();
121
- fs.writeFileSync(filePath, content, 'utf8');
122
- return lines;
123
- }
124
- /**
125
- * {@inheritDoc IReadableStorage.getProjects}
126
- */
127
- getProjects() {
128
- return Array.from(this.coverageByProject.keys());
129
- }
130
- }