Thursday, November 18, 2010

Mimicking Hudson when you must

Just a quick blog about a situation I found myself in because my current employer refuses to use Hudon for CI testing of PHP projects.

I was looking for a way to use phpunit to run tests and create a website that nicely displays the results. I thought it would be simple to create an ant script which would run the tests using the --log-junit feature to create an xml document that the junitreport ant task can use to produce a quick and dirty website that will display the test stats. It turns out that the xml that phpunit emits is not well formed junit xml, so the ant task would not parse it.

I could have used an xslt file and linux's xsltproc to translate the phpunit xml into what junitreport needs but I don't think this is robust solution into the future.

So instead I found this website:
http://clockwerx.blogspot.com/search/label/prototype.js

Here Daniel uses the phpunit json logging feature (--log-json) to emit json which is then used in a simple php file to display the test results. It also allows you to filter based on the status of each test.

I took his code and converted it to use jQuery (pulled down using google api so you don't need to download it yourself) and moved the filter div to the top so it doesn't cover test results. Also, I removed the links to raw output, agile, and recent pages.

Since we run the tests nightly (or when there is an update), we email a unique link to our developers. So the page is expecting a test (get) parameter which is used to locate the correct json file to display.

The url sent to other developers will look like this:
http://localhost/results.php?test=2010-11-16_1219

You will have to find icons and place them in an 'images' directory located in the same directory as this file.

Here it is.. hope it helps you when your company decides not to use a proven tool:

<?php

$file = dirname(__FILE__) . '/' . $_GET["test"]. '.js';
if (!file_exists($file)) {
die("No unit tests have been run?<br/>Nothing found in " . $file);
}
$json = file_get_contents($file);

//Ugh
$json = '[' . str_replace("}{", "},{", $json) . ']';

?>
<html>
<head>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.4.4/jquery.min.js"></script>
</head>
<body>
<style>
table {
border: 1px solid rgb(240, 240, 240);
}

.error {
border: 1px solid orange;
}

.fail {
border: 1px solid red;
}

.pass {
border: 1px solid green;
}

.cell {
width: 200px !important;
text-align: center;
padding: 0.5em;
}
.cell img {
display: block;
margin: auto;
}

h4 {
font-weight: normal !important;
font-style: italic;
}
</style>
<script type="text/javascript">
var total_passes = 0;
var total_failures = 0;
var total_errors = 0;
var total_skipped = 0;
var total_incomplete= 0;

function renderSuiteStart(item) {
var h2 = document.createElement("H2");
h2.appendChild(document.createTextNode(item.suite));

var p = document.createElement("p");
p.appendChild(document.createTextNode(item.tests + " tests in suite"));

var div = document.createElement("div");
var table = document.createElement("table");

div.id = item.suite;
table.id = item.suite + "_table";
table.className = '';

div.appendChild(h2);
div.appendChild(p);
div.appendChild(table);

$('#main').append(div);
}

function renderTest(item) {
var h3 = document.createElement("H3");
var h4 = document.createElement("H4");
h3.appendChild(document.createTextNode(item.test));

var p = document.createElement("p");
p.appendChild(document.createTextNode(item.time + " tests in suite"));

var tr = document.createElement("TR");
var th = document.createElement("TH");
var message = document.createElement("TD");
var detail = document.createElement("TD");
var time = document.createElement("TD");
var img = document.createElement("IMG");
var backtrace = document.createElement("OL");


time.appendChild(document.createTextNode(item.time));


img.alt = item.status;
img.width="30";
img.height="30";
message.className = item.status + " cell";

switch (item.status) {
case 'error':
if (item.message == "Skipped Test") {
message.className = "skipped cell";
total_skipped++;
} else if (item.message == "Incomplete Test") {
message.className = "incomplete cell";
total_incomplete++;
} else {
total_errors++;
}
img.src = 'images/important.png';
break;
case 'fail':
total_failures++;
img.src = 'images/cross.png';
break;
default:
total_passes++;
img.src = 'images/check.png';
break;
}

$.each(item.trace,
function(i, t) {
var li = document.createElement("LI");
var text = t.file + "(line " + t.line + "): " + t.class + t.type + t.function + "()";
li.appendChild(document.createTextNode(text));

backtrace.appendChild(li);
}
);

h4.appendChild(document.createTextNode(item.test));
detail.appendChild(h4);
detail.appendChild(backtrace);

message.appendChild(img);
message.appendChild(document.createTextNode(item.message));
message.style.width = "300px";

tr.appendChild(message);
tr.appendChild(detail);


$("#"+ item.suite + "_table").append(tr);
}

function renderResults() {
var data = <?php print $json; ?>;

$.each(data, function(i,item) {

switch (item.event) {
case 'suiteStart':
renderSuiteStart(item);
break;
case 'test':
renderTest(item);
}
});
renderTOC();
}

function renderTOC() {
var menu = document.createElement('p');
var checkbox_pass = document.createElement('input');
var checkbox_fail = document.createElement('input');
var checkbox_error = document.createElement('input');
var checkbox_skipped = document.createElement('input');
var checkbox_incomplete = document.createElement('input');

var label_pass = document.createElement('label');
var label_fail = document.createElement('label');
var label_error = document.createElement('label');
var label_skipped = document.createElement('label');
var label_incomplete = document.createElement('label');
var status_image;

//menu.style.position = 'fixed';
menu.style.bottom = 0;
menu.style.right = "1.5em";
//menu.style.backgroundColor = 'rgb(240, 240, 240)';
menu.style.padding = "1em;";
menu.style.fontWeight = 'bolder';

checkbox_pass.type = "checkbox";
checkbox_fail.type = "checkbox";
checkbox_error.type = "checkbox";
checkbox_skipped.type = "checkbox";
checkbox_incomplete.type = "checkbox";

checkbox_pass.checked = "checked";
checkbox_fail.checked = "checked";
checkbox_error.checked = "checked";
checkbox_skipped.checked = "checked";
checkbox_incomplete.checked = "checked";


checkbox_pass.onclick=function(){toggle("pass")};
checkbox_fail.onclick=function(){toggle("fail")};
checkbox_error.onclick=function(){toggle("error")};
checkbox_skipped.onclick=function(){toggle("skipped")};
checkbox_incomplete.onclick=function(){toggle("incomplete")};

label_pass.appendChild(checkbox_pass);
label_fail.appendChild(checkbox_fail);
label_error.appendChild(checkbox_error);
label_skipped.appendChild(checkbox_skipped);
label_incomplete.appendChild(checkbox_incomplete);

label_pass.appendChild(document.createTextNode("Passed (" + total_passes + ")"));
label_fail.appendChild(document.createTextNode("Failed (" + total_failures + ")"));
label_error.appendChild(document.createTextNode("Error (" + total_errors + ")"));
label_skipped.appendChild(document.createTextNode("Skipped (" + total_skipped + ")"));
label_incomplete.appendChild(document.createTextNode("Incomplete (" + total_incomplete + ")"));

label_pass.style.color = "green";
label_fail.style.color = "red";
label_error.style.color = "#FF6600";
label_skipped.style.color = "blue";
label_incomplete.style.color = "gray";

status_image = document.createElement("IMG");
status_image.style.display = "block";
status_image.style.margin = "auto";
status_image.style.paddingright = "10px";
status_image.width="50";
status_image.height="50";
status_image.align="left";
if (total_failures > 0) {
status_image.src = "images/fail.gif";
status_image.title= "Some tests failed. Fix them and get a happy face";
} else {
status_image.src = "images/pass.jpg";
status_image.title= "All test passed!!! Give yourself a pat on the back, YOU ROCK!!";
}
menu.appendChild(status_image);

menu.appendChild(document.createElement("br"));
menu.appendChild(label_pass);
menu.appendChild(label_fail);
menu.appendChild(label_error);
menu.appendChild(label_skipped);
menu.appendChild(label_incomplete);

checkbox_pass.onclick();
checkbox_pass.checked = "";

checkbox_skipped.onclick();
checkbox_skipped.checked = "";

checkbox_incomplete.onclick();
checkbox_incomplete.checked = "";

$("#toc").append(menu);
}

function toggle(type) {

$("." + type).each(function(i, td) {
if (td.parentNode.style.display != 'none') {
td.parentNode.style.display = 'none';
} else {
td.parentNode.style.display = 'table-row';
}
});
}

$(document).ready(renderResults);
</script>
<div id="main" class="contentbox">
<h1>Unit tests</h1>
<p><!--<a href="results.txt">Raw results</a> | <a href="docs.html">Agile Documentation</a> | <a href="recent.php">Recent changes</a> | -->
Generated: <?php print date("F j, Y, g:i a", filemtime($file)); ?> </p>
<div id="toc"/>
</div>
</body>
</html>