diff --git a/build.js b/build.js index 40c0bf80a67113..84ab562af4d7f9 100644 --- a/build.js +++ b/build.js @@ -1,24 +1,44 @@ const fs = require("fs"); const os = require("os"); +const path = require("path"); const { spawnSync } = require("child_process"); const node = __dirname; +const OutDir = path.join(node, "out"); -// Download the record/replay driver archive, using the latest version unless -//it was overridden via the environment. -let driverArchive = `${currentPlatform()}-recordreplay.tgz`; -let downloadDriverRevision = process.env.DRIVER_REVISION ? process.env.DRIVER_REVISION : fs.readFileSync("REPLAY_BACKEND_REV", "utf8"); -let downloadArchive = `${currentPlatform()}-recordreplay-${downloadDriverRevision.trim().substring(0,12)}.tgz`; +// Use local driver directory if provided, otherwise download from S3. +const localDriverDir = process.env.REPLAY_LOCAL_DRIVER_DIR; const driverFile = `${currentPlatform()}-recordreplay.${driverExtension()}`; const driverJSON = `${currentPlatform()}-recordreplay.json`; -spawnChecked("curl", [`https://static.replay.io/downloads/${downloadArchive}`, "-o", driverArchive], { stdio: "inherit" }); -spawnChecked("tar", ["xf", driverArchive]); -fs.unlinkSync(driverArchive); - -// Embed the driver in the source. -const driverContents = fs.readFileSync(driverFile); -const { revision: driverRevision, date: driverDate } = JSON.parse(fs.readFileSync(driverJSON, "utf8")); -fs.unlinkSync(driverFile); -fs.unlinkSync(driverJSON); + +let driverContents; +let driverRevision; +let driverDate; + +if (localDriverDir) { + console.log("[build] Loading driver from local directory:", localDriverDir); + driverContents = fs.readFileSync(path.join(localDriverDir, driverFile)); + const driverInfo = JSON.parse(fs.readFileSync(path.join(localDriverDir, driverJSON), "utf8")); + driverRevision = driverInfo.revision; + driverDate = driverInfo.date; +} else { + console.log("[build] Downloading driver from S3..."); + let driverArchive = `${currentPlatform()}-recordreplay.tgz`; + let downloadDriverRevision = process.env.DRIVER_REVISION ? process.env.DRIVER_REVISION : fs.readFileSync("REPLAY_BACKEND_REV", "utf8"); + let downloadArchive = `${currentPlatform()}-recordreplay-${downloadDriverRevision.trim().substring(0,12)}.tgz`; + const driverArchivePath = path.join(OutDir, driverArchive); + spawnChecked("curl", [`https://static.replay.io/downloads/${downloadArchive}`, "-o", driverArchivePath], { stdio: "inherit" }); + spawnChecked("tar", ["xf", driverArchivePath, "-C", OutDir]); + fs.unlinkSync(driverArchivePath); + + driverContents = fs.readFileSync(path.join(OutDir, driverFile)); + const driverInfo = JSON.parse(fs.readFileSync(path.join(OutDir, driverJSON), "utf8")); + driverRevision = driverInfo.revision; + driverDate = driverInfo.date; + fs.unlinkSync(path.join(OutDir, driverFile)); + fs.unlinkSync(path.join(OutDir, driverJSON)); + fs.unlinkSync(path.join(OutDir, `${driverFile}.symbols.json`)); +} +console.log("[build] Generating driver source file..."); let driverString = ""; for (let i = 0; i < driverContents.length; i++) { driverString += `\\${driverContents[i].toString(8)}`; @@ -36,17 +56,28 @@ namespace node { const numCPUs = os.cpus().length; +function getSanitizedEnv() { + const env = { ...process.env }; + if (env.PATH) { + env.PATH = env.PATH.split(":").filter(p => !p.includes("/nix/")).join(":"); + } + delete env.NIX_PROFILES; + delete env.NIX_SSL_CERT_FILE; + env.RECORD_REPLAY_DONT_RECORD = "1"; + return env; +} + +const buildEnv = getSanitizedEnv(); + if (process.env.CONFIGURE_NODE) { - spawnChecked(`${node}/configure`, [], { cwd: node, stdio: "inherit" }); + console.log("[build] Running configure..."); + spawnChecked(`${node}/configure`, [], { cwd: node, stdio: "inherit", env: buildEnv }); } -spawnChecked("make", [`-j${numCPUs}`, "-C", "out", "BUILDTYPE=Release"], { +console.log("[build] Running make..."); +spawnChecked("make", [`-j${numCPUs}`, "-C", OutDir, "BUILDTYPE=Release"], { cwd: node, stdio: "inherit", - env: { - ...process.env, - // Disable recording when node runs as part of its compilation process. - RECORD_REPLAY_DONT_RECORD: "1", - }, + env: buildEnv, }); function spawnChecked(cmd, args, options) { diff --git a/replay-test/client.ts b/replay-test/client.ts deleted file mode 100644 index 41f2382a081452..00000000000000 --- a/replay-test/client.ts +++ /dev/null @@ -1,91 +0,0 @@ -/* Copyright 2021 Record Replay Inc. */ - -// Simple protocol client for use in writing standalone applications. - -import * as WebSocket from "ws"; -import { - CommandMethods, - CommandParams, - CommandResult, - EventMethods, - EventParams, - EventListeners, -} from "@recordreplay/protocol"; -import { assert, Deferred, defer } from "./utils"; - -export type ClientCallbacks = { - onClose: (code: number, reason: string) => void; - onError: (e: unknown) => void; -}; - -export default class ProtocolClient { - socket: WebSocket; - callbacks: ClientCallbacks; - - // Internal state. - openWaiter: Deferred = defer(); - eventListeners: Partial = {}; - pendingMessages = new Map>>(); - nextMessageId = 1; - - constructor(address: string, callbacks: ClientCallbacks) { - this.socket = new (WebSocket.default || WebSocket)(address); - this.callbacks = callbacks; - - this.socket.on("open", this.openWaiter.resolve); - this.socket.on("close", callbacks.onClose); - this.socket.on("error", callbacks.onError); - this.socket.on("message", message => this.onMessage(message)); - } - - close() { - this.socket.close(); - } - - addEventListener( - event: M, - listener: (params: EventParams) => void - ) { - this.eventListeners[event] = listener; - } - - waitUntilOpen() { - return this.openWaiter.promise; - } - - async sendCommand( - method: M, - params: CommandParams, - sessionId?: string, - pauseId?: string - ): Promise> { - const id = this.nextMessageId++; - this.socket.send(JSON.stringify({ id, method, params, sessionId, pauseId })); - const waiter = defer>(); - this.pendingMessages.set(id, waiter); - return waiter.promise; - } - - onMessage(str: string) { - const msg = JSON.parse(str); - if (msg.id) { - const { resolve, reject } = this.pendingMessages.get(msg.id as number)!; - this.pendingMessages.delete(msg.id as number); - if (msg.result) { - resolve(msg.result as any); - } else { - reject(msg.error); - } - } else { - assert(typeof msg.method === "string"); - assert(typeof msg.params === "object" && msg.params); - - const handler = this.eventListeners[msg.method as EventMethods]; - if (handler) { - handler({ ...msg.params } as any); - } else { - console.error("MissingMessageHandler", { method: msg.method }); - } - } - } -} diff --git a/replay-test/examples/async.js b/replay-test/examples/async.js deleted file mode 100644 index 0b8c36628ea9a2..00000000000000 --- a/replay-test/examples/async.js +++ /dev/null @@ -1,26 +0,0 @@ - - - - - -function finished() { - process.exit(0); -} -async function foo() { - console.log("foo"); - bar(); - await baz(4); - setTimeout(finished, 0); -} -function bar() { - console.log("bar"); -} -async function baz(n) { - console.log("baz", n); - if (n) { - await new Promise(r => setTimeout(r, 0)); - await baz(n - 1); - } - return n; -} -foo(); diff --git a/replay-test/examples/basic.js b/replay-test/examples/basic.js deleted file mode 100644 index 6251f4e1d65d27..00000000000000 --- a/replay-test/examples/basic.js +++ /dev/null @@ -1,12 +0,0 @@ - -function foo() { - for (let i = 0; i < 3; i++) { - bar(i); - } -} - -function bar(num) { - console.log("HELLO", num, { num }); -} - -setTimeout(foo, 0); diff --git a/replay-test/examples/control_flow.js b/replay-test/examples/control_flow.js deleted file mode 100644 index c495212e942929..00000000000000 --- a/replay-test/examples/control_flow.js +++ /dev/null @@ -1,120 +0,0 @@ -// -// -//
Hello World!
-// -// -// diff --git a/replay-test/examples/error.js b/replay-test/examples/error.js deleted file mode 100644 index 500b100d78ead9..00000000000000 --- a/replay-test/examples/error.js +++ /dev/null @@ -1,4 +0,0 @@ -function foo() { - a = b; -} -foo(); diff --git a/replay-test/examples/exceptions.js b/replay-test/examples/exceptions.js deleted file mode 100644 index 233022d3975860..00000000000000 --- a/replay-test/examples/exceptions.js +++ /dev/null @@ -1,17 +0,0 @@ - - -function finished() { - process.exit(0); -} -let number = 0; -setTimeout(foo, 0); -function foo() { - try { - bar(); - } catch (e) {} - setTimeout(number == 10 ? finished : foo, 0); -} -function bar() { - number++; - throw { number }; -} diff --git a/replay-test/examples/napi.js b/replay-test/examples/napi.js deleted file mode 100644 index 58c6bfd1ff2f64..00000000000000 --- a/replay-test/examples/napi.js +++ /dev/null @@ -1,26 +0,0 @@ -const isValidUTF8 = require("utf-8-validate"); -function testUTF8() { - const buf = Buffer.from([0xf0, 0x90, 0x80, 0x80]); - console.log(isValidUTF8(buf)); -} -testUTF8(); - -const sharp = require("sharp"); -async function testSharp() { - const data = "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"; - const buf = Buffer.from(data, "base64"); - const resized = await sharp(buf).resize({ height: 5 }).toBuffer(); - console.log(resized.toString("base64")); -} -testSharp(); - -const bufferutil = require("bufferutil"); -function testBufferUtil() { - const buf = Buffer.from([1,2,3,4]); - const mask = Buffer.from([5,6,7,8]); - bufferutil.unmask(buf, mask); - console.log(`Unmasked ${JSON.stringify([...buf])}`); - bufferutil.mask(buf, mask, buf, 0, 4); - console.log(`Remasked ${JSON.stringify([...buf])}`); -} -testBufferUtil(); diff --git a/replay-test/examples/objects.js b/replay-test/examples/objects.js deleted file mode 100644 index 34499c1ef980dd..00000000000000 --- a/replay-test/examples/objects.js +++ /dev/null @@ -1,62 +0,0 @@ - -function foo() { - // Create various objects see doc_rr_objects.html in the devtools repo. - var a = Array(); - var b = new Uint8Array(20); - var c = new Set([{a:0},{b:1}]); - var d = new Map([[{a:0},{b:1}]]); - var e = new WeakSet(); - var f = new WeakMap(); - const keepalive = []; - var g = { a:0 }; - for (let i = 0; i < 20; i++) { - a.push(i); - b[i] = i; - c.add(i); - d.set(i, i + 1); - const k = { i }; - const v = { j: i + 1 }; - keepalive.push(k, v); - e.add(k); - f.set(k, v); - g[`a${i}`] = i; - } - var h = /abc/gi; - var i = new Date(); - var j = RangeError("foo"); - var l = bar; - var m = [undefined, true, 3, null, "z", 40n]; - var n = new Proxy({ a: 0 }, { - get(target, prop, receiver) { - dump("Hello\n"); - } - }); - var o = Symbol(); - var p = Symbol("symbol"); - var q = { [o]: 42, [p]: o }; - console.log(a); - console.log(b); - console.log(c); - console.log(d); - console.log(e); - console.log(f); - console.log(g); - console.log(h); - console.log(i); - console.log(j); - console.log(l); - console.log(m); - console.log(n); - console.log(o); - console.log(p); - console.log(q); -} -foo(); - -function bar(z) { - console.log("bar", z); -} - -function baz() { - console.log("baz"); -} diff --git a/replay-test/examples/run_worker.js b/replay-test/examples/run_worker.js deleted file mode 100644 index 4c6442d1b3c33c..00000000000000 --- a/replay-test/examples/run_worker.js +++ /dev/null @@ -1,19 +0,0 @@ -const { Worker } = require("worker_threads"); - -let gWorker; - -function spawnWorker() { - gWorker = new Worker(`${__dirname}/worker.js`); - gWorker.on("message", onWorkerMessage); - setTimeout(sendWorkerMessage, 0); -} -setTimeout(spawnWorker, 0); - -function sendWorkerMessage() { - gWorker.postMessage({ kind: "ping" }); -} - -function onWorkerMessage({ kind }) { - console.log("GotWorkerMessage", kind); - process.exit(); -} diff --git a/replay-test/examples/spawn.js b/replay-test/examples/spawn.js deleted file mode 100644 index a6fa4f20ca8fec..00000000000000 --- a/replay-test/examples/spawn.js +++ /dev/null @@ -1,60 +0,0 @@ - -const { spawnSync, spawn } = require("child_process"); - -function foo(n) { - const { stdout } = spawnSync("echo", [n.toString()]); - console.log("sync", stdout.toString().trim()); -} -for (let i = 0; i < 10; i++) { - foo(i); -} - -async function bar() { - for (let i = 0; i < 10; i++) { - const { stdout } = await spawnAsync("echo", [i.toString()]); - console.log("async", stdout.toString().trim()); - } -} -bar(); - -async function spawnAsync(command, args, options) { - const process = spawn(command, args, options); - - const parts = []; - const promises = []; - - if (process.stdout) { - process.stdout.on("data", buf => parts.push(buf)); - - // Make sure stdout has been fully read before returning. - const { resolve, promise } = defer(); - promises.push(promise); - process.stdout.on("end", resolve); - } - - const { resolve, reject, promise } = defer(); - - // Make sure the process exits before returning. - promises.push(promise); - - process.on("exit", code => { - if (code) { - reject(`Process exited abnormally ${command} ${args}`); - } else { - resolve(); - } - }); - - await Promise.all(promises); - - return { stdout: Buffer.concat(parts).toString() }; -} - -function defer() { - let resolve, reject; - const promise = new Promise((res, rej) => { - resolve = res; - reject = rej; - }); - return { promise, resolve, reject }; -} diff --git a/replay-test/examples/worker.js b/replay-test/examples/worker.js deleted file mode 100644 index d11884380e3a4d..00000000000000 --- a/replay-test/examples/worker.js +++ /dev/null @@ -1,6 +0,0 @@ -const { parentPort } = require("worker_threads"); - -parentPort.on("message", async ({ kind }) => { - console.log("WorkerReceivedMessage", kind); - parentPort.postMessage({ kind: "pong" }); -}); diff --git a/replay-test/manifest.ts b/replay-test/manifest.ts deleted file mode 100644 index c1e959cb258e66..00000000000000 --- a/replay-test/manifest.ts +++ /dev/null @@ -1,28 +0,0 @@ -interface TestSpec { - name: string; - allowRecordingError?: boolean; - allowUnusable?: boolean; -} - -// Tests to run during the replay test suite. -export const TestManifest: TestSpec[] = [ - { name: "async.js" }, - { name: "basic.js" }, - { name: "control_flow.js" }, - { name: "error.js", allowRecordingError: true }, - { name: "exceptions.js" }, - { name: "napi.js" }, - { name: "objects.js" }, - { name: "run_worker.js" }, - { name: "spawn.js" }, -]; - -// Patterns to ignore in tests in the regular node test suite. -export const NodeTestIgnoreList = [ - // Experimental node features. - "test-vm-module", - "wasi", - - // Tanks performance for some reason. - "test-init.js", -]; diff --git a/replay-test/package.json b/replay-test/package.json deleted file mode 100644 index fcae587315a98c..00000000000000 --- a/replay-test/package.json +++ /dev/null @@ -1,26 +0,0 @@ -{ - "name": "replay-test", - "version": "1.0.0", - "description": "", - "main": "index.js", - "directories": { - "example": "examples" - }, - "scripts": { - "test": "echo \"Error: no test specified\" && exit 1" - }, - "author": "", - "license": "ISC", - "dependencies": { - "@recordreplay/protocol": "^0.17.0", - "@recordreplay/recordings-cli": "^0.4.1", - "bufferutil": "^4.0.6", - "sharp": "^0.27.2", - "typescript": "^4.5.4", - "utf-8-validate": "^5.0.8", - "ws": "^8.4.2" - }, - "devDependencies": { - "@types/node": "^17.0.8" - } -} diff --git a/replay-test/run.ts b/replay-test/run.ts deleted file mode 100644 index 34c0588e152cbe..00000000000000 --- a/replay-test/run.ts +++ /dev/null @@ -1,327 +0,0 @@ -// Test runner for recording node scripts and replaying the resulting recordings. - -// Using require() here to workaround typescript errors. -import * as fs from "fs"; -import * as os from "os"; -import * as path from "path"; -import { spawn } from "child_process"; -import { TestManifest, NodeTestIgnoreList } from "./manifest"; -import { listAllRecordings, uploadRecording } from "@recordreplay/recordings-cli"; -import { defer, Deferred, killTransitiveSubprocesses } from "./utils"; -import ProtocolClient from "./client"; - -const Usage = ` -Usage: node run -Options: - --node : Specify the path to the node binary to use to record. - --api-key : Specify the API key to use for recording and replaying. - --run-suite: Run the default replay test suite. - --run-random-tests : Run random tests from node's regular test suite. - --run-pattern : Run all tests matching a pattern. - --server
: Set server to connect to (default wss://dispatch.replay.io). -`; - -function doExit(code: number) { - // Kill any lingering subprocesses before exiting. - killTransitiveSubprocesses(); - process.exit(code); -} - -function bailout(message: string) { - console.log(message); - doExit(1); -} - -if (process.argv.length == 2) { - bailout(Usage); -} - -let gNodePath: string; -let gAPIKey: string; -let gRunSuite: boolean; -let gRunRandomTests: number; -let gRunPattern: string; -let gDispatchAddress = "wss://dispatch.replay.io"; - -for (let i = 2; i < process.argv.length; i++) { - switch (process.argv[i]) { - case "--node": - gNodePath = path.resolve(process.cwd(), process.argv[++i]); - break; - case "--api-key": - gAPIKey = process.argv[++i]; - break; - case "--run-suite": - gRunSuite = true; - break; - case "--run-random-tests": - gRunRandomTests = +process.argv[++i]; - break; - case "--run-pattern": - gRunPattern = process.argv[++i]; - break; - case "--server": - gDispatchAddress = process.argv[++i]; - break; - default: - bailout(Usage); - } -} - -if (!gNodePath) { - bailout("Node path not specified"); -} - -if (!gAPIKey) { - bailout("API key not specified"); -} - -if (!gRunSuite && !gRunRandomTests && !gRunPattern) { - bailout("No tests specified"); -} - -process.on("unhandledRejection", error => { - console.error("ErrorUnhandledRejection", error); -}); - -const gRecordingDirectory = path.join(os.tmpdir(), `recordings-${(Math.random() * 1e9) | 0}`); - -let gNumFailures = 0; - -async function main() { - fs.mkdirSync(gRecordingDirectory); - - try { - if (gRunSuite) { - await runTestSuite(); - } - - if (gRunRandomTests) { - await runRandomTests(gRunRandomTests); - } - - if (gRunPattern) { - await runTestsMatchingPattern(gRunPattern); - } - } finally { - fs.rmSync(gRecordingDirectory, { force: true, recursive: true }); - } - - if (gNumFailures) { - console.error(`Had ${gNumFailures} test failures`); - doExit(1); - } - - console.log("All tests passed, exiting"); - doExit(0); -} - -main(); - -async function runTestSuite() { - for (const { name, allowRecordingError, allowUnusable } of TestManifest) { - await runSingleTest( - path.join(__dirname, "examples", name), - { allowRecordingError, allowUnusable } - ); - } -} - -async function runRandomTests(count: number) { - const testList = readTests(path.join(__dirname, "..", "test")); - for (let i = 0; i < count; i++) { - const testPath = pickRandomTest(); - if (testPath) { - await runSingleTest(testPath, { allowRecordingError: true, allowUnusable: true }); - } - } - - function pickRandomTest() { - const index = (Math.random() * testList.length) | 0; - const testPath = testList[index]; - if (NodeTestIgnoreList.some(pattern => testPath.includes(pattern))) { - return null; - } - return testPath; - } -} - -async function runTestsMatchingPattern(pattern: string) { - for (const { name, allowRecordingError, allowUnusable } of TestManifest) { - const testPath = path.join(__dirname, "examples", name); - if (testPath.includes(pattern)) { - await runSingleTest(testPath, { allowRecordingError, allowUnusable }); - } - } - - const testList = readTests(path.join(__dirname, "..", "test")); - for (const testPath of testList) { - if (testPath.includes(pattern)) { - await runSingleTest(testPath, { allowRecordingError: true, allowUnusable: true }); - } - } -} - -// Get the full paths to all JS files in a directory. -function readTests(directory: string): string[] { - const rv: string[] = []; - const entries = fs.readdirSync(directory, { withFileTypes: true }); - for (const entry of entries) { - if (entry.name.endsWith(".js")) { - rv.push(path.join(directory, entry.name)); - } - if (entry.isDirectory()) { - const subdirTests = readTests(path.join(directory, entry.name)); - rv.push(...subdirTests); - } - } - return rv; -} - -function logMessage(message: string) { - console.log((new Date).toISOString(), message); -} - -// Spec describing allowed results from a recording process. -interface FailureSpec { - // Whether the recording process is allowed to exit with an abnormal code. - allowRecordingError?: boolean; - - // Whether the recording is allowed to be unusable. - allowUnusable?: boolean; -} - -function recordingFailed( - code: number, - status: string, - failureSpec: FailureSpec, - testPath: string -) { - if ((code != 0 || status) && !failureSpec.allowRecordingError) { - return true; - } - const recording = lastMatchingRecording(testPath); - if (!recording || recording.status == "crashed") { - return true; - } - if (recording.unusableReason && !failureSpec.allowUnusable) { - return true; - } - return false; -} - -async function runSingleTest(path: string, failureSpec: FailureSpec) { - logMessage(`StartingTest ${path}`); - - try { - const child = spawn( - gNodePath, - [path], - { - stdio: "inherit", - env: { - ...process.env, - RECORD_REPLAY_VERBOSE: "1", - RECORD_REPLAY_DIRECTORY: gRecordingDirectory, - }, - } - ); - - const exitWaiter: Deferred<{ code: number, status: string }> = defer(); - - child.on("close", (code, status) => exitWaiter.resolve({ code, status })); - setTimeout(() => exitWaiter.resolve({ code: 1, status: "Timed out" }), 120_000); - - const { code, status } = await exitWaiter.promise; - - if (recordingFailed(code, status, failureSpec, path)) { - logMessage(`TestFailed: Error while recording ${code} ${status}`); - gNumFailures++; - return; - } - - const recordingId = await uploadTestRecording(path); - if (!recordingId) { - logMessage(`TestFailed: Could not find recording ID`); - gNumFailures++; - return; - } - - logMessage(`Found recording ID ${recordingId}`); - - const replayErrorWaiter: Deferred = defer(); - replayRecording(recordingId).then(error => replayErrorWaiter.resolve(error)); - setTimeout(() => replayErrorWaiter.resolve("Timed out"), 120_000); - - const replayError = await replayErrorWaiter.promise; - if (replayError) { - logMessage(`TestFailed: Replaying recording failed: ${replayError}`); - gNumFailures++; - return; - } - - logMessage(`TestPassed`); - } catch (e) { - logMessage(`TestFailed: Exception thrown ${e} ${e.stack}`); - gNumFailures++; - } -} - -// Get info for the last recording created for the given test path. -function lastMatchingRecording(testPath: string) { - const recordings = listAllRecordings({ directory: gRecordingDirectory }); - for (let i = recordings.length - 1; i >= 0; i--) { - const recording = recordings[i]; - const argv = recording.metadata?.argv; - if (argv && argv.some(arg => arg.includes(testPath))) { - return recording; - } - } - return null; -} - -async function uploadTestRecording(testPath: string): Promise { - const recording = lastMatchingRecording(testPath); - if (recording) { - return uploadRecording( - recording.id, - { directory: gRecordingDirectory, server: gDispatchAddress, apiKey: gAPIKey } - ); - } - return null; -} - -async function replayRecording(recordingId: string): Promise { - const client = new ProtocolClient(gDispatchAddress, { - onError: e => logMessage(`Socket error ${e}`), - onClose: (code, reason) => logMessage(`Socket closed ${code} ${reason}`), - }); - await client.waitUntilOpen(); - - const testErrorWaiter: Deferred = defer(); - - client.addEventListener("Session.unprocessedRegions", () => {}); - client.addEventListener("Recording.sessionError", e => { - testErrorWaiter.resolve(`Session error ${JSON.stringify(e)}`); - }); - - try { - const result = await client.sendCommand("Recording.createSession", { - recordingId, - }); - - const { sessionId } = result; - logMessage(`Created session ${sessionId}`); - - client.sendCommand( - "Session.ensureProcessed", - { level: "basic" }, - sessionId - ).then(() => testErrorWaiter.resolve(null)); - - const error = await testErrorWaiter.promise; - return error; - } finally { - client.close(); - } -} diff --git a/replay-test/tsconfig.json b/replay-test/tsconfig.json deleted file mode 100644 index f26903ce0bf01c..00000000000000 --- a/replay-test/tsconfig.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "compilerOptions": { - "target": "es5", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', or 'ESNEXT'. */ - "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */ - }, - "include": [ - "ts/**/*.ts" - ] -} diff --git a/replay-test/utils.ts b/replay-test/utils.ts deleted file mode 100644 index 4ef26c92678258..00000000000000 --- a/replay-test/utils.ts +++ /dev/null @@ -1,64 +0,0 @@ -import { spawnSync } from "child_process"; - -export function assert(v: any, why = ""): asserts v { - if (!v) { - const error = new Error(`Assertion Failed: ${why}`); - error.name = "AssertionFailure"; - console.error(error); - throw error; - } -} - -export type Resolve = (value: T) => void; - -export interface Deferred { - promise: Promise; - resolve: Resolve; - reject: (reason: any) => void; -} - -export function defer() { - let resolve!: (value: T) => void; - let reject!: (reason: any) => void; - const promise = new Promise((res, rej) => { - resolve = res; - reject = rej; - }); - return { promise, resolve, reject }; -} - -// Kill all running subprocesses of the current process. -export function killTransitiveSubprocesses() { - assert(process.platform != "win32", "NYI"); - - const childToParent: Map = new Map(); - - const lines = spawnSync("ps", ["-A", "-o", "ppid,pid"]).stdout.toString().split("\n"); - for (const line of lines) { - const match = /(\d+)\s+(\d+)/.exec(line); - if (match && +match[1] > 1) { - childToParent.set(+match[2], +match[1]); - } - } - - for (const childPid of childToParent.keys()) { - if (shouldKillSubprocess(childPid)) { - try { - spawnSync("kill", ["-KILL", childPid.toString()]); - } catch (e) {} - } - } - - function shouldKillSubprocess(childPid: number) { - while (true) { - const parent = childToParent.get(childPid); - if (!parent) { - return false; - } - if (parent == process.pid) { - return true; - } - childPid = parent; - } - } -}