diff --git a/action.yaml b/action.yaml index d0b413c..f7b0669 100644 --- a/action.yaml +++ b/action.yaml @@ -31,6 +31,10 @@ outputs: description: The overall line coverage percentage branch-coverage: description: The overall branch coverage percentage + pr-line-coverage: + description: The line coverage of the changes in this PR. Equal to the overall line coverage, if not executed on a PR + pr-branch-coverage: + description: The branch coverage of the changes in this PR. Equal to the overall line coverage, if not executed on a PR runs: using: 'node24' diff --git a/src/action.ts b/src/action.ts index 9aa6952..485df1c 100644 --- a/src/action.ts +++ b/src/action.ts @@ -1,7 +1,7 @@ import * as core from '@actions/core' import type { Octokit } from '@octokit/action' -import { processCoverage, type Logger } from './core/index.js' -import { findPullRequestNumber, getComparisonShas, postComment, type Context } from './github.js' +import { type Logger, processCoverage } from './core/index.js' +import { type Context, findPullRequestNumber, getComparisonShas, postComment } from './github.js' export type Inputs = { files: string @@ -39,11 +39,13 @@ export const run = async (inputs: Inputs, octokit: Octokit, context: Context): P return } - const { markdown, lineCoverage, branchCoverage } = result + const { markdown, lineCoverage, branchCoverage, lineCoveragePr, branchCoveragePr } = result // Set GitHub Actions outputs core.setOutput('line-coverage', lineCoverage.toFixed(2)) - core.setOutput('branch-coverage', branchCoverage.toFixed(2)) + core.setOutput('branch-coverage', branchCoverage?.toFixed(2)) + core.setOutput('pr-line-coverage', lineCoveragePr.toFixed(2)) + core.setOutput('pr-branch-coverage', branchCoveragePr?.toFixed(2)) // Find the pull request (needed for posting) const pullNumber = await findPullRequestNumber(octokit, context) diff --git a/src/cli.ts b/src/cli.ts index 726a943..f31da00 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -1,10 +1,10 @@ #!/usr/bin/env node -import * as fs from 'node:fs/promises' import { execFile } from 'node:child_process' +import * as fs from 'node:fs/promises' import { parseArgs, promisify } from 'node:util' import { processCoverage } from './core/index.js' -import { isValidGitRef } from './filter/changed-lines.js' import { createCliLogger } from './core/process-coverage.js' +import { isValidGitRef } from './filter/changed-lines.js' const execFileAsync = promisify(execFile) @@ -121,20 +121,22 @@ async function main(): Promise { process.exit(1) } - const { markdown, lineCoverage, branchCoverage } = result + const { markdown, lineCoverage, branchCoverage, lineCoveragePr, branchCoveragePr } = result // Output results if (values.output) { await fs.writeFile(values.output, markdown, 'utf-8') console.log(`\nReport written to: ${values.output}`) } else { - console.log('\n' + markdown) + console.log(`\n${markdown}`) } // Print metrics summary console.log('\n--- Coverage Summary ---') - console.log(`Line Coverage: ${lineCoverage.toFixed(2)}%`) - console.log(`Branch Coverage: ${branchCoverage.toFixed(2)}%`) + console.log(`Line Coverage: ${lineCoverage.toFixed(2)}%`) + console.log(`Branch Coverage: ${branchCoverage?.toFixed(2)}%`) + console.log(`Line Coverage PR: ${lineCoveragePr.toFixed(2)}%`) + console.log(`Branch Coverage PR: ${branchCoveragePr?.toFixed(2)}%`) } main().catch((error) => { diff --git a/src/core/index.ts b/src/core/index.ts index 8adcafc..d357f6f 100644 --- a/src/core/index.ts +++ b/src/core/index.ts @@ -1,6 +1,6 @@ export { - processCoverage, type Logger, type ProcessCoverageInputs, type ProcessCoverageResult, + processCoverage, } from './process-coverage.js' diff --git a/src/core/process-coverage.ts b/src/core/process-coverage.ts index 845843d..d292ffa 100644 --- a/src/core/process-coverage.ts +++ b/src/core/process-coverage.ts @@ -2,14 +2,15 @@ import * as fs from 'node:fs/promises' import * as path from 'node:path' import { CoberturaCoverageParser, - CoverageMetrics, + type CoverageMetrics, type CoverageReport, - FileCoverage, - LineCoverage, - LineCoverageState, - PackageCoverage, + type FileCoverage, + type LineCoverage, + type LineCoverageState, + type PackageCoverage, } from '../coverage/index.js' -import { filterByChangedLines, filterByGlob, getChangedLinesFromGit, type ChangedLinesMap } from '../filter/index.js' +import type { PercentageCoverageMetrics } from '../coverage/model.js' +import { type ChangedLinesMap, filterByChangedLines, filterByGlob, getChangedLinesFromGit } from '../filter/index.js' import { generateMarkdown } from '../markdown/index.js' /** @@ -53,7 +54,6 @@ export type ProcessCoverageInputs = { /** Explicit head commit SHA for comparison */ headSha?: string | undefined } - /** * Result of coverage processing. */ @@ -61,7 +61,9 @@ export type ProcessCoverageResult = { /** Generated markdown report */ markdown: string lineCoverage: number - branchCoverage: number + branchCoverage: number | undefined + lineCoveragePr: number + branchCoveragePr: number | undefined } /** @@ -151,13 +153,26 @@ export async function processCoverage( logger.debug?.(`Generating markdown for ${file.resolvedPath} with ${file.lines.length} changed lines`) } } - // Generate Markdown from filtered report - const markdown = generateMarkdown(filteredPackages, fileContents) - // Calculate overall metrics for outputs (from original merged report for accuracy) - const metrics = calculateOverallMetrics(mergedPackages) + const overallMetrics = calculateOverallMetrics(mergedPackages) + const prMetrics = calculateOverallMetrics(filteredPackages) + logger.info( + `Calculated overall metrics (LineCoverage: ${overallMetrics.lineCoverage}, BranchCoverage: ${overallMetrics.branchCoverage})`, + ) + logger.info( + `Calculated PR metrics (LineCoverage: ${prMetrics.lineCoverage}, BranchCoverage: ${prMetrics.branchCoverage})`, + ) + + // Generate Markdown from filtered report + const markdown = generateMarkdown(filteredPackages, fileContents, overallMetrics, {}, logger) - return { markdown, lineCoverage: metrics.lineCoverage, branchCoverage: metrics.branchCoverage } + return { + markdown, + lineCoverage: overallMetrics.lineCoverage, + branchCoverage: overallMetrics.branchCoverage, + lineCoveragePr: prMetrics.lineCoverage, + branchCoveragePr: prMetrics.branchCoverage, + } } async function firstExistingDirectory(paths: readonly string[]): Promise { @@ -272,7 +287,7 @@ async function mergeReportAndResolveSources( /** * Calculate overall coverage metrics from a merged packages. */ -function calculateOverallMetrics(packages: PackageCoverage[]): { lineCoverage: number; branchCoverage: number } { +function calculateOverallMetrics(packages: PackageCoverage[]): PercentageCoverageMetrics { let lineCovered = 0 let lineTotal = 0 let branchCovered = 0 diff --git a/src/coverage/model.ts b/src/coverage/model.ts index d25c1e8..6acfc9f 100644 --- a/src/coverage/model.ts +++ b/src/coverage/model.ts @@ -14,6 +14,11 @@ export type CoverageMetrics = { total: number } +export type PercentageCoverageMetrics = { + lineCoverage: number + branchCoverage?: number | undefined +} + export type FileCoverage = { /** Display path (relative to source root, for markdown output) */ filename: string diff --git a/src/filter/filter.ts b/src/filter/filter.ts index a437a7b..d87923d 100644 --- a/src/filter/filter.ts +++ b/src/filter/filter.ts @@ -1,6 +1,6 @@ import * as path from 'node:path' -import { FileCoverage, PackageCoverage } from '../coverage/model.js' import type { Logger } from '../core/index.js' +import type { FileCoverage, PackageCoverage } from '../coverage/model.js' import type { ChangedLinesMap } from './model.js' /** diff --git a/src/github.ts b/src/github.ts index fd3ca62..739f8ea 100644 --- a/src/github.ts +++ b/src/github.ts @@ -38,24 +38,50 @@ const getEnv = (name: string): string => { /** * Find the pull request number associated with the current context. + * Only returns PRs that are still open (not merged or closed). */ export async function findPullRequestNumber(octokit: Octokit, context: Context): Promise { - // Check if we're already in a pull_request event + // Fast path: pull_request event payload if ('pull_request' in context.payload) { - const prPayload = context.payload as { pull_request?: { number: number } } - if (prPayload.pull_request?.number) { - return prPayload.pull_request.number + const pr = (context.payload as { pull_request?: { number: number; state?: string } }).pull_request + if (pr?.number) { + if (pr.state !== 'open') return null + return pr.number } } // Otherwise, find PRs associated with this commit - const { data: pulls } = await octokit.rest.repos.listPullRequestsAssociatedWithCommit({ - owner: context.repo.owner, - repo: context.repo.repo, - commit_sha: context.sha, - }) + let pulls: { number: number; state?: string }[] + try { + const res = await octokit.rest.repos.listPullRequestsAssociatedWithCommit({ + owner: context.repo.owner, + repo: context.repo.repo, + commit_sha: context.sha, + }) + pulls = res.data + } catch { + return null + } + + for (const pull of pulls) { + const prNumber = pull.number + if (!prNumber) continue - return pulls[0]?.number ?? null + // Prefer using state if present + if (pull.state === 'open') return prNumber + if (pull.state && pull.state !== 'open') continue + + // Fallback: fetch PR details if state is missing/unknown + const { data: pr } = await octokit.rest.pulls.get({ + owner: context.repo.owner, + repo: context.repo.repo, + pull_number: prNumber, + }) + + if (pr.state === 'open') return prNumber + } + + return null } /** diff --git a/src/index.ts b/src/index.ts index 79ab5e3..773a0cc 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,6 +1,6 @@ import * as core from '@actions/core' -import { getContext, getOctokit } from './github.js' import { run } from './action.js' +import { getContext, getOctokit } from './github.js' try { await run( diff --git a/src/markdown/generator.ts b/src/markdown/generator.ts index 652ce2e..795ff7b 100644 --- a/src/markdown/generator.ts +++ b/src/markdown/generator.ts @@ -1,11 +1,29 @@ -import type { CoverageMetrics, FileCoverage, LineCoverage, PackageCoverage } from '../coverage/model.js' +import type { Logger } from '../core/index.js' +import type { + CoverageMetrics, + FileCoverage, + LineCoverage, + PackageCoverage, + PercentageCoverageMetrics, +} from '../coverage/model.js' + +// ============================================================================= +// CONSTANTS +// ============================================================================= /** Emoji indicators for line coverage states */ const COVERAGE_ICONS = { covered: '๐ŸŸฉ', partial: '๐ŸŸจ', 'not-covered': '๐ŸŸฅ', - 'no-info': 'โฌœ', + 'no-info': 'โฌ›', +} as const + +/** File status icons */ +const FILE_STATUS_ICONS = { + fullyCovered: '๐ŸŸข', + hasUncovered: '๐Ÿ”ด', + partialCoverage: '๐ŸŸ ', } as const /** Default maximum number of surrounding lines around an uncovered line */ @@ -15,30 +33,76 @@ export const DEFAULT_MAX_NUMBER_OF_SURROUNDING_LINES = 1 export const DEFAULT_MAX_CHARACTERS = 65536 /** Minimum characters required for meaningful markdown output (badges + legend + notice) */ -export const MINIMUM_CHARACTERS = 500 +export const MINIMUM_CHARACTERS = 900 + +// ============================================================================= +// TYPES +// ============================================================================= /** Options for markdown generation */ export type MarkdownOptions = { /** Number of lines to show before and after uncovered lines (default: 1) */ numberOfSurroundingLines?: number - /** Maximum number of characters in the output (default: 65536, minimum: 500) */ + /** Maximum number of characters in the output (default: 65536, minimum: 900) */ maxCharacters?: number } +/** PR metrics for summary line */ +type PRMetrics = { + coveredLines: number + totalLines: number + linePercent: number + branchPercent: number | null +} + +/** Data structure for a package section with its files */ +type PackageSectionData = { + packageName: string + header: string + files: FileSectionData[] + totalUncoveredLines: number + totalPartialBranches: number +} + +/** Data structure for a file section */ +type FileSectionData = { + filename: string + content: string + uncoveredLines: number + partialBranches: number +} + +/** Classification result for a file's coverage state */ +type FileCoverageClassification = { + hasUncovered: boolean + hasPartial: boolean + uncoveredCount: number + partialCount: number + statusIcon: string +} + +// ============================================================================= +// PUBLIC API +// ============================================================================= + /** - * Generate markdown visualization for coverage packages. + * Generate Markdown visualization for coverage packages. * This is a pure function suitable for snapshot testing. * - * @param packages - The normalized coverage packages + * @param packages - Array of packages * @param fileContents - Map of resolved (absolute) path to array of line contents - * @param options - Optional configuration for markdown generation + * @param overallMetrics - The overall percentage metrics + * @param options - Optional configuration for Markdown generation + * @param logger - Logger for emitting log messages * @returns Markdown string representation * @throws Error if maxCharacters is below MINIMUM_CHARACTERS */ export function generateMarkdown( packages: PackageCoverage[], fileContents: Map, + overallMetrics: PercentageCoverageMetrics, options: MarkdownOptions = {}, + logger: Logger, ): string { const numberOfSurroundingLines = options.numberOfSurroundingLines ?? DEFAULT_MAX_NUMBER_OF_SURROUNDING_LINES const maxCharacters = options.maxCharacters ?? DEFAULT_MAX_CHARACTERS @@ -48,148 +112,379 @@ export function generateMarkdown( throw new Error(`maxCharacters must be at least ${MINIMUM_CHARACTERS}, got ${maxCharacters}`) } - // Generate fixed content (badges and legend) - const badges = generateCoverageBadges(packages) + // Step 1: Generate fixed content (badges and legend) + const badges = generateCoverageBadges(overallMetrics) const legend = generateLegend() - // Generate all package sections with their file contents - const packageSections: PackageSectionData[] = [] + // Step 2: Build package section data + const packageSections = buildPackageSections(packages, fileContents, numberOfSurroundingLines) + + // Step 3: Sort packages + sortPackages(packageSections) + + // Step 4: Calculate PR metrics + const prMetrics = calculatePRMetrics(packages) + + // Step 5: Render markdown with truncation + if (packageSections.length === 0) { + const markdown = buildMarkdownForNoUncoveredContent(badges, prMetrics) + logger.info(`Generated Markdown with ${markdown.length} characters`) + return markdown + } + + const markdown = buildMarkdownWithinLimitFileLevel(badges, prMetrics, legend, packageSections, maxCharacters, logger) + logger.info(`Generated Markdown with ${markdown.length} characters`) + return markdown +} + +// ============================================================================= +// CLASSIFICATION & METRICS HELPERS +// ============================================================================= + +/** + * Classify a file's coverage state. + * Returns counts and flags for uncovered/partial lines, plus the appropriate status icon. + */ +function classifyFileCoverage(file: FileCoverage): FileCoverageClassification { + let uncoveredCount = 0 + let partialCount = 0 + + for (const line of file.lines) { + if (line.state === 'not-covered') { + uncoveredCount++ + } else if (line.state === 'partial') { + partialCount++ + } + } + + const hasUncovered = uncoveredCount > 0 + const hasPartial = partialCount > 0 + + // Determine status icon: ๐Ÿ”ด if uncovered, ๐ŸŸ  if only partial, ๐ŸŸข if fully covered + let statusIcon: string + if (hasUncovered) { + statusIcon = FILE_STATUS_ICONS.hasUncovered + } else if (hasPartial) { + statusIcon = FILE_STATUS_ICONS.partialCoverage + } else { + statusIcon = FILE_STATUS_ICONS.fullyCovered + } + + return { hasUncovered, hasPartial, uncoveredCount, partialCount, statusIcon } +} + +/** + * Calculate PR coverage metrics from packages (only changed lines). + * Counts all files, not just ones with uncovered lines. + */ +function calculatePRMetrics(packages: PackageCoverage[]): PRMetrics { + let coveredLines = 0 + let totalLines = 0 + let branchCovered = 0 + let branchTotal = 0 + let hasBranchData = false + for (const pkg of packages) { - const sectionData = generatePackageSectionData(pkg, fileContents, numberOfSurroundingLines) + for (const file of pkg.files) { + totalLines += file.lineMetrics.total + coveredLines += file.lineMetrics.covered + + if (file.branchMetrics) { + hasBranchData = true + branchCovered += file.branchMetrics.covered + branchTotal += file.branchMetrics.total + } + } + } + + const linePercent = totalLines === 0 ? 0 : (coveredLines / totalLines) * 100 + const branchPercent = hasBranchData && branchTotal > 0 ? (branchCovered / branchTotal) * 100 : null + + return { coveredLines, totalLines, linePercent, branchPercent } +} + +// ============================================================================= +// FORMATTING HELPERS +// ============================================================================= + +/** + * Format a ratio as "covered/total". + */ +function formatRatio(covered: number, total: number): string { + return `${covered}/${total}` +} + +/** + * Format a percentage value (0-100) to up to 2 decimal places. + * Removes trailing zeros for cleaner output. + * Examples: 100 โ†’ "100%", 99.99 โ†’ "99.99%", 20.5 โ†’ "20.5%" + */ +function formatPercent(value: number): string { + const formatted = value.toFixed(2) + // Remove trailing zeros and decimal point if not needed + return `${formatted.replace(/\.?0+$/, '')}%` +} + +/** + * Calculate and format percentage from covered/total ratio. + * Returns "0%" if total is 0. + */ +function formatPercentFromRatio(covered: number, total: number): string { + if (total === 0) return '0%' + return formatPercent((covered / total) * 100) +} + +/** + * Format branch percentage or return "n/a" if no data. + */ +function formatBranchPercentOrNA(branchMetrics: CoverageMetrics | undefined): string { + if (!branchMetrics || branchMetrics.total === 0) { + return 'n/a' + } + return formatPercentFromRatio(branchMetrics.covered, branchMetrics.total) +} + +// ============================================================================= +// SECTION DATA BUILDERS +// ============================================================================= + +/** + * Build all package section data from packages. + * Only includes packages that have files with uncovered/partial lines. + */ +function buildPackageSections( + packages: PackageCoverage[], + fileContents: Map, + numberOfSurroundingLines: number, +): PackageSectionData[] { + const sections: PackageSectionData[] = [] + + for (const pkg of packages) { + const sectionData = buildPackageSectionData(pkg, fileContents, numberOfSurroundingLines) if (sectionData !== null) { - packageSections.push(sectionData) + sections.push(sectionData) } } - // Check if all lines are covered (no package sections with uncovered lines) - const hasUncoveredContent = packageSections.length > 0 + return sections +} + +/** + * Build section data for a single package. + * Returns null if the package has no files with uncovered lines. + */ +function buildPackageSectionData( + pkg: PackageCoverage, + fileContents: Map, + numberOfSurroundingLines: number, +): PackageSectionData | null { + if (pkg.files.length === 0) { + return null + } + const header = `### ${pkg.name}` + const files: FileSectionData[] = [] + let totalUncoveredLines = 0 + let totalPartialBranches = 0 + + for (const file of pkg.files) { + const content = file.resolvedPath ? (fileContents.get(file.resolvedPath) ?? []) : [] + const classification = classifyFileCoverage(file) - if (!hasUncoveredContent) { - // No uncovered lines - return badges + success message - // We assume that this always fits the minimum size - const successMessage = 'โœ… All lines are covered!' - return badges ? `${badges}\n\n${successMessage}` : successMessage + totalUncoveredLines += classification.uncoveredCount + totalPartialBranches += classification.partialCount + + files.push({ + filename: file.filename, + content: renderFileSection(file, content, numberOfSurroundingLines, classification), + uncoveredLines: classification.uncoveredCount, + partialBranches: classification.partialCount, + }) } - const separator = '\n\n' - return buildMarkdownWithinLimitFileLevel({ - badges, - legend, - packageSections, - separator, - maxCharacters, - }) + sortFiles(files) + + return { + packageName: pkg.name, + header, + files, + totalUncoveredLines, + totalPartialBranches, + } } -function buildMarkdownWithinLimitFileLevel(params: { - badges: string | null - legend: string - packageSections: PackageSectionData[] - separator: string - maxCharacters: number -}): string { - const { badges, legend, packageSections, separator, maxCharacters } = params +// ============================================================================= +// MARKDOWN RENDERING +// ============================================================================= - // Parts we will join with `separator` +/** + * Build markdown for case when there are no uncovered lines. + */ +function buildMarkdownForNoUncoveredContent(badges: string, prMetrics: PRMetrics): string { const parts: string[] = [] - if (badges) parts.push(badges) - // We will always include legend at the end, so every "fit" check reserves room for it. - const legendOnlyLength = joinedLength([...parts, legend], separator) - if (legendOnlyLength > maxCharacters) { - // Should not happen with MINIMUM_CHARACTERS, but be safe. - const out = [...parts, legend].join(separator) - return out.slice(0, maxCharacters) + parts.push('## Repo Coverage') + parts.push(badges) + parts.push('') + parts.push('---') + parts.push('') + parts.push('## PR Coverage') + parts.push('') + parts.push(renderPRSummaryLine(prMetrics)) + parts.push('') + parts.push('---') + parts.push('') + parts.push(`Generated by \`coverage-pr-comment\``) + + return parts.join('\n') +} + +/** + * Simple budget tracker for building output within a character limit. + * Reserves space for a required tail (e.g., legend) and tracks what fits. + */ +class CharBudget { + private parts: string[] = [] + private currentLength = 0 + private readonly reservedTailLength: number + private readonly maxCharacters: number + + constructor(maxCharacters: number, reservedTail: string) { + this.maxCharacters = maxCharacters + this.reservedTailLength = reservedTail.length + } + + /** Check if adding content would exceed the limit. */ + wouldExceed(content: string): boolean { + return this.currentLength + content.length + this.reservedTailLength > this.maxCharacters + } + + /** Try to append content. Returns true if it fits, false otherwise. */ + tryAppend(content: string): boolean { + if (this.wouldExceed(content)) { + return false + } + this.parts.push(content) + this.currentLength += content.length + return true + } + + /** Force append content (used for initial header that must fit). */ + append(content: string): void { + this.parts.push(content) + this.currentLength += content.length } + /** Get the current output without the reserved tail. */ + getOutput(): string { + return this.parts.join('') + } +} + +/** + * Build markdown with truncation at file level when limit exceeded. + */ +function buildMarkdownWithinLimitFileLevel( + badges: string, + prMetrics: PRMetrics, + legend: string, + packageSections: PackageSectionData[], + maxCharacters: number, + logger: Logger, +): string { + // Legend with separator is always appended at the end + const legendWithSeparator = `\n---\n\n${legend}` + + // Build the fixed header + const header = `## Repo Coverage\n${badges}\n\n---\n\n## PR Coverage\n${renderPRSummaryLine(prMetrics)}\n\n` + + // Check if even the header + legend exceeds the limit + if (header.length + legendWithSeparator.length > maxCharacters) { + return (header + legendWithSeparator).slice(0, maxCharacters) + } + + const budget = new CharBudget(maxCharacters, legendWithSeparator) + budget.append(header) + + // Track omissions let omittedFiles = 0 let omittedPackages = 0 + let truncated = false - // Build incrementally: header, then each file - for (let p = 0; p < packageSections.length; p++) { + // Process packages and files + packageLoop: for (let p = 0; p < packageSections.length; p++) { const pkg = packageSections[p]! - // Decide whether we can include this package header at all - if (!canFitWithLegend(parts, pkg.header, legend, separator, maxCharacters)) { - // Omit this package and all remaining + // Try to include package header + if (!budget.tryAppend(`${pkg.header}\n`)) { + // Can't fit this package header - omit this and all remaining packages omittedPackages += packageSections.length - p - for (let i = p; i < packageSections.length; i++) omittedFiles += packageSections[i]!.files.length + for (let i = p; i < packageSections.length; i++) { + omittedFiles += packageSections[i]!.files.length + } + truncated = true break } - // Include header - parts.push(pkg.header) - + // Process files in this package for (let f = 0; f < pkg.files.length; f++) { - const file = pkg.files[f]!.content + const fileContent = pkg.files[f]?.content + const isLastFileOverall = f === pkg.files.length - 1 && p === packageSections.length - 1 + const spacing = isLastFileOverall ? '\n' : '\n\n' - if (!canFitWithLegend(parts, file, legend, separator, maxCharacters)) { - // Can't fit this file or remaining files in this package + if (!budget.tryAppend(fileContent + spacing)) { + // Can't fit this file - omit remaining files in this package and all remaining packages omittedFiles += pkg.files.length - f - - // Also omit all remaining packages completely omittedPackages += packageSections.length - (p + 1) - for (let i = p + 1; i < packageSections.length; i++) omittedFiles += packageSections[i]!.files.length - - // Optionally: if we added a package header but no files fit, you may want to keep it for context. - // Current behavior: we keep the header. - // If you *don't* want empty headers, you could remove it here when !includedAnyFileInThisPkg. - - p = packageSections.length // break outer - break + for (let i = p + 1; i < packageSections.length; i++) { + omittedFiles += packageSections[i]!.files.length + } + truncated = true + break packageLoop } - - parts.push(file) } } - // Add truncation notice if anything omitted AND it fits - if (omittedFiles > 0 || omittedPackages > 0) { - const notice = generateTruncationNotice(omittedFiles, omittedPackages) - if (canFitWithLegend(parts, notice, legend, separator, maxCharacters)) { - parts.push(notice) + // Add truncation notice if needed and it fits + if (truncated && (omittedFiles > 0 || omittedPackages > 0)) { + const notice = renderTruncationNotice(omittedFiles, omittedPackages) + budget.tryAppend(`${notice}\n`) + + if (omittedFiles > 0) { + logger.warning( + `Truncated ${omittedPackages} packages, ${omittedFiles} files to fit into size constraint of ${maxCharacters} characters`, + ) } } - // Finish with legend - parts.push(legend) + // Build final output with legend + let output = budget.getOutput() + legendWithSeparator // Hard guarantee (should already be true) - let out = parts.join(separator) - if (out.length > maxCharacters) out = out.slice(0, maxCharacters) - return out -} + if (output.length > maxCharacters) { + output = output.slice(0, maxCharacters) + } -function joinedLength(parts: string[], separator: string): number { - if (parts.length === 0) return 0 - let len = 0 - for (const p of parts) len += p.length - len += separator.length * (parts.length - 1) - return len + return output } -function canFitWithLegend(parts: string[], candidate: string, legend: string, separator: string, max: number): boolean { - const test = [...parts, candidate, legend] - return joinedLength(test, separator) <= max -} +/** + * Render PR summary line. + */ +function renderPRSummaryLine(metrics: PRMetrics): string { + if (metrics.totalLines === 0) { + return '0/0 changed lines covered' + } -/** Data structure for a package section with its files */ -type PackageSectionData = { - packageName: string - header: string - files: FileSectionData[] -} + const linePercentStr = formatPercentFromRatio(metrics.coveredLines, metrics.totalLines) + const branchPercentStr = metrics.branchPercent !== null ? formatPercent(metrics.branchPercent) : 'n/a' -/** Data structure for a file section */ -type FileSectionData = { - filename: string - content: string + return `${formatRatio(metrics.coveredLines, metrics.totalLines)} changed lines covered (Lines: ${linePercentStr}, Branches: ${branchPercentStr})` } /** - * Generate truncation notice with counts of omitted items. + * Render truncation notice with counts of omitted items. */ -function generateTruncationNotice(omittedFiles: number, omittedPackages: number): string { +function renderTruncationNotice(omittedFiles: number, omittedPackages: number): string { const parts: string[] = [] if (omittedFiles > 0) { parts.push(`${omittedFiles} file(s)`) @@ -201,86 +496,135 @@ function generateTruncationNotice(omittedFiles: number, omittedPackages: number) } /** - * Generate package section data without joining into final string. - * Returns null if the package has no files with uncovered lines. + * Render the coverage legend explaining the symbols. */ -function generatePackageSectionData( - pkg: PackageCoverage, - fileContents: Map, - numberOfSurroundingLines: number, -): PackageSectionData | null { - // Filter to only files with uncovered lines - const filesWithUncovered = pkg.files.filter(hasUncoveredLines) - - // Skip package entirely if all files are fully covered - if (filesWithUncovered.length === 0) { - return null - } +function generateLegend(): string { + return `Legend: ${COVERAGE_ICONS['not-covered']} uncovered ยท ${COVERAGE_ICONS.partial} partial branch ยท ${COVERAGE_ICONS.covered} covered ยท ${COVERAGE_ICONS['no-info']} non-executable/blank
Generated by \`coverage-pr-comment\`
` +} - // Calculate aggregate metrics for the package - const packageMetrics = calculatePackageMetrics(pkg.files) +/** + * Render coverage badges for the overall packages. + */ +function generateCoverageBadges(overallMetrics: PercentageCoverageMetrics): string { + const badges: string[] = [] + badges.push(renderBadge('Line Coverage', overallMetrics.lineCoverage)) + badges.push(renderBadge('Branch Coverage', overallMetrics.branchCoverage)) + return badges.join(' ') +} - // Package header with coverage summary - const linePercent = formatPercent(packageMetrics.lineMetrics) - const branchPercent = packageMetrics.branchMetrics ? formatPercent(packageMetrics.branchMetrics) : null +/** + * Render a single shields.io badge markdown. + */ +function renderBadge(label: string, percent: number | undefined): string { + const encodedLabel = encodeURIComponent(label) - let header = `**${pkg.name}** (LineCoverage: ${linePercent}` - if (branchPercent) { - header += `, BranchCoverage: ${branchPercent}` + let value: string + let color: string + if (percent === undefined) { + value = 'n/a' + color = 'brightgreen' + } else { + value = formatPercent(percent) + color = getBadgeColor(percent) } - header += ')' + const encodedValue = encodeURIComponent(value) - // Generate file sections - const files: FileSectionData[] = [] - for (const file of filesWithUncovered) { - const content = file.resolvedPath ? (fileContents.get(file.resolvedPath) ?? []) : [] - files.push({ - filename: file.filename, - content: generateFileSection(file, content, numberOfSurroundingLines), - }) - } - - return { - packageName: pkg.name, - header, - files, - } + return `![${label}](https://img.shields.io/badge/${encodedLabel}-${encodedValue}-${color}.svg?style=flat)` } /** - * Check if a file has any uncovered or partial lines. + * Get badge color based on coverage percentage. */ -function hasUncoveredLines(file: FileCoverage): boolean { - return file.lines.some((line) => line.state === 'not-covered' || line.state === 'partial') +function getBadgeColor(percent: number): string { + if (percent >= 80) return 'brightgreen' + if (percent >= 60) return 'green' + if (percent >= 40) return 'yellowgreen' + if (percent >= 20) return 'yellow' + return 'red' } /** - * Generate a collapsible markdown section for a single file. + * Render a markdown section for a single file. + * Uses inline format for fully covered files, details block for files with uncovered lines. */ -function generateFileSection(file: FileCoverage, fileLines: string[], numberOfSurroundingLines: number): string { +function renderFileSection( + file: FileCoverage, + fileLines: string[], + numberOfSurroundingLines: number, + classification: FileCoverageClassification, +): string { + const { hasUncovered, hasPartial, statusIcon } = classification + + // Fully covered - use inline format + if (!hasUncovered && !hasPartial) { + return `โœ“ ${FILE_STATUS_ICONS.fullyCovered} ${file.filename} โ€” ${formatRatio(file.lineMetrics.covered, file.lineMetrics.total)} changed lines covered` + } + + // Has uncovered/partial lines - use details block const lines: string[] = [] - // Start collapsible details section - lines.push(`
${file.filename}`) + const linePercent = formatPercentFromRatio(file.lineMetrics.covered, file.lineMetrics.total) + const branchPercent = formatBranchPercentOrNA(file.branchMetrics) + + lines.push(`
`) + lines.push( + `${statusIcon} ${file.filename} โ€” ${formatRatio(file.lineMetrics.covered, file.lineMetrics.total)} changed lines covered (Lines: ${linePercent}, Branches: ${branchPercent})`, + ) lines.push('') - // Generate code block with coverage annotations const extension = getFileExtension(file.filename) - lines.push('```' + extension) - lines.push(generateAnnotatedLines(file.lines, fileLines, numberOfSurroundingLines)) + lines.push(`\`\`\`${extension}`) + lines.push(renderAnnotatedLines(file.lines, fileLines, numberOfSurroundingLines)) lines.push('```') - lines.push('
') return lines.join('\n') } /** - * Generate annotated line content showing coverage state. + * Extract file extension for syntax highlighting. + */ +function getFileExtension(filename: string): string { + const parts = filename.split('.') + if (parts.length < 2) { + return '' + } + + const lastPart = parts[parts.length - 1] + const ext = lastPart ? lastPart.toLowerCase() : '' + + const extensionMap: Record = { + cs: 'csharp', + rs: 'rust', + ts: 'typescript', + tsx: 'typescript', + js: 'javascript', + jsx: 'javascript', + py: 'python', + rb: 'ruby', + go: 'go', + java: 'java', + kt: 'kotlin', + swift: 'swift', + cpp: 'cpp', + c: 'c', + h: 'c', + hpp: 'cpp', + } + + return extensionMap[ext] || ext +} + +// ============================================================================= +// LINE ANNOTATION +// ============================================================================= + +/** + * Render annotated line content showing coverage state. * Only shows uncovered/partial lines with configurable context lines around them. * Uses smart ellipsis handling: shows single lines instead of "..." when gap is small. */ -function generateAnnotatedLines( +function renderAnnotatedLines( coverageLines: LineCoverage[], fileLines: string[], numberOfSurroundingLines: number, @@ -308,7 +652,6 @@ function generateAnnotatedLines( // Expand to include context lines around interesting lines const linesToShow = new Set() for (const lineNum of interestingLineNumbers) { - // Add context before and after for (let i = lineNum - numberOfSurroundingLines; i <= lineNum + numberOfSurroundingLines; i++) { if (i >= 1 && (i <= fileLines.length || lineMap.has(i))) { linesToShow.add(i) @@ -316,7 +659,6 @@ function generateAnnotatedLines( } } - // Convert to sorted array const linesToShowArray = [...linesToShow].sort((a, b) => a - b) // Helper to get line content from fileLines (1-indexed to 0-indexed) @@ -328,254 +670,116 @@ function generateAnnotatedLines( return '' } + // Helper to render a single annotated line + const renderLine = (lineNum: number): string => { + const line = lineMap.get(lineNum) + const icon = line ? COVERAGE_ICONS[line.state] : COVERAGE_ICONS['no-info'] + const lineNumStr = lineNum.toString().padStart(3, ' ') + const content = getLineContent(lineNum) + return `${lineNumStr} ${icon} ${content}` + } + const outputLines: string[] = [] - // Determine the first and last line numbers in coverage data for ellipsis detection const minLineInData = sortedLines[0]?.lineNumber ?? 1 const maxLineInData = sortedLines[sortedLines.length - 1]?.lineNumber ?? 1 const firstLineToShow = linesToShowArray[0] ?? minLineInData const lastLineToShow = linesToShowArray[linesToShowArray.length - 1] ?? maxLineInData - // Add leading ellipsis if there's content before the first shown line - // If only 1 line is hidden, show the actual line instead of ellipsis + // Add leading gap handling if (firstLineToShow > 1) { - if (firstLineToShow === 2) { - // Only line 1 is hidden, show it - const gapLine = lineMap.get(1) - const gapIcon = gapLine ? COVERAGE_ICONS[gapLine.state] : COVERAGE_ICONS['no-info'] - const gapLineNumStr = '1'.padStart(3, ' ') - const gapContent = getLineContent(1) - outputLines.push(`${gapLineNumStr} ${gapIcon} ${gapContent}`) - } else { - outputLines.push('...') - } + outputLines.push(renderGap(0, firstLineToShow, lineMap, getLineContent)) } let prevLineNumber = -1 for (const lineNum of linesToShowArray) { - const line = lineMap.get(lineNum) - - // Check for gap and handle ellipsis + // Handle gap between consecutive shown lines if (prevLineNumber !== -1 && lineNum > prevLineNumber + 1) { - const gapSize = lineNum - prevLineNumber - 1 - - // If gap is just 1 line, show the actual line - if (gapSize === 1) { - const gapLineNum = prevLineNumber + 1 - const gapLine = lineMap.get(gapLineNum) - const gapIcon = gapLine ? COVERAGE_ICONS[gapLine.state] : COVERAGE_ICONS['no-info'] - const gapLineNumStr = gapLineNum.toString().padStart(3, ' ') - const gapContent = getLineContent(gapLineNum) - outputLines.push(`${gapLineNumStr} ${gapIcon} ${gapContent}`) - } else { - // 2+ line gap: show single ellipsis - outputLines.push('...') - } + outputLines.push(renderGap(prevLineNumber, lineNum, lineMap, getLineContent)) } - const icon = line ? COVERAGE_ICONS[line.state] : COVERAGE_ICONS['no-info'] - const lineNumStr = lineNum.toString().padStart(3, ' ') - const content = getLineContent(lineNum) - outputLines.push(`${lineNumStr} ${icon} ${content}`) - + outputLines.push(renderLine(lineNum)) prevLineNumber = lineNum } - // Add trailing ellipsis if there's content after the last shown line - // If only 1 line is hidden, show the actual line instead of ellipsis + // Add trailing gap handling if (lastLineToShow < fileLines.length) { - if (lastLineToShow === fileLines.length - 1) { - // Only the last line is hidden, show it - const lastLineNum = fileLines.length - const gapLine = lineMap.get(lastLineNum) - const gapIcon = gapLine ? COVERAGE_ICONS[gapLine.state] : COVERAGE_ICONS['no-info'] - const gapLineNumStr = lastLineNum.toString().padStart(3, ' ') - const gapContent = getLineContent(lastLineNum) - outputLines.push(`${gapLineNumStr} ${gapIcon} ${gapContent}`) - } else { - outputLines.push('...') - } + outputLines.push(renderGap(lastLineToShow, fileLines.length + 1, lineMap, getLineContent)) } return outputLines.join('\n') } /** - * Generate the coverage legend explaining the symbols. - */ -function generateLegend(): string { - return `${COVERAGE_ICONS['not-covered']} Not covered, ${COVERAGE_ICONS.partial} Missing branch coverage, ${COVERAGE_ICONS.covered} Covered` -} - -/** - * Generate coverage badges for the overall packages. - * Returns badges for Line, Branch, and Function coverage if data is available. + * Render a gap between two line numbers. + * If gap is just 1 line, renders that line; otherwise renders "...". + * + * @param prevLine - The last shown line (0 if at start) + * @param nextLine - The next line to be shown (fileLines.length + 1 if at end) + * @param lineMap - The map with available line coverage + * @param getLineContent - the callback to get the content based on the line */ -function generateCoverageBadges(packages: PackageCoverage[]): string | null { - const metrics = calculateReportMetrics(packages) - - // Need at least line metrics to generate badges - if (metrics.lineMetrics.total === 0) { - return null - } - - const badges: string[] = [] - - // Line coverage badge - badges.push(generateBadge('Line Coverage', metrics.lineMetrics)) - - // Branch coverage badge - if (metrics.branchMetrics && metrics.branchMetrics.total > 0) { - badges.push(generateBadge('Branch Coverage', metrics.branchMetrics)) +function renderGap( + prevLine: number, + nextLine: number, + lineMap: Map, + getLineContent: (lineNum: number) => string, +): string { + const gapSize = nextLine - prevLine - 1 + + if (gapSize === 1) { + // Show the single hidden line + const gapLineNum = prevLine + 1 + const gapLine = lineMap.get(gapLineNum) + const gapIcon = gapLine ? COVERAGE_ICONS[gapLine.state] : COVERAGE_ICONS['no-info'] + const gapLineNumStr = gapLineNum.toString().padStart(3, ' ') + const gapContent = getLineContent(gapLineNum) + return `${gapLineNumStr} ${gapIcon} ${gapContent}` } - return badges.join('\n') + return '...' } -/** - * Generate a single shields.io badge markdown. - */ -function generateBadge(label: string, metrics: CoverageMetrics): string { - const percent = metrics.total === 0 ? 0 : (metrics.covered / metrics.total) * 100 - const color = getBadgeColor(percent) - const encodedLabel = encodeURIComponent(label) - const encodedValue = encodeURIComponent(`${percent.toFixed(2)}%`) - - return `![${label}](https://img.shields.io/badge/${encodedLabel}-${encodedValue}-${color}.svg?style=flat)` -} - -/** - * Get badge color based on coverage percentage. - */ -function getBadgeColor(percent: number): string { - if (percent >= 80) return 'brightgreen' - if (percent >= 60) return 'green' - if (percent >= 40) return 'yellowgreen' - if (percent >= 20) return 'yellow' - return 'red' -} +// ============================================================================= +// SORTING +// ============================================================================= /** - * Calculate aggregate metrics for the entire packages. + * Sort packages by uncovered lines, partial branches, then alphabetically. */ -function calculateReportMetrics(packages: PackageCoverage[]): { - lineMetrics: CoverageMetrics - branchMetrics?: CoverageMetrics -} { - let lineCovered = 0 - let lineTotal = 0 - let branchCovered = 0 - let branchTotal = 0 - let hasBranchData = false - - for (const pkg of packages) { - for (const file of pkg.files) { - lineCovered += file.lineMetrics.covered - lineTotal += file.lineMetrics.total - - if (file.branchMetrics) { - hasBranchData = true - branchCovered += file.branchMetrics.covered - branchTotal += file.branchMetrics.total - } +function sortPackages(packages: PackageSectionData[]): void { + packages.sort((a, b) => { + if (a.totalUncoveredLines !== b.totalUncoveredLines) { + return b.totalUncoveredLines - a.totalUncoveredLines } - } - - const result: { - lineMetrics: CoverageMetrics - branchMetrics?: CoverageMetrics - } = { - lineMetrics: { covered: lineCovered, total: lineTotal }, - } - - if (hasBranchData) { - result.branchMetrics = { covered: branchCovered, total: branchTotal } - } - - return result -} - -/** - * Calculate aggregate metrics for a package from its files. - */ -function calculatePackageMetrics(files: FileCoverage[]): { - lineMetrics: CoverageMetrics - branchMetrics?: CoverageMetrics -} { - let lineCovered = 0 - let lineTotal = 0 - let branchCovered = 0 - let branchTotal = 0 - let hasBranchData = false - - for (const file of files) { - lineCovered += file.lineMetrics.covered - lineTotal += file.lineMetrics.total - - if (file.branchMetrics) { - hasBranchData = true - branchCovered += file.branchMetrics.covered - branchTotal += file.branchMetrics.total + if (a.totalPartialBranches !== b.totalPartialBranches) { + return b.totalPartialBranches - a.totalPartialBranches } - } - - const result: { - lineMetrics: CoverageMetrics - branchMetrics?: CoverageMetrics - } = { - lineMetrics: { covered: lineCovered, total: lineTotal }, - } - - if (hasBranchData) { - result.branchMetrics = { covered: branchCovered, total: branchTotal } - } - - return result -} - -/** - * Format coverage metrics as a percentage string. - */ -function formatPercent(metrics: CoverageMetrics): string { - if (metrics.total === 0) { - return '0%' - } - const percent = (metrics.covered / metrics.total) * 100 - return `${percent.toFixed(0)}%` + return a.packageName.localeCompare(b.packageName) + }) } /** - * Extract file extension for syntax highlighting. + * Sort files by uncovered lines, partial branches, then alphabetically. + * Fully covered files (0 uncovered) come first, then files with uncovered lines. */ -function getFileExtension(filename: string): string { - const parts = filename.split('.') - if (parts.length < 2) { - return '' - } +function sortFiles(files: FileSectionData[]): void { + files.sort((a, b) => { + const aIsFullyCovered = a.uncoveredLines === 0 + const bIsFullyCovered = b.uncoveredLines === 0 + if (aIsFullyCovered !== bIsFullyCovered) { + return aIsFullyCovered ? -1 : 1 + } - const lastPart = parts[parts.length - 1] - const ext = lastPart ? lastPart.toLowerCase() : '' + if (!aIsFullyCovered && a.uncoveredLines !== b.uncoveredLines) { + return b.uncoveredLines - a.uncoveredLines + } - // Map extensions to markdown code block language identifiers - const extensionMap: Record = { - cs: 'csharp', - rs: 'rust', - ts: 'typescript', - tsx: 'typescript', - js: 'javascript', - jsx: 'javascript', - py: 'python', - rb: 'ruby', - go: 'go', - java: 'java', - kt: 'kotlin', - swift: 'swift', - cpp: 'cpp', - c: 'c', - h: 'c', - hpp: 'cpp', - } + if (a.partialBranches !== b.partialBranches) { + return b.partialBranches - a.partialBranches + } - return extensionMap[ext] || ext + return a.filename.localeCompare(b.filename) + }) } diff --git a/src/markdown/index.ts b/src/markdown/index.ts index a30590a..93b71fc 100644 --- a/src/markdown/index.ts +++ b/src/markdown/index.ts @@ -1,6 +1,6 @@ export { + DEFAULT_MAX_CHARACTERS, generateMarkdown, type MarkdownOptions, - DEFAULT_MAX_CHARACTERS, MINIMUM_CHARACTERS, } from './generator.js' diff --git a/tests/core/__snapshots__/basic-coverage.snap.md b/tests/core/__snapshots__/basic-coverage.snap.md index a0d4a1f..bb0ad49 100644 --- a/tests/core/__snapshots__/basic-coverage.snap.md +++ b/tests/core/__snapshots__/basic-coverage.snap.md @@ -1,9 +1,14 @@ -![Line Coverage](https://img.shields.io/badge/Line%20Coverage-75.00%25-green.svg?style=flat) -![Branch Coverage](https://img.shields.io/badge/Branch%20Coverage-100.00%25-brightgreen.svg?style=flat) +## Repo Coverage +![Line Coverage](https://img.shields.io/badge/Line%20Coverage-75%25-green.svg?style=flat) ![Branch Coverage](https://img.shields.io/badge/Branch%20Coverage-100%25-brightgreen.svg?style=flat) -**app** (LineCoverage: 75%, BranchCoverage: 100%) +--- -
src/calculator.ts +## PR Coverage +3/4 changed lines covered (Lines: 75%, Branches: 100%) + +### app +
+๐Ÿ”ด src/calculator.ts โ€” 3/4 changed lines covered (Lines: 75%, Branches: 100%) ```typescript 1 ๐ŸŸฉ export function add(a: number, b: number): number { @@ -14,4 +19,6 @@ ```
-๐ŸŸฅ Not covered, ๐ŸŸจ Missing branch coverage, ๐ŸŸฉ Covered \ No newline at end of file +--- + +Legend: ๐ŸŸฅ uncovered ยท ๐ŸŸจ partial branch ยท ๐ŸŸฉ covered ยท โฌ› non-executable/blank
Generated by `coverage-pr-comment`
\ No newline at end of file diff --git a/tests/core/__snapshots__/glob-filter.snap.md b/tests/core/__snapshots__/glob-filter.snap.md index 5c3fd32..16cf3d0 100644 --- a/tests/core/__snapshots__/glob-filter.snap.md +++ b/tests/core/__snapshots__/glob-filter.snap.md @@ -1,16 +1,22 @@ -![Line Coverage](https://img.shields.io/badge/Line%20Coverage-50.00%25-yellowgreen.svg?style=flat) -![Branch Coverage](https://img.shields.io/badge/Branch%20Coverage-100.00%25-brightgreen.svg?style=flat) +## Repo Coverage +![Line Coverage](https://img.shields.io/badge/Line%20Coverage-50%25-yellowgreen.svg?style=flat) ![Branch Coverage](https://img.shields.io/badge/Branch%20Coverage-75%25-green.svg?style=flat) -**filtered** (LineCoverage: 50%, BranchCoverage: 100%) +--- -
src/include/wanted.ts +## PR Coverage +2/2 changed lines covered (Lines: 100%, Branches: 50%) + +### filtered +
+๐ŸŸ  src/include/wanted.ts โ€” 2/2 changed lines covered (Lines: 100%, Branches: 50%) ```typescript - 1 ๐ŸŸฉ export function wanted(): string { - 2 ๐ŸŸฅ return 'I am wanted' - 3 โฌœ } - 4 โฌœ + 1 ๐ŸŸจ export function wanted(): string { + 2 ๐ŸŸฉ return 'I am wanted' +... ```
-๐ŸŸฅ Not covered, ๐ŸŸจ Missing branch coverage, ๐ŸŸฉ Covered \ No newline at end of file +--- + +Legend: ๐ŸŸฅ uncovered ยท ๐ŸŸจ partial branch ยท ๐ŸŸฉ covered ยท โฌ› non-executable/blank
Generated by `coverage-pr-comment`
\ No newline at end of file diff --git a/tests/core/__snapshots__/merge-reports.snap.md b/tests/core/__snapshots__/merge-reports.snap.md index a5f66be..eb43afc 100644 --- a/tests/core/__snapshots__/merge-reports.snap.md +++ b/tests/core/__snapshots__/merge-reports.snap.md @@ -1,9 +1,14 @@ -![Line Coverage](https://img.shields.io/badge/Line%20Coverage-60.00%25-green.svg?style=flat) -![Branch Coverage](https://img.shields.io/badge/Branch%20Coverage-100.00%25-brightgreen.svg?style=flat) +## Repo Coverage +![Line Coverage](https://img.shields.io/badge/Line%20Coverage-60%25-green.svg?style=flat) ![Branch Coverage](https://img.shields.io/badge/Branch%20Coverage-100%25-brightgreen.svg?style=flat) -**shared** (LineCoverage: 60%, BranchCoverage: 100%) +--- -
src/shared.ts +## PR Coverage +3/5 changed lines covered (Lines: 60%, Branches: 100%) + +### shared +
+๐Ÿ”ด src/shared.ts โ€” 3/5 changed lines covered (Lines: 60%, Branches: 100%) ```typescript 1 ๐ŸŸฉ export function sharedOne(): number { @@ -11,9 +16,11 @@ 3 ๐ŸŸฅ } 4 ๐ŸŸฉ export function sharedTwo(): number { 5 ๐ŸŸฅ return 2 - 6 โฌœ } - 7 โฌœ + 6 โฌ› } + 7 โฌ› ```
-๐ŸŸฅ Not covered, ๐ŸŸจ Missing branch coverage, ๐ŸŸฉ Covered \ No newline at end of file +--- + +Legend: ๐ŸŸฅ uncovered ยท ๐ŸŸจ partial branch ยท ๐ŸŸฉ covered ยท โฌ› non-executable/blank
Generated by `coverage-pr-comment`
\ No newline at end of file diff --git a/tests/core/__snapshots__/multiple-packages.snap.md b/tests/core/__snapshots__/multiple-packages.snap.md index 57cffd0..21d2ab9 100644 --- a/tests/core/__snapshots__/multiple-packages.snap.md +++ b/tests/core/__snapshots__/multiple-packages.snap.md @@ -1,28 +1,35 @@ -![Line Coverage](https://img.shields.io/badge/Line%20Coverage-50.00%25-yellowgreen.svg?style=flat) -![Branch Coverage](https://img.shields.io/badge/Branch%20Coverage-100.00%25-brightgreen.svg?style=flat) +## Repo Coverage +![Line Coverage](https://img.shields.io/badge/Line%20Coverage-50%25-yellowgreen.svg?style=flat) ![Branch Coverage](https://img.shields.io/badge/Branch%20Coverage-100%25-brightgreen.svg?style=flat) -**core** (LineCoverage: 67%, BranchCoverage: 100%) +--- -
src/core/math.ts +## PR Coverage +3/6 changed lines covered (Lines: 50%, Branches: 100%) + +### utils +
+๐Ÿ”ด src/utils/format.ts โ€” 1/3 changed lines covered (Lines: 33.33%, Branches: 100%) ```typescript - 1 ๐ŸŸฉ export function add(a: number, b: number): number { - 2 ๐ŸŸฉ return a + b + 1 ๐ŸŸฉ export function formatNumber(n: number): string { + 2 ๐ŸŸฅ return n.toFixed(2) 3 ๐ŸŸฅ } - 4 โฌœ + 4 โฌ› ```
-**utils** (LineCoverage: 33%, BranchCoverage: 100%) - -
src/utils/format.ts +### core +
+๐Ÿ”ด src/core/math.ts โ€” 2/3 changed lines covered (Lines: 66.67%, Branches: 100%) ```typescript - 1 ๐ŸŸฉ export function formatNumber(n: number): string { - 2 ๐ŸŸฅ return n.toFixed(2) + 1 ๐ŸŸฉ export function add(a: number, b: number): number { + 2 ๐ŸŸฉ return a + b 3 ๐ŸŸฅ } - 4 โฌœ + 4 โฌ› ```
-๐ŸŸฅ Not covered, ๐ŸŸจ Missing branch coverage, ๐ŸŸฉ Covered \ No newline at end of file +--- + +Legend: ๐ŸŸฅ uncovered ยท ๐ŸŸจ partial branch ยท ๐ŸŸฉ covered ยท โฌ› non-executable/blank
Generated by `coverage-pr-comment`
\ No newline at end of file diff --git a/tests/core/process-coverage.integration.test.ts b/tests/core/process-coverage.integration.test.ts index 10cb1f9..9b2b982 100644 --- a/tests/core/process-coverage.integration.test.ts +++ b/tests/core/process-coverage.integration.test.ts @@ -1,6 +1,6 @@ import * as path from 'node:path' import { describe, expect, it } from 'vitest' -import { processCoverage, createCliLogger } from '../../src/core/process-coverage.js' +import { createCliLogger, processCoverage } from '../../src/core/process-coverage.js' const FIXTURES_DIR = path.join(__dirname, '../resources/integration') @@ -18,7 +18,7 @@ describe('processCoverage integration', () => { ) expect(result).not.toBeNull() - await expect(result!.markdown).toMatchFileSnapshot('./__snapshots__/basic-coverage.snap.md') + await expect(result?.markdown).toMatchFileSnapshot('./__snapshots__/basic-coverage.snap.md') }) it('generates markdown for multiple packages', async () => { @@ -34,7 +34,7 @@ describe('processCoverage integration', () => { ) expect(result).not.toBeNull() - await expect(result!.markdown).toMatchFileSnapshot('./__snapshots__/multiple-packages.snap.md') + await expect(result?.markdown).toMatchFileSnapshot('./__snapshots__/multiple-packages.snap.md') }) it('filters files by glob pattern', async () => { @@ -50,9 +50,12 @@ describe('processCoverage integration', () => { ) expect(result).not.toBeNull() - expect(result!.markdown).toContain('wanted.ts') - expect(result!.markdown).not.toContain('ignored.ts') - await expect(result!.markdown).toMatchFileSnapshot('./__snapshots__/glob-filter.snap.md') + // Outputs must be computed from the already-filtered data (same as markdown) + expect(result?.lineCoverage).toBeCloseTo(50, 5) + expect(result?.branchCoverage).toBeCloseTo(75, 5) + expect(result?.markdown).toContain('wanted.ts') + expect(result?.markdown).not.toContain('ignored.ts') + await expect(result?.markdown).toMatchFileSnapshot('./__snapshots__/glob-filter.snap.md') }) it('merges multiple coverage files', async () => { @@ -68,7 +71,7 @@ describe('processCoverage integration', () => { ) expect(result).not.toBeNull() - await expect(result!.markdown).toMatchFileSnapshot('./__snapshots__/merge-reports.snap.md') + await expect(result?.markdown).toMatchFileSnapshot('./__snapshots__/merge-reports.snap.md') }) it('returns null when no files match pattern', async () => { diff --git a/tests/filter/changed-lines.test.ts b/tests/filter/changed-lines.test.ts index f5fc9b6..c60dc9d 100644 --- a/tests/filter/changed-lines.test.ts +++ b/tests/filter/changed-lines.test.ts @@ -212,7 +212,7 @@ index abc123..def456 100644 line 3 line 4` - const result = parseDiffOutput(diff, "/root/") + const result = parseDiffOutput(diff, '/root/') expect(result.size).toBe(1) expect(result.get('/root/src/file.ts')).toEqual(new Set([2])) @@ -236,7 +236,7 @@ index 111222..333444 100644 +added in file2 line 6` - const result = parseDiffOutput(diff, "/root/") + const result = parseDiffOutput(diff, '/root/') expect(result.size).toBe(2) expect(result.get('/root/src/file1.ts')).toEqual(new Set([2])) @@ -254,7 +254,7 @@ index 0000000..abc123 +line 2 +line 3` - const result = parseDiffOutput(diff, "/root/") + const result = parseDiffOutput(diff, '/root/') expect(result.size).toBe(1) expect(result.get('/root/src/newfile.ts')).toEqual(new Set([1, 2, 3])) @@ -271,7 +271,7 @@ index abc123..0000000 -line 2 -line 3` - const result = parseDiffOutput(diff, "/root/") + const result = parseDiffOutput(diff, '/root/') expect(result.size).toBe(0) }) @@ -292,7 +292,7 @@ index abc123..def456 100644 line 13 line 14` - const result = parseDiffOutput(diff, "/root/") + const result = parseDiffOutput(diff, '/root/') expect(result.size).toBe(1) expect(result.get('/root/src/file.ts')).toEqual(new Set([2, 12])) @@ -301,7 +301,7 @@ index abc123..def456 100644 it('handles empty diff', () => { const diff = '' - const result = parseDiffOutput(diff, "/root/") + const result = parseDiffOutput(diff, '/root/') expect(result.size).toBe(0) }) @@ -320,7 +320,7 @@ index abc123..def456 100644 line 3 line 4` - const result = parseDiffOutput(diff, "/root/") + const result = parseDiffOutput(diff, '/root/') expect(result.size).toBe(1) expect(result.get('/root/new/path.ts')).toEqual(new Set([2])) diff --git a/tests/filter/filter.test.ts b/tests/filter/filter.test.ts index 8c87915..b2af7ba 100644 --- a/tests/filter/filter.test.ts +++ b/tests/filter/filter.test.ts @@ -1,6 +1,7 @@ +import assert from 'node:assert' import { describe, expect, it } from 'vitest' +import { createCliLogger, type Logger } from '../../src/core/process-coverage.js' import type { PackageCoverage } from '../../src/coverage/model.js' -import { createCliLogger, Logger } from '../../src/core/process-coverage.js' import { filterByChangedLines, filterByGlob } from '../../src/filter/index.js' import type { ChangedLinesMap } from '../../src/filter/model.js' @@ -54,28 +55,28 @@ describe('filterByGlob', () => { it('matches all files with ** pattern', () => { const result = filterByGlob(sampleReport, '**', mockLogger) expect(result).toHaveLength(2) - expect(result[0]!.files).toHaveLength(3) - expect(result[1]!.files).toHaveLength(1) + expect(result[0]?.files).toHaveLength(3) + expect(result[1]?.files).toHaveLength(1) }) it('filters files by directory pattern', () => { const result = filterByGlob(sampleReport, '**/src/**', mockLogger) expect(result).toHaveLength(1) - expect(result[0]!.name).toBe('Package1') - expect(result[0]!.files).toHaveLength(2) + expect(result[0]?.name).toBe('Package1') + expect(result[0]?.files).toHaveLength(2) }) it('filters files by extension pattern', () => { const result = filterByGlob(sampleReport, '**/*.tsx', mockLogger) expect(result).toHaveLength(1) - expect(result[0]!.files).toHaveLength(1) - expect(result[0]!.files[0]!.filename).toBe('src/components/Button.tsx') + expect(result[0]?.files).toHaveLength(1) + expect(result[0]?.files[0]?.filename).toBe('src/components/Button.tsx') }) it('removes empty packages after filtering', () => { const result = filterByGlob(sampleReport, '**/lib/**', mockLogger) expect(result).toHaveLength(1) - expect(result[0]!.name).toBe('Package2') + expect(result[0]?.name).toBe('Package2') }) it('returns empty packages array when nothing matches', () => { @@ -86,8 +87,8 @@ describe('filterByGlob', () => { it('supports specific file matching', () => { const result = filterByGlob(sampleReport, '**/helper.ts', mockLogger) expect(result).toHaveLength(1) - expect(result[0]!.files).toHaveLength(1) - expect(result[0]!.files[0]!.filename).toBe('src/utils/helper.ts') + expect(result[0]?.files).toHaveLength(1) + expect(result[0]?.files[0]?.filename).toBe('src/utils/helper.ts') }) }) @@ -128,9 +129,10 @@ describe('filterByChangedLines', () => { const result = filterByChangedLines(sampleReport, changedLines, mockLogger) expect(result).toHaveLength(1) - expect(result[0]!.files).toHaveLength(1) + expect(result[0]?.files).toHaveLength(1) - const file = result[0]!.files[0]! + const file = result[0]?.files[0] + assert(file) expect(file.filename).toBe('src/file1.ts') expect(file.lines).toHaveLength(2) expect(file.lines.map((l) => l.lineNumber)).toEqual([2, 4]) @@ -141,7 +143,8 @@ describe('filterByChangedLines', () => { const result = filterByChangedLines(sampleReport, changedLines, mockLogger) - const file = result[0]!.files[0]! + const file = result[0]?.files[0] + assert(file) expect(file.lineMetrics.covered).toBe(2) // lines 1 and 3 are covered expect(file.lineMetrics.total).toBe(3) // 3 lines total }) @@ -151,7 +154,8 @@ describe('filterByChangedLines', () => { const result = filterByChangedLines(sampleReport, changedLines, mockLogger) - const file = result[0]!.files[0]! + const file = result[0]?.files[0] + assert(file) expect(file.branchMetrics).toEqual({ covered: 1, total: 2 }) }) @@ -160,8 +164,8 @@ describe('filterByChangedLines', () => { const result = filterByChangedLines(sampleReport, changedLines, mockLogger) - expect(result[0]!.files).toHaveLength(1) - expect(result[0]!.files[0]!.filename).toBe('src/file1.ts') + expect(result[0]?.files).toHaveLength(1) + expect(result[0]?.files[0]?.filename).toBe('src/file1.ts') }) it('excludes files with empty changed lines set', () => { @@ -172,7 +176,7 @@ describe('filterByChangedLines', () => { const result = filterByChangedLines(sampleReport, changedLines, mockLogger) - expect(result[0]!.files).toHaveLength(1) + expect(result[0]?.files).toHaveLength(1) }) it('removes packages with no files after filtering', () => { @@ -215,7 +219,7 @@ describe('filterByChangedLines', () => { // File without resolvedPath should be included with all its lines expect(result).toHaveLength(1) - expect(result[0]!.files).toHaveLength(1) - expect(result[0]!.files[0]!.lines).toHaveLength(2) + expect(result[0]?.files).toHaveLength(1) + expect(result[0]?.files[0]?.lines).toHaveLength(2) }) }) diff --git a/tests/markdown/__snapshots__/ellipsis-leading-1-line.snap.md b/tests/markdown/__snapshots__/ellipsis-leading-1-line.snap.md index ece1de3..c7d649c 100644 --- a/tests/markdown/__snapshots__/ellipsis-leading-1-line.snap.md +++ b/tests/markdown/__snapshots__/ellipsis-leading-1-line.snap.md @@ -1,11 +1,17 @@ -![Line Coverage](https://img.shields.io/badge/Line%20Coverage-75.00%25-green.svg?style=flat) +## Repo Coverage +![Line Coverage](https://img.shields.io/badge/Line%20Coverage-70%25-green.svg?style=flat) ![Branch Coverage](https://img.shields.io/badge/Branch%20Coverage-70%25-green.svg?style=flat) -**TestPackage** (LineCoverage: 75%) +--- -
src/test.ts +## PR Coverage +3/4 changed lines covered (Lines: 75%, Branches: n/a) + +### TestPackage +
+๐Ÿ”ด src/test.ts โ€” 3/4 changed lines covered (Lines: 75%, Branches: n/a) ```typescript - 1 โฌœ some content in line 1 + 1 โฌ› some content in line 1 2 ๐ŸŸฉ some content in line 2 3 ๐ŸŸฅ some content in line 3 4 ๐ŸŸฉ some content in line 4 @@ -13,4 +19,6 @@ ```
-๐ŸŸฅ Not covered, ๐ŸŸจ Missing branch coverage, ๐ŸŸฉ Covered \ No newline at end of file +--- + +Legend: ๐ŸŸฅ uncovered ยท ๐ŸŸจ partial branch ยท ๐ŸŸฉ covered ยท โฌ› non-executable/blank
Generated by `coverage-pr-comment`
\ No newline at end of file diff --git a/tests/markdown/__snapshots__/ellipsis-leading.snap.md b/tests/markdown/__snapshots__/ellipsis-leading.snap.md index 662bcae..b4f3126 100644 --- a/tests/markdown/__snapshots__/ellipsis-leading.snap.md +++ b/tests/markdown/__snapshots__/ellipsis-leading.snap.md @@ -1,16 +1,24 @@ -![Line Coverage](https://img.shields.io/badge/Line%20Coverage-50.00%25-yellowgreen.svg?style=flat) +## Repo Coverage +![Line Coverage](https://img.shields.io/badge/Line%20Coverage-30%25-yellow.svg?style=flat) ![Branch Coverage](https://img.shields.io/badge/Branch%20Coverage-30%25-yellow.svg?style=flat) -**TestPackage** (LineCoverage: 50%) +--- -
src/test.ts +## PR Coverage +1/2 changed lines covered (Lines: 50%, Branches: n/a) + +### TestPackage +
+๐Ÿ”ด src/test.ts โ€” 1/2 changed lines covered (Lines: 50%, Branches: n/a) ```typescript ... - 4 โฌœ some content in line 4 + 4 โฌ› some content in line 4 5 ๐ŸŸฅ some content in line 5 6 ๐ŸŸฉ some content in line 6 ... ```
-๐ŸŸฅ Not covered, ๐ŸŸจ Missing branch coverage, ๐ŸŸฉ Covered \ No newline at end of file +--- + +Legend: ๐ŸŸฅ uncovered ยท ๐ŸŸจ partial branch ยท ๐ŸŸฉ covered ยท โฌ› non-executable/blank
Generated by `coverage-pr-comment`
\ No newline at end of file diff --git a/tests/markdown/__snapshots__/ellipsis-trailing-1-line.snap.md b/tests/markdown/__snapshots__/ellipsis-trailing-1-line.snap.md index f8bb1e3..ce7d179 100644 --- a/tests/markdown/__snapshots__/ellipsis-trailing-1-line.snap.md +++ b/tests/markdown/__snapshots__/ellipsis-trailing-1-line.snap.md @@ -1,16 +1,24 @@ -![Line Coverage](https://img.shields.io/badge/Line%20Coverage-80.00%25-brightgreen.svg?style=flat) +## Repo Coverage +![Line Coverage](https://img.shields.io/badge/Line%20Coverage-80%25-brightgreen.svg?style=flat) ![Branch Coverage](https://img.shields.io/badge/Branch%20Coverage-80%25-brightgreen.svg?style=flat) -**TestPackage** (LineCoverage: 80%) +--- -
src/test.ts +## PR Coverage +4/5 changed lines covered (Lines: 80%, Branches: n/a) + +### TestPackage +
+๐Ÿ”ด src/test.ts โ€” 4/5 changed lines covered (Lines: 80%, Branches: n/a) ```typescript 1 ๐ŸŸฉ some content in line 1 2 ๐ŸŸฉ some content in line 2 3 ๐ŸŸฅ some content in line 3 4 ๐ŸŸฉ some content in line 4 - 5 โฌœ some content in line 5 + 5 โฌ› some content in line 5 ```
-๐ŸŸฅ Not covered, ๐ŸŸจ Missing branch coverage, ๐ŸŸฉ Covered \ No newline at end of file +--- + +Legend: ๐ŸŸฅ uncovered ยท ๐ŸŸจ partial branch ยท ๐ŸŸฉ covered ยท โฌ› non-executable/blank
Generated by `coverage-pr-comment`
\ No newline at end of file diff --git a/tests/markdown/__snapshots__/ellipsis-trailing.snap.md b/tests/markdown/__snapshots__/ellipsis-trailing.snap.md index 7123dc1..abc1d45 100644 --- a/tests/markdown/__snapshots__/ellipsis-trailing.snap.md +++ b/tests/markdown/__snapshots__/ellipsis-trailing.snap.md @@ -1,8 +1,14 @@ -![Line Coverage](https://img.shields.io/badge/Line%20Coverage-66.67%25-green.svg?style=flat) +## Repo Coverage +![Line Coverage](https://img.shields.io/badge/Line%20Coverage-40%25-yellowgreen.svg?style=flat) ![Branch Coverage](https://img.shields.io/badge/Branch%20Coverage-40%25-yellowgreen.svg?style=flat) -**TestPackage** (LineCoverage: 67%) +--- -
src/test.ts +## PR Coverage +2/3 changed lines covered (Lines: 66.67%, Branches: n/a) + +### TestPackage +
+๐Ÿ”ด src/test.ts โ€” 2/3 changed lines covered (Lines: 66.67%, Branches: n/a) ```typescript 1 ๐ŸŸฉ some content in line 1 @@ -12,4 +18,6 @@ ```
-๐ŸŸฅ Not covered, ๐ŸŸจ Missing branch coverage, ๐ŸŸฉ Covered \ No newline at end of file +--- + +Legend: ๐ŸŸฅ uncovered ยท ๐ŸŸจ partial branch ยท ๐ŸŸฉ covered ยท โฌ› non-executable/blank
Generated by `coverage-pr-comment`
\ No newline at end of file diff --git a/tests/markdown/__snapshots__/empty-package.snap.md b/tests/markdown/__snapshots__/empty-package.snap.md index f76bfc9..ffa54c0 100644 --- a/tests/markdown/__snapshots__/empty-package.snap.md +++ b/tests/markdown/__snapshots__/empty-package.snap.md @@ -1 +1,12 @@ -โœ… All lines are covered! \ No newline at end of file +## Repo Coverage +![Line Coverage](https://img.shields.io/badge/Line%20Coverage-42.24%25-yellowgreen.svg?style=flat) ![Branch Coverage](https://img.shields.io/badge/Branch%20Coverage-n%2Fa-brightgreen.svg?style=flat) + +--- + +## PR Coverage + +0/0 changed lines covered + +--- + +Generated by `coverage-pr-comment` \ No newline at end of file diff --git a/tests/markdown/__snapshots__/file-not-found.snap.md b/tests/markdown/__snapshots__/file-not-found.snap.md index c099c52..1d4bb7e 100644 --- a/tests/markdown/__snapshots__/file-not-found.snap.md +++ b/tests/markdown/__snapshots__/file-not-found.snap.md @@ -1,8 +1,14 @@ -![Line Coverage](https://img.shields.io/badge/Line%20Coverage-0.00%25-red.svg?style=flat) +## Repo Coverage +![Line Coverage](https://img.shields.io/badge/Line%20Coverage-42.24%25-yellowgreen.svg?style=flat) ![Branch Coverage](https://img.shields.io/badge/Branch%20Coverage-50%25-yellowgreen.svg?style=flat) -**TestPackage** (LineCoverage: 0%) +--- -
src/missing.ts +## PR Coverage +0/2 changed lines covered (Lines: 0%, Branches: n/a) + +### TestPackage +
+๐Ÿ”ด src/missing.ts โ€” 0/2 changed lines covered (Lines: 0%, Branches: n/a) ```typescript 1 ๐ŸŸฅ @@ -10,4 +16,6 @@ ```
-๐ŸŸฅ Not covered, ๐ŸŸจ Missing branch coverage, ๐ŸŸฉ Covered \ No newline at end of file +--- + +Legend: ๐ŸŸฅ uncovered ยท ๐ŸŸจ partial branch ยท ๐ŸŸฉ covered ยท โฌ› non-executable/blank
Generated by `coverage-pr-comment`
\ No newline at end of file diff --git a/tests/markdown/__snapshots__/gap-1-line.snap.md b/tests/markdown/__snapshots__/gap-1-line.snap.md index 9a18a12..e89414a 100644 --- a/tests/markdown/__snapshots__/gap-1-line.snap.md +++ b/tests/markdown/__snapshots__/gap-1-line.snap.md @@ -1,8 +1,14 @@ -![Line Coverage](https://img.shields.io/badge/Line%20Coverage-60.00%25-green.svg?style=flat) +## Repo Coverage +![Line Coverage](https://img.shields.io/badge/Line%20Coverage-50%25-yellowgreen.svg?style=flat) ![Branch Coverage](https://img.shields.io/badge/Branch%20Coverage-50%25-yellowgreen.svg?style=flat) -**TestPackage** (LineCoverage: 60%) +--- -
src/test.ts +## PR Coverage +3/5 changed lines covered (Lines: 60%, Branches: n/a) + +### TestPackage +
+๐Ÿ”ด src/test.ts โ€” 3/5 changed lines covered (Lines: 60%, Branches: n/a) ```typescript 1 ๐ŸŸฉ some content in line 1 @@ -13,4 +19,6 @@ ```
-๐ŸŸฅ Not covered, ๐ŸŸจ Missing branch coverage, ๐ŸŸฉ Covered \ No newline at end of file +--- + +Legend: ๐ŸŸฅ uncovered ยท ๐ŸŸจ partial branch ยท ๐ŸŸฉ covered ยท โฌ› non-executable/blank
Generated by `coverage-pr-comment`
\ No newline at end of file diff --git a/tests/markdown/__snapshots__/gap-2-plus-lines.snap.md b/tests/markdown/__snapshots__/gap-2-plus-lines.snap.md index 24e94b5..b7564c6 100644 --- a/tests/markdown/__snapshots__/gap-2-plus-lines.snap.md +++ b/tests/markdown/__snapshots__/gap-2-plus-lines.snap.md @@ -1,8 +1,14 @@ -![Line Coverage](https://img.shields.io/badge/Line%20Coverage-66.67%25-green.svg?style=flat) +## Repo Coverage +![Line Coverage](https://img.shields.io/badge/Line%20Coverage-60%25-green.svg?style=flat) ![Branch Coverage](https://img.shields.io/badge/Branch%20Coverage-60%25-green.svg?style=flat) -**TestPackage** (LineCoverage: 67%) +--- -
src/test.ts +## PR Coverage +4/6 changed lines covered (Lines: 66.67%, Branches: n/a) + +### TestPackage +
+๐Ÿ”ด src/test.ts โ€” 4/6 changed lines covered (Lines: 66.67%, Branches: n/a) ```typescript 1 ๐ŸŸฉ some content in line 1 @@ -16,4 +22,6 @@ ```
-๐ŸŸฅ Not covered, ๐ŸŸจ Missing branch coverage, ๐ŸŸฉ Covered \ No newline at end of file +--- + +Legend: ๐ŸŸฅ uncovered ยท ๐ŸŸจ partial branch ยท ๐ŸŸฉ covered ยท โฌ› non-executable/blank
Generated by `coverage-pr-comment`
\ No newline at end of file diff --git a/tests/markdown/__snapshots__/line-exceeds-file-length.snap.md b/tests/markdown/__snapshots__/line-exceeds-file-length.snap.md index 9d13d3d..6cc4b52 100644 --- a/tests/markdown/__snapshots__/line-exceeds-file-length.snap.md +++ b/tests/markdown/__snapshots__/line-exceeds-file-length.snap.md @@ -1,8 +1,14 @@ -![Line Coverage](https://img.shields.io/badge/Line%20Coverage-66.67%25-green.svg?style=flat) +## Repo Coverage +![Line Coverage](https://img.shields.io/badge/Line%20Coverage-90%25-brightgreen.svg?style=flat) ![Branch Coverage](https://img.shields.io/badge/Branch%20Coverage-90%25-brightgreen.svg?style=flat) -**TestPackage** (LineCoverage: 67%) +--- -
src/short.ts +## PR Coverage +2/3 changed lines covered (Lines: 66.67%, Branches: n/a) + +### TestPackage +
+๐Ÿ”ด src/short.ts โ€” 2/3 changed lines covered (Lines: 66.67%, Branches: n/a) ```typescript ... @@ -12,4 +18,6 @@ ```
-๐ŸŸฅ Not covered, ๐ŸŸจ Missing branch coverage, ๐ŸŸฉ Covered \ No newline at end of file +--- + +Legend: ๐ŸŸฅ uncovered ยท ๐ŸŸจ partial branch ยท ๐ŸŸฉ covered ยท โฌ› non-executable/blank
Generated by `coverage-pr-comment`
\ No newline at end of file diff --git a/tests/markdown/__snapshots__/line-gaps-ellipsis.snap.md b/tests/markdown/__snapshots__/line-gaps-ellipsis.snap.md index 0be87f4..0001280 100644 --- a/tests/markdown/__snapshots__/line-gaps-ellipsis.snap.md +++ b/tests/markdown/__snapshots__/line-gaps-ellipsis.snap.md @@ -1,20 +1,27 @@ -![Line Coverage](https://img.shields.io/badge/Line%20Coverage-40.00%25-yellowgreen.svg?style=flat) -![Branch Coverage](https://img.shields.io/badge/Branch%20Coverage-0.00%25-red.svg?style=flat) +## Repo Coverage +![Line Coverage](https://img.shields.io/badge/Line%20Coverage-42.24%25-yellowgreen.svg?style=flat) ![Branch Coverage](https://img.shields.io/badge/Branch%20Coverage-n%2Fa-brightgreen.svg?style=flat) -**GapsPackage** (LineCoverage: 40%, BranchCoverage: 0%) +--- -
src/gaps.rs +## PR Coverage +2/5 changed lines covered (Lines: 40%, Branches: 0%) + +### GapsPackage +
+๐Ÿ”ด src/gaps.rs โ€” 2/5 changed lines covered (Lines: 40%, Branches: 0%) ```rust ... - 9 โฌœ some content in line 9 + 9 โฌ› some content in line 9 10 ๐ŸŸฅ some content in line 10 11 ๐ŸŸฅ some content in line 11 - 12 โฌœ some content in line 12 + 12 โฌ› some content in line 12 ... - 49 โฌœ some content in line 49 + 49 โฌ› some content in line 49 50 ๐ŸŸจ some content in line 50 ```
-๐ŸŸฅ Not covered, ๐ŸŸจ Missing branch coverage, ๐ŸŸฉ Covered \ No newline at end of file +--- + +Legend: ๐ŸŸฅ uncovered ยท ๐ŸŸจ partial branch ยท ๐ŸŸฉ covered ยท โฌ› non-executable/blank
Generated by `coverage-pr-comment`
\ No newline at end of file diff --git a/tests/markdown/__snapshots__/multiple-files-same-package.snap.md b/tests/markdown/__snapshots__/multiple-files-same-package.snap.md index ba1a590..ec8be18 100644 --- a/tests/markdown/__snapshots__/multiple-files-same-package.snap.md +++ b/tests/markdown/__snapshots__/multiple-files-same-package.snap.md @@ -1,9 +1,16 @@ -![Line Coverage](https://img.shields.io/badge/Line%20Coverage-60.00%25-green.svg?style=flat) -![Branch Coverage](https://img.shields.io/badge/Branch%20Coverage-0.00%25-red.svg?style=flat) +## Repo Coverage +![Line Coverage](https://img.shields.io/badge/Line%20Coverage-10%25-red.svg?style=flat) ![Branch Coverage](https://img.shields.io/badge/Branch%20Coverage-10%25-red.svg?style=flat) -**MultiFilePackage** (LineCoverage: 60%, BranchCoverage: 0%) +--- -
src/file2.py +## PR Coverage +3/5 changed lines covered (Lines: 60%, Branches: 0%) + +### MultiFilePackage +โœ“ ๐ŸŸข src/file1.py โ€” 2/2 changed lines covered + +
+๐Ÿ”ด src/file2.py โ€” 1/3 changed lines covered (Lines: 33.33%, Branches: 0%) ```python 1 ๐ŸŸฅ some content in line 1 @@ -12,4 +19,6 @@ ```
-๐ŸŸฅ Not covered, ๐ŸŸจ Missing branch coverage, ๐ŸŸฉ Covered \ No newline at end of file +--- + +Legend: ๐ŸŸฅ uncovered ยท ๐ŸŸจ partial branch ยท ๐ŸŸฉ covered ยท โฌ› non-executable/blank
Generated by `coverage-pr-comment`
\ No newline at end of file diff --git a/tests/markdown/__snapshots__/multiple-packages.snap.md b/tests/markdown/__snapshots__/multiple-packages.snap.md index cf9df69..4d2a251 100644 --- a/tests/markdown/__snapshots__/multiple-packages.snap.md +++ b/tests/markdown/__snapshots__/multiple-packages.snap.md @@ -1,17 +1,28 @@ -![Line Coverage](https://img.shields.io/badge/Line%20Coverage-60.00%25-green.svg?style=flat) +## Repo Coverage +![Line Coverage](https://img.shields.io/badge/Line%20Coverage-42.24%25-yellowgreen.svg?style=flat) ![Branch Coverage](https://img.shields.io/badge/Branch%20Coverage-n%2Fa-brightgreen.svg?style=flat) -**Package.Tests** (LineCoverage: 0%) +--- -
tests/test_utils.cs +## PR Coverage +3/5 changed lines covered (Lines: 60%, Branches: n/a) + +### Package.Tests +
+๐Ÿ”ด tests/test_utils.cs โ€” 0/2 changed lines covered (Lines: 0%, Branches: n/a) ```csharp ... - 4 โฌœ some content in line 4 + 4 โฌ› some content in line 4 5 ๐ŸŸฅ some content in line 5 6 ๐ŸŸฅ some content in line 6 - 7 โฌœ some content in line 7 + 7 โฌ› some content in line 7 ... ```
-๐ŸŸฅ Not covered, ๐ŸŸจ Missing branch coverage, ๐ŸŸฉ Covered \ No newline at end of file +### Package.Core +โœ“ ๐ŸŸข src/core/utils.cs โ€” 3/3 changed lines covered + +--- + +Legend: ๐ŸŸฅ uncovered ยท ๐ŸŸจ partial branch ยท ๐ŸŸฉ covered ยท โฌ› non-executable/blank
Generated by `coverage-pr-comment`
\ No newline at end of file diff --git a/tests/markdown/__snapshots__/simple-packages.snap.md b/tests/markdown/__snapshots__/simple-packages.snap.md new file mode 100644 index 0000000..f6b2c7c --- /dev/null +++ b/tests/markdown/__snapshots__/simple-packages.snap.md @@ -0,0 +1,25 @@ +## Repo Coverage +![Line Coverage](https://img.shields.io/badge/Line%20Coverage-42.24%25-yellowgreen.svg?style=flat) ![Branch Coverage](https://img.shields.io/badge/Branch%20Coverage-n%2Fa-brightgreen.svg?style=flat) + +--- + +## PR Coverage +3/4 changed lines covered (Lines: 75%, Branches: 0%) + +### TestPackage +
+๐Ÿ”ด src/example.ts โ€” 3/4 changed lines covered (Lines: 75%, Branches: 0%) + +```typescript + 1 ๐ŸŸฉ some content in line 1 + 2 ๐ŸŸฉ some content in line 2 + 3 ๐ŸŸฅ some content in line 3 + 4 ๐ŸŸจ some content in line 4 + 5 โฌ› some content in line 5 +... +``` +
+ +--- + +Legend: ๐ŸŸฅ uncovered ยท ๐ŸŸจ partial branch ยท ๐ŸŸฉ covered ยท โฌ› non-executable/blank
Generated by `coverage-pr-comment`
\ No newline at end of file diff --git a/tests/markdown/__snapshots__/simple-report.snap.md b/tests/markdown/__snapshots__/simple-report.snap.md deleted file mode 100644 index 57bd921..0000000 --- a/tests/markdown/__snapshots__/simple-report.snap.md +++ /dev/null @@ -1,18 +0,0 @@ -![Line Coverage](https://img.shields.io/badge/Line%20Coverage-75.00%25-green.svg?style=flat) -![Branch Coverage](https://img.shields.io/badge/Branch%20Coverage-0.00%25-red.svg?style=flat) - -**TestPackage** (LineCoverage: 75%, BranchCoverage: 0%) - -
src/example.ts - -```typescript - 1 ๐ŸŸฉ some content in line 1 - 2 ๐ŸŸฉ some content in line 2 - 3 ๐ŸŸฅ some content in line 3 - 4 ๐ŸŸจ some content in line 4 - 5 โฌœ some content in line 5 -... -``` -
- -๐ŸŸฅ Not covered, ๐ŸŸจ Missing branch coverage, ๐ŸŸฉ Covered \ No newline at end of file diff --git a/tests/markdown/__snapshots__/truncation-files.snap.md b/tests/markdown/__snapshots__/truncation-files.snap.md index 0ef9607..dc403c4 100644 --- a/tests/markdown/__snapshots__/truncation-files.snap.md +++ b/tests/markdown/__snapshots__/truncation-files.snap.md @@ -1,23 +1,32 @@ -![Line Coverage](https://img.shields.io/badge/Line%20Coverage-0.00%25-red.svg?style=flat) +## Repo Coverage +![Line Coverage](https://img.shields.io/badge/Line%20Coverage-42.24%25-yellowgreen.svg?style=flat) ![Branch Coverage](https://img.shields.io/badge/Branch%20Coverage-50%25-yellowgreen.svg?style=flat) -**MultiFilePkg** (LineCoverage: 0%) +--- -
file1.ts +## PR Coverage +0/3 changed lines covered (Lines: 0%, Branches: n/a) + +### MultiFilePkg +
+๐Ÿ”ด file1.ts โ€” 0/1 changed lines covered (Lines: 0%, Branches: n/a) ```typescript 1 ๐ŸŸฅ some content in line 1 - 2 โฌœ some content in line 2 + 2 โฌ› some content in line 2 ```
-
file2.ts +
+๐Ÿ”ด file2.ts โ€” 0/1 changed lines covered (Lines: 0%, Branches: n/a) ```typescript 1 ๐ŸŸฅ some content in line 1 - 2 โฌœ some content in line 2 + 2 โฌ› some content in line 2 ```
... (1 file(s) not shown due to size limit) -๐ŸŸฅ Not covered, ๐ŸŸจ Missing branch coverage, ๐ŸŸฉ Covered \ No newline at end of file +--- + +Legend: ๐ŸŸฅ uncovered ยท ๐ŸŸจ partial branch ยท ๐ŸŸฉ covered ยท โฌ› non-executable/blank
Generated by `coverage-pr-comment`
\ No newline at end of file diff --git a/tests/markdown/__snapshots__/truncation-minimal.snap.md b/tests/markdown/__snapshots__/truncation-minimal.snap.md index 11a875e..3b3899c 100644 --- a/tests/markdown/__snapshots__/truncation-minimal.snap.md +++ b/tests/markdown/__snapshots__/truncation-minimal.snap.md @@ -1,23 +1,32 @@ -![Line Coverage](https://img.shields.io/badge/Line%20Coverage-0.00%25-red.svg?style=flat) +## Repo Coverage +![Line Coverage](https://img.shields.io/badge/Line%20Coverage-42.24%25-yellowgreen.svg?style=flat) ![Branch Coverage](https://img.shields.io/badge/Branch%20Coverage-50%25-yellowgreen.svg?style=flat) -**LargePkg** (LineCoverage: 0%) +--- -
file1.ts +## PR Coverage +0/4 changed lines covered (Lines: 0%, Branches: n/a) + +### LargePkg +
+๐Ÿ”ด file1.ts โ€” 0/1 changed lines covered (Lines: 0%, Branches: n/a) ```typescript 1 ๐ŸŸฅ some content in line 1 - 2 โฌœ some content in line 2 + 2 โฌ› some content in line 2 ```
-
file2.ts +
+๐Ÿ”ด file2.ts โ€” 0/1 changed lines covered (Lines: 0%, Branches: n/a) ```typescript 1 ๐ŸŸฅ some content in line 1 - 2 โฌœ some content in line 2 + 2 โฌ› some content in line 2 ```
... (2 file(s) not shown due to size limit) -๐ŸŸฅ Not covered, ๐ŸŸจ Missing branch coverage, ๐ŸŸฉ Covered \ No newline at end of file +--- + +Legend: ๐ŸŸฅ uncovered ยท ๐ŸŸจ partial branch ยท ๐ŸŸฉ covered ยท โฌ› non-executable/blank
Generated by `coverage-pr-comment`
\ No newline at end of file diff --git a/tests/markdown/__snapshots__/truncation-none.snap.md b/tests/markdown/__snapshots__/truncation-none.snap.md index 931f294..ad5a05f 100644 --- a/tests/markdown/__snapshots__/truncation-none.snap.md +++ b/tests/markdown/__snapshots__/truncation-none.snap.md @@ -1,13 +1,21 @@ -![Line Coverage](https://img.shields.io/badge/Line%20Coverage-0.00%25-red.svg?style=flat) +## Repo Coverage +![Line Coverage](https://img.shields.io/badge/Line%20Coverage-42.24%25-yellowgreen.svg?style=flat) ![Branch Coverage](https://img.shields.io/badge/Branch%20Coverage-50%25-yellowgreen.svg?style=flat) -**SmallPkg** (LineCoverage: 0%) +--- -
small.ts +## PR Coverage +0/1 changed lines covered (Lines: 0%, Branches: n/a) + +### SmallPkg +
+๐Ÿ”ด small.ts โ€” 0/1 changed lines covered (Lines: 0%, Branches: n/a) ```typescript 1 ๐ŸŸฅ some content in line 1 - 2 โฌœ some content in line 2 + 2 โฌ› some content in line 2 ```
-๐ŸŸฅ Not covered, ๐ŸŸจ Missing branch coverage, ๐ŸŸฉ Covered \ No newline at end of file +--- + +Legend: ๐ŸŸฅ uncovered ยท ๐ŸŸจ partial branch ยท ๐ŸŸฉ covered ยท โฌ› non-executable/blank
Generated by `coverage-pr-comment`
\ No newline at end of file diff --git a/tests/markdown/__snapshots__/truncation-packages.snap.md b/tests/markdown/__snapshots__/truncation-packages.snap.md index 146b662..951fb6e 100644 --- a/tests/markdown/__snapshots__/truncation-packages.snap.md +++ b/tests/markdown/__snapshots__/truncation-packages.snap.md @@ -1,20 +1,27 @@ -![Line Coverage](https://img.shields.io/badge/Line%20Coverage-0.00%25-red.svg?style=flat) +## Repo Coverage +![Line Coverage](https://img.shields.io/badge/Line%20Coverage-42.24%25-yellowgreen.svg?style=flat) ![Branch Coverage](https://img.shields.io/badge/Branch%20Coverage-50%25-yellowgreen.svg?style=flat) -**Pkg1** (LineCoverage: 0%) +--- -
pkg1/a.ts +## PR Coverage +0/3 changed lines covered (Lines: 0%, Branches: n/a) + +### Pkg1 +
+๐Ÿ”ด pkg1/a.ts โ€” 0/1 changed lines covered (Lines: 0%, Branches: n/a) ```typescript 1 ๐ŸŸฅ some content in line 1 - 2 โฌœ some content in line 2 - 3 โฌœ some content in line 3 + 2 โฌ› some content in line 2 + 3 โฌ› some content in line 3 4 ๐ŸŸฅ some content in line 4 - 5 โฌœ some content in line 5 + 5 โฌ› some content in line 5 ```
-**Pkg2** (LineCoverage: 0%) - +### Pkg2 ... (2 file(s) and 1 package(s) not shown due to size limit) -๐ŸŸฅ Not covered, ๐ŸŸจ Missing branch coverage, ๐ŸŸฉ Covered \ No newline at end of file +--- + +Legend: ๐ŸŸฅ uncovered ยท ๐ŸŸจ partial branch ยท ๐ŸŸฉ covered ยท โฌ› non-executable/blank
Generated by `coverage-pr-comment`
\ No newline at end of file diff --git a/tests/markdown/generator.test.ts b/tests/markdown/generator.test.ts index 068775c..7b837bc 100644 --- a/tests/markdown/generator.test.ts +++ b/tests/markdown/generator.test.ts @@ -1,4 +1,5 @@ import { describe, expect, it } from 'vitest' +import { createCliLogger } from '../../src/core/process-coverage.js' import type { PackageCoverage } from '../../src/coverage/model.js' import { generateMarkdown, MINIMUM_CHARACTERS } from '../../src/markdown/index.js' @@ -7,8 +8,10 @@ type FakeFileInfo = { numberOfLines: number } +const logger = createCliLogger(true) + /** - * Generate fake file contents for a coverage report. + * Generate fake file contents for a coverage packages. * Creates mock lines keyed by resolvedPath. */ function createFakeFileContents(fileInfos: FakeFileInfo[]): Map { @@ -25,8 +28,8 @@ function createFakeFileContents(fileInfos: FakeFileInfo[]): Map { - it('generates markdown for a simple report', async () => { - const report: PackageCoverage[] = [ + it('generates markdown for a simple packages', async () => { + const packages: PackageCoverage[] = [ { name: 'TestPackage', files: [ @@ -47,15 +50,18 @@ describe('generateMarkdown', () => { ] const markdown = generateMarkdown( - report, + packages, createFakeFileContents([{ resolvedPath: '/repo/src/example.ts', numberOfLines: 10 }]), + { lineCoverage: 42.24, branchCoverage: undefined }, + {}, + logger, ) - await expect(markdown).toMatchFileSnapshot('./__snapshots__/simple-report.snap.md') + await expect(markdown).toMatchFileSnapshot('./__snapshots__/simple-packages.snap.md') }) it('generates markdown for multiple packages', async () => { - const report: PackageCoverage[] = [ + const packages: PackageCoverage[] = [ { name: 'Package.Core', files: [ @@ -88,18 +94,21 @@ describe('generateMarkdown', () => { ] const markdown = generateMarkdown( - report, + packages, createFakeFileContents([ { resolvedPath: '/repo/src/core/utils.cs', numberOfLines: 15 }, { resolvedPath: '/repo/tests/test_utils.cs', numberOfLines: 10 }, ]), + { lineCoverage: 42.24, branchCoverage: undefined }, + {}, + logger, ) await expect(markdown).toMatchFileSnapshot('./__snapshots__/multiple-packages.snap.md') }) it('generates markdown with line gaps shown as ellipsis', async () => { - const report: PackageCoverage[] = [ + const packages: PackageCoverage[] = [ { name: 'GapsPackage', files: [ @@ -123,8 +132,11 @@ describe('generateMarkdown', () => { ] const markdown = generateMarkdown( - report, + packages, createFakeFileContents([{ resolvedPath: '/repo/src/gaps.rs', numberOfLines: 50 }]), + { lineCoverage: 42.24, branchCoverage: undefined }, + {}, + logger, ) await expect(markdown).toMatchFileSnapshot('./__snapshots__/line-gaps-ellipsis.snap.md') @@ -132,20 +144,26 @@ describe('generateMarkdown', () => { }) it('generates markdown for empty package', async () => { - const report: PackageCoverage[] = [ + const packages: PackageCoverage[] = [ { name: 'EmptyPackage', files: [], }, ] - const markdown = generateMarkdown(report, createFakeFileContents([])) + const markdown = generateMarkdown( + packages, + createFakeFileContents([]), + { lineCoverage: 42.24, branchCoverage: undefined }, + {}, + logger, + ) await expect(markdown).toMatchFileSnapshot('./__snapshots__/empty-package.snap.md') }) it('generates markdown with multiple files in same package', async () => { - const report: PackageCoverage[] = [ + const packages: PackageCoverage[] = [ { name: 'MultiFilePackage', files: [ @@ -174,18 +192,21 @@ describe('generateMarkdown', () => { ] const markdown = generateMarkdown( - report, + packages, createFakeFileContents([ { resolvedPath: '/repo/src/file1.py', numberOfLines: 3 }, { resolvedPath: '/repo/src/file2.py', numberOfLines: 3 }, ]), + { lineCoverage: 10, branchCoverage: 10 }, + {}, + logger, ) await expect(markdown).toMatchFileSnapshot('./__snapshots__/multiple-files-same-package.snap.md') }) it('correctly maps file extensions to syntax highlighting', () => { - const report: PackageCoverage[] = [ + const packages: PackageCoverage[] = [ { name: 'ExtensionTest', files: [ @@ -212,12 +233,15 @@ describe('generateMarkdown', () => { ] const markdown = generateMarkdown( - report, + packages, createFakeFileContents([ { resolvedPath: '/repo/test.cs', numberOfLines: 1 }, { resolvedPath: '/repo/test.rs', numberOfLines: 2 }, { resolvedPath: '/repo/test.tsx', numberOfLines: 3 }, ]), + { lineCoverage: 20, branchCoverage: 20 }, + {}, + logger, ) expect(markdown).toContain('```csharp') @@ -227,7 +251,7 @@ describe('generateMarkdown', () => { describe('ellipsis handling', () => { it('prepends ellipsis when file has content before first shown line', async () => { - const report: PackageCoverage[] = [ + const packages: PackageCoverage[] = [ { name: 'TestPackage', files: [ @@ -246,15 +270,18 @@ describe('generateMarkdown', () => { ] const markdown = generateMarkdown( - report, + packages, createFakeFileContents([{ resolvedPath: '/repo/src/test.ts', numberOfLines: 10 }]), + { lineCoverage: 30, branchCoverage: 30 }, + {}, + logger, ) await expect(markdown).toMatchFileSnapshot('./__snapshots__/ellipsis-leading.snap.md') }) it('appends ellipsis when file has content after last shown line', async () => { - const report: PackageCoverage[] = [ + const packages: PackageCoverage[] = [ { name: 'TestPackage', files: [ @@ -274,15 +301,18 @@ describe('generateMarkdown', () => { ] const markdown = generateMarkdown( - report, + packages, createFakeFileContents([{ resolvedPath: '/repo/src/test.ts', numberOfLines: 10 }]), + { lineCoverage: 40, branchCoverage: 40 }, + {}, + logger, ) await expect(markdown).toMatchFileSnapshot('./__snapshots__/ellipsis-trailing.snap.md') }) it('shows actual line instead of ellipsis for 1-line gap', async () => { - const report: PackageCoverage[] = [ + const packages: PackageCoverage[] = [ { name: 'TestPackage', files: [ @@ -304,15 +334,18 @@ describe('generateMarkdown', () => { ] const markdown = generateMarkdown( - report, + packages, createFakeFileContents([{ resolvedPath: '/repo/src/test.ts', numberOfLines: 5 }]), + { lineCoverage: 50, branchCoverage: 50 }, + {}, + logger, ) await expect(markdown).toMatchFileSnapshot('./__snapshots__/gap-1-line.snap.md') }) it('shows single ellipsis for 2+ line gap', async () => { - const report: PackageCoverage[] = [ + const packages: PackageCoverage[] = [ { name: 'TestPackage', files: [ @@ -336,15 +369,18 @@ describe('generateMarkdown', () => { ] const markdown = generateMarkdown( - report, + packages, createFakeFileContents([{ resolvedPath: '/repo/src/test.ts', numberOfLines: 15 }]), + { lineCoverage: 60, branchCoverage: 60 }, + {}, + logger, ) await expect(markdown).toMatchFileSnapshot('./__snapshots__/gap-2-plus-lines.snap.md') }) it('shows actual line 1 instead of leading ellipsis when only 1 line hidden at start', async () => { - const report: PackageCoverage[] = [ + const packages: PackageCoverage[] = [ { name: 'TestPackage', files: [ @@ -365,15 +401,18 @@ describe('generateMarkdown', () => { ] const markdown = generateMarkdown( - report, + packages, createFakeFileContents([{ resolvedPath: '/repo/src/test.ts', numberOfLines: 6 }]), + { lineCoverage: 70, branchCoverage: 70 }, + {}, + logger, ) await expect(markdown).toMatchFileSnapshot('./__snapshots__/ellipsis-leading-1-line.snap.md') }) it('shows actual last line instead of trailing ellipsis when only 1 line hidden at end', async () => { - const report: PackageCoverage[] = [ + const packages: PackageCoverage[] = [ { name: 'TestPackage', files: [ @@ -394,8 +433,11 @@ describe('generateMarkdown', () => { ] const markdown = generateMarkdown( - report, + packages, createFakeFileContents([{ resolvedPath: '/repo/src/test.ts', numberOfLines: 5 }]), + { lineCoverage: 80, branchCoverage: 80 }, + {}, + logger, ) await expect(markdown).toMatchFileSnapshot('./__snapshots__/ellipsis-trailing-1-line.snap.md') @@ -404,7 +446,7 @@ describe('generateMarkdown', () => { describe('missing file content handling', () => { it('renders empty content when file is not found', async () => { - const report: PackageCoverage[] = [ + const packages: PackageCoverage[] = [ { name: 'TestPackage', files: [ @@ -424,13 +466,13 @@ describe('generateMarkdown', () => { // Empty map - file not found on disk const fileContents = new Map() - const markdown = generateMarkdown(report, fileContents) + const markdown = generateMarkdown(packages, fileContents, { lineCoverage: 42.24, branchCoverage: 50 }, {}, logger) await expect(markdown).toMatchFileSnapshot('./__snapshots__/file-not-found.snap.md') }) it('renders empty content when line number exceeds file length', async () => { - const report: PackageCoverage[] = [ + const packages: PackageCoverage[] = [ { name: 'TestPackage', files: [ @@ -450,8 +492,11 @@ describe('generateMarkdown', () => { ] const markdown = generateMarkdown( - report, + packages, createFakeFileContents([{ resolvedPath: '/repo/src/short.ts', numberOfLines: 3 }]), + { lineCoverage: 90, branchCoverage: 90 }, + {}, + logger, ) await expect(markdown).toMatchFileSnapshot('./__snapshots__/line-exceeds-file-length.snap.md') @@ -460,7 +505,7 @@ describe('generateMarkdown', () => { describe('character limit and truncation', () => { it('throws error when maxCharacters is below minimum', () => { - const report: PackageCoverage[] = [ + const packages: PackageCoverage[] = [ { name: 'Pkg', files: [ @@ -475,14 +520,20 @@ describe('generateMarkdown', () => { ] expect(() => - generateMarkdown(report, createFakeFileContents([{ resolvedPath: '/repo/a.ts', numberOfLines: 1 }]), { - maxCharacters: 100, - }), + generateMarkdown( + packages, + createFakeFileContents([{ resolvedPath: '/repo/a.ts', numberOfLines: 1 }]), + { lineCoverage: 100, branchCoverage: 100 }, + { + maxCharacters: 100, + }, + logger, + ), ).toThrow(`maxCharacters must be at least ${MINIMUM_CHARACTERS}`) }) it('does not truncate when content fits within limit', async () => { - const report: PackageCoverage[] = [ + const packages: PackageCoverage[] = [ { name: 'SmallPkg', files: [ @@ -497,11 +548,13 @@ describe('generateMarkdown', () => { ] const markdown = generateMarkdown( - report, + packages, createFakeFileContents([{ resolvedPath: '/repo/small.ts', numberOfLines: 2 }]), + { lineCoverage: 42.24, branchCoverage: 50 }, { maxCharacters: 2000, }, + logger, ) await expect(markdown).toMatchFileSnapshot('./__snapshots__/truncation-none.snap.md') @@ -509,7 +562,7 @@ describe('generateMarkdown', () => { }) it('truncates files from end when limit exceeded', async () => { - const report: PackageCoverage[] = [ + const packages: PackageCoverage[] = [ { name: 'MultiFilePkg', files: [ @@ -536,13 +589,15 @@ describe('generateMarkdown', () => { ] const markdown = generateMarkdown( - report, + packages, createFakeFileContents([ { resolvedPath: '/repo/file1.ts', numberOfLines: 2 }, { resolvedPath: '/repo/file2.ts', numberOfLines: 2 }, { resolvedPath: '/repo/file3.ts', numberOfLines: 2 }, ]), - { maxCharacters: 550 }, + { lineCoverage: 42.24, branchCoverage: 50 }, + { maxCharacters: 1000 }, + logger, ) await expect(markdown).toMatchFileSnapshot('./__snapshots__/truncation-files.snap.md') @@ -551,7 +606,7 @@ describe('generateMarkdown', () => { }) it('truncates entire packages from end when limit exceeded', async () => { - const report: PackageCoverage[] = [ + const packages: PackageCoverage[] = [ { name: 'Pkg1', files: [ @@ -591,13 +646,15 @@ describe('generateMarkdown', () => { ] const markdown = generateMarkdown( - report, + packages, createFakeFileContents([ { resolvedPath: '/repo/pkg1/a.ts', numberOfLines: 5 }, { resolvedPath: '/repo/pkg2/b.ts', numberOfLines: 2 }, { resolvedPath: '/repo/pkg3/c.ts', numberOfLines: 2 }, ]), - { maxCharacters: 500 }, + { lineCoverage: 42.24, branchCoverage: 50 }, + { maxCharacters: 900 }, + logger, ) await expect(markdown).toMatchFileSnapshot('./__snapshots__/truncation-packages.snap.md') @@ -607,7 +664,7 @@ describe('generateMarkdown', () => { it('shows minimal output with badges and legend when severely limited', async () => { // Create multiple files to force truncation at minimum limit - const report: PackageCoverage[] = [ + const packages: PackageCoverage[] = [ { name: 'LargePkg', files: [ @@ -640,26 +697,28 @@ describe('generateMarkdown', () => { ] const markdown = generateMarkdown( - report, + packages, createFakeFileContents([ { resolvedPath: '/repo/file1.ts', numberOfLines: 2 }, { resolvedPath: '/repo/file2.ts', numberOfLines: 2 }, { resolvedPath: '/repo/file3.ts', numberOfLines: 2 }, { resolvedPath: '/repo/file4.ts', numberOfLines: 2 }, ]), - { maxCharacters: 500 }, + { lineCoverage: 42.24, branchCoverage: 50 }, + { maxCharacters: 1000 }, + logger, ) await expect(markdown).toMatchFileSnapshot('./__snapshots__/truncation-minimal.snap.md') // Should always have badges and legend expect(markdown).toContain('img.shields.io') - expect(markdown).toContain('Not covered') + expect(markdown).toContain('uncovered') // Should show truncation notice expect(markdown).toContain('not shown due to size limit') }) it('uses default maxCharacters of 65536 when not specified', () => { - const report: PackageCoverage[] = [ + const packages: PackageCoverage[] = [ { name: 'Pkg', files: [ @@ -675,8 +734,11 @@ describe('generateMarkdown', () => { // Should not throw and should not truncate small content const markdown = generateMarkdown( - report, + packages, createFakeFileContents([{ resolvedPath: '/repo/test.ts', numberOfLines: 2 }]), + { lineCoverage: 42.24, branchCoverage: 50 }, + {}, + logger, ) expect(markdown).not.toContain('not shown due to size limit') diff --git a/tests/resources/integration/glob-filter/coverage.xml b/tests/resources/integration/glob-filter/coverage.xml index 46ec905..d495728 100644 --- a/tests/resources/integration/glob-filter/coverage.xml +++ b/tests/resources/integration/glob-filter/coverage.xml @@ -5,14 +5,18 @@ - - + + - - + + + + + + diff --git a/tests/resources/integration/glob-filter/src/exclude/ignored.ts b/tests/resources/integration/glob-filter/src/exclude/ignored.ts index e2a6853..16283c8 100644 --- a/tests/resources/integration/glob-filter/src/exclude/ignored.ts +++ b/tests/resources/integration/glob-filter/src/exclude/ignored.ts @@ -1,3 +1,7 @@ export function ignored(): string { return 'I am ignored' } + +// filler +// filler +// filler