From 6754bc79272f016d4fce14db188af46042bf4480 Mon Sep 17 00:00:00 2001 From: Andrey Sobolev Date: Sun, 28 Sep 2025 23:04:46 +0700 Subject: [PATCH] Fixes + coverage --- common/config/rush/command-line.json | 29 ++-- packages/backrpc/jest.config.js | 4 +- packages/backrpc/package.json | 4 +- packages/client/jest.config.js | 4 +- packages/client/package.json | 4 +- packages/client/src/__tests__/client.spec.ts | 59 +++++++ .../src/__tests__/containerConnection.spec.ts | 48 ++++++ .../client/src/__tests__/establish.spec.ts | 106 ++++++++++++ packages/core/jest.config.js | 4 +- packages/core/package.json | 4 +- packages/core/src/network.ts | 23 ++- packages/server/jest.config.js | 4 +- packages/server/package.json | 4 +- pods/network-pod/jest.config.js | 2 +- pods/network-pod/package.json | 4 +- pods/network-tool/jest.config.js | 2 +- pods/network-tool/package.json | 4 +- scripts/generate-coverage-html.js | 153 ++++++++++++++++++ scripts/merge-coverage.js | 106 ++++++++++++ tests/package.json | 2 +- 20 files changed, 515 insertions(+), 55 deletions(-) create mode 100644 packages/client/src/__tests__/client.spec.ts create mode 100644 packages/client/src/__tests__/containerConnection.spec.ts create mode 100644 packages/client/src/__tests__/establish.spec.ts create mode 100644 scripts/generate-coverage-html.js create mode 100644 scripts/merge-coverage.js diff --git a/common/config/rush/command-line.json b/common/config/rush/command-line.json index c356ff1d2a..bb9944e977 100644 --- a/common/config/rush/command-line.json +++ b/common/config/rush/command-line.json @@ -92,6 +92,14 @@ } ], "commands": [ + { + "commandKind": "global", + "name": "coverage", + "summary": "Run tests, merge LCOV and generate HTML coverage", + "description": "Run 'rush test', then merge per-package LCOV files and generate HTML coverage in coverage/html", + "safeForSimultaneousRushProcesses": true, + "shellCommand": "rush test && node scripts/merge-coverage.js && node scripts/generate-coverage-html.js coverage/lcov.info coverage/html" + }, { "commandKind": "bulk", "name": "format", @@ -207,12 +215,7 @@ "summary": "docker:build", "commandKind": "phased", "name": "docker:build", - "phases": [ - "_phase:build", - "_phase:bundle", - "_phase:package", - "_phase:docker-build" - ], + "phases": ["_phase:build", "_phase:bundle", "_phase:package", "_phase:docker-build"], "enableParallelism": true, "incremental": true }, @@ -220,12 +223,7 @@ "summary": "docker:rebuild", "commandKind": "phased", "name": "docker:rebuild", - "phases": [ - "_phase:build", - "_phase:bundle", - "_phase:package", - "_phase:docker-build" - ], + "phases": ["_phase:build", "_phase:bundle", "_phase:package", "_phase:docker-build"], "enableParallelism": true, "incremental": false }, @@ -233,12 +231,7 @@ "summary": "docker:staging", "commandKind": "phased", "name": "docker:staging", - "phases": [ - "_phase:build", - "_phase:bundle", - "_phase:package", - "_phase:docker-staging" - ], + "phases": ["_phase:build", "_phase:bundle", "_phase:package", "_phase:docker-staging"], "enableParallelism": true, "incremental": true }, diff --git a/packages/backrpc/jest.config.js b/packages/backrpc/jest.config.js index 2cfd408b67..069ea8aa0d 100644 --- a/packages/backrpc/jest.config.js +++ b/packages/backrpc/jest.config.js @@ -2,6 +2,6 @@ module.exports = { preset: 'ts-jest', testEnvironment: 'node', testMatch: ['**/?(*.)+(spec|test).[jt]s?(x)'], - roots: ["./src"], - coverageReporters: ["text-summary", "html"] + roots: ['./src'], + coverageReporters: ['text-summary', 'html', 'lcov'] } diff --git a/packages/backrpc/package.json b/packages/backrpc/package.json index 4ff27a14a7..c1592070f7 100644 --- a/packages/backrpc/package.json +++ b/packages/backrpc/package.json @@ -20,10 +20,10 @@ "build": "compile", "prepublishOnly": "npm run build", "build:watch": "compile", - "test": "jest --passWithNoTests --silent --forceExit", + "test": "jest --coverage --coverageDirectory=./coverage --passWithNoTests --forceExit", "format": "format src", "_phase:build": "compile transpile src", - "_phase:test": "jest --passWithNoTests --silent --forceExit", + "_phase:test": "jest --coverage --coverageDirectory=./coverage --passWithNoTests --forceExit", "_phase:format": "format src", "_phase:validate": "compile validate" }, diff --git a/packages/client/jest.config.js b/packages/client/jest.config.js index 2cfd408b67..069ea8aa0d 100644 --- a/packages/client/jest.config.js +++ b/packages/client/jest.config.js @@ -2,6 +2,6 @@ module.exports = { preset: 'ts-jest', testEnvironment: 'node', testMatch: ['**/?(*.)+(spec|test).[jt]s?(x)'], - roots: ["./src"], - coverageReporters: ["text-summary", "html"] + roots: ['./src'], + coverageReporters: ['text-summary', 'html', 'lcov'] } diff --git a/packages/client/package.json b/packages/client/package.json index 696a36192c..36c988fac6 100644 --- a/packages/client/package.json +++ b/packages/client/package.json @@ -20,10 +20,10 @@ "build": "compile", "prepublishOnly": "npm run build", "build:watch": "compile", - "test": "jest --passWithNoTests --silent --forceExit", + "test": "jest --coverage --coverageDirectory=./coverage --passWithNoTests --forceExit", "format": "format src", "_phase:build": "compile transpile src", - "_phase:test": "jest --passWithNoTests --silent --forceExit", + "_phase:test": "jest --coverage --coverageDirectory=./coverage --passWithNoTests --forceExit", "_phase:format": "format src", "_phase:validate": "compile validate" }, diff --git a/packages/client/src/__tests__/client.spec.ts b/packages/client/src/__tests__/client.spec.ts new file mode 100644 index 0000000000..a559f01863 --- /dev/null +++ b/packages/client/src/__tests__/client.spec.ts @@ -0,0 +1,59 @@ +import type { ContainerEndpointRef, ContainerUuid, GetOptions } from '@hcengineering/network-core' +import { NetworkClientImpl } from '../client' + +// Mock BackRPCClient used inside NetworkClientImpl +jest.mock('@hcengineering/network-backrpc', () => { + return { + BackRPCClient: jest.fn().mockImplementation(() => ({ + waitConnection: jest.fn().mockResolvedValue(undefined), + request: jest.fn().mockResolvedValue(undefined), + close: jest.fn() + })) + } +}) + +// Minimal TickManager mock +const tickMgr = { + tps: 1, + now: () => Date.now(), + waitTick: (n: number) => new Promise((resolve) => setTimeout(resolve, 0)), + register: (_fn: any, _interval: number) => { + return () => {} + } +} + +describe('NetworkClientImpl basic behaviors', () => { + test('onUpdate registers and unregisters listener', () => { + const client = new NetworkClientImpl('localhost', 3000, tickMgr as any) + const unsub = client.onUpdate(async () => { + // noop + }) + // There should be one listener registered + expect(client.containerListeners.size).toBe(1) + unsub() + expect(client.containerListeners.size).toBe(0) + }) + + test('get returns existing reference when uuid provided', async () => { + const client = new NetworkClientImpl('localhost', 3000, tickMgr as any) + const uuid = '00000000-0000-0000-0000-000000000001' as ContainerUuid + // Insert a fake ContainerReferenceImpl-like object + const fakeRef: any = { + uuid, + endpoint: 'endpoint', + close: jest.fn(), + request: jest.fn(), + connect: jest.fn() + } + const entry: { kind: any, ref: any, request: GetOptions, endpoint: ContainerEndpointRef } = { + kind: 'test' as any, + ref: fakeRef, + request: {} as any, + endpoint: 'endpoint' as any + } + client.references.set(uuid, entry) + + const ref = await client.get('test' as any, { uuid } as any) + expect(ref).toBe(fakeRef) + }) +}) diff --git a/packages/client/src/__tests__/containerConnection.spec.ts b/packages/client/src/__tests__/containerConnection.spec.ts new file mode 100644 index 0000000000..35993f2b99 --- /dev/null +++ b/packages/client/src/__tests__/containerConnection.spec.ts @@ -0,0 +1,48 @@ +import { ContainerConnectionImpl } from '../agent' + +describe('ContainerConnectionImpl behavior', () => { + test('resolves promise connection and forwards request/close', async () => { + const connA = { + containerId: 'c1', + request: jest.fn().mockResolvedValue('resA'), + close: jest.fn().mockResolvedValue(undefined), + on: undefined + } + + const promiseA = Promise.resolve(connA as any) + const cc = new ContainerConnectionImpl('c1' as any, promiseA as any) + + // connect should await the promise + await cc.connect() + + const res = await cc.request('op') + expect(res).toBe('resA') + expect(connA.request).toHaveBeenCalledWith('op', undefined) + + await cc.close() + expect(connA.close).toHaveBeenCalled() + }) + + test('setConnection replaces and closes previous connection', async () => { + const connOld = { + containerId: 'c2', + request: jest.fn().mockResolvedValue('old'), + close: jest.fn().mockResolvedValue(undefined), + on: undefined + } + const cc = new ContainerConnectionImpl('c2' as any, connOld as any) + + // set new connection should close old + const connNew = { + containerId: 'c2', + request: jest.fn().mockResolvedValue('new'), + close: jest.fn().mockResolvedValue(undefined), + on: undefined + } + cc.setConnection(connNew as any) + // Give microtasks time + const res = await cc.request('op') + expect(res).toBe('new') + expect(connOld.close).toHaveBeenCalled() + }) +}) diff --git a/packages/client/src/__tests__/establish.spec.ts b/packages/client/src/__tests__/establish.spec.ts new file mode 100644 index 0000000000..a79242c678 --- /dev/null +++ b/packages/client/src/__tests__/establish.spec.ts @@ -0,0 +1,106 @@ +// Mocks for modules used by client +import type { ContainerConnection, TickManager } from '@hcengineering/network-core' +import { NetworkClientImpl } from '../client' + +jest.mock('@hcengineering/network-backrpc', () => ({ + BackRPCClient: jest.fn().mockImplementation(() => ({ + waitConnection: jest.fn().mockResolvedValue(undefined), + request: jest.fn().mockResolvedValue(undefined), + close: jest.fn() + })) +})) + +jest.mock('@hcengineering/network-core', () => ({ + parseEndpointRef: jest.fn((endpoint: string) => { + if (endpoint === 'routed-endpoint') { + return { uuid: 'container-1', kind: 'routed', host: 'h', port: 1234, agentId: 'agent-1' } + } + // direct path + return { uuid: 'container-2', kind: 'direct', host: 'dh', port: 4321 } + }), + EndpointKind: { noconnect: 'noconnect', routed: 'routed' }, + agentDirectRef: (host: string, port: number, agentId?: string) => `${host}:${port}:${agentId ?? ''}` +})) + +// Provide simple mock implementations for agent-level connection classes +jest.mock('../agent', () => { + return { + RoutedNetworkAgentConnectionImpl: class { + connect (containerUuid: string): any { + return Promise.resolve({ + containerId: containerUuid, + request: async (_op: string, _data?: any) => 'ok', + close: async () => {}, + on: undefined + }) + } + + close (): any {} + }, + NetworkDirectConnectionImpl: class { + request = async (_op: string, _data?: any): Promise => 'ok' + close = async (): Promise => {} + }, + ContainerConnectionImpl: class { + connection!: ContainerConnection | Promise + constructor ( + readonly containerId: string, + connection: ContainerConnection | Promise + ) { + this.setConnection(connection) + } + + async connect (): Promise { + if (this.connection instanceof Promise) { + this.connection = await this.connection + } + } + + setConnection (c: ContainerConnection | Promise): void { + this.connection = c + } + + async request (op: string, data?: any): Promise { + let c = this.connection + if (c instanceof Promise) { + c = await c + } + return await c.request(op, data) + } + + async close (): Promise { + if (this.connection instanceof Promise) { + this.connection = await this.connection + } + await this.connection.close() + } + } + } +}) + +const tickMgr: TickManager = { + tps: 1, + now: () => Date.now(), + waitTick: (_n: number) => Promise.resolve(), + register: (_fn: any, _interval: number) => () => {}, + start: () => {}, + stop: () => {} +} + +describe('establishConnection paths', () => { + test('routed connection creates agentConnections and containerConnections', () => { + const client = new NetworkClientImpl('localhost', 3000, tickMgr) + const conn = client.establishConnection('container-1' as any, 'routed-endpoint' as any) + expect(client.agentConnections.size).toBeGreaterThan(0) + expect(client.containerConnections.size).toBeGreaterThan(0) + expect(conn.containerId).toBe('container-1') + }) + + test('direct connection creates containerConnections only', () => { + const client = new NetworkClientImpl('localhost', 3000, tickMgr) + const conn = client.establishConnection('container-2' as any, 'direct-endpoint' as any) + expect(client.agentConnections.size).toBe(0) + expect(client.containerConnections.size).toBeGreaterThan(0) + expect(conn.containerId).toBe('container-2') + }) +}) diff --git a/packages/core/jest.config.js b/packages/core/jest.config.js index 2cfd408b67..069ea8aa0d 100644 --- a/packages/core/jest.config.js +++ b/packages/core/jest.config.js @@ -2,6 +2,6 @@ module.exports = { preset: 'ts-jest', testEnvironment: 'node', testMatch: ['**/?(*.)+(spec|test).[jt]s?(x)'], - roots: ["./src"], - coverageReporters: ["text-summary", "html"] + roots: ['./src'], + coverageReporters: ['text-summary', 'html', 'lcov'] } diff --git a/packages/core/package.json b/packages/core/package.json index 9c1e5a6f61..3460fba801 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -20,10 +20,10 @@ "build": "compile", "prepublishOnly": "npm run build", "build:watch": "compile", - "test": "jest --passWithNoTests --silent --forceExit", + "test": "jest --coverage --coverageDirectory=./coverage --passWithNoTests --forceExit", "format": "format src", "_phase:build": "compile transpile src", - "_phase:test": "jest --passWithNoTests --silent --forceExit", + "_phase:test": "jest --coverage --coverageDirectory=./coverage --passWithNoTests --forceExit", "_phase:format": "format src", "_phase:validate": "compile validate" }, diff --git a/packages/core/src/network.ts b/packages/core/src/network.ts index d35ea30400..e9e60de816 100644 --- a/packages/core/src/network.ts +++ b/packages/core/src/network.ts @@ -84,23 +84,18 @@ export class NetworkImpl implements Network, NetworkWithClients { } async agents (): Promise { - return Array.from( - this._agents.values().map(({ api, containers }) => ({ - agentId: api.uuid, - endpoint: api.endpoint, - kinds: api.kinds, - containers: containers.size - })) - ) + return Array.from(this._agents.values()).map(({ api, containers }) => ({ + agentId: api.uuid, + endpoint: api.endpoint, + kinds: api.kinds, + containers: containers.size + })) } async kinds (): Promise { - return Array.from( - this._agents - .values() - .map((it) => it.kinds) - .flatMap((it) => it) - ) + return Array.from(this._agents.values()) + .map((it) => it.kinds) + .flatMap((it) => it) } async list (kind?: ContainerKind): Promise { diff --git a/packages/server/jest.config.js b/packages/server/jest.config.js index 2cfd408b67..069ea8aa0d 100644 --- a/packages/server/jest.config.js +++ b/packages/server/jest.config.js @@ -2,6 +2,6 @@ module.exports = { preset: 'ts-jest', testEnvironment: 'node', testMatch: ['**/?(*.)+(spec|test).[jt]s?(x)'], - roots: ["./src"], - coverageReporters: ["text-summary", "html"] + roots: ['./src'], + coverageReporters: ['text-summary', 'html', 'lcov'] } diff --git a/packages/server/package.json b/packages/server/package.json index a0b65943b7..d8324d3810 100644 --- a/packages/server/package.json +++ b/packages/server/package.json @@ -20,10 +20,10 @@ "build": "compile", "prepublishOnly": "npm run build", "build:watch": "compile", - "test": "jest --passWithNoTests --silent --forceExit", + "test": "jest --coverage --coverageDirectory=./coverage --passWithNoTests --forceExit", "format": "format src", "_phase:build": "compile transpile src", - "_phase:test": "jest --passWithNoTests --silent --forceExit", + "_phase:test": "jest --coverage --coverageDirectory=./coverage --passWithNoTests --forceExit", "_phase:format": "format src", "_phase:validate": "compile validate" }, diff --git a/pods/network-pod/jest.config.js b/pods/network-pod/jest.config.js index 6479224b48..069ea8aa0d 100644 --- a/pods/network-pod/jest.config.js +++ b/pods/network-pod/jest.config.js @@ -3,5 +3,5 @@ module.exports = { testEnvironment: 'node', testMatch: ['**/?(*.)+(spec|test).[jt]s?(x)'], roots: ['./src'], - coverageReporters: ['text-summary', 'html'] + coverageReporters: ['text-summary', 'html', 'lcov'] } diff --git a/pods/network-pod/package.json b/pods/network-pod/package.json index 17e3873553..5f913ca926 100644 --- a/pods/network-pod/package.json +++ b/pods/network-pod/package.json @@ -30,9 +30,9 @@ "docker:push": "../../common/scripts/docker_tag.sh hardcoreeng/network-pod", "build:watch": "compile", "format": "format src", - "test": "jest --passWithNoTests --silent --forceExit", + "test": "jest --coverage --coverageDirectory=./coverage --passWithNoTests --forceExit", "_phase:build": "compile transpile src", - "_phase:test": "jest --passWithNoTests --silent --forceExit", + "_phase:test": "jest --coverage --coverageDirectory=./coverage --passWithNoTests --forceExit", "_phase:format": "format src", "_phase:validate": "compile validate" }, diff --git a/pods/network-tool/jest.config.js b/pods/network-tool/jest.config.js index 6479224b48..069ea8aa0d 100644 --- a/pods/network-tool/jest.config.js +++ b/pods/network-tool/jest.config.js @@ -3,5 +3,5 @@ module.exports = { testEnvironment: 'node', testMatch: ['**/?(*.)+(spec|test).[jt]s?(x)'], roots: ['./src'], - coverageReporters: ['text-summary', 'html'] + coverageReporters: ['text-summary', 'html', 'lcov'] } diff --git a/pods/network-tool/package.json b/pods/network-tool/package.json index f7e7d3cc4f..d1665f2b09 100644 --- a/pods/network-tool/package.json +++ b/pods/network-tool/package.json @@ -31,9 +31,9 @@ "run": "(rushx bundle || true) && node bundle/bundle.js", "build:watch": "compile", "format": "format src", - "test": "jest --passWithNoTests --silent --forceExit", + "test": "jest --coverage --coverageDirectory=./coverage --passWithNoTests --forceExit", "_phase:build": "compile transpile src", - "_phase:test": "jest --passWithNoTests --silent --forceExit", + "_phase:test": "jest --coverage --coverageDirectory=./coverage --passWithNoTests --forceExit", "_phase:format": "format src", "_phase:validate": "compile validate" }, diff --git a/scripts/generate-coverage-html.js b/scripts/generate-coverage-html.js new file mode 100644 index 0000000000..6bc54b62cb --- /dev/null +++ b/scripts/generate-coverage-html.js @@ -0,0 +1,153 @@ +#!/usr/bin/env node +const fs = require('fs') +const path = require('path') + +const [, , inFile = 'coverage/lcov.info', outDir = 'coverage/html'] = process.argv + +if (!fs.existsSync(inFile)) { + console.error('Input lcov not found:', inFile) + process.exit(1) +} + +const lcovParse = require('lcov-parse') +const libCoverage = require('istanbul-lib-coverage') +const reports = require('istanbul-reports') +const libReport = require('istanbul-lib-report') + +const data = fs.readFileSync(inFile, 'utf8') + +// build repo file index to resolve source files +const root = process.cwd() +const ignoreDirs = new Set(['node_modules', '.git', 'coverage', 'lib', 'dist', 'types', '.rush', 'temp', 'pnpm-store']) +const repoFiles = [] +function walk(dir) { + const items = fs.readdirSync(dir, { withFileTypes: true }) + for (const it of items) { + if (it.isDirectory()) { + if (ignoreDirs.has(it.name)) continue + if (it.name.startsWith('.')) continue + try { + walk(path.join(dir, it.name)) + } catch (e) {} + } else if (it.isFile()) { + repoFiles.push(path.join(dir, it.name)) + } + } +} +try { + walk(root) +} catch (e) {} + +lcovParse(data, (err, parsed) => { + if (err) { + console.error('lcov-parse error:', err) + process.exit(1) + } + + const map = libCoverage.createCoverageMap({}) + for (const file of parsed) { + // parsed entries include 'file', 'lines', 'functions', 'branches' + const coverage = { + path: file.file, + statementMap: {}, + fnMap: {}, + branchMap: {}, + s: {}, + f: {}, + b: {} + } + + // The lcov parser gives line coverage data; create synthetic statement entries per line + if (file.lines && file.lines.details) { + let idx = 0 + for (const d of file.lines.details) { + idx++ + const key = String(idx) + coverage.statementMap[key] = { start: { line: d.line, column: 0 }, end: { line: d.line, column: 0 } } + coverage.s[key] = d.hit + } + } + + // functions and branches are ignored for more accurate tools; keep minimal + map.addFileCoverage(coverage) + } + + // custom source finder: try absolute, repo-relative, and suffix matches + const sourceFinder = (filePath) => { + try { + if (!global.__seenPaths) global.__seenPaths = [] + if (global.__seenPaths.length < 500) global.__seenPaths.push(filePath) + if (global.__seenPaths.length === 500 && !global.__seenLogged) { + console.error('sourceFinder seen paths (sample):\n', global.__seenPaths.join('\n')) + global.__seenLogged = true + } + if (global.__seenPaths.length <= 200) console.error('sourceFinder request:', filePath) + } catch (e) {} + try { + if (path.isAbsolute(filePath) && fs.existsSync(filePath)) return fs.readFileSync(filePath, 'utf8') + const abs1 = path.resolve(root, filePath) + if (fs.existsSync(abs1)) return fs.readFileSync(abs1, 'utf8') + // try suffix match + const found = repoFiles.find((p) => p.endsWith(path.sep + filePath) || p.endsWith(filePath)) + if (found) return fs.readFileSync(found, 'utf8') + // debug unresolved + if (!found) { + try { + if (!global.__unresolved) global.__unresolved = new Set() + if (global.__unresolved.size < 200) global.__unresolved.add(filePath) + } catch (e) {} + } + } catch (e) { + // ignore and return null below + } + return null + } + + const context = libReport.createContext({ dir: outDir, coverageMap: map, sourceFinder }) + const report = reports.create('html', {}) + report.execute(context) + if (global.__unresolved && global.__unresolved.size) { + console.error('Unresolved filePath samples:\n', Array.from(global.__unresolved).slice(0, 50).join('\n')) + } + console.log('HTML report generated in', outDir) + // Post-process HTML files: if any report page contains the 'Unable to lookup source' placeholder, + // replace it with the actual source file contents when we can resolve it. + try { + for (const file of parsed) { + const srcAbs = file.file + // normalize key as used by report (from last '/src/' onward) if present + let key + const idx = srcAbs.lastIndexOf(path.sep + 'src' + path.sep) + if (idx !== -1) key = srcAbs.slice(idx + 1) + else key = path.basename(srcAbs) + + const htmlPath = path.join(outDir, key + '.html') + if (!fs.existsSync(htmlPath)) continue + let html = fs.readFileSync(htmlPath, 'utf8') + if (!html.includes('Unable to lookup source')) continue + // read source + let src + try { + src = fs.readFileSync(srcAbs, 'utf8') + } catch (e) { + src = null + } + if (!src) { + // try suffix match in repoFiles + const found = repoFiles.find((p) => p.endsWith(path.sep + key) || p.endsWith(key)) + if (found) src = fs.readFileSync(found, 'utf8') + } + if (!src) continue + // escape HTML + const esc = src.replace(/&/g, '&').replace(//g, '>') + // replace the first prettyprint
...
block that contains 'Unable to lookup source' + html = html.replace( + /
${esc}
` + ) + fs.writeFileSync(htmlPath, html, 'utf8') + } + } catch (e) { + console.error('post-process html error', e) + } +}) diff --git a/scripts/merge-coverage.js b/scripts/merge-coverage.js new file mode 100644 index 0000000000..7d55b07ce9 --- /dev/null +++ b/scripts/merge-coverage.js @@ -0,0 +1,106 @@ +#!/usr/bin/env node +const fs = require('fs') +const path = require('path') + +const root = process.cwd() +const patterns = ['packages', 'pods', 'tests'] +let files = [] +for (const p of patterns) { + const dir = path.join(root, p) + if (!fs.existsSync(dir)) continue + const items = fs.readdirSync(dir) + for (const it of items) { + const lcov = path.join(dir, it, 'coverage', 'lcov.info') + if (fs.existsSync(lcov)) files.push(lcov) + } +} + +if (files.length === 0) { + console.error('No lcov files found in packages/pods/tests/*/coverage/lcov.info') + process.exit(1) +} + +const outDir = path.join(root, 'coverage') +if (!fs.existsSync(outDir)) fs.mkdirSync(outDir, { recursive: true }) +const outFile = path.join(outDir, 'lcov.info') + +let outData = '' +let seenTN = false +// Build a repo file index to help resolve SF entries that are ambiguous +const ignoreDirs = new Set(['node_modules', '.git', 'coverage', 'lib', 'dist', 'types', '.rush', 'temp', 'pnpm-store']) +const repoFiles = [] +function walk(dir) { + const items = fs.readdirSync(dir, { withFileTypes: true }) + for (const it of items) { + if (it.isDirectory()) { + if (ignoreDirs.has(it.name)) continue + // skip hidden folders except top-level .config maybe + if (it.name.startsWith('.')) continue + try { + walk(path.join(dir, it.name)) + } catch (e) { + // ignore permission errors + } + } else if (it.isFile()) { + repoFiles.push(path.join(dir, it.name)) + } + } +} +try { + walk(root) +} catch (e) { + /* ignore */ +} + +for (const f of files) { + const data = fs.readFileSync(f, 'utf8') + const pkgDir = path.dirname(path.dirname(f)) + const lines = data.split(/\r?\n/) + const outLines = [] + for (const line of lines) { + if (!line) continue + // skip duplicate TN: headers (test name) + if (line.startsWith('TN:')) { + if (seenTN) continue + seenTN = true + outLines.push(line) + continue + } + + if (line.startsWith('SF:')) { + const orig = line.slice(3) + // if path is absolute and exists, keep it; otherwise resolve from package dir + if (path.isAbsolute(orig)) { + outLines.push('SF:' + orig) + continue + } + + const abs = path.resolve(pkgDir, orig) + if (fs.existsSync(abs)) { + outLines.push('SF:' + abs) + } else { + // try package/src/orig if orig is not already prefixed with src + const alt = path.resolve(pkgDir, orig) + if (fs.existsSync(alt)) { + outLines.push('SF:' + path.relative(root, alt)) + } else { + // try to find any file in repo that ends with the orig path + const found = repoFiles.find((p) => p.endsWith(path.sep + orig) || p.endsWith(orig)) + if (found) { + outLines.push('SF:' + found) + } else { + // keep original if we can't resolve + outLines.push('SF:' + orig) + } + } + } + continue + } + + outLines.push(line) + } + + outData += outLines.join('\n') + '\n' +} +fs.writeFileSync(outFile, outData, 'utf8') +console.log('Merged', files.length, 'lcov files into', outFile) diff --git a/tests/package.json b/tests/package.json index d8829606df..ccdf93d831 100644 --- a/tests/package.json +++ b/tests/package.json @@ -11,7 +11,7 @@ "build": "compile", "build:watch": "compile", "test": "echo 'run api-test' for API-tests", - "api-test": "jest --passWithNoTests --silent --forceExit", + "api-test": "jest --coverage --coverageDirectory=./coverage --passWithNoTests --forceExit", "format": "format src", "_phase:build": "compile transpile src", "_phase:test": "echo 'run api-test' for API-tests",