Replaces python test.py with node.js version

This commit is contained in:
Yury Delendik 2014-09-24 20:47:33 -05:00
Родитель da857f8c9b
Коммит 8f71d0c371
14 изменённых файлов: 1899 добавлений и 1375 удалений

Просмотреть файл

@ -307,7 +307,7 @@ module.exports = function(grunt) {
});
grunt.registerTask('server', function () {
var WebServer = require('./utils/webserver.js').WebServer;
var WebServer = require('./test/webserver.js').WebServer;
var done = this.async();
var server = new WebServer();
server.start();
@ -332,8 +332,8 @@ module.exports = function(grunt) {
params.push('--noPrompts');
}
grunt.util.spawn({
cmd: 'python',
args: ['test.py', '--reftest', '--browserManifestFile=' + browserManifestFile,
cmd: 'node',
args: ['test.js', '--reftest', '--browserManifestFile=' + browserManifestFile,
'--manifestFile=' + testManifestFile].concat(params),
opts: { cwd: 'test', stdio: 'inherit' }
}, function () {
@ -360,8 +360,8 @@ module.exports = function(grunt) {
params.push('--noPrompts');
}
grunt.util.spawn({
cmd: 'python',
args: ['test.py', '--browserManifestFile=' + browserManifestFile,
cmd: 'node',
args: ['test.js', '--browserManifestFile=' + browserManifestFile,
'--manifestFile=' + testManifestFile].concat(params),
opts: { cwd: 'test', stdio: 'inherit' }
}, function () {
@ -383,8 +383,8 @@ module.exports = function(grunt) {
params.push('--noPrompts');
}
grunt.util.spawn({
cmd: 'python',
args: ['test.py', '-m', '--browserManifestFile=' + browserManifestFile].concat(params),
cmd: 'node',
args: ['test.js', '-m', '--browserManifestFile=' + browserManifestFile].concat(params),
opts: { cwd: 'test', stdio: 'inherit'}}, function () {
done();
});

Просмотреть файл

@ -9,6 +9,7 @@
"grunt-contrib-watch": "~0.5.3",
"grunt-parallel": "~0.3.1",
"temp": "0.5.0",
"yargs": "~1.2.1",
"esprima": "1.2.2",
"estraverse": "1.5.1"
},

1
test/.gitignore поставляемый
Просмотреть файл

@ -1,3 +1,4 @@
ref/
tmp/
test_snapshots/
*.log

Просмотреть файл

@ -25,11 +25,15 @@ function execManifest(path, bundle) {
var i = 0;
function next() {
if (i >= manifest.length) {
postData('/tellMeToQuit?path=' + escape(path));
postData('/tellMeToQuit?browser=' + escape(browser));
return;
}
var test = manifest[i++];
postData('/progress?browser=' + escape(browser) + '&id=' + escape(test.id));
postData('/progress', JSON.stringify({
browser: browser,
id: test.id
}));
TestContext._slavePath = bundle ? 'harness/slave-bundle.html' :
'harness/slave.html';
@ -75,22 +79,6 @@ function execManifest(path, bundle) {
}
});
break;
case 'sanity':
execSanity(test.filenames,
function (itemNumber, itemsCount, item, result) {
postData('/result', JSON.stringify({
browser: browser,
id: test.id,
failure: result.failure,
item: item,
numItems: itemsCount,
snapshot: result.snapshot
}));
if (itemNumber + 1 == itemsCount) { // last item
next();
}
});
break;
default:
throw 'unknown test type';
}

Просмотреть файл

@ -0,0 +1,177 @@
/*
Copyright 2012 Mozilla Foundation
Version: MPL 1.1/GPL 2.0/LGPL 2.1
The contents of this file are subject to the Mozilla Public License Version
1.1 (the "License"); you may not use this file except in compliance with
the License. You may obtain a copy of the License at
http://www.mozilla.org/MPL
Software distributed under the License is distributed on an "AS IS" basis,
WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
for the specific language governing rights and limitations under the
License.
Alternatively, the contents of this file may be used under the terms of
either the GNU General Public License Version 2 or later (the "GPL"), or
the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
in which case the provisions of the GPL or the LGPL are applicable instead
of those above. If you wish to allow use of your version of this file only
under the terms of either the GPL or the LGPL, and not to allow others to
use your version of this file under the terms of the MPL, indicate your
decision by deleting the provisions above and replace them with the notice
and other provisions required by the LGPL or the GPL. If you do not delete
the provisions above, a recipient may use your version of this file under
the terms of any one of the MPL, the GPL or the LGPL.
Original author: L. David Baron <dbaron@dbaron.org>
*/
* {
padding: 0;
margin: 0;
}
html {
background-color: #FFF;
font: message-box;
font-size: 14px;
}
body {
padding: 10px;
}
a {
color: #000;
}
#loading, #viewer {
display: none;
}
#pixelarea {
position: absolute;
width: 320px;
height: 94px;
overflow: visible;
top: 10px;
left: 10px;
}
#itemlist {
overflow: auto;
position: absolute;
top: 104px;
width: 320px;
bottom: 0;
left: 10px;
}
#leftpane {
width: 320px;
}
#images {
overflow: auto;
position: fixed;
left: 340px;
right: 0;
top: 10px;
bottom: 0;
}
#imgcontrols {
margin: 0;
display: block;
}
#itemtable, #itemtable td, #itemtable th {
border: 1px solid #CCC;
padding: 0;
}
#itemtable {
border-collapse: collapse;
border-spacing: 0;
}
#itemtable td:first-child {
padding-left: 10px;
width: 12px;
}
#itemtable td:last-child {
padding: 0 5px;
}
#itemtable td.selected {
background-color: #DDD;
}
#magnification > svg {
display: block;
width: 84px;
height: 84px;
}
#pixelinfo {
position: absolute;
width: 200px;
left: 85px;
}
#pixelinfo table {
border-collapse: collapse;
}
#pixelinfo table th {
white-space: nowrap;
text-align: left;
padding: 0;
}
#pixelinfo table td {
padding: 0 0 0 0.25em;
}
#pixelhint {
color: #000;
cursor: help;
text-decoration: underline;
width: 15px;
}
#pixelhint > * {
display: none;
position: absolute;
margin: 8px 0 0 8px;
padding: 4px;
width: 400px;
background-color: #ffa;
color: #000;
box-shadow: 3px 3px 2px #888;
z-index: 1;
}
#pixelhint:hover {
color: #000;
}
#pixelhint:hover > * {
display: block;
}
#pixelhint p {
margin: 0;
}
#pixelhint p + p {
margin-top: 1em;
}
#referenceImage, #differences {
margin: 0 0 10px 20px;
}

Просмотреть файл

@ -0,0 +1,175 @@
<!DOCTYPE html>
<!--
Copyright 2012 Mozilla Foundation
Version: MPL 1.1/GPL 2.0/LGPL 2.1
The contents of this file are subject to the Mozilla Public License Version
1.1 (the "License"); you may not use this file except in compliance with
the License. You may obtain a copy of the License at
http://www.mozilla.org/MPL
Software distributed under the License is distributed on an "AS IS" basis,
WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
for the specific language governing rights and limitations under the
License.
Alternatively, the contents of this file may be used under the terms of
either the GNU General Public License Version 2 or later (the "GPL"), or
the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
in which case the provisions of the GPL or the LGPL are applicable instead
of those above. If you wish to allow use of your version of this file only
under the terms of either the GPL or the LGPL, and not to allow others to
use your version of this file under the terms of the MPL, indicate your
decision by deleting the provisions above and replace them with the notice
and other provisions required by the LGPL or the GPL. If you do not delete
the provisions above, a recipient may use your version of this file under
the terms of any one of the MPL, the GPL or the LGPL.
Original author: L. David Baron <dbaron@dbaron.org>
-->
<html>
<head>
<title>Reftest analyzer</title>
<meta charset="utf-8">
<link rel="stylesheet" href="reftest-analyzer.css">
<script src="reftest-analyzer.js"></script>
</head>
<body>
<div id="entry">
<h1>Reftest analyzer</h1>
<p>
Paste your log into this textarea:<br>
<textarea cols="80" rows="10" id="logEntry"></textarea><br>
<input type="button" value="Process pasted log" id="logPasted">
</p>
<p>
<br>...or load it from a file:<br>
<input type="file" id="fileEntry">
</p>
</div>
<div id="loading">Loading log...</div>
<div id="viewer">
<div id="pixelarea">
<div id="pixelinfo">
<table>
<tbody>
<tr>
<th>Pixel at:</th>
<td colspan="2" id="coords"></td>
</tr>
<tr>
<th>Test:</th>
<td id="pix1rgb"></td>
<td id="pix1hex"></td>
</tr>
<tr>
<th>Reference:</th>
<td id="pix2rgb"></td>
<td id="pix2hex"></td>
</tr>
</tbody>
</table>
<div>
<div id="pixelhint">?
<div>
<p>Move the mouse over the reftest image on the right to show
magnified pixels on the left. The color information above is for
the pixel centered in the magnified view.</p>
<p>The test is shown in the upper triangle of each pixel and
the reference is shown in the lower triangle.</p>
</div>
</div>
</div>
</div>
<div id="magnification">
<svg xmlns="http://www.w3.org/2000/svg" width="84" height="84" shape-rendering="optimizeSpeed">
<g id="mag" />
</svg>
</div>
</div>
<div id="itemlist">
<table id="itemtable"></table>
</div>
<div id="images">
<form id="imgcontrols">
<label>
<input type="radio" name="which" id="testImage" value="0" checked="checked"> Test
</label>
<label>
<input type="radio" name="which" id="referenceImage" value="1"> Reference
</label>
<label>
<input type="checkbox" id="differences"> Circle differences
</label>
</form>
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="800px" height="1130px" viewbox="0 0 800 1130" id="svg">
<defs>
<!-- use sRGB to avoid loss of data -->
<filter id="showDifferences" x="0%" y="0%" width="100%" height="100%"
style="color-interpolation-filters: sRGB">
<feImage id="feimage1" result="img1" xlink:href="#image1" />
<feImage id="feimage2" result="img2" xlink:href="#image2" />
<!-- inv1 and inv2 are the images with RGB inverted -->
<feComponentTransfer result="inv1" in="img1">
<feFuncR type="linear" slope="-1" intercept="1" />
<feFuncG type="linear" slope="-1" intercept="1" />
<feFuncB type="linear" slope="-1" intercept="1" />
</feComponentTransfer>
<feComponentTransfer result="inv2" in="img2">
<feFuncR type="linear" slope="-1" intercept="1" />
<feFuncG type="linear" slope="-1" intercept="1" />
<feFuncB type="linear" slope="-1" intercept="1" />
</feComponentTransfer>
<!-- w1 will have non-white pixels anywhere that img2
is brighter than img1, and w2 for the reverse.
It would be nice not to have to go through these
intermediate states, but feComposite
type="arithmetic" can't transform the RGB channels
and leave the alpha channel untouched. -->
<feComposite result="w1" in="img1" in2="inv2" operator="arithmetic" k2="1" k3="1" />
<feComposite result="w2" in="img2" in2="inv1" operator="arithmetic" k2="1" k3="1" />
<!-- c1 will have non-black pixels anywhere that img2
is brighter than img1, and c2 for the reverse -->
<feComponentTransfer result="c1" in="w1">
<feFuncR type="linear" slope="-1" intercept="1" />
<feFuncG type="linear" slope="-1" intercept="1" />
<feFuncB type="linear" slope="-1" intercept="1" />
</feComponentTransfer>
<feComponentTransfer result="c2" in="w2">
<feFuncR type="linear" slope="-1" intercept="1" />
<feFuncG type="linear" slope="-1" intercept="1" />
<feFuncB type="linear" slope="-1" intercept="1" />
</feComponentTransfer>
<!-- c will be nonblack (and fully on) for every pixel+component where there are differences -->
<feComposite result="c" in="c1" in2="c2" operator="arithmetic" k2="255" k3="255" />
<!-- a will be opaque for every pixel with differences and transparent for all others -->
<feColorMatrix result="a" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 0 0" />
<!-- a, dilated by 4 pixels -->
<feMorphology result="dila4" in="a" operator="dilate" radius="4" />
<!-- a, dilated by 1 pixel -->
<feMorphology result="dila1" in="a" operator="dilate" radius="1" />
<!-- all the pixels in the 3-pixel dilation of a but not in the 1-pixel dilation of a, to highlight the diffs -->
<feComposite result="highlight" in="dila4" in2="dila1" operator="out" />
<feFlood result="red" flood-color="red" />
<feComposite result="redhighlight" in="red" in2="highlight" operator="in" />
<feFlood result="black" flood-color="black" flood-opacity="0.5" />
<feMerge>
<feMergeNode in="black" />
<feMergeNode in="redhighlight" />
</feMerge>
</filter>
</defs>
<g id="magnify">
<image x="0" y="0" width="100%" height="100%" id="image1" />
<image x="0" y="0" width="100%" height="100%" id="image2" />
</g>
<rect id="diffrect" filter="url(#showDifferences)" pointer-events="none" x="0" y="0" width="100%" height="100%" />
</svg>
</div>
</body>
</html>

Просмотреть файл

@ -0,0 +1,485 @@
/*
Copyright 2012 Mozilla Foundation
Version: MPL 1.1/GPL 2.0/LGPL 2.1
The contents of this file are subject to the Mozilla Public License Version
1.1 (the "License"); you may not use this file except in compliance with
the License. You may obtain a copy of the License at
http://www.mozilla.org/MPL
Software distributed under the License is distributed on an "AS IS" basis,
WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
for the specific language governing rights and limitations under the
License.
Alternatively, the contents of this file may be used under the terms of
either the GNU General Public License Version 2 or later (the "GPL"), or
the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
in which case the provisions of the GPL or the LGPL are applicable instead
of those above. If you wish to allow use of your version of this file only
under the terms of either the GPL or the LGPL, and not to allow others to
use your version of this file under the terms of the MPL, indicate your
decision by deleting the provisions above and replace them with the notice
and other provisions required by the LGPL or the GPL. If you do not delete
the provisions above, a recipient may use your version of this file under
the terms of any one of the MPL, the GPL or the LGPL.
Original author: L. David Baron <dbaron@dbaron.org>
*/
// Global variables
window.gPhases = null;
window.XLINK_NS = "http://www.w3.org/1999/xlink";
window.SVG_NS = "http://www.w3.org/2000/svg";
window.gMagPixPaths = []; // 2D array of array-of-two <path> objects used in the pixel magnifier
window.gMagWidth = 5; // number of zoomed in pixels to show horizontally
window.gMagHeight = 5; // number of zoomed in pixels to show vertically
window.gMagZoom = 16; // size of the zoomed in pixels
window.gImage1Data; // ImageData object for the test output image
window.gImage2Data; // ImageData object for the reference image
window.gFlashingPixels = []; // array of <path> objects that should be flashed due to pixel color mismatch
window.gPath = ''; // path taken from #web= and prepended to ref/snp urls
window.gSelected = null; // currently selected comparison
window.onload = function() {
load();
function ID(id) {
return document.getElementById(id);
}
function hashParameters() {
var result = { };
var params = window.location.hash.substr(1).split(/[&;]/);
for (var i = 0; i < params.length; i++) {
var parts = params[i].split("=");
result[parts[0]] = unescape(unescape(parts[1]));
}
return result;
}
function load() {
gPhases = [ ID("entry"), ID("loading"), ID("viewer") ];
buildMag();
var params = hashParameters();
if (params.log) {
ID("logEntry").value = params.log;
logPasted();
} else if (params.web) {
loadFromWeb(params.web);
}
ID("logEntry").focus();
}
function buildMag() {
var mag = ID("mag");
var r = document.createElementNS(SVG_NS, "rect");
r.setAttribute("x", gMagZoom * -gMagWidth / 2);
r.setAttribute("y", gMagZoom * -gMagHeight / 2);
r.setAttribute("width", gMagZoom * gMagWidth);
r.setAttribute("height", gMagZoom * gMagHeight);
mag.appendChild(r);
mag.setAttribute("transform", "translate(" + (gMagZoom * (gMagWidth / 2) + 1) + "," + (gMagZoom * (gMagHeight / 2) + 1) + ")");
for (var x = 0; x < gMagWidth; x++) {
gMagPixPaths[x] = [];
for (var y = 0; y < gMagHeight; y++) {
var p1 = document.createElementNS(SVG_NS, "path");
p1.setAttribute("d", "M" + ((x - gMagWidth / 2) + 1) * gMagZoom + "," + (y - gMagHeight / 2) * gMagZoom + "h" + -gMagZoom + "v" + gMagZoom);
p1.setAttribute("stroke", "#CCC");
p1.setAttribute("stroke-width", "1px");
p1.setAttribute("fill", "#aaa");
var p2 = document.createElementNS(SVG_NS, "path");
p2.setAttribute("d", "M" + ((x - gMagWidth / 2) + 1) * gMagZoom + "," + (y - gMagHeight / 2) * gMagZoom + "v" + gMagZoom + "h" + -gMagZoom);
p2.setAttribute("stroke", "#CCC");
p2.setAttribute("stroke-width", "1px");
p2.setAttribute("fill", "#888");
mag.appendChild(p1);
mag.appendChild(p2);
gMagPixPaths[x][y] = [p1, p2];
}
}
var flashedOn = false;
setInterval(function() {
flashedOn = !flashedOn;
flashPixels(flashedOn);
}, 500);
}
function showPhase(phaseId) {
for (var i in gPhases) {
var phase = gPhases[i];
phase.style.display = (phase.id == phaseId) ? "block" : "none";
}
if (phaseId == "viewer") {
ID("images").style.display = "none";
}
}
function loadFromWeb(url) {
var lastSlash = url.lastIndexOf('/');
if (lastSlash) {
gPath = url.substring(0, lastSlash + 1);
}
var r = new XMLHttpRequest();
r.open("GET", url);
r.onreadystatechange = function() {
if (r.readyState == 4) {
processLog(r.response);
}
}
r.send(null);
}
function fileEntryChanged() {
showPhase("loading");
var input = ID("fileEntry");
var files = input.files;
if (files.length > 0) {
// Only handle the first file; don't handle multiple selection.
// The parts of the log we care about are ASCII-only. Since we
// can ignore lines we don't care about, best to read in as
// ISO-8859-1, which guarantees we don't get decoding errors.
var fileReader = new FileReader();
fileReader.onload = function(e) {
var log = e.target.result;
if (log) {
processLog(log);
} else {
showPhase("entry");
}
}
fileReader.readAsText(files[0], "iso-8859-1");
}
// So the user can process the same filename again (after
// overwriting the log), clear the value on the form input so we
// will always get an onchange event.
input.value = "";
}
function logPasted() {
showPhase("loading");
var entry = ID("logEntry");
var log = entry.value;
entry.value = "";
processLog(log);
}
var gTestItems;
function processLog(contents) {
var lines = contents.split(/[\r\n]+/);
gTestItems = [];
for (var j in lines) {
var line = lines[j];
var match = line.match(/^(?:NEXT ERROR )?REFTEST (.*)$/);
if (!match) {
continue;
}
line = match[1];
match = line.match(/^(TEST-PASS|TEST-UNEXPECTED-PASS|TEST-KNOWN-FAIL|TEST-UNEXPECTED-FAIL)(\(EXPECTED RANDOM\)|) \| ([^\|]+) \|(.*)/);
if (match) {
var state = match[1];
var random = match[2];
var url = match[3];
var extra = match[4];
gTestItems.push({
pass: !state.match(/FAIL$/),
// only one of the following three should ever be true
unexpected: !!state.match(/^TEST-UNEXPECTED/),
random: (random == "(EXPECTED RANDOM)"),
skip: (extra == " (SKIP)"),
url: url,
images: []
});
continue;
}
match = line.match(/^ IMAGE[^:]*: (.*)$/);
if (match) {
var item = gTestItems[gTestItems.length - 1];
item.images.push(match[1]);
}
}
buildViewer();
}
function buildViewer() {
if (gTestItems.length == 0) {
showPhase("entry");
return;
}
var cell = ID("itemlist");
var table = document.getElementById("itemtable");
while (table.childNodes.length > 0) {
table.removeChild(table.childNodes[table.childNodes.length - 1]);
}
var tbody = document.createElement("tbody");
table.appendChild(tbody);
for (var i in gTestItems) {
var item = gTestItems[i];
if (item.pass && !item.unexpected) {
continue;
}
var tr = document.createElement("tr");
var rowclass = item.pass ? "pass" : "fail";
var td = document.createElement("td");
var text = "";
if (item.unexpected) {
text += "!";
rowclass += " unexpected";
}
if (item.random) {
text += "R";
rowclass += " random";
}
if (item.skip) {
text += "S";
rowclass += " skip";
}
td.appendChild(document.createTextNode(text));
tr.appendChild(td);
td = document.createElement("td");
td.id = "url" + i;
td.className = "url";
var match = item.url.match(/\/mozilla\/(.*)/);
text = document.createTextNode(match ? match[1] : item.url);
if (item.images.length > 0) {
var a = document.createElement("a");
a.id = i;
a.className = "image";
a.href = "#";
a.appendChild(text);
td.appendChild(a);
} else {
td.appendChild(text);
}
tr.appendChild(td);
tr.className = rowclass;
tbody.appendChild(tr);
}
// Bind an event handler to each image link
var images = document.getElementsByClassName("image");
for (var i = 0; i < images.length; i++) {
images[i].addEventListener("click", function(e) {
showImages(e.target.id);
}, false);
}
showPhase("viewer");
}
function getImageData(src, whenReady) {
var img = new Image();
img.onload = function() {
var canvas = document.createElement("canvas");
canvas.width = 800;
canvas.height = 1000;
var ctx = canvas.getContext("2d");
ctx.drawImage(img, 0, 0);
whenReady(ctx.getImageData(0, 0, 800, 1000));
};
img.src = gPath + src;
}
function showImages(i) {
if (gSelected !== null) {
ID('url' + gSelected).classList.remove('selected');
}
gSelected = i;
ID('url' + gSelected).classList.add('selected');
var item = gTestItems[i];
var cell = ID("images");
ID("image1").style.display = "";
ID("image2").style.display = "none";
ID("diffrect").style.display = "none";
ID("imgcontrols").reset();
ID("image1").setAttributeNS(XLINK_NS, "xlink:href", gPath + item.images[0]);
// Making the href be #image1 doesn't seem to work
ID("feimage1").setAttributeNS(XLINK_NS, "xlink:href", gPath + item.images[0]);
if (item.images.length == 1) {
ID("imgcontrols").style.display = "none";
} else {
ID("imgcontrols").style.display = "";
ID("image2").setAttributeNS(XLINK_NS, "xlink:href", gPath + item.images[1]);
// Making the href be #image2 doesn't seem to work
ID("feimage2").setAttributeNS(XLINK_NS, "xlink:href", gPath + item.images[1]);
}
cell.style.display = "";
getImageData(item.images[0], function(data) {
gImage1Data = data
});
getImageData(item.images[1], function(data) {
gImage2Data = data
});
}
function showImage(i) {
if (i == 1) {
ID("image1").style.display = "";
ID("image2").style.display = "none";
} else {
ID("image1").style.display = "none";
ID("image2").style.display = "";
}
}
function showDifferences(cb) {
ID("diffrect").style.display = cb.checked ? "" : "none";
}
function flashPixels(on) {
var stroke = on ? "#FF0000" : "#CCC";
var strokeWidth = on ? "2px" : "1px";
for (var i = 0; i < gFlashingPixels.length; i++) {
gFlashingPixels[i].setAttribute("stroke", stroke);
gFlashingPixels[i].setAttribute("stroke-width", strokeWidth);
}
}
function cursorPoint(evt) {
var m = evt.target.getScreenCTM().inverse();
var p = ID("svg").createSVGPoint();
p.x = evt.clientX;
p.y = evt.clientY;
p = p.matrixTransform(m);
return { x: Math.floor(p.x), y: Math.floor(p.y) };
}
function hex2(i) {
return (i < 16 ? "0" : "") + i.toString(16);
}
function canvasPixelAsHex(data, x, y) {
var offset = (y * data.width + x) * 4;
var r = data.data[offset];
var g = data.data[offset + 1];
var b = data.data[offset + 2];
return "#" + hex2(r) + hex2(g) + hex2(b);
}
function hexAsRgb(hex) {
return "rgb(" + [parseInt(hex.substring(1, 3), 16), parseInt(hex.substring(3, 5), 16), parseInt(hex.substring(5, 7), 16)] + ")";
}
function magnify(evt) {
var cursor = cursorPoint(evt);
var x = cursor.x;
var y = cursor.y;
var centerPixelColor1, centerPixelColor2;
var dx_lo = -Math.floor(gMagWidth / 2);
var dx_hi = Math.floor(gMagWidth / 2);
var dy_lo = -Math.floor(gMagHeight / 2);
var dy_hi = Math.floor(gMagHeight / 2);
flashPixels(false);
gFlashingPixels = [];
for (var j = dy_lo; j <= dy_hi; j++) {
for (var i = dx_lo; i <= dx_hi; i++) {
var px = x + i;
var py = y + j;
var p1 = gMagPixPaths[i + dx_hi][j + dy_hi][0];
var p2 = gMagPixPaths[i + dx_hi][j + dy_hi][1];
if (px < 0 || py < 0 || px >= 800 || py >= 1000) {
p1.setAttribute("fill", "#aaa");
p2.setAttribute("fill", "#888");
} else {
var color1 = canvasPixelAsHex(gImage1Data, x + i, y + j);
var color2 = canvasPixelAsHex(gImage2Data, x + i, y + j);
p1.setAttribute("fill", color1);
p2.setAttribute("fill", color2);
if (color1 != color2) {
gFlashingPixels.push(p1, p2);
p1.parentNode.appendChild(p1);
p2.parentNode.appendChild(p2);
}
if (i == 0 && j == 0) {
centerPixelColor1 = color1;
centerPixelColor2 = color2;
}
}
}
}
flashPixels(true);
showPixelInfo(x, y, centerPixelColor1, hexAsRgb(centerPixelColor1), centerPixelColor2, hexAsRgb(centerPixelColor2));
}
function showPixelInfo(x, y, pix1rgb, pix1hex, pix2rgb, pix2hex) {
var pixelinfo = ID("pixelinfo");
ID("coords").textContent = [x, y];
ID("pix1hex").textContent = pix1hex;
ID("pix1rgb").textContent = pix1rgb;
ID("pix2hex").textContent = pix2hex;
ID("pix2rgb").textContent = pix2rgb;
}
var logPastedButton = document.getElementById("logPasted");
logPastedButton.addEventListener("click", logPasted, false);
var fileEntryButton = document.getElementById("fileEntry");
fileEntryButton.addEventListener("change", fileEntryChanged, false);
var testImage = document.getElementById("testImage");
testImage.addEventListener("click", function() {
showImage(1);
}, false);
var referenceImage = document.getElementById("referenceImage");
referenceImage.addEventListener("click", function() {
showImage(2);
}, false);
var differences = document.getElementById("differences");
differences.addEventListener("click", function(e) {
showDifferences(e.target);
}, false);
var magnifyElement = document.getElementById("magnify");
magnifyElement.addEventListener("mousemove", function(e) {
magnify(e);
}, false);
window.addEventListener('keydown', function keydown(event) {
if (event.which === 84) {
// 't' switch test/ref images
var val = 0;
if (document.querySelector('input[name="which"][value="0"]:checked')) {
val = 1;
}
document.querySelector('input[name="which"][value="' + val + '"]').click();
} else if (event.which === 68) {
// 'd' toggle differences
document.getElementById("differences").click();
} else if (event.which === 78 || event.which === 80) {
// 'n' next image, 'p' previous image
var select = gSelected;
if (gSelected === null) {
select = 0;
} else if (event.which === 78) {
select++;
} else {
select--;
}
var length = gTestItems.length;
select = select < 0 ? length - 1 : select >= length ? 0 : select;
showImages(select);
}
});
}

Просмотреть файл

@ -1,601 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- vim: set shiftwidth=4 tabstop=4 autoindent noexpandtab: -->
<!-- ***** BEGIN LICENSE BLOCK *****
- Version: MPL 1.1/GPL 2.0/LGPL 2.1
-
- The contents of this file are subject to the Mozilla Public License Version
- 1.1 (the "License"); you may not use this file except in compliance with
- the License. You may obtain a copy of the License at
- http://www.mozilla.org/MPL/
-
- Software distributed under the License is distributed on an "AS IS" basis,
- WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
- for the specific language governing rights and limitations under the
- License.
-
- The Original Code is reftest-analyzer.html.
-
- The Initial Developer of the Original Code is the Mozilla Foundation.
- Portions created by the Initial Developer are Copyright (C) 2008
- the Initial Developer. All Rights Reserved.
-
- Contributor(s):
- L. David Baron <dbaron@dbaron.org>, Mozilla Corporation (original author)
-
- Alternatively, the contents of this file may be used under the terms of
- either the GNU General Public License Version 2 or later (the "GPL"), or
- the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
- in which case the provisions of the GPL or the LGPL are applicable instead
- of those above. If you wish to allow use of your version of this file only
- under the terms of either the GPL or the LGPL, and not to allow others to
- use your version of this file under the terms of the MPL, indicate your
- decision by deleting the provisions above and replace them with the notice
- and other provisions required by the LGPL or the GPL. If you do not delete
- the provisions above, a recipient may use your version of this file under
- the terms of any one of the MPL, the GPL or the LGPL.
-
- ***** END LICENSE BLOCK ***** -->
<!--
Features to add:
* make the left and right parts of the viewer independently scrollable
* make the test list filterable
** default to only showing unexpecteds
* add other ways to highlight differences other than circling?
* add zoom/pan to images
* Add ability to load log via XMLHttpRequest (also triggered via URL param)
* color the test list based on pass/fail and expected/unexpected/random/skip
* ability to load multiple logs ?
** rename them by clicking on the name and editing
** turn the test list into a collapsing tree view
** move log loading into popup from viewer UI
-->
<html lang="en-US" xml:lang="en-US" xmlns="http://www.w3.org/1999/xhtml">
<head>
<title>Reftest analyzer</title>
<style type="text/css"><![CDATA[
html, body { margin: 0; }
html { padding: 0; }
body { padding: 4px; }
#pixelarea, #itemlist, #images { position: absolute; }
#itemlist, #images { overflow: auto; }
#pixelarea { top: 0; left: 0; width: 320px; height: 84px; overflow: visible }
#itemlist { top: 84px; left: 0; width: 320px; bottom: 0; }
#images { top: 0; bottom: 0; left: 320px; right: 0; }
#leftpane { width: 320px; }
#images { position: fixed; top: 10px; left: 340px; }
form#imgcontrols { margin: 0; display: block; }
#itemlist > table { border-collapse: collapse; }
#itemlist > table > tbody > tr > td { border: 1px solid; padding: 1px; }
/*
#itemlist > table > tbody > tr.pass > td.url { background: lime; }
#itemlist > table > tbody > tr.fail > td.url { background: red; }
*/
#magnification > svg { display: block; width: 84px; height: 84px; }
#pixelinfo { font: small sans-serif; position: absolute; width: 200px; left: 84px; }
#pixelinfo table { border-collapse: collapse; }
#pixelinfo table th { white-space: nowrap; text-align: left; padding: 0; }
#pixelinfo table td { font-family: monospace; padding: 0 0 0 0.25em; }
#pixelhint { display: inline; color: #88f; cursor: help; }
#pixelhint > * { display: none; position: absolute; margin: 8px 0 0 8px; padding: 4px; width: 400px; background: #ffa; color: black; box-shadow: 3px 3px 2px #888; z-index: 1; }
#pixelhint:hover { color: #000; }
#pixelhint:hover > * { display: block; }
#pixelhint p { margin: 0; }
#pixelhint p + p { margin-top: 1em; }
]]></style>
<script type="text/javascript"><![CDATA[
var XLINK_NS = "http://www.w3.org/1999/xlink";
var SVG_NS = "http://www.w3.org/2000/svg";
var gPhases = null;
var gIDCache = {};
var gMagPixPaths = []; // 2D array of array-of-two <path> objects used in the pixel magnifier
var gMagWidth = 5; // number of zoomed in pixels to show horizontally
var gMagHeight = 5; // number of zoomed in pixels to show vertically
var gMagZoom = 16; // size of the zoomed in pixels
var gImage1Data; // ImageData object for the reference image
var gImage2Data; // ImageData object for the test output image
var gFlashingPixels = []; // array of <path> objects that should be flashed due to pixel color mismatch
function ID(id) {
if (!(id in gIDCache))
gIDCache[id] = document.getElementById(id);
return gIDCache[id];
}
function hash_parameters() {
var result = { };
var params = window.location.hash.substr(1).split(/[&;]/);
for (var i = 0; i < params.length; i++) {
var parts = params[i].split("=");
result[parts[0]] = unescape(unescape(parts[1]));
}
return result;
}
function load() {
gPhases = [ ID("entry"), ID("loading"), ID("viewer") ];
build_mag();
var params = hash_parameters();
if (params.log) {
ID("logentry").value = params.log;
log_pasted();
} else if (params.web) {
loadFromWeb(params.web);
}
}
function build_mag() {
var mag = ID("mag");
var r = document.createElementNS(SVG_NS, "rect");
r.setAttribute("x", gMagZoom * -gMagWidth / 2);
r.setAttribute("y", gMagZoom * -gMagHeight / 2);
r.setAttribute("width", gMagZoom * gMagWidth);
r.setAttribute("height", gMagZoom * gMagHeight);
mag.appendChild(r);
mag.setAttribute("transform", "translate(" + (gMagZoom * (gMagWidth / 2) + 1) + "," + (gMagZoom * (gMagHeight / 2) + 1) + ")");
for (var x = 0; x < gMagWidth; x++) {
gMagPixPaths[x] = [];
for (var y = 0; y < gMagHeight; y++) {
var p1 = document.createElementNS(SVG_NS, "path");
p1.setAttribute("d", "M" + ((x - gMagWidth / 2) + 1) * gMagZoom + "," + (y - gMagHeight / 2) * gMagZoom + "h" + -gMagZoom + "v" + gMagZoom);
p1.setAttribute("stroke", "black");
p1.setAttribute("stroke-width", "1px");
p1.setAttribute("fill", "#aaa");
var p2 = document.createElementNS(SVG_NS, "path");
p2.setAttribute("d", "M" + ((x - gMagWidth / 2) + 1) * gMagZoom + "," + (y - gMagHeight / 2) * gMagZoom + "v" + gMagZoom + "h" + -gMagZoom);
p2.setAttribute("stroke", "black");
p2.setAttribute("stroke-width", "1px");
p2.setAttribute("fill", "#888");
mag.appendChild(p1);
mag.appendChild(p2);
gMagPixPaths[x][y] = [p1, p2];
}
}
var flashedOn = false;
setInterval(function() {
flashedOn = !flashedOn;
flash_pixels(flashedOn);
}, 500);
}
function show_phase(phaseid) {
for (var i in gPhases) {
var phase = gPhases[i];
phase.style.display = (phase.id == phaseid) ? "" : "none";
}
if (phase == "viewer")
ID("images").style.display = "none";
}
function loadFromWeb(url) {
var r = new XMLHttpRequest();
r.open("GET", url);
r.onreadystatechange = function() {
if (r.readyState == 4) {
process_log(r.response);
}
}
r.send(null);
}
function fileentry_changed() {
show_phase("loading");
var input = ID("fileentry");
var files = input.files;
if (files.length > 0) {
// Only handle the first file; don't handle multiple selection.
// The parts of the log we care about are ASCII-only. Since we
// can ignore lines we don't care about, best to read in as
// iso-8859-1, which guarantees we don't get decoding errors.
var fileReader = new FileReader();
fileReader.onload = function(e) {
var log = null;
log = e.target.result;
if (log)
process_log(log);
else
show_phase("entry");
}
fileReader.readAsText(files[0], "iso-8859-1");
}
// So the user can process the same filename again (after
// overwriting the log), clear the value on the form input so we
// will always get an onchange event.
input.value = "";
}
function log_pasted() {
show_phase("loading");
var entry = ID("logentry");
var log = entry.value;
entry.value = "";
process_log(log);
}
var gTestItems;
function process_log(contents) {
var lines = contents.split(/[\r\n]+/);
gTestItems = [];
for (var j in lines) {
var line = lines[j];
var match = line.match(/^(?:NEXT ERROR )?REFTEST (.*)$/);
if (!match)
continue;
line = match[1];
match = line.match(/^(TEST-PASS|TEST-UNEXPECTED-PASS|TEST-KNOWN-FAIL|TEST-UNEXPECTED-FAIL)(\(EXPECTED RANDOM\)|) \| ([^\|]+) \|(.*)/);
if (match) {
var state = match[1];
var random = match[2];
var url = match[3];
var extra = match[4];
gTestItems.push(
{
pass: !state.match(/FAIL$/),
// only one of the following three should ever be true
unexpected: !!state.match(/^TEST-UNEXPECTED/),
random: (random == "(EXPECTED RANDOM)"),
skip: (extra == " (SKIP)"),
url: url,
images: []
});
continue;
}
match = line.match(/^ IMAGE[^:]*: (.*)$/);
if (match) {
var item = gTestItems[gTestItems.length - 1];
item.images.push(match[1]);
}
}
build_viewer();
}
function build_viewer() {
if (gTestItems.length == 0) {
show_phase("entry");
return;
}
var cell = ID("itemlist");
while (cell.childNodes.length > 0)
cell.removeChild(cell.childNodes[cell.childNodes.length - 1]);
var table = document.createElement("table");
var tbody = document.createElement("tbody");
table.appendChild(tbody);
for (var i in gTestItems) {
var item = gTestItems[i];
// XXX skip expected pass items until we have filtering UI
if (item.pass && !item.unexpected)
continue;
var tr = document.createElement("tr");
var rowclass = item.pass ? "pass" : "fail";
var td;
var text;
td = document.createElement("td");
text = "";
if (item.unexpected) { text += "!"; rowclass += " unexpected"; }
if (item.random) { text += "R"; rowclass += " random"; }
if (item.skip) { text += "S"; rowclass += " skip"; }
td.appendChild(document.createTextNode(text));
tr.appendChild(td);
td = document.createElement("td");
td.className = "url";
// Only display part of URL after "/mozilla/".
var match = item.url.match(/\/mozilla\/(.*)/);
text = document.createTextNode(match ? match[1] : item.url);
if (item.images.length > 0) {
var a = document.createElement("a");
a.href = "javascript:show_images(" + i + ")";
a.appendChild(text);
td.appendChild(a);
} else {
td.appendChild(text);
}
tr.appendChild(td);
tbody.appendChild(tr);
}
cell.appendChild(table);
show_phase("viewer");
}
function get_image_data(src, whenReady) {
var img = new Image();
img.onload = function() {
var canvas = document.createElement("canvas");
canvas.width = 800;
canvas.height = 1000;
var ctx = canvas.getContext("2d");
ctx.drawImage(img, 0, 0);
whenReady(ctx.getImageData(0, 0, 800, 1000));
};
img.src = src;
}
function show_images(i) {
var item = gTestItems[i];
var cell = ID("images");
ID("image1").style.display = "";
ID("image2").style.display = "none";
ID("diffrect").style.display = "none";
ID("imgcontrols").reset();
ID("image1").setAttributeNS(XLINK_NS, "xlink:href", item.images[0]);
// Making the href be #image1 doesn't seem to work
ID("feimage1").setAttributeNS(XLINK_NS, "xlink:href", item.images[0]);
if (item.images.length == 1) {
ID("imgcontrols").style.display = "none";
} else {
ID("imgcontrols").style.display = "";
ID("image2").setAttributeNS(XLINK_NS, "xlink:href", item.images[1]);
// Making the href be #image2 doesn't seem to work
ID("feimage2").setAttributeNS(XLINK_NS, "xlink:href", item.images[1]);
}
cell.style.display = "";
get_image_data(item.images[0], function(data) { gImage1Data = data });
get_image_data(item.images[1], function(data) { gImage2Data = data });
}
function show_image(i) {
if (i == 1) {
ID("image1").style.display = "";
ID("image2").style.display = "none";
} else {
ID("image1").style.display = "none";
ID("image2").style.display = "";
}
}
function show_differences(cb) {
ID("diffrect").style.display = cb.checked ? "" : "none";
}
function flash_pixels(on) {
var stroke = on ? "red" : "black";
var strokeWidth = on ? "2px" : "1px";
for (var i = 0; i < gFlashingPixels.length; i++) {
gFlashingPixels[i].setAttribute("stroke", stroke);
gFlashingPixels[i].setAttribute("stroke-width", strokeWidth);
}
}
function cursor_point(evt) {
var m = evt.target.getScreenCTM().inverse();
var p = ID("svg").createSVGPoint();
p.x = evt.clientX;
p.y = evt.clientY;
p = p.matrixTransform(m);
return { x: Math.floor(p.x), y: Math.floor(p.y) };
}
function hex2(i) {
return (i < 16 ? "0" : "") + i.toString(16);
}
function canvas_pixel_as_hex(data, x, y) {
var offset = (y * data.width + x) * 4;
var r = data.data[offset];
var g = data.data[offset + 1];
var b = data.data[offset + 2];
return "#" + hex2(r) + hex2(g) + hex2(b);
}
function hex_as_rgb(hex) {
return "rgb(" + [parseInt(hex.substring(1, 3), 16), parseInt(hex.substring(3, 5), 16), parseInt(hex.substring(5, 7), 16)] + ")";
}
function magnify(evt) {
var { x: x, y: y } = cursor_point(evt);
var centerPixelColor1, centerPixelColor2;
var dx_lo = -Math.floor(gMagWidth / 2);
var dx_hi = Math.floor(gMagWidth / 2);
var dy_lo = -Math.floor(gMagHeight / 2);
var dy_hi = Math.floor(gMagHeight / 2);
flash_pixels(false);
gFlashingPixels = [];
for (var j = dy_lo; j <= dy_hi; j++) {
for (var i = dx_lo; i <= dx_hi; i++) {
var px = x + i;
var py = y + j;
var p1 = gMagPixPaths[i + dx_hi][j + dy_hi][0];
var p2 = gMagPixPaths[i + dx_hi][j + dy_hi][1];
if (px < 0 || py < 0 || px >= 800 || py >= 1000) {
p1.setAttribute("fill", "#aaa");
p2.setAttribute("fill", "#888");
} else {
var color1 = canvas_pixel_as_hex(gImage1Data, x + i, y + j);
var color2 = canvas_pixel_as_hex(gImage2Data, x + i, y + j);
p1.setAttribute("fill", color1);
p2.setAttribute("fill", color2);
if (color1 != color2) {
gFlashingPixels.push(p1, p2);
p1.parentNode.appendChild(p1);
p2.parentNode.appendChild(p2);
}
if (i == 0 && j == 0) {
centerPixelColor1 = color1;
centerPixelColor2 = color2;
}
}
}
}
flash_pixels(true);
show_pixelinfo(x, y, centerPixelColor1, hex_as_rgb(centerPixelColor1), centerPixelColor2, hex_as_rgb(centerPixelColor2));
}
function show_pixelinfo(x, y, pix1rgb, pix1hex, pix2rgb, pix2hex) {
var pixelinfo = ID("pixelinfo");
ID("coords").textContent = [x, y];
ID("pix1hex").textContent = pix1hex;
ID("pix1rgb").textContent = pix1rgb;
ID("pix2hex").textContent = pix2hex;
ID("pix2rgb").textContent = pix2rgb;
}
]]></script>
</head>
<body onload="load()">
<div id="entry">
<h1>Reftest analyzer: load reftest log</h1>
<p>Either paste your log into this textarea:<br />
<textarea cols="80" rows="10" id="logentry"/><br/>
<input type="button" value="Process pasted log" onclick="log_pasted()" /></p>
<p>... or load it from a file:<br/>
<input type="file" id="fileentry" onchange="fileentry_changed()" />
</p>
</div>
<div id="loading" style="display:none">Loading log...</div>
<div id="viewer" style="display:none">
<div id="pixelarea">
<div id="pixelinfo">
<table>
<tbody>
<tr><th>Pixel at:</th><td colspan="2" id="coords"/></tr>
<tr><th>Image 1:</th><td id="pix1rgb"></td><td id="pix1hex"></td></tr>
<tr><th>Image 2:</th><td id="pix2rgb"></td><td id="pix2hex"></td></tr>
</tbody>
</table>
<div>
<div id="pixelhint">
<div>
<p>Move the mouse over the reftest image on the right to show
magnified pixels on the left. The color information above is for
the pixel centered in the magnified view.</p>
<p>Image 1 is shown in the upper triangle of each pixel and Image 2
is shown in the lower triangle.</p>
</div>
</div>
</div>
</div>
<div id="magnification">
<svg xmlns="http://www.w3.org/2000/svg" width="84" height="84" shape-rendering="optimizeSpeed">
<g id="mag"/>
</svg>
</div>
</div>
<div id="itemlist"></div>
<div id="images" style="display:none">
<form id="imgcontrols">
<label><input type="radio" name="which" value="0" onchange="show_image(1)" checked="checked" />Test</label>
<label><input type="radio" name="which" value="1" onchange="show_image(2)" />Reference</label>
<label><input type="checkbox" onchange="show_differences(this)" />Circle differences</label>
</form>
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="800px" height="1000px" viewbox="0 0 800 1000" id="svg">
<defs>
<!-- use sRGB to avoid loss of data -->
<filter id="showDifferences" x="0%" y="0%" width="100%" height="100%"
style="color-interpolation-filters: sRGB">
<feImage id="feimage1" result="img1" xlink:href="#image1" />
<feImage id="feimage2" result="img2" xlink:href="#image2" />
<!-- inv1 and inv2 are the images with RGB inverted -->
<feComponentTransfer result="inv1" in="img1">
<feFuncR type="linear" slope="-1" intercept="1" />
<feFuncG type="linear" slope="-1" intercept="1" />
<feFuncB type="linear" slope="-1" intercept="1" />
</feComponentTransfer>
<feComponentTransfer result="inv2" in="img2">
<feFuncR type="linear" slope="-1" intercept="1" />
<feFuncG type="linear" slope="-1" intercept="1" />
<feFuncB type="linear" slope="-1" intercept="1" />
</feComponentTransfer>
<!-- w1 will have non-white pixels anywhere that img2
is brighter than img1, and w2 for the reverse.
It would be nice not to have to go through these
intermediate states, but feComposite
type="arithmetic" can't transform the RGB channels
and leave the alpha channel untouched. -->
<feComposite result="w1" in="img1" in2="inv2" operator="arithmetic" k2="1" k3="1" />
<feComposite result="w2" in="img2" in2="inv1" operator="arithmetic" k2="1" k3="1" />
<!-- c1 will have non-black pixels anywhere that img2
is brighter than img1, and c2 for the reverse -->
<feComponentTransfer result="c1" in="w1">
<feFuncR type="linear" slope="-1" intercept="1" />
<feFuncG type="linear" slope="-1" intercept="1" />
<feFuncB type="linear" slope="-1" intercept="1" />
</feComponentTransfer>
<feComponentTransfer result="c2" in="w2">
<feFuncR type="linear" slope="-1" intercept="1" />
<feFuncG type="linear" slope="-1" intercept="1" />
<feFuncB type="linear" slope="-1" intercept="1" />
</feComponentTransfer>
<!-- c will be nonblack (and fully on) for every pixel+component where there are differences -->
<feComposite result="c" in="c1" in2="c2" operator="arithmetic" k2="255" k3="255" />
<!-- a will be opaque for every pixel with differences and transparent for all others -->
<feColorMatrix result="a" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 0 0" />
<!-- a, dilated by 4 pixels -->
<feMorphology result="dila4" in="a" operator="dilate" radius="4" />
<!-- a, dilated by 1 pixel -->
<feMorphology result="dila1" in="a" operator="dilate" radius="1" />
<!-- all the pixels in the 3-pixel dilation of a but not in the 1-pixel dilation of a, to highlight the diffs -->
<feComposite result="highlight" in="dila4" in2="dila1" operator="out" />
<feFlood result="red" flood-color="red" />
<feComposite result="redhighlight" in="red" in2="highlight" operator="in" />
<feFlood result="black" flood-color="black" flood-opacity="0.5" />
<feMerge>
<feMergeNode in="black" />
<feMergeNode in="redhighlight" />
</feMerge>
</filter>
</defs>
<g onmousemove="magnify(evt)">
<image x="0" y="0" width="100%" height="100%" id="image1" />
<image x="0" y="0" width="100%" height="100%" id="image2" />
</g>
<rect id="diffrect" filter="url(#showDifferences)" pointer-events="none" x="0" y="0" width="100%" height="100%" />
</svg>
</div>
</div>
</body>
</html>

Просмотреть файл

@ -36,31 +36,14 @@
}
function displayImage(id, url, div) {
return new Promise(function (resolve, reject) {
var xhr = new XMLHttpRequest();
xhr.open('GET', url);
xhr.responseType = 'text';
xhr.onload = function () {
var src = xhr.response;
var a = document.createElement('a');
a.href = src;
a.title = id;
var img = document.createElement('img');
img.src = src;
img.width = 96;
a.appendChild(img);
div.appendChild(a);
resolve();
};
xhr.onerror = function () {
var span = document.createElement('span');
span.title = 'Failed ' + id + ': ' + xhr.error;
span.textContent = id;
div.appendChild(span);
resolve();
};
xhr.send(null);
});
var a = document.createElement('a');
a.href = url;
a.title = id;
var img = document.createElement('img');
img.src = url;
img.width = 96;
a.appendChild(img);
div.appendChild(a);
}
function displayConfigImage(name, baseUrl, count, div) {
@ -74,8 +57,8 @@
var promise = Promise.resolve(undefined);
for (var i = 1; i <= count; i++) {
var base = baseUrl + '/' + i;
promise = promise.then(displayImage.bind(null, i, base, images));
var imageUrl = baseUrl + '/' + i + '.png';
displayImage(i, imageUrl, images);
}
return promise;
}
@ -108,7 +91,7 @@
}).join(',') + '}') : {};
var platform = navigator.userAgent.indexOf('Mac OS') >= 0 ? 'darwin' :
navigator.userAgent.indexOf('Windows NT') >= 0 ? 'win32' : 'linux2';
navigator.userAgent.indexOf('Windows NT') >= 0 ? 'win32' : 'linux';
var refsBase = '../ref/' + (queryParams.platform || platform);
var refsConfigs;

623
test/test.js Normal file
Просмотреть файл

@ -0,0 +1,623 @@
/* -*- Mode: js; js-indent-level: 2; indent-tabs-mode: nil; tab-width: 2 -*- */
/* vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: */
/*
* Copyright 2014 Mozilla Foundation
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/*jslint node: true */
'use strict';
var WebServer = require('./webserver.js').WebServer;
var WebBrowser = require('./webbrowser.js').WebBrowser;
var path = require('path');
var fs = require('fs');
var os = require('os');
var url = require('url');
var testUtils = require('./testutils.js');
function parseOptions() {
function describeCheck(fn, text) {
fn.toString = function () {
return text;
};
return fn;
}
var yargs = require('yargs')
.usage('Usage: $0')
.boolean(['help', 'masterMode', 'reftest', 'noPrompts'])
.string(['manifestFile', 'browser', 'browserManifestFile',
'port', 'statsFile', 'statsDelay'])
.alias('browser', 'b').alias('help', 'h').alias('masterMode', 'm')
.describe('help', 'Show this help message')
.describe('masterMode', 'Run the script in master mode.')
.describe('noPrompts',
'Uses default answers (intended for CLOUD TESTS only!).')
.describe('manifestFile',
'A path to JSON file in the form of test_manifest.json')
.default('manifestFile', 'test_manifest.json')
.describe('browser', 'The path to a single browser ')
.describe('browserManifestFile', 'A path to JSON file in the form of ' +
'those found in resources/browser_manifests/')
.describe('reftest', 'Automatically start reftest showing comparison ' +
'test failures, if there are any.')
.describe('port', 'The port the HTTP server should listen on.')
.default('port', 8000)
.describe('statsFile', 'The file where to store stats.')
.describe('statsDelay', 'The amount of time in milliseconds the browser ' +
'should wait before starting stats.')
.default('statsDelay', 0)
.check(describeCheck(function (argv) {
return +argv.reftest + argv.masterMode <= 1;
}, '--reftest, --masterMode must not be specified at the same time.'))
.check(describeCheck(function (argv) {
return !argv.masterMode || argv.manifestFile === 'test_manifest.json';
}, 'when --masterMode is specified --manifestFile shall be equal ' +
'test_manifest.json'))
.check(describeCheck(function (argv) {
return !argv.browser || !argv.browserManifestFile;
}, '--browser and --browserManifestFile must not be specified at the ' +'' +
'same time.'));
var result = yargs.argv;
if (result.help) {
yargs.showHelp();
process.exit(0);
}
return result;
}
var refsTmpDir = 'tmp';
var testResultDir = 'test_snapshots';
var refsDir = 'ref';
var eqLog = 'eq.log';
var traceLog = 'reftrace.log';
var browserTimeout = 120;
function monitorBrowserTimeout(session, onTimeout) {
if (session.timeoutMonitor) {
clearTimeout(session.timeoutMonitor);
}
if (!onTimeout) {
session.timeoutMonitor = null;
return;
}
session.timeoutMonitor = setTimeout(function () {
onTimeout(session);
}, browserTimeout * 1000);
}
function updateRefImages() {
function sync(removeTmp) {
console.log(' Updating ref/ ... ');
testUtils.copySubtreeSync(refsTmpDir, refsDir);
if (removeTmp) {
testUtils.removeDirSync(refsTmpDir);
}
console.log('done');
}
if (options.noPrompts) {
sync(false); // don't remove tmp/ for botio
return;
}
testUtils.confirm('Would you like to update the master copy in ref/? [yn] ',
function (confirmed) {
if (confirmed) {
sync(true);
} else {
console.log(' OK, not updating.');
}
});
}
function examineRefImages() {
startServer();
var startUrl = 'http://' + server.host + ':' + server.port +
'/test/resources/reftest-analyzer.html#web=/test/eq.log';
var browser = WebBrowser.create(sessions[0].config);
browser.start(startUrl);
}
function startRefTest(masterMode, showRefImages) {
function finalize() {
stopServer();
var numErrors = 0;
var numStasFailures = 0;
var numEqFailures = 0;
var numEqNoSnapshot = 0;
sessions.forEach(function (session) {
numErrors += session.numErrors;
numStasFailures += session.numStasFailures;
numEqFailures += session.numEqFailures;
numEqNoSnapshot += session.numEqNoSnapshot;
});
var numFatalFailures = numErrors + numStasFailures;
console.log();
if (numFatalFailures + numEqFailures > 0) {
console.log('OHNOES! Some tests failed!');
if (numErrors > 0) {
console.log(' errors: ' + numErrors);
}
if (numEqFailures > 0) {
console.log(' different ref/snapshot: ' + numEqFailures);
}
if (numStasFailures > 0) {
console.log(' failed stas: ' + numStasFailures);
}
} else {
console.log('All regression tests passed.');
}
var runtime = (Date.now() - startTime) / 1000;
console.log('Runtime was ' + runtime.toFixed(1) + ' seconds');
if (options.statsFile) {
fs.writeFileSync(options.statsFile, JSON.stringify(stats, null, 2));
}
if (masterMode) {
if (numEqFailures + numEqNoSnapshot > 0) {
console.log();
console.log('Some eq tests failed or didn\'t have snapshots.');
console.log('Checking to see if master references can be updated...');
if (numFatalFailures > 0 && !options.noPrompts) {
console.log(' No. Some non-eq tests failed.');
} else {
console.log(
' Yes! The references in tmp/ can be synced with ref/.');
updateRefImages();
}
}
} else if (showRefImages && numEqFailures > 0) {
console.log();
console.log('Starting reftest harness to examine ' + numEqFailures +
' eq test failures.');
examineRefImages(numEqFailures);
}
}
function setup() {
if (fs.existsSync(refsTmpDir)) {
console.error('tmp/ exists -- unable to proceed with testing');
process.exit(1);
}
if (fs.existsSync(eqLog)) {
fs.unlink(eqLog);
}
if (fs.existsSync(traceLog)) {
fs.unlink(traceLog);
}
if (fs.existsSync(testResultDir)) {
testUtils.removeDirSync(testResultDir);
}
startTime = Date.now();
startServer();
server.hooks['POST'].push(refTestPostHandler);
onAllSessionsClosed = finalize;
startBrowsers('/test/test.html', function (session) {
session.masterMode = masterMode;
session.taskResults = {};
session.tasks = {};
session.remaining = manifest.length;
manifest.forEach(function (item) {
session.taskResults[item.id] = [];
session.tasks[item.id] = item;
});
session.numErrors = 0;
session.numStasFailures = 0;
session.numEqNoSnapshot = 0;
session.numEqFailures = 0;
session.currentTestId = 'n/a';
monitorBrowserTimeout(session, handleSessionTimeout);
});
}
function checkRefsTmp() {
if (masterMode && fs.existsSync(refsTmpDir)) {
if (options.noPrompts) {
testUtils.removeDirSync(refsTmpDir);
setup();
return;
}
console.log('Temporary snapshot dir tmp/ is still around.');
console.log('tmp/ can be removed if it has nothing you need.');
testUtils.confirm('SHOULD THIS SCRIPT REMOVE tmp/? THINK CAREFULLY [yn] ',
function (confirmed) {
if (confirmed) {
testUtils.removeDirSync(refsTmpDir);
}
setup();
});
} else {
setup();
}
}
var startTime;
var manifest = JSON.parse(fs.readFileSync(options.manifestFile));
checkRefsTmp();
}
function handleSessionTimeout(session) {
if (session.closed) {
return;
}
var browser = session.name;
var id = session.currentTestId;
console.log('TEST-UNEXPECTED-FAIL | test ' + id + ' failed ' + browser +
' has not responded in ' + browserTimeout + 's');
session.numErrors += session.remaining;
session.remaining = 0;
closeSession(browser);
}
// TODO remove shortly after landing
function migrateRefPng(src, dest) {
var text = fs.readFileSync(src).toString();
var i = text.indexOf('base64,');
if (i < 0) {
return;
}
fs.writeFileSync(dest, new Buffer(text.substr(i + 7), 'base64'));
}
function checkEq(task, results, browser, masterMode) {
var taskId = task.id;
var refSnapshotDir = path.join(refsDir, os.platform(), browser, taskId);
var testSnapshotDir = path.join(testResultDir, os.platform(), browser,
taskId);
var taskType = task.type;
var numEqNoSnapshot = 0;
var numEqFailures = 0;
for (var i = 0; i < results.length; i++) {
if (!results[i]) {
continue;
}
var testSnapshot = results[i].snapshot;
if (testSnapshot && testSnapshot.indexOf('data:image/png;base64,') === 0) {
testSnapshot = new Buffer(testSnapshot.substring(22), 'base64');
} else {
console.error('Valid snapshot was not found.');
}
var refSnapshot = null;
var eq = false;
var refPath = path.join(refSnapshotDir, (i + 1) + '.png');
if (!fs.existsSync(refPath) &&
fs.existsSync(path.join(refSnapshotDir, (i + 1).toString()))) {
// old format, converting
migrateRefPng(path.join(refSnapshotDir, (i + 1).toString()), refPath);
// TODO delete old file?
}
if (!fs.existsSync(refPath)) {
numEqNoSnapshot++;
if (!masterMode) {
console.log('WARNING: no reference snapshot ' + refPath);
}
} else {
refSnapshot = fs.readFileSync(refPath);
eq = (refSnapshot.toString('hex') === testSnapshot.toString('hex'));
if (!eq) {
console.log('TEST-UNEXPECTED-FAIL | ' + taskType + ' ' + taskId +
' | in ' + browser + ' | rendering of item ' + (i + 1) +
' != reference rendering');
testUtils.ensureDirSync(testSnapshotDir);
fs.writeFileSync(path.join(testSnapshotDir, (i + 1) + '.png'),
testSnapshot);
fs.writeFileSync(path.join(testSnapshotDir, (i + 1) + '_ref.png'),
refSnapshot);
// NB: this follows the format of Mozilla reftest output so that
// we can reuse its reftest-analyzer script
fs.appendFileSync(eqLog, 'REFTEST TEST-UNEXPECTED-FAIL | ' + browser +
'-' + taskId + '-item' + (i + 1) + ' | image comparison (==)\n' +
'REFTEST IMAGE 1 (TEST): ' +
path.join(testSnapshotDir, (i + 1) + '.png') + '\n' +
'REFTEST IMAGE 2 (REFERENCE): ' +
path.join(testSnapshotDir, (i + 1) + '_ref.png') + '\n');
numEqFailures++;
}
}
if (masterMode && (!refSnapshot || !eq)) {
var tmpSnapshotDir = path.join(refsTmpDir, os.platform(), browser,
taskId);
testUtils.ensureDirSync(tmpSnapshotDir);
fs.writeFileSync(path.join(tmpSnapshotDir, (i + 1) + '.png'),
testSnapshot);
}
}
var session = getSession(browser);
session.numEqNoSnapshot += numEqNoSnapshot;
if (numEqFailures > 0) {
session.numEqFailures += numEqFailures;
} else {
console.log('TEST-PASS | ' + taskType + ' test ' + taskId + ' | in ' +
browser);
}
}
var diffRunQueue = [];
function diffData(testData, refData, callback) {
diffRunQueue.push([testData, refData, callback]);
if (diffRunQueue.length > 1) {
return;
}
(function run() {
var diffRun = diffRunQueue[0];
fs.writeFileSync('refdata~', diffRun[1]);
fs.writeFileSync('testdata~', diffRun[0]);
var diff = require('child_process').
exec('diff -U 2 refdata~ testdata~', function (error, stdout, stderr) {
fs.unlinkSync('refdata~');
fs.unlinkSync('testdata~');
var callback = diffRun[2];
if (!stdout) {
callback('<<<< reference\n' + diffRun[1] + '====\n' + diffRun[0] + '>>>> test\n');
} else {
callback(stdout);
}
diffRunQueue.shift();
if (diffRunQueue.length > 0) {
run();
}
});
})();
}
function checkStas(task, results, browser) {
var taskId = task.id;
var taskType = task.type;
var numStasFailures = 0;
for (var i = 0; i < results.length; i++) {
var snapshot = results[i].snapshot;
var item = results[i].item;
if (snapshot.isDifferent) {
console.log('TEST-UNEXPECTED-FAIL | ' + taskType + ' ' + taskId, ' | in ' +
browser + ' | trace of ' + (i + 1) + ' != reference trace');
diffData(snapshot['data1'], snapshot['data2'], function (item, diff) {
fs.appendFileSync(traceLog,
'REFTEST TEST-UNEXPECTED-FAIL | ' + browser + '-' + taskId + '-item' + (i + 1) +
' | ' + item + ' | trace\n' + diff + '\n');
}.bind(null, item));
numStasFailures++;
}
}
if (numStasFailures > 0) {
getSession(browser).numStasFailures += numStasFailures;
} else {
console.log('TEST-PASS | stas test ' + taskId + ' | in ' + browser);
}
}
function checkRefTestResults(browser, id, results) {
var failed = false;
var session = getSession(browser);
var task = session.tasks[id];
results.forEach(function (itemResult, index) {
if (!itemResult) {
return; // no results
}
if (itemResult.failure) {
failed = true;
if (fs.existsSync(task.file + '.error')) {
console.log('TEST-SKIPPED | SWF was not downloaded ' + id + ' | in ' +
browser + ' | item' + (index + 1) + ' | ' + itemResult.failure);
} else {
session.numErrors++;
console.log('TEST-UNEXPECTED-FAIL | test failed ' + id + ' | in ' +
browser + ' | item' + (index + 1) + ' | ' + itemResult.failure);
}
}
});
if (failed) {
return;
}
switch (task.type) {
case 'eq':
checkEq(task, results, browser, session.masterMode);
break;
case 'stas':
checkStas(task, results, browser);
break;
default:
throw new Error('Unknown test type');
}
// clear memory
results.forEach(function (result, index) {
result.snapshot = null;
});
}
function refTestPostHandler(req, res) {
var parsedUrl = url.parse(req.url, true);
var pathname = parsedUrl.pathname;
if (pathname !== '/tellMeToQuit' &&
pathname !== '/info' &&
pathname !== '/progress' &&
pathname !== '/result') {
return false;
}
var body = '';
req.on('data', function (data) {
body += data;
});
req.on('end', function () {
res.writeHead(200, {'Content-Type': 'text/plain'});
res.end();
var session;
if (pathname === '/tellMeToQuit') {
// finding by path
var browser = parsedUrl.query.browser;
session = getSession(browser);
monitorBrowserTimeout(session, null);
closeSession(session.name);
return;
}
var data = JSON.parse(body);
if (pathname === '/info') {
console.log(data.message);
return;
}
if (pathname === '/progress') {
session = getSession(data.browser);
monitorBrowserTimeout(session, handleSessionTimeout);
session.currentTestId = data.id;
return;
}
var browser = data.browser;
var id = data.id;
var item = data.item;
var failure = data.failure;
var snapshot = data.snapshot;
var numItems = data.numItems;
session = getSession(browser);
monitorBrowserTimeout(session, handleSessionTimeout);
var taskResults = session.taskResults[id];
var itemIndex = taskResults.length;
taskResults[itemIndex] = {
item: item,
failure: failure,
snapshot: snapshot
};
if (stats) {
stats.push({
'browser': browser,
'swf': id,
'item': itemIndex,
'stats': data.stats
});
}
var isDone = taskResults[numItems - 1];
if (isDone) {
checkRefTestResults(browser, id, taskResults);
session.remaining--;
}
});
return true;
}
function startBrowsers(url, initSessionCallback) {
var browsers;
if (options.browserManifestFile) {
browsers = JSON.parse(fs.readFileSync(options.browserManifestFile));
} else if (options.browser) {
var browserPath = options.browser;
var name = path.basename(browserPath, path.extname(browserPath));
browsers = [{name: name, path: browserPath}];
} else {
console.error('Specify either browser or browserManifestFile.');
process.exit(1);
}
sessions = [];
browsers.forEach(function (b) {
var browser = WebBrowser.create(b);
var startUrl = getServerBaseAddress() + url +
'?browser=' + encodeURIComponent(b.name) +
'&manifestFile=' + encodeURIComponent('/test/' + options.manifestFile) +
'&path=' + encodeURIComponent(b.path) +
'&delay=' + options.statsDelay +
'&masterMode=' + options.masterMode;
browser.start(startUrl);
var session = {
name: b.name,
config: b,
browser: browser,
closed: false
};
if (initSessionCallback) {
initSessionCallback(session);
}
sessions.push(session);
});
}
function getServerBaseAddress() {
return 'http://' + host + ':' + server.port;
}
function startServer() {
server = new WebServer();
server.host = host;
server.port = options.port;
server.root = '..';
server.cacheExpirationTime = 3600;
server.start();
}
function stopServer() {
server.stop();
}
function getSession(browser) {
return sessions.filter(function (session) {
return session.name === browser;
})[0];
}
function closeSession(browser) {
var i = 0;
while (i < sessions.length && sessions[i].name !== browser) {
i++;
}
if (i < sessions.length) {
var session = sessions[i];
session.browser.stop(function () {
session.closed = true;
var allClosed = sessions.every(function (s) {
return s.closed;
});
if (allClosed && onAllSessionsClosed) {
onAllSessionsClosed();
}
});
}
}
function main() {
if (options.statsFile) {
stats = [];
}
if (!options.browser && !options.browserManifestFile) {
startServer();
} else {
startRefTest(options.masterMode, options.reftest && !options.noPrompts);
}
}
var server;
var sessions;
var onAllSessionsClosed;
var host = '127.0.0.1';
var options = parseOptions();
var stats;
main();

Просмотреть файл

@ -1,686 +0,0 @@
# Copyright 2012 Mozilla Foundation
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import json, platform, os, shutil, sys, subprocess, tempfile, threading
import time, urllib, urllib2, hashlib, re, base64, uuid, socket, errno
from BaseHTTPServer import BaseHTTPRequestHandler, HTTPServer
from SocketServer import ThreadingMixIn
from optparse import OptionParser
from urlparse import urlparse, parse_qs
from threading import Lock
USAGE_EXAMPLE = "%prog"
# The local web server uses the git repo as the document root.
DOC_ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__),".."))
DEFAULT_MANIFEST_FILE = 'test_manifest.json'
EQLOG_FILE = 'eq.log'
BROWSERLOG_FILE = 'browser.log'
TRACELOG_FILE = 'reftrace.log'
REFDIR = 'ref'
TMPDIR = 'tmp'
VERBOSE = False
BROWSER_TIMEOUT = 90
SERVER_HOST = "localhost"
lock = Lock()
class TestOptions(OptionParser):
def __init__(self, **kwargs):
OptionParser.__init__(self, **kwargs)
self.add_option("-m", "--masterMode", action="store_true", dest="masterMode",
help="Run the script in master mode.", default=False)
self.add_option("--noPrompts", action="store_true", dest="noPrompts",
help="Uses default answers (intended for CLOUD TESTS only!).", default=False)
self.add_option("--manifestFile", action="store", type="string", dest="manifestFile",
help="A JSON file in the form of test_manifest.json (the default).")
self.add_option("-b", "--browser", action="store", type="string", dest="browser",
help="The path to a single browser (right now, only Firefox is supported).")
self.add_option("--browserManifestFile", action="store", type="string",
dest="browserManifestFile",
help="A JSON file in the form of those found in resources/browser_manifests")
self.add_option("--reftest", action="store_true", dest="reftest",
help="Automatically start reftest showing comparison test failures, if there are any.",
default=False)
self.add_option("--bundle", action="store_true", dest="bundle",
help="Runs tests for compiled/bundled files.",
default=False)
self.add_option("--port", action="store", dest="port", type="int",
help="The port the HTTP server should listen on.", default=8080)
self.set_usage(USAGE_EXAMPLE)
def verifyOptions(self, options):
if options.masterMode and options.manifestFile:
self.error("--masterMode and --manifestFile must not be specified at the same time.")
if not options.manifestFile:
options.manifestFile = DEFAULT_MANIFEST_FILE
if options.browser and options.browserManifestFile:
print "Warning: ignoring browser argument since manifest file was also supplied"
if not options.browser and not options.browserManifestFile:
print "Starting server on port %s." % options.port
return options
def prompt(question):
'''Return True if the user answered "yes" to |question|.'''
inp = raw_input(question +' [yes/no] > ')
return inp == 'yes'
MIMEs = {
'.css': 'text/css',
'.html': 'text/html',
'.js': 'application/javascript',
'.json': 'application/json',
'.svg': 'image/svg+xml',
'.swf': 'application/x-shockwave-flash',
'.xhtml': 'application/xhtml+xml',
'.gif': 'image/gif',
'.ico': 'image/x-icon',
'.jpg': 'image/jpeg',
'.png': 'image/png',
'.log': 'text/plain',
'.properties': 'text/plain',
'.stas': 'text/plain',
'.trace': 'text/plain',
'.as': 'text/plain',
'.abc': 'application/octet-stream',
'.abcs': 'application/octet-stream',
'.bin': 'application/octet-stream',
'.txt': 'text/plain',
'.map': 'text/plain',
}
class State:
browsers = [ ]
manifest = { }
taskResults = { }
remaining = { }
results = { }
done = False
numErrors = 0
numEqFailures = 0
numEqNoSnapshot = 0
numStasFailures = 0
eqLog = None
traceLog = None
lastPost = { }
currentTest = { }
class Result:
def __init__(self, snapshot, failure, item):
self.snapshot = snapshot
self.failure = failure
self.item = item
class TestServer(ThreadingMixIn, HTTPServer):
pass
class TestHandlerBase(BaseHTTPRequestHandler):
# Disable annoying noise by default
def log_request(code=0, size=0):
if VERBOSE:
BaseHTTPRequestHandler.log_request(code, size)
def handle_one_request(self):
try:
BaseHTTPRequestHandler.handle_one_request(self)
except socket.error, v:
# Ignoring connection reset by peer exceptions
if v[0] != errno.ECONNRESET:
raise
def sendFile(self, path, ext):
self.send_response(200)
self.send_header("Content-Type", MIMEs[ext])
self.send_header("Content-Length", os.path.getsize(path))
self.end_headers()
with open(path, "rb") as f:
self.wfile.write(f.read())
def do_GET(self):
with lock:
url = urlparse(self.path)
# Ignore query string
path, _ = urllib.unquote_plus(url.path), url.query
path = os.path.abspath(os.path.realpath(DOC_ROOT + os.sep + path))
prefix = os.path.commonprefix(( path, DOC_ROOT ))
_, ext = os.path.splitext(path.lower())
if url.path == "/favicon.ico":
self.sendFile(os.path.join(DOC_ROOT, "test", "resources", "favicon.ico"), ext)
return
if os.path.isdir(path):
self.sendIndex(url.path, url.query)
return
if not (prefix == DOC_ROOT
and os.path.isfile(path)
and ext in MIMEs):
print path
self.send_error(404)
return
if 'Range' in self.headers:
# TODO for fetch-as-you-go
self.send_error(501)
return
self.sendFile(path, ext)
class PDFTestHandler(TestHandlerBase):
def sendIndex(self, path, query):
if not path.endswith("/"):
# we need trailing slash
self.send_response(301)
redirectLocation = path + "/"
if query:
redirectLocation += "?" + query
self.send_header("Location", redirectLocation)
self.end_headers()
return
self.send_response(200)
self.send_header("Content-Type", "text/html")
self.end_headers()
if query == "frame":
self.wfile.write("<html><frameset cols=*,200><frame name=swf>" +
"<frame src='" + path + "'></frameset></html>")
return
location = os.path.abspath(os.path.realpath(DOC_ROOT + os.sep + path))
self.wfile.write("<html><body><h1>SWFs of " + path + "</h1>\n")
for filename in os.listdir(location):
if filename.lower().endswith('.swf'):
self.wfile.write("<a href='/examples/inspector/inspector.html?rfile=" +
urllib.quote_plus(path + filename, '/') + "' target=swf>" +
filename + "</a><br>\n")
self.wfile.write("</body></html>")
def do_POST(self):
with lock:
numBytes = int(self.headers['Content-Length'])
self.send_response(200)
self.send_header('Content-Type', 'text/plain')
self.end_headers()
url = urlparse(self.path)
if url.path == "/tellMeToQuit":
tellAppToQuit(url.path, url.query)
return
if url.path == "/progress":
progressInfo = parse_qs(url.query)
browser = progressInfo['browser'][0]
State.lastPost[browser] = int(time.time())
State.currentTest[browser] = progressInfo['id'][0]
return
result = json.loads(self.rfile.read(numBytes))
browser, id, failure, item, snapshot = result['browser'], result['id'], result['failure'], result['item'], result['snapshot']
State.lastPost[browser] = int(time.time())
taskResults = State.taskResults[browser][id]
taskResults.append(Result(snapshot, failure, item))
def isTaskDone():
numItems = result["numItems"]
if len(taskResults) < numItems:
return False
return True
if isTaskDone():
check(State.manifest[id], taskResults, browser,
self.server.masterMode)
# Please oh please GC this ...
del State.taskResults[browser][id]
State.remaining[browser] -= 1
checkIfDone()
def checkIfDone():
State.done = True
for key in State.remaining:
if State.remaining[key] != 0:
State.done = False
return
# Applescript hack to quit Chrome on Mac
def tellAppToQuit(path, query):
if platform.system() != "Darwin":
return
d = parse_qs(query)
path = d['path'][0]
cmd = """osascript<<END
tell application "%s"
quit
end tell
END""" % path
os.system(cmd)
class BaseBrowserCommand(object):
def __init__(self, browserRecord):
self.name = browserRecord["name"]
self.path = browserRecord["path"]
self.tempDir = None
self.process = None
if platform.system() == "Darwin" and (self.path.endswith(".app") or self.path.endswith(".app/")):
self._fixupMacPath()
if not os.path.exists(self.path):
raise Exception("Path to browser '%s' does not exist." % self.path)
def setup(self):
self.tempDir = tempfile.mkdtemp()
self.profileDir = os.path.join(self.tempDir, "profile")
self.browserLog = open(BROWSERLOG_FILE, "w")
def teardown(self):
print "Tearing down %s ..." % self.name
self.process.terminate()
# Waiting up to ten seconds for it to quit
checks = 0
while self.process.poll() is None and checks < 20:
checks += 1
time.sleep(.5)
# If it's still not dead, trying to kill it
if self.process.poll() is None:
print "Process %s is still running. Killing." % self.name
self.process.kill()
self.process.wait()
self.process = None
time.sleep(1)
if self.tempDir is not None and os.path.exists(self.tempDir):
shutil.rmtree(self.tempDir)
self.browserLog.close()
def start(self, url):
raise Exception("Can't start BaseBrowserCommand")
class FirefoxBrowserCommand(BaseBrowserCommand):
def _fixupMacPath(self):
self.path = os.path.join(self.path, "Contents", "MacOS", "firefox-bin")
def setup(self):
super(FirefoxBrowserCommand, self).setup()
shutil.copytree(os.path.join(DOC_ROOT, "test", "resources", "firefox"),
self.profileDir)
def start(self, url):
cmds = [self.path]
if platform.system() == "Darwin":
cmds.append("-foreground")
cmds.extend(["-no-remote", "-profile", self.profileDir, url])
self.process = subprocess.Popen(cmds, stdout = self.browserLog, stderr = self.browserLog)
class ChromeBrowserCommand(BaseBrowserCommand):
def _fixupMacPath(self):
self.path = os.path.join(self.path, "Contents", "MacOS", "Google Chrome")
def start(self, url):
cmds = [self.path]
cmds.extend(["--user-data-dir=%s" % self.profileDir,
"--no-first-run", "--disable-sync", url])
self.process = subprocess.Popen(cmds, stdout = self.browserLog, stderr = self.browserLog)
def makeBrowserCommand(browser):
path = browser["path"].lower()
name = browser["name"]
if name is not None:
name = name.lower()
types = {"firefox": FirefoxBrowserCommand,
"chrome": ChromeBrowserCommand }
command = None
for key in types.keys():
if (name and name.find(key) > -1) or path.find(key) > -1:
command = types[key](browser)
command.name = command.name or key
break
if command is None:
raise Exception("Unrecognized browser: %s" % browser)
return command
def makeBrowserCommands(browserManifestFile):
with open(browserManifestFile) as bmf:
browsers = [makeBrowserCommand(browser) for browser in json.load(bmf)]
return browsers
def getTestBrowsers(options):
testBrowsers = []
if options.browserManifestFile:
testBrowsers = makeBrowserCommands(options.browserManifestFile)
elif options.browser:
testBrowsers = [makeBrowserCommand({"path":options.browser, "name":None})]
if options.browserManifestFile or options.browser:
assert len(testBrowsers) > 0
return testBrowsers
def setUp(options):
if options.masterMode and os.path.isdir(TMPDIR):
print 'Temporary snapshot dir tmp/ is still around.'
print 'tmp/ can be removed if it has nothing you need.'
if options.noPrompts or prompt('SHOULD THIS SCRIPT REMOVE tmp/? THINK CAREFULLY'):
subprocess.call(( 'rm', '-rf', 'tmp' ))
assert not os.path.isdir(TMPDIR)
testBrowsers = getTestBrowsers(options)
with open(options.manifestFile) as mf:
manifestList = json.load(mf)
for b in testBrowsers:
State.taskResults[b.name] = { }
State.remaining[b.name] = len(manifestList)
State.lastPost[b.name] = int(time.time())
for item in manifestList:
id = item['id']
State.manifest[id] = item
taskResults = [ ]
State.taskResults[b.name][id] = taskResults
return testBrowsers
def setUpUnitTests(options):
# Only serve files from a pdf.js clone
assert not GIT_CLONE_CHECK or os.path.isfile('../src/pdf.js') and os.path.isdir('../.git')
testBrowsers = getTestBrowsers(options)
UnitTestState.browsersRunning = len(testBrowsers)
for b in testBrowsers:
UnitTestState.lastPost[b.name] = int(time.time())
return testBrowsers
def startBrowsers(browsers, options, path):
for b in browsers:
b.setup()
print 'Launching', b.name
host = 'http://%s:%s' % (SERVER_HOST, options.port)
qs = '?browser='+ urllib.quote(b.name) +'&manifestFile='+ urllib.quote(options.manifestFile)
if options.bundle:
qs += '&bundle=true'
qs += '&path=' + b.path
b.start(host + path + qs)
def teardownBrowsers(browsers):
for b in browsers:
try:
b.teardown()
except:
print "Error cleaning up after browser at ", b.path
print "Temp dir was ", b.tempDir
print "Error:", sys.exc_info()[0]
def check(task, results, browser, masterMode):
failed = False
for p in xrange(len(results)):
itemResult = results[p]
if itemResult is None:
continue
failure = itemResult.failure
if failure:
failed = True
State.numErrors += 1
print 'TEST-UNEXPECTED-FAIL | test failed', task['id'], '| in', browser, '|', itemResult.item, '|', failure
if failed:
return
kind = task['type']
if 'eq' == kind:
checkEq(task, results, browser, masterMode)
elif 'stas' == kind:
checkStas(task, results, browser)
elif 'sanity' == kind:
checkSanity(task, results, browser)
else:
assert 0 and 'Unknown test type'
def checkEq(task, results, browser, masterMode):
pfx = os.path.join(REFDIR, sys.platform, browser, task['id'])
taskId = task['id']
taskType = task['type']
passed = True
for p in xrange(len(results)):
snapshot = results[p].snapshot
ref = None
eq = True
path = os.path.join(pfx, str(p + 1))
if not os.access(path, os.R_OK):
State.numEqNoSnapshot += 1
if not masterMode:
print 'WARNING: no reference snapshot', path
else:
f = open(path)
ref = f.read()
f.close()
eq = (ref == snapshot)
if not eq:
print 'TEST-UNEXPECTED-FAIL |', taskType, taskId, '| in', browser, '| rendering of snapshot', p + 1, '!= reference rendering'
if not State.eqLog:
State.eqLog = open(EQLOG_FILE, 'w')
eqLog = State.eqLog
# NB: this follows the format of Mozilla reftest
# output so that we can reuse its reftest-analyzer
# script
eqLog.write('REFTEST TEST-UNEXPECTED-FAIL | ' + browser +'-'+ taskId +'-item'+ str(p + 1) + ' | image comparison (==)\n')
eqLog.write('REFTEST IMAGE 1 (TEST): ' + snapshot + '\n')
eqLog.write('REFTEST IMAGE 2 (REFERENCE): ' + ref + '\n')
passed = False
State.numEqFailures += 1
if masterMode and (ref is None or not eq):
tmpTaskDir = os.path.join(TMPDIR, sys.platform, browser, task['id'])
try:
os.makedirs(tmpTaskDir)
except OSError, e:
if e.errno != 17: # file exists
print >>sys.stderr, 'Creating', tmpTaskDir, 'failed!'
of = open(os.path.join(tmpTaskDir, str(p + 1)), 'w')
of.write(snapshot)
of.close()
if passed:
print 'TEST-PASS |', taskType, 'test', task['id'], '| in', browser
def checkStas(task, results, browser):
taskId = task['id']
taskType = task['type']
passed = True
for p in xrange(len(results)):
snapshot = results[p].snapshot
ref = None
eq = True
if snapshot['isDifferent']:
print 'TEST-UNEXPECTED-FAIL |', taskType, taskId, '| in', browser, '| trace of ', p + 1, '!= reference trace'
if not State.traceLog:
State.traceLog = open(TRACELOG_FILE, 'w')
traceLog = State.traceLog
traceLog.write('REFTEST TEST-UNEXPECTED-FAIL | ' + browser +'-'+ taskId +'-item'+ str(p + 1) + ' | ' + results[p].item + ' | trace\n')
traceLog.write(diffData(snapshot['data1'], snapshot['data2']))
passed = False
State.numStasFailures += 1
if passed:
print 'TEST-PASS | stas test', task['id'], '| in', browser
def diffData(testData, refData):
try:
with open("refdata~", "wb") as f1:
f1.write(refData)
with open("testdata~", "wb") as f2:
f2.write(testData)
with open("diffresult~", "wb") as fresult:
process = subprocess.Popen(['diff', '-U', '2', 'refdata~', 'testdata~'], stdout = fresult)
process.wait()
with open("diffresult~", "rb") as fresult:
result = fresult.read()
os.remove("diffresult~")
os.remove("refdata~")
os.remove("testdata~")
return result
except:
return '<<<< reference\n' + refData.encode('utf-8') + '====\n' + testData.encode('utf-8') + '>>>> test\n';
def checkSanity(task, results, browser):
taskId = task['id']
taskType = task['type']
passed = True
for p in xrange(len(results)):
if results[p].failure:
print 'TEST-UNEXPECTED-FAIL |', taskType, taskId, '| in', browser, '| trace of ', p + 1, '!= reference trace'
passed = False
if passed:
print 'TEST-PASS | sanity test', task['id'], '| in', browser
def processResults():
print ''
numFatalFailures = (State.numErrors + State.numStasFailures)
if 0 == State.numEqFailures and 0 == numFatalFailures:
print 'All regression tests passed.'
else:
print 'OHNOES! Some tests failed!'
if 0 < State.numErrors:
print ' errors:', State.numErrors
if 0 < State.numEqFailures:
print ' different ref/snapshot:', State.numEqFailures
if 0 < State.numStasFailures:
print ' failed stas:', State.numStasFailures
def maybeUpdateRefImages(options, browser):
if options.masterMode and (0 < State.numEqFailures or 0 < State.numEqNoSnapshot):
print "Some eq tests failed or didn't have snapshots."
print 'Checking to see if master references can be updated...'
numFatalFailures = State.numErrors
if 0 < numFatalFailures:
print ' No. Some non-eq tests failed.'
else:
print ' Yes! The references in tmp/ can be synced with ref/.'
if options.reftest:
startReftest(browser, options)
if options.noPrompts or prompt('Would you like to update the master copy in ref/?'):
sys.stdout.write(' Updating ref/ ... ')
if not os.path.exists('ref'):
subprocess.check_call('mkdir ref', shell = True)
subprocess.check_call('cp -Rf tmp/* ref/', shell = True)
subprocess.check_call('rm -rf tmp', shell = True)
print 'done'
else:
print ' OK, not updating.'
def startReftest(browser, options):
if options.noPrompts:
return
url = "http://%s:%s" % (SERVER_HOST, options.port)
url += "/test/resources/reftest-analyzer.xhtml"
url += "#web=/test/eq.log"
try:
browser.setup()
browser.start(url)
print "Waiting for browser..."
browser.process.wait()
finally:
teardownBrowsers([browser])
print "Completed reftest usage."
def runTests(options, browsers):
t1 = time.time()
try:
startBrowsers(browsers, options, '/test/test.html')
while not State.done:
for b in State.lastPost:
if State.remaining[b] > 0 and int(time.time()) - State.lastPost[b] > BROWSER_TIMEOUT:
print 'TEST-UNEXPECTED-FAIL | test', State.currentTest[b], 'failed', b, "has not responded in", BROWSER_TIMEOUT, "s"
State.numErrors += State.remaining[b]
State.remaining[b] = 0
checkIfDone()
time.sleep(1)
processResults()
finally:
teardownBrowsers(browsers)
t2 = time.time()
print "Runtime was", int(t2 - t1), "seconds"
if State.eqLog:
State.eqLog.close();
if State.traceLog:
State.traceLog.close();
if options.masterMode:
maybeUpdateRefImages(options, browsers[0])
elif options.reftest and State.numEqFailures > 0:
print "\nStarting reftest harness to examine %d eq test failures." % State.numEqFailures
startReftest(browsers[0], options)
def main():
optionParser = TestOptions()
options, args = optionParser.parse_args()
options = optionParser.verifyOptions(options)
if options == None:
sys.exit(1)
httpd = TestServer((SERVER_HOST, options.port), PDFTestHandler)
httpd.masterMode = options.masterMode
httpd_thread = threading.Thread(target=httpd.serve_forever)
httpd_thread.setDaemon(True)
httpd_thread.start()
browsers = setUp(options)
if len(browsers) > 0:
runTests(options, browsers)
else:
# just run the server
print "Running HTTP server. Press Ctrl-C to quit."
try:
while True:
time.sleep(1)
except (KeyboardInterrupt):
print "\nExiting."
if __name__ == '__main__':
main()

147
test/testutils.js Normal file
Просмотреть файл

@ -0,0 +1,147 @@
/* -*- Mode: js; js-indent-level: 2; indent-tabs-mode: nil; tab-width: 2 -*- */
/* vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: */
/*
* Copyright 2014 Mozilla Foundation
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/*jslint node: true */
'use strict';
var fs = require('fs');
var path = require('path');
exports.removeDirSync = function removeDirSync(dir) {
var files = fs.readdirSync(dir);
files.forEach(function (filename) {
var file = path.join(dir, filename);
var stats = fs.statSync(file);
if (stats.isDirectory()) {
removeDirSync(file);
} else {
fs.unlinkSync(file);
}
});
fs.rmdirSync(dir);
};
exports.copySubtreeSync = function copySubtreeSync(src, dest) {
var files = fs.readdirSync(src);
if (!fs.existsSync(dest)) {
fs.mkdirSync(dest);
}
files.forEach(function (filename) {
var srcFile = path.join(src, filename);
var file = path.join(dest, filename);
var stats = fs.statSync(srcFile);
if (stats.isDirectory()) {
copySubtreeSync(srcFile, file);
} else {
fs.writeFileSync(file, fs.readFileSync(srcFile));
}
});
};
exports.ensureDirSync = function ensureDirSync(dir) {
if (fs.existsSync(dir)) {
return;
}
var parts = dir.split(path.sep), i = parts.length;
while (i > 1 && !fs.existsSync(parts.slice(0, i - 1).join(path.sep))) {
i--;
}
if (i < 0 || (i === 0 && parts[0])) {
throw new Error();
}
while (i <= parts.length) {
fs.mkdirSync(parts.slice(0, i).join(path.sep));
i++;
}
};
var stdinBuffer = '', endOfStdin = false, stdinInitialized = false;
var stdinOnLineCallbacks = [];
function handleStdinBuffer() {
var callback;
if (endOfStdin) {
if (stdinBuffer && stdinOnLineCallbacks.length > 0) {
callback = stdinOnLineCallbacks.shift();
callback(stdinBuffer);
stdinBuffer = null;
}
while (stdinOnLineCallbacks.length > 0) {
callback = stdinOnLineCallbacks.shift();
callback();
}
return;
}
while (stdinOnLineCallbacks.length > 0) {
var i = stdinBuffer.indexOf('\n');
if (i < 0) {
return;
}
callback = stdinOnLineCallbacks.shift();
var result = stdinBuffer.substring(0, i + 1);
stdinBuffer = stdinBuffer.substring(i + 1);
callback(result);
}
// all callbacks handled, stop stdin processing
process.stdin.pause();
}
function initStdin() {
process.stdin.setEncoding('utf8');
process.stdin.on('data', function(chunk) {
stdinBuffer += chunk;
handleStdinBuffer();
});
process.stdin.on('end', function() {
endOfStdin = true;
handleStdinBuffer();
});
}
exports.prompt = function prompt(message, callback) {
if (!stdinInitialized) {
process.stdin.resume();
initStdin();
stdinInitialized = true;
} else if (stdinOnLineCallbacks.length === 0) {
process.stdin.resume();
}
process.stdout.write(message);
stdinOnLineCallbacks.push(callback);
handleStdinBuffer();
};
exports.confirm = function confirm(message, callback) {
exports.prompt(message, function (answer) {
if (answer === undefined) {
callback();
return;
}
if (answer[0].toLowerCase() === 'y') {
callback(true);
} else if (answer[0].toLowerCase() === 'n') {
callback(false);
} else {
confirm(message, callback);
}
});
};

158
test/webbrowser.js Normal file
Просмотреть файл

@ -0,0 +1,158 @@
/* -*- Mode: js; js-indent-level: 2; indent-tabs-mode: nil; tab-width: 2 -*- */
/* vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: */
/*
* Copyright 2014 Mozilla Foundation
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/*jslint node: true */
'use strict';
var os = require('os');
var fs = require('fs');
var path = require('path');
var spawn = require('child_process').spawn;
var testUtils = require('./testutils.js');
var tempDirPrefix = 'pdfjs_';
function WebBrowser(name, path) {
this.name = name;
this.path = path;
this.tmpDir = null;
this.profileDir = null;
this.process = null;
this.finished = false;
this.callback = null;
}
WebBrowser.prototype = {
start: function (url) {
this.tmpDir = path.join(os.tmpdir(), tempDirPrefix + this.name);
if (!fs.existsSync(this.tmpDir)) {
fs.mkdirSync(this.tmpDir);
}
this.process = this.startProcess(url);
},
getProfileDir: function () {
if (!this.profileDir) {
var profileDir = path.join(this.tmpDir, 'profile');
if (fs.existsSync(profileDir)) {
testUtils.removeDirSync(profileDir);
}
fs.mkdirSync(profileDir);
this.profileDir = profileDir;
this.setupProfileDir(profileDir);
}
return this.profileDir;
},
buildArguments: function (url) {
return [url];
},
setupProfileDir: function (dir) {
},
startProcess: function (url) {
var args = this.buildArguments(url);
var proc = spawn(this.path, args);
proc.on('exit', function (code) {
this.finished = true;
this.cleanup(this.callback && this.callback.bind(null, code));
}.bind(this));
return proc;
},
cleanup: function (callback) {
try {
testUtils.removeDirSync(this.tmpDir);
this.process = null;
if (callback) {
callback();
}
} catch (e) {
console.error('Unable to cleanup after the process: ' + e);
try {
if (this.process) {
this.process.kill('SIGKILL');
}
} catch (e) {}
}
},
stop: function (callback) {
if (this.finished) {
if (callback) {
callback();
}
} else {
this.callback = callback;
}
if (this.process) {
this.process.kill('SIGTERM');
}
}
};
var firefoxResourceDir = path.join(__dirname, 'resources', 'firefox');
function FirefoxBrowser(name, path) {
if (os.platform() === 'darwin') {
var m = /([^.\/]+)\.app(\/?)$/.exec(path);
if (m) {
path += (m[2] ? '' : '/') + 'Contents/MacOS/firefox';
}
}
WebBrowser.call(this, name, path);
}
FirefoxBrowser.prototype = Object.create(WebBrowser.prototype);
FirefoxBrowser.prototype.buildArguments = function (url) {
var profileDir = this.getProfileDir();
var args = [];
if (os.platform() === 'darwin') {
args.push('-foreground');
}
args.push('-no-remote', '-profile', profileDir, url);
return args;
};
FirefoxBrowser.prototype.setupProfileDir = function (dir) {
testUtils.copySubtreeSync(firefoxResourceDir, dir);
};
function ChromiumBrowser(name, path) {
if (os.platform() === 'darwin') {
var m = /([^.\/]+)\.app(\/?)$/.exec(path);
if (m) {
path += (m[2] ? '' : '/') + 'Contents/MacOS/' + m[1];
console.log(path);
}
}
WebBrowser.call(this, name, path);
}
ChromiumBrowser.prototype = Object.create(WebBrowser.prototype);
ChromiumBrowser.prototype.buildArguments = function (url) {
var profileDir = this.getProfileDir();
return ['--user-data-dir=' + profileDir,
'--no-first-run', '--disable-sync', url];
};
WebBrowser.create = function (desc) {
var name = desc.name;
if (/firefox/i.test(name)) {
return new FirefoxBrowser(desc.name, desc.path);
}
if (/(chrome|chromium|opera)/i.test(name)) {
return new ChromiumBrowser(desc.name, desc.path);
}
return new WebBrowser(desc.name, desc.path);
};
exports.WebBrowser = WebBrowser;

Просмотреть файл

@ -1,3 +1,5 @@
/* -*- Mode: js; js-indent-level: 2; indent-tabs-mode: nil; tab-width: 2 -*- */
/* vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: */
/*
* Copyright 2014 Mozilla Foundation
*
@ -13,33 +15,40 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/*jslint node: true */
var http = require('http'),
path = require('path'),
fs = require('fs');
'use strict';
var http = require('http');
var path = require('path');
var fs = require('fs');
var mimeTypes = {
'.html': 'text/html',
'.swf': 'application/x-shockwave-flash',
'.txt': 'text/plain',
'.js': 'text/javascript',
'.css': 'text/css',
'.jpg': 'image/jpg',
'.html': 'text/html',
'.js': 'application/javascript',
'.json': 'application/json',
'.svg': 'image/svg+xml',
'.swf': 'application/x-shockwave-flash',
'.xhtml': 'application/xhtml+xml',
'.gif': 'image/gif',
'.ico': 'image/x-icon',
'.png': 'image/png',
'': 'text/plain',
'.md': 'text/plain',
'.as': 'text/plain',
'.ts': 'text/plain',
'.json': 'text/javascript'
'.log': 'text/plain',
'.bcmap': 'application/octet-stream',
'.properties': 'text/plain'
};
var defaultMimeType = 'application/octet-stream';
function WebServer() {
this.root = '.';
this.host = 'localhost';
this.port = 8000;
this.noCache = true;
this.server = null;
this.verbose = false;
this.cacheExpirationTime = 0;
this.disableRangeRequests = false;
this.hooks = {
'GET': [],
'POST': []
@ -49,7 +58,8 @@ WebServer.prototype = {
start: function (callback) {
this.server = http.createServer(this._handler.bind(this));
this.server.listen(this.port, this.host, callback);
console.log('Server running at http://' + this.host + ':' + this.port + '/');
console.log(
'Server running at http://' + this.host + ':' + this.port + '/');
},
stop: function (callback) {
this.server.close(callback);
@ -59,7 +69,7 @@ WebServer.prototype = {
var url = req.url;
var urlParts = /([^?]*)((?:\?(.*))?)/.exec(url);
var pathPart = decodeURI(urlParts[1]), queryPart = urlParts[3];
var noCache = this.noCache;
var verbose = this.verbose;
var methodHooks = this.hooks[req.method];
if (!methodHooks) {
@ -74,6 +84,15 @@ WebServer.prototype = {
return;
}
if (pathPart === '/favicon.ico') {
fs.realpath(path.join(this.root, 'test/resources/favicon.ico'),
checkFile);
return;
}
var disableRangeRequests = this.disableRangeRequests;
var cacheExpirationTime = this.cacheExpirationTime;
var filePath;
fs.realpath(path.join(this.root, pathPart), checkFile);
@ -81,6 +100,9 @@ WebServer.prototype = {
if (err) {
res.writeHead(404);
res.end();
if (verbose) {
console.error(url + ': not found');
}
return;
}
filePath = file;
@ -109,6 +131,30 @@ WebServer.prototype = {
return;
}
var range = req.headers['range'];
if (range && !disableRangeRequests) {
var rangesMatches = /^bytes=(\d+)\-(\d+)?/.exec(range);
if (!rangesMatches) {
res.writeHead(501);
res.end('Bad range', 'utf8');
if (verbose) {
console.error(url + ': bad range: "' + range + '"');
}
return;
}
var start = +rangesMatches[1];
var end = +rangesMatches[2];
if (verbose) {
console.log(url + ': range ' + start + ' - ' + end);
}
serveRequestedFileRange(filePath,
start,
isNaN(end) ? fileSize : (end + 1));
return;
}
if (verbose) {
console.log(url);
}
serveRequestedFile(filePath);
}
@ -116,10 +162,10 @@ WebServer.prototype = {
res.setHeader('Content-Type', 'text/html');
res.writeHead(200);
var content = '';
if (queryPart === 'frame') {
res.end("<html><frameset cols=*,200><frame name=swf>" +
"<frame src='" + encodeURI(pathPart) + "?side'></frameset></html>", 'utf8');
res.end('<html><frameset cols=*,200><frame name=swf>' +
'<frame src=\"' + encodeURI(pathPart) +
'?side\"></frameset></html>', 'utf8');
return;
}
var all = queryPart === 'all';
@ -128,38 +174,41 @@ WebServer.prototype = {
res.end();
return;
}
res.write("<html><body><h1>SWFs of " + pathPart + "</h1>\n");
res.write('<html><body><h1>SWFs of ' + pathPart + '</h1>\n');
if (pathPart !== '/') {
res.write("<a href='..'>..</a><br>\n");
res.write('<a href=\"..\">..</a><br>\n');
}
files.forEach(function (file) {
var stat = fs.statSync(path.join(dir, file));
var item = pathPart + file;
if (stat.isDirectory()) {
res.write("<a href='" + encodeURI(item) + "/'>" + file + "/</a><br>\n");
res.write('<a href=\"' + encodeURI(item) + '\">' +
file + '</a><br>\n');
return;
}
var ext = path.extname(file).toLowerCase();
if (ext === '.swf') {
res.write("<a href='/examples/inspector/inspector.html?rfile=" +
encodeURI(item) + "' target=swf>" +
file + "</a><br>\n");
res.write('<a href=\"/examples/inspector/inspector.html?rfile=' +
encodeURI(item) + '\" target=swf>' +
file + '</a><br>\n');
} else if (all) {
res.write("<a href='" + encodeURI(item) + "'>" + file + "</a><br>\n");
res.write('<a href=\"' + encodeURI(item) + '\">' +
file + '</a><br>\n');
}
});
if (files.length === 0) {
res.write("<p>no files found</p>\n");
res.write('<p>no files found</p>\n');
}
if (!all && queryPart !== 'side') {
res.write("<hr><p>(only SWF files are shown, <a href='?all'>show all</a>)</p>\n");
res.write('<hr><p>(only SWF files are shown, ' +
'<a href=\"?all\">show all</a>)</p>\n');
}
res.end("</body></html>");
res.end('</body></html>');
});
}
function serveRequestedFile(filePath) {
var stream = fs.createReadStream(filePath);
var stream = fs.createReadStream(filePath, {flags: 'rs'});
stream.on('error', function (error) {
res.writeHead(500);
@ -169,19 +218,43 @@ WebServer.prototype = {
var ext = path.extname(filePath).toLowerCase();
var contentType = mimeTypes[ext] || defaultMimeType;
if (!disableRangeRequests) {
res.setHeader('Accept-Ranges', 'bytes');
}
res.setHeader('Content-Type', contentType);
res.setHeader('Content-Length', fileSize);
if (noCache) {
res.setHeader("Cache-Control", "no-cache, no-store, must-revalidate")
res.setHeader("Pragma", "no-cache")
res.setHeader("Expires", "0")
if (cacheExpirationTime > 0) {
var expireTime = new Date();
expireTime.setSeconds(expireTime.getSeconds() + cacheExpirationTime);
res.setHeader('Expires', expireTime.toUTCString());
}
res.writeHead(200);
stream.pipe(res, function (error) {
stream.pipe(res);
}
function serveRequestedFileRange(filePath, start, end) {
var stream = fs.createReadStream(filePath, {
flags: 'rs', start: start, end: end - 1});
stream.on('error', function (error) {
res.writeHead(500);
res.end();
});
var ext = path.extname(filePath).toLowerCase();
var contentType = mimeTypes[ext] || defaultMimeType;
res.setHeader('Accept-Ranges', 'bytes');
res.setHeader('Content-Type', contentType);
res.setHeader('Content-Length', (end - start));
res.setHeader('Content-Range',
'bytes ' + start + '-' + (end - 1) + '/' + fileSize);
res.writeHead(206);
stream.pipe(res);
}
}
};