🏡 index : cold-air-networks/mirror-jetstream.git

#! /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);