@teamscale/coverage-collector 1.0.5 → 1.0.6
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/dist/main.mjs +1 -1
- package/package.json +13 -13
package/dist/main.mjs
CHANGED
|
@@ -5,4 +5,4 @@ import e from"form-data";import*as t from"fs";import n from"fs";import r from"ax
|
|
|
5
5
|
`)}function he(e){let t=new Map,n=new Map,r=new Map;for(let i of e.getParameters()){let e=i.group?.title;i.group&&n.set(e,i.group.order),i.group?.hints&&r.set(e,i.group?.hints);let a=t.get(e)||[];a.push(i),t.set(e,a)}return Array.from(t.entries()).sort(([e],[t])=>(n.get(e)??Number.MAX_VALUE)-(n.get(t)??Number.MAX_VALUE)).map(([e,t])=>({groupTitle:e,groupHint:r.get(e),parameters:t.sort((e,t)=>e.longParameter.localeCompare(t.longParameter))}))}function B(e,t,n){let r=!0;n||=e=>{console.error(e)};for(let i of e.getArgumentChecks()){let e=i(t);e&&(n(`Invalid configuration: `+e),r=!1)}return r}function ge(e,t,n,r){n??=c.argv.slice(2);let i=e.clone();i.addParameter(`-h`,`--help`,`bool`,{help:`Show this help message and exit.`,group:j,disableEnvironmentVariableOverride:!0}),i.addParameter(`-v`,`--version`,`bool`,{help:`Show the version number and exit.`,group:j,disableEnvironmentVariableOverride:!0});let a=me(n,i);return a.version&&(console.log(`${t.name} v${t.version}`),c.exit(0)),a.help&&(L(i,t.about),c.exit(0)),r&&r(a),B(i,a)||(console.error(`
|
|
6
6
|
Arguments were missing or invalid. Please check the --help for more information. Exiting with error code 1.`),c.exit(1)),a}function _e(e,t){let n=e.clone();return n.addAllOf(t),n}var V=class e{constructor(e,t,n,r){this.baseConfiguration=A.requireDefined(n),this.redefinableParameters=A.requireDefined(t),this.allParameters=A.requireDefined(e),this.overwrites=r??{},this.hash=void 0}get(e){return this.assertParameterExists(e),this.overwrites[e]??this.baseConfiguration[e]}set(e,t){if(this.redefinableParameters.lookupParameter(e)===void 0)throw Error(`Unknown configuration parameter: ${e}`);this.overwriteConfig(e,t),this.hash=void 0}overwriteConfig(e,t){let n=M(e);if(this.assertParameterExists(n),t===void 0){delete this.overwrites[n];return}this.overwrites[n]=this.castConfigArgument(n,t),this.hash=void 0}castConfigArgument(e,t){let n=this.getParameter(e).type;if(n===`int`){if(typeof t==`number`)return Math.floor(t);if(typeof t==`string`)return parseInt(t,10);if(typeof t==`boolean`)return t?1:0}else if(n===`bool`){if(typeof t==`boolean`)return t;if(typeof t==`string`)return t.toLowerCase()===`true`;if(typeof t==`number`)return t!==0}else if(n===`string[]`){if(t===void 0||t==null)return[];if(Array.isArray(t))return t.map(String);if(typeof t==`string`)return t.split(`,`).map(e=>e.trim())}else return String(t);throw Error(`Cannot cast value of type ${typeof t} to ${n}`)}assertParameterExists(e){if(this.allParameters.lookupParameter(e)===void 0)throw Error(`Unknown configuration parameter: ${e}`)}getParameter(e){let t=this.redefinableParameters.lookupParameter(e);if(t===void 0)throw Error(`Unknown configuration parameter: ${e}`);return t}getHash(){if(this.hash===void 0){let e=l(`sha256`);for(let[t,n]of Object.entries(this.baseConfiguration))e.update(t),e.update(String(n));for(let[t,n]of Object.entries(this.overwrites))e.update(t),e.update(String(n));this.hash=e.digest(`hex`)}return this.hash}copy(){let t=JSON.parse(JSON.stringify(this.overwrites));return new e(this.allParameters,this.redefinableParameters,this.baseConfiguration,t)}};function H(e){return new Proxy(e,{get(t,n){return e.get(n.toString())},set(t,n,r){return e.set(n.toString(),r),!0}})}const U={order:1,title:`Collector Connectivity`},W={order:1,title:`Teamscale Server`},G={order:2,title:`Upload to Teamscale`,hints:"NOTE: We generally recommend to set these parameters per app during the instrumentation process, that is, on the instrumenter side, for example, by setting `--config-id` or specifying single options via `--collector-option`. The following parameters are supposed to take effect if the application is instrumented with the `--coverage-target-from-collector` flag."},K={order:3,title:`Coverage Dumping`},q={order:4,title:`Upload to Artifactory`},J={order:5,title:`Logging Behavior`};function Y(){let e=new pe;function t(t,n,r,i){e.addParameter(t,n,r,i)}return t(`-k`,`--keep-coverage-files`,`bool`,{help:`Whether to keep the coverage files on disk after a successful upload to Teamscale.`,default:!1,group:K}),t(`-t`,`--dump-after-mins`,`int`,{help:`Dump the coverage information every N minutes.`,default:120,group:K}),t(void 0,`--artifactory-server-url`,`string`,{help:`Upload the coverage to the given Artifactory server URL. The URL may include a subpath on the artifactory server, e.g. https://artifactory.acme.com/my-repo/my/subpath`,group:q}),t(void 0,`--artifactory-user`,`string`,{help:`The user for uploading coverage to Artifactory. Only needed when not using the --artifactory-access-token option`,group:q}),t(void 0,`--artifactory-password`,`string`,{help:`The password for uploading coverage to Artifactory. Only needed when not using the --artifactory-access-token option`,group:q}),t(void 0,`--artifactory-access-token`,`string`,{help:`The access_token for uploading coverage to Artifactory.`,group:q}),t(void 0,`--artifactory-path-suffix`,`string`,{help:`(optional): The path within the storage location between the default path and the uploaded artifact.`,group:q}),t(void 0,`--teamscale-project`,`string`,{help:`The project ID to upload coverage to.`,group:G}),t(void 0,`--teamscale-partition`,`string`,{help:`The partition to upload coverage to.`,group:G}),t(void 0,`--teamscale-repository`,`string`,{help:`The repository to upload coverage for. Optional: Only needed when uploading via revision to a project that has more than one connector.`,group:G}),t(void 0,`--teamscale-message`,`string`,{help:`The commit message shown within Teamscale for the coverage upload.`,default:`JavaScript coverage upload`,group:G}),e}function ve(){let e=Y();function t(t,n,r,i){e.addParameter(t,n,r,i)}return t(`-p`,`--port`,`int`,{help:`The port to receive coverage information on.`,default:54678,group:U,disableEnvironmentVariableOverride:!0}),t(`-l`,`--log-to-file`,`string`,{help:`Log file`,default:`logs/collector-combined.log`,group:J,disableEnvironmentVariableOverride:!0}),t(`-e`,`--log-level`,`string`,{help:`Log level`,default:`info`,group:J,disableEnvironmentVariableOverride:!0}),t(`-j`,`--json-log`,`bool`,{help:`Additional JSON-like log file format.`,group:J,disableEnvironmentVariableOverride:!0}),t(`-c`,`--enable-control-port`,`int`,{help:`Enables the remote control API on the specified port (<=0 means "disabled").`,default:0,group:U}),t(`-s`,`--teamscale-server-url`,`string`,{help:`Upload the coverage to the given Teamscale server URL, for example, https://teamscale.dev.example.com:8080/production.`,group:W}),t(`-u`,`--teamscale-user`,`string`,{help:`The user for uploading coverage to Teamscale.`,group:W}),t(`-a`,`--teamscale-access-token`,`string`,{help:`The API key to use for uploading to Teamscale.`,group:W}),t(void 0,`--http-proxy`,`string`,{help:`(optional): The HTTP/HTTPS proxy address that should be used in the format: http://host:port/ or http://username:password@host:port/.`,group:U}),t(`-f`,`--dump-folder`,`string`,{help:`Target folder for coverage files.`,default:`./coverage`,group:K}),e.addArgumentCheck(e=>{if(e.teamscaleServerUrl&&(!e.teamscaleUser||!e.teamscaleAccessToken))return`The Teamscale user name and access token must be given if the Teamscale server URL is given.`}),e}function ye(){let e=Y();function t(t,n,r,i){e.addParameter(t,n,r,i)}function n(e,n,r){t(void 0,e,n,r)}return n(`--dump-to-folder`,`string`,{help:`Coverage should be dumped to a folder on the server that the collector is running on.
|
|
7
7
|
Specifies the name of the subfolder within the collector's dump folder (--dump-folder of the collector) where coverage files should be placed.`,group:K}),e}const X=new u({stdTTL:60,deleteOnExpire:!0});var be=class{static async queryConfiguration(e,t,n){if(!e.teamscaleServerUrl||!e.teamscaleAccessToken||!e.teamscaleUser)throw Error(`Access to Teamscale is not configured. Receiving profiler configurations is not possible. Please specify configuration arguments for --teamscale-server-url, --teamscale-user, and --teamscale-access-token`);let i=X.get(t);try{if(i)return n.debug(`Using cached configuration with id `+t),i;n.debug(`Requesting configuration with ID ${t} from Teamscale.`);let a=await y(`${S(e.teamscaleServerUrl)}/api/${h}/profiler-configurations/${t}`,void 0,b(e,{}),r.get,n);if(a===void 0)throw new k(`No configuration found with ID ${t}`);if(!a.configurationOptions||a.configurationOptions.trim().length===0)throw new k(`Configuration with ID "${t}" is empty. Please specify relevant configuration options.`);i=this.parseConfigurationStringIntoMap(a),X.set(t,i)}catch(e){throw Error(`Failed to retrieve configuration with ID ${t}: ${e.message}`,{cause:e})}return i}static parseConfigurationStringIntoMap(e){let t=new Map;return(e.configurationOptions??``).split(/[\r\n]+/).map(e=>e.trim()).filter(e=>e.length>0).filter(e=>!e.startsWith(`#`)).forEach(e=>{let n=e.split(`=`);if(n.length!==2)throw new k(`Invalid configuration line; expecting a valid key=value pair: ${e}`);t.set(n[0].trim(),n[1].trim())}),t}};function Z(){X.flushAll()}var Q=class{appId;commit;coveredLinesByFile;constructor(e,t){this.coveredLinesByFile=new Map,this.appId=e,this.commit=t}putLine(e,t){let n=this.coveredLinesByFile.get(e);n||(n=new Set,this.coveredLinesByFile.set(e,n)),n.add(t)}getCoverage(){function*e(e,t){for(let n of e)yield t(n)}return e(this.coveredLinesByFile.entries(),([e,t])=>({sourceFile:e,coveredLines:t}))}clear(){this.coveredLinesByFile.clear()}},xe=class e{coverageBuckets;configPerApp;validatedPerApp;configIdPerApp;logger;baseConfig;reconfigurableParameters;allParameters;beforeConfigUpdateCallbacks;constructor(e,t,n,r){this.coverageBuckets=new Map,this.configIdPerApp=new Map,this.configPerApp=new Map,this.validatedPerApp=new Map,this.logger=A.requireDefined(e),this.baseConfig=A.requireDefined(r),this.allParameters=A.requireDefined(t),this.reconfigurableParameters=A.requireDefined(n),this.beforeConfigUpdateCallbacks=[]}async setupCoverageBucket(e){this.getBucketFor(e);try{if(e.configId&&await this.changeAppConfiguration(e.appId,e.configId),!e.configOptions)return;let t=e.configOptions.split(/\r?\n/).map(e=>e.trim());for(let n of t){if(n.length===0||n.startsWith(`#`))continue;let t=n.split(`=`);if(t.length===1&&this.setConfigurationParameter(e.appId,t[0],void 0),t.length===2){let[n,r]=t;this.setConfigurationParameter(e.appId,n.trim(),r.trim())}else throw Error(`Invalid configuration option '${n}'. Expected format <key>=<value>.`)}}catch(e){this.logger.error(`Setting up a the configuration of a coverage bucket failed. Your profiler configuration seems to be invalid: `+e?.message)}}async changeAppConfiguration(e,t){let n=this.configIdPerApp.get(e);(!n||n!==t)&&await this.refreshApplicationConfiguration(e,t)}async refreshAllRemoteConfigurations(){Z();for(let e of this.getApplicationIDs())await this.refreshApplicationConfiguration(e)}async refreshApplicationConfiguration(e,t){let n=t??this.configIdPerApp.get(e);if(n){try{let t=await be.queryConfiguration(this.baseConfig,n,this.logger),r=this.getOrCreateAppConfig(e).copy();for(let[n,i]of t.entries())try{r.overwriteConfig(n,i)}catch(t){this.logger.error(`Failed to set configuration parameter ${n}=${i} for app ${e}: ${t}`)}let i=this.validatedPerApp.get(e),a=i!==void 0&&r.getHash()!==i;if(this.validateConfiguration(e,r),a)for(let t of this.beforeConfigUpdateCallbacks)await t(e,this);this.configPerApp.set(e,r)}catch(t){throw this.logger.error(`Failed to retrieve configuration for app ${e} with ID ${n}: ${t}`),this.logger.error(t),t}this.configIdPerApp.set(e,n)}}getOrCreateAppConfig(e){let t=this.configPerApp.get(e);return t||(t=new V(this.allParameters,this.reconfigurableParameters,this.baseConfig),this.configPerApp.set(e,t)),t}setConfigurationParameter(e,t,n){this.getOrCreateAppConfig(e).overwriteConfig(t,n)}validateConfiguration(e,t){if(t===void 0)throw Error(`Configuration for app "${e}" not found`);let n=this.validatedPerApp.get(e),r=t.getHash();n!==r&&(B(this.reconfigurableParameters,H(t),e=>this.logger.error(e)),this.validatedPerApp.set(e,r))}getAppConfiguration(e){let t=this.configPerApp.get(e);return t||(t=new V(this.allParameters,this.reconfigurableParameters,this.baseConfig),this.configPerApp.set(e,t)),H(t)}getBucketFor(e){let t=$(e.appId,e.commit),n=this.coverageBuckets.get(t);return n||(n=new Q(e.appId,e.commit),this.coverageBuckets.set(t,n)),t}getBucket(e,t){return this.coverageBuckets.get($(e,t))}getAppBuckets(e){return Array.from(this.coverageBuckets.values()).filter(t=>e===void 0||t.appId===e)}putCoverage(t,n,r,i){let a=e.normalizeSourceFileName(r),o=$(t,n),s=this.coverageBuckets.get(o);s||(s=new Q(t,n),this.coverageBuckets.set(o,s)),i.forEach(e=>s?.putLine(r,e)),this.logger.trace(`Mapped Coverage: ${t} ${n} ${a} ${i}`)}static normalizeSourceFileName(e){return ue(`webpack:///`,e.replace(`\\`,`/`))}dumpToSimpleCoverageFile(e,n,r){e=e.trim();let i=[],a=!1,o=r?[r]:this.getApplicationIDs();for(let r of o){let o=this.toSimpleCoverage(r);for(let[s,[c,l]]of o)if(a||=c>0,c>0){let a=this.prepareValidCoverageFileName(e,n,s,r);t.writeFileSync(a,l,{flag:`w`,encoding:`utf8`}),this.logger.debug(`Simple coverage with length ${l.length} dumped to file ${a}.`),i.push({appId:r,simpleCoverageFile:a,simpleCoverageFileLines:c,commit:s})}this.resetCoverage(r)}return{hadCoverageToDump:a,details:i}}resetCoverage(e){this.getAppBuckets(e).forEach(e=>e.clear())}prepareValidCoverageFileName(e,n,r,i){t.existsSync(e)||t.mkdirSync(e);let a=Ce(n);return i?d.join(e,Se(`coverage-${i}-${a}-${r}.simple`)):d.join(e,`coverage-${a}.simple`)}toSimpleCoverage(t){let n=new Map;for(let r of this.getAppBuckets(t)){let t=n.get(r.commit);t||(t=[],n.set(r.commit,t));let i=r.getCoverage();for(let n of i){t.push(e.normalizeSourceFileName(n.sourceFile));for(let e of n.coveredLines)t.push(String(e))}}let r=new Map;for(let[e,t]of n)r.set(e,[t.length,t.join(`
|
|
8
|
-
`)]);return r}getApplicationIDs(){return new Set(Array.from(this.coverageBuckets.values()).map(e=>e.appId))}getApplicationsWithConfig(e){return new Set(Array.from(this.coverageBuckets.values()).filter(t=>this.configIdPerApp.get(t.appId)===e).map(e=>e.appId))}discardCollectedCoverage(e){$?this.getAppBuckets(e).forEach(e=>e.clear()):this.coverageBuckets.clear()}addBeforeConfigUpdateCallback(e){this.beforeConfigUpdateCallbacks.push(e)}};function $(e,t){return`${e}-${t}`}function Se(e){return e.replace(/[^a-zA-Z0-9._-]/g,`_`)}function Ce(e){let t=e=>e.toString().padStart(2,`0`);return`${e.getFullYear()}-${t(e.getMonth()+1)}-${t(e.getDate())}-${t(e.getHours())}-${t(e.getMinutes())}-${t(e.getSeconds())}`}var we=class{internalSessionId;storage;appId;commitId;constructor(e){this.storage=A.requireDefined(e),this.internalSessionId=Math.random().toString(36).substring(2,15)+Math.random().toString(36).substring(2,15)}async setupBucketAndApplication(e){this.commitId=e.commit,this.appId=e.appId,await this.storage.setupCoverageBucket(e)}putLineCoverage(e,t,n){A.requireDefined(this.appId,`The application ID must be set before putting coverage.`),A.requireDefined(this.commitId,`The commit must be set before putting coverage.`);let r=[];for(let e=t;e<=n;e++)r.push(e);return this.storage.putCoverage(this.appId,this.commitId,e,r),r.length}get sessionId(){return this.internalSessionId}},Te=class{server;storage;logger;totalNumMessagesReceived;totalNumCoverageMessagesReceived;constructor(e,t,n){A.require(e>0&&e<65536,`Port must be valid (range).`),this.storage=A.requireDefined(t),this.logger=A.requireDefined(n),this.server=new f({port:e}),this.totalNumMessagesReceived=0,this.totalNumCoverageMessagesReceived=0}start(){return this.logger.info(`Starting server on port ${this.server?.options.port}.`),this.server?.on(`connection`,(e,t)=>{let n=new we(this.storage);this.logger.trace(`Connection from: ${t.socket.remoteAddress}`),e.on(`close`,e=>{n&&(n=null,this.logger.trace(`Closing with code ${e}`))}),e.on(`message`,e=>{this.totalNumMessagesReceived+=1,n&&Buffer.isBuffer(e)&&this.handleMessage(n,e)}),e.on(`error`,e=>{this.logger.error(`Error on server socket triggered.`,e)})}),{stop:()=>{this.server?.clients.forEach(e=>e.close()),this.server?.close(),this.server=null}}}async handleMessage(e,t){try{let n=t.toString(`utf8`,0,1);if(n===`b`){let n=t.toString(`utf-8`,1).trim(),r=Buffer.from(n,`base64`).toString(`utf8`),i=JSON.parse(r);this.logger.debug(`Received bucket definition: ${JSON.stringify(i)} for session ${e.sessionId}.`),await e.setupBucketAndApplication(i)}else if(n===`c`){this.totalNumCoverageMessagesReceived+=1;let n=this.handleCoverageMessage(e,t.subarray(1));n>0&&this.logger.debug(`Coverage for approximately ${n} lines processed for session ${e.sessionId}.`)}else this.logger.error(`Unknown message type: ${n}`)}catch(e){this.logger.error(`Error while processing message starting with ${t.toString(`utf8`,0,Math.min(250,t.length))}`),this.logger.error(e.message)}}handleCoverageMessage(e,t){let n=0,r=t.toString().split(/[\n;]/).map(e=>e.trim()),i=``;return r.forEach(t=>{if(t.startsWith(`@`))i=t.substring(1).trim();else if(i){let r=t.split(/,|-/).map(e=>Number.parseInt(e));r.length===1?n+=e.putLineCoverage(i,r[0],r[0]):r.length===2?n+=e.putLineCoverage(i,r[0],r[1]):this.logger.error(`Invalid range token: ${t}`)}}),n}getStatistics(){return{totalMessages:this.totalNumMessagesReceived,totalCoverageMessages:this.totalNumCoverageMessagesReceived}}},Ee=class{collectorStopCallback;constructor(e,t,n){this.config=e,this.storage=t,this.logger=n}start(){if(!this.config.enableControlPort)return{async stop(){}};let e=p();e.use(p.text({})),e.use(p.urlencoded({extended:!0}));let t=e.listen(this.config.enableControlPort);return e.post(`/stop/`,(e,t)=>this.handleStopCollectorReset(e,t)),e.post(`/refresh/`,(e,t)=>this.handleRefreshConfigs(e,t)),e.post(`/dump/`,(e,t)=>this.handleDumpPost(e,t)),e.post(`/dump/:configId`,(e,t)=>this.handleDumpPostForConfig(e,t)),e.post(`/reset`,(e,t)=>this.handleGlobalCoverageReset(e,t)),e.post(`/reset/:configId`,(e,t)=>this.handleCoverageResetForConfig(e,t)),this.logger.info(`Control server enabled at port ${this.config.enableControlPort}.`),{async stop(){return new Promise(e=>{t.close(()=>e())})}}}async handleRefreshConfigs(e,t){this.logger.info(`Remote configuration refresh requested via the control API.`),await this.storage.refreshAllRemoteConfigurations(),this.logger.info(`Refresh done.`),t.sendStatus(200)}async handleDumpPost(e,t){this.logger.info(`Dumping coverage requested via the control API.`),await D.dumpCoverage(this.storage,this.logger),t.sendStatus(200)}async handleDumpPostForConfig(e,t){return this.handleConfigScopedRequest(e,t,async e=>{this.logger.info(`Dumping coverage requested for config '${e}' via the control API.`);let t=this.storage.getApplicationsWithConfig(e);for(let e of t)await D.dumpCoverage(this.storage,this.logger,e)})}async handleGlobalCoverageReset(e,t){this.storage.discardCollectedCoverage(),this.logger.info(`Discarding collected coverage information as requested via the control API.`),t.sendStatus(200)}async handleCoverageResetForConfig(e,t){return this.handleConfigScopedRequest(e,t,async e=>{this.logger.info(`Discarding collected coverage information for config '${e}' as requested via the control API.`);let t=this.storage.getApplicationsWithConfig(e);for(let e of t)this.storage.discardCollectedCoverage(e)})}async handleConfigScopedRequest(e,t,n){let r=e.params.configId;if(typeof r!=`string`||r.trim().length===0)throw Error(`Empty or undefined config ID`);try{await n(r),t.sendStatus(200)}catch(e){t.set(`Content-Type`,`text/plain`),t.send(`Failed to handle request for config '${r}': ${e.message}`),t.sendStatus(500)}}async handleStopCollectorReset(e,t){this.collectorStopCallback?(await this.collectorStopCallback(),this.logger.info(`Shutting down the collector as requested via the control API.`)):this.logger.error(`No collector stop callback was set. Cannot shut down the collector.`),t.status(200).end(),process.exit(0)}setCollectorStopCallback(e){this.collectorStopCallback=e}},De=`@teamscale/coverage-collector`,Oe=`1.0.
|
|
8
|
+
`)]);return r}getApplicationIDs(){return new Set(Array.from(this.coverageBuckets.values()).map(e=>e.appId))}getApplicationsWithConfig(e){return new Set(Array.from(this.coverageBuckets.values()).filter(t=>this.configIdPerApp.get(t.appId)===e).map(e=>e.appId))}discardCollectedCoverage(e){$?this.getAppBuckets(e).forEach(e=>e.clear()):this.coverageBuckets.clear()}addBeforeConfigUpdateCallback(e){this.beforeConfigUpdateCallbacks.push(e)}};function $(e,t){return`${e}-${t}`}function Se(e){return e.replace(/[^a-zA-Z0-9._-]/g,`_`)}function Ce(e){let t=e=>e.toString().padStart(2,`0`);return`${e.getFullYear()}-${t(e.getMonth()+1)}-${t(e.getDate())}-${t(e.getHours())}-${t(e.getMinutes())}-${t(e.getSeconds())}`}var we=class{internalSessionId;storage;appId;commitId;constructor(e){this.storage=A.requireDefined(e),this.internalSessionId=Math.random().toString(36).substring(2,15)+Math.random().toString(36).substring(2,15)}async setupBucketAndApplication(e){this.commitId=e.commit,this.appId=e.appId,await this.storage.setupCoverageBucket(e)}putLineCoverage(e,t,n){A.requireDefined(this.appId,`The application ID must be set before putting coverage.`),A.requireDefined(this.commitId,`The commit must be set before putting coverage.`);let r=[];for(let e=t;e<=n;e++)r.push(e);return this.storage.putCoverage(this.appId,this.commitId,e,r),r.length}get sessionId(){return this.internalSessionId}},Te=class{server;storage;logger;totalNumMessagesReceived;totalNumCoverageMessagesReceived;constructor(e,t,n){A.require(e>0&&e<65536,`Port must be valid (range).`),this.storage=A.requireDefined(t),this.logger=A.requireDefined(n),this.server=new f({port:e}),this.totalNumMessagesReceived=0,this.totalNumCoverageMessagesReceived=0}start(){return this.logger.info(`Starting server on port ${this.server?.options.port}.`),this.server?.on(`connection`,(e,t)=>{let n=new we(this.storage);this.logger.trace(`Connection from: ${t.socket.remoteAddress}`),e.on(`close`,e=>{n&&(n=null,this.logger.trace(`Closing with code ${e}`))}),e.on(`message`,e=>{this.totalNumMessagesReceived+=1,n&&Buffer.isBuffer(e)&&this.handleMessage(n,e)}),e.on(`error`,e=>{this.logger.error(`Error on server socket triggered.`,e)})}),{stop:()=>{this.server?.clients.forEach(e=>e.close()),this.server?.close(),this.server=null}}}async handleMessage(e,t){try{let n=t.toString(`utf8`,0,1);if(n===`b`){let n=t.toString(`utf-8`,1).trim(),r=Buffer.from(n,`base64`).toString(`utf8`),i=JSON.parse(r);this.logger.debug(`Received bucket definition: ${JSON.stringify(i)} for session ${e.sessionId}.`),await e.setupBucketAndApplication(i)}else if(n===`c`){this.totalNumCoverageMessagesReceived+=1;let n=this.handleCoverageMessage(e,t.subarray(1));n>0&&this.logger.debug(`Coverage for approximately ${n} lines processed for session ${e.sessionId}.`)}else this.logger.error(`Unknown message type: ${n}`)}catch(e){this.logger.error(`Error while processing message starting with ${t.toString(`utf8`,0,Math.min(250,t.length))}`),this.logger.error(e.message)}}handleCoverageMessage(e,t){let n=0,r=t.toString().split(/[\n;]/).map(e=>e.trim()),i=``;return r.forEach(t=>{if(t.startsWith(`@`))i=t.substring(1).trim();else if(i){let r=t.split(/,|-/).map(e=>Number.parseInt(e));r.length===1?n+=e.putLineCoverage(i,r[0],r[0]):r.length===2?n+=e.putLineCoverage(i,r[0],r[1]):this.logger.error(`Invalid range token: ${t}`)}}),n}getStatistics(){return{totalMessages:this.totalNumMessagesReceived,totalCoverageMessages:this.totalNumCoverageMessagesReceived}}},Ee=class{collectorStopCallback;constructor(e,t,n){this.config=e,this.storage=t,this.logger=n}start(){if(!this.config.enableControlPort)return{async stop(){}};let e=p();e.use(p.text({})),e.use(p.urlencoded({extended:!0}));let t=e.listen(this.config.enableControlPort);return e.post(`/stop/`,(e,t)=>this.handleStopCollectorReset(e,t)),e.post(`/refresh/`,(e,t)=>this.handleRefreshConfigs(e,t)),e.post(`/dump/`,(e,t)=>this.handleDumpPost(e,t)),e.post(`/dump/:configId`,(e,t)=>this.handleDumpPostForConfig(e,t)),e.post(`/reset`,(e,t)=>this.handleGlobalCoverageReset(e,t)),e.post(`/reset/:configId`,(e,t)=>this.handleCoverageResetForConfig(e,t)),this.logger.info(`Control server enabled at port ${this.config.enableControlPort}.`),{async stop(){return new Promise(e=>{t.close(()=>e())})}}}async handleRefreshConfigs(e,t){this.logger.info(`Remote configuration refresh requested via the control API.`),await this.storage.refreshAllRemoteConfigurations(),this.logger.info(`Refresh done.`),t.sendStatus(200)}async handleDumpPost(e,t){this.logger.info(`Dumping coverage requested via the control API.`),await D.dumpCoverage(this.storage,this.logger),t.sendStatus(200)}async handleDumpPostForConfig(e,t){return this.handleConfigScopedRequest(e,t,async e=>{this.logger.info(`Dumping coverage requested for config '${e}' via the control API.`);let t=this.storage.getApplicationsWithConfig(e);for(let e of t)await D.dumpCoverage(this.storage,this.logger,e)})}async handleGlobalCoverageReset(e,t){this.storage.discardCollectedCoverage(),this.logger.info(`Discarding collected coverage information as requested via the control API.`),t.sendStatus(200)}async handleCoverageResetForConfig(e,t){return this.handleConfigScopedRequest(e,t,async e=>{this.logger.info(`Discarding collected coverage information for config '${e}' as requested via the control API.`);let t=this.storage.getApplicationsWithConfig(e);for(let e of t)this.storage.discardCollectedCoverage(e)})}async handleConfigScopedRequest(e,t,n){let r=e.params.configId;if(typeof r!=`string`||r.trim().length===0)throw Error(`Empty or undefined config ID`);try{await n(r),t.sendStatus(200)}catch(e){t.set(`Content-Type`,`text/plain`),t.send(`Failed to handle request for config '${r}': ${e.message}`),t.sendStatus(500)}}async handleStopCollectorReset(e,t){this.collectorStopCallback?(await this.collectorStopCallback(),this.logger.info(`Shutting down the collector as requested via the control API.`)):this.logger.error(`No collector stop callback was set. Cannot shut down the collector.`),t.status(200).end(),process.exit(0)}setCollectorStopCallback(e){this.collectorStopCallback=e}},De=`@teamscale/coverage-collector`,Oe=`1.0.6`,ke=`Collector for JavaScript code coverage information`,Ae=class e{static buildLogger(e){let n=e.logToFile.trim();m.sync(d.dirname(n));let r=e.logLevel,i=o.createLogger({name:`Collector`,streams:[{level:r,stream:new ce,type:`raw`},{level:r,stream:new le(t.createWriteStream(n)),type:`raw`}]});return e.jsonLog&&i.addStream({level:r,path:`${n}.json`}),i}static async run(){let t=ve(),n=ge(t,{about:ke,version:Oe,name:De});return await e.runWithConfig(t,n)}static async runWithConfig(e,t){let n=this.buildLogger(t);n.info(`Starting collector in working directory "${s.cwd()}".`),n.info(`Will dump coverage to directory "${t.dumpFolder}".`),n.info(`Logging "${t.logLevel}" to "${t.logToFile}".`),t.teamscaleServerUrl&&(await C(t,n)||(n.error(`Could not connect to Teamscale with the given credential. Please check your configuration.`),s.exit(1))),t.dumpFolder&&je(d.resolve(t.dumpFolder),n);let r=ye();r.addArgumentCheck(e=>{if(t.teamscaleServerUrl&&e.teamscaleProject&&!e.teamscalePartition)return`The Teamscale project (parameter teamscaleProject) and coverage partition (parameter teamscalePartition) must be configured for an upload to Teamscale.`});let i=new xe(n,_e(e,r),r,t),a=new Te(t.port,i,n),o=new Ee(t,i,n),c=o.start(),l=a.start(),u=D.startRegularCollectorProcesses(i,n),f=this.startNoMessageTimer(n,a),p=async function(e=!1){n.info(`Stopping the collector.`),l.stop(),f.stop(),u.stop(),await new Promise(e=>{setTimeout(async()=>{await D.dumpCoverage(i,n),e(void 0)},0)}),e||await c.stop(),n.info(`Bye bye.`)};return o.setCollectorStopCallback(()=>p(!0)),{stop:p}}static startNoMessageTimer(e,t){let n=Date.now(),r=setInterval(async()=>{t.getStatistics().totalCoverageMessages===0?e.info(`No coverage received for ${((Date.now()-n)/1e3).toFixed(0)}s.`):clearInterval(r)},1e3*60);return{stop:()=>clearInterval(r)}}};function je(e,n){try{if(m.sync(e),!t.statSync(e).isDirectory())throw Error(`"${e}" exists but is not a directory.`);let n=d.join(e,`.write-test-${Date.now()}`);t.writeFileSync(n,`writable?`),t.unlinkSync(n)}catch(t){n.error(`The configured dump folder "${e}" is not writable or usable: ${t.message}`),s.exit(1)}}(async()=>{let e=await Ae.run(),t=async()=>{await e.stop(),c.exit(c.exitCode)};c.on(`SIGINT`,()=>t()),c.on(`SIGTERM`,()=>t())})().catch(e=>{e instanceof k?console.error(`Failed: ${e.message}`):console.error(`Failed: `,e),c.exit(1)});export{};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@teamscale/coverage-collector",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.6",
|
|
4
4
|
"description": "Collector for JavaScript code coverage information",
|
|
5
5
|
"main": "dist/main.mjs",
|
|
6
6
|
"bin": "dist/main.mjs",
|
|
@@ -20,18 +20,18 @@
|
|
|
20
20
|
"dist/**/*"
|
|
21
21
|
],
|
|
22
22
|
"dependencies": {
|
|
23
|
-
"async": "
|
|
24
|
-
"axios": "
|
|
25
|
-
"bunyan": "
|
|
26
|
-
"dotenv": "
|
|
27
|
-
"express": "
|
|
28
|
-
"form-data": "
|
|
29
|
-
"mkdirp": "
|
|
30
|
-
"node-cache": "
|
|
31
|
-
"source-map": "
|
|
32
|
-
"tmp": "
|
|
33
|
-
"ws": "
|
|
34
|
-
"@cqse/commons": "1.0.
|
|
23
|
+
"async": "3.2.6",
|
|
24
|
+
"axios": "1.13.5",
|
|
25
|
+
"bunyan": "1.8.15",
|
|
26
|
+
"dotenv": "17.2.3",
|
|
27
|
+
"express": "5.1.0",
|
|
28
|
+
"form-data": "4.0.4",
|
|
29
|
+
"mkdirp": "3.0.1",
|
|
30
|
+
"node-cache": "5.1.2",
|
|
31
|
+
"source-map": "0.7.6",
|
|
32
|
+
"tmp": "0.2.5",
|
|
33
|
+
"ws": "8.18.3",
|
|
34
|
+
"@cqse/commons": "1.0.6"
|
|
35
35
|
},
|
|
36
36
|
"publishConfig": {
|
|
37
37
|
"access": "public"
|