@teamscale/coverage-collector 1.0.0-beta.6 → 1.0.4
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 +1 -34
- package/dist/main.mjs +8 -0
- package/package.json +24 -16
- package/dist/package.json +0 -57
- package/dist/src/config/RemoteProfilerConfig.d.ts +0 -24
- package/dist/src/config/RemoteProfilerConfig.js +0 -76
- package/dist/src/control/App.d.ts +0 -31
- package/dist/src/control/App.js +0 -202
- package/dist/src/control/ControlServer.d.ts +0 -24
- package/dist/src/control/ControlServer.js +0 -100
- package/dist/src/control/CoverageDumper.d.ts +0 -27
- package/dist/src/control/CoverageDumper.js +0 -166
- package/dist/src/main.d.ts +0 -2
- package/dist/src/main.js +0 -32
- package/dist/src/receiver/CollectingServer.d.ts +0 -73
- package/dist/src/receiver/CollectingServer.js +0 -207
- package/dist/src/receiver/Session.d.ts +0 -45
- package/dist/src/receiver/Session.js +0 -67
- package/dist/src/storage/DataStorage.d.ts +0 -242
- package/dist/src/storage/DataStorage.js +0 -493
- package/dist/src/upload/ArtifactoryUpload.d.ts +0 -6
- package/dist/src/upload/ArtifactoryUpload.js +0 -63
- package/dist/src/upload/TeamscaleUpload.d.ts +0 -12
- package/dist/src/upload/TeamscaleUpload.js +0 -90
- package/dist/src/utils/PrettyFileLogger.d.ts +0 -12
- package/dist/src/utils/PrettyFileLogger.js +0 -26
- package/dist/src/utils/QueryParameters.d.ts +0 -9
- package/dist/src/utils/QueryParameters.js +0 -19
- package/dist/src/utils/RestApis.d.ts +0 -37
- package/dist/src/utils/RestApis.js +0 -141
- package/dist/src/utils/StdConsoleLogger.d.ts +0 -5
- package/dist/src/utils/StdConsoleLogger.js +0 -26
|
@@ -1,100 +0,0 @@
|
|
|
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.ControlServer = void 0;
|
|
7
|
-
const CoverageDumper_1 = require("../control/CoverageDumper");
|
|
8
|
-
const express_1 = __importDefault(require("express"));
|
|
9
|
-
/**
|
|
10
|
-
* Provides a REST API for remote configuration of the collector.
|
|
11
|
-
*/
|
|
12
|
-
class ControlServer {
|
|
13
|
-
config;
|
|
14
|
-
storage;
|
|
15
|
-
logger;
|
|
16
|
-
constructor(config, storage, logger) {
|
|
17
|
-
this.config = config;
|
|
18
|
-
this.storage = storage;
|
|
19
|
-
this.logger = logger;
|
|
20
|
-
}
|
|
21
|
-
/**
|
|
22
|
-
* Start the collector remote config API.
|
|
23
|
-
*/
|
|
24
|
-
start() {
|
|
25
|
-
if (!this.config.enableControlPort) {
|
|
26
|
-
return {
|
|
27
|
-
async stop() {
|
|
28
|
-
// nothing to stop in this case
|
|
29
|
-
}
|
|
30
|
-
};
|
|
31
|
-
}
|
|
32
|
-
const controlServer = (0, express_1.default)();
|
|
33
|
-
controlServer.use(express_1.default.text({}));
|
|
34
|
-
controlServer.use(express_1.default.urlencoded({ extended: true }));
|
|
35
|
-
const serverSocket = controlServer.listen(this.config.enableControlPort);
|
|
36
|
-
controlServer.post('/refresh/', (request, response) => this.handleRefreshConfigs(request, response));
|
|
37
|
-
controlServer.post('/dump/', (request, response) => this.handleDumpPost(request, response));
|
|
38
|
-
controlServer.post('/dump/:configId', (request, response) => this.handleDumpPostForConfig(request, response));
|
|
39
|
-
controlServer.post('/reset', (request, response) => this.handleGlobalCoverageReset(request, response));
|
|
40
|
-
controlServer.post('/reset/:configId', (request, response) => this.handleCoverageResetForConfig(request, response));
|
|
41
|
-
this.logger.info(`Control server enabled at port ${this.config.enableControlPort}.`);
|
|
42
|
-
return {
|
|
43
|
-
async stop() {
|
|
44
|
-
return new Promise(resolve => {
|
|
45
|
-
serverSocket.close(() => resolve());
|
|
46
|
-
});
|
|
47
|
-
}
|
|
48
|
-
};
|
|
49
|
-
}
|
|
50
|
-
async handleRefreshConfigs(request, response) {
|
|
51
|
-
this.logger.info('Remote configuration refresh requested via the control API.');
|
|
52
|
-
await this.storage.refreshAllRemoteConfigurations();
|
|
53
|
-
this.logger.info('Refresh done.');
|
|
54
|
-
response.sendStatus(200);
|
|
55
|
-
}
|
|
56
|
-
async handleDumpPost(request, response) {
|
|
57
|
-
this.logger.info('Dumping coverage requested via the control API.');
|
|
58
|
-
await CoverageDumper_1.CoverageDumper.dumpCoverage(this.storage, this.logger);
|
|
59
|
-
response.sendStatus(200);
|
|
60
|
-
}
|
|
61
|
-
async handleDumpPostForConfig(request, response) {
|
|
62
|
-
return this.handleConfigScopedRequest(request, response, async (configId) => {
|
|
63
|
-
this.logger.info(`Dumping coverage requested for config '${configId}' via the control API.`);
|
|
64
|
-
const appsWithConfig = this.storage.getApplicationsWithConfig(configId);
|
|
65
|
-
for (const appId of appsWithConfig) {
|
|
66
|
-
await CoverageDumper_1.CoverageDumper.dumpCoverage(this.storage, this.logger, appId);
|
|
67
|
-
}
|
|
68
|
-
});
|
|
69
|
-
}
|
|
70
|
-
async handleGlobalCoverageReset(request, response) {
|
|
71
|
-
this.storage.discardCollectedCoverage();
|
|
72
|
-
this.logger.info(`Discarding collected coverage information as requested via the control API.`);
|
|
73
|
-
response.sendStatus(200);
|
|
74
|
-
}
|
|
75
|
-
async handleCoverageResetForConfig(request, response) {
|
|
76
|
-
return this.handleConfigScopedRequest(request, response, async (configId) => {
|
|
77
|
-
this.logger.info(`Discarding collected coverage information for config '${configId}' as requested via the control API.`);
|
|
78
|
-
const appsWithConfig = this.storage.getApplicationsWithConfig(configId);
|
|
79
|
-
for (const appId of appsWithConfig) {
|
|
80
|
-
this.storage.discardCollectedCoverage(appId);
|
|
81
|
-
}
|
|
82
|
-
});
|
|
83
|
-
}
|
|
84
|
-
async handleConfigScopedRequest(request, response, configHandler) {
|
|
85
|
-
const configId = request.params.configId;
|
|
86
|
-
if (!configId) {
|
|
87
|
-
throw new Error('Invalid config ID');
|
|
88
|
-
}
|
|
89
|
-
try {
|
|
90
|
-
await configHandler(configId);
|
|
91
|
-
response.sendStatus(200);
|
|
92
|
-
}
|
|
93
|
-
catch (error) {
|
|
94
|
-
response.set('Content-Type', 'text/plain');
|
|
95
|
-
response.send(`Failed to handle request for config '${configId}': ${error.message}`);
|
|
96
|
-
response.sendStatus(500);
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
|
-
exports.ControlServer = ControlServer;
|
|
@@ -1,27 +0,0 @@
|
|
|
1
|
-
import { DataStorage } from '../storage/DataStorage';
|
|
2
|
-
import Logger from 'bunyan';
|
|
3
|
-
/**
|
|
4
|
-
* Functionality for dumping coverage regularly to different targets.
|
|
5
|
-
*/
|
|
6
|
-
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
|
-
static startRegularCollectorProcesses(storage: DataStorage, logger: Logger): {
|
|
15
|
-
stop: () => void;
|
|
16
|
-
};
|
|
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
|
-
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
|
-
private static uploadCoverage;
|
|
26
|
-
private static determineCoverageTargetFolder;
|
|
27
|
-
}
|
|
@@ -1,166 +0,0 @@
|
|
|
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.d.ts
DELETED
package/dist/src/main.js
DELETED
|
@@ -1,32 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
"use strict";
|
|
3
|
-
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
4
|
-
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
5
|
-
};
|
|
6
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
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,73 +0,0 @@
|
|
|
1
|
-
import { DataStorage } from '../storage/DataStorage';
|
|
2
|
-
import Logger from 'bunyan';
|
|
3
|
-
/**
|
|
4
|
-
* A WebSocket based implementation of a coverage receiver.
|
|
5
|
-
* Receives coverage from instrumented JavaScript code.
|
|
6
|
-
*/
|
|
7
|
-
export declare class WebSocketCollectingServer {
|
|
8
|
-
/**
|
|
9
|
-
* The WebSocket server component.
|
|
10
|
-
*/
|
|
11
|
-
private server;
|
|
12
|
-
/**
|
|
13
|
-
* The storage to put the received coverage information to for aggregation and further processing.
|
|
14
|
-
*/
|
|
15
|
-
private readonly storage;
|
|
16
|
-
/**
|
|
17
|
-
* The logger to use.
|
|
18
|
-
*/
|
|
19
|
-
private readonly logger;
|
|
20
|
-
/**
|
|
21
|
-
* The number of messages that have been received.
|
|
22
|
-
*/
|
|
23
|
-
private totalNumMessagesReceived;
|
|
24
|
-
/**
|
|
25
|
-
* The number of coverage messages that have been received.
|
|
26
|
-
*/
|
|
27
|
-
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
|
-
constructor(port: number, storage: DataStorage, logger: Logger);
|
|
36
|
-
/**
|
|
37
|
-
* Start the server socket, handle sessions and dispatch messages.
|
|
38
|
-
*/
|
|
39
|
-
start(): {
|
|
40
|
-
stop: () => void;
|
|
41
|
-
};
|
|
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
|
-
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
|
-
private handleCoverageMessage;
|
|
66
|
-
/**
|
|
67
|
-
* Returns a statistic on the number of messages received.
|
|
68
|
-
*/
|
|
69
|
-
getStatistics(): {
|
|
70
|
-
totalMessages: number;
|
|
71
|
-
totalCoverageMessages: number;
|
|
72
|
-
};
|
|
73
|
-
}
|
|
@@ -1,207 +0,0 @@
|
|
|
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 () {
|
|
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
|
-
})();
|
|
35
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
-
exports.WebSocketCollectingServer = void 0;
|
|
37
|
-
const WebSocket = __importStar(require("ws"));
|
|
38
|
-
const commons_1 = require("@cqse/commons");
|
|
39
|
-
const Session_1 = require("./Session");
|
|
40
|
-
/** A message that provides coverage information */
|
|
41
|
-
const MESSAGE_TYPE_COVERAGE = 'c';
|
|
42
|
-
/** A message that defines the bucket to put the coverage into */
|
|
43
|
-
const MESSAGE_TYPE_BUCKET = 'b';
|
|
44
|
-
/**
|
|
45
|
-
* A WebSocket based implementation of a coverage receiver.
|
|
46
|
-
* Receives coverage from instrumented JavaScript code.
|
|
47
|
-
*/
|
|
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;
|
|
69
|
-
/**
|
|
70
|
-
* Constructor.
|
|
71
|
-
*
|
|
72
|
-
* @param port - The port the WebSocket server should listen on.
|
|
73
|
-
* @param storage - The storage to put the received coverage information to.
|
|
74
|
-
* @param logger - The logger to use.
|
|
75
|
-
*/
|
|
76
|
-
constructor(port, storage, logger) {
|
|
77
|
-
commons_1.Contract.require(port > 0 && port < 65536, 'Port must be valid (range).');
|
|
78
|
-
this.storage = commons_1.Contract.requireDefined(storage);
|
|
79
|
-
this.logger = commons_1.Contract.requireDefined(logger);
|
|
80
|
-
this.server = new WebSocket.Server({ port });
|
|
81
|
-
this.totalNumMessagesReceived = 0;
|
|
82
|
-
this.totalNumCoverageMessagesReceived = 0;
|
|
83
|
-
}
|
|
84
|
-
/**
|
|
85
|
-
* Start the server socket, handle sessions and dispatch messages.
|
|
86
|
-
*/
|
|
87
|
-
start() {
|
|
88
|
-
this.logger.info(`Starting server on port ${this.server?.options.port}.`);
|
|
89
|
-
// Handle new connections from clients
|
|
90
|
-
this.server?.on('connection', (webSocket, req) => {
|
|
91
|
-
let session = new Session_1.Session(this.storage);
|
|
92
|
-
this.logger.trace(`Connection from: ${req.socket.remoteAddress}`);
|
|
93
|
-
// Handle disconnecting clients
|
|
94
|
-
webSocket.on('close', code => {
|
|
95
|
-
if (session) {
|
|
96
|
-
session = null;
|
|
97
|
-
this.logger.trace(`Closing with code ${code}`);
|
|
98
|
-
}
|
|
99
|
-
});
|
|
100
|
-
// Handle incoming messages
|
|
101
|
-
webSocket.on('message', (message) => {
|
|
102
|
-
this.totalNumMessagesReceived += 1;
|
|
103
|
-
if (session && Buffer.isBuffer(message)) {
|
|
104
|
-
void this.handleMessage(session, message);
|
|
105
|
-
}
|
|
106
|
-
});
|
|
107
|
-
// Handle errors
|
|
108
|
-
webSocket.on('error', (e) => {
|
|
109
|
-
this.logger.error('Error on server socket triggered.', e);
|
|
110
|
-
});
|
|
111
|
-
});
|
|
112
|
-
return {
|
|
113
|
-
stop: () => {
|
|
114
|
-
this.server?.clients.forEach(client => client.close());
|
|
115
|
-
this.server?.close();
|
|
116
|
-
this.server = null;
|
|
117
|
-
}
|
|
118
|
-
};
|
|
119
|
-
}
|
|
120
|
-
/**
|
|
121
|
-
* Handle a message from a client.
|
|
122
|
-
*
|
|
123
|
-
* @param session - The session that has been started for the client.
|
|
124
|
-
* @param message - The message to handle.
|
|
125
|
-
*/
|
|
126
|
-
async handleMessage(session, message) {
|
|
127
|
-
try {
|
|
128
|
-
const messageType = message.toString('utf8', 0, 1);
|
|
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) {
|
|
137
|
-
this.totalNumCoverageMessagesReceived += 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}`);
|
|
145
|
-
}
|
|
146
|
-
}
|
|
147
|
-
catch (e) {
|
|
148
|
-
this.logger.error(`Error while processing message starting with ${message.toString('utf8', 0, Math.min(250, message.length))}`);
|
|
149
|
-
this.logger.error(e.message);
|
|
150
|
-
}
|
|
151
|
-
}
|
|
152
|
-
/**
|
|
153
|
-
* Handle a message with coverage information.
|
|
154
|
-
*
|
|
155
|
-
* @param session - The session to handle the message for.
|
|
156
|
-
* @param body - The body of the message (to be parsed).
|
|
157
|
-
*
|
|
158
|
-
* Example for a `body`:
|
|
159
|
-
* ```
|
|
160
|
-
* @/foo/bar.ts;1-3;5-6
|
|
161
|
-
* @/wauz/wee.ts;67-67;100-101
|
|
162
|
-
* ```
|
|
163
|
-
*
|
|
164
|
-
* This processing operates as state machine. The input is split into tokens;
|
|
165
|
-
* newline and semicolon symbols separate tokens.
|
|
166
|
-
* A file name is a token; a range (start to end line) is a token.
|
|
167
|
-
*/
|
|
168
|
-
handleCoverageMessage(session, body) {
|
|
169
|
-
// Number of lines signaled to be covered (just an approximation, for debugging purposes)
|
|
170
|
-
let lineCoverageStatements = 0;
|
|
171
|
-
// Split the input into tokens; these are either file names or code ranges.
|
|
172
|
-
const tokens = body.toString().split(/[\n;]/).map(line => line.trim());
|
|
173
|
-
// Placeholder for group/filename.
|
|
174
|
-
let filename = '';
|
|
175
|
-
tokens.forEach(token => {
|
|
176
|
-
// Check if the token starts with '@' - indicating a new group/filename.
|
|
177
|
-
if (token.startsWith('@')) {
|
|
178
|
-
filename = token.substring(1).trim(); // Remove the '@' character and extra spaces.
|
|
179
|
-
}
|
|
180
|
-
else if (filename) {
|
|
181
|
-
// It is not a file name, we have a range token here.
|
|
182
|
-
// Example for range a token: `3-5`, or just `42` (corresponding to `42-42`).
|
|
183
|
-
const range = token.split(/,|-/).map(value => Number.parseInt(value));
|
|
184
|
-
if (range.length === 1) {
|
|
185
|
-
lineCoverageStatements += session.putLineCoverage(filename, range[0], range[0]);
|
|
186
|
-
}
|
|
187
|
-
else if (range.length === 2) {
|
|
188
|
-
lineCoverageStatements += session.putLineCoverage(filename, range[0], range[1]);
|
|
189
|
-
}
|
|
190
|
-
else {
|
|
191
|
-
this.logger.error(`Invalid range token: ${token}`);
|
|
192
|
-
}
|
|
193
|
-
}
|
|
194
|
-
});
|
|
195
|
-
return lineCoverageStatements;
|
|
196
|
-
}
|
|
197
|
-
/**
|
|
198
|
-
* Returns a statistic on the number of messages received.
|
|
199
|
-
*/
|
|
200
|
-
getStatistics() {
|
|
201
|
-
return {
|
|
202
|
-
totalMessages: this.totalNumMessagesReceived,
|
|
203
|
-
totalCoverageMessages: this.totalNumCoverageMessagesReceived
|
|
204
|
-
};
|
|
205
|
-
}
|
|
206
|
-
}
|
|
207
|
-
exports.WebSocketCollectingServer = WebSocketCollectingServer;
|
|
@@ -1,45 +0,0 @@
|
|
|
1
|
-
import { CoverageBucketSpecifier, DataStorage } from '../storage/DataStorage';
|
|
2
|
-
/**
|
|
3
|
-
* The session maintains the relevant information for a client.
|
|
4
|
-
* One session is created for each client.
|
|
5
|
-
* The mapping based on sourcemaps is conducted here.
|
|
6
|
-
*/
|
|
7
|
-
export declare class Session {
|
|
8
|
-
/**
|
|
9
|
-
* An internal session ID for debugging purposes.
|
|
10
|
-
*/
|
|
11
|
-
private readonly internalSessionId;
|
|
12
|
-
/**
|
|
13
|
-
* The storage to forward coverage information to for aggregation.
|
|
14
|
-
*/
|
|
15
|
-
private readonly storage;
|
|
16
|
-
/**
|
|
17
|
-
* The application to contribute coverage to.
|
|
18
|
-
*/
|
|
19
|
-
private appId?;
|
|
20
|
-
/**
|
|
21
|
-
* The commit to contribute coverage for.
|
|
22
|
-
*/
|
|
23
|
-
private commitId?;
|
|
24
|
-
/**
|
|
25
|
-
* Constructor
|
|
26
|
-
*
|
|
27
|
-
* @param storage - The storage to store and aggregate coverage information in.
|
|
28
|
-
*/
|
|
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>;
|
|
34
|
-
/**
|
|
35
|
-
* Put coverage information to the storage for aggregation.
|
|
36
|
-
* This method also conducts the mapping based on the source map.
|
|
37
|
-
*
|
|
38
|
-
* @param fileId - The identifier of the instrumented bundle (file).
|
|
39
|
-
* @param startLine - The line number within the bundle the range starts.
|
|
40
|
-
* @param endLine - The line number within the bundle the range ends.
|
|
41
|
-
*/
|
|
42
|
-
putLineCoverage(fileId: string, startLine: number, endLine: number): number;
|
|
43
|
-
/** Returns the internal session ID for debugging purposes. */
|
|
44
|
-
get sessionId(): string;
|
|
45
|
-
}
|