Source code

Revision control

Copy as Markdown

Other Tools

/*
Copyright (c) 2019 The Khronos Group Inc.
Use of this source code is governed by an MIT-style license that can be
found in the LICENSE.txt file.
*/
OpenGLESTestRunner = (function(){
var wtu = WebGLTestUtils;
var gl;
var HALF_GRID_MAX_SIZE = 32;
var KNOWN_ATTRIBS = [
"gtf_Vertex",
"gtf_Color"
];
var GTFPIXELTOLERANCE = 24;
var GTFACCEPTABLEFAILURECONT = 10;
var GTFAMDPIXELTOLERANCE = 12;
var GTFSCORETOLERANCE = 0.65;
var GTFNCCTOLARANCEZERO = 0.25;
var GTFKERNALSIZE = 5;
function log(msg) {
// debug(msg);
}
function compareImages(refData, tstData, width, height, diff) {
function isPixelSame(offset) {
// First do simple check
if (Math.abs(refData[offset + 0] - tstData[offset + 0]) <= GTFPIXELTOLERANCE &&
Math.abs(refData[offset + 1] - tstData[offset + 1]) <= GTFPIXELTOLERANCE &&
Math.abs(refData[offset + 2] - tstData[offset + 2]) <= GTFPIXELTOLERANCE) {
return true;
}
// TODO: Implement crazy check that's used in OpenGL ES 2.0 conformance tests.
// NOTE: on Desktop things seem to be working. Maybe the more complex check
// is needed for embedded systems?
return false;
}
var same = true;
for (var yy = 0; yy < height; ++yy) {
for (var xx = 0; xx < width; ++xx) {
var offset = (yy * width + xx) * 4;
var diffOffset = ((height - yy - 1) * width + xx) * 4;
diff[diffOffset + 0] = 0;
diff[diffOffset + 1] = 0;
diff[diffOffset + 2] = 0;
diff[diffOffset + 3] = 255;
if (!isPixelSame(offset)) {
diff[diffOffset] = 255;
if (same) {
same = false;
testFailed("pixel @ (" + xx + ", " + yy + " was [" +
tstData[offset + 0] + "," +
tstData[offset + 1] + "," +
tstData[offset + 2] + "," +
tstData[offset + 3] + "] expected [" +
refData[offset + 0] + "," +
refData[offset + 1] + "," +
refData[offset + 2] + "," +
refData[offset + 3] + "]")
}
}
}
}
return same;
}
function persp(fovy, aspect, n, f) {
var dz = f - n;
var rad = fovy / 2.0 * 3.14159265 / 180;
var s = Math.sin(rad);
if (dz == 0 || s == 0 || aspect == 0)
return;
var cot = Math.cos(rad) / s;
return [
cot / aspect,
0.0,
0.0,
0.0,
0.0,
cot,
0.0,
0.0,
0.0,
0.0,
-(f + n) / dz,
-1.0,
0.0,
0.0,
-2.0 * f * n / dz,
0.0
];
}
function setAttribs(attribs, buffers) {
for (var name in attribs) {
var buffer = buffers[name];
if (!buffer) {
testFailed("no buffer for attrib:" + name);
continue;
}
var loc = attribs[name];
log("setup attrib: " + loc + " as " + name);
var buf = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, buf);
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(buffer.data), gl.STATIC_DRAW);
gl.enableVertexAttribArray(loc);
gl.vertexAttribPointer(loc, buffer.numComponents, gl.FLOAT, false, 0, 0);
}
}
function drawSquare(attribs) {
var buffers = {
"gtf_Vertex": {
data: [
1.0, -1.0, -2.0,
1.0, 1.0, -2.0,
-1.0, -1.0, -2.0,
-1.0, 1.0, -2.0
],
numComponents: 3
},
"gtf_Color": {
data: [
0.5, 1.0, 0.0,
0.0, 1.0, 1.0,
1.0, 0.0, 0.0,
0.5, 0.0, 1.0
],
numComponents: 3,
},
"gtf_SecondaryColor": {
data: [
0.5, 0.0, 1.0,
1.0, 0.0, 0.0,
0.0, 1.0, 1.0,
0.5, 1.0, 0.0
],
numComponents: 3,
},
"gtf_Normal": {
data: [
0.5, 0.0, 1.0,
1.0, 0.0, 0.0,
0.0, 1.0, 1.0,
0.5, 1.0, 0.0
],
numComponents: 3,
},
"gtf_MultiTexCoord0": {
data: [
1.0, 0.0,
1.0, 1.0,
0.0, 0.0,
0.0, 1.0
],
numComponents: 2,
},
"gtf_FogCoord": {
data: [
0.0,
1.0,
0.0,
1.0
],
numComponents: 1,
}
};
setAttribs(attribs, buffers);
gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);
}
function drawFrontBackSquare(attribs) {
var front = {
"gtf_Vertex": {
data: [
1.0, -1.0, -2.0,
1.0, 0.0, -2.0,
-1.0, -1.0, -2.0,
-1.0, 0.0, -2.0
],
numComponents: 3
},
"gtf_Color": {
data: [
0.0, 1.0, 0.0,
0.0, 1.0, 0.0,
0.0, 1.0, 0.0,
0.0, 1.0, 0.0
],
numComponents: 3,
},
"gtf_MultiTexCoord0": {
data: [
1.0, 0.0,
1.0, 0.5,
0.0, 0.0,
0.0, 0.5
],
numComponents: 2,
}
};
setAttribs(attribs, front);
gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);
var back = {
"gtf_Vertex": {
data: [
1.0, 1.0, -2.0,
1.0, 0.0, -2.0,
-1.0, 1.0, -2.0,
-1.0, 0.0, -2.0
],
numComponents: 3
},
"gtf_Color": {
data: [
1.0, 0.0, 0.0,
1.0, 0.0, 0.0,
1.0, 0.0, 0.0,
1.0, 0.0, 0.0
],
numComponents: 3,
},
"gtf_MultiTexCoord0": {
data: [
1.0, 0.1,
1.0, 0.5,
0.0, 0.1,
0.0, 0.5
],
numComponents: 2,
}
};
setAttribs(attribs, back);
gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);
}
function drawGrid(attribs, width, height) {
var n = Math.min(Math.floor(Math.max(width, height) / 4), HALF_GRID_MAX_SIZE);
var numVertices = (n + n) * (n + n) * 6;
var gridVertices = [];
var gridColors = [];
var gridSecColors = [];
var gridNormals = [];
var gridFogCoords = [];
var gridTexCoords0 = [];
var currentVertex = 0;
var currentColor = 0;
var currentSecColor = 0;
var currentTexCoord0 = 0;
var currentNormal = 0;
var currentFogCoord = 0;
var z = -2.0;
for(var i = -n; i < n; ++i)
{
var x1 = i / n;
var x2 = (i + 1) / n;
for(var j = -n; j < n; ++j)
{
var y1 = j / n;
var y2 = (j + 1) / n;
// VERTEX 0
gridVertices[currentVertex++] = x1;
gridVertices[currentVertex++] = y1;
gridVertices[currentVertex++] = z;
gridColors[currentColor++] = 1.0 - (x1 + y1 + 2.0) / 4.0;
gridColors[currentColor++] = (x1 + 1.0) / 2.0;
gridColors[currentColor++] = (y1 + 1.0) / 2.0;
gridSecColors[currentSecColor++] = 1.0 - (x2 + y2 + 2.0) / 4.0;
gridSecColors[currentSecColor++] = (x2 + 1.0) / 2.0;
gridSecColors[currentSecColor++] = (y2 + 1.0) / 2.0;
gridTexCoords0[currentTexCoord0++] = (x1 + 1.0) / 2.0;
gridTexCoords0[currentTexCoord0++] = (y1 + 1.0) / 2.0;
gridNormals[currentNormal++] = 1.0 - (x2 + y2 + 2.0) / 4.0;
gridNormals[currentNormal++] = (x2 + 1.0) / 2.0;
gridNormals[currentNormal++] = (y2 + 1.0) / 2.0;
gridFogCoords[currentFogCoord++] = (y1 + 1.0) / 2.0;
// VERTEX 1
gridVertices[currentVertex++] = x2;
gridVertices[currentVertex++] = y1;
gridVertices[currentVertex++] = z;
gridColors[currentColor++] = 1.0 - (x2 + y1 + 2.0) / 4.0;
gridColors[currentColor++] = (x2 + 1.0) / 2.0;
gridColors[currentColor++] = (y1 + 1.0) / 2.0;
gridSecColors[currentSecColor++] = 1.0 - (x1 + y2 + 2.0) / 4.0;
gridSecColors[currentSecColor++] = (x1 + 1.0) / 2.0;
gridSecColors[currentSecColor++] = (y2 + 1.0) / 2.0;
gridTexCoords0[currentTexCoord0++] = (x2 + 1.0) / 2.0;
gridTexCoords0[currentTexCoord0++] = (y1 + 1.0) / 2.0;
gridNormals[currentNormal++] = 1.0 - (x1 + y2 + 2.0) / 4.0;
gridNormals[currentNormal++] = (x1 + 1.0) / 2.0;
gridNormals[currentNormal++] = (y2 + 1.0) / 2.0;
gridFogCoords[currentFogCoord++] = (y1 + 1.0) / 2.0;
// VERTEX 2
gridVertices[currentVertex++] = x2;
gridVertices[currentVertex++] = y2;
gridVertices[currentVertex++] = z;
gridColors[currentColor++] = 1.0 - (x2 + y2 + 2.0) / 4.0;
gridColors[currentColor++] = (x2 + 1.0) / 2.0;
gridColors[currentColor++] = (y2 + 1.0) / 2.0;
gridSecColors[currentSecColor++] = 1.0 - (x1 + y1 + 2.0) / 4.0;
gridSecColors[currentSecColor++] = (x1 + 1.0) / 2.0;
gridSecColors[currentSecColor++] = (y1 + 1.0) / 2.0;
gridTexCoords0[currentTexCoord0++] = (x2 + 1.0) / 2.0;
gridTexCoords0[currentTexCoord0++] = (y2 + 1.0) / 2.0;
gridNormals[currentNormal++] = 1.0 - (x1 + y1 + 2.0) / 4.0;
gridNormals[currentNormal++] = (x1 + 1.0) / 2.0;
gridNormals[currentNormal++] = (y1 + 1.0) / 2.0;
gridFogCoords[currentFogCoord++] = (y2 + 1.0) / 2.0;
// VERTEX 2
gridVertices[currentVertex++] = x2;
gridVertices[currentVertex++] = y2;
gridVertices[currentVertex++] = z;
gridColors[currentColor++] = 1.0 - (x2 + y2 + 2.0) / 4.0;
gridColors[currentColor++] = (x2 + 1.0) / 2.0;
gridColors[currentColor++] = (y2 + 1.0) / 2.0;
gridSecColors[currentSecColor++] = 1.0 - (x1 + y1 + 2.0) / 4.0;
gridSecColors[currentSecColor++] = (x1 + 1.0) / 2.0;
gridSecColors[currentSecColor++] = (y1 + 1.0) / 2.0;
gridTexCoords0[currentTexCoord0++] = (x2 + 1.0) / 2.0;
gridTexCoords0[currentTexCoord0++] = (y2 + 1.0) / 2.0;
gridNormals[currentNormal++] = 1.0 - (x1 + y1 + 2.0) / 4.0;
gridNormals[currentNormal++] = (x1 + 1.0) / 2.0;
gridNormals[currentNormal++] = (y1 + 1.0) / 2.0;
gridFogCoords[currentFogCoord++] = (y2 + 1.0) / 2.0;
// VERTEX 3
gridVertices[currentVertex++] = x1;
gridVertices[currentVertex++] = y2;
gridVertices[currentVertex++] = z;
gridColors[currentColor++] = 1.0 - (x1 + y2 + 2.0) / 4.0;
gridColors[currentColor++] = (x1 + 1.0) / 2.0;
gridColors[currentColor++] = (y2 + 1.0) / 2.0;
gridSecColors[currentSecColor++] = 1.0 - (x2 + y1 + 2.0) / 4.0;
gridSecColors[currentSecColor++] = (x2 + 1.0) / 2.0;
gridSecColors[currentSecColor++] = (y1 + 1.0) / 2.0;
gridTexCoords0[currentTexCoord0++] = (x1 + 1.0) / 2.0;
gridTexCoords0[currentTexCoord0++] = (y2 + 1.0) / 2.0;
gridNormals[currentNormal++] = 1.0 - (x2 + y1 + 2.0) / 4.0;
gridNormals[currentNormal++] = (x2 + 1.0) / 2.0;
gridNormals[currentNormal++] = (y1 + 1.0) / 2.0;
gridFogCoords[currentFogCoord++] = (y2 + 1.0) / 2.0;
// VERTEX 0
gridVertices[currentVertex++] = x1;
gridVertices[currentVertex++] = y1;
gridVertices[currentVertex++] = z;
gridColors[currentColor++] = 1.0 - (x1 + y1 + 2.0) / 4.0;
gridColors[currentColor++] = (x1 + 1.0) / 2.0;
gridColors[currentColor++] = (y1 + 1.0) / 2.0;
gridSecColors[currentSecColor++] = 1.0 - (x2 + y2 + 2.0) / 4.0;
gridSecColors[currentSecColor++] = (x2 + 1.0) / 2.0;
gridSecColors[currentSecColor++] = (y2 + 1.0) / 2.0;
gridTexCoords0[currentTexCoord0++] = (x1 + 1.0) / 2.0;
gridTexCoords0[currentTexCoord0++] = (y1 + 1.0) / 2.0;
gridNormals[currentNormal++] = 1.0 - (x2 + y2 + 2.0) / 4.0;
gridNormals[currentNormal++] = (x2 + 1.0) / 2.0;
gridNormals[currentNormal++] = (y2 + 1.0) / 2.0;
gridFogCoords[currentFogCoord++] = (y1 + 1.0) / 2.0;
}
}
var buffers = {
"gtf_Vertex": { data: gridVertices, numComponents: 3 },
"gtf_Color": { data: gridColors, numComponents: 3 },
"gtf_SecondaryColor": { data: gridSecColors, numComponents: 3 },
"gtf_Normal": { data: gridNormals, numComponents: 3 },
"gtf_FogCoord": { data: gridFogCoords, numComponents: 1 },
"gtf_MultiTexCoord0": { data: gridTexCoords0, numComponents: 2 }
};
setAttribs(attribs, buffers);
gl.drawArrays(gl.TRIANGLES, 0, numVertices);
}
var MODEL_FUNCS = {
square: drawSquare,
frontbacksquare: drawFrontBackSquare,
grid: drawGrid
};
function drawWithProgram(program, programInfo, test) {
gl.useProgram(program);
var attribs = { };
var numAttribs = gl.getProgramParameter(program, gl.ACTIVE_ATTRIBUTES);
for (var ii = 0; ii < numAttribs; ++ii) {
var info = gl.getActiveAttrib(program, ii);
var name = info.name;
var location = gl.getAttribLocation(program, name);
attribs[name] = location;
if (KNOWN_ATTRIBS.indexOf(name) < 0) {
testFailed("unknown attrib:" + name)
}
}
var uniforms = { };
var numUniforms = gl.getProgramParameter(program, gl.ACTIVE_UNIFORMS);
for (var ii = 0; ii < numUniforms; ++ii) {
var info = gl.getActiveUniform(program, ii);
var name = info.name;
if (name.match(/\[0\]$/)) {
name = name.substr(0, name.length - 3);
}
var location = gl.getUniformLocation(program, name);
uniforms[name] = {location: location};
}
var getUniformLocation = function(name) {
var uniform = uniforms[name];
if (uniform) {
uniform.used = true;
return uniform.location;
}
return null;
}
// Set known uniforms
var loc = getUniformLocation("gtf_ModelViewProjectionMatrix");
if (loc) {
gl.uniformMatrix4fv(
loc,
false,
persp(60, 1, 1, 30));
}
var loc = getUniformLocation("viewportwidth");
if (loc) {
gl.uniform1f(loc, gl.canvas.width);
}
var loc = getUniformLocation("viewportheight");
if (loc) {
gl.uniform1f(loc, gl.canvas.height);
}
// Set test specific uniforms
for (var name in programInfo.uniforms) {
var location = getUniformLocation(name);
if (!location) {
continue;
}
var uniform = programInfo.uniforms[name];
var type = uniform.type;
var value = uniform.value;
var transpose = uniform.transpose;
if (transpose !== undefined) {
log("gl." + type + '("' + name + '", ' + transpose + ", " + value + ")");
gl[type](location, transpose, value);
} else if (!type.match("v$")) {
var args = [location];
for (var ii = 0; ii < value.length; ++ii) {
args.push(value[ii]);
}
gl[type].apply(gl, args);
log("gl." + type + '("' + name + '", ' + args.slice(1) + ")");
} else {
log("gl." + type + '("' + name + '", ' + value + ")");
gl[type](location, value);
}
var err = gl.getError();
if (err != gl.NO_ERROR) {
testFailed(wtu.glEnumToString(gl, err) + " generated setting uniform: " + name);
}
}
// Filter out specified built-in uniforms
if (programInfo.builtin_uniforms) {
var num_builtins_found = 0;
var valid_values = programInfo.builtin_uniforms.valid_values;
for (var index in valid_values) {
var uniform = uniforms[valid_values[index]];
if (uniform) {
++num_builtins_found;
uniform.builtin = true;
}
}
var min_required = programInfo.builtin_uniforms.min_required;
if (num_builtins_found < min_required) {
testFailed("only found " + num_builtins_found + " of " + min_required +
" required built-in uniforms: " + valid_values);
}
}
// Check for unset uniforms
for (var name in uniforms) {
var uniform = uniforms[name];
if (!uniform.used && !uniform.builtin) {
testFailed("uniform " + name + " never set");
}
}
for (var state in test.state) {
var fields = test.state[state];
switch (state) {
case 'depthrange':
gl.depthRange(fields.near, fields.far);
break;
default:
testFailed("unknown state: " + state)
}
}
gl.clearColor(0, 0, 0, 0);
gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
var model = test.model || "square";
var fn = MODEL_FUNCS[model];
if (!fn) {
testFailed("unknown model type: " + model)
} else {
log("draw as: " + model)
fn(attribs, gl.canvas.width, gl.canvas.height);
}
var pixels = new Uint8Array(gl.canvas.width * gl.canvas.height * 4);
gl.readPixels(0, 0, gl.canvas.width, gl.canvas.height, gl.RGBA, gl.UNSIGNED_BYTE, pixels);
return {
width: gl.canvas.width,
height: gl.canvas.height,
pixels: pixels,
img: wtu.makeImageFromCanvas(gl.canvas)
};
}
function runProgram(programInfo, test, label, callback) {
var shaders = [];
var source = [];
var count = 0;
function loadShader(path, type, index) {
wtu.loadTextFileAsync(path, function(success, text) {
addShader(success, text, type, path, index);
});
}
function addShader(success, text, type, path, index) {
++count;
if (!success) {
testFailed("could not load: " + path);
} else {
var shader = wtu.loadShader(gl, text, type);
shaders.push(shader);
source[index] = text;
}
if (count == 2) {
var result;
if (shaders.length == 2) {
debug("");
if (!quietMode()) {
var consoleDiv = document.getElementById("console");
wtu.addShaderSources(
gl, consoleDiv, label + " vertex shader", shaders[0], source[0],
programInfo.vertexShader);
wtu.addShaderSources(
gl, consoleDiv, label + " fragment shader", shaders[1], source[1],
programInfo.fragmentShader);
}
var program = wtu.createProgram(gl, shaders[0], shaders[1]);
result = drawWithProgram(program, programInfo, test);
}
callback(result);
}
}
loadShader(programInfo.vertexShader, gl.VERTEX_SHADER, 0);
loadShader(programInfo.fragmentShader, gl.FRAGMENT_SHADER, 1);
}
function compareResults(expected, actual) {
var width = expected.width;
var height = expected.height;
var canvas = document.createElement("canvas");
canvas.width = width;
canvas.height = height;
var ctx = canvas.getContext("2d");
var imgData = ctx.getImageData(0, 0, width, height);
var tolerance = 0;
var expData = expected.pixels;
var actData = actual.pixels;
var same = compareImages(expData, actData, width, height, imgData.data);
var console = document.getElementById("console");
var diffImg = null;
if (!same) {
ctx.putImageData(imgData, 0, 0);
diffImg = wtu.makeImageFromCanvas(canvas);
}
if (!quietMode()) {
var div = document.createElement("div");
div.className = "testimages";
wtu.insertImage(div, "reference", expected.img);
wtu.insertImage(div, "test", actual.img);
if (diffImg) {
wtu.insertImage(div, "diff", diffImg);
}
div.appendChild(document.createElement('br'));
console.appendChild(div);
}
if (!same) {
testFailed("images are different");
} else {
testPassed("images are the same");
}
if (!quietMode())
console.appendChild(document.createElement('hr'));
}
function runCompareTest(test, callback) {
debug("");
debug("test: " + test.name);
var results = [];
var count = 0;
function storeResults(index) {
return function(result) {
results[index] = result;
++count;
if (count == 2) {
compareResults(results[0], results[1]);
wtu.glErrorShouldBe(gl, gl.NO_ERROR, "there should be no errors");
callback();
}
}
}
runProgram(test.referenceProgram, test, "reference", storeResults(0));
runProgram(test.testProgram, test, "test", storeResults(1));
}
function runBuildTest(test, callback) {
debug("");
debug("test: " + test.name);
var shaders = [null, null];
var source = ["",""];
var success = [undefined, undefined];
var count = 0;
function loadShader(path, type, index) {
if (path == "empty") {
shaders[index] = gl.createShader();
success[index] = true;
source[index] = "/* empty */";
attachAndLink();
} else {
wtu.loadTextFileAsync(path, function(loadSuccess, text) {
if (!loadSuccess) {
success[index] = false;
source[index] = "/* could not load */";
testFailed("could not load:" + path);
} else {
source[index] = text;
shaders[index] = wtu.loadShader(gl, text, type, function(index) {
return function(msg) {
success[index] = false
}
}(index));
if (success[index] === undefined) {
success[index] = true;
}
}
attachAndLink();
});
}
}
function attachAndLink() {
++count;
if (count == 2) {
if (!quietMode()) {
debug("");
var c = document.getElementById("console");
wtu.addShaderSource(
c, "vertex shader", source[0], test.testProgram.vertexShader);
debug("compile: " + (success[0] ? "success" : "fail"));
wtu.addShaderSource(
c, "fragment shader", source[1], test.testProgram.fragmentShader);
debug("compile: " + (success[1] ? "success" : "fail"));
}
compileSuccess = (success[0] && success[1]);
if (!test.compstat) {
if (compileSuccess) {
testFailed("expected compile failure but was successful");
} else {
testPassed("expected compile failure and it failed");
}
} else {
if (compileSuccess) {
testPassed("expected compile success and it was successful");
} else {
testFailed("expected compile success but it failed");
}
var linkSuccess = true;
var program = wtu.createProgram(gl, shaders[0], shaders[1], function() {
linkSuccess = false;
});
if (linkSuccess !== test.linkstat) {
testFailed("expected link to " + (test.linkstat ? "succeed" : "fail"));
} else {
testPassed("shaders compiled and linked as expected.");
}
}
callback();
}
}
loadShader(test.testProgram.vertexShader, gl.VERTEX_SHADER, 0);
loadShader(test.testProgram.fragmentShader, gl.FRAGMENT_SHADER, 1);
}
var testPatterns = {
compare: runCompareTest,
build: runBuildTest,
dummy: null // just here to mark the end
};
function LogGLCall(functionName, args) {
console.log("gl." + functionName + "(" +
WebGLDebugUtils.glFunctionArgsToString(functionName, args) + ")");
}
// Runs the tests async since they will load shaders.
function run(obj) {
description();
var canvas = document.getElementById("example");
gl = wtu.create3DContext(canvas);
if (window.WebGLDebugUtils) {
gl = WebGLDebugUtils.makeDebugContext(gl, undefined, LogGLCall);
}
if (!gl) {
testFailed("context does not exist");
finishTest();
return;
}
if (gl.canvas.width != 500 || gl.canvas.height != 500) {
testFailed("canvas must be 500x500 pixels: Several shaders are hard coded to this size.");
}
var tests = obj.tests;
var ndx = 0;
function runNextTest() {
if (ndx < tests.length) {
var test = tests[ndx++];
var fn = testPatterns[test.pattern];
if (!fn) {
testFailed("test pattern: " + test.pattern + " not supoprted")
runNextTest();
} else {
fn(test, runNextTest);
}
} else {
finishTest();
}
}
runNextTest();
}
return {
run: run,
};
}());