6215add877
Change-Id: I2fe6e6a634a406f4c0e5c491b4ff6286ae888175
Reviewed-on: https://chromium-review.googlesource.com/c/external/github.com/WebKit/JetStream/+/7124399
Reviewed-by: Leszek Swirski <leszeks@chromium.org>
Reviewed-by: Igor Sheludko <ishell@chromium.org>
304 lines
10 KiB
JavaScript
304 lines
10 KiB
JavaScript
#! /usr/bin/env node
|
|
/* eslint-disable-next-line no-unused-vars */
|
|
|
|
// Copyright (C) 2007-2025 Apple Inc. All rights reserved.
|
|
|
|
// Redistribution and use in source and binary forms, with or without
|
|
// modification, are permitted provided that the following conditions
|
|
// are met:
|
|
// 1. Redistributions of source code must retain the above copyright
|
|
// notice, this list of conditions and the following disclaimer.
|
|
// 2. Redistributions in binary form must reproduce the above copyright
|
|
// notice, this list of conditions and the following disclaimer in the
|
|
// documentation and/or other materials provided with the distribution.
|
|
|
|
// THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
|
|
// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
|
|
// THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
|
// PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
|
|
// BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
|
// CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
|
// SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
|
// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
|
// CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
|
// ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
|
|
// THE POSSIBILITY OF SUCH DAMAGE.
|
|
|
|
import serve from "./server.mjs";
|
|
import { Builder, Capabilities, logging } from "selenium-webdriver";
|
|
import { Options as ChromeOptions } from "selenium-webdriver/chrome.js";
|
|
import { Options as FirefoxOptions } from "selenium-webdriver/firefox.js";
|
|
import commandLineArgs from "command-line-args";
|
|
import { promises as fs } from "fs";
|
|
import path from "path";
|
|
import os from "os";
|
|
|
|
import {logInfo, logError, printHelp, runTest} from "./helper.mjs";
|
|
|
|
const TESTS = [
|
|
{
|
|
name: "Run Single Suite",
|
|
tags: ["all", "main"],
|
|
run() {
|
|
return runEnd2EndTest("Run Single Suite", { test: "proxy-mobx" });
|
|
}
|
|
},
|
|
{
|
|
name: "Run Multiple Suites",
|
|
tags: ["all", "main"],
|
|
run() {
|
|
return runEnd2EndTest("Run Multiple Suites", { test: "prismjs-startup-es6,postcss-wtb" });
|
|
}
|
|
},
|
|
{
|
|
name: "Run Tag No Prefetch",
|
|
tags: ["all", "main"],
|
|
run() {
|
|
return runEnd2EndTest("Run Tag No Prefetch", { tag: "proxy", prefetchResources: "false" });
|
|
}
|
|
},
|
|
{
|
|
name: "Run Disabled Suite",
|
|
tags: ["all", "disabled"],
|
|
run() {
|
|
return runEnd2EndTest("Run Disabled Suite", { tag: "disabled" });
|
|
}
|
|
},
|
|
{
|
|
name: "Run Default Suite",
|
|
tags: ["all", "default"],
|
|
run() {
|
|
return runEnd2EndTest("Run Default Suite");
|
|
}
|
|
}
|
|
];
|
|
|
|
const VALID_TAGS = Array.from(new Set(TESTS.map((each) => each.tags).flat()));
|
|
|
|
function sleep(ms) {
|
|
return new Promise(resolve => setTimeout(resolve, ms));
|
|
}
|
|
|
|
const optionDefinitions = [
|
|
{ name: "browser", type: String, description: "Set the browser to test, choices are [safari, firefox, chrome, edge]. By default the $BROWSER env variable is used." },
|
|
{ name: "port", type: Number, defaultValue: 8010, description: "Set the test-server port, The default value is 8010." },
|
|
{ name: "help", alias: "h", description: "Print this help text." },
|
|
{ name: "suite", type: String, defaultOption: true, typeLabel: `{underline choices}: ${VALID_TAGS.join(", ")}`, description: "Run a specific suite by name." }
|
|
];
|
|
|
|
const options = commandLineArgs(optionDefinitions);
|
|
|
|
if ("help" in options)
|
|
printHelp("". optionDefinitions);
|
|
|
|
if (options.suite && !VALID_TAGS.includes(options.suite))
|
|
printHelp(`Invalid suite: ${options.suite}. Choices are: ${VALID_TAGS.join(", ")}`);
|
|
|
|
const BROWSER = options?.browser;
|
|
if (!BROWSER)
|
|
printHelp("No browser specified, use $BROWSER or --browser", optionDefinitions);
|
|
const IS_HEADLESS = os.platform() === "linux" && !process.env.DISPLAY;
|
|
|
|
let capabilities;
|
|
let browserOptions;
|
|
switch (BROWSER) {
|
|
case "safari":
|
|
capabilities = Capabilities.safari();
|
|
capabilities.set("safari:diagnose", true);
|
|
break;
|
|
|
|
case "firefox": {
|
|
capabilities = Capabilities.firefox()
|
|
if (IS_HEADLESS) {
|
|
browserOptions = new FirefoxOptions();
|
|
browserOptions.addArguments("-headless");
|
|
}
|
|
break;
|
|
}
|
|
case "chrome": {
|
|
capabilities = Capabilities.chrome()
|
|
if (IS_HEADLESS) {
|
|
browserOptions = new ChromeOptions();
|
|
browserOptions.addArguments("--headless");
|
|
}
|
|
break;
|
|
}
|
|
case "edge": {
|
|
capabilities = Capabilities.edge();
|
|
break;
|
|
}
|
|
default: {
|
|
printHelp(`Invalid browser "${BROWSER}", choices are: "safari", "firefox", "chrome", "edge"`, optionDefinitions);
|
|
}
|
|
}
|
|
|
|
process.on("unhandledRejection", (err) => {
|
|
logError(err);
|
|
process.exit(1);
|
|
});
|
|
process.once("uncaughtException", (err) => {
|
|
logError(err);
|
|
process.exit(1);
|
|
});
|
|
|
|
const PORT = options.port;
|
|
const server = await serve(PORT);
|
|
|
|
async function runTests() {
|
|
let success = true;
|
|
const suiteFilter = options.suite || "all";
|
|
|
|
const testsToRun = TESTS.filter(test => test.tags.includes(suiteFilter));
|
|
|
|
if (testsToRun.length === 0) {
|
|
console.error(`No suite found for filter: ${suiteFilter}`);
|
|
process.exit(1);
|
|
}
|
|
|
|
try {
|
|
for (const test of testsToRun) {
|
|
success &&= await test.run();
|
|
}
|
|
} finally {
|
|
server.close();
|
|
}
|
|
if (!success)
|
|
process.exit(1);
|
|
}
|
|
|
|
async function runEnd2EndTest(name, params) {
|
|
return runTest(name, () => testEnd2End(params));
|
|
}
|
|
|
|
async function testEnd2End(params) {
|
|
const builder = new Builder().withCapabilities(capabilities);
|
|
if (browserOptions) {
|
|
switch(BROWSER) {
|
|
case "firefox":
|
|
builder.setFirefoxOptions(browserOptions);
|
|
break;
|
|
case "chrome":
|
|
builder.setChromeOptions(browserOptions);
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
const driver = await builder.build();
|
|
const sessionId = (await driver.getSession()).getId();
|
|
const driverCapabilities = await driver.getCapabilities();
|
|
logInfo(`Browser: ${driverCapabilities.getBrowserName()} ${driverCapabilities.getBrowserVersion()}`);
|
|
const urlParams = Object.assign({
|
|
worstCaseCount: 2,
|
|
iterationCount: 3
|
|
}, params);
|
|
let results;
|
|
let success = true;
|
|
try {
|
|
const url = new URL(`http://localhost:${PORT}/index.html`);
|
|
url.search = new URLSearchParams(urlParams).toString();
|
|
logInfo(`JetStream PREPARE ${url}`);
|
|
await driver.get(url.toString());
|
|
await driver.executeAsyncScript((callback) => {
|
|
// callback() is explicitly called without the default event
|
|
// as argument to avoid serialization issues with chromedriver.
|
|
globalThis.addEventListener("JetStreamReady", () => callback());
|
|
// We might not get a chance to install the on-ready listener, thus
|
|
// we also check if the runner is ready synchronously.
|
|
if (globalThis?.JetStream?.isReady)
|
|
callback();
|
|
});
|
|
results = await benchmarkResults(driver);
|
|
// FIXME: validate results;
|
|
} catch(e) {
|
|
success = false;
|
|
throw e;
|
|
} finally {
|
|
await driver.quit();
|
|
if (BROWSER === "safari")
|
|
await sleep(1000);
|
|
if (!success) {
|
|
await printLogs(sessionId);
|
|
}
|
|
}
|
|
}
|
|
|
|
async function benchmarkResults(driver) {
|
|
logInfo("JetStream START");
|
|
await driver.manage().setTimeouts({ script: 2 * 60_000 });
|
|
await driver.executeAsyncScript((callback) => {
|
|
globalThis.JetStream.start();
|
|
callback();
|
|
});
|
|
|
|
await new Promise((resolve, reject) => pollResultsUntilDone(driver, resolve, reject));
|
|
const resultsJSON = await driver.executeScript(() => {
|
|
return globalThis.JetStream.resultsJSON();
|
|
});
|
|
return JSON.parse(resultsJSON);
|
|
}
|
|
|
|
class JetStreamTestError extends Error {
|
|
constructor(errors) {
|
|
super(`Tests failed: ${errors.map(e => e.stack).join(", ")}`);
|
|
this.errors = errors;
|
|
}
|
|
}
|
|
|
|
const UPDATE_INTERVAL = 250;
|
|
async function pollResultsUntilDone(driver, resolve, reject) {
|
|
const previousResults = new Set();
|
|
const intervalId = setInterval(async function logResult() {
|
|
const {done, errors, resultsJSON} = await driver.executeScript(() => {
|
|
return {
|
|
done: globalThis.JetStream.isDone,
|
|
errors: globalThis.JetStream.errors,
|
|
resultsJSON: JSON.stringify(globalThis.JetStream.resultsObject("simple")),
|
|
};
|
|
});
|
|
if (errors.length) {
|
|
clearInterval(intervalId);
|
|
reject(new JetStreamTestError(errors));
|
|
}
|
|
logIncrementalResult(previousResults, JSON.parse(resultsJSON));
|
|
if (done) {
|
|
clearInterval(intervalId);
|
|
resolve();
|
|
}
|
|
}, UPDATE_INTERVAL)
|
|
}
|
|
|
|
function logIncrementalResult(previousResults, benchmarkResults) {
|
|
for (const [testName, testResults] of Object.entries(benchmarkResults)) {
|
|
if (previousResults.has(testName))
|
|
continue;
|
|
console.log(testName, testResults);
|
|
previousResults.add(testName);
|
|
}
|
|
}
|
|
|
|
function printLogs(sessionId) {
|
|
if (BROWSER === "safari" && sessionId)
|
|
return printSafariLogs(sessionId);
|
|
}
|
|
|
|
async function printSafariLogs(sessionId) {
|
|
const sessionLogDir = path.join(os.homedir(), "Library", "Logs", "com.apple.WebDriver", sessionId);
|
|
try {
|
|
const files = await fs.readdir(sessionLogDir);
|
|
const logFiles = files.filter(f => f.startsWith("safaridriver.") && f.endsWith(".txt"));
|
|
if (logFiles.length === 0) {
|
|
logInfo(`No safaridriver log files found in session directory: ${sessionLogDir}`);
|
|
return;
|
|
}
|
|
for (const file of logFiles) {
|
|
const logPath = path.join(sessionLogDir, file);
|
|
const logContent = await fs.readFile(logPath, "utf8");
|
|
logGroup(`SafariDriver Log: ${file}`, () => console.log(logContent));
|
|
}
|
|
} catch (err) {
|
|
logError("Error reading SafariDriver logs:", err);
|
|
}
|
|
}
|
|
|
|
setImmediate(runTests);
|