diff --git a/README.md b/README.md index 95a3761f..708e7bfa 100644 --- a/README.md +++ b/README.md @@ -28,6 +28,18 @@ The location of the lcov file to read the coverage report from. Defaults to The location of the lcov file resulting from running the tests in the base branch. When this is set a diff of the coverage percentages is shown. +##### `filter-changed-files` (**Default: false**) +If set to true, only changed files will be included in the report. Total percentage will still include all files. + +##### `delete-old-comments` (**Default: false**) +If set to true, old comments will be deleted before a new comment is posted + +##### `title` (**Optional**) +If included, will be added as a title for the comment produced. + +##### `max-uncovered-lines` (**Optional**) +If included, will limit the number of uncovered lines displayed in the Uncovered Lines column. + ## Example usage ```yml @@ -39,3 +51,5 @@ with: ## Acknowledgements The initial code is based on [ziishaned/jest-reporter-action](https://github.com/ziishaned/jest-reporter-action). + +Changed file retrieval based on [jitterbit/get-changed-files](https://github.com/jitterbit/get-changed-files). diff --git a/action.yml b/action.yml index 159975ff..8630f5f7 100644 --- a/action.yml +++ b/action.yml @@ -15,6 +15,20 @@ inputs: lcov-base: description: The location of the lcov file for the base branch required: false + filter-changed-files: + description: Set to true to only comment with coverage on files changed in this commit + required: false + default: false + delete-old-comments: + description: Set to true to delete old Coverage Report comments + required: false + default: false + title: + description: Title to add to the comment + required: false + max-uncovered-lines: + description: Max number of uncovered lines to display in uncovered lines column (integer) + required: false runs: using: node12 main: dist/main.js diff --git a/dist/main.js b/dist/main.js index ba98502a..4b3a55bd 100644 --- a/dist/main.js +++ b/dist/main.js @@ -22784,11 +22784,16 @@ const b = tag("b"); const table = tag("table"); const tbody = tag("tbody"); const a = tag("a"); +const h2 = tag("h2"); const fragment = function(...children) { return children.join("") }; +function normalisePath(file) { + return file.replace(/\\/g, "/") +} + // Tabulate the lcov data in a HTML table. function tabulate(lcov, options) { const head = tr( @@ -22801,7 +22806,7 @@ function tabulate(lcov, options) { ); const folders = {}; - for (const file of lcov) { + for (const file of filterAndNormaliseLcov(lcov, options)) { const parts = file.file.replace(options.prefix, "").split("/"); const folder = parts.slice(0, -1).join("/"); folders[folder] = folders[folder] || []; @@ -22822,6 +22827,22 @@ function tabulate(lcov, options) { return table(tbody(head, ...rows)) } +function filterAndNormaliseLcov(lcov, options) { + return lcov + .map(file => ({ + ...file, + file: normalisePath(file.file), + })) + .filter(file => shouldBeIncluded(file.file, options)) +} + +function shouldBeIncluded(fileName, options) { + if (!options.shouldFilterChangedFiles) { + return true + } + return options.changedFiles.includes(fileName.replace(options.prefix, "")) +} + function toFolder(path) { if (path === "") { return "" @@ -22833,16 +22854,19 @@ function toFolder(path) { function getStatement(file) { const { branches, functions, lines } = file; - return [branches, functions, lines].reduce(function(acc, curr) { - if (!curr) { - return acc - } + return [branches, functions, lines].reduce( + function(acc, curr) { + if (!curr) { + return acc + } - return { - hit: acc.hit + curr.hit, - found: acc.found + curr.found, - } - }, { hit: 0, found: 0 }) + return { + hit: acc.hit + curr.hit, + found: acc.found + curr.found, + } + }, + { hit: 0, found: 0 }, + ) } function toRow(file, indent, options) { @@ -22889,17 +22913,34 @@ function uncovered(file, options) { const all = ranges([...branches, ...lines]); + var numNotIncluded = 0; + if (options.maxUncoveredLines) { + const notIncluded = all.splice(options.maxUncoveredLines); + numNotIncluded = notIncluded.length; + } - return all + const result = all .map(function(range) { - const fragment = range.start === range.end ? `L${range.start}` : `L${range.start}-L${range.end}`; + const fragment = + range.start === range.end + ? `L${range.start}` + : `L${range.start}-L${range.end}`; const relative = file.file.replace(options.prefix, ""); const href = `https://github.com/${options.repository}/blob/${options.commit}/${relative}#${fragment}`; - const text = range.start === range.end ? range.start : `${range.start}–${range.end}`; + const text = + range.start === range.end + ? range.start + : `${range.start}–${range.end}`; return a({ href }, text) }) - .join(", ") + .join(", "); + + if (numNotIncluded > 0) { + return result + ` and ${numNotIncluded} more...` + } else { + return result + } } function ranges(linenos) { @@ -22929,14 +22970,24 @@ function ranges(linenos) { return res } -function comment (lcov, options) { +function comment(lcov, options) { return fragment( + options.title ? h2(options.title) : "", options.base - ? `Coverage after merging ${b(options.head)} into ${b(options.base)}` + ? `Coverage after merging ${b(options.head)} into ${b( + options.base, + )} will be` : `Coverage for this commit`, table(tbody(tr(th(percentage(lcov).toFixed(2), "%")))), "\n\n", - details(summary("Coverage Report"), tabulate(lcov, options)), + details( + summary( + options.shouldFilterChangedFiles + ? "Coverage Report for Changed Files" + : "Coverage Report", + ), + tabulate(lcov, options), + ), ) } @@ -22949,30 +23000,122 @@ function diff(lcov, before, options) { const pafter = percentage(lcov); const pdiff = pafter - pbefore; const plus = pdiff > 0 ? "+" : ""; - const arrow = - pdiff === 0 - ? "" - : pdiff < 0 - ? "▾" - : "▴"; + const arrow = pdiff === 0 ? "" : pdiff < 0 ? "▾" : "▴"; return fragment( + options.title ? h2(options.title) : "", options.base - ? `Coverage after merging ${b(options.head)} into ${b(options.base)}` + ? `Coverage after merging ${b(options.head)} into ${b( + options.base, + )} will be` : `Coverage for this commit`, - table(tbody(tr( - th(pafter.toFixed(2), "%"), - th(arrow, " ", plus, pdiff.toFixed(2), "%"), - ))), + table( + tbody( + tr( + th(pafter.toFixed(2), "%"), + th(arrow, " ", plus, pdiff.toFixed(2), "%"), + ), + ), + ), "\n\n", - details(summary("Coverage Report"), tabulate(lcov, options)), + details( + summary( + options.shouldFilterChangedFiles + ? "Coverage Report for Changed Files" + : "Coverage Report", + ), + tabulate(lcov, options), + ), ) } +// Get list of changed files +async function getChangedFiles(githubClient, options, context) { + if (!options.commit || !options.baseCommit) { + core_7( + `The base and head commits are missing from the payload for this ${context.eventName} event.`, + ); + } + + // Use GitHub's compare two commits API. + // https://developer.github.com/v3/repos/commits/#compare-two-commits + const response = await githubClient.repos.compareCommits({ + base: options.baseCommit, + head: options.commit, + owner: context.repo.owner, + repo: context.repo.repo, + }); + + if (response.status !== 200) { + core_7( + `The GitHub API for comparing the base and head commits for this ${context.eventName} event returned ${response.status}, expected 200.`, + ); + } + + return response.data.files + .filter(file => file.status == "modified" || file.status == "added") + .map(file => file.filename) +} + +const REQUESTED_COMMENTS_PER_PAGE = 20; + +async function deleteOldComments(github, options, context) { + const existingComments = await getExistingComments(github, options, context); + for (const comment of existingComments) { + core_8(`Deleting comment: ${comment.id}`); + try { + await github.issues.deleteComment({ + owner: context.repo.owner, + repo: context.repo.repo, + comment_id: comment.id, + }); + } catch (error) { + console.error(error); + } + } +} + +async function getExistingComments(github, options, context) { + let page = 0; + let results = []; + let response; + do { + response = await github.issues.listComments({ + issue_number: context.issue.number, + owner: context.repo.owner, + repo: context.repo.repo, + per_page: REQUESTED_COMMENTS_PER_PAGE, + page: page, + }); + results = results.concat(response.data); + page++; + } while (response.data.length === REQUESTED_COMMENTS_PER_PAGE) + + return results.filter( + comment => + !!comment.user && + (!options.title || comment.body.includes(options.title)) && + comment.body.includes("Coverage Report"), + ) +} + +const MAX_COMMENT_CHARS = 65536; + async function main$1() { const token = core$1.getInput("github-token"); + const githubClient = new github_2(token); const lcovFile = core$1.getInput("lcov-file") || "./coverage/lcov.info"; const baseFile = core$1.getInput("lcov-base"); + const shouldFilterChangedFiles = core$1.getInput("filter-changed-files"); + const shouldDeleteOldComments = core$1.getInput("delete-old-comments"); + const title = core$1.getInput("title"); + const maxUncoveredLines = core$1.getInput("max-uncovered-lines"); + if (maxUncoveredLines && isNaN(parseInt(maxUncoveredLines))) { + console.log( + `Invalid parameter for max-uncovered-lines '${maxUncoveredLines}'. Must be an integer. Exiting...`, + ); + return + } const raw = await fs.promises.readFile(lcovFile, "utf-8").catch(err => null); if (!raw) { @@ -22980,42 +23123,59 @@ async function main$1() { return } - const baseRaw = baseFile && await fs.promises.readFile(baseFile, "utf-8").catch(err => null); + const baseRaw = + baseFile && (await fs.promises.readFile(baseFile, "utf-8").catch(err => null)); if (baseFile && !baseRaw) { console.log(`No coverage report found at '${baseFile}', ignoring...`); } const options = { repository: github_1.payload.repository.full_name, - prefix: `${process.env.GITHUB_WORKSPACE}/`, + prefix: normalisePath(`${process.env.GITHUB_WORKSPACE}/`), }; if (github_1.eventName === "pull_request") { options.commit = github_1.payload.pull_request.head.sha; + options.baseCommit = github_1.payload.pull_request.base.sha; options.head = github_1.payload.pull_request.head.ref; options.base = github_1.payload.pull_request.base.ref; } else if (github_1.eventName === "push") { options.commit = github_1.payload.after; + options.baseCommit = github_1.payload.before; options.head = github_1.ref; } + options.shouldFilterChangedFiles = shouldFilterChangedFiles; + options.title = title; + if (maxUncoveredLines) { + options.maxUncoveredLines = parseInt(maxUncoveredLines); + } + + if (shouldFilterChangedFiles) { + options.changedFiles = await getChangedFiles(githubClient, options, github_1); + } + const lcov = await parse$2(raw); - const baselcov = baseRaw && await parse$2(baseRaw); - const body = diff(lcov, baselcov, options); + const baselcov = baseRaw && (await parse$2(baseRaw)); + const body = diff(lcov, baselcov, options).substring(0, MAX_COMMENT_CHARS); + + if (shouldDeleteOldComments) { + await deleteOldComments(githubClient, options, github_1); + } if (github_1.eventName === "pull_request") { - await new github_2(token).issues.createComment({ + await githubClient.issues.createComment({ repo: github_1.repo.repo, owner: github_1.repo.owner, issue_number: github_1.payload.pull_request.number, - body: diff(lcov, baselcov, options), + body: body, }); } else if (github_1.eventName === "push") { - await new github_2(token).repos.createCommitComment({ + await githubClient.repos.createCommitComment({ repo: github_1.repo.repo, owner: github_1.repo.owner, commit_sha: options.commit, - body: diff(lcov, baselcov, options), + body: body, }); } } diff --git a/src/comment.js b/src/comment.js index 2dd01571..73cf3dd5 100644 --- a/src/comment.js +++ b/src/comment.js @@ -1,16 +1,26 @@ -import { details, summary, b, fragment, table, tbody, tr, th } from "./html" +import { details, summary, b, fragment, table, tbody, tr, th, h2 } from "./html" import { percentage } from "./lcov" import { tabulate } from "./tabulate" -export function comment (lcov, options) { +export function comment(lcov, options) { return fragment( + options.title ? h2(options.title) : "", options.base - ? `Coverage after merging ${b(options.head)} into ${b(options.base)}` + ? `Coverage after merging ${b(options.head)} into ${b( + options.base, + )} will be` : `Coverage for this commit`, table(tbody(tr(th(percentage(lcov).toFixed(2), "%")))), "\n\n", - details(summary("Coverage Report"), tabulate(lcov, options)), + details( + summary( + options.shouldFilterChangedFiles + ? "Coverage Report for Changed Files" + : "Coverage Report", + ), + tabulate(lcov, options), + ), ) } @@ -23,22 +33,31 @@ export function diff(lcov, before, options) { const pafter = percentage(lcov) const pdiff = pafter - pbefore const plus = pdiff > 0 ? "+" : "" - const arrow = - pdiff === 0 - ? "" - : pdiff < 0 - ? "▾" - : "▴" + const arrow = pdiff === 0 ? "" : pdiff < 0 ? "▾" : "▴" return fragment( + options.title ? h2(options.title) : "", options.base - ? `Coverage after merging ${b(options.head)} into ${b(options.base)}` + ? `Coverage after merging ${b(options.head)} into ${b( + options.base, + )} will be` : `Coverage for this commit`, - table(tbody(tr( - th(pafter.toFixed(2), "%"), - th(arrow, " ", plus, pdiff.toFixed(2), "%"), - ))), + table( + tbody( + tr( + th(pafter.toFixed(2), "%"), + th(arrow, " ", plus, pdiff.toFixed(2), "%"), + ), + ), + ), "\n\n", - details(summary("Coverage Report"), tabulate(lcov, options)), + details( + summary( + options.shouldFilterChangedFiles + ? "Coverage Report for Changed Files" + : "Coverage Report", + ), + tabulate(lcov, options), + ), ) } diff --git a/src/delete_old_comments.js b/src/delete_old_comments.js new file mode 100644 index 00000000..32272513 --- /dev/null +++ b/src/delete_old_comments.js @@ -0,0 +1,43 @@ +import * as core from "@actions/core" + +const REQUESTED_COMMENTS_PER_PAGE = 20 + +export async function deleteOldComments(github, options, context) { + const existingComments = await getExistingComments(github, options, context) + for (const comment of existingComments) { + core.debug(`Deleting comment: ${comment.id}`) + try { + await github.issues.deleteComment({ + owner: context.repo.owner, + repo: context.repo.repo, + comment_id: comment.id, + }) + } catch (error) { + console.error(error) + } + } +} + +async function getExistingComments(github, options, context) { + let page = 0 + let results = [] + let response + do { + response = await github.issues.listComments({ + issue_number: context.issue.number, + owner: context.repo.owner, + repo: context.repo.repo, + per_page: REQUESTED_COMMENTS_PER_PAGE, + page: page, + }) + results = results.concat(response.data) + page++ + } while (response.data.length === REQUESTED_COMMENTS_PER_PAGE) + + return results.filter( + comment => + !!comment.user && + (!options.title || comment.body.includes(options.title)) && + comment.body.includes("Coverage Report"), + ) +} diff --git a/src/get_changes.js b/src/get_changes.js new file mode 100644 index 00000000..d8f322cb --- /dev/null +++ b/src/get_changes.js @@ -0,0 +1,29 @@ +import * as core from "@actions/core" + +// Get list of changed files +export async function getChangedFiles(githubClient, options, context) { + if (!options.commit || !options.baseCommit) { + core.setFailed( + `The base and head commits are missing from the payload for this ${context.eventName} event.`, + ) + } + + // Use GitHub's compare two commits API. + // https://developer.github.com/v3/repos/commits/#compare-two-commits + const response = await githubClient.repos.compareCommits({ + base: options.baseCommit, + head: options.commit, + owner: context.repo.owner, + repo: context.repo.repo, + }) + + if (response.status !== 200) { + core.setFailed( + `The GitHub API for comparing the base and head commits for this ${context.eventName} event returned ${response.status}, expected 200.`, + ) + } + + return response.data.files + .filter(file => file.status == "modified" || file.status == "added") + .map(file => file.filename) +} diff --git a/src/html.js b/src/html.js index 1c9a95c9..4f88e8cc 100644 --- a/src/html.js +++ b/src/html.js @@ -23,6 +23,7 @@ export const table = tag("table") export const tbody = tag("tbody") export const a = tag("a") export const span = tag("span") +export const h2 = tag("h2") export const fragment = function(...children) { return children.join("") diff --git a/src/index.js b/src/index.js index 18f17a2f..823d596c 100644 --- a/src/index.js +++ b/src/index.js @@ -4,11 +4,27 @@ import { GitHub, context } from "@actions/github" import { parse } from "./lcov" import { diff } from "./comment" +import { getChangedFiles } from "./get_changes" +import { deleteOldComments } from "./delete_old_comments" +import { normalisePath } from "./util" + +const MAX_COMMENT_CHARS = 65536 async function main() { const token = core.getInput("github-token") + const githubClient = new GitHub(token) const lcovFile = core.getInput("lcov-file") || "./coverage/lcov.info" const baseFile = core.getInput("lcov-base") + const shouldFilterChangedFiles = core.getInput("filter-changed-files") + const shouldDeleteOldComments = core.getInput("delete-old-comments") + const title = core.getInput("title") + const maxUncoveredLines = core.getInput("max-uncovered-lines") + if (maxUncoveredLines && isNaN(parseInt(maxUncoveredLines))) { + console.log( + `Invalid parameter for max-uncovered-lines '${maxUncoveredLines}'. Must be an integer. Exiting...`, + ) + return + } const raw = await fs.readFile(lcovFile, "utf-8").catch(err => null) if (!raw) { @@ -16,42 +32,59 @@ async function main() { return } - const baseRaw = baseFile && await fs.readFile(baseFile, "utf-8").catch(err => null) + const baseRaw = + baseFile && (await fs.readFile(baseFile, "utf-8").catch(err => null)) if (baseFile && !baseRaw) { console.log(`No coverage report found at '${baseFile}', ignoring...`) } const options = { repository: context.payload.repository.full_name, - prefix: `${process.env.GITHUB_WORKSPACE}/`, + prefix: normalisePath(`${process.env.GITHUB_WORKSPACE}/`), } if (context.eventName === "pull_request") { options.commit = context.payload.pull_request.head.sha + options.baseCommit = context.payload.pull_request.base.sha options.head = context.payload.pull_request.head.ref options.base = context.payload.pull_request.base.ref } else if (context.eventName === "push") { options.commit = context.payload.after + options.baseCommit = context.payload.before options.head = context.ref } + options.shouldFilterChangedFiles = shouldFilterChangedFiles + options.title = title + if (maxUncoveredLines) { + options.maxUncoveredLines = parseInt(maxUncoveredLines) + } + + if (shouldFilterChangedFiles) { + options.changedFiles = await getChangedFiles(githubClient, options, context) + } + const lcov = await parse(raw) - const baselcov = baseRaw && await parse(baseRaw) - const body = diff(lcov, baselcov, options) + const baselcov = baseRaw && (await parse(baseRaw)) + const body = diff(lcov, baselcov, options).substring(0, MAX_COMMENT_CHARS) + + if (shouldDeleteOldComments) { + await deleteOldComments(githubClient, options, context) + } if (context.eventName === "pull_request") { - await new GitHub(token).issues.createComment({ + await githubClient.issues.createComment({ repo: context.repo.repo, owner: context.repo.owner, issue_number: context.payload.pull_request.number, - body: diff(lcov, baselcov, options), + body: body, }) } else if (context.eventName === "push") { - await new GitHub(token).repos.createCommitComment({ + await githubClient.repos.createCommitComment({ repo: context.repo.repo, owner: context.repo.owner, commit_sha: options.commit, - body: diff(lcov, baselcov, options), + body: body, }) } } diff --git a/src/tabulate.js b/src/tabulate.js index adacf74f..2ebee6c8 100644 --- a/src/tabulate.js +++ b/src/tabulate.js @@ -1,4 +1,5 @@ import { th, tr, td, table, tbody, a, b, span, fragment } from "./html" +import { normalisePath } from "./util" // Tabulate the lcov data in a HTML table. export function tabulate(lcov, options) { @@ -12,7 +13,7 @@ export function tabulate(lcov, options) { ) const folders = {} - for (const file of lcov) { + for (const file of filterAndNormaliseLcov(lcov, options)) { const parts = file.file.replace(options.prefix, "").split("/") const folder = parts.slice(0, -1).join("/") folders[folder] = folders[folder] || [] @@ -33,6 +34,22 @@ export function tabulate(lcov, options) { return table(tbody(head, ...rows)) } +function filterAndNormaliseLcov(lcov, options) { + return lcov + .map(file => ({ + ...file, + file: normalisePath(file.file), + })) + .filter(file => shouldBeIncluded(file.file, options)) +} + +function shouldBeIncluded(fileName, options) { + if (!options.shouldFilterChangedFiles) { + return true + } + return options.changedFiles.includes(fileName.replace(options.prefix, "")) +} + function toFolder(path) { if (path === "") { return "" @@ -44,16 +61,19 @@ function toFolder(path) { function getStatement(file) { const { branches, functions, lines } = file - return [branches, functions, lines].reduce(function(acc, curr) { - if (!curr) { - return acc - } - - return { - hit: acc.hit + curr.hit, - found: acc.found + curr.found, - } - }, { hit: 0, found: 0 }) + return [branches, functions, lines].reduce( + function(acc, curr) { + if (!curr) { + return acc + } + + return { + hit: acc.hit + curr.hit, + found: acc.found + curr.found, + } + }, + { hit: 0, found: 0 }, + ) } function toRow(file, indent, options) { @@ -100,17 +120,34 @@ function uncovered(file, options) { const all = ranges([...branches, ...lines]) + var numNotIncluded = 0 + if (options.maxUncoveredLines) { + const notIncluded = all.splice(options.maxUncoveredLines) + numNotIncluded = notIncluded.length + } - return all + const result = all .map(function(range) { - const fragment = range.start === range.end ? `L${range.start}` : `L${range.start}-L${range.end}` + const fragment = + range.start === range.end + ? `L${range.start}` + : `L${range.start}-L${range.end}` const relative = file.file.replace(options.prefix, "") const href = `https://github.com/${options.repository}/blob/${options.commit}/${relative}#${fragment}` - const text = range.start === range.end ? range.start : `${range.start}–${range.end}` + const text = + range.start === range.end + ? range.start + : `${range.start}–${range.end}` return a({ href }, text) }) .join(", ") + + if (numNotIncluded > 0) { + return result + ` and ${numNotIncluded} more...` + } else { + return result + } } function ranges(linenos) { diff --git a/src/tabulate_test.js b/src/tabulate_test.js index eee1f1b5..ee500967 100644 --- a/src/tabulate_test.js +++ b/src/tabulate_test.js @@ -190,14 +190,14 @@ test("tabulate should generate a correct table", function() { { href: `https://github.com/${options.repository}/blob/${options.commit}/src/bar/baz.js#L20-L21`, }, - '20–21', + "20–21", ), - ', ', + ", ", a( { href: `https://github.com/${options.repository}/blob/${options.commit}/src/bar/baz.js#L27`, }, - '27', + "27", ), ), ), @@ -205,3 +205,430 @@ test("tabulate should generate a correct table", function() { ) expect(tabulate(data, options)).toBe(html) }) + +test("filtered tabulate should generate a correct table with only changed files", function() { + const data = [ + { + file: "/files/project/index.js", + functions: { + found: 0, + hit: 0, + details: [], + }, + }, + { + file: "/files/project/src/foo.js", + lines: { + found: 23, + hit: 21, + details: [ + { + line: 20, + hit: 3, + }, + { + line: 21, + hit: 3, + }, + { + line: 22, + hit: 3, + }, + ], + }, + functions: { + hit: 2, + found: 3, + details: [ + { + name: "foo", + line: 19, + }, + { + name: "bar", + line: 33, + }, + { + name: "baz", + line: 54, + }, + ], + }, + branches: { + hit: 3, + found: 3, + details: [ + { + line: 21, + block: 0, + branch: 0, + taken: 1, + }, + { + line: 21, + block: 0, + branch: 1, + taken: 2, + }, + { + line: 37, + block: 1, + branch: 0, + taken: 0, + }, + ], + }, + }, + { + file: "/files/project/src/bar/baz.js", + lines: { + found: 10, + hit: 5, + details: [ + { + line: 20, + hit: 0, + }, + { + line: 21, + hit: 0, + }, + { + line: 27, + hit: 0, + }, + ], + }, + functions: { + hit: 2, + found: 3, + details: [ + { + name: "foo", + line: 19, + }, + { + name: "bar", + line: 33, + }, + { + name: "baz", + line: 54, + }, + ], + }, + }, + ] + + const options = { + repository: "example/foo", + commit: "2e15bee6fe0df5003389aa5ec894ec0fea2d874a", + prefix: "/files/project/", + shouldFilterChangedFiles: true, + changedFiles: ["src/foo.js"], + } + + const html = table( + tbody( + tr( + th("File"), + th("Stmts"), + th("Branches"), + th("Funcs"), + th("Lines"), + th("Uncovered Lines"), + ), + tr(td({ colspan: 6 }, b("src"))), + tr( + td( + "   ", + a( + { + href: `https://github.com/${options.repository}/blob/${options.commit}/src/foo.js`, + }, + "foo.js", + ), + ), + td(b("89.66%")), + td("100%"), + td(b("66.67%")), + td(b("91.30%")), + td( + a( + { + href: `https://github.com/${options.repository}/blob/${options.commit}/src/foo.js#L37`, + }, + 37, + ), + ), + ), + ), + ) + expect(tabulate(data, options)).toBe(html) +}) + +test("filtered tabulate should fix backwards slashes in filenames", function() { + const data = [ + { + file: "\\files\\project\\index.js", + functions: { + found: 0, + hit: 0, + details: [], + }, + }, + { + file: "\\files\\project\\src\\foo.js", + lines: { + found: 23, + hit: 21, + details: [ + { + line: 20, + hit: 3, + }, + { + line: 21, + hit: 3, + }, + { + line: 22, + hit: 3, + }, + ], + }, + functions: { + hit: 2, + found: 3, + details: [ + { + name: "foo", + line: 19, + }, + { + name: "bar", + line: 33, + }, + { + name: "baz", + line: 54, + }, + ], + }, + branches: { + hit: 3, + found: 3, + details: [ + { + line: 21, + block: 0, + branch: 0, + taken: 1, + }, + { + line: 21, + block: 0, + branch: 1, + taken: 2, + }, + { + line: 37, + block: 1, + branch: 0, + taken: 0, + }, + ], + }, + }, + { + file: "\\files\\project\\src\\bar\\baz.js", + lines: { + found: 10, + hit: 5, + details: [ + { + line: 20, + hit: 0, + }, + { + line: 21, + hit: 0, + }, + { + line: 27, + hit: 0, + }, + ], + }, + functions: { + hit: 2, + found: 3, + details: [ + { + name: "foo", + line: 19, + }, + { + name: "bar", + line: 33, + }, + { + name: "baz", + line: 54, + }, + ], + }, + }, + ] + + const options = { + repository: "example/foo", + commit: "2e15bee6fe0df5003389aa5ec894ec0fea2d874a", + prefix: "/files/project/", + shouldFilterChangedFiles: true, + changedFiles: ["src/foo.js"], + } + + const html = table( + tbody( + tr( + th("File"), + th("Stmts"), + th("Branches"), + th("Funcs"), + th("Lines"), + th("Uncovered Lines"), + ), + tr(td({ colspan: 6 }, b("src"))), + tr( + td( + "   ", + a( + { + href: `https://github.com/${options.repository}/blob/${options.commit}/src/foo.js`, + }, + "foo.js", + ), + ), + td(b("89.66%")), + td("100%"), + td(b("66.67%")), + td(b("91.30%")), + td( + a( + { + href: `https://github.com/${options.repository}/blob/${options.commit}/src/foo.js#L37`, + }, + 37, + ), + ), + ), + ), + ) + expect(tabulate(data, options)).toBe(html) +}) + +test("maxUncoveredLines should limit number of uncovered lines displayed", function() { + const data = [ + { + file: "/files/project/src/bar/baz.js", + lines: { + found: 10, + hit: 5, + details: [ + { + line: 20, + hit: 0, + }, + { + line: 21, + hit: 0, + }, + { + line: 27, + hit: 0, + }, + { + line: 29, + hit: 0, + }, + { + line: 41, + hit: 0, + }, + ], + }, + functions: { + hit: 2, + found: 3, + details: [ + { + name: "foo", + line: 19, + }, + { + name: "bar", + line: 33, + }, + { + name: "baz", + line: 54, + }, + ], + }, + }, + ] + + const options = { + repository: "example/foo", + commit: "2e15bee6fe0df5003389aa5ec894ec0fea2d874a", + prefix: "/files/project/", + maxUncoveredLines: 2, + } + + const html = table( + tbody( + tr( + th("File"), + th("Stmts"), + th("Branches"), + th("Funcs"), + th("Lines"), + th("Uncovered Lines"), + ), + tr(td({ colspan: 6 }, b("src/bar"))), + tr( + td( + "   ", + a( + { + href: `https://github.com/${options.repository}/blob/${options.commit}/src/bar/baz.js`, + }, + "baz.js", + ), + ), + td(b("53.85%")), + td("N/A"), + td(b("66.67%")), + td(b("50%")), + td( + a( + { + href: `https://github.com/${options.repository}/blob/${options.commit}/src/bar/baz.js#L20-L21`, + }, + "20–21", + ), + ", ", + a( + { + href: `https://github.com/${options.repository}/blob/${options.commit}/src/bar/baz.js#L27`, + }, + "27", + ), + " and 2 more...", + ), + ), + ), + ) + expect(tabulate(data, options)).toBe(html) +}) diff --git a/src/util.js b/src/util.js new file mode 100644 index 00000000..24ea31e1 --- /dev/null +++ b/src/util.js @@ -0,0 +1,3 @@ +export function normalisePath(file) { + return file.replace(/\\/g, "/") +}