diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index dd24737f..22376a57 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -41,6 +41,16 @@ jobs: env: POSTGRES_URL: postgres://postgres:postgres@localhost:5432 + windows-test: + runs-on: windows-latest + steps: + - uses: actions/checkout@v3 + - uses: actions/setup-node@v3 + - run: yarn + - run: yarn prepack + - run: yarn test + - run: node test/smoketest.mjs + release: runs-on: ubuntu-latest permissions: diff --git a/.yarn/cache/@esbuild-win32-x64-npm-0.19.5-1e2e5abaa6-8.zip b/.yarn/cache/@esbuild-win32-x64-npm-0.19.5-1e2e5abaa6-8.zip new file mode 100644 index 00000000..96f5b36c Binary files /dev/null and b/.yarn/cache/@esbuild-win32-x64-npm-0.19.5-1e2e5abaa6-8.zip differ diff --git a/.yarn/cache/@next-swc-win32-x64-msvc-npm-14.0.4-e7cf0df5d6-8.zip b/.yarn/cache/@next-swc-win32-x64-msvc-npm-14.0.4-e7cf0df5d6-8.zip new file mode 100644 index 00000000..e1d90d3a Binary files /dev/null and b/.yarn/cache/@next-swc-win32-x64-msvc-npm-14.0.4-e7cf0df5d6-8.zip differ diff --git a/.yarn/cache/@rollup-rollup-win32-x64-msvc-npm-4.1.4-a9b7b64292-8.zip b/.yarn/cache/@rollup-rollup-win32-x64-msvc-npm-4.1.4-a9b7b64292-8.zip new file mode 100644 index 00000000..8c542e80 Binary files /dev/null and b/.yarn/cache/@rollup-rollup-win32-x64-msvc-npm-4.1.4-a9b7b64292-8.zip differ diff --git a/.yarn/cache/@swc-core-win32-x64-msvc-npm-1.3.78-3ed6a5ed32-8.zip b/.yarn/cache/@swc-core-win32-x64-msvc-npm-1.3.78-3ed6a5ed32-8.zip new file mode 100644 index 00000000..8d99b0e6 Binary files /dev/null and b/.yarn/cache/@swc-core-win32-x64-msvc-npm-1.3.78-3ed6a5ed32-8.zip differ diff --git a/src/PackageMatcher.ts b/src/PackageMatcher.ts index f735dd18..b276d934 100644 --- a/src/PackageMatcher.ts +++ b/src/PackageMatcher.ts @@ -1,25 +1,37 @@ import { resolve } from "node:path"; import { fileURLToPath } from "node:url"; +import fwdSlashPath from "./util/fwdSlashPath"; export default class PackageMatcher extends Array { constructor( private root: string, packages: Package[], ) { + // Normalize path separators + packages.forEach((p) => { + p.path = fwdSlashPath(p.path); + if (p.exclude) + for (let i = 0; i < p.exclude.length; i++) p.exclude[i] = fwdSlashPath(p.exclude[i]); + }); + super(...packages); - this.resolved = new Map(packages.map(({ path }) => [path, resolve(root, path)])); + this.resolved = new Map(packages.map(({ path }) => [path, fwdSlashPath(resolve(root, path))])); } private resolved: Map; private resolve(path: string) { - return this.resolved.get(path) ?? resolve(this.root, path); + return this.resolved.get(path) ?? fwdSlashPath(resolve(this.root, path)); } match(path: string): Package | undefined { if (path.startsWith("file:")) path = fileURLToPath(path); - const pkg = this.find((pkg) => path.startsWith(this.resolve(pkg.path))); - return pkg?.exclude?.find((ex) => path.includes(ex)) ? undefined : pkg; + + // Make sure passed path is forward slashed + const fixedPath = fwdSlashPath(path); + + const pkg = this.find((pkg) => fixedPath.startsWith(this.resolve(pkg.path))); + return pkg?.exclude?.find((ex) => fixedPath.includes(ex)) ? undefined : pkg; } } diff --git a/src/Recording.ts b/src/Recording.ts index 484f2130..59a8e392 100644 --- a/src/Recording.ts +++ b/src/Recording.ts @@ -217,7 +217,8 @@ function makeAppMapFilename(name: string): string { return name + ".appmap.json"; } +const charsToQuote = process.platform == "win32" ? /[/\\:<>*"|?]/g : /[/\\]/g; function quotePathSegment(value: string): string { // note replacing spaces isn't strictly necessary improves UX - return value.replaceAll(/[/\\]/g, "-").replaceAll(" ", "_"); + return value.replaceAll(charsToQuote, "-").replaceAll(" ", "_"); } diff --git a/src/__tests__/config.test.ts b/src/__tests__/config.test.ts index 77845112..2fc23d64 100644 --- a/src/__tests__/config.test.ts +++ b/src/__tests__/config.test.ts @@ -8,6 +8,7 @@ import YAML from "yaml"; import PackageMatcher from "../PackageMatcher"; import { Config } from "../config"; +import { fixAbsPath } from "../hooks/__tests__/fixAbsPath"; tmp.setGracefulCleanup(); @@ -98,10 +99,12 @@ describe(Config, () => { describe(PackageMatcher, () => { it("matches packages", () => { const pkg = { path: ".", exclude: ["node_modules", ".yarn"] }; - const matcher = new PackageMatcher("/test/app", [pkg]); - expect(matcher.match("/test/app/lib/foo.js")).toEqual(pkg); - expect(matcher.match("/other/app/lib/foo.js")).toBeUndefined(); - expect(matcher.match("/test/app/node_modules/lib/foo.js")).toBeUndefined(); - expect(matcher.match("/test/app/.yarn/lib/foo.js")).toBeUndefined(); + const matcher = new PackageMatcher(fixAbsPath("/test/app"), [pkg]); + expect(matcher.match(fixAbsPath("/test/app/lib/foo.js"))).toEqual(pkg); + if (process.platform == "win32") + expect(matcher.match(fixAbsPath("\\test\\app\\lib\\foo.js"))).toEqual(pkg); + expect(matcher.match(fixAbsPath("/other/app/lib/foo.js"))).toBeUndefined(); + expect(matcher.match(fixAbsPath("/test/app/node_modules/lib/foo.js"))).toBeUndefined(); + expect(matcher.match(fixAbsPath("/test/app/.yarn/lib/foo.js"))).toBeUndefined(); }); }); diff --git a/src/bin.ts b/src/bin.ts index b2338903..11a333c8 100644 --- a/src/bin.ts +++ b/src/bin.ts @@ -3,6 +3,7 @@ import { ChildProcess, spawn } from "node:child_process"; import { accessSync, readFileSync, writeFileSync } from "node:fs"; import { dirname, join, resolve } from "node:path"; import { kill, pid } from "node:process"; +import { pathToFileURL } from "node:url"; import stripJsonComments from "strip-json-comments"; import YAML from "yaml"; @@ -14,7 +15,10 @@ import forwardSignals from "./util/forwardSignals"; import { readPkgUp } from "./util/readPkgUp"; const registerPath = resolve(__dirname, "../dist/register.js"); -const loaderPath = resolve(__dirname, "../dist/loader.js"); +// We need to convert c: to file:// in Windows because: +// "Error: Only URLs with a scheme in: file, data, and node are supported by the default ESM loader. +// On Windows, absolute paths must be valid file:// URLs. Received protocol 'c:'" +const loaderPath = pathToFileURL(resolve(__dirname, "../dist/loader.js")).href; export function main() { const [cmd, ...args] = process.argv.slice(2); @@ -46,7 +50,14 @@ export function main() { } } else { // it's a command, spawn it - child = spawn(cmd, args, { stdio: "inherit" }); + + // We need to give shell: true in Windows because we get "Error: spawn yarn ENOENT" + // for example when the cmd is yarn. Looks like it needs the full path of the + // executable otherwise. + // Related articles: + // - https://stackoverflow.com/questions/37459717/error-spawn-enoent-on-windows + // - https://github.com/nodejs/node/issues/7367#issuecomment-238594729 + child = spawn(cmd, args, { stdio: "inherit", shell: process.platform == "win32" }); } forwardSignals(child); diff --git a/src/classMap.ts b/src/classMap.ts index ac0ecf3b..24bee6a9 100644 --- a/src/classMap.ts +++ b/src/classMap.ts @@ -1,5 +1,4 @@ import assert from "node:assert"; -import { sep } from "node:path"; import type AppMap from "./AppMap"; import type { FunctionInfo, SourceLocation } from "./registry"; @@ -13,7 +12,8 @@ export function makeClassMap(funs: Iterable): AppMap.ClassMap { // sorting isn't strictly necessary, but it provides for a stable output for (const fun of sortFunctions(funs)) { if (!fun.location) continue; - const pkgs = fun.location.path.replace(/\..+$/, "").split(sep).reverse(); + // fun.location can contain "/" as separator even in Windows + const pkgs = fun.location.path.replace(/\..+$/, "").split(/[/\\]/).reverse(); if (pkgs.length > 1) pkgs.shift(); // remove the file name (e.g. "foo.js") let [tree, classes]: FNode = [root, {}]; diff --git a/src/config.ts b/src/config.ts index daa08fe6..9c074ebc 100644 --- a/src/config.ts +++ b/src/config.ts @@ -32,7 +32,7 @@ export class Config { const config = readConfigFile(this.configPath); this.default = !config; - this.relativeAppmapDir = config?.appmap_dir ?? join("tmp", "appmap"); + this.relativeAppmapDir = config?.appmap_dir ?? "tmp/appmap"; this.appName = config?.name ?? targetPackage()?.name ?? basename(root); diff --git a/src/hooks/__tests__/fixAbsPath.ts b/src/hooks/__tests__/fixAbsPath.ts new file mode 100644 index 00000000..c044315d --- /dev/null +++ b/src/hooks/__tests__/fixAbsPath.ts @@ -0,0 +1,6 @@ +// Add drive parts for absolute paths in Windows. +export function fixAbsPath(pathOrFileUrl: string) { + if (process.platform != "win32") return pathOrFileUrl; + if (pathOrFileUrl.startsWith("/") || pathOrFileUrl.startsWith("\\")) return "F:" + pathOrFileUrl; + return pathOrFileUrl.replace("file:///", "file:///F:/"); +} diff --git a/src/hooks/__tests__/instrument.test.ts b/src/hooks/__tests__/instrument.test.ts index 23a894d2..e05c9dbc 100644 --- a/src/hooks/__tests__/instrument.test.ts +++ b/src/hooks/__tests__/instrument.test.ts @@ -2,23 +2,25 @@ import { full as walk } from "acorn-walk"; import { ESTree, parse } from "meriyah"; import config from "../../config"; +import { fixAbsPath } from "./fixAbsPath"; +import * as instrument from "../instrument"; import PackageMatcher from "../../PackageMatcher"; import * as registry from "../../registry"; -import * as instrument from "../instrument"; describe(instrument.shouldInstrument, () => { - jest.replaceProperty(config, "root", "/test"); + jest.replaceProperty(config, "root", fixAbsPath("/test")); jest.replaceProperty( config, "packages", - new PackageMatcher("/test", [{ path: ".", exclude: ["node_modules"] }]), + new PackageMatcher(fixAbsPath("/test"), [{ path: ".", exclude: ["node_modules"] }]), ); + test.each([ ["node:test", false], - ["file:///test/test.json", false], - ["file:///var/test.js", false], - ["file:///test/test.js", true], - ["file:///test/node_modules/test.js", false], + [fixAbsPath("file:///test/test.json"), false], + [fixAbsPath("file:///var/test.js"), false], + [fixAbsPath("file:///test/test.js"), true], + [fixAbsPath("file:///test/node_modules/test.js"), false], ])("%s", (url, expected) => expect(instrument.shouldInstrument(new URL(url))).toBe(expected)); }); diff --git a/src/hooks/__tests__/jest.test.ts b/src/hooks/__tests__/jest.test.ts index 480997b6..4819f26e 100644 --- a/src/hooks/__tests__/jest.test.ts +++ b/src/hooks/__tests__/jest.test.ts @@ -1,4 +1,6 @@ import { parse } from "meriyah"; + +import { fixAbsPath } from "./fixAbsPath"; import * as jestHook from "../jest"; import transform from "../../transform"; @@ -53,9 +55,11 @@ describe(jestHook.patchRuntime, () => { describe(jestHook.transformJest, () => { it("pushes jest transformed code through appmap hooks", () => { jest.mocked(transform).mockReturnValue("transformed test code"); - const result = jestHook.transformJest.call(undefined, () => "test code", ["/test/test.js"]); + const result = jestHook.transformJest.call(undefined, () => "test code", [ + fixAbsPath("/test/test.js"), + ]); expect(result).toBe("transformed test code"); - expect(transform).toBeCalledWith("test code", new URL("file:///test/test.js")); + expect(transform).toBeCalledWith("test code", new URL(fixAbsPath("file:///test/test.js"))); }); }); diff --git a/src/hooks/vitest.ts b/src/hooks/vitest.ts index fcc49337..fa6f8065 100644 --- a/src/hooks/vitest.ts +++ b/src/hooks/vitest.ts @@ -114,7 +114,7 @@ function patchRunTest(fd: ESTree.FunctionDeclaration) { // Statement: return await import(".../vitest.js").wrapRunTest(function runTest(...) {...}, arguments); ret( call_( - member(awaitImport(__filename), identifier(wrapRunTest.name)), + member(awaitImport(pathToFileURL(__filename).href), identifier(wrapRunTest.name)), { ...fd, type: "FunctionExpression" }, args_, ), @@ -137,7 +137,7 @@ function patchRunModule(md: ESTree.MethodDefinition) { const transformCodeStatement = assignment( identifier("transformed"), call_( - member(awaitImport(__filename), identifier(transformCode.name)), + member(awaitImport(pathToFileURL(__filename).href), identifier(transformCode.name)), identifier("transformed"), memberId("context", "__filename"), ), diff --git a/src/util/__tests__/commonPathPrefix.test.ts b/src/util/__tests__/commonPathPrefix.test.ts index c60c0adb..df03cae8 100644 --- a/src/util/__tests__/commonPathPrefix.test.ts +++ b/src/util/__tests__/commonPathPrefix.test.ts @@ -8,4 +8,7 @@ describe(commonPathPrefix, () => { [["/foo/bar", "/foo/barbara"], "/foo/"], [["/foo/bar", "/other/dir"], "/"], ])("%j => %s", (paths, expected) => expect(commonPathPrefix(paths)).toBe(expected)); + + if (process.platform == "win32") + expect(commonPathPrefix(["c:\\foo\\bar", "C:\\Foo\\Baz"])).toBe("c:\\foo\\"); }); diff --git a/src/util/commonPathPrefix.ts b/src/util/commonPathPrefix.ts index 27bcf7d5..6761006b 100644 --- a/src/util/commonPathPrefix.ts +++ b/src/util/commonPathPrefix.ts @@ -4,10 +4,17 @@ export default function commonPathPrefix(paths: string[]): string { const [first, ...others] = paths; if (!first) return ""; + const isEqual = (a: string, b: string) => { + return process.platform == "win32" + ? a.localeCompare(b, "en", { sensitivity: "base" }) == 0 + : a == b; + }; + let prefixLen = 0; for (let i = 0; i < first.length; i++) - if (!others.every((path) => path[i] === first[i])) break; - else if (first[i] === sep) prefixLen = i; + if (!others.every((path) => isEqual(path[i], first[i]))) break; + // We occasionally convert back slash to forward slash even in Windows + else if (first[i] === sep || first[i] === "/") prefixLen = i; return first.slice(0, prefixLen + 1); } diff --git a/src/util/fwdSlashPath.ts b/src/util/fwdSlashPath.ts new file mode 100644 index 00000000..7318f827 --- /dev/null +++ b/src/util/fwdSlashPath.ts @@ -0,0 +1,4 @@ +export default function fwdSlashPath(path: string): string { + if (process.platform != "win32") return path; + return path?.replaceAll("\\", "/"); +} diff --git a/test/__snapshots__/next.test.ts.snap b/test/__snapshots__/next.test.ts.snap index 5355dd19..a59d75ad 100644 --- a/test/__snapshots__/next.test.ts.snap +++ b/test/__snapshots__/next.test.ts.snap @@ -112,7 +112,7 @@ exports[`mapping a Next.js appmap 1`] = ` }, "version": "1.12", }, - "./tmp/appmap/requests/ -about.appmap.json": { + "./tmp/appmap/requests/_-about.appmap.json": { "classMap": [ { "children": [ @@ -129,7 +129,7 @@ exports[`mapping a Next.js appmap 1`] = ` "type": "class", }, ], - "name": "about", + "name": "pages", "type": "package", }, ], diff --git a/test/helpers.ts b/test/helpers.ts index f0797bea..cabb9eff 100644 --- a/test/helpers.ts +++ b/test/helpers.ts @@ -8,6 +8,7 @@ import caller from "caller"; import { globSync } from "fast-glob"; import type AppMap from "../src/AppMap"; +import fwdSlashPath from "../src/util/fwdSlashPath"; const binPath = resolve(__dirname, "../bin/appmap-node.js"); @@ -29,13 +30,13 @@ export function spawnAppmapNode(...args: string[]): ChildProcessWithoutNullStrea return result; } -let target = cwd(); +let target = fwdSlashPath(cwd()); export function testDir(path: string) { - target = resolve(path); + target = fwdSlashPath(resolve(path)); } -beforeEach(() => rmSync(resolveTarget("tmp"), { recursive: true, force: true })); +beforeEach(() => rmSync(resolveTarget("tmp"), { recursive: true, force: true, maxRetries: 3 })); export function resolveTarget(...path: string[]): string { return resolve(target, ...path); @@ -55,7 +56,7 @@ type AppMap = object & Record<"events", unknown>; export function readAppmap(path?: string): AppMap.AppMap { if (!path) { - const files = globSync(resolve(target, "tmp/**/*.appmap.json")); + const files = globSync(fwdSlashPath(resolve(target, "tmp/**/*.appmap.json"))); expect(files.length).toBe(1); [path] = files; } @@ -80,7 +81,7 @@ export function fixAppmap(map: unknown): AppMap.AppMap { } export function readAppmaps(): Record { - const files = globSync(resolve(target, "tmp/**/*.appmap.json")); + const files = globSync(fwdSlashPath(resolve(target, "tmp/**/*.appmap.json"))); const maps = files.map<[string, AppMap.AppMap]>((path) => [fixPath(path), readAppmap(path)]); return Object.fromEntries(maps); } @@ -128,13 +129,13 @@ beforeEach(() => (timestampId = 0)); function fixTimeStamps(str: string): string { return str.replaceAll( - /\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z/g, - (ts) => (timestamps[ts] ||= ``), + /\d{4}-\d{2}-\d{2}T\d{2}[:-]\d{2}[:-]\d{2}\.\d{3}Z/g, + (ts) => (timestamps[ts.replaceAll(":", "-")] ||= ``), ); } function fixPath(path: string): string { - return fixTimeStamps(path.replace(target, ".")); + return fixTimeStamps(fwdSlashPath(path).replace(target, ".")); } function fixClassMap(classMap: unknown[]) { diff --git a/test/httpServer.test.ts b/test/httpServer.test.ts index 01c50013..31bd524c 100644 --- a/test/httpServer.test.ts +++ b/test/httpServer.test.ts @@ -7,6 +7,8 @@ integrationTest("mapping Express.js requests", async () => { expect.assertions(1); const server = await spawnServer("express.js"); await makeRequests(); + // Wait for the last request to finish + await awaitStdoutOnData(server, "api-bar.appmap.json"); await killServer(server); expect(readAppmaps()).toMatchSnapshot(); }); @@ -15,6 +17,8 @@ integrationTest("mapping node:http requests", async () => { expect.assertions(1); const server = await spawnServer("vanilla.js"); await makeRequests(); + // Wait for the last request to finish + await awaitStdoutOnData(server, "api-bar.appmap.json"); await killServer(server); expect(readAppmaps()).toMatchSnapshot(); }); @@ -39,6 +43,14 @@ integrationTest("mapping node:http requests with remote recording", async () => await killServer(server); }); +async function awaitStdoutOnData(server: ChildProcessWithoutNullStreams, searchString: string) { + await new Promise((r) => + server.stdout.on("data", (chunk: Buffer) => { + if (chunk.toString().includes(searchString)) r(); + }), + ); +} + async function makeRequests() { await makeRequest(""); await makeRequest("/nonexistent"); @@ -58,11 +70,7 @@ async function makeRequests() { async function spawnServer(script: string) { const server = spawnAppmapNode(script); - await new Promise((r) => - server.stdout.on("data", (chunk: Buffer) => { - if (chunk.toString().includes("listening")) r(); - }), - ); + await awaitStdoutOnData(server, "listening"); return server; } diff --git a/test/next.test.ts b/test/next.test.ts index 98f41ddb..840a9b7e 100644 --- a/test/next.test.ts +++ b/test/next.test.ts @@ -1,9 +1,15 @@ -import { IncomingMessage, request } from "http"; +import { IncomingMessage, request } from "node:http"; import { integrationTest, readAppmaps, spawnAppmapNode } from "./helpers"; async function spawnNextJsApp() { - const app = spawnAppmapNode("node_modules/next/dist/bin/next", "dev"); + // On Windows, we give "node" argument explicitly because next is a js file with + // shebang (#!/usr/bin/env node) which does not work on Windows. + const app = + process.platform == "win32" + ? spawnAppmapNode("node", "node_modules\\next\\dist\\bin\\next", "dev") + : spawnAppmapNode("node_modules/next/dist/bin/next", "dev"); + await new Promise((r) => { const onData = (chunk: Buffer) => { console.log("CHUNK", chunk.toString()); @@ -21,13 +27,19 @@ integrationTest( "mapping a Next.js appmap", async () => { const app = await spawnNextJsApp(); - await makeRequest("/hello"); + const response = await makeRequest("/hello"); + console.log("Response", response); + const pid = parseInt((JSON.parse(response) as unknown as { pid: string }).pid); + await makeRequest("/about"); app.kill("SIGINT"); await new Promise((r) => app.once("exit", r)); - expect(readAppmaps()).toMatchSnapshot(); + + // We need to kill the next process explicitly on Windows + // because it's spawn-ed with "shell: true" and app is the shell process. + if (process.platform == "win32") process.kill(pid, "SIGINT"); }, 20000, ); @@ -38,6 +50,7 @@ async function makeRequest(path: string, method = "GET") { const req = request(url, { method }, resolve).once("error", reject); req.end(); }); + const chunks: Buffer[] = []; for await (const chunk of await response) chunks.push(chunk as Buffer); return Buffer.concat(chunks).toString(); diff --git a/test/next/app/hello/route.ts b/test/next/app/hello/route.ts index a320af9b..54ea31d2 100644 --- a/test/next/app/hello/route.ts +++ b/test/next/app/hello/route.ts @@ -1,5 +1,5 @@ import { NextResponse } from "next/server"; export function GET(): NextResponse { - return NextResponse.json({ message: "Hello from appmap-node next.js test project" }); + return NextResponse.json({ pid: process.pid }); } diff --git a/test/simple.test.ts b/test/simple.test.ts index c2b1c10a..7dbe05cc 100644 --- a/test/simple.test.ts +++ b/test/simple.test.ts @@ -13,6 +13,8 @@ import { testDir, } from "./helpers"; +const integrationTestSkipOnWindows = process.platform == "win32" ? test.skip : integrationTest; + integrationTest("mapping a simple script", () => { expect(runAppmapNode("index.js").status).toBe(0); expect(readAppmap()).toMatchSnapshot(); @@ -28,7 +30,7 @@ integrationTest("mapping js class methods and constructors containing super keyw expect(readAppmap()).toMatchSnapshot(); }); -integrationTest("forwarding signals to the child", async () => { +integrationTestSkipOnWindows("forwarding signals to the child", async () => { const daemon = spawnAppmapNode("daemon.mjs"); await new Promise((r) => daemon.stdout.on("data", (chunk: Buffer) => chunk.toString().includes("starting") && r()), @@ -51,7 +53,7 @@ integrationTest("mapping a custom Error class with a message property", () => { expect(readAppmap()).toMatchSnapshot(); }); -integrationTest("finish signal is handled", async () => { +integrationTestSkipOnWindows("finish signal is handled", async () => { const server = spawnAppmapNode("server.mjs"); await new Promise((r) => server.stdout.on("data", (chunk: Buffer) => chunk.toString().includes("starting") && r()), @@ -67,14 +69,21 @@ integrationTest("finish signal is handled", async () => { }); integrationTest("mapping an extensionless CommonJS file", () => { - expect(runAppmapNode("./extensionless").status).toBe(0); + const args = ["./extensionless"]; + if (process.platform == "win32") args.unshift("node"); + expect(runAppmapNode(...args).status).toBe(0); expect(readAppmap()).toMatchSnapshot(); }); integrationTest("running a script after changing the current directory", () => { // Need to make sure the appmap "root" stays the same after // appmap-node is run, even if the current directory changes. - expect(runAppmapNode("bash", "-c", "cd subproject; node index.js").status).toBe(0); + const args = + process.platform == "win32" + ? ["cd subproject & node subproject.js"] + : ["bash", "-c", "cd subproject; node subproject.js"]; + + expect(runAppmapNode(...args).status).toBe(0); expect(readAppmap()).toBeDefined(); }); diff --git a/test/simple/subproject/index.js b/test/simple/subproject/index.js deleted file mode 120000 index e234193f..00000000 --- a/test/simple/subproject/index.js +++ /dev/null @@ -1 +0,0 @@ -../index.js \ No newline at end of file diff --git a/test/simple/subproject/subproject.js b/test/simple/subproject/subproject.js new file mode 100644 index 00000000..a8612bd8 --- /dev/null +++ b/test/simple/subproject/subproject.js @@ -0,0 +1,38 @@ +/* eslint-disable @typescript-eslint/no-var-requires */ +const { setTimeout } = require("timers/promises"); + +function foo(x) { + skipped(); + return x * 2; +} + +async function promised(ok = true) { + await setTimeout(10); + if (!ok) throws(); + return "promised return"; +} + +function immediatePromise() { + return Promise.resolve("immediate"); +} + +function throws() { + console.log("going to throw"); + throw new Error("throws intentionally"); +} + +function skipped() { + console.log("skipped"); +} + +try { + throws(); + console.log(foo(43)); +} catch { + console.log(foo(44)); +} + +console.log(foo(42)); +promised().then(console.log); +promised(false).catch(console.log); +immediatePromise().then(console.log); diff --git a/test/smoketest.mjs b/test/smoketest.mjs index 4c0fc3e0..28d225e8 100755 --- a/test/smoketest.mjs +++ b/test/smoketest.mjs @@ -42,6 +42,9 @@ const files = glob.globSync("tmp/**/*.appmap.json"); assert(files.length === 1); function runCommand(command, ...args) { - const { status } = spawnSync(command, args, { stdio: "inherit" }); + const { status } = spawnSync(command, args, { + stdio: "inherit", + shell: process.platform == "win32", + }); assert(status === 0); }