@teamscale/coverage-collector 0.1.0-beta.7 → 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
package/dist/package.json CHANGED
@@ -1,16 +1,13 @@
1
1
  {
2
2
  "name": "@teamscale/coverage-collector",
3
- "version": "0.1.0-beta.7",
3
+ "version": "1.0.0-beta.2",
4
4
  "description": "Collector for JavaScript code coverage information",
5
5
  "main": "dist/src/main.js",
6
6
  "bin": "dist/src/main.js",
7
7
  "types": "dist/src/main.d.ts",
8
8
  "author": "CQSE GmbH",
9
9
  "license": "Apache-2.0",
10
- "repository": {
11
- "type": "git",
12
- "url": "https://github.com/cqse/teamscale-javascript-profiler.git"
13
- },
10
+ "homepage": "https://docs.teamscale.com/howto/setting-up-profiler-tga/javascript/",
14
11
  "scripts": {
15
12
  "prepublishOnly": "pnpm clean && pnpm build",
16
13
  "clean": "rimraf dist tsconfig.tsbuildinfo",
@@ -22,41 +19,42 @@
22
19
  "dist/**/*"
23
20
  ],
24
21
  "dependencies": {
25
- "@cqse/commons": "0.1.0-beta.6",
22
+ "@cqse/commons": "workspace:../cqse-commons",
26
23
  "argparse": "^2.0.1",
27
- "async": "^3.2.5",
28
- "axios": "^1.6.7",
24
+ "async": "^3.2.6",
25
+ "axios": "^1.7.9",
29
26
  "bunyan": "^1.8.15",
30
- "date-and-time": "^3.1.1",
31
- "dotenv": "^16.4.5",
32
- "express": "^4.18.3",
33
- "form-data": "^4.0.0",
27
+ "date-and-time": "^3.6.0",
28
+ "dotenv": "^16.4.7",
29
+ "express": "^5.0.1",
30
+ "form-data": "^4.0.2",
34
31
  "mkdirp": "^3.0.1",
32
+ "node-cache": "^5.1.2",
35
33
  "rxjs": "^7.8.1",
36
34
  "source-map": "^0.7.4",
37
35
  "tmp": "^0.2.3",
38
36
  "typescript-optional": "^2.0.1",
39
- "ws": "^8.16.0"
37
+ "ws": "^8.18.0"
40
38
  },
41
39
  "devDependencies": {
42
- "@babel/core": "^7.24.0",
43
- "@babel/preset-env": "^7.24.0",
44
- "@types/argparse": "^2.0.15",
40
+ "@babel/core": "^7.26.8",
41
+ "@babel/preset-env": "^7.26.8",
42
+ "@types/argparse": "^2.0.17",
45
43
  "@types/async": "^3.2.24",
46
44
  "@types/bunyan": "^1.8.11",
47
- "@types/express": "^4.17.21",
48
- "@types/jest": "^29.5.12",
49
- "@types/node": "^20.11.28",
45
+ "@types/express": "^5.0.0",
46
+ "@types/jest": "^29.5.14",
47
+ "@types/node": "^22.13.4",
50
48
  "@types/tmp": "^0.2.6",
51
- "@types/ws": "^8.5.10",
49
+ "@types/ws": "^8.5.14",
52
50
  "babel-jest": "^29.7.0",
53
- "esbuild": "^0.20.2",
51
+ "esbuild": "^0.25.0",
54
52
  "jest": "^29.7.0",
55
- "mockttp": "^3.10.1",
56
- "rimraf": "^5.0.5",
57
- "ts-jest": "^29.1.2",
53
+ "mockttp": "3.15.5",
54
+ "rimraf": "^6.0.1",
55
+ "ts-jest": "^29.2.5",
58
56
  "ts-node": "^10.9.2",
59
- "typescript": "^5.4.2"
57
+ "typescript": "^5.7.3"
60
58
  },
61
59
  "publishConfig": {
62
60
  "access": "public"
@@ -0,0 +1,24 @@
1
+ import { CollectorOptions } from '@cqse/commons';
2
+ import Logger from 'bunyan';
3
+ type ProfilerConfiguration = {
4
+ configurationId: string;
5
+ configurationOptions?: string;
6
+ };
7
+ /**
8
+ * Utility methods for handling remote profiler configuration. The configuration is retrieved from Teasmcale.
9
+ */
10
+ export declare class RemoteProfilerConfig {
11
+ /**
12
+ * Query the profiler configuration with the given ID via the Teamscale API.
13
+ */
14
+ static queryConfiguration(baseConfig: CollectorOptions, configId: string, logger: Logger): Promise<Map<string, string>>;
15
+ /**
16
+ * Visible for testing.
17
+ */
18
+ static parseConfigurationStringIntoMap(response: ProfilerConfiguration): Map<string, string>;
19
+ }
20
+ /**
21
+ * For testing, clear the configuration cache.
22
+ */
23
+ export declare function clearConfigurationCache(): void;
24
+ export {};
@@ -0,0 +1,76 @@
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.RemoteProfilerConfig = void 0;
7
+ exports.clearConfigurationCache = clearConfigurationCache;
8
+ const RestApis_1 = require("../utils/RestApis");
9
+ const commons_1 = require("@cqse/commons");
10
+ const axios_1 = __importDefault(require("axios"));
11
+ const node_cache_1 = __importDefault(require("node-cache"));
12
+ /**
13
+ * We cache the configuration we got from Teamscale for 60s.
14
+ */
15
+ 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
+ class RemoteProfilerConfig {
20
+ /**
21
+ * Query the profiler configuration with the given ID via the Teamscale API.
22
+ */
23
+ static async queryConfiguration(baseConfig, configId, logger) {
24
+ if (!baseConfig.teamscaleServerUrl || !baseConfig.teamscaleAccessToken || !baseConfig.teamscaleUser) {
25
+ throw new Error("Access to Teamscale is not configured. Receiving profiler configurations is not possible. " +
26
+ "Please specify configuration arguments for --teamscale-server-url, --teamscale-user, and --teamscale-access-token");
27
+ }
28
+ let result = CONFIG_CACHE.get(configId);
29
+ try {
30
+ if (result) {
31
+ logger.debug('Using cached configuration with id ' + configId);
32
+ return result;
33
+ }
34
+ logger.debug(`Requesting configuration with ID ${configId} from Teamscale.`);
35
+ const response = await (0, RestApis_1.performRequest)(`${(0, RestApis_1.removeTrailingUrlSlash)(baseConfig.teamscaleServerUrl)}/api/${RestApis_1.TEAMSCALE_API_VERSION}/profiler-configurations/${configId}`, undefined, (0, RestApis_1.prepareTeamscaleApiRequestConfig)(baseConfig, {}), axios_1.default.get, logger);
36
+ if (response === undefined) {
37
+ throw new commons_1.InvalidConfigurationException(`No configuration found with ID ${configId}`);
38
+ }
39
+ // Fail for an empty configuration. We do not accept an empty configuration
40
+ // for a specified configuration ID.
41
+ if (!response.configurationOptions || response.configurationOptions.trim().length === 0) {
42
+ throw new commons_1.InvalidConfigurationException(`Configuration with ID "${configId}" is empty. Please specify relevant configuration options.`);
43
+ }
44
+ result = this.parseConfigurationStringIntoMap(response);
45
+ CONFIG_CACHE.set(configId, result);
46
+ }
47
+ catch (error) {
48
+ throw new Error(`Failed to retrieve configuration with ID ${configId}: ${error.message}`, { cause: error });
49
+ }
50
+ return result;
51
+ }
52
+ /**
53
+ * Visible for testing.
54
+ */
55
+ static parseConfigurationStringIntoMap(response) {
56
+ const result = new Map();
57
+ (response.configurationOptions ?? "").split(/[\r\n]+/).map(line => line.trim())
58
+ .filter(line => line.length > 0)
59
+ .filter(line => !line.startsWith('#'))
60
+ .forEach(line => {
61
+ const split = line.split('=');
62
+ if (split.length !== 2) {
63
+ throw new commons_1.InvalidConfigurationException(`Invalid configuration line; expecting a valid key=value pair: ${line}`);
64
+ }
65
+ result.set(split[0].trim(), split[1].trim());
66
+ });
67
+ return result;
68
+ }
69
+ }
70
+ exports.RemoteProfilerConfig = RemoteProfilerConfig;
71
+ /**
72
+ * For testing, clear the configuration cache.
73
+ */
74
+ function clearConfigurationCache() {
75
+ CONFIG_CACHE.flushAll();
76
+ }
@@ -0,0 +1,31 @@
1
+ import { ConfigurationParameters, StaticCollectorOptions } from '@cqse/commons';
2
+ import 'dotenv/config';
3
+ /**
4
+ * Callback interface to be called to stop a process in a controlled fashion.
5
+ */
6
+ export type Stoppable = {
7
+ stop: () => Promise<void>;
8
+ };
9
+ /**
10
+ * The main class of the Teamscale JavaScript Collector.
11
+ * Used to start the collector with a given configuration.
12
+ */
13
+ export declare class App {
14
+ /**
15
+ * Construct the logger.
16
+ */
17
+ private static buildLogger;
18
+ /**
19
+ * Entry point of the Teamscale JavaScript Profiler.
20
+ */
21
+ static run(): Promise<Stoppable>;
22
+ /**
23
+ * Run the collector with the given configuration options.
24
+ */
25
+ 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
+ private static startNoMessageTimer;
31
+ }
@@ -0,0 +1,202 @@
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
+ var __importDefault = (this && this.__importDefault) || function (mod) {
36
+ return (mod && mod.__esModule) ? mod : { "default": mod };
37
+ };
38
+ Object.defineProperty(exports, "__esModule", { value: true });
39
+ exports.App = void 0;
40
+ const CoverageDumper_1 = require("./CoverageDumper");
41
+ const StdConsoleLogger_1 = require("../utils/StdConsoleLogger");
42
+ const PrettyFileLogger_1 = require("../utils/PrettyFileLogger");
43
+ const DataStorage_1 = require("../storage/DataStorage");
44
+ const CollectingServer_1 = require("../receiver/CollectingServer");
45
+ const ControlServer_1 = require("./ControlServer");
46
+ const commons_1 = require("@cqse/commons");
47
+ const package_json_1 = require("../../package.json");
48
+ const bunyan_1 = __importDefault(require("bunyan"));
49
+ require("dotenv/config");
50
+ const fs = __importStar(require("fs"));
51
+ const mkdirp_1 = require("mkdirp");
52
+ const path_1 = __importDefault(require("path"));
53
+ const TeamscaleUpload_1 = require("../upload/TeamscaleUpload");
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
+ class App {
60
+ /**
61
+ * Construct the logger.
62
+ */
63
+ static buildLogger(config) {
64
+ const logfilePath = config.logToFile.trim();
65
+ mkdirp_1.mkdirp.sync(path_1.default.dirname(logfilePath));
66
+ const logLevel = config.logLevel;
67
+ const logger = bunyan_1.default.createLogger({
68
+ name: 'Collector',
69
+ streams: [
70
+ // console output
71
+ { level: logLevel, stream: new StdConsoleLogger_1.StdConsoleLogger(), type: 'raw' },
72
+ // default log file
73
+ { level: logLevel, stream: new PrettyFileLogger_1.PrettyFileLogger(fs.createWriteStream(logfilePath)), type: 'raw' }
74
+ ]
75
+ });
76
+ // If the given flag is set, we also log with a JSON-like format
77
+ if (config.jsonLog) {
78
+ logger.addStream({ level: logLevel, path: `${logfilePath}.json` });
79
+ }
80
+ return logger;
81
+ }
82
+ /**
83
+ * Entry point of the Teamscale JavaScript Profiler.
84
+ */
85
+ static async run() {
86
+ const configParameters = (0, commons_1.buildStaticCollectorParameters)();
87
+ const appInfos = { about: package_json_1.description, version: package_json_1.version, name: package_json_1.name };
88
+ const config = (0, commons_1.processCommandLine)(configParameters, appInfos);
89
+ return await App.runWithConfig(configParameters, config);
90
+ }
91
+ /**
92
+ * Run the collector with the given configuration options.
93
+ */
94
+ static async runWithConfig(staticParameters, config) {
95
+ // Build the logger
96
+ const logger = this.buildLogger(config);
97
+ logger.info(`Starting collector in working directory "${process.cwd()}".`);
98
+ logger.info(`Will dump coverage to directory "${config.dumpFolder}".`);
99
+ logger.info(`Logging "${config.logLevel}" to "${config.logToFile}".`);
100
+ // Check the connection to Teamscale, if needed.
101
+ if (config.teamscaleServerUrl) {
102
+ const checkResult = await (0, TeamscaleUpload_1.checkTeamscaleCredentials)(config, logger);
103
+ if (!checkResult) {
104
+ logger.error('Could not connect to Teamscale with the given credential. Please check your configuration.');
105
+ process.exit(1);
106
+ }
107
+ }
108
+ // Ensure that the root coverage folder is writable.
109
+ if (config.dumpFolder) {
110
+ const dumpFolder = path_1.default.resolve(config.dumpFolder);
111
+ ensureWritableFolder(dumpFolder, logger);
112
+ }
113
+ // Prepare the storage and the server
114
+ 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
+ reconfigurableParameters.addArgumentCheck((options) => {
118
+ 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
+ if (options.teamscaleProject && !options.teamscalePartition) {
123
+ return 'The Teamscale project (parameter teamscaleProject) and coverage partition (parameter teamscalePartition) ' +
124
+ 'must be configured for an upload to Teamscale.';
125
+ }
126
+ }
127
+ });
128
+ const storage = new DataStorage_1.DataStorage(logger, (0, commons_1.parameterUnion)(staticParameters, reconfigurableParameters), reconfigurableParameters, config);
129
+ const server = new CollectingServer_1.WebSocketCollectingServer(config.port, storage, logger);
130
+ // Enable the remote control API if configured
131
+ const controlServer = new ControlServer_1.ControlServer(config, storage, logger);
132
+ const controlServerState = controlServer.start();
133
+ // Start the server socket.
134
+ // ATTENTION: The server is executed asynchronously.
135
+ const serverState = server.start();
136
+ // Optionally, start a timer that dumps the coverage after N seconds
137
+ const dumpTimerState = CoverageDumper_1.CoverageDumper.startRegularCollectorProcesses(storage, logger);
138
+ // Start a timer that informs if no coverage was received within the last minute
139
+ const statsTimerState = this.startNoMessageTimer(logger, server);
140
+ const stop = async function () {
141
+ 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
+ await new Promise(resolve => {
148
+ setTimeout(async () => {
149
+ await CoverageDumper_1.CoverageDumper.dumpCoverage(storage, logger);
150
+ resolve(undefined);
151
+ }, 0);
152
+ });
153
+ // Stop all timers and sockets.
154
+ dumpTimerState.stop();
155
+ statsTimerState.stop();
156
+ await controlServerState.stop();
157
+ serverState.stop();
158
+ logger.info('Bye bye.');
159
+ };
160
+ return { stop };
161
+ }
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
+ static startNoMessageTimer(logger, server) {
167
+ const startTime = Date.now();
168
+ const timer = setInterval(async () => {
169
+ const stats = server.getStatistics();
170
+ if (stats.totalCoverageMessages === 0) {
171
+ logger.info(`No coverage received for ${((Date.now() - startTime) / 1000.0).toFixed(0)}s.`);
172
+ }
173
+ else {
174
+ // We can stop running the timer after we have received the first coverage.
175
+ clearInterval(timer);
176
+ }
177
+ }, 1000 * 60);
178
+ return {
179
+ stop: () => clearInterval(timer)
180
+ };
181
+ }
182
+ }
183
+ exports.App = App;
184
+ function ensureWritableFolder(dumpFolder, logger) {
185
+ try {
186
+ // 1. Create the folder when it doesn't exist (recursive = true is a no-op when it does).
187
+ mkdirp_1.mkdirp.sync(dumpFolder);
188
+ // 2. Verify that we ended up with a directory.
189
+ const stat = fs.statSync(dumpFolder);
190
+ if (!stat.isDirectory()) {
191
+ throw new Error(`"${dumpFolder}" exists but is not a directory.`);
192
+ }
193
+ // 3. Perform a real write test.
194
+ const testFile = path_1.default.join(dumpFolder, `.write-test-${Date.now()}`);
195
+ fs.writeFileSync(testFile, 'writable?');
196
+ fs.unlinkSync(testFile);
197
+ }
198
+ catch (err) {
199
+ logger.error(`The configured dump folder "${dumpFolder}" is not writable or usable: ${err.message}`);
200
+ process.exit(1);
201
+ }
202
+ }
@@ -0,0 +1,24 @@
1
+ import { DataStorage } from '../storage/DataStorage';
2
+ import Logger from 'bunyan';
3
+ import { CollectorOptions } from '@cqse/commons';
4
+ /**
5
+ * Provides a REST API for remote configuration of the collector.
6
+ */
7
+ export declare class ControlServer {
8
+ private config;
9
+ private storage;
10
+ private logger;
11
+ constructor(config: CollectorOptions, storage: DataStorage, logger: Logger);
12
+ /**
13
+ * Start the collector remote config API.
14
+ */
15
+ start(): {
16
+ stop: () => Promise<void>;
17
+ };
18
+ private handleRefreshConfigs;
19
+ private handleDumpPost;
20
+ private handleDumpPostForConfig;
21
+ private handleGlobalCoverageReset;
22
+ private handleCoverageResetForConfig;
23
+ private handleConfigScopedRequest;
24
+ }
@@ -0,0 +1,100 @@
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;
@@ -0,0 +1,27 @@
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
+ }