@teamscale/coverage-collector 1.0.0-beta.7 → 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.
@@ -1,132 +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
- class CoverageDumper {
17
- static startRegularCollectorProcesses(storage, logger) {
18
- const lastConfigRefreshPerApp = new Map();
19
- const lastDumpTimestampPerApp = new Map();
20
- const doIfExpired = async (key, lastExecutionTimestamps, doAfterMillis, action) => {
21
- const lastExecutionTimestamp = lastExecutionTimestamps.get(key) ?? STARTUP_TIMESTAMP;
22
- if (Date.now() - lastExecutionTimestamp > doAfterMillis) {
23
- try {
24
- await action();
25
- }
26
- catch (error) {
27
- logger.error(`Regular collector action failed with an error: ` + error?.message);
28
- logger.error(error);
29
- }
30
- lastExecutionTimestamps.set(key, Date.now());
31
- }
32
- };
33
- const timer = setInterval(async () => {
34
- for (const appId of storage.getApplicationIDs()) {
35
- await doIfExpired(`config-update-${appId}`, lastConfigRefreshPerApp, 1000 * CONFIG_REQUERY_INTERVAL_SECONDS, async () => {
36
- await storage.refreshApplicationConfiguration(appId);
37
- });
38
- }
39
- for (const appId of storage.getApplicationIDs()) {
40
- const appDumpAfterMinutes = storage.getAppConfiguration(appId).dumpAfterMins ?? DEFAULT_COVERAGE_DUMP_AFTER_MINS;
41
- await doIfExpired(`coverage-dump-${appId}`, lastDumpTimestampPerApp, 1000 * DUMP_COVERAGE_CHECK_AFTER_SECONDS * appDumpAfterMinutes, async () => {
42
- await this.dumpCoverage(storage, logger, appId);
43
- });
44
- }
45
- }, 1000 * CHECK_FOR_SCHEDULED_ACTIVITY_AFTER_SECONDS);
46
- storage.addBeforeConfigUpdateCallback(async (appId, dataStorage) => {
47
- logger.info(`Dumping coverage before config update for application ${appId}.`);
48
- await this.dumpCoverage(dataStorage, logger, appId);
49
- });
50
- logger.info(`Started the regular remote configuration refresh and coverage dump.`);
51
- return {
52
- stop: () => {
53
- if (timer) {
54
- clearInterval(timer);
55
- }
56
- }
57
- };
58
- }
59
- static async dumpCoverage(storage, logger, onlyForAppId) {
60
- const appsToDumpFor = onlyForAppId ? [onlyForAppId] : storage.getApplicationIDs();
61
- const dumpTimestamp = new Date();
62
- let hadCoverageToDump = false;
63
- let hadUploadSent = false;
64
- for (const dumpForAppId of appsToDumpFor) {
65
- const dumpConfiguration = storage.getAppConfiguration(dumpForAppId);
66
- const dumpToFolder = this.determineCoverageTargetFolder(dumpForAppId, dumpConfiguration);
67
- const deleteAfterUpload = !(dumpConfiguration.keepCoverageFiles ?? dumpConfiguration.dumpToFolder !== undefined);
68
- try {
69
- const dumpSummary = storage.dumpToSimpleCoverageFile(dumpToFolder, dumpTimestamp, dumpForAppId);
70
- hadCoverageToDump = dumpSummary.hadCoverageToDump || hadCoverageToDump;
71
- for (const { simpleCoverageFile, simpleCoverageFileLines, commit } of dumpSummary.details) {
72
- try {
73
- if (dumpConfiguration.teamscaleServerUrl || dumpConfiguration.artifactoryServerUrl) {
74
- const coverageUploaded = await this.uploadCoverage(dumpConfiguration, simpleCoverageFile, simpleCoverageFileLines, commit, logger, deleteAfterUpload);
75
- hadUploadSent = coverageUploaded || hadUploadSent;
76
- }
77
- }
78
- catch (e) {
79
- if (e instanceof RestApis_1.UploadError) {
80
- logger.error(`Coverage upload failed for application ${dumpForAppId} and commit ${commit}.\n`
81
- + `The coverage files on disk (inside the folder "${dumpToFolder}") were not deleted. `
82
- + `You can still upload them manually. Error: ${e.message}`);
83
- }
84
- else {
85
- logger.error('Coverage dump failed', e);
86
- }
87
- }
88
- }
89
- }
90
- catch (e) {
91
- logger.error(`Dumping coverage failed for application ${dumpForAppId}`, e);
92
- }
93
- }
94
- if (hadUploadSent) {
95
- logger.info(`Uploaded coverage for timestamp ${dumpTimestamp.toISOString()} to a remote server.`);
96
- }
97
- if (!hadCoverageToDump) {
98
- logger.info('Coverage dump request was processed successfully; no new coverage to dump found.');
99
- }
100
- }
101
- static async uploadCoverage(config, coverageFile, lines, commit, logger, deleteAfterUpload) {
102
- let uploadDone = false;
103
- if (config.teamscaleServerUrl && (config.teamscaleProject ?? "").length > 0) {
104
- uploadDone = await (0, TeamscaleUpload_1.uploadToTeamscale)(config, logger, coverageFile, lines, commit);
105
- }
106
- if (config.artifactoryServerUrl) {
107
- uploadDone = uploadDone || await (0, ArtifactoryUpload_1.uploadToArtifactory)(config, logger, coverageFile, lines, commit);
108
- }
109
- if (deleteAfterUpload) {
110
- fs_1.default.unlinkSync(coverageFile);
111
- }
112
- return uploadDone;
113
- }
114
- static determineCoverageTargetFolder(appId, config) {
115
- const result = `${config.dumpFolder}/${makeProperDirectoryPath(config.dumpToFolder)}${appId}`;
116
- fs_1.default.mkdirSync(result, { recursive: true });
117
- return result;
118
- }
119
- }
120
- exports.CoverageDumper = CoverageDumper;
121
- function makeProperDirectoryPath(path) {
122
- if (path === undefined || path.trim().length === 0) {
123
- return '';
124
- }
125
- path = path.trim().replace(/^\/+/g, '');
126
- path = path.replace(/\\/g, '/');
127
- path = path.replace(/[^a-zA-Z0-9\-_/]/g, '_');
128
- if (path.endsWith('/')) {
129
- return path;
130
- }
131
- return `${path}/`;
132
- }
@@ -1,2 +0,0 @@
1
- #!/usr/bin/env node
2
- export {};
package/dist/src/main.js DELETED
@@ -1,26 +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
- (async () => {
11
- const app = await App_1.App.run();
12
- const handleExit = async () => {
13
- await app.stop();
14
- node_process_1.default.exit(node_process_1.default.exitCode);
15
- };
16
- node_process_1.default.on('SIGINT', () => handleExit());
17
- node_process_1.default.on('SIGTERM', () => handleExit());
18
- })().catch(reason => {
19
- if (reason instanceof commons_1.InvalidConfigurationException) {
20
- console.error(`Failed: ${reason.message}`);
21
- }
22
- else {
23
- console.error('Failed: ', reason);
24
- }
25
- node_process_1.default.exit(1);
26
- });
@@ -1,19 +0,0 @@
1
- import { DataStorage } from '../storage/DataStorage';
2
- import Logger from 'bunyan';
3
- export declare class WebSocketCollectingServer {
4
- private server;
5
- private readonly storage;
6
- private readonly logger;
7
- private totalNumMessagesReceived;
8
- private totalNumCoverageMessagesReceived;
9
- constructor(port: number, storage: DataStorage, logger: Logger);
10
- start(): {
11
- stop: () => void;
12
- };
13
- private handleMessage;
14
- private handleCoverageMessage;
15
- getStatistics(): {
16
- totalMessages: number;
17
- totalCoverageMessages: number;
18
- };
19
- }
@@ -1,141 +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
- const MESSAGE_TYPE_COVERAGE = 'c';
41
- const MESSAGE_TYPE_BUCKET = 'b';
42
- class WebSocketCollectingServer {
43
- server;
44
- storage;
45
- logger;
46
- totalNumMessagesReceived;
47
- totalNumCoverageMessagesReceived;
48
- constructor(port, storage, logger) {
49
- commons_1.Contract.require(port > 0 && port < 65536, 'Port must be valid (range).');
50
- this.storage = commons_1.Contract.requireDefined(storage);
51
- this.logger = commons_1.Contract.requireDefined(logger);
52
- this.server = new WebSocket.Server({ port });
53
- this.totalNumMessagesReceived = 0;
54
- this.totalNumCoverageMessagesReceived = 0;
55
- }
56
- start() {
57
- this.logger.info(`Starting server on port ${this.server?.options.port}.`);
58
- this.server?.on('connection', (webSocket, req) => {
59
- let session = new Session_1.Session(this.storage);
60
- this.logger.trace(`Connection from: ${req.socket.remoteAddress}`);
61
- webSocket.on('close', code => {
62
- if (session) {
63
- session = null;
64
- this.logger.trace(`Closing with code ${code}`);
65
- }
66
- });
67
- webSocket.on('message', (message) => {
68
- this.totalNumMessagesReceived += 1;
69
- if (session && Buffer.isBuffer(message)) {
70
- void this.handleMessage(session, message);
71
- }
72
- });
73
- webSocket.on('error', (e) => {
74
- this.logger.error('Error on server socket triggered.', e);
75
- });
76
- });
77
- return {
78
- stop: () => {
79
- this.server?.clients.forEach(client => client.close());
80
- this.server?.close();
81
- this.server = null;
82
- }
83
- };
84
- }
85
- async handleMessage(session, message) {
86
- try {
87
- const messageType = message.toString('utf8', 0, 1);
88
- if (messageType === MESSAGE_TYPE_BUCKET) {
89
- const bucketDefinitionEncoded = message.toString('utf-8', 1).trim();
90
- const bucketDefinitionDecoded = Buffer.from(bucketDefinitionEncoded, 'base64').toString('utf8');
91
- const bucketDefinition = JSON.parse(bucketDefinitionDecoded);
92
- this.logger.debug(`Received bucket definition: ${JSON.stringify(bucketDefinition)} for session ${session.sessionId}.`);
93
- await session.setupBucketAndApplication(bucketDefinition);
94
- }
95
- else if (messageType === MESSAGE_TYPE_COVERAGE) {
96
- this.totalNumCoverageMessagesReceived += 1;
97
- const linesCoveredApprox = this.handleCoverageMessage(session, message.subarray(1));
98
- if (linesCoveredApprox > 0) {
99
- this.logger.debug(`Coverage for approximately ${linesCoveredApprox} lines processed for session ${session.sessionId}.`);
100
- }
101
- }
102
- else {
103
- this.logger.error(`Unknown message type: ${messageType}`);
104
- }
105
- }
106
- catch (e) {
107
- this.logger.error(`Error while processing message starting with ${message.toString('utf8', 0, Math.min(250, message.length))}`);
108
- this.logger.error(e.message);
109
- }
110
- }
111
- handleCoverageMessage(session, body) {
112
- let lineCoverageStatements = 0;
113
- const tokens = body.toString().split(/[\n;]/).map(line => line.trim());
114
- let filename = '';
115
- tokens.forEach(token => {
116
- if (token.startsWith('@')) {
117
- filename = token.substring(1).trim();
118
- }
119
- else if (filename) {
120
- const range = token.split(/,|-/).map(value => Number.parseInt(value));
121
- if (range.length === 1) {
122
- lineCoverageStatements += session.putLineCoverage(filename, range[0], range[0]);
123
- }
124
- else if (range.length === 2) {
125
- lineCoverageStatements += session.putLineCoverage(filename, range[0], range[1]);
126
- }
127
- else {
128
- this.logger.error(`Invalid range token: ${token}`);
129
- }
130
- }
131
- });
132
- return lineCoverageStatements;
133
- }
134
- getStatistics() {
135
- return {
136
- totalMessages: this.totalNumMessagesReceived,
137
- totalCoverageMessages: this.totalNumCoverageMessagesReceived
138
- };
139
- }
140
- }
141
- exports.WebSocketCollectingServer = WebSocketCollectingServer;
@@ -1,11 +0,0 @@
1
- import { CoverageBucketSpecifier, DataStorage } from '../storage/DataStorage';
2
- export declare class Session {
3
- private readonly internalSessionId;
4
- private readonly storage;
5
- private appId?;
6
- private commitId?;
7
- constructor(storage: DataStorage);
8
- setupBucketAndApplication(bucketSpecifier: CoverageBucketSpecifier): Promise<void>;
9
- putLineCoverage(fileId: string, startLine: number, endLine: number): number;
10
- get sessionId(): string;
11
- }
@@ -1,33 +0,0 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.Session = void 0;
4
- const commons_1 = require("@cqse/commons");
5
- class Session {
6
- internalSessionId;
7
- storage;
8
- appId;
9
- commitId;
10
- constructor(storage) {
11
- this.storage = commons_1.Contract.requireDefined(storage);
12
- this.internalSessionId = Math.random().toString(36).substring(2, 15) + Math.random().toString(36).substring(2, 15);
13
- }
14
- async setupBucketAndApplication(bucketSpecifier) {
15
- this.commitId = bucketSpecifier.commit;
16
- this.appId = bucketSpecifier.appId;
17
- await this.storage.setupCoverageBucket(bucketSpecifier);
18
- }
19
- putLineCoverage(fileId, startLine, endLine) {
20
- commons_1.Contract.requireDefined(this.appId, "The application ID must be set before putting coverage.");
21
- commons_1.Contract.requireDefined(this.commitId, "The commit must be set before putting coverage.");
22
- const lines = [];
23
- for (let i = startLine; i <= endLine; i++) {
24
- lines.push(i);
25
- }
26
- this.storage.putCoverage(this.appId, this.commitId, fileId, lines);
27
- return lines.length;
28
- }
29
- get sessionId() {
30
- return this.internalSessionId;
31
- }
32
- }
33
- exports.Session = Session;
@@ -1,69 +0,0 @@
1
- import { CollectorOptions, ConfigurationParameters } from '@cqse/commons';
2
- import Logger from 'bunyan';
3
- export type FileCoverage = {
4
- sourceFile: string;
5
- coveredLines: Set<number>;
6
- };
7
- export type CoverageBucketSpecifier = {
8
- appId: string;
9
- configId?: string;
10
- configOptions?: string;
11
- commit: string;
12
- };
13
- export type DumpSummary = {
14
- hadCoverageToDump: boolean;
15
- details: CoverageDumpDetails[];
16
- };
17
- export type CoverageDumpDetails = {
18
- appId: string;
19
- commit: string;
20
- simpleCoverageFileLines: number;
21
- simpleCoverageFile: string;
22
- };
23
- export declare class CoverageBucket {
24
- readonly appId: string;
25
- readonly commit: string;
26
- private readonly coveredLinesByFile;
27
- constructor(appId: string, commit: string);
28
- putLine(sourceFile: string, line: number): void;
29
- getCoverage(): IterableIterator<FileCoverage>;
30
- clear(): void;
31
- }
32
- type AppId = string;
33
- type ConfigId = string;
34
- export type BeforeConfigUpdateCallback = (appId: string, dataStorage: DataStorage) => Promise<void>;
35
- export declare class DataStorage {
36
- private coverageBuckets;
37
- private readonly configPerApp;
38
- private readonly validatedPerApp;
39
- private readonly configIdPerApp;
40
- private readonly logger;
41
- private readonly baseConfig;
42
- private readonly reconfigurableParameters;
43
- private readonly allParameters;
44
- private readonly beforeConfigUpdateCallbacks;
45
- constructor(logger: Logger, allParameters: ConfigurationParameters, reconfigurableParameters: ConfigurationParameters, baseConfig: CollectorOptions);
46
- setupCoverageBucket(bucketSpecifier: CoverageBucketSpecifier): Promise<void>;
47
- changeAppConfiguration(appId: AppId, configId: ConfigId): Promise<void>;
48
- refreshAllRemoteConfigurations(): Promise<void>;
49
- refreshApplicationConfiguration(appId: string, newConfigId?: string): Promise<void>;
50
- private getOrCreateAppConfig;
51
- private setConfigurationParameter;
52
- private validateConfiguration;
53
- getAppConfiguration(appId: string): CollectorOptions;
54
- private getBucketFor;
55
- getBucket(appId: string, commit: string): CoverageBucket | undefined;
56
- private getAppBuckets;
57
- putCoverage(appId: string, commit: string, sourceFilePath: string, coveredOriginalLines: number[]): void;
58
- private static normalizeSourceFileName;
59
- dumpToSimpleCoverageFile(coverageFolder: string, date: Date, dumpForAppIdOnly?: string): DumpSummary;
60
- private resetCoverage;
61
- private prepareValidCoverageFileName;
62
- private toSimpleCoverage;
63
- getApplicationIDs(): Set<AppId>;
64
- getApplicationsWithConfig(configId: string): Set<AppId>;
65
- discardCollectedCoverage(appId?: AppId): void;
66
- addBeforeConfigUpdateCallback(callback: BeforeConfigUpdateCallback): void;
67
- }
68
- export declare function formatTimestamp(date: Date): string;
69
- export {};