diff --git a/.prettierrc.js b/.prettierrc.js new file mode 100644 index 0000000..49ef22e --- /dev/null +++ b/.prettierrc.js @@ -0,0 +1,9 @@ +module.exports = { + arrowParens: 'avoid', + bracketSameLine: true, + bracketSpacing: true, + singleQuote: true, + trailingComma: 'none', + tabWidth: 4, + printWidth: 100 +}; diff --git a/README.md b/README.md index 7c37e5d..04510d8 100644 --- a/README.md +++ b/README.md @@ -53,6 +53,9 @@ schedule(istanbulCoverage({ // What to do when the PR doesn't meet the minimum code coverage threshold reportMode: "message", // || "warn" || "fail" + // Required for lcov reports which uses relative path. + useAbsolutePath: true, // || false + // Minimum coverage threshold percentages. Compared against the cumulative coverage of the reportFileSet. threshold: { statements: 100, @@ -69,7 +72,7 @@ This plugin requires the 'json-summary' or 'lcov' report modes be enabled with I ### What test runners does this work with? -Anything that integrates with [Istanbul](https://www.npmjs.com/package/istanbul), or produces output in the (lcov)[https://linux.die.net/man/1/lcov] format. Istanbul is test runner agnostic, and can be integrated with anything written in node. Some test runners already integrate Istanbul, for instance [Jest](https://jestjs.io/docs/en/cli.html#coverage) bundles it internally. Helper integrations exists for [Jasmine](https://www.npmjs.com/package/jasmine-istanbul-reporter) and other libraries. +Anything that integrates with [Istanbul](https://www.npmjs.com/package/istanbul), or produces output in the (lcov)[https://linux.die.net/man/1/lcov] format. Istanbul is test runner agnostic, and can be integrated with anything written in node. Some test runners already integrate Istanbul, for instance [Jest](https://jestjs.io/docs/en/cli.html#coverage) bundles it internally. Helper integrations exists for [Jasmine](https://www.npmjs.com/package/jasmine-istanbul-reporter) and other libraries. ### Why should my team see code coverage stats in their PRs? diff --git a/src/config.model.test.ts b/src/config.model.test.ts index 8891cbd..1198a66 100644 --- a/src/config.model.test.ts +++ b/src/config.model.test.ts @@ -1,39 +1,40 @@ -import { makeCompleteConfiguration } from "./config.model" +import { makeCompleteConfiguration } from './config.model'; -describe("makeCompleteConfiguration", () => { - const base = { - coveragePaths: ["./coverage/coverage-summary.json"], - reportFileSet: "all", - reportMode: "message", - entrySortMethod: "alphabetically", - numberOfEntries: 10, - threshold: { - statements: 100, - branches: 100, - functions: 100, - lines: 100, - }, - } +describe('makeCompleteConfiguration', () => { + const base = { + coveragePaths: ['./coverage/coverage-summary.json'], + reportFileSet: 'all', + reportMode: 'message', + entrySortMethod: 'alphabetically', + numberOfEntries: 10, + useAbsolutePath: true, + threshold: { + statements: 100, + branches: 100, + functions: 100, + lines: 100 + } + }; - it("returns a default configuration when sent undefined", () => { - const output = makeCompleteConfiguration() - expect(output).toEqual(base) - }) + it('returns a default configuration when sent undefined', () => { + const output = makeCompleteConfiguration(); + expect(output).toEqual(base); + }); - it("overrides coveragePaths with the value from coveragePath", () => { - const output = makeCompleteConfiguration({ - coveragePath: "some-other-path", - }) - expect(output).toEqual({ ...base, coveragePaths: ["some-other-path"] }) - }) + it('overrides coveragePaths with the value from coveragePath', () => { + const output = makeCompleteConfiguration({ + coveragePath: 'some-other-path' + }); + expect(output).toEqual({ ...base, coveragePaths: ['some-other-path'] }); + }); - it("overrides a specific value from the default", () => { - const output = makeCompleteConfiguration({ - reportMode: "warn", - }) - expect(output).toEqual({ - ...base, - reportMode: "warn", - }) - }) -}) + it('overrides a specific value from the default', () => { + const output = makeCompleteConfiguration({ + reportMode: 'warn' + }); + expect(output).toEqual({ + ...base, + reportMode: 'warn' + }); + }); +}); diff --git a/src/config.model.ts b/src/config.model.ts index e39165b..f649b47 100644 --- a/src/config.model.ts +++ b/src/config.model.ts @@ -1,37 +1,38 @@ -export type ReportFileSet = "created" | "modified" | "createdOrModified" | "all" -export type ReportMode = "fail" | "warn" | "message" +export type ReportFileSet = 'created' | 'modified' | 'createdOrModified' | 'all'; +export type ReportMode = 'fail' | 'warn' | 'message'; export type SortMethod = - | "alphabetically" - | "least-coverage" - | "most-coverage" - | "largest-file-size" - | "smallest-file-size" - | "uncovered-lines" + | 'alphabetically' + | 'least-coverage' + | 'most-coverage' + | 'largest-file-size' + | 'smallest-file-size' + | 'uncovered-lines'; -export type SourceType = "json-summary" | "lcov" +export type SourceType = 'json-summary' | 'lcov'; export interface SourcePathExplicit { - path: string - type: SourceType + path: string; + type: SourceType; } -export type SourcePath = string | SourcePathExplicit +export type SourcePath = string | SourcePathExplicit; export interface CoverageThreshold { - statements: number - branches: number - functions: number - lines: number + statements: number; + branches: number; + functions: number; + lines: number; } export interface Config { - customSuccessMessage?: string - customFailureMessage?: string - numberOfEntries: number - entrySortMethod: SortMethod - coveragePath?: SourcePath - coveragePaths: SourcePath[] - reportFileSet: ReportFileSet - threshold: CoverageThreshold - reportMode: ReportMode + customSuccessMessage?: string; + customFailureMessage?: string; + numberOfEntries: number; + useAbsolutePath: boolean; + entrySortMethod: SortMethod; + coveragePath?: SourcePath; + coveragePaths: SourcePath[]; + reportFileSet: ReportFileSet; + threshold: CoverageThreshold; + reportMode: ReportMode; } /** @@ -40,23 +41,27 @@ export interface Config { * @returns A complete configuration */ export function makeCompleteConfiguration(config?: Partial): Config { - const defaults: Config = { - coveragePaths: [], - reportFileSet: "all", - reportMode: "message", - entrySortMethod: "alphabetically", - numberOfEntries: 10, - threshold: { - statements: 100, - branches: 100, - functions: 100, - lines: 100, - }, - } + const defaults: Config = { + coveragePaths: [], + reportFileSet: 'all', + reportMode: 'message', + entrySortMethod: 'alphabetically', + numberOfEntries: 10, + useAbsolutePath: true, + threshold: { + statements: 100, + branches: 100, + functions: 100, + lines: 100 + } + }; - const combined = config ? { ...defaults, ...config } : defaults - const coveragePath = combined.coveragePath ? combined.coveragePath : "./coverage/coverage-summary.json" - const coveragePaths = combined.coveragePaths.length === 0 ? [coveragePath] : combined.coveragePaths - delete combined.coveragePath - return { ...combined, coveragePaths } + const combined = config ? { ...defaults, ...config } : defaults; + const coveragePath = combined.coveragePath + ? combined.coveragePath + : './coverage/coverage-summary.json'; + const coveragePaths = + combined.coveragePaths.length === 0 ? [coveragePath] : combined.coveragePaths; + delete combined.coveragePath; + return { ...combined, coveragePaths }; } diff --git a/src/index.test.ts b/src/index.test.ts index be096b5..acbf238 100644 --- a/src/index.test.ts +++ b/src/index.test.ts @@ -1,146 +1,146 @@ -import * as path from "path" -import FilesystemService from "./filesystem.service" -import { GitService } from "./git.service" -import { istanbulCoverage } from "./index" -jest.mock("./filesystem.service") -jest.mock("./git.service") +import * as path from 'path'; +import FilesystemService from './filesystem.service'; +import { GitService } from './git.service'; +import { istanbulCoverage } from './index'; +jest.mock('./filesystem.service'); +jest.mock('./git.service'); /* tslint:disable max-line-length */ -declare const global: any +declare const global: any; -const basePath = "/some/random/path/to/repo" +const basePath = '/some/random/path/to/repo'; function makeCoverageEntry(coverage: number) { - return `{ + return `{ "0": ${coverage < 25 ? 0 : 1}, "1": ${coverage < 50 ? 0 : 1}, "2": ${coverage < 75 ? 0 : 1}, "3": ${coverage < 100 ? 0 : 1} - }` + }`; } function makeEntry( - fileName: string, - lineCoverage = 100, - statementCoverage = 100, - functionCoverage = 100, - branchCoverage = 100 + fileName: string, + lineCoverage = 100, + statementCoverage = 100, + functionCoverage = 100, + branchCoverage = 100 ) { - return ` + return ` "${fileName}": { "lines": { "total": 100, "covered": ${lineCoverage}, "skipped": 0, "pct": ${lineCoverage} }, "functions": { "total": 100, "covered": ${functionCoverage}, "skipped": 0, "pct": ${functionCoverage} }, "statements": { "total": 100, "covered": ${statementCoverage}, "skipped": 0, "pct": ${statementCoverage} }, "branches": { "total": 100, "covered": ${branchCoverage}, "skipped": 0, "pct": ${branchCoverage} } } - ` + `; } function setupGitService() { - ;(GitService as any).mockImplementation(() => { - return { - getRootDirectory: () => Promise.resolve(__dirname), - getCurrentCommit: () => Promise.resolve("master"), - } - }) + (GitService as any).mockImplementation(() => { + return { + getRootDirectory: () => Promise.resolve(__dirname), + getCurrentCommit: () => Promise.resolve('master') + }; + }); } function setupCoverageFile(coverages: string[] = []) { - ;(FilesystemService as any).mockImplementation(() => { - return { - exists: p => coverages.length !== 0, - read: p => { - const coverage = coverages.pop() - return coverage !== undefined ? coverage : undefined - }, - } - }) + (FilesystemService as any).mockImplementation(() => { + return { + exists: p => coverages.length !== 0, + read: p => { + const coverage = coverages.pop(); + return coverage !== undefined ? coverage : undefined; + } + }; + }); } -describe("istanbulCoverage()", () => { - beforeEach(() => { - global.warn = jest.fn() - global.message = jest.fn() - global.fail = jest.fn() - global.markdown = jest.fn() - global.danger = { - git: { - modified_files: ["src/modified-file1.ts", "src/modified-file2.ts"], - created_files: ["src/created-file1.ts", "src/created-file2.ts"], - }, - } - setupGitService() - setupCoverageFile([ - `{ - ${makeEntry("total", 50, 50, 50, 50)}, +describe('istanbulCoverage()', () => { + beforeEach(() => { + global.warn = jest.fn(); + global.message = jest.fn(); + global.fail = jest.fn(); + global.markdown = jest.fn(); + global.danger = { + git: { + modified_files: ['src/modified-file1.ts', 'src/modified-file2.ts'], + created_files: ['src/created-file1.ts', 'src/created-file2.ts'] + } + }; + setupGitService(); + setupCoverageFile([ + `{ + ${makeEntry('total', 50, 50, 50, 50)}, ${makeEntry(`${__dirname}/src/modified-file1.ts`, 66, 25, 25, 25)}, ${makeEntry(`${__dirname}/src/modified-file2.ts`, 99, 50, 75, 50)}, ${makeEntry(`${__dirname}/src/created-file1.ts`, 66, 100, 25, 50)}, ${makeEntry(`${__dirname}/src/created-file2.ts`, 99, 75, 50, 25)}, ${makeEntry(`${__dirname}/src/unmodified-field.ts`, 25, 25, 25, 25)} - }`, - ]) - }) + }` + ]); + }); - afterEach(() => { - global.warn = undefined - global.message = undefined - global.fail = undefined - global.markdown = undefined - jest.resetAllMocks() - }) + afterEach(() => { + global.warn = undefined; + global.message = undefined; + global.fail = undefined; + global.markdown = undefined; + jest.resetAllMocks(); + }); - it('will only report on new files when reportFileSet is set to "created"', async () => { - await istanbulCoverage({ - reportFileSet: "created", - }) - expect(global.markdown).toHaveBeenCalledWith( - `## Coverage in New Files + it('will only report on new files when reportFileSet is set to "created"', async () => { + await istanbulCoverage({ + reportFileSet: 'created' + }); + expect(global.markdown).toHaveBeenCalledWith( + `## Coverage in New Files File | Line Coverage | Statement Coverage | Function Coverage | Branch Coverage ---- | ------------: | -----------------: | ----------------: | --------------: [src/created\\-file1.ts](../blob/master/src/created\\-file1.ts) | (66/100) 66% | (100/100) 100% | (25/100) 25% | (50/100) 50% [src/created\\-file2.ts](../blob/master/src/created\\-file2.ts) | (99/100) 99% | (75/100) 75% | (50/100) 50% | (25/100) 25% Total | (165/200) 83% | (175/200) 88% | (75/200) 38% | (75/200) 38% ` - ) - }) + ); + }); - it("will find a coverage file when using an explict source type", async () => { - await istanbulCoverage({ - coveragePath: { path: "coverage-summary.json", type: "json-summary" }, - reportFileSet: "created", - }) - expect(global.markdown).toHaveBeenCalledWith( - `## Coverage in New Files + it('will find a coverage file when using an explict source type', async () => { + await istanbulCoverage({ + coveragePath: { path: 'coverage-summary.json', type: 'json-summary' }, + reportFileSet: 'created' + }); + expect(global.markdown).toHaveBeenCalledWith( + `## Coverage in New Files File | Line Coverage | Statement Coverage | Function Coverage | Branch Coverage ---- | ------------: | -----------------: | ----------------: | --------------: [src/created\\-file1.ts](../blob/master/src/created\\-file1.ts) | (66/100) 66% | (100/100) 100% | (25/100) 25% | (50/100) 50% [src/created\\-file2.ts](../blob/master/src/created\\-file2.ts) | (99/100) 99% | (75/100) 75% | (50/100) 50% | (25/100) 25% Total | (165/200) 83% | (175/200) 88% | (75/200) 38% | (75/200) 38% ` - ) - }) + ); + }); - it("can combine multiple coverage files", async () => { - setupCoverageFile([ - `{ - ${makeEntry("total", 50, 50, 50, 50)}, + it('can combine multiple coverage files', async () => { + setupCoverageFile([ + `{ + ${makeEntry('total', 50, 50, 50, 50)}, ${makeEntry(`${__dirname}/src/modified-file1.ts`, 66, 25, 25, 25)}, ${makeEntry(`${__dirname}/src/modified-file2.ts`, 99, 50, 75, 50)} }`, - `{ - ${makeEntry("total", 50, 50, 50, 50)}, + `{ + ${makeEntry('total', 50, 50, 50, 50)}, ${makeEntry(`${__dirname}/src/created-file1.ts`, 66, 100, 25, 50)}, ${makeEntry(`${__dirname}/src/created-file2.ts`, 99, 75, 50, 25)} - }`, - ]) - await istanbulCoverage({ - reportFileSet: "createdOrModified", - coveragePaths: ["coverage-path-1", "coverage-path-2"], - }) - expect(global.markdown).toHaveBeenCalledWith( - `## Coverage in Created or Modified Files + }` + ]); + await istanbulCoverage({ + reportFileSet: 'createdOrModified', + coveragePaths: ['coverage-path-1', 'coverage-path-2'] + }); + expect(global.markdown).toHaveBeenCalledWith( + `## Coverage in Created or Modified Files File | Line Coverage | Statement Coverage | Function Coverage | Branch Coverage ---- | ------------: | -----------------: | ----------------: | --------------: [src/created\\-file1.ts](../blob/master/src/created\\-file1.ts) | (66/100) 66% | (100/100) 100% | (25/100) 25% | (50/100) 50% @@ -149,11 +149,11 @@ File | Line Coverage | Statement Coverage | Function Coverage | Branch Coverage [src/modified\\-file2.ts](../blob/master/src/modified\\-file2.ts) | (99/100) 99% | (50/100) 50% | (75/100) 75% | (50/100) 50% Total | (330/400) 83% | (250/400) 63% | (175/400) 44% | (150/400) 38% ` - ) - }) - it("will automatically infer the lcov source type", async () => { - setupCoverageFile([ - `TN: + ); + }); + it('will automatically infer the lcov source type', async () => { + setupCoverageFile([ + `TN: SF: ${__dirname}/src/created-file1.ts FN: 1, func1 FNDA: 1, func1 @@ -163,25 +163,25 @@ BRF: 8 BRH: 4 LH: 15 LF: 20 -end_of_record`, - ]) - await istanbulCoverage({ - coveragePath: "lcov.info", - reportFileSet: "created", - }) - expect(global.markdown).toHaveBeenCalledWith( - `## Coverage in New Files +end_of_record` + ]); + await istanbulCoverage({ + coveragePath: 'lcov.info', + reportFileSet: 'created' + }); + expect(global.markdown).toHaveBeenCalledWith( + `## Coverage in New Files File | Line Coverage | Statement Coverage | Function Coverage | Branch Coverage ---- | ------------: | -----------------: | ----------------: | --------------: [src/created\\-file1.ts](../blob/master/src/created\\-file1.ts) | (15/20) 75% | (15/20) 75% | (1/1) 100% | (4/8) 50% Total | (15/20) 75% | (15/20) 75% | (1/1) 100% | (4/8) 50% ` - ) - }) + ); + }); - it("will use the lcov source type when specified explicitly", async () => { - setupCoverageFile([ - `TN: + it('will use the lcov source type when specified explicitly', async () => { + setupCoverageFile([ + `TN: SF: ${__dirname}/src/created-file1.ts FN: 1, func1 FNDA: 1, func1 @@ -191,42 +191,42 @@ BRF: 8 BRH: 4 LH: 15 LF: 20 -end_of_record`, - ]) - await istanbulCoverage({ - coveragePath: { path: "some.path", type: "lcov" }, - reportFileSet: "created", - }) - expect(global.markdown).toHaveBeenCalledWith( - `## Coverage in New Files +end_of_record` + ]); + await istanbulCoverage({ + coveragePath: { path: 'some.path', type: 'lcov' }, + reportFileSet: 'created' + }); + expect(global.markdown).toHaveBeenCalledWith( + `## Coverage in New Files File | Line Coverage | Statement Coverage | Function Coverage | Branch Coverage ---- | ------------: | -----------------: | ----------------: | --------------: [src/created\\-file1.ts](../blob/master/src/created\\-file1.ts) | (15/20) 75% | (15/20) 75% | (1/1) 100% | (4/8) 50% Total | (15/20) 75% | (15/20) 75% | (1/1) 100% | (4/8) 50% ` - ) - }) + ); + }); - it('will only report on modified files when reportFileSet is set to "modified"', async () => { - await istanbulCoverage({ - reportFileSet: "modified", - }) - expect(global.markdown).toHaveBeenCalledWith( - `## Coverage in Modified Files + it('will only report on modified files when reportFileSet is set to "modified"', async () => { + await istanbulCoverage({ + reportFileSet: 'modified' + }); + expect(global.markdown).toHaveBeenCalledWith( + `## Coverage in Modified Files File | Line Coverage | Statement Coverage | Function Coverage | Branch Coverage ---- | ------------: | -----------------: | ----------------: | --------------: [src/modified\\-file1.ts](../blob/master/src/modified\\-file1.ts) | (66/100) 66% | (25/100) 25% | (25/100) 25% | (25/100) 25% [src/modified\\-file2.ts](../blob/master/src/modified\\-file2.ts) | (99/100) 99% | (50/100) 50% | (75/100) 75% | (50/100) 50% Total | (165/200) 83% | (75/200) 38% | (100/200) 50% | (75/200) 38% ` - ) - }) - it('will only report on created and modified files when reportFileSet is set to "createdOrModified"', async () => { - await istanbulCoverage({ - reportFileSet: "createdOrModified", - }) - expect(global.markdown).toHaveBeenCalledWith( - `## Coverage in Created or Modified Files + ); + }); + it('will only report on created and modified files when reportFileSet is set to "createdOrModified"', async () => { + await istanbulCoverage({ + reportFileSet: 'createdOrModified' + }); + expect(global.markdown).toHaveBeenCalledWith( + `## Coverage in Created or Modified Files File | Line Coverage | Statement Coverage | Function Coverage | Branch Coverage ---- | ------------: | -----------------: | ----------------: | --------------: [src/created\\-file1.ts](../blob/master/src/created\\-file1.ts) | (66/100) 66% | (100/100) 100% | (25/100) 25% | (50/100) 50% @@ -235,15 +235,15 @@ File | Line Coverage | Statement Coverage | Function Coverage | Branch Coverage [src/modified\\-file2.ts](../blob/master/src/modified\\-file2.ts) | (99/100) 99% | (50/100) 50% | (75/100) 75% | (50/100) 50% Total | (330/400) 83% | (250/400) 63% | (175/400) 44% | (150/400) 38% ` - ) - }) + ); + }); - it('will report all files when reportFileSet is set to "all"', async () => { - await istanbulCoverage({ - reportFileSet: "all", - }) - expect(global.markdown).toHaveBeenCalledWith( - `## Coverage in All Files + it('will report all files when reportFileSet is set to "all"', async () => { + await istanbulCoverage({ + reportFileSet: 'all' + }); + expect(global.markdown).toHaveBeenCalledWith( + `## Coverage in All Files File | Line Coverage | Statement Coverage | Function Coverage | Branch Coverage ---- | ------------: | -----------------: | ----------------: | --------------: [src/created\\-file1.ts](../blob/master/src/created\\-file1.ts) | (66/100) 66% | (100/100) 100% | (25/100) 25% | (50/100) 50% @@ -253,16 +253,16 @@ File | Line Coverage | Statement Coverage | Function Coverage | Branch Coverage [src/unmodified\\-field.ts](../blob/master/src/unmodified\\-field.ts) | (25/100) 25% | (25/100) 25% | (25/100) 25% | (25/100) 25% Total | (355/500) 71% | (275/500) 55% | (200/500) 40% | (175/500) 35% ` - ) - }) + ); + }); - it("will only show the maximum number of entries", async () => { - await istanbulCoverage({ - reportFileSet: "all", - numberOfEntries: 3, - }) - expect(global.markdown).toHaveBeenCalledWith( - `## Coverage in All Files + it('will only show the maximum number of entries', async () => { + await istanbulCoverage({ + reportFileSet: 'all', + numberOfEntries: 3 + }); + expect(global.markdown).toHaveBeenCalledWith( + `## Coverage in All Files File | Line Coverage | Statement Coverage | Function Coverage | Branch Coverage ---- | ------------: | -----------------: | ----------------: | --------------: [src/created\\-file1.ts](../blob/master/src/created\\-file1.ts) | (66/100) 66% | (100/100) 100% | (25/100) 25% | (50/100) 50% @@ -271,115 +271,207 @@ File | Line Coverage | Statement Coverage | Function Coverage | Branch Coverage Other (2 more) | (124/200) 62% | (75/200) 38% | (100/200) 50% | (75/200) 38% Total | (355/500) 71% | (275/500) 55% | (200/500) 40% | (175/500) 35% ` - ) - }) + ); + }); + + it('fails the build when reportMode is set to FAIL and coverage is below threshold', async () => { + await istanbulCoverage({ + reportMode: 'fail' + }); + expect(global.fail).toBeCalled(); + }); + + it('passes the build when reportMode is set to FAIL and coverage is above threshold', async () => { + await istanbulCoverage({ + reportMode: 'fail', + threshold: { + lines: 25, + statements: 25, + functions: 25, + branches: 25 + } + }); + expect(global.fail).not.toBeCalled(); + }); + + it('warns the build when reportMode is set to WARN and coverage is below threshold', async () => { + await istanbulCoverage({ + reportMode: 'warn' + }); + expect(global.warn).toBeCalled(); + }); - it("fails the build when reportMode is set to FAIL and coverage is below threshold", async () => { - await istanbulCoverage({ - reportMode: "fail", - }) - expect(global.fail).toBeCalled() - }) + it('passes the build when reportMode is set to WARN and coverage is above threshold', async () => { + await istanbulCoverage({ + reportMode: 'warn', + threshold: { + lines: 25, + statements: 25, + functions: 25, + branches: 25 + } + }); + expect(global.warn).not.toBeCalled(); + }); - it("passes the build when reportMode is set to FAIL and coverage is above threshold", async () => { - await istanbulCoverage({ - reportMode: "fail", - threshold: { - lines: 25, - statements: 25, - functions: 25, - branches: 25, - }, - }) - expect(global.fail).not.toBeCalled() - }) + it('logs the custom success message if one is specified and coverage is above threshold', async () => { + const customMessage = 'This is the custom message'; + await istanbulCoverage({ + reportMode: 'message', + customSuccessMessage: customMessage, + threshold: { + lines: 25, + statements: 25, + functions: 25, + branches: 25 + } + }); + expect(global.message).toBeCalledWith(customMessage); + }); - it("warns the build when reportMode is set to WARN and coverage is below threshold", async () => { - await istanbulCoverage({ - reportMode: "warn", - }) - expect(global.warn).toBeCalled() - }) + it('logs the custom failure message if one is specified and coverage is below threshold', async () => { + const customMessage = 'This is the custom message'; + await istanbulCoverage({ + reportMode: 'message', + customFailureMessage: customMessage + }); + expect(global.message).toBeCalledWith(customMessage); + }); - it("passes the build when reportMode is set to WARN and coverage is above threshold", async () => { - await istanbulCoverage({ - reportMode: "warn", - threshold: { - lines: 25, - statements: 25, - functions: 25, - branches: 25, - }, - }) - expect(global.warn).not.toBeCalled() - }) + it('doesn\'t output anything when reportFileSet is set to "created" and there are no created files ', async () => { + global.danger.git.created_files = []; + await istanbulCoverage({ + reportMode: 'fail', + reportFileSet: 'created' + }); + expect(global.fail).not.toBeCalled(); + expect(global.warn).not.toBeCalled(); + expect(global.message).not.toBeCalled(); + }); - it("logs the custom success message if one is specified and coverage is above threshold", async () => { - const customMessage = "This is the custom message" - await istanbulCoverage({ - reportMode: "message", - customSuccessMessage: customMessage, - threshold: { - lines: 25, - statements: 25, - functions: 25, - branches: 25, - }, - }) - expect(global.message).toBeCalledWith(customMessage) - }) + it('doesn\'t output anything when reportFileSet is set to "modified" and there are no modified files ', async () => { + global.danger.git.modified_files = []; + await istanbulCoverage({ + reportMode: 'fail', + reportFileSet: 'modified' + }); + expect(global.fail).not.toBeCalled(); + expect(global.warn).not.toBeCalled(); + expect(global.message).not.toBeCalled(); + }); + it("doesn't output anything when the coverage data is empty", async () => { + setupCoverageFile(['{}']); + await istanbulCoverage({ + reportMode: 'fail' + }); + expect(global.fail).not.toBeCalled(); + expect(global.warn).not.toBeCalled(); + expect(global.message).not.toBeCalled(); + }); + it("outputs a warning when it can't find the coverage file", async () => { + setupCoverageFile([]); + await istanbulCoverage({ + reportMode: 'warn' + }); + expect(global.warn).toBeCalled(); + }); + it('outputs a warning when coverage file is invalidly formatted', async () => { + setupCoverageFile(['{']); + await istanbulCoverage({ + reportMode: 'fail' + }); + expect(global.warn).toBeCalled(); + }); +}); - it("logs the custom failure message if one is specified and coverage is below threshold", async () => { - const customMessage = "This is the custom message" - await istanbulCoverage({ - reportMode: "message", - customFailureMessage: customMessage, - }) - expect(global.message).toBeCalledWith(customMessage) - }) +describe('istanbulCoverage() - with useAbsolutePath as false for lcov (Force change)', () => { + beforeEach(() => { + global.warn = jest.fn(); + global.message = jest.fn(); + global.fail = jest.fn(); + global.markdown = jest.fn(); + global.danger = { + git: { + modified_files: ['src/modified-file1.ts', 'src/modified-file2.ts'], + created_files: ['src/created-file1.ts', 'src/created-file2.ts'] + } + }; + setupGitService(); + setupCoverageFile([ + `{ + ${makeEntry('total', 50, 50, 50, 50)}, + ${makeEntry(`${__dirname}/src/modified-file1.ts`, 66, 25, 25, 25)}, + ${makeEntry(`${__dirname}/src/modified-file2.ts`, 99, 50, 75, 50)}, + ${makeEntry(`${__dirname}/src/created-file1.ts`, 66, 100, 25, 50)}, + ${makeEntry(`${__dirname}/src/created-file2.ts`, 99, 75, 50, 25)}, + ${makeEntry(`${__dirname}/src/unmodified-field.ts`, 25, 25, 25, 25)} + }` + ]); + }); - it('doesn\'t output anything when reportFileSet is set to "created" and there are no created files ', async () => { - global.danger.git.created_files = [] - await istanbulCoverage({ - reportMode: "fail", - reportFileSet: "created", - }) - expect(global.fail).not.toBeCalled() - expect(global.warn).not.toBeCalled() - expect(global.message).not.toBeCalled() - }) + afterEach(() => { + global.warn = undefined; + global.message = undefined; + global.fail = undefined; + global.markdown = undefined; + jest.resetAllMocks(); + }); + + it('will automatically infer the lcov source type', async () => { + setupCoverageFile([ + `TN: +SF: src/created-file1.ts +FN: 1, func1 +FNDA: 1, func1 +FNH: 1 +FNF: 1 +BRF: 8 +BRH: 4 +LH: 15 +LF: 20 +end_of_record` + ]); + await istanbulCoverage({ + coveragePath: 'lcov.info', + reportFileSet: 'created', + useAbsolutePath: false + }); + expect(global.markdown).toHaveBeenCalledWith( + `## Coverage in New Files +File | Line Coverage | Statement Coverage | Function Coverage | Branch Coverage +---- | ------------: | -----------------: | ----------------: | --------------: +[src/created\\-file1.ts](../blob/master/src/created\\-file1.ts) | (15/20) 75% | (15/20) 75% | (1/1) 100% | (4/8) 50% +Total | (15/20) 75% | (15/20) 75% | (1/1) 100% | (4/8) 50% +` + ); + }); - it('doesn\'t output anything when reportFileSet is set to "modified" and there are no modified files ', async () => { - global.danger.git.modified_files = [] - await istanbulCoverage({ - reportMode: "fail", - reportFileSet: "modified", - }) - expect(global.fail).not.toBeCalled() - expect(global.warn).not.toBeCalled() - expect(global.message).not.toBeCalled() - }) - it("doesn't output anything when the coverage data is empty", async () => { - setupCoverageFile(["{}"]) - await istanbulCoverage({ - reportMode: "fail", - }) - expect(global.fail).not.toBeCalled() - expect(global.warn).not.toBeCalled() - expect(global.message).not.toBeCalled() - }) - it("outputs a warning when it can't find the coverage file", async () => { - setupCoverageFile([]) - await istanbulCoverage({ - reportMode: "warn", - }) - expect(global.warn).toBeCalled() - }) - it("outputs a warning when coverage file is invalidly formatted", async () => { - setupCoverageFile(["{"]) - await istanbulCoverage({ - reportMode: "fail", - }) - expect(global.warn).toBeCalled() - }) -}) + it('will use the lcov source type when specified explicitly', async () => { + setupCoverageFile([ + `TN: +SF: src/created-file1.ts +FN: 1, func1 +FNDA: 1, func1 +FNH: 1 +FNF: 1 +BRF: 8 +BRH: 4 +LH: 15 +LF: 20 +end_of_record` + ]); + await istanbulCoverage({ + coveragePath: { path: 'some.path', type: 'lcov' }, + reportFileSet: 'created', + useAbsolutePath: false + }); + expect(global.markdown).toHaveBeenCalledWith( + `## Coverage in New Files +File | Line Coverage | Statement Coverage | Function Coverage | Branch Coverage +---- | ------------: | -----------------: | ----------------: | --------------: +[src/created\\-file1.ts](../blob/master/src/created\\-file1.ts) | (15/20) 75% | (15/20) 75% | (1/1) 100% | (4/8) 50% +Total | (15/20) 75% | (15/20) 75% | (1/1) 100% | (4/8) 50% +` + ); + }); +}); diff --git a/src/index.ts b/src/index.ts index e7d7820..d08a8f7 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,233 +1,283 @@ // Provides dev-time type structures for `danger` - doesn't affect runtime. -import { DangerDSLType } from "../node_modules/danger/distribution/dsl/DangerDSL" +import { DangerDSLType } from '../node_modules/danger/distribution/dsl/DangerDSL'; import { - Config, - CoverageThreshold, - makeCompleteConfiguration, - ReportFileSet, - ReportMode, - SourcePath, - SourcePathExplicit, - SourceType, -} from "./config.model" + Config, + CoverageThreshold, + makeCompleteConfiguration, + ReportFileSet, + ReportMode, + SourcePath, + SourcePathExplicit, + SourceType +} from './config.model'; import { - CoverageCollection, - CoverageEntry, - CoverageItem, - CoverageModel, - makeCoverageModel, - meetsThreshold, -} from "./coverage.model" -import { parseJsonSummary } from "./parser/parse-json-summary" - -declare var danger: DangerDSLType -import * as _ from "lodash" -import * as path from "path" -import { escapeMarkdownCharacters, getPrettyPathName } from "./filename-utils" -import { GitService } from "./git.service" -import { parseLcov } from "./parser/parse-lcov" - -export declare function message(message: string): void -export declare function warn(message: string): void -export declare function fail(message: string): void -export declare function markdown(message: string): void - -function filterForCoveredFiles(basePath: string, files: string[], coverage: CoverageCollection): string[] { - return files.map(filename => path.resolve(basePath, filename)).filter(filename => coverage[filename] !== undefined) + CoverageCollection, + CoverageEntry, + CoverageItem, + CoverageModel, + makeCoverageModel, + meetsThreshold +} from './coverage.model'; +import { parseJsonSummary } from './parser/parse-json-summary'; +import * as _ from 'lodash'; +import * as path from 'path'; +import { escapeMarkdownCharacters, getPrettyPathName } from './filename-utils'; +import { GitService } from './git.service'; +import { parseLcov } from './parser/parse-lcov'; + +declare let danger: DangerDSLType; +export declare function message(message: string): void; +export declare function warn(message: string): void; +export declare function fail(message: string): void; +export declare function markdown(message: string): void; + +function filterForCoveredFiles( + basePath: string, + files: string[], + coverage: CoverageCollection, + useAbs: boolean +): string[] { + let newFiles = files; + if (useAbs) { + newFiles = files.map(filename => path.resolve(basePath, filename)); + } + return newFiles.filter(filename => coverage[filename] !== undefined); } -function getFileSet(reportChangeType: ReportFileSet, all: string[], modified: string[], created: string[]): string[] { - if (reportChangeType === "all") { - return all - } - if (reportChangeType === "modified") { - return modified - } - if (reportChangeType === "created") { - return created - } - return _.union(created, modified) +function getFileSet( + reportChangeType: ReportFileSet, + all: string[], + modified: string[], + created: string[] +): string[] { + if (reportChangeType === 'all') { + return all; + } + if (reportChangeType === 'modified') { + return modified; + } + if (reportChangeType === 'created') { + return created; + } + return _.union(created, modified); } function getReportFunc(reportMode: ReportMode) { - if (reportMode === "warn") { - return warn - } - if (reportMode === "fail") { - return fail - } - return message + if (reportMode === 'warn') { + return warn; + } + if (reportMode === 'fail') { + return fail; + } + return message; } function getFileGroupLongDescription(reportChangeType: ReportFileSet) { - if (reportChangeType === "all") { - return "the whole codebase" - } - if (reportChangeType === "created") { - return "the new files in this PR" - } - if (reportChangeType === "modified") { - return "the modified files in this PR" - } - return "the modified or changed files in this PR" + if (reportChangeType === 'all') { + return 'the whole codebase'; + } + if (reportChangeType === 'created') { + return 'the new files in this PR'; + } + if (reportChangeType === 'modified') { + return 'the modified files in this PR'; + } + return 'the modified or changed files in this PR'; } function getFileGroupShortDescription(reportChangeType: ReportFileSet) { - if (reportChangeType === "all") { - return "All Files" - } - if (reportChangeType === "created") { - return "New Files" - } - if (reportChangeType === "modified") { - return "Modified Files" - } - return "Created or Modified Files" + if (reportChangeType === 'all') { + return 'All Files'; + } + if (reportChangeType === 'created') { + return 'New Files'; + } + if (reportChangeType === 'modified') { + return 'Modified Files'; + } + return 'Created or Modified Files'; } function sendPRComment(config: Config, results: CoverageEntry) { - const reportFunc = getReportFunc(config.reportMode) - const messageType = getFileGroupLongDescription(config.reportFileSet) - if (!meetsThreshold(results, config.threshold)) { - const defaultMessage = `🤔 Hmmm, code coverage is looking low for ${messageType}.` - reportFunc(config.customFailureMessage !== undefined ? config.customFailureMessage : defaultMessage) - } else { - const defaultMessage = `🎉 Test coverage is looking good for ${messageType}` - message(config.customSuccessMessage !== undefined ? config.customSuccessMessage : defaultMessage) - } + const reportFunc = getReportFunc(config.reportMode); + const messageType = getFileGroupLongDescription(config.reportFileSet); + if (!meetsThreshold(results, config.threshold)) { + const defaultMessage = `🤔 Hmmm, code coverage is looking low for ${messageType}.`; + reportFunc( + config.customFailureMessage !== undefined ? config.customFailureMessage : defaultMessage + ); + } else { + const defaultMessage = `🎉 Test coverage is looking good for ${messageType}`; + message( + config.customSuccessMessage !== undefined ? config.customSuccessMessage : defaultMessage + ); + } } function formatItem(item: CoverageItem) { - return `(${item.covered}/${item.total}) ${item.pct.toFixed(0)}%` + return `(${item.covered}/${item.total}) ${item.pct.toFixed(0)}%`; } function formatSourceName(source: string): string { - return escapeMarkdownCharacters(getPrettyPathName(source, 30)) + return escapeMarkdownCharacters(getPrettyPathName(source, 30)); } function formatLinkName(source: string, branchName: string): string { - return escapeMarkdownCharacters(`../blob/${branchName}/${source}`) + return escapeMarkdownCharacters(`../blob/${branchName}/${source}`); } -function generateReport(basePath: string, branch: string, coverage: CoverageModel, reportChangeType: ReportFileSet) { - const header = `## Coverage in ${getFileGroupShortDescription(reportChangeType)} +function generateReport( + basePath: string, + branch: string, + coverage: CoverageModel, + reportChangeType: ReportFileSet, + useAbsolutePath: boolean +) { + const header = `## Coverage in ${getFileGroupShortDescription(reportChangeType)} File | Line Coverage | Statement Coverage | Function Coverage | Branch Coverage ----- | ------------: | -----------------: | ----------------: | --------------:` - - const lines = Object.keys(coverage.displayed).map(filename => { - const e = coverage.displayed[filename] - const shortFilename = formatSourceName(path.relative(basePath, filename)) - const linkFilename = formatLinkName(path.relative(basePath, filename), branch) - return [ - `[${shortFilename}](${linkFilename})`, - formatItem(e.lines), - formatItem(e.statements), - formatItem(e.functions), - formatItem(e.branches), - ].join(" | ") - }) - - const ellided = - coverage.elidedCount === 0 - ? undefined - : [ - `Other (${coverage.elidedCount} more)`, - formatItem(coverage.elided.lines), - formatItem(coverage.elided.statements), - formatItem(coverage.elided.functions), - formatItem(coverage.elided.branches), - ].join(" | ") - - const total = [ - "Total", - formatItem(coverage.total.lines), - formatItem(coverage.total.statements), - formatItem(coverage.total.functions), - formatItem(coverage.total.branches), - ].join(" | ") - return [header, ...lines, ellided, total, ""].filter(part => part !== undefined).join("\n") +---- | ------------: | -----------------: | ----------------: | --------------:`; + + const lines = Object.keys(coverage.displayed).map(filename => { + const e = coverage.displayed[filename]; + const srcPath = useAbsolutePath ? path.relative(basePath, filename) : filename; + const linkPath = useAbsolutePath ? path.relative(basePath, filename) : filename; + const shortFilename = formatSourceName(srcPath); + const linkFilename = formatLinkName(linkPath, branch); + return [ + `[${shortFilename}](${linkFilename})`, + formatItem(e.lines), + formatItem(e.statements), + formatItem(e.functions), + formatItem(e.branches) + ].join(' | '); + }); + + const ellided = + coverage.elidedCount === 0 + ? undefined + : [ + `Other (${coverage.elidedCount} more)`, + formatItem(coverage.elided.lines), + formatItem(coverage.elided.statements), + formatItem(coverage.elided.functions), + formatItem(coverage.elided.branches) + ].join(' | '); + + const total = [ + 'Total', + formatItem(coverage.total.lines), + formatItem(coverage.total.statements), + formatItem(coverage.total.functions), + formatItem(coverage.total.branches) + ].join(' | '); + return [header, ...lines, ellided, total, ''].filter(part => part !== undefined).join('\n'); } function getCoveragePaths(coveragePaths: SourcePath[]): SourcePathExplicit[] { - return coveragePaths.map(singleCoveragePath => { - let originalPath: string - let type: SourceType - if (typeof singleCoveragePath === "string") { - originalPath = singleCoveragePath - type = singleCoveragePath.match(/(lcov\.info)$/) ? "lcov" : "json-summary" - } else { - originalPath = singleCoveragePath.path - type = singleCoveragePath.type - } - if (!process.mainModule) { - return { path: originalPath, type } - } - const appDir = `${process.mainModule.paths[0].split("node_modules")[0].slice(0, -1)}/` - originalPath = path.resolve(appDir, originalPath) - const output: SourcePathExplicit = { path: originalPath, type } - return output - }) + return coveragePaths.map(singleCoveragePath => { + let originalPath: string; + let type: SourceType; + + if (typeof singleCoveragePath === 'string') { + originalPath = singleCoveragePath; + type = singleCoveragePath.match(/(lcov\.info)$/) ? 'lcov' : 'json-summary'; + } else { + originalPath = singleCoveragePath.path; + type = singleCoveragePath.type; + } + if (!process.mainModule) { + return { path: originalPath, type }; + } + const appDir = `${process.mainModule.paths[0].split('node_modules')[0].slice(0, -1)}/`; + originalPath = path.resolve(appDir, originalPath); + const output: SourcePathExplicit = { path: originalPath, type }; + return output; + }); } function parseSourcePath(sourcePath: SourcePathExplicit): CoverageCollection { - if (sourcePath.type === "json-summary") { - return parseJsonSummary(sourcePath.path) - } else { - return parseLcov(sourcePath.path) - } + if (sourcePath.type === 'json-summary') { + return parseJsonSummary(sourcePath.path); + } else { + return parseLcov(sourcePath.path); + } } function getCombinedCoverageCollection(coveragePaths: SourcePathExplicit[]): CoverageCollection { - return coveragePaths - .map(coveragePath => parseSourcePath(coveragePath)) - .reduce((previous, current) => ({ ...previous, ...current }), {}) + return coveragePaths + .map(coveragePath => parseSourcePath(coveragePath)) + .reduce((previous, current) => ({ ...previous, ...current }), {}); } /** * Danger.js plugin for monitoring code coverage on changed files. */ export function istanbulCoverage(config?: Partial): Promise { - const combinedConfig = makeCompleteConfiguration(config) - - const coveragePaths = getCoveragePaths(combinedConfig.coveragePaths) + const combinedConfig = makeCompleteConfiguration(config); + const coveragePaths = getCoveragePaths(combinedConfig.coveragePaths); + let coverage: CoverageCollection; - let coverage: CoverageCollection - try { - const parsedCoverage = getCombinedCoverageCollection(coveragePaths) - if (!parsedCoverage) { - return Promise.resolve() - } - coverage = parsedCoverage - } catch (error) { - warn(error.message) - return Promise.resolve() - } - const gitService = new GitService() - - const gitProperties = Promise.all([gitService.getRootDirectory(), gitService.getCurrentCommit()]) - - return gitProperties.then(values => { - const gitRoot = values[0] - const gitBranch = values[1] - const modifiedFiles = filterForCoveredFiles(gitRoot, danger.git.modified_files, coverage) - const createdFiles = filterForCoveredFiles(gitRoot, danger.git.created_files, coverage) - const allFiles = Object.keys(coverage).filter(filename => filename !== "total") - - const files = getFileSet(combinedConfig.reportFileSet, allFiles, modifiedFiles, createdFiles) - - if (files.length === 0) { - return + try { + const parsedCoverage = getCombinedCoverageCollection(coveragePaths); + if (!parsedCoverage) { + return Promise.resolve(); + } + coverage = parsedCoverage; + } catch (error) { + warn(error.message); + return Promise.resolve(); } + const gitService = new GitService(); + + const gitProperties = Promise.all([ + gitService.getRootDirectory(), + gitService.getCurrentCommit() + ]); + + return gitProperties.then(values => { + const gitRoot = values[0]; + const gitBranch = values[1]; + const useAbs = combinedConfig.useAbsolutePath; + const modifiedFiles = filterForCoveredFiles( + gitRoot, + danger.git.modified_files, + coverage, + useAbs + ); + const createdFiles = filterForCoveredFiles( + gitRoot, + danger.git.created_files, + coverage, + useAbs + ); + const allFiles = Object.keys(coverage).filter(filename => filename !== 'total'); + + const files = getFileSet( + combinedConfig.reportFileSet, + allFiles, + modifiedFiles, + createdFiles + ); + + if (files.length === 0) { + return; + } + + const coverageModel = makeCoverageModel( + combinedConfig.numberOfEntries, + files, + coverage, + combinedConfig.entrySortMethod + ); + sendPRComment(combinedConfig, coverageModel.total); - const coverageModel = makeCoverageModel( - combinedConfig.numberOfEntries, - files, - coverage, - combinedConfig.entrySortMethod - ) - sendPRComment(combinedConfig, coverageModel.total) - - const report = generateReport(gitRoot, gitBranch, coverageModel, combinedConfig.reportFileSet) - markdown(report) - }) + const report = generateReport( + gitRoot, + gitBranch, + coverageModel, + combinedConfig.reportFileSet, + combinedConfig.useAbsolutePath + ); + markdown(report); + }); }