@teamscale/coverage-collector 1.0.0-beta.5 → 1.0.0-beta.7

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/README.md CHANGED
@@ -16,36 +16,3 @@ code coverage information from manually or automatically exercised (tested)
16
16
  JavaScript applications. The server also handles source maps to map coverage
17
17
  information back to the original source code.
18
18
 
19
- ## Building
20
-
21
- The Collector is written in TypeScript/JavaScript. For building and running it,
22
- NodeJs (>= v16) and pnpm are needed as prerequisites.
23
-
24
- ```
25
- pnpm clean
26
- pnpm install
27
- pnpm build
28
- ```
29
-
30
- ## Running the Collector
31
-
32
- There are several options to run the Collector. For example, via `pnpm` by running
33
-
34
- ```
35
- pnpm collector --port 54678 --dump-to-file=./coverage.simple
36
- ```
37
-
38
- or via `npx` by running
39
-
40
- ```
41
- npx @teamscale/coverage-collector --port 54678 --dump-to-file=./coverage.simple
42
- ```
43
-
44
- Note that NodeJs applications (as the Collector) can only access a limited
45
- amount of RAM by default. Ensure to increase the 'max old space' as needed,
46
- for example, by setting a corresponding environment variable.
47
-
48
- ```
49
- export NODE_OPTIONS="$NODE_OPTIONS --max-old-space-size=8192"
50
- ```
51
-
package/dist/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@teamscale/coverage-collector",
3
- "version": "1.0.0-beta.5",
3
+ "version": "1.0.0-beta.7",
4
4
  "description": "Collector for JavaScript code coverage information",
5
5
  "main": "dist/src/main.js",
6
6
  "bin": "dist/src/main.js",
@@ -13,7 +13,7 @@
13
13
  "clean": "rimraf dist tsconfig.tsbuildinfo",
14
14
  "build": "tsc",
15
15
  "collector": "node dist/src/main.js",
16
- "test": "pnpm build && NODE_OPTIONS='--experimental-vm-modules' jest --coverage --silent=true --detectOpenHandles --forceExit"
16
+ "test": "pnpm build && node --import tsx --test --test-concurrency=1 test/**/*.test.ts"
17
17
  },
18
18
  "files": [
19
19
  "dist/**/*"
@@ -21,35 +21,29 @@
21
21
  "dependencies": {
22
22
  "@cqse/commons": "workspace:../cqse-commons",
23
23
  "async": "^3.2.6",
24
- "axios": "^1.7.9",
24
+ "axios": "^1.12.2",
25
25
  "bunyan": "^1.8.15",
26
- "dotenv": "^16.4.7",
27
- "express": "^5.0.1",
28
- "form-data": "^4.0.2",
26
+ "dotenv": "^17.2.3",
27
+ "express": "^5.1.0",
28
+ "form-data": "^4.0.4",
29
29
  "mkdirp": "^3.0.1",
30
30
  "node-cache": "^5.1.2",
31
- "source-map": "^0.7.4",
32
- "tmp": "^0.2.3",
33
- "ws": "^8.18.0"
31
+ "source-map": "^0.7.6",
32
+ "tmp": "^0.2.5",
33
+ "ws": "^8.18.3"
34
34
  },
35
35
  "devDependencies": {
36
- "@babel/core": "^7.26.8",
37
- "@babel/preset-env": "^7.26.8",
38
- "@types/async": "^3.2.24",
36
+ "@types/async": "^3.2.25",
39
37
  "@types/bunyan": "^1.8.11",
40
- "@types/express": "^5.0.0",
41
- "@types/jest": "^29.5.14",
42
- "@types/node": "^22.13.4",
38
+ "@types/express": "^5.0.4",
39
+ "@types/node": "^22.18.12",
43
40
  "@types/tmp": "^0.2.6",
44
- "@types/ws": "^8.5.14",
45
- "babel-jest": "^29.7.0",
46
- "esbuild": "^0.25.0",
47
- "jest": "^29.7.0",
48
- "mockttp": "3.15.5",
41
+ "@types/ws": "^8.18.1",
42
+ "esbuild": "^0.25.11",
43
+ "mockttp": "3.17.1",
49
44
  "rimraf": "^6.0.1",
50
- "ts-jest": "^29.2.5",
51
- "ts-node": "^10.9.2",
52
- "typescript": "^5.7.3"
45
+ "tsx": "^4.19.2",
46
+ "typescript": "^5.8.3"
53
47
  },
54
48
  "publishConfig": {
55
49
  "access": "public"
@@ -4,21 +4,9 @@ type ProfilerConfiguration = {
4
4
  configurationId: string;
5
5
  configurationOptions?: string;
6
6
  };
7
- /**
8
- * Utility methods for handling remote profiler configuration. The configuration is retrieved from Teasmcale.
9
- */
10
7
  export declare class RemoteProfilerConfig {
11
- /**
12
- * Query the profiler configuration with the given ID via the Teamscale API.
13
- */
14
8
  static queryConfiguration(baseConfig: CollectorOptions, configId: string, logger: Logger): Promise<Map<string, string>>;
15
- /**
16
- * Visible for testing.
17
- */
18
9
  static parseConfigurationStringIntoMap(response: ProfilerConfiguration): Map<string, string>;
19
10
  }
20
- /**
21
- * For testing, clear the configuration cache.
22
- */
23
11
  export declare function clearConfigurationCache(): void;
24
12
  export {};
@@ -9,17 +9,8 @@ const RestApis_1 = require("../utils/RestApis");
9
9
  const commons_1 = require("@cqse/commons");
10
10
  const axios_1 = __importDefault(require("axios"));
11
11
  const node_cache_1 = __importDefault(require("node-cache"));
12
- /**
13
- * We cache the configuration we got from Teamscale for 60s.
14
- */
15
12
  const CONFIG_CACHE = new node_cache_1.default({ stdTTL: 60, deleteOnExpire: true });
16
- /**
17
- * Utility methods for handling remote profiler configuration. The configuration is retrieved from Teasmcale.
18
- */
19
13
  class RemoteProfilerConfig {
20
- /**
21
- * Query the profiler configuration with the given ID via the Teamscale API.
22
- */
23
14
  static async queryConfiguration(baseConfig, configId, logger) {
24
15
  if (!baseConfig.teamscaleServerUrl || !baseConfig.teamscaleAccessToken || !baseConfig.teamscaleUser) {
25
16
  throw new Error("Access to Teamscale is not configured. Receiving profiler configurations is not possible. " +
@@ -36,8 +27,6 @@ class RemoteProfilerConfig {
36
27
  if (response === undefined) {
37
28
  throw new commons_1.InvalidConfigurationException(`No configuration found with ID ${configId}`);
38
29
  }
39
- // Fail for an empty configuration. We do not accept an empty configuration
40
- // for a specified configuration ID.
41
30
  if (!response.configurationOptions || response.configurationOptions.trim().length === 0) {
42
31
  throw new commons_1.InvalidConfigurationException(`Configuration with ID "${configId}" is empty. Please specify relevant configuration options.`);
43
32
  }
@@ -49,9 +38,6 @@ class RemoteProfilerConfig {
49
38
  }
50
39
  return result;
51
40
  }
52
- /**
53
- * Visible for testing.
54
- */
55
41
  static parseConfigurationStringIntoMap(response) {
56
42
  const result = new Map();
57
43
  (response.configurationOptions ?? "").split(/[\r\n]+/).map(line => line.trim())
@@ -68,9 +54,6 @@ class RemoteProfilerConfig {
68
54
  }
69
55
  }
70
56
  exports.RemoteProfilerConfig = RemoteProfilerConfig;
71
- /**
72
- * For testing, clear the configuration cache.
73
- */
74
57
  function clearConfigurationCache() {
75
58
  CONFIG_CACHE.flushAll();
76
59
  }
@@ -1,31 +1,11 @@
1
1
  import { ConfigurationParameters, StaticCollectorOptions } from '@cqse/commons';
2
2
  import 'dotenv/config';
3
- /**
4
- * Callback interface to be called to stop a process in a controlled fashion.
5
- */
6
3
  export type Stoppable = {
7
4
  stop: () => Promise<void>;
8
5
  };
9
- /**
10
- * The main class of the Teamscale JavaScript Collector.
11
- * Used to start the collector with a given configuration.
12
- */
13
6
  export declare class App {
14
- /**
15
- * Construct the logger.
16
- */
17
7
  private static buildLogger;
18
- /**
19
- * Entry point of the Teamscale JavaScript Profiler.
20
- */
21
8
  static run(): Promise<Stoppable>;
22
- /**
23
- * Run the collector with the given configuration options.
24
- */
25
9
  static runWithConfig(staticParameters: ConfigurationParameters, config: StaticCollectorOptions): Promise<Stoppable>;
26
- /**
27
- * Starts a timer that shows a message every min that no coverage
28
- * was received until the opposite is the case.
29
- */
30
10
  private static startNoMessageTimer;
31
11
  }
@@ -52,14 +52,7 @@ const mkdirp_1 = require("mkdirp");
52
52
  const path_1 = __importDefault(require("path"));
53
53
  const TeamscaleUpload_1 = require("../upload/TeamscaleUpload");
54
54
  const process = __importStar(require("node:process"));
55
- /**
56
- * The main class of the Teamscale JavaScript Collector.
57
- * Used to start the collector with a given configuration.
58
- */
59
55
  class App {
60
- /**
61
- * Construct the logger.
62
- */
63
56
  static buildLogger(config) {
64
57
  const logfilePath = config.logToFile.trim();
65
58
  mkdirp_1.mkdirp.sync(path_1.default.dirname(logfilePath));
@@ -67,37 +60,26 @@ class App {
67
60
  const logger = bunyan_1.default.createLogger({
68
61
  name: 'Collector',
69
62
  streams: [
70
- // console output
71
63
  { level: logLevel, stream: new StdConsoleLogger_1.StdConsoleLogger(), type: 'raw' },
72
- // default log file
73
64
  { level: logLevel, stream: new PrettyFileLogger_1.PrettyFileLogger(fs.createWriteStream(logfilePath)), type: 'raw' }
74
65
  ]
75
66
  });
76
- // If the given flag is set, we also log with a JSON-like format
77
67
  if (config.jsonLog) {
78
68
  logger.addStream({ level: logLevel, path: `${logfilePath}.json` });
79
69
  }
80
70
  return logger;
81
71
  }
82
- /**
83
- * Entry point of the Teamscale JavaScript Profiler.
84
- */
85
72
  static async run() {
86
73
  const configParameters = (0, commons_1.buildStaticCollectorParameters)();
87
74
  const appInfos = { about: package_json_1.description, version: package_json_1.version, name: package_json_1.name };
88
75
  const config = (0, commons_1.processCommandLine)(configParameters, appInfos);
89
76
  return await App.runWithConfig(configParameters, config);
90
77
  }
91
- /**
92
- * Run the collector with the given configuration options.
93
- */
94
78
  static async runWithConfig(staticParameters, config) {
95
- // Build the logger
96
79
  const logger = this.buildLogger(config);
97
80
  logger.info(`Starting collector in working directory "${process.cwd()}".`);
98
81
  logger.info(`Will dump coverage to directory "${config.dumpFolder}".`);
99
82
  logger.info(`Logging "${config.logLevel}" to "${config.logToFile}".`);
100
- // Check the connection to Teamscale, if needed.
101
83
  if (config.teamscaleServerUrl) {
102
84
  const checkResult = await (0, TeamscaleUpload_1.checkTeamscaleCredentials)(config, logger);
103
85
  if (!checkResult) {
@@ -105,20 +87,13 @@ class App {
105
87
  process.exit(1);
106
88
  }
107
89
  }
108
- // Ensure that the root coverage folder is writable.
109
90
  if (config.dumpFolder) {
110
91
  const dumpFolder = path_1.default.resolve(config.dumpFolder);
111
92
  ensureWritableFolder(dumpFolder, logger);
112
93
  }
113
- // Prepare the storage and the server
114
94
  const reconfigurableParameters = (0, commons_1.buildReconfigurableCollectorParameters)();
115
- // Now also add checks for configuration arguments that are to be online, that is,
116
- // when receiving configuration updates from Teamscale or via the control API.
117
95
  reconfigurableParameters.addArgumentCheck((options) => {
118
96
  if (config.teamscaleServerUrl) {
119
- // While the connection to Teamscale can be configured, not all
120
- // uploads might be targeted to that but some shall still be stored to the disk.
121
- // In case a Teamscale project is configured, we expect that an upload to Teamscale shall happen.
122
97
  if (options.teamscaleProject && !options.teamscalePartition) {
123
98
  return 'The Teamscale project (parameter teamscaleProject) and coverage partition (parameter teamscalePartition) ' +
124
99
  'must be configured for an upload to Teamscale.';
@@ -127,30 +102,19 @@ class App {
127
102
  });
128
103
  const storage = new DataStorage_1.DataStorage(logger, (0, commons_1.parameterUnion)(staticParameters, reconfigurableParameters), reconfigurableParameters, config);
129
104
  const server = new CollectingServer_1.WebSocketCollectingServer(config.port, storage, logger);
130
- // Enable the remote control API if configured
131
105
  const controlServer = new ControlServer_1.ControlServer(config, storage, logger);
132
106
  const controlServerState = controlServer.start();
133
- // Start the server socket.
134
- // ATTENTION: The server is executed asynchronously.
135
107
  const serverState = server.start();
136
- // Optionally, start a timer that dumps the coverage after N seconds
137
108
  const dumpTimerState = CoverageDumper_1.CoverageDumper.startRegularCollectorProcesses(storage, logger);
138
- // Start a timer that informs if no coverage was received within the last minute
139
109
  const statsTimerState = this.startNoMessageTimer(logger, server);
140
110
  const stop = async function () {
141
111
  logger.info('Stopping the collector.');
142
- // Final dump before stop. The await/async construct that we use here
143
- // is used to make sure that other events in the event loop are processed
144
- // before the actual dump happens, that is, to retrieve coverage that was
145
- // already sent but has not yet been processed in the collector.
146
- // We need this, for example, in our system tests where everything runs in one NodeJS environment.
147
112
  await new Promise(resolve => {
148
113
  setTimeout(async () => {
149
114
  await CoverageDumper_1.CoverageDumper.dumpCoverage(storage, logger);
150
115
  resolve(undefined);
151
116
  }, 0);
152
117
  });
153
- // Stop all timers and sockets.
154
118
  dumpTimerState.stop();
155
119
  statsTimerState.stop();
156
120
  await controlServerState.stop();
@@ -159,10 +123,6 @@ class App {
159
123
  };
160
124
  return { stop };
161
125
  }
162
- /**
163
- * Starts a timer that shows a message every min that no coverage
164
- * was received until the opposite is the case.
165
- */
166
126
  static startNoMessageTimer(logger, server) {
167
127
  const startTime = Date.now();
168
128
  const timer = setInterval(async () => {
@@ -171,7 +131,6 @@ class App {
171
131
  logger.info(`No coverage received for ${((Date.now() - startTime) / 1000.0).toFixed(0)}s.`);
172
132
  }
173
133
  else {
174
- // We can stop running the timer after we have received the first coverage.
175
134
  clearInterval(timer);
176
135
  }
177
136
  }, 1000 * 60);
@@ -183,14 +142,11 @@ class App {
183
142
  exports.App = App;
184
143
  function ensureWritableFolder(dumpFolder, logger) {
185
144
  try {
186
- // 1. Create the folder when it doesn't exist (recursive = true is a no-op when it does).
187
145
  mkdirp_1.mkdirp.sync(dumpFolder);
188
- // 2. Verify that we ended up with a directory.
189
146
  const stat = fs.statSync(dumpFolder);
190
147
  if (!stat.isDirectory()) {
191
148
  throw new Error(`"${dumpFolder}" exists but is not a directory.`);
192
149
  }
193
- // 3. Perform a real write test.
194
150
  const testFile = path_1.default.join(dumpFolder, `.write-test-${Date.now()}`);
195
151
  fs.writeFileSync(testFile, 'writable?');
196
152
  fs.unlinkSync(testFile);
@@ -1,17 +1,11 @@
1
1
  import { DataStorage } from '../storage/DataStorage';
2
2
  import Logger from 'bunyan';
3
3
  import { CollectorOptions } from '@cqse/commons';
4
- /**
5
- * Provides a REST API for remote configuration of the collector.
6
- */
7
4
  export declare class ControlServer {
8
5
  private config;
9
6
  private storage;
10
7
  private logger;
11
8
  constructor(config: CollectorOptions, storage: DataStorage, logger: Logger);
12
- /**
13
- * Start the collector remote config API.
14
- */
15
9
  start(): {
16
10
  stop: () => Promise<void>;
17
11
  };
@@ -6,9 +6,6 @@ Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.ControlServer = void 0;
7
7
  const CoverageDumper_1 = require("../control/CoverageDumper");
8
8
  const express_1 = __importDefault(require("express"));
9
- /**
10
- * Provides a REST API for remote configuration of the collector.
11
- */
12
9
  class ControlServer {
13
10
  config;
14
11
  storage;
@@ -18,14 +15,10 @@ class ControlServer {
18
15
  this.storage = storage;
19
16
  this.logger = logger;
20
17
  }
21
- /**
22
- * Start the collector remote config API.
23
- */
24
18
  start() {
25
19
  if (!this.config.enableControlPort) {
26
20
  return {
27
21
  async stop() {
28
- // nothing to stop in this case
29
22
  }
30
23
  };
31
24
  }
@@ -1,27 +1,10 @@
1
1
  import { DataStorage } from '../storage/DataStorage';
2
2
  import Logger from 'bunyan';
3
- /**
4
- * Functionality for dumping coverage regularly to different targets.
5
- */
6
3
  export declare class CoverageDumper {
7
- /**
8
- * Start a timer for dumping the data, depending on the configuration;
9
- * and for retrieving new configuration values from Teamscale.
10
- *
11
- * @param storage - The storage with the information to dump.
12
- * @param logger - The logger to use.
13
- */
14
4
  static startRegularCollectorProcesses(storage: DataStorage, logger: Logger): {
15
5
  stop: () => void;
16
6
  };
17
- /**
18
- * Dump all the collected coverage. If an `onlyForAppId` is given, only for the app with that ID.
19
- * If no `onlyForAppId` is given, coverage will be dumped for every application, separately.
20
- */
21
7
  static dumpCoverage(storage: DataStorage, logger: Logger, onlyForAppId?: string): Promise<void>;
22
- /**
23
- * Returns true if an actual upload request was sent to the remote server.
24
- */
25
8
  private static uploadCoverage;
26
9
  private static determineCoverageTargetFolder;
27
10
  }
@@ -13,17 +13,7 @@ const DUMP_COVERAGE_CHECK_AFTER_SECONDS = 60;
13
13
  const CHECK_FOR_SCHEDULED_ACTIVITY_AFTER_SECONDS = 31;
14
14
  const DEFAULT_COVERAGE_DUMP_AFTER_MINS = 10;
15
15
  const STARTUP_TIMESTAMP = Date.now();
16
- /**
17
- * Functionality for dumping coverage regularly to different targets.
18
- */
19
16
  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
17
  static startRegularCollectorProcesses(storage, logger) {
28
18
  const lastConfigRefreshPerApp = new Map();
29
19
  const lastDumpTimestampPerApp = new Map();
@@ -41,15 +31,11 @@ class CoverageDumper {
41
31
  }
42
32
  };
43
33
  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
34
  for (const appId of storage.getApplicationIDs()) {
47
35
  await doIfExpired(`config-update-${appId}`, lastConfigRefreshPerApp, 1000 * CONFIG_REQUERY_INTERVAL_SECONDS, async () => {
48
36
  await storage.refreshApplicationConfiguration(appId);
49
37
  });
50
38
  }
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
39
  for (const appId of storage.getApplicationIDs()) {
54
40
  const appDumpAfterMinutes = storage.getAppConfiguration(appId).dumpAfterMins ?? DEFAULT_COVERAGE_DUMP_AFTER_MINS;
55
41
  await doIfExpired(`coverage-dump-${appId}`, lastDumpTimestampPerApp, 1000 * DUMP_COVERAGE_CHECK_AFTER_SECONDS * appDumpAfterMinutes, async () => {
@@ -57,7 +43,6 @@ class CoverageDumper {
57
43
  });
58
44
  }
59
45
  }, 1000 * CHECK_FOR_SCHEDULED_ACTIVITY_AFTER_SECONDS);
60
- // Dump the coverage before each (correct) config update.
61
46
  storage.addBeforeConfigUpdateCallback(async (appId, dataStorage) => {
62
47
  logger.info(`Dumping coverage before config update for application ${appId}.`);
63
48
  await this.dumpCoverage(dataStorage, logger, appId);
@@ -71,10 +56,6 @@ class CoverageDumper {
71
56
  }
72
57
  };
73
58
  }
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
59
  static async dumpCoverage(storage, logger, onlyForAppId) {
79
60
  const appsToDumpFor = onlyForAppId ? [onlyForAppId] : storage.getApplicationIDs();
80
61
  const dumpTimestamp = new Date();
@@ -85,10 +66,8 @@ class CoverageDumper {
85
66
  const dumpToFolder = this.determineCoverageTargetFolder(dumpForAppId, dumpConfiguration);
86
67
  const deleteAfterUpload = !(dumpConfiguration.keepCoverageFiles ?? dumpConfiguration.dumpToFolder !== undefined);
87
68
  try {
88
- // Write coverage to a file
89
69
  const dumpSummary = storage.dumpToSimpleCoverageFile(dumpToFolder, dumpTimestamp, dumpForAppId);
90
70
  hadCoverageToDump = dumpSummary.hadCoverageToDump || hadCoverageToDump;
91
- // Upload to Teamscale or Artifactory if configured
92
71
  for (const { simpleCoverageFile, simpleCoverageFileLines, commit } of dumpSummary.details) {
93
72
  try {
94
73
  if (dumpConfiguration.teamscaleServerUrl || dumpConfiguration.artifactoryServerUrl) {
@@ -119,19 +98,14 @@ class CoverageDumper {
119
98
  logger.info('Coverage dump request was processed successfully; no new coverage to dump found.');
120
99
  }
121
100
  }
122
- /**
123
- * Returns true if an actual upload request was sent to the remote server.
124
- */
125
101
  static async uploadCoverage(config, coverageFile, lines, commit, logger, deleteAfterUpload) {
126
102
  let uploadDone = false;
127
103
  if (config.teamscaleServerUrl && (config.teamscaleProject ?? "").length > 0) {
128
- // A Teamscale url might be configured for remote configuration. Uploads might not be intended.
129
104
  uploadDone = await (0, TeamscaleUpload_1.uploadToTeamscale)(config, logger, coverageFile, lines, commit);
130
105
  }
131
106
  if (config.artifactoryServerUrl) {
132
107
  uploadDone = uploadDone || await (0, ArtifactoryUpload_1.uploadToArtifactory)(config, logger, coverageFile, lines, commit);
133
108
  }
134
- // Delete coverage if upload was successful and keeping coverage files on disk was not configure by the user
135
109
  if (deleteAfterUpload) {
136
110
  fs_1.default.unlinkSync(coverageFile);
137
111
  }
@@ -144,21 +118,13 @@ class CoverageDumper {
144
118
  }
145
119
  }
146
120
  exports.CoverageDumper = CoverageDumper;
147
- /**
148
- * Only characters that allowed in most file systems. And replace backslashes with forward slashes.
149
- */
150
121
  function makeProperDirectoryPath(path) {
151
122
  if (path === undefined || path.trim().length === 0) {
152
123
  return '';
153
124
  }
154
- // Remove leading slashes.
155
125
  path = path.trim().replace(/^\/+/g, '');
156
- // Replace backslashes with forward slashes.
157
126
  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
127
  path = path.replace(/[^a-zA-Z0-9\-_/]/g, '_');
161
- // Ensure a trailing slash.
162
128
  if (path.endsWith('/')) {
163
129
  return path;
164
130
  }
package/dist/src/main.js CHANGED
@@ -7,22 +7,16 @@ Object.defineProperty(exports, "__esModule", { value: true });
7
7
  const App_1 = require("./control/App");
8
8
  const node_process_1 = __importDefault(require("node:process"));
9
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
10
  (async () => {
15
11
  const app = await App_1.App.run();
16
12
  const handleExit = async () => {
17
13
  await app.stop();
18
- node_process_1.default.exit();
14
+ node_process_1.default.exit(node_process_1.default.exitCode);
19
15
  };
20
- // Graceful termination on CTRL+C and SIGTERM.
21
16
  node_process_1.default.on('SIGINT', () => handleExit());
22
17
  node_process_1.default.on('SIGTERM', () => handleExit());
23
18
  })().catch(reason => {
24
19
  if (reason instanceof commons_1.InvalidConfigurationException) {
25
- // Do not print the stack trace for invalid configuration parameters from the user.
26
20
  console.error(`Failed: ${reason.message}`);
27
21
  }
28
22
  else {
@@ -1,71 +1,17 @@
1
1
  import { DataStorage } from '../storage/DataStorage';
2
2
  import Logger from 'bunyan';
3
- /**
4
- * A WebSocket based implementation of a coverage receiver.
5
- * Receives coverage from instrumented JavaScript code.
6
- */
7
3
  export declare class WebSocketCollectingServer {
8
- /**
9
- * The WebSocket server component.
10
- */
11
4
  private server;
12
- /**
13
- * The storage to put the received coverage information to for aggregation and further processing.
14
- */
15
5
  private readonly storage;
16
- /**
17
- * The logger to use.
18
- */
19
6
  private readonly logger;
20
- /**
21
- * The number of messages that have been received.
22
- */
23
7
  private totalNumMessagesReceived;
24
- /**
25
- * The number of coverage messages that have been received.
26
- */
27
8
  private totalNumCoverageMessagesReceived;
28
- /**
29
- * Constructor.
30
- *
31
- * @param port - The port the WebSocket server should listen on.
32
- * @param storage - The storage to put the received coverage information to.
33
- * @param logger - The logger to use.
34
- */
35
9
  constructor(port: number, storage: DataStorage, logger: Logger);
36
- /**
37
- * Start the server socket, handle sessions and dispatch messages.
38
- */
39
10
  start(): {
40
11
  stop: () => void;
41
12
  };
42
- /**
43
- * Handle a message from a client.
44
- *
45
- * @param session - The session that has been started for the client.
46
- * @param message - The message to handle.
47
- */
48
13
  private handleMessage;
49
- /**
50
- * Handle a message with coverage information.
51
- *
52
- * @param session - The session to handle the message for.
53
- * @param body - The body of the message (to be parsed).
54
- *
55
- * Example for a `body`:
56
- * ```
57
- * @/foo/bar.ts;1-3;5-6
58
- * @/wauz/wee.ts;67-67;100-101
59
- * ```
60
- *
61
- * This processing operates as state machine. The input is split into tokens;
62
- * newline and semicolon symbols separate tokens.
63
- * A file name is a token; a range (start to end line) is a token.
64
- */
65
14
  private handleCoverageMessage;
66
- /**
67
- * Returns a statistic on the number of messages received.
68
- */
69
15
  getStatistics(): {
70
16
  totalMessages: number;
71
17
  totalCoverageMessages: number;