@teamscale/coverage-collector 1.0.0-beta.7 → 1.0.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/main.mjs +8 -0
- package/package.json +16 -8
- package/dist/package.json +0 -51
- package/dist/src/config/RemoteProfilerConfig.d.ts +0 -12
- package/dist/src/config/RemoteProfilerConfig.js +0 -59
- package/dist/src/control/App.d.ts +0 -11
- package/dist/src/control/App.js +0 -158
- package/dist/src/control/ControlServer.d.ts +0 -18
- package/dist/src/control/ControlServer.js +0 -93
- package/dist/src/control/CoverageDumper.d.ts +0 -10
- package/dist/src/control/CoverageDumper.js +0 -132
- package/dist/src/main.d.ts +0 -2
- package/dist/src/main.js +0 -26
- package/dist/src/receiver/CollectingServer.d.ts +0 -19
- package/dist/src/receiver/CollectingServer.js +0 -141
- package/dist/src/receiver/Session.d.ts +0 -11
- package/dist/src/receiver/Session.js +0 -33
- package/dist/src/storage/DataStorage.d.ts +0 -69
- package/dist/src/storage/DataStorage.js +0 -337
- package/dist/src/upload/ArtifactoryUpload.d.ts +0 -3
- package/dist/src/upload/ArtifactoryUpload.js +0 -60
- package/dist/src/upload/TeamscaleUpload.d.ts +0 -5
- package/dist/src/upload/TeamscaleUpload.js +0 -83
- package/dist/src/utils/PrettyFileLogger.d.ts +0 -8
- package/dist/src/utils/PrettyFileLogger.js +0 -21
- package/dist/src/utils/QueryParameters.d.ts +0 -3
- package/dist/src/utils/QueryParameters.js +0 -13
- package/dist/src/utils/RestApis.d.ts +0 -14
- package/dist/src/utils/RestApis.js +0 -111
- package/dist/src/utils/StdConsoleLogger.d.ts +0 -4
- package/dist/src/utils/StdConsoleLogger.js +0 -24
package/README.md
CHANGED
|
@@ -9,7 +9,7 @@ files in the [Teamscale Simple Coverage Format](https://docs.teamscale.com/refer
|
|
|
9
9
|
The Teamscale JavaScript Profiler consists of this Coverage Collector and the
|
|
10
10
|
[JavaScript Instrumenter](https://www.npmjs.com/package/@teamscale/javascript-instrumenter).
|
|
11
11
|
More details on using them (in combination) can be found
|
|
12
|
-
in the [Teamscale Documentation](https://docs.teamscale.com/howto/
|
|
12
|
+
in the [Teamscale Documentation](https://docs.teamscale.com/howto/setting-up-profiler-tga/javascript/).
|
|
13
13
|
|
|
14
14
|
The JavaScript Coverage Collector starts a server process that listens for
|
|
15
15
|
code coverage information from manually or automatically exercised (tested)
|
package/dist/main.mjs
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import e from"form-data";import*as t from"fs";import n from"fs";import r from"axios";import{inspect as i}from"util";import a from"url";import o from"bunyan";import"dotenv/config";import*as s from"node:process";import c from"node:process";import{createHash as l}from"node:crypto";import u from"node-cache";import d from"path";import{WebSocketServer as f}from"ws";import p from"express";import{mkdirp as m}from"mkdirp";const h=`v2025.2`;var g=class extends Error{};function _(t){let r=new e;return r.append(`report`,n.readFileSync(t),`coverage.simple`),r}async function v(e,t,n,r,i){return y(e,t,n,r,i)}async function y(e,t,n,a,o){try{o.debug(`Requesting via URL: `+e);let i;if(a===r.get)i=await r.get(e,n);else if(a===r.put)i=await r.put(e,t,n);else if(a===r.post)i=await r.post(e,t,n);else throw Error(`Unsupported request function: ${a}`);return o.debug(`Request finished with code ${i.status}.`),i.data}catch(t){if(r.isAxiosError(t)){let n;throw t.message&&o.error(`Request error ${t.status??`UNDEFINED`}: ${t.message}\n\tRequested URL: ${e}`),t.response?(o.error(`Request error response data:`,t.response.data),o.error(`Request error response status:`,t.response.status),o.debug(`Request error response headers:`,JSON.stringify(t.response.headers)),n=`Request failed with status ${t.response.status}: ${t.response.data??``}`):n=t.request?`No response received for the request. `+(t.message??t.code):`Request setup failed.`,new g(`Something went wrong when interacting with the API: ${n}`)}throw new g(`Something went wrong: ${i(t)}`)}}function ee(e,t){return b(e,{"Content-Type":`multipart/form-data; boundary=${t.getBoundary()}`})}function b(e,t){return{auth:{username:e.teamscaleUser??`no username provided`,password:e.teamscaleAccessToken??`no password provided`},headers:{Accept:`*/*`,...t},timeout:15e3,proxy:x(e)}}function x(e){let t=e.httpProxy;if(!t)return;let n=new a.URL(t),r={protocol:n.protocol.replace(`:`,``),host:n.hostname,port:+n.port};return n.username&&n.password&&(r.auth={username:n.username,password:n.password}),r}function S(e){return e?.replace(/\/$/,``)}var te=class extends URLSearchParams{addIfDefined(e,t){t?this.append(e,t):this.delete(e)}};async function C(e,t){if(e.teamscaleAccessToken===void 0)return t.error(`No valid access token provided for accessing the Teamscale server.`),!1;if(e.teamscaleUser===void 0)return t.error(`No valid user name provided for accessing the Teamscale server.`),!1;let n=`${S(e.teamscaleServerUrl)}/api/${h}/auth/login/access-key`;try{return await y(n,JSON.stringify({username:e.teamscaleUser,password:e.teamscaleAccessToken,stayLoggedIn:!1}),b(e,{"Content-Type":`application/json`}),r.post,t),!0}catch(e){return t.error(`Teamscale authentication failed.`,e),!1}}async function ne(e,t,n,r,i){if(!(e.teamscaleAccessToken&&e.teamscaleUser&&e.teamscaleServerUrl))throw new g(`API key and user name must be configured!`);if(!e.teamscaleProject&&!e.teamscalePartition)throw new g(`Teamscale project and partition must be configured!`);if(r===0)return!1;t.debug(`Preparing upload to Teamscale`);let a=_(n);return await re(e,ie(e,i),a,t),!0}async function re(e,t,n,i){await v(`${S(e.teamscaleServerUrl)}/api/${h}/projects/${e.teamscaleProject}/external-analysis/session/auto-create/report?${t.toString()}`,n,ee(e,n),r.post,i)}function ie(e,t){let n=new te;return n.addIfDefined(`format`,`SIMPLE`),n.addIfDefined(`message`,e.teamscaleMessage),n.addIfDefined(`repository`,e.teamscaleRepository),ae(t)?n.addIfDefined(`t`,t):n.addIfDefined(`revision`,t),n.addIfDefined(`partition`,e.teamscalePartition),n}function ae(e){let t=e.split(`:`);return t.length===2?t[1].toUpperCase()===`HEAD`?!0:/^\d+$/.test(t[1]):t.length===1?/^\d+$/.test(e):!1}async function oe(e,t,n,r,i){if(!(e.artifactoryAccessToken||e.artifactoryUser&&e.artifactoryPassword))throw new g(`API key or user name and password must be configured!`);return r===0?!1:(t.debug(`Preparing upload to Artifactory`),await w(e,_(n),t,i),!0)}async function w(e,t,n,i){let a=i.split(`:`);if(a.length!==2)throw new g(`The commit "${i}" does not contain a branch and timestamp; this is needed for the Artifactory upload. Please use the format "branch:timestamp".`);if(a[0].length===0)throw new g(`The branch of the commit "${i}" is empty. Please use the format "branch:timestamp".`);let o=`${S(e.artifactoryServerUrl)}/uploads/${a[0]}/${a[1]}`;o+=`/${e.teamscalePartition}/simple`,e.artifactoryPathSuffix!==void 0&&(o=`${o}/${e.artifactoryPathSuffix}`),o=`${o}/report.simple`,await v(o,t,T(e,t),r.put,n)}function T(e,t){let n=x(e);return e.artifactoryAccessToken?{headers:{Accept:`*/*`,"X-JFrog-Art-Api":e.artifactoryAccessToken,"Content-Type":`multipart/form-data; boundary=${t.getBoundary()}`},proxy:n}:{auth:{username:e.artifactoryUser??`no Artifactory username provided`,password:e.artifactoryPassword??`no Artifactory access token provided`},headers:{Accept:`*/*`,"Content-Type":`multipart/form-data; boundary=${t.getBoundary()}`},proxy:n}}const E=Date.now();var D=class{static startRegularCollectorProcesses(e,t){let n=new Map,r=new Map,i=async(e,n,r,i)=>{let a=n.get(e)??E;if(Date.now()-a>r){try{await i()}catch(e){t.error(`Regular collector action failed with an error: `+e?.message),t.error(e)}n.set(e,Date.now())}},a=setInterval(async()=>{for(let t of e.getApplicationIDs())await i(`config-update-${t}`,n,1e3*53,async()=>{await e.refreshApplicationConfiguration(t)});for(let n of e.getApplicationIDs()){let a=e.getAppConfiguration(n).dumpAfterMins??10;await i(`coverage-dump-${n}`,r,1e3*60*a,async()=>{await this.dumpCoverage(e,t,n)})}},1e3*31);return e.addBeforeConfigUpdateCallback(async(e,n)=>{t.info(`Dumping coverage before config update for application ${e}.`),await this.dumpCoverage(n,t,e)}),t.info(`Started the regular remote configuration refresh and coverage dump.`),{stop:()=>{a&&clearInterval(a)}}}static async dumpCoverage(e,t,n){let r=n?[n]:e.getApplicationIDs(),i=new Date,a=!1,o=!1;for(let n of r){let r=e.getAppConfiguration(n),s=this.determineCoverageTargetFolder(n,r),c=!(r.keepCoverageFiles??r.dumpToFolder!==void 0);try{let l=e.dumpToSimpleCoverageFile(s,i,n);a=l.hadCoverageToDump||a;for(let{simpleCoverageFile:e,simpleCoverageFileLines:i,commit:a}of l.details)try{(r.teamscaleServerUrl||r.artifactoryServerUrl)&&(o=await this.uploadCoverage(r,e,i,a,t,c)||o)}catch(e){e instanceof g?t.error(`Coverage upload failed for application ${n} and commit ${a}.\nThe coverage files on disk (inside the folder "${s}") were not deleted. You can still upload them manually. Error: ${e.message}`):t.error(`Coverage dump failed`,e)}}catch(e){t.error(`Dumping coverage failed for application ${n}`,e)}}o&&t.info(`Uploaded coverage for timestamp ${i.toISOString()} to a remote server.`),a||t.info(`Coverage dump request was processed successfully; no new coverage to dump found.`)}static async uploadCoverage(e,t,r,i,a,o){let s=!1;return e.teamscaleServerUrl&&(e.teamscaleProject??``).length>0&&(s=await ne(e,a,t,r,i)),e.artifactoryServerUrl&&(s||=await oe(e,a,t,r,i)),o&&n.unlinkSync(t),s}static determineCoverageTargetFolder(e,t){let r=`${t.dumpFolder}/${se(t.dumpToFolder)}${e}`;return n.mkdirSync(r,{recursive:!0}),r}};function se(e){return e===void 0||e.trim().length===0?``:(e=e.trim().replace(/^\/+/g,``),e=e.replace(/\\/g,`/`),e=e.replace(/[^a-zA-Z0-9\-_/]/g,`_`),e.endsWith(`/`)?e:`${e}/`)}var ce=class{write(e){let t;t=e.level===o.ERROR?console.error:e.level===o.WARN?console.warn:console.log,t.apply(this,[`[${e.time.toISOString()}] ${o.nameFromLevel[e.level].toUpperCase()}: ${e.msg}`])}},le=class{outputStream;constructor(e){this.outputStream=e}write(e){this.outputStream.write(`[${e.time.toISOString()}] ${o.nameFromLevel[e.level].toUpperCase()}: ${e.msg}\n`)}end(){this.outputStream.close()}};Error,Error;var O=class extends Error{},k=class extends Error{},A=class{static require(e,t){if(!e)throw new O(t)}static requireDefined(e,t){if(e||typeof e==`number`||typeof e==`boolean`||typeof e==`string`||e instanceof String)return e;throw t?new O(t):new O(`Reference must be defined.`)}static requireNonEmpty(e,t){if(this.requireDefined(e),e.length===0)throw new O(t);return e}static requireStringPattern(e,t,n){if(this.requireDefined(e),this.requireDefined(t),!e.match(t))throw new O(n);return e}};function ue(e,t){return t.startsWith(e)?t.substring(e.length):t}const j={order:999,title:`Application`};function de(e){if(e.type===`int`||e.type===`string`)return 1;if(e.type===`bool`)return 0;if(e.type===`string[]`)return 1/0;throw new k(`Unsupported parameter type "${e.type}" for parameter "${e.longParameter}".`)}function M(e){return e.replace(/^-+/,``).replace(/-([a-z])/g,(e,t)=>t.toUpperCase())}function fe(e){return e.replace(/^--/,``).toUpperCase().replace(/-/g,`_`)}var pe=class e{constructor(){this.parameters=new Map,this.shortToLongParameterMap=new Map,this.sequentialParameters=[],this.argumentChecks=[]}addParameter(e,t,n,r={}){let i=M(t),a=fe(t);this.describeParameter(i,{parameterId:i,longParameter:t,type:n,shortParameter:e,help:r.help??``,default:r.default,internal:r.internal??!1,group:r.group,environmentVariableName:a})}addArgumentCheck(e){this.argumentChecks.push(e)}addRequiredArgumentFor(e){let t=this.lookupParameter(e);if(t===void 0)throw new k(`Unknown parameter "${e}".`);if(t.default!==void 0)throw new k(`Parameter "${e}" is required, but has a default value.`);this.argumentChecks.push(n=>{if(n[e]===void 0)return`Missing required argument for parameter "${t.longParameter}"\n\tParameter description: ${t.help}`})}describeParameter(e,t){if(this.parameters.set(e,t),t.shortParameter){let n=t.shortParameter.replace(/^-/,``).trim().toLowerCase();if(n.length<1||n.length>3)throw new k(`Short parameter "${n}" is invalid.`);let r=this.shortToLongParameterMap.get(n);if(r!==void 0&&r!==e)throw new k(`Short parameter "${n}" is already mapped to parameter "${r}".`);this.shortToLongParameterMap.set(n,t.parameterId)}t.longParameter.startsWith(`--`)||this.sequentialParameters.push(e)}lookupParameter(e){let t=this.parameters.get(e);if(t===void 0){let n=this.shortToLongParameterMap.get(e);n!==void 0&&(t=this.parameters.get(n))}return t}getParameters(){return Array.from(this.parameters.values())}getArgumentChecks(){return this.argumentChecks}getSequentialParameters(){return this.sequentialParameters}clone(){let t=new e;for(let e of this.parameters.values())t.describeParameter(e.parameterId,e);for(let e of this.argumentChecks)t.addArgumentCheck(e);return t}addAllOf(e){for(let t of e.getParameters())this.describeParameter(t.parameterId,t);for(let t of e.getArgumentChecks())this.addArgumentCheck(t)}};function me(e,t){let n={},r,i=[],a=0;function o(){if(r){if(a<1/0&&a>0)throw new k(`Missing value for parameter "${r.longParameter}".`);if(r.type===`bool`)n[r.parameterId]=!0,r=void 0;else if(r.type===`string[]`){let e=n[r.parameterId],t=P(i.join(`,`));I(e)?n[r.parameterId]=e.concat(t):n[r.parameterId]=t}else{if(i.length!==1)throw new k(`Invalid value for parameter "${r.longParameter}": Expected a single value, got ${i.length}.`);n[r.parameterId]=F(i[0],r)}r=void 0,i=[]}}for(let n of e){if(n.startsWith(`-`)){o();let e=M(n);if(r=t.lookupParameter(e),!r)throw new k(`Unknown configuration parameter: ${n}`);a=de(r)}else{if(!r)throw new k(`Unexpected or unnamed argument, or unknown parameter '${n}' supplied. Please specify a parameter name (for example, --input) for each argument.`);i.push(n),a--}a===0&&o()}o();for(let e of t.getParameters())if(n[e.parameterId]===void 0){n[e.parameterId]=e.default;let t=c.env[e.environmentVariableName];t&&!e.disableEnvironmentVariableOverride&&(n[e.parameterId]=F(t,e))}return n}function N(e){return e=e.trim(),e.startsWith(`"`)&&e.endsWith(`"`)||e.startsWith(`'`)&&e.endsWith(`'`)||e.startsWith("`")&&e.endsWith("`")?e.substring(1,e.length-1):e}function P(e){return e?e.split(`,`).map(e=>N(e)):[]}function F(e,t){if(t.type===`int`){let n=parseInt(e,10);if(isNaN(n))throw new k(`Invalid value for parameter "${t.longParameter}": Expected an integer, got "${e}".`);return n}else if(t.type===`bool`){let n=e.toLowerCase();if(n===`true`||n===`1`)return!0;if(n===`false`||n===`0`)return!1;throw new k(`Invalid value for parameter "${t.longParameter}": Expected a boolean, got "${e}".`)}else if(t.type===`string`)return N(e);else if(t.type===`string[]`)return P(e);else throw new k(`Unsupported parameter type "${t.type}" for parameter "${t.longParameter}".`)}function I(e){return Array.isArray(e)&&e.every(e=>typeof e==`string`)}function L(e,t,n=!1){let r=[];t&&(r.push(t),r.push(``)),n||r.push(`Usage:`);let i=he(e);for(let e of i){e.groupTitle&&(r.push(``),r.push(`# ${e.groupTitle}`),r.push(``)),e.groupHint&&(r.push(R(e.groupHint,80)),r.push(``));for(let t of e.parameters){if(t.internal)continue;function e(){let e=t.environmentVariableName;return t.type===`bool`?``:t.type===`string[]`?` ${e}, .., ${e}`:` ${e}`}let i=[];n?i.push(`Parameter "${t.parameterId}" (${t.type})`):(t.shortParameter&&i.push(`${t.shortParameter}${e()}`),i.push(`${t.longParameter}${e()}`));let a=z(R(t.help,80)),o=t.default?`Default: ${t.default}`:void 0;r.push(` ${i.join(`, `)}`),a&&r.push(`\t${a}`),o&&r.push(`\t${o}`)}}r.push(``),console.log(r.join(`
|
|
3
|
+
`))}function R(e,t){e=e.replace(/\n/g,` `);let n=e.split(` `),r=[],i=``;for(let e of n)i.length+e.length+1>t&&(r.push(i),i=``),i.length>0&&(i+=` `),i+=e;return i.length>0&&r.push(i),r.join(`
|
|
4
|
+
`)}function z(e){if(e)return e.replace(/\n/g,`
|
|
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
|
+
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
|
+
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.5`,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,13 +1,21 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@teamscale/coverage-collector",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.5",
|
|
4
4
|
"description": "Collector for JavaScript code coverage information",
|
|
5
|
-
"main": "dist/
|
|
6
|
-
"bin": "dist/
|
|
7
|
-
"types": "dist/src/main.d.ts",
|
|
5
|
+
"main": "dist/main.mjs",
|
|
6
|
+
"bin": "dist/main.mjs",
|
|
8
7
|
"author": "CQSE GmbH",
|
|
9
8
|
"license": "Apache-2.0",
|
|
10
9
|
"homepage": "https://docs.teamscale.com/howto/setting-up-profiler-tga/javascript/",
|
|
10
|
+
"keywords": [
|
|
11
|
+
"javascript",
|
|
12
|
+
"coverage",
|
|
13
|
+
"test coverage",
|
|
14
|
+
"coverage profiler",
|
|
15
|
+
"testing",
|
|
16
|
+
"coverage collector",
|
|
17
|
+
"teamscale"
|
|
18
|
+
],
|
|
11
19
|
"files": [
|
|
12
20
|
"dist/**/*"
|
|
13
21
|
],
|
|
@@ -23,15 +31,15 @@
|
|
|
23
31
|
"source-map": "^0.7.6",
|
|
24
32
|
"tmp": "^0.2.5",
|
|
25
33
|
"ws": "^8.18.3",
|
|
26
|
-
"@cqse/commons": "1.0.
|
|
34
|
+
"@cqse/commons": "1.0.5"
|
|
27
35
|
},
|
|
28
36
|
"publishConfig": {
|
|
29
37
|
"access": "public"
|
|
30
38
|
},
|
|
31
39
|
"scripts": {
|
|
32
|
-
"clean": "rimraf dist
|
|
33
|
-
"build": "tsc",
|
|
34
|
-
"collector": "node dist/
|
|
40
|
+
"clean": "rimraf dist",
|
|
41
|
+
"build": "pnpm tsc --noEmit && pnpm tsdown",
|
|
42
|
+
"collector": "node dist/main.mjs",
|
|
35
43
|
"test": "pnpm build && node --import tsx --test --test-concurrency=1 test/**/*.test.ts"
|
|
36
44
|
}
|
|
37
45
|
}
|
package/dist/package.json
DELETED
|
@@ -1,51 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "@teamscale/coverage-collector",
|
|
3
|
-
"version": "1.0.0-beta.7",
|
|
4
|
-
"description": "Collector for JavaScript code coverage information",
|
|
5
|
-
"main": "dist/src/main.js",
|
|
6
|
-
"bin": "dist/src/main.js",
|
|
7
|
-
"types": "dist/src/main.d.ts",
|
|
8
|
-
"author": "CQSE GmbH",
|
|
9
|
-
"license": "Apache-2.0",
|
|
10
|
-
"homepage": "https://docs.teamscale.com/howto/setting-up-profiler-tga/javascript/",
|
|
11
|
-
"scripts": {
|
|
12
|
-
"prepublishOnly": "pnpm clean && pnpm build",
|
|
13
|
-
"clean": "rimraf dist tsconfig.tsbuildinfo",
|
|
14
|
-
"build": "tsc",
|
|
15
|
-
"collector": "node dist/src/main.js",
|
|
16
|
-
"test": "pnpm build && node --import tsx --test --test-concurrency=1 test/**/*.test.ts"
|
|
17
|
-
},
|
|
18
|
-
"files": [
|
|
19
|
-
"dist/**/*"
|
|
20
|
-
],
|
|
21
|
-
"dependencies": {
|
|
22
|
-
"@cqse/commons": "workspace:../cqse-commons",
|
|
23
|
-
"async": "^3.2.6",
|
|
24
|
-
"axios": "^1.12.2",
|
|
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
|
-
},
|
|
35
|
-
"devDependencies": {
|
|
36
|
-
"@types/async": "^3.2.25",
|
|
37
|
-
"@types/bunyan": "^1.8.11",
|
|
38
|
-
"@types/express": "^5.0.4",
|
|
39
|
-
"@types/node": "^22.18.12",
|
|
40
|
-
"@types/tmp": "^0.2.6",
|
|
41
|
-
"@types/ws": "^8.18.1",
|
|
42
|
-
"esbuild": "^0.25.11",
|
|
43
|
-
"mockttp": "3.17.1",
|
|
44
|
-
"rimraf": "^6.0.1",
|
|
45
|
-
"tsx": "^4.19.2",
|
|
46
|
-
"typescript": "^5.8.3"
|
|
47
|
-
},
|
|
48
|
-
"publishConfig": {
|
|
49
|
-
"access": "public"
|
|
50
|
-
}
|
|
51
|
-
}
|
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
import { CollectorOptions } from '@cqse/commons';
|
|
2
|
-
import Logger from 'bunyan';
|
|
3
|
-
type ProfilerConfiguration = {
|
|
4
|
-
configurationId: string;
|
|
5
|
-
configurationOptions?: string;
|
|
6
|
-
};
|
|
7
|
-
export declare class RemoteProfilerConfig {
|
|
8
|
-
static queryConfiguration(baseConfig: CollectorOptions, configId: string, logger: Logger): Promise<Map<string, string>>;
|
|
9
|
-
static parseConfigurationStringIntoMap(response: ProfilerConfiguration): Map<string, string>;
|
|
10
|
-
}
|
|
11
|
-
export declare function clearConfigurationCache(): void;
|
|
12
|
-
export {};
|
|
@@ -1,59 +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.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
|
-
const CONFIG_CACHE = new node_cache_1.default({ stdTTL: 60, deleteOnExpire: true });
|
|
13
|
-
class RemoteProfilerConfig {
|
|
14
|
-
static async queryConfiguration(baseConfig, configId, logger) {
|
|
15
|
-
if (!baseConfig.teamscaleServerUrl || !baseConfig.teamscaleAccessToken || !baseConfig.teamscaleUser) {
|
|
16
|
-
throw new Error("Access to Teamscale is not configured. Receiving profiler configurations is not possible. " +
|
|
17
|
-
"Please specify configuration arguments for --teamscale-server-url, --teamscale-user, and --teamscale-access-token");
|
|
18
|
-
}
|
|
19
|
-
let result = CONFIG_CACHE.get(configId);
|
|
20
|
-
try {
|
|
21
|
-
if (result) {
|
|
22
|
-
logger.debug('Using cached configuration with id ' + configId);
|
|
23
|
-
return result;
|
|
24
|
-
}
|
|
25
|
-
logger.debug(`Requesting configuration with ID ${configId} from Teamscale.`);
|
|
26
|
-
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);
|
|
27
|
-
if (response === undefined) {
|
|
28
|
-
throw new commons_1.InvalidConfigurationException(`No configuration found with ID ${configId}`);
|
|
29
|
-
}
|
|
30
|
-
if (!response.configurationOptions || response.configurationOptions.trim().length === 0) {
|
|
31
|
-
throw new commons_1.InvalidConfigurationException(`Configuration with ID "${configId}" is empty. Please specify relevant configuration options.`);
|
|
32
|
-
}
|
|
33
|
-
result = this.parseConfigurationStringIntoMap(response);
|
|
34
|
-
CONFIG_CACHE.set(configId, result);
|
|
35
|
-
}
|
|
36
|
-
catch (error) {
|
|
37
|
-
throw new Error(`Failed to retrieve configuration with ID ${configId}: ${error.message}`, { cause: error });
|
|
38
|
-
}
|
|
39
|
-
return result;
|
|
40
|
-
}
|
|
41
|
-
static parseConfigurationStringIntoMap(response) {
|
|
42
|
-
const result = new Map();
|
|
43
|
-
(response.configurationOptions ?? "").split(/[\r\n]+/).map(line => line.trim())
|
|
44
|
-
.filter(line => line.length > 0)
|
|
45
|
-
.filter(line => !line.startsWith('#'))
|
|
46
|
-
.forEach(line => {
|
|
47
|
-
const split = line.split('=');
|
|
48
|
-
if (split.length !== 2) {
|
|
49
|
-
throw new commons_1.InvalidConfigurationException(`Invalid configuration line; expecting a valid key=value pair: ${line}`);
|
|
50
|
-
}
|
|
51
|
-
result.set(split[0].trim(), split[1].trim());
|
|
52
|
-
});
|
|
53
|
-
return result;
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
exports.RemoteProfilerConfig = RemoteProfilerConfig;
|
|
57
|
-
function clearConfigurationCache() {
|
|
58
|
-
CONFIG_CACHE.flushAll();
|
|
59
|
-
}
|
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
import { ConfigurationParameters, StaticCollectorOptions } from '@cqse/commons';
|
|
2
|
-
import 'dotenv/config';
|
|
3
|
-
export type Stoppable = {
|
|
4
|
-
stop: () => Promise<void>;
|
|
5
|
-
};
|
|
6
|
-
export declare class App {
|
|
7
|
-
private static buildLogger;
|
|
8
|
-
static run(): Promise<Stoppable>;
|
|
9
|
-
static runWithConfig(staticParameters: ConfigurationParameters, config: StaticCollectorOptions): Promise<Stoppable>;
|
|
10
|
-
private static startNoMessageTimer;
|
|
11
|
-
}
|
package/dist/src/control/App.js
DELETED
|
@@ -1,158 +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
|
-
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
|
-
class App {
|
|
56
|
-
static buildLogger(config) {
|
|
57
|
-
const logfilePath = config.logToFile.trim();
|
|
58
|
-
mkdirp_1.mkdirp.sync(path_1.default.dirname(logfilePath));
|
|
59
|
-
const logLevel = config.logLevel;
|
|
60
|
-
const logger = bunyan_1.default.createLogger({
|
|
61
|
-
name: 'Collector',
|
|
62
|
-
streams: [
|
|
63
|
-
{ level: logLevel, stream: new StdConsoleLogger_1.StdConsoleLogger(), type: 'raw' },
|
|
64
|
-
{ level: logLevel, stream: new PrettyFileLogger_1.PrettyFileLogger(fs.createWriteStream(logfilePath)), type: 'raw' }
|
|
65
|
-
]
|
|
66
|
-
});
|
|
67
|
-
if (config.jsonLog) {
|
|
68
|
-
logger.addStream({ level: logLevel, path: `${logfilePath}.json` });
|
|
69
|
-
}
|
|
70
|
-
return logger;
|
|
71
|
-
}
|
|
72
|
-
static async run() {
|
|
73
|
-
const configParameters = (0, commons_1.buildStaticCollectorParameters)();
|
|
74
|
-
const appInfos = { about: package_json_1.description, version: package_json_1.version, name: package_json_1.name };
|
|
75
|
-
const config = (0, commons_1.processCommandLine)(configParameters, appInfos);
|
|
76
|
-
return await App.runWithConfig(configParameters, config);
|
|
77
|
-
}
|
|
78
|
-
static async runWithConfig(staticParameters, config) {
|
|
79
|
-
const logger = this.buildLogger(config);
|
|
80
|
-
logger.info(`Starting collector in working directory "${process.cwd()}".`);
|
|
81
|
-
logger.info(`Will dump coverage to directory "${config.dumpFolder}".`);
|
|
82
|
-
logger.info(`Logging "${config.logLevel}" to "${config.logToFile}".`);
|
|
83
|
-
if (config.teamscaleServerUrl) {
|
|
84
|
-
const checkResult = await (0, TeamscaleUpload_1.checkTeamscaleCredentials)(config, logger);
|
|
85
|
-
if (!checkResult) {
|
|
86
|
-
logger.error('Could not connect to Teamscale with the given credential. Please check your configuration.');
|
|
87
|
-
process.exit(1);
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
|
-
if (config.dumpFolder) {
|
|
91
|
-
const dumpFolder = path_1.default.resolve(config.dumpFolder);
|
|
92
|
-
ensureWritableFolder(dumpFolder, logger);
|
|
93
|
-
}
|
|
94
|
-
const reconfigurableParameters = (0, commons_1.buildReconfigurableCollectorParameters)();
|
|
95
|
-
reconfigurableParameters.addArgumentCheck((options) => {
|
|
96
|
-
if (config.teamscaleServerUrl) {
|
|
97
|
-
if (options.teamscaleProject && !options.teamscalePartition) {
|
|
98
|
-
return 'The Teamscale project (parameter teamscaleProject) and coverage partition (parameter teamscalePartition) ' +
|
|
99
|
-
'must be configured for an upload to Teamscale.';
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
});
|
|
103
|
-
const storage = new DataStorage_1.DataStorage(logger, (0, commons_1.parameterUnion)(staticParameters, reconfigurableParameters), reconfigurableParameters, config);
|
|
104
|
-
const server = new CollectingServer_1.WebSocketCollectingServer(config.port, storage, logger);
|
|
105
|
-
const controlServer = new ControlServer_1.ControlServer(config, storage, logger);
|
|
106
|
-
const controlServerState = controlServer.start();
|
|
107
|
-
const serverState = server.start();
|
|
108
|
-
const dumpTimerState = CoverageDumper_1.CoverageDumper.startRegularCollectorProcesses(storage, logger);
|
|
109
|
-
const statsTimerState = this.startNoMessageTimer(logger, server);
|
|
110
|
-
const stop = async function () {
|
|
111
|
-
logger.info('Stopping the collector.');
|
|
112
|
-
await new Promise(resolve => {
|
|
113
|
-
setTimeout(async () => {
|
|
114
|
-
await CoverageDumper_1.CoverageDumper.dumpCoverage(storage, logger);
|
|
115
|
-
resolve(undefined);
|
|
116
|
-
}, 0);
|
|
117
|
-
});
|
|
118
|
-
dumpTimerState.stop();
|
|
119
|
-
statsTimerState.stop();
|
|
120
|
-
await controlServerState.stop();
|
|
121
|
-
serverState.stop();
|
|
122
|
-
logger.info('Bye bye.');
|
|
123
|
-
};
|
|
124
|
-
return { stop };
|
|
125
|
-
}
|
|
126
|
-
static startNoMessageTimer(logger, server) {
|
|
127
|
-
const startTime = Date.now();
|
|
128
|
-
const timer = setInterval(async () => {
|
|
129
|
-
const stats = server.getStatistics();
|
|
130
|
-
if (stats.totalCoverageMessages === 0) {
|
|
131
|
-
logger.info(`No coverage received for ${((Date.now() - startTime) / 1000.0).toFixed(0)}s.`);
|
|
132
|
-
}
|
|
133
|
-
else {
|
|
134
|
-
clearInterval(timer);
|
|
135
|
-
}
|
|
136
|
-
}, 1000 * 60);
|
|
137
|
-
return {
|
|
138
|
-
stop: () => clearInterval(timer)
|
|
139
|
-
};
|
|
140
|
-
}
|
|
141
|
-
}
|
|
142
|
-
exports.App = App;
|
|
143
|
-
function ensureWritableFolder(dumpFolder, logger) {
|
|
144
|
-
try {
|
|
145
|
-
mkdirp_1.mkdirp.sync(dumpFolder);
|
|
146
|
-
const stat = fs.statSync(dumpFolder);
|
|
147
|
-
if (!stat.isDirectory()) {
|
|
148
|
-
throw new Error(`"${dumpFolder}" exists but is not a directory.`);
|
|
149
|
-
}
|
|
150
|
-
const testFile = path_1.default.join(dumpFolder, `.write-test-${Date.now()}`);
|
|
151
|
-
fs.writeFileSync(testFile, 'writable?');
|
|
152
|
-
fs.unlinkSync(testFile);
|
|
153
|
-
}
|
|
154
|
-
catch (err) {
|
|
155
|
-
logger.error(`The configured dump folder "${dumpFolder}" is not writable or usable: ${err.message}`);
|
|
156
|
-
process.exit(1);
|
|
157
|
-
}
|
|
158
|
-
}
|
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
import { DataStorage } from '../storage/DataStorage';
|
|
2
|
-
import Logger from 'bunyan';
|
|
3
|
-
import { CollectorOptions } from '@cqse/commons';
|
|
4
|
-
export declare class ControlServer {
|
|
5
|
-
private config;
|
|
6
|
-
private storage;
|
|
7
|
-
private logger;
|
|
8
|
-
constructor(config: CollectorOptions, storage: DataStorage, logger: Logger);
|
|
9
|
-
start(): {
|
|
10
|
-
stop: () => Promise<void>;
|
|
11
|
-
};
|
|
12
|
-
private handleRefreshConfigs;
|
|
13
|
-
private handleDumpPost;
|
|
14
|
-
private handleDumpPostForConfig;
|
|
15
|
-
private handleGlobalCoverageReset;
|
|
16
|
-
private handleCoverageResetForConfig;
|
|
17
|
-
private handleConfigScopedRequest;
|
|
18
|
-
}
|
|
@@ -1,93 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
-
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
-
};
|
|
5
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
-
exports.ControlServer = void 0;
|
|
7
|
-
const CoverageDumper_1 = require("../control/CoverageDumper");
|
|
8
|
-
const express_1 = __importDefault(require("express"));
|
|
9
|
-
class ControlServer {
|
|
10
|
-
config;
|
|
11
|
-
storage;
|
|
12
|
-
logger;
|
|
13
|
-
constructor(config, storage, logger) {
|
|
14
|
-
this.config = config;
|
|
15
|
-
this.storage = storage;
|
|
16
|
-
this.logger = logger;
|
|
17
|
-
}
|
|
18
|
-
start() {
|
|
19
|
-
if (!this.config.enableControlPort) {
|
|
20
|
-
return {
|
|
21
|
-
async stop() {
|
|
22
|
-
}
|
|
23
|
-
};
|
|
24
|
-
}
|
|
25
|
-
const controlServer = (0, express_1.default)();
|
|
26
|
-
controlServer.use(express_1.default.text({}));
|
|
27
|
-
controlServer.use(express_1.default.urlencoded({ extended: true }));
|
|
28
|
-
const serverSocket = controlServer.listen(this.config.enableControlPort);
|
|
29
|
-
controlServer.post('/refresh/', (request, response) => this.handleRefreshConfigs(request, response));
|
|
30
|
-
controlServer.post('/dump/', (request, response) => this.handleDumpPost(request, response));
|
|
31
|
-
controlServer.post('/dump/:configId', (request, response) => this.handleDumpPostForConfig(request, response));
|
|
32
|
-
controlServer.post('/reset', (request, response) => this.handleGlobalCoverageReset(request, response));
|
|
33
|
-
controlServer.post('/reset/:configId', (request, response) => this.handleCoverageResetForConfig(request, response));
|
|
34
|
-
this.logger.info(`Control server enabled at port ${this.config.enableControlPort}.`);
|
|
35
|
-
return {
|
|
36
|
-
async stop() {
|
|
37
|
-
return new Promise(resolve => {
|
|
38
|
-
serverSocket.close(() => resolve());
|
|
39
|
-
});
|
|
40
|
-
}
|
|
41
|
-
};
|
|
42
|
-
}
|
|
43
|
-
async handleRefreshConfigs(request, response) {
|
|
44
|
-
this.logger.info('Remote configuration refresh requested via the control API.');
|
|
45
|
-
await this.storage.refreshAllRemoteConfigurations();
|
|
46
|
-
this.logger.info('Refresh done.');
|
|
47
|
-
response.sendStatus(200);
|
|
48
|
-
}
|
|
49
|
-
async handleDumpPost(request, response) {
|
|
50
|
-
this.logger.info('Dumping coverage requested via the control API.');
|
|
51
|
-
await CoverageDumper_1.CoverageDumper.dumpCoverage(this.storage, this.logger);
|
|
52
|
-
response.sendStatus(200);
|
|
53
|
-
}
|
|
54
|
-
async handleDumpPostForConfig(request, response) {
|
|
55
|
-
return this.handleConfigScopedRequest(request, response, async (configId) => {
|
|
56
|
-
this.logger.info(`Dumping coverage requested for config '${configId}' via the control API.`);
|
|
57
|
-
const appsWithConfig = this.storage.getApplicationsWithConfig(configId);
|
|
58
|
-
for (const appId of appsWithConfig) {
|
|
59
|
-
await CoverageDumper_1.CoverageDumper.dumpCoverage(this.storage, this.logger, appId);
|
|
60
|
-
}
|
|
61
|
-
});
|
|
62
|
-
}
|
|
63
|
-
async handleGlobalCoverageReset(request, response) {
|
|
64
|
-
this.storage.discardCollectedCoverage();
|
|
65
|
-
this.logger.info(`Discarding collected coverage information as requested via the control API.`);
|
|
66
|
-
response.sendStatus(200);
|
|
67
|
-
}
|
|
68
|
-
async handleCoverageResetForConfig(request, response) {
|
|
69
|
-
return this.handleConfigScopedRequest(request, response, async (configId) => {
|
|
70
|
-
this.logger.info(`Discarding collected coverage information for config '${configId}' as requested via the control API.`);
|
|
71
|
-
const appsWithConfig = this.storage.getApplicationsWithConfig(configId);
|
|
72
|
-
for (const appId of appsWithConfig) {
|
|
73
|
-
this.storage.discardCollectedCoverage(appId);
|
|
74
|
-
}
|
|
75
|
-
});
|
|
76
|
-
}
|
|
77
|
-
async handleConfigScopedRequest(request, response, configHandler) {
|
|
78
|
-
const configId = request.params.configId;
|
|
79
|
-
if (!configId) {
|
|
80
|
-
throw new Error('Invalid config ID');
|
|
81
|
-
}
|
|
82
|
-
try {
|
|
83
|
-
await configHandler(configId);
|
|
84
|
-
response.sendStatus(200);
|
|
85
|
-
}
|
|
86
|
-
catch (error) {
|
|
87
|
-
response.set('Content-Type', 'text/plain');
|
|
88
|
-
response.send(`Failed to handle request for config '${configId}': ${error.message}`);
|
|
89
|
-
response.sendStatus(500);
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
exports.ControlServer = ControlServer;
|
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
import { DataStorage } from '../storage/DataStorage';
|
|
2
|
-
import Logger from 'bunyan';
|
|
3
|
-
export declare class CoverageDumper {
|
|
4
|
-
static startRegularCollectorProcesses(storage: DataStorage, logger: Logger): {
|
|
5
|
-
stop: () => void;
|
|
6
|
-
};
|
|
7
|
-
static dumpCoverage(storage: DataStorage, logger: Logger, onlyForAppId?: string): Promise<void>;
|
|
8
|
-
private static uploadCoverage;
|
|
9
|
-
private static determineCoverageTargetFolder;
|
|
10
|
-
}
|