Skip to content

Commit 4ed685e

Browse files
Tests: Add test pass helper / review tool
Running `bin/testpass` will run the passed tests (or the "important" ones) and build a manifest.txt file of the generated ZIP files. It will copy in an HTML/JS single page app into the results and will then launch a Python HTTP server and a browser pointing at the pp. The app lets you scroll up and down through the tests and see the output, and review any snaps generated by the tests. Very very rough, but enough to allow kicking off a multi-hour test pass, then review the results later.
1 parent c25480a commit 4ed685e

3 files changed

Lines changed: 239 additions & 0 deletions

File tree

bin/testpass

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
#!/usr/bin/env bash
2+
3+
cd "$(dirname "$0")/.."
4+
source "bin/util.sh"
5+
6+
START="$(date)"
7+
8+
if [ $# -gt 0 ]; then
9+
FILES="$@"
10+
else
11+
FILES="\
12+
tests/screenshots/*.lua \
13+
tests/configs/*.lua \
14+
tests/launcher/*.lua \
15+
tests/desktop/*.lua \
16+
tests/disk_copy/*.lua \
17+
tests/selector/*.lua \
18+
tests/desk_acc/*.lua \
19+
tests/*.lua"
20+
fi
21+
22+
TMPDIR="/tmp/mametest"
23+
results="$TMPDIR/results"
24+
mkdir -p "$results"
25+
cp "res/testutil/index.html" "$results/index.html"
26+
cp "res/testutil/index.js" "$results/index.js"
27+
MANIFEST="$results/manifest.txt"
28+
rm -f "$MANIFEST"
29+
30+
for i in $FILES
31+
do
32+
cecho yellow "==$i=="
33+
34+
SCRIPT="$(realpath "$1")"
35+
SCRIPTDIR="$(dirname "$SCRIPT")"
36+
37+
bin/mametest --nosnaps "$SCRIPT"
38+
39+
relpath="${SCRIPT#$(pwd)/tests/}"
40+
resdir="$(dirname "$relpath")"
41+
zipfile="${resdir}/$(basename "$i" .lua).zip"
42+
43+
echo "$zipfile" >> "$MANIFEST"
44+
done
45+
46+
echo "Start: $START"
47+
echo "End: $(date)"
48+
49+
echo ""
50+
cecho green "Serving results at http://localhost:8000 - Ctrl+C to stop"
51+
echo ""
52+
53+
# Serve the results. We must enable job control so the server can be
54+
# started in the background, then we open a browser, then we
55+
# foreground the server so it can be terminated with Ctrl+C.
56+
set -m
57+
python3 -m http.server -d "$results" 8000 2> /dev/null &
58+
open http://localhost:8000
59+
fg > /dev/null

res/testutil/index.html

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
<!DOCTYPE html>
2+
<meta charset="utf8">
3+
<title>Test Results Viewer</title>
4+
<script src="https://unpkg.com/unzipit@1.4.0/dist/unzipit.js"></script>
5+
<script src="index.js"></script>
6+
<style>
7+
html { width: 100%; height: 100%; margin: 0; padding: 0; background-color: black; }
8+
body {
9+
width: 100%; height: 100%; margin: 0;
10+
background-color: #404040;
11+
box-sizing: border-box;
12+
display: grid;
13+
grid-template-columns: 200px 580px auto;
14+
grid-template-rows: 384px auto;
15+
gap: 20px;
16+
padding: 20px;
17+
}
18+
19+
option.test { font-weight: bold; }
20+
option.snap { font-style: italic; margin-left: 25px;}
21+
22+
#list { grid-row: 1 / 3; grid-column: 1; }
23+
#snap { grid-row: 1; grid-column: 2; }
24+
#instructions { grid-row: 1; grid-column: 3; }
25+
#output { grid-row: 2; grid-column: 2/4; }
26+
27+
#snap { height: 100%; }
28+
29+
:root { color-scheme: light dark; }
30+
31+
#instructions {
32+
font-family: sans-serif;
33+
font-size: 12pt;
34+
color: #e0e0e0;
35+
36+
display: flex; align-items: flex-end;
37+
}
38+
39+
#output {
40+
padding: 10px;
41+
overflow-y: auto;
42+
color: light-dark(black, white);
43+
background-color: light-dark(white, black);
44+
font-family: monospace;
45+
font-size: 10pt;
46+
white-space: pre-wrap;
47+
}
48+
49+
</style>
50+
<body>
51+
<select id="list" size=2></select>
52+
<img id="snap" src="">
53+
<div id="output"></div>
54+
<div id="instructions"></div>
55+
</body>

res/testutil/index.js

Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
const $ = s => document.querySelector(s);
2+
const $$ = s => [...document.querySelectorAll(s)];
3+
4+
function order(a, b) {
5+
return (a > b) ? 1 : (a < b) ? -1 : 0;
6+
}
7+
8+
window.addEventListener('DOMContentLoaded', async e => {
9+
const list = $('#list');
10+
11+
// Iterate manifest
12+
const manifest = await (await fetch('manifest.txt')).text();
13+
for (const path of manifest.split(/\n/).filter(s => s)) {
14+
const option = document.createElement('option');
15+
option.className = 'test';
16+
option.innerText = path;
17+
list.append(option);
18+
19+
// Load and iterate ZIP
20+
const blob = await (await fetch(path)).arrayBuffer();
21+
const {entries} = await unzipit.unzip(blob);
22+
23+
// Stash properties on list item
24+
option.output = await entries['output.txt'].text();
25+
option.entries = entries;
26+
27+
Object.keys(entries)
28+
.filter(k => k.endsWith('.png'))
29+
.sort((a, b) => order(a.name,b.name))
30+
.forEach(key => {
31+
const entry = entries[key];
32+
33+
const subopt = document.createElement('option');
34+
subopt.className = 'snap';
35+
subopt.innerText = entry.name;
36+
subopt.parent = option;
37+
subopt.instructions = entry.name
38+
.replace(/^\d\d\d\d - /, '')
39+
.replace(/\.png$/, '');
40+
list.append(subopt);
41+
42+
subopt.entry = entry;
43+
});
44+
}
45+
46+
list.addEventListener('change', async e => {
47+
if (list.selectedOptions.length === 0)
48+
return;
49+
const option = list.selectedOptions[0];
50+
review(option);
51+
});
52+
53+
list.focus();
54+
55+
});
56+
57+
function blobToDataURL(blob) {
58+
return new Promise((resolve, reject) => {
59+
const reader = new FileReader();
60+
reader.readAsDataURL(blob);
61+
reader.onload = e => resolve(reader.result);
62+
reader.onerror = e => reject(reader.error);
63+
});
64+
}
65+
66+
async function review(option) {
67+
if (option.output) {
68+
$('#output').innerHTML = ansiToHTML(option.output);
69+
$('#snap').src = '';
70+
$('#instructions').innerText = '';
71+
} else if (option.entry) {
72+
const blob = await option.entry.blob('image/png');
73+
const url = await blobToDataURL(blob);
74+
$('#snap').src = url;
75+
$('#output').value = option.parent.output;
76+
$('#instructions').innerText = option.instructions;
77+
}
78+
}
79+
80+
function ansiToHTML(text) {
81+
// HTML escaping
82+
text = text.replace(/[&<>"']/g, c => {
83+
switch (c) {
84+
case '&': return '&amp;';
85+
case '<': return '&lt;';
86+
case '>': return '&gt;';
87+
default: return c;
88+
}
89+
});
90+
91+
// ANSI sequences
92+
// NOTE: Assumes it's always a pair
93+
94+
95+
const styles = {
96+
30: 'color: black',
97+
31: 'color: red',
98+
32: 'color: green',
99+
33: 'color: yellow',
100+
34: 'color: blue',
101+
35: 'color: magenta',
102+
36: 'color: cyan',
103+
37: 'color: white',
104+
105+
40: 'background-color: black',
106+
41: 'background-color: red',
107+
42: 'background-color: green',
108+
43: 'background-color: yellow',
109+
44: 'background-color: blue',
110+
45: 'background-color: magenta',
111+
46: 'background-color: cyan',
112+
47: 'background-color: white',
113+
};
114+
115+
text = text
116+
.replace(/\x1B\(B/g, '')
117+
.replace(/\x1B\[(\d+)m/g, function(_, n) {
118+
return '<span style="' + styles[n] + '">';
119+
})
120+
.replace(/\x1B\[m/g, '</span>');
121+
122+
123+
return text;
124+
125+
}

0 commit comments

Comments
 (0)