@teamscale/coverage-collector 1.0.0-beta.6 → 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 +0 -33
- package/dist/package.json +17 -23
- package/dist/src/config/RemoteProfilerConfig.d.ts +0 -12
- package/dist/src/config/RemoteProfilerConfig.js +0 -17
- package/dist/src/control/App.d.ts +0 -20
- package/dist/src/control/App.js +0 -44
- package/dist/src/control/ControlServer.d.ts +0 -6
- package/dist/src/control/ControlServer.js +0 -7
- package/dist/src/control/CoverageDumper.d.ts +0 -17
- package/dist/src/control/CoverageDumper.js +0 -34
- package/dist/src/main.js +1 -7
- package/dist/src/receiver/CollectingServer.d.ts +0 -54
- package/dist/src/receiver/CollectingServer.js +1 -67
- package/dist/src/receiver/Session.d.ts +0 -34
- package/dist/src/receiver/Session.js +0 -34
- package/dist/src/storage/DataStorage.d.ts +0 -173
- package/dist/src/storage/DataStorage.js +9 -165
- package/dist/src/upload/ArtifactoryUpload.d.ts +0 -3
- package/dist/src/upload/ArtifactoryUpload.js +0 -3
- package/dist/src/upload/TeamscaleUpload.d.ts +0 -7
- package/dist/src/upload/TeamscaleUpload.js +0 -7
- package/dist/src/utils/PrettyFileLogger.d.ts +0 -4
- package/dist/src/utils/PrettyFileLogger.js +0 -5
- package/dist/src/utils/QueryParameters.d.ts +0 -6
- package/dist/src/utils/QueryParameters.js +0 -6
- package/dist/src/utils/RestApis.d.ts +0 -23
- package/dist/src/utils/RestApis.js +0 -30
- package/dist/src/utils/StdConsoleLogger.d.ts +0 -1
- package/dist/src/utils/StdConsoleLogger.js +0 -2
- package/package.json +10 -10
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.
|
|
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 &&
|
|
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.
|
|
24
|
+
"axios": "^1.12.2",
|
|
25
25
|
"bunyan": "^1.8.15",
|
|
26
|
-
"dotenv": "^
|
|
27
|
-
"express": "^5.0
|
|
28
|
-
"form-data": "^4.0.
|
|
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.
|
|
32
|
-
"tmp": "^0.2.
|
|
33
|
-
"ws": "^8.18.
|
|
31
|
+
"source-map": "^0.7.6",
|
|
32
|
+
"tmp": "^0.2.5",
|
|
33
|
+
"ws": "^8.18.3"
|
|
34
34
|
},
|
|
35
35
|
"devDependencies": {
|
|
36
|
-
"@
|
|
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.
|
|
41
|
-
"@types/
|
|
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.
|
|
45
|
-
"
|
|
46
|
-
"
|
|
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
|
-
"
|
|
51
|
-
"
|
|
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
|
}
|
package/dist/src/control/App.js
CHANGED
|
@@ -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;
|