Use websocket client to run tests

This commit is contained in:
Alexey Andreev 2017-04-22 18:14:55 +03:00
parent 3c3448e812
commit 60bcf97933
7 changed files with 286 additions and 40 deletions

View File

@ -0,0 +1,81 @@
/*
* Copyright 2017 Alexey Andreev.
*
* 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.
*/
"use strict";
function tryConnect() {
let ws = new WebSocket("ws://localhost:9090");
ws.onopen = () => {
console.log("Connected established");
listen(ws);
};
ws.onclose = () => {
ws.close();
setTimeout(() => {
tryConnect();
}, 500);
};
}
function listen(ws) {
ws.onmessage = (event) => {
let resultConsumer = [];
let request = JSON.parse(event.data);
console.log("Request #" + request.id + " received");
runTests(request.tests, resultConsumer, 0, () => {
console.log("Sending response #" + request.id);
ws.send(JSON.stringify({
id: request.id,
result: resultConsumer
}));
});
}
}
function runTests(tests, consumer, index, callback) {
if (index === tests.length) {
callback();
} else {
let test = tests[index];
runSingleTest(test, result => {
consumer.push(result);
runTests(tests, consumer, index + 1, callback);
});
}
}
function runSingleTest(test, callback) {
console.log("Running test " + test.name + " consisting of " + test.files);
let iframe = document.getElementById("test");
let handshakeListener = () => {
window.removeEventListener("message", handshakeListener);
let listener = event => {
window.removeEventListener("message", listener);
callback(event.data);
};
window.addEventListener("message", listener);
iframe.contentWindow.postMessage(test, "*");
};
window.addEventListener("message", handshakeListener);
iframe.src = "about:blank";
iframe.src = "frame.html";
}
tryConnect();

View File

@ -0,0 +1,25 @@
<!--
~ Copyright 2017 Alexey Andreev.
~
~ 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.
-->
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<script src="frame.js"></script>
</head>
<body onload="start()">
</body>
</html>

View File

@ -0,0 +1,81 @@
/*
* Copyright 2017 Alexey Andreev.
*
* 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.
*/
"use strict";
window.addEventListener("message", event => {
let request = event.data;
appendFiles(request.files, 0, () => {
launchTest(response => {
event.source.postMessage(response, "*");
});
});
});
function appendFiles(files, index, callback) {
if (index === files.length) {
callback();
} else {
let fileName = "file://" + files[index];
let script = document.createElement("script");
script.src = fileName;
script.onload = () => {
appendFiles(files, index + 1, callback);
};
document.body.appendChild(script);
}
}
function launchTest(callback) {
$rt_startThread(() => {
let thread = $rt_nativeThread();
let instance;
let message;
if (thread.isResuming()) {
instance = thread.pop();
}
try {
runTest();
} catch (e) {
message = buildErrorMessage(e);
callback({
status: "failed",
errorMessage: buildErrorMessage(e)
});
return;
}
if (thread.isSuspending()) {
thread.push(instance);
} else {
callback({ status: "OK" });
}
});
function buildErrorMessage(e) {
let stack = e.stack;
if (e.$javaException && e.$javaException.constructor.$meta) {
stack = e.$javaException.constructor.$meta.name + ": ";
let exceptionMessage = extractException(e.$javaException);
stack += exceptionMessage ? $rt_ustr(exceptionMessage) : "";
}
stack += "\n" + stack;
return stack;
}
}
function start() {
window.parent.postMessage("ready", "*");
}

View File

@ -0,0 +1,26 @@
<!--
~ Copyright 2017 Alexey Andreev.
~
~ 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.
-->
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<script src="client.js"></script>
</head>
<body>
<iframe id="test" src="frame.html"></iframe>
</body>
</html>

View File

@ -5,7 +5,7 @@
"babel-core": "6.24.1", "babel-core": "6.24.1",
"babel-polyfill": "6.23.0", "babel-polyfill": "6.23.0",
"babel-preset-env": "1.4.0", "babel-preset-env": "1.4.0",
"chrome-remote-interface": "0.20.0" "websocket": "1.0.24"
}, },
"scripts": { "scripts": {
"build": "babel src -d bin --source-maps" "build": "babel src -d bin --source-maps"

View File

@ -14,6 +14,8 @@
* limitations under the License. * limitations under the License.
*/ */
"use strict";
import * as fs from "fs"; import * as fs from "fs";
function wrapFsFunction(fsFunction) { function wrapFsFunction(fsFunction) {

View File

@ -14,8 +14,10 @@
* limitations under the License. * limitations under the License.
*/ */
"use strict";
import * as fs from "./promise-fs.js"; import * as fs from "./promise-fs.js";
import { default as CDP } from 'chrome-remote-interface'; import * as http from "http";
import { server as WebSocketServer } from "websocket";
const TEST_FILE_NAME = "test.js"; const TEST_FILE_NAME = "test.js";
const RUNTIME_FILE_NAME = "runtime.js"; const RUNTIME_FILE_NAME = "runtime.js";
@ -24,6 +26,7 @@ const TEST_FILES = [
{ file: "test-min.js", name: "minified" }, { file: "test-min.js", name: "minified" },
{ file: "test-optimized.js", name: "optimized" } { file: "test-optimized.js", name: "optimized" }
]; ];
let totalTests = 0;
class TestSuite { class TestSuite {
constructor(name) { constructor(name) {
@ -46,31 +49,38 @@ async function runAll() {
console.log("Running tests"); console.log("Running tests");
const stats = { testRun: 0, testsFailed: [] }; const stats = { testRun: 0, testsFailed: [] };
const server = http.createServer((request, response) => {
response.writeHead(404);
response.end();
});
server.listen(9090, () => {
console.log((new Date()) + ' Server is listening on port 8080');
});
const wsServer = new WebSocketServer({
httpServer: server,
autoAcceptConnections: true
});
const startTime = new Date().getTime(); const startTime = new Date().getTime();
await new Promise((resolve, reject) => { await new Promise((resolve, reject) => {
CDP(async (client) => { wsServer.on("connect", async (conn) => {
try { try {
const {Page, Runtime} = client; const runner = new TestRunner(conn);
await Promise.all([Runtime.enable(), Page.enable()]);
await Page.navigate({url: "about:blank"});
const runner = new TestRunner(Page, Runtime);
await runner.runTests(rootSuite, "", 0); await runner.runTests(rootSuite, "", 0);
stats.testRun = runner.testsRun; stats.testRun = runner.testsRun;
stats.testsFailed = runner.testsFailed; stats.testsFailed = runner.testsFailed;
await client.close();
resolve(); resolve();
} catch (e) { } catch (e) {
reject(e); reject(e);
} }
}).on("error", err => { })
reject(err);
}).on("disconnect", () => {
reject("disconnected from chrome");
});
}); });
wsServer.unmount();
server.close();
const endTime = new Date().getTime(); const endTime = new Date().getTime();
for (let i = 0; i < stats.testsFailed.length; i++) { for (let i = 0; i < stats.testsFailed.length; i++) {
const failedTest = stats.testsFailed[i]; const failedTest = stats.testsFailed[i];
@ -84,6 +94,8 @@ async function runAll() {
if (stats.testsFailed.length > 0) { if (stats.testsFailed.length > 0) {
process.exit(1); process.exit(1);
} else {
process.exit(0);
} }
} }
@ -95,6 +107,7 @@ async function walkDir(path, name, suite) {
suite.testCases.push(new TestCase( suite.testCases.push(new TestCase(
name + " " + profileName, name + " " + profileName,
[path + "/" + RUNTIME_FILE_NAME, path + "/" + fileName])); [path + "/" + RUNTIME_FILE_NAME, path + "/" + fileName]));
totalTests++;
} }
} }
} else if (files) { } else if (files) {
@ -111,11 +124,20 @@ async function walkDir(path, name, suite) {
} }
class TestRunner { class TestRunner {
constructor(page, runtime) { constructor(ws) {
this.page = page; this.ws = ws;
this.runtime = runtime;
this.testsRun = 0; this.testsRun = 0;
this.testsFailed = []; this.testsFailed = [];
this.pendingRequests = Object.create(null);
this.requestIdGen = 0;
this.ws.on("message", (message) => {
const response = JSON.parse(message.utf8Data);
const pendingRequest = this.pendingRequests[response.id];
delete this.pendingRequests[response.id];
pendingRequest(response.result);
})
} }
async runTests(suite, path, depth) { async runTests(suite, path, depth) {
@ -123,32 +145,41 @@ class TestRunner {
console.log("Running " + path); console.log("Running " + path);
let testsFailedInSuite = 0; let testsFailedInSuite = 0;
const startTime = new Date().getTime(); const startTime = new Date().getTime();
for (const testCase of suite.testCases) { let request = { id: this.requestIdGen++ };
this.testsRun++; request.tests = suite.testCases.map(testCase => {
try { return {
const testRun = Promise.race([ name: testCase.name,
this.runTeaVMTest(testCase), files: testCase.files.map(fileName => process.cwd() + "/" + fileName)
new Promise(resolve => { };
setTimeout(() => resolve({status: "failed", errorMessage: "timeout"}), 1000); });
}) this.testsRun += suite.testCases.length;
]);
const result = await testRun; const resultPromise = new Promise(resolve => {
switch (result.status) { this.pendingRequests[request.id] = resolve;
case "OK": });
break; const timeoutPromise = new Promise((_, reject) => {
case "failed": setTimeout(() => reject(new Error("connection timeout")), 120000);
this.logFailure(path, testCase, result.errorMessage); });
testsFailedInSuite++; this.ws.send(JSON.stringify(request));
break; const result = await Promise.race([resultPromise, timeoutPromise]);
}
} catch (e) { for (let i = 0; i < suite.testCases.length; i++) {
this.logFailure(path, testCase, e.stack); const testCase = suite.testCases[i];
testsFailedInSuite++; const testResult = result[i];
switch (testResult.status) {
case "OK":
break;
case "failed":
this.logFailure(path, testCase, testResult.errorMessage);
testsFailedInSuite++;
break;
} }
} }
const endTime = new Date().getTime(); const endTime = new Date().getTime();
const percentComplete = (this.testsRun / totalTests * 100).toFixed(1);
console.log("Tests run: " + suite.testCases.length + ", failed: " + testsFailedInSuite console.log("Tests run: " + suite.testCases.length + ", failed: " + testsFailedInSuite
+ ", elapsed: " + ((endTime - startTime) / 1000) + " seconds"); + ", elapsed: " + ((endTime - startTime) / 1000) + " seconds "
+ "(" + percentComplete + "% complete)");
console.log(); console.log();
} }