@teamscale/coverage-collector 0.0.1-beta.42 → 0.0.1-beta.45

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.42",
3
+ "version": "0.0.1-beta.45",
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/**/*"
@@ -29,10 +29,11 @@
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",
35
- "source-map": "^0.7.3",
36
+ "source-map": "^0.7.4",
36
37
  "tmp": "^0.2.1",
37
38
  "typescript-optional": "^2.0.1",
38
39
  "ws": "^7.4.5"
@@ -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.4.0",
55
58
  "rimraf": "^3.0.2",
56
59
  "ts-jest": "^27.0.5",
57
60
  "ts-node": "^10.2.1",
@@ -0,0 +1,39 @@
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 uploadCoverage;
38
+ private static startControlServer;
39
+ }
@@ -0,0 +1,244 @@
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 ConfigParameters_1 = require("./utils/ConfigParameters");
36
+ const mkdirp_1 = __importDefault(require("mkdirp"));
37
+ const path_1 = __importDefault(require("path"));
38
+ const StdConsoleLogger_1 = require("./utils/StdConsoleLogger");
39
+ const PrettyFileLogger_1 = require("./utils/PrettyFileLogger");
40
+ const express_1 = __importDefault(require("express"));
41
+ const TeamscaleUpload_1 = require("./upload/TeamscaleUpload");
42
+ const CommonUpload_1 = require("./upload/CommonUpload");
43
+ const ArtifactoryUpload_1 = require("./upload/ArtifactoryUpload");
44
+ /**
45
+ * The main class of the Teamscale JavaScript Collector.
46
+ * Used to start the collector with a given configuration.
47
+ */
48
+ class App {
49
+ /**
50
+ * Parse the given command line arguments into a corresponding options object.
51
+ */
52
+ static parseArguments() {
53
+ const parser = (0, ConfigParameters_1.buildParameterParser)();
54
+ return parser.parse_args();
55
+ }
56
+ /**
57
+ * Construct the logger.
58
+ */
59
+ static buildLogger(config) {
60
+ const logfilePath = config.log_to_file.trim();
61
+ mkdirp_1.default.sync(path_1.default.dirname(logfilePath));
62
+ const logLevel = config.log_level;
63
+ const logger = bunyan_1.default.createLogger({
64
+ name: 'Collector',
65
+ streams: [
66
+ // console output
67
+ { level: logLevel, stream: new StdConsoleLogger_1.StdConsoleLogger(), type: 'raw' },
68
+ // default log file
69
+ { level: logLevel, stream: new PrettyFileLogger_1.PrettyFileLogger(fs.createWriteStream(logfilePath)), type: 'raw' }
70
+ ]
71
+ });
72
+ // If the given flag is set, we also log with a JSON-like format
73
+ if (config.json_log) {
74
+ logger.addStream({ level: logLevel, path: `${logfilePath}.json` });
75
+ }
76
+ return logger;
77
+ }
78
+ /**
79
+ * Entry point of the Teamscale JavaScript Profiler.
80
+ */
81
+ static run() {
82
+ // Parse the command line arguments
83
+ const config = this.parseArguments();
84
+ App.runWithConfig(config);
85
+ }
86
+ /**
87
+ * Run the collector with the given configuration options.
88
+ *
89
+ * @param config - The configuration options to run the collector with.
90
+ */
91
+ static runWithConfig(config) {
92
+ // Build the logger
93
+ const logger = this.buildLogger(config);
94
+ logger.info(`Starting collector in working directory "${process.cwd()}".`);
95
+ logger.info(`Logging "${config.log_level}" to "${config.log_to_file}".`);
96
+ // Prepare the storage and the server
97
+ const storage = new DataStorage_1.DataStorage(logger);
98
+ const server = new CollectingServer_1.WebSocketCollectingServer(config.port, storage, logger);
99
+ // Enable the remote control API if configured
100
+ const controlServerState = this.startControlServer(config, storage, logger);
101
+ // Start the server socket.
102
+ // ATTENTION: The server is executed asynchronously
103
+ const serverState = server.start();
104
+ // Optionally, start a timer that dumps the coverage after a N seconds
105
+ const timerState = this.maybeStartDumpTimer(config, storage, logger);
106
+ // Say bye bye on CTRL+C and exit the process
107
+ process.on('SIGINT', async () => {
108
+ // ... and do a final dump before.
109
+ await this.dumpCoverage(config, storage, logger);
110
+ logger.info('Bye bye.');
111
+ process.exit();
112
+ });
113
+ return {
114
+ async stop() {
115
+ logger.info('Stopping the collector.');
116
+ timerState.stop();
117
+ await controlServerState.stop();
118
+ serverState.stop();
119
+ }
120
+ };
121
+ }
122
+ /**
123
+ * Start a timer for dumping the data, depending on the configuration.
124
+ *
125
+ * @param config - The config that determines whether to do the timed dump or not.
126
+ * @param storage - The storage with the information to dump.
127
+ * @param logger - The logger to use.
128
+ */
129
+ static maybeStartDumpTimer(config, storage, logger) {
130
+ if (config.dump_after_mins > 0) {
131
+ logger.info(`Will dump coverage information every ${config.dump_after_mins} minute(s).`);
132
+ const timer = setInterval(async () => {
133
+ await this.dumpCoverage(config, storage, logger);
134
+ }, config.dump_after_mins * 1000 * 60);
135
+ process.on('SIGINT', () => {
136
+ // Stop the timed file dump
137
+ if (timer) {
138
+ clearInterval(timer);
139
+ }
140
+ });
141
+ return {
142
+ stop: () => clearInterval(timer)
143
+ };
144
+ }
145
+ return {
146
+ stop() {
147
+ // no timer to stop yet
148
+ }
149
+ };
150
+ }
151
+ static async dumpCoverage(config, storage, logger) {
152
+ try {
153
+ // 1. Write coverage to a file
154
+ const [coverageFile, lines] = storage.dumpToSimpleCoverageFile(config.dump_to_folder, new Date());
155
+ logger.info(`Dumped ${lines} lines of coverage to ${coverageFile}.`);
156
+ // 2. Upload to Teamscale or Artifactory if configured
157
+ if (config.teamscale_server_url || config.artifactory_server_url) {
158
+ await this.uploadCoverage(config, coverageFile, lines, logger);
159
+ }
160
+ }
161
+ catch (e) {
162
+ if (e instanceof CommonUpload_1.UploadError) {
163
+ logger.error(`Coverage upload failed. The coverage files on disk (inside the folder "${config.dump_to_folder}") were not deleted.
164
+ You can still upload them manually.`, e);
165
+ }
166
+ else {
167
+ logger.error('Coverage dump failed.', e);
168
+ }
169
+ }
170
+ }
171
+ static async uploadCoverage(config, coverageFile, lines, logger) {
172
+ if (config.teamscale_server_url) {
173
+ await (0, TeamscaleUpload_1.uploadToTeamscale)(config, logger, coverageFile, lines);
174
+ }
175
+ if (config.artifactory_server_url) {
176
+ await (0, ArtifactoryUpload_1.uploadToArtifactory)(config, logger, coverageFile, lines);
177
+ }
178
+ // Delete coverage if upload was successful and keeping coverage files on disk was not configure by the user
179
+ if (!config.keep_coverage_files) {
180
+ fs.unlinkSync(coverageFile);
181
+ }
182
+ }
183
+ static startControlServer(config, storage, logger) {
184
+ if (!config.enable_control_port) {
185
+ return {
186
+ async stop() {
187
+ // nothing to stop in this case
188
+ }
189
+ };
190
+ }
191
+ const controlServer = (0, express_1.default)();
192
+ controlServer.use(express_1.default.text({}));
193
+ const serverSocket = controlServer.listen(config.enable_control_port);
194
+ controlServer.put('/partition', (request, response) => {
195
+ const targetPartition = request.body.trim();
196
+ config.teamscale_partition = targetPartition;
197
+ logger.info(`Switched the target partition to '${targetPartition}' via the control API.`);
198
+ response.sendStatus(200);
199
+ });
200
+ controlServer.post('/dump', async (request, response) => {
201
+ logger.info('Dumping coverage requested via the control API.');
202
+ await this.dumpCoverage(config, storage, logger);
203
+ response.sendStatus(200);
204
+ });
205
+ controlServer.put('/project', async (request, response) => {
206
+ const targetProject = request.body.trim();
207
+ config.teamscale_project = targetProject;
208
+ logger.info(`Switching the target project to '${targetProject}' via the control API.`);
209
+ response.sendStatus(200);
210
+ });
211
+ controlServer.put('/revision', async (request, response) => {
212
+ const targetRevision = request.body.trim();
213
+ config.teamscale_revision = targetRevision;
214
+ logger.info(`Switching the target revision to '${targetRevision}' via the control API.`);
215
+ response.sendStatus(200);
216
+ });
217
+ controlServer.put('/commit', async (request, response) => {
218
+ const targetCommit = request.body.trim();
219
+ config.teamscale_commit = targetCommit;
220
+ logger.info(`Switching the target commit to '${targetCommit}' via the control API.`);
221
+ response.sendStatus(200);
222
+ });
223
+ controlServer.put('/message', async (request, response) => {
224
+ const uploadMessage = request.body.trim();
225
+ config.teamscale_message = uploadMessage;
226
+ logger.info(`Switching the upload message to '${uploadMessage}' via the control API.`);
227
+ response.sendStatus(200);
228
+ });
229
+ controlServer.post('/reset', async (request, response) => {
230
+ storage.discardCollectedCoverage();
231
+ logger.info(`Discarding collected coverage information as requested via the control API.`);
232
+ response.sendStatus(200);
233
+ });
234
+ logger.info(`Control server enabled at port ${config.enable_control_port}`);
235
+ return {
236
+ async stop() {
237
+ return new Promise(resolve => {
238
+ serverSocket.close(() => resolve());
239
+ });
240
+ }
241
+ };
242
+ }
243
+ }
244
+ exports.App = App;
@@ -1,38 +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
- 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
- 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 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 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);
@@ -2,6 +2,17 @@
2
2
  import { Socket } from 'net';
3
3
  import { IDataStorage } from '../storage/DataStorage';
4
4
  import Logger from 'bunyan';
5
+ /**
6
+ * Coverage information that has not been mapped back to the
7
+ * original code using a source map.
8
+ */
9
+ export declare type UnmappedCoverage = {
10
+ fileId: string;
11
+ startLine: number;
12
+ startColumn: number;
13
+ endLine: number;
14
+ endColumn: number;
15
+ };
5
16
  /**
6
17
  * The session maintains the relevant information for a client.
7
18
  * One session is created for each client.
@@ -22,6 +33,10 @@ export declare class Session {
22
33
  * server per browser window.
23
34
  */
24
35
  private readonly sourceMaps;
36
+ /**
37
+ * Unmapped coverage information.
38
+ */
39
+ private readonly unmappedCoverage;
25
40
  /**
26
41
  * The logger to use.
27
42
  */
@@ -48,7 +63,7 @@ export declare class Session {
48
63
  * @param endLine - The line number within the bundle the range ends.
49
64
  * @param endColumn - The column in the given `startLine` on that the range ends (inclusive).
50
65
  */
51
- putCoverage(fileId: string, startLine: number, startColumn: number, endLine: number, endColumn: number): void;
66
+ putCoverage(fileId: string, startLine: number, startColumn: number, endLine: number, endColumn: number): boolean;
52
67
  /**
53
68
  * Map to the original file position.
54
69
  *
@@ -63,7 +78,8 @@ export declare class Session {
63
78
  * @param fileId - The identifier of the file bundle.
64
79
  * @param sourceMapText - The actual source map.
65
80
  */
66
- putSourcemap(fileId: string, sourceMapText: string): void;
81
+ putSourcemap(fileId: string, sourceMapText: string): Promise<void>;
82
+ private processUnmappedCoverageOf;
67
83
  /**
68
84
  * Destroy the session and free the memory it allocates.
69
85
  * In particular the sourcemaps are freed (important to not run out of memory!).
@@ -44,6 +44,7 @@ class Session {
44
44
  this.storage = commons_1.Contract.requireDefined(storage);
45
45
  this.logger = commons_1.Contract.requireDefined(logger);
46
46
  this.sourceMaps = new Map();
47
+ this.unmappedCoverage = new Map();
47
48
  this.projectId = ''; // We currently only support coverage for one project.
48
49
  }
49
50
  /**
@@ -58,6 +59,17 @@ class Session {
58
59
  */
59
60
  putCoverage(fileId, startLine, startColumn, endLine, endColumn) {
60
61
  var _a, _b;
62
+ // Delay the mapping if the sourcemap has not yet arrived
63
+ if (!this.sourceMaps.has(fileId)) {
64
+ let unmappedForFile = this.unmappedCoverage.get(fileId);
65
+ if (!unmappedForFile) {
66
+ unmappedForFile = [];
67
+ this.unmappedCoverage.set(fileId, unmappedForFile);
68
+ }
69
+ unmappedForFile.push({ endColumn, endLine, fileId, startColumn, startLine });
70
+ return false;
71
+ }
72
+ let mapped = false;
61
73
  // Iterate over the lines to scan
62
74
  let line = startLine;
63
75
  while (line <= endLine) {
@@ -87,12 +99,10 @@ class Session {
87
99
  if (originalPosition.line && originalPosition.source) {
88
100
  if (lastCoveredLine !== originalPosition.line) {
89
101
  this.storage.putCoverage(this.projectId, originalPosition.source, [originalPosition.line]);
102
+ mapped = true;
90
103
  lastCoveredLine = originalPosition.line;
91
104
  }
92
105
  }
93
- else {
94
- this.storage.signalUnmappedCoverage(this.projectId);
95
- }
96
106
  // Step to the next column to map back to the original.
97
107
  // `originalPosition.name` is the token on the position, that is, if it is present
98
108
  // we increment the column by its length.
@@ -101,6 +111,7 @@ class Session {
101
111
  // And the next line
102
112
  line++;
103
113
  }
114
+ return mapped;
104
115
  }
105
116
  /**
106
117
  * Map to the original file position.
@@ -125,14 +136,24 @@ class Session {
125
136
  * @param fileId - The identifier of the file bundle.
126
137
  * @param sourceMapText - The actual source map.
127
138
  */
128
- putSourcemap(fileId, sourceMapText) {
139
+ async putSourcemap(fileId, sourceMapText) {
129
140
  const rawSourceMap = JSON.parse(sourceMapText);
130
- new sourceMap.SourceMapConsumer(rawSourceMap)
131
- .then(consumer => {
132
- this.sourceMaps.set(fileId, consumer);
133
- })
134
- .catch(e => {
141
+ try {
142
+ const sourceMapConsumer = await new sourceMap.SourceMapConsumer(rawSourceMap);
143
+ this.sourceMaps.set(fileId, sourceMapConsumer);
144
+ this.processUnmappedCoverageOf(fileId);
145
+ }
146
+ catch (e) {
135
147
  this.logger.error(`Consuming source map failed! ${e}`);
148
+ }
149
+ }
150
+ processUnmappedCoverageOf(fileId) {
151
+ var _a;
152
+ const unmapped = (_a = this.unmappedCoverage.get(fileId)) !== null && _a !== void 0 ? _a : [];
153
+ unmapped.forEach((entry) => {
154
+ if (!this.putCoverage(entry.fileId, entry.startLine, entry.startColumn, entry.endLine, entry.endColumn)) {
155
+ this.storage.signalUnmappedCoverage(this.projectId);
156
+ }
136
157
  });
137
158
  }
138
159
  /**
@@ -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,6 @@
1
+ import { ConfigParameters } from '../utils/ConfigParameters';
2
+ import Logger from 'bunyan';
3
+ /**
4
+ * Uploads a coverage file to artifactory with the provided configuration.
5
+ */
6
+ export declare function uploadToArtifactory(config: ConfigParameters, logger: Logger, coverageFile: string, lines: number): Promise<void>;
@@ -0,0 +1,62 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.uploadToArtifactory = void 0;
7
+ const CommonUpload_1 = require("./CommonUpload");
8
+ const axios_1 = __importDefault(require("axios"));
9
+ /**
10
+ * Uploads a coverage file to artifactory with the provided configuration.
11
+ */
12
+ async function uploadToArtifactory(config, logger, coverageFile, lines) {
13
+ if (!(config.artifactory_access_token || (config.artifactory_user && config.artifactory_password))) {
14
+ throw new CommonUpload_1.UploadError('API key or user name and password must be configured!');
15
+ }
16
+ if (lines === 0) {
17
+ return;
18
+ }
19
+ logger.info('Preparing upload to Artifactory');
20
+ const form = (0, CommonUpload_1.prepareFormData)(coverageFile);
21
+ await performArtifactoryUpload(config, form, logger);
22
+ }
23
+ exports.uploadToArtifactory = uploadToArtifactory;
24
+ async function performArtifactoryUpload(config, form, logger) {
25
+ var _a;
26
+ if (!config.teamscale_commit) {
27
+ throw new CommonUpload_1.UploadError('The "--teamscale-commit" option must be set with a valid branch and timestamp.');
28
+ }
29
+ const branchAndTimestamp = config.teamscale_commit.split(':');
30
+ let url = `${(_a = config.artifactory_server_url) === null || _a === void 0 ? void 0 : _a.replace(/\/$/, '')}/uploads/${branchAndTimestamp[0]}/${branchAndTimestamp[1]}`;
31
+ if (config.teamscale_revision) {
32
+ url = url + `-${config.teamscale_revision}`;
33
+ }
34
+ url = url + `/${config.teamscale_partition}/simple`;
35
+ if (config.artifactory_path_suffix !== undefined) {
36
+ url = `${url}/${config.artifactory_path_suffix}`;
37
+ }
38
+ url = `${url}/report.simple`;
39
+ await (0, CommonUpload_1.performUpload)(url, form, prepareArtifactoryConfig(config, form), axios_1.default.put, logger);
40
+ }
41
+ function prepareArtifactoryConfig(config, form) {
42
+ var _a, _b;
43
+ if (config.artifactory_access_token) {
44
+ return {
45
+ headers: {
46
+ Accept: '*/*',
47
+ 'X-JFrog-Art-Api': config.artifactory_access_token,
48
+ 'Content-Type': `multipart/form-data; boundary=${form.getBoundary()}`
49
+ }
50
+ };
51
+ }
52
+ return {
53
+ auth: {
54
+ username: (_a = config.artifactory_user) !== null && _a !== void 0 ? _a : 'no username provided',
55
+ password: (_b = config.artifactory_password) !== null && _b !== void 0 ? _b : 'no password provided'
56
+ },
57
+ headers: {
58
+ Accept: '*/*',
59
+ 'Content-Type': `multipart/form-data; boundary=${form.getBoundary()}`
60
+ }
61
+ };
62
+ }
@@ -0,0 +1,16 @@
1
+ import FormData from 'form-data';
2
+ import Logger from 'bunyan';
3
+ import { AxiosRequestConfig, AxiosResponse } from 'axios';
4
+ /**
5
+ * Error that is thrown when the upload failed
6
+ */
7
+ export declare class UploadError extends Error {
8
+ }
9
+ /**
10
+ * Prepares the form data from a given configuration file for the upload.
11
+ */
12
+ export declare function prepareFormData(coverageFile: string): FormData;
13
+ /**
14
+ * Uploads a coverage file with the provided configuration.
15
+ */
16
+ export declare function performUpload(url: string, form: FormData, config: AxiosRequestConfig<FormData>, uploadFunction: <T = any, R = AxiosResponse<T>, D = FormData>(url: string, data?: D, config?: AxiosRequestConfig<D>) => Promise<R>, logger: Logger): Promise<void>;
@@ -0,0 +1,55 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.performUpload = exports.prepareFormData = exports.UploadError = void 0;
7
+ const form_data_1 = __importDefault(require("form-data"));
8
+ const fs_1 = __importDefault(require("fs"));
9
+ const util_1 = require("util");
10
+ /**
11
+ * Error that is thrown when the upload failed
12
+ */
13
+ class UploadError extends Error {
14
+ }
15
+ exports.UploadError = UploadError;
16
+ /**
17
+ * Prepares the form data from a given configuration file for the upload.
18
+ */
19
+ function prepareFormData(coverageFile) {
20
+ const form = new form_data_1.default();
21
+ form.append('report', fs_1.default.createReadStream(coverageFile), 'coverage.simple');
22
+ return form;
23
+ }
24
+ exports.prepareFormData = prepareFormData;
25
+ /**
26
+ * Uploads a coverage file with the provided configuration.
27
+ */
28
+ async function performUpload(url, form, config, uploadFunction, logger) {
29
+ try {
30
+ const response = await uploadFunction(url, form, config);
31
+ logger.info(`Upload finished with code ${response.status}.`);
32
+ }
33
+ catch (error) {
34
+ if (error.response) {
35
+ const response = error.response;
36
+ if (response.status >= 400) {
37
+ throw new UploadError(`Upload failed with code ${response.status}: ${response.statusText}. Response Data: ${response.data}`);
38
+ }
39
+ else {
40
+ logger.info(`Upload with status code ${response.status} finished.`);
41
+ }
42
+ }
43
+ else if (error.request) {
44
+ throw new UploadError(`Upload request did not receive a response.`);
45
+ }
46
+ if (error.message) {
47
+ logger.debug(`Something went wrong when uploading data: ${error.message}. Details of the error: ${(0, util_1.inspect)(error)}`);
48
+ throw new UploadError(`Something went wrong when uploading data: ${error.message}`);
49
+ }
50
+ else {
51
+ throw new UploadError(`Something went wrong when uploading data: ${(0, util_1.inspect)(error)}`);
52
+ }
53
+ }
54
+ }
55
+ exports.performUpload = performUpload;
@@ -0,0 +1,6 @@
1
+ import { ConfigParameters } from '../utils/ConfigParameters';
2
+ import Logger from 'bunyan';
3
+ /**
4
+ * Uploads a coverage file to Teamscale with the provided configuration.
5
+ */
6
+ export declare function uploadToTeamscale(config: ConfigParameters, logger: Logger, coverageFile: string, lines: number): Promise<void>;
@@ -0,0 +1,52 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.uploadToTeamscale = void 0;
7
+ const QueryParameters_1 = __importDefault(require("../utils/QueryParameters"));
8
+ const CommonUpload_1 = require("./CommonUpload");
9
+ const axios_1 = __importDefault(require("axios"));
10
+ /**
11
+ * Uploads a coverage file to Teamscale with the provided configuration.
12
+ */
13
+ async function uploadToTeamscale(config, logger, coverageFile, lines) {
14
+ if (!(config.teamscale_access_token && config.teamscale_user && config.teamscale_server_url)) {
15
+ throw new CommonUpload_1.UploadError('API key and user name must be configured!');
16
+ }
17
+ if (lines === 0) {
18
+ return;
19
+ }
20
+ logger.info('Preparing upload to Teamscale');
21
+ const form = (0, CommonUpload_1.prepareFormData)(coverageFile);
22
+ const queryParameters = prepareQueryParameters(config);
23
+ await performTeamscaleUpload(config, queryParameters, form, logger);
24
+ }
25
+ exports.uploadToTeamscale = uploadToTeamscale;
26
+ async function performTeamscaleUpload(config, parameters, form, logger) {
27
+ var _a;
28
+ await (0, CommonUpload_1.performUpload)(`${(_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, prepareTeamscaleConfig(config, form), axios_1.default.post, logger);
29
+ }
30
+ function prepareQueryParameters(config) {
31
+ const parameters = new QueryParameters_1.default();
32
+ parameters.addIfDefined('format', 'SIMPLE');
33
+ parameters.addIfDefined('message', config.teamscale_message);
34
+ parameters.addIfDefined('repository', config.teamscale_repository);
35
+ parameters.addIfDefined('t', config.teamscale_commit);
36
+ parameters.addIfDefined('revision', config.teamscale_revision);
37
+ parameters.addIfDefined('partition', config.teamscale_partition);
38
+ return parameters;
39
+ }
40
+ function prepareTeamscaleConfig(config, form) {
41
+ var _a, _b;
42
+ return {
43
+ auth: {
44
+ username: (_a = config.teamscale_user) !== null && _a !== void 0 ? _a : 'no username provided',
45
+ password: (_b = config.teamscale_access_token) !== null && _b !== void 0 ? _b : 'no password provided'
46
+ },
47
+ headers: {
48
+ Accept: '*/*',
49
+ 'Content-Type': `multipart/form-data; boundary=${form.getBoundary()}`
50
+ }
51
+ };
52
+ }
@@ -0,0 +1,35 @@
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
+ artifactory_server_url?: string;
26
+ artifactory_user?: string;
27
+ artifactory_password?: string;
28
+ artifactory_access_token?: string;
29
+ artifactory_path_suffix?: string;
30
+ enable_control_port?: number;
31
+ };
32
+ /**
33
+ * Construct the object for parsing the command line arguments.
34
+ */
35
+ export declare function buildParameterParser(): ArgumentParser;
@@ -0,0 +1,103 @@
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
+ parser.add_argument('--artifactory-server-url', {
82
+ help: 'Upload the coverage to the given Artifactory server URL. The URL may include a subpath on the artifactory server, e.g. https://artifactory.acme.com/my-repo/my/subpath',
83
+ default: process.env.ARTIFACTORY_SERVER_URL
84
+ });
85
+ parser.add_argument('--artifactory-user', {
86
+ help: 'The user for uploading coverage to Artifactory. Only needed when not using the --artifactory-access-token option',
87
+ default: process.env.ARTIFACTORY_USER
88
+ });
89
+ parser.add_argument('--artifactory-password', {
90
+ help: 'The password for uploading coverage to Artifactory. Only needed when not using the --artifactory-access-token option',
91
+ default: process.env.ARTIFACTORY_PASSWORD
92
+ });
93
+ parser.add_argument('--artifactory-access-token', {
94
+ help: 'The access_token for uploading coverage to Artifactory.',
95
+ default: process.env.ARTIFACTORY_ACCESS_TOKEN
96
+ });
97
+ parser.add_argument('--artifactory-path-suffix', {
98
+ help: '(optional): The path within the storage location between the default path and the uploaded artifact.',
99
+ default: process.env.ARTIFACTORY_PATH_SUFFIX
100
+ });
101
+ return parser;
102
+ }
103
+ 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.42",
3
+ "version": "0.0.1-beta.45",
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/**/*"
@@ -29,10 +29,11 @@
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",
35
- "source-map": "^0.7.3",
36
+ "source-map": "^0.7.4",
36
37
  "tmp": "^0.2.1",
37
38
  "typescript-optional": "^2.0.1",
38
39
  "ws": "^7.4.5"
@@ -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.4.0",
55
58
  "rimraf": "^3.0.2",
56
59
  "ts-jest": "^27.0.5",
57
60
  "ts-node": "^10.2.1",