blob: 44cc825a7efe45a64a7cd3864fe7fcfc5dfc5bfe [file] [log] [blame] [edit]
#! /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);