Skip to content

Commit

Permalink
fix(build): code coverage
Browse files Browse the repository at this point in the history
  • Loading branch information
divdavem committed Sep 4, 2023
1 parent 46cbb27 commit d8b100b
Show file tree
Hide file tree
Showing 32 changed files with 497 additions and 708 deletions.
1 change: 1 addition & 0 deletions .github/workflows/e2e-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ jobs:
registry-url: 'https://registry.npmjs.org'
- run: npm ci
- run: npx playwright install --with-deps
- run: npm run build code-coverage
- run: npm run build:coverage
- run: npm run e2e -- --shard=${{ matrix.shard }}/${{ strategy.job-total}}
- id: check_lcov
Expand Down
13 changes: 2 additions & 11 deletions angular/angular.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
"prefix": "app",
"architect": {
"build": {
"builder": "ngx-build-plus:browser",
"builder": "@angular-devkit/build-angular:browser",
"options": {
"outputPath": "dist/demo",
"index": "demo/src/index.html",
Expand Down Expand Up @@ -55,16 +55,12 @@
"extractLicenses": false,
"sourceMap": true,
"namedChunks": true
},
"coverage": {
"sourceMap": true,
"extraWebpackConfig": "./demo/coverage.webpack.js"
}
},
"defaultConfiguration": "production"
},
"serve": {
"builder": "ngx-build-plus:dev-server",
"builder": "@angular-devkit/build-angular:dev-server",
"configurations": {
"production": {
"servePath": "/angular/samples",
Expand All @@ -73,11 +69,6 @@
"development": {
"servePath": "/angular/samples",
"browserTarget": "demo:build:development"
},
"coverage": {
"servePath": "/angular/samples",
"browserTarget": "demo:build:coverage",
"extraWebpackConfig": "./demo/coverage.webpack.js"
}
},
"defaultConfiguration": "development"
Expand Down
14 changes: 0 additions & 14 deletions angular/demo/coverage.webpack.js

This file was deleted.

1 change: 1 addition & 0 deletions angular/lib/src/lib/agnos-ui-angular.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import {SlotDirective} from './slot.directive';
import {AlertComponent} from './alert/alert.component';
import {AccordionDirective, AccordionItemComponent, AccordionHeaderDirective, AccordionBodyDirective} from './accordion/accordion.component';

/* istanbul ignore next */
const components = [
SlotDirective,
SelectComponent,
Expand Down
3 changes: 2 additions & 1 deletion angular/lib/src/lib/modal/modal.service.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import {ApplicationRef, createComponent, EnvironmentInjector, EventEmitter, inject, Injectable, Injector} from '@angular/core';
import type {ModalProps} from './modal.component';
import {ModalComponent} from './modal.component';
import type {Subscription} from 'rxjs';

export interface ModalServiceOpenOptions {
injector?: Injector;
Expand All @@ -16,7 +17,7 @@ export class ModalService {
environmentInjector: injector.get(EnvironmentInjector),
elementInjector: injector,
});
const subscriptions = [];
const subscriptions: Subscription[] = [];
try {
for (const prop of Object.keys(options) as (string & keyof ModalProps)[]) {
const value = options[prop];
Expand Down
6 changes: 2 additions & 4 deletions angular/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,11 @@
"scripts": {
"ng": "ng",
"dev": "ng serve",
"dev:coverage": "ng serve --configuration coverage",
"dev:coverage": "node -r @agnos-ui/code-coverage/interceptReadFile ../node_modules/.bin/ng serve",
"build": "npm run build:lib && npm run build:demo",
"build:lib": "ng build lib",
"build:demo": "ng build demo",
"build:coverage": "ng build demo --configuration coverage",
"build:coverage": "node -r @agnos-ui/code-coverage/interceptReadFile ../node_modules/.bin/ng build demo",
"watch": "ng build --watch --configuration development",
"preview": "node ./scripts/preview.cjs dist/demo --port 4200 --single",
"tdd": "npm run test:lib --watch",
Expand Down Expand Up @@ -38,15 +38,13 @@
"@angular/compiler-cli": "^16.2.2",
"@types/jasmine": "^4.3.5",
"@types/webpack-env": "^1.18.1",
"coverage-istanbul-loader": "^3.0.5",
"jasmine-core": "^5.1.1",
"karma": "^6.4.2",
"karma-chrome-launcher": "^3.2.0",
"karma-coverage": "^2.2.1",
"karma-jasmine": "^5.1.0",
"karma-jasmine-html-reporter": "^2.1.0",
"ng-packagr": "^16.2.1",
"ngx-build-plus": "^16.0.0",
"raw-loader": "^4.0.2",
"sirv-cli": "^2.0.2",
"typescript": "~5.1.6"
Expand Down
1 change: 0 additions & 1 deletion angular/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@
"strictPropertyInitialization": false,
"noPropertyAccessFromIndexSignature": true,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true,
"sourceMap": true,
"declaration": false,
"downlevelIteration": true,
Expand Down
15 changes: 15 additions & 0 deletions code-coverage/lib/filter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import {createFilter} from 'vite';
import path from 'path';
import {fileURLToPath} from 'url';

const __dirname = path.dirname(fileURLToPath(import.meta.url));

export const rootFolder = path.join(__dirname, '..', '..', '..');

export const filter = createFilter(
['core/lib/**/*', 'angular/lib/src/lib/**/*', 'react/lib/**/*', 'svelte/lib/**/*'],
['node_modules', '**/*.spec.ts', '**/__mocks__/**'],
{
resolve: rootFolder,
}
);
33 changes: 33 additions & 0 deletions code-coverage/lib/instrument.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import {transform} from '@babel/core';
import {dirname} from 'path';

// TODO: support for svelte files
const supportedExtensionsRegExp = /\.(tsx?|jsx?)$/;
export const canInstrument = (fileName: string) => !fileName.includes('\x00') && supportedExtensionsRegExp.test(fileName);

export const instrumentFile = (code: string, filename: string) => {
console.log('Instrumenting for coverage: ', filename);
const result = transform(code, {
filename,
plugins: [
['@babel/plugin-syntax-decorators', {version: 'legacy'}],
[
'@babel/plugin-syntax-typescript',
{
isTSX: filename.endsWith('.tsx'),
},
],
[
'babel-plugin-istanbul',
{
cwd: dirname(filename),
exclude: [],
},
],
],
});
if (result?.code) {
return result.code;
}
return code;
};
59 changes: 59 additions & 0 deletions code-coverage/lib/interceptReadFile.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import fs from 'fs';
import util from 'util';
import {normalize} from 'path';
import {fileURLToPath} from 'url';
import {instrumentFile, canInstrument} from './instrument';
import {filter} from './filter';
import {intercept} from './interceptSwitch';

const postProcessReadFile = (filePath: string | URL, code: string | Buffer) => {
if (typeof filePath === 'object') {
filePath = fileURLToPath(filePath);
}
const normalizedFilePath = normalize(filePath);
if (intercept() && canInstrument(normalizedFilePath) && filter(normalizedFilePath)) {
const isBuffer = Buffer.isBuffer(code);
if (isBuffer) {
code = code.toString('utf8');
}
try {
code = instrumentFile(code as string, normalizedFilePath);
} catch (error) {
console.log(`Error while instrumenting ${normalizedFilePath}:`, error);
throw error;
}
if (isBuffer) {
code = Buffer.from(code, 'utf8');
}
}
return code;
};

// override readFileSync to provide instrumented files:
const trueReadFileSync = fs.readFileSync;
fs.readFileSync = (...args) => {
return postProcessReadFile(args[0] as string | URL, trueReadFileSync(...args)) as any;
};

// override fs.promises.readFile to provide instrumented files:
const truePromisesReadFile = fs.promises.readFile;
fs.promises.readFile = async (...args) => {
return postProcessReadFile(args[0] as string | URL, await truePromisesReadFile(...args)) as any;
};

// override readFile to provide instrumented files:
const trueReadFile = fs.readFile;
const newReadFile = (...args: Parameters<typeof trueReadFile>) => {
const cbIndex = args.length - 1;
const trueCb = args[cbIndex] as any;
args[cbIndex] = (err: any, result: any) => {
if (err) {
trueCb(err, result);
} else {
trueCb(err, postProcessReadFile(args[0] as string | URL, result));
}
};
trueReadFile(...args);
};
(newReadFile as any)[util.promisify.custom] = fs.promises.readFile;
fs.readFile = newReadFile as any;
14 changes: 14 additions & 0 deletions code-coverage/lib/interceptSwitch.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
const key = '__AGNOS_UI_CODE_COVERAGE_SKIP_INTERCEPT__';
const global = globalThis as any;

export const intercept = () => (global[key] ?? 0) === 0;

export const skipIntercept = async <T>(fn: () => T | Promise<T>): Promise<T> => {
try {
const value = global[key] ?? 0;
global[key] = value + 1;
return await fn();
} finally {
global[key]--;
}
};
1 change: 1 addition & 0 deletions code-coverage/lib/nyc.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
declare module 'nyc';
7 changes: 7 additions & 0 deletions code-coverage/lib/reportCoverage.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import {promises as fs} from 'fs';
import path from 'path';
import {v4 as uuidv4} from 'uuid';

export default async (baseDir: string, coverage: string) => {
await fs.writeFile(path.join(baseDir, '.nyc_output', `${uuidv4()}.json`), coverage);
};
33 changes: 33 additions & 0 deletions code-coverage/lib/setup.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import NYC from 'nyc';
import {promises as fs} from 'fs';
import path from 'path';
import {skipIntercept} from './interceptSwitch';
import {rootFolder} from './filter';

export default async (baseDir: string) => {
console.log('Cleaning up coverage folders...');
const nycDir = path.join(baseDir, '.nyc_output');
const reportDir = path.join(baseDir, 'coverage');
await fs.rm(nycDir, {recursive: true, force: true});
await fs.mkdir(nycDir);
await fs.rm(reportDir, {recursive: true, force: true});
await fs.mkdir(reportDir);
console.log('Coverage setup ready !');
const nycInstance = new NYC({
cwd: rootFolder,
tempDirectory: nycDir,
reportDir,
reporter: ['lcov', 'json', 'text-summary'],
extension: ['.ts', '.tsx', '.svelte'],
});
return async () => {
console.log('Saving coverage report...');
const files = await fs.readdir(nycDir);
if (files.length) {
await skipIntercept(() => nycInstance.report());
console.log('Coverage report saved !');
} else {
console.log('No coverage computed.');
}
};
};
14 changes: 14 additions & 0 deletions code-coverage/lib/vitePlugin.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import type {Plugin} from 'vite';
import {canInstrument, instrumentFile} from './instrument';
import {filter} from './filter';

export default (): Plugin => {
return {
name: '@agnos-ui/code-coverage',
enforce: 'pre',
transform(code, id, options) {
if (!filter(id) || !canInstrument(id)) return;
return instrumentFile(code, id);
},
};
};
38 changes: 38 additions & 0 deletions code-coverage/lib/vitestProvider.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import type {AfterSuiteRunMeta, CoverageProvider, CoverageProviderModule, ReportContext, Vitest} from 'vitest';
import setup from './setup';
import reportCoverage from './reportCoverage';

const customCoverageProviderModule: CoverageProviderModule = {
async getProvider(): Promise<CoverageProvider> {
let ctx: Vitest | undefined;
let finalizeReport: undefined | (() => Promise<void>);
return {
name: '@agnos-ui/code-coverage',
initialize(newCtx: Vitest) {
ctx = newCtx;
},
resolveOptions() {
return ctx!.config.coverage;
},
async clean(clean?: boolean) {
finalizeReport = await setup(ctx!.config.root);
},
async onAfterSuiteRun({coverage}: AfterSuiteRunMeta) {
if (coverage) {
reportCoverage(ctx!.config.root, JSON.stringify(coverage));
}
},
async reportCoverage(reportContext?: ReportContext) {
await finalizeReport?.();
finalizeReport = undefined;
},
};
},
takeCoverage() {
const coverage = (globalThis as any).__coverage__;
(globalThis as any).__coverage__ = {};
return coverage;
},
};

export default customCoverageProviderModule;
Loading

0 comments on commit d8b100b

Please sign in to comment.