@teamscale/coverage-collector 0.0.1-alpha.20 → 0.0.1-beta.1
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 +2 -1
- package/dist/package.json +11 -5
- package/dist/src/main.js +32 -21
- package/dist/src/receiver/CollectingServer.js +34 -10
- package/dist/src/receiver/Session.js +29 -6
- package/dist/src/storage/DataStorage.d.ts +4 -0
- package/dist/src/storage/DataStorage.js +38 -10
- package/package.json +7 -5
- package/dist/main.d.ts +0 -31
- package/dist/main.js +0 -112
- package/dist/receiver/CollectingServer.d.ts +0 -63
- package/dist/receiver/CollectingServer.js +0 -120
- package/dist/receiver/Session.d.ts +0 -70
- package/dist/receiver/Session.js +0 -86
- package/dist/storage/CoveragePersiterBase.d.ts +0 -6
- package/dist/storage/CoveragePersiterBase.js +0 -10
- package/dist/storage/DataStorage.d.ts +0 -134
- package/dist/storage/DataStorage.js +0 -130
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
|
|
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-
|
|
3
|
+
"version": "0.0.1-beta.1",
|
|
4
4
|
"description": "Collector for JavaScript code coverage information",
|
|
5
5
|
"main": "dist/main.js",
|
|
6
6
|
"bin": "dist/main.js",
|
|
7
7
|
"types": "dist/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": "
|
|
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": "
|
|
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
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
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
|
-
|
|
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
|
|
45
|
-
level:
|
|
46
|
-
format:
|
|
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
|
|
50
|
-
new
|
|
51
|
-
new
|
|
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
|
-
|
|
102
|
-
|
|
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
|
-
|
|
2
|
-
|
|
3
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
2
|
-
|
|
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
|
-
|
|
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;
|
|
@@ -1,16 +1,38 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@teamscale/coverage-collector",
|
|
3
|
-
"version": "0.0.1-
|
|
3
|
+
"version": "0.0.1-beta.1",
|
|
4
4
|
"description": "Collector for JavaScript code coverage information",
|
|
5
5
|
"main": "dist/main.js",
|
|
6
6
|
"bin": "dist/main.js",
|
|
@@ -12,16 +12,16 @@
|
|
|
12
12
|
"url": "https://github.com/cqse/teamscale-javascript-profiler.git"
|
|
13
13
|
},
|
|
14
14
|
"scripts": {
|
|
15
|
-
"clean": "
|
|
15
|
+
"clean": "rimraf dist tsconfig.tsbuildinfo",
|
|
16
16
|
"build": "tsc",
|
|
17
|
-
"serve": "node dist/main.js",
|
|
18
|
-
"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"
|
|
19
19
|
},
|
|
20
20
|
"files": [
|
|
21
21
|
"dist/**/*"
|
|
22
22
|
],
|
|
23
23
|
"dependencies": {
|
|
24
|
-
"@cqse/commons": "
|
|
24
|
+
"@cqse/commons": "^0.0.1-beta.1",
|
|
25
25
|
"argparse": "^2.0.1",
|
|
26
26
|
"async": "^3.2.0",
|
|
27
27
|
"rxjs": "^7.1.0",
|
|
@@ -41,7 +41,9 @@
|
|
|
41
41
|
"@types/winston": "^2.4.4",
|
|
42
42
|
"@types/ws": "^7.4.2",
|
|
43
43
|
"babel-jest": "^27.2.0",
|
|
44
|
+
"esbuild": "^0.13.4",
|
|
44
45
|
"jest": "^27.2.0",
|
|
46
|
+
"rimraf": "^3.0.2",
|
|
45
47
|
"ts-jest": "^27.0.5",
|
|
46
48
|
"ts-node": "^10.2.1",
|
|
47
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
|
-
}
|
package/dist/receiver/Session.js
DELETED
|
@@ -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,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
|
-
}
|