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

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.41",
3
+ "version": "0.0.1-beta.42",
4
4
  "description": "Collector for JavaScript code coverage information",
5
5
  "main": "dist/src/main.js",
6
6
  "bin": "dist/src/main.js",
@@ -27,6 +27,7 @@
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",
31
32
  "form-data": "^4.0.0",
32
33
  "mkdirp": "^1.0.4",
@@ -5,6 +5,7 @@ import 'dotenv/config';
5
5
  * Used to start the collector for with a given configuration.
6
6
  */
7
7
  export declare class Main {
8
+ private static readonly DEFAULT_COVERAGE_LOCATION;
8
9
  /**
9
10
  * Construct the object for parsing the command line arguments.
10
11
  */
@@ -31,4 +32,7 @@ export declare class Main {
31
32
  private static maybeStartDumpTimer;
32
33
  private static dumpCoverage;
33
34
  private static uploadToTeamscale;
35
+ private static performTeamscaleUpload;
36
+ private static prepareQueryParameters;
37
+ private static prepareFormData;
34
38
  }
package/dist/src/main.js CHANGED
@@ -39,11 +39,15 @@ const axios_1 = __importDefault(require("axios"));
39
39
  const form_data_1 = __importDefault(require("form-data"));
40
40
  const QueryParameters_1 = __importDefault(require("./utils/QueryParameters"));
41
41
  const util_1 = require("util");
42
- const tmp_1 = __importDefault(require("tmp"));
43
42
  const mkdirp_1 = __importDefault(require("mkdirp"));
44
43
  const path_1 = __importDefault(require("path"));
45
44
  const StdConsoleLogger_1 = require("./utils/StdConsoleLogger");
46
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
+ }
47
51
  /**
48
52
  * The main class of the Teamscale JavaScript Collector.
49
53
  * Used to start the collector for with a given configuration.
@@ -60,7 +64,15 @@ class Main {
60
64
  });
61
65
  parser.add_argument('-v', '--version', { action: 'version', version: package_json_1.version });
62
66
  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.' });
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
+ });
64
76
  parser.add_argument('-l', '--log-to-file', { help: 'Log file', default: 'logs/collector-combined.log' });
65
77
  parser.add_argument('-e', '--log-level', { help: 'Log level', default: 'info' });
66
78
  parser.add_argument('-t', '--dump-after-mins', {
@@ -153,11 +165,6 @@ class Main {
153
165
  const logger = this.buildLogger(config);
154
166
  logger.info(`Starting collector in working directory "${process.cwd()}".`);
155
167
  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
168
  // Prepare the storage and the server
162
169
  const storage = new DataStorage_1.DataStorage(logger);
163
170
  const server = new CollectingServer_1.WebSocketCollectingServer(config.port, storage, logger);
@@ -196,52 +203,48 @@ class Main {
196
203
  }
197
204
  }
198
205
  static async dumpCoverage(config, storage, logger) {
199
- var _a;
200
206
  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) {
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) {
214
215
  fs.unlinkSync(coverageFile);
215
216
  }
216
217
  }
217
218
  }
218
219
  catch (e) {
219
- logger.error('Coverage dump failed.', 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
+ }
220
227
  }
221
228
  }
222
229
  static async uploadToTeamscale(config, logger, coverageFile, lines) {
223
230
  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;
231
+ throw new TeamscaleUploadError('API key and user name must be configured!');
226
232
  }
227
233
  if (lines === 0) {
228
234
  return;
229
235
  }
230
236
  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);
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;
240
243
  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, {
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, {
242
245
  auth: {
243
- username: config.teamscale_user,
244
- password: config.teamscale_access_token
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'
245
248
  },
246
249
  headers: {
247
250
  Accept: '*/*',
@@ -252,25 +255,40 @@ class Main {
252
255
  if (error.response) {
253
256
  const response = error.response;
254
257
  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}`);
258
+ throw new TeamscaleUploadError(`Upload failed with code ${response.status}: ${response.statusText}. Response Data: ${response.data}`);
257
259
  }
258
260
  else {
259
261
  logger.info(`Upload with status code ${response.status} finished.`);
260
262
  }
261
263
  }
262
264
  else if (error.request) {
263
- logger.error(`Upload request did not receive a response.`);
265
+ throw new TeamscaleUploadError(`Upload request did not receive a response.`);
264
266
  }
265
267
  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
+ 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}`);
268
270
  }
269
271
  else {
270
- logger.error(`Something went wrong when uploading data: ${(0, util_1.inspect)(error)}`);
272
+ throw new TeamscaleUploadError(`Something went wrong when uploading data: ${(0, util_1.inspect)(error)}`);
271
273
  }
272
274
  });
273
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
+ }
274
291
  }
275
292
  exports.Main = Main;
293
+ Main.DEFAULT_COVERAGE_LOCATION = 'coverage';
276
294
  Main.run();
@@ -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.
@@ -89,7 +92,7 @@ export declare class DataStorage implements IDataStorage {
89
92
  /**
90
93
  * Coverage information by project.
91
94
  */
92
- private readonly coverageByProject;
95
+ private coverageByProject;
93
96
  /**
94
97
  * Logger to use.
95
98
  */
@@ -98,6 +101,10 @@ export declare class DataStorage implements IDataStorage {
98
101
  * Times unmapped coverage received.
99
102
  */
100
103
  private timesUnmappedCoverage;
104
+ /**
105
+ * Date format for the timestamp appended to the coverage files
106
+ */
107
+ readonly DATE_FORMAT = "YYYY-MM-DD-HH-mm-ss.SSS";
101
108
  /**
102
109
  * Constructs the data storage.
103
110
  *
@@ -118,7 +125,7 @@ export declare class DataStorage implements IDataStorage {
118
125
  *
119
126
  * @param sourceFile - The file name to normalize, produced by the instrumenter.
120
127
  */
121
- private normalizeSourceFileName;
128
+ private static normalizeSourceFileName;
122
129
  /**
123
130
  * {@inheritDoc IWriteableStorage.signalUnmappedCoverage}
124
131
  */
@@ -128,9 +135,25 @@ export declare class DataStorage implements IDataStorage {
128
135
  */
129
136
  getCoverageBySourceFile(project: string): IterableIterator<FileCoverage> | undefined;
130
137
  /**
131
- * {@inheritDoc IReadableStorage.writeToSimpleCoverageFile}
138
+ * @inheritDoc
139
+ */
140
+ dumpToSimpleCoverageFile(coverageFolder: string, date: Date): [string, number];
141
+ /**
142
+ * Set the collected coverage to 0 for all projects
143
+ * @private
144
+ */
145
+ private resetCoverage;
146
+ /**
147
+ * Appends the timestamp given with date to the coverageFolder (before the file ending if there is one)
148
+ * @param coverageFolder Path to the coverage file
149
+ * @param date Represents the timestamp to be appended with the format {@link DataStorage.DATE_FORMAT}
150
+ * @private
151
+ */
152
+ private initCoverageFile;
153
+ /**
154
+ * Generate simple coverage format for the collected coverage
132
155
  */
133
- dumpToSimpleCoverageFile(filePath: string): number;
156
+ private toSimpleCoverage;
134
157
  /**
135
158
  * {@inheritDoc IReadableStorage.getProjects}
136
159
  */
@@ -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}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@teamscale/coverage-collector",
3
- "version": "0.0.1-beta.41",
3
+ "version": "0.0.1-beta.42",
4
4
  "description": "Collector for JavaScript code coverage information",
5
5
  "main": "dist/src/main.js",
6
6
  "bin": "dist/src/main.js",
@@ -27,6 +27,7 @@
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",
31
32
  "form-data": "^4.0.0",
32
33
  "mkdirp": "^1.0.4",