diff --git a/tests/src/test/js/client.js b/tests/src/test/js/client.js new file mode 100644 index 000000000..58eb208c2 --- /dev/null +++ b/tests/src/test/js/client.js @@ -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(); \ No newline at end of file diff --git a/tests/src/test/js/frame.html b/tests/src/test/js/frame.html new file mode 100644 index 000000000..a2425ef93 --- /dev/null +++ b/tests/src/test/js/frame.html @@ -0,0 +1,25 @@ + + + + +
+ + + + + + \ No newline at end of file diff --git a/tests/src/test/js/frame.js b/tests/src/test/js/frame.js new file mode 100644 index 000000000..e09a445ac --- /dev/null +++ b/tests/src/test/js/frame.js @@ -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", "*"); +} \ No newline at end of file diff --git a/tests/src/test/js/index.html b/tests/src/test/js/index.html new file mode 100644 index 000000000..36c250b29 --- /dev/null +++ b/tests/src/test/js/index.html @@ -0,0 +1,26 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/tests/src/test/js/package.json b/tests/src/test/js/package.json index c1a744195..b46a0681d 100644 --- a/tests/src/test/js/package.json +++ b/tests/src/test/js/package.json @@ -5,7 +5,7 @@ "babel-core": "6.24.1", "babel-polyfill": "6.23.0", "babel-preset-env": "1.4.0", - "chrome-remote-interface": "0.20.0" + "websocket": "1.0.24" }, "scripts": { "build": "babel src -d bin --source-maps" diff --git a/tests/src/test/js/src/promise-fs.js b/tests/src/test/js/src/promise-fs.js index 376aba552..ac63a9cb7 100644 --- a/tests/src/test/js/src/promise-fs.js +++ b/tests/src/test/js/src/promise-fs.js @@ -14,6 +14,8 @@ * limitations under the License. */ +"use strict"; + import * as fs from "fs"; function wrapFsFunction(fsFunction) { diff --git a/tests/src/test/js/src/run-tests.js b/tests/src/test/js/src/run-tests.js index 6cfc56546..641f54a06 100644 --- a/tests/src/test/js/src/run-tests.js +++ b/tests/src/test/js/src/run-tests.js @@ -14,8 +14,10 @@ * limitations under the License. */ +"use strict"; 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 RUNTIME_FILE_NAME = "runtime.js"; @@ -24,6 +26,7 @@ const TEST_FILES = [ { file: "test-min.js", name: "minified" }, { file: "test-optimized.js", name: "optimized" } ]; +let totalTests = 0; class TestSuite { constructor(name) { @@ -46,31 +49,38 @@ async function runAll() { console.log("Running tests"); 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(); - await new Promise((resolve, reject) => { - CDP(async (client) => { + wsServer.on("connect", async (conn) => { try { - const {Page, Runtime} = client; - await Promise.all([Runtime.enable(), Page.enable()]); - await Page.navigate({url: "about:blank"}); - - const runner = new TestRunner(Page, Runtime); + const runner = new TestRunner(conn); await runner.runTests(rootSuite, "", 0); stats.testRun = runner.testsRun; stats.testsFailed = runner.testsFailed; - await client.close(); resolve(); } catch (e) { reject(e); } - }).on("error", err => { - reject(err); - }).on("disconnect", () => { - reject("disconnected from chrome"); - }); + }) }); + wsServer.unmount(); + server.close(); + const endTime = new Date().getTime(); for (let i = 0; i < stats.testsFailed.length; i++) { const failedTest = stats.testsFailed[i]; @@ -84,6 +94,8 @@ async function runAll() { if (stats.testsFailed.length > 0) { process.exit(1); + } else { + process.exit(0); } } @@ -95,6 +107,7 @@ async function walkDir(path, name, suite) { suite.testCases.push(new TestCase( name + " " + profileName, [path + "/" + RUNTIME_FILE_NAME, path + "/" + fileName])); + totalTests++; } } } else if (files) { @@ -111,11 +124,20 @@ async function walkDir(path, name, suite) { } class TestRunner { - constructor(page, runtime) { - this.page = page; - this.runtime = runtime; + constructor(ws) { + this.ws = ws; this.testsRun = 0; 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) { @@ -123,32 +145,41 @@ class TestRunner { console.log("Running " + path); let testsFailedInSuite = 0; const startTime = new Date().getTime(); - for (const testCase of suite.testCases) { - this.testsRun++; - try { - const testRun = Promise.race([ - this.runTeaVMTest(testCase), - new Promise(resolve => { - setTimeout(() => resolve({status: "failed", errorMessage: "timeout"}), 1000); - }) - ]); - const result = await testRun; - switch (result.status) { - case "OK": - break; - case "failed": - this.logFailure(path, testCase, result.errorMessage); - testsFailedInSuite++; - break; - } - } catch (e) { - this.logFailure(path, testCase, e.stack); - testsFailedInSuite++; + let request = { id: this.requestIdGen++ }; + request.tests = suite.testCases.map(testCase => { + return { + name: testCase.name, + files: testCase.files.map(fileName => process.cwd() + "/" + fileName) + }; + }); + this.testsRun += suite.testCases.length; + + const resultPromise = new Promise(resolve => { + this.pendingRequests[request.id] = resolve; + }); + const timeoutPromise = new Promise((_, reject) => { + setTimeout(() => reject(new Error("connection timeout")), 120000); + }); + this.ws.send(JSON.stringify(request)); + const result = await Promise.race([resultPromise, timeoutPromise]); + + for (let i = 0; i < suite.testCases.length; i++) { + const testCase = suite.testCases[i]; + 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 percentComplete = (this.testsRun / totalTests * 100).toFixed(1); console.log("Tests run: " + suite.testCases.length + ", failed: " + testsFailedInSuite - + ", elapsed: " + ((endTime - startTime) / 1000) + " seconds"); + + ", elapsed: " + ((endTime - startTime) / 1000) + " seconds " + + "(" + percentComplete + "% complete)"); console.log(); }