@teamscale/coverage-collector 0.0.1-beta.42 → 0.0.1-beta.43
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/dist/package.json +5 -2
- package/dist/src/App.d.ts +42 -0
- package/dist/src/App.js +307 -0
- package/dist/src/main.d.ts +1 -37
- package/dist/src/main.js +2 -291
- package/dist/src/receiver/CollectingServer.d.ts +4 -2
- package/dist/src/receiver/CollectingServer.js +18 -10
- package/dist/src/receiver/Session.d.ts +1 -1
- package/dist/src/receiver/Session.js +7 -7
- package/dist/src/storage/DataStorage.d.ts +8 -0
- package/dist/src/storage/DataStorage.js +6 -0
- package/dist/src/utils/ConfigParameters.d.ts +30 -0
- package/dist/src/utils/ConfigParameters.js +83 -0
- package/package.json +5 -2
package/dist/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@teamscale/coverage-collector",
|
|
3
|
-
"version": "0.0.1-beta.
|
|
3
|
+
"version": "0.0.1-beta.43",
|
|
4
4
|
"description": "Collector for JavaScript code coverage information",
|
|
5
5
|
"main": "dist/src/main.js",
|
|
6
6
|
"bin": "dist/src/main.js",
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
"clean": "rimraf dist tsconfig.tsbuildinfo",
|
|
17
17
|
"build": "tsc",
|
|
18
18
|
"collector": "node dist/src/main.js",
|
|
19
|
-
"test": "yarn build && NODE_OPTIONS='--experimental-vm-modules' jest --
|
|
19
|
+
"test": "yarn build && NODE_OPTIONS='--experimental-vm-modules' jest --coverage --silent=true"
|
|
20
20
|
},
|
|
21
21
|
"files": [
|
|
22
22
|
"dist/**/*"
|
|
@@ -29,6 +29,7 @@
|
|
|
29
29
|
"bunyan": "^1.8.15",
|
|
30
30
|
"date-and-time": "^2.3.1",
|
|
31
31
|
"dotenv": "^14.1.0",
|
|
32
|
+
"express": "^4.18.1",
|
|
32
33
|
"form-data": "^4.0.0",
|
|
33
34
|
"mkdirp": "^1.0.4",
|
|
34
35
|
"rxjs": "^7.1.0",
|
|
@@ -43,6 +44,7 @@
|
|
|
43
44
|
"@types/argparse": "^2.0.5",
|
|
44
45
|
"@types/async": "^3.2.6",
|
|
45
46
|
"@types/bunyan": "^1.8.8",
|
|
47
|
+
"@types/express": "^4.17.13",
|
|
46
48
|
"@types/jest": "^27.0.1",
|
|
47
49
|
"@types/mkdirp": "^1.0.2",
|
|
48
50
|
"@types/node": "^15.0.1",
|
|
@@ -52,6 +54,7 @@
|
|
|
52
54
|
"babel-jest": "^27.2.0",
|
|
53
55
|
"esbuild": "^0.13.4",
|
|
54
56
|
"jest": "^27.2.0",
|
|
57
|
+
"mockttp": "^3.1.0",
|
|
55
58
|
"rimraf": "^3.0.2",
|
|
56
59
|
"ts-jest": "^27.0.5",
|
|
57
60
|
"ts-node": "^10.2.1",
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import 'dotenv/config';
|
|
2
|
+
import { ConfigParameters } from './utils/ConfigParameters';
|
|
3
|
+
/**
|
|
4
|
+
* The main class of the Teamscale JavaScript Collector.
|
|
5
|
+
* Used to start the collector with a given configuration.
|
|
6
|
+
*/
|
|
7
|
+
export declare class App {
|
|
8
|
+
/**
|
|
9
|
+
* Parse the given command line arguments into a corresponding options object.
|
|
10
|
+
*/
|
|
11
|
+
private static parseArguments;
|
|
12
|
+
/**
|
|
13
|
+
* Construct the logger.
|
|
14
|
+
*/
|
|
15
|
+
private static buildLogger;
|
|
16
|
+
/**
|
|
17
|
+
* Entry point of the Teamscale JavaScript Profiler.
|
|
18
|
+
*/
|
|
19
|
+
static run(): void;
|
|
20
|
+
/**
|
|
21
|
+
* Run the collector with the given configuration options.
|
|
22
|
+
*
|
|
23
|
+
* @param config - The configuration options to run the collector with.
|
|
24
|
+
*/
|
|
25
|
+
static runWithConfig(config: ConfigParameters): {
|
|
26
|
+
stop: () => Promise<void>;
|
|
27
|
+
};
|
|
28
|
+
/**
|
|
29
|
+
* Start a timer for dumping the data, depending on the configuration.
|
|
30
|
+
*
|
|
31
|
+
* @param config - The config that determines whether to do the timed dump or not.
|
|
32
|
+
* @param storage - The storage with the information to dump.
|
|
33
|
+
* @param logger - The logger to use.
|
|
34
|
+
*/
|
|
35
|
+
private static maybeStartDumpTimer;
|
|
36
|
+
private static dumpCoverage;
|
|
37
|
+
private static uploadToTeamscale;
|
|
38
|
+
private static performTeamscaleUpload;
|
|
39
|
+
private static prepareQueryParameters;
|
|
40
|
+
private static prepareFormData;
|
|
41
|
+
private static startControlServer;
|
|
42
|
+
}
|
package/dist/src/App.js
ADDED
|
@@ -0,0 +1,307 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || function (mod) {
|
|
19
|
+
if (mod && mod.__esModule) return mod;
|
|
20
|
+
var result = {};
|
|
21
|
+
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
|
|
22
|
+
__setModuleDefault(result, mod);
|
|
23
|
+
return result;
|
|
24
|
+
};
|
|
25
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
26
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
27
|
+
};
|
|
28
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
29
|
+
exports.App = void 0;
|
|
30
|
+
const bunyan_1 = __importDefault(require("bunyan"));
|
|
31
|
+
const DataStorage_1 = require("./storage/DataStorage");
|
|
32
|
+
const CollectingServer_1 = require("./receiver/CollectingServer");
|
|
33
|
+
require("dotenv/config");
|
|
34
|
+
const fs = __importStar(require("fs"));
|
|
35
|
+
const axios_1 = __importDefault(require("axios"));
|
|
36
|
+
const form_data_1 = __importDefault(require("form-data"));
|
|
37
|
+
const QueryParameters_1 = __importDefault(require("./utils/QueryParameters"));
|
|
38
|
+
const ConfigParameters_1 = require("./utils/ConfigParameters");
|
|
39
|
+
const util_1 = require("util");
|
|
40
|
+
const mkdirp_1 = __importDefault(require("mkdirp"));
|
|
41
|
+
const path_1 = __importDefault(require("path"));
|
|
42
|
+
const StdConsoleLogger_1 = require("./utils/StdConsoleLogger");
|
|
43
|
+
const PrettyFileLogger_1 = require("./utils/PrettyFileLogger");
|
|
44
|
+
const express_1 = __importDefault(require("express"));
|
|
45
|
+
/**
|
|
46
|
+
* Error that is thrown when the upload to Teamscale failed
|
|
47
|
+
*/
|
|
48
|
+
class TeamscaleUploadError extends Error {
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* The main class of the Teamscale JavaScript Collector.
|
|
52
|
+
* Used to start the collector with a given configuration.
|
|
53
|
+
*/
|
|
54
|
+
class App {
|
|
55
|
+
/**
|
|
56
|
+
* Parse the given command line arguments into a corresponding options object.
|
|
57
|
+
*/
|
|
58
|
+
static parseArguments() {
|
|
59
|
+
const parser = (0, ConfigParameters_1.buildParameterParser)();
|
|
60
|
+
return parser.parse_args();
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Construct the logger.
|
|
64
|
+
*/
|
|
65
|
+
static buildLogger(config) {
|
|
66
|
+
const logfilePath = config.log_to_file.trim();
|
|
67
|
+
mkdirp_1.default.sync(path_1.default.dirname(logfilePath));
|
|
68
|
+
const logLevel = config.log_level;
|
|
69
|
+
const logger = bunyan_1.default.createLogger({
|
|
70
|
+
name: 'Collector',
|
|
71
|
+
streams: [
|
|
72
|
+
// console output
|
|
73
|
+
{ level: logLevel, stream: new StdConsoleLogger_1.StdConsoleLogger(), type: 'raw' },
|
|
74
|
+
// default log file
|
|
75
|
+
{ level: logLevel, stream: new PrettyFileLogger_1.PrettyFileLogger(fs.createWriteStream(logfilePath)), type: 'raw' }
|
|
76
|
+
]
|
|
77
|
+
});
|
|
78
|
+
// If the given flag is set, we also log with a JSON-like format
|
|
79
|
+
if (config.json_log) {
|
|
80
|
+
logger.addStream({ level: logLevel, path: `${logfilePath}.json` });
|
|
81
|
+
}
|
|
82
|
+
return logger;
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Entry point of the Teamscale JavaScript Profiler.
|
|
86
|
+
*/
|
|
87
|
+
static run() {
|
|
88
|
+
// Parse the command line arguments
|
|
89
|
+
const config = this.parseArguments();
|
|
90
|
+
App.runWithConfig(config);
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Run the collector with the given configuration options.
|
|
94
|
+
*
|
|
95
|
+
* @param config - The configuration options to run the collector with.
|
|
96
|
+
*/
|
|
97
|
+
static runWithConfig(config) {
|
|
98
|
+
// Build the logger
|
|
99
|
+
const logger = this.buildLogger(config);
|
|
100
|
+
logger.info(`Starting collector in working directory "${process.cwd()}".`);
|
|
101
|
+
logger.info(`Logging "${config.log_level}" to "${config.log_to_file}".`);
|
|
102
|
+
// Prepare the storage and the server
|
|
103
|
+
const storage = new DataStorage_1.DataStorage(logger);
|
|
104
|
+
const server = new CollectingServer_1.WebSocketCollectingServer(config.port, storage, logger);
|
|
105
|
+
// Enable the remote control API if configured
|
|
106
|
+
const controlServerState = this.startControlServer(config, storage, logger);
|
|
107
|
+
// Start the server socket.
|
|
108
|
+
// ATTENTION: The server is executed asynchronously
|
|
109
|
+
const serverState = server.start();
|
|
110
|
+
// Optionally, start a timer that dumps the coverage after a N seconds
|
|
111
|
+
const timerState = this.maybeStartDumpTimer(config, storage, logger);
|
|
112
|
+
// Say bye bye on CTRL+C and exit the process
|
|
113
|
+
process.on('SIGINT', async () => {
|
|
114
|
+
// ... and do a final dump before.
|
|
115
|
+
await this.dumpCoverage(config, storage, logger);
|
|
116
|
+
logger.info('Bye bye.');
|
|
117
|
+
process.exit();
|
|
118
|
+
});
|
|
119
|
+
return {
|
|
120
|
+
async stop() {
|
|
121
|
+
logger.info('Stopping the collector.');
|
|
122
|
+
timerState.stop();
|
|
123
|
+
await controlServerState.stop();
|
|
124
|
+
serverState.stop();
|
|
125
|
+
}
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* Start a timer for dumping the data, depending on the configuration.
|
|
130
|
+
*
|
|
131
|
+
* @param config - The config that determines whether to do the timed dump or not.
|
|
132
|
+
* @param storage - The storage with the information to dump.
|
|
133
|
+
* @param logger - The logger to use.
|
|
134
|
+
*/
|
|
135
|
+
static maybeStartDumpTimer(config, storage, logger) {
|
|
136
|
+
if (config.dump_after_mins > 0) {
|
|
137
|
+
logger.info(`Will dump coverage information every ${config.dump_after_mins} minute(s).`);
|
|
138
|
+
const timer = setInterval(async () => {
|
|
139
|
+
await this.dumpCoverage(config, storage, logger);
|
|
140
|
+
}, config.dump_after_mins * 1000 * 60);
|
|
141
|
+
process.on('SIGINT', () => {
|
|
142
|
+
// Stop the timed file dump
|
|
143
|
+
if (timer) {
|
|
144
|
+
clearInterval(timer);
|
|
145
|
+
}
|
|
146
|
+
});
|
|
147
|
+
return {
|
|
148
|
+
stop: () => clearInterval(timer)
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
return {
|
|
152
|
+
stop() {
|
|
153
|
+
// no timer to stop yet
|
|
154
|
+
}
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
static async dumpCoverage(config, storage, logger) {
|
|
158
|
+
try {
|
|
159
|
+
// 1. Write coverage to a file
|
|
160
|
+
const [coverageFile, lines] = storage.dumpToSimpleCoverageFile(config.dump_to_folder, new Date());
|
|
161
|
+
logger.info(`Dumped ${lines} lines of coverage to ${coverageFile}.`);
|
|
162
|
+
// 2. Upload to Teamscale if configured
|
|
163
|
+
if (config.teamscale_server_url) {
|
|
164
|
+
await this.uploadToTeamscale(config, logger, coverageFile, lines);
|
|
165
|
+
// Delete coverage if upload was successful and keeping coverage files on disk was not configure by the user
|
|
166
|
+
if (!config.keep_coverage_files) {
|
|
167
|
+
fs.unlinkSync(coverageFile);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
catch (e) {
|
|
172
|
+
if (e instanceof TeamscaleUploadError) {
|
|
173
|
+
logger.error(`Teamscale upload failed. The coverage files on disk (inside the folder "${config.dump_to_folder}") were not deleted.
|
|
174
|
+
You can still upload them manually.`, e);
|
|
175
|
+
}
|
|
176
|
+
else {
|
|
177
|
+
logger.error('Coverage dump failed.', e);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
static async uploadToTeamscale(config, logger, coverageFile, lines) {
|
|
182
|
+
if (!(config.teamscale_access_token && config.teamscale_user && config.teamscale_server_url)) {
|
|
183
|
+
throw new TeamscaleUploadError('API key and user name must be configured!');
|
|
184
|
+
}
|
|
185
|
+
if (lines === 0) {
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
logger.info('Preparing upload to Teamscale');
|
|
189
|
+
const form = this.prepareFormData(coverageFile);
|
|
190
|
+
const queryParameters = this.prepareQueryParameters(config);
|
|
191
|
+
await this.performTeamscaleUpload(config, queryParameters, form, logger);
|
|
192
|
+
}
|
|
193
|
+
static async performTeamscaleUpload(config, parameters, form, logger) {
|
|
194
|
+
var _a, _b, _c;
|
|
195
|
+
try {
|
|
196
|
+
const response = await axios_1.default
|
|
197
|
+
.post(`${(_a = config.teamscale_server_url) === null || _a === void 0 ? void 0 : _a.replace(/\/$/, '')}/api/projects/${config.teamscale_project}/external-analysis/session/auto-create/report?${parameters.toString()}`, form, {
|
|
198
|
+
auth: {
|
|
199
|
+
username: (_b = config.teamscale_user) !== null && _b !== void 0 ? _b : 'no username provided',
|
|
200
|
+
password: (_c = config.teamscale_access_token) !== null && _c !== void 0 ? _c : 'no password provided'
|
|
201
|
+
},
|
|
202
|
+
headers: {
|
|
203
|
+
Accept: '*/*',
|
|
204
|
+
'Content-Type': `multipart/form-data; boundary=${form.getBoundary()}`
|
|
205
|
+
}
|
|
206
|
+
});
|
|
207
|
+
logger.info(`Upload finished with code ${response.status}.`);
|
|
208
|
+
}
|
|
209
|
+
catch (error) {
|
|
210
|
+
if (error.response) {
|
|
211
|
+
const response = error.response;
|
|
212
|
+
if (response.status >= 400) {
|
|
213
|
+
throw new TeamscaleUploadError(`Upload failed with code ${response.status}: ${response.statusText}. Response Data: ${response.data}`);
|
|
214
|
+
}
|
|
215
|
+
else {
|
|
216
|
+
logger.info(`Upload with status code ${response.status} finished.`);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
else if (error.request) {
|
|
220
|
+
throw new TeamscaleUploadError(`Upload request did not receive a response.`);
|
|
221
|
+
}
|
|
222
|
+
if (error.message) {
|
|
223
|
+
logger.debug(`Something went wrong when uploading data: ${error.message}. Details of the error: ${(0, util_1.inspect)(error)}`);
|
|
224
|
+
throw new TeamscaleUploadError(`Something went wrong when uploading data: ${error.message}`);
|
|
225
|
+
}
|
|
226
|
+
else {
|
|
227
|
+
throw new TeamscaleUploadError(`Something went wrong when uploading data: ${(0, util_1.inspect)(error)}`);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
static prepareQueryParameters(config) {
|
|
232
|
+
const parameters = new QueryParameters_1.default();
|
|
233
|
+
parameters.addIfDefined('format', 'SIMPLE');
|
|
234
|
+
parameters.addIfDefined('message', config.teamscale_message);
|
|
235
|
+
parameters.addIfDefined('repository', config.teamscale_repository);
|
|
236
|
+
parameters.addIfDefined('t', config.teamscale_commit);
|
|
237
|
+
parameters.addIfDefined('revision', config.teamscale_revision);
|
|
238
|
+
parameters.addIfDefined('partition', config.teamscale_partition);
|
|
239
|
+
return parameters;
|
|
240
|
+
}
|
|
241
|
+
static prepareFormData(coverageFile) {
|
|
242
|
+
const form = new form_data_1.default();
|
|
243
|
+
form.append('report', fs.createReadStream(coverageFile), 'coverage.simple');
|
|
244
|
+
return form;
|
|
245
|
+
}
|
|
246
|
+
static startControlServer(config, storage, logger) {
|
|
247
|
+
if (!config.enable_control_port) {
|
|
248
|
+
return {
|
|
249
|
+
async stop() {
|
|
250
|
+
// nothing to stop in this case
|
|
251
|
+
}
|
|
252
|
+
};
|
|
253
|
+
}
|
|
254
|
+
let controlServer = (0, express_1.default)();
|
|
255
|
+
controlServer.use(express_1.default.text({}));
|
|
256
|
+
let serverSocket = controlServer.listen(config.enable_control_port);
|
|
257
|
+
controlServer.put('/partition', (request, response) => {
|
|
258
|
+
const targetPartition = request.body.trim();
|
|
259
|
+
config.teamscale_partition = targetPartition;
|
|
260
|
+
logger.info(`Switched the target partition to '${targetPartition}' via the control API.`);
|
|
261
|
+
response.sendStatus(200);
|
|
262
|
+
});
|
|
263
|
+
controlServer.post('/dump', async (request, response) => {
|
|
264
|
+
logger.info('Dumping coverage requested via the control API.');
|
|
265
|
+
await this.dumpCoverage(config, storage, logger);
|
|
266
|
+
response.sendStatus(200);
|
|
267
|
+
});
|
|
268
|
+
controlServer.put('/project', async (request, response) => {
|
|
269
|
+
const targetProject = request.body.trim();
|
|
270
|
+
config.teamscale_project = targetProject;
|
|
271
|
+
logger.info(`Switching the target project to '${targetProject}' via the control API.`);
|
|
272
|
+
response.sendStatus(200);
|
|
273
|
+
});
|
|
274
|
+
controlServer.put('/revision', async (request, response) => {
|
|
275
|
+
const targetRevision = request.body.trim();
|
|
276
|
+
config.teamscale_revision = targetRevision;
|
|
277
|
+
logger.info(`Switching the target revision to '${targetRevision}' via the control API.`);
|
|
278
|
+
response.sendStatus(200);
|
|
279
|
+
});
|
|
280
|
+
controlServer.put('/commit', async (request, response) => {
|
|
281
|
+
const targetCommit = request.body.trim();
|
|
282
|
+
config.teamscale_commit = targetCommit;
|
|
283
|
+
logger.info(`Switching the target commit to '${targetCommit}' via the control API.`);
|
|
284
|
+
response.sendStatus(200);
|
|
285
|
+
});
|
|
286
|
+
controlServer.put('/message', async (request, response) => {
|
|
287
|
+
const uploadMessage = request.body.trim();
|
|
288
|
+
config.teamscale_message = uploadMessage;
|
|
289
|
+
logger.info(`Switching the upload message to '${uploadMessage}' via the control API.`);
|
|
290
|
+
response.sendStatus(200);
|
|
291
|
+
});
|
|
292
|
+
controlServer.post('/reset', async (request, response) => {
|
|
293
|
+
storage.discardCollectedCoverage();
|
|
294
|
+
logger.info(`Discarding collected coverage information as requested via the control API.`);
|
|
295
|
+
response.sendStatus(200);
|
|
296
|
+
});
|
|
297
|
+
logger.info(`Control server enabled at port ${config.enable_control_port}`);
|
|
298
|
+
return {
|
|
299
|
+
async stop() {
|
|
300
|
+
return new Promise(resolve => {
|
|
301
|
+
serverSocket.close(() => resolve());
|
|
302
|
+
});
|
|
303
|
+
}
|
|
304
|
+
};
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
exports.App = App;
|
package/dist/src/main.d.ts
CHANGED
|
@@ -1,38 +1,2 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* The main class of the Teamscale JavaScript Collector.
|
|
5
|
-
* Used to start the collector for with a given configuration.
|
|
6
|
-
*/
|
|
7
|
-
export declare class Main {
|
|
8
|
-
private static readonly DEFAULT_COVERAGE_LOCATION;
|
|
9
|
-
/**
|
|
10
|
-
* Construct the object for parsing the command line arguments.
|
|
11
|
-
*/
|
|
12
|
-
private static buildParser;
|
|
13
|
-
/**
|
|
14
|
-
* Parse the given command line arguments into a corresponding options object.
|
|
15
|
-
*/
|
|
16
|
-
private static parseArguments;
|
|
17
|
-
/**
|
|
18
|
-
* Construct the logger.
|
|
19
|
-
*/
|
|
20
|
-
private static buildLogger;
|
|
21
|
-
/**
|
|
22
|
-
* Entry point of the Teamscale JavaScript Profiler.
|
|
23
|
-
*/
|
|
24
|
-
static run(): void;
|
|
25
|
-
/**
|
|
26
|
-
* Start a timer for dumping the data, depending on the configuration.
|
|
27
|
-
*
|
|
28
|
-
* @param config - The config that determines whether to do the timed dump or not.
|
|
29
|
-
* @param storage - The storage with the information to dump.
|
|
30
|
-
* @param logger - The logger to use.
|
|
31
|
-
*/
|
|
32
|
-
private static maybeStartDumpTimer;
|
|
33
|
-
private static dumpCoverage;
|
|
34
|
-
private static uploadToTeamscale;
|
|
35
|
-
private static performTeamscaleUpload;
|
|
36
|
-
private static prepareQueryParameters;
|
|
37
|
-
private static prepareFormData;
|
|
38
|
-
}
|
|
2
|
+
export {};
|
package/dist/src/main.js
CHANGED
|
@@ -1,294 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
"use strict";
|
|
3
|
-
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
4
|
-
if (k2 === undefined) k2 = k;
|
|
5
|
-
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
6
|
-
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
7
|
-
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
8
|
-
}
|
|
9
|
-
Object.defineProperty(o, k2, desc);
|
|
10
|
-
}) : (function(o, m, k, k2) {
|
|
11
|
-
if (k2 === undefined) k2 = k;
|
|
12
|
-
o[k2] = m[k];
|
|
13
|
-
}));
|
|
14
|
-
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
15
|
-
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
16
|
-
}) : function(o, v) {
|
|
17
|
-
o["default"] = v;
|
|
18
|
-
});
|
|
19
|
-
var __importStar = (this && this.__importStar) || function (mod) {
|
|
20
|
-
if (mod && mod.__esModule) return mod;
|
|
21
|
-
var result = {};
|
|
22
|
-
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
|
|
23
|
-
__setModuleDefault(result, mod);
|
|
24
|
-
return result;
|
|
25
|
-
};
|
|
26
|
-
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
27
|
-
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
28
|
-
};
|
|
29
3
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
const argparse_1 = require("argparse");
|
|
33
|
-
const bunyan_1 = __importDefault(require("bunyan"));
|
|
34
|
-
const DataStorage_1 = require("./storage/DataStorage");
|
|
35
|
-
const CollectingServer_1 = require("./receiver/CollectingServer");
|
|
36
|
-
require("dotenv/config");
|
|
37
|
-
const fs = __importStar(require("fs"));
|
|
38
|
-
const axios_1 = __importDefault(require("axios"));
|
|
39
|
-
const form_data_1 = __importDefault(require("form-data"));
|
|
40
|
-
const QueryParameters_1 = __importDefault(require("./utils/QueryParameters"));
|
|
41
|
-
const util_1 = require("util");
|
|
42
|
-
const mkdirp_1 = __importDefault(require("mkdirp"));
|
|
43
|
-
const path_1 = __importDefault(require("path"));
|
|
44
|
-
const StdConsoleLogger_1 = require("./utils/StdConsoleLogger");
|
|
45
|
-
const PrettyFileLogger_1 = require("./utils/PrettyFileLogger");
|
|
46
|
-
/**
|
|
47
|
-
* Error that is thrown when the upload to Teamscale failed
|
|
48
|
-
*/
|
|
49
|
-
class TeamscaleUploadError extends Error {
|
|
50
|
-
}
|
|
51
|
-
/**
|
|
52
|
-
* The main class of the Teamscale JavaScript Collector.
|
|
53
|
-
* Used to start the collector for with a given configuration.
|
|
54
|
-
*/
|
|
55
|
-
class Main {
|
|
56
|
-
/**
|
|
57
|
-
* Construct the object for parsing the command line arguments.
|
|
58
|
-
*/
|
|
59
|
-
static buildParser() {
|
|
60
|
-
var _a;
|
|
61
|
-
const parser = new argparse_1.ArgumentParser({
|
|
62
|
-
description: 'Collector of the Teamscale JavaScript Profiler. Collects coverage information from a' +
|
|
63
|
-
'(headless) Web browser that executes code instrumented with our instrumenter.'
|
|
64
|
-
});
|
|
65
|
-
parser.add_argument('-v', '--version', { action: 'version', version: package_json_1.version });
|
|
66
|
-
parser.add_argument('-p', '--port', { help: 'The port to receive coverage information on.', default: 54678 });
|
|
67
|
-
parser.add_argument('-f', '--dump-to-folder', {
|
|
68
|
-
help: 'Target folder for coverage files.',
|
|
69
|
-
default: this.DEFAULT_COVERAGE_LOCATION
|
|
70
|
-
});
|
|
71
|
-
parser.add_argument('-k', '--keep-coverage-files', {
|
|
72
|
-
help: 'Whether to keep the coverage files on disk after a successful upload to Teamsacle',
|
|
73
|
-
action: 'store_true',
|
|
74
|
-
default: false
|
|
75
|
-
});
|
|
76
|
-
parser.add_argument('-l', '--log-to-file', { help: 'Log file', default: 'logs/collector-combined.log' });
|
|
77
|
-
parser.add_argument('-e', '--log-level', { help: 'Log level', default: 'info' });
|
|
78
|
-
parser.add_argument('-t', '--dump-after-mins', {
|
|
79
|
-
help: 'Dump the coverage information to the target file every N minutes.',
|
|
80
|
-
default: 360
|
|
81
|
-
});
|
|
82
|
-
parser.add_argument('-d', '--debug', {
|
|
83
|
-
help: 'Print received coverage information to the terminal?',
|
|
84
|
-
default: false
|
|
85
|
-
});
|
|
86
|
-
parser.add_argument('-j', '--json-log', {
|
|
87
|
-
help: 'Additional JSON-like log file format.',
|
|
88
|
-
action: 'store_true'
|
|
89
|
-
});
|
|
90
|
-
// Parameters for the upload to Teamscale
|
|
91
|
-
parser.add_argument('-u', '--teamscale-server-url', {
|
|
92
|
-
help: 'Upload the coverage to the given Teamscale server URL, for example, https://teamscale.dev.example.com:8080/production.',
|
|
93
|
-
default: process.env.TEAMSCALE_SERVER_URL
|
|
94
|
-
});
|
|
95
|
-
parser.add_argument('--teamscale-access-token', {
|
|
96
|
-
help: 'The API key to use for uploading to Teamscale.',
|
|
97
|
-
default: process.env.TEAMSCALE_ACCESS_TOKEN
|
|
98
|
-
});
|
|
99
|
-
parser.add_argument('--teamscale-project', {
|
|
100
|
-
help: 'The project ID to upload coverage to.',
|
|
101
|
-
default: process.env.TEAMSCALE_PROJECT
|
|
102
|
-
});
|
|
103
|
-
parser.add_argument('--teamscale-user', {
|
|
104
|
-
help: 'The user for uploading coverage to Teamscale.',
|
|
105
|
-
default: process.env.TEAMSCALE_USER
|
|
106
|
-
});
|
|
107
|
-
parser.add_argument('--teamscale-partition', {
|
|
108
|
-
help: 'The partition to upload coverage to.',
|
|
109
|
-
default: process.env.TEAMSCALE_PARTITION
|
|
110
|
-
});
|
|
111
|
-
parser.add_argument('--teamscale-revision', {
|
|
112
|
-
help: 'The revision (commit hash, version id) to upload coverage for.',
|
|
113
|
-
default: process.env.TEAMSCALE_REVISION
|
|
114
|
-
});
|
|
115
|
-
parser.add_argument('--teamscale-commit', {
|
|
116
|
-
help: 'The branch and timestamp to upload coverage for, separated by colon.',
|
|
117
|
-
default: process.env.TEAMSCALE_COMMIT
|
|
118
|
-
});
|
|
119
|
-
parser.add_argument('--teamscale-repository', {
|
|
120
|
-
help: 'The repository to upload coverage for. Optional: Only needed when uploading via revision to a project that has more than one connector.',
|
|
121
|
-
default: process.env.TEAMSCALE_REPOSITORY
|
|
122
|
-
});
|
|
123
|
-
parser.add_argument('--teamscale-message', {
|
|
124
|
-
help: 'The commit message shown within Teamscale for the coverage upload. Default is "JavaScript coverage upload".',
|
|
125
|
-
default: (_a = process.env.TEAMSCALE_MESSAGE) !== null && _a !== void 0 ? _a : 'JavaScript coverage upload'
|
|
126
|
-
});
|
|
127
|
-
return parser;
|
|
128
|
-
}
|
|
129
|
-
/**
|
|
130
|
-
* Parse the given command line arguments into a corresponding options object.
|
|
131
|
-
*/
|
|
132
|
-
static parseArguments() {
|
|
133
|
-
const parser = this.buildParser();
|
|
134
|
-
return parser.parse_args();
|
|
135
|
-
}
|
|
136
|
-
/**
|
|
137
|
-
* Construct the logger.
|
|
138
|
-
*/
|
|
139
|
-
static buildLogger(config) {
|
|
140
|
-
const logfilePath = config.log_to_file.trim();
|
|
141
|
-
mkdirp_1.default.sync(path_1.default.dirname(logfilePath));
|
|
142
|
-
const logLevel = config.log_level;
|
|
143
|
-
const logger = bunyan_1.default.createLogger({
|
|
144
|
-
name: 'Collector',
|
|
145
|
-
streams: [
|
|
146
|
-
// console output
|
|
147
|
-
{ level: logLevel, stream: new StdConsoleLogger_1.StdConsoleLogger(), type: 'raw' },
|
|
148
|
-
// default log file
|
|
149
|
-
{ level: logLevel, stream: new PrettyFileLogger_1.PrettyFileLogger(fs.createWriteStream(logfilePath)), type: 'raw' }
|
|
150
|
-
]
|
|
151
|
-
});
|
|
152
|
-
// If the given flag is set, we also log with a JSON-like format
|
|
153
|
-
if (config.json_log) {
|
|
154
|
-
logger.addStream({ level: logLevel, path: `${logfilePath}.json` });
|
|
155
|
-
}
|
|
156
|
-
return logger;
|
|
157
|
-
}
|
|
158
|
-
/**
|
|
159
|
-
* Entry point of the Teamscale JavaScript Profiler.
|
|
160
|
-
*/
|
|
161
|
-
static run() {
|
|
162
|
-
// Parse the command line arguments
|
|
163
|
-
const config = this.parseArguments();
|
|
164
|
-
// Build the logger
|
|
165
|
-
const logger = this.buildLogger(config);
|
|
166
|
-
logger.info(`Starting collector in working directory "${process.cwd()}".`);
|
|
167
|
-
logger.info(`Logging "${config.log_level}" to "${config.log_to_file}".`);
|
|
168
|
-
// Prepare the storage and the server
|
|
169
|
-
const storage = new DataStorage_1.DataStorage(logger);
|
|
170
|
-
const server = new CollectingServer_1.WebSocketCollectingServer(config.port, storage, logger);
|
|
171
|
-
// Start the server socket.
|
|
172
|
-
// ATTENTION: The server is executed asynchronously
|
|
173
|
-
server.start();
|
|
174
|
-
// Optionally, start a timer that dumps the coverage after a N seconds
|
|
175
|
-
this.maybeStartDumpTimer(config, storage, logger);
|
|
176
|
-
// Say bye bye on CTRL+C and exit the process
|
|
177
|
-
process.on('SIGINT', async () => {
|
|
178
|
-
// ... and do a final dump before.
|
|
179
|
-
await this.dumpCoverage(config, storage, logger).then();
|
|
180
|
-
logger.info('Bye bye.');
|
|
181
|
-
process.exit();
|
|
182
|
-
});
|
|
183
|
-
}
|
|
184
|
-
/**
|
|
185
|
-
* Start a timer for dumping the data, depending on the configuration.
|
|
186
|
-
*
|
|
187
|
-
* @param config - The config that determines whether to do the timed dump or not.
|
|
188
|
-
* @param storage - The storage with the information to dump.
|
|
189
|
-
* @param logger - The logger to use.
|
|
190
|
-
*/
|
|
191
|
-
static maybeStartDumpTimer(config, storage, logger) {
|
|
192
|
-
if (config.dump_after_mins > 0) {
|
|
193
|
-
logger.info(`Will dump coverage information every ${config.dump_after_mins} minute(s).`);
|
|
194
|
-
const timer = setInterval(() => {
|
|
195
|
-
this.dumpCoverage(config, storage, logger).then();
|
|
196
|
-
}, config.dump_after_mins * 1000 * 60);
|
|
197
|
-
process.on('SIGINT', () => {
|
|
198
|
-
// Stop the timed file dump
|
|
199
|
-
if (timer) {
|
|
200
|
-
clearInterval(timer);
|
|
201
|
-
}
|
|
202
|
-
});
|
|
203
|
-
}
|
|
204
|
-
}
|
|
205
|
-
static async dumpCoverage(config, storage, logger) {
|
|
206
|
-
try {
|
|
207
|
-
// 1. Write coverage to a file
|
|
208
|
-
const [coverageFile, lines] = storage.dumpToSimpleCoverageFile(config.dump_to_folder, new Date());
|
|
209
|
-
logger.info(`Dumped ${lines} lines of coverage to ${coverageFile}.`);
|
|
210
|
-
// 2. Upload to Teamscale if configured
|
|
211
|
-
if (config.teamscale_server_url) {
|
|
212
|
-
await this.uploadToTeamscale(config, logger, coverageFile, lines);
|
|
213
|
-
// Delete coverage if upload was successful and keeping coverage files on disk was not configure by the user
|
|
214
|
-
if (!config.keep_coverage_files) {
|
|
215
|
-
fs.unlinkSync(coverageFile);
|
|
216
|
-
}
|
|
217
|
-
}
|
|
218
|
-
}
|
|
219
|
-
catch (e) {
|
|
220
|
-
if (e instanceof TeamscaleUploadError) {
|
|
221
|
-
logger.error(`Teamscale upload failed. The coverage files on disk (inside the folder "${config.dump_to_folder}") were not deleted.
|
|
222
|
-
You can still upload them manually.`, e);
|
|
223
|
-
}
|
|
224
|
-
else {
|
|
225
|
-
logger.error('Coverage dump failed.', e);
|
|
226
|
-
}
|
|
227
|
-
}
|
|
228
|
-
}
|
|
229
|
-
static async uploadToTeamscale(config, logger, coverageFile, lines) {
|
|
230
|
-
if (!(config.teamscale_access_token && config.teamscale_user && config.teamscale_server_url)) {
|
|
231
|
-
throw new TeamscaleUploadError('API key and user name must be configured!');
|
|
232
|
-
}
|
|
233
|
-
if (lines === 0) {
|
|
234
|
-
return;
|
|
235
|
-
}
|
|
236
|
-
logger.info('Preparing upload to Teamscale');
|
|
237
|
-
const form = this.prepareFormData(coverageFile);
|
|
238
|
-
const queryParameters = this.prepareQueryParameters(config);
|
|
239
|
-
await this.performTeamscaleUpload(config, queryParameters, form, logger);
|
|
240
|
-
}
|
|
241
|
-
static async performTeamscaleUpload(config, parameters, form, logger) {
|
|
242
|
-
var _a, _b, _c;
|
|
243
|
-
await axios_1.default
|
|
244
|
-
.post(`${(_a = config.teamscale_server_url) === null || _a === void 0 ? void 0 : _a.replace(/\/$/, '')}/api/projects/${config.teamscale_project}/external-analysis/session/auto-create/report?${parameters.toString()}`, form, {
|
|
245
|
-
auth: {
|
|
246
|
-
username: (_b = config.teamscale_user) !== null && _b !== void 0 ? _b : 'no username provided',
|
|
247
|
-
password: (_c = config.teamscale_access_token) !== null && _c !== void 0 ? _c : 'no password provided'
|
|
248
|
-
},
|
|
249
|
-
headers: {
|
|
250
|
-
Accept: '*/*',
|
|
251
|
-
'Content-Type': `multipart/form-data; boundary=${form.getBoundary()}`
|
|
252
|
-
}
|
|
253
|
-
})
|
|
254
|
-
.catch(function (error) {
|
|
255
|
-
if (error.response) {
|
|
256
|
-
const response = error.response;
|
|
257
|
-
if (response.status >= 400) {
|
|
258
|
-
throw new TeamscaleUploadError(`Upload failed with code ${response.status}: ${response.statusText}. Response Data: ${response.data}`);
|
|
259
|
-
}
|
|
260
|
-
else {
|
|
261
|
-
logger.info(`Upload with status code ${response.status} finished.`);
|
|
262
|
-
}
|
|
263
|
-
}
|
|
264
|
-
else if (error.request) {
|
|
265
|
-
throw new TeamscaleUploadError(`Upload request did not receive a response.`);
|
|
266
|
-
}
|
|
267
|
-
if (error.message) {
|
|
268
|
-
logger.debug(`Something went wrong when uploading data: ${error.message}. Details of the error: ${(0, util_1.inspect)(error)}`);
|
|
269
|
-
throw new TeamscaleUploadError(`Something went wrong when uploading data: ${error.message}`);
|
|
270
|
-
}
|
|
271
|
-
else {
|
|
272
|
-
throw new TeamscaleUploadError(`Something went wrong when uploading data: ${(0, util_1.inspect)(error)}`);
|
|
273
|
-
}
|
|
274
|
-
});
|
|
275
|
-
}
|
|
276
|
-
static prepareQueryParameters(config) {
|
|
277
|
-
const parameters = new QueryParameters_1.default();
|
|
278
|
-
parameters.addIfDefined('format', 'SIMPLE');
|
|
279
|
-
parameters.addIfDefined('message', config.teamscale_message);
|
|
280
|
-
parameters.addIfDefined('repository', config.teamscale_repository);
|
|
281
|
-
parameters.addIfDefined('t', config.teamscale_commit);
|
|
282
|
-
parameters.addIfDefined('revision', config.teamscale_revision);
|
|
283
|
-
parameters.addIfDefined('partition', config.teamscale_partition);
|
|
284
|
-
return parameters;
|
|
285
|
-
}
|
|
286
|
-
static prepareFormData(coverageFile) {
|
|
287
|
-
const form = new form_data_1.default();
|
|
288
|
-
form.append('report', fs.createReadStream(coverageFile), 'coverage.simple');
|
|
289
|
-
return form;
|
|
290
|
-
}
|
|
291
|
-
}
|
|
292
|
-
exports.Main = Main;
|
|
293
|
-
Main.DEFAULT_COVERAGE_LOCATION = 'coverage';
|
|
294
|
-
Main.run();
|
|
4
|
+
const App_1 = require("./App");
|
|
5
|
+
App_1.App.run();
|
|
@@ -18,7 +18,7 @@ export declare class WebSocketCollectingServer {
|
|
|
18
18
|
/**
|
|
19
19
|
* The WebSocket server component.
|
|
20
20
|
*/
|
|
21
|
-
private
|
|
21
|
+
private server;
|
|
22
22
|
/**
|
|
23
23
|
* The storage to put the received coverage information to for aggregation and further processing.
|
|
24
24
|
*/
|
|
@@ -38,7 +38,9 @@ export declare class WebSocketCollectingServer {
|
|
|
38
38
|
/**
|
|
39
39
|
* Start the server socket, handle sessions and dispatch messages.
|
|
40
40
|
*/
|
|
41
|
-
start():
|
|
41
|
+
start(): {
|
|
42
|
+
stop: () => void;
|
|
43
|
+
};
|
|
42
44
|
/**
|
|
43
45
|
* Handle a message from a client.
|
|
44
46
|
*
|
|
@@ -64,9 +64,10 @@ class WebSocketCollectingServer {
|
|
|
64
64
|
* Start the server socket, handle sessions and dispatch messages.
|
|
65
65
|
*/
|
|
66
66
|
start() {
|
|
67
|
-
|
|
67
|
+
var _a, _b;
|
|
68
|
+
this.logger.info(`Starting server on port ${(_a = this.server) === null || _a === void 0 ? void 0 : _a.options.port}.`);
|
|
68
69
|
// Handle new connections from clients
|
|
69
|
-
this.server.on('connection', (webSocket, req) => {
|
|
70
|
+
(_b = this.server) === null || _b === void 0 ? void 0 : _b.on('connection', (webSocket, req) => {
|
|
70
71
|
let session = new Session_1.Session(req.socket, this.storage, this.logger);
|
|
71
72
|
this.logger.debug(`Connection from: ${req.socket.remoteAddress}`);
|
|
72
73
|
// Handle disconnecting clients
|
|
@@ -79,9 +80,9 @@ class WebSocketCollectingServer {
|
|
|
79
80
|
}
|
|
80
81
|
});
|
|
81
82
|
// Handle incoming messages
|
|
82
|
-
webSocket.on('message', (message) => {
|
|
83
|
+
webSocket.on('message', async (message) => {
|
|
83
84
|
if (session && typeof message === 'string') {
|
|
84
|
-
this.handleMessage(session, message);
|
|
85
|
+
await this.handleMessage(session, message);
|
|
85
86
|
}
|
|
86
87
|
});
|
|
87
88
|
// Handle errors
|
|
@@ -89,6 +90,13 @@ class WebSocketCollectingServer {
|
|
|
89
90
|
this.logger.error('Error on server socket triggered.', e);
|
|
90
91
|
});
|
|
91
92
|
});
|
|
93
|
+
return {
|
|
94
|
+
stop: () => {
|
|
95
|
+
var _a;
|
|
96
|
+
(_a = this.server) === null || _a === void 0 ? void 0 : _a.close();
|
|
97
|
+
this.server = null;
|
|
98
|
+
}
|
|
99
|
+
};
|
|
92
100
|
}
|
|
93
101
|
/**
|
|
94
102
|
* Handle a message from a client.
|
|
@@ -96,13 +104,13 @@ class WebSocketCollectingServer {
|
|
|
96
104
|
* @param session - The session that has been started for the client.
|
|
97
105
|
* @param message - The message to handle.
|
|
98
106
|
*/
|
|
99
|
-
handleMessage(session, message) {
|
|
107
|
+
async handleMessage(session, message) {
|
|
100
108
|
try {
|
|
101
109
|
if (message.startsWith(ProtocolMessageTypes.TYPE_SOURCEMAP)) {
|
|
102
|
-
this.handleSourcemapMessage(session, message.substring(1));
|
|
110
|
+
await this.handleSourcemapMessage(session, message.substring(1));
|
|
103
111
|
}
|
|
104
112
|
else if (message.startsWith(ProtocolMessageTypes.TYPE_COVERAGE)) {
|
|
105
|
-
this.handleCoverageMessage(session, message.substring(1));
|
|
113
|
+
await this.handleCoverageMessage(session, message.substring(1));
|
|
106
114
|
}
|
|
107
115
|
}
|
|
108
116
|
catch (e) {
|
|
@@ -116,13 +124,13 @@ class WebSocketCollectingServer {
|
|
|
116
124
|
* @param session - The session to handle the message for.
|
|
117
125
|
* @param body - The body of the message (to be parsed).
|
|
118
126
|
*/
|
|
119
|
-
handleSourcemapMessage(session, body) {
|
|
127
|
+
async handleSourcemapMessage(session, body) {
|
|
120
128
|
const fileIdSeparatorPosition = body.indexOf(INSTRUMENTATION_SUBJECT_SEPARATOR);
|
|
121
129
|
if (fileIdSeparatorPosition > -1) {
|
|
122
130
|
const fileId = body.substring(0, fileIdSeparatorPosition).trim();
|
|
123
131
|
this.logger.debug(`Received source map information for ${fileId}`);
|
|
124
132
|
const sourcemap = body.substring(fileIdSeparatorPosition + 1);
|
|
125
|
-
session.putSourcemap(fileId, sourcemap);
|
|
133
|
+
await session.putSourcemap(fileId, sourcemap);
|
|
126
134
|
}
|
|
127
135
|
}
|
|
128
136
|
/**
|
|
@@ -131,7 +139,7 @@ class WebSocketCollectingServer {
|
|
|
131
139
|
* @param session - The session to handle the message for.
|
|
132
140
|
* @param body - The body of the message (to be parsed).
|
|
133
141
|
*/
|
|
134
|
-
handleCoverageMessage(session, body) {
|
|
142
|
+
async handleCoverageMessage(session, body) {
|
|
135
143
|
var _a;
|
|
136
144
|
const bodyPattern = /(?<fileId>\S+) (?<positions>((\d+:\d+(:\d+:\d+)?\s+)*(\d+:\d+(:\d+:\d+)?)))/;
|
|
137
145
|
const matches = bodyPattern.exec(body);
|
|
@@ -63,7 +63,7 @@ export declare class Session {
|
|
|
63
63
|
* @param fileId - The identifier of the file bundle.
|
|
64
64
|
* @param sourceMapText - The actual source map.
|
|
65
65
|
*/
|
|
66
|
-
putSourcemap(fileId: string, sourceMapText: string): void
|
|
66
|
+
putSourcemap(fileId: string, sourceMapText: string): Promise<void>;
|
|
67
67
|
/**
|
|
68
68
|
* Destroy the session and free the memory it allocates.
|
|
69
69
|
* In particular the sourcemaps are freed (important to not run out of memory!).
|
|
@@ -125,15 +125,15 @@ class Session {
|
|
|
125
125
|
* @param fileId - The identifier of the file bundle.
|
|
126
126
|
* @param sourceMapText - The actual source map.
|
|
127
127
|
*/
|
|
128
|
-
putSourcemap(fileId, sourceMapText) {
|
|
128
|
+
async putSourcemap(fileId, sourceMapText) {
|
|
129
129
|
const rawSourceMap = JSON.parse(sourceMapText);
|
|
130
|
-
|
|
131
|
-
.
|
|
132
|
-
this.sourceMaps.set(fileId,
|
|
133
|
-
}
|
|
134
|
-
|
|
130
|
+
try {
|
|
131
|
+
const sourceMapConsumer = await new sourceMap.SourceMapConsumer(rawSourceMap);
|
|
132
|
+
this.sourceMaps.set(fileId, sourceMapConsumer);
|
|
133
|
+
}
|
|
134
|
+
catch (e) {
|
|
135
135
|
this.logger.error(`Consuming source map failed! ${e}`);
|
|
136
|
-
}
|
|
136
|
+
}
|
|
137
137
|
}
|
|
138
138
|
/**
|
|
139
139
|
* Destroy the session and free the memory it allocates.
|
|
@@ -49,6 +49,10 @@ export interface IWriteableStorage {
|
|
|
49
49
|
* @param project - The project to add the information to.
|
|
50
50
|
*/
|
|
51
51
|
signalUnmappedCoverage(project: string): void;
|
|
52
|
+
/**
|
|
53
|
+
* Discard the coverage information that has been collected up to this point.
|
|
54
|
+
*/
|
|
55
|
+
discardCollectedCoverage(): void;
|
|
52
56
|
}
|
|
53
57
|
/**
|
|
54
58
|
* Union of write and read interface.
|
|
@@ -158,4 +162,8 @@ export declare class DataStorage implements IDataStorage {
|
|
|
158
162
|
* {@inheritDoc IReadableStorage.getProjects}
|
|
159
163
|
*/
|
|
160
164
|
getProjects(): string[];
|
|
165
|
+
/**
|
|
166
|
+
* {@inheritDoc IWritableStorage.discardCollectedCoverage}
|
|
167
|
+
*/
|
|
168
|
+
discardCollectedCoverage(): void;
|
|
161
169
|
}
|
|
@@ -194,5 +194,11 @@ class DataStorage {
|
|
|
194
194
|
getProjects() {
|
|
195
195
|
return Array.from(this.coverageByProject.keys());
|
|
196
196
|
}
|
|
197
|
+
/**
|
|
198
|
+
* {@inheritDoc IWritableStorage.discardCollectedCoverage}
|
|
199
|
+
*/
|
|
200
|
+
discardCollectedCoverage() {
|
|
201
|
+
this.coverageByProject.clear();
|
|
202
|
+
}
|
|
197
203
|
}
|
|
198
204
|
exports.DataStorage = DataStorage;
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { ArgumentParser } from 'argparse';
|
|
2
|
+
/**
|
|
3
|
+
* The command line parameters the profiler can be configured with.
|
|
4
|
+
*
|
|
5
|
+
* ATTENTION: We use snake_case here because ArgParse creates
|
|
6
|
+
* the parameters that way---as in Python from which ArgParse stems.
|
|
7
|
+
*/
|
|
8
|
+
export declare type ConfigParameters = {
|
|
9
|
+
dump_to_folder: string;
|
|
10
|
+
log_to_file: string;
|
|
11
|
+
keep_coverage_files: boolean;
|
|
12
|
+
log_level: string;
|
|
13
|
+
dump_after_mins: number;
|
|
14
|
+
port: number;
|
|
15
|
+
json_log: boolean;
|
|
16
|
+
teamscale_server_url?: string;
|
|
17
|
+
teamscale_access_token?: string;
|
|
18
|
+
teamscale_project?: string;
|
|
19
|
+
teamscale_user?: string;
|
|
20
|
+
teamscale_partition?: string;
|
|
21
|
+
teamscale_revision?: string;
|
|
22
|
+
teamscale_commit?: string;
|
|
23
|
+
teamscale_repository?: string;
|
|
24
|
+
teamscale_message?: string;
|
|
25
|
+
enable_control_port?: number;
|
|
26
|
+
};
|
|
27
|
+
/**
|
|
28
|
+
* Construct the object for parsing the command line arguments.
|
|
29
|
+
*/
|
|
30
|
+
export declare function buildParameterParser(): ArgumentParser;
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.buildParameterParser = void 0;
|
|
4
|
+
const argparse_1 = require("argparse");
|
|
5
|
+
const package_json_1 = require("../../package.json");
|
|
6
|
+
/**
|
|
7
|
+
* Construct the object for parsing the command line arguments.
|
|
8
|
+
*/
|
|
9
|
+
function buildParameterParser() {
|
|
10
|
+
var _a;
|
|
11
|
+
const parser = new argparse_1.ArgumentParser({
|
|
12
|
+
description: 'Collector of the Teamscale JavaScript Profiler. Collects coverage information from a' +
|
|
13
|
+
'(headless) Web browser that executes code instrumented with our instrumenter.'
|
|
14
|
+
});
|
|
15
|
+
parser.add_argument('-v', '--version', { action: 'version', version: package_json_1.version });
|
|
16
|
+
parser.add_argument('-p', '--port', { help: 'The port to receive coverage information on.', default: 54678 });
|
|
17
|
+
parser.add_argument('-f', '--dump-to-folder', {
|
|
18
|
+
help: 'Target folder for coverage files.',
|
|
19
|
+
default: 'coverage'
|
|
20
|
+
});
|
|
21
|
+
parser.add_argument('-k', '--keep-coverage-files', {
|
|
22
|
+
help: 'Whether to keep the coverage files on disk after a successful upload to Teamsacle',
|
|
23
|
+
action: 'store_true',
|
|
24
|
+
default: false
|
|
25
|
+
});
|
|
26
|
+
parser.add_argument('-l', '--log-to-file', { help: 'Log file', default: 'logs/collector-combined.log' });
|
|
27
|
+
parser.add_argument('-e', '--log-level', { help: 'Log level', default: 'info' });
|
|
28
|
+
parser.add_argument('-c', '--enable-control-port', {
|
|
29
|
+
help: 'Enables the remote control API on the specified port (<=0 means "disabled").',
|
|
30
|
+
default: 0
|
|
31
|
+
});
|
|
32
|
+
parser.add_argument('-t', '--dump-after-mins', {
|
|
33
|
+
help: 'Dump the coverage information to the target file every N minutes.',
|
|
34
|
+
default: 360
|
|
35
|
+
});
|
|
36
|
+
parser.add_argument('-d', '--debug', {
|
|
37
|
+
help: 'Print received coverage information to the terminal?',
|
|
38
|
+
default: false
|
|
39
|
+
});
|
|
40
|
+
parser.add_argument('-j', '--json-log', {
|
|
41
|
+
help: 'Additional JSON-like log file format.',
|
|
42
|
+
action: 'store_true'
|
|
43
|
+
});
|
|
44
|
+
// Parameters for the upload to Teamscale
|
|
45
|
+
parser.add_argument('-u', '--teamscale-server-url', {
|
|
46
|
+
help: 'Upload the coverage to the given Teamscale server URL, for example, https://teamscale.dev.example.com:8080/production.',
|
|
47
|
+
default: process.env.TEAMSCALE_SERVER_URL
|
|
48
|
+
});
|
|
49
|
+
parser.add_argument('--teamscale-access-token', {
|
|
50
|
+
help: 'The API key to use for uploading to Teamscale.',
|
|
51
|
+
default: process.env.TEAMSCALE_ACCESS_TOKEN
|
|
52
|
+
});
|
|
53
|
+
parser.add_argument('--teamscale-project', {
|
|
54
|
+
help: 'The project ID to upload coverage to.',
|
|
55
|
+
default: process.env.TEAMSCALE_PROJECT
|
|
56
|
+
});
|
|
57
|
+
parser.add_argument('--teamscale-user', {
|
|
58
|
+
help: 'The user for uploading coverage to Teamscale.',
|
|
59
|
+
default: process.env.TEAMSCALE_USER
|
|
60
|
+
});
|
|
61
|
+
parser.add_argument('--teamscale-partition', {
|
|
62
|
+
help: 'The partition to upload coverage to.',
|
|
63
|
+
default: process.env.TEAMSCALE_PARTITION
|
|
64
|
+
});
|
|
65
|
+
parser.add_argument('--teamscale-revision', {
|
|
66
|
+
help: 'The revision (commit hash, version id) to upload coverage for.',
|
|
67
|
+
default: process.env.TEAMSCALE_REVISION
|
|
68
|
+
});
|
|
69
|
+
parser.add_argument('--teamscale-commit', {
|
|
70
|
+
help: 'The branch and timestamp to upload coverage for, separated by colon.',
|
|
71
|
+
default: process.env.TEAMSCALE_COMMIT
|
|
72
|
+
});
|
|
73
|
+
parser.add_argument('--teamscale-repository', {
|
|
74
|
+
help: 'The repository to upload coverage for. Optional: Only needed when uploading via revision to a project that has more than one connector.',
|
|
75
|
+
default: process.env.TEAMSCALE_REPOSITORY
|
|
76
|
+
});
|
|
77
|
+
parser.add_argument('--teamscale-message', {
|
|
78
|
+
help: 'The commit message shown within Teamscale for the coverage upload. Default is "JavaScript coverage upload".',
|
|
79
|
+
default: (_a = process.env.TEAMSCALE_MESSAGE) !== null && _a !== void 0 ? _a : 'JavaScript coverage upload'
|
|
80
|
+
});
|
|
81
|
+
return parser;
|
|
82
|
+
}
|
|
83
|
+
exports.buildParameterParser = buildParameterParser;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@teamscale/coverage-collector",
|
|
3
|
-
"version": "0.0.1-beta.
|
|
3
|
+
"version": "0.0.1-beta.43",
|
|
4
4
|
"description": "Collector for JavaScript code coverage information",
|
|
5
5
|
"main": "dist/src/main.js",
|
|
6
6
|
"bin": "dist/src/main.js",
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
"clean": "rimraf dist tsconfig.tsbuildinfo",
|
|
17
17
|
"build": "tsc",
|
|
18
18
|
"collector": "node dist/src/main.js",
|
|
19
|
-
"test": "yarn build && NODE_OPTIONS='--experimental-vm-modules' jest --
|
|
19
|
+
"test": "yarn build && NODE_OPTIONS='--experimental-vm-modules' jest --coverage --silent=true"
|
|
20
20
|
},
|
|
21
21
|
"files": [
|
|
22
22
|
"dist/**/*"
|
|
@@ -29,6 +29,7 @@
|
|
|
29
29
|
"bunyan": "^1.8.15",
|
|
30
30
|
"date-and-time": "^2.3.1",
|
|
31
31
|
"dotenv": "^14.1.0",
|
|
32
|
+
"express": "^4.18.1",
|
|
32
33
|
"form-data": "^4.0.0",
|
|
33
34
|
"mkdirp": "^1.0.4",
|
|
34
35
|
"rxjs": "^7.1.0",
|
|
@@ -43,6 +44,7 @@
|
|
|
43
44
|
"@types/argparse": "^2.0.5",
|
|
44
45
|
"@types/async": "^3.2.6",
|
|
45
46
|
"@types/bunyan": "^1.8.8",
|
|
47
|
+
"@types/express": "^4.17.13",
|
|
46
48
|
"@types/jest": "^27.0.1",
|
|
47
49
|
"@types/mkdirp": "^1.0.2",
|
|
48
50
|
"@types/node": "^15.0.1",
|
|
@@ -52,6 +54,7 @@
|
|
|
52
54
|
"babel-jest": "^27.2.0",
|
|
53
55
|
"esbuild": "^0.13.4",
|
|
54
56
|
"jest": "^27.2.0",
|
|
57
|
+
"mockttp": "^3.1.0",
|
|
55
58
|
"rimraf": "^3.0.2",
|
|
56
59
|
"ts-jest": "^27.0.5",
|
|
57
60
|
"ts-node": "^10.2.1",
|