@teamscale/coverage-collector 0.1.0-beta.8 → 1.0.0-beta.2

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.
Files changed (34) hide show
  1. package/dist/package.json +23 -25
  2. package/dist/src/config/RemoteProfilerConfig.d.ts +24 -0
  3. package/dist/src/config/RemoteProfilerConfig.js +76 -0
  4. package/dist/src/control/App.d.ts +31 -0
  5. package/dist/src/control/App.js +202 -0
  6. package/dist/src/control/ControlServer.d.ts +24 -0
  7. package/dist/src/control/ControlServer.js +100 -0
  8. package/dist/src/control/CoverageDumper.d.ts +27 -0
  9. package/dist/src/control/CoverageDumper.js +166 -0
  10. package/dist/src/main.js +29 -2
  11. package/dist/src/receiver/CollectingServer.d.ts +2 -2
  12. package/dist/src/receiver/CollectingServer.js +70 -20
  13. package/dist/src/receiver/Session.d.ts +15 -25
  14. package/dist/src/receiver/Session.js +34 -7
  15. package/dist/src/storage/DataStorage.d.ts +154 -81
  16. package/dist/src/storage/DataStorage.js +361 -81
  17. package/dist/src/upload/ArtifactoryUpload.d.ts +2 -2
  18. package/dist/src/upload/ArtifactoryUpload.js +25 -28
  19. package/dist/src/upload/TeamscaleUpload.d.ts +8 -2
  20. package/dist/src/upload/TeamscaleUpload.js +67 -31
  21. package/dist/src/utils/PrettyFileLogger.d.ts +0 -1
  22. package/dist/src/utils/PrettyFileLogger.js +1 -0
  23. package/dist/src/utils/RestApis.d.ts +37 -0
  24. package/dist/src/utils/RestApis.js +141 -0
  25. package/dist/src/utils/StdConsoleLogger.js +11 -1
  26. package/package.json +23 -25
  27. package/dist/src/App.d.ts +0 -44
  28. package/dist/src/App.js +0 -267
  29. package/dist/src/upload/CommonUpload.d.ts +0 -16
  30. package/dist/src/upload/CommonUpload.js +0 -63
  31. package/dist/src/upload/ProxyUpload.d.ts +0 -6
  32. package/dist/src/upload/ProxyUpload.js +0 -30
  33. package/dist/src/utils/ConfigParameters.d.ts +0 -36
  34. package/dist/src/utils/ConfigParameters.js +0 -107
@@ -0,0 +1,166 @@
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.CoverageDumper = void 0;
7
+ const RestApis_1 = require("../utils/RestApis");
8
+ const TeamscaleUpload_1 = require("../upload/TeamscaleUpload");
9
+ const ArtifactoryUpload_1 = require("../upload/ArtifactoryUpload");
10
+ const fs_1 = __importDefault(require("fs"));
11
+ const CONFIG_REQUERY_INTERVAL_SECONDS = 53;
12
+ const DUMP_COVERAGE_CHECK_AFTER_SECONDS = 60;
13
+ const CHECK_FOR_SCHEDULED_ACTIVITY_AFTER_SECONDS = 31;
14
+ const DEFAULT_COVERAGE_DUMP_AFTER_MINS = 10;
15
+ const STARTUP_TIMESTAMP = Date.now();
16
+ /**
17
+ * Functionality for dumping coverage regularly to different targets.
18
+ */
19
+ class CoverageDumper {
20
+ /**
21
+ * Start a timer for dumping the data, depending on the configuration;
22
+ * and for retrieving new configuration values from Teamscale.
23
+ *
24
+ * @param storage - The storage with the information to dump.
25
+ * @param logger - The logger to use.
26
+ */
27
+ static startRegularCollectorProcesses(storage, logger) {
28
+ const lastConfigRefreshPerApp = new Map();
29
+ const lastDumpTimestampPerApp = new Map();
30
+ const doIfExpired = async (key, lastExecutionTimestamps, doAfterMillis, action) => {
31
+ const lastExecutionTimestamp = lastExecutionTimestamps.get(key) ?? STARTUP_TIMESTAMP;
32
+ if (Date.now() - lastExecutionTimestamp > doAfterMillis) {
33
+ try {
34
+ await action();
35
+ }
36
+ catch (error) {
37
+ logger.error(`Regular collector action failed with an error: ` + error?.message);
38
+ logger.error(error);
39
+ }
40
+ lastExecutionTimestamps.set(key, Date.now());
41
+ }
42
+ };
43
+ const timer = setInterval(async () => {
44
+ // 1. Re-query the profiler configurations (can be more than one,
45
+ // one for each application) once every minute.
46
+ for (const appId of storage.getApplicationIDs()) {
47
+ await doIfExpired(`config-update-${appId}`, lastConfigRefreshPerApp, 1000 * CONFIG_REQUERY_INTERVAL_SECONDS, async () => {
48
+ await storage.refreshApplicationConfiguration(appId);
49
+ });
50
+ }
51
+ // 2. For each application, check if coverage shall be dumped once a minute.
52
+ // Each application can be configured differently, even if the same config id was assigned initially.
53
+ for (const appId of storage.getApplicationIDs()) {
54
+ const appDumpAfterMinutes = storage.getAppConfiguration(appId).dumpAfterMins ?? DEFAULT_COVERAGE_DUMP_AFTER_MINS;
55
+ await doIfExpired(`coverage-dump-${appId}`, lastDumpTimestampPerApp, 1000 * DUMP_COVERAGE_CHECK_AFTER_SECONDS * appDumpAfterMinutes, async () => {
56
+ await this.dumpCoverage(storage, logger, appId);
57
+ });
58
+ }
59
+ }, 1000 * CHECK_FOR_SCHEDULED_ACTIVITY_AFTER_SECONDS);
60
+ // Dump the coverage before each (correct) config update.
61
+ storage.addBeforeConfigUpdateCallback(async (appId, dataStorage) => {
62
+ logger.info(`Dumping coverage before config update for application ${appId}.`);
63
+ await this.dumpCoverage(dataStorage, logger, appId);
64
+ });
65
+ logger.info(`Started the regular remote configuration refresh and coverage dump.`);
66
+ return {
67
+ stop: () => {
68
+ if (timer) {
69
+ clearInterval(timer);
70
+ }
71
+ }
72
+ };
73
+ }
74
+ /**
75
+ * Dump all the collected coverage. If an `onlyForAppId` is given, only for the app with that ID.
76
+ * If no `onlyForAppId` is given, coverage will be dumped for every application, separately.
77
+ */
78
+ static async dumpCoverage(storage, logger, onlyForAppId) {
79
+ const appsToDumpFor = onlyForAppId ? [onlyForAppId] : storage.getApplicationIDs();
80
+ const dumpTimestamp = new Date();
81
+ let hadCoverageToDump = false;
82
+ let hadUploadSent = false;
83
+ for (const dumpForAppId of appsToDumpFor) {
84
+ const dumpConfiguration = storage.getAppConfiguration(dumpForAppId);
85
+ const dumpToFolder = this.determineCoverageTargetFolder(dumpForAppId, dumpConfiguration);
86
+ const deleteAfterUpload = !(dumpConfiguration.keepCoverageFiles ?? dumpConfiguration.dumpToFolder !== undefined);
87
+ try {
88
+ // Write coverage to a file
89
+ const dumpSummary = storage.dumpToSimpleCoverageFile(dumpToFolder, dumpTimestamp, dumpForAppId);
90
+ hadCoverageToDump = dumpSummary.hadCoverageToDump || hadCoverageToDump;
91
+ // Upload to Teamscale or Artifactory if configured
92
+ for (const { simpleCoverageFile, simpleCoverageFileLines, commit } of dumpSummary.details) {
93
+ try {
94
+ if (dumpConfiguration.teamscaleServerUrl || dumpConfiguration.artifactoryServerUrl) {
95
+ const coverageUploaded = await this.uploadCoverage(dumpConfiguration, simpleCoverageFile, simpleCoverageFileLines, commit, logger, deleteAfterUpload);
96
+ hadUploadSent = coverageUploaded || hadUploadSent;
97
+ }
98
+ }
99
+ catch (e) {
100
+ if (e instanceof RestApis_1.UploadError) {
101
+ logger.error(`Coverage upload failed for application ${dumpForAppId} and commit ${commit}.\n`
102
+ + `The coverage files on disk (inside the folder "${dumpToFolder}") were not deleted. `
103
+ + `You can still upload them manually. Error: ${e.message}`);
104
+ }
105
+ else {
106
+ logger.error('Coverage dump failed', e);
107
+ }
108
+ }
109
+ }
110
+ }
111
+ catch (e) {
112
+ logger.error(`Dumping coverage failed for application ${dumpForAppId}`, e);
113
+ }
114
+ }
115
+ if (hadUploadSent) {
116
+ logger.info(`Uploaded coverage for timestamp ${dumpTimestamp.toISOString()} to a remote server.`);
117
+ }
118
+ if (!hadCoverageToDump) {
119
+ logger.info('Coverage dump request was processed successfully; no new coverage to dump found.');
120
+ }
121
+ }
122
+ /**
123
+ * Returns true if an actual upload request was sent to the remote server.
124
+ */
125
+ static async uploadCoverage(config, coverageFile, lines, commit, logger, deleteAfterUpload) {
126
+ let uploadDone = false;
127
+ if (config.teamscaleServerUrl && (config.teamscaleProject ?? "").length > 0) {
128
+ // A Teamscale url might be configured for remote configuration. Uploads might not be intended.
129
+ uploadDone = await (0, TeamscaleUpload_1.uploadToTeamscale)(config, logger, coverageFile, lines, commit);
130
+ }
131
+ if (config.artifactoryServerUrl) {
132
+ uploadDone = uploadDone || await (0, ArtifactoryUpload_1.uploadToArtifactory)(config, logger, coverageFile, lines, commit);
133
+ }
134
+ // Delete coverage if upload was successful and keeping coverage files on disk was not configure by the user
135
+ if (deleteAfterUpload) {
136
+ fs_1.default.unlinkSync(coverageFile);
137
+ }
138
+ return uploadDone;
139
+ }
140
+ static determineCoverageTargetFolder(appId, config) {
141
+ const result = `${config.dumpFolder}/${makeProperDirectoryPath(config.dumpToFolder)}${appId}`;
142
+ fs_1.default.mkdirSync(result, { recursive: true });
143
+ return result;
144
+ }
145
+ }
146
+ exports.CoverageDumper = CoverageDumper;
147
+ /**
148
+ * Only characters that allowed in most file systems. And replace backslashes with forward slashes.
149
+ */
150
+ function makeProperDirectoryPath(path) {
151
+ if (path === undefined || path.trim().length === 0) {
152
+ return '';
153
+ }
154
+ // Remove leading slashes.
155
+ path = path.trim().replace(/^\/+/g, '');
156
+ // Replace backslashes with forward slashes.
157
+ path = path.replace(/\\/g, '/');
158
+ // Remove all characters that are not allowed in file paths.
159
+ // Important: We do not allow points in file paths to avoid directory traversal attacks/confusion.
160
+ path = path.replace(/[^a-zA-Z0-9\-_/]/g, '_');
161
+ // Ensure a trailing slash.
162
+ if (path.endsWith('/')) {
163
+ return path;
164
+ }
165
+ return `${path}/`;
166
+ }
package/dist/src/main.js CHANGED
@@ -1,5 +1,32 @@
1
1
  #!/usr/bin/env node
2
2
  "use strict";
3
+ var __importDefault = (this && this.__importDefault) || function (mod) {
4
+ return (mod && mod.__esModule) ? mod : { "default": mod };
5
+ };
3
6
  Object.defineProperty(exports, "__esModule", { value: true });
4
- const App_1 = require("./App");
5
- App_1.App.run();
7
+ const App_1 = require("./control/App");
8
+ const node_process_1 = __importDefault(require("node:process"));
9
+ const commons_1 = require("@cqse/commons");
10
+ /**
11
+ * Start the application from its entry point function `App.run()`.
12
+ * The present file is defined to be the main file of the collector in `package.json`.
13
+ */
14
+ (async () => {
15
+ const app = await App_1.App.run();
16
+ const handleExit = async () => {
17
+ await app.stop();
18
+ node_process_1.default.exit();
19
+ };
20
+ // Graceful termination on CTRL+C and SIGTERM.
21
+ node_process_1.default.on('SIGINT', () => handleExit());
22
+ node_process_1.default.on('SIGTERM', () => handleExit());
23
+ })().catch(reason => {
24
+ if (reason instanceof commons_1.InvalidConfigurationException) {
25
+ // Do not print the stack trace for invalid configuration parameters from the user.
26
+ console.error(`Failed: ${reason.message}`);
27
+ }
28
+ else {
29
+ console.error('Failed: ', reason);
30
+ }
31
+ node_process_1.default.exit(1);
32
+ });
@@ -1,4 +1,4 @@
1
- import { IDataStorage } from '../storage/DataStorage';
1
+ import { DataStorage } from '../storage/DataStorage';
2
2
  import Logger from 'bunyan';
3
3
  /**
4
4
  * A WebSocket based implementation of a coverage receiver.
@@ -32,7 +32,7 @@ export declare class WebSocketCollectingServer {
32
32
  * @param storage - The storage to put the received coverage information to.
33
33
  * @param logger - The logger to use.
34
34
  */
35
- constructor(port: number, storage: IDataStorage, logger: Logger);
35
+ constructor(port: number, storage: DataStorage, logger: Logger);
36
36
  /**
37
37
  * Start the server socket, handle sessions and dispatch messages.
38
38
  */
@@ -15,13 +15,23 @@ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (
15
15
  }) : function(o, v) {
16
16
  o["default"] = v;
17
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
- };
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
25
35
  Object.defineProperty(exports, "__esModule", { value: true });
26
36
  exports.WebSocketCollectingServer = void 0;
27
37
  const WebSocket = __importStar(require("ws"));
@@ -29,11 +39,33 @@ const commons_1 = require("@cqse/commons");
29
39
  const Session_1 = require("./Session");
30
40
  /** A message that provides coverage information */
31
41
  const MESSAGE_TYPE_COVERAGE = 'c';
42
+ /** A message that defines the bucket to put the coverage into */
43
+ const MESSAGE_TYPE_BUCKET = 'b';
32
44
  /**
33
45
  * A WebSocket based implementation of a coverage receiver.
34
46
  * Receives coverage from instrumented JavaScript code.
35
47
  */
36
48
  class WebSocketCollectingServer {
49
+ /**
50
+ * The WebSocket server component.
51
+ */
52
+ server;
53
+ /**
54
+ * The storage to put the received coverage information to for aggregation and further processing.
55
+ */
56
+ storage;
57
+ /**
58
+ * The logger to use.
59
+ */
60
+ logger;
61
+ /**
62
+ * The number of messages that have been received.
63
+ */
64
+ totalNumMessagesReceived;
65
+ /**
66
+ * The number of coverage messages that have been received.
67
+ */
68
+ totalNumCoverageMessagesReceived;
37
69
  /**
38
70
  * Constructor.
39
71
  *
@@ -53,11 +85,10 @@ class WebSocketCollectingServer {
53
85
  * Start the server socket, handle sessions and dispatch messages.
54
86
  */
55
87
  start() {
56
- var _a, _b;
57
- this.logger.info(`Starting server on port ${(_a = this.server) === null || _a === void 0 ? void 0 : _a.options.port}.`);
88
+ this.logger.info(`Starting server on port ${this.server?.options.port}.`);
58
89
  // Handle new connections from clients
59
- (_b = this.server) === null || _b === void 0 ? void 0 : _b.on('connection', (webSocket, req) => {
60
- let session = new Session_1.Session(req.socket, this.storage, this.logger);
90
+ this.server?.on('connection', (webSocket, req) => {
91
+ let session = new Session_1.Session(this.storage);
61
92
  this.logger.trace(`Connection from: ${req.socket.remoteAddress}`);
62
93
  // Handle disconnecting clients
63
94
  webSocket.on('close', code => {
@@ -80,8 +111,8 @@ class WebSocketCollectingServer {
80
111
  });
81
112
  return {
82
113
  stop: () => {
83
- var _a;
84
- (_a = this.server) === null || _a === void 0 ? void 0 : _a.close();
114
+ this.server?.clients.forEach(client => client.close());
115
+ this.server?.close();
85
116
  this.server = null;
86
117
  }
87
118
  };
@@ -95,13 +126,26 @@ class WebSocketCollectingServer {
95
126
  async handleMessage(session, message) {
96
127
  try {
97
128
  const messageType = message.toString('utf8', 0, 1);
98
- if (messageType.startsWith(MESSAGE_TYPE_COVERAGE)) {
129
+ if (messageType === MESSAGE_TYPE_BUCKET) {
130
+ const bucketDefinitionEncoded = message.toString('utf-8', 1).trim();
131
+ const bucketDefinitionDecoded = Buffer.from(bucketDefinitionEncoded, 'base64').toString('utf8');
132
+ const bucketDefinition = JSON.parse(bucketDefinitionDecoded);
133
+ this.logger.debug(`Received bucket definition: ${JSON.stringify(bucketDefinition)} for session ${session.sessionId}.`);
134
+ await session.setupBucketAndApplication(bucketDefinition);
135
+ }
136
+ else if (messageType === MESSAGE_TYPE_COVERAGE) {
99
137
  this.totalNumCoverageMessagesReceived += 1;
100
- await this.handleCoverageMessage(session, message.subarray(1));
138
+ const linesCoveredApprox = this.handleCoverageMessage(session, message.subarray(1));
139
+ if (linesCoveredApprox > 0) {
140
+ this.logger.debug(`Coverage for approximately ${linesCoveredApprox} lines processed for session ${session.sessionId}.`);
141
+ }
142
+ }
143
+ else {
144
+ this.logger.error(`Unknown message type: ${messageType}`);
101
145
  }
102
146
  }
103
147
  catch (e) {
104
- this.logger.error(`Error while processing message starting with ${message.toString('utf8', 0, Math.min(50, message.length))}`);
148
+ this.logger.error(`Error while processing message starting with ${message.toString('utf8', 0, Math.min(250, message.length))}`);
105
149
  this.logger.error(e.message);
106
150
  }
107
151
  }
@@ -121,7 +165,9 @@ class WebSocketCollectingServer {
121
165
  * newline and semicolon symbols separate tokens.
122
166
  * A file name is a token; a range (start to end line) is a token.
123
167
  */
124
- async handleCoverageMessage(session, body) {
168
+ handleCoverageMessage(session, body) {
169
+ // Number of lines signaled to be covered (just an approximation, for debugging purposes)
170
+ let lineCoverageStatements = 0;
125
171
  // Split the input into tokens; these are either file names or code ranges.
126
172
  const tokens = body.toString().split(/[\n;]/).map(line => line.trim());
127
173
  // Placeholder for group/filename.
@@ -129,20 +175,24 @@ class WebSocketCollectingServer {
129
175
  tokens.forEach(token => {
130
176
  // Check if the token starts with '@' - indicating a new group/filename.
131
177
  if (token.startsWith('@')) {
132
- filename = token.substring(1).trim(); // Remove '@' character and extra spaces.
178
+ filename = token.substring(1).trim(); // Remove the '@' character and extra spaces.
133
179
  }
134
180
  else if (filename) {
135
181
  // It is not a file name, we have a range token here.
136
182
  // Example for range a token: `3-5`, or just `42` (corresponding to `42-42`).
137
183
  const range = token.split(/,|-/).map(value => Number.parseInt(value));
138
184
  if (range.length === 1) {
139
- session.putLineCoverage(filename, range[0], range[0]);
185
+ lineCoverageStatements += session.putLineCoverage(filename, range[0], range[0]);
140
186
  }
141
187
  else if (range.length === 2) {
142
- session.putLineCoverage(filename, range[0], range[1]);
188
+ lineCoverageStatements += session.putLineCoverage(filename, range[0], range[1]);
189
+ }
190
+ else {
191
+ this.logger.error(`Invalid range token: ${token}`);
143
192
  }
144
193
  }
145
194
  });
195
+ return lineCoverageStatements;
146
196
  }
147
197
  /**
148
198
  * Returns a statistic on the number of messages received.
@@ -1,18 +1,4 @@
1
- /// <reference types="node" />
2
- import { Socket } from 'net';
3
- import { IDataStorage } from '../storage/DataStorage';
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 type UnmappedCoverage = {
10
- fileId: string;
11
- startLine: number;
12
- startColumn: number;
13
- endLine: number;
14
- endColumn: number;
15
- };
1
+ import { CoverageBucketSpecifier, DataStorage } from '../storage/DataStorage';
16
2
  /**
17
3
  * The session maintains the relevant information for a client.
18
4
  * One session is created for each client.
@@ -20,29 +6,31 @@ export type UnmappedCoverage = {
20
6
  */
21
7
  export declare class Session {
22
8
  /**
23
- * The client socket.
9
+ * An internal session ID for debugging purposes.
24
10
  */
25
- private readonly socket;
11
+ private readonly internalSessionId;
26
12
  /**
27
13
  * The storage to forward coverage information to for aggregation.
28
14
  */
29
15
  private readonly storage;
30
16
  /**
31
- * The logger to use.
17
+ * The application to contribute coverage to.
32
18
  */
33
- private readonly logger;
19
+ private appId?;
34
20
  /**
35
- * The project the coverage information is for.
21
+ * The commit to contribute coverage for.
36
22
  */
37
- private readonly projectId;
23
+ private commitId?;
38
24
  /**
39
25
  * Constructor
40
26
  *
41
- * @param socket - The client socket.
42
27
  * @param storage - The storage to store and aggregate coverage information in.
43
- * @param logger - The logger to use.
44
28
  */
45
- constructor(socket: Socket, storage: IDataStorage, logger: Logger);
29
+ constructor(storage: DataStorage);
30
+ /**
31
+ * If not already done, the bucket to send coverage into for the given application is set up.
32
+ */
33
+ setupBucketAndApplication(bucketSpecifier: CoverageBucketSpecifier): Promise<void>;
46
34
  /**
47
35
  * Put coverage information to the storage for aggregation.
48
36
  * This method also conducts the mapping based on the source map.
@@ -51,5 +39,7 @@ export declare class Session {
51
39
  * @param startLine - The line number within the bundle the range starts.
52
40
  * @param endLine - The line number within the bundle the range ends.
53
41
  */
54
- putLineCoverage(fileId: string, startLine: number, endLine: number): void;
42
+ putLineCoverage(fileId: string, startLine: number, endLine: number): number;
43
+ /** Returns the internal session ID for debugging purposes. */
44
+ get sessionId(): string;
55
45
  }
@@ -8,18 +8,38 @@ const commons_1 = require("@cqse/commons");
8
8
  * The mapping based on sourcemaps is conducted here.
9
9
  */
10
10
  class Session {
11
+ /**
12
+ * An internal session ID for debugging purposes.
13
+ */
14
+ internalSessionId;
15
+ /**
16
+ * The storage to forward coverage information to for aggregation.
17
+ */
18
+ storage;
19
+ /**
20
+ * The application to contribute coverage to.
21
+ */
22
+ appId;
23
+ /**
24
+ * The commit to contribute coverage for.
25
+ */
26
+ commitId;
11
27
  /**
12
28
  * Constructor
13
29
  *
14
- * @param socket - The client socket.
15
30
  * @param storage - The storage to store and aggregate coverage information in.
16
- * @param logger - The logger to use.
17
31
  */
18
- constructor(socket, storage, logger) {
19
- this.socket = commons_1.Contract.requireDefined(socket);
32
+ constructor(storage) {
20
33
  this.storage = commons_1.Contract.requireDefined(storage);
21
- this.logger = commons_1.Contract.requireDefined(logger);
22
- this.projectId = ''; // We currently only support coverage for one project.
34
+ this.internalSessionId = Math.random().toString(36).substring(2, 15) + Math.random().toString(36).substring(2, 15);
35
+ }
36
+ /**
37
+ * If not already done, the bucket to send coverage into for the given application is set up.
38
+ */
39
+ async setupBucketAndApplication(bucketSpecifier) {
40
+ this.commitId = bucketSpecifier.commit;
41
+ this.appId = bucketSpecifier.appId;
42
+ await this.storage.setupCoverageBucket(bucketSpecifier);
23
43
  }
24
44
  /**
25
45
  * Put coverage information to the storage for aggregation.
@@ -30,11 +50,18 @@ class Session {
30
50
  * @param endLine - The line number within the bundle the range ends.
31
51
  */
32
52
  putLineCoverage(fileId, startLine, endLine) {
53
+ commons_1.Contract.requireDefined(this.appId, "The application ID must be set before putting coverage.");
54
+ commons_1.Contract.requireDefined(this.commitId, "The commit must be set before putting coverage.");
33
55
  const lines = [];
34
56
  for (let i = startLine; i <= endLine; i++) {
35
57
  lines.push(i);
36
58
  }
37
- this.storage.putCoverage(this.projectId, fileId, lines);
59
+ this.storage.putCoverage(this.appId, this.commitId, fileId, lines);
60
+ return lines.length;
61
+ }
62
+ /** Returns the internal session ID for debugging purposes. */
63
+ get sessionId() {
64
+ return this.internalSessionId;
38
65
  }
39
66
  }
40
67
  exports.Session = Session;