@teamscale/coverage-collector 0.0.1-beta.40 → 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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@teamscale/coverage-collector",
3
- "version": "0.0.1-beta.40",
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 --forceExit --coverage --silent=true --detectOpenHandles"
19
+ "test": "yarn build && NODE_OPTIONS='--experimental-vm-modules' jest --coverage --silent=true"
20
20
  },
21
21
  "files": [
22
22
  "dist/**/*"
@@ -27,7 +27,9 @@
27
27
  "async": "^3.2.4",
28
28
  "axios": "^0.24.0",
29
29
  "bunyan": "^1.8.15",
30
+ "date-and-time": "^2.3.1",
30
31
  "dotenv": "^14.1.0",
32
+ "express": "^4.18.1",
31
33
  "form-data": "^4.0.0",
32
34
  "mkdirp": "^1.0.4",
33
35
  "rxjs": "^7.1.0",
@@ -42,6 +44,7 @@
42
44
  "@types/argparse": "^2.0.5",
43
45
  "@types/async": "^3.2.6",
44
46
  "@types/bunyan": "^1.8.8",
47
+ "@types/express": "^4.17.13",
45
48
  "@types/jest": "^27.0.1",
46
49
  "@types/mkdirp": "^1.0.2",
47
50
  "@types/node": "^15.0.1",
@@ -51,6 +54,7 @@
51
54
  "babel-jest": "^27.2.0",
52
55
  "esbuild": "^0.13.4",
53
56
  "jest": "^27.2.0",
57
+ "mockttp": "^3.1.0",
54
58
  "rimraf": "^3.0.2",
55
59
  "ts-jest": "^27.0.5",
56
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
+ }
@@ -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;
@@ -1,34 +1,2 @@
1
1
  #!/usr/bin/env node
2
- import 'dotenv/config';
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
- /**
9
- * Construct the object for parsing the command line arguments.
10
- */
11
- private static buildParser;
12
- /**
13
- * Parse the given command line arguments into a corresponding options object.
14
- */
15
- private static parseArguments;
16
- /**
17
- * Construct the logger.
18
- */
19
- private static buildLogger;
20
- /**
21
- * Entry point of the Teamscale JavaScript Profiler.
22
- */
23
- static run(): void;
24
- /**
25
- * Start a timer for dumping the data, depending on the configuration.
26
- *
27
- * @param config - The config that determines whether to do the timed dump or not.
28
- * @param storage - The storage with the information to dump.
29
- * @param logger - The logger to use.
30
- */
31
- private static maybeStartDumpTimer;
32
- private static dumpCoverage;
33
- private static uploadToTeamscale;
34
- }
2
+ export {};
package/dist/src/main.js CHANGED
@@ -1,276 +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
- exports.Main = void 0;
31
- const package_json_1 = require("../package.json");
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 tmp_1 = __importDefault(require("tmp"));
43
- const mkdirp_1 = __importDefault(require("mkdirp"));
44
- const path_1 = __importDefault(require("path"));
45
- const StdConsoleLogger_1 = require("./utils/StdConsoleLogger");
46
- const PrettyFileLogger_1 = require("./utils/PrettyFileLogger");
47
- /**
48
- * The main class of the Teamscale JavaScript Collector.
49
- * Used to start the collector for with a given configuration.
50
- */
51
- class Main {
52
- /**
53
- * Construct the object for parsing the command line arguments.
54
- */
55
- static buildParser() {
56
- var _a;
57
- const parser = new argparse_1.ArgumentParser({
58
- description: 'Collector of the Teamscale JavaScript Profiler. Collects coverage information from a' +
59
- '(headless) Web browser that executes code instrumented with our instrumenter.'
60
- });
61
- parser.add_argument('-v', '--version', { action: 'version', version: package_json_1.version });
62
- parser.add_argument('-p', '--port', { help: 'The port to receive coverage information on.', default: 54678 });
63
- parser.add_argument('-f', '--dump-to-file', { help: 'Target file to write coverage to.' });
64
- parser.add_argument('-l', '--log-to-file', { help: 'Log file', default: 'logs/collector-combined.log' });
65
- parser.add_argument('-e', '--log-level', { help: 'Log level', default: 'info' });
66
- parser.add_argument('-t', '--dump-after-mins', {
67
- help: 'Dump the coverage information to the target file every N minutes.',
68
- default: 360
69
- });
70
- parser.add_argument('-d', '--debug', {
71
- help: 'Print received coverage information to the terminal?',
72
- default: false
73
- });
74
- parser.add_argument('-j', '--json-log', {
75
- help: 'Additional JSON-like log file format.',
76
- action: 'store_true'
77
- });
78
- // Parameters for the upload to Teamscale
79
- parser.add_argument('-u', '--teamscale-server-url', {
80
- help: 'Upload the coverage to the given Teamscale server URL, for example, https://teamscale.dev.example.com:8080/production.',
81
- default: process.env.TEAMSCALE_SERVER_URL
82
- });
83
- parser.add_argument('--teamscale-access-token', {
84
- help: 'The API key to use for uploading to Teamscale.',
85
- default: process.env.TEAMSCALE_ACCESS_TOKEN
86
- });
87
- parser.add_argument('--teamscale-project', {
88
- help: 'The project ID to upload coverage to.',
89
- default: process.env.TEAMSCALE_PROJECT
90
- });
91
- parser.add_argument('--teamscale-user', {
92
- help: 'The user for uploading coverage to Teamscale.',
93
- default: process.env.TEAMSCALE_USER
94
- });
95
- parser.add_argument('--teamscale-partition', {
96
- help: 'The partition to upload coverage to.',
97
- default: process.env.TEAMSCALE_PARTITION
98
- });
99
- parser.add_argument('--teamscale-revision', {
100
- help: 'The revision (commit hash, version id) to upload coverage for.',
101
- default: process.env.TEAMSCALE_REVISION
102
- });
103
- parser.add_argument('--teamscale-commit', {
104
- help: 'The branch and timestamp to upload coverage for, separated by colon.',
105
- default: process.env.TEAMSCALE_COMMIT
106
- });
107
- parser.add_argument('--teamscale-repository', {
108
- help: 'The repository to upload coverage for. Optional: Only needed when uploading via revision to a project that has more than one connector.',
109
- default: process.env.TEAMSCALE_REPOSITORY
110
- });
111
- parser.add_argument('--teamscale-message', {
112
- help: 'The commit message shown within Teamscale for the coverage upload. Default is "JavaScript coverage upload".',
113
- default: (_a = process.env.TEAMSCALE_MESSAGE) !== null && _a !== void 0 ? _a : 'JavaScript coverage upload'
114
- });
115
- return parser;
116
- }
117
- /**
118
- * Parse the given command line arguments into a corresponding options object.
119
- */
120
- static parseArguments() {
121
- const parser = this.buildParser();
122
- return parser.parse_args();
123
- }
124
- /**
125
- * Construct the logger.
126
- */
127
- static buildLogger(config) {
128
- const logfilePath = config.log_to_file.trim();
129
- mkdirp_1.default.sync(path_1.default.dirname(logfilePath));
130
- const logLevel = config.log_level;
131
- const logger = bunyan_1.default.createLogger({
132
- name: 'Collector',
133
- streams: [
134
- // console output
135
- { level: logLevel, stream: new StdConsoleLogger_1.StdConsoleLogger(), type: 'raw' },
136
- // default log file
137
- { level: logLevel, stream: new PrettyFileLogger_1.PrettyFileLogger(fs.createWriteStream(logfilePath)), type: 'raw' }
138
- ]
139
- });
140
- // If the given flag is set, we also log with a JSON-like format
141
- if (config.json_log) {
142
- logger.addStream({ level: logLevel, path: `${logfilePath}.json` });
143
- }
144
- return logger;
145
- }
146
- /**
147
- * Entry point of the Teamscale JavaScript Profiler.
148
- */
149
- static run() {
150
- // Parse the command line arguments
151
- const config = this.parseArguments();
152
- // Build the logger
153
- const logger = this.buildLogger(config);
154
- logger.info(`Starting collector in working directory "${process.cwd()}".`);
155
- logger.info(`Logging "${config.log_level}" to "${config.log_to_file}".`);
156
- // Check the command line arguments
157
- if (!config.dump_to_file && !config.teamscale_server_url) {
158
- logger.error('The Collector must be configured to either dump to a file or upload to Teamscale.');
159
- process.exit(1);
160
- }
161
- // Prepare the storage and the server
162
- const storage = new DataStorage_1.DataStorage(logger);
163
- const server = new CollectingServer_1.WebSocketCollectingServer(config.port, storage, logger);
164
- // Start the server socket.
165
- // ATTENTION: The server is executed asynchronously
166
- server.start();
167
- // Optionally, start a timer that dumps the coverage after a N seconds
168
- this.maybeStartDumpTimer(config, storage, logger);
169
- // Say bye bye on CTRL+C and exit the process
170
- process.on('SIGINT', async () => {
171
- // ... and do a final dump before.
172
- await this.dumpCoverage(config, storage, logger).then();
173
- logger.info('Bye bye.');
174
- process.exit();
175
- });
176
- }
177
- /**
178
- * Start a timer for dumping the data, depending on the configuration.
179
- *
180
- * @param config - The config that determines whether to do the timed dump or not.
181
- * @param storage - The storage with the information to dump.
182
- * @param logger - The logger to use.
183
- */
184
- static maybeStartDumpTimer(config, storage, logger) {
185
- if (config.dump_after_mins > 0) {
186
- logger.info(`Will dump coverage information every ${config.dump_after_mins} minute(s).`);
187
- const timer = setInterval(() => {
188
- this.dumpCoverage(config, storage, logger).then();
189
- }, config.dump_after_mins * 1000 * 60);
190
- process.on('SIGINT', () => {
191
- // Stop the timed file dump
192
- if (timer) {
193
- clearInterval(timer);
194
- }
195
- });
196
- }
197
- }
198
- static async dumpCoverage(config, storage, logger) {
199
- var _a;
200
- try {
201
- const deleteCoverageFileAfterUpload = !config.dump_to_file;
202
- const coverageFile = (_a = config.dump_to_file) !== null && _a !== void 0 ? _a : tmp_1.default.tmpNameSync();
203
- try {
204
- // 1. Write coverage to a file
205
- const lines = storage.dumpToSimpleCoverageFile(coverageFile);
206
- logger.info(`Dumped ${lines} lines of coverage to ${coverageFile}.`);
207
- // 2. Upload to Teamscale if configured
208
- if (config.teamscale_server_url) {
209
- await this.uploadToTeamscale(config, logger, coverageFile, lines);
210
- }
211
- }
212
- finally {
213
- if (deleteCoverageFileAfterUpload) {
214
- fs.unlinkSync(coverageFile);
215
- }
216
- }
217
- }
218
- catch (e) {
219
- logger.error('Coverage dump failed.', e);
220
- }
221
- }
222
- static async uploadToTeamscale(config, logger, coverageFile, lines) {
223
- if (!(config.teamscale_access_token && config.teamscale_user && config.teamscale_server_url)) {
224
- logger.error('Cannot upload to Teamscale: API key and user name must be configured!');
225
- return;
226
- }
227
- if (lines === 0) {
228
- return;
229
- }
230
- logger.info('Preparing upload to Teamscale');
231
- const form = new form_data_1.default();
232
- form.append('report', fs.createReadStream(coverageFile), 'coverage.simple');
233
- const parameters = new QueryParameters_1.default();
234
- parameters.addIfDefined('format', 'SIMPLE');
235
- parameters.addIfDefined('message', config.teamscale_message);
236
- parameters.addIfDefined('repository', config.teamscale_repository);
237
- parameters.addIfDefined('t', config.teamscale_commit);
238
- parameters.addIfDefined('revision', config.teamscale_revision);
239
- parameters.addIfDefined('partition', config.teamscale_partition);
240
- await axios_1.default
241
- .post(`${config.teamscale_server_url.replace(/\/$/, '')}/api/projects/${config.teamscale_project}/external-analysis/session/auto-create/report?${parameters.toString()}`, form, {
242
- auth: {
243
- username: config.teamscale_user,
244
- password: config.teamscale_access_token
245
- },
246
- headers: {
247
- Accept: '*/*',
248
- 'Content-Type': `multipart/form-data; boundary=${form.getBoundary()}`
249
- }
250
- })
251
- .catch(function (error) {
252
- if (error.response) {
253
- const response = error.response;
254
- if (response.status >= 400) {
255
- logger.error(`Upload failed with code ${response.status}: ${response.statusText}`);
256
- logger.error(`Request failed with following response: ${response.data}`);
257
- }
258
- else {
259
- logger.info(`Upload with status code ${response.status} finished.`);
260
- }
261
- }
262
- else if (error.request) {
263
- logger.error(`Upload request did not receive a response.`);
264
- }
265
- if (error.message) {
266
- logger.error(`Something went wrong when uploading data: ${error.message}`);
267
- logger.debug(`Details of the error: ${(0, util_1.inspect)(error)}`);
268
- }
269
- else {
270
- logger.error(`Something went wrong when uploading data: ${(0, util_1.inspect)(error)}`);
271
- }
272
- });
273
- }
274
- }
275
- exports.Main = Main;
276
- 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 readonly server;
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(): void;
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
- this.logger.info(`Starting server on port ${this.server.options.port}.`);
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
- new sourceMap.SourceMapConsumer(rawSourceMap)
131
- .then(consumer => {
132
- this.sourceMaps.set(fileId, consumer);
133
- })
134
- .catch(e => {
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.
@@ -21,11 +21,14 @@ export interface IReadableStorage {
21
21
  */
22
22
  getCoverageBySourceFile(project: string): IterableIterator<FileCoverage> | undefined;
23
23
  /**
24
- * Write the coverage to the specified file.
24
+ * Write the coverage to the specified file. A timestamp will be appended to the provided file path.
25
25
  *
26
- * @param filePath - Full path of the file to write the coverage to.
26
+ * @param coverageFolder - Full path of the file to write the coverage to.
27
+ * @param date - Date to use for the appended timestamp
28
+ *
29
+ * @return The number of lines written
27
30
  */
28
- dumpToSimpleCoverageFile(filePath: string): void;
31
+ dumpToSimpleCoverageFile(coverageFolder: string, date: Date): [string, number];
29
32
  }
30
33
  /**
31
34
  * Storage interface for writing information.
@@ -46,6 +49,10 @@ export interface IWriteableStorage {
46
49
  * @param project - The project to add the information to.
47
50
  */
48
51
  signalUnmappedCoverage(project: string): void;
52
+ /**
53
+ * Discard the coverage information that has been collected up to this point.
54
+ */
55
+ discardCollectedCoverage(): void;
49
56
  }
50
57
  /**
51
58
  * Union of write and read interface.
@@ -89,7 +96,7 @@ export declare class DataStorage implements IDataStorage {
89
96
  /**
90
97
  * Coverage information by project.
91
98
  */
92
- private readonly coverageByProject;
99
+ private coverageByProject;
93
100
  /**
94
101
  * Logger to use.
95
102
  */
@@ -98,6 +105,10 @@ export declare class DataStorage implements IDataStorage {
98
105
  * Times unmapped coverage received.
99
106
  */
100
107
  private timesUnmappedCoverage;
108
+ /**
109
+ * Date format for the timestamp appended to the coverage files
110
+ */
111
+ readonly DATE_FORMAT = "YYYY-MM-DD-HH-mm-ss.SSS";
101
112
  /**
102
113
  * Constructs the data storage.
103
114
  *
@@ -118,7 +129,7 @@ export declare class DataStorage implements IDataStorage {
118
129
  *
119
130
  * @param sourceFile - The file name to normalize, produced by the instrumenter.
120
131
  */
121
- private normalizeSourceFileName;
132
+ private static normalizeSourceFileName;
122
133
  /**
123
134
  * {@inheritDoc IWriteableStorage.signalUnmappedCoverage}
124
135
  */
@@ -128,11 +139,31 @@ export declare class DataStorage implements IDataStorage {
128
139
  */
129
140
  getCoverageBySourceFile(project: string): IterableIterator<FileCoverage> | undefined;
130
141
  /**
131
- * {@inheritDoc IReadableStorage.writeToSimpleCoverageFile}
142
+ * @inheritDoc
143
+ */
144
+ dumpToSimpleCoverageFile(coverageFolder: string, date: Date): [string, number];
145
+ /**
146
+ * Set the collected coverage to 0 for all projects
147
+ * @private
132
148
  */
133
- dumpToSimpleCoverageFile(filePath: string): number;
149
+ private resetCoverage;
150
+ /**
151
+ * Appends the timestamp given with date to the coverageFolder (before the file ending if there is one)
152
+ * @param coverageFolder Path to the coverage file
153
+ * @param date Represents the timestamp to be appended with the format {@link DataStorage.DATE_FORMAT}
154
+ * @private
155
+ */
156
+ private initCoverageFile;
157
+ /**
158
+ * Generate simple coverage format for the collected coverage
159
+ */
160
+ private toSimpleCoverage;
134
161
  /**
135
162
  * {@inheritDoc IReadableStorage.getProjects}
136
163
  */
137
164
  getProjects(): string[];
165
+ /**
166
+ * {@inheritDoc IWritableStorage.discardCollectedCoverage}
167
+ */
168
+ discardCollectedCoverage(): void;
138
169
  }
@@ -22,10 +22,15 @@ var __importStar = (this && this.__importStar) || function (mod) {
22
22
  __setModuleDefault(result, mod);
23
23
  return result;
24
24
  };
25
+ var __importDefault = (this && this.__importDefault) || function (mod) {
26
+ return (mod && mod.__esModule) ? mod : { "default": mod };
27
+ };
25
28
  Object.defineProperty(exports, "__esModule", { value: true });
26
29
  exports.DataStorage = exports.ProjectCoverage = void 0;
27
30
  const commons_1 = require("@cqse/commons");
28
31
  const fs = __importStar(require("fs"));
32
+ const path_1 = __importDefault(require("path"));
33
+ const dat = __importStar(require("date-and-time"));
29
34
  /**
30
35
  * The coverage information received for one particular project.
31
36
  */
@@ -81,6 +86,10 @@ class DataStorage {
81
86
  * @param logger - The logger to use.
82
87
  */
83
88
  constructor(logger) {
89
+ /**
90
+ * Date format for the timestamp appended to the coverage files
91
+ */
92
+ this.DATE_FORMAT = 'YYYY-MM-DD-HH-mm-ss.SSS';
84
93
  this.coverageByProject = new Map();
85
94
  this.logger = commons_1.Contract.requireDefined(logger);
86
95
  this.timesUnmappedCoverage = 0;
@@ -93,7 +102,7 @@ class DataStorage {
93
102
  * @param coveredOriginalLines - The lines covered in the file.
94
103
  */
95
104
  putCoverage(project, sourceFilePath, coveredOriginalLines) {
96
- const uniformPath = this.normalizeSourceFileName(sourceFilePath);
105
+ const uniformPath = DataStorage.normalizeSourceFileName(sourceFilePath);
97
106
  let projectCoverage = this.coverageByProject.get(project);
98
107
  if (!projectCoverage) {
99
108
  projectCoverage = new ProjectCoverage(project);
@@ -108,7 +117,7 @@ class DataStorage {
108
117
  *
109
118
  * @param sourceFile - The file name to normalize, produced by the instrumenter.
110
119
  */
111
- normalizeSourceFileName(sourceFile) {
120
+ static normalizeSourceFileName(sourceFile) {
112
121
  return (0, commons_1.removePrefix)('webpack:///', sourceFile.replace('\\', '/'));
113
122
  }
114
123
  /**
@@ -129,29 +138,55 @@ class DataStorage {
129
138
  return projectCoverage === null || projectCoverage === void 0 ? void 0 : projectCoverage.getCoverage();
130
139
  }
131
140
  /**
132
- * {@inheritDoc IReadableStorage.writeToSimpleCoverageFile}
141
+ * @inheritDoc
133
142
  */
134
- dumpToSimpleCoverageFile(filePath) {
135
- const toSimpleCoverage = () => {
136
- const result = [];
137
- commons_1.Contract.require(this.getProjects().length < 2, 'Only one project supported to be handled in parallel.');
138
- for (const project of this.getProjects()) {
139
- const projectCoverage = this.getCoverageBySourceFile(project);
140
- if (!projectCoverage) {
141
- return [0, ''];
142
- }
143
- for (const entry of projectCoverage) {
144
- result.push(this.normalizeSourceFileName(entry.sourceFile));
145
- for (const lineNo of entry.coveredLines) {
146
- result.push(String(lineNo));
147
- }
143
+ dumpToSimpleCoverageFile(coverageFolder, date) {
144
+ const [lines, content] = this.toSimpleCoverage();
145
+ const coverageFolderTrimmed = coverageFolder.trim();
146
+ const finalFilePath = this.initCoverageFile(coverageFolderTrimmed, date);
147
+ fs.writeFileSync(finalFilePath, content, { flag: 'w', encoding: 'utf8' });
148
+ this.resetCoverage();
149
+ return [finalFilePath, lines];
150
+ }
151
+ /**
152
+ * Set the collected coverage to 0 for all projects
153
+ * @private
154
+ */
155
+ resetCoverage() {
156
+ this.coverageByProject = new Map();
157
+ }
158
+ /**
159
+ * Appends the timestamp given with date to the coverageFolder (before the file ending if there is one)
160
+ * @param coverageFolder Path to the coverage file
161
+ * @param date Represents the timestamp to be appended with the format {@link DataStorage.DATE_FORMAT}
162
+ * @private
163
+ */
164
+ initCoverageFile(coverageFolder, date) {
165
+ if (!fs.existsSync(coverageFolder)) {
166
+ fs.mkdirSync(coverageFolder);
167
+ }
168
+ const formattedDate = dat.format(date, this.DATE_FORMAT);
169
+ return path_1.default.join(coverageFolder, `coverage-${formattedDate}.simple`);
170
+ }
171
+ /**
172
+ * Generate simple coverage format for the collected coverage
173
+ */
174
+ toSimpleCoverage() {
175
+ const result = [];
176
+ commons_1.Contract.require(this.getProjects().length < 2, 'Only one project supported to be handled in parallel.');
177
+ for (const project of this.getProjects()) {
178
+ const projectCoverage = this.getCoverageBySourceFile(project);
179
+ if (!projectCoverage) {
180
+ return [0, ''];
181
+ }
182
+ for (const entry of projectCoverage) {
183
+ result.push(DataStorage.normalizeSourceFileName(entry.sourceFile));
184
+ for (const lineNo of entry.coveredLines) {
185
+ result.push(String(lineNo));
148
186
  }
149
187
  }
150
- return [result.length, result.join('\n')];
151
- };
152
- const [lines, content] = toSimpleCoverage();
153
- fs.writeFileSync(filePath.trim(), content, { flag: 'w', encoding: 'utf8' });
154
- return lines;
188
+ }
189
+ return [result.length, result.join('\n')];
155
190
  }
156
191
  /**
157
192
  * {@inheritDoc IReadableStorage.getProjects}
@@ -159,5 +194,11 @@ class DataStorage {
159
194
  getProjects() {
160
195
  return Array.from(this.coverageByProject.keys());
161
196
  }
197
+ /**
198
+ * {@inheritDoc IWritableStorage.discardCollectedCoverage}
199
+ */
200
+ discardCollectedCoverage() {
201
+ this.coverageByProject.clear();
202
+ }
162
203
  }
163
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.40",
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 --forceExit --coverage --silent=true --detectOpenHandles"
19
+ "test": "yarn build && NODE_OPTIONS='--experimental-vm-modules' jest --coverage --silent=true"
20
20
  },
21
21
  "files": [
22
22
  "dist/**/*"
@@ -27,7 +27,9 @@
27
27
  "async": "^3.2.4",
28
28
  "axios": "^0.24.0",
29
29
  "bunyan": "^1.8.15",
30
+ "date-and-time": "^2.3.1",
30
31
  "dotenv": "^14.1.0",
32
+ "express": "^4.18.1",
31
33
  "form-data": "^4.0.0",
32
34
  "mkdirp": "^1.0.4",
33
35
  "rxjs": "^7.1.0",
@@ -42,6 +44,7 @@
42
44
  "@types/argparse": "^2.0.5",
43
45
  "@types/async": "^3.2.6",
44
46
  "@types/bunyan": "^1.8.8",
47
+ "@types/express": "^4.17.13",
45
48
  "@types/jest": "^27.0.1",
46
49
  "@types/mkdirp": "^1.0.2",
47
50
  "@types/node": "^15.0.1",
@@ -51,6 +54,7 @@
51
54
  "babel-jest": "^27.2.0",
52
55
  "esbuild": "^0.13.4",
53
56
  "jest": "^27.2.0",
57
+ "mockttp": "^3.1.0",
54
58
  "rimraf": "^3.0.2",
55
59
  "ts-jest": "^27.0.5",
56
60
  "ts-node": "^10.2.1",