Test runner was not properly handling tests which return the control back to browser event loop. It was treating such tests as if they exited with code 0, marking them as succesfull even if they were eventually failing or hanging. This commit adds a callback to TestCase so the runner is notified when a test truly has finished. As a side effect, two tests need to be disabled for now as they are failing for wasm, which was not properly detected previously. Change-Id: I0eb9383e5bb9cd660431c18747b9e94413629d1e Reviewed-by: Morten Johan Sørvig <morten.sorvig@qt.io> Reviewed-by: Qt CI Bot <qt_ci_bot@qt-project.org>
232 lines
7.1 KiB
JavaScript
232 lines
7.1 KiB
JavaScript
// Copyright (C) 2022 The Qt Company Ltd.
|
|
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only
|
|
|
|
// Exposes platform capabilities as static properties
|
|
|
|
export class AbortedError extends Error {
|
|
constructor(stdout) {
|
|
super(`The program has been aborted`)
|
|
|
|
this.stdout = stdout;
|
|
}
|
|
}
|
|
export class Platform {
|
|
static #webAssemblySupported = typeof WebAssembly !== 'undefined';
|
|
|
|
static #canCompileStreaming = WebAssembly.compileStreaming !== 'undefined';
|
|
|
|
static #webGLSupported = (() => {
|
|
// We expect that WebGL is supported if WebAssembly is; however
|
|
// the GPU may be blacklisted.
|
|
try {
|
|
const canvas = document.createElement('canvas');
|
|
return !!(
|
|
window.WebGLRenderingContext &&
|
|
(canvas.getContext('webgl') || canvas.getContext('experimental-webgl'))
|
|
);
|
|
} catch (e) {
|
|
return false;
|
|
}
|
|
})();
|
|
|
|
static #canLoadQt = Platform.#webAssemblySupported && Platform.#webGLSupported;
|
|
|
|
static get webAssemblySupported() {
|
|
return this.#webAssemblySupported;
|
|
}
|
|
static get canCompileStreaming() {
|
|
return this.#canCompileStreaming;
|
|
}
|
|
static get webGLSupported() {
|
|
return this.#webGLSupported;
|
|
}
|
|
static get canLoadQt() {
|
|
return this.#canLoadQt;
|
|
}
|
|
}
|
|
|
|
// Locates a resource, based on its relative path
|
|
export class ResourceLocator {
|
|
#rootPath;
|
|
|
|
constructor(rootPath) {
|
|
this.#rootPath = rootPath;
|
|
if (rootPath.length > 0 && !rootPath.endsWith('/')) rootPath += '/';
|
|
}
|
|
|
|
locate(relativePath) {
|
|
return this.#rootPath + relativePath;
|
|
}
|
|
}
|
|
|
|
// Allows fetching of resources, such as text resources or wasm modules.
|
|
export class ResourceFetcher {
|
|
#locator;
|
|
|
|
constructor(locator) {
|
|
this.#locator = locator;
|
|
}
|
|
|
|
async fetchText(filePath) {
|
|
return (await this.#fetchRawResource(filePath)).text();
|
|
}
|
|
|
|
async fetchCompileWasm(filePath, onFetched) {
|
|
const fetchResponse = await this.#fetchRawResource(filePath);
|
|
onFetched?.();
|
|
|
|
if (Platform.canCompileStreaming) {
|
|
try {
|
|
return await WebAssembly.compileStreaming(fetchResponse);
|
|
} catch {
|
|
// NOOP - fallback to sequential fetching below
|
|
}
|
|
}
|
|
return WebAssembly.compile(await fetchResponse.arrayBuffer());
|
|
}
|
|
|
|
async #fetchRawResource(filePath) {
|
|
const response = await fetch(this.#locator.locate(filePath));
|
|
if (!response.ok)
|
|
throw new Error(
|
|
`${response.status} ${response.statusText} ${response.url}`
|
|
);
|
|
return response;
|
|
}
|
|
}
|
|
|
|
// Represents a WASM module, wrapping the instantiation and execution thereof.
|
|
export class CompiledModule {
|
|
#createQtAppInstanceFn;
|
|
#js;
|
|
#wasm;
|
|
#resourceLocator;
|
|
|
|
constructor(createQtAppInstanceFn, js, wasm, resourceLocator) {
|
|
this.#createQtAppInstanceFn = createQtAppInstanceFn;
|
|
this.#js = js;
|
|
this.#wasm = wasm;
|
|
this.#resourceLocator = resourceLocator;
|
|
}
|
|
|
|
static make(js, wasm, entryFunctionName, resourceLocator)
|
|
{
|
|
const exports = {};
|
|
const module = {};
|
|
eval(js);
|
|
if (!module.exports) {
|
|
throw new Error(
|
|
'${entryFunctionName} has not been exported by the main script'
|
|
);
|
|
}
|
|
|
|
return new CompiledModule(
|
|
module.exports, js, wasm, resourceLocator
|
|
);
|
|
}
|
|
|
|
async exec(parameters) {
|
|
return await new Promise(async (resolve, reject) => {
|
|
let instance = undefined;
|
|
let result = undefined;
|
|
|
|
let testFinished = false;
|
|
const testFinishedEvent = new CustomEvent('testFinished');
|
|
instance = await this.#createQtAppInstanceFn((() => {
|
|
const params = this.#makeDefaultExecParams({
|
|
onInstantiationError: (error) => { reject(error); },
|
|
});
|
|
params.arguments = parameters?.args;
|
|
let data = '';
|
|
params.print = (out) => {
|
|
parameters?.onStdout?.(out);
|
|
data += `${out}\n`;
|
|
};
|
|
params.printErr = () => { };
|
|
params.onAbort = () => reject(new AbortedError(data));
|
|
params.quit = (code, exception) => {
|
|
if (exception && exception.name !== 'ExitStatus')
|
|
reject(exception);
|
|
};
|
|
params.notifyTestFinished = (code) => {
|
|
result = { stdout: data, exitCode: code };
|
|
testFinished = true;
|
|
window.dispatchEvent(testFinishedEvent);
|
|
};
|
|
return params;
|
|
})());
|
|
if (!testFinished) {
|
|
await new Promise((resolve) => {
|
|
window.addEventListener('testFinished', () => {
|
|
resolve();
|
|
});
|
|
});
|
|
}
|
|
resolve({
|
|
stdout: result.stdout,
|
|
exitCode: result.exitCode,
|
|
instance,
|
|
});
|
|
});
|
|
}
|
|
|
|
#makeDefaultExecParams(params) {
|
|
const instanceParams = {};
|
|
instanceParams.instantiateWasm = async (imports, onDone) => {
|
|
try {
|
|
onDone(await WebAssembly.instantiate(this.#wasm, imports), this.#wasm);
|
|
} catch (e) {
|
|
params?.onInstantiationError?.(e);
|
|
}
|
|
};
|
|
instanceParams.locateFile = (filename) =>
|
|
this.#resourceLocator.locate(filename);
|
|
instanceParams.monitorRunDependencies = (name) => { };
|
|
instanceParams.print = (text) => true && console.log(text);
|
|
instanceParams.printErr = (text) => true && console.warn(text);
|
|
|
|
instanceParams.mainScriptUrlOrBlob = new Blob([this.#js], {
|
|
type: 'text/javascript',
|
|
});
|
|
return instanceParams;
|
|
}
|
|
}
|
|
|
|
// Streamlines loading of WASM modules.
|
|
export class ModuleLoader {
|
|
#fetcher;
|
|
#resourceLocator;
|
|
|
|
constructor(
|
|
fetcher,
|
|
resourceLocator
|
|
) {
|
|
this.#fetcher = fetcher;
|
|
this.#resourceLocator = resourceLocator;
|
|
}
|
|
|
|
// Loads an emscripten module named |moduleName| from the main resource path. Provides
|
|
// progress of 'downloading' and 'compiling' to the caller using the |onProgress| callback.
|
|
async loadEmscriptenModule(
|
|
moduleName, onProgress
|
|
) {
|
|
if (!Platform.webAssemblySupported)
|
|
throw new Error('Web assembly not supported');
|
|
if (!Platform.webGLSupported)
|
|
throw new Error('WebGL is not supported');
|
|
|
|
onProgress('downloading');
|
|
|
|
const jsLoadPromise = this.#fetcher.fetchText(`${moduleName}.js`);
|
|
const wasmLoadPromise = this.#fetcher.fetchCompileWasm(
|
|
`${moduleName}.wasm`,
|
|
() => {
|
|
onProgress('compiling');
|
|
}
|
|
);
|
|
|
|
const [js, wasm] = await Promise.all([jsLoadPromise, wasmLoadPromise]);
|
|
return CompiledModule.make(js, wasm, `${moduleName}_entry`, this.#resourceLocator);
|
|
}
|
|
}
|