App-Test-Generator
view release on metacpan or search on metacpan
bin/test-generator-index view on Meta::CPAN
HTML
my $js_data = join(",\n", @data_points);
push @html, "const dataPoints = [ $js_data ];";
push @html, <<'HTML';
const regressionPoints = linearRegression(dataPoints);
// Try to register the zoom plugin (handles different UMD builds)
(function registerZoomPlugin(){
try {
const candidates = ['chartjsPluginZoom','ChartZoom','zoomPlugin','chartjs_plugin_zoom','ChartjsPluginZoom','chartjsPluginZoom'];
for(const name of candidates) {
if(window[name]) {
try { Chart.register(window[name]); console.log('Registered zoom plugin:', name); return; } catch(e) { console.warn('zoom register failed for', name, e); }
}
}
// Some CDN builds auto-register the plugin; if nothing found that's OK (feature disabled).
} catch(e) {
console.warn('registerZoomPlugin error', e);
}
})();
const ctx = document.getElementById('coverageTrend').getContext('2d');
const chart = new Chart(ctx, {
type: 'line',
data: {
datasets: [{
label: 'Total Coverage (%)',
data: dataPoints,
borderColor: 'green',
backgroundColor: 'rgba(0,128,0,0.1)',
pointRadius: 5,
pointHoverRadius: 7,
pointStyle: 'circle',
fill: false,
tension: 0.3,
pointBackgroundColor: function(context) {
return context.raw.pointBackgroundColor || 'gray';
}
}, {
label: 'Regression Line',
data: regressionPoints,
borderColor: 'blue',
borderDash: [5, 5],
pointRadius: 0,
fill: false,
tension: 0.0
}]
}, options: {
scales: {
x: {
type: 'time',
time: {
tooltipFormat: 'MMM d, yyyy HH:mm:ss',
unit: 'day'
},
title: { display: true, text: 'Commit Date' }
},
y: { beginAtZero: true, max: 100, title: { display: true, text: 'Coverage (%)' } }
}, plugins: {
legend: {
display: true,
position: 'top', // You can also use 'bottom', 'left', or 'right'
labels: {
boxWidth: 12,
padding: 10,
font: {
size: 12,
weight: 'bold'
}
}
}, tooltip: {
callbacks: {
label: function(context) {
const raw = context.raw;
const coverage = raw.y.toFixed(1);
const delta = raw.delta?.toFixed(1) ?? '0.0';
const sign = delta > 0 ? '+' : delta < 0 ? '-' : '±';
// const baseLine = `${raw.label}: ${coverage}% (${sign}${Math.abs(delta)}%)`;
const baseLine = `${coverage}% (${sign}${Math.abs(delta)}%)`;
const commentLine = raw.comment ? raw.comment : null;
return commentLine ? [baseLine, commentLine] : [baseLine];
}
}
} , zoom: { // Enable zoom & pan on the x-axis for the trend chart
pan: {
enabled: true,
mode: 'x'
}, zoom: {
wheel: {
enabled: true
}, pinch: {
enabled: true
}, mode: 'x'
}
}
}, onClick: (e) => {
const points = chart.getElementsAtEventForMode(e, 'nearest', { intersect: true }, true);
if(points.length) {
const url = chart.data.datasets[0].data[points[0].index].url;
window.open(url, '_blank');
}
}
}
});
document.getElementById('toggleTrend').addEventListener('change', function(e) {
const show = e.target.checked;
const trendDataset = chart.data.datasets.find(ds => ds.label === 'Regression Line');
trendDataset.hidden = !show;
chart.update();
});
// Reset Zoom button handler (calls plugin API if available)
const resetBtn = document.getElementById('resetZoomBtn');
if(resetBtn) {
resetBtn.addEventListener('click', function() {
try {
if(chart && typeof chart.resetZoom === 'function') {
chart.resetZoom();
} else {
bin/test-generator-index view on Meta::CPAN
if(!arrow) continue;
if(i === colIndex) {
arrow.textContent = asc ? "â²" : "â¼";
arrow.classList.add("active");
} else {
arrow.textContent = "â²";
arrow.classList.remove("active");
}
}
table.setAttribute("data-sort-col", colIndex);
table.setAttribute("data-sort-order", asc ? "asc" : "desc");
}
// Initial display.
// The table has been set up sorted in ascending order on the filename; reflect that in the GUI
document.addEventListener("DOMContentLoaded", () => {
const table = document.querySelector("table");
if(!table) return;
const headers = table.tHead.rows[0].cells;
for(let i = 0; i < headers.length; i++) {
const arrow = headers[i].querySelector(".arrow");
if(!arrow) continue;
if(i === 0) {
arrow.textContent = "â²";
arrow.classList.add("active");
} else {
arrow.textContent = "â²";
arrow.classList.remove("active");
}
}
});
document.addEventListener("DOMContentLoaded", () => {
document.querySelectorAll("canvas.sparkline").forEach(canvas => {
const raw = canvas.getAttribute("data-points");
if(!raw) return;
const points = raw.split(",").map(v => parseFloat(v));
new Chart(canvas.getContext("2d"), {
type: 'line',
data: {
labels: points.map((_, i) => i+1),
datasets: [{
data: points,
borderColor: points.length > 1 && points[points.length-1] >= points[0] ? "green" : "red",
borderWidth: 1,
fill: false,
tension: 0.3,
pointRadius: 0
}]
}, options: {
responsive: false,
maintainAspectRatio: false,
elements: { line: { borderJoinStyle: 'round' } },
plugins: {
legend: { display: false },
tooltip: { enabled: false },
zoom: { // Enable zoom and pan
pan: {
enabled: true,
mode: 'x',
}, zoom: {
wheel: {
enabled: true,
},
pinch: {
enabled: true
},
mode: 'x',
}
}
}, scales: { x: { display: false }, y: { display: false } }
}
});
});
});
function refresh(){
window.location.reload("Refresh")
}
</script>
HTML
push @html, '<p><center>Use mouse wheel or pinch to zoom; drag to pan</center></p>';
} else {
push @html, '<p><i>No history to show coverage trend</i></p>';
}
# -------------------------------
# Issues flagged on RT
# -------------------------------
{
my $rt_count = fetch_open_rt_ticket_count($config{github_repo});
my $rt_url = "https://rt.cpan.org/Public/Dist/Display.html?Name=$config{github_repo}";
if(defined $rt_count && $rt_count > 0) {
push @html, '<p class="notice rt-issues">',
'<strong>RT issues:</strong>',
"<a href=\"$rt_url\" target=\"_blank\" rel=\"noopener\">",
"$rt_count open ticket" . @{[ $rt_count == 1 ? '' : 's' ]},
'</a>',
'</p>';
} else {
push @html, "<p>No issues active on <a href=\"$rt_url\">RT</a></p>";
}
}
# -------------------------------
# CPAN Testers failing reports table
# -------------------------------
my $dist_name = $config{github_repo};
my $cpan_api = "https://api.cpantesters.org/v3/summary/" . uri_escape($dist_name);
my $http = HTTP::Tiny->new(agent => 'cpan-coverage-html/1.0', timeout => 30);
my $retry = 0;
bin/test-generator-index view on Meta::CPAN
my $ups = '../' x ($depth + 2); # +2 for lib/ and mutation_html/
my $index_link = "${ups}index.html";
print $out qq{<a href="$index_link">Index</a>\n};
if($next) {
my $link = _relative_link($file, $next);
print $out qq{ <a href="$link">Next â¡</a>};
}
print $out qq{</div>};
# --------------------------------------------------
# File-level structural coverage (if available)
# --------------------------------------------------
if($coverage_data) {
if(my $file_cov = _coverage_for_file($coverage_data, $file)) {
my $stmt_total = $file_cov->{statement}{total} || 0;
my $stmt_hit = $file_cov->{statement}{covered} || 0;
my $branch_total = $file_cov->{branch}{total} || 0;
my $branch_hit = $file_cov->{branch}{covered} || 0;
my $stmt_pct = $stmt_total ? sprintf('%.2f', ($stmt_hit / $stmt_total) * 100) : 0;
my $branch_pct = $branch_total ? sprintf('%.2f', ($branch_hit / $branch_total) * 100) : 0;
my $approx_lcsaj = $branch_total + 1;
# Compute TER3 (LCSAJ path coverage) for this file if data is available.
# TER3 = covered_paths / total_paths * 100
my ($lcsaj_cov, $lcsaj_total) = _lcsaj_coverage_for_file(
$file, $lcsaj_dir, $lcsaj_hits, []
);
my $ter3_str;
if(!defined $lcsaj_cov) {
# .lcsaj.json not found â TER3 unavailable for this file
$ter3_str = 'n/a';
} elsif(!$lcsaj_total) {
# File found but no paths defined
$ter3_str = '-';
} else {
# Format as percentage with raw fraction for clarity
$ter3_str = sprintf('%.1f%% (%d/%d)',
($lcsaj_cov / $lcsaj_total) * 100,
$lcsaj_cov,
$lcsaj_total
);
}
# Display TER1/TER2/TER3 coverage metrics in the summary block.
# TER1 = statement, TER2 = branch, TER3 = LCSAJ path coverage.
print $out "<div class='summary'>\n";
print $out "<strong>Structural Coverage (Approximate)</strong><p>\n";
print $out "TER1 (Statement): $stmt_pct%<br>\n";
print $out "TER2 (Branch): $branch_pct%<br>\n";
print $out "TER3 (LCSAJ): $ter3_str<br>\n";
print $out "Approximate LCSAJ segments: $approx_lcsaj<br>\n";
print $out "</div>\n";
print $out qq{
<div class="legend">
<h3>LCSAJ Legend</h3>
<p>
<span class="lcsaj-dot">â</span>
<b>Covered</b> â this LCSAJ path was executed during testing.
</p>
<p>
<span class="lcsaj-dot-uncovered">â</span>
<b>Not covered</b> â this LCSAJ path was never executed. These are the paths to focus on.
</p>
<p>
Multiple dots on a line indicate that multiple control-flow paths begin at that line.
Hovering over any dot shows:
</p>
<pre>
start â end â jump
</pre>
<ul>
<li><b>start</b> â first line of the linear sequence</li>
<li><b>end</b> â last line before control flow changes</li>
<li><b>jump</b> â line execution jumps to next</li>
</ul>
<p>
Uncovered paths show <b>[NOT COVERED]</b> in the tooltip.
</p>
</div>
};
}
}
# --------------------------------------------------
# Legend explaining line colours
# --------------------------------------------------
print $out qq{
<div class="legend">
<h3>Mutant Testing Legend</h3>
<span class="legend-box survived-1"></span> Survived (tests missed this)
<span class="legend-box killed"></span> Killed (tests detected this)
<span class="legend-box none"></span> No mutation
</div>
};
my %survived_by_line;
my %killed_by_line;
for my $m (@{ $mutants->{survived} || [] }) {
next unless defined $m->{line};
push @{ $survived_by_line{ $m->{line} } }, $m;
}
for my $m (@{ $mutants->{killed} || [] }) {
next unless defined $m->{line};
push @{ $killed_by_line{ $m->{line} } }, $m;
}
print $out "<pre>\n";
my %lcsaj_by_line;
# Pre-compute normalised hit map for this file so we can
# colour LCSAJ dots covered/uncovered when building lcsaj_by_line
my $norm = $file;
$norm =~ s{^.*/blib/lib/}{lib/};
$norm =~ s{^.*/lib/}{lib/};
my $file_hits = $lcsaj_hits
? ( $lcsaj_hits->{$norm}
// $lcsaj_hits->{ abs_path($file) // $file }
// {} )
: {};
if($lcsaj_hits) {
# Normalize the filename so it matches debugger paths
$file = abs_path($file) if defined $file;
my $base = basename($file);
# convert absolute path to lib-relative path
my $rel = $file;
$rel =~ s{.*?/lib/}{};
my $lcsaj_file = File::Spec->catfile(
$lcsaj_dir,
"$rel.lcsaj",
"$base.lcsaj.json"
);
if(-f $lcsaj_file) {
open my $fh, '<', $lcsaj_file;
my $paths = decode_json(do { local $/; <$fh> });
close $fh;
for my $p (@{ $paths || [] }) {
next unless ref $p eq 'HASH';
my $start = $p->{start};
my $end = $p->{end};
my $jump = $p->{jump} // $p->{target};
next unless defined $start && defined $end;
bin/test-generator-index view on Meta::CPAN
-------------------------------------------------- */
:root {
--bg: #ffffff;
--text: #000000;
--table-header: #333;
--table-header-text: #ffffff;
--survived-1: #f8d7da;
--survived-2: #f5b7b1;
--survived-3: #ec7063;
--killed: #d4edda;
--border: #cccccc;
}
/* --------------------------------------------------
Dark Mode Overrides
-------------------------------------------------- */
html[data-theme='dark'] {
--bg: #1e1e1e;
--text: #dddddd;
--table-header: #222;
--table-header-text: #ffffff;
--survived-1: #5c2b2e;
--survived-2: #7b2c2f;
--survived-3: #a93226;
--killed: #1e4620;
--border: #555;
}
/* --------------------------------------------------
Global Styles
-------------------------------------------------- */
body {
font-family: sans-serif;
background: var(--bg);
color: var(--text);
}
table {
border-collapse: collapse;
width: 100%;
}
th {
background: var(--table-header);
color: var(--table-header-text);
}
.survived-1 { background-color: var(--survived-1); }
.survived-2 { background-color: var(--survived-2); }
.survived-3 { background-color: var(--survived-3); }
.killed { background-color: var(--killed); }
.legend {
border: 1px solid #ccc;
background: #fafafa;
padding: 10px;
margin: 15px 0;
font-size: 0.9em;
}
.legend pre {
background: #f4f4f4;
padding: 5px;
}
.legend-box {
display: inline-block;
width: 16px;
height: 16px;
margin: 0 6px 0 20px;
vertical-align: middle;
border: 1px solid var(--border);
}
/* White box for non-mutated lines */
.legend-box.none {
background-color: var(--bg);
}
/* --------------------------------------------------
Suggested Test Box Styling
Theme-aware and readable in light & dark modes
-------------------------------------------------- */
.suggested-test {
margin-top: 6px;
margin-bottom: 12px;
/* Use theme variables instead of hardcoded colors */
background: var(--bg);
color: var(--text);
padding: 8px;
border-radius: 4px;
/* Subtle border for visual separation */
border: 1px solid var(--border);
}
/* Label styling */
.suggest-label {
font-weight: bold;
margin-bottom: 4px;
}
/* Ensure the test code block inherits readable colors */
.suggested-test pre {
background: transparent; /* Prevent nested dark blocks */
color: inherit; /* Match theme text color */
margin: 0;
font-family: monospace;
}
pre { line-height: 1.4; }
pre > details {
margin: 0.2em 0;
}
pre > details:first-child {
margin-top: 0;
}
pre > details:last-child {
margin-bottom: 0;
}
pre details,
pre summary,
pre ul,
pre li {
white-space: normal;
margin: 0;
padding: 0;
line-height: 1.2;
}
( run in 0.584 second using v1.01-cache-2.11-cpan-39bf76dae61 )